summaryrefslogtreecommitdiff
path: root/toolkit/components
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components')
-rw-r--r--toolkit/components/.eslintrc.js11
-rw-r--r--toolkit/components/aboutcache/content/aboutCache.js44
-rw-r--r--toolkit/components/aboutcache/jar.mn6
-rw-r--r--toolkit/components/aboutcache/moz.build7
-rw-r--r--toolkit/components/aboutcheckerboard/content/aboutCheckerboard.css49
-rw-r--r--toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js276
-rw-r--r--toolkit/components/aboutcheckerboard/content/aboutCheckerboard.xhtml55
-rw-r--r--toolkit/components/aboutcheckerboard/jar.mn8
-rw-r--r--toolkit/components/aboutcheckerboard/moz.build10
-rw-r--r--toolkit/components/aboutmemory/content/aboutCompartments.xhtml16
-rw-r--r--toolkit/components/aboutmemory/content/aboutMemory.css154
-rw-r--r--toolkit/components/aboutmemory/content/aboutMemory.js2042
-rw-r--r--toolkit/components/aboutmemory/content/aboutMemory.xhtml15
-rw-r--r--toolkit/components/aboutmemory/jar.mn8
-rw-r--r--toolkit/components/aboutmemory/moz.build12
-rw-r--r--toolkit/components/aboutmemory/tests/.eslintrc.js7
-rw-r--r--toolkit/components/aboutmemory/tests/chrome.ini28
-rw-r--r--toolkit/components/aboutmemory/tests/crash-dump-diff1.json11
-rw-r--r--toolkit/components/aboutmemory/tests/crash-dump-diff2.json11
-rw-r--r--toolkit/components/aboutmemory/tests/crash-dump-good.json14
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-bad.json3
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-diff1.json45
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-diff2.json44
-rw-r--r--toolkit/components/aboutmemory/tests/memory-reports-good.json29
-rw-r--r--toolkit/components/aboutmemory/tests/remote.xul12
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory.xul602
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory2.xul423
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory3.xul515
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory4.xul179
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory5.xul167
-rw-r--r--toolkit/components/aboutmemory/tests/test_aboutmemory6.xul88
-rw-r--r--toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul98
-rw-r--r--toolkit/components/aboutmemory/tests/test_memoryReporters.xul424
-rw-r--r--toolkit/components/aboutmemory/tests/test_memoryReporters2.xul108
-rw-r--r--toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul54
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.js1077
-rw-r--r--toolkit/components/aboutperformance/content/aboutPerformance.xhtml188
-rw-r--r--toolkit/components/aboutperformance/jar.mn7
-rw-r--r--toolkit/components/aboutperformance/moz.build9
-rw-r--r--toolkit/components/aboutperformance/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser.ini8
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js300
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments.html20
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html12
-rw-r--r--toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js29
-rw-r--r--toolkit/components/aboutperformance/tests/browser/head.js52
-rw-r--r--toolkit/components/addoncompat/CompatWarning.jsm107
-rw-r--r--toolkit/components/addoncompat/Prefetcher.jsm557
-rw-r--r--toolkit/components/addoncompat/RemoteAddonsChild.jsm576
-rw-r--r--toolkit/components/addoncompat/RemoteAddonsParent.jsm1080
-rw-r--r--toolkit/components/addoncompat/ShimWaiver.jsm15
-rw-r--r--toolkit/components/addoncompat/addoncompat.manifest4
-rw-r--r--toolkit/components/addoncompat/defaultShims.js39
-rw-r--r--toolkit/components/addoncompat/moz.build21
-rw-r--r--toolkit/components/addoncompat/multiprocessShims.js182
-rw-r--r--toolkit/components/addoncompat/tests/addon/bootstrap.js653
-rw-r--r--toolkit/components/addoncompat/tests/addon/chrome.manifest1
-rw-r--r--toolkit/components/addoncompat/tests/addon/content/page.html2
-rw-r--r--toolkit/components/addoncompat/tests/addon/install.rdf37
-rw-r--r--toolkit/components/addoncompat/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/addoncompat/tests/browser/addon.xpibin0 -> 10761 bytes
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser.ini9
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser_addonShims.js67
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html17
-rw-r--r--toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html16
-rw-r--r--toolkit/components/addoncompat/tests/browser/compat-addon.xpibin0 -> 5692 bytes
-rw-r--r--toolkit/components/addoncompat/tests/compat-addon/bootstrap.js99
-rw-r--r--toolkit/components/addoncompat/tests/compat-addon/install.rdf37
-rw-r--r--toolkit/components/addoncompat/tests/moz.build7
-rw-r--r--toolkit/components/alerts/AlertNotification.cpp361
-rw-r--r--toolkit/components/alerts/AlertNotification.h81
-rw-r--r--toolkit/components/alerts/AlertNotificationIPCSerializer.h122
-rw-r--r--toolkit/components/alerts/jar.mn8
-rw-r--r--toolkit/components/alerts/moz.build38
-rw-r--r--toolkit/components/alerts/nsAlertsService.cpp320
-rw-r--r--toolkit/components/alerts/nsAlertsService.h48
-rw-r--r--toolkit/components/alerts/nsAlertsUtils.cpp43
-rw-r--r--toolkit/components/alerts/nsAlertsUtils.h32
-rw-r--r--toolkit/components/alerts/nsIAlertsService.idl259
-rw-r--r--toolkit/components/alerts/nsXULAlerts.cpp398
-rw-r--r--toolkit/components/alerts/nsXULAlerts.h84
-rw-r--r--toolkit/components/alerts/resources/content/alert.css34
-rw-r--r--toolkit/components/alerts/resources/content/alert.js332
-rw-r--r--toolkit/components/alerts/resources/content/alert.xul67
-rw-r--r--toolkit/components/alerts/test/.eslintrc.js7
-rw-r--r--toolkit/components/alerts/test/image.gifbin0 -> 60901 bytes
-rw-r--r--toolkit/components/alerts/test/image.pngbin0 -> 2531 bytes
-rw-r--r--toolkit/components/alerts/test/image_server.sjs82
-rw-r--r--toolkit/components/alerts/test/mochitest.ini16
-rw-r--r--toolkit/components/alerts/test/test_alerts.html89
-rw-r--r--toolkit/components/alerts/test/test_alerts_noobserve.html96
-rw-r--r--toolkit/components/alerts/test/test_alerts_requireinteraction.html168
-rw-r--r--toolkit/components/alerts/test/test_image.html118
-rw-r--r--toolkit/components/alerts/test/test_multiple_alerts.html103
-rw-r--r--toolkit/components/alerts/test/test_principal.html122
-rw-r--r--toolkit/components/apppicker/content/appPicker.js210
-rw-r--r--toolkit/components/apppicker/content/appPicker.xul40
-rw-r--r--toolkit/components/apppicker/jar.mn8
-rw-r--r--toolkit/components/apppicker/moz.build7
-rw-r--r--toolkit/components/asyncshutdown/AsyncShutdown.jsm1041
-rw-r--r--toolkit/components/asyncshutdown/moz.build25
-rw-r--r--toolkit/components/asyncshutdown/nsAsyncShutdown.js277
-rw-r--r--toolkit/components/asyncshutdown/nsAsyncShutdown.manifest2
-rw-r--r--toolkit/components/asyncshutdown/nsIAsyncShutdown.idl220
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/head.js174
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js194
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js96
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js88
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini8
-rw-r--r--toolkit/components/autocomplete/moz.build28
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteController.cpp2087
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteController.h176
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp292
-rw-r--r--toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h44
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteController.idl173
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteInput.idl181
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompletePopup.idl71
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteResult.idl89
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteSearch.idl74
-rw-r--r--toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl116
-rw-r--r--toolkit/components/autocomplete/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/autocomplete/tests/unit/head_autocomplete.js206
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_330578.js45
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_378079.js285
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_393191.js272
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_440866.js285
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_463023.js12
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_660156.js101
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js276
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js45
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_autofillSelectedPopupIndex.js78
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_badDefaultIndex.js96
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js63
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js48
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js119
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js107
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js104
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js66
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_immediate_search.js157
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_insertMatchAt.js14
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_previousResult.js280
-rw-r--r--toolkit/components/autocomplete/tests/unit/test_stopSearch.js187
-rw-r--r--toolkit/components/autocomplete/tests/unit/xpcshell.ini24
-rw-r--r--toolkit/components/build/moz.build35
-rw-r--r--toolkit/components/build/nsToolkitCompsCID.h188
-rw-r--r--toolkit/components/build/nsToolkitCompsModule.cpp246
-rw-r--r--toolkit/components/captivedetect/CaptivePortalDetectComponents.manifest2
-rw-r--r--toolkit/components/captivedetect/captivedetect.js476
-rw-r--r--toolkit/components/captivedetect/moz.build19
-rw-r--r--toolkit/components/captivedetect/nsICaptivePortalDetector.idl53
-rw-r--r--toolkit/components/captivedetect/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/captivedetect/test/unit/head_setprefs.js49
-rw-r--r--toolkit/components/captivedetect/test/unit/test_abort.js53
-rw-r--r--toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js66
-rw-r--r--toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js72
-rw-r--r--toolkit/components/captivedetect/test/unit/test_abort_pending_request.js71
-rw-r--r--toolkit/components/captivedetect/test/unit/test_captive_portal_found.js67
-rw-r--r--toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js74
-rw-r--r--toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js52
-rw-r--r--toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js49
-rw-r--r--toolkit/components/captivedetect/test/unit/test_multiple_requests.js83
-rw-r--r--toolkit/components/captivedetect/test/unit/test_user_cancel.js54
-rw-r--r--toolkit/components/captivedetect/test/unit/xpcshell.ini15
-rw-r--r--toolkit/components/commandlines/moz.build30
-rw-r--r--toolkit/components/commandlines/nsCommandLine.cpp660
-rw-r--r--toolkit/components/commandlines/nsICommandLine.idl141
-rw-r--r--toolkit/components/commandlines/nsICommandLineHandler.idl53
-rw-r--r--toolkit/components/commandlines/nsICommandLineRunner.idl55
-rw-r--r--toolkit/components/commandlines/nsICommandLineValidator.idl38
-rw-r--r--toolkit/components/commandlines/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/commandlines/test/unit/data/test_bug410156.desktop7
-rw-r--r--toolkit/components/commandlines/test/unit/data/test_bug410156.url9
-rw-r--r--toolkit/components/commandlines/test/unit/test_bug666224.js6
-rw-r--r--toolkit/components/commandlines/test/unit/test_classinfo.js9
-rw-r--r--toolkit/components/commandlines/test/unit/xpcshell.ini10
-rw-r--r--toolkit/components/commandlines/test/unit_unix/.eslintrc.js7
-rw-r--r--toolkit/components/commandlines/test/unit_unix/test_bug410156.js11
-rw-r--r--toolkit/components/commandlines/test/unit_unix/xpcshell.ini9
-rw-r--r--toolkit/components/commandlines/test/unit_win/.eslintrc.js7
-rw-r--r--toolkit/components/commandlines/test/unit_win/test_bug410156.js11
-rw-r--r--toolkit/components/commandlines/test/unit_win/xpcshell.ini8
-rw-r--r--toolkit/components/contentprefs/ContentPrefInstance.jsm75
-rw-r--r--toolkit/components/contentprefs/ContentPrefService2.jsm885
-rw-r--r--toolkit/components/contentprefs/ContentPrefServiceChild.jsm182
-rw-r--r--toolkit/components/contentprefs/ContentPrefServiceParent.jsm137
-rw-r--r--toolkit/components/contentprefs/ContentPrefStore.jsm123
-rw-r--r--toolkit/components/contentprefs/ContentPrefUtils.jsm70
-rw-r--r--toolkit/components/contentprefs/moz.build31
-rw-r--r--toolkit/components/contentprefs/nsContentPrefService.js1332
-rw-r--r--toolkit/components/contentprefs/nsContentPrefService.manifest5
-rw-r--r--toolkit/components/contentprefs/tests/mochitest/.eslintrc.js7
-rw-r--r--toolkit/components/contentprefs/tests/mochitest/mochitest.ini4
-rw-r--r--toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html311
-rw-r--r--toolkit/components/contentprefs/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/contentprefs/tests/unit/head_contentPrefs.js162
-rw-r--r--toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js6
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_bug248970.js42
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_bug503971.js35
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_bug679784.js103
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_contentPrefs.js463
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js244
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js34
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_stringGroups.js128
-rw-r--r--toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js52
-rw-r--r--toolkit/components/contentprefs/tests/unit/xpcshell.ini12
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js7
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm69
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/head.js401
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js20
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js95
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js186
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js68
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js82
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_observers.js178
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_remove.js222
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js87
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js111
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js199
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js96
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_service.js12
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js206
-rw-r--r--toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini19
-rw-r--r--toolkit/components/contextualidentity/ContextualIdentityService.jsm344
-rw-r--r--toolkit/components/contextualidentity/moz.build11
-rw-r--r--toolkit/components/contextualidentity/tests/unit/test_basic.js67
-rw-r--r--toolkit/components/contextualidentity/tests/unit/xpcshell.ini3
-rw-r--r--toolkit/components/cookie/content/cookieAcceptDialog.js185
-rw-r--r--toolkit/components/cookie/content/cookieAcceptDialog.xul118
-rw-r--r--toolkit/components/cookie/jar.mn10
-rw-r--r--toolkit/components/cookie/moz.build10
-rw-r--r--toolkit/components/crashes/CrashManager.jsm1351
-rw-r--r--toolkit/components/crashes/CrashManagerTest.jsm186
-rw-r--r--toolkit/components/crashes/CrashService.js71
-rw-r--r--toolkit/components/crashes/CrashService.manifest3
-rw-r--r--toolkit/components/crashes/docs/crash-events.rst176
-rw-r--r--toolkit/components/crashes/docs/index.rst24
-rw-r--r--toolkit/components/crashes/moz.build31
-rw-r--r--toolkit/components/crashes/nsICrashService.idl30
-rw-r--r--toolkit/components/crashes/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/crashes/tests/xpcshell/test_crash_manager.js494
-rw-r--r--toolkit/components/crashes/tests/xpcshell/test_crash_service.js31
-rw-r--r--toolkit/components/crashes/tests/xpcshell/test_crash_store.js587
-rw-r--r--toolkit/components/crashes/tests/xpcshell/xpcshell.ini8
-rw-r--r--toolkit/components/crashmonitor/CrashMonitor.jsm224
-rw-r--r--toolkit/components/crashmonitor/crashmonitor.manifest3
-rw-r--r--toolkit/components/crashmonitor/moz.build16
-rw-r--r--toolkit/components/crashmonitor/nsCrashMonitor.js29
-rw-r--r--toolkit/components/crashmonitor/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/crashmonitor/test/unit/head.js22
-rw-r--r--toolkit/components/crashmonitor/test/unit/test_init.js17
-rw-r--r--toolkit/components/crashmonitor/test/unit/test_invalid_file.js22
-rw-r--r--toolkit/components/crashmonitor/test/unit/test_invalid_json.js18
-rw-r--r--toolkit/components/crashmonitor/test/unit/test_missing_file.js13
-rw-r--r--toolkit/components/crashmonitor/test/unit/test_register.js24
-rw-r--r--toolkit/components/crashmonitor/test/unit/test_valid_file.js20
-rw-r--r--toolkit/components/crashmonitor/test/unit/xpcshell.ini11
-rw-r--r--toolkit/components/ctypes/ctypes.cpp151
-rw-r--r--toolkit/components/ctypes/ctypes.h30
-rw-r--r--toolkit/components/ctypes/ctypes.jsm23
-rw-r--r--toolkit/components/ctypes/moz.build24
-rw-r--r--toolkit/components/ctypes/tests/chrome/.eslintrc.js7
-rw-r--r--toolkit/components/ctypes/tests/chrome/chrome.ini8
-rw-r--r--toolkit/components/ctypes/tests/chrome/ctypes_worker.js14
-rw-r--r--toolkit/components/ctypes/tests/chrome/test_ctypes.xul106
-rw-r--r--toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js100
-rw-r--r--toolkit/components/ctypes/tests/jsctypes-test-errno.cpp41
-rw-r--r--toolkit/components/ctypes/tests/jsctypes-test-errno.h21
-rw-r--r--toolkit/components/ctypes/tests/jsctypes-test-finalizer.cpp323
-rw-r--r--toolkit/components/ctypes/tests/jsctypes-test-finalizer.h57
-rw-r--r--toolkit/components/ctypes/tests/jsctypes-test.cpp394
-rw-r--r--toolkit/components/ctypes/tests/jsctypes-test.h197
-rw-r--r--toolkit/components/ctypes/tests/moz.build30
-rw-r--r--toolkit/components/ctypes/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/ctypes/tests/unit/head.js128
-rw-r--r--toolkit/components/ctypes/tests/unit/test_errno.js69
-rw-r--r--toolkit/components/ctypes/tests/unit/test_finalizer.js452
-rw-r--r--toolkit/components/ctypes/tests/unit/test_finalizer_shouldaccept.js174
-rw-r--r--toolkit/components/ctypes/tests/unit/test_finalizer_shouldfail.js176
-rw-r--r--toolkit/components/ctypes/tests/unit/test_jsctypes.js2808
-rw-r--r--toolkit/components/ctypes/tests/unit/xpcshell.ini13
-rw-r--r--toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp161
-rw-r--r--toolkit/components/diskspacewatcher/DiskSpaceWatcher.h32
-rw-r--r--toolkit/components/diskspacewatcher/moz.build23
-rw-r--r--toolkit/components/diskspacewatcher/nsIDiskSpaceWatcher.idl25
-rw-r--r--toolkit/components/downloads/ApplicationReputation.cpp1629
-rw-r--r--toolkit/components/downloads/ApplicationReputation.h55
-rw-r--r--toolkit/components/downloads/SQLFunctions.cpp146
-rw-r--r--toolkit/components/downloads/SQLFunctions.h46
-rw-r--r--toolkit/components/downloads/chromium/LICENSE27
-rw-r--r--toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.cc20037
-rw-r--r--toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.h21771
-rw-r--r--toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.proto839
-rwxr-xr-xtoolkit/components/downloads/generate_csd.sh33
-rw-r--r--toolkit/components/downloads/moz.build74
-rw-r--r--toolkit/components/downloads/nsDownloadManager.cpp3783
-rw-r--r--toolkit/components/downloads/nsDownloadManager.h454
-rw-r--r--toolkit/components/downloads/nsDownloadManagerUI.js107
-rw-r--r--toolkit/components/downloads/nsDownloadManagerUI.manifest2
-rw-r--r--toolkit/components/downloads/nsDownloadProxy.h179
-rw-r--r--toolkit/components/downloads/nsDownloadScanner.cpp728
-rw-r--r--toolkit/components/downloads/nsDownloadScanner.h121
-rw-r--r--toolkit/components/downloads/nsIApplicationReputation.idl122
-rw-r--r--toolkit/components/downloads/nsIDownload.idl175
-rw-r--r--toolkit/components/downloads/nsIDownloadManager.idl358
-rw-r--r--toolkit/components/downloads/nsIDownloadManagerUI.idl55
-rw-r--r--toolkit/components/downloads/nsIDownloadProgressListener.idl60
-rw-r--r--toolkit/components/downloads/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/downloads/test/unit/data/block_digest.chunk2
-rw-r--r--toolkit/components/downloads/test/unit/data/digest.chunk3
-rw-r--r--toolkit/components/downloads/test/unit/data/signed_win.exebin0 -> 61064 bytes
-rw-r--r--toolkit/components/downloads/test/unit/head_download_manager.js26
-rw-r--r--toolkit/components/downloads/test/unit/tail_download_manager.js23
-rw-r--r--toolkit/components/downloads/test/unit/test_app_rep.js342
-rw-r--r--toolkit/components/downloads/test/unit/test_app_rep_maclinux.js303
-rw-r--r--toolkit/components/downloads/test/unit/test_app_rep_windows.js434
-rw-r--r--toolkit/components/downloads/test/unit/xpcshell.ini14
-rw-r--r--toolkit/components/extensions/.eslintrc.js497
-rw-r--r--toolkit/components/extensions/Extension.jsm902
-rw-r--r--toolkit/components/extensions/ExtensionAPI.jsm81
-rw-r--r--toolkit/components/extensions/ExtensionChild.jsm1040
-rw-r--r--toolkit/components/extensions/ExtensionCommon.jsm680
-rw-r--r--toolkit/components/extensions/ExtensionContent.jsm1048
-rw-r--r--toolkit/components/extensions/ExtensionManagement.jsm321
-rw-r--r--toolkit/components/extensions/ExtensionParent.jsm551
-rw-r--r--toolkit/components/extensions/ExtensionStorage.jsm241
-rw-r--r--toolkit/components/extensions/ExtensionStorageSync.jsm848
-rw-r--r--toolkit/components/extensions/ExtensionTestCommon.jsm343
-rw-r--r--toolkit/components/extensions/ExtensionUtils.jsm1215
-rw-r--r--toolkit/components/extensions/ExtensionXPCShellUtils.jsm306
-rw-r--r--toolkit/components/extensions/LegacyExtensionsUtils.jsm250
-rw-r--r--toolkit/components/extensions/MessageChannel.jsm797
-rw-r--r--toolkit/components/extensions/NativeMessaging.jsm443
-rw-r--r--toolkit/components/extensions/Schemas.jsm2143
-rw-r--r--toolkit/components/extensions/ext-alarms.js155
-rw-r--r--toolkit/components/extensions/ext-backgroundPage.js147
-rw-r--r--toolkit/components/extensions/ext-browser-content.js217
-rw-r--r--toolkit/components/extensions/ext-c-backgroundPage.js45
-rw-r--r--toolkit/components/extensions/ext-c-extension.js57
-rw-r--r--toolkit/components/extensions/ext-c-runtime.js93
-rw-r--r--toolkit/components/extensions/ext-c-storage.js62
-rw-r--r--toolkit/components/extensions/ext-c-test.js188
-rw-r--r--toolkit/components/extensions/ext-cookies.js484
-rw-r--r--toolkit/components/extensions/ext-downloads.js799
-rw-r--r--toolkit/components/extensions/ext-extension.js20
-rw-r--r--toolkit/components/extensions/ext-i18n.js34
-rw-r--r--toolkit/components/extensions/ext-idle.js94
-rw-r--r--toolkit/components/extensions/ext-management.js109
-rw-r--r--toolkit/components/extensions/ext-notifications.js161
-rw-r--r--toolkit/components/extensions/ext-runtime.js134
-rw-r--r--toolkit/components/extensions/ext-storage.js68
-rw-r--r--toolkit/components/extensions/ext-topSites.js24
-rw-r--r--toolkit/components/extensions/ext-webNavigation.js192
-rw-r--r--toolkit/components/extensions/ext-webRequest.js115
-rw-r--r--toolkit/components/extensions/extensions-toolkit.manifest49
-rw-r--r--toolkit/components/extensions/jar.mn26
-rw-r--r--toolkit/components/extensions/moz.build42
-rw-r--r--toolkit/components/extensions/schemas/LICENSE27
-rw-r--r--toolkit/components/extensions/schemas/alarms.json145
-rw-r--r--toolkit/components/extensions/schemas/cookies.json224
-rw-r--r--toolkit/components/extensions/schemas/downloads.json793
-rw-r--r--toolkit/components/extensions/schemas/events.json322
-rw-r--r--toolkit/components/extensions/schemas/experiments.json16
-rw-r--r--toolkit/components/extensions/schemas/extension.json178
-rw-r--r--toolkit/components/extensions/schemas/extension_types.json83
-rw-r--r--toolkit/components/extensions/schemas/i18n.json132
-rw-r--r--toolkit/components/extensions/schemas/idle.json70
-rw-r--r--toolkit/components/extensions/schemas/jar.mn25
-rw-r--r--toolkit/components/extensions/schemas/management.json250
-rw-r--r--toolkit/components/extensions/schemas/manifest.json377
-rw-r--r--toolkit/components/extensions/schemas/moz.build7
-rw-r--r--toolkit/components/extensions/schemas/native_host_manifest.json37
-rw-r--r--toolkit/components/extensions/schemas/notifications.json416
-rw-r--r--toolkit/components/extensions/schemas/runtime.json592
-rw-r--r--toolkit/components/extensions/schemas/storage.json229
-rw-r--r--toolkit/components/extensions/schemas/test.json215
-rw-r--r--toolkit/components/extensions/schemas/top_sites.json66
-rw-r--r--toolkit/components/extensions/schemas/web_navigation.json387
-rw-r--r--toolkit/components/extensions/schemas/web_request.json616
-rw-r--r--toolkit/components/extensions/test/mochitest/.eslintrc.js35
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome.ini35
-rw-r--r--toolkit/components/extensions/test/mochitest/chrome_head.js12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html7
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html11
-rw-r--r--toolkit/components/extensions/test/mochitest/file_csp.html14
-rw-r--r--toolkit/components/extensions/test/mochitest/file_csp.html^headers^1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_bad.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_good.pngbin0 -> 580 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_image_redirect.pngbin0 -> 5401 bytes
-rw-r--r--toolkit/components/extensions/test/mochitest/file_mixed.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_permission_xhr.html55
-rw-r--r--toolkit/components/extensions/test/mochitest/file_privilege_escalation.html13
-rw-r--r--toolkit/components/extensions/test/mochitest/file_sample.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_bad.js3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_good.js3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_redirect.js4
-rw-r--r--toolkit/components/extensions/test/mochitest/file_script_xhr.js5
-rw-r--r--toolkit/components/extensions/test/mochitest/file_style_bad.css3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_style_good.css3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_style_redirect.css3
-rw-r--r--toolkit/components/extensions/test/mochitest/file_teardown_test.js24
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html9
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html8
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^1
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html12
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html8
-rw-r--r--toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html7
-rw-r--r--toolkit/components/extensions/test/mochitest/file_with_about_blank.html10
-rw-r--r--toolkit/components/extensions/test/mochitest/head.js13
-rw-r--r--toolkit/components/extensions/test/mochitest/head_cookies.js167
-rw-r--r--toolkit/components/extensions/test/mochitest/head_webrequest.js331
-rw-r--r--toolkit/components/extensions/test/mochitest/mochitest.ini114
-rw-r--r--toolkit/components/extensions/test/mochitest/redirection.sjs4
-rw-r--r--toolkit/components/extensions/test/mochitest/return_headers.sjs20
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html166
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html84
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html80
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html68
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html106
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html141
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html64
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html50
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html164
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html53
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html96
-rw-r--r--toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html61
-rw-r--r--toolkit/components/extensions/test/mochitest/test_clipboard.html140
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_all_apis.js158
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html46
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html47
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html47
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html76
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html162
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html117
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html88
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html54
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html81
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html165
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html48
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html81
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html95
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html89
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html59
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html96
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies.html234
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html93
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html72
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html112
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html92
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_generate.html49
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_geturl.html72
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_i18n.html432
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html49
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_jsversion.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html63
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_notifications.html224
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html119
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html103
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html127
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html78
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html61
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html60
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_schema.html73
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html101
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html83
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html79
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html93
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_content.html330
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html118
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html202
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html150
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_test.html191
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html170
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html353
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html559
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html308
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html116
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html327
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html216
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html199
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html105
-rw-r--r--toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html86
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js8
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_test.jsm22
-rw-r--r--toolkit/components/extensions/test/mochitest/webrequest_worker.js3
-rw-r--r--toolkit/components/extensions/test/xpcshell/.eslintrc.js9
-rw-r--r--toolkit/components/extensions/test/xpcshell/data/file_download.html12
-rw-r--r--toolkit/components/extensions/test/xpcshell/data/file_download.txt1
-rw-r--r--toolkit/components/extensions/test/xpcshell/head.js111
-rw-r--r--toolkit/components/extensions/test/xpcshell/head_native_messaging.js131
-rw-r--r--toolkit/components/extensions/test/xpcshell/head_sync.js67
-rw-r--r--toolkit/components/extensions/test/xpcshell/native_messaging.ini13
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js38
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_csp_validator.js85
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms.js210
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js33
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js44
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js44
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js64
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js91
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js23
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js24
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js22
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js40
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js72
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js45
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js34
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_contexts.js190
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads.js76
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js354
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js862
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js402
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_experiments.js175
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_extension.js55
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_idle.js202
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js37
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js168
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js188
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js50
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_management.js20
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js135
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js30
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js27
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js13
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js514
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js128
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js82
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js30
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js23
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js26
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js25
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js337
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js79
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js59
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js54
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js51
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas.js1427
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js147
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js102
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js232
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_simple.js69
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage.js334
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js1073
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_topSites.js85
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js55
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_locale_converter.js133
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_locale_data.js130
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_native_messaging.js302
-rw-r--r--toolkit/components/extensions/test/xpcshell/xpcshell.ini72
-rw-r--r--toolkit/components/exthelper/extApplication.js719
-rw-r--r--toolkit/components/exthelper/extIApplication.idl416
-rw-r--r--toolkit/components/exthelper/moz.build12
-rw-r--r--toolkit/components/feeds/FeedProcessor.js1792
-rw-r--r--toolkit/components/feeds/FeedProcessor.manifest14
-rw-r--r--toolkit/components/feeds/moz.build31
-rw-r--r--toolkit/components/feeds/nsIFeed.idl86
-rw-r--r--toolkit/components/feeds/nsIFeedContainer.idl85
-rw-r--r--toolkit/components/feeds/nsIFeedElementBase.idl28
-rw-r--r--toolkit/components/feeds/nsIFeedEntry.idl46
-rw-r--r--toolkit/components/feeds/nsIFeedGenerator.idl30
-rw-r--r--toolkit/components/feeds/nsIFeedListener.idl87
-rw-r--r--toolkit/components/feeds/nsIFeedPerson.idl30
-rw-r--r--toolkit/components/feeds/nsIFeedProcessor.idl57
-rw-r--r--toolkit/components/feeds/nsIFeedResult.idl65
-rw-r--r--toolkit/components/feeds/nsIFeedTextConstruct.idl57
-rw-r--r--toolkit/components/feeds/test/.eslintrc.js8
-rw-r--r--toolkit/components/feeds/test/chrome.ini3
-rw-r--r--toolkit/components/feeds/test/head.js80
-rw-r--r--toolkit/components/feeds/test/test_bug675492.xul31
-rw-r--r--toolkit/components/feeds/test/test_xml.js97
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/author_namespaces.xml16
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_author.xml30
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_content.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_content_encoded.xml43
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_content_html.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml.xml39
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml_with_markup.xml39
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_contributor.xml30
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_html_cdata.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_id.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore2.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_IANA.xml29
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_alt_extension.xml25
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure_populate_enclosures.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_otherURI_alt.xml25
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_payment_alt.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_link_random.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_published.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_rights_normalized.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_summary.xml40
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_title.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_title_normalized.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_updated.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_w_content_encoded.xml43
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_xhtml_baseURI_with_amp.xml37
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase_on_link.xml22
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_atom_rights_xhtml.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author2.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author_email.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author_email_2.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author_name.xml13
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author_surrounded.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_author_uri.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_comment_rss_extra_att.xml29
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_contributor.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_entry_count.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_generator.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri_xmlbase.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_generator_version.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_icon.xml11
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_id.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_id_extra_att.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_logo.xml11
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_random_attributes_on_feed_and_entry.xml30
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_rights_normalized.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml_nested_divs.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_subtitle.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_tantek_title.xml46
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_title.xml10
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_title_full_feed.xml936
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml_entities.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_updated.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_updated_invalid.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_updated_normalized.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_version.xml9
-rw-r--r--toolkit/components/feeds/test/xml/rfc4287/feed_xmlBase.xml23
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rss090.xml50
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rss091.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rss091_withNS.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rss092.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rss093.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rss094.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rss09x/rssUnknown.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_bogus_title.xml45
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_description.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_description_normalized.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_description_with_dc.xml19
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_description_with_dc_only.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_generator.xml34
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_id.xml25
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_image.xml39
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_items_length_zero.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_link.xml16
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_link_normalized.xml16
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_textInput.xml40
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_title.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_title_extra_att.xml16
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_title_normalized.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_updated.xml26
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_updated_dctermsmodified.xml27
-rw-r--r--toolkit/components/feeds/test/xml/rss1/feed_version.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss1/full_feed.xml41
-rw-r--r--toolkit/components/feeds/test/xml/rss1/full_feed_not_bozo.xml354
-rw-r--r--toolkit/components/feeds/test/xml/rss1/full_feed_unknown_extension.xml55
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_2_dc_description.xml34
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher_extra_att_invalid_rdf.xml36
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_count.xml32
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_dc_creator.xml34
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_dc_description.xml35
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_dc_description_normalized.xml37
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_description.xml31
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_id.xml32
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_link.xml32
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_link_normalized.xml32
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_title.xml31
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_title_normalized.xml32
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_updated_dcterms.xml28
-rw-r--r--toolkit/components/feeds/test/xml/rss1/item_wiki_importance_extra_att.xml34
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_category.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_category_count.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_cloud.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_copyright.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_copyright_linebreak.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_data_outside_channel.xml13
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_dc_contributor.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_dc_creator.xml13
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_description.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_description_html.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_description_html_cdata.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_docs.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_generator.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_image_desc.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_image_desc_width_height.xml19
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_image_required.xml16
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_language.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_lastBuildDate.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_linebreak_link.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_link.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_link_cdata.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_managingEditor.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_managingEditor_extra_att.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_multiple_categories.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_pubDate.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_pubDate_invalid.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_1.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_2.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_pubDate_timezoneZ.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_pubDate_utc.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_rating.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_single_quote_stylesheet_pi.xml26
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_skipDays.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_skipHours.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_subtitle.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_subtitle_html.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_subtitle_markup_stripped.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_textinput.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_title.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_title_cdata_mixed.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_title_nesting.xml13
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_ttl.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_updated.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_updated_dcdate.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate_priority.xml13
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_webMaster.xml12
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_wfw_commentapi.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_wfw_commentrss.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_wiki.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rss2/feed_wiki_unusual_prefix.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_author.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_category.xml16
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_comments.xml19
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_content_encoded.xml15
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_count.xml26
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_count2.xml30
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_description.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_description_2.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_description_cdata.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_description_decode_entities.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_description_normalized.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_description_normalized_nohtml.xml18
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_enclosure.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates.xml24
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates2.xml24
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed2.xml33
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_bogus_url.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_default.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false_uppercase.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_true_uppercase.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_unknown_value.xml20
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_normalized.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_guid_with_link.xml22
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_link.xml19
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_link_normalized.xml19
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_plain_desc.xml19
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_populated_enclosures.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_pubDate.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_published.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_title.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_title_normalized.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/item_updated_dcdate.xml14
-rw-r--r--toolkit/components/feeds/test/xml/rss2/items_2_titles.xml17
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_content.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_content2.xml22
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_content_429049.xml32
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_content_multiple.xml23
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_content_populate_enclosure.xml21
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_group_content.xml26
-rw-r--r--toolkit/components/feeds/test/xml/rss2/mrss_group_content_populate_enclosure.xml26
-rw-r--r--toolkit/components/feeds/test/xpcshell.ini209
-rw-r--r--toolkit/components/filepicker/content/filepicker.js833
-rw-r--r--toolkit/components/filepicker/content/filepicker.xul80
-rw-r--r--toolkit/components/filepicker/jar.mn8
-rw-r--r--toolkit/components/filepicker/moz.build25
-rw-r--r--toolkit/components/filepicker/nsFilePicker.js319
-rw-r--r--toolkit/components/filepicker/nsFilePicker.manifest4
-rw-r--r--toolkit/components/filepicker/nsFileView.cpp982
-rw-r--r--toolkit/components/filepicker/nsIFileView.idl34
-rw-r--r--toolkit/components/filepicker/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/filepicker/test/unit/test_filecomplete.js45
-rw-r--r--toolkit/components/filepicker/test/unit/xpcshell.ini7
-rw-r--r--toolkit/components/filewatcher/NativeFileWatcherNotSupported.h52
-rw-r--r--toolkit/components/filewatcher/NativeFileWatcherWin.cpp1494
-rw-r--r--toolkit/components/filewatcher/NativeFileWatcherWin.h50
-rw-r--r--toolkit/components/filewatcher/moz.build23
-rw-r--r--toolkit/components/filewatcher/nsINativeFileWatcher.idl111
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/head.js29
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_arguments.js79
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.js69
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.js39
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.js62
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.js49
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js46
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js51
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js54
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js54
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.js73
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js114
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.js55
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js32
-rw-r--r--toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini18
-rw-r--r--toolkit/components/finalizationwitness/FinalizationWitnessService.cpp247
-rw-r--r--toolkit/components/finalizationwitness/FinalizationWitnessService.h32
-rw-r--r--toolkit/components/finalizationwitness/moz.build25
-rw-r--r--toolkit/components/finalizationwitness/nsIFinalizationWitnessService.idl35
-rw-r--r--toolkit/components/find/moz.build17
-rw-r--r--toolkit/components/find/nsFindService.cpp101
-rw-r--r--toolkit/components/find/nsFindService.h46
-rw-r--r--toolkit/components/find/nsIFindService.idl26
-rw-r--r--toolkit/components/formautofill/FormAutofill.jsm85
-rw-r--r--toolkit/components/formautofill/FormAutofillContentService.js272
-rw-r--r--toolkit/components/formautofill/FormAutofillIntegration.jsm62
-rw-r--r--toolkit/components/formautofill/FormAutofillStartup.js64
-rw-r--r--toolkit/components/formautofill/content/RequestAutocompleteUI.jsm58
-rw-r--r--toolkit/components/formautofill/content/requestAutocomplete.js85
-rw-r--r--toolkit/components/formautofill/content/requestAutocomplete.xhtml31
-rw-r--r--toolkit/components/formautofill/formautofill.manifest7
-rw-r--r--toolkit/components/formautofill/jar.mn8
-rw-r--r--toolkit/components/formautofill/moz.build46
-rw-r--r--toolkit/components/formautofill/nsIFormAutofillContentService.idl46
-rw-r--r--toolkit/components/formautofill/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/formautofill/test/browser/browser.ini10
-rw-r--r--toolkit/components/formautofill/test/browser/browser_infrastructure.js48
-rw-r--r--toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js48
-rw-r--r--toolkit/components/formautofill/test/browser/head.js18
-rw-r--r--toolkit/components/formautofill/test/browser/loader.js38
-rw-r--r--toolkit/components/formautofill/test/chrome/.eslintrc.js7
-rw-r--r--toolkit/components/formautofill/test/chrome/chrome.ini17
-rw-r--r--toolkit/components/formautofill/test/chrome/head.js15
-rw-r--r--toolkit/components/formautofill/test/chrome/loader.js116
-rw-r--r--toolkit/components/formautofill/test/chrome/loader_parent.js77
-rw-r--r--toolkit/components/formautofill/test/chrome/test_infrastructure.html8
-rw-r--r--toolkit/components/formautofill/test/chrome/test_infrastructure.js61
-rw-r--r--toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html9
-rw-r--r--toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js26
-rw-r--r--toolkit/components/formautofill/test/head_common.js245
-rw-r--r--toolkit/components/formautofill/test/loader_common.js120
-rw-r--r--toolkit/components/formautofill/test/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/formautofill/test/xpcshell/head.js23
-rw-r--r--toolkit/components/formautofill/test/xpcshell/loader.js46
-rw-r--r--toolkit/components/formautofill/test/xpcshell/test_infrastructure.js48
-rw-r--r--toolkit/components/formautofill/test/xpcshell/test_integration.js72
-rw-r--r--toolkit/components/formautofill/test/xpcshell/xpcshell.ini12
-rw-r--r--toolkit/components/gfx/GfxSanityTest.manifest3
-rw-r--r--toolkit/components/gfx/SanityTest.js315
-rw-r--r--toolkit/components/gfx/content/gfxFrameScript.js62
-rw-r--r--toolkit/components/gfx/content/sanityparent.html7
-rw-r--r--toolkit/components/gfx/content/sanitytest.html6
-rw-r--r--toolkit/components/gfx/content/videotest.mp4bin0 -> 1563 bytes
-rw-r--r--toolkit/components/gfx/jar.mn10
-rw-r--r--toolkit/components/gfx/moz.build15
-rw-r--r--toolkit/components/jsdownloads/moz.build18
-rw-r--r--toolkit/components/jsdownloads/public/moz.build9
-rw-r--r--toolkit/components/jsdownloads/public/mozIDownloadPlatform.idl61
-rw-r--r--toolkit/components/jsdownloads/src/DownloadCore.jsm2871
-rw-r--r--toolkit/components/jsdownloads/src/DownloadImport.jsm193
-rw-r--r--toolkit/components/jsdownloads/src/DownloadIntegration.jsm1273
-rw-r--r--toolkit/components/jsdownloads/src/DownloadLegacy.js309
-rw-r--r--toolkit/components/jsdownloads/src/DownloadList.jsm559
-rw-r--r--toolkit/components/jsdownloads/src/DownloadPlatform.cpp275
-rw-r--r--toolkit/components/jsdownloads/src/DownloadPlatform.h34
-rw-r--r--toolkit/components/jsdownloads/src/DownloadStore.jsm203
-rw-r--r--toolkit/components/jsdownloads/src/DownloadUIHelper.jsm243
-rw-r--r--toolkit/components/jsdownloads/src/Downloads.jsm305
-rw-r--r--toolkit/components/jsdownloads/src/Downloads.manifest2
-rw-r--r--toolkit/components/jsdownloads/src/moz.build31
-rw-r--r--toolkit/components/jsdownloads/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/jsdownloads/test/browser/browser.ini7
-rw-r--r--toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js97
-rw-r--r--toolkit/components/jsdownloads/test/browser/head.js87
-rw-r--r--toolkit/components/jsdownloads/test/browser/testFile.html9
-rw-r--r--toolkit/components/jsdownloads/test/data/.eslintrc.js7
-rw-r--r--toolkit/components/jsdownloads/test/data/empty.txt0
-rw-r--r--toolkit/components/jsdownloads/test/data/source.txt1
-rw-r--r--toolkit/components/jsdownloads/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/jsdownloads/test/unit/common_test_Download.js2432
-rw-r--r--toolkit/components/jsdownloads/test/unit/head.js843
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadCore.js87
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadImport.js701
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js432
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js17
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadList.js564
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_DownloadStore.js315
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_Downloads.js194
-rw-r--r--toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js24
-rw-r--r--toolkit/components/jsdownloads/test/unit/xpcshell.ini19
-rw-r--r--toolkit/components/lz4/lz4.cpp73
-rw-r--r--toolkit/components/lz4/lz4.js156
-rw-r--r--toolkit/components/lz4/lz4_internal.js68
-rw-r--r--toolkit/components/lz4/moz.build18
-rw-r--r--toolkit/components/lz4/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/lz4/tests/xpcshell/data/chrome.manifest1
-rw-r--r--toolkit/components/lz4/tests/xpcshell/data/compression.lzbin0 -> 23 bytes
-rw-r--r--toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js146
-rw-r--r--toolkit/components/lz4/tests/xpcshell/test_lz4.js43
-rw-r--r--toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js41
-rw-r--r--toolkit/components/lz4/tests/xpcshell/xpcshell.ini11
-rw-r--r--toolkit/components/maintenanceservice/Makefile.in13
-rw-r--r--toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi278
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.cpp391
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.exe.manifest26
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.h10
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.rc86
-rw-r--r--toolkit/components/maintenanceservice/moz.build54
-rw-r--r--toolkit/components/maintenanceservice/resource.h20
-rw-r--r--toolkit/components/maintenanceservice/servicebase.cpp86
-rw-r--r--toolkit/components/maintenanceservice/servicebase.h22
-rw-r--r--toolkit/components/maintenanceservice/serviceinstall.cpp759
-rw-r--r--toolkit/components/maintenanceservice/serviceinstall.h21
-rw-r--r--toolkit/components/maintenanceservice/workmonitor.cpp758
-rw-r--r--toolkit/components/maintenanceservice/workmonitor.h5
-rw-r--r--toolkit/components/mediasniffer/moz.build22
-rw-r--r--toolkit/components/mediasniffer/mp3sniff.c156
-rw-r--r--toolkit/components/mediasniffer/mp3sniff.h15
-rw-r--r--toolkit/components/mediasniffer/nsMediaSniffer.cpp200
-rw-r--r--toolkit/components/mediasniffer/nsMediaSniffer.h47
-rw-r--r--toolkit/components/mediasniffer/nsMediaSnifferModule.cpp37
-rw-r--r--toolkit/components/mediasniffer/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/detodos.mp3bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/ff-inst.exebin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/file.mkvbin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/file.webmbin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/fl10.mp2bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/he_free.mp3bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/id3tags.mp3bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/data/notags.mp3bin0 -> 512 bytes
-rw-r--r--toolkit/components/mediasniffer/test/unit/test_mediasniffer.js105
-rw-r--r--toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js122
-rw-r--r--toolkit/components/mediasniffer/test/unit/xpcshell.ini19
-rw-r--r--toolkit/components/microformats/manifest.ini7
-rw-r--r--toolkit/components/microformats/microformat-shiv.js4523
-rw-r--r--toolkit/components/microformats/moz.build12
-rw-r--r--toolkit/components/microformats/test/interface-tests/count-test.js107
-rw-r--r--toolkit/components/microformats/test/interface-tests/experimental-test.js37
-rw-r--r--toolkit/components/microformats/test/interface-tests/get-test.js605
-rw-r--r--toolkit/components/microformats/test/interface-tests/getParent-test.js220
-rw-r--r--toolkit/components/microformats/test/interface-tests/hasMicroformats-test.js185
-rw-r--r--toolkit/components/microformats/test/interface-tests/index.html69
-rw-r--r--toolkit/components/microformats/test/interface-tests/isMicroformat-test.js146
-rw-r--r--toolkit/components/microformats/test/lib/dates.js268
-rw-r--r--toolkit/components/microformats/test/lib/domparser.js103
-rw-r--r--toolkit/components/microformats/test/lib/domutils.js611
-rw-r--r--toolkit/components/microformats/test/lib/html.js107
-rw-r--r--toolkit/components/microformats/test/lib/isodate.js481
-rw-r--r--toolkit/components/microformats/test/lib/living-standard.js1
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-adr.js29
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-card.js85
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-entry.js52
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-event.js64
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-feed.js36
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-geo.js22
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-item.js30
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-listing.js41
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-news.js42
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-org.js24
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-product.js49
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-recipe.js47
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-resume.js34
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-review-aggregate.js40
-rw-r--r--toolkit/components/microformats/test/lib/maps/h-review.js46
-rw-r--r--toolkit/components/microformats/test/lib/maps/rel.js47
-rw-r--r--toolkit/components/microformats/test/lib/parser-implied.js439
-rw-r--r--toolkit/components/microformats/test/lib/parser-includes.js150
-rw-r--r--toolkit/components/microformats/test/lib/parser-rels.js200
-rw-r--r--toolkit/components/microformats/test/lib/parser.js1453
-rw-r--r--toolkit/components/microformats/test/lib/text.js151
-rw-r--r--toolkit/components/microformats/test/lib/url.js73
-rw-r--r--toolkit/components/microformats/test/lib/utilities.js206
-rw-r--r--toolkit/components/microformats/test/lib/version.js1
-rw-r--r--toolkit/components/microformats/test/marionette/microformats_tester.py170
-rw-r--r--toolkit/components/microformats/test/marionette/test_interface.py17
-rw-r--r--toolkit/components/microformats/test/marionette/test_modules.py17
-rw-r--r--toolkit/components/microformats/test/marionette/test_standards.py17
-rw-r--r--toolkit/components/microformats/test/module-tests/dates-test.js113
-rw-r--r--toolkit/components/microformats/test/module-tests/domutils-test.js206
-rw-r--r--toolkit/components/microformats/test/module-tests/html-test.js50
-rw-r--r--toolkit/components/microformats/test/module-tests/index.html76
-rw-r--r--toolkit/components/microformats/test/module-tests/isodate-test.js145
-rw-r--r--toolkit/components/microformats/test/module-tests/text-test.js56
-rw-r--r--toolkit/components/microformats/test/module-tests/url-test.js25
-rw-r--r--toolkit/components/microformats/test/module-tests/utilities-test.js93
-rw-r--r--toolkit/components/microformats/test/standards-tests/index.html179
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-mixedpropertries.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-tworoots.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-mixed-h-entry-mixedroots.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-mixed-h-resume-mixedroots.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-adr-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-geo-abbrpattern.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-geo-hidden.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-geo-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-geo-valuetitleclass.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-ampm.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-attendees.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-combining.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-concatenate.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-time.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-email.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-format.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-hyperlinkedphoto.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-multiple.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-name.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hcard-single.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hentry-summarycontent.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hfeed-simple.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hnews-all.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hnews-minimum.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-aggregate.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hresume-affiliation.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hresume-contact.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hresume-education.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hresume-skill.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hresume-work.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-hcard.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-vevent.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hreview-item.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-hreview-vcard.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-includes-hcarditemref.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-includes-heventitemref.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-includes-hyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-includes-object.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v1-includes-table.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geo.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geourl.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-as-note-note.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-baseurl.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-childimplied.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-extendeddescription.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hcard.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-horghcard.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hyperlinkedphoto.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedphoto.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedurl.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-nested.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-p-property.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-card-relativeurls.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-impliedvalue-nested.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-summarycontent.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-u-property.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-urlincontent.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-ampm.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-attendees.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-combining.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-concatenate.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dates.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dt-property.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-event-time.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-implied-title.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-simple.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-abbrpattern.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-altitude.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-hidden.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-valuetitleclass.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-news-all.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-news-minimum.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-org-hyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simple.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-product-aggregate.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-product-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-all.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-minimum.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-affiliation.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-contact.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-education.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-skill.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-work.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-hevent.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-justahyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-simpleproperties.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-hyperlink.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-implieditem.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-item.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-justaname.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-photo.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-h-review-vcard.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-duplicate-rels.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-license.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-nofollow.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-rel-urls.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-varying-text-duplicate-rels.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-all.js27
-rw-r--r--toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-elsewhere.js27
-rw-r--r--toolkit/components/microformats/test/static/count.html84
-rw-r--r--toolkit/components/microformats/test/static/css/mocha-custom.css9
-rw-r--r--toolkit/components/microformats/test/static/css/mocha.css270
-rw-r--r--toolkit/components/microformats/test/static/css/prettify.css65
-rw-r--r--toolkit/components/microformats/test/static/css/testrunner.css367
-rw-r--r--toolkit/components/microformats/test/static/images/logo.gifbin0 -> 2943 bytes
-rw-r--r--toolkit/components/microformats/test/static/images/photo.gifbin0 -> 2943 bytes
-rw-r--r--toolkit/components/microformats/test/static/javascript/DOMParser.js99
-rw-r--r--toolkit/components/microformats/test/static/javascript/beautify.js518
-rw-r--r--toolkit/components/microformats/test/static/javascript/chai.js5351
-rw-r--r--toolkit/components/microformats/test/static/javascript/count.js62
-rw-r--r--toolkit/components/microformats/test/static/javascript/data.js1
-rw-r--r--toolkit/components/microformats/test/static/javascript/deep-diff-0.3.1.min.js5
-rw-r--r--toolkit/components/microformats/test/static/javascript/mocha.js6573
-rw-r--r--toolkit/components/microformats/test/static/javascript/parse.js133
-rw-r--r--toolkit/components/microformats/test/static/javascript/prettify.js1479
-rw-r--r--toolkit/components/microformats/test/static/javascript/testrunner.js179
-rw-r--r--toolkit/components/microformats/test/static/parse-umd.html85
-rw-r--r--toolkit/components/microformats/test/static/parse.html127
-rw-r--r--toolkit/components/microformats/test/static/testrunner.html69
-rw-r--r--toolkit/components/microformats/update/package.json21
-rw-r--r--toolkit/components/microformats/update/readme.txt33
-rw-r--r--toolkit/components/microformats/update/update.js266
-rw-r--r--toolkit/components/moz.build108
-rw-r--r--toolkit/components/mozintl/MozIntl.cpp74
-rw-r--r--toolkit/components/mozintl/MozIntl.h22
-rw-r--r--toolkit/components/mozintl/moz.build19
-rw-r--r--toolkit/components/mozintl/mozIMozIntl.idl12
-rw-r--r--toolkit/components/mozintl/test/test_mozintl.js32
-rw-r--r--toolkit/components/mozintl/test/xpcshell.ini5
-rw-r--r--toolkit/components/mozprotocol/moz.build14
-rw-r--r--toolkit/components/mozprotocol/mozProtocolHandler.js48
-rw-r--r--toolkit/components/mozprotocol/mozProtocolHandler.manifest2
-rw-r--r--toolkit/components/mozprotocol/tests/browser.ini5
-rw-r--r--toolkit/components/mozprotocol/tests/browser_mozprotocol.js14
-rw-r--r--toolkit/components/mozprotocol/tests/mozprotocol.html7
-rw-r--r--toolkit/components/narrate/.eslintrc.js94
-rw-r--r--toolkit/components/narrate/NarrateControls.jsm343
-rw-r--r--toolkit/components/narrate/Narrator.jsm464
-rw-r--r--toolkit/components/narrate/VoiceSelect.jsm299
-rw-r--r--toolkit/components/narrate/moz.build13
-rw-r--r--toolkit/components/narrate/test/.eslintrc.js23
-rw-r--r--toolkit/components/narrate/test/NarrateTestUtils.jsm148
-rw-r--r--toolkit/components/narrate/test/browser.ini12
-rw-r--r--toolkit/components/narrate/test/browser_narrate.js137
-rw-r--r--toolkit/components/narrate/test/browser_narrate_disable.js37
-rw-r--r--toolkit/components/narrate/test/browser_narrate_language.js73
-rw-r--r--toolkit/components/narrate/test/browser_voiceselect.js112
-rw-r--r--toolkit/components/narrate/test/browser_word_highlight.js69
-rw-r--r--toolkit/components/narrate/test/head.js87
-rw-r--r--toolkit/components/narrate/test/inferno.html238
-rw-r--r--toolkit/components/narrate/test/moby_dick.html218
-rw-r--r--toolkit/components/nsDefaultCLH.js125
-rw-r--r--toolkit/components/nsDefaultCLH.manifest3
-rw-r--r--toolkit/components/osfile/NativeOSFileInternals.cpp916
-rw-r--r--toolkit/components/osfile/NativeOSFileInternals.h25
-rw-r--r--toolkit/components/osfile/modules/moz.build22
-rw-r--r--toolkit/components/osfile/modules/osfile_async_front.jsm1533
-rw-r--r--toolkit/components/osfile/modules/osfile_async_worker.js407
-rw-r--r--toolkit/components/osfile/modules/osfile_native.jsm70
-rw-r--r--toolkit/components/osfile/modules/osfile_shared_allthreads.jsm1315
-rw-r--r--toolkit/components/osfile/modules/osfile_shared_front.jsm567
-rw-r--r--toolkit/components/osfile/modules/osfile_unix_allthreads.jsm375
-rw-r--r--toolkit/components/osfile/modules/osfile_unix_back.jsm735
-rw-r--r--toolkit/components/osfile/modules/osfile_unix_front.jsm1193
-rw-r--r--toolkit/components/osfile/modules/osfile_win_allthreads.jsm425
-rw-r--r--toolkit/components/osfile/modules/osfile_win_back.jsm437
-rw-r--r--toolkit/components/osfile/modules/osfile_win_front.jsm1266
-rw-r--r--toolkit/components/osfile/modules/ospath.jsm45
-rw-r--r--toolkit/components/osfile/modules/ospath_unix.jsm202
-rw-r--r--toolkit/components/osfile/modules/ospath_win.jsm373
-rw-r--r--toolkit/components/osfile/moz.build35
-rw-r--r--toolkit/components/osfile/nsINativeOSFileInternals.idl93
-rw-r--r--toolkit/components/osfile/osfile.jsm32
-rw-r--r--toolkit/components/osfile/tests/mochi/.eslintrc.js7
-rw-r--r--toolkit/components/osfile/tests/mochi/chrome.ini15
-rw-r--r--toolkit/components/osfile/tests/mochi/main_test_osfile_async.js443
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_async.xul23
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_back.xul46
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_comms.xul84
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_front.xul44
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_handler.js34
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js145
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js566
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js32
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js201
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js211
-rw-r--r--toolkit/components/osfile/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/osfile/tests/xpcshell/head.js99
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_available_free_space.js38
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_compression.js98
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_constants.js31
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_creationDate.js31
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_duration.js91
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_exception.js89
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js114
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_loader.js37
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js9
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_logging.js74
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_makeDir.js142
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_open.js70
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async.js16
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js122
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js39
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js113
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js30
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js153
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js211
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js103
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js48
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_error.js63
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js100
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js114
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js143
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js27
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path.js159
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path_constants.js83
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_queue.js38
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_read_write.js103
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_remove.js56
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_removeDir.js177
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js55
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_reset.js95
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_shutdown.js98
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_telemetry.js63
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_unique.js88
-rw-r--r--toolkit/components/osfile/tests/xpcshell/xpcshell.ini51
-rw-r--r--toolkit/components/parentalcontrols/moz.build31
-rw-r--r--toolkit/components/parentalcontrols/nsIParentalControlsService.idl104
-rw-r--r--toolkit/components/parentalcontrols/nsParentalControlsService.h44
-rw-r--r--toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp103
-rw-r--r--toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm79
-rw-r--r--toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp73
-rw-r--r--toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp347
-rw-r--r--toolkit/components/passwordmgr/.eslintrc.js36
-rw-r--r--toolkit/components/passwordmgr/InsecurePasswordUtils.jsm150
-rw-r--r--toolkit/components/passwordmgr/LoginHelper.jsm725
-rw-r--r--toolkit/components/passwordmgr/LoginImport.jsm173
-rw-r--r--toolkit/components/passwordmgr/LoginManagerContent.jsm1619
-rw-r--r--toolkit/components/passwordmgr/LoginManagerContextMenu.jsm199
-rw-r--r--toolkit/components/passwordmgr/LoginManagerParent.jsm511
-rw-r--r--toolkit/components/passwordmgr/LoginRecipes.jsm260
-rw-r--r--toolkit/components/passwordmgr/LoginStore.jsm136
-rw-r--r--toolkit/components/passwordmgr/OSCrypto.jsm22
-rw-r--r--toolkit/components/passwordmgr/OSCrypto_win.js245
-rw-r--r--toolkit/components/passwordmgr/content/passwordManager.js728
-rw-r--r--toolkit/components/passwordmgr/content/passwordManager.xul134
-rw-r--r--toolkit/components/passwordmgr/content/recipes.json31
-rw-r--r--toolkit/components/passwordmgr/crypto-SDR.js207
-rw-r--r--toolkit/components/passwordmgr/jar.mn9
-rw-r--r--toolkit/components/passwordmgr/moz.build78
-rw-r--r--toolkit/components/passwordmgr/nsILoginInfo.idl120
-rw-r--r--toolkit/components/passwordmgr/nsILoginManager.idl262
-rw-r--r--toolkit/components/passwordmgr/nsILoginManagerCrypto.idl67
-rw-r--r--toolkit/components/passwordmgr/nsILoginManagerPrompter.idl94
-rw-r--r--toolkit/components/passwordmgr/nsILoginManagerStorage.idl211
-rw-r--r--toolkit/components/passwordmgr/nsILoginMetaInfo.idl55
-rw-r--r--toolkit/components/passwordmgr/nsLoginInfo.js93
-rw-r--r--toolkit/components/passwordmgr/nsLoginManager.js541
-rw-r--r--toolkit/components/passwordmgr/nsLoginManagerPrompter.js1701
-rw-r--r--toolkit/components/passwordmgr/passwordmgr.manifest17
-rw-r--r--toolkit/components/passwordmgr/storage-json.js514
-rw-r--r--toolkit/components/passwordmgr/storage-mozStorage.js1262
-rw-r--r--toolkit/components/passwordmgr/test/.eslintrc.js13
-rw-r--r--toolkit/components/passwordmgr/test/LoginTestUtils.jsm295
-rw-r--r--toolkit/components/passwordmgr/test/authenticate.sjs228
-rw-r--r--toolkit/components/passwordmgr/test/blank.html8
-rw-r--r--toolkit/components/passwordmgr/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/passwordmgr/test/browser/authenticate.sjs110
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser.ini72
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js94
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js99
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js41
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js600
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js123
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js144
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu.js432
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js99
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js144
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js56
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js126
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js93
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js102
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_http_autofill.js78
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js94
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js59
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications.js81
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications_2.js125
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications_password.js145
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_notifications_username.js119
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js100
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js126
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js65
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js129
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js208
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js42
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js192
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js144
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_autofocus_js.html10
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_iframe.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_same_origin_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/formless_basic.html18
-rw-r--r--toolkit/components/passwordmgr/test/browser/head.js137
-rw-r--r--toolkit/components/passwordmgr/test/browser/insecure_test.html9
-rw-r--r--toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/multiple_forms.html129
-rw-r--r--toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs6
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html25
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html32
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html31
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html26
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html32
-rw-r--r--toolkit/components/passwordmgr/test/chrome/chrome.ini13
-rw-r--r--toolkit/components/passwordmgr/test/chrome/notification_common.js111
-rw-r--r--toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html9
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html33
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html33
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html29
-rw-r--r--toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html40
-rw-r--r--toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html322
-rw-r--r--toolkit/components/passwordmgr/test/chrome_timeout.js11
-rw-r--r--toolkit/components/passwordmgr/test/formsubmit.sjs37
-rw-r--r--toolkit/components/passwordmgr/test/mochitest.ini20
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs220
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/mochitest.ini69
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html218
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html117
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html143
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html115
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form.html44
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html72
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html167
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html109
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html187
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html105
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html177
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html859
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html164
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html55
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html213
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html145
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html56
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_case_differences.html147
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html137
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html170
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html52
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html147
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html183
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html191
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html121
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_input_events.html96
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html51
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html861
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html103
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_maxlength.html137
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html291
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html122
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt.html705
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html362
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html81
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html406
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html264
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html145
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_username_focus.html263
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html55
-rw-r--r--toolkit/components/passwordmgr/test/prompt_common.js79
-rw-r--r--toolkit/components/passwordmgr/test/pwmgr_common.js509
-rw-r--r--toolkit/components/passwordmgr/test/subtst_master_pass.html12
-rw-r--r--toolkit/components/passwordmgr/test/subtst_prompt_async.html12
-rw-r--r--toolkit/components/passwordmgr/test/test_master_password.html308
-rw-r--r--toolkit/components/passwordmgr/test/test_prompt_async.html540
-rw-r--r--toolkit/components/passwordmgr/test/test_xhr.html201
-rw-r--r--toolkit/components/passwordmgr/test/test_xml_load.html191
-rw-r--r--toolkit/components/passwordmgr/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/key3.dbbin0 -> 16384 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlitebin0 -> 8192 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlitebin0 -> 10240 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlitebin0 -> 12288 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlitebin0 -> 294912 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlitebin0 -> 327680 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlitebin0 -> 327680 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlitebin0 -> 8192 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlitebin0 -> 11264 bytes
-rw-r--r--toolkit/components/passwordmgr/test/unit/head.js135
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js75
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_context_menu.js165
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js284
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js196
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_getFormFields.js147
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js156
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js28
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js40
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js107
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_legacy_validation.js76
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_change.js384
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js77
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js284
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_logins_search.js221
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js169
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js243
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js206
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_notifications.js172
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_recipes_add.js177
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_recipes_content.js39
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js69
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js184
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_storage.js102
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js507
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_telemetry.js187
-rw-r--r--toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js488
-rw-r--r--toolkit/components/passwordmgr/test/unit/xpcshell.ini46
-rw-r--r--toolkit/components/perf/.eslintrc.js7
-rw-r--r--toolkit/components/perf/PerfMeasurement.cpp120
-rw-r--r--toolkit/components/perf/PerfMeasurement.h30
-rw-r--r--toolkit/components/perf/PerfMeasurement.jsm19
-rw-r--r--toolkit/components/perf/chrome.ini3
-rw-r--r--toolkit/components/perf/moz.build21
-rw-r--r--toolkit/components/perf/test_pm.xul48
-rw-r--r--toolkit/components/perfmonitoring/AddonWatcher.jsm239
-rw-r--r--toolkit/components/perfmonitoring/PerformanceStats-content.js144
-rw-r--r--toolkit/components/perfmonitoring/PerformanceStats.jsm1000
-rw-r--r--toolkit/components/perfmonitoring/PerformanceWatcher-content.js54
-rw-r--r--toolkit/components/perfmonitoring/PerformanceWatcher.jsm367
-rw-r--r--toolkit/components/perfmonitoring/README.md120
-rw-r--r--toolkit/components/perfmonitoring/moz.build35
-rw-r--r--toolkit/components/perfmonitoring/nsIPerformanceStats.idl333
-rw-r--r--toolkit/components/perfmonitoring/nsPerformanceStats.cpp1620
-rw-r--r--toolkit/components/perfmonitoring/nsPerformanceStats.h825
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser.ini15
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js151
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpibin0 -> 7848 bytes
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js105
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh4
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest1
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js23
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf30
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js91
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js25
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_compartments.html20
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_compartments.js312
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html12
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js29
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js111
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/head.js287
-rw-r--r--toolkit/components/perfmonitoring/tests/browser/install.rdf30
-rw-r--r--toolkit/components/places/BookmarkHTMLUtils.jsm1188
-rw-r--r--toolkit/components/places/BookmarkJSONUtils.jsm589
-rw-r--r--toolkit/components/places/Bookmarks.jsm1536
-rw-r--r--toolkit/components/places/ClusterLib.js248
-rw-r--r--toolkit/components/places/ColorAnalyzer.js90
-rw-r--r--toolkit/components/places/ColorAnalyzer_worker.js392
-rw-r--r--toolkit/components/places/ColorConversion.js64
-rw-r--r--toolkit/components/places/Database.cpp2333
-rw-r--r--toolkit/components/places/Database.h331
-rw-r--r--toolkit/components/places/ExtensionSearchHandler.jsm292
-rw-r--r--toolkit/components/places/FaviconHelpers.cpp934
-rw-r--r--toolkit/components/places/FaviconHelpers.h273
-rw-r--r--toolkit/components/places/Helpers.cpp395
-rw-r--r--toolkit/components/places/Helpers.h296
-rw-r--r--toolkit/components/places/History.cpp2977
-rw-r--r--toolkit/components/places/History.h224
-rw-r--r--toolkit/components/places/History.jsm1049
-rw-r--r--toolkit/components/places/PageIconProtocolHandler.js128
-rw-r--r--toolkit/components/places/PlaceInfo.cpp137
-rw-r--r--toolkit/components/places/PlaceInfo.h50
-rw-r--r--toolkit/components/places/PlacesBackups.jsm550
-rw-r--r--toolkit/components/places/PlacesCategoriesStarter.js110
-rw-r--r--toolkit/components/places/PlacesDBUtils.jsm1138
-rw-r--r--toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm148
-rw-r--r--toolkit/components/places/PlacesSearchAutocompleteProvider.jsm295
-rw-r--r--toolkit/components/places/PlacesSyncUtils.jsm1155
-rw-r--r--toolkit/components/places/PlacesTransactions.jsm1645
-rw-r--r--toolkit/components/places/PlacesUtils.jsm3863
-rw-r--r--toolkit/components/places/SQLFunctions.cpp941
-rw-r--r--toolkit/components/places/SQLFunctions.h394
-rw-r--r--toolkit/components/places/Shutdown.cpp233
-rw-r--r--toolkit/components/places/Shutdown.h171
-rw-r--r--toolkit/components/places/UnifiedComplete.js2149
-rw-r--r--toolkit/components/places/VisitInfo.cpp69
-rw-r--r--toolkit/components/places/VisitInfo.h37
-rw-r--r--toolkit/components/places/moz.build97
-rw-r--r--toolkit/components/places/mozIAsyncFavicons.idl174
-rw-r--r--toolkit/components/places/mozIAsyncHistory.idl188
-rw-r--r--toolkit/components/places/mozIAsyncLivemarks.idl190
-rw-r--r--toolkit/components/places/mozIColorAnalyzer.idl52
-rw-r--r--toolkit/components/places/mozIPlacesAutoComplete.idl138
-rw-r--r--toolkit/components/places/mozIPlacesPendingOperation.idl14
-rw-r--r--toolkit/components/places/nsAnnoProtocolHandler.cpp367
-rw-r--r--toolkit/components/places/nsAnnoProtocolHandler.h54
-rw-r--r--toolkit/components/places/nsAnnotationService.cpp1990
-rw-r--r--toolkit/components/places/nsAnnotationService.h161
-rw-r--r--toolkit/components/places/nsFaviconService.cpp716
-rw-r--r--toolkit/components/places/nsFaviconService.h147
-rw-r--r--toolkit/components/places/nsIAnnotationService.idl422
-rw-r--r--toolkit/components/places/nsIBrowserHistory.idl70
-rw-r--r--toolkit/components/places/nsIFaviconService.idl145
-rw-r--r--toolkit/components/places/nsINavBookmarksService.idl697
-rw-r--r--toolkit/components/places/nsINavHistoryService.idl1451
-rw-r--r--toolkit/components/places/nsITaggingService.idl95
-rw-r--r--toolkit/components/places/nsLivemarkService.js891
-rw-r--r--toolkit/components/places/nsMaybeWeakPtr.h145
-rw-r--r--toolkit/components/places/nsNavBookmarks.cpp2926
-rw-r--r--toolkit/components/places/nsNavBookmarks.h445
-rw-r--r--toolkit/components/places/nsNavHistory.cpp4523
-rw-r--r--toolkit/components/places/nsNavHistory.h659
-rw-r--r--toolkit/components/places/nsNavHistoryQuery.cpp1694
-rw-r--r--toolkit/components/places/nsNavHistoryQuery.h160
-rw-r--r--toolkit/components/places/nsNavHistoryResult.cpp4813
-rw-r--r--toolkit/components/places/nsNavHistoryResult.h782
-rw-r--r--toolkit/components/places/nsPIPlacesDatabase.idl52
-rw-r--r--toolkit/components/places/nsPlacesExpiration.js1105
-rw-r--r--toolkit/components/places/nsPlacesIndexes.h124
-rw-r--r--toolkit/components/places/nsPlacesMacros.h82
-rw-r--r--toolkit/components/places/nsPlacesModule.cpp70
-rw-r--r--toolkit/components/places/nsPlacesTables.h154
-rw-r--r--toolkit/components/places/nsPlacesTriggers.h267
-rw-r--r--toolkit/components/places/nsTaggingService.js709
-rw-r--r--toolkit/components/places/tests/.eslintrc.js9
-rw-r--r--toolkit/components/places/tests/PlacesTestUtils.jsm163
-rw-r--r--toolkit/components/places/tests/bookmarks/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/bookmarks/head_bookmarks.js20
-rw-r--r--toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js103
-rw-r--r--toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js112
-rw-r--r--toolkit/components/places/tests/bookmarks/test_1129529.js76
-rw-r--r--toolkit/components/places/tests/bookmarks/test_384228.js98
-rw-r--r--toolkit/components/places/tests/bookmarks/test_385829.js182
-rw-r--r--toolkit/components/places/tests/bookmarks/test_388695.js52
-rw-r--r--toolkit/components/places/tests/bookmarks/test_393498.js102
-rw-r--r--toolkit/components/places/tests/bookmarks/test_395101.js87
-rw-r--r--toolkit/components/places/tests/bookmarks/test_395593.js69
-rw-r--r--toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js221
-rw-r--r--toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js141
-rw-r--r--toolkit/components/places/tests/bookmarks/test_417228-other-roots.js158
-rw-r--r--toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js91
-rw-r--r--toolkit/components/places/tests/bookmarks/test_448584.js113
-rw-r--r--toolkit/components/places/tests/bookmarks/test_458683.js131
-rw-r--r--toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js124
-rw-r--r--toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js56
-rw-r--r--toolkit/components/places/tests/bookmarks/test_675416.js56
-rw-r--r--toolkit/components/places/tests/bookmarks/test_711914.js56
-rw-r--r--toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js59
-rw-r--r--toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js57
-rw-r--r--toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js57
-rw-r--r--toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js48
-rw-r--r--toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js37
-rw-r--r--toolkit/components/places/tests/bookmarks/test_async_observers.js177
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bmindex.js124
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks.js718
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js116
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js310
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js44
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js264
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js527
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js204
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js177
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_search.js223
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_update.js414
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js18
-rw-r--r--toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js68
-rw-r--r--toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js84
-rw-r--r--toolkit/components/places/tests/bookmarks/test_keywords.js310
-rw-r--r--toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js640
-rw-r--r--toolkit/components/places/tests/bookmarks/test_protectRoots.js37
-rw-r--r--toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js70
-rw-r--r--toolkit/components/places/tests/bookmarks/test_removeItem.js30
-rw-r--r--toolkit/components/places/tests/bookmarks/test_savedsearches.js209
-rw-r--r--toolkit/components/places/tests/bookmarks/xpcshell.ini50
-rw-r--r--toolkit/components/places/tests/browser/.eslintrc.js8
-rw-r--r--toolkit/components/places/tests/browser/399606-history.go-0.html11
-rw-r--r--toolkit/components/places/tests/browser/399606-httprefresh.html8
-rw-r--r--toolkit/components/places/tests/browser/399606-location.reload.html11
-rw-r--r--toolkit/components/places/tests/browser/399606-location.replace.html11
-rw-r--r--toolkit/components/places/tests/browser/399606-window.location.href.html11
-rw-r--r--toolkit/components/places/tests/browser/399606-window.location.html11
-rw-r--r--toolkit/components/places/tests/browser/461710_iframe.html8
-rw-r--r--toolkit/components/places/tests/browser/461710_link_page-2.html13
-rw-r--r--toolkit/components/places/tests/browser/461710_link_page-3.html13
-rw-r--r--toolkit/components/places/tests/browser/461710_link_page.html13
-rw-r--r--toolkit/components/places/tests/browser/461710_visited_page.html9
-rw-r--r--toolkit/components/places/tests/browser/begin.html10
-rw-r--r--toolkit/components/places/tests/browser/browser.ini26
-rw-r--r--toolkit/components/places/tests/browser/browser_bug248970.js152
-rw-r--r--toolkit/components/places/tests/browser/browser_bug399606.js77
-rw-r--r--toolkit/components/places/tests/browser/browser_bug461710.js82
-rw-r--r--toolkit/components/places/tests/browser/browser_bug646422.js51
-rw-r--r--toolkit/components/places/tests/browser/browser_bug680727.js109
-rw-r--r--toolkit/components/places/tests/browser/browser_colorAnalyzer.js259
-rw-r--r--toolkit/components/places/tests/browser/browser_double_redirect.js63
-rw-r--r--toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.js43
-rw-r--r--toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js152
-rw-r--r--toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js261
-rw-r--r--toolkit/components/places/tests/browser/browser_history_post.js23
-rw-r--r--toolkit/components/places/tests/browser/browser_notfound.js46
-rw-r--r--toolkit/components/places/tests/browser/browser_redirect.js61
-rw-r--r--toolkit/components/places/tests/browser/browser_settitle.js76
-rw-r--r--toolkit/components/places/tests/browser/browser_visited_notfound.js51
-rw-r--r--toolkit/components/places/tests/browser/browser_visituri.js84
-rw-r--r--toolkit/components/places/tests/browser/browser_visituri_nohistory.js42
-rw-r--r--toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.js73
-rw-r--r--toolkit/components/places/tests/browser/colorAnalyzer/category-discover.pngbin0 -> 1324 bytes
-rw-r--r--toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.pngbin0 -> 742 bytes
-rw-r--r--toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.pngbin0 -> 554 bytes
-rw-r--r--toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.pngbin0 -> 2410 bytes
-rw-r--r--toolkit/components/places/tests/browser/favicon-normal16.pngbin0 -> 286 bytes
-rw-r--r--toolkit/components/places/tests/browser/favicon-normal32.pngbin0 -> 344 bytes
-rw-r--r--toolkit/components/places/tests/browser/favicon.html13
-rw-r--r--toolkit/components/places/tests/browser/final.html10
-rw-r--r--toolkit/components/places/tests/browser/head.js319
-rw-r--r--toolkit/components/places/tests/browser/history_post.html12
-rw-r--r--toolkit/components/places/tests/browser/history_post.sjs6
-rw-r--r--toolkit/components/places/tests/browser/redirect-target.html1
-rw-r--r--toolkit/components/places/tests/browser/redirect.sjs14
-rw-r--r--toolkit/components/places/tests/browser/redirect_once.sjs9
-rw-r--r--toolkit/components/places/tests/browser/redirect_twice.sjs9
-rw-r--r--toolkit/components/places/tests/browser/title1.html12
-rw-r--r--toolkit/components/places/tests/browser/title2.html14
-rw-r--r--toolkit/components/places/tests/chrome/.eslintrc.js8
-rw-r--r--toolkit/components/places/tests/chrome/bad_links.atom74
-rw-r--r--toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul44
-rw-r--r--toolkit/components/places/tests/chrome/chrome.ini12
-rw-r--r--toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss18
-rw-r--r--toolkit/components/places/tests/chrome/link-less-items.rss19
-rw-r--r--toolkit/components/places/tests/chrome/rss_as_html.rss27
-rw-r--r--toolkit/components/places/tests/chrome/rss_as_html.rss^headers^2
-rw-r--r--toolkit/components/places/tests/chrome/sample_feed.atom23
-rw-r--r--toolkit/components/places/tests/chrome/test_303567.xul122
-rw-r--r--toolkit/components/places/tests/chrome/test_341972a.xul87
-rw-r--r--toolkit/components/places/tests/chrome/test_341972b.xul84
-rw-r--r--toolkit/components/places/tests/chrome/test_342484.xul88
-rw-r--r--toolkit/components/places/tests/chrome/test_371798.xul101
-rw-r--r--toolkit/components/places/tests/chrome/test_381357.xul85
-rw-r--r--toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul26
-rw-r--r--toolkit/components/places/tests/chrome/test_favicon_annotations.xul168
-rw-r--r--toolkit/components/places/tests/chrome/test_reloadLivemarks.xul155
-rw-r--r--toolkit/components/places/tests/cpp/mock_Link.h229
-rw-r--r--toolkit/components/places/tests/cpp/moz.build14
-rw-r--r--toolkit/components/places/tests/cpp/places_test_harness.h413
-rw-r--r--toolkit/components/places/tests/cpp/places_test_harness_tail.h149
-rw-r--r--toolkit/components/places/tests/cpp/test_IHistory.cpp639
-rw-r--r--toolkit/components/places/tests/expiration/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/expiration/head_expiration.js124
-rw-r--r--toolkit/components/places/tests/expiration/test_analyze_runs.js118
-rw-r--r--toolkit/components/places/tests/expiration/test_annos_expire_history.js93
-rw-r--r--toolkit/components/places/tests/expiration/test_annos_expire_never.js95
-rw-r--r--toolkit/components/places/tests/expiration/test_annos_expire_policy.js189
-rw-r--r--toolkit/components/places/tests/expiration/test_annos_expire_session.js83
-rw-r--r--toolkit/components/places/tests/expiration/test_clearHistory.js157
-rw-r--r--toolkit/components/places/tests/expiration/test_debug_expiration.js225
-rw-r--r--toolkit/components/places/tests/expiration/test_idle_daily.js21
-rw-r--r--toolkit/components/places/tests/expiration/test_notifications.js38
-rw-r--r--toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js114
-rw-r--r--toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js142
-rw-r--r--toolkit/components/places/tests/expiration/test_outdated_analyze.js72
-rw-r--r--toolkit/components/places/tests/expiration/test_pref_interval.js61
-rw-r--r--toolkit/components/places/tests/expiration/test_pref_maxpages.js124
-rw-r--r--toolkit/components/places/tests/expiration/xpcshell.ini22
-rw-r--r--toolkit/components/places/tests/favicons/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.pngbin0 -> 3105 bytes
-rw-r--r--toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.pngbin0 -> 563 bytes
-rw-r--r--toolkit/components/places/tests/favicons/expected-favicon-big48.ico.pngbin0 -> 1425 bytes
-rw-r--r--toolkit/components/places/tests/favicons/expected-favicon-big64.png.pngbin0 -> 3157 bytes
-rw-r--r--toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.pngbin0 -> 175 bytes
-rw-r--r--toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.pngbin0 -> 169 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-big16.icobin0 -> 1406 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-big32.jpgbin0 -> 3494 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-big4.jpgbin0 -> 4751 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-big48.icobin0 -> 56646 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-big64.pngbin0 -> 10698 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-normal16.pngbin0 -> 286 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-normal32.pngbin0 -> 344 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-scale160x3.jpgbin0 -> 5095 bytes
-rw-r--r--toolkit/components/places/tests/favicons/favicon-scale3x160.jpgbin0 -> 5059 bytes
-rw-r--r--toolkit/components/places/tests/favicons/head_favicons.js105
-rw-r--r--toolkit/components/places/tests/favicons/test_expireAllFavicons.js39
-rw-r--r--toolkit/components/places/tests/favicons/test_favicons_conversions.js131
-rw-r--r--toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js57
-rw-r--r--toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js51
-rw-r--r--toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js90
-rw-r--r--toolkit/components/places/tests/favicons/test_page-icon_protocol.js66
-rw-r--r--toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js74
-rw-r--r--toolkit/components/places/tests/favicons/test_replaceFaviconData.js264
-rw-r--r--toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js352
-rw-r--r--toolkit/components/places/tests/favicons/xpcshell.ini32
-rw-r--r--toolkit/components/places/tests/head_common.js869
-rw-r--r--toolkit/components/places/tests/history/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/history/head_history.js19
-rw-r--r--toolkit/components/places/tests/history/test_insert.js257
-rw-r--r--toolkit/components/places/tests/history/test_remove.js360
-rw-r--r--toolkit/components/places/tests/history/test_removeVisits.js316
-rw-r--r--toolkit/components/places/tests/history/test_removeVisitsByFilter.js345
-rw-r--r--toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js52
-rw-r--r--toolkit/components/places/tests/history/xpcshell.ini9
-rw-r--r--toolkit/components/places/tests/migration/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/migration/head_migration.js46
-rw-r--r--toolkit/components/places/tests/migration/places_v10.sqlitebin0 -> 172032 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v11.sqlitebin0 -> 1081344 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v17.sqlitebin0 -> 1212416 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v19.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v21.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v22.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v23.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v24.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v25.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v26.sqlitebin0 -> 1179648 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v27.sqlitebin0 -> 1212416 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v28.sqlitebin0 -> 1212416 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v29.sqlitebin0 -> 1245184 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v30.sqlitebin0 -> 1212416 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v31.sqlitebin0 -> 1146880 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v32.sqlitebin0 -> 1146880 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v33.sqlitebin0 -> 1146880 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v34.sqlitebin0 -> 1146880 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v35.sqlitebin0 -> 1146880 bytes
-rw-r--r--toolkit/components/places/tests/migration/places_v6.sqlitebin0 -> 155648 bytes
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_downgraded.js19
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v11.js48
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v19.js42
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v24.js36
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v25.js30
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v26.js98
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v27.js77
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v31.js46
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v34.js141
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js21
-rw-r--r--toolkit/components/places/tests/migration/test_current_from_v6.js38
-rw-r--r--toolkit/components/places/tests/migration/xpcshell.ini36
-rw-r--r--toolkit/components/places/tests/moz.build67
-rw-r--r--toolkit/components/places/tests/queries/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/queries/head_queries.js370
-rw-r--r--toolkit/components/places/tests/queries/readme.txt16
-rw-r--r--toolkit/components/places/tests/queries/test_415716.js108
-rw-r--r--toolkit/components/places/tests/queries/test_abstime-annotation-domain.js210
-rw-r--r--toolkit/components/places/tests/queries/test_abstime-annotation-uri.js162
-rw-r--r--toolkit/components/places/tests/queries/test_async.js371
-rw-r--r--toolkit/components/places/tests/queries/test_containersQueries_sorting.js411
-rw-r--r--toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js200
-rw-r--r--toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js210
-rw-r--r--toolkit/components/places/tests/queries/test_onlyBookmarked.js128
-rw-r--r--toolkit/components/places/tests/queries/test_queryMultipleFolder.js65
-rw-r--r--toolkit/components/places/tests/queries/test_querySerialization.js797
-rw-r--r--toolkit/components/places/tests/queries/test_redirects.js311
-rw-r--r--toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js127
-rw-r--r--toolkit/components/places/tests/queries/test_results-as-visit.js119
-rw-r--r--toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js84
-rw-r--r--toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js70
-rw-r--r--toolkit/components/places/tests/queries/test_searchterms-domain.js125
-rw-r--r--toolkit/components/places/tests/queries/test_searchterms-uri.js87
-rw-r--r--toolkit/components/places/tests/queries/test_sort-date-site-grouping.js225
-rw-r--r--toolkit/components/places/tests/queries/test_sorting.js1265
-rw-r--r--toolkit/components/places/tests/queries/test_tags.js743
-rw-r--r--toolkit/components/places/tests/queries/test_transitions.js178
-rw-r--r--toolkit/components/places/tests/queries/xpcshell.ini34
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml5
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml14
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js505
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_416211.js22
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_416214.js39
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_417798.js51
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_418257.js67
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_422277.js19
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js171
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js39
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js310
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js179
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js41
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_casing.js157
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js91
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js71
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js23
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_empty_search.js98
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_enabled.js68
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_escape_self.js31
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js384
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js24
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js73
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js149
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_keywords.js78
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js54
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js68
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_query_url.js68
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js203
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js51
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js45
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js49
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.js43
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js651
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_special_search.js447
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js153
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js164
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_trimming.js313
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_typed.js84
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_visit_url.js186
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js175
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js35
-rw-r--r--toolkit/components/places/tests/unifiedcomplete/xpcshell.ini49
-rw-r--r--toolkit/components/places/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.corrupt.html36
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.json1
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.preplaces.html35
-rw-r--r--toolkit/components/places/tests/unit/bookmarks_html_singleframe.html10
-rw-r--r--toolkit/components/places/tests/unit/bug476292.sqlitebin0 -> 139264 bytes
-rw-r--r--toolkit/components/places/tests/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--toolkit/components/places/tests/unit/default.sqlitebin0 -> 1081344 bytes
-rw-r--r--toolkit/components/places/tests/unit/head_bookmarks.js20
-rw-r--r--toolkit/components/places/tests/unit/livemark.xml17
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json1
-rw-r--r--toolkit/components/places/tests/unit/nsDummyObserver.js48
-rw-r--r--toolkit/components/places/tests/unit/nsDummyObserver.manifest4
-rw-r--r--toolkit/components/places/tests/unit/places.sparse.sqlitebin0 -> 221184 bytes
-rw-r--r--toolkit/components/places/tests/unit/test_000_frecency.js273
-rw-r--r--toolkit/components/places/tests/unit/test_1085291.js42
-rw-r--r--toolkit/components/places/tests/unit/test_1105208.js24
-rw-r--r--toolkit/components/places/tests/unit/test_1105866.js63
-rw-r--r--toolkit/components/places/tests/unit/test_317472.js65
-rw-r--r--toolkit/components/places/tests/unit/test_331487.js95
-rw-r--r--toolkit/components/places/tests/unit/test_384370.js173
-rw-r--r--toolkit/components/places/tests/unit/test_385397.js142
-rw-r--r--toolkit/components/places/tests/unit/test_399264_query_to_string.js51
-rw-r--r--toolkit/components/places/tests/unit/test_399264_string_to_query.js75
-rw-r--r--toolkit/components/places/tests/unit/test_399266.js78
-rw-r--r--toolkit/components/places/tests/unit/test_402799.js62
-rw-r--r--toolkit/components/places/tests/unit/test_405497.js57
-rw-r--r--toolkit/components/places/tests/unit/test_408221.js165
-rw-r--r--toolkit/components/places/tests/unit/test_412132.js136
-rw-r--r--toolkit/components/places/tests/unit/test_413784.js118
-rw-r--r--toolkit/components/places/tests/unit/test_415460.js43
-rw-r--r--toolkit/components/places/tests/unit/test_415757.js102
-rw-r--r--toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js143
-rw-r--r--toolkit/components/places/tests/unit/test_419731.js96
-rw-r--r--toolkit/components/places/tests/unit/test_419792_node_tags_property.js49
-rw-r--r--toolkit/components/places/tests/unit/test_425563.js74
-rw-r--r--toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js35
-rw-r--r--toolkit/components/places/tests/unit/test_433317_query_title_update.js38
-rw-r--r--toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js56
-rw-r--r--toolkit/components/places/tests/unit/test_452777.js36
-rw-r--r--toolkit/components/places/tests/unit/test_454977.js124
-rw-r--r--toolkit/components/places/tests/unit/test_463863.js60
-rw-r--r--toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js21
-rw-r--r--toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js129
-rw-r--r--toolkit/components/places/tests/unit/test_536081.js56
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js133
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js77
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js25
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js47
-rw-r--r--toolkit/components/places/tests/unit/test_adaptive.js406
-rw-r--r--toolkit/components/places/tests/unit/test_adaptive_bug527311.js141
-rw-r--r--toolkit/components/places/tests/unit/test_analyze.js28
-rw-r--r--toolkit/components/places/tests/unit/test_annotations.js363
-rw-r--r--toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js95
-rw-r--r--toolkit/components/places/tests/unit/test_async_history_api.js1118
-rw-r--r--toolkit/components/places/tests/unit/test_async_in_batchmode.js55
-rw-r--r--toolkit/components/places/tests/unit/test_async_transactions.js1739
-rw-r--r--toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js39
-rw-r--r--toolkit/components/places/tests/unit/test_bookmark_catobs.js57
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html.js385
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js143
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js57
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js32
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_json.js241
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js325
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js44
-rw-r--r--toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js79
-rw-r--r--toolkit/components/places/tests/unit/test_browserhistory.js129
-rw-r--r--toolkit/components/places/tests/unit/test_bug636917_isLivemark.js35
-rw-r--r--toolkit/components/places/tests/unit/test_childlessTags.js117
-rw-r--r--toolkit/components/places/tests/unit/test_corrupt_telemetry.js31
-rw-r--r--toolkit/components/places/tests/unit/test_crash_476292.js28
-rw-r--r--toolkit/components/places/tests/unit/test_database_replaceOnStartup.js46
-rw-r--r--toolkit/components/places/tests/unit/test_download_history.js283
-rw-r--r--toolkit/components/places/tests/unit/test_frecency.js294
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_observers.js84
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_zero_updated.js30
-rw-r--r--toolkit/components/places/tests/unit/test_getChildIndex.js69
-rw-r--r--toolkit/components/places/tests/unit/test_getPlacesInfo.js112
-rw-r--r--toolkit/components/places/tests/unit/test_history.js184
-rw-r--r--toolkit/components/places/tests/unit/test_history_autocomplete_tags.js185
-rw-r--r--toolkit/components/places/tests/unit/test_history_catobs.js55
-rw-r--r--toolkit/components/places/tests/unit/test_history_clear.js169
-rw-r--r--toolkit/components/places/tests/unit/test_history_notifications.js38
-rw-r--r--toolkit/components/places/tests/unit/test_history_observer.js215
-rw-r--r--toolkit/components/places/tests/unit/test_history_sidebar.js447
-rw-r--r--toolkit/components/places/tests/unit/test_hosts_triggers.js226
-rw-r--r--toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js292
-rw-r--r--toolkit/components/places/tests/unit/test_isPageInDB.js10
-rw-r--r--toolkit/components/places/tests/unit/test_isURIVisited.js84
-rw-r--r--toolkit/components/places/tests/unit/test_isvisited.js75
-rw-r--r--toolkit/components/places/tests/unit/test_keywords.js548
-rw-r--r--toolkit/components/places/tests/unit/test_lastModified.js34
-rw-r--r--toolkit/components/places/tests/unit/test_markpageas.js61
-rw-r--r--toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js514
-rw-r--r--toolkit/components/places/tests/unit/test_multi_queries.js53
-rw-r--r--toolkit/components/places/tests/unit/test_multi_word_tags.js150
-rw-r--r--toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js256
-rw-r--r--toolkit/components/places/tests/unit/test_null_interfaces.js98
-rw-r--r--toolkit/components/places/tests/unit/test_onItemChanged_tags.js52
-rw-r--r--toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js179
-rw-r--r--toolkit/components/places/tests/unit/test_placeURIs.js42
-rw-r--r--toolkit/components/places/tests/unit/test_placesTxn.js937
-rw-r--r--toolkit/components/places/tests/unit/test_preventive_maintenance.js1356
-rw-r--r--toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js50
-rw-r--r--toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js46
-rw-r--r--toolkit/components/places/tests/unit/test_promiseBookmarksTree.js256
-rw-r--r--toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js49
-rw-r--r--toolkit/components/places/tests/unit/test_result_sort.js139
-rw-r--r--toolkit/components/places/tests/unit/test_resultsAsVisit_details.js85
-rw-r--r--toolkit/components/places/tests/unit/test_sql_guid_functions.js106
-rw-r--r--toolkit/components/places/tests/unit/test_svg_favicon.js31
-rw-r--r--toolkit/components/places/tests/unit/test_sync_utils.js1150
-rw-r--r--toolkit/components/places/tests/unit/test_tag_autocomplete_search.js137
-rw-r--r--toolkit/components/places/tests/unit/test_tagging.js189
-rw-r--r--toolkit/components/places/tests/unit/test_telemetry.js166
-rw-r--r--toolkit/components/places/tests/unit/test_update_frecency_after_delete.js151
-rw-r--r--toolkit/components/places/tests/unit/test_utils_backups_create.js90
-rw-r--r--toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js180
-rw-r--r--toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js79
-rw-r--r--toolkit/components/places/tests/unit/test_visitsInDB.js12
-rw-r--r--toolkit/components/places/tests/unit/xpcshell.ini163
-rw-r--r--toolkit/components/places/toolkitplaces.manifest32
-rw-r--r--toolkit/components/printing/content/printPageSetup.js478
-rw-r--r--toolkit/components/printing/content/printPageSetup.xul234
-rw-r--r--toolkit/components/printing/content/printPreviewBindings.xml415
-rw-r--r--toolkit/components/printing/content/printPreviewProgress.js154
-rw-r--r--toolkit/components/printing/content/printPreviewProgress.xul42
-rw-r--r--toolkit/components/printing/content/printProgress.js282
-rw-r--r--toolkit/components/printing/content/printProgress.xul60
-rw-r--r--toolkit/components/printing/content/printUtils.js710
-rw-r--r--toolkit/components/printing/content/printdialog.js425
-rw-r--r--toolkit/components/printing/content/printdialog.xul126
-rw-r--r--toolkit/components/printing/content/printjoboptions.js401
-rw-r--r--toolkit/components/printing/content/printjoboptions.xul110
-rw-r--r--toolkit/components/printing/content/simplifyMode.css22
-rw-r--r--toolkit/components/printing/jar.mn22
-rw-r--r--toolkit/components/printing/moz.build14
-rw-r--r--toolkit/components/printing/tests/browser.ini2
-rw-r--r--toolkit/components/printing/tests/browser_page_change_print_original.js57
-rw-r--r--toolkit/components/privatebrowsing/PrivateBrowsing.manifest2
-rw-r--r--toolkit/components/privatebrowsing/PrivateBrowsingTrackingProtectionWhitelist.js68
-rw-r--r--toolkit/components/privatebrowsing/moz.build16
-rw-r--r--toolkit/components/privatebrowsing/nsIPrivateBrowsingTrackingProtectionWhitelist.idl46
-rw-r--r--toolkit/components/processsingleton/ContentProcessSingleton.js117
-rw-r--r--toolkit/components/processsingleton/MainProcessSingleton.js90
-rw-r--r--toolkit/components/processsingleton/ProcessSingleton.manifest7
-rw-r--r--toolkit/components/processsingleton/moz.build11
-rw-r--r--toolkit/components/promiseworker/PromiseWorker.jsm390
-rw-r--r--toolkit/components/promiseworker/moz.build18
-rw-r--r--toolkit/components/promiseworker/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/promiseworker/tests/xpcshell/data/chrome.manifest1
-rw-r--r--toolkit/components/promiseworker/tests/xpcshell/data/worker.js34
-rw-r--r--toolkit/components/promiseworker/tests/xpcshell/test_Promise.js117
-rw-r--r--toolkit/components/promiseworker/tests/xpcshell/xpcshell.ini9
-rw-r--r--toolkit/components/promiseworker/worker/PromiseWorker.js206
-rw-r--r--toolkit/components/promiseworker/worker/moz.build9
-rw-r--r--toolkit/components/prompts/content/commonDialog.css22
-rw-r--r--toolkit/components/prompts/content/commonDialog.js62
-rw-r--r--toolkit/components/prompts/content/commonDialog.xul97
-rw-r--r--toolkit/components/prompts/content/selectDialog.js67
-rw-r--r--toolkit/components/prompts/content/selectDialog.xul22
-rw-r--r--toolkit/components/prompts/content/tabprompts.css35
-rw-r--r--toolkit/components/prompts/content/tabprompts.xml352
-rw-r--r--toolkit/components/prompts/jar.mn12
-rw-r--r--toolkit/components/prompts/moz.build11
-rw-r--r--toolkit/components/prompts/src/CommonDialog.jsm308
-rw-r--r--toolkit/components/prompts/src/SharedPromptUtils.jsm157
-rw-r--r--toolkit/components/prompts/src/moz.build16
-rw-r--r--toolkit/components/prompts/src/nsPrompter.js958
-rw-r--r--toolkit/components/prompts/src/nsPrompter.manifest6
-rw-r--r--toolkit/components/prompts/test/.eslintrc.js7
-rw-r--r--toolkit/components/prompts/test/bug619644_inner.html7
-rw-r--r--toolkit/components/prompts/test/bug625187_iframe.html16
-rw-r--r--toolkit/components/prompts/test/chromeScript.js241
-rw-r--r--toolkit/components/prompts/test/mochitest.ini19
-rw-r--r--toolkit/components/prompts/test/prompt_common.js158
-rw-r--r--toolkit/components/prompts/test/test_bug619644.html76
-rw-r--r--toolkit/components/prompts/test/test_bug620145.html105
-rw-r--r--toolkit/components/prompts/test/test_dom_prompts.html208
-rw-r--r--toolkit/components/prompts/test/test_modal_prompts.html1184
-rw-r--r--toolkit/components/prompts/test/test_modal_select.html146
-rw-r--r--toolkit/components/prompts/test/test_subresources_prompts.html202
-rw-r--r--toolkit/components/protobuf/COPYING.txt33
-rw-r--r--toolkit/components/protobuf/README.txt25
-rw-r--r--toolkit/components/protobuf/m-c-changes.patch410
-rw-r--r--toolkit/components/protobuf/moz.build137
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor.cc5420
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor.h1691
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor.pb.cc9135
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor.pb.h6761
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor.proto687
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor_database.cc543
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/descriptor_database.h369
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/dynamic_message.cc764
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/dynamic_message.h148
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/extension_set.cc1663
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/extension_set.h1234
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/extension_set_heavy.cc734
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/generated_enum_reflection.h91
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.cc1683
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.h504
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/generated_message_util.cc65
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/generated_message_util.h113
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/coded_stream.cc914
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/coded_stream.h1220
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/coded_stream_inl.h69
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.cc325
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.h209
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/package_info.h54
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/printer.cc198
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/printer.h136
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/strtod.cc113
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/strtod.h50
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/tokenizer.cc1127
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/tokenizer.h402
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.cc57
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.h248
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.cc473
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.h358
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.cc405
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h355
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/message.cc358
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/message.h866
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/message_lite.cc335
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/message_lite.h247
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/package_info.h64
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/reflection_ops.cc269
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/reflection_ops.h81
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/repeated_field.cc87
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/repeated_field.h1603
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/service.cc46
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/service.h291
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h231
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h325
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h151
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_qnx.h146
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_atomicword_compat.h122
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h137
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_macosx.h225
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h313
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_pnacl.h73
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_solaris.h188
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_tsan.h219
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc137
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h293
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc112
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.h150
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/common.cc394
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/common.h1186
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/hash.h231
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/map_util.h771
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/once.cc99
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/once.h166
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/platform_macros.h103
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/shared_ptr.h470
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/stl_util.h121
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.cc174
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.h76
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/structurally_valid.cc536
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/strutil.cc1280
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/strutil.h563
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/substitute.cc134
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/substitute.h170
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/template_util.h138
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/stubs/type_traits.h334
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/text_format.cc1746
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/text_format.h473
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/unknown_field_set.cc265
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/unknown_field_set.h318
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/wire_format.cc1106
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/wire_format.h336
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/wire_format_lite.cc471
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/wire_format_lite.h662
-rw-r--r--toolkit/components/protobuf/src/google/protobuf/wire_format_lite_inl.h860
-rwxr-xr-xtoolkit/components/protobuf/upgrade_protobuf.sh71
-rw-r--r--toolkit/components/reader/.eslintrc.js199
-rw-r--r--toolkit/components/reader/AboutReader.jsm997
-rw-r--r--toolkit/components/reader/JSDOMParser.js1195
-rw-r--r--toolkit/components/reader/Readability.js1863
-rw-r--r--toolkit/components/reader/ReaderMode.jsm514
-rw-r--r--toolkit/components/reader/ReaderWorker.js50
-rw-r--r--toolkit/components/reader/ReaderWorker.jsm17
-rw-r--r--toolkit/components/reader/content/aboutReader.html74
-rw-r--r--toolkit/components/reader/content/aboutReader.js9
-rw-r--r--toolkit/components/reader/jar.mn7
-rw-r--r--toolkit/components/reader/moz.build26
-rw-r--r--toolkit/components/reader/test/browser.ini15
-rw-r--r--toolkit/components/reader/test/browser_bug1124271_readerModePinnedTab.js47
-rw-r--r--toolkit/components/reader/test/browser_readerMode.js220
-rw-r--r--toolkit/components/reader/test/browser_readerMode_hidden_nodes.js53
-rw-r--r--toolkit/components/reader/test/browser_readerMode_with_anchor.js21
-rw-r--r--toolkit/components/reader/test/head.js126
-rw-r--r--toolkit/components/reader/test/readerModeArticle.html25
-rw-r--r--toolkit/components/reader/test/readerModeArticleHiddenNodes.html22
-rw-r--r--toolkit/components/reflect/moz.build15
-rw-r--r--toolkit/components/reflect/reflect.cpp77
-rw-r--r--toolkit/components/reflect/reflect.h30
-rw-r--r--toolkit/components/reflect/reflect.jsm24
-rw-r--r--toolkit/components/remote/moz.build24
-rw-r--r--toolkit/components/remote/nsGTKRemoteService.cpp181
-rw-r--r--toolkit/components/remote/nsGTKRemoteService.h49
-rw-r--r--toolkit/components/remote/nsIRemoteService.idl45
-rw-r--r--toolkit/components/remote/nsXRemoteService.cpp324
-rw-r--r--toolkit/components/remote/nsXRemoteService.h62
-rw-r--r--toolkit/components/remotebrowserutils/RemoteWebNavigation.js139
-rw-r--r--toolkit/components/remotebrowserutils/moz.build12
-rw-r--r--toolkit/components/remotebrowserutils/remotebrowserutils.manifest2
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser.ini6
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js156
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/dummy_page.html7
-rw-r--r--toolkit/components/satchel/AutoCompletePopup.jsm317
-rw-r--r--toolkit/components/satchel/FormHistory.jsm1119
-rw-r--r--toolkit/components/satchel/FormHistoryStartup.js146
-rw-r--r--toolkit/components/satchel/formSubmitListener.js190
-rw-r--r--toolkit/components/satchel/jar.mn7
-rw-r--r--toolkit/components/satchel/moz.build44
-rw-r--r--toolkit/components/satchel/nsFormAutoComplete.js624
-rw-r--r--toolkit/components/satchel/nsFormAutoCompleteResult.jsm187
-rw-r--r--toolkit/components/satchel/nsFormFillController.cpp1382
-rw-r--r--toolkit/components/satchel/nsFormFillController.h125
-rw-r--r--toolkit/components/satchel/nsFormHistory.js894
-rw-r--r--toolkit/components/satchel/nsIFormAutoComplete.idl47
-rw-r--r--toolkit/components/satchel/nsIFormFillController.idl56
-rw-r--r--toolkit/components/satchel/nsIFormHistory.idl74
-rw-r--r--toolkit/components/satchel/nsIInputListAutoComplete.idl17
-rw-r--r--toolkit/components/satchel/nsInputListAutoComplete.js64
-rw-r--r--toolkit/components/satchel/satchel.manifest10
-rw-r--r--toolkit/components/satchel/test/.eslintrc.js7
-rw-r--r--toolkit/components/satchel/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/satchel/test/browser/browser.ini5
-rw-r--r--toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.js63
-rw-r--r--toolkit/components/satchel/test/mochitest.ini19
-rw-r--r--toolkit/components/satchel/test/parent_utils.js149
-rw-r--r--toolkit/components/satchel/test/satchel_common.js274
-rw-r--r--toolkit/components/satchel/test/subtst_form_submission_1.html38
-rw-r--r--toolkit/components/satchel/test/subtst_privbrowsing.html22
-rw-r--r--toolkit/components/satchel/test/test_bug_511615.html194
-rw-r--r--toolkit/components/satchel/test/test_bug_787624.html88
-rw-r--r--toolkit/components/satchel/test/test_datalist_with_caching.html139
-rw-r--r--toolkit/components/satchel/test/test_form_autocomplete.html1076
-rw-r--r--toolkit/components/satchel/test/test_form_autocomplete_with_list.html506
-rw-r--r--toolkit/components/satchel/test/test_form_submission.html537
-rw-r--r--toolkit/components/satchel/test/test_form_submission_cap.html85
-rw-r--r--toolkit/components/satchel/test/test_form_submission_cap2.html190
-rw-r--r--toolkit/components/satchel/test/test_password_autocomplete.html107
-rw-r--r--toolkit/components/satchel/test/test_popup_direction.html61
-rw-r--r--toolkit/components/satchel/test/test_popup_enter_event.html86
-rw-r--r--toolkit/components/satchel/test/unit/.eslintrc.js7
-rw-r--r--toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlitebin0 -> 98304 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_1000.sqlitebin0 -> 164864 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite1
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_apitest.sqlitebin0 -> 5120 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlitebin0 -> 72704 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v3.sqlitebin0 -> 5120 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v3v4.sqlitebin0 -> 6144 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v999a.sqlitebin0 -> 6144 bytes
-rw-r--r--toolkit/components/satchel/test/unit/formhistory_v999b.sqlitebin0 -> 4096 bytes
-rw-r--r--toolkit/components/satchel/test/unit/head_satchel.js102
-rw-r--r--toolkit/components/satchel/test/unit/perf_autocomplete.js140
-rw-r--r--toolkit/components/satchel/test/unit/test_async_expire.js168
-rw-r--r--toolkit/components/satchel/test/unit/test_autocomplete.js266
-rw-r--r--toolkit/components/satchel/test/unit/test_db_corrupt.js89
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v4.js60
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v4b.js58
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v999a.js75
-rw-r--r--toolkit/components/satchel/test/unit/test_db_update_v999b.js92
-rw-r--r--toolkit/components/satchel/test/unit/test_history_api.js457
-rw-r--r--toolkit/components/satchel/test/unit/test_notify.js158
-rw-r--r--toolkit/components/satchel/test/unit/test_previous_result.js25
-rw-r--r--toolkit/components/satchel/test/unit/xpcshell.ini26
-rw-r--r--toolkit/components/satchel/towel5
-rw-r--r--toolkit/components/search/SearchStaticData.jsm43
-rw-r--r--toolkit/components/search/SearchSuggestionController.jsm398
-rw-r--r--toolkit/components/search/moz.build33
-rw-r--r--toolkit/components/search/nsSearchService.js4789
-rw-r--r--toolkit/components/search/nsSearchSuggestions.js197
-rw-r--r--toolkit/components/search/nsSidebar.js66
-rw-r--r--toolkit/components/search/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/search/tests/xpcshell/data/chrome.manifest3
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-addon.xml8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-app.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-fr.xml12
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-override.xml8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-pref.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml6
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml5
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml10
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-update.xml10
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine.xml25
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine2.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineImages.xml22
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineMaker.sjs54
-rw-r--r--toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.icobin0 -> 901 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/install.rdf23
-rw-r--r--toolkit/components/search/tests/xpcshell/data/invalid-engine.xml1
-rw-r--r--toolkit/components/search/tests/xpcshell/data/langpack-metadata.json5
-rw-r--r--toolkit/components/search/tests/xpcshell/data/list.json7
-rw-r--r--toolkit/components/search/tests/xpcshell/data/metadata.json30
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search.json86
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search.sqlitebin0 -> 65536 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs78
-rw-r--r--toolkit/components/search/tests/xpcshell/data/searchTest.jarbin0 -> 1249 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/head_search.js544
-rw-r--r--toolkit/components/search/tests/xpcshell/test_645970.js22
-rw-r--r--toolkit/components/search/tests/xpcshell/test_SearchStaticData.js27
-rw-r--r--toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js34
-rw-r--r--toolkit/components/search/tests/xpcshell/test_addEngine_callback.js95
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async.js34
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_addon.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_distribution.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_migration.js27
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_profile_engine.js42
-rw-r--r--toolkit/components/search/tests/xpcshell/test_bug930456.js11
-rw-r--r--toolkit/components/search/tests/xpcshell/test_bug930456_child.js3
-rw-r--r--toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js31
-rw-r--r--toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js23
-rw-r--r--toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js25
-rw-r--r--toolkit/components/search/tests/xpcshell/test_defaultEngine.js51
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engineUpdate.js50
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_set_alias.js80
-rw-r--r--toolkit/components/search/tests/xpcshell/test_geodefaults.js253
-rw-r--r--toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js135
-rw-r--r--toolkit/components/search/tests/xpcshell/test_hidden.js93
-rw-r--r--toolkit/components/search/tests/xpcshell/test_identifiers.js56
-rw-r--r--toolkit/components/search/tests/xpcshell/test_init_async_multiple.js55
-rw-r--r--toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js68
-rw-r--r--toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/test_json_cache.js227
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location.js66
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_error.js30
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_funnelcake.js17
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_malformed_json.js57
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js24
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js30
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js30
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_partner.js16
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_sync.js101
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_timeout.js78
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js85
-rw-r--r--toolkit/components/search/tests/xpcshell/test_migration_langpack.js37
-rw-r--r--toolkit/components/search/tests/xpcshell/test_multipleIcons.js61
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nocache.js60
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nodb.js37
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js57
-rw-r--r--toolkit/components/search/tests/xpcshell/test_notifications.js72
-rw-r--r--toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js148
-rw-r--r--toolkit/components/search/tests/xpcshell/test_pref.js36
-rw-r--r--toolkit/components/search/tests/xpcshell/test_purpose.js70
-rw-r--r--toolkit/components/search/tests/xpcshell/test_rel_searchform.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js74
-rw-r--r--toolkit/components/search/tests/xpcshell/test_resultDomain.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js67
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchReset.js137
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest.js572
-rw-r--r--toolkit/components/search/tests/xpcshell/test_selectedEngine.js165
-rw-r--r--toolkit/components/search/tests/xpcshell/test_svg_icon.js52
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync.js27
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_addon.js26
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js26
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js52
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_distribution.js26
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_fallback.js42
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_migration.js29
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/test_update_telemetry.js36
-rw-r--r--toolkit/components/search/tests/xpcshell/xpcshell.ini102
-rw-r--r--toolkit/components/search/toolkitsearch.manifest10
-rw-r--r--toolkit/components/securityreporter/SecurityReporter.js112
-rw-r--r--toolkit/components/securityreporter/SecurityReporter.manifest2
-rw-r--r--toolkit/components/securityreporter/moz.build16
-rw-r--r--toolkit/components/securityreporter/nsISecurityReporter.idl14
-rw-r--r--toolkit/components/social/test/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/sqlite/moz.build11
-rw-r--r--toolkit/components/sqlite/sqlite_internal.js323
-rw-r--r--toolkit/components/sqlite/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/sqlite/tests/xpcshell/data/chrome.manifest1
-rw-r--r--toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_internal.js279
-rw-r--r--toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_shared.js39
-rw-r--r--toolkit/components/sqlite/tests/xpcshell/test_sqlite_internal.js43
-rw-r--r--toolkit/components/sqlite/tests/xpcshell/xpcshell.ini10
-rw-r--r--toolkit/components/startup/StartupTimeline.cpp36
-rw-r--r--toolkit/components/startup/StartupTimeline.h100
-rw-r--r--toolkit/components/startup/moz.build38
-rw-r--r--toolkit/components/startup/mozprofilerprobe.mof29
-rw-r--r--toolkit/components/startup/nsAppStartup.cpp1030
-rw-r--r--toolkit/components/startup/nsAppStartup.h76
-rw-r--r--toolkit/components/startup/nsUserInfo.h23
-rw-r--r--toolkit/components/startup/nsUserInfoMac.h25
-rw-r--r--toolkit/components/startup/nsUserInfoMac.mm84
-rw-r--r--toolkit/components/startup/nsUserInfoUnix.cpp167
-rw-r--r--toolkit/components/startup/nsUserInfoWin.cpp133
-rw-r--r--toolkit/components/startup/public/moz.build13
-rw-r--r--toolkit/components/startup/public/nsIAppStartup.idl195
-rw-r--r--toolkit/components/startup/public/nsIUserInfo.idl32
-rw-r--r--toolkit/components/startup/tests/browser/.eslintrc.js7
-rw-r--r--toolkit/components/startup/tests/browser/beforeunload.html10
-rw-r--r--toolkit/components/startup/tests/browser/browser.ini8
-rw-r--r--toolkit/components/startup/tests/browser/browser_bug511456.js47
-rw-r--r--toolkit/components/startup/tests/browser/browser_bug537449.js53
-rw-r--r--toolkit/components/startup/tests/browser/browser_crash_detection.js23
-rw-r--r--toolkit/components/startup/tests/browser/head.js29
-rw-r--r--toolkit/components/startup/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/startup/tests/unit/head_startup.js30
-rw-r--r--toolkit/components/startup/tests/unit/test_startup_crash.js300
-rw-r--r--toolkit/components/startup/tests/unit/xpcshell.ini6
-rw-r--r--toolkit/components/statusfilter/moz.build11
-rw-r--r--toolkit/components/statusfilter/nsBrowserStatusFilter.cpp392
-rw-r--r--toolkit/components/statusfilter/nsBrowserStatusFilter.h80
-rw-r--r--toolkit/components/telemetry/EventInfo.h52
-rw-r--r--toolkit/components/telemetry/Events.yaml68
-rw-r--r--toolkit/components/telemetry/GCTelemetry.jsm216
-rw-r--r--toolkit/components/telemetry/Histograms.json11002
-rw-r--r--toolkit/components/telemetry/Makefile.in17
-rw-r--r--toolkit/components/telemetry/ProcessedStack.h63
-rw-r--r--toolkit/components/telemetry/ScalarInfo.h27
-rw-r--r--toolkit/components/telemetry/Scalars.yaml298
-rw-r--r--toolkit/components/telemetry/Telemetry.cpp3076
-rw-r--r--toolkit/components/telemetry/Telemetry.h436
-rw-r--r--toolkit/components/telemetry/TelemetryArchive.jsm125
-rw-r--r--toolkit/components/telemetry/TelemetryCommon.cpp105
-rw-r--r--toolkit/components/telemetry/TelemetryCommon.h75
-rw-r--r--toolkit/components/telemetry/TelemetryComms.h84
-rw-r--r--toolkit/components/telemetry/TelemetryController.jsm954
-rw-r--r--toolkit/components/telemetry/TelemetryEnvironment.jsm1459
-rw-r--r--toolkit/components/telemetry/TelemetryEvent.cpp687
-rw-r--r--toolkit/components/telemetry/TelemetryEvent.h39
-rw-r--r--toolkit/components/telemetry/TelemetryHistogram.cpp2725
-rw-r--r--toolkit/components/telemetry/TelemetryHistogram.h104
-rw-r--r--toolkit/components/telemetry/TelemetryLog.jsm35
-rw-r--r--toolkit/components/telemetry/TelemetryReportingPolicy.jsm496
-rw-r--r--toolkit/components/telemetry/TelemetryScalar.cpp1896
-rw-r--r--toolkit/components/telemetry/TelemetryScalar.h64
-rw-r--r--toolkit/components/telemetry/TelemetrySend.jsm1114
-rw-r--r--toolkit/components/telemetry/TelemetrySession.jsm2124
-rw-r--r--toolkit/components/telemetry/TelemetryStartup.js49
-rw-r--r--toolkit/components/telemetry/TelemetryStartup.manifest4
-rw-r--r--toolkit/components/telemetry/TelemetryStopwatch.jsm335
-rw-r--r--toolkit/components/telemetry/TelemetryStorage.jsm1882
-rw-r--r--toolkit/components/telemetry/TelemetryTimestamps.jsm54
-rw-r--r--toolkit/components/telemetry/TelemetryUtils.jsm152
-rw-r--r--toolkit/components/telemetry/ThirdPartyCookieProbe.jsm181
-rw-r--r--toolkit/components/telemetry/ThreadHangStats.h230
-rw-r--r--toolkit/components/telemetry/UITelemetry.jsm235
-rw-r--r--toolkit/components/telemetry/WebrtcTelemetry.cpp112
-rw-r--r--toolkit/components/telemetry/WebrtcTelemetry.h43
-rw-r--r--toolkit/components/telemetry/datareporting-prefs.js12
-rw-r--r--toolkit/components/telemetry/docs/collection/custom-pings.rst74
-rw-r--r--toolkit/components/telemetry/docs/collection/histograms.rst5
-rw-r--r--toolkit/components/telemetry/docs/collection/index.rst35
-rw-r--r--toolkit/components/telemetry/docs/collection/measuring-time.rst74
-rw-r--r--toolkit/components/telemetry/docs/collection/scalars.rst140
-rw-r--r--toolkit/components/telemetry/docs/concepts/archiving.rst12
-rw-r--r--toolkit/components/telemetry/docs/concepts/crashes.rst23
-rw-r--r--toolkit/components/telemetry/docs/concepts/index.rst23
-rw-r--r--toolkit/components/telemetry/docs/concepts/pings.rst32
-rw-r--r--toolkit/components/telemetry/docs/concepts/sessions.rst40
-rw-r--r--toolkit/components/telemetry/docs/concepts/submission.rst34
-rw-r--r--toolkit/components/telemetry/docs/concepts/subsession_triggers.pngbin0 -> 1219295 bytes
-rw-r--r--toolkit/components/telemetry/docs/data/addons-malware-ping.rst42
-rw-r--r--toolkit/components/telemetry/docs/data/common-ping.rst42
-rw-r--r--toolkit/components/telemetry/docs/data/core-ping.rst191
-rw-r--r--toolkit/components/telemetry/docs/data/crash-ping.rst144
-rw-r--r--toolkit/components/telemetry/docs/data/deletion-ping.rst19
-rw-r--r--toolkit/components/telemetry/docs/data/environment.rst373
-rw-r--r--toolkit/components/telemetry/docs/data/heartbeat-ping.rst63
-rw-r--r--toolkit/components/telemetry/docs/data/index.rst18
-rw-r--r--toolkit/components/telemetry/docs/data/main-ping.rst609
-rw-r--r--toolkit/components/telemetry/docs/data/sync-ping.rst182
-rw-r--r--toolkit/components/telemetry/docs/data/uitour-ping.rst26
-rw-r--r--toolkit/components/telemetry/docs/fhr/architecture.rst226
-rw-r--r--toolkit/components/telemetry/docs/fhr/dataformat.rst1997
-rw-r--r--toolkit/components/telemetry/docs/fhr/identifiers.rst83
-rw-r--r--toolkit/components/telemetry/docs/fhr/index.rst34
-rw-r--r--toolkit/components/telemetry/docs/index.rst25
-rw-r--r--toolkit/components/telemetry/docs/internals/index.rst9
-rw-r--r--toolkit/components/telemetry/docs/internals/preferences.rst119
-rw-r--r--toolkit/components/telemetry/gen-event-data.py142
-rw-r--r--toolkit/components/telemetry/gen-event-enum.py73
-rw-r--r--toolkit/components/telemetry/gen-histogram-bucket-ranges.py52
-rw-r--r--toolkit/components/telemetry/gen-histogram-data.py178
-rw-r--r--toolkit/components/telemetry/gen-histogram-enum.py107
-rw-r--r--toolkit/components/telemetry/gen-scalar-data.py90
-rw-r--r--toolkit/components/telemetry/gen-scalar-enum.py56
-rw-r--r--toolkit/components/telemetry/healthreport-prefs.js10
-rw-r--r--toolkit/components/telemetry/histogram-whitelists.json1990
-rw-r--r--toolkit/components/telemetry/histogram_tools.py513
-rw-r--r--toolkit/components/telemetry/moz.build130
-rw-r--r--toolkit/components/telemetry/nsITelemetry.idl469
-rw-r--r--toolkit/components/telemetry/parse_events.py271
-rw-r--r--toolkit/components/telemetry/parse_scalars.py262
-rw-r--r--toolkit/components/telemetry/schemas/core.schema.json41
-rw-r--r--toolkit/components/telemetry/shared_telemetry_utils.py103
-rw-r--r--toolkit/components/telemetry/tests/addons/dictionary/install.rdf25
-rw-r--r--toolkit/components/telemetry/tests/addons/experiment/install.rdf16
-rw-r--r--toolkit/components/telemetry/tests/addons/extension-2/install.rdf16
-rw-r--r--toolkit/components/telemetry/tests/addons/extension/install.rdf16
-rw-r--r--toolkit/components/telemetry/tests/addons/long-fields/install.rdf24
-rw-r--r--toolkit/components/telemetry/tests/addons/restartless/install.rdf24
-rw-r--r--toolkit/components/telemetry/tests/addons/signed/META-INF/manifest.mf7
-rw-r--r--toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.rsabin0 -> 4190 bytes
-rw-r--r--toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.sf4
-rw-r--r--toolkit/components/telemetry/tests/addons/signed/install.rdf24
-rw-r--r--toolkit/components/telemetry/tests/addons/system/install.rdf24
-rw-r--r--toolkit/components/telemetry/tests/addons/theme/install.rdf16
-rw-r--r--toolkit/components/telemetry/tests/browser/browser.ini5
-rw-r--r--toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js193
-rw-r--r--toolkit/components/telemetry/tests/search/chrome.manifest3
-rw-r--r--toolkit/components/telemetry/tests/search/searchTest.jarbin0 -> 867 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm86
-rw-r--r--toolkit/components/telemetry/tests/unit/engine.xml7
-rw-r--r--toolkit/components/telemetry/tests/unit/head.js319
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildHistograms.js107
-rw-r--r--toolkit/components/telemetry/tests/unit/test_PingAPI.js502
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js236
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController.js507
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js70
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js70
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js73
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js1528
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js249
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js14
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js127
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js53
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLog.js51
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js268
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js574
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySend.js427
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js547
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession.js2029
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js156
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js77
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js102
-rw-r--r--toolkit/components/telemetry/tests/unit/test_nsITelemetry.js883
-rw-r--r--toolkit/components/telemetry/tests/unit/xpcshell.ini63
-rw-r--r--toolkit/components/terminator/moz.build22
-rw-r--r--toolkit/components/terminator/nsTerminator.cpp554
-rw-r--r--toolkit/components/terminator/nsTerminator.h44
-rw-r--r--toolkit/components/terminator/nsTerminatorTelemetry.js105
-rw-r--r--toolkit/components/terminator/terminator.manifest5
-rw-r--r--toolkit/components/terminator/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/terminator/tests/xpcshell/test_terminator_record.js108
-rw-r--r--toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js85
-rw-r--r--toolkit/components/terminator/tests/xpcshell/xpcshell.ini8
-rw-r--r--toolkit/components/thumbnails/BackgroundPageThumbs.jsm495
-rw-r--r--toolkit/components/thumbnails/BrowserPageThumbs.manifest2
-rw-r--r--toolkit/components/thumbnails/PageThumbUtils.jsm354
-rw-r--r--toolkit/components/thumbnails/PageThumbs.jsm901
-rw-r--r--toolkit/components/thumbnails/PageThumbsProtocol.js154
-rw-r--r--toolkit/components/thumbnails/PageThumbsWorker.js176
-rw-r--r--toolkit/components/thumbnails/content/backgroundPageThumbsContent.js205
-rw-r--r--toolkit/components/thumbnails/jar.mn6
-rw-r--r--toolkit/components/thumbnails/moz.build22
-rw-r--r--toolkit/components/thumbnails/test/.eslintrc.js8
-rw-r--r--toolkit/components/thumbnails/test/authenticate.sjs220
-rw-r--r--toolkit/components/thumbnails/test/background_red.html3
-rw-r--r--toolkit/components/thumbnails/test/background_red_redirect.sjs10
-rw-r--r--toolkit/components/thumbnails/test/background_red_scroll.html3
-rw-r--r--toolkit/components/thumbnails/test/browser.ini38
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_bad_url.js23
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_basic.js20
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js35
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_during_capture.js49
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_while_idle.js38
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_destroy_browser.js33
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_no_alert.js13
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_no_auth_prompt.js21
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_sent.js40
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js32
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_no_duplicates.js31
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_queueing.js38
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_redirect.js39
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bg_timeout.js24
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bug726727.js19
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js29
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js42
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_capture.js20
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_expiration.js97
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_privacy.js74
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_redirect.js30
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_storage.js112
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_storage_migrate3.js102
-rw-r--r--toolkit/components/thumbnails/test/browser_thumbnails_update.js169
-rw-r--r--toolkit/components/thumbnails/test/head.js356
-rw-r--r--toolkit/components/thumbnails/test/privacy_cache_control.sjs16
-rw-r--r--toolkit/components/thumbnails/test/test_thumbnails_interfaces.js31
-rw-r--r--toolkit/components/thumbnails/test/thumbnails_background.sjs79
-rw-r--r--toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js31
-rw-r--r--toolkit/components/thumbnails/test/thumbnails_update.sjs56
-rw-r--r--toolkit/components/thumbnails/test/xpcshell.ini6
-rw-r--r--toolkit/components/timermanager/moz.build21
-rw-r--r--toolkit/components/timermanager/nsIUpdateTimerManager.idl54
-rw-r--r--toolkit/components/timermanager/nsUpdateTimerManager.js339
-rw-r--r--toolkit/components/timermanager/nsUpdateTimerManager.manifest3
-rw-r--r--toolkit/components/timermanager/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/timermanager/tests/unit/consumerNotifications.js519
-rw-r--r--toolkit/components/timermanager/tests/unit/xpcshell.ini9
-rw-r--r--toolkit/components/tooltiptext/TooltipTextProvider.js148
-rw-r--r--toolkit/components/tooltiptext/TooltipTextProvider.manifest2
-rw-r--r--toolkit/components/tooltiptext/moz.build15
-rw-r--r--toolkit/components/tooltiptext/tests/browser.ini7
-rw-r--r--toolkit/components/tooltiptext/tests/browser_bug329212.js35
-rw-r--r--toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js19
-rw-r--r--toolkit/components/tooltiptext/tests/browser_bug561623.js24
-rw-r--r--toolkit/components/tooltiptext/tests/browser_bug581947.js87
-rw-r--r--toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js122
-rw-r--r--toolkit/components/tooltiptext/tests/title_test.svg59
-rw-r--r--toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml14
-rw-r--r--toolkit/components/typeaheadfind/content/notfound.wavbin0 -> 1422 bytes
-rw-r--r--toolkit/components/typeaheadfind/jar.mn6
-rw-r--r--toolkit/components/typeaheadfind/moz.build22
-rw-r--r--toolkit/components/typeaheadfind/nsITypeAheadFind.idl95
-rw-r--r--toolkit/components/typeaheadfind/nsTypeAheadFind.cpp1325
-rw-r--r--toolkit/components/typeaheadfind/nsTypeAheadFind.h134
-rw-r--r--toolkit/components/url-classifier/ChunkSet.cpp278
-rw-r--r--toolkit/components/url-classifier/ChunkSet.h99
-rw-r--r--toolkit/components/url-classifier/Classifier.cpp1281
-rw-r--r--toolkit/components/url-classifier/Classifier.h167
-rw-r--r--toolkit/components/url-classifier/Entries.h322
-rw-r--r--toolkit/components/url-classifier/HashStore.cpp1248
-rw-r--r--toolkit/components/url-classifier/HashStore.h314
-rw-r--r--toolkit/components/url-classifier/LookupCache.cpp599
-rw-r--r--toolkit/components/url-classifier/LookupCache.h213
-rw-r--r--toolkit/components/url-classifier/LookupCacheV4.cpp584
-rw-r--r--toolkit/components/url-classifier/LookupCacheV4.h70
-rw-r--r--toolkit/components/url-classifier/ProtocolParser.cpp1108
-rw-r--r--toolkit/components/url-classifier/ProtocolParser.h204
-rw-r--r--toolkit/components/url-classifier/RiceDeltaDecoder.cpp229
-rw-r--r--toolkit/components/url-classifier/RiceDeltaDecoder.h39
-rw-r--r--toolkit/components/url-classifier/SafeBrowsing.jsm429
-rw-r--r--toolkit/components/url-classifier/VariableLengthPrefixSet.cpp443
-rw-r--r--toolkit/components/url-classifier/VariableLengthPrefixSet.h70
-rw-r--r--toolkit/components/url-classifier/chromium/README.txt23
-rw-r--r--toolkit/components/url-classifier/chromium/safebrowsing.proto473
-rw-r--r--toolkit/components/url-classifier/content/listmanager.js601
-rw-r--r--toolkit/components/url-classifier/content/moz/alarm.js157
-rw-r--r--toolkit/components/url-classifier/content/moz/cryptohasher.js176
-rw-r--r--toolkit/components/url-classifier/content/moz/debug.js867
-rw-r--r--toolkit/components/url-classifier/content/moz/lang.js82
-rw-r--r--toolkit/components/url-classifier/content/moz/observer.js145
-rw-r--r--toolkit/components/url-classifier/content/moz/preferences.js276
-rw-r--r--toolkit/components/url-classifier/content/moz/protocol4.js133
-rw-r--r--toolkit/components/url-classifier/content/multi-querier.js137
-rw-r--r--toolkit/components/url-classifier/content/request-backoff.js116
-rw-r--r--toolkit/components/url-classifier/content/trtable.js169
-rw-r--r--toolkit/components/url-classifier/content/wireformat.js230
-rw-r--r--toolkit/components/url-classifier/content/xml-fetcher.js126
-rw-r--r--toolkit/components/url-classifier/moz.build86
-rw-r--r--toolkit/components/url-classifier/nsCheckSummedOutputStream.cpp59
-rw-r--r--toolkit/components/url-classifier/nsCheckSummedOutputStream.h55
-rw-r--r--toolkit/components/url-classifier/nsIUrlClassifierDBService.idl232
-rw-r--r--toolkit/components/url-classifier/nsIUrlClassifierHashCompleter.idl65
-rw-r--r--toolkit/components/url-classifier/nsIUrlClassifierPrefixSet.idl29
-rw-r--r--toolkit/components/url-classifier/nsIUrlClassifierStreamUpdater.idl39
-rw-r--r--toolkit/components/url-classifier/nsIUrlClassifierTable.idl31
-rw-r--r--toolkit/components/url-classifier/nsIUrlClassifierUtils.idl74
-rw-r--r--toolkit/components/url-classifier/nsIUrlListManager.idl67
-rw-r--r--toolkit/components/url-classifier/nsURLClassifier.manifest6
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierDBService.cpp1866
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierDBService.h270
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js589
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierLib.js52
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierListManager.js53
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierPrefixSet.cpp526
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierPrefixSet.h89
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierProxies.cpp356
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierProxies.h373
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp812
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.h103
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierUtils.cpp665
-rw-r--r--toolkit/components/url-classifier/nsUrlClassifierUtils.h99
-rw-r--r--toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc7166
-rw-r--r--toolkit/components/url-classifier/protobuf/safebrowsing.pb.h6283
-rw-r--r--toolkit/components/url-classifier/tests/UrlClassifierTestUtils.jsm98
-rw-r--r--toolkit/components/url-classifier/tests/gtest/Common.cpp78
-rw-r--r--toolkit/components/url-classifier/tests/gtest/Common.h26
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestChunkSet.cpp279
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestFailUpdate.cpp97
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestLookupCacheV4.cpp88
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestPerProviderDirectory.cpp98
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestProtocolParser.cpp159
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestRiceDeltaDecoder.cpp165
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestSafeBrowsingProtobuf.cpp24
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestSafebrowsingHash.cpp52
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestTable.cpp47
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestUrlClassifierTableUpdateV4.cpp755
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestUrlClassifierUtils.cpp276
-rw-r--r--toolkit/components/url-classifier/tests/gtest/TestVariableLengthPrefixSet.cpp559
-rw-r--r--toolkit/components/url-classifier/tests/gtest/moz.build27
-rw-r--r--toolkit/components/url-classifier/tests/jar.mn2
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/.eslintrc.js8
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/allowlistAnnotatedFrame.html144
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/bad.css1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/bad.css^headers^1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/basic.vtt27
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/basic.vtt^headers^1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/bug_1281083.html35
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/chrome.ini23
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedFrame.html213
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html24
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/classifierCommon.js112
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/classifierFrame.html57
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/classifierHelper.js201
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/cleanWorker.js10
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/dnt.html31
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/dnt.sjs9
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/evil.css1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/evil.css^headers^1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/evil.js1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/evil.js^headers^2
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/evilWorker.js3
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/gethash.sjs130
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/gethashFrame.html62
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/good.js1
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/import.css3
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/mochitest.ini39
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/ping.sjs16
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/raptor.jpgbin0 -> 49629 bytes
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/seek.webmbin0 -> 215529 bytes
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_allowlisted_annotations.html56
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_bug1254766.html305
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_classified_annotations.html50
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_classifier.html65
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_classifier_changetablepref.html149
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_classifier_worker.html76
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_classify_ping.html121
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_classify_track.html162
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_donottrack.html150
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_gethash.html157
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_lookup_system_principal.html29
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_privatebrowsing_trackingprotection.html154
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_safebrowsing_bug1272239.html87
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_bug1157081.html107
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_whitelist.html153
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/track.html7
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/unwantedWorker.js3
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/update.sjs114
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/vp9.webmbin0 -> 97465 bytes
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/whitelistFrame.html15
-rw-r--r--toolkit/components/url-classifier/tests/mochitest/workerFrame.html65
-rw-r--r--toolkit/components/url-classifier/tests/moz.build18
-rw-r--r--toolkit/components/url-classifier/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/url-classifier/tests/unit/data/digest1.chunkbin0 -> 939 bytes
-rw-r--r--toolkit/components/url-classifier/tests/unit/data/digest2.chunk2
-rw-r--r--toolkit/components/url-classifier/tests/unit/head_urlclassifier.js429
-rw-r--r--toolkit/components/url-classifier/tests/unit/tail_urlclassifier.js1
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_addsub.js488
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_backoff.js89
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_bug1274685_unowned_list.js32
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_dbservice.js314
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_digest256.js147
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_hashcompleter.js403
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_listmanager.js376
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_partial.js825
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_pref.js14
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_prefixset.js232
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_provider_url.js34
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_safebrowsing_protobuf.js23
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_streamupdater.js288
-rw-r--r--toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js37
-rw-r--r--toolkit/components/url-classifier/tests/unit/xpcshell.ini24
-rw-r--r--toolkit/components/url-classifier/tests/unittests.xul188
-rw-r--r--toolkit/components/urlformatter/api_keys.in4
-rw-r--r--toolkit/components/urlformatter/moz.build27
-rw-r--r--toolkit/components/urlformatter/nsIURLFormatter.idl50
-rw-r--r--toolkit/components/urlformatter/nsURLFormatter.js168
-rw-r--r--toolkit/components/urlformatter/nsURLFormatter.manifest2
-rw-r--r--toolkit/components/urlformatter/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/urlformatter/tests/unit/head_urlformatter.js16
-rw-r--r--toolkit/components/urlformatter/tests/unit/test_urlformatter.js65
-rw-r--r--toolkit/components/urlformatter/tests/unit/xpcshell.ini6
-rw-r--r--toolkit/components/utils/moz.build10
-rw-r--r--toolkit/components/utils/simpleServices.js313
-rw-r--r--toolkit/components/utils/utils.manifest6
-rw-r--r--toolkit/components/viewconfig/content/config.js635
-rw-r--r--toolkit/components/viewconfig/content/config.xul101
-rw-r--r--toolkit/components/viewconfig/jar.mn7
-rw-r--r--toolkit/components/viewconfig/moz.build7
-rw-r--r--toolkit/components/viewsource/ViewSourceBrowser.jsm331
-rw-r--r--toolkit/components/viewsource/content/viewPartialSource.js22
-rw-r--r--toolkit/components/viewsource/content/viewPartialSource.xul163
-rw-r--r--toolkit/components/viewsource/content/viewSource-content.js978
-rw-r--r--toolkit/components/viewsource/content/viewSource.css11
-rw-r--r--toolkit/components/viewsource/content/viewSource.js884
-rw-r--r--toolkit/components/viewsource/content/viewSource.xul235
-rw-r--r--toolkit/components/viewsource/content/viewSourceUtils.js524
-rw-r--r--toolkit/components/viewsource/jar.mn12
-rw-r--r--toolkit/components/viewsource/moz.build17
-rw-r--r--toolkit/components/viewsource/test/.eslintrc.js7
-rw-r--r--toolkit/components/viewsource/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/viewsource/test/browser/browser.ini12
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug464222.js12
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug699356.js19
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug713810.js23
-rw-r--r--toolkit/components/viewsource/test/browser/browser_contextmenu.js107
-rw-r--r--toolkit/components/viewsource/test/browser/browser_gotoline.js36
-rw-r--r--toolkit/components/viewsource/test/browser/browser_srcdoc.js30
-rw-r--r--toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js136
-rw-r--r--toolkit/components/viewsource/test/browser/file_bug464222.html1
-rw-r--r--toolkit/components/viewsource/test/browser/head.js200
-rw-r--r--toolkit/components/viewsource/test/chrome.ini4
-rw-r--r--toolkit/components/viewsource/test/file_empty.html1
-rw-r--r--toolkit/components/viewsource/test/test_bug428653.html45
-rw-r--r--toolkit/components/workerloader/moz.build14
-rw-r--r--toolkit/components/workerloader/require.js161
-rw-r--r--toolkit/components/workerloader/tests/.eslintrc.js7
-rw-r--r--toolkit/components/workerloader/tests/chrome.ini15
-rw-r--r--toolkit/components/workerloader/tests/moduleA-depends.js14
-rw-r--r--toolkit/components/workerloader/tests/moduleB-dependency.js11
-rw-r--r--toolkit/components/workerloader/tests/moduleC-circular.js18
-rw-r--r--toolkit/components/workerloader/tests/moduleD-circular.js11
-rw-r--r--toolkit/components/workerloader/tests/moduleE-throws-during-require.js10
-rw-r--r--toolkit/components/workerloader/tests/moduleF-syntax-error.js6
-rw-r--r--toolkit/components/workerloader/tests/moduleG-throws-later.js12
-rw-r--r--toolkit/components/workerloader/tests/moduleH-module-dot-exports.js12
-rw-r--r--toolkit/components/workerloader/tests/test_loading.xul41
-rw-r--r--toolkit/components/workerloader/tests/utils_mainthread.js34
-rw-r--r--toolkit/components/workerloader/tests/utils_worker.js32
-rw-r--r--toolkit/components/workerloader/tests/worker_handler.js34
-rw-r--r--toolkit/components/workerloader/tests/worker_test_loading.js121
-rw-r--r--toolkit/components/xulstore/XULStore.js336
-rw-r--r--toolkit/components/xulstore/XULStore.manifest2
-rw-r--r--toolkit/components/xulstore/moz.build19
-rw-r--r--toolkit/components/xulstore/nsIXULStore.idl75
-rw-r--r--toolkit/components/xulstore/tests/chrome/.eslintrc.js7
-rw-r--r--toolkit/components/xulstore/tests/chrome/animals.rdf142
-rw-r--r--toolkit/components/xulstore/tests/chrome/chrome.ini6
-rw-r--r--toolkit/components/xulstore/tests/chrome/test_persistence.xul31
-rw-r--r--toolkit/components/xulstore/tests/chrome/window_persistence.xul98
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/localstore.rdf31
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/test_XULStore.js199
-rw-r--r--toolkit/components/xulstore/tests/xpcshell/xpcshell.ini6
2774 files changed, 563329 insertions, 0 deletions
diff --git a/toolkit/components/.eslintrc.js b/toolkit/components/.eslintrc.js
new file mode 100644
index 0000000000..e6cf2032e8
--- /dev/null
+++ b/toolkit/components/.eslintrc.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = {
+ "rules": {
+ "no-unused-vars": ["error", {
+ "vars": "local",
+ "varsIgnorePattern": "^Cc|Ci|Cu|Cr|EXPORTED_SYMBOLS",
+ "args": "none",
+ }]
+ }
+};
diff --git a/toolkit/components/aboutcache/content/aboutCache.js b/toolkit/components/aboutcache/content/aboutCache.js
new file mode 100644
index 0000000000..07067cce36
--- /dev/null
+++ b/toolkit/components/aboutcache/content/aboutCache.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// First, parse and save the incoming arguments ("?storage=name&context=key")
+// Note: window.location.search doesn't work with nsSimpleURIs used for about:* addresses.
+var search = window.location.href.match(/^.*\?(.*)$/);
+var searchParams = new URLSearchParams(search ? search[1] : '');
+var storage = searchParams.get('storage');
+var cacheContext = searchParams.get('context');
+
+// The context is in a format as used by the HTTP cache v2 back end
+if (cacheContext)
+ var [context, isAnon, isInBrowser, appId, isPrivate] = cacheContext.match(/(a,)?(b,)?(i\d+,)?(p,)?/);
+if (appId)
+ appId = appId.match(/i(\d+),/)[1];
+
+
+function $(id) { return document.getElementById(id) || {}; }
+
+// Initialize the context UI controls at the start according what we got in the "context=" argument
+addEventListener('DOMContentLoaded', function() {
+ $('anon').checked = !!isAnon;
+ $('inbrowser').checked = !!isInBrowser;
+ $('appid').value = appId || '';
+ $('priv').checked = !!isPrivate;
+}, false);
+
+// When user presses the [Update] button, we build a new context key according the UI control
+// values and navigate to a new about:cache?storage=<name>&context=<key> URL.
+function navigate()
+{
+ context = '';
+ if ($('anon').checked)
+ context += 'a,';
+ if ($('inbrowser').checked)
+ context += 'b,';
+ if ($('appid').value)
+ context += 'i' + $('appid').value + ',';
+ if ($('priv').checked)
+ context += 'p,';
+
+ window.location.href = 'about:cache?storage=' + storage + '&context=' + context;
+}
diff --git a/toolkit/components/aboutcache/jar.mn b/toolkit/components/aboutcache/jar.mn
new file mode 100644
index 0000000000..b414a8f6d5
--- /dev/null
+++ b/toolkit/components/aboutcache/jar.mn
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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/aboutCache.js (content/aboutCache.js)
diff --git a/toolkit/components/aboutcache/moz.build b/toolkit/components/aboutcache/moz.build
new file mode 100644
index 0000000000..aac3a838c4
--- /dev/null
+++ b/toolkit/components/aboutcache/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/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.css b/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.css
new file mode 100644
index 0000000000..7f88612db1
--- /dev/null
+++ b/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.css
@@ -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/. */
+
+table.listing {
+ width: 100%;
+}
+
+table th, table td {
+ padding: 5px;
+ border: inset 2px black;
+ margin: 0px;
+ width: 50%;
+ vertical-align: top;
+}
+
+hr {
+ clear: both;
+ margin: 10px;
+}
+
+iframe {
+ width: 100%;
+ height: 900px;
+}
+
+#player, #raw {
+ width: 800px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+#controls {
+ text-align: center;
+}
+
+#canvas {
+ border: solid 1px black;
+}
+
+#active {
+ width: 100%;
+ border: solid 1px black;
+ margin-top: 0;
+}
+
+#trace {
+ width: 100%;
+}
diff --git a/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js b/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js
new file mode 100644
index 0000000000..c64a80a05f
--- /dev/null
+++ b/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 trace;
+var service;
+var reports;
+
+function onLoad() {
+ trace = document.getElementById('trace');
+ service = new CheckerboardReportService();
+ updateEnabled();
+ reports = service.getReports();
+ for (var i = 0; i < reports.length; i++) {
+ let text = "Severity " + reports[i].severity + " at " + new Date(reports[i].timestamp).toString();
+ let link = document.createElement('a');
+ link.href = 'javascript:showReport(' + i + ')';
+ link.textContent = text;
+ let bullet = document.createElement('li');
+ bullet.appendChild(link);
+ document.getElementById(reports[i].reason).appendChild(bullet);
+ }
+}
+
+function updateEnabled() {
+ let enabled = document.getElementById('enabled');
+ if (service.isRecordingEnabled()) {
+ enabled.textContent = 'enabled';
+ enabled.style.color = 'green';
+ } else {
+ enabled.textContent = 'disabled';
+ enabled.style.color = 'red';
+ }
+}
+
+function toggleEnabled() {
+ service.setRecordingEnabled(!service.isRecordingEnabled());
+ updateEnabled();
+}
+
+function flushReports() {
+ service.flushActiveReports();
+}
+
+function showReport(index) {
+ trace.value = reports[index].log;
+ loadData();
+}
+
+// -- Code to load and render the trace --
+
+const CANVAS_USE_RATIO = 0.75;
+const FRAME_INTERVAL_MS = 50;
+const VECTOR_NORMALIZED_MAGNITUDE = 30.0;
+
+var renderData = new Array();
+var currentFrame = 0;
+var playing = false;
+var timerId = 0;
+
+var minX = undefined;
+var minY = undefined;
+var maxX = undefined;
+var maxY = undefined;
+
+function log(x) {
+ if (console) {
+ console.log(x);
+ }
+}
+
+function getFlag(flag) {
+ return document.getElementById(flag).checked;
+}
+
+// parses the lines in the textarea, ignoring anything that doesn't have RENDERTRACE.
+// for each matching line, tokenizes on whitespace and ignores all tokens prior to
+// RENDERTRACE. Additional info can be included at the end of the line, and will be
+// displayed but not parsed. Allowed syntaxes:
+// <junk> RENDERTRACE <timestamp> rect <color> <x> <y> <width> <height> [extraInfo]
+function loadData() {
+ stopPlay();
+ renderData = new Array();
+ currentFrame = 0;
+ minX = undefined;
+ minY = undefined;
+ maxX = undefined;
+ maxY = undefined;
+
+ var charPos = 0;
+ var lastLineLength = 0;
+ var lines = trace.value.split(/\r|\n/);
+ for (var i = 0; i < lines.length; i++) {
+ charPos += lastLineLength;
+ lastLineLength = lines[i].length + 1;
+ // skip lines without RENDERTRACE
+ if (! /RENDERTRACE/.test(lines[i])) {
+ continue;
+ }
+
+ var tokens = lines[i].split(/\s+/);
+ var j = 0;
+ // skip tokens until RENDERTRACE
+ while (j < tokens.length && tokens[j++] != "RENDERTRACE"); // empty loop body
+ if (j >= tokens.length - 2) {
+ log("Error parsing line: " + lines[i]);
+ continue;
+ }
+
+ var timestamp = tokens[j++];
+ var destIndex = renderData.length;
+ if (destIndex == 0) {
+ // create the initial frame
+ renderData.push({
+ timestamp: timestamp,
+ rects: {},
+ });
+ } else if (renderData[destIndex - 1].timestamp == timestamp) {
+ // timestamp hasn't changed use, so update the previous object
+ destIndex--;
+ } else {
+ // clone a new copy of the last frame and update timestamp
+ renderData.push(JSON.parse(JSON.stringify(renderData[destIndex - 1])));
+ renderData[destIndex].timestamp = timestamp;
+ }
+
+ switch (tokens[j++]) {
+ case "rect":
+ if (j > tokens.length - 5) {
+ log("Error parsing line: " + lines[i]);
+ continue;
+ }
+
+ var rect = {};
+ var color = tokens[j++];
+ renderData[destIndex].rects[color] = rect;
+ rect.x = parseFloat(tokens[j++]);
+ rect.y = parseFloat(tokens[j++]);
+ rect.width = parseFloat(tokens[j++]);
+ rect.height = parseFloat(tokens[j++]);
+ rect.dataText = trace.value.substring(charPos, charPos + lines[i].length);
+
+ if (!getFlag('excludePageFromZoom') || color != 'brown') {
+ if (typeof minX == "undefined") {
+ minX = rect.x;
+ minY = rect.y;
+ maxX = rect.x + rect.width;
+ maxY = rect.y + rect.height;
+ } else {
+ minX = Math.min(minX, rect.x);
+ minY = Math.min(minY, rect.y);
+ maxX = Math.max(maxX, rect.x + rect.width);
+ maxY = Math.max(maxY, rect.y + rect.height);
+ }
+ }
+ break;
+
+ default:
+ log("Error parsing line " + lines[i]);
+ break;
+ }
+ }
+
+ if (! renderFrame()) {
+ alert("No data found; nothing to render!");
+ }
+}
+
+// render the current frame (i.e. renderData[currentFrame])
+// returns false if currentFrame is out of bounds, true otherwise
+function renderFrame() {
+ var frame = currentFrame;
+ if (frame < 0 || frame >= renderData.length) {
+ log("Invalid frame index");
+ return false;
+ }
+
+ var canvas = document.getElementById('canvas');
+ if (! canvas.getContext) {
+ log("No canvas context");
+ }
+
+ var context = canvas.getContext('2d');
+
+ // midpoint of the bounding box
+ var midX = (minX + maxX) / 2.0;
+ var midY = (minY + maxY) / 2.0;
+
+ // midpoint of the canvas
+ var cmx = canvas.width / 2.0;
+ var cmy = canvas.height / 2.0;
+
+ // scale factor
+ var scale = CANVAS_USE_RATIO * Math.min(canvas.width / (maxX - minX), canvas.height / (maxY - minY));
+
+ function projectX(value) {
+ return cmx + ((value - midX) * scale);
+ }
+
+ function projectY(value) {
+ return cmy + ((value - midY) * scale);
+ }
+
+ function drawRect(color, rect) {
+ context.strokeStyle = color;
+ context.strokeRect(
+ projectX(rect.x),
+ projectY(rect.y),
+ rect.width * scale,
+ rect.height * scale);
+ }
+
+ // clear canvas
+ context.fillStyle = 'white';
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ var activeData = '';
+ // draw rects
+ for (var i in renderData[frame].rects) {
+ drawRect(i, renderData[frame].rects[i]);
+ activeData += "\n" + renderData[frame].rects[i].dataText;
+ }
+ // draw timestamp and frame counter
+ context.fillStyle = 'black';
+ context.fillText((frame + 1) + "/" + renderData.length + ": " + renderData[frame].timestamp, 5, 15);
+
+ document.getElementById('active').textContent = activeData;
+
+ return true;
+}
+
+// -- Player controls --
+
+function reset(beginning) {
+ currentFrame = (beginning ? 0 : renderData.length - 1);
+ renderFrame();
+}
+
+function step(backwards) {
+ if (playing) {
+ togglePlay();
+ }
+ currentFrame += (backwards ? -1 : 1);
+ if (! renderFrame()) {
+ currentFrame -= (backwards ? -1 : 1);
+ }
+}
+
+function pause() {
+ clearInterval(timerId);
+ playing = false;
+}
+
+function togglePlay() {
+ if (playing) {
+ pause();
+ } else {
+ timerId = setInterval(function() {
+ currentFrame++;
+ if (! renderFrame()) {
+ currentFrame--;
+ togglePlay();
+ }
+ }, FRAME_INTERVAL_MS);
+ playing = true;
+ }
+}
+
+function stopPlay() {
+ if (playing) {
+ togglePlay();
+ }
+ currentFrame = 0;
+ renderFrame();
+}
diff --git a/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.xhtml b/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.xhtml
new file mode 100644
index 0000000000..6a8a618967
--- /dev/null
+++ b/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.xhtml
@@ -0,0 +1,55 @@
+<?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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width"/>
+ <link rel="stylesheet" href="chrome://global/content/aboutCheckerboard.css" type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://global/content/aboutCheckerboard.js"></script>
+ </head>
+
+ <body onload="onLoad()">
+ <p>Checkerboard recording is <span id="enabled" style="color: red">undetermined</span>.
+ <button onclick="toggleEnabled()">Toggle it!</button>.</p>
+ <p>If there are active reports in progress, you can stop and flush them by clicking here:
+ <button onclick="flushReports()">Flush active reports</button></p>
+ <table class="listing" cellspacing="0">
+ <tr>
+ <th>Most severe checkerboarding reports</th>
+ <th>Most recent checkerboarding reports</th>
+ </tr>
+ <tr>
+ <td><ul id="severe"></ul></td>
+ <td><ul id="recent"></ul></td>
+ </tr>
+ </table>
+
+ <hr/>
+
+ <div id="player">
+ <div id="controls">
+ <button onclick="reset(true)">&#171;</button><!-- rewind button -->
+ <button onclick="step(true)">&lt;</button><!-- step back button -->
+ <button onclick="togglePlay()">|| &#9654;</button><!-- pause button -->
+ <button onclick="stopPlay()">&#9744;</button><!-- stop button -->
+ <button onclick="step(false)">&gt;</button><!-- step forward button -->
+ <button onclick="reset(false)">&#187;</button><!-- forward button -->
+ </div>
+ <canvas id="canvas" width="800" height="600">Canvas not supported!</canvas>
+ <pre id="active">(Details for currently visible replay frame)</pre>
+ </div>
+
+ <hr/>
+
+ <div id="raw">
+ Raw log:<br/>
+ <textarea id="trace" rows="10"></textarea>
+ <div>
+ <input type="checkbox" id="excludePageFromZoom" onclick="loadData()"/><label for="excludePageFromZoom">Exclude page coordinates from zoom calculations</label><br/>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/toolkit/components/aboutcheckerboard/jar.mn b/toolkit/components/aboutcheckerboard/jar.mn
new file mode 100644
index 0000000000..64d5bfc8e5
--- /dev/null
+++ b/toolkit/components/aboutcheckerboard/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/.
+
+toolkit.jar:
+ content/global/aboutCheckerboard.js (content/aboutCheckerboard.js)
+ content/global/aboutCheckerboard.xhtml (content/aboutCheckerboard.xhtml)
+ content/global/aboutCheckerboard.css (content/aboutCheckerboard.css)
diff --git a/toolkit/components/aboutcheckerboard/moz.build b/toolkit/components/aboutcheckerboard/moz.build
new file mode 100644
index 0000000000..91d6e9662c
--- /dev/null
+++ b/toolkit/components/aboutcheckerboard/moz.build
@@ -0,0 +1,10 @@
+# -*- 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']
+
+with Files('**'):
+ BUG_COMPONENT = ('Core', 'Panning and Zooming')
diff --git a/toolkit/components/aboutmemory/content/aboutCompartments.xhtml b/toolkit/components/aboutmemory/content/aboutCompartments.xhtml
new file mode 100644
index 0000000000..83432295a3
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutCompartments.xhtml
@@ -0,0 +1,16 @@
+<?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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>about:compartments</title>
+ <meta name="viewport" content="width=device-width"/>
+ </head>
+
+ <body>about:compartments no longer exists. The lists of compartments and
+ ghost windows can now be found in the "Other Measurements" section of
+ about:memory.</body>
+</html>
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.css b/toolkit/components/aboutmemory/content/aboutMemory.css
new file mode 100644
index 0000000000..b63bbac13c
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 version used for mobile is located at
+ * mobile/android/themes/core/aboutMemory.css.
+ * Desktop-specific stuff is at the bottom of this file.
+ */
+
+html {
+ background: -moz-Dialog;
+ font: message-box;
+}
+
+body {
+ padding: 0 2em;
+ margin: 0;
+ min-width: 45em;
+ margin: auto;
+}
+
+div.ancillary {
+ margin: 0.5em 0;
+ -moz-user-select: none;
+}
+
+div.section {
+ padding: 2em;
+ margin: 1em 0em;
+ border: 1px solid ThreeDShadow;
+ border-radius: 10px;
+ background: -moz-Field;
+}
+
+div.opsRow {
+ padding: 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ border: 1px solid ThreeDShadow;
+ border-radius: 10px;
+ background: -moz-Field;
+ display: inline-block;
+}
+
+div.opsRowLabel {
+ display: block;
+ margin-bottom: 0.2em;
+ font-weight: bold;
+}
+
+.opsRowLabel label {
+ margin-left: 1em;
+ font-weight: normal;
+}
+
+div.non-verbose pre.entries {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+}
+
+h1 {
+ padding: 0;
+ margin: 0;
+ display: inline; /* allow subsequent text to the right of the heading */
+}
+
+h2 {
+ background: #ddd;
+ padding-left: .1em;
+}
+
+h3 {
+ display: inline; /* allow subsequent text to the right of the heading */
+}
+
+a.upDownArrow {
+ font-size: 130%;
+ text-decoration: none;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.accuracyWarning {
+ color: #d22;
+}
+
+.badInputWarning {
+ color: #f00;
+}
+
+.treeline {
+ color: #888;
+}
+
+.mrValue {
+ font-weight: bold;
+ color: #400;
+}
+
+.mrPerc {
+}
+
+.mrSep {
+}
+
+.mrName {
+ color: #004;
+}
+
+.mrNote {
+ color: #604;
+}
+
+.hasKids {
+ cursor: pointer;
+}
+
+.hasKids:hover {
+ text-decoration: underline;
+}
+
+.noselect {
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.option {
+ font-size: 80%;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.legend {
+ font-size: 80%;
+ -moz-user-select: none; /* no need to include this when cutting+pasting */
+}
+
+.debug {
+ font-size: 80%;
+}
+
+.hidden {
+ display: none;
+}
+
+.invalid {
+ color: #fff;
+ background-color: #f00;
+}
+
+/* Desktop-specific parts go here. */
+
+.hasKids:hover {
+ text-decoration: underline;
+}
+
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.js b/toolkit/components/aboutmemory/content/aboutMemory.js
new file mode 100644
index 0000000000..c62416dc54
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutMemory.js
@@ -0,0 +1,2042 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// You can direct about:memory to immediately load memory reports from a file
+// by providing a file= query string. For example,
+//
+// about:memory?file=/home/username/reports.json.gz
+//
+// "file=" is not case-sensitive. We'll URI-unescape the contents of the
+// "file=" argument, and obviously the filename is case-sensitive iff you're on
+// a case-sensitive filesystem. If you specify more than one "file=" argument,
+// only the first one is used.
+
+"use strict";
+
+// ---------------------------------------------------------------------------
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var CC = Components.Constructor;
+
+const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+
+const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
+const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "nsBinaryStream",
+ () => CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"));
+XPCOMUtils.defineLazyGetter(this, "nsFile",
+ () => CC("@mozilla.org/file/local;1",
+ "nsIFile", "initWithPath"));
+XPCOMUtils.defineLazyGetter(this, "nsGzipConverter",
+ () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
+ "nsIStreamConverter"));
+
+var gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(Ci.nsIMemoryReporterManager);
+
+const gPageName = 'about:memory';
+document.title = gPageName;
+
+const gUnnamedProcessStr = "Main Process";
+
+var gIsDiff = false;
+
+// ---------------------------------------------------------------------------
+
+// Forward slashes in URLs in paths are represented with backslashes to avoid
+// being mistaken for path separators. Paths/names where this hasn't been
+// undone are prefixed with "unsafe"; the rest are prefixed with "safe".
+function flipBackslashes(aUnsafeStr)
+{
+ // Save memory by only doing the replacement if it's necessary.
+ return (aUnsafeStr.indexOf('\\') === -1)
+ ? aUnsafeStr
+ : aUnsafeStr.replace(/\\/g, '/');
+}
+
+const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: ";
+
+// This is used for things that should never fail, and indicate a defect in
+// this file if they do.
+function assert(aCond, aMsg)
+{
+ if (!aCond) {
+ reportAssertionFailure(aMsg)
+ throw new Error(gAssertionFailureMsgPrefix + aMsg);
+ }
+}
+
+// This is used for malformed input from memory reporters.
+function assertInput(aCond, aMsg)
+{
+ if (!aCond) {
+ throw new Error("Invalid memory report(s): " + aMsg);
+ }
+}
+
+function handleException(ex)
+{
+ let str = "" + ex;
+ if (str.startsWith(gAssertionFailureMsgPrefix)) {
+ // Argh, assertion failure within this file! Give up.
+ throw ex;
+ } else {
+ // File or memory reporter problem. Print a message.
+ updateMainAndFooter(str, HIDE_FOOTER, "badInputWarning");
+ }
+}
+
+function reportAssertionFailure(aMsg)
+{
+ let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+ if (debug.isDebugBuild) {
+ debug.assertion(aMsg, "false", "aboutMemory.js", 0);
+ }
+}
+
+function debug(x)
+{
+ let section = appendElement(document.body, 'div', 'section');
+ appendElementWithText(section, "div", "debug", JSON.stringify(x));
+}
+
+// ---------------------------------------------------------------------------
+
+function onUnload()
+{
+}
+
+// ---------------------------------------------------------------------------
+
+// The <div> holding everything but the header and footer (if they're present).
+// It's what is updated each time the page changes.
+var gMain;
+
+// The <div> holding the footer.
+var gFooter;
+
+// The "verbose" checkbox.
+var gVerbose;
+
+// The "anonymize" checkbox.
+var gAnonymize;
+
+// Values for the |aFooterAction| argument to updateTitleMainAndFooter.
+var HIDE_FOOTER = 0;
+var SHOW_FOOTER = 1;
+
+function updateTitleMainAndFooter(aTitleNote, aMsg, aFooterAction, aClassName)
+{
+ document.title = gPageName;
+ if (aTitleNote) {
+ document.title += " (" + aTitleNote + ")";
+ }
+
+ // Clear gMain by replacing it with an empty node.
+ let tmp = gMain.cloneNode(false);
+ gMain.parentNode.replaceChild(tmp, gMain);
+ gMain = tmp;
+
+ gMain.classList.remove('hidden');
+ gMain.classList.remove('verbose');
+ gMain.classList.remove('non-verbose');
+ if (gVerbose) {
+ gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose');
+ }
+
+ let msgElement;
+ if (aMsg) {
+ let className = "section"
+ if (aClassName) {
+ className = className + " " + aClassName;
+ }
+ msgElement = appendElementWithText(gMain, 'div', className, aMsg);
+ }
+
+ switch (aFooterAction) {
+ case HIDE_FOOTER: gFooter.classList.add('hidden'); break;
+ case SHOW_FOOTER: gFooter.classList.remove('hidden'); break;
+ default: assert(false, "bad footer action in updateTitleMainAndFooter");
+ }
+ return msgElement;
+}
+
+function updateMainAndFooter(aMsg, aFooterAction, aClassName)
+{
+ return updateTitleMainAndFooter("", aMsg, aFooterAction, aClassName);
+}
+
+function appendTextNode(aP, aText)
+{
+ let e = document.createTextNode(aText);
+ aP.appendChild(e);
+ return e;
+}
+
+function appendElement(aP, aTagName, aClassName)
+{
+ let e = document.createElement(aTagName);
+ if (aClassName) {
+ e.className = aClassName;
+ }
+ aP.appendChild(e);
+ return e;
+}
+
+function appendElementWithText(aP, aTagName, aClassName, aText)
+{
+ let e = appendElement(aP, aTagName, aClassName);
+ // Setting textContent clobbers existing children, but there are none. More
+ // importantly, it avoids creating a JS-land object for the node, saving
+ // memory.
+ e.textContent = aText;
+ return e;
+}
+
+// ---------------------------------------------------------------------------
+
+const explicitTreeDescription =
+"This tree covers explicit memory allocations by the application. It includes \
+\n\n\
+* allocations made at the operating system level (via calls to functions such as \
+VirtualAlloc, vm_allocate, and mmap), \
+\n\n\
+* allocations made at the heap allocation level (via functions such as malloc, \
+calloc, realloc, memalign, operator new, and operator new[]) that have not been \
+explicitly decommitted (i.e. evicted from memory and swap), and \
+\n\n\
+* where possible, the overhead of the heap allocator itself.\
+\n\n\
+It excludes memory that is mapped implicitly such as code and data segments, \
+and thread stacks. \
+\n\n\
+'explicit' is not guaranteed to cover every explicit allocation, but it does cover \
+most (including the entire heap), and therefore it is the single best number to \
+focus on when trying to reduce memory usage.";
+
+// ---------------------------------------------------------------------------
+
+function appendButton(aP, aTitle, aOnClick, aText, aId)
+{
+ let b = appendElementWithText(aP, "button", "", aText);
+ b.title = aTitle;
+ b.onclick = aOnClick;
+ if (aId) {
+ b.id = aId;
+ }
+ return b;
+}
+
+function appendHiddenFileInput(aP, aId, aChangeListener)
+{
+ let input = appendElementWithText(aP, "input", "hidden", "");
+ input.type = "file";
+ input.id = aId; // used in testing
+ input.addEventListener("change", aChangeListener);
+ return input;
+}
+
+function onLoad()
+{
+ // Generate the header.
+
+ let header = appendElement(document.body, "div", "ancillary");
+
+ // A hidden file input element that can be invoked when necessary.
+ let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
+ let file = this.files[0];
+ let filename = file.mozFullPath;
+ updateAboutMemoryFromFile(filename);
+ });
+
+ // Ditto.
+ let fileInput2 =
+ appendHiddenFileInput(header, "fileInput2", function(e) {
+ let file = this.files[0];
+ // First time around, we stash a copy of the filename and reinvoke. Second
+ // time around we do the diff and display.
+ if (!this.filename1) {
+ this.filename1 = file.mozFullPath;
+
+ // e.skipClick is only true when testing -- it allows fileInput2's
+ // onchange handler to be re-called without having to go via the file
+ // picker.
+ if (!e.skipClick) {
+ this.click();
+ }
+ } else {
+ let filename1 = this.filename1;
+ delete this.filename1;
+ updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
+ }
+ });
+
+ const CuDesc = "Measure current memory reports and show.";
+ const LdDesc = "Load memory reports from file and show.";
+ const DfDesc = "Load memory report data from two files and show the " +
+ "difference.";
+
+ const SvDesc = "Save memory reports to file.";
+
+ const GCDesc = "Do a global garbage collection.";
+ const CCDesc = "Do a cycle collection.";
+ const MMDesc = "Send three \"heap-minimize\" notifications in a " +
+ "row. Each notification triggers a global garbage " +
+ "collection followed by a cycle collection, and causes the " +
+ "process to reduce memory usage in other ways, e.g. by " +
+ "flushing various caches.";
+
+ const GCAndCCLogDesc = "Save garbage collection log and concise cycle " +
+ "collection log.\n" +
+ "WARNING: These logs may be large (>1GB).";
+ const GCAndCCAllLogDesc = "Save garbage collection log and verbose cycle " +
+ "collection log.\n" +
+ "WARNING: These logs may be large (>1GB).";
+
+ const DMDEnabledDesc = "Analyze memory reports coverage and save the " +
+ "output to the temp directory.\n";
+ const DMDDisabledDesc = "DMD is not running. Please re-start with $DMD and " +
+ "the other relevant environment variables set " +
+ "appropriately.";
+
+ let ops = appendElement(header, "div", "");
+
+ let row1 = appendElement(ops, "div", "opsRow");
+
+ let labelDiv1 =
+ appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
+ let label1 = appendElementWithText(labelDiv1, "label", "");
+ gVerbose = appendElement(label1, "input", "");
+ gVerbose.type = "checkbox";
+ gVerbose.id = "verbose"; // used for testing
+ appendTextNode(label1, "verbose");
+
+ const kEllipsis = "\u2026";
+
+ // The "measureButton" id is used for testing.
+ appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
+ appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
+ appendButton(row1, DfDesc, () => fileInput2.click(),
+ "Load and diff" + kEllipsis);
+
+ let row2 = appendElement(ops, "div", "opsRow");
+
+ let labelDiv2 =
+ appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
+ appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
+
+ // XXX: this isn't a great place for this checkbox, but I can't think of
+ // anywhere better.
+ let label2 = appendElementWithText(labelDiv2, "label", "");
+ gAnonymize = appendElement(label2, "input", "");
+ gAnonymize.type = "checkbox";
+ appendTextNode(label2, "anonymize");
+
+ let row3 = appendElement(ops, "div", "opsRow");
+
+ appendElementWithText(row3, "div", "opsRowLabel", "Free memory");
+ appendButton(row3, GCDesc, doGC, "GC");
+ appendButton(row3, CCDesc, doCC, "CC");
+ appendButton(row3, MMDesc, doMMU, "Minimize memory usage");
+
+ let row4 = appendElement(ops, "div", "opsRow");
+
+ appendElementWithText(row4, "div", "opsRowLabel", "Save GC & CC logs");
+ appendButton(row4, GCAndCCLogDesc,
+ saveGCLogAndConciseCCLog, "Save concise", 'saveLogsConcise');
+ appendButton(row4, GCAndCCAllLogDesc,
+ saveGCLogAndVerboseCCLog, "Save verbose", 'saveLogsVerbose');
+
+ // Three cases here:
+ // - DMD is disabled (i.e. not built): don't show the button.
+ // - DMD is enabled but is not running: show the button, but disable it.
+ // - DMD is enabled and is running: show the button and enable it.
+ if (gMgr.isDMDEnabled) {
+ let row5 = appendElement(ops, "div", "opsRow");
+
+ appendElementWithText(row5, "div", "opsRowLabel", "Save DMD output");
+ let enableButtons = gMgr.isDMDRunning;
+
+ let dmdButton =
+ appendButton(row5, enableButtons ? DMDEnabledDesc : DMDDisabledDesc,
+ doDMD, "Save");
+ dmdButton.disabled = !enableButtons;
+ }
+
+ // Generate the main div, where content ("section" divs) will go. It's
+ // hidden at first.
+
+ gMain = appendElement(document.body, 'div', '');
+ gMain.id = 'mainDiv';
+
+ // Generate the footer. It's hidden at first.
+
+ gFooter = appendElement(document.body, 'div', 'ancillary hidden');
+
+ let a = appendElementWithText(gFooter, "a", "option",
+ "Troubleshooting information");
+ a.href = "about:support";
+
+ let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " +
+ "or collapse ('--') its children.";
+ let legendText2 = "Hover the pointer over the name of a memory report " +
+ "to see a description of what it measures.";
+
+ appendElementWithText(gFooter, "div", "legend", legendText1);
+ appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
+
+ // See if we're loading from a file. (Because about:memory is a non-standard
+ // URL, location.search is undefined, so we have to use location.href
+ // instead.)
+ let search = location.href.split('?')[1];
+ if (search) {
+ let searchSplit = search.split('&');
+ for (let i = 0; i < searchSplit.length; i++) {
+ if (searchSplit[i].toLowerCase().startsWith('file=')) {
+ let filename = searchSplit[i].substring('file='.length);
+ updateAboutMemoryFromFile(decodeURIComponent(filename));
+ return;
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+function doGC()
+{
+ Services.obs.notifyObservers(null, "child-gc-request", null);
+ Cu.forceGC();
+ updateMainAndFooter("Garbage collection completed", HIDE_FOOTER);
+}
+
+function doCC()
+{
+ Services.obs.notifyObservers(null, "child-cc-request", null);
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .cycleCollect();
+ updateMainAndFooter("Cycle collection completed", HIDE_FOOTER);
+}
+
+function doMMU()
+{
+ Services.obs.notifyObservers(null, "child-mmu-request", null);
+ gMgr.minimizeMemoryUsage(
+ () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER));
+}
+
+function doMeasure()
+{
+ updateAboutMemoryFromReporters();
+}
+
+function saveGCLogAndConciseCCLog()
+{
+ dumpGCLogAndCCLog(false);
+}
+
+function saveGCLogAndVerboseCCLog()
+{
+ dumpGCLogAndCCLog(true);
+}
+
+function doDMD()
+{
+ updateMainAndFooter("Saving memory reports and DMD output...", HIDE_FOOTER);
+ try {
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
+ .getService(Ci.nsIMemoryInfoDumper);
+
+ dumper.dumpMemoryInfoToTempDir(/* identifier = */ "",
+ gAnonymize.checked,
+ /* minimize = */ false);
+ updateMainAndFooter("Saved memory reports and DMD reports analysis " +
+ "to the temp directory",
+ HIDE_FOOTER);
+ } catch (ex) {
+ updateMainAndFooter(ex.toString(), HIDE_FOOTER);
+ }
+}
+
+function dumpGCLogAndCCLog(aVerbose)
+{
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
+ .getService(Ci.nsIMemoryInfoDumper);
+
+ let inProgress = updateMainAndFooter("Saving logs...", HIDE_FOOTER);
+ let section = appendElement(gMain, 'div', "section");
+
+ function displayInfo(gcLog, ccLog, isParent) {
+ appendElementWithText(section, 'div', "",
+ "Saved GC log to " + gcLog.path);
+
+ let ccLogType = aVerbose ? "verbose" : "concise";
+ appendElementWithText(section, 'div', "",
+ "Saved " + ccLogType + " CC log to " + ccLog.path);
+ }
+
+ dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ true,
+ { onDump: displayInfo,
+ onFinish: function() {
+ inProgress.remove();
+ }
+ });
+}
+
+/**
+ * Top-level function that does the work of generating the page from the memory
+ * reporters.
+ */
+function updateAboutMemoryFromReporters()
+{
+ updateMainAndFooter("Measuring...", HIDE_FOOTER);
+
+ try {
+ let processLiveMemoryReports =
+ function(aHandleReport, aDisplayReports) {
+ let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
+ aAmount, aDescription) {
+ aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
+ aDescription, /* presence = */ undefined);
+ }
+
+ let displayReportsAndFooter = function() {
+ updateTitleMainAndFooter("live measurement", "", SHOW_FOOTER);
+ aDisplayReports();
+ }
+
+ gMgr.getReports(handleReport, null, displayReportsAndFooter, null,
+ gAnonymize.checked);
+ }
+
+ // Process the reports from the live memory reporters.
+ appendAboutMemoryMain(processLiveMemoryReports,
+ gMgr.hasMozMallocUsableSize);
+
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+// Increment this if the JSON format changes.
+//
+var gCurrentFileFormatVersion = 1;
+
+
+/**
+ * Parse a string as JSON and extract the |memory_report| property if it has
+ * one, which indicates the string is from a crash dump.
+ *
+ * @param aStr
+ * The string.
+ * @return The extracted object.
+ */
+function parseAndUnwrapIfCrashDump(aStr) {
+ let obj = JSON.parse(aStr);
+ if (obj.memory_report !== undefined) {
+ // It looks like a crash dump. The memory reports should be in the
+ // |memory_report| property.
+ obj = obj.memory_report;
+ }
+ return obj;
+}
+
+/**
+ * Populate about:memory using the data in the given JSON object.
+ *
+ * @param aObj
+ * An object that (hopefully!) conforms to the JSON schema used by
+ * nsIMemoryInfoDumper.
+ */
+function updateAboutMemoryFromJSONObject(aObj)
+{
+ try {
+ assertInput(aObj.version === gCurrentFileFormatVersion,
+ "data version number missing or doesn't match");
+ assertInput(aObj.hasMozMallocUsableSize !== undefined,
+ "missing 'hasMozMallocUsableSize' property");
+ assertInput(aObj.reports && aObj.reports instanceof Array,
+ "missing or non-array 'reports' property");
+
+ let processMemoryReportsFromFile =
+ function(aHandleReport, aDisplayReports) {
+ for (let i = 0; i < aObj.reports.length; i++) {
+ let r = aObj.reports[i];
+
+ // A hack: for a brief time (late in the FF26 and early in the FF27
+ // cycle) we were dumping memory report files that contained reports
+ // whose path began with "redundant/". Such reports were ignored by
+ // about:memory. These reports are no longer produced, but some older
+ // builds are still floating around and producing files that contain
+ // them, so we need to still handle them (i.e. ignore them). This hack
+ // can be removed once FF26 and associated products (e.g. B2G 1.2) are
+ // no longer in common use.
+ if (!r.path.startsWith("redundant/")) {
+ aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
+ r.description, r._presence);
+ }
+ }
+ aDisplayReports();
+ }
+ appendAboutMemoryMain(processMemoryReportsFromFile,
+ aObj.hasMozMallocUsableSize);
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+/**
+ * Populate about:memory using the data in the given JSON string.
+ *
+ * @param aStr
+ * A string containing JSON data conforming to the schema used by
+ * nsIMemoryReporterManager::dumpReports.
+ */
+function updateAboutMemoryFromJSONString(aStr)
+{
+ try {
+ let obj = parseAndUnwrapIfCrashDump(aStr);
+ updateAboutMemoryFromJSONObject(obj);
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+/**
+ * Loads the contents of a file into a string and passes that to a callback.
+ *
+ * @param aFilename
+ * The name of the file being read from.
+ * @param aTitleNote
+ * A description to put in the page title upon completion.
+ * @param aFn
+ * The function to call and pass the read string to upon completion.
+ */
+function loadMemoryReportsFromFile(aFilename, aTitleNote, aFn)
+{
+ updateMainAndFooter("Loading...", HIDE_FOOTER);
+
+ try {
+ let reader = new FileReader();
+ reader.onerror = () => { throw new Error("FileReader.onerror"); };
+ reader.onabort = () => { throw new Error("FileReader.onabort"); };
+ reader.onload = (aEvent) => {
+ // Clear "Loading..." from above.
+ updateTitleMainAndFooter(aTitleNote, "", SHOW_FOOTER);
+ aFn(aEvent.target.result);
+ };
+
+ // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
+ if (!aFilename.endsWith(".gz")) {
+ reader.readAsText(File.createFromFileName(aFilename));
+ return;
+ }
+
+ // Read compressed gzip file.
+ let converter = new nsGzipConverter();
+ converter.asyncConvertData("gzip", "uncompressed", {
+ data: [],
+ onStartRequest: function(aR, aC) {},
+ onDataAvailable: function(aR, aC, aStream, aO, aCount) {
+ let bi = new nsBinaryStream(aStream);
+ this.data.push(bi.readBytes(aCount));
+ },
+ onStopRequest: function(aR, aC, aStatusCode) {
+ try {
+ if (!Components.isSuccessCode(aStatusCode)) {
+ throw new Components.Exception("Error while reading gzip file", aStatusCode);
+ }
+ reader.readAsText(new Blob(this.data));
+ } catch (ex) {
+ handleException(ex);
+ }
+ }
+ }, null);
+
+ let file = new nsFile(aFilename);
+ let fileChan = NetUtil.newChannel({
+ uri: Services.io.newFileURI(file),
+ loadUsingSystemPrincipal: true
+ });
+ fileChan.asyncOpen2(converter);
+
+ } catch (ex) {
+ handleException(ex);
+ }
+}
+
+/**
+ * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
+ * of the memory reporters.
+ *
+ * @param aFilename
+ * The name of the file being read from. The expected format of the
+ * file's contents is described in a comment in nsIMemoryInfoDumper.idl.
+ */
+function updateAboutMemoryFromFile(aFilename)
+{
+ loadMemoryReportsFromFile(aFilename, /* title note */ aFilename,
+ updateAboutMemoryFromJSONString);
+}
+
+/**
+ * Like updateAboutMemoryFromFile(), but gets its data from a two files and
+ * diffs them.
+ *
+ * @param aFilename1
+ * The name of the first file being read from.
+ * @param aFilename2
+ * The name of the first file being read from.
+ */
+function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2)
+{
+ let titleNote = "diff of " + aFilename1 + " and " + aFilename2;
+ loadMemoryReportsFromFile(aFilename1, titleNote, function(aStr1) {
+ loadMemoryReportsFromFile(aFilename2, titleNote, function(aStr2) {
+ try {
+ let obj1 = parseAndUnwrapIfCrashDump(aStr1);
+ let obj2 = parseAndUnwrapIfCrashDump(aStr2);
+ gIsDiff = true;
+ updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
+ gIsDiff = false;
+ } catch (ex) {
+ handleException(ex);
+ }
+ });
+ });
+}
+
+// ---------------------------------------------------------------------------
+
+// Something unlikely to appear in a process name.
+var kProcessPathSep = "^:^:^";
+
+// Short for "diff report".
+function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence)
+{
+ this._kind = aKind;
+ this._units = aUnits;
+ this._amount = aAmount;
+ this._description = aDescription;
+ this._nMerged = aNMerged;
+ if (aPresence !== undefined) {
+ this._presence = aPresence;
+ }
+}
+
+DReport.prototype = {
+ assertCompatible: function(aKind, aUnits)
+ {
+ assert(this._kind == aKind, "Mismatched kinds");
+ assert(this._units == aUnits, "Mismatched units");
+
+ // We don't check that the "description" properties match. This is because
+ // on Linux we can get cases where the paths are the same but the
+ // descriptions differ, like this:
+ //
+ // "path": "size/other-files/icon-theme.cache/[r--p]",
+ // "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
+ //
+ // "path": "size/other-files/icon-theme.cache/[r--p]"
+ // "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
+ //
+ // In those cases, we just use the description from the first-encountered
+ // one, which is what about:memory also does.
+ // (Note: reports with those paths are no longer generated, but allowing
+ // the descriptions to differ seems reasonable.)
+ },
+
+ merge: function(aJr) {
+ this.assertCompatible(aJr.kind, aJr.units);
+ this._amount += aJr.amount;
+ this._nMerged++;
+ },
+
+ toJSON: function(aProcess, aPath, aAmount) {
+ return {
+ process: aProcess,
+ path: aPath,
+ kind: this._kind,
+ units: this._units,
+ amount: aAmount,
+ description: this._description,
+ _presence: this._presence
+ };
+ }
+};
+
+// Constants that indicate if a DReport was present only in one of the data
+// sets, or had to be added for balance.
+DReport.PRESENT_IN_FIRST_ONLY = 1;
+DReport.PRESENT_IN_SECOND_ONLY = 2;
+DReport.ADDED_FOR_BALANCE = 3;
+
+/**
+ * Make a report map, which has combined path+process strings for keys, and
+ * DReport objects for values.
+ *
+ * @param aJSONReports
+ * The |reports| field of a JSON object.
+ * @return The constructed report map.
+ */
+function makeDReportMap(aJSONReports)
+{
+ let dreportMap = {};
+ for (let i = 0; i < aJSONReports.length; i++) {
+ let jr = aJSONReports[i];
+
+ assert(jr.process !== undefined, "Missing process");
+ assert(jr.path !== undefined, "Missing path");
+ assert(jr.kind !== undefined, "Missing kind");
+ assert(jr.units !== undefined, "Missing units");
+ assert(jr.amount !== undefined, "Missing amount");
+ assert(jr.description !== undefined, "Missing description");
+
+ // Strip out some non-deterministic stuff that prevents clean diffs.
+ // Ideally the memory reports themselves would contain information about
+ // which parts of the the process and path need to be stripped -- saving us
+ // from hardwiring knowledge of specific reporters here -- but we have no
+ // mechanism for that. (Any future redesign of how memory reporters work
+ // should include such a mechanism.)
+
+ // Strip PIDs:
+ // - pid 123
+ // - pid=123
+ let pidRegex = /pid([ =])\d+/g;
+ let pidSubst = "pid$1NNN";
+ let process = jr.process.replace(pidRegex, pidSubst);
+ let path = jr.path.replace(pidRegex, pidSubst);
+
+ // Strip addresses:
+ // - .../js-zone(0x12345678)/...
+ // - .../zone(0x12345678)/...
+ // - .../worker(<URL>, 0x12345678)/...
+ path = path.replace(/zone\(0x[0-9A-Fa-f]+\)\//, "zone(0xNNN)/");
+ path = path.replace(/\/worker\((.+), 0x[0-9A-Fa-f]+\)\//,
+ "/worker($1, 0xNNN)/");
+
+ // Strip top window IDs:
+ // - explicit/window-objects/top(<URL>, id=123)/...
+ path = path.replace(/^(explicit\/window-objects\/top\(.*, id=)\d+\)/,
+ "$1NNN)");
+
+ // Strip null principal UUIDs (but not other UUIDs, because they may be
+ // deterministic, such as those used by add-ons).
+ path = path.replace(
+ /moz-nullprincipal:{........-....-....-....-............}/g,
+ "moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}");
+
+ // Normalize omni.ja! paths.
+ path = path.replace(/jar:file:\\\\\\(.+)\\omni.ja!/,
+ "jar:file:\\\\\\...\\omni.ja!");
+
+ let processPath = process + kProcessPathSep + path;
+ let rOld = dreportMap[processPath];
+ if (rOld === undefined) {
+ dreportMap[processPath] =
+ new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined);
+ } else {
+ rOld.merge(jr);
+ }
+ }
+ return dreportMap;
+}
+
+// Return a new dreportMap which is the diff of two dreportMaps. Empties
+// aDReportMap2 along the way.
+function diffDReportMaps(aDReportMap1, aDReportMap2)
+{
+ let result = {};
+
+ for (let processPath in aDReportMap1) {
+ let r1 = aDReportMap1[processPath];
+ let r2 = aDReportMap2[processPath];
+ let r2_amount, r2_nMerged;
+ let presence;
+ if (r2 !== undefined) {
+ r1.assertCompatible(r2._kind, r2._units);
+ r2_amount = r2._amount;
+ r2_nMerged = r2._nMerged;
+ delete aDReportMap2[processPath];
+ presence = undefined; // represents that it's present in both
+ } else {
+ r2_amount = 0;
+ r2_nMerged = 0;
+ presence = DReport.PRESENT_IN_FIRST_ONLY;
+ }
+ result[processPath] =
+ new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
+ Math.max(r1._nMerged, r2_nMerged), presence);
+ }
+
+ for (let processPath in aDReportMap2) {
+ let r2 = aDReportMap2[processPath];
+ result[processPath] = new DReport(r2._kind, r2._units, r2._amount,
+ r2._description, r2._nMerged,
+ DReport.PRESENT_IN_SECOND_ONLY);
+ }
+
+ return result;
+}
+
+function makeJSONReports(aDReportMap)
+{
+ let reports = [];
+ for (let processPath in aDReportMap) {
+ let r = aDReportMap[processPath];
+ if (r._amount !== 0) {
+ // If _nMerged > 1, we give the full (aggregated) amount in the first
+ // copy, and then use amount=0 in the remainder. When viewed in
+ // about:memory, this shows up as an entry with a "[2]"-style suffix
+ // and the correct amount.
+ let split = processPath.split(kProcessPathSep);
+ assert(split.length >= 2);
+ let process = split.shift();
+ let path = split.join();
+ reports.push(r.toJSON(process, path, r._amount));
+ for (let i = 1; i < r._nMerged; i++) {
+ reports.push(r.toJSON(process, path, 0));
+ }
+ }
+ }
+
+ return reports;
+}
+
+// Diff two JSON objects holding memory reports.
+function diffJSONObjects(aJson1, aJson2)
+{
+ function simpleProp(aProp)
+ {
+ assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
+ aProp + " properties don't match");
+ return aJson1[aProp];
+ }
+
+ return {
+ version: simpleProp("version"),
+
+ hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
+
+ reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports),
+ makeDReportMap(aJson2.reports)))
+ };
+}
+
+// ---------------------------------------------------------------------------
+
+// |PColl| is short for "process collection".
+function PColl()
+{
+ this._trees = {};
+ this._degenerates = {};
+ this._heapTotal = 0;
+}
+
+/**
+ * Processes reports (whether from reporters or from a file) and append the
+ * main part of the page.
+ *
+ * @param aProcessReports
+ * Function that extracts the memory reports from the reporters or from
+ * file.
+ * @param aHasMozMallocUsableSize
+ * Boolean indicating if moz_malloc_usable_size works.
+ */
+function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize)
+{
+ let pcollsByProcess = {};
+
+ function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
+ aDescription, aPresence)
+ {
+ if (aUnsafePath.startsWith("explicit/")) {
+ assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
+ "bad explicit kind");
+ assertInput(aUnits === UNITS_BYTES, "bad explicit units");
+ }
+
+ assert(aPresence === undefined ||
+ aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
+ aPresence == DReport.PRESENT_IN_SECOND_ONLY,
+ "bad presence");
+
+ let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
+ let unsafeNames = aUnsafePath.split('/');
+ let unsafeName0 = unsafeNames[0];
+ let isDegenerate = unsafeNames.length === 1;
+
+ // Get the PColl table for the process, creating it if necessary.
+ let pcoll = pcollsByProcess[process];
+ if (!pcollsByProcess[process]) {
+ pcoll = pcollsByProcess[process] = new PColl();
+ }
+
+ // Get the root node, creating it if necessary.
+ let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees;
+ let t = psubcoll[unsafeName0];
+ if (!t) {
+ t = psubcoll[unsafeName0] =
+ new TreeNode(unsafeName0, aUnits, isDegenerate);
+ }
+
+ if (!isDegenerate) {
+ // Add any missing nodes in the tree implied by aUnsafePath, and fill in
+ // the properties that we can with a top-down traversal.
+ for (let i = 1; i < unsafeNames.length; i++) {
+ let unsafeName = unsafeNames[i];
+ let u = t.findKid(unsafeName);
+ if (!u) {
+ u = new TreeNode(unsafeName, aUnits, isDegenerate);
+ if (!t._kids) {
+ t._kids = [];
+ }
+ t._kids.push(u);
+ }
+ t = u;
+ }
+
+ // Update the heap total if necessary.
+ if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
+ pcollsByProcess[process]._heapTotal += aAmount;
+ }
+ }
+
+ if (t._amount) {
+ // Duplicate! Sum the values and mark it as a dup.
+ t._amount += aAmount;
+ t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
+ assert(t._presence === aPresence, "presence mismatch");
+ } else {
+ // New leaf node. Fill in extra node details from the report.
+ t._amount = aAmount;
+ t._description = aDescription;
+ if (aPresence !== undefined) {
+ t._presence = aPresence;
+ }
+ }
+ }
+
+ function displayReports()
+ {
+ // Sort the processes.
+ let processes = Object.keys(pcollsByProcess);
+ processes.sort(function(aProcessA, aProcessB) {
+ assert(aProcessA != aProcessB,
+ "Elements of Object.keys() should be unique, but " +
+ "saw duplicate '" + aProcessA + "' elem.");
+
+ // Always put the main process first.
+ if (aProcessA == gUnnamedProcessStr) {
+ return -1;
+ }
+ if (aProcessB == gUnnamedProcessStr) {
+ return 1;
+ }
+
+ // Then sort by resident size.
+ let nodeA = pcollsByProcess[aProcessA]._degenerates['resident'];
+ let nodeB = pcollsByProcess[aProcessB]._degenerates['resident'];
+ let residentA = nodeA ? nodeA._amount : -1;
+ let residentB = nodeB ? nodeB._amount : -1;
+
+ if (residentA > residentB) {
+ return -1;
+ }
+ if (residentA < residentB) {
+ return 1;
+ }
+
+ // Then sort by process name.
+ if (aProcessA < aProcessB) {
+ return -1;
+ }
+ if (aProcessA > aProcessB) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ // Generate output for each process.
+ for (let i = 0; i < processes.length; i++) {
+ let process = processes[i];
+ let section = appendElement(gMain, 'div', 'section');
+
+ appendProcessAboutMemoryElements(section, i, process,
+ pcollsByProcess[process]._trees,
+ pcollsByProcess[process]._degenerates,
+ pcollsByProcess[process]._heapTotal,
+ aHasMozMallocUsableSize);
+ }
+ }
+
+ aProcessReports(handleReport, displayReports);
+}
+
+// ---------------------------------------------------------------------------
+
+// There are two kinds of TreeNode.
+// - Leaf TreeNodes correspond to reports.
+// - Non-leaf TreeNodes are just scaffolding nodes for the tree; their values
+// are derived from their children.
+// Some trees are "degenerate", i.e. they contain a single node, i.e. they
+// correspond to a report whose path has no '/' separators.
+function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
+{
+ this._units = aUnits;
+ this._unsafeName = aUnsafeName;
+ if (aIsDegenerate) {
+ this._isDegenerate = true;
+ }
+
+ // Leaf TreeNodes have these properties added immediately after construction:
+ // - _amount
+ // - _description
+ // - _nMerged (only defined if > 1)
+ // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
+ //
+ // Non-leaf TreeNodes have these properties added later:
+ // - _kids
+ // - _amount
+ // - _description
+ // - _hideKids (only defined if true)
+ // - _maxAbsDescendant (on-demand, only when gIsDiff is set)
+}
+
+TreeNode.prototype = {
+ findKid: function(aUnsafeName) {
+ if (this._kids) {
+ for (let i = 0; i < this._kids.length; i++) {
+ if (this._kids[i]._unsafeName === aUnsafeName) {
+ return this._kids[i];
+ }
+ }
+ }
+ return undefined;
+ },
+
+ // When gIsDiff is false, tree operations -- sorting and determining if a
+ // sub-tree is significant -- are straightforward. But when gIsDiff is true,
+ // the combination of positive and negative values within a tree complicates
+ // things. So for a non-leaf node, instead of just looking at _amount, we
+ // instead look at the maximum absolute value of the node and all of its
+ // descendants.
+ maxAbsDescendant: function() {
+ if (!this._kids) {
+ // No kids? Just return the absolute value of the amount.
+ return Math.abs(this._amount);
+ }
+
+ if ('_maxAbsDescendant' in this) {
+ // We've computed this before? Return the saved value.
+ return this._maxAbsDescendant;
+ }
+
+ // Compute the maximum absolute value of all descendants.
+ let max = Math.abs(this._amount);
+ for (let i = 0; i < this._kids.length; i++) {
+ max = Math.max(max, this._kids[i].maxAbsDescendant());
+ }
+ this._maxAbsDescendant = max;
+ return max;
+ },
+
+ toString: function() {
+ switch (this._units) {
+ case UNITS_BYTES: return formatBytes(this._amount);
+ case UNITS_COUNT:
+ case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount);
+ case UNITS_PERCENTAGE: return formatPercentage(this._amount);
+ default:
+ throw "Invalid memory report(s): bad units in TreeNode.toString";
+ }
+ }
+};
+
+// Sort TreeNodes first by size, then by name. The latter is important for the
+// about:memory tests, which need a predictable ordering of reporters which
+// have the same amount.
+TreeNode.compareAmounts = function(aA, aB) {
+ let a, b;
+ if (gIsDiff) {
+ a = aA.maxAbsDescendant();
+ b = aB.maxAbsDescendant();
+ } else {
+ a = aA._amount;
+ b = aB._amount;
+ }
+ if (a > b) {
+ return -1;
+ }
+ if (a < b) {
+ return 1;
+ }
+ return TreeNode.compareUnsafeNames(aA, aB);
+};
+
+TreeNode.compareUnsafeNames = function(aA, aB) {
+ return aA._unsafeName.localeCompare(aB._unsafeName);
+};
+
+
+/**
+ * Fill in the remaining properties for the specified tree in a bottom-up
+ * fashion.
+ *
+ * @param aRoot
+ * The tree root.
+ */
+function fillInTree(aRoot)
+{
+ // Fill in the remaining properties bottom-up.
+ function fillInNonLeafNodes(aT)
+ {
+ if (!aT._kids) {
+ // Leaf node. Has already been filled in.
+
+ } else if (aT._kids.length === 1 && aT != aRoot) {
+ // Non-root, non-leaf node with one child. Merge the child with the node
+ // to avoid redundant entries.
+ let kid = aT._kids[0];
+ let kidBytes = fillInNonLeafNodes(kid);
+ aT._unsafeName += '/' + kid._unsafeName;
+ if (kid._kids) {
+ aT._kids = kid._kids;
+ } else {
+ delete aT._kids;
+ }
+ aT._amount = kidBytes;
+ aT._description = kid._description;
+ if (kid._nMerged !== undefined) {
+ aT._nMerged = kid._nMerged
+ }
+ assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging");
+
+ } else {
+ // Non-leaf node with multiple children. Derive its _amount and
+ // _description entirely from its children...
+ let kidsBytes = 0;
+ for (let i = 0; i < aT._kids.length; i++) {
+ kidsBytes += fillInNonLeafNodes(aT._kids[i]);
+ }
+
+ // ... except in one special case. When diffing two memory report sets,
+ // if one set has a node with children and the other has the same node
+ // but without children -- e.g. the first has "a/b/c" and "a/b/d", but
+ // the second only has "a/b" -- we need to add a fake node "a/b/(fake)"
+ // to the second to make the trees comparable. It's ugly, but it works.
+ if (aT._amount !== undefined &&
+ (aT._presence === DReport.PRESENT_IN_FIRST_ONLY ||
+ aT._presence === DReport.PRESENT_IN_SECOND_ONLY)) {
+ aT._amount += kidsBytes;
+ let fake = new TreeNode('(fake child)', aT._units);
+ fake._presence = DReport.ADDED_FOR_BALANCE;
+ fake._amount = aT._amount - kidsBytes;
+ aT._kids.push(fake);
+ delete aT._presence;
+ } else {
+ assert(aT._amount === undefined,
+ "_amount already set for non-leaf node")
+ aT._amount = kidsBytes;
+ }
+ aT._description = "The sum of all entries below this one.";
+ }
+ return aT._amount;
+ }
+
+ // cannotMerge is set because don't want to merge into a tree's root node.
+ fillInNonLeafNodes(aRoot);
+}
+
+/**
+ * Compute the "heap-unclassified" value and insert it into the "explicit"
+ * tree.
+ *
+ * @param aT
+ * The "explicit" tree.
+ * @param aHeapAllocatedNode
+ * The "heap-allocated" tree node.
+ * @param aHeapTotal
+ * The sum of all explicit HEAP reports for this process.
+ * @return A boolean indicating if "heap-allocated" is known for the process.
+ */
+function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal)
+{
+ if (aHeapAllocatedNode === undefined)
+ return false;
+
+ if (aT.findKid("heap-unclassified")) {
+ // heap-unclassified was already calculated, there's nothing left to do.
+ // This can happen when memory reports are exported from areweslimyet.com.
+ return true;
+ }
+
+ assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
+ let heapAllocatedBytes = aHeapAllocatedNode._amount;
+ let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
+ heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
+ heapUnclassifiedT._description =
+ "Memory not classified by a more specific report. This includes " +
+ "slop bytes due to internal fragmentation in the heap allocator " +
+ "(caused when the allocator rounds up request sizes).";
+ aT._kids.push(heapUnclassifiedT);
+ aT._amount += heapUnclassifiedT._amount;
+ return true;
+}
+
+/**
+ * Sort all kid nodes from largest to smallest, and insert aggregate nodes
+ * where appropriate.
+ *
+ * @param aTotalBytes
+ * The size of the tree's root node.
+ * @param aT
+ * The tree.
+ */
+function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
+{
+ const kSignificanceThresholdPerc = 1;
+
+ function isInsignificant(aT)
+ {
+ if (gVerbose.checked)
+ return false;
+
+ let perc = gIsDiff
+ ? 100 * aT.maxAbsDescendant() / Math.abs(aTotalBytes)
+ : 100 * aT._amount / aTotalBytes;
+ return perc < kSignificanceThresholdPerc;
+ }
+
+ if (!aT._kids) {
+ return;
+ }
+
+ aT._kids.sort(TreeNode.compareAmounts);
+
+ // If the first child is insignificant, they all are, and there's no point
+ // creating an aggregate node that lacks siblings. Just set the parent's
+ // _hideKids property and process all children.
+ if (isInsignificant(aT._kids[0])) {
+ aT._hideKids = true;
+ for (let i = 0; i < aT._kids.length; i++) {
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+ }
+ return;
+ }
+
+ // Look at all children except the last one.
+ let i;
+ for (i = 0; i < aT._kids.length - 1; i++) {
+ if (isInsignificant(aT._kids[i])) {
+ // This child is below the significance threshold. If there are other
+ // (smaller) children remaining, move them under an aggregate node.
+ let i0 = i;
+ let nAgg = aT._kids.length - i0;
+ // Create an aggregate node. Inherit units from the parent; everything
+ // in the tree should have the same units anyway (we test this later).
+ let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
+ aggT._kids = [];
+ let aggBytes = 0;
+ for ( ; i < aT._kids.length; i++) {
+ aggBytes += aT._kids[i]._amount;
+ aggT._kids.push(aT._kids[i]);
+ }
+ aggT._hideKids = true;
+ aggT._amount = aggBytes;
+ aggT._description =
+ nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
+ "% significance threshold.";
+ aT._kids.splice(i0, nAgg, aggT);
+ aT._kids.sort(TreeNode.compareAmounts);
+
+ // Process the moved children.
+ for (i = 0; i < aggT._kids.length; i++) {
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
+ }
+ return;
+ }
+
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+ }
+
+ // The first n-1 children were significant. Don't consider if the last child
+ // is significant; there's no point creating an aggregate node that only has
+ // one child. Just process it.
+ sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
+}
+
+// Global variable indicating if we've seen any invalid values for this
+// process; it holds the unsafePaths of any such reports. It is reset for
+// each new process.
+var gUnsafePathsWithInvalidValuesForThisProcess = [];
+
+function appendWarningElements(aP, aHasKnownHeapAllocated,
+ aHasMozMallocUsableSize)
+{
+ if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
+ appendElementWithText(aP, "p", "",
+ "WARNING: the 'heap-allocated' memory reporter and the " +
+ "moz_malloc_usable_size() function do not work for this platform " +
+ "and/or configuration. This means that 'heap-unclassified' is not " +
+ "shown and the 'explicit' tree shows much less memory than it should.\n\n");
+
+ } else if (!aHasKnownHeapAllocated) {
+ appendElementWithText(aP, "p", "",
+ "WARNING: the 'heap-allocated' memory reporter does not work for this " +
+ "platform and/or configuration. This means that 'heap-unclassified' " +
+ "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
+
+ } else if (!aHasMozMallocUsableSize) {
+ appendElementWithText(aP, "p", "",
+ "WARNING: the moz_malloc_usable_size() function does not work for " +
+ "this platform and/or configuration. This means that much of the " +
+ "heap-allocated memory is not measured by individual memory reporters " +
+ "and so will fall under 'heap-unclassified'.\n\n");
+ }
+
+ if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
+ let div = appendElement(aP, "div");
+ appendElementWithText(div, "p", "",
+ "WARNING: the following values are negative or unreasonably large.\n");
+
+ let ul = appendElement(div, "ul");
+ for (let i = 0;
+ i < gUnsafePathsWithInvalidValuesForThisProcess.length;
+ i++)
+ {
+ appendTextNode(ul, " ");
+ appendElementWithText(ul, "li", "",
+ flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n");
+ }
+
+ appendElementWithText(div, "p", "",
+ "This indicates a defect in one or more memory reporters. The " +
+ "invalid values are highlighted.\n\n");
+ gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process
+ }
+}
+
+/**
+ * Appends the about:memory elements for a single process.
+ *
+ * @param aP
+ * The parent DOM node.
+ * @param aN
+ * The number of the process, starting at 0.
+ * @param aProcess
+ * The name of the process.
+ * @param aTrees
+ * The table of non-degenerate trees for this process.
+ * @param aDegenerates
+ * The table of degenerate trees for this process.
+ * @param aHasMozMallocUsableSize
+ * Boolean indicating if moz_malloc_usable_size works.
+ * @return The generated text.
+ */
+function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
+ aDegenerates, aHeapTotal,
+ aHasMozMallocUsableSize)
+{
+ const kUpwardsArrow = "\u2191",
+ kDownwardsArrow = "\u2193";
+
+ let appendLink = function(aHere, aThere, aArrow) {
+ let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
+ link.href = "#" + aThere + aN;
+ link.id = aHere + aN;
+ link.title = "Go to the " + aThere + " of " + aProcess;
+ link.style = "text-decoration: none";
+
+ // This jumps to the anchor without the page location getting the anchor
+ // name tacked onto its end, which is what happens with a vanilla link.
+ link.addEventListener("click", function(event) {
+ document.documentElement.scrollTop =
+ document.querySelector(event.target.href).offsetTop;
+ event.preventDefault();
+ }, false);
+
+ // This gives nice spacing when we copy and paste.
+ appendElementWithText(aP, "span", "", "\n");
+ }
+
+ appendElementWithText(aP, "h1", "", aProcess);
+ appendLink("start", "end", kDownwardsArrow);
+
+ // We'll fill this in later.
+ let warningsDiv = appendElement(aP, "div", "accuracyWarning");
+
+ // The explicit tree.
+ let hasExplicitTree;
+ let hasKnownHeapAllocated;
+ {
+ let treeName = "explicit";
+ let t = aTrees[treeName];
+ if (t) {
+ let pre = appendSectionHeader(aP, "Explicit Allocations");
+ hasExplicitTree = true;
+ fillInTree(t);
+ // Using the "heap-allocated" reporter here instead of
+ // nsMemoryReporterManager.heapAllocated goes against the usual pattern.
+ // But the "heap-allocated" node will go in the tree like the others, so
+ // we have to deal with it, and once we're dealing with it, it's easier
+ // to keep doing so rather than switching to the distinguished amount.
+ hasKnownHeapAllocated =
+ aDegenerates &&
+ addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
+ sortTreeAndInsertAggregateNodes(t._amount, t);
+ t._description = explicitTreeDescription;
+ appendTreeElements(pre, t, aProcess, "");
+ delete aTrees[treeName];
+ }
+ appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
+ }
+
+ // Fill in and sort all the non-degenerate other trees.
+ let otherTrees = [];
+ for (let unsafeName in aTrees) {
+ let t = aTrees[unsafeName];
+ assert(!t._isDegenerate, "tree is degenerate");
+ fillInTree(t);
+ sortTreeAndInsertAggregateNodes(t._amount, t);
+ otherTrees.push(t);
+ }
+ otherTrees.sort(TreeNode.compareUnsafeNames);
+
+ // Get the length of the longest root value among the degenerate other trees,
+ // and sort them as well.
+ let otherDegenerates = [];
+ let maxStringLength = 0;
+ for (let unsafeName in aDegenerates) {
+ let t = aDegenerates[unsafeName];
+ assert(t._isDegenerate, "tree is not degenerate");
+ let length = t.toString().length;
+ if (length > maxStringLength) {
+ maxStringLength = length;
+ }
+ otherDegenerates.push(t);
+ }
+ otherDegenerates.sort(TreeNode.compareUnsafeNames);
+
+ // Now generate the elements, putting non-degenerate trees first.
+ let pre = appendSectionHeader(aP, "Other Measurements");
+ for (let i = 0; i < otherTrees.length; i++) {
+ let t = otherTrees[i];
+ appendTreeElements(pre, t, aProcess, "");
+ appendTextNode(pre, "\n"); // blank lines after non-degenerate trees
+ }
+ for (let i = 0; i < otherDegenerates.length; i++) {
+ let t = otherDegenerates[i];
+ let padText = pad("", maxStringLength - t.toString().length, ' ');
+ appendTreeElements(pre, t, aProcess, padText);
+ }
+ appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
+
+ // Add any warnings about inaccuracies in the "explicit" tree due to platform
+ // limitations. These must be computed after generating all the text. The
+ // newlines give nice spacing if we copy+paste into a text buffer.
+ if (hasExplicitTree) {
+ appendWarningElements(warningsDiv, hasKnownHeapAllocated,
+ aHasMozMallocUsableSize);
+ }
+
+ appendElementWithText(aP, "h3", "", "End of " + aProcess);
+ appendLink("end", "start", kUpwardsArrow);
+}
+
+/**
+ * Determines if a number has a negative sign when converted to a string.
+ * Works even for -0.
+ *
+ * @param aN
+ * The number.
+ * @return A boolean.
+ */
+function hasNegativeSign(aN)
+{
+ if (aN === 0) { // this succeeds for 0 and -0
+ return 1 / aN === -Infinity; // this succeeds for -0
+ }
+ return aN < 0;
+}
+
+/**
+ * Formats an int as a human-readable string.
+ *
+ * @param aN
+ * The integer to format.
+ * @param aExtra
+ * An extra string to tack onto the end.
+ * @return A human-readable string representing the int.
+ *
+ * Note: building an array of chars and converting that to a string with
+ * Array.join at the end is more memory efficient than using string
+ * concatenation. See bug 722972 for details.
+ */
+function formatInt(aN, aExtra)
+{
+ let neg = false;
+ if (hasNegativeSign(aN)) {
+ neg = true;
+ aN = -aN;
+ }
+ let s = [];
+ while (true) {
+ let k = aN % 1000;
+ aN = Math.floor(aN / 1000);
+ if (aN > 0) {
+ if (k < 10) {
+ s.unshift(",00", k);
+ } else if (k < 100) {
+ s.unshift(",0", k);
+ } else {
+ s.unshift(",", k);
+ }
+ } else {
+ s.unshift(k);
+ break;
+ }
+ }
+ if (neg) {
+ s.unshift("-");
+ }
+ if (aExtra) {
+ s.push(aExtra);
+ }
+ return s.join("");
+}
+
+/**
+ * Converts a byte count to an appropriate string representation.
+ *
+ * @param aBytes
+ * The byte count.
+ * @return The string representation.
+ */
+function formatBytes(aBytes)
+{
+ let unit = gVerbose.checked ? " B" : " MB";
+
+ let s;
+ if (gVerbose.checked) {
+ s = formatInt(aBytes, unit);
+ } else {
+ let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
+ let a = String(mbytes).split(".");
+ // If the argument to formatInt() is -0, it will print the negative sign.
+ s = formatInt(Number(a[0])) + "." + a[1] + unit;
+ }
+ return s;
+}
+
+/**
+ * Converts a percentage to an appropriate string representation.
+ *
+ * @param aPerc100x
+ * The percentage, multiplied by 100 (see nsIMemoryReporter).
+ * @return The string representation
+ */
+function formatPercentage(aPerc100x)
+{
+ return (aPerc100x / 100).toFixed(2) + "%";
+}
+
+/**
+ * Right-justifies a string in a field of a given width, padding as necessary.
+ *
+ * @param aS
+ * The string.
+ * @param aN
+ * The field width.
+ * @param aC
+ * The char used to pad.
+ * @return The string representation.
+ */
+function pad(aS, aN, aC)
+{
+ let padding = "";
+ let n2 = aN - aS.length;
+ for (let i = 0; i < n2; i++) {
+ padding += aC;
+ }
+ return padding + aS;
+}
+
+// There's a subset of the Unicode "light" box-drawing chars that is widely
+// implemented in terminals, and this code sticks to that subset to maximize
+// the chance that copying and pasting about:memory output to a terminal will
+// work correctly.
+const kHorizontal = "\u2500",
+ kVertical = "\u2502",
+ kUpAndRight = "\u2514",
+ kUpAndRight_Right_Right = "\u2514\u2500\u2500",
+ kVerticalAndRight = "\u251c",
+ kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
+ kVertical_Space_Space = "\u2502 ";
+
+const kNoKidsSep = " \u2500\u2500 ",
+ kHideKidsSep = " ++ ",
+ kShowKidsSep = " -- ";
+
+function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
+ aPresence)
+{
+ let safeName = flipBackslashes(aUnsafeName);
+ if (!aIsInvalid && !aNMerged && !aPresence) {
+ safeName += "\n";
+ }
+ let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
+ nameSpan.title = aDescription;
+
+ if (aIsInvalid) {
+ let noteText = " [?!]";
+ if (!aNMerged) {
+ noteText += "\n";
+ }
+ let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
+ noteSpan.title =
+ "Warning: this value is invalid and indicates a bug in one or more " +
+ "memory reporters. ";
+ }
+
+ if (aNMerged) {
+ let noteText = " [" + aNMerged + "]";
+ if (!aPresence) {
+ noteText += "\n";
+ }
+ let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
+ noteSpan.title =
+ "This value is the sum of " + aNMerged +
+ " memory reports that all have the same path.";
+ }
+
+ if (aPresence) {
+ let c, title;
+ switch (aPresence) {
+ case DReport.PRESENT_IN_FIRST_ONLY:
+ c = '-';
+ title = "This value was only present in the first set of memory reports.";
+ break;
+ case DReport.PRESENT_IN_SECOND_ONLY:
+ c = '+';
+ title = "This value was only present in the second set of memory reports.";
+ break;
+ case DReport.ADDED_FOR_BALANCE:
+ c = '!';
+ title = "One of the sets of memory reports lacked children for this " +
+ "node's parent. This is a fake child node added to make the " +
+ "two memory sets comparable.";
+ break;
+ default: assert(false, "bad presence");
+ break;
+ }
+ let noteSpan = appendElementWithText(aP, "span", "mrNote",
+ " [" + c + "]\n");
+ noteSpan.title = title;
+ }
+}
+
+// This is used to record the (safe) IDs of which sub-trees have been manually
+// expanded (marked as true) and collapsed (marked as false). It's used to
+// replicate the collapsed/expanded state when the page is updated. It can end
+// up holding IDs of nodes that no longer exist, e.g. for compartments that
+// have been closed. This doesn't seem like a big deal, because the number is
+// limited by the number of entries the user has changed from their original
+// state.
+var gShowSubtreesBySafeTreeId = {};
+
+function assertClassListContains(e, className) {
+ assert(e, "undefined " + className);
+ assert(e.classList.contains(className), "classname isn't " + className);
+}
+
+function toggle(aEvent)
+{
+ // This relies on each line being a span that contains at least four spans:
+ // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes. All
+ // whitespace must be within one of these spans for this function to find the
+ // right nodes. And the span containing the children of this line must
+ // immediately follow. Assertions check this.
+
+ // |aEvent.target| will be one of the spans. Get the outer span.
+ let outerSpan = aEvent.target.parentNode;
+ assertClassListContains(outerSpan, "hasKids");
+
+ // Toggle the '++'/'--' separator.
+ let isExpansion;
+ let sepSpan = outerSpan.childNodes[2];
+ assertClassListContains(sepSpan, "mrSep");
+ if (sepSpan.textContent === kHideKidsSep) {
+ isExpansion = true;
+ sepSpan.textContent = kShowKidsSep;
+ } else if (sepSpan.textContent === kShowKidsSep) {
+ isExpansion = false;
+ sepSpan.textContent = kHideKidsSep;
+ } else {
+ assert(false, "bad sepSpan textContent");
+ }
+
+ // Toggle visibility of the span containing this node's children.
+ let subTreeSpan = outerSpan.nextSibling;
+ assertClassListContains(subTreeSpan, "kids");
+ subTreeSpan.classList.toggle("hidden");
+
+ // Record/unrecord that this sub-tree was toggled.
+ let safeTreeId = outerSpan.id;
+ if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
+ delete gShowSubtreesBySafeTreeId[safeTreeId];
+ } else {
+ gShowSubtreesBySafeTreeId[safeTreeId] = isExpansion;
+ }
+}
+
+function expandPathToThisElement(aElement)
+{
+ if (aElement.classList.contains("kids")) {
+ // Unhide the kids.
+ aElement.classList.remove("hidden");
+ expandPathToThisElement(aElement.previousSibling); // hasKids
+
+ } else if (aElement.classList.contains("hasKids")) {
+ // Change the separator to '--'.
+ let sepSpan = aElement.childNodes[2];
+ assertClassListContains(sepSpan, "mrSep");
+ sepSpan.textContent = kShowKidsSep;
+ expandPathToThisElement(aElement.parentNode); // kids or pre.entries
+
+ } else {
+ assertClassListContains(aElement, "entries");
+ }
+}
+
+/**
+ * Appends the elements for the tree, including its heading.
+ *
+ * @param aP
+ * The parent DOM node.
+ * @param aRoot
+ * The tree root.
+ * @param aProcess
+ * The process the tree corresponds to.
+ * @param aPadText
+ * A string to pad the start of each entry.
+ */
+function appendTreeElements(aP, aRoot, aProcess, aPadText)
+{
+ /**
+ * Appends the elements for a particular tree, without a heading.
+ *
+ * @param aP
+ * The parent DOM node.
+ * @param aProcess
+ * The process the tree corresponds to.
+ * @param aUnsafeNames
+ * An array of the names forming the path to aT.
+ * @param aRoot
+ * The root of the tree this sub-tree belongs to.
+ * @param aT
+ * The tree.
+ * @param aTreelineText1
+ * The first part of the treeline for this entry and this entry's
+ * children.
+ * @param aTreelineText2a
+ * The second part of the treeline for this entry.
+ * @param aTreelineText2b
+ * The second part of the treeline for this entry's children.
+ * @param aParentStringLength
+ * The length of the formatted byte count of the top node in the tree.
+ */
+ function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
+ aTreelineText1, aTreelineText2a,
+ aTreelineText2b, aParentStringLength)
+ {
+ function appendN(aS, aC, aN)
+ {
+ for (let i = 0; i < aN; i++) {
+ aS += aC;
+ }
+ return aS;
+ }
+
+ // The tree line. Indent more if this entry is narrower than its parent.
+ let valueText = aT.toString();
+ let extraTreelineLength =
+ Math.max(aParentStringLength - valueText.length, 0);
+ if (extraTreelineLength > 0) {
+ aTreelineText2a =
+ appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
+ aTreelineText2b =
+ appendN(aTreelineText2b, " ", extraTreelineLength);
+ }
+ let treelineText = aTreelineText1 + aTreelineText2a;
+ appendElementWithText(aP, "span", "treeline", treelineText);
+
+ // Detect and record invalid values. But not if gIsDiff is true, because
+ // we expect negative values in that case.
+ assertInput(aRoot._units === aT._units,
+ "units within a tree are inconsistent");
+ let tIsInvalid = false;
+ if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) {
+ tIsInvalid = true;
+ let unsafePath = aUnsafeNames.join("/");
+ gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
+ reportAssertionFailure("Invalid value (" + aT._amount + " / " +
+ aRoot._amount + ") for " +
+ flipBackslashes(unsafePath));
+ }
+
+ // For non-leaf nodes, the entire sub-tree is put within a span so it can
+ // be collapsed if the node is clicked on.
+ let d;
+ let sep;
+ let showSubtrees;
+ if (aT._kids) {
+ // Determine if we should show the sub-tree below this entry; this
+ // involves reinstating any previous toggling of the sub-tree.
+ let unsafePath = aUnsafeNames.join("/");
+ let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath);
+ showSubtrees = !aT._hideKids;
+ if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
+ showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId];
+ }
+ d = appendElement(aP, "span", "hasKids");
+ d.id = safeTreeId;
+ d.onclick = toggle;
+ sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
+ } else {
+ assert(!aT._hideKids, "leaf node with _hideKids set")
+ sep = kNoKidsSep;
+ d = aP;
+ }
+
+ // The value.
+ appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
+ valueText);
+
+ // The percentage (omitted for single entries).
+ let percText;
+ if (!aT._isDegenerate) {
+ // Treat 0 / 0 as 100%.
+ let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount);
+ let numText = num.toFixed(2);
+ percText = numText === "100.00"
+ ? " (100.0%)"
+ : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
+ appendElementWithText(d, "span", "mrPerc", percText);
+ }
+
+ // The separator.
+ appendElementWithText(d, "span", "mrSep", sep);
+
+ // The entry's name.
+ appendMrNameSpan(d, aT._description, aT._unsafeName,
+ tIsInvalid, aT._nMerged, aT._presence);
+
+ // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees.
+ // But it's good to always see them, so force this.
+ if (!gVerbose.checked && tIsInvalid) {
+ expandPathToThisElement(d);
+ }
+
+ // Recurse over children.
+ if (aT._kids) {
+ // The 'kids' class is just used for sanity checking in toggle().
+ d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
+
+ let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
+ for (let i = 0; i < aT._kids.length; i++) {
+ let kidTreelineText2a, kidTreelineText2b;
+ if (i < aT._kids.length - 1) {
+ kidTreelineText2a = kVerticalAndRight_Right_Right;
+ kidTreelineText2b = kVertical_Space_Space;
+ } else {
+ kidTreelineText2a = kUpAndRight_Right_Right;
+ kidTreelineText2b = " ";
+ }
+ aUnsafeNames.push(aT._kids[i]._unsafeName);
+ appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
+ kidTreelineText1, kidTreelineText2a,
+ kidTreelineText2b, valueText.length);
+ aUnsafeNames.pop();
+ }
+ }
+ }
+
+ let rootStringLength = aRoot.toString().length;
+ appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
+ aPadText, "", "", rootStringLength);
+}
+
+// ---------------------------------------------------------------------------
+
+function appendSectionHeader(aP, aText)
+{
+ appendElementWithText(aP, "h2", "", aText + "\n");
+ return appendElement(aP, "pre", "entries");
+}
+
+// ---------------------------------------------------------------------------
+
+function saveReportsToFile()
+{
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.appendFilter("Zipped JSON files", "*.json.gz");
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.filterIndex = 0;
+ fp.addToRecentDocs = true;
+ fp.defaultString = "memory-report.json.gz";
+
+ let fpFinish = function(file) {
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
+ .getService(Ci.nsIMemoryInfoDumper);
+ let finishDumping = () => {
+ updateMainAndFooter("Saved memory reports to " + file.path, HIDE_FOOTER);
+ }
+ dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null,
+ gAnonymize.checked);
+ }
+
+ let fpCallback = function(aResult) {
+ if (aResult == Ci.nsIFilePicker.returnOK ||
+ aResult == Ci.nsIFilePicker.returnReplace) {
+ fpFinish(fp.file);
+ }
+ };
+
+ try {
+ fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
+ } catch (ex) {
+ // This will fail on Android, since there is no Save as file picker there.
+ // Just save to the default downloads dir if it does.
+ Downloads.getSystemDownloadsDirectory().then(function(dirPath) {
+ let file = FileUtils.File(dirPath);
+ file.append(fp.defaultString);
+ fpFinish(file);
+ });
+
+ return;
+ }
+ fp.open(fpCallback);
+}
diff --git a/toolkit/components/aboutmemory/content/aboutMemory.xhtml b/toolkit/components/aboutmemory/content/aboutMemory.xhtml
new file mode 100644
index 0000000000..e20b3b624e
--- /dev/null
+++ b/toolkit/components/aboutmemory/content/aboutMemory.xhtml
@@ -0,0 +1,15 @@
+<?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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta name="viewport" content="width=device-width"/>
+ <link rel="stylesheet" href="chrome://global/skin/aboutMemory.css" type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://global/content/aboutMemory.js"/>
+ </head>
+
+ <body onload="onLoad()" onunload="onUnload()"></body>
+</html>
diff --git a/toolkit/components/aboutmemory/jar.mn b/toolkit/components/aboutmemory/jar.mn
new file mode 100644
index 0000000000..0a6b01ed78
--- /dev/null
+++ b/toolkit/components/aboutmemory/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/.
+
+toolkit.jar:
+ content/global/aboutMemory.js (content/aboutMemory.js)
+ content/global/aboutMemory.xhtml (content/aboutMemory.xhtml)
+ content/global/aboutMemory.css (content/aboutMemory.css)
diff --git a/toolkit/components/aboutmemory/moz.build b/toolkit/components/aboutmemory/moz.build
new file mode 100644
index 0000000000..dd3f71d8cb
--- /dev/null
+++ b/toolkit/components/aboutmemory/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/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'about:memory')
diff --git a/toolkit/components/aboutmemory/tests/.eslintrc.js b/toolkit/components/aboutmemory/tests/.eslintrc.js
new file mode 100644
index 0000000000..2c669d844e
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/aboutmemory/tests/chrome.ini b/toolkit/components/aboutmemory/tests/chrome.ini
new file mode 100644
index 0000000000..c25bc30a06
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/chrome.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ crash-dump-diff1.json
+ crash-dump-diff2.json
+ crash-dump-good.json
+ memory-reports-bad.json
+ memory-reports-diff1.json
+ memory-reports-diff2.json
+ memory-reports-good.json
+ remote.xul
+
+[test_aboutmemory.xul]
+subsuite = clipboard
+[test_aboutmemory2.xul]
+subsuite = clipboard
+[test_aboutmemory3.xul]
+subsuite = clipboard
+[test_aboutmemory4.xul]
+subsuite = clipboard
+[test_aboutmemory5.xul]
+subsuite = clipboard
+skip-if = asan # Bug 1116230
+[test_aboutmemory6.xul]
+[test_memoryReporters.xul]
+[test_memoryReporters2.xul]
+[test_sqliteMultiReporter.xul]
+[test_dumpGCAndCCLogsToFile.xul]
diff --git a/toolkit/components/aboutmemory/tests/crash-dump-diff1.json b/toolkit/components/aboutmemory/tests/crash-dump-diff1.json
new file mode 100644
index 0000000000..d41bbcc61e
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/crash-dump-diff1.json
@@ -0,0 +1,11 @@
+{
+ "foo": 1,
+ "blah": 2,
+ "memory_report": {
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."}
+ ]
+ }
+}
diff --git a/toolkit/components/aboutmemory/tests/crash-dump-diff2.json b/toolkit/components/aboutmemory/tests/crash-dump-diff2.json
new file mode 100644
index 0000000000..8f9451f625
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/crash-dump-diff2.json
@@ -0,0 +1,11 @@
+{
+ "foo": 3,
+ "blah": 4,
+ "memory_report": {
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144001, "description": "Heap allocated."}
+ ]
+ }
+}
diff --git a/toolkit/components/aboutmemory/tests/crash-dump-good.json b/toolkit/components/aboutmemory/tests/crash-dump-good.json
new file mode 100644
index 0000000000..6bee54d591
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/crash-dump-good.json
@@ -0,0 +1,14 @@
+{
+ "foo": 1,
+ "blah": 2,
+ "memory_report": {
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."},
+ {"process": "Main Process (pid NNN)", "path": "other/b", "kind": 2, "units": 0, "amount": 104857, "description": "Other b."},
+ {"process": "Main Process (pid NNN)", "path": "other/a", "kind": 2, "units": 0, "amount": 209715, "description": "Other a."},
+ {"process": "Main Process (pid NNN)", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 52428800, "description": "A b."}
+ ]
+ }
+}
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-bad.json b/toolkit/components/aboutmemory/tests/memory-reports-bad.json
new file mode 100644
index 0000000000..61a2092b1b
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-bad.json
@@ -0,0 +1,3 @@
+{
+ "version": 1
+}
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-diff1.json b/toolkit/components/aboutmemory/tests/memory-reports-diff1.json
new file mode 100644
index 0000000000..0bfe0b26bf
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-diff1.json
@@ -0,0 +1,45 @@
+{
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56848, "description": "Desc."},
+ {"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 680000, "description": "Desc."},
+
+ {"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 4, "description": "Desc."},
+ {"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 5, "description": "Desc."},
+
+ {"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
+
+ {"process": "P", "path": "foobar", "kind": 2, "units": 0, "amount": 100, "description": "Desc."},
+ {"process": "P", "path": "zero1", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P", "path": "a/b", "kind": 2, "units": 0, "amount": 1000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/d", "kind": 2, "units": 0, "amount": 2000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/e", "kind": 2, "units": 0, "amount": 2000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/f", "kind": 2, "units": 0, "amount": 3000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/g", "kind": 2, "units": 0, "amount": 3000000, "description": "Desc."},
+ {"process": "P", "path": "a/h", "kind": 2, "units": 0, "amount": 1000, "description": "Desc."},
+
+ {"process": "P2 (pid 22)", "path": "p1 (pid 123)", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p2 (blah, pid=123)", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p3/zone(0x1234)/p3", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p4/js-zone(0x1234)/p4", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p5/worker(foo.com, 0x1234)/p5", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "explicit/window-objects/top(bar.com, id=123)/...", "kind": 0, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p6/z-moz-nullprincipal:{85e250f3-57ae-46c4-a11e-4176dd39d9c5}/p6", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p7/js-main-runtime-compartments/system/jar:file:\\\\\\temp_xyz\\firefox\\omni.ja!/p7", "kind": 2, "units": 0, "amount": 33, "description": "Desc."},
+
+ {"process": "P3", "path": "p3", "kind": 2, "units": 0, "amount": 55, "description": "Desc."},
+
+ {"process": "P5", "path": "p5", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P7", "path": "p7", "kind": 2, "units": 0, "amount": 5, "description": "Desc."},
+
+ {"process": "P8", "path": "p8/a/b/c/d", "kind": 2, "units": 0, "amount": 3, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/b/c/e", "kind": 2, "units": 0, "amount": 4, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/b/f", "kind": 2, "units": 0, "amount": 5, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/g/h", "kind": 2, "units": 0, "amount": 6, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/g/i", "kind": 2, "units": 0, "amount": 7, "description": "Desc."}
+ ]
+}
+
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-diff2.json b/toolkit/components/aboutmemory/tests/memory-reports-diff2.json
new file mode 100644
index 0000000000..e2ef4caa70
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-diff2.json
@@ -0,0 +1,44 @@
+{
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "P", "path": "explicit/xpcom/category-manager", "kind": 1, "units": 0, "amount": 56849, "description": "Desc."},
+ {"process": "P", "path": "explicit/storage/prefixset/goog-phish-shavar", "kind": 1, "units": 0, "amount": 670000, "description": "Desc."},
+
+ {"process": "P", "path": "explicit/spell-check", "kind": 1, "units": 0, "amount": 3, "description": "Desc."},
+
+ {"process": "P", "path": "page-faults-soft", "kind": 2, "units": 2, "amount": 61013, "description": "Desc."},
+
+ {"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 1000, "description": "Desc."},
+ {"process": "P", "path": "canvas-2d-pixel-bytes", "kind": 2, "units": 0, "amount": 2000, "description": "Desc."},
+
+ {"process": "P", "path": "foobaz", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P", "path": "a/b", "kind": 2, "units": 0, "amount": 2000000, "description": "Desc."},
+ {"process": "P", "path": "a/c/d", "kind": 2, "units": 0, "amount": 2998000, "description": "Desc."},
+ {"process": "P", "path": "a/c/e", "kind": 2, "units": 0, "amount": 1001000, "description": "Desc."},
+ {"process": "P", "path": "a/c/f", "kind": 2, "units": 0, "amount": 3001000, "description": "Desc."},
+ {"process": "P", "path": "a/c/g", "kind": 2, "units": 0, "amount": 3001000, "description": "Desc."},
+ {"process": "P", "path": "a/h", "kind": 2, "units": 0, "amount": 2000, "description": "Desc."},
+
+ {"process": "P2 (pid 22)", "path": "p1 (pid 456)", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p2 (blah, pid=456)", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p3/zone(0x5678)/p3", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p4/js-zone(0x5678)/p4", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p5/worker(foo.com, 0x5678)/p5", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "explicit/window-objects/top(bar.com, id=456)/...", "kind": 0, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p6/z-moz-nullprincipal:{161effaa-c1f7-4010-a08e-e7c9aea01aed}/p6", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+ {"process": "P2 (pid 22)", "path": "p7/js-main-runtime-compartments/system/jar:file:\\\\\\temp_abc\\firefox\\omni.ja!/p7", "kind": 2, "units": 0, "amount": 44, "description": "Desc."},
+
+ {"process": "P4", "path": "p4", "kind": 2, "units": 0, "amount": 66, "description": "Desc."},
+
+ {"process": "P6", "path": "p6", "kind": 2, "units": 0, "amount": 0, "description": "Desc."},
+
+ {"process": "P7", "path": "p7/b", "kind": 2, "units": 0, "amount": 3, "description": "Desc."},
+ {"process": "P7", "path": "p7/c", "kind": 2, "units": 0, "amount": 4, "description": "Desc."},
+
+ {"process": "P8", "path": "p8/a/b", "kind": 2, "units": 0, "amount": 1, "description": "Desc."},
+ {"process": "P8", "path": "p8/a/g", "kind": 2, "units": 0, "amount": 2, "description": "Desc."}
+ ]
+}
+
diff --git a/toolkit/components/aboutmemory/tests/memory-reports-good.json b/toolkit/components/aboutmemory/tests/memory-reports-good.json
new file mode 100644
index 0000000000..013b4b1250
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/memory-reports-good.json
@@ -0,0 +1,29 @@
+{
+ "version": 1,
+ "hasMozMallocUsableSize": true,
+ "reports": [
+ {"process": "Main Process (pid NNN)", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."},
+ {"process": "Main Process (pid NNN)", "path": "other/b", "kind": 2, "units": 0, "amount": 104857, "description": "Other b."},
+ {"process": "Main Process (pid NNN)", "path": "other/a", "kind": 2, "units": 0, "amount": 209715, "description": "Other a."},
+ {"process": "Main Process (pid NNN)", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 52428800, "description": "A b."},
+
+ {"process": "Main Process (pid NNN)", "path": "size/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "rss/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "pss/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "swap/a", "kind": 1, "units": 0, "amount": 1024, "description": "non-sentence"},
+ {"process": "Main Process (pid NNN)", "path": "compartments/system/a", "kind": 1, "units": 0, "amount": 1024, "description": ""},
+ {"process": "Main Process (pid NNN)", "path": "ghost-windows/a", "kind": 1, "units": 0, "amount": 1024, "description": ""},
+
+ {"process": "Main Process (pid NNN)", "path": "redundant/should-be-ignored", "kind": 1, "units": 0, "amount": 1024, "description": ""},
+
+ {"process": "Heap-unclassified process", "path": "heap-allocated", "kind": 2, "units": 0, "amount": 262144000, "description": "Heap allocated."},
+ {"process": "Heap-unclassified process", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 52428800, "description": "A b."},
+ {"process": "Heap-unclassified process", "path": "explicit/heap-unclassified", "kind": 1, "units": 0, "amount": 209715200, "description": "Heap unclassified"},
+
+ {"process": "Explicit-only process", "path": "explicit/a/b", "kind": 1, "units": 0, "amount": 100000, "description": "A b."},
+
+ {"process": "Other-only process", "path": "a/b", "kind": 1, "units": 0, "amount": 100000, "description": "A b."},
+ {"process": "Other-only process", "path": "a/c", "kind": 1, "units": 0, "amount": 100000, "description": "A c."},
+ {"process": "Other-only process", "path": "heap-allocated", "kind": 1, "units": 0, "amount": 500000, "description": "D."}
+ ]
+}
diff --git a/toolkit/components/aboutmemory/tests/remote.xul b/toolkit/components/aboutmemory/tests/remote.xul
new file mode 100644
index 0000000000..7d69101305
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/remote.xul
@@ -0,0 +1,12 @@
+<?xml version="1.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="Remote browser"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- test results are displayed in the html:body -->
+ <p>Remote browser</p>
+
+ <browser type="content" src="about:blank" id="remote" remote="true"/>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
new file mode 100644
index 0000000000..bfeab4c7b4
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory.xul
@@ -0,0 +1,602 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file uses fake memory reporters to test the presentation of memory
+ reports in about:memory. test_memoryReporters.xul uses the real
+ memory reporters to test whether the memory reporters are producing
+ sensible results. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ SimpleTest.expectAssertions(27);
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cr = Components.results;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ // Hide all the real reporters; we'll restore them at the end.
+ mgr.blockRegistrationAndHideExistingReporters();
+
+ // Setup various fake-but-deterministic reporters.
+ const KB = 1024;
+ const MB = KB * KB;
+ const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+ const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+ const COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
+ const PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+
+ let fakeReporters = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aU, aA) {
+ aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("heap-allocated", OTHER, BYTES, 500 * MB);
+ f("heap-unallocated", OTHER, BYTES, 100 * MB);
+ f("explicit/a", HEAP, BYTES, 222 * MB);
+ f("explicit/b/a", HEAP, BYTES, 85 * MB);
+ f("explicit/b/b", HEAP, BYTES, 75 * MB);
+ f("explicit/b/c/a", HEAP, BYTES, 70 * MB);
+ f("explicit/b/c/b", HEAP, BYTES, 2 * MB); // omitted
+ f("explicit/g/a", HEAP, BYTES, 6 * MB);
+ f("explicit/g/b", HEAP, BYTES, 5 * MB);
+ f("explicit/g/other", HEAP, BYTES, 4 * MB);
+ // A degenerate tree with the same name as a non-degenerate tree should
+ // work ok.
+ f("explicit", OTHER, BYTES, 888 * MB);
+ f("other1/a/b", OTHER, BYTES, 111 * MB);
+ f("other1/c/d", OTHER, BYTES, 22 * MB);
+ f("other1/c/e", OTHER, BYTES, 33 * MB);
+ f("other4", OTHER, COUNT_CUMULATIVE, 777);
+ f("other4", OTHER, COUNT_CUMULATIVE, 111);
+ f("other3/a/b/c/d/e", OTHER, PERCENTAGE, 2000);
+ f("other3/a/b/c/d/f", OTHER, PERCENTAGE, 10);
+ f("other3/a/b/c/d/g", OTHER, PERCENTAGE, 5);
+ f("other3/a/b/c/d/g", OTHER, PERCENTAGE, 5);
+ // Check that a rounded-up-to-100.00% value is shown as "100.0%" (i.e. one
+ // decimal point).
+ f("other6/big", OTHER, COUNT, 99999);
+ f("other6/small", OTHER, COUNT, 1);
+ // Check that a 0 / 0 is handled correctly.
+ f("other7/zero", OTHER, BYTES, 0);
+ // These compartments ones shouldn't be displayed.
+ f("compartments/user/foo", OTHER, COUNT, 1);
+ f("compartments/system/foo", OTHER, COUNT, 1);
+ }
+ },
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aU, aA) {
+ aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("explicit/c/d", NONHEAP, BYTES, 13 * MB);
+ f("explicit/c/d", NONHEAP, BYTES, 10 * MB); // dup
+ f("explicit/c/other", NONHEAP, BYTES, 77 * MB);
+ f("explicit/cc", NONHEAP, BYTES, 13 * MB);
+ f("explicit/cc", NONHEAP, BYTES, 10 * MB); // dup
+ f("explicit/d", NONHEAP, BYTES, 499 * KB); // omitted
+ f("explicit/e", NONHEAP, BYTES, 100 * KB); // omitted
+ f("explicit/f/g/h/i", HEAP, BYTES, 10 * MB);
+ f("explicit/f/g/h/j", HEAP, BYTES, 10 * MB);
+ }
+ },
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aU, aA) {
+ aCbObj.callback("", aP, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("other3", OTHER, COUNT, 777);
+ f("other2", OTHER, BYTES, 222 * MB);
+ f("perc2", OTHER, PERCENTAGE, 10000);
+ f("perc1", OTHER, PERCENTAGE, 4567);
+ f("compartments/user/https:\\\\very-long-url.com\\very-long\\oh-so-long\\really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789", OTHER, COUNT, 1);
+ }
+ },
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP) {
+ aCbObj.callback("", aP, OTHER, COUNT, 1, "Desc.", aClosure);
+ }
+ f("compartments/user/bar");
+ f("compartments/system/bar");
+ }
+ }
+ ];
+ for (let i = 0; i < fakeReporters.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+ }
+
+ // The main process always comes first when we display about:memory. The
+ // remaining processes are sorted by their |resident| values (starting with
+ // the largest). Processes without a |resident| memory reporter are saved
+ // for the end.
+ let fakeReporters2 = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP1, aP2, aK, aU, aA) {
+ aCbObj.callback(aP1, aP2, aK, aU, aA, "Desc.", aClosure);
+ }
+ f("2nd", "heap-allocated", OTHER, BYTES,1000* MB);
+ f("2nd", "heap-unallocated",OTHER, BYTES,100 * MB);
+ f("2nd", "explicit/a/b/c", HEAP, BYTES,497 * MB);
+ f("2nd", "explicit/a/b/c", HEAP, BYTES, 1 * MB); // dup: merge
+ f("2nd", "explicit/a/b/c", HEAP, BYTES, 1 * MB); // dup: merge
+ f("2nd", "explicit/flip\\the\\backslashes",
+ HEAP, BYTES,200 * MB);
+ f("2nd", "explicit/compartment(compartment-url)",
+ HEAP, BYTES,200 * MB);
+ f("2nd", "other0", OTHER, BYTES,666 * MB);
+ f("2nd", "other1", OTHER, BYTES,111 * MB);
+
+ // Check that we can handle "heap-allocated" not being present.
+ f("3rd", "explicit/a/b", HEAP, BYTES,333 * MB);
+ f("3rd", "explicit/a/c", HEAP, BYTES,444 * MB);
+ f("3rd", "other1", OTHER, BYTES, 1 * MB);
+ f("3rd", "resident", OTHER, BYTES,100 * MB);
+
+ // Invalid values (negative, too-big) should be identified.
+ f("4th", "heap-allocated", OTHER, BYTES,100 * MB);
+ f("4th", "resident", OTHER, BYTES,200 * MB);
+ f("4th", "explicit/js/compartment(http:\\\\too-big.com\\)/stuff",
+ HEAP, BYTES,150 * MB);
+ f("4th", "explicit/ok", HEAP, BYTES, 5 * MB);
+ f("4th", "explicit/neg1", NONHEAP, BYTES, -2 * MB);
+ // -111 becomes "-0.00MB" in non-verbose mode, and getting the negative
+ // sign in there correctly is non-trivial.
+ f("4th", "other1", OTHER, BYTES,-111);
+ f("4th", "other2", OTHER, BYTES,-222 * MB);
+ f("4th", "other3", OTHER, COUNT, -333);
+ f("4th", "other4", OTHER, COUNT_CUMULATIVE, -444);
+ f("4th", "other5", OTHER, PERCENTAGE, -555);
+ f("4th", "other6", OTHER, PERCENTAGE, 66666);
+
+ // If a negative value is within a collapsed sub-tree in non-verbose mode,
+ // we should get the warning at the top and the relevant sub-trees should
+ // be expanded, even in non-verbose mode.
+ f("5th", "heap-allocated", OTHER, BYTES,100 * MB);
+ f("5th", "explicit/big", HEAP, BYTES, 99 * MB);
+ f("5th", "explicit/a/pos", HEAP, BYTES, 40 * KB);
+ f("5th", "explicit/a/neg1", NONHEAP, BYTES,-20 * KB);
+ f("5th", "explicit/a/neg2", NONHEAP, BYTES,-10 * KB);
+ f("5th", "explicit/b/c/d/e", NONHEAP, BYTES, 20 * KB);
+ f("5th", "explicit/b/c/d/f", NONHEAP, BYTES,-60 * KB);
+ f("5th", "explicit/b/c/g/h", NONHEAP, BYTES, 10 * KB);
+ f("5th", "explicit/b/c/i/j", NONHEAP, BYTES, 5 * KB);
+ }
+ }
+ ];
+ for (let i = 0; i < fakeReporters2.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters2[i]);
+ }
+ fakeReporters = fakeReporters.concat(fakeReporters2);
+ ]]>
+ </script>
+
+ <iframe id="amFrame" height="300" src="about:memory"></iframe>
+ <!-- vary the capitalization to make sure that works -->
+ <iframe id="amvFrame" height="300" src="About:Memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ let amExpectedText =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+623.58 MB (100.0%) -- explicit\n\
+├──232.00 MB (37.20%) -- b\n\
+│ ├───85.00 MB (13.63%) ── a\n\
+│ ├───75.00 MB (12.03%) ── b\n\
+│ └───72.00 MB (11.55%) -- c\n\
+│ ├──70.00 MB (11.23%) ── a\n\
+│ └───2.00 MB (00.32%) ── b\n\
+├──222.00 MB (35.60%) ── a\n\
+├──100.00 MB (16.04%) -- c\n\
+│ ├───77.00 MB (12.35%) ── other\n\
+│ └───23.00 MB (03.69%) ── d [2]\n\
+├───23.00 MB (03.69%) ── cc [2]\n\
+├───20.00 MB (03.21%) -- f/g/h\n\
+│ ├──10.00 MB (01.60%) ── i\n\
+│ └──10.00 MB (01.60%) ── j\n\
+├───15.00 MB (02.41%) ++ g\n\
+├───11.00 MB (01.76%) ── heap-unclassified\n\
+└────0.58 MB (00.09%) ++ (2 tiny)\n\
+\n\
+Other Measurements\n\
+\n\
+5 (100.0%) -- compartments\n\
+├──3 (60.00%) -- user\n\
+│ ├──1 (20.00%) ── bar\n\
+│ ├──1 (20.00%) ── foo\n\
+│ └──1 (20.00%) ── https://very-long-url.com/very-long/oh-so-long/really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789\n\
+└──2 (40.00%) -- system\n\
+ ├──1 (20.00%) ── bar\n\
+ └──1 (20.00%) ── foo\n\
+\n\
+166.00 MB (100.0%) -- other1\n\
+├──111.00 MB (66.87%) ── a/b\n\
+└───55.00 MB (33.13%) -- c\n\
+ ├──33.00 MB (19.88%) ── e\n\
+ └──22.00 MB (13.25%) ── d\n\
+\n\
+20.20% (100.0%) -- other3\n\
+└──20.20% (100.0%) -- a/b/c/d\n\
+ ├──20.00% (99.01%) ── e\n\
+ └───0.20% (00.99%) ++ (2 tiny)\n\
+\n\
+100,000 (100.0%) -- other6\n\
+├───99,999 (100.0%) ── big\n\
+└────────1 (00.00%) ── small\n\
+\n\
+0.00 MB (100.0%) -- other7\n\
+└──0.00 MB (100.0%) ── zero\n\
+\n\
+888.00 MB ── explicit\n\
+500.00 MB ── heap-allocated\n\
+100.00 MB ── heap-unallocated\n\
+222.00 MB ── other2\n\
+ 777 ── other3\n\
+ 888 ── other4 [2]\n\
+ 45.67% ── perc1\n\
+ 100.00% ── perc2\n\
+\n\
+End of Main Process\n\
+4th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/js/compartment(http://too-big.com/)/stuff\n\
+ explicit/(2 tiny)\n\
+ explicit/(2 tiny)/neg1\n\
+ explicit/(2 tiny)/heap-unclassified\n\
+ other1\n\
+ other2\n\
+ other3\n\
+ other4\n\
+ other5 \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+98.00 MB (100.0%) -- explicit\n\
+├──150.00 MB (153.06%) ── js/compartment(http://too-big.com/)/stuff [?!]\n\
+├───5.00 MB (05.10%) ── ok\n\
+└──-57.00 MB (-58.16%) -- (2 tiny) [?!]\n\
+ ├───-2.00 MB (-2.04%) ── neg1 [?!]\n\
+ └──-55.00 MB (-56.12%) ── heap-unclassified [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+ 100.00 MB ── heap-allocated\n\
+ -0.00 MB ── other1 [?!]\n\
+-222.00 MB ── other2 [?!]\n\
+ -333 ── other3 [?!]\n\
+ -444 ── other4 [?!]\n\
+ -5.55% ── other5 [?!]\n\
+ 666.66% ── other6\n\
+ 200.00 MB ── resident\n\
+\n\
+End of 4th\n\
+3rd\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+777.00 MB (100.0%) -- explicit\n\
+└──777.00 MB (100.0%) -- a\n\
+ ├──444.00 MB (57.14%) ── c\n\
+ └──333.00 MB (42.86%) ── b\n\
+\n\
+Other Measurements\n\
+\n\
+ 1.00 MB ── other1\n\
+100.00 MB ── resident\n\
+\n\
+End of 3rd\n\
+2nd\n\
+Explicit Allocations\n\
+\n\
+1,000.00 MB (100.0%) -- explicit\n\
+├────499.00 MB (49.90%) ── a/b/c [3]\n\
+├────200.00 MB (20.00%) ── compartment(compartment-url)\n\
+├────200.00 MB (20.00%) ── flip/the/backslashes\n\
+└────101.00 MB (10.10%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+\n\
+1,000.00 MB ── heap-allocated\n\
+ 100.00 MB ── heap-unallocated\n\
+ 666.00 MB ── other0\n\
+ 111.00 MB ── other1\n\
+\n\
+End of 2nd\n\
+5th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/(3 tiny)/a/neg2\n\
+ explicit/(3 tiny)/a/neg1\n\
+ explicit/(3 tiny)/b/c\n\
+ explicit/(3 tiny)/b/c/d\n\
+ explicit/(3 tiny)/b/c/d/f \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+99.95 MB (100.0%) -- explicit\n\
+├──99.00 MB (99.05%) ── big\n\
+└───0.95 MB (00.95%) -- (3 tiny)\n\
+ ├──0.96 MB (00.96%) ── heap-unclassified\n\
+ ├──0.01 MB (00.01%) -- a\n\
+ │ ├──0.04 MB (00.04%) ── pos\n\
+ │ ├──-0.01 MB (-0.01%) ── neg2 [?!]\n\
+ │ └──-0.02 MB (-0.02%) ── neg1 [?!]\n\
+ └──-0.02 MB (-0.02%) -- b/c [?!]\n\
+ ├───0.01 MB (00.01%) ── g/h\n\
+ ├───0.00 MB (00.00%) ── i/j\n\
+ └──-0.04 MB (-0.04%) -- d [?!]\n\
+ ├───0.02 MB (00.02%) ── e\n\
+ └──-0.06 MB (-0.06%) ── f [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+100.00 MB ── heap-allocated\n\
+\n\
+End of 5th\n\
+";
+
+ let amvExpectedText =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+653,876,224 B (100.0%) -- explicit\n\
+├──243,269,632 B (37.20%) -- b\n\
+│ ├───89,128,960 B (13.63%) ── a\n\
+│ ├───78,643,200 B (12.03%) ── b\n\
+│ └───75,497,472 B (11.55%) -- c\n\
+│ ├──73,400,320 B (11.23%) ── a\n\
+│ └───2,097,152 B (00.32%) ── b\n\
+├──232,783,872 B (35.60%) ── a\n\
+├──104,857,600 B (16.04%) -- c\n\
+│ ├───80,740,352 B (12.35%) ── other\n\
+│ └───24,117,248 B (03.69%) ── d [2]\n\
+├───24,117,248 B (03.69%) ── cc [2]\n\
+├───20,971,520 B (03.21%) -- f/g/h\n\
+│ ├──10,485,760 B (01.60%) ── i\n\
+│ └──10,485,760 B (01.60%) ── j\n\
+├───15,728,640 B (02.41%) -- g\n\
+│ ├───6,291,456 B (00.96%) ── a\n\
+│ ├───5,242,880 B (00.80%) ── b\n\
+│ └───4,194,304 B (00.64%) ── other\n\
+├───11,534,336 B (01.76%) ── heap-unclassified\n\
+├──────510,976 B (00.08%) ── d\n\
+└──────102,400 B (00.02%) ── e\n\
+\n\
+Other Measurements\n\
+\n\
+5 (100.0%) -- compartments\n\
+├──3 (60.00%) -- user\n\
+│ ├──1 (20.00%) ── bar\n\
+│ ├──1 (20.00%) ── foo\n\
+│ └──1 (20.00%) ── https://very-long-url.com/very-long/oh-so-long/really-quite-long.html?a=2&b=3&c=4&d=5&e=abcdefghijklmnopqrstuvwxyz&f=123456789123456789123456789\n\
+└──2 (40.00%) -- system\n\
+ ├──1 (20.00%) ── bar\n\
+ └──1 (20.00%) ── foo\n\
+\n\
+174,063,616 B (100.0%) -- other1\n\
+├──116,391,936 B (66.87%) ── a/b\n\
+└───57,671,680 B (33.13%) -- c\n\
+ ├──34,603,008 B (19.88%) ── e\n\
+ └──23,068,672 B (13.25%) ── d\n\
+\n\
+20.20% (100.0%) -- other3\n\
+└──20.20% (100.0%) -- a/b/c/d\n\
+ ├──20.00% (99.01%) ── e\n\
+ ├───0.10% (00.50%) ── f\n\
+ └───0.10% (00.50%) ── g [2]\n\
+\n\
+100,000 (100.0%) -- other6\n\
+├───99,999 (100.0%) ── big\n\
+└────────1 (00.00%) ── small\n\
+\n\
+0 B (100.0%) -- other7\n\
+└──0 B (100.0%) ── zero\n\
+\n\
+931,135,488 B ── explicit\n\
+524,288,000 B ── heap-allocated\n\
+104,857,600 B ── heap-unallocated\n\
+232,783,872 B ── other2\n\
+ 777 ── other3\n\
+ 888 ── other4 [2]\n\
+ 45.67% ── perc1\n\
+ 100.00% ── perc2\n\
+\n\
+End of Main Process\n\
+4th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/js/compartment(http://too-big.com/)/stuff\n\
+ explicit/neg1\n\
+ explicit/heap-unclassified\n\
+ other1\n\
+ other2\n\
+ other3\n\
+ other4\n\
+ other5 \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+102,760,448 B (100.0%) -- explicit\n\
+├──157,286,400 B (153.06%) ── js/compartment(http://too-big.com/)/stuff [?!]\n\
+├────5,242,880 B (05.10%) ── ok\n\
+├───-2,097,152 B (-2.04%) ── neg1 [?!]\n\
+└──-57,671,680 B (-56.12%) ── heap-unclassified [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+ 104,857,600 B ── heap-allocated\n\
+ -111 B ── other1 [?!]\n\
+-232,783,872 B ── other2 [?!]\n\
+ -333 ── other3 [?!]\n\
+ -444 ── other4 [?!]\n\
+ -5.55% ── other5 [?!]\n\
+ 666.66% ── other6\n\
+ 209,715,200 B ── resident\n\
+\n\
+End of 4th\n\
+3rd\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+814,743,552 B (100.0%) -- explicit\n\
+└──814,743,552 B (100.0%) -- a\n\
+ ├──465,567,744 B (57.14%) ── c\n\
+ └──349,175,808 B (42.86%) ── b\n\
+\n\
+Other Measurements\n\
+\n\
+ 1,048,576 B ── other1\n\
+104,857,600 B ── resident\n\
+\n\
+End of 3rd\n\
+2nd\n\
+Explicit Allocations\n\
+\n\
+1,048,576,000 B (100.0%) -- explicit\n\
+├────523,239,424 B (49.90%) ── a/b/c [3]\n\
+├────209,715,200 B (20.00%) ── compartment(compartment-url)\n\
+├────209,715,200 B (20.00%) ── flip/the/backslashes\n\
+└────105,906,176 B (10.10%) ── heap-unclassified\n\
+\n\
+Other Measurements\n\
+\n\
+1,048,576,000 B ── heap-allocated\n\
+ 104,857,600 B ── heap-unallocated\n\
+ 698,351,616 B ── other0\n\
+ 116,391,936 B ── other1\n\
+\n\
+End of 2nd\n\
+5th\n\
+\n\
+WARNING: the following values are negative or unreasonably large.\n\
+\n\
+ explicit/a/neg2\n\
+ explicit/a/neg1\n\
+ explicit/b/c\n\
+ explicit/b/c/d\n\
+ explicit/b/c/d/f \n\
+\n\
+This indicates a defect in one or more memory reporters. The invalid values are highlighted.\n\
+Explicit Allocations\n\
+\n\
+104,801,280 B (100.0%) -- explicit\n\
+├──103,809,024 B (99.05%) ── big\n\
+├────1,007,616 B (00.96%) ── heap-unclassified\n\
+├───────10,240 B (00.01%) -- a\n\
+│ ├──40,960 B (00.04%) ── pos\n\
+│ ├──-10,240 B (-0.01%) ── neg2 [?!]\n\
+│ └──-20,480 B (-0.02%) ── neg1 [?!]\n\
+└──────-25,600 B (-0.02%) -- b/c [?!]\n\
+ ├───10,240 B (00.01%) ── g/h\n\
+ ├────5,120 B (00.00%) ── i/j\n\
+ └──-40,960 B (-0.04%) -- d [?!]\n\
+ ├───20,480 B (00.02%) ── e\n\
+ └──-61,440 B (-0.06%) ── f [?!]\n\
+\n\
+Other Measurements\n\
+\n\
+104,857,600 B ── heap-allocated\n\
+\n\
+End of 5th\n\
+";
+
+ function finish()
+ {
+ mgr.unblockRegistrationAndRestoreOriginalReporters();
+ SimpleTest.finish();
+ }
+
+ // Cut+paste the entire page and check that the cut text matches what we
+ // expect. This tests the output in general and also that the cutting and
+ // pasting works as expected.
+ function test(aFrameId, aVerbose, aExpected, aNext) {
+ SimpleTest.executeSoon(function() {
+ ok(document.title === "about:memory", "document.title is correct");
+ let mostRecentActual;
+ let frame = document.getElementById(aFrameId);
+ frame.focus();
+
+ // Set the verbose checkbox value and click the go button.
+ let doc = frame.contentWindow.document;
+ let measureButton = doc.getElementById("measureButton");
+ let verbose = doc.getElementById("verbose");
+ verbose.checked = aVerbose;
+ measureButton.click();
+
+ SimpleTest.waitForClipboard(
+ function(aActual) {
+ mostRecentActual = aActual;
+ let rslt = aActual.trim() === aExpected.trim();
+ if (!rslt) {
+ // Try copying again.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ }
+
+ return rslt;
+ },
+ function() {
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ },
+ aNext,
+ function() {
+ ok(false, "pasted text doesn't match for " + aFrameId);
+ dump("******EXPECTED******\n");
+ dump("<<<" + aExpected + ">>>\n");
+ dump("*******ACTUAL*******\n");
+ dump("<<<" + mostRecentActual + ">>>\n");
+ dump("********************\n");
+ finish();
+ }
+ );
+ });
+ }
+
+ SimpleTest.waitForFocus(function() {
+ test(
+ "amFrame",
+ /* verbose = */ false,
+ amExpectedText,
+ function() {
+ test(
+ "amvFrame",
+ /* verbose = */ true,
+ amvExpectedText,
+ function() {
+ finish()
+ }
+ )
+ }
+ );
+ });
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
new file mode 100644
index 0000000000..8cf197e6d1
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory2.xul
@@ -0,0 +1,423 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the collapsing and expanding of sub-trees in
+ about:memory. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ // Hide all the real reporters; we'll restore them at the end.
+ mgr.blockRegistrationAndHideExistingReporters();
+
+ // Setup various fake-but-deterministic reporters.
+ const KB = 1024;
+ const MB = KB * KB;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+
+ let hiPath = "explicit/h/i";
+ let hi2Path = "explicit/h/i2";
+ let jkPath = "explicit/j/k";
+ let jk2Path = "explicit/j/k2";
+
+ let fakeReporters = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aA) {
+ aCbObj.callback("", aP, aK, BYTES, aA, "Desc.", aClosure);
+ }
+ f("heap-allocated", OTHER, 250 * MB);
+ f("explicit/a/b", HEAP, 50 * MB);
+ f("explicit/a/c/d", HEAP, 25 * MB);
+ f("explicit/a/c/e", HEAP, 15 * MB);
+ f("explicit/a/f", HEAP, 30 * MB);
+ f("explicit/g", HEAP, 100 * MB);
+ f(hiPath, HEAP, 10 * MB);
+ f(hi2Path, HEAP, 9 * MB);
+ f(jkPath, HEAP, 0.5 * MB);
+ f(jk2Path, HEAP, 0.3 * MB);
+ f("explicit/a/l/m", HEAP, 0.1 * MB);
+ f("explicit/a/l/n", HEAP, 0.1 * MB);
+ }
+ }
+ ];
+
+ for (let i = 0; i < fakeReporters.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+ }
+
+ ]]>
+ </script>
+
+ <iframe id="amFrame" height="500" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function finish()
+ {
+ mgr.unblockRegistrationAndRestoreOriginalReporters();
+ SimpleTest.finish();
+ }
+
+ // Click on the identified element, then cut+paste the entire page and
+ // check that the cut text matches what we expect.
+ function test(aId, aSwap, aExpected, aNext) {
+ let win = document.getElementById("amFrame").contentWindow;
+ if (aId) {
+ let node = win.document.getElementById(aId);
+
+ // Yuk: clicking a button is easy; but for tree entries we need to
+ // click on a child of the span identified via |id|.
+ if (node.nodeName === "button") {
+ if (aSwap) {
+ // We swap hipath/hi2Path and jkPath/jk2Path just before updating, to
+ // test what happens when significant nodes become insignificant and
+ // vice versa.
+ hiPath = "explicit/j/k";
+ hi2Path = "explicit/j/k2";
+ jkPath = "explicit/h/i";
+ jk2Path = "explicit/h/i2";
+ }
+ node.click();
+ } else {
+ node.childNodes[0].click();
+ }
+ }
+
+ SimpleTest.executeSoon(function() {
+ let mostRecentActual;
+ document.getElementById("amFrame").focus();
+ SimpleTest.waitForClipboard(
+ function(aActual) {
+ mostRecentActual = aActual;
+ let rslt = aActual.trim() === aExpected.trim();
+ if (!rslt) {
+ // Try copying again.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ }
+
+ return rslt;
+ },
+ function() {
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ },
+ aNext,
+ function() {
+ ok(false, "pasted text doesn't match");
+ dump("******EXPECTED******\n");
+ dump(aExpected);
+ dump("*******ACTUAL*******\n");
+ dump(mostRecentActual);
+ dump("********************\n");
+ finish();
+ }
+ );
+ });
+ }
+
+ // Returns a function that chains together one test() call per id.
+ function chain(aIds) {
+ let x = aIds.shift();
+ if (x) {
+ return function() { test(x.id, x.swap, x.expected, chain(aIds)); }
+ } else {
+ return function() { finish(); };
+ }
+ }
+
+ let startExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) -- c\n\
+│ │ ├──25.00 MB (10.00%) ── d\n\
+│ │ └──15.00 MB (06.00%) ── e\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let acCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) ++ c\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let alExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) ++ c\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) -- l\n\
+│ ├──0.10 MB (00.04%) ── m\n\
+│ └──0.10 MB (00.04%) ── n\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let aCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- h\n\
+│ ├──10.00 MB (04.00%) ── i\n\
+│ └───9.00 MB (03.60%) ── i2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let hCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) ++ h\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ j\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let jExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) ++ h\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) -- j\n\
+ ├──0.50 MB (00.20%) ── k\n\
+ └──0.30 MB (00.12%) ── k2\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ // The important thing here is that two values have been swapped.
+ // explicit/h/i should remain collapsed, and explicit/j/k should remain
+ // expanded. See bug 724863.
+ let updatedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) ++ a\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let aExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) ++ c\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) -- l\n\
+│ ├──0.10 MB (00.04%) ── m\n\
+│ └──0.10 MB (00.04%) ── n\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let acExpandedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) -- c\n\
+│ │ ├──25.00 MB (10.00%) ── d\n\
+│ │ └──15.00 MB (06.00%) ── e\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) -- l\n\
+│ ├──0.10 MB (00.04%) ── m\n\
+│ └──0.10 MB (00.04%) ── n\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ let alCollapsedExpected =
+"\
+Main Process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──120.20 MB (48.08%) -- a\n\
+│ ├───50.00 MB (20.00%) ── b\n\
+│ ├───40.00 MB (16.00%) -- c\n\
+│ │ ├──25.00 MB (10.00%) ── d\n\
+│ │ └──15.00 MB (06.00%) ── e\n\
+│ ├───30.00 MB (12.00%) ── f\n\
+│ └────0.20 MB (00.08%) ++ l\n\
+├──100.00 MB (40.00%) ── g\n\
+├───19.00 MB (07.60%) -- j\n\
+│ ├──10.00 MB (04.00%) ── k\n\
+│ └───9.00 MB (03.60%) ── k2\n\
+├───10.00 MB (04.00%) ── heap-unclassified\n\
+└────0.80 MB (00.32%) ++ h\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process\n\
+";
+
+ // Test the following cases:
+ // - explicit/a/c is significant, we collapse it, it's unchanged upon
+ // update, we re-expand it
+ // - explicit/a/l is insignificant, we expand it, it's unchanged upon
+ // update, we re-collapse it
+ // - explicit/a is significant, we collapse it (which hides its
+ // sub-trees), it's unchanged upon update, we re-expand it
+ // - explicit/h is significant, we collapse it, it becomes insignificant
+ // upon update (and should remain collapsed)
+ // - explicit/j is insignificant, we expand it, it becomes significant
+ // upon update (and should remain expanded)
+ //
+ let idsToClick = [
+ { id: "measureButton", swap: 0, expected: startExpected },
+ { id: "Main Process:explicit/a/c", swap: 0, expected: acCollapsedExpected },
+ { id: "Main Process:explicit/a/l", swap: 0, expected: alExpandedExpected },
+ { id: "Main Process:explicit/a", swap: 0, expected: aCollapsedExpected },
+ { id: "Main Process:explicit/h", swap: 0, expected: hCollapsedExpected },
+ { id: "Main Process:explicit/j", swap: 0, expected: jExpandedExpected },
+ { id: "measureButton", swap: 1, expected: updatedExpected },
+ { id: "Main Process:explicit/a", swap: 0, expected: aExpandedExpected },
+ { id: "Main Process:explicit/a/c", swap: 0, expected: acExpandedExpected },
+ { id: "Main Process:explicit/a/l", swap: 0, expected: alCollapsedExpected }
+ ];
+
+ SimpleTest.waitForFocus(chain(idsToClick));
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
new file mode 100644
index 0000000000..c712070cc2
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory3.xul
@@ -0,0 +1,515 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the saving and loading of memory reports to/from file in
+ about:memory. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ // Hide all the real reporters; we'll restore them at the end.
+ mgr.blockRegistrationAndHideExistingReporters();
+
+ // Setup a minimal number of fake reporters.
+ const KB = 1024;
+ const MB = KB * KB;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+
+ let fakeReporters = [
+ { collectReports: function(aCbObj, aClosure, aAnonymize) {
+ function f(aP, aK, aA, aD) {
+ aCbObj.callback("", aP, aK, BYTES, aA, aD, aClosure);
+ }
+ f("heap-allocated", OTHER, 250 * MB, "Heap allocated.");
+ f("explicit/a/b", HEAP, 50 * MB, "A b.");
+ f("other/a", OTHER, 0.2 * MB, "Other a.");
+ f("other/b", OTHER, 0.1 * MB, "Other b.");
+ }
+ }
+ ];
+
+ for (let i = 0; i < fakeReporters.length; i++) {
+ mgr.registerStrongReporterEvenIfBlocked(fakeReporters[i]);
+ }
+
+ ]]>
+ </script>
+
+ <iframe id="amFrame" height="400" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function finish()
+ {
+ mgr.unblockRegistrationAndRestoreOriginalReporters();
+ SimpleTest.finish();
+ }
+
+ // Load the given file into the frame, then copy+paste the entire frame and
+ // check that the cut text matches what we expect.
+ function test(aFilename, aFilename2, aExpected, aDumpFirst, aVerbose, aNext) {
+ let frame = document.getElementById("amFrame");
+ frame.focus();
+
+ let doc = frame.contentWindow.document;
+ let verbosity = doc.getElementById("verbose");
+ verbosity.checked = aVerbose;
+
+ function getFilePath(aFilename) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ file.append("chrome");
+ file.append("toolkit");
+ file.append("components");
+ file.append("aboutmemory");
+ file.append("tests");
+ file.append(aFilename);
+ return file.path;
+ }
+
+ let filePath = getFilePath(aFilename);
+
+ let e = document.createEvent('Event');
+ e.initEvent('change', true, true);
+
+ function check() {
+ // Initialize the clipboard contents.
+ SpecialPowers.clipboardCopyString("initial clipboard value");
+
+ let numFailures = 0, maxFailures = 30;
+
+ // Because the file load is async, we don't know when it will finish and
+ // the output will show up. So we poll.
+ function copyPasteAndCheck() {
+ // Copy and paste frame contents, and filter out non-deterministic
+ // differences.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ actual = actual.replace(/\(pid \d+\)/g, "(pid NNN)");
+
+ if (actual.trim() === aExpected.trim()) {
+ SimpleTest.ok(true, "Clipboard has the expected contents");
+ aNext();
+ } else {
+ numFailures++;
+ if (numFailures === maxFailures) {
+ ok(false, "pasted text doesn't match");
+ dump("******EXPECTED******\n");
+ dump(aExpected);
+ dump("*******ACTUAL*******\n");
+ dump(actual);
+ dump("********************\n");
+ finish();
+ } else {
+ setTimeout(copyPasteAndCheck, 100);
+ }
+ }
+ }
+ copyPasteAndCheck();
+ }
+
+ if (!aFilename2) {
+ function loadAndCheck() {
+ let fileInput1 =
+ frame.contentWindow.document.getElementById("fileInput1");
+ fileInput1.value = filePath; // this works because it's a chrome test
+
+ fileInput1.dispatchEvent(e);
+ check();
+ }
+
+ if (aDumpFirst) {
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+ dumper.dumpMemoryReportsToNamedFile(filePath, loadAndCheck, null,
+ /* anonymize = */ false);
+ } else {
+ loadAndCheck();
+ }
+
+ } else {
+ let fileInput2 =
+ frame.contentWindow.document.getElementById("fileInput2");
+ fileInput2.value = filePath; // this works because it's a chrome test
+
+ // Hack alert: fileInput2's onchange handler calls fileInput2.click().
+ // But we don't want that to happen, because we want to bypass the file
+ // picker for the test. So we set |e.skipClick|, which causes
+ // fileInput2.click() to be skipped, and dispatch the second change event
+ // directly ourselves.
+
+ e.skipClick = true;
+ fileInput2.dispatchEvent(e);
+
+ let filePath2 = getFilePath(aFilename2);
+ fileInput2.value = filePath2; // this works because it's a chrome test
+
+ let e2 = document.createEvent('Event');
+ e2.initEvent('change', true, true);
+ fileInput2.dispatchEvent(e);
+
+ check();
+ }
+ }
+
+ // Returns a function that chains together multiple test() calls.
+ function chain(aPieces) {
+ let x = aPieces.shift();
+ if (x) {
+ return function() { test(x.filename, x.filename2, x.expected, x.dumpFirst, x.verbose, chain(aPieces)); }
+ } else {
+ return function() { finish(); };
+ }
+ }
+
+ let expectedGood =
+"\
+Explicit-only process\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+100,000 B (100.0%) -- explicit\n\
+└──100,000 B (100.0%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+End of Explicit-only process\n\
+Heap-unclassified process\n\
+Explicit Allocations\n\
+\n\
+262,144,000 B (100.0%) -- explicit\n\
+├──209,715,200 B (80.00%) ── heap-unclassified\n\
+└───52,428,800 B (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+262,144,000 B ── heap-allocated\n\
+\n\
+End of Heap-unclassified process\n\
+Main Process (pid NNN)\n\
+Explicit Allocations\n\
+\n\
+262,144,000 B (100.0%) -- explicit\n\
+├──209,715,200 B (80.00%) ── heap-unclassified\n\
+└───52,428,800 B (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+1,024 B (100.0%) -- compartments\n\
+└──1,024 B (100.0%) ── system/a\n\
+\n\
+1,024 B (100.0%) -- ghost-windows\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+314,572 B (100.0%) -- other\n\
+├──209,715 B (66.67%) ── a\n\
+└──104,857 B (33.33%) ── b\n\
+\n\
+1,024 B (100.0%) -- pss\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+1,024 B (100.0%) -- rss\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+1,024 B (100.0%) -- size\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+1,024 B (100.0%) -- swap\n\
+└──1,024 B (100.0%) ── a\n\
+\n\
+262,144,000 B ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+Other-only process\n\
+Other Measurements\n\
+\n\
+200,000 B (100.0%) -- a\n\
+├──100,000 B (50.00%) ── b\n\
+└──100,000 B (50.00%) ── c\n\
+\n\
+500,000 B ── heap-allocated\n\
+\n\
+End of Other-only process\n\
+";
+
+ let expectedGood2 =
+"\
+Main Process (pid NNN)\n\
+Explicit Allocations\n\
+\n\
+262,144,000 B (100.0%) -- explicit\n\
+├──209,715,200 B (80.00%) ── heap-unclassified\n\
+└───52,428,800 B (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+314,572 B (100.0%) -- other\n\
+├──209,715 B (66.67%) ── a\n\
+└──104,857 B (33.33%) ── b\n\
+\n\
+262,144,000 B ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+";
+
+ // This is the output for a malformed data file.
+ let expectedBad =
+"\
+Error: Invalid memory report(s): missing 'hasMozMallocUsableSize' property\
+";
+
+ // This is the output for a non-verbose diff.
+ let expectedDiffNonVerbose =
+"\
+P\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+-0.01 MB (100.0%) -- explicit\n\
+├──-0.01 MB (99.95%) ── storage/prefixset/goog-phish-shavar\n\
+└──-0.00 MB (00.05%) ++ (2 tiny)\n\
+\n\
+Other Measurements\n\
+\n\
+0.96 MB (100.0%) -- a\n\
+├──0.95 MB (99.80%) ── b\n\
+├──0.00 MB (00.10%) -- c\n\
+│ ├──-0.95 MB (-99.70%) ── e\n\
+│ ├──0.95 MB (99.60%) ── d\n\
+│ └──0.00 MB (00.20%) ++ (2 tiny)\n\
+└──0.00 MB (00.10%) ── h\n\
+\n\
+ 0.00 MB ── canvas-2d-pixel-bytes [2] [+]\n\
+-0.00 MB ── foobar [-]\n\
+\n\
+End of P\n\
+P2 (pid NNN)\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+0.00 MB (100.0%) -- explicit\n\
+└──0.00 MB (100.0%) ── window-objects/top(bar.com, id=NNN)/...\n\
+\n\
+Other Measurements\n\
+\n\
+0.00 MB (100.0%) -- p3\n\
+└──0.00 MB (100.0%) ── zone(0xNNN)/p3\n\
+\n\
+0.00 MB (100.0%) -- p4\n\
+└──0.00 MB (100.0%) ── js-zone(0xNNN)/p4\n\
+\n\
+0.00 MB (100.0%) -- p5\n\
+└──0.00 MB (100.0%) ── worker(foo.com, 0xNNN)/p5\n\
+\n\
+0.00 MB (100.0%) -- p6\n\
+└──0.00 MB (100.0%) ── z-moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}/p6\n\
+\n\
+0.00 MB (100.0%) -- p7\n\
+└──0.00 MB (100.0%) ── js-main-runtime-compartments/system/jar:file:///.../omni.ja!/p7\n\
+\n\
+0.00 MB ── p1 (pid NNN)\n\
+0.00 MB ── p2 (blah, pid=NNN)\n\
+\n\
+End of P2 (pid NNN)\n\
+P3\n\
+Other Measurements\n\
+\n\
+-0.00 MB ── p3 [-]\n\
+\n\
+End of P3\n\
+P4\n\
+Other Measurements\n\
+\n\
+0.00 MB ── p4 [+]\n\
+\n\
+End of P4\n\
+P7\n\
+Other Measurements\n\
+\n\
+0.00 MB (100.0%) -- p7\n\
+├──0.00 MB (57.14%) ── c [+]\n\
+└──0.00 MB (42.86%) ── b [+]\n\
+\n\
+-0.00 MB ── p7 [-]\n\
+\n\
+End of P7\n\
+P8\n\
+Other Measurements\n\
+\n\
+-0.00 MB (100.0%) -- p8\n\
+└──-0.00 MB (100.0%) -- a\n\
+ ├──-0.00 MB (50.00%) -- b\n\
+ │ ├──-0.00 MB (31.82%) -- c\n\
+ │ │ ├──-0.00 MB (18.18%) ── e [-]\n\
+ │ │ └──-0.00 MB (13.64%) ── d [-]\n\
+ │ ├──-0.00 MB (22.73%) ── f [-]\n\
+ │ └───0.00 MB (-4.55%) ── (fake child) [!]\n\
+ └──-0.00 MB (50.00%) -- g\n\
+ ├──-0.00 MB (31.82%) ── i [-]\n\
+ ├──-0.00 MB (27.27%) ── h [-]\n\
+ └───0.00 MB (-9.09%) ── (fake child) [!]\n\
+\n\
+End of P8\n\
+";
+
+ // This is the output for a verbose diff.
+ let expectedDiffVerbose =
+"\
+P\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+-10,005 B (100.0%) -- explicit\n\
+├──-10,000 B (99.95%) ── storage/prefixset/goog-phish-shavar\n\
+├───────-6 B (00.06%) ── spell-check [2]\n\
+└────────1 B (-0.01%) ── xpcom/category-manager\n\
+\n\
+Other Measurements\n\
+\n\
+1,002,000 B (100.0%) -- a\n\
+├──1,000,000 B (99.80%) ── b\n\
+├──────1,000 B (00.10%) -- c\n\
+│ ├──-999,000 B (-99.70%) ── e\n\
+│ ├──998,000 B (99.60%) ── d\n\
+│ ├──1,000 B (00.10%) ── f\n\
+│ └──1,000 B (00.10%) ── g\n\
+└──────1,000 B (00.10%) ── h\n\
+\n\
+3,000 B ── canvas-2d-pixel-bytes [2] [+]\n\
+ -100 B ── foobar [-]\n\
+\n\
+End of P\n\
+P2 (pid NNN)\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+11 B (100.0%) -- explicit\n\
+└──11 B (100.0%) ── window-objects/top(bar.com, id=NNN)/...\n\
+\n\
+Other Measurements\n\
+\n\
+11 B (100.0%) -- p3\n\
+└──11 B (100.0%) ── zone(0xNNN)/p3\n\
+\n\
+11 B (100.0%) -- p4\n\
+└──11 B (100.0%) ── js-zone(0xNNN)/p4\n\
+\n\
+11 B (100.0%) -- p5\n\
+└──11 B (100.0%) ── worker(foo.com, 0xNNN)/p5\n\
+\n\
+11 B (100.0%) -- p6\n\
+└──11 B (100.0%) ── z-moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}/p6\n\
+\n\
+11 B (100.0%) -- p7\n\
+└──11 B (100.0%) ── js-main-runtime-compartments/system/jar:file:///.../omni.ja!/p7\n\
+\n\
+11 B ── p1 (pid NNN)\n\
+11 B ── p2 (blah, pid=NNN)\n\
+\n\
+End of P2 (pid NNN)\n\
+P3\n\
+Other Measurements\n\
+\n\
+-55 B ── p3 [-]\n\
+\n\
+End of P3\n\
+P4\n\
+Other Measurements\n\
+\n\
+66 B ── p4 [+]\n\
+\n\
+End of P4\n\
+P7\n\
+Other Measurements\n\
+\n\
+7 B (100.0%) -- p7\n\
+├──4 B (57.14%) ── c [+]\n\
+└──3 B (42.86%) ── b [+]\n\
+\n\
+-5 B ── p7 [-]\n\
+\n\
+End of P7\n\
+P8\n\
+Other Measurements\n\
+\n\
+-22 B (100.0%) -- p8\n\
+└──-22 B (100.0%) -- a\n\
+ ├──-11 B (50.00%) -- b\n\
+ │ ├───-7 B (31.82%) -- c\n\
+ │ │ ├──-4 B (18.18%) ── e [-]\n\
+ │ │ └──-3 B (13.64%) ── d [-]\n\
+ │ ├───-5 B (22.73%) ── f [-]\n\
+ │ └────1 B (-4.55%) ── (fake child) [!]\n\
+ └──-11 B (50.00%) -- g\n\
+ ├───-7 B (31.82%) ── i [-]\n\
+ ├───-6 B (27.27%) ── h [-]\n\
+ └────2 B (-9.09%) ── (fake child) [!]\n\
+\n\
+End of P8\n\
+";
+
+ // This is the output for the crash reports diff.
+ let expectedDiff2 =
+"\
+Main Process (pid NNN)\n\
+Other Measurements\n\
+\n\
+1 B ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+";
+
+ let frames = [
+ // This loads a pre-existing memory reports file that is valid.
+ { filename: "memory-reports-good.json", expected: expectedGood, dumpFirst: false, verbose: true },
+
+ // This loads a pre-existing crash dump file that is valid.
+ { filename: "crash-dump-good.json", expected: expectedGood2, dumpFirst: false, verbose: true },
+
+ // This dumps to a file and then reads it back in. (The result is the same
+ // as the previous test.)
+ { filename: "memory-reports-dumped.json.gz", expected: expectedGood2, dumpFirst: true, verbose: true },
+
+ // This loads a pre-existing file that is invalid.
+ { filename: "memory-reports-bad.json", expected: expectedBad, dumpFirst: false, verbose: true },
+
+ // This diffs two pre-existing memory reports files.
+ { filename: "memory-reports-diff1.json", filename2: "memory-reports-diff2.json", expected: expectedDiffNonVerbose, dumpFirst: false, verbose: false },
+
+ // Ditto.
+ { filename: "memory-reports-diff1.json", filename2: "memory-reports-diff2.json", expected: expectedDiffVerbose, dumpFirst: false, verbose: true },
+
+ // This diffs two pre-existing crash report files.
+ { filename: "crash-dump-diff1.json", filename2: "crash-dump-diff2.json", expected: expectedDiff2, dumpFirst: false, verbose: true }
+ ];
+
+ SimpleTest.waitForFocus(chain(frames));
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory4.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xul
new file mode 100644
index 0000000000..f2c752ac57
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory4.xul
@@ -0,0 +1,179 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the loading of memory reports from file when specified
+ in about:memory's URL (via the "file=" suffix). -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ function makePathname(aFilename) {
+ let file = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ file.append("chrome");
+ file.append("toolkit");
+ file.append("components");
+ file.append("aboutmemory");
+ file.append("tests");
+ file.append(aFilename);
+ return file.path;
+ }
+
+ // Load the given file into the frame, then copy+paste the entire frame and
+ // check that the cut text matches what we expect.
+ function test(aFilename, aExpected, aNext) {
+ let frame = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe")
+ frame.height = 300;
+ frame.src = "about:memory?file=" + makePathname(aFilename);
+ document.documentElement.appendChild(frame);
+ frame.focus();
+
+ // Initialize the clipboard contents.
+ SpecialPowers.clipboardCopyString("initial clipboard value");
+
+ let numFailures = 0, maxFailures = 30;
+
+ // Because the file load is async, we don't know when it will finish and
+ // the output will show up. So we poll.
+ function copyPasteAndCheck() {
+ // Copy and paste frame contents, and filter out non-deterministic
+ // differences.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+ actual = actual.replace(/\(pid \d+\)/, "(pid NNN)");
+
+ if (actual.trim() === aExpected.trim()) {
+ SimpleTest.ok(true, "Clipboard has the expected contents");
+ aNext();
+ } else {
+ numFailures++;
+ if (numFailures === maxFailures) {
+ ok(false, "pasted text doesn't match");
+ dump("******EXPECTED******\n");
+ dump(aExpected);
+ dump("*******ACTUAL*******\n");
+ dump(actual);
+ dump("********************\n");
+ SimpleTest.finish();
+ } else {
+ setTimeout(copyPasteAndCheck, 100);
+ }
+ }
+ }
+ copyPasteAndCheck();
+ }
+
+ // Returns a function that chains together multiple test() calls.
+ function chain(aFrameIds) {
+ let x = aFrameIds.shift();
+ if (x) {
+ return function() { test(x.filename, x.expected, chain(aFrameIds)); }
+ } else {
+ return function() { SimpleTest.finish(); };
+ }
+ }
+
+ // This is pretty simple output, but that's ok; this file is about testing
+ // the loading of data from file. If we got this far, we're doing fine.
+ let expectedGood =
+"\
+Explicit-only process\n\
+\n\
+WARNING: the 'heap-allocated' memory reporter does not work for this platform and/or configuration. This means that 'heap-unclassified' is not shown and the 'explicit' tree shows less memory than it should.\n\
+Explicit Allocations\n\
+\n\
+0.10 MB (100.0%) -- explicit\n\
+└──0.10 MB (100.0%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+End of Explicit-only process\n\
+Heap-unclassified process\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──200.00 MB (80.00%) ── heap-unclassified\n\
+└───50.00 MB (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Heap-unclassified process\n\
+Main Process (pid NNN)\n\
+Explicit Allocations\n\
+\n\
+250.00 MB (100.0%) -- explicit\n\
+├──200.00 MB (80.00%) ── heap-unclassified\n\
+└───50.00 MB (20.00%) ── a/b\n\
+\n\
+Other Measurements\n\
+\n\
+0.00 MB (100.0%) -- compartments\n\
+└──0.00 MB (100.0%) ── system/a\n\
+\n\
+0.00 MB (100.0%) -- ghost-windows\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.30 MB (100.0%) -- other\n\
+├──0.20 MB (66.67%) ── a\n\
+└──0.10 MB (33.33%) ── b\n\
+\n\
+0.00 MB (100.0%) -- pss\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.00 MB (100.0%) -- rss\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.00 MB (100.0%) -- size\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+0.00 MB (100.0%) -- swap\n\
+└──0.00 MB (100.0%) ── a\n\
+\n\
+250.00 MB ── heap-allocated\n\
+\n\
+End of Main Process (pid NNN)\n\
+Other-only process\n\
+Other Measurements\n\
+\n\
+0.19 MB (100.0%) -- a\n\
+├──0.10 MB (50.00%) ── b\n\
+└──0.10 MB (50.00%) ── c\n\
+\n\
+0.48 MB ── heap-allocated\n\
+\n\
+End of Other-only process\n\
+";
+
+ // This is the output for a malformed data file.
+ let expectedBad =
+"\
+Error: Invalid memory report(s): missing 'hasMozMallocUsableSize' property";
+
+ let frames = [
+ // This loads a pre-existing file that is valid.
+ { filename: "memory-reports-good.json", expected: expectedGood },
+
+ // This loads a pre-existing file that is valid.
+ { filename: "memory-reports-bad.json", expected: expectedBad }
+ ];
+
+ SimpleTest.waitForFocus(chain(frames));
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory5.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory5.xul
new file mode 100644
index 0000000000..2fec803b9c
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory5.xul
@@ -0,0 +1,167 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the saving and loading of memory reports to/from file in
+ about:memory in the presence of child processes. It is also notable
+ for being an about:memory test that uses the real reporters, rather
+ than fake deterministic ones, and so tends to show up problems in the
+ real reporters (like bogus negative values). -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <iframe id="amFrame" height="400" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ let numRemotes = 3;
+ let numReady = 0;
+
+ // Create some remote processes, and set up message-passing so that
+ // we know when each child is fully initialized.
+ let remotes = [];
+
+ let prefs = [
+ ["dom.ipc.processCount", 3], // Allow up to 3 child processes
+ ["memory.report_concurrency", 2], // Cover more child handling cases
+ ["memory.system_memory_reporter", true] // Test SystemMemoryReporter
+ ];
+
+ SpecialPowers.pushPrefEnv({"set": prefs}, function() {
+ for (let i = 0; i < numRemotes; i++) {
+ let w = remotes[i] = window.open("remote.xul", "", "chrome");
+
+ w.addEventListener("load", function loadHandler() {
+ w.removeEventListener("load", loadHandler);
+ let remoteBrowser = w.document.getElementById("remote");
+ let mm = remoteBrowser.messageManager;
+ mm.addMessageListener("test:ready", function readyHandler() {
+ mm.removeMessageListener("test:ready", readyHandler);
+ numReady++;
+ if (numReady == numRemotes) {
+ // All the remote processes are ready.
+ SimpleTest.waitForFocus(onFocus);
+ }
+ });
+ mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true);
+ });
+ }
+ });
+
+ // Load the given file into the frame, then copy+paste the entire frame and
+ // check that the cut text matches what we expect.
+ function onFocus() {
+ let frame = document.getElementById("amFrame");
+ frame.focus();
+
+ function getFilePath(aFilename) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ file.append("chrome");
+ file.append("toolkit");
+ file.append("components");
+ file.append("aboutmemory");
+ file.append("tests");
+ file.append(aFilename);
+ return file.path;
+ }
+
+ let filePath = getFilePath("memory-reports-dumped.json.gz");
+
+ let e = document.createEvent('Event');
+ e.initEvent('change', true, true);
+
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+ dumper.dumpMemoryReportsToNamedFile(filePath, loadAndCheck, null,
+ /* anonymize = */ false);
+
+ function loadAndCheck() {
+ // Load the file.
+ let fileInput1 =
+ frame.contentWindow.document.getElementById("fileInput1");
+ fileInput1.value = filePath; // this works because it's a chrome test
+ fileInput1.dispatchEvent(e);
+
+ // Initialize the clipboard contents.
+ SpecialPowers.clipboardCopyString("initial clipboard value");
+
+ let numFailures = 0, maxFailures = 30;
+
+ copyPasteAndCheck();
+
+ // Because the file load is async, we don't know when it will finish and
+ // the output will show up. So we poll.
+ function copyPasteAndCheck() {
+ // Copy and paste frame contents, and filter out non-deterministic
+ // differences.
+ synthesizeKey("A", {accelKey: true});
+ synthesizeKey("C", {accelKey: true});
+ let actual = SpecialPowers.getClipboardData("text/unicode");
+
+ // If we have more than 1000 chars, we've probably successfully
+ // copy+pasted.
+ if (actual.length > 1000) {
+
+ let good = true;
+
+ if (actual.match("End of System")) {
+ let m1 = actual.match("anonymous") &&
+ actual.match("shared-libraries");
+ ok(m1, "system-wide reporter")
+ good = good && !!m1;
+ }
+
+ // Note: Match "vsize" but not "vsize-max-contiguous".
+ let vsizes = actual.match(/vsize[^-]/g);
+ let endOfBrowsers = actual.match(/End of Browser/g);
+ if (endOfBrowsers == null) {
+ endOfBrowsers = actual.match(/End of Web Content/g);
+ }
+ let m2 = (vsizes.length == 4 && endOfBrowsers.length == 3);
+ ok(m2, "three child processes present in loaded data");
+ good = good && !!m2;
+
+ if (!good) {
+ dump("*******ACTUAL*******\n");
+ dump(actual);
+ dump("********************\n");
+ }
+
+ // Close the remote processes.
+ for (let i = 0; i < numRemotes; i++) {
+ remotes[i].close();
+ }
+
+ SimpleTest.finish();
+
+ } else {
+ numFailures++;
+ if (numFailures === maxFailures) {
+ ok(false, "not enough chars in pasted output");
+ SimpleTest.finish();
+ } else {
+ setTimeout(copyPasteAndCheck, 100);
+ }
+ }
+ }
+ }
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_aboutmemory6.xul b/toolkit/components/aboutmemory/tests/test_aboutmemory6.xul
new file mode 100644
index 0000000000..365f990918
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_aboutmemory6.xul
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- This file tests the saving of GC and CC logs in both concise and
+ verbose formats. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <iframe id="amFrame" height="400" src="about:memory"></iframe>
+
+ <script type="application/javascript">
+ <![CDATA[
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ function onFocus() {
+ let frame = document.getElementById("amFrame");
+ frame.focus();
+
+ // Checks that a file exists on the local file system and removes it if it
+ // is present.
+ function checkForFileAndRemove(aFilename) {
+ let localFile = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsILocalFile);
+ localFile.initWithPath(aFilename);
+
+ let exists = localFile.exists();
+ if (exists) {
+ localFile.remove(/* recursive = */ false);
+ }
+
+ return exists;
+ }
+
+ // Given a save log button, triggers the action and checks if both CC & GC
+ // logs were written to disk.
+ function saveLogs(aLogButton, aCCLogType)
+ {
+ // trigger the log saving
+ aLogButton.click();
+
+ // mainDiv
+ // |-> section
+ // | -> div gc log path
+ // | -> div cc log path
+ let mainDiv = frame.contentWindow.document.getElementById("mainDiv");
+ let logNodes = mainDiv.childNodes[0];
+
+ // we expect 2 logs listed
+ let numOfLogs = logNodes.childNodes.length;
+ ok(numOfLogs == 2, "two log entries generated")
+
+ // grab the path portion of the text
+ let gcLogPath = logNodes.childNodes[0].textContent
+ .replace("Saved GC log to ", "");
+ let ccLogPath = logNodes.childNodes[1].textContent
+ .replace("Saved " + aCCLogType + " CC log to ", "");
+
+ // check that the files actually exist
+ ok(checkForFileAndRemove(gcLogPath), "GC log file exists");
+ ok(checkForFileAndRemove(ccLogPath), "CC log file exists");
+ }
+
+ // get the log buttons to test
+ let saveConcise = frame.contentWindow.document
+ .getElementById("saveLogsConcise");
+ let saveVerbose = frame.contentWindow.document
+ .getElementById("saveLogsVerbose");
+
+ saveLogs(saveConcise, "concise");
+ saveLogs(saveVerbose, "verbose");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(onFocus);
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul b/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul
new file mode 100644
index 0000000000..a39869b7d6
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_dumpGCAndCCLogsToFile.xul
@@ -0,0 +1,98 @@
+<?xml version="1.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="GC/CC logging with child processes"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ SimpleTest.waitForExplicitFinish();
+
+ let numRemotes = 3;
+ let numReady = 0;
+
+ // Create some remote processes, and set up message-passing so that
+ // we know when each child is fully initialized.
+ let remotes = [];
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", numRemotes]]},
+ function() {
+ for (let i = 0; i < numRemotes; i++) {
+ let w = remotes[i] = window.open("remote.xul", "", "chrome");
+
+ w.addEventListener("load", function loadHandler() {
+ w.removeEventListener("load", loadHandler);
+ let remoteBrowser = w.document.getElementById("remote");
+ let mm = remoteBrowser.messageManager;
+ mm.addMessageListener("test:ready", function readyHandler() {
+ mm.removeMessageListener("test:ready", readyHandler);
+ numReady++;
+ if (numReady == numRemotes) {
+ // All the remote processes are ready. Run test.
+ runTest();
+ }
+ });
+ mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true);
+ });
+ }
+ });
+
+ let dumper = Cc["@mozilla.org/memory-info-dumper;1"].
+ getService(Ci.nsIMemoryInfoDumper);
+
+ function runTest()
+ {
+ let numParents = 0;
+ let numChildren = 0;
+ dumper.dumpGCAndCCLogsToFile(
+ /* identifier: */ "test." + Date.now(),
+ /* allTraces: */ false,
+ /* childProcesses: */ true,
+ {
+ onDump: function(gcLog, ccLog, isParent) {
+ if (isParent) {
+ numParents++;
+ } else {
+ numChildren++;
+ }
+ checkAndRemoveLog(gcLog);
+ checkAndRemoveLog(ccLog);
+ },
+ onFinish: function() {
+ is(numParents, 1,
+ "GC/CC logs for the parent process");
+ is(numChildren, numRemotes,
+ "GC/CC logs for each child process");
+ cleanUpAndFinish();
+ }
+ });
+ }
+
+ function cleanUpAndFinish() {
+ // Close the remote processes.
+ for (let i = 0; i < numRemotes; i++) {
+ remotes[i].close();
+ }
+ SimpleTest.finish();
+ }
+
+ function checkAndRemoveLog(logFile) {
+ let name = logFile.path;
+ ok(logFile.exists(), "log file "+name+" exists");
+ ok(logFile.isFile(), "log file "+name+" is a regular file");
+ ok(logFile.fileSize > 0, "log file "+name+" is not empty");
+ logFile.remove(/* recursive: */ false);
+ }
+
+ ]]></script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters.xul b/toolkit/components/aboutmemory/tests/test_memoryReporters.xul
new file mode 100644
index 0000000000..9d56890b39
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_memoryReporters.xul
@@ -0,0 +1,424 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="Memory reporters"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- This file tests (in a rough fashion) whether the memory reporters are
+ producing sensible results. test_aboutmemory.xul tests the
+ presentation of memory reports in about:memory. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <!-- In bug 773533, <marquee> elements crashed the JS memory reporter -->
+ <marquee>Marquee</marquee>
+ </body>
+
+ <!-- some URIs that should be anonymized in anonymous mode -->
+ <iframe id="amFrame" height="200" src="http://example.org:80"></iframe>
+ <iframe id="amFrame" height="200" src="https://example.com:443"></iframe>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ "use strict";
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cr = Components.results;
+
+ const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
+ const HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
+ const OTHER = Ci.nsIMemoryReporter.KIND_OTHER;
+
+ const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
+ const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
+ const COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
+ const PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
+
+ // Use backslashes instead of forward slashes due to memory reporting's hacky
+ // handling of URLs.
+ const XUL_NS =
+ "http:\\\\www.mozilla.org\\keymaster\\gatekeeper\\there.is.only.xul";
+
+ SimpleTest.waitForExplicitFinish();
+
+ let vsizeAmounts = [];
+ let residentAmounts = [];
+ let heapAllocatedAmounts = [];
+ let storageSqliteAmounts = [];
+
+ let jsGcHeapUsedGcThingsTotal = 0;
+ let jsGcHeapUsedGcThings = {};
+
+ let present = {}
+
+ // Generate a long, random string. We'll check that this string is
+ // reported in at least one of the memory reporters.
+ let bigString = "";
+ while (bigString.length < 10000) {
+ bigString += Math.random();
+ }
+ let bigStringPrefix = bigString.substring(0, 100);
+
+ // Generate many copies of two distinctive short strings, "!)(*&" and
+ // "@)(*&". We'll check that these strings are reported in at least
+ // one of the memory reporters.
+ let shortStrings = [];
+ for (let i = 0; i < 10000; i++) {
+ let str = (Math.random() > 0.5 ? "!" : "@") + ")(*&";
+ shortStrings.push(str);
+ }
+
+ let mySandbox = Components.utils.Sandbox(document.nodePrincipal,
+ { sandboxName: "this-is-a-sandbox-name" });
+
+ function handleReportNormal(aProcess, aPath, aKind, aUnits, aAmount,
+ aDescription)
+ {
+ // Record the values of some notable reporters.
+ if (aPath === "vsize") {
+ vsizeAmounts.push(aAmount);
+ } else if (aPath === "resident") {
+ residentAmounts.push(aAmount);
+ } else if (aPath.search(/^js-main-runtime-gc-heap-committed\/used\/gc-things\//) >= 0) {
+ jsGcHeapUsedGcThingsTotal += aAmount;
+ jsGcHeapUsedGcThings[aPath] = (jsGcHeapUsedGcThings[aPath] | 0) + 1;
+ } else if (aPath === "heap-allocated") {
+ heapAllocatedAmounts.push(aAmount);
+ } else if (aPath === "storage-sqlite") {
+ storageSqliteAmounts.push(aAmount);
+
+ // Check the presence of some other notable reporters.
+ } else if (aPath.search(/^explicit\/js-non-window\/.*compartment\(/) >= 0) {
+ present.jsNonWindowCompartments = true;
+ } else if (aPath.search(/^explicit\/window-objects\/top\(.*\/js-compartment\(/) >= 0) {
+ present.windowObjectsJsCompartments = true;
+ } else if (aPath.search(/^explicit\/storage\/sqlite\/places.sqlite/) >= 0) {
+ present.places = true;
+ } else if (aPath.search(/^explicit\/images/) >= 0) {
+ present.images = true;
+ } else if (aPath.search(/^explicit\/xpti-working-set$/) >= 0) {
+ present.xptiWorkingSet = true;
+ } else if (aPath.search(/^explicit\/atom-tables\/main$/) >= 0) {
+ present.atomTablesMain = true;
+ } else if (/\[System Principal\].*this-is-a-sandbox-name/.test(aPath)) {
+ // A system compartment with a location (such as a sandbox) should
+ // show that location.
+ present.sandboxLocation = true;
+ } else if (aPath.includes(bigStringPrefix)) {
+ present.bigString = true;
+ } else if (aPath.includes("!)(*&")) {
+ present.smallString1 = true;
+ } else if (aPath.includes("@)(*&")) {
+ present.smallString2 = true;
+ }
+
+ // Shouldn't get any anonymized paths.
+ if (aPath.includes('<anonymized')) {
+ present.anonymizedWhenUnnecessary = aPath;
+ }
+ }
+
+ function handleReportAnonymized(aProcess, aPath, aKind, aUnits, aAmount,
+ aDescription)
+ {
+ // Path might include an xmlns using http, which is safe to ignore.
+ let reducedPath = aPath.replace(XUL_NS, "");
+
+ // Shouldn't get http: or https: in any paths.
+ if (reducedPath.includes('http:')) {
+ present.httpWhenAnonymized = aPath;
+ }
+
+ // file: URLs should have their path anonymized.
+ if (reducedPath.search('file:..[^<]') !== -1) {
+ present.unanonymizedFilePathWhenAnonymized = aPath;
+ }
+ }
+
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ let amounts = [
+ "vsize",
+ "vsizeMaxContiguous",
+ "resident",
+ "residentFast",
+ "residentPeak",
+ "residentUnique",
+ "heapAllocated",
+ "heapOverheadFraction",
+ "JSMainRuntimeGCHeap",
+ "JSMainRuntimeTemporaryPeak",
+ "JSMainRuntimeCompartmentsSystem",
+ "JSMainRuntimeCompartmentsUser",
+ "imagesContentUsedUncompressed",
+ "storageSQLite",
+ "lowMemoryEventsVirtual",
+ "lowMemoryEventsPhysical",
+ "ghostWindows",
+ "pageFaultsHard",
+ ];
+ for (let i = 0; i < amounts.length; i++) {
+ try {
+ // If mgr[amounts[i]] throws an exception, just move on -- some amounts
+ // aren't available on all platforms. But if the attribute simply
+ // isn't present, that indicates the distinguished amounts have changed
+ // and this file hasn't been updated appropriately.
+ let dummy = mgr[amounts[i]];
+ ok(dummy !== undefined,
+ "accessed an unknown distinguished amount: " + amounts[i]);
+ } catch (ex) {
+ }
+ }
+
+ // Run sizeOfTab() to make sure it doesn't crash. We can't check the result
+ // values because they're non-deterministic.
+ let jsObjectsSize = {};
+ let jsStringsSize = {};
+ let jsOtherSize = {};
+ let domSize = {};
+ let styleSize = {};
+ let otherSize = {};
+ let totalSize = {};
+ let jsMilliseconds = {};
+ let nonJSMilliseconds = {};
+ mgr.sizeOfTab(window, jsObjectsSize, jsStringsSize, jsOtherSize,
+ domSize, styleSize, otherSize, totalSize,
+ jsMilliseconds, nonJSMilliseconds);
+
+ let asyncSteps = [
+ getReportsNormal,
+ getReportsAnonymized,
+ checkResults,
+ test_register_strong,
+ test_register_strong, // Make sure re-registering works
+ test_register_weak,
+ SimpleTest.finish
+ ];
+
+ function runNext() {
+ setTimeout(asyncSteps.shift(), 0);
+ }
+
+ function getReportsNormal()
+ {
+ mgr.getReports(handleReportNormal, null,
+ runNext, null,
+ /* anonymize = */ false);
+ }
+
+ function getReportsAnonymized()
+ {
+ mgr.getReports(handleReportAnonymized, null,
+ runNext, null,
+ /* anonymize = */ true);
+ }
+
+ function checkSizeReasonable(aName, aAmount)
+ {
+ // Check the size is reasonable -- i.e. not ridiculously large or small.
+ ok(100 * 1000 <= aAmount && aAmount <= 10 * 1000 * 1000 * 1000,
+ aName + "'s size is reasonable");
+ }
+
+ function checkSpecialReport(aName, aAmounts, aCanBeUnreasonable)
+ {
+ ok(aAmounts.length == 1, aName + " has " + aAmounts.length + " report");
+ let n = aAmounts[0];
+ if (!aCanBeUnreasonable) {
+ checkSizeReasonable(aName, n);
+ }
+ }
+
+ function checkResults()
+ {
+ try {
+ // Nb: mgr.heapAllocated will throw NS_ERROR_NOT_AVAILABLE if this is a
+ // --disable-jemalloc build. Allow for skipping this test on that
+ // exception, but *only* that exception.
+ let dummy = mgr.heapAllocated;
+ checkSpecialReport("heap-allocated", heapAllocatedAmounts);
+ } catch (ex) {
+ is(ex.result, Cr.NS_ERROR_NOT_AVAILABLE, "mgr.heapAllocated exception");
+ }
+ // vsize may be unreasonable if ASAN is enabled
+ checkSpecialReport("vsize", vsizeAmounts, /*canBeUnreasonable*/true);
+ checkSpecialReport("resident", residentAmounts);
+
+ for (var reporter in jsGcHeapUsedGcThings) {
+ ok(jsGcHeapUsedGcThings[reporter] == 1);
+ }
+ checkSizeReasonable("js-main-runtime-gc-heap-committed/used/gc-things",
+ jsGcHeapUsedGcThingsTotal);
+
+ ok(present.jsNonWindowCompartments, "js-non-window compartments are present");
+ ok(present.windowObjectsJsCompartments, "window-objects/.../js compartments are present");
+ ok(present.places, "places is present");
+ ok(present.images, "images is present");
+ ok(present.xptiWorkingSet, "xpti-working-set is present");
+ ok(present.atomTablesMain, "atom-tables/main is present");
+ ok(present.sandboxLocation, "sandbox locations are present");
+ ok(present.bigString, "large string is present");
+ ok(present.smallString1, "small string 1 is present");
+ ok(present.smallString2, "small string 2 is present");
+
+ ok(!present.anonymizedWhenUnnecessary,
+ "anonymized paths are not present when unnecessary. Failed case: " +
+ present.anonymizedWhenUnnecessary);
+ ok(!present.httpWhenAnonymized,
+ "http URLs are anonymized when necessary. Failed case: " +
+ present.httpWhenAnonymized);
+ ok(!present.unanonymizedFilePathWhenAnonymized,
+ "file URLs are anonymized when necessary. Failed case: " +
+ present.unanonymizedFilePathWhenAnonymized);
+
+ runNext();
+ }
+
+ // Reporter registration tests
+
+ // collectReports() calls to the test reporter.
+ let called = 0;
+
+ // The test memory reporter, testing the various report units.
+ // Also acts as a report collector, verifying the reported values match the
+ // expected ones after passing through XPConnect / nsMemoryReporterManager
+ // and back.
+ function MemoryReporterAndCallback() {
+ this.seen = 0;
+ }
+ MemoryReporterAndCallback.prototype = {
+ // The test reports.
+ // Each test key corresponds to the path of the report. |amount| is a
+ // function called when generating the report. |expected| is a function
+ // to be tested when receiving a report during collection. If |expected| is
+ // omitted the |amount| will be checked instead.
+ tests: {
+ "test-memory-reporter-bytes1": {
+ units: BYTES,
+ amount: () => 0
+ },
+ "test-memory-reporter-bytes2": {
+ units: BYTES,
+ amount: () => (1<<30) * 8 // awkward way to say 8G in JS
+ },
+ "test-memory-reporter-counter": {
+ units: COUNT,
+ amount: () => 2
+ },
+ "test-memory-reporter-ccounter": {
+ units: COUNT_CUMULATIVE,
+ amount: () => ++called,
+ expected: () => called
+ },
+ "test-memory-reporter-percentage": {
+ units: PERCENTAGE,
+ amount: () => 9999
+ }
+ },
+ // nsIMemoryReporter
+ collectReports: function(callback, data, anonymize) {
+ for (let path of Object.keys(this.tests)) {
+ try {
+ let test = this.tests[path];
+ callback.callback(
+ "", // Process. Should be "" initially.
+ path,
+ OTHER,
+ test.units,
+ test.amount(),
+ "Test " + path + ".",
+ data);
+ }
+ catch (ex) {
+ ok(false, ex);
+ }
+ }
+ },
+ // nsIMemoryReporterCallback
+ callback: function(process, path, kind, units, amount, data) {
+ if (path in this.tests) {
+ this.seen++;
+ let test = this.tests[path];
+ ok(units === test.units, "Test reporter units match");
+ ok(amount === (test.expected || test.amount)(),
+ "Test reporter values match: " + amount);
+ }
+ },
+ // Checks that the callback has seen the expected number of reports, and
+ // resets the callback counter.
+ // @param expected Optional. Expected number of reports the callback
+ // should have processed.
+ finish: function(expected) {
+ if (expected === undefined) {
+ expected = Object.keys(this.tests).length;
+ }
+ is(expected, this.seen,
+ "Test reporter called the correct number of times: " + expected);
+ this.seen = 0;
+ }
+ };
+
+ // General memory reporter + registerStrongReporter tests.
+ function test_register_strong() {
+ let reporterAndCallback = new MemoryReporterAndCallback();
+ // Registration works.
+ mgr.registerStrongReporter(reporterAndCallback);
+
+ // Check the generated reports.
+ mgr.getReports(reporterAndCallback, null,
+ () => {
+ reporterAndCallback.finish();
+ window.setTimeout(test_unregister_strong, 0, reporterAndCallback);
+ }, null,
+ /* anonymize = */ false);
+ }
+
+ function test_unregister_strong(aReporterAndCallback)
+ {
+ mgr.unregisterStrongReporter(aReporterAndCallback);
+
+ // The reporter was unregistered, hence there shouldn't be any reports from
+ // the test reporter.
+ mgr.getReports(aReporterAndCallback, null,
+ () => {
+ aReporterAndCallback.finish(0);
+ runNext();
+ }, null,
+ /* anonymize = */ false);
+ }
+
+ // Check that you cannot register JS components as weak reporters.
+ function test_register_weak() {
+ let reporterAndCallback = new MemoryReporterAndCallback();
+ try {
+ // Should fail! nsMemoryReporterManager will only hold a raw pointer to
+ // "weak" reporters. When registering a weak reporter, XPConnect will
+ // create a WrappedJS for JS components. This WrappedJS would be
+ // successfully registered with the manager, only to be destroyed
+ // immediately after, which would eventually lead to a crash when
+ // collecting the reports. Therefore nsMemoryReporterManager should
+ // reject WrappedJS reporters, which is what is tested here.
+ // See bug 950391 comment #0.
+ mgr.registerWeakReporter(reporterAndCallback);
+ ok(false, "Shouldn't be allowed to register a JS component (WrappedJS)");
+ }
+ catch (ex) {
+ ok(ex.message.indexOf("NS_ERROR_") >= 0,
+ "WrappedJS reporter got rejected: " + ex);
+ }
+
+ runNext();
+ }
+
+ // Kick-off the async tests.
+ runNext();
+
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_memoryReporters2.xul b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xul
new file mode 100644
index 0000000000..0e8ba2e81f
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_memoryReporters2.xul
@@ -0,0 +1,108 @@
+<?xml version="1.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="Memory reporters with child processes"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- This file tests (in a rough fashion) whether the memory reporters are
+ producing sensible results in the presence of child processes. -->
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ SimpleTest.waitForExplicitFinish();
+
+ let numRemotes = 3;
+ let numReady = 0;
+
+ // Create some remote processes, and set up message-passing so that
+ // we know when each child is fully initialized.
+ let remotes = [];
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 3]]}, function() {
+ for (let i = 0; i < numRemotes; i++) {
+ let w = remotes[i] = window.open("remote.xul", "", "chrome");
+
+ w.addEventListener("load", function loadHandler() {
+ w.removeEventListener("load", loadHandler);
+ let remoteBrowser = w.document.getElementById("remote");
+ let mm = remoteBrowser.messageManager;
+ mm.addMessageListener("test:ready", function readyHandler() {
+ mm.removeMessageListener("test:ready", readyHandler);
+ numReady++;
+ if (numReady == numRemotes) {
+ // All the remote processes are ready. Do memory reporting.
+ doReports();
+ }
+ });
+ mm.loadFrameScript("data:," + encodeURI("sendAsyncMessage('test:ready');"), true);
+ });
+ }
+ });
+
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+
+ function doReports()
+ {
+ let residents = {};
+
+ let handleReport = function(aProcess, aPath, aKind, aUnits, aAmount, aDesc) {
+ if (aPath === "resident") {
+ ok(100 * 1000 <= aAmount && aAmount <= 10 * 1000 * 1000 * 1000,
+ "resident is reasonable");
+ residents[aProcess] = aAmount;
+ }
+ }
+
+ let processReports = function() {
+ // First, test a failure case: calling getReports() before the previous
+ // getReports() has finished should silently abort. (And the arguments
+ // won't be used.)
+ mgr.getReports(
+ () => ok(false, "handleReport called for nested getReports() call"),
+ null, null, null, /* anonymize = */ false
+ );
+
+ // Close the remote processes.
+ for (let i = 0; i < numRemotes; i++) {
+ remotes[i].close();
+ }
+
+ // Check the results.
+
+ let processes = Object.keys(residents);
+ ok(processes.length == numRemotes + 1, "correct resident count");
+
+ let numEmptyProcesses = 0, numNonEmptyProcesses = 0;
+ for (let i = 0; i < processes.length; i++) {
+ if (processes[i] == "") {
+ numEmptyProcesses++;
+ } else {
+ ok(processes[i].startsWith("Browser (") || processes[i].startsWith("Web Content ("),
+ "correct non-empty process name prefix: " + processes[i]);
+ numNonEmptyProcesses++;
+ }
+ }
+ ok(numEmptyProcesses == 1, "correct empty process name count");
+ ok(numNonEmptyProcesses == numRemotes,
+ "correct non-empty process name count");
+
+ SimpleTest.finish();
+ }
+
+ mgr.getReports(handleReport, null, processReports, null,
+ /* anonymize = */ false);
+ }
+
+ ]]></script>
+</window>
diff --git a/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul
new file mode 100644
index 0000000000..3452bbbc72
--- /dev/null
+++ b/toolkit/components/aboutmemory/tests/test_sqliteMultiReporter.xul
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="about:memory"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml"></body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ // Test for bug 708248, where the SQLite memory multi-reporter was
+ // crashing when a DB was closed.
+
+ // Nb: this test is all JS and chould be done with an xpcshell test,
+ // but all the other memory reporter tests are mochitests, so it's easier
+ // if this one is too.
+
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+ const Cu = Components.utils;
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Make a fake DB file.
+ let file = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get("ProfD", Ci.nsIFile);
+ file.append("test_sqliteMultiReporter-fake-DB-tmp.sqlite");
+
+ // Open and close the DB.
+ let storage = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let db = storage.openDatabase(file);
+ db.close();
+
+ // Invoke all the reporters. The SQLite multi-reporter is among
+ // them. It shouldn't crash.
+ let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+ mgr.getReports(function(){}, null,
+ () => {
+ ok(true, "didn't crash");
+ SimpleTest.finish();
+ }, null,
+ /* anonymize = */ false);
+
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.js b/toolkit/components/aboutperformance/content/aboutPerformance.js
new file mode 100644
index 0000000000..3b191d8950
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.js
@@ -0,0 +1,1077 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { AddonWatcher } = Cu.import("resource://gre/modules/AddonWatcher.jsm", {});
+const { PerformanceStats } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {});
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+const { ObjectUtils } = Cu.import("resource://gre/modules/ObjectUtils.jsm", {});
+const { Memory } = Cu.import("resource://gre/modules/Memory.jsm");
+const { DownloadUtils } = Cu.import("resource://gre/modules/DownloadUtils.jsm");
+
+// about:performance observes notifications on this topic.
+// if a notification is sent, this causes the page to be updated immediately,
+// regardless of whether the page is on pause.
+const TEST_DRIVER_TOPIC = "test-about:performance-test-driver";
+
+// about:performance posts notifications on this topic whenever the page
+// is updated.
+const UPDATE_COMPLETE_TOPIC = "about:performance-update-complete";
+
+// How often we should add a sample to our buffer.
+const BUFFER_SAMPLING_RATE_MS = 1000;
+
+// The age of the oldest sample to keep.
+const BUFFER_DURATION_MS = 10000;
+
+// How often we should update
+const UPDATE_INTERVAL_MS = 5000;
+
+// The name of the application
+const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties");
+const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+
+// The maximal number of items to display before showing a "Show All"
+// button.
+const MAX_NUMBER_OF_ITEMS_TO_DISPLAY = 3;
+
+// If the frequency of alerts is below this value,
+// we consider that the feature has no impact.
+const MAX_FREQUENCY_FOR_NO_IMPACT = .05;
+// If the frequency of alerts is above `MAX_FREQUENCY_FOR_NO_IMPACT`
+// and below this value, we consider that the feature impacts the
+// user rarely.
+const MAX_FREQUENCY_FOR_RARE = .1;
+// If the frequency of alerts is above `MAX_FREQUENCY_FOR_FREQUENT`
+// and below this value, we consider that the feature impacts the
+// user frequently. Anything above is consider permanent.
+const MAX_FREQUENCY_FOR_FREQUENT = .5;
+
+// If the number of high-impact alerts among all alerts is above
+// this value, we consider that the feature has a major impact
+// on user experience.
+const MIN_PROPORTION_FOR_MAJOR_IMPACT = .05;
+// Otherwise and if the number of medium-impact alerts among all
+// alerts is above this value, we consider that the feature has
+// a noticeable impact on user experience.
+const MIN_PROPORTION_FOR_NOTICEABLE_IMPACT = .1;
+
+// The current mode. Either `MODE_GLOBAL` to display a summary of results
+// since we opened about:performance or `MODE_RECENT` to display the latest
+// BUFFER_DURATION_MS ms.
+const MODE_GLOBAL = "global";
+const MODE_RECENT = "recent";
+
+let tabFinder = {
+ update: function() {
+ this._map = new Map();
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ let tabbrowser = win.gBrowser;
+ for (let browser of tabbrowser.browsers) {
+ let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
+ if (id != null) {
+ this._map.set(id, browser);
+ }
+ }
+ }
+ },
+
+ /**
+ * Find the <xul:tab> for a window id.
+ *
+ * This is useful e.g. for reloading or closing tabs.
+ *
+ * @return null If the xul:tab could not be found, e.g. if the
+ * windowId is that of a chrome window.
+ * @return {{tabbrowser: <xul:tabbrowser>, tab: <xul.tab>}} The
+ * tabbrowser and tab if the latter could be found.
+ */
+ get: function(id) {
+ let browser = this._map.get(id);
+ if (!browser) {
+ return null;
+ }
+ let tabbrowser = browser.getTabBrowser();
+ return {tabbrowser, tab:tabbrowser.getTabForBrowser(browser)};
+ },
+
+ getAny: function(ids) {
+ for (let id of ids) {
+ let result = this.get(id);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+ }
+};
+
+/**
+ * Returns a Promise that's resolved after the next turn of the event loop.
+ *
+ * Just returning a resolved Promise would mean that any `then` callbacks
+ * would be called right after the end of the current turn, so `setTimeout`
+ * is used to delay Promise resolution until the next turn.
+ *
+ * In mochi tests, it's possible for this to be called after the
+ * about:performance window has been torn down, which causes `setTimeout` to
+ * throw an NS_ERROR_NOT_INITIALIZED exception. In that case, returning
+ * `undefined` is fine.
+ */
+function wait(ms = 0) {
+ try {
+ let resolve;
+ let p = new Promise(resolve_ => { resolve = resolve_ });
+ setTimeout(resolve, ms);
+ return p;
+ } catch (e) {
+ dump("WARNING: wait aborted because of an invalid Window state in aboutPerformance.js.\n");
+ return undefined;
+ }
+}
+
+/**
+ * The performance of a webpage or an add-on between two instants.
+ *
+ * Clients should call `promiseInit()` before using the methods of this object.
+ *
+ * @param {PerformanceDiff} The underlying performance data.
+ * @param {"addons"|"webpages"} The kind of delta represented by this object.
+ * @param {Map<groupId, timestamp>} ageMap A map containing the oldest known
+ * appearance of each groupId, used to determine how long we have been monitoring
+ * this item.
+ * @param {Map<Delta key, Array>} alertMap A map containing the alerts that each
+ * item has already triggered in the past.
+ */
+function Delta(diff, kind, snapshotDate, ageMap, alertMap) {
+ if (kind != "addons" && kind != "webpages") {
+ throw new TypeError(`Unknown kind: ${kind}`);
+ }
+
+ /**
+ * Either "addons" or "webpages".
+ */
+ this.kind = kind;
+
+ /**
+ * The underlying PerformanceDiff.
+ * @type {PerformanceDiff}
+ */
+ this.diff = diff;
+
+ /**
+ * A key unique to the item (webpage or add-on), shared by successive
+ * instances of `Delta`.
+ * @type{string}
+ */
+ this.key = kind + diff.key;
+
+ // Find the oldest occurrence of this item.
+ let creationDate = snapshotDate;
+ for (let groupId of diff.groupIds) {
+ let date = ageMap.get(groupId);
+ if (date && date <= creationDate) {
+ creationDate = date;
+ }
+ }
+
+ /**
+ * The timestamp at which the data was measured.
+ */
+ this.creationDate = creationDate;
+
+ /**
+ * Number of milliseconds since the start of the measure.
+ */
+ this.age = snapshotDate - creationDate;
+
+ /**
+ * A UX-friendly, human-readable name for this item.
+ */
+ this.readableName = null;
+
+ /**
+ * A complete name, possibly useful for power users or debugging.
+ */
+ this.fullName = null;
+
+
+ // `true` once initialization is complete.
+ this._initialized = false;
+ // `true` if this item should be displayed
+ this._show = false;
+
+ /**
+ * All the alerts that this item has caused since about:performance
+ * was opened.
+ */
+ this.alerts = (alertMap.get(this.key) || []).slice();
+ switch (this.slowness) {
+ case 0: break;
+ case 1: this.alerts[0] = (this.alerts[0] || 0) + 1; break;
+ case 2: this.alerts[1] = (this.alerts[1] || 0) + 1; break;
+ default: throw new Error();
+ }
+}
+Delta.prototype = {
+ /**
+ * `true` if this item should be displayed, `false` otherwise.
+ */
+ get show() {
+ this._ensureInitialized();
+ return this._show;
+ },
+
+ /**
+ * Estimate the slowness of this item.
+ *
+ * @return 0 if the item has good performance.
+ * @return 1 if the item has average performance.
+ * @return 2 if the item has poor performance.
+ */
+ get slowness() {
+ if (Delta.compare(this, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) {
+ return 0;
+ }
+ if (Delta.compare(this, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE) <= 0) {
+ return 1;
+ }
+ return 2;
+ },
+ _ensureInitialized() {
+ if (!this._initialized) {
+ throw new Error();
+ }
+ },
+
+ /**
+ * Initialize, asynchronously.
+ */
+ promiseInit: function() {
+ if (this.kind == "webpages") {
+ return this._initWebpage();
+ } else if (this.kind == "addons") {
+ return this._promiseInitAddon();
+ }
+ throw new TypeError();
+ },
+ _initWebpage: function() {
+ this._initialized = true;
+ let found = tabFinder.getAny(this.diff.windowIds);
+ if (!found || found.tab.linkedBrowser.contentTitle == null) {
+ // Either this is not a real page or the page isn't restored yet.
+ return;
+ }
+
+ this.readableName = found.tab.linkedBrowser.contentTitle;
+ this.fullName = this.diff.names.join(", ");
+ this._show = true;
+ },
+ _promiseInitAddon: Task.async(function*() {
+ let found = yield (new Promise(resolve =>
+ AddonManager.getAddonByID(this.diff.addonId, a => {
+ if (a) {
+ this.readableName = a.name;
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ })));
+
+ this._initialized = true;
+
+ // If the add-on manager doesn't know about an add-on, it's
+ // probably not a real add-on.
+ this._show = found;
+ this.fullName = this.diff.addonId;
+ }),
+ toString: function() {
+ return `[Delta] ${this.diff.key} => ${this.readableName}, ${this.fullName}`;
+ }
+};
+
+Delta.compare = function(a, b) {
+ return (
+ (a.diff.jank.longestDuration - b.diff.jank.longestDuration) ||
+ (a.diff.jank.totalUserTime - b.diff.jank.totalUserTime) ||
+ (a.diff.jank.totalSystemTime - b.diff.jank.totalSystemTime) ||
+ (a.diff.cpow.totalCPOWTime - b.diff.cpow.totalCPOWTime) ||
+ (a.diff.ticks.ticks - b.diff.ticks.ticks) ||
+ 0
+ );
+};
+
+Delta.revCompare = function(a, b) {
+ return -Delta.compare(a, b);
+};
+
+/**
+ * The highest value considered "good performance".
+ */
+Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE = {
+ diff: {
+ cpow: {
+ totalCPOWTime: 0,
+ },
+ jank: {
+ longestDuration: 3,
+ totalUserTime: Number.POSITIVE_INFINITY,
+ totalSystemTime: Number.POSITIVE_INFINITY
+ },
+ ticks: {
+ ticks: Number.POSITIVE_INFINITY,
+ }
+ }
+};
+
+/**
+ * The highest value considered "average performance".
+ */
+Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE = {
+ diff: {
+ cpow: {
+ totalCPOWTime: Number.POSITIVE_INFINITY,
+ },
+ jank: {
+ longestDuration: 7,
+ totalUserTime: Number.POSITIVE_INFINITY,
+ totalSystemTime: Number.POSITIVE_INFINITY
+ },
+ ticks: {
+ ticks: Number.POSITIVE_INFINITY,
+ }
+ }
+};
+
+/**
+ * Utilities for dealing with state
+ */
+var State = {
+ _monitor: PerformanceStats.getMonitor([
+ "jank", "cpow", "ticks",
+ ]),
+
+ /**
+ * Indexed by the number of minutes since the snapshot was taken.
+ *
+ * @type {Array<ApplicationSnapshot>}
+ */
+ _buffer: [],
+ /**
+ * The first snapshot since opening the page.
+ *
+ * @type ApplicationSnapshot
+ */
+ _oldest: null,
+
+ /**
+ * The latest snapshot.
+ *
+ * @type ApplicationSnapshot
+ */
+ _latest: null,
+
+ /**
+ * The performance alerts for each group.
+ *
+ * This map is cleaned up during each update to avoid leaking references
+ * to groups that have been gc-ed.
+ *
+ * @type{Map<Delta key, Array<number>} A map in which the keys are provided
+ * by property `key` of instances of `Delta` and the values are arrays
+ * [number of moderate-impact alerts, number of high-impact alerts]
+ */
+ _alerts: new Map(),
+
+ /**
+ * The date at which each group was first seen.
+ *
+ * This map is cleaned up during each update to avoid leaking references
+ * to groups that have been gc-ed.
+ *
+ * @type{Map<string, timestamp} A map in which keys are
+ * values for `delta.groupId` and values are approximate
+ * dates at which the group was first encountered, as provided
+ * by `Cu.now()``.
+ */
+ _firstSeen: new Map(),
+
+ /**
+ * Update the internal state.
+ *
+ * @return {Promise}
+ */
+ update: Task.async(function*() {
+ // If the buffer is empty, add one value for bootstraping purposes.
+ if (this._buffer.length == 0) {
+ if (this._oldest) {
+ throw new Error("Internal Error, we shouldn't have a `_oldest` value yet.");
+ }
+ this._latest = this._oldest = yield this._monitor.promiseSnapshot();
+ this._buffer.push(this._oldest);
+ yield wait(BUFFER_SAMPLING_RATE_MS * 1.1);
+ }
+
+
+ let now = Cu.now();
+
+ // If we haven't sampled in a while, add a sample to the buffer.
+ let latestInBuffer = this._buffer[this._buffer.length - 1];
+ let deltaT = now - latestInBuffer.date;
+ if (deltaT > BUFFER_SAMPLING_RATE_MS) {
+ this._latest = yield this._monitor.promiseSnapshot();
+ this._buffer.push(this._latest);
+ }
+
+ // If we have too many samples, remove the oldest sample.
+ let oldestInBuffer = this._buffer[0];
+ if (oldestInBuffer.date + BUFFER_DURATION_MS < this._latest.date) {
+ this._buffer.shift();
+ }
+ }),
+
+ /**
+ * @return {Promise}
+ */
+ promiseDeltaSinceStartOfTime: function() {
+ return this._promiseDeltaSince(this._oldest);
+ },
+
+ /**
+ * @return {Promise}
+ */
+ promiseDeltaSinceStartOfBuffer: function() {
+ return this._promiseDeltaSince(this._buffer[0]);
+ },
+
+ /**
+ * @return {Promise}
+ * @resolve {{
+ * addons: Array<Delta>,
+ * webpages: Array<Delta>,
+ * deltas: Set<Delta key>,
+ * duration: number of milliseconds
+ * }}
+ */
+ _promiseDeltaSince: Task.async(function*(oldest) {
+ let current = this._latest;
+ if (!oldest) {
+ throw new TypeError();
+ }
+ if (!current) {
+ throw new TypeError();
+ }
+
+ tabFinder.update();
+ // We rebuild the maps during each iteration to make sure that
+ // we do not maintain references to groups that has been removed
+ // (e.g. pages that have been closed).
+ let oldFirstSeen = this._firstSeen;
+ let cleanedUpFirstSeen = new Map();
+
+ let oldAlerts = this._alerts;
+ let cleanedUpAlerts = new Map();
+
+ let result = {
+ addons: [],
+ webpages: [],
+ deltas: new Set(),
+ duration: current.date - oldest.date
+ };
+
+ for (let kind of ["webpages", "addons"]) {
+ for (let [key, value] of current[kind]) {
+ let item = ObjectUtils.strict(new Delta(value.subtract(oldest[kind].get(key)), kind, current.date, oldFirstSeen, oldAlerts));
+ yield item.promiseInit();
+
+ if (!item.show) {
+ continue;
+ }
+ result[kind].push(item);
+ result.deltas.add(item.key);
+
+ for (let groupId of item.diff.groupIds) {
+ cleanedUpFirstSeen.set(groupId, item.creationDate);
+ }
+ cleanedUpAlerts.set(item.key, item.alerts);
+ }
+ }
+
+ this._firstSeen = cleanedUpFirstSeen;
+ this._alerts = cleanedUpAlerts;
+ return result;
+ }),
+};
+
+var View = {
+ /**
+ * A cache for all the per-item DOM elements that are reused across refreshes.
+ *
+ * Reusing the same elements means that elements that were hidden (respectively
+ * visible) in an iteration remain hidden (resp visible) in the next iteration.
+ */
+ DOMCache: {
+ _map: new Map(),
+ /**
+ * @param {string} deltaKey The key for the item that we are displaying.
+ * @return {null} If the `deltaKey` doesn't have a component cached yet.
+ * Otherwise, the value stored with `set`.
+ */
+ get: function(deltaKey) {
+ return this._map.get(deltaKey);
+ },
+ set: function(deltaKey, value) {
+ this._map.set(deltaKey, value);
+ },
+ /**
+ * Remove all the elements whose key does not appear in `set`.
+ *
+ * @param {Set} set a set of deltaKey.
+ */
+ trimTo: function(set) {
+ let remove = [];
+ for (let key of this._map.keys()) {
+ if (!set.has(key)) {
+ remove.push(key);
+ }
+ }
+ for (let key of remove) {
+ this._map.delete(key);
+ }
+ }
+ },
+ /**
+ * Display the items in a category.
+ *
+ * @param {Array<PerformanceDiff>} subset The items to display. They will
+ * be displayed in the order of `subset`.
+ * @param {string} id The id of the DOM element that will contain the items.
+ * @param {string} nature The nature of the subset. One of "addons", "webpages" or "system".
+ * @param {string} currentMode The current display mode. One of MODE_GLOBAL or MODE_RECENT.
+ */
+ updateCategory: function(subset, id, nature, currentMode) {
+ subset = subset.slice().sort(Delta.revCompare);
+
+ let watcherAlerts = null;
+ if (nature == "addons") {
+ watcherAlerts = AddonWatcher.alerts;
+ }
+
+ // Grab everything from the DOM before cleaning up
+ this._setupStructure(id);
+
+ // An array of `cachedElements` that need to be added
+ let toAdd = [];
+ for (let delta of subset) {
+ if (!(delta instanceof Delta)) {
+ throw new TypeError();
+ }
+ let cachedElements = this._grabOrCreateElements(delta, nature);
+ toAdd.push(cachedElements);
+ cachedElements.eltTitle.textContent = delta.readableName;
+ cachedElements.eltName.textContent = `Full name: ${delta.fullName}.`;
+ cachedElements.eltLoaded.textContent = `Measure start: ${Math.round(delta.age/1000)} seconds ago.`
+
+ let processes = delta.diff.processes.map(proc => `${proc.processId} (${proc.isChildProcess?"child":"parent"})`);
+ cachedElements.eltProcess.textContent = `Processes: ${processes.join(", ")}`;
+ let jankSuffix = "";
+ if (watcherAlerts) {
+ let deltaAlerts = watcherAlerts.get(delta.diff.addonId);
+ if (deltaAlerts) {
+ if (deltaAlerts.occurrences) {
+ jankSuffix = ` (${deltaAlerts.occurrences} alerts)`;
+ }
+ }
+ }
+
+ let eltImpact = cachedElements.eltImpact;
+ if (currentMode == MODE_RECENT) {
+ cachedElements.eltRoot.setAttribute("impact", delta.diff.jank.longestDuration + 1);
+ if (Delta.compare(delta, Delta.MAX_DELTA_FOR_GOOD_RECENT_PERFORMANCE) <= 0) {
+ eltImpact.textContent = ` currently performs well.`;
+ } else if (Delta.compare(delta, Delta.MAX_DELTA_FOR_AVERAGE_RECENT_PERFORMANCE)) {
+ eltImpact.textContent = ` may currently be slowing down ${BRAND_NAME}.`;
+ } else {
+ eltImpact.textContent = ` is currently considerably slowing down ${BRAND_NAME}.`;
+ }
+
+ cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.diff.jank.longestDuration + 1}/${delta.diff.jank.durations.length}${jankSuffix}.`;
+ cachedElements.eltCPU.textContent = `CPU usage: ${Math.ceil(delta.diff.jank.totalCPUTime/delta.diff.deltaT/10)}%.`;
+ cachedElements.eltSystem.textContent = `System usage: ${Math.ceil(delta.diff.jank.totalSystemTime/delta.diff.deltaT/10)}%.`;
+ cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.diff.cpow.totalCPOWTime/delta.diff.deltaT/10)}%.`;
+ } else {
+ if (delta.alerts.length == 0) {
+ eltImpact.textContent = " has performed well so far.";
+ cachedElements.eltFPS.textContent = `Impact on framerate: no impact.`;
+ cachedElements.eltRoot.setAttribute("impact", 0);
+ } else {
+ let impact = 0;
+ let sum = /* medium impact */ delta.alerts[0] + /* high impact */ delta.alerts[1];
+ let frequency = sum * 1000 / delta.diff.deltaT;
+
+ let describeFrequency;
+ if (frequency <= MAX_FREQUENCY_FOR_NO_IMPACT) {
+ describeFrequency = `has no impact on the performance of ${BRAND_NAME}.`
+ } else {
+ let describeImpact;
+ if (frequency <= MAX_FREQUENCY_FOR_RARE) {
+ describeFrequency = `rarely slows down ${BRAND_NAME}.`;
+ impact += 1;
+ } else if (frequency <= MAX_FREQUENCY_FOR_FREQUENT) {
+ describeFrequency = `has slown down ${BRAND_NAME} frequently.`;
+ impact += 2.5;
+ } else {
+ describeFrequency = `seems to have slown down ${BRAND_NAME} very often.`;
+ impact += 5;
+ }
+ // At this stage, `sum != 0`
+ if (delta.alerts[1] / sum > MIN_PROPORTION_FOR_MAJOR_IMPACT) {
+ describeImpact = "When this happens, the slowdown is generally important."
+ impact *= 2;
+ } else {
+ describeImpact = "When this happens, the slowdown is generally noticeable."
+ }
+
+ eltImpact.textContent = ` ${describeFrequency} ${describeImpact}`;
+ cachedElements.eltFPS.textContent = `Impact on framerate: ${delta.alerts[1] || 0} high-impacts, ${delta.alerts[0] || 0} medium-impact${jankSuffix}.`;
+ }
+ cachedElements.eltRoot.setAttribute("impact", Math.round(impact));
+ }
+
+ cachedElements.eltCPU.textContent = `CPU usage: ${Math.ceil(delta.diff.jank.totalCPUTime/delta.diff.deltaT/10)}% (total ${delta.diff.jank.totalUserTime}ms).`;
+ cachedElements.eltSystem.textContent = `System usage: ${Math.ceil(delta.diff.jank.totalSystemTime/delta.diff.deltaT/10)}% (total ${delta.diff.jank.totalSystemTime}ms).`;
+ cachedElements.eltCPOW.textContent = `Blocking process calls: ${Math.ceil(delta.diff.cpow.totalCPOWTime/delta.diff.deltaT/10)}% (total ${delta.diff.cpow.totalCPOWTime}ms).`;
+ }
+ }
+ this._insertElements(toAdd, id);
+ },
+
+ _insertElements: function(elements, id) {
+ let eltContainer = document.getElementById(id);
+ eltContainer.classList.remove("measuring");
+ eltContainer.eltVisibleContent.innerHTML = "";
+ eltContainer.eltHiddenContent.innerHTML = "";
+ eltContainer.appendChild(eltContainer.eltShowMore);
+
+ for (let i = 0; i < elements.length && i < MAX_NUMBER_OF_ITEMS_TO_DISPLAY; ++i) {
+ let cachedElements = elements[i];
+ eltContainer.eltVisibleContent.appendChild(cachedElements.eltRoot);
+ }
+ for (let i = MAX_NUMBER_OF_ITEMS_TO_DISPLAY; i < elements.length; ++i) {
+ let cachedElements = elements[i];
+ eltContainer.eltHiddenContent.appendChild(cachedElements.eltRoot);
+ }
+ if (elements.length <= MAX_NUMBER_OF_ITEMS_TO_DISPLAY) {
+ eltContainer.eltShowMore.classList.add("hidden");
+ } else {
+ eltContainer.eltShowMore.classList.remove("hidden");
+ }
+ if (elements.length == 0) {
+ eltContainer.textContent = "Nothing";
+ }
+ },
+ _setupStructure: function(id) {
+ let eltContainer = document.getElementById(id);
+ if (!eltContainer.eltVisibleContent) {
+ eltContainer.eltVisibleContent = document.createElement("ul");
+ eltContainer.eltVisibleContent.classList.add("visible_items");
+ eltContainer.appendChild(eltContainer.eltVisibleContent);
+ }
+ if (!eltContainer.eltHiddenContent) {
+ eltContainer.eltHiddenContent = document.createElement("ul");
+ eltContainer.eltHiddenContent.classList.add("hidden");
+ eltContainer.eltHiddenContent.classList.add("hidden_additional_items");
+ eltContainer.appendChild(eltContainer.eltHiddenContent);
+ }
+ if (!eltContainer.eltShowMore) {
+ eltContainer.eltShowMore = document.createElement("button");
+ eltContainer.eltShowMore.textContent = "Show all";
+ eltContainer.eltShowMore.classList.add("show_all_items");
+ eltContainer.appendChild(eltContainer.eltShowMore);
+ eltContainer.eltShowMore.addEventListener("click", function() {
+ if (eltContainer.eltHiddenContent.classList.contains("hidden")) {
+ eltContainer.eltHiddenContent.classList.remove("hidden");
+ eltContainer.eltShowMore.textContent = "Hide";
+ } else {
+ eltContainer.eltHiddenContent.classList.add("hidden");
+ eltContainer.eltShowMore.textContent = "Show all";
+ }
+ });
+ }
+ return eltContainer;
+ },
+
+ _grabOrCreateElements: function(delta, nature) {
+ let cachedElements = this.DOMCache.get(delta.key);
+ if (cachedElements) {
+ if (cachedElements.eltRoot.parentElement) {
+ cachedElements.eltRoot.parentElement.removeChild(cachedElements.eltRoot);
+ }
+ } else {
+ this.DOMCache.set(delta.key, cachedElements = {});
+
+ let eltDelta = document.createElement("li");
+ eltDelta.classList.add("delta");
+ cachedElements.eltRoot = eltDelta;
+
+ let eltSpan = document.createElement("span");
+ eltDelta.appendChild(eltSpan);
+
+ let eltSummary = document.createElement("span");
+ eltSummary.classList.add("summary");
+ eltSpan.appendChild(eltSummary);
+
+ let eltTitle = document.createElement("span");
+ eltTitle.classList.add("title");
+ eltSummary.appendChild(eltTitle);
+ cachedElements.eltTitle = eltTitle;
+
+ let eltImpact = document.createElement("span");
+ eltImpact.classList.add("impact");
+ eltSummary.appendChild(eltImpact);
+ cachedElements.eltImpact = eltImpact;
+
+ let eltShowMore = document.createElement("a");
+ eltShowMore.classList.add("more");
+ eltSpan.appendChild(eltShowMore);
+ eltShowMore.textContent = "more";
+ eltShowMore.href = "";
+ eltShowMore.addEventListener("click", () => {
+ if (eltDetails.classList.contains("hidden")) {
+ eltDetails.classList.remove("hidden");
+ eltShowMore.textContent = "less";
+ } else {
+ eltDetails.classList.add("hidden");
+ eltShowMore.textContent = "more";
+ }
+ });
+
+ // Add buttons
+ if (nature == "addons") {
+ eltSpan.appendChild(document.createElement("br"));
+ let eltDisable = document.createElement("button");
+ eltDisable.textContent = "Disable";
+ eltSpan.appendChild(eltDisable);
+
+ let eltUninstall = document.createElement("button");
+ eltUninstall.textContent = "Uninstall";
+ eltSpan.appendChild(eltUninstall);
+
+ let eltRestart = document.createElement("button");
+ eltRestart.textContent = `Restart ${BRAND_NAME} to apply your changes.`
+ eltRestart.classList.add("hidden");
+ eltSpan.appendChild(eltRestart);
+
+ eltRestart.addEventListener("click", () => {
+ Services.startup.quit(Services.startup.eForceQuit | Services.startup.eRestart);
+ });
+ AddonManager.getAddonByID(delta.diff.addonId, addon => {
+ eltDisable.addEventListener("click", () => {
+ addon.userDisabled = true;
+ if (addon.pendingOperations == addon.PENDING_NONE) {
+ // Restartless add-on is now disabled.
+ return;
+ }
+ eltDisable.classList.add("hidden");
+ eltUninstall.classList.add("hidden");
+ eltRestart.classList.remove("hidden");
+ });
+
+ eltUninstall.addEventListener("click", () => {
+ addon.uninstall();
+ if (addon.pendingOperations == addon.PENDING_NONE) {
+ // Restartless add-on is now disabled.
+ return;
+ }
+ eltDisable.classList.add("hidden");
+ eltUninstall.classList.add("hidden");
+ eltRestart.classList.remove("hidden");
+ });
+ });
+ } else if (nature == "webpages") {
+ eltSpan.appendChild(document.createElement("br"));
+
+ let eltCloseTab = document.createElement("button");
+ eltCloseTab.textContent = "Close tab";
+ eltSpan.appendChild(eltCloseTab);
+ let windowIds = delta.diff.windowIds;
+ eltCloseTab.addEventListener("click", () => {
+ let found = tabFinder.getAny(windowIds);
+ if (!found) {
+ // Cannot find the tab. Maybe it is closed already?
+ return;
+ }
+ let {tabbrowser, tab} = found;
+ tabbrowser.removeTab(tab);
+ });
+
+ let eltReloadTab = document.createElement("button");
+ eltReloadTab.textContent = "Reload tab";
+ eltSpan.appendChild(eltReloadTab);
+ eltReloadTab.addEventListener("click", () => {
+ let found = tabFinder.getAny(windowIds);
+ if (!found) {
+ // Cannot find the tab. Maybe it is closed already?
+ return;
+ }
+ let {tabbrowser, tab} = found;
+ tabbrowser.reloadTab(tab);
+ });
+ }
+
+ // Prepare details
+ let eltDetails = document.createElement("ul");
+ eltDetails.classList.add("details");
+ eltDetails.classList.add("hidden");
+ eltSpan.appendChild(eltDetails);
+
+ for (let [name, className] of [
+ ["eltName", "name"],
+ ["eltFPS", "fps"],
+ ["eltCPU", "cpu"],
+ ["eltSystem", "system"],
+ ["eltCPOW", "cpow"],
+ ["eltLoaded", "loaded"],
+ ["eltProcess", "process"],
+ ]) {
+ let elt = document.createElement("li");
+ elt.classList.add(className);
+ eltDetails.appendChild(elt);
+ cachedElements[name] = elt;
+ }
+ }
+
+ return cachedElements;
+ },
+};
+
+var Control = {
+ init: function() {
+ this._initAutorefresh();
+ this._initDisplayMode();
+ },
+ update: Task.async(function*() {
+ let mode = this._displayMode;
+ if (this._autoRefreshInterval || !State._buffer[0]) {
+ // Update the state only if we are not on pause.
+ yield State.update();
+ }
+ yield wait(0);
+ let state = yield (mode == MODE_GLOBAL?
+ State.promiseDeltaSinceStartOfTime():
+ State.promiseDeltaSinceStartOfBuffer());
+
+ for (let category of ["webpages", "addons"]) {
+ yield wait(0);
+ yield View.updateCategory(state[category], category, category, mode);
+ }
+ yield wait(0);
+
+ // Make sure that we do not keep obsolete stuff around.
+ View.DOMCache.trimTo(state.deltas);
+
+ yield wait(0);
+
+ // Inform watchers
+ Services.obs.notifyObservers(null, UPDATE_COMPLETE_TOPIC, mode);
+ }),
+ _setOptions: function(options) {
+ dump(`about:performance _setOptions ${JSON.stringify(options)}\n`);
+ let eltRefresh = document.getElementById("check-autorefresh");
+ if ((options.autoRefresh > 0) != eltRefresh.checked) {
+ eltRefresh.click();
+ }
+ let eltCheckRecent = document.getElementById("check-display-recent");
+ if (!!options.displayRecent != eltCheckRecent.checked) {
+ eltCheckRecent.click();
+ }
+ },
+ _initAutorefresh: function() {
+ let onRefreshChange = (shouldUpdateNow = false) => {
+ if (eltRefresh.checked == !!this._autoRefreshInterval) {
+ // Nothing to change.
+ return;
+ }
+ if (eltRefresh.checked) {
+ this._autoRefreshInterval = window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
+ if (shouldUpdateNow) {
+ Control.update();
+ }
+ } else {
+ window.clearInterval(this._autoRefreshInterval);
+ this._autoRefreshInterval = null;
+ }
+ }
+
+ let eltRefresh = document.getElementById("check-autorefresh");
+ eltRefresh.addEventListener("change", () => onRefreshChange(true));
+
+ onRefreshChange(false);
+ },
+ _autoRefreshInterval: null,
+ _initDisplayMode: function() {
+ let onModeChange = (shouldUpdateNow) => {
+ if (eltCheckRecent.checked) {
+ this._displayMode = MODE_RECENT;
+ } else {
+ this._displayMode = MODE_GLOBAL;
+ }
+ if (shouldUpdateNow) {
+ Control.update();
+ }
+ };
+
+ let eltCheckRecent = document.getElementById("check-display-recent");
+ let eltLabelRecent = document.getElementById("label-display-recent");
+ eltCheckRecent.addEventListener("click", () => onModeChange(true));
+ eltLabelRecent.textContent = `Display only the latest ${Math.round(BUFFER_DURATION_MS/1000)}s`;
+
+ onModeChange(false);
+ },
+ // The display mode. One of `MODE_GLOBAL` or `MODE_RECENT`.
+ _displayMode: MODE_GLOBAL,
+};
+
+/**
+ * This functionality gets memory related information of sub-processes and
+ * updates the performance table regularly.
+ * If the page goes hidden, it also handles visibility change by not
+ * querying the content processes unnecessarily.
+ */
+var SubprocessMonitor = {
+ _timeout: null,
+
+ /**
+ * Init will start the process of updating the table if the page is not hidden,
+ * and set up an event listener for handling visibility changes.
+ */
+ init: function() {
+ if (!document.hidden) {
+ SubprocessMonitor.updateTable();
+ }
+ document.addEventListener("visibilitychange", SubprocessMonitor.handleVisibilityChange);
+ },
+
+ /**
+ * This function updates the table after an interval if the page is visible
+ * and clears the interval otherwise.
+ */
+ handleVisibilityChange: function() {
+ if (!document.hidden) {
+ SubprocessMonitor.queueUpdate();
+ } else {
+ clearTimeout(this._timeout);
+ this._timeout = null;
+ }
+ },
+
+ /**
+ * This function queues a timer to request the next summary using updateTable
+ * after some delay.
+ */
+ queueUpdate: function() {
+ this._timeout = setTimeout(() => this.updateTable(), UPDATE_INTERVAL_MS);
+ },
+
+ /**
+ * This is a helper function for updateTable, which updates a particular row.
+ * @param {<tr> node} row The row to be updated.
+ * @param {object} summaries The object with the updated RSS and USS values.
+ * @param {string} pid The pid represented by the row for which we update.
+ */
+ updateRow: function(row, summaries, pid) {
+ row.cells[0].textContent = pid;
+ let RSSval = DownloadUtils.convertByteUnits(summaries[pid].rss);
+ row.cells[1].textContent = RSSval.join(" ");
+ let USSval = DownloadUtils.convertByteUnits(summaries[pid].uss);
+ row.cells[2].textContent = USSval.join(" ");
+ },
+
+ /**
+ * This function adds a row to the subprocess-performance table for every new pid
+ * and populates and regularly updates it with RSS/USS measurements.
+ */
+ updateTable: function() {
+ if (!document.hidden) {
+ Memory.summary().then((summaries) => {
+ if (!(Object.keys(summaries).length)) {
+ // The summaries list was empty, which means we timed out getting
+ // the memory reports. We'll try again later.
+ SubprocessMonitor.queueUpdate();
+ return;
+ }
+ let resultTable = document.getElementById("subprocess-reports");
+ let recycle = [];
+ // We first iterate the table to check if summaries exist for rowPids,
+ // if yes, update them and delete the pid's summary or else hide the row
+ // for recycling it. Start at row 1 instead of 0 (to skip the header row).
+ for (let i = 1, row; row = resultTable.rows[i]; i++) {
+ let rowPid = row.dataset.pid;
+ let summary = summaries[rowPid];
+ if (summary) {
+ // Now we update the values in the row, which is hardcoded for now,
+ // but we might want to make this more adaptable in the future.
+ SubprocessMonitor.updateRow(row, summaries, rowPid);
+ delete summaries[rowPid];
+ } else {
+ // Take this unnecessary row, hide it and stash it for potential re-use.
+ row.hidden = true;
+ recycle.push(row);
+ }
+ }
+ // For the remaining pids in summaries, we choose from the recyclable
+ // (hidden) nodes, and if they get exhausted, append a row to the table.
+ for (let pid in summaries) {
+ let row = recycle.pop();
+ if (row) {
+ row.hidden = false;
+ } else {
+ // We create a new row here, and set it to row
+ row = document.createElement("tr");
+ // Insert cell for pid
+ row.insertCell();
+ // Insert a cell for USS.
+ row.insertCell();
+ // Insert another cell for RSS.
+ row.insertCell();
+ }
+ row.dataset.pid = pid;
+ // Update the row and put it at the bottom
+ SubprocessMonitor.updateRow(row, summaries, pid);
+ resultTable.appendChild(row);
+ }
+ });
+ SubprocessMonitor.queueUpdate();
+ }
+ },
+};
+
+var go = Task.async(function*() {
+
+ SubprocessMonitor.init();
+ Control.init();
+
+ // Setup a hook to allow tests to configure and control this page
+ let testUpdate = function(subject, topic, value) {
+ let options = JSON.parse(value);
+ Control._setOptions(options);
+ Control.update();
+ };
+ Services.obs.addObserver(testUpdate, TEST_DRIVER_TOPIC, false);
+ window.addEventListener("unload", () => Services.obs.removeObserver(testUpdate, TEST_DRIVER_TOPIC));
+
+ yield Control.update();
+ yield wait(BUFFER_SAMPLING_RATE_MS * 1.1);
+ yield Control.update();
+});
diff --git a/toolkit/components/aboutperformance/content/aboutPerformance.xhtml b/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
new file mode 100644
index 0000000000..6e0d148021
--- /dev/null
+++ b/toolkit/components/aboutperformance/content/aboutPerformance.xhtml
@@ -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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>about:performance</title>
+ <link rel="icon" type="image/png" id="favicon"
+ href="chrome://branding/content/icon32.png"/>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"
+ type="text/css"/>
+ <script type="text/javascript;version=1.8" src="chrome://global/content/aboutPerformance.js"></script>
+ <style>
+ @import url("chrome://global/skin/in-content/common.css");
+
+ html {
+ --aboutSupport-table-background: #ebebeb;
+ background-color: var(--in-content-page-background);
+ }
+ body {
+ margin: 40px 48px;
+ }
+ .hidden {
+ display: none;
+ }
+ .summary .title {
+ font-weight: bold;
+ }
+ a {
+ text-decoration: none;
+ }
+ a.more {
+ margin-left: 2ch;
+ }
+ ul.hidden_additional_items {
+ padding-top: 0;
+ margin-top: 0;
+ }
+ ul.visible_items {
+ padding-bottom: 0;
+ margin-bottom: 0;
+ }
+ li.delta {
+ margin-top: .5em;
+ }
+ h2 {
+ margin-top: 1cm;
+ }
+ button.show_all_items {
+ margin-top: .5cm;
+ margin-left: 1cm;
+ }
+ body {
+ margin-left: 1cm;
+ }
+ div.measuring {
+ background: url(chrome://global/skin/media/throbber.png) no-repeat center;
+ min-width: 36px;
+ min-height: 36px;
+ }
+ li.delta {
+ border-left-width: 5px;
+ border-left-style: solid;
+ padding-left: 1em;
+ list-style: none;
+ }
+ li.delta[impact="0"] {
+ border-left-color: rgb(0, 255, 0);
+ }
+ li.delta[impact="1"] {
+ border-left-color: rgb(24, 231, 0);
+ }
+ li.delta[impact="2"] {
+ border-left-color: rgb(48, 207, 0);
+ }
+ li.delta[impact="3"] {
+ border-left-color: rgb(72, 183, 0);
+ }
+ li.delta[impact="4"] {
+ border-left-color: rgb(96, 159, 0);
+ }
+ li.delta[impact="5"] {
+ border-left-color: rgb(120, 135, 0);
+ }
+ li.delta[impact="6"] {
+ border-left-color: rgb(144, 111, 0);
+ }
+ li.delta[impact="7"] {
+ border-left-color: rgb(168, 87, 0);
+ }
+ li.delta[impact="8"] {
+ border-left-color: rgb(192, 63, 0);
+ }
+ li.delta[impact="9"] {
+ border-left-color: rgb(216, 39, 0);
+ }
+ li.delta[impact="10"] {
+ border-left-color: rgb(240, 15, 0);
+ }
+ li.delta[impact="11"] {
+ border-left-color: rgb(255, 0, 0);
+ }
+
+ #subprocess-reports {
+ background-color: var(--aboutSupport-table-background);
+ color: var(--in-content-text-color);
+ font: message-box;
+ text-align: start;
+ border: 1px solid var(--in-content-border-color);
+ border-spacing: 0px;
+ float: right;
+ margin-bottom: 20px;
+ -moz-margin-start: 20px;
+ -moz-margin-end: 0;
+ width: 100%;
+ }
+ #subprocess-reports:-moz-dir(rtl) {
+ float: left;
+ }
+ #subprocess-reports th,
+ #subprocess-reports td {
+ border: 1px solid var(--in-content-border-color);
+ padding: 4px;
+ }
+ #subprocess-reports thead th {
+ text-align: center;
+ }
+ #subprocess-reports th {
+ text-align: start;
+ background-color: var(--in-content-table-header-background);
+ color: var(--in-content-selected-text);
+ }
+ #subprocess-reports th.column {
+ white-space: nowrap;
+ width: 0px;
+ }
+ #subprocess-reports td {
+ background-color: #ebebeb;
+ text-align: start;
+ border-color: var(--in-content-table-border-dark-color);
+ border-spacing: 40px;
+ }
+ .options {
+ width: 100%;
+ }
+ .options > .toggle-container-with-text {
+ display: inline-flex;
+ }
+ .options > .toggle-container-with-text:not(:first-child) {
+ margin-inline-start: 2ch;
+ }
+ </style>
+ </head>
+ <body onload="go()">
+ <div>
+ <h2>Memory usage of Subprocesses</h2>
+ <table id="subprocess-reports">
+ <tr>
+ <th>Process ID</th>
+ <th title="RSS measures the pages resident in the main memory for the process">Resident Set Size</th>
+ <th title="USS gives a count of unshared pages, unique to the process">Unique Set Size</th>
+ </tr>
+ </table>
+ </div>
+ <div class="options">
+ <div class="toggle-container-with-text">
+ <input type="checkbox" checked="false" id="check-display-recent"></input>
+ <label for="check-display-recent" id="label-display-recent">Display only the last few seconds.</label>
+ </div>
+ <div class="toggle-container-with-text">
+ <input type="checkbox" checked="true" id="check-autorefresh"></input>
+ <label for="check-autorefresh">Refresh automatically</label>
+ </div>
+ </div>
+ <div>
+ <h2>Performance of Add-ons</h2>
+ <div id="addons" class="measuring">
+ </div>
+ </div>
+ <div>
+ <h2>Performance of Web pages</h2>
+ <div id="webpages" class="measuring">
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/toolkit/components/aboutperformance/jar.mn b/toolkit/components/aboutperformance/jar.mn
new file mode 100644
index 0000000000..96e046d8ee
--- /dev/null
+++ b/toolkit/components/aboutperformance/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/.
+
+toolkit.jar:
+ content/global/aboutPerformance.xhtml (content/aboutPerformance.xhtml)
+ content/global/aboutPerformance.js (content/aboutPerformance.js)
diff --git a/toolkit/components/aboutperformance/moz.build b/toolkit/components/aboutperformance/moz.build
new file mode 100644
index 0000000000..d8e6acd958
--- /dev/null
+++ b/toolkit/components/aboutperformance/moz.build
@@ -0,0 +1,9 @@
+# -*- 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']
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
diff --git a/toolkit/components/aboutperformance/tests/browser/.eslintrc.js b/toolkit/components/aboutperformance/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/aboutperformance/tests/browser/browser.ini b/toolkit/components/aboutperformance/tests/browser/browser.ini
new file mode 100644
index 0000000000..92f1d98e6a
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = head.js
+support-files =
+ browser_compartments.html
+ browser_compartments_frame.html
+ browser_compartments_script.js
+
+[browser_aboutperformance.js]
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
new file mode 100644
index 0000000000..60760ea7f8
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_aboutperformance.js
@@ -0,0 +1,300 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://testing-common/ContentTask.jsm", this);
+
+const URL = "http://example.com/browser/toolkit/components/aboutperformance/tests/browser/browser_compartments.html?test=" + Math.random();
+
+// This function is injected as source as a frameScript
+function frameScript() {
+ "use strict";
+
+ addMessageListener("aboutperformance-test:done", () => {
+ content.postMessage("stop", "*");
+ sendAsyncMessage("aboutperformance-test:done", null);
+ });
+ addMessageListener("aboutperformance-test:setTitle", ({data: title}) => {
+ content.document.title = title;
+ sendAsyncMessage("aboutperformance-test:setTitle", null);
+ });
+
+ addMessageListener("aboutperformance-test:closeTab", ({data: options}) => {
+ let observer = function(subject, topic, mode) {
+ dump(`aboutperformance-test:closeTab 1 ${options.url}\n`);
+ Services.obs.removeObserver(observer, "about:performance-update-complete");
+
+ let exn;
+ let found = false;
+ try {
+ for (let eltContent of content.document.querySelectorAll("li.delta")) {
+ let eltName = eltContent.querySelector("li.name");
+ if (!eltName.textContent.includes(options.url)) {
+ continue;
+ }
+
+ found = true;
+ let [eltCloseTab, eltReloadTab] = eltContent.querySelectorAll("button");
+ let button;
+ if (options.mode == "reload") {
+ button = eltReloadTab;
+ } else if (options.mode == "close") {
+ button = eltCloseTab;
+ } else {
+ throw new TypeError(options.mode);
+ }
+ dump(`aboutperformance-test:closeTab clicking on ${button.textContent}\n`);
+ button.click();
+ return;
+ }
+ } catch (ex) {
+ dump(`aboutperformance-test:closeTab: error ${ex}\n`);
+ exn = ex;
+ } finally {
+ if (exn) {
+ sendAsyncMessage("aboutperformance-test:closeTab", { error: {message: exn.message, lineNumber: exn.lineNumber, fileName: exn.fileName}, found});
+ } else {
+ sendAsyncMessage("aboutperformance-test:closeTab", { ok: true, found });
+ }
+ }
+ }
+ Services.obs.addObserver(observer, "about:performance-update-complete", false);
+ Services.obs.notifyObservers(null, "test-about:performance-test-driver", JSON.stringify(options));
+ });
+
+ addMessageListener("aboutperformance-test:checkSanity", ({data: options}) => {
+ let exn = null;
+ try {
+ let reFullname = /Full name: (.+)/;
+ let reFps = /Impact on framerate: (\d+)\/10( \((\d+) alerts\))?/;
+ let reCpow = /Blocking process calls: (\d+)%( \((\d+) alerts\))?/;
+
+ let getContentOfSelector = function(eltContainer, selector, re) {
+ let elt = eltContainer.querySelector(selector);
+ if (!elt) {
+ throw new Error(`No item ${selector}`);
+ }
+
+ if (!re) {
+ return undefined;
+ }
+
+ let match = elt.textContent.match(re);
+ if (!match) {
+ throw new Error(`Item ${selector} doesn't match regexp ${re}: ${elt.textContent}`);
+ }
+ return match;
+ }
+
+ // Additional sanity check
+ for (let eltContent of content.document.querySelectorAll("delta")) {
+ // Do we have an attribute "impact"? Is it a number between 0 and 10?
+ let impact = eltContent.classList.getAttribute("impact");
+ let value = Number.parseInt(impact);
+ if (isNaN(value) || value < 0 || value > 10) {
+ throw new Error(`Incorrect value ${value}`);
+ }
+
+ // Do we have a button "more"?
+ getContentOfSelector(eltContent, "a.more");
+
+ // Do we have details?
+ getContentOfSelector(eltContent, "ul.details");
+
+ // Do we have a full name? Does it make sense?
+ getContentOfSelector(eltContent, "li.name", reFullname);
+
+ // Do we have an impact on framerate? Does it make sense?
+ let [, jankStr,, alertsStr] = getContentOfSelector(eltDetails, "li.fps", reFps);
+ let jank = Number.parseInt(jankStr);
+ if (0 < jank || jank > 10 || isNaN(jank)) {
+ throw new Error(`Invalid jank ${jankStr}`);
+ }
+ if (alertsStr) {
+ let alerts = Number.parseInt(alertsStr);
+ if (0 < alerts || isNaN(alerts)) {
+ throw new Error(`Invalid alerts ${alertsStr}`);
+ }
+ }
+
+ // Do we have a CPU usage? Does it make sense?
+ let [, cpuStr] = getContentOfSelector(eltDetails, "li.cpu", reCPU);
+ let cpu = Number.parseInt(cpuStr);
+ if (0 < cpu || isNaN(cpu)) { // Note that cpu can be > 100%.
+ throw new Error(`Invalid CPU ${cpuStr}`);
+ }
+
+ // Do we have CPOW? Does it make sense?
+ let [, cpowStr,, alertsStr2] = getContentOfSelector(eltDetails, "li.cpow", reCpow);
+ let cpow = Number.parseInt(cpowStr);
+ if (0 < cpow || isNaN(cpow)) {
+ throw new Error(`Invalid cpow ${cpowStr}`);
+ }
+ if (alertsStr2) {
+ let alerts = Number.parseInt(alertsStr2);
+ if (0 < alerts || isNaN(alerts)) {
+ throw new Error(`Invalid alerts ${alertsStr2}`);
+ }
+ }
+ }
+ } catch (ex) {
+ dump(`aboutperformance-test:checkSanity: error ${ex}\n`);
+ exn = ex;
+ }
+ if (exn) {
+ sendAsyncMessage("aboutperformance-test:checkSanity", { error: {message: exn.message, lineNumber: exn.lineNumber, fileName: exn.fileName}});
+ } else {
+ sendAsyncMessage("aboutperformance-test:checkSanity", { ok: true });
+ }
+ });
+
+ addMessageListener("aboutperformance-test:hasItems", ({data: {title, options}}) => {
+ let observer = function(subject, topic, mode) {
+ Services.obs.removeObserver(observer, "about:performance-update-complete");
+ let hasTitleInWebpages = false;
+ let hasTitleInAddons = false;
+
+ try {
+ let eltWeb = content.document.getElementById("webpages");
+ let eltAddons = content.document.getElementById("addons");
+ if (!eltWeb || !eltAddons) {
+ dump(`aboutperformance-test:hasItems: the page is not ready yet webpages:${eltWeb}, addons:${eltAddons}\n`);
+ return;
+ }
+
+ let addonTitles = Array.from(eltAddons.querySelectorAll("span.title"), elt => elt.textContent);
+ let webTitles = Array.from(eltWeb.querySelectorAll("span.title"), elt => elt.textContent);
+
+ hasTitleInAddons = addonTitles.includes(title);
+ hasTitleInWebpages = webTitles.includes(title);
+ } catch (ex) {
+ Cu.reportError("Error in content: " + ex);
+ Cu.reportError(ex.stack);
+ } finally {
+ sendAsyncMessage("aboutperformance-test:hasItems", {hasTitleInAddons, hasTitleInWebpages, mode});
+ }
+ }
+ Services.obs.addObserver(observer, "about:performance-update-complete", false);
+ Services.obs.notifyObservers(null, "test-about:performance-test-driver", JSON.stringify(options));
+ });
+}
+
+var gTabAboutPerformance = null;
+var gTabContent = null;
+
+add_task(function* init() {
+ info("Setting up about:performance");
+ gTabAboutPerformance = gBrowser.selectedTab = gBrowser.addTab("about:performance");
+ yield ContentTask.spawn(gTabAboutPerformance.linkedBrowser, null, frameScript);
+
+ info(`Setting up ${URL}`);
+ gTabContent = gBrowser.addTab(URL);
+ yield ContentTask.spawn(gTabContent.linkedBrowser, null, frameScript);
+});
+
+var promiseExpectContent = Task.async(function*(options) {
+ let title = "Testing about:performance " + Math.random();
+ for (let i = 0; i < 30; ++i) {
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield promiseContentResponse(gTabContent.linkedBrowser, "aboutperformance-test:setTitle", title);
+ let {hasTitleInWebpages, hasTitleInAddons, mode} = (yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:hasItems", {title, options}));
+
+ info(`aboutperformance-test:hasItems ${hasTitleInAddons}, ${hasTitleInWebpages}, ${mode}, ${options.displayRecent}`);
+ if (!hasTitleInWebpages) {
+ info(`Title not found in webpages`);
+ continue;
+ }
+ if ((mode == "recent") != options.displayRecent) {
+ info(`Wrong mode`);
+ continue;
+ }
+ Assert.ok(!hasTitleInAddons, "The title appears in webpages, but not in addons");
+
+ let { ok, error } = yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:checkSanity", {options});
+ if (ok) {
+ info("aboutperformance-test:checkSanity: success");
+ }
+ if (error) {
+ Assert.ok(false, `aboutperformance-test:checkSanity error: ${JSON.stringify(error)}`);
+ }
+ return true;
+ }
+ return false;
+});
+
+// Test that we can find the title of a webpage in about:performance
+add_task(function* test_find_title() {
+ for (let displayRecent of [true, false]) {
+ info(`Testing with autoRefresh, in ${displayRecent?"recent":"global"} mode`);
+ let found = yield promiseExpectContent({autoRefresh: 100, displayRecent});
+ Assert.ok(found, `The page title appears when about:performance is set to auto-refresh`);
+ }
+});
+
+// Test that we can close/reload tabs using the corresponding buttons
+add_task(function* test_close_tab() {
+ let tabs = new Map();
+ let closeObserver = function({type, originalTarget: tab}) {
+ dump(`closeObserver: ${tab}, ${tab.constructor.name}, ${tab.tagName}, ${type}\n`);
+ let cb = tabs.get(tab);
+ if (cb) {
+ cb(type);
+ }
+ };
+ let promiseTabClosed = function(tab) {
+ return new Promise(resolve => tabs.set(tab, resolve));
+ }
+ window.gBrowser.tabContainer.addEventListener("TabClose", closeObserver);
+ let promiseTabReloaded = function(tab) {
+ return new Promise(resolve =>
+ tab.linkedBrowser.contentDocument.addEventListener("readystatechange", resolve)
+ );
+ }
+ for (let displayRecent of [true, false]) {
+ for (let mode of ["close", "reload"]) {
+ let URL = `about:about?display-recent=${displayRecent}&mode=${mode}&salt=${Math.random()}`;
+ info(`Setting up ${URL}`);
+ let tab = gBrowser.addTab(URL);
+ yield ContentTask.spawn(tab.linkedBrowser, null, frameScript);
+ let promiseClosed = promiseTabClosed(tab);
+ let promiseReloaded = promiseTabReloaded(tab);
+
+ info(`Requesting close`);
+ do {
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield promiseContentResponse(tab.linkedBrowser, "aboutperformance-test:setTitle", URL);
+
+ let {ok, found, error} = yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:closeTab", {url: URL, autoRefresh: true, mode, displayRecent});
+ Assert.ok(ok, `Message aboutperformance-test:closeTab was handled correctly ${JSON.stringify(error)}`);
+ info(`URL ${URL} ${found?"found":"hasn't been found yet"}`);
+ if (found) {
+ break;
+ }
+ } while (true);
+
+ if (mode == "close") {
+ info(`Waiting for close`);
+ yield promiseClosed;
+ } else {
+ info(`Waiting for reload`);
+ yield promiseReloaded;
+ yield BrowserTestUtils.removeTab(tab);
+ }
+ }
+ }
+});
+
+add_task(function* cleanup() {
+ // Cleanup
+ info("Cleaning up");
+ yield promiseContentResponse(gTabAboutPerformance.linkedBrowser, "aboutperformance-test:done", null);
+
+ info("Closing tabs");
+ for (let tab of gBrowser.tabs) {
+ yield BrowserTestUtils.removeTab(tab);
+ }
+
+ info("Done");
+ gBrowser.selectedTab = null;
+});
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments.html b/toolkit/components/aboutperformance/tests/browser/browser_compartments.html
new file mode 100644
index 0000000000..a74a5745af
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>
+ Main frame for test browser_aboutperformance.js
+ </title>
+</head>
+<body>
+Main frame.
+
+<iframe src="browser_compartments_frame.html?frame=1">
+ Subframe 1
+</iframe>
+
+<iframe src="browser_compartments_frame.html?frame=2">
+ Subframe 2.
+</iframe>
+
+</body>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html b/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html
new file mode 100644
index 0000000000..69edfe871b
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>
+ Subframe for test browser_compartments.html (do not change this title)
+ </title>
+ <script src="browser_compartments_script.js"></script>
+</head>
+<body>
+Subframe loaded.
+</body>
+</html>
diff --git a/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js b/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js
new file mode 100644
index 0000000000..3d5f7114f6
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/browser_compartments_script.js
@@ -0,0 +1,29 @@
+
+var carryOn = true;
+
+window.addEventListener("message", e => {
+ console.log("frame content", "message", e);
+ if ("title" in e.data) {
+ document.title = e.data.title;
+ }
+ if ("stop" in e.data) {
+ carryOn = false;
+ }
+});
+
+// Use some CPU.
+var interval = window.setInterval(() => {
+ if (!carryOn) {
+ window.clearInterval(interval);
+ return;
+ }
+
+ // Compute an arbitrary value, print it out to make sure that the JS
+ // engine doesn't discard all our computation.
+ var date = Date.now();
+ var array = [];
+ var i = 0;
+ while (Date.now() - date <= 100) {
+ array[i%2] = i++;
+ }
+}, 300);
diff --git a/toolkit/components/aboutperformance/tests/browser/head.js b/toolkit/components/aboutperformance/tests/browser/head.js
new file mode 100644
index 0000000000..a15536ffd3
--- /dev/null
+++ b/toolkit/components/aboutperformance/tests/browser/head.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+function promiseContentResponse(browser, name, message) {
+ let mm = browser.messageManager;
+ let promise = new Promise(resolve => {
+ function removeListener() {
+ mm.removeMessageListener(name, listener);
+ }
+
+ function listener(msg) {
+ removeListener();
+ resolve(msg.data);
+ }
+
+ mm.addMessageListener(name, listener);
+ registerCleanupFunction(removeListener);
+ });
+ mm.sendAsyncMessage(name, message);
+ return promise;
+}
+function promiseContentResponseOrNull(browser, name, message) {
+ if (!browser.messageManager) {
+ return null;
+ }
+ return promiseContentResponse(browser, name, message);
+}
+
+/**
+ * `true` if we are running an OS in which the OS performance
+ * clock has a low precision and might unpredictably
+ * never be updated during the execution of the test.
+ */
+function hasLowPrecision() {
+ let [sysName, sysVersion] = [Services.sysinfo.getPropertyAsAString("name"), Services.sysinfo.getPropertyAsDouble("version")];
+ info(`Running ${sysName} version ${sysVersion}`);
+
+ if (sysName == "Windows_NT" && sysVersion < 6) {
+ info("Running old Windows, need to deactivate tests due to bad precision.");
+ return true;
+ }
+ if (sysName == "Linux" && sysVersion <= 2.6) {
+ info("Running old Linux, need to deactivate tests due to bad precision.");
+ return true;
+ }
+ info("This platform has good precision.")
+ return false;
+}
diff --git a/toolkit/components/addoncompat/CompatWarning.jsm b/toolkit/components/addoncompat/CompatWarning.jsm
new file mode 100644
index 0000000000..b32409a46b
--- /dev/null
+++ b/toolkit/components/addoncompat/CompatWarning.jsm
@@ -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/.
+
+this.EXPORTED_SYMBOLS = ["CompatWarning"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+function section(number, url)
+{
+ const baseURL = "https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts";
+ return { number, url: baseURL + url };
+}
+
+var CompatWarning = {
+ // Sometimes we want to generate a warning, but put off issuing it
+ // until later. For example, if someone registers a listener, we
+ // might only want to warn about it if the listener actually
+ // fires. However, we want the warning to show a stack for the
+ // registration site.
+ delayedWarning: function(msg, addon, warning) {
+ function isShimLayer(filename) {
+ return filename.indexOf("CompatWarning.jsm") != -1 ||
+ filename.indexOf("RemoteAddonsParent.jsm") != -1 ||
+ filename.indexOf("RemoteAddonsChild.jsm") != -1 ||
+ filename.indexOf("multiprocessShims.js") != -1;
+ }
+
+ let stack = Components.stack;
+ while (stack && isShimLayer(stack.filename))
+ stack = stack.caller;
+
+ let alreadyWarned = false;
+
+ return function() {
+ if (alreadyWarned) {
+ return;
+ }
+ alreadyWarned = true;
+
+ if (addon) {
+ let histogram = Services.telemetry.getKeyedHistogramById("ADDON_SHIM_USAGE");
+ histogram.add(addon, warning ? warning.number : 0);
+ }
+
+ if (!Preferences.get("dom.ipc.shims.enabledWarnings", false))
+ return;
+
+ let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
+ if (!error || !Services.console) {
+ // Too late during shutdown to use the nsIConsole
+ return;
+ }
+
+ let message = `Warning: ${msg}`;
+ if (warning)
+ message += `\nMore info at: ${warning.url}`;
+
+ error.init(
+ /* message*/ message,
+ /* sourceName*/ stack ? stack.filename : "",
+ /* sourceLine*/ stack ? stack.sourceLine : "",
+ /* lineNumber*/ stack ? stack.lineNumber : 0,
+ /* columnNumber*/ 0,
+ /* flags*/ Ci.nsIScriptError.warningFlag,
+ /* category*/ "chrome javascript");
+ Services.console.logMessage(error);
+
+ if (Preferences.get("dom.ipc.shims.dumpWarnings", false)) {
+ dump(message + "\n");
+ while (stack) {
+ dump(stack + "\n");
+ stack = stack.caller;
+ }
+ dump("\n");
+ }
+ };
+ },
+
+ warn: function(msg, addon, warning) {
+ let delayed = this.delayedWarning(msg, addon, warning);
+ delayed();
+ },
+
+ warnings: {
+ content: section(1, "#gBrowser.contentWindow.2C_window.content..."),
+ limitations_of_CPOWs: section(2, "#Limitations_of_CPOWs"),
+ nsIContentPolicy: section(3, "#nsIContentPolicy"),
+ nsIWebProgressListener: section(4, "#nsIWebProgressListener"),
+ observers: section(5, "#Observers_in_the_chrome_process"),
+ DOM_events: section(6, "#DOM_Events"),
+ sandboxes: section(7, "#Sandboxes"),
+ JSMs: section(8, "#JavaScript_code_modules_(JSMs)"),
+ nsIAboutModule: section(9, "#nsIAboutModule"),
+ // If more than 14 values appear here, you need to change the
+ // ADDON_SHIM_USAGE histogram definition in Histograms.json.
+ },
+};
diff --git a/toolkit/components/addoncompat/Prefetcher.jsm b/toolkit/components/addoncompat/Prefetcher.jsm
new file mode 100644
index 0000000000..2d836690cc
--- /dev/null
+++ b/toolkit/components/addoncompat/Prefetcher.jsm
@@ -0,0 +1,557 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.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.EXPORTED_SYMBOLS = ["Prefetcher"];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+// Rules are defined at the bottom of this file.
+var PrefetcherRules = {};
+
+/*
+ * When events that trigger in the content process are forwarded to
+ * add-ons in the chrome process, we expect the add-ons to send a lot
+ * of CPOWs to query content nodes while processing the events. To
+ * speed this up, the prefetching system anticipates which properties
+ * will be read and reads them ahead of time. The prefetched
+ * properties are passed to the chrome process along with each
+ * event. A typical scenario might work like this:
+ *
+ * 1. "load" event fires in content
+ * 2. Content process prefetches:
+ * event.target.defaultView = <win 1>
+ * <win 1>.location = <location obj>
+ * event.target.getElementsByTagName("form") = [<elt 1>, <elt 2>]
+ * <elt 1>.id = "login-form"
+ * <elt 2>.id = "subscribe-form"
+ * 3. Content process forwards "load" event to add-on along with
+ * prefetched data
+ * 4. Add-on reads:
+ * event.target.defaultView (already prefetched)
+ * event.target.getElementsByTagName("form") (already prefetched)
+ * <elt 1>.id (already prefetched)
+ * <elt 1>.className (not prefetched; CPOW must be sent)
+ *
+ * The amount of data to prefetch is determined based on the add-on ID
+ * and the event type. The specific data to select is determined using
+ * a set of Datalog-like rules (http://en.wikipedia.org/wiki/Datalog).
+ *
+ * Rules operate on a series of "tables" like in a database. Each
+ * table contains a set of content-process objects. When an event
+ * handler runs, it seeds some initial tables with objects of
+ * interest. For example, the Event table might start out containing
+ * the event that fired.
+ *
+ * Objects are added to tables using a set of rules of the form "if X
+ * is in table A, then add F(X) to table B", where F(X) is typically a
+ * property access or a method call. The most common functions F are:
+ *
+ * PropertyOp(destTable, sourceTable, property):
+ * For each object X in sourceTable, add X.property to destTable.
+ * MethodOp(destTable, sourceTable, method, args):
+ * For each object X in sourceTable, add X.method(args) to destTable.
+ * CollectionOp(destTable, sourceTable):
+ * For each object X in sourceTable, add X[i] to destTable for
+ * all i from 0 to X.length - 1.
+ *
+ * To generate the prefetching in the example above, the following
+ * rules would work:
+ *
+ * 1. PropertyOp("EventTarget", "Event", "target")
+ * 2. PropertyOp("Window", "EventTarget", "defaultView")
+ * 3. MethodOp("FormCollection", "EventTarget", "getElementsByTagName", "form")
+ * 4. CollectionOp("Form", "FormCollection")
+ * 5. PropertyOp(null, "Form", "id")
+ *
+ * Rules are written at the bottom of this file.
+ *
+ * When a rule runs, it will usually generate some cache entries that
+ * will be passed to the chrome process. For example, when PropertyOp
+ * prefetches obj.prop and gets the value X, it caches the value of
+ * obj and X. When the chrome process receives this data, it creates a
+ * two-level map [obj -> [prop -> X]]. When the add-on accesses a
+ * property on obj, the add-on shim code consults this map to see if
+ * the property has already been cached.
+ */
+
+const PREF_PREFETCHING_ENABLED = "extensions.interposition.prefetching";
+
+function isPrimitive(v) {
+ if (!v)
+ return true;
+ let type = typeof(v);
+ return type !== "object" && type !== "function";
+}
+
+function objAddr(obj)
+{
+/*
+ if (!isPrimitive(obj)) {
+ return String(obj) + "[" + Cu.getJSTestingFunctions().objectAddress(obj) + "]";
+ }
+ return String(obj);
+*/
+}
+
+function log(/* ...args*/)
+{
+/*
+ for (let arg of args) {
+ dump(arg);
+ dump(" ");
+ }
+ dump("\n");
+*/
+}
+
+function logPrefetch(/* kind, value1, component, value2*/)
+{
+/*
+ log("prefetching", kind, objAddr(value1) + "." + component, "=", objAddr(value2));
+*/
+}
+
+/*
+ * All the Op classes (representing Datalog rules) have the same interface:
+ * outputTable: Table that objects generated by the rule are added to.
+ * Note that this can be null.
+ * inputTable: Table that the rule draws objects from.
+ * addObject(database, obj): Called when an object is added to inputTable.
+ * This code should take care of adding objects to outputTable.
+ * Data to be cached should be stored by calling database.cache.
+ * makeCacheEntry(item, cache):
+ * Called by the chrome process to create the two-level map of
+ * prefetched objects. |item| holds the cached data
+ * generated by the content process. |cache| is the map to be
+ * generated.
+ */
+
+function PropertyOp(outputTable, inputTable, prop)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+ this.prop = prop;
+}
+
+PropertyOp.prototype.addObject = function(database, obj)
+{
+ let has = false, propValue;
+ try {
+ if (this.prop in obj) {
+ has = true;
+ propValue = obj[this.prop];
+ }
+ } catch (e) {
+ // Don't cache anything if an exception is thrown.
+ return;
+ }
+
+ logPrefetch("prop", obj, this.prop, propValue);
+ database.cache(this.index, obj, has, propValue);
+ if (has && !isPrimitive(propValue) && this.outputTable) {
+ database.add(this.outputTable, propValue);
+ }
+}
+
+PropertyOp.prototype.makeCacheEntry = function(item, cache)
+{
+ let [, obj, , propValue] = item;
+
+ let desc = { configurable: false, enumerable: true, writable: false, value: propValue };
+
+ if (!cache.has(obj)) {
+ cache.set(obj, new Map());
+ }
+ let propMap = cache.get(obj);
+ propMap.set(this.prop, desc);
+}
+
+function MethodOp(outputTable, inputTable, method, ...args)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+ this.method = method;
+ this.args = args;
+}
+
+MethodOp.prototype.addObject = function(database, obj)
+{
+ let result;
+ try {
+ result = obj[this.method].apply(obj, this.args);
+ } catch (e) {
+ // Don't cache anything if an exception is thrown.
+ return;
+ }
+
+ logPrefetch("method", obj, this.method + "(" + this.args + ")", result);
+ database.cache(this.index, obj, result);
+ if (!isPrimitive(result) && this.outputTable) {
+ database.add(this.outputTable, result);
+ }
+}
+
+MethodOp.prototype.makeCacheEntry = function(item, cache)
+{
+ let [, obj, result] = item;
+
+ if (!cache.has(obj)) {
+ cache.set(obj, new Map());
+ }
+ let propMap = cache.get(obj);
+ let fallback = propMap.get(this.method);
+
+ let method = this.method;
+ let selfArgs = this.args;
+ let methodImpl = function(...args) {
+ if (args.length == selfArgs.length && args.every((v, i) => v === selfArgs[i])) {
+ return result;
+ }
+
+ if (fallback) {
+ return fallback.value(...args);
+ }
+ return obj[method](...args);
+ };
+
+ let desc = { configurable: false, enumerable: true, writable: false, value: methodImpl };
+ propMap.set(this.method, desc);
+}
+
+function CollectionOp(outputTable, inputTable)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+}
+
+CollectionOp.prototype.addObject = function(database, obj)
+{
+ let elements = [];
+ try {
+ let len = obj.length;
+ for (let i = 0; i < len; i++) {
+ logPrefetch("index", obj, i, obj[i]);
+ elements.push(obj[i]);
+ }
+ } catch (e) {
+ // Don't cache anything if an exception is thrown.
+ return;
+ }
+
+ database.cache(this.index, obj, ...elements);
+ for (let i = 0; i < elements.length; i++) {
+ if (!isPrimitive(elements[i]) && this.outputTable) {
+ database.add(this.outputTable, elements[i]);
+ }
+ }
+}
+
+CollectionOp.prototype.makeCacheEntry = function(item, cache)
+{
+ let [, obj, ...elements] = item;
+
+ if (!cache.has(obj)) {
+ cache.set(obj, new Map());
+ }
+ let propMap = cache.get(obj);
+
+ let lenDesc = { configurable: false, enumerable: true, writable: false, value: elements.length };
+ propMap.set("length", lenDesc);
+
+ for (let i = 0; i < elements.length; i++) {
+ let desc = { configurable: false, enumerable: true, writable: false, value: elements[i] };
+ propMap.set(i, desc);
+ }
+}
+
+function CopyOp(outputTable, inputTable)
+{
+ this.outputTable = outputTable;
+ this.inputTable = inputTable;
+}
+
+CopyOp.prototype.addObject = function(database, obj)
+{
+ database.add(this.outputTable, obj);
+}
+
+function Database(trigger, addons)
+{
+ // Create a map of rules that apply to this specific trigger and set
+ // of add-ons. The rules are indexed based on their inputTable.
+ this.rules = new Map();
+ for (let addon of addons) {
+ let addonRules = PrefetcherRules[addon] || {};
+ let triggerRules = addonRules[trigger] || [];
+ for (let rule of triggerRules) {
+ let inTable = rule.inputTable;
+ if (!this.rules.has(inTable)) {
+ this.rules.set(inTable, new Set());
+ }
+ let set = this.rules.get(inTable);
+ set.add(rule);
+ }
+ }
+
+ // this.tables maps table names to sets of objects contained in them.
+ this.tables = new Map();
+
+ // todo is a worklist of items added to tables that have not had
+ // rules run on them yet.
+ this.todo = [];
+
+ // Cached data to be sent to the chrome process.
+ this.cached = [];
+}
+
+Database.prototype = {
+ // Add an object to a table.
+ add: function(table, obj) {
+ if (!this.tables.has(table)) {
+ this.tables.set(table, new Set());
+ }
+ let tableSet = this.tables.get(table);
+ if (tableSet.has(obj)) {
+ return;
+ }
+ tableSet.add(obj);
+
+ this.todo.push([table, obj]);
+ },
+
+ cache: function(...args) {
+ this.cached.push(args);
+ },
+
+ // Run a fixed-point iteration that adds objects to table based on
+ // this.rules until there are no more objects to add.
+ process: function() {
+ while (this.todo.length) {
+ let [table, obj] = this.todo.pop();
+ let rules = this.rules.get(table);
+ if (!rules) {
+ continue;
+ }
+ for (let rule of rules) {
+ rule.addObject(this, obj);
+ }
+ }
+ },
+};
+
+var Prefetcher = {
+ init: function() {
+ // Give an index to each rule and store it in this.ruleMap based
+ // on the index. The index is used to serialize and deserialize
+ // data from content to chrome.
+ let counter = 0;
+ this.ruleMap = new Map();
+ for (let addon in PrefetcherRules) {
+ for (let trigger in PrefetcherRules[addon]) {
+ for (let rule of PrefetcherRules[addon][trigger]) {
+ rule.index = counter++;
+ this.ruleMap.set(rule.index, rule);
+ }
+ }
+ }
+
+ this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
+ Services.prefs.addObserver(PREF_PREFETCHING_ENABLED, this, false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_PREFETCHING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ } else if (topic == PREF_PREFETCHING_ENABLED) {
+ this.prefetchingEnabled = Preferences.get(PREF_PREFETCHING_ENABLED, false);
+ }
+ },
+
+ // Called when an event occurs in the content process. The event is
+ // described by the trigger string. |addons| is a list of addons
+ // that have listeners installed for the event. |args| is
+ // event-specific data (such as the event object).
+ prefetch: function(trigger, addons, args) {
+ if (!this.prefetchingEnabled) {
+ return [[], []];
+ }
+
+ let db = new Database(trigger, addons);
+ for (let table in args) {
+ log("root", table, "=", objAddr(args[table]));
+ db.add(table, args[table]);
+ }
+
+ // Prefetch objects and add them to tables.
+ db.process();
+
+ // Data passed to sendAsyncMessage must be split into a JSON
+ // portion and a CPOW portion. This code splits apart db.cached
+ // into these two pieces. Any object in db.cache is added to an
+ // array of CPOWs and replaced with {cpow: <index in array>}.
+ let cpowIndexes = new Map();
+ let prefetched = [];
+ let cpows = [];
+ for (let item of db.cached) {
+ item = item.map((elt) => {
+ if (!isPrimitive(elt)) {
+ if (!cpowIndexes.has(elt)) {
+ let index = cpows.length;
+ cpows.push(elt);
+ cpowIndexes.set(elt, index);
+ }
+ return {cpow: cpowIndexes.get(elt)};
+ }
+ return elt;
+ });
+
+ prefetched.push(item);
+ }
+
+ return [prefetched, cpows];
+ },
+
+ cache: null,
+
+ // Generate a two-level mapping based on cached data received from
+ // the content process.
+ generateCache: function(prefetched, cpows) {
+ let cache = new Map();
+ for (let item of prefetched) {
+ // Replace anything of the form {cpow: <index>} with the actual
+ // object in |cpows|.
+ item = item.map((elt) => {
+ if (!isPrimitive(elt)) {
+ return cpows[elt.cpow];
+ }
+ return elt;
+ });
+
+ let index = item[0];
+ let op = this.ruleMap.get(index);
+ op.makeCacheEntry(item, cache);
+ }
+ return cache;
+ },
+
+ // Run |func|, using the prefetched data in |prefetched| and |cpows|
+ // as a cache.
+ withPrefetching: function(prefetched, cpows, func) {
+ if (!this.prefetchingEnabled) {
+ return func();
+ }
+
+ this.cache = this.generateCache(prefetched, cpows);
+
+ try {
+ log("Prefetching on");
+ return func();
+ } finally {
+ // After we return from this event handler, the content process
+ // is free to continue executing, so we invalidate our cache.
+ log("Prefetching off");
+ this.cache = null;
+ }
+ },
+
+ // Called by shim code in the chrome process to check if target.prop
+ // is cached.
+ lookupInCache: function(addon, target, prop) {
+ if (!this.cache || !Cu.isCrossProcessWrapper(target)) {
+ return null;
+ }
+
+ let propMap = this.cache.get(target);
+ if (!propMap) {
+ return null;
+ }
+
+ return propMap.get(prop);
+ },
+};
+
+var AdblockId = "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}";
+var AdblockRules = {
+ "ContentPolicy.shouldLoad": [
+ new MethodOp("Node", "InitNode", "QueryInterface", Ci.nsISupports),
+ new PropertyOp("Document", "Node", "ownerDocument"),
+ new PropertyOp("Window", "Node", "defaultView"),
+ new PropertyOp("Window", "Document", "defaultView"),
+ new PropertyOp("TopWindow", "Window", "top"),
+ new PropertyOp("WindowLocation", "Window", "location"),
+ new PropertyOp(null, "WindowLocation", "href"),
+ new PropertyOp("Window", "Window", "parent"),
+ new PropertyOp(null, "Window", "name"),
+ new PropertyOp("Document", "Window", "document"),
+ new PropertyOp("TopDocumentElement", "Document", "documentElement"),
+ new MethodOp(null, "TopDocumentElement", "getAttribute", "data-adblockkey"),
+ ]
+};
+PrefetcherRules[AdblockId] = AdblockRules;
+
+var LastpassId = "support@lastpass.com";
+var LastpassRules = {
+ "EventTarget.handleEvent": [
+ new PropertyOp("EventTarget", "Event", "target"),
+ new PropertyOp("EventOriginalTarget", "Event", "originalTarget"),
+ new PropertyOp("Window", "EventOriginalTarget", "defaultView"),
+
+ new CopyOp("Frame", "Window"),
+ new PropertyOp("FrameCollection", "Window", "frames"),
+ new CollectionOp("Frame", "FrameCollection"),
+ new PropertyOp("FrameCollection", "Frame", "frames"),
+ new PropertyOp("FrameDocument", "Frame", "document"),
+ new PropertyOp(null, "Frame", "window"),
+ new PropertyOp(null, "FrameDocument", "defaultView"),
+
+ new PropertyOp("FrameDocumentLocation", "FrameDocument", "location"),
+ new PropertyOp(null, "FrameDocumentLocation", "href"),
+ new PropertyOp("FrameLocation", "Frame", "location"),
+ new PropertyOp(null, "FrameLocation", "href"),
+
+ new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "form"),
+ new MethodOp("FormCollection", "FrameDocument", "getElementsByTagName", "FORM"),
+ new CollectionOp("Form", "FormCollection"),
+ new PropertyOp("FormElementCollection", "Form", "elements"),
+ new CollectionOp("FormElement", "FormElementCollection"),
+ new PropertyOp("Style", "Form", "style"),
+
+ new PropertyOp(null, "FormElement", "type"),
+ new PropertyOp(null, "FormElement", "name"),
+ new PropertyOp(null, "FormElement", "value"),
+ new PropertyOp(null, "FormElement", "tagName"),
+ new PropertyOp(null, "FormElement", "id"),
+ new PropertyOp("Style", "FormElement", "style"),
+
+ new PropertyOp(null, "Style", "visibility"),
+
+ new MethodOp("MetaElementsCollection", "EventOriginalTarget", "getElementsByTagName", "meta"),
+ new CollectionOp("MetaElement", "MetaElementsCollection"),
+ new PropertyOp(null, "MetaElement", "httpEquiv"),
+
+ new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "input"),
+ new MethodOp("InputElementCollection", "FrameDocument", "getElementsByTagName", "INPUT"),
+ new CollectionOp("InputElement", "InputElementCollection"),
+ new PropertyOp(null, "InputElement", "type"),
+ new PropertyOp(null, "InputElement", "name"),
+ new PropertyOp(null, "InputElement", "tagName"),
+ new PropertyOp(null, "InputElement", "form"),
+
+ new PropertyOp("BodyElement", "FrameDocument", "body"),
+ new PropertyOp("BodyInnerText", "BodyElement", "innerText"),
+
+ new PropertyOp("DocumentFormCollection", "FrameDocument", "forms"),
+ new CollectionOp("DocumentForm", "DocumentFormCollection"),
+ ]
+};
+PrefetcherRules[LastpassId] = LastpassRules;
diff --git a/toolkit/components/addoncompat/RemoteAddonsChild.jsm b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
new file mode 100644
index 0000000000..1aacc7f7ab
--- /dev/null
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -0,0 +1,576 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.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.EXPORTED_SYMBOLS = ["RemoteAddonsChild"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+ "resource://gre/modules/Prefetcher.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "SystemPrincipal",
+ "@mozilla.org/systemprincipal;1", "nsIPrincipal");
+
+XPCOMUtils.defineLazyServiceGetter(this, "contentSecManager",
+ "@mozilla.org/contentsecuritymanager;1",
+ "nsIContentSecurityManager");
+
+// Similar to Python. Returns dict[key] if it exists. Otherwise,
+// sets dict[key] to default_ and returns default_.
+function setDefault(dict, key, default_)
+{
+ if (key in dict) {
+ return dict[key];
+ }
+ dict[key] = default_;
+ return default_;
+}
+
+// This code keeps track of a set of paths of the form [component_1,
+// ..., component_n]. The components can be strings or booleans. The
+// child is notified whenever a path is added or removed, and new
+// children can request the current set of paths. The purpose is to
+// keep track of all the observers and events that the child should
+// monitor for the parent.
+//
+// In the child, clients can watch for changes to all paths that start
+// with a given component.
+var NotificationTracker = {
+ init: function() {
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ cpmm.addMessageListener("Addons:ChangeNotification", this);
+ this._paths = cpmm.initialProcessData.remoteAddonsNotificationPaths;
+ this._registered = new Map();
+ this._watchers = {};
+ },
+
+ receiveMessage: function(msg) {
+ let path = msg.data.path;
+ let count = msg.data.count;
+
+ let tracked = this._paths;
+ for (let component of path) {
+ tracked = setDefault(tracked, component, {});
+ }
+
+ tracked._count = count;
+
+ if (this._watchers[path[0]]) {
+ for (let watcher of this._watchers[path[0]]) {
+ this.runCallback(watcher, path, count);
+ }
+ }
+ },
+
+ runCallback: function(watcher, path, count) {
+ let pathString = path.join("/");
+ let registeredSet = this._registered.get(watcher);
+ let registered = registeredSet.has(pathString);
+ if (count && !registered) {
+ watcher.track(path, true);
+ registeredSet.add(pathString);
+ } else if (!count && registered) {
+ watcher.track(path, false);
+ registeredSet.delete(pathString);
+ }
+ },
+
+ findPaths: function(prefix) {
+ if (!this._paths) {
+ return [];
+ }
+
+ let tracked = this._paths;
+ for (let component of prefix) {
+ tracked = setDefault(tracked, component, {});
+ }
+
+ let result = [];
+ let enumerate = (tracked, curPath) => {
+ for (let component in tracked) {
+ if (component == "_count") {
+ result.push([curPath, tracked._count]);
+ } else {
+ let path = curPath.slice();
+ if (component === "true") {
+ component = true;
+ } else if (component === "false") {
+ component = false;
+ }
+ path.push(component);
+ enumerate(tracked[component], path);
+ }
+ }
+ }
+ enumerate(tracked, prefix);
+
+ return result;
+ },
+
+ findSuffixes: function(prefix) {
+ let paths = this.findPaths(prefix);
+ return paths.map(([path, count]) => path[path.length - 1]);
+ },
+
+ watch: function(component1, watcher) {
+ setDefault(this._watchers, component1, []).push(watcher);
+ this._registered.set(watcher, new Set());
+
+ let paths = this.findPaths([component1]);
+ for (let [path, count] of paths) {
+ this.runCallback(watcher, path, count);
+ }
+ },
+
+ unwatch: function(component1, watcher) {
+ let watchers = this._watchers[component1];
+ let index = watchers.lastIndexOf(watcher);
+ if (index > -1) {
+ watchers.splice(index, 1);
+ }
+
+ this._registered.delete(watcher);
+ },
+
+ getCount(component1) {
+ return this.findPaths([component1]).length;
+ },
+};
+
+// This code registers an nsIContentPolicy in the child process. When
+// it runs, it notifies the parent that it needs to run its own
+// nsIContentPolicy list. If any policy in the parent rejects a
+// resource load, that answer is returned to the child.
+var ContentPolicyChild = {
+ _classDescription: "Addon shim content policy",
+ _classID: Components.ID("6e869130-635c-11e2-bcfd-0800200c9a66"),
+ _contractID: "@mozilla.org/addon-child/policy;1",
+
+ init: function() {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(this._classID, this._classDescription, this._contractID, this);
+
+ NotificationTracker.watch("content-policy", this);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
+ Ci.nsIChannelEventSink, Ci.nsIFactory,
+ Ci.nsISupportsWeakReference]),
+
+ track: function(path, register) {
+ let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
+ if (register) {
+ catMan.addCategoryEntry("content-policy", this._contractID, this._contractID, false, true);
+ } else {
+ catMan.deleteCategoryEntry("content-policy", this._contractID, false);
+ }
+ },
+
+ shouldLoad: function(contentType, contentLocation, requestOrigin,
+ node, mimeTypeGuess, extra, requestPrincipal) {
+ let addons = NotificationTracker.findSuffixes(["content-policy"]);
+ let [prefetched, cpows] = Prefetcher.prefetch("ContentPolicy.shouldLoad",
+ addons, {InitNode: node});
+ cpows.node = node;
+
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ let rval = cpmm.sendRpcMessage("Addons:ContentPolicy:Run", {
+ contentType: contentType,
+ contentLocation: contentLocation.spec,
+ requestOrigin: requestOrigin ? requestOrigin.spec : null,
+ mimeTypeGuess: mimeTypeGuess,
+ requestPrincipal: requestPrincipal,
+ prefetched: prefetched,
+ }, cpows);
+ if (rval.length != 1) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ return rval[0];
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ createInstance: function(outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return this.QueryInterface(iid);
+ },
+};
+
+// This is a shim channel whose only purpose is to return some string
+// data from an about: protocol handler.
+function AboutProtocolChannel(uri, contractID, loadInfo)
+{
+ this.URI = uri;
+ this.originalURI = uri;
+ this._contractID = contractID;
+ this._loadingPrincipal = loadInfo.loadingPrincipal;
+ this._securityFlags = loadInfo.securityFlags;
+ this._contentPolicyType = loadInfo.externalContentPolicyType;
+}
+
+AboutProtocolChannel.prototype = {
+ contentCharset: "utf-8",
+ contentLength: 0,
+ owner: SystemPrincipal,
+ securityInfo: null,
+ notificationCallbacks: null,
+ loadFlags: 0,
+ loadGroup: null,
+ name: null,
+ status: Cr.NS_OK,
+
+ asyncOpen: function(listener, context) {
+ // Ask the parent to synchronously read all the data from the channel.
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:OpenChannel", {
+ uri: this.URI.spec,
+ contractID: this._contractID,
+ loadingPrincipal: this._loadingPrincipal,
+ securityFlags: this._securityFlags,
+ contentPolicyType: this._contentPolicyType
+ }, {
+ notificationCallbacks: this.notificationCallbacks,
+ loadGroupNotificationCallbacks: this.loadGroup ? this.loadGroup.notificationCallbacks : null,
+ });
+
+ if (rval.length != 1) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ let {data, contentType} = rval[0];
+ this.contentType = contentType;
+
+ // Return the data via an nsIStringInputStream.
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+
+ let runnable = {
+ run: () => {
+ try {
+ listener.onStartRequest(this, context);
+ } catch (e) {}
+ try {
+ listener.onDataAvailable(this, context, stream, 0, stream.available());
+ } catch (e) {}
+ try {
+ listener.onStopRequest(this, context, Cr.NS_OK);
+ } catch (e) {}
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ },
+
+ asyncOpen2: function(listener) {
+ // throws an error if security checks fail
+ var outListener = contentSecManager.performSecurityCheck(this, listener);
+ this.asyncOpen(outListener, null);
+ },
+
+ open: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ open2: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ isPending: function() {
+ return false;
+ },
+
+ cancel: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ suspend: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ resume: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+};
+
+// This shim protocol handler is used when content fetches an about: URL.
+function AboutProtocolInstance(contractID)
+{
+ this._contractID = contractID;
+ this._uriFlags = undefined;
+}
+
+AboutProtocolInstance.prototype = {
+ createInstance: function(outer, iid) {
+ if (outer != null) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+
+ return this.QueryInterface(iid);
+ },
+
+ getURIFlags: function(uri) {
+ // Cache the result to avoid the extra IPC.
+ if (this._uriFlags !== undefined) {
+ return this._uriFlags;
+ }
+
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+
+ let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:GetURIFlags", {
+ uri: uri.spec,
+ contractID: this._contractID
+ });
+
+ if (rval.length != 1) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ this._uriFlags = rval[0];
+ return this._uriFlags;
+ },
+
+ // We take some shortcuts here. Ideally, we would return a CPOW that
+ // wraps the add-on's nsIChannel. However, many of the methods
+ // related to nsIChannel are marked [noscript], so they're not
+ // available to CPOWs. Consequently, we return a shim channel that,
+ // when opened, asks the parent to open the channel and read out all
+ // the data.
+ newChannel: function(uri, loadInfo) {
+ return new AboutProtocolChannel(uri, this._contractID, loadInfo);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
+};
+
+var AboutProtocolChild = {
+ _classDescription: "Addon shim about: protocol handler",
+
+ init: function() {
+ // Maps contractIDs to instances
+ this._instances = new Map();
+ // Maps contractIDs to classIDs
+ this._classIDs = new Map();
+ NotificationTracker.watch("about-protocol", this);
+ },
+
+ track: function(path, register) {
+ let contractID = path[1];
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (register) {
+ let instance = new AboutProtocolInstance(contractID);
+ let classID = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+
+ this._instances.set(contractID, instance);
+ this._classIDs.set(contractID, classID);
+ registrar.registerFactory(classID, this._classDescription, contractID, instance);
+ } else {
+ let instance = this._instances.get(contractID);
+ let classID = this._classIDs.get(contractID);
+ registrar.unregisterFactory(classID, instance);
+ this._instances.delete(contractID);
+ this._classIDs.delete(contractID);
+ }
+ },
+};
+
+// This code registers observers in the child whenever an add-on in
+// the parent asks for notifications on the given topic.
+var ObserverChild = {
+ init: function() {
+ NotificationTracker.watch("observer", this);
+ },
+
+ track: function(path, register) {
+ let topic = path[1];
+ if (register) {
+ Services.obs.addObserver(this, topic, false);
+ } else {
+ Services.obs.removeObserver(this, topic);
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsISyncMessageSender);
+ cpmm.sendRpcMessage("Addons:Observer:Run", {}, {
+ topic: topic,
+ subject: subject,
+ data: data
+ });
+ }
+};
+
+// There is one of these objects per browser tab in the child. When an
+// add-on in the parent listens for an event, this child object
+// listens for that event in the child.
+function EventTargetChild(childGlobal)
+{
+ this._childGlobal = childGlobal;
+ this.capturingHandler = (event) => this.handleEvent(true, event);
+ this.nonCapturingHandler = (event) => this.handleEvent(false, event);
+ NotificationTracker.watch("event", this);
+}
+
+EventTargetChild.prototype = {
+ uninit: function() {
+ NotificationTracker.unwatch("event", this);
+ },
+
+ track: function(path, register) {
+ let eventType = path[1];
+ let useCapture = path[2];
+ let listener = useCapture ? this.capturingHandler : this.nonCapturingHandler;
+ if (register) {
+ this._childGlobal.addEventListener(eventType, listener, useCapture, true);
+ } else {
+ this._childGlobal.removeEventListener(eventType, listener, useCapture);
+ }
+ },
+
+ handleEvent: function(capturing, event) {
+ let addons = NotificationTracker.findSuffixes(["event", event.type, capturing]);
+ let [prefetched, cpows] = Prefetcher.prefetch("EventTarget.handleEvent",
+ addons,
+ {Event: event,
+ Window: this._childGlobal.content});
+ cpows.event = event;
+ cpows.eventTarget = event.target;
+
+ this._childGlobal.sendRpcMessage("Addons:Event:Run",
+ {type: event.type,
+ capturing: capturing,
+ isTrusted: event.isTrusted,
+ prefetched: prefetched},
+ cpows);
+ }
+};
+
+// The parent can create a sandbox to run code in the child
+// process. We actually create the sandbox in the child so that the
+// code runs there. However, managing the lifetime of these sandboxes
+// can be tricky. The parent references these sandboxes using CPOWs,
+// which only keep weak references. So we need to create a strong
+// reference in the child. For simplicity, we kill off these strong
+// references whenever we navigate away from the page for which the
+// sandbox was created.
+function SandboxChild(chromeGlobal)
+{
+ this.chromeGlobal = chromeGlobal;
+ this.sandboxes = [];
+}
+
+SandboxChild.prototype = {
+ uninit: function() {
+ this.clearSandboxes();
+ },
+
+ addListener: function() {
+ let webProgress = this.chromeGlobal.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+ },
+
+ removeListener: function() {
+ let webProgress = this.chromeGlobal.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(this);
+ },
+
+ onLocationChange: function(webProgress, request, location, flags) {
+ this.clearSandboxes();
+ },
+
+ addSandbox: function(sandbox) {
+ if (this.sandboxes.length == 0) {
+ this.addListener();
+ }
+ this.sandboxes.push(sandbox);
+ },
+
+ clearSandboxes: function() {
+ if (this.sandboxes.length) {
+ this.removeListener();
+ }
+ this.sandboxes = [];
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+};
+
+var RemoteAddonsChild = {
+ _ready: false,
+
+ makeReady: function() {
+ let shims = [
+ Prefetcher,
+ NotificationTracker,
+ ContentPolicyChild,
+ AboutProtocolChild,
+ ObserverChild,
+ ];
+
+ for (let shim of shims) {
+ try {
+ shim.init();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ init: function(global) {
+
+ if (!this._ready) {
+ if (!Services.cpmm.initialProcessData.remoteAddonsParentInitted) {
+ return null;
+ }
+
+ this.makeReady();
+ this._ready = true;
+ }
+
+ global.sendAsyncMessage("Addons:RegisterGlobal", {}, {global: global});
+
+ let sandboxChild = new SandboxChild(global);
+ global.addSandbox = sandboxChild.addSandbox.bind(sandboxChild);
+
+ // Return this so it gets rooted in the content script.
+ return [new EventTargetChild(global), sandboxChild];
+ },
+
+ uninit: function(perTabShims) {
+ for (let shim of perTabShims) {
+ try {
+ shim.uninit();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ get useSyncWebProgress() {
+ return NotificationTracker.getCount("web-progress") > 0;
+ },
+};
diff --git a/toolkit/components/addoncompat/RemoteAddonsParent.jsm b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
new file mode 100644
index 0000000000..5cadc2902b
--- /dev/null
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -0,0 +1,1080 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.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.EXPORTED_SYMBOLS = ["RemoteAddonsParent"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/RemoteWebProgress.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+ "resource://gre/modules/Prefetcher.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CompatWarning",
+ "resource://gre/modules/CompatWarning.jsm");
+
+Cu.permitCPOWsInScope(this);
+
+// Similar to Python. Returns dict[key] if it exists. Otherwise,
+// sets dict[key] to default_ and returns default_.
+function setDefault(dict, key, default_)
+{
+ if (key in dict) {
+ return dict[key];
+ }
+ dict[key] = default_;
+ return default_;
+}
+
+// This code keeps track of a set of paths of the form [component_1,
+// ..., component_n]. The components can be strings or booleans. The
+// child is notified whenever a path is added or removed, and new
+// children can request the current set of paths. The purpose is to
+// keep track of all the observers and events that the child should
+// monitor for the parent.
+var NotificationTracker = {
+ // _paths is a multi-level dictionary. Let's add paths [A, B] and
+ // [A, C]. Then _paths will look like this:
+ // { 'A': { 'B': { '_count': 1 }, 'C': { '_count': 1 } } }
+ // Each component in a path will be a key in some dictionary. At the
+ // end, the _count property keeps track of how many instances of the
+ // given path are present in _paths.
+ _paths: {},
+
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.initialProcessData.remoteAddonsNotificationPaths = this._paths;
+ },
+
+ add: function(path) {
+ let tracked = this._paths;
+ for (let component of path) {
+ tracked = setDefault(tracked, component, {});
+ }
+ let count = tracked._count || 0;
+ count++;
+ tracked._count = count;
+
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.broadcastAsyncMessage("Addons:ChangeNotification", {path: path, count: count});
+ },
+
+ remove: function(path) {
+ let tracked = this._paths;
+ for (let component of path) {
+ tracked = setDefault(tracked, component, {});
+ }
+ tracked._count--;
+
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.broadcastAsyncMessage("Addons:ChangeNotification", {path: path, count: tracked._count});
+ },
+};
+NotificationTracker.init();
+
+// An interposition is an object with three properties: methods,
+// getters, and setters. See multiprocessShims.js for an explanation
+// of how these are used. The constructor here just allows one
+// interposition to inherit members from another.
+function Interposition(name, base)
+{
+ this.name = name;
+ if (base) {
+ this.methods = Object.create(base.methods);
+ this.getters = Object.create(base.getters);
+ this.setters = Object.create(base.setters);
+ } else {
+ this.methods = Object.create(null);
+ this.getters = Object.create(null);
+ this.setters = Object.create(null);
+ }
+}
+
+// This object is responsible for notifying the child when a new
+// content policy is added or removed. It also runs all the registered
+// add-on content policies when the child asks it to do so.
+var ContentPolicyParent = {
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("Addons:ContentPolicy:Run", this);
+
+ this._policies = new Map();
+ },
+
+ addContentPolicy: function(addon, name, cid) {
+ this._policies.set(name, cid);
+ NotificationTracker.add(["content-policy", addon]);
+ },
+
+ removeContentPolicy: function(addon, name) {
+ this._policies.delete(name);
+ NotificationTracker.remove(["content-policy", addon]);
+ },
+
+ receiveMessage: function (aMessage) {
+ switch (aMessage.name) {
+ case "Addons:ContentPolicy:Run":
+ return this.shouldLoad(aMessage.data, aMessage.objects);
+ }
+ return undefined;
+ },
+
+ shouldLoad: function(aData, aObjects) {
+ for (let policyCID of this._policies.values()) {
+ let policy;
+ try {
+ policy = Cc[policyCID].getService(Ci.nsIContentPolicy);
+ } catch (e) {
+ // Current Gecko behavior is to ignore entries that don't QI.
+ continue;
+ }
+ try {
+ let contentLocation = BrowserUtils.makeURI(aData.contentLocation);
+ let requestOrigin = aData.requestOrigin ? BrowserUtils.makeURI(aData.requestOrigin) : null;
+
+ let result = Prefetcher.withPrefetching(aData.prefetched, aObjects, () => {
+ return policy.shouldLoad(aData.contentType,
+ contentLocation,
+ requestOrigin,
+ aObjects.node,
+ aData.mimeTypeGuess,
+ null,
+ aData.requestPrincipal);
+ });
+ if (result != Ci.nsIContentPolicy.ACCEPT && result != 0)
+ return result;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+};
+ContentPolicyParent.init();
+
+// This interposition intercepts calls to add or remove new content
+// policies and forwards these requests to ContentPolicyParent.
+var CategoryManagerInterposition = new Interposition("CategoryManagerInterposition");
+
+CategoryManagerInterposition.methods.addCategoryEntry =
+ function(addon, target, category, entry, value, persist, replace) {
+ if (category == "content-policy") {
+ CompatWarning.warn("content-policy should be added from the child process only.",
+ addon, CompatWarning.warnings.nsIContentPolicy);
+ ContentPolicyParent.addContentPolicy(addon, entry, value);
+ }
+
+ target.addCategoryEntry(category, entry, value, persist, replace);
+ };
+
+CategoryManagerInterposition.methods.deleteCategoryEntry =
+ function(addon, target, category, entry, persist) {
+ if (category == "content-policy") {
+ CompatWarning.warn("content-policy should be removed from the child process only.",
+ addon, CompatWarning.warnings.nsIContentPolicy);
+ ContentPolicyParent.removeContentPolicy(addon, entry);
+ }
+
+ target.deleteCategoryEntry(category, entry, persist);
+ };
+
+// This shim handles the case where an add-on registers an about:
+// protocol handler in the parent and we want the child to be able to
+// use it. This code is pretty specific to Adblock's usage.
+var AboutProtocolParent = {
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("Addons:AboutProtocol:GetURIFlags", this);
+ ppmm.addMessageListener("Addons:AboutProtocol:OpenChannel", this);
+ this._protocols = [];
+ },
+
+ registerFactory: function(addon, class_, className, contractID, factory) {
+ this._protocols.push({contractID: contractID, factory: factory});
+ NotificationTracker.add(["about-protocol", contractID, addon]);
+ },
+
+ unregisterFactory: function(addon, class_, factory) {
+ for (let i = 0; i < this._protocols.length; i++) {
+ if (this._protocols[i].factory == factory) {
+ NotificationTracker.remove(["about-protocol", this._protocols[i].contractID, addon]);
+ this._protocols.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function (msg) {
+ switch (msg.name) {
+ case "Addons:AboutProtocol:GetURIFlags":
+ return this.getURIFlags(msg);
+ case "Addons:AboutProtocol:OpenChannel":
+ return this.openChannel(msg);
+ }
+ return undefined;
+ },
+
+ getURIFlags: function(msg) {
+ let uri = BrowserUtils.makeURI(msg.data.uri);
+ let contractID = msg.data.contractID;
+ let module = Cc[contractID].getService(Ci.nsIAboutModule);
+ try {
+ return module.getURIFlags(uri);
+ } catch (e) {
+ Cu.reportError(e);
+ return undefined;
+ }
+ },
+
+ // We immediately read all the data out of the channel here and
+ // return it to the child.
+ openChannel: function(msg) {
+ function wrapGetInterface(cpow) {
+ return {
+ getInterface: function(intf) { return cpow.getInterface(intf); }
+ };
+ }
+
+ let uri = BrowserUtils.makeURI(msg.data.uri);
+ let channelParams;
+ if (msg.data.contentPolicyType === Ci.nsIContentPolicy.TYPE_DOCUMENT) {
+ // For TYPE_DOCUMENT loads, we cannot recreate the loadinfo here in the
+ // parent. In that case, treat this as a chrome (addon)-requested
+ // subload. When we use the data in the child, we'll load it into the
+ // correctly-principaled document.
+ channelParams = {
+ uri,
+ contractID: msg.data.contractID,
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
+ };
+ } else {
+ // We can recreate the loadinfo here in the parent for non TYPE_DOCUMENT
+ // loads.
+ channelParams = {
+ uri,
+ contractID: msg.data.contractID,
+ loadingPrincipal: msg.data.loadingPrincipal,
+ securityFlags: msg.data.securityFlags,
+ contentPolicyType: msg.data.contentPolicyType
+ };
+ }
+
+ try {
+ let channel = NetUtil.newChannel(channelParams);
+
+ // We're not allowed to set channel.notificationCallbacks to a
+ // CPOW, since the setter for notificationCallbacks is in C++,
+ // which can't tolerate CPOWs. Instead we just use a JS object
+ // that wraps the CPOW.
+ channel.notificationCallbacks = wrapGetInterface(msg.objects.notificationCallbacks);
+ if (msg.objects.loadGroupNotificationCallbacks) {
+ channel.loadGroup = {notificationCallbacks: msg.objects.loadGroupNotificationCallbacks};
+ } else {
+ channel.loadGroup = null;
+ }
+ let stream = channel.open2();
+ let data = NetUtil.readInputStreamToString(stream, stream.available(), {});
+ return {
+ data: data,
+ contentType: channel.contentType
+ };
+ } catch (e) {
+ Cu.reportError(e);
+ return undefined;
+ }
+ },
+};
+AboutProtocolParent.init();
+
+var ComponentRegistrarInterposition = new Interposition("ComponentRegistrarInterposition");
+
+ComponentRegistrarInterposition.methods.registerFactory =
+ function(addon, target, class_, className, contractID, factory) {
+ if (contractID && contractID.startsWith("@mozilla.org/network/protocol/about;1?")) {
+ CompatWarning.warn("nsIAboutModule should be registered in the content process" +
+ " as well as the chrome process. (If you do that already, ignore" +
+ " this warning.)",
+ addon, CompatWarning.warnings.nsIAboutModule);
+ AboutProtocolParent.registerFactory(addon, class_, className, contractID, factory);
+ }
+
+ target.registerFactory(class_, className, contractID, factory);
+ };
+
+ComponentRegistrarInterposition.methods.unregisterFactory =
+ function(addon, target, class_, factory) {
+ AboutProtocolParent.unregisterFactory(addon, class_, factory);
+ target.unregisterFactory(class_, factory);
+ };
+
+// This object manages add-on observers that might fire in the child
+// process. Rather than managing the observers itself, it uses the
+// parent's observer service. When an add-on listens on topic T,
+// ObserverParent asks the child process to listen on T. It also adds
+// an observer in the parent for the topic e10s-T. When the T observer
+// fires in the child, the parent fires all the e10s-T observers,
+// passing them CPOWs for the subject and data. We don't want to use T
+// in the parent because there might be non-add-on T observers that
+// won't expect to get notified in this case.
+var ObserverParent = {
+ init: function() {
+ let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+ ppmm.addMessageListener("Addons:Observer:Run", this);
+ },
+
+ addObserver: function(addon, observer, topic, ownsWeak) {
+ Services.obs.addObserver(observer, "e10s-" + topic, ownsWeak);
+ NotificationTracker.add(["observer", topic, addon]);
+ },
+
+ removeObserver: function(addon, observer, topic) {
+ Services.obs.removeObserver(observer, "e10s-" + topic);
+ NotificationTracker.remove(["observer", topic, addon]);
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Addons:Observer:Run":
+ this.notify(msg.objects.subject, msg.objects.topic, msg.objects.data);
+ break;
+ }
+ },
+
+ notify: function(subject, topic, data) {
+ let e = Services.obs.enumerateObservers("e10s-" + topic);
+ while (e.hasMoreElements()) {
+ let obs = e.getNext().QueryInterface(Ci.nsIObserver);
+ try {
+ obs.observe(subject, topic, data);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+};
+ObserverParent.init();
+
+// We only forward observers for these topics.
+var TOPIC_WHITELIST = [
+ "content-document-global-created",
+ "document-element-inserted",
+ "dom-window-destroyed",
+ "inner-window-destroyed",
+ "outer-window-destroyed",
+ "csp-on-violate-policy",
+];
+
+// This interposition listens for
+// nsIObserverService.{add,remove}Observer.
+var ObserverInterposition = new Interposition("ObserverInterposition");
+
+ObserverInterposition.methods.addObserver =
+ function(addon, target, observer, topic, ownsWeak) {
+ if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
+ CompatWarning.warn(`${topic} observer should be added from the child process only.`,
+ addon, CompatWarning.warnings.observers);
+
+ ObserverParent.addObserver(addon, observer, topic);
+ }
+
+ target.addObserver(observer, topic, ownsWeak);
+ };
+
+ObserverInterposition.methods.removeObserver =
+ function(addon, target, observer, topic) {
+ if (TOPIC_WHITELIST.indexOf(topic) >= 0) {
+ ObserverParent.removeObserver(addon, observer, topic);
+ }
+
+ target.removeObserver(observer, topic);
+ };
+
+// This object is responsible for forwarding events from the child to
+// the parent.
+var EventTargetParent = {
+ init: function() {
+ // The _listeners map goes from targets (either <browser> elements
+ // or windows) to a dictionary from event types to listeners.
+ this._listeners = new WeakMap();
+
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"].
+ getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("Addons:Event:Run", this);
+ },
+
+ // If target is not on the path from a <browser> element to the
+ // window root, then we return null here to ignore the
+ // target. Otherwise, if the target is a browser-specific element
+ // (the <browser> or <tab> elements), then we return the
+ // <browser>. If it's some generic element, then we return the
+ // window itself.
+ redirectEventTarget: function(target) {
+ if (Cu.isCrossProcessWrapper(target)) {
+ return null;
+ }
+
+ if (target instanceof Ci.nsIDOMChromeWindow) {
+ return target;
+ }
+
+ if (target instanceof Ci.nsIDOMXULElement) {
+ if (target.localName == "browser") {
+ return target;
+ } else if (target.localName == "tab") {
+ return target.linkedBrowser;
+ }
+
+ // Check if |target| is somewhere on the patch from the
+ // <tabbrowser> up to the root element.
+ let window = target.ownerDocument.defaultView;
+ if (window && target.contains(window.gBrowser)) {
+ return window;
+ }
+ }
+
+ return null;
+ },
+
+ // When a given event fires in the child, we fire it on the
+ // <browser> element and the window since those are the two possible
+ // results of redirectEventTarget.
+ getTargets: function(browser) {
+ let window = browser.ownerDocument.defaultView;
+ return [browser, window];
+ },
+
+ addEventListener: function(addon, target, type, listener, useCapture, wantsUntrusted, delayedWarning) {
+ let newTarget = this.redirectEventTarget(target);
+ if (!newTarget) {
+ return;
+ }
+
+ useCapture = useCapture || false;
+ wantsUntrusted = wantsUntrusted || false;
+
+ NotificationTracker.add(["event", type, useCapture, addon]);
+
+ let listeners = this._listeners.get(newTarget);
+ if (!listeners) {
+ listeners = {};
+ this._listeners.set(newTarget, listeners);
+ }
+ let forType = setDefault(listeners, type, []);
+
+ // If there's already an identical listener, don't do anything.
+ for (let i = 0; i < forType.length; i++) {
+ if (forType[i].listener === listener &&
+ forType[i].target === target &&
+ forType[i].useCapture === useCapture &&
+ forType[i].wantsUntrusted === wantsUntrusted) {
+ return;
+ }
+ }
+
+ forType.push({listener: listener,
+ target: target,
+ wantsUntrusted: wantsUntrusted,
+ useCapture: useCapture,
+ delayedWarning: delayedWarning});
+ },
+
+ removeEventListener: function(addon, target, type, listener, useCapture) {
+ let newTarget = this.redirectEventTarget(target);
+ if (!newTarget) {
+ return;
+ }
+
+ useCapture = useCapture || false;
+
+ let listeners = this._listeners.get(newTarget);
+ if (!listeners) {
+ return;
+ }
+ let forType = setDefault(listeners, type, []);
+
+ for (let i = 0; i < forType.length; i++) {
+ if (forType[i].listener === listener &&
+ forType[i].target === target &&
+ forType[i].useCapture === useCapture) {
+ forType.splice(i, 1);
+ NotificationTracker.remove(["event", type, useCapture, addon]);
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Addons:Event:Run":
+ this.dispatch(msg.target, msg.data.type, msg.data.capturing,
+ msg.data.isTrusted, msg.data.prefetched, msg.objects);
+ break;
+ }
+ },
+
+ dispatch: function(browser, type, capturing, isTrusted, prefetched, cpows) {
+ let event = cpows.event;
+ let eventTarget = cpows.eventTarget;
+ let targets = this.getTargets(browser);
+ for (let target of targets) {
+ let listeners = this._listeners.get(target);
+ if (!listeners) {
+ continue;
+ }
+ let forType = setDefault(listeners, type, []);
+
+ // Make a copy in case they call removeEventListener in the listener.
+ let handlers = [];
+ for (let {listener, target, wantsUntrusted, useCapture, delayedWarning} of forType) {
+ if ((wantsUntrusted || isTrusted) && useCapture == capturing) {
+ // Issue a warning for this listener.
+ delayedWarning();
+
+ handlers.push([listener, target]);
+ }
+ }
+
+ for (let [handler, target] of handlers) {
+ let EventProxy = {
+ get: function(knownProps, name) {
+ if (knownProps.hasOwnProperty(name))
+ return knownProps[name];
+ return event[name];
+ }
+ }
+ let proxyEvent = new Proxy({
+ currentTarget: target,
+ target: eventTarget,
+ type: type,
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIDOMEventTarget))
+ return proxyEvent;
+ // If event deson't support the interface this will throw. If it
+ // does we want to return the proxy
+ event.QueryInterface(iid);
+ return proxyEvent;
+ }
+ }, EventProxy);
+
+ try {
+ Prefetcher.withPrefetching(prefetched, cpows, () => {
+ if ("handleEvent" in handler) {
+ handler.handleEvent(proxyEvent);
+ } else {
+ handler.call(eventTarget, proxyEvent);
+ }
+ });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+};
+EventTargetParent.init();
+
+// This function returns a listener that will not fire on events where
+// the target is a remote xul:browser element itself. We'd rather let
+// the child process handle the event and pass it up via
+// EventTargetParent.
+var filteringListeners = new WeakMap();
+function makeFilteringListener(eventType, listener)
+{
+ // Some events are actually targeted at the <browser> element
+ // itself, so we only handle the ones where know that won't happen.
+ let eventTypes = ["mousedown", "mouseup", "click"];
+ if (!eventTypes.includes(eventType) || !listener ||
+ (typeof listener != "object" && typeof listener != "function")) {
+ return listener;
+ }
+
+ if (filteringListeners.has(listener)) {
+ return filteringListeners.get(listener);
+ }
+
+ function filter(event) {
+ let target = event.originalTarget;
+ if (target instanceof Ci.nsIDOMXULElement &&
+ target.localName == "browser" &&
+ target.isRemoteBrowser) {
+ return;
+ }
+
+ if ("handleEvent" in listener) {
+ listener.handleEvent(event);
+ } else {
+ listener.call(event.target, event);
+ }
+ }
+ filteringListeners.set(listener, filter);
+ return filter;
+}
+
+// This interposition redirects addEventListener and
+// removeEventListener to EventTargetParent.
+var EventTargetInterposition = new Interposition("EventTargetInterposition");
+
+EventTargetInterposition.methods.addEventListener =
+ function(addon, target, type, listener, useCapture, wantsUntrusted) {
+ let delayed = CompatWarning.delayedWarning(
+ `Registering a ${type} event listener on content DOM nodes` +
+ " needs to happen in the content process.",
+ addon, CompatWarning.warnings.DOM_events);
+
+ EventTargetParent.addEventListener(addon, target, type, listener, useCapture, wantsUntrusted, delayed);
+ target.addEventListener(type, makeFilteringListener(type, listener), useCapture, wantsUntrusted);
+ };
+
+EventTargetInterposition.methods.removeEventListener =
+ function(addon, target, type, listener, useCapture) {
+ EventTargetParent.removeEventListener(addon, target, type, listener, useCapture);
+ target.removeEventListener(type, makeFilteringListener(type, listener), useCapture);
+ };
+
+// This interposition intercepts accesses to |rootTreeItem| on a child
+// process docshell. In the child, each docshell is its own
+// root. However, add-ons expect the root to be the chrome docshell,
+// so we make that happen here.
+var ContentDocShellTreeItemInterposition = new Interposition("ContentDocShellTreeItemInterposition");
+
+ContentDocShellTreeItemInterposition.getters.rootTreeItem =
+ function(addon, target) {
+ // The chrome global in the child.
+ let chromeGlobal = target.rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ // Map it to a <browser> element and window.
+ let browser = RemoteAddonsParent.globalToBrowser.get(chromeGlobal);
+ if (!browser) {
+ // Somehow we have a CPOW from the child, but it hasn't sent us
+ // its global yet. That shouldn't happen, but return null just
+ // in case.
+ return null;
+ }
+
+ let chromeWin = browser.ownerDocument.defaultView;
+
+ // Return that window's docshell.
+ return chromeWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem);
+ };
+
+function chromeGlobalForContentWindow(window)
+{
+ return window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+}
+
+// This object manages sandboxes created with content principals in
+// the parent. We actually create these sandboxes in the child process
+// so that the code loaded into them runs there. The resulting sandbox
+// object is a CPOW. This is primarly useful for Greasemonkey.
+var SandboxParent = {
+ componentsMap: new WeakMap(),
+
+ makeContentSandbox: function(addon, chromeGlobal, principals, ...rest) {
+ CompatWarning.warn("This sandbox should be created from the child process.",
+ addon, CompatWarning.warnings.sandboxes);
+ if (rest.length) {
+ // Do a shallow copy of the options object into the child
+ // process. This way we don't have to access it through a Chrome
+ // object wrapper, which would require __exposedProps__.
+ //
+ // The only object property here is sandboxPrototype. We assume
+ // it's a child process object (since that's what Greasemonkey
+ // does) and leave it alone.
+ let options = rest[0];
+ let optionsCopy = new chromeGlobal.Object();
+ for (let prop in options) {
+ optionsCopy[prop] = options[prop];
+ }
+ rest[0] = optionsCopy;
+ }
+
+ // Make a sandbox in the child.
+ let cu = chromeGlobal.Components.utils;
+ let sandbox = cu.Sandbox(principals, ...rest);
+
+ // We need to save the sandbox in the child so it won't get
+ // GCed. The child will drop this reference at the next
+ // navigation.
+ chromeGlobal.addSandbox(sandbox);
+
+ // The sandbox CPOW will be kept alive by whomever we return it
+ // to. Its lifetime is unrelated to that of the sandbox object in
+ // the child.
+ this.componentsMap.set(sandbox, cu);
+ return sandbox;
+ },
+
+ evalInSandbox: function(code, sandbox, ...rest) {
+ let cu = this.componentsMap.get(sandbox);
+ return cu.evalInSandbox(code, sandbox, ...rest);
+ }
+};
+
+// This interposition redirects calls to Cu.Sandbox and
+// Cu.evalInSandbox to SandboxParent if the principals are content
+// principals.
+var ComponentsUtilsInterposition = new Interposition("ComponentsUtilsInterposition");
+
+ComponentsUtilsInterposition.methods.Sandbox =
+ function(addon, target, principals, ...rest) {
+ // principals can be a window object, a list of window objects, or
+ // something else (a string, for example).
+ if (principals &&
+ typeof(principals) == "object" &&
+ Cu.isCrossProcessWrapper(principals) &&
+ principals instanceof Ci.nsIDOMWindow) {
+ let chromeGlobal = chromeGlobalForContentWindow(principals);
+ return SandboxParent.makeContentSandbox(addon, chromeGlobal, principals, ...rest);
+ } else if (principals &&
+ typeof(principals) == "object" &&
+ "every" in principals &&
+ principals.length &&
+ principals.every(e => e instanceof Ci.nsIDOMWindow && Cu.isCrossProcessWrapper(e))) {
+ let chromeGlobal = chromeGlobalForContentWindow(principals[0]);
+
+ // The principals we pass to the content process must use an
+ // Array object from the content process.
+ let array = new chromeGlobal.Array();
+ for (let i = 0; i < principals.length; i++) {
+ array[i] = principals[i];
+ }
+ return SandboxParent.makeContentSandbox(addon, chromeGlobal, array, ...rest);
+ }
+ return Components.utils.Sandbox(principals, ...rest);
+ };
+
+ComponentsUtilsInterposition.methods.evalInSandbox =
+ function(addon, target, code, sandbox, ...rest) {
+ if (sandbox && Cu.isCrossProcessWrapper(sandbox)) {
+ return SandboxParent.evalInSandbox(code, sandbox, ...rest);
+ }
+ return Components.utils.evalInSandbox(code, sandbox, ...rest);
+ };
+
+// This interposition handles cases where an add-on tries to import a
+// chrome XUL node into a content document. It doesn't actually do the
+// import, which we can't support. It just avoids throwing an
+// exception.
+var ContentDocumentInterposition = new Interposition("ContentDocumentInterposition");
+
+ContentDocumentInterposition.methods.importNode =
+ function(addon, target, node, deep) {
+ if (!Cu.isCrossProcessWrapper(node)) {
+ // Trying to import a node from the parent process into the
+ // child process. We don't support this now. Video Download
+ // Helper does this in domhook-service.js to add a XUL
+ // popupmenu to content.
+ Cu.reportError("Calling contentDocument.importNode on a XUL node is not allowed.");
+ return node;
+ }
+
+ return target.importNode(node, deep);
+ };
+
+// This interposition ensures that calling browser.docShell from an
+// add-on returns a CPOW around the dochell.
+var RemoteBrowserElementInterposition = new Interposition("RemoteBrowserElementInterposition",
+ EventTargetInterposition);
+
+RemoteBrowserElementInterposition.getters.docShell = function(addon, target) {
+ CompatWarning.warn("Direct access to content docshell will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+ let remoteChromeGlobal = RemoteAddonsParent.browserToGlobal.get(target);
+ if (!remoteChromeGlobal) {
+ // We may not have any messages from this tab yet.
+ return null;
+ }
+ return remoteChromeGlobal.docShell;
+};
+
+RemoteBrowserElementInterposition.getters.sessionHistory = function(addon, target) {
+ CompatWarning.warn("Direct access to browser.sessionHistory will no longer " +
+ "work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ return getSessionHistory(target);
+}
+
+// We use this in place of the real browser.contentWindow if we
+// haven't yet received a CPOW for the child process's window. This
+// happens if the tab has just started loading.
+function makeDummyContentWindow(browser) {
+ let dummyContentWindow = {
+ set location(url) {
+ browser.loadURI(url, null, null);
+ },
+ document: {
+ readyState: "loading",
+ location: { href: "about:blank" }
+ },
+ frames: [],
+ };
+ dummyContentWindow.top = dummyContentWindow;
+ dummyContentWindow.document.defaultView = dummyContentWindow;
+ browser._contentWindow = dummyContentWindow;
+ return dummyContentWindow;
+}
+
+RemoteBrowserElementInterposition.getters.contentWindow = function(addon, target) {
+ CompatWarning.warn("Direct access to browser.contentWindow will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ // If we don't have a CPOW yet, just return something we can use for
+ // setting the location. This is useful for tests that create a tab
+ // and immediately set contentWindow.location.
+ if (!target.contentWindowAsCPOW) {
+ CompatWarning.warn("CPOW to the content window does not exist yet, dummy content window is created.");
+ return makeDummyContentWindow(target);
+ }
+ return target.contentWindowAsCPOW;
+};
+
+function getContentDocument(addon, browser)
+{
+ if (!browser.contentWindowAsCPOW) {
+ return makeDummyContentWindow(browser).document;
+ }
+
+ let doc = Prefetcher.lookupInCache(addon, browser.contentWindowAsCPOW, "document");
+ if (doc) {
+ return doc;
+ }
+
+ return browser.contentWindowAsCPOW.document;
+}
+
+function getSessionHistory(browser) {
+ let remoteChromeGlobal = RemoteAddonsParent.browserToGlobal.get(browser);
+ if (!remoteChromeGlobal) {
+ CompatWarning.warn("CPOW for the remote browser docShell hasn't been received yet.");
+ // We may not have any messages from this tab yet.
+ return null;
+ }
+ return remoteChromeGlobal.docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
+}
+
+RemoteBrowserElementInterposition.getters.contentDocument = function(addon, target) {
+ CompatWarning.warn("Direct access to browser.contentDocument will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ return getContentDocument(addon, target);
+};
+
+var TabBrowserElementInterposition = new Interposition("TabBrowserElementInterposition",
+ EventTargetInterposition);
+
+TabBrowserElementInterposition.getters.contentWindow = function(addon, target) {
+ CompatWarning.warn("Direct access to gBrowser.contentWindow will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ if (!target.selectedBrowser.contentWindowAsCPOW) {
+ return makeDummyContentWindow(target.selectedBrowser);
+ }
+ return target.selectedBrowser.contentWindowAsCPOW;
+};
+
+TabBrowserElementInterposition.getters.contentDocument = function(addon, target) {
+ CompatWarning.warn("Direct access to gBrowser.contentDocument will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ let browser = target.selectedBrowser;
+ return getContentDocument(addon, browser);
+};
+
+TabBrowserElementInterposition.getters.sessionHistory = function(addon, target) {
+ CompatWarning.warn("Direct access to gBrowser.sessionHistory will no " +
+ "longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+ let browser = target.selectedBrowser;
+ if (!browser.isRemoteBrowser) {
+ return browser.sessionHistory;
+ }
+ return getSessionHistory(browser);
+};
+
+// This function returns a wrapper around an
+// nsIWebProgressListener. When the wrapper is invoked, it calls the
+// real listener but passes CPOWs for the nsIWebProgress and
+// nsIRequest arguments.
+var progressListeners = {global: new WeakMap(), tabs: new WeakMap()};
+function wrapProgressListener(kind, listener)
+{
+ if (progressListeners[kind].has(listener)) {
+ return progressListeners[kind].get(listener);
+ }
+
+ let ListenerHandler = {
+ get: function(target, name) {
+ if (name.startsWith("on")) {
+ return function(...args) {
+ listener[name].apply(listener, RemoteWebProgressManager.argumentsForAddonListener(kind, args));
+ };
+ }
+
+ return listener[name];
+ }
+ };
+ let listenerProxy = new Proxy(listener, ListenerHandler);
+
+ progressListeners[kind].set(listener, listenerProxy);
+ return listenerProxy;
+}
+
+TabBrowserElementInterposition.methods.addProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.addProgressListener(listener);
+ }
+
+ NotificationTracker.add(["web-progress", addon]);
+ return target.addProgressListener(wrapProgressListener("global", listener));
+};
+
+TabBrowserElementInterposition.methods.removeProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.removeProgressListener(listener);
+ }
+
+ NotificationTracker.remove(["web-progress", addon]);
+ return target.removeProgressListener(wrapProgressListener("global", listener));
+};
+
+TabBrowserElementInterposition.methods.addTabsProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.addTabsProgressListener(listener);
+ }
+
+ NotificationTracker.add(["web-progress", addon]);
+ return target.addTabsProgressListener(wrapProgressListener("tabs", listener));
+};
+
+TabBrowserElementInterposition.methods.removeTabsProgressListener = function(addon, target, listener) {
+ if (!target.ownerDocument.defaultView.gMultiProcessBrowser) {
+ return target.removeTabsProgressListener(listener);
+ }
+
+ NotificationTracker.remove(["web-progress", addon]);
+ return target.removeTabsProgressListener(wrapProgressListener("tabs", listener));
+};
+
+var ChromeWindowInterposition = new Interposition("ChromeWindowInterposition",
+ EventTargetInterposition);
+
+// _content is for older add-ons like pinboard and all-in-one gestures
+// that should be using content instead.
+ChromeWindowInterposition.getters.content =
+ChromeWindowInterposition.getters._content = function(addon, target) {
+ CompatWarning.warn("Direct access to chromeWindow.content will no longer work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ let browser = target.gBrowser.selectedBrowser;
+ if (!browser.contentWindowAsCPOW) {
+ return makeDummyContentWindow(browser);
+ }
+ return browser.contentWindowAsCPOW;
+};
+
+var RemoteWebNavigationInterposition = new Interposition("RemoteWebNavigation");
+
+RemoteWebNavigationInterposition.getters.sessionHistory = function(addon, target) {
+ CompatWarning.warn("Direct access to webNavigation.sessionHistory will no longer " +
+ "work in the chrome process.",
+ addon, CompatWarning.warnings.content);
+
+ if (target instanceof Ci.nsIDocShell) {
+ // We must have a non-remote browser, so we can go ahead
+ // and just return the real sessionHistory.
+ return target.sessionHistory;
+ }
+
+ let impl = target.wrappedJSObject;
+ if (!impl) {
+ return null;
+ }
+
+ let browser = impl._browser;
+
+ return getSessionHistory(browser);
+}
+
+var RemoteAddonsParent = {
+ init: function() {
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("Addons:RegisterGlobal", this);
+
+ Services.ppmm.initialProcessData.remoteAddonsParentInitted = true;
+
+ this.globalToBrowser = new WeakMap();
+ this.browserToGlobal = new WeakMap();
+ },
+
+ getInterfaceInterpositions: function() {
+ let result = {};
+
+ function register(intf, interp) {
+ result[intf.number] = interp;
+ }
+
+ register(Ci.nsICategoryManager, CategoryManagerInterposition);
+ register(Ci.nsIComponentRegistrar, ComponentRegistrarInterposition);
+ register(Ci.nsIObserverService, ObserverInterposition);
+ register(Ci.nsIXPCComponents_Utils, ComponentsUtilsInterposition);
+ register(Ci.nsIWebNavigation, RemoteWebNavigationInterposition);
+
+ return result;
+ },
+
+ getTaggedInterpositions: function() {
+ let result = {};
+
+ function register(tag, interp) {
+ result[tag] = interp;
+ }
+
+ register("EventTarget", EventTargetInterposition);
+ register("ContentDocShellTreeItem", ContentDocShellTreeItemInterposition);
+ register("ContentDocument", ContentDocumentInterposition);
+ register("RemoteBrowserElement", RemoteBrowserElementInterposition);
+ register("TabBrowserElement", TabBrowserElementInterposition);
+ register("ChromeWindow", ChromeWindowInterposition);
+
+ return result;
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "Addons:RegisterGlobal":
+ this.browserToGlobal.set(msg.target, msg.objects.global);
+ this.globalToBrowser.set(msg.objects.global, msg.target);
+ break;
+ }
+ }
+};
diff --git a/toolkit/components/addoncompat/ShimWaiver.jsm b/toolkit/components/addoncompat/ShimWaiver.jsm
new file mode 100644
index 0000000000..402ab4c322
--- /dev/null
+++ b/toolkit/components/addoncompat/ShimWaiver.jsm
@@ -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/.
+
+this.EXPORTED_SYMBOLS = ["ShimWaiver"];
+
+this.ShimWaiver = {
+ getProperty: function(obj, prop) {
+ let rv = obj[prop];
+ if (rv instanceof Function) {
+ rv = rv.bind(obj);
+ }
+ return rv;
+ }
+};
diff --git a/toolkit/components/addoncompat/addoncompat.manifest b/toolkit/components/addoncompat/addoncompat.manifest
new file mode 100644
index 0000000000..fe38f47d80
--- /dev/null
+++ b/toolkit/components/addoncompat/addoncompat.manifest
@@ -0,0 +1,4 @@
+component {1363d5f0-d95e-11e3-9c1a-0800200c9a66} multiprocessShims.js
+contract @mozilla.org/addons/multiprocess-shims;1 {1363d5f0-d95e-11e3-9c1a-0800200c9a66}
+component {50bc93ce-602a-4bef-bf3a-61fc749c4caf} defaultShims.js
+contract @mozilla.org/addons/default-addon-shims;1 {50bc93ce-602a-4bef-bf3a-61fc749c4caf}
diff --git a/toolkit/components/addoncompat/defaultShims.js b/toolkit/components/addoncompat/defaultShims.js
new file mode 100644
index 0000000000..a786efed7d
--- /dev/null
+++ b/toolkit/components/addoncompat/defaultShims.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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Using multiprocessShims is optional, and if an add-on is e10s compatible it should not
+ * use it. But in some cases we still want to use the interposition service for various
+ * features so we have a default shim service.
+ */
+
+function DefaultInterpositionService() {
+}
+
+DefaultInterpositionService.prototype = {
+ classID: Components.ID("{50bc93ce-602a-4bef-bf3a-61fc749c4caf}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonInterposition, Ci.nsISupportsWeakReference]),
+
+ getWhitelist: function() {
+ return [];
+ },
+
+ interposeProperty: function(addon, target, iid, prop) {
+ return null;
+ },
+
+ interposeCall: function(addonId, originalFunc, originalThis, args) {
+ args.splice(0, 0, addonId);
+ return originalFunc.apply(originalThis, args);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DefaultInterpositionService]);
diff --git a/toolkit/components/addoncompat/moz.build b/toolkit/components/addoncompat/moz.build
new file mode 100644
index 0000000000..58a26eeba4
--- /dev/null
+++ b/toolkit/components/addoncompat/moz.build
@@ -0,0 +1,21 @@
+# -*- 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/.
+
+TEST_DIRS += ['tests']
+
+EXTRA_COMPONENTS += [
+ 'addoncompat.manifest',
+ 'defaultShims.js',
+ 'multiprocessShims.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'CompatWarning.jsm',
+ 'Prefetcher.jsm',
+ 'RemoteAddonsChild.jsm',
+ 'RemoteAddonsParent.jsm',
+ 'ShimWaiver.jsm'
+]
diff --git a/toolkit/components/addoncompat/multiprocessShims.js b/toolkit/components/addoncompat/multiprocessShims.js
new file mode 100644
index 0000000000..8b252a0c48
--- /dev/null
+++ b/toolkit/components/addoncompat/multiprocessShims.js
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prefetcher",
+ "resource://gre/modules/Prefetcher.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemoteAddonsParent",
+ "resource://gre/modules/RemoteAddonsParent.jsm");
+
+/**
+ * This service overlays the API that the browser exposes to
+ * add-ons. The overlay tries to make a multiprocess browser appear as
+ * much as possible like a single process browser. An overlay can
+ * replace methods, getters, and setters of arbitrary browser objects.
+ *
+ * Most of the actual replacement code is implemented in
+ * RemoteAddonsParent. The code in this service simply decides how to
+ * replace code. For a given type of object (say, an
+ * nsIObserverService) the code in RemoteAddonsParent can register a
+ * set of replacement methods. This set is called an
+ * "interposition". The service keeps track of all the different
+ * interpositions. Whenever a method is called on some part of the
+ * browser API, this service gets a chance to replace it. To do so, it
+ * consults its map based on the type of object. If an interposition
+ * is found, the given method is looked up on it and called
+ * instead. If no method (or no interposition) is found, then the
+ * original target method is called as normal.
+ *
+ * For each method call, we need to determine the type of the target
+ * object. If the object is an old-style XPConnect wrapped native,
+ * then the type is simply the interface that the method was called on
+ * (Ci.nsIObserverService, say). For all other objects (WebIDL
+ * objects, CPOWs, and normal JS objects), the type is determined by
+ * calling getObjectTag.
+ *
+ * The interpositions defined in RemoteAddonsParent have three
+ * properties: methods, getters, and setters. When accessing a
+ * property, we first consult methods. If nothing is found, then we
+ * consult getters or setters, depending on whether the access is a
+ * get or a set.
+ *
+ * The methods in |methods| are functions that will be called whenever
+ * the given method is called on the target object. They are passed
+ * the same parameters as the original function except for two
+ * additional ones at the beginning: the add-on ID and the original
+ * target object that the method was called on. Additionally, the
+ * value of |this| is set to the original target object.
+ *
+ * The values in |getters| and |setters| should also be
+ * functions. They are called immediately when the given property is
+ * accessed. The functions in |getters| take two parameters: the
+ * add-on ID and the original target object. The functions in
+ * |setters| take those arguments plus the value that the property is
+ * being set to.
+ */
+
+function AddonInterpositionService()
+{
+ Prefetcher.init();
+ RemoteAddonsParent.init();
+
+ // These maps keep track of the interpositions for all different
+ // kinds of objects.
+ this._interfaceInterpositions = RemoteAddonsParent.getInterfaceInterpositions();
+ this._taggedInterpositions = RemoteAddonsParent.getTaggedInterpositions();
+
+ let wl = [];
+ for (let v in this._interfaceInterpositions) {
+ let interp = this._interfaceInterpositions[v];
+ wl.push(...Object.getOwnPropertyNames(interp.methods));
+ wl.push(...Object.getOwnPropertyNames(interp.getters));
+ wl.push(...Object.getOwnPropertyNames(interp.setters));
+ }
+
+ for (let v in this._taggedInterpositions) {
+ let interp = this._taggedInterpositions[v];
+ wl.push(...Object.getOwnPropertyNames(interp.methods));
+ wl.push(...Object.getOwnPropertyNames(interp.getters));
+ wl.push(...Object.getOwnPropertyNames(interp.setters));
+ }
+
+ let nameSet = new Set();
+ wl = wl.filter(function(item) {
+ if (nameSet.has(item))
+ return true;
+
+ nameSet.add(item);
+ return true;
+ });
+
+ this._whitelist = wl;
+}
+
+AddonInterpositionService.prototype = {
+ classID: Components.ID("{1363d5f0-d95e-11e3-9c1a-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonInterposition, Ci.nsISupportsWeakReference]),
+
+ getWhitelist: function() {
+ return this._whitelist;
+ },
+
+ // When the interface is not known for a method call, this code
+ // determines the type of the target object.
+ getObjectTag: function(target) {
+ if (Cu.isCrossProcessWrapper(target)) {
+ return Cu.getCrossProcessWrapperTag(target);
+ }
+
+ if (target instanceof Ci.nsIDOMXULElement) {
+ if (target.localName == "browser" && target.isRemoteBrowser) {
+ return "RemoteBrowserElement";
+ }
+
+ if (target.localName == "tabbrowser") {
+ return "TabBrowserElement";
+ }
+ }
+
+ if (target instanceof Ci.nsIDOMChromeWindow && target.gMultiProcessBrowser) {
+ return "ChromeWindow";
+ }
+
+ if (target instanceof Ci.nsIDOMEventTarget) {
+ return "EventTarget";
+ }
+
+ return "generic";
+ },
+
+ interposeProperty: function(addon, target, iid, prop) {
+ let interp;
+ if (iid) {
+ interp = this._interfaceInterpositions[iid];
+ } else {
+ try {
+ interp = this._taggedInterpositions[this.getObjectTag(target)];
+ }
+ catch (e) {
+ Cu.reportError(new Components.Exception("Failed to interpose object", e.result, Components.stack.caller));
+ }
+ }
+
+ if (!interp) {
+ return Prefetcher.lookupInCache(addon, target, prop);
+ }
+
+ let desc = { configurable: false, enumerable: true };
+
+ if ("methods" in interp && prop in interp.methods) {
+ desc.writable = false;
+ desc.value = function(...args) {
+ return interp.methods[prop](addon, target, ...args);
+ }
+
+ return desc;
+ } else if ("getters" in interp && prop in interp.getters) {
+ desc.get = function() { return interp.getters[prop](addon, target); };
+
+ if ("setters" in interp && prop in interp.setters) {
+ desc.set = function(v) { return interp.setters[prop](addon, target, v); };
+ }
+
+ return desc;
+ }
+
+ return Prefetcher.lookupInCache(addon, target, prop);
+ },
+
+ interposeCall: function(addonId, originalFunc, originalThis, args) {
+ args.splice(0, 0, addonId);
+ return originalFunc.apply(originalThis, args);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonInterpositionService]);
diff --git a/toolkit/components/addoncompat/tests/addon/bootstrap.js b/toolkit/components/addoncompat/tests/addon/bootstrap.js
new file mode 100644
index 0000000000..5e69fee22c
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/bootstrap.js
@@ -0,0 +1,653 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const baseURL = "http://mochi.test:8888/browser/" +
+ "toolkit/components/addoncompat/tests/browser/";
+
+var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"]
+ .getService(Ci.nsIContentSecurityManager);
+
+function forEachWindow(f)
+{
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+ f(win);
+ }
+}
+
+function addLoadListener(target, listener)
+{
+ target.addEventListener("load", function handler(event) {
+ target.removeEventListener("load", handler, true);
+ return listener(event);
+ }, true);
+}
+
+var gWin;
+var gBrowser;
+var ok, is, info;
+
+function removeTab(tab, done)
+{
+ // Remove the tab in a different turn of the event loop. This way
+ // the nested event loop in removeTab doesn't conflict with the
+ // event listener shims.
+ gWin.setTimeout(() => {
+ gBrowser.removeTab(tab);
+ done();
+ }, 0);
+}
+
+// Make sure that the shims for window.content, browser.contentWindow,
+// and browser.contentDocument are working.
+function testContentWindow()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ ok(gWin.content, "content is defined on chrome window");
+ ok(browser.contentWindow, "contentWindow is defined");
+ ok(browser.contentDocument, "contentWindow is defined");
+ is(gWin.content, browser.contentWindow, "content === contentWindow");
+ ok(browser.webNavigation.sessionHistory, "sessionHistory is defined");
+
+ ok(browser.contentDocument.getElementById("link"), "link present in document");
+
+ // FIXME: Waiting on bug 1073631.
+ // is(browser.contentWindow.wrappedJSObject.global, 3, "global available on document");
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+// Test for bug 1060046 and bug 1072607. We want to make sure that
+// adding and removing listeners works as expected.
+function testListeners()
+{
+ return new Promise(function(resolve, reject) {
+ const url1 = baseURL + "browser_addonShims_testpage.html";
+ const url2 = baseURL + "browser_addonShims_testpage2.html";
+
+ let tab = gBrowser.addTab(url2);
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ function dummyHandler() {}
+
+ // Test that a removed listener stays removed (bug
+ // 1072607). We're looking to make sure that adding and removing
+ // a listener here doesn't cause later listeners to fire more
+ // than once.
+ for (let i = 0; i < 5; i++) {
+ gBrowser.addEventListener("load", dummyHandler, true);
+ gBrowser.removeEventListener("load", dummyHandler, true);
+ }
+
+ // We also want to make sure that this listener doesn't fire
+ // after it's removed.
+ let loadWithRemoveCount = 0;
+ addLoadListener(browser, function handler1(event) {
+ loadWithRemoveCount++;
+ is(event.target.documentURI, url1, "only fire for first url");
+ });
+
+ // Load url1 and then url2. We want to check that:
+ // 1. handler1 only fires for url1.
+ // 2. handler2 only fires once for url1 (so the second time it
+ // fires should be for url2).
+ let loadCount = 0;
+ browser.addEventListener("load", function handler2(event) {
+ loadCount++;
+ if (loadCount == 1) {
+ is(event.target.documentURI, url1, "first load is for first page loaded");
+ browser.loadURI(url2);
+ } else {
+ gBrowser.removeEventListener("load", handler2, true);
+
+ is(event.target.documentURI, url2, "second load is for second page loaded");
+ is(loadWithRemoveCount, 1, "load handler is only called once");
+
+ removeTab(tab, resolve);
+ }
+ }, true);
+
+ browser.loadURI(url1);
+ });
+ });
+}
+
+// Test for bug 1059207. We want to make sure that adding a capturing
+// listener and a non-capturing listener to the same element works as
+// expected.
+function testCapturing()
+{
+ return new Promise(function(resolve, reject) {
+ let capturingCount = 0;
+ let nonCapturingCount = 0;
+
+ function capturingHandler(event) {
+ is(capturingCount, 0, "capturing handler called once");
+ is(nonCapturingCount, 0, "capturing handler called before bubbling handler");
+ capturingCount++;
+ }
+
+ function nonCapturingHandler(event) {
+ is(capturingCount, 1, "bubbling handler called after capturing handler");
+ is(nonCapturingCount, 0, "bubbling handler called once");
+ nonCapturingCount++;
+ }
+
+ gBrowser.addEventListener("mousedown", capturingHandler, true);
+ gBrowser.addEventListener("mousedown", nonCapturingHandler, false);
+
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ let win = browser.contentWindow;
+ let event = win.document.createEvent("MouseEvents");
+ event.initMouseEvent("mousedown", true, false, win, 1,
+ 1, 0, 0, 0, // screenX, screenY, clientX, clientY
+ false, false, false, false, // ctrlKey, altKey, shiftKey, metaKey
+ 0, null); // buttonCode, relatedTarget
+
+ let element = win.document.getElementById("output");
+ element.dispatchEvent(event);
+
+ is(capturingCount, 1, "capturing handler fired");
+ is(nonCapturingCount, 1, "bubbling handler fired");
+
+ gBrowser.removeEventListener("mousedown", capturingHandler, true);
+ gBrowser.removeEventListener("mousedown", nonCapturingHandler, false);
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+// Make sure we get observer notifications that normally fire in the
+// child.
+function testObserver()
+{
+ return new Promise(function(resolve, reject) {
+ let observerFired = 0;
+
+ function observer(subject, topic, data) {
+ Services.obs.removeObserver(observer, "document-element-inserted");
+ observerFired++;
+ }
+ Services.obs.addObserver(observer, "document-element-inserted", false);
+
+ let count = 0;
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function handler() {
+ count++;
+ if (count == 1) {
+ browser.reload();
+ } else {
+ browser.removeEventListener("load", handler);
+
+ is(observerFired, 1, "got observer notification");
+
+ removeTab(tab, resolve);
+ }
+ }, true);
+ });
+}
+
+// Test for bug 1072472. Make sure that creating a sandbox to run code
+// in the content window works. This is essentially a test for
+// Greasemonkey.
+function testSandbox()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function handler() {
+ browser.removeEventListener("load", handler);
+
+ let sandbox = Cu.Sandbox(browser.contentWindow,
+ {sandboxPrototype: browser.contentWindow,
+ wantXrays: false});
+ Cu.evalInSandbox("const unsafeWindow = window;", sandbox);
+ Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello';", sandbox);
+
+ is(browser.contentDocument.getElementById("output").innerHTML, "hello",
+ "sandbox code ran successfully");
+
+ // Now try a sandbox with expanded principals.
+ sandbox = Cu.Sandbox([browser.contentWindow],
+ {sandboxPrototype: browser.contentWindow,
+ wantXrays: false});
+ Cu.evalInSandbox("const unsafeWindow = window;", sandbox);
+ Cu.evalInSandbox("document.getElementById('output').innerHTML = 'hello2';", sandbox);
+
+ is(browser.contentDocument.getElementById("output").innerHTML, "hello2",
+ "EP sandbox code ran successfully");
+
+ removeTab(tab, resolve);
+ }, true);
+ });
+}
+
+// Test for bug 1095305. We just want to make sure that loading some
+// unprivileged content from an add-on package doesn't crash.
+function testAddonContent()
+{
+ let chromeRegistry = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+ let base = chromeRegistry.convertChromeURL(BrowserUtils.makeURI("chrome://addonshim1/content/"));
+
+ let res = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ res.setSubstitution("addonshim1", base);
+
+ return new Promise(function(resolve, reject) {
+ const url = "resource://addonshim1/page.html";
+ let tab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ res.setSubstitution("addonshim1", null);
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+
+// Test for bug 1102410. We check that multiple nsIAboutModule's can be
+// registered in the parent, and that the child can browse to each of
+// the registered about: pages.
+function testAboutModuleRegistration()
+{
+ let Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+ let modulesToUnregister = new Map();
+
+ function TestChannel(uri, aLoadInfo, aboutName) {
+ this.aboutName = aboutName;
+ this.loadInfo = aLoadInfo;
+ this.URI = this.originalURI = uri;
+ }
+
+ TestChannel.prototype = {
+ asyncOpen: function(listener, context) {
+ let stream = this.open();
+ let runnable = {
+ run: () => {
+ try {
+ listener.onStartRequest(this, context);
+ } catch (e) {}
+ try {
+ listener.onDataAvailable(this, context, stream, 0, stream.available());
+ } catch (e) {}
+ try {
+ listener.onStopRequest(this, context, Cr.NS_OK);
+ } catch (e) {}
+ }
+ };
+ Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ },
+
+ asyncOpen2: function(listener) {
+ // throws an error if security checks fail
+ var outListener = contentSecManager.performSecurityCheck(this, listener);
+ return this.asyncOpen(outListener, null);
+ },
+
+ open: function() {
+ function getWindow(channel) {
+ try
+ {
+ if (channel.notificationCallbacks)
+ return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
+ } catch (e) {}
+
+ try
+ {
+ if (channel.loadGroup && channel.loadGroup.notificationCallbacks)
+ return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
+ } catch (e) {}
+
+ return null;
+ }
+
+ let data = `<html><h1>${this.aboutName}</h1></html>`;
+ let wnd = getWindow(this);
+ if (!wnd)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+ return stream;
+ },
+
+ open2: function() {
+ // throws an error if security checks fail
+ contentSecManager.performSecurityCheck(this, null);
+ return this.open();
+ },
+
+ isPending: function() {
+ return false;
+ },
+ cancel: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ suspend: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ resume: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+ };
+
+ /**
+ * This function creates a new nsIAboutModule and registers it. Callers
+ * should also call unregisterModules after using this function to clean
+ * up the nsIAboutModules at the end of this test.
+ *
+ * @param aboutName
+ * This will be the string after about: used to refer to this module.
+ * For example, if aboutName is foo, you can refer to this module by
+ * browsing to about:foo.
+ *
+ * @param uuid
+ * A unique identifer string for this module. For example,
+ * "5f3a921b-250f-4ac5-a61c-8f79372e6063"
+ */
+ let createAndRegisterAboutModule = function(aboutName, uuid) {
+
+ let AboutModule = function() {};
+
+ AboutModule.prototype = {
+ classID: Components.ID(uuid),
+ classDescription: `Testing About Module for about:${aboutName}`,
+ contractID: `@mozilla.org/network/protocol/about;1?what=${aboutName}`,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ newChannel: (aURI, aLoadInfo) => {
+ return new TestChannel(aURI, aLoadInfo, aboutName);
+ },
+
+ getURIFlags: (aURI) => {
+ return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+ };
+
+ let factory = {
+ createInstance: function(outer, iid) {
+ if (outer) {
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ return new AboutModule();
+ },
+ };
+
+ Registrar.registerFactory(AboutModule.prototype.classID,
+ AboutModule.prototype.classDescription,
+ AboutModule.prototype.contractID,
+ factory);
+
+ modulesToUnregister.set(AboutModule.prototype.classID,
+ factory);
+ };
+
+ /**
+ * Unregisters any nsIAboutModules registered with
+ * createAndRegisterAboutModule.
+ */
+ let unregisterModules = () => {
+ for (let [classID, factory] of modulesToUnregister) {
+ Registrar.unregisterFactory(classID, factory);
+ }
+ };
+
+ /**
+ * Takes a browser, and sends it a framescript to attempt to
+ * load some about: pages. The frame script will send a test:result
+ * message on completion, passing back a data object with:
+ *
+ * {
+ * pass: true
+ * }
+ *
+ * on success, and:
+ *
+ * {
+ * pass: false,
+ * errorMsg: message,
+ * }
+ *
+ * on failure.
+ *
+ * @param browser
+ * The browser to send the framescript to.
+ */
+ let testAboutModulesWork = (browser) => {
+ let testConnection = () => {
+ let request = new content.XMLHttpRequest();
+ try {
+ request.open("GET", "about:test1", false);
+ request.send(null);
+ if (request.status != 200) {
+ throw (`about:test1 response had status ${request.status} - expected 200`);
+ }
+ if (request.responseText.indexOf("test1") == -1) {
+ throw (`about:test1 response had result ${request.responseText}`);
+ }
+
+ request = new content.XMLHttpRequest();
+ request.open("GET", "about:test2", false);
+ request.send(null);
+
+ if (request.status != 200) {
+ throw (`about:test2 response had status ${request.status} - expected 200`);
+ }
+ if (request.responseText.indexOf("test2") == -1) {
+ throw (`about:test2 response had result ${request.responseText}`);
+ }
+
+ sendAsyncMessage("test:result", {
+ pass: true,
+ });
+ } catch (e) {
+ sendAsyncMessage("test:result", {
+ pass: false,
+ errorMsg: e.toString(),
+ });
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ let mm = browser.messageManager;
+ mm.addMessageListener("test:result", function onTestResult(message) {
+ mm.removeMessageListener("test:result", onTestResult);
+ if (message.data.pass) {
+ ok(true, "Connections to about: pages were successful");
+ } else {
+ ok(false, message.data.errorMsg);
+ }
+ resolve();
+ });
+ mm.loadFrameScript("data:,(" + testConnection.toString() + ")();", false);
+ });
+ }
+
+ // Here's where the actual test is performed.
+ return new Promise((resolve, reject) => {
+ createAndRegisterAboutModule("test1", "5f3a921b-250f-4ac5-a61c-8f79372e6063");
+ createAndRegisterAboutModule("test2", "d7ec0389-1d49-40fa-b55c-a1fc3a6dbf6f");
+
+ // This needs to be a chrome-privileged page that loads in the
+ // content process. It needs chrome privs because otherwise the
+ // XHRs for about:test[12] will fail with a privilege error
+ // despite the presence of URI_SAFE_FOR_UNTRUSTED_CONTENT.
+ let newTab = gBrowser.addTab("chrome://addonshim1/content/page.html");
+ gBrowser.selectedTab = newTab;
+ let browser = newTab.linkedBrowser;
+
+ addLoadListener(browser, function() {
+ testAboutModulesWork(browser).then(() => {
+ unregisterModules();
+ removeTab(newTab, resolve);
+ });
+ });
+ });
+}
+
+function testProgressListener()
+{
+ const url = baseURL + "browser_addonShims_testpage.html";
+
+ let sawGlobalLocChange = false;
+ let sawTabsLocChange = false;
+
+ let globalListener = {
+ onLocationChange: function(webProgress, request, uri) {
+ if (uri.spec == url) {
+ sawGlobalLocChange = true;
+ ok(request instanceof Ci.nsIHttpChannel, "Global listener channel is an HTTP channel");
+ }
+ },
+ };
+
+ let tabsListener = {
+ onLocationChange: function(browser, webProgress, request, uri) {
+ if (uri.spec == url) {
+ sawTabsLocChange = true;
+ ok(request instanceof Ci.nsIHttpChannel, "Tab listener channel is an HTTP channel");
+ }
+ },
+ };
+
+ gBrowser.addProgressListener(globalListener);
+ gBrowser.addTabsProgressListener(tabsListener);
+ info("Added progress listeners");
+
+ return new Promise(function(resolve, reject) {
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ addLoadListener(tab.linkedBrowser, function handler() {
+ ok(sawGlobalLocChange, "Saw global onLocationChange");
+ ok(sawTabsLocChange, "Saw tabs onLocationChange");
+
+ gBrowser.removeProgressListener(globalListener);
+ gBrowser.removeTabsProgressListener(tabsListener);
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+function testRootTreeItem()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ let win = browser.contentWindow;
+
+ // Add-ons love this crap.
+ let root = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ is(root, gWin, "got correct chrome window");
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+function testImportNode()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab(url);
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ let node = gWin.document.createElement("div");
+ let doc = browser.contentDocument;
+ let result;
+ try {
+ result = doc.importNode(node, false);
+ } catch (e) {
+ ok(false, "importing threw an exception");
+ }
+ if (browser.isRemoteBrowser) {
+ is(result, node, "got expected import result");
+ }
+
+ removeTab(tab, resolve);
+ });
+ });
+}
+
+function runTests(win, funcs)
+{
+ ok = funcs.ok;
+ is = funcs.is;
+ info = funcs.info;
+
+ gWin = win;
+ gBrowser = win.gBrowser;
+
+ return testContentWindow().
+ then(testListeners).
+ then(testCapturing).
+ then(testObserver).
+ then(testSandbox).
+ then(testAddonContent).
+ then(testAboutModuleRegistration).
+ then(testProgressListener).
+ then(testRootTreeItem).
+ then(testImportNode).
+ then(Promise.resolve());
+}
+
+/*
+ bootstrap.js API
+*/
+
+function startup(aData, aReason)
+{
+ forEachWindow(win => {
+ win.runAddonShimTests = (funcs) => runTests(win, funcs);
+ });
+}
+
+function shutdown(aData, aReason)
+{
+ forEachWindow(win => {
+ delete win.runAddonShimTests;
+ });
+}
+
+function install(aData, aReason)
+{
+}
+
+function uninstall(aData, aReason)
+{
+}
+
diff --git a/toolkit/components/addoncompat/tests/addon/chrome.manifest b/toolkit/components/addoncompat/tests/addon/chrome.manifest
new file mode 100644
index 0000000000..602ba3a5dc
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/chrome.manifest
@@ -0,0 +1 @@
+content addonshim1 content/
diff --git a/toolkit/components/addoncompat/tests/addon/content/page.html b/toolkit/components/addoncompat/tests/addon/content/page.html
new file mode 100644
index 0000000000..90531a4b3e
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/content/page.html
@@ -0,0 +1,2 @@
+<html>
+</html>
diff --git a/toolkit/components/addoncompat/tests/addon/install.rdf b/toolkit/components/addoncompat/tests/addon/install.rdf
new file mode 100644
index 0000000000..d59c7b19d1
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/addon/install.rdf
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-shim-1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test addon shim 1</em:name>
+ <em:description>Test an add-on that needs multiprocess shims.</em:description>
+ <em:multiprocessCompatible>false</em:multiprocessCompatible>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>10.0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/components/addoncompat/tests/browser/.eslintrc.js b/toolkit/components/addoncompat/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/addoncompat/tests/browser/addon.xpi b/toolkit/components/addoncompat/tests/browser/addon.xpi
new file mode 100644
index 0000000000..e6392fb40a
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/addon.xpi
Binary files differ
diff --git a/toolkit/components/addoncompat/tests/browser/browser.ini b/toolkit/components/addoncompat/tests/browser/browser.ini
new file mode 100644
index 0000000000..7c85475625
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = addons
+support-files =
+ addon.xpi
+ browser_addonShims_testpage.html
+ browser_addonShims_testpage2.html
+ compat-addon.xpi
+
+[browser_addonShims.js]
diff --git a/toolkit/components/addoncompat/tests/browser/browser_addonShims.js b/toolkit/components/addoncompat/tests/browser/browser_addonShims.js
new file mode 100644
index 0000000000..b642eb3cb0
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims.js
@@ -0,0 +1,67 @@
+var {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+var {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const ADDON_URL = "http://example.com/browser/toolkit/components/addoncompat/tests/browser/addon.xpi";
+const COMPAT_ADDON_URL = "http://example.com/browser/toolkit/components/addoncompat/tests/browser/compat-addon.xpi";
+
+// Install a test add-on that will exercise e10s shims.
+// url: Location of the add-on.
+function addAddon(url)
+{
+ info("Installing add-on: " + url);
+
+ return new Promise(function(resolve, reject) {
+ AddonManager.getInstallForURL(url, installer => {
+ installer.install();
+ let listener = {
+ onInstallEnded: function(addon, addonInstall) {
+ installer.removeListener(listener);
+
+ // Wait for add-on's startup scripts to execute. See bug 997408
+ executeSoon(function() {
+ resolve(addonInstall);
+ });
+ }
+ };
+ installer.addListener(listener);
+ }, "application/x-xpinstall");
+ });
+}
+
+// Uninstall a test add-on.
+// addon: The addon reference returned from addAddon.
+function removeAddon(addon)
+{
+ info("Removing addon.");
+
+ return new Promise(function(resolve, reject) {
+ let listener = {
+ onUninstalled: function(uninstalledAddon) {
+ if (uninstalledAddon != addon) {
+ return;
+ }
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ });
+}
+
+add_task(function* test_addon_shims() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({set: [["dom.ipc.shims.enabledWarnings", true]]},
+ resolve);
+ });
+
+ let addon = yield addAddon(ADDON_URL);
+ yield window.runAddonShimTests({ok: ok, is: is, info: info});
+ yield removeAddon(addon);
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ addon = yield addAddon(COMPAT_ADDON_URL);
+ yield window.runAddonTests({ok: ok, is: is, info: info});
+ yield removeAddon(addon);
+ }
+});
diff --git a/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html
new file mode 100644
index 0000000000..5a8b34e888
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage.html
@@ -0,0 +1,17 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>shim test</title>
+</head>
+
+<body>
+Hello!
+
+<a href="browser_addonShims_testpage2.html" id="link">Link</a>
+<div id="output"></div>
+
+<script type="text/javascript">
+var global = 3;
+</script>
+</body>
+</html>
diff --git a/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html
new file mode 100644
index 0000000000..f644b1129c
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/browser_addonShims_testpage2.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>shim test</title>
+</head>
+
+<body>
+Hello!
+
+<a href="browser_addonShims_testpage.html" id="link">Link</a>
+
+<script type="text/javascript">
+var global = 5;
+</script>
+</body>
+</html>
diff --git a/toolkit/components/addoncompat/tests/browser/compat-addon.xpi b/toolkit/components/addoncompat/tests/browser/compat-addon.xpi
new file mode 100644
index 0000000000..c7ca32cdc6
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/browser/compat-addon.xpi
Binary files differ
diff --git a/toolkit/components/addoncompat/tests/compat-addon/bootstrap.js b/toolkit/components/addoncompat/tests/compat-addon/bootstrap.js
new file mode 100644
index 0000000000..7c93bad089
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/compat-addon/bootstrap.js
@@ -0,0 +1,99 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const baseURL = "http://mochi.test:8888/browser/" +
+ "toolkit/components/addoncompat/tests/browser/";
+
+function forEachWindow(f)
+{
+ let wins = Services.wm.getEnumerator("navigator:browser");
+ while (wins.hasMoreElements()) {
+ let win = wins.getNext();
+ f(win);
+ }
+}
+
+function addLoadListener(target, listener)
+{
+ function frameScript() {
+ addEventListener("load", function handler(event) {
+ removeEventListener("load", handler, true);
+ sendAsyncMessage("compat-test:loaded");
+ }, true);
+ }
+ target.messageManager.loadFrameScript("data:,(" + frameScript.toString() + ")()", false);
+ target.messageManager.addMessageListener("compat-test:loaded", function handler() {
+ target.messageManager.removeMessageListener("compat-test:loaded", handler);
+ listener();
+ });
+}
+
+var gWin;
+var gBrowser;
+var ok, is, info;
+
+// Make sure that the shims for window.content, browser.contentWindow,
+// and browser.contentDocument are working.
+function testContentWindow()
+{
+ return new Promise(function(resolve, reject) {
+ const url = baseURL + "browser_addonShims_testpage.html";
+ let tab = gBrowser.addTab("about:blank");
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ addLoadListener(browser, function handler() {
+ ok(!gWin.content, "content is defined on chrome window");
+ ok(!browser.contentWindow, "contentWindow is defined");
+ ok(!browser.contentDocument, "contentWindow is defined");
+
+ gBrowser.removeTab(tab);
+ resolve();
+ });
+ browser.loadURI(url);
+ });
+}
+
+function runTests(win, funcs)
+{
+ ok = funcs.ok;
+ is = funcs.is;
+ info = funcs.info;
+
+ gWin = win;
+ gBrowser = win.gBrowser;
+
+ return testContentWindow();
+}
+
+/*
+ bootstrap.js API
+*/
+
+function startup(aData, aReason)
+{
+ forEachWindow(win => {
+ win.runAddonTests = (funcs) => runTests(win, funcs);
+ });
+}
+
+function shutdown(aData, aReason)
+{
+ forEachWindow(win => {
+ delete win.runAddonTests;
+ });
+}
+
+function install(aData, aReason)
+{
+}
+
+function uninstall(aData, aReason)
+{
+}
+
diff --git a/toolkit/components/addoncompat/tests/compat-addon/install.rdf b/toolkit/components/addoncompat/tests/compat-addon/install.rdf
new file mode 100644
index 0000000000..331fd1540b
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/compat-addon/install.rdf
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test-addon-shim-2@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test addon shims 2</em:name>
+ <em:description>Test an add-on that doesn't need multiprocess shims.</em:description>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>10.0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/components/addoncompat/tests/moz.build b/toolkit/components/addoncompat/tests/moz.build
new file mode 100644
index 0000000000..589eaa8127
--- /dev/null
+++ b/toolkit/components/addoncompat/tests/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/.
+
+BROWSER_CHROME_MANIFESTS += ['browser/browser.ini']
diff --git a/toolkit/components/alerts/AlertNotification.cpp b/toolkit/components/alerts/AlertNotification.cpp
new file mode 100644
index 0000000000..b828f11003
--- /dev/null
+++ b/toolkit/components/alerts/AlertNotification.cpp
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Pub
+ * License, v. 2.0. If a copy of the MPL was not distributed with t
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/AlertNotification.h"
+
+#include "imgIContainer.h"
+#include "imgINotificationObserver.h"
+#include "imgIRequest.h"
+#include "imgLoader.h"
+#include "nsAlertsUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(AlertNotification, nsIAlertNotification)
+
+AlertNotification::AlertNotification()
+ : mTextClickable(false)
+ , mInPrivateBrowsing(false)
+{}
+
+AlertNotification::~AlertNotification()
+{}
+
+NS_IMETHODIMP
+AlertNotification::Init(const nsAString& aName, const nsAString& aImageURL,
+ const nsAString& aTitle, const nsAString& aText,
+ bool aTextClickable, const nsAString& aCookie,
+ const nsAString& aDir, const nsAString& aLang,
+ const nsAString& aData, nsIPrincipal* aPrincipal,
+ bool aInPrivateBrowsing, bool aRequireInteraction)
+{
+ mName = aName;
+ mImageURL = aImageURL;
+ mTitle = aTitle;
+ mText = aText;
+ mTextClickable = aTextClickable;
+ mCookie = aCookie;
+ mDir = aDir;
+ mLang = aLang;
+ mData = aData;
+ mPrincipal = aPrincipal;
+ mInPrivateBrowsing = aInPrivateBrowsing;
+ mRequireInteraction = aRequireInteraction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetName(nsAString& aName)
+{
+ aName = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetImageURL(nsAString& aImageURL)
+{
+ aImageURL = mImageURL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetTitle(nsAString& aTitle)
+{
+ aTitle = mTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetText(nsAString& aText)
+{
+ aText = mText;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetTextClickable(bool* aTextClickable)
+{
+ *aTextClickable = mTextClickable;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetCookie(nsAString& aCookie)
+{
+ aCookie = mCookie;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetDir(nsAString& aDir)
+{
+ aDir = mDir;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetLang(nsAString& aLang)
+{
+ aLang = mLang;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetRequireInteraction(bool* aRequireInteraction)
+{
+ *aRequireInteraction = mRequireInteraction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetData(nsAString& aData)
+{
+ aData = mData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetPrincipal(nsIPrincipal** aPrincipal)
+{
+ NS_IF_ADDREF(*aPrincipal = mPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetURI(nsIURI** aURI)
+{
+ if (!nsAlertsUtils::IsActionablePrincipal(mPrincipal)) {
+ *aURI = nullptr;
+ return NS_OK;
+ }
+ return mPrincipal->GetURI(aURI);
+}
+
+NS_IMETHODIMP
+AlertNotification::GetInPrivateBrowsing(bool* aInPrivateBrowsing)
+{
+ *aInPrivateBrowsing = mInPrivateBrowsing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetActionable(bool* aActionable)
+{
+ *aActionable = nsAlertsUtils::IsActionablePrincipal(mPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetSource(nsAString& aSource)
+{
+ nsAlertsUtils::GetSourceHostPort(mPrincipal, aSource);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::LoadImage(uint32_t aTimeout,
+ nsIAlertNotificationImageListener* aListener,
+ nsISupports* aUserData,
+ nsICancelable** aRequest)
+{
+ NS_ENSURE_ARG(aListener);
+ NS_ENSURE_ARG_POINTER(aRequest);
+ *aRequest = nullptr;
+
+ // Exit early if this alert doesn't have an image.
+ if (mImageURL.IsEmpty()) {
+ return aListener->OnImageMissing(aUserData);
+ }
+ nsCOMPtr<nsIURI> imageURI;
+ NS_NewURI(getter_AddRefs(imageURI), mImageURL);
+ if (!imageURI) {
+ return aListener->OnImageMissing(aUserData);
+ }
+
+ RefPtr<AlertImageRequest> request = new AlertImageRequest(imageURI, mPrincipal,
+ mInPrivateBrowsing,
+ aTimeout, aListener,
+ aUserData);
+ nsresult rv = request->Start();
+ request.forget(aRequest);
+ return rv;
+}
+
+NS_IMPL_CYCLE_COLLECTION(AlertImageRequest, mURI, mPrincipal, mListener,
+ mUserData)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AlertImageRequest)
+ NS_INTERFACE_MAP_ENTRY(imgINotificationObserver)
+ NS_INTERFACE_MAP_ENTRY(nsICancelable)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, imgINotificationObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AlertImageRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AlertImageRequest)
+
+AlertImageRequest::AlertImageRequest(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ bool aInPrivateBrowsing, uint32_t aTimeout,
+ nsIAlertNotificationImageListener* aListener,
+ nsISupports* aUserData)
+ : mURI(aURI)
+ , mPrincipal(aPrincipal)
+ , mInPrivateBrowsing(aInPrivateBrowsing)
+ , mTimeout(aTimeout)
+ , mListener(aListener)
+ , mUserData(aUserData)
+{}
+
+AlertImageRequest::~AlertImageRequest()
+{
+ if (mRequest) {
+ mRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ }
+}
+
+NS_IMETHODIMP
+AlertImageRequest::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData)
+{
+ MOZ_ASSERT(aRequest == mRequest);
+
+ uint32_t imgStatus = imgIRequest::STATUS_ERROR;
+ nsresult rv = aRequest->GetImageStatus(&imgStatus);
+ if (NS_WARN_IF(NS_FAILED(rv)) ||
+ (imgStatus & imgIRequest::STATUS_ERROR)) {
+ return NotifyMissing();
+ }
+
+ // If the image is already decoded, `FRAME_COMPLETE` will fire before
+ // `LOAD_COMPLETE`, so we can notify the listener immediately. Otherwise,
+ // we'll need to request a decode when `LOAD_COMPLETE` fires, and wait
+ // for the first frame.
+
+ if (aType == imgINotificationObserver::LOAD_COMPLETE) {
+ if (!(imgStatus & imgIRequest::STATUS_FRAME_COMPLETE)) {
+ nsCOMPtr<imgIContainer> image;
+ rv = aRequest->GetImage(getter_AddRefs(image));
+ if (NS_WARN_IF(NS_FAILED(rv) || !image)) {
+ return NotifyMissing();
+ }
+
+ // Ask the image to decode at its intrinsic size.
+ int32_t width = 0, height = 0;
+ image->GetWidth(&width);
+ image->GetHeight(&height);
+ image->RequestDecodeForSize(gfx::IntSize(width, height), imgIContainer::FLAG_NONE);
+ }
+ return NS_OK;
+ }
+
+ if (aType == imgINotificationObserver::FRAME_COMPLETE) {
+ return NotifyComplete();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertImageRequest::Notify(nsITimer* aTimer)
+{
+ MOZ_ASSERT(aTimer == mTimer);
+ return NotifyMissing();
+}
+
+NS_IMETHODIMP
+AlertImageRequest::Cancel(nsresult aReason)
+{
+ if (mRequest) {
+ mRequest->Cancel(aReason);
+ }
+ // We call `NotifyMissing` here because we won't receive a `LOAD_COMPLETE`
+ // notification if we cancel the request before it loads (bug 1233086,
+ // comment 33). Once that's fixed, `nsIAlertNotification::loadImage` could
+ // return the underlying `imgIRequest` instead of the wrapper.
+ return NotifyMissing();
+}
+
+nsresult
+AlertImageRequest::Start()
+{
+ // Keep the request alive until we notify the image listener.
+ NS_ADDREF_THIS();
+
+ nsresult rv;
+ if (mTimeout > 0) {
+ mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ if (NS_WARN_IF(!mTimer)) {
+ return NotifyMissing();
+ }
+ rv = mTimer->InitWithCallback(this, mTimeout,
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NotifyMissing();
+ }
+ }
+
+ // Begin loading the image.
+ imgLoader* il = imgLoader::NormalLoader();
+ if (!il) {
+ return NotifyMissing();
+ }
+
+ // Bug 1237405: `LOAD_ANONYMOUS` disables cookies, but we want to use a
+ // temporary cookie jar instead. We should also use
+ // `imgLoader::PrivateBrowsingLoader()` instead of the normal loader.
+ // Unfortunately, the PB loader checks the load group, and asserts if its
+ // load context's PB flag isn't set. The fix is to pass the load group to
+ // `nsIAlertNotification::loadImage`.
+ int32_t loadFlags = mInPrivateBrowsing ? nsIRequest::LOAD_ANONYMOUS :
+ nsIRequest::LOAD_NORMAL;
+
+ rv = il->LoadImageXPCOM(mURI, nullptr, nullptr,
+ NS_LITERAL_STRING("default"), mPrincipal, nullptr,
+ this, nullptr, loadFlags, nullptr,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE,
+ getter_AddRefs(mRequest));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NotifyMissing();
+ }
+
+ return NS_OK;
+}
+
+nsresult
+AlertImageRequest::NotifyMissing()
+{
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ if (nsCOMPtr<nsIAlertNotificationImageListener> listener = mListener.forget()) {
+ nsresult rv = listener->OnImageMissing(mUserData);
+ NS_RELEASE_THIS();
+ return rv;
+ }
+ return NS_OK;
+}
+
+nsresult
+AlertImageRequest::NotifyComplete()
+{
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ if (nsCOMPtr<nsIAlertNotificationImageListener> listener = mListener.forget()) {
+ nsresult rv = listener->OnImageReady(mUserData, mRequest);
+ NS_RELEASE_THIS();
+ return rv;
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/alerts/AlertNotification.h b/toolkit/components/alerts/AlertNotification.h
new file mode 100644
index 0000000000..c0bcc0ba95
--- /dev/null
+++ b/toolkit/components/alerts/AlertNotification.h
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla_AlertNotification_h__
+#define mozilla_AlertNotification_h__
+
+#include "imgINotificationObserver.h"
+#include "nsIAlertsService.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsICancelable.h"
+#include "nsIPrincipal.h"
+#include "nsString.h"
+#include "nsITimer.h"
+
+namespace mozilla {
+
+class AlertImageRequest final : public imgINotificationObserver,
+ public nsICancelable,
+ public nsITimerCallback
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(AlertImageRequest,
+ imgINotificationObserver)
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+ NS_DECL_NSICANCELABLE
+ NS_DECL_NSITIMERCALLBACK
+
+ AlertImageRequest(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ bool aInPrivateBrowsing, uint32_t aTimeout,
+ nsIAlertNotificationImageListener* aListener,
+ nsISupports* aUserData);
+
+ nsresult Start();
+
+private:
+ virtual ~AlertImageRequest();
+
+ nsresult NotifyMissing();
+ nsresult NotifyComplete();
+
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ bool mInPrivateBrowsing;
+ uint32_t mTimeout;
+ nsCOMPtr<nsIAlertNotificationImageListener> mListener;
+ nsCOMPtr<nsISupports> mUserData;
+ nsCOMPtr<nsITimer> mTimer;
+ nsCOMPtr<imgIRequest> mRequest;
+};
+
+class AlertNotification final : public nsIAlertNotification
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIALERTNOTIFICATION
+ AlertNotification();
+
+protected:
+ virtual ~AlertNotification();
+
+private:
+ nsString mName;
+ nsString mImageURL;
+ nsString mTitle;
+ nsString mText;
+ bool mTextClickable;
+ nsString mCookie;
+ nsString mDir;
+ nsString mLang;
+ bool mRequireInteraction;
+ nsString mData;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ bool mInPrivateBrowsing;
+};
+
+} // namespace mozilla
+
+#endif /* mozilla_AlertNotification_h__ */
diff --git a/toolkit/components/alerts/AlertNotificationIPCSerializer.h b/toolkit/components/alerts/AlertNotificationIPCSerializer.h
new file mode 100644
index 0000000000..9544f9633d
--- /dev/null
+++ b/toolkit/components/alerts/AlertNotificationIPCSerializer.h
@@ -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/. */
+
+#ifndef mozilla_AlertNotificationIPCSerializer_h__
+#define mozilla_AlertNotificationIPCSerializer_h__
+
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIAlertsService.h"
+#include "nsIPrincipal.h"
+#include "nsString.h"
+
+#include "ipc/IPCMessageUtils.h"
+
+#include "mozilla/dom/PermissionMessageUtils.h"
+
+typedef nsIAlertNotification* AlertNotificationType;
+
+namespace IPC {
+
+template <>
+struct ParamTraits<AlertNotificationType>
+{
+ typedef AlertNotificationType paramType;
+
+ static void Write(Message* aMsg, const paramType& aParam)
+ {
+ bool isNull = !aParam;
+ if (isNull) {
+ WriteParam(aMsg, isNull);
+ return;
+ }
+
+ nsString name, imageURL, title, text, cookie, dir, lang, data;
+ bool textClickable, inPrivateBrowsing, requireInteraction;
+ nsCOMPtr<nsIPrincipal> principal;
+
+ if (NS_WARN_IF(NS_FAILED(aParam->GetName(name))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetImageURL(imageURL))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetTitle(title))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetText(text))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetTextClickable(&textClickable))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetCookie(cookie))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetDir(dir))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetLang(lang))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetData(data))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetPrincipal(getter_AddRefs(principal)))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetInPrivateBrowsing(&inPrivateBrowsing))) ||
+ NS_WARN_IF(NS_FAILED(aParam->GetRequireInteraction(&requireInteraction)))) {
+
+ // Write a `null` object if any getter returns an error. Otherwise, the
+ // receiver will try to deserialize an incomplete object and crash.
+ WriteParam(aMsg, /* isNull */ true);
+ return;
+ }
+
+ WriteParam(aMsg, isNull);
+ WriteParam(aMsg, name);
+ WriteParam(aMsg, imageURL);
+ WriteParam(aMsg, title);
+ WriteParam(aMsg, text);
+ WriteParam(aMsg, textClickable);
+ WriteParam(aMsg, cookie);
+ WriteParam(aMsg, dir);
+ WriteParam(aMsg, lang);
+ WriteParam(aMsg, data);
+ WriteParam(aMsg, IPC::Principal(principal));
+ WriteParam(aMsg, inPrivateBrowsing);
+ WriteParam(aMsg, requireInteraction);
+ }
+
+ static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+ {
+ bool isNull;
+ NS_ENSURE_TRUE(ReadParam(aMsg, aIter, &isNull), false);
+ if (isNull) {
+ *aResult = nullptr;
+ return true;
+ }
+
+ nsString name, imageURL, title, text, cookie, dir, lang, data;
+ bool textClickable, inPrivateBrowsing, requireInteraction;
+ IPC::Principal principal;
+
+ if (!ReadParam(aMsg, aIter, &name) ||
+ !ReadParam(aMsg, aIter, &imageURL) ||
+ !ReadParam(aMsg, aIter, &title) ||
+ !ReadParam(aMsg, aIter, &text) ||
+ !ReadParam(aMsg, aIter, &textClickable) ||
+ !ReadParam(aMsg, aIter, &cookie) ||
+ !ReadParam(aMsg, aIter, &dir) ||
+ !ReadParam(aMsg, aIter, &lang) ||
+ !ReadParam(aMsg, aIter, &data) ||
+ !ReadParam(aMsg, aIter, &principal) ||
+ !ReadParam(aMsg, aIter, &inPrivateBrowsing) ||
+ !ReadParam(aMsg, aIter, &requireInteraction)) {
+
+ return false;
+ }
+
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ if (NS_WARN_IF(!alert)) {
+ *aResult = nullptr;
+ return true;
+ }
+ nsresult rv = alert->Init(name, imageURL, title, text, textClickable,
+ cookie, dir, lang, data, principal,
+ inPrivateBrowsing, requireInteraction);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aResult = nullptr;
+ return true;
+ }
+ alert.forget(aResult);
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif /* mozilla_AlertNotificationIPCSerializer_h__ */
diff --git a/toolkit/components/alerts/jar.mn b/toolkit/components/alerts/jar.mn
new file mode 100644
index 0000000000..c45939078f
--- /dev/null
+++ b/toolkit/components/alerts/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/.
+
+toolkit.jar:
+ content/global/alerts/alert.css (resources/content/alert.css)
+ content/global/alerts/alert.xul (resources/content/alert.xul)
+ content/global/alerts/alert.js (resources/content/alert.js)
diff --git a/toolkit/components/alerts/moz.build b/toolkit/components/alerts/moz.build
new file mode 100644
index 0000000000..cdbf92511e
--- /dev/null
+++ b/toolkit/components/alerts/moz.build
@@ -0,0 +1,38 @@
+# -*- 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_MANIFESTS += ['test/mochitest.ini']
+
+XPIDL_SOURCES += [
+ 'nsIAlertsService.idl',
+]
+
+XPIDL_MODULE = 'alerts'
+
+EXPORTS += [
+ 'nsAlertsUtils.h',
+]
+
+EXPORTS.mozilla += [
+ 'AlertNotification.h',
+ 'AlertNotificationIPCSerializer.h',
+]
+
+UNIFIED_SOURCES += [
+ 'AlertNotification.cpp',
+ 'nsAlertsService.cpp',
+ 'nsAlertsUtils.cpp',
+ 'nsXULAlerts.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Notifications and Alerts')
diff --git a/toolkit/components/alerts/nsAlertsService.cpp b/toolkit/components/alerts/nsAlertsService.cpp
new file mode 100644
index 0000000000..35418dd176
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsService.cpp
@@ -0,0 +1,320 @@
+/* -*- 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/dom/ContentChild.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+#include "nsXULAppAPI.h"
+
+#include "nsAlertsService.h"
+
+#include "nsXPCOM.h"
+#include "nsIServiceManager.h"
+#include "nsIDOMWindow.h"
+#include "nsPromiseFlatString.h"
+#include "nsToolkitCompsCID.h"
+
+#ifdef MOZ_PLACES
+#include "mozIAsyncFavicons.h"
+#include "nsIFaviconService.h"
+#endif // MOZ_PLACES
+
+using namespace mozilla;
+
+using mozilla::dom::ContentChild;
+
+namespace {
+
+#ifdef MOZ_PLACES
+
+class IconCallback final : public nsIFaviconDataCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ IconCallback(nsIAlertsService* aBackend,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener)
+ : mBackend(aBackend)
+ , mAlert(aAlert)
+ , mAlertListener(aAlertListener)
+ {}
+
+ NS_IMETHOD
+ OnComplete(nsIURI *aIconURI, uint32_t aIconSize, const uint8_t *aIconData,
+ const nsACString &aMimeType) override
+ {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (aIconSize > 0) {
+ nsCOMPtr<nsIAlertsIconData> alertsIconData(do_QueryInterface(mBackend));
+ if (alertsIconData) {
+ rv = alertsIconData->ShowAlertWithIconData(mAlert, mAlertListener,
+ aIconSize, aIconData);
+ }
+ } else if (aIconURI) {
+ nsCOMPtr<nsIAlertsIconURI> alertsIconURI(do_QueryInterface(mBackend));
+ if (alertsIconURI) {
+ rv = alertsIconURI->ShowAlertWithIconURI(mAlert, mAlertListener,
+ aIconURI);
+ }
+ }
+ if (NS_FAILED(rv)) {
+ rv = mBackend->ShowAlert(mAlert, mAlertListener);
+ }
+ return rv;
+ }
+
+private:
+ virtual ~IconCallback() {}
+
+ nsCOMPtr<nsIAlertsService> mBackend;
+ nsCOMPtr<nsIAlertNotification> mAlert;
+ nsCOMPtr<nsIObserver> mAlertListener;
+};
+
+NS_IMPL_ISUPPORTS(IconCallback, nsIFaviconDataCallback)
+
+#endif // MOZ_PLACES
+
+nsresult
+ShowWithIconBackend(nsIAlertsService* aBackend, nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener)
+{
+#ifdef MOZ_PLACES
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aAlert->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv) || !uri) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Ensure the backend supports favicons.
+ nsCOMPtr<nsIAlertsIconData> alertsIconData(do_QueryInterface(aBackend));
+ nsCOMPtr<nsIAlertsIconURI> alertsIconURI;
+ if (!alertsIconData) {
+ alertsIconURI = do_QueryInterface(aBackend);
+ }
+ if (!alertsIconData && !alertsIconURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsCOMPtr<mozIAsyncFavicons> favicons(do_GetService(
+ "@mozilla.org/browser/favicon-service;1"));
+ NS_ENSURE_TRUE(favicons, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIFaviconDataCallback> callback =
+ new IconCallback(aBackend, aAlert, aAlertListener);
+ if (alertsIconData) {
+ return favicons->GetFaviconDataForPage(uri, callback);
+ }
+ return favicons->GetFaviconURLForPage(uri, callback);
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif // !MOZ_PLACES
+}
+
+nsresult
+ShowWithBackend(nsIAlertsService* aBackend, nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener, const nsAString& aPersistentData)
+{
+ if (!aPersistentData.IsEmpty()) {
+ return aBackend->ShowPersistentNotification(
+ aPersistentData, aAlert, aAlertListener);
+ }
+
+ if (Preferences::GetBool("alerts.showFavicons")) {
+ nsresult rv = ShowWithIconBackend(aBackend, aAlert, aAlertListener);
+ if (NS_SUCCEEDED(rv)) {
+ return rv;
+ }
+ }
+
+ // If favicons are disabled, or the backend doesn't support them, show the
+ // alert without one.
+ return aBackend->ShowAlert(aAlert, aAlertListener);
+}
+
+} // anonymous namespace
+
+NS_IMPL_ISUPPORTS(nsAlertsService, nsIAlertsService, nsIAlertsDoNotDisturb)
+
+nsAlertsService::nsAlertsService() :
+ mBackend(nullptr)
+{
+ mBackend = do_GetService(NS_SYSTEMALERTSERVICE_CONTRACTID);
+}
+
+nsAlertsService::~nsAlertsService()
+{}
+
+bool nsAlertsService::ShouldShowAlert()
+{
+ bool result = true;
+
+#ifdef XP_WIN
+ HMODULE shellDLL = ::LoadLibraryW(L"shell32.dll");
+ if (!shellDLL)
+ return result;
+
+ SHQueryUserNotificationStatePtr pSHQueryUserNotificationState =
+ (SHQueryUserNotificationStatePtr) ::GetProcAddress(shellDLL, "SHQueryUserNotificationState");
+
+ if (pSHQueryUserNotificationState) {
+ MOZ_QUERY_USER_NOTIFICATION_STATE qstate;
+ if (SUCCEEDED(pSHQueryUserNotificationState(&qstate))) {
+ if (qstate != QUNS_ACCEPTS_NOTIFICATIONS) {
+ result = false;
+ }
+ }
+ }
+
+ ::FreeLibrary(shellDLL);
+#endif
+
+ return result;
+}
+
+NS_IMETHODIMP nsAlertsService::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle,
+ const nsAString & aAlertText, bool aAlertTextClickable,
+ const nsAString & aAlertCookie,
+ nsIObserver * aAlertListener,
+ const nsAString & aAlertName,
+ const nsAString & aBidi,
+ const nsAString & aLang,
+ const nsAString & aData,
+ nsIPrincipal * aPrincipal,
+ bool aInPrivateBrowsing,
+ bool aRequireInteraction)
+{
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle,
+ aAlertText, aAlertTextClickable,
+ aAlertCookie, aBidi, aLang, aData,
+ aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowAlert(alert, aAlertListener);
+}
+
+
+NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification * aAlert,
+ nsIObserver * aAlertListener)
+{
+ return ShowPersistentNotification(EmptyString(), aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP nsAlertsService::ShowPersistentNotification(const nsAString & aPersistentData,
+ nsIAlertNotification * aAlert,
+ nsIObserver * aAlertListener)
+{
+ NS_ENSURE_ARG(aAlert);
+
+ nsAutoString cookie;
+ nsresult rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (XRE_IsContentProcess()) {
+ ContentChild* cpc = ContentChild::GetSingleton();
+
+ if (aAlertListener)
+ cpc->AddRemoteAlertObserver(cookie, aAlertListener);
+
+ cpc->SendShowAlert(aAlert);
+ return NS_OK;
+ }
+
+ // Check if there is an optional service that handles system-level notifications
+ if (mBackend) {
+ rv = ShowWithBackend(mBackend, aAlert, aAlertListener, aPersistentData);
+ if (NS_SUCCEEDED(rv)) {
+ return rv;
+ }
+ // If the system backend failed to show the alert, clear the backend and
+ // retry with XUL notifications. Future alerts will always use XUL.
+ mBackend = nullptr;
+ }
+
+ if (!ShouldShowAlert()) {
+ // Do not display the alert. Instead call alertfinished and get out.
+ if (aAlertListener)
+ aAlertListener->Observe(nullptr, "alertfinished", cookie.get());
+ return NS_OK;
+ }
+
+ // Use XUL notifications as a fallback if above methods have failed.
+ nsCOMPtr<nsIAlertsService> xulBackend(nsXULAlerts::GetInstance());
+ NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE);
+ return ShowWithBackend(xulBackend, aAlert, aAlertListener, aPersistentData);
+}
+
+NS_IMETHODIMP nsAlertsService::CloseAlert(const nsAString& aAlertName,
+ nsIPrincipal* aPrincipal)
+{
+ if (XRE_IsContentProcess()) {
+ ContentChild* cpc = ContentChild::GetSingleton();
+ cpc->SendCloseAlert(nsAutoString(aAlertName), IPC::Principal(aPrincipal));
+ return NS_OK;
+ }
+
+ nsresult rv;
+ // Try the system notification service.
+ if (mBackend) {
+ rv = mBackend->CloseAlert(aAlertName, aPrincipal);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If the system backend failed to close the alert, fall back to XUL for
+ // future alerts.
+ mBackend = nullptr;
+ }
+ } else {
+ nsCOMPtr<nsIAlertsService> xulBackend(nsXULAlerts::GetInstance());
+ NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE);
+ rv = xulBackend->CloseAlert(aAlertName, aPrincipal);
+ }
+ return rv;
+}
+
+
+// nsIAlertsDoNotDisturb
+NS_IMETHODIMP nsAlertsService::GetManualDoNotDisturb(bool* aRetVal)
+{
+#ifdef MOZ_WIDGET_ANDROID
+ return NS_ERROR_NOT_IMPLEMENTED;
+#else
+ nsCOMPtr<nsIAlertsDoNotDisturb> alertsDND(GetDNDBackend());
+ NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED);
+ return alertsDND->GetManualDoNotDisturb(aRetVal);
+#endif
+}
+
+NS_IMETHODIMP nsAlertsService::SetManualDoNotDisturb(bool aDoNotDisturb)
+{
+#ifdef MOZ_WIDGET_ANDROID
+ return NS_ERROR_NOT_IMPLEMENTED;
+#else
+ nsCOMPtr<nsIAlertsDoNotDisturb> alertsDND(GetDNDBackend());
+ NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED);
+
+ nsresult rv = alertsDND->SetManualDoNotDisturb(aDoNotDisturb);
+ if (NS_SUCCEEDED(rv)) {
+ Telemetry::Accumulate(Telemetry::ALERTS_SERVICE_DND_ENABLED, 1);
+ }
+ return rv;
+#endif
+}
+
+already_AddRefed<nsIAlertsDoNotDisturb>
+nsAlertsService::GetDNDBackend()
+{
+ // Try the system notification service.
+ nsCOMPtr<nsIAlertsService> backend = mBackend;
+ if (!backend) {
+ backend = nsXULAlerts::GetInstance();
+ }
+
+ nsCOMPtr<nsIAlertsDoNotDisturb> alertsDND(do_QueryInterface(backend));
+ return alertsDND.forget();
+}
diff --git a/toolkit/components/alerts/nsAlertsService.h b/toolkit/components/alerts/nsAlertsService.h
new file mode 100644
index 0000000000..3f23eaabfb
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsService.h
@@ -0,0 +1,48 @@
+// /* -*- 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 nsAlertsService_h__
+#define nsAlertsService_h__
+
+#include "nsIAlertsService.h"
+#include "nsCOMPtr.h"
+#include "nsXULAlerts.h"
+
+#ifdef XP_WIN
+typedef enum tagMOZ_QUERY_USER_NOTIFICATION_STATE {
+ QUNS_NOT_PRESENT = 1,
+ QUNS_BUSY = 2,
+ QUNS_RUNNING_D3D_FULL_SCREEN = 3,
+ QUNS_PRESENTATION_MODE = 4,
+ QUNS_ACCEPTS_NOTIFICATIONS = 5,
+ QUNS_QUIET_TIME = 6,
+ QUNS_IMMERSIVE = 7
+} MOZ_QUERY_USER_NOTIFICATION_STATE;
+
+extern "C" {
+// This function is Windows Vista or later
+typedef HRESULT (__stdcall *SHQueryUserNotificationStatePtr)(MOZ_QUERY_USER_NOTIFICATION_STATE *pquns);
+}
+#endif // defined(XP_WIN)
+
+class nsAlertsService : public nsIAlertsService,
+ public nsIAlertsDoNotDisturb
+{
+public:
+ NS_DECL_NSIALERTSDONOTDISTURB
+ NS_DECL_NSIALERTSSERVICE
+ NS_DECL_ISUPPORTS
+
+ nsAlertsService();
+
+protected:
+ virtual ~nsAlertsService();
+
+ bool ShouldShowAlert();
+ already_AddRefed<nsIAlertsDoNotDisturb> GetDNDBackend();
+ nsCOMPtr<nsIAlertsService> mBackend;
+};
+
+#endif /* nsAlertsService_h__ */
diff --git a/toolkit/components/alerts/nsAlertsUtils.cpp b/toolkit/components/alerts/nsAlertsUtils.cpp
new file mode 100644
index 0000000000..5f7d92d2aa
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsUtils.cpp
@@ -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/. */
+
+#include "nsAlertsUtils.h"
+
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIStringBundle.h"
+#include "nsIURI.h"
+#include "nsXPIDLString.h"
+
+/* static */
+bool
+nsAlertsUtils::IsActionablePrincipal(nsIPrincipal* aPrincipal)
+{
+ return aPrincipal &&
+ !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) &&
+ !aPrincipal->GetIsNullPrincipal();
+}
+
+/* static */
+void
+nsAlertsUtils::GetSourceHostPort(nsIPrincipal* aPrincipal,
+ nsAString& aHostPort)
+{
+ if (!IsActionablePrincipal(aPrincipal)) {
+ return;
+ }
+ nsCOMPtr<nsIURI> principalURI;
+ if (NS_WARN_IF(NS_FAILED(
+ aPrincipal->GetURI(getter_AddRefs(principalURI))))) {
+ return;
+ }
+ if (!principalURI) {
+ return;
+ }
+ nsAutoCString hostPort;
+ if (NS_WARN_IF(NS_FAILED(principalURI->GetHostPort(hostPort)))) {
+ return;
+ }
+ CopyUTF8toUTF16(hostPort, aHostPort);
+}
diff --git a/toolkit/components/alerts/nsAlertsUtils.h b/toolkit/components/alerts/nsAlertsUtils.h
new file mode 100644
index 0000000000..bc11f63516
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsUtils.h
@@ -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/. */
+
+#ifndef nsAlertsUtils_h
+#define nsAlertsUtils_h
+
+#include "nsIPrincipal.h"
+#include "nsString.h"
+
+class nsAlertsUtils final
+{
+private:
+ nsAlertsUtils() = delete;
+
+public:
+ /**
+ * Indicates whether an alert from |aPrincipal| should include the source
+ * string and action buttons. Returns false if |aPrincipal| is |nullptr|, or
+ * a system, expanded, or null principal.
+ */
+ static bool
+ IsActionablePrincipal(nsIPrincipal* aPrincipal);
+
+ /**
+ * Sets |aHostPort| to the host and port from |aPrincipal|'s URI, or an
+ * empty string if |aPrincipal| is not actionable.
+ */
+ static void
+ GetSourceHostPort(nsIPrincipal* aPrincipal, nsAString& aHostPort);
+};
+#endif /* nsAlertsUtils_h */
diff --git a/toolkit/components/alerts/nsIAlertsService.idl b/toolkit/components/alerts/nsIAlertsService.idl
new file mode 100644
index 0000000000..5629dabc93
--- /dev/null
+++ b/toolkit/components/alerts/nsIAlertsService.idl
@@ -0,0 +1,259 @@
+/* -*- 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"
+#include "nsIObserver.idl"
+
+interface imgIRequest;
+interface nsICancelable;
+interface nsIPrincipal;
+interface nsIURI;
+
+%{C++
+#define ALERT_NOTIFICATION_CONTRACTID "@mozilla.org/alert-notification;1"
+%}
+
+[scriptable, uuid(a71a637d-de1d-47c6-a8d2-c60b2596f471)]
+interface nsIAlertNotificationImageListener : nsISupports
+{
+ /**
+ * Called when the image finishes loading.
+ *
+ * @param aUserData An opaque parameter passed to |loadImage|.
+ * @param aRequest The image request.
+ */
+ void onImageReady(in nsISupports aUserData, in imgIRequest aRequest);
+
+ /**
+ * Called if the alert doesn't have an image, or if the image request times
+ * out or fails.
+ *
+ * @param aUserData An opaque parameter passed to |loadImage|.
+ */
+ void onImageMissing(in nsISupports aUserData);
+};
+
+[scriptable, uuid(cf2e4cb6-4b8f-4eca-aea9-d51a8f9f7a50)]
+interface nsIAlertNotification : nsISupports
+{
+ /** Initializes an alert notification. */
+ void init([optional] in AString aName,
+ [optional] in AString aImageURL,
+ [optional] in AString aTitle,
+ [optional] in AString aText,
+ [optional] in boolean aTextClickable,
+ [optional] in AString aCookie,
+ [optional] in AString aDir,
+ [optional] in AString aLang,
+ [optional] in AString aData,
+ [optional] in nsIPrincipal aPrincipal,
+ [optional] in boolean aInPrivateBrowsing,
+ [optional] in boolean aRequireInteraction);
+
+ /**
+ * The name of the notification. On Android, the name is hashed and used as
+ * a notification ID. Notifications will replace previous notifications with
+ * the same name.
+ */
+ readonly attribute AString name;
+
+ /**
+ * A URL identifying the image to put in the alert. The OS X backend limits
+ * the amount of time it will wait for the image to load to six seconds. After
+ * that time, the alert will show without an image.
+ */
+ readonly attribute AString imageURL;
+
+ /** The title for the alert. */
+ readonly attribute AString title;
+
+ /** The contents of the alert. */
+ readonly attribute AString text;
+
+ /**
+ * Controls the click behavior. If true, the alert listener will be notified
+ * when the user clicks on the alert.
+ */
+ readonly attribute boolean textClickable;
+
+ /**
+ * An opaque cookie that will be passed to the alert listener for each
+ * callback.
+ */
+ readonly attribute AString cookie;
+
+ /**
+ * Bidi override for the title and contents. Valid values are "auto", "ltr",
+ * or "rtl". Ignored if the backend doesn't support localization.
+ */
+ readonly attribute AString dir;
+
+ /**
+ * Language of the title and text. Ignored if the backend doesn't support
+ * localization.
+ */
+ readonly attribute AString lang;
+
+ /**
+ * A Base64-encoded structured clone buffer containing data associated with
+ * this alert. Only used for web notifications. Chrome callers should use a
+ * cookie instead.
+ */
+ readonly attribute AString data;
+
+ /**
+ * The principal of the page that created the alert. Used for IPC security
+ * checks, and to determine whether the alert is actionable.
+ */
+ readonly attribute nsIPrincipal principal;
+
+ /**
+ * The URI of the page that created the alert. |null| if the alert is not
+ * actionable.
+ */
+ readonly attribute nsIURI URI;
+
+ /**
+ * Controls the image loading behavior. If true, the image request will be
+ * loaded anonymously (without cookies or authorization tokens).
+ */
+ readonly attribute boolean inPrivateBrowsing;
+
+ /**
+ * Indicates that the notification should remain readily available until
+ * the user activates or dismisses the notification.
+ */
+ readonly attribute boolean requireInteraction;
+
+ /**
+ * Indicates whether this alert should show the source string and action
+ * buttons. False for system alerts (which can omit the principal), or
+ * expanded, system, and null principals.
+ */
+ readonly attribute boolean actionable;
+
+ /**
+ * The host and port of the originating page, or an empty string if the alert
+ * is not actionable.
+ */
+ readonly attribute AString source;
+
+ /**
+ * Loads the image associated with this alert.
+ *
+ * @param aTimeout The number of milliseconds to wait before cancelling the
+ * image request. If zero, there is no timeout.
+ * @param aListener An |nsIAlertNotificationImageListener| implementation,
+ * notified when the image loads. The listener is kept alive
+ * until the request completes.
+ * @param aUserData An opaque parameter passed to the listener's methods.
+ * Not used by the libnotify backend, but the OS X backend
+ * passes the pending notification.
+ */
+ nsICancelable loadImage(in unsigned long aTimeout,
+ in nsIAlertNotificationImageListener aListener,
+ [optional] in nsISupports aUserData);
+};
+
+[scriptable, uuid(f7a36392-d98b-4141-a7d7-4e46642684e3)]
+interface nsIAlertsService : nsISupports
+{
+ void showPersistentNotification(in AString aPersistentData,
+ in nsIAlertNotification aAlert,
+ [optional] in nsIObserver aAlertListener);
+
+ void showAlert(in nsIAlertNotification aAlert,
+ [optional] in nsIObserver aAlertListener);
+ /**
+ * Initializes and shows an |nsIAlertNotification| with the given parameters.
+ *
+ * @param aAlertListener Used for callbacks. May be null if the caller
+ * doesn't care about callbacks.
+ * @see nsIAlertNotification for descriptions of all other parameters.
+ * @throws NS_ERROR_NOT_AVAILABLE If the notification cannot be displayed.
+ *
+ * The following arguments will be passed to the alertListener's observe()
+ * method:
+ * subject - null
+ * topic - "alertfinished" when the alert goes away
+ * "alertdisablecallback" when alerts should be disabled for the principal
+ * "alertsettingscallback" when alert settings should be opened
+ * "alertclickcallback" when the text is clicked
+ * "alertshow" when the alert is shown
+ * data - the value of the cookie parameter passed to showAlertNotification.
+ *
+ * @note Depending on current circumstances (if the user's in a fullscreen
+ * application, for instance), the alert might not be displayed at all.
+ * In that case, if an alert listener is passed in it will receive the
+ * "alertfinished" notification immediately.
+ */
+ void showAlertNotification(in AString aImageURL,
+ in AString aTitle,
+ in AString aText,
+ [optional] in boolean aTextClickable,
+ [optional] in AString aCookie,
+ [optional] in nsIObserver aAlertListener,
+ [optional] in AString aName,
+ [optional] in AString aDir,
+ [optional] in AString aLang,
+ [optional] in AString aData,
+ [optional] in nsIPrincipal aPrincipal,
+ [optional] in boolean aInPrivateBrowsing,
+ [optional] in boolean aRequireInteraction);
+
+ /**
+ * Close alerts created by the service.
+ *
+ * @param aName The name of the notification to close. If no name
+ * is provided then only a notification created with
+ * no name (if any) will be closed.
+ */
+ void closeAlert([optional] in AString aName,
+ [optional] in nsIPrincipal aPrincipal);
+
+};
+
+[scriptable, uuid(c5d63e3a-259d-45a8-b964-8377967cb4d2)]
+interface nsIAlertsDoNotDisturb : nsISupports
+{
+ /**
+ * Toggles a manual Do Not Disturb mode for the service to reduce the amount
+ * of disruption that alerts cause the user.
+ * This may mean only displaying them in a notification tray/center or not
+ * displaying them at all. If a system backend already supports a similar
+ * feature controlled by the user, enabling this may not have any impact on
+ * code to show an alert. e.g. on OS X, the system will take care not
+ * disrupting a user if we simply create a notification like usual.
+ */
+ attribute bool manualDoNotDisturb;
+};
+
+[scriptable, uuid(fc6d7f0a-0cf6-4268-8c71-ab640842b9b1)]
+interface nsIAlertsIconData : nsISupports
+{
+ /**
+ * Shows an alert with an icon. Web notifications use the favicon of the
+ * page that created the alert. If the favicon is not in the Places database,
+ * |aIconSize| will be zero.
+ */
+ void showAlertWithIconData(in nsIAlertNotification aAlert,
+ [optional] in nsIObserver aAlertListener,
+ [optional] in uint32_t aIconSize,
+ [const, array, size_is(aIconSize)] in uint8_t
+ aIconData);
+};
+
+[scriptable, uuid(f3c82915-bf60-41ea-91ce-6c46b22e381a)]
+interface nsIAlertsIconURI : nsISupports
+{
+ /**
+ * Shows an alert with an icon URI. Web notifications use |moz-anno:|
+ * URIs to reference favicons from Places. If the page doesn't have a
+ * favicon, |aIconURI| will be |null|.
+ */
+ void showAlertWithIconURI(in nsIAlertNotification aAlert,
+ [optional] in nsIObserver aAlertListener,
+ [optional] in nsIURI aIconURI);
+};
diff --git a/toolkit/components/alerts/nsXULAlerts.cpp b/toolkit/components/alerts/nsXULAlerts.cpp
new file mode 100644
index 0000000000..882617637a
--- /dev/null
+++ b/toolkit/components/alerts/nsXULAlerts.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 "nsXULAlerts.h"
+
+#include "nsArray.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/dom/Notification.h"
+#include "mozilla/Unused.h"
+#include "nsIServiceManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWindowWatcher.h"
+
+using namespace mozilla;
+using mozilla::dom::NotificationTelemetryService;
+
+#define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul"
+
+namespace {
+StaticRefPtr<nsXULAlerts> gXULAlerts;
+} // anonymous namespace
+
+NS_IMPL_CYCLE_COLLECTION(nsXULAlertObserver, mAlertWindow)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULAlertObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULAlertObserver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULAlertObserver)
+
+NS_IMETHODIMP
+nsXULAlertObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ if (!strcmp("alertfinished", aTopic)) {
+ mozIDOMWindowProxy* currentAlert = mXULAlerts->mNamedWindows.GetWeak(mAlertName);
+ // The window in mNamedWindows might be a replacement, thus it should only
+ // be removed if it is the same window that is associated with this listener.
+ if (currentAlert == mAlertWindow) {
+ mXULAlerts->mNamedWindows.Remove(mAlertName);
+
+ if (mIsPersistent) {
+ mXULAlerts->PersistentAlertFinished();
+ }
+ }
+ }
+
+ nsresult rv = NS_OK;
+ if (mObserver) {
+ rv = mObserver->Observe(aSubject, aTopic, aData);
+ }
+ return rv;
+}
+
+// We don't cycle collect nsXULAlerts since gXULAlerts will keep the instance
+// alive till shutdown anyway.
+NS_IMPL_ISUPPORTS(nsXULAlerts, nsIAlertsService, nsIAlertsDoNotDisturb, nsIAlertsIconURI)
+
+/* static */ already_AddRefed<nsXULAlerts>
+nsXULAlerts::GetInstance()
+{
+ // Gecko on Android does not fully support XUL windows.
+#ifndef MOZ_WIDGET_ANDROID
+ if (!gXULAlerts) {
+ gXULAlerts = new nsXULAlerts();
+ ClearOnShutdown(&gXULAlerts);
+ }
+#endif // MOZ_WIDGET_ANDROID
+ RefPtr<nsXULAlerts> instance = gXULAlerts.get();
+ return instance.forget();
+}
+
+void
+nsXULAlerts::PersistentAlertFinished()
+{
+ MOZ_ASSERT(mPersistentAlertCount);
+ mPersistentAlertCount--;
+
+ // Show next pending persistent alert if any.
+ if (!mPendingPersistentAlerts.IsEmpty()) {
+ ShowAlertWithIconURI(mPendingPersistentAlerts[0].mAlert,
+ mPendingPersistentAlerts[0].mListener,
+ nullptr);
+ mPendingPersistentAlerts.RemoveElementAt(0);
+ }
+}
+
+NS_IMETHODIMP
+nsXULAlerts::ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle,
+ const nsAString& aAlertText, bool aAlertTextClickable,
+ const nsAString& aAlertCookie, nsIObserver* aAlertListener,
+ const nsAString& aAlertName, const nsAString& aBidi,
+ const nsAString& aLang, const nsAString& aData,
+ nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
+ bool aRequireInteraction)
+{
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle,
+ aAlertText, aAlertTextClickable,
+ aAlertCookie, aBidi, aLang, aData,
+ aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowAlert(alert, aAlertListener);
+}
+
+NS_IMETHODIMP
+nsXULAlerts::ShowPersistentNotification(const nsAString& aPersistentData,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener)
+{
+ return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
+nsXULAlerts::ShowAlert(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener)
+{
+ nsAutoString name;
+ nsresult rv = aAlert->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there is a pending alert with the same name in the list of
+ // pending alerts, replace it.
+ if (!mPendingPersistentAlerts.IsEmpty()) {
+ for (uint32_t i = 0; i < mPendingPersistentAlerts.Length(); i++) {
+ nsAutoString pendingAlertName;
+ nsCOMPtr<nsIAlertNotification> pendingAlert = mPendingPersistentAlerts[i].mAlert;
+ rv = pendingAlert->GetName(pendingAlertName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (pendingAlertName.Equals(name)) {
+ nsAutoString cookie;
+ rv = pendingAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mPendingPersistentAlerts[i].mListener) {
+ rv = mPendingPersistentAlerts[i].mListener->Observe(nullptr, "alertfinished", cookie.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mPendingPersistentAlerts[i].Init(aAlert, aAlertListener);
+ return NS_OK;
+ }
+ }
+ }
+
+ bool requireInteraction;
+ rv = aAlert->GetRequireInteraction(&requireInteraction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (requireInteraction &&
+ !mNamedWindows.Contains(name) &&
+ static_cast<int32_t>(mPersistentAlertCount) >=
+ Preferences::GetInt("dom.webnotifications.requireinteraction.count", 0)) {
+ PendingAlert* pa = mPendingPersistentAlerts.AppendElement();
+ pa->Init(aAlert, aAlertListener);
+ return NS_OK;
+ } else {
+ return ShowAlertWithIconURI(aAlert, aAlertListener, nullptr);
+ }
+}
+
+NS_IMETHODIMP
+nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener,
+ nsIURI* aIconURI)
+{
+ bool inPrivateBrowsing;
+ nsresult rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString cookie;
+ rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mDoNotDisturb) {
+ if (!inPrivateBrowsing) {
+ RefPtr<NotificationTelemetryService> telemetry =
+ NotificationTelemetryService::GetInstance();
+ if (telemetry) {
+ // Record the number of unique senders for XUL alerts. The OS X and
+ // libnotify backends will fire `alertshow` even if "do not disturb"
+ // is enabled. In that case, `NotificationObserver` will record the
+ // sender.
+ nsCOMPtr<nsIPrincipal> principal;
+ if (NS_SUCCEEDED(aAlert->GetPrincipal(getter_AddRefs(principal)))) {
+ Unused << NS_WARN_IF(NS_FAILED(telemetry->RecordSender(principal)));
+ }
+ }
+ }
+ if (aAlertListener)
+ aAlertListener->Observe(nullptr, "alertfinished", cookie.get());
+ return NS_OK;
+ }
+
+ nsAutoString name;
+ rv = aAlert->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString imageUrl;
+ rv = aAlert->GetImageURL(imageUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString title;
+ rv = aAlert->GetTitle(title);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString text;
+ rv = aAlert->GetText(text);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool textClickable;
+ rv = aAlert->GetTextClickable(&textClickable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString bidi;
+ rv = aAlert->GetDir(bidi);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString lang;
+ rv = aAlert->GetLang(lang);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString source;
+ rv = aAlert->GetSource(source);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool requireInteraction;
+ rv = aAlert->GetRequireInteraction(&requireInteraction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+
+ nsCOMPtr<nsIMutableArray> argsArray = nsArray::Create();
+
+ // create scriptable versions of our strings that we can store in our nsIMutableArray....
+ nsCOMPtr<nsISupportsString> scriptableImageUrl (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableImageUrl, NS_ERROR_FAILURE);
+
+ scriptableImageUrl->SetData(imageUrl);
+ rv = argsArray->AppendElement(scriptableImageUrl, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsString> scriptableAlertTitle (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableAlertTitle, NS_ERROR_FAILURE);
+
+ scriptableAlertTitle->SetData(title);
+ rv = argsArray->AppendElement(scriptableAlertTitle, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsString> scriptableAlertText (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableAlertText, NS_ERROR_FAILURE);
+
+ scriptableAlertText->SetData(text);
+ rv = argsArray->AppendElement(scriptableAlertText, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsPRBool> scriptableIsClickable (do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableIsClickable, NS_ERROR_FAILURE);
+
+ scriptableIsClickable->SetData(textClickable);
+ rv = argsArray->AppendElement(scriptableIsClickable, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsString> scriptableAlertCookie (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableAlertCookie, NS_ERROR_FAILURE);
+
+ scriptableAlertCookie->SetData(cookie);
+ rv = argsArray->AppendElement(scriptableAlertCookie, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsPRInt32> scriptableOrigin (do_CreateInstance(NS_SUPPORTS_PRINT32_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableOrigin, NS_ERROR_FAILURE);
+
+ int32_t origin =
+ LookAndFeel::GetInt(LookAndFeel::eIntID_AlertNotificationOrigin);
+ scriptableOrigin->SetData(origin);
+
+ rv = argsArray->AppendElement(scriptableOrigin, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsString> scriptableBidi (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableBidi, NS_ERROR_FAILURE);
+
+ scriptableBidi->SetData(bidi);
+ rv = argsArray->AppendElement(scriptableBidi, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsString> scriptableLang (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableLang, NS_ERROR_FAILURE);
+
+ scriptableLang->SetData(lang);
+ rv = argsArray->AppendElement(scriptableLang, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsPRBool> scriptableRequireInteraction (do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableRequireInteraction, NS_ERROR_FAILURE);
+
+ scriptableRequireInteraction->SetData(requireInteraction);
+ rv = argsArray->AppendElement(scriptableRequireInteraction, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Alerts with the same name should replace the old alert in the same position.
+ // Provide the new alert window with a pointer to the replaced window so that
+ // it may take the same position.
+ nsCOMPtr<nsISupportsInterfacePointer> replacedWindow = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv);
+ NS_ENSURE_TRUE(replacedWindow, NS_ERROR_FAILURE);
+ mozIDOMWindowProxy* previousAlert = mNamedWindows.GetWeak(name);
+ replacedWindow->SetData(previousAlert);
+ replacedWindow->SetDataIID(&NS_GET_IID(mozIDOMWindowProxy));
+ rv = argsArray->AppendElement(replacedWindow, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (requireInteraction) {
+ mPersistentAlertCount++;
+ }
+
+ // Add an observer (that wraps aAlertListener) to remove the window from
+ // mNamedWindows when it is closed.
+ nsCOMPtr<nsISupportsInterfacePointer> ifptr = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<nsXULAlertObserver> alertObserver = new nsXULAlertObserver(this, name, aAlertListener, requireInteraction);
+ nsCOMPtr<nsISupports> iSupports(do_QueryInterface(alertObserver));
+ ifptr->SetData(iSupports);
+ ifptr->SetDataIID(&NS_GET_IID(nsIObserver));
+ rv = argsArray->AppendElement(ifptr, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The source contains the host and port of the site that sent the
+ // notification. It is empty for system alerts.
+ nsCOMPtr<nsISupportsString> scriptableAlertSource (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableAlertSource, NS_ERROR_FAILURE);
+ scriptableAlertSource->SetData(source);
+ rv = argsArray->AppendElement(scriptableAlertSource, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsCString> scriptableIconURL (do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableIconURL, NS_ERROR_FAILURE);
+ if (aIconURI) {
+ nsAutoCString iconURL;
+ rv = aIconURI->GetSpec(iconURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ scriptableIconURL->SetData(iconURL);
+ }
+ rv = argsArray->AppendElement(scriptableIconURL, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIDOMWindowProxy> newWindow;
+ nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes");
+ if (inPrivateBrowsing) {
+ features.AppendLiteral(",private");
+ }
+ rv = wwatch->OpenWindow(nullptr, ALERT_CHROME_URL, "_blank", features.get(),
+ argsArray, getter_AddRefs(newWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mNamedWindows.Put(name, newWindow);
+ alertObserver->SetAlertWindow(newWindow);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsXULAlerts::SetManualDoNotDisturb(bool aDoNotDisturb)
+{
+ mDoNotDisturb = aDoNotDisturb;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsXULAlerts::GetManualDoNotDisturb(bool* aRetVal)
+{
+ *aRetVal = mDoNotDisturb;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsXULAlerts::CloseAlert(const nsAString& aAlertName,
+ nsIPrincipal* aPrincipal)
+{
+ mozIDOMWindowProxy* alert = mNamedWindows.GetWeak(aAlertName);
+ if (nsCOMPtr<nsPIDOMWindowOuter> domWindow = nsPIDOMWindowOuter::From(alert)) {
+ domWindow->DispatchCustomEvent(NS_LITERAL_STRING("XULAlertClose"));
+ }
+ return NS_OK;
+}
+
diff --git a/toolkit/components/alerts/nsXULAlerts.h b/toolkit/components/alerts/nsXULAlerts.h
new file mode 100644
index 0000000000..557716ee6b
--- /dev/null
+++ b/toolkit/components/alerts/nsXULAlerts.h
@@ -0,0 +1,84 @@
+/* -*- 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 nsXULAlerts_h__
+#define nsXULAlerts_h__
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsDataHashtable.h"
+#include "nsHashKeys.h"
+#include "nsInterfaceHashtable.h"
+
+#include "mozIDOMWindow.h"
+#include "nsIObserver.h"
+
+struct PendingAlert
+{
+ void Init(nsIAlertNotification* aAlert, nsIObserver* aListener)
+ {
+ mAlert = aAlert;
+ mListener = aListener;
+ }
+ nsCOMPtr<nsIAlertNotification> mAlert;
+ nsCOMPtr<nsIObserver> mListener;
+};
+
+class nsXULAlerts : public nsIAlertsService,
+ public nsIAlertsDoNotDisturb,
+ public nsIAlertsIconURI
+{
+ friend class nsXULAlertObserver;
+public:
+ NS_DECL_NSIALERTSICONURI
+ NS_DECL_NSIALERTSDONOTDISTURB
+ NS_DECL_NSIALERTSSERVICE
+ NS_DECL_ISUPPORTS
+
+ nsXULAlerts()
+ {
+ }
+
+ static already_AddRefed<nsXULAlerts> GetInstance();
+
+protected:
+ virtual ~nsXULAlerts() {}
+ void PersistentAlertFinished();
+
+ nsInterfaceHashtable<nsStringHashKey, mozIDOMWindowProxy> mNamedWindows;
+ uint32_t mPersistentAlertCount = 0;
+ nsTArray<PendingAlert> mPendingPersistentAlerts;
+ bool mDoNotDisturb = false;
+};
+
+/**
+ * This class wraps observers for alerts and watches
+ * for the "alertfinished" event in order to release
+ * the reference on the nsIDOMWindow of the XUL alert.
+ */
+class nsXULAlertObserver : public nsIObserver {
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsXULAlertObserver)
+
+ nsXULAlertObserver(nsXULAlerts* aXULAlerts, const nsAString& aAlertName,
+ nsIObserver* aObserver, bool aIsPersistent)
+ : mXULAlerts(aXULAlerts), mAlertName(aAlertName),
+ mObserver(aObserver), mIsPersistent(aIsPersistent) {}
+
+ void SetAlertWindow(mozIDOMWindowProxy* aWindow) { mAlertWindow = aWindow; }
+
+protected:
+ virtual ~nsXULAlertObserver() {}
+
+ RefPtr<nsXULAlerts> mXULAlerts;
+ nsString mAlertName;
+ nsCOMPtr<mozIDOMWindowProxy> mAlertWindow;
+ nsCOMPtr<nsIObserver> mObserver;
+ bool mIsPersistent;
+};
+
+#endif /* nsXULAlerts_h__ */
+
diff --git a/toolkit/components/alerts/resources/content/alert.css b/toolkit/components/alerts/resources/content/alert.css
new file mode 100644
index 0000000000..c4d94a543b
--- /dev/null
+++ b/toolkit/components/alerts/resources/content/alert.css
@@ -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/. */
+
+#alertBox[animate] {
+ animation-duration: 20s;
+ animation-fill-mode: both;
+ animation-name: alert-animation;
+}
+
+#alertBox[animate]:not([clicked]):not([closing]):hover {
+ animation-play-state: paused;
+}
+
+#alertBox:not([hasOrigin]) > box > #alertTextBox > #alertFooter,
+#alertBox:not([hasIcon]) > box > #alertIcon,
+#alertImage:not([src]) {
+ display: none;
+}
+
+#alertTitleBox {
+ -moz-box-pack: center;
+ -moz-box-align: center;
+}
+
+.alertText {
+ white-space: pre-wrap;
+}
+
+@keyframes alert-animation {
+ to {
+ visibility: hidden;
+ }
+}
diff --git a/toolkit/components/alerts/resources/content/alert.js b/toolkit/components/alerts/resources/content/alert.js
new file mode 100644
index 0000000000..523ec378e7
--- /dev/null
+++ b/toolkit/components/alerts/resources/content/alert.js
@@ -0,0 +1,332 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Copied from nsILookAndFeel.h, see comments on eMetric_AlertNotificationOrigin
+const NS_ALERT_HORIZONTAL = 1;
+const NS_ALERT_LEFT = 2;
+const NS_ALERT_TOP = 4;
+
+const WINDOW_MARGIN = AppConstants.platform == "win" ? 0 : 10;
+const BODY_TEXT_LIMIT = 200;
+const WINDOW_SHADOW_SPREAD = AppConstants.platform == "win" ? 10 : 0;
+
+
+var gOrigin = 0; // Default value: alert from bottom right.
+var gReplacedWindow = null;
+var gAlertListener = null;
+var gAlertTextClickable = false;
+var gAlertCookie = "";
+var gIsReplaced = false;
+var gRequireInteraction = false;
+
+function prefillAlertInfo() {
+ // unwrap all the args....
+ // arguments[0] --> the image src url
+ // arguments[1] --> the alert title
+ // arguments[2] --> the alert text
+ // arguments[3] --> is the text clickable?
+ // arguments[4] --> the alert cookie to be passed back to the listener
+ // arguments[5] --> the alert origin reported by the look and feel
+ // arguments[6] --> bidi
+ // arguments[7] --> lang
+ // arguments[8] --> requires interaction
+ // arguments[9] --> replaced alert window (nsIDOMWindow)
+ // arguments[10] --> an optional callback listener (nsIObserver)
+ // arguments[11] -> the nsIURI.hostPort of the origin, optional
+ // arguments[12] -> the alert icon URL, optional
+
+ switch (window.arguments.length) {
+ default:
+ case 13: {
+ if (window.arguments[12]) {
+ let alertBox = document.getElementById("alertBox");
+ alertBox.setAttribute("hasIcon", true);
+
+ let icon = document.getElementById("alertIcon");
+ icon.src = window.arguments[12];
+ }
+ }
+ case 12: {
+ if (window.arguments[11]) {
+ let alertBox = document.getElementById("alertBox");
+ alertBox.setAttribute("hasOrigin", true);
+
+ let hostPort = window.arguments[11];
+ const ALERT_BUNDLE = Services.strings.createBundle(
+ "chrome://alerts/locale/alert.properties");
+ const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties");
+ const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+ let label = document.getElementById("alertSourceLabel");
+ label.setAttribute("value",
+ ALERT_BUNDLE.formatStringFromName("source.label",
+ [hostPort],
+ 1));
+ let doNotDisturbMenuItem = document.getElementById("doNotDisturbMenuItem");
+ doNotDisturbMenuItem.setAttribute("label",
+ ALERT_BUNDLE.formatStringFromName("doNotDisturb.label",
+ [BRAND_NAME],
+ 1));
+ let disableForOrigin = document.getElementById("disableForOriginMenuItem");
+ disableForOrigin.setAttribute("label",
+ ALERT_BUNDLE.formatStringFromName("webActions.disableForOrigin.label",
+ [hostPort],
+ 1));
+ let openSettings = document.getElementById("openSettingsMenuItem");
+ openSettings.setAttribute("label",
+ ALERT_BUNDLE.GetStringFromName("webActions.settings.label"));
+ }
+ }
+ case 11:
+ gAlertListener = window.arguments[10];
+ case 10:
+ gReplacedWindow = window.arguments[9];
+ case 9:
+ gRequireInteraction = window.arguments[8];
+ case 8:
+ if (window.arguments[7]) {
+ document.getElementById("alertTitleLabel").setAttribute("lang", window.arguments[7]);
+ document.getElementById("alertTextLabel").setAttribute("lang", window.arguments[7]);
+ }
+ case 7:
+ if (window.arguments[6]) {
+ document.getElementById("alertNotification").style.direction = window.arguments[6];
+ }
+ case 6:
+ gOrigin = window.arguments[5];
+ case 5:
+ gAlertCookie = window.arguments[4];
+ case 4:
+ gAlertTextClickable = window.arguments[3];
+ if (gAlertTextClickable) {
+ document.getElementById("alertNotification").setAttribute("clickable", true);
+ document.getElementById("alertTextLabel").setAttribute("clickable", true);
+ }
+ case 3:
+ if (window.arguments[2]) {
+ document.getElementById("alertBox").setAttribute("hasBodyText", true);
+ let bodyText = window.arguments[2];
+ let bodyTextLabel = document.getElementById("alertTextLabel");
+
+ if (bodyText.length > BODY_TEXT_LIMIT) {
+ bodyTextLabel.setAttribute("tooltiptext", bodyText);
+
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue("intl.ellipsis",
+ Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+
+ // Copied from nsContextMenu.js' formatSearchContextItem().
+ // If the JS character after our truncation point is a trail surrogate,
+ // include it in the truncated string to avoid splitting a surrogate pair.
+ let truncLength = BODY_TEXT_LIMIT;
+ let truncChar = bodyText[BODY_TEXT_LIMIT].charCodeAt(0);
+ if (truncChar >= 0xDC00 && truncChar <= 0xDFFF) {
+ truncLength++;
+ }
+
+ bodyText = bodyText.substring(0, truncLength) +
+ ellipsis;
+ }
+ bodyTextLabel.textContent = bodyText;
+ }
+ case 2:
+ document.getElementById("alertTitleLabel").setAttribute("value", window.arguments[1]);
+ case 1:
+ if (window.arguments[0]) {
+ document.getElementById("alertBox").setAttribute("hasImage", true);
+ document.getElementById("alertImage").setAttribute("src", window.arguments[0]);
+ }
+ case 0:
+ break;
+ }
+}
+
+function onAlertLoad() {
+ const ALERT_DURATION_IMMEDIATE = 20000;
+ let alertTextBox = document.getElementById("alertTextBox");
+ let alertImageBox = document.getElementById("alertImageBox");
+ alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px";
+
+ sizeToContent();
+
+ if (gReplacedWindow && !gReplacedWindow.closed) {
+ moveWindowToReplace(gReplacedWindow);
+ gReplacedWindow.gIsReplaced = true;
+ gReplacedWindow.close();
+ } else {
+ moveWindowToEnd();
+ }
+
+ window.addEventListener("XULAlertClose", function() { window.close(); });
+
+ // If the require interaction flag is set, prevent auto-closing the notification.
+ if (!gRequireInteraction) {
+ if (Services.prefs.getBoolPref("alerts.disableSlidingEffect")) {
+ setTimeout(function() { window.close(); }, ALERT_DURATION_IMMEDIATE);
+ } else {
+ let alertBox = document.getElementById("alertBox");
+ alertBox.addEventListener("animationend", function hideAlert(event) {
+ if (event.animationName == "alert-animation" ||
+ event.animationName == "alert-clicked-animation" ||
+ event.animationName == "alert-closing-animation") {
+ alertBox.removeEventListener("animationend", hideAlert, false);
+ window.close();
+ }
+ }, false);
+ alertBox.setAttribute("animate", true);
+ }
+ }
+
+ let alertSettings = document.getElementById("alertSettings");
+ alertSettings.addEventListener("focus", onAlertSettingsFocus);
+ alertSettings.addEventListener("click", onAlertSettingsClick);
+
+ let ev = new CustomEvent("AlertActive", {bubbles: true, cancelable: true});
+ document.documentElement.dispatchEvent(ev);
+
+ if (gAlertListener) {
+ gAlertListener.observe(null, "alertshow", gAlertCookie);
+ }
+}
+
+function moveWindowToReplace(aReplacedAlert) {
+ let heightDelta = window.outerHeight - aReplacedAlert.outerHeight;
+
+ // Move windows that come after the replaced alert if the height is different.
+ if (heightDelta != 0) {
+ let windows = Services.wm.getEnumerator("alert:alert");
+ while (windows.hasMoreElements()) {
+ let alertWindow = windows.getNext();
+ // boolean to determine if the alert window is after the replaced alert.
+ let alertIsAfter = gOrigin & NS_ALERT_TOP ?
+ alertWindow.screenY > aReplacedAlert.screenY :
+ aReplacedAlert.screenY > alertWindow.screenY;
+ if (alertIsAfter) {
+ // The new Y position of the window.
+ let adjustedY = gOrigin & NS_ALERT_TOP ?
+ alertWindow.screenY + heightDelta :
+ alertWindow.screenY - heightDelta;
+ alertWindow.moveTo(alertWindow.screenX, adjustedY);
+ }
+ }
+ }
+
+ let adjustedY = gOrigin & NS_ALERT_TOP ? aReplacedAlert.screenY :
+ aReplacedAlert.screenY - heightDelta;
+ window.moveTo(aReplacedAlert.screenX, adjustedY);
+}
+
+function moveWindowToEnd() {
+ // Determine position
+ let x = gOrigin & NS_ALERT_LEFT ? screen.availLeft :
+ screen.availLeft + screen.availWidth - window.outerWidth;
+ let y = gOrigin & NS_ALERT_TOP ? screen.availTop :
+ screen.availTop + screen.availHeight - window.outerHeight;
+
+ // Position the window at the end of all alerts.
+ let windows = Services.wm.getEnumerator("alert:alert");
+ while (windows.hasMoreElements()) {
+ let alertWindow = windows.getNext();
+ if (alertWindow != window) {
+ if (gOrigin & NS_ALERT_TOP) {
+ y = Math.max(y, alertWindow.screenY + alertWindow.outerHeight - WINDOW_SHADOW_SPREAD);
+ } else {
+ y = Math.min(y, alertWindow.screenY - window.outerHeight + WINDOW_SHADOW_SPREAD);
+ }
+ }
+ }
+
+ // Offset the alert by WINDOW_MARGIN pixels from the edge of the screen
+ y += gOrigin & NS_ALERT_TOP ? WINDOW_MARGIN : -WINDOW_MARGIN;
+ x += gOrigin & NS_ALERT_LEFT ? WINDOW_MARGIN : -WINDOW_MARGIN;
+
+ window.moveTo(x, y);
+}
+
+function onAlertBeforeUnload() {
+ if (!gIsReplaced) {
+ // Move other alert windows to fill the gap left by closing alert.
+ let heightDelta = window.outerHeight + WINDOW_MARGIN - WINDOW_SHADOW_SPREAD;
+ let windows = Services.wm.getEnumerator("alert:alert");
+ while (windows.hasMoreElements()) {
+ let alertWindow = windows.getNext();
+ if (alertWindow != window) {
+ if (gOrigin & NS_ALERT_TOP) {
+ if (alertWindow.screenY > window.screenY) {
+ alertWindow.moveTo(alertWindow.screenX, alertWindow.screenY - heightDelta);
+ }
+ } else if (window.screenY > alertWindow.screenY) {
+ alertWindow.moveTo(alertWindow.screenX, alertWindow.screenY + heightDelta);
+ }
+ }
+ }
+ }
+
+ if (gAlertListener) {
+ gAlertListener.observe(null, "alertfinished", gAlertCookie);
+ }
+}
+
+function onAlertClick() {
+ if (gAlertListener && gAlertTextClickable) {
+ gAlertListener.observe(null, "alertclickcallback", gAlertCookie);
+ }
+
+ let alertBox = document.getElementById("alertBox");
+ if (alertBox.getAttribute("animate") == "true") {
+ // Closed when the animation ends.
+ alertBox.setAttribute("clicked", "true");
+ } else {
+ window.close();
+ }
+}
+
+function doNotDisturb() {
+ const alertService = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService)
+ .QueryInterface(Ci.nsIAlertsDoNotDisturb);
+ alertService.manualDoNotDisturb = true;
+ Services.telemetry.getHistogramById("WEB_NOTIFICATION_MENU")
+ .add(0);
+ onAlertClose();
+}
+
+function disableForOrigin() {
+ gAlertListener.observe(null, "alertdisablecallback", gAlertCookie);
+ onAlertClose();
+}
+
+function onAlertSettingsFocus(event) {
+ event.target.removeAttribute("focusedViaMouse");
+}
+
+function onAlertSettingsClick(event) {
+ // XXXjaws Hack used to remove the focus-ring only
+ // from mouse interaction, but focus-ring drawing
+ // should only be enabled when interacting via keyboard.
+ event.target.setAttribute("focusedViaMouse", true);
+ event.stopPropagation();
+}
+
+function openSettings() {
+ gAlertListener.observe(null, "alertsettingscallback", gAlertCookie);
+ onAlertClose();
+}
+
+function onAlertClose() {
+ let alertBox = document.getElementById("alertBox");
+ if (alertBox.getAttribute("animate") == "true") {
+ // Closed when the animation ends.
+ alertBox.setAttribute("closing", "true");
+ } else {
+ window.close();
+ }
+}
diff --git a/toolkit/components/alerts/resources/content/alert.xul b/toolkit/components/alerts/resources/content/alert.xul
new file mode 100644
index 0000000000..8597d9954d
--- /dev/null
+++ b/toolkit/components/alerts/resources/content/alert.xul
@@ -0,0 +1,67 @@
+<?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 window [
+<!ENTITY % alertDTD SYSTEM "chrome://alerts/locale/alert.dtd">
+%alertDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/content/alerts/alert.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/alerts/alert.css" type="text/css"?>
+
+<window id="alertNotification"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="alert:alert"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ role="alert"
+ pack="start"
+ onload="onAlertLoad();"
+ onclick="onAlertClick();"
+ onbeforeunload="onAlertBeforeUnload();">
+
+ <script type="application/javascript" src="chrome://global/content/alerts/alert.js"/>
+
+ <vbox id="alertBox" class="alertBox">
+ <box id="alertTitleBox">
+ <image id="alertIcon"/>
+ <label id="alertTitleLabel" class="alertTitle plain" crop="end"/>
+ <vbox class="alertCloseBox">
+ <toolbarbutton class="alertCloseButton close-icon"
+ tooltiptext="&closeAlert.tooltip;"
+ onclick="event.stopPropagation();"
+ oncommand="onAlertClose();"/>
+ </vbox>
+ </box>
+ <box>
+ <hbox id="alertImageBox" class="alertImageBox" align="center" pack="center">
+ <image id="alertImage"/>
+ </hbox>
+
+ <vbox id="alertTextBox" class="alertTextBox">
+ <label id="alertTextLabel" class="alertText plain"/>
+ <spacer flex="1"/>
+ <box id="alertFooter">
+ <label id="alertSourceLabel" class="alertSource plain"/>
+ <button type="menu" id="alertSettings" tooltiptext="&settings.label;">
+ <menupopup position="after_end">
+ <menuitem id="doNotDisturbMenuItem"
+ oncommand="doNotDisturb();"/>
+ <menuseparator/>
+ <menuitem id="disableForOriginMenuItem"
+ oncommand="disableForOrigin();"/>
+ <menuitem id="openSettingsMenuItem"
+ oncommand="openSettings();"/>
+ </menupopup>
+ </button>
+ </box>
+ </vbox>
+ </box>
+ </vbox>
+
+ <!-- This method is called inline because we want to make sure we establish the width
+ and height of the alert before we fire the onload handler. -->
+ <script type="application/javascript">prefillAlertInfo();</script>
+</window>
+
diff --git a/toolkit/components/alerts/test/.eslintrc.js b/toolkit/components/alerts/test/.eslintrc.js
new file mode 100644
index 0000000000..3c788d6d68
--- /dev/null
+++ b/toolkit/components/alerts/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/alerts/test/image.gif b/toolkit/components/alerts/test/image.gif
new file mode 100644
index 0000000000..053b4d9261
--- /dev/null
+++ b/toolkit/components/alerts/test/image.gif
Binary files differ
diff --git a/toolkit/components/alerts/test/image.png b/toolkit/components/alerts/test/image.png
new file mode 100644
index 0000000000..430c3c5e65
--- /dev/null
+++ b/toolkit/components/alerts/test/image.png
Binary files differ
diff --git a/toolkit/components/alerts/test/image_server.sjs b/toolkit/components/alerts/test/image_server.sjs
new file mode 100644
index 0000000000..6220529439
--- /dev/null
+++ b/toolkit/components/alerts/test/image_server.sjs
@@ -0,0 +1,82 @@
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC } = Components;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+const LocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
+ "initWithPath");
+
+const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
+ "nsIFileInputStream", "init");
+
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream", "setInputStream");
+
+function handleRequest(request, response) {
+ let params = parseQueryString(request.queryString);
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ // Compare and increment a cookie for this request. This is used to test
+ // private browsing mode; the cookie should not be set if the image is
+ // loaded anonymously.
+ if (params.has("c")) {
+ let expectedValue = parseInt(params.get("c"), 10);
+ let actualValue = !request.hasHeader("Cookie") ? 0 :
+ parseInt(request.getHeader("Cookie")
+ .replace(/^counter=(\d+)/, "$1"), 10);
+ if (actualValue != expectedValue) {
+ response.setStatusLine(request.httpVersion, 400, "Wrong counter value");
+ return;
+ }
+ response.setHeader("Set-Cookie", `counter=${expectedValue + 1}`, false);
+ }
+
+ // Wait to send the image if a timeout is given.
+ let timeout = parseInt(params.get("t"), 10);
+ if (timeout > 0) {
+ response.processAsync();
+ setTimeout(() => {
+ respond(params, request, response);
+ response.finish();
+ }, timeout * 1000);
+ return;
+ }
+
+ respond(params, request, response);
+}
+
+function parseQueryString(queryString) {
+ return queryString.split("&").reduce((params, param) => {
+ let [key, value] = param.split("=", 2);
+ params.set(key, value);
+ return params;
+ }, new Map());
+}
+
+function respond(params, request, response) {
+ if (params.has("s")) {
+ let statusCode = parseInt(params.get("s"), 10);
+ response.setStatusLine(request.httpVersion, statusCode, "Custom status");
+ return;
+ }
+ var filename = params.get("f");
+ writeFile(filename, response);
+}
+
+function writeFile(name, response) {
+ var file = new LocalFile(getState("__LOCATION__")).parent;
+ file.append(name);
+
+ let mimeType = Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromFile(file);
+
+ let fileStream = new FileInputStream(file, 1, 0, false);
+ let binaryStream = new BinaryInputStream(fileStream);
+
+ response.setHeader("Content-Type", mimeType, false);
+ response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available());
+
+ binaryStream.close();
+ fileStream.close();
+}
diff --git a/toolkit/components/alerts/test/mochitest.ini b/toolkit/components/alerts/test/mochitest.ini
new file mode 100644
index 0000000000..12e2a87040
--- /dev/null
+++ b/toolkit/components/alerts/test/mochitest.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ image.gif
+ image.png
+ image_server.sjs
+
+# Synchronous tests like test_alerts.html must come before
+# asynchronous tests like test_alerts_noobserve.html!
+[test_alerts.html]
+skip-if = toolkit == 'android'
+[test_alerts_noobserve.html]
+[test_alerts_requireinteraction.html]
+[test_image.html]
+[test_multiple_alerts.html]
+[test_principal.html]
+skip-if = toolkit == 'android'
diff --git a/toolkit/components/alerts/test/test_alerts.html b/toolkit/components/alerts/test/test_alerts.html
new file mode 100644
index 0000000000..cb087e48ac
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test for Alerts Service</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<br>Alerts service, with observer "synchronous" case.
+<br>
+<br>Did a notification appear anywhere?
+<br>If so, the test will finish once the notification disappears.
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var observer = {
+ alertShow: false,
+ observe: function (aSubject, aTopic, aData) {
+ is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly");
+ if (aTopic == "alertclickcallback") {
+ todo(false, "Did someone click the notification while running mochitests? (Please don't.)");
+ } else if (aTopic == "alertshow") {
+ ok(!this.alertShow, "Alert should not be shown more than once");
+ this.alertShow = true;
+ } else {
+ is(aTopic, "alertfinished", "Checking the topic for a finished notification");
+ SimpleTest.finish();
+ }
+ }
+};
+
+function runTest() {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application");
+
+ var notifier;
+ try {
+ notifier = Cc["@mozilla.org/alerts-service;1"].
+ getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available");
+ } catch (ex) {
+ todo(false,
+ "Alerts service is not available.", ex);
+ return;
+ }
+
+ try {
+ var alertName = "fiorello";
+ SimpleTest.waitForExplicitFinish();
+ notifier.showAlertNotification(null, "Notification test",
+ "Surprise! I'm here to test notifications!",
+ false, "foobarcookie", observer, alertName);
+ ok(true, "showAlertNotification() succeeded. Waiting for notification...");
+
+ if ("@mozilla.org/system-alerts-service;1" in Cc) {
+ // Notifications are native on OS X 10.8 and later, as well as GNOME
+ // Shell with libnotify (bug 1236036). These notifications persist in the
+ // Notification Center, and only fire the `alertfinished` event when
+ // closed. For platforms where native notifications may be used, we need
+ // to close explicitly to avoid a hang. This also works for XUL
+ // notifications when running this test on OS X < 10.8, or a window
+ // manager like Ubuntu Unity with incomplete libnotify support.
+ notifier.closeAlert(alertName);
+ }
+ } catch (ex) {
+ todo(false, "showAlertNotification() failed.", ex);
+ SimpleTest.finish();
+ }
+}
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/alerts/test/test_alerts_noobserve.html b/toolkit/components/alerts/test/test_alerts_noobserve.html
new file mode 100644
index 0000000000..0cc452b8af
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts_noobserve.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test for Alerts Service</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<br>Alerts service, without observer "asynchronous" case.
+<br>
+<br>A notification should soon appear somewhere.
+<br>If there has been no crash when the notification (later) disappears, assume all is good.
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ const { utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Timer.jsm");
+
+ function anyXULAlertsVisible() {
+ var windows = Services.wm.getEnumerator("alert:alert");
+ return windows.hasMoreElements();
+ }
+
+ addMessageListener("anyXULAlertsVisible", anyXULAlertsVisible);
+
+ addMessageListener("waitForAlerts", function waitForAlerts() {
+ if (anyXULAlertsVisible()) {
+ setTimeout(waitForAlerts, 1000);
+ } else {
+ sendAsyncMessage("waitedForAlerts");
+ }
+ });
+});
+
+function waitForAlertsThenFinish() {
+ chromeScript.addMessageListener("waitedForAlerts", function waitedForAlerts() {
+ chromeScript.removeMessageListener("waitedForAlerts", waitedForAlerts);
+ ok(true, "Alert disappeared.");
+ SimpleTest.finish();
+ });
+ chromeScript.sendAsyncMessage("waitForAlerts");
+}
+
+function runTest() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application");
+ } else {
+ ok(true, "Alerts service exists in this application");
+
+ var notifier;
+ try {
+ notifier = Cc["@mozilla.org/alerts-service;1"].
+ getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available");
+ } catch (ex) {
+ todo(false, "Alerts service is not available.", ex);
+ }
+
+ if (notifier) {
+ try {
+ notifier.showAlertNotification(null, "Notification test",
+ "This notification has no observer");
+ ok(true, "showAlertNotification() succeeded");
+ } catch (ex) {
+ todo(false, "showAlertNotification() failed.", ex);
+ }
+ }
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+// sendSyncMessage returns an array of arrays: the outer array is from the
+// message manager, and the inner array is from the chrome script's listeners.
+// See the comment in test_SpecialPowersLoadChromeScript.html.
+var [[alertsVisible]] = chromeScript.sendSyncMessage("anyXULAlertsVisible");
+ok(!alertsVisible, "Alerts should not be present at the start of the test.");
+runTest();
+setTimeout(waitForAlertsThenFinish, 1000);
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/alerts/test/test_alerts_requireinteraction.html b/toolkit/components/alerts/test/test_alerts_requireinteraction.html
new file mode 100644
index 0000000000..26fe871046
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts_requireinteraction.html
@@ -0,0 +1,168 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for alerts with requireInteraction</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ const { utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Timer.jsm");
+
+ addMessageListener("waitForXULAlert", function() {
+ var timer = setTimeout(function() {
+ Services.ww.unregisterNotification(windowObserver);
+ sendAsyncMessage("waitForXULAlert", false);
+ }, 2000);
+
+ var windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ if (windowType == "alert:alert") {
+ clearTimeout(timer);
+ Services.ww.unregisterNotification(windowObserver);
+
+ sendAsyncMessage("waitForXULAlert", true);
+ }
+ });
+ };
+
+ Services.ww.registerNotification(windowObserver);
+ });
+});
+
+var cookie = 0;
+function promiseCreateXULAlert(alertService, listener, name) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("waitForXULAlert", function waitedForAlert(result) {
+ chromeScript.removeMessageListener("waitForXULAlert", waitedForAlert);
+ resolve(result);
+ });
+
+ chromeScript.sendAsyncMessage("waitForXULAlert");
+ alertService.showAlertNotification(null, "title", "body",
+ true, cookie++, listener, name, null, null, null,
+ null, false, true);
+ });
+}
+
+add_task(function* test_require_interaction() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application.");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application.");
+
+ var alertService;
+ try {
+ alertService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available.");
+ } catch (ex) {
+ todo(false, "Alerts service is not available.");
+ return;
+ }
+
+ yield SpecialPowers.pushPrefEnv({"set": [
+ [ "dom.webnotifications.requireinteraction.enabled", true ],
+ [ "dom.webnotifications.requireinteraction.count", 2 ]
+ ]});
+
+ var expectedSequence = [
+ "first show",
+ "second show",
+ "second finished",
+ "second replacement show",
+ "third finished",
+ "first finished",
+ "third replacement show",
+ "second replacement finished",
+ "third replacement finished"
+ ];
+
+ var actualSequence = [];
+
+ function createAlertListener(name, showCallback, finishCallback) {
+ return (subject, topic, data) => {
+ if (topic == "alertshow") {
+ actualSequence.push(name + " show");
+ if (showCallback) {
+ showCallback();
+ }
+ } else if (topic == "alertfinished") {
+ actualSequence.push(name + " finished");
+ if (finishCallback) {
+ finishCallback();
+ }
+ }
+ }
+ }
+
+ var xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("first"), "first");
+ if (!xulAlertCreated) {
+ ok(true, "Platform does not use XUL alerts.");
+ alertService.closeAlert("first");
+ return;
+ }
+
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("second"), "second");
+ ok(xulAlertCreated, "Create XUL alert");
+
+ // Replace second alert
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("second replacement"), "second");
+ ok(xulAlertCreated, "Create XUL alert");
+
+ var testFinishResolve;
+ var testFinishPromise = new Promise((resolve) => { testFinishResolve = resolve; });
+
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("third"), "third"),
+ ok(!xulAlertCreated, "XUL alert should not be visible");
+
+ // Replace the not-yet-visible third alert.
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("third replacement",
+ function showCallback() {
+ alertService.closeAlert("second");
+ alertService.closeAlert("third");
+ },
+ function finishCallback() {
+ // Check actual sequence of alert events compared to expected sequence.
+ for (var i = 0; i < actualSequence.length; i++) {
+ is(actualSequence[i], expectedSequence[i],
+ "Alert callback at index " + i + " should be in expected order.");
+ }
+
+ testFinishResolve();
+ }), "third");
+
+ ok(!xulAlertCreated, "XUL alert should not be visible");
+
+ alertService.closeAlert("first");
+
+ yield testFinishPromise;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/alerts/test/test_image.html b/toolkit/components/alerts/test/test_image.html
new file mode 100644
index 0000000000..7bf89fab22
--- /dev/null
+++ b/toolkit/components/alerts/test/test_image.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1233086</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Services = SpecialPowers.Services;
+
+const imageServerURL = "http://mochi.test:8888/tests/toolkit/components/alerts/test/image_server.sjs";
+
+function makeAlert(...params) {
+ var alert = Cc["@mozilla.org/alert-notification;1"]
+ .createInstance(Ci.nsIAlertNotification);
+ alert.init(...params);
+ return alert;
+}
+
+function promiseImage(alert, timeout = 0, userData = null) {
+ return new Promise(resolve => {
+ var isDone = false;
+ function done(value) {
+ ok(!isDone, "Should call the image listener once");
+ isDone = true;
+ resolve(value);
+ }
+ alert.loadImage(timeout, SpecialPowers.wrapCallbackObject({
+ onImageReady(aUserData, aRequest) {
+ done([true, aRequest, aUserData]);
+ },
+ onImageMissing(aUserData) {
+ done([false, aUserData]);
+ },
+ }), SpecialPowers.wrap(userData));
+ });
+}
+
+add_task(function* testContext() {
+ var inUserData = Cc["@mozilla.org/supports-PRInt64;1"]
+ .createInstance(Ci.nsISupportsPRInt64);
+ inUserData.data = 123;
+
+ var alert = makeAlert(null, imageServerURL + "?f=image.png");
+ var [ready, , userData] = yield promiseImage(alert, 0, inUserData);
+ ok(ready, "Should load requested image");
+ is(userData.QueryInterface(Ci.nsISupportsPRInt64).data, 123,
+ "Should pass user data for loaded image");
+
+ alert = makeAlert(null, imageServerURL + "?s=404");
+ [ready, userData] = yield promiseImage(alert, 0, inUserData);
+ ok(!ready, "Should not load missing image");
+ is(userData.QueryInterface(Ci.nsISupportsPRInt64).data, 123,
+ "Should pass user data for missing image");
+});
+
+add_task(function* testTimeout() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.png&t=3");
+ var [ready] = yield promiseImage(alert, 1000);
+ ok(!ready, "Should cancel request if timeout fires");
+
+ [ready, request] = yield promiseImage(alert, 45000);
+ ok(ready, "Should load image if request finishes before timeout");
+});
+
+add_task(function* testAnimatedGIF() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.gif");
+ var [ready, request] = yield promiseImage(alert);
+ ok(ready, "Should load first animated GIF frame");
+ is(request.mimeType, "image/gif", "Should report correct GIF MIME type");
+ is(request.image.width, 256, "GIF width should be 256px");
+ is(request.image.height, 256, "GIF height should be 256px");
+});
+
+add_task(function* testCancel() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.gif&t=180");
+ yield new Promise((resolve, reject) => {
+ var request = alert.loadImage(0, SpecialPowers.wrapCallbackObject({
+ onImageReady() {
+ reject(new Error("Should not load cancelled request"));
+ },
+ onImageMissing() {
+ resolve();
+ },
+ }), null);
+ request.cancel(SpecialPowers.Cr.NS_BINDING_ABORTED);
+ });
+});
+
+add_task(function* testMixedContent() {
+ // Loading principal is HTTPS; image URL is HTTP.
+ var origin = "https://mochi.test:8888";
+ var principal = Services.scriptSecurityManager
+ .createCodebasePrincipalFromOrigin(origin);
+
+ var alert = makeAlert(null, imageServerURL + "?f=image.png",
+ null, null, false, null, null, null,
+ null, principal);
+ var [ready, request] = yield promiseImage(alert);
+ ok(ready, "Should load cross-protocol image");
+ is(request.mimeType, "image/png", "Should report correct MIME type");
+ is(request.image.width, 32, "Width should be 32px");
+ is(request.image.height, 32, "Height should be 32px");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/alerts/test/test_multiple_alerts.html b/toolkit/components/alerts/test/test_multiple_alerts.html
new file mode 100644
index 0000000000..9d939b63a0
--- /dev/null
+++ b/toolkit/components/alerts/test/test_multiple_alerts.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for multiple alerts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/Timer.jsm");
+
+ const alertService = Components.classes["@mozilla.org/alerts-service;1"]
+ .getService(Components.interfaces.nsIAlertsService);
+
+ addMessageListener("waitForPosition", function() {
+ var timer = setTimeout(function() {
+ Services.ww.unregisterNotification(windowObserver);
+ sendAsyncMessage("waitedForPosition", null);
+ }, 2000);
+
+ var windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ // Alerts are implemented using XUL.
+ clearTimeout(timer);
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("pageshow", function onPageShow() {
+ win.removeEventListener("pageshow", onPageShow, false);
+
+ var x = win.screenX;
+ var y = win.screenY;
+
+ win.addEventListener("pagehide", function onPageHide() {
+ win.removeEventListener("pagehide", onPageHide, false);
+ sendAsyncMessage("waitedForPosition", { x, y });
+ }, false);
+
+ alertService.closeAlert();
+ }, false);
+ };
+
+ Services.ww.registerNotification(windowObserver);
+ });
+});
+
+function promiseAlertPosition(alertService) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("waitedForPosition", function waitedForPosition(result) {
+ chromeScript.removeMessageListener("waitedForPosition", waitedForPosition);
+ resolve(result);
+ });
+ chromeScript.sendAsyncMessage("waitForPosition");
+
+ alertService.showAlertNotification(null, "title", "body");
+ ok(true, "Alert shown.");
+ });
+}
+
+add_task(function* test_multiple_alerts() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application.");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application.");
+
+ var alertService;
+ try {
+ alertService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available.");
+ } catch (ex) {
+ todo(false, "Alerts service is not available.");
+ return;
+ }
+
+ var firstAlertPosition = yield promiseAlertPosition(alertService);
+ if (!firstAlertPosition) {
+ ok(true, "Platform does not use XUL alerts.");
+ return;
+ }
+
+ var secondAlertPosition = yield promiseAlertPosition(alertService);
+ is(secondAlertPosition.x, firstAlertPosition.x, "Second alert should be opened in the same position.");
+ is(secondAlertPosition.y, firstAlertPosition.y, "Second alert should be opened in the same position.");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/alerts/test/test_principal.html b/toolkit/components/alerts/test/test_principal.html
new file mode 100644
index 0000000000..74a20dbd70
--- /dev/null
+++ b/toolkit/components/alerts/test/test_principal.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1202933</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Services = SpecialPowers.Services;
+
+const notifier = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService);
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ addMessageListener("anyXULAlertsVisible", function() {
+ var windows = Services.wm.getEnumerator("alert:alert");
+ return windows.hasMoreElements();
+ });
+
+ addMessageListener("getAlertSource", function() {
+ var alertWindows = Services.wm.getEnumerator("alert:alert");
+ if (!alertWindows) {
+ return null;
+ }
+ var alertWindow = alertWindows.getNext();
+ return alertWindow.document.getElementById("alertSourceLabel").getAttribute("value");
+ });
+});
+
+function notify(alertName, principal) {
+ return new Promise((resolve, reject) => {
+ var source;
+ function observe(subject, topic, data) {
+ if (topic == "alertclickcallback") {
+ reject(new Error("Alerts should not be clicked during test"));
+ } else if (topic == "alertshow") {
+ source = chromeScript.sendSyncMessage("getAlertSource")[0][0];
+ notifier.closeAlert(alertName);
+ } else {
+ is(topic, "alertfinished", "Should hide alert");
+ resolve(source);
+ }
+ }
+ notifier.showAlertNotification(null, "Notification test",
+ "Surprise! I'm here to test notifications!",
+ false, alertName, observe, alertName,
+ null, null, null, principal);
+ if (SpecialPowers.Services.appinfo.OS == "Darwin") {
+ notifier.closeAlert(alertName);
+ }
+ });
+}
+
+function* testNoPrincipal() {
+ var source = yield notify("noPrincipal", null);
+ ok(!source, "Should omit source without principal");
+}
+
+function* testSystemPrincipal() {
+ var principal = Services.scriptSecurityManager.getSystemPrincipal();
+ var source = yield notify("systemPrincipal", principal);
+ ok(!source, "Should omit source for system principal");
+}
+
+function* testNullPrincipal() {
+ var principal = Services.scriptSecurityManager.createNullPrincipal({});
+ var source = yield notify("nullPrincipal", principal);
+ ok(!source, "Should omit source for null principal");
+}
+
+function* testNodePrincipal() {
+ var principal = SpecialPowers.wrap(document).nodePrincipal;
+ var source = yield notify("nodePrincipal", principal);
+
+ var stringBundle = Services.strings.createBundle(
+ "chrome://alerts/locale/alert.properties"
+ );
+ var localizedSource = stringBundle.formatStringFromName(
+ "source.label", [principal.URI.hostPort], 1);
+ is(source, localizedSource, "Should include source for node principal");
+}
+
+function runTest() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application");
+ return;
+ }
+
+ if ("@mozilla.org/system-alerts-service;1" in Cc) {
+ todo(false, "Native alerts service exists in this application");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application");
+
+ // sendSyncMessage returns an array of arrays. See the comments in
+ // test_alerts_noobserve.html and test_SpecialPowersLoadChromeScript.html.
+ var [[alertsVisible]] = chromeScript.sendSyncMessage("anyXULAlertsVisible");
+ ok(!alertsVisible, "Alerts should not be present at the start of the test.");
+
+ add_task(testNoPrincipal);
+ add_task(testSystemPrincipal);
+ add_task(testNullPrincipal);
+ add_task(testNodePrincipal);
+}
+
+runTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/apppicker/content/appPicker.js b/toolkit/components/apppicker/content/appPicker.js
new file mode 100644
index 0000000000..469a6ca231
--- /dev/null
+++ b/toolkit/components/apppicker/content/appPicker.js
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+function AppPicker() {}
+
+AppPicker.prototype =
+{
+ // Class members
+ _incomingParams:null,
+
+ /**
+ * Init the dialog and populate the application list
+ */
+ appPickerLoad: function appPickerLoad() {
+ const nsILocalHandlerApp = Components.interfaces.nsILocalHandlerApp;
+
+ this._incomingParams = window.arguments[0];
+ this._incomingParams.handlerApp = null;
+
+ document.title = this._incomingParams.title;
+
+ // Header creation - at the very least, we must have
+ // a mime type:
+ //
+ // (icon) Zip File
+ // (icon) filename
+ //
+ // (icon) Web Feed
+ // (icon) mime/type
+ //
+ // (icon) mime/type
+ // (icon)
+
+ var mimeInfo = this._incomingParams.mimeInfo;
+ var filename = this._incomingParams.filename;
+ if (!filename) {
+ filename = mimeInfo.MIMEType;
+ }
+ var description = this._incomingParams.description;
+ if (!description) {
+ description = filename;
+ filename = "";
+ }
+
+ // Setup the dialog header information
+ document.getElementById("content-description").setAttribute("value",
+ description);
+ document.getElementById("suggested-filename").setAttribute("value",
+ filename);
+ document.getElementById("content-icon").setAttribute("src",
+ "moz-icon://" + filename + "?size=32&contentType=" +
+ mimeInfo.MIMEType);
+
+ // Grab a list of nsILocalHandlerApp application helpers to list
+ var fileList = mimeInfo.possibleLocalHandlers;
+
+ var list = document.getElementById("app-picker-listbox");
+
+ var primaryCount = 0;
+
+ if (!fileList || fileList.length == 0) {
+ // display a message saying nothing is configured
+ document.getElementById("app-picker-notfound").removeAttribute("hidden");
+ return;
+ }
+
+ for (var idx = 0; idx < fileList.length; idx++) {
+ var file = fileList.queryElementAt(idx, nsILocalHandlerApp);
+ try {
+ if (!file.executable || !file.executable.isFile())
+ continue;
+ } catch (err) {
+ continue;
+ }
+
+ var item = document.createElement("listitem");
+ item.className = "listitem-iconic";
+ item.handlerApp = file;
+ item.setAttribute("label", this.getFileDisplayName(file.executable));
+ item.setAttribute("image", this.getFileIconURL(file.executable));
+ list.appendChild(item);
+
+ primaryCount++;
+ }
+
+ if ( primaryCount == 0 ) {
+ // display a message saying nothing is configured
+ document.getElementById("app-picker-notfound").removeAttribute("hidden");
+ }
+ },
+
+ /**
+ * Retrieve the moz-icon for the app
+ */
+ getFileIconURL: function getFileIconURL(file) {
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+
+ if (!ios) return "";
+ const nsIFileProtocolHandler =
+ Components.interfaces.nsIFileProtocolHandler;
+
+ var fph = ios.getProtocolHandler("file")
+ .QueryInterface(nsIFileProtocolHandler);
+ if (!fph) return "";
+
+ var urlSpec = fph.getURLSpecFromFile(file);
+ return "moz-icon://" + urlSpec + "?size=32";
+ },
+
+ /**
+ * Retrieve the pretty description from the file
+ */
+ getFileDisplayName: function getFileDisplayName(file) {
+ if (AppConstants.platform == "win") {
+ if (file instanceof Components.interfaces.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+ } else if (AppConstants.platform == "macosx") {
+ if (file instanceof Components.interfaces.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ }
+ return file.leafName;
+ },
+
+ /**
+ * Double click accepts an app
+ */
+ appDoubleClick: function appDoubleClick() {
+ var list = document.getElementById("app-picker-listbox");
+ var selItem = list.selectedItem;
+
+ if (!selItem) {
+ this._incomingParams.handlerApp = null;
+ return true;
+ }
+
+ this._incomingParams.handlerApp = selItem.handlerApp;
+ window.close();
+
+ return true;
+ },
+
+ appPickerOK: function appPickerOK() {
+ if (this._incomingParams.handlerApp) return true;
+
+ var list = document.getElementById("app-picker-listbox");
+ var selItem = list.selectedItem;
+
+ if (!selItem) {
+ this._incomingParams.handlerApp = null;
+ return true;
+ }
+ this._incomingParams.handlerApp = selItem.handlerApp;
+
+ return true;
+ },
+
+ appPickerCancel: function appPickerCancel() {
+ this._incomingParams.handlerApp = null;
+ return true;
+ },
+
+ /**
+ * User browse for an app.
+ */
+ appPickerBrowse: function appPickerBrowse() {
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var fp = Components.classes["@mozilla.org/filepicker;1"].
+ createInstance(nsIFilePicker);
+
+ fp.init(window, this._incomingParams.title, nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterApps);
+
+ var fileLoc = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties);
+ var startLocation;
+ if (AppConstants.platform == "win") {
+ startLocation = "ProgF"; // Program Files
+ } else if (AppConstants.platform == "macosx") {
+ startLocation = "LocApp"; // Local Applications
+ } else {
+ startLocation = "Home";
+ }
+ fp.displayDirectory =
+ fileLoc.get(startLocation, Components.interfaces.nsILocalFile);
+
+ if (fp.show() == nsIFilePicker.returnOK && fp.file) {
+ var localHandlerApp =
+ Components.classes["@mozilla.org/uriloader/local-handler-app;1"].
+ createInstance(Components.interfaces.nsILocalHandlerApp);
+ localHandlerApp.executable = fp.file;
+
+ this._incomingParams.handlerApp = localHandlerApp;
+ window.close();
+ }
+ return true;
+ }
+}
+
+// Global object
+var g_dialog = new AppPicker();
diff --git a/toolkit/components/apppicker/content/appPicker.xul b/toolkit/components/apppicker/content/appPicker.xul
new file mode 100644
index 0000000000..3a50483c1c
--- /dev/null
+++ b/toolkit/components/apppicker/content/appPicker.xul
@@ -0,0 +1,40 @@
+<?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/global.css" type="text/css"?>
+ <?xml-stylesheet href="chrome://global/skin/appPicker.css" type="text/css"?>
+
+ <!DOCTYPE dialog SYSTEM "chrome://global/locale/appPicker.dtd" >
+
+ <dialog id="app-picker"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="g_dialog.appPickerLoad();"
+ buttons="accept,cancel,extra2"
+ buttonlabelextra2="&BrowseButton.label;"
+ ondialogextra2="g_dialog.appPickerBrowse();"
+ defaultButton="cancel"
+ ondialogaccept="return g_dialog.appPickerOK();"
+ ondialogcancel="return g_dialog.appPickerCancel();"
+ aria-describedby="content-description suggested-filename"
+ persist="screenX screenY">
+
+ <script type="application/javascript" src="chrome://global/content/appPicker.js"/>
+
+ <hbox id="file-info" align="center">
+ <image id="content-icon" src=""/>
+ <vbox flex="1">
+ <label id="content-description" crop="center" value=""/>
+ <label id="suggested-filename" crop="center" value=""/>
+ </vbox>
+ </hbox>
+
+ <label id="sendto-message" value="&SendMsg.label;" control="app-picker-listbox"/>
+
+ <listbox id="app-picker-listbox" rows="5"
+ ondblclick="g_dialog.appDoubleClick();"/>
+
+ <label id="app-picker-notfound" value="&NoAppFound.label;" hidden="true"/>
+ </dialog>
diff --git a/toolkit/components/apppicker/jar.mn b/toolkit/components/apppicker/jar.mn
new file mode 100644
index 0000000000..60e029d8a7
--- /dev/null
+++ b/toolkit/components/apppicker/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/.
+
+toolkit.jar:
+ content/global/appPicker.xul (content/appPicker.xul)
+ content/global/appPicker.js (content/appPicker.js)
+
diff --git a/toolkit/components/apppicker/moz.build b/toolkit/components/apppicker/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/toolkit/components/apppicker/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'] \ No newline at end of file
diff --git a/toolkit/components/asyncshutdown/AsyncShutdown.jsm b/toolkit/components/asyncshutdown/AsyncShutdown.jsm
new file mode 100644
index 0000000000..62ac36f42c
--- /dev/null
+++ b/toolkit/components/asyncshutdown/AsyncShutdown.jsm
@@ -0,0 +1,1041 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Managing safe shutdown of asynchronous services.
+ *
+ * Firefox shutdown is composed of phases that take place
+ * sequentially. Typically, each shutdown phase removes some
+ * capabilities from the application. For instance, at the end of
+ * phase profileBeforeChange, no service is permitted to write to the
+ * profile directory (with the exception of Telemetry). Consequently,
+ * if any service has requested I/O to the profile directory before or
+ * during phase profileBeforeChange, the system must be informed that
+ * these requests need to be completed before the end of phase
+ * profileBeforeChange. Failing to inform the system of this
+ * requirement can (and has been known to) cause data loss.
+ *
+ * Example: At some point during shutdown, the Add-On Manager needs to
+ * ensure that all add-ons have safely written their data to disk,
+ * before writing its own data. Since the data is saved to the
+ * profile, this must be completed during phase profileBeforeChange.
+ *
+ * AsyncShutdown.profileBeforeChange.addBlocker(
+ * "Add-on manager: shutting down",
+ * function condition() {
+ * // Do things.
+ * // Perform I/O that must take place during phase profile-before-change
+ * return promise;
+ * }
+ * });
+ *
+ * In this example, function |condition| will be called at some point
+ * during phase profileBeforeChange and phase profileBeforeChange
+ * itself is guaranteed to not terminate until |promise| is either
+ * resolved or rejected.
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
+ "@mozilla.org/xpcom/debug;1", "nsIDebug2");
+Object.defineProperty(this, "gCrashReporter", {
+ get: function() {
+ delete this.gCrashReporter;
+ try {
+ let reporter = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsICrashReporter);
+ return this.gCrashReporter = reporter;
+ } catch (ex) {
+ return this.gCrashReporter = null;
+ }
+ },
+ configurable: true
+});
+
+// `true` if this is a content process, `false` otherwise.
+// It would be nicer to go through `Services.appInfo`, but some tests need to be
+// able to replace that field with a custom implementation before it is first
+// called.
+const isContent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
+// Display timeout warnings after 10 seconds
+const DELAY_WARNING_MS = 10 * 1000;
+
+
+// Crash the process if shutdown is really too long
+// (allowing for sleep).
+const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
+var DELAY_CRASH_MS = 60 * 1000; // One minute
+try {
+ DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
+} catch (ex) {
+ // Ignore errors
+}
+Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
+ DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
+}, false);
+
+/**
+ * A set of Promise that supports waiting.
+ *
+ * Promise items may be added or removed during the wait. The wait will
+ * resolve once all Promise items have been resolved or removed.
+ */
+function PromiseSet() {
+ /**
+ * key: the Promise passed pass the client of the `PromiseSet`.
+ * value: an indirection on top of `key`, as an object with
+ * the following fields:
+ * - indirection: a Promise resolved if `key` is resolved or
+ * if `resolve` is called
+ * - resolve: a function used to resolve the indirection.
+ */
+ this._indirections = new Map();
+}
+PromiseSet.prototype = {
+ /**
+ * Wait until all Promise have been resolved or removed.
+ *
+ * Note that calling `wait()` causes Promise to be removed from the
+ * Set once they are resolved.
+ *
+ * @return {Promise} Resolved once all Promise have been resolved or removed,
+ * or rejected after at least one Promise has rejected.
+ */
+ wait: function() {
+ // Pick an arbitrary element in the map, if any exists.
+ let entry = this._indirections.entries().next();
+ if (entry.done) {
+ // No indirections left, we are done.
+ return Promise.resolve();
+ }
+
+ let [, indirection] = entry.value;
+ let promise = indirection.promise;
+ promise = promise.then(() =>
+ // At this stage, the entry has been cleaned up.
+ this.wait()
+ );
+ return promise;
+ },
+
+ /**
+ * Add a new Promise to the set.
+ *
+ * Calls to wait (including ongoing calls) will only return once
+ * `key` has either resolved or been removed.
+ */
+ add: function(key) {
+ this._ensurePromise(key);
+ let indirection = PromiseUtils.defer();
+ key.then(
+ x => {
+ // Clean up immediately.
+ // This needs to be done before the call to `resolve`, otherwise
+ // `wait()` may loop forever.
+ this._indirections.delete(key);
+ indirection.resolve(x);
+ },
+ err => {
+ this._indirections.delete(key);
+ indirection.reject(err);
+ });
+ this._indirections.set(key, indirection);
+ },
+
+ /**
+ * Remove a Promise from the set.
+ *
+ * Calls to wait (including ongoing calls) will ignore this promise,
+ * unless it is added again.
+ */
+ delete: function(key) {
+ this._ensurePromise(key);
+ let value = this._indirections.get(key);
+ if (!value) {
+ return false;
+ }
+ this._indirections.delete(key);
+ value.resolve();
+ return true;
+ },
+
+ _ensurePromise: function(key) {
+ if (!key || typeof key != "object") {
+ throw new Error("Expected an object");
+ }
+ if ((!("then" in key)) || typeof key.then != "function") {
+ throw new Error("Expected a Promise");
+ }
+ },
+
+};
+
+
+/**
+ * Display a warning.
+ *
+ * As this code is generally used during shutdown, there are chances
+ * that the UX will not be available to display warnings on the
+ * console. We therefore use dump() rather than Cu.reportError().
+ */
+function log(msg, prefix = "", error = null) {
+ try {
+ dump(prefix + msg + "\n");
+ if (error) {
+ dump(prefix + error + "\n");
+ if (typeof error == "object" && "stack" in error) {
+ dump(prefix + error.stack + "\n");
+ }
+ }
+ } catch (ex) {
+ dump("INTERNAL ERROR in AsyncShutdown: cannot log message.\n");
+ }
+}
+const PREF_DEBUG_LOG = "toolkit.asyncshutdown.log";
+var DEBUG_LOG = false;
+try {
+ DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
+} catch (ex) {
+ // Ignore errors
+}
+Services.prefs.addObserver(PREF_DEBUG_LOG, function() {
+ DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
+}, false);
+
+function debug(msg, error=null) {
+ if (DEBUG_LOG) {
+ log(msg, "DEBUG: ", error);
+ }
+}
+function warn(msg, error = null) {
+ log(msg, "WARNING: ", error);
+}
+function fatalerr(msg, error = null) {
+ log(msg, "FATAL ERROR: ", error);
+}
+
+// Utility function designed to get the current state of execution
+// of a blocker.
+// We are a little paranoid here to ensure that in case of evaluation
+// error we do not block the AsyncShutdown.
+function safeGetState(fetchState) {
+ if (!fetchState) {
+ return "(none)";
+ }
+ let data, string;
+ try {
+ // Evaluate fetchState(), normalize the result into something that we can
+ // safely stringify or upload.
+ let state = fetchState();
+ if (!state) {
+ return "(none)";
+ }
+ string = JSON.stringify(state);
+ data = JSON.parse(string);
+ // Simplify the rest of the code by ensuring that we can simply
+ // concatenate the result to a message.
+ if (data && typeof data == "object") {
+ data.toString = function() {
+ return string;
+ };
+ }
+ return data;
+ } catch (ex) {
+
+ // Make sure that this causes test failures
+ Promise.reject(ex);
+
+ if (string) {
+ return string;
+ }
+ try {
+ return "Error getting state: " + ex + " at " + ex.stack;
+ } catch (ex2) {
+ return "Error getting state but could not display error";
+ }
+ }
+}
+
+/**
+ * Countdown for a given duration, skipping beats if the computer is too busy,
+ * sleeping or otherwise unavailable.
+ *
+ * @param {number} delay An approximate delay to wait in milliseconds (rounded
+ * up to the closest second).
+ *
+ * @return Deferred
+ */
+function looseTimer(delay) {
+ let DELAY_BEAT = 1000;
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let beats = Math.ceil(delay / DELAY_BEAT);
+ let deferred = Promise.defer();
+ timer.initWithCallback(function() {
+ if (beats <= 0) {
+ deferred.resolve();
+ }
+ --beats;
+ }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
+ // Ensure that the timer is both canceled once we are done with it
+ // and not garbage-collected until then.
+ deferred.promise.then(() => timer.cancel(), () => timer.cancel());
+ return deferred;
+}
+
+/**
+ * Given an nsIStackFrame object, find the caller filename, line number,
+ * and stack if necessary, and return them as an object.
+ *
+ * @param {nsIStackFrame} topFrame Top frame of the call stack.
+ * @param {string} filename Pre-supplied filename or null if unknown.
+ * @param {number} lineNumber Pre-supplied line number or null if unknown.
+ * @param {string} stack Pre-supplied stack or null if unknown.
+ *
+ * @return object
+ */
+function getOrigin(topFrame, filename = null, lineNumber = null, stack = null) {
+ try {
+ // Determine the filename and line number of the caller.
+ let frame = topFrame;
+
+ for (; frame && frame.filename == topFrame.filename; frame = frame.caller) {
+ // Climb up the stack
+ }
+
+ if (filename == null) {
+ filename = frame ? frame.filename : "?";
+ }
+ if (lineNumber == null) {
+ lineNumber = frame ? frame.lineNumber : 0;
+ }
+ if (stack == null) {
+ // Now build the rest of the stack as a string, using Task.jsm's rewriting
+ // to ensure that we do not lose information at each call to `Task.spawn`.
+ let frames = [];
+ while (frame != null) {
+ frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
+ frame = frame.caller;
+ }
+ stack = Task.Debugging.generateReadableStack(frames.join("\n")).split("\n");
+ }
+
+ return {
+ filename: filename,
+ lineNumber: lineNumber,
+ stack: stack,
+ };
+ } catch (ex) {
+ return {
+ filename: "<internal error: could not get origin>",
+ lineNumber: -1,
+ stack: "<internal error: could not get origin>",
+ }
+ }
+}
+
+this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
+
+/**
+ * {string} topic -> phase
+ */
+var gPhases = new Map();
+
+this.AsyncShutdown = {
+ /**
+ * Access function getPhase. For testing purposes only.
+ */
+ get _getPhase() {
+ let accepted = false;
+ try {
+ accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
+ } catch (ex) {
+ // Ignore errors
+ }
+ if (accepted) {
+ return getPhase;
+ }
+ return undefined;
+ }
+};
+
+/**
+ * Register a new phase.
+ *
+ * @param {string} topic The notification topic for this Phase.
+ * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
+ */
+function getPhase(topic) {
+ let phase = gPhases.get(topic);
+ if (phase) {
+ return phase;
+ }
+ let spinner = new Spinner(topic);
+ phase = Object.freeze({
+ /**
+ * Register a blocker for the completion of a phase.
+ *
+ * @param {string} name The human-readable name of the blocker. Used
+ * for debugging/error reporting. Please make sure that the name
+ * respects the following model: "Some Service: some action in progress" -
+ * for instance "OS.File: flushing all pending I/O";
+ * @param {function|promise|*} condition A condition blocking the
+ * completion of the phase. Generally, this is a function
+ * returning a promise. This function is evaluated during the
+ * phase and the phase is guaranteed to not terminate until the
+ * resulting promise is either resolved or rejected. If
+ * |condition| is not a function but another value |v|, it behaves
+ * as if it were a function returning |v|.
+ * @param {object*} details Optionally, an object with details
+ * that may be useful for error reporting, as a subset of of the following
+ * fields:
+ * - fetchState (strongly recommended) A function returning
+ * information about the current state of the blocker as an
+ * object. Used for providing more details when logging errors or
+ * crashing.
+ * - stack. A string containing stack information. This module can
+ * generally infer stack information if it is not provided.
+ * - lineNumber A number containing the line number for the caller.
+ * This module can generally infer this information if it is not
+ * provided.
+ * - filename A string containing the filename for the caller. This
+ * module can generally infer the information if it is not provided.
+ *
+ * Examples:
+ * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
+ * promise); // profileBeforeChange will not complete until
+ * // promise is resolved or rejected
+ *
+ * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
+ * function callback() {
+ * // ...
+ * // Execute this code during profileBeforeChange
+ * return promise;
+ * // profileBeforeChange will not complete until promise
+ * // is resolved or rejected
+ * });
+ *
+ * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
+ * function callback() {
+ * // ...
+ * // Execute this code during profileBeforeChange
+ * // No specific guarantee about completion of profileBeforeChange
+ * });
+ */
+ addBlocker: function(name, condition, details = null) {
+ spinner.addBlocker(name, condition, details);
+ },
+ /**
+ * Remove the blocker for a condition.
+ *
+ * If several blockers have been registered for the same
+ * condition, remove all these blockers. If no blocker has been
+ * registered for this condition, this is a noop.
+ *
+ * @return {boolean} true if a blocker has been removed, false
+ * otherwise. Note that a result of false may mean either that
+ * the blocker has never been installed or that the phase has
+ * completed and the blocker has already been resolved.
+ */
+ removeBlocker: function(condition) {
+ return spinner.removeBlocker(condition);
+ },
+
+ get name() {
+ return spinner.name;
+ },
+
+ /**
+ * Trigger the phase without having to broadcast a
+ * notification. For testing purposes only.
+ */
+ get _trigger() {
+ let accepted = false;
+ try {
+ accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
+ } catch (ex) {
+ // Ignore errors
+ }
+ if (accepted) {
+ return () => spinner.observe();
+ }
+ return undefined;
+ }
+ });
+ gPhases.set(topic, phase);
+ return phase;
+}
+
+/**
+ * Utility class used to spin the event loop until all blockers for a
+ * Phase are satisfied.
+ *
+ * @param {string} topic The xpcom notification for that phase.
+ */
+function Spinner(topic) {
+ this._barrier = new Barrier(topic);
+ this._topic = topic;
+ Services.obs.addObserver(this, topic, false);
+}
+
+Spinner.prototype = {
+ /**
+ * Register a new condition for this phase.
+ *
+ * See the documentation of `addBlocker` in property `client`
+ * of instances of `Barrier`.
+ */
+ addBlocker: function(name, condition, details) {
+ this._barrier.client.addBlocker(name, condition, details);
+ },
+ /**
+ * Remove the blocker for a condition.
+ *
+ * See the documentation of `removeBlocker` in rpoperty `client`
+ * of instances of `Barrier`
+ *
+ * @return {boolean} true if a blocker has been removed, false
+ * otherwise. Note that a result of false may mean either that
+ * the blocker has never been installed or that the phase has
+ * completed and the blocker has already been resolved.
+ */
+ removeBlocker: function(condition) {
+ return this._barrier.client.removeBlocker(condition);
+ },
+
+ get name() {
+ return this._barrier.client.name;
+ },
+
+ // nsIObserver.observe
+ observe: function() {
+ let topic = this._topic;
+ debug(`Starting phase ${ topic }`);
+ Services.obs.removeObserver(this, topic);
+
+ let satisfied = false; // |true| once we have satisfied all conditions
+ let promise;
+ try {
+ promise = this._barrier.wait({
+ warnAfterMS: DELAY_WARNING_MS,
+ crashAfterMS: DELAY_CRASH_MS
+ }).catch(
+ // Additional precaution to be entirely sure that we cannot reject.
+ );
+ } catch (ex) {
+ debug("Error waiting for notification");
+ throw ex;
+ }
+
+ // Now, spin the event loop
+ debug("Spinning the event loop");
+ promise.then(() => satisfied = true); // This promise cannot reject
+ let thread = Services.tm.mainThread;
+ while (!satisfied) {
+ try {
+ thread.processNextEvent(true);
+ } catch (ex) {
+ // An uncaught error should not stop us, but it should still
+ // be reported and cause tests to fail.
+ Promise.reject(ex);
+ }
+ }
+ debug(`Finished phase ${ topic }`);
+ }
+};
+
+/**
+ * A mechanism used to register blockers that prevent some action from
+ * happening.
+ *
+ * An instance of |Barrier| provides a capability |client| that
+ * clients can use to register blockers. The barrier is resolved once
+ * all registered blockers have been resolved. The owner of the
+ * |Barrier| may wait for the resolution of the barrier and obtain
+ * information on which blockers have not been resolved yet.
+ *
+ * @param {string} name The name of the blocker. Used mainly for error-
+ * reporting.
+ */
+function Barrier(name) {
+ if (!name) {
+ throw new TypeError("Instances of Barrier need a (non-empty) name");
+ }
+
+
+ /**
+ * The set of all Promise for which we need to wait before the barrier
+ * is lifted. Note that this set may be changed while we are waiting.
+ *
+ * Set to `null` once the wait is complete.
+ */
+ this._waitForMe = new PromiseSet();
+
+ /**
+ * A map from conditions, as passed by users during the call to `addBlocker`,
+ * to `promise`, as present in `this._waitForMe`.
+ *
+ * Used to let users perform cleanup through `removeBlocker`.
+ * Set to `null` once the wait is complete.
+ *
+ * Key: condition (any, as passed by user)
+ * Value: promise used as a key in `this._waitForMe`. Note that there is
+ * no guarantee that the key is still present in `this._waitForMe`.
+ */
+ this._conditionToPromise = new Map();
+
+ /**
+ * A map from Promise, as present in `this._waitForMe` or
+ * `this._conditionToPromise`, to information on blockers.
+ *
+ * Key: Promise (as present in this._waitForMe or this._conditionToPromise).
+ * Value: {
+ * trigger: function,
+ * promise,
+ * name,
+ * fetchState: function,
+ * stack,
+ * filename,
+ * lineNumber
+ * };
+ */
+ this._promiseToBlocker = new Map();
+
+ /**
+ * The name of the barrier.
+ */
+ if (typeof name != "string") {
+ throw new TypeError("The name of the barrier must be a string");
+ }
+ this._name = name;
+
+ /**
+ * A cache for the promise returned by wait().
+ */
+ this._promise = null;
+
+ /**
+ * `true` once we have started waiting.
+ */
+ this._isStarted = false;
+
+ /**
+ * The capability of adding blockers. This object may safely be returned
+ * or passed to clients.
+ */
+ this.client = {
+ /**
+ * The name of the barrier owning this client.
+ */
+ get name() {
+ return name;
+ },
+
+ /**
+ * Register a blocker for the completion of this barrier.
+ *
+ * @param {string} name The human-readable name of the blocker. Used
+ * for debugging/error reporting. Please make sure that the name
+ * respects the following model: "Some Service: some action in progress" -
+ * for instance "OS.File: flushing all pending I/O";
+ * @param {function|promise|*} condition A condition blocking the
+ * completion of the phase. Generally, this is a function
+ * returning a promise. This function is evaluated during the
+ * phase and the phase is guaranteed to not terminate until the
+ * resulting promise is either resolved or rejected. If
+ * |condition| is not a function but another value |v|, it behaves
+ * as if it were a function returning |v|.
+ * @param {object*} details Optionally, an object with details
+ * that may be useful for error reporting, as a subset of of the following
+ * fields:
+ * - fetchState (strongly recommended) A function returning
+ * information about the current state of the blocker as an
+ * object. Used for providing more details when logging errors or
+ * crashing.
+ * - stack. A string containing stack information. This module can
+ * generally infer stack information if it is not provided.
+ * - lineNumber A number containing the line number for the caller.
+ * This module can generally infer this information if it is not
+ * provided.
+ * - filename A string containing the filename for the caller. This
+ * module can generally infer the information if it is not provided.
+ */
+ addBlocker: (name, condition, details) => {
+ if (typeof name != "string") {
+ throw new TypeError("Expected a human-readable name as first argument");
+ }
+ if (details && typeof details == "function") {
+ details = {
+ fetchState: details
+ };
+ } else if (!details) {
+ details = {};
+ }
+ if (typeof details != "object") {
+ throw new TypeError("Expected an object as third argument to `addBlocker`, got " + details);
+ }
+ if (!this._waitForMe) {
+ throw new Error(`Phase "${ this._name }" is finished, it is too late to register completion condition "${ name }"`);
+ }
+ debug(`Adding blocker ${ name } for phase ${ this._name }`);
+
+ // Normalize the details
+
+ let fetchState = details.fetchState || null;
+ if (fetchState != null && typeof fetchState != "function") {
+ throw new TypeError("Expected a function for option `fetchState`");
+ }
+ let filename = details.filename || null;
+ let lineNumber = details.lineNumber || null;
+ let stack = details.stack || null;
+
+ // Split the condition between a trigger function and a promise.
+
+ // The function to call to notify the blocker that we have started waiting.
+ // This function returns a promise resolved/rejected once the
+ // condition is complete, and never throws.
+ let trigger;
+
+ // A promise resolved once the condition is complete.
+ let promise;
+ if (typeof condition == "function") {
+ promise = new Promise((resolve, reject) => {
+ trigger = () => {
+ try {
+ resolve(condition());
+ } catch (ex) {
+ reject(ex);
+ }
+ }
+ });
+ } else {
+ // If `condition` is not a function, `trigger` is not particularly
+ // interesting, and `condition` needs to be normalized to a promise.
+ trigger = () => {};
+ promise = Promise.resolve(condition);
+ }
+
+ // Make sure that `promise` never rejects.
+ promise = promise.then(null, error => {
+ let msg = `A blocker encountered an error while we were waiting.
+ Blocker: ${ name }
+ Phase: ${ this._name }
+ State: ${ safeGetState(fetchState) }`;
+ warn(msg, error);
+
+ // The error should remain uncaught, to ensure that it
+ // still causes tests to fail.
+ Promise.reject(error);
+ }).catch(
+ // Added as a last line of defense, in case `warn`, `this._name` or
+ // `safeGetState` somehow throws an error.
+ );
+
+ let topFrame = null;
+ if (filename == null || lineNumber == null || stack == null) {
+ topFrame = Components.stack;
+ }
+
+ let blocker = {
+ trigger: trigger,
+ promise: promise,
+ name: name,
+ fetchState: fetchState,
+ getOrigin: () => getOrigin(topFrame, filename, lineNumber, stack),
+ };
+
+ this._waitForMe.add(promise);
+ this._promiseToBlocker.set(promise, blocker);
+ this._conditionToPromise.set(condition, promise);
+
+ // As conditions may hold lots of memory, we attempt to cleanup
+ // as soon as we are done (which might be in the next tick, if
+ // we have been passed a resolved promise).
+ promise = promise.then(() => {
+ debug(`Completed blocker ${ name } for phase ${ this._name }`);
+ this._removeBlocker(condition);
+ });
+
+ if (this._isStarted) {
+ // The wait has already started. The blocker should be
+ // notified asap. We do it out of band as clients probably
+ // expect `addBlocker` to return immediately.
+ Promise.resolve().then(trigger);
+ }
+ },
+
+ /**
+ * Remove the blocker for a condition.
+ *
+ * If several blockers have been registered for the same
+ * condition, remove all these blockers. If no blocker has been
+ * registered for this condition, this is a noop.
+ *
+ * @return {boolean} true if at least one blocker has been
+ * removed, false otherwise.
+ */
+ removeBlocker: (condition) => {
+ return this._removeBlocker(condition);
+ }
+ };
+}
+Barrier.prototype = Object.freeze({
+ /**
+ * The current state of the barrier, as a JSON-serializable object
+ * designed for error-reporting.
+ */
+ get state() {
+ if (!this._isStarted) {
+ return "Not started";
+ }
+ if (!this._waitForMe) {
+ return "Complete";
+ }
+ let frozen = [];
+ for (let blocker of this._promiseToBlocker.values()) {
+ let {name, fetchState} = blocker;
+ let {stack, filename, lineNumber} = blocker.getOrigin();
+ frozen.push({
+ name: name,
+ state: safeGetState(fetchState),
+ filename: filename,
+ lineNumber: lineNumber,
+ stack: stack
+ });
+ }
+ return frozen;
+ },
+
+ /**
+ * Wait until all currently registered blockers are complete.
+ *
+ * Once this method has been called, any attempt to register a new blocker
+ * for this barrier will cause an error.
+ *
+ * Successive calls to this method always return the same value.
+ *
+ * @param {object=} options Optionally, an object that may contain
+ * the following fields:
+ * {number} warnAfterMS If provided and > 0, print a warning if the barrier
+ * has not been resolved after the given number of milliseconds.
+ * {number} crashAfterMS If provided and > 0, crash the process if the barrier
+ * has not been resolved after the give number of milliseconds (rounded up
+ * to the next second). To avoid crashing simply because the computer is busy
+ * or going to sleep, we actually wait for ceil(crashAfterMS/1000) successive
+ * periods of at least one second. Upon crashing, if a crash reporter is present,
+ * prepare a crash report with the state of this barrier.
+ *
+ *
+ * @return {Promise} A promise satisfied once all blockers are complete.
+ */
+ wait: function(options = {}) {
+ // This method only implements caching on top of _wait()
+ if (this._promise) {
+ return this._promise;
+ }
+ return this._promise = this._wait(options);
+ },
+ _wait: function(options) {
+
+ // Sanity checks
+ if (this._isStarted) {
+ throw new TypeError("Internal error: already started " + this._name);
+ }
+ if (!this._waitForMe || !this._conditionToPromise || !this._promiseToBlocker) {
+ throw new TypeError("Internal error: already finished " + this._name);
+ }
+
+ let topic = this._name;
+
+ // Notify blockers
+ for (let blocker of this._promiseToBlocker.values()) {
+ blocker.trigger(); // We have guarantees that this method will never throw
+ }
+
+ this._isStarted = true;
+
+ // Now, wait
+ let promise = this._waitForMe.wait();
+
+ promise = promise.then(null, function onError(error) {
+ // I don't think that this can happen.
+ // However, let's be overcautious with async/shutdown error reporting.
+ let msg = "An uncaught error appeared while completing the phase." +
+ " Phase: " + topic;
+ warn(msg, error);
+ });
+
+ promise = promise.then(() => {
+ // Cleanup memory
+ this._waitForMe = null;
+ this._promiseToBlocker = null;
+ this._conditionToPromise = null;
+ });
+
+ // Now handle warnings and crashes
+ let warnAfterMS = DELAY_WARNING_MS;
+ if (options && "warnAfterMS" in options) {
+ if (typeof options.warnAfterMS == "number"
+ || options.warnAfterMS == null) {
+ // Change the delay or deactivate warnAfterMS
+ warnAfterMS = options.warnAfterMS;
+ } else {
+ throw new TypeError("Wrong option value for warnAfterMS");
+ }
+ }
+
+ if (warnAfterMS && warnAfterMS > 0) {
+ // If the promise takes too long to be resolved/rejected,
+ // we need to notify the user.
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(() => {
+ let msg = "At least one completion condition is taking too long to complete." +
+ " Conditions: " + JSON.stringify(this.state) +
+ " Barrier: " + topic;
+ warn(msg);
+ }, warnAfterMS, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ promise = promise.then(function onSuccess() {
+ timer.cancel();
+ // As a side-effect, this prevents |timer| from
+ // being garbage-collected too early.
+ });
+ }
+
+ let crashAfterMS = DELAY_CRASH_MS;
+ if (options && "crashAfterMS" in options) {
+ if (typeof options.crashAfterMS == "number"
+ || options.crashAfterMS == null) {
+ // Change the delay or deactivate crashAfterMS
+ crashAfterMS = options.crashAfterMS;
+ } else {
+ throw new TypeError("Wrong option value for crashAfterMS");
+ }
+ }
+
+ if (crashAfterMS > 0) {
+ let timeToCrash = null;
+
+ // If after |crashAfterMS| milliseconds (adjusted to take into
+ // account sleep and otherwise busy computer) we have not finished
+ // this shutdown phase, we assume that the shutdown is somehow
+ // frozen, presumably deadlocked. At this stage, the only thing we
+ // can do to avoid leaving the user's computer in an unstable (and
+ // battery-sucking) situation is report the issue and crash.
+ timeToCrash = looseTimer(crashAfterMS);
+ timeToCrash.promise.then(
+ function onTimeout() {
+ // Report the problem as best as we can, then crash.
+ let state = this.state;
+
+ // If you change the following message, please make sure
+ // that any information on the topic and state appears
+ // within the first 200 characters of the message. This
+ // helps automatically sort oranges.
+ let msg = "AsyncShutdown timeout in " + topic +
+ " Conditions: " + JSON.stringify(state) +
+ " At least one completion condition failed to complete" +
+ " within a reasonable amount of time. Causing a crash to" +
+ " ensure that we do not leave the user with an unresponsive" +
+ " process draining resources.";
+ fatalerr(msg);
+ if (gCrashReporter && gCrashReporter.enabled) {
+ let data = {
+ phase: topic,
+ conditions: state
+ };
+ gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
+ JSON.stringify(data));
+ } else {
+ warn("No crash reporter available");
+ }
+
+ // To help sorting out bugs, we want to make sure that the
+ // call to nsIDebug2.abort points to a guilty client, rather
+ // than to AsyncShutdown itself. We pick a client that is
+ // still blocking and use its filename/lineNumber,
+ // which have been determined during the call to `addBlocker`.
+ let filename = "?";
+ let lineNumber = -1;
+ for (let blocker of this._promiseToBlocker.values()) {
+ ({filename, lineNumber} = blocker.getOrigin());
+ break;
+ }
+ gDebug.abort(filename, lineNumber);
+ }.bind(this),
+ function onSatisfied() {
+ // The promise has been rejected, which means that we have satisfied
+ // all completion conditions.
+ });
+
+ promise = promise.then(function() {
+ timeToCrash.reject();
+ }/* No error is possible here*/);
+ }
+
+ return promise;
+ },
+
+ _removeBlocker: function(condition) {
+ if (!this._waitForMe || !this._promiseToBlocker || !this._conditionToPromise) {
+ // We have already cleaned up everything.
+ return false;
+ }
+
+ let promise = this._conditionToPromise.get(condition);
+ if (!promise) {
+ // The blocker has already been removed
+ return false;
+ }
+ this._conditionToPromise.delete(condition);
+ this._promiseToBlocker.delete(promise);
+ return this._waitForMe.delete(promise);
+ },
+
+});
+
+
+
+// List of well-known phases
+// Ideally, phases should be registered from the component that decides
+// when they start/stop. For compatibility with existing startup/shutdown
+// mechanisms, we register a few phases here.
+
+// Parent process
+if (!isContent) {
+ this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
+ this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
+ this.AsyncShutdown.placesClosingInternalConnection = getPhase("places-will-close-connection");
+ this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change-telemetry");
+}
+
+// Notifications that fire in the parent and content process, but should
+// only have phases in the parent process.
+if (!isContent) {
+ this.AsyncShutdown.quitApplicationGranted = getPhase("quit-application-granted");
+}
+
+// Don't add a barrier for content-child-shutdown because this
+// makes it easier to cause shutdown hangs.
+
+// All processes
+this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
+this.AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown");
+
+this.AsyncShutdown.Barrier = Barrier;
+
+Object.freeze(this.AsyncShutdown);
diff --git a/toolkit/components/asyncshutdown/moz.build b/toolkit/components/asyncshutdown/moz.build
new file mode 100644
index 0000000000..79a4c44c70
--- /dev/null
+++ b/toolkit/components/asyncshutdown/moz.build
@@ -0,0 +1,25 @@
+# -*- 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/xpcshell/xpcshell.ini']
+
+XPIDL_MODULE = 'toolkit_asyncshutdown'
+
+XPIDL_SOURCES += [
+ 'nsIAsyncShutdown.idl',
+]
+
+EXTRA_JS_MODULES += [
+ 'AsyncShutdown.jsm',
+]
+
+EXTRA_COMPONENTS += [
+ 'nsAsyncShutdown.js',
+ 'nsAsyncShutdown.manifest',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Async Tooling')
diff --git a/toolkit/components/asyncshutdown/nsAsyncShutdown.js b/toolkit/components/asyncshutdown/nsAsyncShutdown.js
new file mode 100644
index 0000000000..bd2c9a2fdf
--- /dev/null
+++ b/toolkit/components/asyncshutdown/nsAsyncShutdown.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * An implementation of nsIAsyncShutdown* based on AsyncShutdown.jsm
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+var XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+
+/**
+ * Conversion between nsIPropertyBag and JS object
+ */
+var PropertyBagConverter = {
+ // From nsIPropertyBag to JS
+ toObject: function(bag) {
+ if (!(bag instanceof Ci.nsIPropertyBag)) {
+ throw new TypeError("Not a property bag");
+ }
+ let result = {};
+ let enumerator = bag.enumerator;
+ while (enumerator.hasMoreElements()) {
+ let {name, value: property} = enumerator.getNext().QueryInterface(Ci.nsIProperty);
+ let value = this.toValue(property);
+ result[name] = value;
+ }
+ return result;
+ },
+ toValue: function(property) {
+ if (typeof property != "object") {
+ return property;
+ }
+ if (Array.isArray(property)) {
+ return property.map(this.toValue, this);
+ }
+ if (property && property instanceof Ci.nsIPropertyBag) {
+ return this.toObject(property);
+ }
+ return property;
+ },
+
+ // From JS to nsIPropertyBag
+ fromObject: function(obj) {
+ if (obj == null || typeof obj != "object") {
+ throw new TypeError("Invalid object: " + obj);
+ }
+ let bag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let k of Object.keys(obj)) {
+ let value = this.fromValue(obj[k]);
+ bag.setProperty(k, value);
+ }
+ return bag;
+ },
+ fromValue: function(value) {
+ if (typeof value == "function") {
+ return null; // Emulating the behavior of JSON.stringify with functions
+ }
+ if (Array.isArray(value)) {
+ return value.map(this.fromValue, this);
+ }
+ if (value == null || typeof value != "object") {
+ // Auto-converted to nsIVariant
+ return value;
+ }
+ return this.fromObject(value);
+ },
+};
+
+
+
+/**
+ * Construct an instance of nsIAsyncShutdownClient from a
+ * AsyncShutdown.Barrier client.
+ *
+ * @param {object} moduleClient A client, as returned from the `client`
+ * property of an instance of `AsyncShutdown.Barrier`. This client will
+ * serve as back-end for methods `addBlocker` and `removeBlocker`.
+ * @constructor
+ */
+function nsAsyncShutdownClient(moduleClient) {
+ if (!moduleClient) {
+ throw new TypeError("nsAsyncShutdownClient expects one argument");
+ }
+ this._moduleClient = moduleClient;
+ this._byName = new Map();
+}
+nsAsyncShutdownClient.prototype = {
+ _getPromisified: function(xpcomBlocker) {
+ let candidate = this._byName.get(xpcomBlocker.name);
+ if (!candidate) {
+ return null;
+ }
+ if (candidate.xpcom === xpcomBlocker) {
+ return candidate.jsm;
+ }
+ return null;
+ },
+ _setPromisified: function(xpcomBlocker, moduleBlocker) {
+ let candidate = this._byName.get(xpcomBlocker.name);
+ if (!candidate) {
+ this._byName.set(xpcomBlocker.name, {xpcom: xpcomBlocker,
+ jsm: moduleBlocker});
+ return;
+ }
+ if (candidate.xpcom === xpcomBlocker) {
+ return;
+ }
+ throw new Error("We have already registered a distinct blocker with the same name: " + xpcomBlocker.name);
+ },
+ _deletePromisified: function(xpcomBlocker) {
+ let candidate = this._byName.get(xpcomBlocker.name);
+ if (!candidate || candidate.xpcom !== xpcomBlocker) {
+ return false;
+ }
+ this._byName.delete(xpcomBlocker.name);
+ return true;
+ },
+ get jsclient() {
+ return this._moduleClient;
+ },
+ get name() {
+ return this._moduleClient.name;
+ },
+ addBlocker: function(/* nsIAsyncShutdownBlocker*/ xpcomBlocker,
+ fileName, lineNumber, stack) {
+ // We need a Promise-based function with the same behavior as
+ // `xpcomBlocker`. Furthermore, to support `removeBlocker`, we
+ // need to ensure that we always get the same Promise-based
+ // function if we call several `addBlocker`/`removeBlocker` several
+ // times with the same `xpcomBlocker`.
+ //
+ // Ideally, this should be done with a WeakMap() with xpcomBlocker
+ // as a key, but XPConnect NativeWrapped objects cannot serve as
+ // WeakMap keys.
+ //
+ let moduleBlocker = this._getPromisified(xpcomBlocker);
+ if (!moduleBlocker) {
+ moduleBlocker = () => new Promise(
+ // This promise is never resolved. By opposition to AsyncShutdown
+ // blockers, `nsIAsyncShutdownBlocker`s are always lifted by calling
+ // `removeBlocker`.
+ () => xpcomBlocker.blockShutdown(this)
+ );
+
+ this._setPromisified(xpcomBlocker, moduleBlocker);
+ }
+
+ this._moduleClient.addBlocker(xpcomBlocker.name,
+ moduleBlocker,
+ {
+ fetchState: () => {
+ let state = xpcomBlocker.state;
+ if (state) {
+ return PropertyBagConverter.toValue(state);
+ }
+ return null;
+ },
+ filename: fileName,
+ lineNumber: lineNumber,
+ stack: stack,
+ });
+ },
+
+ removeBlocker: function(xpcomBlocker) {
+ let moduleBlocker = this._getPromisified(xpcomBlocker);
+ if (!moduleBlocker) {
+ return false;
+ }
+ this._deletePromisified(xpcomBlocker);
+ return this._moduleClient.removeBlocker(moduleBlocker);
+ },
+
+ /* ........ QueryInterface .............. */
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAsyncShutdownBarrier]),
+ classID: Components.ID("{314e9e96-cc37-4d5c-843b-54709ce11426}"),
+};
+
+/**
+ * Construct an instance of nsIAsyncShutdownBarrier from an instance
+ * of AsyncShutdown.Barrier.
+ *
+ * @param {object} moduleBarrier an instance if
+ * `AsyncShutdown.Barrier`. This instance will serve as back-end for
+ * all methods.
+ * @constructor
+ */
+function nsAsyncShutdownBarrier(moduleBarrier) {
+ this._client = new nsAsyncShutdownClient(moduleBarrier.client);
+ this._moduleBarrier = moduleBarrier;
+}
+nsAsyncShutdownBarrier.prototype = {
+ get state() {
+ return PropertyBagConverter.fromValue(this._moduleBarrier.state);
+ },
+ get client() {
+ return this._client;
+ },
+ wait: function(onReady) {
+ this._moduleBarrier.wait().then(() => {
+ onReady.done();
+ });
+ // By specification, _moduleBarrier.wait() cannot reject.
+ },
+
+ /* ........ QueryInterface .............. */
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAsyncShutdownBarrier]),
+ classID: Components.ID("{29a0e8b5-9111-4c09-a0eb-76cd02bf20fa}"),
+};
+
+function nsAsyncShutdownService() {
+ // Cache for the getters
+
+ for (let _k of
+ [// Parent process
+ "profileBeforeChange",
+ "profileChangeTeardown",
+ "quitApplicationGranted",
+ "sendTelemetry",
+
+ // Child processes
+ "contentChildShutdown",
+
+ // All processes
+ "webWorkersShutdown",
+ "xpcomWillShutdown",
+ ]) {
+ let k = _k;
+ Object.defineProperty(this, k, {
+ configurable: true,
+ get: function() {
+ delete this[k];
+ let wrapped = AsyncShutdown[k]; // May be undefined, if we're on the wrong process.
+ let result = wrapped ? new nsAsyncShutdownClient(wrapped) : undefined;
+ Object.defineProperty(this, k, {
+ value: result
+ });
+ return result;
+ }
+ });
+ }
+
+ // Hooks for testing purpose
+ this.wrappedJSObject = {
+ _propertyBagConverter: PropertyBagConverter
+ };
+}
+nsAsyncShutdownService.prototype = {
+ makeBarrier: function(name) {
+ return new nsAsyncShutdownBarrier(new AsyncShutdown.Barrier(name));
+ },
+
+ /* ........ QueryInterface .............. */
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAsyncShutdownService]),
+ classID: Components.ID("{35c496de-a115-475d-93b5-ffa3f3ae6fe3}"),
+};
+
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+ nsAsyncShutdownService,
+ nsAsyncShutdownBarrier,
+ nsAsyncShutdownClient,
+]);
diff --git a/toolkit/components/asyncshutdown/nsAsyncShutdown.manifest b/toolkit/components/asyncshutdown/nsAsyncShutdown.manifest
new file mode 100644
index 0000000000..67f247902f
--- /dev/null
+++ b/toolkit/components/asyncshutdown/nsAsyncShutdown.manifest
@@ -0,0 +1,2 @@
+component {35c496de-a115-475d-93b5-ffa3f3ae6fe3} nsAsyncShutdown.js
+contract @mozilla.org/async-shutdown-service;1 {35c496de-a115-475d-93b5-ffa3f3ae6fe3}
diff --git a/toolkit/components/asyncshutdown/nsIAsyncShutdown.idl b/toolkit/components/asyncshutdown/nsIAsyncShutdown.idl
new file mode 100644
index 0000000000..216c8047e0
--- /dev/null
+++ b/toolkit/components/asyncshutdown/nsIAsyncShutdown.idl
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A mechanism for specifying shutdown dependencies between
+ * asynchronous services.
+ *
+ * Note that this XPCOM component is designed primarily for C++
+ * clients. JavaScript clients should rather use AsyncShutdown.jsm,
+ * which provides a better API and better error reporting for them.
+ */
+
+
+#include "nsISupports.idl"
+#include "nsIPropertyBag.idl"
+#include "nsIVariant.idl"
+
+interface nsIAsyncShutdownClient;
+
+/**
+ * A blocker installed by a client to be informed during some stage of
+ * shutdown and block shutdown asynchronously until some condition is
+ * complete.
+ *
+ * If you wish to use AsyncShutdown, you will need to implement this
+ * interface (and only this interface).
+ */
+[scriptable, uuid(4ef43f29-6715-4b57-a750-2ff83695ddce)]
+interface nsIAsyncShutdownBlocker: nsISupports {
+ /**
+ * The *unique* name of the blocker.
+ *
+ * By convention, it should respect the following format:
+ * "MyModuleName: Doing something while it's time"
+ * e.g.
+ * "OS.File: Flushing before profile-before-change"
+ *
+ * This attribute is uploaded as part of crash reports.
+ */
+ readonly attribute AString name;
+
+ /**
+ * Inform the blocker that the stage of shutdown has started.
+ * Shutdown will NOT proceed until `aBarrierClient.removeBlocker(this)`
+ * has been called.
+ */
+ void blockShutdown(in nsIAsyncShutdownClient aBarrierClient);
+
+ /**
+ * The current state of the blocker.
+ *
+ * In case of crash, this is converted to JSON and attached to
+ * the crash report.
+ *
+ * This field may be used to provide JSON-style data structures.
+ * For this purpose, use
+ * - nsIPropertyBag to represent objects;
+ * - nsIVariant to represent field values (which may hold nsIPropertyBag
+ * themselves).
+ */
+ readonly attribute nsIPropertyBag state;
+};
+
+/**
+ * A client for a nsIAsyncShutdownBarrier.
+ */
+[scriptable, uuid(d2031049-b990-43a2-95be-59f8a3ca5954)]
+interface nsIAsyncShutdownClient: nsISupports {
+ /**
+ * The name of the barrier.
+ */
+ readonly attribute AString name;
+
+ /**
+ * Add a blocker.
+ *
+ * After a `blocker` has been added with `addBlocker`, if it is not
+ * removed with `removeBlocker`, this will, by design, eventually
+ * CAUSE A CRASH.
+ *
+ * Calling `addBlocker` once nsIAsyncShutdownBarrier::wait() has been
+ * called on the owning barrier returns an error.
+ *
+ * @param aBlocker The blocker to add. Once
+ * nsIAsyncShutdownBarrier::wait() has been called, it will not
+ * call its `aOnReady` callback until all blockers have been
+ * removed, each by a call to `removeBlocker`.
+ * @param aFileName The filename of the callsite, as given by `__FILE__`.
+ * @param aLineNumber The linenumber of the callsite, as given by `__LINE__`.
+ * @param aStack Information on the stack that lead to this call. Generally
+ * empty when called from C++.
+ */
+ void addBlocker(in nsIAsyncShutdownBlocker aBlocker,
+ in AString aFileName,
+ in long aLineNumber,
+ in AString aStack);
+
+ /**
+ * Remove a blocker.
+ *
+ * @param aBlocker A blocker previously added to this client through
+ * `addBlocker`. Noop if the blocker has never been added or has been
+ * removed already.
+ */
+ void removeBlocker(in nsIAsyncShutdownBlocker aBlocker);
+
+ /**
+ * The JS implementation of the client.
+ *
+ * It is strongly recommended that JS clients of this API use
+ * `jsclient` instead of the `nsIAsyncShutdownClient`. See
+ * AsyncShutdown.jsm for more information on the JS version of
+ * this API.
+ */
+ readonly attribute jsval jsclient;
+};
+
+/**
+ * Callback invoked once all blockers of a barrier have been removed.
+ */
+[scriptable, function, uuid(910c9309-1da0-4dd0-8bdb-a325a38c604e)]
+interface nsIAsyncShutdownCompletionCallback: nsISupports {
+ /**
+ * The operation has been completed.
+ */
+ void done();
+};
+
+/**
+ * A stage of shutdown that supports blocker registration.
+ */
+[scriptable, uuid(50fa8a86-9c91-4256-8389-17d310adec90)]
+interface nsIAsyncShutdownBarrier: nsISupports {
+
+ /**
+ * The blocker registration capability. Most services may wish to
+ * publish this capability to let services that depend on it register
+ * blockers.
+ */
+ readonly attribute nsIAsyncShutdownClient client;
+
+ /**
+ * The state of all the blockers of the barrier.
+ *
+ * See the documentation of `nsIAsyncShutdownBlocker` for the
+ * format.
+ */
+ readonly attribute nsIPropertyBag state;
+
+ /**
+ * Wait for all blockers to complete.
+ *
+ * Method `aOnReady` will be called once all blockers have finished.
+ * The callback always receives NS_OK.
+ */
+ void wait(in nsIAsyncShutdownCompletionCallback aOnReady);
+};
+
+/**
+ * A service that allows registering shutdown-time dependencies.
+ */
+[scriptable, uuid(db365c78-c860-4e64-9a63-25b73f89a016)]
+interface nsIAsyncShutdownService: nsISupports {
+ /**
+ * Create a new barrier.
+ *
+ * By convention, the name should respect the following format:
+ * "MyModuleName: Doing something while it's time"
+ * e.g.
+ * "OS.File: Waiting for clients to flush before shutting down"
+ *
+ * This attribute is uploaded as part of crash reports.
+ */
+ nsIAsyncShutdownBarrier makeBarrier(in AString aName);
+
+
+ // Barriers for global shutdown stages in the parent process.
+
+ /**
+ * Barrier for notification profile-before-change.
+ */
+ readonly attribute nsIAsyncShutdownClient profileBeforeChange;
+
+ /**
+ * Barrier for notification profile-change-teardown.
+ */
+ readonly attribute nsIAsyncShutdownClient profileChangeTeardown;
+
+ /**
+ * Barrier for notification quit-application-granted.
+ */
+ readonly attribute nsIAsyncShutdownClient quitApplicationGranted;
+
+ /**
+ * Barrier for notification profile-before-change-telemetry.
+ */
+ readonly attribute nsIAsyncShutdownClient sendTelemetry;
+
+
+ // Barriers for global shutdown stages in all processes.
+
+ /**
+ * Barrier for notification web-workers-shutdown.
+ */
+ readonly attribute nsIAsyncShutdownClient webWorkersShutdown;
+
+ /**
+ * Barrier for notification xpcom-will-shutdown.
+ */
+ readonly attribute nsIAsyncShutdownClient xpcomWillShutdown;
+
+ // Don't add a barrier for content-child-shutdown because this
+ // makes it easier to cause shutdown hangs.
+
+};
+
+%{C++
+#define NS_ASYNCSHUTDOWNSERVICE_CONTRACTID "@mozilla.org/async-shutdown-service;1"
+%}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js b/toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/head.js b/toolkit/components/asyncshutdown/tests/xpcshell/head.js
new file mode 100644
index 0000000000..9de489808d
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/head.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+var asyncShutdownService = Cc["@mozilla.org/async-shutdown-service;1"].
+ getService(Ci.nsIAsyncShutdownService);
+
+
+Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+
+/**
+ * Utility function used to provide the same API for various sources
+ * of async shutdown barriers.
+ *
+ * @param {string} kind One of
+ * - "phase" to test an AsyncShutdown phase;
+ * - "barrier" to test an instance of AsyncShutdown.Barrier;
+ * - "xpcom-barrier" to test an instance of nsIAsyncShutdownBarrier;
+ * - "xpcom-barrier-unwrapped" to test the field `jsclient` of a nsIAsyncShutdownClient.
+ *
+ * @return An object with the following methods:
+ * - addBlocker() - the same method as AsyncShutdown phases and barrier clients
+ * - wait() - trigger the resolution of the lock
+ */
+function makeLock(kind) {
+ if (kind == "phase") {
+ let topic = "test-Phase-" + ++makeLock.counter;
+ let phase = AsyncShutdown._getPhase(topic);
+ return {
+ addBlocker: function(...args) {
+ return phase.addBlocker(...args);
+ },
+ removeBlocker: function(blocker) {
+ return phase.removeBlocker(blocker);
+ },
+ wait: function() {
+ Services.obs.notifyObservers(null, topic, null);
+ return Promise.resolve();
+ }
+ };
+ } else if (kind == "barrier") {
+ let name = "test-Barrier-" + ++makeLock.counter;
+ let barrier = new AsyncShutdown.Barrier(name);
+ return {
+ addBlocker: barrier.client.addBlocker,
+ removeBlocker: barrier.client.removeBlocker,
+ wait: function() {
+ return barrier.wait();
+ }
+ };
+ } else if (kind == "xpcom-barrier") {
+ let name = "test-xpcom-Barrier-" + ++makeLock.counter;
+ let barrier = asyncShutdownService.makeBarrier(name);
+ return {
+ addBlocker: function(blockerName, condition, state) {
+ if (condition == null) {
+ // Slight trick as `null` or `undefined` cannot be used as keys
+ // for `xpcomMap`. Note that this has no incidence on the result
+ // of the test as the XPCOM interface imposes that the condition
+ // is a method, so it cannot be `null`/`undefined`.
+ condition = "<this case can't happen with the xpcom interface>";
+ }
+ let blocker = makeLock.xpcomMap.get(condition);
+ if (!blocker) {
+ blocker = {
+ name: blockerName,
+ state: state,
+ blockShutdown: function(aBarrierClient) {
+ return Task.spawn(function*() {
+ try {
+ if (typeof condition == "function") {
+ yield Promise.resolve(condition());
+ } else {
+ yield Promise.resolve(condition);
+ }
+ } finally {
+ aBarrierClient.removeBlocker(blocker);
+ }
+ });
+ },
+ };
+ makeLock.xpcomMap.set(condition, blocker);
+ }
+ let {fileName, lineNumber, stack} = (new Error());
+ return barrier.client.addBlocker(blocker, fileName, lineNumber, stack);
+ },
+ removeBlocker: function(condition) {
+ let blocker = makeLock.xpcomMap.get(condition);
+ if (!blocker) {
+ return;
+ }
+ barrier.client.removeBlocker(blocker);
+ },
+ wait: function() {
+ return new Promise(resolve => {
+ barrier.wait(resolve);
+ });
+ }
+ };
+ } else if ("unwrapped-xpcom-barrier") {
+ let name = "unwrapped-xpcom-barrier-" + ++makeLock.counter;
+ let barrier = asyncShutdownService.makeBarrier(name);
+ let client = barrier.client.jsclient;
+ return {
+ addBlocker: client.addBlocker,
+ removeBlocker: client.removeBlocker,
+ wait: function() {
+ return new Promise(resolve => {
+ barrier.wait(resolve);
+ });
+ }
+ };
+ }
+ throw new TypeError("Unknown kind " + kind);
+}
+makeLock.counter = 0;
+makeLock.xpcomMap = new Map(); // Note: Not a WeakMap as we wish to handle non-gc-able keys (e.g. strings)
+
+/**
+ * An asynchronous task that takes several ticks to complete.
+ *
+ * @param {*=} resolution The value with which the resulting promise will be
+ * resolved once the task is complete. This may be a rejected promise,
+ * in which case the resulting promise will itself be rejected.
+ * @param {object=} outResult An object modified by side-effect during the task.
+ * Initially, its field |isFinished| is set to |false|. Once the task is
+ * complete, its field |isFinished| is set to |true|.
+ *
+ * @return {promise} A promise fulfilled once the task is complete
+ */
+function longRunningAsyncTask(resolution = undefined, outResult = {}) {
+ outResult.isFinished = false;
+ if (!("countFinished" in outResult)) {
+ outResult.countFinished = 0;
+ }
+ let deferred = Promise.defer();
+ do_timeout(100, function() {
+ ++outResult.countFinished;
+ outResult.isFinished = true;
+ deferred.resolve(resolution);
+ });
+ return deferred.promise;
+}
+
+function get_exn(f) {
+ try {
+ f();
+ return null;
+ } catch (ex) {
+ return ex;
+ }
+}
+
+function do_check_exn(exn, constructor) {
+ do_check_neq(exn, null);
+ if (exn.name == constructor) {
+ do_check_eq(exn.constructor.name, constructor);
+ return;
+ }
+ do_print("Wrong error constructor");
+ do_print(exn.constructor.name);
+ do_print(exn.stack);
+ do_check_true(false);
+}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js
new file mode 100644
index 0000000000..f1aebc3ad2
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_no_condition() {
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ do_print("Testing a barrier with no condition (" + kind + ")");
+ let lock = makeLock(kind);
+ yield lock.wait();
+ do_print("Barrier with no condition didn't lock");
+ }
+});
+
+add_task(function* test_phase_various_failures() {
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ do_print("Kind: " + kind);
+ // Testing with wrong arguments
+ let lock = makeLock(kind);
+
+ Assert.throws(() => lock.addBlocker(), /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/);
+ Assert.throws(() => lock.addBlocker(null, true), /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/);
+
+ if (kind != "xpcom-barrier") {
+ // xpcom-barrier actually expects a string in that position
+ Assert.throws(() => lock.addBlocker("Test 2", () => true, "not a function"), /TypeError/);
+ }
+
+ // Attempting to add a blocker after we are done waiting
+ yield lock.wait();
+ Assert.throws(() => lock.addBlocker("Test 3", () => true), /is finished/);
+ }
+});
+
+add_task(function* test_reentrant() {
+ do_print("Ensure that we can call addBlocker from within a blocker");
+
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ do_print("Kind: " + kind);
+ let lock = makeLock(kind);
+
+ let deferredOuter = PromiseUtils.defer();
+ let deferredInner = PromiseUtils.defer();
+ let deferredBlockInner = PromiseUtils.defer();
+
+ lock.addBlocker("Outer blocker", () => {
+ do_print("Entering outer blocker");
+ deferredOuter.resolve();
+ lock.addBlocker("Inner blocker", () => {
+ do_print("Entering inner blocker");
+ deferredInner.resolve();
+ return deferredBlockInner.promise;
+ });
+ });
+
+ // Note that phase-style locks spin the event loop and do not return from
+ // `lock.wait()` until after all blockers have been resolved. Therefore,
+ // to be able to test them, we need to dispatch the following steps to the
+ // event loop before calling `lock.wait()`, which we do by forcing
+ // a Promise.resolve().
+ //
+ let promiseSteps = Task.spawn(function* () {
+ yield Promise.resolve();
+
+ do_print("Waiting until we have entered the outer blocker");
+ yield deferredOuter.promise;
+
+ do_print("Waiting until we have entered the inner blocker");
+ yield deferredInner.promise;
+
+ do_print("Allowing the lock to resolve")
+ deferredBlockInner.resolve();
+ });
+
+ do_print("Starting wait");
+ yield lock.wait();
+
+ do_print("Waiting until all steps have been walked");
+ yield promiseSteps;
+ }
+});
+
+
+add_task(function* test_phase_removeBlocker() {
+ do_print("Testing that we can call removeBlocker before, during and after the call to wait()");
+
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+
+ do_print("Switching to kind " + kind);
+ do_print("Attempt to add then remove a blocker before wait()");
+ let lock = makeLock(kind);
+ let blocker = () => {
+ do_print("This promise will never be resolved");
+ return Promise.defer().promise;
+ };
+
+ lock.addBlocker("Wait forever", blocker);
+ let do_remove_blocker = function(aLock, aBlocker, aShouldRemove) {
+ do_print("Attempting to remove blocker " + aBlocker + ", expecting result " + aShouldRemove);
+ if (kind == "xpcom-barrier") {
+ // The xpcom variant always returns `undefined`, so we can't
+ // check its result.
+ aLock.removeBlocker(aBlocker);
+ return;
+ }
+ do_check_eq(aLock.removeBlocker(aBlocker), aShouldRemove);
+ };
+ do_remove_blocker(lock, blocker, true);
+ do_remove_blocker(lock, blocker, false);
+ do_print("Attempt to remove non-registered blockers before wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ do_print("Waiting (should lift immediately)");
+ yield lock.wait();
+
+ do_print("Attempt to add a blocker then remove it during wait()");
+ lock = makeLock(kind);
+ let blockers = [
+ () => {
+ do_print("This blocker will self-destruct");
+ do_remove_blocker(lock, blockers[0], true);
+ return Promise.defer().promise;
+ },
+ () => {
+ do_print("This blocker will self-destruct twice");
+ do_remove_blocker(lock, blockers[1], true);
+ do_remove_blocker(lock, blockers[1], false);
+ return Promise.defer().promise;
+ },
+ () => {
+ do_print("Attempt to remove non-registered blockers during wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ }
+ ];
+ for (let i in blockers) {
+ lock.addBlocker("Wait forever again: " + i, blockers[i]);
+ }
+ do_print("Waiting (should lift very quickly)");
+ yield lock.wait();
+ do_remove_blocker(lock, blockers[0], false);
+
+
+ do_print("Attempt to remove a blocker after wait");
+ lock = makeLock(kind);
+ blocker = Promise.resolve.bind(Promise);
+ yield lock.wait();
+ do_remove_blocker(lock, blocker, false);
+
+ do_print("Attempt to remove non-registered blocker after wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ }
+
+});
+
+add_task(function* test_state() {
+ do_print("Testing information contained in `state`");
+
+ let BLOCKER_NAME = "test_state blocker " + Math.random();
+
+ // Set up the barrier. Note that we cannot test `barrier.state`
+ // immediately, as it initially contains "Not started"
+ let barrier = new AsyncShutdown.Barrier("test_filename");
+ let deferred = Promise.defer();
+ let {filename, lineNumber} = Components.stack;
+ barrier.client.addBlocker(BLOCKER_NAME,
+ function() {
+ return deferred.promise;
+ });
+
+ let promiseDone = barrier.wait();
+
+ // Now that we have called `wait()`, the state contains interesting things
+ let state = barrier.state[0];
+ do_print("State: " + JSON.stringify(barrier.state, null, "\t"));
+ Assert.equal(state.filename, filename);
+ Assert.equal(state.lineNumber, lineNumber + 1);
+ Assert.equal(state.name, BLOCKER_NAME);
+ Assert.ok(state.stack.some(x => x.includes("test_state")), "The stack contains the caller function's name");
+ Assert.ok(state.stack.some(x => x.includes(filename)), "The stack contains the calling file's name");
+
+ deferred.resolve();
+ yield promiseDone;
+});
+
+add_task(function*() {
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+});
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js
new file mode 100644
index 0000000000..33da1f53fb
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+//
+// This file contains tests that need to leave uncaught asynchronous
+// errors. If your test catches all its asynchronous errors, please
+// put it in another file.
+//
+
+Promise.Debugging.clearUncaughtErrorObservers();
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_phase_simple_async() {
+ do_print("Testing various combinations of a phase with a single condition");
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
+ for (let resolution of [arg, Promise.reject(arg)]) {
+ for (let success of [false, true]) {
+ for (let state of [[null],
+ [],
+ [() => "some state"],
+ [function() {
+ throw new Error("State BOOM"); }],
+ [function() {
+ return {
+ toJSON: function() {
+ throw new Error("State.toJSON BOOM");
+ }
+ };
+ }]]) {
+ // Asynchronous phase
+ do_print("Asynchronous test with " + arg + ", " + resolution + ", " + kind);
+ let lock = makeLock(kind);
+ let outParam = { isFinished: false };
+ lock.addBlocker(
+ "Async test",
+ function() {
+ if (success) {
+ return longRunningAsyncTask(resolution, outParam);
+ }
+ throw resolution;
+ },
+ ...state
+ );
+ do_check_false(outParam.isFinished);
+ yield lock.wait();
+ do_check_eq(outParam.isFinished, success);
+ }
+ }
+
+ // Synchronous phase - just test that we don't throw/freeze
+ do_print("Synchronous test with " + arg + ", " + resolution + ", " + kind);
+ let lock = makeLock(kind);
+ lock.addBlocker(
+ "Sync test",
+ resolution
+ );
+ yield lock.wait();
+ }
+ }
+ }
+});
+
+add_task(function* test_phase_many() {
+ do_print("Testing various combinations of a phase with many conditions");
+ for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
+ let lock = makeLock(kind);
+ let outParams = [];
+ for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
+ for (let resolve of [true, false]) {
+ do_print("Testing with " + kind + ", " + arg + ", " + resolve);
+ let resolution = resolve ? arg : Promise.reject(arg);
+ let outParam = { isFinished: false };
+ lock.addBlocker(
+ "Test " + Math.random(),
+ () => longRunningAsyncTask(resolution, outParam)
+ );
+ }
+ }
+ do_check_true(outParams.every((x) => !x.isFinished));
+ yield lock.wait();
+ do_check_true(outParams.every((x) => x.isFinished));
+ }
+});
+
+
+
+
+add_task(function*() {
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+});
+
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js
new file mode 100644
index 0000000000..c6c9231871
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test conversion between nsIPropertyBag and JS values.
+ */
+
+var PropertyBagConverter = asyncShutdownService.wrappedJSObject._propertyBagConverter;
+
+function run_test() {
+ test_conversions();
+}
+
+function normalize(obj) {
+ if (obj == null || typeof obj != "object") {
+ return obj;
+ }
+ if (Array.isArray(obj)) {
+ return obj.map(normalize);
+ }
+ let result = {};
+ for (let k of Object.keys(obj).sort()) {
+ result[k] = normalize(obj[k]);
+ }
+ return result;
+}
+
+function test_conversions() {
+ const SAMPLES = [
+ // Simple values
+ 1,
+ true,
+ "string",
+ null,
+
+ // Objects
+ {
+ a: 1,
+ b: true,
+ c: "string",
+ d:.5,
+ e: [2, false, "another string", .3],
+ f: [],
+ g: {
+ a2: 1,
+ b2: true,
+ c2: "string",
+ d2:.5,
+ e2: [2, false, "another string", .3],
+ f2: [],
+ g2: [{
+ a3: 1,
+ b3: true,
+ c3: "string",
+ d3:.5,
+ e3: [2, false, "another string", .3],
+ f3: [],
+ g3: {}
+ }]
+ }
+ }];
+
+ for (let sample of SAMPLES) {
+ let stringified = JSON.stringify(normalize(sample), null, "\t");
+ do_print("Testing conversions of " + stringified);
+ let rewrites = [sample];
+ for (let i = 1; i < 3; ++i) {
+ let source = rewrites[i - 1];
+ let bag = PropertyBagConverter.fromValue(source);
+ do_print(" => " + bag);
+ if (source == null) {
+ Assert.ok(bag == null, "The bag is null");
+ } else if (typeof source == "object") {
+ Assert.ok(bag instanceof Ci.nsIPropertyBag, "The bag is a property bag");
+ } else {
+ Assert.ok(typeof bag != "object", "The bag is not an object");
+ }
+ let dest = PropertyBagConverter.toValue(bag);
+ let restringified = JSON.stringify(normalize(dest), null, "\t");
+ do_print("Comparing");
+ do_print(stringified);
+ do_print(restringified);
+ Assert.deepEqual(sample, dest, "Testing after " + i + " conversions");
+ rewrites.push(dest);
+ }
+ }
+}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini b/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..f573955bc9
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head=head.js
+tail=
+skip-if = toolkit == 'android'
+
+[test_AsyncShutdown.js]
+[test_AsyncShutdown_leave_uncaught.js]
+[test_converters.js]
diff --git a/toolkit/components/autocomplete/moz.build b/toolkit/components/autocomplete/moz.build
new file mode 100644
index 0000000000..f3817d642e
--- /dev/null
+++ b/toolkit/components/autocomplete/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/unit/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIAutoCompleteController.idl',
+ 'nsIAutoCompleteInput.idl',
+ 'nsIAutoCompletePopup.idl',
+ 'nsIAutoCompleteResult.idl',
+ 'nsIAutoCompleteSearch.idl',
+ 'nsIAutoCompleteSimpleResult.idl',
+]
+
+XPIDL_MODULE = 'autocomplete'
+
+UNIFIED_SOURCES += [
+ 'nsAutoCompleteController.cpp',
+ 'nsAutoCompleteSimpleResult.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Autocomplete')
diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.cpp b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
new file mode 100644
index 0000000000..5d69ea1a32
--- /dev/null
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -0,0 +1,2087 @@
+/* -*- 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 "nsAutoCompleteController.h"
+#include "nsAutoCompleteSimpleResult.h"
+
+#include "nsAutoPtr.h"
+#include "nsNetCID.h"
+#include "nsIIOService.h"
+#include "nsToolkitCompsCID.h"
+#include "nsIServiceManager.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsITreeBoxObject.h"
+#include "nsITreeColumns.h"
+#include "nsIObserverService.h"
+#include "nsIDOMKeyEvent.h"
+#include "mozilla/Services.h"
+#include "mozilla/ModuleUtils.h"
+
+static const char *kAutoCompleteSearchCID = "@mozilla.org/autocomplete/search;1?name=";
+
+namespace {
+
+void
+SetTextValue(nsIAutoCompleteInput* aInput,
+ const nsString& aValue,
+ uint16_t aReason) {
+ nsresult rv = aInput->SetTextValueWithReason(aValue, aReason);
+ if (NS_FAILED(rv)) {
+ aInput->SetTextValue(aValue);
+ }
+}
+
+} // anon namespace
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsAutoCompleteController)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsAutoCompleteController)
+ tmp->SetInput(nullptr);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsAutoCompleteController)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInput)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSearches)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResults)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsAutoCompleteController)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAutoCompleteController)
+NS_INTERFACE_TABLE_HEAD(nsAutoCompleteController)
+ NS_INTERFACE_TABLE(nsAutoCompleteController, nsIAutoCompleteController,
+ nsIAutoCompleteObserver, nsITimerCallback, nsITreeView)
+ NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsAutoCompleteController)
+NS_INTERFACE_MAP_END
+
+nsAutoCompleteController::nsAutoCompleteController() :
+ mDefaultIndexCompleted(false),
+ mPopupClosedByCompositionStart(false),
+ mProhibitAutoFill(false),
+ mUserClearedAutoFill(false),
+ mClearingAutoFillSearchesAgain(false),
+ mCompositionState(eCompositionState_None),
+ mSearchStatus(nsAutoCompleteController::STATUS_NONE),
+ mRowCount(0),
+ mSearchesOngoing(0),
+ mSearchesFailed(0),
+ mFirstSearchResult(false),
+ mImmediateSearchesCount(0),
+ mCompletedSelectionIndex(-1)
+{
+}
+
+nsAutoCompleteController::~nsAutoCompleteController()
+{
+ SetInput(nullptr);
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIAutoCompleteController
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetSearchStatus(uint16_t *aSearchStatus)
+{
+ *aSearchStatus = mSearchStatus;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetMatchCount(uint32_t *aMatchCount)
+{
+ *aMatchCount = mRowCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetInput(nsIAutoCompleteInput **aInput)
+{
+ *aInput = mInput;
+ NS_IF_ADDREF(*aInput);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SetInitiallySelectedIndex(int32_t aSelectedIndex)
+{
+ // First forward to the popup.
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ NS_ENSURE_STATE(input);
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_STATE(popup);
+ popup->SetSelectedIndex(aSelectedIndex);
+
+ // Now take care of internal stuff.
+ bool completeSelection;
+ if (NS_SUCCEEDED(input->GetCompleteSelectedIndex(&completeSelection)) &&
+ completeSelection) {
+ mCompletedSelectionIndex = aSelectedIndex;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SetInput(nsIAutoCompleteInput *aInput)
+{
+ // Don't do anything if the input isn't changing.
+ if (mInput == aInput)
+ return NS_OK;
+
+ // Clear out the current search context
+ if (mInput) {
+ // Stop all searches in case they are async.
+ StopSearch();
+ ClearResults();
+ ClosePopup();
+ mSearches.Clear();
+ }
+
+ mInput = aInput;
+
+ // Nothing more to do if the input was just being set to null.
+ if (!aInput)
+ return NS_OK;
+
+ nsAutoString newValue;
+ aInput->GetTextValue(newValue);
+
+ // Clear out this reference in case the new input's popup has no tree
+ mTree = nullptr;
+
+ // Reset all search state members to default values
+ mSearchString = newValue;
+ mPlaceholderCompletionString.Truncate();
+ mDefaultIndexCompleted = false;
+ mProhibitAutoFill = false;
+ mSearchStatus = nsIAutoCompleteController::STATUS_NONE;
+ mRowCount = 0;
+ mSearchesOngoing = 0;
+ mCompletedSelectionIndex = -1;
+
+ // Initialize our list of search objects
+ uint32_t searchCount;
+ aInput->GetSearchCount(&searchCount);
+ mResults.SetCapacity(searchCount);
+ mSearches.SetCapacity(searchCount);
+ mImmediateSearchesCount = 0;
+
+ const char *searchCID = kAutoCompleteSearchCID;
+
+ // Since the controller can be used as a service it's important to reset this.
+ mClearingAutoFillSearchesAgain = false;
+
+ for (uint32_t i = 0; i < searchCount; ++i) {
+ // Use the search name to create the contract id string for the search service
+ nsAutoCString searchName;
+ aInput->GetSearchAt(i, searchName);
+ nsAutoCString cid(searchCID);
+ cid.Append(searchName);
+
+ // Use the created cid to get a pointer to the search service and store it for later
+ nsCOMPtr<nsIAutoCompleteSearch> search = do_GetService(cid.get());
+ if (search) {
+ mSearches.AppendObject(search);
+
+ // Count immediate searches.
+ nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc =
+ do_QueryInterface(search);
+ if (searchDesc) {
+ uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
+ if (NS_SUCCEEDED(searchDesc->GetSearchType(&searchType)) &&
+ searchType == nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE) {
+ mImmediateSearchesCount++;
+ }
+
+ if (!mClearingAutoFillSearchesAgain) {
+ searchDesc->GetClearingAutoFillSearchesAgain(&mClearingAutoFillSearchesAgain);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::StartSearch(const nsAString &aSearchString)
+{
+ mSearchString = aSearchString;
+ StartSearches();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleText(bool *_retval)
+{
+ *_retval = false;
+ // Note: the events occur in the following order when IME is used.
+ // 1. a compositionstart event(HandleStartComposition)
+ // 2. some input events (HandleText), eCompositionState_Composing
+ // 3. a compositionend event(HandleEndComposition)
+ // 4. an input event(HandleText), eCompositionState_Committing
+ // We should do nothing during composition.
+ if (mCompositionState == eCompositionState_Composing) {
+ return NS_OK;
+ }
+
+ bool handlingCompositionCommit =
+ (mCompositionState == eCompositionState_Committing);
+ bool popupClosedByCompositionStart = mPopupClosedByCompositionStart;
+ if (handlingCompositionCommit) {
+ mCompositionState = eCompositionState_None;
+ mPopupClosedByCompositionStart = false;
+ }
+
+ if (!mInput) {
+ // Stop all searches in case they are async.
+ StopSearch();
+ // Note: if now is after blur and IME end composition,
+ // check mInput before calling.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
+ NS_ERROR("Called before attaching to the control or after detaching from the control");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ nsAutoString newValue;
+ input->GetTextValue(newValue);
+
+ // Stop all searches in case they are async.
+ StopSearch();
+
+ if (!mInput) {
+ // StopSearch() can call PostSearchCleanup() which might result
+ // in a blur event, which could null out mInput, so we need to check it
+ // again. See bug #395344 for more details
+ return NS_OK;
+ }
+
+ bool disabled;
+ input->GetDisableAutoComplete(&disabled);
+ NS_ENSURE_TRUE(!disabled, NS_OK);
+
+ // Usually we don't search again if the new string is the same as the last one.
+ // However, if this is called immediately after compositionend event,
+ // we need to search the same value again since the search was canceled
+ // at compositionstart event handler.
+ // The new string might also be the same as the last search if the autofilled
+ // portion was cleared. In this case, we may want to search again.
+
+ // Whether the user removed some text at the end.
+ bool userRemovedText =
+ newValue.Length() < mSearchString.Length() &&
+ Substring(mSearchString, 0, newValue.Length()).Equals(newValue);
+
+ // Whether the user is repeating the previous search.
+ bool repeatingPreviousSearch = !userRemovedText &&
+ newValue.Equals(mSearchString);
+
+ mUserClearedAutoFill =
+ repeatingPreviousSearch &&
+ newValue.Length() < mPlaceholderCompletionString.Length() &&
+ Substring(mPlaceholderCompletionString, 0, newValue.Length()).Equals(newValue);
+ bool searchAgainOnAutoFillClear = mUserClearedAutoFill && mClearingAutoFillSearchesAgain;
+
+ if (!handlingCompositionCommit &&
+ !searchAgainOnAutoFillClear &&
+ newValue.Length() > 0 &&
+ repeatingPreviousSearch) {
+ return NS_OK;
+ }
+
+ if (userRemovedText || searchAgainOnAutoFillClear) {
+ if (userRemovedText) {
+ // We need to throw away previous results so we don't try to search
+ // through them again.
+ ClearResults();
+ }
+ mProhibitAutoFill = true;
+ mPlaceholderCompletionString.Truncate();
+ } else {
+ mProhibitAutoFill = false;
+ }
+
+ mSearchString = newValue;
+
+ // Don't search if the value is empty
+ if (newValue.Length() == 0) {
+ // If autocomplete popup was closed by compositionstart event handler,
+ // we should reopen it forcibly even if the value is empty.
+ if (popupClosedByCompositionStart && handlingCompositionCommit) {
+ bool cancel;
+ HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel);
+ return NS_OK;
+ }
+ ClosePopup();
+ return NS_OK;
+ }
+
+ *_retval = true;
+ StartSearches();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
+ nsIDOMEvent *aEvent,
+ bool *_retval)
+{
+ *_retval = false;
+ if (!mInput)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ // allow the event through unless there is something selected in the popup
+ input->GetPopupOpen(_retval);
+ if (*_retval) {
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+
+ if (popup) {
+ int32_t selectedIndex;
+ popup->GetSelectedIndex(&selectedIndex);
+ *_retval = selectedIndex >= 0;
+ }
+ }
+
+ // Stop the search, and handle the enter.
+ StopSearch();
+ EnterMatch(aIsPopupSelection, aEvent);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleEscape(bool *_retval)
+{
+ *_retval = false;
+ if (!mInput)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ // allow the event through if the popup is closed
+ input->GetPopupOpen(_retval);
+
+ // Stop all searches in case they are async.
+ StopSearch();
+ ClearResults();
+ RevertTextValue();
+ ClosePopup();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleStartComposition()
+{
+ NS_ENSURE_TRUE(mCompositionState != eCompositionState_Composing, NS_OK);
+
+ mPopupClosedByCompositionStart = false;
+ mCompositionState = eCompositionState_Composing;
+
+ if (!mInput)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ bool disabled;
+ input->GetDisableAutoComplete(&disabled);
+ if (disabled)
+ return NS_OK;
+
+ // Stop all searches in case they are async.
+ StopSearch();
+
+ bool isOpen = false;
+ input->GetPopupOpen(&isOpen);
+ if (isOpen) {
+ ClosePopup();
+
+ bool stillOpen = false;
+ input->GetPopupOpen(&stillOpen);
+ mPopupClosedByCompositionStart = !stillOpen;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleEndComposition()
+{
+ NS_ENSURE_TRUE(mCompositionState == eCompositionState_Composing, NS_OK);
+
+ // We can't yet retrieve the committed value from the editor, since it isn't
+ // completely committed yet. Set mCompositionState to
+ // eCompositionState_Committing, so that when HandleText() is called (in
+ // response to the "input" event), we know that we should handle the
+ // committed text.
+ mCompositionState = eCompositionState_Committing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleTab()
+{
+ bool cancel;
+ return HandleEnter(false, nullptr, &cancel);
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleKeyNavigation(uint32_t aKey, bool *_retval)
+{
+ // By default, don't cancel the event
+ *_retval = false;
+
+ if (!mInput) {
+ // Stop all searches in case they are async.
+ StopSearch();
+ // Note: if now is after blur and IME end composition,
+ // check mInput before calling.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
+ NS_ERROR("Called before attaching to the control or after detaching from the control");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
+
+ bool disabled;
+ input->GetDisableAutoComplete(&disabled);
+ NS_ENSURE_TRUE(!disabled, NS_OK);
+
+ if (aKey == nsIDOMKeyEvent::DOM_VK_UP ||
+ aKey == nsIDOMKeyEvent::DOM_VK_DOWN ||
+ aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ||
+ aKey == nsIDOMKeyEvent::DOM_VK_PAGE_DOWN)
+ {
+ // Prevent the input from handling up/down events, as it may move
+ // the cursor to home/end on some systems
+ *_retval = true;
+
+ bool isOpen = false;
+ input->GetPopupOpen(&isOpen);
+ if (isOpen) {
+ bool reverse = aKey == nsIDOMKeyEvent::DOM_VK_UP ||
+ aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ? true : false;
+ bool page = aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ||
+ aKey == nsIDOMKeyEvent::DOM_VK_PAGE_DOWN ? true : false;
+
+ // Fill in the value of the textbox with whatever is selected in the popup
+ // if the completeSelectedIndex attribute is set. We check this before
+ // calling SelectBy of an earlier attempt to avoid crashing.
+ bool completeSelection;
+ input->GetCompleteSelectedIndex(&completeSelection);
+
+ // Instruct the result view to scroll by the given amount and direction
+ popup->SelectBy(reverse, page);
+
+ if (completeSelection)
+ {
+ int32_t selectedIndex;
+ popup->GetSelectedIndex(&selectedIndex);
+ if (selectedIndex >= 0) {
+ // A result is selected, so fill in its value
+ nsAutoString value;
+ if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
+ // If the result is the previously autofilled string, then restore
+ // the search string and selection that existed when the result was
+ // autofilled. Else, fill the result and move the caret to the end.
+ int32_t start;
+ if (value.Equals(mPlaceholderCompletionString,
+ nsCaseInsensitiveStringComparator())) {
+ start = mSearchString.Length();
+ value = mPlaceholderCompletionString;
+ SetTextValue(input, value,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
+ } else {
+ start = value.Length();
+ SetTextValue(input, value,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETESELECTED);
+ }
+
+ input->SelectTextRange(start, value.Length());
+ }
+ mCompletedSelectionIndex = selectedIndex;
+ } else {
+ // Nothing is selected, so fill in the last typed value
+ SetTextValue(input, mSearchString,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_REVERT);
+ input->SelectTextRange(mSearchString.Length(), mSearchString.Length());
+ mCompletedSelectionIndex = -1;
+ }
+ }
+ } else {
+#ifdef XP_MACOSX
+ // on Mac, only show the popup if the caret is at the start or end of
+ // the input and there is no selection, so that the default defined key
+ // shortcuts for up and down move to the beginning and end of the field
+ // otherwise.
+ int32_t start, end;
+ if (aKey == nsIDOMKeyEvent::DOM_VK_UP) {
+ input->GetSelectionStart(&start);
+ input->GetSelectionEnd(&end);
+ if (start > 0 || start != end)
+ *_retval = false;
+ }
+ else if (aKey == nsIDOMKeyEvent::DOM_VK_DOWN) {
+ nsAutoString text;
+ input->GetTextValue(text);
+ input->GetSelectionStart(&start);
+ input->GetSelectionEnd(&end);
+ if (start != end || end < (int32_t)text.Length())
+ *_retval = false;
+ }
+#endif
+ if (*_retval) {
+ // Open the popup if there has been a previous search, or else kick off a new search
+ if (!mResults.IsEmpty()) {
+ if (mRowCount) {
+ OpenPopup();
+ }
+ } else {
+ // Stop all searches in case they are async.
+ StopSearch();
+
+ if (!mInput) {
+ // StopSearch() can call PostSearchCleanup() which might result
+ // in a blur event, which could null out mInput, so we need to check it
+ // again. See bug #395344 for more details
+ return NS_OK;
+ }
+
+ // Some script may have changed the value of the text field since our
+ // last keypress or after our focus handler and we don't want to search
+ // for a stale string.
+ nsAutoString value;
+ input->GetTextValue(value);
+ mSearchString = value;
+
+ StartSearches();
+ }
+ }
+ }
+ } else if ( aKey == nsIDOMKeyEvent::DOM_VK_LEFT
+ || aKey == nsIDOMKeyEvent::DOM_VK_RIGHT
+#ifndef XP_MACOSX
+ || aKey == nsIDOMKeyEvent::DOM_VK_HOME
+#endif
+ )
+ {
+ // The user hit a text-navigation key.
+ bool isOpen = false;
+ input->GetPopupOpen(&isOpen);
+
+ // If minresultsforpopup > 1 and there's less matches than the minimum
+ // required, the popup is not open, but the search suggestion is showing
+ // inline, so we should proceed as if we had the popup.
+ uint32_t minResultsForPopup;
+ input->GetMinResultsForPopup(&minResultsForPopup);
+ if (isOpen || (mRowCount > 0 && mRowCount < minResultsForPopup)) {
+ // For completeSelectedIndex autocomplete fields, if the popup shouldn't
+ // close when the caret is moved, don't adjust the text value or caret
+ // position.
+ if (isOpen) {
+ bool noRollup;
+ input->GetNoRollupOnCaretMove(&noRollup);
+ if (noRollup) {
+ bool completeSelection;
+ input->GetCompleteSelectedIndex(&completeSelection);
+ if (completeSelection) {
+ return NS_OK;
+ }
+ }
+ }
+
+ int32_t selectedIndex;
+ popup->GetSelectedIndex(&selectedIndex);
+ bool shouldComplete;
+ input->GetCompleteDefaultIndex(&shouldComplete);
+ if (selectedIndex >= 0) {
+ // The pop-up is open and has a selection, take its value
+ nsAutoString value;
+ if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
+ SetTextValue(input, value,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETESELECTED);
+ input->SelectTextRange(value.Length(), value.Length());
+ }
+ }
+ else if (shouldComplete) {
+ // We usually try to preserve the casing of what user has typed, but
+ // if he wants to autocomplete, we will replace the value with the
+ // actual autocomplete result. Note that the autocomplete input can also
+ // be showing e.g. "bar >> foo bar" if the search matched "bar", a
+ // word not at the start of the full value "foo bar".
+ // The user wants explicitely to use that result, so this ensures
+ // association of the result with the autocompleted text.
+ nsAutoString value;
+ nsAutoString inputValue;
+ input->GetTextValue(inputValue);
+ if (NS_SUCCEEDED(GetDefaultCompleteValue(-1, false, value))) {
+ nsAutoString suggestedValue;
+ int32_t pos = inputValue.Find(" >> ");
+ if (pos > 0) {
+ inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
+ } else {
+ suggestedValue = inputValue;
+ }
+
+ if (value.Equals(suggestedValue, nsCaseInsensitiveStringComparator())) {
+ SetTextValue(input, value,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
+ input->SelectTextRange(value.Length(), value.Length());
+ }
+ }
+ }
+
+ // Close the pop-up even if nothing was selected
+ ClearSearchTimer();
+ ClosePopup();
+ }
+ // Update last-searched string to the current input, since the input may
+ // have changed. Without this, subsequent backspaces look like text
+ // additions, not text deletions.
+ nsAutoString value;
+ input->GetTextValue(value);
+ mSearchString = value;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HandleDelete(bool *_retval)
+{
+ *_retval = false;
+ if (!mInput)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ bool isOpen = false;
+ input->GetPopupOpen(&isOpen);
+ if (!isOpen || mRowCount <= 0) {
+ // Nothing left to delete, proceed as normal
+ bool unused = false;
+ HandleText(&unused);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+
+ int32_t index, searchIndex, rowIndex;
+ popup->GetSelectedIndex(&index);
+ if (index == -1) {
+ // No row is selected in the list
+ bool unused = false;
+ HandleText(&unused);
+ return NS_OK;
+ }
+
+ RowIndexToSearch(index, &searchIndex, &rowIndex);
+ NS_ENSURE_TRUE(searchIndex >= 0 && rowIndex >= 0, NS_ERROR_FAILURE);
+
+ nsIAutoCompleteResult *result = mResults.SafeObjectAt(searchIndex);
+ NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
+
+ nsAutoString search;
+ input->GetSearchParam(search);
+
+ // Clear the row in our result and in the DB.
+ result->RemoveValueAt(rowIndex, true);
+ --mRowCount;
+
+ // We removed it, so make sure we cancel the event that triggered this call.
+ *_retval = true;
+
+ // Unselect the current item.
+ popup->SetSelectedIndex(-1);
+
+ // Tell the tree that the row count changed.
+ if (mTree)
+ mTree->RowCountChanged(mRowCount, -1);
+
+ // Adjust index, if needed.
+ if (index >= (int32_t)mRowCount)
+ index = mRowCount - 1;
+
+ if (mRowCount > 0) {
+ // There are still rows in the popup, select the current index again.
+ popup->SetSelectedIndex(index);
+
+ // Complete to the new current value.
+ bool shouldComplete = false;
+ input->GetCompleteDefaultIndex(&shouldComplete);
+ if (shouldComplete) {
+ nsAutoString value;
+ if (NS_SUCCEEDED(GetResultValueAt(index, false, value))) {
+ CompleteValue(value);
+ }
+ }
+
+ // Invalidate the popup.
+ popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_DELETE);
+ } else {
+ // Nothing left in the popup, clear any pending search timers and
+ // close the popup.
+ ClearSearchTimer();
+ uint32_t minResults;
+ input->GetMinResultsForPopup(&minResults);
+ if (minResults) {
+ ClosePopup();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::GetResultAt(int32_t aIndex, nsIAutoCompleteResult** aResult,
+ int32_t* aRowIndex)
+{
+ int32_t searchIndex;
+ RowIndexToSearch(aIndex, &searchIndex, aRowIndex);
+ NS_ENSURE_TRUE(searchIndex >= 0 && *aRowIndex >= 0, NS_ERROR_FAILURE);
+
+ *aResult = mResults.SafeObjectAt(searchIndex);
+ NS_ENSURE_TRUE(*aResult, NS_ERROR_FAILURE);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetValueAt(int32_t aIndex, nsAString & _retval)
+{
+ GetResultLabelAt(aIndex, _retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetLabelAt(int32_t aIndex, nsAString & _retval)
+{
+ GetResultLabelAt(aIndex, _retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetCommentAt(int32_t aIndex, nsAString & _retval)
+{
+ int32_t rowIndex;
+ nsIAutoCompleteResult* result;
+ nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return result->GetCommentAt(rowIndex, _retval);
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetStyleAt(int32_t aIndex, nsAString & _retval)
+{
+ int32_t rowIndex;
+ nsIAutoCompleteResult* result;
+ nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return result->GetStyleAt(rowIndex, _retval);
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetImageAt(int32_t aIndex, nsAString & _retval)
+{
+ int32_t rowIndex;
+ nsIAutoCompleteResult* result;
+ nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return result->GetImageAt(rowIndex, _retval);
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetFinalCompleteValueAt(int32_t aIndex,
+ nsAString & _retval)
+{
+ int32_t rowIndex;
+ nsIAutoCompleteResult* result;
+ nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return result->GetFinalCompleteValueAt(rowIndex, _retval);
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SetSearchString(const nsAString &aSearchString)
+{
+ mSearchString = aSearchString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
+{
+ aSearchString = mSearchString;
+ return NS_OK;
+}
+
+void
+nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch,
+ nsIAutoCompleteResult *aResult)
+{
+ // Look up the index of the search which is returning.
+ for (uint32_t i = 0; i < mSearches.Length(); ++i) {
+ if (mSearches[i] == aSearch) {
+ ProcessResult(i, aResult);
+ }
+ }
+}
+
+
+////////////////////////////////////////////////////////////////////////
+//// nsIAutoCompleteObserver
+
+NS_IMETHODIMP
+nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
+{
+ MOZ_ASSERT(mSearches.Contains(aSearch));
+
+ ClearResults();
+ HandleSearchResult(aSearch, aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
+{
+ MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));
+
+ // If this is the first search result we are processing
+ // we should clear out the previously cached results.
+ if (mFirstSearchResult) {
+ ClearResults();
+ mFirstSearchResult = false;
+ }
+
+ uint16_t result = 0;
+ if (aResult) {
+ aResult->GetSearchResult(&result);
+ }
+
+ // If our results are incremental, the search is still ongoing.
+ if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
+ result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
+ --mSearchesOngoing;
+ }
+
+ HandleSearchResult(aSearch, aResult);
+
+ if (mSearchesOngoing == 0) {
+ // If this is the last search to return, cleanup.
+ PostSearchCleanup();
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsITimerCallback
+
+NS_IMETHODIMP
+nsAutoCompleteController::Notify(nsITimer *timer)
+{
+ mTimer = nullptr;
+
+ if (mImmediateSearchesCount == 0) {
+ // If there were no immediate searches, BeforeSearches has not yet been
+ // called, so do it now.
+ nsresult rv = BeforeSearches();
+ if (NS_FAILED(rv))
+ return rv;
+ }
+ StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED);
+ AfterSearches();
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+// nsITreeView
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetRowCount(int32_t *aRowCount)
+{
+ *aRowCount = mRowCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetRowProperties(int32_t index, nsAString& aProps)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetCellProperties(int32_t row, nsITreeColumn* col,
+ nsAString& aProps)
+{
+ if (row >= 0) {
+ GetStyleAt(row, aProps);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetColumnProperties(nsITreeColumn* col, nsAString& aProps)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetImageSrc(int32_t row, nsITreeColumn* col, nsAString& _retval)
+{
+ const char16_t* colID;
+ col->GetIdConst(&colID);
+
+ if (NS_LITERAL_STRING("treecolAutoCompleteValue").Equals(colID))
+ return GetImageAt(row, _retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetProgressMode(int32_t row, nsITreeColumn* col, int32_t* _retval)
+{
+ NS_NOTREACHED("tree has no progress cells");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetCellValue(int32_t row, nsITreeColumn* col, nsAString& _retval)
+{
+ NS_NOTREACHED("all of our cells are text");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetCellText(int32_t row, nsITreeColumn* col, nsAString& _retval)
+{
+ const char16_t* colID;
+ col->GetIdConst(&colID);
+
+ if (NS_LITERAL_STRING("treecolAutoCompleteValue").Equals(colID))
+ GetValueAt(row, _retval);
+ else if (NS_LITERAL_STRING("treecolAutoCompleteComment").Equals(colID))
+ GetCommentAt(row, _retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsContainer(int32_t index, bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsContainerOpen(int32_t index, bool *_retval)
+{
+ NS_NOTREACHED("no container cells");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsContainerEmpty(int32_t index, bool *_retval)
+{
+ NS_NOTREACHED("no container cells");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetLevel(int32_t index, int32_t *_retval)
+{
+ *_retval = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetParentIndex(int32_t rowIndex, int32_t *_retval)
+{
+ *_retval = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::HasNextSibling(int32_t rowIndex, int32_t afterIndex, bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::ToggleOpenState(int32_t index)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SetTree(nsITreeBoxObject *tree)
+{
+ mTree = tree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::GetSelection(nsITreeSelection * *aSelection)
+{
+ *aSelection = mSelection;
+ NS_IF_ADDREF(*aSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsAutoCompleteController::SetSelection(nsITreeSelection * aSelection)
+{
+ mSelection = aSelection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SelectionChanged()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SetCellValue(int32_t row, nsITreeColumn* col, const nsAString& value)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::SetCellText(int32_t row, nsITreeColumn* col, const nsAString& value)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::CycleHeader(nsITreeColumn* col)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::CycleCell(int32_t row, nsITreeColumn* col)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsEditable(int32_t row, nsITreeColumn* col, bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsSelectable(int32_t row, nsITreeColumn* col, bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsSeparator(int32_t index, bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::IsSorted(bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::CanDrop(int32_t index, int32_t orientation,
+ nsIDOMDataTransfer* dataTransfer, bool *_retval)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::Drop(int32_t row, int32_t orientation, nsIDOMDataTransfer* dataTransfer)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::PerformAction(const char16_t *action)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::PerformActionOnRow(const char16_t *action, int32_t row)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::PerformActionOnCell(const char16_t* action, int32_t row, nsITreeColumn* col)
+{
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsAutoCompleteController
+
+nsresult
+nsAutoCompleteController::OpenPopup()
+{
+ uint32_t minResults;
+ mInput->GetMinResultsForPopup(&minResults);
+
+ if (mRowCount >= minResults) {
+ return mInput->SetPopupOpen(true);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::ClosePopup()
+{
+ if (!mInput) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ bool isOpen = false;
+ input->GetPopupOpen(&isOpen);
+ if (!isOpen)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
+ popup->SetSelectedIndex(-1);
+ return input->SetPopupOpen(false);
+}
+
+nsresult
+nsAutoCompleteController::BeforeSearches()
+{
+ NS_ENSURE_STATE(mInput);
+
+ mSearchStatus = nsIAutoCompleteController::STATUS_SEARCHING;
+ mDefaultIndexCompleted = false;
+
+ // The first search result will clear mResults array, though we should pass
+ // the previous result to each search to allow them to reuse it. So we
+ // temporarily cache current results till AfterSearches().
+ if (!mResultCache.AppendObjects(mResults)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mSearchesOngoing = mSearches.Length();
+ mSearchesFailed = 0;
+ mFirstSearchResult = true;
+
+ // notify the input that the search is beginning
+ mInput->OnSearchBegin();
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::StartSearch(uint16_t aSearchType)
+{
+ NS_ENSURE_STATE(mInput);
+ nsCOMPtr<nsIAutoCompleteInput> input = mInput;
+
+ // Iterate a copy of |mSearches| so that we don't run into trouble if the
+ // array is mutated while we're still in the loop. An nsIAutoCompleteSearch
+ // implementation could synchronously start a new search when StartSearch()
+ // is called and that would lead to assertions down the way.
+ nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches);
+ for (uint32_t i = 0; i < searchesCopy.Length(); ++i) {
+ nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i];
+
+ // Filter on search type. Not all the searches implement this interface,
+ // in such a case just consider them delayed.
+ uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
+ nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc =
+ do_QueryInterface(search);
+ if (searchDesc)
+ searchDesc->GetSearchType(&searchType);
+ if (searchType != aSearchType)
+ continue;
+
+ nsIAutoCompleteResult *result = mResultCache.SafeObjectAt(i);
+
+ if (result) {
+ uint16_t searchResult;
+ result->GetSearchResult(&searchResult);
+ if (searchResult != nsIAutoCompleteResult::RESULT_SUCCESS &&
+ searchResult != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
+ searchResult != nsIAutoCompleteResult::RESULT_NOMATCH)
+ result = nullptr;
+ }
+
+ nsAutoString searchParam;
+ nsresult rv = input->GetSearchParam(searchParam);
+ if (NS_FAILED(rv))
+ return rv;
+
+ // FormFill expects the searchParam to only contain the input element id,
+ // other consumers may have other expectations, so this modifies it only
+ // for new consumers handling autoFill by themselves.
+ if (mProhibitAutoFill && mClearingAutoFillSearchesAgain) {
+ searchParam.AppendLiteral(" prohibit-autofill");
+ }
+
+ uint32_t userContextId;
+ rv = input->GetUserContextId(&userContextId);
+ if (NS_SUCCEEDED(rv) &&
+ userContextId != nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID) {
+ searchParam.AppendLiteral(" user-context-id:");
+ searchParam.AppendInt(userContextId, 10);
+ }
+
+ rv = search->StartSearch(mSearchString, searchParam, result, static_cast<nsIAutoCompleteObserver *>(this));
+ if (NS_FAILED(rv)) {
+ ++mSearchesFailed;
+ MOZ_ASSERT(mSearchesOngoing > 0);
+ --mSearchesOngoing;
+ }
+ // Because of the joy of nested event loops (which can easily happen when some
+ // code uses a generator for an asynchronous AutoComplete search),
+ // nsIAutoCompleteSearch::StartSearch might cause us to be detached from our input
+ // field. The next time we iterate, we'd be touching something that we shouldn't
+ // be, and result in a crash.
+ if (!mInput) {
+ // The search operation has been finished.
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+nsAutoCompleteController::AfterSearches()
+{
+ mResultCache.Clear();
+ if (mSearchesFailed == mSearches.Length())
+ PostSearchCleanup();
+}
+
+NS_IMETHODIMP
+nsAutoCompleteController::StopSearch()
+{
+ // Stop the timer if there is one
+ ClearSearchTimer();
+
+ // Stop any ongoing asynchronous searches
+ if (mSearchStatus == nsIAutoCompleteController::STATUS_SEARCHING) {
+ for (uint32_t i = 0; i < mSearches.Length(); ++i) {
+ nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i];
+ search->StopSearch();
+ }
+ mSearchesOngoing = 0;
+ // since we were searching, but now we've stopped,
+ // we need to call PostSearchCleanup()
+ PostSearchCleanup();
+ }
+ return NS_OK;
+}
+
+void
+nsAutoCompleteController::MaybeCompletePlaceholder()
+{
+ MOZ_ASSERT(mInput);
+
+ if (!mInput) { // or mInput depending on what you choose
+ MOZ_ASSERT_UNREACHABLE("Input should always be valid at this point");
+ return;
+ }
+
+ int32_t selectionStart;
+ mInput->GetSelectionStart(&selectionStart);
+ int32_t selectionEnd;
+ mInput->GetSelectionEnd(&selectionEnd);
+
+ // Check if the current input should be completed with the placeholder string
+ // from the last completion until the actual search results come back.
+ // The new input string needs to be compatible with the last completed string.
+ // E.g. if the new value is "fob", but the last completion was "foobar",
+ // then the last completion is incompatible.
+ // If the search string is the same as the last completion value, then don't
+ // complete the value again (this prevents completion to happen e.g. if the
+ // cursor is moved and StartSeaches() is invoked).
+ // In addition, the selection must be at the end of the current input to
+ // trigger the placeholder completion.
+ bool usePlaceholderCompletion =
+ !mUserClearedAutoFill &&
+ !mPlaceholderCompletionString.IsEmpty() &&
+ mPlaceholderCompletionString.Length() > mSearchString.Length() &&
+ selectionEnd == selectionStart &&
+ selectionEnd == (int32_t)mSearchString.Length() &&
+ StringBeginsWith(mPlaceholderCompletionString, mSearchString,
+ nsCaseInsensitiveStringComparator());
+
+ if (usePlaceholderCompletion) {
+ CompleteValue(mPlaceholderCompletionString);
+ } else {
+ mPlaceholderCompletionString.Truncate();
+ }
+}
+
+nsresult
+nsAutoCompleteController::StartSearches()
+{
+ // Don't create a new search timer if we're already waiting for one to fire.
+ // If we don't check for this, we won't be able to cancel the original timer
+ // and may crash when it fires (bug 236659).
+ if (mTimer || !mInput)
+ return NS_OK;
+
+ // Check if the current input should be completed with the placeholder string
+ // from the last completion until the actual search results come back.
+ MaybeCompletePlaceholder();
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ // Get the timeout for delayed searches.
+ uint32_t timeout;
+ input->GetTimeout(&timeout);
+
+ uint32_t immediateSearchesCount = mImmediateSearchesCount;
+ if (timeout == 0) {
+ // All the searches should be executed immediately.
+ immediateSearchesCount = mSearches.Length();
+ }
+
+ if (immediateSearchesCount > 0) {
+ nsresult rv = BeforeSearches();
+ if (NS_FAILED(rv))
+ return rv;
+ StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE);
+
+ if (mSearches.Length() == immediateSearchesCount) {
+ // Either all searches are immediate, or the timeout is 0. In the
+ // latter case we still have to execute the delayed searches, otherwise
+ // this will be a no-op.
+ StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED);
+
+ // All the searches have been started, just finish.
+ AfterSearches();
+ return NS_OK;
+ }
+ }
+
+ MOZ_ASSERT(timeout > 0, "Trying to delay searches with a 0 timeout!");
+
+ // Now start the delayed searches.
+ nsresult rv;
+ mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ if (NS_FAILED(rv))
+ return rv;
+ rv = mTimer->InitWithCallback(this, timeout, nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv))
+ mTimer = nullptr;
+
+ return rv;
+}
+
+nsresult
+nsAutoCompleteController::ClearSearchTimer()
+{
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
+ nsIDOMEvent *aEvent)
+{
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
+
+ bool forceComplete;
+ input->GetForceComplete(&forceComplete);
+
+ // Ask the popup if it wants to enter a special value into the textbox
+ nsAutoString value;
+ popup->GetOverrideValue(value);
+ if (value.IsEmpty()) {
+ bool shouldComplete;
+ input->GetCompleteDefaultIndex(&shouldComplete);
+ bool completeSelection;
+ input->GetCompleteSelectedIndex(&completeSelection);
+
+ int32_t selectedIndex;
+ popup->GetSelectedIndex(&selectedIndex);
+ if (selectedIndex >= 0) {
+ nsAutoString inputValue;
+ input->GetTextValue(inputValue);
+ if (aIsPopupSelection || !completeSelection) {
+ // We need to fill-in the value if:
+ // * completeselectedindex is false
+ // * A row in the popup was confirmed
+ //
+ // TODO: This is not totally correct, cause it will also confirm
+ // a result selected with a simple mouseover, that could also have
+ // happened accidentally, maybe touching a touchpad.
+ // The reason is that autocomplete.xml sets selectedIndex on mousemove
+ // making impossible, in the !completeSelection case, to distinguish if
+ // the user wanted to confirm autoFill or the popup entry.
+ // The solution may be to change autocomplete.xml to set selectedIndex
+ // only on popupClick, but that requires changing the selection behavior.
+ GetResultValueAt(selectedIndex, true, value);
+ } else if (mDefaultIndexCompleted &&
+ inputValue.Equals(mPlaceholderCompletionString,
+ nsCaseInsensitiveStringComparator())) {
+ // We also need to fill-in the value if the default index completion was
+ // confirmed, though we cannot use the selectedIndex cause the selection
+ // may have been changed by the mouse in the meanwhile.
+ GetFinalDefaultCompleteValue(value);
+ } else if (mCompletedSelectionIndex != -1) {
+ // If completeselectedindex is true, and EnterMatch was not invoked by
+ // mouse-clicking a match (for example the user pressed Enter),
+ // don't fill in the value as it will have already been filled in as
+ // needed, unless the selected match has a final complete value that
+ // differs from the user-facing value.
+ nsAutoString finalValue;
+ GetResultValueAt(mCompletedSelectionIndex, true, finalValue);
+ if (!inputValue.Equals(finalValue)) {
+ value = finalValue;
+ }
+ // Note that if the user opens the popup, mouses over entries without
+ // ever selecting one with the keyboard, and then hits enter, none of
+ // the above cases will be hit, since mouseover doesn't activate
+ // completeselectedindex and thus mCompletedSelectionIndex would be
+ // -1.
+ }
+ } else if (shouldComplete) {
+ // We usually try to preserve the casing of what user has typed, but
+ // if he wants to autocomplete, we will replace the value with the
+ // actual autocomplete result.
+ // The user wants explicitely to use that result, so this ensures
+ // association of the result with the autocompleted text.
+ nsAutoString defaultIndexValue;
+ if (NS_SUCCEEDED(GetFinalDefaultCompleteValue(defaultIndexValue)))
+ value = defaultIndexValue;
+ }
+
+ if (forceComplete && value.IsEmpty() && shouldComplete) {
+ // See if inputValue is one of the autocomplete results. It can be an
+ // identical value, or if it matched the middle of a result it can be
+ // something like "bar >> foobar" (user entered bar and foobar is
+ // the result value).
+ // If the current search matches one of the autocomplete results, we
+ // should use that result, and not overwrite it with the default value.
+ // It's indeed possible EnterMatch gets called a second time (for example
+ // by the blur handler) and it should not overwrite the current match.
+ nsAutoString inputValue;
+ input->GetTextValue(inputValue);
+ nsAutoString suggestedValue;
+ int32_t pos = inputValue.Find(" >> ");
+ if (pos > 0) {
+ inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
+ } else {
+ suggestedValue = inputValue;
+ }
+
+ for (uint32_t i = 0; i < mResults.Length(); ++i) {
+ nsIAutoCompleteResult *result = mResults[i];
+ if (result) {
+ uint32_t matchCount = 0;
+ result->GetMatchCount(&matchCount);
+ for (uint32_t j = 0; j < matchCount; ++j) {
+ nsAutoString matchValue;
+ result->GetValueAt(j, matchValue);
+ if (suggestedValue.Equals(matchValue, nsCaseInsensitiveStringComparator())) {
+ nsAutoString finalMatchValue;
+ result->GetFinalCompleteValueAt(j, finalMatchValue);
+ value = finalMatchValue;
+ break;
+ }
+ }
+ }
+ }
+ // The value should have been set at this point. If not, then it's not
+ // a value that should be autocompleted.
+ }
+ else if (forceComplete && value.IsEmpty() && completeSelection) {
+ // Since nothing was selected, and forceComplete is specified, that means
+ // we have to find the first default match and enter it instead.
+ for (uint32_t i = 0; i < mResults.Length(); ++i) {
+ nsIAutoCompleteResult *result = mResults[i];
+ if (result) {
+ int32_t defaultIndex;
+ result->GetDefaultIndex(&defaultIndex);
+ if (defaultIndex >= 0) {
+ result->GetFinalCompleteValueAt(defaultIndex, value);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(obsSvc);
+ obsSvc->NotifyObservers(input, "autocomplete-will-enter-text", nullptr);
+
+ if (!value.IsEmpty()) {
+ SetTextValue(input, value, nsIAutoCompleteInput::TEXTVALUE_REASON_ENTERMATCH);
+ input->SelectTextRange(value.Length(), value.Length());
+ mSearchString = value;
+ }
+
+ obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
+ ClosePopup();
+
+ bool cancel;
+ input->OnTextEntered(aEvent, &cancel);
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::RevertTextValue()
+{
+ // StopSearch() can call PostSearchCleanup() which might result
+ // in a blur event, which could null out mInput, so we need to check it
+ // again. See bug #408463 for more details
+ if (!mInput)
+ return NS_OK;
+
+ nsAutoString oldValue(mSearchString);
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ bool cancel = false;
+ input->OnTextReverted(&cancel);
+
+ if (!cancel) {
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(obsSvc);
+ obsSvc->NotifyObservers(input, "autocomplete-will-revert-text", nullptr);
+
+ nsAutoString inputValue;
+ input->GetTextValue(inputValue);
+ // Don't change the value if it is the same to prevent sending useless events.
+ // NOTE: how can |RevertTextValue| be called with inputValue != oldValue?
+ if (!oldValue.Equals(inputValue)) {
+ SetTextValue(input, oldValue, nsIAutoCompleteInput::TEXTVALUE_REASON_REVERT);
+ }
+
+ obsSvc->NotifyObservers(input, "autocomplete-did-revert-text", nullptr);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult)
+{
+ NS_ENSURE_STATE(mInput);
+ MOZ_ASSERT(aResult, "ProcessResult should always receive a result");
+ NS_ENSURE_ARG(aResult);
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ uint16_t searchResult = 0;
+ aResult->GetSearchResult(&searchResult);
+
+ // The following code supports incremental updating results in 2 ways:
+ // * The search may reuse the same result, just by adding entries to it.
+ // * The search may send a new result every time. In this case we merge
+ // the results and proceed on the same code path as before.
+ // This way both mSearches and mResults can be indexed by the search index,
+ // cause we'll always have only one result per search.
+ if (mResults.IndexOf(aResult) == -1) {
+ nsIAutoCompleteResult* oldResult = mResults.SafeObjectAt(aSearchIndex);
+ if (oldResult) {
+ MOZ_ASSERT(false, "Passing new matches to OnSearchResult with a new "
+ "nsIAutoCompleteResult every time is deprecated, please "
+ "update the same result until the search is done");
+ // Build a new nsIAutocompleteSimpleResult and merge results into it.
+ RefPtr<nsAutoCompleteSimpleResult> mergedResult =
+ new nsAutoCompleteSimpleResult();
+ mergedResult->AppendResult(oldResult);
+ mergedResult->AppendResult(aResult);
+ mResults.ReplaceObjectAt(mergedResult, aSearchIndex);
+ } else {
+ // This inserts and grows the array if needed.
+ mResults.ReplaceObjectAt(aResult, aSearchIndex);
+ }
+ }
+ // When found the result should have the same index as the search.
+ MOZ_ASSERT_IF(mResults.IndexOf(aResult) != -1,
+ mResults.IndexOf(aResult) == aSearchIndex);
+ MOZ_ASSERT(mResults.Count() >= aSearchIndex + 1,
+ "aSearchIndex should always be valid for mResults");
+
+ uint32_t oldRowCount = mRowCount;
+ // If the search failed, increase the match count to include the error
+ // description.
+ if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
+ nsAutoString error;
+ aResult->GetErrorDescription(error);
+ if (!error.IsEmpty()) {
+ ++mRowCount;
+ if (mTree) {
+ mTree->RowCountChanged(oldRowCount, 1);
+ }
+ }
+ } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+ searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+ // Increase the match count for all matches in this result.
+ uint32_t totalMatchCount = 0;
+ for (uint32_t i = 0; i < mResults.Length(); i++) {
+ nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
+ if (result) {
+ uint32_t matchCount = 0;
+ result->GetMatchCount(&matchCount);
+ totalMatchCount += matchCount;
+ }
+ }
+ uint32_t delta = totalMatchCount - oldRowCount;
+
+ mRowCount += delta;
+ if (mTree) {
+ mTree->RowCountChanged(oldRowCount, delta);
+ }
+ }
+
+ // Try to autocomplete the default index for this search.
+ // Do this before invalidating so the binding knows about it.
+ CompleteDefaultIndex(aSearchIndex);
+
+ // Refresh the popup view to display the new search results
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
+ popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);
+
+ uint32_t minResults;
+ input->GetMinResultsForPopup(&minResults);
+
+ // Make sure the popup is open, if necessary, since we now have at least one
+ // search result ready to display. Don't force the popup closed if we might
+ // get results in the future to avoid unnecessarily canceling searches.
+ if (mRowCount || !minResults) {
+ OpenPopup();
+ } else if (mSearchesOngoing == 0) {
+ ClosePopup();
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::PostSearchCleanup()
+{
+ NS_ENSURE_STATE(mInput);
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ uint32_t minResults;
+ input->GetMinResultsForPopup(&minResults);
+
+ if (mRowCount || minResults == 0) {
+ OpenPopup();
+ if (mRowCount)
+ mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_MATCH;
+ else
+ mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH;
+ } else {
+ mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH;
+ ClosePopup();
+ }
+
+ // notify the input that the search is complete
+ input->OnSearchComplete();
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::ClearResults()
+{
+ int32_t oldRowCount = mRowCount;
+ mRowCount = 0;
+ mResults.Clear();
+ if (oldRowCount != 0) {
+ if (mTree)
+ mTree->RowCountChanged(0, -oldRowCount);
+ else if (mInput) {
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ mInput->GetPopup(getter_AddRefs(popup));
+ NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
+ // if we had a tree, RowCountChanged() would have cleared the selection
+ // when the selected row was removed. But since we don't have a tree,
+ // we need to clear the selection manually.
+ popup->SetSelectedIndex(-1);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::CompleteDefaultIndex(int32_t aResultIndex)
+{
+ if (mDefaultIndexCompleted || mProhibitAutoFill || mSearchString.Length() == 0 || !mInput)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+
+ int32_t selectionStart;
+ input->GetSelectionStart(&selectionStart);
+ int32_t selectionEnd;
+ input->GetSelectionEnd(&selectionEnd);
+
+ bool isPlaceholderSelected =
+ selectionEnd == (int32_t)mPlaceholderCompletionString.Length() &&
+ selectionStart == (int32_t)mSearchString.Length() &&
+ StringBeginsWith(mPlaceholderCompletionString,
+ mSearchString, nsCaseInsensitiveStringComparator());
+
+ // Don't try to automatically complete to the first result if there's already
+ // a selection or the cursor isn't at the end of the input. In case the
+ // selection is from the current placeholder completion value, then still
+ // automatically complete.
+ if (!isPlaceholderSelected && (selectionEnd != selectionStart ||
+ selectionEnd != (int32_t)mSearchString.Length()))
+ return NS_OK;
+
+ bool shouldComplete;
+ input->GetCompleteDefaultIndex(&shouldComplete);
+ if (!shouldComplete)
+ return NS_OK;
+
+ nsAutoString resultValue;
+ if (NS_SUCCEEDED(GetDefaultCompleteValue(aResultIndex, true, resultValue))) {
+ CompleteValue(resultValue);
+
+ mDefaultIndexCompleted = true;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::GetDefaultCompleteResult(int32_t aResultIndex,
+ nsIAutoCompleteResult** _result,
+ int32_t* _defaultIndex)
+{
+ *_defaultIndex = -1;
+ int32_t resultIndex = aResultIndex;
+
+ // If a result index was not provided, find the first defaultIndex result.
+ for (int32_t i = 0; resultIndex < 0 && i < mResults.Count(); ++i) {
+ nsIAutoCompleteResult *result = mResults.SafeObjectAt(i);
+ if (result &&
+ NS_SUCCEEDED(result->GetDefaultIndex(_defaultIndex)) &&
+ *_defaultIndex >= 0) {
+ resultIndex = i;
+ }
+ }
+ if (resultIndex < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *_result = mResults.SafeObjectAt(resultIndex);
+ NS_ENSURE_TRUE(*_result, NS_ERROR_FAILURE);
+
+ if (*_defaultIndex < 0) {
+ // The search must explicitly provide a default index in order
+ // for us to be able to complete.
+ (*_result)->GetDefaultIndex(_defaultIndex);
+ }
+
+ if (*_defaultIndex < 0) {
+ // We were given a result index, but that result doesn't want to
+ // be autocompleted.
+ return NS_ERROR_FAILURE;
+ }
+
+ // If the result wrongly notifies a RESULT_SUCCESS with no matches, or
+ // provides a defaultIndex greater than its matchCount, avoid trying to
+ // complete to an empty value.
+ uint32_t matchCount = 0;
+ (*_result)->GetMatchCount(&matchCount);
+ // Here defaultIndex is surely non-negative, so can be cast to unsigned.
+ if ((uint32_t)(*_defaultIndex) >= matchCount) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::GetDefaultCompleteValue(int32_t aResultIndex,
+ bool aPreserveCasing,
+ nsAString &_retval)
+{
+ nsIAutoCompleteResult *result;
+ int32_t defaultIndex = -1;
+ nsresult rv = GetDefaultCompleteResult(aResultIndex, &result, &defaultIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoString resultValue;
+ result->GetValueAt(defaultIndex, resultValue);
+ if (aPreserveCasing &&
+ StringBeginsWith(resultValue, mSearchString,
+ nsCaseInsensitiveStringComparator())) {
+ // We try to preserve user casing, otherwise we would end up changing
+ // the case of what he typed, if we have a result with a different casing.
+ // For example if we have result "Test", and user starts writing "tuna",
+ // after digiting t, we would convert it to T trying to autocomplete "Test".
+ // We will still complete to cased "Test" if the user explicitely choose
+ // that result, by either selecting it in the results popup, or with
+ // keyboard navigation or if autocompleting in the middle.
+ nsAutoString casedResultValue;
+ casedResultValue.Assign(mSearchString);
+ // Use what the user has typed so far.
+ casedResultValue.Append(Substring(resultValue,
+ mSearchString.Length(),
+ resultValue.Length()));
+ _retval = casedResultValue;
+ }
+ else
+ _retval = resultValue;
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::GetFinalDefaultCompleteValue(nsAString &_retval)
+{
+ MOZ_ASSERT(mInput, "Must have a valid input");
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ nsIAutoCompleteResult *result;
+ int32_t defaultIndex = -1;
+ nsresult rv = GetDefaultCompleteResult(-1, &result, &defaultIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ result->GetValueAt(defaultIndex, _retval);
+ nsAutoString inputValue;
+ input->GetTextValue(inputValue);
+ if (!_retval.Equals(inputValue, nsCaseInsensitiveStringComparator())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString finalCompleteValue;
+ rv = result->GetFinalCompleteValueAt(defaultIndex, finalCompleteValue);
+ if (NS_SUCCEEDED(rv)) {
+ _retval = finalCompleteValue;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::CompleteValue(nsString &aValue)
+/* mInput contains mSearchString, which we want to autocomplete to aValue. If
+ * selectDifference is true, select the remaining portion of aValue not
+ * contained in mSearchString. */
+{
+ MOZ_ASSERT(mInput, "Must have a valid input");
+
+ nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+ const int32_t mSearchStringLength = mSearchString.Length();
+ int32_t endSelect = aValue.Length(); // By default, select all of aValue.
+
+ if (aValue.IsEmpty() ||
+ StringBeginsWith(aValue, mSearchString,
+ nsCaseInsensitiveStringComparator())) {
+ // aValue is empty (we were asked to clear mInput), or mSearchString
+ // matches the beginning of aValue. In either case we can simply
+ // autocomplete to aValue.
+ mPlaceholderCompletionString = aValue;
+ SetTextValue(input, aValue,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
+ } else {
+ nsresult rv;
+ nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString scheme;
+ if (NS_SUCCEEDED(ios->ExtractScheme(NS_ConvertUTF16toUTF8(aValue), scheme))) {
+ // Trying to autocomplete a URI from somewhere other than the beginning.
+ // Only succeed if the missing portion is "http://"; otherwise do not
+ // autocomplete. This prevents us from "helpfully" autocompleting to a
+ // URI that isn't equivalent to what the user expected.
+ const int32_t findIndex = 7; // length of "http://"
+
+ if ((endSelect < findIndex + mSearchStringLength) ||
+ !scheme.LowerCaseEqualsLiteral("http") ||
+ !Substring(aValue, findIndex, mSearchStringLength).Equals(
+ mSearchString, nsCaseInsensitiveStringComparator())) {
+ return NS_OK;
+ }
+
+ mPlaceholderCompletionString = mSearchString +
+ Substring(aValue, mSearchStringLength + findIndex, endSelect);
+ SetTextValue(input, mPlaceholderCompletionString,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
+
+ endSelect -= findIndex; // We're skipping this many characters of aValue.
+ } else {
+ // Autocompleting something other than a URI from the middle.
+ // Use the format "searchstring >> full string" to indicate to the user
+ // what we are going to replace their search string with.
+ SetTextValue(input, mSearchString + NS_LITERAL_STRING(" >> ") + aValue,
+ nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
+
+ endSelect = mSearchString.Length() + 4 + aValue.Length();
+
+ // Reset the last search completion.
+ mPlaceholderCompletionString.Truncate();
+ }
+ }
+
+ input->SelectTextRange(mSearchStringLength, endSelect);
+
+ return NS_OK;
+}
+
+nsresult
+nsAutoCompleteController::GetResultLabelAt(int32_t aIndex, nsAString & _retval)
+{
+ return GetResultValueLabelAt(aIndex, false, false, _retval);
+}
+
+nsresult
+nsAutoCompleteController::GetResultValueAt(int32_t aIndex, bool aGetFinalValue,
+ nsAString & _retval)
+{
+ return GetResultValueLabelAt(aIndex, aGetFinalValue, true, _retval);
+}
+
+nsresult
+nsAutoCompleteController::GetResultValueLabelAt(int32_t aIndex,
+ bool aGetFinalValue,
+ bool aGetValue,
+ nsAString & _retval)
+{
+ NS_ENSURE_TRUE(aIndex >= 0 && (uint32_t) aIndex < mRowCount, NS_ERROR_ILLEGAL_VALUE);
+
+ int32_t rowIndex;
+ nsIAutoCompleteResult *result;
+ nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint16_t searchResult;
+ result->GetSearchResult(&searchResult);
+
+ if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
+ if (aGetValue)
+ return NS_ERROR_FAILURE;
+ result->GetErrorDescription(_retval);
+ } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+ searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+ if (aGetFinalValue) {
+ // Some implementations may miss finalCompleteValue, try to be backwards
+ // compatible.
+ if (NS_FAILED(result->GetFinalCompleteValueAt(rowIndex, _retval))) {
+ result->GetValueAt(rowIndex, _retval);
+ }
+ } else if (aGetValue) {
+ result->GetValueAt(rowIndex, _retval);
+ } else {
+ result->GetLabelAt(rowIndex, _retval);
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Given the index of a row in the autocomplete popup, find the
+ * corresponding nsIAutoCompleteSearch index, and sub-index into
+ * the search's results list.
+ */
+nsresult
+nsAutoCompleteController::RowIndexToSearch(int32_t aRowIndex, int32_t *aSearchIndex, int32_t *aItemIndex)
+{
+ *aSearchIndex = -1;
+ *aItemIndex = -1;
+
+ uint32_t index = 0;
+
+ // Move index through the results of each registered nsIAutoCompleteSearch
+ // until we find the given row
+ for (uint32_t i = 0; i < mSearches.Length(); ++i) {
+ nsIAutoCompleteResult *result = mResults.SafeObjectAt(i);
+ if (!result)
+ continue;
+
+ uint32_t rowCount = 0;
+
+ uint16_t searchResult;
+ result->GetSearchResult(&searchResult);
+
+ // Find out how many results were provided by the
+ // current nsIAutoCompleteSearch.
+ if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+ searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+ result->GetMatchCount(&rowCount);
+ }
+
+ // If the given row index is within the results range
+ // of the current nsIAutoCompleteSearch then return the
+ // search index and sub-index into the results array
+ if ((rowCount != 0) && (index + rowCount-1 >= (uint32_t) aRowIndex)) {
+ *aSearchIndex = i;
+ *aItemIndex = aRowIndex - index;
+ return NS_OK;
+ }
+
+ // Advance the popup table index cursor past the
+ // results of the current search.
+ index += rowCount;
+ }
+
+ return NS_OK;
+}
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsAutoCompleteController)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsAutoCompleteSimpleResult)
+
+NS_DEFINE_NAMED_CID(NS_AUTOCOMPLETECONTROLLER_CID);
+NS_DEFINE_NAMED_CID(NS_AUTOCOMPLETESIMPLERESULT_CID);
+
+static const mozilla::Module::CIDEntry kAutoCompleteCIDs[] = {
+ { &kNS_AUTOCOMPLETECONTROLLER_CID, false, nullptr, nsAutoCompleteControllerConstructor },
+ { &kNS_AUTOCOMPLETESIMPLERESULT_CID, false, nullptr, nsAutoCompleteSimpleResultConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kAutoCompleteContracts[] = {
+ { NS_AUTOCOMPLETECONTROLLER_CONTRACTID, &kNS_AUTOCOMPLETECONTROLLER_CID },
+ { NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID, &kNS_AUTOCOMPLETESIMPLERESULT_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kAutoCompleteModule = {
+ mozilla::Module::kVersion,
+ kAutoCompleteCIDs,
+ kAutoCompleteContracts
+};
+
+NSMODULE_DEFN(tkAutoCompleteModule) = &kAutoCompleteModule;
diff --git a/toolkit/components/autocomplete/nsAutoCompleteController.h b/toolkit/components/autocomplete/nsAutoCompleteController.h
new file mode 100644
index 0000000000..62aa980f6c
--- /dev/null
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.h
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 __nsAutoCompleteController__
+#define __nsAutoCompleteController__
+
+#include "nsIAutoCompleteController.h"
+
+#include "nsCOMPtr.h"
+#include "nsIAutoCompleteInput.h"
+#include "nsIAutoCompletePopup.h"
+#include "nsIAutoCompleteResult.h"
+#include "nsIAutoCompleteSearch.h"
+#include "nsString.h"
+#include "nsITreeView.h"
+#include "nsITreeSelection.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsCOMArray.h"
+#include "nsCycleCollectionParticipant.h"
+
+class nsAutoCompleteController final : public nsIAutoCompleteController,
+ public nsIAutoCompleteObserver,
+ public nsITimerCallback,
+ public nsITreeView
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsAutoCompleteController,
+ nsIAutoCompleteController)
+ NS_DECL_NSIAUTOCOMPLETECONTROLLER
+ NS_DECL_NSIAUTOCOMPLETEOBSERVER
+ NS_DECL_NSITREEVIEW
+ NS_DECL_NSITIMERCALLBACK
+
+ nsAutoCompleteController();
+
+protected:
+ virtual ~nsAutoCompleteController();
+
+ nsresult OpenPopup();
+ nsresult ClosePopup();
+
+ nsresult StartSearch(uint16_t aSearchType);
+
+ nsresult BeforeSearches();
+ nsresult StartSearches();
+ void AfterSearches();
+ nsresult ClearSearchTimer();
+ void MaybeCompletePlaceholder();
+
+ void HandleSearchResult(nsIAutoCompleteSearch *aSearch,
+ nsIAutoCompleteResult *aResult);
+ nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult);
+ nsresult PostSearchCleanup();
+
+ nsresult EnterMatch(bool aIsPopupSelection,
+ nsIDOMEvent *aEvent);
+ nsresult RevertTextValue();
+
+ nsresult CompleteDefaultIndex(int32_t aResultIndex);
+ nsresult CompleteValue(nsString &aValue);
+
+ nsresult GetResultAt(int32_t aIndex, nsIAutoCompleteResult** aResult,
+ int32_t* aRowIndex);
+ nsresult GetResultValueAt(int32_t aIndex, bool aGetFinalValue,
+ nsAString & _retval);
+ nsresult GetResultLabelAt(int32_t aIndex, nsAString & _retval);
+private:
+ nsresult GetResultValueLabelAt(int32_t aIndex, bool aGetFinalValue,
+ bool aGetValue, nsAString & _retval);
+protected:
+
+ /**
+ * Gets and validates the defaultComplete result and the relative
+ * defaultIndex value.
+ *
+ * @param aResultIndex
+ * Index of the defaultComplete result to be used. Pass -1 to search
+ * for the first result providing a valid defaultIndex.
+ * @param _result
+ * The found result.
+ * @param _defaultIndex
+ * The defaultIndex relative to _result.
+ */
+ nsresult GetDefaultCompleteResult(int32_t aResultIndex,
+ nsIAutoCompleteResult** _result,
+ int32_t* _defaultIndex);
+
+ /**
+ * Gets the defaultComplete value to be suggested to the user.
+ *
+ * @param aResultIndex
+ * Index of the defaultComplete result to be used.
+ * @param aPreserveCasing
+ * Whether user casing should be preserved.
+ * @param _retval
+ * The value to be completed.
+ */
+ nsresult GetDefaultCompleteValue(int32_t aResultIndex, bool aPreserveCasing,
+ nsAString &_retval);
+
+ /**
+ * Gets the defaultComplete value to be used when the user confirms the
+ * current match.
+ * The value is returned only if it case-insensitively matches the current
+ * input text, otherwise the method returns NS_ERROR_FAILURE.
+ * This happens because we don't want to replace text if the user backspaces
+ * just before Enter.
+ *
+ * @param _retval
+ * The value to be completed.
+ */
+ nsresult GetFinalDefaultCompleteValue(nsAString &_retval);
+
+ nsresult ClearResults();
+
+ nsresult RowIndexToSearch(int32_t aRowIndex,
+ int32_t *aSearchIndex, int32_t *aItemIndex);
+
+ // members //////////////////////////////////////////
+
+ nsCOMPtr<nsIAutoCompleteInput> mInput;
+
+ nsCOMArray<nsIAutoCompleteSearch> mSearches;
+ // This is used as a sparse array, always use SafeObjectAt to access it.
+ nsCOMArray<nsIAutoCompleteResult> mResults;
+ // Temporarily keeps the results alive while invoking startSearch() for each
+ // search. This is needed to allow the searches to reuse the previous result,
+ // since otherwise the first search clears mResults.
+ nsCOMArray<nsIAutoCompleteResult> mResultCache;
+
+ nsCOMPtr<nsITimer> mTimer;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ nsCOMPtr<nsITreeBoxObject> mTree;
+
+ nsString mSearchString;
+ nsString mPlaceholderCompletionString;
+ bool mDefaultIndexCompleted;
+ bool mPopupClosedByCompositionStart;
+
+ // Whether autofill is allowed for the next search. May be retrieved by the
+ // search through the "prohibit-autofill" searchParam.
+ bool mProhibitAutoFill;
+
+ // Indicates whether the user cleared the autofilled part, returning to the
+ // originally entered search string.
+ bool mUserClearedAutoFill;
+
+ // Indicates whether clearing the autofilled string should issue a new search.
+ bool mClearingAutoFillSearchesAgain;
+
+ enum CompositionState {
+ eCompositionState_None,
+ eCompositionState_Composing,
+ eCompositionState_Committing
+ };
+ CompositionState mCompositionState;
+ uint16_t mSearchStatus;
+ uint32_t mRowCount;
+ uint32_t mSearchesOngoing;
+ uint32_t mSearchesFailed;
+ bool mFirstSearchResult;
+ uint32_t mImmediateSearchesCount;
+ // The index of the match on the popup that was selected using the keyboard,
+ // if the completeselectedindex attribute is set.
+ // This is used to distinguish that selection (which would have been put in
+ // the input on being selected) from a moused-over selectedIndex value. This
+ // distinction is used to prevent mouse moves from inadvertently changing
+ // what happens once the user hits Enter on the keyboard.
+ // See bug 1043584 for more details.
+ int32_t mCompletedSelectionIndex;
+};
+
+#endif /* __nsAutoCompleteController__ */
diff --git a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp
new file mode 100644
index 0000000000..9fd2c00228
--- /dev/null
+++ b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.cpp
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsAutoCompleteSimpleResult.h"
+
+#define CHECK_MATCH_INDEX(_index, _insert) \
+ if (_index < 0 || \
+ static_cast<MatchesArray::size_type>(_index) > mMatches.Length() || \
+ (!_insert && static_cast<MatchesArray::size_type>(_index) == mMatches.Length())) { \
+ MOZ_ASSERT(false, "Trying to use an invalid index on mMatches"); \
+ return NS_ERROR_ILLEGAL_VALUE; \
+ } \
+
+NS_IMPL_ISUPPORTS(nsAutoCompleteSimpleResult,
+ nsIAutoCompleteResult,
+ nsIAutoCompleteSimpleResult)
+
+struct AutoCompleteSimpleResultMatch
+{
+ AutoCompleteSimpleResultMatch(const nsAString& aValue,
+ const nsAString& aComment,
+ const nsAString& aImage,
+ const nsAString& aStyle,
+ const nsAString& aFinalCompleteValue,
+ const nsAString& aLabel)
+ : mValue(aValue)
+ , mComment(aComment)
+ , mImage(aImage)
+ , mStyle(aStyle)
+ , mFinalCompleteValue(aFinalCompleteValue)
+ , mLabel(aLabel)
+ {
+ }
+
+ nsString mValue;
+ nsString mComment;
+ nsString mImage;
+ nsString mStyle;
+ nsString mFinalCompleteValue;
+ nsString mLabel;
+};
+
+nsAutoCompleteSimpleResult::nsAutoCompleteSimpleResult() :
+ mDefaultIndex(-1),
+ mSearchResult(RESULT_NOMATCH)
+{
+}
+
+nsresult
+nsAutoCompleteSimpleResult::AppendResult(nsIAutoCompleteResult* aResult)
+{
+ nsAutoString searchString;
+ nsresult rv = aResult->GetSearchString(searchString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSearchString = searchString;
+
+ uint16_t searchResult;
+ rv = aResult->GetSearchResult(&searchResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSearchResult = searchResult;
+
+ nsAutoString errorDescription;
+ if (NS_SUCCEEDED(aResult->GetErrorDescription(errorDescription)) &&
+ !errorDescription.IsEmpty()) {
+ mErrorDescription = errorDescription;
+ }
+
+ int32_t defaultIndex = -1;
+ if (NS_SUCCEEDED(aResult->GetDefaultIndex(&defaultIndex)) &&
+ defaultIndex >= 0) {
+ mDefaultIndex = defaultIndex;
+ }
+
+ nsCOMPtr<nsIAutoCompleteSimpleResult> simpleResult =
+ do_QueryInterface(aResult);
+ if (simpleResult) {
+ nsCOMPtr<nsIAutoCompleteSimpleResultListener> listener;
+ if (NS_SUCCEEDED(simpleResult->GetListener(getter_AddRefs(listener))) &&
+ listener) {
+ listener.swap(mListener);
+ }
+ }
+
+ // Copy matches.
+ uint32_t matchCount = 0;
+ rv = aResult->GetMatchCount(&matchCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (size_t i = 0; i < matchCount; ++i) {
+ nsAutoString value, comment, image, style, finalCompleteValue, label;
+
+ rv = aResult->GetValueAt(i, value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aResult->GetCommentAt(i, comment);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aResult->GetImageAt(i, image);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aResult->GetStyleAt(i, style);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aResult->GetFinalCompleteValueAt(i, finalCompleteValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aResult->GetLabelAt(i, label);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AppendMatch(value, comment, image, style, finalCompleteValue, label);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+// searchString
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetSearchString(nsAString &aSearchString)
+{
+ aSearchString = mSearchString;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::SetSearchString(const nsAString &aSearchString)
+{
+ mSearchString.Assign(aSearchString);
+ return NS_OK;
+}
+
+// searchResult
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetSearchResult(uint16_t *aSearchResult)
+{
+ *aSearchResult = mSearchResult;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::SetSearchResult(uint16_t aSearchResult)
+{
+ mSearchResult = aSearchResult;
+ return NS_OK;
+}
+
+// defaultIndex
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetDefaultIndex(int32_t *aDefaultIndex)
+{
+ *aDefaultIndex = mDefaultIndex;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::SetDefaultIndex(int32_t aDefaultIndex)
+{
+ mDefaultIndex = aDefaultIndex;
+ return NS_OK;
+}
+
+// errorDescription
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetErrorDescription(nsAString & aErrorDescription)
+{
+ aErrorDescription = mErrorDescription;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::SetErrorDescription(
+ const nsAString &aErrorDescription)
+{
+ mErrorDescription.Assign(aErrorDescription);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::InsertMatchAt(int32_t aIndex,
+ const nsAString& aValue,
+ const nsAString& aComment,
+ const nsAString& aImage,
+ const nsAString& aStyle,
+ const nsAString& aFinalCompleteValue,
+ const nsAString& aLabel)
+{
+ CHECK_MATCH_INDEX(aIndex, true);
+
+ AutoCompleteSimpleResultMatch match(aValue, aComment, aImage, aStyle, aFinalCompleteValue, aLabel);
+
+ if (!mMatches.InsertElementAt(aIndex, match)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::AppendMatch(const nsAString& aValue,
+ const nsAString& aComment,
+ const nsAString& aImage,
+ const nsAString& aStyle,
+ const nsAString& aFinalCompleteValue,
+ const nsAString& aLabel)
+{
+ return InsertMatchAt(mMatches.Length(), aValue, aComment, aImage, aStyle,
+ aFinalCompleteValue, aLabel);
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetMatchCount(uint32_t *aMatchCount)
+{
+ *aMatchCount = mMatches.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetValueAt(int32_t aIndex, nsAString& _retval)
+{
+ CHECK_MATCH_INDEX(aIndex, false);
+ _retval = mMatches[aIndex].mValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetLabelAt(int32_t aIndex, nsAString& _retval)
+{
+ CHECK_MATCH_INDEX(aIndex, false);
+ _retval = mMatches[aIndex].mLabel;
+ if (_retval.IsEmpty()) {
+ _retval = mMatches[aIndex].mValue;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetCommentAt(int32_t aIndex, nsAString& _retval)
+{
+ CHECK_MATCH_INDEX(aIndex, false);
+ _retval = mMatches[aIndex].mComment;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetImageAt(int32_t aIndex, nsAString& _retval)
+{
+ CHECK_MATCH_INDEX(aIndex, false);
+ _retval = mMatches[aIndex].mImage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetStyleAt(int32_t aIndex, nsAString& _retval)
+{
+ CHECK_MATCH_INDEX(aIndex, false);
+ _retval = mMatches[aIndex].mStyle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetFinalCompleteValueAt(int32_t aIndex,
+ nsAString& _retval)
+{
+ CHECK_MATCH_INDEX(aIndex, false);
+ _retval = mMatches[aIndex].mFinalCompleteValue;
+ if (_retval.IsEmpty()) {
+ _retval = mMatches[aIndex].mValue;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::SetListener(nsIAutoCompleteSimpleResultListener* aListener)
+{
+ mListener = aListener;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::GetListener(nsIAutoCompleteSimpleResultListener** aListener)
+{
+ nsCOMPtr<nsIAutoCompleteSimpleResultListener> listener(mListener);
+ listener.forget(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAutoCompleteSimpleResult::RemoveValueAt(int32_t aRowIndex,
+ bool aRemoveFromDb)
+{
+ CHECK_MATCH_INDEX(aRowIndex, false);
+
+ nsString value = mMatches[aRowIndex].mValue;
+ mMatches.RemoveElementAt(aRowIndex);
+
+ if (mListener) {
+ mListener->OnValueRemoved(this, value, aRemoveFromDb);
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h
new file mode 100644
index 0000000000..28968aa570
--- /dev/null
+++ b/toolkit/components/autocomplete/nsAutoCompleteSimpleResult.h
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 __nsAutoCompleteSimpleResult__
+#define __nsAutoCompleteSimpleResult__
+
+#include "nsIAutoCompleteResult.h"
+#include "nsIAutoCompleteSimpleResult.h"
+
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "mozilla/Attributes.h"
+
+struct AutoCompleteSimpleResultMatch;
+
+class nsAutoCompleteSimpleResult final : public nsIAutoCompleteSimpleResult
+{
+public:
+ nsAutoCompleteSimpleResult();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOCOMPLETERESULT
+ NS_DECL_NSIAUTOCOMPLETESIMPLERESULT
+
+ nsresult AppendResult(nsIAutoCompleteResult* aResult);
+
+private:
+ ~nsAutoCompleteSimpleResult() {}
+
+protected:
+ typedef nsTArray<AutoCompleteSimpleResultMatch> MatchesArray;
+ MatchesArray mMatches;
+
+ nsString mSearchString;
+ nsString mErrorDescription;
+ int32_t mDefaultIndex;
+ uint32_t mSearchResult;
+
+ nsCOMPtr<nsIAutoCompleteSimpleResultListener> mListener;
+};
+
+#endif // __nsAutoCompleteSimpleResult__
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteController.idl b/toolkit/components/autocomplete/nsIAutoCompleteController.idl
new file mode 100644
index 0000000000..0b68032dc4
--- /dev/null
+++ b/toolkit/components/autocomplete/nsIAutoCompleteController.idl
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIAutoCompleteInput;
+interface nsIDOMEvent;
+
+[scriptable, uuid(ff9f8465-204a-47a6-b3c9-0628b3856684)]
+interface nsIAutoCompleteController : nsISupports
+{
+ /*
+ * Possible values for the searchStatus attribute
+ */
+ const unsigned short STATUS_NONE = 1;
+ const unsigned short STATUS_SEARCHING = 2;
+ const unsigned short STATUS_COMPLETE_NO_MATCH = 3;
+ const unsigned short STATUS_COMPLETE_MATCH = 4;
+
+ /*
+ * The input widget that is currently being controlled.
+ */
+ attribute nsIAutoCompleteInput input;
+
+ /*
+ * State which indicates the status of possible ongoing searches
+ */
+ readonly attribute unsigned short searchStatus;
+
+ /*
+ * The number of matches
+ */
+ readonly attribute unsigned long matchCount;
+
+ /*
+ * Start a search on a string, assuming the input property is already set.
+ */
+ void startSearch(in AString searchString);
+
+ /*
+ * Stop all asynchronous searches
+ */
+ void stopSearch();
+
+ /*
+ * Notify the controller that the user has changed text in the textbox.
+ * This includes all means of changing the text value, including typing a
+ * character, backspacing, deleting, pasting, committing composition or
+ * canceling composition.
+ *
+ * NOTE: handleText() must be called after composition actually ends, even if
+ * the composition is canceled and the textbox value isn't changed.
+ * Then, implementation of handleText() can access the editor when
+ * it's not in composing mode. DOM compositionend event is not good
+ * timing for calling handleText(). DOM input event immediately after
+ * DOM compositionend event is the best timing to call this.
+ *
+ * @return whether this handler started a new search.
+ */
+ boolean handleText();
+
+ /*
+ * Notify the controller that the user wishes to enter the current text. If
+ * aIsPopupSelection is true, then a selection was made from the popup, so
+ * fill this value into the input field before continuing. If false, just
+ * use the current value of the input field.
+ *
+ * @param aIsPopupSelection
+ * Pass true if the selection was made from the popup.
+ * @param aEvent
+ * The event that triggered the enter, like a key event if the user
+ * pressed the Return key or a click event if the user clicked a popup
+ * item.
+ * @return Whether the controller wishes to prevent event propagation and
+ * default event.
+ */
+ boolean handleEnter(in boolean aIsPopupSelection,
+ [optional] in nsIDOMEvent aEvent);
+
+ /*
+ * Notify the controller that the user wishes to revert autocomplete
+ *
+ * @return Whether the controller wishes to prevent event propagation and
+ * default event.
+ */
+ boolean handleEscape();
+
+ /*
+ * Notify the controller that the user wishes to start composition
+ *
+ * NOTE: nsIAutoCompleteController implementation expects that this is called
+ * by DOM compositionstart handler.
+ */
+ void handleStartComposition();
+
+ /*
+ * Notify the controller that the user wishes to end composition
+ *
+ * NOTE: nsIAutoCompleteController implementation expects that this is called
+ * by DOM compositionend handler.
+ */
+ void handleEndComposition();
+
+ /*
+ * Handle tab. Just closes up.
+ */
+ void handleTab();
+
+ /*
+ * Notify the controller of the following key navigation events:
+ * up, down, left, right, page up, page down
+ *
+ * @return Whether the controller wishes to prevent event propagation and
+ * default event
+ */
+ boolean handleKeyNavigation(in unsigned long key);
+
+ /*
+ * Notify the controller that the user chose to delete the current
+ * auto-complete result.
+ *
+ * @return Whether the controller removed a result item.
+ */
+ boolean handleDelete();
+
+ /*
+ * Get the value of the result at a given index in the last completed search
+ */
+ AString getValueAt(in long index);
+
+ /*
+ * Get the label of the result at a given index in the last completed search
+ */
+ AString getLabelAt(in long index);
+
+ /*
+ * Get the comment of the result at a given index in the last completed search
+ */
+ AString getCommentAt(in long index);
+
+ /*
+ * Get the style hint for the result at a given index in the last completed search
+ */
+ AString getStyleAt(in long index);
+
+ /*
+ * Get the url of the image of the result at a given index in the last completed search
+ */
+ AString getImageAt(in long index);
+
+ /*
+ * For the last completed search, get the final value that should be completed
+ * when the user confirms the match at the given index
+ */
+ AString getFinalCompleteValueAt(in long index);
+
+ /*
+ * Get / set the current search string. Note, setting will not start searching
+ */
+ attribute AString searchString;
+
+ /*
+ * Set the index of the result item that should be initially selected.
+ * This should be used when a search wants to pre-select an element before
+ * the user starts using results.
+ *
+ * @note Setting this is not the same as just setting selectedIndex in
+ * nsIAutocompletePopup, since this will take care of updating any internal
+ * tracking variables of features like completeSelectedIndex.
+ */
+ void setInitiallySelectedIndex(in long index);
+};
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteInput.idl b/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
new file mode 100644
index 0000000000..26a75ea776
--- /dev/null
+++ b/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+#include "nsIAutoCompleteController.idl"
+
+interface nsIAutoCompletePopup;
+
+[scriptable, uuid(B068E70F-F82C-4C12-AD87-82E271C5C180)]
+interface nsIAutoCompleteInput : nsISupports
+{
+ /*
+ * The result view that will be used to display results
+ */
+ readonly attribute nsIAutoCompletePopup popup;
+
+ /*
+ * The controller.
+ */
+ readonly attribute nsIAutoCompleteController controller;
+
+ /*
+ * Indicates if the popup is currently open
+ */
+ attribute boolean popupOpen;
+
+ /*
+ * Option to disable autocomplete functionality
+ */
+ attribute boolean disableAutoComplete;
+
+ /*
+ * If a search result has its defaultIndex set, this will optionally
+ * try to complete the text in the textbox to the entire text of the
+ * result at the default index as the user types
+ */
+ attribute boolean completeDefaultIndex;
+
+ /*
+ * complete text in the textbox as the user selects from the dropdown
+ * options if set to true
+ */
+ attribute boolean completeSelectedIndex;
+
+ /*
+ * Option for completing to the default result whenever the user hits
+ * enter or the textbox loses focus
+ */
+ attribute boolean forceComplete;
+
+ /*
+ * Option to open the popup only after a certain number of results are available
+ */
+ attribute unsigned long minResultsForPopup;
+
+ /*
+ * The maximum number of rows to show in the autocomplete popup.
+ */
+ attribute unsigned long maxRows;
+
+ /*
+ * Option to show a second column in the popup which contains
+ * the comment for each autocomplete result
+ */
+ attribute boolean showCommentColumn;
+
+ /*
+ * Option to show a third column in the popup which contains
+ * an additional image for each autocomplete result
+ */
+ attribute boolean showImageColumn;
+
+ /*
+ * Number of milliseconds after a keystroke before a search begins
+ */
+ attribute unsigned long timeout;
+
+ /*
+ * An extra parameter to configure searches with.
+ */
+ attribute AString searchParam;
+
+ /*
+ * The number of autocomplete session to search
+ */
+ readonly attribute unsigned long searchCount;
+
+ /*
+ * Get the name of one of the autocomplete search session objects
+ */
+ ACString getSearchAt(in unsigned long index);
+
+ /*
+ * The value of text in the autocomplete textbox.
+ *
+ * @note when setting a new value, the controller always first tries to use
+ * setTextboxValueWithReason, and only if that throws (unimplemented),
+ * fallbacks to the textValue's setter. If a reason is not provided,
+ * the implementation should assume TEXTVALUE_REASON_UNKNOWN, but it
+ * should only happen in testing code.
+ */
+ attribute AString textValue;
+
+ /*
+ * Set the value of text in the autocomplete textbox, providing a reason to
+ * the autocomplete view.
+ */
+ const unsigned short TEXTVALUE_REASON_UNKNOWN = 0;
+ const unsigned short TEXTVALUE_REASON_COMPLETEDEFAULT = 1;
+ const unsigned short TEXTVALUE_REASON_COMPLETESELECTED = 2;
+ const unsigned short TEXTVALUE_REASON_REVERT = 3;
+ const unsigned short TEXTVALUE_REASON_ENTERMATCH = 4;
+
+ void setTextValueWithReason(in AString aValue,
+ in unsigned short aReason);
+
+ /*
+ * Report the starting index of the cursor in the textbox
+ */
+ readonly attribute long selectionStart;
+
+ /*
+ * Report the ending index of the cursor in the textbox
+ */
+ readonly attribute long selectionEnd;
+
+ /*
+ * Select a range of text in the autocomplete textbox
+ */
+ void selectTextRange(in long startIndex, in long endIndex);
+
+ /*
+ * Notification that the search has started
+ */
+ void onSearchBegin();
+
+ /*
+ * Notification that the search concluded successfully
+ */
+ void onSearchComplete();
+
+ /*
+ * Notification that the user selected and entered a result item
+ *
+ * @param aEvent
+ * The event that triggered the enter.
+ * @return True if the user wishes to prevent the enter
+ */
+ boolean onTextEntered([optional] in nsIDOMEvent aEvent);
+
+ /*
+ * Notification that the user cancelled the autocomplete session
+ *
+ * @return True if the user wishes to prevent the revert
+ */
+ boolean onTextReverted();
+
+ /*
+ * This popup should consume or dispatch the rollup event.
+ * TRUE: should consume; FALSE: should dispatch.
+ */
+ readonly attribute boolean consumeRollupEvent;
+
+ /*
+ * Indicates whether this input is in a "private browsing" context.
+ * nsIAutoCompleteSearches for these inputs should not persist any data to disk
+ * (such as a history database).
+ */
+ readonly attribute boolean inPrivateContext;
+
+ /*
+ * Don't rollup the popup when the caret is moved.
+ */
+ readonly attribute boolean noRollupOnCaretMove;
+
+ /**
+ * The userContextId of the current browser.
+ */
+ readonly attribute unsigned long userContextId;
+};
diff --git a/toolkit/components/autocomplete/nsIAutoCompletePopup.idl b/toolkit/components/autocomplete/nsIAutoCompletePopup.idl
new file mode 100644
index 0000000000..cb5dda6f2d
--- /dev/null
+++ b/toolkit/components/autocomplete/nsIAutoCompletePopup.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMElement;
+interface nsIAutoCompleteInput;
+
+[scriptable, uuid(bd3c2662-a988-41ab-8c94-c15ed0e6ac7d)]
+interface nsIAutoCompletePopup : nsISupports
+{
+ /*
+ * The input object that the popup is currently bound to
+ */
+ readonly attribute nsIAutoCompleteInput input;
+
+ /*
+ * An alternative value to be used when text is entered, rather than the
+ * value of the selected item
+ */
+ readonly attribute AString overrideValue;
+
+ /*
+ * The index of the result item that is currently selected
+ */
+ attribute long selectedIndex;
+
+ /*
+ * Indicates if the popup is currently open
+ */
+ readonly attribute boolean popupOpen;
+
+ /*
+ * Bind the popup to an input object and display it with the given coordinates
+ *
+ * @param input - The input object that the popup will be bound to
+ * @param element - The element that the popup will be aligned with
+ */
+ void openAutocompletePopup(in nsIAutoCompleteInput input, in nsIDOMElement element);
+
+ /*
+ * Close the popup and detach from the bound input
+ */
+ void closePopup();
+
+ /*
+ * Instruct the result view to repaint itself to reflect the most current
+ * underlying data
+ *
+ * @param reason - The reason the popup needs to be invalidated, one of the
+ * INVALIDATE_REASON consts.
+ */
+ void invalidate(in unsigned short reason);
+
+ /*
+ * Possible values of invalidate()'s 'reason' argument.
+ */
+ const unsigned short INVALIDATE_REASON_NEW_RESULT = 0;
+ const unsigned short INVALIDATE_REASON_DELETE = 1;
+
+ /*
+ * Change the selection relative to the current selection and make sure
+ * the newly selected row is visible
+ *
+ * @param reverse - Select a row above the current selection
+ * @param page - Select a row that is a full visible page from the current selection
+ * @return The currently selected result item index
+ */
+ void selectBy(in boolean reverse, in boolean page);
+};
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl
new file mode 100644
index 0000000000..c719d94272
--- /dev/null
+++ b/toolkit/components/autocomplete/nsIAutoCompleteResult.idl
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+[scriptable, uuid(9203c031-c4e7-4537-a4ec-81443d623d5a)]
+interface nsIAutoCompleteResult : nsISupports
+{
+ /**
+ * Possible values for the searchResult attribute
+ */
+ const unsigned short RESULT_IGNORED = 1; /* indicates invalid searchString */
+ const unsigned short RESULT_FAILURE = 2; /* indicates failure */
+ const unsigned short RESULT_NOMATCH = 3; /* indicates success with no matches
+ and that the search is complete */
+ const unsigned short RESULT_SUCCESS = 4; /* indicates success with matches
+ and that the search is complete */
+ const unsigned short RESULT_NOMATCH_ONGOING = 5; /* indicates success
+ with no matches
+ and that the search
+ is still ongoing */
+ const unsigned short RESULT_SUCCESS_ONGOING = 6; /* indicates success
+ with matches
+ and that the search
+ is still ongoing */
+ /**
+ * The original search string
+ */
+ readonly attribute AString searchString;
+
+ /**
+ * The result of the search
+ */
+ readonly attribute unsigned short searchResult;
+
+ /**
+ * Index of the default item that should be entered if none is selected
+ */
+ readonly attribute long defaultIndex;
+
+ /**
+ * A string describing the cause of a search failure
+ */
+ readonly attribute AString errorDescription;
+
+ /**
+ * The number of matches
+ */
+ readonly attribute unsigned long matchCount;
+
+ /**
+ * Get the value of the result at the given index
+ */
+ AString getValueAt(in long index);
+
+ /**
+ * This returns the string that is displayed in the dropdown
+ */
+ AString getLabelAt(in long index);
+
+ /**
+ * Get the comment of the result at the given index
+ */
+ AString getCommentAt(in long index);
+
+ /**
+ * Get the style hint for the result at the given index
+ */
+ AString getStyleAt(in long index);
+
+ /**
+ * Get the image of the result at the given index
+ */
+ AString getImageAt(in long index);
+
+ /**
+ * Get the final value that should be completed when the user confirms
+ * the match at the given index.
+ */
+ AString getFinalCompleteValueAt(in long index);
+
+ /**
+ * Remove the value at the given index from the autocomplete results.
+ * If removeFromDb is set to true, the value should be removed from
+ * persistent storage as well.
+ */
+ void removeValueAt(in long rowIndex, in boolean removeFromDb);
+};
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl b/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl
new file mode 100644
index 0000000000..188c333ac6
--- /dev/null
+++ b/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAutoCompleteResult;
+interface nsIAutoCompleteObserver;
+
+[scriptable, uuid(DE8DB85F-C1DE-4d87-94BA-7844890F91FE)]
+interface nsIAutoCompleteSearch : nsISupports
+{
+ /*
+ * Search for a given string and notify a listener (either synchronously
+ * or asynchronously) of the result
+ *
+ * @param searchString - The string to search for
+ * @param searchParam - An extra parameter
+ * @param previousResult - A previous result to use for faster searching
+ * @param listener - A listener to notify when the search is complete
+ */
+ void startSearch(in AString searchString,
+ in AString searchParam,
+ in nsIAutoCompleteResult previousResult,
+ in nsIAutoCompleteObserver listener);
+
+ /*
+ * Stop all searches that are in progress
+ */
+ void stopSearch();
+};
+
+[scriptable, uuid(8bd1dbbc-dcce-4007-9afa-b551eb687b61)]
+interface nsIAutoCompleteObserver : nsISupports
+{
+ /*
+ * Called when a search is complete and the results are ready
+ *
+ * @param search - The search object that processed this search
+ * @param result - The search result object
+ */
+ void onSearchResult(in nsIAutoCompleteSearch search, in nsIAutoCompleteResult result);
+
+ /*
+ * Called to update with new results
+ *
+ * @param search - The search object that processed this search
+ * @param result - The search result object
+ */
+ void onUpdateSearchResult(in nsIAutoCompleteSearch search, in nsIAutoCompleteResult result);
+};
+
+[scriptable, uuid(4c3e7462-fbfb-4310-8f4b-239238392b75)]
+interface nsIAutoCompleteSearchDescriptor : nsISupports
+{
+ // The search is started after the timeout specified by the corresponding
+ // nsIAutoCompleteInput implementation.
+ const unsigned short SEARCH_TYPE_DELAYED = 0;
+ // The search is started synchronously, before any delayed searches.
+ const unsigned short SEARCH_TYPE_IMMEDIATE = 1;
+
+ /**
+ * Identifies the search behavior.
+ * Should be one of the SEARCH_TYPE_* constants above.
+ * Defaults to SEARCH_TYPE_DELAYED.
+ */
+ readonly attribute unsigned short searchType;
+
+ /*
+ * Whether a new search should be triggered when the user deletes the
+ * autofilled part.
+ */
+ readonly attribute boolean clearingAutoFillSearchesAgain;
+};
diff --git a/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl b/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl
new file mode 100644
index 0000000000..5e92e037ae
--- /dev/null
+++ b/toolkit/components/autocomplete/nsIAutoCompleteSimpleResult.idl
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+#include "nsIAutoCompleteResult.idl"
+
+interface nsIAutoCompleteSimpleResultListener;
+
+/**
+ * This class implements nsIAutoCompleteResult and provides simple methods
+ * for setting the value and result items. It can be used whenever some basic
+ * auto complete results are needed that can be pre-generated and filled into
+ * an array.
+ */
+
+[scriptable, uuid(23de9c96-becb-4d0d-a9bb-1d131ce361b5)]
+interface nsIAutoCompleteSimpleResult : nsIAutoCompleteResult
+{
+ /**
+ * A writer for the readonly attribute 'searchString' which should contain
+ * the string that the user typed.
+ */
+ void setSearchString(in AString aSearchString);
+
+ /**
+ * A writer for the readonly attribute 'errorDescription'.
+ */
+ void setErrorDescription(in AString aErrorDescription);
+
+ /**
+ * A writer for the readonly attribute 'defaultIndex' which should contain
+ * the index of the list that will be selected by default (normally 0).
+ */
+ void setDefaultIndex(in long aDefaultIndex);
+
+ /**
+ * A writer for the readonly attribute 'searchResult' which should contain
+ * one of the constants nsIAutoCompleteResult.RESULT_* indicating the success
+ * of the search.
+ */
+ void setSearchResult(in unsigned short aSearchResult);
+
+ /**
+ * Inserts a match consisting of the given value, comment, image, style and
+ * the value to use for defaultIndex completion at a given position.
+ * @param aIndex
+ * The index to insert at
+ * @param aValue
+ * The value to autocomplete to
+ * @param aComment
+ * Comment shown in the autocomplete widget to describe this match
+ * @param aImage
+ * Image shown in the autocomplete widget for this match.
+ * @param aStyle
+ * Describes how to style the match in the autocomplete widget
+ * @param aFinalCompleteValue
+ * Value used when the user confirms selecting this match. If not
+ * provided, aValue will be used.
+ */
+ void insertMatchAt(in long aIndex,
+ in AString aValue,
+ in AString aComment,
+ [optional] in AString aImage,
+ [optional] in AString aStyle,
+ [optional] in AString aFinalCompleteValue,
+ [optional] in AString aLabel);
+
+ /**
+ * Appends a match consisting of the given value, comment, image, style and
+ * the value to use for defaultIndex completion.
+ * @param aValue
+ * The value to autocomplete to
+ * @param aComment
+ * Comment shown in the autocomplete widget to describe this match
+ * @param aImage
+ * Image shown in the autocomplete widget for this match.
+ * @param aStyle
+ * Describes how to style the match in the autocomplete widget
+ * @param aFinalCompleteValue
+ * Value used when the user confirms selecting this match. If not
+ * provided, aValue will be used.
+ */
+ void appendMatch(in AString aValue,
+ in AString aComment,
+ [optional] in AString aImage,
+ [optional] in AString aStyle,
+ [optional] in AString aFinalCompleteValue,
+ [optional] in AString aLabel);
+
+ /**
+ * Gets the listener for changes in the result.
+ */
+ nsIAutoCompleteSimpleResultListener getListener();
+
+ /**
+ * Sets a listener for changes in the result.
+ */
+ void setListener(in nsIAutoCompleteSimpleResultListener aListener);
+};
+
+[scriptable, uuid(004efdc5-1989-4874-8a7a-345bf2fa33af)]
+interface nsIAutoCompleteSimpleResultListener : nsISupports
+{
+ /**
+ * Dispatched after a value is removed from the result.
+ * @param aResult
+ * The result from which aValue has been removed.
+ * @param aValue
+ * The removed value.
+ * @param aRemoveFromDb
+ * Whether the value should be removed from persistent storage as well.
+ */
+ void onValueRemoved(in nsIAutoCompleteSimpleResult aResult, in AString aValue,
+ in boolean aRemoveFromDb);
+};
diff --git a/toolkit/components/autocomplete/tests/unit/.eslintrc.js b/toolkit/components/autocomplete/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/autocomplete/tests/unit/head_autocomplete.js b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js
new file mode 100644
index 0000000000..1443879f09
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/head_autocomplete.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInputBase(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInputBase.prototype = {
+
+ // Array of AutoCompleteSearch names
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ // Text selection range
+ _selStart: 0,
+ _selEnd: 0,
+ get selectionStart() {
+ return this._selStart;
+ },
+ get selectionEnd() {
+ return this._selEnd;
+ },
+ selectTextRange: function(aStart, aEnd) {
+ this._selStart = aStart;
+ this._selEnd = aEnd;
+ },
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ get popup() {
+ if (!this._popup) {
+ this._popup = new AutocompletePopupBase(this);
+ }
+ return this._popup;
+ },
+
+ // nsISupports implementation
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
+}
+
+/**
+ * nsIAutoCompleteResult implementation
+ */
+function AutoCompleteResultBase(aValues) {
+ this._values = aValues;
+}
+AutoCompleteResultBase.prototype = {
+
+ // Arrays
+ _values: null,
+ _comments: [],
+ _styles: [],
+ _finalCompleteValues: [],
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: -1,
+
+ get matchCount() {
+ return this._values.length;
+ },
+
+ getValueAt: function(aIndex) {
+ return this._values[aIndex];
+ },
+
+ getLabelAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function(aIndex) {
+ return this._comments[aIndex];
+ },
+
+ getStyleAt: function(aIndex) {
+ return this._styles[aIndex];
+ },
+
+ getImageAt: function(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt: function(aIndex) {
+ return this._finalCompleteValues[aIndex] || this._values[aIndex];
+ },
+
+ removeValueAt: function (aRowIndex, aRemoveFromDb) {},
+
+ // nsISupports implementation
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult])
+}
+
+/**
+ * nsIAutoCompleteSearch implementation that always returns
+ * the same result set.
+ */
+function AutoCompleteSearchBase(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSearchBase.prototype = {
+
+ // Search name. Used by AutoCompleteController
+ name: null,
+
+ // AutoCompleteResult
+ _result: null,
+
+ startSearch: function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener) {
+ var result = this._result;
+
+ result.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch: function() {},
+
+ // nsISupports implementation
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
+ Ci.nsIAutoCompleteSearch]),
+
+ // nsIFactory implementation
+ createInstance: function(outer, iid) {
+ return this.QueryInterface(iid);
+ }
+}
+
+function AutocompletePopupBase(input) {
+ this.input = input;
+}
+AutocompletePopupBase.prototype = {
+ selectedIndex: 0,
+ invalidate() {},
+ selectBy(reverse, page) {
+ let numRows = this.input.controller.matchCount;
+ if (numRows > 0) {
+ let delta = reverse ? -1 : 1;
+ this.selectedIndex = (this.selectedIndex + delta) % numRows;
+ if (this.selectedIndex < 0) {
+ this.selectedIndex = numRows - 1;
+ }
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]),
+};
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+ var cid = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator).
+ generateUUID();
+
+ var desc = "Test AutoCompleteSearch";
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
diff --git a/toolkit/components/autocomplete/tests/unit/test_330578.js b/toolkit/components/autocomplete/tests/unit/test_330578.js
new file mode 100644
index 0000000000..c422dbb6a0
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_330578.js
@@ -0,0 +1,45 @@
+/* -*- 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 gResultListener = {
+ _lastResult: null,
+ _lastValue: "",
+ _lastRemoveFromDb: false,
+
+ onValueRemoved: function(aResult, aValue, aRemoveFromDb) {
+ this._lastResult = aResult;
+ this._lastValue = aValue;
+ this._lastRemoveFromDb = aRemoveFromDb;
+ }
+};
+
+
+// main
+function run_test() {
+ var result = Cc["@mozilla.org/autocomplete/simple-result;1"].
+ createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.appendMatch("a", "");
+ result.appendMatch("b", "");
+ result.appendMatch("c", "");
+ result.setListener(gResultListener);
+ do_check_eq(result.matchCount, 3);
+ result.removeValueAt(0, true);
+ do_check_eq(result.matchCount, 2);
+ do_check_eq(gResultListener._lastResult, result);
+ do_check_eq(gResultListener._lastValue, "a");
+ do_check_eq(gResultListener._lastRemoveFromDb, true);
+
+ result.removeValueAt(0, false);
+ do_check_eq(result.matchCount, 1);
+ do_check_eq(gResultListener._lastValue, "b");
+ do_check_eq(gResultListener._lastRemoveFromDb, false);
+
+ // check that we don't get notified if the listener is unset
+ result.setListener(null);
+ result.removeValueAt(0, true); // "c"
+ do_check_eq(result.matchCount, 0);
+ do_check_eq(gResultListener._lastValue, "b");
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_378079.js b/toolkit/components/autocomplete/tests/unit/test_378079.js
new file mode 100644
index 0000000000..ad7e5590f2
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_378079.js
@@ -0,0 +1,285 @@
+/* -*- 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/. */
+
+/**
+ * Unit test for Bug 378079 - AutoComplete returns invalid rows when
+ * more than one AutoCompleteSearch is used.
+ */
+
+
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ // Array of AutoCompleteSearch names
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteResult implementation
+ */
+function AutoCompleteResult(aValues, aComments, aStyles) {
+ this._values = aValues;
+ this._comments = aComments;
+ this._styles = aStyles;
+
+ if (this._values.length > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ } else {
+ this.searchResult = Ci.nsIAutoCompleteResult.NOMATCH;
+ }
+}
+AutoCompleteResult.prototype = {
+ constructor: AutoCompleteResult,
+
+ // Arrays
+ _values: null,
+ _comments: null,
+ _styles: null,
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: 0,
+
+ get matchCount() {
+ return this._values.length;
+ },
+
+ getValueAt: function(aIndex) {
+ return this._values[aIndex];
+ },
+
+ getLabelAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function(aIndex) {
+ return this._comments[aIndex];
+ },
+
+ getStyleAt: function(aIndex) {
+ return this._styles[aIndex];
+ },
+
+ getImageAt: function(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function (aRowIndex, aRemoveFromDb) {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteResult))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteSearch implementation that always returns
+ * the same result set.
+ */
+function AutoCompleteSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+
+ // Search name. Used by AutoCompleteController
+ name: null,
+
+ // AutoCompleteResult
+ _result:null,
+
+
+ /**
+ * Return the same result set for every search
+ */
+ startSearch: function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener)
+ {
+ aListener.onSearchResult(this, this._result);
+ },
+
+ stopSearch: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsIAutoCompleteSearch))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ // nsIFactory implementation
+ createInstance: function(outer, iid) {
+ return this.QueryInterface(iid);
+ }
+}
+
+
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+
+ var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ var cid = uuidGenerator.generateUUID();
+
+ var desc = "Test AutoCompleteSearch";
+
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+
+
+/**
+ * Test AutoComplete with multiple AutoCompleteSearch sources.
+ */
+function run_test() {
+
+ // Make an AutoCompleteSearch that always returns nothing
+ var emptySearch = new AutoCompleteSearch("test-empty-search",
+ new AutoCompleteResult([], [], []));
+
+ // Make an AutoCompleteSearch that returns two values
+ var expectedValues = ["test1", "test2"];
+ var regularSearch = new AutoCompleteSearch("test-regular-search",
+ new AutoCompleteResult(expectedValues, [], []));
+
+ // Register searches so AutoCompleteController can find them
+ registerAutoCompleteSearch(emptySearch);
+ registerAutoCompleteSearch(regularSearch);
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput([emptySearch.name, regularSearch.name]);
+ var numSearchesStarted = 0;
+
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+
+ do_check_eq(numSearchesStarted, 1);
+
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, 2);
+
+ // Confirm expected result values
+ for (var i = 0; i < expectedValues.length; i++) {
+ do_check_eq(expectedValues[i], controller.getValueAt(i));
+ }
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(emptySearch);
+ unregisterAutoCompleteSearch(regularSearch);
+
+ do_test_finished();
+ };
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ controller.startSearch("test");
+}
+
diff --git a/toolkit/components/autocomplete/tests/unit/test_393191.js b/toolkit/components/autocomplete/tests/unit/test_393191.js
new file mode 100644
index 0000000000..6fb57e6c4a
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_393191.js
@@ -0,0 +1,272 @@
+/* -*- 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/. */
+
+/**
+ * Unit test for Bug 393191 - AutoComplete crashes if result is null
+ */
+
+
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ // Array of AutoCompleteSearch names
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteResult implementation
+ */
+function AutoCompleteResult(aValues, aComments, aStyles) {
+ this._values = aValues;
+ this._comments = aComments;
+ this._styles = aStyles;
+
+ if (this._values.length > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ } else {
+ this.searchResult = Ci.nsIAutoCompleteResult.NOMATCH;
+ }
+}
+AutoCompleteResult.prototype = {
+ constructor: AutoCompleteResult,
+
+ // Arrays
+ _values: null,
+ _comments: null,
+ _styles: null,
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: 0,
+
+ get matchCount() {
+ return this._values.length;
+ },
+
+ getValueAt: function(aIndex) {
+ return this._values[aIndex];
+ },
+
+ getLabelAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function(aIndex) {
+ return this._comments[aIndex];
+ },
+
+ getStyleAt: function(aIndex) {
+ return this._styles[aIndex];
+ },
+
+ getImageAt: function(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function (aRowIndex, aRemoveFromDb) {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteResult))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteSearch implementation that always returns
+ * the same result set.
+ */
+function AutoCompleteSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+
+ // Search name. Used by AutoCompleteController
+ name: null,
+
+ // AutoCompleteResult
+ _result: null,
+
+
+ /**
+ * Return the same result set for every search
+ */
+ startSearch: function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener)
+ {
+ aListener.onSearchResult(this, this._result);
+ },
+
+ stopSearch: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsIAutoCompleteSearch))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ // nsIFactory implementation
+ createInstance: function(outer, iid) {
+ return this.QueryInterface(iid);
+ }
+}
+
+
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+
+ var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ var cid = uuidGenerator.generateUUID();
+
+ var desc = "Test AutoCompleteSearch";
+
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+
+
+/**
+ * Test AutoComplete with a search that returns a null result
+ */
+function run_test() {
+
+ // Make an AutoCompleteSearch that always returns nothing
+ var emptySearch = new AutoCompleteSearch("test-empty-search",
+ new AutoCompleteResult([], [], []));
+
+ // Register search so AutoCompleteController can find them
+ registerAutoCompleteSearch(emptySearch);
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our search
+ // and confirms results on search complete
+ var input = new AutoCompleteInput([emptySearch.name]);
+ var numSearchesStarted = 0;
+
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+
+ do_check_eq(numSearchesStarted, 1);
+
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+ do_check_eq(controller.matchCount, 0);
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(emptySearch);
+
+ do_test_finished();
+ };
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ controller.startSearch("test");
+}
+
diff --git a/toolkit/components/autocomplete/tests/unit/test_440866.js b/toolkit/components/autocomplete/tests/unit/test_440866.js
new file mode 100644
index 0000000000..e450aebbf8
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_440866.js
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Unit test for Bug 440866 - First AutoCompleteSearch that returns
+ * RESULT_NOMATCH cancels all other searches when popup is open
+ */
+
+
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ // Array of AutoCompleteSearch names
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteResult implementation
+ */
+function AutoCompleteResult(aValues, aComments, aStyles) {
+ this._values = aValues;
+ this._comments = aComments;
+ this._styles = aStyles;
+
+ if (this._values.length > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ } else {
+ this.searchResult = Ci.nsIAutoCompleteResult.NOMATCH;
+ }
+}
+AutoCompleteResult.prototype = {
+ constructor: AutoCompleteResult,
+
+ // Arrays
+ _values: null,
+ _comments: null,
+ _styles: null,
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: 0,
+
+ get matchCount() {
+ return this._values.length;
+ },
+
+ getValueAt: function(aIndex) {
+ return this._values[aIndex];
+ },
+
+ getLabelAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function(aIndex) {
+ return this._comments[aIndex];
+ },
+
+ getStyleAt: function(aIndex) {
+ return this._styles[aIndex];
+ },
+
+ getImageAt: function(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function (aRowIndex, aRemoveFromDb) {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteResult))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteSearch implementation that always returns
+ * the same result set.
+ */
+function AutoCompleteSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+
+ // Search name. Used by AutoCompleteController
+ name: null,
+
+ // AutoCompleteResult
+ _result:null,
+
+
+ /**
+ * Return the same result set for every search
+ */
+ startSearch: function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener)
+ {
+ aListener.onSearchResult(this, this._result);
+ },
+
+ stopSearch: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsIAutoCompleteSearch))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ // nsIFactory implementation
+ createInstance: function(outer, iid) {
+ return this.QueryInterface(iid);
+ }
+}
+
+
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+
+ var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ var cid = uuidGenerator.generateUUID();
+
+ var desc = "Test AutoCompleteSearch";
+
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+
+
+/**
+ * Test AutoComplete with multiple AutoCompleteSearch sources.
+ */
+function run_test() {
+
+ // Make an AutoCompleteSearch that always returns nothing
+ var emptySearch = new AutoCompleteSearch("test-empty-search",
+ new AutoCompleteResult([], [], []));
+
+ // Make an AutoCompleteSearch that returns two values
+ var expectedValues = ["test1", "test2"];
+ var regularSearch = new AutoCompleteSearch("test-regular-search",
+ new AutoCompleteResult(expectedValues, [], []));
+
+ // Register searches so AutoCompleteController can find them
+ registerAutoCompleteSearch(emptySearch);
+ registerAutoCompleteSearch(regularSearch);
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput([emptySearch.name, regularSearch.name]);
+ var numSearchesStarted = 0;
+
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(input.searchCount, 2);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, 2);
+
+ // Confirm expected result values
+ for (var i = 0; i < expectedValues.length; i++) {
+ do_check_eq(expectedValues[i], controller.getValueAt(i));
+ }
+
+ do_check_true(input.popupOpen);
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(emptySearch);
+ unregisterAutoCompleteSearch(regularSearch);
+
+ do_test_finished();
+ };
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ controller.startSearch("test");
+}
+
diff --git a/toolkit/components/autocomplete/tests/unit/test_463023.js b/toolkit/components/autocomplete/tests/unit/test_463023.js
new file mode 100644
index 0000000000..a2639fd032
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_463023.js
@@ -0,0 +1,12 @@
+/* -*- 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/. */
+
+// main
+function run_test() {
+ var result = Cc["@mozilla.org/autocomplete/controller;1"].
+ createInstance(Ci.nsIAutoCompleteController);
+ do_check_eq(result.searchStatus, Ci.nsIAutoCompleteController.STATUS_NONE);
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_660156.js b/toolkit/components/autocomplete/tests/unit/test_660156.js
new file mode 100644
index 0000000000..98acb243e0
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_660156.js
@@ -0,0 +1,101 @@
+/**
+ * Search object that returns results at different times.
+ * First, the search that returns results asynchronously.
+ */
+function AutoCompleteAsyncSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteAsyncSearch.prototype = Object.create(AutoCompleteSearchBase.prototype);
+AutoCompleteAsyncSearch.prototype.startSearch = function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener) {
+ this._result.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING;
+ aListener.onSearchResult(this, this._result);
+
+ do_timeout(500, () => {
+ this._returnResults(aListener);
+ });
+};
+
+AutoCompleteAsyncSearch.prototype._returnResults = function(aListener) {
+ var result = this._result;
+
+ result.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ aListener.onSearchResult(this, result);
+};
+
+/**
+ * The synchronous version
+ */
+function AutoCompleteSyncSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSyncSearch.prototype = Object.create(AutoCompleteAsyncSearch.prototype);
+AutoCompleteSyncSearch.prototype.startSearch = function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener) {
+ this._returnResults(aListener);
+};
+
+/**
+ * Results object
+ */
+function AutoCompleteResult(aValues, aDefaultIndex) {
+ this._values = aValues;
+ this.defaultIndex = aDefaultIndex;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+
+/**
+ * Test AutoComplete with multiple AutoCompleteSearch sources, with one of them
+ * (index != 0) returning before the rest.
+ */
+function run_test() {
+ do_test_pending();
+
+ var results = ["mozillaTest"];
+ var inputStr = "moz";
+
+ // Async search
+ var asyncSearch = new AutoCompleteAsyncSearch("Async",
+ new AutoCompleteResult(results, -1));
+ // Sync search
+ var syncSearch = new AutoCompleteSyncSearch("Sync",
+ new AutoCompleteResult(results, 0));
+
+ // Register searches so AutoCompleteController can find them
+ registerAutoCompleteSearch(asyncSearch);
+ registerAutoCompleteSearch(syncSearch);
+
+ var controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete.
+ // Async search MUST be FIRST to trigger the bug this tests.
+ var input = new AutoCompleteInputBase([asyncSearch.name, syncSearch.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = inputStr;
+
+ // Caret must be at the end. Autofill doesn't happen unless you're typing
+ // characters at the end.
+ var strLen = inputStr.length;
+ input.selectTextRange(strLen, strLen);
+
+ controller.input = input;
+ controller.startSearch(inputStr);
+
+ input.onSearchComplete = function() {
+ do_check_eq(input.textValue, results[0]);
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(asyncSearch);
+ unregisterAutoCompleteSearch(syncSearch);
+ do_test_finished();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js b/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js
new file mode 100644
index 0000000000..7fee48d555
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_autocomplete_multiple.js
@@ -0,0 +1,276 @@
+/* -*- 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/. */
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ // Array of AutoCompleteSearch names
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteResult implementation
+ */
+function AutoCompleteResult(aValues, aComments, aStyles) {
+ this._values = aValues;
+ this._comments = aComments;
+ this._styles = aStyles;
+}
+AutoCompleteResult.prototype = {
+ constructor: AutoCompleteResult,
+
+ // Arrays
+ _values: null,
+ _comments: null,
+ _styles: null,
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: 0,
+
+ get matchCount() {
+ return this._values.length;
+ },
+
+ getValueAt: function(aIndex) {
+ return this._values[aIndex];
+ },
+
+ getLabelAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function(aIndex) {
+ return this._comments[aIndex];
+ },
+
+ getStyleAt: function(aIndex) {
+ return this._styles[aIndex];
+ },
+
+ getImageAt: function(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function (aRowIndex, aRemoveFromDb) {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteResult))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteSearch implementation that always returns
+ * the same result set.
+ */
+function AutoCompleteSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+
+ // Search name. Used by AutoCompleteController
+ name: null,
+
+ // AutoCompleteResult
+ _result:null,
+
+
+ /**
+ * Return the same result set for every search
+ */
+ startSearch: function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener)
+ {
+ var result = this._result;
+ if (result._values.length > 0) {
+ result.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING;
+ } else {
+ result.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING;
+ }
+ aListener.onSearchResult(this, result);
+
+ if (result._values.length > 0) {
+ result.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ } else {
+ result.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
+ }
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsIAutoCompleteSearch))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ // nsIFactory implementation
+ createInstance: function(outer, iid) {
+ return this.QueryInterface(iid);
+ }
+}
+
+
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+
+ var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ var cid = uuidGenerator.generateUUID();
+
+ var desc = "Test AutoCompleteSearch";
+
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+
+
+/**
+ * Test AutoComplete with multiple AutoCompleteSearch sources.
+ */
+function run_test() {
+ var expected1 = ["1", "2", "3"];
+ var expected2 = ["a", "b", "c"];
+ var search1 = new AutoCompleteSearch("search1",
+ new AutoCompleteResult(expected1, [], []));
+ var search2 = new AutoCompleteSearch("search2",
+ new AutoCompleteResult(expected2, [], []));
+
+ // Register searches so AutoCompleteController can find them
+ registerAutoCompleteSearch(search1);
+ registerAutoCompleteSearch(search2);
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput([search1.name, search2.name]);
+ var numSearchesStarted = 0;
+
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+
+ do_check_eq(numSearchesStarted, 1);
+
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, expected1.length + expected2.length);
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(search1);
+ unregisterAutoCompleteSearch(search2);
+
+ do_test_finished();
+ };
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ controller.startSearch("test");
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js b/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js
new file mode 100644
index 0000000000..c98db7f8fd
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_autocomplete_userContextId.js
@@ -0,0 +1,45 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Promise.jsm");
+
+function AutoCompleteInput(aSearches, aUserContextId) {
+ this.searches = aSearches;
+ this.userContextId = aUserContextId;
+ this.popup.selectedIndex = -1;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+function AutoCompleteSearch(aName) {
+ this.name = aName;
+}
+AutoCompleteSearch.prototype = Object.create(AutoCompleteSearchBase.prototype);
+
+add_task(function *test_userContextId() {
+ let searchParam = yield doSearch("test", 1);
+ Assert.equal(searchParam, " user-context-id:1");
+});
+
+function doSearch(aString, aUserContextId) {
+ let deferred = Promise.defer();
+ let search = new AutoCompleteSearch("test");
+
+ search.startSearch = function (aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener) {
+ unregisterAutoCompleteSearch(search);
+ deferred.resolve(aSearchParam);
+ };
+
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ let input = new AutoCompleteInput([ search.name ], aUserContextId);
+ controller.input = input;
+ controller.startSearch(aString);
+
+ return deferred.promise;
+ }
+
diff --git a/toolkit/components/autocomplete/tests/unit/test_autofillSelectedPopupIndex.js b/toolkit/components/autocomplete/tests/unit/test_autofillSelectedPopupIndex.js
new file mode 100644
index 0000000000..5fb93abc1b
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_autofillSelectedPopupIndex.js
@@ -0,0 +1,78 @@
+"use strict";
+
+add_task(function* sameCaseAsMatch() {
+ yield runTest("moz");
+});
+
+add_task(function* differentCaseFromMatch() {
+ yield runTest("MOZ");
+});
+
+function* runTest(searchStr) {
+ let matches = [
+ "mozilla.org",
+ "example.com",
+ ];
+ let result = new AutoCompleteResultBase(matches);
+ result.defaultIndex = 0;
+
+ let search = new AutoCompleteSearchBase("search", result);
+ registerAutoCompleteSearch(search);
+
+ let input = new AutoCompleteInputBase([search.name]);
+ input.completeSelectedIndex = true;
+ input.completeDefaultIndex = true;
+
+ // Start off with the search string in the input. The selection must be
+ // collapsed and the caret must be at the end to trigger autofill below.
+ input.textValue = searchStr;
+ input.selectTextRange(searchStr.length, searchStr.length);
+ Assert.equal(input.selectionStart, searchStr.length,
+ "Selection should start at the end of the input");
+ Assert.equal(input.selectionEnd, searchStr.length,
+ "Selection should end at the end of the input");
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ createInstance(Ci.nsIAutoCompleteController);
+ controller.input = input;
+ input.controller = controller;
+
+ // Start a search.
+ yield new Promise(resolve => {
+ controller.startSearch(searchStr);
+ input.onSearchComplete = () => {
+ // The first match should have autofilled, but the case of the search
+ // string should be preserved.
+ let expectedValue = searchStr + matches[0].substr(searchStr.length);
+ Assert.equal(input.textValue, expectedValue,
+ "Should have autofilled");
+ Assert.equal(input.selectionStart, searchStr.length,
+ "Selection should start after search string");
+ Assert.equal(input.selectionEnd, expectedValue.length,
+ "Selection should end at the end of the input");
+ resolve();
+ };
+ });
+
+ // Key down to select the second match in the popup.
+ controller.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_DOWN);
+ let expectedValue = matches[1];
+ Assert.equal(input.textValue, expectedValue,
+ "Should have filled second match");
+ Assert.equal(input.selectionStart, expectedValue.length,
+ "Selection should start at the end of the input");
+ Assert.equal(input.selectionEnd, expectedValue.length,
+ "Selection should end at the end of the input");
+
+ // Key up to select the first match again. The input should be restored
+ // exactly as it was when the first match was autofilled above: the search
+ // string's case should be preserved, and the selection should be preserved.
+ controller.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_UP);
+ expectedValue = searchStr + matches[0].substr(searchStr.length);
+ Assert.equal(input.textValue, expectedValue,
+ "Should have filled first match again");
+ Assert.equal(input.selectionStart, searchStr.length,
+ "Selection should start after search string again");
+ Assert.equal(input.selectionEnd, expectedValue.length,
+ "Selection should end at the end of the input again");
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_badDefaultIndex.js b/toolkit/components/autocomplete/tests/unit/test_badDefaultIndex.js
new file mode 100644
index 0000000000..17f735388e
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_badDefaultIndex.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A results that wants to defaultComplete to 0, but it has no matches,
+ * though it notifies SUCCESS to the controller.
+ */
+function AutoCompleteNoMatchResult() {
+ this.defaultIndex = 0;
+}
+AutoCompleteNoMatchResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+/**
+ * A results that wants to defaultComplete to an index greater than the number
+ * of matches.
+ */
+function AutoCompleteBadIndexResult(aValues, aDefaultIndex) {
+ do_check_true(aValues.length <= aDefaultIndex);
+ this._values = aValues;
+ this.defaultIndex = aDefaultIndex;
+}
+AutoCompleteBadIndexResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+add_test(function autocomplete_noMatch_success() {
+ const INPUT_STR = "moz";
+
+ let searchNoMatch =
+ new AutoCompleteSearchBase("searchNoMatch",
+ new AutoCompleteNoMatchResult());
+ registerAutoCompleteSearch(searchNoMatch);
+
+ // Make an AutoCompleteInput that uses our search and confirms results.
+ let input = new AutoCompleteInputBase([searchNoMatch.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = INPUT_STR;
+
+ // Caret must be at the end for autoFill to happen.
+ let strLen = INPUT_STR.length;
+ input.selectTextRange(strLen, strLen);
+ do_check_eq(input.selectionStart, strLen);
+ do_check_eq(input.selectionEnd, strLen);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ controller.input = input;
+ controller.startSearch(INPUT_STR);
+
+ input.onSearchComplete = function () {
+ // Should not try to autoFill to an empty value.
+ do_check_eq(input.textValue, "moz");
+
+ // Clean up.
+ unregisterAutoCompleteSearch(searchNoMatch);
+ run_next_test();
+ };
+});
+
+add_test(function autocomplete_defaultIndex_exceeds_matchCount() {
+ const INPUT_STR = "moz";
+
+ // Result returning matches, but a bad defaultIndex.
+ let searchBadIndex =
+ new AutoCompleteSearchBase("searchBadIndex",
+ new AutoCompleteBadIndexResult(["mozillaTest"], 1));
+ registerAutoCompleteSearch(searchBadIndex);
+
+ // Make an AutoCompleteInput that uses our search and confirms results.
+ let input = new AutoCompleteInputBase([searchBadIndex.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = INPUT_STR;
+
+ // Caret must be at the end for autoFill to happen.
+ let strLen = INPUT_STR.length;
+ input.selectTextRange(strLen, strLen);
+ do_check_eq(input.selectionStart, strLen);
+ do_check_eq(input.selectionEnd, strLen);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ controller.input = input;
+ controller.startSearch(INPUT_STR);
+
+ input.onSearchComplete = function () {
+ // Should not try to autoFill to an empty value.
+ do_check_eq(input.textValue, "moz");
+
+ // Clean up.
+ unregisterAutoCompleteSearch(searchBadIndex);
+ run_next_test();
+ };
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js b/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js
new file mode 100644
index 0000000000..c25b009071
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.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/. */
+
+function AutoCompleteResult(aValues) {
+ this._values = aValues;
+ this.defaultIndex = 0;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popup.selectedIndex = -1;
+ this.completeDefaultIndex = true;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_keyNavigation() {
+ doSearch("MOZ", "mozilla", function(aController) {
+ do_check_eq(aController.input.textValue, "MOZilla");
+ aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
+ do_check_eq(aController.input.textValue, "mozilla");
+ });
+});
+
+add_test(function test_handleEnter() {
+ doSearch("MOZ", "mozilla", function(aController) {
+ do_check_eq(aController.input.textValue, "MOZilla");
+ aController.handleEnter(false);
+ do_check_eq(aController.input.textValue, "mozilla");
+ });
+});
+
+function doSearch(aSearchString, aResultValue, aOnCompleteCallback) {
+ let search = new AutoCompleteSearchBase("search",
+ new AutoCompleteResult([ "mozilla", "toolkit" ], 0));
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput([ search.name ]);
+ input.textValue = aSearchString;
+
+ // Caret must be at the end for autofill to happen.
+ let strLen = aSearchString.length;
+ input.selectTextRange(strLen, strLen);
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ // Clean up.
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
new file mode 100644
index 0000000000..fcac8ae43f
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
@@ -0,0 +1,48 @@
+function AutoCompleteResult(aValues, aFinalCompleteValues) {
+ this._values = aValues;
+ this._finalCompleteValues = aFinalCompleteValues;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popup.selectedIndex = 0;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+add_test(function test_handleEnter_mouse() {
+ doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
+ do_check_eq(aController.input.textValue, "moz");
+ do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ // Keyboard interaction is tested by test_finalCompleteValueSelectedIndex.js
+ // so here just test popup selection.
+ aController.handleEnter(true);
+ do_check_eq(aController.input.textValue, "http://www.mozilla.com");
+ });
+});
+
+function doSearch(aSearchString, aResultValue, aFinalCompleteValue, aOnCompleteCallback) {
+ let search = new AutoCompleteSearchBase(
+ "search",
+ new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
+ );
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput([ search.name ]);
+ input.textValue = aSearchString;
+
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ // Clean up.
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
new file mode 100644
index 0000000000..6556a26dc5
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
@@ -0,0 +1,119 @@
+function AutoCompleteResult(aResultValues) {
+ this._values = aResultValues.map(x => x[0]);
+ this._finalCompleteValues = aResultValues.map(x => x[1]);
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+var selectByWasCalled = false;
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popup.selectedIndex = 0;
+ this.popup.selectBy = function(reverse, page) {
+ Assert.equal(selectByWasCalled, false);
+ selectByWasCalled = true;
+ Assert.equal(reverse, false);
+ Assert.equal(page, false);
+ this.selectedIndex += (reverse ? -1 : 1) * (page ? 100 : 1);
+ };
+ this.completeSelectedIndex = true;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+add_test(function test_handleEnter_key() {
+ let results = [
+ ["mozilla.com", "http://www.mozilla.com"],
+ ["mozilla.org", "http://www.mozilla.org"],
+ ];
+ // First check the case where we do select a value with the keyboard:
+ doSearch("moz", results, function(aController) {
+ Assert.equal(aController.input.textValue, "moz");
+ Assert.equal(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ Assert.equal(aController.getFinalCompleteValueAt(1), "http://www.mozilla.org");
+
+ Assert.equal(aController.input.popup.selectedIndex, 0);
+ aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_DOWN);
+ Assert.equal(aController.input.popup.selectedIndex, 1);
+ // Simulate mouse interaction changing selectedIndex
+ // ie NOT keyboard interaction:
+ aController.input.popup.selectedIndex = 0;
+
+ aController.handleEnter(false);
+ // Verify that the keyboard-selected thing got inserted,
+ // and not the mouse selection:
+ Assert.equal(aController.input.textValue, "http://www.mozilla.org");
+ });
+});
+
+add_test(function test_handleEnter_mouse() {
+ let results = [
+ ["mozilla.com", "http://www.mozilla.com"],
+ ["mozilla.org", "http://www.mozilla.org"],
+ ];
+ // Then the case where we do not:
+ doSearch("moz", results, function(aController) {
+ Assert.equal(aController.input.textValue, "moz");
+ Assert.equal(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ Assert.equal(aController.getFinalCompleteValueAt(1), "http://www.mozilla.org");
+
+ Assert.equal(aController.input.popup.selectedIndex, 0);
+ aController.input.popupOpen = true;
+ // Simulate mouse interaction changing selectedIndex
+ // ie NOT keyboard interaction:
+ aController.input.popup.selectedIndex = 1;
+ Assert.equal(selectByWasCalled, false);
+ Assert.equal(aController.input.popup.selectedIndex, 1);
+
+ aController.handleEnter(false);
+ // Verify that the input stayed the same, because no selection was made
+ // with the keyboard:
+ Assert.equal(aController.input.textValue, "moz");
+ });
+});
+
+add_test(function test_handleEnter_preselected() {
+ let results = [
+ ["mozilla.com", "http://www.mozilla.com"],
+ ["mozilla.org", "http://www.mozilla.org"],
+ ];
+ // Then test a preselection.
+ doSearch("moz", results, function(aController) {
+ Assert.equal(aController.input.textValue, "moz");
+ Assert.equal(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ Assert.equal(aController.getFinalCompleteValueAt(1), "http://www.mozilla.org");
+
+ aController.setInitiallySelectedIndex(0);
+
+ aController.handleEnter(false);
+ // Verify that the input stayed the same, because no selection was made
+ // with the keyboard:
+ Assert.equal(aController.input.textValue, "http://www.mozilla.com");
+ });
+});
+
+function doSearch(aSearchString, aResults, aOnCompleteCallback) {
+ selectByWasCalled = false;
+ let search = new AutoCompleteSearchBase(
+ "search",
+ new AutoCompleteResult(aResults)
+ );
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput([ search.name ]);
+ input.textValue = aSearchString;
+
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ // Clean up.
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
+
diff --git a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
new file mode 100644
index 0000000000..4942e7a9ff
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
@@ -0,0 +1,107 @@
+function AutoCompleteResult(aResultValues) {
+ this.defaultIndex = 0;
+ this._values = aResultValues.map(x => x[0]);
+ this._finalCompleteValues = aResultValues.map(x => x[1]);
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popup.selectedIndex = 0;
+ this.completeSelectedIndex = true;
+ this.completeDefaultIndex = true;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+add_test(function test_handleEnter() {
+ let results = [
+ ["mozilla.com", "https://www.mozilla.com"],
+ ["gomozilla.org", "http://www.gomozilla.org"],
+ ];
+ doSearch("moz", results, { selectedIndex: 0 }, controller => {
+ let input = controller.input;
+ Assert.equal(input.textValue, "mozilla.com");
+ Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
+ Assert.equal(controller.getFinalCompleteValueAt(1), results[1][1]);
+ Assert.equal(input.popup.selectedIndex, 0);
+
+ controller.handleEnter(false);
+ // Verify that the keyboard-selected thing got inserted,
+ // and not the mouse selection:
+ Assert.equal(controller.input.textValue, "https://www.mozilla.com");
+ });
+});
+
+add_test(function test_handleEnter_otherSelected() {
+ // The popup selection may not coincide with what is filled into the input
+ // field, for example if the user changed it with the mouse and then pressed
+ // Enter. In such a case we should still use the inputField value and not the
+ // popup selected value.
+ let results = [
+ ["mozilla.com", "https://www.mozilla.com"],
+ ["gomozilla.org", "http://www.gomozilla.org"],
+ ];
+ doSearch("moz", results, { selectedIndex: 1 }, controller => {
+ let input = controller.input;
+ Assert.equal(input.textValue, "mozilla.com");
+ Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
+ Assert.equal(controller.getFinalCompleteValueAt(1), results[1][1]);
+ Assert.equal(input.popup.selectedIndex, 1);
+
+ controller.handleEnter(false);
+ // Verify that the keyboard-selected thing got inserted,
+ // and not the mouse selection:
+ Assert.equal(controller.input.textValue, "https://www.mozilla.com");
+ });
+});
+
+add_test(function test_handleEnter_otherSelected_nocompleteselectedindex() {
+ let results = [
+ ["mozilla.com", "https://www.mozilla.com"],
+ ["gomozilla.org", "http://www.gomozilla.org"],
+ ];
+ doSearch("moz", results, { selectedIndex: 1,
+ completeSelectedIndex: false }, controller => {
+ let input = controller.input;
+ Assert.equal(input.textValue, "mozilla.com");
+ Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
+ Assert.equal(controller.getFinalCompleteValueAt(1), results[1][1]);
+ Assert.equal(input.popup.selectedIndex, 1);
+
+ controller.handleEnter(false);
+ // Verify that the keyboard-selected result is inserted, not the
+ // defaultComplete.
+ Assert.equal(controller.input.textValue, "http://www.gomozilla.org");
+ });
+});
+
+function doSearch(aSearchString, aResults, aOptions, aOnCompleteCallback) {
+ let search = new AutoCompleteSearchBase(
+ "search",
+ new AutoCompleteResult(aResults)
+ );
+ registerAutoCompleteSearch(search);
+
+ let input = new AutoCompleteInput([ search.name ]);
+ input.textValue = aSearchString;
+ if ("selectedIndex" in aOptions) {
+ input.popup.selectedIndex = aOptions.selectedIndex;
+ }
+ if ("completeSelectedIndex" in aOptions) {
+ input.completeSelectedIndex = aOptions.completeSelectedIndex;
+ }
+ // Needed for defaultIndex completion.
+ input.selectTextRange(aSearchString.length, aSearchString.length);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
new file mode 100644
index 0000000000..5642d3e3ec
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 AutoCompleteResult(aValues, aFinalCompleteValues) {
+ this._values = aValues;
+ this._finalCompleteValues = aFinalCompleteValues;
+ this.defaultIndex = 0;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popup.selectedIndex = -1;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_handleEnterWithDirectMatchCompleteSelectedIndex() {
+ doSearch("moz", "mozilla.com", "http://www.mozilla.com",
+ { forceComplete: true, completeSelectedIndex: true }, function(aController) {
+ do_check_eq(aController.input.textValue, "moz");
+ do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ aController.handleEnter(false);
+ // After enter the final complete value should be shown in the input.
+ do_check_eq(aController.input.textValue, "http://www.mozilla.com");
+ });
+});
+
+add_test(function test_handleEnterWithDirectMatch() {
+ doSearch("mozilla", "mozilla.com", "http://www.mozilla.com",
+ { forceComplete: true, completeDefaultIndex: true }, function(aController) {
+ // Should autocomplete the search string to a suggestion.
+ do_check_eq(aController.input.textValue, "mozilla.com");
+ do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ aController.handleEnter(false);
+ // After enter the final complete value should be shown in the input.
+ do_check_eq(aController.input.textValue, "http://www.mozilla.com");
+ });
+});
+
+add_test(function test_handleEnterWithNoMatch() {
+ doSearch("mozilla", "mozilla.com", "http://www.mozilla.com",
+ { forceComplete: true, completeDefaultIndex: true }, function(aController) {
+ // Should autocomplete the search string to a suggestion.
+ do_check_eq(aController.input.textValue, "mozilla.com");
+ do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ // Now input something that does not match...
+ aController.input.textValue = "mozillax";
+ // ... and confirm. We don't want one of the values from the previous
+ // results to be taken, since what's now in the input field doesn't match.
+ aController.handleEnter(false);
+ do_check_eq(aController.input.textValue, "mozillax");
+ });
+});
+
+add_test(function test_handleEnterWithIndirectMatch() {
+ doSearch("com", "mozilla.com", "http://www.mozilla.com",
+ { forceComplete: true, completeDefaultIndex: true }, function(aController) {
+ // Should autocomplete the search string to a suggestion.
+ do_check_eq(aController.input.textValue, "com >> mozilla.com");
+ do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
+ aController.handleEnter(false);
+ // After enter the final complete value from the suggestion should be shown
+ // in the input.
+ do_check_eq(aController.input.textValue, "http://www.mozilla.com");
+ });
+});
+
+function doSearch(aSearchString, aResultValue, aFinalCompleteValue,
+ aInputProps, aOnCompleteCallback) {
+ let search = new AutoCompleteSearchBase(
+ "search",
+ new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
+ );
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput([ search.name ]);
+ for (var p in aInputProps) {
+ input[p] = aInputProps[p];
+ }
+ input.textValue = aSearchString;
+ // Place the cursor at the end of the input so that completion to
+ // default index will kick in.
+ input.selectTextRange(aSearchString.length, aSearchString.length);
+
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ // Clean up.
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js b/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js
new file mode 100644
index 0000000000..c983d969b3
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.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/. */
+
+function AutoCompleteResult(aValues, aFinalCompleteValues) {
+ this._values = aValues;
+ this._finalCompleteValues = aFinalCompleteValues;
+ this.defaultIndex = 0;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ this.popup.selectedIndex = -1;
+ this.completeDefaultIndex = true;
+}
+AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_keyNavigation() {
+ doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
+ do_check_eq(aController.input.textValue, "mozilla.com");
+ aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
+ do_check_eq(aController.input.textValue, "mozilla.com");
+ });
+});
+
+add_test(function test_handleEnter() {
+ doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
+ do_check_eq(aController.input.textValue, "mozilla.com");
+ aController.handleEnter(false);
+ do_check_eq(aController.input.textValue, "http://www.mozilla.com");
+ });
+});
+
+function doSearch(aSearchString, aResultValue, aFinalCompleteValue, aOnCompleteCallback) {
+ let search = new AutoCompleteSearchBase(
+ "search",
+ new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
+ );
+ registerAutoCompleteSearch(search);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput([ search.name ]);
+ input.textValue = aSearchString;
+
+ // Caret must be at the end for autofill to happen.
+ let strLen = aSearchString.length;
+ input.selectTextRange(strLen, strLen);
+ controller.input = input;
+ controller.startSearch(aSearchString);
+
+ input.onSearchComplete = function onSearchComplete() {
+ aOnCompleteCallback(controller);
+
+ // Clean up.
+ unregisterAutoCompleteSearch(search);
+ run_next_test();
+ };
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_immediate_search.js b/toolkit/components/autocomplete/tests/unit/test_immediate_search.js
new file mode 100644
index 0000000000..0579f5dcb0
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_immediate_search.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 AutoCompleteImmediateSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteImmediateSearch.prototype = Object.create(AutoCompleteSearchBase.prototype);
+AutoCompleteImmediateSearch.prototype.searchType =
+ Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
+AutoCompleteImmediateSearch.prototype.QueryInterface =
+ XPCOMUtils.generateQI([Ci.nsIFactory,
+ Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteSearchDescriptor]);
+
+function AutoCompleteDelayedSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteDelayedSearch.prototype = Object.create(AutoCompleteSearchBase.prototype);
+
+function AutoCompleteResult(aValues, aDefaultIndex) {
+ this._values = aValues;
+ this.defaultIndex = aDefaultIndex;
+}
+AutoCompleteResult.prototype = Object.create(AutoCompleteResultBase.prototype);
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * An immediate search should be executed synchronously.
+ */
+add_test(function test_immediate_search() {
+ let inputStr = "moz";
+
+ let immediateSearch = new AutoCompleteImmediateSearch(
+ "immediate", new AutoCompleteResult(["moz-immediate"], 0));
+ registerAutoCompleteSearch(immediateSearch);
+ let delayedSearch = new AutoCompleteDelayedSearch(
+ "delayed", new AutoCompleteResult(["moz-delayed"], 0));
+ registerAutoCompleteSearch(delayedSearch);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ let input = new AutoCompleteInputBase([delayedSearch.name,
+ immediateSearch.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = inputStr;
+
+ // Caret must be at the end. Autofill doesn't happen unless you're typing
+ // characters at the end.
+ let strLen = inputStr.length;
+ input.selectTextRange(strLen, strLen);
+
+ controller.input = input;
+ controller.startSearch(inputStr);
+
+ // Immediately check the result, the immediate search should have finished.
+ do_check_eq(input.textValue, "moz-immediate");
+
+ // Wait for both queries to finish.
+ input.onSearchComplete = function() {
+ // Sanity check.
+ do_check_eq(input.textValue, "moz-immediate");
+
+ unregisterAutoCompleteSearch(immediateSearch);
+ unregisterAutoCompleteSearch(delayedSearch);
+ run_next_test();
+ };
+});
+
+/**
+ * An immediate search should be executed before any delayed search.
+ */
+add_test(function test_immediate_search_notimeout() {
+ let inputStr = "moz";
+
+ let immediateSearch = new AutoCompleteImmediateSearch(
+ "immediate", new AutoCompleteResult(["moz-immediate"], 0));
+ registerAutoCompleteSearch(immediateSearch);
+
+ let delayedSearch = new AutoCompleteDelayedSearch(
+ "delayed", new AutoCompleteResult(["moz-delayed"], 0));
+ registerAutoCompleteSearch(delayedSearch);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ let input = new AutoCompleteInputBase([delayedSearch.name,
+ immediateSearch.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = inputStr;
+ input.timeout = 0;
+
+ // Caret must be at the end. Autofill doesn't happen unless you're typing
+ // characters at the end.
+ let strLen = inputStr.length;
+ input.selectTextRange(strLen, strLen);
+
+ controller.input = input;
+ let complete = false;
+ input.onSearchComplete = function() {
+ complete = true;
+ };
+ controller.startSearch(inputStr);
+ do_check_true(complete);
+
+ // Immediately check the result, the immediate search should have finished.
+ do_check_eq(input.textValue, "moz-immediate");
+
+ unregisterAutoCompleteSearch(immediateSearch);
+ unregisterAutoCompleteSearch(delayedSearch);
+ run_next_test();
+});
+
+/**
+ * A delayed search should be executed synchronously with a zero timeout.
+ */
+add_test(function test_delayed_search_notimeout() {
+ let inputStr = "moz";
+
+ let delayedSearch = new AutoCompleteDelayedSearch(
+ "delayed", new AutoCompleteResult(["moz-delayed"], 0));
+ registerAutoCompleteSearch(delayedSearch);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ let input = new AutoCompleteInputBase([delayedSearch.name]);
+ input.completeDefaultIndex = true;
+ input.textValue = inputStr;
+ input.timeout = 0;
+
+ // Caret must be at the end. Autofill doesn't happen unless you're typing
+ // characters at the end.
+ let strLen = inputStr.length;
+ input.selectTextRange(strLen, strLen);
+
+ controller.input = input;
+ let complete = false;
+ input.onSearchComplete = function() {
+ complete = true;
+ };
+ controller.startSearch(inputStr);
+ do_check_true(complete);
+
+ // Immediately check the result, the delayed search should have finished.
+ do_check_eq(input.textValue, "moz-delayed");
+
+ unregisterAutoCompleteSearch(delayedSearch);
+ run_next_test();
+});
diff --git a/toolkit/components/autocomplete/tests/unit/test_insertMatchAt.js b/toolkit/components/autocomplete/tests/unit/test_insertMatchAt.js
new file mode 100644
index 0000000000..14ee388b80
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_insertMatchAt.js
@@ -0,0 +1,14 @@
+function run_test() {
+ let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
+ .createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.appendMatch("a", "");
+ result.appendMatch("c", "");
+ result.insertMatchAt(1, "b", "");
+ result.insertMatchAt(3, "d", "");
+
+ Assert.equal(result.matchCount, 4);
+ Assert.equal(result.getValueAt(0), "a");
+ Assert.equal(result.getValueAt(1), "b");
+ Assert.equal(result.getValueAt(2), "c");
+ Assert.equal(result.getValueAt(3), "d");
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_previousResult.js b/toolkit/components/autocomplete/tests/unit/test_previousResult.js
new file mode 100644
index 0000000000..bfe6c7aaea
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_previousResult.js
@@ -0,0 +1,280 @@
+/* -*- 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/. */
+
+/**
+ * Unit test for Bug 438861 - Previous search results not returned to multiple
+ * searches.
+ */
+
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ // Array of AutoCompleteSearch names
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+
+/**
+ * nsIAutoCompleteResult implementation
+ */
+function AutoCompleteResult(aValues, aComments, aStyles) {
+ this._values = aValues;
+ this._comments = aComments;
+ this._styles = aStyles;
+ if (this._values.length > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ } else {
+ this.searchResult = Ci.nsIAutoCompleteResult.NOMATCH;
+ }
+}
+AutoCompleteResult.prototype = {
+ constructor: AutoCompleteResult,
+
+ // Arrays
+ _values: null,
+ _comments: null,
+ _styles: null,
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: 0,
+
+ get matchCount() {
+ return this._values.length;
+ },
+
+ getValueAt: function(aIndex) {
+ return this._values[aIndex];
+ },
+
+ getLabelAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function(aIndex) {
+ return this._comments[aIndex];
+ },
+
+ getStyleAt: function(aIndex) {
+ return this._styles[aIndex];
+ },
+
+ getImageAt: function(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt: function(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function (aRowIndex, aRemoveFromDb) {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteResult))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+/**
+ * nsIAutoCompleteSearch implementation that always returns
+ * the same result set.
+ */
+function AutoCompleteSearch(aName, aResult) {
+ this.name = aName;
+ this._result = aResult;
+}
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+
+ // Search name. Used by AutoCompleteController
+ name: null,
+
+ // AutoCompleteResult
+ _result: null,
+
+ _previousResult: null,
+
+
+ /**
+ * Return the same result set for every search
+ */
+ startSearch: function(aSearchString,
+ aSearchParam,
+ aPreviousResult,
+ aListener)
+ {
+ this._previousResult = aPreviousResult;
+ aListener.onSearchResult(this, this._result);
+ },
+
+ stopSearch: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsIAutoCompleteSearch))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ // nsIFactory implementation
+ createInstance: function(outer, iid) {
+ return this.QueryInterface(iid);
+ }
+}
+
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+
+ var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ var cid = uuidGenerator.generateUUID();
+
+ var desc = "Test AutoCompleteSearch";
+
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+
+/**
+ */
+function run_test() {
+ // Make an AutoCompleteSearch that always returns nothing
+ var search1 = new AutoCompleteSearch("test-previous-result1",
+ new AutoCompleteResult(["hello1"], [""], [""]));
+
+ var search2 = new AutoCompleteSearch("test-previous-result2",
+ new AutoCompleteResult(["hello2"], [""], [""]));
+
+ // Register search so AutoCompleteController can find them
+ registerAutoCompleteSearch(search1);
+ registerAutoCompleteSearch(search2);
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our search
+ // and confirms results on search complete
+ var input = new AutoCompleteInput([search1.name,
+ search2.name]);
+ var numSearchesStarted = 0;
+
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, 2);
+
+ if (numSearchesStarted == 1) {
+ do_check_eq(search1._previousResult, null);
+ do_check_eq(search2._previousResult, null);
+
+ // Now start it again
+ controller.startSearch("test");
+ return;
+ }
+ do_check_neq(search1._previousResult, null);
+ do_check_neq(search2._previousResult, null);
+
+ // Unregister searches
+ unregisterAutoCompleteSearch(search1);
+ unregisterAutoCompleteSearch(search2);
+
+ do_test_finished();
+ };
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ controller.startSearch("test");
+}
diff --git a/toolkit/components/autocomplete/tests/unit/test_stopSearch.js b/toolkit/components/autocomplete/tests/unit/test_stopSearch.js
new file mode 100644
index 0000000000..5ef3454b49
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/test_stopSearch.js
@@ -0,0 +1,187 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Purpose of the test is to check that a stopSearch call comes always before a
+ * startSearch call.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+
+/**
+ * Dummy nsIAutoCompleteInput source that returns
+ * the given list of AutoCompleteSearch names.
+ *
+ * Implements only the methods needed for this test.
+ */
+function AutoCompleteInput(aSearches)
+{
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "hello",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+ set popupOpen(val) { return val; }, // ignore
+ get popupOpen() { return false; },
+ get searchCount() { return this.searches.length; },
+ getSearchAt: function(aIndex) { return this.searches[aIndex]; },
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+ onTextReverted: function () {},
+ onTextEntered: function () {},
+ popup: {
+ selectBy: function() {},
+ invalidate: function() {},
+ set selectedIndex(val) { return val; }, // ignore
+ get selectedIndex() { return -1 },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup])
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
+}
+
+
+/**
+ * nsIAutoCompleteSearch implementation.
+ */
+function AutoCompleteSearch(aName)
+{
+ this.name = aName;
+}
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+ stopSearchInvoked: true,
+ startSearch: function(aSearchString, aSearchParam, aPreviousResult, aListener)
+ {
+ print("Check stop search has been called");
+ do_check_true(this.stopSearchInvoked);
+ this.stopSearchInvoked = false;
+ },
+ stopSearch: function()
+ {
+ this.stopSearchInvoked = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIFactory
+ , Ci.nsIAutoCompleteSearch
+ ]),
+ createInstance: function(outer, iid)
+ {
+ return this.QueryInterface(iid);
+ }
+}
+
+
+/**
+ * Helper to register an AutoCompleteSearch with the given name.
+ * Allows the AutoCompleteController to find the search.
+ */
+function registerAutoCompleteSearch(aSearch)
+{
+ let name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+ let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ let cid = uuidGenerator.generateUUID();
+ let desc = "Test AutoCompleteSearch";
+ let componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.registerFactory(cid, desc, name, aSearch);
+ // Keep the id on the object so we can unregister later
+ aSearch.cid = cid;
+}
+
+
+/**
+ * Helper to unregister an AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ let componentManager = Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar);
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+
+var gTests = [
+ function(controller) {
+ print("handleText");
+ controller.input.textValue = "hel";
+ controller.handleText();
+ },
+ function(controller) {
+ print("handleStartComposition");
+ controller.handleStartComposition();
+ },
+ function(controller) {
+ print("handleEndComposition");
+ controller.handleEndComposition();
+ // an input event always follows compositionend event.
+ controller.handleText();
+ },
+ function(controller) {
+ print("handleEscape");
+ controller.handleEscape();
+ },
+ function(controller) {
+ print("handleEnter");
+ controller.handleEnter(false);
+ },
+ function(controller) {
+ print("handleTab");
+ controller.handleTab();
+ },
+
+ function(controller) {
+ print("handleKeyNavigation");
+ controller.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_UP);
+ },
+];
+
+
+var gSearch;
+var gCurrentTest;
+function run_test() {
+ // Make an AutoCompleteSearch that always returns nothing
+ gSearch = new AutoCompleteSearch("test");
+ registerAutoCompleteSearch(gSearch);
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our search.
+ let input = new AutoCompleteInput([gSearch.name]);
+ controller.input = input;
+
+ input.onSearchBegin = function() {
+ do_execute_soon(function() {
+ gCurrentTest(controller);
+ });
+ };
+ input.onSearchComplete = function() {
+ run_next_test(controller);
+ }
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ run_next_test(controller);
+}
+
+function run_next_test(controller) {
+ if (gTests.length == 0) {
+ unregisterAutoCompleteSearch(gSearch);
+ controller.stopSearch();
+ controller.input = null;
+ do_test_finished();
+ return;
+ }
+
+ gCurrentTest = gTests.shift();
+ controller.startSearch("hello");
+}
diff --git a/toolkit/components/autocomplete/tests/unit/xpcshell.ini b/toolkit/components/autocomplete/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..4d193965cb
--- /dev/null
+++ b/toolkit/components/autocomplete/tests/unit/xpcshell.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+head = head_autocomplete.js
+tail =
+
+[test_330578.js]
+[test_378079.js]
+[test_393191.js]
+[test_440866.js]
+[test_463023.js]
+[test_660156.js]
+[test_autocomplete_multiple.js]
+[test_autocomplete_userContextId.js]
+[test_autofillSelectedPopupIndex.js]
+[test_badDefaultIndex.js]
+[test_completeDefaultIndex_casing.js]
+[test_finalCompleteValue.js]
+[test_finalCompleteValue_defaultIndex.js]
+[test_finalCompleteValue_forceComplete.js]
+[test_finalCompleteValueSelectedIndex.js]
+[test_finalDefaultCompleteValue.js]
+[test_immediate_search.js]
+[test_insertMatchAt.js]
+[test_previousResult.js]
+[test_stopSearch.js]
diff --git a/toolkit/components/build/moz.build b/toolkit/components/build/moz.build
new file mode 100644
index 0000000000..0851091b1f
--- /dev/null
+++ b/toolkit/components/build/moz.build
@@ -0,0 +1,35 @@
+# -*- 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 += [
+ 'nsToolkitCompsCID.h',
+]
+
+SOURCES += [
+ 'nsToolkitCompsModule.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+LOCAL_INCLUDES += [
+ '../../xre',
+ '../alerts',
+ '../downloads',
+ '../feeds',
+ '../find',
+ '../jsdownloads/src',
+ '../perfmonitoring',
+ '../protobuf',
+ '../startup',
+ '../statusfilter',
+ '../typeaheadfind',
+ '../url-classifier',
+]
+
+if not CONFIG['MOZ_DISABLE_PARENTAL_CONTROLS']:
+ LOCAL_INCLUDES += [
+ '../parentalcontrols',
+ ]
diff --git a/toolkit/components/build/nsToolkitCompsCID.h b/toolkit/components/build/nsToolkitCompsCID.h
new file mode 100644
index 0000000000..064f1dbc45
--- /dev/null
+++ b/toolkit/components/build/nsToolkitCompsCID.h
@@ -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/. */
+
+#define NS_ALERTSERVICE_CONTRACTID \
+ "@mozilla.org/alerts-service;1"
+
+// This separate service uses the same nsIAlertsService interface,
+// but instead sends a notification to a platform alerts API
+// if available. Using a separate CID allows us to overwrite the XUL
+// alerts service at runtime.
+#define NS_SYSTEMALERTSERVICE_CONTRACTID \
+ "@mozilla.org/system-alerts-service;1"
+
+#define NS_AUTOCOMPLETECONTROLLER_CONTRACTID \
+ "@mozilla.org/autocomplete/controller;1"
+
+#define NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID \
+ "@mozilla.org/autocomplete/simple-result;1"
+
+#define NS_AUTOCOMPLETEMDBRESULT_CONTRACTID \
+ "@mozilla.org/autocomplete/mdb-result;1"
+
+#define NS_DOWNLOADMANAGER_CONTRACTID \
+ "@mozilla.org/download-manager;1"
+
+#define NS_DOWNLOADPLATFORM_CONTRACTID \
+ "@mozilla.org/toolkit/download-platform;1"
+
+#define NS_FORMHISTORY_CONTRACTID \
+ "@mozilla.org/satchel/form-history;1"
+
+#define NS_FORMFILLCONTROLLER_CONTRACTID \
+ "@mozilla.org/satchel/form-fill-controller;1"
+
+#define NS_FORMHISTORYAUTOCOMPLETE_CONTRACTID \
+ "@mozilla.org/autocomplete/search;1?name=form-history"
+
+#define NS_GLOBALHISTORY_DATASOURCE_CONTRACTID \
+ "@mozilla.org/rdf/datasource;1?name=history"
+
+#define NS_TYPEAHEADFIND_CONTRACTID \
+ "@mozilla.org/typeaheadfind;1"
+
+#define NS_PARENTALCONTROLSSERVICE_CONTRACTID \
+ "@mozilla.org/parental-controls-service;1"
+
+#define NS_URLCLASSIFIERPREFIXSET_CONTRACTID \
+ "@mozilla.org/url-classifier/prefixset;1"
+
+#define NS_URLCLASSIFIERDBSERVICE_CONTRACTID \
+ "@mozilla.org/url-classifier/dbservice;1"
+
+#define NS_URLCLASSIFIERSTREAMUPDATER_CONTRACTID \
+ "@mozilla.org/url-classifier/streamupdater;1"
+
+#define NS_URLCLASSIFIERUTILS_CONTRACTID \
+ "@mozilla.org/url-classifier/utils;1"
+
+#define NS_URLCLASSIFIERHASHCOMPLETER_CONTRACTID \
+ "@mozilla.org/url-classifier/hashcompleter;1"
+
+#define NS_NAVHISTORYSERVICE_CONTRACTID \
+ "@mozilla.org/browser/nav-history-service;1"
+
+#define NS_ANNOTATIONSERVICE_CONTRACTID \
+ "@mozilla.org/browser/annotation-service;1"
+
+#define NS_NAVBOOKMARKSSERVICE_CONTRACTID \
+ "@mozilla.org/browser/nav-bookmarks-service;1"
+
+#define NS_LIVEMARKSERVICE_CONTRACTID \
+ "@mozilla.org/browser/livemark-service;2"
+
+#define NS_TAGGINGSERVICE_CONTRACTID \
+"@mozilla.org/browser/tagging-service;1"
+
+#define NS_FAVICONSERVICE_CONTRACTID \
+ "@mozilla.org/browser/favicon-service;1"
+
+#define NS_APPSTARTUP_CONTRACTID \
+ "@mozilla.org/toolkit/app-startup;1"
+
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+#define NS_UPDATEPROCESSOR_CONTRACTID \
+ "@mozilla.org/updates/update-processor;1"
+#endif
+
+#define NS_ADDONCONTENTPOLICY_CONTRACTID \
+ "@mozilla.org/addons/content-policy;1"
+
+#define NS_ADDONPATHSERVICE_CONTRACTID \
+ "@mozilla.org/addon-path-service;1"
+
+/////////////////////////////////////////////////////////////////////////////
+
+#define ALERT_NOTIFICATION_CID \
+{ 0x9a7b7a41, 0x0b47, 0x47f7, { 0xb6, 0x1b, 0x15, 0xa2, 0x10, 0xd6, 0xf0, 0x20 } }
+
+// {A0CCAAF8-09DA-44D8-B250-9AC3E93C8117}
+#define NS_ALERTSSERVICE_CID \
+{ 0xa0ccaaf8, 0x9da, 0x44d8, { 0xb2, 0x50, 0x9a, 0xc3, 0xe9, 0x3c, 0x81, 0x17 } }
+
+// {84E11F80-CA55-11DD-AD8B-0800200C9A66}
+#define NS_SYSTEMALERTSSERVICE_CID \
+{ 0x84e11f80, 0xca55, 0x11dd, { 0xad, 0x8b, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } }
+
+// {F6D5EBBD-34F4-487d-9D10-3D34123E3EB9}
+#define NS_AUTOCOMPLETECONTROLLER_CID \
+{ 0xf6d5ebbd, 0x34f4, 0x487d, { 0x9d, 0x10, 0x3d, 0x34, 0x12, 0x3e, 0x3e, 0xb9 } }
+
+// {2ee3039b-2de4-43d9-93b0-649beacff39a}
+#define NS_AUTOCOMPLETESIMPLERESULT_CID \
+{ 0x2ee3039b, 0x2de4, 0x43d9, { 0x93, 0xb0, 0x64, 0x9b, 0xea, 0xcf, 0xf3, 0x9a } }
+
+// {7A6F70B6-2BBD-44b5-9304-501352D44AB5}
+#define NS_AUTOCOMPLETEMDBRESULT_CID \
+{ 0x7a6f70b6, 0x2bbd, 0x44b5, { 0x93, 0x4, 0x50, 0x13, 0x52, 0xd4, 0x4a, 0xb5 } }
+
+#define NS_DOWNLOADMANAGER_CID \
+ { 0xedb0490e, 0x1dd1, 0x11b2, { 0x83, 0xb8, 0xdb, 0xf8, 0xd8, 0x59, 0x06, 0xa6 } }
+
+#define NS_DOWNLOADPLATFORM_CID \
+ { 0x649a14c9, 0xfe5c, 0x48ec, { 0x9c, 0x85, 0x00, 0xca, 0xd9, 0xcc, 0xf3, 0x2e } }
+
+// {895DB6C7-DBDF-40ea-9F64-B175033243DC}
+#define NS_FORMFILLCONTROLLER_CID \
+{ 0x895db6c7, 0xdbdf, 0x40ea, { 0x9f, 0x64, 0xb1, 0x75, 0x3, 0x32, 0x43, 0xdc } }
+
+// {59648a91-5a60-4122-8ff2-54b839c84aed}
+#define NS_GLOBALHISTORY_CID \
+{ 0x59648a91, 0x5a60, 0x4122, { 0x8f, 0xf2, 0x54, 0xb8, 0x39, 0xc8, 0x4a, 0xed} }
+
+// {59648a91-5a60-4122-8ff2-54b839c84aed}
+#define NS_PARENTALCONTROLSSERVICE_CID \
+{ 0x580530e5, 0x118c, 0x4bc7, { 0xab, 0x88, 0xbc, 0x2c, 0xd2, 0xb9, 0x72, 0x23 } }
+
+// {e7f70966-9a37-48d7-8aeb-35998f31090e}
+#define NS_TYPEAHEADFIND_CID \
+{ 0xe7f70966, 0x9a37, 0x48d7, { 0x8a, 0xeb, 0x35, 0x99, 0x8f, 0x31, 0x09, 0x0e} }
+
+// {3d8579f0-75fa-4e00-ba41-38661d5b5d17}
+ #define NS_URLCLASSIFIERPREFIXSET_CID \
+{ 0x3d8579f0, 0x75fa, 0x4e00, { 0xba, 0x41, 0x38, 0x66, 0x1d, 0x5b, 0x5d, 0x17} }
+
+// {7a258022-6765-11e5-b379-b37b1f2354be}
+#define NS_URLCLASSIFIERDBSERVICE_CID \
+{ 0x7a258022, 0x6765, 0x11e5, { 0xb3, 0x79, 0xb3, 0x7b, 0x1f, 0x23, 0x54, 0xbe} }
+
+// e1797597-f4d6-4dd3-a1e1-745ad352cd80
+#define NS_URLCLASSIFIERSTREAMUPDATER_CID \
+{ 0xe1797597, 0xf4d6, 0x4dd3, { 0xa1, 0xe1, 0x74, 0x5a, 0xd3, 0x52, 0xcd, 0x80 }}
+
+// {b7b2ccec-7912-4ea6-a548-b038447004bd}
+#define NS_URLCLASSIFIERUTILS_CID \
+{ 0xb7b2ccec, 0x7912, 0x4ea6, { 0xa5, 0x48, 0xb0, 0x38, 0x44, 0x70, 0x04, 0xbd} }
+
+#define NS_NAVHISTORYSERVICE_CID \
+{ 0x88cecbb7, 0x6c63, 0x4b3b, { 0x8c, 0xd4, 0x84, 0xf3, 0xb8, 0x22, 0x8c, 0x69 } }
+
+#define NS_NAVHISTORYRESULTTREEVIEWER_CID \
+{ 0x2ea8966f, 0x0671, 0x4c02, { 0x9c, 0x70, 0x94, 0x59, 0x56, 0xd4, 0x54, 0x34 } }
+
+#define NS_ANNOTATIONSERVICE_CID \
+{ 0x5e8d4751, 0x1852, 0x434b, { 0xa9, 0x92, 0x2c, 0x6d, 0x2a, 0x25, 0xfa, 0x46 } }
+
+#define NS_NAVBOOKMARKSSERVICE_CID \
+{ 0x9de95a0c, 0x39a4, 0x4d64, {0x9a, 0x53, 0x17, 0x94, 0x0d, 0xd7, 0xca, 0xbb}}
+
+#define NS_FAVICONSERVICE_CID \
+{ 0x984e3259, 0x9266, 0x49cf, { 0xb6, 0x05, 0x60, 0xb0, 0x22, 0xa0, 0x07, 0x56 } }
+
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+#define NS_UPDATEPROCESSOR_CID \
+{ 0xf3dcf644, 0x79e8, 0x4f59, { 0xa1, 0xbb, 0x87, 0x84, 0x54, 0x48, 0x8e, 0xf9 } }
+#endif
+
+#define NS_APPLICATION_REPUTATION_SERVICE_CONTRACTID \
+ "@mozilla.org/downloads/application-reputation-service;1"
+
+#define NS_APPLICATION_REPUTATION_SERVICE_CID \
+{ 0x8576c950, 0xf4a2, 0x11e2, { 0xb7, 0x78, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } }
+
+#define NS_ADDONCONTENTPOLICY_CID \
+{ 0xc26a8241, 0xecf4, 0x4aed, { 0x9f, 0x3c, 0xf1, 0xf5, 0xc7, 0x13, 0xb9, 0xa5 } }
+
+#define NS_ADDON_PATH_SERVICE_CID \
+{ 0xa39f39d0, 0xdfb6, 0x11e3, { 0x8b, 0x68, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } }
diff --git a/toolkit/components/build/nsToolkitCompsModule.cpp b/toolkit/components/build/nsToolkitCompsModule.cpp
new file mode 100644
index 0000000000..675c8c92b0
--- /dev/null
+++ b/toolkit/components/build/nsToolkitCompsModule.cpp
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsAppStartup.h"
+#include "nsNetCID.h"
+#include "nsUserInfo.h"
+#include "nsToolkitCompsCID.h"
+#include "nsFindService.h"
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+#include "nsUpdateDriver.h"
+#endif
+
+#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
+#include "nsParentalControlsService.h"
+#endif
+
+#include "mozilla/AlertNotification.h"
+#include "nsAlertsService.h"
+
+#include "nsDownloadManager.h"
+#include "DownloadPlatform.h"
+#include "nsDownloadProxy.h"
+#include "rdf.h"
+
+#include "nsTypeAheadFind.h"
+
+#include "ApplicationReputation.h"
+#include "nsUrlClassifierDBService.h"
+#include "nsUrlClassifierStreamUpdater.h"
+#include "nsUrlClassifierUtils.h"
+#include "nsUrlClassifierPrefixSet.h"
+
+#include "nsBrowserStatusFilter.h"
+#include "mozilla/FinalizationWitnessService.h"
+#include "mozilla/NativeOSFileInternals.h"
+#include "mozilla/AddonContentPolicy.h"
+#include "mozilla/AddonPathService.h"
+
+#if defined(XP_WIN)
+#include "NativeFileWatcherWin.h"
+#else
+#include "NativeFileWatcherNotSupported.h"
+#endif // (XP_WIN)
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+#define MOZ_HAS_TERMINATOR
+#endif
+
+#if defined(MOZ_HAS_TERMINATOR)
+#include "nsTerminator.h"
+#endif
+
+#define MOZ_HAS_PERFSTATS
+
+#if defined(MOZ_HAS_PERFSTATS)
+#include "nsPerformanceStats.h"
+#endif // defined (MOZ_HAS_PERFSTATS)
+
+using namespace mozilla;
+
+/////////////////////////////////////////////////////////////////////////////
+
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsAppStartup, Init)
+
+#if defined(MOZ_HAS_PERFSTATS)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsPerformanceStatsService, Init)
+#endif // defined (MOZ_HAS_PERFSTATS)
+
+#if defined(MOZ_HAS_TERMINATOR)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsTerminator)
+#endif
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsUserInfo)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsFindService)
+
+#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsParentalControlsService)
+#endif
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(AlertNotification)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsAlertsService)
+
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsDownloadManager,
+ nsDownloadManager::GetSingleton)
+NS_GENERIC_FACTORY_CONSTRUCTOR(DownloadPlatform)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsDownloadProxy)
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsTypeAheadFind)
+
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ApplicationReputationService,
+ ApplicationReputationService::GetSingleton)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsUrlClassifierPrefixSet)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsUrlClassifierStreamUpdater)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsUrlClassifierUtils, Init)
+
+static nsresult
+nsUrlClassifierDBServiceConstructor(nsISupports *aOuter, REFNSIID aIID,
+ void **aResult)
+{
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_NO_AGGREGATION(aOuter);
+
+ nsUrlClassifierDBService *inst = nsUrlClassifierDBService::GetInstance(&rv);
+ if (nullptr == inst) {
+ return rv;
+ }
+ /* NS_ADDREF(inst); */
+ rv = inst->QueryInterface(aIID, aResult);
+ NS_RELEASE(inst);
+
+ return rv;
+}
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsBrowserStatusFilter)
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateProcessor)
+#endif
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init)
+NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init)
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance)
+
+NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
+#if defined(MOZ_HAS_PERFSTATS)
+NS_DEFINE_NAMED_CID(NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID);
+#endif // defined (MOZ_HAS_PERFSTATS)
+
+#if defined(MOZ_HAS_TERMINATOR)
+NS_DEFINE_NAMED_CID(NS_TOOLKIT_TERMINATOR_CID);
+#endif
+NS_DEFINE_NAMED_CID(NS_USERINFO_CID);
+NS_DEFINE_NAMED_CID(ALERT_NOTIFICATION_CID);
+NS_DEFINE_NAMED_CID(NS_ALERTSSERVICE_CID);
+#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
+NS_DEFINE_NAMED_CID(NS_PARENTALCONTROLSSERVICE_CID);
+#endif
+NS_DEFINE_NAMED_CID(NS_DOWNLOADMANAGER_CID);
+NS_DEFINE_NAMED_CID(NS_DOWNLOADPLATFORM_CID);
+NS_DEFINE_NAMED_CID(NS_DOWNLOAD_CID);
+NS_DEFINE_NAMED_CID(NS_FIND_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_TYPEAHEADFIND_CID);
+NS_DEFINE_NAMED_CID(NS_APPLICATION_REPUTATION_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_URLCLASSIFIERPREFIXSET_CID);
+NS_DEFINE_NAMED_CID(NS_URLCLASSIFIERDBSERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_URLCLASSIFIERSTREAMUPDATER_CID);
+NS_DEFINE_NAMED_CID(NS_URLCLASSIFIERUTILS_CID);
+NS_DEFINE_NAMED_CID(NS_BROWSERSTATUSFILTER_CID);
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
+#endif
+NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
+NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID);
+NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID);
+NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID);
+
+static const Module::CIDEntry kToolkitCIDs[] = {
+ { &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
+#if defined(MOZ_HAS_TERMINATOR)
+ { &kNS_TOOLKIT_TERMINATOR_CID, false, nullptr, nsTerminatorConstructor },
+#endif
+#if defined(MOZ_HAS_PERFSTATS)
+ { &kNS_TOOLKIT_PERFORMANCESTATSSERVICE_CID, false, nullptr, nsPerformanceStatsServiceConstructor },
+#endif // defined (MOZ_HAS_PERFSTATS)
+ { &kNS_USERINFO_CID, false, nullptr, nsUserInfoConstructor },
+ { &kALERT_NOTIFICATION_CID, false, nullptr, AlertNotificationConstructor },
+ { &kNS_ALERTSSERVICE_CID, false, nullptr, nsAlertsServiceConstructor },
+#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
+ { &kNS_PARENTALCONTROLSSERVICE_CID, false, nullptr, nsParentalControlsServiceConstructor },
+#endif
+ { &kNS_DOWNLOADMANAGER_CID, false, nullptr, nsDownloadManagerConstructor },
+ { &kNS_DOWNLOADPLATFORM_CID, false, nullptr, DownloadPlatformConstructor },
+ { &kNS_DOWNLOAD_CID, false, nullptr, nsDownloadProxyConstructor },
+ { &kNS_FIND_SERVICE_CID, false, nullptr, nsFindServiceConstructor },
+ { &kNS_TYPEAHEADFIND_CID, false, nullptr, nsTypeAheadFindConstructor },
+ { &kNS_APPLICATION_REPUTATION_SERVICE_CID, false, nullptr, ApplicationReputationServiceConstructor },
+ { &kNS_URLCLASSIFIERPREFIXSET_CID, false, nullptr, nsUrlClassifierPrefixSetConstructor },
+ { &kNS_URLCLASSIFIERDBSERVICE_CID, false, nullptr, nsUrlClassifierDBServiceConstructor },
+ { &kNS_URLCLASSIFIERSTREAMUPDATER_CID, false, nullptr, nsUrlClassifierStreamUpdaterConstructor },
+ { &kNS_URLCLASSIFIERUTILS_CID, false, nullptr, nsUrlClassifierUtilsConstructor },
+ { &kNS_BROWSERSTATUSFILTER_CID, false, nullptr, nsBrowserStatusFilterConstructor },
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+ { &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
+#endif
+ { &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor },
+ { &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor },
+ { &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor },
+ { &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor },
+ { &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor },
+ { nullptr }
+};
+
+static const Module::ContractIDEntry kToolkitContracts[] = {
+ { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID },
+#if defined(MOZ_HAS_TERMINATOR)
+ { NS_TOOLKIT_TERMINATOR_CONTRACTID, &kNS_TOOLKIT_TERMINATOR_CID },
+#endif
+#if defined(MOZ_HAS_PERFSTATS)
+ { NS_TOOLKIT_PERFORMANCESTATSSERVICE_CONTRACTID, &kNS_TOOLKIT_PERFORMANCESTATSSERVICE_CID },
+#endif // defined (MOZ_HAS_PERFSTATS)
+ { NS_USERINFO_CONTRACTID, &kNS_USERINFO_CID },
+ { ALERT_NOTIFICATION_CONTRACTID, &kALERT_NOTIFICATION_CID },
+ { NS_ALERTSERVICE_CONTRACTID, &kNS_ALERTSSERVICE_CID },
+#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
+ { NS_PARENTALCONTROLSSERVICE_CONTRACTID, &kNS_PARENTALCONTROLSSERVICE_CID },
+#endif
+ { NS_DOWNLOADMANAGER_CONTRACTID, &kNS_DOWNLOADMANAGER_CID },
+ { NS_DOWNLOADPLATFORM_CONTRACTID, &kNS_DOWNLOADPLATFORM_CID },
+ { NS_FIND_SERVICE_CONTRACTID, &kNS_FIND_SERVICE_CID },
+ { NS_TYPEAHEADFIND_CONTRACTID, &kNS_TYPEAHEADFIND_CID },
+ { NS_APPLICATION_REPUTATION_SERVICE_CONTRACTID, &kNS_APPLICATION_REPUTATION_SERVICE_CID },
+ { NS_URLCLASSIFIERPREFIXSET_CONTRACTID, &kNS_URLCLASSIFIERPREFIXSET_CID },
+ { NS_URLCLASSIFIERDBSERVICE_CONTRACTID, &kNS_URLCLASSIFIERDBSERVICE_CID },
+ { NS_URICLASSIFIERSERVICE_CONTRACTID, &kNS_URLCLASSIFIERDBSERVICE_CID },
+ { NS_URLCLASSIFIERSTREAMUPDATER_CONTRACTID, &kNS_URLCLASSIFIERSTREAMUPDATER_CID },
+ { NS_URLCLASSIFIERUTILS_CONTRACTID, &kNS_URLCLASSIFIERUTILS_CID },
+ { NS_BROWSERSTATUSFILTER_CONTRACTID, &kNS_BROWSERSTATUSFILTER_CID },
+#if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
+ { NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
+#endif
+ { FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
+ { NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID },
+ { NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID },
+ { NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID },
+ { NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID },
+ { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kToolkitCategories[] = {
+ { "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID },
+ { nullptr }
+};
+
+static const Module kToolkitModule = {
+ Module::kVersion,
+ kToolkitCIDs,
+ kToolkitContracts,
+ kToolkitCategories
+};
+
+NSMODULE_DEFN(nsToolkitCompsModule) = &kToolkitModule;
diff --git a/toolkit/components/captivedetect/CaptivePortalDetectComponents.manifest b/toolkit/components/captivedetect/CaptivePortalDetectComponents.manifest
new file mode 100644
index 0000000000..490545c82b
--- /dev/null
+++ b/toolkit/components/captivedetect/CaptivePortalDetectComponents.manifest
@@ -0,0 +1,2 @@
+component {d9cd00ba-aa4d-47b1-8792-b1fe0cd35060} captivedetect.js
+contract @mozilla.org/toolkit/captive-detector;1 {d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}
diff --git a/toolkit/components/captivedetect/captivedetect.js b/toolkit/components/captivedetect/captivedetect.js
new file mode 100644
index 0000000000..5493ecec65
--- /dev/null
+++ b/toolkit/components/captivedetect/captivedetect.js
@@ -0,0 +1,476 @@
+/* -*- 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/. */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+XPCOMUtils.defineLazyServiceGetter(this, "gSysMsgr",
+ "@mozilla.org/system-message-internal;1",
+ "nsISystemMessagesInternal");
+
+const DEBUG = false; // set to true to show debug messages
+
+const kCAPTIVEPORTALDETECTOR_CONTRACTID = '@mozilla.org/toolkit/captive-detector;1';
+const kCAPTIVEPORTALDETECTOR_CID = Components.ID('{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}');
+
+const kOpenCaptivePortalLoginEvent = 'captive-portal-login';
+const kAbortCaptivePortalLoginEvent = 'captive-portal-login-abort';
+const kCaptivePortalLoginSuccessEvent = 'captive-portal-login-success';
+const kCaptivePortalCheckComplete = 'captive-portal-check-complete';
+
+const kCaptivePortalSystemMessage = 'captive-portal';
+
+function URLFetcher(url, timeout) {
+ let self = this;
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', url, true);
+ // Prevent the request from reading from the cache.
+ xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ // Prevent privacy leaks
+ xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
+ // The Cache-Control header is only interpreted by proxies and the
+ // final destination. It does not help if a resource is already
+ // cached locally.
+ xhr.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ xhr.setRequestHeader("Pragma", "no-cache");
+
+ xhr.timeout = timeout;
+ xhr.ontimeout = function () { self.ontimeout(); };
+ xhr.onerror = function () { self.onerror(); };
+ xhr.onreadystatechange = function(oEvent) {
+ if (xhr.readyState === 4) {
+ if (self._isAborted) {
+ return;
+ }
+ if (xhr.status === 200) {
+ self.onsuccess(xhr.responseText);
+ } else if (xhr.status) {
+ self.onredirectorerror(xhr.status);
+ }
+ }
+ };
+ xhr.send();
+ this._xhr = xhr;
+}
+
+URLFetcher.prototype = {
+ _isAborted: false,
+ ontimeout: function() {},
+ onerror: function() {},
+ abort: function() {
+ if (!this._isAborted) {
+ this._isAborted = true;
+ this._xhr.abort();
+ }
+ },
+}
+
+function LoginObserver(captivePortalDetector) {
+ const LOGIN_OBSERVER_STATE_DETACHED = 0; /* Should not monitor network activity since no ongoing login procedure */
+ const LOGIN_OBSERVER_STATE_IDLE = 1; /* No network activity currently, waiting for a longer enough idle period */
+ const LOGIN_OBSERVER_STATE_BURST = 2; /* Network activity is detected, probably caused by a login procedure */
+ const LOGIN_OBSERVER_STATE_VERIFY_NEEDED = 3; /* Verifing network accessiblity is required after a long enough idle */
+ const LOGIN_OBSERVER_STATE_VERIFYING = 4; /* LoginObserver is probing if public network is available */
+
+ let state = LOGIN_OBSERVER_STATE_DETACHED;
+
+ let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
+ let activityDistributor = Cc['@mozilla.org/network/http-activity-distributor;1']
+ .getService(Ci.nsIHttpActivityDistributor);
+ let urlFetcher = null;
+
+ let waitForNetworkActivity = Services.appinfo.widgetToolkit == "gonk";
+
+ let pageCheckingDone = function pageCheckingDone() {
+ if (state === LOGIN_OBSERVER_STATE_VERIFYING) {
+ urlFetcher = null;
+ // Finish polling the canonical site, switch back to idle state and
+ // waiting for next burst
+ state = LOGIN_OBSERVER_STATE_IDLE;
+ timer.initWithCallback(observer,
+ captivePortalDetector._pollingTime,
+ timer.TYPE_ONE_SHOT);
+ }
+ };
+
+ let checkPageContent = function checkPageContent() {
+ debug("checking if public network is available after the login procedure");
+
+ urlFetcher = new URLFetcher(captivePortalDetector._canonicalSiteURL,
+ captivePortalDetector._maxWaitingTime);
+ urlFetcher.ontimeout = pageCheckingDone;
+ urlFetcher.onerror = pageCheckingDone;
+ urlFetcher.onsuccess = function (content) {
+ if (captivePortalDetector.validateContent(content)) {
+ urlFetcher = null;
+ captivePortalDetector.executeCallback(true);
+ } else {
+ pageCheckingDone();
+ }
+ };
+ urlFetcher.onredirectorerror = pageCheckingDone;
+ };
+
+ // Public interface of LoginObserver
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpActivityObserver,
+ Ci.nsITimerCallback]),
+
+ attach: function attach() {
+ if (state === LOGIN_OBSERVER_STATE_DETACHED) {
+ activityDistributor.addObserver(this);
+ state = LOGIN_OBSERVER_STATE_IDLE;
+ timer.initWithCallback(this,
+ captivePortalDetector._pollingTime,
+ timer.TYPE_ONE_SHOT);
+ debug('attach HttpObserver for login activity');
+ }
+ },
+
+ detach: function detach() {
+ if (state !== LOGIN_OBSERVER_STATE_DETACHED) {
+ if (urlFetcher) {
+ urlFetcher.abort();
+ urlFetcher = null;
+ }
+ activityDistributor.removeObserver(this);
+ timer.cancel();
+ state = LOGIN_OBSERVER_STATE_DETACHED;
+ debug('detach HttpObserver for login activity');
+ }
+ },
+
+ /*
+ * Treat all HTTP transactions as captive portal login activities.
+ */
+ observeActivity: function observeActivity(aHttpChannel, aActivityType,
+ aActivitySubtype, aTimestamp,
+ aExtraSizeData, aExtraStringData) {
+ if (aActivityType === Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION
+ && aActivitySubtype === Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) {
+ switch (state) {
+ case LOGIN_OBSERVER_STATE_IDLE:
+ case LOGIN_OBSERVER_STATE_VERIFY_NEEDED:
+ state = LOGIN_OBSERVER_STATE_BURST;
+ break;
+ default:
+ break;
+ }
+ }
+ },
+
+ /*
+ * Check if login activity is finished according to HTTP burst.
+ */
+ notify : function notify() {
+ switch (state) {
+ case LOGIN_OBSERVER_STATE_BURST:
+ // Wait while network stays idle for a short period
+ state = LOGIN_OBSERVER_STATE_VERIFY_NEEDED;
+ // Fall though to start polling timer
+ case LOGIN_OBSERVER_STATE_IDLE:
+ if (waitForNetworkActivity) {
+ timer.initWithCallback(this,
+ captivePortalDetector._pollingTime,
+ timer.TYPE_ONE_SHOT);
+ break;
+ }
+ // if we don't need to wait for network activity, just fall through
+ // to perform a captive portal check.
+ case LOGIN_OBSERVER_STATE_VERIFY_NEEDED:
+ // Polling the canonical website since network stays idle for a while
+ state = LOGIN_OBSERVER_STATE_VERIFYING;
+ checkPageContent();
+ break;
+
+ default:
+ break;
+ }
+ },
+ };
+
+ return observer;
+}
+
+function CaptivePortalDetector() {
+ // Load preference
+ this._canonicalSiteURL = null;
+ this._canonicalSiteExpectedContent = null;
+
+ try {
+ this._canonicalSiteURL =
+ Services.prefs.getCharPref('captivedetect.canonicalURL');
+ this._canonicalSiteExpectedContent =
+ Services.prefs.getCharPref('captivedetect.canonicalContent');
+ } catch (e) {
+ debug('canonicalURL or canonicalContent not set.')
+ }
+
+ this._maxWaitingTime =
+ Services.prefs.getIntPref('captivedetect.maxWaitingTime');
+ this._pollingTime =
+ Services.prefs.getIntPref('captivedetect.pollingTime');
+ this._maxRetryCount =
+ Services.prefs.getIntPref('captivedetect.maxRetryCount');
+ debug('Load Prefs {site=' + this._canonicalSiteURL + ',content='
+ + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime
+ + "max-retry=" + this._maxRetryCount + '}');
+
+ // Create HttpObserver for monitoring the login procedure
+ this._loginObserver = LoginObserver(this);
+
+ this._nextRequestId = 0;
+ this._runningRequest = null;
+ this._requestQueue = []; // Maintain a progress table, store callbacks and the ongoing XHR
+ this._interfaceNames = {}; // Maintain names of the requested network interfaces
+
+ debug('CaptiveProtalDetector initiated, waiting for network connection established');
+}
+
+CaptivePortalDetector.prototype = {
+ classID: kCAPTIVEPORTALDETECTOR_CID,
+ classInfo: XPCOMUtils.generateCI({classID: kCAPTIVEPORTALDETECTOR_CID,
+ contractID: kCAPTIVEPORTALDETECTOR_CONTRACTID,
+ classDescription: 'Captive Portal Detector',
+ interfaces: [Ci.nsICaptivePortalDetector]}),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalDetector]),
+
+ // nsICaptivePortalDetector
+ checkCaptivePortal: function checkCaptivePortal(aInterfaceName, aCallback) {
+ if (!this._canonicalSiteURL) {
+ throw Components.Exception('No canonical URL set up.');
+ }
+
+ // Prevent multiple requests on a single network interface
+ if (this._interfaceNames[aInterfaceName]) {
+ throw Components.Exception('Do not allow multiple request on one interface: ' + aInterfaceName);
+ }
+
+ let request = {interfaceName: aInterfaceName};
+ if (aCallback) {
+ let callback = aCallback.QueryInterface(Ci.nsICaptivePortalCallback);
+ request['callback'] = callback;
+ request['retryCount'] = 0;
+ }
+ this._addRequest(request);
+ },
+
+ abort: function abort(aInterfaceName) {
+ debug('abort for ' + aInterfaceName);
+ this._removeRequest(aInterfaceName);
+ },
+
+ finishPreparation: function finishPreparation(aInterfaceName) {
+ debug('finish preparation phase for interface "' + aInterfaceName + '"');
+ if (!this._runningRequest
+ || this._runningRequest.interfaceName !== aInterfaceName) {
+ debug('invalid finishPreparation for ' + aInterfaceName);
+ throw Components.Exception('only first request is allowed to invoke |finishPreparation|');
+ }
+
+ this._startDetection();
+ },
+
+ cancelLogin: function cancelLogin(eventId) {
+ debug('login canceled by user for request "' + eventId + '"');
+ // Captive portal login procedure is canceled by user
+ if (this._runningRequest && this._runningRequest.hasOwnProperty('eventId')) {
+ let id = this._runningRequest.eventId;
+ if (eventId === id) {
+ this.executeCallback(false);
+ }
+ }
+ },
+
+ _applyDetection: function _applyDetection() {
+ debug('enter applyDetection('+ this._runningRequest.interfaceName + ')');
+
+ // Execute network interface preparation
+ if (this._runningRequest.hasOwnProperty('callback')) {
+ this._runningRequest.callback.prepare();
+ } else {
+ this._startDetection();
+ }
+ },
+
+ _startDetection: function _startDetection() {
+ debug('startDetection {site=' + this._canonicalSiteURL + ',content='
+ + this._canonicalSiteExpectedContent + ',time=' + this._maxWaitingTime + '}');
+ let self = this;
+
+ let urlFetcher = new URLFetcher(this._canonicalSiteURL, this._maxWaitingTime);
+
+ let mayRetry = this._mayRetry.bind(this);
+
+ urlFetcher.ontimeout = mayRetry;
+ urlFetcher.onerror = mayRetry;
+ urlFetcher.onsuccess = function (content) {
+ if (self.validateContent(content)) {
+ self.executeCallback(true);
+ } else {
+ // Content of the canonical website has been overwrite
+ self._startLogin();
+ }
+ };
+ urlFetcher.onredirectorerror = function (status) {
+ if (status >= 300 && status <= 399) {
+ // The canonical website has been redirected to an unknown location
+ self._startLogin();
+ } else {
+ mayRetry();
+ }
+ };
+
+ this._runningRequest['urlFetcher'] = urlFetcher;
+ },
+
+ _startLogin: function _startLogin() {
+ let id = this._allocateRequestId();
+ let details = {
+ type: kOpenCaptivePortalLoginEvent,
+ id: id,
+ url: this._canonicalSiteURL,
+ };
+ this._loginObserver.attach();
+ this._runningRequest['eventId'] = id;
+ this._sendEvent(kOpenCaptivePortalLoginEvent, details);
+ gSysMsgr.broadcastMessage(kCaptivePortalSystemMessage, {});
+ },
+
+ _mayRetry: function _mayRetry() {
+ if (this._runningRequest.retryCount++ < this._maxRetryCount) {
+ debug('retry-Detection: ' + this._runningRequest.retryCount + '/' + this._maxRetryCount);
+ this._startDetection();
+ } else {
+ this.executeCallback(false);
+ }
+ },
+
+ executeCallback: function executeCallback(success) {
+ if (this._runningRequest) {
+ debug('callback executed');
+ if (this._runningRequest.hasOwnProperty('callback')) {
+ this._runningRequest.callback.complete(success);
+ }
+
+ // Only when the request has a event id and |success| is true
+ // do we need to notify the login-success event.
+ if (this._runningRequest.hasOwnProperty('eventId') && success) {
+ let details = {
+ type: kCaptivePortalLoginSuccessEvent,
+ id: this._runningRequest['eventId'],
+ };
+ this._sendEvent(kCaptivePortalLoginSuccessEvent, details);
+ }
+
+ // Continue the following request
+ this._runningRequest['complete'] = true;
+ this._removeRequest(this._runningRequest.interfaceName);
+ }
+ },
+
+ _sendEvent: function _sendEvent(topic, details) {
+ debug('sendEvent "' + JSON.stringify(details) + '"');
+ Services.obs.notifyObservers(this,
+ topic,
+ JSON.stringify(details));
+ },
+
+ validateContent: function validateContent(content) {
+ debug('received content: ' + content);
+ let valid = content === this._canonicalSiteExpectedContent;
+ // We need a way to indicate that a check has been performed, and if we are
+ // still in a captive portal.
+ this._sendEvent(kCaptivePortalCheckComplete, !valid);
+ return valid;
+ },
+
+ _allocateRequestId: function _allocateRequestId() {
+ let newId = this._nextRequestId++;
+ return newId.toString();
+ },
+
+ _runNextRequest: function _runNextRequest() {
+ let nextRequest = this._requestQueue.shift();
+ if (nextRequest) {
+ this._runningRequest = nextRequest;
+ this._applyDetection();
+ }
+ },
+
+ _addRequest: function _addRequest(request) {
+ this._interfaceNames[request.interfaceName] = true;
+ this._requestQueue.push(request);
+ if (!this._runningRequest) {
+ this._runNextRequest();
+ }
+ },
+
+ _removeRequest: function _removeRequest(aInterfaceName) {
+ if (!this._interfaceNames[aInterfaceName]) {
+ return;
+ }
+
+ delete this._interfaceNames[aInterfaceName];
+
+ if (this._runningRequest
+ && this._runningRequest.interfaceName === aInterfaceName) {
+ this._loginObserver.detach();
+
+ if (!this._runningRequest.complete) {
+ // Abort the user login procedure
+ if (this._runningRequest.hasOwnProperty('eventId')) {
+ let details = {
+ type: kAbortCaptivePortalLoginEvent,
+ id: this._runningRequest.eventId
+ };
+ this._sendEvent(kAbortCaptivePortalLoginEvent, details);
+ }
+
+ // Abort the ongoing HTTP request
+ if (this._runningRequest.hasOwnProperty('urlFetcher')) {
+ this._runningRequest.urlFetcher.abort();
+ }
+ }
+
+ debug('remove running request');
+ this._runningRequest = null;
+
+ // Continue next pending reqeust if the ongoing one has been aborted
+ this._runNextRequest();
+ return;
+ }
+
+ // Check if a pending request has been aborted
+ for (let i = 0; i < this._requestQueue.length; i++) {
+ if (this._requestQueue[i].interfaceName == aInterfaceName) {
+ this._requestQueue.splice(i, 1);
+
+ debug('remove pending request #' + i + ', remaining ' + this._requestQueue.length);
+ break;
+ }
+ }
+ },
+};
+
+var debug;
+if (DEBUG) {
+ debug = function (s) {
+ dump('-*- CaptivePortalDetector component: ' + s + '\n');
+ };
+} else {
+ debug = function (s) {};
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CaptivePortalDetector]);
diff --git a/toolkit/components/captivedetect/moz.build b/toolkit/components/captivedetect/moz.build
new file mode 100644
index 0000000000..3bb9cf5733
--- /dev/null
+++ b/toolkit/components/captivedetect/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsICaptivePortalDetector.idl',
+]
+
+XPIDL_MODULE = 'captivedetect'
+
+EXTRA_COMPONENTS += [
+ 'captivedetect.js',
+ 'CaptivePortalDetectComponents.manifest',
+]
+
diff --git a/toolkit/components/captivedetect/nsICaptivePortalDetector.idl b/toolkit/components/captivedetect/nsICaptivePortalDetector.idl
new file mode 100644
index 0000000000..6316854517
--- /dev/null
+++ b/toolkit/components/captivedetect/nsICaptivePortalDetector.idl
@@ -0,0 +1,53 @@
+/* -*- 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"
+
+[scriptable, uuid(593fdeec-6284-4de8-b416-8e63cbdc695e)]
+interface nsICaptivePortalCallback : nsISupports
+{
+ /**
+ * Preparation for network interface before captive portal detection started.
+ */
+ void prepare();
+
+ /**
+ * Invoke callbacks after captive portal detection finished.
+ */
+ void complete(in bool success);
+};
+
+[scriptable, uuid(2f827c5a-f551-477f-af09-71adbfbd854a)]
+interface nsICaptivePortalDetector : nsISupports
+{
+ /**
+ * Perform captive portal detection on specific network interface.
+ * @param ifname The name of network interface, exception will be thrwon
+ * if the same interface has unfinished request.
+ * @param callback Callbacks when detection procedure starts and finishes.
+ */
+ void checkCaptivePortal(in wstring ifname,
+ in nsICaptivePortalCallback callback);
+
+ /**
+ * Abort captive portal detection for specific network interface
+ * due to system failure, callback will not be invoked.
+ * @param ifname The name of network interface.
+ */
+ void abort(in wstring ifname);
+
+ /**
+ * Cancel captive portal login procedure by user, callback will be invoked.
+ * @param eventId Login event id provided in |captive-portal-login| event.
+ */
+ void cancelLogin(in wstring eventId);
+
+ /**
+ * Notify prepare phase is finished, routing and dns must be ready for sending
+ * out XMLHttpRequest. this is callback for CaptivePortalDetector API user.
+ * @param ifname The name of network interface, must be unique.
+ */
+ void finishPreparation(in wstring ifname);
+};
diff --git a/toolkit/components/captivedetect/test/unit/.eslintrc.js b/toolkit/components/captivedetect/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/captivedetect/test/unit/head_setprefs.js b/toolkit/components/captivedetect/test/unit/head_setprefs.js
new file mode 100644
index 0000000000..bf621e31e7
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/head_setprefs.js
@@ -0,0 +1,49 @@
+/* -*- 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/. */
+'use strict';
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://testing-common/httpd.js');
+
+XPCOMUtils.defineLazyServiceGetter(this, 'gCaptivePortalDetector',
+ '@mozilla.org/toolkit/captive-detector;1',
+ 'nsICaptivePortalDetector');
+
+const kCanonicalSitePath = '/canonicalSite.html';
+const kCanonicalSiteContent = 'true';
+const kPrefsCanonicalURL = 'captivedetect.canonicalURL';
+const kPrefsCanonicalContent = 'captivedetect.canonicalContent';
+const kPrefsMaxWaitingTime = 'captivedetect.maxWaitingTime';
+const kPrefsPollingTime = 'captivedetect.pollingTime';
+
+var gServer;
+var gServerURL;
+
+function setupPrefs() {
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService)
+ .QueryInterface(Components.interfaces.nsIPrefBranch);
+ prefs.setCharPref(kPrefsCanonicalURL, gServerURL + kCanonicalSitePath);
+ prefs.setCharPref(kPrefsCanonicalContent, kCanonicalSiteContent);
+ prefs.setIntPref(kPrefsMaxWaitingTime, 0);
+ prefs.setIntPref(kPrefsPollingTime, 1);
+}
+
+function run_captivedetect_test(xhr_handler, fakeUIResponse, testfun)
+{
+ gServer = new HttpServer();
+ gServer.registerPathHandler(kCanonicalSitePath, xhr_handler);
+ gServer.start(-1);
+ gServerURL = 'http://localhost:' + gServer.identity.primaryPort;
+
+ setupPrefs();
+
+ fakeUIResponse();
+
+ testfun();
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_abort.js b/toolkit/components/captivedetect/test/unit/test_abort.js
new file mode 100644
index 0000000000..f99805dfba
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_abort.js
@@ -0,0 +1,53 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var server;
+var step = 0;
+var loginFinished = false;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ if (loginFinished) {
+ response.write('true');
+ } else {
+ response.write('false');
+ }
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ do_throw('should not receive captive-portal-login event');
+ }
+ }, 'captive-portal-login', false);
+}
+
+function test_abort() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_throw('should not execute |complete| callback');
+ },
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+ gCaptivePortalDetector.abort(kInterfaceName);
+ gServer.stop(do_test_finished);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_abort);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js b/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js
new file mode 100644
index 0000000000..ef98ac5eae
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_abort_during_user_login.js
@@ -0,0 +1,66 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var server;
+var step = 0;
+var loginFinished = false;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ if (loginFinished) {
+ response.write('true');
+ } else {
+ response.write('false');
+ }
+}
+
+function fakeUIResponse() {
+ let requestId;
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ loginFinished = true;
+ do_check_eq(++step, 2);
+ requestId = JSON.parse(data).id;
+ gCaptivePortalDetector.abort(kInterfaceName);
+ }
+ }, 'captive-portal-login', false);
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login-abort') {
+ do_check_eq(++step, 3);
+ do_check_eq(JSON.parse(data).id, requestId);
+ gServer.stop(do_test_finished);
+ }
+ }, 'captive-portal-login-abort', false);
+}
+
+function test_abort() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_throw('should not execute |complete| callback');
+ },
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_abort);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js b/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js
new file mode 100644
index 0000000000..ad99903dff
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_abort_ongoing_request.js
@@ -0,0 +1,72 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+const kOtherInterfaceName = 'ril';
+
+var server;
+var step = 0;
+var loginFinished = false;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ if (loginFinished) {
+ response.write('true');
+ } else {
+ response.write('false');
+ }
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ loginFinished = true;
+ do_check_eq(++step, 3);
+ }
+ }, 'captive-portal-login', false);
+}
+
+function test_multiple_requests_abort() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_throw('should not execute |complete| callback for ' + kInterfaceName);
+ },
+ };
+
+ let otherCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 2);
+ gCaptivePortalDetector.finishPreparation(kOtherInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 4);
+ do_check_true(success);
+ gServer.stop(do_test_finished);
+ }
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+ gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback);
+ gCaptivePortalDetector.abort(kInterfaceName);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_multiple_requests_abort);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js b/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js
new file mode 100644
index 0000000000..ce36f1e79a
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_abort_pending_request.js
@@ -0,0 +1,71 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+const kOtherInterfaceName = 'ril';
+
+var server;
+var step = 0;
+var loginFinished = false;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ if (loginFinished) {
+ response.write('true');
+ } else {
+ response.write('false');
+ }
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ loginFinished = true;
+ do_check_eq(++step, 2);
+ }
+ }, 'captive-portal-login', false);
+}
+
+function test_abort() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 3);
+ do_check_true(success);
+ gServer.stop(do_test_finished);
+ },
+ };
+
+ let otherCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_throw('should not execute |prepare| callback for ' + kOtherInterfaceName);
+ },
+ complete: function complete(success) {
+ do_throw('should not execute |complete| callback for ' + kInterfaceName);
+ }
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+ gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback);
+ gCaptivePortalDetector.abort(kOtherInterfaceName);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_abort);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js
new file mode 100644
index 0000000000..7fb7ba89e0
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js
@@ -0,0 +1,67 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var server;
+var step = 0;
+var loginFinished = false;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ if (loginFinished) {
+ response.write('true');
+ } else {
+ response.write('false');
+ }
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ loginFinished = true;
+ do_check_eq(++step, 2);
+ }
+ }, 'captive-portal-login', false);
+
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login-success') {
+ do_check_eq(++step, 4);
+ gServer.stop(do_test_finished);
+ }
+ }, 'captive-portal-login-success', false);
+}
+
+function test_portal_found() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ // Since this is a synchronous callback, it must happen before
+ // 'captive-portal-login-success' is received.
+ // (Check captivedetect.js::executeCallback
+ do_check_eq(++step, 3);
+ do_check_true(success);
+ },
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_found);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js
new file mode 100644
index 0000000000..7064e12c9c
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js
@@ -0,0 +1,74 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var step = 0;
+var loginFinished = false;
+
+var gRedirectServer;
+var gRedirectServerURL;
+
+function xhr_handler(metadata, response) {
+ if (loginFinished) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ response.write('true');
+ } else {
+ response.setStatusLine(metadata.httpVersion, 303, "See Other");
+ response.setHeader("Location", gRedirectServerURL, false);
+ response.setHeader("Content-Type", "text/html", false);
+ }
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ loginFinished = true;
+ do_check_eq(++step, 2);
+ }
+ }, 'captive-portal-login', false);
+
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login-success') {
+ do_check_eq(++step, 4);
+ gServer.stop(function () {
+ gRedirectServer.stop(do_test_finished);
+ });
+ }
+ }, 'captive-portal-login-success', false);
+}
+
+function test_portal_found() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 3);
+ do_check_true(success);
+ },
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+}
+
+function run_test() {
+ gRedirectServer = new HttpServer();
+ gRedirectServer.start(-1);
+ gRedirectServerURL = 'http://localhost:' + gRedirectServer.identity.primaryPort;
+
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_found);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js
new file mode 100644
index 0000000000..1dc4fe0092
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found.js
@@ -0,0 +1,52 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var server;
+var step = 0;
+var attempt = 0;
+
+function xhr_handler(metadata, response) {
+ dump('HTTP activity\n');
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ response.write('true');
+ attempt++;
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic == 'captive-portal-login') {
+ do_throw('should not receive captive-portal-login event');
+ }
+ }, 'captive-portal-login', false);
+}
+
+function test_portal_not_found() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 2);
+ do_check_true(success);
+ do_check_eq(attempt, 1);
+ gServer.stop(function() { dump('server stop\n'); do_test_finished(); });
+ }
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_not_found);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js
new file mode 100644
index 0000000000..66bcdd0778
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_not_found_404.js
@@ -0,0 +1,49 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var server;
+var step = 0;
+var loginFinished = false;
+var attempt = 0;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 404, "Page not Found");
+ attempt++;
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ do_throw('should not receive captive-portal-login event');
+ }
+ }, 'captive-portal-login', false);
+}
+
+function test_portal_not_found() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 2);
+ do_check_false(success);
+ do_check_eq(attempt, 6);
+ gServer.stop(do_test_finished);
+ },
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_not_found);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_multiple_requests.js b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js
new file mode 100644
index 0000000000..11cf5e4b2f
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+const kOtherInterfaceName = 'ril';
+
+var server;
+var step = 0;
+var loginFinished = false;
+var loginSuccessCount = 0;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ if (loginFinished) {
+ response.write('true');
+ } else {
+ response.write('false');
+ }
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ loginFinished = true;
+ do_check_eq(++step, 2);
+ }
+ }, 'captive-portal-login', false);
+
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login-success') {
+ loginSuccessCount++;
+ if (loginSuccessCount > 1) {
+ throw "We should only receive 'captive-portal-login-success' once";
+ }
+ do_check_eq(++step, 4);
+ }
+ }, 'captive-portal-login-success', false);
+}
+
+function test_multiple_requests() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 3);
+ do_check_true(success);
+ },
+ };
+
+ let otherCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 5);
+ gCaptivePortalDetector.finishPreparation(kOtherInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 6);
+ do_check_true(success);
+ gServer.stop(do_test_finished);
+ }
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+ gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_multiple_requests);
+}
diff --git a/toolkit/components/captivedetect/test/unit/test_user_cancel.js b/toolkit/components/captivedetect/test/unit/test_user_cancel.js
new file mode 100644
index 0000000000..a038768176
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/test_user_cancel.js
@@ -0,0 +1,54 @@
+/* -*- 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/. */
+'use strict';
+
+const kInterfaceName = 'wifi';
+
+var server;
+var step = 0;
+
+function xhr_handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, 'OK');
+ response.setHeader('Cache-Control', 'no-cache', false);
+ response.setHeader('Content-Type', 'text/plain', false);
+ response.write('false');
+}
+
+function fakeUIResponse() {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (topic === 'captive-portal-login') {
+ let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', gServerURL + kCanonicalSitePath, true);
+ xhr.send();
+ do_check_eq(++step, 2);
+ let details = JSON.parse(data);
+ gCaptivePortalDetector.cancelLogin(details.id);
+ }
+ }, 'captive-portal-login', false);
+}
+
+function test_cancel() {
+ do_test_pending();
+
+ let callback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
+ prepare: function prepare() {
+ do_check_eq(++step, 1);
+ gCaptivePortalDetector.finishPreparation(kInterfaceName);
+ },
+ complete: function complete(success) {
+ do_check_eq(++step, 3);
+ do_check_false(success);
+ gServer.stop(do_test_finished);
+ },
+ };
+
+ gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
+}
+
+function run_test() {
+ run_captivedetect_test(xhr_handler, fakeUIResponse, test_cancel);
+}
diff --git a/toolkit/components/captivedetect/test/unit/xpcshell.ini b/toolkit/components/captivedetect/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..0f440c438e
--- /dev/null
+++ b/toolkit/components/captivedetect/test/unit/xpcshell.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+head = head_setprefs.js
+tail =
+
+[test_captive_portal_not_found.js]
+[test_captive_portal_not_found_404.js]
+[test_captive_portal_found.js]
+[test_captive_portal_found_303.js]
+[test_abort.js]
+[test_abort_during_user_login.js]
+[test_user_cancel.js]
+[test_multiple_requests.js]
+[test_abort_ongoing_request.js]
+[test_abort_pending_request.js]
+
diff --git a/toolkit/components/commandlines/moz.build b/toolkit/components/commandlines/moz.build
new file mode 100644
index 0000000000..5798ccfcc5
--- /dev/null
+++ b/toolkit/components/commandlines/moz.build
@@ -0,0 +1,30 @@
+# -*- 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 += ['test/unit/xpcshell.ini']
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ XPCSHELL_TESTS_MANIFESTS += ['test/unit_win/xpcshell.ini']
+elif CONFIG['OS_ARCH'] != 'Darwin':
+ XPCSHELL_TESTS_MANIFESTS += ['test/unit_unix/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsICommandLine.idl',
+ 'nsICommandLineHandler.idl',
+ 'nsICommandLineRunner.idl',
+ 'nsICommandLineValidator.idl',
+]
+
+XPIDL_MODULE = 'commandlines'
+
+SOURCES += [
+ 'nsCommandLine.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Startup and Profile System')
diff --git a/toolkit/components/commandlines/nsCommandLine.cpp b/toolkit/components/commandlines/nsCommandLine.cpp
new file mode 100644
index 0000000000..280b1d24a6
--- /dev/null
+++ b/toolkit/components/commandlines/nsCommandLine.cpp
@@ -0,0 +1,660 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsICommandLineRunner.h"
+
+#include "nsICategoryManager.h"
+#include "nsICommandLineHandler.h"
+#include "nsICommandLineValidator.h"
+#include "nsIConsoleService.h"
+#include "nsIClassInfoImpl.h"
+#include "nsIDOMWindow.h"
+#include "nsIFile.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIStringEnumerator.h"
+
+#include "nsCOMPtr.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsISupportsImpl.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsNetUtil.h"
+#include "nsIFileProtocolHandler.h"
+#include "nsIURI.h"
+#include "nsUnicharUtils.h"
+#include "nsTArray.h"
+#include "nsTextFormatter.h"
+#include "nsXPCOMCID.h"
+#include "plstr.h"
+#include "mozilla/Attributes.h"
+
+#ifdef MOZ_WIDGET_COCOA
+#include <CoreFoundation/CoreFoundation.h>
+#include "nsILocalFileMac.h"
+#elif defined(XP_WIN)
+#include <windows.h>
+#include <shlobj.h>
+#elif defined(XP_UNIX)
+#include <unistd.h>
+#endif
+
+#ifdef DEBUG_bsmedberg
+#define DEBUG_COMMANDLINE
+#endif
+
+#define NS_COMMANDLINE_CID \
+ { 0x23bcc750, 0xdc20, 0x460b, { 0xb2, 0xd4, 0x74, 0xd8, 0xf5, 0x8d, 0x36, 0x15 } }
+
+class nsCommandLine final : public nsICommandLineRunner
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOMMANDLINE
+ NS_DECL_NSICOMMANDLINERUNNER
+
+ nsCommandLine();
+
+protected:
+ ~nsCommandLine() { }
+
+ typedef nsresult (*EnumerateHandlersCallback)(nsICommandLineHandler* aHandler,
+ nsICommandLine* aThis,
+ void *aClosure);
+ typedef nsresult (*EnumerateValidatorsCallback)(nsICommandLineValidator* aValidator,
+ nsICommandLine* aThis,
+ void *aClosure);
+
+ void appendArg(const char* arg);
+ MOZ_MUST_USE nsresult resolveShortcutURL(nsIFile* aFile, nsACString& outURL);
+ nsresult EnumerateHandlers(EnumerateHandlersCallback aCallback, void *aClosure);
+ nsresult EnumerateValidators(EnumerateValidatorsCallback aCallback, void *aClosure);
+
+ nsTArray<nsString> mArgs;
+ uint32_t mState;
+ nsCOMPtr<nsIFile> mWorkingDir;
+ nsCOMPtr<nsIDOMWindow> mWindowContext;
+ bool mPreventDefault;
+};
+
+nsCommandLine::nsCommandLine() :
+ mState(STATE_INITIAL_LAUNCH),
+ mPreventDefault(false)
+{
+
+}
+
+
+NS_IMPL_CLASSINFO(nsCommandLine, nullptr, 0, NS_COMMANDLINE_CID)
+NS_IMPL_ISUPPORTS_CI(nsCommandLine,
+ nsICommandLine,
+ nsICommandLineRunner)
+
+NS_IMETHODIMP
+nsCommandLine::GetLength(int32_t *aResult)
+{
+ *aResult = int32_t(mArgs.Length());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::GetArgument(int32_t aIndex, nsAString& aResult)
+{
+ NS_ENSURE_ARG_MIN(aIndex, 0);
+ NS_ENSURE_ARG_MAX(aIndex, int32_t(mArgs.Length() - 1));
+
+ aResult = mArgs[aIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::FindFlag(const nsAString& aFlag, bool aCaseSensitive, int32_t *aResult)
+{
+ NS_ENSURE_ARG(!aFlag.IsEmpty());
+
+ nsDefaultStringComparator caseCmp;
+ nsCaseInsensitiveStringComparator caseICmp;
+ nsStringComparator& c = aCaseSensitive ?
+ static_cast<nsStringComparator&>(caseCmp) :
+ static_cast<nsStringComparator&>(caseICmp);
+
+ for (uint32_t f = 0; f < mArgs.Length(); f++) {
+ const nsString &arg = mArgs[f];
+
+ if (arg.Length() >= 2 && arg.First() == char16_t('-')) {
+ if (aFlag.Equals(Substring(arg, 1), c)) {
+ *aResult = f;
+ return NS_OK;
+ }
+ }
+ }
+
+ *aResult = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::RemoveArguments(int32_t aStart, int32_t aEnd)
+{
+ NS_ENSURE_ARG_MIN(aStart, 0);
+ NS_ENSURE_ARG_MAX(uint32_t(aEnd) + 1, mArgs.Length());
+
+ for (int32_t i = aEnd; i >= aStart; --i) {
+ mArgs.RemoveElementAt(i);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::HandleFlag(const nsAString& aFlag, bool aCaseSensitive,
+ bool *aResult)
+{
+ nsresult rv;
+
+ int32_t found;
+ rv = FindFlag(aFlag, aCaseSensitive, &found);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (found == -1) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ *aResult = true;
+ RemoveArguments(found, found);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::HandleFlagWithParam(const nsAString& aFlag, bool aCaseSensitive,
+ nsAString& aResult)
+{
+ nsresult rv;
+
+ int32_t found;
+ rv = FindFlag(aFlag, aCaseSensitive, &found);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (found == -1) {
+ aResult.SetIsVoid(true);
+ return NS_OK;
+ }
+
+ if (found == int32_t(mArgs.Length()) - 1) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ++found;
+
+ if (mArgs[found].First() == '-') {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aResult = mArgs[found];
+ RemoveArguments(found - 1, found);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::GetState(uint32_t *aResult)
+{
+ *aResult = mState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::GetPreventDefault(bool *aResult)
+{
+ *aResult = mPreventDefault;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::SetPreventDefault(bool aValue)
+{
+ mPreventDefault = aValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::GetWorkingDirectory(nsIFile* *aResult)
+{
+ NS_ENSURE_TRUE(mWorkingDir, NS_ERROR_NOT_INITIALIZED);
+
+ NS_ADDREF(*aResult = mWorkingDir);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::GetWindowContext(nsIDOMWindow* *aResult)
+{
+ NS_IF_ADDREF(*aResult = mWindowContext);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::SetWindowContext(nsIDOMWindow* aValue)
+{
+ mWindowContext = aValue;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::ResolveFile(const nsAString& aArgument, nsIFile* *aResult)
+{
+ NS_ENSURE_TRUE(mWorkingDir, NS_ERROR_NOT_INITIALIZED);
+
+ // This is some seriously screwed-up code. nsIFile.appendRelativeNativePath
+ // explicitly does not accept .. or . path parts, but that is exactly what we
+ // need here. So we hack around it.
+
+ nsresult rv;
+
+#if defined(MOZ_WIDGET_COCOA)
+ nsCOMPtr<nsILocalFileMac> lfm (do_QueryInterface(mWorkingDir));
+ NS_ENSURE_TRUE(lfm, NS_ERROR_NO_INTERFACE);
+
+ nsCOMPtr<nsILocalFileMac> newfile (do_CreateInstance(NS_LOCAL_FILE_CONTRACTID));
+ NS_ENSURE_TRUE(newfile, NS_ERROR_OUT_OF_MEMORY);
+
+ CFURLRef baseurl;
+ rv = lfm->GetCFURL(&baseurl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString path;
+ NS_CopyUnicodeToNative(aArgument, path);
+
+ CFURLRef newurl =
+ CFURLCreateFromFileSystemRepresentationRelativeToBase(nullptr, (const UInt8*) path.get(),
+ path.Length(),
+ true, baseurl);
+
+ CFRelease(baseurl);
+
+ rv = newfile->InitWithCFURL(newurl);
+ CFRelease(newurl);
+ if (NS_FAILED(rv)) return rv;
+
+ newfile.forget(aResult);
+ return NS_OK;
+
+#elif defined(XP_UNIX)
+ nsCOMPtr<nsIFile> lf (do_CreateInstance(NS_LOCAL_FILE_CONTRACTID));
+ NS_ENSURE_TRUE(lf, NS_ERROR_OUT_OF_MEMORY);
+
+ if (aArgument.First() == '/') {
+ // absolute path
+ rv = lf->InitWithPath(aArgument);
+ if (NS_FAILED(rv)) return rv;
+
+ NS_ADDREF(*aResult = lf);
+ return NS_OK;
+ }
+
+ nsAutoCString nativeArg;
+ NS_CopyUnicodeToNative(aArgument, nativeArg);
+
+ nsAutoCString newpath;
+ mWorkingDir->GetNativePath(newpath);
+
+ newpath.Append('/');
+ newpath.Append(nativeArg);
+
+ rv = lf->InitWithNativePath(newpath);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = lf->Normalize();
+ if (NS_FAILED(rv)) return rv;
+
+ lf.forget(aResult);
+ return NS_OK;
+
+#elif defined(XP_WIN32)
+ nsCOMPtr<nsIFile> lf (do_CreateInstance(NS_LOCAL_FILE_CONTRACTID));
+ NS_ENSURE_TRUE(lf, NS_ERROR_OUT_OF_MEMORY);
+
+ rv = lf->InitWithPath(aArgument);
+ if (NS_FAILED(rv)) {
+ // If it's a relative path, the Init is *going* to fail. We use string magic and
+ // win32 _fullpath. Note that paths of the form "\Relative\To\CurDrive" are
+ // going to fail, and I haven't figured out a way to work around this without
+ // the PathCombine() function, which is not available in plain win95/nt4
+
+ nsAutoString fullPath;
+ mWorkingDir->GetPath(fullPath);
+
+ fullPath.Append('\\');
+ fullPath.Append(aArgument);
+
+ WCHAR pathBuf[MAX_PATH];
+ if (!_wfullpath(pathBuf, fullPath.get(), MAX_PATH))
+ return NS_ERROR_FAILURE;
+
+ rv = lf->InitWithPath(nsDependentString(pathBuf));
+ if (NS_FAILED(rv)) return rv;
+ }
+ lf.forget(aResult);
+ return NS_OK;
+
+#else
+#error Need platform-specific logic here.
+#endif
+}
+
+NS_IMETHODIMP
+nsCommandLine::ResolveURI(const nsAString& aArgument, nsIURI* *aResult)
+{
+ nsresult rv;
+
+ // First, we try to init the argument as an absolute file path. If this doesn't
+ // work, it is an absolute or relative URI.
+
+ nsCOMPtr<nsIIOService> io = do_GetIOService();
+ NS_ENSURE_TRUE(io, NS_ERROR_OUT_OF_MEMORY);
+
+ nsCOMPtr<nsIURI> workingDirURI;
+ if (mWorkingDir) {
+ io->NewFileURI(mWorkingDir, getter_AddRefs(workingDirURI));
+ }
+
+ nsCOMPtr<nsIFile> lf (do_CreateInstance(NS_LOCAL_FILE_CONTRACTID));
+ rv = lf->InitWithPath(aArgument);
+ if (NS_SUCCEEDED(rv)) {
+ lf->Normalize();
+ nsAutoCString url;
+ // Try to resolve the url for .url files.
+ rv = resolveShortcutURL(lf, url);
+ if (NS_SUCCEEDED(rv) && !url.IsEmpty()) {
+ return io->NewURI(url,
+ nullptr,
+ workingDirURI,
+ aResult);
+ }
+
+ return io->NewFileURI(lf, aResult);
+ }
+
+ return io->NewURI(NS_ConvertUTF16toUTF8(aArgument),
+ nullptr,
+ workingDirURI,
+ aResult);
+}
+
+void
+nsCommandLine::appendArg(const char* arg)
+{
+#ifdef DEBUG_COMMANDLINE
+ printf("Adding XP arg: %s\n", arg);
+#endif
+
+ nsAutoString warg;
+#ifdef XP_WIN
+ CopyUTF8toUTF16(nsDependentCString(arg), warg);
+#else
+ NS_CopyNativeToUnicode(nsDependentCString(arg), warg);
+#endif
+
+ mArgs.AppendElement(warg);
+}
+
+nsresult
+nsCommandLine::resolveShortcutURL(nsIFile* aFile, nsACString& outURL)
+{
+ nsCOMPtr<nsIFileProtocolHandler> fph;
+ nsresult rv = NS_GetFileProtocolHandler(getter_AddRefs(fph));
+ if (NS_FAILED(rv))
+ return rv;
+
+ nsCOMPtr<nsIURI> uri;
+ rv = fph->ReadURLFile(aFile, getter_AddRefs(uri));
+ if (NS_FAILED(rv))
+ return rv;
+
+ return uri->GetSpec(outURL);
+}
+
+NS_IMETHODIMP
+nsCommandLine::Init(int32_t argc, const char* const* argv, nsIFile* aWorkingDir,
+ uint32_t aState)
+{
+ NS_ENSURE_ARG_MAX(aState, 2);
+
+ int32_t i;
+
+ mWorkingDir = aWorkingDir;
+
+ // skip argv[0], we don't want it
+ for (i = 1; i < argc; ++i) {
+ const char* curarg = argv[i];
+
+#ifdef DEBUG_COMMANDLINE
+ printf("Testing native arg %i: '%s'\n", i, curarg);
+#endif
+#if defined(XP_WIN)
+ if (*curarg == '/') {
+ char* dup = PL_strdup(curarg);
+ if (!dup) return NS_ERROR_OUT_OF_MEMORY;
+
+ *dup = '-';
+ char* colon = PL_strchr(dup, ':');
+ if (colon) {
+ *colon = '\0';
+ appendArg(dup);
+ appendArg(colon+1);
+ } else {
+ appendArg(dup);
+ }
+ PL_strfree(dup);
+ continue;
+ }
+#endif
+#ifdef XP_UNIX
+ if (*curarg == '-' &&
+ *(curarg+1) == '-') {
+ ++curarg;
+
+ char* dup = PL_strdup(curarg);
+ if (!dup) return NS_ERROR_OUT_OF_MEMORY;
+
+ char* eq = PL_strchr(dup, '=');
+ if (eq) {
+ *eq = '\0';
+ appendArg(dup);
+ appendArg(eq + 1);
+ } else {
+ appendArg(dup);
+ }
+ PL_strfree(dup);
+ continue;
+ }
+#endif
+
+ appendArg(curarg);
+ }
+
+ mState = aState;
+
+ return NS_OK;
+}
+
+static void
+LogConsoleMessage(const char16_t* fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ char16_t* msg = nsTextFormatter::vsmprintf(fmt, args);
+ va_end(args);
+
+ nsCOMPtr<nsIConsoleService> cs = do_GetService("@mozilla.org/consoleservice;1");
+ if (cs)
+ cs->LogStringMessage(msg);
+
+ free(msg);
+}
+
+nsresult
+nsCommandLine::EnumerateHandlers(EnumerateHandlersCallback aCallback, void *aClosure)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsICategoryManager> catman
+ (do_GetService(NS_CATEGORYMANAGER_CONTRACTID));
+ NS_ENSURE_TRUE(catman, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsISimpleEnumerator> entenum;
+ rv = catman->EnumerateCategory("command-line-handler",
+ getter_AddRefs(entenum));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIUTF8StringEnumerator> strenum (do_QueryInterface(entenum));
+ NS_ENSURE_TRUE(strenum, NS_ERROR_UNEXPECTED);
+
+ nsAutoCString entry;
+ bool hasMore;
+ while (NS_SUCCEEDED(strenum->HasMore(&hasMore)) && hasMore) {
+ strenum->GetNext(entry);
+
+ nsCString contractID;
+ rv = catman->GetCategoryEntry("command-line-handler",
+ entry.get(),
+ getter_Copies(contractID));
+ if (NS_FAILED(rv))
+ continue;
+
+ nsCOMPtr<nsICommandLineHandler> clh(do_GetService(contractID.get()));
+ if (!clh) {
+ LogConsoleMessage(u"Contract ID '%s' was registered as a command line handler for entry '%s', but could not be created.",
+ contractID.get(), entry.get());
+ continue;
+ }
+
+ rv = (aCallback)(clh, this, aClosure);
+ if (rv == NS_ERROR_ABORT)
+ break;
+
+ rv = NS_OK;
+ }
+
+ return rv;
+}
+
+nsresult
+nsCommandLine::EnumerateValidators(EnumerateValidatorsCallback aCallback, void *aClosure)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsICategoryManager> catman
+ (do_GetService(NS_CATEGORYMANAGER_CONTRACTID));
+ NS_ENSURE_TRUE(catman, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsISimpleEnumerator> entenum;
+ rv = catman->EnumerateCategory("command-line-validator",
+ getter_AddRefs(entenum));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIUTF8StringEnumerator> strenum (do_QueryInterface(entenum));
+ NS_ENSURE_TRUE(strenum, NS_ERROR_UNEXPECTED);
+
+ nsAutoCString entry;
+ bool hasMore;
+ while (NS_SUCCEEDED(strenum->HasMore(&hasMore)) && hasMore) {
+ strenum->GetNext(entry);
+
+ nsXPIDLCString contractID;
+ rv = catman->GetCategoryEntry("command-line-validator",
+ entry.get(),
+ getter_Copies(contractID));
+ if (!contractID)
+ continue;
+
+ nsCOMPtr<nsICommandLineValidator> clv(do_GetService(contractID.get()));
+ if (!clv)
+ continue;
+
+ rv = (aCallback)(clv, this, aClosure);
+ if (rv == NS_ERROR_ABORT)
+ break;
+
+ rv = NS_OK;
+ }
+
+ return rv;
+}
+
+static nsresult
+EnumValidate(nsICommandLineValidator* aValidator, nsICommandLine* aThis, void*)
+{
+ return aValidator->Validate(aThis);
+}
+
+static nsresult
+EnumRun(nsICommandLineHandler* aHandler, nsICommandLine* aThis, void*)
+{
+ return aHandler->Handle(aThis);
+}
+
+NS_IMETHODIMP
+nsCommandLine::Run()
+{
+ nsresult rv;
+
+ rv = EnumerateValidators(EnumValidate, nullptr);
+ if (rv == NS_ERROR_ABORT)
+ return rv;
+
+ rv = EnumerateHandlers(EnumRun, nullptr);
+ if (rv == NS_ERROR_ABORT)
+ return rv;
+
+ return NS_OK;
+}
+
+static nsresult
+EnumHelp(nsICommandLineHandler* aHandler, nsICommandLine* aThis, void* aClosure)
+{
+ nsresult rv;
+
+ nsCString text;
+ rv = aHandler->GetHelpInfo(text);
+ if (NS_SUCCEEDED(rv)) {
+ NS_ASSERTION(text.Length() == 0 || text.Last() == '\n',
+ "Help text from command line handlers should end in a newline.");
+
+ nsACString* totalText = reinterpret_cast<nsACString*>(aClosure);
+ totalText->Append(text);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCommandLine::GetHelpText(nsACString& aResult)
+{
+ EnumerateHandlers(EnumHelp, &aResult);
+
+ return NS_OK;
+}
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsCommandLine)
+
+NS_DEFINE_NAMED_CID(NS_COMMANDLINE_CID);
+
+static const mozilla::Module::CIDEntry kCommandLineCIDs[] = {
+ { &kNS_COMMANDLINE_CID, false, nullptr, nsCommandLineConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kCommandLineContracts[] = {
+ { "@mozilla.org/toolkit/command-line;1", &kNS_COMMANDLINE_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kCommandLineModule = {
+ mozilla::Module::kVersion,
+ kCommandLineCIDs,
+ kCommandLineContracts
+};
+
+NSMODULE_DEFN(CommandLineModule) = &kCommandLineModule;
diff --git a/toolkit/components/commandlines/nsICommandLine.idl b/toolkit/components/commandlines/nsICommandLine.idl
new file mode 100644
index 0000000000..e44b3de9a3
--- /dev/null
+++ b/toolkit/components/commandlines/nsICommandLine.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIURI;
+interface nsIDOMWindow;
+
+/**
+ * Represents the command line used to invoke a XUL application. This may be the
+ * original command-line of this instance, or a command line remoted from another
+ * instance of the application.
+ *
+ * DEFINITIONS:
+ * "arguments" are any values found on the command line.
+ * "flags" are switches. In normalized form they are preceded by a single dash.
+ * Some flags may take "parameters", e.g. "--url <param>".
+ */
+
+[scriptable, uuid(bc3173bd-aa46-46a0-9d25-d9867a9659b6)]
+interface nsICommandLine : nsISupports
+{
+ /**
+ * Number of arguments in the command line. The application name is not
+ * part of the command line.
+ */
+ readonly attribute long length;
+
+ /**
+ * Get an argument from the array of command-line arguments.
+ *
+ * On windows, flags of the form /flag are normalized to -flag. /flag:param
+ * are normalized to -flag param.
+ *
+ * On *nix and mac flags of the form --flag are normalized to -flag. --flag=param
+ * are normalized to the form -flag param.
+ *
+ * @param aIndex The argument to retrieve. This index is 0-based, and does
+ * not include the application name.
+ * @return The indexth argument.
+ * @throws NS_ERROR_INVALID_ARG if aIndex is out of bounds.
+ */
+ AString getArgument(in long aIndex);
+
+ /**
+ * Find a command-line flag.
+ *
+ * @param aFlag The flag name to locate. Do not include the initial
+ * hyphen.
+ * @param aCaseSensitive Whether to do case-sensitive comparisons.
+ * @return The position of the flag in the command line.
+ */
+ long findFlag(in AString aFlag, in boolean aCaseSensitive);
+
+ /**
+ * Remove arguments from the command line. This normally occurs after
+ * a handler has processed the arguments.
+ *
+ * @param aStart Index to begin removing.
+ * @param aEnd Index to end removing, inclusive.
+ */
+ void removeArguments(in long aStart, in long aEnd);
+
+ /**
+ * A helper method which will find a flag and remove it in one step.
+ *
+ * @param aFlag The flag name to find and remove.
+ * @param aCaseSensitive Whether to do case-sensitive comparisons.
+ * @return Whether the flag was found.
+ */
+ boolean handleFlag(in AString aFlag, in boolean aCaseSensitive);
+
+ /**
+ * Find a flag with a parameter and remove both. This is a helper
+ * method that combines "findFlag" and "removeArguments" in one step.
+ *
+ * @return null (a void astring) if the flag is not found. The parameter value
+ * if found. Note that null and the empty string are not the same.
+ * @throws NS_ERROR_INVALID_ARG if the flag exists without a parameter
+ *
+ * @param aFlag The flag name to find and remove.
+ * @param aCaseSensitive Whether to do case-sensitive flag search.
+ */
+ AString handleFlagWithParam(in AString aFlag, in boolean aCaseSensitive);
+
+ /**
+ * The type of command line being processed.
+ *
+ * STATE_INITIAL_LAUNCH is the first launch of the application instance.
+ * STATE_REMOTE_AUTO is a remote command line automatically redirected to
+ * this instance.
+ * STATE_REMOTE_EXPLICIT is a remote command line explicitly redirected to
+ * this instance using xremote/windde/appleevents.
+ */
+ readonly attribute unsigned long state;
+
+ const unsigned long STATE_INITIAL_LAUNCH = 0;
+ const unsigned long STATE_REMOTE_AUTO = 1;
+ const unsigned long STATE_REMOTE_EXPLICIT = 2;
+
+ /**
+ * There may be a command-line handler which performs a default action if
+ * there was no explicit action on the command line (open a default browser
+ * window, for example). This flag allows the default action to be prevented.
+ */
+ attribute boolean preventDefault;
+
+ /**
+ * The working directory for this command line. Use this property instead
+ * of the working directory for the current process, since a redirected
+ * command line may have had a different working directory.
+ */
+ readonly attribute nsIFile workingDirectory;
+
+ /**
+ * A window to be targeted by this command line. In most cases, this will
+ * be null (xremote will sometimes set this attribute).
+ */
+ readonly attribute nsIDOMWindow windowContext;
+
+ /**
+ * Resolve a file-path argument into an nsIFile. This method gracefully
+ * handles relative or absolute file paths, according to the working
+ * directory of this command line.
+ *
+ * @param aArgument The command-line argument to resolve.
+ */
+ nsIFile resolveFile(in AString aArgument);
+
+ /**
+ * Resolves a URI argument into a URI. This method has platform-specific
+ * logic for converting an absolute URI or a relative file-path into the
+ * appropriate URI object; it gracefully handles win32 C:\ paths which would
+ * confuse the ioservice if passed directly.
+ *
+ * @param aArgument The command-line argument to resolve.
+ */
+ nsIURI resolveURI(in AString aArgument);
+};
diff --git a/toolkit/components/commandlines/nsICommandLineHandler.idl b/toolkit/components/commandlines/nsICommandLineHandler.idl
new file mode 100644
index 0000000000..cd042d6a53
--- /dev/null
+++ b/toolkit/components/commandlines/nsICommandLineHandler.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsICommandLine;
+
+/**
+ * Handles arguments on the command line of an XUL application.
+ *
+ * Each handler is registered in the category "command-line-handler".
+ * The entries in this category are read in alphabetical order, and each
+ * category value is treated as a service contractid implementing this
+ * interface.
+ *
+ * By convention, handler with ordinary priority should begin with "m".
+ *
+ * Example:
+ * Category Entry Value
+ * command-line-handler c-extensions @mozilla.org/extension-manager/clh;1
+ * command-line-handler m-edit @mozilla.org/composer/clh;1
+ * command-line-handler m-irc @mozilla.org/chatzilla/clh;1
+ * command-line-handler y-final @mozilla.org/browser/clh-final;1
+ *
+ * @note What do we do about localizing helpInfo? Do we make each handler do it,
+ * or provide a generic solution of some sort? Don't freeze this interface
+ * without thinking about this!
+ */
+
+[scriptable, uuid(d4b123df-51ee-48b1-a663-002180e60d3b)]
+interface nsICommandLineHandler : nsISupports
+{
+ /**
+ * Process a command line. If this handler finds arguments that it
+ * understands, it should perform the appropriate actions (such as opening
+ * a window), and remove the arguments from the command-line array.
+ *
+ * @throw NS_ERROR_ABORT to immediately cease command-line handling
+ * (if this is STATE_INITIAL_LAUNCH, quits the app).
+ * All other exceptions are silently ignored.
+ */
+ void handle(in nsICommandLine aCommandLine);
+
+ /**
+ * When the app is launched with the --help argument, this attribute
+ * is retrieved and displayed to the user (on stdout). The text should
+ * have embedded newlines which wrap at 76 columns, and should include
+ * a newline at the end. By convention, the right column which contains flag
+ * descriptions begins at the 24th character.
+ */
+ readonly attribute AUTF8String helpInfo;
+};
diff --git a/toolkit/components/commandlines/nsICommandLineRunner.idl b/toolkit/components/commandlines/nsICommandLineRunner.idl
new file mode 100644
index 0000000000..0cd3f0c90c
--- /dev/null
+++ b/toolkit/components/commandlines/nsICommandLineRunner.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+#include "nsICommandLine.idl"
+
+[ptr] native nsArgvArray(const char* const);
+
+/**
+ * Extension of nsICommandLine that allows for initialization of new command lines
+ * and running the command line actions by processing the command line handlers.
+ *
+ * @status INTERNAL - This interface is not meant for use by embedders, and is
+ * not intended to be frozen. If you are an embedder and need
+ * functionality provided by this interface, talk to Benjamin
+ * Smedberg <benjamin@smedbergs.us>.
+ */
+
+[uuid(c9f2996c-b25a-4d3d-821f-4cd0c4bc8afb)]
+interface nsICommandLineRunner : nsICommandLine
+{
+ /**
+ * This method assumes a native character set, and is meant to be called
+ * with the argc/argv passed to main(). Talk to bsmedberg if you need to
+ * create a command line using other data. argv will not be altered in any
+ * way.
+ *
+ * On Windows, the "native" character set is UTF-8, not the native codepage.
+ *
+ * @param workingDir The working directory for resolving file and URI paths.
+ * @param state The nsICommandLine.state flag.
+ */
+ void init(in long argc, in nsArgvArray argv,
+ in nsIFile workingDir, in unsigned long state);
+
+ /**
+ * Set the windowContext parameter.
+ */
+ void setWindowContext(in nsIDOMWindow aWindow);
+
+ /**
+ * Process the command-line handlers in the proper order, calling "handle()" on
+ * each.
+ *
+ * @throws NS_ERROR_ABORT if any handler throws NS_ERROR_ABORT. All other errors
+ * thrown by handlers will be silently ignored.
+ */
+ void run();
+
+ /**
+ * Process and combine the help text provided by each command-line handler.
+ */
+ readonly attribute AUTF8String helpText;
+};
diff --git a/toolkit/components/commandlines/nsICommandLineValidator.idl b/toolkit/components/commandlines/nsICommandLineValidator.idl
new file mode 100644
index 0000000000..eba949bf30
--- /dev/null
+++ b/toolkit/components/commandlines/nsICommandLineValidator.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsICommandLine;
+
+/**
+ * Validates arguments on the command line of an XUL application.
+ *
+ * Each validator is registered in the category "command-line-validator".
+ * The entries in this category are read in alphabetical order, and each
+ * category value is treated as a service contractid implementing this
+ * interface.
+ *
+ * By convention, validator with ordinary priority should begin with "m".
+ *
+ * Example:
+ * Category Entry Value
+ * command-line-validator b-browser @mozilla.org/browser/clh;1
+ * command-line-validator m-edit @mozilla.org/composer/clh;1
+ * command-line-validator m-irc @mozilla.org/chatzilla/clh;1
+ *
+ */
+
+[scriptable, uuid(5ecaa593-7660-4a3a-957a-92d5770671c7)]
+interface nsICommandLineValidator : nsISupports
+{
+ /**
+ * Process the command-line validators in the proper order, calling
+ * "validate()" on each.
+ *
+ * @throws NS_ERROR_ABORT if any validator throws NS_ERROR_ABORT. All other
+ * errors thrown by validators will be silently ignored.
+ */
+ void validate(in nsICommandLine aCommandLine);
+};
diff --git a/toolkit/components/commandlines/test/unit/.eslintrc.js b/toolkit/components/commandlines/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/commandlines/test/unit/data/test_bug410156.desktop b/toolkit/components/commandlines/test/unit/data/test_bug410156.desktop
new file mode 100644
index 0000000000..1847cdd98e
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit/data/test_bug410156.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Version=1.0
+Encoding=UTF-8
+Name=test_bug410156
+Type=Link
+URL=http://www.bug410156.com/
+Icon=gnome-fs-bookmark
diff --git a/toolkit/components/commandlines/test/unit/data/test_bug410156.url b/toolkit/components/commandlines/test/unit/data/test_bug410156.url
new file mode 100644
index 0000000000..6920e1f774
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit/data/test_bug410156.url
@@ -0,0 +1,9 @@
+[InternetShortcut]
+URL=http://www.bug410156.com/
+IDList=
+HotKey=0
+[{000214A0-0000-0000-C000-000000000046}]
+Prop3=19,2
+[InternetShortcut.A]
+[InternetShortcut.W]
+URL=http://www.bug410156.com/
diff --git a/toolkit/components/commandlines/test/unit/test_bug666224.js b/toolkit/components/commandlines/test/unit/test_bug666224.js
new file mode 100644
index 0000000000..8d372097a5
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit/test_bug666224.js
@@ -0,0 +1,6 @@
+function run_test() {
+ var cmdLine=Components.classes["@mozilla.org/toolkit/command-line;1"].createInstance(Components.interfaces.nsICommandLine);
+ try {
+ cmdLine.getArgument(cmdLine.length);
+ } catch (e) {}
+}
diff --git a/toolkit/components/commandlines/test/unit/test_classinfo.js b/toolkit/components/commandlines/test/unit/test_classinfo.js
new file mode 100644
index 0000000000..a0fb1ff0a6
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit/test_classinfo.js
@@ -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/. */
+
+function run_test() {
+ var clClass = Components.classes["@mozilla.org/toolkit/command-line;1"];
+ var commandLine = clClass.createInstance();
+ do_check_true("length" in commandLine);
+}
diff --git a/toolkit/components/commandlines/test/unit/xpcshell.ini b/toolkit/components/commandlines/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..4939a3d642
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ data/test_bug410156.desktop
+ data/test_bug410156.url
+
+[test_classinfo.js]
+[test_bug666224.js]
diff --git a/toolkit/components/commandlines/test/unit_unix/.eslintrc.js b/toolkit/components/commandlines/test/unit_unix/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit_unix/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/commandlines/test/unit_unix/test_bug410156.js b/toolkit/components/commandlines/test/unit_unix/test_bug410156.js
new file mode 100644
index 0000000000..06c95ac357
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit_unix/test_bug410156.js
@@ -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/. */
+
+function run_test() {
+ var clClass = Components.classes["@mozilla.org/toolkit/command-line;1"];
+ var commandLine = clClass.createInstance();
+ var urlFile = do_get_file("../unit/data/test_bug410156.desktop");
+ var uri = commandLine.resolveURI(urlFile.path);
+ do_check_eq(uri.spec, "http://www.bug410156.com/");
+}
diff --git a/toolkit/components/commandlines/test/unit_unix/xpcshell.ini b/toolkit/components/commandlines/test/unit_unix/xpcshell.ini
new file mode 100644
index 0000000000..41f71f48d1
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit_unix/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ !/toolkit/components/commandlines/test/unit/data/test_bug410156.desktop
+ !/toolkit/components/commandlines/test/unit/data/test_bug410156.url
+
+[test_bug410156.js]
diff --git a/toolkit/components/commandlines/test/unit_win/.eslintrc.js b/toolkit/components/commandlines/test/unit_win/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit_win/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/commandlines/test/unit_win/test_bug410156.js b/toolkit/components/commandlines/test/unit_win/test_bug410156.js
new file mode 100644
index 0000000000..cc04426d6a
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit_win/test_bug410156.js
@@ -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/. */
+
+function run_test() {
+ var clClass = Components.classes["@mozilla.org/toolkit/command-line;1"];
+ var commandLine = clClass.createInstance();
+ var urlFile = do_get_file("../unit/data/test_bug410156.url");
+ var uri = commandLine.resolveURI(urlFile.path);
+ do_check_eq(uri.spec, "http://www.bug410156.com/");
+}
diff --git a/toolkit/components/commandlines/test/unit_win/xpcshell.ini b/toolkit/components/commandlines/test/unit_win/xpcshell.ini
new file mode 100644
index 0000000000..efc2cfccf6
--- /dev/null
+++ b/toolkit/components/commandlines/test/unit_win/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head =
+tail =
+support-files =
+ !/toolkit/components/commandlines/test/unit/data/test_bug410156.desktop
+ !/toolkit/components/commandlines/test/unit/data/test_bug410156.url
+
+[test_bug410156.js]
diff --git a/toolkit/components/contentprefs/ContentPrefInstance.jsm b/toolkit/components/contentprefs/ContentPrefInstance.jsm
new file mode 100644
index 0000000000..395569995f
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefInstance.jsm
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+this.EXPORTED_SYMBOLS = ['ContentPrefInstance'];
+
+// This is a wrapper for nsIContentPrefService that alleviates the need to pass
+// an nsILoadContext argument to every method. Pass the context to the constructor
+// instead and continue on your way in blissful ignorance.
+
+this.ContentPrefInstance = function ContentPrefInstance(aContext) {
+ this._contentPrefSvc = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService);
+ this._context = aContext;
+};
+
+ContentPrefInstance.prototype = {
+ getPref: function ContentPrefInstance_init(aName, aGroup, aCallback) {
+ return this._contentPrefSvc.getPref(aName, aGroup, this._context, aCallback);
+ },
+
+ setPref: function ContentPrefInstance_setPref(aGroup, aName, aValue, aContext) {
+ return this._contentPrefSvc.setPref(aGroup, aName, aValue,
+ aContext ? aContext : this._context);
+ },
+
+ hasPref: function ContentPrefInstance_hasPref(aGroup, aName) {
+ return this._contentPrefSvc.hasPref(aGroup, aName, this._context);
+ },
+
+ hasCachedPref: function ContentPrefInstance_hasCachedPref(aGroup, aName) {
+ return this._contentPrefSvc.hasCachedPref(aGroup, aName, this._context);
+ },
+
+ removePref: function ContentPrefInstance_removePref(aGroup, aName) {
+ return this._contentPrefSvc.removePref(aGroup, aName, this._context);
+ },
+
+ removeGroupedPrefs: function ContentPrefInstance_removeGroupedPrefs() {
+ return this._contentPrefSvc.removeGroupedPrefs(this._context);
+ },
+
+ removePrefsByName: function ContentPrefInstance_removePrefsByName(aName) {
+ return this._contentPrefSvc.removePrefsByName(aName, this._context);
+ },
+
+ getPrefs: function ContentPrefInstance_getPrefs(aGroup) {
+ return this._contentPrefSvc.getPrefs(aGroup, this._context);
+ },
+
+ getPrefsByName: function ContentPrefInstance_getPrefsByName(aName) {
+ return this._contentPrefSvc.getPrefsByName(aName, this._context);
+ },
+
+ addObserver: function ContentPrefInstance_addObserver(aName, aObserver) {
+ return this._contentPrefSvc.addObserver(aName, aObserver);
+ },
+
+ removeObserver: function ContentPrefInstance_removeObserver(aName, aObserver) {
+ return this._contentPrefSvc.removeObserver(aName, aObserver);
+ },
+
+ get grouper() {
+ return this._contentPrefSvc.grouper;
+ },
+
+ get DBConnection() {
+ return this._contentPrefSvc.DBConnection;
+ }
+};
diff --git a/toolkit/components/contentprefs/ContentPrefService2.jsm b/toolkit/components/contentprefs/ContentPrefService2.jsm
new file mode 100644
index 0000000000..87063d1708
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefService2.jsm
@@ -0,0 +1,885 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file is an XPCOM component that implements nsIContentPrefService2.
+// Although it's a JSM, it's not intended to be imported by consumers like JSMs
+// are usually imported. It's only a JSM so that nsContentPrefService.js can
+// easily use it. Consumers should access this component with the usual XPCOM
+// rigmarole:
+//
+// Cc["@mozilla.org/content-pref/service;1"].
+// getService(Ci.nsIContentPrefService2);
+//
+// That contract ID actually belongs to nsContentPrefService.js, which, when
+// QI'ed to nsIContentPrefService2, returns an instance of this component.
+//
+// The plan is to eventually remove nsIContentPrefService and its
+// implementation, nsContentPrefService.js. At such time this file can stop
+// being a JSM, and the "_cps" parts that ContentPrefService2 relies on and
+// NSGetFactory and all the other XPCOM initialization goop in
+// nsContentPrefService.js can be moved here.
+//
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=699859
+
+var EXPORTED_SYMBOLS = [
+ "ContentPrefService2",
+];
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+
+const GROUP_CLAUSE = `
+ SELECT id
+ FROM groups
+ WHERE name = :group OR
+ (:includeSubdomains AND name LIKE :pattern ESCAPE '/')
+`;
+
+function ContentPrefService2(cps) {
+ this._cps = cps;
+ this._cache = cps._cache;
+ this._pbStore = cps._privModeStorage;
+}
+
+ContentPrefService2.prototype = {
+
+ getByName: function CPS2_getByName(name, context, callback) {
+ checkNameArg(name);
+ checkCallbackArg(callback, true);
+
+ // Some prefs may be in both the database and the private browsing store.
+ // Notify the caller of such prefs only once, using the values from private
+ // browsing.
+ let pbPrefs = new ContentPrefStore();
+ if (context && context.usePrivateBrowsing) {
+ for (let [sgroup, sname, val] of this._pbStore) {
+ if (sname == name) {
+ pbPrefs.set(sgroup, sname, val);
+ }
+ }
+ }
+
+ let stmt1 = this._stmt(`
+ SELECT groups.name AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE settings.name = :name
+ `);
+ stmt1.params.name = name;
+
+ let stmt2 = this._stmt(`
+ SELECT NULL AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE settings.name = :name AND prefs.groupID ISNULL
+ `);
+ stmt2.params.name = name;
+
+ this._execStmts([stmt1, stmt2], {
+ onRow: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ let val = row.getResultByName("value");
+ this._cache.set(grp, name, val);
+ if (!pbPrefs.has(grp, name))
+ cbHandleResult(callback, new ContentPref(grp, name, val));
+ },
+ onDone: function onDone(reason, ok, gotRow) {
+ if (ok) {
+ for (let [pbGroup, pbName, pbVal] of pbPrefs) {
+ cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ getByDomainAndName: function CPS2_getByDomainAndName(group, name, context,
+ callback) {
+ checkGroupArg(group);
+ this._get(group, name, false, context, callback);
+ },
+
+ getBySubdomainAndName: function CPS2_getBySubdomainAndName(group, name,
+ context,
+ callback) {
+ checkGroupArg(group);
+ this._get(group, name, true, context, callback);
+ },
+
+ getGlobal: function CPS2_getGlobal(name, context, callback) {
+ this._get(null, name, false, context, callback);
+ },
+
+ _get: function CPS2__get(group, name, includeSubdomains, context, callback) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+ checkCallbackArg(callback, true);
+
+ // Some prefs may be in both the database and the private browsing store.
+ // Notify the caller of such prefs only once, using the values from private
+ // browsing.
+ let pbPrefs = new ContentPrefStore();
+ if (context && context.usePrivateBrowsing) {
+ for (let [sgroup, val] of
+ this._pbStore.match(group, name, includeSubdomains)) {
+ pbPrefs.set(sgroup, name, val);
+ }
+ }
+
+ this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
+ onRow: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ let val = row.getResultByName("value");
+ this._cache.set(grp, name, val);
+ if (!pbPrefs.has(group, name))
+ cbHandleResult(callback, new ContentPref(grp, name, val));
+ },
+ onDone: function onDone(reason, ok, gotRow) {
+ if (ok) {
+ if (!gotRow)
+ this._cache.set(group, name, undefined);
+ for (let [pbGroup, pbName, pbVal] of pbPrefs) {
+ cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
+ let stmt = group ?
+ this._stmtWithGroupClause(group, includeSubdomains, `
+ SELECT groups.name AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE})
+ `) :
+ this._stmt(`
+ SELECT NULL AS grp, prefs.value AS value
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE settings.name = :name AND prefs.groupID ISNULL
+ `);
+ stmt.params.name = name;
+ return stmt;
+ },
+
+ _stmtWithGroupClause: function CPS2__stmtWithGroupClause(group,
+ includeSubdomains,
+ sql) {
+ let stmt = this._stmt(sql);
+ stmt.params.group = group;
+ stmt.params.includeSubdomains = includeSubdomains || false;
+ stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/");
+ return stmt;
+ },
+
+ getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group,
+ name,
+ context) {
+ checkGroupArg(group);
+ let prefs = this._getCached(group, name, false, context);
+ return prefs[0] || null;
+ },
+
+ getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName(group,
+ name,
+ context,
+ len) {
+ checkGroupArg(group);
+ let prefs = this._getCached(group, name, true, context);
+ if (len)
+ len.value = prefs.length;
+ return prefs;
+ },
+
+ getCachedGlobal: function CPS2_getCachedGlobal(name, context) {
+ let prefs = this._getCached(null, name, false, context);
+ return prefs[0] || null;
+ },
+
+ _getCached: function CPS2__getCached(group, name, includeSubdomains,
+ context) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+
+ let storesToCheck = [this._cache];
+ if (context && context.usePrivateBrowsing)
+ storesToCheck.push(this._pbStore);
+
+ let outStore = new ContentPrefStore();
+ storesToCheck.forEach(function (store) {
+ for (let [sgroup, val] of store.match(group, name, includeSubdomains)) {
+ outStore.set(sgroup, name, val);
+ }
+ });
+
+ let prefs = [];
+ for (let [sgroup, sname, val] of outStore) {
+ prefs.push(new ContentPref(sgroup, sname, val));
+ }
+ return prefs;
+ },
+
+ set: function CPS2_set(group, name, value, context, callback) {
+ checkGroupArg(group);
+ this._set(group, name, value, context, callback);
+ },
+
+ setGlobal: function CPS2_setGlobal(name, value, context, callback) {
+ this._set(null, name, value, context, callback);
+ },
+
+ _set: function CPS2__set(group, name, value, context, callback) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+ checkValueArg(value);
+ checkCallbackArg(callback, false);
+
+ if (context && context.usePrivateBrowsing) {
+ this._pbStore.set(group, name, value);
+ this._schedule(function () {
+ cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK);
+ this._cps._notifyPrefSet(group, name, value, context.usePrivateBrowsing);
+ });
+ return;
+ }
+
+ // Invalidate the cached value so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ this._cache.remove(group, name);
+
+ let stmts = [];
+
+ // Create the setting if it doesn't exist.
+ let stmt = this._stmt(`
+ INSERT OR IGNORE INTO settings (id, name)
+ VALUES((SELECT id FROM settings WHERE name = :name), :name)
+ `);
+ stmt.params.name = name;
+ stmts.push(stmt);
+
+ // Create the group if it doesn't exist.
+ if (group) {
+ stmt = this._stmt(`
+ INSERT OR IGNORE INTO groups (id, name)
+ VALUES((SELECT id FROM groups WHERE name = :group), :group)
+ `);
+ stmt.params.group = group;
+ stmts.push(stmt);
+ }
+
+ // Finally create or update the pref.
+ if (group) {
+ stmt = this._stmt(`
+ INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
+ VALUES(
+ (SELECT prefs.id
+ FROM prefs
+ JOIN groups ON groups.id = prefs.groupID
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE groups.name = :group AND settings.name = :name),
+ (SELECT id FROM groups WHERE name = :group),
+ (SELECT id FROM settings WHERE name = :name),
+ :value,
+ :now
+ )
+ `);
+ stmt.params.group = group;
+ }
+ else {
+ stmt = this._stmt(`
+ INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp)
+ VALUES(
+ (SELECT prefs.id
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE prefs.groupID IS NULL AND settings.name = :name),
+ NULL,
+ (SELECT id FROM settings WHERE name = :name),
+ :value,
+ :now
+ )
+ `);
+ }
+ stmt.params.name = name;
+ stmt.params.value = value;
+ stmt.params.now = Date.now() / 1000;
+ stmts.push(stmt);
+
+ this._execStmts(stmts, {
+ onDone: function onDone(reason, ok) {
+ if (ok)
+ this._cache.setWithCast(group, name, value);
+ cbHandleCompletion(callback, reason);
+ if (ok)
+ this._cps._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing);
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ removeByDomainAndName: function CPS2_removeByDomainAndName(group, name,
+ context,
+ callback) {
+ checkGroupArg(group);
+ this._remove(group, name, false, context, callback);
+ },
+
+ removeBySubdomainAndName: function CPS2_removeBySubdomainAndName(group, name,
+ context,
+ callback) {
+ checkGroupArg(group);
+ this._remove(group, name, true, context, callback);
+ },
+
+ removeGlobal: function CPS2_removeGlobal(name, context, callback) {
+ this._remove(null, name, false, context, callback);
+ },
+
+ _remove: function CPS2__remove(group, name, includeSubdomains, context,
+ callback) {
+ group = this._parseGroup(group);
+ checkNameArg(name);
+ checkCallbackArg(callback, false);
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
+ this._cache.remove(sgroup, name);
+ }
+
+ let stmts = [];
+
+ // First get the matching prefs.
+ stmts.push(this._commonGetStmt(group, name, includeSubdomains));
+
+ // Delete the matching prefs.
+ let stmt = this._stmtWithGroupClause(group, includeSubdomains, `
+ DELETE FROM prefs
+ WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND
+ CASE typeof(:group)
+ WHEN 'null' THEN prefs.groupID IS NULL
+ ELSE prefs.groupID IN (${GROUP_CLAUSE})
+ END
+ `);
+ stmt.params.name = name;
+ stmts.push(stmt);
+
+ stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
+
+ let prefs = new ContentPrefStore();
+
+ let isPrivate = context && context.usePrivateBrowsing;
+ this._execStmts(stmts, {
+ onRow: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function onDone(reason, ok) {
+ if (ok) {
+ this._cache.set(group, name, undefined);
+ if (isPrivate) {
+ for (let [sgroup, ] of
+ this._pbStore.match(group, name, includeSubdomains)) {
+ prefs.set(sgroup, name, undefined);
+ this._pbStore.remove(sgroup, name);
+ }
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, , ] of prefs) {
+ this._cps._notifyPrefRemoved(sgroup, name, isPrivate);
+ }
+ }
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ // Deletes settings and groups that are no longer used.
+ _settingsAndGroupsCleanupStmts: function() {
+ // The NOTNULL term in the subquery of the second statment is needed because of
+ // SQLite's weird IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html.
+ return [
+ this._stmt(`
+ DELETE FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `),
+ this._stmt(`
+ DELETE FROM groups WHERE id NOT IN (
+ SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
+ )
+ `)
+ ];
+ },
+
+ removeByDomain: function CPS2_removeByDomain(group, context, callback) {
+ checkGroupArg(group);
+ this._removeByDomain(group, false, context, callback);
+ },
+
+ removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) {
+ checkGroupArg(group);
+ this._removeByDomain(group, true, context, callback);
+ },
+
+ removeAllGlobals: function CPS2_removeAllGlobals(context, callback) {
+ this._removeByDomain(null, false, context, callback);
+ },
+
+ _removeByDomain: function CPS2__removeByDomain(group, includeSubdomains,
+ context, callback) {
+ group = this._parseGroup(group);
+ checkCallbackArg(callback, false);
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) {
+ this._cache.removeGroup(sgroup);
+ }
+
+ let stmts = [];
+
+ // First get the matching prefs, then delete groups and prefs that reference
+ // deleted groups.
+ if (group) {
+ stmts.push(this._stmtWithGroupClause(group, includeSubdomains, `
+ SELECT groups.name AS grp, settings.name AS name
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE prefs.groupID IN (${GROUP_CLAUSE})
+ `));
+ stmts.push(this._stmtWithGroupClause(group, includeSubdomains,
+ `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})`
+ ));
+ stmts.push(this._stmt(`
+ DELETE FROM prefs
+ WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups)
+ `));
+ }
+ else {
+ stmts.push(this._stmt(`
+ SELECT NULL AS grp, settings.name AS name
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE prefs.groupID IS NULL
+ `));
+ stmts.push(this._stmt(
+ "DELETE FROM prefs WHERE groupID IS NULL"
+ ));
+ }
+
+ // Finally delete settings that are no longer referenced.
+ stmts.push(this._stmt(`
+ DELETE FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `));
+
+ let prefs = new ContentPrefStore();
+
+ let isPrivate = context && context.usePrivateBrowsing;
+ this._execStmts(stmts, {
+ onRow: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ let name = row.getResultByName("name");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function onDone(reason, ok) {
+ if (ok && isPrivate) {
+ for (let [sgroup, sname, ] of this._pbStore) {
+ if (!group ||
+ (!includeSubdomains && group == sgroup) ||
+ (includeSubdomains && sgroup && this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))) {
+ prefs.set(sgroup, sname, undefined);
+ this._pbStore.remove(sgroup, sname);
+ }
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, sname, ] of prefs) {
+ this._cps._notifyPrefRemoved(sgroup, sname, isPrivate);
+ }
+ }
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ _removeAllDomainsSince: function CPS2__removeAllDomainsSince(since, context, callback) {
+ checkCallbackArg(callback, false);
+
+ since /= 1000;
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ // Invalidate all the group cache because we don't know which groups will be removed.
+ this._cache.removeAllGroups();
+
+ let stmts = [];
+
+ // Get prefs that are about to be removed to notify about their removal.
+ let stmt = this._stmt(`
+ SELECT groups.name AS grp, settings.name AS name
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE timestamp >= :since
+ `);
+ stmt.params.since = since;
+ stmts.push(stmt);
+
+ // Do the actual remove.
+ stmt = this._stmt(`
+ DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since
+ `);
+ stmt.params.since = since;
+ stmts.push(stmt);
+
+ // Cleanup no longer used values.
+ stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
+
+ let prefs = new ContentPrefStore();
+ let isPrivate = context && context.usePrivateBrowsing;
+ this._execStmts(stmts, {
+ onRow: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ let name = row.getResultByName("name");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function onDone(reason, ok) {
+ // This nukes all the groups in _pbStore since we don't have their timestamp
+ // information.
+ if (ok && isPrivate) {
+ for (let [sgroup, sname, ] of this._pbStore) {
+ if (sgroup) {
+ prefs.set(sgroup, sname, undefined);
+ }
+ }
+ this._pbStore.removeAllGroups();
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, sname, ] of prefs) {
+ this._cps._notifyPrefRemoved(sgroup, sname, isPrivate);
+ }
+ }
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ removeAllDomainsSince: function CPS2_removeAllDomainsSince(since, context, callback) {
+ this._removeAllDomainsSince(since, context, callback);
+ },
+
+ removeAllDomains: function CPS2_removeAllDomains(context, callback) {
+ this._removeAllDomainsSince(0, context, callback);
+ },
+
+ removeByName: function CPS2_removeByName(name, context, callback) {
+ checkNameArg(name);
+ checkCallbackArg(callback, false);
+
+ // Invalidate the cached values so consumers accessing the cache between now
+ // and when the operation finishes don't get old data.
+ for (let [group, sname, ] of this._cache) {
+ if (sname == name)
+ this._cache.remove(group, name);
+ }
+
+ let stmts = [];
+
+ // First get the matching prefs. Include null if any of those prefs are
+ // global.
+ let stmt = this._stmt(`
+ SELECT groups.name AS grp
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ JOIN groups ON groups.id = prefs.groupID
+ WHERE settings.name = :name
+ UNION
+ SELECT NULL AS grp
+ WHERE EXISTS (
+ SELECT prefs.id
+ FROM prefs
+ JOIN settings ON settings.id = prefs.settingID
+ WHERE settings.name = :name AND prefs.groupID IS NULL
+ )
+ `);
+ stmt.params.name = name;
+ stmts.push(stmt);
+
+ // Delete the target settings.
+ stmt = this._stmt(
+ "DELETE FROM settings WHERE name = :name"
+ );
+ stmt.params.name = name;
+ stmts.push(stmt);
+
+ // Delete prefs and groups that are no longer used.
+ stmts.push(this._stmt(
+ "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)"
+ ));
+ stmts.push(this._stmt(`
+ DELETE FROM groups WHERE id NOT IN (
+ SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
+ )
+ `));
+
+ let prefs = new ContentPrefStore();
+ let isPrivate = context && context.usePrivateBrowsing;
+
+ this._execStmts(stmts, {
+ onRow: function onRow(row) {
+ let grp = row.getResultByName("grp");
+ prefs.set(grp, name, undefined);
+ this._cache.set(grp, name, undefined);
+ },
+ onDone: function onDone(reason, ok) {
+ if (ok && isPrivate) {
+ for (let [sgroup, sname, ] of this._pbStore) {
+ if (sname === name) {
+ prefs.set(sgroup, name, undefined);
+ this._pbStore.remove(sgroup, name);
+ }
+ }
+ }
+ cbHandleCompletion(callback, reason);
+ if (ok) {
+ for (let [sgroup, , ] of prefs) {
+ this._cps._notifyPrefRemoved(sgroup, name, isPrivate);
+ }
+ }
+ },
+ onError: function onError(nsresult) {
+ cbHandleError(callback, nsresult);
+ }
+ });
+ },
+
+ destroy: function CPS2_destroy() {
+ if (this._statements) {
+ for (let sql in this._statements) {
+ let stmt = this._statements[sql];
+ stmt.finalize();
+ }
+ }
+ },
+
+ /**
+ * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such
+ * statement is cached, one is created and cached.
+ *
+ * @param sql The SQL query string.
+ * @return The cached, possibly new, statement.
+ */
+ _stmt: function CPS2__stmt(sql) {
+ if (!this._statements)
+ this._statements = {};
+ if (!this._statements[sql])
+ this._statements[sql] = this._cps._dbConnection.createAsyncStatement(sql);
+ return this._statements[sql];
+ },
+
+ /**
+ * Executes some async statements.
+ *
+ * @param stmts An array of mozIStorageAsyncStatements.
+ * @param callbacks An object with the following methods:
+ * onRow(row) (optional)
+ * Called once for each result row.
+ * row: A mozIStorageRow.
+ * onDone(reason, reasonOK, didGetRow) (required)
+ * Called when done.
+ * reason: A nsIContentPrefService2.COMPLETE_* value.
+ * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
+ * didGetRow: True if onRow was ever called.
+ * onError(nsresult) (optional)
+ * Called on error.
+ * nsresult: The error code.
+ */
+ _execStmts: function CPS2__execStmts(stmts, callbacks) {
+ let self = this;
+ let gotRow = false;
+ this._cps._dbConnection.executeAsync(stmts, stmts.length, {
+ handleResult: function handleResult(results) {
+ try {
+ let row = null;
+ while ((row = results.getNextRow())) {
+ gotRow = true;
+ if (callbacks.onRow)
+ callbacks.onRow.call(self, row);
+ }
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ },
+ handleCompletion: function handleCompletion(reason) {
+ try {
+ let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED;
+ callbacks.onDone.call(self,
+ ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
+ Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
+ ok, gotRow);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ },
+ handleError: function handleError(error) {
+ try {
+ if (callbacks.onError)
+ callbacks.onError.call(self, Cr.NS_ERROR_FAILURE);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ }
+ });
+ },
+
+ /**
+ * Parses the domain (the "group", to use the database's term) from the given
+ * string.
+ *
+ * @param groupStr Assumed to be either a string or falsey.
+ * @return If groupStr is a valid URL string, returns the domain of
+ * that URL. If groupStr is some other nonempty string,
+ * returns groupStr itself. Otherwise returns null.
+ */
+ _parseGroup: function CPS2__parseGroup(groupStr) {
+ if (!groupStr)
+ return null;
+ try {
+ var groupURI = Services.io.newURI(groupStr, null, null);
+ }
+ catch (err) {
+ return groupStr;
+ }
+ return this._cps._grouper.group(groupURI);
+ },
+
+ _schedule: function CPS2__schedule(fn) {
+ Services.tm.mainThread.dispatch(fn.bind(this),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ addObserverForName: function CPS2_addObserverForName(name, observer) {
+ this._cps._addObserver(name, observer);
+ },
+
+ removeObserverForName: function CPS2_removeObserverForName(name, observer) {
+ this._cps._removeObserver(name, observer);
+ },
+
+ extractDomain: function CPS2_extractDomain(str) {
+ return this._parseGroup(str);
+ },
+
+ /**
+ * Tests use this as a backchannel by calling it directly.
+ *
+ * @param subj This value depends on topic.
+ * @param topic The backchannel "method" name.
+ * @param data This value depends on topic.
+ */
+ observe: function CPS2_observe(subj, topic, data) {
+ switch (topic) {
+ case "test:reset":
+ let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
+ this._reset(fn);
+ break;
+ case "test:db":
+ let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
+ obj.value = this._cps._dbConnection;
+ break;
+ }
+ },
+
+ /**
+ * Removes all state from the service. Used by tests.
+ *
+ * @param callback A function that will be called when done.
+ */
+ _reset: function CPS2__reset(callback) {
+ this._pbStore.removeAll();
+ this._cache.removeAll();
+
+ let cps = this._cps;
+ cps._observers = {};
+ cps._genericObservers = [];
+
+ let tables = ["prefs", "groups", "settings"];
+ let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
+ this._execStmts(stmts, { onDone: () => callback() });
+ },
+
+ QueryInterface: function CPS2_QueryInterface(iid) {
+ let supportedIIDs = [
+ Ci.nsIContentPrefService2,
+ Ci.nsIObserver,
+ Ci.nsISupports,
+ ];
+ if (supportedIIDs.some(i => iid.equals(i)))
+ return this;
+ if (iid.equals(Ci.nsIContentPrefService))
+ return this._cps;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+};
+
+function checkGroupArg(group) {
+ if (!group || typeof(group) != "string")
+ throw invalidArg("domain must be nonempty string.");
+}
+
+function checkNameArg(name) {
+ if (!name || typeof(name) != "string")
+ throw invalidArg("name must be nonempty string.");
+}
+
+function checkValueArg(value) {
+ if (value === undefined)
+ throw invalidArg("value must not be undefined.");
+}
+
+function checkCallbackArg(callback, required) {
+ if (callback && !(callback instanceof Ci.nsIContentPrefCallback2))
+ throw invalidArg("callback must be an nsIContentPrefCallback2.");
+ if (!callback && required)
+ throw invalidArg("callback must be given.");
+}
+
+function invalidArg(msg) {
+ return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG);
+}
diff --git a/toolkit/components/contentprefs/ContentPrefServiceChild.jsm b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm
new file mode 100644
index 0000000000..7faca89709
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceChild.jsm
@@ -0,0 +1,182 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [ "ContentPrefServiceChild" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+
+// We only need one bit of information out of the context.
+function contextArg(context) {
+ return (context && context.usePrivateBrowsing) ?
+ { usePrivateBrowsing: true } :
+ null;
+}
+
+function NYI() {
+ throw new Error("Do not add any new users of these functions");
+}
+
+function CallbackCaller(callback) {
+ this._callback = callback;
+}
+
+CallbackCaller.prototype = {
+ handleResult: function(contentPref) {
+ cbHandleResult(this._callback,
+ new ContentPref(contentPref.domain,
+ contentPref.name,
+ contentPref.value));
+ },
+
+ handleError: function(result) {
+ cbHandleError(this._callback, result);
+ },
+
+ handleCompletion: function(reason) {
+ cbHandleCompletion(this._callback, reason);
+ },
+};
+
+var ContentPrefServiceChild = {
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIContentPrefService2 ]),
+
+ // Map from pref name -> set of observers
+ _observers: new Map(),
+
+ _mm: Cc["@mozilla.org/childprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageSender),
+
+ _getRandomId: function() {
+ return Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ },
+
+ // Map from random ID string -> CallbackCaller, per request
+ _requests: new Map(),
+
+ init: function() {
+ this._mm.addMessageListener("ContentPrefs:HandleResult", this);
+ this._mm.addMessageListener("ContentPrefs:HandleError", this);
+ this._mm.addMessageListener("ContentPrefs:HandleCompletion", this);
+ },
+
+ receiveMessage: function(msg) {
+ let data = msg.data;
+ let callback;
+ switch (msg.name) {
+ case "ContentPrefs:HandleResult":
+ callback = this._requests.get(data.requestId);
+ callback.handleResult(data.contentPref);
+ break;
+
+ case "ContentPrefs:HandleError":
+ callback = this._requests.get(data.requestId);
+ callback.handleError(data.error);
+ break;
+
+ case "ContentPrefs:HandleCompletion":
+ callback = this._requests.get(data.requestId);
+ this._requests.delete(data.requestId);
+ callback.handleCompletion(data.reason);
+ break;
+
+ case "ContentPrefs:NotifyObservers": {
+ let observerList = this._observers.get(data.name);
+ if (!observerList)
+ break;
+
+ for (let observer of observerList) {
+ safeCallback(observer, data.callback, data.args);
+ }
+
+ break;
+ }
+ }
+ },
+
+ _callFunction: function(call, args, callback) {
+ let requestId = this._getRandomId();
+ let data = { call: call, args: args, requestId: requestId };
+
+ this._mm.sendAsyncMessage("ContentPrefs:FunctionCall", data);
+
+ this._requests.set(requestId, new CallbackCaller(callback));
+ },
+
+ getCachedByDomainAndName: NYI,
+ getCachedBySubdomainAndName: NYI,
+ getCachedGlobal: NYI,
+
+ addObserverForName: function(name, observer) {
+ let set = this._observers.get(name);
+ if (!set) {
+ set = new Set();
+ if (this._observers.size === 0) {
+ // This is the first observer of any kind. Start listening for changes.
+ this._mm.addMessageListener("ContentPrefs:NotifyObservers", this);
+ }
+
+ // This is the first observer for this name. Start listening for changes
+ // to it.
+ this._mm.sendAsyncMessage("ContentPrefs:AddObserverForName", { name: name });
+ this._observers.set(name, set);
+ }
+
+ set.add(observer);
+ },
+
+ removeObserverForName: function(name, observer) {
+ let set = this._observers.get(name);
+ if (!set)
+ return;
+
+ set.delete(observer);
+ if (set.size === 0) {
+ // This was the last observer for this name. Stop listening for changes.
+ this._mm.sendAsyncMessage("ContentPrefs:RemoveObserverForName", { name: name });
+
+ this._observers.delete(name);
+ if (this._observers.size === 0) {
+ // This was the last observer for this process. Stop listing for all
+ // changes.
+ this._mm.removeMessageListener("ContentPrefs:NotifyObservers", this);
+ }
+ }
+ },
+
+ extractDomain: NYI
+};
+
+function forwardMethodToParent(method, signature, ...args) {
+ // Ignore superfluous arguments
+ args = args.slice(0, signature.length);
+
+ // Process context argument for forwarding
+ let contextIndex = signature.indexOf("context");
+ if (contextIndex > -1) {
+ args[contextIndex] = contextArg(args[contextIndex]);
+ }
+ // Take out the callback argument, if present.
+ let callbackIndex = signature.indexOf("callback");
+ let callback = null;
+ if (callbackIndex > -1 && args.length > callbackIndex) {
+ callback = args.splice(callbackIndex, 1)[0];
+ }
+ this._callFunction(method, args, callback);
+}
+
+for (let [method, signature] of _methodsCallableFromChild) {
+ ContentPrefServiceChild[method] = forwardMethodToParent.bind(ContentPrefServiceChild, method, signature);
+}
+
+ContentPrefServiceChild.init();
diff --git a/toolkit/components/contentprefs/ContentPrefServiceParent.jsm b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm
new file mode 100644
index 0000000000..32e31a7890
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefServiceParent.jsm
@@ -0,0 +1,137 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [ "ContentPrefServiceParent" ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/ContentPrefUtils.jsm");
+
+var ContentPrefServiceParent = {
+ _cps2: null,
+
+ init: function() {
+ let globalMM = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+ this._cps2 = Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2);
+
+ globalMM.addMessageListener("ContentPrefs:FunctionCall", this);
+
+ let observerChangeHandler = this.handleObserverChange.bind(this);
+ globalMM.addMessageListener("ContentPrefs:AddObserverForName", observerChangeHandler);
+ globalMM.addMessageListener("ContentPrefs:RemoveObserverForName", observerChangeHandler);
+ globalMM.addMessageListener("child-process-shutdown", observerChangeHandler);
+ },
+
+ // Map from message manager -> content pref observer.
+ _observers: new Map(),
+
+ handleObserverChange: function(msg) {
+ let observer = this._observers.get(msg.target);
+ if (msg.name === "child-process-shutdown") {
+ // If we didn't have any observers for this child process, don't do
+ // anything.
+ if (!observer)
+ return;
+
+ for (let i of observer._names) {
+ this._cps2.removeObserverForName(i, observer);
+ }
+
+ this._observers.delete(msg.target);
+ return;
+ }
+
+ let prefName = msg.data.name;
+ if (msg.name === "ContentPrefs:AddObserverForName") {
+ // The child process is responsible for not adding multiple parent
+ // observers for the same name.
+ if (!observer) {
+ observer = {
+ onContentPrefSet: function(group, name, value, isPrivate) {
+ msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers",
+ { name: name, callback: "onContentPrefSet",
+ args: [ group, name, value, isPrivate ] });
+ },
+
+ onContentPrefRemoved: function(group, name, isPrivate) {
+ msg.target.sendAsyncMessage("ContentPrefs:NotifyObservers",
+ { name: name, callback: "onContentPrefRemoved",
+ args: [ group, name, isPrivate ] });
+ },
+
+ // The names we're using this observer object for, used to keep track
+ // of the number of names we care about as well as for removing this
+ // observer if its associated process goes away.
+ _names: new Set()
+ };
+
+ this._observers.set(msg.target, observer);
+ }
+
+ observer._names.add(prefName);
+
+ this._cps2.addObserverForName(prefName, observer);
+ } else {
+ // RemoveObserverForName
+
+ // We must have an observer.
+ this._cps2.removeObserverForName(prefName, observer);
+
+ observer._names.delete(prefName);
+ if (observer._names.size === 0) {
+ // This was the last use for this observer.
+ this._observers.delete(msg.target);
+ }
+ }
+ },
+
+ receiveMessage: function(msg) {
+ let data = msg.data;
+
+ if (!_methodsCallableFromChild.some(([method, args]) => method == data.call)) {
+ throw new Error(`Can't call ${data.call} from child!`);
+ }
+
+ let args = data.args;
+ let requestId = data.requestId;
+
+ let listener = {
+ handleResult: function(pref) {
+ msg.target.sendAsyncMessage("ContentPrefs:HandleResult",
+ { requestId: requestId,
+ contentPref: {
+ domain: pref.domain,
+ name: pref.name,
+ value: pref.value
+ }
+ });
+ },
+
+ handleError: function(error) {
+ msg.target.sendAsyncMessage("ContentPrefs:HandleError",
+ { requestId: requestId,
+ error: error });
+ },
+ handleCompletion: function(reason) {
+ msg.target.sendAsyncMessage("ContentPrefs:HandleCompletion",
+ { requestId: requestId,
+ reason: reason });
+ }
+ };
+
+ // Push our special listener.
+ args.push(listener);
+
+ // And call the function.
+ this._cps2[data.call](...args);
+ }
+};
diff --git a/toolkit/components/contentprefs/ContentPrefStore.jsm b/toolkit/components/contentprefs/ContentPrefStore.jsm
new file mode 100644
index 0000000000..7a552662ff
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefStore.jsm
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = [
+ "ContentPrefStore",
+];
+
+function ContentPrefStore() {
+ this._groups = new Map();
+ this._globalNames = new Map();
+}
+
+ContentPrefStore.prototype = {
+
+ set: function CPS_set(group, name, val) {
+ if (group) {
+ if (!this._groups.has(group))
+ this._groups.set(group, new Map());
+ this._groups.get(group).set(name, val);
+ }
+ else {
+ this._globalNames.set(name, val);
+ }
+ },
+
+ setWithCast: function CPS_setWithCast(group, name, val) {
+ if (typeof(val) == "boolean")
+ val = val ? 1 : 0;
+ else if (val === undefined)
+ val = null;
+ this.set(group, name, val);
+ },
+
+ has: function CPS_has(group, name) {
+ if (group) {
+ return this._groups.has(group) &&
+ this._groups.get(group).has(name);
+ }
+ return this._globalNames.has(name);
+ },
+
+ get: function CPS_get(group, name) {
+ if (group && this._groups.has(group))
+ return this._groups.get(group).get(name);
+ return this._globalNames.get(name);
+ },
+
+ remove: function CPS_remove(group, name) {
+ if (group) {
+ if (this._groups.has(group)) {
+ this._groups.get(group).delete(name);
+ if (this._groups.get(group).size == 0)
+ this._groups.delete(group);
+ }
+ }
+ else {
+ this._globalNames.delete(name);
+ }
+ },
+
+ removeGroup: function CPS_removeGroup(group) {
+ if (group) {
+ this._groups.delete(group);
+ }
+ else {
+ this._globalNames.clear();
+ }
+ },
+
+ removeAllGroups: function CPS_removeAllGroups() {
+ this._groups.clear();
+ },
+
+ removeAll: function CPS_removeAll() {
+ this.removeAllGroups();
+ this._globalNames.clear();
+ },
+
+ groupsMatchIncludingSubdomains: function CPS_groupsMatchIncludingSubdomains(group, group2) {
+ let idx = group2.indexOf(group);
+ return (idx == group2.length - group.length &&
+ (idx == 0 || group2[idx - 1] == "."));
+ },
+
+ * [Symbol.iterator]() {
+ for (let [group, names] of this._groups) {
+ for (let [name, val] of names) {
+ yield [group, name, val];
+ }
+ }
+ for (let [name, val] of this._globalNames) {
+ yield [null, name, val];
+ }
+ },
+
+ * match(group, name, includeSubdomains) {
+ for (let sgroup of this.matchGroups(group, includeSubdomains)) {
+ if (this.has(sgroup, name))
+ yield [sgroup, this.get(sgroup, name)];
+ }
+ },
+
+ * matchGroups(group, includeSubdomains) {
+ if (group) {
+ if (includeSubdomains) {
+ for (let [sgroup, , ] of this) {
+ if (sgroup) {
+ if (this.groupsMatchIncludingSubdomains(group, sgroup)) {
+ yield sgroup;
+ }
+ }
+ }
+ }
+ else if (this._groups.has(group)) {
+ yield group;
+ }
+ }
+ else if (this._globalNames.size) {
+ yield null;
+ }
+ },
+};
diff --git a/toolkit/components/contentprefs/ContentPrefUtils.jsm b/toolkit/components/contentprefs/ContentPrefUtils.jsm
new file mode 100644
index 0000000000..557872e1a6
--- /dev/null
+++ b/toolkit/components/contentprefs/ContentPrefUtils.jsm
@@ -0,0 +1,70 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [
+ "ContentPref",
+ "cbHandleResult",
+ "cbHandleError",
+ "cbHandleCompletion",
+ "safeCallback",
+ "_methodsCallableFromChild",
+];
+
+const { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentPref(domain, name, value) {
+ this.domain = domain;
+ this.name = name;
+ this.value = value;
+}
+
+ContentPref.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPref]),
+};
+
+function cbHandleResult(callback, pref) {
+ safeCallback(callback, "handleResult", [pref]);
+}
+
+function cbHandleCompletion(callback, reason) {
+ safeCallback(callback, "handleCompletion", [reason]);
+}
+
+function cbHandleError(callback, nsresult) {
+ safeCallback(callback, "handleError", [nsresult]);
+}
+
+function safeCallback(callbackObj, methodName, args) {
+ if (!callbackObj || typeof(callbackObj[methodName]) != "function")
+ return;
+ try {
+ callbackObj[methodName].apply(callbackObj, args);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+}
+
+const _methodsCallableFromChild = Object.freeze([
+ ["getByName", ["name", "context", "callback"]],
+ ["getByDomainAndName", ["domain", "name", "context", "callback"]],
+ ["getBySubdomainAndName", ["domain", "name", "context", "callback"]],
+ ["getGlobal", ["name", "context", "callback"]],
+ ["set", ["domain", "name", "value", "context", "callback"]],
+ ["setGlobal", ["name", "value", "context", "callback"]],
+ ["removeByDomainAndName", ["domain", "name", "context", "callback"]],
+ ["removeBySubdomainAndName", ["domain", "name", "context", "callback"]],
+ ["removeGlobal", ["name", "context", "callback"]],
+ ["removeByDomain", ["domain", "context", "callback"]],
+ ["removeBySubdomain", ["domain", "context", "callback"]],
+ ["removeByName", ["name", "context", "callback"]],
+ ["removeAllDomains", ["context", "callback"]],
+ ["removeAllGlobals", ["context", "callback"]],
+]);
diff --git a/toolkit/components/contentprefs/moz.build b/toolkit/components/contentprefs/moz.build
new file mode 100644
index 0000000000..24bb296f15
--- /dev/null
+++ b/toolkit/components/contentprefs/moz.build
@@ -0,0 +1,31 @@
+# -*- 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/unit/xpcshell.ini',
+ 'tests/unit_cps2/xpcshell.ini',
+]
+
+MOCHITEST_MANIFESTS += [
+ 'tests/mochitest/mochitest.ini'
+]
+
+EXTRA_COMPONENTS += [
+ 'nsContentPrefService.js',
+ 'nsContentPrefService.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'ContentPrefInstance.jsm',
+ 'ContentPrefService2.jsm',
+ 'ContentPrefServiceChild.jsm',
+ 'ContentPrefServiceParent.jsm',
+ 'ContentPrefStore.jsm',
+ 'ContentPrefUtils.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Preferences')
diff --git a/toolkit/components/contentprefs/nsContentPrefService.js b/toolkit/components/contentprefs/nsContentPrefService.js
new file mode 100644
index 0000000000..6360134a54
--- /dev/null
+++ b/toolkit/components/contentprefs/nsContentPrefService.js
@@ -0,0 +1,1332 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+const CACHE_MAX_GROUP_ENTRIES = 100;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentPrefService() {
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return Cu.import("resource://gre/modules/ContentPrefServiceChild.jsm")
+ .ContentPrefServiceChild;
+ }
+
+ // If this throws an exception, it causes the getService call to fail,
+ // but the next time a consumer tries to retrieve the service, we'll try
+ // to initialize the database again, which might work if the failure
+ // was due to a temporary condition (like being out of disk space).
+ this._dbInit();
+
+ this._observerSvc.addObserver(this, "last-pb-context-exited", false);
+
+ // Observe shutdown so we can shut down the database connection.
+ this._observerSvc.addObserver(this, "xpcom-shutdown", false);
+}
+
+Cu.import("resource://gre/modules/ContentPrefStore.jsm");
+const cache = new ContentPrefStore();
+cache.set = function CPS_cache_set(group, name, val) {
+ Object.getPrototypeOf(this).set.apply(this, arguments);
+ let groupCount = this._groups.size;
+ if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
+ // Clean half of the entries
+ for (let [group, name, ] of this) {
+ this.remove(group, name);
+ groupCount--;
+ if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2)
+ break;
+ }
+ }
+};
+
+const privModeStorage = new ContentPrefStore();
+
+ContentPrefService.prototype = {
+ // XPCOM Plumbing
+
+ classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
+
+ QueryInterface: function CPS_QueryInterface(iid) {
+ let supportedIIDs = [
+ Ci.nsIContentPrefService,
+ Ci.nsISupports,
+ ];
+ if (supportedIIDs.some(i => iid.equals(i)))
+ return this;
+ if (iid.equals(Ci.nsIContentPrefService2)) {
+ if (!this._contentPrefService2) {
+ let s = {};
+ Cu.import("resource://gre/modules/ContentPrefService2.jsm", s);
+ this._contentPrefService2 = new s.ContentPrefService2(this);
+ }
+ return this._contentPrefService2;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ // Convenience Getters
+
+ // Observer Service
+ __observerSvc: null,
+ get _observerSvc() {
+ if (!this.__observerSvc)
+ this.__observerSvc = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ return this.__observerSvc;
+ },
+
+ // Console Service
+ __consoleSvc: null,
+ get _consoleSvc() {
+ if (!this.__consoleSvc)
+ this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService);
+ return this.__consoleSvc;
+ },
+
+ // Preferences Service
+ __prefSvc: null,
+ get _prefSvc() {
+ if (!this.__prefSvc)
+ this.__prefSvc = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ return this.__prefSvc;
+ },
+
+
+ // Destruction
+
+ _destroy: function ContentPrefService__destroy() {
+ this._observerSvc.removeObserver(this, "xpcom-shutdown");
+ this._observerSvc.removeObserver(this, "last-pb-context-exited");
+
+ // Finalize statements which may have been used asynchronously.
+ // FIXME(696499): put them in an object cache like other components.
+ if (this.__stmtSelectPrefID) {
+ this.__stmtSelectPrefID.finalize();
+ this.__stmtSelectPrefID = null;
+ }
+ if (this.__stmtSelectGlobalPrefID) {
+ this.__stmtSelectGlobalPrefID.finalize();
+ this.__stmtSelectGlobalPrefID = null;
+ }
+ if (this.__stmtInsertPref) {
+ this.__stmtInsertPref.finalize();
+ this.__stmtInsertPref = null;
+ }
+ if (this.__stmtInsertGroup) {
+ this.__stmtInsertGroup.finalize();
+ this.__stmtInsertGroup = null;
+ }
+ if (this.__stmtInsertSetting) {
+ this.__stmtInsertSetting.finalize();
+ this.__stmtInsertSetting = null;
+ }
+ if (this.__stmtSelectGroupID) {
+ this.__stmtSelectGroupID.finalize();
+ this.__stmtSelectGroupID = null;
+ }
+ if (this.__stmtSelectSettingID) {
+ this.__stmtSelectSettingID.finalize();
+ this.__stmtSelectSettingID = null;
+ }
+ if (this.__stmtSelectPref) {
+ this.__stmtSelectPref.finalize();
+ this.__stmtSelectPref = null;
+ }
+ if (this.__stmtSelectGlobalPref) {
+ this.__stmtSelectGlobalPref.finalize();
+ this.__stmtSelectGlobalPref = null;
+ }
+ if (this.__stmtSelectPrefsByName) {
+ this.__stmtSelectPrefsByName.finalize();
+ this.__stmtSelectPrefsByName = null;
+ }
+ if (this.__stmtDeleteSettingIfUnused) {
+ this.__stmtDeleteSettingIfUnused.finalize();
+ this.__stmtDeleteSettingIfUnused = null;
+ }
+ if (this.__stmtSelectPrefs) {
+ this.__stmtSelectPrefs.finalize();
+ this.__stmtSelectPrefs = null;
+ }
+ if (this.__stmtDeleteGroupIfUnused) {
+ this.__stmtDeleteGroupIfUnused.finalize();
+ this.__stmtDeleteGroupIfUnused = null;
+ }
+ if (this.__stmtDeletePref) {
+ this.__stmtDeletePref.finalize();
+ this.__stmtDeletePref = null;
+ }
+ if (this.__stmtUpdatePref) {
+ this.__stmtUpdatePref.finalize();
+ this.__stmtUpdatePref = null;
+ }
+
+ if (this._contentPrefService2)
+ this._contentPrefService2.destroy();
+
+ this._dbConnection.asyncClose();
+
+ // Delete references to XPCOM components to make sure we don't leak them
+ // (although we haven't observed leakage in tests). Also delete references
+ // in _observers and _genericObservers to avoid cycles with those that
+ // refer to us and don't remove themselves from those observer pools.
+ delete this._observers;
+ delete this._genericObservers;
+ delete this.__consoleSvc;
+ delete this.__grouper;
+ delete this.__observerSvc;
+ delete this.__prefSvc;
+ },
+
+
+ // nsIObserver
+
+ observe: function ContentPrefService_observe(subject, topic, data) {
+ switch (topic) {
+ case "xpcom-shutdown":
+ this._destroy();
+ break;
+ case "last-pb-context-exited":
+ this._privModeStorage.removeAll();
+ break;
+ }
+ },
+
+
+ // in-memory cache and private-browsing stores
+
+ _cache: cache,
+ _privModeStorage: privModeStorage,
+
+ // nsIContentPrefService
+
+ getPref: function ContentPrefService_getPref(aGroup, aName, aContext, aCallback) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ var group = this._parseGroupParam(aGroup);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ if (this._privModeStorage.has(group, aName)) {
+ let value = this._privModeStorage.get(group, aName);
+ if (aCallback) {
+ this._scheduleCallback(function() { aCallback.onResult(value); });
+ return undefined;
+ }
+ return value;
+ }
+ // if we don't have a pref specific to this private mode browsing
+ // session, to try to get one from normal mode
+ }
+
+ if (group == null)
+ return this._selectGlobalPref(aName, aCallback);
+ return this._selectPref(group, aName, aCallback);
+ },
+
+ setPref: function ContentPrefService_setPref(aGroup, aName, aValue, aContext) {
+ warnDeprecated();
+
+ // If the pref is already set to the value, there's nothing more to do.
+ var currentValue = this.getPref(aGroup, aName, aContext);
+ if (typeof currentValue != "undefined") {
+ if (currentValue == aValue)
+ return;
+ }
+
+ var group = this._parseGroupParam(aGroup);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ this._privModeStorage.setWithCast(group, aName, aValue);
+ this._notifyPrefSet(group, aName, aValue, aContext.usePrivateBrowsing);
+ return;
+ }
+
+ var settingID = this._selectSettingID(aName) || this._insertSetting(aName);
+ var groupID, prefID;
+ if (group == null) {
+ groupID = null;
+ prefID = this._selectGlobalPrefID(settingID);
+ }
+ else {
+ groupID = this._selectGroupID(group) || this._insertGroup(group);
+ prefID = this._selectPrefID(groupID, settingID);
+ }
+
+ // Update the existing record, if any, or create a new one.
+ if (prefID)
+ this._updatePref(prefID, aValue);
+ else
+ this._insertPref(groupID, settingID, aValue);
+
+ this._cache.setWithCast(group, aName, aValue);
+
+ this._notifyPrefSet(group, aName, aValue,
+ aContext ? aContext.usePrivateBrowsing : false);
+ },
+
+ hasPref: function ContentPrefService_hasPref(aGroup, aName, aContext) {
+ warnDeprecated();
+
+ // XXX If consumers end up calling this method regularly, then we should
+ // optimize this to query the database directly.
+ return (typeof this.getPref(aGroup, aName, aContext) != "undefined");
+ },
+
+ hasCachedPref: function ContentPrefService_hasCachedPref(aGroup, aName, aContext) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ let group = this._parseGroupParam(aGroup);
+ let storage = aContext && aContext.usePrivateBrowsing ? this._privModeStorage: this._cache;
+ return storage.has(group, aName);
+ },
+
+ removePref: function ContentPrefService_removePref(aGroup, aName, aContext) {
+ warnDeprecated();
+
+ // If there's no old value, then there's nothing to remove.
+ if (!this.hasPref(aGroup, aName, aContext))
+ return;
+
+ var group = this._parseGroupParam(aGroup);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ this._privModeStorage.remove(group, aName);
+ this._notifyPrefRemoved(group, aName, true);
+ return;
+ }
+
+ var settingID = this._selectSettingID(aName);
+ var groupID, prefID;
+ if (group == null) {
+ groupID = null;
+ prefID = this._selectGlobalPrefID(settingID);
+ }
+ else {
+ groupID = this._selectGroupID(group);
+ prefID = this._selectPrefID(groupID, settingID);
+ }
+
+ this._deletePref(prefID);
+
+ // Get rid of extraneous records that are no longer being used.
+ this._deleteSettingIfUnused(settingID);
+ if (groupID)
+ this._deleteGroupIfUnused(groupID);
+
+ this._cache.remove(group, aName);
+ this._notifyPrefRemoved(group, aName, false);
+ },
+
+ removeGroupedPrefs: function ContentPrefService_removeGroupedPrefs(aContext) {
+ warnDeprecated();
+
+ // will not delete global preferences
+ if (aContext && aContext.usePrivateBrowsing) {
+ // keep only global prefs
+ this._privModeStorage.removeAllGroups();
+ }
+ this._cache.removeAllGroups();
+ this._dbConnection.beginTransaction();
+ try {
+ this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE groupID IS NOT NULL");
+ this._dbConnection.executeSimpleSQL("DELETE FROM groups");
+ this._dbConnection.executeSimpleSQL(`
+ DELETE FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `);
+ this._dbConnection.commitTransaction();
+ }
+ catch (ex) {
+ this._dbConnection.rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ removePrefsByName: function ContentPrefService_removePrefsByName(aName, aContext) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ for (let [group, name, ] of this._privModeStorage) {
+ if (name === aName) {
+ this._privModeStorage.remove(group, aName);
+ this._notifyPrefRemoved(group, aName, true);
+ }
+ }
+ }
+
+ var settingID = this._selectSettingID(aName);
+ if (!settingID)
+ return;
+
+ var selectGroupsStmt = this._dbCreateStatement(`
+ SELECT groups.id AS groupID, groups.name AS groupName
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ WHERE prefs.settingID = :setting
+ `);
+
+ var groupNames = [];
+ var groupIDs = [];
+ try {
+ selectGroupsStmt.params.setting = settingID;
+
+ while (selectGroupsStmt.executeStep()) {
+ groupIDs.push(selectGroupsStmt.row["groupID"]);
+ groupNames.push(selectGroupsStmt.row["groupName"]);
+ }
+ }
+ finally {
+ selectGroupsStmt.reset();
+ }
+
+ if (this.hasPref(null, aName)) {
+ groupNames.push(null);
+ }
+
+ this._dbConnection.executeSimpleSQL("DELETE FROM prefs WHERE settingID = " + settingID);
+ this._dbConnection.executeSimpleSQL("DELETE FROM settings WHERE id = " + settingID);
+
+ for (var i = 0; i < groupNames.length; i++) {
+ this._cache.remove(groupNames[i], aName);
+ if (groupNames[i]) // ie. not null, which will be last (and i == groupIDs.length)
+ this._deleteGroupIfUnused(groupIDs[i]);
+ if (!aContext || !aContext.usePrivateBrowsing) {
+ this._notifyPrefRemoved(groupNames[i], aName, false);
+ }
+ }
+ },
+
+ getPrefs: function ContentPrefService_getPrefs(aGroup, aContext) {
+ warnDeprecated();
+
+ var group = this._parseGroupParam(aGroup);
+ if (aContext && aContext.usePrivateBrowsing) {
+ let prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let [sgroup, sname, sval] of this._privModeStorage) {
+ if (sgroup === group)
+ prefs.setProperty(sname, sval);
+ }
+ return prefs;
+ }
+
+ if (group == null)
+ return this._selectGlobalPrefs();
+ return this._selectPrefs(group);
+ },
+
+ getPrefsByName: function ContentPrefService_getPrefsByName(aName, aContext) {
+ warnDeprecated();
+
+ if (!aName)
+ throw Components.Exception("aName cannot be null or an empty string",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+
+ if (aContext && aContext.usePrivateBrowsing) {
+ let prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let [sgroup, sname, sval] of this._privModeStorage) {
+ if (sname === aName)
+ prefs.setProperty(sgroup, sval);
+ }
+ return prefs;
+ }
+
+ return this._selectPrefsByName(aName);
+ },
+
+ // A hash of arrays of observers, indexed by setting name.
+ _observers: {},
+
+ // An array of generic observers, which observe all settings.
+ _genericObservers: [],
+
+ addObserver: function ContentPrefService_addObserver(aName, aObserver) {
+ warnDeprecated();
+ this._addObserver.apply(this, arguments);
+ },
+
+ _addObserver: function ContentPrefService__addObserver(aName, aObserver) {
+ var observers;
+ if (aName) {
+ if (!this._observers[aName])
+ this._observers[aName] = [];
+ observers = this._observers[aName];
+ }
+ else
+ observers = this._genericObservers;
+
+ if (observers.indexOf(aObserver) == -1)
+ observers.push(aObserver);
+ },
+
+ removeObserver: function ContentPrefService_removeObserver(aName, aObserver) {
+ warnDeprecated();
+ this._removeObserver.apply(this, arguments);
+ },
+
+ _removeObserver: function ContentPrefService__removeObserver(aName, aObserver) {
+ var observers;
+ if (aName) {
+ if (!this._observers[aName])
+ return;
+ observers = this._observers[aName];
+ }
+ else
+ observers = this._genericObservers;
+
+ if (observers.indexOf(aObserver) != -1)
+ observers.splice(observers.indexOf(aObserver), 1);
+ },
+
+ /**
+ * Construct a list of observers to notify about a change to some setting,
+ * putting setting-specific observers before before generic ones, so observers
+ * that initialize individual settings (like the page style controller)
+ * execute before observers that display multiple settings and depend on them
+ * being initialized first (like the content prefs sidebar).
+ */
+ _getObservers: function ContentPrefService__getObservers(aName) {
+ var observers = [];
+
+ if (aName && this._observers[aName])
+ observers = observers.concat(this._observers[aName]);
+ observers = observers.concat(this._genericObservers);
+
+ return observers;
+ },
+
+ /**
+ * Notify all observers about the removal of a preference.
+ */
+ _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved(aGroup, aName, aIsPrivate) {
+ for (var observer of this._getObservers(aName)) {
+ try {
+ observer.onContentPrefRemoved(aGroup, aName, aIsPrivate);
+ }
+ catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ /**
+ * Notify all observers about a preference change.
+ */
+ _notifyPrefSet: function ContentPrefService__notifyPrefSet(aGroup, aName, aValue, aIsPrivate) {
+ for (var observer of this._getObservers(aName)) {
+ try {
+ observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate);
+ }
+ catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ get grouper() {
+ warnDeprecated();
+ return this._grouper;
+ },
+ __grouper: null,
+ get _grouper() {
+ if (!this.__grouper)
+ this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
+ getService(Ci.nsIContentURIGrouper);
+ return this.__grouper;
+ },
+
+ get DBConnection() {
+ warnDeprecated();
+ return this._dbConnection;
+ },
+
+
+ // Data Retrieval & Modification
+
+ __stmtSelectPref: null,
+ get _stmtSelectPref() {
+ if (!this.__stmtSelectPref)
+ this.__stmtSelectPref = this._dbCreateStatement(`
+ SELECT prefs.value AS value
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE groups.name = :group
+ AND settings.name = :setting
+ `);
+
+ return this.__stmtSelectPref;
+ },
+
+ _scheduleCallback: function(func) {
+ let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+ tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ _selectPref: function ContentPrefService__selectPref(aGroup, aSetting, aCallback) {
+ let value = undefined;
+ if (this._cache.has(aGroup, aSetting)) {
+ value = this._cache.get(aGroup, aSetting);
+ if (aCallback) {
+ this._scheduleCallback(function() { aCallback.onResult(value); });
+ return undefined;
+ }
+ return value;
+ }
+
+ try {
+ this._stmtSelectPref.params.group = aGroup;
+ this._stmtSelectPref.params.setting = aSetting;
+
+ if (aCallback) {
+ let cache = this._cache;
+ new AsyncStatement(this._stmtSelectPref).execute({onResult: function(aResult) {
+ cache.set(aGroup, aSetting, aResult);
+ aCallback.onResult(aResult);
+ }});
+ }
+ else {
+ if (this._stmtSelectPref.executeStep()) {
+ value = this._stmtSelectPref.row["value"];
+ }
+ this._cache.set(aGroup, aSetting, value);
+ }
+ }
+ finally {
+ this._stmtSelectPref.reset();
+ }
+
+ return value;
+ },
+
+ __stmtSelectGlobalPref: null,
+ get _stmtSelectGlobalPref() {
+ if (!this.__stmtSelectGlobalPref)
+ this.__stmtSelectGlobalPref = this._dbCreateStatement(`
+ SELECT prefs.value AS value
+ FROM prefs
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE prefs.groupID IS NULL
+ AND settings.name = :name
+ `);
+
+ return this.__stmtSelectGlobalPref;
+ },
+
+ _selectGlobalPref: function ContentPrefService__selectGlobalPref(aName, aCallback) {
+ let value = undefined;
+ if (this._cache.has(null, aName)) {
+ value = this._cache.get(null, aName);
+ if (aCallback) {
+ this._scheduleCallback(function() { aCallback.onResult(value); });
+ return undefined;
+ }
+ return value;
+ }
+
+ try {
+ this._stmtSelectGlobalPref.params.name = aName;
+
+ if (aCallback) {
+ let cache = this._cache;
+ new AsyncStatement(this._stmtSelectGlobalPref).execute({onResult: function(aResult) {
+ cache.set(null, aName, aResult);
+ aCallback.onResult(aResult);
+ }});
+ }
+ else {
+ if (this._stmtSelectGlobalPref.executeStep()) {
+ value = this._stmtSelectGlobalPref.row["value"];
+ }
+ this._cache.set(null, aName, value);
+ }
+ }
+ finally {
+ this._stmtSelectGlobalPref.reset();
+ }
+
+ return value;
+ },
+
+ __stmtSelectGroupID: null,
+ get _stmtSelectGroupID() {
+ if (!this.__stmtSelectGroupID)
+ this.__stmtSelectGroupID = this._dbCreateStatement(`
+ SELECT groups.id AS id
+ FROM groups
+ WHERE groups.name = :name
+ `);
+
+ return this.__stmtSelectGroupID;
+ },
+
+ _selectGroupID: function ContentPrefService__selectGroupID(aName) {
+ var id;
+
+ try {
+ this._stmtSelectGroupID.params.name = aName;
+
+ if (this._stmtSelectGroupID.executeStep())
+ id = this._stmtSelectGroupID.row["id"];
+ }
+ finally {
+ this._stmtSelectGroupID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtInsertGroup: null,
+ get _stmtInsertGroup() {
+ if (!this.__stmtInsertGroup)
+ this.__stmtInsertGroup = this._dbCreateStatement(
+ "INSERT INTO groups (name) VALUES (:name)"
+ );
+
+ return this.__stmtInsertGroup;
+ },
+
+ _insertGroup: function ContentPrefService__insertGroup(aName) {
+ this._stmtInsertGroup.params.name = aName;
+ this._stmtInsertGroup.execute();
+ return this._dbConnection.lastInsertRowID;
+ },
+
+ __stmtSelectSettingID: null,
+ get _stmtSelectSettingID() {
+ if (!this.__stmtSelectSettingID)
+ this.__stmtSelectSettingID = this._dbCreateStatement(
+ "SELECT id FROM settings WHERE name = :name"
+ );
+
+ return this.__stmtSelectSettingID;
+ },
+
+ _selectSettingID: function ContentPrefService__selectSettingID(aName) {
+ var id;
+
+ try {
+ this._stmtSelectSettingID.params.name = aName;
+
+ if (this._stmtSelectSettingID.executeStep())
+ id = this._stmtSelectSettingID.row["id"];
+ }
+ finally {
+ this._stmtSelectSettingID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtInsertSetting: null,
+ get _stmtInsertSetting() {
+ if (!this.__stmtInsertSetting)
+ this.__stmtInsertSetting = this._dbCreateStatement(
+ "INSERT INTO settings (name) VALUES (:name)"
+ );
+
+ return this.__stmtInsertSetting;
+ },
+
+ _insertSetting: function ContentPrefService__insertSetting(aName) {
+ this._stmtInsertSetting.params.name = aName;
+ this._stmtInsertSetting.execute();
+ return this._dbConnection.lastInsertRowID;
+ },
+
+ __stmtSelectPrefID: null,
+ get _stmtSelectPrefID() {
+ if (!this.__stmtSelectPrefID)
+ this.__stmtSelectPrefID = this._dbCreateStatement(
+ "SELECT id FROM prefs WHERE groupID = :groupID AND settingID = :settingID"
+ );
+
+ return this.__stmtSelectPrefID;
+ },
+
+ _selectPrefID: function ContentPrefService__selectPrefID(aGroupID, aSettingID) {
+ var id;
+
+ try {
+ this._stmtSelectPrefID.params.groupID = aGroupID;
+ this._stmtSelectPrefID.params.settingID = aSettingID;
+
+ if (this._stmtSelectPrefID.executeStep())
+ id = this._stmtSelectPrefID.row["id"];
+ }
+ finally {
+ this._stmtSelectPrefID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtSelectGlobalPrefID: null,
+ get _stmtSelectGlobalPrefID() {
+ if (!this.__stmtSelectGlobalPrefID)
+ this.__stmtSelectGlobalPrefID = this._dbCreateStatement(
+ "SELECT id FROM prefs WHERE groupID IS NULL AND settingID = :settingID"
+ );
+
+ return this.__stmtSelectGlobalPrefID;
+ },
+
+ _selectGlobalPrefID: function ContentPrefService__selectGlobalPrefID(aSettingID) {
+ var id;
+
+ try {
+ this._stmtSelectGlobalPrefID.params.settingID = aSettingID;
+
+ if (this._stmtSelectGlobalPrefID.executeStep())
+ id = this._stmtSelectGlobalPrefID.row["id"];
+ }
+ finally {
+ this._stmtSelectGlobalPrefID.reset();
+ }
+
+ return id;
+ },
+
+ __stmtInsertPref: null,
+ get _stmtInsertPref() {
+ if (!this.__stmtInsertPref)
+ this.__stmtInsertPref = this._dbCreateStatement(`
+ INSERT INTO prefs (groupID, settingID, value)
+ VALUES (:groupID, :settingID, :value)
+ `);
+
+ return this.__stmtInsertPref;
+ },
+
+ _insertPref: function ContentPrefService__insertPref(aGroupID, aSettingID, aValue) {
+ this._stmtInsertPref.params.groupID = aGroupID;
+ this._stmtInsertPref.params.settingID = aSettingID;
+ this._stmtInsertPref.params.value = aValue;
+ this._stmtInsertPref.execute();
+ return this._dbConnection.lastInsertRowID;
+ },
+
+ __stmtUpdatePref: null,
+ get _stmtUpdatePref() {
+ if (!this.__stmtUpdatePref)
+ this.__stmtUpdatePref = this._dbCreateStatement(
+ "UPDATE prefs SET value = :value WHERE id = :id"
+ );
+
+ return this.__stmtUpdatePref;
+ },
+
+ _updatePref: function ContentPrefService__updatePref(aPrefID, aValue) {
+ this._stmtUpdatePref.params.id = aPrefID;
+ this._stmtUpdatePref.params.value = aValue;
+ this._stmtUpdatePref.execute();
+ },
+
+ __stmtDeletePref: null,
+ get _stmtDeletePref() {
+ if (!this.__stmtDeletePref)
+ this.__stmtDeletePref = this._dbCreateStatement(
+ "DELETE FROM prefs WHERE id = :id"
+ );
+
+ return this.__stmtDeletePref;
+ },
+
+ _deletePref: function ContentPrefService__deletePref(aPrefID) {
+ this._stmtDeletePref.params.id = aPrefID;
+ this._stmtDeletePref.execute();
+ },
+
+ __stmtDeleteSettingIfUnused: null,
+ get _stmtDeleteSettingIfUnused() {
+ if (!this.__stmtDeleteSettingIfUnused)
+ this.__stmtDeleteSettingIfUnused = this._dbCreateStatement(`
+ DELETE FROM settings WHERE id = :id
+ AND id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `);
+
+ return this.__stmtDeleteSettingIfUnused;
+ },
+
+ _deleteSettingIfUnused: function ContentPrefService__deleteSettingIfUnused(aSettingID) {
+ this._stmtDeleteSettingIfUnused.params.id = aSettingID;
+ this._stmtDeleteSettingIfUnused.execute();
+ },
+
+ __stmtDeleteGroupIfUnused: null,
+ get _stmtDeleteGroupIfUnused() {
+ if (!this.__stmtDeleteGroupIfUnused)
+ this.__stmtDeleteGroupIfUnused = this._dbCreateStatement(`
+ DELETE FROM groups WHERE id = :id
+ AND id NOT IN (SELECT DISTINCT groupID FROM prefs)
+ `);
+
+ return this.__stmtDeleteGroupIfUnused;
+ },
+
+ _deleteGroupIfUnused: function ContentPrefService__deleteGroupIfUnused(aGroupID) {
+ this._stmtDeleteGroupIfUnused.params.id = aGroupID;
+ this._stmtDeleteGroupIfUnused.execute();
+ },
+
+ __stmtSelectPrefs: null,
+ get _stmtSelectPrefs() {
+ if (!this.__stmtSelectPrefs)
+ this.__stmtSelectPrefs = this._dbCreateStatement(`
+ SELECT settings.name AS name, prefs.value AS value
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE groups.name = :group
+ `);
+
+ return this.__stmtSelectPrefs;
+ },
+
+ _selectPrefs: function ContentPrefService__selectPrefs(aGroup) {
+ var prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+
+ try {
+ this._stmtSelectPrefs.params.group = aGroup;
+
+ while (this._stmtSelectPrefs.executeStep())
+ prefs.setProperty(this._stmtSelectPrefs.row["name"],
+ this._stmtSelectPrefs.row["value"]);
+ }
+ finally {
+ this._stmtSelectPrefs.reset();
+ }
+
+ return prefs;
+ },
+
+ __stmtSelectGlobalPrefs: null,
+ get _stmtSelectGlobalPrefs() {
+ if (!this.__stmtSelectGlobalPrefs)
+ this.__stmtSelectGlobalPrefs = this._dbCreateStatement(`
+ SELECT settings.name AS name, prefs.value AS value
+ FROM prefs
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE prefs.groupID IS NULL
+ `);
+
+ return this.__stmtSelectGlobalPrefs;
+ },
+
+ _selectGlobalPrefs: function ContentPrefService__selectGlobalPrefs() {
+ var prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+
+ try {
+ while (this._stmtSelectGlobalPrefs.executeStep())
+ prefs.setProperty(this._stmtSelectGlobalPrefs.row["name"],
+ this._stmtSelectGlobalPrefs.row["value"]);
+ }
+ finally {
+ this._stmtSelectGlobalPrefs.reset();
+ }
+
+ return prefs;
+ },
+
+ __stmtSelectPrefsByName: null,
+ get _stmtSelectPrefsByName() {
+ if (!this.__stmtSelectPrefsByName)
+ this.__stmtSelectPrefsByName = this._dbCreateStatement(`
+ SELECT groups.name AS groupName, prefs.value AS value
+ FROM prefs
+ JOIN groups ON prefs.groupID = groups.id
+ JOIN settings ON prefs.settingID = settings.id
+ WHERE settings.name = :setting
+ `);
+
+ return this.__stmtSelectPrefsByName;
+ },
+
+ _selectPrefsByName: function ContentPrefService__selectPrefsByName(aName) {
+ var prefs = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+
+ try {
+ this._stmtSelectPrefsByName.params.setting = aName;
+
+ while (this._stmtSelectPrefsByName.executeStep())
+ prefs.setProperty(this._stmtSelectPrefsByName.row["groupName"],
+ this._stmtSelectPrefsByName.row["value"]);
+ }
+ finally {
+ this._stmtSelectPrefsByName.reset();
+ }
+
+ var global = this._selectGlobalPref(aName);
+ if (typeof global != "undefined") {
+ prefs.setProperty(null, global);
+ }
+
+ return prefs;
+ },
+
+
+ // Database Creation & Access
+
+ _dbVersion: 4,
+
+ _dbSchema: {
+ tables: {
+ groups: "id INTEGER PRIMARY KEY, \
+ name TEXT NOT NULL",
+
+ settings: "id INTEGER PRIMARY KEY, \
+ name TEXT NOT NULL",
+
+ prefs: "id INTEGER PRIMARY KEY, \
+ groupID INTEGER REFERENCES groups(id), \
+ settingID INTEGER NOT NULL REFERENCES settings(id), \
+ value BLOB, \
+ timestamp INTEGER NOT NULL DEFAULT 0" // Storage in seconds, API in ms. 0 for migrated values.
+ },
+ indices: {
+ groups_idx: {
+ table: "groups",
+ columns: ["name"]
+ },
+ settings_idx: {
+ table: "settings",
+ columns: ["name"]
+ },
+ prefs_idx: {
+ table: "prefs",
+ columns: ["timestamp", "groupID", "settingID"]
+ }
+ }
+ },
+
+ _dbConnection: null,
+
+ _dbCreateStatement: function ContentPrefService__dbCreateStatement(aSQLString) {
+ try {
+ var statement = this._dbConnection.createStatement(aSQLString);
+ }
+ catch (ex) {
+ Cu.reportError("error creating statement " + aSQLString + ": " +
+ this._dbConnection.lastError + " - " +
+ this._dbConnection.lastErrorString);
+ throw ex;
+ }
+
+ return statement;
+ },
+
+ // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version-
+ // specific migration methods) must be careful not to call any method
+ // of the service that assumes the database connection has already been
+ // initialized, since it won't be initialized until at the end of _dbInit.
+
+ _dbInit: function ContentPrefService__dbInit() {
+ var dirService = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ var dbFile = dirService.get("ProfD", Ci.nsIFile);
+ dbFile.append("content-prefs.sqlite");
+
+ var dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+
+ var dbConnection;
+
+ if (!dbFile.exists())
+ dbConnection = this._dbCreate(dbService, dbFile);
+ else {
+ try {
+ dbConnection = dbService.openDatabase(dbFile);
+ }
+ // If the connection isn't ready after we open the database, that means
+ // the database has been corrupted, so we back it up and then recreate it.
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
+ throw e;
+ dbConnection = this._dbBackUpAndRecreate(dbService, dbFile,
+ dbConnection);
+ }
+
+ // Get the version of the schema in the file.
+ var version = dbConnection.schemaVersion;
+
+ // Try to migrate the schema in the database to the current schema used by
+ // the service. If migration fails, back up the database and recreate it.
+ if (version != this._dbVersion) {
+ try {
+ this._dbMigrate(dbConnection, version, this._dbVersion);
+ }
+ catch (ex) {
+ Cu.reportError("error migrating DB: " + ex + "; backing up and recreating");
+ dbConnection = this._dbBackUpAndRecreate(dbService, dbFile, dbConnection);
+ }
+ }
+ }
+
+ // Turn off disk synchronization checking to reduce disk churn and speed up
+ // operations when prefs are changed rapidly (such as when a user repeatedly
+ // changes the value of the browser zoom setting for a site).
+ //
+ // Note: this could cause database corruption if the OS crashes or machine
+ // loses power before the data gets written to disk, but this is considered
+ // a reasonable risk for the not-so-critical data stored in this database.
+ //
+ // If you really don't want to take this risk, however, just set the
+ // toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2
+ // (FULL synchronization), in which case mozStorageConnection::Initialize
+ // will use that value, and we won't override it here.
+ if (!this._prefSvc.prefHasUserValue("toolkit.storage.synchronous"))
+ dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF");
+
+ this._dbConnection = dbConnection;
+ },
+
+ _dbCreate: function ContentPrefService__dbCreate(aDBService, aDBFile) {
+ var dbConnection = aDBService.openDatabase(aDBFile);
+
+ try {
+ this._dbCreateSchema(dbConnection);
+ dbConnection.schemaVersion = this._dbVersion;
+ }
+ catch (ex) {
+ // If we failed to create the database (perhaps because the disk ran out
+ // of space), then remove the database file so we don't leave it in some
+ // half-created state from which we won't know how to recover.
+ dbConnection.close();
+ aDBFile.remove(false);
+ throw ex;
+ }
+
+ return dbConnection;
+ },
+
+ _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) {
+ this._dbCreateTables(aDBConnection);
+ this._dbCreateIndices(aDBConnection);
+ },
+
+ _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) {
+ for (let name in this._dbSchema.tables)
+ aDBConnection.createTable(name, this._dbSchema.tables[name]);
+ },
+
+ _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) {
+ for (let name in this._dbSchema.indices) {
+ let index = this._dbSchema.indices[name];
+ let statement = `
+ CREATE INDEX IF NOT EXISTS ${name} ON ${index.table}
+ (${index.columns.join(", ")})
+ `;
+ aDBConnection.executeSimpleSQL(statement);
+ }
+ },
+
+ _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBService,
+ aDBFile,
+ aDBConnection) {
+ aDBService.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt");
+
+ // Close the database, ignoring the "already closed" exception, if any.
+ // It'll be open if we're here because of a migration failure but closed
+ // if we're here because of database corruption.
+ try { aDBConnection.close() } catch (ex) {}
+
+ aDBFile.remove(false);
+
+ let dbConnection = this._dbCreate(aDBService, aDBFile);
+
+ return dbConnection;
+ },
+
+ _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, aOldVersion, aNewVersion) {
+ /**
+ * Migrations should follow the template rules in bug 1074817 comment 3 which are:
+ * 1. Migration should be incremental and non-breaking.
+ * 2. It should be idempotent because one can downgrade an upgrade again.
+ * On downgrade:
+ * 1. Decrement schema version so that upgrade runs the migrations again.
+ */
+ aDBConnection.beginTransaction();
+
+ try {
+ /**
+ * If the schema version is 0, that means it was never set, which means
+ * the database was somehow created without the schema being applied, perhaps
+ * because the system ran out of disk space (although we check for this
+ * in _createDB) or because some other code created the database file without
+ * applying the schema. In any case, recover by simply reapplying the schema.
+ */
+ if (aOldVersion == 0) {
+ this._dbCreateSchema(aDBConnection);
+ } else {
+ for (let i = aOldVersion; i < aNewVersion; i++) {
+ let migrationName = "_dbMigrate" + i + "To" + (i + 1);
+ if (typeof this[migrationName] != 'function') {
+ throw ("no migrator function from version " + aOldVersion + " to version " + aNewVersion);
+ }
+ this[migrationName](aDBConnection);
+ }
+ }
+ aDBConnection.schemaVersion = aNewVersion;
+ aDBConnection.commitTransaction();
+ } catch (ex) {
+ aDBConnection.rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(aDBConnection) {
+ aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld");
+ aDBConnection.createTable("groups", this._dbSchema.tables.groups);
+ aDBConnection.executeSimpleSQL(`
+ INSERT INTO groups (id, name)
+ SELECT id, name FROM groupsOld
+ `);
+
+ aDBConnection.executeSimpleSQL("DROP TABLE groupers");
+ aDBConnection.executeSimpleSQL("DROP TABLE groupsOld");
+ },
+
+ _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
+ this._dbCreateIndices(aDBConnection);
+ },
+
+ _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) {
+ // Add timestamp column if it does not exist yet. This operation is idempotent.
+ try {
+ let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs");
+ stmt.finalize();
+ } catch (e) {
+ aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
+ }
+
+ // To modify prefs_idx drop it and create again.
+ aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx");
+ this._dbCreateIndices(aDBConnection);
+ },
+
+ _parseGroupParam: function ContentPrefService__parseGroupParam(aGroup) {
+ if (aGroup == null)
+ return null;
+ if (aGroup.constructor.name == "String")
+ return aGroup.toString();
+ if (aGroup instanceof Ci.nsIURI)
+ return this.grouper.group(aGroup);
+
+ throw Components.Exception("aGroup is not a string, nsIURI or null",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ },
+};
+
+function warnDeprecated() {
+ let Deprecated = Cu.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ Deprecated.warning("nsIContentPrefService is deprecated. Please use nsIContentPrefService2 instead.",
+ "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIContentPrefService2",
+ Components.stack.caller);
+}
+
+
+function HostnameGrouper() {}
+
+HostnameGrouper.prototype = {
+ // XPCOM Plumbing
+
+ classID: Components.ID("{8df290ae-dcaa-4c11-98a5-2429a4dc97bb}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentURIGrouper]),
+
+ // nsIContentURIGrouper
+
+ group: function HostnameGrouper_group(aURI) {
+ var group;
+
+ try {
+ // Accessing the host property of the URI will throw an exception
+ // if the URI is of a type that doesn't have a host property.
+ // Otherwise, we manually throw an exception if the host is empty,
+ // since the effect is the same (we can't derive a group from it).
+
+ group = aURI.host;
+ if (!group)
+ throw ("can't derive group from host; no host in URI");
+ }
+ catch (ex) {
+ // If we don't have a host, then use the entire URI (minus the query,
+ // reference, and hash, if possible) as the group. This means that URIs
+ // like about:mozilla and about:blank will be considered separate groups,
+ // but at least they'll be grouped somehow.
+
+ // This also means that each individual file: URL will be considered
+ // its own group. This seems suboptimal, but so does treating the entire
+ // file: URL space as a single group (especially if folks start setting
+ // group-specific capabilities prefs).
+
+ // XXX Is there something better we can do here?
+
+ try {
+ var url = aURI.QueryInterface(Ci.nsIURL);
+ group = aURI.prePath + url.filePath;
+ }
+ catch (ex) {
+ group = aURI.spec;
+ }
+ }
+
+ return group;
+ }
+};
+
+function AsyncStatement(aStatement) {
+ this.stmt = aStatement;
+}
+
+AsyncStatement.prototype = {
+ execute: function AsyncStmt_execute(aCallback) {
+ let stmt = this.stmt;
+ stmt.executeAsync({
+ _callback: aCallback,
+ _hadResult: false,
+ handleResult: function(aResult) {
+ this._hadResult = true;
+ if (this._callback) {
+ let row = aResult.getNextRow();
+ this._callback.onResult(row.getResultByName("value"));
+ }
+ },
+ handleCompletion: function(aReason) {
+ if (!this._hadResult && this._callback &&
+ aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED)
+ this._callback.onResult(undefined);
+ },
+ handleError: function(aError) {}
+ });
+ }
+};
+
+// XPCOM Plumbing
+
+var components = [ContentPrefService, HostnameGrouper];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/components/contentprefs/nsContentPrefService.manifest b/toolkit/components/contentprefs/nsContentPrefService.manifest
new file mode 100644
index 0000000000..b6bc157212
--- /dev/null
+++ b/toolkit/components/contentprefs/nsContentPrefService.manifest
@@ -0,0 +1,5 @@
+component {e3f772f3-023f-4b32-b074-36cf0fd5d414} nsContentPrefService.js
+contract @mozilla.org/content-pref/service;1 {e3f772f3-023f-4b32-b074-36cf0fd5d414}
+component {8df290ae-dcaa-4c11-98a5-2429a4dc97bb} nsContentPrefService.js
+contract @mozilla.org/content-pref/hostname-grouper;1 {8df290ae-dcaa-4c11-98a5-2429a4dc97bb}
+
diff --git a/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js b/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..64a4eda731
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/contentprefs/tests/mochitest/mochitest.ini b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..ec4f059458
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/mochitest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_remoteContentPrefs.html]
+skip-if = toolkit == 'android' || e10s # bug 783513
diff --git a/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html
new file mode 100644
index 0000000000..d14e85a255
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/mochitest/test_remoteContentPrefs.html
@@ -0,0 +1,311 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for nsIContentPrefService2 in child processes</title>
+ <script type="application/javascript"
+ src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+
+ const childFrameURL =
+ "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
+
+ function childFrameScript(isFramePrivate) {
+ "use strict";
+
+ function Tester(resultArray) {
+ this.results = [];
+ }
+
+ Tester.prototype.is =
+ function(a, b, note) {
+ this.results.push([a === b, note + " (" + a + ", " + b + ")"]);
+ };
+ Tester.prototype.ok =
+ function(b, note) {
+ this.results.push([b != false, note]);
+ };
+
+ var cps = Components.classes["@mozilla.org/content-pref/service;1"]
+ .getService(Components.interfaces.nsIContentPrefService2);
+
+ let test = null;
+ function* test1(message) {
+ let tester = new Tester();
+
+ tester.ok(cps !== null, "got the content pref service");
+
+ cps.setGlobal("testing", 42, null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "set a pref?");
+ test.next();
+ }
+ });
+
+ yield;
+
+ let numResults = 0;
+ cps.getGlobal("testing", null, {
+ handleResult: function(pref) {
+ numResults++;
+ tester.is(pref.name, "testing", "pref has the right name");
+ tester.is(pref.value, 42, "pref has the right value");
+ },
+
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "get a pref?");
+ tester.is(numResults, 1, "got the right number of prefs");
+ tester.is(test.next().done, true, "done with test1");
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test1Finished",
+ { results: tester.results });
+ }
+ });
+
+ yield;
+ }
+
+ function* test2(message) {
+ let tester = new Tester();
+
+ let observer;
+ let removed = false;
+ cps.addObserverForName("testName", observer = {
+ onContentPrefSet: function(group, name, value, isPrivate) {
+ if (removed) {
+ message.target.sendAsyncMessage("testRemoteContentPrefs:fail",
+ { reason: "unexpected notification" });
+ }
+ tester.is(group, null, "group should be null");
+ tester.is(name, "testName", "should only see testName");
+ tester.is(value, 42, "value should be correct");
+ tester.is(isPrivate, isFramePrivate, "privacy should match");
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke2", {})
+ },
+
+ onContentPrefRemoved: function(group, name, isPrivate) {
+ tester.is(group, null, "group should be null");
+ tester.is(name, "testName");
+ tester.is(isPrivate, isFramePrivate, "privacy should match");
+ tester.is(test.next().done, true, "should be done with test2");
+
+ cps.removeObserverForName("testName", observer);
+ removed = true;
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test2Finished",
+ { results: tester.results });
+ }
+ });
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test2poke", {});
+ yield;
+ }
+
+ function* test3(message) {
+ let tester = new Tester();
+
+ cps.setGlobal("testName", 42, null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "set a pref");
+ cps.set("http://mochi.test", "testpref", "str", null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "set a pref");
+ test.next();
+ }
+ });
+ }
+ });
+
+ yield;
+
+ cps.removeByDomain("http://mochi.test", null, {
+ handleCompletion: function(reason) {
+ tester.is(reason, 0, "remove succeeded");
+ cps.getByDomainAndName("http://mochi.test", "testpref", null, {
+ handleResult: function() {
+ message.target.sendAsyncMessage("testRemoteContentPrefs:fail",
+ { reason: "got removed pref in test3" });
+ },
+ handleCompletion: function() {
+ test.next();
+ }
+ });
+ }
+ });
+
+ yield;
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test3Finished",
+ { results: tester.results });
+ }
+
+ function* test4(message) {
+ let tester = new Tester();
+
+ let prefObserver = {
+ onContentPrefSet: function(group, name, value, isPrivate) {
+ test.next({ group: group, name: name, value: value, isPrivate: isPrivate });
+ },
+ onContentPrefRemoved: function(group, name, isPrivate) {
+ test.next({ group: group, name: name, isPrivate: isPrivate });
+ }
+ };
+
+ addMessageListener("testRemoteContentPrefs:prefResults", (msg) => {
+ test.next(msg.data.results);
+ });
+
+ cps.addObserverForName("test", prefObserver);
+
+ cps.set("http://mochi.test", "test", 42, { usePrivateBrowsing: true });
+ let event = yield;
+ tester.is(event.name, "test");
+ tester.is(event.isPrivate, true);
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:getPref",
+ { group: "http://mochi.test", name: "test" });
+
+ let results = yield;
+ tester.is(results.length, 0, "should not have seen the pb pref");
+
+ message.target.sendAsyncMessage("testRemoteContentPrefs:test4Finished",
+ { results: tester.results });
+ }
+
+ addMessageListener("testRemoteContentPrefs:test1", function(message) {
+ test = test1(message);
+ test.next();
+ });
+ addMessageListener("testRemoteContentPrefs:test2", function(message) {
+ test = test2(message);
+ test.next();
+ });
+ addMessageListener("testRemoteContentPrefs:test3", function(message) {
+ test = test3(message);
+ test.next();
+ });
+ addMessageListener("testRemoteContentPrefs:test4", function(message) {
+ test = test4(message);
+ test.next();
+ });
+ }
+
+ function processResults(results) {
+ for (let i of results) {
+ ok(...i);
+ }
+ }
+
+ let test;
+ function* testStructure(mm, isPrivate, callback) {
+ let lastResult;
+
+ function testDone(msg) {
+ test.next(msg.data);
+ }
+
+ mm.addMessageListener("testRemoteContentPrefs:test1Finished", testDone);
+ mm.addMessageListener("testRemoteContentPrefs:test2Finished", testDone);
+ mm.addMessageListener("testRemoteContentPrefs:test3Finished", testDone);
+ mm.addMessageListener("testRemoteContentPrefs:test4Finished", testDone);
+
+ mm.addMessageListener("testRemoteContentPrefs:fail", function(msg) {
+ ok(false, msg.data.reason);
+ });
+
+ mm.sendAsyncMessage("testRemoteContentPrefs:test1", {});
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ var cps = SpecialPowers.Cc["@mozilla.org/content-pref/service;1"]
+ .getService(SpecialPowers.Ci.nsIContentPrefService2);
+ mm.sendAsyncMessage("testRemoteContentPrefs:test2", {});
+ mm.addMessageListener("testRemoteContentPrefs:test2poke", function() {
+ cps.setGlobal("testName", 42, {usePrivateBrowsing: isPrivate});
+ });
+ mm.addMessageListener("testRemoteContentPrefs:test2poke2", function() {
+ cps.removeGlobal("testName", {usePrivateBrowsing: isPrivate});
+ });
+
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ mm.sendAsyncMessage("testRemoteContentPrefs:test3", {});
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ mm.addMessageListener("testRemoteContentPrefs:getPref", function(msg) {
+ let results = [];
+ cps.getByDomainAndName(msg.data.group, msg.data.name, null, {
+ handleResult: function(pref) {
+ results.push(pref);
+ },
+ handleCompletion: function(reason) {
+ mm.sendAsyncMessage("testRemoteContentPrefs:prefResults",
+ { results: results });
+ }
+ });
+ });
+
+ mm.sendAsyncMessage("testRemoteContentPrefs:test4", {});
+ lastResult = yield;
+ processResults(lastResult.results);
+
+ document.getElementById('iframe').remove();
+ setTimeout(callback, 0);
+ }
+
+ function runTest(isPrivate, callback) {
+ info("testing with isPrivate=" + isPrivate);
+ let iframe = document.createElement("iframe");
+ SpecialPowers.wrap(iframe).mozbrowser = true;
+ if (isPrivate) {
+ SpecialPowers.wrap(iframe).mozprivatebrowsing = true;
+ }
+ iframe.id = "iframe";
+ iframe.src = childFrameURL;
+
+ iframe.addEventListener("mozbrowserloadend", function() {
+ info("Got iframe load event.");
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ mm.loadFrameScript("data:,(" + childFrameScript.toString() + ")(" + isPrivate + ");",
+ false);
+
+ test = testStructure(mm, isPrivate, callback);
+ test.next();
+ });
+
+ document.body.appendChild(iframe);
+ }
+
+ function runTests() {
+ info("Browser prefs set.");
+ runTest(false, function() {
+ runTest(true, function() {
+ SimpleTest.finish();
+ });
+ });
+ }
+
+ addEventListener("load", function() {
+ info("Got load event.");
+
+ SpecialPowers.addPermission("browser", true, document);
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.ipc.browser_frames.oop_by_default", true],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["browser.pagethumbnails.capturing_disabled", true]
+ ]
+ }, runTests);
+ });
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/contentprefs/tests/unit/.eslintrc.js b/toolkit/components/contentprefs/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js
new file mode 100644
index 0000000000..84ca1bebff
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/head_contentPrefs.js
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Inspired by the Places infrastructure in head_bookmarks.js
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/ContentPrefInstance.jsm');
+
+const CONTENT_PREFS_DB_FILENAME = "content-prefs.sqlite";
+const CONTENT_PREFS_BACKUP_DB_FILENAME = "content-prefs.sqlite.corrupt";
+
+var ContentPrefTest = {
+ // Convenience Getters
+
+ __dirSvc: null,
+ get _dirSvc() {
+ if (!this.__dirSvc)
+ this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ return this.__dirSvc;
+ },
+
+ __consoleSvc: null,
+ get _consoleSvc() {
+ if (!this.__consoleSvc)
+ this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService);
+ return this.__consoleSvc;
+ },
+
+ __ioSvc: null,
+ get _ioSvc() {
+ if (!this.__ioSvc)
+ this.__ioSvc = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ return this.__ioSvc;
+ },
+
+
+ // nsISupports
+
+ interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+
+ // nsIDirectoryServiceProvider
+
+ getFile: function ContentPrefTest_getFile(property, persistent) {
+ persistent.value = true;
+
+ if (property == "ProfD")
+ return this._dirSvc.get("CurProcD", Ci.nsIFile);
+
+ // This causes extraneous errors to show up in the log when the directory
+ // service asks us first for CurProcD and MozBinD. I wish there was a way
+ // to suppress those errors.
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+
+ // Utilities
+
+ getURI: function ContentPrefTest_getURI(spec) {
+ return this._ioSvc.newURI(spec, null, null);
+ },
+
+ /**
+ * Get the profile directory.
+ */
+ getProfileDir: function ContentPrefTest_getProfileDir() {
+ // do_get_profile can be only called from a parent process
+ if (runningInParent) {
+ return do_get_profile();
+ }
+ // if running in a content process, this just returns the path
+ // profile was initialized in the ipc head file
+ let env = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ // the python harness sets this in the environment for us
+ let profd = env.get("XPCSHELL_TEST_PROFILE_DIR");
+ let file = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ file.initWithPath(profd);
+ return file;
+ },
+
+ /**
+ * Delete the content pref service's persistent datastore. We do this before
+ * and after running tests to make sure we start from scratch each time. We
+ * also do it during the database creation, schema migration, and backup tests.
+ */
+ deleteDatabase: function ContentPrefTest_deleteDatabase() {
+ var file = this.getProfileDir();
+ file.append(CONTENT_PREFS_DB_FILENAME);
+ if (file.exists())
+ try { file.remove(false); } catch (e) { /* stupid windows box */ }
+ return file;
+ },
+
+ /**
+ * Delete the backup of the content pref service's persistent datastore.
+ * We do this during the database creation, schema migration, and backup tests.
+ */
+ deleteBackupDatabase: function ContentPrefTest_deleteBackupDatabase() {
+ var file = this.getProfileDir();
+ file.append(CONTENT_PREFS_BACKUP_DB_FILENAME);
+ if (file.exists())
+ file.remove(false);
+ return file;
+ },
+
+ /**
+ * Log a message to the console and the test log.
+ */
+ log: function ContentPrefTest_log(message) {
+ message = "*** ContentPrefTest: " + message;
+ this._consoleSvc.logStringMessage(message);
+ print(message);
+ }
+
+};
+
+var gInPrivateBrowsing = false;
+function enterPBMode() {
+ gInPrivateBrowsing = true;
+}
+function exitPBMode() {
+ gInPrivateBrowsing = false;
+ Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+}
+
+ContentPrefTest.deleteDatabase();
+
+function inChildProcess() {
+ var appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ if (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType ==
+ Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ return false;
+ }
+ return true;
+}
+
+// Turn on logging for the content preferences service so we can troubleshoot
+// problems with the tests. Note that we cannot do this in a child process
+// without crashing (but we don't need it anyhow)
+if (!inChildProcess()) {
+ var prefBranch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefBranch.setBoolPref("browser.preferences.content.log", true);
+}
+
diff --git a/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
new file mode 100644
index 0000000000..f3c95dac89
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/tail_contentPrefs.js
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ContentPrefTest.deleteDatabase();
+ContentPrefTest.__dirSvc = null;
diff --git a/toolkit/components/contentprefs/tests/unit/test_bug248970.js b/toolkit/components/contentprefs/tests/unit/test_bug248970.js
new file mode 100644
index 0000000000..5f4aa25c50
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug248970.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 run_test() {
+ let loadContext = { get usePrivateBrowsing() { return gInPrivateBrowsing; } };
+
+ ContentPrefTest.deleteDatabase();
+ var cp = new ContentPrefInstance(loadContext);
+ do_check_neq(cp, null, "Retrieving the content prefs service failed");
+
+ try {
+ const uri1 = ContentPrefTest.getURI("http://www.example.com/");
+ const uri2 = ContentPrefTest.getURI("http://www.anotherexample.com/");
+ const pref_name = "browser.content.full-zoom";
+ const zoomA = 1.5, zoomA_new = 0.8, zoomB = 1.3;
+ // save Zoom-A
+ cp.setPref(uri1, pref_name, zoomA);
+ // make sure Zoom-A is retrievable
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+ // enter private browsing mode
+ enterPBMode();
+ // make sure Zoom-A is retrievable
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+ // save Zoom-B
+ cp.setPref(uri2, pref_name, zoomB);
+ // make sure Zoom-B is retrievable
+ do_check_eq(cp.getPref(uri2, pref_name), zoomB);
+ // update Zoom-A
+ cp.setPref(uri1, pref_name, zoomA_new);
+ // make sure Zoom-A has changed
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA_new);
+ // exit private browsing mode
+ exitPBMode();
+ // make sure Zoom-A change has not persisted
+ do_check_eq(cp.getPref(uri1, pref_name), zoomA);
+ // make sure Zoom-B change has not persisted
+ do_check_eq(cp.hasPref(uri2, pref_name), false);
+ } catch (e) {
+ do_throw("Unexpected exception: " + e);
+ }
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_bug503971.js b/toolkit/components/contentprefs/tests/unit/test_bug503971.js
new file mode 100644
index 0000000000..ccfe1d02bc
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug503971.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ var cps = new ContentPrefInstance(null);
+
+ var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+ do_check_thrown(function () { cps.setPref(uri, null, 8); });
+ do_check_thrown(function () { cps.hasPref(uri, null); });
+ do_check_thrown(function () { cps.getPref(uri, null); });
+ do_check_thrown(function () { cps.removePref(uri, null); });
+ do_check_thrown(function () { cps.getPrefsByName(null); });
+ do_check_thrown(function () { cps.removePrefsByName(null); });
+
+ do_check_thrown(function () { cps.setPref(uri, "", 21); });
+ do_check_thrown(function () { cps.hasPref(uri, ""); });
+ do_check_thrown(function () { cps.getPref(uri, ""); });
+ do_check_thrown(function () { cps.removePref(uri, ""); });
+ do_check_thrown(function () { cps.getPrefsByName(""); });
+ do_check_thrown(function () { cps.removePrefsByName(""); });
+}
+
+function do_check_thrown (aCallback) {
+ var exThrown = false;
+ try {
+ aCallback();
+ do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here");
+ } catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ exThrown = true;
+ }
+ do_check_true(exThrown);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_bug679784.js b/toolkit/components/contentprefs/tests/unit/test_bug679784.js
new file mode 100644
index 0000000000..97251d87ba
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_bug679784.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 prefObserver = {
+ setCalledNum: 0,
+ onContentPrefSet: function(aGroup, aName, aValue) {
+ this.setCalledNum++;
+ },
+ removedCalledNum: 0,
+ onContentPrefRemoved: function(aGroup, aName) {
+ this.removedCalledNum++;
+ }
+};
+
+function run_test() {
+ let loadContext = { get usePrivateBrowsing() { return gInPrivateBrowsing; } };
+
+ var cps = new ContentPrefInstance(loadContext);
+ cps.removeGroupedPrefs();
+
+ var uri = ContentPrefTest.getURI("http://www.example.com/");
+ var group = cps.grouper.group(uri);
+
+ // first, set a pref in normal mode
+ cps.setPref(uri, "value", "foo");
+ cps.setPref(null, "value-global", "foo-global");
+
+ var num;
+ cps.addObserver("value", prefObserver);
+ cps.addObserver("value-global", prefObserver);
+
+ enterPBMode();
+
+ // test setPref
+ num = prefObserver.setCalledNum;
+ cps.setPref(uri, "value", "foo-private-browsing");
+ do_check_eq(cps.hasPref(uri, "value"), true);
+ do_check_eq(cps.getPref(uri, "value"), "foo-private-browsing");
+ do_check_eq(prefObserver.setCalledNum, num + 1);
+
+ num = prefObserver.setCalledNum;
+ cps.setPref(null, "value-global", "foo-private-browsing-global");
+ do_check_eq(cps.hasPref(null, "value-global"), true);
+ do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global");
+ do_check_eq(prefObserver.setCalledNum, num + 1);
+
+ // test removePref
+ num = prefObserver.removedCalledNum;
+ cps.removePref(uri, "value");
+ do_check_eq(cps.hasPref(uri, "value"), true);
+ // fallback to non private mode value
+ do_check_eq(cps.getPref(uri, "value"), "foo");
+ do_check_eq(prefObserver.removedCalledNum, num + 1);
+
+ num = prefObserver.removedCalledNum;
+ cps.removePref(null, "value-global");
+ do_check_eq(cps.hasPref(null, "value-global"), true);
+ // fallback to non private mode value
+ do_check_eq(cps.getPref(null, "value-global"), "foo-global") ;
+ do_check_eq(prefObserver.removedCalledNum, num + 1);
+
+ // test removeGroupedPrefs
+ cps.setPref(uri, "value", "foo-private-browsing");
+ cps.removeGroupedPrefs();
+ do_check_eq(cps.hasPref(uri, "value"), false);
+ do_check_eq(cps.getPref(uri, "value"), undefined);
+
+ cps.setPref(null, "value-global", "foo-private-browsing-global");
+ cps.removeGroupedPrefs();
+ do_check_eq(cps.hasPref(null, "value-global"), true);
+ do_check_eq(cps.getPref(null, "value-global"), "foo-private-browsing-global");
+
+ // test removePrefsByName
+ num = prefObserver.removedCalledNum;
+ cps.setPref(uri, "value", "foo-private-browsing");
+ cps.removePrefsByName("value");
+ do_check_eq(cps.hasPref(uri, "value"), false);
+ do_check_eq(cps.getPref(uri, "value"), undefined);
+ do_check_true(prefObserver.removedCalledNum > num);
+
+ num = prefObserver.removedCalledNum;
+ cps.setPref(null, "value-global", "foo-private-browsing");
+ cps.removePrefsByName("value-global");
+ do_check_eq(cps.hasPref(null, "value-global"), false);
+ do_check_eq(cps.getPref(null, "value-global"), undefined);
+ do_check_true(prefObserver.removedCalledNum > num);
+
+ // test getPrefs
+ cps.setPref(uri, "value", "foo-private-browsing");
+ do_check_eq(cps.getPrefs(uri).getProperty("value"), "foo-private-browsing");
+
+ cps.setPref(null, "value-global", "foo-private-browsing-global");
+ do_check_eq(cps.getPrefs(null).getProperty("value-global"), "foo-private-browsing-global");
+
+ // test getPrefsByName
+ do_check_eq(cps.getPrefsByName("value").getProperty(group), "foo-private-browsing");
+ do_check_eq(cps.getPrefsByName("value-global").getProperty(null), "foo-private-browsing-global");
+
+ cps.removeObserver("value", prefObserver);
+ cps.removeObserver("value-global", prefObserver);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js
new file mode 100644
index 0000000000..f7e99ea9d1
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefs.js
@@ -0,0 +1,463 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ // Database Creation, Schema Migration, and Backup
+
+ // Note: in these tests we use createInstance instead of getService
+ // so we can instantiate the service multiple times and make it run
+ // its database initialization code each time.
+
+ // Create a new database.
+ {
+ ContentPrefTest.deleteDatabase();
+
+ // Get the service and make sure it has a ready database connection.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(cps.DBConnection.connectionReady);
+ cps.DBConnection.close();
+ }
+
+ // Open an existing database.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ cps.DBConnection.close();
+ do_check_true(dbFile.exists());
+
+ // Get the service and make sure it has a ready database connection.
+ cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(cps.DBConnection.connectionReady);
+ cps.DBConnection.close();
+ }
+
+ // Open an empty database.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+
+ // Create an empty database.
+ let dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let dbConnection = dbService.openDatabase(dbFile);
+ do_check_eq(dbConnection.schemaVersion, 0);
+ dbConnection.close();
+ do_check_true(dbFile.exists());
+
+ // Get the service and make sure it has created the schema.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_neq(cps.DBConnection.schemaVersion, 0);
+ cps.DBConnection.close();
+ }
+
+ // Open a corrupted database.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+ let backupDBFile = ContentPrefTest.deleteBackupDatabase();
+
+ // Create a corrupted database.
+ let foStream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ foStream.init(dbFile, 0x02 | 0x08 | 0x20, 0o666, 0);
+ let garbageData = "garbage that makes SQLite think the file is corrupted";
+ foStream.write(garbageData, garbageData.length);
+ foStream.close();
+
+ // Get the service and make sure it backs up and recreates the database.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(backupDBFile.exists());
+ do_check_true(cps.DBConnection.connectionReady);
+
+ cps.DBConnection.close();
+ }
+
+ // Open a database with a corrupted schema.
+ {
+ let dbFile = ContentPrefTest.deleteDatabase();
+ let backupDBFile = ContentPrefTest.deleteBackupDatabase();
+
+ // Create an empty database and set the schema version to a number
+ // that will trigger a schema migration that will fail.
+ let dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let dbConnection = dbService.openDatabase(dbFile);
+ dbConnection.schemaVersion = -1;
+ dbConnection.close();
+ do_check_true(dbFile.exists());
+
+ // Get the service and make sure it backs up and recreates the database.
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ createInstance(Ci.nsIContentPrefService);
+ do_check_true(backupDBFile.exists());
+ do_check_true(cps.DBConnection.connectionReady);
+
+ cps.DBConnection.close();
+ }
+
+
+ // Now get the content pref service for real for use by the rest of the tests.
+ let cps = new ContentPrefInstance(null);
+
+ var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+ // Make sure disk synchronization checking is turned off by default.
+ var statement = cps.DBConnection.createStatement("PRAGMA synchronous");
+ statement.executeStep();
+ do_check_eq(0, statement.getInt32(0));
+
+ // Nonexistent Pref
+
+ do_check_eq(cps.getPref(uri, "test.nonexistent.getPref"), undefined);
+ do_check_eq(cps.setPref(uri, "test.nonexistent.setPref", 5), undefined);
+ do_check_false(cps.hasPref(uri, "test.nonexistent.hasPref"));
+ do_check_eq(cps.removePref(uri, "test.nonexistent.removePref"), undefined);
+
+
+ // Existing Pref
+
+ cps.setPref(uri, "test.existing", 5);
+
+ // getPref should return the pref value
+ do_check_eq(cps.getPref(uri, "test.existing"), 5);
+
+ // setPref should return undefined and change the value of the pref
+ do_check_eq(cps.setPref(uri, "test.existing", 6), undefined);
+ do_check_eq(cps.getPref(uri, "test.existing"), 6);
+
+ // hasPref should return true
+ do_check_true(cps.hasPref(uri, "test.existing"));
+
+ // removePref should return undefined and remove the pref
+ do_check_eq(cps.removePref(uri, "test.existing"), undefined);
+ do_check_false(cps.hasPref(uri, "test.existing"));
+
+
+ // Round-Trip Data Integrity
+
+ // Make sure pref values remain the same from setPref to getPref.
+
+ cps.setPref(uri, "test.data-integrity.integer", 5);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.integer"), 5);
+
+ cps.setPref(uri, "test.data-integrity.float", 5.5);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.float"), 5.5);
+
+ cps.setPref(uri, "test.data-integrity.boolean", true);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.boolean"), true);
+
+ cps.setPref(uri, "test.data-integrity.string", "test");
+ do_check_eq(cps.getPref(uri, "test.data-integrity.string"), "test");
+
+ cps.setPref(uri, "test.data-integrity.null", null);
+ do_check_eq(cps.getPref(uri, "test.data-integrity.null"), null);
+
+ // XXX Test arbitrary binary data.
+
+ // Make sure hasPref and removePref work on all data types.
+
+ do_check_true(cps.hasPref(uri, "test.data-integrity.integer"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.float"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.boolean"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.string"));
+ do_check_true(cps.hasPref(uri, "test.data-integrity.null"));
+
+ do_check_eq(cps.removePref(uri, "test.data-integrity.integer"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.float"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.boolean"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.string"), undefined);
+ do_check_eq(cps.removePref(uri, "test.data-integrity.null"), undefined);
+
+ do_check_false(cps.hasPref(uri, "test.data-integrity.integer"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.float"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.boolean"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.string"));
+ do_check_false(cps.hasPref(uri, "test.data-integrity.null"));
+
+
+ // getPrefs
+
+ cps.setPref(uri, "test.getPrefs.a", 1);
+ cps.setPref(uri, "test.getPrefs.b", 2);
+ cps.setPref(uri, "test.getPrefs.c", 3);
+
+ var prefs = cps.getPrefs(uri);
+ do_check_true(prefs.hasKey("test.getPrefs.a"));
+ do_check_eq(prefs.get("test.getPrefs.a"), 1);
+ do_check_true(prefs.hasKey("test.getPrefs.b"));
+ do_check_eq(prefs.get("test.getPrefs.b"), 2);
+ do_check_true(prefs.hasKey("test.getPrefs.c"));
+ do_check_eq(prefs.get("test.getPrefs.c"), 3);
+
+
+ // Site-Specificity
+
+ {
+ // These are all different sites, and setting a pref for one of them
+ // shouldn't set it for the others.
+ let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+ let uri3 = ContentPrefTest.getURI("http://domain1.com/");
+ let uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+ cps.setPref(uri1, "test.site-specificity.uri1", 5);
+ do_check_false(cps.hasPref(uri2, "test.site-specificity.uri1"));
+ do_check_false(cps.hasPref(uri3, "test.site-specificity.uri1"));
+ do_check_false(cps.hasPref(uri4, "test.site-specificity.uri1"));
+
+ cps.setPref(uri2, "test.site-specificity.uri2", 5);
+ do_check_false(cps.hasPref(uri1, "test.site-specificity.uri2"));
+ do_check_false(cps.hasPref(uri3, "test.site-specificity.uri2"));
+ do_check_false(cps.hasPref(uri4, "test.site-specificity.uri2"));
+
+ cps.setPref(uri3, "test.site-specificity.uri3", 5);
+ do_check_false(cps.hasPref(uri1, "test.site-specificity.uri3"));
+ do_check_false(cps.hasPref(uri2, "test.site-specificity.uri3"));
+ do_check_false(cps.hasPref(uri4, "test.site-specificity.uri3"));
+
+ cps.setPref(uri4, "test.site-specificity.uri4", 5);
+ do_check_false(cps.hasPref(uri1, "test.site-specificity.uri4"));
+ do_check_false(cps.hasPref(uri2, "test.site-specificity.uri4"));
+ do_check_false(cps.hasPref(uri3, "test.site-specificity.uri4"));
+ }
+
+ // Observers
+
+ var specificObserver = {
+ interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ numTimesSetCalled: 0,
+ onContentPrefSet: function specificObserver_onContentPrefSet(group, name, value) {
+ ++this.numTimesSetCalled;
+ do_check_eq(group, "www.example.com");
+ do_check_eq(name, "test.observer.1");
+ do_check_eq(value, "test value");
+ },
+
+ numTimesRemovedCalled: 0,
+ onContentPrefRemoved: function specificObserver_onContentPrefRemoved(group, name) {
+ ++this.numTimesRemovedCalled;
+ do_check_eq(group, "www.example.com");
+ do_check_eq(name, "test.observer.1");
+ }
+
+ };
+
+ var genericObserver = {
+ interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ numTimesSetCalled: 0,
+ onContentPrefSet: function genericObserver_onContentPrefSet(group, name, value, isPrivate) {
+ ++this.numTimesSetCalled;
+ do_check_eq(group, "www.example.com");
+ if (name == "test.observer.private")
+ do_check_true(isPrivate);
+ else if (name == "test.observer.normal")
+ do_check_false(isPrivate);
+ else if (name != "test.observer.1" && name != "test.observer.2")
+ do_throw("genericObserver.onContentPrefSet: " +
+ "name not in (test.observer.(1|2|normal|private))");
+ do_check_eq(value, "test value");
+ },
+
+ numTimesRemovedCalled: 0,
+ onContentPrefRemoved: function genericObserver_onContentPrefRemoved(group, name, isPrivate) {
+ ++this.numTimesRemovedCalled;
+ do_check_eq(group, "www.example.com");
+ if (name == "test.observer.private")
+ do_check_true(isPrivate);
+ else if (name == "test.observer.normal")
+ do_check_false(isPrivate);
+ if (name != "test.observer.1" && name != "test.observer.2" &&
+ name != "test.observer.normal" && name != "test.observer.private") {
+ do_throw("genericObserver.onContentPrefSet: " +
+ "name not in (test.observer.(1|2|normal|private))");
+ }
+ }
+
+ };
+
+ // Make sure we can add observers, observers get notified about changes,
+ // specific observers only get notified about changes to the specific setting,
+ // and generic observers get notified about changes to all settings.
+ cps.addObserver("test.observer.1", specificObserver);
+ cps.addObserver(null, genericObserver);
+ cps.setPref(uri, "test.observer.1", "test value");
+ cps.setPref(uri, "test.observer.2", "test value");
+ cps.removePref(uri, "test.observer.1");
+ cps.removePref(uri, "test.observer.2");
+ do_check_eq(specificObserver.numTimesSetCalled, 1);
+ do_check_eq(genericObserver.numTimesSetCalled, 2);
+ do_check_eq(specificObserver.numTimesRemovedCalled, 1);
+ do_check_eq(genericObserver.numTimesRemovedCalled, 2);
+
+ // Make sure information about private context is properly
+ // retrieved by the observer.
+ cps.setPref(uri, "test.observer.private", "test value", {usePrivateBrowsing: true});
+ cps.setPref(uri, "test.observer.normal", "test value", {usePrivateBrowsing: false});
+ cps.removePref(uri, "test.observer.private");
+ cps.removePref(uri, "test.observer.normal");
+
+ // Make sure we can remove observers and they don't get notified
+ // about changes anymore.
+ cps.removeObserver("test.observer.1", specificObserver);
+ cps.removeObserver(null, genericObserver);
+ cps.setPref(uri, "test.observer.1", "test value");
+ cps.removePref(uri, "test.observer.1", "test value");
+ do_check_eq(specificObserver.numTimesSetCalled, 1);
+ do_check_eq(genericObserver.numTimesSetCalled, 4);
+ do_check_eq(specificObserver.numTimesRemovedCalled, 1);
+ do_check_eq(genericObserver.numTimesRemovedCalled, 3);
+
+
+ // Get/Remove Prefs By Name
+
+ {
+ var anObserver = {
+ interfaces: [Ci.nsIContentPrefObserver, Ci.nsISupports],
+
+ QueryInterface: function ContentPrefTest_QueryInterface(iid) {
+ if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ onContentPrefSet: function anObserver_onContentPrefSet(group, name, value) {
+ },
+
+ expectedDomains: [],
+ numTimesRemovedCalled: 0,
+ onContentPrefRemoved: function anObserver_onContentPrefRemoved(group, name) {
+ ++this.numTimesRemovedCalled;
+
+ // remove the domain from the list of expected domains
+ var index = this.expectedDomains.indexOf(group);
+ do_check_true(index >= 0);
+ this.expectedDomains.splice(index, 1);
+ }
+ };
+
+ let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ let uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+ let uri3 = ContentPrefTest.getURI("http://domain1.com/");
+ let uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+ cps.setPref(uri1, "test.byname.1", 1);
+ cps.setPref(uri1, "test.byname.2", 2);
+ cps.setPref(uri2, "test.byname.1", 4);
+ cps.setPref(uri3, "test.byname.3", 8);
+ cps.setPref(uri4, "test.byname.1", 16);
+ cps.setPref(null, "test.byname.1", 32);
+ cps.setPref(null, "test.byname.2", false);
+
+ function enumerateAndCheck(testName, expectedSum, expectedDomains) {
+ var prefsByName = cps.getPrefsByName(testName);
+ var enumerator = prefsByName.enumerator;
+ var sum = 0;
+ while (enumerator.hasMoreElements()) {
+ var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
+ sum += parseInt(property.value);
+
+ // remove the domain from the list of expected domains
+ var index = expectedDomains.indexOf(property.name);
+ do_check_true(index >= 0);
+ expectedDomains.splice(index, 1);
+ }
+ do_check_eq(sum, expectedSum);
+ // check all domains have been removed from the array
+ do_check_eq(expectedDomains.length, 0);
+ }
+
+ enumerateAndCheck("test.byname.1", 53,
+ ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"]);
+ enumerateAndCheck("test.byname.2", 2, ["www.domain1.com", null]);
+ enumerateAndCheck("test.byname.3", 8, ["domain1.com"]);
+
+ cps.addObserver("test.byname.1", anObserver);
+ anObserver.expectedDomains = ["foo.domain1.com", null, "www.domain1.com", "www.domain2.com"];
+
+ cps.removePrefsByName("test.byname.1");
+ do_check_false(cps.hasPref(uri1, "test.byname.1"));
+ do_check_false(cps.hasPref(uri2, "test.byname.1"));
+ do_check_false(cps.hasPref(uri3, "test.byname.1"));
+ do_check_false(cps.hasPref(uri4, "test.byname.1"));
+ do_check_false(cps.hasPref(null, "test.byname.1"));
+ do_check_true(cps.hasPref(uri1, "test.byname.2"));
+ do_check_true(cps.hasPref(uri3, "test.byname.3"));
+
+ do_check_eq(anObserver.numTimesRemovedCalled, 4);
+ do_check_eq(anObserver.expectedDomains.length, 0);
+
+ cps.removeObserver("test.byname.1", anObserver);
+
+ // Clean up after ourselves
+ cps.removePref(uri1, "test.byname.2");
+ cps.removePref(uri3, "test.byname.3");
+ cps.removePref(null, "test.byname.2");
+ }
+
+
+ // Clear Private Data Pref Removal
+
+ {
+ let uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ let uri2 = ContentPrefTest.getURI("http://www.domain2.com/");
+ let uri3 = ContentPrefTest.getURI("http://www.domain3.com/");
+
+ let dbConnection = cps.DBConnection;
+
+ let prefCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM prefs");
+
+ let groupCount = dbConnection.createStatement("SELECT COUNT(*) AS count FROM groups");
+
+ // Add some prefs for multiple domains.
+ cps.setPref(uri1, "test.removeAllGroups", 1);
+ cps.setPref(uri2, "test.removeAllGroups", 2);
+ cps.setPref(uri3, "test.removeAllGroups", 3);
+
+ // Add a global pref.
+ cps.setPref(null, "test.removeAllGroups", 1);
+
+ // Make sure there are some prefs and groups in the database.
+ prefCount.executeStep();
+ do_check_true(prefCount.row.count > 0);
+ prefCount.reset();
+ groupCount.executeStep();
+ do_check_true(groupCount.row.count > 0);
+ groupCount.reset();
+
+ // Remove all prefs and groups from the database using the same routine
+ // the Clear Private Data dialog uses.
+ cps.removeGroupedPrefs();
+
+ // Make sure there are no longer any groups in the database and the only pref
+ // is the global one.
+ prefCount.executeStep();
+ do_check_true(prefCount.row.count == 1);
+ prefCount.reset();
+ groupCount.executeStep();
+ do_check_true(groupCount.row.count == 0);
+ groupCount.reset();
+ let globalPref = dbConnection.createStatement("SELECT groupID FROM prefs");
+ globalPref.executeStep();
+ do_check_true(globalPref.row.groupID == null);
+ globalPref.reset();
+ }
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
new file mode 100644
index 0000000000..38a2faddc4
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_contentPrefsCache.js
@@ -0,0 +1,244 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var cps = new ContentPrefInstance(null);
+
+function run_test() {
+ testCacheWorks("test1.example.com", "test-pref1");
+ testHasCachedPrefFunction("test2.example.com", "test-pref2");
+ testSetCaches("test3.example.com", "test-pref3");
+ testGetCaches("test4.example.com", "test-pref4");
+ testRemovePrefs("test5.example.com", "test-pref5");
+ testTypeConversions("test6.example.com", "test-pref6");
+ testNonExistingPrefCachesAsUndefined("test7.example.com", "test-pref7");
+ testCacheEviction("test8.example.com", "test-pref8");
+}
+
+function testCacheWorks(uri, prefName) {
+ const CACHED_VALUE = 3;
+ const NEW_VALUE = 5;
+
+ cps.setPref(uri, prefName, CACHED_VALUE);
+ do_check_eq(cps.getPref(uri, prefName), CACHED_VALUE);
+
+ // Now change the value directly through the DB and check
+ // that the cached value is different
+
+ let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+ let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+ let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+ "id", groupId, settingId);
+
+ let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+ stmt.params.value = NEW_VALUE;
+ stmt.params.id = prefId;
+ stmt.execute();
+
+ let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+ let cacheValue = cps.getPref(uri, prefName);
+
+ do_check_eq(dbValue, NEW_VALUE);
+ do_check_eq(cacheValue, CACHED_VALUE);
+ do_check_neq(cacheValue, dbValue);
+
+ do_test_pending();
+ cps.getPref(uri, prefName, function (value) {
+ do_check_eq(dbValue, NEW_VALUE);
+ do_check_eq(value, CACHED_VALUE);
+ do_check_neq(value, dbValue);
+ do_test_finished();
+ });
+}
+
+function testHasCachedPrefFunction(uri, prefName) {
+ const STARTING_VALUE = 3;
+ const NEW_VALUE = 5;
+
+ do_check_false(isCached(uri, prefName));
+
+ cps.setPref(uri, prefName, STARTING_VALUE);
+
+ let groupId = selectValue("SELECT id FROM groups WHERE name = :param1", "id", uri);
+ let settingId = selectValue("SELECT id FROM settings WHERE name = :param1", "id", prefName);
+ let prefId = selectValue("SELECT id FROM prefs WHERE groupID = :param1 AND settingID = :param2",
+ "id", groupId, settingId);
+
+ do_check_neq(prefId, undefined);
+
+ let originalValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+ do_check_eq(originalValue, STARTING_VALUE);
+
+ let stmt = cps.DBConnection.createStatement("UPDATE prefs SET value = :value WHERE id = :id");
+ stmt.params.value = NEW_VALUE;
+ stmt.params.id = prefId;
+ stmt.execute();
+
+ let newValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+ do_check_eq(newValue, NEW_VALUE);
+
+ let cachedValue = cps.getPref(uri, prefName);
+ do_check_eq(cachedValue, STARTING_VALUE);
+ do_check_true(isCached(uri, prefName));
+}
+
+function testSetCaches(uri, prefName) {
+ cps.setPref(uri, prefName, 0);
+ do_check_true(isCached(uri, prefName));
+}
+
+function testRemovePrefs(uri, prefName) {
+
+ /* removePref */
+ cps.setPref("www1." + uri, prefName, 1);
+
+ do_check_eq(cps.getPref("www1." + uri, prefName), 1);
+
+ cps.removePref("www1." + uri, prefName);
+
+ do_check_false(isCached("www1." + uri, prefName));
+ do_check_false(cps.hasPref("www1." + uri, prefName));
+ do_check_neq(cps.getPref("www1." + uri, prefName), 1);
+
+ /* removeGroupedPrefs */
+ cps.setPref("www2." + uri, prefName, 2);
+ cps.setPref("www3." + uri, prefName, 3);
+
+ do_check_eq(cps.getPref("www2." + uri, prefName), 2);
+ do_check_eq(cps.getPref("www3." + uri, prefName), 3);
+
+ cps.removeGroupedPrefs();
+
+ do_check_false(isCached("www2." + uri, prefName));
+ do_check_false(isCached("www3." + uri, prefName));
+ do_check_false(cps.hasPref("www2." + uri, prefName));
+ do_check_false(cps.hasPref("www3." + uri, prefName));
+ do_check_neq(cps.getPref("www2." + uri, prefName), 2);
+ do_check_neq(cps.getPref("www3." + uri, prefName), 3);
+
+ /* removePrefsByName */
+ cps.setPref("www4." + uri, prefName, 4);
+ cps.setPref("www5." + uri, prefName, 5);
+
+ do_check_eq(cps.getPref("www4." + uri, prefName), 4);
+ do_check_eq(cps.getPref("www5." + uri, prefName), 5);
+
+ cps.removePrefsByName(prefName);
+
+ do_check_false(isCached("www4." + uri, prefName));
+ do_check_false(isCached("www5." + uri, prefName));
+ do_check_false(cps.hasPref("www4." + uri, prefName));
+ do_check_false(cps.hasPref("www5." + uri, prefName));
+ do_check_neq(cps.getPref("www4." + uri, prefName), 4);
+ do_check_neq(cps.getPref("www5." + uri, prefName), 5);
+}
+
+function testGetCaches(uri, prefName) {
+ const VALUE = 4;
+
+ let insertGroup = cps.DBConnection.createStatement("INSERT INTO groups (name) VALUES (:name)");
+ insertGroup.params.name = uri;
+ insertGroup.execute();
+ let groupId = cps.DBConnection.lastInsertRowID;
+
+ let insertSetting = cps.DBConnection.createStatement("INSERT INTO settings (name) VALUES (:name)");
+ insertSetting.params.name = prefName;
+ insertSetting.execute();
+ let settingId = cps.DBConnection.lastInsertRowID;
+
+ let insertPref = cps.DBConnection.createStatement(`
+ INSERT INTO prefs (groupID, settingID, value)
+ VALUES (:groupId, :settingId, :value)
+ `);
+ insertPref.params.groupId = groupId;
+ insertPref.params.settingId = settingId;
+ insertPref.params.value = VALUE;
+ insertPref.execute();
+ let prefId = cps.DBConnection.lastInsertRowID;
+
+ let dbValue = selectValue("SELECT value FROM prefs WHERE id = :param1", "value", prefId);
+
+ // First access from service should hit the DB
+ let svcValue = cps.getPref(uri, prefName);
+
+ // Second time should get the value from cache
+ let cacheValue = cps.getPref(uri, prefName);
+
+ do_check_eq(VALUE, dbValue);
+ do_check_eq(VALUE, svcValue);
+ do_check_eq(VALUE, cacheValue);
+
+ do_check_true(isCached(uri, prefName));
+}
+
+function testTypeConversions(uri, prefName) {
+ let value;
+
+ cps.setPref(uri, prefName, true);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === 1);
+
+ cps.setPref(uri, prefName, false);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === 0);
+
+ cps.setPref(uri, prefName, null);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === null);
+
+ cps.setPref(uri, prefName, undefined);
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === null);
+}
+
+function testNonExistingPrefCachesAsUndefined(uri, prefName) {
+
+ do_check_false(isCached(uri, prefName));
+
+ // Cache the pref
+ let value = cps.getPref(uri, prefName);
+ do_check_true(value === undefined);
+
+ do_check_true(isCached(uri, prefName));
+
+ // Cached pref
+ value = cps.getPref(uri, prefName);
+ do_check_true(value === undefined);
+}
+
+function testCacheEviction(uri, prefName) {
+
+ cps.setPref(uri, prefName, 5);
+ do_check_eq(cps.getPref(uri, prefName), 5);
+ do_check_true(isCached(uri, prefName));
+
+ // try to evict value from cache by adding various other entries
+ const ENTRIES_TO_ADD = 200;
+ for (let i = 0; i < ENTRIES_TO_ADD; i++) {
+ let uriToAdd = "www" + i + uri;
+ cps.setPref(uriToAdd, prefName, 0);
+ }
+
+ do_check_false(isCached(uri, prefName));
+
+}
+
+function selectValue(stmt, columnName, param1, param2) {
+ stmt = cps.DBConnection.createStatement(stmt);
+ if (param1)
+ stmt.params.param1 = param1;
+
+ if (param2)
+ stmt.params.param2 = param2;
+
+ stmt.executeStep();
+ let val = stmt.row[columnName];
+ stmt.reset();
+ stmt.finalize();
+ return val;
+}
+
+function isCached(uri, prefName) {
+ return cps.hasCachedPref(uri, prefName);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.js
new file mode 100644
index 0000000000..27d239f793
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_getPrefAsync.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/. */
+var cps = new ContentPrefInstance(null);
+var uri = ContentPrefTest.getURI("http://www.example.com/");
+
+function run_test() {
+ do_test_pending();
+
+ cps.setPref(uri, "asynctest", "pie");
+ do_check_eq(cps.getPref(uri, "asynctest"), "pie");
+
+ cps.getPref(uri, "asynctest", function(aValue) {
+ do_check_eq(aValue, "pie");
+ testCallbackObj();
+ });
+}
+
+function testCallbackObj() {
+ cps.getPref(uri, "asynctest", {
+ onResult: function(aValue) {
+ do_check_eq(aValue, "pie");
+ cps.removePref(uri, "asynctest");
+ testNoResult();
+ }
+ });
+}
+
+function testNoResult() {
+ cps.getPref(uri, "asynctest", function(aValue) {
+ do_check_eq(aValue, undefined);
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_stringGroups.js b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js
new file mode 100644
index 0000000000..afce3b64a3
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_stringGroups.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+
+ var cps = new ContentPrefInstance(null);
+
+ // Make sure disk synchronization checking is turned off by default.
+ var statement = cps.DBConnection.createStatement("PRAGMA synchronous");
+ statement.executeStep();
+ do_check_eq(0, statement.getInt32(0));
+
+ // These are the different types of aGroup arguments we'll test.
+ var anObject = {"foo":"bar"}; // a simple object
+ var uri = ContentPrefTest.getURI("http://www.example.com/"); // nsIURI
+ var stringURI = "www.example.com"; // typeof = "string"
+ var stringObjectURI = new String("www.example.com"); // typeof = "object"
+
+ {
+ // First check that all the methods work or don't work.
+ function simple_test_methods(aGroup, shouldThrow) {
+ var prefName = "test.pref.0";
+ var prefValue = Math.floor(Math.random() * 100);
+
+ if (shouldThrow) {
+ do_check_thrown(function () { cps.getPref(aGroup, prefName); });
+ do_check_thrown(function () { cps.setPref(aGroup, prefName, prefValue); });
+ do_check_thrown(function () { cps.hasPref(aGroup, prefName); });
+ do_check_thrown(function () { cps.removePref(aGroup, prefName); });
+ do_check_thrown(function () { cps.getPrefs(aGroup); });
+ } else {
+ do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined);
+ do_check_true(cps.hasPref(aGroup, prefName));
+ do_check_eq(cps.getPref(aGroup, prefName), prefValue);
+ do_check_eq(cps.removePref(aGroup, prefName), undefined);
+ do_check_false(cps.hasPref(aGroup, prefName));
+ }
+ }
+
+ simple_test_methods(cps, true); // arbitrary nsISupports object, should throw too
+ simple_test_methods(anObject, true);
+ simple_test_methods(uri, false);
+ simple_test_methods(stringURI, false);
+ simple_test_methods(stringObjectURI, false);
+ }
+
+ {
+ // Now we'll check that each argument produces the same result.
+ function complex_test_methods(aGroup) {
+ var prefName = "test.pref.1";
+ var prefValue = Math.floor(Math.random() * 100);
+
+ do_check_eq(cps.setPref(aGroup, prefName, prefValue), undefined);
+
+ do_check_true(cps.hasPref(uri, prefName));
+ do_check_true(cps.hasPref(stringURI, prefName));
+ do_check_true(cps.hasPref(stringObjectURI, prefName));
+
+ do_check_eq(cps.getPref(uri, prefName), prefValue);
+ do_check_eq(cps.getPref(stringURI, prefName), prefValue);
+ do_check_eq(cps.getPref(stringObjectURI, prefName), prefValue);
+
+ do_check_eq(cps.removePref(aGroup, prefName), undefined);
+
+ do_check_false(cps.hasPref(uri, prefName));
+ do_check_false(cps.hasPref(stringURI, prefName));
+ do_check_false(cps.hasPref(stringObjectURI, prefName));
+ }
+
+ complex_test_methods(uri);
+ complex_test_methods(stringURI);
+ complex_test_methods(stringObjectURI);
+ }
+
+ {
+ // test getPrefs returns the same prefs
+ do_check_eq(cps.setPref(stringObjectURI, "test.5", 5), undefined);
+ do_check_eq(cps.setPref(stringURI, "test.2", 2), undefined);
+ do_check_eq(cps.setPref(uri, "test.1", 1), undefined);
+
+ enumerateAndCheck(cps.getPrefs(uri), 8, ["test.1", "test.2", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringURI), 8, ["test.1", "test.2", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringObjectURI), 8, ["test.1", "test.2", "test.5"]);
+
+ do_check_eq(cps.setPref(uri, "test.4", 4), undefined);
+ do_check_eq(cps.setPref(stringObjectURI, "test.0", 0), undefined);
+
+ enumerateAndCheck(cps.getPrefs(uri), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringObjectURI), 12, ["test.0", "test.1", "test.2", "test.4", "test.5"]);
+
+ do_check_eq(cps.setPref(stringURI, "test.3", 3), undefined);
+
+ enumerateAndCheck(cps.getPrefs(uri), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+ enumerateAndCheck(cps.getPrefs(stringObjectURI), 15, ["test.0", "test.1", "test.2", "test.3", "test.4", "test.5"]);
+ }
+}
+
+function do_check_thrown (aCallback) {
+ var exThrown = false;
+ try {
+ aCallback();
+ do_throw("NS_ERROR_ILLEGAL_VALUE should have been thrown here");
+ } catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_ILLEGAL_VALUE);
+ exThrown = true;
+ }
+ do_check_true(exThrown);
+}
+
+function enumerateAndCheck(prefs, expectedSum, expectedNames) {
+ var enumerator = prefs.enumerator;
+ var sum = 0;
+ while (enumerator.hasMoreElements()) {
+ var property = enumerator.getNext().QueryInterface(Components.interfaces.nsIProperty);
+ sum += parseInt(property.value);
+
+ // remove the pref name from the list of expected names
+ var index = expectedNames.indexOf(property.name);
+ do_check_true(index >= 0);
+ expectedNames.splice(index, 1);
+ }
+ do_check_eq(sum, expectedSum);
+ // check all pref names have been removed from the array
+ do_check_eq(expectedNames.length, 0);
+}
diff --git a/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.js
new file mode 100644
index 0000000000..24a86bcc06
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/test_unusedGroupsAndSettings.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/. */
+
+var cps = new ContentPrefInstance(null);
+
+function run_test() {
+ var uri1 = ContentPrefTest.getURI("http://www.domain1.com/");
+ var uri2 = ContentPrefTest.getURI("http://foo.domain1.com/");
+ var uri3 = ContentPrefTest.getURI("http://domain1.com/");
+ var uri4 = ContentPrefTest.getURI("http://www.domain2.com/");
+
+ cps.setPref(uri1, "one", 1);
+ cps.setPref(uri1, "two", 2);
+ cps.setPref(uri2, "one", 4);
+ cps.setPref(uri3, "three", 8);
+ cps.setPref(uri4, "two", 16);
+
+ cps.removePref(uri3, "three"); // uri3 should be removed now
+ checkForUnusedGroups();
+ checkForUnusedSettings();
+
+ cps.removePrefsByName("two"); // uri4 should be removed now
+ checkForUnusedGroups();
+ checkForUnusedSettings();
+
+ cps.removeGroupedPrefs();
+ checkForUnusedGroups();
+ checkForUnusedSettings();
+}
+
+function checkForUnusedGroups() {
+ var stmt = cps.DBConnection.createStatement(`
+ SELECT COUNT(*) AS count FROM groups
+ WHERE id NOT IN (SELECT DISTINCT groupID FROM prefs)
+ `);
+ stmt.executeStep();
+ do_check_eq(0, stmt.row.count);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function checkForUnusedSettings() {
+ var stmt = cps.DBConnection.createStatement(`
+ SELECT COUNT(*) AS count FROM settings
+ WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
+ `);
+ stmt.executeStep();
+ do_check_eq(0, stmt.row.count);
+ stmt.reset();
+ stmt.finalize();
+}
diff --git a/toolkit/components/contentprefs/tests/unit/xpcshell.ini b/toolkit/components/contentprefs/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..cbae178b1b
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = head_contentPrefs.js
+tail = tail_contentPrefs.js
+
+[test_bug248970.js]
+[test_bug503971.js]
+[test_bug679784.js]
+[test_contentPrefs.js]
+[test_contentPrefsCache.js]
+[test_getPrefAsync.js]
+[test_stringGroups.js]
+[test_unusedGroupsAndSettings.js]
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js b/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm
new file mode 100644
index 0000000000..ac878c28cd
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/AsyncRunner.jsm
@@ -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/. */
+
+var EXPORTED_SYMBOLS = [
+ "AsyncRunner",
+];
+
+const { interfaces: Ci, classes: Cc } = Components;
+
+function AsyncRunner(callbacks) {
+ this._callbacks = callbacks;
+ this._iteratorQueue = [];
+
+ // This catches errors reported to the console, e.g., via Cu.reportError.
+ Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService).
+ registerListener(this);
+}
+
+AsyncRunner.prototype = {
+
+ appendIterator: function AR_appendIterator(iter) {
+ this._iteratorQueue.push(iter);
+ },
+
+ next: function AR_next(arg) {
+ if (!this._iteratorQueue.length) {
+ this.destroy();
+ this._callbacks.done();
+ return;
+ }
+
+ try {
+ var { done, value } = this._iteratorQueue[0].next(arg);
+ if (done) {
+ this._iteratorQueue.shift();
+ this.next();
+ return;
+ }
+ }
+ catch (err) {
+ this._callbacks.error(err);
+ }
+
+ // val is truthy => call next
+ // val is an iterator => prepend it to the queue and start on it
+ if (value) {
+ if (typeof(value) != "boolean")
+ this._iteratorQueue.unshift(value);
+ this.next();
+ }
+ },
+
+ destroy: function AR_destroy() {
+ Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService).
+ unregisterListener(this);
+ this.destroy = function AR_alreadyDestroyed() {};
+ },
+
+ observe: function AR_consoleServiceListener(msg) {
+ if (msg instanceof Ci.nsIScriptError &&
+ !(msg.flags & Ci.nsIScriptError.warningFlag))
+ {
+ this._callbacks.consoleError(msg);
+ }
+ },
+};
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/head.js b/toolkit/components/contentprefs/tests/unit_cps2/head.js
new file mode 100644
index 0000000000..b86abe208a
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/head.js
@@ -0,0 +1,401 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { interfaces: Ci, classes: Cc, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var cps;
+var asyncRunner;
+var next;
+
+(function init() {
+ // There has to be a profile directory before the CPS service is gotten.
+ do_get_profile();
+})();
+
+function runAsyncTests(tests, dontResetBefore = false) {
+ do_test_pending();
+
+ cps = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+
+ let s = {};
+ Cu.import("resource://test/AsyncRunner.jsm", s);
+ asyncRunner = new s.AsyncRunner({
+ done: do_test_finished,
+ error: function (err) {
+ // xpcshell test functions like equal throw NS_ERROR_ABORT on
+ // failure. Ignore those and catch only uncaught exceptions.
+ if (err !== Cr.NS_ERROR_ABORT) {
+ if (err.stack) {
+ err = err + "\n\nTraceback (most recent call first):\n" + err.stack +
+ "\nUseless do_throw stack:";
+ }
+ do_throw(err);
+ }
+ },
+ consoleError: function (scriptErr) {
+ // Previously, this code checked for console errors related to the test,
+ // and treated them as failures. This was problematic, because our current
+ // very-broken exception reporting machinery in XPCWrappedJSClass reports
+ // errors to the console even if there's actually JS on the stack above
+ // that will catch them. And a lot of the tests here intentionally trigger
+ // error conditions on the JS-implemented XPCOM component (see erroneous()
+ // in test_getSubdomains.js, for example). In the old world, we got lucky,
+ // and the errors were never reported to the console due to happenstantial
+ // JSContext reasons that aren't really worth going into.
+ //
+ // So. We make sure to dump this stuff so that it shows up in the logs, but
+ // don't turn them into duplicate failures of the exception that was already
+ // propagated to the caller.
+ dump("AsyncRunner.jsm observed console error: " + scriptErr + "\n");
+ }
+ });
+
+ next = asyncRunner.next.bind(asyncRunner);
+
+ do_register_cleanup(function () {
+ asyncRunner.destroy();
+ asyncRunner = null;
+ });
+
+ tests.forEach(function (test) {
+ function* gen() {
+ do_print("Running " + test.name);
+ yield test();
+ yield reset();
+ }
+ asyncRunner.appendIterator(gen());
+ });
+
+ // reset() ends up calling asyncRunner.next(), starting the tests.
+ if (dontResetBefore) {
+ next();
+ } else {
+ reset();
+ }
+}
+
+function makeCallback(callbacks, success = null) {
+ callbacks = callbacks || {};
+ if (!callbacks.handleError) {
+ callbacks.handleError = function (error) {
+ do_throw("handleError call was not expected, error: " + error);
+ };
+ }
+ if (!callbacks.handleResult) {
+ callbacks.handleResult = function() {
+ do_throw("handleResult call was not expected");
+ };
+ }
+ if (!callbacks.handleCompletion)
+ callbacks.handleCompletion = function (reason) {
+ equal(reason, Ci.nsIContentPrefCallback2.COMPLETE_OK);
+ if (success) {
+ success();
+ } else {
+ next();
+ }
+ };
+ return callbacks;
+}
+
+function do_check_throws(fn) {
+ let threw = false;
+ try {
+ fn();
+ }
+ catch (err) {
+ threw = true;
+ }
+ ok(threw);
+}
+
+function sendMessage(msg, callback) {
+ let obj = callback || {};
+ let ref = Cu.getWeakReference(obj);
+ cps.QueryInterface(Ci.nsIObserver).observe(ref, "test:" + msg, null);
+ return "value" in obj ? obj.value : undefined;
+}
+
+function reset() {
+ sendMessage("reset", next);
+}
+
+function setWithDate(group, name, val, timestamp, context) {
+ function updateDate() {
+ let db = sendMessage("db");
+ let stmt = db.createAsyncStatement(`
+ UPDATE prefs SET timestamp = :timestamp
+ WHERE
+ settingID = (SELECT id FROM settings WHERE name = :name)
+ AND groupID = (SELECT id FROM groups WHERE name = :group)
+ `);
+ stmt.params.timestamp = timestamp / 1000;
+ stmt.params.name = name;
+ stmt.params.group = group;
+
+ stmt.executeAsync({
+ handleCompletion: function (reason) {
+ next();
+ },
+ handleError: function (err) {
+ do_throw(err);
+ }
+ });
+ stmt.finalize();
+ }
+
+ cps.set(group, name, val, context, makeCallback(null, updateDate));
+}
+
+function getDate(group, name, context) {
+ let db = sendMessage("db");
+ let stmt = db.createAsyncStatement(`
+ SELECT timestamp FROM prefs
+ WHERE
+ settingID = (SELECT id FROM settings WHERE name = :name)
+ AND groupID = (SELECT id FROM groups WHERE name = :group)
+ `);
+ stmt.params.name = name;
+ stmt.params.group = group;
+
+ let res;
+ stmt.executeAsync({
+ handleResult: function (results) {
+ let row = results.getNextRow();
+ res = row.getResultByName("timestamp");
+ },
+ handleCompletion: function (reason) {
+ next(res * 1000);
+ },
+ handleError: function (err) {
+ do_throw(err);
+ }
+ });
+ stmt.finalize();
+}
+
+function set(group, name, val, context) {
+ cps.set(group, name, val, context, makeCallback());
+}
+
+function setGlobal(name, val, context) {
+ cps.setGlobal(name, val, context, makeCallback());
+}
+
+function prefOK(actual, expected, strict) {
+ ok(actual instanceof Ci.nsIContentPref);
+ equal(actual.domain, expected.domain);
+ equal(actual.name, expected.name);
+ if (strict)
+ strictEqual(actual.value, expected.value);
+ else
+ equal(actual.value, expected.value);
+}
+
+function* getOK(args, expectedVal, expectedGroup, strict) {
+ if (args.length == 2)
+ args.push(undefined);
+ let expectedPrefs = expectedVal === undefined ? [] :
+ [{ domain: expectedGroup || args[0],
+ name: args[1],
+ value: expectedVal }];
+ yield getOKEx("getByDomainAndName", args, expectedPrefs, strict);
+}
+
+function* getSubdomainsOK(args, expectedGroupValPairs) {
+ if (args.length == 2)
+ args.push(undefined);
+ let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) {
+ return { domain: group, name: args[1], value: val };
+ });
+ yield getOKEx("getBySubdomainAndName", args, expectedPrefs);
+}
+
+function* getGlobalOK(args, expectedVal) {
+ if (args.length == 1)
+ args.push(undefined);
+ let expectedPrefs = expectedVal === undefined ? [] :
+ [{ domain: null, name: args[0], value: expectedVal }];
+ yield getOKEx("getGlobal", args, expectedPrefs);
+}
+
+function* getOKEx(methodName, args, expectedPrefs, strict, context) {
+ let actualPrefs = [];
+ args.push(makeCallback({
+ handleResult: pref => actualPrefs.push(pref)
+ }));
+ yield cps[methodName].apply(cps, args);
+ arraysOfArraysOK([actualPrefs], [expectedPrefs], function (actual, expected) {
+ prefOK(actual, expected, strict);
+ });
+}
+
+function getCachedOK(args, expectedIsCached, expectedVal, expectedGroup,
+ strict) {
+ if (args.length == 2)
+ args.push(undefined);
+ let expectedPref = !expectedIsCached ? null : {
+ domain: expectedGroup || args[0],
+ name: args[1],
+ value: expectedVal
+ };
+ getCachedOKEx("getCachedByDomainAndName", args, expectedPref, strict);
+}
+
+function getCachedSubdomainsOK(args, expectedGroupValPairs) {
+ if (args.length == 2)
+ args.push(undefined);
+ let len = {};
+ args.push(len);
+ let actualPrefs = cps.getCachedBySubdomainAndName.apply(cps, args);
+ actualPrefs = actualPrefs.sort(function (a, b) {
+ return a.domain.localeCompare(b.domain);
+ });
+ equal(actualPrefs.length, len.value);
+ let expectedPrefs = expectedGroupValPairs.map(function ([group, val]) {
+ return { domain: group, name: args[1], value: val };
+ });
+ arraysOfArraysOK([actualPrefs], [expectedPrefs], prefOK);
+}
+
+function getCachedGlobalOK(args, expectedIsCached, expectedVal) {
+ if (args.length == 1)
+ args.push(undefined);
+ let expectedPref = !expectedIsCached ? null : {
+ domain: null,
+ name: args[0],
+ value: expectedVal
+ };
+ getCachedOKEx("getCachedGlobal", args, expectedPref);
+}
+
+function getCachedOKEx(methodName, args, expectedPref, strict) {
+ let actualPref = cps[methodName].apply(cps, args);
+ if (expectedPref)
+ prefOK(actualPref, expectedPref, strict);
+ else
+ strictEqual(actualPref, null);
+}
+
+function arraysOK(actual, expected, cmp) {
+ if (actual.length != expected.length) {
+ do_throw("Length is not equal: " + JSON.stringify(actual) + "==" + JSON.stringify(expected));
+ } else {
+ actual.forEach(function (actualElt, j) {
+ let expectedElt = expected[j];
+ cmp(actualElt, expectedElt);
+ });
+ }
+}
+
+function arraysOfArraysOK(actual, expected, cmp) {
+ cmp = cmp || equal;
+ arraysOK(actual, expected, function (act, exp) {
+ arraysOK(act, exp, cmp)
+ });
+}
+
+function dbOK(expectedRows) {
+ let db = sendMessage("db");
+ let stmt = db.createAsyncStatement(`
+ SELECT groups.name AS grp, settings.name AS name, prefs.value AS value
+ FROM prefs
+ LEFT JOIN groups ON groups.id = prefs.groupID
+ LEFT JOIN settings ON settings.id = prefs.settingID
+ UNION
+
+ /*
+ These second two SELECTs get the rows of the groups and settings tables
+ that aren't referenced by the prefs table. Neither should return any
+ rows if the component is working properly.
+ */
+ SELECT groups.name AS grp, NULL AS name, NULL AS value
+ FROM groups
+ WHERE id NOT IN (
+ SELECT DISTINCT groupID
+ FROM prefs
+ WHERE groupID NOTNULL
+ )
+ UNION
+ SELECT NULL AS grp, settings.name AS name, NULL AS value
+ FROM settings
+ WHERE id NOT IN (
+ SELECT DISTINCT settingID
+ FROM prefs
+ WHERE settingID NOTNULL
+ )
+
+ ORDER BY value ASC, grp ASC, name ASC
+ `);
+
+ let actualRows = [];
+ let cols = ["grp", "name", "value"];
+
+ db.executeAsync([stmt], 1, {
+ handleCompletion: function (reason) {
+ arraysOfArraysOK(actualRows, expectedRows);
+ next();
+ },
+ handleResult: function (results) {
+ let row = null;
+ while (row = results.getNextRow()) {
+ actualRows.push(cols.map(c => row.getResultByName(c)));
+ }
+ },
+ handleError: function (err) {
+ do_throw(err);
+ }
+ });
+ stmt.finalize();
+}
+
+function on(event, names, dontRemove) {
+ let args = {
+ reset: function () {
+ for (let prop in this) {
+ if (Array.isArray(this[prop]))
+ this[prop].splice(0, this[prop].length);
+ }
+ },
+ };
+
+ let observers = {};
+
+ names.forEach(function (name) {
+ let obs = {};
+ ["onContentPrefSet", "onContentPrefRemoved"].forEach(function (meth) {
+ obs[meth] = () => do_throw(meth + " should not be called");
+ });
+ obs["onContentPref" + event] = function () {
+ args[name].push(Array.slice(arguments));
+ };
+ observers[name] = obs;
+ args[name] = [];
+ args[name].observer = obs;
+ cps.addObserverForName(name, obs);
+ });
+
+ do_execute_soon(function () {
+ if (!dontRemove)
+ names.forEach(n => cps.removeObserverForName(n, observers[n]));
+ next(args);
+ });
+}
+
+function schemaVersionIs(expectedVersion) {
+ let db = sendMessage("db");
+ equal(db.schemaVersion, expectedVersion);
+}
+
+function wait() {
+ do_execute_soon(next);
+}
+
+function observerArgsOK(actualArgs, expectedArgs) {
+ notEqual(actualArgs, undefined);
+ arraysOfArraysOK(actualArgs, expectedArgs);
+}
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js
new file mode 100644
index 0000000000..2ec3d6878b
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_extractDomain.js
@@ -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/. */
+
+function run_test() {
+ let tests = {
+ "http://example.com": "example.com",
+ "http://example.com/": "example.com",
+ "http://example.com/foo/bar/baz": "example.com",
+ "http://subdomain.example.com/foo/bar/baz": "subdomain.example.com",
+ "http://qix.quux.example.com/foo/bar/baz": "qix.quux.example.com",
+ "file:///home/foo/bar": "file:///home/foo/bar",
+ "not a url": "not a url",
+ };
+ let cps = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+ for (let url in tests) {
+ do_check_eq(cps.extractDomain(url), tests[url]);
+ }
+}
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.js
new file mode 100644
index 0000000000..33a965b7f7
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCached.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/. */
+
+function run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ getCachedOK(["a.com", "foo"], false, undefined);
+ getCachedGlobalOK(["foo"], false, undefined);
+ yield true;
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["http://a.com/huh", "foo"], true, 1, "a.com");
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+
+ yield set("a.com", "bar", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+
+ yield setGlobal("foo", 3);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+
+ yield setGlobal("bar", 4);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.a.com", "foo"], true, 2);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ getCachedOK(["a.com", "foo", context], true, 6);
+ getCachedOK(["a.com", "bar", context], true, 2);
+ getCachedGlobalOK(["foo", context], true, 7);
+ getCachedGlobalOK(["bar", context], true, 4);
+ getCachedOK(["b.com", "foo", context], true, 5);
+
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ getCachedOK(["b.com", "foo"], true, 5);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.getCachedByDomainAndName(null, "foo", null));
+ do_check_throws(() => cps.getCachedByDomainAndName("", "foo", null));
+ do_check_throws(() => cps.getCachedByDomainAndName("a.com", "", null));
+ do_check_throws(() => cps.getCachedByDomainAndName("a.com", null, null));
+ do_check_throws(() => cps.getCachedGlobal("", null));
+ do_check_throws(() => cps.getCachedGlobal(null, null));
+ yield true;
+ },
+
+ function* casts() {
+ // SQLite casts booleans to integers. This makes sure the values stored in
+ // the cache are the same as the casted values in the database.
+
+ yield set("a.com", "foo", false);
+ yield getOK(["a.com", "foo"], 0, "a.com", true);
+ getCachedOK(["a.com", "foo"], true, 0, "a.com", true);
+
+ yield set("a.com", "bar", true);
+ yield getOK(["a.com", "bar"], 1, "a.com", true);
+ getCachedOK(["a.com", "bar"], true, 1, "a.com", true);
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
new file mode 100644
index 0000000000..9f2599708f
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getCachedSubdomains.js
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ yield true;
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+ yield set("a.com", "bar", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+ yield setGlobal("foo", 3);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+
+ yield setGlobal("bar", 4);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]);
+ getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+ },
+
+ function* populateViaGet() {
+ yield cps.getByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+ yield cps.getGlobal("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ },
+
+ function* populateViaGetSubdomains() {
+ yield cps.getBySubdomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ },
+
+ function* populateViaRemove() {
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+
+ yield cps.removeBySubdomainAndName("b.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+
+ yield cps.removeGlobal("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+
+ yield set("a.com", "foo", 1);
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+
+ yield set("a.com", "foo", 2);
+ yield set("b.a.com", "foo", 3);
+ yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"],
+ [["a.com", undefined], ["b.a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]);
+
+ yield setGlobal("foo", 4);
+ yield cps.removeGlobal("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"],
+ [["a.com", undefined], ["b.a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedSubdomainsOK(["b.a.com", "foo"], [["b.a.com", undefined]]);
+ },
+
+ function* populateViaRemoveByDomain() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield set("b.a.com", "foo", 3);
+ yield set("b.a.com", "bar", 4);
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"],
+ [["a.com", undefined], ["b.a.com", 3]]);
+ getCachedSubdomainsOK(["a.com", "bar"],
+ [["a.com", undefined], ["b.a.com", 4]]);
+
+ yield set("a.com", "foo", 5);
+ yield set("a.com", "bar", 6);
+ yield cps.removeBySubdomain("a.com", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"],
+ [["a.com", undefined], ["b.a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"],
+ [["a.com", undefined], ["b.a.com", undefined]]);
+
+ yield setGlobal("foo", 7);
+ yield setGlobal("bar", 8);
+ yield cps.removeAllGlobals(null, makeCallback());
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, undefined);
+ },
+
+ function* populateViaRemoveAllDomains() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield set("b.com", "foo", 3);
+ yield set("b.com", "bar", 4);
+ yield cps.removeAllDomains(null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", undefined]]);
+ getCachedSubdomainsOK(["b.com", "bar"], [["b.com", undefined]]);
+ },
+
+ function* populateViaRemoveByName() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield cps.removeByName("foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, 4);
+
+ yield cps.removeByName("bar", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", undefined]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", undefined]]);
+ getCachedGlobalOK(["foo"], true, undefined);
+ getCachedGlobalOK(["bar"], true, undefined);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ getCachedSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+ getCachedSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+ getCachedGlobalOK(["foo", context], true, 7);
+ getCachedGlobalOK(["bar", context], true, 4);
+ getCachedSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+ getCachedSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ getCachedSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ getCachedSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.getCachedBySubdomainAndName(null, "foo", null));
+ do_check_throws(() => cps.getCachedBySubdomainAndName("", "foo", null));
+ do_check_throws(() => cps.getCachedBySubdomainAndName("a.com", "", null));
+ do_check_throws(() => cps.getCachedBySubdomainAndName("a.com", null, null));
+ yield true;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.js
new file mode 100644
index 0000000000..d08d6fe695
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_getSubdomains.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/. */
+
+function run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* get_nonexistent() {
+ yield getSubdomainsOK(["a.com", "foo"], []);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["http://a.com/huh", "foo"], [["a.com", 1]]);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+
+ yield set("a.com", "bar", 2);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+
+ yield setGlobal("foo", 3);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1], ["b.a.com", 2]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield getSubdomainsOK(["a.com", "foo", context], [["a.com", 6]]);
+ yield getSubdomainsOK(["a.com", "bar", context], [["a.com", 2]]);
+ yield getSubdomainsOK(["b.com", "foo", context], [["b.com", 5]]);
+
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 1]]);
+ yield getSubdomainsOK(["a.com", "bar"], [["a.com", 2]]);
+ yield getSubdomainsOK(["b.com", "foo"], [["b.com", 5]]);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.getBySubdomainAndName(null, "foo", null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("", "foo", null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("a.com", "", null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("a.com", null, null, {}));
+ do_check_throws(() => cps.getBySubdomainAndName("a.com", "foo", null, null));
+ yield true;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.js
new file mode 100644
index 0000000000..85d23e3558
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_migrationToSchema4.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/. */
+
+// Dump of version we migrate from
+var schema_version3 = `
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+ CREATE TABLE groups (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
+ INSERT INTO "groups" VALUES(1,'foo.com');
+ INSERT INTO "groups" VALUES(2,'bar.com');
+
+ CREATE TABLE settings (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
+ INSERT INTO "settings" VALUES(1,'zoom-setting');
+ INSERT INTO "settings" VALUES(2,'dir-setting');
+
+ CREATE TABLE prefs (id INTEGER PRIMARY KEY, groupID INTEGER REFERENCES groups(id), settingID INTEGER NOT NULL REFERENCES settings(id), value BLOB);
+ INSERT INTO "prefs" VALUES(1,1,1,0.5);
+ INSERT INTO "prefs" VALUES(2,1,2,'/download/dir');
+ INSERT INTO "prefs" VALUES(3,2,1,0.3);
+ INSERT INTO "prefs" VALUES(4,NULL,1,0.1);
+
+ CREATE INDEX groups_idx ON groups(name);
+ CREATE INDEX settings_idx ON settings(name);
+ CREATE INDEX prefs_idx ON prefs(groupID, settingID);
+COMMIT;`;
+
+function prepareVersion3Schema(callback) {
+ var dirService = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+
+ var dbFile = dirService.get("ProfD", Ci.nsIFile);
+ dbFile.append("content-prefs.sqlite");
+
+ var dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ ok(!dbFile.exists(), "Db should not exist yet.");
+
+ var dbConnection = dbService.openDatabase(dbFile);
+ equal(dbConnection.schemaVersion, 0);
+
+ dbConnection.executeSimpleSQL(schema_version3);
+ dbConnection.schemaVersion = 3;
+
+ dbConnection.close();
+}
+
+function run_test() {
+ prepareVersion3Schema();
+ runAsyncTests(tests, true);
+}
+
+
+// WARNING: Database will reset after every test. This limitation comes from
+// the fact that we ContentPrefService constructor is run only once per test file
+// and so migration will be run only once.
+var tests = [
+ function* testMigration() {
+ // Test migrated db content.
+ schemaVersionIs(4);
+ let dbExpectedState = [
+ [null, "zoom-setting", 0.1],
+ ["bar.com", "zoom-setting", 0.3],
+ ["foo.com", "zoom-setting", 0.5],
+ ["foo.com", "dir-setting", "/download/dir"],
+ ];
+ yield dbOK(dbExpectedState);
+
+ // Migrated fields should have timestamp set to 0.
+ yield cps.removeAllDomainsSince(1000, null, makeCallback());
+ yield dbOK(dbExpectedState);
+
+ yield cps.removeAllDomainsSince(0, null, makeCallback());
+ yield dbOK([[null, "zoom-setting", 0.1]]);
+
+ // Test that dates are present after migration (column is added).
+ const timestamp = 1234;
+ yield setWithDate("a.com", "pref-name", "val", timestamp);
+ let actualTimestamp = yield getDate("a.com", "pref-name");
+ equal(actualTimestamp, timestamp);
+ }
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.js
new file mode 100644
index 0000000000..c48918cd92
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_observers.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/. */
+
+let global = this;
+
+function run_test() {
+ var allTests = [];
+ for (var i = 0; i < tests.length; i++) {
+ // Generate two wrappers of each test function that invoke the original test with an
+ // appropriate privacy context.
+ var pub = eval('var f = function* ' + tests[i].name + '() { yield tests[' + i + ']({ usePrivateBrowsing: false }); }; f');
+ var priv = eval('var f = function* ' + tests[i].name + '_private() { yield tests[' + i + ']({ usePrivateBrowsing: true }); }; f');
+ allTests.push(pub);
+ allTests.push(priv);
+ }
+ allTests = allTests.concat(specialTests);
+ runAsyncTests(allTests);
+}
+
+var tests = [
+
+ function* observerForName_set(context) {
+ yield set("a.com", "foo", 1, context);
+ let args = yield on("Set", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+
+ yield setGlobal("foo", 2, context);
+ args = yield on("Set", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [[null, "foo", 2, context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", 2, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* observerForName_remove(context) {
+ yield set("a.com", "foo", 1, context);
+ yield setGlobal("foo", 2, context);
+
+ yield cps.removeByDomainAndName("a.com", "bogus", context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeGlobal("foo", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* observerForName_removeByDomain(context) {
+ yield set("a.com", "foo", 1, context);
+ yield set("b.a.com", "bar", 2, context);
+ yield setGlobal("foo", 3, context);
+
+ yield cps.removeByDomain("bogus", context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeBySubdomain("a.com", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing], ["b.a.com", "bar", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, [["b.a.com", "bar", context.usePrivateBrowsing]]);
+
+ yield cps.removeAllGlobals(context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [[null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* observerForName_removeAllDomains(context) {
+ yield set("a.com", "foo", 1, context);
+ yield setGlobal("foo", 2, context);
+ yield set("b.com", "bar", 3, context);
+
+ yield cps.removeAllDomains(context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing], ["b.com", "bar", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, [["b.com", "bar", context.usePrivateBrowsing]]);
+ },
+
+ function* observerForName_removeByName(context) {
+ yield set("a.com", "foo", 1, context);
+ yield set("a.com", "bar", 2, context);
+ yield setGlobal("foo", 3, context);
+
+ yield cps.removeByName("bogus", context, makeCallback());
+ let args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+
+ yield cps.removeByName("foo", context, makeCallback());
+ args = yield on("Removed", ["foo", null, "bar"]);
+ observerArgsOK(args.foo, [["a.com", "foo", context.usePrivateBrowsing], [null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.null, [["a.com", "foo", context.usePrivateBrowsing], [null, "foo", context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ },
+
+ function* removeObserverForName(context) {
+ let args = yield on("Set", ["foo", null, "bar"], true);
+
+ cps.removeObserverForName("foo", args.foo.observer);
+ yield set("a.com", "foo", 1, context);
+ yield wait();
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, [["a.com", "foo", 1, context.usePrivateBrowsing]]);
+ observerArgsOK(args.bar, []);
+ args.reset();
+
+ cps.removeObserverForName(null, args.null.observer);
+ yield set("a.com", "foo", 2, context);
+ yield wait();
+ observerArgsOK(args.foo, []);
+ observerArgsOK(args.null, []);
+ observerArgsOK(args.bar, []);
+ args.reset();
+ },
+];
+
+// These tests are for functionality that doesn't behave the same way in private and public
+// contexts, so the expected results cannot be automatically generated like the previous tests.
+var specialTests = [
+ function* observerForName_removeAllDomainsSince() {
+ yield setWithDate("a.com", "foo", 1, 100, null);
+ yield setWithDate("b.com", "foo", 2, 200, null);
+ yield setWithDate("c.com", "foo", 3, 300, null);
+
+ yield setWithDate("a.com", "bar", 1, 0, null);
+ yield setWithDate("b.com", "bar", 2, 100, null);
+ yield setWithDate("c.com", "bar", 3, 200, null);
+ yield setGlobal("foo", 2, null);
+
+ yield cps.removeAllDomainsSince(200, null, makeCallback());
+
+ let args = yield on("Removed", ["foo", "bar", null]);
+
+ observerArgsOK(args.foo, [["b.com", "foo", false], ["c.com", "foo", false]]);
+ observerArgsOK(args.bar, [["c.com", "bar", false]]);
+ observerArgsOK(args.null, [["b.com", "foo", false], ["c.com", "bar", false], ["c.com", "foo", false]]);
+ },
+
+ function* observerForName_removeAllDomainsSince_private() {
+ let context = {usePrivateBrowsing: true};
+ yield setWithDate("a.com", "foo", 1, 100, context);
+ yield setWithDate("b.com", "foo", 2, 200, context);
+ yield setWithDate("c.com", "foo", 3, 300, context);
+
+ yield setWithDate("a.com", "bar", 1, 0, context);
+ yield setWithDate("b.com", "bar", 2, 100, context);
+ yield setWithDate("c.com", "bar", 3, 200, context);
+ yield setGlobal("foo", 2, context);
+
+ yield cps.removeAllDomainsSince(200, context, makeCallback());
+
+ let args = yield on("Removed", ["foo", "bar", null]);
+
+ observerArgsOK(args.foo, [["a.com", "foo", true], ["b.com", "foo", true], ["c.com", "foo", true]]);
+ observerArgsOK(args.bar, [["a.com", "bar", true], ["b.com", "bar", true], ["c.com", "bar", true]]);
+ observerArgsOK(args.null, [["a.com", "foo", true], ["a.com", "bar", true],
+ ["b.com", "foo", true], ["b.com", "bar", true],
+ ["c.com", "foo", true], ["c.com", "bar", true]]);
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
new file mode 100644
index 0000000000..9853293fc7
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_remove.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield set("a.com", "foo", 1);
+ yield setGlobal("foo", 2);
+
+ yield cps.removeByDomainAndName("a.com", "bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeBySubdomainAndName("a.com", "bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeGlobal("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeByDomainAndName("bogus", "bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+
+ yield set("a.com", "foo", 2);
+ yield cps.removeByDomainAndName("http://a.com/huh", "foo", null,
+ makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+
+ yield cps.removeGlobal("foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+
+ yield cps.removeByDomainAndName("a.com", "bar", null, makeCallback());
+ yield dbOK([
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+
+ yield cps.removeGlobal("bar", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["b.a.com", "foo", 2],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+ yield set("a.com", "foo", 3);
+ yield cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], []);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("a.com", "foo", 4);
+ yield set("b.a.com", "foo", 5);
+ yield cps.removeByDomainAndName("b.a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("b.a.com", "foo", 6);
+ yield cps.removeBySubdomainAndName("b.a.com", "foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield setGlobal("qux", 5);
+ yield set("b.com", "foo", 6);
+ yield set("b.com", "bar", 7);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 8, context);
+ yield setGlobal("foo", 9, context);
+ yield cps.removeByDomainAndName("a.com", "foo", context, makeCallback());
+ yield cps.removeGlobal("foo", context, makeCallback());
+ yield cps.removeGlobal("qux", context, makeCallback());
+ yield cps.removeByDomainAndName("b.com", "foo", context, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ["b.com", "bar", 7],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], 2);
+ yield getGlobalOK(["foo", context], undefined);
+ yield getGlobalOK(["bar", context], 4);
+ yield getGlobalOK(["qux", context], undefined);
+ yield getOK(["b.com", "foo", context], undefined);
+ yield getOK(["b.com", "bar", context], 7);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+ yield getGlobalOK(["qux"], undefined);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], 7);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeByDomainAndName(null, "foo", null));
+ do_check_throws(() => cps.removeByDomainAndName("", "foo", null));
+ do_check_throws(() => cps.removeByDomainAndName("a.com", "foo", null,
+ "bogus"));
+ do_check_throws(() => cps.removeBySubdomainAndName(null, "foo",
+ null));
+ do_check_throws(() => cps.removeBySubdomainAndName("", "foo", null));
+ do_check_throws(() => cps.removeBySubdomainAndName("a.com", "foo",
+ null, "bogus"));
+ do_check_throws(() => cps.removeGlobal("", null));
+ do_check_throws(() => cps.removeGlobal(null, null));
+ do_check_throws(() => cps.removeGlobal("foo", null, "bogus"));
+ yield true;
+ },
+
+ function* removeByDomainAndName_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ getCachedOK(["a.com", "foo"], true, 1);
+ cps.removeByDomainAndName("a.com", "foo", null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ yield;
+ },
+
+ function* removeBySubdomainAndName_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]);
+ cps.removeBySubdomainAndName("a.com", "foo", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ yield;
+ },
+
+ function* removeGlobal_invalidateCache() {
+ yield setGlobal("foo", 1);
+ getCachedGlobalOK(["foo"], true, 1);
+ cps.removeGlobal("foo", null, makeCallback());
+ getCachedGlobalOK(["foo"], false);
+ yield;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.js
new file mode 100644
index 0000000000..63e1b0552e
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomains.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/. */
+
+function run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield setGlobal("foo", 1);
+ yield cps.removeAllDomains(null, makeCallback());
+ yield dbOK([
+ [null, "foo", 1],
+ ]);
+ yield getGlobalOK(["foo"], 1);
+ },
+
+ function* domains() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeAllDomains(null, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield cps.removeAllDomains(context, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], 7);
+ yield getGlobalOK(["bar", context], 4);
+ yield getOK(["b.com", "foo", context], undefined);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeAllDomains(null, "bogus"));
+ yield true;
+ },
+
+ function* invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.com", "bar", 2);
+ yield setGlobal("baz", 3);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.com", "bar"], true, 2);
+ getCachedGlobalOK(["baz"], true, 3);
+ cps.removeAllDomains(null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["b.com", "bar"], false);
+ getCachedGlobalOK(["baz"], true, 3);
+ yield;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js
new file mode 100644
index 0000000000..fa0bf31c37
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeAllDomainsSince.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield setGlobal("foo", 1);
+ yield cps.removeAllDomainsSince(0, null, makeCallback());
+ yield getGlobalOK(["foo"], 1);
+ },
+
+ function* domainsAll() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeAllDomainsSince(0, null, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* domainsWithDate() {
+ yield setWithDate("a.com", "foobar", 0, 0);
+ yield setWithDate("a.com", "foo", 1, 1000);
+ yield setWithDate("a.com", "bar", 2, 4000);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield setWithDate("b.com", "foo", 5, 2000);
+ yield setWithDate("b.com", "bar", 6, 3000);
+ yield setWithDate("b.com", "foobar", 7, 1000);
+
+ yield cps.removeAllDomainsSince(2000, null, makeCallback());
+ yield dbOK([
+ ["a.com", "foobar", 0],
+ ["a.com", "foo", 1],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foobar", 7],
+ ]);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield cps.removeAllDomainsSince(0, context, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], 7);
+ yield getGlobalOK(["bar", context], 4);
+ yield getOK(["b.com", "foo", context], undefined);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeAllDomainsSince(null, "bogus"));
+ yield true;
+ },
+
+ function* invalidateCache() {
+ yield setWithDate("a.com", "foobar", 0, 0);
+ yield setWithDate("a.com", "foo", 1, 1000);
+ yield setWithDate("a.com", "bar", 2, 4000);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield setWithDate("b.com", "foo", 5, 2000);
+ yield setWithDate("b.com", "bar", 6, 3000);
+ yield setWithDate("b.com", "foobar", 7, 1000);
+ cps.removeAllDomainsSince(0, null, makeCallback());
+ getCachedOK(["a.com", "foobar"], false);
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["a.com", "bar"], false);
+ getCachedGlobalOK(["foo"], true, 3);
+ getCachedGlobalOK(["bar"], true, 4);
+ getCachedOK(["b.com", "foo"], false);
+ getCachedOK(["b.com", "bar"], false);
+ getCachedOK(["b.com", "foobar"], false);
+ yield true;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
new file mode 100644
index 0000000000..1cf6bd8f2e
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByDomain.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield set("a.com", "foo", 1);
+ yield setGlobal("foo", 2);
+
+ yield cps.removeByDomain("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+
+ yield cps.removeBySubdomain("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+
+ yield set("a.com", "foo", 2);
+ yield cps.removeByDomain("http://a.com/huh", null, makeCallback());
+ yield dbOK([]);
+ yield getOK(["a.com", "foo"], undefined);
+ },
+
+ function* domains() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ yield dbOK([
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foo", 5],
+ ["b.com", "bar", 6],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], 5);
+ yield getOK(["b.com", "bar"], 6);
+
+ yield cps.removeAllGlobals(null, makeCallback());
+ yield dbOK([
+ ["b.com", "foo", 5],
+ ["b.com", "bar", 6],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], 5);
+ yield getOK(["b.com", "bar"], 6);
+
+ yield cps.removeByDomain("b.com", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield cps.removeByDomain("a.com", null, makeCallback());
+ yield dbOK([
+ ["b.a.com", "foo", 2],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["b.a.com", 2]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], [["b.a.com", 2]]);
+
+ yield set("a.com", "foo", 3);
+ yield cps.removeBySubdomain("a.com", null, makeCallback());
+ yield dbOK([
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], []);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("a.com", "foo", 4);
+ yield set("b.a.com", "foo", 5);
+ yield cps.removeByDomain("b.a.com", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+
+ yield set("b.a.com", "foo", 6);
+ yield cps.removeBySubdomain("b.a.com", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 4],
+ ]);
+ yield getSubdomainsOK(["a.com", "foo"], [["a.com", 4]]);
+ yield getSubdomainsOK(["b.a.com", "foo"], []);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield set("b.com", "foo", 7, context);
+ yield setGlobal("foo", 8, context);
+ yield cps.removeByDomain("a.com", context, makeCallback());
+ yield getOK(["b.com", "foo", context], 7);
+ yield getGlobalOK(["foo", context], 8);
+ yield cps.removeAllGlobals(context, makeCallback());
+ yield dbOK([
+ ["b.com", "foo", 5],
+ ]);
+ yield getOK(["a.com", "foo", context], undefined);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], undefined);
+ yield getGlobalOK(["bar", context], undefined);
+ yield getOK(["b.com", "foo", context], 5);
+
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], 5);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeByDomain(null, null));
+ do_check_throws(() => cps.removeByDomain("", null));
+ do_check_throws(() => cps.removeByDomain("a.com", null, "bogus"));
+ do_check_throws(() => cps.removeBySubdomain(null, null));
+ do_check_throws(() => cps.removeBySubdomain("", null));
+ do_check_throws(() => cps.removeBySubdomain("a.com", null, "bogus"));
+ do_check_throws(() => cps.removeAllGlobals(null, "bogus"));
+ yield true;
+ },
+
+ function* removeByDomain_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["a.com", "bar"], true, 2);
+ cps.removeByDomain("a.com", null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["a.com", "bar"], false);
+ yield;
+ },
+
+ function* removeBySubdomain_invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ getCachedSubdomainsOK(["a.com", "foo"], [
+ ["a.com", 1],
+ ["b.a.com", 2],
+ ]);
+ cps.removeBySubdomain("a.com", null, makeCallback());
+ getCachedSubdomainsOK(["a.com", "foo"], []);
+ yield;
+ },
+
+ function* removeAllGlobals_invalidateCache() {
+ yield setGlobal("foo", 1);
+ yield setGlobal("bar", 2);
+ getCachedGlobalOK(["foo"], true, 1);
+ getCachedGlobalOK(["bar"], true, 2);
+ cps.removeAllGlobals(null, makeCallback());
+ getCachedGlobalOK(["foo"], false);
+ getCachedGlobalOK(["bar"], false);
+ yield;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
new file mode 100644
index 0000000000..fa04656e25
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_removeByName.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* nonexistent() {
+ yield set("a.com", "foo", 1);
+ yield setGlobal("foo", 2);
+
+ yield cps.removeByName("bogus", null, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getGlobalOK(["foo"], 2);
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ yield cps.removeByName("foo", null, makeCallback());
+ yield dbOK([
+ ["a.com", "bar", 2],
+ [null, "bar", 4],
+ ["b.com", "bar", 6],
+ ]);
+ yield getOK(["a.com", "foo"], undefined);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], undefined);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], undefined);
+ yield getOK(["b.com", "bar"], 6);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+ yield set("b.com", "bar", 6);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 7, context);
+ yield setGlobal("foo", 8, context);
+ yield set("b.com", "bar", 9, context);
+ yield cps.removeByName("bar", context, makeCallback());
+ yield dbOK([
+ ["a.com", "foo", 1],
+ [null, "foo", 3],
+ ["b.com", "foo", 5],
+ ]);
+ yield getOK(["a.com", "foo", context], 7);
+ yield getOK(["a.com", "bar", context], undefined);
+ yield getGlobalOK(["foo", context], 8);
+ yield getGlobalOK(["bar", context], undefined);
+ yield getOK(["b.com", "foo", context], 5);
+ yield getOK(["b.com", "bar", context], undefined);
+
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], undefined);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], undefined);
+ yield getOK(["b.com", "foo"], 5);
+ yield getOK(["b.com", "bar"], undefined);
+ },
+
+ function* erroneous() {
+ do_check_throws(() => cps.removeByName("", null));
+ do_check_throws(() => cps.removeByName(null, null));
+ do_check_throws(() => cps.removeByName("foo", null, "bogus"));
+ yield true;
+ },
+
+ function* invalidateCache() {
+ yield set("a.com", "foo", 1);
+ yield set("b.com", "foo", 2);
+ getCachedOK(["a.com", "foo"], true, 1);
+ getCachedOK(["b.com", "foo"], true, 2);
+ cps.removeByName("foo", null, makeCallback());
+ getCachedOK(["a.com", "foo"], false);
+ getCachedOK(["b.com", "foo"], false);
+ yield;
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_service.js b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js
new file mode 100644
index 0000000000..75292063e8
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_service.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ let serv = Cc["@mozilla.org/content-pref/service;1"].
+ getService(Ci.nsIContentPrefService2);
+ do_check_eq(serv.QueryInterface(Ci.nsIContentPrefService2), serv);
+ do_check_eq(serv.QueryInterface(Ci.nsISupports), serv);
+ let val = serv.QueryInterface(Ci.nsIContentPrefService);
+ do_check_true(val instanceof Ci.nsIContentPrefService);
+}
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
new file mode 100644
index 0000000000..b10a05bbc2
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/test_setGet.js
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ runAsyncTests(tests);
+}
+
+var tests = [
+
+ function* get_nonexistent() {
+ yield getOK(["a.com", "foo"], undefined);
+ yield getGlobalOK(["foo"], undefined);
+ },
+
+ function* isomorphicDomains() {
+ yield set("a.com", "foo", 1);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["http://a.com/huh", "foo"], 1, "a.com");
+
+ yield set("http://a.com/huh", "foo", 2);
+ yield dbOK([
+ ["a.com", "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 2);
+ yield getOK(["http://a.com/yeah", "foo"], 2, "a.com");
+ },
+
+ function* names() {
+ yield set("a.com", "foo", 1);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+
+ yield set("a.com", "bar", 2);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+
+ yield setGlobal("foo", 3);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+
+ yield setGlobal("bar", 4);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ },
+
+ function* subdomains() {
+ yield set("a.com", "foo", 1);
+ yield set("b.a.com", "foo", 2);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["b.a.com", "foo", 2],
+ ]);
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["b.a.com", "foo"], 2);
+ },
+
+ function* privateBrowsing() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield setGlobal("foo", 3);
+ yield setGlobal("bar", 4);
+ yield set("b.com", "foo", 5);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("a.com", "foo", 6, context);
+ yield setGlobal("foo", 7, context);
+ yield dbOK([
+ ["a.com", "foo", 1],
+ ["a.com", "bar", 2],
+ [null, "foo", 3],
+ [null, "bar", 4],
+ ["b.com", "foo", 5],
+ ]);
+ yield getOK(["a.com", "foo", context], 6, "a.com");
+ yield getOK(["a.com", "bar", context], 2);
+ yield getGlobalOK(["foo", context], 7);
+ yield getGlobalOK(["bar", context], 4);
+ yield getOK(["b.com", "foo", context], 5);
+
+ yield getOK(["a.com", "foo"], 1);
+ yield getOK(["a.com", "bar"], 2);
+ yield getGlobalOK(["foo"], 3);
+ yield getGlobalOK(["bar"], 4);
+ yield getOK(["b.com", "foo"], 5);
+ },
+
+ function* set_erroneous() {
+ do_check_throws(() => cps.set(null, "foo", 1, null));
+ do_check_throws(() => cps.set("", "foo", 1, null));
+ do_check_throws(() => cps.set("a.com", "", 1, null));
+ do_check_throws(() => cps.set("a.com", null, 1, null));
+ do_check_throws(() => cps.set("a.com", "foo", undefined, null));
+ do_check_throws(() => cps.set("a.com", "foo", 1, null, "bogus"));
+ do_check_throws(() => cps.setGlobal("", 1, null));
+ do_check_throws(() => cps.setGlobal(null, 1, null));
+ do_check_throws(() => cps.setGlobal("foo", undefined, null));
+ do_check_throws(() => cps.setGlobal("foo", 1, null, "bogus"));
+ yield true;
+ },
+
+ function* get_erroneous() {
+ do_check_throws(() => cps.getByDomainAndName(null, "foo", null, {}));
+ do_check_throws(() => cps.getByDomainAndName("", "foo", null, {}));
+ do_check_throws(() => cps.getByDomainAndName("a.com", "", null, {}));
+ do_check_throws(() => cps.getByDomainAndName("a.com", null, null, {}));
+ do_check_throws(() => cps.getByDomainAndName("a.com", "foo", null, null));
+ do_check_throws(() => cps.getGlobal("", null, {}));
+ do_check_throws(() => cps.getGlobal(null, null, {}));
+ do_check_throws(() => cps.getGlobal("foo", null, null));
+ yield true;
+ },
+
+ function* set_invalidateCache() {
+ // (1) Set a pref and wait for it to finish.
+ yield set("a.com", "foo", 1);
+
+ // (2) It should be cached.
+ getCachedOK(["a.com", "foo"], true, 1);
+
+ // (3) Set the pref to a new value but don't wait for it to finish.
+ cps.set("a.com", "foo", 2, null, {
+ handleCompletion: function () {
+ // (6) The pref should be cached after setting it.
+ getCachedOK(["a.com", "foo"], true, 2);
+ },
+ });
+
+ // (4) Group "a.com" and name "foo" should no longer be cached.
+ getCachedOK(["a.com", "foo"], false);
+
+ // (5) Call getByDomainAndName.
+ var fetchedPref;
+ cps.getByDomainAndName("a.com", "foo", null, {
+ handleResult: function (pref) {
+ fetchedPref = pref;
+ },
+ handleCompletion: function () {
+ // (7) Finally, this callback should be called after set's above.
+ do_check_true(!!fetchedPref);
+ do_check_eq(fetchedPref.value, 2);
+ next();
+ },
+ });
+
+ yield;
+ },
+
+ function* get_nameOnly() {
+ yield set("a.com", "foo", 1);
+ yield set("a.com", "bar", 2);
+ yield set("b.com", "foo", 3);
+ yield setGlobal("foo", 4);
+
+ yield getOKEx("getByName", ["foo", undefined], [
+ {"domain": "a.com", "name": "foo", "value": 1},
+ {"domain": "b.com", "name": "foo", "value": 3},
+ {"domain": null, "name": "foo", "value": 4}
+ ]);
+
+ let context = { usePrivateBrowsing: true };
+ yield set("b.com", "foo", 5, context);
+
+ yield getOKEx("getByName", ["foo", context], [
+ {"domain": "a.com", "name": "foo", "value": 1},
+ {"domain": null, "name": "foo", "value": 4},
+ {"domain": "b.com", "name": "foo", "value": 5}
+ ]);
+ },
+
+ function* setSetsCurrentDate() {
+ // Because Date.now() is not guaranteed to be monotonically increasing
+ // we just do here rough sanity check with one minute tolerance.
+ const MINUTE = 60 * 1000;
+ let now = Date.now();
+ let start = now - MINUTE;
+ let end = now + MINUTE;
+ yield set("a.com", "foo", 1);
+ let timestamp = yield getDate("a.com", "foo");
+ ok(start <= timestamp, "Timestamp is not too early (" + start + "<=" + timestamp + ").");
+ ok(timestamp <= end, "Timestamp is not too late (" + timestamp + "<=" + end + ").");
+ },
+];
diff --git a/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
new file mode 100644
index 0000000000..bdbcaf8fd6
--- /dev/null
+++ b/toolkit/components/contentprefs/tests/unit_cps2/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+support-files = AsyncRunner.jsm
+
+[test_service.js]
+[test_setGet.js]
+[test_getSubdomains.js]
+[test_remove.js]
+[test_removeByDomain.js]
+[test_removeAllDomains.js]
+[test_removeByName.js]
+[test_getCached.js]
+[test_getCachedSubdomains.js]
+[test_observers.js]
+[test_extractDomain.js]
+[test_migrationToSchema4.js]
+[test_removeAllDomainsSince.js]
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.jsm b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
new file mode 100644
index 0000000000..6aae3673d2
--- /dev/null
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.jsm
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = ["ContextualIdentityService"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const DEFAULT_TAB_COLOR = "#909090";
+const SAVE_DELAY_MS = 1500;
+
+XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
+ return Services.strings.createBundle("chrome://browser/locale/browser.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
+ return new TextDecoder();
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
+ return new TextEncoder();
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+function _ContextualIdentityService(path) {
+ this.init(path);
+}
+
+_ContextualIdentityService.prototype = {
+ _defaultIdentities: [
+ { userContextId: 1,
+ public: true,
+ icon: "fingerprint",
+ color: "blue",
+ l10nID: "userContextPersonal.label",
+ accessKey: "userContextPersonal.accesskey",
+ telemetryId: 1,
+ },
+ { userContextId: 2,
+ public: true,
+ icon: "briefcase",
+ color: "orange",
+ l10nID: "userContextWork.label",
+ accessKey: "userContextWork.accesskey",
+ telemetryId: 2,
+ },
+ { userContextId: 3,
+ public: true,
+ icon: "dollar",
+ color: "green",
+ l10nID: "userContextBanking.label",
+ accessKey: "userContextBanking.accesskey",
+ telemetryId: 3,
+ },
+ { userContextId: 4,
+ public: true,
+ icon: "cart",
+ color: "pink",
+ l10nID: "userContextShopping.label",
+ accessKey: "userContextShopping.accesskey",
+ telemetryId: 4,
+ },
+ { userContextId: 5,
+ public: false,
+ icon: "",
+ color: "",
+ name: "userContextIdInternal.thumbnail",
+ accessKey: "" },
+ ],
+
+ _identities: null,
+ _openedIdentities: new Set(),
+ _lastUserContextId: 0,
+
+ _path: null,
+ _dataReady: false,
+
+ _saver: null,
+
+ init(path) {
+ this._path = path;
+ this._saver = new DeferredTask(() => this.save(), SAVE_DELAY_MS);
+ AsyncShutdown.profileBeforeChange.addBlocker("ContextualIdentityService: writing data",
+ () => this._saver.finalize());
+
+ this.load();
+ },
+
+ load() {
+ OS.File.read(this._path).then(bytes => {
+ // If synchronous loading happened in the meantime, exit now.
+ if (this._dataReady) {
+ return;
+ }
+
+ try {
+ let data = JSON.parse(gTextDecoder.decode(bytes));
+ if (data.version == 1) {
+ this.resetDefault();
+ }
+ if (data.version != 2) {
+ dump("ERROR - ContextualIdentityService - Unknown version found in " + this._path + "\n");
+ this.loadError(null);
+ return;
+ }
+
+ this._identities = data.identities;
+ this._lastUserContextId = data.lastUserContextId;
+
+ this._dataReady = true;
+ } catch (error) {
+ this.loadError(error);
+ }
+ }, (error) => {
+ this.loadError(error);
+ });
+ },
+
+ resetDefault() {
+ this._identities = this._defaultIdentities;
+ this._lastUserContextId = this._defaultIdentities.length;
+
+ this._dataReady = true;
+
+ this.saveSoon();
+ },
+
+ loadError(error) {
+ if (error != null &&
+ !(error instanceof OS.File.Error && error.becauseNoSuchFile) &&
+ !(error instanceof Components.Exception &&
+ error.result == Cr.NS_ERROR_FILE_NOT_FOUND)) {
+ // Let's report the error.
+ Cu.reportError(error);
+ }
+
+ // If synchronous loading happened in the meantime, exit now.
+ if (this._dataReady) {
+ return;
+ }
+
+ this.resetDefault();
+ },
+
+ saveSoon() {
+ this._saver.arm();
+ },
+
+ save() {
+ let object = {
+ version: 2,
+ lastUserContextId: this._lastUserContextId,
+ identities: this._identities
+ };
+
+ let bytes = gTextEncoder.encode(JSON.stringify(object));
+ return OS.File.writeAtomic(this._path, bytes,
+ { tmpPath: this._path + ".tmp" });
+ },
+
+ create(name, icon, color) {
+ let identity = {
+ userContextId: ++this._lastUserContextId,
+ public: true,
+ icon,
+ color,
+ name
+ };
+
+ this._identities.push(identity);
+ this.saveSoon();
+
+ return Cu.cloneInto(identity, {});
+ },
+
+ update(userContextId, name, icon, color) {
+ let identity = this._identities.find(identity => identity.userContextId == userContextId &&
+ identity.public);
+ if (identity && name) {
+ identity.name = name;
+ identity.color = color;
+ identity.icon = icon;
+ delete identity.l10nID;
+ delete identity.accessKey;
+ this.saveSoon();
+ }
+
+ return !!identity;
+ },
+
+ remove(userContextId) {
+ let index = this._identities.findIndex(i => i.userContextId == userContextId && i.public);
+ if (index == -1) {
+ return false;
+ }
+
+ Services.obs.notifyObservers(null, "clear-origin-attributes-data",
+ JSON.stringify({ userContextId }));
+
+ this._identities.splice(index, 1);
+ this._openedIdentities.delete(userContextId);
+ this.saveSoon();
+
+ return true;
+ },
+
+ ensureDataReady() {
+ if (this._dataReady) {
+ return;
+ }
+
+ try {
+ // This reads the file and automatically detects the UTF-8 encoding.
+ let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ inputStream.init(new FileUtils.File(this._path),
+ FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ try {
+ let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+ let data = json.decodeFromStream(inputStream,
+ inputStream.available());
+ this._identities = data.identities;
+ this._lastUserContextId = data.lastUserContextId;
+
+ this._dataReady = true;
+ } finally {
+ inputStream.close();
+ }
+ } catch (error) {
+ this.loadError(error);
+ return;
+ }
+ },
+
+ getIdentities() {
+ this.ensureDataReady();
+ return Cu.cloneInto(this._identities.filter(info => info.public), {});
+ },
+
+ getPrivateIdentity(name) {
+ this.ensureDataReady();
+ return Cu.cloneInto(this._identities.find(info => !info.public && info.name == name), {});
+ },
+
+ getIdentityFromId(userContextId) {
+ this.ensureDataReady();
+ return Cu.cloneInto(this._identities.find(info => info.userContextId == userContextId &&
+ info.public), {});
+ },
+
+ getUserContextLabel(userContextId) {
+ let identity = this.getIdentityFromId(userContextId);
+ if (!identity || !identity.public) {
+ return "";
+ }
+
+ // We cannot localize the user-created identity names.
+ if (identity.name) {
+ return identity.name;
+ }
+
+ return gBrowserBundle.GetStringFromName(identity.l10nID);
+ },
+
+ setTabStyle(tab) {
+ if (!tab.hasAttribute("usercontextid")) {
+ return;
+ }
+
+ let userContextId = tab.getAttribute("usercontextid");
+ let identity = this.getIdentityFromId(userContextId);
+ tab.setAttribute("data-identity-color", identity ? identity.color : "");
+ },
+
+ countContainerTabs() {
+ let count = 0;
+ this._forEachContainerTab(function() { ++count; });
+ return count;
+ },
+
+ closeAllContainerTabs() {
+ this._forEachContainerTab(function(tab, tabbrowser) {
+ tabbrowser.removeTab(tab);
+ });
+ },
+
+ _forEachContainerTab(callback) {
+ let windowList = Services.wm.getEnumerator("navigator:browser");
+ while (windowList.hasMoreElements()) {
+ let win = windowList.getNext();
+
+ if (win.closed || !win.gBrowser) {
+ continue;
+ }
+
+ let tabbrowser = win.gBrowser;
+ for (let i = tabbrowser.tabContainer.childNodes.length - 1; i >= 0; --i) {
+ let tab = tabbrowser.tabContainer.childNodes[i];
+ if (tab.hasAttribute("usercontextid")) {
+ callback(tab, tabbrowser);
+ }
+ }
+ }
+ },
+
+ telemetry(userContextId) {
+ let identity = this.getIdentityFromId(userContextId);
+
+ // Let's ignore unknown identities for now.
+ if (!identity || !identity.public) {
+ return;
+ }
+
+ if (!this._openedIdentities.has(userContextId)) {
+ this._openedIdentities.add(userContextId);
+ Services.telemetry.getHistogramById("UNIQUE_CONTAINERS_OPENED").add(1);
+ }
+
+ Services.telemetry.getHistogramById("TOTAL_CONTAINERS_OPENED").add(1);
+
+ if (identity.telemetryId) {
+ Services.telemetry.getHistogramById("CONTAINER_USED")
+ .add(identity.telemetryId);
+ }
+ },
+
+ createNewInstanceForTesting(path) {
+ return new _ContextualIdentityService(path);
+ },
+};
+
+let path = OS.Path.join(OS.Constants.Path.profileDir, "containers.json");
+this.ContextualIdentityService = new _ContextualIdentityService(path);
diff --git a/toolkit/components/contextualidentity/moz.build b/toolkit/components/contextualidentity/moz.build
new file mode 100644
index 0000000000..9188421f9e
--- /dev/null
+++ b/toolkit/components/contextualidentity/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/.
+
+EXTRA_JS_MODULES += [
+ 'ContextualIdentityService.jsm',
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
diff --git a/toolkit/components/contextualidentity/tests/unit/test_basic.js b/toolkit/components/contextualidentity/tests/unit/test_basic.js
new file mode 100644
index 0000000000..4d17b9a267
--- /dev/null
+++ b/toolkit/components/contextualidentity/tests/unit/test_basic.js
@@ -0,0 +1,67 @@
+"use strict";
+
+do_get_profile();
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ContextualIdentityService.jsm");
+
+const TEST_STORE_FILE_NAME = "test-containers.json";
+
+let cis;
+
+// Basic tests
+add_task(function() {
+ ok(!!ContextualIdentityService, "ContextualIdentityService exists");
+
+ cis = ContextualIdentityService.createNewInstanceForTesting(TEST_STORE_FILE_NAME);
+ ok(!!cis, "We have our instance of ContextualIdentityService");
+
+ equal(cis.getIdentities().length, 4, "By default, 4 containers.");
+ equal(cis.getIdentityFromId(0), null, "No identity with id 0");
+
+ ok(!!cis.getIdentityFromId(1), "Identity 1 exists");
+ ok(!!cis.getIdentityFromId(2), "Identity 2 exists");
+ ok(!!cis.getIdentityFromId(3), "Identity 3 exists");
+ ok(!!cis.getIdentityFromId(4), "Identity 4 exists");
+});
+
+// Create a new identity
+add_task(function() {
+ equal(cis.getIdentities().length, 4, "By default, 4 containers.");
+
+ let identity = cis.create("New Container", "Icon", "Color");
+ ok(!!identity, "New container created");
+ equal(identity.name, "New Container", "Name matches");
+ equal(identity.icon, "Icon", "Icon matches");
+ equal(identity.color, "Color", "Color matches");
+
+ equal(cis.getIdentities().length, 5, "Expected 5 containers.");
+
+ ok(!!cis.getIdentityFromId(identity.userContextId), "Identity exists");
+ equal(cis.getIdentityFromId(identity.userContextId).name, "New Container", "Identity name is OK");
+ equal(cis.getIdentityFromId(identity.userContextId).icon, "Icon", "Identity icon is OK");
+ equal(cis.getIdentityFromId(identity.userContextId).color, "Color", "Identity color is OK");
+ equal(cis.getUserContextLabel(identity.userContextId), "New Container", "Identity label is OK");
+
+ // Remove an identity
+ equal(cis.remove(-1), false, "cis.remove() returns false if identity doesn't exist.");
+ equal(cis.remove(1), true, "cis.remove() returns true if identity exists.");
+
+ equal(cis.getIdentities().length, 4, "Expected 4 containers.");
+});
+
+// Update an identity
+add_task(function() {
+ ok(!!cis.getIdentityFromId(2), "Identity 2 exists");
+
+ equal(cis.update(-1, "Container", "Icon", "Color"), false, "Update returns false if the identity doesn't exist");
+
+ equal(cis.update(2, "Container", "Icon", "Color"), true, "Update returns true if everything is OK");
+
+ ok(!!cis.getIdentityFromId(2), "Identity exists");
+ equal(cis.getIdentityFromId(2).name, "Container", "Identity name is OK");
+ equal(cis.getIdentityFromId(2).icon, "Icon", "Identity icon is OK");
+ equal(cis.getIdentityFromId(2).color, "Color", "Identity color is OK");
+ equal(cis.getUserContextLabel(2), "Container", "Identity label is OK");
+});
diff --git a/toolkit/components/contextualidentity/tests/unit/xpcshell.ini b/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..b45ff2c30f
--- /dev/null
+++ b/toolkit/components/contextualidentity/tests/unit/xpcshell.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_basic.js]
diff --git a/toolkit/components/cookie/content/cookieAcceptDialog.js b/toolkit/components/cookie/content/cookieAcceptDialog.js
new file mode 100644
index 0000000000..4b322e95d9
--- /dev/null
+++ b/toolkit/components/cookie/content/cookieAcceptDialog.js
@@ -0,0 +1,185 @@
+// -*- 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 nsICookieAcceptDialog = Components.interfaces.nsICookieAcceptDialog;
+const nsIDialogParamBlock = Components.interfaces.nsIDialogParamBlock;
+const nsICookie = Components.interfaces.nsICookie;
+const nsICookiePromptService = Components.interfaces.nsICookiePromptService;
+
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+var params;
+var cookieBundle;
+
+var showDetails = "";
+var hideDetails = "";
+var detailsAccessKey = "";
+
+function onload()
+{
+ doSetOKCancel(cookieAcceptNormal, cookieDeny, cookieAcceptSession);
+
+ var dialog = document.documentElement;
+
+ document.getElementById("Button2").collapsed = false;
+
+ document.getElementById("ok").label = dialog.getAttribute("acceptLabel");
+ document.getElementById("ok").accessKey = dialog.getAttribute("acceptKey");
+ document.getElementById("Button2").label = dialog.getAttribute("extra1Label");
+ document.getElementById("Button2").accessKey = dialog.getAttribute("extra1Key");
+ document.getElementById("cancel").label = dialog.getAttribute("cancelLabel");
+ document.getElementById("cancel").accessKey = dialog.getAttribute("cancelKey");
+
+ // hook up button icons where implemented
+ document.getElementById("ok").setAttribute("icon", "accept");
+ document.getElementById("cancel").setAttribute("icon", "cancel");
+ document.getElementById("disclosureButton").setAttribute("icon", "properties");
+
+ cookieBundle = document.getElementById("cookieBundle");
+
+ // cache strings
+ if (!showDetails) {
+ showDetails = cookieBundle.getString('showDetails');
+ }
+ if (!hideDetails) {
+ hideDetails = cookieBundle.getString('hideDetails');
+ }
+ detailsAccessKey = cookieBundle.getString('detailsAccessKey');
+
+ if (document.getElementById('infobox').hidden) {
+ document.getElementById('disclosureButton').setAttribute("label", showDetails);
+ } else {
+ document.getElementById('disclosureButton').setAttribute("label", hideDetails);
+ }
+ document.getElementById('disclosureButton').setAttribute("accesskey", detailsAccessKey);
+
+ if ("arguments" in window && window.arguments.length >= 1 && window.arguments[0]) {
+ try {
+ params = window.arguments[0].QueryInterface(nsIDialogParamBlock);
+ var cookie = params.objects.queryElementAt(0, nsICookie);
+ var cookiesFromHost = params.GetInt(nsICookieAcceptDialog.COOKIESFROMHOST);
+
+ var messageFormat;
+ if (params.GetInt(nsICookieAcceptDialog.CHANGINGCOOKIE))
+ messageFormat = 'permissionToModifyCookie';
+ else if (cookiesFromHost > 1)
+ messageFormat = 'permissionToSetAnotherCookie';
+ else if (cookiesFromHost == 1)
+ messageFormat = 'permissionToSetSecondCookie';
+ else
+ messageFormat = 'permissionToSetACookie';
+
+ var hostname = params.GetString(nsICookieAcceptDialog.HOSTNAME);
+
+ var messageText;
+ if (cookie)
+ messageText = cookieBundle.getFormattedString(messageFormat, [hostname, cookiesFromHost]);
+ else
+ // No cookies means something went wrong. Bring up the dialog anyway
+ // to not make the mess worse.
+ messageText = cookieBundle.getFormattedString(messageFormat, ["", cookiesFromHost]);
+
+ var messageParent = document.getElementById("dialogtextbox");
+ var messageParagraphs = messageText.split("\n");
+
+ // use value for the header, so it doesn't wrap.
+ var headerNode = document.getElementById("dialog-header");
+ headerNode.setAttribute("value", messageParagraphs[0]);
+
+ // use childnodes here, the text can wrap
+ for (var i = 1; i < messageParagraphs.length; i++) {
+ var descriptionNode = document.createElement("description");
+ text = document.createTextNode(messageParagraphs[i]);
+ descriptionNode.appendChild(text);
+ messageParent.appendChild(descriptionNode);
+ }
+
+ if (cookie) {
+ document.getElementById('ifl_name').setAttribute("value", cookie.name);
+ document.getElementById('ifl_value').setAttribute("value", cookie.value);
+ document.getElementById('ifl_host').setAttribute("value", cookie.host);
+ document.getElementById('ifl_path').setAttribute("value", cookie.path);
+ document.getElementById('ifl_isSecure').setAttribute("value",
+ cookie.isSecure ?
+ cookieBundle.getString("forSecureOnly") : cookieBundle.getString("forAnyConnection")
+ );
+ document.getElementById('ifl_expires').setAttribute("value", GetExpiresString(cookie.expires));
+ document.getElementById('ifl_isDomain').setAttribute("value",
+ cookie.isDomain ?
+ cookieBundle.getString("domainColon") : cookieBundle.getString("hostColon")
+ );
+ }
+ // set default result to not accept the cookie
+ params.SetInt(nsICookieAcceptDialog.ACCEPT_COOKIE, 0);
+ // and to not persist
+ params.SetInt(nsICookieAcceptDialog.REMEMBER_DECISION, 0);
+ } catch (e) {
+ }
+ }
+
+ // The Private Browsing service might not be available
+ try {
+ if (window.opener && PrivateBrowsingUtils.isWindowPrivate(window.opener)) {
+ var persistCheckbox = document.getElementById("persistDomainAcceptance");
+ persistCheckbox.removeAttribute("checked");
+ persistCheckbox.setAttribute("disabled", "true");
+ }
+ } catch (ex) {}
+}
+
+function showhideinfo()
+{
+ var infobox=document.getElementById('infobox');
+
+ if (infobox.hidden) {
+ infobox.setAttribute("hidden", "false");
+ document.getElementById('disclosureButton').setAttribute("label", hideDetails);
+ } else {
+ infobox.setAttribute("hidden", "true");
+ document.getElementById('disclosureButton').setAttribute("label", showDetails);
+ }
+ sizeToContent();
+}
+
+function cookieAcceptNormal()
+{
+ // accept the cookie normally
+ params.SetInt(nsICookieAcceptDialog.ACCEPT_COOKIE, nsICookiePromptService.ACCEPT_COOKIE);
+ // And remember that when needed
+ params.SetInt(nsICookieAcceptDialog.REMEMBER_DECISION, document.getElementById('persistDomainAcceptance').checked);
+ window.close();
+}
+
+function cookieAcceptSession()
+{
+ // accept for the session only
+ params.SetInt(nsICookieAcceptDialog.ACCEPT_COOKIE, nsICookiePromptService.ACCEPT_SESSION_COOKIE);
+ // And remember that when needed
+ params.SetInt(nsICookieAcceptDialog.REMEMBER_DECISION, document.getElementById('persistDomainAcceptance').checked);
+ window.close();
+}
+
+function cookieDeny()
+{
+ // say that the cookie was rejected
+ params.SetInt(nsICookieAcceptDialog.ACCEPT_COOKIE, nsICookiePromptService.DENY_COOKIE);
+ // And remember that when needed
+ params.SetInt(nsICookieAcceptDialog.REMEMBER_DECISION, document.getElementById('persistDomainAcceptance').checked);
+ window.close();
+}
+
+function GetExpiresString(secondsUntilExpires) {
+ if (secondsUntilExpires) {
+ var date = new Date(1000*secondsUntilExpires);
+ const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+ const dtOptions = { year: 'numeric', month: 'long', day: 'numeric',
+ hour: 'numeric', minute: 'numeric', second: 'numeric' };
+ return date.toLocaleString(locale, dtOptions);
+ }
+ return cookieBundle.getString("expireAtEndOfSession");
+}
diff --git a/toolkit/components/cookie/content/cookieAcceptDialog.xul b/toolkit/components/cookie/content/cookieAcceptDialog.xul
new file mode 100644
index 0000000000..99fd92c0c9
--- /dev/null
+++ b/toolkit/components/cookie/content/cookieAcceptDialog.xul
@@ -0,0 +1,118 @@
+<?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://cookie/locale/cookieAcceptDialog.dtd">
+
+<!-- use a overlay te be able to put the accept/deny buttons not on the bottom -->
+<?xul-overlay href="chrome://global/content/dialogOverlay.xul"?>
+
+<!-- use buttons="disclosure" to hide ok/cancel buttons. Those are added manually later -->
+<dialog id="cookieAcceptDialog"
+ acceptLabel="&button.allow.label;"
+ acceptKey="&button.allow.accesskey;"
+ extra1Label="&button.session.label;"
+ extra1Key="&button.session.accesskey;"
+ cancelLabel="&button.deny.label;"
+ cancelKey="&button.deny.accesskey;"
+ onload="onload();"
+ ondialogaccept="return doOKButton();"
+ title="&dialog.title;"
+ buttons="disclosure"
+ aria-describedby="dialog-header"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="cookieAcceptDialog.js" type="application/javascript"/>
+ <stringbundle id="cookieBundle"
+ src="chrome://cookie/locale/cookieAcceptDialog.properties"/>
+
+ <vbox>
+ <hbox>
+ <hbox align="start">
+ <image id="infoicon" class="spaced alert-icon"/>
+ </hbox>
+
+ <vbox flex="1">
+ <!-- text -->
+ <vbox id="dialogtextbox">
+ <description id="dialog-header" class="header"/>
+ </vbox>
+
+ <hbox id="checkboxContainer">
+ <checkbox id="persistDomainAcceptance"
+ label="&dialog.remember.label;"
+ accesskey="&dialog.remember.accesskey;"
+ persist="checked"/>
+ </hbox>
+ </vbox>
+
+ </hbox>
+
+ <hbox>
+ <button id="disclosureButton" dlgtype="disclosure" class="exit-dialog"
+ oncommand="showhideinfo();"/>
+ <spacer flex="1"/>
+ <hbox id="okCancelButtonsRight"/>
+ </hbox>
+
+ <vbox id="infobox" hidden="true" persist="hidden">
+ <separator class="groove"/>
+ <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>
+ </vbox>
+ </vbox>
+</dialog>
+
diff --git a/toolkit/components/cookie/jar.mn b/toolkit/components/cookie/jar.mn
new file mode 100644
index 0000000000..109e6cd4f9
--- /dev/null
+++ b/toolkit/components/cookie/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/.
+
+toolkit.jar:
+#ifndef MOZ_FENNEC
+% content cookie %content/cookie/
+ content/cookie/cookieAcceptDialog.xul (content/cookieAcceptDialog.xul)
+ content/cookie/cookieAcceptDialog.js (content/cookieAcceptDialog.js)
+#endif
diff --git a/toolkit/components/cookie/moz.build b/toolkit/components/cookie/moz.build
new file mode 100644
index 0000000000..e3ed997031
--- /dev/null
+++ b/toolkit/components/cookie/moz.build
@@ -0,0 +1,10 @@
+# -*- 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/.
+
+if CONFIG['MOZ_BUILD_APP'] == 'mobile/android':
+ DEFINES['MOZ_FENNEC'] = True
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/crashes/CrashManager.jsm b/toolkit/components/crashes/CrashManager.jsm
new file mode 100644
index 0000000000..3aac33254a
--- /dev/null
+++ b/toolkit/components/crashes/CrashManager.jsm
@@ -0,0 +1,1351 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const myScope = this;
+
+Cu.import("resource://gre/modules/Log.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm");
+Cu.import("resource://gre/modules/KeyValueParser.jsm");
+
+this.EXPORTED_SYMBOLS = [
+ "CrashManager",
+];
+
+/**
+ * How long to wait after application startup before crash event files are
+ * automatically aggregated.
+ *
+ * We defer aggregation for performance reasons, as we don't want too many
+ * services competing for I/O immediately after startup.
+ */
+const AGGREGATE_STARTUP_DELAY_MS = 57000;
+
+const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+
+// Converts Date to days since UNIX epoch.
+// This was copied from /services/metrics.storage.jsm. The implementation
+// does not account for leap seconds.
+function dateToDays(date) {
+ return Math.floor(date.getTime() / MILLISECONDS_IN_DAY);
+}
+
+
+/**
+ * A gateway to crash-related data.
+ *
+ * This type is generic and can be instantiated any number of times.
+ * However, most applications will typically only have one instance
+ * instantiated and that instance will point to profile and user appdata
+ * directories.
+ *
+ * Instances are created by passing an object with properties.
+ * Recognized properties are:
+ *
+ * pendingDumpsDir (string) (required)
+ * Where dump files that haven't been uploaded are located.
+ *
+ * submittedDumpsDir (string) (required)
+ * Where records of uploaded dumps are located.
+ *
+ * eventsDirs (array)
+ * Directories (defined as strings) where events files are written. This
+ * instance will collects events from files in the directories specified.
+ *
+ * storeDir (string)
+ * Directory we will use for our data store. This instance will write
+ * data files into the directory specified.
+ *
+ * telemetryStoreSizeKey (string)
+ * Telemetry histogram to report store size under.
+ */
+this.CrashManager = function (options) {
+ for (let k of ["pendingDumpsDir", "submittedDumpsDir", "eventsDirs",
+ "storeDir"]) {
+ if (!(k in options)) {
+ throw new Error("Required key not present in options: " + k);
+ }
+ }
+
+ this._log = Log.repository.getLogger("Crashes.CrashManager");
+
+ for (let k in options) {
+ let v = options[k];
+
+ switch (k) {
+ case "pendingDumpsDir":
+ this._pendingDumpsDir = v;
+ break;
+
+ case "submittedDumpsDir":
+ this._submittedDumpsDir = v;
+ break;
+
+ case "eventsDirs":
+ this._eventsDirs = v;
+ break;
+
+ case "storeDir":
+ this._storeDir = v;
+ break;
+
+ case "telemetryStoreSizeKey":
+ this._telemetryStoreSizeKey = v;
+ break;
+
+ default:
+ throw new Error("Unknown property in options: " + k);
+ }
+ }
+
+ // Promise for in-progress aggregation operation. We store it on the
+ // object so it can be returned for in-progress operations.
+ this._aggregatePromise = null;
+
+ // The CrashStore currently attached to this object.
+ this._store = null;
+
+ // A Task to retrieve the store. This is needed to avoid races when
+ // _getStore() is called multiple times in a short interval.
+ this._getStoreTask = null;
+
+ // The timer controlling the expiration of the CrashStore instance.
+ this._storeTimer = null;
+
+ // This is a semaphore that prevents the store from being freed by our
+ // timer-based resource freeing mechanism.
+ this._storeProtectedCount = 0;
+};
+
+this.CrashManager.prototype = Object.freeze({
+ // A crash in the main process.
+ PROCESS_TYPE_MAIN: "main",
+
+ // A crash in a content process.
+ PROCESS_TYPE_CONTENT: "content",
+
+ // A crash in a plugin process.
+ PROCESS_TYPE_PLUGIN: "plugin",
+
+ // A crash in a Gecko media plugin process.
+ PROCESS_TYPE_GMPLUGIN: "gmplugin",
+
+ // A crash in the GPU process.
+ PROCESS_TYPE_GPU: "gpu",
+
+ // A real crash.
+ CRASH_TYPE_CRASH: "crash",
+
+ // A hang.
+ CRASH_TYPE_HANG: "hang",
+
+ // Submission result values.
+ SUBMISSION_RESULT_OK: "ok",
+ SUBMISSION_RESULT_FAILED: "failed",
+
+ DUMP_REGEX: /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.dmp$/i,
+ SUBMITTED_REGEX: /^bp-(?:hr-)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.txt$/i,
+ ALL_REGEX: /^(.*)$/,
+
+ // How long the store object should persist in memory before being
+ // automatically garbage collected.
+ STORE_EXPIRATION_MS: 60 * 1000,
+
+ // Number of days after which a crash with no activity will get purged.
+ PURGE_OLDER_THAN_DAYS: 180,
+
+ // The following are return codes for individual event file processing.
+ // File processed OK.
+ EVENT_FILE_SUCCESS: "ok",
+ // The event appears to be malformed.
+ EVENT_FILE_ERROR_MALFORMED: "malformed",
+ // The type of event is unknown.
+ EVENT_FILE_ERROR_UNKNOWN_EVENT: "unknown-event",
+
+ /**
+ * Obtain a list of all dumps pending upload.
+ *
+ * The returned value is a promise that resolves to an array of objects
+ * on success. Each element in the array has the following properties:
+ *
+ * id (string)
+ * The ID of the crash (a UUID).
+ *
+ * path (string)
+ * The filename of the crash (<UUID.dmp>)
+ *
+ * date (Date)
+ * When this dump was created
+ *
+ * The returned arry is sorted by the modified time of the file backing
+ * the entry, oldest to newest.
+ *
+ * @return Promise<Array>
+ */
+ pendingDumps: function () {
+ return this._getDirectoryEntries(this._pendingDumpsDir, this.DUMP_REGEX);
+ },
+
+ /**
+ * Obtain a list of all dump files corresponding to submitted crashes.
+ *
+ * The returned value is a promise that resolves to an Array of
+ * objects. Each object has the following properties:
+ *
+ * path (string)
+ * The path of the file this entry comes from.
+ *
+ * id (string)
+ * The crash UUID.
+ *
+ * date (Date)
+ * The (estimated) date this crash was submitted.
+ *
+ * The returned array is sorted by the modified time of the file backing
+ * the entry, oldest to newest.
+ *
+ * @return Promise<Array>
+ */
+ submittedDumps: function () {
+ return this._getDirectoryEntries(this._submittedDumpsDir,
+ this.SUBMITTED_REGEX);
+ },
+
+ /**
+ * Aggregates "loose" events files into the unified "database."
+ *
+ * This function should be called periodically to collect metadata from
+ * all events files into the central data store maintained by this manager.
+ *
+ * Once events have been stored in the backing store the corresponding
+ * source files are deleted.
+ *
+ * Only one aggregation operation is allowed to occur at a time. If this
+ * is called when an existing aggregation is in progress, the promise for
+ * the original call will be returned.
+ *
+ * @return promise<int> The number of event files that were examined.
+ */
+ aggregateEventsFiles: function () {
+ if (this._aggregatePromise) {
+ return this._aggregatePromise;
+ }
+
+ return this._aggregatePromise = Task.spawn(function* () {
+ if (this._aggregatePromise) {
+ return this._aggregatePromise;
+ }
+
+ try {
+ let unprocessedFiles = yield this._getUnprocessedEventsFiles();
+
+ let deletePaths = [];
+ let needsSave = false;
+
+ this._storeProtectedCount++;
+ for (let entry of unprocessedFiles) {
+ try {
+ let result = yield this._processEventFile(entry);
+
+ switch (result) {
+ case this.EVENT_FILE_SUCCESS:
+ needsSave = true;
+ // Fall through.
+
+ case this.EVENT_FILE_ERROR_MALFORMED:
+ deletePaths.push(entry.path);
+ break;
+
+ case this.EVENT_FILE_ERROR_UNKNOWN_EVENT:
+ break;
+
+ default:
+ Cu.reportError("Unhandled crash event file return code. Please " +
+ "file a bug: " + result);
+ }
+ } catch (ex) {
+ if (ex instanceof OS.File.Error) {
+ this._log.warn("I/O error reading " + entry.path, ex);
+ } else {
+ // We should never encounter an exception. This likely represents
+ // a coding error because all errors should be detected and
+ // converted to return codes.
+ //
+ // If we get here, report the error and delete the source file
+ // so we don't see it again.
+ Cu.reportError("Exception when processing crash event file: " +
+ Log.exceptionStr(ex));
+ deletePaths.push(entry.path);
+ }
+ }
+ }
+
+ if (needsSave) {
+ let store = yield this._getStore();
+ yield store.save();
+ }
+
+ for (let path of deletePaths) {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex) {
+ this._log.warn("Error removing event file (" + path + ")", ex);
+ }
+ }
+
+ return unprocessedFiles.length;
+
+ } finally {
+ this._aggregatePromise = false;
+ this._storeProtectedCount--;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Prune old crash data.
+ *
+ * @param date
+ * (Date) The cutoff point for pruning. Crashes without data newer
+ * than this will be pruned.
+ */
+ pruneOldCrashes: function (date) {
+ return Task.spawn(function* () {
+ let store = yield this._getStore();
+ store.pruneOldCrashes(date);
+ yield store.save();
+ }.bind(this));
+ },
+
+ /**
+ * Run tasks that should be periodically performed.
+ */
+ runMaintenanceTasks: function () {
+ return Task.spawn(function* () {
+ yield this.aggregateEventsFiles();
+
+ let offset = this.PURGE_OLDER_THAN_DAYS * MILLISECONDS_IN_DAY;
+ yield this.pruneOldCrashes(new Date(Date.now() - offset));
+ }.bind(this));
+ },
+
+ /**
+ * Schedule maintenance tasks for some point in the future.
+ *
+ * @param delay
+ * (integer) Delay in milliseconds when maintenance should occur.
+ */
+ scheduleMaintenance: function (delay) {
+ let deferred = Promise.defer();
+
+ setTimeout(() => {
+ this.runMaintenanceTasks().then(deferred.resolve, deferred.reject);
+ }, delay);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Record the occurrence of a crash.
+ *
+ * This method skips event files altogether and writes directly and
+ * immediately to the manager's data store.
+ *
+ * @param processType (string) One of the PROCESS_TYPE constants.
+ * @param crashType (string) One of the CRASH_TYPE constants.
+ * @param id (string) Crash ID. Likely a UUID.
+ * @param date (Date) When the crash occurred.
+ * @param metadata (dictionary) Crash metadata, may be empty.
+ *
+ * @return promise<null> Resolved when the store has been saved.
+ */
+ addCrash: function (processType, crashType, id, date, metadata) {
+ return Task.spawn(function* () {
+ let store = yield this._getStore();
+ if (store.addCrash(processType, crashType, id, date, metadata)) {
+ yield store.save();
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Record the remote ID for a crash.
+ *
+ * @param crashID (string) Crash ID. Likely a UUID.
+ * @param remoteID (Date) Server/Breakpad ID.
+ *
+ * @return boolean True if the remote ID was recorded.
+ */
+ setRemoteCrashID: Task.async(function* (crashID, remoteID) {
+ let store = yield this._getStore();
+ if (store.setRemoteCrashID(crashID, remoteID)) {
+ yield store.save();
+ }
+ }),
+
+ /**
+ * Generate a submission ID for use with addSubmission{Attempt,Result}.
+ */
+ generateSubmissionID() {
+ return "sub-" + Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID().toString().slice(1, -1);
+ },
+
+ /**
+ * Record the occurrence of a submission attempt for a crash.
+ *
+ * @param crashID (string) Crash ID. Likely a UUID.
+ * @param submissionID (string) Submission ID. Likely a UUID.
+ * @param date (Date) When the attempt occurred.
+ *
+ * @return boolean True if the attempt was recorded and false if not.
+ */
+ addSubmissionAttempt: Task.async(function* (crashID, submissionID, date) {
+ let store = yield this._getStore();
+ if (store.addSubmissionAttempt(crashID, submissionID, date)) {
+ yield store.save();
+ }
+ }),
+
+ /**
+ * Record the occurrence of a submission result for a crash.
+ *
+ * @param crashID (string) Crash ID. Likely a UUID.
+ * @param submissionID (string) Submission ID. Likely a UUID.
+ * @param date (Date) When the submission result was obtained.
+ * @param result (string) One of the SUBMISSION_RESULT constants.
+ *
+ * @return boolean True if the result was recorded and false if not.
+ */
+ addSubmissionResult: Task.async(function* (crashID, submissionID, date, result) {
+ let store = yield this._getStore();
+ if (store.addSubmissionResult(crashID, submissionID, date, result)) {
+ yield store.save();
+ }
+ }),
+
+ /**
+ * Set the classification of a crash.
+ *
+ * @param crashID (string) Crash ID. Likely a UUID.
+ * @param classifications (array) Crash classifications.
+ *
+ * @return boolean True if the data was recorded and false if not.
+ */
+ setCrashClassifications: Task.async(function* (crashID, classifications) {
+ let store = yield this._getStore();
+ if (store.setCrashClassifications(crashID, classifications)) {
+ yield store.save();
+ }
+ }),
+
+ /**
+ * Obtain the paths of all unprocessed events files.
+ *
+ * The promise-resolved array is sorted by file mtime, oldest to newest.
+ */
+ _getUnprocessedEventsFiles: function () {
+ return Task.spawn(function* () {
+ let entries = [];
+
+ for (let dir of this._eventsDirs) {
+ for (let e of yield this._getDirectoryEntries(dir, this.ALL_REGEX)) {
+ entries.push(e);
+ }
+ }
+
+ entries.sort((a, b) => { return a.date - b.date; });
+
+ return entries;
+ }.bind(this));
+ },
+
+ // See docs/crash-events.rst for the file format specification.
+ _processEventFile: function (entry) {
+ return Task.spawn(function* () {
+ let data = yield OS.File.read(entry.path);
+ let store = yield this._getStore();
+
+ let decoder = new TextDecoder();
+ data = decoder.decode(data);
+
+ let type, time;
+ let start = 0;
+ for (let i = 0; i < 2; i++) {
+ let index = data.indexOf("\n", start);
+ if (index == -1) {
+ return this.EVENT_FILE_ERROR_MALFORMED;
+ }
+
+ let sub = data.substring(start, index);
+ switch (i) {
+ case 0:
+ type = sub;
+ break;
+ case 1:
+ time = sub;
+ try {
+ time = parseInt(time, 10);
+ } catch (ex) {
+ return this.EVENT_FILE_ERROR_MALFORMED;
+ }
+ }
+
+ start = index + 1;
+ }
+ let date = new Date(time * 1000);
+ let payload = data.substring(start);
+
+ return this._handleEventFilePayload(store, entry, type, date, payload);
+ }.bind(this));
+ },
+
+ _handleEventFilePayload: function (store, entry, type, date, payload) {
+ // The payload types and formats are documented in docs/crash-events.rst.
+ // Do not change the format of an existing type. Instead, invent a new
+ // type.
+ // DO NOT ADD NEW TYPES WITHOUT DOCUMENTING!
+ let lines = payload.split("\n");
+
+ switch (type) {
+ case "crash.main.1":
+ if (lines.length > 1) {
+ this._log.warn("Multiple lines unexpected in payload for " +
+ entry.path);
+ return this.EVENT_FILE_ERROR_MALFORMED;
+ }
+ // fall-through
+ case "crash.main.2":
+ let crashID = lines[0];
+ let metadata = parseKeyValuePairsFromLines(lines.slice(1));
+ store.addCrash(this.PROCESS_TYPE_MAIN, this.CRASH_TYPE_CRASH,
+ crashID, date, metadata);
+
+ // If we have a saved environment, use it. Otherwise report
+ // the current environment.
+ let crashEnvironment = null;
+ let sessionId = null;
+ let stackTraces = null;
+ let reportMeta = Cu.cloneInto(metadata, myScope);
+ if ('TelemetryEnvironment' in reportMeta) {
+ try {
+ crashEnvironment = JSON.parse(reportMeta.TelemetryEnvironment);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ delete reportMeta.TelemetryEnvironment;
+ }
+ if ('TelemetrySessionId' in reportMeta) {
+ sessionId = reportMeta.TelemetrySessionId;
+ delete reportMeta.TelemetrySessionId;
+ }
+ if ('StackTraces' in reportMeta) {
+ try {
+ stackTraces = JSON.parse(reportMeta.StackTraces);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ delete reportMeta.StackTraces;
+ }
+ TelemetryController.submitExternalPing("crash",
+ {
+ version: 1,
+ crashDate: date.toISOString().slice(0, 10), // YYYY-MM-DD
+ sessionId: sessionId,
+ crashId: entry.id,
+ stackTraces: stackTraces,
+ metadata: reportMeta,
+ hasCrashEnvironment: (crashEnvironment !== null),
+ },
+ {
+ retentionDays: 180,
+ addClientId: true,
+ addEnvironment: true,
+ overrideEnvironment: crashEnvironment,
+ });
+ break;
+
+ case "crash.submission.1":
+ if (lines.length == 3) {
+ let [crashID, result, remoteID] = lines;
+ store.addCrash(this.PROCESS_TYPE_MAIN, this.CRASH_TYPE_CRASH,
+ crashID, date);
+
+ let submissionID = this.generateSubmissionID();
+ let succeeded = result === "true";
+ store.addSubmissionAttempt(crashID, submissionID, date);
+ store.addSubmissionResult(crashID, submissionID, date,
+ succeeded ? this.SUBMISSION_RESULT_OK :
+ this.SUBMISSION_RESULT_FAILED);
+ if (succeeded) {
+ store.setRemoteCrashID(crashID, remoteID);
+ }
+ } else {
+ return this.EVENT_FILE_ERROR_MALFORMED;
+ }
+ break;
+
+ default:
+ return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
+ }
+
+ return this.EVENT_FILE_SUCCESS;
+ },
+
+ /**
+ * The resolved promise is an array of objects with the properties:
+ *
+ * path -- String filename
+ * id -- regexp.match()[1] (likely the crash ID)
+ * date -- Date mtime of the file
+ */
+ _getDirectoryEntries: function (path, re) {
+ return Task.spawn(function* () {
+ try {
+ yield OS.File.stat(path);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ return [];
+ }
+
+ let it = new OS.File.DirectoryIterator(path);
+ let entries = [];
+
+ try {
+ yield it.forEach((entry, index, it) => {
+ if (entry.isDir) {
+ return undefined;
+ }
+
+ let match = re.exec(entry.name);
+ if (!match) {
+ return undefined;
+ }
+
+ return OS.File.stat(entry.path).then((info) => {
+ entries.push({
+ path: entry.path,
+ id: match[1],
+ date: info.lastModificationDate,
+ });
+ });
+ });
+ } finally {
+ it.close();
+ }
+
+ entries.sort((a, b) => { return a.date - b.date; });
+
+ return entries;
+ }.bind(this));
+ },
+
+ _getStore: function () {
+ if (this._getStoreTask) {
+ return this._getStoreTask;
+ }
+
+ return this._getStoreTask = Task.spawn(function* () {
+ try {
+ if (!this._store) {
+ yield OS.File.makeDir(this._storeDir, {
+ ignoreExisting: true,
+ unixMode: OS.Constants.libc.S_IRWXU,
+ });
+
+ let store = new CrashStore(this._storeDir,
+ this._telemetryStoreSizeKey);
+ yield store.load();
+
+ this._store = store;
+ this._storeTimer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ }
+
+ // The application can go long periods without interacting with the
+ // store. Since the store takes up resources, we automatically "free"
+ // the store after inactivity so resources can be returned to the
+ // system. We do this via a timer and a mechanism that tracks when the
+ // store is being accessed.
+ this._storeTimer.cancel();
+
+ // This callback frees resources from the store unless the store
+ // is protected from freeing by some other process.
+ let timerCB = function () {
+ if (this._storeProtectedCount) {
+ this._storeTimer.initWithCallback(timerCB, this.STORE_EXPIRATION_MS,
+ this._storeTimer.TYPE_ONE_SHOT);
+ return;
+ }
+
+ // We kill the reference that we hold. GC will kill it later. If
+ // someone else holds a reference, that will prevent GC until that
+ // reference is gone.
+ this._store = null;
+ this._storeTimer = null;
+ }.bind(this);
+
+ this._storeTimer.initWithCallback(timerCB, this.STORE_EXPIRATION_MS,
+ this._storeTimer.TYPE_ONE_SHOT);
+
+ return this._store;
+ } finally {
+ this._getStoreTask = null;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Obtain information about all known crashes.
+ *
+ * Returns an array of CrashRecord instances. Instances are read-only.
+ */
+ getCrashes: function () {
+ return Task.spawn(function* () {
+ let store = yield this._getStore();
+
+ return store.crashes;
+ }.bind(this));
+ },
+
+ getCrashCountsByDay: function () {
+ return Task.spawn(function* () {
+ let store = yield this._getStore();
+
+ return store._countsByDay;
+ }.bind(this));
+ },
+});
+
+var gCrashManager;
+
+/**
+ * Interface to storage of crash data.
+ *
+ * This type handles storage of crash metadata. It exists as a separate type
+ * from the crash manager for performance reasons: since all crash metadata
+ * needs to be loaded into memory for access, we wish to easily dispose of all
+ * associated memory when this data is no longer needed. Having an isolated
+ * object whose references can easily be lost faciliates that simple disposal.
+ *
+ * When metadata is updated, the caller must explicitly persist the changes
+ * to disk. This prevents excessive I/O during updates.
+ *
+ * The store has a mechanism for ensuring it doesn't grow too large. A ceiling
+ * is placed on the number of daily events that can occur for events that can
+ * occur with relatively high frequency, notably plugin crashes and hangs
+ * (plugins can enter cycles where they repeatedly crash). If we've reached
+ * the high water mark and new data arrives, it's silently dropped.
+ * However, the count of actual events is always preserved. This allows
+ * us to report on the severity of problems beyond the storage threshold.
+ *
+ * Main process crashes are excluded from limits because they are both
+ * important and should be rare.
+ *
+ * @param storeDir (string)
+ * Directory the store should be located in.
+ * @param telemetrySizeKey (string)
+ * The telemetry histogram that should be used to store the size
+ * of the data file.
+ */
+function CrashStore(storeDir, telemetrySizeKey) {
+ this._storeDir = storeDir;
+ this._telemetrySizeKey = telemetrySizeKey;
+
+ this._storePath = OS.Path.join(storeDir, "store.json.mozlz4");
+
+ // Holds the read data from disk.
+ this._data = null;
+
+ // Maps days since UNIX epoch to a Map of event types to counts.
+ // This data structure is populated when the JSON file is loaded
+ // and is also updated when new events are added.
+ this._countsByDay = new Map();
+}
+
+CrashStore.prototype = Object.freeze({
+ // Maximum number of events to store per day. This establishes a
+ // ceiling on the per-type/per-day records that will be stored.
+ HIGH_WATER_DAILY_THRESHOLD: 100,
+
+ /**
+ * Reset all data.
+ */
+ reset() {
+ this._data = {
+ v: 1,
+ crashes: new Map(),
+ corruptDate: null,
+ };
+ this._countsByDay = new Map();
+ },
+
+ /**
+ * Load data from disk.
+ *
+ * @return Promise
+ */
+ load: function () {
+ return Task.spawn(function* () {
+ // Loading replaces data.
+ this.reset();
+
+ try {
+ let decoder = new TextDecoder();
+ let data = yield OS.File.read(this._storePath, {compression: "lz4"});
+ data = JSON.parse(decoder.decode(data));
+
+ if (data.corruptDate) {
+ this._data.corruptDate = new Date(data.corruptDate);
+ }
+
+ // actualCounts is used to validate that the derived counts by
+ // days stored in the payload matches up to actual data.
+ let actualCounts = new Map();
+
+ // In the past, submissions were stored as separate crash records
+ // with an id of e.g. "someID-submission". If we find IDs ending
+ // with "-submission", we will need to convert the data to be stored
+ // as actual submissions.
+ //
+ // The old way of storing submissions was used from FF33 - FF34. We
+ // drop this old data on the floor.
+ for (let id in data.crashes) {
+ if (id.endsWith("-submission")) {
+ continue;
+ }
+
+ let crash = data.crashes[id];
+ let denormalized = this._denormalize(crash);
+
+ denormalized.submissions = new Map();
+ if (crash.submissions) {
+ for (let submissionID in crash.submissions) {
+ let submission = crash.submissions[submissionID];
+ denormalized.submissions.set(submissionID,
+ this._denormalize(submission));
+ }
+ }
+
+ this._data.crashes.set(id, denormalized);
+
+ let key = dateToDays(denormalized.crashDate) + "-" + denormalized.type;
+ actualCounts.set(key, (actualCounts.get(key) || 0) + 1);
+
+ // If we have an OOM size, count the crash as an OOM in addition to
+ // being a main process crash.
+ if (denormalized.metadata &&
+ denormalized.metadata.OOMAllocationSize) {
+ let oomKey = key + "-oom";
+ actualCounts.set(oomKey, (actualCounts.get(oomKey) || 0) + 1);
+ }
+
+ }
+
+ // The validation in this loop is arguably not necessary. We perform
+ // it as a defense against unknown bugs.
+ for (let dayKey in data.countsByDay) {
+ let day = parseInt(dayKey, 10);
+ for (let type in data.countsByDay[day]) {
+ this._ensureCountsForDay(day);
+
+ let count = data.countsByDay[day][type];
+ let key = day + "-" + type;
+
+ // If the payload says we have data for a given day but we
+ // don't, the payload is wrong. Ignore it.
+ if (!actualCounts.has(key)) {
+ continue;
+ }
+
+ // If we encountered more data in the payload than what the
+ // data structure says, use the proper value.
+ count = Math.max(count, actualCounts.get(key));
+
+ this._countsByDay.get(day).set(type, count);
+ }
+ }
+ } catch (ex) {
+ // Missing files (first use) are allowed.
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ // If we can't load for any reason, mark a corrupt date in the instance
+ // and swallow the error.
+ //
+ // The marking of a corrupted file is intentionally not persisted to
+ // disk yet. Instead, we wait until the next save(). This is to give
+ // non-permanent failures the opportunity to recover on their own.
+ this._data.corruptDate = new Date();
+ }
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Save data to disk.
+ *
+ * @return Promise<null>
+ */
+ save: function () {
+ return Task.spawn(function* () {
+ if (!this._data) {
+ return;
+ }
+
+ let normalized = {
+ // The version should be incremented whenever the format
+ // changes.
+ v: 1,
+ // Maps crash IDs to objects defining the crash.
+ crashes: {},
+ // Maps days since UNIX epoch to objects mapping event types to
+ // counts. This is a mirror of this._countsByDay. e.g.
+ // {
+ // 15000: {
+ // "main-crash": 2,
+ // "plugin-crash": 1
+ // }
+ // }
+ countsByDay: {},
+
+ // When the store was last corrupted.
+ corruptDate: null,
+ };
+
+ if (this._data.corruptDate) {
+ normalized.corruptDate = this._data.corruptDate.getTime();
+ }
+
+ for (let [id, crash] of this._data.crashes) {
+ let c = this._normalize(crash);
+
+ c.submissions = {};
+ for (let [submissionID, submission] of crash.submissions) {
+ c.submissions[submissionID] = this._normalize(submission);
+ }
+
+ normalized.crashes[id] = c;
+ }
+
+ for (let [day, m] of this._countsByDay) {
+ normalized.countsByDay[day] = {};
+ for (let [type, count] of m) {
+ normalized.countsByDay[day][type] = count;
+ }
+ }
+
+ let encoder = new TextEncoder();
+ let data = encoder.encode(JSON.stringify(normalized));
+ let size = yield OS.File.writeAtomic(this._storePath, data, {
+ tmpPath: this._storePath + ".tmp",
+ compression: "lz4"});
+ if (this._telemetrySizeKey) {
+ Services.telemetry.getHistogramById(this._telemetrySizeKey).add(size);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Normalize an object into one fit for serialization.
+ *
+ * This function along with _denormalize() serve to hack around the
+ * default handling of Date JSON serialization because Date serialization
+ * is undefined by JSON.
+ *
+ * Fields ending with "Date" are assumed to contain Date instances.
+ * We convert these to milliseconds since epoch on output and back to
+ * Date on input.
+ */
+ _normalize: function (o) {
+ let normalized = {};
+
+ for (let k in o) {
+ let v = o[k];
+ if (v && k.endsWith("Date")) {
+ normalized[k] = v.getTime();
+ } else {
+ normalized[k] = v;
+ }
+ }
+
+ return normalized;
+ },
+
+ /**
+ * Convert a serialized object back to its native form.
+ */
+ _denormalize: function (o) {
+ let n = {};
+
+ for (let k in o) {
+ let v = o[k];
+ if (v && k.endsWith("Date")) {
+ n[k] = new Date(parseInt(v, 10));
+ } else {
+ n[k] = v;
+ }
+ }
+
+ return n;
+ },
+
+ /**
+ * Prune old crash data.
+ *
+ * Crashes without recent activity are pruned from the store so the
+ * size of the store is not unbounded. If there is activity on a crash,
+ * that activity will keep the crash and all its data around for longer.
+ *
+ * @param date
+ * (Date) The cutoff at which data will be pruned. If an entry
+ * doesn't have data newer than this, it will be pruned.
+ */
+ pruneOldCrashes: function (date) {
+ for (let crash of this.crashes) {
+ let newest = crash.newestDate;
+ if (!newest || newest.getTime() < date.getTime()) {
+ this._data.crashes.delete(crash.id);
+ }
+ }
+ },
+
+ /**
+ * Date the store was last corrupted and required a reset.
+ *
+ * May be null (no corruption has ever occurred) or a Date instance.
+ */
+ get corruptDate() {
+ return this._data.corruptDate;
+ },
+
+ /**
+ * The number of distinct crashes tracked.
+ */
+ get crashesCount() {
+ return this._data.crashes.size;
+ },
+
+ /**
+ * All crashes tracked.
+ *
+ * This is an array of CrashRecord.
+ */
+ get crashes() {
+ let crashes = [];
+ for (let [, crash] of this._data.crashes) {
+ crashes.push(new CrashRecord(crash));
+ }
+
+ return crashes;
+ },
+
+ /**
+ * Obtain a particular crash from its ID.
+ *
+ * A CrashRecord will be returned if the crash exists. null will be returned
+ * if the crash is unknown.
+ */
+ getCrash: function (id) {
+ for (let crash of this.crashes) {
+ if (crash.id == id) {
+ return crash;
+ }
+ }
+
+ return null;
+ },
+
+ _ensureCountsForDay: function (day) {
+ if (!this._countsByDay.has(day)) {
+ this._countsByDay.set(day, new Map());
+ }
+ },
+
+ /**
+ * Ensure the crash record is present in storage.
+ *
+ * Returns the crash record if we're allowed to store it or null
+ * if we've hit the high water mark.
+ *
+ * @param processType
+ * (string) One of the PROCESS_TYPE constants.
+ * @param crashType
+ * (string) One of the CRASH_TYPE constants.
+ * @param id
+ * (string) The crash ID.
+ * @param date
+ * (Date) When this crash occurred.
+ * @param metadata
+ * (dictionary) Crash metadata, may be empty.
+ *
+ * @return null | object crash record
+ */
+ _ensureCrashRecord: function (processType, crashType, id, date, metadata) {
+ if (!id) {
+ // Crashes are keyed on ID, so it's not really helpful to store crashes
+ // without IDs.
+ return null;
+ }
+
+ let type = processType + "-" + crashType;
+
+ if (!this._data.crashes.has(id)) {
+ let day = dateToDays(date);
+ this._ensureCountsForDay(day);
+
+ let count = (this._countsByDay.get(day).get(type) || 0) + 1;
+ this._countsByDay.get(day).set(type, count);
+
+ if (count > this.HIGH_WATER_DAILY_THRESHOLD &&
+ processType != CrashManager.prototype.PROCESS_TYPE_MAIN) {
+ return null;
+ }
+
+ // If we have an OOM size, count the crash as an OOM in addition to
+ // being a main process crash.
+ if (metadata && metadata.OOMAllocationSize) {
+ let oomType = type + "-oom";
+ let oomCount = (this._countsByDay.get(day).get(oomType) || 0) + 1;
+ this._countsByDay.get(day).set(oomType, oomCount);
+ }
+
+ this._data.crashes.set(id, {
+ id: id,
+ remoteID: null,
+ type: type,
+ crashDate: date,
+ submissions: new Map(),
+ classifications: [],
+ metadata: metadata,
+ });
+ }
+
+ let crash = this._data.crashes.get(id);
+ crash.type = type;
+ crash.crashDate = date;
+
+ return crash;
+ },
+
+ /**
+ * Record the occurrence of a crash.
+ *
+ * @param processType (string) One of the PROCESS_TYPE constants.
+ * @param crashType (string) One of the CRASH_TYPE constants.
+ * @param id (string) Crash ID. Likely a UUID.
+ * @param date (Date) When the crash occurred.
+ * @param metadata (dictionary) Crash metadata, may be empty.
+ *
+ * @return boolean True if the crash was recorded and false if not.
+ */
+ addCrash: function (processType, crashType, id, date, metadata) {
+ return !!this._ensureCrashRecord(processType, crashType, id, date, metadata);
+ },
+
+ /**
+ * @return boolean True if the remote ID was recorded and false if not.
+ */
+ setRemoteCrashID: function (crashID, remoteID) {
+ let crash = this._data.crashes.get(crashID);
+ if (!crash || !remoteID) {
+ return false;
+ }
+
+ crash.remoteID = remoteID;
+ return true;
+ },
+
+ getCrashesOfType: function (processType, crashType) {
+ let crashes = [];
+ for (let crash of this.crashes) {
+ if (crash.isOfType(processType, crashType)) {
+ crashes.push(crash);
+ }
+ }
+
+ return crashes;
+ },
+
+ /**
+ * Ensure the submission record is present in storage.
+ * @returns [submission, crash]
+ */
+ _ensureSubmissionRecord: function (crashID, submissionID) {
+ let crash = this._data.crashes.get(crashID);
+ if (!crash || !submissionID) {
+ return null;
+ }
+
+ if (!crash.submissions.has(submissionID)) {
+ crash.submissions.set(submissionID, {
+ requestDate: null,
+ responseDate: null,
+ result: null,
+ });
+ }
+
+ return [crash.submissions.get(submissionID), crash];
+ },
+
+ /**
+ * @return boolean True if the attempt was recorded.
+ */
+ addSubmissionAttempt: function (crashID, submissionID, date) {
+ let [submission, crash] =
+ this._ensureSubmissionRecord(crashID, submissionID);
+ if (!submission) {
+ return false;
+ }
+
+ submission.requestDate = date;
+ Services.telemetry.getKeyedHistogramById("PROCESS_CRASH_SUBMIT_ATTEMPT")
+ .add(crash.type, 1);
+ return true;
+ },
+
+ /**
+ * @return boolean True if the response was recorded.
+ */
+ addSubmissionResult: function (crashID, submissionID, date, result) {
+ let crash = this._data.crashes.get(crashID);
+ if (!crash || !submissionID) {
+ return false;
+ }
+ let submission = crash.submissions.get(submissionID);
+ if (!submission) {
+ return false;
+ }
+
+ submission.responseDate = date;
+ submission.result = result;
+ Services.telemetry.getKeyedHistogramById("PROCESS_CRASH_SUBMIT_SUCCESS")
+ .add(crash.type, result == "ok");
+ return true;
+ },
+
+ /**
+ * @return boolean True if the classifications were set.
+ */
+ setCrashClassifications: function (crashID, classifications) {
+ let crash = this._data.crashes.get(crashID);
+ if (!crash) {
+ return false;
+ }
+
+ crash.classifications = classifications;
+ return true;
+ },
+});
+
+/**
+ * Represents an individual crash with metadata.
+ *
+ * This is a wrapper around the low-level anonymous JS objects that define
+ * crashes. It exposes a consistent and helpful API.
+ *
+ * Instances of this type should only be constructured inside this module,
+ * not externally. The constructor is not considered a public API.
+ *
+ * @param o (object)
+ * The crash's entry from the CrashStore.
+ */
+function CrashRecord(o) {
+ this._o = o;
+}
+
+CrashRecord.prototype = Object.freeze({
+ get id() {
+ return this._o.id;
+ },
+
+ get remoteID() {
+ return this._o.remoteID;
+ },
+
+ get crashDate() {
+ return this._o.crashDate;
+ },
+
+ /**
+ * Obtain the newest date in this record.
+ *
+ * This is a convenience getter. The returned value is used to determine when
+ * to expire a record.
+ */
+ get newestDate() {
+ // We currently only have 1 date, so this is easy.
+ return this._o.crashDate;
+ },
+
+ get oldestDate() {
+ return this._o.crashDate;
+ },
+
+ get type() {
+ return this._o.type;
+ },
+
+ isOfType: function (processType, crashType) {
+ return processType + "-" + crashType == this.type;
+ },
+
+ get submissions() {
+ return this._o.submissions;
+ },
+
+ get classifications() {
+ return this._o.classifications;
+ },
+
+ get metadata() {
+ return this._o.metadata;
+ },
+});
+
+/**
+ * Obtain the global CrashManager instance used by the running application.
+ *
+ * CrashManager is likely only ever instantiated once per application lifetime.
+ * The main reason it's implemented as a reusable type is to facilitate testing.
+ */
+XPCOMUtils.defineLazyGetter(this.CrashManager, "Singleton", function () {
+ if (gCrashManager) {
+ return gCrashManager;
+ }
+
+ let crPath = OS.Path.join(OS.Constants.Path.userApplicationDataDir,
+ "Crash Reports");
+ let storePath = OS.Path.join(OS.Constants.Path.profileDir, "crashes");
+
+ gCrashManager = new CrashManager({
+ pendingDumpsDir: OS.Path.join(crPath, "pending"),
+ submittedDumpsDir: OS.Path.join(crPath, "submitted"),
+ eventsDirs: [OS.Path.join(crPath, "events"), OS.Path.join(storePath, "events")],
+ storeDir: storePath,
+ telemetryStoreSizeKey: "CRASH_STORE_COMPRESSED_BYTES",
+ });
+
+ // Automatically aggregate event files shortly after startup. This
+ // ensures it happens with some frequency.
+ //
+ // There are performance considerations here. While this is doing
+ // work and could negatively impact performance, the amount of work
+ // is kept small per run by periodically aggregating event files.
+ // Furthermore, well-behaving installs should not have much work
+ // here to do. If there is a lot of work, that install has bigger
+ // issues beyond reduced performance near startup.
+ gCrashManager.scheduleMaintenance(AGGREGATE_STARTUP_DELAY_MS);
+
+ return gCrashManager;
+});
diff --git a/toolkit/components/crashes/CrashManagerTest.jsm b/toolkit/components/crashes/CrashManagerTest.jsm
new file mode 100644
index 0000000000..2c6c4b1a05
--- /dev/null
+++ b/toolkit/components/crashes/CrashManagerTest.jsm
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file provides common and shared functionality to facilitate
+ * testing of the Crashes component (CrashManager.jsm).
+ */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+this.EXPORTED_SYMBOLS = [
+ "configureLogging",
+ "getManager",
+ "sleep",
+ "TestingCrashManager",
+];
+
+Cu.import("resource://gre/modules/CrashManager.jsm", this);
+Cu.import("resource://gre/modules/Log.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+var loggingConfigured = false;
+
+this.configureLogging = function () {
+ if (loggingConfigured) {
+ return;
+ }
+
+ let log = Log.repository.getLogger("Crashes.CrashManager");
+ log.level = Log.Level.All;
+ let appender = new Log.DumpAppender();
+ appender.level = Log.Level.All;
+ log.addAppender(appender);
+ loggingConfigured = true;
+};
+
+this.sleep = function (wait) {
+ let deferred = Promise.defer();
+
+ setTimeout(() => {
+ deferred.resolve();
+ }, wait);
+
+ return deferred.promise;
+};
+
+this.TestingCrashManager = function (options) {
+ CrashManager.call(this, options);
+}
+
+this.TestingCrashManager.prototype = {
+ __proto__: CrashManager.prototype,
+
+ createDummyDump: function (submitted=false, date=new Date(), hr=false) {
+ let uuid = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID()
+ .toString();
+ uuid = uuid.substring(1, uuid.length - 1);
+
+ let path;
+ let mode;
+ if (submitted) {
+ if (hr) {
+ path = OS.Path.join(this._submittedDumpsDir, "bp-hr-" + uuid + ".txt");
+ } else {
+ path = OS.Path.join(this._submittedDumpsDir, "bp-" + uuid + ".txt");
+ }
+ mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR |
+ OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IROTH;
+ } else {
+ path = OS.Path.join(this._pendingDumpsDir, uuid + ".dmp");
+ mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR;
+ }
+
+ return Task.spawn(function* () {
+ let f = yield OS.File.open(path, {create: true}, {unixMode: mode});
+ yield f.setDates(date, date);
+ yield f.close();
+ dump("Created fake crash: " + path + "\n");
+
+ return uuid;
+ });
+ },
+
+ createIgnoredDumpFile: function (filename, submitted=false) {
+ let path;
+ if (submitted) {
+ path = OS.Path.join(this._submittedDumpsDir, filename);
+ } else {
+ path = OS.Path.join(this._pendingDumpsDir, filename);
+ }
+
+ return Task.spawn(function* () {
+ let mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR;
+ yield OS.File.open(path, {create: true}, {unixMode: mode});
+ dump ("Create ignored dump file: " + path + "\n");
+ });
+ },
+
+ createEventsFile: function (filename, type, date, content, index=0) {
+ let path = OS.Path.join(this._eventsDirs[index], filename);
+
+ let data = type + "\n" +
+ Math.floor(date.getTime() / 1000) + "\n" +
+ content;
+ let encoder = new TextEncoder();
+ let array = encoder.encode(data);
+
+ return Task.spawn(function* () {
+ yield OS.File.writeAtomic(path, array);
+ yield OS.File.setDates(path, date, date);
+ });
+ },
+
+ /**
+ * Overwrite event file handling to process our test file type.
+ *
+ * We can probably delete this once we have actual events defined.
+ */
+ _handleEventFilePayload: function (store, entry, type, date, payload) {
+ if (type == "test.1") {
+ if (payload == "malformed") {
+ return this.EVENT_FILE_ERROR_MALFORMED;
+ } else if (payload == "success") {
+ return this.EVENT_FILE_SUCCESS;
+ }
+ return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
+ }
+
+ return CrashManager.prototype._handleEventFilePayload.call(this,
+ store,
+ entry,
+ type,
+ date,
+ payload);
+ },
+};
+
+var DUMMY_DIR_COUNT = 0;
+
+this.getManager = function () {
+ return Task.spawn(function* () {
+ const dirMode = OS.Constants.libc.S_IRWXU;
+ let baseFile = OS.Constants.Path.profileDir;
+
+ function makeDir(create=true) {
+ return Task.spawn(function* () {
+ let path = OS.Path.join(baseFile, "dummy-dir-" + DUMMY_DIR_COUNT++);
+
+ if (!create) {
+ return path;
+ }
+
+ dump("Creating directory: " + path + "\n");
+ yield OS.File.makeDir(path, {unixMode: dirMode});
+
+ return path;
+ });
+ }
+
+ let pendingD = yield makeDir();
+ let submittedD = yield makeDir();
+ let eventsD1 = yield makeDir();
+ let eventsD2 = yield makeDir();
+
+ // Store directory is created at run-time if needed. Ensure those code
+ // paths are triggered.
+ let storeD = yield makeDir(false);
+
+ let m = new TestingCrashManager({
+ pendingDumpsDir: pendingD,
+ submittedDumpsDir: submittedD,
+ eventsDirs: [eventsD1, eventsD2],
+ storeDir: storeD,
+ });
+
+ return m;
+ });
+};
diff --git a/toolkit/components/crashes/CrashService.js b/toolkit/components/crashes/CrashService.js
new file mode 100644
index 0000000000..56f8b69e74
--- /dev/null
+++ b/toolkit/components/crashes/CrashService.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/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+/**
+ * This component makes crash data available throughout the application.
+ *
+ * It is a service because some background activity will eventually occur.
+ */
+this.CrashService = function () {};
+
+CrashService.prototype = Object.freeze({
+ classID: Components.ID("{92668367-1b17-4190-86b2-1061b2179744}"),
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsICrashService,
+ Ci.nsIObserver,
+ ]),
+
+ addCrash: function (processType, crashType, id) {
+ switch (processType) {
+ case Ci.nsICrashService.PROCESS_TYPE_MAIN:
+ processType = Services.crashmanager.PROCESS_TYPE_MAIN;
+ break;
+ case Ci.nsICrashService.PROCESS_TYPE_CONTENT:
+ processType = Services.crashmanager.PROCESS_TYPE_CONTENT;
+ break;
+ case Ci.nsICrashService.PROCESS_TYPE_PLUGIN:
+ processType = Services.crashmanager.PROCESS_TYPE_PLUGIN;
+ break;
+ case Ci.nsICrashService.PROCESS_TYPE_GMPLUGIN:
+ processType = Services.crashmanager.PROCESS_TYPE_GMPLUGIN;
+ break;
+ case Ci.nsICrashService.PROCESS_TYPE_GPU:
+ processType = Services.crashmanager.PROCESS_TYPE_GPU;
+ break;
+ default:
+ throw new Error("Unrecognized PROCESS_TYPE: " + processType);
+ }
+
+ switch (crashType) {
+ case Ci.nsICrashService.CRASH_TYPE_CRASH:
+ crashType = Services.crashmanager.CRASH_TYPE_CRASH;
+ break;
+ case Ci.nsICrashService.CRASH_TYPE_HANG:
+ crashType = Services.crashmanager.CRASH_TYPE_HANG;
+ break;
+ default:
+ throw new Error("Unrecognized CRASH_TYPE: " + crashType);
+ }
+
+ Services.crashmanager.addCrash(processType, crashType, id, new Date());
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "profile-after-change":
+ // Side-effect is the singleton is instantiated.
+ Services.crashmanager;
+ break;
+ }
+ },
+});
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CrashService]);
diff --git a/toolkit/components/crashes/CrashService.manifest b/toolkit/components/crashes/CrashService.manifest
new file mode 100644
index 0000000000..ed45109fea
--- /dev/null
+++ b/toolkit/components/crashes/CrashService.manifest
@@ -0,0 +1,3 @@
+component {92668367-1b17-4190-86b2-1061b2179744} CrashService.js
+contract @mozilla.org/crashservice;1 {92668367-1b17-4190-86b2-1061b2179744}
+category profile-after-change CrashService @mozilla.org/crashservice;1
diff --git a/toolkit/components/crashes/docs/crash-events.rst b/toolkit/components/crashes/docs/crash-events.rst
new file mode 100644
index 0000000000..b29b279890
--- /dev/null
+++ b/toolkit/components/crashes/docs/crash-events.rst
@@ -0,0 +1,176 @@
+============
+Crash Events
+============
+
+**Crash Events** refers to a special subsystem of Gecko that aims to capture
+events of interest related to process crashing and hanging.
+
+When an event worthy of recording occurs, a file containing that event's
+information is written to a well-defined location on the filesystem. The Gecko
+process periodically scans for produced files and consolidates information
+into a more unified and efficient backend store.
+
+Crash Event Files
+=================
+
+When a crash-related event occurs, a file describing that event is written
+to a well-defined directory. That directory is likely in the directory of
+the currently-active profile. However, if a profile is not yet active in
+the Gecko process, that directory likely resides in the user's *app data*
+directory (*UAppData* from the directory service).
+
+The filename of the event file is not relevant. However, producers need
+to choose a filename intelligently to avoid name collisions and race
+conditions. Since file locking is potentially dangerous at crash time,
+the convention of generating a UUID and using it as a filename has been
+adopted.
+
+File Format
+-----------
+
+All crash event files share the same high-level file format. The format
+consists of the following fields delimited by a UNIX newline (*\n*)
+character:
+
+* String event name (valid UTF-8, but likely ASCII)
+* String representation of integer seconds since UNIX epoch
+* Payload
+
+The payload is event specific and may contain UNIX newline characters.
+The recommended method for parsing is to split at most 3 times on UNIX
+newline and then dispatch to an event-specific parsed based on the
+event name.
+
+If an unknown event type is encountered, the event can safely be ignored
+until later. This helps ensure that application downgrades (potentially
+due to elevated crash rate) don't result in data loss.
+
+The format and semantics of each event type are meant to be constant once
+that event type is committed to the main Firefox repository. If new metadata
+needs to be captured or the meaning of data captured in an event changes,
+that change should be expressed through the invention of a new event type.
+For this reason, event names are highly recommended to contain a version.
+e.g. instead of a *Gecko process crashed* event, we prefer a *Gecko process
+crashed v1* event.
+
+Event Types
+-----------
+
+Each subsection documents the different types of crash events that may be
+produced. Each section name corresponds to the first line of the crash
+event file.
+
+Currently only main process crashes produce event files. Because crashes and
+hangs in child processes can be easily recorded by the main process, we do not
+foresee the need for writing event files for child processes, design
+considerations below notwithstanding.
+
+crash.main.2
+^^^^^^^^^^^^
+
+This event is produced when the main process crashes.
+
+The payload of this event is delimited by UNIX newlines (*\n*) and contains the
+following fields:
+
+* The crash ID string, very likely a UUID
+* 0 or more lines of metadata, each containing one key=value pair of text
+
+crash.main.1
+^^^^^^^^^^^^
+
+This event is produced when the main process crashes.
+
+The payload of this event is the string crash ID, very likely a UUID.
+There should be ``UUID.dmp`` and ``UUID.extra`` files on disk, saved by
+Breakpad.
+
+crash.submission.1
+^^^^^^^^^^^^^^^^^^
+
+This event is produced when a crash is submitted.
+
+The payload of this event is delimited by UNIX newlines (*\n*) and contains the
+following fields:
+
+* The crash ID string
+* "true" if the submission succeeded or "false" otherwise
+* The remote crash ID string if the submission succeeded
+
+Aggregated Event Log
+====================
+
+Crash events are aggregated together into a unified event *log*. Currently,
+this *log* is really a JSON file. However, this is an implementation detail
+and it could change at any time. The interface to crash data provided by
+the JavaScript API is the only supported interface.
+
+Design Considerations
+=====================
+
+There are many considerations influencing the design of this subsystem.
+We attempt to document them in this section.
+
+Decoupling of Event Files from Final Data Structure
+---------------------------------------------------
+
+While it is certainly possible for the Gecko process to write directly to
+the final data structure on disk, there is an intentional decoupling between
+the production of events and their transition into final storage. Along the
+same vein, the choice to have events written to multiple files by producers
+is deliberate.
+
+Some recorded events are written immediately after a process crash. This is
+a very uncertain time for the host system. There is a high liklihood the
+system is in an exceptional state, such as memory exhaustion. Therefore, any
+action taken after crashing needs to be very deliberate about what it does.
+Excessive memory allocation and certain system calls may cause the system
+to crash again or the machine's condition to worsen. This means that the act
+of recording a crash event must be very light weight. Writing a new file from
+nothing is very light weight. This is one reason we write separate files.
+
+Another reason we write separate files is because if the main Gecko process
+itself crashes (as opposed to say a plugin process), the crash reporter (not
+Gecko) is running and the crash reporter needs to handle the writing of the
+event info. If this writing is involved (say loading, parsing, updating, and
+reserializing back to disk), this logic would need to be implemented in both
+Gecko and the crash reporter or would need to be implemented in such a way
+that both could use. Neither of these is very practical from a software
+lifecycle management perspective. It's much easier to have separate processes
+write a simple file and to let a single implementation do all the complex
+work.
+
+Idempotent Event Processing
+===========================
+
+Processing of event files has been designed such that the result is
+idempotent regardless of what order those files are processed in. This is
+not only a good design decision, but it is arguably necessary. While event
+files are processed in order by file mtime, filesystem times may not have
+the resolution required for proper sorting. Therefore, processing order is
+merely an optimistic assumption.
+
+Aggregated Storage Format
+=========================
+
+Crash events are aggregated into a unified data structure on disk. That data
+structure is currently LZ4-compressed JSON and is represented by a single file.
+
+The choice of a single JSON file was initially driven by time and complexity
+concerns. Before changing the format or adding significant amounts of new
+data, some considerations must be taken into account.
+
+First, in well-behaving installs, crash data should be minimal. Crashes and
+hangs will be rare and thus the size of the crash data should remain small
+over time.
+
+The choice of a single JSON file has larger implications as the amount of
+crash data grows. As new data is accumulated, we need to read and write
+an entire file to make small updates. LZ4 compression helps reduce I/O.
+But, there is a potential for unbounded file growth. We establish a
+limit for the max age of records. Anything older than that limit is
+pruned. We also establish a daily limit on the number of crashes we will
+store. All crashes beyond the first N in a day have no payload and are
+only recorded by the presence of a count. This count ensures we can
+distinguish between ``N`` and ``100 * N``, which are very different
+values!
diff --git a/toolkit/components/crashes/docs/index.rst b/toolkit/components/crashes/docs/index.rst
new file mode 100644
index 0000000000..e2ab50ea4d
--- /dev/null
+++ b/toolkit/components/crashes/docs/index.rst
@@ -0,0 +1,24 @@
+.. _crashes_crashmanager:
+
+=============
+Crash Manager
+=============
+
+The **Crash Manager** is a service and interface for managing crash
+data within the Gecko application.
+
+From JavaScript, the service can be accessed via::
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ let crashManager = Services.crashmanager;
+
+That will give you an instance of ``CrashManager`` from ``CrashManager.jsm``.
+From there, you can access and manipulate crash data.
+
+Other Documents
+===============
+
+.. toctree::
+ :maxdepth: 1
+
+ crash-events
diff --git a/toolkit/components/crashes/moz.build b/toolkit/components/crashes/moz.build
new file mode 100644
index 0000000000..5a36a3cd3c
--- /dev/null
+++ b/toolkit/components/crashes/moz.build
@@ -0,0 +1,31 @@
+# -*- 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/.
+
+SPHINX_TREES['crash-manager'] = 'docs'
+
+EXTRA_COMPONENTS += [
+ 'CrashService.js',
+ 'CrashService.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'CrashManager.jsm',
+]
+
+TESTING_JS_MODULES += [
+ 'CrashManagerTest.jsm',
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+XPIDL_MODULE = 'toolkit_crashservice'
+
+XPIDL_SOURCES += [
+ 'nsICrashService.idl',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Breakpad Integration')
diff --git a/toolkit/components/crashes/nsICrashService.idl b/toolkit/components/crashes/nsICrashService.idl
new file mode 100644
index 0000000000..57a412804b
--- /dev/null
+++ b/toolkit/components/crashes/nsICrashService.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(f60d76e5-62c3-4f58-89f6-b726c2b7bc20)]
+interface nsICrashService : nsISupports
+{
+ /**
+ * Records the occurrence of a crash.
+ *
+ * @param processType
+ * One of the PROCESS_TYPE constants defined below.
+ * @param crashType
+ * One of the CRASH_TYPE constants defined below.
+ * @param id
+ * Crash ID. Likely a UUID.
+ */
+ void addCrash(in long processType, in long crashType, in AString id);
+
+ const long PROCESS_TYPE_MAIN = 0;
+ const long PROCESS_TYPE_CONTENT = 1;
+ const long PROCESS_TYPE_PLUGIN = 2;
+ const long PROCESS_TYPE_GMPLUGIN = 3;
+ const long PROCESS_TYPE_GPU = 4;
+
+ const long CRASH_TYPE_CRASH = 0;
+ const long CRASH_TYPE_HANG = 1;
+};
diff --git a/toolkit/components/crashes/tests/xpcshell/.eslintrc.js b/toolkit/components/crashes/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/crashes/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
new file mode 100644
index 0000000000..9844e78c4e
--- /dev/null
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
@@ -0,0 +1,494 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
+
+Cu.import("resource://testing-common/CrashManagerTest.jsm", this);
+Cu.import("resource://testing-common/TelemetryArchiveTesting.jsm", this);
+
+const DUMMY_DATE = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
+DUMMY_DATE.setMilliseconds(0);
+
+const DUMMY_DATE_2 = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000);
+DUMMY_DATE_2.setMilliseconds(0);
+
+function run_test() {
+ do_get_profile();
+ configureLogging();
+ TelemetryArchiveTesting.setup();
+ run_next_test();
+}
+
+add_task(function* test_constructor_ok() {
+ let m = new CrashManager({
+ pendingDumpsDir: "/foo",
+ submittedDumpsDir: "/bar",
+ eventsDirs: [],
+ storeDir: "/baz",
+ });
+ Assert.ok(m, "CrashManager can be created.");
+});
+
+add_task(function* test_constructor_invalid() {
+ Assert.throws(() => {
+ new CrashManager({foo: true});
+ });
+});
+
+add_task(function* test_get_manager() {
+ let m = yield getManager();
+ Assert.ok(m, "CrashManager obtained.");
+
+ yield m.createDummyDump(true);
+ yield m.createDummyDump(false);
+});
+
+// Unsubmitted dump files on disk are detected properly.
+add_task(function* test_pending_dumps() {
+ let m = yield getManager();
+ let now = Date.now();
+ let ids = [];
+ const COUNT = 5;
+
+ for (let i = 0; i < COUNT; i++) {
+ ids.push(yield m.createDummyDump(false, new Date(now - i * 86400000)));
+ }
+ yield m.createIgnoredDumpFile("ignored", false);
+
+ let entries = yield m.pendingDumps();
+ Assert.equal(entries.length, COUNT, "proper number detected.");
+
+ for (let entry of entries) {
+ Assert.equal(typeof(entry), "object", "entry is an object");
+ Assert.ok("id" in entry, "id in entry");
+ Assert.ok("path" in entry, "path in entry");
+ Assert.ok("date" in entry, "date in entry");
+ Assert.notEqual(ids.indexOf(entry.id), -1, "ID is known");
+ }
+
+ for (let i = 0; i < COUNT; i++) {
+ Assert.equal(entries[i].id, ids[COUNT-i-1], "Entries sorted by mtime");
+ }
+});
+
+// Submitted dump files on disk are detected properly.
+add_task(function* test_submitted_dumps() {
+ let m = yield getManager();
+ let COUNT = 5;
+
+ for (let i = 0; i < COUNT; i++) {
+ yield m.createDummyDump(true);
+ }
+ yield m.createIgnoredDumpFile("ignored", true);
+
+ let entries = yield m.submittedDumps();
+ Assert.equal(entries.length, COUNT, "proper number detected.");
+
+ let hrID = yield m.createDummyDump(true, new Date(), true);
+ entries = yield m.submittedDumps();
+ Assert.equal(entries.length, COUNT + 1, "hr- in filename detected.");
+
+ let gotIDs = new Set(entries.map(e => e.id));
+ Assert.ok(gotIDs.has(hrID));
+});
+
+// The store should expire after inactivity.
+add_task(function* test_store_expires() {
+ let m = yield getManager();
+
+ Object.defineProperty(m, "STORE_EXPIRATION_MS", {
+ value: 250,
+ });
+
+ let store = yield m._getStore();
+ Assert.ok(store);
+ Assert.equal(store, m._store);
+
+ yield sleep(300);
+ Assert.ok(!m._store, "Store has gone away.");
+});
+
+// Ensure discovery of unprocessed events files works.
+add_task(function* test_unprocessed_events_files() {
+ let m = yield getManager();
+ yield m.createEventsFile("1", "test.1", new Date(), "foo", 0);
+ yield m.createEventsFile("2", "test.1", new Date(), "bar", 0);
+ yield m.createEventsFile("1", "test.1", new Date(), "baz", 1);
+
+ let paths = yield m._getUnprocessedEventsFiles();
+ Assert.equal(paths.length, 3);
+});
+
+// Ensure only 1 aggregateEventsFiles() is allowed at a time.
+add_task(function* test_aggregate_events_locking() {
+ let m = yield getManager();
+
+ let p1 = m.aggregateEventsFiles();
+ let p2 = m.aggregateEventsFiles();
+
+ Assert.strictEqual(p1, p2, "Same promise should be returned.");
+});
+
+// Malformed events files should be deleted.
+add_task(function* test_malformed_files_deleted() {
+ let m = yield getManager();
+
+ yield m.createEventsFile("1", "crash.main.1", new Date(), "foo\nbar");
+
+ let count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 1);
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 0);
+
+ count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 0);
+});
+
+// Unknown event types should be ignored.
+add_task(function* test_aggregate_ignore_unknown_events() {
+ let m = yield getManager();
+
+ yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "id1");
+ yield m.createEventsFile("2", "foobar.1", new Date(), "dummy");
+
+ let count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 2);
+
+ count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 1);
+
+ count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 1);
+});
+
+add_task(function* test_prune_old() {
+ let m = yield getManager();
+ let oldDate = new Date(Date.now() - 86400000);
+ let newDate = new Date(Date.now() - 10000);
+ yield m.createEventsFile("1", "crash.main.2", oldDate, "id1");
+ yield m.addCrash(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_CRASH, "id2", newDate);
+
+ yield m.aggregateEventsFiles();
+
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 2);
+
+ yield m.pruneOldCrashes(new Date(oldDate.getTime() + 10000));
+
+ crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 1, "Old crash has been pruned.");
+
+ let c = crashes[0];
+ Assert.equal(c.id, "id2", "Proper crash was pruned.");
+
+ // We can't test exact boundary conditions because dates from filesystem
+ // don't have same guarantees as JS dates.
+ yield m.pruneOldCrashes(new Date(newDate.getTime() + 5000));
+ crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 0);
+});
+
+add_task(function* test_schedule_maintenance() {
+ let m = yield getManager();
+ yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "id1");
+
+ let oldDate = new Date(Date.now() - m.PURGE_OLDER_THAN_DAYS * 2 * 24 * 60 * 60 * 1000);
+ yield m.createEventsFile("2", "crash.main.2", oldDate, "id2");
+
+ yield m.scheduleMaintenance(25);
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 1);
+ Assert.equal(crashes[0].id, "id1");
+});
+
+add_task(function* test_main_crash_event_file() {
+ let ac = new TelemetryArchiveTesting.Checker();
+ yield ac.promiseInit();
+ let theEnvironment = TelemetryEnvironment.currentEnvironment;
+ let sessionId = "be66af2f-2ee5-4330-ae95-44462dfbdf0c";
+ let stackTraces = { status: "OK" };
+
+ // To test proper escaping, add data to the environment with an embedded
+ // double-quote
+ theEnvironment.testValue = "MyValue\"";
+
+ let m = yield getManager();
+ const fileContent = "id1\nk1=v1\nk2=v2\n" +
+ "TelemetryEnvironment=" + JSON.stringify(theEnvironment) + "\n" +
+ "TelemetrySessionId=" + sessionId + "\n" +
+ "StackTraces=" + JSON.stringify(stackTraces) + "\n";
+
+ yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, fileContent);
+ let count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 1);
+
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 1);
+ Assert.equal(crashes[0].id, "id1");
+ Assert.equal(crashes[0].type, "main-crash");
+ Assert.equal(crashes[0].metadata.k1, "v1");
+ Assert.equal(crashes[0].metadata.k2, "v2");
+ Assert.ok(crashes[0].metadata.TelemetryEnvironment);
+ Assert.equal(Object.getOwnPropertyNames(crashes[0].metadata).length, 5);
+ Assert.equal(crashes[0].metadata.TelemetrySessionId, sessionId);
+ Assert.ok(crashes[0].metadata.StackTraces);
+ Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
+
+ let found = yield ac.promiseFindPing("crash", [
+ [["payload", "hasCrashEnvironment"], true],
+ [["payload", "metadata", "k1"], "v1"],
+ [["payload", "crashId"], "1"],
+ [["payload", "stackTraces", "status"], "OK"],
+ [["payload", "sessionId"], sessionId],
+ ]);
+ Assert.ok(found, "Telemetry ping submitted for found crash");
+ Assert.deepEqual(found.environment, theEnvironment, "The saved environment should be present");
+
+ count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 0);
+});
+
+add_task(function* test_main_crash_event_file_noenv() {
+ let ac = new TelemetryArchiveTesting.Checker();
+ yield ac.promiseInit();
+
+ let m = yield getManager();
+ yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "id1\nk1=v3\nk2=v2");
+ let count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 1);
+
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 1);
+ Assert.equal(crashes[0].id, "id1");
+ Assert.equal(crashes[0].type, "main-crash");
+ Assert.deepEqual(crashes[0].metadata, { k1: "v3", k2: "v2"});
+ Assert.deepEqual(crashes[0].crashDate, DUMMY_DATE);
+
+ let found = yield ac.promiseFindPing("crash", [
+ [["payload", "hasCrashEnvironment"], false],
+ [["payload", "metadata", "k1"], "v3"],
+ ]);
+ Assert.ok(found, "Telemetry ping submitted for found crash");
+ Assert.ok(found.environment, "There is an environment");
+
+ count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 0);
+});
+
+add_task(function* test_crash_submission_event_file() {
+ let m = yield getManager();
+ yield m.createEventsFile("1", "crash.main.2", DUMMY_DATE, "crash1");
+ yield m.createEventsFile("1-submission", "crash.submission.1", DUMMY_DATE_2,
+ "crash1\nfalse\n");
+
+ // The line below has been intentionally commented out to make sure that
+ // the crash record is created when one does not exist.
+ // yield m.createEventsFile("2", "crash.main.1", DUMMY_DATE, "crash2");
+ yield m.createEventsFile("2-submission", "crash.submission.1", DUMMY_DATE_2,
+ "crash2\ntrue\nbp-2");
+ let count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 3);
+
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 2);
+
+ let map = new Map(crashes.map(crash => [crash.id, crash]));
+
+ let crash1 = map.get("crash1");
+ Assert.ok(!!crash1);
+ Assert.equal(crash1.remoteID, null);
+ let crash2 = map.get("crash2");
+ Assert.ok(!!crash2);
+ Assert.equal(crash2.remoteID, "bp-2");
+
+ Assert.equal(crash1.submissions.size, 1);
+ let submission = crash1.submissions.values().next().value;
+ Assert.equal(submission.result, m.SUBMISSION_RESULT_FAILED);
+ Assert.equal(submission.requestDate.getTime(), DUMMY_DATE_2.getTime());
+ Assert.equal(submission.responseDate.getTime(), DUMMY_DATE_2.getTime());
+
+ Assert.equal(crash2.submissions.size, 1);
+ submission = crash2.submissions.values().next().value;
+ Assert.equal(submission.result, m.SUBMISSION_RESULT_OK);
+ Assert.equal(submission.requestDate.getTime(), DUMMY_DATE_2.getTime());
+ Assert.equal(submission.responseDate.getTime(), DUMMY_DATE_2.getTime());
+
+ count = yield m.aggregateEventsFiles();
+ Assert.equal(count, 0);
+});
+
+add_task(function* test_multiline_crash_id_rejected() {
+ let m = yield getManager();
+ yield m.createEventsFile("1", "crash.main.1", DUMMY_DATE, "id1\nid2");
+ yield m.aggregateEventsFiles();
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 0);
+});
+
+// Main process crashes should be remembered beyond the high water mark.
+add_task(function* test_high_water_mark() {
+ let m = yield getManager();
+
+ let store = yield m._getStore();
+
+ for (let i = 0; i < store.HIGH_WATER_DAILY_THRESHOLD + 1; i++) {
+ yield m.createEventsFile("m" + i, "crash.main.2", DUMMY_DATE, "m" + i);
+ }
+
+ let count = yield m.aggregateEventsFiles();
+ Assert.equal(count, bsp.CrashStore.prototype.HIGH_WATER_DAILY_THRESHOLD + 1);
+
+ // Need to fetch again in case the first one was garbage collected.
+ store = yield m._getStore();
+
+ Assert.equal(store.crashesCount, store.HIGH_WATER_DAILY_THRESHOLD + 1);
+});
+
+add_task(function* test_addCrash() {
+ let m = yield getManager();
+
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 0);
+
+ yield m.addCrash(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH,
+ "main-crash", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_HANG,
+ "main-hang", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_CRASH,
+ "content-crash", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_HANG,
+ "content-hang", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_CRASH,
+ "plugin-crash", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_HANG,
+ "plugin-hang", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_GMPLUGIN, m.CRASH_TYPE_CRASH,
+ "gmplugin-crash", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_GPU, m.CRASH_TYPE_CRASH,
+ "gpu-crash", DUMMY_DATE);
+
+ yield m.addCrash(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH,
+ "changing-item", DUMMY_DATE);
+ yield m.addCrash(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_HANG,
+ "changing-item", DUMMY_DATE_2);
+
+ crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 9);
+
+ let map = new Map(crashes.map(crash => [crash.id, crash]));
+
+ let crash = map.get("main-crash");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_MAIN + "-" + m.CRASH_TYPE_CRASH);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH));
+
+ crash = map.get("main-hang");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_MAIN + "-" + m.CRASH_TYPE_HANG);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_HANG));
+
+ crash = map.get("content-crash");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_CONTENT + "-" + m.CRASH_TYPE_CRASH);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_CRASH));
+
+ crash = map.get("content-hang");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_CONTENT + "-" + m.CRASH_TYPE_HANG);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_HANG));
+
+ crash = map.get("plugin-crash");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_PLUGIN + "-" + m.CRASH_TYPE_CRASH);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_CRASH));
+
+ crash = map.get("plugin-hang");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_PLUGIN + "-" + m.CRASH_TYPE_HANG);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_HANG));
+
+ crash = map.get("gmplugin-crash");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_GMPLUGIN + "-" + m.CRASH_TYPE_CRASH);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_GMPLUGIN, m.CRASH_TYPE_CRASH));
+
+ crash = map.get("gpu-crash");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE);
+ Assert.equal(crash.type, m.PROCESS_TYPE_GPU+ "-" + m.CRASH_TYPE_CRASH);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_GPU, m.CRASH_TYPE_CRASH));
+
+ crash = map.get("changing-item");
+ Assert.ok(!!crash);
+ Assert.equal(crash.crashDate, DUMMY_DATE_2);
+ Assert.equal(crash.type, m.PROCESS_TYPE_CONTENT + "-" + m.CRASH_TYPE_HANG);
+ Assert.ok(crash.isOfType(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_HANG));
+});
+
+add_task(function* test_generateSubmissionID() {
+ let m = yield getManager();
+
+ const SUBMISSION_ID_REGEX =
+ /^(sub-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
+ let id = m.generateSubmissionID();
+ Assert.ok(SUBMISSION_ID_REGEX.test(id));
+});
+
+add_task(function* test_addSubmissionAttemptAndResult() {
+ let m = yield getManager();
+
+ let crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 0);
+
+ yield m.addCrash(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH,
+ "main-crash", DUMMY_DATE);
+ yield m.addSubmissionAttempt("main-crash", "submission", DUMMY_DATE);
+ yield m.addSubmissionResult("main-crash", "submission", DUMMY_DATE_2,
+ m.SUBMISSION_RESULT_OK);
+
+ crashes = yield m.getCrashes();
+ Assert.equal(crashes.length, 1);
+
+ let submissions = crashes[0].submissions;
+ Assert.ok(!!submissions);
+
+ let submission = submissions.get("submission");
+ Assert.ok(!!submission);
+ Assert.equal(submission.requestDate.getTime(), DUMMY_DATE.getTime());
+ Assert.equal(submission.responseDate.getTime(), DUMMY_DATE_2.getTime());
+ Assert.equal(submission.result, m.SUBMISSION_RESULT_OK);
+});
+
+add_task(function* test_setCrashClassifications() {
+ let m = yield getManager();
+
+ yield m.addCrash(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH,
+ "main-crash", DUMMY_DATE);
+ yield m.setCrashClassifications("main-crash", ["a"]);
+ let classifications = (yield m.getCrashes())[0].classifications;
+ Assert.ok(classifications.indexOf("a") != -1);
+});
+
+add_task(function* test_setRemoteCrashID() {
+ let m = yield getManager();
+
+ yield m.addCrash(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH,
+ "main-crash", DUMMY_DATE);
+ yield m.setRemoteCrashID("main-crash", "bp-1");
+ Assert.equal((yield m.getCrashes())[0].remoteID, "bp-1");
+});
diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_service.js b/toolkit/components/crashes/tests/xpcshell/test_crash_service.js
new file mode 100644
index 0000000000..c207057e01
--- /dev/null
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_service.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://testing-common/AppData.jsm", this);
+var bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_instantiation() {
+ Assert.ok(!bsp.gCrashManager, "CrashManager global instance not initially defined.");
+
+ do_get_profile();
+ yield makeFakeAppDir();
+
+ // Fake profile creation.
+ Cc["@mozilla.org/crashservice;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "profile-after-change", null);
+
+ Assert.ok(bsp.gCrashManager, "Profile creation makes it available.");
+ Assert.ok(Services.crashmanager, "CrashManager available via Services.");
+ Assert.strictEqual(bsp.gCrashManager, Services.crashmanager,
+ "The objects are the same.");
+});
diff --git a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js
new file mode 100644
index 0000000000..12b180e91c
--- /dev/null
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js
@@ -0,0 +1,587 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This file tests the CrashStore type in CrashManager.jsm.
+ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+const DUMMY_DATE = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000);
+DUMMY_DATE.setMilliseconds(0);
+
+const DUMMY_DATE_2 = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000);
+DUMMY_DATE_2.setMilliseconds(0);
+
+const {
+ PROCESS_TYPE_MAIN,
+ PROCESS_TYPE_CONTENT,
+ PROCESS_TYPE_PLUGIN,
+ PROCESS_TYPE_GMPLUGIN,
+ PROCESS_TYPE_GPU,
+ CRASH_TYPE_CRASH,
+ CRASH_TYPE_HANG,
+ SUBMISSION_RESULT_OK,
+ SUBMISSION_RESULT_FAILED,
+} = CrashManager.prototype;
+
+const CrashStore = bsp.CrashStore;
+
+var STORE_DIR_COUNT = 0;
+
+function getStore() {
+ return Task.spawn(function* () {
+ let storeDir = do_get_tempdir().path;
+ storeDir = OS.Path.join(storeDir, "store-" + STORE_DIR_COUNT++);
+
+ yield OS.File.makeDir(storeDir, {unixMode: OS.Constants.libc.S_IRWXU});
+
+ let s = new CrashStore(storeDir);
+ yield s.load();
+
+ return s;
+ });
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_constructor() {
+ let s = new CrashStore("/some/path");
+ Assert.ok(s instanceof CrashStore);
+});
+
+add_task(function* test_add_crash() {
+ let s = yield getStore();
+
+ Assert.equal(s.crashesCount, 0);
+ let d = new Date(Date.now() - 5000);
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id1", d));
+
+ Assert.equal(s.crashesCount, 1);
+
+ let crashes = s.crashes;
+ Assert.equal(crashes.length, 1);
+ let c = crashes[0];
+
+ Assert.equal(c.id, "id1", "ID set properly.");
+ Assert.equal(c.crashDate.getTime(), d.getTime(), "Date set.");
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+});
+
+add_task(function* test_reset() {
+ let s = yield getStore();
+
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id1", DUMMY_DATE));
+ Assert.equal(s.crashes.length, 1);
+ s.reset();
+ Assert.equal(s.crashes.length, 0);
+});
+
+add_task(function* test_save_load() {
+ let s = yield getStore();
+
+ yield s.save();
+
+ let d1 = new Date();
+ let d2 = new Date(d1.getTime() - 10000);
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id1", d1));
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id2", d2));
+ Assert.ok(s.addSubmissionAttempt("id1", "sub1", d1));
+ Assert.ok(s.addSubmissionResult("id1", "sub1", d2, SUBMISSION_RESULT_OK));
+ Assert.ok(s.setRemoteCrashID("id1", "bp-1"));
+
+ yield s.save();
+
+ yield s.load();
+ Assert.ok(!s.corruptDate);
+ let crashes = s.crashes;
+
+ Assert.equal(crashes.length, 2);
+ let c = s.getCrash("id1");
+ Assert.equal(c.crashDate.getTime(), d1.getTime());
+ Assert.equal(c.remoteID, "bp-1");
+
+ Assert.ok(!!c.submissions);
+ let submission = c.submissions.get("sub1");
+ Assert.ok(!!submission);
+ Assert.equal(submission.requestDate.getTime(), d1.getTime());
+ Assert.equal(submission.responseDate.getTime(), d2.getTime());
+ Assert.equal(submission.result, SUBMISSION_RESULT_OK);
+});
+
+add_task(function* test_corrupt_json() {
+ let s = yield getStore();
+
+ let buffer = new TextEncoder().encode("{bad: json-file");
+ yield OS.File.writeAtomic(s._storePath, buffer, {compression: "lz4"});
+
+ yield s.load();
+ Assert.ok(s.corruptDate, "Corrupt date is defined.");
+
+ let date = s.corruptDate;
+ yield s.save();
+ s._data = null;
+ yield s.load();
+ Assert.ok(s.corruptDate);
+ Assert.equal(date.getTime(), s.corruptDate.getTime());
+});
+
+add_task(function* test_add_main_crash() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH);
+ Assert.ok(c.isOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ // Duplicate.
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "id3", new Date(),
+ { OOMAllocationSize: 1048576 })
+ );
+ Assert.equal(s.crashesCount, 3);
+ Assert.deepEqual(s.crashes[2].metadata, { OOMAllocationSize: 1048576 });
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 3);
+});
+
+add_task(function* test_add_main_hang() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_HANG);
+ Assert.ok(c.isOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_content_crash() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_CONTENT + "-" + CRASH_TYPE_CRASH);
+ Assert.ok(c.isOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_content_hang() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_CONTENT + "-" + CRASH_TYPE_HANG);
+ Assert.ok(c.isOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_plugin_crash() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_CRASH);
+ Assert.ok(c.isOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_plugin_hang() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_HANG);
+ Assert.ok(c.isOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_gmplugin_crash() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_GMPLUGIN + "-" + CRASH_TYPE_CRASH);
+ Assert.ok(c.isOfType(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_gpu_crash() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 1);
+
+ let c = s.crashes[0];
+ Assert.ok(c.crashDate);
+ Assert.equal(c.type, PROCESS_TYPE_GPU + "-" + CRASH_TYPE_CRASH);
+ Assert.ok(c.isOfType(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH));
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH, "id2", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH, "id1", new Date())
+ );
+ Assert.equal(s.crashesCount, 2);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2);
+});
+
+add_task(function* test_add_mixed_types() {
+ let s = yield getStore();
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "mcrash", new Date()) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "mhang", new Date()) &&
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "ccrash", new Date()) &&
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "chang", new Date()) &&
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "pcrash", new Date()) &&
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "phang", new Date()) &&
+ s.addCrash(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH, "gmpcrash", new Date()) &&
+ s.addCrash(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH, "gpucrash", new Date())
+ );
+
+ Assert.equal(s.crashesCount, 8);
+
+ yield s.save();
+
+ s._data.crashes.clear();
+ Assert.equal(s.crashesCount, 0);
+
+ yield s.load();
+
+ Assert.equal(s.crashesCount, 8);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_GMPLUGIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 1);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_GPU, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 1);
+});
+
+// Crashes added beyond the high water mark behave properly.
+add_task(function* test_high_water() {
+ let s = yield getStore();
+
+ let d1 = new Date(2014, 0, 1, 0, 0, 0);
+ let d2 = new Date(2014, 0, 2, 0, 0, 0);
+
+ let i = 0;
+ for (; i < s.HIGH_WATER_DAILY_THRESHOLD; i++) {
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "mc1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "mc2" + i, d2) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "mh1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "mh2" + i, d2) &&
+
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "cc1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "cc2" + i, d2) &&
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "ch1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "ch2" + i, d2) &&
+
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "pc1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "pc2" + i, d2) &&
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "ph1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "ph2" + i, d2)
+ );
+ }
+
+ Assert.ok(
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "mc1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "mc2" + i, d2) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "mh1" + i, d1) &&
+ s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "mh2" + i, d2)
+ );
+
+ Assert.ok(!s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "cc1" + i, d1));
+ Assert.ok(!s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "cc2" + i, d2));
+ Assert.ok(!s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "ch1" + i, d1));
+ Assert.ok(!s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "ch2" + i, d2));
+
+ Assert.ok(!s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "pc1" + i, d1));
+ Assert.ok(!s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH, "pc2" + i, d2));
+ Assert.ok(!s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "ph1" + i, d1));
+ Assert.ok(!s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "ph2" + i, d2));
+
+ // We preserve main process crashes and hangs. Content and plugin crashes and
+ // hangs beyond should be discarded.
+ Assert.equal(s.crashesCount, 12 * s.HIGH_WATER_DAILY_THRESHOLD + 4);
+
+ let crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD + 2);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD + 2);
+
+ crashes = s.getCrashesOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD);
+
+ crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_CRASH);
+ Assert.equal(crashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD);
+ crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG);
+ Assert.equal(crashes.length, 2 * s.HIGH_WATER_DAILY_THRESHOLD);
+
+ // But raw counts should be preserved.
+ let day1 = bsp.dateToDays(d1);
+ let day2 = bsp.dateToDays(d2);
+ Assert.ok(s._countsByDay.has(day1));
+ Assert.ok(s._countsByDay.has(day2));
+
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_HANG),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_CONTENT + "-" + CRASH_TYPE_CRASH),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_CONTENT + "-" + CRASH_TYPE_HANG),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_CRASH),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_HANG),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+
+ yield s.save();
+ yield s.load();
+
+ Assert.ok(s._countsByDay.has(day1));
+ Assert.ok(s._countsByDay.has(day2));
+
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_HANG),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_CONTENT + "-" + CRASH_TYPE_CRASH),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_CONTENT + "-" + CRASH_TYPE_HANG),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_CRASH),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+ Assert.equal(s._countsByDay.get(day1).
+ get(PROCESS_TYPE_PLUGIN + "-" + CRASH_TYPE_HANG),
+ s.HIGH_WATER_DAILY_THRESHOLD + 1);
+});
+
+add_task(function* test_addSubmission() {
+ let s = yield getStore();
+
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
+ DUMMY_DATE));
+
+ Assert.ok(s.addSubmissionAttempt("crash1", "sub1", DUMMY_DATE));
+
+ let crash = s.getCrash("crash1");
+ let submission = crash.submissions.get("sub1");
+ Assert.ok(!!submission);
+ Assert.equal(submission.requestDate.getTime(), DUMMY_DATE.getTime());
+ Assert.equal(submission.responseDate, null);
+ Assert.equal(submission.result, null);
+
+ Assert.ok(s.addSubmissionResult("crash1", "sub1", DUMMY_DATE_2,
+ SUBMISSION_RESULT_FAILED));
+
+ crash = s.getCrash("crash1");
+ Assert.equal(crash.submissions.size, 1);
+ submission = crash.submissions.get("sub1");
+ Assert.ok(!!submission);
+ Assert.equal(submission.requestDate.getTime(), DUMMY_DATE.getTime());
+ Assert.equal(submission.responseDate.getTime(), DUMMY_DATE_2.getTime());
+ Assert.equal(submission.result, SUBMISSION_RESULT_FAILED);
+
+ Assert.ok(s.addSubmissionAttempt("crash1", "sub2", DUMMY_DATE));
+ Assert.ok(s.addSubmissionResult("crash1", "sub2", DUMMY_DATE_2,
+ SUBMISSION_RESULT_OK));
+
+ Assert.equal(crash.submissions.size, 2);
+ submission = crash.submissions.get("sub2");
+ Assert.ok(!!submission);
+ Assert.equal(submission.result, SUBMISSION_RESULT_OK);
+});
+
+add_task(function* test_setCrashClassification() {
+ let s = yield getStore();
+
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
+ new Date()));
+ let classifications = s.crashes[0].classifications;
+ Assert.ok(!!classifications);
+ Assert.equal(classifications.length, 0);
+
+ Assert.ok(s.setCrashClassifications("crash1", ["foo", "bar"]));
+ classifications = s.crashes[0].classifications;
+ Assert.equal(classifications.length, 2);
+ Assert.ok(classifications.indexOf("foo") != -1);
+ Assert.ok(classifications.indexOf("bar") != -1);
+});
+
+add_task(function* test_setRemoteCrashID() {
+ let s = yield getStore();
+
+ Assert.ok(s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "crash1",
+ new Date()));
+ Assert.equal(s.crashes[0].remoteID, null);
+ Assert.ok(s.setRemoteCrashID("crash1", "bp-1"));
+ Assert.equal(s.crashes[0].remoteID, "bp-1");
+});
+
diff --git a/toolkit/components/crashes/tests/xpcshell/xpcshell.ini b/toolkit/components/crashes/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..5cb8a69d53
--- /dev/null
+++ b/toolkit/components/crashes/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+
+[test_crash_manager.js]
+[test_crash_service.js]
+[test_crash_store.js]
diff --git a/toolkit/components/crashmonitor/CrashMonitor.jsm b/toolkit/components/crashmonitor/CrashMonitor.jsm
new file mode 100644
index 0000000000..34f4f26d14
--- /dev/null
+++ b/toolkit/components/crashmonitor/CrashMonitor.jsm
@@ -0,0 +1,224 @@
+/* -*- 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/. */
+
+/**
+ * Crash Monitor
+ *
+ * Monitors execution of a program to detect possible crashes. After
+ * program termination, the monitor can be queried during the next run
+ * to determine whether the last run exited cleanly or not.
+ *
+ * The monitoring is done by registering and listening for special
+ * notifications, or checkpoints, known to be sent by the monitored
+ * program as different stages in the execution are reached. As they
+ * are observed, these notifications are written asynchronously to a
+ * checkpoint file.
+ *
+ * During next program startup the crash monitor reads the checkpoint
+ * file from the last session. If notifications are missing, a crash
+ * has likely happened. By inspecting the notifications present, it is
+ * possible to determine what stages were reached in the program
+ * before the crash.
+ *
+ * Note that since the file is written asynchronously it is possible
+ * that a received notification is lost if the program crashes right
+ * after a checkpoint, but before crash monitor has been able to write
+ * it to disk. Thus, while the presence of a notification in the
+ * checkpoint file tells us that the corresponding stage was reached
+ * during the last run, the absence of a notification after a crash
+ * does not necessarily tell us that the checkpoint wasn't reached.
+ */
+
+this.EXPORTED_SYMBOLS = [ "CrashMonitor" ];
+
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+
+const NOTIFICATIONS = [
+ "final-ui-startup",
+ "sessionstore-windows-restored",
+ "quit-application-granted",
+ "quit-application",
+ "profile-change-net-teardown",
+ "profile-change-teardown",
+ "profile-before-change",
+ "sessionstore-final-state-write-complete"
+];
+
+var CrashMonitorInternal = {
+
+ /**
+ * Notifications received during the current session.
+ *
+ * Object where a property with a value of |true| means that the
+ * notification of the same name has been received at least once by
+ * the CrashMonitor during this session. Notifications that have not
+ * yet been received are not present as properties. |NOTIFICATIONS|
+ * lists the notifications tracked by the CrashMonitor.
+ */
+ checkpoints: {},
+
+ /**
+ * Notifications received during previous session.
+ *
+ * Available after |loadPreviousCheckpoints|. Promise which resolves
+ * to an object containing a set of properties, where a property
+ * with a value of |true| means that the notification with the same
+ * name as the property name was received at least once last
+ * session.
+ */
+ previousCheckpoints: null,
+
+ /* Deferred for AsyncShutdown blocker */
+ profileBeforeChangeDeferred: Promise.defer(),
+
+ /**
+ * Path to checkpoint file.
+ *
+ * Each time a new notification is received, this file is written to
+ * disc to reflect the information in |checkpoints|.
+ */
+ path: OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
+
+ /**
+ * Load checkpoints from previous session asynchronously.
+ *
+ * @return {Promise} A promise that resolves/rejects once loading is complete
+ */
+ loadPreviousCheckpoints: function () {
+ this.previousCheckpoints = Task.spawn(function*() {
+ let data;
+ try {
+ data = yield OS.File.read(CrashMonitorInternal.path, { encoding: "utf-8" });
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error)) {
+ throw ex;
+ }
+ if (!ex.becauseNoSuchFile) {
+ Cu.reportError("Error while loading crash monitor data: " + ex.toString());
+ }
+
+ return null;
+ }
+
+ let notifications;
+ try {
+ notifications = JSON.parse(data);
+ } catch (ex) {
+ Cu.reportError("Error while parsing crash monitor data: " + ex);
+ return null;
+ }
+
+ // If `notifications` isn't an object, then the monitor data isn't valid.
+ if (Object(notifications) !== notifications) {
+ Cu.reportError("Error while parsing crash monitor data: invalid monitor data");
+ return null;
+ }
+
+ return Object.freeze(notifications);
+ });
+
+ return this.previousCheckpoints;
+ }
+};
+
+this.CrashMonitor = {
+
+ /**
+ * Notifications received during previous session.
+ *
+ * Return object containing the set of notifications received last
+ * session as keys with values set to |true|.
+ *
+ * @return {Promise} A promise resolving to previous checkpoints
+ */
+ get previousCheckpoints() {
+ if (!CrashMonitorInternal.initialized) {
+ throw new Error("CrashMonitor must be initialized before getting previous checkpoints");
+ }
+
+ return CrashMonitorInternal.previousCheckpoints
+ },
+
+ /**
+ * Initialize CrashMonitor.
+ *
+ * Should only be called from the CrashMonitor XPCOM component.
+ *
+ * @return {Promise}
+ */
+ init: function () {
+ if (CrashMonitorInternal.initialized) {
+ throw new Error("CrashMonitor.init() must only be called once!");
+ }
+
+ let promise = CrashMonitorInternal.loadPreviousCheckpoints();
+ // Add "profile-after-change" to checkpoint as this method is
+ // called after receiving it
+ CrashMonitorInternal.checkpoints["profile-after-change"] = true;
+
+ NOTIFICATIONS.forEach(function (aTopic) {
+ Services.obs.addObserver(this, aTopic, false);
+ }, this);
+
+ // Add shutdown blocker for profile-before-change
+ OS.File.profileBeforeChange.addBlocker(
+ "CrashMonitor: Writing notifications to file after receiving profile-before-change",
+ CrashMonitorInternal.profileBeforeChangeDeferred.promise,
+ () => this.checkpoints
+ );
+
+ CrashMonitorInternal.initialized = true;
+ return promise;
+ },
+
+ /**
+ * Handle registered notifications.
+ *
+ * Update checkpoint file for every new notification received.
+ */
+ observe: function (aSubject, aTopic, aData) {
+ if (!(aTopic in CrashMonitorInternal.checkpoints)) {
+ // If this is the first time this notification is received,
+ // remember it and write it to file
+ CrashMonitorInternal.checkpoints[aTopic] = true;
+ Task.spawn(function* () {
+ try {
+ let data = JSON.stringify(CrashMonitorInternal.checkpoints);
+
+ /* Write to the checkpoint file asynchronously, off the main
+ * thread, for performance reasons. Note that this means
+ * that there's not a 100% guarantee that the file will be
+ * written by the time the notification completes. The
+ * exception is profile-before-change which has a shutdown
+ * blocker. */
+ yield OS.File.writeAtomic(
+ CrashMonitorInternal.path,
+ data, {tmpPath: CrashMonitorInternal.path + ".tmp"});
+
+ } finally {
+ // Resolve promise for blocker
+ if (aTopic == "profile-before-change") {
+ CrashMonitorInternal.profileBeforeChangeDeferred.resolve();
+ }
+ }
+ });
+ }
+
+ if (NOTIFICATIONS.every(elem => elem in CrashMonitorInternal.checkpoints)) {
+ // All notifications received, unregister observers
+ NOTIFICATIONS.forEach(function (aTopic) {
+ Services.obs.removeObserver(this, aTopic);
+ }, this);
+ }
+ }
+};
+Object.freeze(this.CrashMonitor);
diff --git a/toolkit/components/crashmonitor/crashmonitor.manifest b/toolkit/components/crashmonitor/crashmonitor.manifest
new file mode 100644
index 0000000000..59e336f82a
--- /dev/null
+++ b/toolkit/components/crashmonitor/crashmonitor.manifest
@@ -0,0 +1,3 @@
+component {d9d75e86-8f17-4c57-993e-f738f0d86d42} nsCrashMonitor.js
+contract @mozilla.org/toolkit/crashmonitor;1 {d9d75e86-8f17-4c57-993e-f738f0d86d42}
+category profile-after-change CrashMonitor @mozilla.org/toolkit/crashmonitor;1
diff --git a/toolkit/components/crashmonitor/moz.build b/toolkit/components/crashmonitor/moz.build
new file mode 100644
index 0000000000..4656f6ab8c
--- /dev/null
+++ b/toolkit/components/crashmonitor/moz.build
@@ -0,0 +1,16 @@
+# -*- 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 += ['test/unit/xpcshell.ini']
+
+EXTRA_JS_MODULES += [
+ 'CrashMonitor.jsm',
+]
+
+EXTRA_COMPONENTS += [
+ 'crashmonitor.manifest',
+ 'nsCrashMonitor.js',
+]
diff --git a/toolkit/components/crashmonitor/nsCrashMonitor.js b/toolkit/components/crashmonitor/nsCrashMonitor.js
new file mode 100644
index 0000000000..41e0dc9016
--- /dev/null
+++ b/toolkit/components/crashmonitor/nsCrashMonitor.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Scope = {}
+Components.utils.import("resource://gre/modules/CrashMonitor.jsm", Scope);
+var MonitorAPI = Scope.CrashMonitor;
+
+function CrashMonitor() {}
+
+CrashMonitor.prototype = {
+
+ classID: Components.ID("{d9d75e86-8f17-4c57-993e-f738f0d86d42}"),
+ contractID: "@mozilla.org/toolkit/crashmonitor;1",
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]),
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "profile-after-change":
+ MonitorAPI.init();
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CrashMonitor]);
diff --git a/toolkit/components/crashmonitor/test/unit/.eslintrc.js b/toolkit/components/crashmonitor/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/crashmonitor/test/unit/head.js b/toolkit/components/crashmonitor/test/unit/head.js
new file mode 100644
index 0000000000..6d7d50d0ca
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/head.js
@@ -0,0 +1,22 @@
+/* -*- 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+var sessionCheckpointsPath;
+
+/**
+ * Start the tasks of the different tests
+ */
+function run_test()
+{
+ do_get_profile();
+ sessionCheckpointsPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "sessionCheckpoints.json");
+ Components.utils.import("resource://gre/modules/CrashMonitor.jsm");
+ run_next_test();
+}
diff --git a/toolkit/components/crashmonitor/test/unit/test_init.js b/toolkit/components/crashmonitor/test/unit/test_init.js
new file mode 100644
index 0000000000..d72f46aca9
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/test_init.js
@@ -0,0 +1,17 @@
+/* -*- 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/. */
+
+/**
+ * Test that calling |init| twice throws an error
+ */
+add_task(function test_init() {
+ CrashMonitor.init();
+ try {
+ CrashMonitor.init();
+ do_check_true(false);
+ } catch (ex) {
+ do_check_true(true);
+ }
+});
diff --git a/toolkit/components/crashmonitor/test/unit/test_invalid_file.js b/toolkit/components/crashmonitor/test/unit/test_invalid_file.js
new file mode 100644
index 0000000000..cc55a2755a
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/test_invalid_file.js
@@ -0,0 +1,22 @@
+/* -*- 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/. */
+
+/**
+ * Test with sessionCheckpoints.json containing invalid data
+ */
+add_task(function* test_invalid_file() {
+ // Write bogus data to checkpoint file
+ let data = "1234";
+ yield OS.File.writeAtomic(sessionCheckpointsPath, data,
+ {tmpPath: sessionCheckpointsPath + ".tmp"});
+
+ // An invalid file will cause |init| to return null
+ let status = yield CrashMonitor.init();
+ do_check_true(status === null ? true : false);
+
+ // and |previousCheckpoints| will be null
+ let checkpoints = yield CrashMonitor.previousCheckpoints;
+ do_check_true(checkpoints === null ? true : false);
+});
diff --git a/toolkit/components/crashmonitor/test/unit/test_invalid_json.js b/toolkit/components/crashmonitor/test/unit/test_invalid_json.js
new file mode 100644
index 0000000000..f3b05208ac
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/test_invalid_json.js
@@ -0,0 +1,18 @@
+/* -*- 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/. */
+
+/**
+ * Test with sessionCheckpoints.json containing invalid JSON data
+ */
+add_task(function* test_invalid_file() {
+ // Write bogus data to checkpoint file
+ let data = "[}";
+ yield OS.File.writeAtomic(sessionCheckpointsPath, data,
+ {tmpPath: sessionCheckpointsPath + ".tmp"});
+
+ CrashMonitor.init();
+ let checkpoints = yield CrashMonitor.previousCheckpoints;
+ do_check_eq(checkpoints, null);
+});
diff --git a/toolkit/components/crashmonitor/test/unit/test_missing_file.js b/toolkit/components/crashmonitor/test/unit/test_missing_file.js
new file mode 100644
index 0000000000..9ce31da954
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/test_missing_file.js
@@ -0,0 +1,13 @@
+/* -*- 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/. */
+
+/**
+ * Test with non-existing sessionCheckpoints.json
+ */
+add_task(function* test_missing_file() {
+ CrashMonitor.init();
+ let checkpoints = yield CrashMonitor.previousCheckpoints;
+ do_check_eq(checkpoints, null);
+});
diff --git a/toolkit/components/crashmonitor/test/unit/test_register.js b/toolkit/components/crashmonitor/test/unit/test_register.js
new file mode 100644
index 0000000000..33c73a5aed
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/test_register.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/. */
+
+/**
+ * Test that CrashMonitor.jsm is correctly loaded from XPCOM component
+ */
+add_task(function test_register() {
+ let cm = Components.classes["@mozilla.org/toolkit/crashmonitor;1"]
+ .createInstance(Components.interfaces.nsIObserver);
+
+ // Send "profile-after-change" to trigger the initialization
+ cm.observe(null, "profile-after-change", null);
+
+ // If CrashMonitor was initialized properly a new call to |init|
+ // should fail
+ try {
+ CrashMonitor.init();
+ do_check_true(false);
+ } catch (ex) {
+ do_check_true(true);
+ }
+});
diff --git a/toolkit/components/crashmonitor/test/unit/test_valid_file.js b/toolkit/components/crashmonitor/test/unit/test_valid_file.js
new file mode 100644
index 0000000000..d2f214cc05
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/test_valid_file.js
@@ -0,0 +1,20 @@
+/* -*- 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/. */
+
+/**
+ * Test with sessionCheckpoints.json containing valid data
+ */
+add_task(function* test_valid_file() {
+ // Write valid data to checkpoint file
+ let data = JSON.stringify({"final-ui-startup": true});
+ yield OS.File.writeAtomic(sessionCheckpointsPath, data,
+ {tmpPath: sessionCheckpointsPath + ".tmp"});
+
+ CrashMonitor.init();
+ let checkpoints = yield CrashMonitor.previousCheckpoints;
+
+ do_check_true(checkpoints["final-ui-startup"]);
+ do_check_eq(Object.keys(checkpoints).length, 1);
+});
diff --git a/toolkit/components/crashmonitor/test/unit/xpcshell.ini b/toolkit/components/crashmonitor/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..cd86b2535e
--- /dev/null
+++ b/toolkit/components/crashmonitor/test/unit/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_init.js]
+[test_valid_file.js]
+[test_invalid_file.js]
+[test_invalid_json.js]
+[test_missing_file.js]
+[test_register.js]
diff --git a/toolkit/components/ctypes/ctypes.cpp b/toolkit/components/ctypes/ctypes.cpp
new file mode 100644
index 0000000000..249e269831
--- /dev/null
+++ b/toolkit/components/ctypes/ctypes.cpp
@@ -0,0 +1,151 @@
+/* -*- 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 "ctypes.h"
+#include "jsapi.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsMemory.h"
+#include "nsString.h"
+#include "nsNativeCharsetUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozJSComponentLoader.h"
+#include "nsZipArchive.h"
+#include "xpc_make_class.h"
+
+#define JSCTYPES_CONTRACTID \
+ "@mozilla.org/jsctypes;1"
+
+
+#define JSCTYPES_CID \
+{ 0xc797702, 0x1c60, 0x4051, { 0x9d, 0xd7, 0x4d, 0x74, 0x5, 0x60, 0x56, 0x42 } }
+
+namespace mozilla {
+namespace ctypes {
+
+static char*
+UnicodeToNative(JSContext *cx, const char16_t *source, size_t slen)
+{
+ nsAutoCString native;
+ nsDependentString unicode(reinterpret_cast<const char16_t*>(source), slen);
+ nsresult rv = NS_CopyUnicodeToNative(unicode, native);
+ if (NS_FAILED(rv)) {
+ JS_ReportErrorASCII(cx, "could not convert string to native charset");
+ return nullptr;
+ }
+
+ char* result = static_cast<char*>(JS_malloc(cx, native.Length() + 1));
+ if (!result)
+ return nullptr;
+
+ memcpy(result, native.get(), native.Length() + 1);
+ return result;
+}
+
+static JSCTypesCallbacks sCallbacks = {
+ UnicodeToNative
+};
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(Module)
+
+NS_IMPL_ISUPPORTS(Module, nsIXPCScriptable)
+
+Module::Module()
+{
+}
+
+Module::~Module()
+{
+}
+
+#define XPC_MAP_CLASSNAME Module
+#define XPC_MAP_QUOTED_CLASSNAME "Module"
+#define XPC_MAP_WANT_CALL
+#define XPC_MAP_FLAGS nsIXPCScriptable::WANT_CALL
+#include "xpc_map_end.h"
+
+static bool
+SealObjectAndPrototype(JSContext* cx, JS::Handle<JSObject *> parent, const char* name)
+{
+ JS::Rooted<JS::Value> prop(cx);
+ if (!JS_GetProperty(cx, parent, name, &prop))
+ return false;
+
+ if (prop.isUndefined()) {
+ // Pretend we sealed the object.
+ return true;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, prop.toObjectOrNull());
+ if (!JS_GetProperty(cx, obj, "prototype", &prop))
+ return false;
+
+ JS::Rooted<JSObject*> prototype(cx, prop.toObjectOrNull());
+ return JS_FreezeObject(cx, obj) && JS_FreezeObject(cx, prototype);
+}
+
+static bool
+InitAndSealCTypesClass(JSContext* cx, JS::Handle<JSObject*> global)
+{
+ // Init the ctypes object.
+ if (!JS_InitCTypesClass(cx, global))
+ return false;
+
+ // Set callbacks for charset conversion and such.
+ JS::Rooted<JS::Value> ctypes(cx);
+ if (!JS_GetProperty(cx, global, "ctypes", &ctypes))
+ return false;
+
+ JS_SetCTypesCallbacks(ctypes.toObjectOrNull(), &sCallbacks);
+
+ // Seal up Object, Function, Array and Error and their prototypes. (This
+ // single object instance is shared amongst everyone who imports the ctypes
+ // module.)
+ if (!SealObjectAndPrototype(cx, global, "Object") ||
+ !SealObjectAndPrototype(cx, global, "Function") ||
+ !SealObjectAndPrototype(cx, global, "Array") ||
+ !SealObjectAndPrototype(cx, global, "Error"))
+ return false;
+
+ return true;
+}
+
+NS_IMETHODIMP
+Module::Call(nsIXPConnectWrappedNative* wrapper,
+ JSContext* cx,
+ JSObject* obj,
+ const JS::CallArgs& args,
+ bool* _retval)
+{
+ mozJSComponentLoader* loader = mozJSComponentLoader::Get();
+ JS::Rooted<JSObject*> targetObj(cx);
+ nsresult rv = loader->FindTargetObject(cx, &targetObj);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_retval = InitAndSealCTypesClass(cx, targetObj);
+ return NS_OK;
+}
+
+} // namespace ctypes
+} // namespace mozilla
+
+NS_DEFINE_NAMED_CID(JSCTYPES_CID);
+
+static const mozilla::Module::CIDEntry kCTypesCIDs[] = {
+ { &kJSCTYPES_CID, false, nullptr, mozilla::ctypes::ModuleConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kCTypesContracts[] = {
+ { JSCTYPES_CONTRACTID, &kJSCTYPES_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kCTypesModule = {
+ mozilla::Module::kVersion,
+ kCTypesCIDs,
+ kCTypesContracts
+};
+
+NSMODULE_DEFN(jsctypes) = &kCTypesModule;
diff --git a/toolkit/components/ctypes/ctypes.h b/toolkit/components/ctypes/ctypes.h
new file mode 100644
index 0000000000..b72f22c1c0
--- /dev/null
+++ b/toolkit/components/ctypes/ctypes.h
@@ -0,0 +1,30 @@
+/* -*- 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 COMPONENTS_CTYPES_H
+#define COMPONENTS_CTYPES_H
+
+#include "nsIXPCScriptable.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+namespace ctypes {
+
+class Module final : public nsIXPCScriptable
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIXPCSCRIPTABLE
+
+ Module();
+
+private:
+ ~Module();
+};
+
+} // namespace ctypes
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/ctypes/ctypes.jsm b/toolkit/components/ctypes/ctypes.jsm
new file mode 100644
index 0000000000..f22d011840
--- /dev/null
+++ b/toolkit/components/ctypes/ctypes.jsm
@@ -0,0 +1,23 @@
+/* -*- 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/. */
+
+this.EXPORTED_SYMBOLS = [ "ctypes" ];
+
+/*
+ * This is the js module for ctypes. Import it like so:
+ * Components.utils.import("resource://gre/modules/ctypes.jsm");
+ *
+ * This will create a 'ctypes' object, which provides an interface to describe
+ * and instantiate C types and call C functions from a dynamic library.
+ *
+ * For documentation on the API, see:
+ * https://developer.mozilla.org/en/js-ctypes/js-ctypes_reference
+ *
+ */
+
+// Initialize the ctypes object. You do not need to do this yourself.
+const init = Components.classes["@mozilla.org/jsctypes;1"].createInstance();
+init();
+
diff --git a/toolkit/components/ctypes/moz.build b/toolkit/components/ctypes/moz.build
new file mode 100644
index 0000000000..c79110eebc
--- /dev/null
+++ b/toolkit/components/ctypes/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/.
+
+TEST_DIRS += ['tests']
+
+SOURCES += [
+ 'ctypes.cpp',
+]
+
+LOCAL_INCLUDES += [
+ '/js/xpconnect/loader',
+]
+
+EXTRA_JS_MODULES += [
+ 'ctypes.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Core', 'js-ctypes')
diff --git a/toolkit/components/ctypes/tests/chrome/.eslintrc.js b/toolkit/components/ctypes/tests/chrome/.eslintrc.js
new file mode 100644
index 0000000000..8c0f4f574c
--- /dev/null
+++ b/toolkit/components/ctypes/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/ctypes/tests/chrome/chrome.ini b/toolkit/components/ctypes/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..e34866be2a
--- /dev/null
+++ b/toolkit/components/ctypes/tests/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ xpcshellTestHarnessAdaptor.js
+ ctypes_worker.js
+ ../unit/test_jsctypes.js
+
+[test_ctypes.xul]
diff --git a/toolkit/components/ctypes/tests/chrome/ctypes_worker.js b/toolkit/components/ctypes/tests/chrome/ctypes_worker.js
new file mode 100644
index 0000000000..ff128a758a
--- /dev/null
+++ b/toolkit/components/ctypes/tests/chrome/ctypes_worker.js
@@ -0,0 +1,14 @@
+/* -*- 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/. */
+
+importScripts("xpcshellTestHarnessAdaptor.js");
+
+onmessage = function(event) {
+ _WORKINGDIR_ = event.data.dir;
+ _OS_ = event.data.os;
+ importScripts("test_jsctypes.js");
+ run_test();
+ postMessage("Done!");
+}
diff --git a/toolkit/components/ctypes/tests/chrome/test_ctypes.xul b/toolkit/components/ctypes/tests/chrome/test_ctypes.xul
new file mode 100644
index 0000000000..bbe7fb0c97
--- /dev/null
+++ b/toolkit/components/ctypes/tests/chrome/test_ctypes.xul
@@ -0,0 +1,106 @@
+<?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/. -->
+
+<window title="DOM Worker Threads Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+
+ CTYPES_TEST_LIB = ctypes.libraryName("jsctypes-test");
+ CTYPES_UNICODE_LIB = ctypes.libraryName("jsctyp\u00E8s-t\u00EB\u00DFt");
+
+ /*
+ * input: string of the url where we are running from
+ * return: nsILocalFile
+ */
+ function getCurrentDir(path) {
+ var rootDir = getRootDirectory(window.location.href);
+ var jar = getJar(rootDir);
+
+ if (jar) {
+ return extractJarToTmp(jar);
+ } else {
+ return getLocalDir(path);
+ }
+ }
+
+ function getLocalDir(path) {
+ let dir = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsILocalFile);
+ path = location.pathname;
+ path = path.slice("content/".length,
+ -1 * "/test_ctypes.xul".length);
+ let components = path.split("/");
+ for (let part in components) {
+ dir.append(components[part]);
+ }
+ return dir;
+ }
+
+ function setupLibs(path) {
+ let libFile = path.clone();
+ libFile.append(CTYPES_TEST_LIB);
+ ok(libFile.exists(), "ctypes test library doesn't exist!?");
+
+ libFile.copyTo(null, CTYPES_UNICODE_LIB);
+ }
+
+ function cleanupLibs(path) {
+ let unicodeFile = path.clone();
+ unicodeFile.append(CTYPES_UNICODE_LIB);
+ ok(unicodeFile.exists(), "ctypes unicode test library doesn't exist!?");
+ unicodeFile.remove(false);
+ }
+
+ function test()
+ {
+ SimpleTest.waitForExplicitFinish();
+
+ var dir = getCurrentDir(location.path);
+ ok(dir.exists() && dir.isDirectory(), "Chrome test dir doesn't exist?!");
+ setupLibs(dir);
+
+ var worker = new ChromeWorker("ctypes_worker.js");
+ worker.onmessage = function(event) {
+ is(event.data, "Done!", "Wrong message!");
+ cleanupLibs(dir);
+ SimpleTest.finish();
+ }
+ worker.onerror = function(event) {
+ if (event.message == "uncaught exception: 7.5 million years for that?" ||
+ event.message == "uncaught exception: Just following orders, sir!") {
+ // We throw those on purpose in the worker, so ignore them.
+ return true;
+ }
+ ok(false, "Worker had an error: " + event.message);
+ worker.terminate();
+ cleanupLibs(dir);
+ SimpleTest.finish();
+ }
+
+ worker.postMessage({dir: dir.path, os: Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS});
+ }
+
+ ]]>
+ </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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js b/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js
new file mode 100644
index 0000000000..eec85025b5
--- /dev/null
+++ b/toolkit/components/ctypes/tests/chrome/xpcshellTestHarnessAdaptor.js
@@ -0,0 +1,100 @@
+/* -*- 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 _WORKINGDIR_ = null;
+var _OS_ = null;
+
+var Components = {
+ classes: { },
+ interfaces: { },
+ stack: {
+ caller: null
+ },
+ utils: {
+ import: function() { }
+ }
+};
+
+function do_throw(message, stack) {
+ do_print("error: " + message);
+ do_print("stack: " + (stack ? stack : new Error().stack));
+ throw message;
+}
+
+function do_check_neq(left, right, stack) {
+ if (left == right) {
+ var text = "do_check_neq failed";
+ try {
+ text += ": " + left + " == " + right;
+ } catch (e) {
+ }
+ do_throw(text, stack);
+ }
+}
+
+function do_check_eq(left, right, stack) {
+ if (left != right) {
+ var text = "do_check_eq failed";
+ try {
+ text += ": " + left + " != " + right;
+ } catch (e) {
+ }
+ do_throw(text, stack);
+ }
+}
+
+function do_check_true(condition, stack) {
+ do_check_eq(condition, true, stack);
+}
+
+function do_check_false(condition, stack) {
+ do_check_eq(condition, false, stack);
+}
+
+function do_print(text) {
+ dump("INFO: " + text + "\n");
+}
+
+function FileFaker(path) {
+ this._path = path;
+}
+FileFaker.prototype = {
+ get path() {
+ return this._path;
+ },
+ get parent() {
+ let lastSlash = this._path.lastIndexOf("/");
+ if (lastSlash == -1) {
+ return "";
+ }
+ this._path = this._path.substring(0, lastSlash);
+ return this;
+ },
+ append: function(leaf) {
+ this._path = this._path + "/" + leaf;
+ }
+};
+
+function do_get_file(path, allowNonexistent) {
+ if (!_WORKINGDIR_) {
+ do_throw("No way to fake files if working directory is unknown!");
+ }
+
+ let lf = new FileFaker(_WORKINGDIR_);
+ let bits = path.split("/");
+ for (let i = 0; i < bits.length; i++) {
+ if (bits[i]) {
+ if (bits[i] == "..")
+ lf = lf.parent;
+ else
+ lf.append(bits[i]);
+ }
+ }
+ return lf;
+}
+
+function get_os() {
+ return _OS_;
+}
diff --git a/toolkit/components/ctypes/tests/jsctypes-test-errno.cpp b/toolkit/components/ctypes/tests/jsctypes-test-errno.cpp
new file mode 100644
index 0000000000..83a29e6328
--- /dev/null
+++ b/toolkit/components/ctypes/tests/jsctypes-test-errno.cpp
@@ -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 <stdio.h>
+#include <errno.h>
+#if defined(XP_WIN)
+#include <windows.h>
+#endif // defined(XP_WIN)
+
+#include "jsctypes-test-errno.h"
+
+
+
+#define FAIL \
+{ \
+ fprintf(stderr, "Assertion failed at line %i\n", __LINE__); \
+ (*(int*)nullptr)++; \
+}
+
+
+void set_errno(int status)
+{
+ errno = status;
+}
+int get_errno()
+{
+ return errno;
+}
+
+#if defined(XP_WIN)
+void set_last_error(int status)
+{
+ SetLastError((int)status);
+}
+int get_last_error()
+{
+ return (int)GetLastError();
+}
+#endif // defined(XP_WIN)
diff --git a/toolkit/components/ctypes/tests/jsctypes-test-errno.h b/toolkit/components/ctypes/tests/jsctypes-test-errno.h
new file mode 100644
index 0000000000..4d11b905bc
--- /dev/null
+++ b/toolkit/components/ctypes/tests/jsctypes-test-errno.h
@@ -0,0 +1,21 @@
+/* -*- 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/Attributes.h"
+#include "mozilla/Types.h"
+
+#define EXPORT_CDECL(type) MOZ_EXPORT type
+
+MOZ_BEGIN_EXTERN_C
+
+ EXPORT_CDECL(void) set_errno(int status);
+ EXPORT_CDECL(int) get_errno();
+
+#if defined(XP_WIN)
+ EXPORT_CDECL(void) set_last_error(int status);
+ EXPORT_CDECL(int) get_last_error();
+#endif // defined(XP_WIN)
+
+MOZ_END_EXTERN_C
diff --git a/toolkit/components/ctypes/tests/jsctypes-test-finalizer.cpp b/toolkit/components/ctypes/tests/jsctypes-test-finalizer.cpp
new file mode 100644
index 0000000000..79e21cc898
--- /dev/null
+++ b/toolkit/components/ctypes/tests/jsctypes-test-finalizer.cpp
@@ -0,0 +1,323 @@
+#include "errno.h"
+
+#include "jsctypes-test.h"
+#include "jsctypes-test-finalizer.h"
+
+/**
+ * Shared infrastructure
+ */
+
+
+/**
+ * An array of integers representing resources.
+ * - 0: unacquired
+ * - 1: acquired
+ * - < 0: error, resource has been released several times.
+ */
+int *gFinalizerTestResources = nullptr;
+char **gFinalizerTestNames = nullptr;
+size_t gFinalizerTestSize;
+
+void
+test_finalizer_start(size_t size)
+{
+ gFinalizerTestResources = new int[size];
+ gFinalizerTestNames = new char*[size];
+ gFinalizerTestSize = size;
+ for (size_t i = 0; i < size; ++i) {
+ gFinalizerTestResources[i] = 0;
+ gFinalizerTestNames[i] = nullptr;
+ }
+}
+
+void
+test_finalizer_stop()
+{
+ delete[] gFinalizerTestResources;
+}
+
+/**
+ * Check if an acquired resource has been released
+ */
+bool
+test_finalizer_resource_is_acquired(size_t i)
+{
+ return gFinalizerTestResources[i] == 1;
+}
+// Resource type: size_t
+
+// Acquire resource i
+size_t
+test_finalizer_acq_size_t(size_t i)
+{
+ gFinalizerTestResources[i] = 1;
+ return i;
+}
+
+// Release resource i
+void
+test_finalizer_rel_size_t(size_t i)
+{
+ if (--gFinalizerTestResources[i] < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+}
+
+size_t
+test_finalizer_rel_size_t_return_size_t(size_t i)
+{
+ if (-- gFinalizerTestResources[i] < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+ return i;
+}
+
+myRECT
+test_finalizer_rel_size_t_return_struct_t(size_t i)
+{
+ if (-- gFinalizerTestResources[i] < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+ const int32_t narrowed = (int32_t)i;
+ myRECT result = { narrowed, narrowed, narrowed, narrowed };
+ return result;
+}
+
+bool
+test_finalizer_cmp_size_t(size_t a, size_t b)
+{
+ return a==b;
+}
+
+// Resource type: int32_t
+
+// Acquire resource i
+int32_t
+test_finalizer_acq_int32_t(size_t i)
+{
+ gFinalizerTestResources[i] = 1;
+ return i;
+}
+
+// Release resource i
+void
+test_finalizer_rel_int32_t(int32_t i)
+{
+ if (--gFinalizerTestResources[i] < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+}
+
+bool
+test_finalizer_cmp_int32_t(int32_t a, int32_t b)
+{
+ return a==b;
+}
+
+// Resource type: int64_t
+
+// Acquire resource i
+int64_t
+test_finalizer_acq_int64_t(size_t i)
+{
+ gFinalizerTestResources[i] = 1;
+ return i;
+}
+
+// Release resource i
+void
+test_finalizer_rel_int64_t(int64_t i)
+{
+ if (-- gFinalizerTestResources[i] < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+}
+
+bool
+test_finalizer_cmp_int64_t(int64_t a, int64_t b)
+{
+ return a==b;
+}
+
+// Resource type: void*
+
+// Acquire resource i
+void*
+test_finalizer_acq_ptr_t(size_t i)
+{
+ gFinalizerTestResources[i] = 1;
+ return (void*)&gFinalizerTestResources[i];
+}
+
+// Release resource i
+void
+test_finalizer_rel_ptr_t(void *i)
+{
+ int *as_int = (int*)i;
+ -- (*as_int);
+ if (*as_int < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+}
+
+bool
+test_finalizer_cmp_ptr_t(void *a, void *b)
+{
+ return a==b;
+}
+
+// Resource type: int32_t*
+
+// Acquire resource i
+int32_t*
+test_finalizer_acq_int32_ptr_t(size_t i)
+{
+ gFinalizerTestResources[i] = 1;
+ return (int32_t*)&gFinalizerTestResources[i];
+}
+
+// Release resource i
+void
+test_finalizer_rel_int32_ptr_t(int32_t *i)
+{
+ -- (*i);
+ if (*i < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+}
+
+bool
+test_finalizer_cmp_int32_ptr_t(int32_t *a, int32_t *b)
+{
+ return a==b;
+}
+
+// Resource type: nullptr
+
+// Acquire resource i
+void*
+test_finalizer_acq_null_t(size_t i)
+{
+ gFinalizerTestResources[0] = 1;//Always index 0
+ return nullptr;
+}
+
+// Release resource i
+void
+test_finalizer_rel_null_t(void *i)
+{
+ if (i != nullptr) {
+ MOZ_CRASH("Assertion failed");
+ }
+ gFinalizerTestResources[0] --;
+}
+
+bool
+test_finalizer_null_resource_is_acquired(size_t)
+{
+ return gFinalizerTestResources[0] == 1;
+}
+
+bool
+test_finalizer_cmp_null_t(void *a, void *b)
+{
+ return a==b;
+}
+
+// Resource type: char*
+
+// Acquire resource i
+char*
+test_finalizer_acq_string_t(int i)
+{
+ gFinalizerTestResources[i] = 1;
+ if (!gFinalizerTestNames[i]) {
+ char* buf = new char[10];
+ snprintf(buf, 10, "%d", i);
+ gFinalizerTestNames[i] = buf;
+ return buf;
+ }
+ return gFinalizerTestNames[i];
+}
+
+// Release resource i
+void
+test_finalizer_rel_string_t(char *i)
+{
+ int index = atoi(i);
+ if (index < 0 || index >= (int)gFinalizerTestSize) {
+ MOZ_CRASH("Assertion failed");
+ }
+ gFinalizerTestResources[index] --;
+}
+
+bool
+test_finalizer_string_resource_is_acquired(size_t i)
+{
+ return gFinalizerTestResources[i] == 1;
+}
+
+bool
+test_finalizer_cmp_string_t(char *a, char *b)
+{
+ return !strncmp(a, b, 10);
+}
+
+// Resource type: myRECT
+
+// Acquire resource i
+myRECT
+test_finalizer_acq_struct_t(int i)
+{
+ gFinalizerTestResources[i] = 1;
+ myRECT result = { i, i, i, i };
+ return result;
+}
+
+// Release resource i
+void
+test_finalizer_rel_struct_t(myRECT i)
+{
+ int index = i.top;
+ if (index < 0 || index >= (int)gFinalizerTestSize) {
+ MOZ_CRASH("Assertion failed");
+ }
+ gFinalizerTestResources[index] --;
+}
+
+bool
+test_finalizer_struct_resource_is_acquired(myRECT i)
+{
+ int index = i.top;
+ if (index < 0 || index >= (int)gFinalizerTestSize) {
+ MOZ_CRASH("Assertion failed");
+ }
+ return gFinalizerTestResources[index] == 1;
+}
+
+bool
+test_finalizer_cmp_struct_t(myRECT a, myRECT b)
+{
+ return a.top == b.top;
+}
+
+// Support for checking that we reject nullptr finalizer
+afun* test_finalizer_rel_null_function()
+{
+ return nullptr;
+}
+
+void
+test_finalizer_rel_size_t_set_errno(size_t i)
+{
+ if (-- gFinalizerTestResources[i] < 0) {
+ MOZ_CRASH("Assertion failed");
+ }
+ errno = 10;
+}
+
+void
+reset_errno()
+{
+ errno = 0;
+}
diff --git a/toolkit/components/ctypes/tests/jsctypes-test-finalizer.h b/toolkit/components/ctypes/tests/jsctypes-test-finalizer.h
new file mode 100644
index 0000000000..f942e7f44b
--- /dev/null
+++ b/toolkit/components/ctypes/tests/jsctypes-test-finalizer.h
@@ -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/. */
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Types.h"
+
+#define EXPORT_CDECL(type) MOZ_EXPORT type
+
+MOZ_BEGIN_EXTERN_C
+
+ EXPORT_CDECL(void) test_finalizer_start(size_t size);
+ EXPORT_CDECL(void) test_finalizer_stop();
+ EXPORT_CDECL(bool) test_finalizer_resource_is_acquired(size_t i);
+
+ EXPORT_CDECL(size_t) test_finalizer_acq_size_t(size_t i);
+ EXPORT_CDECL(void) test_finalizer_rel_size_t(size_t i);
+ EXPORT_CDECL(size_t) test_finalizer_rel_size_t_return_size_t(size_t i);
+ EXPORT_CDECL(myRECT) test_finalizer_rel_size_t_return_struct_t(size_t i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_size_t(size_t a, size_t b);
+
+ EXPORT_CDECL(int32_t) test_finalizer_acq_int32_t(size_t i);
+ EXPORT_CDECL(void) test_finalizer_rel_int32_t(int32_t i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_int32_t(int32_t a, int32_t b);
+
+ EXPORT_CDECL(int64_t) test_finalizer_acq_int64_t(size_t i);
+ EXPORT_CDECL(void) test_finalizer_rel_int64_t(int64_t i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_int64_t(int64_t a, int64_t b);
+
+ EXPORT_CDECL(void*) test_finalizer_acq_ptr_t(size_t i);
+ EXPORT_CDECL(void) test_finalizer_rel_ptr_t(void *i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_ptr_t(void *a, void *b);
+
+ EXPORT_CDECL(int32_t*) test_finalizer_acq_int32_ptr_t(size_t i);
+ EXPORT_CDECL(void) test_finalizer_rel_int32_ptr_t(int32_t *i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_int32_ptr_t(int32_t *a, int32_t *b);
+
+ EXPORT_CDECL(char*) test_finalizer_acq_string_t(int i);
+ EXPORT_CDECL(void) test_finalizer_rel_string_t(char *i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_string_t(char *a, char *b);
+
+ EXPORT_CDECL(void*) test_finalizer_acq_null_t(size_t i);
+ EXPORT_CDECL(void) test_finalizer_rel_null_t(void *i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_null_t(void *a, void *b);
+ EXPORT_CDECL(bool) test_finalizer_null_resource_is_acquired(size_t i);
+
+ EXPORT_CDECL(myRECT) test_finalizer_acq_struct_t(int i);
+ EXPORT_CDECL(void) test_finalizer_rel_struct_t(myRECT i);
+ EXPORT_CDECL(bool) test_finalizer_cmp_struct_t(myRECT a, myRECT b);
+
+ typedef void (*afun)(size_t);
+ EXPORT_CDECL(afun*) test_finalizer_rel_null_function();
+
+ EXPORT_CDECL(void) test_finalizer_rel_size_t_set_errno(size_t i);
+ EXPORT_CDECL(void) reset_errno();
+
+MOZ_END_EXTERN_C
diff --git a/toolkit/components/ctypes/tests/jsctypes-test.cpp b/toolkit/components/ctypes/tests/jsctypes-test.cpp
new file mode 100644
index 0000000000..d0e84a66c2
--- /dev/null
+++ b/toolkit/components/ctypes/tests/jsctypes-test.cpp
@@ -0,0 +1,394 @@
+/* -*- 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 "jsctypes-test.h"
+#include <math.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include "typedefs.h"
+
+template <typename T> struct ValueTraits {
+ static T literal() { return static_cast<T>(109.25); }
+ static T sum(T a, T b) { return a + b; }
+ static T sum_many(
+ T a, T b, T c, T d, T e, T f, T g, T h, T i,
+ T j, T k, T l, T m, T n, T o, T p, T q, T r)
+ {
+ return a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + q + r;
+ }
+};
+
+template <> struct ValueTraits<bool> {
+ typedef bool T;
+ static T literal() { return true; }
+ static T sum(T a, T b) { return a || b; }
+ static T sum_many(
+ T a, T b, T c, T d, T e, T f, T g, T h, T i,
+ T j, T k, T l, T m, T n, T o, T p, T q, T r)
+ {
+ return a || b || c || d || e || f || g || h || i ||
+ j || k || l || m || n || o || p || q || r;
+ }
+};
+
+void
+test_void_t_cdecl()
+{
+ // do nothing
+ return;
+}
+
+// The "AndUnderscore" bit here is an unfortunate hack: the first argument to
+// DEFINE_CDECL_FUNCTIONS and DEFINE_STDCALL_FUNCTIONS, in addition to being a
+// type, may also be a *macro* on NetBSD -- #define int8_t __int8_t and so on.
+// See <http://mail-index.netbsd.org/tech-toolchain/2014/12/18/msg002479.html>.
+// And unfortunately, passing that macro as an argument to this macro causes it
+// to be expanded -- producing get___int8_t_cdecl() and so on. Concatenating
+// int8_t with _ slightly muddies this code but inhibits expansion. See also
+// bug 1113379.
+#define FUNCTION_TESTS(nameAndUnderscore, type, ffiType, suffix) \
+type ABI \
+get_##nameAndUnderscore##suffix() \
+{ \
+ return ValueTraits<type>::literal(); \
+} \
+ \
+type ABI \
+set_##nameAndUnderscore##suffix(type x) \
+{ \
+ return x; \
+} \
+ \
+type ABI \
+sum_##nameAndUnderscore##suffix(type x, type y) \
+{ \
+ return ValueTraits<type>::sum(x, y); \
+} \
+ \
+type ABI \
+sum_alignb_##nameAndUnderscore##suffix(char a, type x, char b, type y, char c)\
+{ \
+ return ValueTraits<type>::sum(x, y); \
+} \
+ \
+type ABI \
+sum_alignf_##nameAndUnderscore##suffix(float a, type x, float b, type y, float c)\
+{ \
+ return ValueTraits<type>::sum(x, y); \
+} \
+ \
+type ABI \
+sum_many_##nameAndUnderscore##suffix( \
+ type a, type b, type c, type d, type e, type f, type g, type h, type i, \
+ type j, type k, type l, type m, type n, type o, type p, type q, type r) \
+{ \
+ return ValueTraits<type>::sum_many(a, b, c, d, e, f, g, h, i, \
+ j, k, l, m, n, o, p, q, r); \
+}
+
+#define ABI /* cdecl */
+#define DEFINE_CDECL_FUNCTIONS(x, y, z) FUNCTION_TESTS(x##_, y, z, cdecl)
+CTYPES_FOR_EACH_TYPE(DEFINE_CDECL_FUNCTIONS)
+#undef DEFINE_CDECL_FUNCTIONS
+#undef ABI
+
+#if defined(_WIN32)
+
+void NS_STDCALL
+test_void_t_stdcall()
+{
+ // do nothing
+ return;
+}
+
+#define ABI NS_STDCALL
+#define DEFINE_STDCALL_FUNCTIONS(x, y, z) FUNCTION_TESTS(x##_, y, z, stdcall)
+CTYPES_FOR_EACH_TYPE(DEFINE_STDCALL_FUNCTIONS)
+#undef DEFINE_STDCALL_FUNCTIONS
+#undef ABI
+
+#endif /* defined(_WIN32) */
+
+#define DEFINE_CDECL_TYPE_STATS(name, type, ffiType) \
+struct align_##name { \
+ char x; \
+ type y; \
+}; \
+struct nested_##name { \
+ char a; \
+ align_##name b; \
+ char c; \
+}; \
+ \
+void \
+get_##name##_stats(size_t* align, size_t* size, size_t* nalign, size_t* nsize, \
+ size_t offsets[]) \
+{ \
+ *align = offsetof(align_##name, y); \
+ *size = sizeof(align_##name); \
+ *nalign = offsetof(nested_##name, b); \
+ *nsize = sizeof(nested_##name); \
+ offsets[0] = offsetof(align_##name, y); \
+ offsets[1] = offsetof(nested_##name, b); \
+ offsets[2] = offsetof(nested_##name, c); \
+}
+CTYPES_FOR_EACH_TYPE(DEFINE_CDECL_TYPE_STATS)
+#undef DEFINE_CDECL_TYPE_STATS
+
+template <typename T>
+int32_t StrLen(const T* string)
+{
+ const T *end;
+ for (end = string; *end; ++end);
+ return end - string;
+}
+
+int32_t
+test_ansi_len(const char* string)
+{
+ return StrLen(string);
+}
+
+int32_t
+test_wide_len(const char16_t* string)
+{
+ return StrLen(string);
+}
+
+const char *
+test_ansi_ret()
+{
+ return "success";
+}
+
+const char16_t *
+test_wide_ret()
+{
+ static const char16_t kSuccess[] = {'s', 'u', 'c', 'c', 'e', 's', 's', '\0'};
+ return kSuccess;
+}
+
+char *
+test_ansi_echo(const char* string)
+{
+ return (char*)string;
+}
+
+int32_t
+test_pt_in_rect(myRECT rc, myPOINT pt)
+{
+ if (pt.x < rc.left || pt.x > rc.right)
+ return 0;
+ if (pt.y < rc.bottom || pt.y > rc.top)
+ return 0;
+ return 1;
+}
+
+void
+test_init_pt(myPOINT* pt, int32_t x, int32_t y)
+{
+ pt->x = x;
+ pt->y = y;
+}
+
+int32_t
+test_nested_struct(NESTED n)
+{
+ return int32_t(n.n1 + n.n2 + n.inner.i1 + n.inner.i2 + n.inner.i3 + n.n3 + n.n4);
+}
+
+myPOINT
+test_struct_return(myRECT r)
+{
+ myPOINT p;
+ p.x = r.left; p.y = r.top;
+ return p;
+}
+
+myRECT
+test_large_struct_return(myRECT a, myRECT b)
+{
+ myRECT r;
+ r.left = a.left; r.right = a.right;
+ r.top = b.top; r.bottom = b.bottom;
+ return r;
+}
+
+ONE_BYTE
+test_1_byte_struct_return(myRECT r)
+{
+ ONE_BYTE s;
+ s.a = r.top;
+ return s;
+}
+
+TWO_BYTE
+test_2_byte_struct_return(myRECT r)
+{
+ TWO_BYTE s;
+ s.a = r.top;
+ s.b = r.left;
+ return s;
+}
+
+THREE_BYTE
+test_3_byte_struct_return(myRECT r)
+{
+ THREE_BYTE s;
+ s.a = r.top;
+ s.b = r.left;
+ s.c = r.bottom;
+ return s;
+}
+
+FOUR_BYTE
+test_4_byte_struct_return(myRECT r)
+{
+ FOUR_BYTE s;
+ s.a = r.top;
+ s.b = r.left;
+ s.c = r.bottom;
+ s.d = r.right;
+ return s;
+}
+
+FIVE_BYTE
+test_5_byte_struct_return(myRECT r)
+{
+ FIVE_BYTE s;
+ s.a = r.top;
+ s.b = r.left;
+ s.c = r.bottom;
+ s.d = r.right;
+ s.e = r.top;
+ return s;
+}
+
+SIX_BYTE
+test_6_byte_struct_return(myRECT r)
+{
+ SIX_BYTE s;
+ s.a = r.top;
+ s.b = r.left;
+ s.c = r.bottom;
+ s.d = r.right;
+ s.e = r.top;
+ s.f = r.left;
+ return s;
+}
+
+SEVEN_BYTE
+test_7_byte_struct_return(myRECT r)
+{
+ SEVEN_BYTE s;
+ s.a = r.top;
+ s.b = r.left;
+ s.c = r.bottom;
+ s.d = r.right;
+ s.e = r.top;
+ s.f = r.left;
+ s.g = r.bottom;
+ return s;
+}
+
+void *
+test_fnptr()
+{
+ return (void*)(uintptr_t)test_ansi_len;
+}
+
+int32_t
+test_closure_cdecl(int8_t i, test_func_ptr f)
+{
+ return f(i);
+}
+
+#if defined(_WIN32)
+int32_t
+test_closure_stdcall(int8_t i, test_func_ptr_stdcall f)
+{
+ return f(i);
+}
+#endif /* defined(_WIN32) */
+
+template <typename T> struct PromotedTraits {
+ typedef T type;
+};
+#define DECL_PROMOTED(FROM, TO) \
+ template <> struct PromotedTraits<FROM> { \
+ typedef TO type; \
+ }
+DECL_PROMOTED(bool, int);
+DECL_PROMOTED(char, int);
+DECL_PROMOTED(short, int);
+
+int32_t
+test_sum_va_cdecl(uint8_t n, ...)
+{
+ va_list list;
+ int32_t sum = 0;
+ va_start(list, n);
+ for (uint8_t i = 0; i < n; ++i)
+ sum += va_arg(list, PromotedTraits<int32_t>::type);
+ va_end(list);
+ return sum;
+}
+
+uint8_t
+test_count_true_va_cdecl(uint8_t n, ...)
+{
+ va_list list;
+ uint8_t count = 0;
+ va_start(list, n);
+ for (uint8_t i = 0; i < n; ++i)
+ if (va_arg(list, PromotedTraits<bool>::type))
+ count += 1;
+ va_end(list);
+ return count;
+}
+
+void
+test_add_char_short_int_va_cdecl(uint32_t* result, ...)
+{
+ va_list list;
+ va_start(list, result);
+ *result += va_arg(list, PromotedTraits<char>::type);
+ *result += va_arg(list, PromotedTraits<short>::type);
+ *result += va_arg(list, PromotedTraits<int>::type);
+ va_end(list);
+}
+
+int32_t*
+test_vector_add_va_cdecl(uint8_t num_vecs,
+ uint8_t vec_len,
+ int32_t* result, ...)
+{
+ va_list list;
+ va_start(list, result);
+ uint8_t i;
+ for (i = 0; i < vec_len; ++i)
+ result[i] = 0;
+ for (i = 0; i < num_vecs; ++i) {
+ int32_t* vec = va_arg(list, int32_t*);
+ for (uint8_t j = 0; j < vec_len; ++j)
+ result[j] += vec[j];
+ }
+ va_end(list);
+ return result;
+}
+
+myRECT data_rect = { -1, -2, 3, 4 };
+
+TestClass::TestClass(int32_t a)
+{
+ mInt =a;
+}
+
+int32_t
+TestClass::Add(int32_t aOther)
+{
+ mInt += aOther;
+ return mInt;
+}
diff --git a/toolkit/components/ctypes/tests/jsctypes-test.h b/toolkit/components/ctypes/tests/jsctypes-test.h
new file mode 100644
index 0000000000..14eb8c9152
--- /dev/null
+++ b/toolkit/components/ctypes/tests/jsctypes-test.h
@@ -0,0 +1,197 @@
+/* -*- 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 jsctypes_test_h
+#define jsctypes_test_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Types.h"
+#include "jspubtd.h"
+#include "typedefs.h"
+
+#define EXPORT_CDECL(type) MOZ_EXPORT type
+#if defined(_WIN32)
+#if defined(_WIN64)
+#define NS_STDCALL
+#else
+#define NS_STDCALL __stdcall
+#endif
+#define EXPORT_STDCALL(type) MOZ_EXPORT type NS_STDCALL
+#endif
+
+MOZ_BEGIN_EXTERN_C
+
+ EXPORT_CDECL(void) test_void_t_cdecl();
+
+ EXPORT_CDECL(void*) get_voidptr_t_cdecl();
+ EXPORT_CDECL(void*) set_voidptr_t_cdecl(void*);
+
+#define DECLARE_CDECL_FUNCTIONS(name, type, ffiType) \
+ EXPORT_CDECL(type) get_##name##_cdecl(); \
+ EXPORT_CDECL(type) set_##name##_cdecl(type); \
+ EXPORT_CDECL(type) sum_##name##_cdecl(type, type); \
+ EXPORT_CDECL(type) sum_alignb_##name##_cdecl(char, type, char, type, char); \
+ EXPORT_CDECL(type) sum_alignf_##name##_cdecl( \
+ float, type, float, type, float); \
+ EXPORT_CDECL(type) sum_many_##name##_cdecl( \
+ type, type, type, type, type, type, type, type, type, \
+ type, type, type, type, type, type, type, type, type); \
+ \
+ EXPORT_CDECL(void) get_##name##_stats(size_t* align, size_t* size, \
+ size_t* nalign, size_t* nsize, \
+ size_t offsets[]);
+ CTYPES_FOR_EACH_TYPE(DECLARE_CDECL_FUNCTIONS)
+#undef DECLARE_CDECL_FUNCTIONS
+
+#if defined(_WIN32)
+ EXPORT_STDCALL(void) test_void_t_stdcall();
+
+ EXPORT_STDCALL(void*) get_voidptr_t_stdcall();
+ EXPORT_STDCALL(void*) set_voidptr_t_stdcall(void*);
+
+#define DECLARE_STDCALL_FUNCTIONS(name, type, ffiType) \
+ EXPORT_STDCALL(type) get_##name##_stdcall(); \
+ EXPORT_STDCALL(type) set_##name##_stdcall(type); \
+ EXPORT_STDCALL(type) sum_##name##_stdcall(type, type); \
+ EXPORT_STDCALL(type) sum_alignb_##name##_stdcall( \
+ char, type, char, type, char); \
+ EXPORT_STDCALL(type) sum_alignf_##name##_stdcall( \
+ float, type, float, type, float); \
+ EXPORT_STDCALL(type) sum_many_##name##_stdcall( \
+ type, type, type, type, type, type, type, type, type, \
+ type, type, type, type, type, type, type, type, type);
+ CTYPES_FOR_EACH_TYPE(DECLARE_STDCALL_FUNCTIONS)
+#undef DECLARE_STDCALL_FUNCTIONS
+
+#endif /* defined(_WIN32) */
+
+ MOZ_EXPORT int32_t test_ansi_len(const char*);
+ MOZ_EXPORT int32_t test_wide_len(const char16_t*);
+ MOZ_EXPORT const char* test_ansi_ret();
+ MOZ_EXPORT const char16_t* test_wide_ret();
+ MOZ_EXPORT char* test_ansi_echo(const char*);
+
+ struct ONE_BYTE {
+ char a;
+ };
+
+ struct TWO_BYTE {
+ char a;
+ char b;
+ };
+
+ struct THREE_BYTE {
+ char a;
+ char b;
+ char c;
+ };
+
+ struct FOUR_BYTE {
+ char a;
+ char b;
+ char c;
+ char d;
+ };
+
+ struct FIVE_BYTE {
+ char a;
+ char b;
+ char c;
+ char d;
+ char e;
+ };
+
+ struct SIX_BYTE {
+ char a;
+ char b;
+ char c;
+ char d;
+ char e;
+ char f;
+ };
+
+ struct SEVEN_BYTE {
+ char a;
+ char b;
+ char c;
+ char d;
+ char e;
+ char f;
+ char g;
+ };
+
+ struct myPOINT {
+ int32_t x;
+ int32_t y;
+ };
+
+ struct myRECT {
+ int32_t top;
+ int32_t left;
+ int32_t bottom;
+ int32_t right;
+ };
+
+ struct INNER {
+ uint8_t i1;
+ int64_t i2;
+ uint8_t i3;
+ };
+
+ struct NESTED {
+ int32_t n1;
+ int16_t n2;
+ INNER inner;
+ int64_t n3;
+ int32_t n4;
+ };
+
+ MOZ_EXPORT int32_t test_pt_in_rect(myRECT, myPOINT);
+ MOZ_EXPORT void test_init_pt(myPOINT* pt, int32_t x, int32_t y);
+
+ MOZ_EXPORT int32_t test_nested_struct(NESTED);
+ MOZ_EXPORT myPOINT test_struct_return(myRECT);
+ MOZ_EXPORT myRECT test_large_struct_return(myRECT, myRECT);
+ MOZ_EXPORT ONE_BYTE test_1_byte_struct_return(myRECT);
+ MOZ_EXPORT TWO_BYTE test_2_byte_struct_return(myRECT);
+ MOZ_EXPORT THREE_BYTE test_3_byte_struct_return(myRECT);
+ MOZ_EXPORT FOUR_BYTE test_4_byte_struct_return(myRECT);
+ MOZ_EXPORT FIVE_BYTE test_5_byte_struct_return(myRECT);
+ MOZ_EXPORT SIX_BYTE test_6_byte_struct_return(myRECT);
+ MOZ_EXPORT SEVEN_BYTE test_7_byte_struct_return(myRECT);
+
+ MOZ_EXPORT void * test_fnptr();
+
+ typedef int32_t (* test_func_ptr)(int8_t);
+ MOZ_EXPORT int32_t test_closure_cdecl(int8_t, test_func_ptr);
+#if defined(_WIN32)
+ typedef int32_t (NS_STDCALL * test_func_ptr_stdcall)(int8_t);
+ MOZ_EXPORT int32_t test_closure_stdcall(int8_t, test_func_ptr_stdcall);
+#endif /* defined(_WIN32) */
+
+ MOZ_EXPORT int32_t test_callme(int8_t);
+ MOZ_EXPORT void* test_getfn();
+
+ EXPORT_CDECL(int32_t) test_sum_va_cdecl(uint8_t n, ...);
+ EXPORT_CDECL(uint8_t) test_count_true_va_cdecl(uint8_t n, ...);
+ EXPORT_CDECL(void) test_add_char_short_int_va_cdecl(uint32_t* result, ...);
+ EXPORT_CDECL(int32_t*) test_vector_add_va_cdecl(uint8_t num_vecs,
+ uint8_t vec_len,
+ int32_t* result, ...);
+
+ MOZ_EXPORT extern myRECT data_rect;
+
+MOZ_END_EXTERN_C
+
+class MOZ_EXPORT TestClass final {
+public:
+ explicit TestClass(int32_t);
+ int32_t Add(int32_t);
+
+private:
+ int32_t mInt;
+};
+
+#endif
diff --git a/toolkit/components/ctypes/tests/moz.build b/toolkit/components/ctypes/tests/moz.build
new file mode 100644
index 0000000000..22cbe4edcd
--- /dev/null
+++ b/toolkit/components/ctypes/tests/moz.build
@@ -0,0 +1,30 @@
+# -*- 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/.
+
+DIST_INSTALL = False
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
+MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini']
+
+UNIFIED_SOURCES += [
+ 'jsctypes-test-errno.cpp',
+ 'jsctypes-test-finalizer.cpp',
+ 'jsctypes-test.cpp',
+]
+
+SharedLibrary('jsctypes-test')
+
+LOCAL_INCLUDES += [
+ '/js/src/ctypes',
+]
+
+# Don't use STL wrappers here (i.e. wrapped <new>); they require mozalloc.
+DISABLE_STL_WRAPPING = True
+
+if CONFIG['COMPILE_ENVIRONMENT']:
+ shared_library = '!%sjsctypes-test%s' % (CONFIG['DLL_PREFIX'], CONFIG['DLL_SUFFIX'])
+ TEST_HARNESS_FILES.xpcshell.toolkit.components.ctypes.tests.unit += [shared_library]
+ TEST_HARNESS_FILES.testing.mochitest.chrome.toolkit.components.ctypes.tests.chrome += [shared_library]
diff --git a/toolkit/components/ctypes/tests/unit/.eslintrc.js b/toolkit/components/ctypes/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/ctypes/tests/unit/head.js b/toolkit/components/ctypes/tests/unit/head.js
new file mode 100644
index 0000000000..e95b949b6e
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/head.js
@@ -0,0 +1,128 @@
+try {
+ // We might be running without privileges, in which case it's up to the
+ // harness to give us the 'ctypes' object.
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+} catch (e) {
+}
+
+function open_ctypes_test_lib()
+{
+ return ctypes.open(do_get_file(ctypes.libraryName("jsctypes-test")).path);
+}
+
+/**
+ * A weak set of CDataFinalizer values that need to be cleaned up before
+ * proceeding to the next test.
+ */
+function ResourceCleaner() {
+ this._map = new WeakMap();
+}
+ResourceCleaner.prototype = {
+ add: function ResourceCleaner_add(v) {
+ this._map.set(v);
+ return v;
+ },
+ cleanup: function ResourceCleaner_cleanup() {
+ let keys = ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(this._map);
+ keys.forEach((function cleaner(k) {
+ try {
+ k.dispose();
+ } catch (x) {
+ // This can fail if |forget|/|dispose| has been called manually
+ // during the test. This is normal.
+ }
+ this._map.delete(k);
+ }).bind(this));
+ }
+};
+
+/**
+ * Simple wrapper for tests that require cleanup.
+ */
+function ResourceTester(start, stop) {
+ this._start = start;
+ this._stop = stop;
+}
+ResourceTester.prototype = {
+ launch: function(size, test, args) {
+ trigger_gc();
+ let cleaner = new ResourceCleaner();
+ this._start(size);
+ try {
+ test(size, args, cleaner);
+ } catch (x) {
+ cleaner.cleanup();
+ this._stop();
+ throw x;
+ }
+ trigger_gc();
+ cleaner.cleanup();
+ this._stop();
+ }
+};
+
+function structural_check_eq(a, b) {
+ // 1. If objects can be "toSource()-ed", use this.
+
+ let result;
+ let finished = false;
+ let asource, bsource;
+ try {
+ asource = a.toSource();
+ bsource = b.toSource();
+ finished = true;
+ } catch (x) {
+ }
+ if (finished) {
+ do_check_eq(asource, bsource);
+ return;
+ }
+
+ // 2. Otherwise, perform slower comparison
+
+ try {
+ structural_check_eq_aux(a, b);
+ result = true;
+ } catch (x) {
+ dump(x);
+ result = false;
+ }
+ do_check_true(result);
+}
+function structural_check_eq_aux(a, b) {
+ let ak;
+ try {
+ ak = Object.keys(a);
+ } catch (x) {
+ if (a != b) {
+ throw new Error("Distinct values "+a, b);
+ }
+ return;
+ }
+ ak.forEach(
+ function(k) {
+ let av = a[k];
+ let bv = b[k];
+ structural_check_eq_aux(av, bv);
+ }
+ );
+}
+
+function trigger_gc() {
+ dump("Triggering garbage-collection");
+ Components.utils.forceGC();
+}
+
+function must_throw(f) {
+ let has_thrown = false;
+ try {
+ f();
+ } catch (x) {
+ has_thrown = true;
+ }
+ do_check_true(has_thrown);
+}
+
+function get_os() {
+ return Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS;
+}
diff --git a/toolkit/components/ctypes/tests/unit/test_errno.js b/toolkit/components/ctypes/tests/unit/test_errno.js
new file mode 100644
index 0000000000..6bf6b4b057
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/test_errno.js
@@ -0,0 +1,69 @@
+Components.utils.import("resource://gre/modules/ctypes.jsm");
+
+// Scope used to relaunch the tests with |ctypes| opened in a limited scope.
+var scope = {};
+var ctypes = ctypes;
+
+function run_test()
+{
+ // Launch the test with regular loading of ctypes.jsm
+ main_test();
+
+ // Relaunch the test with exotic loading of ctypes.jsm
+ Components.utils.unload("resource://gre/modules/ctypes.jsm");
+ Components.utils.import("resource://gre/modules/ctypes.jsm", scope);
+ ctypes = scope.ctypes;
+ main_test();
+}
+
+function main_test()
+{
+ "use strict";
+ let library = open_ctypes_test_lib();
+ let set_errno = library.declare("set_errno", ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int);
+ let get_errno = library.declare("get_errno", ctypes.default_abi,
+ ctypes.int);
+
+ for (let i = 50; i >= 0; --i) {
+ set_errno(i);
+ let status = ctypes.errno;
+ do_check_eq(status, i);
+
+ status = get_errno();
+ do_check_eq(status, 0);
+
+ status = ctypes.errno;
+ do_check_eq(status, 0);
+ }
+
+ let set_last_error, get_last_error;
+ try { // The following test is Windows-specific
+ set_last_error = library.declare("set_last_error", ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int);
+ get_last_error = library.declare("get_last_error", ctypes.default_abi,
+ ctypes.int);
+
+ } catch (x) {
+ do_check_eq(ctypes.winLastError, undefined);
+ }
+
+ if (set_last_error) {
+ do_check_neq(ctypes.winLastError, undefined);
+ for (let i = 0; i < 50; ++i) {
+ set_last_error(i);
+ let status = ctypes.winLastError;
+ do_check_eq(status, i);
+
+ status = get_last_error();
+ do_check_eq(status, 0);
+
+ status = ctypes.winLastError;
+ do_check_eq(status, 0);
+ }
+ }
+
+ library.close();
+}
diff --git a/toolkit/components/ctypes/tests/unit/test_finalizer.js b/toolkit/components/ctypes/tests/unit/test_finalizer.js
new file mode 100644
index 0000000000..adfb4c4b45
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/test_finalizer.js
@@ -0,0 +1,452 @@
+var TEST_SIZE = 100;
+
+function run_test()
+{
+ let library = open_ctypes_test_lib();
+
+ let start = library.declare("test_finalizer_start", ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t);
+ let stop = library.declare("test_finalizer_stop", ctypes.default_abi,
+ ctypes.void_t);
+ let status = library.declare("test_finalizer_resource_is_acquired",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t);
+ let released = function released(value, witness) {
+ return witness == undefined;
+ };
+
+ let samples = [];
+ samples.push(
+ {
+ name: "size_t",
+ acquire: library.declare("test_finalizer_acq_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_size_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t),
+ compare: library.declare("test_finalizer_cmp_size_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t,
+ ctypes.size_t),
+ status: status,
+ released: released
+ });
+ samples.push(
+ {
+ name: "size_t",
+ acquire: library.declare("test_finalizer_acq_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_size_t_set_errno",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t),
+ compare: library.declare("test_finalizer_cmp_size_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t,
+ ctypes.size_t),
+ status: status,
+ released: released
+ });
+ samples.push(
+ {
+ name: "int32_t",
+ acquire: library.declare("test_finalizer_acq_int32_t",
+ ctypes.default_abi,
+ ctypes.int32_t,
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_int32_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int32_t),
+ compare: library.declare("test_finalizer_cmp_int32_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.int32_t,
+ ctypes.int32_t),
+ status: status,
+ released: released
+ }
+ );
+ samples.push(
+ {
+ name: "int64_t",
+ acquire: library.declare("test_finalizer_acq_int64_t",
+ ctypes.default_abi,
+ ctypes.int64_t,
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_int64_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int64_t),
+ compare: library.declare("test_finalizer_cmp_int64_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.int64_t,
+ ctypes.int64_t),
+ status: status,
+ released: released
+ }
+ );
+ samples.push(
+ {
+ name: "ptr",
+ acquire: library.declare("test_finalizer_acq_ptr_t",
+ ctypes.default_abi,
+ ctypes.PointerType(ctypes.void_t),
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_ptr_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.PointerType(ctypes.void_t)),
+ compare: library.declare("test_finalizer_cmp_ptr_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.void_t.ptr,
+ ctypes.void_t.ptr),
+ status: status,
+ released: released
+ }
+ );
+ samples.push(
+ {
+ name: "string",
+ acquire: library.declare("test_finalizer_acq_string_t",
+ ctypes.default_abi,
+ ctypes.char.ptr,
+ ctypes.int),
+ release: library.declare("test_finalizer_rel_string_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.char.ptr),
+ compare: library.declare("test_finalizer_cmp_string_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.char.ptr,
+ ctypes.char.ptr),
+ status: status,
+ released: released
+ }
+ );
+ const rect_t = new ctypes.StructType("myRECT",
+ [{ top : ctypes.int32_t },
+ { left : ctypes.int32_t },
+ { bottom: ctypes.int32_t },
+ { right : ctypes.int32_t }]);
+ samples.push(
+ {
+ name: "struct",
+ acquire: library.declare("test_finalizer_acq_struct_t",
+ ctypes.default_abi,
+ rect_t,
+ ctypes.int),
+ release: library.declare("test_finalizer_rel_struct_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ rect_t),
+ compare: library.declare("test_finalizer_cmp_struct_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ rect_t,
+ rect_t),
+ status: status,
+ released: released
+ }
+ );
+ samples.push(
+ {
+ name: "size_t, release returns size_t",
+ acquire: library.declare("test_finalizer_acq_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_size_t_return_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t),
+ compare: library.declare("test_finalizer_cmp_size_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t,
+ ctypes.size_t),
+ status: status,
+ released: function released_eq(i, witness) {
+ return i == witness;
+ }
+ }
+ );
+ samples.push(
+ {
+ name: "size_t, release returns myRECT",
+ acquire: library.declare("test_finalizer_acq_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_size_t_return_struct_t",
+ ctypes.default_abi,
+ rect_t,
+ ctypes.size_t),
+ compare: library.declare("test_finalizer_cmp_size_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t,
+ ctypes.size_t),
+ status: status,
+ released: function released_rect_eq(i, witness) {
+ return witness.top == i
+ && witness.bottom == i
+ && witness.left == i
+ && witness.right == i;
+ }
+ }
+ );
+ samples.push(
+ {
+ name: "using null",
+ acquire: library.declare("test_finalizer_acq_null_t",
+ ctypes.default_abi,
+ ctypes.PointerType(ctypes.void_t),
+ ctypes.size_t),
+ release: library.declare("test_finalizer_rel_null_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.PointerType(ctypes.void_t)),
+ status: library.declare("test_finalizer_null_resource_is_acquired",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t),
+ compare: library.declare("test_finalizer_cmp_null_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.void_t.ptr,
+ ctypes.void_t.ptr),
+ released: released
+ }
+ );
+
+ let tester = new ResourceTester(start, stop);
+ samples.forEach(
+ function run_sample(sample) {
+ dump("Executing finalization test for data " + sample.name + "\n");
+ tester.launch(TEST_SIZE, test_executing_finalizers, sample);
+ tester.launch(TEST_SIZE, test_do_not_execute_finalizers_on_referenced_stuff, sample);
+ tester.launch(TEST_SIZE, test_executing_dispose, sample);
+ tester.launch(TEST_SIZE, test_executing_forget, sample);
+ tester.launch(TEST_SIZE, test_result_dispose, sample);
+ dump("Successfully completed finalization test for data " + sample.name + "\n");
+ }
+ );
+
+ /*
+ * Following test deactivated: Cycle collection never takes place
+ * (see bug 727371)
+ tester.launch(TEST_SIZE, test_cycles, samples[0]);
+ */
+ dump("Successfully completed all finalization tests\n");
+ library.close();
+}
+
+// If only I could have Promises to test this :)
+// There is only so much we can do at this stage,
+// if we want to avoid tests overlapping.
+function test_cycles(size, tc) {
+ // Now, restart this with unreferenced cycles
+ for (i = 0; i < size/2; ++i) {
+ let a = {
+ a: ctypes.CDataFinalizer(tc.acquire(i*2), tc.release),
+ b: {
+ b: ctypes.CDataFinalizer(tc.acquire(i*2+1), tc.release)
+ }
+ };
+ a.b.a = a;
+ }
+ do_test_pending();
+
+ Components.utils.schedulePreciseGC(
+ function after_gc() {
+ // Check that _something_ has been finalized
+ do_check_true(count_finalized(size, tc) > 0);
+ do_test_finished();
+ }
+ );
+
+ do_timeout(10000, do_throw);
+}
+
+
+function count_finalized(size, tc) {
+ let finalizedItems = 0;
+ for (let i = 0; i < size; ++i) {
+ if (!tc.status(i)) {
+ ++finalizedItems;
+ }
+ }
+ return finalizedItems;
+}
+
+/**
+ * Test:
+ * - that (some) finalizers are executed;
+ * - that no finalizer is executed twice (this is done on the C side).
+ */
+function test_executing_finalizers(size, tc, cleanup)
+{
+ dump("test_executing_finalizers " + tc.name + "\n");
+ // Allocate |size| items without references
+ for (let i = 0; i < size; ++i) {
+ cleanup.add(ctypes.CDataFinalizer(tc.acquire(i), tc.release));
+ }
+ trigger_gc(); // This should trigger some finalizations, hopefully all
+
+ // Check that _something_ has been finalized
+ do_check_true(count_finalized(size, tc) > 0);
+}
+
+/**
+ * Check that
+ * - |dispose| returns the proper result
+ */
+function test_result_dispose(size, tc, cleanup) {
+ dump("test_result_dispose " + tc.name + "\n");
+ let ref = [];
+ // Allocate |size| items with references
+ for (let i = 0; i < size; ++i) {
+ let value = ctypes.CDataFinalizer(tc.acquire(i), tc.release);
+ cleanup.add(value);
+ ref.push(value);
+ }
+ do_check_eq(count_finalized(size, tc), 0);
+
+ for (i = 0; i < size; ++i) {
+ let witness = ref[i].dispose();
+ ref[i] = null;
+ if (!tc.released(i, witness)) {
+ do_print("test_result_dispose failure at index "+i);
+ do_check_true(false);
+ }
+ }
+
+ do_check_eq(count_finalized(size, tc), size);
+}
+
+
+/**
+ * Check that
+ * - |dispose| is executed properly
+ * - finalizers are not executed after |dispose|
+ */
+function test_executing_dispose(size, tc, cleanup)
+{
+ dump("test_executing_dispose " + tc.name + "\n");
+ let ref = [];
+ // Allocate |size| items with references
+ for (let i = 0; i < size; ++i) {
+ let value = ctypes.CDataFinalizer(tc.acquire(i), tc.release);
+ cleanup.add(value);
+ ref.push(value);
+ }
+ do_check_eq(count_finalized(size, tc), 0);
+
+ // Dispose of everything and make sure that everything has been cleaned up
+ ref.forEach(
+ function dispose(v) {
+ v.dispose();
+ }
+ );
+ do_check_eq(count_finalized(size, tc), size);
+
+ // Remove references
+ ref = [];
+
+ // Re-acquire data and make sure that everything has been reinialized
+ for (i = 0; i < size; ++i) {
+ tc.acquire(i);
+ }
+
+ do_check_eq(count_finalized(size, tc), 0);
+
+
+ // Attempt to trigger finalizations, ensure that they do not take place
+ trigger_gc();
+
+ do_check_eq(count_finalized(size, tc), 0);
+}
+
+
+/**
+ * Check that
+ * - |forget| does not dispose
+ * - |forget| has the right content
+ * - finalizers are not executed after |forget|
+ */
+function test_executing_forget(size, tc, cleanup)
+{
+ dump("test_executing_forget " + tc.name + "\n");
+ let ref = [];
+ // Allocate |size| items with references
+ for (let i = 0; i < size; ++i) {
+ let original = tc.acquire(i);
+ let finalizer = ctypes.CDataFinalizer(original, tc.release);
+ ref.push(
+ {
+ original: original,
+ finalizer: finalizer
+ }
+ );
+ cleanup.add(finalizer);
+ do_check_true(tc.compare(original, finalizer));
+ }
+ do_check_eq(count_finalized(size, tc), 0);
+
+ // Forget everything, making sure that we recover the original info
+ ref.forEach(
+ function compare_original_to_recovered(v) {
+ let original = v.original;
+ let recovered = v.finalizer.forget();
+ // Note: Cannot use do_check_eq on Uint64 et al.
+ do_check_true(tc.compare(original, recovered));
+ do_check_eq(original.constructor, recovered.constructor);
+ }
+ );
+
+ // Also make sure that we have not performed any clean up
+ do_check_eq(count_finalized(size, tc), 0);
+
+ // Remove references
+ ref = [];
+
+ // Attempt to trigger finalizations, ensure that they have no effect
+ trigger_gc();
+
+ do_check_eq(count_finalized(size, tc), 0);
+}
+
+
+/**
+ * Check that finalizers are not executed
+ */
+function test_do_not_execute_finalizers_on_referenced_stuff(size, tc, cleanup)
+{
+ dump("test_do_not_execute_finalizers_on_referenced_stuff " + tc.name + "\n");
+
+ let ref = [];
+ // Allocate |size| items without references
+ for (let i = 0; i < size; ++i) {
+ let value = ctypes.CDataFinalizer(tc.acquire(i), tc.release);
+ cleanup.add(value);
+ ref.push(value);
+ }
+ trigger_gc(); // This might trigger some finalizations, but it should not
+
+ // Check that _nothing_ has been finalized
+ do_check_eq(count_finalized(size, tc), 0);
+}
+
diff --git a/toolkit/components/ctypes/tests/unit/test_finalizer_shouldaccept.js b/toolkit/components/ctypes/tests/unit/test_finalizer_shouldaccept.js
new file mode 100644
index 0000000000..f683008e46
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/test_finalizer_shouldaccept.js
@@ -0,0 +1,174 @@
+try {
+ // We might be running without privileges, in which case it's up to the
+ // harness to give us the 'ctypes' object.
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+} catch (e) {
+}
+
+var acquire, dispose, reset_errno, dispose_errno,
+ acquire_ptr, dispose_ptr,
+ acquire_void_ptr, dispose_void_ptr,
+ acquire_string, dispose_string;
+
+function run_test()
+{
+ let library = open_ctypes_test_lib();
+
+ let start = library.declare("test_finalizer_start", ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t);
+ let stop = library.declare("test_finalizer_stop", ctypes.default_abi,
+ ctypes.void_t);
+ let tester = new ResourceTester(start, stop);
+ acquire = library.declare("test_finalizer_acq_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t);
+ dispose = library.declare("test_finalizer_rel_size_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t);
+ reset_errno = library.declare("reset_errno",
+ ctypes.default_abi,
+ ctypes.void_t);
+ dispose_errno = library.declare("test_finalizer_rel_size_t_set_errno",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t);
+ acquire_ptr = library.declare("test_finalizer_acq_int32_ptr_t",
+ ctypes.default_abi,
+ ctypes.int32_t.ptr,
+ ctypes.size_t);
+ dispose_ptr = library.declare("test_finalizer_rel_int32_ptr_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int32_t.ptr);
+ acquire_string = library.declare("test_finalizer_acq_string_t",
+ ctypes.default_abi,
+ ctypes.char.ptr,
+ ctypes.size_t);
+ dispose_string = library.declare("test_finalizer_rel_string_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.char.ptr);
+
+ tester.launch(10, test_to_string);
+ tester.launch(10, test_to_source);
+ tester.launch(10, test_to_int);
+ tester.launch(10, test_errno);
+ tester.launch(10, test_to_pointer);
+ tester.launch(10, test_readstring);
+}
+
+/**
+ * Check that toString succeeds before/after forget/dispose.
+ */
+function test_to_string()
+{
+ do_print("Starting test_to_string");
+ let a = ctypes.CDataFinalizer(acquire(0), dispose);
+ do_check_eq(a.toString(), "0");
+
+ a.forget();
+ do_check_eq(a.toString(), "[CDataFinalizer - empty]");
+
+ a = ctypes.CDataFinalizer(acquire(0), dispose);
+ a.dispose();
+ do_check_eq(a.toString(), "[CDataFinalizer - empty]");
+}
+
+/**
+ * Check that toSource succeeds before/after forget/dispose.
+ */
+function test_to_source()
+{
+ do_print("Starting test_to_source");
+ let value = acquire(0);
+ let a = ctypes.CDataFinalizer(value, dispose);
+ do_check_eq(a.toSource(),
+ "ctypes.CDataFinalizer("
+ + ctypes.size_t(value).toSource()
+ +", "
+ +dispose.toSource()
+ +")");
+ value = null;
+
+ a.forget();
+ do_check_eq(a.toSource(), "ctypes.CDataFinalizer()");
+
+ a = ctypes.CDataFinalizer(acquire(0), dispose);
+ a.dispose();
+ do_check_eq(a.toSource(), "ctypes.CDataFinalizer()");
+}
+
+/**
+ * Test conversion to int32
+ */
+function test_to_int()
+{
+ let value = 2;
+ let wrapped, converted, finalizable;
+ wrapped = ctypes.int32_t(value);
+ finalizable = ctypes.CDataFinalizer(acquire(value), dispose);
+ converted = ctypes.int32_t(finalizable);
+
+ structural_check_eq(converted, wrapped);
+ structural_check_eq(converted, ctypes.int32_t(finalizable.forget()));
+
+ finalizable = ctypes.CDataFinalizer(acquire(value), dispose);
+ wrapped = ctypes.int64_t(value);
+ converted = ctypes.int64_t(finalizable);
+ structural_check_eq(converted, wrapped);
+ finalizable.dispose();
+}
+
+/**
+ * Test that dispose can change errno but finalization cannot
+ */
+function test_errno(size, tc, cleanup)
+{
+ reset_errno();
+ do_check_eq(ctypes.errno, 0);
+
+ let finalizable = ctypes.CDataFinalizer(acquire(3), dispose_errno);
+ finalizable.dispose();
+ do_check_eq(ctypes.errno, 10);
+ reset_errno();
+
+ do_check_eq(ctypes.errno, 0);
+ for (let i = 0; i < size; ++i) {
+ finalizable = ctypes.CDataFinalizer(acquire(i), dispose_errno);
+ cleanup.add(finalizable);
+ }
+
+ trigger_gc();
+ do_check_eq(ctypes.errno, 0);
+}
+
+/**
+ * Check that a finalizable of a pointer can be used as a pointer
+ */
+function test_to_pointer()
+{
+ let ptr = ctypes.int32_t(2).address();
+ let finalizable = ctypes.CDataFinalizer(ptr, dispose_ptr);
+ let unwrapped = ctypes.int32_t.ptr(finalizable);
+
+ do_check_eq(""+ptr, ""+unwrapped);
+
+ finalizable.forget(); // Do not dispose: This is not a real pointer.
+}
+
+/**
+ * Test that readstring can be applied to a finalizer
+ */
+function test_readstring(size)
+{
+ for (let i = 0; i < size; ++i) {
+ let acquired = acquire_string(i);
+ let finalizable = ctypes.CDataFinalizer(acquired,
+ dispose_string);
+ do_check_eq(finalizable.readString(), acquired.readString());
+ finalizable.dispose();
+ }
+}
diff --git a/toolkit/components/ctypes/tests/unit/test_finalizer_shouldfail.js b/toolkit/components/ctypes/tests/unit/test_finalizer_shouldfail.js
new file mode 100644
index 0000000000..ffbe1b613a
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/test_finalizer_shouldfail.js
@@ -0,0 +1,176 @@
+try {
+ // We might be running without privileges, in which case it's up to the
+ // harness to give us the 'ctypes' object.
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+} catch (e) {
+}
+
+var acquire, dispose, null_dispose, compare, dispose_64;
+
+function run_test()
+{
+ let library = open_ctypes_test_lib();
+
+ let start = library.declare("test_finalizer_start", ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t);
+ let stop = library.declare("test_finalizer_stop", ctypes.default_abi,
+ ctypes.void_t);
+ let tester = new ResourceTester(start, stop);
+ acquire = library.declare("test_finalizer_acq_size_t",
+ ctypes.default_abi,
+ ctypes.size_t,
+ ctypes.size_t);
+ dispose = library.declare("test_finalizer_rel_size_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.size_t);
+ compare = library.declare("test_finalizer_cmp_size_t",
+ ctypes.default_abi,
+ ctypes.bool,
+ ctypes.size_t,
+ ctypes.size_t);
+
+ dispose_64 = library.declare("test_finalizer_rel_int64_t",
+ ctypes.default_abi,
+ ctypes.void_t,
+ ctypes.int64_t);
+
+ let type_afun = ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [ctypes.size_t]).ptr;
+
+ let null_dispose_maker =
+ library.declare("test_finalizer_rel_null_function",
+ ctypes.default_abi,
+ type_afun
+ );
+ null_dispose = null_dispose_maker();
+
+ tester.launch(10, test_double_dispose);
+ tester.launch(10, test_finalize_bad_construction);
+ tester.launch(10, test_null_dispose);
+ tester.launch(10, test_pass_disposed);
+ tester.launch(10, test_wrong_type);
+}
+
+
+/**
+ * Testing construction of finalizers with wrong arguments.
+ */
+function test_finalize_bad_construction() {
+ // First argument does not match second
+ must_throw(function() { ctypes.CDataFinalizer({}, dispose); });
+ must_throw(function() { ctypes.CDataFinalizer(dispose, dispose); });
+
+ // Not enough arguments
+ must_throw(function() { ctypes.CDataFinalizer(init(0)); });
+
+ // Too many arguments
+ must_throw(function() { ctypes.CDataFinalizer(init(0), dispose, dispose); });
+
+ // Second argument is null
+ must_throw(function() { ctypes.CDataFinalizer(init(0), null); });
+
+ // Second argument is undefined
+ must_throw(function() {
+ let a;
+ ctypes.CDataFinalizer(init(0), a);
+ });
+
+}
+
+/**
+ * Test that forget/dispose can only take place once.
+ */
+function test_double_dispose() {
+ function test_one_combination(i, a, b) {
+ let v = ctypes.CDataFinalizer(acquire(i), dispose);
+ a(v);
+ must_throw(function() { b(v); } );
+ }
+
+ let call_dispose = function(v) {
+ v.dispose();
+ };
+ let call_forget = function(v) {
+ v.forget();
+ };
+
+ test_one_combination(0, call_dispose, call_dispose);
+ test_one_combination(1, call_dispose, call_forget);
+ test_one_combination(2, call_forget, call_dispose);
+ test_one_combination(3, call_forget, call_forget);
+}
+
+
+/**
+ * Test that nothing (too) bad happens when the finalizer is NULL
+ */
+function test_null_dispose()
+{
+ let exception;
+
+ exception = false;
+ try {
+ ctypes.CDataFinalizer(acquire(0), null_dispose);
+ } catch (x) {
+ exception = true;
+ }
+ do_check_true(exception);
+}
+
+/**
+ * Test that conversion of a disposed/forgotten CDataFinalizer to a C
+ * value fails nicely.
+ */
+function test_pass_disposed()
+{
+ let exception, v;
+
+ exception = false;
+ v = ctypes.CDataFinalizer(acquire(0), dispose);
+ do_check_true(compare(v, 0));
+ v.forget();
+
+ try {
+ compare(v, 0);
+ } catch (x) {
+ exception = true;
+ }
+ do_check_true(exception);
+
+ exception = false;
+ v = ctypes.CDataFinalizer(acquire(0), dispose);
+ do_check_true(compare(v, 0));
+ v.dispose();
+
+ try {
+ compare(v, 0);
+ } catch (x) {
+ exception = true;
+ }
+ do_check_true(exception);
+
+ exception = false;
+ try {
+ ctypes.int32_t(ctypes.CDataFinalizer(v, dispose));
+ } catch (x) {
+ exception = true;
+ }
+ do_check_true(exception);
+}
+
+function test_wrong_type()
+{
+ let int32_v = ctypes.int32_t(99);
+ let exception;
+ try {
+ ctypes.CDataFinalizer(int32_v, dispose_64);
+ } catch (x) {
+ exception = x;
+ }
+
+ do_check_true(!!exception);
+ do_check_eq(exception.constructor.name, "TypeError");
+}
diff --git a/toolkit/components/ctypes/tests/unit/test_jsctypes.js b/toolkit/components/ctypes/tests/unit/test_jsctypes.js
new file mode 100644
index 0000000000..ec35ee18e8
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/test_jsctypes.js
@@ -0,0 +1,2808 @@
+/* -*- 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/. */
+
+try {
+ // We might be running without privileges, in which case it's up to the
+ // harness to give us the 'ctypes' object.
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+} catch (e) {
+}
+
+CTYPES_TEST_LIB = ctypes.libraryName("jsctypes-test");
+CTYPES_UNICODE_LIB = ctypes.libraryName("jsctyp\u00E8s-t\u00EB\u00DFt");
+
+function do_check_throws(f, type, stack)
+{
+ if (!stack) {
+ try {
+ // We might not have a 'Components' object.
+ stack = Components.stack.caller;
+ } catch (e) {
+ }
+ }
+
+ try {
+ f();
+ } catch (exc) {
+ if (exc.constructor.name === type.name) {
+ do_check_true(true);
+ return;
+ }
+ do_throw("expected " + type.name + " exception, caught " + exc, stack);
+ }
+ do_throw("expected " + type.name + " exception, none thrown", stack);
+}
+
+function run_test()
+{
+ // Test ctypes.CType and ctypes.CData are set up correctly.
+ run_abstract_class_tests();
+
+ // open the library
+ let libfile = do_get_file(CTYPES_TEST_LIB);
+ let library = ctypes.open(libfile.path);
+
+ // Make sure we can call a function in the library.
+ run_void_tests(library);
+
+ // Test Int64 and UInt64.
+ run_Int64_tests();
+ run_UInt64_tests();
+
+ // Test the basic bool, integer, and float types.
+ run_bool_tests(library);
+
+ run_integer_tests(library, ctypes.int8_t, "int8_t", 1, true, [-0x80, 0x7f]);
+ run_integer_tests(library, ctypes.int16_t, "int16_t", 2, true, [-0x8000, 0x7fff]);
+ run_integer_tests(library, ctypes.int32_t, "int32_t", 4, true, [-0x80000000, 0x7fffffff]);
+ run_integer_tests(library, ctypes.uint8_t, "uint8_t", 1, false, [0, 0xff]);
+ run_integer_tests(library, ctypes.uint16_t, "uint16_t", 2, false, [0, 0xffff]);
+ run_integer_tests(library, ctypes.uint32_t, "uint32_t", 4, false, [0, 0xffffffff]);
+ run_integer_tests(library, ctypes.short, "short", 2, true, [-0x8000, 0x7fff]);
+ run_integer_tests(library, ctypes.unsigned_short, "unsigned_short", 2, false, [0, 0xffff]);
+ run_integer_tests(library, ctypes.int, "int", 4, true, [-0x80000000, 0x7fffffff]);
+ run_integer_tests(library, ctypes.unsigned_int, "unsigned_int", 4, false, [0, 0xffffffff]);
+ run_integer_tests(library, ctypes.unsigned, "unsigned_int", 4, false, [0, 0xffffffff]);
+
+ run_float_tests(library, ctypes.float32_t, "float32_t", 4);
+ run_float_tests(library, ctypes.float64_t, "float64_t", 8);
+ run_float_tests(library, ctypes.float, "float", 4);
+ run_float_tests(library, ctypes.double, "double", 8);
+
+ // Test the wrapped integer types.
+ s64limits = ["-9223372036854775808", "9223372036854775807",
+ "-9223372036854775809", "9223372036854775808"];
+ u64limits = ["0", "18446744073709551615", "-1", "18446744073709551616"];
+
+ run_wrapped_integer_tests(library, ctypes.int64_t, "int64_t", 8, true,
+ ctypes.Int64, "ctypes.Int64", s64limits);
+ run_wrapped_integer_tests(library, ctypes.uint64_t, "uint64_t", 8, false,
+ ctypes.UInt64, "ctypes.UInt64", u64limits);
+ run_wrapped_integer_tests(library, ctypes.long_long, "long_long", 8, true,
+ ctypes.Int64, "ctypes.Int64", s64limits);
+ run_wrapped_integer_tests(library, ctypes.unsigned_long_long, "unsigned_long_long", 8, false,
+ ctypes.UInt64, "ctypes.UInt64", u64limits);
+
+ s32limits = [-0x80000000, 0x7fffffff, -0x80000001, 0x80000000];
+ u32limits = [0, 0xffffffff, -1, 0x100000000];
+
+ let slimits, ulimits;
+ if (ctypes.long.size == 8) {
+ slimits = s64limits;
+ ulimits = u64limits;
+ } else if (ctypes.long.size == 4) {
+ slimits = s32limits;
+ ulimits = u32limits;
+ } else {
+ do_throw("ctypes.long is not 4 or 8 bytes");
+ }
+
+ run_wrapped_integer_tests(library, ctypes.long, "long", ctypes.long.size, true,
+ ctypes.Int64, "ctypes.Int64", slimits);
+ run_wrapped_integer_tests(library, ctypes.unsigned_long, "unsigned_long", ctypes.long.size, false,
+ ctypes.UInt64, "ctypes.UInt64", ulimits);
+
+ if (ctypes.size_t.size == 8) {
+ slimits = s64limits;
+ ulimits = u64limits;
+ } else if (ctypes.size_t.size == 4) {
+ slimits = s32limits;
+ ulimits = u32limits;
+ } else {
+ do_throw("ctypes.size_t is not 4 or 8 bytes");
+ }
+
+ run_wrapped_integer_tests(library, ctypes.size_t, "size_t", ctypes.size_t.size, false,
+ ctypes.UInt64, "ctypes.UInt64", ulimits);
+ run_wrapped_integer_tests(library, ctypes.ssize_t, "ssize_t", ctypes.size_t.size, true,
+ ctypes.Int64, "ctypes.Int64", slimits);
+ run_wrapped_integer_tests(library, ctypes.uintptr_t, "uintptr_t", ctypes.size_t.size, false,
+ ctypes.UInt64, "ctypes.UInt64", ulimits);
+ run_wrapped_integer_tests(library, ctypes.intptr_t, "intptr_t", ctypes.size_t.size, true,
+ ctypes.Int64, "ctypes.Int64", slimits);
+
+ if (ctypes.off_t.size == 8) {
+ slimits = s64limits;
+ ulimits = u64limits;
+ } else if (ctypes.off_t.size == 4) {
+ slimits = s32limits;
+ ulimits = u32limits;
+ } else {
+ do_throw("ctypes.off_t is not 4 or 8 bytes");
+ }
+ run_wrapped_integer_tests(library, ctypes.off_t, "off_t", ctypes.off_t.size, true,
+ ctypes.Int64, "ctypes.Int64", slimits);
+
+ // Test the character types.
+ run_char_tests(library, ctypes.char, "char", 1, true, [-0x80, 0x7f]);
+ run_char_tests(library, ctypes.signed_char, "signed_char", 1, true, [-0x80, 0x7f]);
+ run_char_tests(library, ctypes.unsigned_char, "unsigned_char", 1, false, [0, 0xff]);
+ run_char16_tests(library, ctypes.char16_t, "char16_t", [0, 0xffff]);
+
+ // Test the special types.
+ run_StructType_tests();
+ run_PointerType_tests();
+ run_FunctionType_tests();
+ run_ArrayType_tests();
+
+ // Check that types print properly.
+ run_type_toString_tests();
+
+ // Test the 'name' and 'toSource' of a long typename.
+ let ptrTo_ptrTo_arrayOf4_ptrTo_int32s =
+ new ctypes.PointerType(
+ new ctypes.PointerType(
+ new ctypes.ArrayType(
+ new ctypes.PointerType(ctypes.int32_t), 4)));
+ do_check_eq(ptrTo_ptrTo_arrayOf4_ptrTo_int32s.name, "int32_t*(**)[4]");
+
+ let source_t = new ctypes.StructType("source",
+ [{ a: ptrTo_ptrTo_arrayOf4_ptrTo_int32s }, { b: ctypes.int64_t }]);
+ do_check_eq(source_t.toSource(),
+ 'ctypes.StructType("source", [{ "a": ctypes.int32_t.ptr.array(4).ptr.ptr }, ' +
+ '{ "b": ctypes.int64_t }])');
+
+ // Test ctypes.cast.
+ run_cast_tests();
+
+ run_string_tests(library);
+ run_readstring_tests(library);
+ run_struct_tests(library);
+ run_function_tests(library);
+ run_closure_tests(library);
+ run_variadic_tests(library);
+ run_static_data_tests(library);
+ run_cpp_class_tests(library);
+
+ // test library.close
+ let test_void_t = library.declare("test_void_t_cdecl", ctypes.default_abi, ctypes.void_t);
+ library.close();
+ do_check_throws(function() { test_void_t(); }, Error);
+ do_check_throws(function() {
+ library.declare("test_void_t_cdecl", ctypes.default_abi, ctypes.void_t);
+ }, Error);
+
+ // test that library functions throw when bound to other objects
+ library = ctypes.open(libfile.path);
+ let obj = {};
+ obj.declare = library.declare;
+ do_check_throws(function () { run_void_tests(obj); }, Error);
+ obj.close = library.close;
+ do_check_throws(function () { obj.close(); }, Error);
+
+ // test that functions work as properties of other objects
+ let getter = library.declare("get_int8_t_cdecl", ctypes.default_abi, ctypes.int8_t);
+ do_check_eq(getter(), 109);
+ obj.t = getter;
+ do_check_eq(obj.t(), 109);
+
+ // bug 521937
+ do_check_throws(function () { let nolib = ctypes.open("notfoundlibrary.dll"); nolib.close(); }, Error);
+
+ // bug 522360
+ do_check_eq(run_load_system_library(), true);
+
+ // Test loading a library with a unicode name (bug 589413). Note that nsIFile
+ // implementations are not available in some harnesses; if not, the harness
+ // should take care of the copy for us.
+ let unicodefile = do_get_file(CTYPES_UNICODE_LIB, true);
+ let copy = libfile.copyTo instanceof Function;
+ if (copy)
+ libfile.copyTo(null, unicodefile.leafName);
+ library = ctypes.open(unicodefile.path);
+ run_void_tests(library);
+ library.close();
+ if (copy)
+ unicodefile.remove(false);
+}
+
+function run_abstract_class_tests()
+{
+ // Test that ctypes.CType is an abstract constructor that throws.
+ do_check_throws(function() { ctypes.CType(); }, TypeError);
+ do_check_throws(function() { new ctypes.CType() }, TypeError);
+
+ do_check_true(ctypes.CType.hasOwnProperty("prototype"));
+ do_check_throws(function() { ctypes.CType.prototype(); }, TypeError);
+ do_check_throws(function() { new ctypes.CType.prototype() }, TypeError);
+
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("constructor"));
+ do_check_true(ctypes.CType.prototype.constructor === ctypes.CType);
+
+ // Check that ctypes.CType.prototype has the correct properties and functions.
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("name"));
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("size"));
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("ptr"));
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("array"));
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("toString"));
+ do_check_true(ctypes.CType.prototype.hasOwnProperty("toSource"));
+
+ // Make sure we can access 'prototype' on a CTypeProto.
+ do_check_true(ctypes.CType.prototype.prototype === ctypes.CData.prototype);
+
+ // Check that the shared properties and functions on ctypes.CType.prototype throw.
+ do_check_throws(function() { ctypes.CType.prototype.name; }, TypeError);
+ do_check_throws(function() { ctypes.CType.prototype.size; }, TypeError);
+ do_check_throws(function() { ctypes.CType.prototype.ptr; }, TypeError);
+ do_check_throws(function() { ctypes.CType.prototype.array(); }, TypeError);
+
+
+ // toString and toSource are called by the web console during inspection,
+ // so we don't want them to throw.
+ do_check_eq(typeof ctypes.CType.prototype.toString(), 'string');
+ do_check_eq(typeof ctypes.CType.prototype.toSource(), 'string');
+
+ // Test that ctypes.CData is an abstract constructor that throws.
+ do_check_throws(function() { ctypes.CData(); }, TypeError);
+ do_check_throws(function() { new ctypes.CData() }, TypeError);
+
+ do_check_true(ctypes.CData.__proto__ === ctypes.CType.prototype);
+ do_check_true(ctypes.CData instanceof ctypes.CType);
+
+ do_check_true(ctypes.CData.hasOwnProperty("prototype"));
+ do_check_true(ctypes.CData.prototype.hasOwnProperty("constructor"));
+ do_check_true(ctypes.CData.prototype.constructor === ctypes.CData);
+
+ // Check that ctypes.CData.prototype has the correct properties and functions.
+ do_check_true(ctypes.CData.prototype.hasOwnProperty("value"));
+ do_check_true(ctypes.CData.prototype.hasOwnProperty("address"));
+ do_check_true(ctypes.CData.prototype.hasOwnProperty("readString"));
+ do_check_true(ctypes.CData.prototype.hasOwnProperty("toString"));
+ do_check_true(ctypes.CData.prototype.hasOwnProperty("toSource"));
+
+ // Check that the shared properties and functions on ctypes.CData.prototype throw.
+ do_check_throws(function() { ctypes.CData.prototype.value; }, TypeError);
+ do_check_throws(function() { ctypes.CData.prototype.value = null; }, TypeError);
+ do_check_throws(function() { ctypes.CData.prototype.address(); }, TypeError);
+ do_check_throws(function() { ctypes.CData.prototype.readString(); }, TypeError);
+
+ // toString and toSource are called by the web console during inspection,
+ // so we don't want them to throw.
+ do_check_eq(ctypes.CData.prototype.toString(), '[CData proto object]');
+ do_check_eq(ctypes.CData.prototype.toSource(), '[CData proto object]');
+}
+
+function run_Int64_tests() {
+ do_check_throws(function() { ctypes.Int64(); }, TypeError);
+
+ do_check_true(ctypes.Int64.hasOwnProperty("prototype"));
+ do_check_true(ctypes.Int64.prototype.hasOwnProperty("constructor"));
+ do_check_true(ctypes.Int64.prototype.constructor === ctypes.Int64);
+
+ // Check that ctypes.Int64 and ctypes.Int64.prototype have the correct
+ // properties and functions.
+ do_check_true(ctypes.Int64.hasOwnProperty("compare"));
+ do_check_true(ctypes.Int64.hasOwnProperty("lo"));
+ do_check_true(ctypes.Int64.hasOwnProperty("hi"));
+ do_check_true(ctypes.Int64.hasOwnProperty("join"));
+ do_check_true(ctypes.Int64.prototype.hasOwnProperty("toString"));
+ do_check_true(ctypes.Int64.prototype.hasOwnProperty("toSource"));
+
+ // Check that the shared functions on ctypes.Int64.prototype throw.
+ do_check_throws(function() { ctypes.Int64.prototype.toString(); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.prototype.toSource(); }, TypeError);
+
+ let int64 = ctypes.Int64(0);
+ do_check_true(int64.__proto__ === ctypes.Int64.prototype);
+ do_check_true(int64 instanceof ctypes.Int64);
+
+ // Test Int64.toString([radix]).
+ do_check_eq(int64.toString(), "0");
+ for (let radix = 2; radix <= 36; ++radix)
+ do_check_eq(int64.toString(radix), "0");
+ do_check_throws(function() { int64.toString(0); }, RangeError);
+ do_check_throws(function() { int64.toString(1); }, RangeError);
+ do_check_throws(function() { int64.toString(37); }, RangeError);
+ do_check_throws(function() { int64.toString(10, 2); }, TypeError);
+
+ // Test Int64.toSource().
+ do_check_eq(int64.toSource(), "ctypes.Int64(\"0\")");
+ do_check_throws(function() { int64.toSource(10); }, TypeError);
+
+ int64 = ctypes.Int64("0x28590a1c921def71");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "2907366152271163249");
+ do_check_eq(int64.toString(16), "28590a1c921def71");
+ do_check_eq(int64.toString(2), "10100001011001000010100001110010010010000111011110111101110001");
+ do_check_eq(int64.toSource(), "ctypes.Int64(\"" + int64.toString(10) + "\")");
+
+ int64 = ctypes.Int64("-0x28590a1c921def71");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "-2907366152271163249");
+ do_check_eq(int64.toString(16), "-28590a1c921def71");
+ do_check_eq(int64.toString(2), "-10100001011001000010100001110010010010000111011110111101110001");
+ do_check_eq(int64.toSource(), "ctypes.Int64(\"" + int64.toString(10) + "\")");
+
+ int64 = ctypes.Int64("-0X28590A1c921DEf71");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "-2907366152271163249");
+ do_check_eq(int64.toString(16), "-28590a1c921def71");
+ do_check_eq(int64.toString(2), "-10100001011001000010100001110010010010000111011110111101110001");
+ do_check_eq(int64.toSource(), "ctypes.Int64(\"" + int64.toString(10) + "\")");
+
+ // Test Int64(primitive double) constructor.
+ int64 = ctypes.Int64(-0);
+ do_check_eq(int64.toString(), "0");
+
+ int64 = ctypes.Int64(0x7ffffffffffff000);
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "9223372036854771712");
+ do_check_eq(int64.toString(16), "7ffffffffffff000");
+ do_check_eq(int64.toString(2), "111111111111111111111111111111111111111111111111111000000000000");
+
+ int64 = ctypes.Int64(-0x8000000000000000);
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "-9223372036854775808");
+ do_check_eq(int64.toString(16), "-8000000000000000");
+ do_check_eq(int64.toString(2), "-1000000000000000000000000000000000000000000000000000000000000000");
+
+ // Test Int64(string) constructor.
+ int64 = ctypes.Int64("0x7fffffffffffffff");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "9223372036854775807");
+ do_check_eq(int64.toString(16), "7fffffffffffffff");
+ do_check_eq(int64.toString(2), "111111111111111111111111111111111111111111111111111111111111111");
+
+ int64 = ctypes.Int64("-0x8000000000000000");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "-9223372036854775808");
+ do_check_eq(int64.toString(16), "-8000000000000000");
+ do_check_eq(int64.toString(2), "-1000000000000000000000000000000000000000000000000000000000000000");
+
+ int64 = ctypes.Int64("9223372036854775807");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "9223372036854775807");
+ do_check_eq(int64.toString(16), "7fffffffffffffff");
+ do_check_eq(int64.toString(2), "111111111111111111111111111111111111111111111111111111111111111");
+
+ int64 = ctypes.Int64("-9223372036854775808");
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "-9223372036854775808");
+ do_check_eq(int64.toString(16), "-8000000000000000");
+ do_check_eq(int64.toString(2), "-1000000000000000000000000000000000000000000000000000000000000000");
+
+ // Test Int64(other Int64) constructor.
+ int64 = ctypes.Int64(ctypes.Int64(0));
+ do_check_eq(int64.toString(), "0");
+
+ int64 = ctypes.Int64(ctypes.Int64("0x7fffffffffffffff"));
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "9223372036854775807");
+ do_check_eq(int64.toString(16), "7fffffffffffffff");
+ do_check_eq(int64.toString(2), "111111111111111111111111111111111111111111111111111111111111111");
+
+ int64 = ctypes.Int64(ctypes.Int64("-0x8000000000000000"));
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "-9223372036854775808");
+ do_check_eq(int64.toString(16), "-8000000000000000");
+ do_check_eq(int64.toString(2), "-1000000000000000000000000000000000000000000000000000000000000000");
+
+ // Test Int64(other UInt64) constructor.
+ int64 = ctypes.Int64(ctypes.UInt64(0));
+ do_check_eq(int64.toString(), "0");
+
+ int64 = ctypes.Int64(ctypes.UInt64("0x7fffffffffffffff"));
+ do_check_eq(int64.toString(), int64.toString(10));
+ do_check_eq(int64.toString(10), "9223372036854775807");
+ do_check_eq(int64.toString(16), "7fffffffffffffff");
+ do_check_eq(int64.toString(2), "111111111111111111111111111111111111111111111111111111111111111");
+
+ let vals = [-0x8000000000001000, 0x8000000000000000,
+ ctypes.UInt64("0x8000000000000000"),
+ Infinity, -Infinity, NaN, 0.1,
+ 5.68e21, null, undefined, "", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { ctypes.Int64(vals[i]); }, TypeError);
+
+ vals = ["-0x8000000000000001", "0x8000000000000000"];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { ctypes.Int64(vals[i]); }, RangeError);
+
+ // Test ctypes.Int64.compare.
+ do_check_eq(ctypes.Int64.compare(ctypes.Int64(5), ctypes.Int64(5)), 0);
+ do_check_eq(ctypes.Int64.compare(ctypes.Int64(5), ctypes.Int64(4)), 1);
+ do_check_eq(ctypes.Int64.compare(ctypes.Int64(4), ctypes.Int64(5)), -1);
+ do_check_eq(ctypes.Int64.compare(ctypes.Int64(-5), ctypes.Int64(-5)), 0);
+ do_check_eq(ctypes.Int64.compare(ctypes.Int64(-5), ctypes.Int64(-4)), -1);
+ do_check_eq(ctypes.Int64.compare(ctypes.Int64(-4), ctypes.Int64(-5)), 1);
+ do_check_throws(function() { ctypes.Int64.compare(ctypes.Int64(4), ctypes.UInt64(4)); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.compare(4, 5); }, TypeError);
+
+ // Test ctypes.Int64.{lo,hi}.
+ do_check_eq(ctypes.Int64.lo(ctypes.Int64(0x28590a1c921de000)), 0x921de000);
+ do_check_eq(ctypes.Int64.hi(ctypes.Int64(0x28590a1c921de000)), 0x28590a1c);
+ do_check_eq(ctypes.Int64.lo(ctypes.Int64(-0x28590a1c921de000)), 0x6de22000);
+ do_check_eq(ctypes.Int64.hi(ctypes.Int64(-0x28590a1c921de000)), -0x28590a1d);
+ do_check_throws(function() { ctypes.Int64.lo(ctypes.UInt64(0)); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.hi(ctypes.UInt64(0)); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.lo(0); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.hi(0); }, TypeError);
+
+ // Test ctypes.Int64.join.
+ do_check_eq(ctypes.Int64.join(0, 0).toString(), "0");
+ do_check_eq(ctypes.Int64.join(0x28590a1c, 0x921de000).toString(16), "28590a1c921de000");
+ do_check_eq(ctypes.Int64.join(-0x28590a1d, 0x6de22000).toString(16), "-28590a1c921de000");
+ do_check_eq(ctypes.Int64.join(0x7fffffff, 0xffffffff).toString(16), "7fffffffffffffff");
+ do_check_eq(ctypes.Int64.join(-0x80000000, 0x00000000).toString(16), "-8000000000000000");
+ do_check_throws(function() { ctypes.Int64.join(-0x80000001, 0); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.join(0x80000000, 0); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.join(0, -0x1); }, TypeError);
+ do_check_throws(function() { ctypes.Int64.join(0, 0x800000000); }, TypeError);
+}
+
+function run_UInt64_tests() {
+ do_check_throws(function() { ctypes.UInt64(); }, TypeError);
+
+ do_check_true(ctypes.UInt64.hasOwnProperty("prototype"));
+ do_check_true(ctypes.UInt64.prototype.hasOwnProperty("constructor"));
+ do_check_true(ctypes.UInt64.prototype.constructor === ctypes.UInt64);
+
+ // Check that ctypes.UInt64 and ctypes.UInt64.prototype have the correct
+ // properties and functions.
+ do_check_true(ctypes.UInt64.hasOwnProperty("compare"));
+ do_check_true(ctypes.UInt64.hasOwnProperty("lo"));
+ do_check_true(ctypes.UInt64.hasOwnProperty("hi"));
+ do_check_true(ctypes.UInt64.hasOwnProperty("join"));
+ do_check_true(ctypes.UInt64.prototype.hasOwnProperty("toString"));
+ do_check_true(ctypes.UInt64.prototype.hasOwnProperty("toSource"));
+
+ // Check that the shared functions on ctypes.UInt64.prototype throw.
+ do_check_throws(function() { ctypes.UInt64.prototype.toString(); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.prototype.toSource(); }, TypeError);
+
+ let uint64 = ctypes.UInt64(0);
+ do_check_true(uint64.__proto__ === ctypes.UInt64.prototype);
+ do_check_true(uint64 instanceof ctypes.UInt64);
+
+ // Test UInt64.toString([radix]).
+ do_check_eq(uint64.toString(), "0");
+ for (let radix = 2; radix <= 36; ++radix)
+ do_check_eq(uint64.toString(radix), "0");
+ do_check_throws(function() { uint64.toString(0); }, RangeError);
+ do_check_throws(function() { uint64.toString(1); }, RangeError);
+ do_check_throws(function() { uint64.toString(37); }, RangeError);
+ do_check_throws(function() { uint64.toString(10, 2); }, TypeError);
+
+ // Test UInt64.toSource().
+ do_check_eq(uint64.toSource(), "ctypes.UInt64(\"0\")");
+ do_check_throws(function() { uint64.toSource(10); }, TypeError);
+
+ uint64 = ctypes.UInt64("0x28590a1c921def71");
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "2907366152271163249");
+ do_check_eq(uint64.toString(16), "28590a1c921def71");
+ do_check_eq(uint64.toString(2), "10100001011001000010100001110010010010000111011110111101110001");
+ do_check_eq(uint64.toSource(), "ctypes.UInt64(\"" + uint64.toString(10) + "\")");
+
+ uint64 = ctypes.UInt64("0X28590A1c921DEf71");
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "2907366152271163249");
+ do_check_eq(uint64.toString(16), "28590a1c921def71");
+ do_check_eq(uint64.toString(2), "10100001011001000010100001110010010010000111011110111101110001");
+ do_check_eq(uint64.toSource(), "ctypes.UInt64(\"" + uint64.toString(10) + "\")");
+
+ // Test UInt64(primitive double) constructor.
+ uint64 = ctypes.UInt64(-0);
+ do_check_eq(uint64.toString(), "0");
+
+ uint64 = ctypes.UInt64(0xfffffffffffff000);
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "18446744073709547520");
+ do_check_eq(uint64.toString(16), "fffffffffffff000");
+ do_check_eq(uint64.toString(2), "1111111111111111111111111111111111111111111111111111000000000000");
+
+ // Test UInt64(string) constructor.
+ uint64 = ctypes.UInt64("0xffffffffffffffff");
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "18446744073709551615");
+ do_check_eq(uint64.toString(16), "ffffffffffffffff");
+ do_check_eq(uint64.toString(2), "1111111111111111111111111111111111111111111111111111111111111111");
+
+ uint64 = ctypes.UInt64("0x0");
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "0");
+ do_check_eq(uint64.toString(16), "0");
+ do_check_eq(uint64.toString(2), "0");
+
+ uint64 = ctypes.UInt64("18446744073709551615");
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "18446744073709551615");
+ do_check_eq(uint64.toString(16), "ffffffffffffffff");
+ do_check_eq(uint64.toString(2), "1111111111111111111111111111111111111111111111111111111111111111");
+
+ uint64 = ctypes.UInt64("0");
+ do_check_eq(uint64.toString(), "0");
+
+ // Test UInt64(other UInt64) constructor.
+ uint64 = ctypes.UInt64(ctypes.UInt64(0));
+ do_check_eq(uint64.toString(), "0");
+
+ uint64 = ctypes.UInt64(ctypes.UInt64("0xffffffffffffffff"));
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "18446744073709551615");
+ do_check_eq(uint64.toString(16), "ffffffffffffffff");
+ do_check_eq(uint64.toString(2), "1111111111111111111111111111111111111111111111111111111111111111");
+
+ uint64 = ctypes.UInt64(ctypes.UInt64("0x0"));
+ do_check_eq(uint64.toString(), "0");
+
+ // Test UInt64(other Int64) constructor.
+ uint64 = ctypes.UInt64(ctypes.Int64(0));
+ do_check_eq(uint64.toString(), "0");
+
+ uint64 = ctypes.UInt64(ctypes.Int64("0x7fffffffffffffff"));
+ do_check_eq(uint64.toString(), uint64.toString(10));
+ do_check_eq(uint64.toString(10), "9223372036854775807");
+ do_check_eq(uint64.toString(16), "7fffffffffffffff");
+ do_check_eq(uint64.toString(2), "111111111111111111111111111111111111111111111111111111111111111");
+
+ let vals = [-1, 0x10000000000000000, "-1", "-0x1",
+ ctypes.Int64("-1"), Infinity, -Infinity, NaN, 0.1,
+ 5.68e21, null, undefined, "", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { ctypes.UInt64(vals[i]); }, TypeError);
+
+ vals = ["0x10000000000000000"];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { ctypes.UInt64(vals[i]); }, RangeError);
+
+ // Test ctypes.UInt64.compare.
+ do_check_eq(ctypes.UInt64.compare(ctypes.UInt64(5), ctypes.UInt64(5)), 0);
+ do_check_eq(ctypes.UInt64.compare(ctypes.UInt64(5), ctypes.UInt64(4)), 1);
+ do_check_eq(ctypes.UInt64.compare(ctypes.UInt64(4), ctypes.UInt64(5)), -1);
+ do_check_throws(function() { ctypes.UInt64.compare(ctypes.UInt64(4), ctypes.Int64(4)); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.compare(4, 5); }, TypeError);
+
+ // Test ctypes.UInt64.{lo,hi}.
+ do_check_eq(ctypes.UInt64.lo(ctypes.UInt64(0x28590a1c921de000)), 0x921de000);
+ do_check_eq(ctypes.UInt64.hi(ctypes.UInt64(0x28590a1c921de000)), 0x28590a1c);
+ do_check_eq(ctypes.UInt64.lo(ctypes.UInt64(0xa8590a1c921de000)), 0x921de000);
+ do_check_eq(ctypes.UInt64.hi(ctypes.UInt64(0xa8590a1c921de000)), 0xa8590a1c);
+ do_check_throws(function() { ctypes.UInt64.lo(ctypes.Int64(0)); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.hi(ctypes.Int64(0)); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.lo(0); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.hi(0); }, TypeError);
+
+ // Test ctypes.UInt64.join.
+ do_check_eq(ctypes.UInt64.join(0, 0).toString(), "0");
+ do_check_eq(ctypes.UInt64.join(0x28590a1c, 0x921de000).toString(16), "28590a1c921de000");
+ do_check_eq(ctypes.UInt64.join(0xa8590a1c, 0x921de000).toString(16), "a8590a1c921de000");
+ do_check_eq(ctypes.UInt64.join(0xffffffff, 0xffffffff).toString(16), "ffffffffffffffff");
+ do_check_eq(ctypes.UInt64.join(0, 0).toString(16), "0");
+ do_check_throws(function() { ctypes.UInt64.join(-0x1, 0); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.join(0x100000000, 0); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.join(0, -0x1); }, TypeError);
+ do_check_throws(function() { ctypes.UInt64.join(0, 0x1000000000); }, TypeError);
+}
+
+function run_basic_abi_tests(library, t, name, toprimitive,
+ get_test, set_tests, sum_tests, sum_many_tests) {
+ // Test the function call ABI for calls involving the type.
+ function declare_fn_cdecl(fn_t, prefix) {
+ return library.declare(prefix + name + "_cdecl", fn_t);
+ }
+ run_single_abi_tests(declare_fn_cdecl, ctypes.default_abi, t,
+ toprimitive, get_test, set_tests, sum_tests, sum_many_tests);
+
+ if ("winLastError" in ctypes) {
+ function declare_fn_stdcall(fn_t, prefix) {
+ return library.declare(prefix + name + "_stdcall", fn_t);
+ }
+ run_single_abi_tests(declare_fn_stdcall, ctypes.stdcall_abi, t,
+ toprimitive, get_test, set_tests, sum_tests, sum_many_tests);
+
+ // Check that declaring a WINAPI function gets the right symbol name.
+ let libuser32 = ctypes.open("user32.dll");
+ let charupper = libuser32.declare("CharUpperA",
+ ctypes.winapi_abi,
+ ctypes.char.ptr,
+ ctypes.char.ptr);
+ let hello = ctypes.char.array()("hello!");
+ do_check_eq(charupper(hello).readString(), "HELLO!");
+ }
+
+ // Check the alignment of the type, and its behavior in a struct,
+ // against what C says.
+ check_struct_stats(library, t);
+
+ // Check the ToSource functions defined in the namespace ABI
+ do_check_eq(ctypes.default_abi.toSource(), "ctypes.default_abi");
+
+ let exn;
+ try {
+ ctypes.default_abi.toSource.call(null);
+ } catch (x) {
+ exn = x;
+ }
+ do_check_true(!!exn); // Check that some exception was raised
+}
+
+function run_single_abi_tests(decl, abi, t, toprimitive,
+ get_test, set_tests, sum_tests, sum_many_tests) {
+ let getter_t = ctypes.FunctionType(abi, t).ptr;
+ let getter = decl(getter_t, "get_");
+ do_check_eq(toprimitive(getter()), get_test);
+
+ let setter_t = ctypes.FunctionType(abi, t, [t]).ptr;
+ let setter = decl(setter_t, "set_");
+ for (let i of set_tests)
+ do_check_eq(toprimitive(setter(i)), i);
+
+ let sum_t = ctypes.FunctionType(abi, t, [t, t]).ptr;
+ let sum = decl(sum_t, "sum_");
+ for (let a of sum_tests)
+ do_check_eq(toprimitive(sum(a[0], a[1])), a[2]);
+
+ let sum_alignb_t = ctypes.FunctionType(abi, t,
+ [ctypes.char, t, ctypes.char, t, ctypes.char]).ptr;
+ let sum_alignb = decl(sum_alignb_t, "sum_alignb_");
+ let sum_alignf_t = ctypes.FunctionType(abi, t,
+ [ctypes.float, t, ctypes.float, t, ctypes.float]).ptr;
+ let sum_alignf = decl(sum_alignf_t, "sum_alignf_");
+ for (let a of sum_tests) {
+ do_check_eq(toprimitive(sum_alignb(0, a[0], 0, a[1], 0)), a[2]);
+ do_check_eq(toprimitive(sum_alignb(1, a[0], 1, a[1], 1)), a[2]);
+ do_check_eq(toprimitive(sum_alignf(0, a[0], 0, a[1], 0)), a[2]);
+ do_check_eq(toprimitive(sum_alignf(1, a[0], 1, a[1], 1)), a[2]);
+ }
+
+ let sum_many_t = ctypes.FunctionType(abi, t,
+ [t, t, t, t, t, t, t, t, t, t, t, t, t, t, t, t, t, t]).ptr;
+ let sum_many = decl(sum_many_t, "sum_many_");
+ for (let a of sum_many_tests)
+ do_check_eq(
+ toprimitive(sum_many(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7],
+ a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15],
+ a[16], a[17])), a[18]);
+}
+
+function check_struct_stats(library, t) {
+ let s_t = ctypes.StructType("s_t", [{ x: ctypes.char }, { y: t }]);
+ let n_t = ctypes.StructType("n_t", [{ a: ctypes.char }, { b: s_t }, { c: ctypes.char }]);
+ let get_stats = library.declare("get_" + t.name + "_stats",
+ ctypes.default_abi, ctypes.void_t,
+ ctypes.size_t.ptr, ctypes.size_t.ptr, ctypes.size_t.ptr, ctypes.size_t.ptr,
+ ctypes.size_t.array());
+
+ let align = ctypes.size_t();
+ let size = ctypes.size_t();
+ let nalign = ctypes.size_t();
+ let nsize = ctypes.size_t();
+ let offsets = ctypes.size_t.array(3)();
+ get_stats(align.address(), size.address(), nalign.address(), nsize.address(),
+ offsets);
+
+ do_check_eq(size.value, s_t.size);
+ do_check_eq(align.value, s_t.size - t.size);
+ do_check_eq(align.value, offsetof(s_t, "y"));
+ do_check_eq(nsize.value, n_t.size);
+ do_check_eq(nalign.value, offsetof(n_t, "b"));
+ do_check_eq(offsets[0], offsetof(s_t, "y"));
+ do_check_eq(offsets[1], offsetof(n_t, "b"));
+ do_check_eq(offsets[2], offsetof(n_t, "c"));
+}
+
+// Determine the offset, in bytes, of 'member' within 'struct'.
+function offsetof(struct, member) {
+ let instance = struct();
+ let memberptr = ptrValue(instance.addressOfField(member));
+ let chararray = ctypes.cast(instance, ctypes.char.array(struct.size));
+ let offset = 0;
+ while (memberptr != ptrValue(chararray.addressOfElement(offset)))
+ ++offset;
+ return offset;
+}
+
+// Test the class and prototype hierarchy for a given basic type 't'.
+function run_basic_class_tests(t)
+{
+ do_check_true(t.__proto__ === ctypes.CType.prototype);
+ do_check_true(t instanceof ctypes.CType);
+
+ do_check_true(t.prototype.__proto__ === ctypes.CData.prototype);
+ do_check_true(t.prototype instanceof ctypes.CData);
+ do_check_true(t.prototype.constructor === t);
+
+ // Check that the shared properties and functions on 't.prototype' throw.
+ do_check_throws(function() { t.prototype.value; }, TypeError);
+ do_check_throws(function() { t.prototype.value = null; }, TypeError);
+ do_check_throws(function() { t.prototype.address(); }, TypeError);
+ do_check_throws(function() { t.prototype.readString(); }, TypeError);
+
+ // toString and toSource are called by the web console during inspection,
+ // so we don't want them to throw.
+ do_check_eq(t.prototype.toString(), '[CData proto object]');
+ do_check_eq(t.prototype.toSource(), '[CData proto object]');
+
+ // Test that an instance 'd' of 't' is a CData.
+ let d = t();
+ do_check_true(d.__proto__ === t.prototype);
+ do_check_true(d instanceof t);
+ do_check_true(d.constructor === t);
+}
+
+function run_bool_tests(library) {
+ let t = ctypes.bool;
+ run_basic_class_tests(t);
+
+ let name = "bool";
+ do_check_eq(t.name, name);
+ do_check_true(t.size == 1 || t.size == 4);
+
+ do_check_eq(t.toString(), "type " + name);
+ do_check_eq(t.toSource(), "ctypes." + name);
+ do_check_true(t.ptr === ctypes.PointerType(t));
+ do_check_eq(t.array().name, name + "[]");
+ do_check_eq(t.array(5).name, name + "[5]");
+
+ let d = t();
+ do_check_eq(d.value, 0);
+ d.value = 1;
+ do_check_eq(d.value, 1);
+ d.value = -0;
+ do_check_eq(1/d.value, 1/0);
+ d.value = false;
+ do_check_eq(d.value, 0);
+ d.value = true;
+ do_check_eq(d.value, 1);
+ d = new t(1);
+ do_check_eq(d.value, 1);
+
+ // don't convert anything else
+ let vals = [-1, 2, Infinity, -Infinity, NaN, 0.1,
+ ctypes.Int64(0), ctypes.UInt64(0),
+ null, undefined, "", "0", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { d.value = vals[i]; }, TypeError);
+
+ do_check_true(d.address().constructor === t.ptr);
+ do_check_eq(d.address().contents, d.value);
+ do_check_eq(d.toSource(), "ctypes." + name + "(" + d.value + ")");
+ do_check_eq(d.toSource(), d.toString());
+
+ // Test the function call ABI for calls involving the type,
+ // and check the alignment of the type against what C says.
+ function toprimitive(a) { return a; }
+ run_basic_abi_tests(library, t, name, toprimitive,
+ true,
+ [ false, true ],
+ [ [ false, false, false ], [ false, true, true ],
+ [ true, false, true ], [true, true, true ] ],
+ [ [ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false ],
+ [ true, true, true, true, true, true, true, true, true,
+ true, true, true, true, true, true, true, true, true,
+ true ] ]);
+}
+
+function run_integer_tests(library, t, name, size, signed, limits) {
+ run_basic_class_tests(t);
+
+ do_check_eq(t.name, name);
+ do_check_eq(t.size, size);
+
+ do_check_eq(t.toString(), "type " + name);
+ do_check_eq(t.toSource(), "ctypes." + name);
+ do_check_true(t.ptr === ctypes.PointerType(t));
+ do_check_eq(t.array().name, name + "[]");
+ do_check_eq(t.array(5).name, name + "[5]");
+
+ // Check the alignment of the type, and its behavior in a struct,
+ // against what C says.
+ check_struct_stats(library, t);
+
+ let d = t();
+ do_check_eq(d.value, 0);
+ d.value = 5;
+ do_check_eq(d.value, 5);
+ d = t(10);
+ do_check_eq(d.value, 10);
+ if (signed) {
+ d.value = -10;
+ do_check_eq(d.value, -10);
+ }
+ d = new t(20);
+ do_check_eq(d.value, 20);
+
+ d.value = ctypes.Int64(5);
+ do_check_eq(d.value, 5);
+ if (signed) {
+ d.value = ctypes.Int64(-5);
+ do_check_eq(d.value, -5);
+ }
+ d.value = ctypes.UInt64(5);
+ do_check_eq(d.value, 5);
+
+ d.value = limits[0];
+ do_check_eq(d.value, limits[0]);
+ d.value = limits[1];
+ do_check_eq(d.value, limits[1]);
+ d.value = 0;
+ do_check_eq(d.value, 0);
+ d.value = -0;
+ do_check_eq(1/d.value, 1/0);
+ d.value = false;
+ do_check_eq(d.value, 0);
+ d.value = true;
+ do_check_eq(d.value, 1);
+
+ // don't convert anything else
+ let vals = [limits[0] - 1, limits[1] + 1, Infinity, -Infinity, NaN, 0.1,
+ null, undefined, "", "0", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { d.value = vals[i]; }, TypeError);
+
+ do_check_true(d.address().constructor === t.ptr);
+ do_check_eq(d.address().contents, d.value);
+ do_check_eq(d.toSource(), "ctypes." + name + "(" + d.value + ")");
+ do_check_eq(d.toSource(), d.toString());
+
+ // Test the function call ABI for calls involving the type,
+ // and check the alignment of the type against what C says.
+ function toprimitive(a) { return a; }
+ run_basic_abi_tests(library, t, name, toprimitive,
+ 109,
+ [ 0, limits[0], limits[1] ],
+ [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 2 ],
+ [ limits[0], 1, limits[0] + 1 ],
+ [ limits[1], 1, limits[0] ] ],
+ [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0 ],
+ [ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 18 ] ]);
+}
+
+function run_float_tests(library, t, name, size) {
+ run_basic_class_tests(t);
+
+ do_check_eq(t.name, name);
+ do_check_eq(t.size, size);
+
+ do_check_eq(t.toString(), "type " + name);
+ do_check_eq(t.toSource(), "ctypes." + name);
+ do_check_true(t.ptr === ctypes.PointerType(t));
+ do_check_eq(t.array().name, name + "[]");
+ do_check_eq(t.array(5).name, name + "[5]");
+
+ let d = t();
+ do_check_eq(d.value, 0);
+ d.value = 5;
+ do_check_eq(d.value, 5);
+ d.value = 5.25;
+ do_check_eq(d.value, 5.25);
+ d = t(10);
+ do_check_eq(d.value, 10);
+ d.value = -10;
+ do_check_eq(d.value, -10);
+ d = new t(20);
+ do_check_eq(d.value, 20);
+
+ do_check_throws(function() { d.value = ctypes.Int64(5); }, TypeError);
+ do_check_throws(function() { d.value = ctypes.Int64(-5); }, TypeError);
+ do_check_throws(function() { d.value = ctypes.UInt64(5); }, TypeError);
+
+ if (size == 4) {
+ d.value = 0x7fffff;
+ do_check_eq(d.value, 0x7fffff);
+
+ // allow values that can't be represented precisely as a float
+ d.value = 0xffffffff;
+ let delta = 1 - d.value/0xffffffff;
+ do_check_true(delta != 0);
+ do_check_true(delta > -0.01 && delta < 0.01);
+ d.value = 1 + 1/0x80000000;
+ do_check_eq(d.value, 1);
+ } else {
+ d.value = 0xfffffffffffff000;
+ do_check_eq(d.value, 0xfffffffffffff000);
+
+ do_check_throws(function() { d.value = ctypes.Int64("0x7fffffffffffffff"); }, TypeError);
+ }
+
+ d.value = Infinity;
+ do_check_eq(d.value, Infinity);
+ d.value = -Infinity;
+ do_check_eq(d.value, -Infinity);
+ d.value = NaN;
+ do_check_true(isNaN(d.value));
+ d.value = 0;
+ do_check_eq(d.value, 0);
+ d.value = -0;
+ do_check_eq(1/d.value, 1/-0);
+
+ // don't convert anything else
+ let vals = [true, false, null, undefined, "", "0", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { d.value = vals[i]; }, TypeError);
+
+ // Check that values roundtrip through toSource() correctly.
+ function test_roundtrip(tFn, val)
+ {
+ let f1 = tFn(val);
+ eval("var f2 = " + f1.toSource());
+ do_check_eq(f1.value, f2.value);
+ }
+ vals = [Infinity, -Infinity, -0, 0, 1, -1, 1/3, -1/3, 1/4, -1/4,
+ 1e-14, -1e-14, 0xfffffffffffff000, -0xfffffffffffff000];
+ for (let i = 0; i < vals.length; i++)
+ test_roundtrip(t, vals[i]);
+ do_check_eq(t(NaN).toSource(), t.toSource() + "(NaN)");
+
+ do_check_true(d.address().constructor === t.ptr);
+ do_check_eq(d.address().contents, d.value);
+ do_check_eq(d.toSource(), "ctypes." + name + "(" + d.value + ")");
+ do_check_eq(d.toSource(), d.toString());
+
+ // Test the function call ABI for calls involving the type,
+ // and check the alignment of the type against what C says.
+ let operand = [];
+ if (size == 4) {
+ operand[0] = 503859.75;
+ operand[1] = 1012385.25;
+ operand[2] = 1516245;
+ } else {
+ operand[0] = 501823873859.75;
+ operand[1] = 171290577385.25;
+ operand[2] = 673114451245;
+ }
+ function toprimitive(a) { return a; }
+ run_basic_abi_tests(library, t, name, toprimitive,
+ 109.25,
+ [ 0, operand[0], operand[1] ],
+ [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 2 ],
+ [ operand[0], operand[1], operand[2] ] ],
+ [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0 ],
+ [ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 18 ] ]);
+}
+
+function run_wrapped_integer_tests(library, t, name, size, signed, w, wname, limits) {
+ run_basic_class_tests(t);
+
+ do_check_eq(t.name, name);
+ do_check_eq(t.size, size);
+
+ do_check_eq(t.toString(), "type " + name);
+ do_check_eq(t.toSource(), "ctypes." + name);
+ do_check_true(t.ptr === ctypes.PointerType(t));
+ do_check_eq(t.array().name, name + "[]");
+ do_check_eq(t.array(5).name, name + "[5]");
+
+ let d = t();
+ do_check_true(d.value instanceof w);
+ do_check_eq(d.value, 0);
+ d.value = 5;
+ do_check_eq(d.value, 5);
+ d = t(10);
+ do_check_eq(d.value, 10);
+ if (signed) {
+ d.value = -10;
+ do_check_eq(d.value, -10);
+ }
+ d = new t(20);
+ do_check_eq(d.value, 20);
+
+ d.value = ctypes.Int64(5);
+ do_check_eq(d.value, 5);
+ if (signed) {
+ d.value = ctypes.Int64(-5);
+ do_check_eq(d.value, -5);
+ }
+ d.value = ctypes.UInt64(5);
+ do_check_eq(d.value, 5);
+
+ d.value = w(limits[0]);
+ do_check_eq(d.value, limits[0]);
+ d.value = w(limits[1]);
+ do_check_eq(d.value, limits[1]);
+ d.value = 0;
+ do_check_eq(d.value, 0);
+ d.value = -0;
+ do_check_eq(1/d.value, 1/0);
+ d.value = false;
+ do_check_eq(d.value, 0);
+ d.value = true;
+ do_check_eq(d.value, 1);
+
+ // don't convert anything else
+ let vals = [limits[2], limits[3], Infinity, -Infinity, NaN, 0.1,
+ null, undefined, "", "0", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { d.value = vals[i]; }, TypeError);
+
+ do_check_true(d.address().constructor === t.ptr);
+ do_check_eq(d.address().contents.toString(), d.value.toString());
+ do_check_eq(d.toSource(), "ctypes." + name + "(" + wname + "(\"" + d.value + "\"))");
+ do_check_eq(d.toSource(), d.toString());
+
+ // Test the function call ABI for calls involving the type,
+ // and check the alignment of the type against what C says.
+ function toprimitive(a) { return a.toString(); }
+ run_basic_abi_tests(library, t, name, toprimitive,
+ 109,
+ [ 0, w(limits[0]), w(limits[1]) ],
+ [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 2 ],
+ signed ? [ w(limits[0]), -1, w(limits[1]) ]
+ : [ w(limits[1]), 1, w(limits[0]) ] ],
+ [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0 ],
+ [ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 18 ] ]);
+}
+
+function run_char_tests(library, t, name, size, signed, limits) {
+ run_basic_class_tests(t);
+
+ do_check_eq(t.name, name);
+ do_check_eq(t.size, size);
+
+ do_check_eq(t.toString(), "type " + name);
+ do_check_eq(t.toSource(), "ctypes." + name);
+ do_check_true(t.ptr === ctypes.PointerType(t));
+ do_check_eq(t.array().name, name + "[]");
+ do_check_eq(t.array(5).name, name + "[5]");
+
+ let d = t();
+ do_check_eq(d.value, 0);
+ d.value = 5;
+ do_check_eq(d.value, 5);
+ d = t(10);
+ do_check_eq(d.value, 10);
+ if (signed) {
+ d.value = -10;
+ do_check_eq(d.value, -10);
+ } else {
+ do_check_throws(function() { d.value = -10; }, TypeError);
+ }
+ d = new t(20);
+ do_check_eq(d.value, 20);
+
+ function toprimitive(a) { return a; }
+
+ d.value = ctypes.Int64(5);
+ do_check_eq(d.value, 5);
+ if (signed) {
+ d.value = ctypes.Int64(-10);
+ do_check_eq(d.value, -10);
+ }
+ d.value = ctypes.UInt64(5);
+ do_check_eq(d.value, 5);
+
+ d.value = limits[0];
+ do_check_eq(d.value, limits[0]);
+ d.value = limits[1];
+ do_check_eq(d.value, limits[1]);
+ d.value = 0;
+ do_check_eq(d.value, 0);
+ d.value = -0;
+ do_check_eq(1/d.value, 1/0);
+ d.value = false;
+ do_check_eq(d.value, 0);
+ d.value = true;
+ do_check_eq(d.value, 1);
+
+ do_check_throws(function() { d.value = "5"; }, TypeError);
+
+ // don't convert anything else
+ let vals = [limits[0] - 1, limits[1] + 1, Infinity, -Infinity, NaN, 0.1,
+ null, undefined, "", "aa", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { d.value = vals[i]; }, TypeError);
+
+ do_check_true(d.address().constructor === t.ptr);
+ do_check_eq(d.address().contents, 1);
+ do_check_eq(d.toSource(), "ctypes." + name + "(" + d.value + ")");
+ do_check_eq(d.toSource(), d.toString());
+
+ // Test string autoconversion (and lack thereof).
+ let literal = "autoconverted";
+ let s = t.array()(literal);
+ do_check_eq(s.readString(), literal);
+ do_check_eq(s.constructor.length, literal.length + 1);
+ s = t.array(50)(literal);
+ do_check_eq(s.readString(), literal);
+ do_check_throws(function() { t.array(3)(literal); }, TypeError);
+
+ do_check_throws(function() { t.ptr(literal); }, TypeError);
+ let p = t.ptr(s);
+ do_check_eq(p.readString(), literal);
+
+ // Test the function call ABI for calls involving the type,
+ // and check the alignment of the type against what C says.
+ run_basic_abi_tests(library, t, name, toprimitive,
+ 109,
+ [ 0, limits[0], limits[1] ],
+ [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 2 ],
+ [ limits[0], 1, limits[0] + 1 ],
+ [ limits[1], 1, limits[0] ] ],
+ [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0 ],
+ [ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 18 ] ]);
+}
+
+function run_char16_tests(library, t, name, limits) {
+ run_basic_class_tests(t);
+
+ do_check_eq(t.name, name);
+ do_check_eq(t.size, 2);
+
+ do_check_eq(t.toString(), "type " + name);
+ do_check_eq(t.toSource(), "ctypes." + name);
+ do_check_true(t.ptr === ctypes.PointerType(t));
+ do_check_eq(t.array().name, name + "[]");
+ do_check_eq(t.array(5).name, name + "[5]");
+
+ function toprimitive(a) { return a.charCodeAt(0); }
+
+ let d = t();
+ do_check_eq(d.value.length, 1);
+ do_check_eq(toprimitive(d.value), 0);
+ d.value = 5;
+ do_check_eq(d.value.length, 1);
+ do_check_eq(toprimitive(d.value), 5);
+ d = t(10);
+ do_check_eq(toprimitive(d.value), 10);
+ do_check_throws(function() { d.value = -10; }, TypeError);
+ d = new t(20);
+ do_check_eq(toprimitive(d.value), 20);
+
+ d.value = ctypes.Int64(5);
+ do_check_eq(d.value.charCodeAt(0), 5);
+ do_check_throws(function() { d.value = ctypes.Int64(-10); }, TypeError);
+ d.value = ctypes.UInt64(5);
+ do_check_eq(d.value.charCodeAt(0), 5);
+
+ d.value = limits[0];
+ do_check_eq(toprimitive(d.value), limits[0]);
+ d.value = limits[1];
+ do_check_eq(toprimitive(d.value), limits[1]);
+ d.value = 0;
+ do_check_eq(toprimitive(d.value), 0);
+ d.value = -0;
+ do_check_eq(1/toprimitive(d.value), 1/0);
+ d.value = false;
+ do_check_eq(toprimitive(d.value), 0);
+ d.value = true;
+ do_check_eq(toprimitive(d.value), 1);
+
+ d.value = "\0";
+ do_check_eq(toprimitive(d.value), 0);
+ d.value = "a";
+ do_check_eq(d.value, "a");
+
+ // don't convert anything else
+ let vals = [limits[0] - 1, limits[1] + 1, Infinity, -Infinity, NaN, 0.1,
+ null, undefined, "", "aa", {}, [], new Number(16),
+ {toString: function () { return 7; }},
+ {valueOf: function () { return 7; }}];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function () { d.value = vals[i]; }, TypeError);
+
+ do_check_true(d.address().constructor === t.ptr);
+ do_check_eq(d.address().contents, "a");
+ do_check_eq(d.toSource(), "ctypes." + name + "(\"" + d.value + "\")");
+ do_check_eq(d.toSource(), d.toString());
+
+ // Test string autoconversion (and lack thereof).
+ let literal = "autoconverted";
+ let s = t.array()(literal);
+ do_check_eq(s.readString(), literal);
+ do_check_eq(s.constructor.length, literal.length + 1);
+ s = t.array(50)(literal);
+ do_check_eq(s.readString(), literal);
+ do_check_throws(function() { t.array(3)(literal); }, TypeError);
+
+ do_check_throws(function() { t.ptr(literal); }, TypeError);
+ let p = t.ptr(s);
+ do_check_eq(p.readString(), literal);
+
+ // Test the function call ABI for calls involving the type,
+ // and check the alignment of the type against what C says.
+ run_basic_abi_tests(library, t, name, toprimitive,
+ 109,
+ [ 0, limits[0], limits[1] ],
+ [ [ 0, 0, 0 ], [ 0, 1, 1 ], [ 1, 0, 1 ], [ 1, 1, 2 ],
+ [ limits[0], 1, limits[0] + 1 ],
+ [ limits[1], 1, limits[0] ] ],
+ [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0 ],
+ [ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 18 ] ]);
+}
+
+// Test the class and prototype hierarchy for a given type constructor 'c'.
+function run_type_ctor_class_tests(c, t, t2, props=[], fns=[], instanceProps=[], instanceFns=[], specialProps=[])
+{
+ do_check_true(c.prototype.__proto__ === ctypes.CType.prototype);
+ do_check_true(c.prototype instanceof ctypes.CType);
+ do_check_true(c.prototype.constructor === c);
+
+ // Check that 'c.prototype' has the correct properties and functions.
+ for (let p of props)
+ do_check_true(c.prototype.hasOwnProperty(p));
+ for (let f of fns)
+ do_check_true(c.prototype.hasOwnProperty(f));
+
+ // Check that the shared properties and functions on 'c.prototype' throw.
+ for (let p of props)
+ do_check_throws(function() { c.prototype[p]; }, TypeError);
+ for (let f of fns)
+ do_check_throws(function() { c.prototype[f](); }, TypeError);
+
+ do_check_true(t.__proto__ === c.prototype);
+ do_check_true(t instanceof c);
+
+ // 't.prototype.__proto__' is the common ancestor of all types constructed
+ // from 'c'; while not available from 'c' directly, it should be identically
+ // equal to 't2.prototype.__proto__' where 't2' is a different CType
+ // constructed from 'c'.
+ do_check_true(t.prototype.__proto__ === t2.prototype.__proto__);
+ if (t instanceof ctypes.FunctionType)
+ do_check_true(t.prototype.__proto__.__proto__ === ctypes.PointerType.prototype.prototype);
+ else
+ do_check_true(t.prototype.__proto__.__proto__ === ctypes.CData.prototype);
+ do_check_true(t.prototype instanceof ctypes.CData);
+ do_check_true(t.prototype.constructor === t);
+
+ // Check that 't.prototype.__proto__' has the correct properties and
+ // functions.
+ for (let p of instanceProps)
+ do_check_true(t.prototype.__proto__.hasOwnProperty(p));
+ for (let f of instanceFns)
+ do_check_true(t.prototype.__proto__.hasOwnProperty(f));
+
+ // Check that the shared properties and functions on 't.prototype.__proto__'
+ // (and thus also 't.prototype') throw.
+ for (let p of instanceProps) {
+ do_check_throws(function() { t.prototype.__proto__[p]; }, TypeError);
+ do_check_throws(function() { t.prototype[p]; }, TypeError);
+ }
+ for (let f of instanceFns) {
+ do_check_throws(function() { t.prototype.__proto__[f]() }, TypeError);
+ do_check_throws(function() { t.prototype[f]() }, TypeError);
+ }
+
+ // Check that 't.prototype' has the correct special properties.
+ for (let p of specialProps)
+ do_check_true(t.prototype.hasOwnProperty(p));
+
+ // Check that the shared special properties on 't.prototype' throw.
+ for (let p of specialProps)
+ do_check_throws(function() { t.prototype[p]; }, TypeError);
+
+ // Make sure we can access 'prototype' on a CTypeProto.
+ if (t instanceof ctypes.FunctionType)
+ do_check_true(Object.getPrototypeOf(c.prototype.prototype) === ctypes.PointerType.prototype.prototype);
+ else
+ do_check_true(Object.getPrototypeOf(c.prototype.prototype) === ctypes.CType.prototype.prototype);
+
+ // Test that an instance 'd' of 't' is a CData.
+ if (t.__proto__ != ctypes.FunctionType.prototype) {
+ let d = t();
+ do_check_true(d.__proto__ === t.prototype);
+ do_check_true(d instanceof t);
+ do_check_true(d.constructor === t);
+ }
+}
+
+function run_StructType_tests() {
+ run_type_ctor_class_tests(ctypes.StructType,
+ ctypes.StructType("s", [{"a": ctypes.int32_t}, {"b": ctypes.int64_t}]),
+ ctypes.StructType("t", [{"c": ctypes.int32_t}, {"d": ctypes.int64_t}]),
+ [ "fields" ], [ "define" ], [], [ "addressOfField" ], [ "a", "b" ]);
+
+ do_check_throws(function() { ctypes.StructType(); }, TypeError);
+ do_check_throws(function() { ctypes.StructType("a", [], 5); }, TypeError);
+ do_check_throws(function() { ctypes.StructType(null, []); }, TypeError);
+ do_check_throws(function() { ctypes.StructType("a", null); }, TypeError);
+
+ // Check that malformed descriptors are an error.
+ do_check_throws(function() {
+ ctypes.StructType("a", [{"x":ctypes.int32_t}, {"x":ctypes.int8_t}]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("a", [5]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("a", [{}]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("a", [{5:ctypes.int32_t}]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("a", [{"5":ctypes.int32_t}]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("a", [{"x":5}]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("a", [{"x":ctypes.int32_t()}]);
+ }, TypeError);
+
+ // Check that opaque structs work.
+ let opaque_t = ctypes.StructType("a");
+ do_check_eq(opaque_t.name, "a");
+ do_check_eq(opaque_t.toString(), "type a");
+ do_check_eq(opaque_t.toSource(), 'ctypes.StructType("a")');
+ do_check_true(opaque_t.prototype === undefined);
+ do_check_true(opaque_t.fields === undefined);
+ do_check_true(opaque_t.size === undefined);
+ do_check_throws(function() { opaque_t(); }, Error);
+ let opaqueptr_t = opaque_t.ptr;
+ do_check_true(opaqueptr_t.targetType === opaque_t);
+ do_check_eq(opaqueptr_t.name, "a*");
+ do_check_eq(opaqueptr_t.toString(), "type a*");
+ do_check_eq(opaqueptr_t.toSource(), 'ctypes.StructType("a").ptr');
+
+ // Check that type checking works with opaque structs.
+ let opaqueptr = opaqueptr_t();
+ opaqueptr.value = opaqueptr_t(1);
+ do_check_eq(ptrValue(opaqueptr), 1);
+ do_check_throws(function() {
+ opaqueptr.value = ctypes.StructType("a").ptr();
+ }, TypeError);
+
+ // Check that 'define' works.
+ do_check_throws(function() { opaque_t.define(); }, TypeError);
+ do_check_throws(function() { opaque_t.define([], 0); }, TypeError);
+ do_check_throws(function() { opaque_t.define([{}]); }, TypeError);
+ do_check_throws(function() { opaque_t.define([{ a: 0 }]); }, TypeError);
+ do_check_throws(function() {
+ opaque_t.define([{ a: ctypes.int32_t, b: ctypes.int64_t }]);
+ }, TypeError);
+ do_check_throws(function() {
+ opaque_t.define([{ a: ctypes.int32_t }, { b: 0 }]);
+ }, TypeError);
+ do_check_false(opaque_t.hasOwnProperty("prototype"));
+
+ // Check that circular references work with opaque structs...
+ // but not crazy ones.
+ do_check_throws(function() { opaque_t.define([{ b: opaque_t }]); }, TypeError);
+ let circular_t = ctypes.StructType("circular", [{ a: opaqueptr_t }]);
+ opaque_t.define([{ b: circular_t }]);
+ let opaque = opaque_t();
+ let circular = circular_t(opaque.address());
+ opaque.b = circular;
+ do_check_eq(circular.a.toSource(), opaque.address().toSource());
+ do_check_eq(opaque.b.toSource(), circular.toSource());
+
+ // Check that attempting to redefine a struct fails and if attempted, the
+ // original definition is preserved.
+ do_check_throws(function() {
+ opaque_t.define([{ c: ctypes.int32_t.array(8) }]);
+ }, Error);
+ do_check_eq(opaque_t.size, circular_t.size);
+ do_check_true(opaque_t.prototype.hasOwnProperty("b"));
+ do_check_false(opaque_t.prototype.hasOwnProperty("c"));
+
+ // StructType size, alignment, and offset calculations have already been
+ // checked for each basic type. We do not need to check them again.
+ let name = "g_t";
+ let g_t = ctypes.StructType(name, [{ a: ctypes.int32_t }, { b: ctypes.double }]);
+ do_check_eq(g_t.name, name);
+
+ do_check_eq(g_t.toString(), "type " + name);
+ do_check_eq(g_t.toSource(),
+ "ctypes.StructType(\"g_t\", [{ \"a\": ctypes.int32_t }, { \"b\": ctypes.double }])");
+ do_check_true(g_t.ptr === ctypes.PointerType(g_t));
+ do_check_eq(g_t.array().name, name + "[]");
+ do_check_eq(g_t.array(5).name, name + "[5]");
+
+ let s_t = new ctypes.StructType("s_t", [{ a: ctypes.int32_t }, { b: g_t }, { c: ctypes.int8_t }]);
+
+ let fields = [{ a: ctypes.int32_t }, { b: ctypes.int8_t }, { c: g_t }, { d: ctypes.int8_t }];
+ let t_t = new ctypes.StructType("t_t", fields);
+ do_check_eq(t_t.fields.length, 4);
+ do_check_true(t_t.fields[0].a === ctypes.int32_t);
+ do_check_true(t_t.fields[1].b === ctypes.int8_t);
+ do_check_true(t_t.fields[2].c === g_t);
+ do_check_true(t_t.fields[3].d === ctypes.int8_t);
+/* disabled temporarily per bug 598225.
+ do_check_throws(function() { t_t.fields.z = 0; }, Error);
+ do_check_throws(function() { t_t.fields[4] = 0; }, Error);
+ do_check_throws(function() { t_t.fields[4].a = 0; }, Error);
+ do_check_throws(function() { t_t.fields[4].e = 0; }, Error);
+*/
+
+ // Check that struct size bounds work, and that large, but not illegal, sizes
+ // are OK.
+ if (ctypes.size_t.size == 4) {
+ // Test 1: overflow struct size + field padding + field size.
+ let large_t = ctypes.StructType("large_t",
+ [{"a": ctypes.int8_t.array(0xffffffff)}]);
+ do_check_eq(large_t.size, 0xffffffff);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t}, {"b": ctypes.int8_t}]);
+ }, RangeError);
+
+ // Test 2: overflow struct size + struct tail padding.
+ // To do this, we use a struct with maximum size and alignment 2.
+ large_t = ctypes.StructType("large_t",
+ [{"a": ctypes.int16_t.array(0xfffffffe / 2)}]);
+ do_check_eq(large_t.size, 0xfffffffe);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t}, {"b": ctypes.int8_t}]);
+ }, RangeError);
+
+ } else {
+ // Test 1: overflow struct size when converting from size_t to jsdouble.
+ let large_t = ctypes.StructType("large_t",
+ [{"a": ctypes.int8_t.array(0xfffffffffffff800)}]);
+ do_check_eq(large_t.size, 0xfffffffffffff800);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t}, {"b": ctypes.int8_t}]);
+ }, RangeError);
+ let small_t = ctypes.int8_t.array(0x400);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t}, {"b": small_t}]);
+ }, RangeError);
+
+ large_t = ctypes.StructType("large_t",
+ [{"a": ctypes.int8_t.array(0x1fffffffffffff)}]);
+ do_check_eq(large_t.size, 0x1fffffffffffff);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t.array(2)}, {"b": ctypes.int8_t}]);
+ }, RangeError);
+
+ // Test 2: overflow struct size + field padding + field size.
+ large_t = ctypes.int8_t.array(0xfffffffffffff800);
+ small_t = ctypes.int8_t.array(0x800);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t}, {"b": small_t}]);
+ }, RangeError);
+
+ // Test 3: overflow struct size + struct tail padding.
+ // To do this, we use a struct with maximum size and alignment 2.
+ large_t = ctypes.StructType("large_t",
+ [{"a": ctypes.int16_t.array(0xfffffffffffff000 / 2)}]);
+ do_check_eq(large_t.size, 0xfffffffffffff000);
+ small_t = ctypes.int8_t.array(0xfff);
+ do_check_throws(function() {
+ ctypes.StructType("large_t", [{"a": large_t}, {"b": small_t}]);
+ }, RangeError);
+ }
+
+ let g = g_t();
+ do_check_eq(g.a, 0);
+ do_check_eq(g.b, 0);
+ g = new g_t(1, 2);
+ do_check_eq(g.a, 1);
+ do_check_eq(g.b, 2);
+ do_check_throws(function() { g_t(1); }, TypeError);
+ do_check_throws(function() { g_t(1, 2, 3); }, TypeError);
+
+ for (let field in g)
+ do_check_true(field == "a" || field == "b");
+
+ let g_a = g.address();
+ do_check_true(g_a.constructor === g_t.ptr);
+ do_check_eq(g_a.contents.a, g.a);
+
+ let s = new s_t(3, g, 10);
+ do_check_eq(s.a, 3);
+ s.a = 4;
+ do_check_eq(s.a, 4);
+ do_check_eq(s.b.a, 1);
+ do_check_eq(s.b.b, 2);
+ do_check_eq(s.c, 10);
+ let g2 = s.b;
+ do_check_eq(g2.a, 1);
+ g2.a = 7;
+ do_check_eq(g2.a, 7);
+ do_check_eq(s.b.a, 7);
+
+ g_a = s.addressOfField("b");
+ do_check_true(g_a.constructor === g_t.ptr);
+ do_check_eq(g_a.contents.a, s.b.a);
+ do_check_throws(function() { s.addressOfField(); }, TypeError);
+ do_check_throws(function() { s.addressOfField("d"); }, TypeError);
+ do_check_throws(function() { s.addressOfField("a", 2); }, TypeError);
+
+ do_check_eq(s.toSource(), "s_t(4, {\"a\": 7, \"b\": 2}, 10)");
+ do_check_eq(s.toSource(), s.toString());
+ eval("var s2 = " + s.toSource());
+ do_check_true(s2.constructor === s_t);
+ do_check_eq(s.b.b, s2.b.b);
+
+ // Test that structs can be set from an object using 'value'.
+ do_check_throws(function() { s.value; }, TypeError);
+ let s_init = { "a": 2, "b": { "a": 9, "b": 5 }, "c": 13 };
+ s.value = s_init;
+ do_check_eq(s.b.a, 9);
+ do_check_eq(s.c, 13);
+ do_check_throws(function() { s.value = 5; }, TypeError);
+ do_check_throws(function() { s.value = ctypes.int32_t(); }, TypeError);
+ do_check_throws(function() { s.value = {}; }, TypeError);
+ do_check_throws(function() { s.value = { "a": 2 }; }, TypeError);
+ do_check_throws(function() { s.value = { "a": 2, "b": 5, "c": 10 }; }, TypeError);
+ do_check_throws(function() {
+ s.value = { "5": 2, "b": { "a": 9, "b": 5 }, "c": 13 };
+ }, TypeError);
+ do_check_throws(function() {
+ s.value = { "a": 2, "b": { "a": 9, "b": 5 }, "c": 13, "d": 17 };
+ }, TypeError);
+ do_check_throws(function() {
+ s.value = { "a": 2, "b": { "a": 9, "b": 5, "e": 9 }, "c": 13 };
+ }, TypeError);
+
+ // Test that structs can be constructed similarly through ExplicitConvert,
+ // and that the single-field case is disambiguated correctly.
+ s = s_t(s_init);
+ do_check_eq(s.b.a, 9);
+ do_check_eq(s.c, 13);
+ let v_t = ctypes.StructType("v_t", [{ "x": ctypes.int32_t }]);
+ let v = v_t({ "x": 5 });
+ do_check_eq(v.x, 5);
+ v = v_t(8);
+ do_check_eq(v.x, 8);
+ let w_t = ctypes.StructType("w_t", [{ "y": v_t }]);
+ do_check_throws(function() { w_t(9); }, TypeError);
+ let w = w_t({ "x": 3 });
+ do_check_eq(w.y.x, 3);
+ w = w_t({ "y": { "x": 19 } });
+ do_check_eq(w.y.x, 19);
+ let u_t = ctypes.StructType("u_t", [{ "z": ctypes.ArrayType(ctypes.int32_t, 3) }]);
+ let u = u_t([1, 2, 3]);
+ do_check_eq(u.z[1], 2);
+ u = u_t({ "z": [4, 5, 6] });
+ do_check_eq(u.z[1], 5);
+
+ // Check that the empty struct has size 1.
+ let z_t = ctypes.StructType("z_t", []);
+ do_check_eq(z_t.size, 1);
+ do_check_eq(z_t.fields.length, 0);
+
+ // Check that structs containing arrays of undefined or zero length
+ // are illegal, but arrays of defined length work.
+ do_check_throws(function() {
+ ctypes.StructType("z_t", [{ a: ctypes.int32_t.array() }]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.StructType("z_t", [{ a: ctypes.int32_t.array(0) }]);
+ }, TypeError);
+ z_t = ctypes.StructType("z_t", [{ a: ctypes.int32_t.array(6) }]);
+ do_check_eq(z_t.size, ctypes.int32_t.size * 6);
+ let z = z_t([1, 2, 3, 4, 5, 6]);
+ do_check_eq(z.a[3], 4);
+}
+
+function ptrValue(p) {
+ return ctypes.cast(p, ctypes.uintptr_t).value.toString();
+}
+
+function run_PointerType_tests() {
+ run_type_ctor_class_tests(ctypes.PointerType,
+ ctypes.PointerType(ctypes.int32_t), ctypes.PointerType(ctypes.int64_t),
+ [ "targetType" ], [], [ "contents" ], [ "isNull", "increment", "decrement" ], []);
+
+ do_check_throws(function() { ctypes.PointerType(); }, TypeError);
+ do_check_throws(function() { ctypes.PointerType(ctypes.int32_t, 5); }, TypeError);
+ do_check_throws(function() { ctypes.PointerType(null); }, TypeError);
+ do_check_throws(function() { ctypes.PointerType(ctypes.int32_t()); }, TypeError);
+ do_check_throws(function() { ctypes.PointerType("void"); }, TypeError);
+
+ let name = "g_t";
+ let g_t = ctypes.StructType(name, [{ a: ctypes.int32_t }, { b: ctypes.double }]);
+ let g = g_t(1, 2);
+
+ let p_t = ctypes.PointerType(g_t);
+ do_check_eq(p_t.name, name + "*");
+ do_check_eq(p_t.size, ctypes.uintptr_t.size);
+ do_check_true(p_t.targetType === g_t);
+ do_check_true(p_t === g_t.ptr);
+
+ do_check_eq(p_t.toString(), "type " + name + "*");
+ do_check_eq(p_t.toSource(),
+ "ctypes.StructType(\"g_t\", [{ \"a\": ctypes.int32_t }, { \"b\": ctypes.double }]).ptr");
+ do_check_true(p_t.ptr === ctypes.PointerType(p_t));
+ do_check_eq(p_t.array().name, name + "*[]");
+ do_check_eq(p_t.array(5).name, name + "*[5]");
+
+ // Test ExplicitConvert.
+ let p = p_t();
+ do_check_throws(function() { p.value; }, TypeError);
+ do_check_eq(ptrValue(p), 0);
+ do_check_throws(function() { p.contents; }, TypeError);
+ do_check_throws(function() { p.contents = g; }, TypeError);
+ p = p_t(5);
+ do_check_eq(ptrValue(p), 5);
+ p = p_t(ctypes.UInt64(10));
+ do_check_eq(ptrValue(p), 10);
+
+ // Test ImplicitConvert.
+ p.value = null;
+ do_check_eq(ptrValue(p), 0);
+ do_check_throws(function() { p.value = 5; }, TypeError);
+
+ // Test opaque pointers.
+ let f_t = ctypes.StructType("FILE").ptr;
+ do_check_eq(f_t.name, "FILE*");
+ do_check_eq(f_t.toSource(), 'ctypes.StructType("FILE").ptr');
+ let f = new f_t();
+ do_check_throws(function() { f.contents; }, TypeError);
+ do_check_throws(function() { f.contents = 0; }, TypeError);
+ f = f_t(5);
+ do_check_throws(function() { f.contents = 0; }, TypeError);
+ do_check_eq(f.toSource(), 'FILE.ptr(ctypes.UInt64("0x5"))');
+
+ do_check_throws(function() { f_t(p); }, TypeError);
+ do_check_throws(function() { f.value = p; }, TypeError);
+ do_check_throws(function() { p.value = f; }, TypeError);
+
+ // Test void pointers.
+ let v_t = ctypes.PointerType(ctypes.void_t);
+ do_check_true(v_t === ctypes.voidptr_t);
+ let v = v_t(p);
+ do_check_eq(ptrValue(v), ptrValue(p));
+
+ // Test 'contents'.
+ let int32_t = ctypes.int32_t(9);
+ p = int32_t.address();
+ do_check_eq(p.contents, int32_t.value);
+ p.contents = ctypes.int32_t(12);
+ do_check_eq(int32_t.value, 12);
+
+ // Test 'isNull'.
+ let n = f_t(0);
+ do_check_true(n.isNull() === true);
+ n = p.address();
+ do_check_true(n.isNull() === false);
+
+ // Test 'increment'/'decrement'.
+ g_t = ctypes.StructType("g_t", [{ a: ctypes.int32_t }, { b: ctypes.double }]);
+ let a_t = ctypes.ArrayType(g_t, 2);
+ let a = new a_t();
+ a[0] = g_t(1, 2);
+ a[1] = g_t(2, 4);
+ let a_p = a.addressOfElement(0).increment();
+ do_check_eq(a_p.contents.a, 2);
+ do_check_eq(a_p.contents.b, 4);
+ a_p = a_p.decrement();
+ do_check_eq(a_p.contents.a, 1);
+ do_check_eq(a_p.contents.b, 2);
+
+ // Check that pointers to arrays of undefined or zero length are legal,
+ // but that the former cannot be dereferenced.
+ let z_t = ctypes.int32_t.array().ptr;
+ do_check_eq(ptrValue(z_t()), 0);
+ do_check_throws(function() { z_t().contents }, TypeError);
+ z_t = ctypes.int32_t.array(0).ptr;
+ do_check_eq(ptrValue(z_t()), 0);
+ let z = ctypes.int32_t.array(0)().address();
+ do_check_eq(z.contents.length, 0);
+
+ // TODO: Somehow, somewhere we should check that:
+ //
+ // (a) ArrayBuffer and TypedArray can be passed by pointer to a C function
+ // (b) SharedArrayBuffer and TypedArray on SAB can NOT be passed in that
+ // way (at least not at the moment).
+
+ // Set up conversion tests on AB, SAB, TA
+ let c_arraybuffer = new ArrayBuffer(256);
+ let typed_array_samples =
+ [
+ [new Int8Array(c_arraybuffer), ctypes.int8_t],
+ [new Uint8Array(c_arraybuffer), ctypes.uint8_t],
+ [new Int16Array(c_arraybuffer), ctypes.int16_t],
+ [new Uint16Array(c_arraybuffer), ctypes.uint16_t],
+ [new Int32Array(c_arraybuffer), ctypes.int32_t],
+ [new Uint32Array(c_arraybuffer), ctypes.uint32_t],
+ [new Float32Array(c_arraybuffer), ctypes.float32_t],
+ [new Float64Array(c_arraybuffer), ctypes.float64_t]
+ ];
+
+ if (typeof SharedArrayBuffer !== "undefined") {
+ let c_shared_arraybuffer = new SharedArrayBuffer(256);
+ typed_array_samples.push([new Int8Array(c_shared_arraybuffer), ctypes.int8_t],
+ [new Uint8Array(c_shared_arraybuffer), ctypes.uint8_t],
+ [new Int16Array(c_shared_arraybuffer), ctypes.int16_t],
+ [new Uint16Array(c_shared_arraybuffer), ctypes.uint16_t],
+ [new Int32Array(c_shared_arraybuffer), ctypes.int32_t],
+ [new Uint32Array(c_shared_arraybuffer), ctypes.uint32_t],
+ [new Float32Array(c_shared_arraybuffer), ctypes.float32_t],
+ [new Float64Array(c_shared_arraybuffer), ctypes.float64_t])
+ }
+
+ // Check that you can convert (Shared)ArrayBuffer or typed array to a C array
+ for (let i = 0; i < typed_array_samples.length; ++i) {
+ for (let j = 0; j < typed_array_samples.length; ++j) {
+ let view = typed_array_samples[i][0];
+ let item_type = typed_array_samples[j][1];
+ let number_of_items = c_arraybuffer.byteLength / item_type.size;
+ let array_type = item_type.array(number_of_items);
+
+ // Int8Array on unshared memory is interconvertible with Int8Array on
+ // shared memory, etc.
+ if (i % 8 != j % 8) {
+ do_print("Checking that typed array " + (view.constructor.name) +
+ " can NOT be converted to " + item_type + " array");
+ do_check_throws(function() { array_type(view); }, TypeError);
+ } else {
+ do_print("Checking that typed array " + (view.constructor.name) +
+ " can be converted to " + item_type + " array");
+
+ // Convert ArrayBuffer to array of the right size and check contents
+ c_array = array_type(c_arraybuffer);
+ for (let k = 0; k < number_of_items; ++k) {
+ do_check_eq(c_array[k], view[k]);
+ }
+
+ // Convert typed array to array of the right size and check contents
+ c_array = array_type(view);
+ for (let k = 0; k < number_of_items; ++k) {
+ do_check_eq(c_array[k], view[k]);
+ }
+
+ // Convert typed array to array of wrong size, ensure that it fails
+ let array_type_too_large = item_type.array(number_of_items + 1);
+ let array_type_too_small = item_type.array(number_of_items - 1);
+
+ do_check_throws(function() { array_type_too_large(c_arraybuffer); }, TypeError);
+ do_check_throws(function() { array_type_too_small(c_arraybuffer); }, TypeError);
+ do_check_throws(function() { array_type_too_large(view); }, TypeError);
+ do_check_throws(function() { array_type_too_small(view); }, TypeError);
+
+ // Convert subarray of typed array to array of right size and check contents
+ c_array = array_type_too_small(view.subarray(1));
+ for (let k = 1; k < number_of_items; ++k) {
+ do_check_eq(c_array[k - 1], view[k]);
+ }
+ }
+ }
+ }
+
+ // Check that you can't use a (Shared)ArrayBuffer or a typed array as a pointer
+ for (let i = 0; i < typed_array_samples.length; ++i) {
+ for (let j = 0; j < typed_array_samples.length; ++j) {
+ let view = typed_array_samples[i][0];
+ let item_type = typed_array_samples[j][1];
+
+ do_print("Checking that typed array " + (view.constructor.name) +
+ " can NOT be converted to " + item_type + " pointer/array");
+ do_check_throws(function() { item_type.ptr(c_arraybuffer); }, TypeError);
+ do_check_throws(function() { item_type.ptr(view); }, TypeError);
+ do_check_throws(function() { ctypes.voidptr_t(c_arraybuffer); }, TypeError);
+ do_check_throws(function() { ctypes.voidptr_t(view); }, TypeError);
+ }
+ }
+}
+
+function run_FunctionType_tests() {
+ run_type_ctor_class_tests(ctypes.FunctionType,
+ ctypes.FunctionType(ctypes.default_abi, ctypes.void_t),
+ ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [ ctypes.int32_t ]),
+ [ "abi", "returnType", "argTypes", "isVariadic" ],
+ undefined, undefined, undefined, undefined);
+
+ do_check_throws(function() { ctypes.FunctionType(); }, TypeError);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [ ctypes.void_t ]);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [ ctypes.void_t ], 5);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, ctypes.void_t);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, null);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.int32_t());
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.void_t, ctypes.void_t);
+ }, Error);
+
+ let g_t = ctypes.StructType("g_t", [{ a: ctypes.int32_t }, { b: ctypes.double }]);
+
+ let f_t = ctypes.FunctionType(ctypes.default_abi, g_t);
+ let name = "g_t()";
+ do_check_eq(f_t.name, name);
+ do_check_eq(f_t.size, undefined);
+ do_check_true(f_t.abi === ctypes.default_abi);
+ do_check_true(f_t.returnType === g_t);
+ do_check_true(f_t.argTypes.length == 0);
+
+ do_check_eq(f_t.toString(), "type " + name);
+ do_check_eq(f_t.toSource(),
+ "ctypes.FunctionType(ctypes.default_abi, g_t)");
+
+ let fp_t = f_t.ptr;
+ name = "g_t(*)()";
+ do_check_eq(fp_t.name, name);
+ do_check_eq(fp_t.size, ctypes.uintptr_t.size);
+
+ do_check_eq(fp_t.toString(), "type " + name);
+ do_check_eq(fp_t.toSource(),
+ "ctypes.FunctionType(ctypes.default_abi, g_t).ptr");
+
+ // Check that constructing a FunctionType CData directly throws.
+ do_check_throws(function() { f_t(); }, TypeError);
+
+ // Test ExplicitConvert.
+ let f = fp_t();
+ do_check_throws(function() { f.value; }, TypeError);
+ do_check_eq(ptrValue(f), 0);
+ f = fp_t(5);
+ do_check_eq(ptrValue(f), 5);
+ f = fp_t(ctypes.UInt64(10));
+ do_check_eq(ptrValue(f), 10);
+
+ // Test ImplicitConvert.
+ f.value = null;
+ do_check_eq(ptrValue(f), 0);
+ do_check_throws(function() { f.value = 5; }, TypeError);
+ do_check_eq(f.toSource(),
+ 'ctypes.FunctionType(ctypes.default_abi, g_t).ptr(ctypes.UInt64("0x0"))');
+
+ // Test ImplicitConvert from a function pointer of different type.
+ let f2_t = ctypes.FunctionType(ctypes.default_abi, g_t, [ ctypes.int32_t ]);
+ let f2 = f2_t.ptr();
+ do_check_throws(function() { f.value = f2; }, TypeError);
+ do_check_throws(function() { f2.value = f; }, TypeError);
+
+ // Test that converting to a voidptr_t works.
+ let v = ctypes.voidptr_t(f2);
+ do_check_eq(v.toSource(), 'ctypes.voidptr_t(ctypes.UInt64("0x0"))');
+
+ // Test some more complex names.
+ do_check_eq(fp_t.array().name, "g_t(*[])()");
+ do_check_eq(fp_t.array().ptr.name, "g_t(*(*)[])()");
+
+ let f3_t = ctypes.FunctionType(ctypes.default_abi,
+ ctypes.char.ptr.array().ptr).ptr.ptr.array(8).array();
+ do_check_eq(f3_t.name, "char*(*(**[][8])())[]");
+
+ if ("winLastError" in ctypes) {
+ f3_t = ctypes.FunctionType(ctypes.stdcall_abi,
+ ctypes.char.ptr.array().ptr).ptr.ptr.array(8).array();
+ do_check_eq(f3_t.name, "char*(*(__stdcall**[][8])())[]");
+ f3_t = ctypes.FunctionType(ctypes.winapi_abi,
+ ctypes.char.ptr.array().ptr).ptr.ptr.array(8).array();
+ do_check_eq(f3_t.name, "char*(*(WINAPI**[][8])())[]");
+ }
+
+ let f4_t = ctypes.FunctionType(ctypes.default_abi,
+ ctypes.char.ptr.array().ptr, [ ctypes.int32_t, fp_t ]);
+ do_check_true(f4_t.argTypes.length == 2);
+ do_check_true(f4_t.argTypes[0] === ctypes.int32_t);
+ do_check_true(f4_t.argTypes[1] === fp_t);
+/* disabled temporarily per bug 598225.
+ do_check_throws(function() { f4_t.argTypes.z = 0; }, Error);
+ do_check_throws(function() { f4_t.argTypes[0] = 0; }, Error);
+*/
+
+ let t4_t = f4_t.ptr.ptr.array(8).array();
+ do_check_eq(t4_t.name, "char*(*(**[][8])(int32_t, g_t(*)()))[]");
+
+ // Not available in a Worker
+ if ("@mozilla.org/systemprincipal;1" in Components.classes) {
+ var sp = Components.classes["@mozilla.org/systemprincipal;1"].
+ createInstance(Components.interfaces.nsIPrincipal);
+ var s = new Components.utils.Sandbox(sp);
+ s.ctypes = ctypes;
+ s.do_check_eq = do_check_eq;
+ s.do_check_true = do_check_true;
+ Components.utils.evalInSandbox("var f5_t = ctypes.FunctionType(ctypes.default_abi, ctypes.int, [ctypes.int]);", s);
+ Components.utils.evalInSandbox("do_check_eq(f5_t.toSource(), 'ctypes.FunctionType(ctypes.default_abi, ctypes.int, [ctypes.int])');", s);
+ Components.utils.evalInSandbox("do_check_eq(f5_t.name, 'int(int)');", s);
+ Components.utils.evalInSandbox("function f5(aArg) { return 5; };", s);
+ Components.utils.evalInSandbox("var f = f5_t.ptr(f5);", s);
+ Components.utils.evalInSandbox("do_check_true(f(6) == 5);", s);
+ }
+}
+
+function run_ArrayType_tests() {
+ run_type_ctor_class_tests(ctypes.ArrayType,
+ ctypes.ArrayType(ctypes.int32_t, 10), ctypes.ArrayType(ctypes.int64_t),
+ [ "elementType", "length" ], [], [ "length" ], [ "addressOfElement" ]);
+
+ do_check_throws(function() { ctypes.ArrayType(); }, TypeError);
+ do_check_throws(function() { ctypes.ArrayType(null); }, TypeError);
+ do_check_throws(function() { ctypes.ArrayType(ctypes.int32_t, 1, 5); }, TypeError);
+ do_check_throws(function() { ctypes.ArrayType(ctypes.int32_t, -1); }, TypeError);
+
+ let name = "g_t";
+ let g_t = ctypes.StructType(name, [{ a: ctypes.int32_t }, { b: ctypes.double }]);
+ let g = g_t(1, 2);
+
+ let a_t = ctypes.ArrayType(g_t, 10);
+ do_check_eq(a_t.name, name + "[10]");
+ do_check_eq(a_t.length, 10);
+ do_check_eq(a_t.size, g_t.size * 10);
+ do_check_true(a_t.elementType === g_t);
+
+ do_check_eq(a_t.toString(), "type " + name + "[10]");
+ do_check_eq(a_t.toSource(),
+ "ctypes.StructType(\"g_t\", [{ \"a\": ctypes.int32_t }, { \"b\": ctypes.double }]).array(10)");
+ do_check_eq(a_t.array().name, name + "[][10]");
+ do_check_eq(a_t.array(5).name, name + "[5][10]");
+ do_check_throws(function() { ctypes.int32_t.array().array(); }, Error);
+
+ let a = new a_t();
+ do_check_eq(a[0].a, 0);
+ do_check_eq(a[0].b, 0);
+ a[0] = g;
+ do_check_eq(a[0].a, 1);
+ do_check_eq(a[0].b, 2);
+ do_check_throws(function() { a[-1]; }, TypeError);
+ do_check_eq(a[9].a, 0);
+ do_check_throws(function() { a[10]; }, RangeError);
+
+ do_check_eq(a[ctypes.Int64(0)].a, 1);
+ do_check_eq(a[ctypes.UInt64(0)].b, 2);
+
+ let a_p = a.addressOfElement(0);
+ do_check_true(a_p.constructor.targetType === g_t);
+ do_check_true(a_p.constructor === g_t.ptr);
+ do_check_eq(a_p.contents.a, a[0].a);
+ do_check_eq(a_p.contents.b, a[0].b);
+ a_p.contents.a = 5;
+ do_check_eq(a[0].a, 5);
+
+ let a2_t = ctypes.ArrayType(g_t);
+ do_check_eq(a2_t.name, "g_t[]");
+ do_check_eq(a2_t.length, undefined);
+ do_check_eq(a2_t.size, undefined);
+ let a2 = new a2_t(5);
+ do_check_eq(a2.constructor.length, 5);
+ do_check_eq(a2.length, 5);
+ do_check_eq(a2.constructor.size, g_t.size * 5);
+ do_check_throws(function() { new a2_t(); }, TypeError);
+ do_check_throws(function() { ctypes.ArrayType(ctypes.ArrayType(g_t)); }, Error);
+ do_check_throws(function() { ctypes.ArrayType(ctypes.ArrayType(g_t), 5); }, Error);
+
+ let b_t = ctypes.int8_t.array(ctypes.UInt64(0xffff));
+ do_check_eq(b_t.length, 0xffff);
+ b_t = ctypes.int8_t.array(ctypes.Int64(0xffff));
+ do_check_eq(b_t.length, 0xffff);
+
+ // Check that array size bounds work, and that large, but not illegal, sizes
+ // are OK.
+ if (ctypes.size_t.size == 4) {
+ do_check_throws(function() {
+ ctypes.ArrayType(ctypes.int8_t, 0x100000000);
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.ArrayType(ctypes.int16_t, 0x80000000);
+ }, RangeError);
+
+ let large_t = ctypes.int8_t.array(0x80000000);
+ do_check_throws(function() { large_t.array(2); }, RangeError);
+
+ } else {
+ do_check_throws(function() {
+ ctypes.ArrayType(ctypes.int8_t, ctypes.UInt64("0xffffffffffffffff"));
+ }, TypeError);
+ do_check_throws(function() {
+ ctypes.ArrayType(ctypes.int16_t, ctypes.UInt64("0x8000000000000000"));
+ }, RangeError);
+
+ let large_t = ctypes.int8_t.array(0x8000000000000000);
+ do_check_throws(function() { large_t.array(2); }, RangeError);
+ }
+
+ // Test that arrays ImplicitConvert to pointers.
+ let b = ctypes.int32_t.array(10)();
+ let p = ctypes.int32_t.ptr();
+ p.value = b;
+ do_check_eq(ptrValue(b.addressOfElement(0)), ptrValue(p));
+ p = ctypes.voidptr_t();
+ p.value = b;
+ do_check_eq(ptrValue(b.addressOfElement(0)), ptrValue(p));
+
+ // Test that arrays can be constructed through ImplicitConvert.
+ let c_t = ctypes.int32_t.array(6);
+ let c = c_t();
+ c.value = [1, 2, 3, 4, 5, 6];
+ do_check_eq(c.toSource(), "ctypes.int32_t.array(6)([1, 2, 3, 4, 5, 6])");
+ do_check_eq(c.toSource(), c.toString());
+ eval("var c2 = " + c.toSource());
+ do_check_eq(c2.constructor.name, "int32_t[6]");
+ do_check_eq(c2.length, 6);
+ do_check_eq(c2[3], c[3]);
+
+ c.value = c;
+ do_check_eq(c[3], 4);
+ do_check_throws(function() { c.value; }, TypeError);
+ do_check_throws(function() { c.value = [1, 2, 3, 4, 5]; }, TypeError);
+ do_check_throws(function() { c.value = [1, 2, 3, 4, 5, 6, 7]; }, TypeError);
+ do_check_throws(function() { c.value = [1, 2, 7.4, 4, 5, 6]; }, TypeError);
+ do_check_throws(function() { c.value = []; }, TypeError);
+}
+
+function run_type_toString_tests() {
+ var c = ctypes;
+
+ // Figure out whether we can create functions with ctypes.stdcall_abi and ctypes.winapi_abi.
+ var haveStdCallABI;
+ try {
+ c.FunctionType(c.stdcall_abi, c.int);
+ haveStdCallABI = true;
+ } catch (x) {
+ haveStdCallABI = false;
+ }
+
+ var haveWinAPIABI;
+ try {
+ c.FunctionType(c.winapi_abi, c.int);
+ haveWinAPIABI = true;
+ } catch (x) {
+ haveWinAPIABI = false;
+ }
+
+ do_check_eq(c.char.toString(), "type char");
+ do_check_eq(c.short.toString(), "type short");
+ do_check_eq(c.int.toString(), "type int");
+ do_check_eq(c.long.toString(), "type long");
+ do_check_eq(c.long_long.toString(), "type long_long");
+ do_check_eq(c.ssize_t.toString(), "type ssize_t");
+ do_check_eq(c.int8_t.toString(), "type int8_t");
+ do_check_eq(c.int16_t.toString(), "type int16_t");
+ do_check_eq(c.int32_t.toString(), "type int32_t");
+ do_check_eq(c.int64_t.toString(), "type int64_t");
+ do_check_eq(c.intptr_t.toString(), "type intptr_t");
+
+ do_check_eq(c.unsigned_char.toString(), "type unsigned_char");
+ do_check_eq(c.unsigned_short.toString(), "type unsigned_short");
+ do_check_eq(c.unsigned_int.toString(), "type unsigned_int");
+ do_check_eq(c.unsigned_long.toString(), "type unsigned_long");
+ do_check_eq(c.unsigned_long_long.toString(), "type unsigned_long_long");
+ do_check_eq(c.size_t.toString(), "type size_t");
+ do_check_eq(c.uint8_t.toString(), "type uint8_t");
+ do_check_eq(c.uint16_t.toString(), "type uint16_t");
+ do_check_eq(c.uint32_t.toString(), "type uint32_t");
+ do_check_eq(c.uint64_t.toString(), "type uint64_t");
+ do_check_eq(c.uintptr_t.toString(), "type uintptr_t");
+
+ do_check_eq(c.float.toString(), "type float");
+ do_check_eq(c.double.toString(), "type double");
+ do_check_eq(c.bool.toString(), "type bool");
+ do_check_eq(c.void_t.toString(), "type void");
+ do_check_eq(c.voidptr_t.toString(), "type void*");
+ do_check_eq(c.char16_t.toString(), "type char16_t");
+
+ var simplestruct = c.StructType("simplestruct", [{"smitty":c.voidptr_t}]);
+ do_check_eq(simplestruct.toString(), "type simplestruct");
+
+ // One type modifier, int base type.
+ do_check_eq(c.int.ptr.toString(), "type int*");
+ do_check_eq(c.ArrayType(c.int).toString(), "type int[]");
+ do_check_eq(c.ArrayType(c.int, 4).toString(), "type int[4]");
+ do_check_eq(c.FunctionType(c.default_abi, c.int).toString(), "type int()");
+ do_check_eq(c.FunctionType(c.default_abi, c.int, [c.bool]).toString(), "type int(bool)");
+ do_check_eq(c.FunctionType(c.default_abi, c.int, [c.bool, c.short]).toString(),
+ "type int(bool, short)");
+ if (haveStdCallABI)
+ do_check_eq(c.FunctionType(c.stdcall_abi, c.int).toString(), "type int __stdcall()");
+ if (haveWinAPIABI)
+ do_check_eq(c.FunctionType(c.winapi_abi, c.int).toString(), "type int WINAPI()");
+
+ // One type modifier, struct base type.
+ do_check_eq(simplestruct.ptr.toString(), "type simplestruct*");
+ do_check_eq(c.ArrayType(simplestruct).toString(), "type simplestruct[]");
+ do_check_eq(c.ArrayType(simplestruct, 4).toString(), "type simplestruct[4]");
+ do_check_eq(c.FunctionType(c.default_abi, simplestruct).toString(), "type simplestruct()");
+
+ // Two levels of type modifiers, int base type.
+ do_check_eq(c.int.ptr.ptr.toString(), "type int**");
+ do_check_eq(c.ArrayType(c.int.ptr).toString(), "type int*[]");
+ do_check_eq(c.FunctionType(c.default_abi, c.int.ptr).toString(), "type int*()");
+
+ do_check_eq(c.ArrayType(c.int).ptr.toString(), "type int(*)[]");
+ do_check_eq(c.ArrayType(c.ArrayType(c.int, 4)).toString(), "type int[][4]");
+ // Functions can't return arrays.
+
+ do_check_eq(c.FunctionType(c.default_abi, c.int).ptr.toString(), "type int(*)()");
+ // You can't have an array of functions.
+ // Functions can't return functions.
+
+ // We don't try all the permissible three-deep combinations, but this is fun.
+ do_check_eq(c.FunctionType(c.default_abi, c.FunctionType(c.default_abi, c.int).ptr).toString(),
+ "type int(*())()");
+}
+
+function run_cast_tests() {
+ // Test casting between basic types.
+ let i = ctypes.int32_t();
+ let j = ctypes.cast(i, ctypes.int16_t);
+ do_check_eq(ptrValue(i.address()), ptrValue(j.address()));
+ do_check_eq(i.value, j.value);
+ let k = ctypes.cast(i, ctypes.uint32_t);
+ do_check_eq(ptrValue(i.address()), ptrValue(k.address()));
+ do_check_eq(i.value, k.value);
+
+ // Test casting to a type of undefined or larger size.
+ do_check_throws(function() { ctypes.cast(i, ctypes.void_t); }, TypeError);
+ do_check_throws(function() { ctypes.cast(i, ctypes.int32_t.array()); }, TypeError);
+ do_check_throws(function() { ctypes.cast(i, ctypes.int64_t); }, TypeError);
+
+ // Test casting between special types.
+ let g_t = ctypes.StructType("g_t", [{ a: ctypes.int32_t }, { b: ctypes.double }]);
+ let a_t = ctypes.ArrayType(g_t, 4);
+ let f_t = ctypes.FunctionType(ctypes.default_abi, ctypes.void_t).ptr;
+
+ let a = a_t();
+ a[0] = { a: 5, b: 7.5 };
+ let g = ctypes.cast(a, g_t);
+ do_check_eq(ptrValue(a.address()), ptrValue(g.address()));
+ do_check_eq(a[0].a, g.a);
+
+ let a2 = ctypes.cast(g, g_t.array(1));
+ do_check_eq(ptrValue(a2.address()), ptrValue(g.address()));
+ do_check_eq(a2[0].a, g.a);
+
+ let p = g.address();
+ let ip = ctypes.cast(p, ctypes.int32_t.ptr);
+ do_check_eq(ptrValue(ip), ptrValue(p));
+ do_check_eq(ptrValue(ip.address()), ptrValue(p.address()));
+ do_check_eq(ip.contents, g.a);
+
+ let f = f_t(0x5);
+ let f2 = ctypes.cast(f, ctypes.voidptr_t);
+ do_check_eq(ptrValue(f2), ptrValue(f));
+ do_check_eq(ptrValue(f2.address()), ptrValue(f.address()));
+}
+
+function run_void_tests(library) {
+ let test_void_t = library.declare("test_void_t_cdecl", ctypes.default_abi, ctypes.void_t);
+ do_check_eq(test_void_t(), undefined);
+
+ // Test that library.declare throws with void function args.
+ do_check_throws(function() {
+ library.declare("test_void_t_cdecl", ctypes.default_abi, ctypes.void_t, ctypes.void_t);
+ }, TypeError);
+
+ if ("winLastError" in ctypes) {
+ test_void_t = library.declare("test_void_t_stdcall", ctypes.stdcall_abi, ctypes.void_t);
+ do_check_eq(test_void_t(), undefined);
+
+ // Check that WINAPI symbol lookup for a regular stdcall function fails on
+ // Win32 (it's all the same on Win64 though).
+ if (ctypes.voidptr_t.size == 4) {
+ do_check_throws(function() {
+ library.declare("test_void_t_stdcall", ctypes.winapi_abi, ctypes.void_t);
+ }, Error);
+ }
+ }
+}
+
+function run_string_tests(library) {
+ let test_ansi_len = library.declare("test_ansi_len", ctypes.default_abi, ctypes.int32_t, ctypes.char.ptr);
+ do_check_eq(test_ansi_len(""), 0);
+ do_check_eq(test_ansi_len("hello world"), 11);
+
+ // don't convert anything else to a string
+ let vals = [true, 0, 1/3, undefined, {}, {toString: function () { return "bad"; }}, []];
+ for (let i = 0; i < vals.length; i++)
+ do_check_throws(function() { test_ansi_len(vals[i]); }, TypeError);
+
+ let test_wide_len = library.declare("test_wide_len", ctypes.default_abi, ctypes.int32_t, ctypes.char16_t.ptr);
+ do_check_eq(test_wide_len("hello world"), 11);
+
+ let test_ansi_ret = library.declare("test_ansi_ret", ctypes.default_abi, ctypes.char.ptr);
+ do_check_eq(test_ansi_ret().readString(), "success");
+
+ let test_wide_ret = library.declare("test_wide_ret", ctypes.default_abi, ctypes.char16_t.ptr);
+ do_check_eq(test_wide_ret().readString(), "success");
+
+ let test_ansi_echo = library.declare("test_ansi_echo", ctypes.default_abi, ctypes.char.ptr, ctypes.char.ptr);
+ // We cannot pass a string literal directly into test_ansi_echo, since the
+ // conversion to ctypes.char.ptr is only valid for the duration of the ffi
+ // call. The escaped pointer that's returned will point to freed memory.
+ let arg = ctypes.char.array()("anybody in there?");
+ do_check_eq(test_ansi_echo(arg).readString(), "anybody in there?");
+ do_check_eq(ptrValue(test_ansi_echo(null)), 0);
+}
+
+function run_readstring_tests(library) {
+ // ASCII decode test, "hello world"
+ let ascii_string = ctypes.unsigned_char.array(12)();
+ ascii_string[0] = 0x68;
+ ascii_string[1] = 0x65;
+ ascii_string[2] = 0x6C;
+ ascii_string[3] = 0x6C;
+ ascii_string[4] = 0x6F;
+ ascii_string[5] = 0x20;
+ ascii_string[6] = 0x77;
+ ascii_string[7] = 0x6F;
+ ascii_string[8] = 0x72;
+ ascii_string[9] = 0x6C;
+ ascii_string[10] = 0x64;
+ ascii_string[11] = 0;
+ do_check_eq("hello world", ascii_string.readStringReplaceMalformed());
+
+ // UTF-8 decode test, "U+AC00 U+B098 U+B2E4"
+ let utf8_string = ctypes.unsigned_char.array(10)();
+ utf8_string[0] = 0xEA;
+ utf8_string[1] = 0xB0;
+ utf8_string[2] = 0x80;
+ utf8_string[3] = 0xEB;
+ utf8_string[4] = 0x82;
+ utf8_string[5] = 0x98;
+ utf8_string[6] = 0xEB;
+ utf8_string[7] = 0x8B;
+ utf8_string[8] = 0xA4;
+ utf8_string[9] = 0x00;
+ let utf8_result = utf8_string.readStringReplaceMalformed();
+ do_check_eq(0xAC00, utf8_result.charCodeAt(0));
+ do_check_eq(0xB098, utf8_result.charCodeAt(1));
+ do_check_eq(0xB2E4, utf8_result.charCodeAt(2));
+
+ // KS5601 decode test, invalid encoded byte should be replaced with U+FFFD
+ let ks5601_string = ctypes.unsigned_char.array(7)();
+ ks5601_string[0] = 0xB0;
+ ks5601_string[1] = 0xA1;
+ ks5601_string[2] = 0xB3;
+ ks5601_string[3] = 0xAA;
+ ks5601_string[4] = 0xB4;
+ ks5601_string[5] = 0xD9;
+ ks5601_string[6] = 0x00;
+ let ks5601_result = ks5601_string.readStringReplaceMalformed();
+ do_check_eq(0xFFFD, ks5601_result.charCodeAt(0));
+ do_check_eq(0xFFFD, ks5601_result.charCodeAt(1));
+ do_check_eq(0xFFFD, ks5601_result.charCodeAt(2));
+ do_check_eq(0xFFFD, ks5601_result.charCodeAt(3));
+ do_check_eq(0xFFFD, ks5601_result.charCodeAt(4));
+ do_check_eq(0xFFFD, ks5601_result.charCodeAt(5));
+
+ // Mixed decode test, "test" + "U+AC00 U+B098 U+B2E4" + "test"
+ // invalid encoded byte should be replaced with U+FFFD
+ let mixed_string = ctypes.unsigned_char.array(15)();
+ mixed_string[0] = 0x74;
+ mixed_string[1] = 0x65;
+ mixed_string[2] = 0x73;
+ mixed_string[3] = 0x74;
+ mixed_string[4] = 0xB0;
+ mixed_string[5] = 0xA1;
+ mixed_string[6] = 0xB3;
+ mixed_string[7] = 0xAA;
+ mixed_string[8] = 0xB4;
+ mixed_string[9] = 0xD9;
+ mixed_string[10] = 0x74;
+ mixed_string[11] = 0x65;
+ mixed_string[12] = 0x73;
+ mixed_string[13] = 0x74;
+ mixed_string[14] = 0x00;
+ let mixed_result = mixed_string.readStringReplaceMalformed();
+ do_check_eq(0x74, mixed_result.charCodeAt(0));
+ do_check_eq(0x65, mixed_result.charCodeAt(1));
+ do_check_eq(0x73, mixed_result.charCodeAt(2));
+ do_check_eq(0x74, mixed_result.charCodeAt(3));
+ do_check_eq(0xFFFD, mixed_result.charCodeAt(4));
+ do_check_eq(0xFFFD, mixed_result.charCodeAt(5));
+ do_check_eq(0xFFFD, mixed_result.charCodeAt(6));
+ do_check_eq(0xFFFD, mixed_result.charCodeAt(7));
+ do_check_eq(0xFFFD, mixed_result.charCodeAt(8));
+ do_check_eq(0xFFFD, mixed_result.charCodeAt(9));
+ do_check_eq(0x74, mixed_result.charCodeAt(10));
+ do_check_eq(0x65, mixed_result.charCodeAt(11));
+ do_check_eq(0x73, mixed_result.charCodeAt(12));
+ do_check_eq(0x74, mixed_result.charCodeAt(13));
+
+ // Test of all posible invalid encoded sequence
+ let invalid_string = ctypes.unsigned_char.array(27)();
+ invalid_string[0] = 0x80; // 10000000
+ invalid_string[1] = 0xD0; // 11000000 01110100
+ invalid_string[2] = 0x74;
+ invalid_string[3] = 0xE0; // 11100000 01110100
+ invalid_string[4] = 0x74;
+ invalid_string[5] = 0xE0; // 11100000 10100000 01110100
+ invalid_string[6] = 0xA0;
+ invalid_string[7] = 0x74;
+ invalid_string[8] = 0xE0; // 11100000 10000000 01110100
+ invalid_string[9] = 0x80;
+ invalid_string[10] = 0x74;
+ invalid_string[11] = 0xF0; // 11110000 01110100
+ invalid_string[12] = 0x74;
+ invalid_string[13] = 0xF0; // 11110000 10010000 01110100
+ invalid_string[14] = 0x90;
+ invalid_string[15] = 0x74;
+ invalid_string[16] = 0xF0; // 11110000 10010000 10000000 01110100
+ invalid_string[17] = 0x90;
+ invalid_string[18] = 0x80;
+ invalid_string[19] = 0x74;
+ invalid_string[20] = 0xF0; // 11110000 10000000 10000000 01110100
+ invalid_string[21] = 0x80;
+ invalid_string[22] = 0x80;
+ invalid_string[23] = 0x74;
+ invalid_string[24] = 0xF0; // 11110000 01110100
+ invalid_string[25] = 0x74;
+ invalid_string[26] = 0x00;
+ let invalid_result = invalid_string.readStringReplaceMalformed();
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(0)); // 10000000
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(1)); // 11000000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(2));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(3)); // 11100000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(4));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(5)); // 11100000 10100000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(6));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(7)); // 11100000 10000000 01110100
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(8));
+ do_check_eq(0x74, invalid_result.charCodeAt(9));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(10)); // 11110000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(11));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(12)); // 11110000 10010000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(13));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(14)); // 11110000 10010000 10000000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(15));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(16)); // 11110000 10000000 10000000 01110100
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(17));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(18));
+ do_check_eq(0x74, invalid_result.charCodeAt(19));
+ do_check_eq(0xFFFD, invalid_result.charCodeAt(20)); // 11110000 01110100
+ do_check_eq(0x74, invalid_result.charCodeAt(21));
+
+ // Test decoding of UTF-8 and CESU-8
+ let utf8_cesu8_string = ctypes.unsigned_char.array(10)();
+ utf8_cesu8_string[0] = 0xF0; // U+10400 in UTF-8
+ utf8_cesu8_string[1] = 0x90;
+ utf8_cesu8_string[2] = 0x90;
+ utf8_cesu8_string[3] = 0x80;
+ utf8_cesu8_string[4] = 0xED; // U+10400 in CESU-8
+ utf8_cesu8_string[5] = 0xA0;
+ utf8_cesu8_string[6] = 0x81;
+ utf8_cesu8_string[7] = 0xED;
+ utf8_cesu8_string[8] = 0xB0;
+ utf8_cesu8_string[9] = 0x80;
+ let utf8_cesu8_result = utf8_cesu8_string.readStringReplaceMalformed();
+ do_check_eq(0xD801, utf8_cesu8_result.charCodeAt(0));
+ do_check_eq(0xDC00, utf8_cesu8_result.charCodeAt(1));
+ do_check_eq(0xFFFD, utf8_cesu8_result.charCodeAt(2));
+ do_check_eq(0xFFFD, utf8_cesu8_result.charCodeAt(3));
+}
+
+function run_struct_tests(library) {
+ const point_t = new ctypes.StructType("myPOINT",
+ [{ x: ctypes.int32_t },
+ { y: ctypes.int32_t }]);
+ const rect_t = new ctypes.StructType("myRECT",
+ [{ top : ctypes.int32_t },
+ { left : ctypes.int32_t },
+ { bottom: ctypes.int32_t },
+ { right : ctypes.int32_t }]);
+
+ let test_pt_in_rect = library.declare("test_pt_in_rect", ctypes.default_abi, ctypes.int32_t, rect_t, point_t);
+ let rect = new rect_t(10, 5, 5, 10);
+ let pt1 = new point_t(6, 6);
+ do_check_eq(test_pt_in_rect(rect, pt1), 1);
+ let pt2 = new point_t(2, 2);
+ do_check_eq(test_pt_in_rect(rect, pt2), 0);
+
+ const inner_t = new ctypes.StructType("INNER",
+ [{ i1: ctypes.uint8_t },
+ { i2: ctypes.int64_t },
+ { i3: ctypes.uint8_t }]);
+ const nested_t = new ctypes.StructType("NESTED",
+ [{ n1 : ctypes.int32_t },
+ { n2 : ctypes.int16_t },
+ { inner: inner_t },
+ { n3 : ctypes.int64_t },
+ { n4 : ctypes.int32_t }]);
+
+ let test_nested_struct = library.declare("test_nested_struct", ctypes.default_abi, ctypes.int32_t, nested_t);
+ let inner = new inner_t(161, 523412, 43);
+ let nested = new nested_t(13155, 1241, inner, 24512115, 1234111);
+ // add up all the numbers and make sure the C function agrees
+ do_check_eq(test_nested_struct(nested), 26284238);
+
+ // test returning a struct by value
+ let test_struct_return = library.declare("test_struct_return", ctypes.default_abi, point_t, rect_t);
+ let ret = test_struct_return(rect);
+ do_check_eq(ret.x, rect.left);
+ do_check_eq(ret.y, rect.top);
+
+ // struct parameter ABI depends on size; test returning a large struct by value
+ test_struct_return = library.declare("test_large_struct_return", ctypes.default_abi, rect_t, rect_t, rect_t);
+ ret = test_struct_return(rect_t(1, 2, 3, 4), rect_t(5, 6, 7, 8));
+ do_check_eq(ret.left, 2);
+ do_check_eq(ret.right, 4);
+ do_check_eq(ret.top, 5);
+ do_check_eq(ret.bottom, 7);
+
+ // ... and tests structs < 8 bytes in size
+ for (let i = 1; i < 8; ++i)
+ run_small_struct_test(library, rect_t, i);
+
+ // test passing a struct by pointer
+ let test_init_pt = library.declare("test_init_pt", ctypes.default_abi, ctypes.void_t, point_t.ptr, ctypes.int32_t, ctypes.int32_t);
+ test_init_pt(pt1.address(), 9, 10);
+ do_check_eq(pt1.x, 9);
+ do_check_eq(pt1.y, 10);
+}
+
+function run_small_struct_test(library, rect_t, bytes)
+{
+ let fields = [];
+ for (let i = 0; i < bytes; ++i) {
+ let field = {};
+ field["f" + i] = ctypes.uint8_t;
+ fields.push(field);
+ }
+ const small_t = new ctypes.StructType("SMALL", fields);
+
+ let test_small_struct_return = library.declare("test_" + bytes + "_byte_struct_return", ctypes.default_abi, small_t, rect_t);
+ let ret = test_small_struct_return(rect_t(1, 7, 13, 45));
+
+ let exp = [1, 7, 13, 45];
+ let j = 0;
+ for (let i = 0; i < bytes; ++i) {
+ do_check_eq(ret["f" + i], exp[j]);
+ if (++j == 4)
+ j = 0;
+ }
+}
+
+function run_function_tests(library)
+{
+ let test_ansi_len = library.declare("test_ansi_len", ctypes.default_abi,
+ ctypes.int32_t, ctypes.char.ptr);
+ let fn_t = ctypes.FunctionType(ctypes.default_abi, ctypes.int32_t,
+ [ ctypes.char.ptr ]).ptr;
+
+ let test_fnptr = library.declare("test_fnptr", ctypes.default_abi, fn_t);
+
+ // Test that the value handed back by test_fnptr matches the function pointer
+ // for test_ansi_len itself.
+ let ptr = test_fnptr();
+ do_check_eq(ptrValue(test_ansi_len), ptrValue(ptr));
+
+ // Test that we can call ptr().
+ do_check_eq(ptr("function pointers rule!"), 23);
+
+ // Test that we can call via call and apply
+ do_check_eq(ptr.call(null, "function pointers rule!"), 23);
+ do_check_eq(ptr.apply(null, ["function pointers rule!"]), 23);
+
+ // Test that we cannot call non-function pointers via call and apply
+ let p_t = ctypes.PointerType(ctypes.int32_t);
+ let p = p_t();
+ do_check_throws(function() { p.call(null, "woo"); }, TypeError);
+ do_check_throws(function() { p.apply(null, ["woo"]); }, TypeError);
+
+ // Test the function pointers still behave as regular pointers
+ do_check_false(ptr.isNull(), "PointerType methods should still be valid");
+
+ // Test that library.declare() returns data of type FunctionType.ptr, and that
+ // it is immutable.
+ do_check_true(test_ansi_len.constructor.targetType.__proto__ ===
+ ctypes.FunctionType.prototype);
+ do_check_eq(test_ansi_len.constructor.toSource(),
+ "ctypes.FunctionType(ctypes.default_abi, ctypes.int32_t, [ctypes.char.ptr]).ptr");
+/* disabled temporarily per bug 598225.
+ do_check_throws(function() { test_ansi_len.value = null; }, Error);
+ do_check_eq(ptrValue(test_ansi_len), ptrValue(ptr));
+*/
+
+ // Test that the library.declare(name, functionType) form works.
+ let test_ansi_len_2 = library.declare("test_ansi_len", fn_t);
+ do_check_true(test_ansi_len_2.constructor === fn_t);
+ do_check_eq(ptrValue(test_ansi_len), ptrValue(test_ansi_len_2));
+/* disabled temporarily per bug 598225.
+ do_check_throws(function() { test_ansi_len_2.value = null; }, Error);
+ do_check_eq(ptrValue(test_ansi_len_2), ptrValue(ptr));
+*/
+}
+
+function run_closure_tests(library)
+{
+ run_single_closure_tests(library, ctypes.default_abi, "cdecl");
+ if ("winLastError" in ctypes) {
+ run_single_closure_tests(library, ctypes.stdcall_abi, "stdcall");
+
+ // Check that attempting to construct a ctypes.winapi_abi closure throws.
+ function closure_fn()
+ {
+ return 1;
+ }
+ let fn_t = ctypes.FunctionType(ctypes.winapi_abi, ctypes.int32_t, []).ptr;
+ do_check_throws(function() { fn_t(closure_fn) }, Error);
+ }
+}
+
+function run_single_closure_tests(library, abi, suffix)
+{
+ let b = 23;
+
+ function closure_fn(i)
+ {
+ if (i == 42)
+ throw "7.5 million years for that?";
+ return "a" in this ? i + this.a : i + b;
+ }
+
+ do_check_eq(closure_fn(7), 7 + b);
+ let thisobj = { a: 5 };
+ do_check_eq(closure_fn.call(thisobj, 7), 7 + thisobj.a);
+
+ // Construct a closure, and call it ourselves.
+ let fn_t = ctypes.FunctionType(abi, ctypes.int32_t, [ ctypes.int8_t ]).ptr;
+ let closure = fn_t(closure_fn);
+ do_check_eq(closure(-17), -17 + b);
+
+ // Have C code call it.
+ let test_closure = library.declare("test_closure_" + suffix,
+ ctypes.default_abi, ctypes.int32_t, ctypes.int8_t, fn_t);
+ do_check_eq(test_closure(-52, closure), -52 + b);
+
+ // Do the same, but specify 'this'.
+ let closure2 = fn_t(closure_fn, thisobj);
+ do_check_eq(closure2(-17), -17 + thisobj.a);
+ do_check_eq(test_closure(-52, closure2), -52 + thisobj.a);
+
+ // Specify an error sentinel, and have the JS code throw (see bug 599791).
+ let closure3 = fn_t(closure_fn, null, 54);
+ do_check_eq(closure3(42), 54);
+ do_check_eq(test_closure(42, closure3), 54);
+
+ // Check what happens when the return type is bigger than a word.
+ var fn_64_t = ctypes.FunctionType(ctypes.default_abi, ctypes.uint64_t, [ctypes.bool]).ptr;
+ var bignum1 = ctypes.UInt64.join(0xDEADBEEF, 0xBADF00D);
+ var bignum2 = ctypes.UInt64.join(0xDEFEC8ED, 0xD15EA5E);
+ function closure_fn_64(fail)
+ {
+ if (fail)
+ throw "Just following orders, sir!";
+ return bignum1;
+ }
+ var closure64 = fn_64_t(closure_fn_64, null, bignum2);
+ do_check_eq(ctypes.UInt64.compare(closure64(false), bignum1), 0);
+ do_check_eq(ctypes.UInt64.compare(closure64(true), bignum2), 0);
+
+ // Test a callback that returns void (see bug 682504).
+ var fn_v_t = ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, []).ptr;
+ fn_v_t(function() {})(); // Don't crash
+
+ // Code evaluated in a sandbox uses (and pushes) a separate JSContext.
+ // Make sure that we don't run into an assertion caused by a cx stack
+ // mismatch with the cx stashed in the closure.
+ try {
+ var sb = Components.utils.Sandbox("http://www.example.com");
+ sb.fn = fn_v_t(function() { sb.foo = {}; });
+ Components.utils.evalInSandbox("fn();", sb);
+ } catch (e) {} // Components not available in workers.
+
+ // Make sure that a void callback can't return an error sentinel.
+ var sentinelThrew = false;
+ try {
+ fn_v_t(function() {}, null, -1);
+ } catch (e) {
+ sentinelThrew = true;
+ }
+ do_check_true(sentinelThrew);
+}
+
+function run_variadic_tests(library) {
+ let sum_va_type = ctypes.FunctionType(ctypes.default_abi,
+ ctypes.int32_t,
+ [ctypes.uint8_t, "..."]).ptr,
+ sum_va = library.declare("test_sum_va_cdecl", ctypes.default_abi, ctypes.int32_t,
+ ctypes.uint8_t, "...");
+
+ do_check_eq(sum_va_type.toSource(),
+ 'ctypes.FunctionType(ctypes.default_abi, ctypes.int32_t, [ctypes.uint8_t, "..."]).ptr');
+ do_check_eq(sum_va.constructor.name, "int32_t(*)(uint8_t, ...)");
+ do_check_true(sum_va.constructor.targetType.isVariadic);
+
+ do_check_eq(sum_va(3,
+ ctypes.int32_t(1),
+ ctypes.int32_t(2),
+ ctypes.int32_t(3)),
+ 6);
+
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.bool,
+ [ctypes.bool, "...", ctypes.bool]);
+ }, Error);
+
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.default_abi, ctypes.bool, ["..."]);
+ }, Error);
+
+ if ("winLastError" in ctypes) {
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.stdcall_abi, ctypes.bool,
+ [ctypes.bool, "..."]);
+ }, Error);
+ do_check_throws(function() {
+ ctypes.FunctionType(ctypes.winapi_abi, ctypes.bool,
+ [ctypes.bool, "..."]);
+ }, Error);
+ }
+
+ do_check_throws(function() {
+ // No variadic closure callbacks allowed.
+ sum_va_type(function() {});
+ }, Error);
+
+ let count_true_va = library.declare("test_sum_va_cdecl", ctypes.default_abi, ctypes.uint8_t,
+ ctypes.uint8_t, "...");
+ do_check_eq(count_true_va(8,
+ ctypes.bool(false),
+ ctypes.bool(false),
+ ctypes.bool(false),
+ ctypes.bool(true),
+ ctypes.bool(true),
+ ctypes.bool(false),
+ ctypes.bool(true),
+ ctypes.bool(true)),
+ 4);
+
+ let add_char_short_int_va = library.declare("test_add_char_short_int_va_cdecl",
+ ctypes.default_abi, ctypes.void_t,
+ ctypes.uint32_t.ptr, "..."),
+ result = ctypes.uint32_t(3);
+
+ add_char_short_int_va(result.address(),
+ ctypes.char(5),
+ ctypes.short(7),
+ ctypes.uint32_t(11));
+
+ do_check_eq(result.value, 3 + 5 + 7 + 11);
+
+ result = ctypes.int32_t.array(3)([1, 1, 1]),
+ v1 = ctypes.int32_t.array(4)([1, 2, 3, 5]),
+ v2 = ctypes.int32_t.array(3)([7, 11, 13]),
+ vector_add_va = library.declare("test_vector_add_va_cdecl",
+ ctypes.default_abi, ctypes.int32_t.ptr,
+ ctypes.uint8_t, ctypes.uint8_t, "..."),
+ // Note that vector_add_va zeroes out result first.
+ vec_sum = vector_add_va(2, 3, result, v1, v2);
+ do_check_eq(vec_sum.contents, 8);
+ do_check_eq(result[0], 8);
+ do_check_eq(result[1], 13);
+ do_check_eq(result[2], 16);
+
+ do_check_true(!!(sum_va_type().value = sum_va_type()));
+ let sum_notva_type = ctypes.FunctionType(sum_va_type.targetType.abi,
+ sum_va_type.targetType.returnType,
+ [ctypes.uint8_t]).ptr;
+ do_check_throws(function() {
+ sum_va_type().value = sum_notva_type();
+ }, TypeError);
+}
+
+function run_static_data_tests(library)
+{
+ const rect_t = new ctypes.StructType("myRECT",
+ [{ top : ctypes.int32_t },
+ { left : ctypes.int32_t },
+ { bottom: ctypes.int32_t },
+ { right : ctypes.int32_t }]);
+
+ let data_rect = library.declare("data_rect", rect_t);
+
+ // Test reading static data.
+ do_check_true(data_rect.constructor === rect_t);
+ do_check_eq(data_rect.top, -1);
+ do_check_eq(data_rect.left, -2);
+ do_check_eq(data_rect.bottom, 3);
+ do_check_eq(data_rect.right, 4);
+
+ // Test writing.
+ data_rect.top = 9;
+ data_rect.left = 8;
+ data_rect.bottom = -11;
+ data_rect.right = -12;
+ do_check_eq(data_rect.top, 9);
+ do_check_eq(data_rect.left, 8);
+ do_check_eq(data_rect.bottom, -11);
+ do_check_eq(data_rect.right, -12);
+
+ // Make sure it's been written, not copied.
+ let data_rect_2 = library.declare("data_rect", rect_t);
+ do_check_eq(data_rect_2.top, 9);
+ do_check_eq(data_rect_2.left, 8);
+ do_check_eq(data_rect_2.bottom, -11);
+ do_check_eq(data_rect_2.right, -12);
+ do_check_eq(ptrValue(data_rect.address()), ptrValue(data_rect_2.address()));
+}
+
+function run_cpp_class_tests(library)
+{
+ // try the gcc mangling, unless we're using MSVC.
+ let OS = get_os();
+ let ctor_symbol;
+ let add_symbol;
+ let abi;
+ if (OS == "WINNT") {
+ // for compatibility for Win32 vs Win64
+ abi = ctypes.thiscall_abi;
+ if (ctypes.size_t.size == 8) {
+ ctor_symbol = '??0TestClass@@QEAA@H@Z';
+ add_symbol = '?Add@TestClass@@QEAAHH@Z';
+ } else {
+ ctor_symbol = '??0TestClass@@QAE@H@Z';
+ add_symbol = '?Add@TestClass@@QAEHH@Z';
+ }
+ } else {
+ abi = ctypes.default_abi;
+ ctor_symbol = "_ZN9TestClassC1Ei";
+ add_symbol = "_ZN9TestClass3AddEi";
+ }
+
+ let test_class_ctor = library.declare(ctor_symbol, abi, ctypes.void_t,
+ ctypes.int32_t.ptr, ctypes.int32_t);
+ let i = ctypes.int32_t();
+ test_class_ctor(i.address(), 8);
+ do_check_eq(i.value, 8);
+
+ let test_class_add = library.declare(add_symbol, abi, ctypes.int32_t,
+ ctypes.int32_t.ptr, ctypes.int32_t);
+ let j = test_class_add(i.address(), 5);
+ do_check_eq(j, 13);
+ do_check_eq(i.value, 13);
+}
+
+// bug 522360 - try loading system library without full path
+function run_load_system_library()
+{
+ let syslib;
+ let OS = get_os();
+ if (OS == "WINNT") {
+ syslib = ctypes.open("user32.dll");
+ } else if (OS == "Darwin") {
+ syslib = ctypes.open("libm.dylib");
+ } else if (OS == "Linux" || OS == "Android" || OS.match(/BSD$/)) {
+ try {
+ syslib = ctypes.open("libm.so");
+ } catch (e) {
+ // limb.so wasn't available, try libm.so.6 instead
+ syslib = ctypes.open("libm.so.6");
+ }
+ } else {
+ do_throw("please add a system library for this test");
+ }
+ syslib.close();
+ return true;
+}
diff --git a/toolkit/components/ctypes/tests/unit/xpcshell.ini b/toolkit/components/ctypes/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..8920d4f9b9
--- /dev/null
+++ b/toolkit/components/ctypes/tests/unit/xpcshell.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_errno.js]
+
+[test_finalizer.js]
+[test_finalizer_shouldfail.js]
+[test_finalizer_shouldaccept.js]
+[test_jsctypes.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
diff --git a/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp b/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp
new file mode 100644
index 0000000000..950d3b4877
--- /dev/null
+++ b/toolkit/components/diskspacewatcher/DiskSpaceWatcher.cpp
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "DiskSpaceWatcher.h"
+#include "nsIObserverService.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/Hal.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Services.h"
+
+#define NS_DISKSPACEWATCHER_CID \
+ { 0xab218518, 0xf197, 0x4fb4, { 0x8b, 0x0f, 0x8b, 0xb3, 0x4d, 0xf2, 0x4b, 0xf4 } }
+
+using namespace mozilla;
+
+StaticRefPtr<DiskSpaceWatcher> gDiskSpaceWatcher;
+
+NS_IMPL_ISUPPORTS(DiskSpaceWatcher, nsIDiskSpaceWatcher, nsIObserver)
+
+uint64_t DiskSpaceWatcher::sFreeSpace = 0;
+bool DiskSpaceWatcher::sIsDiskFull = false;
+
+DiskSpaceWatcher::DiskSpaceWatcher()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gDiskSpaceWatcher);
+}
+
+DiskSpaceWatcher::~DiskSpaceWatcher()
+{
+ MOZ_ASSERT(!gDiskSpaceWatcher);
+}
+
+already_AddRefed<DiskSpaceWatcher>
+DiskSpaceWatcher::FactoryCreate()
+{
+ if (!XRE_IsParentProcess()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!Preferences::GetBool("disk_space_watcher.enabled", false)) {
+ return nullptr;
+ }
+
+ if (!gDiskSpaceWatcher) {
+ gDiskSpaceWatcher = new DiskSpaceWatcher();
+ ClearOnShutdown(&gDiskSpaceWatcher);
+ }
+
+ RefPtr<DiskSpaceWatcher> service = gDiskSpaceWatcher.get();
+ return service.forget();
+}
+
+NS_IMETHODIMP
+DiskSpaceWatcher::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!strcmp(aTopic, "profile-after-change")) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->AddObserver(this, "profile-before-change", false);
+ mozilla::hal::StartDiskSpaceWatcher();
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, "profile-before-change")) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->RemoveObserver(this, "profile-before-change");
+ mozilla::hal::StopDiskSpaceWatcher();
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(false, "DiskSpaceWatcher got unexpected topic!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP DiskSpaceWatcher::GetIsDiskFull(bool* aIsDiskFull)
+{
+ *aIsDiskFull = sIsDiskFull;
+ return NS_OK;
+}
+
+// GetFreeSpace is a macro on windows, and that messes up with the c++
+// compiler.
+#ifdef XP_WIN
+#undef GetFreeSpace
+#endif
+NS_IMETHODIMP DiskSpaceWatcher::GetFreeSpace(uint64_t* aFreeSpace)
+{
+ *aFreeSpace = sFreeSpace;
+ return NS_OK;
+}
+
+// static
+void DiskSpaceWatcher::UpdateState(bool aIsDiskFull, uint64_t aFreeSpace)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!gDiskSpaceWatcher) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ sIsDiskFull = aIsDiskFull;
+ sFreeSpace = aFreeSpace;
+
+ if (!observerService) {
+ return;
+ }
+
+ const char16_t stateFull[] = { 'f', 'u', 'l', 'l', 0 };
+ const char16_t stateFree[] = { 'f', 'r', 'e', 'e', 0 };
+
+ nsCOMPtr<nsISupports> subject;
+ CallQueryInterface(gDiskSpaceWatcher.get(), getter_AddRefs(subject));
+ MOZ_ASSERT(subject);
+ observerService->NotifyObservers(subject,
+ DISKSPACEWATCHER_OBSERVER_TOPIC,
+ sIsDiskFull ? stateFull : stateFree);
+ return;
+}
+
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(DiskSpaceWatcher,
+ DiskSpaceWatcher::FactoryCreate)
+
+NS_DEFINE_NAMED_CID(NS_DISKSPACEWATCHER_CID);
+
+static const mozilla::Module::CIDEntry kDiskSpaceWatcherCIDs[] = {
+ { &kNS_DISKSPACEWATCHER_CID, false, nullptr, DiskSpaceWatcherConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kDiskSpaceWatcherContracts[] = {
+ { "@mozilla.org/toolkit/disk-space-watcher;1", &kNS_DISKSPACEWATCHER_CID },
+ { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kDiskSpaceWatcherCategories[] = {
+#ifdef MOZ_WIDGET_GONK
+ { "profile-after-change", "Disk Space Watcher Service", DISKSPACEWATCHER_CONTRACTID },
+#endif
+ { nullptr }
+};
+
+static const mozilla::Module kDiskSpaceWatcherModule = {
+ mozilla::Module::kVersion,
+ kDiskSpaceWatcherCIDs,
+ kDiskSpaceWatcherContracts,
+ kDiskSpaceWatcherCategories
+};
+
+NSMODULE_DEFN(DiskSpaceWatcherModule) = &kDiskSpaceWatcherModule;
diff --git a/toolkit/components/diskspacewatcher/DiskSpaceWatcher.h b/toolkit/components/diskspacewatcher/DiskSpaceWatcher.h
new file mode 100644
index 0000000000..6559af3cde
--- /dev/null
+++ b/toolkit/components/diskspacewatcher/DiskSpaceWatcher.h
@@ -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/. */
+
+#ifndef __DISKSPACEWATCHER_H__
+
+#include "nsIDiskSpaceWatcher.h"
+#include "nsIObserver.h"
+#include "nsCOMPtr.h"
+
+class DiskSpaceWatcher final : public nsIDiskSpaceWatcher,
+ public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDISKSPACEWATCHER
+ NS_DECL_NSIOBSERVER
+
+ static already_AddRefed<DiskSpaceWatcher>
+ FactoryCreate();
+
+ static void UpdateState(bool aIsDiskFull, uint64_t aFreeSpace);
+
+private:
+ DiskSpaceWatcher();
+ ~DiskSpaceWatcher();
+
+ static uint64_t sFreeSpace;
+ static bool sIsDiskFull;
+};
+
+#endif // __DISKSPACEWATCHER_H__
diff --git a/toolkit/components/diskspacewatcher/moz.build b/toolkit/components/diskspacewatcher/moz.build
new file mode 100644
index 0000000000..168af46a68
--- /dev/null
+++ b/toolkit/components/diskspacewatcher/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/.
+
+XPIDL_SOURCES += [
+ 'nsIDiskSpaceWatcher.idl',
+]
+
+EXPORTS += [
+ 'DiskSpaceWatcher.h'
+]
+
+XPIDL_MODULE = 'diskspacewatcher'
+
+SOURCES = [
+ 'DiskSpaceWatcher.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/diskspacewatcher/nsIDiskSpaceWatcher.idl b/toolkit/components/diskspacewatcher/nsIDiskSpaceWatcher.idl
new file mode 100644
index 0000000000..a9c60ca9f4
--- /dev/null
+++ b/toolkit/components/diskspacewatcher/nsIDiskSpaceWatcher.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+%{ C++
+#ifdef XP_WIN
+#undef GetFreeSpace
+#endif
+%}
+
+[scriptable, uuid(3aceba74-2ed5-4e99-8fe4-06e90e2b8ef0)]
+interface nsIDiskSpaceWatcher : nsISupports
+{
+ readonly attribute bool isDiskFull; // True if we are low on disk space.
+ readonly attribute unsigned long long freeSpace; // The free space currently available.
+};
+
+%{ C++
+#define DISKSPACEWATCHER_CONTRACTID "@mozilla.org/toolkit/disk-space-watcher;1"
+
+// The data for this notification will be either 'free' or 'full'.
+#define DISKSPACEWATCHER_OBSERVER_TOPIC "disk-space-watcher"
+%}
diff --git a/toolkit/components/downloads/ApplicationReputation.cpp b/toolkit/components/downloads/ApplicationReputation.cpp
new file mode 100644
index 0000000000..7bd219dbf2
--- /dev/null
+++ b/toolkit/components/downloads/ApplicationReputation.cpp
@@ -0,0 +1,1629 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+// See
+// https://wiki.mozilla.org/Security/Features/Application_Reputation_Design_Doc
+// for a description of Chrome's implementation of this feature.
+#include "ApplicationReputation.h"
+#include "chrome/common/safe_browsing/csd.pb.h"
+
+#include "nsIArray.h"
+#include "nsIApplicationReputation.h"
+#include "nsIChannel.h"
+#include "nsICryptoHash.h"
+#include "nsIHttpChannel.h"
+#include "nsIIOService.h"
+#include "nsIPrefService.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIStreamListener.h"
+#include "nsIStringStream.h"
+#include "nsITimer.h"
+#include "nsIUploadChannel2.h"
+#include "nsIURI.h"
+#include "nsIURL.h"
+#include "nsIUrlClassifierDBService.h"
+#include "nsIX509Cert.h"
+#include "nsIX509CertDB.h"
+#include "nsIX509CertList.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/LoadContext.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+
+#include "nsAutoPtr.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsNetCID.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+#include "nsXPCOMStrings.h"
+
+#include "nsIContentPolicy.h"
+#include "nsILoadInfo.h"
+#include "nsContentUtils.h"
+
+using mozilla::ArrayLength;
+using mozilla::BasePrincipal;
+using mozilla::DocShellOriginAttributes;
+using mozilla::PrincipalOriginAttributes;
+using mozilla::Preferences;
+using mozilla::TimeStamp;
+using mozilla::Telemetry::Accumulate;
+using safe_browsing::ClientDownloadRequest;
+using safe_browsing::ClientDownloadRequest_CertificateChain;
+using safe_browsing::ClientDownloadRequest_Resource;
+using safe_browsing::ClientDownloadRequest_SignatureInfo;
+
+// Preferences that we need to initialize the query.
+#define PREF_SB_APP_REP_URL "browser.safebrowsing.downloads.remote.url"
+#define PREF_SB_MALWARE_ENABLED "browser.safebrowsing.malware.enabled"
+#define PREF_SB_DOWNLOADS_ENABLED "browser.safebrowsing.downloads.enabled"
+#define PREF_SB_DOWNLOADS_REMOTE_ENABLED "browser.safebrowsing.downloads.remote.enabled"
+#define PREF_SB_DOWNLOADS_REMOTE_TIMEOUT "browser.safebrowsing.downloads.remote.timeout_ms"
+#define PREF_GENERAL_LOCALE "general.useragent.locale"
+#define PREF_DOWNLOAD_BLOCK_TABLE "urlclassifier.downloadBlockTable"
+#define PREF_DOWNLOAD_ALLOW_TABLE "urlclassifier.downloadAllowTable"
+
+// Preferences that are needed to action the verdict.
+#define PREF_BLOCK_DANGEROUS "browser.safebrowsing.downloads.remote.block_dangerous"
+#define PREF_BLOCK_DANGEROUS_HOST "browser.safebrowsing.downloads.remote.block_dangerous_host"
+#define PREF_BLOCK_POTENTIALLY_UNWANTED "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+#define PREF_BLOCK_UNCOMMON "browser.safebrowsing.downloads.remote.block_uncommon"
+
+// MOZ_LOG=ApplicationReputation:5
+mozilla::LazyLogModule ApplicationReputationService::prlog("ApplicationReputation");
+#define LOG(args) MOZ_LOG(ApplicationReputationService::prlog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(ApplicationReputationService::prlog, mozilla::LogLevel::Debug)
+
+class PendingDBLookup;
+
+// A single use class private to ApplicationReputationService encapsulating an
+// nsIApplicationReputationQuery and an nsIApplicationReputationCallback. Once
+// created by ApplicationReputationService, it is guaranteed to call mCallback.
+// This class is private to ApplicationReputationService.
+class PendingLookup final : public nsIStreamListener,
+ public nsITimerCallback,
+ public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSIOBSERVER
+
+ // Constructor and destructor.
+ PendingLookup(nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback);
+
+ // Start the lookup. The lookup may have 2 parts: local and remote. In the
+ // local lookup, PendingDBLookups are created to query the local allow and
+ // blocklists for various URIs associated with this downloaded file. In the
+ // event that no results are found, a remote lookup is sent to the Application
+ // Reputation server.
+ nsresult StartLookup();
+
+private:
+ ~PendingLookup();
+
+ friend class PendingDBLookup;
+
+ // Telemetry states.
+ // Status of the remote response (valid or not).
+ enum SERVER_RESPONSE_TYPES {
+ SERVER_RESPONSE_VALID = 0,
+ SERVER_RESPONSE_FAILED = 1,
+ SERVER_RESPONSE_INVALID = 2,
+ };
+
+ // Number of blocklist and allowlist hits we have seen.
+ uint32_t mBlocklistCount;
+ uint32_t mAllowlistCount;
+
+ // The query containing metadata about the downloaded file.
+ nsCOMPtr<nsIApplicationReputationQuery> mQuery;
+
+ // The callback with which to report the verdict.
+ nsCOMPtr<nsIApplicationReputationCallback> mCallback;
+
+ // An array of strings created from certificate information used to whitelist
+ // the downloaded file.
+ nsTArray<nsCString> mAllowlistSpecs;
+ // The source URI of the download, the referrer and possibly any redirects.
+ nsTArray<nsCString> mAnylistSpecs;
+
+ // When we started this query
+ TimeStamp mStartTime;
+
+ // The channel used to talk to the remote lookup server
+ nsCOMPtr<nsIChannel> mChannel;
+
+ // Timer to abort this lookup if it takes too long
+ nsCOMPtr<nsITimer> mTimeoutTimer;
+
+ // A protocol buffer for storing things we need in the remote request. We
+ // store the resource chain (redirect information) as well as signature
+ // information extracted using the Windows Authenticode API, if the binary is
+ // signed.
+ ClientDownloadRequest mRequest;
+
+ // The response from the application reputation query. This is read in chunks
+ // as part of our nsIStreamListener implementation and may contain embedded
+ // NULLs.
+ nsCString mResponse;
+
+ // Returns true if the file is likely to be binary.
+ bool IsBinaryFile();
+
+ // Returns the type of download binary for the file.
+ ClientDownloadRequest::DownloadType GetDownloadType(const nsAString& aFilename);
+
+ // Clean up and call the callback. PendingLookup must not be used after this
+ // function is called.
+ nsresult OnComplete(bool shouldBlock, nsresult rv,
+ uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE);
+
+ // Wrapper function for nsIStreamListener.onStopRequest to make it easy to
+ // guarantee calling the callback
+ nsresult OnStopRequestInternal(nsIRequest *aRequest,
+ nsISupports *aContext,
+ nsresult aResult,
+ bool* aShouldBlock,
+ uint32_t* aVerdict);
+
+ // Return the hex-encoded hash of the whole URI.
+ nsresult GetSpecHash(nsACString& aSpec, nsACString& hexEncodedHash);
+
+ // Strip url parameters, fragments, and user@pass fields from the URI spec
+ // using nsIURL. Hash data URIs and return blob URIs unfiltered.
+ nsresult GetStrippedSpec(nsIURI* aUri, nsACString& spec);
+
+ // Escape '/' and '%' in certificate attribute values.
+ nsCString EscapeCertificateAttribute(const nsACString& aAttribute);
+
+ // Escape ':' in fingerprint values.
+ nsCString EscapeFingerprint(const nsACString& aAttribute);
+
+ // Generate whitelist strings for the given certificate pair from the same
+ // certificate chain.
+ nsresult GenerateWhitelistStringsForPair(
+ nsIX509Cert* certificate, nsIX509Cert* issuer);
+
+ // Generate whitelist strings for the given certificate chain, which starts
+ // with the signer and may go all the way to the root cert.
+ nsresult GenerateWhitelistStringsForChain(
+ const ClientDownloadRequest_CertificateChain& aChain);
+
+ // For signed binaries, generate strings of the form:
+ // http://sb-ssl.google.com/safebrowsing/csd/certificate/
+ // <issuer_cert_sha1_fingerprint>[/CN=<cn>][/O=<org>][/OU=<unit>]
+ // for each (cert, issuer) pair in each chain of certificates that is
+ // associated with the binary.
+ nsresult GenerateWhitelistStrings();
+
+ // Parse the XPCOM certificate lists and stick them into the protocol buffer
+ // version.
+ nsresult ParseCertificates(nsIArray* aSigArray);
+
+ // Adds the redirects to mAnylistSpecs to be looked up.
+ nsresult AddRedirects(nsIArray* aRedirects);
+
+ // Helper function to ensure that we call PendingLookup::LookupNext or
+ // PendingLookup::OnComplete.
+ nsresult DoLookupInternal();
+
+ // Looks up all the URIs that may be responsible for allowlisting or
+ // blocklisting the downloaded file. These URIs may include whitelist strings
+ // generated by certificates verifying the binary as well as the target URI
+ // from which the file was downloaded.
+ nsresult LookupNext();
+
+ // Sends a query to the remote application reputation service. Returns NS_OK
+ // on success.
+ nsresult SendRemoteQuery();
+
+ // Helper function to ensure that we always call the callback.
+ nsresult SendRemoteQueryInternal();
+};
+
+// A single-use class for looking up a single URI in the safebrowsing DB. This
+// class is private to PendingLookup.
+class PendingDBLookup final : public nsIUrlClassifierCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERCALLBACK
+
+ // Constructor and destructor
+ explicit PendingDBLookup(PendingLookup* aPendingLookup);
+
+ // Look up the given URI in the safebrowsing DBs, optionally on both the allow
+ // list and the blocklist. If there is a match, call
+ // PendingLookup::OnComplete. Otherwise, call PendingLookup::LookupNext.
+ nsresult LookupSpec(const nsACString& aSpec, bool aAllowlistOnly);
+
+private:
+ ~PendingDBLookup();
+
+ // The download appeared on the allowlist, blocklist, or no list (and thus
+ // could trigger a remote query.
+ enum LIST_TYPES {
+ ALLOW_LIST = 0,
+ BLOCK_LIST = 1,
+ NO_LIST = 2,
+ };
+
+ nsCString mSpec;
+ bool mAllowlistOnly;
+ RefPtr<PendingLookup> mPendingLookup;
+ nsresult LookupSpecInternal(const nsACString& aSpec);
+};
+
+NS_IMPL_ISUPPORTS(PendingDBLookup,
+ nsIUrlClassifierCallback)
+
+PendingDBLookup::PendingDBLookup(PendingLookup* aPendingLookup) :
+ mAllowlistOnly(false),
+ mPendingLookup(aPendingLookup)
+{
+ LOG(("Created pending DB lookup [this = %p]", this));
+}
+
+PendingDBLookup::~PendingDBLookup()
+{
+ LOG(("Destroying pending DB lookup [this = %p]", this));
+ mPendingLookup = nullptr;
+}
+
+nsresult
+PendingDBLookup::LookupSpec(const nsACString& aSpec,
+ bool aAllowlistOnly)
+{
+ LOG(("Checking principal %s [this=%p]", aSpec.Data(), this));
+ mSpec = aSpec;
+ mAllowlistOnly = aAllowlistOnly;
+ nsresult rv = LookupSpecInternal(aSpec);
+ if (NS_FAILED(rv)) {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(rv, errorName);
+ LOG(("Error in LookupSpecInternal() [rv = %s, this = %p]",
+ errorName.get(), this));
+ return mPendingLookup->LookupNext(); // ignore this lookup and move to next
+ }
+ // LookupSpecInternal has called nsIUrlClassifierCallback.lookup, which is
+ // guaranteed to call HandleEvent.
+ return rv;
+}
+
+nsresult
+PendingDBLookup::LookupSpecInternal(const nsACString& aSpec)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ rv = ios->NewURI(aSpec, nullptr, nullptr, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PrincipalOriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateCodebasePrincipal(uri, attrs);
+ if (!principal) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check local lists to see if the URI has already been whitelisted or
+ // blacklisted.
+ LOG(("Checking DB service for principal %s [this = %p]", mSpec.get(), this));
+ nsCOMPtr<nsIUrlClassifierDBService> dbService =
+ do_GetService(NS_URLCLASSIFIERDBSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tables;
+ nsAutoCString allowlist;
+ Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, &allowlist);
+ if (!allowlist.IsEmpty()) {
+ tables.Append(allowlist);
+ }
+ nsAutoCString blocklist;
+ Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, &blocklist);
+ if (!mAllowlistOnly && !blocklist.IsEmpty()) {
+ tables.Append(',');
+ tables.Append(blocklist);
+ }
+ return dbService->Lookup(principal, tables, this);
+}
+
+NS_IMETHODIMP
+PendingDBLookup::HandleEvent(const nsACString& tables)
+{
+ // HandleEvent is guaranteed to call either:
+ // 1) PendingLookup::OnComplete if the URL matches the blocklist, or
+ // 2) PendingLookup::LookupNext if the URL does not match the blocklist.
+ // Blocklisting trumps allowlisting.
+ nsAutoCString blockList;
+ Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, &blockList);
+ if (!mAllowlistOnly && FindInReadable(blockList, tables)) {
+ mPendingLookup->mBlocklistCount++;
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_LOCAL, BLOCK_LIST);
+ LOG(("Found principal %s on blocklist [this = %p]", mSpec.get(), this));
+ return mPendingLookup->OnComplete(true, NS_OK,
+ nsIApplicationReputationService::VERDICT_DANGEROUS);
+ }
+
+ nsAutoCString allowList;
+ Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, &allowList);
+ if (FindInReadable(allowList, tables)) {
+ mPendingLookup->mAllowlistCount++;
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_LOCAL, ALLOW_LIST);
+ LOG(("Found principal %s on allowlist [this = %p]", mSpec.get(), this));
+ // Don't call onComplete, since blocklisting trumps allowlisting
+ } else {
+ LOG(("Didn't find principal %s on any list [this = %p]", mSpec.get(),
+ this));
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_LOCAL, NO_LIST);
+ }
+ return mPendingLookup->LookupNext();
+}
+
+NS_IMPL_ISUPPORTS(PendingLookup,
+ nsIStreamListener,
+ nsIRequestObserver,
+ nsIObserver)
+
+PendingLookup::PendingLookup(nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback) :
+ mBlocklistCount(0),
+ mAllowlistCount(0),
+ mQuery(aQuery),
+ mCallback(aCallback)
+{
+ LOG(("Created pending lookup [this = %p]", this));
+}
+
+PendingLookup::~PendingLookup()
+{
+ LOG(("Destroying pending lookup [this = %p]", this));
+}
+
+static const char16_t* kBinaryFileExtensions[] = {
+ // Extracted from the "File Type Policies" Chrome extension
+ //u".001",
+ //u".7z",
+ //u".ace",
+ //u".action", // Mac script
+ //u".ad", // Windows
+ u".ade", // MS Access
+ u".adp", // MS Access
+ u".apk", // Android package
+ u".app", // Executable application
+ u".application", // MS ClickOnce
+ u".appref-ms", // MS ClickOnce
+ //u".arc",
+ //u".arj",
+ u".as", // Mac archive
+ u".asp", // Windows Server script
+ u".asx", // Windows Media Player
+ //u".b64",
+ //u".balz",
+ u".bas", // Basic script
+ u".bash", // Linux shell
+ u".bat", // Windows shell
+ //u".bhx",
+ //u".bin",
+ u".bz", // Linux archive (bzip)
+ u".bz2", // Linux archive (bzip2)
+ u".bzip2", // Linux archive (bzip2)
+ u".cab", // Windows archive
+ u".cdr", // Mac disk image
+ u".cfg", // Windows
+ u".chi", // Windows Help
+ u".chm", // Windows Help
+ u".class", // Java
+ u".cmd", // Windows executable
+ u".com", // Windows executable
+ u".command", // Mac script
+ u".cpgz", // Mac archive
+ //u".cpio",
+ u".cpl", // Windows executable
+ u".crt", // Windows signed certificate
+ u".crx", // Chrome extensions
+ u".csh", // Linux shell
+ u".dart", // Mac disk image
+ u".dc42", // Apple DiskCopy Image
+ u".deb", // Linux package
+ u".dex", // Android
+ u".diskcopy42", // Apple DiskCopy Image
+ u".dll", // Windows executable
+ u".dmg", // Mac disk image
+ u".dmgpart", // Mac disk image
+ //u".docb", // MS Office
+ //u".docm", // MS Word
+ //u".docx", // MS Word
+ //u".dotm", // MS Word
+ //u".dott", // MS Office
+ u".drv", // Windows driver
+ u".dvdr", // Mac Disk image
+ u".efi", // Firmware
+ u".eml", // MS Outlook
+ u".exe", // Windows executable
+ //u".fat",
+ u".fon", // Windows font
+ u".fxp", // MS FoxPro
+ u".gadget", // Windows
+ u".grp", // Windows
+ u".gz", // Linux archive (gzip)
+ u".gzip", // Linux archive (gzip)
+ u".hfs", // Mac disk image
+ u".hlp", // Windows Help
+ u".hqx", // Mac archive
+ u".hta", // HTML trusted application
+ u".htt", // MS HTML template
+ u".img", // Mac disk image
+ u".imgpart", // Mac disk image
+ u".inf", // Windows installer
+ u".ini", // Generic config file
+ u".ins", // IIS config
+ //u".inx", // InstallShield
+ u".iso", // CD image
+ u".isp", // IIS config
+ //u".isu", // InstallShield
+ u".jar", // Java
+ u".jnlp", // Java
+ //u".job", // Windows
+ u".js", // JavaScript script
+ u".jse", // JScript
+ u".ksh", // Linux shell
+ //u".lha",
+ u".lnk", // Windows
+ u".local", // Windows
+ //u".lpaq1",
+ //u".lpaq5",
+ //u".lpaq8",
+ //u".lzh",
+ //u".lzma",
+ u".mad", // MS Access
+ u".maf", // MS Access
+ u".mag", // MS Access
+ u".mam", // MS Access
+ u".manifest", // Windows
+ u".maq", // MS Access
+ u".mar", // MS Access
+ u".mas", // MS Access
+ u".mat", // MS Access
+ u".mau", // Media attachment
+ u".mav", // MS Access
+ u".maw", // MS Access
+ u".mda", // MS Access
+ u".mdb", // MS Access
+ u".mde", // MS Access
+ u".mdt", // MS Access
+ u".mdw", // MS Access
+ u".mdz", // MS Access
+ u".mht", // MS HTML
+ u".mhtml", // MS HTML
+ u".mim", // MS Mail
+ u".mmc", // MS Office
+ u".mof", // Windows
+ u".mpkg", // Mac installer
+ u".msc", // Windows executable
+ u".msg", // MS Outlook
+ u".msh", // Windows shell
+ u".msh1", // Windows shell
+ u".msh1xml", // Windows shell
+ u".msh2", // Windows shell
+ u".msh2xml", // Windows shell
+ u".mshxml", // Windows
+ u".msi", // Windows installer
+ u".msp", // Windows installer
+ u".mst", // Windows installer
+ u".ndif", // Mac disk image
+ //u".ntfs", // 7z
+ u".ocx", // ActiveX
+ u".ops", // MS Office
+ //u".out", // Linux binary
+ //u".paf", // PortableApps package
+ //u".paq8f",
+ //u".paq8jd",
+ //u".paq8l",
+ //u".paq8o",
+ u".partial", // Downloads
+ u".pax", // Mac archive
+ u".pcd", // Microsoft Visual Test
+ u".pdf", // Adobe Acrobat
+ //u".pea",
+ u".pet", // Linux package
+ u".pif", // Windows
+ u".pkg", // Mac installer
+ u".pl", // Perl script
+ u".plg", // MS Visual Studio
+ //u".potx", // MS PowerPoint
+ //u".ppam", // MS PowerPoint
+ //u".ppsx", // MS PowerPoint
+ //u".pptm", // MS PowerPoint
+ //u".pptx", // MS PowerPoint
+ u".prf", // MS Outlook
+ u".prg", // Windows
+ u".ps1", // Windows shell
+ u".ps1xml", // Windows shell
+ u".ps2", // Windows shell
+ u".ps2xml", // Windows shell
+ u".psc1", // Windows shell
+ u".psc2", // Windows shell
+ u".pst", // MS Outlook
+ u".pup", // Linux package
+ u".py", // Python script
+ u".pyc", // Python binary
+ u".pyw", // Python GUI
+ //u".quad",
+ //u".r00",
+ //u".r01",
+ //u".r02",
+ //u".r03",
+ //u".r04",
+ //u".r05",
+ //u".r06",
+ //u".r07",
+ //u".r08",
+ //u".r09",
+ //u".r10",
+ //u".r11",
+ //u".r12",
+ //u".r13",
+ //u".r14",
+ //u".r15",
+ //u".r16",
+ //u".r17",
+ //u".r18",
+ //u".r19",
+ //u".r20",
+ //u".r21",
+ //u".r22",
+ //u".r23",
+ //u".r24",
+ //u".r25",
+ //u".r26",
+ //u".r27",
+ //u".r28",
+ //u".r29",
+ //u".rar",
+ u".rb", // Ruby script
+ u".reg", // Windows Registry
+ u".rels", // MS Office
+ //u".rgs", // Windows Registry
+ u".rpm", // Linux package
+ //u".rtf", // MS Office
+ //u".run", // Linux shell
+ u".scf", // Windows shell
+ u".scr", // Windows
+ u".sct", // Windows shell
+ u".search-ms", // Windows
+ u".sh", // Linux shell
+ u".shar", // Linux shell
+ u".shb", // Windows
+ u".shs", // Windows shell
+ //u".sldm", // MS PowerPoint
+ //u".sldx", // MS PowerPoint
+ u".slp", // Linux package
+ u".smi", // Mac disk image
+ u".sparsebundle", // Mac disk image
+ u".sparseimage", // Mac disk image
+ u".spl", // Adobe Flash
+ //u".squashfs",
+ u".svg",
+ u".swf", // Adobe Flash
+ u".swm", // Windows Imaging
+ u".sys", // Windows
+ u".tar", // Linux archive
+ u".taz", // Linux archive (bzip2)
+ u".tbz", // Linux archive (bzip2)
+ u".tbz2", // Linux archive (bzip2)
+ u".tcsh", // Linux shell
+ u".tgz", // Linux archive (gzip)
+ //u".toast", // Roxio disk image
+ //u".torrent", // Bittorrent
+ u".tpz", // Linux archive (gzip)
+ u".txz", // Linux archive (xz)
+ u".tz", // Linux archive (gzip)
+ //u".u3p", // U3 Smart Apps
+ u".udf", // MS Excel
+ u".udif", // Mac disk image
+ u".url", // Windows
+ //u".uu",
+ //u".uue",
+ u".vb", // Visual Basic script
+ u".vbe", // Visual Basic script
+ u".vbs", // Visual Basic script
+ //u".vbscript", // Visual Basic script
+ u".vhd", // Windows virtual hard drive
+ u".vhdx", // Windows virtual hard drive
+ u".vmdk", // VMware virtual disk
+ u".vsd", // MS Visio
+ u".vsmacros", // MS Visual Studio
+ u".vss", // MS Visio
+ u".vst", // MS Visio
+ u".vsw", // MS Visio
+ u".website", // Windows
+ u".wim", // Windows Imaging
+ //u".workflow", // Mac Automator
+ //u".wrc", // FreeArc archive
+ u".ws", // Windows script
+ u".wsc", // Windows script
+ u".wsf", // Windows script
+ u".wsh", // Windows script
+ u".xar", // MS Excel
+ u".xbap", // XAML Browser Application
+ u".xip", // Mac archive
+ //u".xlsm", // MS Excel
+ //u".xlsx", // MS Excel
+ //u".xltm", // MS Excel
+ //u".xltx", // MS Excel
+ u".xml",
+ u".xnk", // MS Exchange
+ u".xrm-ms", // Windows
+ u".xsl", // XML Stylesheet
+ //u".xxe",
+ u".xz", // Linux archive (xz)
+ u".z", // InstallShield
+#ifdef XP_WIN // disable on Mac/Linux, see 1167493
+ u".zip", // Generic archive
+#endif
+ u".zipx", // WinZip
+ //u".zpaq",
+};
+
+bool
+PendingLookup::IsBinaryFile()
+{
+ nsString fileName;
+ nsresult rv = mQuery->GetSuggestedFileName(fileName);
+ if (NS_FAILED(rv)) {
+ LOG(("No suggested filename [this = %p]", this));
+ return false;
+ }
+ LOG(("Suggested filename: %s [this = %p]",
+ NS_ConvertUTF16toUTF8(fileName).get(), this));
+
+ for (size_t i = 0; i < ArrayLength(kBinaryFileExtensions); ++i) {
+ if (StringEndsWith(fileName, nsDependentString(kBinaryFileExtensions[i]))) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+ClientDownloadRequest::DownloadType
+PendingLookup::GetDownloadType(const nsAString& aFilename) {
+ MOZ_ASSERT(IsBinaryFile());
+
+ // From https://cs.chromium.org/chromium/src/chrome/common/safe_browsing/download_protection_util.cc?l=17
+ if (StringEndsWith(aFilename, NS_LITERAL_STRING(".zip"))) {
+ return ClientDownloadRequest::ZIPPED_EXECUTABLE;
+ } else if (StringEndsWith(aFilename, NS_LITERAL_STRING(".apk"))) {
+ return ClientDownloadRequest::ANDROID_APK;
+ } else if (StringEndsWith(aFilename, NS_LITERAL_STRING(".app")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".cdr")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".dart")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".dc42")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".diskcopy42")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".dmg")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".dmgpart")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".dvdr")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".img")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".imgpart")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".iso")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".mpkg")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".ndif")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".pkg")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".smi")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".sparsebundle")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".sparseimage")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".toast")) ||
+ StringEndsWith(aFilename, NS_LITERAL_STRING(".udif"))) {
+ return ClientDownloadRequest::MAC_EXECUTABLE;
+ }
+
+ return ClientDownloadRequest::WIN_EXECUTABLE; // default to Windows binaries
+}
+
+nsresult
+PendingLookup::LookupNext()
+{
+ // We must call LookupNext or SendRemoteQuery upon return.
+ // Look up all of the URLs that could allow or block this download.
+ // Blocklist first.
+ if (mBlocklistCount > 0) {
+ return OnComplete(true, NS_OK,
+ nsIApplicationReputationService::VERDICT_DANGEROUS);
+ }
+ int index = mAnylistSpecs.Length() - 1;
+ nsCString spec;
+ if (index >= 0) {
+ // Check the source URI, referrer and redirect chain.
+ spec = mAnylistSpecs[index];
+ mAnylistSpecs.RemoveElementAt(index);
+ RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
+ return lookup->LookupSpec(spec, false);
+ }
+ // If any of mAnylistSpecs matched the blocklist, go ahead and block.
+ if (mBlocklistCount > 0) {
+ return OnComplete(true, NS_OK,
+ nsIApplicationReputationService::VERDICT_DANGEROUS);
+ }
+ // If any of mAnylistSpecs matched the allowlist, go ahead and pass.
+ if (mAllowlistCount > 0) {
+ return OnComplete(false, NS_OK);
+ }
+ // Only binary signatures remain.
+ index = mAllowlistSpecs.Length() - 1;
+ if (index >= 0) {
+ spec = mAllowlistSpecs[index];
+ LOG(("PendingLookup::LookupNext: checking %s on allowlist", spec.get()));
+ mAllowlistSpecs.RemoveElementAt(index);
+ RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
+ return lookup->LookupSpec(spec, true);
+ }
+ // There are no more URIs to check against local list. If the file is
+ // not eligible for remote lookup, bail.
+ if (!IsBinaryFile()) {
+ LOG(("Not eligible for remote lookups [this=%x]", this));
+ return OnComplete(false, NS_OK);
+ }
+ nsresult rv = SendRemoteQuery();
+ if (NS_FAILED(rv)) {
+ return OnComplete(false, rv);
+ }
+ return NS_OK;
+}
+
+nsCString
+PendingLookup::EscapeCertificateAttribute(const nsACString& aAttribute)
+{
+ // Escape '/' because it's a field separator, and '%' because Chrome does
+ nsCString escaped;
+ escaped.SetCapacity(aAttribute.Length());
+ for (unsigned int i = 0; i < aAttribute.Length(); ++i) {
+ if (aAttribute.Data()[i] == '%') {
+ escaped.AppendLiteral("%25");
+ } else if (aAttribute.Data()[i] == '/') {
+ escaped.AppendLiteral("%2F");
+ } else if (aAttribute.Data()[i] == ' ') {
+ escaped.AppendLiteral("%20");
+ } else {
+ escaped.Append(aAttribute.Data()[i]);
+ }
+ }
+ return escaped;
+}
+
+nsCString
+PendingLookup::EscapeFingerprint(const nsACString& aFingerprint)
+{
+ // Google's fingerprint doesn't have colons
+ nsCString escaped;
+ escaped.SetCapacity(aFingerprint.Length());
+ for (unsigned int i = 0; i < aFingerprint.Length(); ++i) {
+ if (aFingerprint.Data()[i] != ':') {
+ escaped.Append(aFingerprint.Data()[i]);
+ }
+ }
+ return escaped;
+}
+
+nsresult
+PendingLookup::GenerateWhitelistStringsForPair(
+ nsIX509Cert* certificate,
+ nsIX509Cert* issuer)
+{
+ // The whitelist paths have format:
+ // http://sb-ssl.google.com/safebrowsing/csd/certificate/<issuer_cert_fingerprint>[/CN=<cn>][/O=<org>][/OU=<unit>]
+ // Any of CN, O, or OU may be omitted from the whitelist entry. Unfortunately
+ // this is not publicly documented, but the Chrome implementation can be found
+ // here:
+ // https://code.google.com/p/chromium/codesearch#search/&q=GetCertificateWhitelistStrings
+ nsCString whitelistString(
+ "http://sb-ssl.google.com/safebrowsing/csd/certificate/");
+
+ nsString fingerprint;
+ nsresult rv = issuer->GetSha1Fingerprint(fingerprint);
+ NS_ENSURE_SUCCESS(rv, rv);
+ whitelistString.Append(
+ EscapeFingerprint(NS_ConvertUTF16toUTF8(fingerprint)));
+
+ nsString commonName;
+ rv = certificate->GetCommonName(commonName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!commonName.IsEmpty()) {
+ whitelistString.AppendLiteral("/CN=");
+ whitelistString.Append(
+ EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(commonName)));
+ }
+
+ nsString organization;
+ rv = certificate->GetOrganization(organization);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!organization.IsEmpty()) {
+ whitelistString.AppendLiteral("/O=");
+ whitelistString.Append(
+ EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(organization)));
+ }
+
+ nsString organizationalUnit;
+ rv = certificate->GetOrganizationalUnit(organizationalUnit);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!organizationalUnit.IsEmpty()) {
+ whitelistString.AppendLiteral("/OU=");
+ whitelistString.Append(
+ EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(organizationalUnit)));
+ }
+ LOG(("Whitelisting %s", whitelistString.get()));
+
+ mAllowlistSpecs.AppendElement(whitelistString);
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::GenerateWhitelistStringsForChain(
+ const safe_browsing::ClientDownloadRequest_CertificateChain& aChain)
+{
+ // We need a signing certificate and an issuer to construct a whitelist
+ // entry.
+ if (aChain.element_size() < 2) {
+ return NS_OK;
+ }
+
+ // Get the signer.
+ nsresult rv;
+ nsCOMPtr<nsIX509CertDB> certDB = do_GetService(NS_X509CERTDB_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIX509Cert> signer;
+ rv = certDB->ConstructX509(
+ const_cast<char *>(aChain.element(0).certificate().data()),
+ aChain.element(0).certificate().size(), getter_AddRefs(signer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int i = 1; i < aChain.element_size(); ++i) {
+ // Get the issuer.
+ nsCOMPtr<nsIX509Cert> issuer;
+ rv = certDB->ConstructX509(
+ const_cast<char *>(aChain.element(i).certificate().data()),
+ aChain.element(i).certificate().size(), getter_AddRefs(issuer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GenerateWhitelistStringsForPair(signer, issuer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::GenerateWhitelistStrings()
+{
+ for (int i = 0; i < mRequest.signature().certificate_chain_size(); ++i) {
+ nsresult rv = GenerateWhitelistStringsForChain(
+ mRequest.signature().certificate_chain(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::AddRedirects(nsIArray* aRedirects)
+{
+ uint32_t length = 0;
+ aRedirects->GetLength(&length);
+ LOG(("ApplicationReputation: Got %u redirects", length));
+ nsCOMPtr<nsISimpleEnumerator> iter;
+ nsresult rv = aRedirects->Enumerate(getter_AddRefs(iter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMoreRedirects = false;
+ rv = iter->HasMoreElements(&hasMoreRedirects);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (hasMoreRedirects) {
+ nsCOMPtr<nsISupports> supports;
+ rv = iter->GetNext(getter_AddRefs(supports));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> principal = do_QueryInterface(supports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = principal->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the spec to our list of local lookups. The most recent redirect is
+ // the last element.
+ nsCString spec;
+ rv = GetStrippedSpec(uri, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mAnylistSpecs.AppendElement(spec);
+ LOG(("ApplicationReputation: Appending redirect %s\n", spec.get()));
+
+ // Store the redirect information in the remote request.
+ ClientDownloadRequest_Resource* resource = mRequest.add_resources();
+ resource->set_url(spec.get());
+ resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT);
+
+ rv = iter->HasMoreElements(&hasMoreRedirects);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::StartLookup()
+{
+ mStartTime = TimeStamp::Now();
+ nsresult rv = DoLookupInternal();
+ if (NS_FAILED(rv)) {
+ return OnComplete(false, NS_OK);
+ }
+ return rv;
+}
+
+nsresult
+PendingLookup::GetSpecHash(nsACString& aSpec, nsACString& hexEncodedHash)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsICryptoHash> cryptoHash =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = cryptoHash->Init(nsICryptoHash::SHA256);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cryptoHash->Update(reinterpret_cast<const uint8_t*>(aSpec.BeginReading()),
+ aSpec.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString binaryHash;
+ rv = cryptoHash->Finish(false, binaryHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This needs to match HexEncode() in Chrome's
+ // src/base/strings/string_number_conversions.cc
+ static const char* const hex = "0123456789ABCDEF";
+ hexEncodedHash.SetCapacity(2 * binaryHash.Length());
+ for (size_t i = 0; i < binaryHash.Length(); ++i) {
+ auto c = static_cast<const unsigned char>(binaryHash[i]);
+ hexEncodedHash.Append(hex[(c >> 4) & 0x0F]);
+ hexEncodedHash.Append(hex[c & 0x0F]);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::GetStrippedSpec(nsIURI* aUri, nsACString& escaped)
+{
+ if (NS_WARN_IF(!aUri)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+ rv = aUri->GetScheme(escaped);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (escaped.EqualsLiteral("blob")) {
+ aUri->GetSpec(escaped);
+ LOG(("PendingLookup::GetStrippedSpec(): blob URL left unstripped as '%s' [this = %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return NS_OK;
+
+ } else if (escaped.EqualsLiteral("data")) {
+ // Replace URI with "data:<everything before comma>,SHA256(<whole URI>)"
+ aUri->GetSpec(escaped);
+ int32_t comma = escaped.FindChar(',');
+ if (comma > -1 &&
+ static_cast<nsCString::size_type>(comma) < escaped.Length() - 1) {
+ MOZ_ASSERT(comma > 4, "Data URIs start with 'data:'");
+ nsAutoCString hexEncodedHash;
+ rv = GetSpecHash(escaped, hexEncodedHash);
+ if (NS_SUCCEEDED(rv)) {
+ escaped.Truncate(comma + 1);
+ escaped.Append(hexEncodedHash);
+ }
+ }
+
+ LOG(("PendingLookup::GetStrippedSpec(): data URL stripped to '%s' [this = %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return NS_OK;
+ }
+
+ // If aURI is not an nsIURL, we do not want to check the lists or send a
+ // remote query.
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aUri, &rv);
+ if (NS_FAILED(rv)) {
+ LOG(("PendingLookup::GetStrippedSpec(): scheme '%s' is not supported [this = %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return rv;
+ }
+
+ nsCString temp;
+ rv = url->GetHostPort(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ escaped.Append("://");
+ escaped.Append(temp);
+
+ rv = url->GetFilePath(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nsIUrl.filePath starts with '/'
+ escaped.Append(temp);
+
+ LOG(("PendingLookup::GetStrippedSpec(): URL stripped to '%s' [this = %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::DoLookupInternal()
+{
+ // We want to check the target URI, its referrer, and associated redirects
+ // against the local lists.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = mQuery->GetSourceURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString sourceSpec;
+ rv = GetStrippedSpec(uri, sourceSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mAnylistSpecs.AppendElement(sourceSpec);
+
+ ClientDownloadRequest_Resource* resource = mRequest.add_resources();
+ resource->set_url(sourceSpec.get());
+ resource->set_type(ClientDownloadRequest::DOWNLOAD_URL);
+
+ nsCOMPtr<nsIURI> referrer = nullptr;
+ rv = mQuery->GetReferrerURI(getter_AddRefs(referrer));
+ if (referrer) {
+ nsCString referrerSpec;
+ rv = GetStrippedSpec(referrer, referrerSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mAnylistSpecs.AppendElement(referrerSpec);
+ resource->set_referrer(referrerSpec.get());
+ }
+ nsCOMPtr<nsIArray> redirects;
+ rv = mQuery->GetRedirects(getter_AddRefs(redirects));
+ if (redirects) {
+ AddRedirects(redirects);
+ } else {
+ LOG(("ApplicationReputation: Got no redirects [this=%p]", this));
+ }
+
+ // Extract the signature and parse certificates so we can use it to check
+ // whitelists.
+ nsCOMPtr<nsIArray> sigArray;
+ rv = mQuery->GetSignatureInfo(getter_AddRefs(sigArray));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (sigArray) {
+ rv = ParseCertificates(sigArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = GenerateWhitelistStrings();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Start the call chain.
+ return LookupNext();
+}
+
+nsresult
+PendingLookup::OnComplete(bool shouldBlock, nsresult rv, uint32_t verdict)
+{
+ MOZ_ASSERT(!shouldBlock ||
+ verdict != nsIApplicationReputationService::VERDICT_SAFE);
+
+ if (NS_FAILED(rv)) {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(rv, errorName);
+ LOG(("Failed sending remote query for application reputation "
+ "[rv = %s, this = %p]", errorName.get(), this));
+ }
+
+ if (mTimeoutTimer) {
+ mTimeoutTimer->Cancel();
+ mTimeoutTimer = nullptr;
+ }
+
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SHOULD_BLOCK,
+ shouldBlock);
+ double t = (TimeStamp::Now() - mStartTime).ToMilliseconds();
+ LOG(("Application Reputation verdict is %lu, obtained in %f ms [this = %p]",
+ verdict, t, this));
+ if (shouldBlock) {
+ LOG(("Application Reputation check failed, blocking bad binary [this = %p]",
+ this));
+ } else {
+ LOG(("Application Reputation check passed [this = %p]", this));
+ }
+ nsresult res = mCallback->OnComplete(shouldBlock, rv, verdict);
+ return res;
+}
+
+nsresult
+PendingLookup::ParseCertificates(nsIArray* aSigArray)
+{
+ // If we haven't been set for any reason, bail.
+ NS_ENSURE_ARG_POINTER(aSigArray);
+
+ // Binaries may be signed by multiple chains of certificates. If there are no
+ // chains, the binary is unsigned (or we were unable to extract signature
+ // information on a non-Windows platform)
+ nsCOMPtr<nsISimpleEnumerator> chains;
+ nsresult rv = aSigArray->Enumerate(getter_AddRefs(chains));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMoreChains = false;
+ rv = chains->HasMoreElements(&hasMoreChains);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ while (hasMoreChains) {
+ nsCOMPtr<nsISupports> chainSupports;
+ rv = chains->GetNext(getter_AddRefs(chainSupports));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIX509CertList> certList = do_QueryInterface(chainSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ safe_browsing::ClientDownloadRequest_CertificateChain* certChain =
+ mRequest.mutable_signature()->add_certificate_chain();
+ nsCOMPtr<nsISimpleEnumerator> chainElt;
+ rv = certList->GetEnumerator(getter_AddRefs(chainElt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Each chain may have multiple certificates.
+ bool hasMoreCerts = false;
+ rv = chainElt->HasMoreElements(&hasMoreCerts);
+ while (hasMoreCerts) {
+ nsCOMPtr<nsISupports> certSupports;
+ rv = chainElt->GetNext(getter_AddRefs(certSupports));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIX509Cert> cert = do_QueryInterface(certSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint8_t* data = nullptr;
+ uint32_t len = 0;
+ rv = cert->GetRawDER(&len, &data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add this certificate to the protobuf to send remotely.
+ certChain->add_element()->set_certificate(data, len);
+ free(data);
+
+ rv = chainElt->HasMoreElements(&hasMoreCerts);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = chains->HasMoreElements(&hasMoreChains);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (mRequest.signature().certificate_chain_size() > 0) {
+ mRequest.mutable_signature()->set_trusted(true);
+ }
+ return NS_OK;
+}
+
+nsresult
+PendingLookup::SendRemoteQuery()
+{
+ nsresult rv = SendRemoteQueryInternal();
+ if (NS_FAILED(rv)) {
+ return OnComplete(false, rv);
+ }
+ // SendRemoteQueryInternal has fired off the query and we call OnComplete in
+ // the nsIStreamListener.onStopRequest.
+ return rv;
+}
+
+nsresult
+PendingLookup::SendRemoteQueryInternal()
+{
+ // If we aren't supposed to do remote lookups, bail.
+ if (!Preferences::GetBool(PREF_SB_DOWNLOADS_REMOTE_ENABLED, false)) {
+ LOG(("Remote lookups are disabled [this = %p]", this));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ // If the remote lookup URL is empty or absent, bail.
+ nsCString serviceUrl;
+ NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_SB_APP_REP_URL, &serviceUrl),
+ NS_ERROR_NOT_AVAILABLE);
+ if (serviceUrl.IsEmpty()) {
+ LOG(("Remote lookup URL is empty [this = %p]", this));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // If the blocklist or allowlist is empty (so we couldn't do local lookups),
+ // bail
+ {
+ nsAutoCString table;
+ NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE,
+ &table),
+ NS_ERROR_NOT_AVAILABLE);
+ if (table.IsEmpty()) {
+ LOG(("Blocklist is empty [this = %p]", this));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+#ifdef XP_WIN
+ // The allowlist is only needed to do signature verification on Windows
+ {
+ nsAutoCString table;
+ NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE,
+ &table),
+ NS_ERROR_NOT_AVAILABLE);
+ if (table.IsEmpty()) {
+ LOG(("Allowlist is empty [this = %p]", this));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+#endif
+
+ LOG(("Sending remote query for application reputation [this = %p]",
+ this));
+ // We did not find a local result, so fire off the query to the
+ // application reputation service.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv;
+ rv = mQuery->GetSourceURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString spec;
+ rv = GetStrippedSpec(uri, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.set_url(spec.get());
+
+ uint32_t fileSize;
+ rv = mQuery->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.set_length(fileSize);
+ // We have no way of knowing whether or not a user initiated the
+ // download. Set it to true to lessen the chance of false positives.
+ mRequest.set_user_initiated(true);
+
+ nsCString locale;
+ NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_GENERAL_LOCALE, &locale),
+ NS_ERROR_NOT_AVAILABLE);
+ mRequest.set_locale(locale.get());
+ nsCString sha256Hash;
+ rv = mQuery->GetSha256Hash(sha256Hash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.mutable_digests()->set_sha256(sha256Hash.Data());
+ nsString fileName;
+ rv = mQuery->GetSuggestedFileName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.set_file_basename(NS_ConvertUTF16toUTF8(fileName).get());
+ mRequest.set_download_type(GetDownloadType(fileName));
+
+ if (mRequest.signature().trusted()) {
+ LOG(("Got signed binary for remote application reputation check "
+ "[this = %p]", this));
+ } else {
+ LOG(("Got unsigned binary for remote application reputation check "
+ "[this = %p]", this));
+ }
+
+ // Serialize the protocol buffer to a string. This can only fail if we are
+ // out of memory, or if the protocol buffer req is missing required fields
+ // (only the URL for now).
+ std::string serialized;
+ if (!mRequest.SerializeToString(&serialized)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ LOG(("Serialized protocol buffer [this = %p]: (length=%d) %s", this,
+ serialized.length(), serialized.c_str()));
+
+ // Set the input stream to the serialized protocol buffer
+ nsCOMPtr<nsIStringInputStream> sstream =
+ do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = sstream->SetData(serialized.c_str(), serialized.length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set up the channel to transmit the request to the service.
+ nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ rv = ios->NewChannel2(serviceUrl,
+ nullptr,
+ nullptr,
+ nullptr, // aLoadingNode
+ nsContentUtils::GetSystemPrincipal(),
+ nullptr, // aTriggeringPrincipal
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER,
+ getter_AddRefs(mChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
+ if (loadInfo) {
+ loadInfo->SetOriginAttributes(
+ mozilla::NeckoOriginAttributes(NECKO_SAFEBROWSING_APP_ID, false));
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozilla::Unused << httpChannel;
+
+ // Upload the protobuf to the application reputation service.
+ nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = uploadChannel->ExplicitSetUploadStream(sstream,
+ NS_LITERAL_CSTRING("application/octet-stream"), serialized.size(),
+ NS_LITERAL_CSTRING("POST"), false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set the Safebrowsing cookie jar, so that the regular Google cookie is not
+ // sent with this request. See bug 897516.
+ DocShellOriginAttributes attrs;
+ attrs.mAppId = NECKO_SAFEBROWSING_APP_ID;
+ nsCOMPtr<nsIInterfaceRequestor> loadContext = new mozilla::LoadContext(attrs);
+ rv = mChannel->SetNotificationCallbacks(loadContext);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t timeoutMs = Preferences::GetUint(PREF_SB_DOWNLOADS_REMOTE_TIMEOUT, 10000);
+ mTimeoutTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ mTimeoutTimer->InitWithCallback(this, timeoutMs, nsITimer::TYPE_ONE_SHOT);
+
+ rv = mChannel->AsyncOpen2(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PendingLookup::Notify(nsITimer* aTimer)
+{
+ LOG(("Remote lookup timed out [this = %p]", this));
+ MOZ_ASSERT(aTimer == mTimeoutTimer);
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT,
+ true);
+ mChannel->Cancel(NS_ERROR_NET_TIMEOUT);
+ mTimeoutTimer->Cancel();
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIObserver implementation
+NS_IMETHODIMP
+PendingLookup::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ if (!strcmp(aTopic, "quit-application")) {
+ if (mTimeoutTimer) {
+ mTimeoutTimer->Cancel();
+ mTimeoutTimer = nullptr;
+ }
+ if (mChannel) {
+ mChannel->Cancel(NS_ERROR_ABORT);
+ }
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIStreamListener
+static nsresult
+AppendSegmentToString(nsIInputStream* inputStream,
+ void *closure,
+ const char *rawSegment,
+ uint32_t toOffset,
+ uint32_t count,
+ uint32_t *writeCount) {
+ nsAutoCString* decodedData = static_cast<nsAutoCString*>(closure);
+ decodedData->Append(rawSegment, count);
+ *writeCount = count;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PendingLookup::OnDataAvailable(nsIRequest *aRequest,
+ nsISupports *aContext,
+ nsIInputStream *aStream,
+ uint64_t offset,
+ uint32_t count) {
+ uint32_t read;
+ return aStream->ReadSegments(AppendSegmentToString, &mResponse, count, &read);
+}
+
+NS_IMETHODIMP
+PendingLookup::OnStartRequest(nsIRequest *aRequest,
+ nsISupports *aContext) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PendingLookup::OnStopRequest(nsIRequest *aRequest,
+ nsISupports *aContext,
+ nsresult aResult) {
+ NS_ENSURE_STATE(mCallback);
+
+ bool shouldBlock = false;
+ uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE;
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT,
+ false);
+
+ nsresult rv = OnStopRequestInternal(aRequest, aContext, aResult,
+ &shouldBlock, &verdict);
+ OnComplete(shouldBlock, rv, verdict);
+ return rv;
+}
+
+nsresult
+PendingLookup::OnStopRequestInternal(nsIRequest *aRequest,
+ nsISupports *aContext,
+ nsresult aResult,
+ bool* aShouldBlock,
+ uint32_t* aVerdict) {
+ if (NS_FAILED(aResult)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ return aResult;
+ }
+
+ *aShouldBlock = false;
+ *aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
+ nsresult rv;
+ nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
+ if (NS_FAILED(rv)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ return rv;
+ }
+
+ uint32_t status = 0;
+ rv = channel->GetResponseStatus(&status);
+ if (NS_FAILED(rv)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ return rv;
+ }
+
+ if (status != 200) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ std::string buf(mResponse.Data(), mResponse.Length());
+ safe_browsing::ClientDownloadResponse response;
+ if (!response.ParseFromString(buf)) {
+ LOG(("Invalid protocol buffer response [this = %p]: %s", this, buf.c_str()));
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_INVALID);
+ return NS_ERROR_CANNOT_CONVERT_DATA;
+ }
+
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_VALID);
+ // Clamp responses 0-7, we only know about 0-4 for now.
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER_VERDICT,
+ std::min<uint32_t>(response.verdict(), 7));
+ switch(response.verdict()) {
+ case safe_browsing::ClientDownloadResponse::DANGEROUS:
+ *aShouldBlock = Preferences::GetBool(PREF_BLOCK_DANGEROUS, true);
+ *aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS;
+ break;
+ case safe_browsing::ClientDownloadResponse::DANGEROUS_HOST:
+ *aShouldBlock = Preferences::GetBool(PREF_BLOCK_DANGEROUS_HOST, true);
+ *aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS_HOST;
+ break;
+ case safe_browsing::ClientDownloadResponse::POTENTIALLY_UNWANTED:
+ *aShouldBlock = Preferences::GetBool(PREF_BLOCK_POTENTIALLY_UNWANTED, false);
+ *aVerdict = nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED;
+ break;
+ case safe_browsing::ClientDownloadResponse::UNCOMMON:
+ *aShouldBlock = Preferences::GetBool(PREF_BLOCK_UNCOMMON, false);
+ *aVerdict = nsIApplicationReputationService::VERDICT_UNCOMMON;
+ break;
+ default:
+ // Treat everything else as safe
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(ApplicationReputationService,
+ nsIApplicationReputationService)
+
+ApplicationReputationService*
+ ApplicationReputationService::gApplicationReputationService = nullptr;
+
+ApplicationReputationService*
+ApplicationReputationService::GetSingleton()
+{
+ if (gApplicationReputationService) {
+ NS_ADDREF(gApplicationReputationService);
+ return gApplicationReputationService;
+ }
+
+ // We're not initialized yet.
+ gApplicationReputationService = new ApplicationReputationService();
+ if (gApplicationReputationService) {
+ NS_ADDREF(gApplicationReputationService);
+ }
+
+ return gApplicationReputationService;
+}
+
+ApplicationReputationService::ApplicationReputationService()
+{
+ LOG(("Application reputation service started up"));
+}
+
+ApplicationReputationService::~ApplicationReputationService() {
+ LOG(("Application reputation service shutting down"));
+ MOZ_ASSERT(gApplicationReputationService == this);
+ gApplicationReputationService = nullptr;
+}
+
+NS_IMETHODIMP
+ApplicationReputationService::QueryReputation(
+ nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback) {
+ LOG(("Starting application reputation check [query=%p]", aQuery));
+ NS_ENSURE_ARG_POINTER(aQuery);
+ NS_ENSURE_ARG_POINTER(aCallback);
+
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_COUNT, true);
+ nsresult rv = QueryReputationInternal(aQuery, aCallback);
+ if (NS_FAILED(rv)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SHOULD_BLOCK,
+ false);
+ aCallback->OnComplete(false, rv,
+ nsIApplicationReputationService::VERDICT_SAFE);
+ }
+ return NS_OK;
+}
+
+nsresult ApplicationReputationService::QueryReputationInternal(
+ nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback) {
+ nsresult rv;
+ // If malware checks aren't enabled, don't query application reputation.
+ if (!Preferences::GetBool(PREF_SB_MALWARE_ENABLED, false)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!Preferences::GetBool(PREF_SB_DOWNLOADS_ENABLED, false)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = aQuery->GetSourceURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Bail if the URI hasn't been set.
+ NS_ENSURE_STATE(uri);
+
+ // Create a new pending lookup and start the call chain.
+ RefPtr<PendingLookup> lookup(new PendingLookup(aQuery, aCallback));
+ NS_ENSURE_STATE(lookup);
+
+ // Add an observer for shutdown
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) {
+ return NS_ERROR_FAILURE;
+ }
+
+ observerService->AddObserver(lookup, "quit-application", false);
+ return lookup->StartLookup();
+}
diff --git a/toolkit/components/downloads/ApplicationReputation.h b/toolkit/components/downloads/ApplicationReputation.h
new file mode 100644
index 0000000000..0ed68d6163
--- /dev/null
+++ b/toolkit/components/downloads/ApplicationReputation.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 ApplicationReputation_h__
+#define ApplicationReputation_h__
+
+#include "nsIApplicationReputation.h"
+#include "nsIRequestObserver.h"
+#include "nsIStreamListener.h"
+#include "nsISupports.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozilla/Logging.h"
+
+class nsIRequest;
+class PendingDBLookup;
+class PendingLookup;
+
+class ApplicationReputationService final :
+ public nsIApplicationReputationService {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAPPLICATIONREPUTATIONSERVICE
+
+public:
+ static ApplicationReputationService* GetSingleton();
+
+private:
+ friend class PendingLookup;
+ friend class PendingDBLookup;
+ /**
+ * Global singleton object for holding this factory service.
+ */
+ static ApplicationReputationService* gApplicationReputationService;
+ /**
+ * MOZ_LOG=ApplicationReputation:5
+ */
+ static mozilla::LazyLogModule prlog;
+ /**
+ * This is a singleton, so disallow construction.
+ */
+ ApplicationReputationService();
+ ~ApplicationReputationService();
+ /**
+ * Wrapper function for QueryReputation that makes it easier to ensure the
+ * callback is called.
+ */
+ nsresult QueryReputationInternal(nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback);
+};
+#endif /* ApplicationReputation_h__ */
diff --git a/toolkit/components/downloads/SQLFunctions.cpp b/toolkit/components/downloads/SQLFunctions.cpp
new file mode 100644
index 0000000000..8f2d3e77bc
--- /dev/null
+++ b/toolkit/components/downloads/SQLFunctions.cpp
@@ -0,0 +1,146 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/storage.h"
+#include "mozilla/storage/Variant.h"
+#include "mozilla/mozalloc.h"
+#include "nsString.h"
+#include "SQLFunctions.h"
+#include "nsUTF8Utils.h"
+#include "plbase64.h"
+#include "prio.h"
+
+#ifdef XP_WIN
+#include <windows.h>
+#include <wincrypt.h>
+#endif
+
+// The length of guids that are used by the download manager
+#define GUID_LENGTH 12
+
+namespace mozilla {
+namespace downloads {
+
+// Keep this file in sync with the GUID-related code in toolkit/places/SQLFunctions.cpp
+// and toolkit/places/Helpers.cpp!
+
+////////////////////////////////////////////////////////////////////////////////
+//// GUID Creation Function
+
+//////////////////////////////////////////////////////////////////////////////
+//// GenerateGUIDFunction
+
+/* static */
+nsresult
+GenerateGUIDFunction::create(mozIStorageConnection *aDBConn)
+{
+ RefPtr<GenerateGUIDFunction> function = new GenerateGUIDFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("generate_guid"), 0, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(
+ GenerateGUIDFunction,
+ mozIStorageFunction
+)
+
+static
+nsresult
+Base64urlEncode(const uint8_t* aBytes,
+ uint32_t aNumBytes,
+ nsCString& _result)
+{
+ // SetLength does not set aside space for null termination. PL_Base64Encode
+ // will not null terminate, however, nsCStrings must be null terminated. As a
+ // result, we set the capacity to be one greater than what we need, and the
+ // length to our desired length.
+ uint32_t length = (aNumBytes + 2) / 3 * 4; // +2 due to integer math.
+ NS_ENSURE_TRUE(_result.SetCapacity(length + 1, mozilla::fallible),
+ NS_ERROR_OUT_OF_MEMORY);
+ _result.SetLength(length);
+ (void)PL_Base64Encode(reinterpret_cast<const char*>(aBytes), aNumBytes,
+ _result.BeginWriting());
+
+ // base64url encoding is defined in RFC 4648. It replaces the last two
+ // alphabet characters of base64 encoding with '-' and '_' respectively.
+ _result.ReplaceChar('+', '-');
+ _result.ReplaceChar('/', '_');
+ return NS_OK;
+}
+
+static
+nsresult
+GenerateRandomBytes(uint32_t aSize,
+ uint8_t* _buffer)
+{
+ // On Windows, we'll use its built-in cryptographic API.
+#if defined(XP_WIN)
+ HCRYPTPROV cryptoProvider;
+ BOOL rc = CryptAcquireContext(&cryptoProvider, 0, 0, PROV_RSA_FULL,
+ CRYPT_VERIFYCONTEXT | CRYPT_SILENT);
+ if (rc) {
+ rc = CryptGenRandom(cryptoProvider, aSize, _buffer);
+ (void)CryptReleaseContext(cryptoProvider, 0);
+ }
+ return rc ? NS_OK : NS_ERROR_FAILURE;
+
+ // On Unix, we'll just read in from /dev/urandom.
+#elif defined(XP_UNIX)
+ NS_ENSURE_ARG_MAX(aSize, INT32_MAX);
+ PRFileDesc* urandom = PR_Open("/dev/urandom", PR_RDONLY, 0);
+ nsresult rv = NS_ERROR_FAILURE;
+ if (urandom) {
+ int32_t bytesRead = PR_Read(urandom, _buffer, aSize);
+ if (bytesRead == static_cast<int32_t>(aSize)) {
+ rv = NS_OK;
+ }
+ (void)PR_Close(urandom);
+ }
+ return rv;
+#endif
+}
+
+nsresult
+GenerateGUID(nsCString& _guid)
+{
+ _guid.Truncate();
+
+ // Request raw random bytes and base64url encode them. For each set of three
+ // bytes, we get one character.
+ const uint32_t kRequiredBytesLength =
+ static_cast<uint32_t>(GUID_LENGTH / 4 * 3);
+
+ uint8_t buffer[kRequiredBytesLength];
+ nsresult rv = GenerateRandomBytes(kRequiredBytesLength, buffer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Base64urlEncode(buffer, kRequiredBytesLength, _guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(_guid.Length() == GUID_LENGTH, "GUID is not the right size!");
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// mozIStorageFunction
+
+NS_IMETHODIMP
+GenerateGUIDFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+{
+ nsAutoCString guid;
+ nsresult rv = GenerateGUID(guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*_result = new mozilla::storage::UTF8TextVariant(guid));
+ return NS_OK;
+}
+
+} // namespace downloads
+} // namespace mozilla
diff --git a/toolkit/components/downloads/SQLFunctions.h b/toolkit/components/downloads/SQLFunctions.h
new file mode 100644
index 0000000000..ae207788c2
--- /dev/null
+++ b/toolkit/components/downloads/SQLFunctions.h
@@ -0,0 +1,46 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla_downloads_SQLFunctions_h
+#define mozilla_downloads_SQLFunctions_h
+
+#include "mozIStorageFunction.h"
+#include "mozilla/Attributes.h"
+
+class nsCString;
+class mozIStorageConnection;
+
+namespace mozilla {
+namespace downloads {
+
+/**
+ * SQL function to generate a GUID for a place or bookmark item. This is just
+ * a wrapper around GenerateGUID in SQLFunctions.cpp.
+ *
+ * @return a guid for the item.
+ * @see toolkit/components/places/SQLFunctions.h - keep this in sync
+ */
+class GenerateGUIDFunction final : public mozIStorageFunction
+{
+ ~GenerateGUIDFunction() {}
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+};
+
+nsresult GenerateGUID(nsCString& _guid);
+
+} // namespace downloads
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/downloads/chromium/LICENSE b/toolkit/components/downloads/chromium/LICENSE
new file mode 100644
index 0000000000..a32e00ce6b
--- /dev/null
+++ b/toolkit/components/downloads/chromium/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.cc b/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.cc
new file mode 100644
index 0000000000..d52b822c05
--- /dev/null
+++ b/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.cc
@@ -0,0 +1,20037 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: chromium/chrome/common/safe_browsing/csd.proto
+
+#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION
+#include "chromium/chrome/common/safe_browsing/csd.pb.h"
+
+#include <algorithm>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+// @@protoc_insertion_point(includes)
+
+namespace safe_browsing {
+
+void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto() {
+ delete ChromeUserPopulation::default_instance_;
+ delete ClientPhishingRequest::default_instance_;
+ delete ClientPhishingRequest_Feature::default_instance_;
+ delete ClientPhishingResponse::default_instance_;
+ delete ClientMalwareRequest::default_instance_;
+ delete ClientMalwareRequest_UrlInfo::default_instance_;
+ delete ClientMalwareResponse::default_instance_;
+ delete ClientDownloadRequest::default_instance_;
+ delete ClientDownloadRequest_Digests::default_instance_;
+ delete ClientDownloadRequest_Resource::default_instance_;
+ delete ClientDownloadRequest_CertificateChain::default_instance_;
+ delete ClientDownloadRequest_CertificateChain_Element::default_instance_;
+ delete ClientDownloadRequest_ExtendedAttr::default_instance_;
+ delete ClientDownloadRequest_SignatureInfo::default_instance_;
+ delete ClientDownloadRequest_PEImageHeaders::default_instance_;
+ delete ClientDownloadRequest_PEImageHeaders_DebugData::default_instance_;
+ delete ClientDownloadRequest_MachOHeaders::default_instance_;
+ delete ClientDownloadRequest_MachOHeaders_LoadCommand::default_instance_;
+ delete ClientDownloadRequest_ImageHeaders::default_instance_;
+ delete ClientDownloadRequest_ArchivedBinary::default_instance_;
+ delete ClientDownloadRequest_URLChainEntry::default_instance_;
+ delete ClientDownloadResponse::default_instance_;
+ delete ClientDownloadResponse_MoreInfo::default_instance_;
+ delete ClientDownloadReport::default_instance_;
+ delete ClientDownloadReport_UserInformation::default_instance_;
+ delete ClientUploadResponse::default_instance_;
+ delete ClientIncidentReport::default_instance_;
+ delete ClientIncidentReport_IncidentData::default_instance_;
+ delete ClientIncidentReport_IncidentData_TrackedPreferenceIncident::default_instance_;
+ delete ClientIncidentReport_IncidentData_BinaryIntegrityIncident::default_instance_;
+ delete ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::default_instance_;
+ delete ClientIncidentReport_IncidentData_BlacklistLoadIncident::default_instance_;
+ delete ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::default_instance_;
+ delete ClientIncidentReport_IncidentData_ResourceRequestIncident::default_instance_;
+ delete ClientIncidentReport_IncidentData_SuspiciousModuleIncident::default_instance_;
+ delete ClientIncidentReport_DownloadDetails::default_instance_;
+ delete ClientIncidentReport_EnvironmentData::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_OS::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_OS_RegistryValue::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_OS_RegistryKey::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Machine::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Process::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Process_Patch::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Process_NetworkProvider::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Process_Dll::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Process_ModuleState::default_instance_;
+ delete ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::default_instance_;
+ delete ClientIncidentReport_ExtensionData::default_instance_;
+ delete ClientIncidentReport_ExtensionData_ExtensionInfo::default_instance_;
+ delete ClientIncidentReport_NonBinaryDownloadDetails::default_instance_;
+ delete ClientIncidentResponse::default_instance_;
+ delete ClientIncidentResponse_EnvironmentRequest::default_instance_;
+ delete DownloadMetadata::default_instance_;
+ delete ClientSafeBrowsingReportRequest::default_instance_;
+ delete ClientSafeBrowsingReportRequest_HTTPHeader::default_instance_;
+ delete ClientSafeBrowsingReportRequest_HTTPRequest::default_instance_;
+ delete ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::default_instance_;
+ delete ClientSafeBrowsingReportRequest_HTTPResponse::default_instance_;
+ delete ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::default_instance_;
+ delete ClientSafeBrowsingReportRequest_Resource::default_instance_;
+}
+
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl() {
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+#else
+void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto() {
+ static bool already_here = false;
+ if (already_here) return;
+ already_here = true;
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+#endif
+ ChromeUserPopulation::default_instance_ = new ChromeUserPopulation();
+ ClientPhishingRequest::default_instance_ = new ClientPhishingRequest();
+ ClientPhishingRequest_Feature::default_instance_ = new ClientPhishingRequest_Feature();
+ ClientPhishingResponse::default_instance_ = new ClientPhishingResponse();
+ ClientMalwareRequest::default_instance_ = new ClientMalwareRequest();
+ ClientMalwareRequest_UrlInfo::default_instance_ = new ClientMalwareRequest_UrlInfo();
+ ClientMalwareResponse::default_instance_ = new ClientMalwareResponse();
+ ClientDownloadRequest::default_instance_ = new ClientDownloadRequest();
+ ClientDownloadRequest_Digests::default_instance_ = new ClientDownloadRequest_Digests();
+ ClientDownloadRequest_Resource::default_instance_ = new ClientDownloadRequest_Resource();
+ ClientDownloadRequest_CertificateChain::default_instance_ = new ClientDownloadRequest_CertificateChain();
+ ClientDownloadRequest_CertificateChain_Element::default_instance_ = new ClientDownloadRequest_CertificateChain_Element();
+ ClientDownloadRequest_ExtendedAttr::default_instance_ = new ClientDownloadRequest_ExtendedAttr();
+ ClientDownloadRequest_SignatureInfo::default_instance_ = new ClientDownloadRequest_SignatureInfo();
+ ClientDownloadRequest_PEImageHeaders::default_instance_ = new ClientDownloadRequest_PEImageHeaders();
+ ClientDownloadRequest_PEImageHeaders_DebugData::default_instance_ = new ClientDownloadRequest_PEImageHeaders_DebugData();
+ ClientDownloadRequest_MachOHeaders::default_instance_ = new ClientDownloadRequest_MachOHeaders();
+ ClientDownloadRequest_MachOHeaders_LoadCommand::default_instance_ = new ClientDownloadRequest_MachOHeaders_LoadCommand();
+ ClientDownloadRequest_ImageHeaders::default_instance_ = new ClientDownloadRequest_ImageHeaders();
+ ClientDownloadRequest_ArchivedBinary::default_instance_ = new ClientDownloadRequest_ArchivedBinary();
+ ClientDownloadRequest_URLChainEntry::default_instance_ = new ClientDownloadRequest_URLChainEntry();
+ ClientDownloadResponse::default_instance_ = new ClientDownloadResponse();
+ ClientDownloadResponse_MoreInfo::default_instance_ = new ClientDownloadResponse_MoreInfo();
+ ClientDownloadReport::default_instance_ = new ClientDownloadReport();
+ ClientDownloadReport_UserInformation::default_instance_ = new ClientDownloadReport_UserInformation();
+ ClientUploadResponse::default_instance_ = new ClientUploadResponse();
+ ClientIncidentReport::default_instance_ = new ClientIncidentReport();
+ ClientIncidentReport_IncidentData::default_instance_ = new ClientIncidentReport_IncidentData();
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident::default_instance_ = new ClientIncidentReport_IncidentData_TrackedPreferenceIncident();
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident::default_instance_ = new ClientIncidentReport_IncidentData_BinaryIntegrityIncident();
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::default_instance_ = new ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile();
+ ClientIncidentReport_IncidentData_BlacklistLoadIncident::default_instance_ = new ClientIncidentReport_IncidentData_BlacklistLoadIncident();
+ ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::default_instance_ = new ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident();
+ ClientIncidentReport_IncidentData_ResourceRequestIncident::default_instance_ = new ClientIncidentReport_IncidentData_ResourceRequestIncident();
+ ClientIncidentReport_IncidentData_SuspiciousModuleIncident::default_instance_ = new ClientIncidentReport_IncidentData_SuspiciousModuleIncident();
+ ClientIncidentReport_DownloadDetails::default_instance_ = new ClientIncidentReport_DownloadDetails();
+ ClientIncidentReport_EnvironmentData::default_instance_ = new ClientIncidentReport_EnvironmentData();
+ ClientIncidentReport_EnvironmentData_OS::default_instance_ = new ClientIncidentReport_EnvironmentData_OS();
+ ClientIncidentReport_EnvironmentData_OS_RegistryValue::default_instance_ = new ClientIncidentReport_EnvironmentData_OS_RegistryValue();
+ ClientIncidentReport_EnvironmentData_OS_RegistryKey::default_instance_ = new ClientIncidentReport_EnvironmentData_OS_RegistryKey();
+ ClientIncidentReport_EnvironmentData_Machine::default_instance_ = new ClientIncidentReport_EnvironmentData_Machine();
+ ClientIncidentReport_EnvironmentData_Process::default_instance_ = new ClientIncidentReport_EnvironmentData_Process();
+ ClientIncidentReport_EnvironmentData_Process_Patch::default_instance_ = new ClientIncidentReport_EnvironmentData_Process_Patch();
+ ClientIncidentReport_EnvironmentData_Process_NetworkProvider::default_instance_ = new ClientIncidentReport_EnvironmentData_Process_NetworkProvider();
+ ClientIncidentReport_EnvironmentData_Process_Dll::default_instance_ = new ClientIncidentReport_EnvironmentData_Process_Dll();
+ ClientIncidentReport_EnvironmentData_Process_ModuleState::default_instance_ = new ClientIncidentReport_EnvironmentData_Process_ModuleState();
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::default_instance_ = new ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification();
+ ClientIncidentReport_ExtensionData::default_instance_ = new ClientIncidentReport_ExtensionData();
+ ClientIncidentReport_ExtensionData_ExtensionInfo::default_instance_ = new ClientIncidentReport_ExtensionData_ExtensionInfo();
+ ClientIncidentReport_NonBinaryDownloadDetails::default_instance_ = new ClientIncidentReport_NonBinaryDownloadDetails();
+ ClientIncidentResponse::default_instance_ = new ClientIncidentResponse();
+ ClientIncidentResponse_EnvironmentRequest::default_instance_ = new ClientIncidentResponse_EnvironmentRequest();
+ DownloadMetadata::default_instance_ = new DownloadMetadata();
+ ClientSafeBrowsingReportRequest::default_instance_ = new ClientSafeBrowsingReportRequest();
+ ClientSafeBrowsingReportRequest_HTTPHeader::default_instance_ = new ClientSafeBrowsingReportRequest_HTTPHeader();
+ ClientSafeBrowsingReportRequest_HTTPRequest::default_instance_ = new ClientSafeBrowsingReportRequest_HTTPRequest();
+ ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::default_instance_ = new ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine();
+ ClientSafeBrowsingReportRequest_HTTPResponse::default_instance_ = new ClientSafeBrowsingReportRequest_HTTPResponse();
+ ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::default_instance_ = new ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine();
+ ClientSafeBrowsingReportRequest_Resource::default_instance_ = new ClientSafeBrowsingReportRequest_Resource();
+ ChromeUserPopulation::default_instance_->InitAsDefaultInstance();
+ ClientPhishingRequest::default_instance_->InitAsDefaultInstance();
+ ClientPhishingRequest_Feature::default_instance_->InitAsDefaultInstance();
+ ClientPhishingResponse::default_instance_->InitAsDefaultInstance();
+ ClientMalwareRequest::default_instance_->InitAsDefaultInstance();
+ ClientMalwareRequest_UrlInfo::default_instance_->InitAsDefaultInstance();
+ ClientMalwareResponse::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_Digests::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_Resource::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_CertificateChain::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_CertificateChain_Element::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_ExtendedAttr::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_SignatureInfo::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_PEImageHeaders::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_PEImageHeaders_DebugData::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_MachOHeaders::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_MachOHeaders_LoadCommand::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_ImageHeaders::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_ArchivedBinary::default_instance_->InitAsDefaultInstance();
+ ClientDownloadRequest_URLChainEntry::default_instance_->InitAsDefaultInstance();
+ ClientDownloadResponse::default_instance_->InitAsDefaultInstance();
+ ClientDownloadResponse_MoreInfo::default_instance_->InitAsDefaultInstance();
+ ClientDownloadReport::default_instance_->InitAsDefaultInstance();
+ ClientDownloadReport_UserInformation::default_instance_->InitAsDefaultInstance();
+ ClientUploadResponse::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_BlacklistLoadIncident::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_ResourceRequestIncident::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_IncidentData_SuspiciousModuleIncident::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_DownloadDetails::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_OS::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_OS_RegistryValue::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_OS_RegistryKey::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Machine::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Process::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Process_Patch::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Process_NetworkProvider::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Process_Dll::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Process_ModuleState::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_ExtensionData::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_ExtensionData_ExtensionInfo::default_instance_->InitAsDefaultInstance();
+ ClientIncidentReport_NonBinaryDownloadDetails::default_instance_->InitAsDefaultInstance();
+ ClientIncidentResponse::default_instance_->InitAsDefaultInstance();
+ ClientIncidentResponse_EnvironmentRequest::default_instance_->InitAsDefaultInstance();
+ DownloadMetadata::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest_HTTPHeader::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest_HTTPRequest::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest_HTTPResponse::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::default_instance_->InitAsDefaultInstance();
+ ClientSafeBrowsingReportRequest_Resource::default_instance_->InitAsDefaultInstance();
+ ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto);
+}
+
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_once_);
+void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto() {
+ ::google::protobuf::GoogleOnceInit(&protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_once_,
+ &protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl);
+}
+#else
+// Force AddDescriptors() to be called at static initialization time.
+struct StaticDescriptorInitializer_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto {
+ StaticDescriptorInitializer_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto() {
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ }
+} static_descriptor_initializer_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_;
+#endif
+
+// ===================================================================
+
+bool ChromeUserPopulation_UserPopulation_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation::UNKNOWN_USER_POPULATION;
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation::SAFE_BROWSING;
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation::EXTENDED_REPORTING;
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation::UserPopulation_MIN;
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation::UserPopulation_MAX;
+const int ChromeUserPopulation::UserPopulation_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ChromeUserPopulation::kUserPopulationFieldNumber;
+#endif // !_MSC_VER
+
+ChromeUserPopulation::ChromeUserPopulation()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ChromeUserPopulation)
+}
+
+void ChromeUserPopulation::InitAsDefaultInstance() {
+}
+
+ChromeUserPopulation::ChromeUserPopulation(const ChromeUserPopulation& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ChromeUserPopulation)
+}
+
+void ChromeUserPopulation::SharedCtor() {
+ _cached_size_ = 0;
+ user_population_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ChromeUserPopulation::~ChromeUserPopulation() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ChromeUserPopulation)
+ SharedDtor();
+}
+
+void ChromeUserPopulation::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ChromeUserPopulation::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ChromeUserPopulation& ChromeUserPopulation::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ChromeUserPopulation* ChromeUserPopulation::default_instance_ = NULL;
+
+ChromeUserPopulation* ChromeUserPopulation::New() const {
+ return new ChromeUserPopulation;
+}
+
+void ChromeUserPopulation::Clear() {
+ user_population_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ChromeUserPopulation::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ChromeUserPopulation)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ChromeUserPopulation.UserPopulation user_population = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ChromeUserPopulation_UserPopulation_IsValid(value)) {
+ set_user_population(static_cast< ::safe_browsing::ChromeUserPopulation_UserPopulation >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ChromeUserPopulation)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ChromeUserPopulation)
+ return false;
+#undef DO_
+}
+
+void ChromeUserPopulation::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ChromeUserPopulation)
+ // optional .safe_browsing.ChromeUserPopulation.UserPopulation user_population = 1;
+ if (has_user_population()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->user_population(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ChromeUserPopulation)
+}
+
+int ChromeUserPopulation::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ChromeUserPopulation.UserPopulation user_population = 1;
+ if (has_user_population()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->user_population());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ChromeUserPopulation::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ChromeUserPopulation*>(&from));
+}
+
+void ChromeUserPopulation::MergeFrom(const ChromeUserPopulation& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_user_population()) {
+ set_user_population(from.user_population());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ChromeUserPopulation::CopyFrom(const ChromeUserPopulation& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ChromeUserPopulation::IsInitialized() const {
+
+ return true;
+}
+
+void ChromeUserPopulation::Swap(ChromeUserPopulation* other) {
+ if (other != this) {
+ std::swap(user_population_, other->user_population_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ChromeUserPopulation::GetTypeName() const {
+ return "safe_browsing.ChromeUserPopulation";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientPhishingRequest_Feature::kNameFieldNumber;
+const int ClientPhishingRequest_Feature::kValueFieldNumber;
+#endif // !_MSC_VER
+
+ClientPhishingRequest_Feature::ClientPhishingRequest_Feature()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientPhishingRequest.Feature)
+}
+
+void ClientPhishingRequest_Feature::InitAsDefaultInstance() {
+}
+
+ClientPhishingRequest_Feature::ClientPhishingRequest_Feature(const ClientPhishingRequest_Feature& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientPhishingRequest.Feature)
+}
+
+void ClientPhishingRequest_Feature::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ value_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientPhishingRequest_Feature::~ClientPhishingRequest_Feature() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientPhishingRequest.Feature)
+ SharedDtor();
+}
+
+void ClientPhishingRequest_Feature::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientPhishingRequest_Feature::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientPhishingRequest_Feature& ClientPhishingRequest_Feature::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientPhishingRequest_Feature* ClientPhishingRequest_Feature::default_instance_ = NULL;
+
+ClientPhishingRequest_Feature* ClientPhishingRequest_Feature::New() const {
+ return new ClientPhishingRequest_Feature;
+}
+
+void ClientPhishingRequest_Feature::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ value_ = 0;
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientPhishingRequest_Feature::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientPhishingRequest.Feature)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(17)) goto parse_value;
+ break;
+ }
+
+ // required double value = 2;
+ case 2: {
+ if (tag == 17) {
+ parse_value:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ double, ::google::protobuf::internal::WireFormatLite::TYPE_DOUBLE>(
+ input, &value_)));
+ set_has_value();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientPhishingRequest.Feature)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientPhishingRequest.Feature)
+ return false;
+#undef DO_
+}
+
+void ClientPhishingRequest_Feature::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientPhishingRequest.Feature)
+ // required string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // required double value = 2;
+ if (has_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteDouble(2, this->value(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientPhishingRequest.Feature)
+}
+
+int ClientPhishingRequest_Feature::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // required double value = 2;
+ if (has_value()) {
+ total_size += 1 + 8;
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientPhishingRequest_Feature::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientPhishingRequest_Feature*>(&from));
+}
+
+void ClientPhishingRequest_Feature::MergeFrom(const ClientPhishingRequest_Feature& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_value()) {
+ set_value(from.value());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientPhishingRequest_Feature::CopyFrom(const ClientPhishingRequest_Feature& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientPhishingRequest_Feature::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+
+ return true;
+}
+
+void ClientPhishingRequest_Feature::Swap(ClientPhishingRequest_Feature* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(value_, other->value_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientPhishingRequest_Feature::GetTypeName() const {
+ return "safe_browsing.ClientPhishingRequest.Feature";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientPhishingRequest::kUrlFieldNumber;
+const int ClientPhishingRequest::kOBSOLETEHashPrefixFieldNumber;
+const int ClientPhishingRequest::kClientScoreFieldNumber;
+const int ClientPhishingRequest::kIsPhishingFieldNumber;
+const int ClientPhishingRequest::kFeatureMapFieldNumber;
+const int ClientPhishingRequest::kModelVersionFieldNumber;
+const int ClientPhishingRequest::kNonModelFeatureMapFieldNumber;
+const int ClientPhishingRequest::kOBSOLETEReferrerUrlFieldNumber;
+const int ClientPhishingRequest::kShingleHashesFieldNumber;
+const int ClientPhishingRequest::kModelFilenameFieldNumber;
+const int ClientPhishingRequest::kPopulationFieldNumber;
+#endif // !_MSC_VER
+
+ClientPhishingRequest::ClientPhishingRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientPhishingRequest)
+}
+
+void ClientPhishingRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(
+ ::safe_browsing::ChromeUserPopulation::internal_default_instance());
+#else
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(&::safe_browsing::ChromeUserPopulation::default_instance());
+#endif
+}
+
+ClientPhishingRequest::ClientPhishingRequest(const ClientPhishingRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientPhishingRequest)
+}
+
+void ClientPhishingRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ obsolete_hash_prefix_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ client_score_ = 0;
+ is_phishing_ = false;
+ model_version_ = 0;
+ obsolete_referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ model_filename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ population_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientPhishingRequest::~ClientPhishingRequest() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientPhishingRequest)
+ SharedDtor();
+}
+
+void ClientPhishingRequest::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (obsolete_hash_prefix_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete obsolete_hash_prefix_;
+ }
+ if (obsolete_referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete obsolete_referrer_url_;
+ }
+ if (model_filename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete model_filename_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete population_;
+ }
+}
+
+void ClientPhishingRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientPhishingRequest& ClientPhishingRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientPhishingRequest* ClientPhishingRequest::default_instance_ = NULL;
+
+ClientPhishingRequest* ClientPhishingRequest::New() const {
+ return new ClientPhishingRequest;
+}
+
+void ClientPhishingRequest::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientPhishingRequest*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 175) {
+ ZR_(client_score_, is_phishing_);
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ if (has_obsolete_hash_prefix()) {
+ if (obsolete_hash_prefix_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_hash_prefix_->clear();
+ }
+ }
+ model_version_ = 0;
+ if (has_obsolete_referrer_url()) {
+ if (obsolete_referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_referrer_url_->clear();
+ }
+ }
+ }
+ if (_has_bits_[8 / 32] & 1536) {
+ if (has_model_filename()) {
+ if (model_filename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ model_filename_->clear();
+ }
+ }
+ if (has_population()) {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ feature_map_.Clear();
+ non_model_feature_map_.Clear();
+ shingle_hashes_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientPhishingRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientPhishingRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(21)) goto parse_client_score;
+ break;
+ }
+
+ // required float client_score = 2;
+ case 2: {
+ if (tag == 21) {
+ parse_client_score:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ float, ::google::protobuf::internal::WireFormatLite::TYPE_FLOAT>(
+ input, &client_score_)));
+ set_has_client_score();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_is_phishing;
+ break;
+ }
+
+ // optional bool is_phishing = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_is_phishing:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &is_phishing_)));
+ set_has_is_phishing();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_feature_map;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature feature_map = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_feature_map:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_feature_map()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_feature_map;
+ if (input->ExpectTag(48)) goto parse_model_version;
+ break;
+ }
+
+ // optional int32 model_version = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_model_version:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &model_version_)));
+ set_has_model_version();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_non_model_feature_map;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature non_model_feature_map = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_non_model_feature_map:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_non_model_feature_map()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_non_model_feature_map;
+ if (input->ExpectTag(74)) goto parse_OBSOLETE_referrer_url;
+ break;
+ }
+
+ // optional string OBSOLETE_referrer_url = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_OBSOLETE_referrer_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_obsolete_referrer_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(82)) goto parse_OBSOLETE_hash_prefix;
+ break;
+ }
+
+ // optional bytes OBSOLETE_hash_prefix = 10;
+ case 10: {
+ if (tag == 82) {
+ parse_OBSOLETE_hash_prefix:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_obsolete_hash_prefix()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(98)) goto parse_shingle_hashes;
+ break;
+ }
+
+ // repeated uint32 shingle_hashes = 12 [packed = true];
+ case 12: {
+ if (tag == 98) {
+ parse_shingle_hashes:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, this->mutable_shingle_hashes())));
+ } else if (tag == 96) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitiveNoInline<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ 1, 98, input, this->mutable_shingle_hashes())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(106)) goto parse_model_filename;
+ break;
+ }
+
+ // optional string model_filename = 13;
+ case 13: {
+ if (tag == 106) {
+ parse_model_filename:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_model_filename()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(114)) goto parse_population;
+ break;
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 14;
+ case 14: {
+ if (tag == 114) {
+ parse_population:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_population()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientPhishingRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientPhishingRequest)
+ return false;
+#undef DO_
+}
+
+void ClientPhishingRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientPhishingRequest)
+ // optional string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // required float client_score = 2;
+ if (has_client_score()) {
+ ::google::protobuf::internal::WireFormatLite::WriteFloat(2, this->client_score(), output);
+ }
+
+ // optional bool is_phishing = 4;
+ if (has_is_phishing()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(4, this->is_phishing(), output);
+ }
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature feature_map = 5;
+ for (int i = 0; i < this->feature_map_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->feature_map(i), output);
+ }
+
+ // optional int32 model_version = 6;
+ if (has_model_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(6, this->model_version(), output);
+ }
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature non_model_feature_map = 8;
+ for (int i = 0; i < this->non_model_feature_map_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 8, this->non_model_feature_map(i), output);
+ }
+
+ // optional string OBSOLETE_referrer_url = 9;
+ if (has_obsolete_referrer_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 9, this->obsolete_referrer_url(), output);
+ }
+
+ // optional bytes OBSOLETE_hash_prefix = 10;
+ if (has_obsolete_hash_prefix()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 10, this->obsolete_hash_prefix(), output);
+ }
+
+ // repeated uint32 shingle_hashes = 12 [packed = true];
+ if (this->shingle_hashes_size() > 0) {
+ ::google::protobuf::internal::WireFormatLite::WriteTag(12, ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED, output);
+ output->WriteVarint32(_shingle_hashes_cached_byte_size_);
+ }
+ for (int i = 0; i < this->shingle_hashes_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32NoTag(
+ this->shingle_hashes(i), output);
+ }
+
+ // optional string model_filename = 13;
+ if (has_model_filename()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 13, this->model_filename(), output);
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 14;
+ if (has_population()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 14, this->population(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientPhishingRequest)
+}
+
+int ClientPhishingRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional bytes OBSOLETE_hash_prefix = 10;
+ if (has_obsolete_hash_prefix()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->obsolete_hash_prefix());
+ }
+
+ // required float client_score = 2;
+ if (has_client_score()) {
+ total_size += 1 + 4;
+ }
+
+ // optional bool is_phishing = 4;
+ if (has_is_phishing()) {
+ total_size += 1 + 1;
+ }
+
+ // optional int32 model_version = 6;
+ if (has_model_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->model_version());
+ }
+
+ // optional string OBSOLETE_referrer_url = 9;
+ if (has_obsolete_referrer_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->obsolete_referrer_url());
+ }
+
+ }
+ if (_has_bits_[9 / 32] & (0xffu << (9 % 32))) {
+ // optional string model_filename = 13;
+ if (has_model_filename()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->model_filename());
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 14;
+ if (has_population()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->population());
+ }
+
+ }
+ // repeated .safe_browsing.ClientPhishingRequest.Feature feature_map = 5;
+ total_size += 1 * this->feature_map_size();
+ for (int i = 0; i < this->feature_map_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->feature_map(i));
+ }
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature non_model_feature_map = 8;
+ total_size += 1 * this->non_model_feature_map_size();
+ for (int i = 0; i < this->non_model_feature_map_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->non_model_feature_map(i));
+ }
+
+ // repeated uint32 shingle_hashes = 12 [packed = true];
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->shingle_hashes_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ UInt32Size(this->shingle_hashes(i));
+ }
+ if (data_size > 0) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(data_size);
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _shingle_hashes_cached_byte_size_ = data_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ total_size += data_size;
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientPhishingRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientPhishingRequest*>(&from));
+}
+
+void ClientPhishingRequest::MergeFrom(const ClientPhishingRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ feature_map_.MergeFrom(from.feature_map_);
+ non_model_feature_map_.MergeFrom(from.non_model_feature_map_);
+ shingle_hashes_.MergeFrom(from.shingle_hashes_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_obsolete_hash_prefix()) {
+ set_obsolete_hash_prefix(from.obsolete_hash_prefix());
+ }
+ if (from.has_client_score()) {
+ set_client_score(from.client_score());
+ }
+ if (from.has_is_phishing()) {
+ set_is_phishing(from.is_phishing());
+ }
+ if (from.has_model_version()) {
+ set_model_version(from.model_version());
+ }
+ if (from.has_obsolete_referrer_url()) {
+ set_obsolete_referrer_url(from.obsolete_referrer_url());
+ }
+ }
+ if (from._has_bits_[9 / 32] & (0xffu << (9 % 32))) {
+ if (from.has_model_filename()) {
+ set_model_filename(from.model_filename());
+ }
+ if (from.has_population()) {
+ mutable_population()->::safe_browsing::ChromeUserPopulation::MergeFrom(from.population());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientPhishingRequest::CopyFrom(const ClientPhishingRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientPhishingRequest::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000004) != 0x00000004) return false;
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->feature_map())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->non_model_feature_map())) return false;
+ return true;
+}
+
+void ClientPhishingRequest::Swap(ClientPhishingRequest* other) {
+ if (other != this) {
+ std::swap(url_, other->url_);
+ std::swap(obsolete_hash_prefix_, other->obsolete_hash_prefix_);
+ std::swap(client_score_, other->client_score_);
+ std::swap(is_phishing_, other->is_phishing_);
+ feature_map_.Swap(&other->feature_map_);
+ std::swap(model_version_, other->model_version_);
+ non_model_feature_map_.Swap(&other->non_model_feature_map_);
+ std::swap(obsolete_referrer_url_, other->obsolete_referrer_url_);
+ shingle_hashes_.Swap(&other->shingle_hashes_);
+ std::swap(model_filename_, other->model_filename_);
+ std::swap(population_, other->population_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientPhishingRequest::GetTypeName() const {
+ return "safe_browsing.ClientPhishingRequest";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientPhishingResponse::kPhishyFieldNumber;
+const int ClientPhishingResponse::kOBSOLETEWhitelistExpressionFieldNumber;
+#endif // !_MSC_VER
+
+ClientPhishingResponse::ClientPhishingResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientPhishingResponse)
+}
+
+void ClientPhishingResponse::InitAsDefaultInstance() {
+}
+
+ClientPhishingResponse::ClientPhishingResponse(const ClientPhishingResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientPhishingResponse)
+}
+
+void ClientPhishingResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ phishy_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientPhishingResponse::~ClientPhishingResponse() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientPhishingResponse)
+ SharedDtor();
+}
+
+void ClientPhishingResponse::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientPhishingResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientPhishingResponse& ClientPhishingResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientPhishingResponse* ClientPhishingResponse::default_instance_ = NULL;
+
+ClientPhishingResponse* ClientPhishingResponse::New() const {
+ return new ClientPhishingResponse;
+}
+
+void ClientPhishingResponse::Clear() {
+ phishy_ = false;
+ obsolete_whitelist_expression_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientPhishingResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientPhishingResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required bool phishy = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &phishy_)));
+ set_has_phishy();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_OBSOLETE_whitelist_expression;
+ break;
+ }
+
+ // repeated string OBSOLETE_whitelist_expression = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_OBSOLETE_whitelist_expression:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_obsolete_whitelist_expression()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_OBSOLETE_whitelist_expression;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientPhishingResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientPhishingResponse)
+ return false;
+#undef DO_
+}
+
+void ClientPhishingResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientPhishingResponse)
+ // required bool phishy = 1;
+ if (has_phishy()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(1, this->phishy(), output);
+ }
+
+ // repeated string OBSOLETE_whitelist_expression = 2;
+ for (int i = 0; i < this->obsolete_whitelist_expression_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 2, this->obsolete_whitelist_expression(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientPhishingResponse)
+}
+
+int ClientPhishingResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required bool phishy = 1;
+ if (has_phishy()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated string OBSOLETE_whitelist_expression = 2;
+ total_size += 1 * this->obsolete_whitelist_expression_size();
+ for (int i = 0; i < this->obsolete_whitelist_expression_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->obsolete_whitelist_expression(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientPhishingResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientPhishingResponse*>(&from));
+}
+
+void ClientPhishingResponse::MergeFrom(const ClientPhishingResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ obsolete_whitelist_expression_.MergeFrom(from.obsolete_whitelist_expression_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_phishy()) {
+ set_phishy(from.phishy());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientPhishingResponse::CopyFrom(const ClientPhishingResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientPhishingResponse::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ return true;
+}
+
+void ClientPhishingResponse::Swap(ClientPhishingResponse* other) {
+ if (other != this) {
+ std::swap(phishy_, other->phishy_);
+ obsolete_whitelist_expression_.Swap(&other->obsolete_whitelist_expression_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientPhishingResponse::GetTypeName() const {
+ return "safe_browsing.ClientPhishingResponse";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientMalwareRequest_UrlInfo::kIpFieldNumber;
+const int ClientMalwareRequest_UrlInfo::kUrlFieldNumber;
+const int ClientMalwareRequest_UrlInfo::kMethodFieldNumber;
+const int ClientMalwareRequest_UrlInfo::kReferrerFieldNumber;
+const int ClientMalwareRequest_UrlInfo::kResourceTypeFieldNumber;
+#endif // !_MSC_VER
+
+ClientMalwareRequest_UrlInfo::ClientMalwareRequest_UrlInfo()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientMalwareRequest.UrlInfo)
+}
+
+void ClientMalwareRequest_UrlInfo::InitAsDefaultInstance() {
+}
+
+ClientMalwareRequest_UrlInfo::ClientMalwareRequest_UrlInfo(const ClientMalwareRequest_UrlInfo& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientMalwareRequest.UrlInfo)
+}
+
+void ClientMalwareRequest_UrlInfo::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ method_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ resource_type_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientMalwareRequest_UrlInfo::~ClientMalwareRequest_UrlInfo() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientMalwareRequest.UrlInfo)
+ SharedDtor();
+}
+
+void ClientMalwareRequest_UrlInfo::SharedDtor() {
+ if (ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete ip_;
+ }
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (method_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete method_;
+ }
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientMalwareRequest_UrlInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientMalwareRequest_UrlInfo& ClientMalwareRequest_UrlInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientMalwareRequest_UrlInfo* ClientMalwareRequest_UrlInfo::default_instance_ = NULL;
+
+ClientMalwareRequest_UrlInfo* ClientMalwareRequest_UrlInfo::New() const {
+ return new ClientMalwareRequest_UrlInfo;
+}
+
+void ClientMalwareRequest_UrlInfo::Clear() {
+ if (_has_bits_[0 / 32] & 31) {
+ if (has_ip()) {
+ if (ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_->clear();
+ }
+ }
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ if (has_method()) {
+ if (method_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ method_->clear();
+ }
+ }
+ if (has_referrer()) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ }
+ resource_type_ = 0;
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientMalwareRequest_UrlInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientMalwareRequest.UrlInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string ip = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_ip()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_url;
+ break;
+ }
+
+ // required string url = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_method;
+ break;
+ }
+
+ // optional string method = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_method:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_method()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_referrer;
+ break;
+ }
+
+ // optional string referrer = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_referrer:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_referrer()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_resource_type;
+ break;
+ }
+
+ // optional int32 resource_type = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_resource_type:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &resource_type_)));
+ set_has_resource_type();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientMalwareRequest.UrlInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientMalwareRequest.UrlInfo)
+ return false;
+#undef DO_
+}
+
+void ClientMalwareRequest_UrlInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientMalwareRequest.UrlInfo)
+ // required string ip = 1;
+ if (has_ip()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->ip(), output);
+ }
+
+ // required string url = 2;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->url(), output);
+ }
+
+ // optional string method = 3;
+ if (has_method()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->method(), output);
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->referrer(), output);
+ }
+
+ // optional int32 resource_type = 5;
+ if (has_resource_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(5, this->resource_type(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientMalwareRequest.UrlInfo)
+}
+
+int ClientMalwareRequest_UrlInfo::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string ip = 1;
+ if (has_ip()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->ip());
+ }
+
+ // required string url = 2;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional string method = 3;
+ if (has_method()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->method());
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->referrer());
+ }
+
+ // optional int32 resource_type = 5;
+ if (has_resource_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->resource_type());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientMalwareRequest_UrlInfo::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientMalwareRequest_UrlInfo*>(&from));
+}
+
+void ClientMalwareRequest_UrlInfo::MergeFrom(const ClientMalwareRequest_UrlInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_ip()) {
+ set_ip(from.ip());
+ }
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_method()) {
+ set_method(from.method());
+ }
+ if (from.has_referrer()) {
+ set_referrer(from.referrer());
+ }
+ if (from.has_resource_type()) {
+ set_resource_type(from.resource_type());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientMalwareRequest_UrlInfo::CopyFrom(const ClientMalwareRequest_UrlInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientMalwareRequest_UrlInfo::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+
+ return true;
+}
+
+void ClientMalwareRequest_UrlInfo::Swap(ClientMalwareRequest_UrlInfo* other) {
+ if (other != this) {
+ std::swap(ip_, other->ip_);
+ std::swap(url_, other->url_);
+ std::swap(method_, other->method_);
+ std::swap(referrer_, other->referrer_);
+ std::swap(resource_type_, other->resource_type_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientMalwareRequest_UrlInfo::GetTypeName() const {
+ return "safe_browsing.ClientMalwareRequest.UrlInfo";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientMalwareRequest::kUrlFieldNumber;
+const int ClientMalwareRequest::kReferrerUrlFieldNumber;
+const int ClientMalwareRequest::kBadIpUrlInfoFieldNumber;
+const int ClientMalwareRequest::kPopulationFieldNumber;
+#endif // !_MSC_VER
+
+ClientMalwareRequest::ClientMalwareRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientMalwareRequest)
+}
+
+void ClientMalwareRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(
+ ::safe_browsing::ChromeUserPopulation::internal_default_instance());
+#else
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(&::safe_browsing::ChromeUserPopulation::default_instance());
+#endif
+}
+
+ClientMalwareRequest::ClientMalwareRequest(const ClientMalwareRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientMalwareRequest)
+}
+
+void ClientMalwareRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ population_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientMalwareRequest::~ClientMalwareRequest() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientMalwareRequest)
+ SharedDtor();
+}
+
+void ClientMalwareRequest::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_url_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete population_;
+ }
+}
+
+void ClientMalwareRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientMalwareRequest& ClientMalwareRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientMalwareRequest* ClientMalwareRequest::default_instance_ = NULL;
+
+ClientMalwareRequest* ClientMalwareRequest::New() const {
+ return new ClientMalwareRequest;
+}
+
+void ClientMalwareRequest::Clear() {
+ if (_has_bits_[0 / 32] & 11) {
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ if (has_referrer_url()) {
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_->clear();
+ }
+ }
+ if (has_population()) {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ }
+ }
+ bad_ip_url_info_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientMalwareRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientMalwareRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_referrer_url;
+ break;
+ }
+
+ // optional string referrer_url = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_referrer_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_referrer_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_bad_ip_url_info;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientMalwareRequest.UrlInfo bad_ip_url_info = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_bad_ip_url_info:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_bad_ip_url_info()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_bad_ip_url_info;
+ if (input->ExpectTag(74)) goto parse_population;
+ break;
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_population:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_population()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientMalwareRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientMalwareRequest)
+ return false;
+#undef DO_
+}
+
+void ClientMalwareRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientMalwareRequest)
+ // required string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // optional string referrer_url = 4;
+ if (has_referrer_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->referrer_url(), output);
+ }
+
+ // repeated .safe_browsing.ClientMalwareRequest.UrlInfo bad_ip_url_info = 7;
+ for (int i = 0; i < this->bad_ip_url_info_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 7, this->bad_ip_url_info(i), output);
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 9;
+ if (has_population()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 9, this->population(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientMalwareRequest)
+}
+
+int ClientMalwareRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional string referrer_url = 4;
+ if (has_referrer_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->referrer_url());
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 9;
+ if (has_population()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->population());
+ }
+
+ }
+ // repeated .safe_browsing.ClientMalwareRequest.UrlInfo bad_ip_url_info = 7;
+ total_size += 1 * this->bad_ip_url_info_size();
+ for (int i = 0; i < this->bad_ip_url_info_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->bad_ip_url_info(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientMalwareRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientMalwareRequest*>(&from));
+}
+
+void ClientMalwareRequest::MergeFrom(const ClientMalwareRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ bad_ip_url_info_.MergeFrom(from.bad_ip_url_info_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_referrer_url()) {
+ set_referrer_url(from.referrer_url());
+ }
+ if (from.has_population()) {
+ mutable_population()->::safe_browsing::ChromeUserPopulation::MergeFrom(from.population());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientMalwareRequest::CopyFrom(const ClientMalwareRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientMalwareRequest::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->bad_ip_url_info())) return false;
+ return true;
+}
+
+void ClientMalwareRequest::Swap(ClientMalwareRequest* other) {
+ if (other != this) {
+ std::swap(url_, other->url_);
+ std::swap(referrer_url_, other->referrer_url_);
+ bad_ip_url_info_.Swap(&other->bad_ip_url_info_);
+ std::swap(population_, other->population_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientMalwareRequest::GetTypeName() const {
+ return "safe_browsing.ClientMalwareRequest";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientMalwareResponse::kBlacklistFieldNumber;
+const int ClientMalwareResponse::kBadIpFieldNumber;
+const int ClientMalwareResponse::kBadUrlFieldNumber;
+#endif // !_MSC_VER
+
+ClientMalwareResponse::ClientMalwareResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientMalwareResponse)
+}
+
+void ClientMalwareResponse::InitAsDefaultInstance() {
+}
+
+ClientMalwareResponse::ClientMalwareResponse(const ClientMalwareResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientMalwareResponse)
+}
+
+void ClientMalwareResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ blacklist_ = false;
+ bad_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ bad_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientMalwareResponse::~ClientMalwareResponse() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientMalwareResponse)
+ SharedDtor();
+}
+
+void ClientMalwareResponse::SharedDtor() {
+ if (bad_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bad_ip_;
+ }
+ if (bad_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bad_url_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientMalwareResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientMalwareResponse& ClientMalwareResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientMalwareResponse* ClientMalwareResponse::default_instance_ = NULL;
+
+ClientMalwareResponse* ClientMalwareResponse::New() const {
+ return new ClientMalwareResponse;
+}
+
+void ClientMalwareResponse::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ blacklist_ = false;
+ if (has_bad_ip()) {
+ if (bad_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_ip_->clear();
+ }
+ }
+ if (has_bad_url()) {
+ if (bad_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_url_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientMalwareResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientMalwareResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required bool blacklist = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &blacklist_)));
+ set_has_blacklist();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_bad_ip;
+ break;
+ }
+
+ // optional string bad_ip = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_bad_ip:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_bad_ip()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_bad_url;
+ break;
+ }
+
+ // optional string bad_url = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_bad_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_bad_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientMalwareResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientMalwareResponse)
+ return false;
+#undef DO_
+}
+
+void ClientMalwareResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientMalwareResponse)
+ // required bool blacklist = 1;
+ if (has_blacklist()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(1, this->blacklist(), output);
+ }
+
+ // optional string bad_ip = 2;
+ if (has_bad_ip()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->bad_ip(), output);
+ }
+
+ // optional string bad_url = 3;
+ if (has_bad_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->bad_url(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientMalwareResponse)
+}
+
+int ClientMalwareResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required bool blacklist = 1;
+ if (has_blacklist()) {
+ total_size += 1 + 1;
+ }
+
+ // optional string bad_ip = 2;
+ if (has_bad_ip()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->bad_ip());
+ }
+
+ // optional string bad_url = 3;
+ if (has_bad_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->bad_url());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientMalwareResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientMalwareResponse*>(&from));
+}
+
+void ClientMalwareResponse::MergeFrom(const ClientMalwareResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_blacklist()) {
+ set_blacklist(from.blacklist());
+ }
+ if (from.has_bad_ip()) {
+ set_bad_ip(from.bad_ip());
+ }
+ if (from.has_bad_url()) {
+ set_bad_url(from.bad_url());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientMalwareResponse::CopyFrom(const ClientMalwareResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientMalwareResponse::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ return true;
+}
+
+void ClientMalwareResponse::Swap(ClientMalwareResponse* other) {
+ if (other != this) {
+ std::swap(blacklist_, other->blacklist_);
+ std::swap(bad_ip_, other->bad_ip_);
+ std::swap(bad_url_, other->bad_url_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientMalwareResponse::GetTypeName() const {
+ return "safe_browsing.ClientMalwareResponse";
+}
+
+
+// ===================================================================
+
+bool ClientDownloadRequest_ResourceType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::DOWNLOAD_URL;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::DOWNLOAD_REDIRECT;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::TAB_URL;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::TAB_REDIRECT;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::PPAPI_DOCUMENT;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::PPAPI_PLUGIN;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::ResourceType_MIN;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest::ResourceType_MAX;
+const int ClientDownloadRequest::ResourceType_ARRAYSIZE;
+#endif // _MSC_VER
+bool ClientDownloadRequest_DownloadType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ case 10:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::WIN_EXECUTABLE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::CHROME_EXTENSION;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::ANDROID_APK;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::ZIPPED_EXECUTABLE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::MAC_EXECUTABLE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::ZIPPED_ARCHIVE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::ARCHIVE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::INVALID_ZIP;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::INVALID_MAC_ARCHIVE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::PPAPI_SAVE_REQUEST;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::SAMPLED_UNSUPPORTED_FILE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::DownloadType_MIN;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest::DownloadType_MAX;
+const int ClientDownloadRequest::DownloadType_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientDownloadRequest_Digests::kSha256FieldNumber;
+const int ClientDownloadRequest_Digests::kSha1FieldNumber;
+const int ClientDownloadRequest_Digests::kMd5FieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_Digests::ClientDownloadRequest_Digests()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.Digests)
+}
+
+void ClientDownloadRequest_Digests::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_Digests::ClientDownloadRequest_Digests(const ClientDownloadRequest_Digests& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.Digests)
+}
+
+void ClientDownloadRequest_Digests::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ sha1_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ md5_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_Digests::~ClientDownloadRequest_Digests() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.Digests)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_Digests::SharedDtor() {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete sha256_;
+ }
+ if (sha1_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete sha1_;
+ }
+ if (md5_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete md5_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_Digests::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_Digests& ClientDownloadRequest_Digests::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_Digests* ClientDownloadRequest_Digests::default_instance_ = NULL;
+
+ClientDownloadRequest_Digests* ClientDownloadRequest_Digests::New() const {
+ return new ClientDownloadRequest_Digests;
+}
+
+void ClientDownloadRequest_Digests::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_sha256()) {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_->clear();
+ }
+ }
+ if (has_sha1()) {
+ if (sha1_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha1_->clear();
+ }
+ }
+ if (has_md5()) {
+ if (md5_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ md5_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_Digests::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.Digests)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes sha256 = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_sha256()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_sha1;
+ break;
+ }
+
+ // optional bytes sha1 = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_sha1:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_sha1()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_md5;
+ break;
+ }
+
+ // optional bytes md5 = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_md5:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_md5()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.Digests)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.Digests)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_Digests::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.Digests)
+ // optional bytes sha256 = 1;
+ if (has_sha256()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->sha256(), output);
+ }
+
+ // optional bytes sha1 = 2;
+ if (has_sha1()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->sha1(), output);
+ }
+
+ // optional bytes md5 = 3;
+ if (has_md5()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->md5(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.Digests)
+}
+
+int ClientDownloadRequest_Digests::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes sha256 = 1;
+ if (has_sha256()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->sha256());
+ }
+
+ // optional bytes sha1 = 2;
+ if (has_sha1()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->sha1());
+ }
+
+ // optional bytes md5 = 3;
+ if (has_md5()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->md5());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_Digests::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_Digests*>(&from));
+}
+
+void ClientDownloadRequest_Digests::MergeFrom(const ClientDownloadRequest_Digests& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_sha256()) {
+ set_sha256(from.sha256());
+ }
+ if (from.has_sha1()) {
+ set_sha1(from.sha1());
+ }
+ if (from.has_md5()) {
+ set_md5(from.md5());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_Digests::CopyFrom(const ClientDownloadRequest_Digests& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_Digests::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadRequest_Digests::Swap(ClientDownloadRequest_Digests* other) {
+ if (other != this) {
+ std::swap(sha256_, other->sha256_);
+ std::swap(sha1_, other->sha1_);
+ std::swap(md5_, other->md5_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_Digests::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.Digests";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_Resource::kUrlFieldNumber;
+const int ClientDownloadRequest_Resource::kTypeFieldNumber;
+const int ClientDownloadRequest_Resource::kRemoteIpFieldNumber;
+const int ClientDownloadRequest_Resource::kReferrerFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_Resource::ClientDownloadRequest_Resource()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.Resource)
+}
+
+void ClientDownloadRequest_Resource::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_Resource::ClientDownloadRequest_Resource(const ClientDownloadRequest_Resource& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.Resource)
+}
+
+void ClientDownloadRequest_Resource::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ type_ = 0;
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_Resource::~ClientDownloadRequest_Resource() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.Resource)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_Resource::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete remote_ip_;
+ }
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_Resource::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_Resource& ClientDownloadRequest_Resource::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_Resource* ClientDownloadRequest_Resource::default_instance_ = NULL;
+
+ClientDownloadRequest_Resource* ClientDownloadRequest_Resource::New() const {
+ return new ClientDownloadRequest_Resource;
+}
+
+void ClientDownloadRequest_Resource::Clear() {
+ if (_has_bits_[0 / 32] & 15) {
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ type_ = 0;
+ if (has_remote_ip()) {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_->clear();
+ }
+ }
+ if (has_referrer()) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_Resource::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.Resource)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_type;
+ break;
+ }
+
+ // required .safe_browsing.ClientDownloadRequest.ResourceType type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadRequest_ResourceType_IsValid(value)) {
+ set_type(static_cast< ::safe_browsing::ClientDownloadRequest_ResourceType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_remote_ip;
+ break;
+ }
+
+ // optional bytes remote_ip = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_remote_ip:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_remote_ip()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_referrer;
+ break;
+ }
+
+ // optional string referrer = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_referrer:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_referrer()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.Resource)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.Resource)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_Resource::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.Resource)
+ // required string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // required .safe_browsing.ClientDownloadRequest.ResourceType type = 2;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->type(), output);
+ }
+
+ // optional bytes remote_ip = 3;
+ if (has_remote_ip()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->remote_ip(), output);
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->referrer(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.Resource)
+}
+
+int ClientDownloadRequest_Resource::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // required .safe_browsing.ClientDownloadRequest.ResourceType type = 2;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+ }
+
+ // optional bytes remote_ip = 3;
+ if (has_remote_ip()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->remote_ip());
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->referrer());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_Resource::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_Resource*>(&from));
+}
+
+void ClientDownloadRequest_Resource::MergeFrom(const ClientDownloadRequest_Resource& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_remote_ip()) {
+ set_remote_ip(from.remote_ip());
+ }
+ if (from.has_referrer()) {
+ set_referrer(from.referrer());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_Resource::CopyFrom(const ClientDownloadRequest_Resource& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_Resource::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+
+ return true;
+}
+
+void ClientDownloadRequest_Resource::Swap(ClientDownloadRequest_Resource* other) {
+ if (other != this) {
+ std::swap(url_, other->url_);
+ std::swap(type_, other->type_);
+ std::swap(remote_ip_, other->remote_ip_);
+ std::swap(referrer_, other->referrer_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_Resource::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.Resource";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_CertificateChain_Element::kCertificateFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_CertificateChain_Element::ClientDownloadRequest_CertificateChain_Element()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+}
+
+void ClientDownloadRequest_CertificateChain_Element::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_CertificateChain_Element::ClientDownloadRequest_CertificateChain_Element(const ClientDownloadRequest_CertificateChain_Element& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+}
+
+void ClientDownloadRequest_CertificateChain_Element::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ certificate_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_CertificateChain_Element::~ClientDownloadRequest_CertificateChain_Element() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_CertificateChain_Element::SharedDtor() {
+ if (certificate_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete certificate_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_CertificateChain_Element::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_CertificateChain_Element& ClientDownloadRequest_CertificateChain_Element::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_CertificateChain_Element* ClientDownloadRequest_CertificateChain_Element::default_instance_ = NULL;
+
+ClientDownloadRequest_CertificateChain_Element* ClientDownloadRequest_CertificateChain_Element::New() const {
+ return new ClientDownloadRequest_CertificateChain_Element;
+}
+
+void ClientDownloadRequest_CertificateChain_Element::Clear() {
+ if (has_certificate()) {
+ if (certificate_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ certificate_->clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_CertificateChain_Element::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes certificate = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_certificate()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_CertificateChain_Element::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+ // optional bytes certificate = 1;
+ if (has_certificate()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->certificate(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+}
+
+int ClientDownloadRequest_CertificateChain_Element::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes certificate = 1;
+ if (has_certificate()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->certificate());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_CertificateChain_Element::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_CertificateChain_Element*>(&from));
+}
+
+void ClientDownloadRequest_CertificateChain_Element::MergeFrom(const ClientDownloadRequest_CertificateChain_Element& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_certificate()) {
+ set_certificate(from.certificate());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_CertificateChain_Element::CopyFrom(const ClientDownloadRequest_CertificateChain_Element& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_CertificateChain_Element::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadRequest_CertificateChain_Element::Swap(ClientDownloadRequest_CertificateChain_Element* other) {
+ if (other != this) {
+ std::swap(certificate_, other->certificate_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_CertificateChain_Element::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.CertificateChain.Element";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_CertificateChain::kElementFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_CertificateChain::ClientDownloadRequest_CertificateChain()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.CertificateChain)
+}
+
+void ClientDownloadRequest_CertificateChain::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_CertificateChain::ClientDownloadRequest_CertificateChain(const ClientDownloadRequest_CertificateChain& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.CertificateChain)
+}
+
+void ClientDownloadRequest_CertificateChain::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_CertificateChain::~ClientDownloadRequest_CertificateChain() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.CertificateChain)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_CertificateChain::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_CertificateChain::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_CertificateChain& ClientDownloadRequest_CertificateChain::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_CertificateChain* ClientDownloadRequest_CertificateChain::default_instance_ = NULL;
+
+ClientDownloadRequest_CertificateChain* ClientDownloadRequest_CertificateChain::New() const {
+ return new ClientDownloadRequest_CertificateChain;
+}
+
+void ClientDownloadRequest_CertificateChain::Clear() {
+ element_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_CertificateChain::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.CertificateChain)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain.Element element = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_element:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_element()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_element;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.CertificateChain)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.CertificateChain)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_CertificateChain::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.CertificateChain)
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain.Element element = 1;
+ for (int i = 0; i < this->element_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->element(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.CertificateChain)
+}
+
+int ClientDownloadRequest_CertificateChain::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain.Element element = 1;
+ total_size += 1 * this->element_size();
+ for (int i = 0; i < this->element_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->element(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_CertificateChain::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_CertificateChain*>(&from));
+}
+
+void ClientDownloadRequest_CertificateChain::MergeFrom(const ClientDownloadRequest_CertificateChain& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ element_.MergeFrom(from.element_);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_CertificateChain::CopyFrom(const ClientDownloadRequest_CertificateChain& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_CertificateChain::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadRequest_CertificateChain::Swap(ClientDownloadRequest_CertificateChain* other) {
+ if (other != this) {
+ element_.Swap(&other->element_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_CertificateChain::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.CertificateChain";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_ExtendedAttr::kKeyFieldNumber;
+const int ClientDownloadRequest_ExtendedAttr::kValueFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_ExtendedAttr::ClientDownloadRequest_ExtendedAttr()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+}
+
+void ClientDownloadRequest_ExtendedAttr::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_ExtendedAttr::ClientDownloadRequest_ExtendedAttr(const ClientDownloadRequest_ExtendedAttr& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+}
+
+void ClientDownloadRequest_ExtendedAttr::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_ExtendedAttr::~ClientDownloadRequest_ExtendedAttr() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_ExtendedAttr::SharedDtor() {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete key_;
+ }
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete value_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_ExtendedAttr::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_ExtendedAttr& ClientDownloadRequest_ExtendedAttr::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_ExtendedAttr* ClientDownloadRequest_ExtendedAttr::default_instance_ = NULL;
+
+ClientDownloadRequest_ExtendedAttr* ClientDownloadRequest_ExtendedAttr::New() const {
+ return new ClientDownloadRequest_ExtendedAttr;
+}
+
+void ClientDownloadRequest_ExtendedAttr::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_key()) {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_->clear();
+ }
+ }
+ if (has_value()) {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_ExtendedAttr::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string key = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_key()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ break;
+ }
+
+ // optional bytes value = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_ExtendedAttr::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+ // required string key = 1;
+ if (has_key()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->key(), output);
+ }
+
+ // optional bytes value = 2;
+ if (has_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->value(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+}
+
+int ClientDownloadRequest_ExtendedAttr::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string key = 1;
+ if (has_key()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->key());
+ }
+
+ // optional bytes value = 2;
+ if (has_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->value());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_ExtendedAttr::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_ExtendedAttr*>(&from));
+}
+
+void ClientDownloadRequest_ExtendedAttr::MergeFrom(const ClientDownloadRequest_ExtendedAttr& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_key()) {
+ set_key(from.key());
+ }
+ if (from.has_value()) {
+ set_value(from.value());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_ExtendedAttr::CopyFrom(const ClientDownloadRequest_ExtendedAttr& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_ExtendedAttr::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ return true;
+}
+
+void ClientDownloadRequest_ExtendedAttr::Swap(ClientDownloadRequest_ExtendedAttr* other) {
+ if (other != this) {
+ std::swap(key_, other->key_);
+ std::swap(value_, other->value_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_ExtendedAttr::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.ExtendedAttr";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_SignatureInfo::kCertificateChainFieldNumber;
+const int ClientDownloadRequest_SignatureInfo::kTrustedFieldNumber;
+const int ClientDownloadRequest_SignatureInfo::kSignedDataFieldNumber;
+const int ClientDownloadRequest_SignatureInfo::kXattrFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_SignatureInfo::ClientDownloadRequest_SignatureInfo()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.SignatureInfo)
+}
+
+void ClientDownloadRequest_SignatureInfo::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_SignatureInfo::ClientDownloadRequest_SignatureInfo(const ClientDownloadRequest_SignatureInfo& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.SignatureInfo)
+}
+
+void ClientDownloadRequest_SignatureInfo::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ trusted_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_SignatureInfo::~ClientDownloadRequest_SignatureInfo() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.SignatureInfo)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_SignatureInfo::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_SignatureInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_SignatureInfo& ClientDownloadRequest_SignatureInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_SignatureInfo* ClientDownloadRequest_SignatureInfo::default_instance_ = NULL;
+
+ClientDownloadRequest_SignatureInfo* ClientDownloadRequest_SignatureInfo::New() const {
+ return new ClientDownloadRequest_SignatureInfo;
+}
+
+void ClientDownloadRequest_SignatureInfo::Clear() {
+ trusted_ = false;
+ certificate_chain_.Clear();
+ signed_data_.Clear();
+ xattr_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_SignatureInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.SignatureInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain certificate_chain = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_certificate_chain:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_certificate_chain()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_certificate_chain;
+ if (input->ExpectTag(16)) goto parse_trusted;
+ break;
+ }
+
+ // optional bool trusted = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_trusted:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &trusted_)));
+ set_has_trusted();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_signed_data;
+ break;
+ }
+
+ // repeated bytes signed_data = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_signed_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->add_signed_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_signed_data;
+ if (input->ExpectTag(34)) goto parse_xattr;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.ExtendedAttr xattr = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_xattr:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_xattr()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_xattr;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.SignatureInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.SignatureInfo)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_SignatureInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.SignatureInfo)
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain certificate_chain = 1;
+ for (int i = 0; i < this->certificate_chain_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->certificate_chain(i), output);
+ }
+
+ // optional bool trusted = 2;
+ if (has_trusted()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->trusted(), output);
+ }
+
+ // repeated bytes signed_data = 3;
+ for (int i = 0; i < this->signed_data_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytes(
+ 3, this->signed_data(i), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.ExtendedAttr xattr = 4;
+ for (int i = 0; i < this->xattr_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->xattr(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.SignatureInfo)
+}
+
+int ClientDownloadRequest_SignatureInfo::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ // optional bool trusted = 2;
+ if (has_trusted()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain certificate_chain = 1;
+ total_size += 1 * this->certificate_chain_size();
+ for (int i = 0; i < this->certificate_chain_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->certificate_chain(i));
+ }
+
+ // repeated bytes signed_data = 3;
+ total_size += 1 * this->signed_data_size();
+ for (int i = 0; i < this->signed_data_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->signed_data(i));
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.ExtendedAttr xattr = 4;
+ total_size += 1 * this->xattr_size();
+ for (int i = 0; i < this->xattr_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->xattr(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_SignatureInfo::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_SignatureInfo*>(&from));
+}
+
+void ClientDownloadRequest_SignatureInfo::MergeFrom(const ClientDownloadRequest_SignatureInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ certificate_chain_.MergeFrom(from.certificate_chain_);
+ signed_data_.MergeFrom(from.signed_data_);
+ xattr_.MergeFrom(from.xattr_);
+ if (from._has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ if (from.has_trusted()) {
+ set_trusted(from.trusted());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_SignatureInfo::CopyFrom(const ClientDownloadRequest_SignatureInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_SignatureInfo::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->xattr())) return false;
+ return true;
+}
+
+void ClientDownloadRequest_SignatureInfo::Swap(ClientDownloadRequest_SignatureInfo* other) {
+ if (other != this) {
+ certificate_chain_.Swap(&other->certificate_chain_);
+ std::swap(trusted_, other->trusted_);
+ signed_data_.Swap(&other->signed_data_);
+ xattr_.Swap(&other->xattr_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_SignatureInfo::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.SignatureInfo";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_PEImageHeaders_DebugData::kDirectoryEntryFieldNumber;
+const int ClientDownloadRequest_PEImageHeaders_DebugData::kRawDataFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_PEImageHeaders_DebugData::ClientDownloadRequest_PEImageHeaders_DebugData()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_PEImageHeaders_DebugData::ClientDownloadRequest_PEImageHeaders_DebugData(const ClientDownloadRequest_PEImageHeaders_DebugData& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ directory_entry_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ raw_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_PEImageHeaders_DebugData::~ClientDownloadRequest_PEImageHeaders_DebugData() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::SharedDtor() {
+ if (directory_entry_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete directory_entry_;
+ }
+ if (raw_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete raw_data_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_PEImageHeaders_DebugData& ClientDownloadRequest_PEImageHeaders_DebugData::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_PEImageHeaders_DebugData* ClientDownloadRequest_PEImageHeaders_DebugData::default_instance_ = NULL;
+
+ClientDownloadRequest_PEImageHeaders_DebugData* ClientDownloadRequest_PEImageHeaders_DebugData::New() const {
+ return new ClientDownloadRequest_PEImageHeaders_DebugData;
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_directory_entry()) {
+ if (directory_entry_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ directory_entry_->clear();
+ }
+ }
+ if (has_raw_data()) {
+ if (raw_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_data_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_PEImageHeaders_DebugData::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes directory_entry = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_directory_entry()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_raw_data;
+ break;
+ }
+
+ // optional bytes raw_data = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_raw_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_raw_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+ // optional bytes directory_entry = 1;
+ if (has_directory_entry()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->directory_entry(), output);
+ }
+
+ // optional bytes raw_data = 2;
+ if (has_raw_data()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->raw_data(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+}
+
+int ClientDownloadRequest_PEImageHeaders_DebugData::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes directory_entry = 1;
+ if (has_directory_entry()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->directory_entry());
+ }
+
+ // optional bytes raw_data = 2;
+ if (has_raw_data()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->raw_data());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_PEImageHeaders_DebugData*>(&from));
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::MergeFrom(const ClientDownloadRequest_PEImageHeaders_DebugData& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_directory_entry()) {
+ set_directory_entry(from.directory_entry());
+ }
+ if (from.has_raw_data()) {
+ set_raw_data(from.raw_data());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::CopyFrom(const ClientDownloadRequest_PEImageHeaders_DebugData& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_PEImageHeaders_DebugData::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadRequest_PEImageHeaders_DebugData::Swap(ClientDownloadRequest_PEImageHeaders_DebugData* other) {
+ if (other != this) {
+ std::swap(directory_entry_, other->directory_entry_);
+ std::swap(raw_data_, other->raw_data_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_PEImageHeaders_DebugData::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_PEImageHeaders::kDosHeaderFieldNumber;
+const int ClientDownloadRequest_PEImageHeaders::kFileHeaderFieldNumber;
+const int ClientDownloadRequest_PEImageHeaders::kOptionalHeaders32FieldNumber;
+const int ClientDownloadRequest_PEImageHeaders::kOptionalHeaders64FieldNumber;
+const int ClientDownloadRequest_PEImageHeaders::kSectionHeaderFieldNumber;
+const int ClientDownloadRequest_PEImageHeaders::kExportSectionDataFieldNumber;
+const int ClientDownloadRequest_PEImageHeaders::kDebugDataFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_PEImageHeaders::ClientDownloadRequest_PEImageHeaders()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+}
+
+void ClientDownloadRequest_PEImageHeaders::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_PEImageHeaders::ClientDownloadRequest_PEImageHeaders(const ClientDownloadRequest_PEImageHeaders& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+}
+
+void ClientDownloadRequest_PEImageHeaders::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ dos_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ file_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ optional_headers32_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ optional_headers64_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ export_section_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_PEImageHeaders::~ClientDownloadRequest_PEImageHeaders() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_PEImageHeaders::SharedDtor() {
+ if (dos_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete dos_header_;
+ }
+ if (file_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_header_;
+ }
+ if (optional_headers32_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete optional_headers32_;
+ }
+ if (optional_headers64_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete optional_headers64_;
+ }
+ if (export_section_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete export_section_data_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_PEImageHeaders::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_PEImageHeaders& ClientDownloadRequest_PEImageHeaders::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_PEImageHeaders* ClientDownloadRequest_PEImageHeaders::default_instance_ = NULL;
+
+ClientDownloadRequest_PEImageHeaders* ClientDownloadRequest_PEImageHeaders::New() const {
+ return new ClientDownloadRequest_PEImageHeaders;
+}
+
+void ClientDownloadRequest_PEImageHeaders::Clear() {
+ if (_has_bits_[0 / 32] & 47) {
+ if (has_dos_header()) {
+ if (dos_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ dos_header_->clear();
+ }
+ }
+ if (has_file_header()) {
+ if (file_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_header_->clear();
+ }
+ }
+ if (has_optional_headers32()) {
+ if (optional_headers32_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers32_->clear();
+ }
+ }
+ if (has_optional_headers64()) {
+ if (optional_headers64_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers64_->clear();
+ }
+ }
+ if (has_export_section_data()) {
+ if (export_section_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_section_data_->clear();
+ }
+ }
+ }
+ section_header_.Clear();
+ debug_data_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_PEImageHeaders::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes dos_header = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_dos_header()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_file_header;
+ break;
+ }
+
+ // optional bytes file_header = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_file_header:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_file_header()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_optional_headers32;
+ break;
+ }
+
+ // optional bytes optional_headers32 = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_optional_headers32:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_optional_headers32()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_optional_headers64;
+ break;
+ }
+
+ // optional bytes optional_headers64 = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_optional_headers64:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_optional_headers64()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_section_header;
+ break;
+ }
+
+ // repeated bytes section_header = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_section_header:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->add_section_header()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_section_header;
+ if (input->ExpectTag(50)) goto parse_export_section_data;
+ break;
+ }
+
+ // optional bytes export_section_data = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_export_section_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_export_section_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_debug_data;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData debug_data = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_debug_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_debug_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_debug_data;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_PEImageHeaders::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+ // optional bytes dos_header = 1;
+ if (has_dos_header()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->dos_header(), output);
+ }
+
+ // optional bytes file_header = 2;
+ if (has_file_header()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->file_header(), output);
+ }
+
+ // optional bytes optional_headers32 = 3;
+ if (has_optional_headers32()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->optional_headers32(), output);
+ }
+
+ // optional bytes optional_headers64 = 4;
+ if (has_optional_headers64()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 4, this->optional_headers64(), output);
+ }
+
+ // repeated bytes section_header = 5;
+ for (int i = 0; i < this->section_header_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytes(
+ 5, this->section_header(i), output);
+ }
+
+ // optional bytes export_section_data = 6;
+ if (has_export_section_data()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 6, this->export_section_data(), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData debug_data = 7;
+ for (int i = 0; i < this->debug_data_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 7, this->debug_data(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+}
+
+int ClientDownloadRequest_PEImageHeaders::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes dos_header = 1;
+ if (has_dos_header()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->dos_header());
+ }
+
+ // optional bytes file_header = 2;
+ if (has_file_header()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->file_header());
+ }
+
+ // optional bytes optional_headers32 = 3;
+ if (has_optional_headers32()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->optional_headers32());
+ }
+
+ // optional bytes optional_headers64 = 4;
+ if (has_optional_headers64()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->optional_headers64());
+ }
+
+ // optional bytes export_section_data = 6;
+ if (has_export_section_data()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->export_section_data());
+ }
+
+ }
+ // repeated bytes section_header = 5;
+ total_size += 1 * this->section_header_size();
+ for (int i = 0; i < this->section_header_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->section_header(i));
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData debug_data = 7;
+ total_size += 1 * this->debug_data_size();
+ for (int i = 0; i < this->debug_data_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->debug_data(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_PEImageHeaders::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_PEImageHeaders*>(&from));
+}
+
+void ClientDownloadRequest_PEImageHeaders::MergeFrom(const ClientDownloadRequest_PEImageHeaders& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ section_header_.MergeFrom(from.section_header_);
+ debug_data_.MergeFrom(from.debug_data_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_dos_header()) {
+ set_dos_header(from.dos_header());
+ }
+ if (from.has_file_header()) {
+ set_file_header(from.file_header());
+ }
+ if (from.has_optional_headers32()) {
+ set_optional_headers32(from.optional_headers32());
+ }
+ if (from.has_optional_headers64()) {
+ set_optional_headers64(from.optional_headers64());
+ }
+ if (from.has_export_section_data()) {
+ set_export_section_data(from.export_section_data());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_PEImageHeaders::CopyFrom(const ClientDownloadRequest_PEImageHeaders& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_PEImageHeaders::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadRequest_PEImageHeaders::Swap(ClientDownloadRequest_PEImageHeaders* other) {
+ if (other != this) {
+ std::swap(dos_header_, other->dos_header_);
+ std::swap(file_header_, other->file_header_);
+ std::swap(optional_headers32_, other->optional_headers32_);
+ std::swap(optional_headers64_, other->optional_headers64_);
+ section_header_.Swap(&other->section_header_);
+ std::swap(export_section_data_, other->export_section_data_);
+ debug_data_.Swap(&other->debug_data_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_PEImageHeaders::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.PEImageHeaders";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_MachOHeaders_LoadCommand::kCommandIdFieldNumber;
+const int ClientDownloadRequest_MachOHeaders_LoadCommand::kCommandFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_MachOHeaders_LoadCommand::ClientDownloadRequest_MachOHeaders_LoadCommand()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_MachOHeaders_LoadCommand::ClientDownloadRequest_MachOHeaders_LoadCommand(const ClientDownloadRequest_MachOHeaders_LoadCommand& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ command_id_ = 0u;
+ command_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_MachOHeaders_LoadCommand::~ClientDownloadRequest_MachOHeaders_LoadCommand() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::SharedDtor() {
+ if (command_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete command_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_MachOHeaders_LoadCommand& ClientDownloadRequest_MachOHeaders_LoadCommand::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_MachOHeaders_LoadCommand* ClientDownloadRequest_MachOHeaders_LoadCommand::default_instance_ = NULL;
+
+ClientDownloadRequest_MachOHeaders_LoadCommand* ClientDownloadRequest_MachOHeaders_LoadCommand::New() const {
+ return new ClientDownloadRequest_MachOHeaders_LoadCommand;
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ command_id_ = 0u;
+ if (has_command()) {
+ if (command_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ command_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_MachOHeaders_LoadCommand::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required uint32 command_id = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &command_id_)));
+ set_has_command_id();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_command;
+ break;
+ }
+
+ // required bytes command = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_command:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_command()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+ // required uint32 command_id = 1;
+ if (has_command_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(1, this->command_id(), output);
+ }
+
+ // required bytes command = 2;
+ if (has_command()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->command(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+}
+
+int ClientDownloadRequest_MachOHeaders_LoadCommand::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required uint32 command_id = 1;
+ if (has_command_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->command_id());
+ }
+
+ // required bytes command = 2;
+ if (has_command()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->command());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_MachOHeaders_LoadCommand*>(&from));
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::MergeFrom(const ClientDownloadRequest_MachOHeaders_LoadCommand& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_command_id()) {
+ set_command_id(from.command_id());
+ }
+ if (from.has_command()) {
+ set_command(from.command());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::CopyFrom(const ClientDownloadRequest_MachOHeaders_LoadCommand& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_MachOHeaders_LoadCommand::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+
+ return true;
+}
+
+void ClientDownloadRequest_MachOHeaders_LoadCommand::Swap(ClientDownloadRequest_MachOHeaders_LoadCommand* other) {
+ if (other != this) {
+ std::swap(command_id_, other->command_id_);
+ std::swap(command_, other->command_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_MachOHeaders_LoadCommand::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_MachOHeaders::kMachHeaderFieldNumber;
+const int ClientDownloadRequest_MachOHeaders::kLoadCommandsFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_MachOHeaders::ClientDownloadRequest_MachOHeaders()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.MachOHeaders)
+}
+
+void ClientDownloadRequest_MachOHeaders::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_MachOHeaders::ClientDownloadRequest_MachOHeaders(const ClientDownloadRequest_MachOHeaders& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.MachOHeaders)
+}
+
+void ClientDownloadRequest_MachOHeaders::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ mach_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_MachOHeaders::~ClientDownloadRequest_MachOHeaders() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.MachOHeaders)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_MachOHeaders::SharedDtor() {
+ if (mach_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete mach_header_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_MachOHeaders::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_MachOHeaders& ClientDownloadRequest_MachOHeaders::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_MachOHeaders* ClientDownloadRequest_MachOHeaders::default_instance_ = NULL;
+
+ClientDownloadRequest_MachOHeaders* ClientDownloadRequest_MachOHeaders::New() const {
+ return new ClientDownloadRequest_MachOHeaders;
+}
+
+void ClientDownloadRequest_MachOHeaders::Clear() {
+ if (has_mach_header()) {
+ if (mach_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ mach_header_->clear();
+ }
+ }
+ load_commands_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_MachOHeaders::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.MachOHeaders)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required bytes mach_header = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_mach_header()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_load_commands;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand load_commands = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_load_commands:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_load_commands()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_load_commands;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.MachOHeaders)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.MachOHeaders)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_MachOHeaders::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.MachOHeaders)
+ // required bytes mach_header = 1;
+ if (has_mach_header()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->mach_header(), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand load_commands = 2;
+ for (int i = 0; i < this->load_commands_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->load_commands(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.MachOHeaders)
+}
+
+int ClientDownloadRequest_MachOHeaders::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required bytes mach_header = 1;
+ if (has_mach_header()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->mach_header());
+ }
+
+ }
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand load_commands = 2;
+ total_size += 1 * this->load_commands_size();
+ for (int i = 0; i < this->load_commands_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->load_commands(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_MachOHeaders::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_MachOHeaders*>(&from));
+}
+
+void ClientDownloadRequest_MachOHeaders::MergeFrom(const ClientDownloadRequest_MachOHeaders& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ load_commands_.MergeFrom(from.load_commands_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_mach_header()) {
+ set_mach_header(from.mach_header());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_MachOHeaders::CopyFrom(const ClientDownloadRequest_MachOHeaders& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_MachOHeaders::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->load_commands())) return false;
+ return true;
+}
+
+void ClientDownloadRequest_MachOHeaders::Swap(ClientDownloadRequest_MachOHeaders* other) {
+ if (other != this) {
+ std::swap(mach_header_, other->mach_header_);
+ load_commands_.Swap(&other->load_commands_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_MachOHeaders::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.MachOHeaders";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_ImageHeaders::kPeHeadersFieldNumber;
+const int ClientDownloadRequest_ImageHeaders::kMachOHeadersFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_ImageHeaders::ClientDownloadRequest_ImageHeaders()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.ImageHeaders)
+}
+
+void ClientDownloadRequest_ImageHeaders::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ pe_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_PEImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_PEImageHeaders::internal_default_instance());
+#else
+ pe_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_PEImageHeaders*>(&::safe_browsing::ClientDownloadRequest_PEImageHeaders::default_instance());
+#endif
+}
+
+ClientDownloadRequest_ImageHeaders::ClientDownloadRequest_ImageHeaders(const ClientDownloadRequest_ImageHeaders& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.ImageHeaders)
+}
+
+void ClientDownloadRequest_ImageHeaders::SharedCtor() {
+ _cached_size_ = 0;
+ pe_headers_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_ImageHeaders::~ClientDownloadRequest_ImageHeaders() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.ImageHeaders)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_ImageHeaders::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete pe_headers_;
+ }
+}
+
+void ClientDownloadRequest_ImageHeaders::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_ImageHeaders& ClientDownloadRequest_ImageHeaders::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_ImageHeaders* ClientDownloadRequest_ImageHeaders::default_instance_ = NULL;
+
+ClientDownloadRequest_ImageHeaders* ClientDownloadRequest_ImageHeaders::New() const {
+ return new ClientDownloadRequest_ImageHeaders;
+}
+
+void ClientDownloadRequest_ImageHeaders::Clear() {
+ if (has_pe_headers()) {
+ if (pe_headers_ != NULL) pe_headers_->::safe_browsing::ClientDownloadRequest_PEImageHeaders::Clear();
+ }
+ mach_o_headers_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_ImageHeaders::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.ImageHeaders)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientDownloadRequest.PEImageHeaders pe_headers = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_pe_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_mach_o_headers;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders mach_o_headers = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_mach_o_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_mach_o_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_mach_o_headers;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.ImageHeaders)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.ImageHeaders)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_ImageHeaders::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.ImageHeaders)
+ // optional .safe_browsing.ClientDownloadRequest.PEImageHeaders pe_headers = 1;
+ if (has_pe_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->pe_headers(), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders mach_o_headers = 2;
+ for (int i = 0; i < this->mach_o_headers_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->mach_o_headers(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.ImageHeaders)
+}
+
+int ClientDownloadRequest_ImageHeaders::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientDownloadRequest.PEImageHeaders pe_headers = 1;
+ if (has_pe_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->pe_headers());
+ }
+
+ }
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders mach_o_headers = 2;
+ total_size += 1 * this->mach_o_headers_size();
+ for (int i = 0; i < this->mach_o_headers_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->mach_o_headers(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_ImageHeaders::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_ImageHeaders*>(&from));
+}
+
+void ClientDownloadRequest_ImageHeaders::MergeFrom(const ClientDownloadRequest_ImageHeaders& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ mach_o_headers_.MergeFrom(from.mach_o_headers_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_pe_headers()) {
+ mutable_pe_headers()->::safe_browsing::ClientDownloadRequest_PEImageHeaders::MergeFrom(from.pe_headers());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_ImageHeaders::CopyFrom(const ClientDownloadRequest_ImageHeaders& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_ImageHeaders::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->mach_o_headers())) return false;
+ return true;
+}
+
+void ClientDownloadRequest_ImageHeaders::Swap(ClientDownloadRequest_ImageHeaders* other) {
+ if (other != this) {
+ std::swap(pe_headers_, other->pe_headers_);
+ mach_o_headers_.Swap(&other->mach_o_headers_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_ImageHeaders::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.ImageHeaders";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest_ArchivedBinary::kFileBasenameFieldNumber;
+const int ClientDownloadRequest_ArchivedBinary::kDownloadTypeFieldNumber;
+const int ClientDownloadRequest_ArchivedBinary::kDigestsFieldNumber;
+const int ClientDownloadRequest_ArchivedBinary::kLengthFieldNumber;
+const int ClientDownloadRequest_ArchivedBinary::kSignatureFieldNumber;
+const int ClientDownloadRequest_ArchivedBinary::kImageHeadersFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_ArchivedBinary::ClientDownloadRequest_ArchivedBinary()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+}
+
+void ClientDownloadRequest_ArchivedBinary::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ digests_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(
+ ::safe_browsing::ClientDownloadRequest_Digests::internal_default_instance());
+#else
+ digests_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(&::safe_browsing::ClientDownloadRequest_Digests::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo::internal_default_instance());
+#else
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(&::safe_browsing::ClientDownloadRequest_SignatureInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+}
+
+ClientDownloadRequest_ArchivedBinary::ClientDownloadRequest_ArchivedBinary(const ClientDownloadRequest_ArchivedBinary& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+}
+
+void ClientDownloadRequest_ArchivedBinary::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ download_type_ = 0;
+ digests_ = NULL;
+ length_ = GOOGLE_LONGLONG(0);
+ signature_ = NULL;
+ image_headers_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_ArchivedBinary::~ClientDownloadRequest_ArchivedBinary() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_ArchivedBinary::SharedDtor() {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_basename_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete digests_;
+ delete signature_;
+ delete image_headers_;
+ }
+}
+
+void ClientDownloadRequest_ArchivedBinary::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_ArchivedBinary& ClientDownloadRequest_ArchivedBinary::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_ArchivedBinary* ClientDownloadRequest_ArchivedBinary::default_instance_ = NULL;
+
+ClientDownloadRequest_ArchivedBinary* ClientDownloadRequest_ArchivedBinary::New() const {
+ return new ClientDownloadRequest_ArchivedBinary;
+}
+
+void ClientDownloadRequest_ArchivedBinary::Clear() {
+ if (_has_bits_[0 / 32] & 63) {
+ if (has_file_basename()) {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_->clear();
+ }
+ }
+ download_type_ = 0;
+ if (has_digests()) {
+ if (digests_ != NULL) digests_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ }
+ length_ = GOOGLE_LONGLONG(0);
+ if (has_signature()) {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ }
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_ArchivedBinary::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string file_basename = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_file_basename()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_download_type;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_download_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadRequest_DownloadType_IsValid(value)) {
+ set_download_type(static_cast< ::safe_browsing::ClientDownloadRequest_DownloadType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_digests;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digests = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_digests:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_digests()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_length;
+ break;
+ }
+
+ // optional int64 length = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_length:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &length_)));
+ set_has_length();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_ArchivedBinary::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+ // optional string file_basename = 1;
+ if (has_file_basename()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->file_basename(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 2;
+ if (has_download_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->download_type(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digests = 3;
+ if (has_digests()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->digests(), output);
+ }
+
+ // optional int64 length = 4;
+ if (has_length()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(4, this->length(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ if (has_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->signature(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 6, this->image_headers(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+}
+
+int ClientDownloadRequest_ArchivedBinary::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string file_basename = 1;
+ if (has_file_basename()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->file_basename());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 2;
+ if (has_download_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->download_type());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digests = 3;
+ if (has_digests()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->digests());
+ }
+
+ // optional int64 length = 4;
+ if (has_length()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->length());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ if (has_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->signature());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ if (has_image_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_ArchivedBinary::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_ArchivedBinary*>(&from));
+}
+
+void ClientDownloadRequest_ArchivedBinary::MergeFrom(const ClientDownloadRequest_ArchivedBinary& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_file_basename()) {
+ set_file_basename(from.file_basename());
+ }
+ if (from.has_download_type()) {
+ set_download_type(from.download_type());
+ }
+ if (from.has_digests()) {
+ mutable_digests()->::safe_browsing::ClientDownloadRequest_Digests::MergeFrom(from.digests());
+ }
+ if (from.has_length()) {
+ set_length(from.length());
+ }
+ if (from.has_signature()) {
+ mutable_signature()->::safe_browsing::ClientDownloadRequest_SignatureInfo::MergeFrom(from.signature());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_ArchivedBinary::CopyFrom(const ClientDownloadRequest_ArchivedBinary& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_ArchivedBinary::IsInitialized() const {
+
+ if (has_signature()) {
+ if (!this->signature().IsInitialized()) return false;
+ }
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientDownloadRequest_ArchivedBinary::Swap(ClientDownloadRequest_ArchivedBinary* other) {
+ if (other != this) {
+ std::swap(file_basename_, other->file_basename_);
+ std::swap(download_type_, other->download_type_);
+ std::swap(digests_, other->digests_);
+ std::swap(length_, other->length_);
+ std::swap(signature_, other->signature_);
+ std::swap(image_headers_, other->image_headers_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_ArchivedBinary::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.ArchivedBinary";
+}
+
+
+// -------------------------------------------------------------------
+
+bool ClientDownloadRequest_URLChainEntry_URLType_IsValid(int value) {
+ switch(value) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::DOWNLOAD_URL;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::DOWNLOAD_REFERRER;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::LANDING_PAGE;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::LANDING_REFERRER;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::CLIENT_REDIRECT;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::SERVER_REDIRECT;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::URLType_MIN;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::URLType_MAX;
+const int ClientDownloadRequest_URLChainEntry::URLType_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientDownloadRequest_URLChainEntry::kUrlFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kTypeFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kIpAddressFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kReferrerFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kMainFrameReferrerFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kIsRetargetingFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kIsUserInitiatedFieldNumber;
+const int ClientDownloadRequest_URLChainEntry::kTimestampInMillisecFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest_URLChainEntry::ClientDownloadRequest_URLChainEntry()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest.URLChainEntry)
+}
+
+void ClientDownloadRequest_URLChainEntry::InitAsDefaultInstance() {
+}
+
+ClientDownloadRequest_URLChainEntry::ClientDownloadRequest_URLChainEntry(const ClientDownloadRequest_URLChainEntry& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest.URLChainEntry)
+}
+
+void ClientDownloadRequest_URLChainEntry::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ type_ = 1;
+ ip_address_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ main_frame_referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ is_retargeting_ = false;
+ is_user_initiated_ = false;
+ timestamp_in_millisec_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest_URLChainEntry::~ClientDownloadRequest_URLChainEntry() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest.URLChainEntry)
+ SharedDtor();
+}
+
+void ClientDownloadRequest_URLChainEntry::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (ip_address_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete ip_address_;
+ }
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ if (main_frame_referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete main_frame_referrer_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadRequest_URLChainEntry::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest_URLChainEntry& ClientDownloadRequest_URLChainEntry::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest_URLChainEntry* ClientDownloadRequest_URLChainEntry::default_instance_ = NULL;
+
+ClientDownloadRequest_URLChainEntry* ClientDownloadRequest_URLChainEntry::New() const {
+ return new ClientDownloadRequest_URLChainEntry;
+}
+
+void ClientDownloadRequest_URLChainEntry::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientDownloadRequest_URLChainEntry*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 255) {
+ ZR_(is_retargeting_, timestamp_in_millisec_);
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ type_ = 1;
+ if (has_ip_address()) {
+ if (ip_address_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_address_->clear();
+ }
+ }
+ if (has_referrer()) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ }
+ if (has_main_frame_referrer()) {
+ if (main_frame_referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ main_frame_referrer_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest_URLChainEntry::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest.URLChainEntry)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_type;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.URLChainEntry.URLType type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType_IsValid(value)) {
+ set_type(static_cast< ::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_ip_address;
+ break;
+ }
+
+ // optional string ip_address = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_ip_address:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_ip_address()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_referrer;
+ break;
+ }
+
+ // optional string referrer = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_referrer:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_referrer()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_main_frame_referrer;
+ break;
+ }
+
+ // optional string main_frame_referrer = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_main_frame_referrer:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_main_frame_referrer()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_is_retargeting;
+ break;
+ }
+
+ // optional bool is_retargeting = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_is_retargeting:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &is_retargeting_)));
+ set_has_is_retargeting();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(56)) goto parse_is_user_initiated;
+ break;
+ }
+
+ // optional bool is_user_initiated = 7;
+ case 7: {
+ if (tag == 56) {
+ parse_is_user_initiated:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &is_user_initiated_)));
+ set_has_is_user_initiated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(65)) goto parse_timestamp_in_millisec;
+ break;
+ }
+
+ // optional double timestamp_in_millisec = 8;
+ case 8: {
+ if (tag == 65) {
+ parse_timestamp_in_millisec:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ double, ::google::protobuf::internal::WireFormatLite::TYPE_DOUBLE>(
+ input, &timestamp_in_millisec_)));
+ set_has_timestamp_in_millisec();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest.URLChainEntry)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest.URLChainEntry)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest_URLChainEntry::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest.URLChainEntry)
+ // optional string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.URLChainEntry.URLType type = 2;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->type(), output);
+ }
+
+ // optional string ip_address = 3;
+ if (has_ip_address()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->ip_address(), output);
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->referrer(), output);
+ }
+
+ // optional string main_frame_referrer = 5;
+ if (has_main_frame_referrer()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 5, this->main_frame_referrer(), output);
+ }
+
+ // optional bool is_retargeting = 6;
+ if (has_is_retargeting()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(6, this->is_retargeting(), output);
+ }
+
+ // optional bool is_user_initiated = 7;
+ if (has_is_user_initiated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(7, this->is_user_initiated(), output);
+ }
+
+ // optional double timestamp_in_millisec = 8;
+ if (has_timestamp_in_millisec()) {
+ ::google::protobuf::internal::WireFormatLite::WriteDouble(8, this->timestamp_in_millisec(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest.URLChainEntry)
+}
+
+int ClientDownloadRequest_URLChainEntry::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.URLChainEntry.URLType type = 2;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+ }
+
+ // optional string ip_address = 3;
+ if (has_ip_address()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->ip_address());
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->referrer());
+ }
+
+ // optional string main_frame_referrer = 5;
+ if (has_main_frame_referrer()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->main_frame_referrer());
+ }
+
+ // optional bool is_retargeting = 6;
+ if (has_is_retargeting()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool is_user_initiated = 7;
+ if (has_is_user_initiated()) {
+ total_size += 1 + 1;
+ }
+
+ // optional double timestamp_in_millisec = 8;
+ if (has_timestamp_in_millisec()) {
+ total_size += 1 + 8;
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest_URLChainEntry::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest_URLChainEntry*>(&from));
+}
+
+void ClientDownloadRequest_URLChainEntry::MergeFrom(const ClientDownloadRequest_URLChainEntry& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_ip_address()) {
+ set_ip_address(from.ip_address());
+ }
+ if (from.has_referrer()) {
+ set_referrer(from.referrer());
+ }
+ if (from.has_main_frame_referrer()) {
+ set_main_frame_referrer(from.main_frame_referrer());
+ }
+ if (from.has_is_retargeting()) {
+ set_is_retargeting(from.is_retargeting());
+ }
+ if (from.has_is_user_initiated()) {
+ set_is_user_initiated(from.is_user_initiated());
+ }
+ if (from.has_timestamp_in_millisec()) {
+ set_timestamp_in_millisec(from.timestamp_in_millisec());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest_URLChainEntry::CopyFrom(const ClientDownloadRequest_URLChainEntry& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest_URLChainEntry::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadRequest_URLChainEntry::Swap(ClientDownloadRequest_URLChainEntry* other) {
+ if (other != this) {
+ std::swap(url_, other->url_);
+ std::swap(type_, other->type_);
+ std::swap(ip_address_, other->ip_address_);
+ std::swap(referrer_, other->referrer_);
+ std::swap(main_frame_referrer_, other->main_frame_referrer_);
+ std::swap(is_retargeting_, other->is_retargeting_);
+ std::swap(is_user_initiated_, other->is_user_initiated_);
+ std::swap(timestamp_in_millisec_, other->timestamp_in_millisec_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest_URLChainEntry::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest.URLChainEntry";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadRequest::kUrlFieldNumber;
+const int ClientDownloadRequest::kDigestsFieldNumber;
+const int ClientDownloadRequest::kLengthFieldNumber;
+const int ClientDownloadRequest::kResourcesFieldNumber;
+const int ClientDownloadRequest::kSignatureFieldNumber;
+const int ClientDownloadRequest::kUserInitiatedFieldNumber;
+const int ClientDownloadRequest::kFileBasenameFieldNumber;
+const int ClientDownloadRequest::kDownloadTypeFieldNumber;
+const int ClientDownloadRequest::kLocaleFieldNumber;
+const int ClientDownloadRequest::kImageHeadersFieldNumber;
+const int ClientDownloadRequest::kArchivedBinaryFieldNumber;
+const int ClientDownloadRequest::kPopulationFieldNumber;
+const int ClientDownloadRequest::kArchiveValidFieldNumber;
+const int ClientDownloadRequest::kSkippedUrlWhitelistFieldNumber;
+const int ClientDownloadRequest::kSkippedCertificateWhitelistFieldNumber;
+const int ClientDownloadRequest::kAlternateExtensionsFieldNumber;
+const int ClientDownloadRequest::kUrlChainFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadRequest::ClientDownloadRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadRequest)
+}
+
+void ClientDownloadRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ digests_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(
+ ::safe_browsing::ClientDownloadRequest_Digests::internal_default_instance());
+#else
+ digests_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(&::safe_browsing::ClientDownloadRequest_Digests::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo::internal_default_instance());
+#else
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(&::safe_browsing::ClientDownloadRequest_SignatureInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(
+ ::safe_browsing::ChromeUserPopulation::internal_default_instance());
+#else
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(&::safe_browsing::ChromeUserPopulation::default_instance());
+#endif
+}
+
+ClientDownloadRequest::ClientDownloadRequest(const ClientDownloadRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadRequest)
+}
+
+void ClientDownloadRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ digests_ = NULL;
+ length_ = GOOGLE_LONGLONG(0);
+ signature_ = NULL;
+ user_initiated_ = false;
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ download_type_ = 0;
+ locale_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ image_headers_ = NULL;
+ population_ = NULL;
+ archive_valid_ = false;
+ skipped_url_whitelist_ = false;
+ skipped_certificate_whitelist_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadRequest::~ClientDownloadRequest() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadRequest)
+ SharedDtor();
+}
+
+void ClientDownloadRequest::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_basename_;
+ }
+ if (locale_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete locale_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete digests_;
+ delete signature_;
+ delete image_headers_;
+ delete population_;
+ }
+}
+
+void ClientDownloadRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadRequest& ClientDownloadRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadRequest* ClientDownloadRequest::default_instance_ = NULL;
+
+ClientDownloadRequest* ClientDownloadRequest::New() const {
+ return new ClientDownloadRequest;
+}
+
+void ClientDownloadRequest::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientDownloadRequest*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 247) {
+ ZR_(download_type_, user_initiated_);
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ if (has_digests()) {
+ if (digests_ != NULL) digests_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ }
+ length_ = GOOGLE_LONGLONG(0);
+ if (has_signature()) {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ }
+ if (has_file_basename()) {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_->clear();
+ }
+ }
+ }
+ if (_has_bits_[8 / 32] & 31488) {
+ ZR_(archive_valid_, skipped_certificate_whitelist_);
+ if (has_locale()) {
+ if (locale_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ locale_->clear();
+ }
+ }
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ if (has_population()) {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ resources_.Clear();
+ archived_binary_.Clear();
+ alternate_extensions_.Clear();
+ url_chain_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_digests;
+ break;
+ }
+
+ // required .safe_browsing.ClientDownloadRequest.Digests digests = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_digests:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_digests()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_length;
+ break;
+ }
+
+ // required int64 length = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_length:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &length_)));
+ set_has_length();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_resources;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.Resource resources = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_resources:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_resources()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_resources;
+ if (input->ExpectTag(42)) goto parse_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_user_initiated;
+ break;
+ }
+
+ // optional bool user_initiated = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_user_initiated:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &user_initiated_)));
+ set_has_user_initiated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(74)) goto parse_file_basename;
+ break;
+ }
+
+ // optional string file_basename = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_file_basename:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_file_basename()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_download_type;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 10 [default = WIN_EXECUTABLE];
+ case 10: {
+ if (tag == 80) {
+ parse_download_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadRequest_DownloadType_IsValid(value)) {
+ set_download_type(static_cast< ::safe_browsing::ClientDownloadRequest_DownloadType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(90)) goto parse_locale;
+ break;
+ }
+
+ // optional string locale = 11;
+ case 11: {
+ if (tag == 90) {
+ parse_locale:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_locale()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(146)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 18;
+ case 18: {
+ if (tag == 146) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(178)) goto parse_archived_binary;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.ArchivedBinary archived_binary = 22;
+ case 22: {
+ if (tag == 178) {
+ parse_archived_binary:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_archived_binary()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(178)) goto parse_archived_binary;
+ if (input->ExpectTag(194)) goto parse_population;
+ break;
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 24;
+ case 24: {
+ if (tag == 194) {
+ parse_population:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_population()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(208)) goto parse_archive_valid;
+ break;
+ }
+
+ // optional bool archive_valid = 26;
+ case 26: {
+ if (tag == 208) {
+ parse_archive_valid:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &archive_valid_)));
+ set_has_archive_valid();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(224)) goto parse_skipped_url_whitelist;
+ break;
+ }
+
+ // optional bool skipped_url_whitelist = 28;
+ case 28: {
+ if (tag == 224) {
+ parse_skipped_url_whitelist:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &skipped_url_whitelist_)));
+ set_has_skipped_url_whitelist();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(248)) goto parse_skipped_certificate_whitelist;
+ break;
+ }
+
+ // optional bool skipped_certificate_whitelist = 31;
+ case 31: {
+ if (tag == 248) {
+ parse_skipped_certificate_whitelist:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &skipped_certificate_whitelist_)));
+ set_has_skipped_certificate_whitelist();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(282)) goto parse_alternate_extensions;
+ break;
+ }
+
+ // repeated string alternate_extensions = 35;
+ case 35: {
+ if (tag == 282) {
+ parse_alternate_extensions:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_alternate_extensions()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(282)) goto parse_alternate_extensions;
+ if (input->ExpectTag(290)) goto parse_url_chain;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.URLChainEntry url_chain = 36;
+ case 36: {
+ if (tag == 290) {
+ parse_url_chain:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_url_chain()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(290)) goto parse_url_chain;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadRequest)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadRequest)
+ // required string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // required .safe_browsing.ClientDownloadRequest.Digests digests = 2;
+ if (has_digests()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->digests(), output);
+ }
+
+ // required int64 length = 3;
+ if (has_length()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(3, this->length(), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.Resource resources = 4;
+ for (int i = 0; i < this->resources_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->resources(i), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ if (has_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->signature(), output);
+ }
+
+ // optional bool user_initiated = 6;
+ if (has_user_initiated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(6, this->user_initiated(), output);
+ }
+
+ // optional string file_basename = 9;
+ if (has_file_basename()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 9, this->file_basename(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 10 [default = WIN_EXECUTABLE];
+ if (has_download_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 10, this->download_type(), output);
+ }
+
+ // optional string locale = 11;
+ if (has_locale()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 11, this->locale(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 18;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 18, this->image_headers(), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.ArchivedBinary archived_binary = 22;
+ for (int i = 0; i < this->archived_binary_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 22, this->archived_binary(i), output);
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 24;
+ if (has_population()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 24, this->population(), output);
+ }
+
+ // optional bool archive_valid = 26;
+ if (has_archive_valid()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(26, this->archive_valid(), output);
+ }
+
+ // optional bool skipped_url_whitelist = 28;
+ if (has_skipped_url_whitelist()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(28, this->skipped_url_whitelist(), output);
+ }
+
+ // optional bool skipped_certificate_whitelist = 31;
+ if (has_skipped_certificate_whitelist()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(31, this->skipped_certificate_whitelist(), output);
+ }
+
+ // repeated string alternate_extensions = 35;
+ for (int i = 0; i < this->alternate_extensions_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 35, this->alternate_extensions(i), output);
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.URLChainEntry url_chain = 36;
+ for (int i = 0; i < this->url_chain_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 36, this->url_chain(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadRequest)
+}
+
+int ClientDownloadRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // required .safe_browsing.ClientDownloadRequest.Digests digests = 2;
+ if (has_digests()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->digests());
+ }
+
+ // required int64 length = 3;
+ if (has_length()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->length());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ if (has_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->signature());
+ }
+
+ // optional bool user_initiated = 6;
+ if (has_user_initiated()) {
+ total_size += 1 + 1;
+ }
+
+ // optional string file_basename = 9;
+ if (has_file_basename()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->file_basename());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 10 [default = WIN_EXECUTABLE];
+ if (has_download_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->download_type());
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional string locale = 11;
+ if (has_locale()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->locale());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 18;
+ if (has_image_headers()) {
+ total_size += 2 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 24;
+ if (has_population()) {
+ total_size += 2 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->population());
+ }
+
+ // optional bool archive_valid = 26;
+ if (has_archive_valid()) {
+ total_size += 2 + 1;
+ }
+
+ // optional bool skipped_url_whitelist = 28;
+ if (has_skipped_url_whitelist()) {
+ total_size += 2 + 1;
+ }
+
+ // optional bool skipped_certificate_whitelist = 31;
+ if (has_skipped_certificate_whitelist()) {
+ total_size += 2 + 1;
+ }
+
+ }
+ // repeated .safe_browsing.ClientDownloadRequest.Resource resources = 4;
+ total_size += 1 * this->resources_size();
+ for (int i = 0; i < this->resources_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->resources(i));
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.ArchivedBinary archived_binary = 22;
+ total_size += 2 * this->archived_binary_size();
+ for (int i = 0; i < this->archived_binary_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->archived_binary(i));
+ }
+
+ // repeated string alternate_extensions = 35;
+ total_size += 2 * this->alternate_extensions_size();
+ for (int i = 0; i < this->alternate_extensions_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->alternate_extensions(i));
+ }
+
+ // repeated .safe_browsing.ClientDownloadRequest.URLChainEntry url_chain = 36;
+ total_size += 2 * this->url_chain_size();
+ for (int i = 0; i < this->url_chain_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->url_chain(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadRequest*>(&from));
+}
+
+void ClientDownloadRequest::MergeFrom(const ClientDownloadRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ resources_.MergeFrom(from.resources_);
+ archived_binary_.MergeFrom(from.archived_binary_);
+ alternate_extensions_.MergeFrom(from.alternate_extensions_);
+ url_chain_.MergeFrom(from.url_chain_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_digests()) {
+ mutable_digests()->::safe_browsing::ClientDownloadRequest_Digests::MergeFrom(from.digests());
+ }
+ if (from.has_length()) {
+ set_length(from.length());
+ }
+ if (from.has_signature()) {
+ mutable_signature()->::safe_browsing::ClientDownloadRequest_SignatureInfo::MergeFrom(from.signature());
+ }
+ if (from.has_user_initiated()) {
+ set_user_initiated(from.user_initiated());
+ }
+ if (from.has_file_basename()) {
+ set_file_basename(from.file_basename());
+ }
+ if (from.has_download_type()) {
+ set_download_type(from.download_type());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_locale()) {
+ set_locale(from.locale());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ if (from.has_population()) {
+ mutable_population()->::safe_browsing::ChromeUserPopulation::MergeFrom(from.population());
+ }
+ if (from.has_archive_valid()) {
+ set_archive_valid(from.archive_valid());
+ }
+ if (from.has_skipped_url_whitelist()) {
+ set_skipped_url_whitelist(from.skipped_url_whitelist());
+ }
+ if (from.has_skipped_certificate_whitelist()) {
+ set_skipped_certificate_whitelist(from.skipped_certificate_whitelist());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadRequest::CopyFrom(const ClientDownloadRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadRequest::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000007) != 0x00000007) return false;
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->resources())) return false;
+ if (has_signature()) {
+ if (!this->signature().IsInitialized()) return false;
+ }
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ if (!::google::protobuf::internal::AllAreInitialized(this->archived_binary())) return false;
+ return true;
+}
+
+void ClientDownloadRequest::Swap(ClientDownloadRequest* other) {
+ if (other != this) {
+ std::swap(url_, other->url_);
+ std::swap(digests_, other->digests_);
+ std::swap(length_, other->length_);
+ resources_.Swap(&other->resources_);
+ std::swap(signature_, other->signature_);
+ std::swap(user_initiated_, other->user_initiated_);
+ std::swap(file_basename_, other->file_basename_);
+ std::swap(download_type_, other->download_type_);
+ std::swap(locale_, other->locale_);
+ std::swap(image_headers_, other->image_headers_);
+ archived_binary_.Swap(&other->archived_binary_);
+ std::swap(population_, other->population_);
+ std::swap(archive_valid_, other->archive_valid_);
+ std::swap(skipped_url_whitelist_, other->skipped_url_whitelist_);
+ std::swap(skipped_certificate_whitelist_, other->skipped_certificate_whitelist_);
+ alternate_extensions_.Swap(&other->alternate_extensions_);
+ url_chain_.Swap(&other->url_chain_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadRequest::GetTypeName() const {
+ return "safe_browsing.ClientDownloadRequest";
+}
+
+
+// ===================================================================
+
+bool ClientDownloadResponse_Verdict_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientDownloadResponse_Verdict ClientDownloadResponse::SAFE;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::DANGEROUS;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::UNCOMMON;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::POTENTIALLY_UNWANTED;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::DANGEROUS_HOST;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::UNKNOWN;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::Verdict_MIN;
+const ClientDownloadResponse_Verdict ClientDownloadResponse::Verdict_MAX;
+const int ClientDownloadResponse::Verdict_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientDownloadResponse_MoreInfo::kDescriptionFieldNumber;
+const int ClientDownloadResponse_MoreInfo::kUrlFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadResponse_MoreInfo::ClientDownloadResponse_MoreInfo()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadResponse.MoreInfo)
+}
+
+void ClientDownloadResponse_MoreInfo::InitAsDefaultInstance() {
+}
+
+ClientDownloadResponse_MoreInfo::ClientDownloadResponse_MoreInfo(const ClientDownloadResponse_MoreInfo& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadResponse.MoreInfo)
+}
+
+void ClientDownloadResponse_MoreInfo::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ description_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadResponse_MoreInfo::~ClientDownloadResponse_MoreInfo() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadResponse.MoreInfo)
+ SharedDtor();
+}
+
+void ClientDownloadResponse_MoreInfo::SharedDtor() {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete description_;
+ }
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadResponse_MoreInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadResponse_MoreInfo& ClientDownloadResponse_MoreInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadResponse_MoreInfo* ClientDownloadResponse_MoreInfo::default_instance_ = NULL;
+
+ClientDownloadResponse_MoreInfo* ClientDownloadResponse_MoreInfo::New() const {
+ return new ClientDownloadResponse_MoreInfo;
+}
+
+void ClientDownloadResponse_MoreInfo::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_description()) {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_->clear();
+ }
+ }
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadResponse_MoreInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadResponse.MoreInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string description = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_description()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_url;
+ break;
+ }
+
+ // optional string url = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadResponse.MoreInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadResponse.MoreInfo)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadResponse_MoreInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadResponse.MoreInfo)
+ // optional string description = 1;
+ if (has_description()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->description(), output);
+ }
+
+ // optional string url = 2;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->url(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadResponse.MoreInfo)
+}
+
+int ClientDownloadResponse_MoreInfo::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string description = 1;
+ if (has_description()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->description());
+ }
+
+ // optional string url = 2;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadResponse_MoreInfo::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadResponse_MoreInfo*>(&from));
+}
+
+void ClientDownloadResponse_MoreInfo::MergeFrom(const ClientDownloadResponse_MoreInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_description()) {
+ set_description(from.description());
+ }
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadResponse_MoreInfo::CopyFrom(const ClientDownloadResponse_MoreInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadResponse_MoreInfo::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadResponse_MoreInfo::Swap(ClientDownloadResponse_MoreInfo* other) {
+ if (other != this) {
+ std::swap(description_, other->description_);
+ std::swap(url_, other->url_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadResponse_MoreInfo::GetTypeName() const {
+ return "safe_browsing.ClientDownloadResponse.MoreInfo";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadResponse::kVerdictFieldNumber;
+const int ClientDownloadResponse::kMoreInfoFieldNumber;
+const int ClientDownloadResponse::kTokenFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadResponse::ClientDownloadResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadResponse)
+}
+
+void ClientDownloadResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ more_info_ = const_cast< ::safe_browsing::ClientDownloadResponse_MoreInfo*>(
+ ::safe_browsing::ClientDownloadResponse_MoreInfo::internal_default_instance());
+#else
+ more_info_ = const_cast< ::safe_browsing::ClientDownloadResponse_MoreInfo*>(&::safe_browsing::ClientDownloadResponse_MoreInfo::default_instance());
+#endif
+}
+
+ClientDownloadResponse::ClientDownloadResponse(const ClientDownloadResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadResponse)
+}
+
+void ClientDownloadResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ verdict_ = 0;
+ more_info_ = NULL;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadResponse::~ClientDownloadResponse() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadResponse)
+ SharedDtor();
+}
+
+void ClientDownloadResponse::SharedDtor() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete more_info_;
+ }
+}
+
+void ClientDownloadResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadResponse& ClientDownloadResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadResponse* ClientDownloadResponse::default_instance_ = NULL;
+
+ClientDownloadResponse* ClientDownloadResponse::New() const {
+ return new ClientDownloadResponse;
+}
+
+void ClientDownloadResponse::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ verdict_ = 0;
+ if (has_more_info()) {
+ if (more_info_ != NULL) more_info_->::safe_browsing::ClientDownloadResponse_MoreInfo::Clear();
+ }
+ if (has_token()) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientDownloadResponse.Verdict verdict = 1 [default = SAFE];
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadResponse_Verdict_IsValid(value)) {
+ set_verdict(static_cast< ::safe_browsing::ClientDownloadResponse_Verdict >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_more_info;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse.MoreInfo more_info = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_more_info:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_more_info()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_token;
+ break;
+ }
+
+ // optional bytes token = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_token:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_token()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadResponse)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadResponse)
+ // optional .safe_browsing.ClientDownloadResponse.Verdict verdict = 1 [default = SAFE];
+ if (has_verdict()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->verdict(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse.MoreInfo more_info = 2;
+ if (has_more_info()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->more_info(), output);
+ }
+
+ // optional bytes token = 3;
+ if (has_token()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->token(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadResponse)
+}
+
+int ClientDownloadResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientDownloadResponse.Verdict verdict = 1 [default = SAFE];
+ if (has_verdict()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->verdict());
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse.MoreInfo more_info = 2;
+ if (has_more_info()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->more_info());
+ }
+
+ // optional bytes token = 3;
+ if (has_token()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->token());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadResponse*>(&from));
+}
+
+void ClientDownloadResponse::MergeFrom(const ClientDownloadResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_verdict()) {
+ set_verdict(from.verdict());
+ }
+ if (from.has_more_info()) {
+ mutable_more_info()->::safe_browsing::ClientDownloadResponse_MoreInfo::MergeFrom(from.more_info());
+ }
+ if (from.has_token()) {
+ set_token(from.token());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadResponse::CopyFrom(const ClientDownloadResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadResponse::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadResponse::Swap(ClientDownloadResponse* other) {
+ if (other != this) {
+ std::swap(verdict_, other->verdict_);
+ std::swap(more_info_, other->more_info_);
+ std::swap(token_, other->token_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadResponse::GetTypeName() const {
+ return "safe_browsing.ClientDownloadResponse";
+}
+
+
+// ===================================================================
+
+bool ClientDownloadReport_Reason_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientDownloadReport_Reason ClientDownloadReport::SHARE;
+const ClientDownloadReport_Reason ClientDownloadReport::FALSE_POSITIVE;
+const ClientDownloadReport_Reason ClientDownloadReport::APPEAL;
+const ClientDownloadReport_Reason ClientDownloadReport::Reason_MIN;
+const ClientDownloadReport_Reason ClientDownloadReport::Reason_MAX;
+const int ClientDownloadReport::Reason_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientDownloadReport_UserInformation::kEmailFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadReport_UserInformation::ClientDownloadReport_UserInformation()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadReport.UserInformation)
+}
+
+void ClientDownloadReport_UserInformation::InitAsDefaultInstance() {
+}
+
+ClientDownloadReport_UserInformation::ClientDownloadReport_UserInformation(const ClientDownloadReport_UserInformation& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadReport.UserInformation)
+}
+
+void ClientDownloadReport_UserInformation::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ email_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadReport_UserInformation::~ClientDownloadReport_UserInformation() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadReport.UserInformation)
+ SharedDtor();
+}
+
+void ClientDownloadReport_UserInformation::SharedDtor() {
+ if (email_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete email_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientDownloadReport_UserInformation::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadReport_UserInformation& ClientDownloadReport_UserInformation::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadReport_UserInformation* ClientDownloadReport_UserInformation::default_instance_ = NULL;
+
+ClientDownloadReport_UserInformation* ClientDownloadReport_UserInformation::New() const {
+ return new ClientDownloadReport_UserInformation;
+}
+
+void ClientDownloadReport_UserInformation::Clear() {
+ if (has_email()) {
+ if (email_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ email_->clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadReport_UserInformation::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadReport.UserInformation)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string email = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_email()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadReport.UserInformation)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadReport.UserInformation)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadReport_UserInformation::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadReport.UserInformation)
+ // optional string email = 1;
+ if (has_email()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->email(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadReport.UserInformation)
+}
+
+int ClientDownloadReport_UserInformation::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string email = 1;
+ if (has_email()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->email());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadReport_UserInformation::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadReport_UserInformation*>(&from));
+}
+
+void ClientDownloadReport_UserInformation::MergeFrom(const ClientDownloadReport_UserInformation& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_email()) {
+ set_email(from.email());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadReport_UserInformation::CopyFrom(const ClientDownloadReport_UserInformation& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadReport_UserInformation::IsInitialized() const {
+
+ return true;
+}
+
+void ClientDownloadReport_UserInformation::Swap(ClientDownloadReport_UserInformation* other) {
+ if (other != this) {
+ std::swap(email_, other->email_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadReport_UserInformation::GetTypeName() const {
+ return "safe_browsing.ClientDownloadReport.UserInformation";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientDownloadReport::kReasonFieldNumber;
+const int ClientDownloadReport::kDownloadRequestFieldNumber;
+const int ClientDownloadReport::kUserInformationFieldNumber;
+const int ClientDownloadReport::kCommentFieldNumber;
+const int ClientDownloadReport::kDownloadResponseFieldNumber;
+#endif // !_MSC_VER
+
+ClientDownloadReport::ClientDownloadReport()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientDownloadReport)
+}
+
+void ClientDownloadReport::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ download_request_ = const_cast< ::safe_browsing::ClientDownloadRequest*>(
+ ::safe_browsing::ClientDownloadRequest::internal_default_instance());
+#else
+ download_request_ = const_cast< ::safe_browsing::ClientDownloadRequest*>(&::safe_browsing::ClientDownloadRequest::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ user_information_ = const_cast< ::safe_browsing::ClientDownloadReport_UserInformation*>(
+ ::safe_browsing::ClientDownloadReport_UserInformation::internal_default_instance());
+#else
+ user_information_ = const_cast< ::safe_browsing::ClientDownloadReport_UserInformation*>(&::safe_browsing::ClientDownloadReport_UserInformation::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ download_response_ = const_cast< ::safe_browsing::ClientDownloadResponse*>(
+ ::safe_browsing::ClientDownloadResponse::internal_default_instance());
+#else
+ download_response_ = const_cast< ::safe_browsing::ClientDownloadResponse*>(&::safe_browsing::ClientDownloadResponse::default_instance());
+#endif
+}
+
+ClientDownloadReport::ClientDownloadReport(const ClientDownloadReport& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientDownloadReport)
+}
+
+void ClientDownloadReport::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ reason_ = 0;
+ download_request_ = NULL;
+ user_information_ = NULL;
+ comment_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ download_response_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientDownloadReport::~ClientDownloadReport() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientDownloadReport)
+ SharedDtor();
+}
+
+void ClientDownloadReport::SharedDtor() {
+ if (comment_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete comment_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete download_request_;
+ delete user_information_;
+ delete download_response_;
+ }
+}
+
+void ClientDownloadReport::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientDownloadReport& ClientDownloadReport::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientDownloadReport* ClientDownloadReport::default_instance_ = NULL;
+
+ClientDownloadReport* ClientDownloadReport::New() const {
+ return new ClientDownloadReport;
+}
+
+void ClientDownloadReport::Clear() {
+ if (_has_bits_[0 / 32] & 31) {
+ reason_ = 0;
+ if (has_download_request()) {
+ if (download_request_ != NULL) download_request_->::safe_browsing::ClientDownloadRequest::Clear();
+ }
+ if (has_user_information()) {
+ if (user_information_ != NULL) user_information_->::safe_browsing::ClientDownloadReport_UserInformation::Clear();
+ }
+ if (has_comment()) {
+ if (comment_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ comment_->clear();
+ }
+ }
+ if (has_download_response()) {
+ if (download_response_ != NULL) download_response_->::safe_browsing::ClientDownloadResponse::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientDownloadReport::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientDownloadReport)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientDownloadReport.Reason reason = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadReport_Reason_IsValid(value)) {
+ set_reason(static_cast< ::safe_browsing::ClientDownloadReport_Reason >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_download_request;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest download_request = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_download_request:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_download_request()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_user_information;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadReport.UserInformation user_information = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_user_information:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_user_information()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_comment;
+ break;
+ }
+
+ // optional bytes comment = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_comment:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_comment()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_download_response;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse download_response = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_download_response:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_download_response()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientDownloadReport)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientDownloadReport)
+ return false;
+#undef DO_
+}
+
+void ClientDownloadReport::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientDownloadReport)
+ // optional .safe_browsing.ClientDownloadReport.Reason reason = 1;
+ if (has_reason()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->reason(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest download_request = 2;
+ if (has_download_request()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->download_request(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadReport.UserInformation user_information = 3;
+ if (has_user_information()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->user_information(), output);
+ }
+
+ // optional bytes comment = 4;
+ if (has_comment()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 4, this->comment(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse download_response = 5;
+ if (has_download_response()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->download_response(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientDownloadReport)
+}
+
+int ClientDownloadReport::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientDownloadReport.Reason reason = 1;
+ if (has_reason()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->reason());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest download_request = 2;
+ if (has_download_request()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->download_request());
+ }
+
+ // optional .safe_browsing.ClientDownloadReport.UserInformation user_information = 3;
+ if (has_user_information()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->user_information());
+ }
+
+ // optional bytes comment = 4;
+ if (has_comment()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->comment());
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse download_response = 5;
+ if (has_download_response()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->download_response());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientDownloadReport::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientDownloadReport*>(&from));
+}
+
+void ClientDownloadReport::MergeFrom(const ClientDownloadReport& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_reason()) {
+ set_reason(from.reason());
+ }
+ if (from.has_download_request()) {
+ mutable_download_request()->::safe_browsing::ClientDownloadRequest::MergeFrom(from.download_request());
+ }
+ if (from.has_user_information()) {
+ mutable_user_information()->::safe_browsing::ClientDownloadReport_UserInformation::MergeFrom(from.user_information());
+ }
+ if (from.has_comment()) {
+ set_comment(from.comment());
+ }
+ if (from.has_download_response()) {
+ mutable_download_response()->::safe_browsing::ClientDownloadResponse::MergeFrom(from.download_response());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientDownloadReport::CopyFrom(const ClientDownloadReport& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientDownloadReport::IsInitialized() const {
+
+ if (has_download_request()) {
+ if (!this->download_request().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientDownloadReport::Swap(ClientDownloadReport* other) {
+ if (other != this) {
+ std::swap(reason_, other->reason_);
+ std::swap(download_request_, other->download_request_);
+ std::swap(user_information_, other->user_information_);
+ std::swap(comment_, other->comment_);
+ std::swap(download_response_, other->download_response_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientDownloadReport::GetTypeName() const {
+ return "safe_browsing.ClientDownloadReport";
+}
+
+
+// ===================================================================
+
+bool ClientUploadResponse_UploadStatus_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientUploadResponse_UploadStatus ClientUploadResponse::SUCCESS;
+const ClientUploadResponse_UploadStatus ClientUploadResponse::UPLOAD_FAILURE;
+const ClientUploadResponse_UploadStatus ClientUploadResponse::UploadStatus_MIN;
+const ClientUploadResponse_UploadStatus ClientUploadResponse::UploadStatus_MAX;
+const int ClientUploadResponse::UploadStatus_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientUploadResponse::kStatusFieldNumber;
+const int ClientUploadResponse::kPermalinkFieldNumber;
+#endif // !_MSC_VER
+
+ClientUploadResponse::ClientUploadResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientUploadResponse)
+}
+
+void ClientUploadResponse::InitAsDefaultInstance() {
+}
+
+ClientUploadResponse::ClientUploadResponse(const ClientUploadResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientUploadResponse)
+}
+
+void ClientUploadResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ status_ = 0;
+ permalink_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientUploadResponse::~ClientUploadResponse() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientUploadResponse)
+ SharedDtor();
+}
+
+void ClientUploadResponse::SharedDtor() {
+ if (permalink_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete permalink_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientUploadResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientUploadResponse& ClientUploadResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientUploadResponse* ClientUploadResponse::default_instance_ = NULL;
+
+ClientUploadResponse* ClientUploadResponse::New() const {
+ return new ClientUploadResponse;
+}
+
+void ClientUploadResponse::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ status_ = 0;
+ if (has_permalink()) {
+ if (permalink_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ permalink_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientUploadResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientUploadResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientUploadResponse.UploadStatus status = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientUploadResponse_UploadStatus_IsValid(value)) {
+ set_status(static_cast< ::safe_browsing::ClientUploadResponse_UploadStatus >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_permalink;
+ break;
+ }
+
+ // optional string permalink = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_permalink:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_permalink()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientUploadResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientUploadResponse)
+ return false;
+#undef DO_
+}
+
+void ClientUploadResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientUploadResponse)
+ // optional .safe_browsing.ClientUploadResponse.UploadStatus status = 1;
+ if (has_status()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->status(), output);
+ }
+
+ // optional string permalink = 2;
+ if (has_permalink()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->permalink(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientUploadResponse)
+}
+
+int ClientUploadResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientUploadResponse.UploadStatus status = 1;
+ if (has_status()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->status());
+ }
+
+ // optional string permalink = 2;
+ if (has_permalink()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->permalink());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientUploadResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientUploadResponse*>(&from));
+}
+
+void ClientUploadResponse::MergeFrom(const ClientUploadResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_status()) {
+ set_status(from.status());
+ }
+ if (from.has_permalink()) {
+ set_permalink(from.permalink());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientUploadResponse::CopyFrom(const ClientUploadResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientUploadResponse::IsInitialized() const {
+
+ return true;
+}
+
+void ClientUploadResponse::Swap(ClientUploadResponse* other) {
+ if (other != this) {
+ std::swap(status_, other->status_);
+ std::swap(permalink_, other->permalink_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientUploadResponse::GetTypeName() const {
+ return "safe_browsing.ClientUploadResponse";
+}
+
+
+// ===================================================================
+
+bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::UNKNOWN;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::CLEARED;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::WEAK_LEGACY_OBSOLETE;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::CHANGED;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::UNTRUSTED_UNKNOWN_VALUE;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::ValueState_MIN;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::ValueState_MAX;
+const int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::ValueState_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::kPathFieldNumber;
+const int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::kAtomicValueFieldNumber;
+const int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::kSplitKeyFieldNumber;
+const int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::kValueStateFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident::ClientIncidentReport_IncidentData_TrackedPreferenceIncident()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident::ClientIncidentReport_IncidentData_TrackedPreferenceIncident(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ atomic_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ value_state_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident::~ClientIncidentReport_IncidentData_TrackedPreferenceIncident() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::SharedDtor() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (atomic_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete atomic_value_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& ClientIncidentReport_IncidentData_TrackedPreferenceIncident::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::New() const {
+ return new ClientIncidentReport_IncidentData_TrackedPreferenceIncident;
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::Clear() {
+ if (_has_bits_[0 / 32] & 11) {
+ if (has_path()) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ }
+ if (has_atomic_value()) {
+ if (atomic_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ atomic_value_->clear();
+ }
+ }
+ value_state_ = 0;
+ }
+ split_key_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string path = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_path()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_atomic_value;
+ break;
+ }
+
+ // optional string atomic_value = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_atomic_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_atomic_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_split_key;
+ break;
+ }
+
+ // repeated string split_key = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_split_key:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_split_key()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_split_key;
+ if (input->ExpectTag(32)) goto parse_value_state;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.ValueState value_state = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_value_state:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_IsValid(value)) {
+ set_value_state(static_cast< ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+ // optional string path = 1;
+ if (has_path()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->path(), output);
+ }
+
+ // optional string atomic_value = 2;
+ if (has_atomic_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->atomic_value(), output);
+ }
+
+ // repeated string split_key = 3;
+ for (int i = 0; i < this->split_key_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 3, this->split_key(i), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.ValueState value_state = 4;
+ if (has_value_state()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 4, this->value_state(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+}
+
+int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string path = 1;
+ if (has_path()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->path());
+ }
+
+ // optional string atomic_value = 2;
+ if (has_atomic_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->atomic_value());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.ValueState value_state = 4;
+ if (has_value_state()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->value_state());
+ }
+
+ }
+ // repeated string split_key = 3;
+ total_size += 1 * this->split_key_size();
+ for (int i = 0; i < this->split_key_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->split_key(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_TrackedPreferenceIncident*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::MergeFrom(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ split_key_.MergeFrom(from.split_key_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_path()) {
+ set_path(from.path());
+ }
+ if (from.has_atomic_value()) {
+ set_atomic_value(from.atomic_value());
+ }
+ if (from.has_value_state()) {
+ set_value_state(from.value_state());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::CopyFrom(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::Swap(ClientIncidentReport_IncidentData_TrackedPreferenceIncident* other) {
+ if (other != this) {
+ std::swap(path_, other->path_);
+ std::swap(atomic_value_, other->atomic_value_);
+ split_key_.Swap(&other->split_key_);
+ std::swap(value_state_, other->value_state_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_TrackedPreferenceIncident::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::kRelativePathFieldNumber;
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::kSignatureFieldNumber;
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::kImageHeadersFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo::internal_default_instance());
+#else
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(&::safe_browsing::ClientDownloadRequest_SignatureInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+}
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ relative_path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ signature_ = NULL;
+ image_headers_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::~ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::SharedDtor() {
+ if (relative_path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete relative_path_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete signature_;
+ delete image_headers_;
+ }
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::New() const {
+ return new ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile;
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_relative_path()) {
+ if (relative_path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ relative_path_->clear();
+ }
+ }
+ if (has_signature()) {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ }
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string relative_path = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_relative_path()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+ // optional string relative_path = 1;
+ if (has_relative_path()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->relative_path(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ if (has_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->signature(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->image_headers(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+}
+
+int ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string relative_path = 1;
+ if (has_relative_path()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->relative_path());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ if (has_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->signature());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ if (has_image_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::MergeFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_relative_path()) {
+ set_relative_path(from.relative_path());
+ }
+ if (from.has_signature()) {
+ mutable_signature()->::safe_browsing::ClientDownloadRequest_SignatureInfo::MergeFrom(from.signature());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::CopyFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::IsInitialized() const {
+
+ if (has_signature()) {
+ if (!this->signature().IsInitialized()) return false;
+ }
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::Swap(ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* other) {
+ if (other != this) {
+ std::swap(relative_path_, other->relative_path_);
+ std::swap(signature_, other->signature_);
+ std::swap(image_headers_, other->image_headers_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::kFileBasenameFieldNumber;
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::kSignatureFieldNumber;
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::kImageHeadersFieldNumber;
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::kSecErrorFieldNumber;
+const int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::kContainedFileFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident::ClientIncidentReport_IncidentData_BinaryIntegrityIncident()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo::internal_default_instance());
+#else
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(&::safe_browsing::ClientDownloadRequest_SignatureInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+}
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident::ClientIncidentReport_IncidentData_BinaryIntegrityIncident(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ signature_ = NULL;
+ image_headers_ = NULL;
+ sec_error_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident::~ClientIncidentReport_IncidentData_BinaryIntegrityIncident() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::SharedDtor() {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_basename_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete signature_;
+ delete image_headers_;
+ }
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& ClientIncidentReport_IncidentData_BinaryIntegrityIncident::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::New() const {
+ return new ClientIncidentReport_IncidentData_BinaryIntegrityIncident;
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::Clear() {
+ if (_has_bits_[0 / 32] & 15) {
+ if (has_file_basename()) {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_->clear();
+ }
+ }
+ if (has_signature()) {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ }
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ sec_error_ = 0;
+ }
+ contained_file_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string file_basename = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_file_basename()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_sec_error;
+ break;
+ }
+
+ // optional int32 sec_error = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_sec_error:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &sec_error_)));
+ set_has_sec_error();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_contained_file;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile contained_file = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_contained_file:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_contained_file()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_contained_file;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+ // optional string file_basename = 1;
+ if (has_file_basename()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->file_basename(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ if (has_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->signature(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->image_headers(), output);
+ }
+
+ // optional int32 sec_error = 4;
+ if (has_sec_error()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(4, this->sec_error(), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile contained_file = 5;
+ for (int i = 0; i < this->contained_file_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->contained_file(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+}
+
+int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string file_basename = 1;
+ if (has_file_basename()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->file_basename());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ if (has_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->signature());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ if (has_image_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ // optional int32 sec_error = 4;
+ if (has_sec_error()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->sec_error());
+ }
+
+ }
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile contained_file = 5;
+ total_size += 1 * this->contained_file_size();
+ for (int i = 0; i < this->contained_file_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->contained_file(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_BinaryIntegrityIncident*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::MergeFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ contained_file_.MergeFrom(from.contained_file_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_file_basename()) {
+ set_file_basename(from.file_basename());
+ }
+ if (from.has_signature()) {
+ mutable_signature()->::safe_browsing::ClientDownloadRequest_SignatureInfo::MergeFrom(from.signature());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ if (from.has_sec_error()) {
+ set_sec_error(from.sec_error());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::CopyFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident::IsInitialized() const {
+
+ if (has_signature()) {
+ if (!this->signature().IsInitialized()) return false;
+ }
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ if (!::google::protobuf::internal::AllAreInitialized(this->contained_file())) return false;
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::Swap(ClientIncidentReport_IncidentData_BinaryIntegrityIncident* other) {
+ if (other != this) {
+ std::swap(file_basename_, other->file_basename_);
+ std::swap(signature_, other->signature_);
+ std::swap(image_headers_, other->image_headers_);
+ std::swap(sec_error_, other->sec_error_);
+ contained_file_.Swap(&other->contained_file_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_BinaryIntegrityIncident::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_BlacklistLoadIncident::kPathFieldNumber;
+const int ClientIncidentReport_IncidentData_BlacklistLoadIncident::kDigestFieldNumber;
+const int ClientIncidentReport_IncidentData_BlacklistLoadIncident::kVersionFieldNumber;
+const int ClientIncidentReport_IncidentData_BlacklistLoadIncident::kBlacklistInitializedFieldNumber;
+const int ClientIncidentReport_IncidentData_BlacklistLoadIncident::kSignatureFieldNumber;
+const int ClientIncidentReport_IncidentData_BlacklistLoadIncident::kImageHeadersFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_BlacklistLoadIncident::ClientIncidentReport_IncidentData_BlacklistLoadIncident()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ digest_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(
+ ::safe_browsing::ClientDownloadRequest_Digests::internal_default_instance());
+#else
+ digest_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(&::safe_browsing::ClientDownloadRequest_Digests::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo::internal_default_instance());
+#else
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(&::safe_browsing::ClientDownloadRequest_SignatureInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+}
+
+ClientIncidentReport_IncidentData_BlacklistLoadIncident::ClientIncidentReport_IncidentData_BlacklistLoadIncident(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ digest_ = NULL;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ blacklist_initialized_ = false;
+ signature_ = NULL;
+ image_headers_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_BlacklistLoadIncident::~ClientIncidentReport_IncidentData_BlacklistLoadIncident() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::SharedDtor() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete digest_;
+ delete signature_;
+ delete image_headers_;
+ }
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_BlacklistLoadIncident& ClientIncidentReport_IncidentData_BlacklistLoadIncident::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_BlacklistLoadIncident* ClientIncidentReport_IncidentData_BlacklistLoadIncident::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_BlacklistLoadIncident* ClientIncidentReport_IncidentData_BlacklistLoadIncident::New() const {
+ return new ClientIncidentReport_IncidentData_BlacklistLoadIncident;
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::Clear() {
+ if (_has_bits_[0 / 32] & 63) {
+ if (has_path()) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ }
+ if (has_digest()) {
+ if (digest_ != NULL) digest_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ }
+ if (has_version()) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ }
+ blacklist_initialized_ = false;
+ if (has_signature()) {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ }
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string path = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_path()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_digest;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_digest:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_digest()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_version;
+ break;
+ }
+
+ // optional string version = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_blacklist_initialized;
+ break;
+ }
+
+ // optional bool blacklist_initialized = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_blacklist_initialized:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &blacklist_initialized_)));
+ set_has_blacklist_initialized();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+ // optional string path = 1;
+ if (has_path()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->path(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ if (has_digest()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->digest(), output);
+ }
+
+ // optional string version = 3;
+ if (has_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->version(), output);
+ }
+
+ // optional bool blacklist_initialized = 4;
+ if (has_blacklist_initialized()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(4, this->blacklist_initialized(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ if (has_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->signature(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 6, this->image_headers(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+}
+
+int ClientIncidentReport_IncidentData_BlacklistLoadIncident::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string path = 1;
+ if (has_path()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->path());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ if (has_digest()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->digest());
+ }
+
+ // optional string version = 3;
+ if (has_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->version());
+ }
+
+ // optional bool blacklist_initialized = 4;
+ if (has_blacklist_initialized()) {
+ total_size += 1 + 1;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ if (has_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->signature());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ if (has_image_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_BlacklistLoadIncident*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::MergeFrom(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_path()) {
+ set_path(from.path());
+ }
+ if (from.has_digest()) {
+ mutable_digest()->::safe_browsing::ClientDownloadRequest_Digests::MergeFrom(from.digest());
+ }
+ if (from.has_version()) {
+ set_version(from.version());
+ }
+ if (from.has_blacklist_initialized()) {
+ set_blacklist_initialized(from.blacklist_initialized());
+ }
+ if (from.has_signature()) {
+ mutable_signature()->::safe_browsing::ClientDownloadRequest_SignatureInfo::MergeFrom(from.signature());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::CopyFrom(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::IsInitialized() const {
+
+ if (has_signature()) {
+ if (!this->signature().IsInitialized()) return false;
+ }
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_BlacklistLoadIncident::Swap(ClientIncidentReport_IncidentData_BlacklistLoadIncident* other) {
+ if (other != this) {
+ std::swap(path_, other->path_);
+ std::swap(digest_, other->digest_);
+ std::swap(version_, other->version_);
+ std::swap(blacklist_initialized_, other->blacklist_initialized_);
+ std::swap(signature_, other->signature_);
+ std::swap(image_headers_, other->image_headers_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_BlacklistLoadIncident::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::kVariationsSeedSignatureFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ variations_seed_signature_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::~ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::SharedDtor() {
+ if (variations_seed_signature_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete variations_seed_signature_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::New() const {
+ return new ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident;
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::Clear() {
+ if (has_variations_seed_signature()) {
+ if (variations_seed_signature_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ variations_seed_signature_->clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string variations_seed_signature = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_variations_seed_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+ // optional string variations_seed_signature = 1;
+ if (has_variations_seed_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->variations_seed_signature(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+}
+
+int ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string variations_seed_signature = 1;
+ if (has_variations_seed_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->variations_seed_signature());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::MergeFrom(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_variations_seed_signature()) {
+ set_variations_seed_signature(from.variations_seed_signature());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::CopyFrom(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::Swap(ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* other) {
+ if (other != this) {
+ std::swap(variations_seed_signature_, other->variations_seed_signature_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident";
+}
+
+
+// -------------------------------------------------------------------
+
+bool ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident::UNKNOWN;
+const ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident::TYPE_PATTERN;
+const ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident::Type_MIN;
+const ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident::Type_MAX;
+const int ClientIncidentReport_IncidentData_ResourceRequestIncident::Type_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_ResourceRequestIncident::kDigestFieldNumber;
+const int ClientIncidentReport_IncidentData_ResourceRequestIncident::kOriginFieldNumber;
+const int ClientIncidentReport_IncidentData_ResourceRequestIncident::kTypeFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_ResourceRequestIncident::ClientIncidentReport_IncidentData_ResourceRequestIncident()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_IncidentData_ResourceRequestIncident::ClientIncidentReport_IncidentData_ResourceRequestIncident(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ digest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ origin_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ type_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_ResourceRequestIncident::~ClientIncidentReport_IncidentData_ResourceRequestIncident() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::SharedDtor() {
+ if (digest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete digest_;
+ }
+ if (origin_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete origin_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_ResourceRequestIncident& ClientIncidentReport_IncidentData_ResourceRequestIncident::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_ResourceRequestIncident* ClientIncidentReport_IncidentData_ResourceRequestIncident::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_ResourceRequestIncident* ClientIncidentReport_IncidentData_ResourceRequestIncident::New() const {
+ return new ClientIncidentReport_IncidentData_ResourceRequestIncident;
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_digest()) {
+ if (digest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ digest_->clear();
+ }
+ }
+ if (has_origin()) {
+ if (origin_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ origin_->clear();
+ }
+ }
+ type_ = 0;
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_ResourceRequestIncident::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes digest = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_digest()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_origin;
+ break;
+ }
+
+ // optional string origin = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_origin:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_origin()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_type;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.Type type = 3 [default = UNKNOWN];
+ case 3: {
+ if (tag == 24) {
+ parse_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_IsValid(value)) {
+ set_type(static_cast< ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+ // optional bytes digest = 1;
+ if (has_digest()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->digest(), output);
+ }
+
+ // optional string origin = 2;
+ if (has_origin()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->origin(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.Type type = 3 [default = UNKNOWN];
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 3, this->type(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+}
+
+int ClientIncidentReport_IncidentData_ResourceRequestIncident::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes digest = 1;
+ if (has_digest()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->digest());
+ }
+
+ // optional string origin = 2;
+ if (has_origin()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->origin());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.Type type = 3 [default = UNKNOWN];
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_ResourceRequestIncident*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::MergeFrom(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_digest()) {
+ set_digest(from.digest());
+ }
+ if (from.has_origin()) {
+ set_origin(from.origin());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::CopyFrom(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_ResourceRequestIncident::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_ResourceRequestIncident::Swap(ClientIncidentReport_IncidentData_ResourceRequestIncident* other) {
+ if (other != this) {
+ std::swap(digest_, other->digest_);
+ std::swap(origin_, other->origin_);
+ std::swap(type_, other->type_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_ResourceRequestIncident::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData_SuspiciousModuleIncident::kPathFieldNumber;
+const int ClientIncidentReport_IncidentData_SuspiciousModuleIncident::kDigestFieldNumber;
+const int ClientIncidentReport_IncidentData_SuspiciousModuleIncident::kVersionFieldNumber;
+const int ClientIncidentReport_IncidentData_SuspiciousModuleIncident::kSignatureFieldNumber;
+const int ClientIncidentReport_IncidentData_SuspiciousModuleIncident::kImageHeadersFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData_SuspiciousModuleIncident::ClientIncidentReport_IncidentData_SuspiciousModuleIncident()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ digest_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(
+ ::safe_browsing::ClientDownloadRequest_Digests::internal_default_instance());
+#else
+ digest_ = const_cast< ::safe_browsing::ClientDownloadRequest_Digests*>(&::safe_browsing::ClientDownloadRequest_Digests::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo::internal_default_instance());
+#else
+ signature_ = const_cast< ::safe_browsing::ClientDownloadRequest_SignatureInfo*>(&::safe_browsing::ClientDownloadRequest_SignatureInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+}
+
+ClientIncidentReport_IncidentData_SuspiciousModuleIncident::ClientIncidentReport_IncidentData_SuspiciousModuleIncident(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ digest_ = NULL;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ signature_ = NULL;
+ image_headers_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData_SuspiciousModuleIncident::~ClientIncidentReport_IncidentData_SuspiciousModuleIncident() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::SharedDtor() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete digest_;
+ delete signature_;
+ delete image_headers_;
+ }
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& ClientIncidentReport_IncidentData_SuspiciousModuleIncident::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData_SuspiciousModuleIncident* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData_SuspiciousModuleIncident* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::New() const {
+ return new ClientIncidentReport_IncidentData_SuspiciousModuleIncident;
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::Clear() {
+ if (_has_bits_[0 / 32] & 31) {
+ if (has_path()) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ }
+ if (has_digest()) {
+ if (digest_ != NULL) digest_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ }
+ if (has_version()) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ }
+ if (has_signature()) {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ }
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string path = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_path()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_digest;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_digest:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_digest()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_version;
+ break;
+ }
+
+ // optional string version = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+ // optional string path = 1;
+ if (has_path()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->path(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ if (has_digest()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->digest(), output);
+ }
+
+ // optional string version = 3;
+ if (has_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->version(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 4;
+ if (has_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->signature(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->image_headers(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+}
+
+int ClientIncidentReport_IncidentData_SuspiciousModuleIncident::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string path = 1;
+ if (has_path()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->path());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ if (has_digest()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->digest());
+ }
+
+ // optional string version = 3;
+ if (has_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->version());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 4;
+ if (has_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->signature());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ if (has_image_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData_SuspiciousModuleIncident*>(&from));
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::MergeFrom(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_path()) {
+ set_path(from.path());
+ }
+ if (from.has_digest()) {
+ mutable_digest()->::safe_browsing::ClientDownloadRequest_Digests::MergeFrom(from.digest());
+ }
+ if (from.has_version()) {
+ set_version(from.version());
+ }
+ if (from.has_signature()) {
+ mutable_signature()->::safe_browsing::ClientDownloadRequest_SignatureInfo::MergeFrom(from.signature());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::CopyFrom(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::IsInitialized() const {
+
+ if (has_signature()) {
+ if (!this->signature().IsInitialized()) return false;
+ }
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::Swap(ClientIncidentReport_IncidentData_SuspiciousModuleIncident* other) {
+ if (other != this) {
+ std::swap(path_, other->path_);
+ std::swap(digest_, other->digest_);
+ std::swap(version_, other->version_);
+ std::swap(signature_, other->signature_);
+ std::swap(image_headers_, other->image_headers_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData_SuspiciousModuleIncident::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_IncidentData::kIncidentTimeMsecFieldNumber;
+const int ClientIncidentReport_IncidentData::kTrackedPreferenceFieldNumber;
+const int ClientIncidentReport_IncidentData::kBinaryIntegrityFieldNumber;
+const int ClientIncidentReport_IncidentData::kBlacklistLoadFieldNumber;
+const int ClientIncidentReport_IncidentData::kVariationsSeedSignatureFieldNumber;
+const int ClientIncidentReport_IncidentData::kResourceRequestFieldNumber;
+const int ClientIncidentReport_IncidentData::kSuspiciousModuleFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_IncidentData::ClientIncidentReport_IncidentData()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.IncidentData)
+}
+
+void ClientIncidentReport_IncidentData::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ tracked_preference_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident*>(
+ ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident::internal_default_instance());
+#else
+ tracked_preference_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident*>(&::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ binary_integrity_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident*>(
+ ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident::internal_default_instance());
+#else
+ binary_integrity_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident*>(&::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ blacklist_load_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident*>(
+ ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident::internal_default_instance());
+#else
+ blacklist_load_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident*>(&::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ variations_seed_signature_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident*>(
+ ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::internal_default_instance());
+#else
+ variations_seed_signature_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident*>(&::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ resource_request_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident*>(
+ ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident::internal_default_instance());
+#else
+ resource_request_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident*>(&::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ suspicious_module_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident*>(
+ ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident::internal_default_instance());
+#else
+ suspicious_module_ = const_cast< ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident*>(&::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident::default_instance());
+#endif
+}
+
+ClientIncidentReport_IncidentData::ClientIncidentReport_IncidentData(const ClientIncidentReport_IncidentData& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.IncidentData)
+}
+
+void ClientIncidentReport_IncidentData::SharedCtor() {
+ _cached_size_ = 0;
+ incident_time_msec_ = GOOGLE_LONGLONG(0);
+ tracked_preference_ = NULL;
+ binary_integrity_ = NULL;
+ blacklist_load_ = NULL;
+ variations_seed_signature_ = NULL;
+ resource_request_ = NULL;
+ suspicious_module_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_IncidentData::~ClientIncidentReport_IncidentData() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.IncidentData)
+ SharedDtor();
+}
+
+void ClientIncidentReport_IncidentData::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete tracked_preference_;
+ delete binary_integrity_;
+ delete blacklist_load_;
+ delete variations_seed_signature_;
+ delete resource_request_;
+ delete suspicious_module_;
+ }
+}
+
+void ClientIncidentReport_IncidentData::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_IncidentData& ClientIncidentReport_IncidentData::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_IncidentData* ClientIncidentReport_IncidentData::default_instance_ = NULL;
+
+ClientIncidentReport_IncidentData* ClientIncidentReport_IncidentData::New() const {
+ return new ClientIncidentReport_IncidentData;
+}
+
+void ClientIncidentReport_IncidentData::Clear() {
+ if (_has_bits_[0 / 32] & 127) {
+ incident_time_msec_ = GOOGLE_LONGLONG(0);
+ if (has_tracked_preference()) {
+ if (tracked_preference_ != NULL) tracked_preference_->::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident::Clear();
+ }
+ if (has_binary_integrity()) {
+ if (binary_integrity_ != NULL) binary_integrity_->::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident::Clear();
+ }
+ if (has_blacklist_load()) {
+ if (blacklist_load_ != NULL) blacklist_load_->::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident::Clear();
+ }
+ if (has_variations_seed_signature()) {
+ if (variations_seed_signature_ != NULL) variations_seed_signature_->::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::Clear();
+ }
+ if (has_resource_request()) {
+ if (resource_request_ != NULL) resource_request_->::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident::Clear();
+ }
+ if (has_suspicious_module()) {
+ if (suspicious_module_ != NULL) suspicious_module_->::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_IncidentData::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.IncidentData)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int64 incident_time_msec = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &incident_time_msec_)));
+ set_has_incident_time_msec();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_tracked_preference;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident tracked_preference = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_tracked_preference:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_tracked_preference()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_binary_integrity;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident binary_integrity = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_binary_integrity:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_binary_integrity()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_blacklist_load;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident blacklist_load = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_blacklist_load:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_blacklist_load()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_variations_seed_signature;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident variations_seed_signature = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_variations_seed_signature:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_variations_seed_signature()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_resource_request;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident resource_request = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_resource_request:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_resource_request()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_suspicious_module;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident suspicious_module = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_suspicious_module:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_suspicious_module()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.IncidentData)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.IncidentData)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_IncidentData::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.IncidentData)
+ // optional int64 incident_time_msec = 1;
+ if (has_incident_time_msec()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(1, this->incident_time_msec(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident tracked_preference = 2;
+ if (has_tracked_preference()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->tracked_preference(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident binary_integrity = 3;
+ if (has_binary_integrity()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->binary_integrity(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident blacklist_load = 4;
+ if (has_blacklist_load()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->blacklist_load(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident variations_seed_signature = 6;
+ if (has_variations_seed_signature()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 6, this->variations_seed_signature(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident resource_request = 7;
+ if (has_resource_request()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 7, this->resource_request(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident suspicious_module = 8;
+ if (has_suspicious_module()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 8, this->suspicious_module(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.IncidentData)
+}
+
+int ClientIncidentReport_IncidentData::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int64 incident_time_msec = 1;
+ if (has_incident_time_msec()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->incident_time_msec());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident tracked_preference = 2;
+ if (has_tracked_preference()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->tracked_preference());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident binary_integrity = 3;
+ if (has_binary_integrity()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->binary_integrity());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident blacklist_load = 4;
+ if (has_blacklist_load()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->blacklist_load());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident variations_seed_signature = 6;
+ if (has_variations_seed_signature()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->variations_seed_signature());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident resource_request = 7;
+ if (has_resource_request()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->resource_request());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident suspicious_module = 8;
+ if (has_suspicious_module()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->suspicious_module());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_IncidentData::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_IncidentData*>(&from));
+}
+
+void ClientIncidentReport_IncidentData::MergeFrom(const ClientIncidentReport_IncidentData& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_incident_time_msec()) {
+ set_incident_time_msec(from.incident_time_msec());
+ }
+ if (from.has_tracked_preference()) {
+ mutable_tracked_preference()->::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident::MergeFrom(from.tracked_preference());
+ }
+ if (from.has_binary_integrity()) {
+ mutable_binary_integrity()->::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident::MergeFrom(from.binary_integrity());
+ }
+ if (from.has_blacklist_load()) {
+ mutable_blacklist_load()->::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident::MergeFrom(from.blacklist_load());
+ }
+ if (from.has_variations_seed_signature()) {
+ mutable_variations_seed_signature()->::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::MergeFrom(from.variations_seed_signature());
+ }
+ if (from.has_resource_request()) {
+ mutable_resource_request()->::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident::MergeFrom(from.resource_request());
+ }
+ if (from.has_suspicious_module()) {
+ mutable_suspicious_module()->::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident::MergeFrom(from.suspicious_module());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_IncidentData::CopyFrom(const ClientIncidentReport_IncidentData& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_IncidentData::IsInitialized() const {
+
+ if (has_binary_integrity()) {
+ if (!this->binary_integrity().IsInitialized()) return false;
+ }
+ if (has_blacklist_load()) {
+ if (!this->blacklist_load().IsInitialized()) return false;
+ }
+ if (has_suspicious_module()) {
+ if (!this->suspicious_module().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_IncidentData::Swap(ClientIncidentReport_IncidentData* other) {
+ if (other != this) {
+ std::swap(incident_time_msec_, other->incident_time_msec_);
+ std::swap(tracked_preference_, other->tracked_preference_);
+ std::swap(binary_integrity_, other->binary_integrity_);
+ std::swap(blacklist_load_, other->blacklist_load_);
+ std::swap(variations_seed_signature_, other->variations_seed_signature_);
+ std::swap(resource_request_, other->resource_request_);
+ std::swap(suspicious_module_, other->suspicious_module_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_IncidentData::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.IncidentData";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_DownloadDetails::kTokenFieldNumber;
+const int ClientIncidentReport_DownloadDetails::kDownloadFieldNumber;
+const int ClientIncidentReport_DownloadDetails::kDownloadTimeMsecFieldNumber;
+const int ClientIncidentReport_DownloadDetails::kOpenTimeMsecFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_DownloadDetails::ClientIncidentReport_DownloadDetails()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.DownloadDetails)
+}
+
+void ClientIncidentReport_DownloadDetails::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ download_ = const_cast< ::safe_browsing::ClientDownloadRequest*>(
+ ::safe_browsing::ClientDownloadRequest::internal_default_instance());
+#else
+ download_ = const_cast< ::safe_browsing::ClientDownloadRequest*>(&::safe_browsing::ClientDownloadRequest::default_instance());
+#endif
+}
+
+ClientIncidentReport_DownloadDetails::ClientIncidentReport_DownloadDetails(const ClientIncidentReport_DownloadDetails& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.DownloadDetails)
+}
+
+void ClientIncidentReport_DownloadDetails::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ download_ = NULL;
+ download_time_msec_ = GOOGLE_LONGLONG(0);
+ open_time_msec_ = GOOGLE_LONGLONG(0);
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_DownloadDetails::~ClientIncidentReport_DownloadDetails() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.DownloadDetails)
+ SharedDtor();
+}
+
+void ClientIncidentReport_DownloadDetails::SharedDtor() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete download_;
+ }
+}
+
+void ClientIncidentReport_DownloadDetails::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_DownloadDetails& ClientIncidentReport_DownloadDetails::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_DownloadDetails* ClientIncidentReport_DownloadDetails::default_instance_ = NULL;
+
+ClientIncidentReport_DownloadDetails* ClientIncidentReport_DownloadDetails::New() const {
+ return new ClientIncidentReport_DownloadDetails;
+}
+
+void ClientIncidentReport_DownloadDetails::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientIncidentReport_DownloadDetails*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 15) {
+ ZR_(download_time_msec_, open_time_msec_);
+ if (has_token()) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ }
+ if (has_download()) {
+ if (download_ != NULL) download_->::safe_browsing::ClientDownloadRequest::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_DownloadDetails::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.DownloadDetails)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes token = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_token()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_download;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest download = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_download:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_download()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_download_time_msec;
+ break;
+ }
+
+ // optional int64 download_time_msec = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_download_time_msec:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &download_time_msec_)));
+ set_has_download_time_msec();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_open_time_msec;
+ break;
+ }
+
+ // optional int64 open_time_msec = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_open_time_msec:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &open_time_msec_)));
+ set_has_open_time_msec();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.DownloadDetails)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.DownloadDetails)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_DownloadDetails::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.DownloadDetails)
+ // optional bytes token = 1;
+ if (has_token()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->token(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest download = 2;
+ if (has_download()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->download(), output);
+ }
+
+ // optional int64 download_time_msec = 3;
+ if (has_download_time_msec()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(3, this->download_time_msec(), output);
+ }
+
+ // optional int64 open_time_msec = 4;
+ if (has_open_time_msec()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(4, this->open_time_msec(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.DownloadDetails)
+}
+
+int ClientIncidentReport_DownloadDetails::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes token = 1;
+ if (has_token()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->token());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest download = 2;
+ if (has_download()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->download());
+ }
+
+ // optional int64 download_time_msec = 3;
+ if (has_download_time_msec()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->download_time_msec());
+ }
+
+ // optional int64 open_time_msec = 4;
+ if (has_open_time_msec()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->open_time_msec());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_DownloadDetails::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_DownloadDetails*>(&from));
+}
+
+void ClientIncidentReport_DownloadDetails::MergeFrom(const ClientIncidentReport_DownloadDetails& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_token()) {
+ set_token(from.token());
+ }
+ if (from.has_download()) {
+ mutable_download()->::safe_browsing::ClientDownloadRequest::MergeFrom(from.download());
+ }
+ if (from.has_download_time_msec()) {
+ set_download_time_msec(from.download_time_msec());
+ }
+ if (from.has_open_time_msec()) {
+ set_open_time_msec(from.open_time_msec());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_DownloadDetails::CopyFrom(const ClientIncidentReport_DownloadDetails& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_DownloadDetails::IsInitialized() const {
+
+ if (has_download()) {
+ if (!this->download().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_DownloadDetails::Swap(ClientIncidentReport_DownloadDetails* other) {
+ if (other != this) {
+ std::swap(token_, other->token_);
+ std::swap(download_, other->download_);
+ std::swap(download_time_msec_, other->download_time_msec_);
+ std::swap(open_time_msec_, other->open_time_msec_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_DownloadDetails::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.DownloadDetails";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_OS_RegistryValue::kNameFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS_RegistryValue::kTypeFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS_RegistryValue::kDataFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_OS_RegistryValue::ClientIncidentReport_EnvironmentData_OS_RegistryValue()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_OS_RegistryValue::ClientIncidentReport_EnvironmentData_OS_RegistryValue(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ type_ = 0u;
+ data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_OS_RegistryValue::~ClientIncidentReport_EnvironmentData_OS_RegistryValue() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete data_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_OS_RegistryValue& ClientIncidentReport_EnvironmentData_OS_RegistryValue::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_OS_RegistryValue* ClientIncidentReport_EnvironmentData_OS_RegistryValue::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_OS_RegistryValue* ClientIncidentReport_EnvironmentData_OS_RegistryValue::New() const {
+ return new ClientIncidentReport_EnvironmentData_OS_RegistryValue;
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ type_ = 0u;
+ if (has_data()) {
+ if (data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ data_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_OS_RegistryValue::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_type;
+ break;
+ }
+
+ // optional uint32 type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_type:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &type_)));
+ set_has_type();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_data;
+ break;
+ }
+
+ // optional bytes data = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional uint32 type = 2;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(2, this->type(), output);
+ }
+
+ // optional bytes data = 3;
+ if (has_data()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->data(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+}
+
+int ClientIncidentReport_EnvironmentData_OS_RegistryValue::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional uint32 type = 2;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->type());
+ }
+
+ // optional bytes data = 3;
+ if (has_data()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->data());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_OS_RegistryValue*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::MergeFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_data()) {
+ set_data(from.data());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::CopyFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_OS_RegistryValue::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryValue::Swap(ClientIncidentReport_EnvironmentData_OS_RegistryValue* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(type_, other->type_);
+ std::swap(data_, other->data_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_OS_RegistryValue::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_OS_RegistryKey::kNameFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS_RegistryKey::kValueFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS_RegistryKey::kKeyFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::ClientIncidentReport_EnvironmentData_OS_RegistryKey()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::ClientIncidentReport_EnvironmentData_OS_RegistryKey(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::~ClientIncidentReport_EnvironmentData_OS_RegistryKey() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_OS_RegistryKey& ClientIncidentReport_EnvironmentData_OS_RegistryKey::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_OS_RegistryKey* ClientIncidentReport_EnvironmentData_OS_RegistryKey::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_OS_RegistryKey* ClientIncidentReport_EnvironmentData_OS_RegistryKey::New() const {
+ return new ClientIncidentReport_EnvironmentData_OS_RegistryKey;
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::Clear() {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ value_.Clear();
+ key_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_OS_RegistryKey::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue value = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ if (input->ExpectTag(26)) goto parse_key;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey key = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_key:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_key()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_key;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue value = 2;
+ for (int i = 0; i < this->value_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->value(i), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey key = 3;
+ for (int i = 0; i < this->key_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->key(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+}
+
+int ClientIncidentReport_EnvironmentData_OS_RegistryKey::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ }
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue value = 2;
+ total_size += 1 * this->value_size();
+ for (int i = 0; i < this->value_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->value(i));
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey key = 3;
+ total_size += 1 * this->key_size();
+ for (int i = 0; i < this->key_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->key(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_OS_RegistryKey*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::MergeFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ value_.MergeFrom(from.value_);
+ key_.MergeFrom(from.key_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::CopyFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_OS_RegistryKey::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_OS_RegistryKey::Swap(ClientIncidentReport_EnvironmentData_OS_RegistryKey* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ value_.Swap(&other->value_);
+ key_.Swap(&other->key_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_OS_RegistryKey::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_OS::kOsNameFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS::kOsVersionFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS::kRegistryKeyFieldNumber;
+const int ClientIncidentReport_EnvironmentData_OS::kIsEnrolledToDomainFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_OS::ClientIncidentReport_EnvironmentData_OS()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+}
+
+void ClientIncidentReport_EnvironmentData_OS::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_OS::ClientIncidentReport_EnvironmentData_OS(const ClientIncidentReport_EnvironmentData_OS& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+}
+
+void ClientIncidentReport_EnvironmentData_OS::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ os_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ os_version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ is_enrolled_to_domain_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_OS::~ClientIncidentReport_EnvironmentData_OS() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_OS::SharedDtor() {
+ if (os_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete os_name_;
+ }
+ if (os_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete os_version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_OS::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_OS& ClientIncidentReport_EnvironmentData_OS::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_OS* ClientIncidentReport_EnvironmentData_OS::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_OS* ClientIncidentReport_EnvironmentData_OS::New() const {
+ return new ClientIncidentReport_EnvironmentData_OS;
+}
+
+void ClientIncidentReport_EnvironmentData_OS::Clear() {
+ if (_has_bits_[0 / 32] & 11) {
+ if (has_os_name()) {
+ if (os_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_name_->clear();
+ }
+ }
+ if (has_os_version()) {
+ if (os_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_version_->clear();
+ }
+ }
+ is_enrolled_to_domain_ = false;
+ }
+ registry_key_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_OS::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string os_name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_os_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_os_version;
+ break;
+ }
+
+ // optional string os_version = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_os_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_os_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_registry_key;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey registry_key = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_registry_key:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_registry_key()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_registry_key;
+ if (input->ExpectTag(32)) goto parse_is_enrolled_to_domain;
+ break;
+ }
+
+ // optional bool is_enrolled_to_domain = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_is_enrolled_to_domain:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &is_enrolled_to_domain_)));
+ set_has_is_enrolled_to_domain();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_OS::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+ // optional string os_name = 1;
+ if (has_os_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->os_name(), output);
+ }
+
+ // optional string os_version = 2;
+ if (has_os_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->os_version(), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey registry_key = 3;
+ for (int i = 0; i < this->registry_key_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->registry_key(i), output);
+ }
+
+ // optional bool is_enrolled_to_domain = 4;
+ if (has_is_enrolled_to_domain()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(4, this->is_enrolled_to_domain(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+}
+
+int ClientIncidentReport_EnvironmentData_OS::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string os_name = 1;
+ if (has_os_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->os_name());
+ }
+
+ // optional string os_version = 2;
+ if (has_os_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->os_version());
+ }
+
+ // optional bool is_enrolled_to_domain = 4;
+ if (has_is_enrolled_to_domain()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey registry_key = 3;
+ total_size += 1 * this->registry_key_size();
+ for (int i = 0; i < this->registry_key_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->registry_key(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_OS::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_OS*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_OS::MergeFrom(const ClientIncidentReport_EnvironmentData_OS& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ registry_key_.MergeFrom(from.registry_key_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_os_name()) {
+ set_os_name(from.os_name());
+ }
+ if (from.has_os_version()) {
+ set_os_version(from.os_version());
+ }
+ if (from.has_is_enrolled_to_domain()) {
+ set_is_enrolled_to_domain(from.is_enrolled_to_domain());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_OS::CopyFrom(const ClientIncidentReport_EnvironmentData_OS& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_OS::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_OS::Swap(ClientIncidentReport_EnvironmentData_OS* other) {
+ if (other != this) {
+ std::swap(os_name_, other->os_name_);
+ std::swap(os_version_, other->os_version_);
+ registry_key_.Swap(&other->registry_key_);
+ std::swap(is_enrolled_to_domain_, other->is_enrolled_to_domain_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_OS::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.OS";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_Machine::kCpuArchitectureFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Machine::kCpuVendorFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Machine::kCpuidFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Machine::ClientIncidentReport_EnvironmentData_Machine()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_Machine::ClientIncidentReport_EnvironmentData_Machine(const ClientIncidentReport_EnvironmentData_Machine& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ cpu_architecture_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ cpu_vendor_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ cpuid_ = 0u;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Machine::~ClientIncidentReport_EnvironmentData_Machine() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::SharedDtor() {
+ if (cpu_architecture_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete cpu_architecture_;
+ }
+ if (cpu_vendor_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete cpu_vendor_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Machine& ClientIncidentReport_EnvironmentData_Machine::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Machine* ClientIncidentReport_EnvironmentData_Machine::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Machine* ClientIncidentReport_EnvironmentData_Machine::New() const {
+ return new ClientIncidentReport_EnvironmentData_Machine;
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_cpu_architecture()) {
+ if (cpu_architecture_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_architecture_->clear();
+ }
+ }
+ if (has_cpu_vendor()) {
+ if (cpu_vendor_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_vendor_->clear();
+ }
+ }
+ cpuid_ = 0u;
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Machine::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string cpu_architecture = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_cpu_architecture()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_cpu_vendor;
+ break;
+ }
+
+ // optional string cpu_vendor = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_cpu_vendor:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_cpu_vendor()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_cpuid;
+ break;
+ }
+
+ // optional uint32 cpuid = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_cpuid:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &cpuid_)));
+ set_has_cpuid();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+ // optional string cpu_architecture = 1;
+ if (has_cpu_architecture()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->cpu_architecture(), output);
+ }
+
+ // optional string cpu_vendor = 2;
+ if (has_cpu_vendor()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->cpu_vendor(), output);
+ }
+
+ // optional uint32 cpuid = 3;
+ if (has_cpuid()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(3, this->cpuid(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+}
+
+int ClientIncidentReport_EnvironmentData_Machine::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string cpu_architecture = 1;
+ if (has_cpu_architecture()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->cpu_architecture());
+ }
+
+ // optional string cpu_vendor = 2;
+ if (has_cpu_vendor()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->cpu_vendor());
+ }
+
+ // optional uint32 cpuid = 3;
+ if (has_cpuid()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->cpuid());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Machine*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::MergeFrom(const ClientIncidentReport_EnvironmentData_Machine& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_cpu_architecture()) {
+ set_cpu_architecture(from.cpu_architecture());
+ }
+ if (from.has_cpu_vendor()) {
+ set_cpu_vendor(from.cpu_vendor());
+ }
+ if (from.has_cpuid()) {
+ set_cpuid(from.cpuid());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::CopyFrom(const ClientIncidentReport_EnvironmentData_Machine& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Machine::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Machine::Swap(ClientIncidentReport_EnvironmentData_Machine* other) {
+ if (other != this) {
+ std::swap(cpu_architecture_, other->cpu_architecture_);
+ std::swap(cpu_vendor_, other->cpu_vendor_);
+ std::swap(cpuid_, other->cpuid_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Machine::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Machine";
+}
+
+
+// -------------------------------------------------------------------
+
+bool ClientIncidentReport_EnvironmentData_Process_Channel_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::CHANNEL_UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::CHANNEL_CANARY;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::CHANNEL_DEV;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::CHANNEL_BETA;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::CHANNEL_STABLE;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::Channel_MIN;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::Channel_MAX;
+const int ClientIncidentReport_EnvironmentData_Process::Channel_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_Process_Patch::kFunctionFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_Patch::kTargetDllFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Process_Patch::ClientIncidentReport_EnvironmentData_Process_Patch()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_Process_Patch::ClientIncidentReport_EnvironmentData_Process_Patch(const ClientIncidentReport_EnvironmentData_Process_Patch& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ function_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ target_dll_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Process_Patch::~ClientIncidentReport_EnvironmentData_Process_Patch() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::SharedDtor() {
+ if (function_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete function_;
+ }
+ if (target_dll_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete target_dll_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Process_Patch& ClientIncidentReport_EnvironmentData_Process_Patch::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Process_Patch* ClientIncidentReport_EnvironmentData_Process_Patch::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Process_Patch* ClientIncidentReport_EnvironmentData_Process_Patch::New() const {
+ return new ClientIncidentReport_EnvironmentData_Process_Patch;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_function()) {
+ if (function_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ function_->clear();
+ }
+ }
+ if (has_target_dll()) {
+ if (target_dll_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ target_dll_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_Patch::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string function = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_function()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_target_dll;
+ break;
+ }
+
+ // optional string target_dll = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_target_dll:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_target_dll()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+ // optional string function = 1;
+ if (has_function()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->function(), output);
+ }
+
+ // optional string target_dll = 2;
+ if (has_target_dll()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->target_dll(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+}
+
+int ClientIncidentReport_EnvironmentData_Process_Patch::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string function = 1;
+ if (has_function()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->function());
+ }
+
+ // optional string target_dll = 2;
+ if (has_target_dll()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->target_dll());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Process_Patch*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::MergeFrom(const ClientIncidentReport_EnvironmentData_Process_Patch& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_function()) {
+ set_function(from.function());
+ }
+ if (from.has_target_dll()) {
+ set_target_dll(from.target_dll());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::CopyFrom(const ClientIncidentReport_EnvironmentData_Process_Patch& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_Patch::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Patch::Swap(ClientIncidentReport_EnvironmentData_Process_Patch* other) {
+ if (other != this) {
+ std::swap(function_, other->function_);
+ std::swap(target_dll_, other->target_dll_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Process_Patch::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Process_NetworkProvider::ClientIncidentReport_EnvironmentData_Process_NetworkProvider()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_Process_NetworkProvider::ClientIncidentReport_EnvironmentData_Process_NetworkProvider(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Process_NetworkProvider::~ClientIncidentReport_EnvironmentData_Process_NetworkProvider() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& ClientIncidentReport_EnvironmentData_Process_NetworkProvider::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Process_NetworkProvider* ClientIncidentReport_EnvironmentData_Process_NetworkProvider::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Process_NetworkProvider* ClientIncidentReport_EnvironmentData_Process_NetworkProvider::New() const {
+ return new ClientIncidentReport_EnvironmentData_Process_NetworkProvider;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::Clear() {
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_NetworkProvider::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+}
+
+int ClientIncidentReport_EnvironmentData_Process_NetworkProvider::ByteSize() const {
+ int total_size = 0;
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Process_NetworkProvider*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::MergeFrom(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::CopyFrom(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_NetworkProvider::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_NetworkProvider::Swap(ClientIncidentReport_EnvironmentData_Process_NetworkProvider* other) {
+ if (other != this) {
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Process_NetworkProvider::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider";
+}
+
+
+// -------------------------------------------------------------------
+
+bool ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll::UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll::LSP;
+const ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll::Feature_MIN;
+const ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll::Feature_MAX;
+const int ClientIncidentReport_EnvironmentData_Process_Dll::Feature_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_Process_Dll::kPathFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_Dll::kBaseAddressFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_Dll::kLengthFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_Dll::kFeatureFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_Dll::kImageHeadersFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Process_Dll::ClientIncidentReport_EnvironmentData_Process_Dll()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders::internal_default_instance());
+#else
+ image_headers_ = const_cast< ::safe_browsing::ClientDownloadRequest_ImageHeaders*>(&::safe_browsing::ClientDownloadRequest_ImageHeaders::default_instance());
+#endif
+}
+
+ClientIncidentReport_EnvironmentData_Process_Dll::ClientIncidentReport_EnvironmentData_Process_Dll(const ClientIncidentReport_EnvironmentData_Process_Dll& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ base_address_ = GOOGLE_ULONGLONG(0);
+ length_ = 0u;
+ image_headers_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Process_Dll::~ClientIncidentReport_EnvironmentData_Process_Dll() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::SharedDtor() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete image_headers_;
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Process_Dll& ClientIncidentReport_EnvironmentData_Process_Dll::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Process_Dll* ClientIncidentReport_EnvironmentData_Process_Dll::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Process_Dll* ClientIncidentReport_EnvironmentData_Process_Dll::New() const {
+ return new ClientIncidentReport_EnvironmentData_Process_Dll;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::Clear() {
+ if (_has_bits_[0 / 32] & 23) {
+ if (has_path()) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ }
+ base_address_ = GOOGLE_ULONGLONG(0);
+ length_ = 0u;
+ if (has_image_headers()) {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ }
+ }
+ feature_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_Dll::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string path = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_path()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_base_address;
+ break;
+ }
+
+ // optional uint64 base_address = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_base_address:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &base_address_)));
+ set_has_base_address();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_length;
+ break;
+ }
+
+ // optional uint32 length = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_length:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &length_)));
+ set_has_length();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_feature;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.Feature feature = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_feature:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid(value)) {
+ add_feature(static_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else if (tag == 34) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+ input,
+ &::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid,
+ this->mutable_feature())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_feature;
+ if (input->ExpectTag(42)) goto parse_image_headers;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_image_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_image_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+ // optional string path = 1;
+ if (has_path()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->path(), output);
+ }
+
+ // optional uint64 base_address = 2;
+ if (has_base_address()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(2, this->base_address(), output);
+ }
+
+ // optional uint32 length = 3;
+ if (has_length()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(3, this->length(), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.Feature feature = 4;
+ for (int i = 0; i < this->feature_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 4, this->feature(i), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ if (has_image_headers()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->image_headers(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+}
+
+int ClientIncidentReport_EnvironmentData_Process_Dll::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string path = 1;
+ if (has_path()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->path());
+ }
+
+ // optional uint64 base_address = 2;
+ if (has_base_address()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->base_address());
+ }
+
+ // optional uint32 length = 3;
+ if (has_length()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->length());
+ }
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ if (has_image_headers()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->image_headers());
+ }
+
+ }
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.Feature feature = 4;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->feature_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+ this->feature(i));
+ }
+ total_size += 1 * this->feature_size() + data_size;
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Process_Dll*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::MergeFrom(const ClientIncidentReport_EnvironmentData_Process_Dll& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ feature_.MergeFrom(from.feature_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_path()) {
+ set_path(from.path());
+ }
+ if (from.has_base_address()) {
+ set_base_address(from.base_address());
+ }
+ if (from.has_length()) {
+ set_length(from.length());
+ }
+ if (from.has_image_headers()) {
+ mutable_image_headers()->::safe_browsing::ClientDownloadRequest_ImageHeaders::MergeFrom(from.image_headers());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::CopyFrom(const ClientIncidentReport_EnvironmentData_Process_Dll& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_Dll::IsInitialized() const {
+
+ if (has_image_headers()) {
+ if (!this->image_headers().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_Dll::Swap(ClientIncidentReport_EnvironmentData_Process_Dll* other) {
+ if (other != this) {
+ std::swap(path_, other->path_);
+ std::swap(base_address_, other->base_address_);
+ std::swap(length_, other->length_);
+ feature_.Swap(&other->feature_);
+ std::swap(image_headers_, other->image_headers_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Process_Dll::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll";
+}
+
+
+// -------------------------------------------------------------------
+
+bool ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::MODULE_STATE_UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::MODULE_STATE_UNMODIFIED;
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::MODULE_STATE_MODIFIED;
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::ModifiedState_MIN;
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::ModifiedState_MAX;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState::ModifiedState_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::kFileOffsetFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::kByteCountFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::kModifiedBytesFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::kExportNameFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ file_offset_ = 0u;
+ byte_count_ = 0;
+ modified_bytes_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ export_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::~ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::SharedDtor() {
+ if (modified_bytes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete modified_bytes_;
+ }
+ if (export_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete export_name_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::New() const {
+ return new ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 15) {
+ ZR_(file_offset_, byte_count_);
+ if (has_modified_bytes()) {
+ if (modified_bytes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ modified_bytes_->clear();
+ }
+ }
+ if (has_export_name()) {
+ if (export_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_name_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional uint32 file_offset = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &file_offset_)));
+ set_has_file_offset();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_byte_count;
+ break;
+ }
+
+ // optional int32 byte_count = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_byte_count:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &byte_count_)));
+ set_has_byte_count();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_modified_bytes;
+ break;
+ }
+
+ // optional bytes modified_bytes = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_modified_bytes:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_modified_bytes()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_export_name;
+ break;
+ }
+
+ // optional string export_name = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_export_name:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_export_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+ // optional uint32 file_offset = 1;
+ if (has_file_offset()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(1, this->file_offset(), output);
+ }
+
+ // optional int32 byte_count = 2;
+ if (has_byte_count()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->byte_count(), output);
+ }
+
+ // optional bytes modified_bytes = 3;
+ if (has_modified_bytes()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->modified_bytes(), output);
+ }
+
+ // optional string export_name = 4;
+ if (has_export_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->export_name(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+}
+
+int ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional uint32 file_offset = 1;
+ if (has_file_offset()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->file_offset());
+ }
+
+ // optional int32 byte_count = 2;
+ if (has_byte_count()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->byte_count());
+ }
+
+ // optional bytes modified_bytes = 3;
+ if (has_modified_bytes()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->modified_bytes());
+ }
+
+ // optional string export_name = 4;
+ if (has_export_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->export_name());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::MergeFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_file_offset()) {
+ set_file_offset(from.file_offset());
+ }
+ if (from.has_byte_count()) {
+ set_byte_count(from.byte_count());
+ }
+ if (from.has_modified_bytes()) {
+ set_modified_bytes(from.modified_bytes());
+ }
+ if (from.has_export_name()) {
+ set_export_name(from.export_name());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::CopyFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::Swap(ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* other) {
+ if (other != this) {
+ std::swap(file_offset_, other->file_offset_);
+ std::swap(byte_count_, other->byte_count_);
+ std::swap(modified_bytes_, other->modified_bytes_);
+ std::swap(export_name_, other->export_name_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState::kNameFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState::kModifiedStateFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState::kOBSOLETEModifiedExportFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState::kModificationFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState::ClientIncidentReport_EnvironmentData_Process_ModuleState()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState::ClientIncidentReport_EnvironmentData_Process_ModuleState(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ modified_state_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState::~ClientIncidentReport_EnvironmentData_Process_ModuleState() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Process_ModuleState& ClientIncidentReport_EnvironmentData_Process_ModuleState::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState* ClientIncidentReport_EnvironmentData_Process_ModuleState::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Process_ModuleState* ClientIncidentReport_EnvironmentData_Process_ModuleState::New() const {
+ return new ClientIncidentReport_EnvironmentData_Process_ModuleState;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ modified_state_ = 0;
+ }
+ obsolete_modified_export_.Clear();
+ modification_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_ModuleState::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_modified_state;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.ModifiedState modified_state = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_modified_state:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_IsValid(value)) {
+ set_modified_state(static_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_OBSOLETE_modified_export;
+ break;
+ }
+
+ // repeated string OBSOLETE_modified_export = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_OBSOLETE_modified_export:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_obsolete_modified_export()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_OBSOLETE_modified_export;
+ if (input->ExpectTag(34)) goto parse_modification;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification modification = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_modification:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_modification()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_modification;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.ModifiedState modified_state = 2;
+ if (has_modified_state()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->modified_state(), output);
+ }
+
+ // repeated string OBSOLETE_modified_export = 3;
+ for (int i = 0; i < this->obsolete_modified_export_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 3, this->obsolete_modified_export(i), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification modification = 4;
+ for (int i = 0; i < this->modification_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->modification(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+}
+
+int ClientIncidentReport_EnvironmentData_Process_ModuleState::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.ModifiedState modified_state = 2;
+ if (has_modified_state()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->modified_state());
+ }
+
+ }
+ // repeated string OBSOLETE_modified_export = 3;
+ total_size += 1 * this->obsolete_modified_export_size();
+ for (int i = 0; i < this->obsolete_modified_export_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->obsolete_modified_export(i));
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification modification = 4;
+ total_size += 1 * this->modification_size();
+ for (int i = 0; i < this->modification_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->modification(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Process_ModuleState*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::MergeFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ obsolete_modified_export_.MergeFrom(from.obsolete_modified_export_);
+ modification_.MergeFrom(from.modification_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_modified_state()) {
+ set_modified_state(from.modified_state());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::CopyFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Process_ModuleState::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Process_ModuleState::Swap(ClientIncidentReport_EnvironmentData_Process_ModuleState* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(modified_state_, other->modified_state_);
+ obsolete_modified_export_.Swap(&other->obsolete_modified_export_);
+ modification_.Swap(&other->modification_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Process_ModuleState::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData_Process::kVersionFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kOBSOLETEDllsFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kPatchesFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kNetworkProvidersFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kChromeUpdateChannelFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kUptimeMsecFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kMetricsConsentFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kExtendedConsentFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kDllFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kBlacklistedDllFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kModuleStateFieldNumber;
+const int ClientIncidentReport_EnvironmentData_Process::kFieldTrialParticipantFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData_Process::ClientIncidentReport_EnvironmentData_Process()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+}
+
+void ClientIncidentReport_EnvironmentData_Process::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_EnvironmentData_Process::ClientIncidentReport_EnvironmentData_Process(const ClientIncidentReport_EnvironmentData_Process& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+}
+
+void ClientIncidentReport_EnvironmentData_Process::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ chrome_update_channel_ = 0;
+ uptime_msec_ = GOOGLE_LONGLONG(0);
+ metrics_consent_ = false;
+ extended_consent_ = false;
+ field_trial_participant_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData_Process::~ClientIncidentReport_EnvironmentData_Process() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData_Process::SharedDtor() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_EnvironmentData_Process::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData_Process& ClientIncidentReport_EnvironmentData_Process::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData_Process* ClientIncidentReport_EnvironmentData_Process::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData_Process* ClientIncidentReport_EnvironmentData_Process::New() const {
+ return new ClientIncidentReport_EnvironmentData_Process;
+}
+
+void ClientIncidentReport_EnvironmentData_Process::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientIncidentReport_EnvironmentData_Process*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 241) {
+ ZR_(uptime_msec_, extended_consent_);
+ if (has_version()) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ }
+ }
+ field_trial_participant_ = false;
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ obsolete_dlls_.Clear();
+ patches_.Clear();
+ network_providers_.Clear();
+ dll_.Clear();
+ blacklisted_dll_.Clear();
+ module_state_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData_Process::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string version = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_OBSOLETE_dlls;
+ break;
+ }
+
+ // repeated string OBSOLETE_dlls = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_OBSOLETE_dlls:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_obsolete_dlls()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_OBSOLETE_dlls;
+ if (input->ExpectTag(26)) goto parse_patches;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch patches = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_patches:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_patches()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_patches;
+ if (input->ExpectTag(34)) goto parse_network_providers;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider network_providers = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_network_providers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_network_providers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_network_providers;
+ if (input->ExpectTag(40)) goto parse_chrome_update_channel;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Channel chrome_update_channel = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_chrome_update_channel:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel_IsValid(value)) {
+ set_chrome_update_channel(static_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_uptime_msec;
+ break;
+ }
+
+ // optional int64 uptime_msec = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_uptime_msec:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &uptime_msec_)));
+ set_has_uptime_msec();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(56)) goto parse_metrics_consent;
+ break;
+ }
+
+ // optional bool metrics_consent = 7;
+ case 7: {
+ if (tag == 56) {
+ parse_metrics_consent:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &metrics_consent_)));
+ set_has_metrics_consent();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(64)) goto parse_extended_consent;
+ break;
+ }
+
+ // optional bool extended_consent = 8;
+ case 8: {
+ if (tag == 64) {
+ parse_extended_consent:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &extended_consent_)));
+ set_has_extended_consent();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(74)) goto parse_dll;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll dll = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_dll:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_dll()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(74)) goto parse_dll;
+ if (input->ExpectTag(82)) goto parse_blacklisted_dll;
+ break;
+ }
+
+ // repeated string blacklisted_dll = 10;
+ case 10: {
+ if (tag == 82) {
+ parse_blacklisted_dll:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_blacklisted_dll()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(82)) goto parse_blacklisted_dll;
+ if (input->ExpectTag(90)) goto parse_module_state;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState module_state = 11;
+ case 11: {
+ if (tag == 90) {
+ parse_module_state:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_module_state()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(90)) goto parse_module_state;
+ if (input->ExpectTag(96)) goto parse_field_trial_participant;
+ break;
+ }
+
+ // optional bool field_trial_participant = 12;
+ case 12: {
+ if (tag == 96) {
+ parse_field_trial_participant:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &field_trial_participant_)));
+ set_has_field_trial_participant();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData_Process::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+ // optional string version = 1;
+ if (has_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->version(), output);
+ }
+
+ // repeated string OBSOLETE_dlls = 2;
+ for (int i = 0; i < this->obsolete_dlls_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 2, this->obsolete_dlls(i), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch patches = 3;
+ for (int i = 0; i < this->patches_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->patches(i), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider network_providers = 4;
+ for (int i = 0; i < this->network_providers_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->network_providers(i), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Channel chrome_update_channel = 5;
+ if (has_chrome_update_channel()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 5, this->chrome_update_channel(), output);
+ }
+
+ // optional int64 uptime_msec = 6;
+ if (has_uptime_msec()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(6, this->uptime_msec(), output);
+ }
+
+ // optional bool metrics_consent = 7;
+ if (has_metrics_consent()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(7, this->metrics_consent(), output);
+ }
+
+ // optional bool extended_consent = 8;
+ if (has_extended_consent()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(8, this->extended_consent(), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll dll = 9;
+ for (int i = 0; i < this->dll_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 9, this->dll(i), output);
+ }
+
+ // repeated string blacklisted_dll = 10;
+ for (int i = 0; i < this->blacklisted_dll_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 10, this->blacklisted_dll(i), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState module_state = 11;
+ for (int i = 0; i < this->module_state_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 11, this->module_state(i), output);
+ }
+
+ // optional bool field_trial_participant = 12;
+ if (has_field_trial_participant()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(12, this->field_trial_participant(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+}
+
+int ClientIncidentReport_EnvironmentData_Process::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string version = 1;
+ if (has_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->version());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Channel chrome_update_channel = 5;
+ if (has_chrome_update_channel()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->chrome_update_channel());
+ }
+
+ // optional int64 uptime_msec = 6;
+ if (has_uptime_msec()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->uptime_msec());
+ }
+
+ // optional bool metrics_consent = 7;
+ if (has_metrics_consent()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool extended_consent = 8;
+ if (has_extended_consent()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ if (_has_bits_[11 / 32] & (0xffu << (11 % 32))) {
+ // optional bool field_trial_participant = 12;
+ if (has_field_trial_participant()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated string OBSOLETE_dlls = 2;
+ total_size += 1 * this->obsolete_dlls_size();
+ for (int i = 0; i < this->obsolete_dlls_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->obsolete_dlls(i));
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch patches = 3;
+ total_size += 1 * this->patches_size();
+ for (int i = 0; i < this->patches_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->patches(i));
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider network_providers = 4;
+ total_size += 1 * this->network_providers_size();
+ for (int i = 0; i < this->network_providers_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->network_providers(i));
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll dll = 9;
+ total_size += 1 * this->dll_size();
+ for (int i = 0; i < this->dll_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->dll(i));
+ }
+
+ // repeated string blacklisted_dll = 10;
+ total_size += 1 * this->blacklisted_dll_size();
+ for (int i = 0; i < this->blacklisted_dll_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->blacklisted_dll(i));
+ }
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState module_state = 11;
+ total_size += 1 * this->module_state_size();
+ for (int i = 0; i < this->module_state_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->module_state(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData_Process::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData_Process*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData_Process::MergeFrom(const ClientIncidentReport_EnvironmentData_Process& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ obsolete_dlls_.MergeFrom(from.obsolete_dlls_);
+ patches_.MergeFrom(from.patches_);
+ network_providers_.MergeFrom(from.network_providers_);
+ dll_.MergeFrom(from.dll_);
+ blacklisted_dll_.MergeFrom(from.blacklisted_dll_);
+ module_state_.MergeFrom(from.module_state_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_version()) {
+ set_version(from.version());
+ }
+ if (from.has_chrome_update_channel()) {
+ set_chrome_update_channel(from.chrome_update_channel());
+ }
+ if (from.has_uptime_msec()) {
+ set_uptime_msec(from.uptime_msec());
+ }
+ if (from.has_metrics_consent()) {
+ set_metrics_consent(from.metrics_consent());
+ }
+ if (from.has_extended_consent()) {
+ set_extended_consent(from.extended_consent());
+ }
+ }
+ if (from._has_bits_[11 / 32] & (0xffu << (11 % 32))) {
+ if (from.has_field_trial_participant()) {
+ set_field_trial_participant(from.field_trial_participant());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData_Process::CopyFrom(const ClientIncidentReport_EnvironmentData_Process& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData_Process::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->dll())) return false;
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData_Process::Swap(ClientIncidentReport_EnvironmentData_Process* other) {
+ if (other != this) {
+ std::swap(version_, other->version_);
+ obsolete_dlls_.Swap(&other->obsolete_dlls_);
+ patches_.Swap(&other->patches_);
+ network_providers_.Swap(&other->network_providers_);
+ std::swap(chrome_update_channel_, other->chrome_update_channel_);
+ std::swap(uptime_msec_, other->uptime_msec_);
+ std::swap(metrics_consent_, other->metrics_consent_);
+ std::swap(extended_consent_, other->extended_consent_);
+ dll_.Swap(&other->dll_);
+ blacklisted_dll_.Swap(&other->blacklisted_dll_);
+ module_state_.Swap(&other->module_state_);
+ std::swap(field_trial_participant_, other->field_trial_participant_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData_Process::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData.Process";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_EnvironmentData::kOsFieldNumber;
+const int ClientIncidentReport_EnvironmentData::kMachineFieldNumber;
+const int ClientIncidentReport_EnvironmentData::kProcessFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_EnvironmentData::ClientIncidentReport_EnvironmentData()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.EnvironmentData)
+}
+
+void ClientIncidentReport_EnvironmentData::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ os_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS*>(
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_OS::internal_default_instance());
+#else
+ os_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS*>(&::safe_browsing::ClientIncidentReport_EnvironmentData_OS::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ machine_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine*>(
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine::internal_default_instance());
+#else
+ machine_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine*>(&::safe_browsing::ClientIncidentReport_EnvironmentData_Machine::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ process_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process*>(
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_Process::internal_default_instance());
+#else
+ process_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process*>(&::safe_browsing::ClientIncidentReport_EnvironmentData_Process::default_instance());
+#endif
+}
+
+ClientIncidentReport_EnvironmentData::ClientIncidentReport_EnvironmentData(const ClientIncidentReport_EnvironmentData& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.EnvironmentData)
+}
+
+void ClientIncidentReport_EnvironmentData::SharedCtor() {
+ _cached_size_ = 0;
+ os_ = NULL;
+ machine_ = NULL;
+ process_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_EnvironmentData::~ClientIncidentReport_EnvironmentData() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.EnvironmentData)
+ SharedDtor();
+}
+
+void ClientIncidentReport_EnvironmentData::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete os_;
+ delete machine_;
+ delete process_;
+ }
+}
+
+void ClientIncidentReport_EnvironmentData::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_EnvironmentData& ClientIncidentReport_EnvironmentData::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_EnvironmentData* ClientIncidentReport_EnvironmentData::default_instance_ = NULL;
+
+ClientIncidentReport_EnvironmentData* ClientIncidentReport_EnvironmentData::New() const {
+ return new ClientIncidentReport_EnvironmentData;
+}
+
+void ClientIncidentReport_EnvironmentData::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_os()) {
+ if (os_ != NULL) os_->::safe_browsing::ClientIncidentReport_EnvironmentData_OS::Clear();
+ }
+ if (has_machine()) {
+ if (machine_ != NULL) machine_->::safe_browsing::ClientIncidentReport_EnvironmentData_Machine::Clear();
+ }
+ if (has_process()) {
+ if (process_ != NULL) process_->::safe_browsing::ClientIncidentReport_EnvironmentData_Process::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_EnvironmentData::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.EnvironmentData)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.OS os = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_os()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_machine;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Machine machine = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_machine:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_machine()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_process;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process process = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_process:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_process()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.EnvironmentData)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.EnvironmentData)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_EnvironmentData::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.EnvironmentData)
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.OS os = 1;
+ if (has_os()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->os(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Machine machine = 2;
+ if (has_machine()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->machine(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process process = 3;
+ if (has_process()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->process(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.EnvironmentData)
+}
+
+int ClientIncidentReport_EnvironmentData::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.OS os = 1;
+ if (has_os()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->os());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Machine machine = 2;
+ if (has_machine()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->machine());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process process = 3;
+ if (has_process()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->process());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_EnvironmentData::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_EnvironmentData*>(&from));
+}
+
+void ClientIncidentReport_EnvironmentData::MergeFrom(const ClientIncidentReport_EnvironmentData& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_os()) {
+ mutable_os()->::safe_browsing::ClientIncidentReport_EnvironmentData_OS::MergeFrom(from.os());
+ }
+ if (from.has_machine()) {
+ mutable_machine()->::safe_browsing::ClientIncidentReport_EnvironmentData_Machine::MergeFrom(from.machine());
+ }
+ if (from.has_process()) {
+ mutable_process()->::safe_browsing::ClientIncidentReport_EnvironmentData_Process::MergeFrom(from.process());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_EnvironmentData::CopyFrom(const ClientIncidentReport_EnvironmentData& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_EnvironmentData::IsInitialized() const {
+
+ if (has_process()) {
+ if (!this->process().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport_EnvironmentData::Swap(ClientIncidentReport_EnvironmentData* other) {
+ if (other != this) {
+ std::swap(os_, other->os_);
+ std::swap(machine_, other->machine_);
+ std::swap(process_, other->process_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_EnvironmentData::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.EnvironmentData";
+}
+
+
+// -------------------------------------------------------------------
+
+bool ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::STATE_UNKNOWN;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::STATE_ENABLED;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::STATE_DISABLED;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::STATE_BLACKLISTED;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::STATE_BLOCKED;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::STATE_TERMINATED;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::ExtensionState_MIN;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::ExtensionState_MAX;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::ExtensionState_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kIdFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kVersionFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kNameFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kDescriptionFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kStateFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kTypeFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kUpdateUrlFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kHasSignatureValidationFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kSignatureIsValidFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kInstalledByCustodianFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kInstalledByDefaultFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kInstalledByOemFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kFromBookmarkFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kFromWebstoreFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kConvertedFromUserScriptFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kMayBeUntrustedFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kInstallTimeMsecFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kManifestLocationTypeFieldNumber;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo::kManifestFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_ExtensionData_ExtensionInfo::ClientIncidentReport_ExtensionData_ExtensionInfo()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_ExtensionData_ExtensionInfo::ClientIncidentReport_ExtensionData_ExtensionInfo(const ClientIncidentReport_ExtensionData_ExtensionInfo& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ id_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ description_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ state_ = 0;
+ type_ = 0;
+ update_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ has_signature_validation_ = false;
+ signature_is_valid_ = false;
+ installed_by_custodian_ = false;
+ installed_by_default_ = false;
+ installed_by_oem_ = false;
+ from_bookmark_ = false;
+ from_webstore_ = false;
+ converted_from_user_script_ = false;
+ may_be_untrusted_ = false;
+ install_time_msec_ = GOOGLE_LONGLONG(0);
+ manifest_location_type_ = 0;
+ manifest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_ExtensionData_ExtensionInfo::~ClientIncidentReport_ExtensionData_ExtensionInfo() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+ SharedDtor();
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::SharedDtor() {
+ if (id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete id_;
+ }
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete description_;
+ }
+ if (update_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete update_url_;
+ }
+ if (manifest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete manifest_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_ExtensionData_ExtensionInfo& ClientIncidentReport_ExtensionData_ExtensionInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_ExtensionData_ExtensionInfo* ClientIncidentReport_ExtensionData_ExtensionInfo::default_instance_ = NULL;
+
+ClientIncidentReport_ExtensionData_ExtensionInfo* ClientIncidentReport_ExtensionData_ExtensionInfo::New() const {
+ return new ClientIncidentReport_ExtensionData_ExtensionInfo;
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientIncidentReport_ExtensionData_ExtensionInfo*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 255) {
+ ZR_(state_, type_);
+ if (has_id()) {
+ if (id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ id_->clear();
+ }
+ }
+ if (has_version()) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ }
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_description()) {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_->clear();
+ }
+ }
+ if (has_update_url()) {
+ if (update_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ update_url_->clear();
+ }
+ }
+ has_signature_validation_ = false;
+ }
+ if (_has_bits_[8 / 32] & 65280) {
+ ZR_(signature_is_valid_, converted_from_user_script_);
+ may_be_untrusted_ = false;
+ }
+ if (_has_bits_[16 / 32] & 458752) {
+ install_time_msec_ = GOOGLE_LONGLONG(0);
+ manifest_location_type_ = 0;
+ if (has_manifest()) {
+ if (manifest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ manifest_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_ExtensionData_ExtensionInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string id = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_id()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_version;
+ break;
+ }
+
+ // optional string version = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_name;
+ break;
+ }
+
+ // optional string name = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_name:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_description;
+ break;
+ }
+
+ // optional string description = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_description:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_description()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_state;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.ExtensionState state = 5 [default = STATE_UNKNOWN];
+ case 5: {
+ if (tag == 40) {
+ parse_state:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_IsValid(value)) {
+ set_state(static_cast< ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_type;
+ break;
+ }
+
+ // optional int32 type = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_type:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &type_)));
+ set_has_type();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_update_url;
+ break;
+ }
+
+ // optional string update_url = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_update_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_update_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(64)) goto parse_has_signature_validation;
+ break;
+ }
+
+ // optional bool has_signature_validation = 8;
+ case 8: {
+ if (tag == 64) {
+ parse_has_signature_validation:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &has_signature_validation_)));
+ set_has_has_signature_validation();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(72)) goto parse_signature_is_valid;
+ break;
+ }
+
+ // optional bool signature_is_valid = 9;
+ case 9: {
+ if (tag == 72) {
+ parse_signature_is_valid:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &signature_is_valid_)));
+ set_has_signature_is_valid();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_installed_by_custodian;
+ break;
+ }
+
+ // optional bool installed_by_custodian = 10;
+ case 10: {
+ if (tag == 80) {
+ parse_installed_by_custodian:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &installed_by_custodian_)));
+ set_has_installed_by_custodian();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(88)) goto parse_installed_by_default;
+ break;
+ }
+
+ // optional bool installed_by_default = 11;
+ case 11: {
+ if (tag == 88) {
+ parse_installed_by_default:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &installed_by_default_)));
+ set_has_installed_by_default();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(96)) goto parse_installed_by_oem;
+ break;
+ }
+
+ // optional bool installed_by_oem = 12;
+ case 12: {
+ if (tag == 96) {
+ parse_installed_by_oem:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &installed_by_oem_)));
+ set_has_installed_by_oem();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(104)) goto parse_from_bookmark;
+ break;
+ }
+
+ // optional bool from_bookmark = 13;
+ case 13: {
+ if (tag == 104) {
+ parse_from_bookmark:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &from_bookmark_)));
+ set_has_from_bookmark();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(112)) goto parse_from_webstore;
+ break;
+ }
+
+ // optional bool from_webstore = 14;
+ case 14: {
+ if (tag == 112) {
+ parse_from_webstore:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &from_webstore_)));
+ set_has_from_webstore();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(120)) goto parse_converted_from_user_script;
+ break;
+ }
+
+ // optional bool converted_from_user_script = 15;
+ case 15: {
+ if (tag == 120) {
+ parse_converted_from_user_script:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &converted_from_user_script_)));
+ set_has_converted_from_user_script();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(128)) goto parse_may_be_untrusted;
+ break;
+ }
+
+ // optional bool may_be_untrusted = 16;
+ case 16: {
+ if (tag == 128) {
+ parse_may_be_untrusted:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &may_be_untrusted_)));
+ set_has_may_be_untrusted();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(136)) goto parse_install_time_msec;
+ break;
+ }
+
+ // optional int64 install_time_msec = 17;
+ case 17: {
+ if (tag == 136) {
+ parse_install_time_msec:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &install_time_msec_)));
+ set_has_install_time_msec();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(144)) goto parse_manifest_location_type;
+ break;
+ }
+
+ // optional int32 manifest_location_type = 18;
+ case 18: {
+ if (tag == 144) {
+ parse_manifest_location_type:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &manifest_location_type_)));
+ set_has_manifest_location_type();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(154)) goto parse_manifest;
+ break;
+ }
+
+ // optional string manifest = 19;
+ case 19: {
+ if (tag == 154) {
+ parse_manifest:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_manifest()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+ // optional string id = 1;
+ if (has_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->id(), output);
+ }
+
+ // optional string version = 2;
+ if (has_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->version(), output);
+ }
+
+ // optional string name = 3;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->name(), output);
+ }
+
+ // optional string description = 4;
+ if (has_description()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->description(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.ExtensionState state = 5 [default = STATE_UNKNOWN];
+ if (has_state()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 5, this->state(), output);
+ }
+
+ // optional int32 type = 6;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(6, this->type(), output);
+ }
+
+ // optional string update_url = 7;
+ if (has_update_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 7, this->update_url(), output);
+ }
+
+ // optional bool has_signature_validation = 8;
+ if (has_has_signature_validation()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(8, this->has_signature_validation(), output);
+ }
+
+ // optional bool signature_is_valid = 9;
+ if (has_signature_is_valid()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(9, this->signature_is_valid(), output);
+ }
+
+ // optional bool installed_by_custodian = 10;
+ if (has_installed_by_custodian()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(10, this->installed_by_custodian(), output);
+ }
+
+ // optional bool installed_by_default = 11;
+ if (has_installed_by_default()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(11, this->installed_by_default(), output);
+ }
+
+ // optional bool installed_by_oem = 12;
+ if (has_installed_by_oem()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(12, this->installed_by_oem(), output);
+ }
+
+ // optional bool from_bookmark = 13;
+ if (has_from_bookmark()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(13, this->from_bookmark(), output);
+ }
+
+ // optional bool from_webstore = 14;
+ if (has_from_webstore()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(14, this->from_webstore(), output);
+ }
+
+ // optional bool converted_from_user_script = 15;
+ if (has_converted_from_user_script()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(15, this->converted_from_user_script(), output);
+ }
+
+ // optional bool may_be_untrusted = 16;
+ if (has_may_be_untrusted()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(16, this->may_be_untrusted(), output);
+ }
+
+ // optional int64 install_time_msec = 17;
+ if (has_install_time_msec()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(17, this->install_time_msec(), output);
+ }
+
+ // optional int32 manifest_location_type = 18;
+ if (has_manifest_location_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(18, this->manifest_location_type(), output);
+ }
+
+ // optional string manifest = 19;
+ if (has_manifest()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 19, this->manifest(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+}
+
+int ClientIncidentReport_ExtensionData_ExtensionInfo::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string id = 1;
+ if (has_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->id());
+ }
+
+ // optional string version = 2;
+ if (has_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->version());
+ }
+
+ // optional string name = 3;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional string description = 4;
+ if (has_description()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->description());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.ExtensionState state = 5 [default = STATE_UNKNOWN];
+ if (has_state()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->state());
+ }
+
+ // optional int32 type = 6;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->type());
+ }
+
+ // optional string update_url = 7;
+ if (has_update_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->update_url());
+ }
+
+ // optional bool has_signature_validation = 8;
+ if (has_has_signature_validation()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional bool signature_is_valid = 9;
+ if (has_signature_is_valid()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool installed_by_custodian = 10;
+ if (has_installed_by_custodian()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool installed_by_default = 11;
+ if (has_installed_by_default()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool installed_by_oem = 12;
+ if (has_installed_by_oem()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool from_bookmark = 13;
+ if (has_from_bookmark()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool from_webstore = 14;
+ if (has_from_webstore()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool converted_from_user_script = 15;
+ if (has_converted_from_user_script()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool may_be_untrusted = 16;
+ if (has_may_be_untrusted()) {
+ total_size += 2 + 1;
+ }
+
+ }
+ if (_has_bits_[16 / 32] & (0xffu << (16 % 32))) {
+ // optional int64 install_time_msec = 17;
+ if (has_install_time_msec()) {
+ total_size += 2 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->install_time_msec());
+ }
+
+ // optional int32 manifest_location_type = 18;
+ if (has_manifest_location_type()) {
+ total_size += 2 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->manifest_location_type());
+ }
+
+ // optional string manifest = 19;
+ if (has_manifest()) {
+ total_size += 2 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->manifest());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_ExtensionData_ExtensionInfo*>(&from));
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::MergeFrom(const ClientIncidentReport_ExtensionData_ExtensionInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_id()) {
+ set_id(from.id());
+ }
+ if (from.has_version()) {
+ set_version(from.version());
+ }
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_description()) {
+ set_description(from.description());
+ }
+ if (from.has_state()) {
+ set_state(from.state());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_update_url()) {
+ set_update_url(from.update_url());
+ }
+ if (from.has_has_signature_validation()) {
+ set_has_signature_validation(from.has_signature_validation());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_signature_is_valid()) {
+ set_signature_is_valid(from.signature_is_valid());
+ }
+ if (from.has_installed_by_custodian()) {
+ set_installed_by_custodian(from.installed_by_custodian());
+ }
+ if (from.has_installed_by_default()) {
+ set_installed_by_default(from.installed_by_default());
+ }
+ if (from.has_installed_by_oem()) {
+ set_installed_by_oem(from.installed_by_oem());
+ }
+ if (from.has_from_bookmark()) {
+ set_from_bookmark(from.from_bookmark());
+ }
+ if (from.has_from_webstore()) {
+ set_from_webstore(from.from_webstore());
+ }
+ if (from.has_converted_from_user_script()) {
+ set_converted_from_user_script(from.converted_from_user_script());
+ }
+ if (from.has_may_be_untrusted()) {
+ set_may_be_untrusted(from.may_be_untrusted());
+ }
+ }
+ if (from._has_bits_[16 / 32] & (0xffu << (16 % 32))) {
+ if (from.has_install_time_msec()) {
+ set_install_time_msec(from.install_time_msec());
+ }
+ if (from.has_manifest_location_type()) {
+ set_manifest_location_type(from.manifest_location_type());
+ }
+ if (from.has_manifest()) {
+ set_manifest(from.manifest());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::CopyFrom(const ClientIncidentReport_ExtensionData_ExtensionInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_ExtensionData_ExtensionInfo::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_ExtensionData_ExtensionInfo::Swap(ClientIncidentReport_ExtensionData_ExtensionInfo* other) {
+ if (other != this) {
+ std::swap(id_, other->id_);
+ std::swap(version_, other->version_);
+ std::swap(name_, other->name_);
+ std::swap(description_, other->description_);
+ std::swap(state_, other->state_);
+ std::swap(type_, other->type_);
+ std::swap(update_url_, other->update_url_);
+ std::swap(has_signature_validation_, other->has_signature_validation_);
+ std::swap(signature_is_valid_, other->signature_is_valid_);
+ std::swap(installed_by_custodian_, other->installed_by_custodian_);
+ std::swap(installed_by_default_, other->installed_by_default_);
+ std::swap(installed_by_oem_, other->installed_by_oem_);
+ std::swap(from_bookmark_, other->from_bookmark_);
+ std::swap(from_webstore_, other->from_webstore_);
+ std::swap(converted_from_user_script_, other->converted_from_user_script_);
+ std::swap(may_be_untrusted_, other->may_be_untrusted_);
+ std::swap(install_time_msec_, other->install_time_msec_);
+ std::swap(manifest_location_type_, other->manifest_location_type_);
+ std::swap(manifest_, other->manifest_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_ExtensionData_ExtensionInfo::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_ExtensionData::kLastInstalledExtensionFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_ExtensionData::ClientIncidentReport_ExtensionData()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.ExtensionData)
+}
+
+void ClientIncidentReport_ExtensionData::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ last_installed_extension_ = const_cast< ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo*>(
+ ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo::internal_default_instance());
+#else
+ last_installed_extension_ = const_cast< ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo*>(&::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo::default_instance());
+#endif
+}
+
+ClientIncidentReport_ExtensionData::ClientIncidentReport_ExtensionData(const ClientIncidentReport_ExtensionData& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.ExtensionData)
+}
+
+void ClientIncidentReport_ExtensionData::SharedCtor() {
+ _cached_size_ = 0;
+ last_installed_extension_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_ExtensionData::~ClientIncidentReport_ExtensionData() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.ExtensionData)
+ SharedDtor();
+}
+
+void ClientIncidentReport_ExtensionData::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete last_installed_extension_;
+ }
+}
+
+void ClientIncidentReport_ExtensionData::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_ExtensionData& ClientIncidentReport_ExtensionData::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_ExtensionData* ClientIncidentReport_ExtensionData::default_instance_ = NULL;
+
+ClientIncidentReport_ExtensionData* ClientIncidentReport_ExtensionData::New() const {
+ return new ClientIncidentReport_ExtensionData;
+}
+
+void ClientIncidentReport_ExtensionData::Clear() {
+ if (has_last_installed_extension()) {
+ if (last_installed_extension_ != NULL) last_installed_extension_->::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo::Clear();
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_ExtensionData::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.ExtensionData)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo last_installed_extension = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_last_installed_extension()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.ExtensionData)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.ExtensionData)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_ExtensionData::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.ExtensionData)
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo last_installed_extension = 1;
+ if (has_last_installed_extension()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->last_installed_extension(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.ExtensionData)
+}
+
+int ClientIncidentReport_ExtensionData::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo last_installed_extension = 1;
+ if (has_last_installed_extension()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->last_installed_extension());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_ExtensionData::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_ExtensionData*>(&from));
+}
+
+void ClientIncidentReport_ExtensionData::MergeFrom(const ClientIncidentReport_ExtensionData& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_last_installed_extension()) {
+ mutable_last_installed_extension()->::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo::MergeFrom(from.last_installed_extension());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_ExtensionData::CopyFrom(const ClientIncidentReport_ExtensionData& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_ExtensionData::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_ExtensionData::Swap(ClientIncidentReport_ExtensionData* other) {
+ if (other != this) {
+ std::swap(last_installed_extension_, other->last_installed_extension_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_ExtensionData::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.ExtensionData";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport_NonBinaryDownloadDetails::kFileTypeFieldNumber;
+const int ClientIncidentReport_NonBinaryDownloadDetails::kUrlSpecSha256FieldNumber;
+const int ClientIncidentReport_NonBinaryDownloadDetails::kHostFieldNumber;
+const int ClientIncidentReport_NonBinaryDownloadDetails::kLengthFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport_NonBinaryDownloadDetails::ClientIncidentReport_NonBinaryDownloadDetails()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::InitAsDefaultInstance() {
+}
+
+ClientIncidentReport_NonBinaryDownloadDetails::ClientIncidentReport_NonBinaryDownloadDetails(const ClientIncidentReport_NonBinaryDownloadDetails& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ file_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ url_spec_sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ host_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ length_ = GOOGLE_LONGLONG(0);
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport_NonBinaryDownloadDetails::~ClientIncidentReport_NonBinaryDownloadDetails() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+ SharedDtor();
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::SharedDtor() {
+ if (file_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_type_;
+ }
+ if (url_spec_sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_spec_sha256_;
+ }
+ if (host_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete host_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport_NonBinaryDownloadDetails& ClientIncidentReport_NonBinaryDownloadDetails::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport_NonBinaryDownloadDetails* ClientIncidentReport_NonBinaryDownloadDetails::default_instance_ = NULL;
+
+ClientIncidentReport_NonBinaryDownloadDetails* ClientIncidentReport_NonBinaryDownloadDetails::New() const {
+ return new ClientIncidentReport_NonBinaryDownloadDetails;
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::Clear() {
+ if (_has_bits_[0 / 32] & 15) {
+ if (has_file_type()) {
+ if (file_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_type_->clear();
+ }
+ }
+ if (has_url_spec_sha256()) {
+ if (url_spec_sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_spec_sha256_->clear();
+ }
+ }
+ if (has_host()) {
+ if (host_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ host_->clear();
+ }
+ }
+ length_ = GOOGLE_LONGLONG(0);
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport_NonBinaryDownloadDetails::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string file_type = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_file_type()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_url_spec_sha256;
+ break;
+ }
+
+ // optional bytes url_spec_sha256 = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_url_spec_sha256:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_url_spec_sha256()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_host;
+ break;
+ }
+
+ // optional string host = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_host:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_host()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_length;
+ break;
+ }
+
+ // optional int64 length = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_length:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &length_)));
+ set_has_length();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+ // optional string file_type = 1;
+ if (has_file_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->file_type(), output);
+ }
+
+ // optional bytes url_spec_sha256 = 2;
+ if (has_url_spec_sha256()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->url_spec_sha256(), output);
+ }
+
+ // optional string host = 3;
+ if (has_host()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->host(), output);
+ }
+
+ // optional int64 length = 4;
+ if (has_length()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(4, this->length(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+}
+
+int ClientIncidentReport_NonBinaryDownloadDetails::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string file_type = 1;
+ if (has_file_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->file_type());
+ }
+
+ // optional bytes url_spec_sha256 = 2;
+ if (has_url_spec_sha256()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->url_spec_sha256());
+ }
+
+ // optional string host = 3;
+ if (has_host()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->host());
+ }
+
+ // optional int64 length = 4;
+ if (has_length()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->length());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport_NonBinaryDownloadDetails*>(&from));
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::MergeFrom(const ClientIncidentReport_NonBinaryDownloadDetails& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_file_type()) {
+ set_file_type(from.file_type());
+ }
+ if (from.has_url_spec_sha256()) {
+ set_url_spec_sha256(from.url_spec_sha256());
+ }
+ if (from.has_host()) {
+ set_host(from.host());
+ }
+ if (from.has_length()) {
+ set_length(from.length());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::CopyFrom(const ClientIncidentReport_NonBinaryDownloadDetails& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport_NonBinaryDownloadDetails::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentReport_NonBinaryDownloadDetails::Swap(ClientIncidentReport_NonBinaryDownloadDetails* other) {
+ if (other != this) {
+ std::swap(file_type_, other->file_type_);
+ std::swap(url_spec_sha256_, other->url_spec_sha256_);
+ std::swap(host_, other->host_);
+ std::swap(length_, other->length_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport_NonBinaryDownloadDetails::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentReport::kIncidentFieldNumber;
+const int ClientIncidentReport::kDownloadFieldNumber;
+const int ClientIncidentReport::kEnvironmentFieldNumber;
+const int ClientIncidentReport::kPopulationFieldNumber;
+const int ClientIncidentReport::kExtensionDataFieldNumber;
+const int ClientIncidentReport::kNonBinaryDownloadFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentReport::ClientIncidentReport()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentReport)
+}
+
+void ClientIncidentReport::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ download_ = const_cast< ::safe_browsing::ClientIncidentReport_DownloadDetails*>(
+ ::safe_browsing::ClientIncidentReport_DownloadDetails::internal_default_instance());
+#else
+ download_ = const_cast< ::safe_browsing::ClientIncidentReport_DownloadDetails*>(&::safe_browsing::ClientIncidentReport_DownloadDetails::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ environment_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData*>(
+ ::safe_browsing::ClientIncidentReport_EnvironmentData::internal_default_instance());
+#else
+ environment_ = const_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData*>(&::safe_browsing::ClientIncidentReport_EnvironmentData::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(
+ ::safe_browsing::ChromeUserPopulation::internal_default_instance());
+#else
+ population_ = const_cast< ::safe_browsing::ChromeUserPopulation*>(&::safe_browsing::ChromeUserPopulation::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ extension_data_ = const_cast< ::safe_browsing::ClientIncidentReport_ExtensionData*>(
+ ::safe_browsing::ClientIncidentReport_ExtensionData::internal_default_instance());
+#else
+ extension_data_ = const_cast< ::safe_browsing::ClientIncidentReport_ExtensionData*>(&::safe_browsing::ClientIncidentReport_ExtensionData::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ non_binary_download_ = const_cast< ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails*>(
+ ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails::internal_default_instance());
+#else
+ non_binary_download_ = const_cast< ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails*>(&::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails::default_instance());
+#endif
+}
+
+ClientIncidentReport::ClientIncidentReport(const ClientIncidentReport& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentReport)
+}
+
+void ClientIncidentReport::SharedCtor() {
+ _cached_size_ = 0;
+ download_ = NULL;
+ environment_ = NULL;
+ population_ = NULL;
+ extension_data_ = NULL;
+ non_binary_download_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentReport::~ClientIncidentReport() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentReport)
+ SharedDtor();
+}
+
+void ClientIncidentReport::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete download_;
+ delete environment_;
+ delete population_;
+ delete extension_data_;
+ delete non_binary_download_;
+ }
+}
+
+void ClientIncidentReport::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentReport& ClientIncidentReport::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentReport* ClientIncidentReport::default_instance_ = NULL;
+
+ClientIncidentReport* ClientIncidentReport::New() const {
+ return new ClientIncidentReport;
+}
+
+void ClientIncidentReport::Clear() {
+ if (_has_bits_[0 / 32] & 62) {
+ if (has_download()) {
+ if (download_ != NULL) download_->::safe_browsing::ClientIncidentReport_DownloadDetails::Clear();
+ }
+ if (has_environment()) {
+ if (environment_ != NULL) environment_->::safe_browsing::ClientIncidentReport_EnvironmentData::Clear();
+ }
+ if (has_population()) {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ }
+ if (has_extension_data()) {
+ if (extension_data_ != NULL) extension_data_->::safe_browsing::ClientIncidentReport_ExtensionData::Clear();
+ }
+ if (has_non_binary_download()) {
+ if (non_binary_download_ != NULL) non_binary_download_->::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails::Clear();
+ }
+ }
+ incident_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentReport::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentReport)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData incident = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_incident:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_incident()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_incident;
+ if (input->ExpectTag(18)) goto parse_download;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_download:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_download()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_environment;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData environment = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_environment:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_environment()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_population;
+ break;
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_population:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_population()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_extension_data;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData extension_data = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_extension_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_extension_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(74)) goto parse_non_binary_download;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails non_binary_download = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_non_binary_download:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_non_binary_download()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentReport)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentReport)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentReport::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentReport)
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData incident = 1;
+ for (int i = 0; i < this->incident_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->incident(i), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ if (has_download()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->download(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData environment = 3;
+ if (has_environment()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->environment(), output);
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 7;
+ if (has_population()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 7, this->population(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData extension_data = 8;
+ if (has_extension_data()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 8, this->extension_data(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails non_binary_download = 9;
+ if (has_non_binary_download()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 9, this->non_binary_download(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentReport)
+}
+
+int ClientIncidentReport::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ if (has_download()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->download());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData environment = 3;
+ if (has_environment()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->environment());
+ }
+
+ // optional .safe_browsing.ChromeUserPopulation population = 7;
+ if (has_population()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->population());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData extension_data = 8;
+ if (has_extension_data()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->extension_data());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails non_binary_download = 9;
+ if (has_non_binary_download()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->non_binary_download());
+ }
+
+ }
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData incident = 1;
+ total_size += 1 * this->incident_size();
+ for (int i = 0; i < this->incident_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->incident(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentReport::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentReport*>(&from));
+}
+
+void ClientIncidentReport::MergeFrom(const ClientIncidentReport& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ incident_.MergeFrom(from.incident_);
+ if (from._has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ if (from.has_download()) {
+ mutable_download()->::safe_browsing::ClientIncidentReport_DownloadDetails::MergeFrom(from.download());
+ }
+ if (from.has_environment()) {
+ mutable_environment()->::safe_browsing::ClientIncidentReport_EnvironmentData::MergeFrom(from.environment());
+ }
+ if (from.has_population()) {
+ mutable_population()->::safe_browsing::ChromeUserPopulation::MergeFrom(from.population());
+ }
+ if (from.has_extension_data()) {
+ mutable_extension_data()->::safe_browsing::ClientIncidentReport_ExtensionData::MergeFrom(from.extension_data());
+ }
+ if (from.has_non_binary_download()) {
+ mutable_non_binary_download()->::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails::MergeFrom(from.non_binary_download());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentReport::CopyFrom(const ClientIncidentReport& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentReport::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->incident())) return false;
+ if (has_download()) {
+ if (!this->download().IsInitialized()) return false;
+ }
+ if (has_environment()) {
+ if (!this->environment().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientIncidentReport::Swap(ClientIncidentReport* other) {
+ if (other != this) {
+ incident_.Swap(&other->incident_);
+ std::swap(download_, other->download_);
+ std::swap(environment_, other->environment_);
+ std::swap(population_, other->population_);
+ std::swap(extension_data_, other->extension_data_);
+ std::swap(non_binary_download_, other->non_binary_download_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentReport::GetTypeName() const {
+ return "safe_browsing.ClientIncidentReport";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientIncidentResponse_EnvironmentRequest::kDllIndexFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentResponse_EnvironmentRequest::ClientIncidentResponse_EnvironmentRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+}
+
+void ClientIncidentResponse_EnvironmentRequest::InitAsDefaultInstance() {
+}
+
+ClientIncidentResponse_EnvironmentRequest::ClientIncidentResponse_EnvironmentRequest(const ClientIncidentResponse_EnvironmentRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+}
+
+void ClientIncidentResponse_EnvironmentRequest::SharedCtor() {
+ _cached_size_ = 0;
+ dll_index_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentResponse_EnvironmentRequest::~ClientIncidentResponse_EnvironmentRequest() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+ SharedDtor();
+}
+
+void ClientIncidentResponse_EnvironmentRequest::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentResponse_EnvironmentRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentResponse_EnvironmentRequest& ClientIncidentResponse_EnvironmentRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentResponse_EnvironmentRequest* ClientIncidentResponse_EnvironmentRequest::default_instance_ = NULL;
+
+ClientIncidentResponse_EnvironmentRequest* ClientIncidentResponse_EnvironmentRequest::New() const {
+ return new ClientIncidentResponse_EnvironmentRequest;
+}
+
+void ClientIncidentResponse_EnvironmentRequest::Clear() {
+ dll_index_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentResponse_EnvironmentRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int32 dll_index = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &dll_index_)));
+ set_has_dll_index();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentResponse_EnvironmentRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+ // optional int32 dll_index = 1;
+ if (has_dll_index()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->dll_index(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+}
+
+int ClientIncidentResponse_EnvironmentRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int32 dll_index = 1;
+ if (has_dll_index()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->dll_index());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentResponse_EnvironmentRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentResponse_EnvironmentRequest*>(&from));
+}
+
+void ClientIncidentResponse_EnvironmentRequest::MergeFrom(const ClientIncidentResponse_EnvironmentRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_dll_index()) {
+ set_dll_index(from.dll_index());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentResponse_EnvironmentRequest::CopyFrom(const ClientIncidentResponse_EnvironmentRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentResponse_EnvironmentRequest::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentResponse_EnvironmentRequest::Swap(ClientIncidentResponse_EnvironmentRequest* other) {
+ if (other != this) {
+ std::swap(dll_index_, other->dll_index_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentResponse_EnvironmentRequest::GetTypeName() const {
+ return "safe_browsing.ClientIncidentResponse.EnvironmentRequest";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientIncidentResponse::kTokenFieldNumber;
+const int ClientIncidentResponse::kDownloadRequestedFieldNumber;
+const int ClientIncidentResponse::kEnvironmentRequestsFieldNumber;
+#endif // !_MSC_VER
+
+ClientIncidentResponse::ClientIncidentResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientIncidentResponse)
+}
+
+void ClientIncidentResponse::InitAsDefaultInstance() {
+}
+
+ClientIncidentResponse::ClientIncidentResponse(const ClientIncidentResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientIncidentResponse)
+}
+
+void ClientIncidentResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ download_requested_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientIncidentResponse::~ClientIncidentResponse() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientIncidentResponse)
+ SharedDtor();
+}
+
+void ClientIncidentResponse::SharedDtor() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientIncidentResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientIncidentResponse& ClientIncidentResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientIncidentResponse* ClientIncidentResponse::default_instance_ = NULL;
+
+ClientIncidentResponse* ClientIncidentResponse::New() const {
+ return new ClientIncidentResponse;
+}
+
+void ClientIncidentResponse::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_token()) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ }
+ download_requested_ = false;
+ }
+ environment_requests_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientIncidentResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientIncidentResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes token = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_token()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_download_requested;
+ break;
+ }
+
+ // optional bool download_requested = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_download_requested:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &download_requested_)));
+ set_has_download_requested();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_environment_requests;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientIncidentResponse.EnvironmentRequest environment_requests = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_environment_requests:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_environment_requests()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_environment_requests;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientIncidentResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientIncidentResponse)
+ return false;
+#undef DO_
+}
+
+void ClientIncidentResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientIncidentResponse)
+ // optional bytes token = 1;
+ if (has_token()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->token(), output);
+ }
+
+ // optional bool download_requested = 2;
+ if (has_download_requested()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->download_requested(), output);
+ }
+
+ // repeated .safe_browsing.ClientIncidentResponse.EnvironmentRequest environment_requests = 3;
+ for (int i = 0; i < this->environment_requests_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->environment_requests(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientIncidentResponse)
+}
+
+int ClientIncidentResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes token = 1;
+ if (has_token()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->token());
+ }
+
+ // optional bool download_requested = 2;
+ if (has_download_requested()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .safe_browsing.ClientIncidentResponse.EnvironmentRequest environment_requests = 3;
+ total_size += 1 * this->environment_requests_size();
+ for (int i = 0; i < this->environment_requests_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->environment_requests(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientIncidentResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientIncidentResponse*>(&from));
+}
+
+void ClientIncidentResponse::MergeFrom(const ClientIncidentResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ environment_requests_.MergeFrom(from.environment_requests_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_token()) {
+ set_token(from.token());
+ }
+ if (from.has_download_requested()) {
+ set_download_requested(from.download_requested());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientIncidentResponse::CopyFrom(const ClientIncidentResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientIncidentResponse::IsInitialized() const {
+
+ return true;
+}
+
+void ClientIncidentResponse::Swap(ClientIncidentResponse* other) {
+ if (other != this) {
+ std::swap(token_, other->token_);
+ std::swap(download_requested_, other->download_requested_);
+ environment_requests_.Swap(&other->environment_requests_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientIncidentResponse::GetTypeName() const {
+ return "safe_browsing.ClientIncidentResponse";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int DownloadMetadata::kDownloadIdFieldNumber;
+const int DownloadMetadata::kDownloadFieldNumber;
+#endif // !_MSC_VER
+
+DownloadMetadata::DownloadMetadata()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.DownloadMetadata)
+}
+
+void DownloadMetadata::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ download_ = const_cast< ::safe_browsing::ClientIncidentReport_DownloadDetails*>(
+ ::safe_browsing::ClientIncidentReport_DownloadDetails::internal_default_instance());
+#else
+ download_ = const_cast< ::safe_browsing::ClientIncidentReport_DownloadDetails*>(&::safe_browsing::ClientIncidentReport_DownloadDetails::default_instance());
+#endif
+}
+
+DownloadMetadata::DownloadMetadata(const DownloadMetadata& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.DownloadMetadata)
+}
+
+void DownloadMetadata::SharedCtor() {
+ _cached_size_ = 0;
+ download_id_ = 0u;
+ download_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+DownloadMetadata::~DownloadMetadata() {
+ // @@protoc_insertion_point(destructor:safe_browsing.DownloadMetadata)
+ SharedDtor();
+}
+
+void DownloadMetadata::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete download_;
+ }
+}
+
+void DownloadMetadata::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const DownloadMetadata& DownloadMetadata::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+DownloadMetadata* DownloadMetadata::default_instance_ = NULL;
+
+DownloadMetadata* DownloadMetadata::New() const {
+ return new DownloadMetadata;
+}
+
+void DownloadMetadata::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ download_id_ = 0u;
+ if (has_download()) {
+ if (download_ != NULL) download_->::safe_browsing::ClientIncidentReport_DownloadDetails::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool DownloadMetadata::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.DownloadMetadata)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional uint32 download_id = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint32, ::google::protobuf::internal::WireFormatLite::TYPE_UINT32>(
+ input, &download_id_)));
+ set_has_download_id();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_download;
+ break;
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_download:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_download()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.DownloadMetadata)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.DownloadMetadata)
+ return false;
+#undef DO_
+}
+
+void DownloadMetadata::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.DownloadMetadata)
+ // optional uint32 download_id = 1;
+ if (has_download_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt32(1, this->download_id(), output);
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ if (has_download()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->download(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.DownloadMetadata)
+}
+
+int DownloadMetadata::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional uint32 download_id = 1;
+ if (has_download_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt32Size(
+ this->download_id());
+ }
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ if (has_download()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->download());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void DownloadMetadata::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const DownloadMetadata*>(&from));
+}
+
+void DownloadMetadata::MergeFrom(const DownloadMetadata& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_download_id()) {
+ set_download_id(from.download_id());
+ }
+ if (from.has_download()) {
+ mutable_download()->::safe_browsing::ClientIncidentReport_DownloadDetails::MergeFrom(from.download());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void DownloadMetadata::CopyFrom(const DownloadMetadata& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool DownloadMetadata::IsInitialized() const {
+
+ if (has_download()) {
+ if (!this->download().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void DownloadMetadata::Swap(DownloadMetadata* other) {
+ if (other != this) {
+ std::swap(download_id_, other->download_id_);
+ std::swap(download_, other->download_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string DownloadMetadata::GetTypeName() const {
+ return "safe_browsing.DownloadMetadata";
+}
+
+
+// ===================================================================
+
+bool ClientSafeBrowsingReportRequest_ReportType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 10:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::UNKNOWN;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::URL_PHISHING;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::URL_MALWARE;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::URL_UNWANTED;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::CLIENT_SIDE_PHISHING_URL;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::CLIENT_SIDE_MALWARE_URL;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::DANGEROUS_DOWNLOAD_RECOVERY;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::DANGEROUS_DOWNLOAD_WARNING;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::DANGEROUS_DOWNLOAD_BY_API;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::ReportType_MIN;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::ReportType_MAX;
+const int ClientSafeBrowsingReportRequest::ReportType_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest_HTTPHeader::kNameFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPHeader::kValueFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest_HTTPHeader::ClientSafeBrowsingReportRequest_HTTPHeader()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::InitAsDefaultInstance() {
+}
+
+ClientSafeBrowsingReportRequest_HTTPHeader::ClientSafeBrowsingReportRequest_HTTPHeader(const ClientSafeBrowsingReportRequest_HTTPHeader& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest_HTTPHeader::~ClientSafeBrowsingReportRequest_HTTPHeader() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete value_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest_HTTPHeader& ClientSafeBrowsingReportRequest_HTTPHeader::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest_HTTPHeader* ClientSafeBrowsingReportRequest_HTTPHeader::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest_HTTPHeader* ClientSafeBrowsingReportRequest_HTTPHeader::New() const {
+ return new ClientSafeBrowsingReportRequest_HTTPHeader;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_value()) {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPHeader::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required bytes name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ break;
+ }
+
+ // optional bytes value = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+ // required bytes name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional bytes value = 2;
+ if (has_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->value(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+}
+
+int ClientSafeBrowsingReportRequest_HTTPHeader::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required bytes name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->name());
+ }
+
+ // optional bytes value = 2;
+ if (has_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->value());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest_HTTPHeader*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::MergeFrom(const ClientSafeBrowsingReportRequest_HTTPHeader& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_value()) {
+ set_value(from.value());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::CopyFrom(const ClientSafeBrowsingReportRequest_HTTPHeader& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPHeader::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPHeader::Swap(ClientSafeBrowsingReportRequest_HTTPHeader* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(value_, other->value_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest_HTTPHeader::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::kVerbFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::kUriFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::kVersionFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::InitAsDefaultInstance() {
+}
+
+ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ verb_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ uri_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::~ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::SharedDtor() {
+ if (verb_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete verb_;
+ }
+ if (uri_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete uri_;
+ }
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::New() const {
+ return new ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_verb()) {
+ if (verb_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ verb_->clear();
+ }
+ }
+ if (has_uri()) {
+ if (uri_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ uri_->clear();
+ }
+ }
+ if (has_version()) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes verb = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_verb()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_uri;
+ break;
+ }
+
+ // optional bytes uri = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_uri:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_uri()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_version;
+ break;
+ }
+
+ // optional bytes version = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+ // optional bytes verb = 1;
+ if (has_verb()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->verb(), output);
+ }
+
+ // optional bytes uri = 2;
+ if (has_uri()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->uri(), output);
+ }
+
+ // optional bytes version = 3;
+ if (has_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->version(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+}
+
+int ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes verb = 1;
+ if (has_verb()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->verb());
+ }
+
+ // optional bytes uri = 2;
+ if (has_uri()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->uri());
+ }
+
+ // optional bytes version = 3;
+ if (has_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->version());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::MergeFrom(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_verb()) {
+ set_verb(from.verb());
+ }
+ if (from.has_uri()) {
+ set_uri(from.uri());
+ }
+ if (from.has_version()) {
+ set_version(from.version());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::CopyFrom(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::IsInitialized() const {
+
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::Swap(ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* other) {
+ if (other != this) {
+ std::swap(verb_, other->verb_);
+ std::swap(uri_, other->uri_);
+ std::swap(version_, other->version_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest_HTTPRequest::kFirstlineFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPRequest::kHeadersFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPRequest::kBodyFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPRequest::kBodydigestFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPRequest::kBodylengthFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest_HTTPRequest::ClientSafeBrowsingReportRequest_HTTPRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ firstline_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine*>(
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::internal_default_instance());
+#else
+ firstline_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine*>(&::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::default_instance());
+#endif
+}
+
+ClientSafeBrowsingReportRequest_HTTPRequest::ClientSafeBrowsingReportRequest_HTTPRequest(const ClientSafeBrowsingReportRequest_HTTPRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ firstline_ = NULL;
+ body_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ bodydigest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ bodylength_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest_HTTPRequest::~ClientSafeBrowsingReportRequest_HTTPRequest() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::SharedDtor() {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete body_;
+ }
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bodydigest_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete firstline_;
+ }
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest_HTTPRequest& ClientSafeBrowsingReportRequest_HTTPRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest_HTTPRequest* ClientSafeBrowsingReportRequest_HTTPRequest::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest_HTTPRequest* ClientSafeBrowsingReportRequest_HTTPRequest::New() const {
+ return new ClientSafeBrowsingReportRequest_HTTPRequest;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::Clear() {
+ if (_has_bits_[0 / 32] & 29) {
+ if (has_firstline()) {
+ if (firstline_ != NULL) firstline_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::Clear();
+ }
+ if (has_body()) {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_->clear();
+ }
+ }
+ if (has_bodydigest()) {
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_->clear();
+ }
+ }
+ bodylength_ = 0;
+ }
+ headers_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine firstline = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_firstline()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_headers;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_headers;
+ if (input->ExpectTag(26)) goto parse_body;
+ break;
+ }
+
+ // optional bytes body = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_body:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_body()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_bodydigest;
+ break;
+ }
+
+ // optional bytes bodydigest = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_bodydigest:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_bodydigest()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_bodylength;
+ break;
+ }
+
+ // optional int32 bodylength = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_bodylength:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &bodylength_)));
+ set_has_bodylength();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine firstline = 1;
+ if (has_firstline()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->firstline(), output);
+ }
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ for (int i = 0; i < this->headers_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->headers(i), output);
+ }
+
+ // optional bytes body = 3;
+ if (has_body()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->body(), output);
+ }
+
+ // optional bytes bodydigest = 4;
+ if (has_bodydigest()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 4, this->bodydigest(), output);
+ }
+
+ // optional int32 bodylength = 5;
+ if (has_bodylength()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(5, this->bodylength(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+}
+
+int ClientSafeBrowsingReportRequest_HTTPRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine firstline = 1;
+ if (has_firstline()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->firstline());
+ }
+
+ // optional bytes body = 3;
+ if (has_body()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->body());
+ }
+
+ // optional bytes bodydigest = 4;
+ if (has_bodydigest()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->bodydigest());
+ }
+
+ // optional int32 bodylength = 5;
+ if (has_bodylength()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->bodylength());
+ }
+
+ }
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ total_size += 1 * this->headers_size();
+ for (int i = 0; i < this->headers_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->headers(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest_HTTPRequest*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::MergeFrom(const ClientSafeBrowsingReportRequest_HTTPRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ headers_.MergeFrom(from.headers_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_firstline()) {
+ mutable_firstline()->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::MergeFrom(from.firstline());
+ }
+ if (from.has_body()) {
+ set_body(from.body());
+ }
+ if (from.has_bodydigest()) {
+ set_bodydigest(from.bodydigest());
+ }
+ if (from.has_bodylength()) {
+ set_bodylength(from.bodylength());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::CopyFrom(const ClientSafeBrowsingReportRequest_HTTPRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPRequest::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->headers())) return false;
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPRequest::Swap(ClientSafeBrowsingReportRequest_HTTPRequest* other) {
+ if (other != this) {
+ std::swap(firstline_, other->firstline_);
+ headers_.Swap(&other->headers_);
+ std::swap(body_, other->body_);
+ std::swap(bodydigest_, other->bodydigest_);
+ std::swap(bodylength_, other->bodylength_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest_HTTPRequest::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::kCodeFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::kReasonFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::kVersionFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::InitAsDefaultInstance() {
+}
+
+ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ code_ = 0;
+ reason_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::~ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::SharedDtor() {
+ if (reason_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete reason_;
+ }
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::New() const {
+ return new ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ code_ = 0;
+ if (has_reason()) {
+ if (reason_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ reason_->clear();
+ }
+ }
+ if (has_version()) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int32 code = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &code_)));
+ set_has_code();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_reason;
+ break;
+ }
+
+ // optional bytes reason = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_reason:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_reason()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_version;
+ break;
+ }
+
+ // optional bytes version = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+ // optional int32 code = 1;
+ if (has_code()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->code(), output);
+ }
+
+ // optional bytes reason = 2;
+ if (has_reason()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->reason(), output);
+ }
+
+ // optional bytes version = 3;
+ if (has_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->version(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+}
+
+int ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int32 code = 1;
+ if (has_code()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->code());
+ }
+
+ // optional bytes reason = 2;
+ if (has_reason()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->reason());
+ }
+
+ // optional bytes version = 3;
+ if (has_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->version());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::MergeFrom(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_code()) {
+ set_code(from.code());
+ }
+ if (from.has_reason()) {
+ set_reason(from.reason());
+ }
+ if (from.has_version()) {
+ set_version(from.version());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::CopyFrom(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::IsInitialized() const {
+
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::Swap(ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* other) {
+ if (other != this) {
+ std::swap(code_, other->code_);
+ std::swap(reason_, other->reason_);
+ std::swap(version_, other->version_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest_HTTPResponse::kFirstlineFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse::kHeadersFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse::kBodyFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse::kBodydigestFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse::kBodylengthFieldNumber;
+const int ClientSafeBrowsingReportRequest_HTTPResponse::kRemoteIpFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest_HTTPResponse::ClientSafeBrowsingReportRequest_HTTPResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ firstline_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine*>(
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::internal_default_instance());
+#else
+ firstline_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine*>(&::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::default_instance());
+#endif
+}
+
+ClientSafeBrowsingReportRequest_HTTPResponse::ClientSafeBrowsingReportRequest_HTTPResponse(const ClientSafeBrowsingReportRequest_HTTPResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ firstline_ = NULL;
+ body_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ bodydigest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ bodylength_ = 0;
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest_HTTPResponse::~ClientSafeBrowsingReportRequest_HTTPResponse() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::SharedDtor() {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete body_;
+ }
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bodydigest_;
+ }
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete remote_ip_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete firstline_;
+ }
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest_HTTPResponse& ClientSafeBrowsingReportRequest_HTTPResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest_HTTPResponse* ClientSafeBrowsingReportRequest_HTTPResponse::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest_HTTPResponse* ClientSafeBrowsingReportRequest_HTTPResponse::New() const {
+ return new ClientSafeBrowsingReportRequest_HTTPResponse;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::Clear() {
+ if (_has_bits_[0 / 32] & 61) {
+ if (has_firstline()) {
+ if (firstline_ != NULL) firstline_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::Clear();
+ }
+ if (has_body()) {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_->clear();
+ }
+ }
+ if (has_bodydigest()) {
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_->clear();
+ }
+ }
+ bodylength_ = 0;
+ if (has_remote_ip()) {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_->clear();
+ }
+ }
+ }
+ headers_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine firstline = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_firstline()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_headers;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_headers:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_headers()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_headers;
+ if (input->ExpectTag(26)) goto parse_body;
+ break;
+ }
+
+ // optional bytes body = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_body:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_body()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_bodydigest;
+ break;
+ }
+
+ // optional bytes bodydigest = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_bodydigest:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_bodydigest()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_bodylength;
+ break;
+ }
+
+ // optional int32 bodylength = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_bodylength:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &bodylength_)));
+ set_has_bodylength();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_remote_ip;
+ break;
+ }
+
+ // optional bytes remote_ip = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_remote_ip:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_remote_ip()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine firstline = 1;
+ if (has_firstline()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->firstline(), output);
+ }
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ for (int i = 0; i < this->headers_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->headers(i), output);
+ }
+
+ // optional bytes body = 3;
+ if (has_body()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->body(), output);
+ }
+
+ // optional bytes bodydigest = 4;
+ if (has_bodydigest()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 4, this->bodydigest(), output);
+ }
+
+ // optional int32 bodylength = 5;
+ if (has_bodylength()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(5, this->bodylength(), output);
+ }
+
+ // optional bytes remote_ip = 6;
+ if (has_remote_ip()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 6, this->remote_ip(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+}
+
+int ClientSafeBrowsingReportRequest_HTTPResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine firstline = 1;
+ if (has_firstline()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->firstline());
+ }
+
+ // optional bytes body = 3;
+ if (has_body()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->body());
+ }
+
+ // optional bytes bodydigest = 4;
+ if (has_bodydigest()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->bodydigest());
+ }
+
+ // optional int32 bodylength = 5;
+ if (has_bodylength()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->bodylength());
+ }
+
+ // optional bytes remote_ip = 6;
+ if (has_remote_ip()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->remote_ip());
+ }
+
+ }
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ total_size += 1 * this->headers_size();
+ for (int i = 0; i < this->headers_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->headers(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest_HTTPResponse*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::MergeFrom(const ClientSafeBrowsingReportRequest_HTTPResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ headers_.MergeFrom(from.headers_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_firstline()) {
+ mutable_firstline()->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::MergeFrom(from.firstline());
+ }
+ if (from.has_body()) {
+ set_body(from.body());
+ }
+ if (from.has_bodydigest()) {
+ set_bodydigest(from.bodydigest());
+ }
+ if (from.has_bodylength()) {
+ set_bodylength(from.bodylength());
+ }
+ if (from.has_remote_ip()) {
+ set_remote_ip(from.remote_ip());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::CopyFrom(const ClientSafeBrowsingReportRequest_HTTPResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest_HTTPResponse::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->headers())) return false;
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest_HTTPResponse::Swap(ClientSafeBrowsingReportRequest_HTTPResponse* other) {
+ if (other != this) {
+ std::swap(firstline_, other->firstline_);
+ headers_.Swap(&other->headers_);
+ std::swap(body_, other->body_);
+ std::swap(bodydigest_, other->bodydigest_);
+ std::swap(bodylength_, other->bodylength_);
+ std::swap(remote_ip_, other->remote_ip_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest_HTTPResponse::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest_Resource::kIdFieldNumber;
+const int ClientSafeBrowsingReportRequest_Resource::kUrlFieldNumber;
+const int ClientSafeBrowsingReportRequest_Resource::kRequestFieldNumber;
+const int ClientSafeBrowsingReportRequest_Resource::kResponseFieldNumber;
+const int ClientSafeBrowsingReportRequest_Resource::kParentIdFieldNumber;
+const int ClientSafeBrowsingReportRequest_Resource::kChildIdsFieldNumber;
+const int ClientSafeBrowsingReportRequest_Resource::kTagNameFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest_Resource::ClientSafeBrowsingReportRequest_Resource()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+}
+
+void ClientSafeBrowsingReportRequest_Resource::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ request_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest*>(
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest::internal_default_instance());
+#else
+ request_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest*>(&::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ response_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse*>(
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse::internal_default_instance());
+#else
+ response_ = const_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse*>(&::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse::default_instance());
+#endif
+}
+
+ClientSafeBrowsingReportRequest_Resource::ClientSafeBrowsingReportRequest_Resource(const ClientSafeBrowsingReportRequest_Resource& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+}
+
+void ClientSafeBrowsingReportRequest_Resource::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ id_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ request_ = NULL;
+ response_ = NULL;
+ parent_id_ = 0;
+ tag_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest_Resource::~ClientSafeBrowsingReportRequest_Resource() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest_Resource::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (tag_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete tag_name_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete request_;
+ delete response_;
+ }
+}
+
+void ClientSafeBrowsingReportRequest_Resource::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest_Resource& ClientSafeBrowsingReportRequest_Resource::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest_Resource* ClientSafeBrowsingReportRequest_Resource::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest_Resource* ClientSafeBrowsingReportRequest_Resource::New() const {
+ return new ClientSafeBrowsingReportRequest_Resource;
+}
+
+void ClientSafeBrowsingReportRequest_Resource::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientSafeBrowsingReportRequest_Resource*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 95) {
+ ZR_(id_, parent_id_);
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ if (has_request()) {
+ if (request_ != NULL) request_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest::Clear();
+ }
+ if (has_response()) {
+ if (response_ != NULL) response_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse::Clear();
+ }
+ if (has_tag_name()) {
+ if (tag_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ tag_name_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ child_ids_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest_Resource::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required int32 id = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &id_)));
+ set_has_id();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_url;
+ break;
+ }
+
+ // optional string url = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_request;
+ break;
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest request = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_request:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_request()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_response;
+ break;
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse response = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_response:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_response()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_parent_id;
+ break;
+ }
+
+ // optional int32 parent_id = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_parent_id:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &parent_id_)));
+ set_has_parent_id();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_child_ids;
+ break;
+ }
+
+ // repeated int32 child_ids = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_child_ids:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ 1, 48, input, this->mutable_child_ids())));
+ } else if (tag == 50) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitiveNoInline<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, this->mutable_child_ids())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_child_ids;
+ if (input->ExpectTag(58)) goto parse_tag_name;
+ break;
+ }
+
+ // optional string tag_name = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_tag_name:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_tag_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest_Resource::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+ // required int32 id = 1;
+ if (has_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->id(), output);
+ }
+
+ // optional string url = 2;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->url(), output);
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest request = 3;
+ if (has_request()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->request(), output);
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse response = 4;
+ if (has_response()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->response(), output);
+ }
+
+ // optional int32 parent_id = 5;
+ if (has_parent_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(5, this->parent_id(), output);
+ }
+
+ // repeated int32 child_ids = 6;
+ for (int i = 0; i < this->child_ids_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(
+ 6, this->child_ids(i), output);
+ }
+
+ // optional string tag_name = 7;
+ if (has_tag_name()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 7, this->tag_name(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+}
+
+int ClientSafeBrowsingReportRequest_Resource::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required int32 id = 1;
+ if (has_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->id());
+ }
+
+ // optional string url = 2;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest request = 3;
+ if (has_request()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->request());
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse response = 4;
+ if (has_response()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->response());
+ }
+
+ // optional int32 parent_id = 5;
+ if (has_parent_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->parent_id());
+ }
+
+ // optional string tag_name = 7;
+ if (has_tag_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->tag_name());
+ }
+
+ }
+ // repeated int32 child_ids = 6;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->child_ids_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ Int32Size(this->child_ids(i));
+ }
+ total_size += 1 * this->child_ids_size() + data_size;
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest_Resource::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest_Resource*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest_Resource::MergeFrom(const ClientSafeBrowsingReportRequest_Resource& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ child_ids_.MergeFrom(from.child_ids_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_id()) {
+ set_id(from.id());
+ }
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_request()) {
+ mutable_request()->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest::MergeFrom(from.request());
+ }
+ if (from.has_response()) {
+ mutable_response()->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse::MergeFrom(from.response());
+ }
+ if (from.has_parent_id()) {
+ set_parent_id(from.parent_id());
+ }
+ if (from.has_tag_name()) {
+ set_tag_name(from.tag_name());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest_Resource::CopyFrom(const ClientSafeBrowsingReportRequest_Resource& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest_Resource::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000001) != 0x00000001) return false;
+
+ if (has_request()) {
+ if (!this->request().IsInitialized()) return false;
+ }
+ if (has_response()) {
+ if (!this->response().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest_Resource::Swap(ClientSafeBrowsingReportRequest_Resource* other) {
+ if (other != this) {
+ std::swap(id_, other->id_);
+ std::swap(url_, other->url_);
+ std::swap(request_, other->request_);
+ std::swap(response_, other->response_);
+ std::swap(parent_id_, other->parent_id_);
+ child_ids_.Swap(&other->child_ids_);
+ std::swap(tag_name_, other->tag_name_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest_Resource::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest.Resource";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ClientSafeBrowsingReportRequest::kTypeFieldNumber;
+const int ClientSafeBrowsingReportRequest::kDownloadVerdictFieldNumber;
+const int ClientSafeBrowsingReportRequest::kUrlFieldNumber;
+const int ClientSafeBrowsingReportRequest::kPageUrlFieldNumber;
+const int ClientSafeBrowsingReportRequest::kReferrerUrlFieldNumber;
+const int ClientSafeBrowsingReportRequest::kResourcesFieldNumber;
+const int ClientSafeBrowsingReportRequest::kCompleteFieldNumber;
+const int ClientSafeBrowsingReportRequest::kClientAsnFieldNumber;
+const int ClientSafeBrowsingReportRequest::kClientCountryFieldNumber;
+const int ClientSafeBrowsingReportRequest::kDidProceedFieldNumber;
+const int ClientSafeBrowsingReportRequest::kRepeatVisitFieldNumber;
+const int ClientSafeBrowsingReportRequest::kTokenFieldNumber;
+#endif // !_MSC_VER
+
+ClientSafeBrowsingReportRequest::ClientSafeBrowsingReportRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:safe_browsing.ClientSafeBrowsingReportRequest)
+}
+
+void ClientSafeBrowsingReportRequest::InitAsDefaultInstance() {
+}
+
+ClientSafeBrowsingReportRequest::ClientSafeBrowsingReportRequest(const ClientSafeBrowsingReportRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:safe_browsing.ClientSafeBrowsingReportRequest)
+}
+
+void ClientSafeBrowsingReportRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ type_ = 0;
+ download_verdict_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ page_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ complete_ = false;
+ client_country_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ did_proceed_ = false;
+ repeat_visit_ = false;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSafeBrowsingReportRequest::~ClientSafeBrowsingReportRequest() {
+ // @@protoc_insertion_point(destructor:safe_browsing.ClientSafeBrowsingReportRequest)
+ SharedDtor();
+}
+
+void ClientSafeBrowsingReportRequest::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (page_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete page_url_;
+ }
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_url_;
+ }
+ if (client_country_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete client_country_;
+ }
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientSafeBrowsingReportRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSafeBrowsingReportRequest& ClientSafeBrowsingReportRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientSafeBrowsingReportRequest* ClientSafeBrowsingReportRequest::default_instance_ = NULL;
+
+ClientSafeBrowsingReportRequest* ClientSafeBrowsingReportRequest::New() const {
+ return new ClientSafeBrowsingReportRequest;
+}
+
+void ClientSafeBrowsingReportRequest::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ClientSafeBrowsingReportRequest*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 95) {
+ ZR_(type_, download_verdict_);
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ if (has_page_url()) {
+ if (page_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ page_url_->clear();
+ }
+ }
+ if (has_referrer_url()) {
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_->clear();
+ }
+ }
+ complete_ = false;
+ }
+ if (_has_bits_[8 / 32] & 3840) {
+ ZR_(did_proceed_, repeat_visit_);
+ if (has_client_country()) {
+ if (client_country_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_country_->clear();
+ }
+ }
+ if (has_token()) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ resources_.Clear();
+ client_asn_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientSafeBrowsingReportRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:safe_browsing.ClientSafeBrowsingReportRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_page_url;
+ break;
+ }
+
+ // optional string page_url = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_page_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_page_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_referrer_url;
+ break;
+ }
+
+ // optional string referrer_url = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_referrer_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_referrer_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_resources;
+ break;
+ }
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.Resource resources = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_resources:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_resources()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_resources;
+ if (input->ExpectTag(40)) goto parse_complete;
+ break;
+ }
+
+ // optional bool complete = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_complete:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &complete_)));
+ set_has_complete();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_client_asn;
+ break;
+ }
+
+ // repeated string client_asn = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_client_asn:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_client_asn()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_client_asn;
+ if (input->ExpectTag(58)) goto parse_client_country;
+ break;
+ }
+
+ // optional string client_country = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_client_country:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_client_country()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(64)) goto parse_did_proceed;
+ break;
+ }
+
+ // optional bool did_proceed = 8;
+ case 8: {
+ if (tag == 64) {
+ parse_did_proceed:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &did_proceed_)));
+ set_has_did_proceed();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(72)) goto parse_repeat_visit;
+ break;
+ }
+
+ // optional bool repeat_visit = 9;
+ case 9: {
+ if (tag == 72) {
+ parse_repeat_visit:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &repeat_visit_)));
+ set_has_repeat_visit();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_type;
+ break;
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.ReportType type = 10;
+ case 10: {
+ if (tag == 80) {
+ parse_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientSafeBrowsingReportRequest_ReportType_IsValid(value)) {
+ set_type(static_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_ReportType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(88)) goto parse_download_verdict;
+ break;
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse.Verdict download_verdict = 11;
+ case 11: {
+ if (tag == 88) {
+ parse_download_verdict:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::safe_browsing::ClientDownloadResponse_Verdict_IsValid(value)) {
+ set_download_verdict(static_cast< ::safe_browsing::ClientDownloadResponse_Verdict >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(122)) goto parse_token;
+ break;
+ }
+
+ // optional bytes token = 15;
+ case 15: {
+ if (tag == 122) {
+ parse_token:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_token()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:safe_browsing.ClientSafeBrowsingReportRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:safe_browsing.ClientSafeBrowsingReportRequest)
+ return false;
+#undef DO_
+}
+
+void ClientSafeBrowsingReportRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:safe_browsing.ClientSafeBrowsingReportRequest)
+ // optional string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // optional string page_url = 2;
+ if (has_page_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->page_url(), output);
+ }
+
+ // optional string referrer_url = 3;
+ if (has_referrer_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->referrer_url(), output);
+ }
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.Resource resources = 4;
+ for (int i = 0; i < this->resources_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->resources(i), output);
+ }
+
+ // optional bool complete = 5;
+ if (has_complete()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(5, this->complete(), output);
+ }
+
+ // repeated string client_asn = 6;
+ for (int i = 0; i < this->client_asn_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 6, this->client_asn(i), output);
+ }
+
+ // optional string client_country = 7;
+ if (has_client_country()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 7, this->client_country(), output);
+ }
+
+ // optional bool did_proceed = 8;
+ if (has_did_proceed()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(8, this->did_proceed(), output);
+ }
+
+ // optional bool repeat_visit = 9;
+ if (has_repeat_visit()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(9, this->repeat_visit(), output);
+ }
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.ReportType type = 10;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 10, this->type(), output);
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse.Verdict download_verdict = 11;
+ if (has_download_verdict()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 11, this->download_verdict(), output);
+ }
+
+ // optional bytes token = 15;
+ if (has_token()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 15, this->token(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:safe_browsing.ClientSafeBrowsingReportRequest)
+}
+
+int ClientSafeBrowsingReportRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.ReportType type = 10;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+ }
+
+ // optional .safe_browsing.ClientDownloadResponse.Verdict download_verdict = 11;
+ if (has_download_verdict()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->download_verdict());
+ }
+
+ // optional string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional string page_url = 2;
+ if (has_page_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->page_url());
+ }
+
+ // optional string referrer_url = 3;
+ if (has_referrer_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->referrer_url());
+ }
+
+ // optional bool complete = 5;
+ if (has_complete()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional string client_country = 7;
+ if (has_client_country()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->client_country());
+ }
+
+ // optional bool did_proceed = 8;
+ if (has_did_proceed()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool repeat_visit = 9;
+ if (has_repeat_visit()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bytes token = 15;
+ if (has_token()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->token());
+ }
+
+ }
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.Resource resources = 4;
+ total_size += 1 * this->resources_size();
+ for (int i = 0; i < this->resources_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->resources(i));
+ }
+
+ // repeated string client_asn = 6;
+ total_size += 1 * this->client_asn_size();
+ for (int i = 0; i < this->client_asn_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->client_asn(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientSafeBrowsingReportRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientSafeBrowsingReportRequest*>(&from));
+}
+
+void ClientSafeBrowsingReportRequest::MergeFrom(const ClientSafeBrowsingReportRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ resources_.MergeFrom(from.resources_);
+ client_asn_.MergeFrom(from.client_asn_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_download_verdict()) {
+ set_download_verdict(from.download_verdict());
+ }
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_page_url()) {
+ set_page_url(from.page_url());
+ }
+ if (from.has_referrer_url()) {
+ set_referrer_url(from.referrer_url());
+ }
+ if (from.has_complete()) {
+ set_complete(from.complete());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_client_country()) {
+ set_client_country(from.client_country());
+ }
+ if (from.has_did_proceed()) {
+ set_did_proceed(from.did_proceed());
+ }
+ if (from.has_repeat_visit()) {
+ set_repeat_visit(from.repeat_visit());
+ }
+ if (from.has_token()) {
+ set_token(from.token());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSafeBrowsingReportRequest::CopyFrom(const ClientSafeBrowsingReportRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientSafeBrowsingReportRequest::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->resources())) return false;
+ return true;
+}
+
+void ClientSafeBrowsingReportRequest::Swap(ClientSafeBrowsingReportRequest* other) {
+ if (other != this) {
+ std::swap(type_, other->type_);
+ std::swap(download_verdict_, other->download_verdict_);
+ std::swap(url_, other->url_);
+ std::swap(page_url_, other->page_url_);
+ std::swap(referrer_url_, other->referrer_url_);
+ resources_.Swap(&other->resources_);
+ std::swap(complete_, other->complete_);
+ client_asn_.Swap(&other->client_asn_);
+ std::swap(client_country_, other->client_country_);
+ std::swap(did_proceed_, other->did_proceed_);
+ std::swap(repeat_visit_, other->repeat_visit_);
+ std::swap(token_, other->token_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientSafeBrowsingReportRequest::GetTypeName() const {
+ return "safe_browsing.ClientSafeBrowsingReportRequest";
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace safe_browsing
+
+// @@protoc_insertion_point(global_scope)
diff --git a/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.h b/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.h
new file mode 100644
index 0000000000..0ec320b66e
--- /dev/null
+++ b/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.pb.h
@@ -0,0 +1,21771 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: chromium/chrome/common/safe_browsing/csd.proto
+
+#ifndef PROTOBUF_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto__INCLUDED
+#define PROTOBUF_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto__INCLUDED
+
+#include <string>
+
+#include <google/protobuf/stubs/common.h>
+
+#if GOOGLE_PROTOBUF_VERSION < 2006000
+#error This file was generated by a newer version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please update
+#error your headers.
+#endif
+#if 2006001 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION
+#error This file was generated by an older version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please
+#error regenerate this file with a newer version of protoc.
+#endif
+
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/message_lite.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/extension_set.h>
+// @@protoc_insertion_point(includes)
+
+namespace safe_browsing {
+
+// Internal implementation detail -- do not call these.
+void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+class ChromeUserPopulation;
+class ClientPhishingRequest;
+class ClientPhishingRequest_Feature;
+class ClientPhishingResponse;
+class ClientMalwareRequest;
+class ClientMalwareRequest_UrlInfo;
+class ClientMalwareResponse;
+class ClientDownloadRequest;
+class ClientDownloadRequest_Digests;
+class ClientDownloadRequest_Resource;
+class ClientDownloadRequest_CertificateChain;
+class ClientDownloadRequest_CertificateChain_Element;
+class ClientDownloadRequest_ExtendedAttr;
+class ClientDownloadRequest_SignatureInfo;
+class ClientDownloadRequest_PEImageHeaders;
+class ClientDownloadRequest_PEImageHeaders_DebugData;
+class ClientDownloadRequest_MachOHeaders;
+class ClientDownloadRequest_MachOHeaders_LoadCommand;
+class ClientDownloadRequest_ImageHeaders;
+class ClientDownloadRequest_ArchivedBinary;
+class ClientDownloadRequest_URLChainEntry;
+class ClientDownloadResponse;
+class ClientDownloadResponse_MoreInfo;
+class ClientDownloadReport;
+class ClientDownloadReport_UserInformation;
+class ClientUploadResponse;
+class ClientIncidentReport;
+class ClientIncidentReport_IncidentData;
+class ClientIncidentReport_IncidentData_TrackedPreferenceIncident;
+class ClientIncidentReport_IncidentData_BinaryIntegrityIncident;
+class ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile;
+class ClientIncidentReport_IncidentData_BlacklistLoadIncident;
+class ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident;
+class ClientIncidentReport_IncidentData_ResourceRequestIncident;
+class ClientIncidentReport_IncidentData_SuspiciousModuleIncident;
+class ClientIncidentReport_DownloadDetails;
+class ClientIncidentReport_EnvironmentData;
+class ClientIncidentReport_EnvironmentData_OS;
+class ClientIncidentReport_EnvironmentData_OS_RegistryValue;
+class ClientIncidentReport_EnvironmentData_OS_RegistryKey;
+class ClientIncidentReport_EnvironmentData_Machine;
+class ClientIncidentReport_EnvironmentData_Process;
+class ClientIncidentReport_EnvironmentData_Process_Patch;
+class ClientIncidentReport_EnvironmentData_Process_NetworkProvider;
+class ClientIncidentReport_EnvironmentData_Process_Dll;
+class ClientIncidentReport_EnvironmentData_Process_ModuleState;
+class ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification;
+class ClientIncidentReport_ExtensionData;
+class ClientIncidentReport_ExtensionData_ExtensionInfo;
+class ClientIncidentReport_NonBinaryDownloadDetails;
+class ClientIncidentResponse;
+class ClientIncidentResponse_EnvironmentRequest;
+class DownloadMetadata;
+class ClientSafeBrowsingReportRequest;
+class ClientSafeBrowsingReportRequest_HTTPHeader;
+class ClientSafeBrowsingReportRequest_HTTPRequest;
+class ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine;
+class ClientSafeBrowsingReportRequest_HTTPResponse;
+class ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine;
+class ClientSafeBrowsingReportRequest_Resource;
+
+enum ChromeUserPopulation_UserPopulation {
+ ChromeUserPopulation_UserPopulation_UNKNOWN_USER_POPULATION = 0,
+ ChromeUserPopulation_UserPopulation_SAFE_BROWSING = 1,
+ ChromeUserPopulation_UserPopulation_EXTENDED_REPORTING = 2
+};
+bool ChromeUserPopulation_UserPopulation_IsValid(int value);
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation_UserPopulation_UserPopulation_MIN = ChromeUserPopulation_UserPopulation_UNKNOWN_USER_POPULATION;
+const ChromeUserPopulation_UserPopulation ChromeUserPopulation_UserPopulation_UserPopulation_MAX = ChromeUserPopulation_UserPopulation_EXTENDED_REPORTING;
+const int ChromeUserPopulation_UserPopulation_UserPopulation_ARRAYSIZE = ChromeUserPopulation_UserPopulation_UserPopulation_MAX + 1;
+
+enum ClientDownloadRequest_URLChainEntry_URLType {
+ ClientDownloadRequest_URLChainEntry_URLType_DOWNLOAD_URL = 1,
+ ClientDownloadRequest_URLChainEntry_URLType_DOWNLOAD_REFERRER = 2,
+ ClientDownloadRequest_URLChainEntry_URLType_LANDING_PAGE = 3,
+ ClientDownloadRequest_URLChainEntry_URLType_LANDING_REFERRER = 4,
+ ClientDownloadRequest_URLChainEntry_URLType_CLIENT_REDIRECT = 5,
+ ClientDownloadRequest_URLChainEntry_URLType_SERVER_REDIRECT = 6
+};
+bool ClientDownloadRequest_URLChainEntry_URLType_IsValid(int value);
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry_URLType_URLType_MIN = ClientDownloadRequest_URLChainEntry_URLType_DOWNLOAD_URL;
+const ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry_URLType_URLType_MAX = ClientDownloadRequest_URLChainEntry_URLType_SERVER_REDIRECT;
+const int ClientDownloadRequest_URLChainEntry_URLType_URLType_ARRAYSIZE = ClientDownloadRequest_URLChainEntry_URLType_URLType_MAX + 1;
+
+enum ClientDownloadRequest_ResourceType {
+ ClientDownloadRequest_ResourceType_DOWNLOAD_URL = 0,
+ ClientDownloadRequest_ResourceType_DOWNLOAD_REDIRECT = 1,
+ ClientDownloadRequest_ResourceType_TAB_URL = 2,
+ ClientDownloadRequest_ResourceType_TAB_REDIRECT = 3,
+ ClientDownloadRequest_ResourceType_PPAPI_DOCUMENT = 4,
+ ClientDownloadRequest_ResourceType_PPAPI_PLUGIN = 5
+};
+bool ClientDownloadRequest_ResourceType_IsValid(int value);
+const ClientDownloadRequest_ResourceType ClientDownloadRequest_ResourceType_ResourceType_MIN = ClientDownloadRequest_ResourceType_DOWNLOAD_URL;
+const ClientDownloadRequest_ResourceType ClientDownloadRequest_ResourceType_ResourceType_MAX = ClientDownloadRequest_ResourceType_PPAPI_PLUGIN;
+const int ClientDownloadRequest_ResourceType_ResourceType_ARRAYSIZE = ClientDownloadRequest_ResourceType_ResourceType_MAX + 1;
+
+enum ClientDownloadRequest_DownloadType {
+ ClientDownloadRequest_DownloadType_WIN_EXECUTABLE = 0,
+ ClientDownloadRequest_DownloadType_CHROME_EXTENSION = 1,
+ ClientDownloadRequest_DownloadType_ANDROID_APK = 2,
+ ClientDownloadRequest_DownloadType_ZIPPED_EXECUTABLE = 3,
+ ClientDownloadRequest_DownloadType_MAC_EXECUTABLE = 4,
+ ClientDownloadRequest_DownloadType_ZIPPED_ARCHIVE = 5,
+ ClientDownloadRequest_DownloadType_ARCHIVE = 6,
+ ClientDownloadRequest_DownloadType_INVALID_ZIP = 7,
+ ClientDownloadRequest_DownloadType_INVALID_MAC_ARCHIVE = 8,
+ ClientDownloadRequest_DownloadType_PPAPI_SAVE_REQUEST = 9,
+ ClientDownloadRequest_DownloadType_SAMPLED_UNSUPPORTED_FILE = 10
+};
+bool ClientDownloadRequest_DownloadType_IsValid(int value);
+const ClientDownloadRequest_DownloadType ClientDownloadRequest_DownloadType_DownloadType_MIN = ClientDownloadRequest_DownloadType_WIN_EXECUTABLE;
+const ClientDownloadRequest_DownloadType ClientDownloadRequest_DownloadType_DownloadType_MAX = ClientDownloadRequest_DownloadType_SAMPLED_UNSUPPORTED_FILE;
+const int ClientDownloadRequest_DownloadType_DownloadType_ARRAYSIZE = ClientDownloadRequest_DownloadType_DownloadType_MAX + 1;
+
+enum ClientDownloadResponse_Verdict {
+ ClientDownloadResponse_Verdict_SAFE = 0,
+ ClientDownloadResponse_Verdict_DANGEROUS = 1,
+ ClientDownloadResponse_Verdict_UNCOMMON = 2,
+ ClientDownloadResponse_Verdict_POTENTIALLY_UNWANTED = 3,
+ ClientDownloadResponse_Verdict_DANGEROUS_HOST = 4,
+ ClientDownloadResponse_Verdict_UNKNOWN = 5
+};
+bool ClientDownloadResponse_Verdict_IsValid(int value);
+const ClientDownloadResponse_Verdict ClientDownloadResponse_Verdict_Verdict_MIN = ClientDownloadResponse_Verdict_SAFE;
+const ClientDownloadResponse_Verdict ClientDownloadResponse_Verdict_Verdict_MAX = ClientDownloadResponse_Verdict_UNKNOWN;
+const int ClientDownloadResponse_Verdict_Verdict_ARRAYSIZE = ClientDownloadResponse_Verdict_Verdict_MAX + 1;
+
+enum ClientDownloadReport_Reason {
+ ClientDownloadReport_Reason_SHARE = 0,
+ ClientDownloadReport_Reason_FALSE_POSITIVE = 1,
+ ClientDownloadReport_Reason_APPEAL = 2
+};
+bool ClientDownloadReport_Reason_IsValid(int value);
+const ClientDownloadReport_Reason ClientDownloadReport_Reason_Reason_MIN = ClientDownloadReport_Reason_SHARE;
+const ClientDownloadReport_Reason ClientDownloadReport_Reason_Reason_MAX = ClientDownloadReport_Reason_APPEAL;
+const int ClientDownloadReport_Reason_Reason_ARRAYSIZE = ClientDownloadReport_Reason_Reason_MAX + 1;
+
+enum ClientUploadResponse_UploadStatus {
+ ClientUploadResponse_UploadStatus_SUCCESS = 0,
+ ClientUploadResponse_UploadStatus_UPLOAD_FAILURE = 1
+};
+bool ClientUploadResponse_UploadStatus_IsValid(int value);
+const ClientUploadResponse_UploadStatus ClientUploadResponse_UploadStatus_UploadStatus_MIN = ClientUploadResponse_UploadStatus_SUCCESS;
+const ClientUploadResponse_UploadStatus ClientUploadResponse_UploadStatus_UploadStatus_MAX = ClientUploadResponse_UploadStatus_UPLOAD_FAILURE;
+const int ClientUploadResponse_UploadStatus_UploadStatus_ARRAYSIZE = ClientUploadResponse_UploadStatus_UploadStatus_MAX + 1;
+
+enum ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState {
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_UNKNOWN = 0,
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_CLEARED = 1,
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_WEAK_LEGACY_OBSOLETE = 2,
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_CHANGED = 3,
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_UNTRUSTED_UNKNOWN_VALUE = 4
+};
+bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_IsValid(int value);
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_MIN = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_UNKNOWN;
+const ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_MAX = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_UNTRUSTED_UNKNOWN_VALUE;
+const int ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_ARRAYSIZE = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_MAX + 1;
+
+enum ClientIncidentReport_IncidentData_ResourceRequestIncident_Type {
+ ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_UNKNOWN = 0,
+ ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_TYPE_PATTERN = 3
+};
+bool ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_IsValid(int value);
+const ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_MIN = ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_UNKNOWN;
+const ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_MAX = ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_TYPE_PATTERN;
+const int ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_ARRAYSIZE = ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_MAX + 1;
+
+enum ClientIncidentReport_EnvironmentData_Process_Dll_Feature {
+ ClientIncidentReport_EnvironmentData_Process_Dll_Feature_UNKNOWN = 0,
+ ClientIncidentReport_EnvironmentData_Process_Dll_Feature_LSP = 1
+};
+bool ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid(int value);
+const ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_MIN = ClientIncidentReport_EnvironmentData_Process_Dll_Feature_UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_MAX = ClientIncidentReport_EnvironmentData_Process_Dll_Feature_LSP;
+const int ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_ARRAYSIZE = ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_MAX + 1;
+
+enum ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState {
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_UNKNOWN = 0,
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_UNKNOWN = 1,
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_UNMODIFIED = 2,
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_MODIFIED = 3
+};
+bool ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_IsValid(int value);
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_MIN = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_MAX = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_MODIFIED;
+const int ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_ARRAYSIZE = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_MAX + 1;
+
+enum ClientIncidentReport_EnvironmentData_Process_Channel {
+ ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_UNKNOWN = 0,
+ ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_CANARY = 1,
+ ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_DEV = 2,
+ ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_BETA = 3,
+ ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_STABLE = 4
+};
+bool ClientIncidentReport_EnvironmentData_Process_Channel_IsValid(int value);
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process_Channel_Channel_MIN = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_UNKNOWN;
+const ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process_Channel_Channel_MAX = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_STABLE;
+const int ClientIncidentReport_EnvironmentData_Process_Channel_Channel_ARRAYSIZE = ClientIncidentReport_EnvironmentData_Process_Channel_Channel_MAX + 1;
+
+enum ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState {
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_UNKNOWN = 0,
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_ENABLED = 1,
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_DISABLED = 2,
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_BLACKLISTED = 3,
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_BLOCKED = 4,
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_TERMINATED = 5
+};
+bool ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_IsValid(int value);
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_MIN = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_UNKNOWN;
+const ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_MAX = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_TERMINATED;
+const int ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_ARRAYSIZE = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_MAX + 1;
+
+enum ClientSafeBrowsingReportRequest_ReportType {
+ ClientSafeBrowsingReportRequest_ReportType_UNKNOWN = 0,
+ ClientSafeBrowsingReportRequest_ReportType_URL_PHISHING = 1,
+ ClientSafeBrowsingReportRequest_ReportType_URL_MALWARE = 2,
+ ClientSafeBrowsingReportRequest_ReportType_URL_UNWANTED = 3,
+ ClientSafeBrowsingReportRequest_ReportType_CLIENT_SIDE_PHISHING_URL = 4,
+ ClientSafeBrowsingReportRequest_ReportType_CLIENT_SIDE_MALWARE_URL = 5,
+ ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_RECOVERY = 6,
+ ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_WARNING = 7,
+ ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_BY_API = 10
+};
+bool ClientSafeBrowsingReportRequest_ReportType_IsValid(int value);
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest_ReportType_ReportType_MIN = ClientSafeBrowsingReportRequest_ReportType_UNKNOWN;
+const ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest_ReportType_ReportType_MAX = ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_BY_API;
+const int ClientSafeBrowsingReportRequest_ReportType_ReportType_ARRAYSIZE = ClientSafeBrowsingReportRequest_ReportType_ReportType_MAX + 1;
+
+// ===================================================================
+
+class ChromeUserPopulation : public ::google::protobuf::MessageLite {
+ public:
+ ChromeUserPopulation();
+ virtual ~ChromeUserPopulation();
+
+ ChromeUserPopulation(const ChromeUserPopulation& from);
+
+ inline ChromeUserPopulation& operator=(const ChromeUserPopulation& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ChromeUserPopulation& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ChromeUserPopulation* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ChromeUserPopulation* other);
+
+ // implements Message ----------------------------------------------
+
+ ChromeUserPopulation* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ChromeUserPopulation& from);
+ void MergeFrom(const ChromeUserPopulation& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ChromeUserPopulation_UserPopulation UserPopulation;
+ static const UserPopulation UNKNOWN_USER_POPULATION = ChromeUserPopulation_UserPopulation_UNKNOWN_USER_POPULATION;
+ static const UserPopulation SAFE_BROWSING = ChromeUserPopulation_UserPopulation_SAFE_BROWSING;
+ static const UserPopulation EXTENDED_REPORTING = ChromeUserPopulation_UserPopulation_EXTENDED_REPORTING;
+ static inline bool UserPopulation_IsValid(int value) {
+ return ChromeUserPopulation_UserPopulation_IsValid(value);
+ }
+ static const UserPopulation UserPopulation_MIN =
+ ChromeUserPopulation_UserPopulation_UserPopulation_MIN;
+ static const UserPopulation UserPopulation_MAX =
+ ChromeUserPopulation_UserPopulation_UserPopulation_MAX;
+ static const int UserPopulation_ARRAYSIZE =
+ ChromeUserPopulation_UserPopulation_UserPopulation_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ChromeUserPopulation.UserPopulation user_population = 1;
+ inline bool has_user_population() const;
+ inline void clear_user_population();
+ static const int kUserPopulationFieldNumber = 1;
+ inline ::safe_browsing::ChromeUserPopulation_UserPopulation user_population() const;
+ inline void set_user_population(::safe_browsing::ChromeUserPopulation_UserPopulation value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ChromeUserPopulation)
+ private:
+ inline void set_has_user_population();
+ inline void clear_has_user_population();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int user_population_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ChromeUserPopulation* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientPhishingRequest_Feature : public ::google::protobuf::MessageLite {
+ public:
+ ClientPhishingRequest_Feature();
+ virtual ~ClientPhishingRequest_Feature();
+
+ ClientPhishingRequest_Feature(const ClientPhishingRequest_Feature& from);
+
+ inline ClientPhishingRequest_Feature& operator=(const ClientPhishingRequest_Feature& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientPhishingRequest_Feature& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientPhishingRequest_Feature* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientPhishingRequest_Feature* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientPhishingRequest_Feature* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientPhishingRequest_Feature& from);
+ void MergeFrom(const ClientPhishingRequest_Feature& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // required double value = 2;
+ inline bool has_value() const;
+ inline void clear_value();
+ static const int kValueFieldNumber = 2;
+ inline double value() const;
+ inline void set_value(double value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientPhishingRequest.Feature)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_value();
+ inline void clear_has_value();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ double value_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientPhishingRequest_Feature* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientPhishingRequest : public ::google::protobuf::MessageLite {
+ public:
+ ClientPhishingRequest();
+ virtual ~ClientPhishingRequest();
+
+ ClientPhishingRequest(const ClientPhishingRequest& from);
+
+ inline ClientPhishingRequest& operator=(const ClientPhishingRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientPhishingRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientPhishingRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientPhishingRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientPhishingRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientPhishingRequest& from);
+ void MergeFrom(const ClientPhishingRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientPhishingRequest_Feature Feature;
+
+ // accessors -------------------------------------------------------
+
+ // optional string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional bytes OBSOLETE_hash_prefix = 10;
+ inline bool has_obsolete_hash_prefix() const;
+ inline void clear_obsolete_hash_prefix();
+ static const int kOBSOLETEHashPrefixFieldNumber = 10;
+ inline const ::std::string& obsolete_hash_prefix() const;
+ inline void set_obsolete_hash_prefix(const ::std::string& value);
+ inline void set_obsolete_hash_prefix(const char* value);
+ inline void set_obsolete_hash_prefix(const void* value, size_t size);
+ inline ::std::string* mutable_obsolete_hash_prefix();
+ inline ::std::string* release_obsolete_hash_prefix();
+ inline void set_allocated_obsolete_hash_prefix(::std::string* obsolete_hash_prefix);
+
+ // required float client_score = 2;
+ inline bool has_client_score() const;
+ inline void clear_client_score();
+ static const int kClientScoreFieldNumber = 2;
+ inline float client_score() const;
+ inline void set_client_score(float value);
+
+ // optional bool is_phishing = 4;
+ inline bool has_is_phishing() const;
+ inline void clear_is_phishing();
+ static const int kIsPhishingFieldNumber = 4;
+ inline bool is_phishing() const;
+ inline void set_is_phishing(bool value);
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature feature_map = 5;
+ inline int feature_map_size() const;
+ inline void clear_feature_map();
+ static const int kFeatureMapFieldNumber = 5;
+ inline const ::safe_browsing::ClientPhishingRequest_Feature& feature_map(int index) const;
+ inline ::safe_browsing::ClientPhishingRequest_Feature* mutable_feature_map(int index);
+ inline ::safe_browsing::ClientPhishingRequest_Feature* add_feature_map();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >&
+ feature_map() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >*
+ mutable_feature_map();
+
+ // optional int32 model_version = 6;
+ inline bool has_model_version() const;
+ inline void clear_model_version();
+ static const int kModelVersionFieldNumber = 6;
+ inline ::google::protobuf::int32 model_version() const;
+ inline void set_model_version(::google::protobuf::int32 value);
+
+ // repeated .safe_browsing.ClientPhishingRequest.Feature non_model_feature_map = 8;
+ inline int non_model_feature_map_size() const;
+ inline void clear_non_model_feature_map();
+ static const int kNonModelFeatureMapFieldNumber = 8;
+ inline const ::safe_browsing::ClientPhishingRequest_Feature& non_model_feature_map(int index) const;
+ inline ::safe_browsing::ClientPhishingRequest_Feature* mutable_non_model_feature_map(int index);
+ inline ::safe_browsing::ClientPhishingRequest_Feature* add_non_model_feature_map();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >&
+ non_model_feature_map() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >*
+ mutable_non_model_feature_map();
+
+ // optional string OBSOLETE_referrer_url = 9;
+ inline bool has_obsolete_referrer_url() const;
+ inline void clear_obsolete_referrer_url();
+ static const int kOBSOLETEReferrerUrlFieldNumber = 9;
+ inline const ::std::string& obsolete_referrer_url() const;
+ inline void set_obsolete_referrer_url(const ::std::string& value);
+ inline void set_obsolete_referrer_url(const char* value);
+ inline void set_obsolete_referrer_url(const char* value, size_t size);
+ inline ::std::string* mutable_obsolete_referrer_url();
+ inline ::std::string* release_obsolete_referrer_url();
+ inline void set_allocated_obsolete_referrer_url(::std::string* obsolete_referrer_url);
+
+ // repeated uint32 shingle_hashes = 12 [packed = true];
+ inline int shingle_hashes_size() const;
+ inline void clear_shingle_hashes();
+ static const int kShingleHashesFieldNumber = 12;
+ inline ::google::protobuf::uint32 shingle_hashes(int index) const;
+ inline void set_shingle_hashes(int index, ::google::protobuf::uint32 value);
+ inline void add_shingle_hashes(::google::protobuf::uint32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::uint32 >&
+ shingle_hashes() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::uint32 >*
+ mutable_shingle_hashes();
+
+ // optional string model_filename = 13;
+ inline bool has_model_filename() const;
+ inline void clear_model_filename();
+ static const int kModelFilenameFieldNumber = 13;
+ inline const ::std::string& model_filename() const;
+ inline void set_model_filename(const ::std::string& value);
+ inline void set_model_filename(const char* value);
+ inline void set_model_filename(const char* value, size_t size);
+ inline ::std::string* mutable_model_filename();
+ inline ::std::string* release_model_filename();
+ inline void set_allocated_model_filename(::std::string* model_filename);
+
+ // optional .safe_browsing.ChromeUserPopulation population = 14;
+ inline bool has_population() const;
+ inline void clear_population();
+ static const int kPopulationFieldNumber = 14;
+ inline const ::safe_browsing::ChromeUserPopulation& population() const;
+ inline ::safe_browsing::ChromeUserPopulation* mutable_population();
+ inline ::safe_browsing::ChromeUserPopulation* release_population();
+ inline void set_allocated_population(::safe_browsing::ChromeUserPopulation* population);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientPhishingRequest)
+ private:
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_obsolete_hash_prefix();
+ inline void clear_has_obsolete_hash_prefix();
+ inline void set_has_client_score();
+ inline void clear_has_client_score();
+ inline void set_has_is_phishing();
+ inline void clear_has_is_phishing();
+ inline void set_has_model_version();
+ inline void clear_has_model_version();
+ inline void set_has_obsolete_referrer_url();
+ inline void clear_has_obsolete_referrer_url();
+ inline void set_has_model_filename();
+ inline void clear_has_model_filename();
+ inline void set_has_population();
+ inline void clear_has_population();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::std::string* obsolete_hash_prefix_;
+ float client_score_;
+ bool is_phishing_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature > feature_map_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature > non_model_feature_map_;
+ ::std::string* obsolete_referrer_url_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::uint32 > shingle_hashes_;
+ mutable int _shingle_hashes_cached_byte_size_;
+ ::std::string* model_filename_;
+ ::safe_browsing::ChromeUserPopulation* population_;
+ ::google::protobuf::int32 model_version_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientPhishingRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientPhishingResponse : public ::google::protobuf::MessageLite {
+ public:
+ ClientPhishingResponse();
+ virtual ~ClientPhishingResponse();
+
+ ClientPhishingResponse(const ClientPhishingResponse& from);
+
+ inline ClientPhishingResponse& operator=(const ClientPhishingResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientPhishingResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientPhishingResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientPhishingResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientPhishingResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientPhishingResponse& from);
+ void MergeFrom(const ClientPhishingResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required bool phishy = 1;
+ inline bool has_phishy() const;
+ inline void clear_phishy();
+ static const int kPhishyFieldNumber = 1;
+ inline bool phishy() const;
+ inline void set_phishy(bool value);
+
+ // repeated string OBSOLETE_whitelist_expression = 2;
+ inline int obsolete_whitelist_expression_size() const;
+ inline void clear_obsolete_whitelist_expression();
+ static const int kOBSOLETEWhitelistExpressionFieldNumber = 2;
+ inline const ::std::string& obsolete_whitelist_expression(int index) const;
+ inline ::std::string* mutable_obsolete_whitelist_expression(int index);
+ inline void set_obsolete_whitelist_expression(int index, const ::std::string& value);
+ inline void set_obsolete_whitelist_expression(int index, const char* value);
+ inline void set_obsolete_whitelist_expression(int index, const char* value, size_t size);
+ inline ::std::string* add_obsolete_whitelist_expression();
+ inline void add_obsolete_whitelist_expression(const ::std::string& value);
+ inline void add_obsolete_whitelist_expression(const char* value);
+ inline void add_obsolete_whitelist_expression(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& obsolete_whitelist_expression() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_obsolete_whitelist_expression();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientPhishingResponse)
+ private:
+ inline void set_has_phishy();
+ inline void clear_has_phishy();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> obsolete_whitelist_expression_;
+ bool phishy_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientPhishingResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientMalwareRequest_UrlInfo : public ::google::protobuf::MessageLite {
+ public:
+ ClientMalwareRequest_UrlInfo();
+ virtual ~ClientMalwareRequest_UrlInfo();
+
+ ClientMalwareRequest_UrlInfo(const ClientMalwareRequest_UrlInfo& from);
+
+ inline ClientMalwareRequest_UrlInfo& operator=(const ClientMalwareRequest_UrlInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientMalwareRequest_UrlInfo& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientMalwareRequest_UrlInfo* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientMalwareRequest_UrlInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientMalwareRequest_UrlInfo* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientMalwareRequest_UrlInfo& from);
+ void MergeFrom(const ClientMalwareRequest_UrlInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required string ip = 1;
+ inline bool has_ip() const;
+ inline void clear_ip();
+ static const int kIpFieldNumber = 1;
+ inline const ::std::string& ip() const;
+ inline void set_ip(const ::std::string& value);
+ inline void set_ip(const char* value);
+ inline void set_ip(const char* value, size_t size);
+ inline ::std::string* mutable_ip();
+ inline ::std::string* release_ip();
+ inline void set_allocated_ip(::std::string* ip);
+
+ // required string url = 2;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 2;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional string method = 3;
+ inline bool has_method() const;
+ inline void clear_method();
+ static const int kMethodFieldNumber = 3;
+ inline const ::std::string& method() const;
+ inline void set_method(const ::std::string& value);
+ inline void set_method(const char* value);
+ inline void set_method(const char* value, size_t size);
+ inline ::std::string* mutable_method();
+ inline ::std::string* release_method();
+ inline void set_allocated_method(::std::string* method);
+
+ // optional string referrer = 4;
+ inline bool has_referrer() const;
+ inline void clear_referrer();
+ static const int kReferrerFieldNumber = 4;
+ inline const ::std::string& referrer() const;
+ inline void set_referrer(const ::std::string& value);
+ inline void set_referrer(const char* value);
+ inline void set_referrer(const char* value, size_t size);
+ inline ::std::string* mutable_referrer();
+ inline ::std::string* release_referrer();
+ inline void set_allocated_referrer(::std::string* referrer);
+
+ // optional int32 resource_type = 5;
+ inline bool has_resource_type() const;
+ inline void clear_resource_type();
+ static const int kResourceTypeFieldNumber = 5;
+ inline ::google::protobuf::int32 resource_type() const;
+ inline void set_resource_type(::google::protobuf::int32 value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientMalwareRequest.UrlInfo)
+ private:
+ inline void set_has_ip();
+ inline void clear_has_ip();
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_method();
+ inline void clear_has_method();
+ inline void set_has_referrer();
+ inline void clear_has_referrer();
+ inline void set_has_resource_type();
+ inline void clear_has_resource_type();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* ip_;
+ ::std::string* url_;
+ ::std::string* method_;
+ ::std::string* referrer_;
+ ::google::protobuf::int32 resource_type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientMalwareRequest_UrlInfo* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientMalwareRequest : public ::google::protobuf::MessageLite {
+ public:
+ ClientMalwareRequest();
+ virtual ~ClientMalwareRequest();
+
+ ClientMalwareRequest(const ClientMalwareRequest& from);
+
+ inline ClientMalwareRequest& operator=(const ClientMalwareRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientMalwareRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientMalwareRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientMalwareRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientMalwareRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientMalwareRequest& from);
+ void MergeFrom(const ClientMalwareRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientMalwareRequest_UrlInfo UrlInfo;
+
+ // accessors -------------------------------------------------------
+
+ // required string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional string referrer_url = 4;
+ inline bool has_referrer_url() const;
+ inline void clear_referrer_url();
+ static const int kReferrerUrlFieldNumber = 4;
+ inline const ::std::string& referrer_url() const;
+ inline void set_referrer_url(const ::std::string& value);
+ inline void set_referrer_url(const char* value);
+ inline void set_referrer_url(const char* value, size_t size);
+ inline ::std::string* mutable_referrer_url();
+ inline ::std::string* release_referrer_url();
+ inline void set_allocated_referrer_url(::std::string* referrer_url);
+
+ // repeated .safe_browsing.ClientMalwareRequest.UrlInfo bad_ip_url_info = 7;
+ inline int bad_ip_url_info_size() const;
+ inline void clear_bad_ip_url_info();
+ static const int kBadIpUrlInfoFieldNumber = 7;
+ inline const ::safe_browsing::ClientMalwareRequest_UrlInfo& bad_ip_url_info(int index) const;
+ inline ::safe_browsing::ClientMalwareRequest_UrlInfo* mutable_bad_ip_url_info(int index);
+ inline ::safe_browsing::ClientMalwareRequest_UrlInfo* add_bad_ip_url_info();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientMalwareRequest_UrlInfo >&
+ bad_ip_url_info() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientMalwareRequest_UrlInfo >*
+ mutable_bad_ip_url_info();
+
+ // optional .safe_browsing.ChromeUserPopulation population = 9;
+ inline bool has_population() const;
+ inline void clear_population();
+ static const int kPopulationFieldNumber = 9;
+ inline const ::safe_browsing::ChromeUserPopulation& population() const;
+ inline ::safe_browsing::ChromeUserPopulation* mutable_population();
+ inline ::safe_browsing::ChromeUserPopulation* release_population();
+ inline void set_allocated_population(::safe_browsing::ChromeUserPopulation* population);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientMalwareRequest)
+ private:
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_referrer_url();
+ inline void clear_has_referrer_url();
+ inline void set_has_population();
+ inline void clear_has_population();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::std::string* referrer_url_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientMalwareRequest_UrlInfo > bad_ip_url_info_;
+ ::safe_browsing::ChromeUserPopulation* population_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientMalwareRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientMalwareResponse : public ::google::protobuf::MessageLite {
+ public:
+ ClientMalwareResponse();
+ virtual ~ClientMalwareResponse();
+
+ ClientMalwareResponse(const ClientMalwareResponse& from);
+
+ inline ClientMalwareResponse& operator=(const ClientMalwareResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientMalwareResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientMalwareResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientMalwareResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientMalwareResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientMalwareResponse& from);
+ void MergeFrom(const ClientMalwareResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required bool blacklist = 1;
+ inline bool has_blacklist() const;
+ inline void clear_blacklist();
+ static const int kBlacklistFieldNumber = 1;
+ inline bool blacklist() const;
+ inline void set_blacklist(bool value);
+
+ // optional string bad_ip = 2;
+ inline bool has_bad_ip() const;
+ inline void clear_bad_ip();
+ static const int kBadIpFieldNumber = 2;
+ inline const ::std::string& bad_ip() const;
+ inline void set_bad_ip(const ::std::string& value);
+ inline void set_bad_ip(const char* value);
+ inline void set_bad_ip(const char* value, size_t size);
+ inline ::std::string* mutable_bad_ip();
+ inline ::std::string* release_bad_ip();
+ inline void set_allocated_bad_ip(::std::string* bad_ip);
+
+ // optional string bad_url = 3;
+ inline bool has_bad_url() const;
+ inline void clear_bad_url();
+ static const int kBadUrlFieldNumber = 3;
+ inline const ::std::string& bad_url() const;
+ inline void set_bad_url(const ::std::string& value);
+ inline void set_bad_url(const char* value);
+ inline void set_bad_url(const char* value, size_t size);
+ inline ::std::string* mutable_bad_url();
+ inline ::std::string* release_bad_url();
+ inline void set_allocated_bad_url(::std::string* bad_url);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientMalwareResponse)
+ private:
+ inline void set_has_blacklist();
+ inline void clear_has_blacklist();
+ inline void set_has_bad_ip();
+ inline void clear_has_bad_ip();
+ inline void set_has_bad_url();
+ inline void clear_has_bad_url();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* bad_ip_;
+ ::std::string* bad_url_;
+ bool blacklist_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientMalwareResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_Digests : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_Digests();
+ virtual ~ClientDownloadRequest_Digests();
+
+ ClientDownloadRequest_Digests(const ClientDownloadRequest_Digests& from);
+
+ inline ClientDownloadRequest_Digests& operator=(const ClientDownloadRequest_Digests& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_Digests& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_Digests* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_Digests* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_Digests* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_Digests& from);
+ void MergeFrom(const ClientDownloadRequest_Digests& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes sha256 = 1;
+ inline bool has_sha256() const;
+ inline void clear_sha256();
+ static const int kSha256FieldNumber = 1;
+ inline const ::std::string& sha256() const;
+ inline void set_sha256(const ::std::string& value);
+ inline void set_sha256(const char* value);
+ inline void set_sha256(const void* value, size_t size);
+ inline ::std::string* mutable_sha256();
+ inline ::std::string* release_sha256();
+ inline void set_allocated_sha256(::std::string* sha256);
+
+ // optional bytes sha1 = 2;
+ inline bool has_sha1() const;
+ inline void clear_sha1();
+ static const int kSha1FieldNumber = 2;
+ inline const ::std::string& sha1() const;
+ inline void set_sha1(const ::std::string& value);
+ inline void set_sha1(const char* value);
+ inline void set_sha1(const void* value, size_t size);
+ inline ::std::string* mutable_sha1();
+ inline ::std::string* release_sha1();
+ inline void set_allocated_sha1(::std::string* sha1);
+
+ // optional bytes md5 = 3;
+ inline bool has_md5() const;
+ inline void clear_md5();
+ static const int kMd5FieldNumber = 3;
+ inline const ::std::string& md5() const;
+ inline void set_md5(const ::std::string& value);
+ inline void set_md5(const char* value);
+ inline void set_md5(const void* value, size_t size);
+ inline ::std::string* mutable_md5();
+ inline ::std::string* release_md5();
+ inline void set_allocated_md5(::std::string* md5);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.Digests)
+ private:
+ inline void set_has_sha256();
+ inline void clear_has_sha256();
+ inline void set_has_sha1();
+ inline void clear_has_sha1();
+ inline void set_has_md5();
+ inline void clear_has_md5();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* sha256_;
+ ::std::string* sha1_;
+ ::std::string* md5_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_Digests* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_Resource : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_Resource();
+ virtual ~ClientDownloadRequest_Resource();
+
+ ClientDownloadRequest_Resource(const ClientDownloadRequest_Resource& from);
+
+ inline ClientDownloadRequest_Resource& operator=(const ClientDownloadRequest_Resource& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_Resource& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_Resource* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_Resource* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_Resource* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_Resource& from);
+ void MergeFrom(const ClientDownloadRequest_Resource& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // required .safe_browsing.ClientDownloadRequest.ResourceType type = 2;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 2;
+ inline ::safe_browsing::ClientDownloadRequest_ResourceType type() const;
+ inline void set_type(::safe_browsing::ClientDownloadRequest_ResourceType value);
+
+ // optional bytes remote_ip = 3;
+ inline bool has_remote_ip() const;
+ inline void clear_remote_ip();
+ static const int kRemoteIpFieldNumber = 3;
+ inline const ::std::string& remote_ip() const;
+ inline void set_remote_ip(const ::std::string& value);
+ inline void set_remote_ip(const char* value);
+ inline void set_remote_ip(const void* value, size_t size);
+ inline ::std::string* mutable_remote_ip();
+ inline ::std::string* release_remote_ip();
+ inline void set_allocated_remote_ip(::std::string* remote_ip);
+
+ // optional string referrer = 4;
+ inline bool has_referrer() const;
+ inline void clear_referrer();
+ static const int kReferrerFieldNumber = 4;
+ inline const ::std::string& referrer() const;
+ inline void set_referrer(const ::std::string& value);
+ inline void set_referrer(const char* value);
+ inline void set_referrer(const char* value, size_t size);
+ inline ::std::string* mutable_referrer();
+ inline ::std::string* release_referrer();
+ inline void set_allocated_referrer(::std::string* referrer);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.Resource)
+ private:
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_remote_ip();
+ inline void clear_has_remote_ip();
+ inline void set_has_referrer();
+ inline void clear_has_referrer();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::std::string* remote_ip_;
+ ::std::string* referrer_;
+ int type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_Resource* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_CertificateChain_Element : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_CertificateChain_Element();
+ virtual ~ClientDownloadRequest_CertificateChain_Element();
+
+ ClientDownloadRequest_CertificateChain_Element(const ClientDownloadRequest_CertificateChain_Element& from);
+
+ inline ClientDownloadRequest_CertificateChain_Element& operator=(const ClientDownloadRequest_CertificateChain_Element& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_CertificateChain_Element& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_CertificateChain_Element* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_CertificateChain_Element* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_CertificateChain_Element* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_CertificateChain_Element& from);
+ void MergeFrom(const ClientDownloadRequest_CertificateChain_Element& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes certificate = 1;
+ inline bool has_certificate() const;
+ inline void clear_certificate();
+ static const int kCertificateFieldNumber = 1;
+ inline const ::std::string& certificate() const;
+ inline void set_certificate(const ::std::string& value);
+ inline void set_certificate(const char* value);
+ inline void set_certificate(const void* value, size_t size);
+ inline ::std::string* mutable_certificate();
+ inline ::std::string* release_certificate();
+ inline void set_allocated_certificate(::std::string* certificate);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.CertificateChain.Element)
+ private:
+ inline void set_has_certificate();
+ inline void clear_has_certificate();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* certificate_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_CertificateChain_Element* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_CertificateChain : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_CertificateChain();
+ virtual ~ClientDownloadRequest_CertificateChain();
+
+ ClientDownloadRequest_CertificateChain(const ClientDownloadRequest_CertificateChain& from);
+
+ inline ClientDownloadRequest_CertificateChain& operator=(const ClientDownloadRequest_CertificateChain& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_CertificateChain& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_CertificateChain* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_CertificateChain* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_CertificateChain* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_CertificateChain& from);
+ void MergeFrom(const ClientDownloadRequest_CertificateChain& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadRequest_CertificateChain_Element Element;
+
+ // accessors -------------------------------------------------------
+
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain.Element element = 1;
+ inline int element_size() const;
+ inline void clear_element();
+ static const int kElementFieldNumber = 1;
+ inline const ::safe_browsing::ClientDownloadRequest_CertificateChain_Element& element(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_CertificateChain_Element* mutable_element(int index);
+ inline ::safe_browsing::ClientDownloadRequest_CertificateChain_Element* add_element();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain_Element >&
+ element() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain_Element >*
+ mutable_element();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.CertificateChain)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain_Element > element_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_CertificateChain* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_ExtendedAttr : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_ExtendedAttr();
+ virtual ~ClientDownloadRequest_ExtendedAttr();
+
+ ClientDownloadRequest_ExtendedAttr(const ClientDownloadRequest_ExtendedAttr& from);
+
+ inline ClientDownloadRequest_ExtendedAttr& operator=(const ClientDownloadRequest_ExtendedAttr& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_ExtendedAttr& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_ExtendedAttr* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_ExtendedAttr* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_ExtendedAttr* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_ExtendedAttr& from);
+ void MergeFrom(const ClientDownloadRequest_ExtendedAttr& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required string key = 1;
+ inline bool has_key() const;
+ inline void clear_key();
+ static const int kKeyFieldNumber = 1;
+ inline const ::std::string& key() const;
+ inline void set_key(const ::std::string& value);
+ inline void set_key(const char* value);
+ inline void set_key(const char* value, size_t size);
+ inline ::std::string* mutable_key();
+ inline ::std::string* release_key();
+ inline void set_allocated_key(::std::string* key);
+
+ // optional bytes value = 2;
+ inline bool has_value() const;
+ inline void clear_value();
+ static const int kValueFieldNumber = 2;
+ inline const ::std::string& value() const;
+ inline void set_value(const ::std::string& value);
+ inline void set_value(const char* value);
+ inline void set_value(const void* value, size_t size);
+ inline ::std::string* mutable_value();
+ inline ::std::string* release_value();
+ inline void set_allocated_value(::std::string* value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.ExtendedAttr)
+ private:
+ inline void set_has_key();
+ inline void clear_has_key();
+ inline void set_has_value();
+ inline void clear_has_value();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* key_;
+ ::std::string* value_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_ExtendedAttr* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_SignatureInfo : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_SignatureInfo();
+ virtual ~ClientDownloadRequest_SignatureInfo();
+
+ ClientDownloadRequest_SignatureInfo(const ClientDownloadRequest_SignatureInfo& from);
+
+ inline ClientDownloadRequest_SignatureInfo& operator=(const ClientDownloadRequest_SignatureInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_SignatureInfo& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_SignatureInfo* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_SignatureInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_SignatureInfo* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_SignatureInfo& from);
+ void MergeFrom(const ClientDownloadRequest_SignatureInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated .safe_browsing.ClientDownloadRequest.CertificateChain certificate_chain = 1;
+ inline int certificate_chain_size() const;
+ inline void clear_certificate_chain();
+ static const int kCertificateChainFieldNumber = 1;
+ inline const ::safe_browsing::ClientDownloadRequest_CertificateChain& certificate_chain(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_CertificateChain* mutable_certificate_chain(int index);
+ inline ::safe_browsing::ClientDownloadRequest_CertificateChain* add_certificate_chain();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain >&
+ certificate_chain() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain >*
+ mutable_certificate_chain();
+
+ // optional bool trusted = 2;
+ inline bool has_trusted() const;
+ inline void clear_trusted();
+ static const int kTrustedFieldNumber = 2;
+ inline bool trusted() const;
+ inline void set_trusted(bool value);
+
+ // repeated bytes signed_data = 3;
+ inline int signed_data_size() const;
+ inline void clear_signed_data();
+ static const int kSignedDataFieldNumber = 3;
+ inline const ::std::string& signed_data(int index) const;
+ inline ::std::string* mutable_signed_data(int index);
+ inline void set_signed_data(int index, const ::std::string& value);
+ inline void set_signed_data(int index, const char* value);
+ inline void set_signed_data(int index, const void* value, size_t size);
+ inline ::std::string* add_signed_data();
+ inline void add_signed_data(const ::std::string& value);
+ inline void add_signed_data(const char* value);
+ inline void add_signed_data(const void* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& signed_data() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_signed_data();
+
+ // repeated .safe_browsing.ClientDownloadRequest.ExtendedAttr xattr = 4;
+ inline int xattr_size() const;
+ inline void clear_xattr();
+ static const int kXattrFieldNumber = 4;
+ inline const ::safe_browsing::ClientDownloadRequest_ExtendedAttr& xattr(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_ExtendedAttr* mutable_xattr(int index);
+ inline ::safe_browsing::ClientDownloadRequest_ExtendedAttr* add_xattr();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ExtendedAttr >&
+ xattr() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ExtendedAttr >*
+ mutable_xattr();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.SignatureInfo)
+ private:
+ inline void set_has_trusted();
+ inline void clear_has_trusted();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain > certificate_chain_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> signed_data_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ExtendedAttr > xattr_;
+ bool trusted_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_SignatureInfo* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_PEImageHeaders_DebugData : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_PEImageHeaders_DebugData();
+ virtual ~ClientDownloadRequest_PEImageHeaders_DebugData();
+
+ ClientDownloadRequest_PEImageHeaders_DebugData(const ClientDownloadRequest_PEImageHeaders_DebugData& from);
+
+ inline ClientDownloadRequest_PEImageHeaders_DebugData& operator=(const ClientDownloadRequest_PEImageHeaders_DebugData& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_PEImageHeaders_DebugData& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_PEImageHeaders_DebugData* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_PEImageHeaders_DebugData* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_PEImageHeaders_DebugData* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_PEImageHeaders_DebugData& from);
+ void MergeFrom(const ClientDownloadRequest_PEImageHeaders_DebugData& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes directory_entry = 1;
+ inline bool has_directory_entry() const;
+ inline void clear_directory_entry();
+ static const int kDirectoryEntryFieldNumber = 1;
+ inline const ::std::string& directory_entry() const;
+ inline void set_directory_entry(const ::std::string& value);
+ inline void set_directory_entry(const char* value);
+ inline void set_directory_entry(const void* value, size_t size);
+ inline ::std::string* mutable_directory_entry();
+ inline ::std::string* release_directory_entry();
+ inline void set_allocated_directory_entry(::std::string* directory_entry);
+
+ // optional bytes raw_data = 2;
+ inline bool has_raw_data() const;
+ inline void clear_raw_data();
+ static const int kRawDataFieldNumber = 2;
+ inline const ::std::string& raw_data() const;
+ inline void set_raw_data(const ::std::string& value);
+ inline void set_raw_data(const char* value);
+ inline void set_raw_data(const void* value, size_t size);
+ inline ::std::string* mutable_raw_data();
+ inline ::std::string* release_raw_data();
+ inline void set_allocated_raw_data(::std::string* raw_data);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData)
+ private:
+ inline void set_has_directory_entry();
+ inline void clear_has_directory_entry();
+ inline void set_has_raw_data();
+ inline void clear_has_raw_data();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* directory_entry_;
+ ::std::string* raw_data_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_PEImageHeaders_DebugData* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_PEImageHeaders : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_PEImageHeaders();
+ virtual ~ClientDownloadRequest_PEImageHeaders();
+
+ ClientDownloadRequest_PEImageHeaders(const ClientDownloadRequest_PEImageHeaders& from);
+
+ inline ClientDownloadRequest_PEImageHeaders& operator=(const ClientDownloadRequest_PEImageHeaders& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_PEImageHeaders& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_PEImageHeaders* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_PEImageHeaders* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_PEImageHeaders* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_PEImageHeaders& from);
+ void MergeFrom(const ClientDownloadRequest_PEImageHeaders& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadRequest_PEImageHeaders_DebugData DebugData;
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes dos_header = 1;
+ inline bool has_dos_header() const;
+ inline void clear_dos_header();
+ static const int kDosHeaderFieldNumber = 1;
+ inline const ::std::string& dos_header() const;
+ inline void set_dos_header(const ::std::string& value);
+ inline void set_dos_header(const char* value);
+ inline void set_dos_header(const void* value, size_t size);
+ inline ::std::string* mutable_dos_header();
+ inline ::std::string* release_dos_header();
+ inline void set_allocated_dos_header(::std::string* dos_header);
+
+ // optional bytes file_header = 2;
+ inline bool has_file_header() const;
+ inline void clear_file_header();
+ static const int kFileHeaderFieldNumber = 2;
+ inline const ::std::string& file_header() const;
+ inline void set_file_header(const ::std::string& value);
+ inline void set_file_header(const char* value);
+ inline void set_file_header(const void* value, size_t size);
+ inline ::std::string* mutable_file_header();
+ inline ::std::string* release_file_header();
+ inline void set_allocated_file_header(::std::string* file_header);
+
+ // optional bytes optional_headers32 = 3;
+ inline bool has_optional_headers32() const;
+ inline void clear_optional_headers32();
+ static const int kOptionalHeaders32FieldNumber = 3;
+ inline const ::std::string& optional_headers32() const;
+ inline void set_optional_headers32(const ::std::string& value);
+ inline void set_optional_headers32(const char* value);
+ inline void set_optional_headers32(const void* value, size_t size);
+ inline ::std::string* mutable_optional_headers32();
+ inline ::std::string* release_optional_headers32();
+ inline void set_allocated_optional_headers32(::std::string* optional_headers32);
+
+ // optional bytes optional_headers64 = 4;
+ inline bool has_optional_headers64() const;
+ inline void clear_optional_headers64();
+ static const int kOptionalHeaders64FieldNumber = 4;
+ inline const ::std::string& optional_headers64() const;
+ inline void set_optional_headers64(const ::std::string& value);
+ inline void set_optional_headers64(const char* value);
+ inline void set_optional_headers64(const void* value, size_t size);
+ inline ::std::string* mutable_optional_headers64();
+ inline ::std::string* release_optional_headers64();
+ inline void set_allocated_optional_headers64(::std::string* optional_headers64);
+
+ // repeated bytes section_header = 5;
+ inline int section_header_size() const;
+ inline void clear_section_header();
+ static const int kSectionHeaderFieldNumber = 5;
+ inline const ::std::string& section_header(int index) const;
+ inline ::std::string* mutable_section_header(int index);
+ inline void set_section_header(int index, const ::std::string& value);
+ inline void set_section_header(int index, const char* value);
+ inline void set_section_header(int index, const void* value, size_t size);
+ inline ::std::string* add_section_header();
+ inline void add_section_header(const ::std::string& value);
+ inline void add_section_header(const char* value);
+ inline void add_section_header(const void* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& section_header() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_section_header();
+
+ // optional bytes export_section_data = 6;
+ inline bool has_export_section_data() const;
+ inline void clear_export_section_data();
+ static const int kExportSectionDataFieldNumber = 6;
+ inline const ::std::string& export_section_data() const;
+ inline void set_export_section_data(const ::std::string& value);
+ inline void set_export_section_data(const char* value);
+ inline void set_export_section_data(const void* value, size_t size);
+ inline ::std::string* mutable_export_section_data();
+ inline ::std::string* release_export_section_data();
+ inline void set_allocated_export_section_data(::std::string* export_section_data);
+
+ // repeated .safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData debug_data = 7;
+ inline int debug_data_size() const;
+ inline void clear_debug_data();
+ static const int kDebugDataFieldNumber = 7;
+ inline const ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData& debug_data(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData* mutable_debug_data(int index);
+ inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData* add_debug_data();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData >&
+ debug_data() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData >*
+ mutable_debug_data();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.PEImageHeaders)
+ private:
+ inline void set_has_dos_header();
+ inline void clear_has_dos_header();
+ inline void set_has_file_header();
+ inline void clear_has_file_header();
+ inline void set_has_optional_headers32();
+ inline void clear_has_optional_headers32();
+ inline void set_has_optional_headers64();
+ inline void clear_has_optional_headers64();
+ inline void set_has_export_section_data();
+ inline void clear_has_export_section_data();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* dos_header_;
+ ::std::string* file_header_;
+ ::std::string* optional_headers32_;
+ ::std::string* optional_headers64_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> section_header_;
+ ::std::string* export_section_data_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData > debug_data_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_PEImageHeaders* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_MachOHeaders_LoadCommand : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_MachOHeaders_LoadCommand();
+ virtual ~ClientDownloadRequest_MachOHeaders_LoadCommand();
+
+ ClientDownloadRequest_MachOHeaders_LoadCommand(const ClientDownloadRequest_MachOHeaders_LoadCommand& from);
+
+ inline ClientDownloadRequest_MachOHeaders_LoadCommand& operator=(const ClientDownloadRequest_MachOHeaders_LoadCommand& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_MachOHeaders_LoadCommand& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_MachOHeaders_LoadCommand* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_MachOHeaders_LoadCommand* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_MachOHeaders_LoadCommand* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_MachOHeaders_LoadCommand& from);
+ void MergeFrom(const ClientDownloadRequest_MachOHeaders_LoadCommand& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required uint32 command_id = 1;
+ inline bool has_command_id() const;
+ inline void clear_command_id();
+ static const int kCommandIdFieldNumber = 1;
+ inline ::google::protobuf::uint32 command_id() const;
+ inline void set_command_id(::google::protobuf::uint32 value);
+
+ // required bytes command = 2;
+ inline bool has_command() const;
+ inline void clear_command();
+ static const int kCommandFieldNumber = 2;
+ inline const ::std::string& command() const;
+ inline void set_command(const ::std::string& value);
+ inline void set_command(const char* value);
+ inline void set_command(const void* value, size_t size);
+ inline ::std::string* mutable_command();
+ inline ::std::string* release_command();
+ inline void set_allocated_command(::std::string* command);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand)
+ private:
+ inline void set_has_command_id();
+ inline void clear_has_command_id();
+ inline void set_has_command();
+ inline void clear_has_command();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* command_;
+ ::google::protobuf::uint32 command_id_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_MachOHeaders_LoadCommand* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_MachOHeaders : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_MachOHeaders();
+ virtual ~ClientDownloadRequest_MachOHeaders();
+
+ ClientDownloadRequest_MachOHeaders(const ClientDownloadRequest_MachOHeaders& from);
+
+ inline ClientDownloadRequest_MachOHeaders& operator=(const ClientDownloadRequest_MachOHeaders& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_MachOHeaders& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_MachOHeaders* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_MachOHeaders* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_MachOHeaders* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_MachOHeaders& from);
+ void MergeFrom(const ClientDownloadRequest_MachOHeaders& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadRequest_MachOHeaders_LoadCommand LoadCommand;
+
+ // accessors -------------------------------------------------------
+
+ // required bytes mach_header = 1;
+ inline bool has_mach_header() const;
+ inline void clear_mach_header();
+ static const int kMachHeaderFieldNumber = 1;
+ inline const ::std::string& mach_header() const;
+ inline void set_mach_header(const ::std::string& value);
+ inline void set_mach_header(const char* value);
+ inline void set_mach_header(const void* value, size_t size);
+ inline ::std::string* mutable_mach_header();
+ inline ::std::string* release_mach_header();
+ inline void set_allocated_mach_header(::std::string* mach_header);
+
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand load_commands = 2;
+ inline int load_commands_size() const;
+ inline void clear_load_commands();
+ static const int kLoadCommandsFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand& load_commands(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand* mutable_load_commands(int index);
+ inline ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand* add_load_commands();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand >&
+ load_commands() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand >*
+ mutable_load_commands();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.MachOHeaders)
+ private:
+ inline void set_has_mach_header();
+ inline void clear_has_mach_header();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* mach_header_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand > load_commands_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_MachOHeaders* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_ImageHeaders : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_ImageHeaders();
+ virtual ~ClientDownloadRequest_ImageHeaders();
+
+ ClientDownloadRequest_ImageHeaders(const ClientDownloadRequest_ImageHeaders& from);
+
+ inline ClientDownloadRequest_ImageHeaders& operator=(const ClientDownloadRequest_ImageHeaders& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_ImageHeaders& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_ImageHeaders* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_ImageHeaders* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_ImageHeaders* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_ImageHeaders& from);
+ void MergeFrom(const ClientDownloadRequest_ImageHeaders& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientDownloadRequest.PEImageHeaders pe_headers = 1;
+ inline bool has_pe_headers() const;
+ inline void clear_pe_headers();
+ static const int kPeHeadersFieldNumber = 1;
+ inline const ::safe_browsing::ClientDownloadRequest_PEImageHeaders& pe_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders* mutable_pe_headers();
+ inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders* release_pe_headers();
+ inline void set_allocated_pe_headers(::safe_browsing::ClientDownloadRequest_PEImageHeaders* pe_headers);
+
+ // repeated .safe_browsing.ClientDownloadRequest.MachOHeaders mach_o_headers = 2;
+ inline int mach_o_headers_size() const;
+ inline void clear_mach_o_headers();
+ static const int kMachOHeadersFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_MachOHeaders& mach_o_headers(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_MachOHeaders* mutable_mach_o_headers(int index);
+ inline ::safe_browsing::ClientDownloadRequest_MachOHeaders* add_mach_o_headers();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders >&
+ mach_o_headers() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders >*
+ mutable_mach_o_headers();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.ImageHeaders)
+ private:
+ inline void set_has_pe_headers();
+ inline void clear_has_pe_headers();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientDownloadRequest_PEImageHeaders* pe_headers_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders > mach_o_headers_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_ImageHeaders* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_ArchivedBinary : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_ArchivedBinary();
+ virtual ~ClientDownloadRequest_ArchivedBinary();
+
+ ClientDownloadRequest_ArchivedBinary(const ClientDownloadRequest_ArchivedBinary& from);
+
+ inline ClientDownloadRequest_ArchivedBinary& operator=(const ClientDownloadRequest_ArchivedBinary& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_ArchivedBinary& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_ArchivedBinary* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_ArchivedBinary* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_ArchivedBinary* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_ArchivedBinary& from);
+ void MergeFrom(const ClientDownloadRequest_ArchivedBinary& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string file_basename = 1;
+ inline bool has_file_basename() const;
+ inline void clear_file_basename();
+ static const int kFileBasenameFieldNumber = 1;
+ inline const ::std::string& file_basename() const;
+ inline void set_file_basename(const ::std::string& value);
+ inline void set_file_basename(const char* value);
+ inline void set_file_basename(const char* value, size_t size);
+ inline ::std::string* mutable_file_basename();
+ inline ::std::string* release_file_basename();
+ inline void set_allocated_file_basename(::std::string* file_basename);
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 2;
+ inline bool has_download_type() const;
+ inline void clear_download_type();
+ static const int kDownloadTypeFieldNumber = 2;
+ inline ::safe_browsing::ClientDownloadRequest_DownloadType download_type() const;
+ inline void set_download_type(::safe_browsing::ClientDownloadRequest_DownloadType value);
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digests = 3;
+ inline bool has_digests() const;
+ inline void clear_digests();
+ static const int kDigestsFieldNumber = 3;
+ inline const ::safe_browsing::ClientDownloadRequest_Digests& digests() const;
+ inline ::safe_browsing::ClientDownloadRequest_Digests* mutable_digests();
+ inline ::safe_browsing::ClientDownloadRequest_Digests* release_digests();
+ inline void set_allocated_digests(::safe_browsing::ClientDownloadRequest_Digests* digests);
+
+ // optional int64 length = 4;
+ inline bool has_length() const;
+ inline void clear_length();
+ static const int kLengthFieldNumber = 4;
+ inline ::google::protobuf::int64 length() const;
+ inline void set_length(::google::protobuf::int64 value);
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ inline bool has_signature() const;
+ inline void clear_signature();
+ static const int kSignatureFieldNumber = 5;
+ inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& signature() const;
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* mutable_signature();
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* release_signature();
+ inline void set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature);
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 6;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.ArchivedBinary)
+ private:
+ inline void set_has_file_basename();
+ inline void clear_has_file_basename();
+ inline void set_has_download_type();
+ inline void clear_has_download_type();
+ inline void set_has_digests();
+ inline void clear_has_digests();
+ inline void set_has_length();
+ inline void clear_has_length();
+ inline void set_has_signature();
+ inline void clear_has_signature();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* file_basename_;
+ ::safe_browsing::ClientDownloadRequest_Digests* digests_;
+ ::google::protobuf::int64 length_;
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* signature_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ int download_type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_ArchivedBinary* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest_URLChainEntry : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest_URLChainEntry();
+ virtual ~ClientDownloadRequest_URLChainEntry();
+
+ ClientDownloadRequest_URLChainEntry(const ClientDownloadRequest_URLChainEntry& from);
+
+ inline ClientDownloadRequest_URLChainEntry& operator=(const ClientDownloadRequest_URLChainEntry& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest_URLChainEntry& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest_URLChainEntry* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest_URLChainEntry* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest_URLChainEntry* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest_URLChainEntry& from);
+ void MergeFrom(const ClientDownloadRequest_URLChainEntry& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadRequest_URLChainEntry_URLType URLType;
+ static const URLType DOWNLOAD_URL = ClientDownloadRequest_URLChainEntry_URLType_DOWNLOAD_URL;
+ static const URLType DOWNLOAD_REFERRER = ClientDownloadRequest_URLChainEntry_URLType_DOWNLOAD_REFERRER;
+ static const URLType LANDING_PAGE = ClientDownloadRequest_URLChainEntry_URLType_LANDING_PAGE;
+ static const URLType LANDING_REFERRER = ClientDownloadRequest_URLChainEntry_URLType_LANDING_REFERRER;
+ static const URLType CLIENT_REDIRECT = ClientDownloadRequest_URLChainEntry_URLType_CLIENT_REDIRECT;
+ static const URLType SERVER_REDIRECT = ClientDownloadRequest_URLChainEntry_URLType_SERVER_REDIRECT;
+ static inline bool URLType_IsValid(int value) {
+ return ClientDownloadRequest_URLChainEntry_URLType_IsValid(value);
+ }
+ static const URLType URLType_MIN =
+ ClientDownloadRequest_URLChainEntry_URLType_URLType_MIN;
+ static const URLType URLType_MAX =
+ ClientDownloadRequest_URLChainEntry_URLType_URLType_MAX;
+ static const int URLType_ARRAYSIZE =
+ ClientDownloadRequest_URLChainEntry_URLType_URLType_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional .safe_browsing.ClientDownloadRequest.URLChainEntry.URLType type = 2;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 2;
+ inline ::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType type() const;
+ inline void set_type(::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType value);
+
+ // optional string ip_address = 3;
+ inline bool has_ip_address() const;
+ inline void clear_ip_address();
+ static const int kIpAddressFieldNumber = 3;
+ inline const ::std::string& ip_address() const;
+ inline void set_ip_address(const ::std::string& value);
+ inline void set_ip_address(const char* value);
+ inline void set_ip_address(const char* value, size_t size);
+ inline ::std::string* mutable_ip_address();
+ inline ::std::string* release_ip_address();
+ inline void set_allocated_ip_address(::std::string* ip_address);
+
+ // optional string referrer = 4;
+ inline bool has_referrer() const;
+ inline void clear_referrer();
+ static const int kReferrerFieldNumber = 4;
+ inline const ::std::string& referrer() const;
+ inline void set_referrer(const ::std::string& value);
+ inline void set_referrer(const char* value);
+ inline void set_referrer(const char* value, size_t size);
+ inline ::std::string* mutable_referrer();
+ inline ::std::string* release_referrer();
+ inline void set_allocated_referrer(::std::string* referrer);
+
+ // optional string main_frame_referrer = 5;
+ inline bool has_main_frame_referrer() const;
+ inline void clear_main_frame_referrer();
+ static const int kMainFrameReferrerFieldNumber = 5;
+ inline const ::std::string& main_frame_referrer() const;
+ inline void set_main_frame_referrer(const ::std::string& value);
+ inline void set_main_frame_referrer(const char* value);
+ inline void set_main_frame_referrer(const char* value, size_t size);
+ inline ::std::string* mutable_main_frame_referrer();
+ inline ::std::string* release_main_frame_referrer();
+ inline void set_allocated_main_frame_referrer(::std::string* main_frame_referrer);
+
+ // optional bool is_retargeting = 6;
+ inline bool has_is_retargeting() const;
+ inline void clear_is_retargeting();
+ static const int kIsRetargetingFieldNumber = 6;
+ inline bool is_retargeting() const;
+ inline void set_is_retargeting(bool value);
+
+ // optional bool is_user_initiated = 7;
+ inline bool has_is_user_initiated() const;
+ inline void clear_is_user_initiated();
+ static const int kIsUserInitiatedFieldNumber = 7;
+ inline bool is_user_initiated() const;
+ inline void set_is_user_initiated(bool value);
+
+ // optional double timestamp_in_millisec = 8;
+ inline bool has_timestamp_in_millisec() const;
+ inline void clear_timestamp_in_millisec();
+ static const int kTimestampInMillisecFieldNumber = 8;
+ inline double timestamp_in_millisec() const;
+ inline void set_timestamp_in_millisec(double value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest.URLChainEntry)
+ private:
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_ip_address();
+ inline void clear_has_ip_address();
+ inline void set_has_referrer();
+ inline void clear_has_referrer();
+ inline void set_has_main_frame_referrer();
+ inline void clear_has_main_frame_referrer();
+ inline void set_has_is_retargeting();
+ inline void clear_has_is_retargeting();
+ inline void set_has_is_user_initiated();
+ inline void clear_has_is_user_initiated();
+ inline void set_has_timestamp_in_millisec();
+ inline void clear_has_timestamp_in_millisec();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::std::string* ip_address_;
+ ::std::string* referrer_;
+ ::std::string* main_frame_referrer_;
+ int type_;
+ bool is_retargeting_;
+ bool is_user_initiated_;
+ double timestamp_in_millisec_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest_URLChainEntry* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadRequest : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadRequest();
+ virtual ~ClientDownloadRequest();
+
+ ClientDownloadRequest(const ClientDownloadRequest& from);
+
+ inline ClientDownloadRequest& operator=(const ClientDownloadRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadRequest& from);
+ void MergeFrom(const ClientDownloadRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadRequest_Digests Digests;
+ typedef ClientDownloadRequest_Resource Resource;
+ typedef ClientDownloadRequest_CertificateChain CertificateChain;
+ typedef ClientDownloadRequest_ExtendedAttr ExtendedAttr;
+ typedef ClientDownloadRequest_SignatureInfo SignatureInfo;
+ typedef ClientDownloadRequest_PEImageHeaders PEImageHeaders;
+ typedef ClientDownloadRequest_MachOHeaders MachOHeaders;
+ typedef ClientDownloadRequest_ImageHeaders ImageHeaders;
+ typedef ClientDownloadRequest_ArchivedBinary ArchivedBinary;
+ typedef ClientDownloadRequest_URLChainEntry URLChainEntry;
+
+ typedef ClientDownloadRequest_ResourceType ResourceType;
+ static const ResourceType DOWNLOAD_URL = ClientDownloadRequest_ResourceType_DOWNLOAD_URL;
+ static const ResourceType DOWNLOAD_REDIRECT = ClientDownloadRequest_ResourceType_DOWNLOAD_REDIRECT;
+ static const ResourceType TAB_URL = ClientDownloadRequest_ResourceType_TAB_URL;
+ static const ResourceType TAB_REDIRECT = ClientDownloadRequest_ResourceType_TAB_REDIRECT;
+ static const ResourceType PPAPI_DOCUMENT = ClientDownloadRequest_ResourceType_PPAPI_DOCUMENT;
+ static const ResourceType PPAPI_PLUGIN = ClientDownloadRequest_ResourceType_PPAPI_PLUGIN;
+ static inline bool ResourceType_IsValid(int value) {
+ return ClientDownloadRequest_ResourceType_IsValid(value);
+ }
+ static const ResourceType ResourceType_MIN =
+ ClientDownloadRequest_ResourceType_ResourceType_MIN;
+ static const ResourceType ResourceType_MAX =
+ ClientDownloadRequest_ResourceType_ResourceType_MAX;
+ static const int ResourceType_ARRAYSIZE =
+ ClientDownloadRequest_ResourceType_ResourceType_ARRAYSIZE;
+
+ typedef ClientDownloadRequest_DownloadType DownloadType;
+ static const DownloadType WIN_EXECUTABLE = ClientDownloadRequest_DownloadType_WIN_EXECUTABLE;
+ static const DownloadType CHROME_EXTENSION = ClientDownloadRequest_DownloadType_CHROME_EXTENSION;
+ static const DownloadType ANDROID_APK = ClientDownloadRequest_DownloadType_ANDROID_APK;
+ static const DownloadType ZIPPED_EXECUTABLE = ClientDownloadRequest_DownloadType_ZIPPED_EXECUTABLE;
+ static const DownloadType MAC_EXECUTABLE = ClientDownloadRequest_DownloadType_MAC_EXECUTABLE;
+ static const DownloadType ZIPPED_ARCHIVE = ClientDownloadRequest_DownloadType_ZIPPED_ARCHIVE;
+ static const DownloadType ARCHIVE = ClientDownloadRequest_DownloadType_ARCHIVE;
+ static const DownloadType INVALID_ZIP = ClientDownloadRequest_DownloadType_INVALID_ZIP;
+ static const DownloadType INVALID_MAC_ARCHIVE = ClientDownloadRequest_DownloadType_INVALID_MAC_ARCHIVE;
+ static const DownloadType PPAPI_SAVE_REQUEST = ClientDownloadRequest_DownloadType_PPAPI_SAVE_REQUEST;
+ static const DownloadType SAMPLED_UNSUPPORTED_FILE = ClientDownloadRequest_DownloadType_SAMPLED_UNSUPPORTED_FILE;
+ static inline bool DownloadType_IsValid(int value) {
+ return ClientDownloadRequest_DownloadType_IsValid(value);
+ }
+ static const DownloadType DownloadType_MIN =
+ ClientDownloadRequest_DownloadType_DownloadType_MIN;
+ static const DownloadType DownloadType_MAX =
+ ClientDownloadRequest_DownloadType_DownloadType_MAX;
+ static const int DownloadType_ARRAYSIZE =
+ ClientDownloadRequest_DownloadType_DownloadType_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // required string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // required .safe_browsing.ClientDownloadRequest.Digests digests = 2;
+ inline bool has_digests() const;
+ inline void clear_digests();
+ static const int kDigestsFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_Digests& digests() const;
+ inline ::safe_browsing::ClientDownloadRequest_Digests* mutable_digests();
+ inline ::safe_browsing::ClientDownloadRequest_Digests* release_digests();
+ inline void set_allocated_digests(::safe_browsing::ClientDownloadRequest_Digests* digests);
+
+ // required int64 length = 3;
+ inline bool has_length() const;
+ inline void clear_length();
+ static const int kLengthFieldNumber = 3;
+ inline ::google::protobuf::int64 length() const;
+ inline void set_length(::google::protobuf::int64 value);
+
+ // repeated .safe_browsing.ClientDownloadRequest.Resource resources = 4;
+ inline int resources_size() const;
+ inline void clear_resources();
+ static const int kResourcesFieldNumber = 4;
+ inline const ::safe_browsing::ClientDownloadRequest_Resource& resources(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_Resource* mutable_resources(int index);
+ inline ::safe_browsing::ClientDownloadRequest_Resource* add_resources();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_Resource >&
+ resources() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_Resource >*
+ mutable_resources();
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ inline bool has_signature() const;
+ inline void clear_signature();
+ static const int kSignatureFieldNumber = 5;
+ inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& signature() const;
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* mutable_signature();
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* release_signature();
+ inline void set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature);
+
+ // optional bool user_initiated = 6;
+ inline bool has_user_initiated() const;
+ inline void clear_user_initiated();
+ static const int kUserInitiatedFieldNumber = 6;
+ inline bool user_initiated() const;
+ inline void set_user_initiated(bool value);
+
+ // optional string file_basename = 9;
+ inline bool has_file_basename() const;
+ inline void clear_file_basename();
+ static const int kFileBasenameFieldNumber = 9;
+ inline const ::std::string& file_basename() const;
+ inline void set_file_basename(const ::std::string& value);
+ inline void set_file_basename(const char* value);
+ inline void set_file_basename(const char* value, size_t size);
+ inline ::std::string* mutable_file_basename();
+ inline ::std::string* release_file_basename();
+ inline void set_allocated_file_basename(::std::string* file_basename);
+
+ // optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 10 [default = WIN_EXECUTABLE];
+ inline bool has_download_type() const;
+ inline void clear_download_type();
+ static const int kDownloadTypeFieldNumber = 10;
+ inline ::safe_browsing::ClientDownloadRequest_DownloadType download_type() const;
+ inline void set_download_type(::safe_browsing::ClientDownloadRequest_DownloadType value);
+
+ // optional string locale = 11;
+ inline bool has_locale() const;
+ inline void clear_locale();
+ static const int kLocaleFieldNumber = 11;
+ inline const ::std::string& locale() const;
+ inline void set_locale(const ::std::string& value);
+ inline void set_locale(const char* value);
+ inline void set_locale(const char* value, size_t size);
+ inline ::std::string* mutable_locale();
+ inline ::std::string* release_locale();
+ inline void set_allocated_locale(::std::string* locale);
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 18;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 18;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // repeated .safe_browsing.ClientDownloadRequest.ArchivedBinary archived_binary = 22;
+ inline int archived_binary_size() const;
+ inline void clear_archived_binary();
+ static const int kArchivedBinaryFieldNumber = 22;
+ inline const ::safe_browsing::ClientDownloadRequest_ArchivedBinary& archived_binary(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_ArchivedBinary* mutable_archived_binary(int index);
+ inline ::safe_browsing::ClientDownloadRequest_ArchivedBinary* add_archived_binary();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ArchivedBinary >&
+ archived_binary() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ArchivedBinary >*
+ mutable_archived_binary();
+
+ // optional .safe_browsing.ChromeUserPopulation population = 24;
+ inline bool has_population() const;
+ inline void clear_population();
+ static const int kPopulationFieldNumber = 24;
+ inline const ::safe_browsing::ChromeUserPopulation& population() const;
+ inline ::safe_browsing::ChromeUserPopulation* mutable_population();
+ inline ::safe_browsing::ChromeUserPopulation* release_population();
+ inline void set_allocated_population(::safe_browsing::ChromeUserPopulation* population);
+
+ // optional bool archive_valid = 26;
+ inline bool has_archive_valid() const;
+ inline void clear_archive_valid();
+ static const int kArchiveValidFieldNumber = 26;
+ inline bool archive_valid() const;
+ inline void set_archive_valid(bool value);
+
+ // optional bool skipped_url_whitelist = 28;
+ inline bool has_skipped_url_whitelist() const;
+ inline void clear_skipped_url_whitelist();
+ static const int kSkippedUrlWhitelistFieldNumber = 28;
+ inline bool skipped_url_whitelist() const;
+ inline void set_skipped_url_whitelist(bool value);
+
+ // optional bool skipped_certificate_whitelist = 31;
+ inline bool has_skipped_certificate_whitelist() const;
+ inline void clear_skipped_certificate_whitelist();
+ static const int kSkippedCertificateWhitelistFieldNumber = 31;
+ inline bool skipped_certificate_whitelist() const;
+ inline void set_skipped_certificate_whitelist(bool value);
+
+ // repeated string alternate_extensions = 35;
+ inline int alternate_extensions_size() const;
+ inline void clear_alternate_extensions();
+ static const int kAlternateExtensionsFieldNumber = 35;
+ inline const ::std::string& alternate_extensions(int index) const;
+ inline ::std::string* mutable_alternate_extensions(int index);
+ inline void set_alternate_extensions(int index, const ::std::string& value);
+ inline void set_alternate_extensions(int index, const char* value);
+ inline void set_alternate_extensions(int index, const char* value, size_t size);
+ inline ::std::string* add_alternate_extensions();
+ inline void add_alternate_extensions(const ::std::string& value);
+ inline void add_alternate_extensions(const char* value);
+ inline void add_alternate_extensions(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& alternate_extensions() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_alternate_extensions();
+
+ // repeated .safe_browsing.ClientDownloadRequest.URLChainEntry url_chain = 36;
+ inline int url_chain_size() const;
+ inline void clear_url_chain();
+ static const int kUrlChainFieldNumber = 36;
+ inline const ::safe_browsing::ClientDownloadRequest_URLChainEntry& url_chain(int index) const;
+ inline ::safe_browsing::ClientDownloadRequest_URLChainEntry* mutable_url_chain(int index);
+ inline ::safe_browsing::ClientDownloadRequest_URLChainEntry* add_url_chain();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_URLChainEntry >&
+ url_chain() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_URLChainEntry >*
+ mutable_url_chain();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadRequest)
+ private:
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_digests();
+ inline void clear_has_digests();
+ inline void set_has_length();
+ inline void clear_has_length();
+ inline void set_has_signature();
+ inline void clear_has_signature();
+ inline void set_has_user_initiated();
+ inline void clear_has_user_initiated();
+ inline void set_has_file_basename();
+ inline void clear_has_file_basename();
+ inline void set_has_download_type();
+ inline void clear_has_download_type();
+ inline void set_has_locale();
+ inline void clear_has_locale();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+ inline void set_has_population();
+ inline void clear_has_population();
+ inline void set_has_archive_valid();
+ inline void clear_has_archive_valid();
+ inline void set_has_skipped_url_whitelist();
+ inline void clear_has_skipped_url_whitelist();
+ inline void set_has_skipped_certificate_whitelist();
+ inline void clear_has_skipped_certificate_whitelist();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::safe_browsing::ClientDownloadRequest_Digests* digests_;
+ ::google::protobuf::int64 length_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_Resource > resources_;
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* signature_;
+ ::std::string* file_basename_;
+ ::std::string* locale_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ArchivedBinary > archived_binary_;
+ int download_type_;
+ bool user_initiated_;
+ bool archive_valid_;
+ bool skipped_url_whitelist_;
+ bool skipped_certificate_whitelist_;
+ ::safe_browsing::ChromeUserPopulation* population_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> alternate_extensions_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_URLChainEntry > url_chain_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadResponse_MoreInfo : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadResponse_MoreInfo();
+ virtual ~ClientDownloadResponse_MoreInfo();
+
+ ClientDownloadResponse_MoreInfo(const ClientDownloadResponse_MoreInfo& from);
+
+ inline ClientDownloadResponse_MoreInfo& operator=(const ClientDownloadResponse_MoreInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadResponse_MoreInfo& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadResponse_MoreInfo* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadResponse_MoreInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadResponse_MoreInfo* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadResponse_MoreInfo& from);
+ void MergeFrom(const ClientDownloadResponse_MoreInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string description = 1;
+ inline bool has_description() const;
+ inline void clear_description();
+ static const int kDescriptionFieldNumber = 1;
+ inline const ::std::string& description() const;
+ inline void set_description(const ::std::string& value);
+ inline void set_description(const char* value);
+ inline void set_description(const char* value, size_t size);
+ inline ::std::string* mutable_description();
+ inline ::std::string* release_description();
+ inline void set_allocated_description(::std::string* description);
+
+ // optional string url = 2;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 2;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadResponse.MoreInfo)
+ private:
+ inline void set_has_description();
+ inline void clear_has_description();
+ inline void set_has_url();
+ inline void clear_has_url();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* description_;
+ ::std::string* url_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadResponse_MoreInfo* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadResponse : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadResponse();
+ virtual ~ClientDownloadResponse();
+
+ ClientDownloadResponse(const ClientDownloadResponse& from);
+
+ inline ClientDownloadResponse& operator=(const ClientDownloadResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadResponse& from);
+ void MergeFrom(const ClientDownloadResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadResponse_MoreInfo MoreInfo;
+
+ typedef ClientDownloadResponse_Verdict Verdict;
+ static const Verdict SAFE = ClientDownloadResponse_Verdict_SAFE;
+ static const Verdict DANGEROUS = ClientDownloadResponse_Verdict_DANGEROUS;
+ static const Verdict UNCOMMON = ClientDownloadResponse_Verdict_UNCOMMON;
+ static const Verdict POTENTIALLY_UNWANTED = ClientDownloadResponse_Verdict_POTENTIALLY_UNWANTED;
+ static const Verdict DANGEROUS_HOST = ClientDownloadResponse_Verdict_DANGEROUS_HOST;
+ static const Verdict UNKNOWN = ClientDownloadResponse_Verdict_UNKNOWN;
+ static inline bool Verdict_IsValid(int value) {
+ return ClientDownloadResponse_Verdict_IsValid(value);
+ }
+ static const Verdict Verdict_MIN =
+ ClientDownloadResponse_Verdict_Verdict_MIN;
+ static const Verdict Verdict_MAX =
+ ClientDownloadResponse_Verdict_Verdict_MAX;
+ static const int Verdict_ARRAYSIZE =
+ ClientDownloadResponse_Verdict_Verdict_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientDownloadResponse.Verdict verdict = 1 [default = SAFE];
+ inline bool has_verdict() const;
+ inline void clear_verdict();
+ static const int kVerdictFieldNumber = 1;
+ inline ::safe_browsing::ClientDownloadResponse_Verdict verdict() const;
+ inline void set_verdict(::safe_browsing::ClientDownloadResponse_Verdict value);
+
+ // optional .safe_browsing.ClientDownloadResponse.MoreInfo more_info = 2;
+ inline bool has_more_info() const;
+ inline void clear_more_info();
+ static const int kMoreInfoFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadResponse_MoreInfo& more_info() const;
+ inline ::safe_browsing::ClientDownloadResponse_MoreInfo* mutable_more_info();
+ inline ::safe_browsing::ClientDownloadResponse_MoreInfo* release_more_info();
+ inline void set_allocated_more_info(::safe_browsing::ClientDownloadResponse_MoreInfo* more_info);
+
+ // optional bytes token = 3;
+ inline bool has_token() const;
+ inline void clear_token();
+ static const int kTokenFieldNumber = 3;
+ inline const ::std::string& token() const;
+ inline void set_token(const ::std::string& value);
+ inline void set_token(const char* value);
+ inline void set_token(const void* value, size_t size);
+ inline ::std::string* mutable_token();
+ inline ::std::string* release_token();
+ inline void set_allocated_token(::std::string* token);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadResponse)
+ private:
+ inline void set_has_verdict();
+ inline void clear_has_verdict();
+ inline void set_has_more_info();
+ inline void clear_has_more_info();
+ inline void set_has_token();
+ inline void clear_has_token();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientDownloadResponse_MoreInfo* more_info_;
+ ::std::string* token_;
+ int verdict_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadReport_UserInformation : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadReport_UserInformation();
+ virtual ~ClientDownloadReport_UserInformation();
+
+ ClientDownloadReport_UserInformation(const ClientDownloadReport_UserInformation& from);
+
+ inline ClientDownloadReport_UserInformation& operator=(const ClientDownloadReport_UserInformation& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadReport_UserInformation& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadReport_UserInformation* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadReport_UserInformation* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadReport_UserInformation* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadReport_UserInformation& from);
+ void MergeFrom(const ClientDownloadReport_UserInformation& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string email = 1;
+ inline bool has_email() const;
+ inline void clear_email();
+ static const int kEmailFieldNumber = 1;
+ inline const ::std::string& email() const;
+ inline void set_email(const ::std::string& value);
+ inline void set_email(const char* value);
+ inline void set_email(const char* value, size_t size);
+ inline ::std::string* mutable_email();
+ inline ::std::string* release_email();
+ inline void set_allocated_email(::std::string* email);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadReport.UserInformation)
+ private:
+ inline void set_has_email();
+ inline void clear_has_email();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* email_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadReport_UserInformation* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientDownloadReport : public ::google::protobuf::MessageLite {
+ public:
+ ClientDownloadReport();
+ virtual ~ClientDownloadReport();
+
+ ClientDownloadReport(const ClientDownloadReport& from);
+
+ inline ClientDownloadReport& operator=(const ClientDownloadReport& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientDownloadReport& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientDownloadReport* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientDownloadReport* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientDownloadReport* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientDownloadReport& from);
+ void MergeFrom(const ClientDownloadReport& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientDownloadReport_UserInformation UserInformation;
+
+ typedef ClientDownloadReport_Reason Reason;
+ static const Reason SHARE = ClientDownloadReport_Reason_SHARE;
+ static const Reason FALSE_POSITIVE = ClientDownloadReport_Reason_FALSE_POSITIVE;
+ static const Reason APPEAL = ClientDownloadReport_Reason_APPEAL;
+ static inline bool Reason_IsValid(int value) {
+ return ClientDownloadReport_Reason_IsValid(value);
+ }
+ static const Reason Reason_MIN =
+ ClientDownloadReport_Reason_Reason_MIN;
+ static const Reason Reason_MAX =
+ ClientDownloadReport_Reason_Reason_MAX;
+ static const int Reason_ARRAYSIZE =
+ ClientDownloadReport_Reason_Reason_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientDownloadReport.Reason reason = 1;
+ inline bool has_reason() const;
+ inline void clear_reason();
+ static const int kReasonFieldNumber = 1;
+ inline ::safe_browsing::ClientDownloadReport_Reason reason() const;
+ inline void set_reason(::safe_browsing::ClientDownloadReport_Reason value);
+
+ // optional .safe_browsing.ClientDownloadRequest download_request = 2;
+ inline bool has_download_request() const;
+ inline void clear_download_request();
+ static const int kDownloadRequestFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest& download_request() const;
+ inline ::safe_browsing::ClientDownloadRequest* mutable_download_request();
+ inline ::safe_browsing::ClientDownloadRequest* release_download_request();
+ inline void set_allocated_download_request(::safe_browsing::ClientDownloadRequest* download_request);
+
+ // optional .safe_browsing.ClientDownloadReport.UserInformation user_information = 3;
+ inline bool has_user_information() const;
+ inline void clear_user_information();
+ static const int kUserInformationFieldNumber = 3;
+ inline const ::safe_browsing::ClientDownloadReport_UserInformation& user_information() const;
+ inline ::safe_browsing::ClientDownloadReport_UserInformation* mutable_user_information();
+ inline ::safe_browsing::ClientDownloadReport_UserInformation* release_user_information();
+ inline void set_allocated_user_information(::safe_browsing::ClientDownloadReport_UserInformation* user_information);
+
+ // optional bytes comment = 4;
+ inline bool has_comment() const;
+ inline void clear_comment();
+ static const int kCommentFieldNumber = 4;
+ inline const ::std::string& comment() const;
+ inline void set_comment(const ::std::string& value);
+ inline void set_comment(const char* value);
+ inline void set_comment(const void* value, size_t size);
+ inline ::std::string* mutable_comment();
+ inline ::std::string* release_comment();
+ inline void set_allocated_comment(::std::string* comment);
+
+ // optional .safe_browsing.ClientDownloadResponse download_response = 5;
+ inline bool has_download_response() const;
+ inline void clear_download_response();
+ static const int kDownloadResponseFieldNumber = 5;
+ inline const ::safe_browsing::ClientDownloadResponse& download_response() const;
+ inline ::safe_browsing::ClientDownloadResponse* mutable_download_response();
+ inline ::safe_browsing::ClientDownloadResponse* release_download_response();
+ inline void set_allocated_download_response(::safe_browsing::ClientDownloadResponse* download_response);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientDownloadReport)
+ private:
+ inline void set_has_reason();
+ inline void clear_has_reason();
+ inline void set_has_download_request();
+ inline void clear_has_download_request();
+ inline void set_has_user_information();
+ inline void clear_has_user_information();
+ inline void set_has_comment();
+ inline void clear_has_comment();
+ inline void set_has_download_response();
+ inline void clear_has_download_response();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientDownloadRequest* download_request_;
+ ::safe_browsing::ClientDownloadReport_UserInformation* user_information_;
+ ::std::string* comment_;
+ ::safe_browsing::ClientDownloadResponse* download_response_;
+ int reason_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientDownloadReport* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientUploadResponse : public ::google::protobuf::MessageLite {
+ public:
+ ClientUploadResponse();
+ virtual ~ClientUploadResponse();
+
+ ClientUploadResponse(const ClientUploadResponse& from);
+
+ inline ClientUploadResponse& operator=(const ClientUploadResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientUploadResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientUploadResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientUploadResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientUploadResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientUploadResponse& from);
+ void MergeFrom(const ClientUploadResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientUploadResponse_UploadStatus UploadStatus;
+ static const UploadStatus SUCCESS = ClientUploadResponse_UploadStatus_SUCCESS;
+ static const UploadStatus UPLOAD_FAILURE = ClientUploadResponse_UploadStatus_UPLOAD_FAILURE;
+ static inline bool UploadStatus_IsValid(int value) {
+ return ClientUploadResponse_UploadStatus_IsValid(value);
+ }
+ static const UploadStatus UploadStatus_MIN =
+ ClientUploadResponse_UploadStatus_UploadStatus_MIN;
+ static const UploadStatus UploadStatus_MAX =
+ ClientUploadResponse_UploadStatus_UploadStatus_MAX;
+ static const int UploadStatus_ARRAYSIZE =
+ ClientUploadResponse_UploadStatus_UploadStatus_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientUploadResponse.UploadStatus status = 1;
+ inline bool has_status() const;
+ inline void clear_status();
+ static const int kStatusFieldNumber = 1;
+ inline ::safe_browsing::ClientUploadResponse_UploadStatus status() const;
+ inline void set_status(::safe_browsing::ClientUploadResponse_UploadStatus value);
+
+ // optional string permalink = 2;
+ inline bool has_permalink() const;
+ inline void clear_permalink();
+ static const int kPermalinkFieldNumber = 2;
+ inline const ::std::string& permalink() const;
+ inline void set_permalink(const ::std::string& value);
+ inline void set_permalink(const char* value);
+ inline void set_permalink(const char* value, size_t size);
+ inline ::std::string* mutable_permalink();
+ inline ::std::string* release_permalink();
+ inline void set_allocated_permalink(::std::string* permalink);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientUploadResponse)
+ private:
+ inline void set_has_status();
+ inline void clear_has_status();
+ inline void set_has_permalink();
+ inline void clear_has_permalink();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* permalink_;
+ int status_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientUploadResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_TrackedPreferenceIncident : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident();
+ virtual ~ClientIncidentReport_IncidentData_TrackedPreferenceIncident();
+
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from);
+
+ inline ClientIncidentReport_IncidentData_TrackedPreferenceIncident& operator=(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_TrackedPreferenceIncident* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_TrackedPreferenceIncident* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_TrackedPreferenceIncident& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ValueState;
+ static const ValueState UNKNOWN = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_UNKNOWN;
+ static const ValueState CLEARED = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_CLEARED;
+ static const ValueState WEAK_LEGACY_OBSOLETE = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_WEAK_LEGACY_OBSOLETE;
+ static const ValueState CHANGED = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_CHANGED;
+ static const ValueState UNTRUSTED_UNKNOWN_VALUE = ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_UNTRUSTED_UNKNOWN_VALUE;
+ static inline bool ValueState_IsValid(int value) {
+ return ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_IsValid(value);
+ }
+ static const ValueState ValueState_MIN =
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_MIN;
+ static const ValueState ValueState_MAX =
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_MAX;
+ static const int ValueState_ARRAYSIZE =
+ ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_ValueState_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional string path = 1;
+ inline bool has_path() const;
+ inline void clear_path();
+ static const int kPathFieldNumber = 1;
+ inline const ::std::string& path() const;
+ inline void set_path(const ::std::string& value);
+ inline void set_path(const char* value);
+ inline void set_path(const char* value, size_t size);
+ inline ::std::string* mutable_path();
+ inline ::std::string* release_path();
+ inline void set_allocated_path(::std::string* path);
+
+ // optional string atomic_value = 2;
+ inline bool has_atomic_value() const;
+ inline void clear_atomic_value();
+ static const int kAtomicValueFieldNumber = 2;
+ inline const ::std::string& atomic_value() const;
+ inline void set_atomic_value(const ::std::string& value);
+ inline void set_atomic_value(const char* value);
+ inline void set_atomic_value(const char* value, size_t size);
+ inline ::std::string* mutable_atomic_value();
+ inline ::std::string* release_atomic_value();
+ inline void set_allocated_atomic_value(::std::string* atomic_value);
+
+ // repeated string split_key = 3;
+ inline int split_key_size() const;
+ inline void clear_split_key();
+ static const int kSplitKeyFieldNumber = 3;
+ inline const ::std::string& split_key(int index) const;
+ inline ::std::string* mutable_split_key(int index);
+ inline void set_split_key(int index, const ::std::string& value);
+ inline void set_split_key(int index, const char* value);
+ inline void set_split_key(int index, const char* value, size_t size);
+ inline ::std::string* add_split_key();
+ inline void add_split_key(const ::std::string& value);
+ inline void add_split_key(const char* value);
+ inline void add_split_key(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& split_key() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_split_key();
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.ValueState value_state = 4;
+ inline bool has_value_state() const;
+ inline void clear_value_state();
+ static const int kValueStateFieldNumber = 4;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState value_state() const;
+ inline void set_value_state(::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident)
+ private:
+ inline void set_has_path();
+ inline void clear_has_path();
+ inline void set_has_atomic_value();
+ inline void clear_has_atomic_value();
+ inline void set_has_value_state();
+ inline void clear_has_value_state();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* path_;
+ ::std::string* atomic_value_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> split_key_;
+ int value_state_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_TrackedPreferenceIncident* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile();
+ virtual ~ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile();
+
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from);
+
+ inline ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& operator=(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string relative_path = 1;
+ inline bool has_relative_path() const;
+ inline void clear_relative_path();
+ static const int kRelativePathFieldNumber = 1;
+ inline const ::std::string& relative_path() const;
+ inline void set_relative_path(const ::std::string& value);
+ inline void set_relative_path(const char* value);
+ inline void set_relative_path(const char* value, size_t size);
+ inline ::std::string* mutable_relative_path();
+ inline ::std::string* release_relative_path();
+ inline void set_allocated_relative_path(::std::string* relative_path);
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ inline bool has_signature() const;
+ inline void clear_signature();
+ static const int kSignatureFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& signature() const;
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* mutable_signature();
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* release_signature();
+ inline void set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature);
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 3;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile)
+ private:
+ inline void set_has_relative_path();
+ inline void clear_has_relative_path();
+ inline void set_has_signature();
+ inline void clear_has_signature();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* relative_path_;
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* signature_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_BinaryIntegrityIncident : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident();
+ virtual ~ClientIncidentReport_IncidentData_BinaryIntegrityIncident();
+
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from);
+
+ inline ClientIncidentReport_IncidentData_BinaryIntegrityIncident& operator=(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_BinaryIntegrityIncident* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_BinaryIntegrityIncident* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_BinaryIntegrityIncident* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_BinaryIntegrityIncident& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile ContainedFile;
+
+ // accessors -------------------------------------------------------
+
+ // optional string file_basename = 1;
+ inline bool has_file_basename() const;
+ inline void clear_file_basename();
+ static const int kFileBasenameFieldNumber = 1;
+ inline const ::std::string& file_basename() const;
+ inline void set_file_basename(const ::std::string& value);
+ inline void set_file_basename(const char* value);
+ inline void set_file_basename(const char* value, size_t size);
+ inline ::std::string* mutable_file_basename();
+ inline ::std::string* release_file_basename();
+ inline void set_allocated_file_basename(::std::string* file_basename);
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+ inline bool has_signature() const;
+ inline void clear_signature();
+ static const int kSignatureFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& signature() const;
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* mutable_signature();
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* release_signature();
+ inline void set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature);
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 3;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // optional int32 sec_error = 4;
+ inline bool has_sec_error() const;
+ inline void clear_sec_error();
+ static const int kSecErrorFieldNumber = 4;
+ inline ::google::protobuf::int32 sec_error() const;
+ inline void set_sec_error(::google::protobuf::int32 value);
+
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile contained_file = 5;
+ inline int contained_file_size() const;
+ inline void clear_contained_file();
+ static const int kContainedFileFieldNumber = 5;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& contained_file(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* mutable_contained_file(int index);
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* add_contained_file();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile >&
+ contained_file() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile >*
+ mutable_contained_file();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident)
+ private:
+ inline void set_has_file_basename();
+ inline void clear_has_file_basename();
+ inline void set_has_signature();
+ inline void clear_has_signature();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+ inline void set_has_sec_error();
+ inline void clear_has_sec_error();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* file_basename_;
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* signature_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile > contained_file_;
+ ::google::protobuf::int32 sec_error_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_BinaryIntegrityIncident* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_BlacklistLoadIncident : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_BlacklistLoadIncident();
+ virtual ~ClientIncidentReport_IncidentData_BlacklistLoadIncident();
+
+ ClientIncidentReport_IncidentData_BlacklistLoadIncident(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from);
+
+ inline ClientIncidentReport_IncidentData_BlacklistLoadIncident& operator=(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_BlacklistLoadIncident& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_BlacklistLoadIncident* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_BlacklistLoadIncident* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_BlacklistLoadIncident* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_BlacklistLoadIncident& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string path = 1;
+ inline bool has_path() const;
+ inline void clear_path();
+ static const int kPathFieldNumber = 1;
+ inline const ::std::string& path() const;
+ inline void set_path(const ::std::string& value);
+ inline void set_path(const char* value);
+ inline void set_path(const char* value, size_t size);
+ inline ::std::string* mutable_path();
+ inline ::std::string* release_path();
+ inline void set_allocated_path(::std::string* path);
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ inline bool has_digest() const;
+ inline void clear_digest();
+ static const int kDigestFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_Digests& digest() const;
+ inline ::safe_browsing::ClientDownloadRequest_Digests* mutable_digest();
+ inline ::safe_browsing::ClientDownloadRequest_Digests* release_digest();
+ inline void set_allocated_digest(::safe_browsing::ClientDownloadRequest_Digests* digest);
+
+ // optional string version = 3;
+ inline bool has_version() const;
+ inline void clear_version();
+ static const int kVersionFieldNumber = 3;
+ inline const ::std::string& version() const;
+ inline void set_version(const ::std::string& value);
+ inline void set_version(const char* value);
+ inline void set_version(const char* value, size_t size);
+ inline ::std::string* mutable_version();
+ inline ::std::string* release_version();
+ inline void set_allocated_version(::std::string* version);
+
+ // optional bool blacklist_initialized = 4;
+ inline bool has_blacklist_initialized() const;
+ inline void clear_blacklist_initialized();
+ static const int kBlacklistInitializedFieldNumber = 4;
+ inline bool blacklist_initialized() const;
+ inline void set_blacklist_initialized(bool value);
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+ inline bool has_signature() const;
+ inline void clear_signature();
+ static const int kSignatureFieldNumber = 5;
+ inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& signature() const;
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* mutable_signature();
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* release_signature();
+ inline void set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature);
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 6;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident)
+ private:
+ inline void set_has_path();
+ inline void clear_has_path();
+ inline void set_has_digest();
+ inline void clear_has_digest();
+ inline void set_has_version();
+ inline void clear_has_version();
+ inline void set_has_blacklist_initialized();
+ inline void clear_has_blacklist_initialized();
+ inline void set_has_signature();
+ inline void clear_has_signature();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* path_;
+ ::safe_browsing::ClientDownloadRequest_Digests* digest_;
+ ::std::string* version_;
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* signature_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ bool blacklist_initialized_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_BlacklistLoadIncident* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident();
+ virtual ~ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident();
+
+ ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from);
+
+ inline ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& operator=(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string variations_seed_signature = 1;
+ inline bool has_variations_seed_signature() const;
+ inline void clear_variations_seed_signature();
+ static const int kVariationsSeedSignatureFieldNumber = 1;
+ inline const ::std::string& variations_seed_signature() const;
+ inline void set_variations_seed_signature(const ::std::string& value);
+ inline void set_variations_seed_signature(const char* value);
+ inline void set_variations_seed_signature(const char* value, size_t size);
+ inline ::std::string* mutable_variations_seed_signature();
+ inline ::std::string* release_variations_seed_signature();
+ inline void set_allocated_variations_seed_signature(::std::string* variations_seed_signature);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident)
+ private:
+ inline void set_has_variations_seed_signature();
+ inline void clear_has_variations_seed_signature();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* variations_seed_signature_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_ResourceRequestIncident : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_ResourceRequestIncident();
+ virtual ~ClientIncidentReport_IncidentData_ResourceRequestIncident();
+
+ ClientIncidentReport_IncidentData_ResourceRequestIncident(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from);
+
+ inline ClientIncidentReport_IncidentData_ResourceRequestIncident& operator=(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_ResourceRequestIncident& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_ResourceRequestIncident* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_ResourceRequestIncident* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_ResourceRequestIncident* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_ResourceRequestIncident& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_IncidentData_ResourceRequestIncident_Type Type;
+ static const Type UNKNOWN = ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_UNKNOWN;
+ static const Type TYPE_PATTERN = ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_TYPE_PATTERN;
+ static inline bool Type_IsValid(int value) {
+ return ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_IsValid(value);
+ }
+ static const Type Type_MIN =
+ ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_MIN;
+ static const Type Type_MAX =
+ ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_MAX;
+ static const int Type_ARRAYSIZE =
+ ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_Type_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes digest = 1;
+ inline bool has_digest() const;
+ inline void clear_digest();
+ static const int kDigestFieldNumber = 1;
+ inline const ::std::string& digest() const;
+ inline void set_digest(const ::std::string& value);
+ inline void set_digest(const char* value);
+ inline void set_digest(const void* value, size_t size);
+ inline ::std::string* mutable_digest();
+ inline ::std::string* release_digest();
+ inline void set_allocated_digest(::std::string* digest);
+
+ // optional string origin = 2;
+ inline bool has_origin() const;
+ inline void clear_origin();
+ static const int kOriginFieldNumber = 2;
+ inline const ::std::string& origin() const;
+ inline void set_origin(const ::std::string& value);
+ inline void set_origin(const char* value);
+ inline void set_origin(const char* value, size_t size);
+ inline ::std::string* mutable_origin();
+ inline ::std::string* release_origin();
+ inline void set_allocated_origin(::std::string* origin);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.Type type = 3 [default = UNKNOWN];
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 3;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type type() const;
+ inline void set_type(::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident)
+ private:
+ inline void set_has_digest();
+ inline void clear_has_digest();
+ inline void set_has_origin();
+ inline void clear_has_origin();
+ inline void set_has_type();
+ inline void clear_has_type();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* digest_;
+ ::std::string* origin_;
+ int type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_ResourceRequestIncident* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData_SuspiciousModuleIncident : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData_SuspiciousModuleIncident();
+ virtual ~ClientIncidentReport_IncidentData_SuspiciousModuleIncident();
+
+ ClientIncidentReport_IncidentData_SuspiciousModuleIncident(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from);
+
+ inline ClientIncidentReport_IncidentData_SuspiciousModuleIncident& operator=(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData_SuspiciousModuleIncident* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData_SuspiciousModuleIncident* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData_SuspiciousModuleIncident* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData_SuspiciousModuleIncident& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string path = 1;
+ inline bool has_path() const;
+ inline void clear_path();
+ static const int kPathFieldNumber = 1;
+ inline const ::std::string& path() const;
+ inline void set_path(const ::std::string& value);
+ inline void set_path(const char* value);
+ inline void set_path(const char* value, size_t size);
+ inline ::std::string* mutable_path();
+ inline ::std::string* release_path();
+ inline void set_allocated_path(::std::string* path);
+
+ // optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+ inline bool has_digest() const;
+ inline void clear_digest();
+ static const int kDigestFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest_Digests& digest() const;
+ inline ::safe_browsing::ClientDownloadRequest_Digests* mutable_digest();
+ inline ::safe_browsing::ClientDownloadRequest_Digests* release_digest();
+ inline void set_allocated_digest(::safe_browsing::ClientDownloadRequest_Digests* digest);
+
+ // optional string version = 3;
+ inline bool has_version() const;
+ inline void clear_version();
+ static const int kVersionFieldNumber = 3;
+ inline const ::std::string& version() const;
+ inline void set_version(const ::std::string& value);
+ inline void set_version(const char* value);
+ inline void set_version(const char* value, size_t size);
+ inline ::std::string* mutable_version();
+ inline ::std::string* release_version();
+ inline void set_allocated_version(::std::string* version);
+
+ // optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 4;
+ inline bool has_signature() const;
+ inline void clear_signature();
+ static const int kSignatureFieldNumber = 4;
+ inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& signature() const;
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* mutable_signature();
+ inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* release_signature();
+ inline void set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature);
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 5;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident)
+ private:
+ inline void set_has_path();
+ inline void clear_has_path();
+ inline void set_has_digest();
+ inline void clear_has_digest();
+ inline void set_has_version();
+ inline void clear_has_version();
+ inline void set_has_signature();
+ inline void clear_has_signature();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* path_;
+ ::safe_browsing::ClientDownloadRequest_Digests* digest_;
+ ::std::string* version_;
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* signature_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData_SuspiciousModuleIncident* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_IncidentData : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_IncidentData();
+ virtual ~ClientIncidentReport_IncidentData();
+
+ ClientIncidentReport_IncidentData(const ClientIncidentReport_IncidentData& from);
+
+ inline ClientIncidentReport_IncidentData& operator=(const ClientIncidentReport_IncidentData& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_IncidentData& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_IncidentData* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_IncidentData* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_IncidentData* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_IncidentData& from);
+ void MergeFrom(const ClientIncidentReport_IncidentData& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_IncidentData_TrackedPreferenceIncident TrackedPreferenceIncident;
+ typedef ClientIncidentReport_IncidentData_BinaryIntegrityIncident BinaryIntegrityIncident;
+ typedef ClientIncidentReport_IncidentData_BlacklistLoadIncident BlacklistLoadIncident;
+ typedef ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident VariationsSeedSignatureIncident;
+ typedef ClientIncidentReport_IncidentData_ResourceRequestIncident ResourceRequestIncident;
+ typedef ClientIncidentReport_IncidentData_SuspiciousModuleIncident SuspiciousModuleIncident;
+
+ // accessors -------------------------------------------------------
+
+ // optional int64 incident_time_msec = 1;
+ inline bool has_incident_time_msec() const;
+ inline void clear_incident_time_msec();
+ static const int kIncidentTimeMsecFieldNumber = 1;
+ inline ::google::protobuf::int64 incident_time_msec() const;
+ inline void set_incident_time_msec(::google::protobuf::int64 value);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident tracked_preference = 2;
+ inline bool has_tracked_preference() const;
+ inline void clear_tracked_preference();
+ static const int kTrackedPreferenceFieldNumber = 2;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident& tracked_preference() const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* mutable_tracked_preference();
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* release_tracked_preference();
+ inline void set_allocated_tracked_preference(::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* tracked_preference);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident binary_integrity = 3;
+ inline bool has_binary_integrity() const;
+ inline void clear_binary_integrity();
+ static const int kBinaryIntegrityFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident& binary_integrity() const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* mutable_binary_integrity();
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* release_binary_integrity();
+ inline void set_allocated_binary_integrity(::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* binary_integrity);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident blacklist_load = 4;
+ inline bool has_blacklist_load() const;
+ inline void clear_blacklist_load();
+ static const int kBlacklistLoadFieldNumber = 4;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident& blacklist_load() const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* mutable_blacklist_load();
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* release_blacklist_load();
+ inline void set_allocated_blacklist_load(::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* blacklist_load);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident variations_seed_signature = 6;
+ inline bool has_variations_seed_signature() const;
+ inline void clear_variations_seed_signature();
+ static const int kVariationsSeedSignatureFieldNumber = 6;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& variations_seed_signature() const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* mutable_variations_seed_signature();
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* release_variations_seed_signature();
+ inline void set_allocated_variations_seed_signature(::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* variations_seed_signature);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident resource_request = 7;
+ inline bool has_resource_request() const;
+ inline void clear_resource_request();
+ static const int kResourceRequestFieldNumber = 7;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident& resource_request() const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* mutable_resource_request();
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* release_resource_request();
+ inline void set_allocated_resource_request(::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* resource_request);
+
+ // optional .safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident suspicious_module = 8;
+ inline bool has_suspicious_module() const;
+ inline void clear_suspicious_module();
+ static const int kSuspiciousModuleFieldNumber = 8;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident& suspicious_module() const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* mutable_suspicious_module();
+ inline ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* release_suspicious_module();
+ inline void set_allocated_suspicious_module(::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* suspicious_module);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.IncidentData)
+ private:
+ inline void set_has_incident_time_msec();
+ inline void clear_has_incident_time_msec();
+ inline void set_has_tracked_preference();
+ inline void clear_has_tracked_preference();
+ inline void set_has_binary_integrity();
+ inline void clear_has_binary_integrity();
+ inline void set_has_blacklist_load();
+ inline void clear_has_blacklist_load();
+ inline void set_has_variations_seed_signature();
+ inline void clear_has_variations_seed_signature();
+ inline void set_has_resource_request();
+ inline void clear_has_resource_request();
+ inline void set_has_suspicious_module();
+ inline void clear_has_suspicious_module();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::int64 incident_time_msec_;
+ ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* tracked_preference_;
+ ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* binary_integrity_;
+ ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* blacklist_load_;
+ ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* variations_seed_signature_;
+ ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* resource_request_;
+ ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* suspicious_module_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_IncidentData* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_DownloadDetails : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_DownloadDetails();
+ virtual ~ClientIncidentReport_DownloadDetails();
+
+ ClientIncidentReport_DownloadDetails(const ClientIncidentReport_DownloadDetails& from);
+
+ inline ClientIncidentReport_DownloadDetails& operator=(const ClientIncidentReport_DownloadDetails& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_DownloadDetails& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_DownloadDetails* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_DownloadDetails* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_DownloadDetails* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_DownloadDetails& from);
+ void MergeFrom(const ClientIncidentReport_DownloadDetails& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes token = 1;
+ inline bool has_token() const;
+ inline void clear_token();
+ static const int kTokenFieldNumber = 1;
+ inline const ::std::string& token() const;
+ inline void set_token(const ::std::string& value);
+ inline void set_token(const char* value);
+ inline void set_token(const void* value, size_t size);
+ inline ::std::string* mutable_token();
+ inline ::std::string* release_token();
+ inline void set_allocated_token(::std::string* token);
+
+ // optional .safe_browsing.ClientDownloadRequest download = 2;
+ inline bool has_download() const;
+ inline void clear_download();
+ static const int kDownloadFieldNumber = 2;
+ inline const ::safe_browsing::ClientDownloadRequest& download() const;
+ inline ::safe_browsing::ClientDownloadRequest* mutable_download();
+ inline ::safe_browsing::ClientDownloadRequest* release_download();
+ inline void set_allocated_download(::safe_browsing::ClientDownloadRequest* download);
+
+ // optional int64 download_time_msec = 3;
+ inline bool has_download_time_msec() const;
+ inline void clear_download_time_msec();
+ static const int kDownloadTimeMsecFieldNumber = 3;
+ inline ::google::protobuf::int64 download_time_msec() const;
+ inline void set_download_time_msec(::google::protobuf::int64 value);
+
+ // optional int64 open_time_msec = 4;
+ inline bool has_open_time_msec() const;
+ inline void clear_open_time_msec();
+ static const int kOpenTimeMsecFieldNumber = 4;
+ inline ::google::protobuf::int64 open_time_msec() const;
+ inline void set_open_time_msec(::google::protobuf::int64 value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.DownloadDetails)
+ private:
+ inline void set_has_token();
+ inline void clear_has_token();
+ inline void set_has_download();
+ inline void clear_has_download();
+ inline void set_has_download_time_msec();
+ inline void clear_has_download_time_msec();
+ inline void set_has_open_time_msec();
+ inline void clear_has_open_time_msec();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* token_;
+ ::safe_browsing::ClientDownloadRequest* download_;
+ ::google::protobuf::int64 download_time_msec_;
+ ::google::protobuf::int64 open_time_msec_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_DownloadDetails* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_OS_RegistryValue : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_OS_RegistryValue();
+ virtual ~ClientIncidentReport_EnvironmentData_OS_RegistryValue();
+
+ ClientIncidentReport_EnvironmentData_OS_RegistryValue(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from);
+
+ inline ClientIncidentReport_EnvironmentData_OS_RegistryValue& operator=(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_OS_RegistryValue& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_OS_RegistryValue* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_OS_RegistryValue* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_OS_RegistryValue* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryValue& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional uint32 type = 2;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 2;
+ inline ::google::protobuf::uint32 type() const;
+ inline void set_type(::google::protobuf::uint32 value);
+
+ // optional bytes data = 3;
+ inline bool has_data() const;
+ inline void clear_data();
+ static const int kDataFieldNumber = 3;
+ inline const ::std::string& data() const;
+ inline void set_data(const ::std::string& value);
+ inline void set_data(const char* value);
+ inline void set_data(const void* value, size_t size);
+ inline ::std::string* mutable_data();
+ inline ::std::string* release_data();
+ inline void set_allocated_data(::std::string* data);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_data();
+ inline void clear_has_data();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::std::string* data_;
+ ::google::protobuf::uint32 type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_OS_RegistryValue* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_OS_RegistryKey : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_OS_RegistryKey();
+ virtual ~ClientIncidentReport_EnvironmentData_OS_RegistryKey();
+
+ ClientIncidentReport_EnvironmentData_OS_RegistryKey(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from);
+
+ inline ClientIncidentReport_EnvironmentData_OS_RegistryKey& operator=(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_OS_RegistryKey& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_OS_RegistryKey* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_OS_RegistryKey* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_OS_RegistryKey* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_OS_RegistryKey& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue value = 2;
+ inline int value_size() const;
+ inline void clear_value();
+ static const int kValueFieldNumber = 2;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue& value(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue* mutable_value(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue* add_value();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue >&
+ value() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue >*
+ mutable_value();
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey key = 3;
+ inline int key_size() const;
+ inline void clear_key();
+ static const int kKeyFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey& key(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* mutable_key(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* add_key();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >&
+ key() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >*
+ mutable_key();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue > value_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey > key_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_OS_RegistryKey* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_OS : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_OS();
+ virtual ~ClientIncidentReport_EnvironmentData_OS();
+
+ ClientIncidentReport_EnvironmentData_OS(const ClientIncidentReport_EnvironmentData_OS& from);
+
+ inline ClientIncidentReport_EnvironmentData_OS& operator=(const ClientIncidentReport_EnvironmentData_OS& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_OS& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_OS* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_OS* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_OS* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_OS& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_OS& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_EnvironmentData_OS_RegistryValue RegistryValue;
+ typedef ClientIncidentReport_EnvironmentData_OS_RegistryKey RegistryKey;
+
+ // accessors -------------------------------------------------------
+
+ // optional string os_name = 1;
+ inline bool has_os_name() const;
+ inline void clear_os_name();
+ static const int kOsNameFieldNumber = 1;
+ inline const ::std::string& os_name() const;
+ inline void set_os_name(const ::std::string& value);
+ inline void set_os_name(const char* value);
+ inline void set_os_name(const char* value, size_t size);
+ inline ::std::string* mutable_os_name();
+ inline ::std::string* release_os_name();
+ inline void set_allocated_os_name(::std::string* os_name);
+
+ // optional string os_version = 2;
+ inline bool has_os_version() const;
+ inline void clear_os_version();
+ static const int kOsVersionFieldNumber = 2;
+ inline const ::std::string& os_version() const;
+ inline void set_os_version(const ::std::string& value);
+ inline void set_os_version(const char* value);
+ inline void set_os_version(const char* value, size_t size);
+ inline ::std::string* mutable_os_version();
+ inline ::std::string* release_os_version();
+ inline void set_allocated_os_version(::std::string* os_version);
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey registry_key = 3;
+ inline int registry_key_size() const;
+ inline void clear_registry_key();
+ static const int kRegistryKeyFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey& registry_key(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* mutable_registry_key(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* add_registry_key();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >&
+ registry_key() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >*
+ mutable_registry_key();
+
+ // optional bool is_enrolled_to_domain = 4;
+ inline bool has_is_enrolled_to_domain() const;
+ inline void clear_is_enrolled_to_domain();
+ static const int kIsEnrolledToDomainFieldNumber = 4;
+ inline bool is_enrolled_to_domain() const;
+ inline void set_is_enrolled_to_domain(bool value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.OS)
+ private:
+ inline void set_has_os_name();
+ inline void clear_has_os_name();
+ inline void set_has_os_version();
+ inline void clear_has_os_version();
+ inline void set_has_is_enrolled_to_domain();
+ inline void clear_has_is_enrolled_to_domain();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* os_name_;
+ ::std::string* os_version_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey > registry_key_;
+ bool is_enrolled_to_domain_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_OS* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Machine : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Machine();
+ virtual ~ClientIncidentReport_EnvironmentData_Machine();
+
+ ClientIncidentReport_EnvironmentData_Machine(const ClientIncidentReport_EnvironmentData_Machine& from);
+
+ inline ClientIncidentReport_EnvironmentData_Machine& operator=(const ClientIncidentReport_EnvironmentData_Machine& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Machine& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Machine* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Machine* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Machine* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Machine& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Machine& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string cpu_architecture = 1;
+ inline bool has_cpu_architecture() const;
+ inline void clear_cpu_architecture();
+ static const int kCpuArchitectureFieldNumber = 1;
+ inline const ::std::string& cpu_architecture() const;
+ inline void set_cpu_architecture(const ::std::string& value);
+ inline void set_cpu_architecture(const char* value);
+ inline void set_cpu_architecture(const char* value, size_t size);
+ inline ::std::string* mutable_cpu_architecture();
+ inline ::std::string* release_cpu_architecture();
+ inline void set_allocated_cpu_architecture(::std::string* cpu_architecture);
+
+ // optional string cpu_vendor = 2;
+ inline bool has_cpu_vendor() const;
+ inline void clear_cpu_vendor();
+ static const int kCpuVendorFieldNumber = 2;
+ inline const ::std::string& cpu_vendor() const;
+ inline void set_cpu_vendor(const ::std::string& value);
+ inline void set_cpu_vendor(const char* value);
+ inline void set_cpu_vendor(const char* value, size_t size);
+ inline ::std::string* mutable_cpu_vendor();
+ inline ::std::string* release_cpu_vendor();
+ inline void set_allocated_cpu_vendor(::std::string* cpu_vendor);
+
+ // optional uint32 cpuid = 3;
+ inline bool has_cpuid() const;
+ inline void clear_cpuid();
+ static const int kCpuidFieldNumber = 3;
+ inline ::google::protobuf::uint32 cpuid() const;
+ inline void set_cpuid(::google::protobuf::uint32 value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Machine)
+ private:
+ inline void set_has_cpu_architecture();
+ inline void clear_has_cpu_architecture();
+ inline void set_has_cpu_vendor();
+ inline void clear_has_cpu_vendor();
+ inline void set_has_cpuid();
+ inline void clear_has_cpuid();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* cpu_architecture_;
+ ::std::string* cpu_vendor_;
+ ::google::protobuf::uint32 cpuid_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Machine* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Process_Patch : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Process_Patch();
+ virtual ~ClientIncidentReport_EnvironmentData_Process_Patch();
+
+ ClientIncidentReport_EnvironmentData_Process_Patch(const ClientIncidentReport_EnvironmentData_Process_Patch& from);
+
+ inline ClientIncidentReport_EnvironmentData_Process_Patch& operator=(const ClientIncidentReport_EnvironmentData_Process_Patch& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Process_Patch& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Process_Patch* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Process_Patch* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Process_Patch* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Process_Patch& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Process_Patch& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string function = 1;
+ inline bool has_function() const;
+ inline void clear_function();
+ static const int kFunctionFieldNumber = 1;
+ inline const ::std::string& function() const;
+ inline void set_function(const ::std::string& value);
+ inline void set_function(const char* value);
+ inline void set_function(const char* value, size_t size);
+ inline ::std::string* mutable_function();
+ inline ::std::string* release_function();
+ inline void set_allocated_function(::std::string* function);
+
+ // optional string target_dll = 2;
+ inline bool has_target_dll() const;
+ inline void clear_target_dll();
+ static const int kTargetDllFieldNumber = 2;
+ inline const ::std::string& target_dll() const;
+ inline void set_target_dll(const ::std::string& value);
+ inline void set_target_dll(const char* value);
+ inline void set_target_dll(const char* value, size_t size);
+ inline ::std::string* mutable_target_dll();
+ inline ::std::string* release_target_dll();
+ inline void set_allocated_target_dll(::std::string* target_dll);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch)
+ private:
+ inline void set_has_function();
+ inline void clear_has_function();
+ inline void set_has_target_dll();
+ inline void clear_has_target_dll();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* function_;
+ ::std::string* target_dll_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Process_Patch* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Process_NetworkProvider : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Process_NetworkProvider();
+ virtual ~ClientIncidentReport_EnvironmentData_Process_NetworkProvider();
+
+ ClientIncidentReport_EnvironmentData_Process_NetworkProvider(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from);
+
+ inline ClientIncidentReport_EnvironmentData_Process_NetworkProvider& operator=(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Process_NetworkProvider* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Process_NetworkProvider* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Process_NetworkProvider* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Process_NetworkProvider& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Process_NetworkProvider* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Process_Dll : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Process_Dll();
+ virtual ~ClientIncidentReport_EnvironmentData_Process_Dll();
+
+ ClientIncidentReport_EnvironmentData_Process_Dll(const ClientIncidentReport_EnvironmentData_Process_Dll& from);
+
+ inline ClientIncidentReport_EnvironmentData_Process_Dll& operator=(const ClientIncidentReport_EnvironmentData_Process_Dll& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Process_Dll& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Process_Dll* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Process_Dll* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Process_Dll* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Process_Dll& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Process_Dll& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_EnvironmentData_Process_Dll_Feature Feature;
+ static const Feature UNKNOWN = ClientIncidentReport_EnvironmentData_Process_Dll_Feature_UNKNOWN;
+ static const Feature LSP = ClientIncidentReport_EnvironmentData_Process_Dll_Feature_LSP;
+ static inline bool Feature_IsValid(int value) {
+ return ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid(value);
+ }
+ static const Feature Feature_MIN =
+ ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_MIN;
+ static const Feature Feature_MAX =
+ ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_MAX;
+ static const int Feature_ARRAYSIZE =
+ ClientIncidentReport_EnvironmentData_Process_Dll_Feature_Feature_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional string path = 1;
+ inline bool has_path() const;
+ inline void clear_path();
+ static const int kPathFieldNumber = 1;
+ inline const ::std::string& path() const;
+ inline void set_path(const ::std::string& value);
+ inline void set_path(const char* value);
+ inline void set_path(const char* value, size_t size);
+ inline ::std::string* mutable_path();
+ inline ::std::string* release_path();
+ inline void set_allocated_path(::std::string* path);
+
+ // optional uint64 base_address = 2;
+ inline bool has_base_address() const;
+ inline void clear_base_address();
+ static const int kBaseAddressFieldNumber = 2;
+ inline ::google::protobuf::uint64 base_address() const;
+ inline void set_base_address(::google::protobuf::uint64 value);
+
+ // optional uint32 length = 3;
+ inline bool has_length() const;
+ inline void clear_length();
+ static const int kLengthFieldNumber = 3;
+ inline ::google::protobuf::uint32 length() const;
+ inline void set_length(::google::protobuf::uint32 value);
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.Feature feature = 4;
+ inline int feature_size() const;
+ inline void clear_feature();
+ static const int kFeatureFieldNumber = 4;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature feature(int index) const;
+ inline void set_feature(int index, ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature value);
+ inline void add_feature(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature value);
+ inline const ::google::protobuf::RepeatedField<int>& feature() const;
+ inline ::google::protobuf::RepeatedField<int>* mutable_feature();
+
+ // optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+ inline bool has_image_headers() const;
+ inline void clear_image_headers();
+ static const int kImageHeadersFieldNumber = 5;
+ inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& image_headers() const;
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* mutable_image_headers();
+ inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* release_image_headers();
+ inline void set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll)
+ private:
+ inline void set_has_path();
+ inline void clear_has_path();
+ inline void set_has_base_address();
+ inline void clear_has_base_address();
+ inline void set_has_length();
+ inline void clear_has_length();
+ inline void set_has_image_headers();
+ inline void clear_has_image_headers();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* path_;
+ ::google::protobuf::uint64 base_address_;
+ ::google::protobuf::RepeatedField<int> feature_;
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers_;
+ ::google::protobuf::uint32 length_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Process_Dll* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification();
+ virtual ~ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification();
+
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from);
+
+ inline ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& operator=(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional uint32 file_offset = 1;
+ inline bool has_file_offset() const;
+ inline void clear_file_offset();
+ static const int kFileOffsetFieldNumber = 1;
+ inline ::google::protobuf::uint32 file_offset() const;
+ inline void set_file_offset(::google::protobuf::uint32 value);
+
+ // optional int32 byte_count = 2;
+ inline bool has_byte_count() const;
+ inline void clear_byte_count();
+ static const int kByteCountFieldNumber = 2;
+ inline ::google::protobuf::int32 byte_count() const;
+ inline void set_byte_count(::google::protobuf::int32 value);
+
+ // optional bytes modified_bytes = 3;
+ inline bool has_modified_bytes() const;
+ inline void clear_modified_bytes();
+ static const int kModifiedBytesFieldNumber = 3;
+ inline const ::std::string& modified_bytes() const;
+ inline void set_modified_bytes(const ::std::string& value);
+ inline void set_modified_bytes(const char* value);
+ inline void set_modified_bytes(const void* value, size_t size);
+ inline ::std::string* mutable_modified_bytes();
+ inline ::std::string* release_modified_bytes();
+ inline void set_allocated_modified_bytes(::std::string* modified_bytes);
+
+ // optional string export_name = 4;
+ inline bool has_export_name() const;
+ inline void clear_export_name();
+ static const int kExportNameFieldNumber = 4;
+ inline const ::std::string& export_name() const;
+ inline void set_export_name(const ::std::string& value);
+ inline void set_export_name(const char* value);
+ inline void set_export_name(const char* value, size_t size);
+ inline ::std::string* mutable_export_name();
+ inline ::std::string* release_export_name();
+ inline void set_allocated_export_name(::std::string* export_name);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification)
+ private:
+ inline void set_has_file_offset();
+ inline void clear_has_file_offset();
+ inline void set_has_byte_count();
+ inline void clear_has_byte_count();
+ inline void set_has_modified_bytes();
+ inline void clear_has_modified_bytes();
+ inline void set_has_export_name();
+ inline void clear_has_export_name();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::uint32 file_offset_;
+ ::google::protobuf::int32 byte_count_;
+ ::std::string* modified_bytes_;
+ ::std::string* export_name_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Process_ModuleState : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Process_ModuleState();
+ virtual ~ClientIncidentReport_EnvironmentData_Process_ModuleState();
+
+ ClientIncidentReport_EnvironmentData_Process_ModuleState(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from);
+
+ inline ClientIncidentReport_EnvironmentData_Process_ModuleState& operator=(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Process_ModuleState& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Process_ModuleState* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Process_ModuleState* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Process_ModuleState* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Process_ModuleState& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification Modification;
+
+ typedef ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ModifiedState;
+ static const ModifiedState UNKNOWN = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_UNKNOWN;
+ static const ModifiedState MODULE_STATE_UNKNOWN = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_UNKNOWN;
+ static const ModifiedState MODULE_STATE_UNMODIFIED = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_UNMODIFIED;
+ static const ModifiedState MODULE_STATE_MODIFIED = ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_MODULE_STATE_MODIFIED;
+ static inline bool ModifiedState_IsValid(int value) {
+ return ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_IsValid(value);
+ }
+ static const ModifiedState ModifiedState_MIN =
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_MIN;
+ static const ModifiedState ModifiedState_MAX =
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_MAX;
+ static const int ModifiedState_ARRAYSIZE =
+ ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_ModifiedState_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.ModifiedState modified_state = 2;
+ inline bool has_modified_state() const;
+ inline void clear_modified_state();
+ static const int kModifiedStateFieldNumber = 2;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState modified_state() const;
+ inline void set_modified_state(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState value);
+
+ // repeated string OBSOLETE_modified_export = 3;
+ inline int obsolete_modified_export_size() const;
+ inline void clear_obsolete_modified_export();
+ static const int kOBSOLETEModifiedExportFieldNumber = 3;
+ inline const ::std::string& obsolete_modified_export(int index) const;
+ inline ::std::string* mutable_obsolete_modified_export(int index);
+ inline void set_obsolete_modified_export(int index, const ::std::string& value);
+ inline void set_obsolete_modified_export(int index, const char* value);
+ inline void set_obsolete_modified_export(int index, const char* value, size_t size);
+ inline ::std::string* add_obsolete_modified_export();
+ inline void add_obsolete_modified_export(const ::std::string& value);
+ inline void add_obsolete_modified_export(const char* value);
+ inline void add_obsolete_modified_export(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& obsolete_modified_export() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_obsolete_modified_export();
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification modification = 4;
+ inline int modification_size() const;
+ inline void clear_modification();
+ static const int kModificationFieldNumber = 4;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& modification(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* mutable_modification(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* add_modification();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification >&
+ modification() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification >*
+ mutable_modification();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_modified_state();
+ inline void clear_has_modified_state();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> obsolete_modified_export_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification > modification_;
+ int modified_state_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Process_ModuleState* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData_Process : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData_Process();
+ virtual ~ClientIncidentReport_EnvironmentData_Process();
+
+ ClientIncidentReport_EnvironmentData_Process(const ClientIncidentReport_EnvironmentData_Process& from);
+
+ inline ClientIncidentReport_EnvironmentData_Process& operator=(const ClientIncidentReport_EnvironmentData_Process& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData_Process& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData_Process* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData_Process* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData_Process* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData_Process& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData_Process& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_EnvironmentData_Process_Patch Patch;
+ typedef ClientIncidentReport_EnvironmentData_Process_NetworkProvider NetworkProvider;
+ typedef ClientIncidentReport_EnvironmentData_Process_Dll Dll;
+ typedef ClientIncidentReport_EnvironmentData_Process_ModuleState ModuleState;
+
+ typedef ClientIncidentReport_EnvironmentData_Process_Channel Channel;
+ static const Channel CHANNEL_UNKNOWN = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_UNKNOWN;
+ static const Channel CHANNEL_CANARY = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_CANARY;
+ static const Channel CHANNEL_DEV = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_DEV;
+ static const Channel CHANNEL_BETA = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_BETA;
+ static const Channel CHANNEL_STABLE = ClientIncidentReport_EnvironmentData_Process_Channel_CHANNEL_STABLE;
+ static inline bool Channel_IsValid(int value) {
+ return ClientIncidentReport_EnvironmentData_Process_Channel_IsValid(value);
+ }
+ static const Channel Channel_MIN =
+ ClientIncidentReport_EnvironmentData_Process_Channel_Channel_MIN;
+ static const Channel Channel_MAX =
+ ClientIncidentReport_EnvironmentData_Process_Channel_Channel_MAX;
+ static const int Channel_ARRAYSIZE =
+ ClientIncidentReport_EnvironmentData_Process_Channel_Channel_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional string version = 1;
+ inline bool has_version() const;
+ inline void clear_version();
+ static const int kVersionFieldNumber = 1;
+ inline const ::std::string& version() const;
+ inline void set_version(const ::std::string& value);
+ inline void set_version(const char* value);
+ inline void set_version(const char* value, size_t size);
+ inline ::std::string* mutable_version();
+ inline ::std::string* release_version();
+ inline void set_allocated_version(::std::string* version);
+
+ // repeated string OBSOLETE_dlls = 2;
+ inline int obsolete_dlls_size() const;
+ inline void clear_obsolete_dlls();
+ static const int kOBSOLETEDllsFieldNumber = 2;
+ inline const ::std::string& obsolete_dlls(int index) const;
+ inline ::std::string* mutable_obsolete_dlls(int index);
+ inline void set_obsolete_dlls(int index, const ::std::string& value);
+ inline void set_obsolete_dlls(int index, const char* value);
+ inline void set_obsolete_dlls(int index, const char* value, size_t size);
+ inline ::std::string* add_obsolete_dlls();
+ inline void add_obsolete_dlls(const ::std::string& value);
+ inline void add_obsolete_dlls(const char* value);
+ inline void add_obsolete_dlls(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& obsolete_dlls() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_obsolete_dlls();
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch patches = 3;
+ inline int patches_size() const;
+ inline void clear_patches();
+ static const int kPatchesFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch& patches(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch* mutable_patches(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch* add_patches();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch >&
+ patches() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch >*
+ mutable_patches();
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider network_providers = 4;
+ inline int network_providers_size() const;
+ inline void clear_network_providers();
+ static const int kNetworkProvidersFieldNumber = 4;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider& network_providers(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider* mutable_network_providers(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider* add_network_providers();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider >&
+ network_providers() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider >*
+ mutable_network_providers();
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Channel chrome_update_channel = 5;
+ inline bool has_chrome_update_channel() const;
+ inline void clear_chrome_update_channel();
+ static const int kChromeUpdateChannelFieldNumber = 5;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel chrome_update_channel() const;
+ inline void set_chrome_update_channel(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel value);
+
+ // optional int64 uptime_msec = 6;
+ inline bool has_uptime_msec() const;
+ inline void clear_uptime_msec();
+ static const int kUptimeMsecFieldNumber = 6;
+ inline ::google::protobuf::int64 uptime_msec() const;
+ inline void set_uptime_msec(::google::protobuf::int64 value);
+
+ // optional bool metrics_consent = 7;
+ inline bool has_metrics_consent() const;
+ inline void clear_metrics_consent();
+ static const int kMetricsConsentFieldNumber = 7;
+ inline bool metrics_consent() const;
+ inline void set_metrics_consent(bool value);
+
+ // optional bool extended_consent = 8;
+ inline bool has_extended_consent() const;
+ inline void clear_extended_consent();
+ static const int kExtendedConsentFieldNumber = 8;
+ inline bool extended_consent() const;
+ inline void set_extended_consent(bool value);
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll dll = 9;
+ inline int dll_size() const;
+ inline void clear_dll();
+ static const int kDllFieldNumber = 9;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll& dll(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll* mutable_dll(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll* add_dll();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll >&
+ dll() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll >*
+ mutable_dll();
+
+ // repeated string blacklisted_dll = 10;
+ inline int blacklisted_dll_size() const;
+ inline void clear_blacklisted_dll();
+ static const int kBlacklistedDllFieldNumber = 10;
+ inline const ::std::string& blacklisted_dll(int index) const;
+ inline ::std::string* mutable_blacklisted_dll(int index);
+ inline void set_blacklisted_dll(int index, const ::std::string& value);
+ inline void set_blacklisted_dll(int index, const char* value);
+ inline void set_blacklisted_dll(int index, const char* value, size_t size);
+ inline ::std::string* add_blacklisted_dll();
+ inline void add_blacklisted_dll(const ::std::string& value);
+ inline void add_blacklisted_dll(const char* value);
+ inline void add_blacklisted_dll(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& blacklisted_dll() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_blacklisted_dll();
+
+ // repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState module_state = 11;
+ inline int module_state_size() const;
+ inline void clear_module_state();
+ static const int kModuleStateFieldNumber = 11;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState& module_state(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState* mutable_module_state(int index);
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState* add_module_state();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState >&
+ module_state() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState >*
+ mutable_module_state();
+
+ // optional bool field_trial_participant = 12;
+ inline bool has_field_trial_participant() const;
+ inline void clear_field_trial_participant();
+ static const int kFieldTrialParticipantFieldNumber = 12;
+ inline bool field_trial_participant() const;
+ inline void set_field_trial_participant(bool value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData.Process)
+ private:
+ inline void set_has_version();
+ inline void clear_has_version();
+ inline void set_has_chrome_update_channel();
+ inline void clear_has_chrome_update_channel();
+ inline void set_has_uptime_msec();
+ inline void clear_has_uptime_msec();
+ inline void set_has_metrics_consent();
+ inline void clear_has_metrics_consent();
+ inline void set_has_extended_consent();
+ inline void clear_has_extended_consent();
+ inline void set_has_field_trial_participant();
+ inline void clear_has_field_trial_participant();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* version_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> obsolete_dlls_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch > patches_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider > network_providers_;
+ ::google::protobuf::int64 uptime_msec_;
+ int chrome_update_channel_;
+ bool metrics_consent_;
+ bool extended_consent_;
+ bool field_trial_participant_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll > dll_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> blacklisted_dll_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState > module_state_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData_Process* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_EnvironmentData : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_EnvironmentData();
+ virtual ~ClientIncidentReport_EnvironmentData();
+
+ ClientIncidentReport_EnvironmentData(const ClientIncidentReport_EnvironmentData& from);
+
+ inline ClientIncidentReport_EnvironmentData& operator=(const ClientIncidentReport_EnvironmentData& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_EnvironmentData& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_EnvironmentData* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_EnvironmentData* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_EnvironmentData* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_EnvironmentData& from);
+ void MergeFrom(const ClientIncidentReport_EnvironmentData& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_EnvironmentData_OS OS;
+ typedef ClientIncidentReport_EnvironmentData_Machine Machine;
+ typedef ClientIncidentReport_EnvironmentData_Process Process;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.OS os = 1;
+ inline bool has_os() const;
+ inline void clear_os();
+ static const int kOsFieldNumber = 1;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS& os() const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS* mutable_os();
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS* release_os();
+ inline void set_allocated_os(::safe_browsing::ClientIncidentReport_EnvironmentData_OS* os);
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Machine machine = 2;
+ inline bool has_machine() const;
+ inline void clear_machine();
+ static const int kMachineFieldNumber = 2;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine& machine() const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* mutable_machine();
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* release_machine();
+ inline void set_allocated_machine(::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* machine);
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process process = 3;
+ inline bool has_process() const;
+ inline void clear_process();
+ static const int kProcessFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process& process() const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process* mutable_process();
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process* release_process();
+ inline void set_allocated_process(::safe_browsing::ClientIncidentReport_EnvironmentData_Process* process);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.EnvironmentData)
+ private:
+ inline void set_has_os();
+ inline void clear_has_os();
+ inline void set_has_machine();
+ inline void clear_has_machine();
+ inline void set_has_process();
+ inline void clear_has_process();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_OS* os_;
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* machine_;
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_Process* process_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_EnvironmentData* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_ExtensionData_ExtensionInfo : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_ExtensionData_ExtensionInfo();
+ virtual ~ClientIncidentReport_ExtensionData_ExtensionInfo();
+
+ ClientIncidentReport_ExtensionData_ExtensionInfo(const ClientIncidentReport_ExtensionData_ExtensionInfo& from);
+
+ inline ClientIncidentReport_ExtensionData_ExtensionInfo& operator=(const ClientIncidentReport_ExtensionData_ExtensionInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_ExtensionData_ExtensionInfo& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_ExtensionData_ExtensionInfo* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_ExtensionData_ExtensionInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_ExtensionData_ExtensionInfo* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_ExtensionData_ExtensionInfo& from);
+ void MergeFrom(const ClientIncidentReport_ExtensionData_ExtensionInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ExtensionState;
+ static const ExtensionState STATE_UNKNOWN = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_UNKNOWN;
+ static const ExtensionState STATE_ENABLED = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_ENABLED;
+ static const ExtensionState STATE_DISABLED = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_DISABLED;
+ static const ExtensionState STATE_BLACKLISTED = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_BLACKLISTED;
+ static const ExtensionState STATE_BLOCKED = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_BLOCKED;
+ static const ExtensionState STATE_TERMINATED = ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_STATE_TERMINATED;
+ static inline bool ExtensionState_IsValid(int value) {
+ return ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_IsValid(value);
+ }
+ static const ExtensionState ExtensionState_MIN =
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_MIN;
+ static const ExtensionState ExtensionState_MAX =
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_MAX;
+ static const int ExtensionState_ARRAYSIZE =
+ ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_ExtensionState_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional string id = 1;
+ inline bool has_id() const;
+ inline void clear_id();
+ static const int kIdFieldNumber = 1;
+ inline const ::std::string& id() const;
+ inline void set_id(const ::std::string& value);
+ inline void set_id(const char* value);
+ inline void set_id(const char* value, size_t size);
+ inline ::std::string* mutable_id();
+ inline ::std::string* release_id();
+ inline void set_allocated_id(::std::string* id);
+
+ // optional string version = 2;
+ inline bool has_version() const;
+ inline void clear_version();
+ static const int kVersionFieldNumber = 2;
+ inline const ::std::string& version() const;
+ inline void set_version(const ::std::string& value);
+ inline void set_version(const char* value);
+ inline void set_version(const char* value, size_t size);
+ inline ::std::string* mutable_version();
+ inline ::std::string* release_version();
+ inline void set_allocated_version(::std::string* version);
+
+ // optional string name = 3;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 3;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional string description = 4;
+ inline bool has_description() const;
+ inline void clear_description();
+ static const int kDescriptionFieldNumber = 4;
+ inline const ::std::string& description() const;
+ inline void set_description(const ::std::string& value);
+ inline void set_description(const char* value);
+ inline void set_description(const char* value, size_t size);
+ inline ::std::string* mutable_description();
+ inline ::std::string* release_description();
+ inline void set_allocated_description(::std::string* description);
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.ExtensionState state = 5 [default = STATE_UNKNOWN];
+ inline bool has_state() const;
+ inline void clear_state();
+ static const int kStateFieldNumber = 5;
+ inline ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState state() const;
+ inline void set_state(::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState value);
+
+ // optional int32 type = 6;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 6;
+ inline ::google::protobuf::int32 type() const;
+ inline void set_type(::google::protobuf::int32 value);
+
+ // optional string update_url = 7;
+ inline bool has_update_url() const;
+ inline void clear_update_url();
+ static const int kUpdateUrlFieldNumber = 7;
+ inline const ::std::string& update_url() const;
+ inline void set_update_url(const ::std::string& value);
+ inline void set_update_url(const char* value);
+ inline void set_update_url(const char* value, size_t size);
+ inline ::std::string* mutable_update_url();
+ inline ::std::string* release_update_url();
+ inline void set_allocated_update_url(::std::string* update_url);
+
+ // optional bool has_signature_validation = 8;
+ inline bool has_has_signature_validation() const;
+ inline void clear_has_signature_validation();
+ static const int kHasSignatureValidationFieldNumber = 8;
+ inline bool has_signature_validation() const;
+ inline void set_has_signature_validation(bool value);
+
+ // optional bool signature_is_valid = 9;
+ inline bool has_signature_is_valid() const;
+ inline void clear_signature_is_valid();
+ static const int kSignatureIsValidFieldNumber = 9;
+ inline bool signature_is_valid() const;
+ inline void set_signature_is_valid(bool value);
+
+ // optional bool installed_by_custodian = 10;
+ inline bool has_installed_by_custodian() const;
+ inline void clear_installed_by_custodian();
+ static const int kInstalledByCustodianFieldNumber = 10;
+ inline bool installed_by_custodian() const;
+ inline void set_installed_by_custodian(bool value);
+
+ // optional bool installed_by_default = 11;
+ inline bool has_installed_by_default() const;
+ inline void clear_installed_by_default();
+ static const int kInstalledByDefaultFieldNumber = 11;
+ inline bool installed_by_default() const;
+ inline void set_installed_by_default(bool value);
+
+ // optional bool installed_by_oem = 12;
+ inline bool has_installed_by_oem() const;
+ inline void clear_installed_by_oem();
+ static const int kInstalledByOemFieldNumber = 12;
+ inline bool installed_by_oem() const;
+ inline void set_installed_by_oem(bool value);
+
+ // optional bool from_bookmark = 13;
+ inline bool has_from_bookmark() const;
+ inline void clear_from_bookmark();
+ static const int kFromBookmarkFieldNumber = 13;
+ inline bool from_bookmark() const;
+ inline void set_from_bookmark(bool value);
+
+ // optional bool from_webstore = 14;
+ inline bool has_from_webstore() const;
+ inline void clear_from_webstore();
+ static const int kFromWebstoreFieldNumber = 14;
+ inline bool from_webstore() const;
+ inline void set_from_webstore(bool value);
+
+ // optional bool converted_from_user_script = 15;
+ inline bool has_converted_from_user_script() const;
+ inline void clear_converted_from_user_script();
+ static const int kConvertedFromUserScriptFieldNumber = 15;
+ inline bool converted_from_user_script() const;
+ inline void set_converted_from_user_script(bool value);
+
+ // optional bool may_be_untrusted = 16;
+ inline bool has_may_be_untrusted() const;
+ inline void clear_may_be_untrusted();
+ static const int kMayBeUntrustedFieldNumber = 16;
+ inline bool may_be_untrusted() const;
+ inline void set_may_be_untrusted(bool value);
+
+ // optional int64 install_time_msec = 17;
+ inline bool has_install_time_msec() const;
+ inline void clear_install_time_msec();
+ static const int kInstallTimeMsecFieldNumber = 17;
+ inline ::google::protobuf::int64 install_time_msec() const;
+ inline void set_install_time_msec(::google::protobuf::int64 value);
+
+ // optional int32 manifest_location_type = 18;
+ inline bool has_manifest_location_type() const;
+ inline void clear_manifest_location_type();
+ static const int kManifestLocationTypeFieldNumber = 18;
+ inline ::google::protobuf::int32 manifest_location_type() const;
+ inline void set_manifest_location_type(::google::protobuf::int32 value);
+
+ // optional string manifest = 19;
+ inline bool has_manifest() const;
+ inline void clear_manifest();
+ static const int kManifestFieldNumber = 19;
+ inline const ::std::string& manifest() const;
+ inline void set_manifest(const ::std::string& value);
+ inline void set_manifest(const char* value);
+ inline void set_manifest(const char* value, size_t size);
+ inline ::std::string* mutable_manifest();
+ inline ::std::string* release_manifest();
+ inline void set_allocated_manifest(::std::string* manifest);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo)
+ private:
+ inline void set_has_id();
+ inline void clear_has_id();
+ inline void set_has_version();
+ inline void clear_has_version();
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_description();
+ inline void clear_has_description();
+ inline void set_has_state();
+ inline void clear_has_state();
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_update_url();
+ inline void clear_has_update_url();
+ inline void set_has_has_signature_validation();
+ inline void clear_has_has_signature_validation();
+ inline void set_has_signature_is_valid();
+ inline void clear_has_signature_is_valid();
+ inline void set_has_installed_by_custodian();
+ inline void clear_has_installed_by_custodian();
+ inline void set_has_installed_by_default();
+ inline void clear_has_installed_by_default();
+ inline void set_has_installed_by_oem();
+ inline void clear_has_installed_by_oem();
+ inline void set_has_from_bookmark();
+ inline void clear_has_from_bookmark();
+ inline void set_has_from_webstore();
+ inline void clear_has_from_webstore();
+ inline void set_has_converted_from_user_script();
+ inline void clear_has_converted_from_user_script();
+ inline void set_has_may_be_untrusted();
+ inline void clear_has_may_be_untrusted();
+ inline void set_has_install_time_msec();
+ inline void clear_has_install_time_msec();
+ inline void set_has_manifest_location_type();
+ inline void clear_has_manifest_location_type();
+ inline void set_has_manifest();
+ inline void clear_has_manifest();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* id_;
+ ::std::string* version_;
+ ::std::string* name_;
+ ::std::string* description_;
+ int state_;
+ ::google::protobuf::int32 type_;
+ ::std::string* update_url_;
+ bool has_signature_validation_;
+ bool signature_is_valid_;
+ bool installed_by_custodian_;
+ bool installed_by_default_;
+ bool installed_by_oem_;
+ bool from_bookmark_;
+ bool from_webstore_;
+ bool converted_from_user_script_;
+ ::google::protobuf::int64 install_time_msec_;
+ bool may_be_untrusted_;
+ ::google::protobuf::int32 manifest_location_type_;
+ ::std::string* manifest_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_ExtensionData_ExtensionInfo* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_ExtensionData : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_ExtensionData();
+ virtual ~ClientIncidentReport_ExtensionData();
+
+ ClientIncidentReport_ExtensionData(const ClientIncidentReport_ExtensionData& from);
+
+ inline ClientIncidentReport_ExtensionData& operator=(const ClientIncidentReport_ExtensionData& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_ExtensionData& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_ExtensionData* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_ExtensionData* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_ExtensionData* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_ExtensionData& from);
+ void MergeFrom(const ClientIncidentReport_ExtensionData& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_ExtensionData_ExtensionInfo ExtensionInfo;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo last_installed_extension = 1;
+ inline bool has_last_installed_extension() const;
+ inline void clear_last_installed_extension();
+ static const int kLastInstalledExtensionFieldNumber = 1;
+ inline const ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo& last_installed_extension() const;
+ inline ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* mutable_last_installed_extension();
+ inline ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* release_last_installed_extension();
+ inline void set_allocated_last_installed_extension(::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* last_installed_extension);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.ExtensionData)
+ private:
+ inline void set_has_last_installed_extension();
+ inline void clear_has_last_installed_extension();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* last_installed_extension_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_ExtensionData* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport_NonBinaryDownloadDetails : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport_NonBinaryDownloadDetails();
+ virtual ~ClientIncidentReport_NonBinaryDownloadDetails();
+
+ ClientIncidentReport_NonBinaryDownloadDetails(const ClientIncidentReport_NonBinaryDownloadDetails& from);
+
+ inline ClientIncidentReport_NonBinaryDownloadDetails& operator=(const ClientIncidentReport_NonBinaryDownloadDetails& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport_NonBinaryDownloadDetails& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport_NonBinaryDownloadDetails* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport_NonBinaryDownloadDetails* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport_NonBinaryDownloadDetails* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport_NonBinaryDownloadDetails& from);
+ void MergeFrom(const ClientIncidentReport_NonBinaryDownloadDetails& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string file_type = 1;
+ inline bool has_file_type() const;
+ inline void clear_file_type();
+ static const int kFileTypeFieldNumber = 1;
+ inline const ::std::string& file_type() const;
+ inline void set_file_type(const ::std::string& value);
+ inline void set_file_type(const char* value);
+ inline void set_file_type(const char* value, size_t size);
+ inline ::std::string* mutable_file_type();
+ inline ::std::string* release_file_type();
+ inline void set_allocated_file_type(::std::string* file_type);
+
+ // optional bytes url_spec_sha256 = 2;
+ inline bool has_url_spec_sha256() const;
+ inline void clear_url_spec_sha256();
+ static const int kUrlSpecSha256FieldNumber = 2;
+ inline const ::std::string& url_spec_sha256() const;
+ inline void set_url_spec_sha256(const ::std::string& value);
+ inline void set_url_spec_sha256(const char* value);
+ inline void set_url_spec_sha256(const void* value, size_t size);
+ inline ::std::string* mutable_url_spec_sha256();
+ inline ::std::string* release_url_spec_sha256();
+ inline void set_allocated_url_spec_sha256(::std::string* url_spec_sha256);
+
+ // optional string host = 3;
+ inline bool has_host() const;
+ inline void clear_host();
+ static const int kHostFieldNumber = 3;
+ inline const ::std::string& host() const;
+ inline void set_host(const ::std::string& value);
+ inline void set_host(const char* value);
+ inline void set_host(const char* value, size_t size);
+ inline ::std::string* mutable_host();
+ inline ::std::string* release_host();
+ inline void set_allocated_host(::std::string* host);
+
+ // optional int64 length = 4;
+ inline bool has_length() const;
+ inline void clear_length();
+ static const int kLengthFieldNumber = 4;
+ inline ::google::protobuf::int64 length() const;
+ inline void set_length(::google::protobuf::int64 value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails)
+ private:
+ inline void set_has_file_type();
+ inline void clear_has_file_type();
+ inline void set_has_url_spec_sha256();
+ inline void clear_has_url_spec_sha256();
+ inline void set_has_host();
+ inline void clear_has_host();
+ inline void set_has_length();
+ inline void clear_has_length();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* file_type_;
+ ::std::string* url_spec_sha256_;
+ ::std::string* host_;
+ ::google::protobuf::int64 length_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport_NonBinaryDownloadDetails* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentReport : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentReport();
+ virtual ~ClientIncidentReport();
+
+ ClientIncidentReport(const ClientIncidentReport& from);
+
+ inline ClientIncidentReport& operator=(const ClientIncidentReport& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentReport& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentReport* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentReport* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentReport* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentReport& from);
+ void MergeFrom(const ClientIncidentReport& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentReport_IncidentData IncidentData;
+ typedef ClientIncidentReport_DownloadDetails DownloadDetails;
+ typedef ClientIncidentReport_EnvironmentData EnvironmentData;
+ typedef ClientIncidentReport_ExtensionData ExtensionData;
+ typedef ClientIncidentReport_NonBinaryDownloadDetails NonBinaryDownloadDetails;
+
+ // accessors -------------------------------------------------------
+
+ // repeated .safe_browsing.ClientIncidentReport.IncidentData incident = 1;
+ inline int incident_size() const;
+ inline void clear_incident();
+ static const int kIncidentFieldNumber = 1;
+ inline const ::safe_browsing::ClientIncidentReport_IncidentData& incident(int index) const;
+ inline ::safe_browsing::ClientIncidentReport_IncidentData* mutable_incident(int index);
+ inline ::safe_browsing::ClientIncidentReport_IncidentData* add_incident();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData >&
+ incident() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData >*
+ mutable_incident();
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ inline bool has_download() const;
+ inline void clear_download();
+ static const int kDownloadFieldNumber = 2;
+ inline const ::safe_browsing::ClientIncidentReport_DownloadDetails& download() const;
+ inline ::safe_browsing::ClientIncidentReport_DownloadDetails* mutable_download();
+ inline ::safe_browsing::ClientIncidentReport_DownloadDetails* release_download();
+ inline void set_allocated_download(::safe_browsing::ClientIncidentReport_DownloadDetails* download);
+
+ // optional .safe_browsing.ClientIncidentReport.EnvironmentData environment = 3;
+ inline bool has_environment() const;
+ inline void clear_environment();
+ static const int kEnvironmentFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentReport_EnvironmentData& environment() const;
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData* mutable_environment();
+ inline ::safe_browsing::ClientIncidentReport_EnvironmentData* release_environment();
+ inline void set_allocated_environment(::safe_browsing::ClientIncidentReport_EnvironmentData* environment);
+
+ // optional .safe_browsing.ChromeUserPopulation population = 7;
+ inline bool has_population() const;
+ inline void clear_population();
+ static const int kPopulationFieldNumber = 7;
+ inline const ::safe_browsing::ChromeUserPopulation& population() const;
+ inline ::safe_browsing::ChromeUserPopulation* mutable_population();
+ inline ::safe_browsing::ChromeUserPopulation* release_population();
+ inline void set_allocated_population(::safe_browsing::ChromeUserPopulation* population);
+
+ // optional .safe_browsing.ClientIncidentReport.ExtensionData extension_data = 8;
+ inline bool has_extension_data() const;
+ inline void clear_extension_data();
+ static const int kExtensionDataFieldNumber = 8;
+ inline const ::safe_browsing::ClientIncidentReport_ExtensionData& extension_data() const;
+ inline ::safe_browsing::ClientIncidentReport_ExtensionData* mutable_extension_data();
+ inline ::safe_browsing::ClientIncidentReport_ExtensionData* release_extension_data();
+ inline void set_allocated_extension_data(::safe_browsing::ClientIncidentReport_ExtensionData* extension_data);
+
+ // optional .safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails non_binary_download = 9;
+ inline bool has_non_binary_download() const;
+ inline void clear_non_binary_download();
+ static const int kNonBinaryDownloadFieldNumber = 9;
+ inline const ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails& non_binary_download() const;
+ inline ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* mutable_non_binary_download();
+ inline ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* release_non_binary_download();
+ inline void set_allocated_non_binary_download(::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* non_binary_download);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentReport)
+ private:
+ inline void set_has_download();
+ inline void clear_has_download();
+ inline void set_has_environment();
+ inline void clear_has_environment();
+ inline void set_has_population();
+ inline void clear_has_population();
+ inline void set_has_extension_data();
+ inline void clear_has_extension_data();
+ inline void set_has_non_binary_download();
+ inline void clear_has_non_binary_download();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData > incident_;
+ ::safe_browsing::ClientIncidentReport_DownloadDetails* download_;
+ ::safe_browsing::ClientIncidentReport_EnvironmentData* environment_;
+ ::safe_browsing::ChromeUserPopulation* population_;
+ ::safe_browsing::ClientIncidentReport_ExtensionData* extension_data_;
+ ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* non_binary_download_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentReport* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentResponse_EnvironmentRequest : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentResponse_EnvironmentRequest();
+ virtual ~ClientIncidentResponse_EnvironmentRequest();
+
+ ClientIncidentResponse_EnvironmentRequest(const ClientIncidentResponse_EnvironmentRequest& from);
+
+ inline ClientIncidentResponse_EnvironmentRequest& operator=(const ClientIncidentResponse_EnvironmentRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentResponse_EnvironmentRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentResponse_EnvironmentRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentResponse_EnvironmentRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentResponse_EnvironmentRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentResponse_EnvironmentRequest& from);
+ void MergeFrom(const ClientIncidentResponse_EnvironmentRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int32 dll_index = 1;
+ inline bool has_dll_index() const;
+ inline void clear_dll_index();
+ static const int kDllIndexFieldNumber = 1;
+ inline ::google::protobuf::int32 dll_index() const;
+ inline void set_dll_index(::google::protobuf::int32 value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentResponse.EnvironmentRequest)
+ private:
+ inline void set_has_dll_index();
+ inline void clear_has_dll_index();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::int32 dll_index_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentResponse_EnvironmentRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientIncidentResponse : public ::google::protobuf::MessageLite {
+ public:
+ ClientIncidentResponse();
+ virtual ~ClientIncidentResponse();
+
+ ClientIncidentResponse(const ClientIncidentResponse& from);
+
+ inline ClientIncidentResponse& operator=(const ClientIncidentResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientIncidentResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientIncidentResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientIncidentResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientIncidentResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientIncidentResponse& from);
+ void MergeFrom(const ClientIncidentResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientIncidentResponse_EnvironmentRequest EnvironmentRequest;
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes token = 1;
+ inline bool has_token() const;
+ inline void clear_token();
+ static const int kTokenFieldNumber = 1;
+ inline const ::std::string& token() const;
+ inline void set_token(const ::std::string& value);
+ inline void set_token(const char* value);
+ inline void set_token(const void* value, size_t size);
+ inline ::std::string* mutable_token();
+ inline ::std::string* release_token();
+ inline void set_allocated_token(::std::string* token);
+
+ // optional bool download_requested = 2;
+ inline bool has_download_requested() const;
+ inline void clear_download_requested();
+ static const int kDownloadRequestedFieldNumber = 2;
+ inline bool download_requested() const;
+ inline void set_download_requested(bool value);
+
+ // repeated .safe_browsing.ClientIncidentResponse.EnvironmentRequest environment_requests = 3;
+ inline int environment_requests_size() const;
+ inline void clear_environment_requests();
+ static const int kEnvironmentRequestsFieldNumber = 3;
+ inline const ::safe_browsing::ClientIncidentResponse_EnvironmentRequest& environment_requests(int index) const;
+ inline ::safe_browsing::ClientIncidentResponse_EnvironmentRequest* mutable_environment_requests(int index);
+ inline ::safe_browsing::ClientIncidentResponse_EnvironmentRequest* add_environment_requests();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentResponse_EnvironmentRequest >&
+ environment_requests() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentResponse_EnvironmentRequest >*
+ mutable_environment_requests();
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientIncidentResponse)
+ private:
+ inline void set_has_token();
+ inline void clear_has_token();
+ inline void set_has_download_requested();
+ inline void clear_has_download_requested();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* token_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentResponse_EnvironmentRequest > environment_requests_;
+ bool download_requested_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientIncidentResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class DownloadMetadata : public ::google::protobuf::MessageLite {
+ public:
+ DownloadMetadata();
+ virtual ~DownloadMetadata();
+
+ DownloadMetadata(const DownloadMetadata& from);
+
+ inline DownloadMetadata& operator=(const DownloadMetadata& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const DownloadMetadata& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const DownloadMetadata* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(DownloadMetadata* other);
+
+ // implements Message ----------------------------------------------
+
+ DownloadMetadata* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const DownloadMetadata& from);
+ void MergeFrom(const DownloadMetadata& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional uint32 download_id = 1;
+ inline bool has_download_id() const;
+ inline void clear_download_id();
+ static const int kDownloadIdFieldNumber = 1;
+ inline ::google::protobuf::uint32 download_id() const;
+ inline void set_download_id(::google::protobuf::uint32 value);
+
+ // optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+ inline bool has_download() const;
+ inline void clear_download();
+ static const int kDownloadFieldNumber = 2;
+ inline const ::safe_browsing::ClientIncidentReport_DownloadDetails& download() const;
+ inline ::safe_browsing::ClientIncidentReport_DownloadDetails* mutable_download();
+ inline ::safe_browsing::ClientIncidentReport_DownloadDetails* release_download();
+ inline void set_allocated_download(::safe_browsing::ClientIncidentReport_DownloadDetails* download);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.DownloadMetadata)
+ private:
+ inline void set_has_download_id();
+ inline void clear_has_download_id();
+ inline void set_has_download();
+ inline void clear_has_download();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientIncidentReport_DownloadDetails* download_;
+ ::google::protobuf::uint32 download_id_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static DownloadMetadata* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest_HTTPHeader : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest_HTTPHeader();
+ virtual ~ClientSafeBrowsingReportRequest_HTTPHeader();
+
+ ClientSafeBrowsingReportRequest_HTTPHeader(const ClientSafeBrowsingReportRequest_HTTPHeader& from);
+
+ inline ClientSafeBrowsingReportRequest_HTTPHeader& operator=(const ClientSafeBrowsingReportRequest_HTTPHeader& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest_HTTPHeader& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest_HTTPHeader* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest_HTTPHeader* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest_HTTPHeader* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest_HTTPHeader& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest_HTTPHeader& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required bytes name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const void* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional bytes value = 2;
+ inline bool has_value() const;
+ inline void clear_value();
+ static const int kValueFieldNumber = 2;
+ inline const ::std::string& value() const;
+ inline void set_value(const ::std::string& value);
+ inline void set_value(const char* value);
+ inline void set_value(const void* value, size_t size);
+ inline ::std::string* mutable_value();
+ inline ::std::string* release_value();
+ inline void set_allocated_value(::std::string* value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_value();
+ inline void clear_has_value();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::std::string* value_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest_HTTPHeader* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine();
+ virtual ~ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine();
+
+ ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from);
+
+ inline ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& operator=(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes verb = 1;
+ inline bool has_verb() const;
+ inline void clear_verb();
+ static const int kVerbFieldNumber = 1;
+ inline const ::std::string& verb() const;
+ inline void set_verb(const ::std::string& value);
+ inline void set_verb(const char* value);
+ inline void set_verb(const void* value, size_t size);
+ inline ::std::string* mutable_verb();
+ inline ::std::string* release_verb();
+ inline void set_allocated_verb(::std::string* verb);
+
+ // optional bytes uri = 2;
+ inline bool has_uri() const;
+ inline void clear_uri();
+ static const int kUriFieldNumber = 2;
+ inline const ::std::string& uri() const;
+ inline void set_uri(const ::std::string& value);
+ inline void set_uri(const char* value);
+ inline void set_uri(const void* value, size_t size);
+ inline ::std::string* mutable_uri();
+ inline ::std::string* release_uri();
+ inline void set_allocated_uri(::std::string* uri);
+
+ // optional bytes version = 3;
+ inline bool has_version() const;
+ inline void clear_version();
+ static const int kVersionFieldNumber = 3;
+ inline const ::std::string& version() const;
+ inline void set_version(const ::std::string& value);
+ inline void set_version(const char* value);
+ inline void set_version(const void* value, size_t size);
+ inline ::std::string* mutable_version();
+ inline ::std::string* release_version();
+ inline void set_allocated_version(::std::string* version);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine)
+ private:
+ inline void set_has_verb();
+ inline void clear_has_verb();
+ inline void set_has_uri();
+ inline void clear_has_uri();
+ inline void set_has_version();
+ inline void clear_has_version();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* verb_;
+ ::std::string* uri_;
+ ::std::string* version_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest_HTTPRequest : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest_HTTPRequest();
+ virtual ~ClientSafeBrowsingReportRequest_HTTPRequest();
+
+ ClientSafeBrowsingReportRequest_HTTPRequest(const ClientSafeBrowsingReportRequest_HTTPRequest& from);
+
+ inline ClientSafeBrowsingReportRequest_HTTPRequest& operator=(const ClientSafeBrowsingReportRequest_HTTPRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest_HTTPRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest_HTTPRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest_HTTPRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest_HTTPRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest_HTTPRequest& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest_HTTPRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine FirstLine;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine firstline = 1;
+ inline bool has_firstline() const;
+ inline void clear_firstline();
+ static const int kFirstlineFieldNumber = 1;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& firstline() const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* mutable_firstline();
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* release_firstline();
+ inline void set_allocated_firstline(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* firstline);
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ inline int headers_size() const;
+ inline void clear_headers();
+ static const int kHeadersFieldNumber = 2;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader& headers(int index) const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* mutable_headers(int index);
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* add_headers();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >&
+ headers() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >*
+ mutable_headers();
+
+ // optional bytes body = 3;
+ inline bool has_body() const;
+ inline void clear_body();
+ static const int kBodyFieldNumber = 3;
+ inline const ::std::string& body() const;
+ inline void set_body(const ::std::string& value);
+ inline void set_body(const char* value);
+ inline void set_body(const void* value, size_t size);
+ inline ::std::string* mutable_body();
+ inline ::std::string* release_body();
+ inline void set_allocated_body(::std::string* body);
+
+ // optional bytes bodydigest = 4;
+ inline bool has_bodydigest() const;
+ inline void clear_bodydigest();
+ static const int kBodydigestFieldNumber = 4;
+ inline const ::std::string& bodydigest() const;
+ inline void set_bodydigest(const ::std::string& value);
+ inline void set_bodydigest(const char* value);
+ inline void set_bodydigest(const void* value, size_t size);
+ inline ::std::string* mutable_bodydigest();
+ inline ::std::string* release_bodydigest();
+ inline void set_allocated_bodydigest(::std::string* bodydigest);
+
+ // optional int32 bodylength = 5;
+ inline bool has_bodylength() const;
+ inline void clear_bodylength();
+ static const int kBodylengthFieldNumber = 5;
+ inline ::google::protobuf::int32 bodylength() const;
+ inline void set_bodylength(::google::protobuf::int32 value);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest)
+ private:
+ inline void set_has_firstline();
+ inline void clear_has_firstline();
+ inline void set_has_body();
+ inline void clear_has_body();
+ inline void set_has_bodydigest();
+ inline void clear_has_bodydigest();
+ inline void set_has_bodylength();
+ inline void clear_has_bodylength();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* firstline_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader > headers_;
+ ::std::string* body_;
+ ::std::string* bodydigest_;
+ ::google::protobuf::int32 bodylength_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest_HTTPRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine();
+ virtual ~ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine();
+
+ ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from);
+
+ inline ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& operator=(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int32 code = 1;
+ inline bool has_code() const;
+ inline void clear_code();
+ static const int kCodeFieldNumber = 1;
+ inline ::google::protobuf::int32 code() const;
+ inline void set_code(::google::protobuf::int32 value);
+
+ // optional bytes reason = 2;
+ inline bool has_reason() const;
+ inline void clear_reason();
+ static const int kReasonFieldNumber = 2;
+ inline const ::std::string& reason() const;
+ inline void set_reason(const ::std::string& value);
+ inline void set_reason(const char* value);
+ inline void set_reason(const void* value, size_t size);
+ inline ::std::string* mutable_reason();
+ inline ::std::string* release_reason();
+ inline void set_allocated_reason(::std::string* reason);
+
+ // optional bytes version = 3;
+ inline bool has_version() const;
+ inline void clear_version();
+ static const int kVersionFieldNumber = 3;
+ inline const ::std::string& version() const;
+ inline void set_version(const ::std::string& value);
+ inline void set_version(const char* value);
+ inline void set_version(const void* value, size_t size);
+ inline ::std::string* mutable_version();
+ inline ::std::string* release_version();
+ inline void set_allocated_version(::std::string* version);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine)
+ private:
+ inline void set_has_code();
+ inline void clear_has_code();
+ inline void set_has_reason();
+ inline void clear_has_reason();
+ inline void set_has_version();
+ inline void clear_has_version();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* reason_;
+ ::std::string* version_;
+ ::google::protobuf::int32 code_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest_HTTPResponse : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest_HTTPResponse();
+ virtual ~ClientSafeBrowsingReportRequest_HTTPResponse();
+
+ ClientSafeBrowsingReportRequest_HTTPResponse(const ClientSafeBrowsingReportRequest_HTTPResponse& from);
+
+ inline ClientSafeBrowsingReportRequest_HTTPResponse& operator=(const ClientSafeBrowsingReportRequest_HTTPResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest_HTTPResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest_HTTPResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest_HTTPResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest_HTTPResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest_HTTPResponse& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest_HTTPResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine FirstLine;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine firstline = 1;
+ inline bool has_firstline() const;
+ inline void clear_firstline();
+ static const int kFirstlineFieldNumber = 1;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& firstline() const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* mutable_firstline();
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* release_firstline();
+ inline void set_allocated_firstline(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* firstline);
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+ inline int headers_size() const;
+ inline void clear_headers();
+ static const int kHeadersFieldNumber = 2;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader& headers(int index) const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* mutable_headers(int index);
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* add_headers();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >&
+ headers() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >*
+ mutable_headers();
+
+ // optional bytes body = 3;
+ inline bool has_body() const;
+ inline void clear_body();
+ static const int kBodyFieldNumber = 3;
+ inline const ::std::string& body() const;
+ inline void set_body(const ::std::string& value);
+ inline void set_body(const char* value);
+ inline void set_body(const void* value, size_t size);
+ inline ::std::string* mutable_body();
+ inline ::std::string* release_body();
+ inline void set_allocated_body(::std::string* body);
+
+ // optional bytes bodydigest = 4;
+ inline bool has_bodydigest() const;
+ inline void clear_bodydigest();
+ static const int kBodydigestFieldNumber = 4;
+ inline const ::std::string& bodydigest() const;
+ inline void set_bodydigest(const ::std::string& value);
+ inline void set_bodydigest(const char* value);
+ inline void set_bodydigest(const void* value, size_t size);
+ inline ::std::string* mutable_bodydigest();
+ inline ::std::string* release_bodydigest();
+ inline void set_allocated_bodydigest(::std::string* bodydigest);
+
+ // optional int32 bodylength = 5;
+ inline bool has_bodylength() const;
+ inline void clear_bodylength();
+ static const int kBodylengthFieldNumber = 5;
+ inline ::google::protobuf::int32 bodylength() const;
+ inline void set_bodylength(::google::protobuf::int32 value);
+
+ // optional bytes remote_ip = 6;
+ inline bool has_remote_ip() const;
+ inline void clear_remote_ip();
+ static const int kRemoteIpFieldNumber = 6;
+ inline const ::std::string& remote_ip() const;
+ inline void set_remote_ip(const ::std::string& value);
+ inline void set_remote_ip(const char* value);
+ inline void set_remote_ip(const void* value, size_t size);
+ inline ::std::string* mutable_remote_ip();
+ inline ::std::string* release_remote_ip();
+ inline void set_allocated_remote_ip(::std::string* remote_ip);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse)
+ private:
+ inline void set_has_firstline();
+ inline void clear_has_firstline();
+ inline void set_has_body();
+ inline void clear_has_body();
+ inline void set_has_bodydigest();
+ inline void clear_has_bodydigest();
+ inline void set_has_bodylength();
+ inline void clear_has_bodylength();
+ inline void set_has_remote_ip();
+ inline void clear_has_remote_ip();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* firstline_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader > headers_;
+ ::std::string* body_;
+ ::std::string* bodydigest_;
+ ::std::string* remote_ip_;
+ ::google::protobuf::int32 bodylength_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest_HTTPResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest_Resource : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest_Resource();
+ virtual ~ClientSafeBrowsingReportRequest_Resource();
+
+ ClientSafeBrowsingReportRequest_Resource(const ClientSafeBrowsingReportRequest_Resource& from);
+
+ inline ClientSafeBrowsingReportRequest_Resource& operator=(const ClientSafeBrowsingReportRequest_Resource& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest_Resource& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest_Resource* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest_Resource* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest_Resource* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest_Resource& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest_Resource& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required int32 id = 1;
+ inline bool has_id() const;
+ inline void clear_id();
+ static const int kIdFieldNumber = 1;
+ inline ::google::protobuf::int32 id() const;
+ inline void set_id(::google::protobuf::int32 value);
+
+ // optional string url = 2;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 2;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest request = 3;
+ inline bool has_request() const;
+ inline void clear_request();
+ static const int kRequestFieldNumber = 3;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest& request() const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* mutable_request();
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* release_request();
+ inline void set_allocated_request(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* request);
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse response = 4;
+ inline bool has_response() const;
+ inline void clear_response();
+ static const int kResponseFieldNumber = 4;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse& response() const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* mutable_response();
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* release_response();
+ inline void set_allocated_response(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* response);
+
+ // optional int32 parent_id = 5;
+ inline bool has_parent_id() const;
+ inline void clear_parent_id();
+ static const int kParentIdFieldNumber = 5;
+ inline ::google::protobuf::int32 parent_id() const;
+ inline void set_parent_id(::google::protobuf::int32 value);
+
+ // repeated int32 child_ids = 6;
+ inline int child_ids_size() const;
+ inline void clear_child_ids();
+ static const int kChildIdsFieldNumber = 6;
+ inline ::google::protobuf::int32 child_ids(int index) const;
+ inline void set_child_ids(int index, ::google::protobuf::int32 value);
+ inline void add_child_ids(::google::protobuf::int32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ child_ids() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ mutable_child_ids();
+
+ // optional string tag_name = 7;
+ inline bool has_tag_name() const;
+ inline void clear_tag_name();
+ static const int kTagNameFieldNumber = 7;
+ inline const ::std::string& tag_name() const;
+ inline void set_tag_name(const ::std::string& value);
+ inline void set_tag_name(const char* value);
+ inline void set_tag_name(const char* value, size_t size);
+ inline ::std::string* mutable_tag_name();
+ inline ::std::string* release_tag_name();
+ inline void set_allocated_tag_name(::std::string* tag_name);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest.Resource)
+ private:
+ inline void set_has_id();
+ inline void clear_has_id();
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_request();
+ inline void clear_has_request();
+ inline void set_has_response();
+ inline void clear_has_response();
+ inline void set_has_parent_id();
+ inline void clear_has_parent_id();
+ inline void set_has_tag_name();
+ inline void clear_has_tag_name();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* request_;
+ ::google::protobuf::int32 id_;
+ ::google::protobuf::int32 parent_id_;
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* response_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::int32 > child_ids_;
+ ::std::string* tag_name_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest_Resource* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientSafeBrowsingReportRequest : public ::google::protobuf::MessageLite {
+ public:
+ ClientSafeBrowsingReportRequest();
+ virtual ~ClientSafeBrowsingReportRequest();
+
+ ClientSafeBrowsingReportRequest(const ClientSafeBrowsingReportRequest& from);
+
+ inline ClientSafeBrowsingReportRequest& operator=(const ClientSafeBrowsingReportRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientSafeBrowsingReportRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientSafeBrowsingReportRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientSafeBrowsingReportRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientSafeBrowsingReportRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientSafeBrowsingReportRequest& from);
+ void MergeFrom(const ClientSafeBrowsingReportRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ClientSafeBrowsingReportRequest_HTTPHeader HTTPHeader;
+ typedef ClientSafeBrowsingReportRequest_HTTPRequest HTTPRequest;
+ typedef ClientSafeBrowsingReportRequest_HTTPResponse HTTPResponse;
+ typedef ClientSafeBrowsingReportRequest_Resource Resource;
+
+ typedef ClientSafeBrowsingReportRequest_ReportType ReportType;
+ static const ReportType UNKNOWN = ClientSafeBrowsingReportRequest_ReportType_UNKNOWN;
+ static const ReportType URL_PHISHING = ClientSafeBrowsingReportRequest_ReportType_URL_PHISHING;
+ static const ReportType URL_MALWARE = ClientSafeBrowsingReportRequest_ReportType_URL_MALWARE;
+ static const ReportType URL_UNWANTED = ClientSafeBrowsingReportRequest_ReportType_URL_UNWANTED;
+ static const ReportType CLIENT_SIDE_PHISHING_URL = ClientSafeBrowsingReportRequest_ReportType_CLIENT_SIDE_PHISHING_URL;
+ static const ReportType CLIENT_SIDE_MALWARE_URL = ClientSafeBrowsingReportRequest_ReportType_CLIENT_SIDE_MALWARE_URL;
+ static const ReportType DANGEROUS_DOWNLOAD_RECOVERY = ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_RECOVERY;
+ static const ReportType DANGEROUS_DOWNLOAD_WARNING = ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_WARNING;
+ static const ReportType DANGEROUS_DOWNLOAD_BY_API = ClientSafeBrowsingReportRequest_ReportType_DANGEROUS_DOWNLOAD_BY_API;
+ static inline bool ReportType_IsValid(int value) {
+ return ClientSafeBrowsingReportRequest_ReportType_IsValid(value);
+ }
+ static const ReportType ReportType_MIN =
+ ClientSafeBrowsingReportRequest_ReportType_ReportType_MIN;
+ static const ReportType ReportType_MAX =
+ ClientSafeBrowsingReportRequest_ReportType_ReportType_MAX;
+ static const int ReportType_ARRAYSIZE =
+ ClientSafeBrowsingReportRequest_ReportType_ReportType_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .safe_browsing.ClientSafeBrowsingReportRequest.ReportType type = 10;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 10;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_ReportType type() const;
+ inline void set_type(::safe_browsing::ClientSafeBrowsingReportRequest_ReportType value);
+
+ // optional .safe_browsing.ClientDownloadResponse.Verdict download_verdict = 11;
+ inline bool has_download_verdict() const;
+ inline void clear_download_verdict();
+ static const int kDownloadVerdictFieldNumber = 11;
+ inline ::safe_browsing::ClientDownloadResponse_Verdict download_verdict() const;
+ inline void set_download_verdict(::safe_browsing::ClientDownloadResponse_Verdict value);
+
+ // optional string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional string page_url = 2;
+ inline bool has_page_url() const;
+ inline void clear_page_url();
+ static const int kPageUrlFieldNumber = 2;
+ inline const ::std::string& page_url() const;
+ inline void set_page_url(const ::std::string& value);
+ inline void set_page_url(const char* value);
+ inline void set_page_url(const char* value, size_t size);
+ inline ::std::string* mutable_page_url();
+ inline ::std::string* release_page_url();
+ inline void set_allocated_page_url(::std::string* page_url);
+
+ // optional string referrer_url = 3;
+ inline bool has_referrer_url() const;
+ inline void clear_referrer_url();
+ static const int kReferrerUrlFieldNumber = 3;
+ inline const ::std::string& referrer_url() const;
+ inline void set_referrer_url(const ::std::string& value);
+ inline void set_referrer_url(const char* value);
+ inline void set_referrer_url(const char* value, size_t size);
+ inline ::std::string* mutable_referrer_url();
+ inline ::std::string* release_referrer_url();
+ inline void set_allocated_referrer_url(::std::string* referrer_url);
+
+ // repeated .safe_browsing.ClientSafeBrowsingReportRequest.Resource resources = 4;
+ inline int resources_size() const;
+ inline void clear_resources();
+ static const int kResourcesFieldNumber = 4;
+ inline const ::safe_browsing::ClientSafeBrowsingReportRequest_Resource& resources(int index) const;
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_Resource* mutable_resources(int index);
+ inline ::safe_browsing::ClientSafeBrowsingReportRequest_Resource* add_resources();
+ inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_Resource >&
+ resources() const;
+ inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_Resource >*
+ mutable_resources();
+
+ // optional bool complete = 5;
+ inline bool has_complete() const;
+ inline void clear_complete();
+ static const int kCompleteFieldNumber = 5;
+ inline bool complete() const;
+ inline void set_complete(bool value);
+
+ // repeated string client_asn = 6;
+ inline int client_asn_size() const;
+ inline void clear_client_asn();
+ static const int kClientAsnFieldNumber = 6;
+ inline const ::std::string& client_asn(int index) const;
+ inline ::std::string* mutable_client_asn(int index);
+ inline void set_client_asn(int index, const ::std::string& value);
+ inline void set_client_asn(int index, const char* value);
+ inline void set_client_asn(int index, const char* value, size_t size);
+ inline ::std::string* add_client_asn();
+ inline void add_client_asn(const ::std::string& value);
+ inline void add_client_asn(const char* value);
+ inline void add_client_asn(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& client_asn() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_client_asn();
+
+ // optional string client_country = 7;
+ inline bool has_client_country() const;
+ inline void clear_client_country();
+ static const int kClientCountryFieldNumber = 7;
+ inline const ::std::string& client_country() const;
+ inline void set_client_country(const ::std::string& value);
+ inline void set_client_country(const char* value);
+ inline void set_client_country(const char* value, size_t size);
+ inline ::std::string* mutable_client_country();
+ inline ::std::string* release_client_country();
+ inline void set_allocated_client_country(::std::string* client_country);
+
+ // optional bool did_proceed = 8;
+ inline bool has_did_proceed() const;
+ inline void clear_did_proceed();
+ static const int kDidProceedFieldNumber = 8;
+ inline bool did_proceed() const;
+ inline void set_did_proceed(bool value);
+
+ // optional bool repeat_visit = 9;
+ inline bool has_repeat_visit() const;
+ inline void clear_repeat_visit();
+ static const int kRepeatVisitFieldNumber = 9;
+ inline bool repeat_visit() const;
+ inline void set_repeat_visit(bool value);
+
+ // optional bytes token = 15;
+ inline bool has_token() const;
+ inline void clear_token();
+ static const int kTokenFieldNumber = 15;
+ inline const ::std::string& token() const;
+ inline void set_token(const ::std::string& value);
+ inline void set_token(const char* value);
+ inline void set_token(const void* value, size_t size);
+ inline ::std::string* mutable_token();
+ inline ::std::string* release_token();
+ inline void set_allocated_token(::std::string* token);
+
+ // @@protoc_insertion_point(class_scope:safe_browsing.ClientSafeBrowsingReportRequest)
+ private:
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_download_verdict();
+ inline void clear_has_download_verdict();
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_page_url();
+ inline void clear_has_page_url();
+ inline void set_has_referrer_url();
+ inline void clear_has_referrer_url();
+ inline void set_has_complete();
+ inline void clear_has_complete();
+ inline void set_has_client_country();
+ inline void clear_has_client_country();
+ inline void set_has_did_proceed();
+ inline void clear_has_did_proceed();
+ inline void set_has_repeat_visit();
+ inline void clear_has_repeat_visit();
+ inline void set_has_token();
+ inline void clear_has_token();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int type_;
+ int download_verdict_;
+ ::std::string* url_;
+ ::std::string* page_url_;
+ ::std::string* referrer_url_;
+ ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_Resource > resources_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> client_asn_;
+ ::std::string* client_country_;
+ ::std::string* token_;
+ bool complete_;
+ bool did_proceed_;
+ bool repeat_visit_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+ friend void protobuf_ShutdownFile_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientSafeBrowsingReportRequest* default_instance_;
+};
+// ===================================================================
+
+
+// ===================================================================
+
+// ChromeUserPopulation
+
+// optional .safe_browsing.ChromeUserPopulation.UserPopulation user_population = 1;
+inline bool ChromeUserPopulation::has_user_population() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ChromeUserPopulation::set_has_user_population() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ChromeUserPopulation::clear_has_user_population() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ChromeUserPopulation::clear_user_population() {
+ user_population_ = 0;
+ clear_has_user_population();
+}
+inline ::safe_browsing::ChromeUserPopulation_UserPopulation ChromeUserPopulation::user_population() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ChromeUserPopulation.user_population)
+ return static_cast< ::safe_browsing::ChromeUserPopulation_UserPopulation >(user_population_);
+}
+inline void ChromeUserPopulation::set_user_population(::safe_browsing::ChromeUserPopulation_UserPopulation value) {
+ assert(::safe_browsing::ChromeUserPopulation_UserPopulation_IsValid(value));
+ set_has_user_population();
+ user_population_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ChromeUserPopulation.user_population)
+}
+
+// -------------------------------------------------------------------
+
+// ClientPhishingRequest_Feature
+
+// required string name = 1;
+inline bool ClientPhishingRequest_Feature::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientPhishingRequest_Feature::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientPhishingRequest_Feature::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientPhishingRequest_Feature::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ClientPhishingRequest_Feature::name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.Feature.name)
+ return *name_;
+}
+inline void ClientPhishingRequest_Feature::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.Feature.name)
+}
+inline void ClientPhishingRequest_Feature::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientPhishingRequest.Feature.name)
+}
+inline void ClientPhishingRequest_Feature::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientPhishingRequest.Feature.name)
+}
+inline ::std::string* ClientPhishingRequest_Feature::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.Feature.name)
+ return name_;
+}
+inline ::std::string* ClientPhishingRequest_Feature::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientPhishingRequest_Feature::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientPhishingRequest.Feature.name)
+}
+
+// required double value = 2;
+inline bool ClientPhishingRequest_Feature::has_value() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientPhishingRequest_Feature::set_has_value() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientPhishingRequest_Feature::clear_has_value() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientPhishingRequest_Feature::clear_value() {
+ value_ = 0;
+ clear_has_value();
+}
+inline double ClientPhishingRequest_Feature::value() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.Feature.value)
+ return value_;
+}
+inline void ClientPhishingRequest_Feature::set_value(double value) {
+ set_has_value();
+ value_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.Feature.value)
+}
+
+// -------------------------------------------------------------------
+
+// ClientPhishingRequest
+
+// optional string url = 1;
+inline bool ClientPhishingRequest::has_url() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientPhishingRequest::set_has_url() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientPhishingRequest::clear_has_url() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientPhishingRequest::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientPhishingRequest::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.url)
+ return *url_;
+}
+inline void ClientPhishingRequest::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.url)
+}
+inline void ClientPhishingRequest::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientPhishingRequest.url)
+}
+inline void ClientPhishingRequest::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientPhishingRequest.url)
+}
+inline ::std::string* ClientPhishingRequest::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.url)
+ return url_;
+}
+inline ::std::string* ClientPhishingRequest::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientPhishingRequest::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientPhishingRequest.url)
+}
+
+// optional bytes OBSOLETE_hash_prefix = 10;
+inline bool ClientPhishingRequest::has_obsolete_hash_prefix() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientPhishingRequest::set_has_obsolete_hash_prefix() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientPhishingRequest::clear_has_obsolete_hash_prefix() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientPhishingRequest::clear_obsolete_hash_prefix() {
+ if (obsolete_hash_prefix_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_hash_prefix_->clear();
+ }
+ clear_has_obsolete_hash_prefix();
+}
+inline const ::std::string& ClientPhishingRequest::obsolete_hash_prefix() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.OBSOLETE_hash_prefix)
+ return *obsolete_hash_prefix_;
+}
+inline void ClientPhishingRequest::set_obsolete_hash_prefix(const ::std::string& value) {
+ set_has_obsolete_hash_prefix();
+ if (obsolete_hash_prefix_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_hash_prefix_ = new ::std::string;
+ }
+ obsolete_hash_prefix_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.OBSOLETE_hash_prefix)
+}
+inline void ClientPhishingRequest::set_obsolete_hash_prefix(const char* value) {
+ set_has_obsolete_hash_prefix();
+ if (obsolete_hash_prefix_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_hash_prefix_ = new ::std::string;
+ }
+ obsolete_hash_prefix_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientPhishingRequest.OBSOLETE_hash_prefix)
+}
+inline void ClientPhishingRequest::set_obsolete_hash_prefix(const void* value, size_t size) {
+ set_has_obsolete_hash_prefix();
+ if (obsolete_hash_prefix_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_hash_prefix_ = new ::std::string;
+ }
+ obsolete_hash_prefix_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientPhishingRequest.OBSOLETE_hash_prefix)
+}
+inline ::std::string* ClientPhishingRequest::mutable_obsolete_hash_prefix() {
+ set_has_obsolete_hash_prefix();
+ if (obsolete_hash_prefix_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_hash_prefix_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.OBSOLETE_hash_prefix)
+ return obsolete_hash_prefix_;
+}
+inline ::std::string* ClientPhishingRequest::release_obsolete_hash_prefix() {
+ clear_has_obsolete_hash_prefix();
+ if (obsolete_hash_prefix_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = obsolete_hash_prefix_;
+ obsolete_hash_prefix_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientPhishingRequest::set_allocated_obsolete_hash_prefix(::std::string* obsolete_hash_prefix) {
+ if (obsolete_hash_prefix_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete obsolete_hash_prefix_;
+ }
+ if (obsolete_hash_prefix) {
+ set_has_obsolete_hash_prefix();
+ obsolete_hash_prefix_ = obsolete_hash_prefix;
+ } else {
+ clear_has_obsolete_hash_prefix();
+ obsolete_hash_prefix_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientPhishingRequest.OBSOLETE_hash_prefix)
+}
+
+// required float client_score = 2;
+inline bool ClientPhishingRequest::has_client_score() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientPhishingRequest::set_has_client_score() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientPhishingRequest::clear_has_client_score() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientPhishingRequest::clear_client_score() {
+ client_score_ = 0;
+ clear_has_client_score();
+}
+inline float ClientPhishingRequest::client_score() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.client_score)
+ return client_score_;
+}
+inline void ClientPhishingRequest::set_client_score(float value) {
+ set_has_client_score();
+ client_score_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.client_score)
+}
+
+// optional bool is_phishing = 4;
+inline bool ClientPhishingRequest::has_is_phishing() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientPhishingRequest::set_has_is_phishing() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientPhishingRequest::clear_has_is_phishing() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientPhishingRequest::clear_is_phishing() {
+ is_phishing_ = false;
+ clear_has_is_phishing();
+}
+inline bool ClientPhishingRequest::is_phishing() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.is_phishing)
+ return is_phishing_;
+}
+inline void ClientPhishingRequest::set_is_phishing(bool value) {
+ set_has_is_phishing();
+ is_phishing_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.is_phishing)
+}
+
+// repeated .safe_browsing.ClientPhishingRequest.Feature feature_map = 5;
+inline int ClientPhishingRequest::feature_map_size() const {
+ return feature_map_.size();
+}
+inline void ClientPhishingRequest::clear_feature_map() {
+ feature_map_.Clear();
+}
+inline const ::safe_browsing::ClientPhishingRequest_Feature& ClientPhishingRequest::feature_map(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.feature_map)
+ return feature_map_.Get(index);
+}
+inline ::safe_browsing::ClientPhishingRequest_Feature* ClientPhishingRequest::mutable_feature_map(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.feature_map)
+ return feature_map_.Mutable(index);
+}
+inline ::safe_browsing::ClientPhishingRequest_Feature* ClientPhishingRequest::add_feature_map() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientPhishingRequest.feature_map)
+ return feature_map_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >&
+ClientPhishingRequest::feature_map() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientPhishingRequest.feature_map)
+ return feature_map_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >*
+ClientPhishingRequest::mutable_feature_map() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientPhishingRequest.feature_map)
+ return &feature_map_;
+}
+
+// optional int32 model_version = 6;
+inline bool ClientPhishingRequest::has_model_version() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientPhishingRequest::set_has_model_version() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientPhishingRequest::clear_has_model_version() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientPhishingRequest::clear_model_version() {
+ model_version_ = 0;
+ clear_has_model_version();
+}
+inline ::google::protobuf::int32 ClientPhishingRequest::model_version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.model_version)
+ return model_version_;
+}
+inline void ClientPhishingRequest::set_model_version(::google::protobuf::int32 value) {
+ set_has_model_version();
+ model_version_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.model_version)
+}
+
+// repeated .safe_browsing.ClientPhishingRequest.Feature non_model_feature_map = 8;
+inline int ClientPhishingRequest::non_model_feature_map_size() const {
+ return non_model_feature_map_.size();
+}
+inline void ClientPhishingRequest::clear_non_model_feature_map() {
+ non_model_feature_map_.Clear();
+}
+inline const ::safe_browsing::ClientPhishingRequest_Feature& ClientPhishingRequest::non_model_feature_map(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.non_model_feature_map)
+ return non_model_feature_map_.Get(index);
+}
+inline ::safe_browsing::ClientPhishingRequest_Feature* ClientPhishingRequest::mutable_non_model_feature_map(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.non_model_feature_map)
+ return non_model_feature_map_.Mutable(index);
+}
+inline ::safe_browsing::ClientPhishingRequest_Feature* ClientPhishingRequest::add_non_model_feature_map() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientPhishingRequest.non_model_feature_map)
+ return non_model_feature_map_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >&
+ClientPhishingRequest::non_model_feature_map() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientPhishingRequest.non_model_feature_map)
+ return non_model_feature_map_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientPhishingRequest_Feature >*
+ClientPhishingRequest::mutable_non_model_feature_map() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientPhishingRequest.non_model_feature_map)
+ return &non_model_feature_map_;
+}
+
+// optional string OBSOLETE_referrer_url = 9;
+inline bool ClientPhishingRequest::has_obsolete_referrer_url() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void ClientPhishingRequest::set_has_obsolete_referrer_url() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void ClientPhishingRequest::clear_has_obsolete_referrer_url() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void ClientPhishingRequest::clear_obsolete_referrer_url() {
+ if (obsolete_referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_referrer_url_->clear();
+ }
+ clear_has_obsolete_referrer_url();
+}
+inline const ::std::string& ClientPhishingRequest::obsolete_referrer_url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.OBSOLETE_referrer_url)
+ return *obsolete_referrer_url_;
+}
+inline void ClientPhishingRequest::set_obsolete_referrer_url(const ::std::string& value) {
+ set_has_obsolete_referrer_url();
+ if (obsolete_referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_referrer_url_ = new ::std::string;
+ }
+ obsolete_referrer_url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.OBSOLETE_referrer_url)
+}
+inline void ClientPhishingRequest::set_obsolete_referrer_url(const char* value) {
+ set_has_obsolete_referrer_url();
+ if (obsolete_referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_referrer_url_ = new ::std::string;
+ }
+ obsolete_referrer_url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientPhishingRequest.OBSOLETE_referrer_url)
+}
+inline void ClientPhishingRequest::set_obsolete_referrer_url(const char* value, size_t size) {
+ set_has_obsolete_referrer_url();
+ if (obsolete_referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_referrer_url_ = new ::std::string;
+ }
+ obsolete_referrer_url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientPhishingRequest.OBSOLETE_referrer_url)
+}
+inline ::std::string* ClientPhishingRequest::mutable_obsolete_referrer_url() {
+ set_has_obsolete_referrer_url();
+ if (obsolete_referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ obsolete_referrer_url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.OBSOLETE_referrer_url)
+ return obsolete_referrer_url_;
+}
+inline ::std::string* ClientPhishingRequest::release_obsolete_referrer_url() {
+ clear_has_obsolete_referrer_url();
+ if (obsolete_referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = obsolete_referrer_url_;
+ obsolete_referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientPhishingRequest::set_allocated_obsolete_referrer_url(::std::string* obsolete_referrer_url) {
+ if (obsolete_referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete obsolete_referrer_url_;
+ }
+ if (obsolete_referrer_url) {
+ set_has_obsolete_referrer_url();
+ obsolete_referrer_url_ = obsolete_referrer_url;
+ } else {
+ clear_has_obsolete_referrer_url();
+ obsolete_referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientPhishingRequest.OBSOLETE_referrer_url)
+}
+
+// repeated uint32 shingle_hashes = 12 [packed = true];
+inline int ClientPhishingRequest::shingle_hashes_size() const {
+ return shingle_hashes_.size();
+}
+inline void ClientPhishingRequest::clear_shingle_hashes() {
+ shingle_hashes_.Clear();
+}
+inline ::google::protobuf::uint32 ClientPhishingRequest::shingle_hashes(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.shingle_hashes)
+ return shingle_hashes_.Get(index);
+}
+inline void ClientPhishingRequest::set_shingle_hashes(int index, ::google::protobuf::uint32 value) {
+ shingle_hashes_.Set(index, value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.shingle_hashes)
+}
+inline void ClientPhishingRequest::add_shingle_hashes(::google::protobuf::uint32 value) {
+ shingle_hashes_.Add(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientPhishingRequest.shingle_hashes)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::uint32 >&
+ClientPhishingRequest::shingle_hashes() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientPhishingRequest.shingle_hashes)
+ return shingle_hashes_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::uint32 >*
+ClientPhishingRequest::mutable_shingle_hashes() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientPhishingRequest.shingle_hashes)
+ return &shingle_hashes_;
+}
+
+// optional string model_filename = 13;
+inline bool ClientPhishingRequest::has_model_filename() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void ClientPhishingRequest::set_has_model_filename() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void ClientPhishingRequest::clear_has_model_filename() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void ClientPhishingRequest::clear_model_filename() {
+ if (model_filename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ model_filename_->clear();
+ }
+ clear_has_model_filename();
+}
+inline const ::std::string& ClientPhishingRequest::model_filename() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.model_filename)
+ return *model_filename_;
+}
+inline void ClientPhishingRequest::set_model_filename(const ::std::string& value) {
+ set_has_model_filename();
+ if (model_filename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ model_filename_ = new ::std::string;
+ }
+ model_filename_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingRequest.model_filename)
+}
+inline void ClientPhishingRequest::set_model_filename(const char* value) {
+ set_has_model_filename();
+ if (model_filename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ model_filename_ = new ::std::string;
+ }
+ model_filename_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientPhishingRequest.model_filename)
+}
+inline void ClientPhishingRequest::set_model_filename(const char* value, size_t size) {
+ set_has_model_filename();
+ if (model_filename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ model_filename_ = new ::std::string;
+ }
+ model_filename_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientPhishingRequest.model_filename)
+}
+inline ::std::string* ClientPhishingRequest::mutable_model_filename() {
+ set_has_model_filename();
+ if (model_filename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ model_filename_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.model_filename)
+ return model_filename_;
+}
+inline ::std::string* ClientPhishingRequest::release_model_filename() {
+ clear_has_model_filename();
+ if (model_filename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = model_filename_;
+ model_filename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientPhishingRequest::set_allocated_model_filename(::std::string* model_filename) {
+ if (model_filename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete model_filename_;
+ }
+ if (model_filename) {
+ set_has_model_filename();
+ model_filename_ = model_filename;
+ } else {
+ clear_has_model_filename();
+ model_filename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientPhishingRequest.model_filename)
+}
+
+// optional .safe_browsing.ChromeUserPopulation population = 14;
+inline bool ClientPhishingRequest::has_population() const {
+ return (_has_bits_[0] & 0x00000400u) != 0;
+}
+inline void ClientPhishingRequest::set_has_population() {
+ _has_bits_[0] |= 0x00000400u;
+}
+inline void ClientPhishingRequest::clear_has_population() {
+ _has_bits_[0] &= ~0x00000400u;
+}
+inline void ClientPhishingRequest::clear_population() {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ clear_has_population();
+}
+inline const ::safe_browsing::ChromeUserPopulation& ClientPhishingRequest::population() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingRequest.population)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return population_ != NULL ? *population_ : *default_instance().population_;
+#else
+ return population_ != NULL ? *population_ : *default_instance_->population_;
+#endif
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientPhishingRequest::mutable_population() {
+ set_has_population();
+ if (population_ == NULL) population_ = new ::safe_browsing::ChromeUserPopulation;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingRequest.population)
+ return population_;
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientPhishingRequest::release_population() {
+ clear_has_population();
+ ::safe_browsing::ChromeUserPopulation* temp = population_;
+ population_ = NULL;
+ return temp;
+}
+inline void ClientPhishingRequest::set_allocated_population(::safe_browsing::ChromeUserPopulation* population) {
+ delete population_;
+ population_ = population;
+ if (population) {
+ set_has_population();
+ } else {
+ clear_has_population();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientPhishingRequest.population)
+}
+
+// -------------------------------------------------------------------
+
+// ClientPhishingResponse
+
+// required bool phishy = 1;
+inline bool ClientPhishingResponse::has_phishy() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientPhishingResponse::set_has_phishy() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientPhishingResponse::clear_has_phishy() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientPhishingResponse::clear_phishy() {
+ phishy_ = false;
+ clear_has_phishy();
+}
+inline bool ClientPhishingResponse::phishy() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingResponse.phishy)
+ return phishy_;
+}
+inline void ClientPhishingResponse::set_phishy(bool value) {
+ set_has_phishy();
+ phishy_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingResponse.phishy)
+}
+
+// repeated string OBSOLETE_whitelist_expression = 2;
+inline int ClientPhishingResponse::obsolete_whitelist_expression_size() const {
+ return obsolete_whitelist_expression_.size();
+}
+inline void ClientPhishingResponse::clear_obsolete_whitelist_expression() {
+ obsolete_whitelist_expression_.Clear();
+}
+inline const ::std::string& ClientPhishingResponse::obsolete_whitelist_expression(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+ return obsolete_whitelist_expression_.Get(index);
+}
+inline ::std::string* ClientPhishingResponse::mutable_obsolete_whitelist_expression(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+ return obsolete_whitelist_expression_.Mutable(index);
+}
+inline void ClientPhishingResponse::set_obsolete_whitelist_expression(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+ obsolete_whitelist_expression_.Mutable(index)->assign(value);
+}
+inline void ClientPhishingResponse::set_obsolete_whitelist_expression(int index, const char* value) {
+ obsolete_whitelist_expression_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+}
+inline void ClientPhishingResponse::set_obsolete_whitelist_expression(int index, const char* value, size_t size) {
+ obsolete_whitelist_expression_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+}
+inline ::std::string* ClientPhishingResponse::add_obsolete_whitelist_expression() {
+ return obsolete_whitelist_expression_.Add();
+}
+inline void ClientPhishingResponse::add_obsolete_whitelist_expression(const ::std::string& value) {
+ obsolete_whitelist_expression_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+}
+inline void ClientPhishingResponse::add_obsolete_whitelist_expression(const char* value) {
+ obsolete_whitelist_expression_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+}
+inline void ClientPhishingResponse::add_obsolete_whitelist_expression(const char* value, size_t size) {
+ obsolete_whitelist_expression_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientPhishingResponse::obsolete_whitelist_expression() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+ return obsolete_whitelist_expression_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientPhishingResponse::mutable_obsolete_whitelist_expression() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientPhishingResponse.OBSOLETE_whitelist_expression)
+ return &obsolete_whitelist_expression_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientMalwareRequest_UrlInfo
+
+// required string ip = 1;
+inline bool ClientMalwareRequest_UrlInfo::has_ip() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientMalwareRequest_UrlInfo::set_has_ip() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_has_ip() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_ip() {
+ if (ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_->clear();
+ }
+ clear_has_ip();
+}
+inline const ::std::string& ClientMalwareRequest_UrlInfo::ip() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.UrlInfo.ip)
+ return *ip_;
+}
+inline void ClientMalwareRequest_UrlInfo::set_ip(const ::std::string& value) {
+ set_has_ip();
+ if (ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_ = new ::std::string;
+ }
+ ip_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.UrlInfo.ip)
+}
+inline void ClientMalwareRequest_UrlInfo::set_ip(const char* value) {
+ set_has_ip();
+ if (ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_ = new ::std::string;
+ }
+ ip_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareRequest.UrlInfo.ip)
+}
+inline void ClientMalwareRequest_UrlInfo::set_ip(const char* value, size_t size) {
+ set_has_ip();
+ if (ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_ = new ::std::string;
+ }
+ ip_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareRequest.UrlInfo.ip)
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::mutable_ip() {
+ set_has_ip();
+ if (ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.UrlInfo.ip)
+ return ip_;
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::release_ip() {
+ clear_has_ip();
+ if (ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = ip_;
+ ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareRequest_UrlInfo::set_allocated_ip(::std::string* ip) {
+ if (ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete ip_;
+ }
+ if (ip) {
+ set_has_ip();
+ ip_ = ip;
+ } else {
+ clear_has_ip();
+ ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.UrlInfo.ip)
+}
+
+// required string url = 2;
+inline bool ClientMalwareRequest_UrlInfo::has_url() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientMalwareRequest_UrlInfo::set_has_url() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_has_url() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientMalwareRequest_UrlInfo::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.UrlInfo.url)
+ return *url_;
+}
+inline void ClientMalwareRequest_UrlInfo::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.UrlInfo.url)
+}
+inline void ClientMalwareRequest_UrlInfo::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareRequest.UrlInfo.url)
+}
+inline void ClientMalwareRequest_UrlInfo::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareRequest.UrlInfo.url)
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.UrlInfo.url)
+ return url_;
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareRequest_UrlInfo::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.UrlInfo.url)
+}
+
+// optional string method = 3;
+inline bool ClientMalwareRequest_UrlInfo::has_method() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientMalwareRequest_UrlInfo::set_has_method() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_has_method() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_method() {
+ if (method_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ method_->clear();
+ }
+ clear_has_method();
+}
+inline const ::std::string& ClientMalwareRequest_UrlInfo::method() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.UrlInfo.method)
+ return *method_;
+}
+inline void ClientMalwareRequest_UrlInfo::set_method(const ::std::string& value) {
+ set_has_method();
+ if (method_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ method_ = new ::std::string;
+ }
+ method_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.UrlInfo.method)
+}
+inline void ClientMalwareRequest_UrlInfo::set_method(const char* value) {
+ set_has_method();
+ if (method_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ method_ = new ::std::string;
+ }
+ method_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareRequest.UrlInfo.method)
+}
+inline void ClientMalwareRequest_UrlInfo::set_method(const char* value, size_t size) {
+ set_has_method();
+ if (method_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ method_ = new ::std::string;
+ }
+ method_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareRequest.UrlInfo.method)
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::mutable_method() {
+ set_has_method();
+ if (method_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ method_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.UrlInfo.method)
+ return method_;
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::release_method() {
+ clear_has_method();
+ if (method_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = method_;
+ method_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareRequest_UrlInfo::set_allocated_method(::std::string* method) {
+ if (method_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete method_;
+ }
+ if (method) {
+ set_has_method();
+ method_ = method;
+ } else {
+ clear_has_method();
+ method_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.UrlInfo.method)
+}
+
+// optional string referrer = 4;
+inline bool ClientMalwareRequest_UrlInfo::has_referrer() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientMalwareRequest_UrlInfo::set_has_referrer() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_has_referrer() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_referrer() {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ clear_has_referrer();
+}
+inline const ::std::string& ClientMalwareRequest_UrlInfo::referrer() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.UrlInfo.referrer)
+ return *referrer_;
+}
+inline void ClientMalwareRequest_UrlInfo::set_referrer(const ::std::string& value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.UrlInfo.referrer)
+}
+inline void ClientMalwareRequest_UrlInfo::set_referrer(const char* value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareRequest.UrlInfo.referrer)
+}
+inline void ClientMalwareRequest_UrlInfo::set_referrer(const char* value, size_t size) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareRequest.UrlInfo.referrer)
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::mutable_referrer() {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.UrlInfo.referrer)
+ return referrer_;
+}
+inline ::std::string* ClientMalwareRequest_UrlInfo::release_referrer() {
+ clear_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = referrer_;
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareRequest_UrlInfo::set_allocated_referrer(::std::string* referrer) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ if (referrer) {
+ set_has_referrer();
+ referrer_ = referrer;
+ } else {
+ clear_has_referrer();
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.UrlInfo.referrer)
+}
+
+// optional int32 resource_type = 5;
+inline bool ClientMalwareRequest_UrlInfo::has_resource_type() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientMalwareRequest_UrlInfo::set_has_resource_type() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_has_resource_type() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientMalwareRequest_UrlInfo::clear_resource_type() {
+ resource_type_ = 0;
+ clear_has_resource_type();
+}
+inline ::google::protobuf::int32 ClientMalwareRequest_UrlInfo::resource_type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.UrlInfo.resource_type)
+ return resource_type_;
+}
+inline void ClientMalwareRequest_UrlInfo::set_resource_type(::google::protobuf::int32 value) {
+ set_has_resource_type();
+ resource_type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.UrlInfo.resource_type)
+}
+
+// -------------------------------------------------------------------
+
+// ClientMalwareRequest
+
+// required string url = 1;
+inline bool ClientMalwareRequest::has_url() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientMalwareRequest::set_has_url() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientMalwareRequest::clear_has_url() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientMalwareRequest::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientMalwareRequest::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.url)
+ return *url_;
+}
+inline void ClientMalwareRequest::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.url)
+}
+inline void ClientMalwareRequest::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareRequest.url)
+}
+inline void ClientMalwareRequest::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareRequest.url)
+}
+inline ::std::string* ClientMalwareRequest::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.url)
+ return url_;
+}
+inline ::std::string* ClientMalwareRequest::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareRequest::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.url)
+}
+
+// optional string referrer_url = 4;
+inline bool ClientMalwareRequest::has_referrer_url() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientMalwareRequest::set_has_referrer_url() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientMalwareRequest::clear_has_referrer_url() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientMalwareRequest::clear_referrer_url() {
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_->clear();
+ }
+ clear_has_referrer_url();
+}
+inline const ::std::string& ClientMalwareRequest::referrer_url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.referrer_url)
+ return *referrer_url_;
+}
+inline void ClientMalwareRequest::set_referrer_url(const ::std::string& value) {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ referrer_url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareRequest.referrer_url)
+}
+inline void ClientMalwareRequest::set_referrer_url(const char* value) {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ referrer_url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareRequest.referrer_url)
+}
+inline void ClientMalwareRequest::set_referrer_url(const char* value, size_t size) {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ referrer_url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareRequest.referrer_url)
+}
+inline ::std::string* ClientMalwareRequest::mutable_referrer_url() {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.referrer_url)
+ return referrer_url_;
+}
+inline ::std::string* ClientMalwareRequest::release_referrer_url() {
+ clear_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = referrer_url_;
+ referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareRequest::set_allocated_referrer_url(::std::string* referrer_url) {
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_url_;
+ }
+ if (referrer_url) {
+ set_has_referrer_url();
+ referrer_url_ = referrer_url;
+ } else {
+ clear_has_referrer_url();
+ referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.referrer_url)
+}
+
+// repeated .safe_browsing.ClientMalwareRequest.UrlInfo bad_ip_url_info = 7;
+inline int ClientMalwareRequest::bad_ip_url_info_size() const {
+ return bad_ip_url_info_.size();
+}
+inline void ClientMalwareRequest::clear_bad_ip_url_info() {
+ bad_ip_url_info_.Clear();
+}
+inline const ::safe_browsing::ClientMalwareRequest_UrlInfo& ClientMalwareRequest::bad_ip_url_info(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.bad_ip_url_info)
+ return bad_ip_url_info_.Get(index);
+}
+inline ::safe_browsing::ClientMalwareRequest_UrlInfo* ClientMalwareRequest::mutable_bad_ip_url_info(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.bad_ip_url_info)
+ return bad_ip_url_info_.Mutable(index);
+}
+inline ::safe_browsing::ClientMalwareRequest_UrlInfo* ClientMalwareRequest::add_bad_ip_url_info() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientMalwareRequest.bad_ip_url_info)
+ return bad_ip_url_info_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientMalwareRequest_UrlInfo >&
+ClientMalwareRequest::bad_ip_url_info() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientMalwareRequest.bad_ip_url_info)
+ return bad_ip_url_info_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientMalwareRequest_UrlInfo >*
+ClientMalwareRequest::mutable_bad_ip_url_info() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientMalwareRequest.bad_ip_url_info)
+ return &bad_ip_url_info_;
+}
+
+// optional .safe_browsing.ChromeUserPopulation population = 9;
+inline bool ClientMalwareRequest::has_population() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientMalwareRequest::set_has_population() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientMalwareRequest::clear_has_population() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientMalwareRequest::clear_population() {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ clear_has_population();
+}
+inline const ::safe_browsing::ChromeUserPopulation& ClientMalwareRequest::population() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareRequest.population)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return population_ != NULL ? *population_ : *default_instance().population_;
+#else
+ return population_ != NULL ? *population_ : *default_instance_->population_;
+#endif
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientMalwareRequest::mutable_population() {
+ set_has_population();
+ if (population_ == NULL) population_ = new ::safe_browsing::ChromeUserPopulation;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareRequest.population)
+ return population_;
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientMalwareRequest::release_population() {
+ clear_has_population();
+ ::safe_browsing::ChromeUserPopulation* temp = population_;
+ population_ = NULL;
+ return temp;
+}
+inline void ClientMalwareRequest::set_allocated_population(::safe_browsing::ChromeUserPopulation* population) {
+ delete population_;
+ population_ = population;
+ if (population) {
+ set_has_population();
+ } else {
+ clear_has_population();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareRequest.population)
+}
+
+// -------------------------------------------------------------------
+
+// ClientMalwareResponse
+
+// required bool blacklist = 1;
+inline bool ClientMalwareResponse::has_blacklist() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientMalwareResponse::set_has_blacklist() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientMalwareResponse::clear_has_blacklist() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientMalwareResponse::clear_blacklist() {
+ blacklist_ = false;
+ clear_has_blacklist();
+}
+inline bool ClientMalwareResponse::blacklist() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareResponse.blacklist)
+ return blacklist_;
+}
+inline void ClientMalwareResponse::set_blacklist(bool value) {
+ set_has_blacklist();
+ blacklist_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareResponse.blacklist)
+}
+
+// optional string bad_ip = 2;
+inline bool ClientMalwareResponse::has_bad_ip() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientMalwareResponse::set_has_bad_ip() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientMalwareResponse::clear_has_bad_ip() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientMalwareResponse::clear_bad_ip() {
+ if (bad_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_ip_->clear();
+ }
+ clear_has_bad_ip();
+}
+inline const ::std::string& ClientMalwareResponse::bad_ip() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareResponse.bad_ip)
+ return *bad_ip_;
+}
+inline void ClientMalwareResponse::set_bad_ip(const ::std::string& value) {
+ set_has_bad_ip();
+ if (bad_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_ip_ = new ::std::string;
+ }
+ bad_ip_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareResponse.bad_ip)
+}
+inline void ClientMalwareResponse::set_bad_ip(const char* value) {
+ set_has_bad_ip();
+ if (bad_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_ip_ = new ::std::string;
+ }
+ bad_ip_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareResponse.bad_ip)
+}
+inline void ClientMalwareResponse::set_bad_ip(const char* value, size_t size) {
+ set_has_bad_ip();
+ if (bad_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_ip_ = new ::std::string;
+ }
+ bad_ip_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareResponse.bad_ip)
+}
+inline ::std::string* ClientMalwareResponse::mutable_bad_ip() {
+ set_has_bad_ip();
+ if (bad_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_ip_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareResponse.bad_ip)
+ return bad_ip_;
+}
+inline ::std::string* ClientMalwareResponse::release_bad_ip() {
+ clear_has_bad_ip();
+ if (bad_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = bad_ip_;
+ bad_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareResponse::set_allocated_bad_ip(::std::string* bad_ip) {
+ if (bad_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bad_ip_;
+ }
+ if (bad_ip) {
+ set_has_bad_ip();
+ bad_ip_ = bad_ip;
+ } else {
+ clear_has_bad_ip();
+ bad_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareResponse.bad_ip)
+}
+
+// optional string bad_url = 3;
+inline bool ClientMalwareResponse::has_bad_url() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientMalwareResponse::set_has_bad_url() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientMalwareResponse::clear_has_bad_url() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientMalwareResponse::clear_bad_url() {
+ if (bad_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_url_->clear();
+ }
+ clear_has_bad_url();
+}
+inline const ::std::string& ClientMalwareResponse::bad_url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientMalwareResponse.bad_url)
+ return *bad_url_;
+}
+inline void ClientMalwareResponse::set_bad_url(const ::std::string& value) {
+ set_has_bad_url();
+ if (bad_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_url_ = new ::std::string;
+ }
+ bad_url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientMalwareResponse.bad_url)
+}
+inline void ClientMalwareResponse::set_bad_url(const char* value) {
+ set_has_bad_url();
+ if (bad_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_url_ = new ::std::string;
+ }
+ bad_url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientMalwareResponse.bad_url)
+}
+inline void ClientMalwareResponse::set_bad_url(const char* value, size_t size) {
+ set_has_bad_url();
+ if (bad_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_url_ = new ::std::string;
+ }
+ bad_url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientMalwareResponse.bad_url)
+}
+inline ::std::string* ClientMalwareResponse::mutable_bad_url() {
+ set_has_bad_url();
+ if (bad_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bad_url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientMalwareResponse.bad_url)
+ return bad_url_;
+}
+inline ::std::string* ClientMalwareResponse::release_bad_url() {
+ clear_has_bad_url();
+ if (bad_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = bad_url_;
+ bad_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientMalwareResponse::set_allocated_bad_url(::std::string* bad_url) {
+ if (bad_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bad_url_;
+ }
+ if (bad_url) {
+ set_has_bad_url();
+ bad_url_ = bad_url;
+ } else {
+ clear_has_bad_url();
+ bad_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientMalwareResponse.bad_url)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_Digests
+
+// optional bytes sha256 = 1;
+inline bool ClientDownloadRequest_Digests::has_sha256() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_Digests::set_has_sha256() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_Digests::clear_has_sha256() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_Digests::clear_sha256() {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_->clear();
+ }
+ clear_has_sha256();
+}
+inline const ::std::string& ClientDownloadRequest_Digests::sha256() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Digests.sha256)
+ return *sha256_;
+}
+inline void ClientDownloadRequest_Digests::set_sha256(const ::std::string& value) {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ sha256_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Digests.sha256)
+}
+inline void ClientDownloadRequest_Digests::set_sha256(const char* value) {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ sha256_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.Digests.sha256)
+}
+inline void ClientDownloadRequest_Digests::set_sha256(const void* value, size_t size) {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ sha256_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.Digests.sha256)
+}
+inline ::std::string* ClientDownloadRequest_Digests::mutable_sha256() {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.Digests.sha256)
+ return sha256_;
+}
+inline ::std::string* ClientDownloadRequest_Digests::release_sha256() {
+ clear_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = sha256_;
+ sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_Digests::set_allocated_sha256(::std::string* sha256) {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete sha256_;
+ }
+ if (sha256) {
+ set_has_sha256();
+ sha256_ = sha256;
+ } else {
+ clear_has_sha256();
+ sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.Digests.sha256)
+}
+
+// optional bytes sha1 = 2;
+inline bool ClientDownloadRequest_Digests::has_sha1() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_Digests::set_has_sha1() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_Digests::clear_has_sha1() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_Digests::clear_sha1() {
+ if (sha1_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha1_->clear();
+ }
+ clear_has_sha1();
+}
+inline const ::std::string& ClientDownloadRequest_Digests::sha1() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Digests.sha1)
+ return *sha1_;
+}
+inline void ClientDownloadRequest_Digests::set_sha1(const ::std::string& value) {
+ set_has_sha1();
+ if (sha1_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha1_ = new ::std::string;
+ }
+ sha1_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Digests.sha1)
+}
+inline void ClientDownloadRequest_Digests::set_sha1(const char* value) {
+ set_has_sha1();
+ if (sha1_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha1_ = new ::std::string;
+ }
+ sha1_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.Digests.sha1)
+}
+inline void ClientDownloadRequest_Digests::set_sha1(const void* value, size_t size) {
+ set_has_sha1();
+ if (sha1_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha1_ = new ::std::string;
+ }
+ sha1_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.Digests.sha1)
+}
+inline ::std::string* ClientDownloadRequest_Digests::mutable_sha1() {
+ set_has_sha1();
+ if (sha1_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha1_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.Digests.sha1)
+ return sha1_;
+}
+inline ::std::string* ClientDownloadRequest_Digests::release_sha1() {
+ clear_has_sha1();
+ if (sha1_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = sha1_;
+ sha1_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_Digests::set_allocated_sha1(::std::string* sha1) {
+ if (sha1_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete sha1_;
+ }
+ if (sha1) {
+ set_has_sha1();
+ sha1_ = sha1;
+ } else {
+ clear_has_sha1();
+ sha1_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.Digests.sha1)
+}
+
+// optional bytes md5 = 3;
+inline bool ClientDownloadRequest_Digests::has_md5() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadRequest_Digests::set_has_md5() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadRequest_Digests::clear_has_md5() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadRequest_Digests::clear_md5() {
+ if (md5_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ md5_->clear();
+ }
+ clear_has_md5();
+}
+inline const ::std::string& ClientDownloadRequest_Digests::md5() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Digests.md5)
+ return *md5_;
+}
+inline void ClientDownloadRequest_Digests::set_md5(const ::std::string& value) {
+ set_has_md5();
+ if (md5_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ md5_ = new ::std::string;
+ }
+ md5_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Digests.md5)
+}
+inline void ClientDownloadRequest_Digests::set_md5(const char* value) {
+ set_has_md5();
+ if (md5_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ md5_ = new ::std::string;
+ }
+ md5_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.Digests.md5)
+}
+inline void ClientDownloadRequest_Digests::set_md5(const void* value, size_t size) {
+ set_has_md5();
+ if (md5_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ md5_ = new ::std::string;
+ }
+ md5_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.Digests.md5)
+}
+inline ::std::string* ClientDownloadRequest_Digests::mutable_md5() {
+ set_has_md5();
+ if (md5_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ md5_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.Digests.md5)
+ return md5_;
+}
+inline ::std::string* ClientDownloadRequest_Digests::release_md5() {
+ clear_has_md5();
+ if (md5_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = md5_;
+ md5_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_Digests::set_allocated_md5(::std::string* md5) {
+ if (md5_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete md5_;
+ }
+ if (md5) {
+ set_has_md5();
+ md5_ = md5;
+ } else {
+ clear_has_md5();
+ md5_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.Digests.md5)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_Resource
+
+// required string url = 1;
+inline bool ClientDownloadRequest_Resource::has_url() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_Resource::set_has_url() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_Resource::clear_has_url() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_Resource::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientDownloadRequest_Resource::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Resource.url)
+ return *url_;
+}
+inline void ClientDownloadRequest_Resource::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Resource.url)
+}
+inline void ClientDownloadRequest_Resource::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.Resource.url)
+}
+inline void ClientDownloadRequest_Resource::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.Resource.url)
+}
+inline ::std::string* ClientDownloadRequest_Resource::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.Resource.url)
+ return url_;
+}
+inline ::std::string* ClientDownloadRequest_Resource::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_Resource::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.Resource.url)
+}
+
+// required .safe_browsing.ClientDownloadRequest.ResourceType type = 2;
+inline bool ClientDownloadRequest_Resource::has_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_Resource::set_has_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_Resource::clear_has_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_Resource::clear_type() {
+ type_ = 0;
+ clear_has_type();
+}
+inline ::safe_browsing::ClientDownloadRequest_ResourceType ClientDownloadRequest_Resource::type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Resource.type)
+ return static_cast< ::safe_browsing::ClientDownloadRequest_ResourceType >(type_);
+}
+inline void ClientDownloadRequest_Resource::set_type(::safe_browsing::ClientDownloadRequest_ResourceType value) {
+ assert(::safe_browsing::ClientDownloadRequest_ResourceType_IsValid(value));
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Resource.type)
+}
+
+// optional bytes remote_ip = 3;
+inline bool ClientDownloadRequest_Resource::has_remote_ip() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadRequest_Resource::set_has_remote_ip() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadRequest_Resource::clear_has_remote_ip() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadRequest_Resource::clear_remote_ip() {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_->clear();
+ }
+ clear_has_remote_ip();
+}
+inline const ::std::string& ClientDownloadRequest_Resource::remote_ip() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Resource.remote_ip)
+ return *remote_ip_;
+}
+inline void ClientDownloadRequest_Resource::set_remote_ip(const ::std::string& value) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Resource.remote_ip)
+}
+inline void ClientDownloadRequest_Resource::set_remote_ip(const char* value) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.Resource.remote_ip)
+}
+inline void ClientDownloadRequest_Resource::set_remote_ip(const void* value, size_t size) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.Resource.remote_ip)
+}
+inline ::std::string* ClientDownloadRequest_Resource::mutable_remote_ip() {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.Resource.remote_ip)
+ return remote_ip_;
+}
+inline ::std::string* ClientDownloadRequest_Resource::release_remote_ip() {
+ clear_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = remote_ip_;
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_Resource::set_allocated_remote_ip(::std::string* remote_ip) {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete remote_ip_;
+ }
+ if (remote_ip) {
+ set_has_remote_ip();
+ remote_ip_ = remote_ip;
+ } else {
+ clear_has_remote_ip();
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.Resource.remote_ip)
+}
+
+// optional string referrer = 4;
+inline bool ClientDownloadRequest_Resource::has_referrer() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientDownloadRequest_Resource::set_has_referrer() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientDownloadRequest_Resource::clear_has_referrer() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientDownloadRequest_Resource::clear_referrer() {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ clear_has_referrer();
+}
+inline const ::std::string& ClientDownloadRequest_Resource::referrer() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.Resource.referrer)
+ return *referrer_;
+}
+inline void ClientDownloadRequest_Resource::set_referrer(const ::std::string& value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.Resource.referrer)
+}
+inline void ClientDownloadRequest_Resource::set_referrer(const char* value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.Resource.referrer)
+}
+inline void ClientDownloadRequest_Resource::set_referrer(const char* value, size_t size) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.Resource.referrer)
+}
+inline ::std::string* ClientDownloadRequest_Resource::mutable_referrer() {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.Resource.referrer)
+ return referrer_;
+}
+inline ::std::string* ClientDownloadRequest_Resource::release_referrer() {
+ clear_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = referrer_;
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_Resource::set_allocated_referrer(::std::string* referrer) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ if (referrer) {
+ set_has_referrer();
+ referrer_ = referrer;
+ } else {
+ clear_has_referrer();
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.Resource.referrer)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_CertificateChain_Element
+
+// optional bytes certificate = 1;
+inline bool ClientDownloadRequest_CertificateChain_Element::has_certificate() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_CertificateChain_Element::set_has_certificate() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_CertificateChain_Element::clear_has_certificate() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_CertificateChain_Element::clear_certificate() {
+ if (certificate_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ certificate_->clear();
+ }
+ clear_has_certificate();
+}
+inline const ::std::string& ClientDownloadRequest_CertificateChain_Element::certificate() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.CertificateChain.Element.certificate)
+ return *certificate_;
+}
+inline void ClientDownloadRequest_CertificateChain_Element::set_certificate(const ::std::string& value) {
+ set_has_certificate();
+ if (certificate_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ certificate_ = new ::std::string;
+ }
+ certificate_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.CertificateChain.Element.certificate)
+}
+inline void ClientDownloadRequest_CertificateChain_Element::set_certificate(const char* value) {
+ set_has_certificate();
+ if (certificate_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ certificate_ = new ::std::string;
+ }
+ certificate_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.CertificateChain.Element.certificate)
+}
+inline void ClientDownloadRequest_CertificateChain_Element::set_certificate(const void* value, size_t size) {
+ set_has_certificate();
+ if (certificate_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ certificate_ = new ::std::string;
+ }
+ certificate_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.CertificateChain.Element.certificate)
+}
+inline ::std::string* ClientDownloadRequest_CertificateChain_Element::mutable_certificate() {
+ set_has_certificate();
+ if (certificate_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ certificate_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.CertificateChain.Element.certificate)
+ return certificate_;
+}
+inline ::std::string* ClientDownloadRequest_CertificateChain_Element::release_certificate() {
+ clear_has_certificate();
+ if (certificate_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = certificate_;
+ certificate_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_CertificateChain_Element::set_allocated_certificate(::std::string* certificate) {
+ if (certificate_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete certificate_;
+ }
+ if (certificate) {
+ set_has_certificate();
+ certificate_ = certificate;
+ } else {
+ clear_has_certificate();
+ certificate_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.CertificateChain.Element.certificate)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_CertificateChain
+
+// repeated .safe_browsing.ClientDownloadRequest.CertificateChain.Element element = 1;
+inline int ClientDownloadRequest_CertificateChain::element_size() const {
+ return element_.size();
+}
+inline void ClientDownloadRequest_CertificateChain::clear_element() {
+ element_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_CertificateChain_Element& ClientDownloadRequest_CertificateChain::element(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.CertificateChain.element)
+ return element_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_CertificateChain_Element* ClientDownloadRequest_CertificateChain::mutable_element(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.CertificateChain.element)
+ return element_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_CertificateChain_Element* ClientDownloadRequest_CertificateChain::add_element() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.CertificateChain.element)
+ return element_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain_Element >&
+ClientDownloadRequest_CertificateChain::element() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.CertificateChain.element)
+ return element_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain_Element >*
+ClientDownloadRequest_CertificateChain::mutable_element() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.CertificateChain.element)
+ return &element_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_ExtendedAttr
+
+// required string key = 1;
+inline bool ClientDownloadRequest_ExtendedAttr::has_key() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_has_key() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_ExtendedAttr::clear_has_key() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_ExtendedAttr::clear_key() {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_->clear();
+ }
+ clear_has_key();
+}
+inline const ::std::string& ClientDownloadRequest_ExtendedAttr::key() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ExtendedAttr.key)
+ return *key_;
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_key(const ::std::string& value) {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ key_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.ExtendedAttr.key)
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_key(const char* value) {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ key_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.ExtendedAttr.key)
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_key(const char* value, size_t size) {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ key_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.ExtendedAttr.key)
+}
+inline ::std::string* ClientDownloadRequest_ExtendedAttr::mutable_key() {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ExtendedAttr.key)
+ return key_;
+}
+inline ::std::string* ClientDownloadRequest_ExtendedAttr::release_key() {
+ clear_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = key_;
+ key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_allocated_key(::std::string* key) {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete key_;
+ }
+ if (key) {
+ set_has_key();
+ key_ = key;
+ } else {
+ clear_has_key();
+ key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ExtendedAttr.key)
+}
+
+// optional bytes value = 2;
+inline bool ClientDownloadRequest_ExtendedAttr::has_value() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_has_value() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_ExtendedAttr::clear_has_value() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_ExtendedAttr::clear_value() {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_->clear();
+ }
+ clear_has_value();
+}
+inline const ::std::string& ClientDownloadRequest_ExtendedAttr::value() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ExtendedAttr.value)
+ return *value_;
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_value(const ::std::string& value) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.ExtendedAttr.value)
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_value(const char* value) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.ExtendedAttr.value)
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_value(const void* value, size_t size) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.ExtendedAttr.value)
+}
+inline ::std::string* ClientDownloadRequest_ExtendedAttr::mutable_value() {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ExtendedAttr.value)
+ return value_;
+}
+inline ::std::string* ClientDownloadRequest_ExtendedAttr::release_value() {
+ clear_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = value_;
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_ExtendedAttr::set_allocated_value(::std::string* value) {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete value_;
+ }
+ if (value) {
+ set_has_value();
+ value_ = value;
+ } else {
+ clear_has_value();
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ExtendedAttr.value)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_SignatureInfo
+
+// repeated .safe_browsing.ClientDownloadRequest.CertificateChain certificate_chain = 1;
+inline int ClientDownloadRequest_SignatureInfo::certificate_chain_size() const {
+ return certificate_chain_.size();
+}
+inline void ClientDownloadRequest_SignatureInfo::clear_certificate_chain() {
+ certificate_chain_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_CertificateChain& ClientDownloadRequest_SignatureInfo::certificate_chain(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.SignatureInfo.certificate_chain)
+ return certificate_chain_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_CertificateChain* ClientDownloadRequest_SignatureInfo::mutable_certificate_chain(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.SignatureInfo.certificate_chain)
+ return certificate_chain_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_CertificateChain* ClientDownloadRequest_SignatureInfo::add_certificate_chain() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.SignatureInfo.certificate_chain)
+ return certificate_chain_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain >&
+ClientDownloadRequest_SignatureInfo::certificate_chain() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.SignatureInfo.certificate_chain)
+ return certificate_chain_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_CertificateChain >*
+ClientDownloadRequest_SignatureInfo::mutable_certificate_chain() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.SignatureInfo.certificate_chain)
+ return &certificate_chain_;
+}
+
+// optional bool trusted = 2;
+inline bool ClientDownloadRequest_SignatureInfo::has_trusted() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_SignatureInfo::set_has_trusted() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_SignatureInfo::clear_has_trusted() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_SignatureInfo::clear_trusted() {
+ trusted_ = false;
+ clear_has_trusted();
+}
+inline bool ClientDownloadRequest_SignatureInfo::trusted() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.SignatureInfo.trusted)
+ return trusted_;
+}
+inline void ClientDownloadRequest_SignatureInfo::set_trusted(bool value) {
+ set_has_trusted();
+ trusted_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.SignatureInfo.trusted)
+}
+
+// repeated bytes signed_data = 3;
+inline int ClientDownloadRequest_SignatureInfo::signed_data_size() const {
+ return signed_data_.size();
+}
+inline void ClientDownloadRequest_SignatureInfo::clear_signed_data() {
+ signed_data_.Clear();
+}
+inline const ::std::string& ClientDownloadRequest_SignatureInfo::signed_data(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+ return signed_data_.Get(index);
+}
+inline ::std::string* ClientDownloadRequest_SignatureInfo::mutable_signed_data(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+ return signed_data_.Mutable(index);
+}
+inline void ClientDownloadRequest_SignatureInfo::set_signed_data(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+ signed_data_.Mutable(index)->assign(value);
+}
+inline void ClientDownloadRequest_SignatureInfo::set_signed_data(int index, const char* value) {
+ signed_data_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+}
+inline void ClientDownloadRequest_SignatureInfo::set_signed_data(int index, const void* value, size_t size) {
+ signed_data_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+}
+inline ::std::string* ClientDownloadRequest_SignatureInfo::add_signed_data() {
+ return signed_data_.Add();
+}
+inline void ClientDownloadRequest_SignatureInfo::add_signed_data(const ::std::string& value) {
+ signed_data_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+}
+inline void ClientDownloadRequest_SignatureInfo::add_signed_data(const char* value) {
+ signed_data_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+}
+inline void ClientDownloadRequest_SignatureInfo::add_signed_data(const void* value, size_t size) {
+ signed_data_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientDownloadRequest_SignatureInfo::signed_data() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+ return signed_data_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientDownloadRequest_SignatureInfo::mutable_signed_data() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.SignatureInfo.signed_data)
+ return &signed_data_;
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.ExtendedAttr xattr = 4;
+inline int ClientDownloadRequest_SignatureInfo::xattr_size() const {
+ return xattr_.size();
+}
+inline void ClientDownloadRequest_SignatureInfo::clear_xattr() {
+ xattr_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ExtendedAttr& ClientDownloadRequest_SignatureInfo::xattr(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.SignatureInfo.xattr)
+ return xattr_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_ExtendedAttr* ClientDownloadRequest_SignatureInfo::mutable_xattr(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.SignatureInfo.xattr)
+ return xattr_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_ExtendedAttr* ClientDownloadRequest_SignatureInfo::add_xattr() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.SignatureInfo.xattr)
+ return xattr_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ExtendedAttr >&
+ClientDownloadRequest_SignatureInfo::xattr() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.SignatureInfo.xattr)
+ return xattr_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ExtendedAttr >*
+ClientDownloadRequest_SignatureInfo::mutable_xattr() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.SignatureInfo.xattr)
+ return &xattr_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_PEImageHeaders_DebugData
+
+// optional bytes directory_entry = 1;
+inline bool ClientDownloadRequest_PEImageHeaders_DebugData::has_directory_entry() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_has_directory_entry() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::clear_has_directory_entry() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::clear_directory_entry() {
+ if (directory_entry_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ directory_entry_->clear();
+ }
+ clear_has_directory_entry();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders_DebugData::directory_entry() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.directory_entry)
+ return *directory_entry_;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_directory_entry(const ::std::string& value) {
+ set_has_directory_entry();
+ if (directory_entry_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ directory_entry_ = new ::std::string;
+ }
+ directory_entry_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.directory_entry)
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_directory_entry(const char* value) {
+ set_has_directory_entry();
+ if (directory_entry_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ directory_entry_ = new ::std::string;
+ }
+ directory_entry_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.directory_entry)
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_directory_entry(const void* value, size_t size) {
+ set_has_directory_entry();
+ if (directory_entry_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ directory_entry_ = new ::std::string;
+ }
+ directory_entry_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.directory_entry)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders_DebugData::mutable_directory_entry() {
+ set_has_directory_entry();
+ if (directory_entry_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ directory_entry_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.directory_entry)
+ return directory_entry_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders_DebugData::release_directory_entry() {
+ clear_has_directory_entry();
+ if (directory_entry_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = directory_entry_;
+ directory_entry_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_allocated_directory_entry(::std::string* directory_entry) {
+ if (directory_entry_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete directory_entry_;
+ }
+ if (directory_entry) {
+ set_has_directory_entry();
+ directory_entry_ = directory_entry;
+ } else {
+ clear_has_directory_entry();
+ directory_entry_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.directory_entry)
+}
+
+// optional bytes raw_data = 2;
+inline bool ClientDownloadRequest_PEImageHeaders_DebugData::has_raw_data() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_has_raw_data() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::clear_has_raw_data() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::clear_raw_data() {
+ if (raw_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_data_->clear();
+ }
+ clear_has_raw_data();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders_DebugData::raw_data() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.raw_data)
+ return *raw_data_;
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_raw_data(const ::std::string& value) {
+ set_has_raw_data();
+ if (raw_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_data_ = new ::std::string;
+ }
+ raw_data_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.raw_data)
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_raw_data(const char* value) {
+ set_has_raw_data();
+ if (raw_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_data_ = new ::std::string;
+ }
+ raw_data_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.raw_data)
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_raw_data(const void* value, size_t size) {
+ set_has_raw_data();
+ if (raw_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_data_ = new ::std::string;
+ }
+ raw_data_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.raw_data)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders_DebugData::mutable_raw_data() {
+ set_has_raw_data();
+ if (raw_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_data_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.raw_data)
+ return raw_data_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders_DebugData::release_raw_data() {
+ clear_has_raw_data();
+ if (raw_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = raw_data_;
+ raw_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders_DebugData::set_allocated_raw_data(::std::string* raw_data) {
+ if (raw_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete raw_data_;
+ }
+ if (raw_data) {
+ set_has_raw_data();
+ raw_data_ = raw_data;
+ } else {
+ clear_has_raw_data();
+ raw_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData.raw_data)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_PEImageHeaders
+
+// optional bytes dos_header = 1;
+inline bool ClientDownloadRequest_PEImageHeaders::has_dos_header() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_has_dos_header() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_has_dos_header() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_dos_header() {
+ if (dos_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ dos_header_->clear();
+ }
+ clear_has_dos_header();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders::dos_header() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.dos_header)
+ return *dos_header_;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_dos_header(const ::std::string& value) {
+ set_has_dos_header();
+ if (dos_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ dos_header_ = new ::std::string;
+ }
+ dos_header_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.dos_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_dos_header(const char* value) {
+ set_has_dos_header();
+ if (dos_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ dos_header_ = new ::std::string;
+ }
+ dos_header_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.dos_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_dos_header(const void* value, size_t size) {
+ set_has_dos_header();
+ if (dos_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ dos_header_ = new ::std::string;
+ }
+ dos_header_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.dos_header)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::mutable_dos_header() {
+ set_has_dos_header();
+ if (dos_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ dos_header_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.dos_header)
+ return dos_header_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::release_dos_header() {
+ clear_has_dos_header();
+ if (dos_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = dos_header_;
+ dos_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_allocated_dos_header(::std::string* dos_header) {
+ if (dos_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete dos_header_;
+ }
+ if (dos_header) {
+ set_has_dos_header();
+ dos_header_ = dos_header;
+ } else {
+ clear_has_dos_header();
+ dos_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.dos_header)
+}
+
+// optional bytes file_header = 2;
+inline bool ClientDownloadRequest_PEImageHeaders::has_file_header() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_has_file_header() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_has_file_header() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_file_header() {
+ if (file_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_header_->clear();
+ }
+ clear_has_file_header();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders::file_header() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.file_header)
+ return *file_header_;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_file_header(const ::std::string& value) {
+ set_has_file_header();
+ if (file_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_header_ = new ::std::string;
+ }
+ file_header_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.file_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_file_header(const char* value) {
+ set_has_file_header();
+ if (file_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_header_ = new ::std::string;
+ }
+ file_header_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.file_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_file_header(const void* value, size_t size) {
+ set_has_file_header();
+ if (file_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_header_ = new ::std::string;
+ }
+ file_header_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.file_header)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::mutable_file_header() {
+ set_has_file_header();
+ if (file_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_header_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.file_header)
+ return file_header_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::release_file_header() {
+ clear_has_file_header();
+ if (file_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = file_header_;
+ file_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_allocated_file_header(::std::string* file_header) {
+ if (file_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_header_;
+ }
+ if (file_header) {
+ set_has_file_header();
+ file_header_ = file_header;
+ } else {
+ clear_has_file_header();
+ file_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.file_header)
+}
+
+// optional bytes optional_headers32 = 3;
+inline bool ClientDownloadRequest_PEImageHeaders::has_optional_headers32() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_has_optional_headers32() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_has_optional_headers32() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_optional_headers32() {
+ if (optional_headers32_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers32_->clear();
+ }
+ clear_has_optional_headers32();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders::optional_headers32() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers32)
+ return *optional_headers32_;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_optional_headers32(const ::std::string& value) {
+ set_has_optional_headers32();
+ if (optional_headers32_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers32_ = new ::std::string;
+ }
+ optional_headers32_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers32)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_optional_headers32(const char* value) {
+ set_has_optional_headers32();
+ if (optional_headers32_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers32_ = new ::std::string;
+ }
+ optional_headers32_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers32)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_optional_headers32(const void* value, size_t size) {
+ set_has_optional_headers32();
+ if (optional_headers32_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers32_ = new ::std::string;
+ }
+ optional_headers32_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers32)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::mutable_optional_headers32() {
+ set_has_optional_headers32();
+ if (optional_headers32_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers32_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers32)
+ return optional_headers32_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::release_optional_headers32() {
+ clear_has_optional_headers32();
+ if (optional_headers32_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = optional_headers32_;
+ optional_headers32_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_allocated_optional_headers32(::std::string* optional_headers32) {
+ if (optional_headers32_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete optional_headers32_;
+ }
+ if (optional_headers32) {
+ set_has_optional_headers32();
+ optional_headers32_ = optional_headers32;
+ } else {
+ clear_has_optional_headers32();
+ optional_headers32_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers32)
+}
+
+// optional bytes optional_headers64 = 4;
+inline bool ClientDownloadRequest_PEImageHeaders::has_optional_headers64() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_has_optional_headers64() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_has_optional_headers64() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_optional_headers64() {
+ if (optional_headers64_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers64_->clear();
+ }
+ clear_has_optional_headers64();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders::optional_headers64() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers64)
+ return *optional_headers64_;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_optional_headers64(const ::std::string& value) {
+ set_has_optional_headers64();
+ if (optional_headers64_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers64_ = new ::std::string;
+ }
+ optional_headers64_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers64)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_optional_headers64(const char* value) {
+ set_has_optional_headers64();
+ if (optional_headers64_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers64_ = new ::std::string;
+ }
+ optional_headers64_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers64)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_optional_headers64(const void* value, size_t size) {
+ set_has_optional_headers64();
+ if (optional_headers64_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers64_ = new ::std::string;
+ }
+ optional_headers64_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers64)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::mutable_optional_headers64() {
+ set_has_optional_headers64();
+ if (optional_headers64_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ optional_headers64_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers64)
+ return optional_headers64_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::release_optional_headers64() {
+ clear_has_optional_headers64();
+ if (optional_headers64_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = optional_headers64_;
+ optional_headers64_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_allocated_optional_headers64(::std::string* optional_headers64) {
+ if (optional_headers64_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete optional_headers64_;
+ }
+ if (optional_headers64) {
+ set_has_optional_headers64();
+ optional_headers64_ = optional_headers64;
+ } else {
+ clear_has_optional_headers64();
+ optional_headers64_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.optional_headers64)
+}
+
+// repeated bytes section_header = 5;
+inline int ClientDownloadRequest_PEImageHeaders::section_header_size() const {
+ return section_header_.size();
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_section_header() {
+ section_header_.Clear();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders::section_header(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+ return section_header_.Get(index);
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::mutable_section_header(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+ return section_header_.Mutable(index);
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_section_header(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+ section_header_.Mutable(index)->assign(value);
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_section_header(int index, const char* value) {
+ section_header_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_section_header(int index, const void* value, size_t size) {
+ section_header_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::add_section_header() {
+ return section_header_.Add();
+}
+inline void ClientDownloadRequest_PEImageHeaders::add_section_header(const ::std::string& value) {
+ section_header_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::add_section_header(const char* value) {
+ section_header_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+}
+inline void ClientDownloadRequest_PEImageHeaders::add_section_header(const void* value, size_t size) {
+ section_header_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientDownloadRequest_PEImageHeaders::section_header() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+ return section_header_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientDownloadRequest_PEImageHeaders::mutable_section_header() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.PEImageHeaders.section_header)
+ return &section_header_;
+}
+
+// optional bytes export_section_data = 6;
+inline bool ClientDownloadRequest_PEImageHeaders::has_export_section_data() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_has_export_section_data() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_has_export_section_data() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_export_section_data() {
+ if (export_section_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_section_data_->clear();
+ }
+ clear_has_export_section_data();
+}
+inline const ::std::string& ClientDownloadRequest_PEImageHeaders::export_section_data() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.export_section_data)
+ return *export_section_data_;
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_export_section_data(const ::std::string& value) {
+ set_has_export_section_data();
+ if (export_section_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_section_data_ = new ::std::string;
+ }
+ export_section_data_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.PEImageHeaders.export_section_data)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_export_section_data(const char* value) {
+ set_has_export_section_data();
+ if (export_section_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_section_data_ = new ::std::string;
+ }
+ export_section_data_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.PEImageHeaders.export_section_data)
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_export_section_data(const void* value, size_t size) {
+ set_has_export_section_data();
+ if (export_section_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_section_data_ = new ::std::string;
+ }
+ export_section_data_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.PEImageHeaders.export_section_data)
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::mutable_export_section_data() {
+ set_has_export_section_data();
+ if (export_section_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_section_data_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.export_section_data)
+ return export_section_data_;
+}
+inline ::std::string* ClientDownloadRequest_PEImageHeaders::release_export_section_data() {
+ clear_has_export_section_data();
+ if (export_section_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = export_section_data_;
+ export_section_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_PEImageHeaders::set_allocated_export_section_data(::std::string* export_section_data) {
+ if (export_section_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete export_section_data_;
+ }
+ if (export_section_data) {
+ set_has_export_section_data();
+ export_section_data_ = export_section_data;
+ } else {
+ clear_has_export_section_data();
+ export_section_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.PEImageHeaders.export_section_data)
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.PEImageHeaders.DebugData debug_data = 7;
+inline int ClientDownloadRequest_PEImageHeaders::debug_data_size() const {
+ return debug_data_.size();
+}
+inline void ClientDownloadRequest_PEImageHeaders::clear_debug_data() {
+ debug_data_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData& ClientDownloadRequest_PEImageHeaders::debug_data(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.PEImageHeaders.debug_data)
+ return debug_data_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData* ClientDownloadRequest_PEImageHeaders::mutable_debug_data(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.PEImageHeaders.debug_data)
+ return debug_data_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData* ClientDownloadRequest_PEImageHeaders::add_debug_data() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.PEImageHeaders.debug_data)
+ return debug_data_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData >&
+ClientDownloadRequest_PEImageHeaders::debug_data() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.PEImageHeaders.debug_data)
+ return debug_data_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_PEImageHeaders_DebugData >*
+ClientDownloadRequest_PEImageHeaders::mutable_debug_data() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.PEImageHeaders.debug_data)
+ return &debug_data_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_MachOHeaders_LoadCommand
+
+// required uint32 command_id = 1;
+inline bool ClientDownloadRequest_MachOHeaders_LoadCommand::has_command_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_has_command_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::clear_has_command_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::clear_command_id() {
+ command_id_ = 0u;
+ clear_has_command_id();
+}
+inline ::google::protobuf::uint32 ClientDownloadRequest_MachOHeaders_LoadCommand::command_id() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command_id)
+ return command_id_;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_command_id(::google::protobuf::uint32 value) {
+ set_has_command_id();
+ command_id_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command_id)
+}
+
+// required bytes command = 2;
+inline bool ClientDownloadRequest_MachOHeaders_LoadCommand::has_command() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_has_command() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::clear_has_command() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::clear_command() {
+ if (command_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ command_->clear();
+ }
+ clear_has_command();
+}
+inline const ::std::string& ClientDownloadRequest_MachOHeaders_LoadCommand::command() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command)
+ return *command_;
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_command(const ::std::string& value) {
+ set_has_command();
+ if (command_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ command_ = new ::std::string;
+ }
+ command_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command)
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_command(const char* value) {
+ set_has_command();
+ if (command_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ command_ = new ::std::string;
+ }
+ command_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command)
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_command(const void* value, size_t size) {
+ set_has_command();
+ if (command_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ command_ = new ::std::string;
+ }
+ command_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command)
+}
+inline ::std::string* ClientDownloadRequest_MachOHeaders_LoadCommand::mutable_command() {
+ set_has_command();
+ if (command_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ command_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command)
+ return command_;
+}
+inline ::std::string* ClientDownloadRequest_MachOHeaders_LoadCommand::release_command() {
+ clear_has_command();
+ if (command_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = command_;
+ command_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_MachOHeaders_LoadCommand::set_allocated_command(::std::string* command) {
+ if (command_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete command_;
+ }
+ if (command) {
+ set_has_command();
+ command_ = command;
+ } else {
+ clear_has_command();
+ command_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand.command)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_MachOHeaders
+
+// required bytes mach_header = 1;
+inline bool ClientDownloadRequest_MachOHeaders::has_mach_header() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_MachOHeaders::set_has_mach_header() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_MachOHeaders::clear_has_mach_header() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_MachOHeaders::clear_mach_header() {
+ if (mach_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ mach_header_->clear();
+ }
+ clear_has_mach_header();
+}
+inline const ::std::string& ClientDownloadRequest_MachOHeaders::mach_header() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.MachOHeaders.mach_header)
+ return *mach_header_;
+}
+inline void ClientDownloadRequest_MachOHeaders::set_mach_header(const ::std::string& value) {
+ set_has_mach_header();
+ if (mach_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ mach_header_ = new ::std::string;
+ }
+ mach_header_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.MachOHeaders.mach_header)
+}
+inline void ClientDownloadRequest_MachOHeaders::set_mach_header(const char* value) {
+ set_has_mach_header();
+ if (mach_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ mach_header_ = new ::std::string;
+ }
+ mach_header_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.MachOHeaders.mach_header)
+}
+inline void ClientDownloadRequest_MachOHeaders::set_mach_header(const void* value, size_t size) {
+ set_has_mach_header();
+ if (mach_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ mach_header_ = new ::std::string;
+ }
+ mach_header_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.MachOHeaders.mach_header)
+}
+inline ::std::string* ClientDownloadRequest_MachOHeaders::mutable_mach_header() {
+ set_has_mach_header();
+ if (mach_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ mach_header_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.MachOHeaders.mach_header)
+ return mach_header_;
+}
+inline ::std::string* ClientDownloadRequest_MachOHeaders::release_mach_header() {
+ clear_has_mach_header();
+ if (mach_header_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = mach_header_;
+ mach_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_MachOHeaders::set_allocated_mach_header(::std::string* mach_header) {
+ if (mach_header_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete mach_header_;
+ }
+ if (mach_header) {
+ set_has_mach_header();
+ mach_header_ = mach_header;
+ } else {
+ clear_has_mach_header();
+ mach_header_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.MachOHeaders.mach_header)
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.MachOHeaders.LoadCommand load_commands = 2;
+inline int ClientDownloadRequest_MachOHeaders::load_commands_size() const {
+ return load_commands_.size();
+}
+inline void ClientDownloadRequest_MachOHeaders::clear_load_commands() {
+ load_commands_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand& ClientDownloadRequest_MachOHeaders::load_commands(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.MachOHeaders.load_commands)
+ return load_commands_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand* ClientDownloadRequest_MachOHeaders::mutable_load_commands(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.MachOHeaders.load_commands)
+ return load_commands_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand* ClientDownloadRequest_MachOHeaders::add_load_commands() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.MachOHeaders.load_commands)
+ return load_commands_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand >&
+ClientDownloadRequest_MachOHeaders::load_commands() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.MachOHeaders.load_commands)
+ return load_commands_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders_LoadCommand >*
+ClientDownloadRequest_MachOHeaders::mutable_load_commands() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.MachOHeaders.load_commands)
+ return &load_commands_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_ImageHeaders
+
+// optional .safe_browsing.ClientDownloadRequest.PEImageHeaders pe_headers = 1;
+inline bool ClientDownloadRequest_ImageHeaders::has_pe_headers() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_ImageHeaders::set_has_pe_headers() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_ImageHeaders::clear_has_pe_headers() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_ImageHeaders::clear_pe_headers() {
+ if (pe_headers_ != NULL) pe_headers_->::safe_browsing::ClientDownloadRequest_PEImageHeaders::Clear();
+ clear_has_pe_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_PEImageHeaders& ClientDownloadRequest_ImageHeaders::pe_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ImageHeaders.pe_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return pe_headers_ != NULL ? *pe_headers_ : *default_instance().pe_headers_;
+#else
+ return pe_headers_ != NULL ? *pe_headers_ : *default_instance_->pe_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders* ClientDownloadRequest_ImageHeaders::mutable_pe_headers() {
+ set_has_pe_headers();
+ if (pe_headers_ == NULL) pe_headers_ = new ::safe_browsing::ClientDownloadRequest_PEImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ImageHeaders.pe_headers)
+ return pe_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_PEImageHeaders* ClientDownloadRequest_ImageHeaders::release_pe_headers() {
+ clear_has_pe_headers();
+ ::safe_browsing::ClientDownloadRequest_PEImageHeaders* temp = pe_headers_;
+ pe_headers_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest_ImageHeaders::set_allocated_pe_headers(::safe_browsing::ClientDownloadRequest_PEImageHeaders* pe_headers) {
+ delete pe_headers_;
+ pe_headers_ = pe_headers;
+ if (pe_headers) {
+ set_has_pe_headers();
+ } else {
+ clear_has_pe_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ImageHeaders.pe_headers)
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.MachOHeaders mach_o_headers = 2;
+inline int ClientDownloadRequest_ImageHeaders::mach_o_headers_size() const {
+ return mach_o_headers_.size();
+}
+inline void ClientDownloadRequest_ImageHeaders::clear_mach_o_headers() {
+ mach_o_headers_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_MachOHeaders& ClientDownloadRequest_ImageHeaders::mach_o_headers(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ImageHeaders.mach_o_headers)
+ return mach_o_headers_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_MachOHeaders* ClientDownloadRequest_ImageHeaders::mutable_mach_o_headers(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ImageHeaders.mach_o_headers)
+ return mach_o_headers_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_MachOHeaders* ClientDownloadRequest_ImageHeaders::add_mach_o_headers() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.ImageHeaders.mach_o_headers)
+ return mach_o_headers_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders >&
+ClientDownloadRequest_ImageHeaders::mach_o_headers() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.ImageHeaders.mach_o_headers)
+ return mach_o_headers_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_MachOHeaders >*
+ClientDownloadRequest_ImageHeaders::mutable_mach_o_headers() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.ImageHeaders.mach_o_headers)
+ return &mach_o_headers_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_ArchivedBinary
+
+// optional string file_basename = 1;
+inline bool ClientDownloadRequest_ArchivedBinary::has_file_basename() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_has_file_basename() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_has_file_basename() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_file_basename() {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_->clear();
+ }
+ clear_has_file_basename();
+}
+inline const ::std::string& ClientDownloadRequest_ArchivedBinary::file_basename() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ArchivedBinary.file_basename)
+ return *file_basename_;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_file_basename(const ::std::string& value) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.ArchivedBinary.file_basename)
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_file_basename(const char* value) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.ArchivedBinary.file_basename)
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_file_basename(const char* value, size_t size) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.ArchivedBinary.file_basename)
+}
+inline ::std::string* ClientDownloadRequest_ArchivedBinary::mutable_file_basename() {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ArchivedBinary.file_basename)
+ return file_basename_;
+}
+inline ::std::string* ClientDownloadRequest_ArchivedBinary::release_file_basename() {
+ clear_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = file_basename_;
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_allocated_file_basename(::std::string* file_basename) {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_basename_;
+ }
+ if (file_basename) {
+ set_has_file_basename();
+ file_basename_ = file_basename;
+ } else {
+ clear_has_file_basename();
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ArchivedBinary.file_basename)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 2;
+inline bool ClientDownloadRequest_ArchivedBinary::has_download_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_has_download_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_has_download_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_download_type() {
+ download_type_ = 0;
+ clear_has_download_type();
+}
+inline ::safe_browsing::ClientDownloadRequest_DownloadType ClientDownloadRequest_ArchivedBinary::download_type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ArchivedBinary.download_type)
+ return static_cast< ::safe_browsing::ClientDownloadRequest_DownloadType >(download_type_);
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_download_type(::safe_browsing::ClientDownloadRequest_DownloadType value) {
+ assert(::safe_browsing::ClientDownloadRequest_DownloadType_IsValid(value));
+ set_has_download_type();
+ download_type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.ArchivedBinary.download_type)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.Digests digests = 3;
+inline bool ClientDownloadRequest_ArchivedBinary::has_digests() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_has_digests() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_has_digests() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_digests() {
+ if (digests_ != NULL) digests_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ clear_has_digests();
+}
+inline const ::safe_browsing::ClientDownloadRequest_Digests& ClientDownloadRequest_ArchivedBinary::digests() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ArchivedBinary.digests)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return digests_ != NULL ? *digests_ : *default_instance().digests_;
+#else
+ return digests_ != NULL ? *digests_ : *default_instance_->digests_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientDownloadRequest_ArchivedBinary::mutable_digests() {
+ set_has_digests();
+ if (digests_ == NULL) digests_ = new ::safe_browsing::ClientDownloadRequest_Digests;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ArchivedBinary.digests)
+ return digests_;
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientDownloadRequest_ArchivedBinary::release_digests() {
+ clear_has_digests();
+ ::safe_browsing::ClientDownloadRequest_Digests* temp = digests_;
+ digests_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_allocated_digests(::safe_browsing::ClientDownloadRequest_Digests* digests) {
+ delete digests_;
+ digests_ = digests;
+ if (digests) {
+ set_has_digests();
+ } else {
+ clear_has_digests();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ArchivedBinary.digests)
+}
+
+// optional int64 length = 4;
+inline bool ClientDownloadRequest_ArchivedBinary::has_length() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_has_length() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_has_length() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_length() {
+ length_ = GOOGLE_LONGLONG(0);
+ clear_has_length();
+}
+inline ::google::protobuf::int64 ClientDownloadRequest_ArchivedBinary::length() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ArchivedBinary.length)
+ return length_;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_length(::google::protobuf::int64 value) {
+ set_has_length();
+ length_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.ArchivedBinary.length)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+inline bool ClientDownloadRequest_ArchivedBinary::has_signature() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_has_signature() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_has_signature() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_signature() {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ clear_has_signature();
+}
+inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& ClientDownloadRequest_ArchivedBinary::signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ArchivedBinary.signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return signature_ != NULL ? *signature_ : *default_instance().signature_;
+#else
+ return signature_ != NULL ? *signature_ : *default_instance_->signature_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientDownloadRequest_ArchivedBinary::mutable_signature() {
+ set_has_signature();
+ if (signature_ == NULL) signature_ = new ::safe_browsing::ClientDownloadRequest_SignatureInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ArchivedBinary.signature)
+ return signature_;
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientDownloadRequest_ArchivedBinary::release_signature() {
+ clear_has_signature();
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* temp = signature_;
+ signature_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature) {
+ delete signature_;
+ signature_ = signature;
+ if (signature) {
+ set_has_signature();
+ } else {
+ clear_has_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ArchivedBinary.signature)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+inline bool ClientDownloadRequest_ArchivedBinary::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientDownloadRequest_ArchivedBinary::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientDownloadRequest_ArchivedBinary::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.ArchivedBinary.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientDownloadRequest_ArchivedBinary::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.ArchivedBinary.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientDownloadRequest_ArchivedBinary::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest_ArchivedBinary::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.ArchivedBinary.image_headers)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest_URLChainEntry
+
+// optional string url = 1;
+inline bool ClientDownloadRequest_URLChainEntry::has_url() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_url() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_url() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientDownloadRequest_URLChainEntry::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.url)
+ return *url_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.url)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.URLChainEntry.url)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.URLChainEntry.url)
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.URLChainEntry.url)
+ return url_;
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_URLChainEntry::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.URLChainEntry.url)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.URLChainEntry.URLType type = 2;
+inline bool ClientDownloadRequest_URLChainEntry::has_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_type() {
+ type_ = 1;
+ clear_has_type();
+}
+inline ::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType ClientDownloadRequest_URLChainEntry::type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.type)
+ return static_cast< ::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType >(type_);
+}
+inline void ClientDownloadRequest_URLChainEntry::set_type(::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType value) {
+ assert(::safe_browsing::ClientDownloadRequest_URLChainEntry_URLType_IsValid(value));
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.type)
+}
+
+// optional string ip_address = 3;
+inline bool ClientDownloadRequest_URLChainEntry::has_ip_address() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_ip_address() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_ip_address() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_ip_address() {
+ if (ip_address_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_address_->clear();
+ }
+ clear_has_ip_address();
+}
+inline const ::std::string& ClientDownloadRequest_URLChainEntry::ip_address() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.ip_address)
+ return *ip_address_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_ip_address(const ::std::string& value) {
+ set_has_ip_address();
+ if (ip_address_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_address_ = new ::std::string;
+ }
+ ip_address_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.ip_address)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_ip_address(const char* value) {
+ set_has_ip_address();
+ if (ip_address_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_address_ = new ::std::string;
+ }
+ ip_address_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.URLChainEntry.ip_address)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_ip_address(const char* value, size_t size) {
+ set_has_ip_address();
+ if (ip_address_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_address_ = new ::std::string;
+ }
+ ip_address_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.URLChainEntry.ip_address)
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::mutable_ip_address() {
+ set_has_ip_address();
+ if (ip_address_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ ip_address_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.URLChainEntry.ip_address)
+ return ip_address_;
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::release_ip_address() {
+ clear_has_ip_address();
+ if (ip_address_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = ip_address_;
+ ip_address_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_URLChainEntry::set_allocated_ip_address(::std::string* ip_address) {
+ if (ip_address_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete ip_address_;
+ }
+ if (ip_address) {
+ set_has_ip_address();
+ ip_address_ = ip_address;
+ } else {
+ clear_has_ip_address();
+ ip_address_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.URLChainEntry.ip_address)
+}
+
+// optional string referrer = 4;
+inline bool ClientDownloadRequest_URLChainEntry::has_referrer() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_referrer() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_referrer() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_referrer() {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ clear_has_referrer();
+}
+inline const ::std::string& ClientDownloadRequest_URLChainEntry::referrer() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.referrer)
+ return *referrer_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_referrer(const ::std::string& value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.referrer)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_referrer(const char* value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.URLChainEntry.referrer)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_referrer(const char* value, size_t size) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.URLChainEntry.referrer)
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::mutable_referrer() {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.URLChainEntry.referrer)
+ return referrer_;
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::release_referrer() {
+ clear_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = referrer_;
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_URLChainEntry::set_allocated_referrer(::std::string* referrer) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ if (referrer) {
+ set_has_referrer();
+ referrer_ = referrer;
+ } else {
+ clear_has_referrer();
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.URLChainEntry.referrer)
+}
+
+// optional string main_frame_referrer = 5;
+inline bool ClientDownloadRequest_URLChainEntry::has_main_frame_referrer() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_main_frame_referrer() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_main_frame_referrer() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_main_frame_referrer() {
+ if (main_frame_referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ main_frame_referrer_->clear();
+ }
+ clear_has_main_frame_referrer();
+}
+inline const ::std::string& ClientDownloadRequest_URLChainEntry::main_frame_referrer() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.main_frame_referrer)
+ return *main_frame_referrer_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_main_frame_referrer(const ::std::string& value) {
+ set_has_main_frame_referrer();
+ if (main_frame_referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ main_frame_referrer_ = new ::std::string;
+ }
+ main_frame_referrer_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.main_frame_referrer)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_main_frame_referrer(const char* value) {
+ set_has_main_frame_referrer();
+ if (main_frame_referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ main_frame_referrer_ = new ::std::string;
+ }
+ main_frame_referrer_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.URLChainEntry.main_frame_referrer)
+}
+inline void ClientDownloadRequest_URLChainEntry::set_main_frame_referrer(const char* value, size_t size) {
+ set_has_main_frame_referrer();
+ if (main_frame_referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ main_frame_referrer_ = new ::std::string;
+ }
+ main_frame_referrer_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.URLChainEntry.main_frame_referrer)
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::mutable_main_frame_referrer() {
+ set_has_main_frame_referrer();
+ if (main_frame_referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ main_frame_referrer_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.URLChainEntry.main_frame_referrer)
+ return main_frame_referrer_;
+}
+inline ::std::string* ClientDownloadRequest_URLChainEntry::release_main_frame_referrer() {
+ clear_has_main_frame_referrer();
+ if (main_frame_referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = main_frame_referrer_;
+ main_frame_referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest_URLChainEntry::set_allocated_main_frame_referrer(::std::string* main_frame_referrer) {
+ if (main_frame_referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete main_frame_referrer_;
+ }
+ if (main_frame_referrer) {
+ set_has_main_frame_referrer();
+ main_frame_referrer_ = main_frame_referrer;
+ } else {
+ clear_has_main_frame_referrer();
+ main_frame_referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.URLChainEntry.main_frame_referrer)
+}
+
+// optional bool is_retargeting = 6;
+inline bool ClientDownloadRequest_URLChainEntry::has_is_retargeting() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_is_retargeting() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_is_retargeting() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_is_retargeting() {
+ is_retargeting_ = false;
+ clear_has_is_retargeting();
+}
+inline bool ClientDownloadRequest_URLChainEntry::is_retargeting() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.is_retargeting)
+ return is_retargeting_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_is_retargeting(bool value) {
+ set_has_is_retargeting();
+ is_retargeting_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.is_retargeting)
+}
+
+// optional bool is_user_initiated = 7;
+inline bool ClientDownloadRequest_URLChainEntry::has_is_user_initiated() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_is_user_initiated() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_is_user_initiated() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_is_user_initiated() {
+ is_user_initiated_ = false;
+ clear_has_is_user_initiated();
+}
+inline bool ClientDownloadRequest_URLChainEntry::is_user_initiated() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.is_user_initiated)
+ return is_user_initiated_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_is_user_initiated(bool value) {
+ set_has_is_user_initiated();
+ is_user_initiated_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.is_user_initiated)
+}
+
+// optional double timestamp_in_millisec = 8;
+inline bool ClientDownloadRequest_URLChainEntry::has_timestamp_in_millisec() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_has_timestamp_in_millisec() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_has_timestamp_in_millisec() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void ClientDownloadRequest_URLChainEntry::clear_timestamp_in_millisec() {
+ timestamp_in_millisec_ = 0;
+ clear_has_timestamp_in_millisec();
+}
+inline double ClientDownloadRequest_URLChainEntry::timestamp_in_millisec() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.URLChainEntry.timestamp_in_millisec)
+ return timestamp_in_millisec_;
+}
+inline void ClientDownloadRequest_URLChainEntry::set_timestamp_in_millisec(double value) {
+ set_has_timestamp_in_millisec();
+ timestamp_in_millisec_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.URLChainEntry.timestamp_in_millisec)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadRequest
+
+// required string url = 1;
+inline bool ClientDownloadRequest::has_url() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadRequest::set_has_url() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadRequest::clear_has_url() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadRequest::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientDownloadRequest::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.url)
+ return *url_;
+}
+inline void ClientDownloadRequest::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.url)
+}
+inline void ClientDownloadRequest::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.url)
+}
+inline void ClientDownloadRequest::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.url)
+}
+inline ::std::string* ClientDownloadRequest::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.url)
+ return url_;
+}
+inline ::std::string* ClientDownloadRequest::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.url)
+}
+
+// required .safe_browsing.ClientDownloadRequest.Digests digests = 2;
+inline bool ClientDownloadRequest::has_digests() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadRequest::set_has_digests() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadRequest::clear_has_digests() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadRequest::clear_digests() {
+ if (digests_ != NULL) digests_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ clear_has_digests();
+}
+inline const ::safe_browsing::ClientDownloadRequest_Digests& ClientDownloadRequest::digests() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.digests)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return digests_ != NULL ? *digests_ : *default_instance().digests_;
+#else
+ return digests_ != NULL ? *digests_ : *default_instance_->digests_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientDownloadRequest::mutable_digests() {
+ set_has_digests();
+ if (digests_ == NULL) digests_ = new ::safe_browsing::ClientDownloadRequest_Digests;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.digests)
+ return digests_;
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientDownloadRequest::release_digests() {
+ clear_has_digests();
+ ::safe_browsing::ClientDownloadRequest_Digests* temp = digests_;
+ digests_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest::set_allocated_digests(::safe_browsing::ClientDownloadRequest_Digests* digests) {
+ delete digests_;
+ digests_ = digests;
+ if (digests) {
+ set_has_digests();
+ } else {
+ clear_has_digests();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.digests)
+}
+
+// required int64 length = 3;
+inline bool ClientDownloadRequest::has_length() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadRequest::set_has_length() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadRequest::clear_has_length() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadRequest::clear_length() {
+ length_ = GOOGLE_LONGLONG(0);
+ clear_has_length();
+}
+inline ::google::protobuf::int64 ClientDownloadRequest::length() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.length)
+ return length_;
+}
+inline void ClientDownloadRequest::set_length(::google::protobuf::int64 value) {
+ set_has_length();
+ length_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.length)
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.Resource resources = 4;
+inline int ClientDownloadRequest::resources_size() const {
+ return resources_.size();
+}
+inline void ClientDownloadRequest::clear_resources() {
+ resources_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_Resource& ClientDownloadRequest::resources(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.resources)
+ return resources_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_Resource* ClientDownloadRequest::mutable_resources(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.resources)
+ return resources_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_Resource* ClientDownloadRequest::add_resources() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.resources)
+ return resources_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_Resource >&
+ClientDownloadRequest::resources() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.resources)
+ return resources_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_Resource >*
+ClientDownloadRequest::mutable_resources() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.resources)
+ return &resources_;
+}
+
+// optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+inline bool ClientDownloadRequest::has_signature() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientDownloadRequest::set_has_signature() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientDownloadRequest::clear_has_signature() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientDownloadRequest::clear_signature() {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ clear_has_signature();
+}
+inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& ClientDownloadRequest::signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return signature_ != NULL ? *signature_ : *default_instance().signature_;
+#else
+ return signature_ != NULL ? *signature_ : *default_instance_->signature_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientDownloadRequest::mutable_signature() {
+ set_has_signature();
+ if (signature_ == NULL) signature_ = new ::safe_browsing::ClientDownloadRequest_SignatureInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.signature)
+ return signature_;
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientDownloadRequest::release_signature() {
+ clear_has_signature();
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* temp = signature_;
+ signature_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest::set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature) {
+ delete signature_;
+ signature_ = signature;
+ if (signature) {
+ set_has_signature();
+ } else {
+ clear_has_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.signature)
+}
+
+// optional bool user_initiated = 6;
+inline bool ClientDownloadRequest::has_user_initiated() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientDownloadRequest::set_has_user_initiated() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientDownloadRequest::clear_has_user_initiated() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientDownloadRequest::clear_user_initiated() {
+ user_initiated_ = false;
+ clear_has_user_initiated();
+}
+inline bool ClientDownloadRequest::user_initiated() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.user_initiated)
+ return user_initiated_;
+}
+inline void ClientDownloadRequest::set_user_initiated(bool value) {
+ set_has_user_initiated();
+ user_initiated_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.user_initiated)
+}
+
+// optional string file_basename = 9;
+inline bool ClientDownloadRequest::has_file_basename() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientDownloadRequest::set_has_file_basename() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientDownloadRequest::clear_has_file_basename() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientDownloadRequest::clear_file_basename() {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_->clear();
+ }
+ clear_has_file_basename();
+}
+inline const ::std::string& ClientDownloadRequest::file_basename() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.file_basename)
+ return *file_basename_;
+}
+inline void ClientDownloadRequest::set_file_basename(const ::std::string& value) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.file_basename)
+}
+inline void ClientDownloadRequest::set_file_basename(const char* value) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.file_basename)
+}
+inline void ClientDownloadRequest::set_file_basename(const char* value, size_t size) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.file_basename)
+}
+inline ::std::string* ClientDownloadRequest::mutable_file_basename() {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.file_basename)
+ return file_basename_;
+}
+inline ::std::string* ClientDownloadRequest::release_file_basename() {
+ clear_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = file_basename_;
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest::set_allocated_file_basename(::std::string* file_basename) {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_basename_;
+ }
+ if (file_basename) {
+ set_has_file_basename();
+ file_basename_ = file_basename;
+ } else {
+ clear_has_file_basename();
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.file_basename)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.DownloadType download_type = 10 [default = WIN_EXECUTABLE];
+inline bool ClientDownloadRequest::has_download_type() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void ClientDownloadRequest::set_has_download_type() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void ClientDownloadRequest::clear_has_download_type() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void ClientDownloadRequest::clear_download_type() {
+ download_type_ = 0;
+ clear_has_download_type();
+}
+inline ::safe_browsing::ClientDownloadRequest_DownloadType ClientDownloadRequest::download_type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.download_type)
+ return static_cast< ::safe_browsing::ClientDownloadRequest_DownloadType >(download_type_);
+}
+inline void ClientDownloadRequest::set_download_type(::safe_browsing::ClientDownloadRequest_DownloadType value) {
+ assert(::safe_browsing::ClientDownloadRequest_DownloadType_IsValid(value));
+ set_has_download_type();
+ download_type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.download_type)
+}
+
+// optional string locale = 11;
+inline bool ClientDownloadRequest::has_locale() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void ClientDownloadRequest::set_has_locale() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void ClientDownloadRequest::clear_has_locale() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void ClientDownloadRequest::clear_locale() {
+ if (locale_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ locale_->clear();
+ }
+ clear_has_locale();
+}
+inline const ::std::string& ClientDownloadRequest::locale() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.locale)
+ return *locale_;
+}
+inline void ClientDownloadRequest::set_locale(const ::std::string& value) {
+ set_has_locale();
+ if (locale_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ locale_ = new ::std::string;
+ }
+ locale_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.locale)
+}
+inline void ClientDownloadRequest::set_locale(const char* value) {
+ set_has_locale();
+ if (locale_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ locale_ = new ::std::string;
+ }
+ locale_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.locale)
+}
+inline void ClientDownloadRequest::set_locale(const char* value, size_t size) {
+ set_has_locale();
+ if (locale_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ locale_ = new ::std::string;
+ }
+ locale_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.locale)
+}
+inline ::std::string* ClientDownloadRequest::mutable_locale() {
+ set_has_locale();
+ if (locale_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ locale_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.locale)
+ return locale_;
+}
+inline ::std::string* ClientDownloadRequest::release_locale() {
+ clear_has_locale();
+ if (locale_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = locale_;
+ locale_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadRequest::set_allocated_locale(::std::string* locale) {
+ if (locale_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete locale_;
+ }
+ if (locale) {
+ set_has_locale();
+ locale_ = locale;
+ } else {
+ clear_has_locale();
+ locale_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.locale)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 18;
+inline bool ClientDownloadRequest::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void ClientDownloadRequest::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void ClientDownloadRequest::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void ClientDownloadRequest::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientDownloadRequest::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientDownloadRequest::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientDownloadRequest::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.image_headers)
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.ArchivedBinary archived_binary = 22;
+inline int ClientDownloadRequest::archived_binary_size() const {
+ return archived_binary_.size();
+}
+inline void ClientDownloadRequest::clear_archived_binary() {
+ archived_binary_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ArchivedBinary& ClientDownloadRequest::archived_binary(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.archived_binary)
+ return archived_binary_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_ArchivedBinary* ClientDownloadRequest::mutable_archived_binary(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.archived_binary)
+ return archived_binary_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_ArchivedBinary* ClientDownloadRequest::add_archived_binary() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.archived_binary)
+ return archived_binary_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ArchivedBinary >&
+ClientDownloadRequest::archived_binary() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.archived_binary)
+ return archived_binary_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_ArchivedBinary >*
+ClientDownloadRequest::mutable_archived_binary() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.archived_binary)
+ return &archived_binary_;
+}
+
+// optional .safe_browsing.ChromeUserPopulation population = 24;
+inline bool ClientDownloadRequest::has_population() const {
+ return (_has_bits_[0] & 0x00000800u) != 0;
+}
+inline void ClientDownloadRequest::set_has_population() {
+ _has_bits_[0] |= 0x00000800u;
+}
+inline void ClientDownloadRequest::clear_has_population() {
+ _has_bits_[0] &= ~0x00000800u;
+}
+inline void ClientDownloadRequest::clear_population() {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ clear_has_population();
+}
+inline const ::safe_browsing::ChromeUserPopulation& ClientDownloadRequest::population() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.population)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return population_ != NULL ? *population_ : *default_instance().population_;
+#else
+ return population_ != NULL ? *population_ : *default_instance_->population_;
+#endif
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientDownloadRequest::mutable_population() {
+ set_has_population();
+ if (population_ == NULL) population_ = new ::safe_browsing::ChromeUserPopulation;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.population)
+ return population_;
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientDownloadRequest::release_population() {
+ clear_has_population();
+ ::safe_browsing::ChromeUserPopulation* temp = population_;
+ population_ = NULL;
+ return temp;
+}
+inline void ClientDownloadRequest::set_allocated_population(::safe_browsing::ChromeUserPopulation* population) {
+ delete population_;
+ population_ = population;
+ if (population) {
+ set_has_population();
+ } else {
+ clear_has_population();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadRequest.population)
+}
+
+// optional bool archive_valid = 26;
+inline bool ClientDownloadRequest::has_archive_valid() const {
+ return (_has_bits_[0] & 0x00001000u) != 0;
+}
+inline void ClientDownloadRequest::set_has_archive_valid() {
+ _has_bits_[0] |= 0x00001000u;
+}
+inline void ClientDownloadRequest::clear_has_archive_valid() {
+ _has_bits_[0] &= ~0x00001000u;
+}
+inline void ClientDownloadRequest::clear_archive_valid() {
+ archive_valid_ = false;
+ clear_has_archive_valid();
+}
+inline bool ClientDownloadRequest::archive_valid() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.archive_valid)
+ return archive_valid_;
+}
+inline void ClientDownloadRequest::set_archive_valid(bool value) {
+ set_has_archive_valid();
+ archive_valid_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.archive_valid)
+}
+
+// optional bool skipped_url_whitelist = 28;
+inline bool ClientDownloadRequest::has_skipped_url_whitelist() const {
+ return (_has_bits_[0] & 0x00002000u) != 0;
+}
+inline void ClientDownloadRequest::set_has_skipped_url_whitelist() {
+ _has_bits_[0] |= 0x00002000u;
+}
+inline void ClientDownloadRequest::clear_has_skipped_url_whitelist() {
+ _has_bits_[0] &= ~0x00002000u;
+}
+inline void ClientDownloadRequest::clear_skipped_url_whitelist() {
+ skipped_url_whitelist_ = false;
+ clear_has_skipped_url_whitelist();
+}
+inline bool ClientDownloadRequest::skipped_url_whitelist() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.skipped_url_whitelist)
+ return skipped_url_whitelist_;
+}
+inline void ClientDownloadRequest::set_skipped_url_whitelist(bool value) {
+ set_has_skipped_url_whitelist();
+ skipped_url_whitelist_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.skipped_url_whitelist)
+}
+
+// optional bool skipped_certificate_whitelist = 31;
+inline bool ClientDownloadRequest::has_skipped_certificate_whitelist() const {
+ return (_has_bits_[0] & 0x00004000u) != 0;
+}
+inline void ClientDownloadRequest::set_has_skipped_certificate_whitelist() {
+ _has_bits_[0] |= 0x00004000u;
+}
+inline void ClientDownloadRequest::clear_has_skipped_certificate_whitelist() {
+ _has_bits_[0] &= ~0x00004000u;
+}
+inline void ClientDownloadRequest::clear_skipped_certificate_whitelist() {
+ skipped_certificate_whitelist_ = false;
+ clear_has_skipped_certificate_whitelist();
+}
+inline bool ClientDownloadRequest::skipped_certificate_whitelist() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.skipped_certificate_whitelist)
+ return skipped_certificate_whitelist_;
+}
+inline void ClientDownloadRequest::set_skipped_certificate_whitelist(bool value) {
+ set_has_skipped_certificate_whitelist();
+ skipped_certificate_whitelist_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.skipped_certificate_whitelist)
+}
+
+// repeated string alternate_extensions = 35;
+inline int ClientDownloadRequest::alternate_extensions_size() const {
+ return alternate_extensions_.size();
+}
+inline void ClientDownloadRequest::clear_alternate_extensions() {
+ alternate_extensions_.Clear();
+}
+inline const ::std::string& ClientDownloadRequest::alternate_extensions(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.alternate_extensions)
+ return alternate_extensions_.Get(index);
+}
+inline ::std::string* ClientDownloadRequest::mutable_alternate_extensions(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.alternate_extensions)
+ return alternate_extensions_.Mutable(index);
+}
+inline void ClientDownloadRequest::set_alternate_extensions(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadRequest.alternate_extensions)
+ alternate_extensions_.Mutable(index)->assign(value);
+}
+inline void ClientDownloadRequest::set_alternate_extensions(int index, const char* value) {
+ alternate_extensions_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadRequest.alternate_extensions)
+}
+inline void ClientDownloadRequest::set_alternate_extensions(int index, const char* value, size_t size) {
+ alternate_extensions_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadRequest.alternate_extensions)
+}
+inline ::std::string* ClientDownloadRequest::add_alternate_extensions() {
+ return alternate_extensions_.Add();
+}
+inline void ClientDownloadRequest::add_alternate_extensions(const ::std::string& value) {
+ alternate_extensions_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.alternate_extensions)
+}
+inline void ClientDownloadRequest::add_alternate_extensions(const char* value) {
+ alternate_extensions_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientDownloadRequest.alternate_extensions)
+}
+inline void ClientDownloadRequest::add_alternate_extensions(const char* value, size_t size) {
+ alternate_extensions_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientDownloadRequest.alternate_extensions)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientDownloadRequest::alternate_extensions() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.alternate_extensions)
+ return alternate_extensions_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientDownloadRequest::mutable_alternate_extensions() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.alternate_extensions)
+ return &alternate_extensions_;
+}
+
+// repeated .safe_browsing.ClientDownloadRequest.URLChainEntry url_chain = 36;
+inline int ClientDownloadRequest::url_chain_size() const {
+ return url_chain_.size();
+}
+inline void ClientDownloadRequest::clear_url_chain() {
+ url_chain_.Clear();
+}
+inline const ::safe_browsing::ClientDownloadRequest_URLChainEntry& ClientDownloadRequest::url_chain(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadRequest.url_chain)
+ return url_chain_.Get(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_URLChainEntry* ClientDownloadRequest::mutable_url_chain(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadRequest.url_chain)
+ return url_chain_.Mutable(index);
+}
+inline ::safe_browsing::ClientDownloadRequest_URLChainEntry* ClientDownloadRequest::add_url_chain() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientDownloadRequest.url_chain)
+ return url_chain_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_URLChainEntry >&
+ClientDownloadRequest::url_chain() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientDownloadRequest.url_chain)
+ return url_chain_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientDownloadRequest_URLChainEntry >*
+ClientDownloadRequest::mutable_url_chain() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientDownloadRequest.url_chain)
+ return &url_chain_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadResponse_MoreInfo
+
+// optional string description = 1;
+inline bool ClientDownloadResponse_MoreInfo::has_description() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadResponse_MoreInfo::set_has_description() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadResponse_MoreInfo::clear_has_description() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadResponse_MoreInfo::clear_description() {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_->clear();
+ }
+ clear_has_description();
+}
+inline const ::std::string& ClientDownloadResponse_MoreInfo::description() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadResponse.MoreInfo.description)
+ return *description_;
+}
+inline void ClientDownloadResponse_MoreInfo::set_description(const ::std::string& value) {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ description_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadResponse.MoreInfo.description)
+}
+inline void ClientDownloadResponse_MoreInfo::set_description(const char* value) {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ description_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadResponse.MoreInfo.description)
+}
+inline void ClientDownloadResponse_MoreInfo::set_description(const char* value, size_t size) {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ description_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadResponse.MoreInfo.description)
+}
+inline ::std::string* ClientDownloadResponse_MoreInfo::mutable_description() {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadResponse.MoreInfo.description)
+ return description_;
+}
+inline ::std::string* ClientDownloadResponse_MoreInfo::release_description() {
+ clear_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = description_;
+ description_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadResponse_MoreInfo::set_allocated_description(::std::string* description) {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete description_;
+ }
+ if (description) {
+ set_has_description();
+ description_ = description;
+ } else {
+ clear_has_description();
+ description_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadResponse.MoreInfo.description)
+}
+
+// optional string url = 2;
+inline bool ClientDownloadResponse_MoreInfo::has_url() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadResponse_MoreInfo::set_has_url() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadResponse_MoreInfo::clear_has_url() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadResponse_MoreInfo::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientDownloadResponse_MoreInfo::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadResponse.MoreInfo.url)
+ return *url_;
+}
+inline void ClientDownloadResponse_MoreInfo::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadResponse.MoreInfo.url)
+}
+inline void ClientDownloadResponse_MoreInfo::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadResponse.MoreInfo.url)
+}
+inline void ClientDownloadResponse_MoreInfo::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadResponse.MoreInfo.url)
+}
+inline ::std::string* ClientDownloadResponse_MoreInfo::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadResponse.MoreInfo.url)
+ return url_;
+}
+inline ::std::string* ClientDownloadResponse_MoreInfo::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadResponse_MoreInfo::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadResponse.MoreInfo.url)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadResponse
+
+// optional .safe_browsing.ClientDownloadResponse.Verdict verdict = 1 [default = SAFE];
+inline bool ClientDownloadResponse::has_verdict() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadResponse::set_has_verdict() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadResponse::clear_has_verdict() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadResponse::clear_verdict() {
+ verdict_ = 0;
+ clear_has_verdict();
+}
+inline ::safe_browsing::ClientDownloadResponse_Verdict ClientDownloadResponse::verdict() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadResponse.verdict)
+ return static_cast< ::safe_browsing::ClientDownloadResponse_Verdict >(verdict_);
+}
+inline void ClientDownloadResponse::set_verdict(::safe_browsing::ClientDownloadResponse_Verdict value) {
+ assert(::safe_browsing::ClientDownloadResponse_Verdict_IsValid(value));
+ set_has_verdict();
+ verdict_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadResponse.verdict)
+}
+
+// optional .safe_browsing.ClientDownloadResponse.MoreInfo more_info = 2;
+inline bool ClientDownloadResponse::has_more_info() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadResponse::set_has_more_info() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadResponse::clear_has_more_info() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadResponse::clear_more_info() {
+ if (more_info_ != NULL) more_info_->::safe_browsing::ClientDownloadResponse_MoreInfo::Clear();
+ clear_has_more_info();
+}
+inline const ::safe_browsing::ClientDownloadResponse_MoreInfo& ClientDownloadResponse::more_info() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadResponse.more_info)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return more_info_ != NULL ? *more_info_ : *default_instance().more_info_;
+#else
+ return more_info_ != NULL ? *more_info_ : *default_instance_->more_info_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadResponse_MoreInfo* ClientDownloadResponse::mutable_more_info() {
+ set_has_more_info();
+ if (more_info_ == NULL) more_info_ = new ::safe_browsing::ClientDownloadResponse_MoreInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadResponse.more_info)
+ return more_info_;
+}
+inline ::safe_browsing::ClientDownloadResponse_MoreInfo* ClientDownloadResponse::release_more_info() {
+ clear_has_more_info();
+ ::safe_browsing::ClientDownloadResponse_MoreInfo* temp = more_info_;
+ more_info_ = NULL;
+ return temp;
+}
+inline void ClientDownloadResponse::set_allocated_more_info(::safe_browsing::ClientDownloadResponse_MoreInfo* more_info) {
+ delete more_info_;
+ more_info_ = more_info;
+ if (more_info) {
+ set_has_more_info();
+ } else {
+ clear_has_more_info();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadResponse.more_info)
+}
+
+// optional bytes token = 3;
+inline bool ClientDownloadResponse::has_token() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadResponse::set_has_token() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadResponse::clear_has_token() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadResponse::clear_token() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ clear_has_token();
+}
+inline const ::std::string& ClientDownloadResponse::token() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadResponse.token)
+ return *token_;
+}
+inline void ClientDownloadResponse::set_token(const ::std::string& value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadResponse.token)
+}
+inline void ClientDownloadResponse::set_token(const char* value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadResponse.token)
+}
+inline void ClientDownloadResponse::set_token(const void* value, size_t size) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadResponse.token)
+}
+inline ::std::string* ClientDownloadResponse::mutable_token() {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadResponse.token)
+ return token_;
+}
+inline ::std::string* ClientDownloadResponse::release_token() {
+ clear_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = token_;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadResponse::set_allocated_token(::std::string* token) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ if (token) {
+ set_has_token();
+ token_ = token;
+ } else {
+ clear_has_token();
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadResponse.token)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadReport_UserInformation
+
+// optional string email = 1;
+inline bool ClientDownloadReport_UserInformation::has_email() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadReport_UserInformation::set_has_email() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadReport_UserInformation::clear_has_email() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadReport_UserInformation::clear_email() {
+ if (email_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ email_->clear();
+ }
+ clear_has_email();
+}
+inline const ::std::string& ClientDownloadReport_UserInformation::email() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadReport.UserInformation.email)
+ return *email_;
+}
+inline void ClientDownloadReport_UserInformation::set_email(const ::std::string& value) {
+ set_has_email();
+ if (email_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ email_ = new ::std::string;
+ }
+ email_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadReport.UserInformation.email)
+}
+inline void ClientDownloadReport_UserInformation::set_email(const char* value) {
+ set_has_email();
+ if (email_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ email_ = new ::std::string;
+ }
+ email_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadReport.UserInformation.email)
+}
+inline void ClientDownloadReport_UserInformation::set_email(const char* value, size_t size) {
+ set_has_email();
+ if (email_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ email_ = new ::std::string;
+ }
+ email_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadReport.UserInformation.email)
+}
+inline ::std::string* ClientDownloadReport_UserInformation::mutable_email() {
+ set_has_email();
+ if (email_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ email_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadReport.UserInformation.email)
+ return email_;
+}
+inline ::std::string* ClientDownloadReport_UserInformation::release_email() {
+ clear_has_email();
+ if (email_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = email_;
+ email_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadReport_UserInformation::set_allocated_email(::std::string* email) {
+ if (email_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete email_;
+ }
+ if (email) {
+ set_has_email();
+ email_ = email;
+ } else {
+ clear_has_email();
+ email_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadReport.UserInformation.email)
+}
+
+// -------------------------------------------------------------------
+
+// ClientDownloadReport
+
+// optional .safe_browsing.ClientDownloadReport.Reason reason = 1;
+inline bool ClientDownloadReport::has_reason() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientDownloadReport::set_has_reason() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientDownloadReport::clear_has_reason() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientDownloadReport::clear_reason() {
+ reason_ = 0;
+ clear_has_reason();
+}
+inline ::safe_browsing::ClientDownloadReport_Reason ClientDownloadReport::reason() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadReport.reason)
+ return static_cast< ::safe_browsing::ClientDownloadReport_Reason >(reason_);
+}
+inline void ClientDownloadReport::set_reason(::safe_browsing::ClientDownloadReport_Reason value) {
+ assert(::safe_browsing::ClientDownloadReport_Reason_IsValid(value));
+ set_has_reason();
+ reason_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadReport.reason)
+}
+
+// optional .safe_browsing.ClientDownloadRequest download_request = 2;
+inline bool ClientDownloadReport::has_download_request() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientDownloadReport::set_has_download_request() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientDownloadReport::clear_has_download_request() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientDownloadReport::clear_download_request() {
+ if (download_request_ != NULL) download_request_->::safe_browsing::ClientDownloadRequest::Clear();
+ clear_has_download_request();
+}
+inline const ::safe_browsing::ClientDownloadRequest& ClientDownloadReport::download_request() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadReport.download_request)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return download_request_ != NULL ? *download_request_ : *default_instance().download_request_;
+#else
+ return download_request_ != NULL ? *download_request_ : *default_instance_->download_request_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest* ClientDownloadReport::mutable_download_request() {
+ set_has_download_request();
+ if (download_request_ == NULL) download_request_ = new ::safe_browsing::ClientDownloadRequest;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadReport.download_request)
+ return download_request_;
+}
+inline ::safe_browsing::ClientDownloadRequest* ClientDownloadReport::release_download_request() {
+ clear_has_download_request();
+ ::safe_browsing::ClientDownloadRequest* temp = download_request_;
+ download_request_ = NULL;
+ return temp;
+}
+inline void ClientDownloadReport::set_allocated_download_request(::safe_browsing::ClientDownloadRequest* download_request) {
+ delete download_request_;
+ download_request_ = download_request;
+ if (download_request) {
+ set_has_download_request();
+ } else {
+ clear_has_download_request();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadReport.download_request)
+}
+
+// optional .safe_browsing.ClientDownloadReport.UserInformation user_information = 3;
+inline bool ClientDownloadReport::has_user_information() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientDownloadReport::set_has_user_information() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientDownloadReport::clear_has_user_information() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientDownloadReport::clear_user_information() {
+ if (user_information_ != NULL) user_information_->::safe_browsing::ClientDownloadReport_UserInformation::Clear();
+ clear_has_user_information();
+}
+inline const ::safe_browsing::ClientDownloadReport_UserInformation& ClientDownloadReport::user_information() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadReport.user_information)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return user_information_ != NULL ? *user_information_ : *default_instance().user_information_;
+#else
+ return user_information_ != NULL ? *user_information_ : *default_instance_->user_information_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadReport_UserInformation* ClientDownloadReport::mutable_user_information() {
+ set_has_user_information();
+ if (user_information_ == NULL) user_information_ = new ::safe_browsing::ClientDownloadReport_UserInformation;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadReport.user_information)
+ return user_information_;
+}
+inline ::safe_browsing::ClientDownloadReport_UserInformation* ClientDownloadReport::release_user_information() {
+ clear_has_user_information();
+ ::safe_browsing::ClientDownloadReport_UserInformation* temp = user_information_;
+ user_information_ = NULL;
+ return temp;
+}
+inline void ClientDownloadReport::set_allocated_user_information(::safe_browsing::ClientDownloadReport_UserInformation* user_information) {
+ delete user_information_;
+ user_information_ = user_information;
+ if (user_information) {
+ set_has_user_information();
+ } else {
+ clear_has_user_information();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadReport.user_information)
+}
+
+// optional bytes comment = 4;
+inline bool ClientDownloadReport::has_comment() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientDownloadReport::set_has_comment() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientDownloadReport::clear_has_comment() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientDownloadReport::clear_comment() {
+ if (comment_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ comment_->clear();
+ }
+ clear_has_comment();
+}
+inline const ::std::string& ClientDownloadReport::comment() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadReport.comment)
+ return *comment_;
+}
+inline void ClientDownloadReport::set_comment(const ::std::string& value) {
+ set_has_comment();
+ if (comment_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ comment_ = new ::std::string;
+ }
+ comment_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientDownloadReport.comment)
+}
+inline void ClientDownloadReport::set_comment(const char* value) {
+ set_has_comment();
+ if (comment_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ comment_ = new ::std::string;
+ }
+ comment_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientDownloadReport.comment)
+}
+inline void ClientDownloadReport::set_comment(const void* value, size_t size) {
+ set_has_comment();
+ if (comment_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ comment_ = new ::std::string;
+ }
+ comment_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientDownloadReport.comment)
+}
+inline ::std::string* ClientDownloadReport::mutable_comment() {
+ set_has_comment();
+ if (comment_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ comment_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadReport.comment)
+ return comment_;
+}
+inline ::std::string* ClientDownloadReport::release_comment() {
+ clear_has_comment();
+ if (comment_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = comment_;
+ comment_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientDownloadReport::set_allocated_comment(::std::string* comment) {
+ if (comment_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete comment_;
+ }
+ if (comment) {
+ set_has_comment();
+ comment_ = comment;
+ } else {
+ clear_has_comment();
+ comment_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadReport.comment)
+}
+
+// optional .safe_browsing.ClientDownloadResponse download_response = 5;
+inline bool ClientDownloadReport::has_download_response() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientDownloadReport::set_has_download_response() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientDownloadReport::clear_has_download_response() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientDownloadReport::clear_download_response() {
+ if (download_response_ != NULL) download_response_->::safe_browsing::ClientDownloadResponse::Clear();
+ clear_has_download_response();
+}
+inline const ::safe_browsing::ClientDownloadResponse& ClientDownloadReport::download_response() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientDownloadReport.download_response)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return download_response_ != NULL ? *download_response_ : *default_instance().download_response_;
+#else
+ return download_response_ != NULL ? *download_response_ : *default_instance_->download_response_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadResponse* ClientDownloadReport::mutable_download_response() {
+ set_has_download_response();
+ if (download_response_ == NULL) download_response_ = new ::safe_browsing::ClientDownloadResponse;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientDownloadReport.download_response)
+ return download_response_;
+}
+inline ::safe_browsing::ClientDownloadResponse* ClientDownloadReport::release_download_response() {
+ clear_has_download_response();
+ ::safe_browsing::ClientDownloadResponse* temp = download_response_;
+ download_response_ = NULL;
+ return temp;
+}
+inline void ClientDownloadReport::set_allocated_download_response(::safe_browsing::ClientDownloadResponse* download_response) {
+ delete download_response_;
+ download_response_ = download_response;
+ if (download_response) {
+ set_has_download_response();
+ } else {
+ clear_has_download_response();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientDownloadReport.download_response)
+}
+
+// -------------------------------------------------------------------
+
+// ClientUploadResponse
+
+// optional .safe_browsing.ClientUploadResponse.UploadStatus status = 1;
+inline bool ClientUploadResponse::has_status() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientUploadResponse::set_has_status() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientUploadResponse::clear_has_status() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientUploadResponse::clear_status() {
+ status_ = 0;
+ clear_has_status();
+}
+inline ::safe_browsing::ClientUploadResponse_UploadStatus ClientUploadResponse::status() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientUploadResponse.status)
+ return static_cast< ::safe_browsing::ClientUploadResponse_UploadStatus >(status_);
+}
+inline void ClientUploadResponse::set_status(::safe_browsing::ClientUploadResponse_UploadStatus value) {
+ assert(::safe_browsing::ClientUploadResponse_UploadStatus_IsValid(value));
+ set_has_status();
+ status_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientUploadResponse.status)
+}
+
+// optional string permalink = 2;
+inline bool ClientUploadResponse::has_permalink() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientUploadResponse::set_has_permalink() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientUploadResponse::clear_has_permalink() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientUploadResponse::clear_permalink() {
+ if (permalink_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ permalink_->clear();
+ }
+ clear_has_permalink();
+}
+inline const ::std::string& ClientUploadResponse::permalink() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientUploadResponse.permalink)
+ return *permalink_;
+}
+inline void ClientUploadResponse::set_permalink(const ::std::string& value) {
+ set_has_permalink();
+ if (permalink_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ permalink_ = new ::std::string;
+ }
+ permalink_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientUploadResponse.permalink)
+}
+inline void ClientUploadResponse::set_permalink(const char* value) {
+ set_has_permalink();
+ if (permalink_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ permalink_ = new ::std::string;
+ }
+ permalink_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientUploadResponse.permalink)
+}
+inline void ClientUploadResponse::set_permalink(const char* value, size_t size) {
+ set_has_permalink();
+ if (permalink_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ permalink_ = new ::std::string;
+ }
+ permalink_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientUploadResponse.permalink)
+}
+inline ::std::string* ClientUploadResponse::mutable_permalink() {
+ set_has_permalink();
+ if (permalink_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ permalink_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientUploadResponse.permalink)
+ return permalink_;
+}
+inline ::std::string* ClientUploadResponse::release_permalink() {
+ clear_has_permalink();
+ if (permalink_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = permalink_;
+ permalink_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientUploadResponse::set_allocated_permalink(::std::string* permalink) {
+ if (permalink_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete permalink_;
+ }
+ if (permalink) {
+ set_has_permalink();
+ permalink_ = permalink;
+ } else {
+ clear_has_permalink();
+ permalink_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientUploadResponse.permalink)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_TrackedPreferenceIncident
+
+// optional string path = 1;
+inline bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident::has_path() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_has_path() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_has_path() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_path() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ clear_has_path();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_TrackedPreferenceIncident::path() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.path)
+ return *path_;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_path(const ::std::string& value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.path)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_path(const char* value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.path)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_path(const char* value, size_t size) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.path)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::mutable_path() {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.path)
+ return path_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::release_path() {
+ clear_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = path_;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_allocated_path(::std::string* path) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (path) {
+ set_has_path();
+ path_ = path;
+ } else {
+ clear_has_path();
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.path)
+}
+
+// optional string atomic_value = 2;
+inline bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident::has_atomic_value() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_has_atomic_value() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_has_atomic_value() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_atomic_value() {
+ if (atomic_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ atomic_value_->clear();
+ }
+ clear_has_atomic_value();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_TrackedPreferenceIncident::atomic_value() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.atomic_value)
+ return *atomic_value_;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_atomic_value(const ::std::string& value) {
+ set_has_atomic_value();
+ if (atomic_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ atomic_value_ = new ::std::string;
+ }
+ atomic_value_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.atomic_value)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_atomic_value(const char* value) {
+ set_has_atomic_value();
+ if (atomic_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ atomic_value_ = new ::std::string;
+ }
+ atomic_value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.atomic_value)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_atomic_value(const char* value, size_t size) {
+ set_has_atomic_value();
+ if (atomic_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ atomic_value_ = new ::std::string;
+ }
+ atomic_value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.atomic_value)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::mutable_atomic_value() {
+ set_has_atomic_value();
+ if (atomic_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ atomic_value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.atomic_value)
+ return atomic_value_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::release_atomic_value() {
+ clear_has_atomic_value();
+ if (atomic_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = atomic_value_;
+ atomic_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_allocated_atomic_value(::std::string* atomic_value) {
+ if (atomic_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete atomic_value_;
+ }
+ if (atomic_value) {
+ set_has_atomic_value();
+ atomic_value_ = atomic_value;
+ } else {
+ clear_has_atomic_value();
+ atomic_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.atomic_value)
+}
+
+// repeated string split_key = 3;
+inline int ClientIncidentReport_IncidentData_TrackedPreferenceIncident::split_key_size() const {
+ return split_key_.size();
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_split_key() {
+ split_key_.Clear();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_TrackedPreferenceIncident::split_key(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+ return split_key_.Get(index);
+}
+inline ::std::string* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::mutable_split_key(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+ return split_key_.Mutable(index);
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_split_key(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+ split_key_.Mutable(index)->assign(value);
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_split_key(int index, const char* value) {
+ split_key_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_split_key(int index, const char* value, size_t size) {
+ split_key_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_TrackedPreferenceIncident::add_split_key() {
+ return split_key_.Add();
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::add_split_key(const ::std::string& value) {
+ split_key_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::add_split_key(const char* value) {
+ split_key_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::add_split_key(const char* value, size_t size) {
+ split_key_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident::split_key() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+ return split_key_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientIncidentReport_IncidentData_TrackedPreferenceIncident::mutable_split_key() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.split_key)
+ return &split_key_;
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.ValueState value_state = 4;
+inline bool ClientIncidentReport_IncidentData_TrackedPreferenceIncident::has_value_state() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_has_value_state() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_has_value_state() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::clear_value_state() {
+ value_state_ = 0;
+ clear_has_value_state();
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState ClientIncidentReport_IncidentData_TrackedPreferenceIncident::value_state() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.value_state)
+ return static_cast< ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState >(value_state_);
+}
+inline void ClientIncidentReport_IncidentData_TrackedPreferenceIncident::set_value_state(::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState value) {
+ assert(::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident_ValueState_IsValid(value));
+ set_has_value_state();
+ value_state_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident.value_state)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile
+
+// optional string relative_path = 1;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::has_relative_path() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_has_relative_path() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::clear_has_relative_path() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::clear_relative_path() {
+ if (relative_path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ relative_path_->clear();
+ }
+ clear_has_relative_path();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::relative_path() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.relative_path)
+ return *relative_path_;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_relative_path(const ::std::string& value) {
+ set_has_relative_path();
+ if (relative_path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ relative_path_ = new ::std::string;
+ }
+ relative_path_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.relative_path)
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_relative_path(const char* value) {
+ set_has_relative_path();
+ if (relative_path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ relative_path_ = new ::std::string;
+ }
+ relative_path_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.relative_path)
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_relative_path(const char* value, size_t size) {
+ set_has_relative_path();
+ if (relative_path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ relative_path_ = new ::std::string;
+ }
+ relative_path_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.relative_path)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::mutable_relative_path() {
+ set_has_relative_path();
+ if (relative_path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ relative_path_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.relative_path)
+ return relative_path_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::release_relative_path() {
+ clear_has_relative_path();
+ if (relative_path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = relative_path_;
+ relative_path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_allocated_relative_path(::std::string* relative_path) {
+ if (relative_path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete relative_path_;
+ }
+ if (relative_path) {
+ set_has_relative_path();
+ relative_path_ = relative_path;
+ } else {
+ clear_has_relative_path();
+ relative_path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.relative_path)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::has_signature() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_has_signature() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::clear_has_signature() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::clear_signature() {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ clear_has_signature();
+}
+inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return signature_ != NULL ? *signature_ : *default_instance().signature_;
+#else
+ return signature_ != NULL ? *signature_ : *default_instance_->signature_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::mutable_signature() {
+ set_has_signature();
+ if (signature_ == NULL) signature_ = new ::safe_browsing::ClientDownloadRequest_SignatureInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.signature)
+ return signature_;
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::release_signature() {
+ clear_has_signature();
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* temp = signature_;
+ signature_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature) {
+ delete signature_;
+ signature_ = signature;
+ if (signature) {
+ set_has_signature();
+ } else {
+ clear_has_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.signature)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile.image_headers)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_BinaryIntegrityIncident
+
+// optional string file_basename = 1;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident::has_file_basename() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_has_file_basename() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_has_file_basename() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_file_basename() {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_->clear();
+ }
+ clear_has_file_basename();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_BinaryIntegrityIncident::file_basename() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.file_basename)
+ return *file_basename_;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_file_basename(const ::std::string& value) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.file_basename)
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_file_basename(const char* value) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.file_basename)
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_file_basename(const char* value, size_t size) {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ file_basename_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.file_basename)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::mutable_file_basename() {
+ set_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_basename_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.file_basename)
+ return file_basename_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::release_file_basename() {
+ clear_has_file_basename();
+ if (file_basename_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = file_basename_;
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_allocated_file_basename(::std::string* file_basename) {
+ if (file_basename_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_basename_;
+ }
+ if (file_basename) {
+ set_has_file_basename();
+ file_basename_ = file_basename;
+ } else {
+ clear_has_file_basename();
+ file_basename_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.file_basename)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 2;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident::has_signature() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_has_signature() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_has_signature() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_signature() {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ clear_has_signature();
+}
+inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& ClientIncidentReport_IncidentData_BinaryIntegrityIncident::signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return signature_ != NULL ? *signature_ : *default_instance().signature_;
+#else
+ return signature_ != NULL ? *signature_ : *default_instance_->signature_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::mutable_signature() {
+ set_has_signature();
+ if (signature_ == NULL) signature_ = new ::safe_browsing::ClientDownloadRequest_SignatureInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.signature)
+ return signature_;
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::release_signature() {
+ clear_has_signature();
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* temp = signature_;
+ signature_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature) {
+ delete signature_;
+ signature_ = signature;
+ if (signature) {
+ set_has_signature();
+ } else {
+ clear_has_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.signature)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 3;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientIncidentReport_IncidentData_BinaryIntegrityIncident::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.image_headers)
+}
+
+// optional int32 sec_error = 4;
+inline bool ClientIncidentReport_IncidentData_BinaryIntegrityIncident::has_sec_error() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_has_sec_error() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_has_sec_error() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_sec_error() {
+ sec_error_ = 0;
+ clear_has_sec_error();
+}
+inline ::google::protobuf::int32 ClientIncidentReport_IncidentData_BinaryIntegrityIncident::sec_error() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.sec_error)
+ return sec_error_;
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::set_sec_error(::google::protobuf::int32 value) {
+ set_has_sec_error();
+ sec_error_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.sec_error)
+}
+
+// repeated .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.ContainedFile contained_file = 5;
+inline int ClientIncidentReport_IncidentData_BinaryIntegrityIncident::contained_file_size() const {
+ return contained_file_.size();
+}
+inline void ClientIncidentReport_IncidentData_BinaryIntegrityIncident::clear_contained_file() {
+ contained_file_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile& ClientIncidentReport_IncidentData_BinaryIntegrityIncident::contained_file(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.contained_file)
+ return contained_file_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::mutable_contained_file(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.contained_file)
+ return contained_file_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile* ClientIncidentReport_IncidentData_BinaryIntegrityIncident::add_contained_file() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.contained_file)
+ return contained_file_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile >&
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident::contained_file() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.contained_file)
+ return contained_file_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile >*
+ClientIncidentReport_IncidentData_BinaryIntegrityIncident::mutable_contained_file() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident.contained_file)
+ return &contained_file_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_BlacklistLoadIncident
+
+// optional string path = 1;
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::has_path() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_has_path() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_has_path() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_path() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ clear_has_path();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_BlacklistLoadIncident::path() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.path)
+ return *path_;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_path(const ::std::string& value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.path)
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_path(const char* value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.path)
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_path(const char* value, size_t size) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.path)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BlacklistLoadIncident::mutable_path() {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.path)
+ return path_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BlacklistLoadIncident::release_path() {
+ clear_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = path_;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_allocated_path(::std::string* path) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (path) {
+ set_has_path();
+ path_ = path;
+ } else {
+ clear_has_path();
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.path)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::has_digest() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_has_digest() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_has_digest() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_digest() {
+ if (digest_ != NULL) digest_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ clear_has_digest();
+}
+inline const ::safe_browsing::ClientDownloadRequest_Digests& ClientIncidentReport_IncidentData_BlacklistLoadIncident::digest() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.digest)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return digest_ != NULL ? *digest_ : *default_instance().digest_;
+#else
+ return digest_ != NULL ? *digest_ : *default_instance_->digest_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientIncidentReport_IncidentData_BlacklistLoadIncident::mutable_digest() {
+ set_has_digest();
+ if (digest_ == NULL) digest_ = new ::safe_browsing::ClientDownloadRequest_Digests;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.digest)
+ return digest_;
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientIncidentReport_IncidentData_BlacklistLoadIncident::release_digest() {
+ clear_has_digest();
+ ::safe_browsing::ClientDownloadRequest_Digests* temp = digest_;
+ digest_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_allocated_digest(::safe_browsing::ClientDownloadRequest_Digests* digest) {
+ delete digest_;
+ digest_ = digest;
+ if (digest) {
+ set_has_digest();
+ } else {
+ clear_has_digest();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.digest)
+}
+
+// optional string version = 3;
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::has_version() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_has_version() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_has_version() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_version() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ clear_has_version();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_BlacklistLoadIncident::version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.version)
+ return *version_;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_version(const ::std::string& value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.version)
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_version(const char* value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.version)
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_version(const char* value, size_t size) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.version)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BlacklistLoadIncident::mutable_version() {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.version)
+ return version_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_BlacklistLoadIncident::release_version() {
+ clear_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = version_;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_allocated_version(::std::string* version) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (version) {
+ set_has_version();
+ version_ = version;
+ } else {
+ clear_has_version();
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.version)
+}
+
+// optional bool blacklist_initialized = 4;
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::has_blacklist_initialized() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_has_blacklist_initialized() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_has_blacklist_initialized() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_blacklist_initialized() {
+ blacklist_initialized_ = false;
+ clear_has_blacklist_initialized();
+}
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::blacklist_initialized() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.blacklist_initialized)
+ return blacklist_initialized_;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_blacklist_initialized(bool value) {
+ set_has_blacklist_initialized();
+ blacklist_initialized_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.blacklist_initialized)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 5;
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::has_signature() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_has_signature() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_has_signature() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_signature() {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ clear_has_signature();
+}
+inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& ClientIncidentReport_IncidentData_BlacklistLoadIncident::signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return signature_ != NULL ? *signature_ : *default_instance().signature_;
+#else
+ return signature_ != NULL ? *signature_ : *default_instance_->signature_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_BlacklistLoadIncident::mutable_signature() {
+ set_has_signature();
+ if (signature_ == NULL) signature_ = new ::safe_browsing::ClientDownloadRequest_SignatureInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.signature)
+ return signature_;
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_BlacklistLoadIncident::release_signature() {
+ clear_has_signature();
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* temp = signature_;
+ signature_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature) {
+ delete signature_;
+ signature_ = signature;
+ if (signature) {
+ set_has_signature();
+ } else {
+ clear_has_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.signature)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 6;
+inline bool ClientIncidentReport_IncidentData_BlacklistLoadIncident::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientIncidentReport_IncidentData_BlacklistLoadIncident::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_BlacklistLoadIncident::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_BlacklistLoadIncident::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_BlacklistLoadIncident::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident.image_headers)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident
+
+// optional string variations_seed_signature = 1;
+inline bool ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::has_variations_seed_signature() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::set_has_variations_seed_signature() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::clear_has_variations_seed_signature() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::clear_variations_seed_signature() {
+ if (variations_seed_signature_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ variations_seed_signature_->clear();
+ }
+ clear_has_variations_seed_signature();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::variations_seed_signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident.variations_seed_signature)
+ return *variations_seed_signature_;
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::set_variations_seed_signature(const ::std::string& value) {
+ set_has_variations_seed_signature();
+ if (variations_seed_signature_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ variations_seed_signature_ = new ::std::string;
+ }
+ variations_seed_signature_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident.variations_seed_signature)
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::set_variations_seed_signature(const char* value) {
+ set_has_variations_seed_signature();
+ if (variations_seed_signature_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ variations_seed_signature_ = new ::std::string;
+ }
+ variations_seed_signature_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident.variations_seed_signature)
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::set_variations_seed_signature(const char* value, size_t size) {
+ set_has_variations_seed_signature();
+ if (variations_seed_signature_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ variations_seed_signature_ = new ::std::string;
+ }
+ variations_seed_signature_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident.variations_seed_signature)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::mutable_variations_seed_signature() {
+ set_has_variations_seed_signature();
+ if (variations_seed_signature_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ variations_seed_signature_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident.variations_seed_signature)
+ return variations_seed_signature_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::release_variations_seed_signature() {
+ clear_has_variations_seed_signature();
+ if (variations_seed_signature_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = variations_seed_signature_;
+ variations_seed_signature_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::set_allocated_variations_seed_signature(::std::string* variations_seed_signature) {
+ if (variations_seed_signature_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete variations_seed_signature_;
+ }
+ if (variations_seed_signature) {
+ set_has_variations_seed_signature();
+ variations_seed_signature_ = variations_seed_signature;
+ } else {
+ clear_has_variations_seed_signature();
+ variations_seed_signature_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident.variations_seed_signature)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_ResourceRequestIncident
+
+// optional bytes digest = 1;
+inline bool ClientIncidentReport_IncidentData_ResourceRequestIncident::has_digest() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_has_digest() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::clear_has_digest() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::clear_digest() {
+ if (digest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ digest_->clear();
+ }
+ clear_has_digest();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_ResourceRequestIncident::digest() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.digest)
+ return *digest_;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_digest(const ::std::string& value) {
+ set_has_digest();
+ if (digest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ digest_ = new ::std::string;
+ }
+ digest_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.digest)
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_digest(const char* value) {
+ set_has_digest();
+ if (digest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ digest_ = new ::std::string;
+ }
+ digest_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.digest)
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_digest(const void* value, size_t size) {
+ set_has_digest();
+ if (digest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ digest_ = new ::std::string;
+ }
+ digest_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.digest)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_ResourceRequestIncident::mutable_digest() {
+ set_has_digest();
+ if (digest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ digest_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.digest)
+ return digest_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_ResourceRequestIncident::release_digest() {
+ clear_has_digest();
+ if (digest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = digest_;
+ digest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_allocated_digest(::std::string* digest) {
+ if (digest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete digest_;
+ }
+ if (digest) {
+ set_has_digest();
+ digest_ = digest;
+ } else {
+ clear_has_digest();
+ digest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.digest)
+}
+
+// optional string origin = 2;
+inline bool ClientIncidentReport_IncidentData_ResourceRequestIncident::has_origin() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_has_origin() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::clear_has_origin() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::clear_origin() {
+ if (origin_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ origin_->clear();
+ }
+ clear_has_origin();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_ResourceRequestIncident::origin() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.origin)
+ return *origin_;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_origin(const ::std::string& value) {
+ set_has_origin();
+ if (origin_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ origin_ = new ::std::string;
+ }
+ origin_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.origin)
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_origin(const char* value) {
+ set_has_origin();
+ if (origin_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ origin_ = new ::std::string;
+ }
+ origin_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.origin)
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_origin(const char* value, size_t size) {
+ set_has_origin();
+ if (origin_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ origin_ = new ::std::string;
+ }
+ origin_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.origin)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_ResourceRequestIncident::mutable_origin() {
+ set_has_origin();
+ if (origin_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ origin_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.origin)
+ return origin_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_ResourceRequestIncident::release_origin() {
+ clear_has_origin();
+ if (origin_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = origin_;
+ origin_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_allocated_origin(::std::string* origin) {
+ if (origin_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete origin_;
+ }
+ if (origin) {
+ set_has_origin();
+ origin_ = origin;
+ } else {
+ clear_has_origin();
+ origin_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.origin)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.Type type = 3 [default = UNKNOWN];
+inline bool ClientIncidentReport_IncidentData_ResourceRequestIncident::has_type() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_has_type() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::clear_has_type() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::clear_type() {
+ type_ = 0;
+ clear_has_type();
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type ClientIncidentReport_IncidentData_ResourceRequestIncident::type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.type)
+ return static_cast< ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type >(type_);
+}
+inline void ClientIncidentReport_IncidentData_ResourceRequestIncident::set_type(::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type value) {
+ assert(::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident_Type_IsValid(value));
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident.type)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData_SuspiciousModuleIncident
+
+// optional string path = 1;
+inline bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::has_path() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_has_path() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_has_path() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_path() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ clear_has_path();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_SuspiciousModuleIncident::path() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.path)
+ return *path_;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_path(const ::std::string& value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.path)
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_path(const char* value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.path)
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_path(const char* value, size_t size) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.path)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::mutable_path() {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.path)
+ return path_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::release_path() {
+ clear_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = path_;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_allocated_path(::std::string* path) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (path) {
+ set_has_path();
+ path_ = path;
+ } else {
+ clear_has_path();
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.path)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.Digests digest = 2;
+inline bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::has_digest() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_has_digest() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_has_digest() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_digest() {
+ if (digest_ != NULL) digest_->::safe_browsing::ClientDownloadRequest_Digests::Clear();
+ clear_has_digest();
+}
+inline const ::safe_browsing::ClientDownloadRequest_Digests& ClientIncidentReport_IncidentData_SuspiciousModuleIncident::digest() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.digest)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return digest_ != NULL ? *digest_ : *default_instance().digest_;
+#else
+ return digest_ != NULL ? *digest_ : *default_instance_->digest_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::mutable_digest() {
+ set_has_digest();
+ if (digest_ == NULL) digest_ = new ::safe_browsing::ClientDownloadRequest_Digests;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.digest)
+ return digest_;
+}
+inline ::safe_browsing::ClientDownloadRequest_Digests* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::release_digest() {
+ clear_has_digest();
+ ::safe_browsing::ClientDownloadRequest_Digests* temp = digest_;
+ digest_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_allocated_digest(::safe_browsing::ClientDownloadRequest_Digests* digest) {
+ delete digest_;
+ digest_ = digest;
+ if (digest) {
+ set_has_digest();
+ } else {
+ clear_has_digest();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.digest)
+}
+
+// optional string version = 3;
+inline bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::has_version() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_has_version() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_has_version() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_version() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ clear_has_version();
+}
+inline const ::std::string& ClientIncidentReport_IncidentData_SuspiciousModuleIncident::version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.version)
+ return *version_;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_version(const ::std::string& value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.version)
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_version(const char* value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.version)
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_version(const char* value, size_t size) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.version)
+}
+inline ::std::string* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::mutable_version() {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.version)
+ return version_;
+}
+inline ::std::string* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::release_version() {
+ clear_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = version_;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_allocated_version(::std::string* version) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (version) {
+ set_has_version();
+ version_ = version;
+ } else {
+ clear_has_version();
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.version)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.SignatureInfo signature = 4;
+inline bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::has_signature() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_has_signature() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_has_signature() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_signature() {
+ if (signature_ != NULL) signature_->::safe_browsing::ClientDownloadRequest_SignatureInfo::Clear();
+ clear_has_signature();
+}
+inline const ::safe_browsing::ClientDownloadRequest_SignatureInfo& ClientIncidentReport_IncidentData_SuspiciousModuleIncident::signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return signature_ != NULL ? *signature_ : *default_instance().signature_;
+#else
+ return signature_ != NULL ? *signature_ : *default_instance_->signature_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::mutable_signature() {
+ set_has_signature();
+ if (signature_ == NULL) signature_ = new ::safe_browsing::ClientDownloadRequest_SignatureInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.signature)
+ return signature_;
+}
+inline ::safe_browsing::ClientDownloadRequest_SignatureInfo* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::release_signature() {
+ clear_has_signature();
+ ::safe_browsing::ClientDownloadRequest_SignatureInfo* temp = signature_;
+ signature_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_allocated_signature(::safe_browsing::ClientDownloadRequest_SignatureInfo* signature) {
+ delete signature_;
+ signature_ = signature;
+ if (signature) {
+ set_has_signature();
+ } else {
+ clear_has_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.signature)
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+inline bool ClientIncidentReport_IncidentData_SuspiciousModuleIncident::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientIncidentReport_IncidentData_SuspiciousModuleIncident::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_IncidentData_SuspiciousModuleIncident::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData_SuspiciousModuleIncident::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident.image_headers)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_IncidentData
+
+// optional int64 incident_time_msec = 1;
+inline bool ClientIncidentReport_IncidentData::has_incident_time_msec() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_incident_time_msec() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_incident_time_msec() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_IncidentData::clear_incident_time_msec() {
+ incident_time_msec_ = GOOGLE_LONGLONG(0);
+ clear_has_incident_time_msec();
+}
+inline ::google::protobuf::int64 ClientIncidentReport_IncidentData::incident_time_msec() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.incident_time_msec)
+ return incident_time_msec_;
+}
+inline void ClientIncidentReport_IncidentData::set_incident_time_msec(::google::protobuf::int64 value) {
+ set_has_incident_time_msec();
+ incident_time_msec_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.IncidentData.incident_time_msec)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.TrackedPreferenceIncident tracked_preference = 2;
+inline bool ClientIncidentReport_IncidentData::has_tracked_preference() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_tracked_preference() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_tracked_preference() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_IncidentData::clear_tracked_preference() {
+ if (tracked_preference_ != NULL) tracked_preference_->::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident::Clear();
+ clear_has_tracked_preference();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident& ClientIncidentReport_IncidentData::tracked_preference() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.tracked_preference)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return tracked_preference_ != NULL ? *tracked_preference_ : *default_instance().tracked_preference_;
+#else
+ return tracked_preference_ != NULL ? *tracked_preference_ : *default_instance_->tracked_preference_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* ClientIncidentReport_IncidentData::mutable_tracked_preference() {
+ set_has_tracked_preference();
+ if (tracked_preference_ == NULL) tracked_preference_ = new ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.tracked_preference)
+ return tracked_preference_;
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* ClientIncidentReport_IncidentData::release_tracked_preference() {
+ clear_has_tracked_preference();
+ ::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* temp = tracked_preference_;
+ tracked_preference_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData::set_allocated_tracked_preference(::safe_browsing::ClientIncidentReport_IncidentData_TrackedPreferenceIncident* tracked_preference) {
+ delete tracked_preference_;
+ tracked_preference_ = tracked_preference;
+ if (tracked_preference) {
+ set_has_tracked_preference();
+ } else {
+ clear_has_tracked_preference();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.tracked_preference)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.BinaryIntegrityIncident binary_integrity = 3;
+inline bool ClientIncidentReport_IncidentData::has_binary_integrity() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_binary_integrity() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_binary_integrity() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_IncidentData::clear_binary_integrity() {
+ if (binary_integrity_ != NULL) binary_integrity_->::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident::Clear();
+ clear_has_binary_integrity();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident& ClientIncidentReport_IncidentData::binary_integrity() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.binary_integrity)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return binary_integrity_ != NULL ? *binary_integrity_ : *default_instance().binary_integrity_;
+#else
+ return binary_integrity_ != NULL ? *binary_integrity_ : *default_instance_->binary_integrity_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* ClientIncidentReport_IncidentData::mutable_binary_integrity() {
+ set_has_binary_integrity();
+ if (binary_integrity_ == NULL) binary_integrity_ = new ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.binary_integrity)
+ return binary_integrity_;
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* ClientIncidentReport_IncidentData::release_binary_integrity() {
+ clear_has_binary_integrity();
+ ::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* temp = binary_integrity_;
+ binary_integrity_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData::set_allocated_binary_integrity(::safe_browsing::ClientIncidentReport_IncidentData_BinaryIntegrityIncident* binary_integrity) {
+ delete binary_integrity_;
+ binary_integrity_ = binary_integrity;
+ if (binary_integrity) {
+ set_has_binary_integrity();
+ } else {
+ clear_has_binary_integrity();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.binary_integrity)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.BlacklistLoadIncident blacklist_load = 4;
+inline bool ClientIncidentReport_IncidentData::has_blacklist_load() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_blacklist_load() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_blacklist_load() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_IncidentData::clear_blacklist_load() {
+ if (blacklist_load_ != NULL) blacklist_load_->::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident::Clear();
+ clear_has_blacklist_load();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident& ClientIncidentReport_IncidentData::blacklist_load() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.blacklist_load)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return blacklist_load_ != NULL ? *blacklist_load_ : *default_instance().blacklist_load_;
+#else
+ return blacklist_load_ != NULL ? *blacklist_load_ : *default_instance_->blacklist_load_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* ClientIncidentReport_IncidentData::mutable_blacklist_load() {
+ set_has_blacklist_load();
+ if (blacklist_load_ == NULL) blacklist_load_ = new ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.blacklist_load)
+ return blacklist_load_;
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* ClientIncidentReport_IncidentData::release_blacklist_load() {
+ clear_has_blacklist_load();
+ ::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* temp = blacklist_load_;
+ blacklist_load_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData::set_allocated_blacklist_load(::safe_browsing::ClientIncidentReport_IncidentData_BlacklistLoadIncident* blacklist_load) {
+ delete blacklist_load_;
+ blacklist_load_ = blacklist_load;
+ if (blacklist_load) {
+ set_has_blacklist_load();
+ } else {
+ clear_has_blacklist_load();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.blacklist_load)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.VariationsSeedSignatureIncident variations_seed_signature = 6;
+inline bool ClientIncidentReport_IncidentData::has_variations_seed_signature() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_variations_seed_signature() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_variations_seed_signature() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport_IncidentData::clear_variations_seed_signature() {
+ if (variations_seed_signature_ != NULL) variations_seed_signature_->::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident::Clear();
+ clear_has_variations_seed_signature();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident& ClientIncidentReport_IncidentData::variations_seed_signature() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.variations_seed_signature)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return variations_seed_signature_ != NULL ? *variations_seed_signature_ : *default_instance().variations_seed_signature_;
+#else
+ return variations_seed_signature_ != NULL ? *variations_seed_signature_ : *default_instance_->variations_seed_signature_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* ClientIncidentReport_IncidentData::mutable_variations_seed_signature() {
+ set_has_variations_seed_signature();
+ if (variations_seed_signature_ == NULL) variations_seed_signature_ = new ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.variations_seed_signature)
+ return variations_seed_signature_;
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* ClientIncidentReport_IncidentData::release_variations_seed_signature() {
+ clear_has_variations_seed_signature();
+ ::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* temp = variations_seed_signature_;
+ variations_seed_signature_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData::set_allocated_variations_seed_signature(::safe_browsing::ClientIncidentReport_IncidentData_VariationsSeedSignatureIncident* variations_seed_signature) {
+ delete variations_seed_signature_;
+ variations_seed_signature_ = variations_seed_signature;
+ if (variations_seed_signature) {
+ set_has_variations_seed_signature();
+ } else {
+ clear_has_variations_seed_signature();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.variations_seed_signature)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.ResourceRequestIncident resource_request = 7;
+inline bool ClientIncidentReport_IncidentData::has_resource_request() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_resource_request() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_resource_request() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientIncidentReport_IncidentData::clear_resource_request() {
+ if (resource_request_ != NULL) resource_request_->::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident::Clear();
+ clear_has_resource_request();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident& ClientIncidentReport_IncidentData::resource_request() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.resource_request)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return resource_request_ != NULL ? *resource_request_ : *default_instance().resource_request_;
+#else
+ return resource_request_ != NULL ? *resource_request_ : *default_instance_->resource_request_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* ClientIncidentReport_IncidentData::mutable_resource_request() {
+ set_has_resource_request();
+ if (resource_request_ == NULL) resource_request_ = new ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.resource_request)
+ return resource_request_;
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* ClientIncidentReport_IncidentData::release_resource_request() {
+ clear_has_resource_request();
+ ::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* temp = resource_request_;
+ resource_request_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData::set_allocated_resource_request(::safe_browsing::ClientIncidentReport_IncidentData_ResourceRequestIncident* resource_request) {
+ delete resource_request_;
+ resource_request_ = resource_request;
+ if (resource_request) {
+ set_has_resource_request();
+ } else {
+ clear_has_resource_request();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.resource_request)
+}
+
+// optional .safe_browsing.ClientIncidentReport.IncidentData.SuspiciousModuleIncident suspicious_module = 8;
+inline bool ClientIncidentReport_IncidentData::has_suspicious_module() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientIncidentReport_IncidentData::set_has_suspicious_module() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientIncidentReport_IncidentData::clear_has_suspicious_module() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientIncidentReport_IncidentData::clear_suspicious_module() {
+ if (suspicious_module_ != NULL) suspicious_module_->::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident::Clear();
+ clear_has_suspicious_module();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident& ClientIncidentReport_IncidentData::suspicious_module() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.IncidentData.suspicious_module)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return suspicious_module_ != NULL ? *suspicious_module_ : *default_instance().suspicious_module_;
+#else
+ return suspicious_module_ != NULL ? *suspicious_module_ : *default_instance_->suspicious_module_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* ClientIncidentReport_IncidentData::mutable_suspicious_module() {
+ set_has_suspicious_module();
+ if (suspicious_module_ == NULL) suspicious_module_ = new ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.IncidentData.suspicious_module)
+ return suspicious_module_;
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* ClientIncidentReport_IncidentData::release_suspicious_module() {
+ clear_has_suspicious_module();
+ ::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* temp = suspicious_module_;
+ suspicious_module_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_IncidentData::set_allocated_suspicious_module(::safe_browsing::ClientIncidentReport_IncidentData_SuspiciousModuleIncident* suspicious_module) {
+ delete suspicious_module_;
+ suspicious_module_ = suspicious_module;
+ if (suspicious_module) {
+ set_has_suspicious_module();
+ } else {
+ clear_has_suspicious_module();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.IncidentData.suspicious_module)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_DownloadDetails
+
+// optional bytes token = 1;
+inline bool ClientIncidentReport_DownloadDetails::has_token() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_DownloadDetails::set_has_token() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_has_token() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_token() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ clear_has_token();
+}
+inline const ::std::string& ClientIncidentReport_DownloadDetails::token() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.DownloadDetails.token)
+ return *token_;
+}
+inline void ClientIncidentReport_DownloadDetails::set_token(const ::std::string& value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.DownloadDetails.token)
+}
+inline void ClientIncidentReport_DownloadDetails::set_token(const char* value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.DownloadDetails.token)
+}
+inline void ClientIncidentReport_DownloadDetails::set_token(const void* value, size_t size) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.DownloadDetails.token)
+}
+inline ::std::string* ClientIncidentReport_DownloadDetails::mutable_token() {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.DownloadDetails.token)
+ return token_;
+}
+inline ::std::string* ClientIncidentReport_DownloadDetails::release_token() {
+ clear_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = token_;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_DownloadDetails::set_allocated_token(::std::string* token) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ if (token) {
+ set_has_token();
+ token_ = token;
+ } else {
+ clear_has_token();
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.DownloadDetails.token)
+}
+
+// optional .safe_browsing.ClientDownloadRequest download = 2;
+inline bool ClientIncidentReport_DownloadDetails::has_download() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_DownloadDetails::set_has_download() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_has_download() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_download() {
+ if (download_ != NULL) download_->::safe_browsing::ClientDownloadRequest::Clear();
+ clear_has_download();
+}
+inline const ::safe_browsing::ClientDownloadRequest& ClientIncidentReport_DownloadDetails::download() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.DownloadDetails.download)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return download_ != NULL ? *download_ : *default_instance().download_;
+#else
+ return download_ != NULL ? *download_ : *default_instance_->download_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest* ClientIncidentReport_DownloadDetails::mutable_download() {
+ set_has_download();
+ if (download_ == NULL) download_ = new ::safe_browsing::ClientDownloadRequest;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.DownloadDetails.download)
+ return download_;
+}
+inline ::safe_browsing::ClientDownloadRequest* ClientIncidentReport_DownloadDetails::release_download() {
+ clear_has_download();
+ ::safe_browsing::ClientDownloadRequest* temp = download_;
+ download_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_DownloadDetails::set_allocated_download(::safe_browsing::ClientDownloadRequest* download) {
+ delete download_;
+ download_ = download;
+ if (download) {
+ set_has_download();
+ } else {
+ clear_has_download();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.DownloadDetails.download)
+}
+
+// optional int64 download_time_msec = 3;
+inline bool ClientIncidentReport_DownloadDetails::has_download_time_msec() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_DownloadDetails::set_has_download_time_msec() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_has_download_time_msec() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_download_time_msec() {
+ download_time_msec_ = GOOGLE_LONGLONG(0);
+ clear_has_download_time_msec();
+}
+inline ::google::protobuf::int64 ClientIncidentReport_DownloadDetails::download_time_msec() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.DownloadDetails.download_time_msec)
+ return download_time_msec_;
+}
+inline void ClientIncidentReport_DownloadDetails::set_download_time_msec(::google::protobuf::int64 value) {
+ set_has_download_time_msec();
+ download_time_msec_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.DownloadDetails.download_time_msec)
+}
+
+// optional int64 open_time_msec = 4;
+inline bool ClientIncidentReport_DownloadDetails::has_open_time_msec() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_DownloadDetails::set_has_open_time_msec() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_has_open_time_msec() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_DownloadDetails::clear_open_time_msec() {
+ open_time_msec_ = GOOGLE_LONGLONG(0);
+ clear_has_open_time_msec();
+}
+inline ::google::protobuf::int64 ClientIncidentReport_DownloadDetails::open_time_msec() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.DownloadDetails.open_time_msec)
+ return open_time_msec_;
+}
+inline void ClientIncidentReport_DownloadDetails::set_open_time_msec(::google::protobuf::int64 value) {
+ set_has_open_time_msec();
+ open_time_msec_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.DownloadDetails.open_time_msec)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_OS_RegistryValue
+
+// optional string name = 1;
+inline bool ClientIncidentReport_EnvironmentData_OS_RegistryValue::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_OS_RegistryValue::name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.name)
+ return *name_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.name)
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.name)
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.name)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS_RegistryValue::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.name)
+ return name_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS_RegistryValue::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.name)
+}
+
+// optional uint32 type = 2;
+inline bool ClientIncidentReport_EnvironmentData_OS_RegistryValue::has_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_has_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::clear_has_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::clear_type() {
+ type_ = 0u;
+ clear_has_type();
+}
+inline ::google::protobuf::uint32 ClientIncidentReport_EnvironmentData_OS_RegistryValue::type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.type)
+ return type_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_type(::google::protobuf::uint32 value) {
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.type)
+}
+
+// optional bytes data = 3;
+inline bool ClientIncidentReport_EnvironmentData_OS_RegistryValue::has_data() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_has_data() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::clear_has_data() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::clear_data() {
+ if (data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ data_->clear();
+ }
+ clear_has_data();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_OS_RegistryValue::data() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.data)
+ return *data_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_data(const ::std::string& value) {
+ set_has_data();
+ if (data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ data_ = new ::std::string;
+ }
+ data_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.data)
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_data(const char* value) {
+ set_has_data();
+ if (data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ data_ = new ::std::string;
+ }
+ data_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.data)
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_data(const void* value, size_t size) {
+ set_has_data();
+ if (data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ data_ = new ::std::string;
+ }
+ data_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.data)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS_RegistryValue::mutable_data() {
+ set_has_data();
+ if (data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ data_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.data)
+ return data_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS_RegistryValue::release_data() {
+ clear_has_data();
+ if (data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = data_;
+ data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryValue::set_allocated_data(::std::string* data) {
+ if (data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete data_;
+ }
+ if (data) {
+ set_has_data();
+ data_ = data;
+ } else {
+ clear_has_data();
+ data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue.data)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_OS_RegistryKey
+
+// optional string name = 1;
+inline bool ClientIncidentReport_EnvironmentData_OS_RegistryKey::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_OS_RegistryKey::name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.name)
+ return *name_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.name)
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.name)
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.name)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS_RegistryKey::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.name)
+ return name_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS_RegistryKey::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.name)
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryValue value = 2;
+inline int ClientIncidentReport_EnvironmentData_OS_RegistryKey::value_size() const {
+ return value_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::clear_value() {
+ value_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue& ClientIncidentReport_EnvironmentData_OS_RegistryKey::value(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.value)
+ return value_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue* ClientIncidentReport_EnvironmentData_OS_RegistryKey::mutable_value(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.value)
+ return value_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue* ClientIncidentReport_EnvironmentData_OS_RegistryKey::add_value() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.value)
+ return value_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue >&
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::value() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.value)
+ return value_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryValue >*
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::mutable_value() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.value)
+ return &value_;
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey key = 3;
+inline int ClientIncidentReport_EnvironmentData_OS_RegistryKey::key_size() const {
+ return key_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_OS_RegistryKey::clear_key() {
+ key_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey& ClientIncidentReport_EnvironmentData_OS_RegistryKey::key(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.key)
+ return key_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* ClientIncidentReport_EnvironmentData_OS_RegistryKey::mutable_key(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.key)
+ return key_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* ClientIncidentReport_EnvironmentData_OS_RegistryKey::add_key() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.key)
+ return key_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >&
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::key() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.key)
+ return key_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >*
+ClientIncidentReport_EnvironmentData_OS_RegistryKey::mutable_key() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey.key)
+ return &key_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_OS
+
+// optional string os_name = 1;
+inline bool ClientIncidentReport_EnvironmentData_OS::has_os_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_has_os_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_has_os_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_os_name() {
+ if (os_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_name_->clear();
+ }
+ clear_has_os_name();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_OS::os_name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_name)
+ return *os_name_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_os_name(const ::std::string& value) {
+ set_has_os_name();
+ if (os_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_name_ = new ::std::string;
+ }
+ os_name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_name)
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_os_name(const char* value) {
+ set_has_os_name();
+ if (os_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_name_ = new ::std::string;
+ }
+ os_name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_name)
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_os_name(const char* value, size_t size) {
+ set_has_os_name();
+ if (os_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_name_ = new ::std::string;
+ }
+ os_name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_name)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS::mutable_os_name() {
+ set_has_os_name();
+ if (os_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_name)
+ return os_name_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS::release_os_name() {
+ clear_has_os_name();
+ if (os_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = os_name_;
+ os_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_allocated_os_name(::std::string* os_name) {
+ if (os_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete os_name_;
+ }
+ if (os_name) {
+ set_has_os_name();
+ os_name_ = os_name;
+ } else {
+ clear_has_os_name();
+ os_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_name)
+}
+
+// optional string os_version = 2;
+inline bool ClientIncidentReport_EnvironmentData_OS::has_os_version() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_has_os_version() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_has_os_version() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_os_version() {
+ if (os_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_version_->clear();
+ }
+ clear_has_os_version();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_OS::os_version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_version)
+ return *os_version_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_os_version(const ::std::string& value) {
+ set_has_os_version();
+ if (os_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_version_ = new ::std::string;
+ }
+ os_version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_version)
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_os_version(const char* value) {
+ set_has_os_version();
+ if (os_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_version_ = new ::std::string;
+ }
+ os_version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_version)
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_os_version(const char* value, size_t size) {
+ set_has_os_version();
+ if (os_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_version_ = new ::std::string;
+ }
+ os_version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_version)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS::mutable_os_version() {
+ set_has_os_version();
+ if (os_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ os_version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_version)
+ return os_version_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_OS::release_os_version() {
+ clear_has_os_version();
+ if (os_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = os_version_;
+ os_version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_allocated_os_version(::std::string* os_version) {
+ if (os_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete os_version_;
+ }
+ if (os_version) {
+ set_has_os_version();
+ os_version_ = os_version;
+ } else {
+ clear_has_os_version();
+ os_version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.OS.os_version)
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.OS.RegistryKey registry_key = 3;
+inline int ClientIncidentReport_EnvironmentData_OS::registry_key_size() const {
+ return registry_key_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_registry_key() {
+ registry_key_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey& ClientIncidentReport_EnvironmentData_OS::registry_key(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.registry_key)
+ return registry_key_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* ClientIncidentReport_EnvironmentData_OS::mutable_registry_key(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.OS.registry_key)
+ return registry_key_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey* ClientIncidentReport_EnvironmentData_OS::add_registry_key() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.OS.registry_key)
+ return registry_key_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >&
+ClientIncidentReport_EnvironmentData_OS::registry_key() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.OS.registry_key)
+ return registry_key_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_OS_RegistryKey >*
+ClientIncidentReport_EnvironmentData_OS::mutable_registry_key() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.OS.registry_key)
+ return &registry_key_;
+}
+
+// optional bool is_enrolled_to_domain = 4;
+inline bool ClientIncidentReport_EnvironmentData_OS::has_is_enrolled_to_domain() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_has_is_enrolled_to_domain() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_has_is_enrolled_to_domain() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::clear_is_enrolled_to_domain() {
+ is_enrolled_to_domain_ = false;
+ clear_has_is_enrolled_to_domain();
+}
+inline bool ClientIncidentReport_EnvironmentData_OS::is_enrolled_to_domain() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.OS.is_enrolled_to_domain)
+ return is_enrolled_to_domain_;
+}
+inline void ClientIncidentReport_EnvironmentData_OS::set_is_enrolled_to_domain(bool value) {
+ set_has_is_enrolled_to_domain();
+ is_enrolled_to_domain_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.OS.is_enrolled_to_domain)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Machine
+
+// optional string cpu_architecture = 1;
+inline bool ClientIncidentReport_EnvironmentData_Machine::has_cpu_architecture() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_has_cpu_architecture() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::clear_has_cpu_architecture() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::clear_cpu_architecture() {
+ if (cpu_architecture_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_architecture_->clear();
+ }
+ clear_has_cpu_architecture();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Machine::cpu_architecture() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_architecture)
+ return *cpu_architecture_;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpu_architecture(const ::std::string& value) {
+ set_has_cpu_architecture();
+ if (cpu_architecture_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_architecture_ = new ::std::string;
+ }
+ cpu_architecture_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_architecture)
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpu_architecture(const char* value) {
+ set_has_cpu_architecture();
+ if (cpu_architecture_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_architecture_ = new ::std::string;
+ }
+ cpu_architecture_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_architecture)
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpu_architecture(const char* value, size_t size) {
+ set_has_cpu_architecture();
+ if (cpu_architecture_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_architecture_ = new ::std::string;
+ }
+ cpu_architecture_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_architecture)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Machine::mutable_cpu_architecture() {
+ set_has_cpu_architecture();
+ if (cpu_architecture_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_architecture_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_architecture)
+ return cpu_architecture_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Machine::release_cpu_architecture() {
+ clear_has_cpu_architecture();
+ if (cpu_architecture_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = cpu_architecture_;
+ cpu_architecture_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_allocated_cpu_architecture(::std::string* cpu_architecture) {
+ if (cpu_architecture_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete cpu_architecture_;
+ }
+ if (cpu_architecture) {
+ set_has_cpu_architecture();
+ cpu_architecture_ = cpu_architecture;
+ } else {
+ clear_has_cpu_architecture();
+ cpu_architecture_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_architecture)
+}
+
+// optional string cpu_vendor = 2;
+inline bool ClientIncidentReport_EnvironmentData_Machine::has_cpu_vendor() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_has_cpu_vendor() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::clear_has_cpu_vendor() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::clear_cpu_vendor() {
+ if (cpu_vendor_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_vendor_->clear();
+ }
+ clear_has_cpu_vendor();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Machine::cpu_vendor() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_vendor)
+ return *cpu_vendor_;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpu_vendor(const ::std::string& value) {
+ set_has_cpu_vendor();
+ if (cpu_vendor_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_vendor_ = new ::std::string;
+ }
+ cpu_vendor_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_vendor)
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpu_vendor(const char* value) {
+ set_has_cpu_vendor();
+ if (cpu_vendor_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_vendor_ = new ::std::string;
+ }
+ cpu_vendor_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_vendor)
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpu_vendor(const char* value, size_t size) {
+ set_has_cpu_vendor();
+ if (cpu_vendor_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_vendor_ = new ::std::string;
+ }
+ cpu_vendor_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_vendor)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Machine::mutable_cpu_vendor() {
+ set_has_cpu_vendor();
+ if (cpu_vendor_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ cpu_vendor_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_vendor)
+ return cpu_vendor_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Machine::release_cpu_vendor() {
+ clear_has_cpu_vendor();
+ if (cpu_vendor_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = cpu_vendor_;
+ cpu_vendor_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_allocated_cpu_vendor(::std::string* cpu_vendor) {
+ if (cpu_vendor_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete cpu_vendor_;
+ }
+ if (cpu_vendor) {
+ set_has_cpu_vendor();
+ cpu_vendor_ = cpu_vendor;
+ } else {
+ clear_has_cpu_vendor();
+ cpu_vendor_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpu_vendor)
+}
+
+// optional uint32 cpuid = 3;
+inline bool ClientIncidentReport_EnvironmentData_Machine::has_cpuid() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_has_cpuid() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::clear_has_cpuid() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::clear_cpuid() {
+ cpuid_ = 0u;
+ clear_has_cpuid();
+}
+inline ::google::protobuf::uint32 ClientIncidentReport_EnvironmentData_Machine::cpuid() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpuid)
+ return cpuid_;
+}
+inline void ClientIncidentReport_EnvironmentData_Machine::set_cpuid(::google::protobuf::uint32 value) {
+ set_has_cpuid();
+ cpuid_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Machine.cpuid)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Process_Patch
+
+// optional string function = 1;
+inline bool ClientIncidentReport_EnvironmentData_Process_Patch::has_function() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_has_function() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::clear_has_function() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::clear_function() {
+ if (function_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ function_->clear();
+ }
+ clear_has_function();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_Patch::function() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.function)
+ return *function_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_function(const ::std::string& value) {
+ set_has_function();
+ if (function_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ function_ = new ::std::string;
+ }
+ function_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.function)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_function(const char* value) {
+ set_has_function();
+ if (function_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ function_ = new ::std::string;
+ }
+ function_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.function)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_function(const char* value, size_t size) {
+ set_has_function();
+ if (function_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ function_ = new ::std::string;
+ }
+ function_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.function)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_Patch::mutable_function() {
+ set_has_function();
+ if (function_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ function_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.function)
+ return function_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_Patch::release_function() {
+ clear_has_function();
+ if (function_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = function_;
+ function_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_allocated_function(::std::string* function) {
+ if (function_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete function_;
+ }
+ if (function) {
+ set_has_function();
+ function_ = function;
+ } else {
+ clear_has_function();
+ function_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.function)
+}
+
+// optional string target_dll = 2;
+inline bool ClientIncidentReport_EnvironmentData_Process_Patch::has_target_dll() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_has_target_dll() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::clear_has_target_dll() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::clear_target_dll() {
+ if (target_dll_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ target_dll_->clear();
+ }
+ clear_has_target_dll();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_Patch::target_dll() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.target_dll)
+ return *target_dll_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_target_dll(const ::std::string& value) {
+ set_has_target_dll();
+ if (target_dll_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ target_dll_ = new ::std::string;
+ }
+ target_dll_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.target_dll)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_target_dll(const char* value) {
+ set_has_target_dll();
+ if (target_dll_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ target_dll_ = new ::std::string;
+ }
+ target_dll_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.target_dll)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_target_dll(const char* value, size_t size) {
+ set_has_target_dll();
+ if (target_dll_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ target_dll_ = new ::std::string;
+ }
+ target_dll_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.target_dll)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_Patch::mutable_target_dll() {
+ set_has_target_dll();
+ if (target_dll_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ target_dll_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.target_dll)
+ return target_dll_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_Patch::release_target_dll() {
+ clear_has_target_dll();
+ if (target_dll_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = target_dll_;
+ target_dll_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Patch::set_allocated_target_dll(::std::string* target_dll) {
+ if (target_dll_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete target_dll_;
+ }
+ if (target_dll) {
+ set_has_target_dll();
+ target_dll_ = target_dll;
+ } else {
+ clear_has_target_dll();
+ target_dll_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch.target_dll)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Process_NetworkProvider
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Process_Dll
+
+// optional string path = 1;
+inline bool ClientIncidentReport_EnvironmentData_Process_Dll::has_path() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_has_path() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_has_path() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_path() {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_->clear();
+ }
+ clear_has_path();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_Dll::path() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.path)
+ return *path_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_path(const ::std::string& value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.path)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_path(const char* value) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.path)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_path(const char* value, size_t size) {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ path_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.path)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_Dll::mutable_path() {
+ set_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ path_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.path)
+ return path_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_Dll::release_path() {
+ clear_has_path();
+ if (path_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = path_;
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_allocated_path(::std::string* path) {
+ if (path_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete path_;
+ }
+ if (path) {
+ set_has_path();
+ path_ = path;
+ } else {
+ clear_has_path();
+ path_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.path)
+}
+
+// optional uint64 base_address = 2;
+inline bool ClientIncidentReport_EnvironmentData_Process_Dll::has_base_address() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_has_base_address() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_has_base_address() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_base_address() {
+ base_address_ = GOOGLE_ULONGLONG(0);
+ clear_has_base_address();
+}
+inline ::google::protobuf::uint64 ClientIncidentReport_EnvironmentData_Process_Dll::base_address() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.base_address)
+ return base_address_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_base_address(::google::protobuf::uint64 value) {
+ set_has_base_address();
+ base_address_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.base_address)
+}
+
+// optional uint32 length = 3;
+inline bool ClientIncidentReport_EnvironmentData_Process_Dll::has_length() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_has_length() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_has_length() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_length() {
+ length_ = 0u;
+ clear_has_length();
+}
+inline ::google::protobuf::uint32 ClientIncidentReport_EnvironmentData_Process_Dll::length() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.length)
+ return length_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_length(::google::protobuf::uint32 value) {
+ set_has_length();
+ length_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.length)
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.Feature feature = 4;
+inline int ClientIncidentReport_EnvironmentData_Process_Dll::feature_size() const {
+ return feature_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_feature() {
+ feature_.Clear();
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature ClientIncidentReport_EnvironmentData_Process_Dll::feature(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.feature)
+ return static_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature >(feature_.Get(index));
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_feature(int index, ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature value) {
+ assert(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid(value));
+ feature_.Set(index, value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.feature)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::add_feature(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature value) {
+ assert(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll_Feature_IsValid(value));
+ feature_.Add(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.feature)
+}
+inline const ::google::protobuf::RepeatedField<int>&
+ClientIncidentReport_EnvironmentData_Process_Dll::feature() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.feature)
+ return feature_;
+}
+inline ::google::protobuf::RepeatedField<int>*
+ClientIncidentReport_EnvironmentData_Process_Dll::mutable_feature() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.feature)
+ return &feature_;
+}
+
+// optional .safe_browsing.ClientDownloadRequest.ImageHeaders image_headers = 5;
+inline bool ClientIncidentReport_EnvironmentData_Process_Dll::has_image_headers() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_has_image_headers() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_has_image_headers() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::clear_image_headers() {
+ if (image_headers_ != NULL) image_headers_->::safe_browsing::ClientDownloadRequest_ImageHeaders::Clear();
+ clear_has_image_headers();
+}
+inline const ::safe_browsing::ClientDownloadRequest_ImageHeaders& ClientIncidentReport_EnvironmentData_Process_Dll::image_headers() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.image_headers)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return image_headers_ != NULL ? *image_headers_ : *default_instance().image_headers_;
+#else
+ return image_headers_ != NULL ? *image_headers_ : *default_instance_->image_headers_;
+#endif
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_EnvironmentData_Process_Dll::mutable_image_headers() {
+ set_has_image_headers();
+ if (image_headers_ == NULL) image_headers_ = new ::safe_browsing::ClientDownloadRequest_ImageHeaders;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.image_headers)
+ return image_headers_;
+}
+inline ::safe_browsing::ClientDownloadRequest_ImageHeaders* ClientIncidentReport_EnvironmentData_Process_Dll::release_image_headers() {
+ clear_has_image_headers();
+ ::safe_browsing::ClientDownloadRequest_ImageHeaders* temp = image_headers_;
+ image_headers_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_Dll::set_allocated_image_headers(::safe_browsing::ClientDownloadRequest_ImageHeaders* image_headers) {
+ delete image_headers_;
+ image_headers_ = image_headers;
+ if (image_headers) {
+ set_has_image_headers();
+ } else {
+ clear_has_image_headers();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll.image_headers)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification
+
+// optional uint32 file_offset = 1;
+inline bool ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::has_file_offset() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_has_file_offset() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_has_file_offset() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_file_offset() {
+ file_offset_ = 0u;
+ clear_has_file_offset();
+}
+inline ::google::protobuf::uint32 ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::file_offset() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.file_offset)
+ return file_offset_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_file_offset(::google::protobuf::uint32 value) {
+ set_has_file_offset();
+ file_offset_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.file_offset)
+}
+
+// optional int32 byte_count = 2;
+inline bool ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::has_byte_count() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_has_byte_count() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_has_byte_count() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_byte_count() {
+ byte_count_ = 0;
+ clear_has_byte_count();
+}
+inline ::google::protobuf::int32 ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::byte_count() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.byte_count)
+ return byte_count_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_byte_count(::google::protobuf::int32 value) {
+ set_has_byte_count();
+ byte_count_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.byte_count)
+}
+
+// optional bytes modified_bytes = 3;
+inline bool ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::has_modified_bytes() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_has_modified_bytes() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_has_modified_bytes() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_modified_bytes() {
+ if (modified_bytes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ modified_bytes_->clear();
+ }
+ clear_has_modified_bytes();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::modified_bytes() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.modified_bytes)
+ return *modified_bytes_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_modified_bytes(const ::std::string& value) {
+ set_has_modified_bytes();
+ if (modified_bytes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ modified_bytes_ = new ::std::string;
+ }
+ modified_bytes_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.modified_bytes)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_modified_bytes(const char* value) {
+ set_has_modified_bytes();
+ if (modified_bytes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ modified_bytes_ = new ::std::string;
+ }
+ modified_bytes_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.modified_bytes)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_modified_bytes(const void* value, size_t size) {
+ set_has_modified_bytes();
+ if (modified_bytes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ modified_bytes_ = new ::std::string;
+ }
+ modified_bytes_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.modified_bytes)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::mutable_modified_bytes() {
+ set_has_modified_bytes();
+ if (modified_bytes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ modified_bytes_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.modified_bytes)
+ return modified_bytes_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::release_modified_bytes() {
+ clear_has_modified_bytes();
+ if (modified_bytes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = modified_bytes_;
+ modified_bytes_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_allocated_modified_bytes(::std::string* modified_bytes) {
+ if (modified_bytes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete modified_bytes_;
+ }
+ if (modified_bytes) {
+ set_has_modified_bytes();
+ modified_bytes_ = modified_bytes;
+ } else {
+ clear_has_modified_bytes();
+ modified_bytes_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.modified_bytes)
+}
+
+// optional string export_name = 4;
+inline bool ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::has_export_name() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_has_export_name() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_has_export_name() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::clear_export_name() {
+ if (export_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_name_->clear();
+ }
+ clear_has_export_name();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::export_name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.export_name)
+ return *export_name_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_export_name(const ::std::string& value) {
+ set_has_export_name();
+ if (export_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_name_ = new ::std::string;
+ }
+ export_name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.export_name)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_export_name(const char* value) {
+ set_has_export_name();
+ if (export_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_name_ = new ::std::string;
+ }
+ export_name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.export_name)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_export_name(const char* value, size_t size) {
+ set_has_export_name();
+ if (export_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_name_ = new ::std::string;
+ }
+ export_name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.export_name)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::mutable_export_name() {
+ set_has_export_name();
+ if (export_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ export_name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.export_name)
+ return export_name_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::release_export_name() {
+ clear_has_export_name();
+ if (export_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = export_name_;
+ export_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification::set_allocated_export_name(::std::string* export_name) {
+ if (export_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete export_name_;
+ }
+ if (export_name) {
+ set_has_export_name();
+ export_name_ = export_name;
+ } else {
+ clear_has_export_name();
+ export_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification.export_name)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Process_ModuleState
+
+// optional string name = 1;
+inline bool ClientIncidentReport_EnvironmentData_Process_ModuleState::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_ModuleState::name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.name)
+ return *name_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.name)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.name)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.name)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.name)
+ return name_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.name)
+}
+
+// optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.ModifiedState modified_state = 2;
+inline bool ClientIncidentReport_EnvironmentData_Process_ModuleState::has_modified_state() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_has_modified_state() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::clear_has_modified_state() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::clear_modified_state() {
+ modified_state_ = 0;
+ clear_has_modified_state();
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState ClientIncidentReport_EnvironmentData_Process_ModuleState::modified_state() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modified_state)
+ return static_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState >(modified_state_);
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_modified_state(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState value) {
+ assert(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_ModifiedState_IsValid(value));
+ set_has_modified_state();
+ modified_state_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modified_state)
+}
+
+// repeated string OBSOLETE_modified_export = 3;
+inline int ClientIncidentReport_EnvironmentData_Process_ModuleState::obsolete_modified_export_size() const {
+ return obsolete_modified_export_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::clear_obsolete_modified_export() {
+ obsolete_modified_export_.Clear();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process_ModuleState::obsolete_modified_export(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+ return obsolete_modified_export_.Get(index);
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState::mutable_obsolete_modified_export(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+ return obsolete_modified_export_.Mutable(index);
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_obsolete_modified_export(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+ obsolete_modified_export_.Mutable(index)->assign(value);
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_obsolete_modified_export(int index, const char* value) {
+ obsolete_modified_export_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::set_obsolete_modified_export(int index, const char* value, size_t size) {
+ obsolete_modified_export_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process_ModuleState::add_obsolete_modified_export() {
+ return obsolete_modified_export_.Add();
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::add_obsolete_modified_export(const ::std::string& value) {
+ obsolete_modified_export_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::add_obsolete_modified_export(const char* value) {
+ obsolete_modified_export_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::add_obsolete_modified_export(const char* value, size_t size) {
+ obsolete_modified_export_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientIncidentReport_EnvironmentData_Process_ModuleState::obsolete_modified_export() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+ return obsolete_modified_export_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientIncidentReport_EnvironmentData_Process_ModuleState::mutable_obsolete_modified_export() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.OBSOLETE_modified_export)
+ return &obsolete_modified_export_;
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.Modification modification = 4;
+inline int ClientIncidentReport_EnvironmentData_Process_ModuleState::modification_size() const {
+ return modification_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process_ModuleState::clear_modification() {
+ modification_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification& ClientIncidentReport_EnvironmentData_Process_ModuleState::modification(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modification)
+ return modification_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* ClientIncidentReport_EnvironmentData_Process_ModuleState::mutable_modification(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modification)
+ return modification_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification* ClientIncidentReport_EnvironmentData_Process_ModuleState::add_modification() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modification)
+ return modification_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification >&
+ClientIncidentReport_EnvironmentData_Process_ModuleState::modification() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modification)
+ return modification_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState_Modification >*
+ClientIncidentReport_EnvironmentData_Process_ModuleState::mutable_modification() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState.modification)
+ return &modification_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData_Process
+
+// optional string version = 1;
+inline bool ClientIncidentReport_EnvironmentData_Process::has_version() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_has_version() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_has_version() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_version() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ clear_has_version();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process::version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.version)
+ return *version_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_version(const ::std::string& value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.version)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_version(const char* value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.version)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_version(const char* value, size_t size) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.version)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process::mutable_version() {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.version)
+ return version_;
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process::release_version() {
+ clear_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = version_;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_allocated_version(::std::string* version) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (version) {
+ set_has_version();
+ version_ = version;
+ } else {
+ clear_has_version();
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.Process.version)
+}
+
+// repeated string OBSOLETE_dlls = 2;
+inline int ClientIncidentReport_EnvironmentData_Process::obsolete_dlls_size() const {
+ return obsolete_dlls_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_obsolete_dlls() {
+ obsolete_dlls_.Clear();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process::obsolete_dlls(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+ return obsolete_dlls_.Get(index);
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process::mutable_obsolete_dlls(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+ return obsolete_dlls_.Mutable(index);
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_obsolete_dlls(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+ obsolete_dlls_.Mutable(index)->assign(value);
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_obsolete_dlls(int index, const char* value) {
+ obsolete_dlls_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_obsolete_dlls(int index, const char* value, size_t size) {
+ obsolete_dlls_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process::add_obsolete_dlls() {
+ return obsolete_dlls_.Add();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::add_obsolete_dlls(const ::std::string& value) {
+ obsolete_dlls_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::add_obsolete_dlls(const char* value) {
+ obsolete_dlls_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::add_obsolete_dlls(const char* value, size_t size) {
+ obsolete_dlls_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientIncidentReport_EnvironmentData_Process::obsolete_dlls() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+ return obsolete_dlls_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientIncidentReport_EnvironmentData_Process::mutable_obsolete_dlls() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.OBSOLETE_dlls)
+ return &obsolete_dlls_;
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Patch patches = 3;
+inline int ClientIncidentReport_EnvironmentData_Process::patches_size() const {
+ return patches_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_patches() {
+ patches_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch& ClientIncidentReport_EnvironmentData_Process::patches(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.patches)
+ return patches_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch* ClientIncidentReport_EnvironmentData_Process::mutable_patches(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.patches)
+ return patches_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch* ClientIncidentReport_EnvironmentData_Process::add_patches() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.patches)
+ return patches_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch >&
+ClientIncidentReport_EnvironmentData_Process::patches() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.patches)
+ return patches_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Patch >*
+ClientIncidentReport_EnvironmentData_Process::mutable_patches() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.patches)
+ return &patches_;
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.NetworkProvider network_providers = 4;
+inline int ClientIncidentReport_EnvironmentData_Process::network_providers_size() const {
+ return network_providers_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_network_providers() {
+ network_providers_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider& ClientIncidentReport_EnvironmentData_Process::network_providers(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.network_providers)
+ return network_providers_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider* ClientIncidentReport_EnvironmentData_Process::mutable_network_providers(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.network_providers)
+ return network_providers_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider* ClientIncidentReport_EnvironmentData_Process::add_network_providers() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.network_providers)
+ return network_providers_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider >&
+ClientIncidentReport_EnvironmentData_Process::network_providers() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.network_providers)
+ return network_providers_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_NetworkProvider >*
+ClientIncidentReport_EnvironmentData_Process::mutable_network_providers() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.network_providers)
+ return &network_providers_;
+}
+
+// optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Channel chrome_update_channel = 5;
+inline bool ClientIncidentReport_EnvironmentData_Process::has_chrome_update_channel() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_has_chrome_update_channel() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_has_chrome_update_channel() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_chrome_update_channel() {
+ chrome_update_channel_ = 0;
+ clear_has_chrome_update_channel();
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel ClientIncidentReport_EnvironmentData_Process::chrome_update_channel() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.chrome_update_channel)
+ return static_cast< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel >(chrome_update_channel_);
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_chrome_update_channel(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel value) {
+ assert(::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Channel_IsValid(value));
+ set_has_chrome_update_channel();
+ chrome_update_channel_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.chrome_update_channel)
+}
+
+// optional int64 uptime_msec = 6;
+inline bool ClientIncidentReport_EnvironmentData_Process::has_uptime_msec() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_has_uptime_msec() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_has_uptime_msec() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_uptime_msec() {
+ uptime_msec_ = GOOGLE_LONGLONG(0);
+ clear_has_uptime_msec();
+}
+inline ::google::protobuf::int64 ClientIncidentReport_EnvironmentData_Process::uptime_msec() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.uptime_msec)
+ return uptime_msec_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_uptime_msec(::google::protobuf::int64 value) {
+ set_has_uptime_msec();
+ uptime_msec_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.uptime_msec)
+}
+
+// optional bool metrics_consent = 7;
+inline bool ClientIncidentReport_EnvironmentData_Process::has_metrics_consent() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_has_metrics_consent() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_has_metrics_consent() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_metrics_consent() {
+ metrics_consent_ = false;
+ clear_has_metrics_consent();
+}
+inline bool ClientIncidentReport_EnvironmentData_Process::metrics_consent() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.metrics_consent)
+ return metrics_consent_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_metrics_consent(bool value) {
+ set_has_metrics_consent();
+ metrics_consent_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.metrics_consent)
+}
+
+// optional bool extended_consent = 8;
+inline bool ClientIncidentReport_EnvironmentData_Process::has_extended_consent() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_has_extended_consent() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_has_extended_consent() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_extended_consent() {
+ extended_consent_ = false;
+ clear_has_extended_consent();
+}
+inline bool ClientIncidentReport_EnvironmentData_Process::extended_consent() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.extended_consent)
+ return extended_consent_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_extended_consent(bool value) {
+ set_has_extended_consent();
+ extended_consent_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.extended_consent)
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.Dll dll = 9;
+inline int ClientIncidentReport_EnvironmentData_Process::dll_size() const {
+ return dll_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_dll() {
+ dll_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll& ClientIncidentReport_EnvironmentData_Process::dll(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.dll)
+ return dll_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll* ClientIncidentReport_EnvironmentData_Process::mutable_dll(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.dll)
+ return dll_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll* ClientIncidentReport_EnvironmentData_Process::add_dll() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.dll)
+ return dll_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll >&
+ClientIncidentReport_EnvironmentData_Process::dll() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.dll)
+ return dll_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_Dll >*
+ClientIncidentReport_EnvironmentData_Process::mutable_dll() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.dll)
+ return &dll_;
+}
+
+// repeated string blacklisted_dll = 10;
+inline int ClientIncidentReport_EnvironmentData_Process::blacklisted_dll_size() const {
+ return blacklisted_dll_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_blacklisted_dll() {
+ blacklisted_dll_.Clear();
+}
+inline const ::std::string& ClientIncidentReport_EnvironmentData_Process::blacklisted_dll(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+ return blacklisted_dll_.Get(index);
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process::mutable_blacklisted_dll(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+ return blacklisted_dll_.Mutable(index);
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_blacklisted_dll(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+ blacklisted_dll_.Mutable(index)->assign(value);
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_blacklisted_dll(int index, const char* value) {
+ blacklisted_dll_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_blacklisted_dll(int index, const char* value, size_t size) {
+ blacklisted_dll_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+}
+inline ::std::string* ClientIncidentReport_EnvironmentData_Process::add_blacklisted_dll() {
+ return blacklisted_dll_.Add();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::add_blacklisted_dll(const ::std::string& value) {
+ blacklisted_dll_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::add_blacklisted_dll(const char* value) {
+ blacklisted_dll_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+}
+inline void ClientIncidentReport_EnvironmentData_Process::add_blacklisted_dll(const char* value, size_t size) {
+ blacklisted_dll_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientIncidentReport_EnvironmentData_Process::blacklisted_dll() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+ return blacklisted_dll_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientIncidentReport_EnvironmentData_Process::mutable_blacklisted_dll() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.blacklisted_dll)
+ return &blacklisted_dll_;
+}
+
+// repeated .safe_browsing.ClientIncidentReport.EnvironmentData.Process.ModuleState module_state = 11;
+inline int ClientIncidentReport_EnvironmentData_Process::module_state_size() const {
+ return module_state_.size();
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_module_state() {
+ module_state_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState& ClientIncidentReport_EnvironmentData_Process::module_state(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.module_state)
+ return module_state_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState* ClientIncidentReport_EnvironmentData_Process::mutable_module_state(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.Process.module_state)
+ return module_state_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState* ClientIncidentReport_EnvironmentData_Process::add_module_state() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.EnvironmentData.Process.module_state)
+ return module_state_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState >&
+ClientIncidentReport_EnvironmentData_Process::module_state() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.module_state)
+ return module_state_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_EnvironmentData_Process_ModuleState >*
+ClientIncidentReport_EnvironmentData_Process::mutable_module_state() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.EnvironmentData.Process.module_state)
+ return &module_state_;
+}
+
+// optional bool field_trial_participant = 12;
+inline bool ClientIncidentReport_EnvironmentData_Process::has_field_trial_participant() const {
+ return (_has_bits_[0] & 0x00000800u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_has_field_trial_participant() {
+ _has_bits_[0] |= 0x00000800u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_has_field_trial_participant() {
+ _has_bits_[0] &= ~0x00000800u;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::clear_field_trial_participant() {
+ field_trial_participant_ = false;
+ clear_has_field_trial_participant();
+}
+inline bool ClientIncidentReport_EnvironmentData_Process::field_trial_participant() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.Process.field_trial_participant)
+ return field_trial_participant_;
+}
+inline void ClientIncidentReport_EnvironmentData_Process::set_field_trial_participant(bool value) {
+ set_has_field_trial_participant();
+ field_trial_participant_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.EnvironmentData.Process.field_trial_participant)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_EnvironmentData
+
+// optional .safe_browsing.ClientIncidentReport.EnvironmentData.OS os = 1;
+inline bool ClientIncidentReport_EnvironmentData::has_os() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData::set_has_os() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData::clear_has_os() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_EnvironmentData::clear_os() {
+ if (os_ != NULL) os_->::safe_browsing::ClientIncidentReport_EnvironmentData_OS::Clear();
+ clear_has_os();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_OS& ClientIncidentReport_EnvironmentData::os() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.os)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return os_ != NULL ? *os_ : *default_instance().os_;
+#else
+ return os_ != NULL ? *os_ : *default_instance_->os_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS* ClientIncidentReport_EnvironmentData::mutable_os() {
+ set_has_os();
+ if (os_ == NULL) os_ = new ::safe_browsing::ClientIncidentReport_EnvironmentData_OS;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.os)
+ return os_;
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_OS* ClientIncidentReport_EnvironmentData::release_os() {
+ clear_has_os();
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_OS* temp = os_;
+ os_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_EnvironmentData::set_allocated_os(::safe_browsing::ClientIncidentReport_EnvironmentData_OS* os) {
+ delete os_;
+ os_ = os;
+ if (os) {
+ set_has_os();
+ } else {
+ clear_has_os();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.os)
+}
+
+// optional .safe_browsing.ClientIncidentReport.EnvironmentData.Machine machine = 2;
+inline bool ClientIncidentReport_EnvironmentData::has_machine() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData::set_has_machine() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData::clear_has_machine() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_EnvironmentData::clear_machine() {
+ if (machine_ != NULL) machine_->::safe_browsing::ClientIncidentReport_EnvironmentData_Machine::Clear();
+ clear_has_machine();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine& ClientIncidentReport_EnvironmentData::machine() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.machine)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return machine_ != NULL ? *machine_ : *default_instance().machine_;
+#else
+ return machine_ != NULL ? *machine_ : *default_instance_->machine_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* ClientIncidentReport_EnvironmentData::mutable_machine() {
+ set_has_machine();
+ if (machine_ == NULL) machine_ = new ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.machine)
+ return machine_;
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* ClientIncidentReport_EnvironmentData::release_machine() {
+ clear_has_machine();
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* temp = machine_;
+ machine_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_EnvironmentData::set_allocated_machine(::safe_browsing::ClientIncidentReport_EnvironmentData_Machine* machine) {
+ delete machine_;
+ machine_ = machine;
+ if (machine) {
+ set_has_machine();
+ } else {
+ clear_has_machine();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.machine)
+}
+
+// optional .safe_browsing.ClientIncidentReport.EnvironmentData.Process process = 3;
+inline bool ClientIncidentReport_EnvironmentData::has_process() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_EnvironmentData::set_has_process() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData::clear_has_process() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_EnvironmentData::clear_process() {
+ if (process_ != NULL) process_->::safe_browsing::ClientIncidentReport_EnvironmentData_Process::Clear();
+ clear_has_process();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData_Process& ClientIncidentReport_EnvironmentData::process() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.EnvironmentData.process)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return process_ != NULL ? *process_ : *default_instance().process_;
+#else
+ return process_ != NULL ? *process_ : *default_instance_->process_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process* ClientIncidentReport_EnvironmentData::mutable_process() {
+ set_has_process();
+ if (process_ == NULL) process_ = new ::safe_browsing::ClientIncidentReport_EnvironmentData_Process;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.EnvironmentData.process)
+ return process_;
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData_Process* ClientIncidentReport_EnvironmentData::release_process() {
+ clear_has_process();
+ ::safe_browsing::ClientIncidentReport_EnvironmentData_Process* temp = process_;
+ process_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_EnvironmentData::set_allocated_process(::safe_browsing::ClientIncidentReport_EnvironmentData_Process* process) {
+ delete process_;
+ process_ = process;
+ if (process) {
+ set_has_process();
+ } else {
+ clear_has_process();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.EnvironmentData.process)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_ExtensionData_ExtensionInfo
+
+// optional string id = 1;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_id() {
+ if (id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ id_->clear();
+ }
+ clear_has_id();
+}
+inline const ::std::string& ClientIncidentReport_ExtensionData_ExtensionInfo::id() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.id)
+ return *id_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_id(const ::std::string& value) {
+ set_has_id();
+ if (id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ id_ = new ::std::string;
+ }
+ id_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.id)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_id(const char* value) {
+ set_has_id();
+ if (id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ id_ = new ::std::string;
+ }
+ id_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.id)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_id(const char* value, size_t size) {
+ set_has_id();
+ if (id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ id_ = new ::std::string;
+ }
+ id_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.id)
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::mutable_id() {
+ set_has_id();
+ if (id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ id_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.id)
+ return id_;
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::release_id() {
+ clear_has_id();
+ if (id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = id_;
+ id_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_allocated_id(::std::string* id) {
+ if (id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete id_;
+ }
+ if (id) {
+ set_has_id();
+ id_ = id;
+ } else {
+ clear_has_id();
+ id_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.id)
+}
+
+// optional string version = 2;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_version() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_version() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_version() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_version() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ clear_has_version();
+}
+inline const ::std::string& ClientIncidentReport_ExtensionData_ExtensionInfo::version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.version)
+ return *version_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_version(const ::std::string& value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.version)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_version(const char* value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.version)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_version(const char* value, size_t size) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.version)
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::mutable_version() {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.version)
+ return version_;
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::release_version() {
+ clear_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = version_;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_allocated_version(::std::string* version) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (version) {
+ set_has_version();
+ version_ = version;
+ } else {
+ clear_has_version();
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.version)
+}
+
+// optional string name = 3;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_name() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_name() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_name() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ClientIncidentReport_ExtensionData_ExtensionInfo::name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.name)
+ return *name_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.name)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.name)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.name)
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.name)
+ return name_;
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.name)
+}
+
+// optional string description = 4;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_description() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_description() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_description() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_description() {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_->clear();
+ }
+ clear_has_description();
+}
+inline const ::std::string& ClientIncidentReport_ExtensionData_ExtensionInfo::description() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.description)
+ return *description_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_description(const ::std::string& value) {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ description_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.description)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_description(const char* value) {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ description_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.description)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_description(const char* value, size_t size) {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ description_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.description)
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::mutable_description() {
+ set_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ description_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.description)
+ return description_;
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::release_description() {
+ clear_has_description();
+ if (description_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = description_;
+ description_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_allocated_description(::std::string* description) {
+ if (description_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete description_;
+ }
+ if (description) {
+ set_has_description();
+ description_ = description;
+ } else {
+ clear_has_description();
+ description_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.description)
+}
+
+// optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.ExtensionState state = 5 [default = STATE_UNKNOWN];
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_state() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_state() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_state() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_state() {
+ state_ = 0;
+ clear_has_state();
+}
+inline ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState ClientIncidentReport_ExtensionData_ExtensionInfo::state() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.state)
+ return static_cast< ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState >(state_);
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_state(::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState value) {
+ assert(::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo_ExtensionState_IsValid(value));
+ set_has_state();
+ state_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.state)
+}
+
+// optional int32 type = 6;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_type() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_type() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_type() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_type() {
+ type_ = 0;
+ clear_has_type();
+}
+inline ::google::protobuf::int32 ClientIncidentReport_ExtensionData_ExtensionInfo::type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.type)
+ return type_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_type(::google::protobuf::int32 value) {
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.type)
+}
+
+// optional string update_url = 7;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_update_url() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_update_url() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_update_url() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_update_url() {
+ if (update_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ update_url_->clear();
+ }
+ clear_has_update_url();
+}
+inline const ::std::string& ClientIncidentReport_ExtensionData_ExtensionInfo::update_url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.update_url)
+ return *update_url_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_update_url(const ::std::string& value) {
+ set_has_update_url();
+ if (update_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ update_url_ = new ::std::string;
+ }
+ update_url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.update_url)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_update_url(const char* value) {
+ set_has_update_url();
+ if (update_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ update_url_ = new ::std::string;
+ }
+ update_url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.update_url)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_update_url(const char* value, size_t size) {
+ set_has_update_url();
+ if (update_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ update_url_ = new ::std::string;
+ }
+ update_url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.update_url)
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::mutable_update_url() {
+ set_has_update_url();
+ if (update_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ update_url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.update_url)
+ return update_url_;
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::release_update_url() {
+ clear_has_update_url();
+ if (update_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = update_url_;
+ update_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_allocated_update_url(::std::string* update_url) {
+ if (update_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete update_url_;
+ }
+ if (update_url) {
+ set_has_update_url();
+ update_url_ = update_url;
+ } else {
+ clear_has_update_url();
+ update_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.update_url)
+}
+
+// optional bool has_signature_validation = 8;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_has_signature_validation() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_has_signature_validation() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_has_signature_validation() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_signature_validation() {
+ has_signature_validation_ = false;
+ clear_has_has_signature_validation();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_signature_validation() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.has_signature_validation)
+ return has_signature_validation_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_signature_validation(bool value) {
+ set_has_has_signature_validation();
+ has_signature_validation_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.has_signature_validation)
+}
+
+// optional bool signature_is_valid = 9;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_signature_is_valid() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_signature_is_valid() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_signature_is_valid() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_signature_is_valid() {
+ signature_is_valid_ = false;
+ clear_has_signature_is_valid();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::signature_is_valid() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.signature_is_valid)
+ return signature_is_valid_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_signature_is_valid(bool value) {
+ set_has_signature_is_valid();
+ signature_is_valid_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.signature_is_valid)
+}
+
+// optional bool installed_by_custodian = 10;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_installed_by_custodian() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_installed_by_custodian() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_installed_by_custodian() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_installed_by_custodian() {
+ installed_by_custodian_ = false;
+ clear_has_installed_by_custodian();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::installed_by_custodian() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.installed_by_custodian)
+ return installed_by_custodian_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_installed_by_custodian(bool value) {
+ set_has_installed_by_custodian();
+ installed_by_custodian_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.installed_by_custodian)
+}
+
+// optional bool installed_by_default = 11;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_installed_by_default() const {
+ return (_has_bits_[0] & 0x00000400u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_installed_by_default() {
+ _has_bits_[0] |= 0x00000400u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_installed_by_default() {
+ _has_bits_[0] &= ~0x00000400u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_installed_by_default() {
+ installed_by_default_ = false;
+ clear_has_installed_by_default();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::installed_by_default() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.installed_by_default)
+ return installed_by_default_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_installed_by_default(bool value) {
+ set_has_installed_by_default();
+ installed_by_default_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.installed_by_default)
+}
+
+// optional bool installed_by_oem = 12;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_installed_by_oem() const {
+ return (_has_bits_[0] & 0x00000800u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_installed_by_oem() {
+ _has_bits_[0] |= 0x00000800u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_installed_by_oem() {
+ _has_bits_[0] &= ~0x00000800u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_installed_by_oem() {
+ installed_by_oem_ = false;
+ clear_has_installed_by_oem();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::installed_by_oem() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.installed_by_oem)
+ return installed_by_oem_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_installed_by_oem(bool value) {
+ set_has_installed_by_oem();
+ installed_by_oem_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.installed_by_oem)
+}
+
+// optional bool from_bookmark = 13;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_from_bookmark() const {
+ return (_has_bits_[0] & 0x00001000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_from_bookmark() {
+ _has_bits_[0] |= 0x00001000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_from_bookmark() {
+ _has_bits_[0] &= ~0x00001000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_from_bookmark() {
+ from_bookmark_ = false;
+ clear_has_from_bookmark();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::from_bookmark() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.from_bookmark)
+ return from_bookmark_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_from_bookmark(bool value) {
+ set_has_from_bookmark();
+ from_bookmark_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.from_bookmark)
+}
+
+// optional bool from_webstore = 14;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_from_webstore() const {
+ return (_has_bits_[0] & 0x00002000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_from_webstore() {
+ _has_bits_[0] |= 0x00002000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_from_webstore() {
+ _has_bits_[0] &= ~0x00002000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_from_webstore() {
+ from_webstore_ = false;
+ clear_has_from_webstore();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::from_webstore() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.from_webstore)
+ return from_webstore_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_from_webstore(bool value) {
+ set_has_from_webstore();
+ from_webstore_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.from_webstore)
+}
+
+// optional bool converted_from_user_script = 15;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_converted_from_user_script() const {
+ return (_has_bits_[0] & 0x00004000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_converted_from_user_script() {
+ _has_bits_[0] |= 0x00004000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_converted_from_user_script() {
+ _has_bits_[0] &= ~0x00004000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_converted_from_user_script() {
+ converted_from_user_script_ = false;
+ clear_has_converted_from_user_script();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::converted_from_user_script() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.converted_from_user_script)
+ return converted_from_user_script_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_converted_from_user_script(bool value) {
+ set_has_converted_from_user_script();
+ converted_from_user_script_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.converted_from_user_script)
+}
+
+// optional bool may_be_untrusted = 16;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_may_be_untrusted() const {
+ return (_has_bits_[0] & 0x00008000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_may_be_untrusted() {
+ _has_bits_[0] |= 0x00008000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_may_be_untrusted() {
+ _has_bits_[0] &= ~0x00008000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_may_be_untrusted() {
+ may_be_untrusted_ = false;
+ clear_has_may_be_untrusted();
+}
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::may_be_untrusted() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.may_be_untrusted)
+ return may_be_untrusted_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_may_be_untrusted(bool value) {
+ set_has_may_be_untrusted();
+ may_be_untrusted_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.may_be_untrusted)
+}
+
+// optional int64 install_time_msec = 17;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_install_time_msec() const {
+ return (_has_bits_[0] & 0x00010000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_install_time_msec() {
+ _has_bits_[0] |= 0x00010000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_install_time_msec() {
+ _has_bits_[0] &= ~0x00010000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_install_time_msec() {
+ install_time_msec_ = GOOGLE_LONGLONG(0);
+ clear_has_install_time_msec();
+}
+inline ::google::protobuf::int64 ClientIncidentReport_ExtensionData_ExtensionInfo::install_time_msec() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.install_time_msec)
+ return install_time_msec_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_install_time_msec(::google::protobuf::int64 value) {
+ set_has_install_time_msec();
+ install_time_msec_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.install_time_msec)
+}
+
+// optional int32 manifest_location_type = 18;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_manifest_location_type() const {
+ return (_has_bits_[0] & 0x00020000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_manifest_location_type() {
+ _has_bits_[0] |= 0x00020000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_manifest_location_type() {
+ _has_bits_[0] &= ~0x00020000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_manifest_location_type() {
+ manifest_location_type_ = 0;
+ clear_has_manifest_location_type();
+}
+inline ::google::protobuf::int32 ClientIncidentReport_ExtensionData_ExtensionInfo::manifest_location_type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest_location_type)
+ return manifest_location_type_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_manifest_location_type(::google::protobuf::int32 value) {
+ set_has_manifest_location_type();
+ manifest_location_type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest_location_type)
+}
+
+// optional string manifest = 19;
+inline bool ClientIncidentReport_ExtensionData_ExtensionInfo::has_manifest() const {
+ return (_has_bits_[0] & 0x00040000u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_has_manifest() {
+ _has_bits_[0] |= 0x00040000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_has_manifest() {
+ _has_bits_[0] &= ~0x00040000u;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::clear_manifest() {
+ if (manifest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ manifest_->clear();
+ }
+ clear_has_manifest();
+}
+inline const ::std::string& ClientIncidentReport_ExtensionData_ExtensionInfo::manifest() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest)
+ return *manifest_;
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_manifest(const ::std::string& value) {
+ set_has_manifest();
+ if (manifest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ manifest_ = new ::std::string;
+ }
+ manifest_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_manifest(const char* value) {
+ set_has_manifest();
+ if (manifest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ manifest_ = new ::std::string;
+ }
+ manifest_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest)
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_manifest(const char* value, size_t size) {
+ set_has_manifest();
+ if (manifest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ manifest_ = new ::std::string;
+ }
+ manifest_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest)
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::mutable_manifest() {
+ set_has_manifest();
+ if (manifest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ manifest_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest)
+ return manifest_;
+}
+inline ::std::string* ClientIncidentReport_ExtensionData_ExtensionInfo::release_manifest() {
+ clear_has_manifest();
+ if (manifest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = manifest_;
+ manifest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_ExtensionData_ExtensionInfo::set_allocated_manifest(::std::string* manifest) {
+ if (manifest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete manifest_;
+ }
+ if (manifest) {
+ set_has_manifest();
+ manifest_ = manifest;
+ } else {
+ clear_has_manifest();
+ manifest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo.manifest)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_ExtensionData
+
+// optional .safe_browsing.ClientIncidentReport.ExtensionData.ExtensionInfo last_installed_extension = 1;
+inline bool ClientIncidentReport_ExtensionData::has_last_installed_extension() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_ExtensionData::set_has_last_installed_extension() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_ExtensionData::clear_has_last_installed_extension() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_ExtensionData::clear_last_installed_extension() {
+ if (last_installed_extension_ != NULL) last_installed_extension_->::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo::Clear();
+ clear_has_last_installed_extension();
+}
+inline const ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo& ClientIncidentReport_ExtensionData::last_installed_extension() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.ExtensionData.last_installed_extension)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return last_installed_extension_ != NULL ? *last_installed_extension_ : *default_instance().last_installed_extension_;
+#else
+ return last_installed_extension_ != NULL ? *last_installed_extension_ : *default_instance_->last_installed_extension_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* ClientIncidentReport_ExtensionData::mutable_last_installed_extension() {
+ set_has_last_installed_extension();
+ if (last_installed_extension_ == NULL) last_installed_extension_ = new ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.ExtensionData.last_installed_extension)
+ return last_installed_extension_;
+}
+inline ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* ClientIncidentReport_ExtensionData::release_last_installed_extension() {
+ clear_has_last_installed_extension();
+ ::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* temp = last_installed_extension_;
+ last_installed_extension_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport_ExtensionData::set_allocated_last_installed_extension(::safe_browsing::ClientIncidentReport_ExtensionData_ExtensionInfo* last_installed_extension) {
+ delete last_installed_extension_;
+ last_installed_extension_ = last_installed_extension;
+ if (last_installed_extension) {
+ set_has_last_installed_extension();
+ } else {
+ clear_has_last_installed_extension();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.ExtensionData.last_installed_extension)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport_NonBinaryDownloadDetails
+
+// optional string file_type = 1;
+inline bool ClientIncidentReport_NonBinaryDownloadDetails::has_file_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_has_file_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_has_file_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_file_type() {
+ if (file_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_type_->clear();
+ }
+ clear_has_file_type();
+}
+inline const ::std::string& ClientIncidentReport_NonBinaryDownloadDetails::file_type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.file_type)
+ return *file_type_;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_file_type(const ::std::string& value) {
+ set_has_file_type();
+ if (file_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_type_ = new ::std::string;
+ }
+ file_type_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.file_type)
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_file_type(const char* value) {
+ set_has_file_type();
+ if (file_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_type_ = new ::std::string;
+ }
+ file_type_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.file_type)
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_file_type(const char* value, size_t size) {
+ set_has_file_type();
+ if (file_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_type_ = new ::std::string;
+ }
+ file_type_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.file_type)
+}
+inline ::std::string* ClientIncidentReport_NonBinaryDownloadDetails::mutable_file_type() {
+ set_has_file_type();
+ if (file_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ file_type_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.file_type)
+ return file_type_;
+}
+inline ::std::string* ClientIncidentReport_NonBinaryDownloadDetails::release_file_type() {
+ clear_has_file_type();
+ if (file_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = file_type_;
+ file_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_allocated_file_type(::std::string* file_type) {
+ if (file_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete file_type_;
+ }
+ if (file_type) {
+ set_has_file_type();
+ file_type_ = file_type;
+ } else {
+ clear_has_file_type();
+ file_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.file_type)
+}
+
+// optional bytes url_spec_sha256 = 2;
+inline bool ClientIncidentReport_NonBinaryDownloadDetails::has_url_spec_sha256() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_has_url_spec_sha256() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_has_url_spec_sha256() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_url_spec_sha256() {
+ if (url_spec_sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_spec_sha256_->clear();
+ }
+ clear_has_url_spec_sha256();
+}
+inline const ::std::string& ClientIncidentReport_NonBinaryDownloadDetails::url_spec_sha256() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.url_spec_sha256)
+ return *url_spec_sha256_;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_url_spec_sha256(const ::std::string& value) {
+ set_has_url_spec_sha256();
+ if (url_spec_sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_spec_sha256_ = new ::std::string;
+ }
+ url_spec_sha256_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.url_spec_sha256)
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_url_spec_sha256(const char* value) {
+ set_has_url_spec_sha256();
+ if (url_spec_sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_spec_sha256_ = new ::std::string;
+ }
+ url_spec_sha256_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.url_spec_sha256)
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_url_spec_sha256(const void* value, size_t size) {
+ set_has_url_spec_sha256();
+ if (url_spec_sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_spec_sha256_ = new ::std::string;
+ }
+ url_spec_sha256_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.url_spec_sha256)
+}
+inline ::std::string* ClientIncidentReport_NonBinaryDownloadDetails::mutable_url_spec_sha256() {
+ set_has_url_spec_sha256();
+ if (url_spec_sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_spec_sha256_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.url_spec_sha256)
+ return url_spec_sha256_;
+}
+inline ::std::string* ClientIncidentReport_NonBinaryDownloadDetails::release_url_spec_sha256() {
+ clear_has_url_spec_sha256();
+ if (url_spec_sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_spec_sha256_;
+ url_spec_sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_allocated_url_spec_sha256(::std::string* url_spec_sha256) {
+ if (url_spec_sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_spec_sha256_;
+ }
+ if (url_spec_sha256) {
+ set_has_url_spec_sha256();
+ url_spec_sha256_ = url_spec_sha256;
+ } else {
+ clear_has_url_spec_sha256();
+ url_spec_sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.url_spec_sha256)
+}
+
+// optional string host = 3;
+inline bool ClientIncidentReport_NonBinaryDownloadDetails::has_host() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_has_host() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_has_host() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_host() {
+ if (host_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ host_->clear();
+ }
+ clear_has_host();
+}
+inline const ::std::string& ClientIncidentReport_NonBinaryDownloadDetails::host() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.host)
+ return *host_;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_host(const ::std::string& value) {
+ set_has_host();
+ if (host_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ host_ = new ::std::string;
+ }
+ host_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.host)
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_host(const char* value) {
+ set_has_host();
+ if (host_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ host_ = new ::std::string;
+ }
+ host_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.host)
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_host(const char* value, size_t size) {
+ set_has_host();
+ if (host_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ host_ = new ::std::string;
+ }
+ host_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.host)
+}
+inline ::std::string* ClientIncidentReport_NonBinaryDownloadDetails::mutable_host() {
+ set_has_host();
+ if (host_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ host_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.host)
+ return host_;
+}
+inline ::std::string* ClientIncidentReport_NonBinaryDownloadDetails::release_host() {
+ clear_has_host();
+ if (host_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = host_;
+ host_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_allocated_host(::std::string* host) {
+ if (host_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete host_;
+ }
+ if (host) {
+ set_has_host();
+ host_ = host;
+ } else {
+ clear_has_host();
+ host_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.host)
+}
+
+// optional int64 length = 4;
+inline bool ClientIncidentReport_NonBinaryDownloadDetails::has_length() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_has_length() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_has_length() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::clear_length() {
+ length_ = GOOGLE_LONGLONG(0);
+ clear_has_length();
+}
+inline ::google::protobuf::int64 ClientIncidentReport_NonBinaryDownloadDetails::length() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.length)
+ return length_;
+}
+inline void ClientIncidentReport_NonBinaryDownloadDetails::set_length(::google::protobuf::int64 value) {
+ set_has_length();
+ length_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails.length)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentReport
+
+// repeated .safe_browsing.ClientIncidentReport.IncidentData incident = 1;
+inline int ClientIncidentReport::incident_size() const {
+ return incident_.size();
+}
+inline void ClientIncidentReport::clear_incident() {
+ incident_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentReport_IncidentData& ClientIncidentReport::incident(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.incident)
+ return incident_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData* ClientIncidentReport::mutable_incident(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.incident)
+ return incident_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentReport_IncidentData* ClientIncidentReport::add_incident() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentReport.incident)
+ return incident_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData >&
+ClientIncidentReport::incident() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentReport.incident)
+ return incident_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentReport_IncidentData >*
+ClientIncidentReport::mutable_incident() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentReport.incident)
+ return &incident_;
+}
+
+// optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+inline bool ClientIncidentReport::has_download() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentReport::set_has_download() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentReport::clear_has_download() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentReport::clear_download() {
+ if (download_ != NULL) download_->::safe_browsing::ClientIncidentReport_DownloadDetails::Clear();
+ clear_has_download();
+}
+inline const ::safe_browsing::ClientIncidentReport_DownloadDetails& ClientIncidentReport::download() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.download)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return download_ != NULL ? *download_ : *default_instance().download_;
+#else
+ return download_ != NULL ? *download_ : *default_instance_->download_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_DownloadDetails* ClientIncidentReport::mutable_download() {
+ set_has_download();
+ if (download_ == NULL) download_ = new ::safe_browsing::ClientIncidentReport_DownloadDetails;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.download)
+ return download_;
+}
+inline ::safe_browsing::ClientIncidentReport_DownloadDetails* ClientIncidentReport::release_download() {
+ clear_has_download();
+ ::safe_browsing::ClientIncidentReport_DownloadDetails* temp = download_;
+ download_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport::set_allocated_download(::safe_browsing::ClientIncidentReport_DownloadDetails* download) {
+ delete download_;
+ download_ = download;
+ if (download) {
+ set_has_download();
+ } else {
+ clear_has_download();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.download)
+}
+
+// optional .safe_browsing.ClientIncidentReport.EnvironmentData environment = 3;
+inline bool ClientIncidentReport::has_environment() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientIncidentReport::set_has_environment() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientIncidentReport::clear_has_environment() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientIncidentReport::clear_environment() {
+ if (environment_ != NULL) environment_->::safe_browsing::ClientIncidentReport_EnvironmentData::Clear();
+ clear_has_environment();
+}
+inline const ::safe_browsing::ClientIncidentReport_EnvironmentData& ClientIncidentReport::environment() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.environment)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return environment_ != NULL ? *environment_ : *default_instance().environment_;
+#else
+ return environment_ != NULL ? *environment_ : *default_instance_->environment_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData* ClientIncidentReport::mutable_environment() {
+ set_has_environment();
+ if (environment_ == NULL) environment_ = new ::safe_browsing::ClientIncidentReport_EnvironmentData;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.environment)
+ return environment_;
+}
+inline ::safe_browsing::ClientIncidentReport_EnvironmentData* ClientIncidentReport::release_environment() {
+ clear_has_environment();
+ ::safe_browsing::ClientIncidentReport_EnvironmentData* temp = environment_;
+ environment_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport::set_allocated_environment(::safe_browsing::ClientIncidentReport_EnvironmentData* environment) {
+ delete environment_;
+ environment_ = environment;
+ if (environment) {
+ set_has_environment();
+ } else {
+ clear_has_environment();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.environment)
+}
+
+// optional .safe_browsing.ChromeUserPopulation population = 7;
+inline bool ClientIncidentReport::has_population() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientIncidentReport::set_has_population() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientIncidentReport::clear_has_population() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientIncidentReport::clear_population() {
+ if (population_ != NULL) population_->::safe_browsing::ChromeUserPopulation::Clear();
+ clear_has_population();
+}
+inline const ::safe_browsing::ChromeUserPopulation& ClientIncidentReport::population() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.population)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return population_ != NULL ? *population_ : *default_instance().population_;
+#else
+ return population_ != NULL ? *population_ : *default_instance_->population_;
+#endif
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientIncidentReport::mutable_population() {
+ set_has_population();
+ if (population_ == NULL) population_ = new ::safe_browsing::ChromeUserPopulation;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.population)
+ return population_;
+}
+inline ::safe_browsing::ChromeUserPopulation* ClientIncidentReport::release_population() {
+ clear_has_population();
+ ::safe_browsing::ChromeUserPopulation* temp = population_;
+ population_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport::set_allocated_population(::safe_browsing::ChromeUserPopulation* population) {
+ delete population_;
+ population_ = population;
+ if (population) {
+ set_has_population();
+ } else {
+ clear_has_population();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.population)
+}
+
+// optional .safe_browsing.ClientIncidentReport.ExtensionData extension_data = 8;
+inline bool ClientIncidentReport::has_extension_data() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientIncidentReport::set_has_extension_data() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientIncidentReport::clear_has_extension_data() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientIncidentReport::clear_extension_data() {
+ if (extension_data_ != NULL) extension_data_->::safe_browsing::ClientIncidentReport_ExtensionData::Clear();
+ clear_has_extension_data();
+}
+inline const ::safe_browsing::ClientIncidentReport_ExtensionData& ClientIncidentReport::extension_data() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.extension_data)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return extension_data_ != NULL ? *extension_data_ : *default_instance().extension_data_;
+#else
+ return extension_data_ != NULL ? *extension_data_ : *default_instance_->extension_data_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_ExtensionData* ClientIncidentReport::mutable_extension_data() {
+ set_has_extension_data();
+ if (extension_data_ == NULL) extension_data_ = new ::safe_browsing::ClientIncidentReport_ExtensionData;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.extension_data)
+ return extension_data_;
+}
+inline ::safe_browsing::ClientIncidentReport_ExtensionData* ClientIncidentReport::release_extension_data() {
+ clear_has_extension_data();
+ ::safe_browsing::ClientIncidentReport_ExtensionData* temp = extension_data_;
+ extension_data_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport::set_allocated_extension_data(::safe_browsing::ClientIncidentReport_ExtensionData* extension_data) {
+ delete extension_data_;
+ extension_data_ = extension_data;
+ if (extension_data) {
+ set_has_extension_data();
+ } else {
+ clear_has_extension_data();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.extension_data)
+}
+
+// optional .safe_browsing.ClientIncidentReport.NonBinaryDownloadDetails non_binary_download = 9;
+inline bool ClientIncidentReport::has_non_binary_download() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientIncidentReport::set_has_non_binary_download() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientIncidentReport::clear_has_non_binary_download() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientIncidentReport::clear_non_binary_download() {
+ if (non_binary_download_ != NULL) non_binary_download_->::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails::Clear();
+ clear_has_non_binary_download();
+}
+inline const ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails& ClientIncidentReport::non_binary_download() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentReport.non_binary_download)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return non_binary_download_ != NULL ? *non_binary_download_ : *default_instance().non_binary_download_;
+#else
+ return non_binary_download_ != NULL ? *non_binary_download_ : *default_instance_->non_binary_download_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* ClientIncidentReport::mutable_non_binary_download() {
+ set_has_non_binary_download();
+ if (non_binary_download_ == NULL) non_binary_download_ = new ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentReport.non_binary_download)
+ return non_binary_download_;
+}
+inline ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* ClientIncidentReport::release_non_binary_download() {
+ clear_has_non_binary_download();
+ ::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* temp = non_binary_download_;
+ non_binary_download_ = NULL;
+ return temp;
+}
+inline void ClientIncidentReport::set_allocated_non_binary_download(::safe_browsing::ClientIncidentReport_NonBinaryDownloadDetails* non_binary_download) {
+ delete non_binary_download_;
+ non_binary_download_ = non_binary_download;
+ if (non_binary_download) {
+ set_has_non_binary_download();
+ } else {
+ clear_has_non_binary_download();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentReport.non_binary_download)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentResponse_EnvironmentRequest
+
+// optional int32 dll_index = 1;
+inline bool ClientIncidentResponse_EnvironmentRequest::has_dll_index() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentResponse_EnvironmentRequest::set_has_dll_index() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentResponse_EnvironmentRequest::clear_has_dll_index() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentResponse_EnvironmentRequest::clear_dll_index() {
+ dll_index_ = 0;
+ clear_has_dll_index();
+}
+inline ::google::protobuf::int32 ClientIncidentResponse_EnvironmentRequest::dll_index() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentResponse.EnvironmentRequest.dll_index)
+ return dll_index_;
+}
+inline void ClientIncidentResponse_EnvironmentRequest::set_dll_index(::google::protobuf::int32 value) {
+ set_has_dll_index();
+ dll_index_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentResponse.EnvironmentRequest.dll_index)
+}
+
+// -------------------------------------------------------------------
+
+// ClientIncidentResponse
+
+// optional bytes token = 1;
+inline bool ClientIncidentResponse::has_token() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientIncidentResponse::set_has_token() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientIncidentResponse::clear_has_token() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientIncidentResponse::clear_token() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ clear_has_token();
+}
+inline const ::std::string& ClientIncidentResponse::token() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentResponse.token)
+ return *token_;
+}
+inline void ClientIncidentResponse::set_token(const ::std::string& value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentResponse.token)
+}
+inline void ClientIncidentResponse::set_token(const char* value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientIncidentResponse.token)
+}
+inline void ClientIncidentResponse::set_token(const void* value, size_t size) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientIncidentResponse.token)
+}
+inline ::std::string* ClientIncidentResponse::mutable_token() {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentResponse.token)
+ return token_;
+}
+inline ::std::string* ClientIncidentResponse::release_token() {
+ clear_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = token_;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientIncidentResponse::set_allocated_token(::std::string* token) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ if (token) {
+ set_has_token();
+ token_ = token;
+ } else {
+ clear_has_token();
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientIncidentResponse.token)
+}
+
+// optional bool download_requested = 2;
+inline bool ClientIncidentResponse::has_download_requested() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientIncidentResponse::set_has_download_requested() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientIncidentResponse::clear_has_download_requested() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientIncidentResponse::clear_download_requested() {
+ download_requested_ = false;
+ clear_has_download_requested();
+}
+inline bool ClientIncidentResponse::download_requested() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentResponse.download_requested)
+ return download_requested_;
+}
+inline void ClientIncidentResponse::set_download_requested(bool value) {
+ set_has_download_requested();
+ download_requested_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientIncidentResponse.download_requested)
+}
+
+// repeated .safe_browsing.ClientIncidentResponse.EnvironmentRequest environment_requests = 3;
+inline int ClientIncidentResponse::environment_requests_size() const {
+ return environment_requests_.size();
+}
+inline void ClientIncidentResponse::clear_environment_requests() {
+ environment_requests_.Clear();
+}
+inline const ::safe_browsing::ClientIncidentResponse_EnvironmentRequest& ClientIncidentResponse::environment_requests(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientIncidentResponse.environment_requests)
+ return environment_requests_.Get(index);
+}
+inline ::safe_browsing::ClientIncidentResponse_EnvironmentRequest* ClientIncidentResponse::mutable_environment_requests(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientIncidentResponse.environment_requests)
+ return environment_requests_.Mutable(index);
+}
+inline ::safe_browsing::ClientIncidentResponse_EnvironmentRequest* ClientIncidentResponse::add_environment_requests() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientIncidentResponse.environment_requests)
+ return environment_requests_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentResponse_EnvironmentRequest >&
+ClientIncidentResponse::environment_requests() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientIncidentResponse.environment_requests)
+ return environment_requests_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientIncidentResponse_EnvironmentRequest >*
+ClientIncidentResponse::mutable_environment_requests() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientIncidentResponse.environment_requests)
+ return &environment_requests_;
+}
+
+// -------------------------------------------------------------------
+
+// DownloadMetadata
+
+// optional uint32 download_id = 1;
+inline bool DownloadMetadata::has_download_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void DownloadMetadata::set_has_download_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void DownloadMetadata::clear_has_download_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void DownloadMetadata::clear_download_id() {
+ download_id_ = 0u;
+ clear_has_download_id();
+}
+inline ::google::protobuf::uint32 DownloadMetadata::download_id() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.DownloadMetadata.download_id)
+ return download_id_;
+}
+inline void DownloadMetadata::set_download_id(::google::protobuf::uint32 value) {
+ set_has_download_id();
+ download_id_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.DownloadMetadata.download_id)
+}
+
+// optional .safe_browsing.ClientIncidentReport.DownloadDetails download = 2;
+inline bool DownloadMetadata::has_download() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void DownloadMetadata::set_has_download() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void DownloadMetadata::clear_has_download() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void DownloadMetadata::clear_download() {
+ if (download_ != NULL) download_->::safe_browsing::ClientIncidentReport_DownloadDetails::Clear();
+ clear_has_download();
+}
+inline const ::safe_browsing::ClientIncidentReport_DownloadDetails& DownloadMetadata::download() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.DownloadMetadata.download)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return download_ != NULL ? *download_ : *default_instance().download_;
+#else
+ return download_ != NULL ? *download_ : *default_instance_->download_;
+#endif
+}
+inline ::safe_browsing::ClientIncidentReport_DownloadDetails* DownloadMetadata::mutable_download() {
+ set_has_download();
+ if (download_ == NULL) download_ = new ::safe_browsing::ClientIncidentReport_DownloadDetails;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.DownloadMetadata.download)
+ return download_;
+}
+inline ::safe_browsing::ClientIncidentReport_DownloadDetails* DownloadMetadata::release_download() {
+ clear_has_download();
+ ::safe_browsing::ClientIncidentReport_DownloadDetails* temp = download_;
+ download_ = NULL;
+ return temp;
+}
+inline void DownloadMetadata::set_allocated_download(::safe_browsing::ClientIncidentReport_DownloadDetails* download) {
+ delete download_;
+ download_ = download;
+ if (download) {
+ set_has_download();
+ } else {
+ clear_has_download();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.DownloadMetadata.download)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest_HTTPHeader
+
+// required bytes name = 1;
+inline bool ClientSafeBrowsingReportRequest_HTTPHeader::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPHeader::name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.name)
+ return *name_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.name)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.name)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_name(const void* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.name)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPHeader::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.name)
+ return name_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPHeader::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.name)
+}
+
+// optional bytes value = 2;
+inline bool ClientSafeBrowsingReportRequest_HTTPHeader::has_value() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_has_value() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::clear_has_value() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::clear_value() {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_->clear();
+ }
+ clear_has_value();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPHeader::value() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.value)
+ return *value_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_value(const ::std::string& value) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.value)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_value(const char* value) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.value)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_value(const void* value, size_t size) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.value)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPHeader::mutable_value() {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.value)
+ return value_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPHeader::release_value() {
+ clear_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = value_;
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPHeader::set_allocated_value(::std::string* value) {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete value_;
+ }
+ if (value) {
+ set_has_value();
+ value_ = value;
+ } else {
+ clear_has_value();
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader.value)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine
+
+// optional bytes verb = 1;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::has_verb() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_has_verb() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::clear_has_verb() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::clear_verb() {
+ if (verb_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ verb_->clear();
+ }
+ clear_has_verb();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::verb() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.verb)
+ return *verb_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_verb(const ::std::string& value) {
+ set_has_verb();
+ if (verb_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ verb_ = new ::std::string;
+ }
+ verb_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.verb)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_verb(const char* value) {
+ set_has_verb();
+ if (verb_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ verb_ = new ::std::string;
+ }
+ verb_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.verb)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_verb(const void* value, size_t size) {
+ set_has_verb();
+ if (verb_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ verb_ = new ::std::string;
+ }
+ verb_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.verb)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::mutable_verb() {
+ set_has_verb();
+ if (verb_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ verb_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.verb)
+ return verb_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::release_verb() {
+ clear_has_verb();
+ if (verb_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = verb_;
+ verb_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_allocated_verb(::std::string* verb) {
+ if (verb_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete verb_;
+ }
+ if (verb) {
+ set_has_verb();
+ verb_ = verb;
+ } else {
+ clear_has_verb();
+ verb_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.verb)
+}
+
+// optional bytes uri = 2;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::has_uri() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_has_uri() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::clear_has_uri() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::clear_uri() {
+ if (uri_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ uri_->clear();
+ }
+ clear_has_uri();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::uri() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.uri)
+ return *uri_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_uri(const ::std::string& value) {
+ set_has_uri();
+ if (uri_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ uri_ = new ::std::string;
+ }
+ uri_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.uri)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_uri(const char* value) {
+ set_has_uri();
+ if (uri_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ uri_ = new ::std::string;
+ }
+ uri_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.uri)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_uri(const void* value, size_t size) {
+ set_has_uri();
+ if (uri_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ uri_ = new ::std::string;
+ }
+ uri_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.uri)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::mutable_uri() {
+ set_has_uri();
+ if (uri_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ uri_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.uri)
+ return uri_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::release_uri() {
+ clear_has_uri();
+ if (uri_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = uri_;
+ uri_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_allocated_uri(::std::string* uri) {
+ if (uri_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete uri_;
+ }
+ if (uri) {
+ set_has_uri();
+ uri_ = uri;
+ } else {
+ clear_has_uri();
+ uri_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.uri)
+}
+
+// optional bytes version = 3;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::has_version() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_has_version() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::clear_has_version() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::clear_version() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ clear_has_version();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.version)
+ return *version_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_version(const ::std::string& value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.version)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_version(const char* value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.version)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_version(const void* value, size_t size) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.version)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::mutable_version() {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.version)
+ return version_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::release_version() {
+ clear_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = version_;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::set_allocated_version(::std::string* version) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (version) {
+ set_has_version();
+ version_ = version;
+ } else {
+ clear_has_version();
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine.version)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest_HTTPRequest
+
+// optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.FirstLine firstline = 1;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest::has_firstline() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_has_firstline() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_has_firstline() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_firstline() {
+ if (firstline_ != NULL) firstline_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine::Clear();
+ clear_has_firstline();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine& ClientSafeBrowsingReportRequest_HTTPRequest::firstline() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.firstline)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return firstline_ != NULL ? *firstline_ : *default_instance().firstline_;
+#else
+ return firstline_ != NULL ? *firstline_ : *default_instance_->firstline_;
+#endif
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* ClientSafeBrowsingReportRequest_HTTPRequest::mutable_firstline() {
+ set_has_firstline();
+ if (firstline_ == NULL) firstline_ = new ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.firstline)
+ return firstline_;
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* ClientSafeBrowsingReportRequest_HTTPRequest::release_firstline() {
+ clear_has_firstline();
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* temp = firstline_;
+ firstline_ = NULL;
+ return temp;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_allocated_firstline(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest_FirstLine* firstline) {
+ delete firstline_;
+ firstline_ = firstline;
+ if (firstline) {
+ set_has_firstline();
+ } else {
+ clear_has_firstline();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.firstline)
+}
+
+// repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+inline int ClientSafeBrowsingReportRequest_HTTPRequest::headers_size() const {
+ return headers_.size();
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_headers() {
+ headers_.Clear();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader& ClientSafeBrowsingReportRequest_HTTPRequest::headers(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.headers)
+ return headers_.Get(index);
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* ClientSafeBrowsingReportRequest_HTTPRequest::mutable_headers(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.headers)
+ return headers_.Mutable(index);
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* ClientSafeBrowsingReportRequest_HTTPRequest::add_headers() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.headers)
+ return headers_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >&
+ClientSafeBrowsingReportRequest_HTTPRequest::headers() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.headers)
+ return headers_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >*
+ClientSafeBrowsingReportRequest_HTTPRequest::mutable_headers() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.headers)
+ return &headers_;
+}
+
+// optional bytes body = 3;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest::has_body() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_has_body() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_has_body() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_body() {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_->clear();
+ }
+ clear_has_body();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPRequest::body() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.body)
+ return *body_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_body(const ::std::string& value) {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ body_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.body)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_body(const char* value) {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ body_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.body)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_body(const void* value, size_t size) {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ body_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.body)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest::mutable_body() {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.body)
+ return body_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest::release_body() {
+ clear_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = body_;
+ body_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_allocated_body(::std::string* body) {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete body_;
+ }
+ if (body) {
+ set_has_body();
+ body_ = body;
+ } else {
+ clear_has_body();
+ body_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.body)
+}
+
+// optional bytes bodydigest = 4;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest::has_bodydigest() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_has_bodydigest() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_has_bodydigest() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_bodydigest() {
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_->clear();
+ }
+ clear_has_bodydigest();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPRequest::bodydigest() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodydigest)
+ return *bodydigest_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_bodydigest(const ::std::string& value) {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ bodydigest_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodydigest)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_bodydigest(const char* value) {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ bodydigest_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodydigest)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_bodydigest(const void* value, size_t size) {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ bodydigest_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodydigest)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest::mutable_bodydigest() {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodydigest)
+ return bodydigest_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPRequest::release_bodydigest() {
+ clear_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = bodydigest_;
+ bodydigest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_allocated_bodydigest(::std::string* bodydigest) {
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bodydigest_;
+ }
+ if (bodydigest) {
+ set_has_bodydigest();
+ bodydigest_ = bodydigest;
+ } else {
+ clear_has_bodydigest();
+ bodydigest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodydigest)
+}
+
+// optional int32 bodylength = 5;
+inline bool ClientSafeBrowsingReportRequest_HTTPRequest::has_bodylength() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_has_bodylength() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_has_bodylength() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::clear_bodylength() {
+ bodylength_ = 0;
+ clear_has_bodylength();
+}
+inline ::google::protobuf::int32 ClientSafeBrowsingReportRequest_HTTPRequest::bodylength() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodylength)
+ return bodylength_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPRequest::set_bodylength(::google::protobuf::int32 value) {
+ set_has_bodylength();
+ bodylength_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest.bodylength)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine
+
+// optional int32 code = 1;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::has_code() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_has_code() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::clear_has_code() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::clear_code() {
+ code_ = 0;
+ clear_has_code();
+}
+inline ::google::protobuf::int32 ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::code() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.code)
+ return code_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_code(::google::protobuf::int32 value) {
+ set_has_code();
+ code_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.code)
+}
+
+// optional bytes reason = 2;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::has_reason() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_has_reason() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::clear_has_reason() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::clear_reason() {
+ if (reason_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ reason_->clear();
+ }
+ clear_has_reason();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::reason() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.reason)
+ return *reason_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_reason(const ::std::string& value) {
+ set_has_reason();
+ if (reason_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ reason_ = new ::std::string;
+ }
+ reason_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.reason)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_reason(const char* value) {
+ set_has_reason();
+ if (reason_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ reason_ = new ::std::string;
+ }
+ reason_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.reason)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_reason(const void* value, size_t size) {
+ set_has_reason();
+ if (reason_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ reason_ = new ::std::string;
+ }
+ reason_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.reason)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::mutable_reason() {
+ set_has_reason();
+ if (reason_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ reason_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.reason)
+ return reason_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::release_reason() {
+ clear_has_reason();
+ if (reason_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = reason_;
+ reason_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_allocated_reason(::std::string* reason) {
+ if (reason_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete reason_;
+ }
+ if (reason) {
+ set_has_reason();
+ reason_ = reason;
+ } else {
+ clear_has_reason();
+ reason_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.reason)
+}
+
+// optional bytes version = 3;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::has_version() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_has_version() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::clear_has_version() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::clear_version() {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_->clear();
+ }
+ clear_has_version();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::version() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.version)
+ return *version_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_version(const ::std::string& value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.version)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_version(const char* value) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.version)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_version(const void* value, size_t size) {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.version)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::mutable_version() {
+ set_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.version)
+ return version_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::release_version() {
+ clear_has_version();
+ if (version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = version_;
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::set_allocated_version(::std::string* version) {
+ if (version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete version_;
+ }
+ if (version) {
+ set_has_version();
+ version_ = version;
+ } else {
+ clear_has_version();
+ version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine.version)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest_HTTPResponse
+
+// optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.FirstLine firstline = 1;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse::has_firstline() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_has_firstline() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_has_firstline() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_firstline() {
+ if (firstline_ != NULL) firstline_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine::Clear();
+ clear_has_firstline();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine& ClientSafeBrowsingReportRequest_HTTPResponse::firstline() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.firstline)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return firstline_ != NULL ? *firstline_ : *default_instance().firstline_;
+#else
+ return firstline_ != NULL ? *firstline_ : *default_instance_->firstline_;
+#endif
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* ClientSafeBrowsingReportRequest_HTTPResponse::mutable_firstline() {
+ set_has_firstline();
+ if (firstline_ == NULL) firstline_ = new ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.firstline)
+ return firstline_;
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* ClientSafeBrowsingReportRequest_HTTPResponse::release_firstline() {
+ clear_has_firstline();
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* temp = firstline_;
+ firstline_ = NULL;
+ return temp;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_allocated_firstline(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse_FirstLine* firstline) {
+ delete firstline_;
+ firstline_ = firstline;
+ if (firstline) {
+ set_has_firstline();
+ } else {
+ clear_has_firstline();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.firstline)
+}
+
+// repeated .safe_browsing.ClientSafeBrowsingReportRequest.HTTPHeader headers = 2;
+inline int ClientSafeBrowsingReportRequest_HTTPResponse::headers_size() const {
+ return headers_.size();
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_headers() {
+ headers_.Clear();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader& ClientSafeBrowsingReportRequest_HTTPResponse::headers(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.headers)
+ return headers_.Get(index);
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* ClientSafeBrowsingReportRequest_HTTPResponse::mutable_headers(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.headers)
+ return headers_.Mutable(index);
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader* ClientSafeBrowsingReportRequest_HTTPResponse::add_headers() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.headers)
+ return headers_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >&
+ClientSafeBrowsingReportRequest_HTTPResponse::headers() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.headers)
+ return headers_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPHeader >*
+ClientSafeBrowsingReportRequest_HTTPResponse::mutable_headers() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.headers)
+ return &headers_;
+}
+
+// optional bytes body = 3;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse::has_body() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_has_body() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_has_body() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_body() {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_->clear();
+ }
+ clear_has_body();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPResponse::body() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.body)
+ return *body_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_body(const ::std::string& value) {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ body_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.body)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_body(const char* value) {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ body_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.body)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_body(const void* value, size_t size) {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ body_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.body)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse::mutable_body() {
+ set_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ body_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.body)
+ return body_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse::release_body() {
+ clear_has_body();
+ if (body_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = body_;
+ body_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_allocated_body(::std::string* body) {
+ if (body_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete body_;
+ }
+ if (body) {
+ set_has_body();
+ body_ = body;
+ } else {
+ clear_has_body();
+ body_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.body)
+}
+
+// optional bytes bodydigest = 4;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse::has_bodydigest() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_has_bodydigest() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_has_bodydigest() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_bodydigest() {
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_->clear();
+ }
+ clear_has_bodydigest();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPResponse::bodydigest() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodydigest)
+ return *bodydigest_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_bodydigest(const ::std::string& value) {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ bodydigest_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodydigest)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_bodydigest(const char* value) {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ bodydigest_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodydigest)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_bodydigest(const void* value, size_t size) {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ bodydigest_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodydigest)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse::mutable_bodydigest() {
+ set_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ bodydigest_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodydigest)
+ return bodydigest_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse::release_bodydigest() {
+ clear_has_bodydigest();
+ if (bodydigest_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = bodydigest_;
+ bodydigest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_allocated_bodydigest(::std::string* bodydigest) {
+ if (bodydigest_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete bodydigest_;
+ }
+ if (bodydigest) {
+ set_has_bodydigest();
+ bodydigest_ = bodydigest;
+ } else {
+ clear_has_bodydigest();
+ bodydigest_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodydigest)
+}
+
+// optional int32 bodylength = 5;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse::has_bodylength() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_has_bodylength() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_has_bodylength() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_bodylength() {
+ bodylength_ = 0;
+ clear_has_bodylength();
+}
+inline ::google::protobuf::int32 ClientSafeBrowsingReportRequest_HTTPResponse::bodylength() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodylength)
+ return bodylength_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_bodylength(::google::protobuf::int32 value) {
+ set_has_bodylength();
+ bodylength_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.bodylength)
+}
+
+// optional bytes remote_ip = 6;
+inline bool ClientSafeBrowsingReportRequest_HTTPResponse::has_remote_ip() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_has_remote_ip() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_has_remote_ip() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::clear_remote_ip() {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_->clear();
+ }
+ clear_has_remote_ip();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_HTTPResponse::remote_ip() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.remote_ip)
+ return *remote_ip_;
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_remote_ip(const ::std::string& value) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.remote_ip)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_remote_ip(const char* value) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.remote_ip)
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_remote_ip(const void* value, size_t size) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.remote_ip)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse::mutable_remote_ip() {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.remote_ip)
+ return remote_ip_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_HTTPResponse::release_remote_ip() {
+ clear_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = remote_ip_;
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_HTTPResponse::set_allocated_remote_ip(::std::string* remote_ip) {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete remote_ip_;
+ }
+ if (remote_ip) {
+ set_has_remote_ip();
+ remote_ip_ = remote_ip;
+ } else {
+ clear_has_remote_ip();
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse.remote_ip)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest_Resource
+
+// required int32 id = 1;
+inline bool ClientSafeBrowsingReportRequest_Resource::has_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_has_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_has_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_id() {
+ id_ = 0;
+ clear_has_id();
+}
+inline ::google::protobuf::int32 ClientSafeBrowsingReportRequest_Resource::id() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.id)
+ return id_;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_id(::google::protobuf::int32 value) {
+ set_has_id();
+ id_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.Resource.id)
+}
+
+// optional string url = 2;
+inline bool ClientSafeBrowsingReportRequest_Resource::has_url() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_has_url() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_has_url() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_Resource::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.url)
+ return *url_;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.Resource.url)
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.Resource.url)
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.Resource.url)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_Resource::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.Resource.url)
+ return url_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_Resource::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.Resource.url)
+}
+
+// optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPRequest request = 3;
+inline bool ClientSafeBrowsingReportRequest_Resource::has_request() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_has_request() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_has_request() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_request() {
+ if (request_ != NULL) request_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest::Clear();
+ clear_has_request();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest& ClientSafeBrowsingReportRequest_Resource::request() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.request)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return request_ != NULL ? *request_ : *default_instance().request_;
+#else
+ return request_ != NULL ? *request_ : *default_instance_->request_;
+#endif
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* ClientSafeBrowsingReportRequest_Resource::mutable_request() {
+ set_has_request();
+ if (request_ == NULL) request_ = new ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.Resource.request)
+ return request_;
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* ClientSafeBrowsingReportRequest_Resource::release_request() {
+ clear_has_request();
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* temp = request_;
+ request_ = NULL;
+ return temp;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_allocated_request(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPRequest* request) {
+ delete request_;
+ request_ = request;
+ if (request) {
+ set_has_request();
+ } else {
+ clear_has_request();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.Resource.request)
+}
+
+// optional .safe_browsing.ClientSafeBrowsingReportRequest.HTTPResponse response = 4;
+inline bool ClientSafeBrowsingReportRequest_Resource::has_response() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_has_response() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_has_response() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_response() {
+ if (response_ != NULL) response_->::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse::Clear();
+ clear_has_response();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse& ClientSafeBrowsingReportRequest_Resource::response() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.response)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return response_ != NULL ? *response_ : *default_instance().response_;
+#else
+ return response_ != NULL ? *response_ : *default_instance_->response_;
+#endif
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* ClientSafeBrowsingReportRequest_Resource::mutable_response() {
+ set_has_response();
+ if (response_ == NULL) response_ = new ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse;
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.Resource.response)
+ return response_;
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* ClientSafeBrowsingReportRequest_Resource::release_response() {
+ clear_has_response();
+ ::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* temp = response_;
+ response_ = NULL;
+ return temp;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_allocated_response(::safe_browsing::ClientSafeBrowsingReportRequest_HTTPResponse* response) {
+ delete response_;
+ response_ = response;
+ if (response) {
+ set_has_response();
+ } else {
+ clear_has_response();
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.Resource.response)
+}
+
+// optional int32 parent_id = 5;
+inline bool ClientSafeBrowsingReportRequest_Resource::has_parent_id() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_has_parent_id() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_has_parent_id() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_parent_id() {
+ parent_id_ = 0;
+ clear_has_parent_id();
+}
+inline ::google::protobuf::int32 ClientSafeBrowsingReportRequest_Resource::parent_id() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.parent_id)
+ return parent_id_;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_parent_id(::google::protobuf::int32 value) {
+ set_has_parent_id();
+ parent_id_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.Resource.parent_id)
+}
+
+// repeated int32 child_ids = 6;
+inline int ClientSafeBrowsingReportRequest_Resource::child_ids_size() const {
+ return child_ids_.size();
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_child_ids() {
+ child_ids_.Clear();
+}
+inline ::google::protobuf::int32 ClientSafeBrowsingReportRequest_Resource::child_ids(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.child_ids)
+ return child_ids_.Get(index);
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_child_ids(int index, ::google::protobuf::int32 value) {
+ child_ids_.Set(index, value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.Resource.child_ids)
+}
+inline void ClientSafeBrowsingReportRequest_Resource::add_child_ids(::google::protobuf::int32 value) {
+ child_ids_.Add(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientSafeBrowsingReportRequest.Resource.child_ids)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ClientSafeBrowsingReportRequest_Resource::child_ids() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientSafeBrowsingReportRequest.Resource.child_ids)
+ return child_ids_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ClientSafeBrowsingReportRequest_Resource::mutable_child_ids() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientSafeBrowsingReportRequest.Resource.child_ids)
+ return &child_ids_;
+}
+
+// optional string tag_name = 7;
+inline bool ClientSafeBrowsingReportRequest_Resource::has_tag_name() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_has_tag_name() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_has_tag_name() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::clear_tag_name() {
+ if (tag_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ tag_name_->clear();
+ }
+ clear_has_tag_name();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest_Resource::tag_name() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.Resource.tag_name)
+ return *tag_name_;
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_tag_name(const ::std::string& value) {
+ set_has_tag_name();
+ if (tag_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ tag_name_ = new ::std::string;
+ }
+ tag_name_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.Resource.tag_name)
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_tag_name(const char* value) {
+ set_has_tag_name();
+ if (tag_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ tag_name_ = new ::std::string;
+ }
+ tag_name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.Resource.tag_name)
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_tag_name(const char* value, size_t size) {
+ set_has_tag_name();
+ if (tag_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ tag_name_ = new ::std::string;
+ }
+ tag_name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.Resource.tag_name)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_Resource::mutable_tag_name() {
+ set_has_tag_name();
+ if (tag_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ tag_name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.Resource.tag_name)
+ return tag_name_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest_Resource::release_tag_name() {
+ clear_has_tag_name();
+ if (tag_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = tag_name_;
+ tag_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest_Resource::set_allocated_tag_name(::std::string* tag_name) {
+ if (tag_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete tag_name_;
+ }
+ if (tag_name) {
+ set_has_tag_name();
+ tag_name_ = tag_name;
+ } else {
+ clear_has_tag_name();
+ tag_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.Resource.tag_name)
+}
+
+// -------------------------------------------------------------------
+
+// ClientSafeBrowsingReportRequest
+
+// optional .safe_browsing.ClientSafeBrowsingReportRequest.ReportType type = 10;
+inline bool ClientSafeBrowsingReportRequest::has_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_type() {
+ type_ = 0;
+ clear_has_type();
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_ReportType ClientSafeBrowsingReportRequest::type() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.type)
+ return static_cast< ::safe_browsing::ClientSafeBrowsingReportRequest_ReportType >(type_);
+}
+inline void ClientSafeBrowsingReportRequest::set_type(::safe_browsing::ClientSafeBrowsingReportRequest_ReportType value) {
+ assert(::safe_browsing::ClientSafeBrowsingReportRequest_ReportType_IsValid(value));
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.type)
+}
+
+// optional .safe_browsing.ClientDownloadResponse.Verdict download_verdict = 11;
+inline bool ClientSafeBrowsingReportRequest::has_download_verdict() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_download_verdict() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_download_verdict() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_download_verdict() {
+ download_verdict_ = 0;
+ clear_has_download_verdict();
+}
+inline ::safe_browsing::ClientDownloadResponse_Verdict ClientSafeBrowsingReportRequest::download_verdict() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.download_verdict)
+ return static_cast< ::safe_browsing::ClientDownloadResponse_Verdict >(download_verdict_);
+}
+inline void ClientSafeBrowsingReportRequest::set_download_verdict(::safe_browsing::ClientDownloadResponse_Verdict value) {
+ assert(::safe_browsing::ClientDownloadResponse_Verdict_IsValid(value));
+ set_has_download_verdict();
+ download_verdict_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.download_verdict)
+}
+
+// optional string url = 1;
+inline bool ClientSafeBrowsingReportRequest::has_url() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_url() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_url() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest::url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.url)
+ return *url_;
+}
+inline void ClientSafeBrowsingReportRequest::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.url)
+}
+inline void ClientSafeBrowsingReportRequest::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.url)
+}
+inline void ClientSafeBrowsingReportRequest::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.url)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.url)
+ return url_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.url)
+}
+
+// optional string page_url = 2;
+inline bool ClientSafeBrowsingReportRequest::has_page_url() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_page_url() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_page_url() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_page_url() {
+ if (page_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ page_url_->clear();
+ }
+ clear_has_page_url();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest::page_url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.page_url)
+ return *page_url_;
+}
+inline void ClientSafeBrowsingReportRequest::set_page_url(const ::std::string& value) {
+ set_has_page_url();
+ if (page_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ page_url_ = new ::std::string;
+ }
+ page_url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.page_url)
+}
+inline void ClientSafeBrowsingReportRequest::set_page_url(const char* value) {
+ set_has_page_url();
+ if (page_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ page_url_ = new ::std::string;
+ }
+ page_url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.page_url)
+}
+inline void ClientSafeBrowsingReportRequest::set_page_url(const char* value, size_t size) {
+ set_has_page_url();
+ if (page_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ page_url_ = new ::std::string;
+ }
+ page_url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.page_url)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::mutable_page_url() {
+ set_has_page_url();
+ if (page_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ page_url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.page_url)
+ return page_url_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::release_page_url() {
+ clear_has_page_url();
+ if (page_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = page_url_;
+ page_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest::set_allocated_page_url(::std::string* page_url) {
+ if (page_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete page_url_;
+ }
+ if (page_url) {
+ set_has_page_url();
+ page_url_ = page_url;
+ } else {
+ clear_has_page_url();
+ page_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.page_url)
+}
+
+// optional string referrer_url = 3;
+inline bool ClientSafeBrowsingReportRequest::has_referrer_url() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_referrer_url() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_referrer_url() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_referrer_url() {
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_->clear();
+ }
+ clear_has_referrer_url();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest::referrer_url() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.referrer_url)
+ return *referrer_url_;
+}
+inline void ClientSafeBrowsingReportRequest::set_referrer_url(const ::std::string& value) {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ referrer_url_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.referrer_url)
+}
+inline void ClientSafeBrowsingReportRequest::set_referrer_url(const char* value) {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ referrer_url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.referrer_url)
+}
+inline void ClientSafeBrowsingReportRequest::set_referrer_url(const char* value, size_t size) {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ referrer_url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.referrer_url)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::mutable_referrer_url() {
+ set_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.referrer_url)
+ return referrer_url_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::release_referrer_url() {
+ clear_has_referrer_url();
+ if (referrer_url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = referrer_url_;
+ referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest::set_allocated_referrer_url(::std::string* referrer_url) {
+ if (referrer_url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_url_;
+ }
+ if (referrer_url) {
+ set_has_referrer_url();
+ referrer_url_ = referrer_url;
+ } else {
+ clear_has_referrer_url();
+ referrer_url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.referrer_url)
+}
+
+// repeated .safe_browsing.ClientSafeBrowsingReportRequest.Resource resources = 4;
+inline int ClientSafeBrowsingReportRequest::resources_size() const {
+ return resources_.size();
+}
+inline void ClientSafeBrowsingReportRequest::clear_resources() {
+ resources_.Clear();
+}
+inline const ::safe_browsing::ClientSafeBrowsingReportRequest_Resource& ClientSafeBrowsingReportRequest::resources(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.resources)
+ return resources_.Get(index);
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_Resource* ClientSafeBrowsingReportRequest::mutable_resources(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.resources)
+ return resources_.Mutable(index);
+}
+inline ::safe_browsing::ClientSafeBrowsingReportRequest_Resource* ClientSafeBrowsingReportRequest::add_resources() {
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientSafeBrowsingReportRequest.resources)
+ return resources_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_Resource >&
+ClientSafeBrowsingReportRequest::resources() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientSafeBrowsingReportRequest.resources)
+ return resources_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::safe_browsing::ClientSafeBrowsingReportRequest_Resource >*
+ClientSafeBrowsingReportRequest::mutable_resources() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientSafeBrowsingReportRequest.resources)
+ return &resources_;
+}
+
+// optional bool complete = 5;
+inline bool ClientSafeBrowsingReportRequest::has_complete() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_complete() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_complete() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_complete() {
+ complete_ = false;
+ clear_has_complete();
+}
+inline bool ClientSafeBrowsingReportRequest::complete() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.complete)
+ return complete_;
+}
+inline void ClientSafeBrowsingReportRequest::set_complete(bool value) {
+ set_has_complete();
+ complete_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.complete)
+}
+
+// repeated string client_asn = 6;
+inline int ClientSafeBrowsingReportRequest::client_asn_size() const {
+ return client_asn_.size();
+}
+inline void ClientSafeBrowsingReportRequest::clear_client_asn() {
+ client_asn_.Clear();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest::client_asn(int index) const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+ return client_asn_.Get(index);
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::mutable_client_asn(int index) {
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+ return client_asn_.Mutable(index);
+}
+inline void ClientSafeBrowsingReportRequest::set_client_asn(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+ client_asn_.Mutable(index)->assign(value);
+}
+inline void ClientSafeBrowsingReportRequest::set_client_asn(int index, const char* value) {
+ client_asn_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+}
+inline void ClientSafeBrowsingReportRequest::set_client_asn(int index, const char* value, size_t size) {
+ client_asn_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::add_client_asn() {
+ return client_asn_.Add();
+}
+inline void ClientSafeBrowsingReportRequest::add_client_asn(const ::std::string& value) {
+ client_asn_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+}
+inline void ClientSafeBrowsingReportRequest::add_client_asn(const char* value) {
+ client_asn_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+}
+inline void ClientSafeBrowsingReportRequest::add_client_asn(const char* value, size_t size) {
+ client_asn_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+ClientSafeBrowsingReportRequest::client_asn() const {
+ // @@protoc_insertion_point(field_list:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+ return client_asn_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+ClientSafeBrowsingReportRequest::mutable_client_asn() {
+ // @@protoc_insertion_point(field_mutable_list:safe_browsing.ClientSafeBrowsingReportRequest.client_asn)
+ return &client_asn_;
+}
+
+// optional string client_country = 7;
+inline bool ClientSafeBrowsingReportRequest::has_client_country() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_client_country() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_client_country() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_client_country() {
+ if (client_country_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_country_->clear();
+ }
+ clear_has_client_country();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest::client_country() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.client_country)
+ return *client_country_;
+}
+inline void ClientSafeBrowsingReportRequest::set_client_country(const ::std::string& value) {
+ set_has_client_country();
+ if (client_country_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_country_ = new ::std::string;
+ }
+ client_country_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.client_country)
+}
+inline void ClientSafeBrowsingReportRequest::set_client_country(const char* value) {
+ set_has_client_country();
+ if (client_country_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_country_ = new ::std::string;
+ }
+ client_country_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.client_country)
+}
+inline void ClientSafeBrowsingReportRequest::set_client_country(const char* value, size_t size) {
+ set_has_client_country();
+ if (client_country_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_country_ = new ::std::string;
+ }
+ client_country_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.client_country)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::mutable_client_country() {
+ set_has_client_country();
+ if (client_country_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_country_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.client_country)
+ return client_country_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::release_client_country() {
+ clear_has_client_country();
+ if (client_country_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = client_country_;
+ client_country_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest::set_allocated_client_country(::std::string* client_country) {
+ if (client_country_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete client_country_;
+ }
+ if (client_country) {
+ set_has_client_country();
+ client_country_ = client_country;
+ } else {
+ clear_has_client_country();
+ client_country_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.client_country)
+}
+
+// optional bool did_proceed = 8;
+inline bool ClientSafeBrowsingReportRequest::has_did_proceed() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_did_proceed() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_did_proceed() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_did_proceed() {
+ did_proceed_ = false;
+ clear_has_did_proceed();
+}
+inline bool ClientSafeBrowsingReportRequest::did_proceed() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.did_proceed)
+ return did_proceed_;
+}
+inline void ClientSafeBrowsingReportRequest::set_did_proceed(bool value) {
+ set_has_did_proceed();
+ did_proceed_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.did_proceed)
+}
+
+// optional bool repeat_visit = 9;
+inline bool ClientSafeBrowsingReportRequest::has_repeat_visit() const {
+ return (_has_bits_[0] & 0x00000400u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_repeat_visit() {
+ _has_bits_[0] |= 0x00000400u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_repeat_visit() {
+ _has_bits_[0] &= ~0x00000400u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_repeat_visit() {
+ repeat_visit_ = false;
+ clear_has_repeat_visit();
+}
+inline bool ClientSafeBrowsingReportRequest::repeat_visit() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.repeat_visit)
+ return repeat_visit_;
+}
+inline void ClientSafeBrowsingReportRequest::set_repeat_visit(bool value) {
+ set_has_repeat_visit();
+ repeat_visit_ = value;
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.repeat_visit)
+}
+
+// optional bytes token = 15;
+inline bool ClientSafeBrowsingReportRequest::has_token() const {
+ return (_has_bits_[0] & 0x00000800u) != 0;
+}
+inline void ClientSafeBrowsingReportRequest::set_has_token() {
+ _has_bits_[0] |= 0x00000800u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_has_token() {
+ _has_bits_[0] &= ~0x00000800u;
+}
+inline void ClientSafeBrowsingReportRequest::clear_token() {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_->clear();
+ }
+ clear_has_token();
+}
+inline const ::std::string& ClientSafeBrowsingReportRequest::token() const {
+ // @@protoc_insertion_point(field_get:safe_browsing.ClientSafeBrowsingReportRequest.token)
+ return *token_;
+}
+inline void ClientSafeBrowsingReportRequest::set_token(const ::std::string& value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set:safe_browsing.ClientSafeBrowsingReportRequest.token)
+}
+inline void ClientSafeBrowsingReportRequest::set_token(const char* value) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(value);
+ // @@protoc_insertion_point(field_set_char:safe_browsing.ClientSafeBrowsingReportRequest.token)
+}
+inline void ClientSafeBrowsingReportRequest::set_token(const void* value, size_t size) {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ token_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:safe_browsing.ClientSafeBrowsingReportRequest.token)
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::mutable_token() {
+ set_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ token_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:safe_browsing.ClientSafeBrowsingReportRequest.token)
+ return token_;
+}
+inline ::std::string* ClientSafeBrowsingReportRequest::release_token() {
+ clear_has_token();
+ if (token_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = token_;
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientSafeBrowsingReportRequest::set_allocated_token(::std::string* token) {
+ if (token_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete token_;
+ }
+ if (token) {
+ set_has_token();
+ token_ = token;
+ } else {
+ clear_has_token();
+ token_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:safe_browsing.ClientSafeBrowsingReportRequest.token)
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace safe_browsing
+
+// @@protoc_insertion_point(global_scope)
+
+#endif // PROTOBUF_chromium_2fchrome_2fcommon_2fsafe_5fbrowsing_2fcsd_2eproto__INCLUDED
diff --git a/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.proto b/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.proto
new file mode 100644
index 0000000000..fc588cc1ff
--- /dev/null
+++ b/toolkit/components/downloads/chromium/chrome/common/safe_browsing/csd.proto
@@ -0,0 +1,839 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// This proto file includes:
+// (1) Client side phishing and malware detection request and response
+// protocol buffers. Those protocol messages should be kept in sync
+// with the server implementation.
+//
+// (2) Safe Browsing reporting protocol buffers.
+// A ClientSafeBrowsingReportRequest is sent when a user opts-in to
+// sending detailed threat reports from the safe browsing interstitial page.
+// It is a list of Resource messages, which may contain the url of a
+// resource such as the page in the address bar or any other resource
+// that was loaded for this page.
+// In addition to the url, a resource can contain HTTP request and response
+// headers and bodies.
+//
+// If you want to change this protocol definition or you have questions
+// regarding its format please contact chrome-anti-phishing@googlegroups.com.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package safe_browsing;
+
+// Protocol buffer describing the Chrome user population of the user reporting
+// data.
+message ChromeUserPopulation {
+ enum UserPopulation {
+ UNKNOWN_USER_POPULATION = 0;
+ SAFE_BROWSING = 1;
+ EXTENDED_REPORTING = 2;
+ }
+ optional UserPopulation user_population = 1;
+}
+
+
+message ClientPhishingRequest {
+ // URL that the client visited. The CGI parameters are stripped by the
+ // client.
+ optional string url = 1;
+
+ // A 5-byte SHA-256 hash prefix of the URL. Before hashing the URL is
+ // canonicalized, converted to a suffix-prefix expression and broadened
+ // (www prefix is removed and everything past the last '/' is stripped).
+ //
+ // Marked OBSOLETE because the URL is sent for all users, making the hash
+ // prefix unnecessary.
+ optional bytes OBSOLETE_hash_prefix = 10;
+
+ // Score that was computed on the client. Value is between 0.0 and 1.0.
+ // The larger the value the more likely the url is phishing.
+ required float client_score = 2;
+
+ // Note: we're skipping tag 3 because it was previously used.
+
+ // Is true if the features for this URL were classified as phishing.
+ // Currently, this will always be true for all client-phishing requests
+ // that are sent to the server.
+ optional bool is_phishing = 4;
+
+ message Feature {
+ // Feature name. E.g., 'PageHasForms'.
+ required string name = 1;
+
+ // Feature value is always in the range [0.0, 1.0]. Boolean features
+ // have value 1.0.
+ required double value = 2;
+ }
+
+ // List of features that were extracted. Those are the features that were
+ // sent to the scorer and which resulted in client_score being computed.
+ repeated Feature feature_map = 5;
+
+ // The version number of the model that was used to compute the client-score.
+ // Copied from ClientSideModel.version().
+ optional int32 model_version = 6;
+
+ // Field 7 is only used on the server.
+
+ // List of features that are extracted in the client but are not used in the
+ // machine learning model.
+ repeated Feature non_model_feature_map = 8;
+
+ // The referrer URL. This field might not be set, for example, in the case
+ // where the referrer uses HTTPs.
+ // OBSOLETE: Use feature 'Referrer=<referrer>' instead.
+ optional string OBSOLETE_referrer_url = 9;
+
+ // Field 11 is only used on the server.
+
+ // List of shingle hashes we extracted.
+ repeated uint32 shingle_hashes = 12 [packed = true];
+
+ // The model filename (basename) that was used by the client.
+ optional string model_filename = 13;
+
+ // Population that the reporting user is part of.
+ optional ChromeUserPopulation population = 14;
+}
+
+message ClientPhishingResponse {
+ required bool phishy = 1;
+
+ // A list of SafeBrowsing host-suffix / path-prefix expressions that
+ // are whitelisted. The client must match the current top-level URL
+ // against these whitelisted expressions and only apply a positive
+ // phishing verdict above if the URL does not match any expression
+ // on this whitelist. The client must not cache these whitelisted
+ // expressions. This whitelist will be empty for the vast majority
+ // of the responses but might contain up to 100 entries in emergency
+ // situations.
+ //
+ // Marked OBSOLETE because the URL is sent for all users, so the server
+ // can do whitelist matching.
+ repeated string OBSOLETE_whitelist_expression = 2;
+}
+
+message ClientMalwareRequest {
+ // URL that the client visited. The CGI parameters are stripped by the
+ // client.
+ required string url = 1;
+
+ // Field 2 is deleted and no longer in use.
+
+ // Field 3 is only used on the server.
+
+ // The referrer URL. This field might not be set, for example, in the case
+ // where the referrer uses HTTPS.
+ optional string referrer_url = 4;
+
+ // Field 5 and 6 are only used on the server.
+
+ message UrlInfo {
+ required string ip = 1;
+ required string url = 2;
+ optional string method = 3;
+ optional string referrer = 4;
+ // Resource type, the int value is a direct cast from the Type enum
+ // of ResourceType class defined in //src/webkit/commom/resource_type.h
+ optional int32 resource_type = 5;
+ }
+
+ // List of resource urls that match the malware IP list.
+ repeated UrlInfo bad_ip_url_info = 7;
+
+ // Population that the reporting user is part of.
+ optional ChromeUserPopulation population = 9;
+}
+
+message ClientMalwareResponse {
+ required bool blacklist = 1;
+ // The confirmed blacklisted bad IP and its url, which will be shown in
+ // malware warning, if the blacklist verdict is true.
+ // This IP string could be either in IPv4 or IPv6 format, which is the same
+ // as the ones client sent to server.
+ optional string bad_ip = 2;
+ optional string bad_url = 3;
+}
+
+message ClientDownloadRequest {
+ // The final URL of the download (after all redirects).
+ required string url = 1;
+
+ // This message contains various binary digests of the download payload.
+ message Digests {
+ optional bytes sha256 = 1;
+ optional bytes sha1 = 2;
+ optional bytes md5 = 3;
+ }
+ required Digests digests = 2;
+
+ // This is the length in bytes of the download payload.
+ required int64 length = 3;
+
+ // Type of the resources stored below.
+ enum ResourceType {
+ // The final URL of the download payload. The resource URL should
+ // correspond to the URL field above.
+ DOWNLOAD_URL = 0;
+ // A redirect URL that was fetched before hitting the final DOWNLOAD_URL.
+ DOWNLOAD_REDIRECT = 1;
+ // The final top-level URL of the tab that triggered the download.
+ TAB_URL = 2;
+ // A redirect URL thas was fetched before hitting the final TAB_URL.
+ TAB_REDIRECT = 3;
+ // The document URL for a PPAPI plugin instance that initiated the download.
+ // This is the document.url for the container element for the plugin
+ // instance.
+ PPAPI_DOCUMENT = 4;
+ // The plugin URL for a PPAPI plugin instance that initiated the download.
+ PPAPI_PLUGIN = 5;
+ }
+
+ message Resource {
+ required string url = 1;
+ required ResourceType type = 2;
+ optional bytes remote_ip = 3;
+ // This will only be set if the referrer is available and if the
+ // resource type is either TAB_URL or DOWNLOAD_URL.
+ optional string referrer = 4;
+
+ // TODO(noelutz): add the transition type?
+ }
+
+ // This repeated field will store all the redirects as well as the
+ // final URLs for the top-level tab URL (i.e., the URL that
+ // triggered the download) as well as for the download URL itself.
+ repeated Resource resources = 4;
+
+ // A trust chain of certificates. Each chain begins with the signing
+ // certificate of the binary, and ends with a self-signed certificate,
+ // typically from a trusted root CA. This structure is analogous to
+ // CERT_CHAIN_CONTEXT on Windows.
+ message CertificateChain {
+ // A single link in the chain.
+ message Element {
+ // DER-encoded X.509 representation of the certificate.
+ optional bytes certificate = 1;
+ // Fields 2 - 7 are only used on the server.
+ }
+ repeated Element element = 1;
+ }
+
+ // This is an OS X only message to report extended attribute informations.
+ // Extended attributes on OS X are used for various security mechanisms,
+ // which makes them interesting to Chrome.
+ message ExtendedAttr {
+ // This is the name of the extended attribute.
+ required string key = 1;
+ // This is the value of the extended attribute.
+ optional bytes value = 2;
+ }
+
+ message SignatureInfo {
+ // All certificate chains for each of the binary's signers. Multiple chains
+ // may be present if the binary or any certificate has multiple signers.
+ // Absence of certificate chains does not imply that the binary is not
+ // signed (in that case, SignedData blobs extracted from the binary may be
+ // preset), but does mean that trust has not been verified.
+ repeated CertificateChain certificate_chain = 1;
+
+ // True if the signature was trusted on the client.
+ optional bool trusted = 2;
+
+ // On Windows, PKCS#7 SignedData blobs extracted from a portable executable
+ // image's attribute certificate table. The presence of these does not imply
+ // that the signatures were deemed trusted by the client.
+ // On Mac, this is the code signature blob referenced by the
+ // LC_CODE_SIGNATURE load command.
+ repeated bytes signed_data = 3;
+
+ // On OS X, code signing data can be contained in the extended attributes of
+ // a file. As Gatekeeper respects this signature, we look for it and collect
+ // it.
+ repeated ExtendedAttr xattr = 4;
+ }
+
+ // This field will only be set if the binary is signed.
+ optional SignatureInfo signature = 5;
+
+ // True if the download was user initiated.
+ optional bool user_initiated = 6;
+
+ // Fields 7 and 8 are only used on the server.
+
+ // Name of the file where the download would be stored if the
+ // download completes. E.g., "bla.exe".
+ optional string file_basename = 9;
+
+ // Starting with Chrome M19 we're also sending back pings for Chrome
+ // extensions that get downloaded by users.
+ enum DownloadType {
+ WIN_EXECUTABLE = 0; // Currently all .exe, .cab and .msi files.
+ CHROME_EXTENSION = 1; // .crx files.
+ ANDROID_APK = 2; // .apk files.
+ // .zip files containing one of the other executable types.
+ ZIPPED_EXECUTABLE = 3;
+ MAC_EXECUTABLE = 4; // .dmg, .pkg, etc.
+ ZIPPED_ARCHIVE = 5; // .zip file containing another archive.
+ ARCHIVE = 6; // Archive that doesn't have a specific DownloadType.
+ // A .zip that Chrome failed to unpack to the point of finding exe/zips.
+ INVALID_ZIP = 7;
+ // A .dmg, .pkg, etc, that Chrome failed to unpack to the point of finding
+ // Mach O's.
+ INVALID_MAC_ARCHIVE = 8;
+ // A download request initiated via PPAPI. Typically the requestor is
+ // a Flash applet.
+ PPAPI_SAVE_REQUEST = 9;
+ // A file we don't support, but we've decided to sample and send
+ // a light-ping.
+ SAMPLED_UNSUPPORTED_FILE = 10;
+ }
+ optional DownloadType download_type = 10 [default = WIN_EXECUTABLE];
+
+ // Locale of the device, eg en, en_US.
+ optional string locale = 11;
+
+ message PEImageHeaders {
+ // IMAGE_DOS_HEADER.
+ optional bytes dos_header = 1;
+ // IMAGE_FILE_HEADER.
+ optional bytes file_header = 2;
+ // IMAGE_OPTIONAL_HEADER32. Present only for 32-bit PE images.
+ optional bytes optional_headers32 = 3;
+ // IMAGE_OPTIONAL_HEADER64. Present only for 64-bit PE images.
+ optional bytes optional_headers64 = 4;
+ // IMAGE_SECTION_HEADER.
+ repeated bytes section_header = 5;
+ // Contents of the .edata section.
+ optional bytes export_section_data = 6;
+
+ message DebugData {
+ // IMAGE_DEBUG_DIRECTORY.
+ optional bytes directory_entry = 1;
+ optional bytes raw_data = 2;
+ }
+
+ repeated DebugData debug_data = 7;
+ }
+
+ message MachOHeaders {
+ // The mach_header or mach_header_64 struct.
+ required bytes mach_header = 1;
+
+ message LoadCommand {
+ // |command_id| is the first uint32 of |command| as well, but is
+ // extracted for easier processing.
+ required uint32 command_id = 1;
+ // The entire data stream of the load command.
+ required bytes command = 2;
+ }
+
+ // All the load commands of the Mach-O file.
+ repeated LoadCommand load_commands = 2;
+ }
+
+ message ImageHeaders {
+ // Windows Portable Executable image headers.
+ optional PEImageHeaders pe_headers = 1;
+
+ // OS X Mach-O image headers.
+ repeated MachOHeaders mach_o_headers = 2;
+ };
+
+ // Fields 12-17 are reserved for server-side use and are never sent by the
+ // client.
+
+ optional ImageHeaders image_headers = 18;
+
+ // Fields 19-21 are reserved for server-side use and are never sent by the
+ // client.
+
+ // A binary contained in an archive (e.g., a .zip archive).
+ message ArchivedBinary {
+ optional string file_basename = 1;
+ optional DownloadType download_type = 2;
+ optional Digests digests = 3;
+ optional int64 length = 4;
+ optional SignatureInfo signature = 5;
+ optional ImageHeaders image_headers = 6;
+ }
+
+ repeated ArchivedBinary archived_binary = 22;
+
+ // Population that the reporting user is part of.
+ optional ChromeUserPopulation population = 24;
+
+ // True if the .zip or DMG, etc, was 100% successfully unpacked.
+ optional bool archive_valid = 26;
+
+ // True if this ClientDownloadRequest is from a whitelisted domain.
+ optional bool skipped_url_whitelist = 28;
+
+ // True if this ClientDownloadRequest contains a whitelisted certificate.
+ optional bool skipped_certificate_whitelist = 31;
+
+ // PPAPI_SAVE_REQUEST type messages may have more than one suggested filetype.
+ // Each element in this collection indicates an alternate extension including
+ // the leading extension separator.
+ repeated string alternate_extensions = 35;
+
+ message URLChainEntry {
+ enum URLType {
+ DOWNLOAD_URL = 1;
+ DOWNLOAD_REFERRER = 2;
+ LANDING_PAGE = 3;
+ LANDING_REFERRER = 4;
+ CLIENT_REDIRECT = 5;
+ SERVER_REDIRECT = 6;
+ }
+
+ // [required] The url of this Entry.
+ optional string url = 1;
+
+ // Type of URLs, such as download url, download referrer, etc.
+ optional URLType type = 2;
+
+ // IP address corresponding to url.
+ optional string ip_address = 3;
+
+ // Referrer url of this entry.
+ optional string referrer = 4;
+
+ // Main frame URL of referrer.
+ optional string main_frame_referrer = 5;
+
+ // If this URL loads in a different tab/frame from previous one.
+ optional bool is_retargeting = 6;
+
+ // If there is a user gesture attached to this transition.
+ optional bool is_user_initiated = 7;
+
+ optional double timestamp_in_millisec = 8;
+ } // End of URLChainEntry
+
+ // URLs transitions from landing referrer to download in reverse chronological
+ // order, i.e. download url comes first in this list, and landing referrer
+ // comes last.
+ repeated URLChainEntry url_chain = 36;
+}
+
+message ClientDownloadResponse {
+ enum Verdict {
+ // Download is considered safe.
+ SAFE = 0;
+ // Download is considered dangerous. Chrome should show a warning to the
+ // user.
+ DANGEROUS = 1;
+ // Download is uncommon. Chrome should display a less severe warning.
+ UNCOMMON = 2;
+ // The download is potentially unwanted.
+ POTENTIALLY_UNWANTED = 3;
+ // The download is from a dangerous host.
+ DANGEROUS_HOST = 4;
+ // The backend doesn't have confidence in its verdict of this file.
+ // Chrome should show the default warning if configured for this file type.
+ UNKNOWN = 5;
+ }
+ optional Verdict verdict = 1 [default = SAFE];
+
+ message MoreInfo {
+ // A human-readable string describing the nature of the warning.
+ // Only if verdict != SAFE. Localized based on request.locale.
+ optional string description = 1;
+
+ // A URL to get more information about this warning, if available.
+ optional string url = 2;
+ }
+ optional MoreInfo more_info = 2;
+
+ // An arbitrary token that should be sent along for further server requests.
+ optional bytes token = 3;
+}
+
+// The following protocol buffer holds the feedback report gathered
+// from the user regarding the download.
+message ClientDownloadReport {
+ // The information of user who provided the feedback.
+ // This is going to be useful for handling appeals.
+ message UserInformation {
+ optional string email = 1;
+ }
+
+ enum Reason {
+ SHARE = 0;
+ FALSE_POSITIVE = 1;
+ APPEAL = 2;
+ }
+
+ // The type of feedback for this report.
+ optional Reason reason = 1;
+
+ // The original download ping
+ optional ClientDownloadRequest download_request = 2;
+
+ // Stores the information of the user who provided the feedback.
+ optional UserInformation user_information = 3;
+
+ // Unstructed comments provided by the user.
+ optional bytes comment = 4;
+
+ // The original download response sent from the verdict server.
+ optional ClientDownloadResponse download_response = 5;
+}
+
+// This is used to send back upload status to the client after upload completion
+message ClientUploadResponse {
+ enum UploadStatus {
+ // The upload was successful and a complete response can be expected
+ SUCCESS = 0;
+
+ // The upload was unsuccessful and the response is incomplete.
+ UPLOAD_FAILURE = 1;
+ }
+
+ // Holds the upload status
+ optional UploadStatus status = 1;
+
+ // Holds the permalink where the results of scanning the binary are available
+ optional string permalink = 2;
+}
+
+message ClientIncidentReport {
+ message IncidentData {
+ message TrackedPreferenceIncident {
+ enum ValueState {
+ UNKNOWN = 0;
+ CLEARED = 1;
+ WEAK_LEGACY_OBSOLETE = 2;
+ CHANGED = 3;
+ UNTRUSTED_UNKNOWN_VALUE = 4;
+ }
+
+ optional string path = 1;
+ optional string atomic_value = 2;
+ repeated string split_key = 3;
+ optional ValueState value_state = 4;
+ }
+
+ message BinaryIntegrityIncident {
+ optional string file_basename = 1;
+ optional ClientDownloadRequest.SignatureInfo signature = 2;
+ optional ClientDownloadRequest.ImageHeaders image_headers = 3;
+ optional int32 sec_error = 4;
+
+ message ContainedFile {
+ optional string relative_path = 1;
+ optional ClientDownloadRequest.SignatureInfo signature = 2;
+ optional ClientDownloadRequest.ImageHeaders image_headers = 3;
+ }
+ repeated ContainedFile contained_file = 5;
+ }
+
+ message BlacklistLoadIncident {
+ optional string path = 1;
+ optional ClientDownloadRequest.Digests digest = 2;
+ optional string version = 3;
+ optional bool blacklist_initialized = 4;
+ optional ClientDownloadRequest.SignatureInfo signature = 5;
+ optional ClientDownloadRequest.ImageHeaders image_headers = 6;
+ }
+ message VariationsSeedSignatureIncident {
+ optional string variations_seed_signature = 1;
+ }
+ message ResourceRequestIncident {
+ enum Type {
+ UNKNOWN = 0;
+ TYPE_PATTERN = 3;
+ }
+ optional bytes digest = 1;
+ optional string origin = 2;
+ optional Type type = 3 [default = UNKNOWN];
+ }
+ message SuspiciousModuleIncident {
+ optional string path = 1;
+ optional ClientDownloadRequest.Digests digest = 2;
+ optional string version = 3;
+ optional ClientDownloadRequest.SignatureInfo signature = 4;
+ optional ClientDownloadRequest.ImageHeaders image_headers = 5;
+ }
+ optional int64 incident_time_msec = 1;
+ optional TrackedPreferenceIncident tracked_preference = 2;
+ optional BinaryIntegrityIncident binary_integrity = 3;
+ optional BlacklistLoadIncident blacklist_load = 4;
+ // Note: skip tag 5 because it was previously used.
+ optional VariationsSeedSignatureIncident variations_seed_signature = 6;
+ optional ResourceRequestIncident resource_request = 7;
+ optional SuspiciousModuleIncident suspicious_module = 8;
+ }
+
+ repeated IncidentData incident = 1;
+
+ message DownloadDetails {
+ optional bytes token = 1;
+ optional ClientDownloadRequest download = 2;
+ optional int64 download_time_msec = 3;
+ optional int64 open_time_msec = 4;
+ }
+
+ optional DownloadDetails download = 2;
+
+ message EnvironmentData {
+ message OS {
+ optional string os_name = 1;
+ optional string os_version = 2;
+
+ message RegistryValue {
+ optional string name = 1;
+ optional uint32 type = 2;
+ optional bytes data = 3;
+ }
+
+ message RegistryKey {
+ optional string name = 1;
+ repeated RegistryValue value = 2;
+ repeated RegistryKey key = 3;
+ }
+
+ repeated RegistryKey registry_key = 3;
+
+ optional bool is_enrolled_to_domain = 4;
+ }
+ optional OS os = 1;
+ message Machine {
+ optional string cpu_architecture = 1;
+ optional string cpu_vendor = 2;
+ optional uint32 cpuid = 3;
+ }
+ optional Machine machine = 2;
+ message Process {
+ optional string version = 1;
+ repeated string OBSOLETE_dlls = 2;
+ message Patch {
+ optional string function = 1;
+ optional string target_dll = 2;
+ }
+ repeated Patch patches = 3;
+ message NetworkProvider {}
+ repeated NetworkProvider network_providers = 4;
+ enum Channel {
+ CHANNEL_UNKNOWN = 0;
+ CHANNEL_CANARY = 1;
+ CHANNEL_DEV = 2;
+ CHANNEL_BETA = 3;
+ CHANNEL_STABLE = 4;
+ }
+ optional Channel chrome_update_channel = 5;
+ optional int64 uptime_msec = 6;
+ optional bool metrics_consent = 7;
+ optional bool extended_consent = 8;
+ message Dll {
+ enum Feature {
+ UNKNOWN = 0;
+ LSP = 1;
+ }
+ optional string path = 1;
+ optional uint64 base_address = 2;
+ optional uint32 length = 3;
+ repeated Feature feature = 4;
+ optional ClientDownloadRequest.ImageHeaders image_headers = 5;
+ }
+ repeated Dll dll = 9;
+ repeated string blacklisted_dll = 10;
+ message ModuleState {
+ enum ModifiedState {
+ UNKNOWN = 0;
+ MODULE_STATE_UNKNOWN = 1;
+ MODULE_STATE_UNMODIFIED = 2;
+ MODULE_STATE_MODIFIED = 3;
+ }
+ optional string name = 1;
+ optional ModifiedState modified_state = 2;
+ repeated string OBSOLETE_modified_export = 3;
+
+ message Modification {
+ optional uint32 file_offset = 1;
+ optional int32 byte_count = 2;
+ optional bytes modified_bytes = 3;
+ optional string export_name = 4;
+ }
+ repeated Modification modification = 4;
+ }
+ repeated ModuleState module_state = 11;
+ optional bool field_trial_participant = 12;
+ }
+ optional Process process = 3;
+ }
+
+ message ExtensionData {
+ message ExtensionInfo {
+ enum ExtensionState {
+ STATE_UNKNOWN = 0;
+ STATE_ENABLED = 1;
+ STATE_DISABLED = 2;
+ STATE_BLACKLISTED = 3;
+ STATE_BLOCKED = 4;
+ STATE_TERMINATED = 5;
+ }
+
+ optional string id = 1;
+ optional string version = 2;
+ optional string name = 3;
+ optional string description = 4;
+ optional ExtensionState state = 5 [default = STATE_UNKNOWN];
+ optional int32 type = 6;
+ optional string update_url = 7;
+ optional bool has_signature_validation = 8;
+ optional bool signature_is_valid = 9;
+ optional bool installed_by_custodian = 10;
+ optional bool installed_by_default = 11;
+ optional bool installed_by_oem = 12;
+ optional bool from_bookmark = 13;
+ optional bool from_webstore = 14;
+ optional bool converted_from_user_script = 15;
+ optional bool may_be_untrusted = 16;
+ optional int64 install_time_msec = 17;
+ optional int32 manifest_location_type = 18;
+ optional string manifest = 19;
+ }
+
+ optional ExtensionInfo last_installed_extension = 1;
+ }
+
+ optional EnvironmentData environment = 3;
+
+ // Population that the reporting user is part of.
+ optional ChromeUserPopulation population = 7;
+
+ optional ExtensionData extension_data = 8;
+
+ message NonBinaryDownloadDetails {
+ optional string file_type = 1;
+ optional bytes url_spec_sha256 = 2;
+ optional string host = 3;
+ optional int64 length = 4;
+ }
+
+ optional NonBinaryDownloadDetails non_binary_download = 9;
+}
+
+message ClientIncidentResponse {
+ optional bytes token = 1;
+ optional bool download_requested = 2;
+
+ message EnvironmentRequest { optional int32 dll_index = 1; }
+
+ repeated EnvironmentRequest environment_requests = 3;
+}
+
+message DownloadMetadata {
+ optional uint32 download_id = 1;
+
+ optional ClientIncidentReport.DownloadDetails download = 2;
+}
+
+// A Detailed Safebrowsing Report from clients. Chrome safebrowsing reports are
+// only sent by Chrome users who have opted into extended Safe Browsing.
+// This proto is replacing ClientMalwareReportRequest.
+// Next tag: 16
+message ClientSafeBrowsingReportRequest {
+ // Note: A lot of the "optional" fields would make sense to be
+ // "required" instead. However, having them as optional allows the
+ // clients to send "stripped down" versions of the message in the
+ // future, if we want to.
+
+ enum ReportType {
+ UNKNOWN = 0;
+ URL_PHISHING = 1;
+ URL_MALWARE = 2;
+ URL_UNWANTED = 3;
+ CLIENT_SIDE_PHISHING_URL = 4;
+ CLIENT_SIDE_MALWARE_URL = 5;
+ DANGEROUS_DOWNLOAD_RECOVERY = 6;
+ DANGEROUS_DOWNLOAD_WARNING = 7;
+ DANGEROUS_DOWNLOAD_BY_API = 10;
+ }
+
+ message HTTPHeader {
+ required bytes name = 1;
+ optional bytes value = 2;
+ }
+
+ message HTTPRequest {
+ message FirstLine {
+ optional bytes verb = 1;
+ optional bytes uri = 2;
+ optional bytes version = 3;
+ }
+
+ optional FirstLine firstline = 1;
+ repeated HTTPHeader headers = 2;
+ optional bytes body = 3;
+
+ // bodydigest and bodylength can be useful if the report does not
+ // contain the body itself.
+ optional bytes bodydigest = 4; // 32-byte hex md5 digest of body.
+ optional int32 bodylength = 5; // length of body.
+ }
+
+ message HTTPResponse {
+ message FirstLine {
+ optional int32 code = 1;
+ optional bytes reason = 2;
+ optional bytes version = 3;
+ }
+
+ optional FirstLine firstline = 1;
+ repeated HTTPHeader headers = 2;
+ optional bytes body = 3;
+ optional bytes bodydigest = 4; // 32-byte hex md5 digest of body.
+ optional int32 bodylength = 5; // length of body.
+ optional bytes remote_ip = 6; // IP of the server.
+ }
+
+ message Resource {
+ required int32 id = 1;
+ optional string url = 2;
+ optional HTTPRequest request = 3;
+ optional HTTPResponse response = 4;
+ optional int32 parent_id = 5;
+ repeated int32 child_ids = 6;
+ optional string tag_name = 7;
+ }
+
+ optional ReportType type = 10;
+
+ // Only set if ReportType is DANGEROUS_DOWNLOAD_RECOVERY,
+ // DANGEROUS_DOWNLOAD_WARNING or DANGEROUS_DOWNLOAD_BY_API.
+ optional ClientDownloadResponse.Verdict download_verdict = 11;
+
+ // URL of the page in the address bar.
+ optional string url = 1;
+ optional string page_url = 2;
+ optional string referrer_url = 3;
+
+ repeated Resource resources = 4;
+
+ // Whether the report is complete.
+ optional bool complete = 5;
+
+ // The ASN and country of the client IP. These fields are filled up by
+ // csd_frontend
+ repeated string client_asn = 6;
+ optional string client_country = 7;
+
+ // Whether user chose to proceed.
+ optional bool did_proceed = 8;
+
+ // Whether user visited this origin before.
+ optional bool repeat_visit = 9;
+
+ // The same token in ClientDownloadResponse. This field is only set if its
+ // report type is DANGEROUS_DOWNLOAD_RECOVERY, DANGEROUS_DOWNLOAD_WARNING or
+ // DANGEROUS_DOWNLOAD_BY_API.
+ optional bytes token = 15;
+}
diff --git a/toolkit/components/downloads/generate_csd.sh b/toolkit/components/downloads/generate_csd.sh
new file mode 100755
index 0000000000..a322d4c3c4
--- /dev/null
+++ b/toolkit/components/downloads/generate_csd.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+# A script to generate
+# chromium/chrome/common/safe_browsing/csd.pb.{cc,h} for use in
+# nsIApplicationReputationQuery. This script assumes you have
+# downloaded and installed the protocol buffer compiler.
+
+set -e
+
+if [ "${PROTOC_PATH:+set}" != "set" ]; then
+ PROTOC_PATH=/usr/local/bin/protoc
+fi
+
+echo "Using $PROTOC_PATH as protocol compiler"
+
+if [ ! -e $PROTOC_PATH ]; then
+ echo "You must install the protocol compiler from " \
+ "https://github.com/google/protobuf/releases"
+ exit 1
+fi
+
+if [ ! -f nsDownloadManager.cpp ]; then
+ echo "You must run this script in the toolkit/components/downloads" >&2
+ echo "directory of the source tree." >&2
+ exit 1
+fi
+
+# Get the protocol buffer and compile it
+CSD_PROTO_URL="https://chromium.googlesource.com/chromium/src/+/master/chrome/common/safe_browsing/csd.proto?format=TEXT"
+CSD_PATH="chromium/chrome/common/safe_browsing"
+
+curl "$CSD_PROTO_URL" | base64 --decode > "$CSD_PATH"/csd.proto
+"$PROTOC_PATH" "$CSD_PATH"/csd.proto --cpp_out=.
diff --git a/toolkit/components/downloads/moz.build b/toolkit/components/downloads/moz.build
new file mode 100644
index 0000000000..477db0bd64
--- /dev/null
+++ b/toolkit/components/downloads/moz.build
@@ -0,0 +1,74 @@
+# -*- 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/.
+
+with Files('*'):
+ BUG_COMPONENT = ('Toolkit', 'Download Manager')
+
+with Files('ApplicationReputation.*'):
+ BUG_COMPONENT = ('Toolkit', 'Safe Browsing')
+
+with Files('chromium/*'):
+ BUG_COMPONENT = ('Toolkit', 'Safe Browsing')
+
+with Files('generate_csd.sh'):
+ BUG_COMPONENT = ('Toolkit', 'Safe Browsing')
+
+with Files('nsIApplicationReputation.idl'):
+ BUG_COMPONENT = ('Toolkit', 'Safe Browsing')
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIApplicationReputation.idl',
+ 'nsIDownload.idl',
+ 'nsIDownloadManager.idl',
+ 'nsIDownloadManagerUI.idl',
+ 'nsIDownloadProgressListener.idl',
+]
+
+XPIDL_MODULE = 'downloads'
+
+UNIFIED_SOURCES += [
+ 'ApplicationReputation.cpp',
+ 'chromium/chrome/common/safe_browsing/csd.pb.cc',
+ 'nsDownloadManager.cpp'
+]
+
+# SQLFunctions.cpp cannot be built in unified mode because of Windows headers.
+SOURCES += [
+ 'SQLFunctions.cpp',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ # Can't build unified because we need CreateEvent which some IPC code
+ # included in LoadContext ends up undefining.
+ SOURCES += [
+ 'nsDownloadScanner.cpp',
+ ]
+
+# XXX - Until Suite builds off XULRunner we can't guarantee our implementation
+# of nsIDownloadManagerUI overrides toolkit's.
+if not CONFIG['MOZ_SUITE']:
+ EXTRA_COMPONENTS += [
+ 'nsDownloadManagerUI.js',
+ 'nsDownloadManagerUI.manifest',
+ ]
+
+FINAL_LIBRARY = 'xul'
+
+LOCAL_INCLUDES += [
+ '../protobuf',
+ '/ipc/chromium/src',
+ 'chromium'
+]
+
+DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
+DEFINES['GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER'] = True
+
+CXXFLAGS += CONFIG['TK_CFLAGS']
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-shadow']
diff --git a/toolkit/components/downloads/nsDownloadManager.cpp b/toolkit/components/downloads/nsDownloadManager.cpp
new file mode 100644
index 0000000000..ab984c5f20
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -0,0 +1,3783 @@
+/* -*- Mode: C++; 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/. */
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Unused.h"
+
+#include "mozIStorageService.h"
+#include "nsIAlertsService.h"
+#include "nsIArray.h"
+#include "nsIClassInfoImpl.h"
+#include "nsIDOMWindow.h"
+#include "nsIDownloadHistory.h"
+#include "nsIDownloadManagerUI.h"
+#include "nsIFileURL.h"
+#include "nsIMIMEService.h"
+#include "nsIParentalControlsService.h"
+#include "nsIPrefService.h"
+#include "nsIPrivateBrowsingChannel.h"
+#include "nsIPromptService.h"
+#include "nsIPropertyBag2.h"
+#include "nsIResumableChannel.h"
+#include "nsIWebBrowserPersist.h"
+#include "nsIWindowMediator.h"
+#include "nsILocalFileWin.h"
+#include "nsILoadContext.h"
+#include "nsIXULAppInfo.h"
+#include "nsContentUtils.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsArrayEnumerator.h"
+#include "nsCExternalHandlerService.h"
+#include "nsCRTGlue.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDownloadManager.h"
+#include "nsNetUtil.h"
+#include "nsThreadUtils.h"
+#include "prtime.h"
+
+#include "mozStorageCID.h"
+#include "nsDocShellCID.h"
+#include "nsEmbedCID.h"
+#include "nsToolkitCompsCID.h"
+
+#include "mozilla/net/ReferrerPolicy.h"
+
+#include "SQLFunctions.h"
+
+#include "mozilla/Preferences.h"
+
+#ifdef XP_WIN
+#include <shlobj.h>
+#include "nsWindowsHelpers.h"
+#ifdef DOWNLOAD_SCANNER
+#include "nsDownloadScanner.h"
+#endif
+#endif
+
+#ifdef XP_MACOSX
+#include <CoreFoundation/CoreFoundation.h>
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "FennecJNIWrappers.h"
+#endif
+
+#ifdef MOZ_WIDGET_GTK
+#include <gtk/gtk.h>
+#endif
+
+using namespace mozilla;
+using mozilla::downloads::GenerateGUID;
+
+#define DOWNLOAD_MANAGER_BUNDLE "chrome://mozapps/locale/downloads/downloads.properties"
+#define DOWNLOAD_MANAGER_ALERT_ICON "chrome://mozapps/skin/downloads/downloadIcon.png"
+#define PREF_BD_USEJSTRANSFER "browser.download.useJSTransfer"
+#define PREF_BDM_SHOWALERTONCOMPLETE "browser.download.manager.showAlertOnComplete"
+#define PREF_BDM_SHOWALERTINTERVAL "browser.download.manager.showAlertInterval"
+#define PREF_BDM_RETENTION "browser.download.manager.retention"
+#define PREF_BDM_QUITBEHAVIOR "browser.download.manager.quitBehavior"
+#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs"
+#define PREF_BDM_SCANWHENDONE "browser.download.manager.scanWhenDone"
+#define PREF_BDM_RESUMEONWAKEDELAY "browser.download.manager.resumeOnWakeDelay"
+#define PREF_BH_DELETETEMPFILEONEXIT "browser.helperApps.deleteTempFileOnExit"
+
+static const int64_t gUpdateInterval = 400 * PR_USEC_PER_MSEC;
+
+#define DM_SCHEMA_VERSION 9
+#define DM_DB_NAME NS_LITERAL_STRING("downloads.sqlite")
+#define DM_DB_CORRUPT_FILENAME NS_LITERAL_STRING("downloads.sqlite.corrupt")
+
+#define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1"
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsDownloadManager
+
+NS_IMPL_ISUPPORTS(
+ nsDownloadManager
+, nsIDownloadManager
+, nsINavHistoryObserver
+, nsIObserver
+, nsISupportsWeakReference
+)
+
+nsDownloadManager *nsDownloadManager::gDownloadManagerService = nullptr;
+
+nsDownloadManager *
+nsDownloadManager::GetSingleton()
+{
+ if (gDownloadManagerService) {
+ NS_ADDREF(gDownloadManagerService);
+ return gDownloadManagerService;
+ }
+
+ gDownloadManagerService = new nsDownloadManager();
+ if (gDownloadManagerService) {
+#if defined(MOZ_WIDGET_GTK)
+ g_type_init();
+#endif
+ NS_ADDREF(gDownloadManagerService);
+ if (NS_FAILED(gDownloadManagerService->Init()))
+ NS_RELEASE(gDownloadManagerService);
+ }
+
+ return gDownloadManagerService;
+}
+
+nsDownloadManager::~nsDownloadManager()
+{
+#ifdef DOWNLOAD_SCANNER
+ if (mScanner) {
+ delete mScanner;
+ mScanner = nullptr;
+ }
+#endif
+ gDownloadManagerService = nullptr;
+}
+
+nsresult
+nsDownloadManager::ResumeRetry(nsDownload *aDl)
+{
+ // Keep a reference in case we need to cancel the download
+ RefPtr<nsDownload> dl = aDl;
+
+ // Try to resume the active download
+ nsresult rv = dl->Resume();
+
+ // If not, try to retry the download
+ if (NS_FAILED(rv)) {
+ // First cancel the download so it's no longer active
+ rv = dl->Cancel();
+
+ // Then retry it
+ if (NS_SUCCEEDED(rv))
+ rv = dl->Retry();
+ }
+
+ return rv;
+}
+
+nsresult
+nsDownloadManager::PauseAllDownloads(bool aSetResume)
+{
+ nsresult rv = PauseAllDownloads(mCurrentDownloads, aSetResume);
+ nsresult rv2 = PauseAllDownloads(mCurrentPrivateDownloads, aSetResume);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::PauseAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aSetResume)
+{
+ nsresult retVal = NS_OK;
+ for (int32_t i = aDownloads.Count() - 1; i >= 0; --i) {
+ RefPtr<nsDownload> dl = aDownloads[i];
+
+ // Only pause things that need to be paused
+ if (!dl->IsPaused()) {
+ // Set auto-resume before pausing so that it gets into the DB
+ dl->mAutoResume = aSetResume ? nsDownload::AUTO_RESUME :
+ nsDownload::DONT_RESUME;
+
+ // Try to pause the download but don't bail now if we fail
+ nsresult rv = dl->Pause();
+ if (NS_FAILED(rv))
+ retVal = rv;
+ }
+ }
+
+ return retVal;
+}
+
+nsresult
+nsDownloadManager::ResumeAllDownloads(bool aResumeAll)
+{
+ nsresult rv = ResumeAllDownloads(mCurrentDownloads, aResumeAll);
+ nsresult rv2 = ResumeAllDownloads(mCurrentPrivateDownloads, aResumeAll);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::ResumeAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aResumeAll)
+{
+ nsresult retVal = NS_OK;
+ for (int32_t i = aDownloads.Count() - 1; i >= 0; --i) {
+ RefPtr<nsDownload> dl = aDownloads[i];
+
+ // If aResumeAll is true, then resume everything; otherwise, check if the
+ // download should auto-resume
+ if (aResumeAll || dl->ShouldAutoResume()) {
+ // Reset auto-resume before retrying so that it gets into the DB through
+ // ResumeRetry's eventual call to SetState. We clear the value now so we
+ // don't accidentally query completed downloads that were previously
+ // auto-resumed (and try to resume them).
+ dl->mAutoResume = nsDownload::DONT_RESUME;
+
+ // Try to resume/retry the download but don't bail now if we fail
+ nsresult rv = ResumeRetry(dl);
+ if (NS_FAILED(rv))
+ retVal = rv;
+ }
+ }
+
+ return retVal;
+}
+
+nsresult
+nsDownloadManager::RemoveAllDownloads()
+{
+ nsresult rv = RemoveAllDownloads(mCurrentDownloads);
+ nsresult rv2 = RemoveAllDownloads(mCurrentPrivateDownloads);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::RemoveAllDownloads(nsCOMArray<nsDownload>& aDownloads)
+{
+ nsresult rv = NS_OK;
+ for (int32_t i = aDownloads.Count() - 1; i >= 0; --i) {
+ RefPtr<nsDownload> dl = aDownloads[0];
+
+ nsresult result = NS_OK;
+ if (!dl->mPrivate && dl->IsPaused() && GetQuitBehavior() != QUIT_AND_CANCEL)
+ aDownloads.RemoveObject(dl);
+ else
+ result = dl->Cancel();
+
+ // Track the failure, but don't miss out on other downloads
+ if (NS_FAILED(result))
+ rv = result;
+ }
+
+ return rv;
+}
+
+nsresult
+nsDownloadManager::RemoveDownloadsForURI(mozIStorageStatement* aStatement, nsIURI *aURI)
+{
+ mozStorageStatementScoper scope(aStatement);
+
+ nsAutoCString source;
+ nsresult rv = aURI->GetSpec(source);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aStatement->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("source"), source);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ AutoTArray<nsCString, 4> downloads;
+ // Get all the downloads that match the provided URI
+ while (NS_SUCCEEDED(aStatement->ExecuteStep(&hasMore)) &&
+ hasMore) {
+ nsAutoCString downloadGuid;
+ rv = aStatement->GetUTF8String(0, downloadGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ downloads.AppendElement(downloadGuid);
+ }
+
+ // Remove each download ignoring any failure so we reach other downloads
+ for (int32_t i = downloads.Length(); --i >= 0; )
+ (void)RemoveDownload(downloads[i]);
+
+ return NS_OK;
+}
+
+void // static
+nsDownloadManager::ResumeOnWakeCallback(nsITimer *aTimer, void *aClosure)
+{
+ // Resume the downloads that were set to autoResume
+ nsDownloadManager *dlMgr = static_cast<nsDownloadManager *>(aClosure);
+ (void)dlMgr->ResumeAllDownloads(false);
+}
+
+already_AddRefed<mozIStorageConnection>
+nsDownloadManager::GetFileDBConnection(nsIFile *dbFile) const
+{
+ NS_ASSERTION(dbFile, "GetFileDBConnection called with an invalid nsIFile");
+
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, nullptr);
+
+ nsCOMPtr<mozIStorageConnection> conn;
+ nsresult rv = storage->OpenDatabase(dbFile, getter_AddRefs(conn));
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ // delete and try again, since we don't care so much about losing a user's
+ // download history
+ rv = dbFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ rv = storage->OpenDatabase(dbFile, getter_AddRefs(conn));
+ }
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return conn.forget();
+}
+
+already_AddRefed<mozIStorageConnection>
+nsDownloadManager::GetPrivateDBConnection() const
+{
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, nullptr);
+
+ nsCOMPtr<mozIStorageConnection> conn;
+ nsresult rv = storage->OpenSpecialDatabase("memory", getter_AddRefs(conn));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return conn.forget();
+}
+
+void
+nsDownloadManager::CloseAllDBs()
+{
+ CloseDB(mDBConn, mUpdateDownloadStatement, mGetIdsForURIStatement);
+ CloseDB(mPrivateDBConn, mUpdatePrivateDownloadStatement, mGetPrivateIdsForURIStatement);
+}
+
+void
+nsDownloadManager::CloseDB(mozIStorageConnection* aDBConn,
+ mozIStorageStatement* aUpdateStmt,
+ mozIStorageStatement* aGetIdsStmt)
+{
+ DebugOnly<nsresult> rv = aGetIdsStmt->Finalize();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = aUpdateStmt->Finalize();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = aDBConn->AsyncClose(nullptr);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+static nsresult
+InitSQLFunctions(mozIStorageConnection* aDBConn)
+{
+ nsresult rv = mozilla::downloads::GenerateGUIDFunction::create(aDBConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::InitPrivateDB()
+{
+ bool ready = false;
+ if (mPrivateDBConn && NS_SUCCEEDED(mPrivateDBConn->GetConnectionReady(&ready)) && ready)
+ CloseDB(mPrivateDBConn, mUpdatePrivateDownloadStatement, mGetPrivateIdsForURIStatement);
+ mPrivateDBConn = GetPrivateDBConnection();
+ if (!mPrivateDBConn)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ nsresult rv = InitSQLFunctions(mPrivateDBConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CreateTable(mPrivateDBConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InitStatements(mPrivateDBConn, getter_AddRefs(mUpdatePrivateDownloadStatement),
+ getter_AddRefs(mGetPrivateIdsForURIStatement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::InitFileDB()
+{
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> dbFile;
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = dbFile->Append(DM_DB_NAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool ready = false;
+ if (mDBConn && NS_SUCCEEDED(mDBConn->GetConnectionReady(&ready)) && ready)
+ CloseDB(mDBConn, mUpdateDownloadStatement, mGetIdsForURIStatement);
+ mDBConn = GetFileDBConnection(dbFile);
+ NS_ENSURE_TRUE(mDBConn, NS_ERROR_NOT_AVAILABLE);
+
+ rv = InitSQLFunctions(mDBConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool tableExists;
+ rv = mDBConn->TableExists(NS_LITERAL_CSTRING("moz_downloads"), &tableExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!tableExists) {
+ rv = CreateTable(mDBConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We're done with the initialization now and can skip the remaining
+ // upgrading logic.
+ return NS_OK;
+ }
+
+ // Checking the database schema now
+ int32_t schemaVersion;
+ rv = mDBConn->GetSchemaVersion(&schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Changing the database? Be sure to do these two things!
+ // 1) Increment DM_SCHEMA_VERSION
+ // 2) Implement the proper downgrade/upgrade code for the current version
+
+ switch (schemaVersion) {
+ // Upgrading
+ // Every time you increment the database schema, you need to implement
+ // the upgrading code from the previous version to the new one.
+ // Also, don't forget to make a unit test to test your upgrading code!
+ case 1: // Drop a column (iconURL) from the database (bug 385875)
+ {
+ // Safely wrap this in a transaction so we don't hose the whole DB
+ mozStorageTransaction safeTransaction(mDBConn, true);
+
+ // Create a temporary table that will store the existing records
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TEMPORARY TABLE moz_downloads_backup ("
+ "id INTEGER PRIMARY KEY, "
+ "name TEXT, "
+ "source TEXT, "
+ "target TEXT, "
+ "startTime INTEGER, "
+ "endTime INTEGER, "
+ "state INTEGER"
+ ")"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Insert into a temporary table
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_downloads_backup "
+ "SELECT id, name, source, target, startTime, endTime, state "
+ "FROM moz_downloads"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Drop the old table
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE moz_downloads"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now recreate it with this schema version
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE moz_downloads ("
+ "id INTEGER PRIMARY KEY, "
+ "name TEXT, "
+ "source TEXT, "
+ "target TEXT, "
+ "startTime INTEGER, "
+ "endTime INTEGER, "
+ "state INTEGER"
+ ")"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Insert the data back into it
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_downloads "
+ "SELECT id, name, source, target, startTime, endTime, state "
+ "FROM moz_downloads_backup"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // And drop our temporary table
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE moz_downloads_backup"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 2;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+ MOZ_FALLTHROUGH;
+
+ case 2: // Add referrer column to the database
+ {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN referrer TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 3;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+ MOZ_FALLTHROUGH;
+
+ case 3: // This version adds a column to the database (entityID)
+ {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN entityID TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 4;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+ MOZ_FALLTHROUGH;
+
+ case 4: // This version adds a column to the database (tempPath)
+ {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN tempPath TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 5;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+ MOZ_FALLTHROUGH;
+
+ case 5: // This version adds two columns for tracking transfer progress
+ {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN currBytes INTEGER NOT NULL DEFAULT 0"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN maxBytes INTEGER NOT NULL DEFAULT -1"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 6;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+ MOZ_FALLTHROUGH;
+
+ case 6: // This version adds three columns to DB (MIME type related info)
+ {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN mimeType TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN preferredApplication TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN preferredAction INTEGER NOT NULL DEFAULT 0"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 7;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to next upgrade
+ MOZ_FALLTHROUGH;
+
+ case 7: // This version adds a column to remember to auto-resume downloads
+ {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads "
+ "ADD COLUMN autoResume INTEGER NOT NULL DEFAULT 0"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the schemaVersion variable and the database schema
+ schemaVersion = 8;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+ MOZ_FALLTHROUGH;
+
+ // Warning: schema versions >=8 must take into account that they can
+ // be operating on schemas from unknown, future versions that have
+ // been downgraded. Operations such as adding columns may fail,
+ // since the column may already exist.
+
+ case 8: // This version adds a column for GUIDs
+ {
+ bool exists;
+ rv = mDBConn->IndexExists(NS_LITERAL_CSTRING("moz_downloads_guid_uniqueindex"),
+ &exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) {
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_downloads ADD COLUMN guid TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE UNIQUE INDEX moz_downloads_guid_uniqueindex ON moz_downloads (guid)"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_downloads SET guid = GENERATE_GUID() WHERE guid ISNULL"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, update the database schema
+ schemaVersion = 9;
+ rv = mDBConn->SetSchemaVersion(schemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to the next upgrade
+
+ // Extra sanity checking for developers
+#ifndef DEBUG
+ MOZ_FALLTHROUGH;
+ case DM_SCHEMA_VERSION:
+#endif
+ break;
+
+ case 0:
+ {
+ NS_WARNING("Could not get download database's schema version!");
+
+ // The table may still be usable - someone may have just messed with the
+ // schema version, so let's just treat this like a downgrade and verify
+ // that the needed columns are there. If they aren't there, we'll drop
+ // the table anyway.
+ rv = mDBConn->SetSchemaVersion(DM_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Fallthrough to downgrade check
+ MOZ_FALLTHROUGH;
+
+ // Downgrading
+ // If columns have been added to the table, we can still use the ones we
+ // understand safely. If columns have been deleted or alterd, we just
+ // drop the table and start from scratch. If you change how a column
+ // should be interpreted, make sure you also change its name so this
+ // check will catch it.
+ default:
+ {
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id, name, source, target, tempPath, startTime, endTime, state, "
+ "referrer, entityID, currBytes, maxBytes, mimeType, "
+ "preferredApplication, preferredAction, autoResume, guid "
+ "FROM moz_downloads"), getter_AddRefs(stmt));
+ if (NS_SUCCEEDED(rv)) {
+ // We have a database that contains all of the elements that make up
+ // the latest known schema. Reset the version to force an upgrade
+ // path if this downgraded database is used in a later version.
+ mDBConn->SetSchemaVersion(DM_SCHEMA_VERSION);
+ break;
+ }
+
+ // if the statement fails, that means all the columns were not there.
+ // First we backup the database
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, NS_ERROR_NOT_AVAILABLE);
+ nsCOMPtr<nsIFile> backup;
+ rv = storage->BackupDatabaseFile(dbFile, DM_DB_CORRUPT_FILENAME, nullptr,
+ getter_AddRefs(backup));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Then we dump it
+ rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE moz_downloads"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CreateTable(mDBConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::CreateTable(mozIStorageConnection* aDBConn)
+{
+ nsresult rv = aDBConn->SetSchemaVersion(DM_SCHEMA_VERSION);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE moz_downloads ("
+ "id INTEGER PRIMARY KEY, "
+ "name TEXT, "
+ "source TEXT, "
+ "target TEXT, "
+ "tempPath TEXT, "
+ "startTime INTEGER, "
+ "endTime INTEGER, "
+ "state INTEGER, "
+ "referrer TEXT, "
+ "entityID TEXT, "
+ "currBytes INTEGER NOT NULL DEFAULT 0, "
+ "maxBytes INTEGER NOT NULL DEFAULT -1, "
+ "mimeType TEXT, "
+ "preferredApplication TEXT, "
+ "preferredAction INTEGER NOT NULL DEFAULT 0, "
+ "autoResume INTEGER NOT NULL DEFAULT 0, "
+ "guid TEXT"
+ ")"));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE UNIQUE INDEX moz_downloads_guid_uniqueindex "
+ "ON moz_downloads(guid)"));
+ return rv;
+}
+
+nsresult
+nsDownloadManager::RestoreDatabaseState()
+{
+ // Restore downloads that were in a scanning state. We can assume that they
+ // have been dealt with by the virus scanner
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_downloads "
+ "SET state = :state "
+ "WHERE state = :state_cond"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_FINISHED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state_cond"), nsIDownloadManager::DOWNLOAD_SCANNING);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert supposedly-active downloads into downloads that should auto-resume
+ rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_downloads "
+ "SET autoResume = :autoResume "
+ "WHERE state = :notStarted "
+ "OR state = :queued "
+ "OR state = :downloading"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), nsDownload::AUTO_RESUME);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("notStarted"), nsIDownloadManager::DOWNLOAD_NOTSTARTED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("queued"), nsIDownloadManager::DOWNLOAD_QUEUED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("downloading"), nsIDownloadManager::DOWNLOAD_DOWNLOADING);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Switch any download that is supposed to automatically resume and is in a
+ // finished state to *not* automatically resume. See Bug 409179 for details.
+ rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_downloads "
+ "SET autoResume = :autoResume "
+ "WHERE state = :state "
+ "AND autoResume = :autoResume_cond"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), nsDownload::DONT_RESUME);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_FINISHED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume_cond"), nsDownload::AUTO_RESUME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::RestoreActiveDownloads()
+{
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id "
+ "FROM moz_downloads "
+ "WHERE (state = :state AND LENGTH(entityID) > 0) "
+ "OR autoResume != :autoResume"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_PAUSED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), nsDownload::DONT_RESUME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsresult retVal = NS_OK;
+ bool hasResults;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResults)) && hasResults) {
+ RefPtr<nsDownload> dl;
+ // Keep trying to add even if we fail one, but make sure to return failure.
+ // Additionally, be careful to not call anything that tries to change the
+ // database because we're iterating over a live statement.
+ if (NS_FAILED(GetDownloadFromDB(stmt->AsInt32(0), getter_AddRefs(dl))) ||
+ NS_FAILED(AddToCurrentDownloads(dl)))
+ retVal = NS_ERROR_FAILURE;
+ }
+
+ // Try to resume only the downloads that should auto-resume
+ rv = ResumeAllDownloads(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return retVal;
+}
+
+int64_t
+nsDownloadManager::AddDownloadToDB(const nsAString &aName,
+ const nsACString &aSource,
+ const nsACString &aTarget,
+ const nsAString &aTempPath,
+ int64_t aStartTime,
+ int64_t aEndTime,
+ const nsACString &aMimeType,
+ const nsACString &aPreferredApp,
+ nsHandlerInfoAction aPreferredAction,
+ bool aPrivate,
+ nsACString& aNewGUID)
+{
+ mozIStorageConnection* dbConn = aPrivate ? mPrivateDBConn : mDBConn;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_downloads "
+ "(name, source, target, tempPath, startTime, endTime, state, "
+ "mimeType, preferredApplication, preferredAction, guid) VALUES "
+ "(:name, :source, :target, :tempPath, :startTime, :endTime, :state, "
+ ":mimeType, :preferredApplication, :preferredAction, :guid)"),
+ getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), aName);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("source"), aSource);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("target"), aTarget);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("tempPath"), aTempPath);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), aStartTime);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), aEndTime);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), nsIDownloadManager::DOWNLOAD_NOTSTARTED);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("mimeType"), aMimeType);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("preferredApplication"), aPreferredApp);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("preferredAction"), aPreferredAction);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ nsAutoCString guid;
+ rv = GenerateGUID(guid);
+ NS_ENSURE_SUCCESS(rv, 0);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), guid);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ bool hasMore;
+ rv = stmt->ExecuteStep(&hasMore); // we want to keep our lock
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ int64_t id = 0;
+ rv = dbConn->GetLastInsertRowID(&id);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ aNewGUID = guid;
+
+ // lock on DB from statement will be released once we return
+ return id;
+}
+
+nsresult
+nsDownloadManager::InitDB()
+{
+ nsresult rv = InitPrivateDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InitFileDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InitStatements(mDBConn, getter_AddRefs(mUpdateDownloadStatement),
+ getter_AddRefs(mGetIdsForURIStatement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::InitStatements(mozIStorageConnection* aDBConn,
+ mozIStorageStatement** aUpdateStatement,
+ mozIStorageStatement** aGetIdsStatement)
+{
+ nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_downloads "
+ "SET tempPath = :tempPath, startTime = :startTime, endTime = :endTime, "
+ "state = :state, referrer = :referrer, entityID = :entityID, "
+ "currBytes = :currBytes, maxBytes = :maxBytes, autoResume = :autoResume "
+ "WHERE id = :id"), aUpdateStatement);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT guid "
+ "FROM moz_downloads "
+ "WHERE source = :source"), aGetIdsStatement);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::Init()
+{
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::services::GetStringBundleService();
+ if (!bundleService)
+ return NS_ERROR_FAILURE;
+
+ rv = bundleService->CreateBundle(DOWNLOAD_MANAGER_BUNDLE,
+ getter_AddRefs(mBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#if !defined(MOZ_JSDOWNLOADS)
+ // When MOZ_JSDOWNLOADS is undefined, we still check the preference that can
+ // be used to enable the JavaScript API during the migration process.
+ mUseJSTransfer = Preferences::GetBool(PREF_BD_USEJSTRANSFER, false);
+#else
+ mUseJSTransfer = true;
+#endif
+
+ if (mUseJSTransfer)
+ return NS_OK;
+
+ // Clean up any old downloads.rdf files from before Firefox 3
+ {
+ nsCOMPtr<nsIFile> oldDownloadsFile;
+ bool fileExists;
+ if (NS_SUCCEEDED(NS_GetSpecialDirectory(NS_APP_DOWNLOADS_50_FILE,
+ getter_AddRefs(oldDownloadsFile))) &&
+ NS_SUCCEEDED(oldDownloadsFile->Exists(&fileExists)) &&
+ fileExists) {
+ (void)oldDownloadsFile->Remove(false);
+ }
+ }
+
+ mObserverService = mozilla::services::GetObserverService();
+ if (!mObserverService)
+ return NS_ERROR_FAILURE;
+
+ rv = InitDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef DOWNLOAD_SCANNER
+ mScanner = new nsDownloadScanner();
+ if (!mScanner)
+ return NS_ERROR_OUT_OF_MEMORY;
+ rv = mScanner->Init();
+ if (NS_FAILED(rv)) {
+ delete mScanner;
+ mScanner = nullptr;
+ }
+#endif
+
+ // Do things *after* initializing various download manager properties such as
+ // restoring downloads to a consistent state
+ rv = RestoreDatabaseState();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = RestoreActiveDownloads();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to restore all active downloads");
+
+ nsCOMPtr<nsINavHistoryService> history =
+ do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);
+
+ (void)mObserverService->NotifyObservers(
+ static_cast<nsIDownloadManager *>(this),
+ "download-manager-initialized",
+ nullptr);
+
+ // The following AddObserver calls must be the last lines in this function,
+ // because otherwise, this function may fail (and thus, this object would be not
+ // completely initialized), but the observerservice would still keep a reference
+ // to us and notify us about shutdown, which may cause crashes.
+ // failure to add an observer is not critical
+ (void)mObserverService->AddObserver(this, "quit-application", true);
+ (void)mObserverService->AddObserver(this, "quit-application-requested", true);
+ (void)mObserverService->AddObserver(this, "offline-requested", true);
+ (void)mObserverService->AddObserver(this, "sleep_notification", true);
+ (void)mObserverService->AddObserver(this, "wake_notification", true);
+ (void)mObserverService->AddObserver(this, "suspend_process_notification", true);
+ (void)mObserverService->AddObserver(this, "resume_process_notification", true);
+ (void)mObserverService->AddObserver(this, "profile-before-change", true);
+ (void)mObserverService->AddObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, true);
+ (void)mObserverService->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, true);
+ (void)mObserverService->AddObserver(this, "last-pb-context-exited", true);
+ (void)mObserverService->AddObserver(this, "last-pb-context-exiting", true);
+
+ if (history)
+ (void)history->AddObserver(this, true);
+
+ return NS_OK;
+}
+
+int32_t
+nsDownloadManager::GetRetentionBehavior()
+{
+ // We use 0 as the default, which is "remove when done"
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pref = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ int32_t val;
+ rv = pref->GetIntPref(PREF_BDM_RETENTION, &val);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ // Allow the Downloads Panel to change the retention behavior. We do this to
+ // allow proper migration to the new feature when using the same profile on
+ // multiple versions of the product (bug 697678). Implementation note: in
+ // order to allow observers to change the retention value, we have to pass an
+ // object in the aSubject parameter, we cannot use aData for that.
+ nsCOMPtr<nsISupportsPRInt32> retentionBehavior =
+ do_CreateInstance(NS_SUPPORTS_PRINT32_CONTRACTID);
+ retentionBehavior->SetData(val);
+ (void)mObserverService->NotifyObservers(retentionBehavior,
+ "download-manager-change-retention",
+ nullptr);
+ retentionBehavior->GetData(&val);
+
+ return val;
+}
+
+enum nsDownloadManager::QuitBehavior
+nsDownloadManager::GetQuitBehavior()
+{
+ // We use 0 as the default, which is "remember and resume the download"
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> pref = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, QUIT_AND_RESUME);
+
+ int32_t val;
+ rv = pref->GetIntPref(PREF_BDM_QUITBEHAVIOR, &val);
+ NS_ENSURE_SUCCESS(rv, QUIT_AND_RESUME);
+
+ switch (val) {
+ case 1:
+ return QUIT_AND_PAUSE;
+ case 2:
+ return QUIT_AND_CANCEL;
+ default:
+ return QUIT_AND_RESUME;
+ }
+}
+
+// Using a globally-unique GUID, search all databases (both private and public).
+// A return value of NS_ERROR_NOT_AVAILABLE means no download with the given GUID
+// could be found, either private or public.
+
+nsresult
+nsDownloadManager::GetDownloadFromDB(const nsACString& aGUID, nsDownload **retVal)
+{
+ MOZ_ASSERT(!FindDownload(aGUID),
+ "If it is a current download, you should not call this method!");
+
+ NS_NAMED_LITERAL_CSTRING(query,
+ "SELECT id, state, startTime, source, target, tempPath, name, referrer, "
+ "entityID, currBytes, maxBytes, mimeType, preferredAction, "
+ "preferredApplication, autoResume, guid "
+ "FROM moz_downloads "
+ "WHERE guid = :guid");
+ // First, let's query the database and see if it even exists
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mDBConn->CreateStatement(query, getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetDownloadFromDB(mDBConn, stmt, retVal);
+
+ // If the download cannot be found in the public database, try again
+ // in the private one. Otherwise, return whatever successful result
+ // or failure obtained from the public database.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ rv = mPrivateDBConn->CreateStatement(query, getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetDownloadFromDB(mPrivateDBConn, stmt, retVal);
+
+ // Only if it still cannot be found do we report the failure.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ *retVal = nullptr;
+ }
+ }
+ return rv;
+}
+
+nsresult
+nsDownloadManager::GetDownloadFromDB(uint32_t aID, nsDownload **retVal)
+{
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ MOZ_ASSERT(!FindDownload(aID),
+ "If it is a current download, you should not call this method!");
+
+ // First, let's query the database and see if it even exists
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id, state, startTime, source, target, tempPath, name, referrer, "
+ "entityID, currBytes, maxBytes, mimeType, preferredAction, "
+ "preferredApplication, autoResume, guid "
+ "FROM moz_downloads "
+ "WHERE id = :id"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetDownloadFromDB(mDBConn, stmt, retVal);
+}
+
+nsresult
+nsDownloadManager::GetDownloadFromDB(mozIStorageConnection* aDBConn,
+ mozIStorageStatement* stmt,
+ nsDownload **retVal)
+{
+ bool hasResults = false;
+ nsresult rv = stmt->ExecuteStep(&hasResults);
+ if (NS_FAILED(rv) || !hasResults)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ // We have a download, so lets create it
+ RefPtr<nsDownload> dl = new nsDownload();
+ if (!dl)
+ return NS_ERROR_OUT_OF_MEMORY;
+ dl->mPrivate = aDBConn == mPrivateDBConn;
+
+ dl->mDownloadManager = this;
+
+ int32_t i = 0;
+ // Setting all properties of the download now
+ dl->mCancelable = nullptr;
+ dl->mID = stmt->AsInt64(i++);
+ dl->mDownloadState = stmt->AsInt32(i++);
+ dl->mStartTime = stmt->AsInt64(i++);
+
+ nsCString source;
+ stmt->GetUTF8String(i++, source);
+ rv = NS_NewURI(getter_AddRefs(dl->mSource), source);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString target;
+ stmt->GetUTF8String(i++, target);
+ rv = NS_NewURI(getter_AddRefs(dl->mTarget), target);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString tempPath;
+ stmt->GetString(i++, tempPath);
+ if (!tempPath.IsEmpty()) {
+ rv = NS_NewLocalFile(tempPath, true, getter_AddRefs(dl->mTempFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ stmt->GetString(i++, dl->mDisplayName);
+
+ nsCString referrer;
+ rv = stmt->GetUTF8String(i++, referrer);
+ if (NS_SUCCEEDED(rv) && !referrer.IsEmpty()) {
+ rv = NS_NewURI(getter_AddRefs(dl->mReferrer), referrer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = stmt->GetUTF8String(i++, dl->mEntityID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t currBytes = stmt->AsInt64(i++);
+ int64_t maxBytes = stmt->AsInt64(i++);
+ dl->SetProgressBytes(currBytes, maxBytes);
+
+ // Build mMIMEInfo only if the mimeType in DB is not empty
+ nsAutoCString mimeType;
+ rv = stmt->GetUTF8String(i++, mimeType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mimeType.IsEmpty()) {
+ nsCOMPtr<nsIMIMEService> mimeService =
+ do_GetService(NS_MIMESERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mimeService->GetFromTypeAndExtension(mimeType, EmptyCString(),
+ getter_AddRefs(dl->mMIMEInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsHandlerInfoAction action = stmt->AsInt32(i++);
+ rv = dl->mMIMEInfo->SetPreferredAction(action);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString persistentDescriptor;
+ rv = stmt->GetUTF8String(i++, persistentDescriptor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!persistentDescriptor.IsEmpty()) {
+ nsCOMPtr<nsILocalHandlerApp> handler =
+ do_CreateInstance(NS_LOCALHANDLERAPP_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> localExecutable;
+ rv = NS_NewNativeLocalFile(EmptyCString(), false,
+ getter_AddRefs(localExecutable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = localExecutable->SetPersistentDescriptor(persistentDescriptor);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = handler->SetExecutable(localExecutable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = dl->mMIMEInfo->SetPreferredApplicationHandler(handler);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Compensate for the i++s skipped in the true block
+ i += 2;
+ }
+
+ dl->mAutoResume =
+ static_cast<enum nsDownload::AutoResume>(stmt->AsInt32(i++));
+
+ rv = stmt->GetUTF8String(i++, dl->mGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Handle situations where we load a download from a database that has been
+ // used in an older version and not gone through the upgrade path (ie. it
+ // contains empty GUID entries).
+ if (dl->mGUID.IsEmpty()) {
+ rv = GenerateGUID(dl->mGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStorageStatement> updateStmt;
+ rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_downloads SET guid = :guid "
+ "WHERE id = :id"),
+ getter_AddRefs(updateStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = updateStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), dl->mGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), dl->mID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = updateStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Addrefing and returning
+ dl.forget(retVal);
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::AddToCurrentDownloads(nsDownload *aDl)
+{
+ nsCOMArray<nsDownload>& currentDownloads =
+ aDl->mPrivate ? mCurrentPrivateDownloads : mCurrentDownloads;
+ if (!currentDownloads.AppendObject(aDl))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ aDl->mDownloadManager = this;
+ return NS_OK;
+}
+
+void
+nsDownloadManager::SendEvent(nsDownload *aDownload, const char *aTopic)
+{
+ (void)mObserverService->NotifyObservers(aDownload, aTopic, nullptr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIDownloadManager
+
+NS_IMETHODIMP
+nsDownloadManager::GetActivePrivateDownloadCount(int32_t* aResult)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ *aResult = mCurrentPrivateDownloads.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetActiveDownloadCount(int32_t *aResult)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ *aResult = mCurrentDownloads.Count();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetActiveDownloads(nsISimpleEnumerator **aResult)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ return NS_NewArrayEnumerator(aResult, mCurrentDownloads);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetActivePrivateDownloads(nsISimpleEnumerator **aResult)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ return NS_NewArrayEnumerator(aResult, mCurrentPrivateDownloads);
+}
+
+/**
+ * For platforms where helper apps use the downloads directory (i.e. mobile),
+ * this should be kept in sync with nsExternalHelperAppService.cpp
+ */
+NS_IMETHODIMP
+nsDownloadManager::GetDefaultDownloadsDirectory(nsIFile **aResult)
+{
+ nsCOMPtr<nsIFile> downloadDir;
+
+ nsresult rv;
+ nsCOMPtr<nsIProperties> dirService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // OSX 10.4:
+ // Desktop
+ // OSX 10.5:
+ // User download directory
+ // Vista:
+ // Downloads
+ // XP/2K:
+ // My Documents/Downloads
+ // Linux:
+ // XDG user dir spec, with a fallback to Home/Downloads
+
+ nsXPIDLString folderName;
+ mBundle->GetStringFromName(u"downloadsFolder",
+ getter_Copies(folderName));
+
+#if defined (XP_MACOSX)
+ rv = dirService->Get(NS_OSX_DEFAULT_DOWNLOAD_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+#elif defined(XP_WIN)
+ rv = dirService->Get(NS_WIN_DEFAULT_DOWNLOAD_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check the os version
+ nsCOMPtr<nsIPropertyBag2> infoService =
+ do_GetService(NS_SYSTEMINFO_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t version;
+ NS_NAMED_LITERAL_STRING(osVersion, "version");
+ rv = infoService->GetPropertyAsInt32(osVersion, &version);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (version < 6) { // XP/2K
+ // First get "My Documents"
+ rv = dirService->Get(NS_WIN_PERSONAL_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = downloadDir->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This could be the first time we are creating the downloads folder in My
+ // Documents, so make sure it exists.
+ bool exists;
+ rv = downloadDir->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!exists) {
+ rv = downloadDir->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+#elif defined(XP_UNIX)
+#if defined(MOZ_WIDGET_ANDROID)
+ // Android doesn't have a $HOME directory, and by default we only have
+ // write access to /data/data/org.mozilla.{$APP} and /sdcard
+ char* downloadDirPath = getenv("DOWNLOADS_DIRECTORY");
+ if (downloadDirPath) {
+ rv = NS_NewNativeLocalFile(nsDependentCString(downloadDirPath),
+ true, getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ rv = NS_ERROR_FAILURE;
+ }
+#else
+ rv = dirService->Get(NS_UNIX_DEFAULT_DOWNLOAD_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ // fallback to Home/Downloads
+ if (NS_FAILED(rv)) {
+ rv = dirService->Get(NS_UNIX_HOME_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = downloadDir->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+#endif
+#else
+ rv = dirService->Get(NS_OS_HOME_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = downloadDir->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+#endif
+
+ downloadDir.forget(aResult);
+
+ return NS_OK;
+}
+
+#define NS_BRANCH_DOWNLOAD "browser.download."
+#define NS_PREF_FOLDERLIST "folderList"
+#define NS_PREF_DIR "dir"
+
+NS_IMETHODIMP
+nsDownloadManager::GetUserDownloadsDirectory(nsIFile **aResult)
+{
+ nsresult rv;
+ nsCOMPtr<nsIProperties> dirService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefService> prefService =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ rv = prefService->GetBranch(NS_BRANCH_DOWNLOAD,
+ getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t val;
+ rv = prefBranch->GetIntPref(NS_PREF_FOLDERLIST,
+ &val);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch(val) {
+ case 0: // Desktop
+ {
+ nsCOMPtr<nsIFile> downloadDir;
+ rv = dirService->Get(NS_OS_DESKTOP_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(downloadDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ downloadDir.forget(aResult);
+ return NS_OK;
+ }
+ break;
+ case 1: // Downloads
+ return GetDefaultDownloadsDirectory(aResult);
+ case 2: // Custom
+ {
+ nsCOMPtr<nsIFile> customDirectory;
+ prefBranch->GetComplexValue(NS_PREF_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(customDirectory));
+ if (customDirectory) {
+ bool exists = false;
+ (void)customDirectory->Exists(&exists);
+
+ if (!exists) {
+ rv = customDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (NS_SUCCEEDED(rv)) {
+ customDirectory.forget(aResult);
+ return NS_OK;
+ }
+
+ // Create failed, so it still doesn't exist. Fall out and get the
+ // default downloads directory.
+ }
+
+ bool writable = false;
+ bool directory = false;
+ (void)customDirectory->IsWritable(&writable);
+ (void)customDirectory->IsDirectory(&directory);
+
+ if (exists && writable && directory) {
+ customDirectory.forget(aResult);
+ return NS_OK;
+ }
+ }
+ rv = GetDefaultDownloadsDirectory(aResult);
+ if (NS_SUCCEEDED(rv)) {
+ (void)prefBranch->SetComplexValue(NS_PREF_DIR,
+ NS_GET_IID(nsIFile),
+ *aResult);
+ }
+ return rv;
+ }
+ break;
+ }
+ return NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::AddDownload(DownloadType aDownloadType,
+ nsIURI *aSource,
+ nsIURI *aTarget,
+ const nsAString& aDisplayName,
+ nsIMIMEInfo *aMIMEInfo,
+ PRTime aStartTime,
+ nsIFile *aTempFile,
+ nsICancelable *aCancelable,
+ bool aIsPrivate,
+ nsIDownload **aDownload)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_ENSURE_ARG_POINTER(aSource);
+ NS_ENSURE_ARG_POINTER(aTarget);
+ NS_ENSURE_ARG_POINTER(aDownload);
+
+ nsresult rv;
+
+ // target must be on the local filesystem
+ nsCOMPtr<nsIFileURL> targetFileURL = do_QueryInterface(aTarget, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> targetFile;
+ rv = targetFileURL->GetFile(getter_AddRefs(targetFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsDownload> dl = new nsDownload();
+ if (!dl)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // give our new nsIDownload some info so it's ready to go off into the world
+ dl->mTarget = aTarget;
+ dl->mSource = aSource;
+ dl->mTempFile = aTempFile;
+ dl->mPrivate = aIsPrivate;
+
+ dl->mDisplayName = aDisplayName;
+ if (dl->mDisplayName.IsEmpty())
+ targetFile->GetLeafName(dl->mDisplayName);
+
+ dl->mMIMEInfo = aMIMEInfo;
+ dl->SetStartTime(aStartTime == 0 ? PR_Now() : aStartTime);
+
+ // Creates a cycle that will be broken when the download finishes
+ dl->mCancelable = aCancelable;
+
+ // Adding to the DB
+ nsAutoCString source, target;
+ rv = aSource->GetSpec(source);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aTarget->GetSpec(target);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Track the temp file for exthandler downloads
+ nsAutoString tempPath;
+ if (aTempFile)
+ aTempFile->GetPath(tempPath);
+
+ // Break down MIMEInfo but don't panic if we can't get all the pieces - we
+ // can still download the file
+ nsAutoCString persistentDescriptor, mimeType;
+ nsHandlerInfoAction action = nsIMIMEInfo::saveToDisk;
+ if (aMIMEInfo) {
+ (void)aMIMEInfo->GetType(mimeType);
+
+ nsCOMPtr<nsIHandlerApp> handlerApp;
+ (void)aMIMEInfo->GetPreferredApplicationHandler(getter_AddRefs(handlerApp));
+ nsCOMPtr<nsILocalHandlerApp> locHandlerApp = do_QueryInterface(handlerApp);
+
+ if (locHandlerApp) {
+ nsCOMPtr<nsIFile> executable;
+ (void)locHandlerApp->GetExecutable(getter_AddRefs(executable));
+ Unused << executable->GetPersistentDescriptor(persistentDescriptor);
+ }
+
+ (void)aMIMEInfo->GetPreferredAction(&action);
+ }
+
+ int64_t id = AddDownloadToDB(dl->mDisplayName, source, target, tempPath,
+ dl->mStartTime, dl->mLastUpdate,
+ mimeType, persistentDescriptor, action,
+ dl->mPrivate, dl->mGUID /* outparam */);
+ NS_ENSURE_TRUE(id, NS_ERROR_FAILURE);
+ dl->mID = id;
+
+ rv = AddToCurrentDownloads(dl);
+ (void)dl->SetState(nsIDownloadManager::DOWNLOAD_QUEUED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef DOWNLOAD_SCANNER
+ if (mScanner) {
+ bool scan = true;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ (void)prefs->GetBoolPref(PREF_BDM_SCANWHENDONE, &scan);
+ }
+ // We currently apply local security policy to downloads when we scan
+ // via windows all-in-one download security api. The CheckPolicy call
+ // below is a pre-emptive part of that process. So tie applying security
+ // zone policy settings when downloads are intiated to the same pref
+ // that triggers applying security zone policy settings after a download
+ // completes. (bug 504804)
+ if (scan) {
+ AVCheckPolicyState res = mScanner->CheckPolicy(aSource, aTarget);
+ if (res == AVPOLICY_BLOCKED) {
+ // This download will get deleted during a call to IAE's Save,
+ // so go ahead and mark it as blocked and avoid the download.
+ (void)CancelDownload(id);
+ (void)dl->SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY);
+ }
+ }
+ }
+#endif
+
+ // Check with parental controls to see if file downloads
+ // are allowed for this user. If not allowed, cancel the
+ // download and mark its state as being blocked.
+ nsCOMPtr<nsIParentalControlsService> pc =
+ do_CreateInstance(NS_PARENTALCONTROLSSERVICE_CONTRACTID);
+ if (pc) {
+ bool enabled = false;
+ (void)pc->GetBlockFileDownloadsEnabled(&enabled);
+ if (enabled) {
+ (void)CancelDownload(id);
+ (void)dl->SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL);
+ }
+
+ // Log the event if required by pc settings.
+ bool logEnabled = false;
+ (void)pc->GetLoggingEnabled(&logEnabled);
+ if (logEnabled) {
+ (void)pc->Log(nsIParentalControlsService::ePCLog_FileDownload,
+ enabled,
+ aSource,
+ nullptr);
+ }
+ }
+
+ dl.forget(aDownload);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetDownload(uint32_t aID, nsIDownload **aDownloadItem)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ nsDownload *itm = FindDownload(aID);
+
+ RefPtr<nsDownload> dl;
+ if (!itm) {
+ nsresult rv = GetDownloadFromDB(aID, getter_AddRefs(dl));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ itm = dl.get();
+ }
+
+ NS_ADDREF(*aDownloadItem = itm);
+
+ return NS_OK;
+}
+
+namespace {
+class AsyncResult : public Runnable
+{
+public:
+ AsyncResult(nsresult aStatus, nsIDownload* aResult,
+ nsIDownloadManagerResult* aCallback)
+ : mStatus(aStatus), mResult(aResult), mCallback(aCallback)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ mCallback->HandleResult(mStatus, mResult);
+ return NS_OK;
+ }
+
+private:
+ nsresult mStatus;
+ nsCOMPtr<nsIDownload> mResult;
+ nsCOMPtr<nsIDownloadManagerResult> mCallback;
+};
+} // namespace
+
+NS_IMETHODIMP
+nsDownloadManager::GetDownloadByGUID(const nsACString& aGUID,
+ nsIDownloadManagerResult* aCallback)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ nsDownload *itm = FindDownload(aGUID);
+
+ nsresult rv = NS_OK;
+ RefPtr<nsDownload> dl;
+ if (!itm) {
+ rv = GetDownloadFromDB(aGUID, getter_AddRefs(dl));
+ itm = dl.get();
+ }
+
+ RefPtr<AsyncResult> runnable = new AsyncResult(rv, itm, aCallback);
+ NS_DispatchToMainThread(runnable);
+ return NS_OK;
+}
+
+nsDownload *
+nsDownloadManager::FindDownload(uint32_t aID)
+{
+ // we shouldn't ever have many downloads, so we can loop over them
+ for (int32_t i = mCurrentDownloads.Count() - 1; i >= 0; --i) {
+ nsDownload *dl = mCurrentDownloads[i];
+ if (dl->mID == aID)
+ return dl;
+ }
+
+ return nullptr;
+}
+
+nsDownload *
+nsDownloadManager::FindDownload(const nsACString& aGUID)
+{
+ // we shouldn't ever have many downloads, so we can loop over them
+ for (int32_t i = mCurrentDownloads.Count() - 1; i >= 0; --i) {
+ nsDownload *dl = mCurrentDownloads[i];
+ if (dl->mGUID == aGUID)
+ return dl;
+ }
+
+ for (int32_t i = mCurrentPrivateDownloads.Count() - 1; i >= 0; --i) {
+ nsDownload *dl = mCurrentPrivateDownloads[i];
+ if (dl->mGUID == aGUID)
+ return dl;
+ }
+
+ return nullptr;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::CancelDownload(uint32_t aID)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ // We AddRef here so we don't lose access to member variables when we remove
+ RefPtr<nsDownload> dl = FindDownload(aID);
+
+ // if it's null, someone passed us a bad id.
+ if (!dl)
+ return NS_ERROR_FAILURE;
+
+ return dl->Cancel();
+}
+
+nsresult
+nsDownloadManager::RetryDownload(const nsACString& aGUID)
+{
+ RefPtr<nsDownload> dl;
+ nsresult rv = GetDownloadFromDB(aGUID, getter_AddRefs(dl));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RetryDownload(dl);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::RetryDownload(uint32_t aID)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ RefPtr<nsDownload> dl;
+ nsresult rv = GetDownloadFromDB(aID, getter_AddRefs(dl));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RetryDownload(dl);
+}
+
+nsresult
+nsDownloadManager::RetryDownload(nsDownload* dl)
+{
+ // if our download is not canceled or failed, we should fail
+ if (dl->mDownloadState != nsIDownloadManager::DOWNLOAD_FAILED &&
+ dl->mDownloadState != nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL &&
+ dl->mDownloadState != nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY &&
+ dl->mDownloadState != nsIDownloadManager::DOWNLOAD_DIRTY &&
+ dl->mDownloadState != nsIDownloadManager::DOWNLOAD_CANCELED)
+ return NS_ERROR_FAILURE;
+
+ // If the download has failed and is resumable then we first try resuming it
+ nsresult rv;
+ if (dl->mDownloadState == nsIDownloadManager::DOWNLOAD_FAILED && dl->IsResumable()) {
+ rv = dl->Resume();
+ if (NS_SUCCEEDED(rv))
+ return rv;
+ }
+
+ // reset time and download progress
+ dl->SetStartTime(PR_Now());
+ dl->SetProgressBytes(0, -1);
+
+ nsCOMPtr<nsIWebBrowserPersist> wbp =
+ do_CreateInstance("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = wbp->SetPersistFlags(nsIWebBrowserPersist::PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddToCurrentDownloads(dl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = dl->SetState(nsIDownloadManager::DOWNLOAD_QUEUED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Creates a cycle that will be broken when the download finishes
+ dl->mCancelable = wbp;
+ (void)wbp->SetProgressListener(dl);
+
+ // referrer policy can be anything since referrer is nullptr
+ rv = wbp->SavePrivacyAwareURI(dl->mSource, nullptr,
+ nullptr, mozilla::net::RP_Default,
+ nullptr, nullptr,
+ dl->mTarget, dl->mPrivate);
+ if (NS_FAILED(rv)) {
+ dl->mCancelable = nullptr;
+ (void)wbp->SetProgressListener(nullptr);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+static nsresult
+RemoveDownloadByGUID(const nsACString& aGUID, mozIStorageConnection* aDBConn)
+{
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_downloads "
+ "WHERE guid = :guid"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+nsDownloadManager::RemoveDownload(const nsACString& aGUID)
+{
+ RefPtr<nsDownload> dl = FindDownload(aGUID);
+ MOZ_ASSERT(!dl, "Can't call RemoveDownload on a download in progress!");
+ if (dl)
+ return NS_ERROR_FAILURE;
+
+ nsresult rv = GetDownloadFromDB(aGUID, getter_AddRefs(dl));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (dl->mPrivate) {
+ RemoveDownloadByGUID(aGUID, mPrivateDBConn);
+ } else {
+ RemoveDownloadByGUID(aGUID, mDBConn);
+ }
+
+ return NotifyDownloadRemoval(dl);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::RemoveDownload(uint32_t aID)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ RefPtr<nsDownload> dl = FindDownload(aID);
+ MOZ_ASSERT(!dl, "Can't call RemoveDownload on a download in progress!");
+ if (dl)
+ return NS_ERROR_FAILURE;
+
+ nsresult rv = GetDownloadFromDB(aID, getter_AddRefs(dl));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_downloads "
+ "WHERE id = :id"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aID); // unsigned; 64-bit to prevent overflow
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Notify the UI with the topic and download id
+ return NotifyDownloadRemoval(dl);
+}
+
+nsresult
+nsDownloadManager::NotifyDownloadRemoval(nsDownload* aRemoved)
+{
+ nsCOMPtr<nsISupportsPRUint32> id;
+ nsCOMPtr<nsISupportsCString> guid;
+ nsresult rv;
+
+ // Only send an integer ID notification if the download is public.
+ bool sendDeprecatedNotification = !(aRemoved && aRemoved->mPrivate);
+
+ if (sendDeprecatedNotification && aRemoved) {
+ id = do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t dlID;
+ rv = aRemoved->GetId(&dlID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = id->SetData(dlID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (sendDeprecatedNotification) {
+ mObserverService->NotifyObservers(id,
+ "download-manager-remove-download",
+ nullptr);
+ }
+
+ if (aRemoved) {
+ guid = do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString guidStr;
+ rv = aRemoved->GetGuid(guidStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = guid->SetData(guidStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mObserverService->NotifyObservers(guid,
+ "download-manager-remove-download-guid",
+ nullptr);
+ return NS_OK;
+}
+
+static nsresult
+DoRemoveDownloadsByTimeframe(mozIStorageConnection* aDBConn,
+ int64_t aStartTime,
+ int64_t aEndTime)
+{
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_downloads "
+ "WHERE startTime >= :startTime "
+ "AND startTime <= :endTime "
+ "AND state NOT IN (:downloading, :paused, :queued)"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Bind the times
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), aStartTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), aEndTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Bind the active states
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("downloading"), nsIDownloadManager::DOWNLOAD_DOWNLOADING);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("paused"), nsIDownloadManager::DOWNLOAD_PAUSED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("queued"), nsIDownloadManager::DOWNLOAD_QUEUED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Execute
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::RemoveDownloadsByTimeframe(int64_t aStartTime,
+ int64_t aEndTime)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ nsresult rv = DoRemoveDownloadsByTimeframe(mDBConn, aStartTime, aEndTime);
+ nsresult rv2 = DoRemoveDownloadsByTimeframe(mPrivateDBConn, aStartTime, aEndTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+
+ // Notify the UI with the topic and null subject to indicate "remove multiple"
+ return NotifyDownloadRemoval(nullptr);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::CleanUp()
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ return CleanUp(mDBConn);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::CleanUpPrivate()
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ return CleanUp(mPrivateDBConn);
+}
+
+nsresult
+nsDownloadManager::CleanUp(mozIStorageConnection* aDBConn)
+{
+ DownloadState states[] = { nsIDownloadManager::DOWNLOAD_FINISHED,
+ nsIDownloadManager::DOWNLOAD_FAILED,
+ nsIDownloadManager::DOWNLOAD_CANCELED,
+ nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL,
+ nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY,
+ nsIDownloadManager::DOWNLOAD_DIRTY };
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_downloads "
+ "WHERE state = ? "
+ "OR state = ? "
+ "OR state = ? "
+ "OR state = ? "
+ "OR state = ? "
+ "OR state = ?"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < ArrayLength(states); ++i) {
+ rv = stmt->BindInt32ByIndex(i, states[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Notify the UI with the topic and null subject to indicate "remove multiple"
+ return NotifyDownloadRemoval(nullptr);
+}
+
+static nsresult
+DoGetCanCleanUp(mozIStorageConnection* aDBConn, bool *aResult)
+{
+ // This method should never return anything but NS_OK for the benefit of
+ // unwitting consumers.
+
+ *aResult = false;
+
+ DownloadState states[] = { nsIDownloadManager::DOWNLOAD_FINISHED,
+ nsIDownloadManager::DOWNLOAD_FAILED,
+ nsIDownloadManager::DOWNLOAD_CANCELED,
+ nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL,
+ nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY,
+ nsIDownloadManager::DOWNLOAD_DIRTY };
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT COUNT(*) "
+ "FROM moz_downloads "
+ "WHERE state = ? "
+ "OR state = ? "
+ "OR state = ? "
+ "OR state = ? "
+ "OR state = ? "
+ "OR state = ?"), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ for (uint32_t i = 0; i < ArrayLength(states); ++i) {
+ rv = stmt->BindInt32ByIndex(i, states[i]);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ }
+
+ bool moreResults; // We don't really care...
+ rv = stmt->ExecuteStep(&moreResults);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ int32_t count;
+ rv = stmt->GetInt32(0, &count);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (count > 0)
+ *aResult = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetCanCleanUp(bool *aResult)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ return DoGetCanCleanUp(mDBConn, aResult);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetCanCleanUpPrivate(bool *aResult)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ return DoGetCanCleanUp(mPrivateDBConn, aResult);
+}
+
+NS_IMETHODIMP
+nsDownloadManager::PauseDownload(uint32_t aID)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ nsDownload *dl = FindDownload(aID);
+ if (!dl)
+ return NS_ERROR_FAILURE;
+
+ return dl->Pause();
+}
+
+NS_IMETHODIMP
+nsDownloadManager::ResumeDownload(uint32_t aID)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_WARNING("Using integer IDs without compat mode enabled");
+
+ nsDownload *dl = FindDownload(aID);
+ if (!dl)
+ return NS_ERROR_FAILURE;
+
+ return dl->Resume();
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetDBConnection(mozIStorageConnection **aDBConn)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_ADDREF(*aDBConn = mDBConn);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::GetPrivateDBConnection(mozIStorageConnection **aDBConn)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ NS_ADDREF(*aDBConn = mPrivateDBConn);
+
+ return NS_OK;
+ }
+
+NS_IMETHODIMP
+nsDownloadManager::AddListener(nsIDownloadProgressListener *aListener)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ mListeners.AppendObject(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::AddPrivacyAwareListener(nsIDownloadProgressListener *aListener)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ mPrivacyAwareListeners.AppendObject(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::RemoveListener(nsIDownloadProgressListener *aListener)
+{
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ mListeners.RemoveObject(aListener);
+ mPrivacyAwareListeners.RemoveObject(aListener);
+ return NS_OK;
+}
+
+void
+nsDownloadManager::NotifyListenersOnDownloadStateChange(int16_t aOldState,
+ nsDownload *aDownload)
+{
+ for (int32_t i = mPrivacyAwareListeners.Count() - 1; i >= 0; --i) {
+ mPrivacyAwareListeners[i]->OnDownloadStateChange(aOldState, aDownload);
+ }
+
+ // Only privacy-aware listeners should receive notifications about private
+ // downloads, while non-privacy-aware listeners receive no sign they exist.
+ if (aDownload->mPrivate) {
+ return;
+ }
+
+ for (int32_t i = mListeners.Count() - 1; i >= 0; --i) {
+ mListeners[i]->OnDownloadStateChange(aOldState, aDownload);
+ }
+}
+
+void
+nsDownloadManager::NotifyListenersOnProgressChange(nsIWebProgress *aProgress,
+ nsIRequest *aRequest,
+ int64_t aCurSelfProgress,
+ int64_t aMaxSelfProgress,
+ int64_t aCurTotalProgress,
+ int64_t aMaxTotalProgress,
+ nsDownload *aDownload)
+{
+ for (int32_t i = mPrivacyAwareListeners.Count() - 1; i >= 0; --i) {
+ mPrivacyAwareListeners[i]->OnProgressChange(aProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress, aDownload);
+ }
+
+ // Only privacy-aware listeners should receive notifications about private
+ // downloads, while non-privacy-aware listeners receive no sign they exist.
+ if (aDownload->mPrivate) {
+ return;
+ }
+
+ for (int32_t i = mListeners.Count() - 1; i >= 0; --i) {
+ mListeners[i]->OnProgressChange(aProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress, aDownload);
+ }
+}
+
+void
+nsDownloadManager::NotifyListenersOnStateChange(nsIWebProgress *aProgress,
+ nsIRequest *aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus,
+ nsDownload *aDownload)
+{
+ for (int32_t i = mPrivacyAwareListeners.Count() - 1; i >= 0; --i) {
+ mPrivacyAwareListeners[i]->OnStateChange(aProgress, aRequest, aStateFlags, aStatus,
+ aDownload);
+ }
+
+ // Only privacy-aware listeners should receive notifications about private
+ // downloads, while non-privacy-aware listeners receive no sign they exist.
+ if (aDownload->mPrivate) {
+ return;
+ }
+
+ for (int32_t i = mListeners.Count() - 1; i >= 0; --i) {
+ mListeners[i]->OnStateChange(aProgress, aRequest, aStateFlags, aStatus,
+ aDownload);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsINavHistoryObserver
+
+NS_IMETHODIMP
+nsDownloadManager::OnBeginUpdateBatch()
+{
+ // This method in not normally invoked when mUseJSTransfer is enabled, however
+ // we provide an extra check in case it is called manually by add-ons.
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ // We already have a transaction, so don't make another
+ if (mHistoryTransaction)
+ return NS_OK;
+
+ // Start a transaction that commits when deleted
+ mHistoryTransaction = new mozStorageTransaction(mDBConn, true);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnEndUpdateBatch()
+{
+ // Get rid of the transaction and cause it to commit
+ mHistoryTransaction = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnVisit(nsIURI *aURI, int64_t aVisitID, PRTime aTime,
+ int64_t aSessionID, int64_t aReferringID,
+ uint32_t aTransitionType, const nsACString& aGUID,
+ bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnTitleChanged(nsIURI *aURI,
+ const nsAString &aPageTitle,
+ const nsACString &aGUID)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnFrecencyChanged(nsIURI* aURI,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnManyFrecenciesChanged()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnDeleteURI(nsIURI *aURI,
+ const nsACString& aGUID,
+ uint16_t aReason)
+{
+ // This method in not normally invoked when mUseJSTransfer is enabled, however
+ // we provide an extra check in case it is called manually by add-ons.
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ nsresult rv = RemoveDownloadsForURI(mGetIdsForURIStatement, aURI);
+ nsresult rv2 = RemoveDownloadsForURI(mGetPrivateIdsForURIStatement, aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnClearHistory()
+{
+ return CleanUp();
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnPageChanged(nsIURI *aURI,
+ uint32_t aChangedAttribute,
+ const nsAString& aNewValue,
+ const nsACString &aGUID)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownloadManager::OnDeleteVisits(nsIURI *aURI, PRTime aVisitTime,
+ const nsACString& aGUID,
+ uint16_t aReason, uint32_t aTransitionType)
+{
+ // Don't bother removing downloads until the page is removed.
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+nsDownloadManager::Observe(nsISupports *aSubject,
+ const char *aTopic,
+ const char16_t *aData)
+{
+ // This method in not normally invoked when mUseJSTransfer is enabled, however
+ // we provide an extra check in case it is called manually by add-ons.
+ NS_ENSURE_STATE(!mUseJSTransfer);
+
+ // We need to count the active public downloads that could be lost
+ // by quitting, and add any active private ones as well, since per-window
+ // private browsing may be active.
+ int32_t currDownloadCount = mCurrentDownloads.Count();
+
+ // If we don't need to cancel all the downloads on quit, only count the ones
+ // that aren't resumable.
+ if (GetQuitBehavior() != QUIT_AND_CANCEL) {
+ for (int32_t i = currDownloadCount - 1; i >= 0; --i) {
+ if (mCurrentDownloads[i]->IsResumable()) {
+ currDownloadCount--;
+ }
+ }
+
+ // We have a count of the public, non-resumable downloads. Now we need
+ // to add the total number of private downloads, since they are in danger
+ // of being lost.
+ currDownloadCount += mCurrentPrivateDownloads.Count();
+ }
+
+ nsresult rv;
+ if (strcmp(aTopic, "oncancel") == 0) {
+ nsCOMPtr<nsIDownload> dl = do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dl->Cancel();
+ } else if (strcmp(aTopic, "profile-before-change") == 0) {
+ CloseAllDBs();
+ } else if (strcmp(aTopic, "quit-application") == 0) {
+ // Try to pause all downloads and, if appropriate, mark them as auto-resume
+ // unless user has specified that downloads should be canceled
+ enum QuitBehavior behavior = GetQuitBehavior();
+ if (behavior != QUIT_AND_CANCEL)
+ (void)PauseAllDownloads(bool(behavior != QUIT_AND_PAUSE));
+
+ // Remove downloads to break cycles and cancel downloads
+ (void)RemoveAllDownloads();
+
+ // Now that active downloads have been canceled, remove all completed or
+ // aborted downloads if the user's retention policy specifies it.
+ if (GetRetentionBehavior() == 1)
+ CleanUp();
+ } else if (strcmp(aTopic, "quit-application-requested") == 0 &&
+ currDownloadCount) {
+ nsCOMPtr<nsISupportsPRBool> cancelDownloads =
+ do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+#ifndef XP_MACOSX
+ ConfirmCancelDownloads(currDownloadCount, cancelDownloads,
+ u"quitCancelDownloadsAlertTitle",
+ u"quitCancelDownloadsAlertMsgMultiple",
+ u"quitCancelDownloadsAlertMsg",
+ u"dontQuitButtonWin");
+#else
+ ConfirmCancelDownloads(currDownloadCount, cancelDownloads,
+ u"quitCancelDownloadsAlertTitle",
+ u"quitCancelDownloadsAlertMsgMacMultiple",
+ u"quitCancelDownloadsAlertMsgMac",
+ u"dontQuitButtonMac");
+#endif
+ } else if (strcmp(aTopic, "offline-requested") == 0 && currDownloadCount) {
+ nsCOMPtr<nsISupportsPRBool> cancelDownloads =
+ do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ConfirmCancelDownloads(currDownloadCount, cancelDownloads,
+ u"offlineCancelDownloadsAlertTitle",
+ u"offlineCancelDownloadsAlertMsgMultiple",
+ u"offlineCancelDownloadsAlertMsg",
+ u"dontGoOfflineButton");
+ }
+ else if (strcmp(aTopic, NS_IOSERVICE_GOING_OFFLINE_TOPIC) == 0) {
+ // Pause all downloads, and mark them to auto-resume.
+ (void)PauseAllDownloads(true);
+ }
+ else if (strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC) == 0 &&
+ nsDependentString(aData).EqualsLiteral(NS_IOSERVICE_ONLINE)) {
+ // We can now resume all downloads that are supposed to auto-resume.
+ (void)ResumeAllDownloads(false);
+ }
+ else if (strcmp(aTopic, "alertclickcallback") == 0) {
+ nsCOMPtr<nsIDownloadManagerUI> dmui =
+ do_GetService("@mozilla.org/download-manager-ui;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return dmui->Show(nullptr, nullptr, nsIDownloadManagerUI::REASON_USER_INTERACTED,
+ aData && NS_strcmp(aData, u"private") == 0);
+ } else if (strcmp(aTopic, "sleep_notification") == 0 ||
+ strcmp(aTopic, "suspend_process_notification") == 0) {
+ // Pause downloads if we're sleeping, and mark the downloads as auto-resume
+ (void)PauseAllDownloads(true);
+ } else if (strcmp(aTopic, "wake_notification") == 0 ||
+ strcmp(aTopic, "resume_process_notification") == 0) {
+ int32_t resumeOnWakeDelay = 10000;
+ nsCOMPtr<nsIPrefBranch> pref = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (pref)
+ (void)pref->GetIntPref(PREF_BDM_RESUMEONWAKEDELAY, &resumeOnWakeDelay);
+
+ // Wait a little bit before trying to resume to avoid resuming when network
+ // connections haven't restarted yet
+ mResumeOnWakeTimer = do_CreateInstance("@mozilla.org/timer;1");
+ if (resumeOnWakeDelay >= 0 && mResumeOnWakeTimer) {
+ (void)mResumeOnWakeTimer->InitWithFuncCallback(ResumeOnWakeCallback,
+ this, resumeOnWakeDelay, nsITimer::TYPE_ONE_SHOT);
+ }
+ } else if (strcmp(aTopic, "last-pb-context-exited") == 0) {
+ // Upon leaving private browsing mode, cancel all private downloads,
+ // remove all trace of them, and then blow away the private database
+ // and recreate a blank one.
+ RemoveAllDownloads(mCurrentPrivateDownloads);
+ InitPrivateDB();
+ } else if (strcmp(aTopic, "last-pb-context-exiting") == 0) {
+ // If there are active private downloads, prompt the user to confirm leaving
+ // private browsing mode (thereby cancelling them). Otherwise, silently proceed.
+ if (!mCurrentPrivateDownloads.Count())
+ return NS_OK;
+
+ nsCOMPtr<nsISupportsPRBool> cancelDownloads = do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ConfirmCancelDownloads(mCurrentPrivateDownloads.Count(), cancelDownloads,
+ u"leavePrivateBrowsingCancelDownloadsAlertTitle",
+ u"leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2",
+ u"leavePrivateBrowsingWindowsCancelDownloadsAlertMsg2",
+ u"dontLeavePrivateBrowsingButton2");
+ }
+
+ return NS_OK;
+}
+
+void
+nsDownloadManager::ConfirmCancelDownloads(int32_t aCount,
+ nsISupportsPRBool *aCancelDownloads,
+ const char16_t *aTitle,
+ const char16_t *aCancelMessageMultiple,
+ const char16_t *aCancelMessageSingle,
+ const char16_t *aDontCancelButton)
+{
+ // If user has already dismissed quit request, then do nothing
+ bool quitRequestCancelled = false;
+ aCancelDownloads->GetData(&quitRequestCancelled);
+ if (quitRequestCancelled)
+ return;
+
+ nsXPIDLString title, message, quitButton, dontQuitButton;
+
+ mBundle->GetStringFromName(aTitle, getter_Copies(title));
+
+ nsAutoString countString;
+ countString.AppendInt(aCount);
+ const char16_t *strings[1] = { countString.get() };
+ if (aCount > 1) {
+ mBundle->FormatStringFromName(aCancelMessageMultiple, strings, 1,
+ getter_Copies(message));
+ mBundle->FormatStringFromName(u"cancelDownloadsOKTextMultiple",
+ strings, 1, getter_Copies(quitButton));
+ } else {
+ mBundle->GetStringFromName(aCancelMessageSingle, getter_Copies(message));
+ mBundle->GetStringFromName(u"cancelDownloadsOKText",
+ getter_Copies(quitButton));
+ }
+
+ mBundle->GetStringFromName(aDontCancelButton, getter_Copies(dontQuitButton));
+
+ // Get Download Manager window, to be parent of alert.
+ nsCOMPtr<nsIWindowMediator> wm = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID);
+ nsCOMPtr<mozIDOMWindowProxy> dmWindow;
+ if (wm) {
+ wm->GetMostRecentWindow(u"Download:Manager",
+ getter_AddRefs(dmWindow));
+ }
+
+ // Show alert.
+ nsCOMPtr<nsIPromptService> prompter(do_GetService(NS_PROMPTSERVICE_CONTRACTID));
+ if (prompter) {
+ int32_t flags = (nsIPromptService::BUTTON_TITLE_IS_STRING * nsIPromptService::BUTTON_POS_0) + (nsIPromptService::BUTTON_TITLE_IS_STRING * nsIPromptService::BUTTON_POS_1);
+ bool nothing = false;
+ int32_t button;
+ prompter->ConfirmEx(dmWindow, title, message, flags, quitButton.get(), dontQuitButton.get(), nullptr, nullptr, &nothing, &button);
+
+ aCancelDownloads->SetData(button == 1);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsDownload
+
+NS_IMPL_CLASSINFO(nsDownload, nullptr, 0, NS_DOWNLOAD_CID)
+NS_IMPL_ISUPPORTS_CI(
+ nsDownload
+ , nsIDownload
+ , nsITransfer
+ , nsIWebProgressListener
+ , nsIWebProgressListener2
+)
+
+nsDownload::nsDownload() : mDownloadState(nsIDownloadManager::DOWNLOAD_NOTSTARTED),
+ mID(0),
+ mPercentComplete(0),
+ mCurrBytes(0),
+ mMaxBytes(-1),
+ mStartTime(0),
+ mLastUpdate(PR_Now() - (uint32_t)gUpdateInterval),
+ mResumedAt(-1),
+ mSpeed(0),
+ mHasMultipleFiles(false),
+ mPrivate(false),
+ mAutoResume(DONT_RESUME)
+{
+}
+
+nsDownload::~nsDownload()
+{
+}
+
+NS_IMETHODIMP nsDownload::SetSha256Hash(const nsACString& aHash) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must call SetSha256Hash on main thread");
+ // This will be used later to query the application reputation service.
+ mHash = aHash;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDownload::SetSignatureInfo(nsIArray* aSignatureInfo) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must call SetSignatureInfo on main thread");
+ // This will be used later to query the application reputation service.
+ mSignatureInfo = aSignatureInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDownload::SetRedirects(nsIArray* aRedirects) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must call SetRedirects on main thread");
+ // This will be used later to query the application reputation service.
+ mRedirects = aRedirects;
+ return NS_OK;
+}
+
+#ifdef MOZ_ENABLE_GIO
+static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data)
+{
+ GError *err = nullptr;
+ g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err);
+ if (err) {
+#ifdef DEBUG
+ NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__);
+#endif
+ g_error_free(err);
+ }
+}
+#endif
+
+nsresult
+nsDownload::SetState(DownloadState aState)
+{
+ NS_ASSERTION(mDownloadState != aState,
+ "Trying to set the download state to what it already is set to!");
+
+ int16_t oldState = mDownloadState;
+ mDownloadState = aState;
+
+ // We don't want to lose access to our member variables
+ RefPtr<nsDownload> kungFuDeathGrip = this;
+
+ // When the state changed listener is dispatched, queries to the database and
+ // the download manager api should reflect what the nsIDownload object would
+ // return. So, if a download is done (finished, canceled, etc.), it should
+ // first be removed from the current downloads. We will also have to update
+ // the database *before* notifying listeners. At this point, you can safely
+ // dispatch to the observers as well.
+ switch (aState) {
+ case nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY:
+ case nsIDownloadManager::DOWNLOAD_DIRTY:
+ case nsIDownloadManager::DOWNLOAD_CANCELED:
+ case nsIDownloadManager::DOWNLOAD_FAILED:
+#ifdef ANDROID
+ // If we still have a temp file, remove it
+ bool tempExists;
+ if (mTempFile && NS_SUCCEEDED(mTempFile->Exists(&tempExists)) && tempExists) {
+ nsresult rv = mTempFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+#endif
+
+ // Transfers are finished, so break the reference cycle
+ Finalize();
+ break;
+#ifdef DOWNLOAD_SCANNER
+ case nsIDownloadManager::DOWNLOAD_SCANNING:
+ {
+ nsresult rv = mDownloadManager->mScanner ? mDownloadManager->mScanner->ScanDownload(this) : NS_ERROR_NOT_INITIALIZED;
+ // If we failed, then fall through to 'download finished'
+ if (NS_SUCCEEDED(rv))
+ break;
+ mDownloadState = aState = nsIDownloadManager::DOWNLOAD_FINISHED;
+ }
+#endif
+ case nsIDownloadManager::DOWNLOAD_FINISHED:
+ {
+ nsresult rv = ExecuteDesiredAction();
+ if (NS_FAILED(rv)) {
+ // We've failed to execute the desired action. As a result, we should
+ // fail the download so the user can try again.
+ (void)FailDownload(rv, nullptr);
+ return rv;
+ }
+
+ // Now that we're done with handling the download, clean it up
+ Finalize();
+
+ nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ // Master pref to control this function.
+ bool showTaskbarAlert = true;
+ if (pref)
+ pref->GetBoolPref(PREF_BDM_SHOWALERTONCOMPLETE, &showTaskbarAlert);
+
+ if (showTaskbarAlert) {
+ int32_t alertInterval = 2000;
+ if (pref)
+ pref->GetIntPref(PREF_BDM_SHOWALERTINTERVAL, &alertInterval);
+
+ int64_t alertIntervalUSec = alertInterval * PR_USEC_PER_MSEC;
+ int64_t goat = PR_Now() - mStartTime;
+ showTaskbarAlert = goat > alertIntervalUSec;
+
+ int32_t size = mPrivate ?
+ mDownloadManager->mCurrentPrivateDownloads.Count() :
+ mDownloadManager->mCurrentDownloads.Count();
+ if (showTaskbarAlert && size == 0) {
+ nsCOMPtr<nsIAlertsService> alerts =
+ do_GetService("@mozilla.org/alerts-service;1");
+ if (alerts) {
+ nsXPIDLString title, message;
+
+ mDownloadManager->mBundle->GetStringFromName(
+ u"downloadsCompleteTitle",
+ getter_Copies(title));
+ mDownloadManager->mBundle->GetStringFromName(
+ u"downloadsCompleteMsg",
+ getter_Copies(message));
+
+ bool removeWhenDone =
+ mDownloadManager->GetRetentionBehavior() == 0;
+
+ // If downloads are automatically removed per the user's
+ // retention policy, there's no reason to make the text clickable
+ // because if it is, they'll click open the download manager and
+ // the items they downloaded will have been removed.
+ alerts->ShowAlertNotification(
+ NS_LITERAL_STRING(DOWNLOAD_MANAGER_ALERT_ICON), title,
+ message, !removeWhenDone,
+ mPrivate ? NS_LITERAL_STRING("private") : NS_LITERAL_STRING("non-private"),
+ mDownloadManager, EmptyString(), NS_LITERAL_STRING("auto"),
+ EmptyString(), EmptyString(), nullptr, mPrivate,
+ false /* requireInteraction */);
+ }
+ }
+ }
+
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GTK)
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mTarget);
+ nsCOMPtr<nsIFile> file;
+ nsAutoString path;
+
+ if (fileURL &&
+ NS_SUCCEEDED(fileURL->GetFile(getter_AddRefs(file))) &&
+ file &&
+ NS_SUCCEEDED(file->GetPath(path))) {
+
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID)
+ // On Windows and Gtk, add the download to the system's "recent documents"
+ // list, with a pref to disable.
+ {
+ bool addToRecentDocs = true;
+ if (pref)
+ pref->GetBoolPref(PREF_BDM_ADDTORECENTDOCS, &addToRecentDocs);
+#ifdef MOZ_WIDGET_ANDROID
+ if (addToRecentDocs) {
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+ nsAutoCString contentType;
+ GetMIMEInfo(getter_AddRefs(mimeInfo));
+
+ if (mimeInfo)
+ mimeInfo->GetMIMEType(contentType);
+
+ if (jni::IsFennec()) {
+ java::DownloadsIntegration::ScanMedia(path, NS_ConvertUTF8toUTF16(contentType));
+ }
+ }
+#else
+ if (addToRecentDocs && !mPrivate) {
+#ifdef XP_WIN
+ ::SHAddToRecentDocs(SHARD_PATHW, path.get());
+#elif defined(MOZ_WIDGET_GTK)
+ GtkRecentManager* manager = gtk_recent_manager_get_default();
+
+ gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(),
+ nullptr, nullptr);
+ if (uri) {
+ gtk_recent_manager_add_item(manager, uri);
+ g_free(uri);
+ }
+#endif
+ }
+#endif
+#ifdef MOZ_ENABLE_GIO
+ // Use GIO to store the source URI for later display in the file manager.
+ GFile* gio_file = g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get());
+ nsCString source_uri;
+ rv = mSource->GetSpec(source_uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ GFileInfo *file_info = g_file_info_new();
+ g_file_info_set_attribute_string(file_info, "metadata::download-uri", source_uri.get());
+ g_file_set_attributes_async(gio_file,
+ file_info,
+ G_FILE_QUERY_INFO_NONE,
+ G_PRIORITY_DEFAULT,
+ nullptr, gio_set_metadata_done, nullptr);
+ g_object_unref(file_info);
+ g_object_unref(gio_file);
+#endif
+ }
+#endif
+
+#ifdef XP_MACOSX
+ // On OS X, make the downloads stack bounce.
+ CFStringRef observedObject = ::CFStringCreateWithCString(kCFAllocatorDefault,
+ NS_ConvertUTF16toUTF8(path).get(),
+ kCFStringEncodingUTF8);
+ CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
+ ::CFNotificationCenterPostNotification(center, CFSTR("com.apple.DownloadFileFinished"),
+ observedObject, nullptr, TRUE);
+ ::CFRelease(observedObject);
+#endif
+ }
+
+#ifdef XP_WIN
+ // Adjust file attributes so that by default, new files are indexed
+ // by desktop search services. Skip off those that land in the temp
+ // folder.
+ nsCOMPtr<nsIFile> tempDir, fileDir;
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tempDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ (void)file->GetParent(getter_AddRefs(fileDir));
+
+ bool isTemp = false;
+ if (fileDir)
+ (void)fileDir->Equals(tempDir, &isTemp);
+
+ nsCOMPtr<nsILocalFileWin> localFileWin(do_QueryInterface(file));
+ if (!isTemp && localFileWin)
+ (void)localFileWin->SetFileAttributesWin(nsILocalFileWin::WFA_SEARCH_INDEXED);
+#endif
+
+#endif
+ // Now remove the download if the user's retention policy is "Remove when Done"
+ if (mDownloadManager->GetRetentionBehavior() == 0)
+ mDownloadManager->RemoveDownload(mGUID);
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Before notifying the listener, we must update the database so that calls
+ // to it work out properly.
+ nsresult rv = UpdateDB();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mDownloadManager->NotifyListenersOnDownloadStateChange(oldState, this);
+
+ switch (mDownloadState) {
+ case nsIDownloadManager::DOWNLOAD_DOWNLOADING:
+ // Only send the dl-start event to downloads that are actually starting.
+ if (oldState == nsIDownloadManager::DOWNLOAD_QUEUED) {
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-start");
+ }
+ break;
+ case nsIDownloadManager::DOWNLOAD_FAILED:
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-failed");
+ break;
+ case nsIDownloadManager::DOWNLOAD_SCANNING:
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-scanning");
+ break;
+ case nsIDownloadManager::DOWNLOAD_FINISHED:
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-done");
+ break;
+ case nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY:
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-blocked");
+ break;
+ case nsIDownloadManager::DOWNLOAD_DIRTY:
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-dirty");
+ break;
+ case nsIDownloadManager::DOWNLOAD_CANCELED:
+ if (!mPrivate)
+ mDownloadManager->SendEvent(this, "dl-cancel");
+ break;
+ default:
+ break;
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIWebProgressListener2
+
+NS_IMETHODIMP
+nsDownload::OnProgressChange64(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int64_t aCurSelfProgress,
+ int64_t aMaxSelfProgress,
+ int64_t aCurTotalProgress,
+ int64_t aMaxTotalProgress)
+{
+ if (!mRequest)
+ mRequest = aRequest; // used for pause/resume
+
+ if (mDownloadState == nsIDownloadManager::DOWNLOAD_QUEUED) {
+ // Obtain the referrer
+ nsresult rv;
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+ nsCOMPtr<nsIURI> referrer = mReferrer;
+ if (channel)
+ (void)NS_GetReferrerFromChannel(channel, getter_AddRefs(mReferrer));
+
+ // Restore the original referrer if the new one isn't useful
+ if (!mReferrer)
+ mReferrer = referrer;
+
+ // If we have a MIME info, we know that exthandler has already added this to
+ // the history, but if we do not, we'll have to add it ourselves.
+ if (!mMIMEInfo && !mPrivate) {
+ nsCOMPtr<nsIDownloadHistory> dh =
+ do_GetService(NS_DOWNLOADHISTORY_CONTRACTID);
+ if (dh)
+ (void)dh->AddDownload(mSource, mReferrer, mStartTime, mTarget);
+ }
+
+ // Fetch the entityID, but if we can't get it, don't panic (non-resumable)
+ nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(aRequest));
+ if (resumableChannel)
+ (void)resumableChannel->GetEntityID(mEntityID);
+
+ // Before we update the state and dispatch state notifications, we want to
+ // ensure that we have the correct state for this download with regards to
+ // its percent completion and size.
+ SetProgressBytes(0, aMaxTotalProgress);
+
+ // Update the state and the database
+ rv = SetState(nsIDownloadManager::DOWNLOAD_DOWNLOADING);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // filter notifications since they come in so frequently
+ PRTime now = PR_Now();
+ PRIntervalTime delta = now - mLastUpdate;
+ if (delta < gUpdateInterval)
+ return NS_OK;
+
+ mLastUpdate = now;
+
+ // Calculate the speed using the elapsed delta time and bytes downloaded
+ // during that time for more accuracy.
+ double elapsedSecs = double(delta) / PR_USEC_PER_SEC;
+ if (elapsedSecs > 0) {
+ double speed = double(aCurTotalProgress - mCurrBytes) / elapsedSecs;
+ if (mCurrBytes == 0) {
+ mSpeed = speed;
+ } else {
+ // Calculate 'smoothed average' of 10 readings.
+ mSpeed = mSpeed * 0.9 + speed * 0.1;
+ }
+ }
+
+ SetProgressBytes(aCurTotalProgress, aMaxTotalProgress);
+
+ // Report to the listener our real sizes
+ int64_t currBytes, maxBytes;
+ (void)GetAmountTransferred(&currBytes);
+ (void)GetSize(&maxBytes);
+ mDownloadManager->NotifyListenersOnProgressChange(
+ aWebProgress, aRequest, currBytes, maxBytes, currBytes, maxBytes, this);
+
+ // If the maximums are different, then there must be more than one file
+ if (aMaxSelfProgress != aMaxTotalProgress)
+ mHasMultipleFiles = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::OnRefreshAttempted(nsIWebProgress *aWebProgress,
+ nsIURI *aUri,
+ int32_t aDelay,
+ bool aSameUri,
+ bool *allowRefresh)
+{
+ *allowRefresh = true;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIWebProgressListener
+
+NS_IMETHODIMP
+nsDownload::OnProgressChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress)
+{
+ return OnProgressChange64(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress);
+}
+
+NS_IMETHODIMP
+nsDownload::OnLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, nsIURI *aLocation,
+ uint32_t aFlags)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::OnStatusChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, nsresult aStatus,
+ const char16_t *aMessage)
+{
+ if (NS_FAILED(aStatus))
+ return FailDownload(aStatus, aMessage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::OnStateChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, uint32_t aStateFlags,
+ nsresult aStatus)
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Must call OnStateChange in main thread");
+
+ // We don't want to lose access to our member variables
+ RefPtr<nsDownload> kungFuDeathGrip = this;
+
+ // Check if we're starting a request; the NETWORK flag is necessary to not
+ // pick up the START of *each* file but only for the whole request
+ if ((aStateFlags & STATE_START) && (aStateFlags & STATE_IS_NETWORK)) {
+ nsresult rv;
+ nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t status;
+ rv = channel->GetResponseStatus(&status);
+ // HTTP 450 - Blocked by parental control proxies
+ if (NS_SUCCEEDED(rv) && status == 450) {
+ // Cancel using the provided object
+ (void)Cancel();
+
+ // Fail the download
+ (void)SetState(nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL);
+ }
+ }
+ } else if ((aStateFlags & STATE_STOP) && (aStateFlags & STATE_IS_NETWORK) &&
+ IsFinishable()) {
+ // We got both STOP and NETWORK so that means the whole request is done
+ // (and not just a single file if there are multiple files)
+ if (NS_SUCCEEDED(aStatus)) {
+ // We can't completely trust the bytes we've added up because we might be
+ // missing on some/all of the progress updates (especially from cache).
+ // Our best bet is the file itself, but if for some reason it's gone or
+ // if we have multiple files, the next best is what we've calculated.
+ int64_t fileSize;
+ nsCOMPtr<nsIFile> file;
+ // We need a nsIFile clone to deal with file size caching issues. :(
+ nsCOMPtr<nsIFile> clone;
+ if (!mHasMultipleFiles &&
+ NS_SUCCEEDED(GetTargetFile(getter_AddRefs(file))) &&
+ NS_SUCCEEDED(file->Clone(getter_AddRefs(clone))) &&
+ NS_SUCCEEDED(clone->GetFileSize(&fileSize)) && fileSize > 0) {
+ mCurrBytes = mMaxBytes = fileSize;
+
+ // If we resumed, keep the fact that we did and fix size calculations
+ if (WasResumed())
+ mResumedAt = 0;
+ } else if (mMaxBytes == -1) {
+ mMaxBytes = mCurrBytes;
+ } else {
+ mCurrBytes = mMaxBytes;
+ }
+
+ mPercentComplete = 100;
+ mLastUpdate = PR_Now();
+
+#ifdef DOWNLOAD_SCANNER
+ bool scan = true;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs)
+ (void)prefs->GetBoolPref(PREF_BDM_SCANWHENDONE, &scan);
+
+ if (scan)
+ (void)SetState(nsIDownloadManager::DOWNLOAD_SCANNING);
+ else
+ (void)SetState(nsIDownloadManager::DOWNLOAD_FINISHED);
+#else
+ (void)SetState(nsIDownloadManager::DOWNLOAD_FINISHED);
+#endif
+ } else {
+ // We failed for some unknown reason -- fail with a generic message
+ (void)FailDownload(aStatus, nullptr);
+ }
+ }
+
+ mDownloadManager->NotifyListenersOnStateChange(aWebProgress, aRequest,
+ aStateFlags, aStatus, this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::OnSecurityChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, uint32_t aState)
+{
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIDownload
+
+NS_IMETHODIMP
+nsDownload::Init(nsIURI *aSource,
+ nsIURI *aTarget,
+ const nsAString& aDisplayName,
+ nsIMIMEInfo *aMIMEInfo,
+ PRTime aStartTime,
+ nsIFile *aTempFile,
+ nsICancelable *aCancelable,
+ bool aIsPrivate)
+{
+ NS_WARNING("Huh...how did we get here?!");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetState(int16_t *aState)
+{
+ *aState = mDownloadState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetDisplayName(nsAString &aDisplayName)
+{
+ aDisplayName = mDisplayName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetCancelable(nsICancelable **aCancelable)
+{
+ *aCancelable = mCancelable;
+ NS_IF_ADDREF(*aCancelable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetTarget(nsIURI **aTarget)
+{
+ *aTarget = mTarget;
+ NS_IF_ADDREF(*aTarget);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetSource(nsIURI **aSource)
+{
+ *aSource = mSource;
+ NS_IF_ADDREF(*aSource);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetStartTime(int64_t *aStartTime)
+{
+ *aStartTime = mStartTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetPercentComplete(int32_t *aPercentComplete)
+{
+ *aPercentComplete = mPercentComplete;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetAmountTransferred(int64_t *aAmountTransferred)
+{
+ *aAmountTransferred = mCurrBytes + (WasResumed() ? mResumedAt : 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetSize(int64_t *aSize)
+{
+ *aSize = mMaxBytes + (WasResumed() && mMaxBytes != -1 ? mResumedAt : 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetMIMEInfo(nsIMIMEInfo **aMIMEInfo)
+{
+ *aMIMEInfo = mMIMEInfo;
+ NS_IF_ADDREF(*aMIMEInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetTargetFile(nsIFile **aTargetFile)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(mTarget, &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ rv = fileURL->GetFile(getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ file.forget(aTargetFile);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDownload::GetSpeed(double *aSpeed)
+{
+ *aSpeed = mSpeed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetId(uint32_t *aId)
+{
+ if (mPrivate) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ *aId = mID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetGuid(nsACString &aGUID)
+{
+ aGUID = mGUID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetReferrer(nsIURI **referrer)
+{
+ NS_IF_ADDREF(*referrer = mReferrer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetResumable(bool *resumable)
+{
+ *resumable = IsResumable();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::GetIsPrivate(bool *isPrivate)
+{
+ *isPrivate = mPrivate;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsDownload Helper Functions
+
+void
+nsDownload::Finalize()
+{
+ // We're stopping, so break the cycle we created at download start
+ mCancelable = nullptr;
+
+ // Reset values that aren't needed anymore, so the DB can be updated as well
+ mEntityID.Truncate();
+ mTempFile = nullptr;
+
+ // Remove ourself from the active downloads
+ nsCOMArray<nsDownload>& currentDownloads = mPrivate ?
+ mDownloadManager->mCurrentPrivateDownloads :
+ mDownloadManager->mCurrentDownloads;
+ (void)currentDownloads.RemoveObject(this);
+
+ // Make sure we do not automatically resume
+ mAutoResume = DONT_RESUME;
+}
+
+nsresult
+nsDownload::ExecuteDesiredAction()
+{
+ // nsExternalHelperAppHandler is the only caller of AddDownload that sets a
+ // tempfile parameter. In this case, execute the desired action according to
+ // the saved mime info.
+ if (!mTempFile) {
+ return NS_OK;
+ }
+
+ // We need to bail if for some reason the temp file got removed
+ bool fileExists;
+ if (NS_FAILED(mTempFile->Exists(&fileExists)) || !fileExists)
+ return NS_ERROR_FILE_NOT_FOUND;
+
+ // Assume an unknown action is save to disk
+ nsHandlerInfoAction action = nsIMIMEInfo::saveToDisk;
+ if (mMIMEInfo) {
+ nsresult rv = mMIMEInfo->GetPreferredAction(&action);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsresult rv = NS_OK;
+ switch (action) {
+ case nsIMIMEInfo::saveToDisk:
+ // Move the file to the proper location
+ rv = MoveTempToTarget();
+ if (NS_SUCCEEDED(rv)) {
+ rv = FixTargetPermissions();
+ }
+ break;
+ case nsIMIMEInfo::useHelperApp:
+ case nsIMIMEInfo::useSystemDefault:
+ // For these cases we have to move the file to the target location and
+ // open with the appropriate application
+ rv = OpenWithApplication();
+ break;
+ default:
+ break;
+ }
+
+ return rv;
+}
+
+nsresult
+nsDownload::FixTargetPermissions()
+{
+ nsCOMPtr<nsIFile> target;
+ nsresult rv = GetTargetFile(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set perms according to umask.
+ nsCOMPtr<nsIPropertyBag2> infoService =
+ do_GetService("@mozilla.org/system-info;1");
+ uint32_t gUserUmask = 0;
+ rv = infoService->GetPropertyAsUint32(NS_LITERAL_STRING("umask"),
+ &gUserUmask);
+ if (NS_SUCCEEDED(rv)) {
+ (void)target->SetPermissions(0666 & ~gUserUmask);
+ }
+ return NS_OK;
+}
+
+nsresult
+nsDownload::MoveTempToTarget()
+{
+ nsCOMPtr<nsIFile> target;
+ nsresult rv = GetTargetFile(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // MoveTo will fail if the file already exists, but we've already obtained
+ // confirmation from the user that this is OK, so remove it if it exists.
+ bool fileExists;
+ if (NS_SUCCEEDED(target->Exists(&fileExists)) && fileExists) {
+ rv = target->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Extract the new leaf name from the file location
+ nsAutoString fileName;
+ rv = target->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> dir;
+ rv = target->GetParent(getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mTempFile->MoveTo(dir, fileName);
+ return rv;
+}
+
+nsresult
+nsDownload::OpenWithApplication()
+{
+ // First move the temporary file to the target location
+ nsCOMPtr<nsIFile> target;
+ nsresult rv = GetTargetFile(getter_AddRefs(target));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Move the temporary file to the target location
+ rv = MoveTempToTarget();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool deleteTempFileOnExit;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (!prefs || NS_FAILED(prefs->GetBoolPref(PREF_BH_DELETETEMPFILEONEXIT,
+ &deleteTempFileOnExit))) {
+ // No prefservice or no pref set; use default value
+#if !defined(XP_MACOSX)
+ // Mac users have been very verbal about temp files being deleted on
+ // app exit - they don't like it - but we'll continue to do this on
+ // other platforms for now.
+ deleteTempFileOnExit = true;
+#else
+ deleteTempFileOnExit = false;
+#endif
+ }
+
+ // Always schedule files to be deleted at the end of the private browsing
+ // mode, regardless of the value of the pref.
+ if (deleteTempFileOnExit || mPrivate) {
+
+ // Make the tmp file readonly so users won't lose changes.
+ target->SetPermissions(0400);
+
+ // Use the ExternalHelperAppService to push the temporary file to the list
+ // of files to be deleted on exit.
+ nsCOMPtr<nsPIExternalAppLauncher> appLauncher(do_GetService
+ (NS_EXTERNALHELPERAPPSERVICE_CONTRACTID));
+
+ // Even if we are unable to get this service we return the result
+ // of LaunchWithFile() which makes more sense.
+ if (appLauncher) {
+ if (mPrivate) {
+ (void)appLauncher->DeleteTemporaryPrivateFileWhenPossible(target);
+ } else {
+ (void)appLauncher->DeleteTemporaryFileOnExit(target);
+ }
+ }
+ }
+
+ return mMIMEInfo->LaunchWithFile(target);
+}
+
+void
+nsDownload::SetStartTime(int64_t aStartTime)
+{
+ mStartTime = aStartTime;
+ mLastUpdate = aStartTime;
+}
+
+void
+nsDownload::SetProgressBytes(int64_t aCurrBytes, int64_t aMaxBytes)
+{
+ mCurrBytes = aCurrBytes;
+ mMaxBytes = aMaxBytes;
+
+ // Get the real bytes that include resume position
+ int64_t currBytes, maxBytes;
+ (void)GetAmountTransferred(&currBytes);
+ (void)GetSize(&maxBytes);
+
+ if (currBytes == maxBytes)
+ mPercentComplete = 100;
+ else if (maxBytes <= 0)
+ mPercentComplete = -1;
+ else
+ mPercentComplete = (int32_t)((double)currBytes / maxBytes * 100 + .5);
+}
+
+NS_IMETHODIMP
+nsDownload::Pause()
+{
+ if (!IsResumable())
+ return NS_ERROR_UNEXPECTED;
+
+ nsresult rv = CancelTransfer();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return SetState(nsIDownloadManager::DOWNLOAD_PAUSED);
+}
+
+nsresult
+nsDownload::CancelTransfer()
+{
+ nsresult rv = NS_OK;
+ if (mCancelable) {
+ rv = mCancelable->Cancel(NS_BINDING_ABORTED);
+ // we're done with this, so break the cycle
+ mCancelable = nullptr;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDownload::Cancel()
+{
+ // Don't cancel if download is already finished
+ if (IsFinished())
+ return NS_OK;
+
+ // Have the download cancel its connection
+ (void)CancelTransfer();
+
+ // Dump the temp file because we know we don't need the file anymore. The
+ // underlying transfer creating the file doesn't delete the file because it
+ // can't distinguish between a pause that cancels the transfer or a real
+ // cancel.
+ if (mTempFile) {
+ bool exists;
+ mTempFile->Exists(&exists);
+ if (exists)
+ mTempFile->Remove(false);
+ }
+
+ nsCOMPtr<nsIFile> file;
+ if (NS_SUCCEEDED(GetTargetFile(getter_AddRefs(file))))
+ {
+ bool exists;
+ file->Exists(&exists);
+ if (exists)
+ file->Remove(false);
+ }
+
+ nsresult rv = SetState(nsIDownloadManager::DOWNLOAD_CANCELED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDownload::Resume()
+{
+ if (!IsPaused() || !IsResumable())
+ return NS_ERROR_UNEXPECTED;
+
+ nsresult rv;
+ nsCOMPtr<nsIWebBrowserPersist> wbp =
+ do_CreateInstance("@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = wbp->SetPersistFlags(nsIWebBrowserPersist::PERSIST_FLAGS_APPEND_TO_FILE |
+ nsIWebBrowserPersist::PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create a new channel for the source URI
+ nsCOMPtr<nsIChannel> channel;
+ nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(wbp));
+ rv = NS_NewChannel(getter_AddRefs(channel),
+ mSource,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER,
+ nullptr, // aLoadGroup
+ ir);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(channel);
+ if (pbChannel) {
+ pbChannel->SetPrivate(mPrivate);
+ }
+
+ // Make sure we can get a file, either the temporary or the real target, for
+ // both purposes of file size and a target to write to
+ nsCOMPtr<nsIFile> targetLocalFile(mTempFile);
+ if (!targetLocalFile) {
+ rv = GetTargetFile(getter_AddRefs(targetLocalFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Get the file size to be used as an offset, but if anything goes wrong
+ // along the way, we'll silently restart at 0.
+ int64_t fileSize;
+ // We need a nsIFile clone to deal with file size caching issues. :(
+ nsCOMPtr<nsIFile> clone;
+ if (NS_FAILED(targetLocalFile->Clone(getter_AddRefs(clone))) ||
+ NS_FAILED(clone->GetFileSize(&fileSize)))
+ fileSize = 0;
+
+ // Set the channel to resume at the right position along with the entityID
+ nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(channel));
+ if (!resumableChannel)
+ return NS_ERROR_UNEXPECTED;
+ rv = resumableChannel->ResumeAt(fileSize, mEntityID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we know the max size, we know what it should be when resuming
+ int64_t maxBytes;
+ GetSize(&maxBytes);
+ SetProgressBytes(0, maxBytes != -1 ? maxBytes - fileSize : -1);
+ // Track where we resumed because progress notifications restart at 0
+ mResumedAt = fileSize;
+
+ // Set the referrer
+ if (mReferrer) {
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
+ if (httpChannel) {
+ rv = httpChannel->SetReferrer(mReferrer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Creates a cycle that will be broken when the download finishes
+ mCancelable = wbp;
+ (void)wbp->SetProgressListener(this);
+
+ // Save the channel using nsIWBP
+ rv = wbp->SaveChannel(channel, targetLocalFile);
+ if (NS_FAILED(rv)) {
+ mCancelable = nullptr;
+ (void)wbp->SetProgressListener(nullptr);
+ return rv;
+ }
+
+ return SetState(nsIDownloadManager::DOWNLOAD_DOWNLOADING);
+}
+
+NS_IMETHODIMP
+nsDownload::Remove()
+{
+ return mDownloadManager->RemoveDownload(mGUID);
+}
+
+NS_IMETHODIMP
+nsDownload::Retry()
+{
+ return mDownloadManager->RetryDownload(mGUID);
+}
+
+bool
+nsDownload::IsPaused()
+{
+ return mDownloadState == nsIDownloadManager::DOWNLOAD_PAUSED;
+}
+
+bool
+nsDownload::IsResumable()
+{
+ return !mEntityID.IsEmpty();
+}
+
+bool
+nsDownload::WasResumed()
+{
+ return mResumedAt != -1;
+}
+
+bool
+nsDownload::ShouldAutoResume()
+{
+ return mAutoResume == AUTO_RESUME;
+}
+
+bool
+nsDownload::IsFinishable()
+{
+ return mDownloadState == nsIDownloadManager::DOWNLOAD_NOTSTARTED ||
+ mDownloadState == nsIDownloadManager::DOWNLOAD_QUEUED ||
+ mDownloadState == nsIDownloadManager::DOWNLOAD_DOWNLOADING;
+}
+
+bool
+nsDownload::IsFinished()
+{
+ return mDownloadState == nsIDownloadManager::DOWNLOAD_FINISHED;
+}
+
+nsresult
+nsDownload::UpdateDB()
+{
+ NS_ASSERTION(mID, "Download ID is stored as zero. This is bad!");
+ NS_ASSERTION(mDownloadManager, "Egads! We have no download manager!");
+
+ mozIStorageStatement *stmt = mPrivate ?
+ mDownloadManager->mUpdatePrivateDownloadStatement : mDownloadManager->mUpdateDownloadStatement;
+
+ nsAutoString tempPath;
+ if (mTempFile)
+ (void)mTempFile->GetPath(tempPath);
+ nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("tempPath"), tempPath);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("startTime"), mStartTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("endTime"), mLastUpdate);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("state"), mDownloadState);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mReferrer) {
+ nsAutoCString referrer;
+ rv = mReferrer->GetSpec(referrer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("referrer"), referrer);
+ } else {
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("referrer"));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("entityID"), mEntityID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t currBytes;
+ (void)GetAmountTransferred(&currBytes);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("currBytes"), currBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t maxBytes;
+ (void)GetSize(&maxBytes);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("maxBytes"), maxBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("autoResume"), mAutoResume);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return stmt->Execute();
+}
+
+nsresult
+nsDownload::FailDownload(nsresult aStatus, const char16_t *aMessage)
+{
+ // Grab the bundle before potentially losing our member variables
+ nsCOMPtr<nsIStringBundle> bundle = mDownloadManager->mBundle;
+
+ (void)SetState(nsIDownloadManager::DOWNLOAD_FAILED);
+
+ // Get title for alert.
+ nsXPIDLString title;
+ nsresult rv = bundle->GetStringFromName(
+ u"downloadErrorAlertTitle", getter_Copies(title));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get a generic message if we weren't supplied one
+ nsXPIDLString message;
+ message = aMessage;
+ if (message.IsEmpty()) {
+ rv = bundle->GetStringFromName(
+ u"downloadErrorGeneric", getter_Copies(message));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Get Download Manager window to be parent of alert
+ nsCOMPtr<nsIWindowMediator> wm =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIDOMWindowProxy> dmWindow;
+ rv = wm->GetMostRecentWindow(u"Download:Manager",
+ getter_AddRefs(dmWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Show alert
+ nsCOMPtr<nsIPromptService> prompter =
+ do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prompter->Alert(dmWindow, title, message);
+}
diff --git a/toolkit/components/downloads/nsDownloadManager.h b/toolkit/components/downloads/nsDownloadManager.h
new file mode 100644
index 0000000000..566e3560ad
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadManager.h
@@ -0,0 +1,454 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef downloadmanager___h___
+#define downloadmanager___h___
+
+#if defined(XP_WIN)
+#define DOWNLOAD_SCANNER
+#endif
+
+#include "nsIDownload.h"
+#include "nsIDownloadManager.h"
+#include "nsIDownloadProgressListener.h"
+#include "nsIFile.h"
+#include "nsIMIMEInfo.h"
+#include "nsINavHistoryService.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIStringBundle.h"
+#include "nsISupportsPrimitives.h"
+#include "nsWeakReference.h"
+#include "nsITimer.h"
+#include "nsString.h"
+
+#include "mozIDOMWindow.h"
+#include "mozStorageHelper.h"
+#include "nsAutoPtr.h"
+#include "nsCOMArray.h"
+
+typedef int16_t DownloadState;
+typedef int16_t DownloadType;
+
+class nsIArray;
+class nsDownload;
+
+#ifdef DOWNLOAD_SCANNER
+#include "nsDownloadScanner.h"
+#endif
+
+class nsDownloadManager final : public nsIDownloadManager,
+ public nsINavHistoryObserver,
+ public nsIObserver,
+ public nsSupportsWeakReference
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOWNLOADMANAGER
+ NS_DECL_NSINAVHISTORYOBSERVER
+ NS_DECL_NSIOBSERVER
+
+ nsresult Init();
+
+ static nsDownloadManager *GetSingleton();
+
+ nsDownloadManager()
+#ifdef DOWNLOAD_SCANNER
+ : mScanner(nullptr)
+#endif
+ {
+ }
+
+protected:
+ virtual ~nsDownloadManager();
+
+ nsresult InitDB();
+ nsresult InitFileDB();
+ void CloseAllDBs();
+ void CloseDB(mozIStorageConnection* aDBConn,
+ mozIStorageStatement* aUpdateStmt,
+ mozIStorageStatement* aGetIdsStmt);
+ nsresult InitPrivateDB();
+ already_AddRefed<mozIStorageConnection> GetFileDBConnection(nsIFile *dbFile) const;
+ already_AddRefed<mozIStorageConnection> GetPrivateDBConnection() const;
+ nsresult CreateTable(mozIStorageConnection* aDBConn);
+
+ /**
+ * Fix up the database after a crash such as dealing with previously-active
+ * downloads. Call this before RestoreActiveDownloads to get the downloads
+ * fixed here to be auto-resumed.
+ */
+ nsresult RestoreDatabaseState();
+
+ /**
+ * Paused downloads that survive across sessions are considered active, so
+ * rebuild the list of these downloads.
+ */
+ nsresult RestoreActiveDownloads();
+
+ nsresult GetDownloadFromDB(const nsACString& aGUID, nsDownload **retVal);
+ nsresult GetDownloadFromDB(uint32_t aID, nsDownload **retVal);
+ nsresult GetDownloadFromDB(mozIStorageConnection* aDBConn,
+ mozIStorageStatement* stmt,
+ nsDownload **retVal);
+
+ /**
+ * Specially track the active downloads so that we don't need to check
+ * every download to see if they're in progress.
+ */
+ nsresult AddToCurrentDownloads(nsDownload *aDl);
+
+ void SendEvent(nsDownload *aDownload, const char *aTopic);
+
+ /**
+ * Adds a download with the specified information to the DB.
+ *
+ * @return The id of the download, or 0 if there was an error.
+ */
+ int64_t AddDownloadToDB(const nsAString &aName,
+ const nsACString &aSource,
+ const nsACString &aTarget,
+ const nsAString &aTempPath,
+ int64_t aStartTime,
+ int64_t aEndTime,
+ const nsACString &aMimeType,
+ const nsACString &aPreferredApp,
+ nsHandlerInfoAction aPreferredAction,
+ bool aPrivate,
+ nsACString &aNewGUID);
+
+ void NotifyListenersOnDownloadStateChange(int16_t aOldState,
+ nsDownload *aDownload);
+ void NotifyListenersOnProgressChange(nsIWebProgress *aProgress,
+ nsIRequest *aRequest,
+ int64_t aCurSelfProgress,
+ int64_t aMaxSelfProgress,
+ int64_t aCurTotalProgress,
+ int64_t aMaxTotalProgress,
+ nsDownload *aDownload);
+ void NotifyListenersOnStateChange(nsIWebProgress *aProgress,
+ nsIRequest *aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus,
+ nsDownload *aDownload);
+
+ nsDownload *FindDownload(const nsACString& aGUID);
+ nsDownload *FindDownload(uint32_t aID);
+
+ /**
+ * First try to resume the download, and if that fails, retry it.
+ *
+ * @param aDl The download to resume and/or retry.
+ */
+ nsresult ResumeRetry(nsDownload *aDl);
+
+ /**
+ * Pause all active downloads and remember if they should try to auto-resume
+ * when the download manager starts again.
+ *
+ * @param aSetResume Indicate if the downloads that get paused should be set
+ * as auto-resume.
+ */
+ nsresult PauseAllDownloads(bool aSetResume);
+
+ /**
+ * Resume all paused downloads unless we're only supposed to do the automatic
+ * ones; in that case, try to retry them as well if resuming doesn't work.
+ *
+ * @param aResumeAll If true, all downloads will be resumed; otherwise, only
+ * those that are marked as auto-resume will resume.
+ */
+ nsresult ResumeAllDownloads(bool aResumeAll);
+
+ /**
+ * Stop tracking the active downloads. Only use this when we're about to quit
+ * the download manager because we destroy our list of active downloads to
+ * break the dlmgr<->dl cycle. Active downloads that aren't real-paused will
+ * be canceled.
+ */
+ nsresult RemoveAllDownloads();
+
+ /**
+ * Find all downloads from a source URI and delete them.
+ *
+ * @param aURI
+ * The source URI to remove downloads
+ */
+ nsresult RemoveDownloadsForURI(nsIURI *aURI);
+
+ /**
+ * Callback used for resuming downloads after getting a wake notification.
+ *
+ * @param aTimer
+ * Timer object fired after some delay after a wake notification
+ * @param aClosure
+ * nsDownloadManager object used to resume downloads
+ */
+ static void ResumeOnWakeCallback(nsITimer *aTimer, void *aClosure);
+ nsCOMPtr<nsITimer> mResumeOnWakeTimer;
+
+ void ConfirmCancelDownloads(int32_t aCount,
+ nsISupportsPRBool *aCancelDownloads,
+ const char16_t *aTitle,
+ const char16_t *aCancelMessageMultiple,
+ const char16_t *aCancelMessageSingle,
+ const char16_t *aDontCancelButton);
+
+ int32_t GetRetentionBehavior();
+
+ /**
+ * Type to indicate possible behaviors for active downloads across sessions.
+ *
+ * Possible values are:
+ * QUIT_AND_RESUME - downloads should be auto-resumed
+ * QUIT_AND_PAUSE - downloads should be paused
+ * QUIT_AND_CANCEL - downloads should be cancelled
+ */
+ enum QuitBehavior {
+ QUIT_AND_RESUME = 0,
+ QUIT_AND_PAUSE = 1,
+ QUIT_AND_CANCEL = 2
+ };
+
+ /**
+ * Indicates user-set behavior for active downloads across sessions,
+ *
+ * @return value of user-set pref for active download behavior
+ */
+ enum QuitBehavior GetQuitBehavior();
+
+ void OnEnterPrivateBrowsingMode();
+ void OnLeavePrivateBrowsingMode();
+
+ nsresult RetryDownload(const nsACString& aGUID);
+ nsresult RetryDownload(nsDownload* dl);
+
+ nsresult RemoveDownload(const nsACString& aGUID);
+
+ nsresult NotifyDownloadRemoval(nsDownload* aRemoved);
+
+ // Virus scanner for windows
+#ifdef DOWNLOAD_SCANNER
+private:
+ nsDownloadScanner* mScanner;
+#endif
+
+private:
+ nsresult CleanUp(mozIStorageConnection* aDBConn);
+ nsresult InitStatements(mozIStorageConnection* aDBConn,
+ mozIStorageStatement** aUpdateStatement,
+ mozIStorageStatement** aGetIdsStatement);
+ nsresult RemoveAllDownloads(nsCOMArray<nsDownload>& aDownloads);
+ nsresult PauseAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aSetResume);
+ nsresult ResumeAllDownloads(nsCOMArray<nsDownload>& aDownloads, bool aResumeAll);
+ nsresult RemoveDownloadsForURI(mozIStorageStatement* aStatement, nsIURI *aURI);
+
+ bool mUseJSTransfer;
+ nsCOMArray<nsIDownloadProgressListener> mListeners;
+ nsCOMArray<nsIDownloadProgressListener> mPrivacyAwareListeners;
+ nsCOMPtr<nsIStringBundle> mBundle;
+ nsCOMPtr<mozIStorageConnection> mDBConn;
+ nsCOMPtr<mozIStorageConnection> mPrivateDBConn;
+ nsCOMArray<nsDownload> mCurrentDownloads;
+ nsCOMArray<nsDownload> mCurrentPrivateDownloads;
+ nsCOMPtr<nsIObserverService> mObserverService;
+ nsCOMPtr<mozIStorageStatement> mUpdateDownloadStatement;
+ nsCOMPtr<mozIStorageStatement> mUpdatePrivateDownloadStatement;
+ nsCOMPtr<mozIStorageStatement> mGetIdsForURIStatement;
+ nsCOMPtr<mozIStorageStatement> mGetPrivateIdsForURIStatement;
+ nsAutoPtr<mozStorageTransaction> mHistoryTransaction;
+
+ static nsDownloadManager *gDownloadManagerService;
+
+ friend class nsDownload;
+};
+
+class nsDownload final : public nsIDownload
+{
+public:
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIWEBPROGRESSLISTENER2
+ NS_DECL_NSITRANSFER
+ NS_DECL_NSIDOWNLOAD
+ NS_DECL_ISUPPORTS
+
+ nsDownload();
+
+ /**
+ * This method MUST be called when changing states on a download. It will
+ * notify the download listener when a change happens. This also updates the
+ * database, by calling UpdateDB().
+ */
+ nsresult SetState(DownloadState aState);
+
+protected:
+ virtual ~nsDownload();
+
+ /**
+ * Finish up the download by breaking reference cycles and clearing unneeded
+ * data. Additionally, the download removes itself from the download
+ * manager's list of current downloads.
+ *
+ * NOTE: This method removes the cycle created when starting the download, so
+ * make sure to use kungFuDeathGrip if you want to access member variables.
+ */
+ void Finalize();
+
+ /**
+ * For finished resumed downloads that came in from exthandler, perform the
+ * action that would have been done if the download wasn't resumed.
+ */
+ nsresult ExecuteDesiredAction();
+
+ /**
+ * Move the temporary file to the final destination by removing the existing
+ * dummy target and renaming the temporary.
+ */
+ nsresult MoveTempToTarget();
+
+ /**
+ * Set the target file permissions to be appropriate.
+ */
+ nsresult FixTargetPermissions();
+
+ /**
+ * Update the start time which also implies the last update time is the same.
+ */
+ void SetStartTime(int64_t aStartTime);
+
+ /**
+ * Update the amount of bytes transferred and max bytes; and recalculate the
+ * download percent.
+ */
+ void SetProgressBytes(int64_t aCurrBytes, int64_t aMaxBytes);
+
+ /**
+ * All this does is cancel the connection that the download is using. It does
+ * not remove it from the download manager.
+ */
+ nsresult CancelTransfer();
+
+ /**
+ * Download is not transferring?
+ */
+ bool IsPaused();
+
+ /**
+ * Download can continue from the middle of a transfer?
+ */
+ bool IsResumable();
+
+ /**
+ * Download was resumed?
+ */
+ bool WasResumed();
+
+ /**
+ * Indicates if the download should try to automatically resume or not.
+ */
+ bool ShouldAutoResume();
+
+ /**
+ * Download is in a state to stop and complete the download?
+ */
+ bool IsFinishable();
+
+ /**
+ * Download is totally done transferring and all?
+ */
+ bool IsFinished();
+
+ /**
+ * Update the DB with the current state of the download including time,
+ * download state and other values not known when first creating the
+ * download DB entry.
+ */
+ nsresult UpdateDB();
+
+ /**
+ * Fail a download because of a failure status and prompt the provided
+ * message or use a generic download failure message if nullptr.
+ */
+ nsresult FailDownload(nsresult aStatus, const char16_t *aMessage);
+
+ /**
+ * Opens the downloaded file with the appropriate application, which is
+ * either the OS default, MIME type default, or the one selected by the user.
+ *
+ * This also adds the temporary file to the "To be deleted on Exit" list, if
+ * the corresponding user preference is set (except on OS X).
+ *
+ * This function was adopted from nsExternalAppHandler::OpenWithApplication
+ * (uriloader/exthandler/nsExternalHelperAppService.cpp).
+ */
+ nsresult OpenWithApplication();
+
+ nsDownloadManager *mDownloadManager;
+ nsCOMPtr<nsIURI> mTarget;
+
+private:
+ nsString mDisplayName;
+ nsCString mEntityID;
+ nsCString mGUID;
+
+ nsCOMPtr<nsIURI> mSource;
+ nsCOMPtr<nsIURI> mReferrer;
+ nsCOMPtr<nsICancelable> mCancelable;
+ nsCOMPtr<nsIRequest> mRequest;
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsIMIMEInfo> mMIMEInfo;
+
+ DownloadState mDownloadState;
+
+ uint32_t mID;
+ int32_t mPercentComplete;
+
+ /**
+ * These bytes are based on the position of where the request started, so 0
+ * doesn't necessarily mean we have nothing. Use GetAmountTransferred and
+ * GetSize for the real transferred amount and size.
+ */
+ int64_t mCurrBytes;
+ int64_t mMaxBytes;
+
+ PRTime mStartTime;
+ PRTime mLastUpdate;
+ int64_t mResumedAt;
+ double mSpeed;
+
+ bool mHasMultipleFiles;
+ bool mPrivate;
+
+ /**
+ * Track various states of the download trying to auto-resume when starting
+ * the download manager or restoring from a crash.
+ *
+ * DONT_RESUME: Don't automatically resume the download
+ * AUTO_RESUME: Automaically resume the download
+ */
+ enum AutoResume { DONT_RESUME, AUTO_RESUME };
+ AutoResume mAutoResume;
+
+ /**
+ * Stores the SHA-256 hash associated with the downloaded file.
+ */
+ nsCString mHash;
+
+ /**
+ * Stores the certificate chains in an nsIArray of nsIX509CertList of
+ * nsIX509Cert, if this binary is signed.
+ */
+ nsCOMPtr<nsIArray> mSignatureInfo;
+
+ /**
+ * Stores the redirects that led to this download in an nsIArray of
+ * nsIPrincipal.
+ */
+ nsCOMPtr<nsIArray> mRedirects;
+
+ friend class nsDownloadManager;
+};
+
+#endif
diff --git a/toolkit/components/downloads/nsDownloadManagerUI.js b/toolkit/components/downloads/nsDownloadManagerUI.js
new file mode 100644
index 0000000000..11e241403e
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadManagerUI.js
@@ -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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const DOWNLOAD_MANAGER_URL = "chrome://mozapps/content/downloads/downloads.xul";
+const PREF_FLASH_COUNT = "browser.download.manager.flashCount";
+
+// nsDownloadManagerUI class
+
+function nsDownloadManagerUI() {}
+
+nsDownloadManagerUI.prototype = {
+ classID: Components.ID("7dfdf0d1-aff6-4a34-bad1-d0fe74601642"),
+
+ // nsIDownloadManagerUI
+
+ show: function show(aWindowContext, aDownload, aReason, aUsePrivateUI)
+ {
+ if (!aReason)
+ aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED;
+
+ // First we see if it is already visible
+ let window = this.recentWindow;
+ if (window) {
+ window.focus();
+
+ // If we are being asked to show again, with a user interaction reason,
+ // set the appropriate variable.
+ if (aReason == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED)
+ window.gUserInteracted = true;
+ return;
+ }
+
+ let parent = null;
+ // We try to get a window to use as the parent here. If we don't have one,
+ // the download manager will close immediately after opening if the pref
+ // browser.download.manager.closeWhenDone is set to true.
+ try {
+ if (aWindowContext)
+ parent = aWindowContext.getInterface(Ci.nsIDOMWindow);
+ } catch (e) { /* it's OK to not have a parent window */ }
+
+ // We pass the download manager and the nsIDownload we want selected (if any)
+ var params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ params.appendElement(aDownload, false);
+
+ // Pass in the reason as well
+ let reason = Cc["@mozilla.org/supports-PRInt16;1"].
+ createInstance(Ci.nsISupportsPRInt16);
+ reason.data = aReason;
+ params.appendElement(reason, false);
+
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(parent,
+ DOWNLOAD_MANAGER_URL,
+ "Download:Manager",
+ "chrome,dialog=no,resizable",
+ params);
+ },
+
+ get visible() {
+ return (null != this.recentWindow);
+ },
+
+ getAttention: function getAttention()
+ {
+ if (!this.visible)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ // This preference may not be set, so defaulting to two.
+ let flashCount = 2;
+ try {
+ flashCount = prefs.getIntPref(PREF_FLASH_COUNT);
+ } catch (e) { }
+
+ var win = this.recentWindow.QueryInterface(Ci.nsIDOMChromeWindow);
+ win.getAttentionWithCycleCount(flashCount);
+ },
+
+ // nsDownloadManagerUI
+
+ get recentWindow() {
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ return wm.getMostRecentWindow("Download:Manager");
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI])
+};
+
+// Module
+
+var components = [nsDownloadManagerUI];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
+
diff --git a/toolkit/components/downloads/nsDownloadManagerUI.manifest b/toolkit/components/downloads/nsDownloadManagerUI.manifest
new file mode 100644
index 0000000000..4073c23fb0
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadManagerUI.manifest
@@ -0,0 +1,2 @@
+component {7dfdf0d1-aff6-4a34-bad1-d0fe74601642} nsDownloadManagerUI.js
+contract @mozilla.org/download-manager-ui;1 {7dfdf0d1-aff6-4a34-bad1-d0fe74601642}
diff --git a/toolkit/components/downloads/nsDownloadProxy.h b/toolkit/components/downloads/nsDownloadProxy.h
new file mode 100644
index 0000000000..ca48c9dadb
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadProxy.h
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef downloadproxy___h___
+#define downloadproxy___h___
+
+#include "nsIDownloadManager.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIMIMEInfo.h"
+#include "nsIFileURL.h"
+#include "nsIDownloadManagerUI.h"
+
+#define PREF_BDM_SHOWWHENSTARTING "browser.download.manager.showWhenStarting"
+#define PREF_BDM_FOCUSWHENSTARTING "browser.download.manager.focusWhenStarting"
+
+// This class only exists because nsDownload cannot inherit from nsITransfer
+// directly. The reason for this is that nsDownloadManager (incorrectly) keeps
+// an nsCOMArray of nsDownloads, and nsCOMArray is only intended for use with
+// abstract classes. Using a concrete class that multiply inherits from classes
+// deriving from nsISupports will throw ambiguous base class errors.
+class nsDownloadProxy : public nsITransfer
+{
+protected:
+
+ virtual ~nsDownloadProxy() { }
+
+public:
+
+ nsDownloadProxy() { }
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Init(nsIURI* aSource,
+ nsIURI* aTarget,
+ const nsAString& aDisplayName,
+ nsIMIMEInfo *aMIMEInfo,
+ PRTime aStartTime,
+ nsIFile* aTempFile,
+ nsICancelable* aCancelable,
+ bool aIsPrivate) override {
+ nsresult rv;
+ nsCOMPtr<nsIDownloadManager> dm = do_GetService("@mozilla.org/download-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = dm->AddDownload(nsIDownloadManager::DOWNLOAD_TYPE_DOWNLOAD, aSource,
+ aTarget, aDisplayName, aMIMEInfo, aStartTime,
+ aTempFile, aCancelable, aIsPrivate,
+ getter_AddRefs(mInner));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefService> prefs = do_GetService("@mozilla.org/preferences-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs);
+
+ bool showDM = true;
+ if (branch)
+ branch->GetBoolPref(PREF_BDM_SHOWWHENSTARTING, &showDM);
+
+ if (showDM) {
+ nsCOMPtr<nsIDownloadManagerUI> dmui =
+ do_GetService("@mozilla.org/download-manager-ui;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool visible;
+ rv = dmui->GetVisible(&visible);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool focusWhenStarting = true;
+ if (branch)
+ (void)branch->GetBoolPref(PREF_BDM_FOCUSWHENSTARTING, &focusWhenStarting);
+
+ if (visible && !focusWhenStarting)
+ return NS_OK;
+
+ return dmui->Show(nullptr, mInner, nsIDownloadManagerUI::REASON_NEW_DOWNLOAD, aIsPrivate);
+ }
+ return rv;
+ }
+
+ NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aStateFlags,
+ nsresult aStatus) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->OnStateChange(aWebProgress, aRequest, aStateFlags, aStatus);
+ }
+
+ NS_IMETHOD OnStatusChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, nsresult aStatus,
+ const char16_t *aMessage) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage);
+ }
+
+ NS_IMETHOD OnLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, nsIURI *aLocation,
+ uint32_t aFlags) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->OnLocationChange(aWebProgress, aRequest, aLocation, aFlags);
+ }
+
+ NS_IMETHOD OnProgressChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->OnProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress);
+ }
+
+ NS_IMETHOD OnProgressChange64(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int64_t aCurSelfProgress,
+ int64_t aMaxSelfProgress,
+ int64_t aCurTotalProgress,
+ int64_t aMaxTotalProgress) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->OnProgressChange64(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress);
+ }
+
+ NS_IMETHOD OnRefreshAttempted(nsIWebProgress *aWebProgress,
+ nsIURI *aUri,
+ int32_t aDelay,
+ bool aSameUri,
+ bool *allowRefresh) override
+ {
+ *allowRefresh = true;
+ return NS_OK;
+ }
+
+ NS_IMETHOD OnSecurityChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest, uint32_t aState) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->OnSecurityChange(aWebProgress, aRequest, aState);
+ }
+
+ NS_IMETHOD SetSha256Hash(const nsACString& aHash) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->SetSha256Hash(aHash);
+ }
+
+ NS_IMETHOD SetSignatureInfo(nsIArray* aSignatureInfo) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->SetSignatureInfo(aSignatureInfo);
+ }
+
+ NS_IMETHOD SetRedirects(nsIArray* aRedirects) override
+ {
+ NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+ return mInner->SetRedirects(aRedirects);
+ }
+
+private:
+ nsCOMPtr<nsIDownload> mInner;
+};
+
+NS_IMPL_ISUPPORTS(nsDownloadProxy, nsITransfer,
+ nsIWebProgressListener, nsIWebProgressListener2)
+
+#endif
diff --git a/toolkit/components/downloads/nsDownloadScanner.cpp b/toolkit/components/downloads/nsDownloadScanner.cpp
new file mode 100644
index 0000000000..1ef5b36602
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadScanner.cpp
@@ -0,0 +1,728 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: se cin sw=2 ts=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/. */
+
+#include "nsDownloadScanner.h"
+#include <comcat.h>
+#include <process.h>
+#include "nsDownloadManager.h"
+#include "nsIXULAppInfo.h"
+#include "nsXULAppAPI.h"
+#include "nsIPrefService.h"
+#include "nsNetUtil.h"
+#include "prtime.h"
+#include "nsDeque.h"
+#include "nsIFileURL.h"
+#include "nsIPrefBranch.h"
+#include "nsXPCOMCIDInternal.h"
+
+/**
+ * Code overview
+ *
+ * Download scanner attempts to make use of one of two different virus
+ * scanning interfaces available on Windows - IOfficeAntiVirus (Windows
+ * 95/NT 4 and IE 5) and IAttachmentExecute (XPSP2 and up). The latter
+ * interface supports calling IOfficeAntiVirus internally, while also
+ * adding support for XPSP2+ ADS forks which define security related
+ * prompting on downloaded content.
+ *
+ * Both interfaces are synchronous and can take a while, so it is not a
+ * good idea to call either from the main thread. Some antivirus scanners can
+ * take a long time to scan or the call might block while the scanner shows
+ * its UI so if the user were to download many files that finished around the
+ * same time, they would have to wait a while if the scanning were done on
+ * exactly one other thread. Since the overhead of creating a thread is
+ * relatively small compared to the time it takes to download a file and scan
+ * it, a new thread is spawned for each download that is to be scanned. Since
+ * most of the mozilla codebase is not threadsafe, all the information needed
+ * for the scanner is gathered in the main thread in nsDownloadScanner::Scan::Start.
+ * The only function of nsDownloadScanner::Scan which is invoked on another
+ * thread is DoScan.
+ *
+ * Watchdog overview
+ *
+ * The watchdog is used internally by the scanner. It maintains a queue of
+ * current download scans. In a separate thread, it dequeues the oldest scan
+ * and waits on that scan's thread with a timeout given by WATCHDOG_TIMEOUT
+ * (default is 30 seconds). If the wait times out, then the watchdog notifies
+ * the Scan that it has timed out. If the scan really has timed out, then the
+ * Scan object will dispatch its run method to the main thread; this will
+ * release the watchdog thread's addref on the Scan. If it has not timed out
+ * (i.e. the Scan just finished in time), then the watchdog dispatches a
+ * ReleaseDispatcher to release its ref of the Scan on the main thread.
+ *
+ * In order to minimize execution time, there are two events used to notify the
+ * watchdog thread of a non-empty queue and a quit event. Every blocking wait
+ * that the watchdog thread does waits on the quit event; this lets the thread
+ * quickly exit when shutting down. Also, the download scan queue will be empty
+ * most of the time; rather than use a spin loop, a simple event is triggered
+ * by the main thread when a new scan is added to an empty queue. When the
+ * watchdog thread knows that it has run out of elements in the queue, it will
+ * wait on the new item event.
+ *
+ * Memory/resource leaks due to timeout:
+ * In the event of a timeout, the thread must remain alive; terminating it may
+ * very well cause the antivirus scanner to crash or be put into an
+ * inconsistent state; COM resources may also not be cleaned up. The downside
+ * is that we need to leave the thread running; suspending it may lead to a
+ * deadlock. Because the scan call may be ongoing, it may be dependent on the
+ * memory referenced by the MSOAVINFO structure, so we cannot free mName, mPath
+ * or mOrigin; this means that we cannot free the Scan object since doing so
+ * will deallocate that memory. Note that mDownload is set to null upon timeout
+ * or completion, so the download itself is never leaked. If the scan does
+ * eventually complete, then the all the memory and resources will be freed.
+ * It is possible, however extremely rare, that in the event of a timeout, the
+ * mStateSync critical section will leak its event; this will happen only if
+ * the scanning thread, watchdog thread or main thread try to enter the
+ * critical section when one of the others is already in it.
+ *
+ * Reasoning for CheckAndSetState - there exists a race condition between the time when
+ * either the timeout or normal scan sets the state and when Scan::Run is
+ * executed on the main thread. Ex: mStatus could be set by Scan::DoScan* which
+ * then queues a dispatch on the main thread. Before that dispatch is executed,
+ * the timeout code fires and sets mStatus to AVSCAN_TIMEDOUT which then queues
+ * its dispatch to the main thread (the same function as DoScan*). Both
+ * dispatches run and both try to set the download state to AVSCAN_TIMEDOUT
+ * which is incorrect.
+ *
+ * There are 5 possible outcomes of the virus scan:
+ * AVSCAN_GOOD => the file is clean
+ * AVSCAN_BAD => the file has a virus
+ * AVSCAN_UGLY => the file had a virus, but it was cleaned
+ * AVSCAN_FAILED => something else went wrong with the virus scanner.
+ * AVSCAN_TIMEDOUT => the scan (thread setup + execution) took too long
+ *
+ * Both the good and ugly states leave the user with a benign file, so they
+ * transition to the finished state. Bad files are sent to the blocked state.
+ * The failed and timedout states transition to finished downloads.
+ *
+ * Possible Future enhancements:
+ * * Create an interface for scanning files in general
+ * * Make this a service
+ * * Get antivirus scanner status via WMI/registry
+ */
+
+// IAttachementExecute supports user definable settings for certain
+// security related prompts. This defines a general GUID for use in
+// all projects. Individual projects can define an individual guid
+// if they want to.
+#ifndef MOZ_VIRUS_SCANNER_PROMPT_GUID
+#define MOZ_VIRUS_SCANNER_PROMPT_GUID \
+ { 0xb50563d1, 0x16b6, 0x43c2, { 0xa6, 0x6a, 0xfa, 0xe6, 0xd2, 0x11, 0xf2, \
+ 0xea } }
+#endif
+static const GUID GUID_MozillaVirusScannerPromptGeneric =
+ MOZ_VIRUS_SCANNER_PROMPT_GUID;
+
+// Initial timeout is 30 seconds
+#define WATCHDOG_TIMEOUT (30*PR_USEC_PER_SEC)
+
+// Maximum length for URI's passed into IAE
+#define MAX_IAEURILENGTH 1683
+
+class nsDownloadScannerWatchdog
+{
+ typedef nsDownloadScanner::Scan Scan;
+public:
+ nsDownloadScannerWatchdog();
+ ~nsDownloadScannerWatchdog();
+
+ nsresult Init();
+ nsresult Shutdown();
+
+ void Watch(Scan *scan);
+private:
+ static unsigned int __stdcall WatchdogThread(void *p);
+ CRITICAL_SECTION mQueueSync;
+ nsDeque mScanQueue;
+ HANDLE mThread;
+ HANDLE mNewItemEvent;
+ HANDLE mQuitEvent;
+};
+
+nsDownloadScanner::nsDownloadScanner() :
+ mAESExists(false)
+{
+}
+
+// This destructor appeases the compiler; it would otherwise complain about an
+// incomplete type for nsDownloadWatchdog in the instantiation of
+// nsAutoPtr::~nsAutoPtr
+// Plus, it's a handy location to call nsDownloadScannerWatchdog::Shutdown from
+nsDownloadScanner::~nsDownloadScanner() {
+ if (mWatchdog)
+ (void)mWatchdog->Shutdown();
+}
+
+nsresult
+nsDownloadScanner::Init()
+{
+ // This CoInitialize/CoUninitialize pattern seems to be common in the Mozilla
+ // codebase. All other COM calls/objects are made on different threads.
+ nsresult rv = NS_OK;
+ CoInitialize(nullptr);
+
+ if (!IsAESAvailable()) {
+ CoUninitialize();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mAESExists = true;
+
+ // Initialize scanning
+ mWatchdog = new nsDownloadScannerWatchdog();
+ if (mWatchdog) {
+ rv = mWatchdog->Init();
+ if (FAILED(rv))
+ mWatchdog = nullptr;
+ } else {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (NS_FAILED(rv))
+ return rv;
+
+ return rv;
+}
+
+bool
+nsDownloadScanner::IsAESAvailable()
+{
+ // Try to instantiate IAE to see if it's available.
+ RefPtr<IAttachmentExecute> ae;
+ HRESULT hr;
+ hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC,
+ IID_IAttachmentExecute, getter_AddRefs(ae));
+ if (FAILED(hr)) {
+ NS_WARNING("Could not instantiate attachment execution service\n");
+ return false;
+ }
+ return true;
+}
+
+// If IAttachementExecute is available, use the CheckPolicy call to find out
+// if this download should be prevented due to Security Zone Policy settings.
+AVCheckPolicyState
+nsDownloadScanner::CheckPolicy(nsIURI *aSource, nsIURI *aTarget)
+{
+ nsresult rv;
+
+ if (!mAESExists || !aSource || !aTarget)
+ return AVPOLICY_DOWNLOAD;
+
+ nsAutoCString source;
+ rv = aSource->GetSpec(source);
+ if (NS_FAILED(rv))
+ return AVPOLICY_DOWNLOAD;
+
+ nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(aTarget));
+ if (!fileUrl)
+ return AVPOLICY_DOWNLOAD;
+
+ nsCOMPtr<nsIFile> theFile;
+ nsAutoString aFileName;
+ if (NS_FAILED(fileUrl->GetFile(getter_AddRefs(theFile))) ||
+ NS_FAILED(theFile->GetLeafName(aFileName)))
+ return AVPOLICY_DOWNLOAD;
+
+ // IAttachementExecute prohibits src data: schemes by default but we
+ // support them. If this is a data src, skip off doing a policy check.
+ // (The file will still be scanned once it lands on the local system.)
+ bool isDataScheme(false);
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aSource);
+ if (innerURI)
+ (void)innerURI->SchemeIs("data", &isDataScheme);
+ if (isDataScheme)
+ return AVPOLICY_DOWNLOAD;
+
+ RefPtr<IAttachmentExecute> ae;
+ HRESULT hr;
+ hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC,
+ IID_IAttachmentExecute, getter_AddRefs(ae));
+ if (FAILED(hr))
+ return AVPOLICY_DOWNLOAD;
+
+ (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric);
+ (void)ae->SetSource(NS_ConvertUTF8toUTF16(source).get());
+ (void)ae->SetFileName(aFileName.get());
+
+ // Any failure means the file download/exec will be blocked by the system.
+ // S_OK or S_FALSE imply it's ok.
+ hr = ae->CheckPolicy();
+
+ if (hr == S_OK)
+ return AVPOLICY_DOWNLOAD;
+
+ if (hr == S_FALSE)
+ return AVPOLICY_PROMPT;
+
+ if (hr == E_INVALIDARG)
+ return AVPOLICY_PROMPT;
+
+ return AVPOLICY_BLOCKED;
+}
+
+#ifndef THREAD_MODE_BACKGROUND_BEGIN
+#define THREAD_MODE_BACKGROUND_BEGIN 0x00010000
+#endif
+
+#ifndef THREAD_MODE_BACKGROUND_END
+#define THREAD_MODE_BACKGROUND_END 0x00020000
+#endif
+
+unsigned int __stdcall
+nsDownloadScanner::ScannerThreadFunction(void *p)
+{
+ HANDLE currentThread = GetCurrentThread();
+ NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan should not be run on the main thread");
+ nsDownloadScanner::Scan *scan = static_cast<nsDownloadScanner::Scan*>(p);
+ if (!SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_BEGIN))
+ (void)SetThreadPriority(currentThread, THREAD_PRIORITY_IDLE);
+ scan->DoScan();
+ (void)SetThreadPriority(currentThread, THREAD_MODE_BACKGROUND_END);
+ _endthreadex(0);
+ return 0;
+}
+
+// The sole purpose of this class is to release an object on the main thread
+// It assumes that its creator will addref it and it will release itself on
+// the main thread too
+class ReleaseDispatcher : public mozilla::Runnable {
+public:
+ ReleaseDispatcher(nsISupports *ptr)
+ : mPtr(ptr) {}
+ NS_IMETHOD Run();
+private:
+ nsISupports *mPtr;
+};
+
+nsresult ReleaseDispatcher::Run() {
+ NS_ASSERTION(NS_IsMainThread(), "Antivirus scan release dispatch should be run on the main thread");
+ NS_RELEASE(mPtr);
+ NS_RELEASE_THIS();
+ return NS_OK;
+}
+
+nsDownloadScanner::Scan::Scan(nsDownloadScanner *scanner, nsDownload *download)
+ : mDLScanner(scanner), mThread(nullptr),
+ mDownload(download), mStatus(AVSCAN_NOTSTARTED),
+ mSkipSource(false)
+{
+ InitializeCriticalSection(&mStateSync);
+}
+
+nsDownloadScanner::Scan::~Scan() {
+ DeleteCriticalSection(&mStateSync);
+}
+
+nsresult
+nsDownloadScanner::Scan::Start()
+{
+ mStartTime = PR_Now();
+
+ mThread = (HANDLE)_beginthreadex(nullptr, 0, ScannerThreadFunction,
+ this, CREATE_SUSPENDED, nullptr);
+ if (!mThread)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv = NS_OK;
+
+ // Get the path to the file on disk
+ nsCOMPtr<nsIFile> file;
+ rv = mDownload->GetTargetFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = file->GetPath(mPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Grab the app name
+ nsCOMPtr<nsIXULAppInfo> appinfo =
+ do_GetService(XULAPPINFO_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString name;
+ rv = appinfo->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF8toUTF16(name, mName);
+
+ // Get the origin
+ nsCOMPtr<nsIURI> uri;
+ rv = mDownload->GetSource(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString origin;
+ rv = uri->GetSpec(origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Certain virus interfaces do not like extremely long uris.
+ // Chop off the path and cgi data and just pass the base domain.
+ if (origin.Length() > MAX_IAEURILENGTH) {
+ rv = uri->GetPrePath(origin);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ CopyUTF8toUTF16(origin, mOrigin);
+
+ // We count https/ftp/http as an http download
+ bool isHttp(false), isFtp(false), isHttps(false);
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri);
+ if (!innerURI) innerURI = uri;
+ (void)innerURI->SchemeIs("http", &isHttp);
+ (void)innerURI->SchemeIs("ftp", &isFtp);
+ (void)innerURI->SchemeIs("https", &isHttps);
+ mIsHttpDownload = isHttp || isFtp || isHttps;
+
+ // IAttachementExecute prohibits src data: schemes by default but we
+ // support them. Mark the download if it's a data scheme, so we
+ // can skip off supplying the src to IAttachementExecute when we scan
+ // the resulting file.
+ (void)innerURI->SchemeIs("data", &mSkipSource);
+
+ // ResumeThread returns the previous suspend count
+ if (1 != ::ResumeThread(mThread)) {
+ CloseHandle(mThread);
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+nsresult
+nsDownloadScanner::Scan::Run()
+{
+ NS_ASSERTION(NS_IsMainThread(), "Antivirus scan dispatch should be run on the main thread");
+
+ // Cleanup our thread
+ if (mStatus != AVSCAN_TIMEDOUT)
+ WaitForSingleObject(mThread, INFINITE);
+ CloseHandle(mThread);
+
+ DownloadState downloadState = 0;
+ EnterCriticalSection(&mStateSync);
+ switch (mStatus) {
+ case AVSCAN_BAD:
+ downloadState = nsIDownloadManager::DOWNLOAD_DIRTY;
+ break;
+ default:
+ case AVSCAN_FAILED:
+ case AVSCAN_GOOD:
+ case AVSCAN_UGLY:
+ case AVSCAN_TIMEDOUT:
+ downloadState = nsIDownloadManager::DOWNLOAD_FINISHED;
+ break;
+ }
+ LeaveCriticalSection(&mStateSync);
+ // Download will be null if we already timed out
+ if (mDownload)
+ (void)mDownload->SetState(downloadState);
+
+ // Clean up some other variables
+ // In the event of a timeout, our destructor won't be called
+ mDownload = nullptr;
+
+ NS_RELEASE_THIS();
+ return NS_OK;
+}
+
+static DWORD
+ExceptionFilterFunction(DWORD exceptionCode) {
+ switch(exceptionCode) {
+ case EXCEPTION_ACCESS_VIOLATION:
+ case EXCEPTION_ILLEGAL_INSTRUCTION:
+ case EXCEPTION_IN_PAGE_ERROR:
+ case EXCEPTION_PRIV_INSTRUCTION:
+ case EXCEPTION_STACK_OVERFLOW:
+ return EXCEPTION_EXECUTE_HANDLER;
+ default:
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+}
+
+bool
+nsDownloadScanner::Scan::DoScanAES()
+{
+ // This warning is for the destructor of ae which will not be invoked in the
+ // event of a win32 exception
+#pragma warning(disable: 4509)
+ HRESULT hr;
+ RefPtr<IAttachmentExecute> ae;
+ MOZ_SEH_TRY {
+ hr = CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_ALL,
+ IID_IAttachmentExecute, getter_AddRefs(ae));
+ } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) {
+ return CheckAndSetState(AVSCAN_NOTSTARTED,AVSCAN_FAILED);
+ }
+
+ // If we (somehow) already timed out, then don't bother scanning
+ if (CheckAndSetState(AVSCAN_SCANNING, AVSCAN_NOTSTARTED)) {
+ AVScanState newState;
+ if (SUCCEEDED(hr)) {
+ bool gotException = false;
+ MOZ_SEH_TRY {
+ (void)ae->SetClientGuid(GUID_MozillaVirusScannerPromptGeneric);
+ (void)ae->SetLocalPath(mPath.get());
+ // Provide the src for everything but data: schemes.
+ if (!mSkipSource)
+ (void)ae->SetSource(mOrigin.get());
+
+ // Save() will invoke the scanner
+ hr = ae->Save();
+ } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) {
+ gotException = true;
+ }
+
+ MOZ_SEH_TRY {
+ ae = nullptr;
+ } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) {
+ gotException = true;
+ }
+
+ if(gotException) {
+ newState = AVSCAN_FAILED;
+ }
+ else if (SUCCEEDED(hr)) { // Passed the scan
+ newState = AVSCAN_GOOD;
+ }
+ else if (HRESULT_CODE(hr) == ERROR_FILE_NOT_FOUND) {
+ NS_WARNING("Downloaded file disappeared before it could be scanned");
+ newState = AVSCAN_FAILED;
+ }
+ else if (hr == E_INVALIDARG) {
+ NS_WARNING("IAttachementExecute returned invalid argument error");
+ newState = AVSCAN_FAILED;
+ }
+ else {
+ newState = AVSCAN_UGLY;
+ }
+ }
+ else {
+ newState = AVSCAN_FAILED;
+ }
+ return CheckAndSetState(newState, AVSCAN_SCANNING);
+ }
+ return false;
+}
+#pragma warning(default: 4509)
+
+void
+nsDownloadScanner::Scan::DoScan()
+{
+ CoInitialize(nullptr);
+
+ if (DoScanAES()) {
+ // We need to do a few more things on the main thread
+ NS_DispatchToMainThread(this);
+ } else {
+ // We timed out, so just release
+ ReleaseDispatcher* releaser = new ReleaseDispatcher(this);
+ if(releaser) {
+ NS_ADDREF(releaser);
+ NS_DispatchToMainThread(releaser);
+ }
+ }
+
+ MOZ_SEH_TRY {
+ CoUninitialize();
+ } MOZ_SEH_EXCEPT(ExceptionFilterFunction(GetExceptionCode())) {
+ // Not much we can do at this point...
+ }
+}
+
+HANDLE
+nsDownloadScanner::Scan::GetWaitableThreadHandle() const
+{
+ HANDLE targetHandle = INVALID_HANDLE_VALUE;
+ (void)DuplicateHandle(GetCurrentProcess(), mThread,
+ GetCurrentProcess(), &targetHandle,
+ SYNCHRONIZE, // Only allow clients to wait on this handle
+ FALSE, // cannot be inherited by child processes
+ 0);
+ return targetHandle;
+}
+
+bool
+nsDownloadScanner::Scan::NotifyTimeout()
+{
+ bool didTimeout = CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_SCANNING) ||
+ CheckAndSetState(AVSCAN_TIMEDOUT, AVSCAN_NOTSTARTED);
+ if (didTimeout) {
+ // We need to do a few more things on the main thread
+ NS_DispatchToMainThread(this);
+ }
+ return didTimeout;
+}
+
+bool
+nsDownloadScanner::Scan::CheckAndSetState(AVScanState newState, AVScanState expectedState) {
+ bool gotExpectedState = false;
+ EnterCriticalSection(&mStateSync);
+ if((gotExpectedState = (mStatus == expectedState)))
+ mStatus = newState;
+ LeaveCriticalSection(&mStateSync);
+ return gotExpectedState;
+}
+
+nsresult
+nsDownloadScanner::ScanDownload(nsDownload *download)
+{
+ if (!mAESExists)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ // No ref ptr, see comment below
+ Scan *scan = new Scan(this, download);
+ if (!scan)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ NS_ADDREF(scan);
+
+ nsresult rv = scan->Start();
+
+ // Note that we only release upon error. On success, the scan is passed off
+ // to a new thread. It is eventually released in Scan::Run on the main thread.
+ if (NS_FAILED(rv))
+ NS_RELEASE(scan);
+ else
+ // Notify the watchdog
+ mWatchdog->Watch(scan);
+
+ return rv;
+}
+
+nsDownloadScannerWatchdog::nsDownloadScannerWatchdog()
+ : mNewItemEvent(nullptr), mQuitEvent(nullptr) {
+ InitializeCriticalSection(&mQueueSync);
+}
+nsDownloadScannerWatchdog::~nsDownloadScannerWatchdog() {
+ DeleteCriticalSection(&mQueueSync);
+}
+
+nsresult
+nsDownloadScannerWatchdog::Init() {
+ // Both events are auto-reset
+ mNewItemEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
+ if (INVALID_HANDLE_VALUE == mNewItemEvent)
+ return NS_ERROR_OUT_OF_MEMORY;
+ mQuitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
+ if (INVALID_HANDLE_VALUE == mQuitEvent) {
+ (void)CloseHandle(mNewItemEvent);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // This thread is always running, however it will be asleep
+ // for most of the dlmgr's lifetime
+ mThread = (HANDLE)_beginthreadex(nullptr, 0, WatchdogThread,
+ this, 0, nullptr);
+ if (!mThread) {
+ (void)CloseHandle(mNewItemEvent);
+ (void)CloseHandle(mQuitEvent);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsDownloadScannerWatchdog::Shutdown() {
+ // Tell the watchdog thread to quite
+ (void)SetEvent(mQuitEvent);
+ (void)WaitForSingleObject(mThread, INFINITE);
+ (void)CloseHandle(mThread);
+ // Manually clear and release the queued scans
+ while (mScanQueue.GetSize() != 0) {
+ Scan *scan = reinterpret_cast<Scan*>(mScanQueue.Pop());
+ NS_RELEASE(scan);
+ }
+ (void)CloseHandle(mNewItemEvent);
+ (void)CloseHandle(mQuitEvent);
+ return NS_OK;
+}
+
+void
+nsDownloadScannerWatchdog::Watch(Scan *scan) {
+ bool wasEmpty;
+ // Note that there is no release in this method
+ // The scan will be released by the watchdog ALWAYS on the main thread
+ // when either the watchdog thread processes the scan or the watchdog
+ // is shut down
+ NS_ADDREF(scan);
+ EnterCriticalSection(&mQueueSync);
+ wasEmpty = mScanQueue.GetSize()==0;
+ mScanQueue.Push(scan);
+ LeaveCriticalSection(&mQueueSync);
+ // If the queue was empty, then the watchdog thread is/will be asleep
+ if (wasEmpty)
+ (void)SetEvent(mNewItemEvent);
+}
+
+unsigned int
+__stdcall
+nsDownloadScannerWatchdog::WatchdogThread(void *p) {
+ NS_ASSERTION(!NS_IsMainThread(), "Antivirus scan watchdog should not be run on the main thread");
+ nsDownloadScannerWatchdog *watchdog = (nsDownloadScannerWatchdog*)p;
+ HANDLE waitHandles[3] = {watchdog->mNewItemEvent, watchdog->mQuitEvent, INVALID_HANDLE_VALUE};
+ DWORD waitStatus;
+ DWORD queueItemsLeft = 0;
+ // Loop until quit event or error
+ while (0 != queueItemsLeft ||
+ ((WAIT_OBJECT_0 + 1) !=
+ (waitStatus =
+ WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE)) &&
+ waitStatus != WAIT_FAILED)) {
+ Scan *scan = nullptr;
+ PRTime startTime, expectedEndTime, now;
+ DWORD waitTime;
+
+ // Pop scan from queue
+ EnterCriticalSection(&watchdog->mQueueSync);
+ scan = reinterpret_cast<Scan*>(watchdog->mScanQueue.Pop());
+ queueItemsLeft = watchdog->mScanQueue.GetSize();
+ LeaveCriticalSection(&watchdog->mQueueSync);
+
+ // Calculate expected end time
+ startTime = scan->GetStartTime();
+ expectedEndTime = WATCHDOG_TIMEOUT + startTime;
+ now = PR_Now();
+ // PRTime is not guaranteed to be a signed integral type (afaik), but
+ // currently it is
+ if (now > expectedEndTime) {
+ waitTime = 0;
+ } else {
+ // This is a positive value, and we know that it will not overflow
+ // (bounded by WATCHDOG_TIMEOUT)
+ // waitTime is in milliseconds, nspr uses microseconds
+ waitTime = static_cast<DWORD>((expectedEndTime - now)/PR_USEC_PER_MSEC);
+ }
+ HANDLE hThread = waitHandles[2] = scan->GetWaitableThreadHandle();
+
+ // Wait for the thread (obj 1) or quit event (obj 0)
+ waitStatus = WaitForMultipleObjects(2, (waitHandles+1), FALSE, waitTime);
+ CloseHandle(hThread);
+
+ ReleaseDispatcher* releaser = new ReleaseDispatcher(scan);
+ if(!releaser)
+ continue;
+ NS_ADDREF(releaser);
+ // Got quit event or error
+ if (waitStatus == WAIT_FAILED || waitStatus == WAIT_OBJECT_0) {
+ NS_DispatchToMainThread(releaser);
+ break;
+ // Thread exited normally
+ } else if (waitStatus == (WAIT_OBJECT_0+1)) {
+ NS_DispatchToMainThread(releaser);
+ continue;
+ // Timeout case
+ } else {
+ NS_ASSERTION(waitStatus == WAIT_TIMEOUT, "Unexpected wait status in dlmgr watchdog thread");
+ if (!scan->NotifyTimeout()) {
+ // If we didn't time out, then release the thread
+ NS_DispatchToMainThread(releaser);
+ } else {
+ // NotifyTimeout did a dispatch which will release the scan, so we
+ // don't need to release the scan
+ NS_RELEASE(releaser);
+ }
+ }
+ }
+ _endthreadex(0);
+ return 0;
+}
diff --git a/toolkit/components/downloads/nsDownloadScanner.h b/toolkit/components/downloads/nsDownloadScanner.h
new file mode 100644
index 0000000000..3301489fe6
--- /dev/null
+++ b/toolkit/components/downloads/nsDownloadScanner.h
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; 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/. */
+
+/* vim: se cin sw=2 ts=2 et : */
+
+#ifndef nsDownloadScanner_h_
+#define nsDownloadScanner_h_
+
+#ifdef WIN32_LEAN_AND_MEAN
+#undef WIN32_LEAN_AND_MEAN
+#endif
+#include <windows.h>
+#define AVVENDOR
+#include <objidl.h>
+#include <msoav.h>
+#include <shlobj.h>
+
+#include "nsAutoPtr.h"
+#include "nsThreadUtils.h"
+#include "nsTArray.h"
+#include "nsIObserver.h"
+#include "nsIURI.h"
+
+enum AVScanState
+{
+ AVSCAN_NOTSTARTED = 0,
+ AVSCAN_SCANNING,
+ AVSCAN_GOOD,
+ AVSCAN_BAD,
+ AVSCAN_UGLY,
+ AVSCAN_FAILED,
+ AVSCAN_TIMEDOUT
+};
+
+enum AVCheckPolicyState
+{
+ AVPOLICY_DOWNLOAD,
+ AVPOLICY_PROMPT,
+ AVPOLICY_BLOCKED
+};
+
+// See nsDownloadScanner.cpp for declaration and definition
+class nsDownloadScannerWatchdog;
+class nsDownload;
+
+class nsDownloadScanner
+{
+public:
+ nsDownloadScanner();
+ ~nsDownloadScanner();
+ nsresult Init();
+ nsresult ScanDownload(nsDownload *download);
+ AVCheckPolicyState CheckPolicy(nsIURI *aSource, nsIURI *aTarget);
+
+private:
+ bool mAESExists;
+ nsTArray<CLSID> mScanCLSID;
+ bool IsAESAvailable();
+ bool EnumerateOAVProviders();
+
+ nsAutoPtr<nsDownloadScannerWatchdog> mWatchdog;
+
+ static unsigned int __stdcall ScannerThreadFunction(void *p);
+ class Scan : public mozilla::Runnable
+ {
+ public:
+ Scan(nsDownloadScanner *scanner, nsDownload *download);
+ ~Scan();
+ nsresult Start();
+
+ // Returns the time that Start was called
+ PRTime GetStartTime() const { return mStartTime; }
+ // Returns a copy of the thread handle that can be waited on, but not
+ // terminated
+ // The caller is responsible for closing the handle
+ // If the thread has terminated, then this will return the pseudo-handle
+ // INVALID_HANDLE_VALUE
+ HANDLE GetWaitableThreadHandle() const;
+
+ // Called on a secondary thread to notify the scan that it has timed out
+ // this is used only by the watchdog thread
+ bool NotifyTimeout();
+
+ private:
+ nsDownloadScanner *mDLScanner;
+ PRTime mStartTime;
+ HANDLE mThread;
+ RefPtr<nsDownload> mDownload;
+ // Guards mStatus
+ CRITICAL_SECTION mStateSync;
+ AVScanState mStatus;
+ nsString mPath;
+ nsString mName;
+ nsString mOrigin;
+ // Also true if it is an ftp download
+ bool mIsHttpDownload;
+ bool mSkipSource;
+
+ /* @summary Sets the Scan's state to newState if the current state is
+ expectedState
+ * @param newState The new state of the scan
+ * @param expectedState The state that the caller expects the scan to be in
+ * @return If the old state matched expectedState
+ */
+ bool CheckAndSetState(AVScanState newState, AVScanState expectedState);
+
+ NS_IMETHOD Run();
+
+ void DoScan();
+ bool DoScanAES();
+ bool DoScanOAV();
+
+ friend unsigned int __stdcall nsDownloadScanner::ScannerThreadFunction(void *);
+ };
+ // Used to give access to Scan
+ friend class nsDownloadScannerWatchdog;
+};
+#endif
+
diff --git a/toolkit/components/downloads/nsIApplicationReputation.idl b/toolkit/components/downloads/nsIApplicationReputation.idl
new file mode 100644
index 0000000000..3250cbb752
--- /dev/null
+++ b/toolkit/components/downloads/nsIApplicationReputation.idl
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIApplicationReputationCallback;
+interface nsIApplicationReputationQuery;
+interface nsIArray;
+interface nsIURI;
+
+/*
+ * A service for asynchronously querying an application reputation service
+ * based on metadata of the downloaded file.
+ */
+[scriptable, uuid(c9f03479-fd68-4393-acb2-c88d4f563174)]
+interface nsIApplicationReputationService : nsISupports {
+ /**
+ * Indicates the reason for the application reputation block.
+ */
+ const unsigned long VERDICT_SAFE = 0;
+ const unsigned long VERDICT_DANGEROUS = 1;
+ const unsigned long VERDICT_UNCOMMON = 2;
+ const unsigned long VERDICT_POTENTIALLY_UNWANTED = 3;
+ const unsigned long VERDICT_DANGEROUS_HOST = 4;
+
+ /**
+ * Start querying the application reputation service.
+ *
+ * @param aQuery
+ * The nsIApplicationReputationQuery containing metadata of the
+ * downloaded file.
+ *
+ * @param aCallback
+ * The callback for receiving the results of the query.
+ *
+ * @remarks aCallback may not be null. onComplete is guaranteed to be called
+ * on aCallback. This function may not be called more than once with
+ * the same query object. If any of the attributes of aQuery have
+ * not been set or have been set with empty data (with the exception
+ * of sourceURI), then a valid request can still be constructed and
+ * will solicit a valid response, but won't produce any useful
+ * information.
+ */
+ void queryReputation(in nsIApplicationReputationQuery aQuery,
+ in nsIApplicationReputationCallback aCallback);
+};
+
+/**
+ * A single-use, write-once interface for recording the metadata of the
+ * downloaded file. nsIApplicationReputationService.Start() may only be called
+ * once with a single query.
+ */
+[scriptable, uuid(812d7509-a9a3-446e-a66f-3ed8cc91ebd0)]
+interface nsIApplicationReputationQuery : nsISupports {
+ /*
+ * The nsIURI from which the file was downloaded. This may not be null.
+ */
+ readonly attribute nsIURI sourceURI;
+
+ /*
+ * The reference, if any.
+ */
+ readonly attribute nsIURI referrerURI;
+
+ /*
+ * The target filename for the downloaded file, as inferred from the source
+ * URI or provided by the Content-Disposition attachment file name. If this
+ * is not set by the caller, it will be passed as an empty string but the
+ * query won't produce any useful information.
+ */
+ readonly attribute AString suggestedFileName;
+
+ /*
+ * The size of the downloaded file in bytes.
+ */
+ readonly attribute unsigned long fileSize;
+
+ /*
+ * The SHA256 hash of the downloaded file in raw bytes. If this is not set by
+ * the caller, it will be passed as an empty string but the query won't
+ * produce any useful information.
+ */
+ readonly attribute ACString sha256Hash;
+
+ /*
+ * The nsIArray of nsIX509CertList of nsIX509Cert that verify for this
+ * binary, if it is signed.
+ */
+ readonly attribute nsIArray signatureInfo;
+
+ /*
+ * The nsIArray of nsIPrincipal of redirects that lead to this download. The
+ * most recent redirect is the last element.
+ */
+ readonly attribute nsIArray redirects;
+};
+
+[scriptable, function, uuid(9a228470-cfe5-11e2-8b8b-0800200c9a66)]
+interface nsIApplicationReputationCallback : nsISupports {
+ /**
+ * Callback for the result of the application reputation query.
+ * @param aStatus
+ * NS_OK if and only if the query succeeded. If it did, then
+ * shouldBlock is meaningful (otherwise it defaults to false). This
+ * may be NS_ERROR_FAILURE if the response cannot be parsed, or
+ * NS_ERROR_NOT_AVAILABLE if the service has been disabled or is not
+ * reachable.
+ * @param aShouldBlock
+ * Whether or not the download should be blocked.
+ * @param aVerdict
+ * Indicates the result of the lookup that determines whether the
+ * download should be blocked, according to the "VERDICT_" constants.
+ * This may be set to a value different than "VERDICT_SAFE" even if
+ * aShouldBlock is false, so you should always check aShouldBlock.
+ */
+ void onComplete(in bool aShouldBlock,
+ in nsresult aStatus,
+ in unsigned long aVerdict);
+};
diff --git a/toolkit/components/downloads/nsIDownload.idl b/toolkit/components/downloads/nsIDownload.idl
new file mode 100644
index 0000000000..47eb487808
--- /dev/null
+++ b/toolkit/components/downloads/nsIDownload.idl
@@ -0,0 +1,175 @@
+/* -*- 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 "nsITransfer.idl"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIObserver;
+interface nsICancelable;
+interface nsIWebProgressListener;
+interface nsIMIMEInfo;
+
+/**
+ * Represents a download object.
+ *
+ * @note This object is no longer updated once it enters a completed state.
+ * Completed states are the following:
+ * nsIDownloadManager::DOWNLOAD_FINISHED
+ * nsIDownloadManager::DOWNLOAD_FAILED
+ * nsIDownloadManager::DOWNLOAD_CANCELED
+ * nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL
+ * nsIDownloadManager::DOWNLOAD_DIRTY
+ * nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY
+ */
+[scriptable, uuid(2258f465-656e-4566-87cb-f791dbaf0322)]
+interface nsIDownload : nsITransfer {
+
+ /**
+ * The target of a download is always a file on the local file system.
+ */
+ readonly attribute nsIFile targetFile;
+
+ /**
+ * The percentage of transfer completed.
+ * If the file size is unknown it'll be -1 here.
+ */
+ readonly attribute long percentComplete;
+
+ /**
+ * The amount of bytes downloaded so far.
+ */
+ readonly attribute long long amountTransferred;
+
+ /**
+ * The size of file in bytes.
+ * Unknown size is represented by -1.
+ */
+ readonly attribute long long size;
+
+ /**
+ * The source of the transfer.
+ */
+ readonly attribute nsIURI source;
+
+ /**
+ * The target of the transfer.
+ */
+ readonly attribute nsIURI target;
+
+ /**
+ * Object that can be used to cancel the download.
+ * Will be null after the download is finished.
+ */
+ readonly attribute nsICancelable cancelable;
+
+ /**
+ * The user-readable description of the transfer.
+ */
+ readonly attribute AString displayName;
+
+ /**
+ * The time a transfer was started.
+ */
+ readonly attribute long long startTime;
+
+ /**
+ * The speed of the transfer in bytes/sec.
+ */
+ readonly attribute double speed;
+
+ /**
+ * Optional. If set, it will contain the target's relevant MIME information.
+ * This includes its MIME Type, helper app, and whether that helper should be
+ * executed.
+ */
+ readonly attribute nsIMIMEInfo MIMEInfo;
+
+ /**
+ * The id of the download that is stored in the database - not globally unique.
+ * For example, a private download and a public one might have identical ids.
+ * Can only be safely used for direct database manipulation in the database that
+ * contains this download. Use the guid property instead for safe, database-agnostic
+ * searching and manipulation.
+ *
+ * @deprecated
+ */
+ readonly attribute unsigned long id;
+
+ /**
+ * The guid of the download that is stored in the database.
+ * Has the form of twelve alphanumeric characters.
+ */
+ readonly attribute ACString guid;
+
+ /**
+ * The state of the download.
+ * @see nsIDownloadManager and nsIXPInstallManagerUI
+ */
+ readonly attribute short state;
+
+ /**
+ * The referrer uri of the download. This is only valid for HTTP downloads,
+ * and can be null.
+ */
+ readonly attribute nsIURI referrer;
+
+ /**
+ * Indicates if the download can be resumed after being paused or not. This
+ * is only the case if the download is over HTTP/1.1 or FTP and if the
+ * server supports it.
+ */
+ readonly attribute boolean resumable;
+
+ /**
+ * Indicates if the download was initiated from a context marked as private,
+ * controlling whether it should be stored in a permanent manner or not.
+ */
+ readonly attribute boolean isPrivate;
+
+ /**
+ * Cancel this download if it's currently in progress.
+ */
+ void cancel();
+
+ /**
+ * Pause this download if it is in progress.
+ *
+ * @throws NS_ERROR_UNEXPECTED if it cannot be paused.
+ */
+ void pause();
+
+ /**
+ * Resume this download if it is paused.
+ *
+ * @throws NS_ERROR_UNEXPECTED if it cannot be resumed or is not paused.
+ */
+ void resume();
+
+ /**
+ * Instruct the download manager to remove this download. Whereas
+ * cancel simply cancels the transfer, but retains information about it,
+ * remove removes all knowledge of it.
+ *
+ * @see nsIDownloadManager.removeDownload for more detail
+ * @throws NS_ERROR_FAILURE if the download is active.
+ */
+ void remove();
+
+ /**
+ * Instruct the download manager to retry this failed download
+ * @throws NS_ERROR_NOT_AVAILABLE if the download is not known.
+ * @throws NS_ERROR_FAILURE if the download is not in the following states:
+ * nsIDownloadManager::DOWNLOAD_CANCELED
+ * nsIDownloadManager::DOWNLOAD_FAILED
+ */
+ void retry();
+};
+
+%{C++
+// {b02be33b-d47c-4bd3-afd9-402a942426b0}
+#define NS_DOWNLOAD_CID \
+ { 0xb02be33b, 0xd47c, 0x4bd3, { 0xaf, 0xd9, 0x40, 0x2a, 0x94, 0x24, 0x26, 0xb0 } }
+%}
diff --git a/toolkit/components/downloads/nsIDownloadManager.idl b/toolkit/components/downloads/nsIDownloadManager.idl
new file mode 100644
index 0000000000..d7eba89408
--- /dev/null
+++ b/toolkit/components/downloads/nsIDownloadManager.idl
@@ -0,0 +1,358 @@
+/* -*- 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/. */
+
+// Keeps track of ongoing downloads, in the form of nsIDownload's.
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIDownload;
+interface nsICancelable;
+interface nsIMIMEInfo;
+interface nsIDownloadProgressListener;
+interface nsISimpleEnumerator;
+interface mozIStorageConnection;
+
+[scriptable, function, uuid(0c07ffeb-791b-49f3-ae38-2c331fd55a52)]
+interface nsIDownloadManagerResult : nsISupports {
+ /**
+ * Process an asynchronous result from getDownloadByGUID.
+ *
+ * @param aStatus The result code of the operation:
+ * * NS_OK: an item was found. No other success values are returned.
+ * * NS_ERROR_NOT_AVAILABLE: no such item was found.
+ * * Other error values are possible, but less well-defined.
+ */
+ void handleResult(in nsresult aStatus, in nsIDownload aDownload);
+};
+
+[scriptable, uuid(b29aac15-7ec4-4ab3-a53b-08f78aed3b34)]
+interface nsIDownloadManager : nsISupports {
+ /**
+ * Download type for generic file download.
+ */
+ const short DOWNLOAD_TYPE_DOWNLOAD = 0;
+
+ /**
+ * Download state for uninitialized download object.
+ */
+ const short DOWNLOAD_NOTSTARTED = -1;
+
+ /**
+ * Download is currently transferring data.
+ */
+ const short DOWNLOAD_DOWNLOADING = 0;
+
+ /**
+ * Download completed including any processing of the target
+ * file. (completed)
+ */
+ const short DOWNLOAD_FINISHED = 1;
+
+ /**
+ * Transfer failed due to error. (completed)
+ */
+ const short DOWNLOAD_FAILED = 2;
+
+ /**
+ * Download was canceled by the user. (completed)
+ */
+ const short DOWNLOAD_CANCELED = 3;
+
+ /**
+ * Transfer was paused by the user.
+ */
+ const short DOWNLOAD_PAUSED = 4;
+
+ /**
+ * Download is active but data has not yet been received.
+ */
+ const short DOWNLOAD_QUEUED = 5;
+
+ /**
+ * Transfer request was blocked by parental controls proxies. (completed)
+ */
+ const short DOWNLOAD_BLOCKED_PARENTAL = 6;
+
+ /**
+ * Transferred download is being scanned by virus scanners.
+ */
+ const short DOWNLOAD_SCANNING = 7;
+
+ /**
+ * A virus was detected in the download. The target will most likely
+ * no longer exist. (completed)
+ */
+ const short DOWNLOAD_DIRTY = 8;
+
+ /**
+ * Win specific: Request was blocked by zone policy settings.
+ * (see bug #416683) (completed)
+ */
+ const short DOWNLOAD_BLOCKED_POLICY = 9;
+
+
+ /**
+ * Creates an nsIDownload and adds it to be managed by the download manager.
+ *
+ * @param aSource The source URI of the transfer. Must not be null.
+ *
+ * @param aTarget The target URI of the transfer. Must not be null.
+ *
+ * @param aDisplayName The user-readable description of the transfer.
+ * Can be empty.
+ *
+ * @param aMIMEInfo The MIME info associated with the target,
+ * including MIME type and helper app when appropriate.
+ * This parameter is optional.
+ *
+ * @param startTime Time when the download started
+ *
+ * @param aTempFile The location of a temporary file; i.e. a file in which
+ * the received data will be stored, but which is not
+ * equal to the target file. (will be moved to the real
+ * target by the DownloadManager, when the download is
+ * finished). This will be null for all callers except for
+ * nsExternalHelperAppHandler. Addons should generally pass
+ * null for aTempFile. This will be moved to the real target
+ * by the download manager when the download is finished,
+ * and the action indicated by aMIMEInfo will be executed.
+ *
+ * @param aCancelable An object that can be used to abort the download.
+ * Must not be null.
+ *
+ * @param aIsPrivate Used to determine the privacy status of the new download.
+ * If true, the download is stored in a manner that leaves
+ * no permanent trace outside of the current private session.
+ *
+ * @return The newly created download item with the passed-in properties.
+ *
+ * @note This does not actually start a download. If you want to add and
+ * start a download, you need to create an nsIWebBrowserPersist, pass it
+ * as the aCancelable object, call this method, set the progressListener
+ * as the returned download object, then call saveURI.
+ */
+ nsIDownload addDownload(in short aDownloadType,
+ in nsIURI aSource,
+ in nsIURI aTarget,
+ in AString aDisplayName,
+ in nsIMIMEInfo aMIMEInfo,
+ in PRTime aStartTime,
+ in nsIFile aTempFile,
+ in nsICancelable aCancelable,
+ in boolean aIsPrivate);
+
+ /**
+ * Retrieves a download managed by the download manager. This can be one that
+ * is in progress, or one that has completed in the past and is stored in the
+ * database.
+ *
+ * @param aID The unique ID of the download.
+ * @return The download with the specified ID.
+ * @throws NS_ERROR_NOT_AVAILABLE if the download is not in the database.
+ */
+ nsIDownload getDownload(in unsigned long aID);
+
+ /**
+ * Retrieves a download managed by the download manager. This can be one that
+ * is in progress, or one that has completed in the past and is stored in the
+ * database. The result of this method is returned via an asynchronous callback,
+ * the parameter of which will be an nsIDownload object, or null if none exists
+ * with the provided GUID.
+ *
+ * @param aGUID The unique GUID of the download.
+ * @param aCallback The callback to invoke with the result of the search.
+ */
+ void getDownloadByGUID(in ACString aGUID, in nsIDownloadManagerResult aCallback);
+
+ /**
+ * Cancels the download with the specified ID if it's currently in-progress.
+ * This calls cancel(NS_BINDING_ABORTED) on the nsICancelable provided by the
+ * download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is not in-progress.
+ */
+ void cancelDownload(in unsigned long aID);
+
+ /**
+ * Removes the download with the specified id if it's not currently
+ * in-progress. Whereas cancelDownload simply cancels the transfer, but
+ * retains information about it, removeDownload removes all knowledge of it.
+ *
+ * Also notifies observers of the "download-manager-remove-download-guid"
+ * topic with the download guid as the subject to allow any DM consumers to
+ * react to the removal.
+ *
+ * Also may notify observers of the "download-manager-remove-download" topic
+ * with the download id as the subject, if the download removed is public
+ * or if global private browsing mode is in use. This notification is deprecated;
+ * the guid notification should be relied upon instead.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is active.
+ */
+ void removeDownload(in unsigned long aID);
+
+ /**
+ * Removes all inactive downloads that were started inclusively within the
+ * specified time frame.
+ *
+ * @param aBeginTime
+ * The start time to remove downloads by in microseconds.
+ * @param aEndTime
+ * The end time to remove downloads by in microseconds.
+ */
+ void removeDownloadsByTimeframe(in long long aBeginTime,
+ in long long aEndTime);
+
+ /**
+ * Pause the specified download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is not in-progress.
+ */
+ void pauseDownload(in unsigned long aID);
+
+ /**
+ * Resume the specified download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_FAILURE if the download is not in-progress.
+ */
+ void resumeDownload(in unsigned long aID);
+
+ /**
+ * Retries a failed download.
+ *
+ * @param aID The unique ID of the download.
+ * @throws NS_ERROR_NOT_AVAILALE if the download id is not known.
+ * @throws NS_ERROR_FAILURE if the download is not in the following states:
+ * nsIDownloadManager::DOWNLOAD_CANCELED
+ * nsIDownloadManager::DOWNLOAD_FAILED
+ */
+ void retryDownload(in unsigned long aID);
+
+ /**
+ * The database connection to the downloads database.
+ */
+ readonly attribute mozIStorageConnection DBConnection;
+ readonly attribute mozIStorageConnection privateDBConnection;
+
+ /**
+ * Whether or not there are downloads that can be cleaned up (removed)
+ * i.e. downloads that have completed, have failed or have been canceled.
+ * In global private browsing mode, this reports the status of the relevant
+ * private or public downloads. In per-window mode, it only reports for
+ * public ones.
+ */
+ readonly attribute boolean canCleanUp;
+
+ /**
+ * Whether or not there are private downloads that can be cleaned up (removed)
+ * i.e. downloads that have completed, have failed or have been canceled.
+ */
+readonly attribute boolean canCleanUpPrivate;
+
+ /**
+ * Removes completed, failed, and canceled downloads from the list.
+ * In global private browsing mode, this operates on the relevant
+ * private or public downloads. In per-window mode, it only operates
+ * on public ones.
+ *
+ * Also notifies observers of the "download-manager-remove-download-gui"
+ * and "download-manager-remove-download" topics with a null subject to
+ * allow any DM consumers to react to the removals.
+ */
+ void cleanUp();
+
+ /**
+ * Removes completed, failed, and canceled downloads from the list
+ * of private downloads.
+ *
+ * Also notifies observers of the "download-manager-remove-download-gui"
+ * and "download-manager-remove-download" topics with a null subject to
+ * allow any DM consumers to react to the removals.
+ */
+void cleanUpPrivate();
+
+ /**
+ * The number of files currently being downloaded.
+ *
+ * In global private browsing mode, this reports the status of the relevant
+ * private or public downloads. In per-window mode, it only reports public
+ * ones.
+ */
+ readonly attribute long activeDownloadCount;
+
+ /**
+ * The number of private files currently being downloaded.
+ */
+ readonly attribute long activePrivateDownloadCount;
+
+ /**
+ * An enumeration of active nsIDownloads
+ *
+ * In global private browsing mode, this reports the status of the relevant
+ * private or public downloads. In per-window mode, it only reports public
+ * ones.
+ */
+ readonly attribute nsISimpleEnumerator activeDownloads;
+
+ /**
+ * An enumeration of active private nsIDownloads
+ */
+ readonly attribute nsISimpleEnumerator activePrivateDownloads;
+
+ /**
+ * Adds a listener to the download manager. It is expected that this
+ * listener will only access downloads via their deprecated integer id attribute,
+ * and when global private browsing compatibility mode is disabled, this listener
+ * will receive no notifications for downloads marked private.
+ */
+ void addListener(in nsIDownloadProgressListener aListener);
+
+ /**
+ * Adds a listener to the download manager. This listener must be able to
+ * understand and use the guid attribute of downloads for all interactions
+ * with the download manager.
+ */
+ void addPrivacyAwareListener(in nsIDownloadProgressListener aListener);
+
+ /**
+ * Removes a listener from the download manager.
+ */
+ void removeListener(in nsIDownloadProgressListener aListener);
+
+ /**
+ * Returns the platform default downloads directory.
+ */
+ readonly attribute nsIFile defaultDownloadsDirectory;
+
+ /**
+ * Returns the user configured downloads directory.
+ * The path is dependent on two user configurable prefs
+ * set in preferences:
+ *
+ * browser.download.folderList
+ * Indicates the location users wish to save downloaded
+ * files too.
+ * Values:
+ * 0 - The desktop is the default download location.
+ * 1 - The system's downloads folder is the default download location.
+ * 2 - The default download location is elsewhere as specified in
+ * browser.download.dir. If invalid, userDownloadsDirectory
+ * will fallback on defaultDownloadsDirectory.
+ *
+ * browser.download.dir -
+ * A local path the user may have selected at some point
+ * where downloaded files are saved. The use of which is
+ * enabled when folderList equals 2.
+ */
+ readonly attribute nsIFile userDownloadsDirectory;
+};
+
+
diff --git a/toolkit/components/downloads/nsIDownloadManagerUI.idl b/toolkit/components/downloads/nsIDownloadManagerUI.idl
new file mode 100644
index 0000000000..b5ceff5b04
--- /dev/null
+++ b/toolkit/components/downloads/nsIDownloadManagerUI.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+interface nsIInterfaceRequestor;
+interface nsIDownload;
+
+[scriptable, uuid(0c76d4cf-0b06-4c1a-9bea-520c7bbdba99)]
+interface nsIDownloadManagerUI : nsISupports {
+ /**
+ * The reason that should be passed when the user requests to show the
+ * download manager's UI.
+ */
+ const short REASON_USER_INTERACTED = 0;
+
+ /**
+ * The reason that should be passed to the show method when we are displaying
+ * the UI because a new download is being added to it.
+ */
+ const short REASON_NEW_DOWNLOAD = 1;
+
+ /**
+ * Shows the Download Manager's UI to the user.
+ *
+ * @param [optional] aWindowContext
+ * The parent window context to show the UI.
+ * @param [optional] aDownload
+ * The download to be preselected upon opening.
+ * @param [optional] aReason
+ * The reason to show the download manager's UI. This defaults to
+ * REASON_USER_INTERACTED, and should be one of the previously listed
+ * constants.
+ * @param [optional] aUsePrivateUI
+ * Pass true as this argument to hint to the implementation that it
+ * should only display private downloads in the UI, if possible.
+ */
+ void show([optional] in nsIInterfaceRequestor aWindowContext,
+ [optional] in nsIDownload aDownload,
+ [optional] in short aReason,
+ [optional] in boolean aUsePrivateUI);
+
+ /**
+ * Indicates if the UI is visible or not.
+ */
+ readonly attribute boolean visible;
+
+ /**
+ * Brings attention to the UI if it is already visible
+ *
+ * @throws NS_ERROR_UNEXPECTED if the UI is not visible.
+ */
+ void getAttention();
+};
+
diff --git a/toolkit/components/downloads/nsIDownloadProgressListener.idl b/toolkit/components/downloads/nsIDownloadProgressListener.idl
new file mode 100644
index 0000000000..e406f64d61
--- /dev/null
+++ b/toolkit/components/downloads/nsIDownloadProgressListener.idl
@@ -0,0 +1,60 @@
+/* -*- 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/. */
+
+/* A minimally extended progress listener used by download manager
+ * to update its default UI. This is implemented in nsDownloadProgressListener.js.
+ * See nsIWebProgressListener for documentation, and use its constants. This isn't
+ * too pretty, but the alternative is having this extend nsIWebProgressListener and
+ * adding an |item| attribute, which would mean a separate nsIDownloadProgressListener
+ * for every nsIDownloadItem, which is a waste...
+ */
+
+#include "nsISupports.idl"
+
+interface nsIWebProgress;
+interface nsIRequest;
+interface nsIURI;
+interface nsIDownload;
+interface nsIDOMDocument;
+
+[scriptable, uuid(7acb07ea-cac2-4c15-a3ad-23aaa789ed51)]
+interface nsIDownloadProgressListener : nsISupports {
+
+ /**
+ * document
+ * The document of the download manager frontend.
+ */
+
+ attribute nsIDOMDocument document;
+
+ /**
+ * Dispatched whenever the state of the download changes.
+ *
+ * @param aState The previous download sate.
+ * @param aDownload The download object.
+ * @see nsIDownloadManager for download states.
+ */
+ void onDownloadStateChange(in short aState, in nsIDownload aDownload);
+
+ void onStateChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aStateFlags,
+ in nsresult aStatus,
+ in nsIDownload aDownload);
+
+ void onProgressChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in long long aCurSelfProgress,
+ in long long aMaxSelfProgress,
+ in long long aCurTotalProgress,
+ in long long aMaxTotalProgress,
+ in nsIDownload aDownload);
+
+ void onSecurityChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aState,
+ in nsIDownload aDownload);
+
+};
diff --git a/toolkit/components/downloads/test/unit/.eslintrc.js b/toolkit/components/downloads/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/downloads/test/unit/data/block_digest.chunk b/toolkit/components/downloads/test/unit/data/block_digest.chunk
new file mode 100644
index 0000000000..34c47c4bb5
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/data/block_digest.chunk
@@ -0,0 +1,2 @@
+a:5:32:37
+,AJ,AJ8Wbb_e;OτCV \ No newline at end of file
diff --git a/toolkit/components/downloads/test/unit/data/digest.chunk b/toolkit/components/downloads/test/unit/data/digest.chunk
new file mode 100644
index 0000000000..b1fbb46673
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/data/digest.chunk
@@ -0,0 +1,3 @@
+a:5:32:64
+_H^a7]=#nmnoQ
+@.R0D7Y4ퟆS$8 \ No newline at end of file
diff --git a/toolkit/components/downloads/test/unit/data/signed_win.exe b/toolkit/components/downloads/test/unit/data/signed_win.exe
new file mode 100644
index 0000000000..de3bb40e84
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/data/signed_win.exe
Binary files differ
diff --git a/toolkit/components/downloads/test/unit/head_download_manager.js b/toolkit/components/downloads/test/unit/head_download_manager.js
new file mode 100644
index 0000000000..1e8248071d
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/head_download_manager.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file tests the download manager backend
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+do_get_profile();
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+function createURI(aObj)
+{
+ var ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ return (aObj instanceof Ci.nsIFile) ? ios.newFileURI(aObj) :
+ ios.newURI(aObj, null, null);
+}
diff --git a/toolkit/components/downloads/test/unit/tail_download_manager.js b/toolkit/components/downloads/test/unit/tail_download_manager.js
new file mode 100644
index 0000000000..4043f31b9d
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/tail_download_manager.js
@@ -0,0 +1,23 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+// Termination functions common to all tests
+
+add_task(function* test_common_terminate()
+{
+ // Stop the HTTP server. We must do this inside a task in "tail.js" until the
+ // xpcshell testing framework supports asynchronous termination functions.
+ let deferred = Promise.defer();
+ gHttpServer.stop(deferred.resolve);
+ yield deferred.promise;
+});
+
diff --git a/toolkit/components/downloads/test/unit/test_app_rep.js b/toolkit/components/downloads/test/unit/test_app_rep.js
new file mode 100644
index 0000000000..636a71e78f
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/test_app_rep.js
@@ -0,0 +1,342 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import('resource://gre/modules/NetUtil.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const gAppRep = Cc["@mozilla.org/downloads/application-reputation-service;1"].
+ getService(Ci.nsIApplicationReputationService);
+var gHttpServ = null;
+var gTables = {};
+
+var ALLOW_LIST = 0;
+var BLOCK_LIST = 1;
+var NO_LIST = 2;
+
+var whitelistedURI = createURI("http://foo:bar@whitelisted.com/index.htm#junk");
+var exampleURI = createURI("http://user:password@example.com/i.html?foo=bar");
+var blocklistedURI = createURI("http://baz:qux@blocklisted.com?xyzzy");
+
+const appRepURLPref = "browser.safebrowsing.downloads.remote.url";
+
+function readFileToString(aFilename) {
+ let f = do_get_file(aFilename);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(f, -1, 0, 0);
+ let buf = NetUtil.readInputStreamToString(stream, stream.available());
+ return buf;
+}
+
+// Registers a table for which to serve update chunks. Returns a promise that
+// resolves when that chunk has been downloaded.
+function registerTableUpdate(aTable, aFilename) {
+ // If we haven't been given an update for this table yet, add it to the map
+ if (!(aTable in gTables)) {
+ gTables[aTable] = [];
+ }
+
+ // The number of chunks associated with this table.
+ let numChunks = gTables[aTable].length + 1;
+ let redirectPath = "/" + aTable + "-" + numChunks;
+ let redirectUrl = "localhost:4444" + redirectPath;
+
+ // Store redirect url for that table so we can return it later when we
+ // process an update request.
+ gTables[aTable].push(redirectUrl);
+
+ gHttpServ.registerPathHandler(redirectPath, function(request, response) {
+ do_print("Mock safebrowsing server handling request for " + redirectPath);
+ let contents = readFileToString(aFilename);
+ do_print("Length of " + aFilename + ": " + contents.length);
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(contents, contents.length);
+ });
+}
+
+add_task(function* test_setup() {
+ // Set up a local HTTP server to return bad verdicts.
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // Ensure safebrowsing is enabled for this test, even if the app
+ // doesn't have it enabled.
+ Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", true);
+ Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", true);
+ do_register_cleanup(function() {
+ Services.prefs.clearUserPref("browser.safebrowsing.malware.enabled");
+ Services.prefs.clearUserPref("browser.safebrowsing.downloads.enabled");
+ });
+
+ // Set block and allow tables explicitly, since the allowlist is normally
+ // disabled on non-Windows platforms.
+ Services.prefs.setCharPref("urlclassifier.downloadBlockTable",
+ "goog-badbinurl-shavar");
+ Services.prefs.setCharPref("urlclassifier.downloadAllowTable",
+ "goog-downloadwhite-digest256");
+ do_register_cleanup(function() {
+ Services.prefs.clearUserPref("urlclassifier.downloadBlockTable");
+ Services.prefs.clearUserPref("urlclassifier.downloadAllowTable");
+ });
+
+ gHttpServ = new HttpServer();
+ gHttpServ.registerDirectory("/", do_get_cwd());
+ gHttpServ.registerPathHandler("/download", function(request, response) {
+ do_throw("This test should never make a remote lookup");
+ });
+ gHttpServ.start(4444);
+});
+
+function run_test() {
+ run_next_test();
+}
+
+function check_telemetry(aCount,
+ aShouldBlockCount,
+ aListCounts) {
+ let count = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry)
+ .getHistogramById("APPLICATION_REPUTATION_COUNT")
+ .snapshot();
+ do_check_eq(count.counts[1], aCount);
+ let local = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry)
+ .getHistogramById("APPLICATION_REPUTATION_LOCAL")
+ .snapshot();
+ do_check_eq(local.counts[ALLOW_LIST], aListCounts[ALLOW_LIST],
+ "Allow list counts don't match");
+ do_check_eq(local.counts[BLOCK_LIST], aListCounts[BLOCK_LIST],
+ "Block list counts don't match");
+ do_check_eq(local.counts[NO_LIST], aListCounts[NO_LIST],
+ "No list counts don't match");
+
+ let shouldBlock = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry)
+ .getHistogramById("APPLICATION_REPUTATION_SHOULD_BLOCK")
+ .snapshot();
+ // SHOULD_BLOCK = true
+ do_check_eq(shouldBlock.counts[1], aShouldBlockCount);
+ // Sanity check that SHOULD_BLOCK total adds up to the COUNT.
+ do_check_eq(shouldBlock.counts[0] + shouldBlock.counts[1], aCount);
+}
+
+function get_telemetry_counts() {
+ let count = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry)
+ .getHistogramById("APPLICATION_REPUTATION_COUNT")
+ .snapshot();
+ let local = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry)
+ .getHistogramById("APPLICATION_REPUTATION_LOCAL")
+ .snapshot();
+ let shouldBlock = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry)
+ .getHistogramById("APPLICATION_REPUTATION_SHOULD_BLOCK")
+ .snapshot();
+ return { total: count.counts[1],
+ shouldBlock: shouldBlock.counts[1],
+ listCounts: local.counts };
+}
+
+add_test(function test_nullSourceURI() {
+ let counts = get_telemetry_counts();
+ gAppRep.queryReputation({
+ // No source URI
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_ERROR_UNEXPECTED, aStatus);
+ do_check_false(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock, counts.listCounts);
+ run_next_test();
+ });
+});
+
+add_test(function test_nullCallback() {
+ let counts = get_telemetry_counts();
+ try {
+ gAppRep.queryReputation({
+ sourceURI: createURI("http://example.com"),
+ fileSize: 12,
+ }, null);
+ do_throw("Callback cannot be null");
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_INVALID_POINTER)
+ throw ex;
+ // We don't even increment the count here, because there's no callback.
+ check_telemetry(counts.total, counts.shouldBlock, counts.listCounts);
+ run_next_test();
+ }
+});
+
+// Set up the local whitelist.
+add_test(function test_local_list() {
+ // Construct a response with redirect urls.
+ function processUpdateRequest() {
+ let response = "n:1000\n";
+ for (let table in gTables) {
+ response += "i:" + table + "\n";
+ for (let i = 0; i < gTables[table].length; ++i) {
+ response += "u:" + gTables[table][i] + "\n";
+ }
+ }
+ do_print("Returning update response: " + response);
+ return response;
+ }
+ gHttpServ.registerPathHandler("/downloads", function(request, response) {
+ let blob = processUpdateRequest();
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(blob, blob.length);
+ });
+
+ let streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"]
+ .getService(Ci.nsIUrlClassifierStreamUpdater);
+
+ // Load up some update chunks for the safebrowsing server to serve.
+ // This chunk contains the hash of blocklisted.com/.
+ registerTableUpdate("goog-badbinurl-shavar", "data/block_digest.chunk");
+ // This chunk contains the hash of whitelisted.com/.
+ registerTableUpdate("goog-downloadwhite-digest256", "data/digest.chunk");
+
+ // Download some updates, and don't continue until the downloads are done.
+ function updateSuccess(aEvent) {
+ // Timeout of n:1000 is constructed in processUpdateRequest above and
+ // passed back in the callback in nsIUrlClassifierStreamUpdater on success.
+ do_check_eq("1000", aEvent);
+ do_print("All data processed");
+ run_next_test();
+ }
+ // Just throw if we ever get an update or download error.
+ function handleError(aEvent) {
+ do_throw("We didn't download or update correctly: " + aEvent);
+ }
+ streamUpdater.downloadUpdates(
+ "goog-downloadwhite-digest256,goog-badbinurl-shavar",
+ "goog-downloadwhite-digest256,goog-badbinurl-shavar;\n",
+ true, // isPostRequest.
+ "http://localhost:4444/downloads",
+ updateSuccess, handleError, handleError);
+});
+
+add_test(function test_unlisted() {
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ let counts = get_telemetry_counts();
+ let listCounts = counts.listCounts;
+ listCounts[NO_LIST]++;
+ gAppRep.queryReputation({
+ sourceURI: exampleURI,
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_false(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock, listCounts);
+ run_next_test();
+ });
+});
+
+add_test(function test_non_uri() {
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ let counts = get_telemetry_counts();
+ let listCounts = counts.listCounts;
+ // No listcount is incremented, since the sourceURI is not an nsIURL
+ let source = NetUtil.newURI("data:application/octet-stream,ABC");
+ do_check_false(source instanceof Ci.nsIURL);
+ gAppRep.queryReputation({
+ sourceURI: source,
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_false(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock, listCounts);
+ run_next_test();
+ });
+});
+
+add_test(function test_local_blacklist() {
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ let counts = get_telemetry_counts();
+ let listCounts = counts.listCounts;
+ listCounts[BLOCK_LIST]++;
+ gAppRep.queryReputation({
+ sourceURI: blocklistedURI,
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_true(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
+ run_next_test();
+ });
+});
+
+add_test(function test_referer_blacklist() {
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ let counts = get_telemetry_counts();
+ let listCounts = counts.listCounts;
+ listCounts[BLOCK_LIST]++;
+ gAppRep.queryReputation({
+ sourceURI: exampleURI,
+ referrerURI: blocklistedURI,
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_true(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
+ run_next_test();
+ });
+});
+
+add_test(function test_blocklist_trumps_allowlist() {
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ let counts = get_telemetry_counts();
+ let listCounts = counts.listCounts;
+ listCounts[BLOCK_LIST]++;
+ gAppRep.queryReputation({
+ sourceURI: whitelistedURI,
+ referrerURI: blocklistedURI,
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_true(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
+ run_next_test();
+ });
+});
+
+add_test(function test_redirect_on_blocklist() {
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ let counts = get_telemetry_counts();
+ let listCounts = counts.listCounts;
+ listCounts[BLOCK_LIST]++;
+ listCounts[ALLOW_LIST]++;
+ let secman = Services.scriptSecurityManager;
+ let badRedirects = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ badRedirects.appendElement(secman.createCodebasePrincipal(exampleURI, {}),
+ false);
+ badRedirects.appendElement(secman.createCodebasePrincipal(blocklistedURI, {}),
+ false);
+ badRedirects.appendElement(secman.createCodebasePrincipal(whitelistedURI, {}),
+ false);
+ gAppRep.queryReputation({
+ sourceURI: whitelistedURI,
+ referrerURI: exampleURI,
+ redirects: badRedirects,
+ fileSize: 12,
+ }, function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_true(aShouldBlock);
+ check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/downloads/test/unit/test_app_rep_maclinux.js b/toolkit/components/downloads/test/unit/test_app_rep_maclinux.js
new file mode 100644
index 0000000000..7f94d15204
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/test_app_rep_maclinux.js
@@ -0,0 +1,303 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests signature extraction using Windows Authenticode APIs of
+ * downloaded files.
+ */
+
+// Globals
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const gAppRep = Cc["@mozilla.org/downloads/application-reputation-service;1"].
+ getService(Ci.nsIApplicationReputationService);
+var gStillRunning = true;
+var gTables = {};
+var gHttpServer = null;
+
+const appRepURLPref = "browser.safebrowsing.downloads.remote.url";
+const remoteEnabledPref = "browser.safebrowsing.downloads.remote.enabled";
+
+function readFileToString(aFilename) {
+ let f = do_get_file(aFilename);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(f, -1, 0, 0);
+ let buf = NetUtil.readInputStreamToString(stream, stream.available());
+ return buf;
+}
+
+function registerTableUpdate(aTable, aFilename) {
+ // If we haven't been given an update for this table yet, add it to the map
+ if (!(aTable in gTables)) {
+ gTables[aTable] = [];
+ }
+
+ // The number of chunks associated with this table.
+ let numChunks = gTables[aTable].length + 1;
+ let redirectPath = "/" + aTable + "-" + numChunks;
+ let redirectUrl = "localhost:4444" + redirectPath;
+
+ // Store redirect url for that table so we can return it later when we
+ // process an update request.
+ gTables[aTable].push(redirectUrl);
+
+ gHttpServer.registerPathHandler(redirectPath, function(request, response) {
+ do_print("Mock safebrowsing server handling request for " + redirectPath);
+ let contents = readFileToString(aFilename);
+ do_print("Length of " + aFilename + ": " + contents.length);
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(contents, contents.length);
+ });
+}
+
+// Tests
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function test_setup()
+{
+ // Wait 10 minutes, that is half of the external xpcshell timeout.
+ do_timeout(10 * 60 * 1000, function() {
+ if (gStillRunning) {
+ do_throw("Test timed out.");
+ }
+ });
+ // Set up a local HTTP server to return bad verdicts.
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // Ensure safebrowsing is enabled for this test, even if the app
+ // doesn't have it enabled.
+ Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", true);
+ Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", true);
+ // Set block table explicitly, no need for the allow table though
+ Services.prefs.setCharPref("urlclassifier.downloadBlockTable",
+ "goog-badbinurl-shavar");
+ // SendRemoteQueryInternal needs locale preference.
+ let locale = Services.prefs.getCharPref("general.useragent.locale");
+ Services.prefs.setCharPref("general.useragent.locale", "en-US");
+
+ do_register_cleanup(function() {
+ Services.prefs.clearUserPref("browser.safebrowsing.malware.enabled");
+ Services.prefs.clearUserPref("browser.safebrowsing.downloads.enabled");
+ Services.prefs.clearUserPref("urlclassifier.downloadBlockTable");
+ Services.prefs.setCharPref("general.useragent.locale", locale);
+ });
+
+ gHttpServer = new HttpServer();
+ gHttpServer.registerDirectory("/", do_get_cwd());
+
+ function createVerdict(aShouldBlock) {
+ // We can't programmatically create a protocol buffer here, so just
+ // hardcode some already serialized ones.
+ let blob = String.fromCharCode(parseInt(0x08, 16));
+ if (aShouldBlock) {
+ // A safe_browsing::ClientDownloadRequest with a DANGEROUS verdict
+ blob += String.fromCharCode(parseInt(0x01, 16));
+ } else {
+ // A safe_browsing::ClientDownloadRequest with a SAFE verdict
+ blob += String.fromCharCode(parseInt(0x00, 16));
+ }
+ return blob;
+ }
+
+ gHttpServer.registerPathHandler("/throw", function(request, response) {
+ do_throw("We shouldn't be getting here");
+ });
+
+ gHttpServer.registerPathHandler("/download", function(request, response) {
+ do_print("Querying remote server for verdict");
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ let buf = NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available());
+ do_print("Request length: " + buf.length);
+ // A garbage response. By default this produces NS_CANNOT_CONVERT_DATA as
+ // the callback status.
+ let blob = "this is not a serialized protocol buffer (the length doesn't match our hard-coded values)";
+ // We can't actually parse the protocol buffer here, so just switch on the
+ // length instead of inspecting the contents.
+ if (buf.length == 67) {
+ // evil.com
+ blob = createVerdict(true);
+ } else if (buf.length == 73) {
+ // mozilla.com
+ blob = createVerdict(false);
+ }
+ response.bodyOutputStream.write(blob, blob.length);
+ });
+
+ gHttpServer.start(4444);
+});
+
+// Construct a response with redirect urls.
+function processUpdateRequest() {
+ let response = "n:1000\n";
+ for (let table in gTables) {
+ response += "i:" + table + "\n";
+ for (let i = 0; i < gTables[table].length; ++i) {
+ response += "u:" + gTables[table][i] + "\n";
+ }
+ }
+ do_print("Returning update response: " + response);
+ return response;
+}
+
+// Set up the local whitelist.
+function waitForUpdates() {
+ let deferred = Promise.defer();
+ gHttpServer.registerPathHandler("/downloads", function(request, response) {
+ let blob = processUpdateRequest();
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(blob, blob.length);
+ });
+
+ let streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"]
+ .getService(Ci.nsIUrlClassifierStreamUpdater);
+
+ // Load up some update chunks for the safebrowsing server to serve. This
+ // particular chunk contains the hash of whitelisted.com/ and
+ // sb-ssl.google.com/safebrowsing/csd/certificate/.
+ registerTableUpdate("goog-downloadwhite-digest256", "data/digest.chunk");
+
+ // Resolve the promise once processing the updates is complete.
+ function updateSuccess(aEvent) {
+ // Timeout of n:1000 is constructed in processUpdateRequest above and
+ // passed back in the callback in nsIUrlClassifierStreamUpdater on success.
+ do_check_eq("1000", aEvent);
+ do_print("All data processed");
+ deferred.resolve(true);
+ }
+ // Just throw if we ever get an update or download error.
+ function handleError(aEvent) {
+ do_throw("We didn't download or update correctly: " + aEvent);
+ deferred.reject();
+ }
+ streamUpdater.downloadUpdates(
+ "goog-downloadwhite-digest256",
+ "goog-downloadwhite-digest256;\n",
+ true,
+ "http://localhost:4444/downloads",
+ updateSuccess, handleError, handleError);
+ return deferred.promise;
+}
+
+function promiseQueryReputation(query, expectedShouldBlock) {
+ let deferred = Promise.defer();
+ function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_eq(aShouldBlock, expectedShouldBlock);
+ deferred.resolve(true);
+ }
+ gAppRep.queryReputation(query, onComplete);
+ return deferred.promise;
+}
+
+add_task(function* ()
+{
+ // Wait for Safebrowsing local list updates to complete.
+ yield waitForUpdates();
+});
+
+add_task(function* test_blocked_binary()
+{
+ // We should reach the remote server for a verdict.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // evil.com should return a malware verdict from the remote server.
+ yield promiseQueryReputation({sourceURI: createURI("http://evil.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12}, true);
+});
+
+add_task(function* test_non_binary()
+{
+ // We should not reach the remote server for a verdict for non-binary files.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/throw");
+ yield promiseQueryReputation({sourceURI: createURI("http://evil.com"),
+ suggestedFileName: "noop.txt",
+ fileSize: 12}, false);
+});
+
+add_task(function* test_good_binary()
+{
+ // We should reach the remote server for a verdict.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // mozilla.com should return a not-guilty verdict from the remote server.
+ yield promiseQueryReputation({sourceURI: createURI("http://mozilla.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12}, false);
+});
+
+add_task(function* test_disabled()
+{
+ // Explicitly disable remote checks
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ false);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/throw");
+ let query = {sourceURI: createURI("http://example.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12};
+ let deferred = Promise.defer();
+ gAppRep.queryReputation(query,
+ function onComplete(aShouldBlock, aStatus) {
+ // We should be getting NS_ERROR_NOT_AVAILABLE if the service is disabled
+ do_check_eq(Cr.NS_ERROR_NOT_AVAILABLE, aStatus);
+ do_check_false(aShouldBlock);
+ deferred.resolve(true);
+ }
+ );
+ yield deferred.promise;
+});
+
+add_task(function* test_disabled_through_lists()
+{
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ false);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ Services.prefs.setCharPref("urlclassifier.downloadBlockTable", "");
+ let query = {sourceURI: createURI("http://example.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12};
+ let deferred = Promise.defer();
+ gAppRep.queryReputation(query,
+ function onComplete(aShouldBlock, aStatus) {
+ // We should be getting NS_ERROR_NOT_AVAILABLE if the service is disabled
+ do_check_eq(Cr.NS_ERROR_NOT_AVAILABLE, aStatus);
+ do_check_false(aShouldBlock);
+ deferred.resolve(true);
+ }
+ );
+ yield deferred.promise;
+});
+add_task(function* test_teardown()
+{
+ gStillRunning = false;
+});
diff --git a/toolkit/components/downloads/test/unit/test_app_rep_windows.js b/toolkit/components/downloads/test/unit/test_app_rep_windows.js
new file mode 100644
index 0000000000..4ff772e611
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/test_app_rep_windows.js
@@ -0,0 +1,434 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests signature extraction using Windows Authenticode APIs of
+ * downloaded files.
+ */
+
+// Globals
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const BackgroundFileSaverOutputStream = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=outputstream",
+ "nsIBackgroundFileSaver");
+
+const StringInputStream = Components.Constructor(
+ "@mozilla.org/io/string-input-stream;1",
+ "nsIStringInputStream",
+ "setData");
+
+const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
+
+const gAppRep = Cc["@mozilla.org/downloads/application-reputation-service;1"].
+ getService(Ci.nsIApplicationReputationService);
+var gStillRunning = true;
+var gTables = {};
+var gHttpServer = null;
+
+const appRepURLPref = "browser.safebrowsing.downloads.remote.url";
+const remoteEnabledPref = "browser.safebrowsing.downloads.remote.enabled";
+
+/**
+ * Returns a reference to a temporary file. If the file is then created, it
+ * will be removed when tests in this file finish.
+ */
+function getTempFile(aLeafName) {
+ let file = FileUtils.getFile("TmpD", [aLeafName]);
+ do_register_cleanup(function GTF_cleanup() {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+ return file;
+}
+
+function readFileToString(aFilename) {
+ let f = do_get_file(aFilename);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(f, -1, 0, 0);
+ let buf = NetUtil.readInputStreamToString(stream, stream.available());
+ return buf;
+}
+
+/**
+ * Waits for the given saver object to complete.
+ *
+ * @param aSaver
+ * The saver, with the output stream or a stream listener implementation.
+ * @param aOnTargetChangeFn
+ * Optional callback invoked with the target file name when it changes.
+ *
+ * @return {Promise}
+ * @resolves When onSaveComplete is called with a success code.
+ * @rejects With an exception, if onSaveComplete is called with a failure code.
+ */
+function promiseSaverComplete(aSaver, aOnTargetChangeFn) {
+ let deferred = Promise.defer();
+ aSaver.observer = {
+ onTargetChange: function BFSO_onSaveComplete(unused, aTarget)
+ {
+ if (aOnTargetChangeFn) {
+ aOnTargetChangeFn(aTarget);
+ }
+ },
+ onSaveComplete: function BFSO_onSaveComplete(unused, aStatus)
+ {
+ if (Components.isSuccessCode(aStatus)) {
+ deferred.resolve();
+ } else {
+ deferred.reject(new Components.Exception("Saver failed.", aStatus));
+ }
+ },
+ };
+ return deferred.promise;
+}
+
+/**
+ * Feeds a string to a BackgroundFileSaverOutputStream.
+ *
+ * @param aSourceString
+ * The source data to copy.
+ * @param aSaverOutputStream
+ * The BackgroundFileSaverOutputStream to feed.
+ * @param aCloseWhenDone
+ * If true, the output stream will be closed when the copy finishes.
+ *
+ * @return {Promise}
+ * @resolves When the copy completes with a success code.
+ * @rejects With an exception, if the copy fails.
+ */
+function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) {
+ let deferred = Promise.defer();
+ let inputStream = new StringInputStream(aSourceString, aSourceString.length);
+ let copier = Cc["@mozilla.org/network/async-stream-copier;1"]
+ .createInstance(Ci.nsIAsyncStreamCopier);
+ copier.init(inputStream, aSaverOutputStream, null, false, true, 0x8000, true,
+ aCloseWhenDone);
+ copier.asyncCopy({
+ onStartRequest: function () { },
+ onStopRequest: function (aRequest, aContext, aStatusCode)
+ {
+ if (Components.isSuccessCode(aStatusCode)) {
+ deferred.resolve();
+ } else {
+ deferred.reject(new Components.Exception(aResult));
+ }
+ },
+ }, null);
+ return deferred.promise;
+}
+
+// Registers a table for which to serve update chunks.
+function registerTableUpdate(aTable, aFilename) {
+ // If we haven't been given an update for this table yet, add it to the map
+ if (!(aTable in gTables)) {
+ gTables[aTable] = [];
+ }
+
+ // The number of chunks associated with this table.
+ let numChunks = gTables[aTable].length + 1;
+ let redirectPath = "/" + aTable + "-" + numChunks;
+ let redirectUrl = "localhost:4444" + redirectPath;
+
+ // Store redirect url for that table so we can return it later when we
+ // process an update request.
+ gTables[aTable].push(redirectUrl);
+
+ gHttpServer.registerPathHandler(redirectPath, function(request, response) {
+ do_print("Mock safebrowsing server handling request for " + redirectPath);
+ let contents = readFileToString(aFilename);
+ do_print("Length of " + aFilename + ": " + contents.length);
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(contents, contents.length);
+ });
+}
+
+// Tests
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_setup()
+{
+ // Wait 10 minutes, that is half of the external xpcshell timeout.
+ do_timeout(10 * 60 * 1000, function() {
+ if (gStillRunning) {
+ do_throw("Test timed out.");
+ }
+ });
+ // Set up a local HTTP server to return bad verdicts.
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // Ensure safebrowsing is enabled for this test, even if the app
+ // doesn't have it enabled.
+ Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", true);
+ Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", true);
+ // Set block and allow tables explicitly, since the allowlist is normally
+ // disabled on comm-central.
+ Services.prefs.setCharPref("urlclassifier.downloadBlockTable",
+ "goog-badbinurl-shavar");
+ Services.prefs.setCharPref("urlclassifier.downloadAllowTable",
+ "goog-downloadwhite-digest256");
+ // SendRemoteQueryInternal needs locale preference.
+ let locale = Services.prefs.getCharPref("general.useragent.locale");
+ Services.prefs.setCharPref("general.useragent.locale", "en-US");
+
+ do_register_cleanup(function() {
+ Services.prefs.clearUserPref("browser.safebrowsing.malware.enabled");
+ Services.prefs.clearUserPref("browser.safebrowsing.downloads.enabled");
+ Services.prefs.clearUserPref("urlclassifier.downloadBlockTable");
+ Services.prefs.clearUserPref("urlclassifier.downloadAllowTable");
+ Services.prefs.setCharPref("general.useragent.locale", locale);
+ });
+
+ gHttpServer = new HttpServer();
+ gHttpServer.registerDirectory("/", do_get_cwd());
+
+ function createVerdict(aShouldBlock) {
+ // We can't programmatically create a protocol buffer here, so just
+ // hardcode some already serialized ones.
+ let blob = String.fromCharCode(parseInt(0x08, 16));
+ if (aShouldBlock) {
+ // A safe_browsing::ClientDownloadRequest with a DANGEROUS verdict
+ blob += String.fromCharCode(parseInt(0x01, 16));
+ } else {
+ // A safe_browsing::ClientDownloadRequest with a SAFE verdict
+ blob += String.fromCharCode(parseInt(0x00, 16));
+ }
+ return blob;
+ }
+
+ gHttpServer.registerPathHandler("/throw", function(request, response) {
+ do_throw("We shouldn't be getting here");
+ });
+
+ gHttpServer.registerPathHandler("/download", function(request, response) {
+ do_print("Querying remote server for verdict");
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ let buf = NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available());
+ do_print("Request length: " + buf.length);
+ // A garbage response. By default this produces NS_CANNOT_CONVERT_DATA as
+ // the callback status.
+ let blob = "this is not a serialized protocol buffer (the length doesn't match our hard-coded values)";
+ // We can't actually parse the protocol buffer here, so just switch on the
+ // length instead of inspecting the contents.
+ if (buf.length == 67) {
+ // evil.com
+ blob = createVerdict(true);
+ } else if (buf.length == 73) {
+ // mozilla.com
+ blob = createVerdict(false);
+ }
+ response.bodyOutputStream.write(blob, blob.length);
+ });
+
+ gHttpServer.start(4444);
+});
+
+// Construct a response with redirect urls.
+function processUpdateRequest() {
+ let response = "n:1000\n";
+ for (let table in gTables) {
+ response += "i:" + table + "\n";
+ for (let i = 0; i < gTables[table].length; ++i) {
+ response += "u:" + gTables[table][i] + "\n";
+ }
+ }
+ do_print("Returning update response: " + response);
+ return response;
+}
+
+// Set up the local whitelist.
+function waitForUpdates() {
+ let deferred = Promise.defer();
+ gHttpServer.registerPathHandler("/downloads", function(request, response) {
+ let blob = processUpdateRequest();
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(blob, blob.length);
+ });
+
+ let streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"]
+ .getService(Ci.nsIUrlClassifierStreamUpdater);
+
+ // Load up some update chunks for the safebrowsing server to serve. This
+ // particular chunk contains the hash of whitelisted.com/ and
+ // sb-ssl.google.com/safebrowsing/csd/certificate/.
+ registerTableUpdate("goog-downloadwhite-digest256", "data/digest.chunk");
+
+ // Resolve the promise once processing the updates is complete.
+ function updateSuccess(aEvent) {
+ // Timeout of n:1000 is constructed in processUpdateRequest above and
+ // passed back in the callback in nsIUrlClassifierStreamUpdater on success.
+ do_check_eq("1000", aEvent);
+ do_print("All data processed");
+ deferred.resolve(true);
+ }
+ // Just throw if we ever get an update or download error.
+ function handleError(aEvent) {
+ do_throw("We didn't download or update correctly: " + aEvent);
+ deferred.reject();
+ }
+ streamUpdater.downloadUpdates(
+ "goog-downloadwhite-digest256",
+ "goog-downloadwhite-digest256;\n",
+ true,
+ "http://localhost:4444/downloads",
+ updateSuccess, handleError, handleError);
+ return deferred.promise;
+}
+
+function promiseQueryReputation(query, expectedShouldBlock) {
+ let deferred = Promise.defer();
+ function onComplete(aShouldBlock, aStatus) {
+ do_check_eq(Cr.NS_OK, aStatus);
+ do_check_eq(aShouldBlock, expectedShouldBlock);
+ deferred.resolve(true);
+ }
+ gAppRep.queryReputation(query, onComplete);
+ return deferred.promise;
+}
+
+add_task(function* ()
+{
+ // Wait for Safebrowsing local list updates to complete.
+ yield waitForUpdates();
+});
+
+add_task(function* test_signature_whitelists()
+{
+ // We should never get to the remote server.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/throw");
+
+ // Use BackgroundFileSaver to extract the signature on Windows.
+ let destFile = getTempFile(TEST_FILE_NAME_1);
+
+ let data = readFileToString("data/signed_win.exe");
+ let saver = new BackgroundFileSaverOutputStream();
+ let completionPromise = promiseSaverComplete(saver);
+ saver.enableSignatureInfo();
+ saver.setTarget(destFile, false);
+ yield promiseCopyToSaver(data, saver, true);
+
+ saver.finish(Cr.NS_OK);
+ yield completionPromise;
+
+ // Clean up.
+ destFile.remove(false);
+
+ // evil.com is not on the allowlist, but this binary is signed by an entity
+ // whose certificate information is on the allowlist.
+ yield promiseQueryReputation({sourceURI: createURI("http://evil.com"),
+ signatureInfo: saver.signatureInfo,
+ fileSize: 12}, false);
+});
+
+add_task(function* test_blocked_binary()
+{
+ // We should reach the remote server for a verdict.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // evil.com should return a malware verdict from the remote server.
+ yield promiseQueryReputation({sourceURI: createURI("http://evil.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12}, true);
+});
+
+add_task(function* test_non_binary()
+{
+ // We should not reach the remote server for a verdict for non-binary files.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/throw");
+ yield promiseQueryReputation({sourceURI: createURI("http://evil.com"),
+ suggestedFileName: "noop.txt",
+ fileSize: 12}, false);
+});
+
+add_task(function* test_good_binary()
+{
+ // We should reach the remote server for a verdict.
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ true);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ // mozilla.com should return a not-guilty verdict from the remote server.
+ yield promiseQueryReputation({sourceURI: createURI("http://mozilla.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12}, false);
+});
+
+add_task(function* test_disabled()
+{
+ // Explicitly disable remote checks
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ false);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/throw");
+ let query = {sourceURI: createURI("http://example.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12};
+ let deferred = Promise.defer();
+ gAppRep.queryReputation(query,
+ function onComplete(aShouldBlock, aStatus) {
+ // We should be getting NS_ERROR_NOT_AVAILABLE if the service is disabled
+ do_check_eq(Cr.NS_ERROR_NOT_AVAILABLE, aStatus);
+ do_check_false(aShouldBlock);
+ deferred.resolve(true);
+ }
+ );
+ yield deferred.promise;
+});
+
+add_task(function* test_disabled_through_lists()
+{
+ Services.prefs.setBoolPref(remoteEnabledPref,
+ false);
+ Services.prefs.setCharPref(appRepURLPref,
+ "http://localhost:4444/download");
+ Services.prefs.setCharPref("urlclassifier.downloadBlockTable", "");
+ let query = {sourceURI: createURI("http://example.com"),
+ suggestedFileName: "noop.bat",
+ fileSize: 12};
+ let deferred = Promise.defer();
+ gAppRep.queryReputation(query,
+ function onComplete(aShouldBlock, aStatus) {
+ // We should be getting NS_ERROR_NOT_AVAILABLE if the service is disabled
+ do_check_eq(Cr.NS_ERROR_NOT_AVAILABLE, aStatus);
+ do_check_false(aShouldBlock);
+ deferred.resolve(true);
+ }
+ );
+ yield deferred.promise;
+});
+add_task(function* test_teardown()
+{
+ gStillRunning = false;
+});
diff --git a/toolkit/components/downloads/test/unit/xpcshell.ini b/toolkit/components/downloads/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..68b6e1fc32
--- /dev/null
+++ b/toolkit/components/downloads/test/unit/xpcshell.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+head = head_download_manager.js
+tail = tail_download_manager.js
+skip-if = toolkit == 'android'
+support-files =
+ data/digest.chunk
+ data/block_digest.chunk
+ data/signed_win.exe
+
+[test_app_rep.js]
+[test_app_rep_windows.js]
+skip-if = os != "win"
+[test_app_rep_maclinux.js]
+skip-if = os == "win"
diff --git a/toolkit/components/extensions/.eslintrc.js b/toolkit/components/extensions/.eslintrc.js
new file mode 100644
index 0000000000..70196fc6ac
--- /dev/null
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -0,0 +1,497 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../.eslintrc.js",
+
+ "parserOptions": {
+ "ecmaVersion": 8,
+ },
+
+ "globals": {
+ "Cc": true,
+ "Ci": true,
+ "Components": true,
+ "Cr": true,
+ "Cu": true,
+ "dump": true,
+ "TextDecoder": false,
+ "TextEncoder": false,
+ // Specific to WebExtensions:
+ "Extension": true,
+ "ExtensionManagement": true,
+ "extensions": true,
+ "global": true,
+ "NetUtil": true,
+ "openOptionsPage": true,
+ "require": false,
+ "runSafe": true,
+ "runSafeSync": true,
+ "runSafeSyncWithoutClone": true,
+ "Services": true,
+ "TabManager": true,
+ "WindowListManager": true,
+ "XPCOMUtils": true,
+ },
+
+ "rules": {
+ // Rules from the mozilla plugin
+ "mozilla/balanced-listeners": "error",
+ "mozilla/no-aArgs": "error",
+ "mozilla/no-cpows-in-tests": "warn",
+ "mozilla/var-only-at-top-level": "warn",
+
+ "valid-jsdoc": ["error", {
+ "prefer": {
+ "return": "returns",
+ },
+ "preferType": {
+ "Boolean": "boolean",
+ "Number": "number",
+ "String": "string",
+ "bool": "boolean",
+ },
+ "requireParamDescription": false,
+ "requireReturn": false,
+ "requireReturnDescription": false,
+ }],
+
+ // Braces only needed for multi-line arrow function blocks
+ // "arrow-body-style": ["error", "as-needed"],
+
+ // Require spacing around =>
+ "arrow-spacing": "error",
+
+ // Always require spacing around a single line block
+ "block-spacing": "warn",
+
+ // Forbid spaces inside the square brackets of array literals.
+ "array-bracket-spacing": ["error", "never"],
+
+ // Forbid spaces inside the curly brackets of object literals.
+ "object-curly-spacing": ["error", "never"],
+
+ // No space padding in parentheses
+ "space-in-parens": ["error", "never"],
+
+ // Enforce one true brace style (opening brace on the same line) and avoid
+ // start and end braces on the same line.
+ "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+
+ // No space before always a space after a comma
+ "comma-spacing": ["error", {"before": false, "after": true}],
+
+ // Commas at the end of the line not the start
+ "comma-style": "error",
+
+ // Don't require spaces around computed properties
+ "computed-property-spacing": ["error", "never"],
+
+ // Functions are not required to consistently return something or nothing
+ "consistent-return": "off",
+
+ // Require braces around blocks that start a new line
+ "curly": ["error", "all"],
+
+ // Always require a trailing EOL
+ "eol-last": "error",
+
+ // Require function* name()
+ "generator-star-spacing": ["error", {"before": false, "after": true}],
+
+ // Two space indent
+ "indent": ["error", 2, {"SwitchCase": 1}],
+
+ // Space after colon not before in property declarations
+ "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "minimum"}],
+
+ // Require spaces before and after finally, catch, etc.
+ "keyword-spacing": "error",
+
+ // Unix linebreaks
+ "linebreak-style": ["error", "unix"],
+
+ // Always require parenthesis for new calls
+ "new-parens": "error",
+
+ // Use [] instead of Array()
+ "no-array-constructor": "error",
+
+ // No duplicate arguments in function declarations
+ "no-dupe-args": "error",
+
+ // No duplicate keys in object declarations
+ "no-dupe-keys": "error",
+
+ // No duplicate cases in switch statements
+ "no-duplicate-case": "error",
+
+ // If an if block ends with a return no need for an else block
+ // "no-else-return": "error",
+
+ // Disallow empty statements. This will report an error for:
+ // try { something(); } catch (e) {}
+ // but will not report it for:
+ // try { something(); } catch (e) { /* Silencing the error because ...*/ }
+ // which is a valid use case.
+ "no-empty": "error",
+
+ // No empty character classes in regex
+ "no-empty-character-class": "error",
+
+ // Disallow empty destructuring
+ "no-empty-pattern": "error",
+
+ // No assiging to exception variable
+ "no-ex-assign": "error",
+
+ // No using !! where casting to boolean is already happening
+ "no-extra-boolean-cast": "warn",
+
+ // No double semicolon
+ "no-extra-semi": "error",
+
+ // No overwriting defined functions
+ "no-func-assign": "error",
+
+ // No invalid regular expresions
+ "no-invalid-regexp": "error",
+
+ // No odd whitespace characters
+ "no-irregular-whitespace": "error",
+
+ // No single if block inside an else block
+ "no-lonely-if": "warn",
+
+ // No mixing spaces and tabs in indent
+ "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+
+ // Disallow use of multiple spaces (sometimes used to align const values,
+ // array or object items, etc.). It's hard to maintain and doesn't add that
+ // much benefit.
+ "no-multi-spaces": "warn",
+
+ // No reassigning native JS objects
+ "no-native-reassign": "error",
+
+ // No (!foo in bar)
+ "no-negated-in-lhs": "error",
+
+ // Nested ternary statements are confusing
+ "no-nested-ternary": "error",
+
+ // Use {} instead of new Object()
+ "no-new-object": "error",
+
+ // No Math() or JSON()
+ "no-obj-calls": "error",
+
+ // No octal literals
+ "no-octal": "error",
+
+ // No redeclaring variables
+ "no-redeclare": "error",
+
+ // No unnecessary comparisons
+ "no-self-compare": "error",
+
+ // No spaces between function name and parentheses
+ "no-spaced-func": "warn",
+
+ // No trailing whitespace
+ "no-trailing-spaces": "error",
+
+ // Error on newline where a semicolon is needed
+ "no-unexpected-multiline": "error",
+
+ // No unreachable statements
+ "no-unreachable": "error",
+
+ // No expressions where a statement is expected
+ "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": ["error", {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}],
+
+ // No using variables before defined
+ "no-use-before-define": "error",
+
+ // No using with
+ "no-with": "error",
+
+ // Always require semicolon at end of statement
+ "semi": ["error", "always"],
+
+ // Require space before blocks
+ "space-before-blocks": "error",
+
+ // Never use spaces before function parentheses
+ "space-before-function-paren": ["error", {"anonymous": "never", "named": "never"}],
+
+ // Require spaces around operators, except for a|0.
+ "space-infix-ops": ["error", {"int32Hint": true}],
+
+ // ++ and -- should not need spacing
+ "space-unary-ops": ["warn", {"nonwords": false, "words": true, "overrides": {"typeof": false}}],
+
+ // No comparisons to NaN
+ "use-isnan": "error",
+
+ // Only check typeof against valid results
+ "valid-typeof": "error",
+
+ // Disallow using variables outside the blocks they are defined (especially
+ // since only let and const are used, see "no-var").
+ "block-scoped-var": "error",
+
+ // Allow trailing commas for easy list extension. Having them does not
+ // impair readability, but also not required either.
+ "comma-dangle": ["error", "always-multiline"],
+
+ // Warn about cyclomatic complexity in functions.
+ "complexity": "warn",
+
+ // Don't warn for inconsistent naming when capturing this (not so important
+ // with auto-binding fat arrow functions).
+ // "consistent-this": ["error", "self"],
+
+ // Don't require a default case in switch statements. Avoid being forced to
+ // add a bogus default when you know all possible cases are handled.
+ "default-case": "off",
+
+ // Enforce dots on the next line with property name.
+ "dot-location": ["error", "property"],
+
+ // Encourage the use of dot notation whenever possible.
+ "dot-notation": "error",
+
+ // Allow using == instead of ===, in the interest of landing something since
+ // the devtools codebase is split on convention here.
+ "eqeqeq": "off",
+
+ // Don't require function expressions to have a name.
+ // This makes the code more verbose and hard to read. Our engine already
+ // does a fantastic job assigning a name to the function, which includes
+ // the enclosing function name, and worst case you have a line number that
+ // you can just look up.
+ "func-names": "off",
+
+ // Allow use of function declarations and expressions.
+ "func-style": "off",
+
+ // Don't enforce the maximum depth that blocks can be nested. The complexity
+ // rule is a better rule to check this.
+ "max-depth": "off",
+
+ // Maximum length of a line.
+ // Disabled because we exceed this in too many places.
+ "max-len": [0, 80],
+
+ // Maximum depth callbacks can be nested.
+ "max-nested-callbacks": ["error", 4],
+
+ // Don't limit the number of parameters that can be used in a function.
+ "max-params": "off",
+
+ // Don't limit the maximum number of statement allowed in a function. We
+ // already have the complexity rule that's a better measurement.
+ "max-statements": "off",
+
+ // Don't require a capital letter for constructors, only check if all new
+ // operators are followed by a capital letter. Don't warn when capitalized
+ // functions are used without the new operator.
+ "new-cap": ["off", {"capIsNew": false}],
+
+ // Allow use of bitwise operators.
+ "no-bitwise": "off",
+
+ // Disallow use of arguments.caller or arguments.callee.
+ "no-caller": "error",
+
+ // Disallow the catch clause parameter name being the same as a variable in
+ // the outer scope, to avoid confusion.
+ "no-catch-shadow": "off",
+
+ // Disallow assignment in conditional expressions.
+ "no-cond-assign": "error",
+
+ // Disallow using the console API.
+ "no-console": "error",
+
+ // Allow using constant expressions in conditions like while (true)
+ "no-constant-condition": "off",
+
+ // Allow use of the continue statement.
+ "no-continue": "off",
+
+ // Disallow control characters in regular expressions.
+ "no-control-regex": "error",
+
+ // Disallow use of debugger.
+ "no-debugger": "error",
+
+ // Disallow deletion of variables (deleting properties is fine).
+ "no-delete-var": "error",
+
+ // Allow division operators explicitly at beginning of regular expression.
+ "no-div-regex": "off",
+
+ // Disallow use of eval(). We have other APIs to evaluate code in content.
+ "no-eval": "error",
+
+ // Disallow adding to native types
+ "no-extend-native": "error",
+
+ // Disallow unnecessary function binding.
+ "no-extra-bind": "error",
+
+ // Allow unnecessary parentheses, as they may make the code more readable.
+ "no-extra-parens": "off",
+
+ // Disallow fallthrough of case statements, except if there is a comment.
+ "no-fallthrough": "error",
+
+ // Allow the use of leading or trailing decimal points in numeric literals.
+ "no-floating-decimal": "off",
+
+ // Allow comments inline after code.
+ "no-inline-comments": "off",
+
+ // Disallow use of labels for anything other then loops and switches.
+ "no-labels": ["error", {"allowLoop": true}],
+
+ // Disallow use of multiline strings (use template strings instead).
+ "no-multi-str": "warn",
+
+ // Disallow multiple empty lines.
+ "no-multiple-empty-lines": [1, {"max": 2}],
+
+ // Allow reassignment of function parameters.
+ "no-param-reassign": "off",
+
+ // Allow string concatenation with __dirname and __filename (not a node env).
+ "no-path-concat": "off",
+
+ // Allow use of unary operators, ++ and --.
+ "no-plusplus": "off",
+
+ // Allow using process.env (not a node environment).
+ "no-process-env": "off",
+
+ // Allow using process.exit (not a node environment).
+ "no-process-exit": "off",
+
+ // Disallow usage of __proto__ property.
+ "no-proto": "error",
+
+ // Disallow multiple spaces in a regular expression literal.
+ "no-regex-spaces": "error",
+
+ // Allow reserved words being used as object literal keys.
+ "no-reserved-keys": "off",
+
+ // Don't restrict usage of specified node modules (not a node environment).
+ "no-restricted-modules": "off",
+
+ // Disallow use of assignment in return statement. It is preferable for a
+ // single line of code to have only one easily predictable effect.
+ "no-return-assign": "error",
+
+ // Don't warn about declaration of variables already declared in the outer scope.
+ "no-shadow": "off",
+
+ // Disallow shadowing of names such as arguments.
+ "no-shadow-restricted-names": "error",
+
+ // Allow use of synchronous methods (not a node environment).
+ "no-sync": "off",
+
+ // Allow the use of ternary operators.
+ "no-ternary": "off",
+
+ // Disallow throwing literals (eg. throw "error" instead of
+ // throw new Error("error")).
+ "no-throw-literal": "error",
+
+ // Disallow use of undeclared variables unless mentioned in a /* global */
+ // block. Note that globals from head.js are automatically imported in tests
+ // by the import-headjs-globals rule form the mozilla eslint plugin.
+ "no-undef": "error",
+
+ // Allow dangling underscores in identifiers (for privates).
+ "no-underscore-dangle": "off",
+
+ // Allow use of undefined variable.
+ "no-undefined": "off",
+
+ // Disallow the use of Boolean literals in conditional expressions.
+ "no-unneeded-ternary": "error",
+
+ // We use var-only-at-top-level instead of no-var as we allow top level
+ // vars.
+ "no-var": "off",
+
+ // Allow using TODO/FIXME comments.
+ "no-warning-comments": "off",
+
+ // Don't require method and property shorthand syntax for object literals.
+ // We use this in the code a lot, but not consistently, and this seems more
+ // like something to check at code review time.
+ "object-shorthand": "off",
+
+ // Allow more than one variable declaration per function.
+ "one-var": "off",
+
+ // Disallow padding within blocks.
+ "padded-blocks": ["warn", "never"],
+
+ // Don't require quotes around object literal property names.
+ "quote-props": "off",
+
+ // Double quotes should be used.
+ "quotes": ["warn", "double", {"avoidEscape": true, "allowTemplateLiterals": true}],
+
+ // Require use of the second argument for parseInt().
+ "radix": "error",
+
+ // Enforce spacing after semicolons.
+ "semi-spacing": ["error", {"before": false, "after": true}],
+
+ // Don't require to sort variables within the same declaration block.
+ // Anyway, one-var is disabled.
+ "sort-vars": "off",
+
+ // Require a space immediately following the // in a line comment.
+ "spaced-comment": ["error", "always"],
+
+ // Require "use strict" to be defined globally in the script.
+ "strict": ["error", "global"],
+
+ // Allow vars to be declared anywhere in the scope.
+ "vars-on-top": "off",
+
+ // Don't require immediate function invocation to be wrapped in parentheses.
+ "wrap-iife": "off",
+
+ // Don't require regex literals to be wrapped in parentheses (which
+ // supposedly prevent them from being mistaken for division operators).
+ "wrap-regex": "off",
+
+ // Disallow Yoda conditions (where literal value comes first).
+ "yoda": "error",
+
+ // disallow use of eval()-like methods
+ "no-implied-eval": "error",
+
+ // Disallow function or variable declarations in nested blocks
+ "no-inner-declarations": "error",
+
+ // Disallow usage of __iterator__ property
+ "no-iterator": "error",
+
+ // Disallow labels that share a name with a variable
+ "no-label-var": "error",
+
+ // Disallow creating new instances of String, Number, and Boolean
+ "no-new-wrappers": "error",
+ },
+};
diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm
new file mode 100644
index 0000000000..3468f2594c
--- /dev/null
+++ b/toolkit/components/extensions/Extension.jsm
@@ -0,0 +1,902 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
+
+/* globals Extension ExtensionData */
+
+/*
+ * This file is the main entry point for extensions. When an extension
+ * loads, its bootstrap.js file creates a Extension instance
+ * and calls .startup() on it. It calls .shutdown() when the extension
+ * unloads. Extension manages any extension-specific state in
+ * the chrome process.
+ */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.importGlobalProperties(["TextEncoder"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
+ "resource://gre/modules/ExtensionAPI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+ "resource://gre/modules/ExtensionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
+ "resource://testing-common/ExtensionTestCommon.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+ "resource://gre/modules/Locale.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "require",
+ "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+Cu.import("resource://gre/modules/ExtensionContent.jsm");
+Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+var {
+ GlobalManager,
+ ParentAPIManager,
+ apiManager: Management,
+} = ExtensionParent;
+
+const {
+ EventEmitter,
+ LocaleData,
+ getUniqueId,
+} = ExtensionUtils;
+
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+
+const LOGGER_ID_BASE = "addons.webextension.";
+const UUID_MAP_PREF = "extensions.webextensions.uuids";
+const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
+const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
+
+const COMMENT_REGEXP = new RegExp(String.raw`
+ ^
+ (
+ (?:
+ [^"\n] |
+ " (?:[^"\\\n] | \\.)* "
+ )*?
+ )
+
+ //.*
+ `.replace(/\s+/g, ""), "gm");
+
+// All moz-extension URIs use a machine-specific UUID rather than the
+// extension's own ID in the host component. This makes it more
+// difficult for web pages to detect whether a user has a given add-on
+// installed (by trying to load a moz-extension URI referring to a
+// web_accessible_resource from the extension). UUIDMap.get()
+// returns the UUID for a given add-on ID.
+var UUIDMap = {
+ _read() {
+ let pref = Preferences.get(UUID_MAP_PREF, "{}");
+ try {
+ return JSON.parse(pref);
+ } catch (e) {
+ Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
+ return {};
+ }
+ },
+
+ _write(map) {
+ Preferences.set(UUID_MAP_PREF, JSON.stringify(map));
+ },
+
+ get(id, create = true) {
+ let map = this._read();
+
+ if (id in map) {
+ return map[id];
+ }
+
+ let uuid = null;
+ if (create) {
+ uuid = uuidGen.generateUUID().number;
+ uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
+
+ map[id] = uuid;
+ this._write(map);
+ }
+ return uuid;
+ },
+
+ remove(id) {
+ let map = this._read();
+ delete map[id];
+ this._write(map);
+ },
+};
+
+// This is the old interface that UUIDMap replaced, to be removed when
+// the references listed in bug 1291399 are updated.
+/* exported getExtensionUUID */
+function getExtensionUUID(id) {
+ return UUIDMap.get(id, true);
+}
+
+// For extensions that have called setUninstallURL(), send an event
+// so the browser can display the URL.
+var UninstallObserver = {
+ initialized: false,
+
+ init() {
+ if (!this.initialized) {
+ AddonManager.addAddonListener(this);
+ XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
+ XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
+ this.initialized = true;
+ }
+ },
+
+ onUninstalling(addon) {
+ let extension = GlobalManager.extensionMap.get(addon.id);
+ if (extension) {
+ // Let any other interested listeners respond
+ // (e.g., display the uninstall URL)
+ Management.emit("uninstall", extension);
+ }
+ },
+
+ onUninstalled(addon) {
+ let uuid = UUIDMap.get(addon.id, false);
+ if (!uuid) {
+ return;
+ }
+
+ if (!this.leaveStorage) {
+ // Clear browser.local.storage
+ ExtensionStorage.clear(addon.id);
+
+ // Clear any IndexedDB storage created by the extension
+ let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(
+ baseURI, {addonId: addon.id}
+ );
+ Services.qms.clearStoragesForPrincipal(principal);
+
+ // Clear localStorage created by the extension
+ let attrs = JSON.stringify({addonId: addon.id});
+ Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs);
+ }
+
+ if (!this.leaveUuid) {
+ // Clear the entry in the UUID map
+ UUIDMap.remove(addon.id);
+ }
+ },
+};
+
+UninstallObserver.init();
+
+// Represents the data contained in an extension, contained either
+// in a directory or a zip file, which may or may not be installed.
+// This class implements the functionality of the Extension class,
+// primarily related to manifest parsing and localization, which is
+// useful prior to extension installation or initialization.
+//
+// No functionality of this class is guaranteed to work before
+// |readManifest| has been called, and completed.
+this.ExtensionData = class {
+ constructor(rootURI) {
+ this.rootURI = rootURI;
+
+ this.manifest = null;
+ this.id = null;
+ this.uuid = null;
+ this.localeData = null;
+ this._promiseLocales = null;
+
+ this.apiNames = new Set();
+ this.dependencies = new Set();
+ this.permissions = new Set();
+
+ this.errors = [];
+ }
+
+ get builtinMessages() {
+ return null;
+ }
+
+ get logger() {
+ let id = this.id || "<unknown>";
+ return Log.repository.getLogger(LOGGER_ID_BASE + id);
+ }
+
+ // Report an error about the extension's manifest file.
+ manifestError(message) {
+ this.packagingError(`Reading manifest: ${message}`);
+ }
+
+ // Report an error about the extension's general packaging.
+ packagingError(message) {
+ this.errors.push(message);
+ this.logger.error(`Loading extension '${this.id}': ${message}`);
+ }
+
+ /**
+ * Returns the moz-extension: URL for the given path within this
+ * extension.
+ *
+ * Must not be called unless either the `id` or `uuid` property has
+ * already been set.
+ *
+ * @param {string} path The path portion of the URL.
+ * @returns {string}
+ */
+ getURL(path = "") {
+ if (!(this.id || this.uuid)) {
+ throw new Error("getURL may not be called before an `id` or `uuid` has been set");
+ }
+ if (!this.uuid) {
+ this.uuid = UUIDMap.get(this.id);
+ }
+ return `moz-extension://${this.uuid}/${path}`;
+ }
+
+ readDirectory(path) {
+ return Task.spawn(function* () {
+ if (this.rootURI instanceof Ci.nsIFileURL) {
+ let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
+ let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
+
+ let iter = new OS.File.DirectoryIterator(fullPath);
+ let results = [];
+
+ try {
+ yield iter.forEach(entry => {
+ results.push(entry);
+ });
+ } catch (e) {
+ // Always return a list, even if the directory does not exist (or is
+ // not a directory) for symmetry with the ZipReader behavior.
+ }
+ iter.close();
+
+ return results;
+ }
+
+ // FIXME: We need a way to do this without main thread IO.
+
+ let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
+
+ let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
+ zipReader.open(file);
+ try {
+ let results = [];
+
+ // Normalize the directory path.
+ path = `${uri.JAREntry}/${path}`;
+ path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
+
+ // Escape pattern metacharacters.
+ let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
+
+ let enumerator = zipReader.findEntries(pattern + "*");
+ while (enumerator.hasMore()) {
+ let name = enumerator.getNext();
+ if (!name.startsWith(path)) {
+ throw new Error("Unexpected ZipReader entry");
+ }
+
+ // The enumerator returns the full path of all entries.
+ // Trim off the leading path, and filter out entries from
+ // subdirectories.
+ name = name.slice(path.length);
+ if (name && !/\/./.test(name)) {
+ results.push({
+ name: name.replace("/", ""),
+ isDir: name.endsWith("/"),
+ });
+ }
+ }
+
+ return results;
+ } finally {
+ zipReader.close();
+ }
+ }.bind(this));
+ }
+
+ readJSON(path) {
+ return new Promise((resolve, reject) => {
+ let uri = this.rootURI.resolve(`./${path}`);
+
+ NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ // Convert status code to a string
+ let e = Components.Exception("", status);
+ reject(new Error(`Error while loading '${uri}' (${e.name})`));
+ return;
+ }
+ try {
+ let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
+ {charset: "utf-8"});
+
+ text = text.replace(COMMENT_REGEXP, "$1");
+
+ resolve(JSON.parse(text));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ }
+
+ // Reads the extension's |manifest.json| file, and stores its
+ // parsed contents in |this.manifest|.
+ readManifest() {
+ return Promise.all([
+ this.readJSON("manifest.json"),
+ Management.lazyInit(),
+ ]).then(([manifest]) => {
+ this.manifest = manifest;
+ this.rawManifest = manifest;
+
+ if (manifest && manifest.default_locale) {
+ return this.initLocale();
+ }
+ }).then(() => {
+ let context = {
+ url: this.baseURI && this.baseURI.spec,
+
+ principal: this.principal,
+
+ logError: error => {
+ this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
+ },
+
+ preprocessors: {},
+ };
+
+ if (this.localeData) {
+ context.preprocessors.localize = (value, context) => this.localize(value);
+ }
+
+ let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
+ if (normalized.error) {
+ this.manifestError(normalized.error);
+ } else {
+ this.manifest = normalized.value;
+ }
+
+ try {
+ // Do not override the add-on id that has been already assigned.
+ if (!this.id && this.manifest.applications.gecko.id) {
+ this.id = this.manifest.applications.gecko.id;
+ }
+ } catch (e) {
+ // Errors are handled by the type checks above.
+ }
+
+ let permissions = this.manifest.permissions || [];
+
+ let whitelist = [];
+ for (let perm of permissions) {
+ this.permissions.add(perm);
+
+ let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
+ if (!match) {
+ whitelist.push(perm);
+ } else if (match[1] == "experiments" && match[2]) {
+ this.apiNames.add(match[2]);
+ }
+ }
+ this.whiteListedHosts = new MatchPattern(whitelist);
+
+ for (let api of this.apiNames) {
+ this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
+ }
+
+ return this.manifest;
+ });
+ }
+
+ localizeMessage(...args) {
+ return this.localeData.localizeMessage(...args);
+ }
+
+ localize(...args) {
+ return this.localeData.localize(...args);
+ }
+
+ // If a "default_locale" is specified in that manifest, returns it
+ // as a Gecko-compatible locale string. Otherwise, returns null.
+ get defaultLocale() {
+ if (this.manifest.default_locale != null) {
+ return this.normalizeLocaleCode(this.manifest.default_locale);
+ }
+
+ return null;
+ }
+
+ // Normalizes a Chrome-compatible locale code to the appropriate
+ // Gecko-compatible variant. Currently, this means simply
+ // replacing underscores with hyphens.
+ normalizeLocaleCode(locale) {
+ return String.replace(locale, /_/g, "-");
+ }
+
+ // Reads the locale file for the given Gecko-compatible locale code, and
+ // stores its parsed contents in |this.localeMessages.get(locale)|.
+ readLocaleFile(locale) {
+ return Task.spawn(function* () {
+ let locales = yield this.promiseLocales();
+ let dir = locales.get(locale) || locale;
+ let file = `_locales/${dir}/messages.json`;
+
+ try {
+ let messages = yield this.readJSON(file);
+ return this.localeData.addLocale(locale, messages, this);
+ } catch (e) {
+ this.packagingError(`Loading locale file ${file}: ${e}`);
+ return new Map();
+ }
+ }.bind(this));
+ }
+
+ // Reads the list of locales available in the extension, and returns a
+ // Promise which resolves to a Map upon completion.
+ // Each map key is a Gecko-compatible locale code, and each value is the
+ // "_locales" subdirectory containing that locale:
+ //
+ // Map(gecko-locale-code -> locale-directory-name)
+ promiseLocales() {
+ if (!this._promiseLocales) {
+ this._promiseLocales = Task.spawn(function* () {
+ let locales = new Map();
+
+ let entries = yield this.readDirectory("_locales");
+ for (let file of entries) {
+ if (file.isDir) {
+ let locale = this.normalizeLocaleCode(file.name);
+ locales.set(locale, file.name);
+ }
+ }
+
+ this.localeData = new LocaleData({
+ defaultLocale: this.defaultLocale,
+ locales,
+ builtinMessages: this.builtinMessages,
+ });
+
+ return locales;
+ }.bind(this));
+ }
+
+ return this._promiseLocales;
+ }
+
+ // Reads the locale messages for all locales, and returns a promise which
+ // resolves to a Map of locale messages upon completion. Each key in the map
+ // is a Gecko-compatible locale code, and each value is a locale data object
+ // as returned by |readLocaleFile|.
+ initAllLocales() {
+ return Task.spawn(function* () {
+ let locales = yield this.promiseLocales();
+
+ yield Promise.all(Array.from(locales.keys(),
+ locale => this.readLocaleFile(locale)));
+
+ let defaultLocale = this.defaultLocale;
+ if (defaultLocale) {
+ if (!locales.has(defaultLocale)) {
+ this.manifestError('Value for "default_locale" property must correspond to ' +
+ 'a directory in "_locales/". Not found: ' +
+ JSON.stringify(`_locales/${this.manifest.default_locale}/`));
+ }
+ } else if (locales.size) {
+ this.manifestError('The "default_locale" property is required when a ' +
+ '"_locales/" directory is present.');
+ }
+
+ return this.localeData.messages;
+ }.bind(this));
+ }
+
+ // Reads the locale file for the given Gecko-compatible locale code, or the
+ // default locale if no locale code is given, and sets it as the currently
+ // selected locale on success.
+ //
+ // Pre-loads the default locale for fallback message processing, regardless
+ // of the locale specified.
+ //
+ // If no locales are unavailable, resolves to |null|.
+ initLocale(locale = this.defaultLocale) {
+ return Task.spawn(function* () {
+ if (locale == null) {
+ return null;
+ }
+
+ let promises = [this.readLocaleFile(locale)];
+
+ let {defaultLocale} = this;
+ if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
+ promises.push(this.readLocaleFile(defaultLocale));
+ }
+
+ let results = yield Promise.all(promises);
+
+ this.localeData.selectedLocale = locale;
+ return results[0];
+ }.bind(this));
+ }
+};
+
+let _browserUpdated = false;
+
+const PROXIED_EVENTS = new Set(["test-harness-message"]);
+
+// We create one instance of this class per extension. |addonData|
+// comes directly from bootstrap.js when initializing.
+this.Extension = class extends ExtensionData {
+ constructor(addonData, startupReason) {
+ super(addonData.resourceURI);
+
+ this.uuid = UUIDMap.get(addonData.id);
+ this.instanceId = getUniqueId();
+
+ this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+ Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ if (addonData.cleanupFile) {
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.cleanupFile = addonData.cleanupFile || null;
+ delete addonData.cleanupFile;
+ }
+
+ this.addonData = addonData;
+ this.startupReason = startupReason;
+
+ this.id = addonData.id;
+ this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
+ this.principal = this.createPrincipal();
+
+ this.onStartup = null;
+
+ this.hasShutdown = false;
+ this.onShutdown = new Set();
+
+ this.uninstallURL = null;
+
+ this.apis = [];
+ this.whiteListedHosts = null;
+ this.webAccessibleResources = null;
+
+ this.emitter = new EventEmitter();
+ }
+
+ static set browserUpdated(updated) {
+ _browserUpdated = updated;
+ }
+
+ static get browserUpdated() {
+ return _browserUpdated;
+ }
+
+ static generateXPI(data) {
+ return ExtensionTestCommon.generateXPI(data);
+ }
+
+ static generateZipFile(files, baseName = "generated-extension.xpi") {
+ return ExtensionTestCommon.generateZipFile(files, baseName);
+ }
+
+ static generate(data) {
+ return ExtensionTestCommon.generate(data);
+ }
+
+ on(hook, f) {
+ return this.emitter.on(hook, f);
+ }
+
+ off(hook, f) {
+ return this.emitter.off(hook, f);
+ }
+
+ emit(event, ...args) {
+ if (PROXIED_EVENTS.has(event)) {
+ Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
+ }
+
+ return this.emitter.emit(event, ...args);
+ }
+
+ receiveMessage({name, data}) {
+ if (name === this.MESSAGE_EMIT_EVENT) {
+ this.emitter.emit(data.event, ...data.args);
+ }
+ }
+
+ testMessage(...args) {
+ this.emit("test-harness-message", ...args);
+ }
+
+ createPrincipal(uri = this.baseURI) {
+ return Services.scriptSecurityManager.createCodebasePrincipal(
+ uri, {addonId: this.id});
+ }
+
+ // Checks that the given URL is a child of our baseURI.
+ isExtensionURL(url) {
+ let uri = Services.io.newURI(url, null, null);
+
+ let common = this.baseURI.getCommonBaseSpec(uri);
+ return common == this.baseURI.spec;
+ }
+
+ readManifest() {
+ return super.readManifest().then(manifest => {
+ if (AppConstants.RELEASE_OR_BETA) {
+ return manifest;
+ }
+
+ // Load Experiments APIs that this extension depends on.
+ return Promise.all(
+ Array.from(this.apiNames, api => ExtensionAPIs.load(api))
+ ).then(apis => {
+ for (let API of apis) {
+ this.apis.push(new API(this));
+ }
+
+ return manifest;
+ });
+ });
+ }
+
+ // Representation of the extension to send to content
+ // processes. This should include anything the content process might
+ // need.
+ serialize() {
+ return {
+ id: this.id,
+ uuid: this.uuid,
+ instanceId: this.instanceId,
+ manifest: this.manifest,
+ resourceURL: this.addonData.resourceURI.spec,
+ baseURL: this.baseURI.spec,
+ content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
+ webAccessibleResources: this.webAccessibleResources.serialize(),
+ whiteListedHosts: this.whiteListedHosts.serialize(),
+ localeData: this.localeData.serialize(),
+ permissions: this.permissions,
+ principal: this.principal,
+ };
+ }
+
+ broadcast(msg, data) {
+ return new Promise(resolve => {
+ let count = Services.ppmm.childCount;
+ Services.ppmm.addMessageListener(msg + "Complete", function listener() {
+ count--;
+ if (count == 0) {
+ Services.ppmm.removeMessageListener(msg + "Complete", listener);
+ resolve();
+ }
+ });
+ Services.ppmm.broadcastAsyncMessage(msg, data);
+ });
+ }
+
+ runManifest(manifest) {
+ // Strip leading slashes from web_accessible_resources.
+ let strippedWebAccessibleResources = [];
+ if (manifest.web_accessible_resources) {
+ strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
+ }
+
+ this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
+
+ let promises = [];
+ for (let directive in manifest) {
+ if (manifest[directive] !== null) {
+ promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
+ }
+ }
+
+ let data = Services.ppmm.initialProcessData;
+ if (!data["Extension:Extensions"]) {
+ data["Extension:Extensions"] = [];
+ }
+ let serial = this.serialize();
+ data["Extension:Extensions"].push(serial);
+
+ return this.broadcast("Extension:Startup", serial).then(() => {
+ return Promise.all(promises);
+ });
+ }
+
+ callOnClose(obj) {
+ this.onShutdown.add(obj);
+ }
+
+ forgetOnClose(obj) {
+ this.onShutdown.delete(obj);
+ }
+
+ get builtinMessages() {
+ return new Map([
+ ["@@extension_id", this.uuid],
+ ]);
+ }
+
+ // Reads the locale file for the given Gecko-compatible locale code, or if
+ // no locale is given, the available locale closest to the UI locale.
+ // Sets the currently selected locale on success.
+ initLocale(locale = undefined) {
+ // Ugh.
+ let super_ = super.initLocale.bind(this);
+
+ return Task.spawn(function* () {
+ if (locale === undefined) {
+ let locales = yield this.promiseLocales();
+
+ let localeList = Array.from(locales.keys(), locale => {
+ return {name: locale, locales: [locale]};
+ });
+
+ let match = Locale.findClosestLocale(localeList);
+ locale = match ? match.name : this.defaultLocale;
+ }
+
+ return super_(locale);
+ }.bind(this));
+ }
+
+ startup() {
+ let started = false;
+ return this.readManifest().then(() => {
+ ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
+ started = true;
+
+ if (!this.hasShutdown) {
+ return this.initLocale();
+ }
+ }).then(() => {
+ if (this.errors.length) {
+ return Promise.reject({errors: this.errors});
+ }
+
+ if (this.hasShutdown) {
+ return;
+ }
+
+ GlobalManager.init(this);
+
+ // The "startup" Management event sent on the extension instance itself
+ // is emitted just before the Management "startup" event,
+ // and it is used to run code that needs to be executed before
+ // any of the "startup" listeners.
+ this.emit("startup", this);
+ Management.emit("startup", this);
+
+ return this.runManifest(this.manifest);
+ }).then(() => {
+ Management.emit("ready", this);
+ }).catch(e => {
+ dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
+ Cu.reportError(e);
+
+ if (started) {
+ ExtensionManagement.shutdownExtension(this.uuid);
+ }
+
+ this.cleanupGeneratedFile();
+
+ throw e;
+ });
+ }
+
+ cleanupGeneratedFile() {
+ if (!this.cleanupFile) {
+ return;
+ }
+
+ let file = this.cleanupFile;
+ this.cleanupFile = null;
+
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+
+ this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
+ // We can't delete this file until everyone using it has
+ // closed it (because Windows is dumb). So we wait for all the
+ // child processes (including the parent) to flush their JAR
+ // caches. These caches may keep the file open.
+ file.remove(false);
+ });
+ }
+
+ shutdown() {
+ this.hasShutdown = true;
+
+ Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ if (!this.manifest) {
+ ExtensionManagement.shutdownExtension(this.uuid);
+
+ this.cleanupGeneratedFile();
+ return;
+ }
+
+ GlobalManager.uninit(this);
+
+ for (let obj of this.onShutdown) {
+ obj.close();
+ }
+
+ for (let api of this.apis) {
+ api.destroy();
+ }
+
+ ParentAPIManager.shutdownExtension(this.id);
+
+ Management.emit("shutdown", this);
+
+ Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
+
+ MessageChannel.abortResponses({extensionId: this.id});
+
+ ExtensionManagement.shutdownExtension(this.uuid);
+
+ this.cleanupGeneratedFile();
+ }
+
+ observe(subject, topic, data) {
+ if (topic == "xpcom-shutdown") {
+ this.cleanupGeneratedFile();
+ }
+ }
+
+ hasPermission(perm) {
+ let match = /^manifest:(.*)/.exec(perm);
+ if (match) {
+ return this.manifest[match[1]] != null;
+ }
+
+ return this.permissions.has(perm);
+ }
+
+ get name() {
+ return this.manifest.name;
+ }
+};
diff --git a/toolkit/components/extensions/ExtensionAPI.jsm b/toolkit/components/extensions/ExtensionAPI.jsm
new file mode 100644
index 0000000000..54dab8e3b6
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionAPI.jsm
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionAPI", "ExtensionAPIs"];
+
+/* exported ExtensionAPIs */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const global = this;
+
+class ExtensionAPI {
+ constructor(extension) {
+ this.extension = extension;
+ }
+
+ destroy() {
+ }
+
+ getAPI(context) {
+ throw new Error("Not Implemented");
+ }
+}
+
+var ExtensionAPIs = {
+ apis: ExtensionManagement.APIs.apis,
+
+ load(apiName) {
+ let api = this.apis.get(apiName);
+
+ if (api.loadPromise) {
+ return api.loadPromise;
+ }
+
+ let {script, schema} = api;
+
+ let addonId = `${apiName}@experiments.addons.mozilla.org`;
+ api.sandbox = Cu.Sandbox(global, {
+ wantXrays: false,
+ sandboxName: script,
+ addonId,
+ metadata: {addonID: addonId},
+ });
+
+ api.sandbox.ExtensionAPI = ExtensionAPI;
+
+ Services.scriptloader.loadSubScript(script, api.sandbox, "UTF-8");
+
+ api.loadPromise = Schemas.load(schema).then(() => {
+ return Cu.evalInSandbox("API", api.sandbox);
+ });
+
+ return api.loadPromise;
+ },
+
+ unload(apiName) {
+ let api = this.apis.get(apiName);
+
+ let {schema} = api;
+
+ Schemas.unload(schema);
+ Cu.nukeSandbox(api.sandbox);
+
+ api.sandbox = null;
+ api.loadPromise = null;
+ },
+};
diff --git a/toolkit/components/extensions/ExtensionChild.jsm b/toolkit/components/extensions/ExtensionChild.jsm
new file mode 100644
index 0000000000..c953dd685b
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -0,0 +1,1040 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionChild"];
+
+/*
+ * This file handles addon logic that is independent of the chrome process.
+ * When addons run out-of-process, this is the main entry point.
+ * Its primary function is managing addon globals.
+ *
+ * Don't put contentscript logic here, use ExtensionContent.jsm instead.
+ */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
+ "resource://gre/modules/NativeMessaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+
+const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+const {
+ DefaultMap,
+ EventManager,
+ SingletonEventManager,
+ SpreadArgs,
+ defineLazyGetter,
+ getInnerWindowID,
+ getMessageManager,
+ getUniqueId,
+ injectAPI,
+} = ExtensionUtils;
+
+const {
+ BaseContext,
+ LocalAPIImplementation,
+ SchemaAPIInterface,
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+var ExtensionChild;
+
+/**
+ * Abstraction for a Port object in the extension API.
+ *
+ * @param {BaseContext} context The context that owns this port.
+ * @param {nsIMessageSender} senderMM The message manager to send messages to.
+ * @param {Array<nsIMessageListenerManager>} receiverMMs Message managers to
+ * listen on.
+ * @param {string} name Arbitrary port name as defined by the addon.
+ * @param {string} id An ID that uniquely identifies this port's channel.
+ * @param {object} sender The `port.sender` property.
+ * @param {object} recipient The recipient of messages sent from this port.
+ */
+class Port {
+ constructor(context, senderMM, receiverMMs, name, id, sender, recipient) {
+ this.context = context;
+ this.senderMM = senderMM;
+ this.receiverMMs = receiverMMs;
+ this.name = name;
+ this.id = id;
+ this.sender = sender;
+ this.recipient = recipient;
+ this.disconnected = false;
+ this.disconnectListeners = new Set();
+ this.unregisterMessageFuncs = new Set();
+
+ // Common options for onMessage and onDisconnect.
+ this.handlerBase = {
+ messageFilterStrict: {portId: id},
+
+ filterMessage: (sender, recipient) => {
+ return sender.contextId !== this.context.contextId;
+ },
+ };
+
+ this.disconnectHandler = Object.assign({
+ receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
+ }, this.handlerBase);
+
+ MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
+
+ this.context.callOnClose(this);
+ }
+
+ api() {
+ let portObj = Cu.createObjectIn(this.context.cloneScope);
+
+ let portError = null;
+ let publicAPI = {
+ name: this.name,
+
+ disconnect: () => {
+ this.disconnect();
+ },
+
+ postMessage: json => {
+ this.postMessage(json);
+ },
+
+ onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
+ return this.registerOnDisconnect(error => {
+ portError = error && this.context.normalizeError(error);
+ fire.withoutClone(portObj);
+ });
+ }).api(),
+
+ onMessage: new EventManager(this.context, "Port.onMessage", fire => {
+ return this.registerOnMessage(msg => {
+ msg = Cu.cloneInto(msg, this.context.cloneScope);
+ fire.withoutClone(msg, portObj);
+ });
+ }).api(),
+
+ get error() {
+ return portError;
+ },
+ };
+
+ if (this.sender) {
+ publicAPI.sender = this.sender;
+ }
+
+ injectAPI(publicAPI, portObj);
+ return portObj;
+ }
+
+ postMessage(json) {
+ if (this.disconnected) {
+ throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
+ }
+
+ this._sendMessage("Extension:Port:PostMessage", json);
+ }
+
+ /**
+ * Register a callback that is called when the port is disconnected by the
+ * *other* end. The callback is automatically unregistered when the port or
+ * context is closed.
+ *
+ * @param {function} callback Called when the other end disconnects the port.
+ * If the disconnect is caused by an error, the first parameter is an
+ * object with a "message" string property that describes the cause.
+ * @returns {function} Function to unregister the listener.
+ */
+ registerOnDisconnect(callback) {
+ let listener = error => {
+ if (this.context.active && !this.disconnected) {
+ callback(error);
+ }
+ };
+ this.disconnectListeners.add(listener);
+ return () => {
+ this.disconnectListeners.delete(listener);
+ };
+ }
+
+ /**
+ * Register a callback that is called when a message is received. The callback
+ * is automatically unregistered when the port or context is closed.
+ *
+ * @param {function} callback Called when a message is received.
+ * @returns {function} Function to unregister the listener.
+ */
+ registerOnMessage(callback) {
+ let handler = Object.assign({
+ receiveMessage: ({data}) => {
+ if (this.context.active && !this.disconnected) {
+ callback(data);
+ }
+ },
+ }, this.handlerBase);
+
+ let unregister = () => {
+ this.unregisterMessageFuncs.delete(unregister);
+ MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
+ };
+ MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
+ this.unregisterMessageFuncs.add(unregister);
+ return unregister;
+ }
+
+ _sendMessage(message, data) {
+ let options = {
+ recipient: Object.assign({}, this.recipient, {portId: this.id}),
+ responseType: MessageChannel.RESPONSE_NONE,
+ };
+
+ return this.context.sendMessage(this.senderMM, message, data, options);
+ }
+
+ handleDisconnection() {
+ MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
+ for (let unregister of this.unregisterMessageFuncs) {
+ unregister();
+ }
+ this.context.forgetOnClose(this);
+ this.disconnected = true;
+ }
+
+ /**
+ * Disconnect the port from the other end (which may not even exist).
+ *
+ * @param {Error|{message: string}} [error] The reason for disconnecting,
+ * if it is an abnormal disconnect.
+ */
+ disconnectByOtherEnd(error = null) {
+ if (this.disconnected) {
+ return;
+ }
+
+ for (let listener of this.disconnectListeners) {
+ listener(error);
+ }
+
+ this.handleDisconnection();
+ }
+
+ /**
+ * Disconnect the port from this end.
+ *
+ * @param {Error|{message: string}} [error] The reason for disconnecting,
+ * if it is an abnormal disconnect.
+ */
+ disconnect(error = null) {
+ if (this.disconnected) {
+ // disconnect() may be called without side effects even after the port is
+ // closed - https://developer.chrome.com/extensions/runtime#type-Port
+ return;
+ }
+ this.handleDisconnection();
+ if (error) {
+ error = {message: this.context.normalizeError(error).message};
+ }
+ this._sendMessage("Extension:Port:Disconnect", error);
+ }
+
+ close() {
+ this.disconnect();
+ }
+}
+
+class NativePort extends Port {
+ postMessage(data) {
+ data = NativeApp.encodeMessage(this.context, data);
+
+ return super.postMessage(data);
+ }
+}
+
+/**
+ * Each extension context gets its own Messenger object. It handles the
+ * basics of sendMessage, onMessage, connect and onConnect.
+ *
+ * @param {BaseContext} context The context to which this Messenger is tied.
+ * @param {Array<nsIMessageListenerManager>} messageManagers
+ * The message managers used to receive messages (e.g. onMessage/onConnect
+ * requests).
+ * @param {object} sender Describes this sender to the recipient. This object
+ * is extended further by BaseContext's sendMessage method and appears as
+ * the `sender` object to `onConnect` and `onMessage`.
+ * Do not set the `extensionId`, `contextId` or `tab` properties. The former
+ * two are added by BaseContext's sendMessage, while `sender.tab` is set by
+ * the ProxyMessenger in the main process.
+ * @param {object} filter A recipient filter to apply to incoming messages from
+ * the broker. Messages are only handled by this Messenger if all key-value
+ * pairs match the `recipient` as specified by the sender of the message.
+ * In other words, this filter defines the required fields of `recipient`.
+ * @param {object} [optionalFilter] An additional filter to apply to incoming
+ * messages. Unlike `filter`, the keys from `optionalFilter` are allowed to
+ * be omitted from `recipient`. Only keys that are present in both
+ * `optionalFilter` and `recipient` are applied to filter incoming messages.
+ */
+class Messenger {
+ constructor(context, messageManagers, sender, filter, optionalFilter) {
+ this.context = context;
+ this.messageManagers = messageManagers;
+ this.sender = sender;
+ this.filter = filter;
+ this.optionalFilter = optionalFilter;
+ }
+
+ _sendMessage(messageManager, message, data, recipient) {
+ let options = {
+ recipient,
+ sender: this.sender,
+ responseType: MessageChannel.RESPONSE_FIRST,
+ };
+
+ return this.context.sendMessage(messageManager, message, data, options);
+ }
+
+ sendMessage(messageManager, msg, recipient, responseCallback) {
+ let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
+ .catch(error => {
+ if (error.result == MessageChannel.RESULT_NO_HANDLER) {
+ return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
+ } else if (error.result != MessageChannel.RESULT_NO_RESPONSE) {
+ return Promise.reject({message: error.message});
+ }
+ });
+
+ return this.context.wrapPromise(promise, responseCallback);
+ }
+
+ sendNativeMessage(messageManager, msg, recipient, responseCallback) {
+ msg = NativeApp.encodeMessage(this.context, msg);
+ return this.sendMessage(messageManager, msg, recipient, responseCallback);
+ }
+
+ onMessage(name) {
+ return new SingletonEventManager(this.context, name, callback => {
+ let listener = {
+ messageFilterPermissive: this.optionalFilter,
+ messageFilterStrict: this.filter,
+
+ filterMessage: (sender, recipient) => {
+ // Ignore the message if it was sent by this Messenger.
+ return sender.contextId !== this.context.contextId;
+ },
+
+ receiveMessage: ({target, data: message, sender, recipient}) => {
+ if (!this.context.active) {
+ return;
+ }
+
+ let sendResponse;
+ let response = undefined;
+ let promise = new Promise(resolve => {
+ sendResponse = value => {
+ resolve(value);
+ response = promise;
+ };
+ });
+
+ message = Cu.cloneInto(message, this.context.cloneScope);
+ sender = Cu.cloneInto(sender, this.context.cloneScope);
+ sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
+
+ // Note: We intentionally do not use runSafe here so that any
+ // errors are propagated to the message sender.
+ let result = callback(message, sender, sendResponse);
+ if (result instanceof this.context.cloneScope.Promise) {
+ return result;
+ } else if (result === true) {
+ return promise;
+ }
+ return response;
+ },
+ };
+
+ MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
+ return () => {
+ MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
+ };
+ }).api();
+ }
+
+ _connect(messageManager, port, recipient) {
+ let msg = {
+ name: port.name,
+ portId: port.id,
+ };
+
+ this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => {
+ if (error.result === MessageChannel.RESULT_NO_HANDLER) {
+ error = {message: "Could not establish connection. Receiving end does not exist."};
+ } else if (error.result === MessageChannel.RESULT_DISCONNECTED) {
+ error = null;
+ }
+ port.disconnectByOtherEnd(error);
+ });
+
+ return port.api();
+ }
+
+ connect(messageManager, name, recipient) {
+ let portId = getUniqueId();
+
+ let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
+
+ return this._connect(messageManager, port, recipient);
+ }
+
+ connectNative(messageManager, name, recipient) {
+ let portId = getUniqueId();
+
+ let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
+
+ return this._connect(messageManager, port, recipient);
+ }
+
+ onConnect(name) {
+ return new SingletonEventManager(this.context, name, callback => {
+ let listener = {
+ messageFilterPermissive: this.optionalFilter,
+ messageFilterStrict: this.filter,
+
+ filterMessage: (sender, recipient) => {
+ // Ignore the port if it was created by this Messenger.
+ return sender.contextId !== this.context.contextId;
+ },
+
+ receiveMessage: ({target, data: message, sender}) => {
+ let {name, portId} = message;
+ let mm = getMessageManager(target);
+ let recipient = Object.assign({}, sender);
+ if (recipient.tab) {
+ recipient.tabId = recipient.tab.id;
+ delete recipient.tab;
+ }
+ let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
+ this.context.runSafeWithoutClone(callback, port.api());
+ return true;
+ },
+ };
+
+ MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
+ return () => {
+ MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
+ };
+ }).api();
+ }
+}
+
+var apiManager = new class extends SchemaAPIManager {
+ constructor() {
+ super("addon");
+ this.initialized = false;
+ }
+
+ generateAPIs(...args) {
+ if (!this.initialized) {
+ this.initialized = true;
+ for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_ADDON)) {
+ this.loadScript(value);
+ }
+ }
+ return super.generateAPIs(...args);
+ }
+
+ registerSchemaAPI(namespace, envType, getAPI) {
+ if (envType == "addon_child") {
+ super.registerSchemaAPI(namespace, envType, getAPI);
+ }
+ }
+}();
+
+/**
+ * An object that runs an remote implementation of an API.
+ */
+class ProxyAPIImplementation extends SchemaAPIInterface {
+ /**
+ * @param {string} namespace The full path to the namespace that contains the
+ * `name` member. This may contain dots, e.g. "storage.local".
+ * @param {string} name The name of the method or property.
+ * @param {ChildAPIManager} childApiManager The owner of this implementation.
+ */
+ constructor(namespace, name, childApiManager) {
+ super();
+ this.path = `${namespace}.${name}`;
+ this.childApiManager = childApiManager;
+ }
+
+ callFunctionNoReturn(args) {
+ this.childApiManager.callParentFunctionNoReturn(this.path, args);
+ }
+
+ callAsyncFunction(args, callback) {
+ return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
+ }
+
+ addListener(listener, args) {
+ let map = this.childApiManager.listeners.get(this.path);
+
+ if (map.listeners.has(listener)) {
+ // TODO: Called with different args?
+ return;
+ }
+
+ let id = getUniqueId();
+
+ map.ids.set(id, listener);
+ map.listeners.set(listener, id);
+
+ this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
+ childId: this.childApiManager.id,
+ listenerId: id,
+ path: this.path,
+ args,
+ });
+ }
+
+ removeListener(listener) {
+ let map = this.childApiManager.listeners.get(this.path);
+
+ if (!map.listeners.has(listener)) {
+ return;
+ }
+
+ let id = map.listeners.get(listener);
+ map.listeners.delete(listener);
+ map.ids.delete(id);
+
+ this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
+ childId: this.childApiManager.id,
+ listenerId: id,
+ path: this.path,
+ });
+ }
+
+ hasListener(listener) {
+ let map = this.childApiManager.listeners.get(this.path);
+ return map.listeners.has(listener);
+ }
+}
+
+// We create one instance of this class for every extension context that
+// needs to use remote APIs. It uses the message manager to communicate
+// with the ParentAPIManager singleton in ExtensionParent.jsm. It
+// handles asynchronous function calls as well as event listeners.
+class ChildAPIManager {
+ constructor(context, messageManager, localApis, contextData) {
+ this.context = context;
+ this.messageManager = messageManager;
+ this.url = contextData.url;
+
+ // The root namespace of all locally implemented APIs. If an extension calls
+ // an API that does not exist in this object, then the implementation is
+ // delegated to the ParentAPIManager.
+ this.localApis = localApis;
+
+ this.id = `${context.extension.id}.${context.contextId}`;
+
+ MessageChannel.addListener(messageManager, "API:RunListener", this);
+ messageManager.addMessageListener("API:CallResult", this);
+
+ this.messageFilterStrict = {childId: this.id};
+
+ this.listeners = new DefaultMap(() => ({
+ ids: new Map(),
+ listeners: new Map(),
+ }));
+
+ // Map[callId -> Deferred]
+ this.callPromises = new Map();
+
+ let params = {
+ childId: this.id,
+ extensionId: context.extension.id,
+ principal: context.principal,
+ };
+ Object.assign(params, contextData);
+
+ this.messageManager.sendAsyncMessage("API:CreateProxyContext", params);
+ }
+
+ receiveMessage({name, messageName, data}) {
+ if (data.childId != this.id) {
+ return;
+ }
+
+ switch (name || messageName) {
+ case "API:RunListener":
+ let map = this.listeners.get(data.path);
+ let listener = map.ids.get(data.listenerId);
+
+ if (listener) {
+ return this.context.runSafe(listener, ...data.args);
+ }
+
+ Cu.reportError(`Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`);
+ break;
+
+ case "API:CallResult":
+ let deferred = this.callPromises.get(data.callId);
+ if ("error" in data) {
+ deferred.reject(data.error);
+ } else {
+ deferred.resolve(new SpreadArgs(data.result));
+ }
+ this.callPromises.delete(data.callId);
+ break;
+ }
+ }
+
+ /**
+ * Call a function in the parent process and ignores its return value.
+ *
+ * @param {string} path The full name of the method, e.g. "tabs.create".
+ * @param {Array} args The parameters for the function.
+ */
+ callParentFunctionNoReturn(path, args) {
+ this.messageManager.sendAsyncMessage("API:Call", {
+ childId: this.id,
+ path,
+ args,
+ });
+ }
+
+ /**
+ * Calls a function in the parent process and returns its result
+ * asynchronously.
+ *
+ * @param {string} path The full name of the method, e.g. "tabs.create".
+ * @param {Array} args The parameters for the function.
+ * @param {function(*)} [callback] The callback to be called when the function
+ * completes.
+ * @returns {Promise|undefined} Must be void if `callback` is set, and a
+ * promise otherwise. The promise is resolved when the function completes.
+ */
+ callParentAsyncFunction(path, args, callback) {
+ let callId = getUniqueId();
+ let deferred = PromiseUtils.defer();
+ this.callPromises.set(callId, deferred);
+
+ this.messageManager.sendAsyncMessage("API:Call", {
+ childId: this.id,
+ callId,
+ path,
+ args,
+ });
+
+ return this.context.wrapPromise(deferred.promise, callback);
+ }
+
+ /**
+ * Create a proxy for an event in the parent process. The returned event
+ * object shares its internal state with other instances. For instance, if
+ * `removeListener` is used on a listener that was added on another object
+ * through `addListener`, then the event is unregistered.
+ *
+ * @param {string} path The full name of the event, e.g. "tabs.onCreated".
+ * @returns {object} An object with the addListener, removeListener and
+ * hasListener methods. See SchemaAPIInterface for documentation.
+ */
+ getParentEvent(path) {
+ path = path.split(".");
+
+ let name = path.pop();
+ let namespace = path.join(".");
+
+ let impl = new ProxyAPIImplementation(namespace, name, this);
+ return {
+ addListener: (listener, ...args) => impl.addListener(listener, args),
+ removeListener: (listener) => impl.removeListener(listener),
+ hasListener: (listener) => impl.hasListener(listener),
+ };
+ }
+
+ close() {
+ this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
+ }
+
+ get cloneScope() {
+ return this.context.cloneScope;
+ }
+
+ get principal() {
+ return this.context.principal;
+ }
+
+ shouldInject(namespace, name, allowedContexts) {
+ // Do not generate content script APIs, unless explicitly allowed.
+ if (this.context.envType === "content_child" &&
+ !allowedContexts.includes("content")) {
+ return false;
+ }
+ if (allowedContexts.includes("addon_parent_only")) {
+ return false;
+ }
+ return true;
+ }
+
+ getImplementation(namespace, name) {
+ let obj = namespace.split(".").reduce(
+ (object, prop) => object && object[prop],
+ this.localApis);
+
+ if (obj && name in obj) {
+ return new LocalAPIImplementation(obj, name, this.context);
+ }
+
+ return this.getFallbackImplementation(namespace, name);
+ }
+
+ getFallbackImplementation(namespace, name) {
+ // No local API found, defer implementation to the parent.
+ return new ProxyAPIImplementation(namespace, name, this);
+ }
+
+ hasPermission(permission) {
+ return this.context.extension.hasPermission(permission);
+ }
+}
+
+class ExtensionPageContextChild extends BaseContext {
+ /**
+ * This ExtensionPageContextChild represents a privileged addon
+ * execution environment that has full access to the WebExtensions
+ * APIs (provided that the correct permissions have been requested).
+ *
+ * This is the child side of the ExtensionPageContextParent class
+ * defined in ExtensionParent.jsm.
+ *
+ * @param {BrowserExtensionContent} extension This context's owner.
+ * @param {object} params
+ * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
+ * @param {string} params.viewType One of "background", "popup" or "tab".
+ * "background" and "tab" are used by `browser.extension.getViews`.
+ * "popup" is only used internally to identify page action and browser
+ * action popups and options_ui pages.
+ * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
+ */
+ constructor(extension, params) {
+ super("addon_child", extension);
+ if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ // This check is temporary. It should be removed once the proxy creation
+ // is asynchronous.
+ throw new Error("ExtensionPageContextChild cannot be created in child processes");
+ }
+
+ let {viewType, uri, contentWindow, tabId} = params;
+ this.viewType = viewType;
+ this.uri = uri || extension.baseURI;
+
+ this.setContentWindow(contentWindow);
+
+ // This is the MessageSender property passed to extension.
+ // It can be augmented by the "page-open" hook.
+ let sender = {id: extension.uuid};
+ if (viewType == "tab") {
+ sender.tabId = tabId;
+ this.tabId = tabId;
+ }
+ if (uri) {
+ sender.url = uri.spec;
+ }
+ this.sender = sender;
+
+ Schemas.exportLazyGetter(contentWindow, "browser", () => {
+ let browserObj = Cu.createObjectIn(contentWindow);
+ Schemas.inject(browserObj, this.childManager);
+ return browserObj;
+ });
+
+ Schemas.exportLazyGetter(contentWindow, "chrome", () => {
+ let chromeApiWrapper = Object.create(this.childManager);
+ chromeApiWrapper.isChromeCompat = true;
+
+ let chromeObj = Cu.createObjectIn(contentWindow);
+ Schemas.inject(chromeObj, chromeApiWrapper);
+ return chromeObj;
+ });
+
+ this.extension.views.add(this);
+ }
+
+ get cloneScope() {
+ return this.contentWindow;
+ }
+
+ get principal() {
+ return this.contentWindow.document.nodePrincipal;
+ }
+
+ get windowId() {
+ if (this.viewType == "tab" || this.viewType == "popup") {
+ let globalView = ExtensionChild.contentGlobals.get(this.messageManager);
+ return globalView ? globalView.windowId : -1;
+ }
+ }
+
+ // Called when the extension shuts down.
+ shutdown() {
+ this.unload();
+ }
+
+ // This method is called when an extension page navigates away or
+ // its tab is closed.
+ unload() {
+ // Note that without this guard, we end up running unload code
+ // multiple times for tab pages closed by the "page-unload" handlers
+ // triggered below.
+ if (this.unloaded) {
+ return;
+ }
+
+ if (this.contentWindow) {
+ this.contentWindow.close();
+ }
+
+ super.unload();
+ this.extension.views.delete(this);
+ }
+}
+
+defineLazyGetter(ExtensionPageContextChild.prototype, "messenger", function() {
+ let filter = {extensionId: this.extension.id};
+ let optionalFilter = {};
+ // Addon-generated messages (not necessarily from the same process as the
+ // addon itself) are sent to the main process, which forwards them via the
+ // parent process message manager. Specific replies can be sent to the frame
+ // message manager.
+ return new Messenger(this, [Services.cpmm, this.messageManager], this.sender,
+ filter, optionalFilter);
+});
+
+defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() {
+ let localApis = {};
+ apiManager.generateAPIs(this, localApis);
+
+ if (this.viewType == "background") {
+ apiManager.global.initializeBackgroundPage(this.contentWindow);
+ }
+
+ let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+ envType: "addon_parent",
+ viewType: this.viewType,
+ url: this.uri.spec,
+ incognito: this.incognito,
+ });
+
+ this.callOnClose(childManager);
+
+ return childManager;
+});
+
+// All subframes in a tab, background page, popup, etc. have the same view type.
+// This class keeps track of such global state.
+// Note that this is created even for non-extension tabs because at present we
+// do not have a way to distinguish regular tabs from extension tabs at the
+// initialization of a frame script.
+class ContentGlobal {
+ /**
+ * @param {nsIContentFrameMessageManager} global The frame script's global.
+ */
+ constructor(global) {
+ this.global = global;
+ // Unless specified otherwise assume that the extension page is in a tab,
+ // because the majority of all class instances are going to be a tab. Any
+ // special views (background page, extension popup) will immediately send an
+ // Extension:InitExtensionView message to change the viewType.
+ this.viewType = "tab";
+ this.tabId = -1;
+ this.windowId = -1;
+ this.initialized = false;
+ this.global.addMessageListener("Extension:InitExtensionView", this);
+ this.global.addMessageListener("Extension:SetTabAndWindowId", this);
+
+ this.initialDocuments = new WeakSet();
+ }
+
+ uninit() {
+ this.global.removeMessageListener("Extension:InitExtensionView", this);
+ this.global.removeMessageListener("Extension:SetTabAndWindowId", this);
+ this.global.removeEventListener("DOMContentLoaded", this);
+ }
+
+ ensureInitialized() {
+ if (!this.initialized) {
+ // Request tab and window ID in case "Extension:InitExtensionView" is not
+ // sent (e.g. when `viewType` is "tab").
+ let reply = this.global.sendSyncMessage("Extension:GetTabAndWindowId");
+ this.handleSetTabAndWindowId(reply[0] || {});
+ }
+ return this;
+ }
+
+ receiveMessage({name, data}) {
+ switch (name) {
+ case "Extension:InitExtensionView":
+ // The view type is initialized once and then fixed.
+ this.global.removeMessageListener("Extension:InitExtensionView", this);
+ let {viewType, url} = data;
+ this.viewType = viewType;
+ this.global.addEventListener("DOMContentLoaded", this);
+ if (url) {
+ // TODO(robwu): Remove this check. It is only here because the popup
+ // implementation does not always load a URL at the initialization,
+ // and the logic is too complex to fix at once.
+ let {document} = this.global.content;
+ this.initialDocuments.add(document);
+ document.location.replace(url);
+ }
+ /* Falls through to allow these properties to be initialized at once */
+ case "Extension:SetTabAndWindowId":
+ this.handleSetTabAndWindowId(data);
+ break;
+ }
+ }
+
+ handleSetTabAndWindowId(data) {
+ let {tabId, windowId} = data;
+ if (tabId) {
+ // Tab IDs are not expected to change.
+ if (this.tabId !== -1 && tabId !== this.tabId) {
+ throw new Error("Attempted to change a tabId after it was set");
+ }
+ this.tabId = tabId;
+ }
+ if (windowId !== undefined) {
+ // Window IDs may change if a tab is moved to a different location.
+ // Note: This is the ID of the browser window for the extension API.
+ // Do not confuse it with the innerWindowID of DOMWindows!
+ this.windowId = windowId;
+ }
+ this.initialized = true;
+ }
+
+ // "DOMContentLoaded" event.
+ handleEvent(event) {
+ let {document} = this.global.content;
+ if (event.target === document) {
+ // If the document was still being loaded at the time of navigation, then
+ // the DOMContentLoaded event is fired for the old document. Ignore it.
+ if (this.initialDocuments.has(document)) {
+ this.initialDocuments.delete(document);
+ return;
+ }
+ this.global.removeEventListener("DOMContentLoaded", this);
+ this.global.sendAsyncMessage("Extension:ExtensionViewLoaded");
+ }
+ }
+}
+
+ExtensionChild = {
+ // Map<nsIContentFrameMessageManager, ContentGlobal>
+ contentGlobals: new Map(),
+
+ // Map<innerWindowId, ExtensionPageContextChild>
+ extensionContexts: new Map(),
+
+ initOnce() {
+ // This initializes the default message handler for messages targeted at
+ // an addon process, in case the addon process receives a message before
+ // its Messenger has been instantiated. For example, if a content script
+ // sends a message while there is no background page.
+ MessageChannel.setupMessageManagers([Services.cpmm]);
+ },
+
+ init(global) {
+ this.contentGlobals.set(global, new ContentGlobal(global));
+ },
+
+ uninit(global) {
+ this.contentGlobals.get(global).uninit();
+ this.contentGlobals.delete(global);
+ },
+
+ /**
+ * Create a privileged context at document-element-inserted.
+ *
+ * @param {BrowserExtensionContent} extension
+ * The extension for which the context should be created.
+ * @param {nsIDOMWindow} contentWindow The global of the page.
+ */
+ createExtensionContext(extension, contentWindow) {
+ let windowId = getInnerWindowID(contentWindow);
+ let context = this.extensionContexts.get(windowId);
+ if (context) {
+ if (context.extension !== extension) {
+ // Oops. This should never happen.
+ Cu.reportError("A different extension context already exists in this frame!");
+ } else {
+ // This should not happen either.
+ Cu.reportError("The extension context was already initialized in this frame.");
+ }
+ return;
+ }
+
+ let mm = contentWindow
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ let {viewType, tabId} = this.contentGlobals.get(mm).ensureInitialized();
+
+ let uri = contentWindow.document.documentURIObject;
+
+ context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId});
+ this.extensionContexts.set(windowId, context);
+ },
+
+ /**
+ * Close the ExtensionPageContextChild belonging to the given window, if any.
+ *
+ * @param {number} windowId The inner window ID of the destroyed context.
+ */
+ destroyExtensionContext(windowId) {
+ let context = this.extensionContexts.get(windowId);
+ if (context) {
+ context.unload();
+ this.extensionContexts.delete(windowId);
+ }
+ },
+
+ shutdownExtension(extensionId) {
+ for (let [windowId, context] of this.extensionContexts) {
+ if (context.extension.id == extensionId) {
+ context.shutdown();
+ this.extensionContexts.delete(windowId);
+ }
+ }
+ },
+};
+
+// TODO(robwu): Change this condition when addons move to a separate process.
+if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Object.keys(ExtensionChild).forEach(function(key) {
+ if (typeof ExtensionChild[key] == "function") {
+ // :/
+ ExtensionChild[key] = () => {};
+ }
+ });
+}
+
+Object.assign(ExtensionChild, {
+ ChildAPIManager,
+ Messenger,
+ Port,
+});
+
diff --git a/toolkit/components/extensions/ExtensionCommon.jsm b/toolkit/components/extensions/ExtensionCommon.jsm
new file mode 100644
index 0000000000..a339fb27e6
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -0,0 +1,680 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * This module contains utilities and base classes for logic which is
+ * common between the parent and child process, and in particular
+ * between ExtensionParent.jsm and ExtensionChild.jsm.
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* exported ExtensionCommon */
+
+this.EXPORTED_SYMBOLS = ["ExtensionCommon"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ EventEmitter,
+ ExtensionError,
+ SpreadArgs,
+ getConsole,
+ getInnerWindowID,
+ getUniqueId,
+ runSafeSync,
+ runSafeSyncWithoutClone,
+ instanceOf,
+} = ExtensionUtils;
+
+XPCOMUtils.defineLazyGetter(this, "console", getConsole);
+
+class BaseContext {
+ constructor(envType, extension) {
+ this.envType = envType;
+ this.onClose = new Set();
+ this.checkedLastError = false;
+ this._lastError = null;
+ this.contextId = getUniqueId();
+ this.unloaded = false;
+ this.extension = extension;
+ this.jsonSandbox = null;
+ this.active = true;
+ this.incognito = null;
+ this.messageManager = null;
+ this.docShell = null;
+ this.contentWindow = null;
+ this.innerWindowID = 0;
+ }
+
+ setContentWindow(contentWindow) {
+ let {document} = contentWindow;
+ let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ this.innerWindowID = getInnerWindowID(contentWindow);
+ this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ if (this.incognito == null) {
+ this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
+ }
+
+ MessageChannel.setupMessageManagers([this.messageManager]);
+
+ let onPageShow = event => {
+ if (!event || event.target === document) {
+ this.docShell = docShell;
+ this.contentWindow = contentWindow;
+ this.active = true;
+ }
+ };
+ let onPageHide = event => {
+ if (!event || event.target === document) {
+ // Put this off until the next tick.
+ Promise.resolve().then(() => {
+ this.docShell = null;
+ this.contentWindow = null;
+ this.active = false;
+ });
+ }
+ };
+
+ onPageShow();
+ contentWindow.addEventListener("pagehide", onPageHide, true);
+ contentWindow.addEventListener("pageshow", onPageShow, true);
+ this.callOnClose({
+ close: () => {
+ onPageHide();
+ if (this.active) {
+ contentWindow.removeEventListener("pagehide", onPageHide, true);
+ contentWindow.removeEventListener("pageshow", onPageShow, true);
+ }
+ },
+ });
+ }
+
+ get cloneScope() {
+ throw new Error("Not implemented");
+ }
+
+ get principal() {
+ throw new Error("Not implemented");
+ }
+
+ runSafe(...args) {
+ if (this.unloaded) {
+ Cu.reportError("context.runSafe called after context unloaded");
+ } else if (!this.active) {
+ Cu.reportError("context.runSafe called while context is inactive");
+ } else {
+ return runSafeSync(this, ...args);
+ }
+ }
+
+ runSafeWithoutClone(...args) {
+ if (this.unloaded) {
+ Cu.reportError("context.runSafeWithoutClone called after context unloaded");
+ } else if (!this.active) {
+ Cu.reportError("context.runSafeWithoutClone called while context is inactive");
+ } else {
+ return runSafeSyncWithoutClone(...args);
+ }
+ }
+
+ checkLoadURL(url, options = {}) {
+ let ssm = Services.scriptSecurityManager;
+
+ let flags = ssm.STANDARD;
+ if (!options.allowScript) {
+ flags |= ssm.DISALLOW_SCRIPT;
+ }
+ if (!options.allowInheritsPrincipal) {
+ flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
+ }
+ if (options.dontReportErrors) {
+ flags |= ssm.DONT_REPORT_ERRORS;
+ }
+
+ try {
+ ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
+ } catch (e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Safely call JSON.stringify() on an object that comes from an
+ * extension.
+ *
+ * @param {array<any>} args Arguments for JSON.stringify()
+ * @returns {string} The stringified representation of obj
+ */
+ jsonStringify(...args) {
+ if (!this.jsonSandbox) {
+ this.jsonSandbox = Cu.Sandbox(this.principal, {
+ sameZoneAs: this.cloneScope,
+ wantXrays: false,
+ });
+ }
+
+ return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
+ }
+
+ callOnClose(obj) {
+ this.onClose.add(obj);
+ }
+
+ forgetOnClose(obj) {
+ this.onClose.delete(obj);
+ }
+
+ /**
+ * A wrapper around MessageChannel.sendMessage which adds the extension ID
+ * to the recipient object, and ensures replies are not processed after the
+ * context has been unloaded.
+ *
+ * @param {nsIMessageManager} target
+ * @param {string} messageName
+ * @param {object} data
+ * @param {object} [options]
+ * @param {object} [options.sender]
+ * @param {object} [options.recipient]
+ *
+ * @returns {Promise}
+ */
+ sendMessage(target, messageName, data, options = {}) {
+ options.recipient = options.recipient || {};
+ options.sender = options.sender || {};
+
+ options.recipient.extensionId = this.extension.id;
+ options.sender.extensionId = this.extension.id;
+ options.sender.contextId = this.contextId;
+
+ return MessageChannel.sendMessage(target, messageName, data, options);
+ }
+
+ get lastError() {
+ this.checkedLastError = true;
+ return this._lastError;
+ }
+
+ set lastError(val) {
+ this.checkedLastError = false;
+ this._lastError = val;
+ }
+
+ /**
+ * Normalizes the given error object for use by the target scope. If
+ * the target is an error object which belongs to that scope, it is
+ * returned as-is. If it is an ordinary object with a `message`
+ * property, it is converted into an error belonging to the target
+ * scope. If it is an Error object which does *not* belong to the
+ * clone scope, it is reported, and converted to an unexpected
+ * exception error.
+ *
+ * @param {Error|object} error
+ * @returns {Error}
+ */
+ normalizeError(error) {
+ if (error instanceof this.cloneScope.Error) {
+ return error;
+ }
+ let message;
+ if (instanceOf(error, "Object") || error instanceof ExtensionError) {
+ message = error.message;
+ } else if (typeof error == "object" &&
+ this.principal.subsumes(Cu.getObjectPrincipal(error))) {
+ message = error.message;
+ } else {
+ Cu.reportError(error);
+ }
+ message = message || "An unexpected error occurred";
+ return new this.cloneScope.Error(message);
+ }
+
+ /**
+ * Sets the value of `.lastError` to `error`, calls the given
+ * callback, and reports an error if the value has not been checked
+ * when the callback returns.
+ *
+ * @param {object} error An object with a `message` property. May
+ * optionally be an `Error` object belonging to the target scope.
+ * @param {function} callback The callback to call.
+ * @returns {*} The return value of callback.
+ */
+ withLastError(error, callback) {
+ this.lastError = this.normalizeError(error);
+ try {
+ return callback();
+ } finally {
+ if (!this.checkedLastError) {
+ Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
+ }
+ this.lastError = null;
+ }
+ }
+
+ /**
+ * Wraps the given promise so it can be safely returned to extension
+ * code in this context.
+ *
+ * If `callback` is provided, however, it is used as a completion
+ * function for the promise, and no promise is returned. In this case,
+ * the callback is called when the promise resolves or rejects. In the
+ * latter case, `lastError` is set to the rejection value, and the
+ * callback function must check `browser.runtime.lastError` or
+ * `extension.runtime.lastError` in order to prevent it being reported
+ * to the console.
+ *
+ * @param {Promise} promise The promise with which to wrap the
+ * callback. May resolve to a `SpreadArgs` instance, in which case
+ * each element will be used as a separate argument.
+ *
+ * Unless the promise object belongs to the cloneScope global, its
+ * resolution value is cloned into cloneScope prior to calling the
+ * `callback` function or resolving the wrapped promise.
+ *
+ * @param {function} [callback] The callback function to wrap
+ *
+ * @returns {Promise|undefined} If callback is null, a promise object
+ * belonging to the target scope. Otherwise, undefined.
+ */
+ wrapPromise(promise, callback = null) {
+ let runSafe = this.runSafe.bind(this);
+ if (promise instanceof this.cloneScope.Promise) {
+ runSafe = this.runSafeWithoutClone.bind(this);
+ }
+
+ if (callback) {
+ promise.then(
+ args => {
+ if (this.unloaded) {
+ dump(`Promise resolved after context unloaded\n`);
+ } else if (!this.active) {
+ dump(`Promise resolved while context is inactive\n`);
+ } else if (args instanceof SpreadArgs) {
+ runSafe(callback, ...args);
+ } else {
+ runSafe(callback, args);
+ }
+ },
+ error => {
+ this.withLastError(error, () => {
+ if (this.unloaded) {
+ dump(`Promise rejected after context unloaded\n`);
+ } else if (!this.active) {
+ dump(`Promise rejected while context is inactive\n`);
+ } else {
+ this.runSafeWithoutClone(callback);
+ }
+ });
+ });
+ } else {
+ return new this.cloneScope.Promise((resolve, reject) => {
+ promise.then(
+ value => {
+ if (this.unloaded) {
+ dump(`Promise resolved after context unloaded\n`);
+ } else if (!this.active) {
+ dump(`Promise resolved while context is inactive\n`);
+ } else if (value instanceof SpreadArgs) {
+ runSafe(resolve, value.length == 1 ? value[0] : value);
+ } else {
+ runSafe(resolve, value);
+ }
+ },
+ value => {
+ if (this.unloaded) {
+ dump(`Promise rejected after context unloaded: ${value && value.message}\n`);
+ } else if (!this.active) {
+ dump(`Promise rejected while context is inactive: ${value && value.message}\n`);
+ } else {
+ this.runSafeWithoutClone(reject, this.normalizeError(value));
+ }
+ });
+ });
+ }
+ }
+
+ unload() {
+ this.unloaded = true;
+
+ MessageChannel.abortResponses({
+ extensionId: this.extension.id,
+ contextId: this.contextId,
+ });
+
+ for (let obj of this.onClose) {
+ obj.close();
+ }
+ }
+
+ /**
+ * A simple proxy for unload(), for use with callOnClose().
+ */
+ close() {
+ this.unload();
+ }
+}
+
+/**
+ * An object that runs the implementation of a schema API. Instantiations of
+ * this interfaces are used by Schemas.jsm.
+ *
+ * @interface
+ */
+class SchemaAPIInterface {
+ /**
+ * Calls this as a function that returns its return value.
+ *
+ * @abstract
+ * @param {Array} args The parameters for the function.
+ * @returns {*} The return value of the invoked function.
+ */
+ callFunction(args) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Calls this as a function and ignores its return value.
+ *
+ * @abstract
+ * @param {Array} args The parameters for the function.
+ */
+ callFunctionNoReturn(args) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Calls this as a function that completes asynchronously.
+ *
+ * @abstract
+ * @param {Array} args The parameters for the function.
+ * @param {function(*)} [callback] The callback to be called when the function
+ * completes.
+ * @returns {Promise|undefined} Must be void if `callback` is set, and a
+ * promise otherwise. The promise is resolved when the function completes.
+ */
+ callAsyncFunction(args, callback) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Retrieves the value of this as a property.
+ *
+ * @abstract
+ * @returns {*} The value of the property.
+ */
+ getProperty() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Assigns the value to this as property.
+ *
+ * @abstract
+ * @param {string} value The new value of the property.
+ */
+ setProperty(value) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Registers a `listener` to this as an event.
+ *
+ * @abstract
+ * @param {function} listener The callback to be called when the event fires.
+ * @param {Array} args Extra parameters for EventManager.addListener.
+ * @see EventManager.addListener
+ */
+ addListener(listener, args) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Checks whether `listener` is listening to this as an event.
+ *
+ * @abstract
+ * @param {function} listener The event listener.
+ * @returns {boolean} Whether `listener` is registered with this as an event.
+ * @see EventManager.hasListener
+ */
+ hasListener(listener) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Unregisters `listener` from this as an event.
+ *
+ * @abstract
+ * @param {function} listener The event listener.
+ * @see EventManager.removeListener
+ */
+ removeListener(listener) {
+ throw new Error("Not implemented");
+ }
+}
+
+/**
+ * An object that runs a locally implemented API.
+ */
+class LocalAPIImplementation extends SchemaAPIInterface {
+ /**
+ * Constructs an implementation of the `name` method or property of `pathObj`.
+ *
+ * @param {object} pathObj The object containing the member with name `name`.
+ * @param {string} name The name of the implemented member.
+ * @param {BaseContext} context The context in which the schema is injected.
+ */
+ constructor(pathObj, name, context) {
+ super();
+ this.pathObj = pathObj;
+ this.name = name;
+ this.context = context;
+ }
+
+ callFunction(args) {
+ return this.pathObj[this.name](...args);
+ }
+
+ callFunctionNoReturn(args) {
+ this.pathObj[this.name](...args);
+ }
+
+ callAsyncFunction(args, callback) {
+ let promise;
+ try {
+ promise = this.pathObj[this.name](...args) || Promise.resolve();
+ } catch (e) {
+ promise = Promise.reject(e);
+ }
+ return this.context.wrapPromise(promise, callback);
+ }
+
+ getProperty() {
+ return this.pathObj[this.name];
+ }
+
+ setProperty(value) {
+ this.pathObj[this.name] = value;
+ }
+
+ addListener(listener, args) {
+ try {
+ this.pathObj[this.name].addListener.call(null, listener, ...args);
+ } catch (e) {
+ throw this.context.normalizeError(e);
+ }
+ }
+
+ hasListener(listener) {
+ return this.pathObj[this.name].hasListener.call(null, listener);
+ }
+
+ removeListener(listener) {
+ this.pathObj[this.name].removeListener.call(null, listener);
+ }
+}
+
+/**
+ * This object loads the ext-*.js scripts that define the extension API.
+ *
+ * This class instance is shared with the scripts that it loads, so that the
+ * ext-*.js scripts and the instantiator can communicate with each other.
+ */
+class SchemaAPIManager extends EventEmitter {
+ /**
+ * @param {string} processType
+ * "main" - The main, one and only chrome browser process.
+ * "addon" - An addon process.
+ * "content" - A content process.
+ */
+ constructor(processType) {
+ super();
+ this.processType = processType;
+ this.global = this._createExtGlobal();
+ this._scriptScopes = [];
+ this._schemaApis = {
+ addon_parent: [],
+ addon_child: [],
+ content_parent: [],
+ content_child: [],
+ };
+ }
+
+ /**
+ * Create a global object that is used as the shared global for all ext-*.js
+ * scripts that are loaded via `loadScript`.
+ *
+ * @returns {object} A sandbox that is used as the global by `loadScript`.
+ */
+ _createExtGlobal() {
+ let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
+ wantXrays: false,
+ sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
+ });
+
+ Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
+
+ XPCOMUtils.defineLazyGetter(global, "console", getConsole);
+
+ XPCOMUtils.defineLazyModuleGetter(global, "require",
+ "resource://devtools/shared/Loader.jsm");
+
+ return global;
+ }
+
+ /**
+ * Load an ext-*.js script. The script runs in its own scope, if it wishes to
+ * share state with another script it can assign to the `global` variable. If
+ * it wishes to communicate with this API manager, use `extensions`.
+ *
+ * @param {string} scriptUrl The URL of the ext-*.js script.
+ */
+ loadScript(scriptUrl) {
+ // Create the object in the context of the sandbox so that the script runs
+ // in the sandbox's context instead of here.
+ let scope = Cu.createObjectIn(this.global);
+
+ Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
+
+ // Save the scope to avoid it being garbage collected.
+ this._scriptScopes.push(scope);
+ }
+
+ /**
+ * Called by an ext-*.js script to register an API.
+ *
+ * @param {string} namespace The API namespace.
+ * Intended to match the namespace of the generated API, but not used at
+ * the moment - see bugzil.la/1295774.
+ * @param {string} envType Restricts the API to contexts that run in the
+ * given environment. Must be one of the following:
+ * - "addon_parent" - addon APIs that runs in the main process.
+ * - "addon_child" - addon APIs that runs in an addon process.
+ * - "content_parent" - content script APIs that runs in the main process.
+ * - "content_child" - content script APIs that runs in a content process.
+ * @param {function(BaseContext)} getAPI A function that returns an object
+ * that will be merged with |chrome| and |browser|. The next example adds
+ * the create, update and remove methods to the tabs API.
+ *
+ * registerSchemaAPI("tabs", "addon_parent", (context) => ({
+ * tabs: { create, update },
+ * }));
+ * registerSchemaAPI("tabs", "addon_parent", (context) => ({
+ * tabs: { remove },
+ * }));
+ */
+ registerSchemaAPI(namespace, envType, getAPI) {
+ this._schemaApis[envType].push({namespace, getAPI});
+ }
+
+ /**
+ * Exports all registered scripts to `obj`.
+ *
+ * @param {BaseContext} context The context for which the API bindings are
+ * generated.
+ * @param {object} obj The destination of the API.
+ */
+ generateAPIs(context, obj) {
+ let apis = this._schemaApis[context.envType];
+ if (!apis) {
+ Cu.reportError(`No APIs have been registered for ${context.envType}`);
+ return;
+ }
+ SchemaAPIManager.generateAPIs(context, apis, obj);
+ }
+
+ /**
+ * Mash together all the APIs from `apis` into `obj`.
+ *
+ * @param {BaseContext} context The context for which the API bindings are
+ * generated.
+ * @param {Array} apis A list of objects, see `registerSchemaAPI`.
+ * @param {object} obj The destination of the API.
+ */
+ static generateAPIs(context, apis, obj) {
+ // Recursively copy properties from source to dest.
+ function copy(dest, source) {
+ for (let prop in source) {
+ let desc = Object.getOwnPropertyDescriptor(source, prop);
+ if (typeof(desc.value) == "object") {
+ if (!(prop in dest)) {
+ dest[prop] = {};
+ }
+ copy(dest[prop], source[prop]);
+ } else {
+ Object.defineProperty(dest, prop, desc);
+ }
+ }
+ }
+
+ for (let api of apis) {
+ if (Schemas.checkPermissions(api.namespace, context.extension)) {
+ api = api.getAPI(context);
+ copy(obj, api);
+ }
+ }
+ }
+}
+
+const ExtensionCommon = {
+ BaseContext,
+ LocalAPIImplementation,
+ SchemaAPIInterface,
+ SchemaAPIManager,
+};
diff --git a/toolkit/components/extensions/ExtensionContent.jsm b/toolkit/components/extensions/ExtensionContent.jsm
new file mode 100644
index 0000000000..9b9a02091e
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -0,0 +1,1048 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionContent"];
+
+/* globals ExtensionContent */
+
+/*
+ * This file handles the content process side of extensions. It mainly
+ * takes care of content script injection, content script APIs, and
+ * messaging.
+ *
+ * This file is also the initial entry point for addon processes.
+ * ExtensionChild.jsm is responsible for functionality specific to addon
+ * processes.
+ */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
+ "resource:///modules/translation/LanguageDetector.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
+ "resource://gre/modules/WebNavigationFrames.jsm");
+
+Cu.import("resource://gre/modules/ExtensionChild.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+const {
+ EventEmitter,
+ LocaleData,
+ defineLazyGetter,
+ flushJarCache,
+ getInnerWindowID,
+ promiseDocumentReady,
+ runSafeSyncWithoutClone,
+} = ExtensionUtils;
+
+const {
+ BaseContext,
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+const {
+ ChildAPIManager,
+ Messenger,
+} = ExtensionChild;
+
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+
+const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
+
+function isWhenBeforeOrSame(when1, when2) {
+ let table = {"document_start": 0,
+ "document_end": 1,
+ "document_idle": 2};
+ return table[when1] <= table[when2];
+}
+
+var apiManager = new class extends SchemaAPIManager {
+ constructor() {
+ super("content");
+ this.initialized = false;
+ }
+
+ generateAPIs(...args) {
+ if (!this.initialized) {
+ this.initialized = true;
+ for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
+ this.loadScript(value);
+ }
+ }
+ return super.generateAPIs(...args);
+ }
+
+ registerSchemaAPI(namespace, envType, getAPI) {
+ if (envType == "content_child") {
+ super.registerSchemaAPI(namespace, envType, getAPI);
+ }
+ }
+}();
+
+// Represents a content script.
+function Script(extension, options, deferred = PromiseUtils.defer()) {
+ this.extension = extension;
+ this.options = options;
+ this.run_at = this.options.run_at;
+ this.js = this.options.js || [];
+ this.css = this.options.css || [];
+ this.remove_css = this.options.remove_css;
+ this.match_about_blank = this.options.match_about_blank;
+
+ this.deferred = deferred;
+
+ this.matches_ = new MatchPattern(this.options.matches);
+ this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
+ // TODO: MatchPattern should pre-mangle host-only patterns so that we
+ // don't need to call a separate match function.
+ this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
+ this.include_globs_ = new MatchGlobs(this.options.include_globs);
+ this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
+
+ this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
+}
+
+Script.prototype = {
+ get cssURLs() {
+ // We can handle CSS urls (css) and CSS code (cssCode).
+ let urls = [];
+ for (let url of this.css) {
+ urls.push(this.extension.baseURI.resolve(url));
+ }
+
+ if (this.options.cssCode) {
+ let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
+ urls.push(url);
+ }
+
+ return urls;
+ },
+
+ matches(window) {
+ let uri = window.document.documentURIObject;
+ let principal = window.document.nodePrincipal;
+
+ // If mozAddonManager is present on this page, don't allow
+ // content scripts.
+ if (window.navigator.mozAddonManager !== undefined) {
+ return false;
+ }
+
+ if (this.match_about_blank && ["about:blank", "about:srcdoc"].includes(uri.spec)) {
+ // When matching about:blank/srcdoc documents, the checks below
+ // need to be performed against the "owner" document's URI.
+ uri = principal.URI;
+ }
+
+ // Documents from data: URIs also inherit the principal.
+ if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
+ if (!this.match_about_blank) {
+ return false;
+ }
+ uri = principal.URI;
+ }
+
+ if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
+ return false;
+ }
+
+ if (this.exclude_matches_.matches(uri)) {
+ return false;
+ }
+
+ if (this.options.include_globs != null) {
+ if (!this.include_globs_.matches(uri.spec)) {
+ return false;
+ }
+ }
+
+ if (this.exclude_globs_.matches(uri.spec)) {
+ return false;
+ }
+
+ if (this.options.frame_id != null) {
+ if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) {
+ return false;
+ }
+ } else if (!this.options.all_frames && window.top != window) {
+ return false;
+ }
+
+ return true;
+ },
+
+ cleanup(window) {
+ if (!this.remove_css) {
+ let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ for (let url of this.cssURLs) {
+ runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
+ }
+ }
+ },
+
+ /**
+ * Tries to inject this script into the given window and sandbox, if
+ * there are pending operations for the window's current load state.
+ *
+ * @param {Window} window
+ * The DOM Window to inject the scripts and CSS into.
+ * @param {Sandbox} sandbox
+ * A Sandbox inheriting from `window` in which to evaluate the
+ * injected scripts.
+ * @param {function} shouldRun
+ * A function which, when passed the document load state that a
+ * script is expected to run at, returns `true` if we should
+ * currently be injecting scripts for that load state.
+ *
+ * For initial injection of a script, this function should
+ * return true if the document is currently in or has already
+ * passed through the given state. For injections triggered by
+ * document state changes, it should only return true if the
+ * given state exactly matches the state that triggered the
+ * change.
+ * @param {string} when
+ * The document's current load state, or if triggered by a
+ * document state change, the new document state that triggered
+ * the injection.
+ */
+ tryInject(window, sandbox, shouldRun, when) {
+ if (shouldRun("document_start")) {
+ let {cssURLs} = this;
+ if (cssURLs.length > 0) {
+ let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString;
+ for (let url of cssURLs) {
+ runSafeSyncWithoutClone(method, url, winUtils.AUTHOR_SHEET);
+ }
+
+ this.deferred.resolve();
+ }
+ }
+
+ let result;
+ let scheduled = this.run_at || "document_idle";
+ if (shouldRun(scheduled)) {
+ for (let [i, url] of this.js.entries()) {
+ let options = {
+ target: sandbox,
+ charset: "UTF-8",
+ // Inject the last script asynchronously unless we're expected to
+ // inject before any page scripts have run, and we haven't already
+ // missed that boat.
+ async: (i === this.js.length - 1) &&
+ (this.run_at !== "document_start" || when !== "document_start"),
+ };
+ try {
+ result = Services.scriptloader.loadSubScriptWithOptions(url, options);
+ } catch (e) {
+ Cu.reportError(e);
+ this.deferred.reject(e);
+ }
+ }
+
+ if (this.options.jsCode) {
+ try {
+ result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
+ } catch (e) {
+ Cu.reportError(e);
+ this.deferred.reject(e);
+ }
+ }
+
+ this.deferred.resolve(result);
+ }
+ },
+};
+
+function getWindowMessageManager(contentWindow) {
+ let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor);
+ try {
+ return ir.getInterface(Ci.nsIContentFrameMessageManager);
+ } catch (e) {
+ // Some windows don't support this interface (hidden window).
+ return null;
+ }
+}
+
+var DocumentManager;
+var ExtensionManager;
+
+/**
+ * An execution context for semi-privileged extension content scripts.
+ *
+ * This is the child side of the ContentScriptContextParent class
+ * defined in ExtensionParent.jsm.
+ */
+class ContentScriptContextChild extends BaseContext {
+ constructor(extension, contentWindow, contextOptions = {}) {
+ super("content_child", extension);
+
+ let {isExtensionPage} = contextOptions;
+
+ this.isExtensionPage = isExtensionPage;
+
+ this.setContentWindow(contentWindow);
+
+ let frameId = WebNavigationFrames.getFrameId(contentWindow);
+ this.frameId = frameId;
+
+ this.scripts = [];
+
+ let contentPrincipal = contentWindow.document.nodePrincipal;
+ let ssm = Services.scriptSecurityManager;
+
+ // copy origin attributes from the content window origin attributes to
+ // preserve the user context id. overwrite the addonId.
+ let attrs = contentPrincipal.originAttributes;
+ attrs.addonId = this.extension.id;
+ let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
+
+ let principal;
+ if (ssm.isSystemPrincipal(contentPrincipal)) {
+ // Make sure we don't hand out the system principal by accident.
+ // also make sure that the null principal has the right origin attributes
+ principal = ssm.createNullPrincipal(attrs);
+ } else {
+ principal = [contentPrincipal, extensionPrincipal];
+ }
+
+ if (isExtensionPage) {
+ if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
+ throw new Error("Invalid target window for this extension context");
+ }
+ // This is an iframe with content script API enabled and its principal should be the
+ // contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled
+ // because it enables us to create the APIs object in this sandbox object and then copying it
+ // into the iframe's window, see Bug 1214658 for rationale)
+ this.sandbox = Cu.Sandbox(contentWindow, {
+ sandboxPrototype: contentWindow,
+ sameZoneAs: contentWindow,
+ wantXrays: false,
+ isWebExtensionContentScript: true,
+ });
+ } else {
+ // This metadata is required by the Developer Tools, in order for
+ // the content script to be associated with both the extension and
+ // the tab holding the content page.
+ let metadata = {
+ "inner-window-id": this.innerWindowID,
+ addonId: attrs.addonId,
+ };
+
+ this.sandbox = Cu.Sandbox(principal, {
+ metadata,
+ sandboxPrototype: contentWindow,
+ sameZoneAs: contentWindow,
+ wantXrays: true,
+ isWebExtensionContentScript: true,
+ wantExportHelpers: true,
+ wantGlobalProperties: ["XMLHttpRequest", "fetch"],
+ originAttributes: attrs,
+ });
+
+ Cu.evalInSandbox(`
+ window.JSON = JSON;
+ window.XMLHttpRequest = XMLHttpRequest;
+ window.fetch = fetch;
+ `, this.sandbox);
+ }
+
+ Object.defineProperty(this, "principal", {
+ value: Cu.getObjectPrincipal(this.sandbox),
+ enumerable: true,
+ configurable: true,
+ });
+
+ this.url = contentWindow.location.href;
+
+ defineLazyGetter(this, "chromeObj", () => {
+ let chromeObj = Cu.createObjectIn(this.sandbox);
+
+ Schemas.inject(chromeObj, this.childManager);
+ return chromeObj;
+ });
+
+ Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
+ Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
+
+ // This is an iframe with content script API enabled (bug 1214658)
+ if (isExtensionPage) {
+ Schemas.exportLazyGetter(this.contentWindow,
+ "browser", () => this.chromeObj);
+ Schemas.exportLazyGetter(this.contentWindow,
+ "chrome", () => this.chromeObj);
+ }
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+
+ execute(script, shouldRun, when) {
+ script.tryInject(this.contentWindow, this.sandbox, shouldRun, when);
+ }
+
+ addScript(script, when) {
+ let state = DocumentManager.getWindowState(this.contentWindow);
+ this.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state), when);
+
+ // Save the script in case it has pending operations in later load
+ // states, but only if we're before document_idle, or require cleanup.
+ if (state != "document_idle" || script.requiresCleanup) {
+ this.scripts.push(script);
+ }
+ }
+
+ triggerScripts(documentState) {
+ for (let script of this.scripts) {
+ this.execute(script, scheduled => scheduled == documentState, documentState);
+ }
+ if (documentState == "document_idle") {
+ // Don't bother saving scripts after document_idle.
+ this.scripts = this.scripts.filter(script => script.requiresCleanup);
+ }
+ }
+
+ close() {
+ super.unload();
+
+ if (this.contentWindow) {
+ for (let script of this.scripts) {
+ if (script.requiresCleanup) {
+ script.cleanup(this.contentWindow);
+ }
+ }
+
+ // Overwrite the content script APIs with an empty object if the APIs objects are still
+ // defined in the content window (bug 1214658).
+ if (this.isExtensionPage) {
+ Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
+ Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
+ }
+ }
+ Cu.nukeSandbox(this.sandbox);
+ this.sandbox = null;
+ }
+}
+
+defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
+ // The |sender| parameter is passed directly to the extension.
+ let sender = {id: this.extension.uuid, frameId: this.frameId, url: this.url};
+ let filter = {extensionId: this.extension.id};
+ let optionalFilter = {frameId: this.frameId};
+
+ return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
+});
+
+defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
+ let localApis = {};
+ apiManager.generateAPIs(this, localApis);
+
+ let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+ envType: "content_parent",
+ url: this.url,
+ });
+
+ this.callOnClose(childManager);
+
+ return childManager;
+});
+
+// Responsible for creating ExtensionContexts and injecting content
+// scripts into them when new documents are created.
+DocumentManager = {
+ extensionCount: 0,
+
+ // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
+ contentScriptWindows: new Map(),
+
+ // Map[windowId -> ContentScriptContextChild]
+ extensionPageWindows: new Map(),
+
+ init() {
+ Services.obs.addObserver(this, "content-document-global-created", false);
+ Services.obs.addObserver(this, "document-element-inserted", false);
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "content-document-global-created");
+ Services.obs.removeObserver(this, "document-element-inserted");
+ Services.obs.removeObserver(this, "inner-window-destroyed");
+ },
+
+ getWindowState(contentWindow) {
+ let readyState = contentWindow.document.readyState;
+ if (readyState == "complete") {
+ return "document_idle";
+ }
+ if (readyState == "interactive") {
+ return "document_end";
+ }
+ return "document_start";
+ },
+
+ loadInto(window) {
+ // Enable the content script APIs should be available in subframes' window
+ // if it is recognized as a valid addon id (see Bug 1214658 for rationale).
+ const {
+ NO_PRIVILEGES,
+ CONTENTSCRIPT_PRIVILEGES,
+ FULL_PRIVILEGES,
+ } = ExtensionManagement.API_LEVELS;
+ let extensionId = ExtensionManagement.getAddonIdForWindow(window);
+ let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
+
+ if (apiLevel != NO_PRIVILEGES) {
+ let extension = ExtensionManager.get(extensionId);
+ if (extension) {
+ if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
+ DocumentManager.getExtensionPageContext(extension, window);
+ } else if (apiLevel == FULL_PRIVILEGES) {
+ ExtensionChild.createExtensionContext(extension, window);
+ }
+ }
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ // For some types of documents (about:blank), we only see the first
+ // notification, for others (data: URIs) we only observe the second.
+ if (topic == "content-document-global-created" || topic == "document-element-inserted") {
+ let document = subject;
+ let window = document && document.defaultView;
+
+ if (topic == "content-document-global-created") {
+ window = subject;
+ document = window && window.document;
+ }
+
+ if (!document || !document.location || !window) {
+ return;
+ }
+
+ // Make sure we only load into frames that ExtensionContent.init
+ // was called on (i.e., not frames for social or sidebars).
+ let mm = getWindowMessageManager(window);
+ if (!mm || !ExtensionContent.globals.has(mm)) {
+ return;
+ }
+
+ // Load on document-element-inserted, except for about:blank which doesn't
+ // see it, and needs special late handling on DOMContentLoaded event.
+ if (topic === "document-element-inserted") {
+ this.loadInto(window);
+ this.trigger("document_start", window);
+ }
+
+ /* eslint-disable mozilla/balanced-listeners */
+ window.addEventListener("DOMContentLoaded", this, true);
+ window.addEventListener("load", this, true);
+ /* eslint-enable mozilla/balanced-listeners */
+ } else if (topic == "inner-window-destroyed") {
+ let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+
+ MessageChannel.abortResponses({innerWindowID: windowId});
+
+ // Close any existent content-script context for the destroyed window.
+ if (this.contentScriptWindows.has(windowId)) {
+ let extensions = this.contentScriptWindows.get(windowId);
+ for (let [, context] of extensions) {
+ context.close();
+ }
+
+ this.contentScriptWindows.delete(windowId);
+ }
+
+ // Close any existent iframe extension page context for the destroyed window.
+ if (this.extensionPageWindows.has(windowId)) {
+ let context = this.extensionPageWindows.get(windowId);
+ context.close();
+ this.extensionPageWindows.delete(windowId);
+ }
+
+ ExtensionChild.destroyExtensionContext(windowId);
+ }
+ },
+
+ handleEvent: function(event) {
+ let window = event.currentTarget;
+ if (event.target != window.document) {
+ // We use capturing listeners so we have precedence over content script
+ // listeners, but only care about events targeted to the element we're
+ // listening on.
+ return;
+ }
+ window.removeEventListener(event.type, this, true);
+
+ // Need to check if we're still on the right page? Greasemonkey does this.
+
+ if (event.type == "DOMContentLoaded") {
+ // By this time, we can be sure if this is an explicit about:blank
+ // document, and if it needs special late loading and fake trigger.
+ if (window.location.href === "about:blank") {
+ this.loadInto(window);
+ this.trigger("document_start", window);
+ }
+ this.trigger("document_end", window);
+ } else if (event.type == "load") {
+ this.trigger("document_idle", window);
+ }
+ },
+
+ // Used to executeScript, insertCSS and removeCSS.
+ executeScript(global, extensionId, options) {
+ let extension = ExtensionManager.get(extensionId);
+
+ let executeInWin = (window) => {
+ let deferred = PromiseUtils.defer();
+ let script = new Script(extension, options, deferred);
+
+ if (script.matches(window)) {
+ let context = this.getContentScriptContext(extension, window);
+ context.addScript(script);
+ return deferred.promise;
+ }
+ return null;
+ };
+
+ let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
+ .filter(promise => promise);
+
+ if (!promises.length) {
+ let details = {};
+ for (let key of ["all_frames", "frame_id", "matches_about_blank", "matchesHost"]) {
+ if (key in options) {
+ details[key] = options[key];
+ }
+ }
+
+ return Promise.reject({message: `No window matching ${JSON.stringify(details)}`});
+ }
+ if (!options.all_frames && promises.length > 1) {
+ return Promise.reject({message: `Internal error: Script matched multiple windows`});
+ }
+ return Promise.all(promises);
+ },
+
+ enumerateWindows: function* (docShell) {
+ let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ yield window;
+
+ for (let i = 0; i < docShell.childCount; i++) {
+ let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+ yield* this.enumerateWindows(child);
+ }
+ },
+
+ getContentScriptGlobalsForWindow(window) {
+ let winId = getInnerWindowID(window);
+ let extensions = this.contentScriptWindows.get(winId);
+
+ if (extensions) {
+ return Array.from(extensions.values(), ctx => ctx.sandbox);
+ }
+
+ return [];
+ },
+
+ getContentScriptContext(extension, window) {
+ let winId = getInnerWindowID(window);
+ if (!this.contentScriptWindows.has(winId)) {
+ this.contentScriptWindows.set(winId, new Map());
+ }
+
+ let extensions = this.contentScriptWindows.get(winId);
+ if (!extensions.has(extension.id)) {
+ let context = new ContentScriptContextChild(extension, window);
+ extensions.set(extension.id, context);
+ }
+
+ return extensions.get(extension.id);
+ },
+
+ getExtensionPageContext(extension, window) {
+ let winId = getInnerWindowID(window);
+
+ let context = this.extensionPageWindows.get(winId);
+ if (!context) {
+ let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true});
+ this.extensionPageWindows.set(winId, context);
+ }
+
+ return context;
+ },
+
+ startupExtension(extensionId) {
+ if (this.extensionCount == 0) {
+ this.init();
+ }
+ this.extensionCount++;
+
+ let extension = ExtensionManager.get(extensionId);
+ for (let global of ExtensionContent.globals.keys()) {
+ // Note that we miss windows in the bfcache here. In theory we
+ // could execute content scripts on a pageshow event for that
+ // window, but that seems extreme.
+ for (let window of this.enumerateWindows(global.docShell)) {
+ for (let script of extension.scripts) {
+ if (script.matches(window)) {
+ let context = this.getContentScriptContext(extension, window);
+ context.addScript(script);
+ }
+ }
+ }
+ }
+ },
+
+ shutdownExtension(extensionId) {
+ // Clean up content-script contexts on extension shutdown.
+ for (let [, extensions] of this.contentScriptWindows) {
+ let context = extensions.get(extensionId);
+ if (context) {
+ context.close();
+ extensions.delete(extensionId);
+ }
+ }
+
+ // Clean up iframe extension page contexts on extension shutdown.
+ for (let [winId, context] of this.extensionPageWindows) {
+ if (context.extension.id == extensionId) {
+ context.close();
+ this.extensionPageWindows.delete(winId);
+ }
+ }
+
+ ExtensionChild.shutdownExtension(extensionId);
+
+ MessageChannel.abortResponses({extensionId});
+
+ this.extensionCount--;
+ if (this.extensionCount == 0) {
+ this.uninit();
+ }
+ },
+
+ trigger(when, window) {
+ if (when === "document_start") {
+ for (let extension of ExtensionManager.extensions.values()) {
+ for (let script of extension.scripts) {
+ if (script.matches(window)) {
+ let context = this.getContentScriptContext(extension, window);
+ context.addScript(script, when);
+ }
+ }
+ }
+ } else {
+ let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
+ for (let context of contexts.values()) {
+ context.triggerScripts(when);
+ }
+ }
+ },
+};
+
+// Represents a browser extension in the content process.
+class BrowserExtensionContent extends EventEmitter {
+ constructor(data) {
+ super();
+
+ this.id = data.id;
+ this.uuid = data.uuid;
+ this.data = data;
+ this.instanceId = data.instanceId;
+
+ this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+ Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
+ this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
+ this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+ this.permissions = data.permissions;
+ this.principal = data.principal;
+
+ this.localeData = new LocaleData(data.localeData);
+
+ this.manifest = data.manifest;
+ this.baseURI = Services.io.newURI(data.baseURL, null, null);
+
+ // Only used in addon processes.
+ this.views = new Set();
+
+ let uri = Services.io.newURI(data.resourceURL, null, null);
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ // Extension.jsm takes care of this in the parent.
+ ExtensionManagement.startupExtension(this.uuid, uri, this);
+ }
+ }
+
+ shutdown() {
+ Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ ExtensionManagement.shutdownExtension(this.uuid);
+ }
+ }
+
+ emit(event, ...args) {
+ Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
+
+ super.emit(event, ...args);
+ }
+
+ receiveMessage({name, data}) {
+ if (name === this.MESSAGE_EMIT_EVENT) {
+ super.emit(data.event, ...data.args);
+ }
+ }
+
+ localizeMessage(...args) {
+ return this.localeData.localizeMessage(...args);
+ }
+
+ localize(...args) {
+ return this.localeData.localize(...args);
+ }
+
+ hasPermission(perm) {
+ let match = /^manifest:(.*)/.exec(perm);
+ if (match) {
+ return this.manifest[match[1]] != null;
+ }
+ return this.permissions.has(perm);
+ }
+}
+
+ExtensionManager = {
+ // Map[extensionId, BrowserExtensionContent]
+ extensions: new Map(),
+
+ init() {
+ Schemas.init();
+ ExtensionChild.initOnce();
+
+ Services.cpmm.addMessageListener("Extension:Startup", this);
+ Services.cpmm.addMessageListener("Extension:Shutdown", this);
+ Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+
+ if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
+ let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
+ for (let data of extensions) {
+ this.extensions.set(data.id, new BrowserExtensionContent(data));
+ DocumentManager.startupExtension(data.id);
+ }
+ }
+ },
+
+ get(extensionId) {
+ return this.extensions.get(extensionId);
+ },
+
+ receiveMessage({name, data}) {
+ let extension;
+ switch (name) {
+ case "Extension:Startup": {
+ extension = new BrowserExtensionContent(data);
+
+ this.extensions.set(data.id, extension);
+
+ DocumentManager.startupExtension(data.id);
+
+ Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
+ break;
+ }
+
+ case "Extension:Shutdown": {
+ extension = this.extensions.get(data.id);
+ extension.shutdown();
+
+ DocumentManager.shutdownExtension(data.id);
+
+ this.extensions.delete(data.id);
+ break;
+ }
+
+ case "Extension:FlushJarCache": {
+ let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+ "initWithPath");
+ let file = new nsIFile(data.path);
+ flushJarCache(file);
+ Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
+ break;
+ }
+ }
+ },
+};
+
+class ExtensionGlobal {
+ constructor(global) {
+ this.global = global;
+
+ MessageChannel.addListener(global, "Extension:Capture", this);
+ MessageChannel.addListener(global, "Extension:DetectLanguage", this);
+ MessageChannel.addListener(global, "Extension:Execute", this);
+ MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
+ MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
+
+ this.windowId = global.content
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+
+ global.sendAsyncMessage("Extension:TopWindowID", {windowId: this.windowId});
+ }
+
+ uninit() {
+ this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId});
+ }
+
+ get messageFilterStrict() {
+ return {
+ innerWindowID: getInnerWindowID(this.global.content),
+ };
+ }
+
+ receiveMessage({target, messageName, recipient, data}) {
+ switch (messageName) {
+ case "Extension:Capture":
+ return this.handleExtensionCapture(data.width, data.height, data.options);
+ case "Extension:DetectLanguage":
+ return this.handleDetectLanguage(target);
+ case "Extension:Execute":
+ return this.handleExtensionExecute(target, recipient.extensionId, data.options);
+ case "WebNavigation:GetFrame":
+ return this.handleWebNavigationGetFrame(data.options);
+ case "WebNavigation:GetAllFrames":
+ return this.handleWebNavigationGetAllFrames();
+ }
+ }
+
+ handleExtensionCapture(width, height, options) {
+ let win = this.global.content;
+
+ const XHTML_NS = "http://www.w3.org/1999/xhtml";
+ let canvas = win.document.createElementNS(XHTML_NS, "canvas");
+ canvas.width = width;
+ canvas.height = height;
+ canvas.mozOpaque = true;
+
+ let ctx = canvas.getContext("2d");
+
+ // We need to scale the image to the visible size of the browser,
+ // in order for the result to appear as the user sees it when
+ // settings like full zoom come into play.
+ ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
+
+ ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
+
+ return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
+ }
+
+ handleDetectLanguage(target) {
+ let doc = target.content.document;
+
+ return promiseDocumentReady(doc).then(() => {
+ let elem = doc.documentElement;
+
+ let language = (elem.getAttribute("xml:lang") || elem.getAttribute("lang") ||
+ doc.contentLanguage || null);
+
+ // We only want the last element of the TLD here.
+ // Only country codes have any effect on the results, but other
+ // values cause no harm.
+ let tld = doc.location.hostname.match(/[a-z]*$/)[0];
+
+ // The CLD2 library used by the language detector is capable of
+ // analyzing raw HTML. Unfortunately, that takes much more memory,
+ // and since it's hosted by emscripten, and therefore can't shrink
+ // its heap after it's grown, it has a performance cost.
+ // So we send plain text instead.
+ let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"].createInstance(Ci.nsIDocumentEncoder);
+ encoder.init(doc, "text/plain", encoder.SkipInvisibleContent);
+ let text = encoder.encodeToStringWithMaxLength(60 * 1024);
+
+ let encoding = doc.characterSet;
+
+ return LanguageDetector.detectLanguage({language, tld, text, encoding})
+ .then(result => result.language === "un" ? "und" : result.language);
+ });
+ }
+
+ // Used to executeScript, insertCSS and removeCSS.
+ handleExtensionExecute(target, extensionId, options) {
+ return DocumentManager.executeScript(target, extensionId, options).then(result => {
+ try {
+ // Make sure we can structured-clone the result value before
+ // we try to send it back over the message manager.
+ Cu.cloneInto(result, target);
+ } catch (e) {
+ return Promise.reject({message: "Script returned non-structured-clonable data"});
+ }
+ return result;
+ });
+ }
+
+ handleWebNavigationGetFrame({frameId}) {
+ return WebNavigationFrames.getFrame(this.global.docShell, frameId);
+ }
+
+ handleWebNavigationGetAllFrames() {
+ return WebNavigationFrames.getAllFrames(this.global.docShell);
+ }
+}
+
+this.ExtensionContent = {
+ globals: new Map(),
+
+ init(global) {
+ this.globals.set(global, new ExtensionGlobal(global));
+ ExtensionChild.init(global);
+ },
+
+ uninit(global) {
+ ExtensionChild.uninit(global);
+ this.globals.get(global).uninit();
+ this.globals.delete(global);
+ },
+
+ // This helper is exported to be integrated in the devtools RDP actors,
+ // that can use it to retrieve the existent WebExtensions ContentScripts
+ // of a target window and be able to show the ContentScripts source in the
+ // DevTools Debugger panel.
+ getContentScriptGlobalsForWindow(window) {
+ return DocumentManager.getContentScriptGlobalsForWindow(window);
+ },
+};
+
+ExtensionManager.init();
diff --git a/toolkit/components/extensions/ExtensionManagement.jsm b/toolkit/components/extensions/ExtensionManagement.jsm
new file mode 100644
index 0000000000..324c5b71b9
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -0,0 +1,321 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionManagement"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+ "resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
+
+XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => {
+ let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ return UUIDMap;
+});
+
+/*
+ * This file should be kept short and simple since it's loaded even
+ * when no extensions are running.
+ */
+
+// Keep track of frame IDs for content windows. Mostly we can just use
+// the outer window ID as the frame ID. However, the API specifies
+// that top-level windows have a frame ID of 0. So we need to keep
+// track of which windows are top-level. This code listens to messages
+// from ExtensionContent to do that.
+var Frames = {
+ // Window IDs of top-level content windows.
+ topWindowIds: new Set(),
+
+ init() {
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return;
+ }
+
+ Services.mm.addMessageListener("Extension:TopWindowID", this);
+ Services.mm.addMessageListener("Extension:RemoveTopWindowID", this, true);
+ },
+
+ isTopWindowId(windowId) {
+ return this.topWindowIds.has(windowId);
+ },
+
+ // Convert an outer window ID to a frame ID. An outer window ID of 0
+ // is invalid.
+ getId(windowId) {
+ if (this.isTopWindowId(windowId)) {
+ return 0;
+ }
+ if (windowId == 0) {
+ return -1;
+ }
+ return windowId;
+ },
+
+ // Convert an outer window ID for a parent window to a frame
+ // ID. Outer window IDs follow the same convention that
+ // |window.top.parent === window.top|. The API works differently,
+ // giving a frame ID of -1 for the the parent of a top-level
+ // window. This function handles the conversion.
+ getParentId(parentWindowId, windowId) {
+ if (parentWindowId == windowId) {
+ // We have a top-level window.
+ return -1;
+ }
+
+ // Not a top-level window. Just return the ID as normal.
+ return this.getId(parentWindowId);
+ },
+
+ receiveMessage({name, data}) {
+ switch (name) {
+ case "Extension:TopWindowID":
+ // FIXME: Need to handle the case where the content process
+ // crashes. Right now we leak its top window IDs.
+ this.topWindowIds.add(data.windowId);
+ break;
+
+ case "Extension:RemoveTopWindowID":
+ this.topWindowIds.delete(data.windowId);
+ break;
+ }
+ },
+};
+Frames.init();
+
+var APIs = {
+ apis: new Map(),
+
+ register(namespace, schema, script) {
+ if (this.apis.has(namespace)) {
+ throw new Error(`API namespace already exists: ${namespace}`);
+ }
+
+ this.apis.set(namespace, {schema, script});
+ },
+
+ unregister(namespace) {
+ if (!this.apis.has(namespace)) {
+ throw new Error(`API namespace does not exist: ${namespace}`);
+ }
+
+ this.apis.delete(namespace);
+ },
+};
+
+function getURLForExtension(id, path = "") {
+ let uuid = UUIDMap.get(id, false);
+ if (!uuid) {
+ Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`);
+ return null;
+ }
+ return `moz-extension://${uuid}/${path}`;
+}
+
+// This object manages various platform-level issues related to
+// moz-extension:// URIs. It lives here so that it can be used in both
+// the parent and child processes.
+//
+// moz-extension URIs have the form moz-extension://uuid/path. Each
+// extension has its own UUID, unique to the machine it's installed
+// on. This is easier and more secure than using the extension ID,
+// since it makes it slightly harder to fingerprint for extensions if
+// each user uses different URIs for the extension.
+var Service = {
+ initialized: false,
+
+ // Map[uuid -> extension].
+ // extension can be an Extension (parent process) or BrowserExtensionContent (child process).
+ uuidMap: new Map(),
+
+ init() {
+ let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService);
+ aps = aps.wrappedJSObject;
+ this.aps = aps;
+ aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this));
+ aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this));
+ },
+
+ // Called when a new extension is loaded.
+ startupExtension(uuid, uri, extension) {
+ if (!this.initialized) {
+ this.initialized = true;
+ this.init();
+ }
+
+ // Create the moz-extension://uuid mapping.
+ let handler = Services.io.getProtocolHandler("moz-extension");
+ handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
+ handler.setSubstitution(uuid, uri);
+
+ this.uuidMap.set(uuid, extension);
+ this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension));
+ this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
+ this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
+ this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
+ this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension));
+ },
+
+ // Called when an extension is unloaded.
+ shutdownExtension(uuid) {
+ let extension = this.uuidMap.get(uuid);
+ this.uuidMap.delete(uuid);
+ this.aps.setAddonHasPermissionCallback(extension.id, null);
+ this.aps.setAddonLoadURICallback(extension.id, null);
+ this.aps.setAddonLocalizeCallback(extension.id, null);
+ this.aps.setAddonCSP(extension.id, null);
+ this.aps.setBackgroundPageUrlCallback(uuid, null);
+
+ let handler = Services.io.getProtocolHandler("moz-extension");
+ handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
+ handler.setSubstitution(uuid, null);
+ },
+
+ // Return true if the given URI can be loaded from arbitrary web
+ // content. The manifest.json |web_accessible_resources| directive
+ // determines this.
+ extensionURILoadableByAnyone(uri) {
+ let uuid = uri.host;
+ let extension = this.uuidMap.get(uuid);
+ if (!extension || !extension.webAccessibleResources) {
+ return false;
+ }
+
+ let path = uri.QueryInterface(Ci.nsIURL).filePath;
+ if (path.length > 0 && path[0] == "/") {
+ path = path.substr(1);
+ }
+ return extension.webAccessibleResources.matches(path);
+ },
+
+ // Checks whether a given extension can load this URI (typically via
+ // an XML HTTP request). The manifest.json |permissions| directive
+ // determines this.
+ checkAddonMayLoad(extension, uri) {
+ return extension.whiteListedHosts.matchesIgnoringPath(uri);
+ },
+
+ generateBackgroundPageUrl(extension) {
+ let background_scripts = extension.manifest.background &&
+ extension.manifest.background.scripts;
+ if (!background_scripts) {
+ return;
+ }
+ let html = "<!DOCTYPE html>\n<body>\n";
+ for (let script of background_scripts) {
+ script = script.replace(/"/g, "&quot;");
+ html += `<script src="${script}"></script>\n`;
+ }
+ html += "</body>\n</html>\n";
+ return "data:text/html;charset=utf-8," + encodeURIComponent(html);
+ },
+
+ // Finds the add-on ID associated with a given moz-extension:// URI.
+ // This is used to set the addonId on the originAttributes for the
+ // nsIPrincipal attached to the URI.
+ extensionURIToAddonID(uri) {
+ let uuid = uri.host;
+ let extension = this.uuidMap.get(uuid);
+ return extension ? extension.id : undefined;
+ },
+};
+
+// API Levels Helpers
+
+// Find the add-on associated with this document via the
+// principal's originAttributes. This value is computed by
+// extensionURIToAddonID, which ensures that we don't inject our
+// API into webAccessibleResources or remote web pages.
+function getAddonIdForWindow(window) {
+ return Cu.getObjectPrincipal(window).originAttributes.addonId;
+}
+
+const API_LEVELS = Object.freeze({
+ NO_PRIVILEGES: 0,
+ CONTENTSCRIPT_PRIVILEGES: 1,
+ FULL_PRIVILEGES: 2,
+});
+
+// Finds the API Level ("FULL_PRIVILEGES", "CONTENTSCRIPT_PRIVILEGES", "NO_PRIVILEGES")
+// with a given a window object.
+function getAPILevelForWindow(window, addonId) {
+ const {NO_PRIVILEGES, CONTENTSCRIPT_PRIVILEGES, FULL_PRIVILEGES} = API_LEVELS;
+
+ // Non WebExtension URLs and WebExtension URLs from a different extension
+ // has no access to APIs.
+ if (!addonId || getAddonIdForWindow(window) != addonId) {
+ return NO_PRIVILEGES;
+ }
+
+ // Extension pages running in the content process always defaults to
+ // "content script API level privileges".
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return CONTENTSCRIPT_PRIVILEGES;
+ }
+
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ // Handling of ExtensionPages running inside sub-frames.
+ if (docShell.sameTypeParent) {
+ let parentWindow = docShell.sameTypeParent.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ // The option page iframe embedded in the about:addons tab should have
+ // full API level privileges. (see Bug 1256282 for rationale)
+ let parentDocument = parentWindow.document;
+ let parentIsSystemPrincipal = Services.scriptSecurityManager
+ .isSystemPrincipal(parentDocument.nodePrincipal);
+ if (parentDocument.location.href == "about:addons" && parentIsSystemPrincipal) {
+ return FULL_PRIVILEGES;
+ }
+
+ // The addon iframes embedded in a addon page from with the same addonId
+ // should have the same privileges of the sameTypeParent.
+ // (see Bug 1258347 for rationale)
+ let parentSameAddonPrivileges = getAPILevelForWindow(parentWindow, addonId);
+ if (parentSameAddonPrivileges > NO_PRIVILEGES) {
+ return parentSameAddonPrivileges;
+ }
+
+ // In all the other cases, WebExtension URLs loaded into sub-frame UI
+ // will have "content script API level privileges".
+ // (see Bug 1214658 for rationale)
+ return CONTENTSCRIPT_PRIVILEGES;
+ }
+
+ // WebExtension URLs loaded into top frames UI could have full API level privileges.
+ return FULL_PRIVILEGES;
+}
+
+this.ExtensionManagement = {
+ startupExtension: Service.startupExtension.bind(Service),
+ shutdownExtension: Service.shutdownExtension.bind(Service),
+
+ registerAPI: APIs.register.bind(APIs),
+ unregisterAPI: APIs.unregister.bind(APIs),
+
+ getFrameId: Frames.getId.bind(Frames),
+ getParentFrameId: Frames.getParentId.bind(Frames),
+
+ getURLForExtension,
+
+ // exported API Level Helpers
+ getAddonIdForWindow,
+ getAPILevelForWindow,
+ API_LEVELS,
+
+ APIs,
+};
diff --git a/toolkit/components/extensions/ExtensionParent.jsm b/toolkit/components/extensions/ExtensionParent.jsm
new file mode 100644
index 0000000000..b88500d1e3
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -0,0 +1,551 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * This module contains code for managing APIs that need to run in the
+ * parent process, and handles the parent side of operations that need
+ * to be proxied from ExtensionChild.jsm.
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* exported ExtensionParent */
+
+this.EXPORTED_SYMBOLS = ["ExtensionParent"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
+ "resource://gre/modules/NativeMessaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ BaseContext,
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+var {
+ MessageManagerProxy,
+ SpreadArgs,
+ defineLazyGetter,
+ findPathInObject,
+} = ExtensionUtils;
+
+const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
+const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
+const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
+
+let schemaURLs = new Set();
+
+if (!AppConstants.RELEASE_OR_BETA) {
+ schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
+}
+
+let GlobalManager;
+let ParentAPIManager;
+let ProxyMessenger;
+
+// This object loads the ext-*.js scripts that define the extension API.
+let apiManager = new class extends SchemaAPIManager {
+ constructor() {
+ super("main");
+ this.initialized = null;
+ }
+
+ // Loads all the ext-*.js scripts currently registered.
+ lazyInit() {
+ if (this.initialized) {
+ return this.initialized;
+ }
+
+ // Load order matters here. The base manifest defines types which are
+ // extended by other schemas, so needs to be loaded first.
+ let promise = Schemas.load(BASE_SCHEMA).then(() => {
+ let promises = [];
+ for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
+ promises.push(Schemas.load(url));
+ }
+ for (let url of schemaURLs) {
+ promises.push(Schemas.load(url));
+ }
+ return Promise.all(promises);
+ });
+
+ for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
+ this.loadScript(value);
+ }
+
+ this.initialized = promise;
+ return this.initialized;
+ }
+
+ registerSchemaAPI(namespace, envType, getAPI) {
+ if (envType == "addon_parent" || envType == "content_parent") {
+ super.registerSchemaAPI(namespace, envType, getAPI);
+ }
+ }
+}();
+
+// Subscribes to messages related to the extension messaging API and forwards it
+// to the relevant message manager. The "sender" field for the `onMessage` and
+// `onConnect` events are updated if needed.
+ProxyMessenger = {
+ _initialized: false,
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ // TODO(robwu): When addons move to a separate process, we should use the
+ // parent process manager(s) of the addon process(es) instead of the
+ // in-process one.
+ let pipmm = Services.ppmm.getChildAt(0);
+ // Listen on the global frame message manager because content scripts send
+ // and receive extension messages via their frame.
+ // Listen on the parent process message manager because `runtime.connect`
+ // and `runtime.sendMessage` requests must be delivered to all frames in an
+ // addon process (by the API contract).
+ // And legacy addons are not associated with a frame, so that is another
+ // reason for having a parent process manager here.
+ let messageManagers = [Services.mm, pipmm];
+
+ MessageChannel.addListener(messageManagers, "Extension:Connect", this);
+ MessageChannel.addListener(messageManagers, "Extension:Message", this);
+ MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
+ MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
+ },
+
+ receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
+ if (recipient.toNativeApp) {
+ let {childId, toNativeApp} = recipient;
+ if (messageName == "Extension:Message") {
+ let context = ParentAPIManager.getContextById(childId);
+ return new NativeApp(context, toNativeApp).sendMessage(data);
+ }
+ if (messageName == "Extension:Connect") {
+ let context = ParentAPIManager.getContextById(childId);
+ NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
+ return true;
+ }
+ // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
+ // native messages are handled by NativeApp.
+ return;
+ }
+ let extension = GlobalManager.extensionMap.get(sender.extensionId);
+ let receiverMM = this._getMessageManagerForRecipient(recipient);
+ if (!extension || !receiverMM) {
+ return Promise.reject({
+ result: MessageChannel.RESULT_NO_HANDLER,
+ message: "No matching message handler for the given recipient.",
+ });
+ }
+
+ if ((messageName == "Extension:Message" ||
+ messageName == "Extension:Connect") &&
+ apiManager.global.tabGetSender) {
+ // From ext-tabs.js, undefined on Android.
+ apiManager.global.tabGetSender(extension, target, sender);
+ }
+ return MessageChannel.sendMessage(receiverMM, messageName, data, {
+ sender,
+ recipient,
+ responseType,
+ });
+ },
+
+ /**
+ * @param {object} recipient An object that was passed to
+ * `MessageChannel.sendMessage`.
+ * @returns {object|null} The message manager matching the recipient if found.
+ */
+ _getMessageManagerForRecipient(recipient) {
+ let {extensionId, tabId} = recipient;
+ // tabs.sendMessage / tabs.connect
+ if (tabId) {
+ // `tabId` being set implies that the tabs API is supported, so we don't
+ // need to check whether `TabManager` exists.
+ let tab = apiManager.global.TabManager.getTab(tabId, null, null);
+ return tab && tab.linkedBrowser.messageManager;
+ }
+
+ // runtime.sendMessage / runtime.connect
+ if (extensionId) {
+ // TODO(robwu): map the extensionId to the addon parent process's message
+ // manager when they run in a separate process.
+ return Services.ppmm.getChildAt(0);
+ }
+
+ return null;
+ },
+};
+
+// Responsible for loading extension APIs into the right globals.
+GlobalManager = {
+ // Map[extension ID -> Extension]. Determines which extension is
+ // responsible for content under a particular extension ID.
+ extensionMap: new Map(),
+ initialized: false,
+
+ init(extension) {
+ if (this.extensionMap.size == 0) {
+ ProxyMessenger.init();
+ apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
+ this.initialized = true;
+ }
+
+ this.extensionMap.set(extension.id, extension);
+ },
+
+ uninit(extension) {
+ this.extensionMap.delete(extension.id);
+
+ if (this.extensionMap.size == 0 && this.initialized) {
+ apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
+ this.initialized = false;
+ }
+ },
+
+ _onExtensionBrowser(type, browser) {
+ browser.messageManager.loadFrameScript(`data:,
+ Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
+ ExtensionContent.init(this);
+ addEventListener("unload", function() {
+ ExtensionContent.uninit(this);
+ });
+ `, false);
+ },
+
+ getExtension(extensionId) {
+ return this.extensionMap.get(extensionId);
+ },
+
+ injectInObject(context, isChromeCompat, dest) {
+ apiManager.generateAPIs(context, dest);
+ SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
+ },
+};
+
+/**
+ * The proxied parent side of a context in ExtensionChild.jsm, for the
+ * parent side of a proxied API.
+ */
+class ProxyContextParent extends BaseContext {
+ constructor(envType, extension, params, xulBrowser, principal) {
+ super(envType, extension);
+
+ this.uri = NetUtil.newURI(params.url);
+
+ this.incognito = params.incognito;
+
+ // This message manager is used by ParentAPIManager to send messages and to
+ // close the ProxyContext if the underlying message manager closes. This
+ // message manager object may change when `xulBrowser` swaps docshells, e.g.
+ // when a tab is moved to a different window.
+ this.messageManagerProxy = new MessageManagerProxy(xulBrowser);
+
+ Object.defineProperty(this, "principal", {
+ value: principal, enumerable: true, configurable: true,
+ });
+
+ this.listenerProxies = new Map();
+
+ apiManager.emit("proxy-context-load", this);
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+
+ get xulBrowser() {
+ return this.messageManagerProxy.eventTarget;
+ }
+
+ get parentMessageManager() {
+ return this.messageManagerProxy.messageManager;
+ }
+
+ shutdown() {
+ this.unload();
+ }
+
+ unload() {
+ if (this.unloaded) {
+ return;
+ }
+ this.messageManagerProxy.dispose();
+ super.unload();
+ apiManager.emit("proxy-context-unload", this);
+ }
+}
+
+defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
+ let obj = {};
+ GlobalManager.injectInObject(this, false, obj);
+ return obj;
+});
+
+defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
+ return Cu.Sandbox(this.principal);
+});
+
+/**
+ * The parent side of proxied API context for extension content script
+ * running in ExtensionContent.jsm.
+ */
+class ContentScriptContextParent extends ProxyContextParent {
+}
+
+/**
+ * The parent side of proxied API context for extension page, such as a
+ * background script, a tab page, or a popup, running in
+ * ExtensionChild.jsm.
+ */
+class ExtensionPageContextParent extends ProxyContextParent {
+ constructor(envType, extension, params, xulBrowser) {
+ super(envType, extension, params, xulBrowser, extension.principal);
+
+ this.viewType = params.viewType;
+ }
+
+ // The window that contains this context. This may change due to moving tabs.
+ get xulWindow() {
+ return this.xulBrowser.ownerGlobal;
+ }
+
+ get windowId() {
+ if (!apiManager.global.WindowManager || this.viewType == "background") {
+ return;
+ }
+ // viewType popup or tab:
+ return apiManager.global.WindowManager.getId(this.xulWindow);
+ }
+
+ get tabId() {
+ if (!apiManager.global.TabManager) {
+ return; // Not yet supported on Android.
+ }
+ let {gBrowser} = this.xulBrowser.ownerGlobal;
+ let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
+ return tab && apiManager.global.TabManager.getId(tab);
+ }
+
+ onBrowserChange(browser) {
+ super.onBrowserChange(browser);
+ this.xulBrowser = browser;
+ }
+
+ shutdown() {
+ apiManager.emit("page-shutdown", this);
+ super.shutdown();
+ }
+}
+
+ParentAPIManager = {
+ proxyContexts: new Map(),
+
+ init() {
+ Services.obs.addObserver(this, "message-manager-close", false);
+
+ Services.mm.addMessageListener("API:CreateProxyContext", this);
+ Services.mm.addMessageListener("API:CloseProxyContext", this, true);
+ Services.mm.addMessageListener("API:Call", this);
+ Services.mm.addMessageListener("API:AddListener", this);
+ Services.mm.addMessageListener("API:RemoveListener", this);
+ },
+
+ observe(subject, topic, data) {
+ if (topic === "message-manager-close") {
+ let mm = subject;
+ for (let [childId, context] of this.proxyContexts) {
+ if (context.parentMessageManager === mm) {
+ this.closeProxyContext(childId);
+ }
+ }
+ }
+ },
+
+ shutdownExtension(extensionId) {
+ for (let [childId, context] of this.proxyContexts) {
+ if (context.extension.id == extensionId) {
+ context.shutdown();
+ this.proxyContexts.delete(childId);
+ }
+ }
+ },
+
+ receiveMessage({name, data, target}) {
+ switch (name) {
+ case "API:CreateProxyContext":
+ this.createProxyContext(data, target);
+ break;
+
+ case "API:CloseProxyContext":
+ this.closeProxyContext(data.childId);
+ break;
+
+ case "API:Call":
+ this.call(data, target);
+ break;
+
+ case "API:AddListener":
+ this.addListener(data, target);
+ break;
+
+ case "API:RemoveListener":
+ this.removeListener(data);
+ break;
+ }
+ },
+
+ createProxyContext(data, target) {
+ let {envType, extensionId, childId, principal} = data;
+ if (this.proxyContexts.has(childId)) {
+ throw new Error("A WebExtension context with the given ID already exists!");
+ }
+
+ let extension = GlobalManager.getExtension(extensionId);
+ if (!extension) {
+ throw new Error(`No WebExtension found with ID ${extensionId}`);
+ }
+
+ let context;
+ if (envType == "addon_parent") {
+ // Privileged addon contexts can only be loaded in documents whose main
+ // frame is also the same addon.
+ if (principal.URI.prePath !== extension.baseURI.prePath ||
+ !target.contentPrincipal.subsumes(principal)) {
+ throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
+ }
+ context = new ExtensionPageContextParent(envType, extension, data, target);
+ } else if (envType == "content_parent") {
+ context = new ContentScriptContextParent(envType, extension, data, target, principal);
+ } else {
+ throw new Error(`Invalid WebExtension context envType: ${envType}`);
+ }
+ this.proxyContexts.set(childId, context);
+ },
+
+ closeProxyContext(childId) {
+ let context = this.proxyContexts.get(childId);
+ if (context) {
+ context.unload();
+ this.proxyContexts.delete(childId);
+ }
+ },
+
+ call(data, target) {
+ let context = this.getContextById(data.childId);
+ if (context.parentMessageManager !== target.messageManager) {
+ throw new Error("Got message on unexpected message manager");
+ }
+
+ let reply = result => {
+ if (!context.parentMessageManager) {
+ Cu.reportError("Cannot send function call result: other side closed connection");
+ return;
+ }
+
+ context.parentMessageManager.sendAsyncMessage(
+ "API:CallResult",
+ Object.assign({
+ childId: data.childId,
+ callId: data.callId,
+ }, result));
+ };
+
+ try {
+ let args = Cu.cloneInto(data.args, context.sandbox);
+ let result = findPathInObject(context.apiObj, data.path)(...args);
+
+ if (data.callId) {
+ result = result || Promise.resolve();
+
+ result.then(result => {
+ result = result instanceof SpreadArgs ? [...result] : [result];
+
+ reply({result});
+ }, error => {
+ error = context.normalizeError(error);
+ reply({error: {message: error.message}});
+ });
+ }
+ } catch (e) {
+ if (data.callId) {
+ let error = context.normalizeError(e);
+ reply({error: {message: error.message}});
+ } else {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ addListener(data, target) {
+ let context = this.getContextById(data.childId);
+ if (context.parentMessageManager !== target.messageManager) {
+ throw new Error("Got message on unexpected message manager");
+ }
+
+ let {childId} = data;
+
+ function listener(...listenerArgs) {
+ return context.sendMessage(
+ context.parentMessageManager,
+ "API:RunListener",
+ {
+ childId,
+ listenerId: data.listenerId,
+ path: data.path,
+ args: listenerArgs,
+ },
+ {
+ recipient: {childId},
+ });
+ }
+
+ context.listenerProxies.set(data.listenerId, listener);
+
+ let args = Cu.cloneInto(data.args, context.sandbox);
+ findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
+ },
+
+ removeListener(data) {
+ let context = this.getContextById(data.childId);
+ let listener = context.listenerProxies.get(data.listenerId);
+ findPathInObject(context.apiObj, data.path).removeListener(listener);
+ },
+
+ getContextById(childId) {
+ let context = this.proxyContexts.get(childId);
+ if (!context) {
+ let error = new Error("WebExtension context not found!");
+ Cu.reportError(error);
+ throw error;
+ }
+ return context;
+ },
+};
+
+ParentAPIManager.init();
+
+
+const ExtensionParent = {
+ GlobalManager,
+ ParentAPIManager,
+ apiManager,
+};
diff --git a/toolkit/components/extensions/ExtensionStorage.jsm b/toolkit/components/extensions/ExtensionStorage.jsm
new file mode 100644
index 0000000000..0b0ffb0003
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionStorage.jsm
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionStorage"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+
+function jsonReplacer(key, value) {
+ switch (typeof(value)) {
+ // Serialize primitive types as-is.
+ case "string":
+ case "number":
+ case "boolean":
+ return value;
+
+ case "object":
+ if (value === null) {
+ return value;
+ }
+
+ switch (Cu.getClassName(value, true)) {
+ // Serialize arrays and ordinary objects as-is.
+ case "Array":
+ case "Object":
+ return value;
+
+ // Serialize Date objects and regular expressions as their
+ // string representations.
+ case "Date":
+ case "RegExp":
+ return String(value);
+ }
+ break;
+ }
+
+ if (!key) {
+ // If this is the root object, and we can't serialize it, serialize
+ // the value to an empty object.
+ return {};
+ }
+
+ // Everything else, omit entirely.
+ return undefined;
+}
+
+this.ExtensionStorage = {
+ cache: new Map(),
+ listeners: new Map(),
+
+ /**
+ * Sanitizes the given value, and returns a JSON-compatible
+ * representation of it, based on the privileges of the given global.
+ *
+ * @param {value} value
+ * The value to sanitize.
+ * @param {Context} context
+ * The extension context in which to sanitize the value
+ * @returns {value}
+ * The sanitized value.
+ */
+ sanitize(value, context) {
+ let json = context.jsonStringify(value, jsonReplacer);
+ return JSON.parse(json);
+ },
+
+ getExtensionDir(extensionId) {
+ return OS.Path.join(this.extensionDir, extensionId);
+ },
+
+ getStorageFile(extensionId) {
+ return OS.Path.join(this.extensionDir, extensionId, "storage.js");
+ },
+
+ read(extensionId) {
+ if (this.cache.has(extensionId)) {
+ return this.cache.get(extensionId);
+ }
+
+ let path = this.getStorageFile(extensionId);
+ let decoder = new TextDecoder();
+ let promise = OS.File.read(path);
+ promise = promise.then(array => {
+ return JSON.parse(decoder.decode(array));
+ }).catch((error) => {
+ if (!error.becauseNoSuchFile) {
+ Cu.reportError("Unable to parse JSON data for extension storage.");
+ }
+ return {};
+ });
+ this.cache.set(extensionId, promise);
+ return promise;
+ },
+
+ write(extensionId) {
+ let promise = this.read(extensionId).then(extData => {
+ let encoder = new TextEncoder();
+ let array = encoder.encode(JSON.stringify(extData));
+ let path = this.getStorageFile(extensionId);
+ OS.File.makeDir(this.getExtensionDir(extensionId), {
+ ignoreExisting: true,
+ from: OS.Constants.Path.profileDir,
+ });
+ let promise = OS.File.writeAtomic(path, array);
+ return promise;
+ }).catch(() => {
+ // Make sure this promise is never rejected.
+ Cu.reportError("Unable to write JSON data for extension storage.");
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "ExtensionStorage: Finish writing extension data",
+ promise);
+
+ return promise.then(() => {
+ AsyncShutdown.profileBeforeChange.removeBlocker(promise);
+ });
+ },
+
+ set(extensionId, items, context) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop in items) {
+ let item = this.sanitize(items[prop], context);
+ changes[prop] = {oldValue: extData[prop], newValue: item};
+ extData[prop] = item;
+ }
+
+ this.notifyListeners(extensionId, changes);
+
+ return this.write(extensionId);
+ });
+ },
+
+ remove(extensionId, items) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop of [].concat(items)) {
+ changes[prop] = {oldValue: extData[prop]};
+ delete extData[prop];
+ }
+
+ this.notifyListeners(extensionId, changes);
+
+ return this.write(extensionId);
+ });
+ },
+
+ clear(extensionId) {
+ return this.read(extensionId).then(extData => {
+ let changes = {};
+ for (let prop of Object.keys(extData)) {
+ changes[prop] = {oldValue: extData[prop]};
+ delete extData[prop];
+ }
+
+ this.notifyListeners(extensionId, changes);
+
+ return this.write(extensionId);
+ });
+ },
+
+ get(extensionId, keys) {
+ return this.read(extensionId).then(extData => {
+ let result = {};
+ if (keys === null) {
+ Object.assign(result, extData);
+ } else if (typeof(keys) == "object" && !Array.isArray(keys)) {
+ for (let prop in keys) {
+ if (prop in extData) {
+ result[prop] = extData[prop];
+ } else {
+ result[prop] = keys[prop];
+ }
+ }
+ } else {
+ for (let prop of [].concat(keys)) {
+ if (prop in extData) {
+ result[prop] = extData[prop];
+ }
+ }
+ }
+
+ return result;
+ });
+ },
+
+ addOnChangedListener(extensionId, listener) {
+ let listeners = this.listeners.get(extensionId) || new Set();
+ listeners.add(listener);
+ this.listeners.set(extensionId, listeners);
+ },
+
+ removeOnChangedListener(extensionId, listener) {
+ let listeners = this.listeners.get(extensionId);
+ listeners.delete(listener);
+ },
+
+ notifyListeners(extensionId, changes) {
+ let listeners = this.listeners.get(extensionId);
+ if (listeners) {
+ for (let listener of listeners) {
+ listener(changes);
+ }
+ }
+ },
+
+ init() {
+ if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ return;
+ }
+ Services.obs.addObserver(this, "extension-invalidate-storage-cache", false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ observe(subject, topic, data) {
+ if (topic == "xpcom-shutdown") {
+ Services.obs.removeObserver(this, "extension-invalidate-storage-cache");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ } else if (topic == "extension-invalidate-storage-cache") {
+ this.cache.clear();
+ }
+ },
+};
+
+XPCOMUtils.defineLazyGetter(ExtensionStorage, "extensionDir",
+ () => OS.Path.join(OS.Constants.Path.profileDir, "browser-extension-data"));
+
+ExtensionStorage.init();
diff --git a/toolkit/components/extensions/ExtensionStorageSync.jsm b/toolkit/components/extensions/ExtensionStorageSync.jsm
new file mode 100644
index 0000000000..2455b8e0a1
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -0,0 +1,848 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// TODO:
+// * find out how the Chrome implementation deals with conflicts
+
+"use strict";
+
+/* exported extensionIdToCollectionId */
+
+this.EXPORTED_SYMBOLS = ["ExtensionStorageSync"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+const global = this;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+const KINTO_PROD_SERVER_URL = "https://webextensions.settings.services.mozilla.com/v1";
+const KINTO_DEV_SERVER_URL = "https://webextensions.dev.mozaws.net/v1";
+const KINTO_DEFAULT_SERVER_URL = AppConstants.RELEASE_OR_BETA ? KINTO_PROD_SERVER_URL : KINTO_DEV_SERVER_URL;
+
+const STORAGE_SYNC_ENABLED_PREF = "webextensions.storage.sync.enabled";
+const STORAGE_SYNC_SERVER_URL_PREF = "webextensions.storage.sync.serverURL";
+const STORAGE_SYNC_SCOPE = "sync:addon_storage";
+const STORAGE_SYNC_CRYPTO_COLLECTION_NAME = "storage-sync-crypto";
+const STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID = "keys";
+const FXA_OAUTH_OPTIONS = {
+ scope: STORAGE_SYNC_SCOPE,
+};
+// Default is 5sec, which seems a bit aggressive on the open internet
+const KINTO_REQUEST_TIMEOUT = 30000;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+const {
+ runSafeSyncWithoutClone,
+} = Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils",
+ "resource://gre/modules/AppsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
+ "resource://services-sync/record.js");
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
+ "resource://services-common/utils.js");
+XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
+ "resource://services-crypto/utils.js");
+XPCOMUtils.defineLazyModuleGetter(this, "EncryptionRemoteTransformer",
+ "resource://services-sync/engines/extension-storage.js");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+ "resource://gre/modules/ExtensionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "KintoHttpClient",
+ "resource://services-common/kinto-http-client.js");
+XPCOMUtils.defineLazyModuleGetter(this, "loadKinto",
+ "resource://services-common/kinto-offline-client.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Observers",
+ "resource://services-common/observers.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "KeyRingEncryptionRemoteTransformer",
+ "resource://services-sync/engines/extension-storage.js");
+XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
+ STORAGE_SYNC_ENABLED_PREF, false);
+XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
+ STORAGE_SYNC_SERVER_URL_PREF,
+ KINTO_DEFAULT_SERVER_URL);
+
+/* globals prefPermitsStorageSync, prefStorageSyncServerURL */
+
+// Map of Extensions to Set<Contexts> to track contexts that are still
+// "live" and use storage.sync.
+const extensionContexts = new Map();
+// Borrow logger from Sync.
+const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
+
+/**
+ * A Promise that centralizes initialization of ExtensionStorageSync.
+ *
+ * This centralizes the use of the Sqlite database, to which there is
+ * only one connection which is shared by all threads.
+ *
+ * Fields in the object returned by this Promise:
+ *
+ * - connection: a Sqlite connection. Meant for internal use only.
+ * - kinto: a KintoBase object, suitable for using in Firefox. All
+ * collections in this database will use the same Sqlite connection.
+ */
+const storageSyncInit = Task.spawn(function* () {
+ const Kinto = loadKinto();
+ const path = "storage-sync.sqlite";
+ const opts = {path, sharedMemoryCache: false};
+ const connection = yield Sqlite.openConnection(opts);
+ yield Kinto.adapters.FirefoxAdapter._init(connection);
+ return {
+ connection,
+ kinto: new Kinto({
+ adapter: Kinto.adapters.FirefoxAdapter,
+ adapterOptions: {sqliteHandle: connection},
+ timeout: KINTO_REQUEST_TIMEOUT,
+ }),
+ };
+});
+
+AsyncShutdown.profileBeforeChange.addBlocker(
+ "ExtensionStorageSync: close Sqlite handle",
+ Task.async(function* () {
+ const ret = yield storageSyncInit;
+ const {connection} = ret;
+ yield connection.close();
+ })
+);
+// Kinto record IDs have two condtions:
+//
+// - They must contain only ASCII alphanumerics plus - and _. To fix
+// this, we encode all non-letters using _C_, where C is the
+// percent-encoded character, so space becomes _20_
+// and underscore becomes _5F_.
+//
+// - They must start with an ASCII letter. To ensure this, we prefix
+// all keys with "key-".
+function keyToId(key) {
+ function escapeChar(match) {
+ return "_" + match.codePointAt(0).toString(16).toUpperCase() + "_";
+ }
+ return "key-" + key.replace(/[^a-zA-Z0-9]/g, escapeChar);
+}
+
+// Convert a Kinto ID back into a chrome.storage key.
+// Returns null if a key couldn't be parsed.
+function idToKey(id) {
+ function unescapeNumber(match, group1) {
+ return String.fromCodePoint(parseInt(group1, 16));
+ }
+ // An escaped ID should match this regex.
+ // An escaped ID should consist of only letters and numbers, plus
+ // code points escaped as _[0-9a-f]+_.
+ const ESCAPED_ID_FORMAT = /^(?:[a-zA-Z0-9]|_[0-9A-F]+_)*$/;
+
+ if (!id.startsWith("key-")) {
+ return null;
+ }
+ const unprefixed = id.slice(4);
+ // Verify that the ID is the correct format.
+ if (!ESCAPED_ID_FORMAT.test(unprefixed)) {
+ return null;
+ }
+ return unprefixed.replace(/_([0-9A-F]+)_/g, unescapeNumber);
+}
+
+// An "id schema" used to validate Kinto IDs and generate new ones.
+const storageSyncIdSchema = {
+ // We should never generate IDs; chrome.storage only acts as a
+ // key-value store, so we should always have a key.
+ generate() {
+ throw new Error("cannot generate IDs");
+ },
+
+ // See keyToId and idToKey for more details.
+ validate(id) {
+ return idToKey(id) !== null;
+ },
+};
+
+// An "id schema" used for the system collection, which doesn't
+// require validation or generation of IDs.
+const cryptoCollectionIdSchema = {
+ generate() {
+ throw new Error("cannot generate IDs for system collection");
+ },
+
+ validate(id) {
+ return true;
+ },
+};
+
+let cryptoCollection, CollectionKeyEncryptionRemoteTransformer;
+if (AppConstants.platform != "android") {
+ /**
+ * Wrapper around the crypto collection providing some handy utilities.
+ */
+ cryptoCollection = this.cryptoCollection = {
+ getCollection: Task.async(function* () {
+ const {kinto} = yield storageSyncInit;
+ return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
+ idSchema: cryptoCollectionIdSchema,
+ remoteTransformers: [new KeyRingEncryptionRemoteTransformer()],
+ });
+ }),
+
+ /**
+ * Retrieve the keyring record from the crypto collection.
+ *
+ * You can use this if you want to check metadata on the keyring
+ * record rather than use the keyring itself.
+ *
+ * @returns {Promise<Object>}
+ */
+ getKeyRingRecord: Task.async(function* () {
+ const collection = yield this.getCollection();
+ const cryptoKeyRecord = yield collection.getAny(STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID);
+
+ let data = cryptoKeyRecord.data;
+ if (!data) {
+ // This is a new keyring. Invent an ID for this record. If this
+ // changes, it means a client replaced the keyring, so we need to
+ // reupload everything.
+ const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ const uuid = uuidgen.generateUUID().toString();
+ data = {uuid};
+ }
+ return data;
+ }),
+
+ /**
+ * Retrieve the actual keyring from the crypto collection.
+ *
+ * @returns {Promise<CollectionKeyManager>}
+ */
+ getKeyRing: Task.async(function* () {
+ const cryptoKeyRecord = yield this.getKeyRingRecord();
+ const collectionKeys = new CollectionKeyManager();
+ if (cryptoKeyRecord.keys) {
+ collectionKeys.setContents(cryptoKeyRecord.keys, cryptoKeyRecord.last_modified);
+ } else {
+ // We never actually use the default key, so it's OK if we
+ // generate one multiple times.
+ collectionKeys.generateDefaultKey();
+ }
+ // Pass through uuid field so that we can save it if we need to.
+ collectionKeys.uuid = cryptoKeyRecord.uuid;
+ return collectionKeys;
+ }),
+
+ updateKBHash: Task.async(function* (kbHash) {
+ const coll = yield this.getCollection();
+ yield coll.update({id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
+ kbHash: kbHash},
+ {patch: true});
+ }),
+
+ upsert: Task.async(function* (record) {
+ const collection = yield this.getCollection();
+ yield collection.upsert(record);
+ }),
+
+ sync: Task.async(function* () {
+ const collection = yield this.getCollection();
+ return yield ExtensionStorageSync._syncCollection(collection, {
+ strategy: "server_wins",
+ });
+ }),
+
+ /**
+ * Reset sync status for ALL collections by directly
+ * accessing the FirefoxAdapter.
+ */
+ resetSyncStatus: Task.async(function* () {
+ const coll = yield this.getCollection();
+ yield coll.db.resetSyncStatus();
+ }),
+
+ // Used only for testing.
+ _clear: Task.async(function* () {
+ const collection = yield this.getCollection();
+ yield collection.clear();
+ }),
+ };
+
+ /**
+ * An EncryptionRemoteTransformer that uses the special "keys" record
+ * to find a key for a given extension.
+ *
+ * @param {string} extensionId The extension ID for which to find a key.
+ */
+ CollectionKeyEncryptionRemoteTransformer = class extends EncryptionRemoteTransformer {
+ constructor(extensionId) {
+ super();
+ this.extensionId = extensionId;
+ }
+
+ getKeys() {
+ const self = this;
+ return Task.spawn(function* () {
+ // FIXME: cache the crypto record for the duration of a sync cycle?
+ const collectionKeys = yield cryptoCollection.getKeyRing();
+ if (!collectionKeys.hasKeysFor([self.extensionId])) {
+ // This should never happen. Keys should be created (and
+ // synced) at the beginning of the sync cycle.
+ throw new Error(`tried to encrypt records for ${this.extensionId}, but key is not present`);
+ }
+ return collectionKeys.keyForCollection(self.extensionId);
+ });
+ }
+ };
+ global.CollectionKeyEncryptionRemoteTransformer = CollectionKeyEncryptionRemoteTransformer;
+}
+/**
+ * Clean up now that one context is no longer using this extension's collection.
+ *
+ * @param {Extension} extension
+ * The extension whose context just ended.
+ * @param {Context} context
+ * The context that just ended.
+ */
+function cleanUpForContext(extension, context) {
+ const contexts = extensionContexts.get(extension);
+ if (!contexts) {
+ Cu.reportError(new Error(`Internal error: cannot find any contexts for extension ${extension.id}`));
+ }
+ contexts.delete(context);
+ if (contexts.size === 0) {
+ // Nobody else is using this collection. Clean up.
+ extensionContexts.delete(extension);
+ }
+}
+
+/**
+ * Generate a promise that produces the Collection for an extension.
+ *
+ * @param {Extension} extension
+ * The extension whose collection needs to
+ * be opened.
+ * @param {Context} context
+ * The context for this extension. The Collection
+ * will shut down automatically when all contexts
+ * close.
+ * @returns {Promise<Collection>}
+ */
+const openCollection = Task.async(function* (extension, context) {
+ let collectionId = extension.id;
+ const {kinto} = yield storageSyncInit;
+ const remoteTransformers = [];
+ if (CollectionKeyEncryptionRemoteTransformer) {
+ remoteTransformers.push(new CollectionKeyEncryptionRemoteTransformer(extension.id));
+ }
+ const coll = kinto.collection(collectionId, {
+ idSchema: storageSyncIdSchema,
+ remoteTransformers,
+ });
+ return coll;
+});
+
+/**
+ * Hash an extension ID for a given user so that an attacker can't
+ * identify the extensions a user has installed.
+ *
+ * @param {User} user
+ * The user for whom to choose a collection to sync
+ * an extension to.
+ * @param {string} extensionId The extension ID to obfuscate.
+ * @returns {string} A collection ID suitable for use to sync to.
+ */
+function extensionIdToCollectionId(user, extensionId) {
+ const userFingerprint = CryptoUtils.hkdf(user.uid, undefined,
+ "identity.mozilla.com/picl/v1/chrome.storage.sync.collectionIds", 2 * 32);
+ let data = new TextEncoder().encode(userFingerprint + extensionId);
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return CommonUtils.bytesAsHex(hasher.finish(false));
+}
+
+/**
+ * Verify that we were built on not-Android. Call this as a sanity
+ * check before using cryptoCollection.
+ */
+function ensureCryptoCollection() {
+ if (!cryptoCollection) {
+ throw new Error("Call to ensureKeysFor, but no sync code; are you on Android?");
+ }
+}
+
+// FIXME: This is kind of ugly. Probably we should have
+// ExtensionStorageSync not be a singleton, but a constructed object,
+// and this should be a constructor argument.
+let _fxaService = null;
+if (AppConstants.platform != "android") {
+ _fxaService = fxAccounts;
+}
+
+this.ExtensionStorageSync = {
+ _fxaService,
+ listeners: new WeakMap(),
+
+ syncAll: Task.async(function* () {
+ const extensions = extensionContexts.keys();
+ const extIds = Array.from(extensions, extension => extension.id);
+ log.debug(`Syncing extension settings for ${JSON.stringify(extIds)}\n`);
+ if (extIds.length == 0) {
+ // No extensions to sync. Get out.
+ return;
+ }
+ yield this.ensureKeysFor(extIds);
+ yield this.checkSyncKeyRing();
+ const promises = Array.from(extensionContexts.keys(), extension => {
+ return openCollection(extension).then(coll => {
+ return this.sync(extension, coll);
+ });
+ });
+ yield Promise.all(promises);
+ }),
+
+ sync: Task.async(function* (extension, collection) {
+ const signedInUser = yield this._fxaService.getSignedInUser();
+ if (!signedInUser) {
+ // FIXME: this should support syncing to self-hosted
+ log.info("User was not signed into FxA; cannot sync");
+ throw new Error("Not signed in to FxA");
+ }
+ const collectionId = extensionIdToCollectionId(signedInUser, extension.id);
+ let syncResults;
+ try {
+ syncResults = yield this._syncCollection(collection, {
+ strategy: "client_wins",
+ collection: collectionId,
+ });
+ } catch (err) {
+ log.warn("Syncing failed", err);
+ throw err;
+ }
+
+ let changes = {};
+ for (const record of syncResults.created) {
+ changes[record.key] = {
+ newValue: record.data,
+ };
+ }
+ for (const record of syncResults.updated) {
+ // N.B. It's safe to just pick old.key because it's not
+ // possible to "rename" a record in the storage.sync API.
+ const key = record.old.key;
+ changes[key] = {
+ oldValue: record.old.data,
+ newValue: record.new.data,
+ };
+ }
+ for (const record of syncResults.deleted) {
+ changes[record.key] = {
+ oldValue: record.data,
+ };
+ }
+ for (const conflict of syncResults.resolved) {
+ // FIXME: Should we even send a notification? If so, what
+ // best values for "old" and "new"? This might violate
+ // client code's assumptions, since from their perspective,
+ // we were in state L, but this diff is from R -> L.
+ changes[conflict.remote.key] = {
+ oldValue: conflict.local.data,
+ newValue: conflict.remote.data,
+ };
+ }
+ if (Object.keys(changes).length > 0) {
+ this.notifyListeners(extension, changes);
+ }
+ }),
+
+ /**
+ * Utility function that handles the common stuff about syncing all
+ * Kinto collections (including "meta" collections like the crypto
+ * one).
+ *
+ * @param {Collection} collection
+ * @param {Object} options
+ * Additional options to be passed to sync().
+ * @returns {Promise<SyncResultObject>}
+ */
+ _syncCollection: Task.async(function* (collection, options) {
+ // FIXME: this should support syncing to self-hosted
+ return yield this._requestWithToken(`Syncing ${collection.name}`, function* (token) {
+ const allOptions = Object.assign({}, {
+ remote: prefStorageSyncServerURL,
+ headers: {
+ Authorization: "Bearer " + token,
+ },
+ }, options);
+
+ return yield collection.sync(allOptions);
+ });
+ }),
+
+ // Make a Kinto request with a current FxA token.
+ // If the response indicates that the token might have expired,
+ // retry the request.
+ _requestWithToken: Task.async(function* (description, f) {
+ const fxaToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
+ try {
+ return yield f(fxaToken);
+ } catch (e) {
+ log.error(`${description}: request failed`, e);
+ if (e && e.data && e.data.code == 401) {
+ // Our token might have expired. Refresh and retry.
+ log.info("Token might have expired");
+ yield this._fxaService.removeCachedOAuthToken({token: fxaToken});
+ const newToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
+
+ // If this fails too, let it go.
+ return yield f(newToken);
+ }
+ // Otherwise, we don't know how to handle this error, so just reraise.
+ throw e;
+ }
+ }),
+
+ /**
+ * Helper similar to _syncCollection, but for deleting the user's bucket.
+ */
+ _deleteBucket: Task.async(function* () {
+ return yield this._requestWithToken("Clearing server", function* (token) {
+ const headers = {Authorization: "Bearer " + token};
+ const kintoHttp = new KintoHttpClient(prefStorageSyncServerURL, {
+ headers: headers,
+ timeout: KINTO_REQUEST_TIMEOUT,
+ });
+ return yield kintoHttp.deleteBucket("default");
+ });
+ }),
+
+ /**
+ * Recursive promise that terminates when our local collectionKeys,
+ * as well as that on the server, have keys for all the extensions
+ * in extIds.
+ *
+ * @param {Array<string>} extIds
+ * The IDs of the extensions which need keys.
+ * @returns {Promise<CollectionKeyManager>}
+ */
+ ensureKeysFor: Task.async(function* (extIds) {
+ ensureCryptoCollection();
+
+ const collectionKeys = yield cryptoCollection.getKeyRing();
+ if (collectionKeys.hasKeysFor(extIds)) {
+ return collectionKeys;
+ }
+
+ const kbHash = yield this.getKBHash();
+ const newKeys = yield collectionKeys.ensureKeysFor(extIds);
+ const newRecord = {
+ id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
+ keys: newKeys.asWBO().cleartext,
+ uuid: collectionKeys.uuid,
+ // Add a field for the current kB hash.
+ kbHash: kbHash,
+ };
+ yield cryptoCollection.upsert(newRecord);
+ const result = yield this._syncKeyRing(newRecord);
+ if (result.resolved.length != 0) {
+ // We had a conflict which was automatically resolved. We now
+ // have a new keyring which might have keys for the
+ // collections. Recurse.
+ return yield this.ensureKeysFor(extIds);
+ }
+
+ // No conflicts. We're good.
+ return newKeys;
+ }),
+
+ /**
+ * Get the current user's hashed kB.
+ *
+ * @returns sha256 of the user's kB as a hex string
+ */
+ getKBHash: Task.async(function* () {
+ const signedInUser = yield this._fxaService.getSignedInUser();
+ if (!signedInUser) {
+ throw new Error("User isn't signed in!");
+ }
+
+ if (!signedInUser.kB) {
+ throw new Error("User doesn't have kB??");
+ }
+
+ let kBbytes = CommonUtils.hexToBytes(signedInUser.kB);
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ return CommonUtils.bytesAsHex(CryptoUtils.digestBytes(signedInUser.uid + kBbytes, hasher));
+ }),
+
+ /**
+ * Update the kB in the crypto record.
+ */
+ updateKeyRingKB: Task.async(function* () {
+ ensureCryptoCollection();
+
+ const signedInUser = yield this._fxaService.getSignedInUser();
+ if (!signedInUser) {
+ // Although this function is meant to be called on login,
+ // it's not unreasonable to check any time, even if we aren't
+ // logged in.
+ //
+ // If we aren't logged in, we don't have any information about
+ // the user's kB, so we can't be sure that the user changed
+ // their kB, so just return.
+ return;
+ }
+
+ const thisKBHash = yield this.getKBHash();
+ yield cryptoCollection.updateKBHash(thisKBHash);
+ }),
+
+ /**
+ * Make sure the keyring is up to date and synced.
+ *
+ * This is called on syncs to make sure that we don't sync anything
+ * to any collection unless the key for that collection is on the
+ * server.
+ */
+ checkSyncKeyRing: Task.async(function* () {
+ ensureCryptoCollection();
+
+ yield this.updateKeyRingKB();
+
+ const cryptoKeyRecord = yield cryptoCollection.getKeyRingRecord();
+ if (cryptoKeyRecord && cryptoKeyRecord._status !== "synced") {
+ // We haven't successfully synced the keyring since the last
+ // change. This could be because kB changed and we touched the
+ // keyring, or it could be because we failed to sync after
+ // adding a key. Either way, take this opportunity to sync the
+ // keyring.
+ yield this._syncKeyRing(cryptoKeyRecord);
+ }
+ }),
+
+ _syncKeyRing: Task.async(function* (cryptoKeyRecord) {
+ ensureCryptoCollection();
+
+ try {
+ // Try to sync using server_wins.
+ //
+ // We use server_wins here because whatever is on the server is
+ // at least consistent with itself -- the crypto in the keyring
+ // matches the crypto on the collection records. This is because
+ // we generate and upload keys just before syncing data.
+ //
+ // It's possible that we can't decode the version on the server.
+ // This can happen if a user is locked out of their account, and
+ // does a "reset password" to get in on a new device. In this
+ // case, we are in a bind -- we can't decrypt the record on the
+ // server, so we can't merge keys. If this happens, we try to
+ // figure out if we're the one with the correct (new) kB or if
+ // we just got locked out because we have the old kB. If we're
+ // the one with the correct kB, we wipe the server and reupload
+ // everything, including a new keyring.
+ //
+ // If another device has wiped the server, we need to reupload
+ // everything we have on our end too, so we detect this by
+ // adding a UUID to the keyring. UUIDs are preserved throughout
+ // the lifetime of a keyring, so the only time a keyring UUID
+ // changes is when a new keyring is uploaded, which only happens
+ // after a server wipe. So when we get a "conflict" (resolved by
+ // server_wins), we check whether the server version has a new
+ // UUID. If so, reset our sync status, so that we'll reupload
+ // everything.
+ const result = yield cryptoCollection.sync();
+ if (result.resolved.length > 0) {
+ if (result.resolved[0].uuid != cryptoKeyRecord.uuid) {
+ log.info(`Detected a new UUID (${result.resolved[0].uuid}, was ${cryptoKeyRecord.uuid}). Reseting sync status for everything.`);
+ yield cryptoCollection.resetSyncStatus();
+
+ // Server version is now correct. Return that result.
+ return result;
+ }
+ }
+ // No conflicts, or conflict was just someone else adding keys.
+ return result;
+ } catch (e) {
+ if (KeyRingEncryptionRemoteTransformer.isOutdatedKB(e)) {
+ // Check if our token is still valid, or if we got locked out
+ // between starting the sync and talking to Kinto.
+ const isSessionValid = yield this._fxaService.sessionStatus();
+ if (isSessionValid) {
+ yield this._deleteBucket();
+ yield cryptoCollection.resetSyncStatus();
+
+ // Reupload our keyring, which is the only new keyring.
+ // We don't want client_wins here because another device
+ // could have uploaded another keyring in the meantime.
+ return yield cryptoCollection.sync();
+ }
+ }
+ throw e;
+ }
+ }),
+
+ /**
+ * Get the collection for an extension, and register the extension
+ * as being "in use".
+ *
+ * @param {Extension} extension
+ * The extension for which we are seeking
+ * a collection.
+ * @param {Context} context
+ * The context of the extension, so that we can
+ * stop syncing the collection when the extension ends.
+ * @returns {Promise<Collection>}
+ */
+ getCollection(extension, context) {
+ if (prefPermitsStorageSync !== true) {
+ return Promise.reject({message: `Please set ${STORAGE_SYNC_ENABLED_PREF} to true in about:config`});
+ }
+ // Register that the extension and context are in use.
+ if (!extensionContexts.has(extension)) {
+ extensionContexts.set(extension, new Set());
+ }
+ const contexts = extensionContexts.get(extension);
+ if (!contexts.has(context)) {
+ // New context. Register it and make sure it cleans itself up
+ // when it closes.
+ contexts.add(context);
+ context.callOnClose({
+ close: () => cleanUpForContext(extension, context),
+ });
+ }
+
+ return openCollection(extension, context);
+ },
+
+ set: Task.async(function* (extension, items, context) {
+ const coll = yield this.getCollection(extension, context);
+ const keys = Object.keys(items);
+ const ids = keys.map(keyToId);
+ const changes = yield coll.execute(txn => {
+ let changes = {};
+ for (let [i, key] of keys.entries()) {
+ const id = ids[i];
+ let item = items[key];
+ let {oldRecord} = txn.upsert({
+ id,
+ key,
+ data: item,
+ });
+ changes[key] = {
+ newValue: item,
+ };
+ if (oldRecord && oldRecord.data) {
+ // Extract the "data" field from the old record, which
+ // represents the value part of the key-value store
+ changes[key].oldValue = oldRecord.data;
+ }
+ }
+ return changes;
+ }, {preloadIds: ids});
+ this.notifyListeners(extension, changes);
+ }),
+
+ remove: Task.async(function* (extension, keys, context) {
+ const coll = yield this.getCollection(extension, context);
+ keys = [].concat(keys);
+ const ids = keys.map(keyToId);
+ let changes = {};
+ yield coll.execute(txn => {
+ for (let [i, key] of keys.entries()) {
+ const id = ids[i];
+ const res = txn.deleteAny(id);
+ if (res.deleted) {
+ changes[key] = {
+ oldValue: res.data.data,
+ };
+ }
+ }
+ return changes;
+ }, {preloadIds: ids});
+ if (Object.keys(changes).length > 0) {
+ this.notifyListeners(extension, changes);
+ }
+ }),
+
+ clear: Task.async(function* (extension, context) {
+ // We can't call Collection#clear here, because that just clears
+ // the local database. We have to explicitly delete everything so
+ // that the deletions can be synced as well.
+ const coll = yield this.getCollection(extension, context);
+ const res = yield coll.list();
+ const records = res.data;
+ const keys = records.map(record => record.key);
+ yield this.remove(extension, keys, context);
+ }),
+
+ get: Task.async(function* (extension, spec, context) {
+ const coll = yield this.getCollection(extension, context);
+ let keys, records;
+ if (spec === null) {
+ records = {};
+ const res = yield coll.list();
+ for (let record of res.data) {
+ records[record.key] = record.data;
+ }
+ return records;
+ }
+ if (typeof spec === "string") {
+ keys = [spec];
+ records = {};
+ } else if (Array.isArray(spec)) {
+ keys = spec;
+ records = {};
+ } else {
+ keys = Object.keys(spec);
+ records = Cu.cloneInto(spec, global);
+ }
+
+ for (let key of keys) {
+ const res = yield coll.getAny(keyToId(key));
+ if (res.data && res.data._status != "deleted") {
+ records[res.data.key] = res.data.data;
+ }
+ }
+
+ return records;
+ }),
+
+ addOnChangedListener(extension, listener, context) {
+ let listeners = this.listeners.get(extension) || new Set();
+ listeners.add(listener);
+ this.listeners.set(extension, listeners);
+
+ // Force opening the collection so that we will sync for this extension.
+ return this.getCollection(extension, context);
+ },
+
+ removeOnChangedListener(extension, listener) {
+ let listeners = this.listeners.get(extension);
+ listeners.delete(listener);
+ if (listeners.size == 0) {
+ this.listeners.delete(extension);
+ }
+ },
+
+ notifyListeners(extension, changes) {
+ Observers.notify("ext.storage.sync-changed");
+ let listeners = this.listeners.get(extension) || new Set();
+ if (listeners) {
+ for (let listener of listeners) {
+ runSafeSyncWithoutClone(listener, changes);
+ }
+ }
+ },
+};
diff --git a/toolkit/components/extensions/ExtensionTestCommon.jsm b/toolkit/components/extensions/ExtensionTestCommon.jsm
new file mode 100644
index 0000000000..02453ddfd4
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * This module contains extension testing helper logic which is common
+ * between all test suites.
+ */
+
+/* exported ExtensionTestCommon, MockExtension */
+
+this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.importGlobalProperties(["TextEncoder"]);
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "apiManager",
+ () => ExtensionParent.apiManager);
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const {
+ flushJarCache,
+ instanceOf,
+} = ExtensionUtils;
+
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+
+
+/**
+ * A skeleton Extension-like object, used for testing, which installs an
+ * add-on via the add-on manager when startup() is called, and
+ * uninstalles it on shutdown().
+ *
+ * @param {string} id
+ * @param {nsIFile} file
+ * @param {nsIURI} rootURI
+ * @param {string} installType
+ */
+class MockExtension {
+ constructor(file, rootURI, installType) {
+ this.id = null;
+ this.file = file;
+ this.rootURI = rootURI;
+ this.installType = installType;
+ this.addon = null;
+
+ let promiseEvent = eventName => new Promise(resolve => {
+ let onstartup = (msg, extension) => {
+ if (this.addon && extension.id == this.addon.id) {
+ apiManager.off(eventName, onstartup);
+
+ this.id = extension.id;
+ this._extension = extension;
+ resolve(extension);
+ }
+ };
+ apiManager.on(eventName, onstartup);
+ });
+
+ this._extension = null;
+ this._extensionPromise = promiseEvent("startup");
+ this._readyPromise = promiseEvent("ready");
+ }
+
+ testMessage(...args) {
+ return this._extension.testMessage(...args);
+ }
+
+ on(...args) {
+ this._extensionPromise.then(extension => {
+ extension.on(...args);
+ });
+ }
+
+ off(...args) {
+ this._extensionPromise.then(extension => {
+ extension.off(...args);
+ });
+ }
+
+ startup() {
+ if (this.installType == "temporary") {
+ return AddonManager.installTemporaryAddon(this.file).then(addon => {
+ this.addon = addon;
+ return this._readyPromise;
+ });
+ } else if (this.installType == "permanent") {
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(this.file, install => {
+ let listener = {
+ onInstallFailed: reject,
+ onInstallEnded: (install, newAddon) => {
+ this.addon = newAddon;
+ resolve(this._readyPromise);
+ },
+ };
+
+ install.addListener(listener);
+ install.install();
+ });
+ });
+ }
+ throw new Error("installType must be one of: temporary, permanent");
+ }
+
+ shutdown() {
+ this.addon.uninstall();
+ return this.cleanupGeneratedFile();
+ }
+
+ cleanupGeneratedFile() {
+ flushJarCache(this.file);
+ return OS.File.remove(this.file.path);
+ }
+}
+
+class ExtensionTestCommon {
+ /**
+ * This code is designed to make it easy to test a WebExtension
+ * without creating a bunch of files. Everything is contained in a
+ * single JSON blob.
+ *
+ * Properties:
+ * "background": "<JS code>"
+ * A script to be loaded as the background script.
+ * The "background" section of the "manifest" property is overwritten
+ * if this is provided.
+ * "manifest": {...}
+ * Contents of manifest.json
+ * "files": {"filename1": "contents1", ...}
+ * Data to be included as files. Can be referenced from the manifest.
+ * If a manifest file is provided here, it takes precedence over
+ * a generated one. Always use "/" as a directory separator.
+ * Directories should appear here only implicitly (as a prefix
+ * to file names)
+ *
+ * To make things easier, the value of "background" and "files"[] can
+ * be a function, which is converted to source that is run.
+ *
+ * The generated extension is stored in the system temporary directory,
+ * and an nsIFile object pointing to it is returned.
+ *
+ * @param {object} data
+ * @returns {nsIFile}
+ */
+ static generateXPI(data) {
+ let manifest = data.manifest;
+ if (!manifest) {
+ manifest = {};
+ }
+
+ let files = data.files;
+ if (!files) {
+ files = {};
+ }
+
+ function provide(obj, keys, value, override = false) {
+ if (keys.length == 1) {
+ if (!(keys[0] in obj) || override) {
+ obj[keys[0]] = value;
+ }
+ } else {
+ if (!(keys[0] in obj)) {
+ obj[keys[0]] = {};
+ }
+ provide(obj[keys[0]], keys.slice(1), value, override);
+ }
+ }
+
+ provide(manifest, ["name"], "Generated extension");
+ provide(manifest, ["manifest_version"], 2);
+ provide(manifest, ["version"], "1.0");
+
+ if (data.background) {
+ let bgScript = uuidGen.generateUUID().number + ".js";
+
+ provide(manifest, ["background", "scripts"], [bgScript], true);
+ files[bgScript] = data.background;
+ }
+
+ provide(files, ["manifest.json"], manifest);
+
+ if (data.embedded) {
+ // Package this as a webextension embedded inside a legacy
+ // extension.
+
+ let xpiFiles = {
+ "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="${manifest.applications.gecko.id}"
+ em:name="${manifest.name}"
+ em:type="2"
+ em:version="${manifest.version}"
+ em:description=""
+ em:hasEmbeddedWebExtension="true"
+ em:bootstrap="true">
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description
+ em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
+ em:minVersion="51.0a1"
+ em:maxVersion="*"/>
+ </em:targetApplication>
+ </Description>
+ </RDF>
+ `,
+
+ "bootstrap.js": `
+ function install() {}
+ function uninstall() {}
+ function shutdown() {}
+
+ function startup(data) {
+ data.webExtension.startup();
+ }
+ `,
+ };
+
+ for (let [path, data] of Object.entries(files)) {
+ xpiFiles[`webextension/${path}`] = data;
+ }
+
+ files = xpiFiles;
+ }
+
+ return this.generateZipFile(files);
+ }
+
+ static generateZipFile(files, baseName = "generated-extension.xpi") {
+ let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
+ let zipW = new ZipWriter();
+
+ let file = FileUtils.getFile("TmpD", [baseName]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ const MODE_WRONLY = 0x02;
+ const MODE_TRUNCATE = 0x20;
+ zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
+
+ // Needs to be in microseconds for some reason.
+ let time = Date.now() * 1000;
+
+ function generateFile(filename) {
+ let components = filename.split("/");
+ let path = "";
+ for (let component of components.slice(0, -1)) {
+ path += component + "/";
+ if (!zipW.hasEntry(path)) {
+ zipW.addEntryDirectory(path, time, false);
+ }
+ }
+ }
+
+ for (let filename in files) {
+ let script = files[filename];
+ if (typeof(script) == "function") {
+ script = "(" + script.toString() + ")()";
+ } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
+ script = JSON.stringify(script);
+ }
+
+ if (!instanceOf(script, "ArrayBuffer")) {
+ script = new TextEncoder("utf-8").encode(script).buffer;
+ }
+
+ let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
+ stream.setData(script, 0, script.byteLength);
+
+ generateFile(filename);
+ zipW.addEntryStream(filename, time, 0, stream, false);
+ }
+
+ zipW.close();
+
+ return file;
+ }
+
+ /**
+ * Generates a new extension using |Extension.generateXPI|, and initializes a
+ * new |Extension| instance which will execute it.
+ *
+ * @param {object} data
+ * @returns {Extension}
+ */
+ static generate(data) {
+ let file = this.generateXPI(data);
+
+ flushJarCache(file);
+ Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
+
+ let fileURI = Services.io.newFileURI(file);
+ let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
+
+ // This may be "temporary" or "permanent".
+ if (data.useAddonManager) {
+ return new MockExtension(file, jarURI, data.useAddonManager);
+ }
+
+ let id;
+ if (data.manifest) {
+ if (data.manifest.applications && data.manifest.applications.gecko) {
+ id = data.manifest.applications.gecko.id;
+ } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
+ id = data.manifest.browser_specific_settings.gecko.id;
+ }
+ }
+ if (!id) {
+ id = uuidGen.generateUUID().number;
+ }
+
+ return new Extension({
+ id,
+ resourceURI: jarURI,
+ cleanupFile: file,
+ });
+ }
+}
diff --git a/toolkit/components/extensions/ExtensionUtils.jsm b/toolkit/components/extensions/ExtensionUtils.jsm
new file mode 100644
index 0000000000..e7f768c07c
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -0,0 +1,1215 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionUtils"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+const INTEGER = /^[1-9]\d*$/;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
+ "resource:///modules/translation/LanguageDetector.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+ "resource://gre/modules/Locale.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
+ "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
+ "@mozilla.org/content/style-sheet-service;1",
+ "nsIStyleSheetService");
+
+function getConsole() {
+ return new ConsoleAPI({
+ maxLogLevelPref: "extensions.webextensions.log.level",
+ prefix: "WebExtensions",
+ });
+}
+
+XPCOMUtils.defineLazyGetter(this, "console", getConsole);
+
+let nextId = 0;
+const {uniqueProcessID} = Services.appinfo;
+
+function getUniqueId() {
+ return `${nextId++}-${uniqueProcessID}`;
+}
+
+/**
+ * An Error subclass for which complete error messages are always passed
+ * to extensions, rather than being interpreted as an unknown error.
+ */
+class ExtensionError extends Error {}
+
+function filterStack(error) {
+ return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
+}
+
+// Run a function and report exceptions.
+function runSafeSyncWithoutClone(f, ...args) {
+ try {
+ return f(...args);
+ } catch (e) {
+ dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
+ Cu.reportError(e);
+ }
+}
+
+// Run a function and report exceptions.
+function runSafeWithoutClone(f, ...args) {
+ if (typeof(f) != "function") {
+ dump(`Extension error: expected function\n${filterStack(Error())}`);
+ return;
+ }
+
+ Promise.resolve().then(() => {
+ runSafeSyncWithoutClone(f, ...args);
+ });
+}
+
+// Run a function, cloning arguments into context.cloneScope, and
+// report exceptions. |f| is expected to be in context.cloneScope.
+function runSafeSync(context, f, ...args) {
+ if (context.unloaded) {
+ Cu.reportError("runSafeSync called after context unloaded");
+ return;
+ }
+
+ try {
+ args = Cu.cloneInto(args, context.cloneScope);
+ } catch (e) {
+ Cu.reportError(e);
+ dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
+ }
+ return runSafeSyncWithoutClone(f, ...args);
+}
+
+// Run a function, cloning arguments into context.cloneScope, and
+// report exceptions. |f| is expected to be in context.cloneScope.
+function runSafe(context, f, ...args) {
+ try {
+ args = Cu.cloneInto(args, context.cloneScope);
+ } catch (e) {
+ Cu.reportError(e);
+ dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
+ }
+ if (context.unloaded) {
+ dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
+ return undefined;
+ }
+ return runSafeWithoutClone(f, ...args);
+}
+
+function getInnerWindowID(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+}
+
+// Return true if the given value is an instance of the given
+// native type.
+function instanceOf(value, type) {
+ return {}.toString.call(value) == `[object ${type}]`;
+}
+
+// Extend the object |obj| with the property descriptors of each object in
+// |args|.
+function extend(obj, ...args) {
+ for (let arg of args) {
+ let props = [...Object.getOwnPropertyNames(arg),
+ ...Object.getOwnPropertySymbols(arg)];
+ for (let prop of props) {
+ let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
+ Object.defineProperty(obj, prop, descriptor);
+ }
+ }
+
+ return obj;
+}
+
+/**
+ * Similar to a WeakMap, but creates a new key with the given
+ * constructor if one is not present.
+ */
+class DefaultWeakMap extends WeakMap {
+ constructor(defaultConstructor, init) {
+ super(init);
+ this.defaultConstructor = defaultConstructor;
+ }
+
+ get(key) {
+ if (!this.has(key)) {
+ this.set(key, this.defaultConstructor(key));
+ }
+ return super.get(key);
+ }
+}
+
+class DefaultMap extends Map {
+ constructor(defaultConstructor, init) {
+ super(init);
+ this.defaultConstructor = defaultConstructor;
+ }
+
+ get(key) {
+ if (!this.has(key)) {
+ this.set(key, this.defaultConstructor(key));
+ }
+ return super.get(key);
+ }
+}
+
+class SpreadArgs extends Array {
+ constructor(args) {
+ super();
+ this.push(...args);
+ }
+}
+
+// Manages icon details for toolbar buttons in the |pageAction| and
+// |browserAction| APIs.
+let IconDetails = {
+ // Normalizes the various acceptable input formats into an object
+ // with icon size as key and icon URL as value.
+ //
+ // If a context is specified (function is called from an extension):
+ // Throws an error if an invalid icon size was provided or the
+ // extension is not allowed to load the specified resources.
+ //
+ // If no context is specified, instead of throwing an error, this
+ // function simply logs a warning message.
+ normalize(details, extension, context = null) {
+ let result = {};
+
+ try {
+ if (details.imageData) {
+ let imageData = details.imageData;
+
+ if (typeof imageData == "string") {
+ imageData = {"19": imageData};
+ }
+
+ for (let size of Object.keys(imageData)) {
+ if (!INTEGER.test(size)) {
+ throw new ExtensionError(`Invalid icon size ${size}, must be an integer`);
+ }
+ result[size] = imageData[size];
+ }
+ }
+
+ if (details.path) {
+ let path = details.path;
+ if (typeof path != "object") {
+ path = {"19": path};
+ }
+
+ let baseURI = context ? context.uri : extension.baseURI;
+
+ for (let size of Object.keys(path)) {
+ if (!INTEGER.test(size)) {
+ throw new ExtensionError(`Invalid icon size ${size}, must be an integer`);
+ }
+
+ let url = baseURI.resolve(path[size]);
+
+ // The Chrome documentation specifies these parameters as
+ // relative paths. We currently accept absolute URLs as well,
+ // which means we need to check that the extension is allowed
+ // to load them. This will throw an error if it's not allowed.
+ try {
+ Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+ extension.principal, url,
+ Services.scriptSecurityManager.DISALLOW_SCRIPT);
+ } catch (e) {
+ throw new ExtensionError(`Illegal URL ${url}`);
+ }
+
+ result[size] = url;
+ }
+ }
+ } catch (e) {
+ // Function is called from extension code, delegate error.
+ if (context) {
+ throw e;
+ }
+ // If there's no context, it's because we're handling this
+ // as a manifest directive. Log a warning rather than
+ // raising an error.
+ extension.manifestError(`Invalid icon data: ${e}`);
+ }
+
+ return result;
+ },
+
+ // Returns the appropriate icon URL for the given icons object and the
+ // screen resolution of the given window.
+ getPreferredIcon(icons, extension = null, size = 16) {
+ const DEFAULT = "chrome://browser/content/extension.svg";
+
+ let bestSize = null;
+ if (icons[size]) {
+ bestSize = size;
+ } else if (icons[2 * size]) {
+ bestSize = 2 * size;
+ } else {
+ let sizes = Object.keys(icons)
+ .map(key => parseInt(key, 10))
+ .sort((a, b) => a - b);
+
+ bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
+ }
+
+ if (bestSize) {
+ return {size: bestSize, icon: icons[bestSize]};
+ }
+
+ return {size, icon: DEFAULT};
+ },
+
+ convertImageURLToDataURL(imageURL, contentWindow, browserWindow, size = 18) {
+ return new Promise((resolve, reject) => {
+ let image = new contentWindow.Image();
+ image.onload = function() {
+ let canvas = contentWindow.document.createElement("canvas");
+ let ctx = canvas.getContext("2d");
+ let dSize = size * browserWindow.devicePixelRatio;
+
+ // Scales the image while maintaing width to height ratio.
+ // If the width and height differ, the image is centered using the
+ // smaller of the two dimensions.
+ let dWidth, dHeight, dx, dy;
+ if (this.width > this.height) {
+ dWidth = dSize;
+ dHeight = image.height * (dSize / image.width);
+ dx = 0;
+ dy = (dSize - dHeight) / 2;
+ } else {
+ dWidth = image.width * (dSize / image.height);
+ dHeight = dSize;
+ dx = (dSize - dWidth) / 2;
+ dy = 0;
+ }
+
+ ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
+ resolve(canvas.toDataURL("image/png"));
+ };
+ image.onerror = reject;
+ image.src = imageURL;
+ });
+ },
+};
+
+const LISTENERS = Symbol("listeners");
+
+class EventEmitter {
+ constructor() {
+ this[LISTENERS] = new Map();
+ }
+
+ /**
+ * Adds the given function as a listener for the given event.
+ *
+ * The listener function may optionally return a Promise which
+ * resolves when it has completed all operations which event
+ * dispatchers may need to block on.
+ *
+ * @param {string} event
+ * The name of the event to listen for.
+ * @param {function(string, ...any)} listener
+ * The listener to call when events are emitted.
+ */
+ on(event, listener) {
+ if (!this[LISTENERS].has(event)) {
+ this[LISTENERS].set(event, new Set());
+ }
+
+ this[LISTENERS].get(event).add(listener);
+ }
+
+ /**
+ * Removes the given function as a listener for the given event.
+ *
+ * @param {string} event
+ * The name of the event to stop listening for.
+ * @param {function(string, ...any)} listener
+ * The listener function to remove.
+ */
+ off(event, listener) {
+ if (this[LISTENERS].has(event)) {
+ let set = this[LISTENERS].get(event);
+
+ set.delete(listener);
+ if (!set.size) {
+ this[LISTENERS].delete(event);
+ }
+ }
+ }
+
+ /**
+ * Triggers all listeners for the given event, and returns a promise
+ * which resolves when all listeners have been called, and any
+ * promises they have returned have likewise resolved.
+ *
+ * @param {string} event
+ * The name of the event to emit.
+ * @param {any} args
+ * Arbitrary arguments to pass to the listener functions, after
+ * the event name.
+ * @returns {Promise}
+ */
+ emit(event, ...args) {
+ let listeners = this[LISTENERS].get(event) || new Set();
+
+ let promises = Array.from(listeners, listener => {
+ return runSafeSyncWithoutClone(listener, event, ...args);
+ });
+
+ return Promise.all(promises);
+ }
+}
+
+function LocaleData(data) {
+ this.defaultLocale = data.defaultLocale;
+ this.selectedLocale = data.selectedLocale;
+ this.locales = data.locales || new Map();
+ this.warnedMissingKeys = new Set();
+
+ // Map(locale-name -> Map(message-key -> localized-string))
+ //
+ // Contains a key for each loaded locale, each of which is a
+ // Map of message keys to their localized strings.
+ this.messages = data.messages || new Map();
+
+ if (data.builtinMessages) {
+ this.messages.set(this.BUILTIN, data.builtinMessages);
+ }
+}
+
+
+LocaleData.prototype = {
+ // Representation of the object to send to content processes. This
+ // should include anything the content process might need.
+ serialize() {
+ return {
+ defaultLocale: this.defaultLocale,
+ selectedLocale: this.selectedLocale,
+ messages: this.messages,
+ locales: this.locales,
+ };
+ },
+
+ BUILTIN: "@@BUILTIN_MESSAGES",
+
+ has(locale) {
+ return this.messages.has(locale);
+ },
+
+ // https://developer.chrome.com/extensions/i18n
+ localizeMessage(message, substitutions = [], options = {}) {
+ let defaultOptions = {
+ locale: this.selectedLocale,
+ defaultValue: "",
+ cloneScope: null,
+ };
+
+ options = Object.assign(defaultOptions, options);
+
+ let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale]
+ .filter(locale => this.messages.has(locale)));
+
+ // Message names are case-insensitive, so normalize them to lower-case.
+ message = message.toLowerCase();
+ for (let locale of locales) {
+ let messages = this.messages.get(locale);
+ if (messages.has(message)) {
+ let str = messages.get(message);
+
+ if (!Array.isArray(substitutions)) {
+ substitutions = [substitutions];
+ }
+
+ let replacer = (matched, index, dollarSigns) => {
+ if (index) {
+ // This is not quite Chrome-compatible. Chrome consumes any number
+ // of digits following the $, but only accepts 9 substitutions. We
+ // accept any number of substitutions.
+ index = parseInt(index, 10) - 1;
+ return index in substitutions ? substitutions[index] : "";
+ }
+ // For any series of contiguous `$`s, the first is dropped, and
+ // the rest remain in the output string.
+ return dollarSigns;
+ };
+ return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
+ }
+ }
+
+ // Check for certain pre-defined messages.
+ if (message == "@@ui_locale") {
+ return this.uiLocale;
+ } else if (message.startsWith("@@bidi_")) {
+ let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+ let rtl = registry.isLocaleRTL("global");
+
+ if (message == "@@bidi_dir") {
+ return rtl ? "rtl" : "ltr";
+ } else if (message == "@@bidi_reversed_dir") {
+ return rtl ? "ltr" : "rtl";
+ } else if (message == "@@bidi_start_edge") {
+ return rtl ? "right" : "left";
+ } else if (message == "@@bidi_end_edge") {
+ return rtl ? "left" : "right";
+ }
+ }
+
+ if (!this.warnedMissingKeys.has(message)) {
+ let error = `Unknown localization message ${message}`;
+ if (options.cloneScope) {
+ error = new options.cloneScope.Error(error);
+ }
+ Cu.reportError(error);
+ this.warnedMissingKeys.add(message);
+ }
+ return options.defaultValue;
+ },
+
+ // Localize a string, replacing all |__MSG_(.*)__| tokens with the
+ // matching string from the current locale, as determined by
+ // |this.selectedLocale|.
+ //
+ // This may not be called before calling either |initLocale| or
+ // |initAllLocales|.
+ localize(str, locale = this.selectedLocale) {
+ if (!str) {
+ return str;
+ }
+
+ return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
+ return this.localizeMessage(message, [], {locale, defaultValue: matched});
+ });
+ },
+
+ // Validates the contents of a locale JSON file, normalizes the
+ // messages into a Map of message key -> localized string pairs.
+ addLocale(locale, messages, extension) {
+ let result = new Map();
+
+ // Chrome does not document the semantics of its localization
+ // system very well. It handles replacements by pre-processing
+ // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
+ // replacements. Later, it processes the resulting string for
+ // |$[0-9]| replacements.
+ //
+ // Again, it does not document this, but it accepts any number
+ // of sequential |$|s, and replaces them with that number minus
+ // 1. It also accepts |$| followed by any number of sequential
+ // digits, but refuses to process a localized string which
+ // provides more than 9 substitutions.
+ if (!instanceOf(messages, "Object")) {
+ extension.packagingError(`Invalid locale data for ${locale}`);
+ return result;
+ }
+
+ for (let key of Object.keys(messages)) {
+ let msg = messages[key];
+
+ if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
+ extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
+ continue;
+ }
+
+ // Substitutions are case-insensitive, so normalize all of their names
+ // to lower-case.
+ let placeholders = new Map();
+ if (instanceOf(msg.placeholders, "Object")) {
+ for (let key of Object.keys(msg.placeholders)) {
+ placeholders.set(key.toLowerCase(), msg.placeholders[key]);
+ }
+ }
+
+ let replacer = (match, name) => {
+ let replacement = placeholders.get(name.toLowerCase());
+ if (instanceOf(replacement, "Object") && "content" in replacement) {
+ return replacement.content;
+ }
+ return "";
+ };
+
+ let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
+
+ // Message names are also case-insensitive, so normalize them to lower-case.
+ result.set(key.toLowerCase(), value);
+ }
+
+ this.messages.set(locale, result);
+ return result;
+ },
+
+ get acceptLanguages() {
+ let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString);
+ return result.split(/\s*,\s*/g);
+ },
+
+
+ get uiLocale() {
+ // Return the browser locale, but convert it to a Chrome-style
+ // locale code.
+ return Locale.getLocale().replace(/-/g, "_");
+ },
+};
+
+// This is a generic class for managing event listeners. Example usage:
+//
+// new EventManager(context, "api.subAPI", fire => {
+// let listener = (...) => {
+// // Fire any listeners registered with addListener.
+// fire(arg1, arg2);
+// };
+// // Register the listener.
+// SomehowRegisterListener(listener);
+// return () => {
+// // Return a way to unregister the listener.
+// SomehowUnregisterListener(listener);
+// };
+// }).api()
+//
+// The result is an object with addListener, removeListener, and
+// hasListener methods. |context| is an add-on scope (either an
+// ExtensionContext in the chrome process or ExtensionContext in a
+// content process). |name| is for debugging. |register| is a function
+// to register the listener. |register| is only called once, even if
+// multiple listeners are registered. |register| should return an
+// unregister function that will unregister the listener.
+function EventManager(context, name, register) {
+ this.context = context;
+ this.name = name;
+ this.register = register;
+ this.unregister = null;
+ this.callbacks = new Set();
+}
+
+EventManager.prototype = {
+ addListener(callback) {
+ if (typeof(callback) != "function") {
+ dump(`Expected function\n${Error().stack}`);
+ return;
+ }
+ if (this.context.unloaded) {
+ dump(`Cannot add listener to ${this.name} after context unloaded`);
+ return;
+ }
+
+ if (!this.callbacks.size) {
+ this.context.callOnClose(this);
+
+ let fireFunc = this.fire.bind(this);
+ let fireWithoutClone = this.fireWithoutClone.bind(this);
+ fireFunc.withoutClone = fireWithoutClone;
+ this.unregister = this.register(fireFunc);
+ }
+ this.callbacks.add(callback);
+ },
+
+ removeListener(callback) {
+ if (!this.callbacks.size) {
+ return;
+ }
+
+ this.callbacks.delete(callback);
+ if (this.callbacks.size == 0) {
+ this.unregister();
+ this.unregister = null;
+
+ this.context.forgetOnClose(this);
+ }
+ },
+
+ hasListener(callback) {
+ return this.callbacks.has(callback);
+ },
+
+ fire(...args) {
+ this._fireCommon("runSafe", args);
+ },
+
+ fireWithoutClone(...args) {
+ this._fireCommon("runSafeWithoutClone", args);
+ },
+
+ _fireCommon(runSafeMethod, args) {
+ for (let callback of this.callbacks) {
+ Promise.resolve(callback).then(callback => {
+ if (this.context.unloaded) {
+ dump(`${this.name} event fired after context unloaded.\n`);
+ } else if (!this.context.active) {
+ dump(`${this.name} event fired while context is inactive.\n`);
+ } else if (this.callbacks.has(callback)) {
+ this.context[runSafeMethod](callback, ...args);
+ }
+ });
+ }
+ },
+
+ close() {
+ if (this.callbacks.size) {
+ this.unregister();
+ }
+ this.callbacks.clear();
+ this.register = null;
+ this.unregister = null;
+ },
+
+ api() {
+ return {
+ addListener: callback => this.addListener(callback),
+ removeListener: callback => this.removeListener(callback),
+ hasListener: callback => this.hasListener(callback),
+ };
+ },
+};
+
+// Similar to EventManager, but it doesn't try to consolidate event
+// notifications. Each addListener call causes us to register once. It
+// allows extra arguments to be passed to addListener.
+function SingletonEventManager(context, name, register) {
+ this.context = context;
+ this.name = name;
+ this.register = register;
+ this.unregister = new Map();
+}
+
+SingletonEventManager.prototype = {
+ addListener(callback, ...args) {
+ let wrappedCallback = (...args) => {
+ if (this.context.unloaded) {
+ dump(`${this.name} event fired after context unloaded.\n`);
+ } else if (this.unregister.has(callback)) {
+ return callback(...args);
+ }
+ };
+
+ let unregister = this.register(wrappedCallback, ...args);
+ this.unregister.set(callback, unregister);
+ this.context.callOnClose(this);
+ },
+
+ removeListener(callback) {
+ if (!this.unregister.has(callback)) {
+ return;
+ }
+
+ let unregister = this.unregister.get(callback);
+ this.unregister.delete(callback);
+ unregister();
+ },
+
+ hasListener(callback) {
+ return this.unregister.has(callback);
+ },
+
+ close() {
+ for (let unregister of this.unregister.values()) {
+ unregister();
+ }
+ },
+
+ api() {
+ return {
+ addListener: (...args) => this.addListener(...args),
+ removeListener: (...args) => this.removeListener(...args),
+ hasListener: (...args) => this.hasListener(...args),
+ };
+ },
+};
+
+// Simple API for event listeners where events never fire.
+function ignoreEvent(context, name) {
+ return {
+ addListener: function(callback) {
+ let id = context.extension.id;
+ let frame = Components.stack.caller;
+ let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
+ let scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(Ci.nsIScriptError);
+ scriptError.init(msg, frame.filename, null, frame.lineNumber,
+ frame.columnNumber, Ci.nsIScriptError.warningFlag,
+ "content javascript");
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+ consoleService.logMessage(scriptError);
+ },
+ removeListener: function(callback) {},
+ hasListener: function(callback) {},
+ };
+}
+
+// Copy an API object from |source| into the scope |dest|.
+function injectAPI(source, dest) {
+ for (let prop in source) {
+ // Skip names prefixed with '_'.
+ if (prop[0] == "_") {
+ continue;
+ }
+
+ let desc = Object.getOwnPropertyDescriptor(source, prop);
+ if (typeof(desc.value) == "function") {
+ Cu.exportFunction(desc.value, dest, {defineAs: prop});
+ } else if (typeof(desc.value) == "object") {
+ let obj = Cu.createObjectIn(dest, {defineAs: prop});
+ injectAPI(desc.value, obj);
+ } else {
+ Object.defineProperty(dest, prop, desc);
+ }
+ }
+}
+
+/**
+ * Returns a Promise which resolves when the given document's DOM has
+ * fully loaded.
+ *
+ * @param {Document} doc The document to await the load of.
+ * @returns {Promise<Document>}
+ */
+function promiseDocumentReady(doc) {
+ if (doc.readyState == "interactive" || doc.readyState == "complete") {
+ return Promise.resolve(doc);
+ }
+
+ return new Promise(resolve => {
+ doc.addEventListener("DOMContentLoaded", function onReady(event) {
+ if (event.target === event.currentTarget) {
+ doc.removeEventListener("DOMContentLoaded", onReady, true);
+ resolve(doc);
+ }
+ }, true);
+ });
+}
+
+/**
+ * Returns a Promise which resolves when the given document is fully
+ * loaded.
+ *
+ * @param {Document} doc The document to await the load of.
+ * @returns {Promise<Document>}
+ */
+function promiseDocumentLoaded(doc) {
+ if (doc.readyState == "complete") {
+ return Promise.resolve(doc);
+ }
+
+ return new Promise(resolve => {
+ doc.defaultView.addEventListener("load", function onReady(event) {
+ doc.defaultView.removeEventListener("load", onReady);
+ resolve(doc);
+ });
+ });
+}
+
+/**
+ * Returns a Promise which resolves when the given event is dispatched to the
+ * given element.
+ *
+ * @param {Element} element
+ * The element on which to listen.
+ * @param {string} eventName
+ * The event to listen for.
+ * @param {boolean} [useCapture = true]
+ * If true, listen for the even in the capturing rather than
+ * bubbling phase.
+ * @param {Event} [test]
+ * An optional test function which, when called with the
+ * observer's subject and data, should return true if this is the
+ * expected event, false otherwise.
+ * @returns {Promise<Event>}
+ */
+function promiseEvent(element, eventName, useCapture = true, test = event => true) {
+ return new Promise(resolve => {
+ function listener(event) {
+ if (test(event)) {
+ element.removeEventListener(eventName, listener, useCapture);
+ resolve(event);
+ }
+ }
+ element.addEventListener(eventName, listener, useCapture);
+ });
+}
+
+/**
+ * Returns a Promise which resolves the given observer topic has been
+ * observed.
+ *
+ * @param {string} topic
+ * The topic to observe.
+ * @param {function(nsISupports, string)} [test]
+ * An optional test function which, when called with the
+ * observer's subject and data, should return true if this is the
+ * expected notification, false otherwise.
+ * @returns {Promise<object>}
+ */
+function promiseObserved(topic, test = () => true) {
+ return new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ if (test(subject, data)) {
+ Services.obs.removeObserver(observer, topic);
+ resolve({subject, data});
+ }
+ };
+ Services.obs.addObserver(observer, topic, false);
+ });
+}
+
+function getMessageManager(target) {
+ if (target instanceof Ci.nsIFrameLoaderOwner) {
+ return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+ }
+ return target.QueryInterface(Ci.nsIMessageSender);
+}
+
+function flushJarCache(jarFile) {
+ Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
+}
+
+const PlatformInfo = Object.freeze({
+ os: (function() {
+ let os = AppConstants.platform;
+ if (os == "macosx") {
+ os = "mac";
+ }
+ return os;
+ })(),
+ arch: (function() {
+ let abi = Services.appinfo.XPCOMABI;
+ let [arch] = abi.split("-");
+ if (arch == "x86") {
+ arch = "x86-32";
+ } else if (arch == "x86_64") {
+ arch = "x86-64";
+ }
+ return arch;
+ })(),
+});
+
+function detectLanguage(text) {
+ return LanguageDetector.detectLanguage(text).then(result => ({
+ isReliable: result.confident,
+ languages: result.languages.map(lang => {
+ return {
+ language: lang.languageCode,
+ percentage: lang.percent,
+ };
+ }),
+ }));
+}
+
+/**
+ * Convert any of several different representations of a date/time to a Date object.
+ * Accepts several formats:
+ * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
+ * either a number or a string.
+ *
+ * @param {Date|string|number} date
+ * The date to convert.
+ * @returns {Date}
+ * A Date object
+ */
+function normalizeTime(date) {
+ // Of all the formats we accept the "number of milliseconds since the epoch as a string"
+ // is an outlier, everything else can just be passed directly to the Date constructor.
+ return new Date((typeof date == "string" && /^\d+$/.test(date))
+ ? parseInt(date, 10) : date);
+}
+
+const stylesheetMap = new DefaultMap(url => {
+ let uri = NetUtil.newURI(url);
+ return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
+});
+
+/**
+ * Defines a lazy getter for the given property on the given object. The
+ * first time the property is accessed, the return value of the getter
+ * is defined on the current `this` object with the given property name.
+ * Importantly, this means that a lazy getter defined on an object
+ * prototype will be invoked separately for each object instance that
+ * it's accessed on.
+ *
+ * @param {object} object
+ * The prototype object on which to define the getter.
+ * @param {string|Symbol} prop
+ * The property name for which to define the getter.
+ * @param {function} getter
+ * The function to call in order to generate the final property
+ * value.
+ */
+function defineLazyGetter(object, prop, getter) {
+ let redefine = (obj, value) => {
+ Object.defineProperty(obj, prop, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value,
+ });
+ return value;
+ };
+
+ Object.defineProperty(object, prop, {
+ enumerable: true,
+ configurable: true,
+
+ get() {
+ return redefine(this, getter.call(this));
+ },
+
+ set(value) {
+ redefine(this, value);
+ },
+ });
+}
+
+function findPathInObject(obj, path, printErrors = true) {
+ let parent;
+ for (let elt of path.split(".")) {
+ if (!obj || !(elt in obj)) {
+ if (printErrors) {
+ Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
+ }
+ return null;
+ }
+
+ parent = obj;
+ obj = obj[elt];
+ }
+
+ if (typeof obj === "function") {
+ return obj.bind(parent);
+ }
+ return obj;
+}
+
+/**
+ * Acts as a proxy for a message manager or message manager owner, and
+ * tracks docShell swaps so that messages are always sent to the same
+ * receiver, even if it is moved to a different <browser>.
+ *
+ * @param {nsIMessageSender|Element} target
+ * The target message manager on which to send messages, or the
+ * <browser> element which owns it.
+ */
+class MessageManagerProxy {
+ constructor(target) {
+ this.listeners = new DefaultMap(() => new Map());
+
+ if (target instanceof Ci.nsIMessageSender) {
+ Object.defineProperty(this, "messageManager", {
+ value: target,
+ configurable: true,
+ writable: true,
+ });
+ } else {
+ this.addListeners(target);
+ }
+ }
+
+ /**
+ * Disposes of the proxy object, removes event listeners, and drops
+ * all references to the underlying message manager.
+ *
+ * Must be called before the last reference to the proxy is dropped,
+ * unless the underlying message manager or <browser> is also being
+ * destroyed.
+ */
+ dispose() {
+ if (this.eventTarget) {
+ this.removeListeners(this.eventTarget);
+ this.eventTarget = null;
+ } else {
+ this.messageManager = null;
+ }
+ }
+
+ /**
+ * Returns true if the given target is the same as, or owns, the given
+ * message manager.
+ *
+ * @param {nsIMessageSender|MessageManagerProxy|Element} target
+ * The message manager, MessageManagerProxy, or <browser>
+ * element agaisnt which to match.
+ * @param {nsIMessageSender} messageManager
+ * The message manager against which to match `target`.
+ *
+ * @returns {boolean}
+ * True if `messageManager` is the same object as `target`, or
+ * `target` is a MessageManagerProxy or <browser> element that
+ * is tied to it.
+ */
+ static matches(target, messageManager) {
+ return target === messageManager || target.messageManager === messageManager;
+ }
+
+ /**
+ * @property {nsIMessageSender|null} messageManager
+ * The message manager that is currently being proxied. This
+ * may change during the life of the proxy object, so should
+ * not be stored elsewhere.
+ */
+ get messageManager() {
+ return this.eventTarget && this.eventTarget.messageManager;
+ }
+
+ /**
+ * Sends a message on the proxied message manager.
+ *
+ * @param {array} args
+ * Arguments to be passed verbatim to the underlying
+ * sendAsyncMessage method.
+ * @returns {undefined}
+ */
+ sendAsyncMessage(...args) {
+ if (this.messageManager) {
+ return this.messageManager.sendAsyncMessage(...args);
+ }
+ /* globals uneval */
+ Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`);
+ }
+
+ /**
+ * Adds a message listener to the current message manager, and
+ * transfers it to the new message manager after a docShell swap.
+ *
+ * @param {string} message
+ * The name of the message to listen for.
+ * @param {nsIMessageListener} listener
+ * The listener to add.
+ * @param {boolean} [listenWhenClosed = false]
+ * If true, the listener will receive messages which were sent
+ * after the remote side of the listener began closing.
+ */
+ addMessageListener(message, listener, listenWhenClosed = false) {
+ this.messageManager.addMessageListener(message, listener, listenWhenClosed);
+ this.listeners.get(message).set(listener, listenWhenClosed);
+ }
+
+ /**
+ * Adds a message listener from the current message manager.
+ *
+ * @param {string} message
+ * The name of the message to stop listening for.
+ * @param {nsIMessageListener} listener
+ * The listener to remove.
+ */
+ removeMessageListener(message, listener) {
+ this.messageManager.removeMessageListener(message, listener);
+
+ let listeners = this.listeners.get(message);
+ listeners.delete(listener);
+ if (!listeners.size) {
+ this.listeners.delete(message);
+ }
+ }
+
+ /**
+ * @private
+ * Iterates over all of the currently registered message listeners.
+ */
+ * iterListeners() {
+ for (let [message, listeners] of this.listeners) {
+ for (let [listener, listenWhenClosed] of listeners) {
+ yield {message, listener, listenWhenClosed};
+ }
+ }
+ }
+
+ /**
+ * @private
+ * Adds docShell swap listeners to the message manager owner.
+ *
+ * @param {Element} target
+ * The target element.
+ */
+ addListeners(target) {
+ target.addEventListener("SwapDocShells", this);
+
+ for (let {message, listener, listenWhenClosed} of this.iterListeners()) {
+ target.addMessageListener(message, listener, listenWhenClosed);
+ }
+
+ this.eventTarget = target;
+ }
+
+ /**
+ * @private
+ * Removes docShell swap listeners to the message manager owner.
+ *
+ * @param {Element} target
+ * The target element.
+ */
+ removeListeners(target) {
+ target.removeEventListener("SwapDocShells", this);
+
+ for (let {message, listener} of this.iterListeners()) {
+ target.removeMessageListener(message, listener);
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type == "SwapDocShells") {
+ this.removeListeners(this.eventTarget);
+ this.addListeners(event.detail);
+ }
+ }
+}
+
+this.ExtensionUtils = {
+ defineLazyGetter,
+ detectLanguage,
+ extend,
+ findPathInObject,
+ flushJarCache,
+ getConsole,
+ getInnerWindowID,
+ getMessageManager,
+ getUniqueId,
+ ignoreEvent,
+ injectAPI,
+ instanceOf,
+ normalizeTime,
+ promiseDocumentLoaded,
+ promiseDocumentReady,
+ promiseEvent,
+ promiseObserved,
+ runSafe,
+ runSafeSync,
+ runSafeSyncWithoutClone,
+ runSafeWithoutClone,
+ stylesheetMap,
+ DefaultMap,
+ DefaultWeakMap,
+ EventEmitter,
+ EventManager,
+ ExtensionError,
+ IconDetails,
+ LocaleData,
+ MessageManagerProxy,
+ PlatformInfo,
+ SingletonEventManager,
+ SpreadArgs,
+};
diff --git a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
new file mode 100644
index 0000000000..339709a19d
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -0,0 +1,306 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["ExtensionTestUtils"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+ const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ return Management;
+});
+
+/* exported ExtensionTestUtils */
+
+let BASE_MANIFEST = Object.freeze({
+ "applications": Object.freeze({
+ "gecko": Object.freeze({
+ "id": "test@web.ext",
+ }),
+ }),
+
+ "manifest_version": 2,
+
+ "name": "name",
+ "version": "0",
+});
+
+class ExtensionWrapper {
+ constructor(extension, testScope) {
+ this.extension = extension;
+ this.testScope = testScope;
+
+ this.state = "uninitialized";
+
+ this.testResolve = null;
+ this.testDone = new Promise(resolve => { this.testResolve = resolve; });
+
+ this.messageHandler = new Map();
+ this.messageAwaiter = new Map();
+
+ this.messageQueue = new Set();
+
+ this.attachListeners();
+
+ this.testScope.do_register_cleanup(() => {
+ if (this.messageQueue.size) {
+ let names = Array.from(this.messageQueue, ([msg]) => msg);
+ this.testScope.equal(JSON.stringify(names), "[]", "message queue is empty");
+ }
+ if (this.messageAwaiter.size) {
+ let names = Array.from(this.messageAwaiter.keys());
+ this.testScope.equal(JSON.stringify(names), "[]", "no tasks awaiting on messages");
+ }
+ });
+
+ this.testScope.do_register_cleanup(() => {
+ if (this.state == "pending" || this.state == "running") {
+ this.testScope.equal(this.state, "unloaded", "Extension left running at test shutdown");
+ return this.unload();
+ } else if (extension.state == "unloading") {
+ this.testScope.equal(this.state, "unloaded", "Extension not fully unloaded at test shutdown");
+ }
+ });
+
+ this.testScope.do_print(`Extension loaded`);
+ }
+
+ attachListeners() {
+ /* eslint-disable mozilla/balanced-listeners */
+ this.extension.on("test-eq", (kind, pass, msg, expected, actual) => {
+ this.testScope.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`);
+ });
+ this.extension.on("test-log", (kind, pass, msg) => {
+ this.testScope.do_print(msg);
+ });
+ this.extension.on("test-result", (kind, pass, msg) => {
+ this.testScope.ok(pass, msg);
+ });
+ this.extension.on("test-done", (kind, pass, msg, expected, actual) => {
+ this.testScope.ok(pass, msg);
+ this.testResolve(msg);
+ });
+
+ this.extension.on("test-message", (kind, msg, ...args) => {
+ let handler = this.messageHandler.get(msg);
+ if (handler) {
+ handler(...args);
+ } else {
+ this.messageQueue.add([msg, ...args]);
+ this.checkMessages();
+ }
+ });
+ /* eslint-enable mozilla/balanced-listeners */
+ }
+
+ startup() {
+ if (this.state != "uninitialized") {
+ throw new Error("Extension already started");
+ }
+ this.state = "pending";
+
+ return this.extension.startup().then(
+ result => {
+ this.state = "running";
+
+ return result;
+ },
+ error => {
+ this.state = "failed";
+
+ return Promise.reject(error);
+ });
+ }
+
+ unload() {
+ if (this.state != "running") {
+ throw new Error("Extension not running");
+ }
+ this.state = "unloading";
+
+ this.extension.shutdown();
+
+ this.state = "unloaded";
+
+ return Promise.resolve();
+ }
+
+ /*
+ * This method marks the extension unloading without actually calling
+ * shutdown, since shutting down a MockExtension causes it to be uninstalled.
+ *
+ * Normally you shouldn't need to use this unless you need to test something
+ * that requires a restart, such as updates.
+ */
+ markUnloaded() {
+ if (this.state != "running") {
+ throw new Error("Extension not running");
+ }
+ this.state = "unloaded";
+
+ return Promise.resolve();
+ }
+
+ sendMessage(...args) {
+ this.extension.testMessage(...args);
+ }
+
+ awaitFinish(msg) {
+ return this.testDone.then(actual => {
+ if (msg) {
+ this.testScope.equal(actual, msg, "test result correct");
+ }
+ return actual;
+ });
+ }
+
+ checkMessages() {
+ for (let message of this.messageQueue) {
+ let [msg, ...args] = message;
+
+ let listener = this.messageAwaiter.get(msg);
+ if (listener) {
+ this.messageQueue.delete(message);
+ this.messageAwaiter.delete(msg);
+
+ listener.resolve(...args);
+ return;
+ }
+ }
+ }
+
+ checkDuplicateListeners(msg) {
+ if (this.messageHandler.has(msg) || this.messageAwaiter.has(msg)) {
+ throw new Error("only one message handler allowed");
+ }
+ }
+
+ awaitMessage(msg) {
+ return new Promise(resolve => {
+ this.checkDuplicateListeners(msg);
+
+ this.messageAwaiter.set(msg, {resolve});
+ this.checkMessages();
+ });
+ }
+
+ onMessage(msg, callback) {
+ this.checkDuplicateListeners(msg);
+ this.messageHandler.set(msg, callback);
+ }
+}
+
+var ExtensionTestUtils = {
+ BASE_MANIFEST,
+
+ normalizeManifest: Task.async(function* (manifest, baseManifest = BASE_MANIFEST) {
+ yield Management.lazyInit();
+
+ let errors = [];
+ let context = {
+ url: null,
+
+ logError: error => {
+ errors.push(error);
+ },
+
+ preprocessors: {},
+ };
+
+ manifest = Object.assign({}, baseManifest, manifest);
+
+ let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
+ normalized.errors = errors;
+
+ return normalized;
+ }),
+
+ currentScope: null,
+
+ profileDir: null,
+
+ init(scope) {
+ this.currentScope = scope;
+
+ this.profileDir = scope.do_get_profile();
+
+ // We need to load at least one frame script into every message
+ // manager to ensure that the scriptable wrapper for its global gets
+ // created before we try to access it externally. If we don't, we
+ // fail sanity checks on debug builds the first time we try to
+ // create a wrapper, because we should never have a global without a
+ // cached wrapper.
+ Services.mm.loadFrameScript("data:text/javascript,//", true);
+
+
+ let tmpD = this.profileDir.clone();
+ tmpD.append("tmp");
+ tmpD.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let dirProvider = {
+ getFile(prop, persistent) {
+ persistent.value = false;
+ if (prop == "TmpD") {
+ return tmpD.clone();
+ }
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+ };
+ Services.dirsvc.registerProvider(dirProvider);
+
+
+ scope.do_register_cleanup(() => {
+ tmpD.remove(true);
+ Services.dirsvc.unregisterProvider(dirProvider);
+
+ this.currentScope = null;
+ });
+ },
+
+ addonManagerStarted: false,
+
+ mockAppInfo() {
+ const {updateAppInfo} = Cu.import("resource://testing-common/AppInfo.jsm", {});
+ updateAppInfo({
+ ID: "xpcshell@tests.mozilla.org",
+ name: "XPCShell",
+ version: "48",
+ platformVersion: "48",
+ });
+ },
+
+ startAddonManager() {
+ if (this.addonManagerStarted) {
+ return;
+ }
+ this.addonManagerStarted = true;
+ this.mockAppInfo();
+
+ let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver)
+ .QueryInterface(Ci.nsITimerCallback);
+ manager.observe(null, "addons-startup", null);
+ },
+
+ loadExtension(data) {
+ let extension = Extension.generate(data);
+
+ return new ExtensionWrapper(extension, this.currentScope);
+ },
+};
diff --git a/toolkit/components/extensions/LegacyExtensionsUtils.jsm b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
new file mode 100644
index 0000000000..7632548e30
--- /dev/null
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["LegacyExtensionsUtils"];
+
+/* exported LegacyExtensionsUtils, LegacyExtensionContext */
+
+/**
+ * This file exports helpers for Legacy Extensions that want to embed a webextensions
+ * and exchange messages with the embedded WebExtension.
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+Cu.import("resource://gre/modules/ExtensionChild.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+
+var {
+ BaseContext,
+} = ExtensionCommon;
+
+var {
+ Messenger,
+} = ExtensionChild;
+
+/**
+ * Instances created from this class provide to a legacy extension
+ * a simple API to exchange messages with a webextension.
+ */
+var LegacyExtensionContext = class extends BaseContext {
+ /**
+ * Create a new LegacyExtensionContext given a target Extension instance.
+ *
+ * @param {Extension} targetExtension
+ * The webextension instance associated with this context. This will be the
+ * instance of the newly created embedded webextension when this class is
+ * used through the EmbeddedWebExtensionsUtils.
+ */
+ constructor(targetExtension) {
+ super("legacy_extension", targetExtension);
+
+ // Legacy Extensions (xul overlays, bootstrap restartless and Addon SDK)
+ // runs with a systemPrincipal.
+ let addonPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ Object.defineProperty(
+ this, "principal",
+ {value: addonPrincipal, enumerable: true, configurable: true}
+ );
+
+ let cloneScope = Cu.Sandbox(this.principal, {});
+ Cu.setSandboxMetadata(cloneScope, {addonId: targetExtension.id});
+ Object.defineProperty(
+ this, "cloneScope",
+ {value: cloneScope, enumerable: true, configurable: true, writable: true}
+ );
+
+ let sender = {id: targetExtension.uuid};
+ let filter = {extensionId: targetExtension.id};
+ // Legacy addons live in the main process. Messages from other addons are
+ // Messages from WebExtensions are sent to the main process and forwarded via
+ // the parent process manager to the legacy extension.
+ this.messenger = new Messenger(this, [Services.cpmm], sender, filter);
+
+ this.api = {
+ browser: {
+ runtime: {
+ onConnect: this.messenger.onConnect("runtime.onConnect"),
+ onMessage: this.messenger.onMessage("runtime.onMessage"),
+ },
+ },
+ };
+ }
+
+ /**
+ * This method is called when the extension shuts down or is unloaded,
+ * and it nukes the cloneScope sandbox, if any.
+ */
+ unload() {
+ if (this.unloaded) {
+ throw new Error("Error trying to unload LegacyExtensionContext twice.");
+ }
+ super.unload();
+ Cu.nukeSandbox(this.cloneScope);
+ this.cloneScope = null;
+ }
+};
+
+var EmbeddedExtensionManager;
+
+/**
+ * Instances of this class are used internally by the exported EmbeddedWebExtensionsUtils
+ * to manage the embedded webextension instance and the related LegacyExtensionContext
+ * instance used to exchange messages with it.
+ */
+class EmbeddedExtension {
+ /**
+ * Create a new EmbeddedExtension given the add-on id and the base resource URI of the
+ * container add-on (the webextension resources will be loaded from the "webextension/"
+ * subdir of the base resource URI for the legacy extension add-on).
+ *
+ * @param {Object} containerAddonParams
+ * An object with the following properties:
+ * @param {string} containerAddonParams.id
+ * The Add-on id of the Legacy Extension which will contain the embedded webextension.
+ * @param {nsIURI} containerAddonParams.resourceURI
+ * The nsIURI of the Legacy Extension container add-on.
+ */
+ constructor({id, resourceURI}) {
+ this.addonId = id;
+ this.resourceURI = resourceURI;
+
+ // Setup status flag.
+ this.started = false;
+ }
+
+ /**
+ * Start the embedded webextension.
+ *
+ * @returns {Promise<LegacyContextAPI>} A promise which resolve to the API exposed to the
+ * legacy context.
+ */
+ startup() {
+ if (this.started) {
+ return Promise.reject(new Error("This embedded extension has already been started"));
+ }
+
+ // Setup the startup promise.
+ this.startupPromise = new Promise((resolve, reject) => {
+ let embeddedExtensionURI = Services.io.newURI("webextension/", null, this.resourceURI);
+
+ // This is the instance of the WebExtension embedded in the hybrid add-on.
+ this.extension = new Extension({
+ id: this.addonId,
+ resourceURI: embeddedExtensionURI,
+ });
+
+ // This callback is register to the "startup" event, emitted by the Extension instance
+ // after the extension manifest.json has been loaded without any errors, but before
+ // starting any of the defined contexts (which give the legacy part a chance to subscribe
+ // runtime.onMessage/onConnect listener before the background page has been loaded).
+ const onBeforeStarted = () => {
+ this.extension.off("startup", onBeforeStarted);
+
+ // Resolve the startup promise and reset the startupError.
+ this.started = true;
+ this.startupPromise = null;
+
+ // Create the legacy extension context, the legacy container addon
+ // needs to use it before the embedded webextension startup,
+ // because it is supposed to be used during the legacy container startup
+ // to subscribe its message listeners (which are supposed to be able to
+ // receive any message that the embedded part can try to send to it
+ // during its startup).
+ this.context = new LegacyExtensionContext(this.extension);
+
+ // Destroy the LegacyExtensionContext cloneScope when
+ // the embedded webextensions is unloaded.
+ this.extension.callOnClose({
+ close: () => {
+ this.context.unload();
+ },
+ });
+
+ // resolve startupPromise to execute any pending shutdown that has been
+ // chained to it.
+ resolve(this.context.api);
+ };
+
+ this.extension.on("startup", onBeforeStarted);
+
+ // Run ambedded extension startup and catch any error during embedded extension
+ // startup.
+ this.extension.startup().catch((err) => {
+ this.started = false;
+ this.startupPromise = null;
+ this.extension.off("startup", onBeforeStarted);
+
+ reject(err);
+ });
+ });
+
+ return this.startupPromise;
+ }
+
+ /**
+ * Shuts down the embedded webextension.
+ *
+ * @returns {Promise<void>} a promise that is resolved when the shutdown has been done
+ */
+ shutdown() {
+ EmbeddedExtensionManager.untrackEmbeddedExtension(this);
+
+ // If there is a pending startup, wait to be completed and then shutdown.
+ if (this.startupPromise) {
+ return this.startupPromise.then(() => {
+ this.extension.shutdown();
+ });
+ }
+
+ // Run shutdown now if the embedded webextension has been correctly started
+ if (this.extension && this.started && !this.extension.hasShutdown) {
+ this.extension.shutdown();
+ }
+
+ return Promise.resolve();
+ }
+}
+
+// Keep track on the created EmbeddedExtension instances and destroy
+// them when their container addon is going to be disabled or uninstalled.
+EmbeddedExtensionManager = {
+ // Map of the existent EmbeddedExtensions instances by addon id.
+ embeddedExtensionsByAddonId: new Map(),
+
+ untrackEmbeddedExtension(embeddedExtensionInstance) {
+ // Remove this instance from the tracked embedded extensions
+ let id = embeddedExtensionInstance.addonId;
+ if (this.embeddedExtensionsByAddonId.get(id) == embeddedExtensionInstance) {
+ this.embeddedExtensionsByAddonId.delete(id);
+ }
+ },
+
+ getEmbeddedExtensionFor({id, resourceURI}) {
+ let embeddedExtension = this.embeddedExtensionsByAddonId.get(id);
+
+ if (!embeddedExtension) {
+ embeddedExtension = new EmbeddedExtension({id, resourceURI});
+ // Keep track of the embedded extension instance.
+ this.embeddedExtensionsByAddonId.set(id, embeddedExtension);
+ }
+
+ return embeddedExtension;
+ },
+};
+
+this.LegacyExtensionsUtils = {
+ getEmbeddedExtensionFor: (addon) => {
+ return EmbeddedExtensionManager.getEmbeddedExtensionFor(addon);
+ },
+};
diff --git a/toolkit/components/extensions/MessageChannel.jsm b/toolkit/components/extensions/MessageChannel.jsm
new file mode 100644
index 0000000000..c5b326405d
--- /dev/null
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -0,0 +1,797 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * This module provides wrappers around standard message managers to
+ * simplify bidirectional communication. It currently allows a caller to
+ * send a message to a single listener, and receive a reply. If there
+ * are no matching listeners, or the message manager disconnects before
+ * a reply is received, the caller is returned an error.
+ *
+ * The listener end may specify filters for the messages it wishes to
+ * receive, and the sender end likewise may specify recipient tags to
+ * match the filters.
+ *
+ * The message handler on the listener side may return its response
+ * value directly, or may return a promise, the resolution or rejection
+ * of which will be returned instead. The sender end likewise receives a
+ * promise which resolves or rejects to the listener's response.
+ *
+ *
+ * A basic setup works something like this:
+ *
+ * A content script adds a message listener to its global
+ * nsIContentFrameMessageManager, with an appropriate set of filters:
+ *
+ * {
+ * init(messageManager, window, extensionID) {
+ * this.window = window;
+ *
+ * MessageChannel.addListener(
+ * messageManager, "ContentScript:TouchContent",
+ * this);
+ *
+ * this.messageFilterStrict = {
+ * innerWindowID: getInnerWindowID(window),
+ * extensionID: extensionID,
+ * };
+ *
+ * this.messageFilterPermissive = {
+ * outerWindowID: getOuterWindowID(window),
+ * };
+ * },
+ *
+ * receiveMessage({ target, messageName, sender, recipient, data }) {
+ * if (messageName == "ContentScript:TouchContent") {
+ * return new Promise(resolve => {
+ * this.touchWindow(data.touchWith, result => {
+ * resolve({ touchResult: result });
+ * });
+ * });
+ * }
+ * },
+ * };
+ *
+ * A script in the parent process sends a message to the content process
+ * via a tab message manager, including recipient tags to match its
+ * filter, and an optional sender tag to identify itself:
+ *
+ * let data = { touchWith: "pencil" };
+ * let sender = { extensionID, contextID };
+ * let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
+ *
+ * MessageChannel.sendMessage(
+ * tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
+ * data, {recipient, sender}
+ * ).then(result => {
+ * alert(result.touchResult);
+ * });
+ *
+ * Since the lifetimes of message senders and receivers may not always
+ * match, either side of the message channel may cancel pending
+ * responses which match its sender or recipient tags.
+ *
+ * For the above client, this might be done from an
+ * inner-window-destroyed observer, when its target scope is destroyed:
+ *
+ * observe(subject, topic, data) {
+ * if (topic == "inner-window-destroyed") {
+ * let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ *
+ * MessageChannel.abortResponses({ innerWindowID });
+ * }
+ * },
+ *
+ * From the parent, it may be done when its context is being destroyed:
+ *
+ * onDestroy() {
+ * MessageChannel.abortResponses({
+ * extensionID: this.extensionID,
+ * contextID: this.contextID,
+ * });
+ * },
+ *
+ */
+
+this.EXPORTED_SYMBOLS = ["MessageChannel"];
+
+/* globals MessageChannel */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+ "resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "MessageManagerProxy",
+ () => ExtensionUtils.MessageManagerProxy);
+
+/**
+ * Handles the mapping and dispatching of messages to their registered
+ * handlers. There is one broker per message manager and class of
+ * messages. Each class of messages is mapped to one native message
+ * name, e.g., "MessageChannel:Message", and is dispatched to handlers
+ * based on an internal message name, e.g., "Extension:ExecuteScript".
+ */
+class FilteringMessageManager {
+ /**
+ * @param {string} messageName
+ * The name of the native message this broker listens for.
+ * @param {function} callback
+ * A function which is called for each message after it has been
+ * mapped to its handler. The function receives two arguments:
+ *
+ * result:
+ * An object containing either a `handler` or an `error` property.
+ * If no error occurs, `handler` will be a matching handler that
+ * was registered by `addHandler`. Otherwise, the `error` property
+ * will contain an object describing the error.
+ *
+ * data:
+ * An object describing the message, as defined in
+ * `MessageChannel.addListener`.
+ * @param {nsIMessageListenerManager} messageManager
+ */
+ constructor(messageName, callback, messageManager) {
+ this.messageName = messageName;
+ this.callback = callback;
+ this.messageManager = messageManager;
+
+ this.messageManager.addMessageListener(this.messageName, this, true);
+
+ this.handlers = new Map();
+ }
+
+ /**
+ * Receives a message from our message manager, maps it to a handler, and
+ * passes the result to our message callback.
+ */
+ receiveMessage({data, target}) {
+ let handlers = Array.from(this.getHandlers(data.messageName, data.sender, data.recipient));
+
+ data.target = target;
+ this.callback(handlers, data);
+ }
+
+ /**
+ * Iterates over all handlers for the given message name. If `recipient`
+ * is provided, only iterates over handlers whose filters match it.
+ *
+ * @param {string|number} messageName
+ * The message for which to return handlers.
+ * @param {object} sender
+ * The sender data on which to filter handlers.
+ * @param {object} recipient
+ * The recipient data on which to filter handlers.
+ */
+ * getHandlers(messageName, sender, recipient) {
+ let handlers = this.handlers.get(messageName) || new Set();
+ for (let handler of handlers) {
+ if (MessageChannel.matchesFilter(handler.messageFilterStrict || {}, recipient) &&
+ MessageChannel.matchesFilter(handler.messageFilterPermissive || {}, recipient, false) &&
+ (!handler.filterMessage || handler.filterMessage(sender, recipient))) {
+ yield handler;
+ }
+ }
+ }
+
+ /**
+ * Registers a handler for the given message.
+ *
+ * @param {string} messageName
+ * The internal message name for which to register the handler.
+ * @param {object} handler
+ * An opaque handler object. The object may have a
+ * `messageFilterStrict` and/or a `messageFilterPermissive`
+ * property and/or a `filterMessage` method on which to filter messages.
+ *
+ * Final dispatching is handled by the message callback passed to
+ * the constructor.
+ */
+ addHandler(messageName, handler) {
+ if (!this.handlers.has(messageName)) {
+ this.handlers.set(messageName, new Set());
+ }
+
+ this.handlers.get(messageName).add(handler);
+ }
+
+ /**
+ * Unregisters a handler for the given message.
+ *
+ * @param {string} messageName
+ * The internal message name for which to unregister the handler.
+ * @param {object} handler
+ * The handler object to unregister.
+ */
+ removeHandler(messageName, handler) {
+ this.handlers.get(messageName).delete(handler);
+ }
+}
+
+/**
+ * Manages mappings of message managers to their corresponding message
+ * brokers. Brokers are lazily created for each message manager the
+ * first time they are accessed. In the case of content frame message
+ * managers, they are also automatically destroyed when the frame
+ * unload event fires.
+ */
+class FilteringMessageManagerMap extends Map {
+ // Unfortunately, we can't use a WeakMap for this, because message
+ // managers do not support preserved wrappers.
+
+ /**
+ * @param {string} messageName
+ * The native message name passed to `FilteringMessageManager` constructors.
+ * @param {function} callback
+ * The message callback function passed to
+ * `FilteringMessageManager` constructors.
+ */
+ constructor(messageName, callback) {
+ super();
+
+ this.messageName = messageName;
+ this.callback = callback;
+ }
+
+ /**
+ * Returns, and possibly creates, a message broker for the given
+ * message manager.
+ *
+ * @param {nsIMessageListenerManager} target
+ * The message manager for which to return a broker.
+ *
+ * @returns {FilteringMessageManager}
+ */
+ get(target) {
+ if (this.has(target)) {
+ return super.get(target);
+ }
+
+ let broker = new FilteringMessageManager(this.messageName, this.callback, target);
+ this.set(target, broker);
+
+ if (target instanceof Ci.nsIDOMEventTarget) {
+ let onUnload = event => {
+ target.removeEventListener("unload", onUnload);
+ this.delete(target);
+ };
+ target.addEventListener("unload", onUnload);
+ }
+
+ return broker;
+ }
+}
+
+const MESSAGE_MESSAGE = "MessageChannel:Message";
+const MESSAGE_RESPONSE = "MessageChannel:Response";
+
+this.MessageChannel = {
+ init() {
+ Services.obs.addObserver(this, "message-manager-close", false);
+ Services.obs.addObserver(this, "message-manager-disconnect", false);
+
+ this.messageManagers = new FilteringMessageManagerMap(
+ MESSAGE_MESSAGE, this._handleMessage.bind(this));
+
+ this.responseManagers = new FilteringMessageManagerMap(
+ MESSAGE_RESPONSE, this._handleResponse.bind(this));
+
+ /**
+ * Contains a list of pending responses, either waiting to be
+ * received or waiting to be sent. @see _addPendingResponse
+ */
+ this.pendingResponses = new Set();
+ },
+
+ RESULT_SUCCESS: 0,
+ RESULT_DISCONNECTED: 1,
+ RESULT_NO_HANDLER: 2,
+ RESULT_MULTIPLE_HANDLERS: 3,
+ RESULT_ERROR: 4,
+ RESULT_NO_RESPONSE: 5,
+
+ REASON_DISCONNECTED: {
+ result: this.RESULT_DISCONNECTED,
+ message: "Message manager disconnected",
+ },
+
+ /**
+ * Specifies that only a single listener matching the specified
+ * recipient tag may be listening for the given message, at the other
+ * end of the target message manager.
+ *
+ * If no matching listeners exist, a RESULT_NO_HANDLER error will be
+ * returned. If multiple matching listeners exist, a
+ * RESULT_MULTIPLE_HANDLERS error will be returned.
+ */
+ RESPONSE_SINGLE: 0,
+
+ /**
+ * If multiple message managers matching the specified recipient tag
+ * are listening for a message, all listeners are notified, but only
+ * the first response or error is returned.
+ *
+ * Only handlers which return a value other than `undefined` are
+ * considered to have responded. Returning a Promise which evaluates
+ * to `undefined` is interpreted as an explicit response.
+ *
+ * If no matching listeners exist, a RESULT_NO_HANDLER error will be
+ * returned. If no listeners return a response, a RESULT_NO_RESPONSE
+ * error will be returned.
+ */
+ RESPONSE_FIRST: 1,
+
+ /**
+ * If multiple message managers matching the specified recipient tag
+ * are listening for a message, all listeners are notified, and all
+ * responses are returned as an array, once all listeners have
+ * replied.
+ */
+ RESPONSE_ALL: 2,
+
+ /**
+ * Fire-and-forget: The sender of this message does not expect a reply.
+ */
+ RESPONSE_NONE: 3,
+
+ /**
+ * Initializes message handlers for the given message managers if needed.
+ *
+ * @param {Array<nsIMessageListenerManager>} messageManagers
+ */
+ setupMessageManagers(messageManagers) {
+ for (let mm of messageManagers) {
+ // This call initializes a FilteringMessageManager for |mm| if needed.
+ // The FilteringMessageManager must be created to make sure that senders
+ // of messages that expect a reply, such as MessageChannel:Message, do
+ // actually receive a default reply even if there are no explicit message
+ // handlers.
+ this.messageManagers.get(mm);
+ }
+ },
+
+ /**
+ * Returns true if the properties of the `data` object match those in
+ * the `filter` object. Matching is done on a strict equality basis,
+ * and the behavior varies depending on the value of the `strict`
+ * parameter.
+ *
+ * @param {object} filter
+ * The filter object to match against.
+ * @param {object} data
+ * The data object being matched.
+ * @param {boolean} [strict=false]
+ * If true, all properties in the `filter` object have a
+ * corresponding property in `data` with the same value. If
+ * false, properties present in both objects must have the same
+ * value.
+ * @returns {boolean} True if the objects match.
+ */
+ matchesFilter(filter, data, strict = true) {
+ if (strict) {
+ return Object.keys(filter).every(key => {
+ return key in data && data[key] === filter[key];
+ });
+ }
+ return Object.keys(filter).every(key => {
+ return !(key in data) || data[key] === filter[key];
+ });
+ },
+
+ /**
+ * Adds a message listener to the given message manager.
+ *
+ * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
+ * The message managers on which to listen.
+ * @param {string|number} messageName
+ * The name of the message to listen for.
+ * @param {MessageReceiver} handler
+ * The handler to dispatch to. Must be an object with the following
+ * properties:
+ *
+ * receiveMessage:
+ * A method which is called for each message received by the
+ * listener. The method takes one argument, an object, with the
+ * following properties:
+ *
+ * messageName:
+ * The internal message name, as passed to `sendMessage`.
+ *
+ * target:
+ * The message manager which received this message.
+ *
+ * channelId:
+ * The internal ID of the transaction, used to map responses to
+ * the original sender.
+ *
+ * sender:
+ * An object describing the sender, as passed to `sendMessage`.
+ *
+ * recipient:
+ * An object describing the recipient, as passed to
+ * `sendMessage`.
+ *
+ * data:
+ * The contents of the message, as passed to `sendMessage`.
+ *
+ * The method may return any structured-clone-compatible
+ * object, which will be returned as a response to the message
+ * sender. It may also instead return a `Promise`, the
+ * resolution or rejection value of which will likewise be
+ * returned to the message sender.
+ *
+ * messageFilterStrict:
+ * An object containing arbitrary properties on which to filter
+ * received messages. Messages will only be dispatched to this
+ * object if the `recipient` object passed to `sendMessage`
+ * matches this filter, as determined by `matchesFilter` with
+ * `strict=true`.
+ *
+ * messageFilterPermissive:
+ * An object containing arbitrary properties on which to filter
+ * received messages. Messages will only be dispatched to this
+ * object if the `recipient` object passed to `sendMessage`
+ * matches this filter, as determined by `matchesFilter` with
+ * `strict=false`.
+ *
+ * filterMessage:
+ * An optional function that prevents the handler from handling a
+ * message by returning `false`. See `getHandlers` for the parameters.
+ */
+ addListener(targets, messageName, handler) {
+ for (let target of [].concat(targets)) {
+ this.messageManagers.get(target).addHandler(messageName, handler);
+ }
+ },
+
+ /**
+ * Removes a message listener from the given message manager.
+ *
+ * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
+ * The message managers on which to stop listening.
+ * @param {string|number} messageName
+ * The name of the message to stop listening for.
+ * @param {MessageReceiver} handler
+ * The handler to stop dispatching to.
+ */
+ removeListener(targets, messageName, handler) {
+ for (let target of [].concat(targets)) {
+ if (this.messageManagers.has(target)) {
+ this.messageManagers.get(target).removeHandler(messageName, handler);
+ }
+ }
+ },
+
+ /**
+ * Sends a message via the given message manager. Returns a promise which
+ * resolves or rejects with the return value of the message receiver.
+ *
+ * The promise also rejects if there is no matching listener, or the other
+ * side of the message manager disconnects before the response is received.
+ *
+ * @param {nsIMessageSender} target
+ * The message manager on which to send the message.
+ * @param {string} messageName
+ * The name of the message to send, as passed to `addListener`.
+ * @param {object} data
+ * A structured-clone-compatible object to send to the message
+ * recipient.
+ * @param {object} [options]
+ * An object containing any of the following properties:
+ * @param {object} [options.recipient]
+ * A structured-clone-compatible object to identify the message
+ * recipient. The object must match the `messageFilterStrict` and
+ * `messageFilterPermissive` filters defined by recipients in order
+ * for the message to be received.
+ * @param {object} [options.sender]
+ * A structured-clone-compatible object to identify the message
+ * sender. This object may also be used to avoid delivering the
+ * message to the sender, and as a filter to prematurely
+ * abort responses when the sender is being destroyed.
+ * @see `abortResponses`.
+ * @param {integer} [options.responseType=RESPONSE_SINGLE]
+ * Specifies the type of response expected. See the `RESPONSE_*`
+ * contents for details.
+ * @returns {Promise}
+ */
+ sendMessage(target, messageName, data, options = {}) {
+ let sender = options.sender || {};
+ let recipient = options.recipient || {};
+ let responseType = options.responseType || this.RESPONSE_SINGLE;
+
+ let channelId = ExtensionUtils.getUniqueId();
+ let message = {messageName, channelId, sender, recipient, data, responseType};
+
+ if (responseType == this.RESPONSE_NONE) {
+ try {
+ target.sendAsyncMessage(MESSAGE_MESSAGE, message);
+ } catch (e) {
+ // Caller is not expecting a reply, so dump the error to the console.
+ Cu.reportError(e);
+ return Promise.reject(e);
+ }
+ return Promise.resolve(); // Not expecting any reply.
+ }
+
+ let deferred = PromiseUtils.defer();
+ deferred.sender = recipient;
+ deferred.messageManager = target;
+
+ this._addPendingResponse(deferred);
+
+ // The channel ID is used as the message name when routing responses.
+ // Add a message listener to the response broker, and remove it once
+ // we've gotten (or canceled) a response.
+ let broker = this.responseManagers.get(target);
+ broker.addHandler(channelId, deferred);
+
+ let cleanup = () => {
+ broker.removeHandler(channelId, deferred);
+ };
+ deferred.promise.then(cleanup, cleanup);
+
+ try {
+ target.sendAsyncMessage(MESSAGE_MESSAGE, message);
+ } catch (e) {
+ deferred.reject(e);
+ }
+ return deferred.promise;
+ },
+
+ _callHandlers(handlers, data) {
+ let responseType = data.responseType;
+
+ // At least one handler is required for all response types but
+ // RESPONSE_ALL.
+ if (handlers.length == 0 && responseType != this.RESPONSE_ALL) {
+ return Promise.reject({result: MessageChannel.RESULT_NO_HANDLER,
+ message: "No matching message handler"});
+ }
+
+ if (responseType == this.RESPONSE_SINGLE) {
+ if (handlers.length > 1) {
+ return Promise.reject({result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
+ message: `Multiple matching handlers for ${data.messageName}`});
+ }
+
+ // Note: We use `new Promise` rather than `Promise.resolve` here
+ // so that errors from the handler are trapped and converted into
+ // rejected promises.
+ return new Promise(resolve => {
+ resolve(handlers[0].receiveMessage(data));
+ });
+ }
+
+ let responses = handlers.map(handler => {
+ try {
+ return handler.receiveMessage(data);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ });
+ responses = responses.filter(response => response !== undefined);
+
+ switch (responseType) {
+ case this.RESPONSE_FIRST:
+ if (responses.length == 0) {
+ return Promise.reject({result: MessageChannel.RESULT_NO_RESPONSE,
+ message: "No handler returned a response"});
+ }
+
+ return Promise.race(responses);
+
+ case this.RESPONSE_ALL:
+ return Promise.all(responses);
+ }
+ return Promise.reject({message: "Invalid response type"});
+ },
+
+ /**
+ * Handles dispatching message callbacks from the message brokers to their
+ * appropriate `MessageReceivers`, and routing the responses back to the
+ * original senders.
+ *
+ * Each handler object is a `MessageReceiver` object as passed to
+ * `addListener`.
+ *
+ * @param {Array<MessageHandler>} handlers
+ * @param {object} data
+ * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
+ */
+ _handleMessage(handlers, data) {
+ if (data.responseType == this.RESPONSE_NONE) {
+ handlers.forEach(handler => {
+ // The sender expects no reply, so dump any errors to the console.
+ new Promise(resolve => {
+ resolve(handler.receiveMessage(data));
+ }).catch(e => {
+ Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
+ });
+ });
+ // Note: Unhandled messages are silently dropped.
+ return;
+ }
+
+ let target = new MessageManagerProxy(data.target);
+
+ let deferred = {
+ sender: data.sender,
+ messageManager: target,
+ };
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.reject = reject;
+
+ this._callHandlers(handlers, data).then(resolve, reject);
+ }).then(
+ value => {
+ let response = {
+ result: this.RESULT_SUCCESS,
+ messageName: data.channelId,
+ recipient: {},
+ value,
+ };
+
+ target.sendAsyncMessage(MESSAGE_RESPONSE, response);
+ },
+ error => {
+ let response = {
+ result: this.RESULT_ERROR,
+ messageName: data.channelId,
+ recipient: {},
+ error: {},
+ };
+
+ if (error && typeof(error) == "object") {
+ if (error.result) {
+ response.result = error.result;
+ }
+ // Error objects are not structured-clonable, so just copy
+ // over the important properties.
+ for (let key of ["fileName", "filename", "lineNumber",
+ "columnNumber", "message", "stack", "result"]) {
+ if (key in error) {
+ response.error[key] = error[key];
+ }
+ }
+ }
+
+ target.sendAsyncMessage(MESSAGE_RESPONSE, response);
+ }).catch(e => {
+ Cu.reportError(e);
+ }).then(() => {
+ target.dispose();
+ });
+
+ this._addPendingResponse(deferred);
+ },
+
+ /**
+ * Handles message callbacks from the response brokers.
+ *
+ * Each handler object is a deferred object created by `sendMessage`, and
+ * should be resolved or rejected based on the contents of the response.
+ *
+ * @param {Array<MessageHandler>} handlers
+ * @param {object} data
+ * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
+ */
+ _handleResponse(handlers, data) {
+ // If we have an error at this point, we have handler to report it to,
+ // so just log it.
+ if (handlers.length == 0) {
+ Cu.reportError(`No matching message response handler for ${data.messageName}`);
+ } else if (handlers.length > 1) {
+ Cu.reportError(`Multiple matching response handlers for ${data.messageName}`);
+ } else if (data.result === this.RESULT_SUCCESS) {
+ handlers[0].resolve(data.value);
+ } else {
+ handlers[0].reject(data.error);
+ }
+ },
+
+ /**
+ * Adds a pending response to the the `pendingResponses` list.
+ *
+ * The response object must be a deferred promise with the following
+ * properties:
+ *
+ * promise:
+ * The promise object which resolves or rejects when the response
+ * is no longer pending.
+ *
+ * reject:
+ * A function which, when called, causes the `promise` object to be
+ * rejected.
+ *
+ * sender:
+ * A sender object, as passed to `sendMessage.
+ *
+ * messageManager:
+ * The message manager the response will be sent or received on.
+ *
+ * When the promise resolves or rejects, it will be removed from the
+ * list.
+ *
+ * These values are used to clear pending responses when execution
+ * contexts are destroyed.
+ *
+ * @param {Deferred} deferred
+ */
+ _addPendingResponse(deferred) {
+ let cleanup = () => {
+ this.pendingResponses.delete(deferred);
+ };
+ this.pendingResponses.add(deferred);
+ deferred.promise.then(cleanup, cleanup);
+ },
+
+ /**
+ * Aborts any pending message responses to senders matching the given
+ * filter.
+ *
+ * @param {object} sender
+ * The object on which to filter senders, as determined by
+ * `matchesFilter`.
+ * @param {object} [reason]
+ * An optional object describing the reason the response was aborted.
+ * Will be passed to the promise rejection handler of all aborted
+ * responses.
+ */
+ abortResponses(sender, reason = this.REASON_DISCONNECTED) {
+ for (let response of this.pendingResponses) {
+ if (this.matchesFilter(sender, response.sender)) {
+ response.reject(reason);
+ }
+ }
+ },
+
+ /**
+ * Aborts any pending message responses to the broker for the given
+ * message manager.
+ *
+ * @param {nsIMessageListenerManager} target
+ * The message manager for which to abort brokers.
+ * @param {object} reason
+ * An object describing the reason the responses were aborted.
+ * Will be passed to the promise rejection handler of all aborted
+ * responses.
+ */
+ abortMessageManager(target, reason) {
+ for (let response of this.pendingResponses) {
+ if (MessageManagerProxy.matches(response.messageManager, target)) {
+ response.reject(reason);
+ }
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "message-manager-close":
+ case "message-manager-disconnect":
+ try {
+ if (this.responseManagers.has(subject)) {
+ this.abortMessageManager(subject, this.REASON_DISCONNECTED);
+ }
+ } finally {
+ this.responseManagers.delete(subject);
+ this.messageManagers.delete(subject);
+ }
+ break;
+ }
+ },
+};
+
+MessageChannel.init();
diff --git a/toolkit/components/extensions/NativeMessaging.jsm b/toolkit/components/extensions/NativeMessaging.jsm
new file mode 100644
index 0000000000..3d8658a3f4
--- /dev/null
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -0,0 +1,443 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["HostManifestManager", "NativeApp"];
+/* globals NativeApp */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+ "resource://gre/modules/ExtensionChild.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
+ "resource://gre/modules/Subprocess.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm");
+
+const HOST_MANIFEST_SCHEMA = "chrome://extensions/content/schemas/native_host_manifest.json";
+const VALID_APPLICATION = /^\w+(\.\w+)*$/;
+
+// For a graceful shutdown (i.e., when the extension is unloaded or when it
+// explicitly calls disconnect() on a native port), how long we give the native
+// application to exit before we start trying to kill it. (in milliseconds)
+const GRACEFUL_SHUTDOWN_TIME = 3000;
+
+// Hard limits on maximum message size that can be read/written
+// These are defined in the native messaging documentation, note that
+// the write limit is imposed by the "wire protocol" in which message
+// boundaries are defined by preceding each message with its length as
+// 4-byte unsigned integer so this is the largest value that can be
+// represented. Good luck generating a serialized message that large,
+// the practical write limit is likely to be dictated by available memory.
+const MAX_READ = 1024 * 1024;
+const MAX_WRITE = 0xffffffff;
+
+// Preferences that can lower the message size limits above,
+// used for testing the limits.
+const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
+const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
+
+const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
+
+this.HostManifestManager = {
+ _initializePromise: null,
+ _lookup: null,
+
+ init() {
+ if (!this._initializePromise) {
+ let platform = AppConstants.platform;
+ if (platform == "win") {
+ this._lookup = this._winLookup;
+ } else if (platform == "macosx" || platform == "linux") {
+ let dirs = [
+ Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path,
+ Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path,
+ ];
+ this._lookup = (application, context) => this._tryPaths(application, dirs, context);
+ } else {
+ throw new Error(`Native messaging is not supported on ${AppConstants.platform}`);
+ }
+ this._initializePromise = Schemas.load(HOST_MANIFEST_SCHEMA);
+ }
+ return this._initializePromise;
+ },
+
+ _winLookup(application, context) {
+ const REGISTRY = Ci.nsIWindowsRegKey;
+ let regPath = `${REGPATH}\\${application}`;
+ let path = WindowsRegistry.readRegKey(REGISTRY.ROOT_KEY_CURRENT_USER,
+ regPath, "", REGISTRY.WOW64_64);
+ if (!path) {
+ path = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ regPath, "", REGISTRY.WOW64_64);
+ }
+ if (!path) {
+ return null;
+ }
+ return this._tryPath(path, application, context)
+ .then(manifest => manifest ? {path, manifest} : null);
+ },
+
+ _tryPath(path, application, context) {
+ return Promise.resolve()
+ .then(() => OS.File.read(path, {encoding: "utf-8"}))
+ .then(data => {
+ let manifest;
+ try {
+ manifest = JSON.parse(data);
+ } catch (ex) {
+ let msg = `Error parsing native host manifest ${path}: ${ex.message}`;
+ Cu.reportError(msg);
+ return null;
+ }
+
+ let normalized = Schemas.normalize(manifest, "manifest.NativeHostManifest", context);
+ if (normalized.error) {
+ Cu.reportError(normalized.error);
+ return null;
+ }
+ manifest = normalized.value;
+ if (manifest.name != application) {
+ let msg = `Native host manifest ${path} has name property ${manifest.name} (expected ${application})`;
+ Cu.reportError(msg);
+ return null;
+ }
+ return normalized.value;
+ }).catch(ex => {
+ if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ return null;
+ }
+ throw ex;
+ });
+ },
+
+ _tryPaths: Task.async(function* (application, dirs, context) {
+ for (let dir of dirs) {
+ let path = OS.Path.join(dir, `${application}.json`);
+ let manifest = yield this._tryPath(path, application, context);
+ if (manifest) {
+ return {path, manifest};
+ }
+ }
+ return null;
+ }),
+
+ /**
+ * Search for a valid native host manifest for the given application name.
+ * The directories searched and rules for manifest validation are all
+ * detailed in the native messaging documentation.
+ *
+ * @param {string} application The name of the applciation to search for.
+ * @param {object} context A context object as expected by Schemas.normalize.
+ * @returns {object} The contents of the validated manifest, or null if
+ * no valid manifest can be found for this application.
+ */
+ lookupApplication(application, context) {
+ if (!VALID_APPLICATION.test(application)) {
+ throw new Error(`Invalid application "${application}"`);
+ }
+ return this.init().then(() => this._lookup(application, context));
+ },
+};
+
+this.NativeApp = class extends EventEmitter {
+ /**
+ * @param {BaseContext} context The context that initiated the native app.
+ * @param {string} application The identifier of the native app.
+ */
+ constructor(context, application) {
+ super();
+
+ this.context = context;
+ this.name = application;
+
+ // We want a close() notification when the window is destroyed.
+ this.context.callOnClose(this);
+
+ this.proc = null;
+ this.readPromise = null;
+ this.sendQueue = [];
+ this.writePromise = null;
+ this.sentDisconnect = false;
+
+ this.startupPromise = HostManifestManager.lookupApplication(application, context)
+ .then(hostInfo => {
+ // Put the two errors together to not leak information about whether a native
+ // application is installed to addons that do not have the right permission.
+ if (!hostInfo || !hostInfo.manifest.allowed_extensions.includes(context.extension.id)) {
+ throw new context.cloneScope.Error(`This extension does not have permission to use native application ${application} (or the application is not installed)`);
+ }
+
+ let command = hostInfo.manifest.path;
+ if (AppConstants.platform == "win") {
+ // OS.Path.join() ignores anything before the last absolute path
+ // it sees, so if command is already absolute, it remains unchanged
+ // here. If it is relative, we get the proper absolute path here.
+ command = OS.Path.join(OS.Path.dirname(hostInfo.path), command);
+ }
+
+ let subprocessOpts = {
+ command: command,
+ arguments: [hostInfo.path],
+ workdir: OS.Path.dirname(command),
+ stderr: "pipe",
+ };
+ return Subprocess.call(subprocessOpts);
+ }).then(proc => {
+ this.startupPromise = null;
+ this.proc = proc;
+ this._startRead();
+ this._startWrite();
+ this._startStderrRead();
+ }).catch(err => {
+ this.startupPromise = null;
+ Cu.reportError(err instanceof Error ? err : err.message);
+ this._cleanup(err);
+ });
+ }
+
+ /**
+ * Open a connection to a native messaging host.
+ *
+ * @param {BaseContext} context The context associated with the port.
+ * @param {nsIMessageSender} messageManager The message manager used to send
+ * and receive messages from the port's creator.
+ * @param {string} portId A unique internal ID that identifies the port.
+ * @param {object} sender The object describing the creator of the connection
+ * request.
+ * @param {string} application The name of the native messaging host.
+ */
+ static onConnectNative(context, messageManager, portId, sender, application) {
+ let app = new NativeApp(context, application);
+ let port = new ExtensionChild.Port(context, messageManager, [Services.mm], "", portId, sender, sender);
+ app.once("disconnect", (what, err) => port.disconnect(err));
+
+ /* eslint-disable mozilla/balanced-listeners */
+ app.on("message", (what, msg) => port.postMessage(msg));
+ /* eslint-enable mozilla/balanced-listeners */
+
+ port.registerOnMessage(msg => app.send(msg));
+ port.registerOnDisconnect(msg => app.close());
+ }
+
+ /**
+ * @param {BaseContext} context The scope from where `message` originates.
+ * @param {*} message A message from the extension, meant for a native app.
+ * @returns {ArrayBuffer} An ArrayBuffer that can be sent to the native app.
+ */
+ static encodeMessage(context, message) {
+ message = context.jsonStringify(message);
+ let buffer = new TextEncoder().encode(message).buffer;
+ if (buffer.byteLength > NativeApp.maxWrite) {
+ throw new context.cloneScope.Error("Write too big");
+ }
+ return buffer;
+ }
+
+ // A port is definitely "alive" if this.proc is non-null. But we have
+ // to provide a live port object immediately when connecting so we also
+ // need to consider a port alive if proc is null but the startupPromise
+ // is still pending.
+ get _isDisconnected() {
+ return (!this.proc && !this.startupPromise);
+ }
+
+ _startRead() {
+ if (this.readPromise) {
+ throw new Error("Entered _startRead() while readPromise is non-null");
+ }
+ this.readPromise = this.proc.stdout.readUint32()
+ .then(len => {
+ if (len > NativeApp.maxRead) {
+ throw new this.context.cloneScope.Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.`);
+ }
+ return this.proc.stdout.readJSON(len);
+ }).then(msg => {
+ this.emit("message", msg);
+ this.readPromise = null;
+ this._startRead();
+ }).catch(err => {
+ if (err.errorCode != Subprocess.ERROR_END_OF_FILE) {
+ Cu.reportError(err instanceof Error ? err : err.message);
+ }
+ this._cleanup(err);
+ });
+ }
+
+ _startWrite() {
+ if (this.sendQueue.length == 0) {
+ return;
+ }
+
+ if (this.writePromise) {
+ throw new Error("Entered _startWrite() while writePromise is non-null");
+ }
+
+ let buffer = this.sendQueue.shift();
+ let uintArray = Uint32Array.of(buffer.byteLength);
+
+ this.writePromise = Promise.all([
+ this.proc.stdin.write(uintArray.buffer),
+ this.proc.stdin.write(buffer),
+ ]).then(() => {
+ this.writePromise = null;
+ this._startWrite();
+ }).catch(err => {
+ Cu.reportError(err.message);
+ this._cleanup(err);
+ });
+ }
+
+ _startStderrRead() {
+ let proc = this.proc;
+ let app = this.name;
+ Task.spawn(function* () {
+ let partial = "";
+ while (true) {
+ let data = yield proc.stderr.readString();
+ if (data.length == 0) {
+ // We have hit EOF, just stop reading
+ if (partial) {
+ Services.console.logStringMessage(`stderr output from native app ${app}: ${partial}`);
+ }
+ break;
+ }
+
+ let lines = data.split(/\r?\n/);
+ lines[0] = partial + lines[0];
+ partial = lines.pop();
+
+ for (let line of lines) {
+ Services.console.logStringMessage(`stderr output from native app ${app}: ${line}`);
+ }
+ }
+ });
+ }
+
+ send(msg) {
+ if (this._isDisconnected) {
+ throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
+ }
+ if (Cu.getClassName(msg, true) != "ArrayBuffer") {
+ // This error cannot be triggered by extensions; it indicates an error in
+ // our implementation.
+ throw new Error("The message to the native messaging host is not an ArrayBuffer");
+ }
+
+ let buffer = msg;
+
+ if (buffer.byteLength > NativeApp.maxWrite) {
+ throw new this.context.cloneScope.Error("Write too big");
+ }
+
+ this.sendQueue.push(buffer);
+ if (!this.startupPromise && !this.writePromise) {
+ this._startWrite();
+ }
+ }
+
+ // Shut down the native application and also signal to the extension
+ // that the connect has been disconnected.
+ _cleanup(err) {
+ this.context.forgetOnClose(this);
+
+ let doCleanup = () => {
+ // Set a timer to kill the process gracefully after one timeout
+ // interval and kill it forcefully after two intervals.
+ let timer = setTimeout(() => {
+ this.proc.kill(GRACEFUL_SHUTDOWN_TIME);
+ }, GRACEFUL_SHUTDOWN_TIME);
+
+ let promise = Promise.all([
+ this.proc.stdin.close()
+ .catch(err => {
+ if (err.errorCode != Subprocess.ERROR_END_OF_FILE) {
+ throw err;
+ }
+ }),
+ this.proc.wait(),
+ ]).then(() => {
+ this.proc = null;
+ clearTimeout(timer);
+ });
+
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ `Native Messaging: Wait for application ${this.name} to exit`,
+ promise);
+
+ promise.then(() => {
+ AsyncShutdown.profileBeforeChange.removeBlocker(promise);
+ });
+
+ return promise;
+ };
+
+ if (this.proc) {
+ doCleanup();
+ } else if (this.startupPromise) {
+ this.startupPromise.then(doCleanup);
+ }
+
+ if (!this.sentDisconnect) {
+ this.sentDisconnect = true;
+ if (err && err.errorCode == Subprocess.ERROR_END_OF_FILE) {
+ err = null;
+ }
+ this.emit("disconnect", err);
+ }
+ }
+
+ // Called from Context when the extension is shut down.
+ close() {
+ this._cleanup();
+ }
+
+ sendMessage(msg) {
+ let responsePromise = new Promise((resolve, reject) => {
+ this.once("message", (what, msg) => { resolve(msg); });
+ this.once("disconnect", (what, err) => { reject(err); });
+ });
+
+ let result = this.startupPromise.then(() => {
+ this.send(msg);
+ return responsePromise;
+ });
+
+ result.then(() => {
+ this._cleanup();
+ }, () => {
+ // Prevent the response promise from being reported as an
+ // unchecked rejection if the startup promise fails.
+ responsePromise.catch(() => {});
+
+ this._cleanup();
+ });
+
+ return result;
+ }
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxRead", PREF_MAX_READ, MAX_READ);
+XPCOMUtils.defineLazyPreferenceGetter(NativeApp, "maxWrite", PREF_MAX_WRITE, MAX_WRITE);
diff --git a/toolkit/components/extensions/Schemas.jsm b/toolkit/components/extensions/Schemas.jsm
new file mode 100644
index 0000000000..159211c79e
--- /dev/null
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -0,0 +1,2143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+const global = this;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ DefaultMap,
+ instanceOf,
+} = ExtensionUtils;
+
+class DeepMap extends DefaultMap {
+ constructor() {
+ super(() => new DeepMap());
+ }
+
+ getPath(...keys) {
+ return keys.reduce((map, prop) => map.get(prop), this);
+ }
+}
+
+XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
+ "@mozilla.org/addons/content-policy;1",
+ "nsIAddonContentPolicy");
+
+this.EXPORTED_SYMBOLS = ["Schemas"];
+
+/* globals Schemas, URL */
+
+function readJSON(url) {
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ // Convert status code to a string
+ let e = Components.Exception("", status);
+ reject(new Error(`Error while loading '${url}' (${e.name})`));
+ return;
+ }
+ try {
+ let text = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+
+ // Chrome JSON files include a license comment that we need to
+ // strip off for this to be valid JSON. As a hack, we just
+ // look for the first '[' character, which signals the start
+ // of the JSON content.
+ let index = text.indexOf("[");
+ text = text.slice(index);
+
+ resolve(JSON.parse(text));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+}
+
+/**
+ * Defines a lazy getter for the given property on the given object. Any
+ * security wrappers are waived on the object before the property is
+ * defined, and the getter and setter methods are wrapped for the target
+ * scope.
+ *
+ * The given getter function is guaranteed to be called only once, even
+ * if the target scope retrieves the wrapped getter from the property
+ * descriptor and calls it directly.
+ *
+ * @param {object} object
+ * The object on which to define the getter.
+ * @param {string|Symbol} prop
+ * The property name for which to define the getter.
+ * @param {function} getter
+ * The function to call in order to generate the final property
+ * value.
+ */
+function exportLazyGetter(object, prop, getter) {
+ object = Cu.waiveXrays(object);
+
+ let redefine = value => {
+ if (value === undefined) {
+ delete object[prop];
+ } else {
+ Object.defineProperty(object, prop, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value,
+ });
+ }
+
+ getter = null;
+
+ return value;
+ };
+
+ Object.defineProperty(object, prop, {
+ enumerable: true,
+ configurable: true,
+
+ get: Cu.exportFunction(function() {
+ return redefine(getter.call(this));
+ }, object),
+
+ set: Cu.exportFunction(value => {
+ redefine(value);
+ }, object),
+ });
+}
+
+const POSTPROCESSORS = {
+ convertImageDataToURL(imageData, context) {
+ let document = context.cloneScope.document;
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = imageData.width;
+ canvas.height = imageData.height;
+ canvas.getContext("2d").putImageData(imageData, 0, 0);
+
+ return canvas.toDataURL("image/png");
+ },
+};
+
+// Parses a regular expression, with support for the Python extended
+// syntax that allows setting flags by including the string (?im)
+function parsePattern(pattern) {
+ let flags = "";
+ let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
+ if (match) {
+ [, flags, pattern] = match;
+ }
+ return new RegExp(pattern, flags);
+}
+
+function getValueBaseType(value) {
+ let t = typeof(value);
+ if (t == "object") {
+ if (value === null) {
+ return "null";
+ } else if (Array.isArray(value)) {
+ return "array";
+ } else if (Object.prototype.toString.call(value) == "[object ArrayBuffer]") {
+ return "binary";
+ }
+ } else if (t == "number") {
+ if (value % 1 == 0) {
+ return "integer";
+ }
+ }
+ return t;
+}
+
+// Methods of Context that are used by Schemas.normalize. These methods can be
+// overridden at the construction of Context.
+const CONTEXT_FOR_VALIDATION = [
+ "checkLoadURL",
+ "hasPermission",
+ "logError",
+];
+
+// Methods of Context that are used by Schemas.inject.
+// Callers of Schemas.inject should implement all of these methods.
+const CONTEXT_FOR_INJECTION = [
+ ...CONTEXT_FOR_VALIDATION,
+ "shouldInject",
+ "getImplementation",
+];
+
+/**
+ * A context for schema validation and error reporting. This class is only used
+ * internally within Schemas.
+ */
+class Context {
+ /**
+ * @param {object} params Provides the implementation of this class.
+ * @param {Array<string>} overridableMethods
+ */
+ constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
+ this.params = params;
+
+ this.path = [];
+ this.preprocessors = {
+ localize(value, context) {
+ return value;
+ },
+ };
+ this.postprocessors = POSTPROCESSORS;
+ this.isChromeCompat = false;
+
+ this.currentChoices = new Set();
+ this.choicePathIndex = 0;
+
+ for (let method of overridableMethods) {
+ if (method in params) {
+ this[method] = params[method].bind(params);
+ }
+ }
+
+ let props = ["preprocessors", "isChromeCompat"];
+ for (let prop of props) {
+ if (prop in params) {
+ if (prop in this && typeof this[prop] == "object") {
+ Object.assign(this[prop], params[prop]);
+ } else {
+ this[prop] = params[prop];
+ }
+ }
+ }
+ }
+
+ get choicePath() {
+ let path = this.path.slice(this.choicePathIndex);
+ return path.join(".");
+ }
+
+ get cloneScope() {
+ return this.params.cloneScope;
+ }
+
+ get url() {
+ return this.params.url;
+ }
+
+ get principal() {
+ return this.params.principal || Services.scriptSecurityManager.createNullPrincipal({});
+ }
+
+ /**
+ * Checks whether `url` may be loaded by the extension in this context.
+ *
+ * @param {string} url The URL that the extension wished to load.
+ * @returns {boolean} Whether the context may load `url`.
+ */
+ checkLoadURL(url) {
+ let ssm = Services.scriptSecurityManager;
+ try {
+ ssm.checkLoadURIStrWithPrincipal(this.principal, url,
+ ssm.DISALLOW_INHERIT_PRINCIPAL);
+ } catch (e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether this context has the given permission.
+ *
+ * @param {string} permission
+ * The name of the permission to check.
+ *
+ * @returns {boolean} True if the context has the given permission.
+ */
+ hasPermission(permission) {
+ return false;
+ }
+
+ /**
+ * Returns an error result object with the given message, for return
+ * by Type normalization functions.
+ *
+ * If the context has a `currentTarget` value, this is prepended to
+ * the message to indicate the location of the error.
+ *
+ * @param {string} errorMessage
+ * The error message which will be displayed when this is the
+ * only possible matching schema.
+ * @param {string} choicesMessage
+ * The message describing the valid what constitutes a valid
+ * value for this schema, which will be displayed when multiple
+ * schema choices are available and none match.
+ *
+ * A caller may pass `null` to prevent a choice from being
+ * added, but this should *only* be done from code processing a
+ * choices type.
+ * @returns {object}
+ */
+ error(errorMessage, choicesMessage = undefined) {
+ if (choicesMessage !== null) {
+ let {choicePath} = this;
+ if (choicePath) {
+ choicesMessage = `.${choicePath} must ${choicesMessage}`;
+ }
+
+ this.currentChoices.add(choicesMessage);
+ }
+
+ if (this.currentTarget) {
+ return {error: `Error processing ${this.currentTarget}: ${errorMessage}`};
+ }
+ return {error: errorMessage};
+ }
+
+ /**
+ * Creates an `Error` object belonging to the current unprivileged
+ * scope. If there is no unprivileged scope associated with this
+ * context, the message is returned as a string.
+ *
+ * If the context has a `currentTarget` value, this is prepended to
+ * the message, in the same way as for the `error` method.
+ *
+ * @param {string} message
+ * @returns {Error}
+ */
+ makeError(message) {
+ let {error} = this.error(message);
+ if (this.cloneScope) {
+ return new this.cloneScope.Error(error);
+ }
+ return error;
+ }
+
+ /**
+ * Logs the given error to the console. May be overridden to enable
+ * custom logging.
+ *
+ * @param {Error|string} error
+ */
+ logError(error) {
+ Cu.reportError(error);
+ }
+
+ /**
+ * Returns the name of the value currently being normalized. For a
+ * nested object, this is usually approximately equivalent to the
+ * JavaScript property accessor for that property. Given:
+ *
+ * { foo: { bar: [{ baz: x }] } }
+ *
+ * When processing the value for `x`, the currentTarget is
+ * 'foo.bar.0.baz'
+ */
+ get currentTarget() {
+ return this.path.join(".");
+ }
+
+ /**
+ * Executes the given callback, and returns an array of choice strings
+ * passed to {@see #error} during its execution.
+ *
+ * @param {function} callback
+ * @returns {object}
+ * An object with a `result` property containing the return
+ * value of the callback, and a `choice` property containing
+ * an array of choices.
+ */
+ withChoices(callback) {
+ let {currentChoices, choicePathIndex} = this;
+
+ let choices = new Set();
+ this.currentChoices = choices;
+ this.choicePathIndex = this.path.length;
+
+ try {
+ let result = callback();
+
+ return {result, choices: Array.from(choices)};
+ } finally {
+ this.currentChoices = currentChoices;
+ this.choicePathIndex = choicePathIndex;
+
+ choices = Array.from(choices);
+ if (choices.length == 1) {
+ currentChoices.add(choices[0]);
+ } else if (choices.length) {
+ let n = choices.length - 1;
+ choices[n] = `or ${choices[n]}`;
+
+ this.error(null, `must either [${choices.join(", ")}]`);
+ }
+ }
+ }
+
+ /**
+ * Appends the given component to the `currentTarget` path to indicate
+ * that it is being processed, calls the given callback function, and
+ * then restores the original path.
+ *
+ * This is used to identify the path of the property being processed
+ * when reporting type errors.
+ *
+ * @param {string} component
+ * @param {function} callback
+ * @returns {*}
+ */
+ withPath(component, callback) {
+ this.path.push(component);
+ try {
+ return callback();
+ } finally {
+ this.path.pop();
+ }
+ }
+}
+
+/**
+ * Holds methods that run the actual implementation of the extension APIs. These
+ * methods are only called if the extension API invocation matches the signature
+ * as defined in the schema. Otherwise an error is reported to the context.
+ */
+class InjectionContext extends Context {
+ constructor(params) {
+ super(params, CONTEXT_FOR_INJECTION);
+ }
+
+ /**
+ * Check whether the API should be injected.
+ *
+ * @abstract
+ * @param {string} namespace The namespace of the API. This may contain dots,
+ * e.g. in the case of "devtools.inspectedWindow".
+ * @param {string} [name] The name of the property in the namespace.
+ * `null` if we are checking whether the namespace should be injected.
+ * @param {Array<string>} allowedContexts A list of additional contexts in which
+ * this API should be available. May include any of:
+ * "main" - The main chrome browser process.
+ * "addon" - An addon process.
+ * "content" - A content process.
+ * @returns {boolean} Whether the API should be injected.
+ */
+ shouldInject(namespace, name, allowedContexts) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Generate the implementation for `namespace`.`name`.
+ *
+ * @abstract
+ * @param {string} namespace The full path to the namespace of the API, minus
+ * the name of the method or property. E.g. "storage.local".
+ * @param {string} name The name of the method, property or event.
+ * @returns {SchemaAPIInterface} The implementation of the API.
+ */
+ getImplementation(namespace, name) {
+ throw new Error("Not implemented");
+ }
+}
+
+/**
+ * The methods in this singleton represent the "format" specifier for
+ * JSON Schema string types.
+ *
+ * Each method either returns a normalized version of the original
+ * value, or throws an error if the value is not valid for the given
+ * format.
+ */
+const FORMATS = {
+ url(string, context) {
+ let url = new URL(string).href;
+
+ if (!context.checkLoadURL(url)) {
+ throw new Error(`Access denied for URL ${url}`);
+ }
+ return url;
+ },
+
+ relativeUrl(string, context) {
+ if (!context.url) {
+ // If there's no context URL, return relative URLs unresolved, and
+ // skip security checks for them.
+ try {
+ new URL(string);
+ } catch (e) {
+ return string;
+ }
+ }
+
+ let url = new URL(string, context.url).href;
+
+ if (!context.checkLoadURL(url)) {
+ throw new Error(`Access denied for URL ${url}`);
+ }
+ return url;
+ },
+
+ strictRelativeUrl(string, context) {
+ // Do not accept a string which resolves as an absolute URL, or any
+ // protocol-relative URL.
+ if (!string.startsWith("//")) {
+ try {
+ new URL(string);
+ } catch (e) {
+ return FORMATS.relativeUrl(string, context);
+ }
+ }
+
+ throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`);
+ },
+
+ contentSecurityPolicy(string, context) {
+ let error = contentPolicyService.validateAddonCSP(string);
+ if (error != null) {
+ throw new SyntaxError(error);
+ }
+ return string;
+ },
+
+ date(string, context) {
+ // A valid ISO 8601 timestamp.
+ const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
+ if (!PATTERN.test(string)) {
+ throw new Error(`Invalid date string ${string}`);
+ }
+ // Our pattern just checks the format, we could still have invalid
+ // values (e.g., month=99 or month=02 and day=31). Let the Date
+ // constructor do the dirty work of validating.
+ if (isNaN(new Date(string))) {
+ throw new Error(`Invalid date string ${string}`);
+ }
+ return string;
+ },
+};
+
+// Schema files contain namespaces, and each namespace contains types,
+// properties, functions, and events. An Entry is a base class for
+// types, properties, functions, and events.
+class Entry {
+ constructor(schema = {}) {
+ /**
+ * If set to any value which evaluates as true, this entry is
+ * deprecated, and any access to it will result in a deprecation
+ * warning being logged to the browser console.
+ *
+ * If the value is a string, it will be appended to the deprecation
+ * message. If it contains the substring "${value}", it will be
+ * replaced with a string representation of the value being
+ * processed.
+ *
+ * If the value is any other truthy value, a generic deprecation
+ * message will be emitted.
+ */
+ this.deprecated = false;
+ if ("deprecated" in schema) {
+ this.deprecated = schema.deprecated;
+ }
+
+ /**
+ * @property {string} [preprocessor]
+ * If set to a string value, and a preprocessor of the same is
+ * defined in the validation context, it will be applied to this
+ * value prior to any normalization.
+ */
+ this.preprocessor = schema.preprocess || null;
+
+ /**
+ * @property {string} [postprocessor]
+ * If set to a string value, and a postprocessor of the same is
+ * defined in the validation context, it will be applied to this
+ * value after any normalization.
+ */
+ this.postprocessor = schema.postprocess || null;
+
+ /**
+ * @property {Array<string>} allowedContexts A list of allowed contexts
+ * to consider before generating the API.
+ * These are not parsed by the schema, but passed to `shouldInject`.
+ */
+ this.allowedContexts = schema.allowedContexts || [];
+ }
+
+ /**
+ * Preprocess the given value with the preprocessor declared in
+ * `preprocessor`.
+ *
+ * @param {*} value
+ * @param {Context} context
+ * @returns {*}
+ */
+ preprocess(value, context) {
+ if (this.preprocessor) {
+ return context.preprocessors[this.preprocessor](value, context);
+ }
+ return value;
+ }
+
+ /**
+ * Postprocess the given result with the postprocessor declared in
+ * `postprocessor`.
+ *
+ * @param {object} result
+ * @param {Context} context
+ * @returns {object}
+ */
+ postprocess(result, context) {
+ if (result.error || !this.postprocessor) {
+ return result;
+ }
+
+ let value = context.postprocessors[this.postprocessor](result.value, context);
+ return {value};
+ }
+
+ /**
+ * Logs a deprecation warning for this entry, based on the value of
+ * its `deprecated` property.
+ *
+ * @param {Context} context
+ * @param {value} [value]
+ */
+ logDeprecation(context, value = null) {
+ let message = "This property is deprecated";
+ if (typeof(this.deprecated) == "string") {
+ message = this.deprecated;
+ if (message.includes("${value}")) {
+ try {
+ value = JSON.stringify(value);
+ } catch (e) {
+ value = String(value);
+ }
+ message = message.replace(/\$\{value\}/g, () => value);
+ }
+ }
+
+ context.logError(context.makeError(message));
+ }
+
+ /**
+ * Checks whether the entry is deprecated and, if so, logs a
+ * deprecation message.
+ *
+ * @param {Context} context
+ * @param {value} [value]
+ */
+ checkDeprecated(context, value = null) {
+ if (this.deprecated) {
+ this.logDeprecation(context, value);
+ }
+ }
+
+ /**
+ * Injects JS values for the entry into the extension API
+ * namespace. The default implementation is to do nothing.
+ * `context` is used to call the actual implementation
+ * of a given function or event.
+ *
+ * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
+ * @param {string} name The method name, e.g. "get".
+ * @param {object} dest The object where `path`.`name` should be stored.
+ * @param {InjectionContext} context
+ */
+ inject(path, name, dest, context) {
+ }
+}
+
+// Corresponds either to a type declared in the "types" section of the
+// schema or else to any type object used throughout the schema.
+class Type extends Entry {
+ /**
+ * @property {Array<string>} EXTRA_PROPERTIES
+ * An array of extra properties which may be present for
+ * schemas of this type.
+ */
+ static get EXTRA_PROPERTIES() {
+ return ["description", "deprecated", "preprocess", "postprocess", "allowedContexts"];
+ }
+
+ /**
+ * Parses the given schema object and returns an instance of this
+ * class which corresponds to its properties.
+ *
+ * @param {object} schema
+ * A JSON schema object which corresponds to a definition of
+ * this type.
+ * @param {Array<string>} path
+ * The path to this schema object from the root schema,
+ * corresponding to the property names and array indices
+ * traversed during parsing in order to arrive at this schema
+ * object.
+ * @param {Array<string>} [extraProperties]
+ * An array of extra property names which are valid for this
+ * schema in the current context.
+ * @returns {Type}
+ * An instance of this type which corresponds to the given
+ * schema object.
+ * @static
+ */
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ return new this(schema);
+ }
+
+ /**
+ * Checks that all of the properties present in the given schema
+ * object are valid properties for this type, and throws if invalid.
+ *
+ * @param {object} schema
+ * A JSON schema object.
+ * @param {Array<string>} path
+ * The path to this schema object from the root schema,
+ * corresponding to the property names and array indices
+ * traversed during parsing in order to arrive at this schema
+ * object.
+ * @param {Array<string>} [extra]
+ * An array of extra property names which are valid for this
+ * schema in the current context.
+ * @throws {Error}
+ * An error describing the first invalid property found in the
+ * schema object.
+ */
+ static checkSchemaProperties(schema, path, extra = []) {
+ let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
+
+ for (let prop of Object.keys(schema)) {
+ if (!allowedSet.has(prop)) {
+ throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${schema.id || JSON.stringify(schema)}"`);
+ }
+ }
+ }
+
+ // Takes a value, checks that it has the correct type, and returns a
+ // "normalized" version of the value. The normalized version will
+ // include "nulls" in place of omitted optional properties. The
+ // result of this function is either {error: "Some type error"} or
+ // {value: <normalized-value>}.
+ normalize(value, context) {
+ return context.error("invalid type");
+ }
+
+ // Unlike normalize, this function does a shallow check to see if
+ // |baseType| (one of the possible getValueBaseType results) is
+ // valid for this type. It returns true or false. It's used to fill
+ // in optional arguments to functions before actually type checking
+
+ checkBaseType(baseType) {
+ return false;
+ }
+
+ // Helper method that simply relies on checkBaseType to implement
+ // normalize. Subclasses can choose to use it or not.
+ normalizeBase(type, value, context) {
+ if (this.checkBaseType(getValueBaseType(value))) {
+ this.checkDeprecated(context, value);
+ return {value: this.preprocess(value, context)};
+ }
+
+ let choice;
+ if (/^[aeiou]/.test(type)) {
+ choice = `be an ${type} value`;
+ } else {
+ choice = `be a ${type} value`;
+ }
+
+ return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`,
+ choice);
+ }
+}
+
+// Type that allows any value.
+class AnyType extends Type {
+ normalize(value, context) {
+ this.checkDeprecated(context, value);
+ return this.postprocess({value}, context);
+ }
+
+ checkBaseType(baseType) {
+ return true;
+ }
+}
+
+// An untagged union type.
+class ChoiceType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["choices", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ let choices = schema.choices.map(t => Schemas.parseSchema(t, path));
+ return new this(schema, choices);
+ }
+
+ constructor(schema, choices) {
+ super(schema);
+ this.choices = choices;
+ }
+
+ extend(type) {
+ this.choices.push(...type.choices);
+
+ return this;
+ }
+
+ normalize(value, context) {
+ this.checkDeprecated(context, value);
+
+ let error;
+ let {choices, result} = context.withChoices(() => {
+ for (let choice of this.choices) {
+ let r = choice.normalize(value, context);
+ if (!r.error) {
+ return r;
+ }
+
+ error = r;
+ }
+ });
+
+ if (result) {
+ return result;
+ }
+ if (choices.length <= 1) {
+ return error;
+ }
+
+ let n = choices.length - 1;
+ choices[n] = `or ${choices[n]}`;
+
+ let message = `Value must either: ${choices.join(", ")}`;
+
+ return context.error(message, null);
+ }
+
+ checkBaseType(baseType) {
+ return this.choices.some(t => t.checkBaseType(baseType));
+ }
+}
+
+// This is a reference to another type--essentially a typedef.
+class RefType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["$ref", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ let ref = schema.$ref;
+ let ns = path[0];
+ if (ref.includes(".")) {
+ [ns, ref] = ref.split(".");
+ }
+ return new this(schema, ns, ref);
+ }
+
+ // For a reference to a type named T declared in namespace NS,
+ // namespaceName will be NS and reference will be T.
+ constructor(schema, namespaceName, reference) {
+ super(schema);
+ this.namespaceName = namespaceName;
+ this.reference = reference;
+ }
+
+ get targetType() {
+ let ns = Schemas.namespaces.get(this.namespaceName);
+ let type = ns.get(this.reference);
+ if (!type) {
+ throw new Error(`Internal error: Type ${this.reference} not found`);
+ }
+ return type;
+ }
+
+ normalize(value, context) {
+ this.checkDeprecated(context, value);
+ return this.targetType.normalize(value, context);
+ }
+
+ checkBaseType(baseType) {
+ return this.targetType.checkBaseType(baseType);
+ }
+}
+
+class StringType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["enum", "minLength", "maxLength", "pattern", "format",
+ ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ let enumeration = schema.enum || null;
+ if (enumeration) {
+ // The "enum" property is either a list of strings that are
+ // valid values or else a list of {name, description} objects,
+ // where the .name values are the valid values.
+ enumeration = enumeration.map(e => {
+ if (typeof(e) == "object") {
+ return e.name;
+ }
+ return e;
+ });
+ }
+
+ let pattern = null;
+ if (schema.pattern) {
+ try {
+ pattern = parsePattern(schema.pattern);
+ } catch (e) {
+ throw new Error(`Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`);
+ }
+ }
+
+ let format = null;
+ if (schema.format) {
+ if (!(schema.format in FORMATS)) {
+ throw new Error(`Internal error: Invalid string format ${schema.format}`);
+ }
+ format = FORMATS[schema.format];
+ }
+ return new this(schema, enumeration,
+ schema.minLength || 0,
+ schema.maxLength || Infinity,
+ pattern,
+ format);
+ }
+
+ constructor(schema, enumeration, minLength, maxLength, pattern, format) {
+ super(schema);
+ this.enumeration = enumeration;
+ this.minLength = minLength;
+ this.maxLength = maxLength;
+ this.pattern = pattern;
+ this.format = format;
+ }
+
+ normalize(value, context) {
+ let r = this.normalizeBase("string", value, context);
+ if (r.error) {
+ return r;
+ }
+ value = r.value;
+
+ if (this.enumeration) {
+ if (this.enumeration.includes(value)) {
+ return this.postprocess({value}, context);
+ }
+
+ let choices = this.enumeration.map(JSON.stringify).join(", ");
+
+ return context.error(`Invalid enumeration value ${JSON.stringify(value)}`,
+ `be one of [${choices}]`);
+ }
+
+ if (value.length < this.minLength) {
+ return context.error(`String ${JSON.stringify(value)} is too short (must be ${this.minLength})`,
+ `be longer than ${this.minLength}`);
+ }
+ if (value.length > this.maxLength) {
+ return context.error(`String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`,
+ `be shorter than ${this.maxLength}`);
+ }
+
+ if (this.pattern && !this.pattern.test(value)) {
+ return context.error(`String ${JSON.stringify(value)} must match ${this.pattern}`,
+ `match the pattern ${this.pattern.toSource()}`);
+ }
+
+ if (this.format) {
+ try {
+ r.value = this.format(r.value, context);
+ } catch (e) {
+ return context.error(String(e), `match the format "${this.format.name}"`);
+ }
+ }
+
+ return r;
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "string";
+ }
+
+ inject(path, name, dest, context) {
+ if (this.enumeration) {
+ exportLazyGetter(dest, name, () => {
+ let obj = Cu.createObjectIn(dest);
+ for (let e of this.enumeration) {
+ obj[e.toUpperCase()] = e;
+ }
+ return obj;
+ });
+ }
+ }
+}
+
+let SubModuleType;
+class ObjectType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ if ("functions" in schema) {
+ return SubModuleType.parseSchema(schema, path, extraProperties);
+ }
+
+ if (!("$extend" in schema)) {
+ // Only allow extending "properties" and "patternProperties".
+ extraProperties = ["additionalProperties", "isInstanceOf", ...extraProperties];
+ }
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ let parseProperty = (schema, extraProps = []) => {
+ return {
+ type: Schemas.parseSchema(schema, path,
+ ["unsupported", "onError", "permissions", ...extraProps]),
+ optional: schema.optional || false,
+ unsupported: schema.unsupported || false,
+ onError: schema.onError || null,
+ };
+ };
+
+ // Parse explicit "properties" object.
+ let properties = Object.create(null);
+ for (let propName of Object.keys(schema.properties || {})) {
+ properties[propName] = parseProperty(schema.properties[propName], ["optional"]);
+ }
+
+ // Parse regexp properties from "patternProperties" object.
+ let patternProperties = [];
+ for (let propName of Object.keys(schema.patternProperties || {})) {
+ let pattern;
+ try {
+ pattern = parsePattern(propName);
+ } catch (e) {
+ throw new Error(`Internal error: Invalid property pattern ${JSON.stringify(propName)}`);
+ }
+
+ patternProperties.push({
+ pattern,
+ type: parseProperty(schema.patternProperties[propName]),
+ });
+ }
+
+ // Parse "additionalProperties" schema.
+ let additionalProperties = null;
+ if (schema.additionalProperties) {
+ let type = schema.additionalProperties;
+ if (type === true) {
+ type = {"type": "any"};
+ }
+
+ additionalProperties = Schemas.parseSchema(type, path);
+ }
+
+ return new this(schema, properties, additionalProperties, patternProperties, schema.isInstanceOf || null);
+ }
+
+ constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) {
+ super(schema);
+ this.properties = properties;
+ this.additionalProperties = additionalProperties;
+ this.patternProperties = patternProperties;
+ this.isInstanceOf = isInstanceOf;
+ }
+
+ extend(type) {
+ for (let key of Object.keys(type.properties)) {
+ if (key in this.properties) {
+ throw new Error(`InternalError: Attempt to extend an object with conflicting property "${key}"`);
+ }
+ this.properties[key] = type.properties[key];
+ }
+
+ this.patternProperties.push(...type.patternProperties);
+
+ return this;
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "object";
+ }
+
+ /**
+ * Extracts the enumerable properties of the given object, including
+ * function properties which would normally be omitted by X-ray
+ * wrappers.
+ *
+ * @param {object} value
+ * @param {Context} context
+ * The current parse context.
+ * @returns {object}
+ * An object with an `error` or `value` property.
+ */
+ extractProperties(value, context) {
+ // |value| should be a JS Xray wrapping an object in the
+ // extension compartment. This works well except when we need to
+ // access callable properties on |value| since JS Xrays don't
+ // support those. To work around the problem, we verify that
+ // |value| is a plain JS object (i.e., not anything scary like a
+ // Proxy). Then we copy the properties out of it into a normal
+ // object using a waiver wrapper.
+
+ let klass = Cu.getClassName(value, true);
+ if (klass != "Object") {
+ throw context.error(`Expected a plain JavaScript object, got a ${klass}`,
+ `be a plain JavaScript object`);
+ }
+
+ let properties = Object.create(null);
+
+ let waived = Cu.waiveXrays(value);
+ for (let prop of Object.getOwnPropertyNames(waived)) {
+ let desc = Object.getOwnPropertyDescriptor(waived, prop);
+ if (desc.get || desc.set) {
+ throw context.error("Objects cannot have getters or setters on properties",
+ "contain no getter or setter properties");
+ }
+ // Chrome ignores non-enumerable properties.
+ if (desc.enumerable) {
+ properties[prop] = Cu.unwaiveXrays(desc.value);
+ }
+ }
+
+ return properties;
+ }
+
+ checkProperty(context, prop, propType, result, properties, remainingProps) {
+ let {type, optional, unsupported, onError} = propType;
+ let error = null;
+
+ if (unsupported) {
+ if (prop in properties) {
+ error = context.error(`Property "${prop}" is unsupported by Firefox`,
+ `not contain an unsupported "${prop}" property`);
+ }
+ } else if (prop in properties) {
+ if (optional && (properties[prop] === null || properties[prop] === undefined)) {
+ result[prop] = null;
+ } else {
+ let r = context.withPath(prop, () => type.normalize(properties[prop], context));
+ if (r.error) {
+ error = r;
+ } else {
+ result[prop] = r.value;
+ properties[prop] = r.value;
+ }
+ }
+ remainingProps.delete(prop);
+ } else if (!optional) {
+ error = context.error(`Property "${prop}" is required`,
+ `contain the required "${prop}" property`);
+ } else if (optional !== "omit-key-if-missing") {
+ result[prop] = null;
+ }
+
+ if (error) {
+ if (onError == "warn") {
+ context.logError(error.error);
+ } else if (onError != "ignore") {
+ throw error;
+ }
+
+ result[prop] = null;
+ }
+ }
+
+ normalize(value, context) {
+ try {
+ let v = this.normalizeBase("object", value, context);
+ if (v.error) {
+ return v;
+ }
+ value = v.value;
+
+ if (this.isInstanceOf) {
+ if (Object.keys(this.properties).length ||
+ this.patternProperties.length ||
+ !(this.additionalProperties instanceof AnyType)) {
+ throw new Error("InternalError: isInstanceOf can only be used with objects that are otherwise unrestricted");
+ }
+
+ if (!instanceOf(value, this.isInstanceOf)) {
+ return context.error(`Object must be an instance of ${this.isInstanceOf}`,
+ `be an instance of ${this.isInstanceOf}`);
+ }
+
+ // This is kind of a hack, but we can't normalize things that
+ // aren't JSON, so we just return them.
+ return this.postprocess({value}, context);
+ }
+
+ let properties = this.extractProperties(value, context);
+ let remainingProps = new Set(Object.keys(properties));
+
+ let result = {};
+ for (let prop of Object.keys(this.properties)) {
+ this.checkProperty(context, prop, this.properties[prop], result,
+ properties, remainingProps);
+ }
+
+ for (let prop of Object.keys(properties)) {
+ for (let {pattern, type} of this.patternProperties) {
+ if (pattern.test(prop)) {
+ this.checkProperty(context, prop, type, result,
+ properties, remainingProps);
+ }
+ }
+ }
+
+ if (this.additionalProperties) {
+ for (let prop of remainingProps) {
+ let type = this.additionalProperties;
+ let r = context.withPath(prop, () => type.normalize(properties[prop], context));
+ if (r.error) {
+ return r;
+ }
+ result[prop] = r.value;
+ }
+ } else if (remainingProps.size == 1) {
+ return context.error(`Unexpected property "${[...remainingProps]}"`,
+ `not contain an unexpected "${[...remainingProps]}" property`);
+ } else if (remainingProps.size) {
+ let props = [...remainingProps].sort().join(", ");
+ return context.error(`Unexpected properties: ${props}`,
+ `not contain the unexpected properties [${props}]`);
+ }
+
+ return this.postprocess({value: result}, context);
+ } catch (e) {
+ if (e.error) {
+ return e;
+ }
+ throw e;
+ }
+ }
+}
+
+// This type is just a placeholder to be referred to by
+// SubModuleProperty. No value is ever expected to have this type.
+SubModuleType = class SubModuleType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ // The path we pass in here is only used for error messages.
+ path = [...path, schema.id];
+ let functions = schema.functions.map(fun => Schemas.parseFunction(path, fun));
+
+ return new this(functions);
+ }
+
+ constructor(functions) {
+ super();
+ this.functions = functions;
+ }
+};
+
+class NumberType extends Type {
+ normalize(value, context) {
+ let r = this.normalizeBase("number", value, context);
+ if (r.error) {
+ return r;
+ }
+
+ if (isNaN(r.value) || !Number.isFinite(r.value)) {
+ return context.error("NaN and infinity are not valid",
+ "be a finite number");
+ }
+
+ return r;
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "number" || baseType == "integer";
+ }
+}
+
+class IntegerType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ return new this(schema, schema.minimum || -Infinity, schema.maximum || Infinity);
+ }
+
+ constructor(schema, minimum, maximum) {
+ super(schema);
+ this.minimum = minimum;
+ this.maximum = maximum;
+ }
+
+ normalize(value, context) {
+ let r = this.normalizeBase("integer", value, context);
+ if (r.error) {
+ return r;
+ }
+ value = r.value;
+
+ // Ensure it's between -2**31 and 2**31-1
+ if (!Number.isSafeInteger(value)) {
+ return context.error("Integer is out of range",
+ "be a valid 32 bit signed integer");
+ }
+
+ if (value < this.minimum) {
+ return context.error(`Integer ${value} is too small (must be at least ${this.minimum})`,
+ `be at least ${this.minimum}`);
+ }
+ if (value > this.maximum) {
+ return context.error(`Integer ${value} is too big (must be at most ${this.maximum})`,
+ `be no greater than ${this.maximum}`);
+ }
+
+ return this.postprocess(r, context);
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "integer";
+ }
+}
+
+class BooleanType extends Type {
+ normalize(value, context) {
+ return this.normalizeBase("boolean", value, context);
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "boolean";
+ }
+}
+
+class ArrayType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ let items = Schemas.parseSchema(schema.items, path);
+
+ return new this(schema, items, schema.minItems || 0, schema.maxItems || Infinity);
+ }
+
+ constructor(schema, itemType, minItems, maxItems) {
+ super(schema);
+ this.itemType = itemType;
+ this.minItems = minItems;
+ this.maxItems = maxItems;
+ }
+
+ normalize(value, context) {
+ let v = this.normalizeBase("array", value, context);
+ if (v.error) {
+ return v;
+ }
+ value = v.value;
+
+ let result = [];
+ for (let [i, element] of value.entries()) {
+ element = context.withPath(String(i), () => this.itemType.normalize(element, context));
+ if (element.error) {
+ return element;
+ }
+ result.push(element.value);
+ }
+
+ if (result.length < this.minItems) {
+ return context.error(`Array requires at least ${this.minItems} items; you have ${result.length}`,
+ `have at least ${this.minItems} items`);
+ }
+
+ if (result.length > this.maxItems) {
+ return context.error(`Array requires at most ${this.maxItems} items; you have ${result.length}`,
+ `have at most ${this.maxItems} items`);
+ }
+
+ return this.postprocess({value: result}, context);
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "array";
+ }
+}
+
+class FunctionType extends Type {
+ static get EXTRA_PROPERTIES() {
+ return ["parameters", "async", "returns", ...super.EXTRA_PROPERTIES];
+ }
+
+ static parseSchema(schema, path, extraProperties = []) {
+ this.checkSchemaProperties(schema, path, extraProperties);
+
+ let isAsync = !!schema.async;
+ let isExpectingCallback = typeof schema.async === "string";
+ let parameters = null;
+ if ("parameters" in schema) {
+ parameters = [];
+ for (let param of schema.parameters) {
+ // Callbacks default to optional for now, because of promise
+ // handling.
+ let isCallback = isAsync && param.name == schema.async;
+ if (isCallback) {
+ isExpectingCallback = false;
+ }
+
+ parameters.push({
+ type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
+ name: param.name,
+ optional: param.optional == null ? isCallback : param.optional,
+ default: param.default == undefined ? null : param.default,
+ });
+ }
+ }
+ if (isExpectingCallback) {
+ throw new Error(`Internal error: Expected a callback parameter with name ${schema.async}`);
+ }
+
+ let hasAsyncCallback = false;
+ if (isAsync) {
+ hasAsyncCallback = (parameters &&
+ parameters.length &&
+ parameters[parameters.length - 1].name == schema.async);
+
+ if (schema.returns) {
+ throw new Error("Internal error: Async functions must not have return values.");
+ }
+ if (schema.allowAmbiguousOptionalArguments && !hasAsyncCallback) {
+ throw new Error("Internal error: Async functions with ambiguous arguments must declare the callback as the last parameter");
+ }
+ }
+
+ return new this(schema, parameters, isAsync, hasAsyncCallback);
+ }
+
+ constructor(schema, parameters, isAsync, hasAsyncCallback) {
+ super(schema);
+ this.parameters = parameters;
+ this.isAsync = isAsync;
+ this.hasAsyncCallback = hasAsyncCallback;
+ }
+
+ normalize(value, context) {
+ return this.normalizeBase("function", value, context);
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "function";
+ }
+}
+
+// Represents a "property" defined in a schema namespace with a
+// particular value. Essentially this is a constant.
+class ValueProperty extends Entry {
+ constructor(schema, name, value) {
+ super(schema);
+ this.name = name;
+ this.value = value;
+ }
+
+ inject(path, name, dest, context) {
+ dest[name] = this.value;
+ }
+}
+
+// Represents a "property" defined in a schema namespace that is not a
+// constant.
+class TypeProperty extends Entry {
+ constructor(schema, namespaceName, name, type, writable) {
+ super(schema);
+ this.namespaceName = namespaceName;
+ this.name = name;
+ this.type = type;
+ this.writable = writable;
+ }
+
+ throwError(context, msg) {
+ throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
+ }
+
+ inject(path, name, dest, context) {
+ if (this.unsupported) {
+ return;
+ }
+
+ let apiImpl = context.getImplementation(path.join("."), name);
+
+ let getStub = () => {
+ this.checkDeprecated(context);
+ return apiImpl.getProperty();
+ };
+
+ let desc = {
+ configurable: false,
+ enumerable: true,
+
+ get: Cu.exportFunction(getStub, dest),
+ };
+
+ if (this.writable) {
+ let setStub = (value) => {
+ let normalized = this.type.normalize(value, context);
+ if (normalized.error) {
+ this.throwError(context, normalized.error);
+ }
+
+ apiImpl.setProperty(normalized.value);
+ };
+
+ desc.set = Cu.exportFunction(setStub, dest);
+ }
+
+ Object.defineProperty(dest, name, desc);
+ }
+}
+
+class SubModuleProperty extends Entry {
+ // A SubModuleProperty represents a tree of objects and properties
+ // to expose to an extension. Currently we support only a limited
+ // form of sub-module properties, where "$ref" points to a
+ // SubModuleType containing a list of functions and "properties" is
+ // a list of additional simple properties.
+ //
+ // name: Name of the property stuff is being added to.
+ // namespaceName: Namespace in which the property lives.
+ // reference: Name of the type defining the functions to add to the property.
+ // properties: Additional properties to add to the module (unsupported).
+ constructor(schema, name, namespaceName, reference, properties) {
+ super(schema);
+ this.name = name;
+ this.namespaceName = namespaceName;
+ this.reference = reference;
+ this.properties = properties;
+ }
+
+ inject(path, name, dest, context) {
+ exportLazyGetter(dest, name, () => {
+ let obj = Cu.createObjectIn(dest);
+
+ let ns = Schemas.namespaces.get(this.namespaceName);
+ let type = ns.get(this.reference);
+ if (!type && this.reference.includes(".")) {
+ let [namespaceName, ref] = this.reference.split(".");
+ ns = Schemas.namespaces.get(namespaceName);
+ type = ns.get(ref);
+ }
+ if (!type || !(type instanceof SubModuleType)) {
+ throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
+ }
+
+ let functions = type.functions;
+ for (let fun of functions) {
+ let subpath = path.concat(name);
+ let namespace = subpath.join(".");
+ let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
+ if (context.shouldInject(namespace, fun.name, allowedContexts)) {
+ fun.inject(subpath, fun.name, obj, context);
+ }
+ }
+
+ // TODO: Inject this.properties.
+
+ return obj;
+ });
+ }
+}
+
+// This class is a base class for FunctionEntrys and Events. It takes
+// care of validating parameter lists (i.e., handling of optional
+// parameters and parameter type checking).
+class CallEntry extends Entry {
+ constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
+ super(schema);
+ this.path = path;
+ this.name = name;
+ this.parameters = parameters;
+ this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
+ }
+
+ throwError(context, msg) {
+ throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
+ }
+
+ checkParameters(args, context) {
+ let fixedArgs = [];
+
+ // First we create a new array, fixedArgs, that is the same as
+ // |args| but with default values in place of omitted optional parameters.
+ let check = (parameterIndex, argIndex) => {
+ if (parameterIndex == this.parameters.length) {
+ if (argIndex == args.length) {
+ return true;
+ }
+ return false;
+ }
+
+ let parameter = this.parameters[parameterIndex];
+ if (parameter.optional) {
+ // Try skipping it.
+ fixedArgs[parameterIndex] = parameter.default;
+ if (check(parameterIndex + 1, argIndex)) {
+ return true;
+ }
+ }
+
+ if (argIndex == args.length) {
+ return false;
+ }
+
+ let arg = args[argIndex];
+ if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
+ // For Chrome compatibility, use the default value if null or undefined
+ // is explicitly passed but is not a valid argument in this position.
+ if (parameter.optional && (arg === null || arg === undefined)) {
+ fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, global);
+ } else {
+ return false;
+ }
+ } else {
+ fixedArgs[parameterIndex] = arg;
+ }
+
+ return check(parameterIndex + 1, argIndex + 1);
+ };
+
+ if (this.allowAmbiguousOptionalArguments) {
+ // When this option is set, it's up to the implementation to
+ // parse arguments.
+ // The last argument for asynchronous methods is either a function or null.
+ // This is specifically done for runtime.sendMessage.
+ if (this.hasAsyncCallback && typeof(args[args.length - 1]) != "function") {
+ args.push(null);
+ }
+ return args;
+ }
+ let success = check(0, 0);
+ if (!success) {
+ this.throwError(context, "Incorrect argument types");
+ }
+
+ // Now we normalize (and fully type check) all non-omitted arguments.
+ fixedArgs = fixedArgs.map((arg, parameterIndex) => {
+ if (arg === null) {
+ return null;
+ }
+ let parameter = this.parameters[parameterIndex];
+ let r = parameter.type.normalize(arg, context);
+ if (r.error) {
+ this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`);
+ }
+ return r.value;
+ });
+
+ return fixedArgs;
+ }
+}
+
+// Represents a "function" defined in a schema namespace.
+class FunctionEntry extends CallEntry {
+ constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) {
+ super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
+ this.unsupported = unsupported;
+ this.returns = returns;
+ this.permissions = permissions;
+
+ this.isAsync = type.isAsync;
+ this.hasAsyncCallback = type.hasAsyncCallback;
+ }
+
+ inject(path, name, dest, context) {
+ if (this.unsupported) {
+ return;
+ }
+
+ if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
+ return;
+ }
+
+ exportLazyGetter(dest, name, () => {
+ let apiImpl = context.getImplementation(path.join("."), name);
+
+ let stub;
+ if (this.isAsync) {
+ stub = (...args) => {
+ this.checkDeprecated(context);
+ let actuals = this.checkParameters(args, context);
+ let callback = null;
+ if (this.hasAsyncCallback) {
+ callback = actuals.pop();
+ }
+ if (callback === null && context.isChromeCompat) {
+ // We pass an empty stub function as a default callback for
+ // the `chrome` API, so promise objects are not returned,
+ // and lastError values are reported immediately.
+ callback = () => {};
+ }
+ return apiImpl.callAsyncFunction(actuals, callback);
+ };
+ } else if (!this.returns) {
+ stub = (...args) => {
+ this.checkDeprecated(context);
+ let actuals = this.checkParameters(args, context);
+ return apiImpl.callFunctionNoReturn(actuals);
+ };
+ } else {
+ stub = (...args) => {
+ this.checkDeprecated(context);
+ let actuals = this.checkParameters(args, context);
+ return apiImpl.callFunction(actuals);
+ };
+ }
+ return Cu.exportFunction(stub, dest);
+ });
+ }
+}
+
+// Represents an "event" defined in a schema namespace.
+class Event extends CallEntry {
+ constructor(schema, path, name, type, extraParameters, unsupported, permissions) {
+ super(schema, path, name, extraParameters);
+ this.type = type;
+ this.unsupported = unsupported;
+ this.permissions = permissions;
+ }
+
+ checkListener(listener, context) {
+ let r = this.type.normalize(listener, context);
+ if (r.error) {
+ this.throwError(context, "Invalid listener");
+ }
+ return r.value;
+ }
+
+ inject(path, name, dest, context) {
+ if (this.unsupported) {
+ return;
+ }
+
+ if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
+ return;
+ }
+
+ exportLazyGetter(dest, name, () => {
+ let apiImpl = context.getImplementation(path.join("."), name);
+
+ let addStub = (listener, ...args) => {
+ listener = this.checkListener(listener, context);
+ let actuals = this.checkParameters(args, context);
+ apiImpl.addListener(listener, actuals);
+ };
+
+ let removeStub = (listener) => {
+ listener = this.checkListener(listener, context);
+ apiImpl.removeListener(listener);
+ };
+
+ let hasStub = (listener) => {
+ listener = this.checkListener(listener, context);
+ return apiImpl.hasListener(listener);
+ };
+
+ let obj = Cu.createObjectIn(dest);
+
+ Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
+ Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
+ Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
+
+ return obj;
+ });
+ }
+}
+
+const TYPES = Object.freeze(Object.assign(Object.create(null), {
+ any: AnyType,
+ array: ArrayType,
+ boolean: BooleanType,
+ function: FunctionType,
+ integer: IntegerType,
+ number: NumberType,
+ object: ObjectType,
+ string: StringType,
+}));
+
+this.Schemas = {
+ initialized: false,
+
+ // Maps a schema URL to the JSON contained in that schema file. This
+ // is useful for sending the JSON across processes.
+ schemaJSON: new Map(),
+
+ // Map[<schema-name> -> Map[<symbol-name> -> Entry]]
+ // This keeps track of all the schemas that have been loaded so far.
+ namespaces: new Map(),
+
+ register(namespaceName, symbol, value) {
+ let ns = this.namespaces.get(namespaceName);
+ if (!ns) {
+ ns = new Map();
+ ns.name = namespaceName;
+ ns.permissions = null;
+ ns.allowedContexts = [];
+ ns.defaultContexts = [];
+ this.namespaces.set(namespaceName, ns);
+ }
+ ns.set(symbol, value);
+ },
+
+ parseSchema(schema, path, extraProperties = []) {
+ let allowedProperties = new Set(extraProperties);
+
+ if ("choices" in schema) {
+ return ChoiceType.parseSchema(schema, path, allowedProperties);
+ } else if ("$ref" in schema) {
+ return RefType.parseSchema(schema, path, allowedProperties);
+ }
+
+ if (!("type" in schema)) {
+ throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
+ }
+
+ allowedProperties.add("type");
+
+ let type = TYPES[schema.type];
+ if (!type) {
+ throw new Error(`Unexpected type ${schema.type}`);
+ }
+ return type.parseSchema(schema, path, allowedProperties);
+ },
+
+ parseFunction(path, fun) {
+ let f = new FunctionEntry(fun, path, fun.name,
+ this.parseSchema(fun, path,
+ ["name", "unsupported", "returns",
+ "permissions",
+ "allowAmbiguousOptionalArguments"]),
+ fun.unsupported || false,
+ fun.allowAmbiguousOptionalArguments || false,
+ fun.returns || null,
+ fun.permissions || null);
+ return f;
+ },
+
+ loadType(namespaceName, type) {
+ if ("$extend" in type) {
+ this.extendType(namespaceName, type);
+ } else {
+ this.register(namespaceName, type.id, this.parseSchema(type, [namespaceName], ["id"]));
+ }
+ },
+
+ extendType(namespaceName, type) {
+ let ns = Schemas.namespaces.get(namespaceName);
+ let targetType = ns && ns.get(type.$extend);
+
+ // Only allow extending object and choices types for now.
+ if (targetType instanceof ObjectType) {
+ type.type = "object";
+ } else if (!targetType) {
+ throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`);
+ } else if (!(targetType instanceof ChoiceType)) {
+ throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`);
+ }
+
+ let parsed = this.parseSchema(type, [namespaceName], ["$extend"]);
+ if (parsed.constructor !== targetType.constructor) {
+ throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
+ }
+
+ targetType.extend(parsed);
+ },
+
+ loadProperty(namespaceName, name, prop) {
+ if ("$ref" in prop) {
+ if (!prop.unsupported) {
+ this.register(namespaceName, name, new SubModuleProperty(prop, name, namespaceName, prop.$ref,
+ prop.properties || {}));
+ }
+ } else if ("value" in prop) {
+ this.register(namespaceName, name, new ValueProperty(prop, name, prop.value));
+ } else {
+ // We ignore the "optional" attribute on properties since we
+ // don't inject anything here anyway.
+ let type = this.parseSchema(prop, [namespaceName], ["optional", "writable"]);
+ this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable || false));
+ }
+ },
+
+ loadFunction(namespaceName, fun) {
+ let f = this.parseFunction([namespaceName], fun);
+ this.register(namespaceName, fun.name, f);
+ },
+
+ loadEvent(namespaceName, event) {
+ let extras = event.extraParameters || [];
+ extras = extras.map(param => {
+ return {
+ type: this.parseSchema(param, [namespaceName], ["name", "optional", "default"]),
+ name: param.name,
+ optional: param.optional || false,
+ default: param.default == undefined ? null : param.default,
+ };
+ });
+
+ // We ignore these properties for now.
+ /* eslint-disable no-unused-vars */
+ let returns = event.returns;
+ let filters = event.filters;
+ /* eslint-enable no-unused-vars */
+
+ let type = this.parseSchema(event, [namespaceName],
+ ["name", "unsupported", "permissions",
+ "extraParameters", "returns", "filters"]);
+
+ let e = new Event(event, [namespaceName], event.name, type, extras,
+ event.unsupported || false,
+ event.permissions || null);
+ this.register(namespaceName, event.name, e);
+ },
+
+ init() {
+ if (this.initialized) {
+ return;
+ }
+ this.initialized = true;
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ let data = Services.cpmm.initialProcessData;
+ let schemas = data["Extension:Schemas"];
+ if (schemas) {
+ this.schemaJSON = schemas;
+ }
+ Services.cpmm.addMessageListener("Schema:Add", this);
+ }
+
+ this.flushSchemas();
+ },
+
+ receiveMessage(msg) {
+ switch (msg.name) {
+ case "Schema:Add":
+ this.schemaJSON.set(msg.data.url, msg.data.schema);
+ this.flushSchemas();
+ break;
+
+ case "Schema:Delete":
+ this.schemaJSON.delete(msg.data.url);
+ this.flushSchemas();
+ break;
+ }
+ },
+
+ flushSchemas() {
+ XPCOMUtils.defineLazyGetter(this, "namespaces",
+ () => this.parseSchemas());
+ },
+
+ parseSchemas() {
+ Object.defineProperty(this, "namespaces", {
+ enumerable: true,
+ configurable: true,
+ value: new Map(),
+ });
+
+ for (let json of this.schemaJSON.values()) {
+ try {
+ this.loadSchema(json);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ return this.namespaces;
+ },
+
+ loadSchema(json) {
+ for (let namespace of json) {
+ let name = namespace.namespace;
+
+ let types = namespace.types || [];
+ for (let type of types) {
+ this.loadType(name, type);
+ }
+
+ let properties = namespace.properties || {};
+ for (let propertyName of Object.keys(properties)) {
+ this.loadProperty(name, propertyName, properties[propertyName]);
+ }
+
+ let functions = namespace.functions || [];
+ for (let fun of functions) {
+ this.loadFunction(name, fun);
+ }
+
+ let events = namespace.events || [];
+ for (let event of events) {
+ this.loadEvent(name, event);
+ }
+
+ let ns = this.namespaces.get(name);
+ ns.permissions = namespace.permissions || null;
+ ns.allowedContexts = namespace.allowedContexts || [];
+ ns.defaultContexts = namespace.defaultContexts || [];
+ }
+ },
+
+ load(url) {
+ if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
+ return readJSON(url).then(json => {
+ this.schemaJSON.set(url, json);
+
+ let data = Services.ppmm.initialProcessData;
+ data["Extension:Schemas"] = this.schemaJSON;
+
+ Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json});
+
+ this.flushSchemas();
+ });
+ }
+ },
+
+ unload(url) {
+ this.schemaJSON.delete(url);
+
+ let data = Services.ppmm.initialProcessData;
+ data["Extension:Schemas"] = this.schemaJSON;
+
+ Services.ppmm.broadcastAsyncMessage("Schema:Delete", {url});
+
+ this.flushSchemas();
+ },
+
+ /**
+ * Checks whether a given object has the necessary permissions to
+ * expose the given namespace.
+ *
+ * @param {string} namespace
+ * The top-level namespace to check permissions for.
+ * @param {object} wrapperFuncs
+ * Wrapper functions for the given context.
+ * @param {function} wrapperFuncs.hasPermission
+ * A function which, when given a string argument, returns true
+ * if the context has the given permission.
+ * @returns {boolean}
+ * True if the context has permission for the given namespace.
+ */
+ checkPermissions(namespace, wrapperFuncs) {
+ let ns = this.namespaces.get(namespace);
+ if (ns && ns.permissions) {
+ return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
+ }
+ return true;
+ },
+
+ exportLazyGetter,
+
+ /**
+ * Inject registered extension APIs into `dest`.
+ *
+ * @param {object} dest The root namespace for the APIs.
+ * This object is usually exposed to extensions as "chrome" or "browser".
+ * @param {object} wrapperFuncs An implementation of the InjectionContext
+ * interface, which runs the actual functionality of the generated API.
+ */
+ inject(dest, wrapperFuncs) {
+ let context = new InjectionContext(wrapperFuncs);
+
+ let createNamespace = ns => {
+ let obj = Cu.createObjectIn(dest);
+
+ for (let [name, entry] of ns) {
+ let allowedContexts = entry.allowedContexts;
+ if (!allowedContexts.length) {
+ allowedContexts = ns.defaultContexts;
+ }
+
+ if (context.shouldInject(ns.name, name, allowedContexts)) {
+ entry.inject([ns.name], name, obj, context);
+ }
+ }
+
+ // Remove the namespace object if it is empty
+ if (Object.keys(obj).length) {
+ return obj;
+ }
+ };
+
+ let createNestedNamespaces = (parent, namespaces) => {
+ for (let [prop, namespace] of namespaces) {
+ if (namespace instanceof DeepMap) {
+ exportLazyGetter(parent, prop, () => {
+ let obj = Cu.createObjectIn(parent);
+ createNestedNamespaces(obj, namespace);
+ return obj;
+ });
+ } else {
+ exportLazyGetter(parent, prop,
+ () => createNamespace(namespace));
+ }
+ }
+ };
+
+ let nestedNamespaces = new DeepMap();
+ for (let ns of this.namespaces.values()) {
+ if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
+ continue;
+ }
+
+ if (!wrapperFuncs.shouldInject(ns.name, null, ns.allowedContexts)) {
+ continue;
+ }
+
+ if (ns.name.includes(".")) {
+ let path = ns.name.split(".");
+ let leafName = path.pop();
+
+ let parent = nestedNamespaces.getPath(...path);
+
+ parent.set(leafName, ns);
+ } else {
+ exportLazyGetter(dest, ns.name,
+ () => createNamespace(ns));
+ }
+ }
+
+ createNestedNamespaces(dest, nestedNamespaces);
+ },
+
+ /**
+ * Normalize `obj` according to the loaded schema for `typeName`.
+ *
+ * @param {object} obj The object to normalize against the schema.
+ * @param {string} typeName The name in the format namespace.propertyname
+ * @param {object} context An implementation of Context. Any validation errors
+ * are reported to the given context.
+ * @returns {object} The normalized object.
+ */
+ normalize(obj, typeName, context) {
+ let [namespaceName, prop] = typeName.split(".");
+ let ns = this.namespaces.get(namespaceName);
+ let type = ns.get(prop);
+
+ return type.normalize(obj, new Context(context));
+ },
+};
diff --git a/toolkit/components/extensions/ext-alarms.js b/toolkit/components/extensions/ext-alarms.js
new file mode 100644
index 0000000000..2171e7dba6
--- /dev/null
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -0,0 +1,155 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ EventManager,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Map[name -> Alarm]]
+var alarmsMap = new WeakMap();
+
+// WeakMap[Extension -> Set[callback]]
+var alarmCallbacksMap = new WeakMap();
+
+// Manages an alarm created by the extension (alarms API).
+function Alarm(extension, name, alarmInfo) {
+ this.extension = extension;
+ this.name = name;
+ this.when = alarmInfo.when;
+ this.delayInMinutes = alarmInfo.delayInMinutes;
+ this.periodInMinutes = alarmInfo.periodInMinutes;
+ this.canceled = false;
+
+ let delay, scheduledTime;
+ if (this.when) {
+ scheduledTime = this.when;
+ delay = this.when - Date.now();
+ } else {
+ if (!this.delayInMinutes) {
+ this.delayInMinutes = this.periodInMinutes;
+ }
+ delay = this.delayInMinutes * 60 * 1000;
+ scheduledTime = Date.now() + delay;
+ }
+
+ this.scheduledTime = scheduledTime;
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ this.timer = timer;
+}
+
+Alarm.prototype = {
+ clear() {
+ this.timer.cancel();
+ alarmsMap.get(this.extension).delete(this.name);
+ this.canceled = true;
+ },
+
+ observe(subject, topic, data) {
+ if (this.canceled) {
+ return;
+ }
+
+ for (let callback of alarmCallbacksMap.get(this.extension)) {
+ callback(this);
+ }
+
+ if (!this.periodInMinutes) {
+ this.clear();
+ return;
+ }
+
+ let delay = this.periodInMinutes * 60 * 1000;
+ this.scheduledTime = Date.now() + delay;
+ this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ get data() {
+ return {
+ name: this.name,
+ scheduledTime: this.scheduledTime,
+ periodInMinutes: this.periodInMinutes,
+ };
+ },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("startup", (type, extension) => {
+ alarmsMap.set(extension, new Map());
+ alarmCallbacksMap.set(extension, new Set());
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (alarmsMap.has(extension)) {
+ for (let alarm of alarmsMap.get(extension).values()) {
+ alarm.clear();
+ }
+ alarmsMap.delete(extension);
+ alarmCallbacksMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("alarms", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ alarms: {
+ create: function(name, alarmInfo) {
+ name = name || "";
+ let alarms = alarmsMap.get(extension);
+ if (alarms.has(name)) {
+ alarms.get(name).clear();
+ }
+ let alarm = new Alarm(extension, name, alarmInfo);
+ alarms.set(alarm.name, alarm);
+ },
+
+ get: function(name) {
+ name = name || "";
+ let alarms = alarmsMap.get(extension);
+ if (alarms.has(name)) {
+ return Promise.resolve(alarms.get(name).data);
+ }
+ return Promise.resolve();
+ },
+
+ getAll: function() {
+ let result = Array.from(alarmsMap.get(extension).values(), alarm => alarm.data);
+ return Promise.resolve(result);
+ },
+
+ clear: function(name) {
+ name = name || "";
+ let alarms = alarmsMap.get(extension);
+ if (alarms.has(name)) {
+ alarms.get(name).clear();
+ return Promise.resolve(true);
+ }
+ return Promise.resolve(false);
+ },
+
+ clearAll: function() {
+ let cleared = false;
+ for (let alarm of alarmsMap.get(extension).values()) {
+ alarm.clear();
+ cleared = true;
+ }
+ return Promise.resolve(cleared);
+ },
+
+ onAlarm: new EventManager(context, "alarms.onAlarm", fire => {
+ let callback = alarm => {
+ fire(alarm.data);
+ };
+
+ alarmCallbacksMap.get(extension).add(callback);
+ return () => {
+ alarmCallbacksMap.get(extension).delete(callback);
+ };
+ }).api(),
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-backgroundPage.js b/toolkit/components/extensions/ext-backgroundPage.js
new file mode 100644
index 0000000000..fce6100ca1
--- /dev/null
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -0,0 +1,147 @@
+"use strict";
+
+var {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+const {
+ promiseDocumentLoaded,
+ promiseObserved,
+} = ExtensionUtils;
+
+const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
+ `<?xml version="1.0"?>
+ <window id="documentElement"/>`);
+
+// WeakMap[Extension -> BackgroundPage]
+var backgroundPagesMap = new WeakMap();
+
+// Responsible for the background_page section of the manifest.
+function BackgroundPage(options, extension) {
+ this.extension = extension;
+ this.page = options.page || null;
+ this.isGenerated = !!options.scripts;
+ this.windowlessBrowser = null;
+ this.webNav = null;
+}
+
+BackgroundPage.prototype = {
+ build: Task.async(function* () {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
+ this.windowlessBrowser = windowlessBrowser;
+
+ let url;
+ if (this.page) {
+ url = this.extension.baseURI.resolve(this.page);
+ } else if (this.isGenerated) {
+ url = this.extension.baseURI.resolve("_generated_background_page.html");
+ }
+
+ if (!this.extension.isExtensionURL(url)) {
+ this.extension.manifestError("Background page must be a file within the extension");
+ url = this.extension.baseURI.resolve("_blank.html");
+ }
+
+ let system = Services.scriptSecurityManager.getSystemPrincipal();
+
+ // The windowless browser is a thin wrapper around a docShell that keeps
+ // its related resources alive. It implements nsIWebNavigation and
+ // forwards its methods to the underlying docShell, but cannot act as a
+ // docShell itself. Calling `getInterface(nsIDocShell)` gives us the
+ // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
+ // access to the webNav methods that are already available on the
+ // windowless browser, but contrary to appearances, they are not the same
+ // object.
+ let chromeShell = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIWebNavigation);
+
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ let attrs = chromeShell.getOriginAttributes();
+ attrs.privateBrowsingId = 1;
+ chromeShell.setOriginAttributes(attrs);
+ }
+
+ chromeShell.useGlobalHistory = false;
+ chromeShell.createAboutBlankContentViewer(system);
+ chromeShell.loadURI(XUL_URL, 0, null, null, null);
+
+
+ yield promiseObserved("chrome-document-global-created",
+ win => win.document == chromeShell.document);
+
+ let chromeDoc = yield promiseDocumentLoaded(chromeShell.document);
+
+ let browser = chromeDoc.createElement("browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("disableglobalhistory", "true");
+ chromeDoc.documentElement.appendChild(browser);
+
+ extensions.emit("extension-browser-inserted", browser);
+ browser.messageManager.sendAsyncMessage("Extension:InitExtensionView", {
+ viewType: "background",
+ url,
+ });
+
+ yield new Promise(resolve => {
+ browser.messageManager.addMessageListener("Extension:ExtensionViewLoaded", function onLoad() {
+ browser.messageManager.removeMessageListener("Extension:ExtensionViewLoaded", onLoad);
+ resolve();
+ });
+ });
+
+ // TODO(robwu): This is not webext-oop compatible.
+ this.webNav = browser.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let window = this.webNav.document.defaultView;
+
+
+ // Set the add-on's main debugger global, for use in the debugger
+ // console.
+ if (this.extension.addonData.instanceID) {
+ AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+ .then(addon => addon.setDebugGlobal(window));
+ }
+
+ this.extension.emit("startup");
+ }),
+
+ shutdown() {
+ if (this.extension.addonData.instanceID) {
+ AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+ .then(addon => addon.setDebugGlobal(null));
+ }
+
+ // Navigate away from the background page to invalidate any
+ // setTimeouts or other callbacks.
+ if (this.webNav) {
+ this.webNav.loadURI("about:blank", 0, null, null, null);
+ this.webNav = null;
+ }
+
+ this.windowlessBrowser.loadURI("about:blank", 0, null, null, null);
+ this.windowlessBrowser.close();
+ this.windowlessBrowser = null;
+ },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_background", (type, directive, extension, manifest) => {
+ let bgPage = new BackgroundPage(manifest.background, extension);
+ backgroundPagesMap.set(extension, bgPage);
+ return bgPage.build();
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (backgroundPagesMap.has(extension)) {
+ backgroundPagesMap.get(extension).shutdown();
+ backgroundPagesMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
diff --git a/toolkit/components/extensions/ext-browser-content.js b/toolkit/components/extensions/ext-browser-content.js
new file mode 100644
index 0000000000..e14ca50d6a
--- /dev/null
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -0,0 +1,217 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "require",
+ "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "colorUtils", () => {
+ return require("devtools/shared/css/color").colorUtils;
+});
+
+const {
+ stylesheetMap,
+} = ExtensionUtils;
+
+/* globals addMessageListener, content, docShell, sendAsyncMessage */
+
+// Minimum time between two resizes.
+const RESIZE_TIMEOUT = 100;
+
+const BrowserListener = {
+ init({allowScriptsToClose, fixedWidth, maxHeight, maxWidth, stylesheets}) {
+ this.fixedWidth = fixedWidth;
+ this.stylesheets = stylesheets || [];
+
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+
+ this.oldBackground = null;
+
+ if (allowScriptsToClose) {
+ content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .allowScriptsToClose();
+ }
+
+ addEventListener("DOMWindowCreated", this, true);
+ addEventListener("load", this, true);
+ addEventListener("DOMContentLoaded", this, true);
+ addEventListener("DOMWindowClose", this, true);
+ addEventListener("MozScrolledAreaChanged", this, true);
+ },
+
+ destroy() {
+ removeEventListener("DOMWindowCreated", this, true);
+ removeEventListener("load", this, true);
+ removeEventListener("DOMContentLoaded", this, true);
+ removeEventListener("DOMWindowClose", this, true);
+ removeEventListener("MozScrolledAreaChanged", this, true);
+ },
+
+ receiveMessage({name, data}) {
+ if (name === "Extension:InitBrowser") {
+ this.init(data);
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "DOMWindowCreated":
+ if (event.target === content.document) {
+ let winUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ for (let url of this.stylesheets) {
+ winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET);
+ }
+ }
+ break;
+
+ case "DOMWindowClose":
+ if (event.target === content) {
+ event.preventDefault();
+
+ sendAsyncMessage("Extension:DOMWindowClose");
+ }
+ break;
+
+ case "DOMContentLoaded":
+ if (event.target === content.document) {
+ sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
+ this.handleDOMChange(true);
+ }
+ break;
+
+ case "load":
+ if (event.target.contentWindow === content) {
+ // For about:addons inline <browsers>, we currently receive a load
+ // event on the <browser> element, but no load or DOMContentLoaded
+ // events from the content window.
+ sendAsyncMessage("Extension:BrowserContentLoaded", {url: content.location.href});
+ } else if (event.target !== content.document) {
+ break;
+ }
+
+ // We use a capturing listener, so we get this event earlier than any
+ // load listeners in the content page. Resizing after a timeout ensures
+ // that we calculate the size after the entire event cycle has completed
+ // (unless someone spins the event loop, anyway), and hopefully after
+ // the content has made any modifications.
+ Promise.resolve().then(() => {
+ this.handleDOMChange(true);
+ });
+
+ // Mutation observer to make sure the panel shrinks when the content does.
+ new content.MutationObserver(this.handleDOMChange.bind(this)).observe(
+ content.document.documentElement, {
+ attributes: true,
+ characterData: true,
+ childList: true,
+ subtree: true,
+ });
+ break;
+
+ case "MozScrolledAreaChanged":
+ this.handleDOMChange();
+ break;
+ }
+ },
+
+ // Resizes the browser to match the preferred size of the content (debounced).
+ handleDOMChange(ignoreThrottling = false) {
+ if (ignoreThrottling && this.resizeTimeout) {
+ clearTimeout(this.resizeTimeout);
+ this.resizeTimeout = null;
+ }
+
+ if (this.resizeTimeout == null) {
+ this.resizeTimeout = setTimeout(() => {
+ try {
+ if (content) {
+ this._handleDOMChange("delayed");
+ }
+ } finally {
+ this.resizeTimeout = null;
+ }
+ }, RESIZE_TIMEOUT);
+
+ this._handleDOMChange();
+ }
+ },
+
+ _handleDOMChange(detail) {
+ let doc = content.document;
+
+ let body = doc.body;
+ if (!body || doc.compatMode === "BackCompat") {
+ // In quirks mode, the root element is used as the scroll frame, and the
+ // body lies about its scroll geometry, and returns the values for the
+ // root instead.
+ body = doc.documentElement;
+ }
+
+
+ let result;
+ if (this.fixedWidth) {
+ // If we're in a fixed-width area (namely a slide-in subview of the main
+ // menu panel), we need to calculate the view height based on the
+ // preferred height of the content document's root scrollable element at the
+ // current width, rather than the complete preferred dimensions of the
+ // content window.
+
+ // Compensate for any offsets (margin, padding, ...) between the scroll
+ // area of the body and the outer height of the document.
+ let getHeight = elem => elem.getBoundingClientRect(elem).height;
+ let bodyPadding = getHeight(doc.documentElement) - getHeight(body);
+
+ let height = Math.ceil(body.scrollHeight + bodyPadding);
+
+ result = {height, detail};
+ } else {
+ let background = doc.defaultView.getComputedStyle(body).backgroundColor;
+ let bgColor = colorUtils.colorToRGBA(background);
+ if (bgColor.a !== 1) {
+ // Ignore non-opaque backgrounds.
+ background = null;
+ }
+
+ if (background !== this.oldBackground) {
+ sendAsyncMessage("Extension:BrowserBackgroundChanged", {background});
+ }
+ this.oldBackground = background;
+
+
+ // Adjust the size of the browser based on its content's preferred size.
+ let {contentViewer} = docShell;
+ let ratio = content.devicePixelRatio;
+
+ let w = {}, h = {};
+ contentViewer.getContentSizeConstrained(this.maxWidth * ratio,
+ this.maxHeight * ratio,
+ w, h);
+
+ let width = Math.ceil(w.value / ratio);
+ let height = Math.ceil(h.value / ratio);
+
+ result = {width, height, detail};
+ }
+
+ sendAsyncMessage("Extension:BrowserResized", result);
+ },
+};
+
+addMessageListener("Extension:InitBrowser", BrowserListener);
diff --git a/toolkit/components/extensions/ext-c-backgroundPage.js b/toolkit/components/extensions/ext-c-backgroundPage.js
new file mode 100644
index 0000000000..b5074dd9a6
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-backgroundPage.js
@@ -0,0 +1,45 @@
+"use strict";
+
+global.initializeBackgroundPage = (contentWindow) => {
+ // Override the `alert()` method inside background windows;
+ // we alias it to console.log().
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1203394
+ let alertDisplayedWarning = false;
+ let alertOverwrite = text => {
+ if (!alertDisplayedWarning) {
+ require("devtools/client/framework/devtools-browser");
+
+ let hudservice = require("devtools/client/webconsole/hudservice");
+ hudservice.openBrowserConsoleOrFocus();
+
+ contentWindow.console.warn("alert() is not supported in background windows; please use console.log instead.");
+
+ alertDisplayedWarning = true;
+ }
+
+ contentWindow.console.log(text);
+ };
+ Cu.exportFunction(alertOverwrite, contentWindow, {defineAs: "alert"});
+};
+
+extensions.registerSchemaAPI("extension", "addon_child", context => {
+ function getBackgroundPage() {
+ for (let view of context.extension.views) {
+ if (view.viewType == "background" && context.principal.subsumes(view.principal)) {
+ return view.contentWindow;
+ }
+ }
+ return null;
+ }
+ return {
+ extension: {
+ getBackgroundPage,
+ },
+
+ runtime: {
+ getBackgroundPage() {
+ return context.cloneScope.Promise.resolve(getBackgroundPage());
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-c-extension.js b/toolkit/components/extensions/ext-c-extension.js
new file mode 100644
index 0000000000..669309beab
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-extension.js
@@ -0,0 +1,57 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+function extensionApiFactory(context) {
+ return {
+ extension: {
+ getURL(url) {
+ return context.extension.baseURI.resolve(url);
+ },
+
+ get lastError() {
+ return context.lastError;
+ },
+
+ get inIncognitoContext() {
+ return context.incognito;
+ },
+ },
+ };
+}
+
+extensions.registerSchemaAPI("extension", "addon_child", extensionApiFactory);
+extensions.registerSchemaAPI("extension", "content_child", extensionApiFactory);
+extensions.registerSchemaAPI("extension", "addon_child", context => {
+ return {
+ extension: {
+ getViews: function(fetchProperties) {
+ let result = Cu.cloneInto([], context.cloneScope);
+
+ for (let view of context.extension.views) {
+ if (!view.active) {
+ continue;
+ }
+ if (!context.principal.subsumes(view.principal)) {
+ continue;
+ }
+
+ if (fetchProperties !== null) {
+ if (fetchProperties.type !== null && view.viewType != fetchProperties.type) {
+ continue;
+ }
+
+ if (fetchProperties.windowId !== null && view.windowId != fetchProperties.windowId) {
+ continue;
+ }
+ }
+
+ result.push(view.contentWindow);
+ }
+
+ return result;
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-c-runtime.js b/toolkit/components/extensions/ext-c-runtime.js
new file mode 100644
index 0000000000..8adca60ca3
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -0,0 +1,93 @@
+"use strict";
+
+function runtimeApiFactory(context) {
+ let {extension} = context;
+
+ return {
+ runtime: {
+ onConnect: context.messenger.onConnect("runtime.onConnect"),
+
+ onMessage: context.messenger.onMessage("runtime.onMessage"),
+
+ connect: function(extensionId, connectInfo) {
+ let name = connectInfo !== null && connectInfo.name || "";
+ extensionId = extensionId || extension.id;
+ let recipient = {extensionId};
+
+ return context.messenger.connect(context.messageManager, name, recipient);
+ },
+
+ sendMessage: function(...args) {
+ let options; // eslint-disable-line no-unused-vars
+ let extensionId, message, responseCallback;
+ if (typeof args[args.length - 1] == "function") {
+ responseCallback = args.pop();
+ }
+ if (!args.length) {
+ return Promise.reject({message: "runtime.sendMessage's message argument is missing"});
+ } else if (args.length == 1) {
+ message = args[0];
+ } else if (args.length == 2) {
+ if (typeof args[0] == "string" && args[0]) {
+ [extensionId, message] = args;
+ } else {
+ [message, options] = args;
+ }
+ } else if (args.length == 3) {
+ [extensionId, message, options] = args;
+ } else if (args.length == 4 && !responseCallback) {
+ return Promise.reject({message: "runtime.sendMessage's last argument is not a function"});
+ } else {
+ return Promise.reject({message: "runtime.sendMessage received too many arguments"});
+ }
+
+ if (extensionId != null && typeof extensionId != "string") {
+ return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"});
+ }
+ if (options != null && typeof options != "object") {
+ return Promise.reject({message: "runtime.sendMessage's options argument is invalid"});
+ }
+ // TODO(robwu): Validate option keys and values when we support it.
+
+ extensionId = extensionId || extension.id;
+ let recipient = {extensionId};
+
+ return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
+ },
+
+ connectNative(application) {
+ let recipient = {
+ childId: context.childManager.id,
+ toNativeApp: application,
+ };
+
+ return context.messenger.connectNative(context.messageManager, "", recipient);
+ },
+
+ sendNativeMessage(application, message) {
+ let recipient = {
+ childId: context.childManager.id,
+ toNativeApp: application,
+ };
+ return context.messenger.sendNativeMessage(context.messageManager, message, recipient);
+ },
+
+ get lastError() {
+ return context.lastError;
+ },
+
+ getManifest() {
+ return Cu.cloneInto(extension.manifest, context.cloneScope);
+ },
+
+ id: extension.id,
+
+ getURL: function(url) {
+ return extension.baseURI.resolve(url);
+ },
+ },
+ };
+}
+
+extensions.registerSchemaAPI("runtime", "addon_child", runtimeApiFactory);
+extensions.registerSchemaAPI("runtime", "content_child", runtimeApiFactory);
diff --git a/toolkit/components/extensions/ext-c-storage.js b/toolkit/components/extensions/ext-c-storage.js
new file mode 100644
index 0000000000..e8d53058f6
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-storage.js
@@ -0,0 +1,62 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+ "resource://gre/modules/ExtensionStorage.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function storageApiFactory(context) {
+ function sanitize(items) {
+ // The schema validator already takes care of arrays (which are only allowed
+ // to contain strings). Strings and null are safe values.
+ if (typeof items != "object" || items === null || Array.isArray(items)) {
+ return items;
+ }
+ // If we got here, then `items` is an object generated by `ObjectType`'s
+ // `normalize` method from Schemas.jsm. The object returned by `normalize`
+ // lives in this compartment, while the values live in compartment of
+ // `context.contentWindow`. The `sanitize` method runs with the principal
+ // of `context`, so we cannot just use `ExtensionStorage.sanitize` because
+ // it is not allowed to access properties of `items`.
+ // So we enumerate all properties and sanitize each value individually.
+ let sanitized = {};
+ for (let [key, value] of Object.entries(items)) {
+ sanitized[key] = ExtensionStorage.sanitize(value, context);
+ }
+ return sanitized;
+ }
+ return {
+ storage: {
+ local: {
+ get: function(keys) {
+ keys = sanitize(keys);
+ return context.childManager.callParentAsyncFunction("storage.local.get", [
+ keys,
+ ]);
+ },
+ set: function(items) {
+ items = sanitize(items);
+ return context.childManager.callParentAsyncFunction("storage.local.set", [
+ items,
+ ]);
+ },
+ },
+
+ sync: {
+ get: function(keys) {
+ keys = sanitize(keys);
+ return context.childManager.callParentAsyncFunction("storage.sync.get", [
+ keys,
+ ]);
+ },
+ set: function(items) {
+ items = sanitize(items);
+ return context.childManager.callParentAsyncFunction("storage.sync.set", [
+ items,
+ ]);
+ },
+ },
+ },
+ };
+}
+extensions.registerSchemaAPI("storage", "addon_child", storageApiFactory);
+extensions.registerSchemaAPI("storage", "content_child", storageApiFactory);
diff --git a/toolkit/components/extensions/ext-c-test.js b/toolkit/components/extensions/ext-c-test.js
new file mode 100644
index 0000000000..b0c92f79f7
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-test.js
@@ -0,0 +1,188 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ SingletonEventManager,
+} = ExtensionUtils;
+
+/**
+ * Checks whether the given error matches the given expectations.
+ *
+ * @param {*} error
+ * The error to check.
+ * @param {string|RegExp|function|null} expectedError
+ * The expectation to check against. If this parameter is:
+ *
+ * - a string, the error message must exactly equal the string.
+ * - a regular expression, it must match the error message.
+ * - a function, it is called with the error object and its
+ * return value is returned.
+ * - null, the function always returns true.
+ * @param {BaseContext} context
+ *
+ * @returns {boolean}
+ * True if the error matches the expected error.
+ */
+function errorMatches(error, expectedError, context) {
+ if (expectedError === null) {
+ return true;
+ }
+
+ if (typeof expectedError === "function") {
+ return context.runSafeWithoutClone(expectedError, error);
+ }
+
+ if (typeof error !== "object" || error == null ||
+ typeof error.message !== "string") {
+ return false;
+ }
+
+ if (typeof expectedError === "string") {
+ return error.message === expectedError;
+ }
+
+ try {
+ return expectedError.test(error.message);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ return false;
+}
+
+/**
+ * Calls .toSource() on the given value, but handles null, undefined,
+ * and errors.
+ *
+ * @param {*} value
+ * @returns {string}
+ */
+function toSource(value) {
+ if (value === null) {
+ return "null";
+ }
+ if (value === undefined) {
+ return "undefined";
+ }
+ if (typeof value === "string") {
+ return JSON.stringify(value);
+ }
+
+ try {
+ return String(value.toSource());
+ } catch (e) {
+ return "<unknown>";
+ }
+}
+
+function makeTestAPI(context) {
+ const {extension} = context;
+
+ function getStack() {
+ return new context.cloneScope.Error().stack.replace(/^/gm, " ");
+ }
+
+ function assertTrue(value, msg) {
+ extension.emit("test-result", Boolean(value), String(msg), getStack());
+ }
+
+ return {
+ test: {
+ sendMessage(...args) {
+ extension.emit("test-message", ...args);
+ },
+
+ notifyPass(msg) {
+ extension.emit("test-done", true, msg, getStack());
+ },
+
+ notifyFail(msg) {
+ extension.emit("test-done", false, msg, getStack());
+ },
+
+ log(msg) {
+ extension.emit("test-log", true, msg, getStack());
+ },
+
+ fail(msg) {
+ assertTrue(false, msg);
+ },
+
+ succeed(msg) {
+ assertTrue(true, msg);
+ },
+
+ assertTrue(value, msg) {
+ assertTrue(value, msg);
+ },
+
+ assertFalse(value, msg) {
+ assertTrue(!value, msg);
+ },
+
+ assertEq(expected, actual, msg) {
+ let equal = expected === actual;
+
+ expected = String(expected);
+ actual = String(actual);
+
+ if (!equal && expected === actual) {
+ actual += " (different)";
+ }
+ extension.emit("test-eq", equal, String(msg), expected, actual, getStack());
+ },
+
+ assertRejects(promise, expectedError, msg) {
+ // Wrap in a native promise for consistency.
+ promise = Promise.resolve(promise);
+
+ if (msg) {
+ msg = `: ${msg}`;
+ }
+
+ return promise.then(result => {
+ assertTrue(false, `Promise resolved, expected rejection${msg}`);
+ }, error => {
+ let errorMessage = toSource(error && error.message);
+
+ assertTrue(errorMatches(error, expectedError, context),
+ `Promise rejected, expecting rejection to match ${toSource(expectedError)}, ` +
+ `got ${errorMessage}${msg}`);
+ });
+ },
+
+ assertThrows(func, expectedError, msg) {
+ if (msg) {
+ msg = `: ${msg}`;
+ }
+
+ try {
+ func();
+
+ assertTrue(false, `Function did not throw, expected error${msg}`);
+ } catch (error) {
+ let errorMessage = toSource(error && error.message);
+
+ assertTrue(errorMatches(error, expectedError, context),
+ `Function threw, expecting error to match ${toSource(expectedError)}` +
+ `got ${errorMessage}${msg}`);
+ }
+ },
+
+ onMessage: new SingletonEventManager(context, "test.onMessage", fire => {
+ let handler = (event, ...args) => {
+ context.runSafe(fire, ...args);
+ };
+
+ extension.on("test-harness-message", handler);
+ return () => {
+ extension.off("test-harness-message", handler);
+ };
+ }).api(),
+ },
+ };
+}
+
+extensions.registerSchemaAPI("test", "addon_child", makeTestAPI);
+extensions.registerSchemaAPI("test", "content_child", makeTestAPI);
+
diff --git a/toolkit/components/extensions/ext-cookies.js b/toolkit/components/extensions/ext-cookies.js
new file mode 100644
index 0000000000..d0a7034218
--- /dev/null
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -0,0 +1,484 @@
+"use strict";
+
+const {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
+ "resource://gre/modules/ContextualIdentityService.jsm");
+
+var {
+ EventManager,
+} = ExtensionUtils;
+
+var DEFAULT_STORE = "firefox-default";
+var PRIVATE_STORE = "firefox-private";
+var CONTAINER_STORE = "firefox-container-";
+
+global.getCookieStoreIdForTab = function(data, tab) {
+ if (data.incognito) {
+ return PRIVATE_STORE;
+ }
+
+ if (tab.userContextId) {
+ return CONTAINER_STORE + tab.userContextId;
+ }
+
+ return DEFAULT_STORE;
+};
+
+global.isPrivateCookieStoreId = function(storeId) {
+ return storeId == PRIVATE_STORE;
+};
+
+global.isDefaultCookieStoreId = function(storeId) {
+ return storeId == DEFAULT_STORE;
+};
+
+global.isContainerCookieStoreId = function(storeId) {
+ return storeId !== null && storeId.startsWith(CONTAINER_STORE);
+};
+
+global.getContainerForCookieStoreId = function(storeId) {
+ if (!global.isContainerCookieStoreId(storeId)) {
+ return null;
+ }
+
+ let containerId = storeId.substring(CONTAINER_STORE.length);
+ if (ContextualIdentityService.getIdentityFromId(containerId)) {
+ return parseInt(containerId, 10);
+ }
+
+ return null;
+};
+
+global.isValidCookieStoreId = function(storeId) {
+ return global.isDefaultCookieStoreId(storeId) ||
+ global.isPrivateCookieStoreId(storeId) ||
+ global.isContainerCookieStoreId(storeId);
+};
+
+function convert({cookie, isPrivate}) {
+ let result = {
+ name: cookie.name,
+ value: cookie.value,
+ domain: cookie.host,
+ hostOnly: !cookie.isDomain,
+ path: cookie.path,
+ secure: cookie.isSecure,
+ httpOnly: cookie.isHttpOnly,
+ session: cookie.isSession,
+ };
+
+ if (!cookie.isSession) {
+ result.expirationDate = cookie.expiry;
+ }
+
+ if (cookie.originAttributes.userContextId) {
+ result.storeId = CONTAINER_STORE + cookie.originAttributes.userContextId;
+ } else if (cookie.originAttributes.privateBrowsingId || isPrivate) {
+ result.storeId = PRIVATE_STORE;
+ } else {
+ result.storeId = DEFAULT_STORE;
+ }
+
+ return result;
+}
+
+function isSubdomain(otherDomain, baseDomain) {
+ return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain);
+}
+
+// Checks that the given extension has permission to set the given cookie for
+// the given URI.
+function checkSetCookiePermissions(extension, uri, cookie) {
+ // Permission checks:
+ //
+ // - If the extension does not have permissions for the specified
+ // URL, it cannot set cookies for it.
+ //
+ // - If the specified URL could not set the given cookie, neither can
+ // the extension.
+ //
+ // Ideally, we would just have the cookie service make the latter
+ // determination, but that turns out to be quite complicated. At the
+ // moment, it requires constructing a cookie string and creating a
+ // dummy channel, both of which can be problematic. It also triggers
+ // a whole set of additional permission and preference checks, which
+ // may or may not be desirable.
+ //
+ // So instead, we do a similar set of checks here. Exactly what
+ // cookies a given URL should be able to set is not well-documented,
+ // and is not standardized in any standard that anyone actually
+ // follows. So instead, we follow the rules used by the cookie
+ // service.
+ //
+ // See source/netwerk/cookie/nsCookieService.cpp, in particular
+ // CheckDomain() and SetCookieInternal().
+
+ if (uri.scheme != "http" && uri.scheme != "https") {
+ return false;
+ }
+
+ if (!extension.whiteListedHosts.matchesIgnoringPath(uri)) {
+ return false;
+ }
+
+ if (!cookie.host) {
+ // If no explicit host is specified, this becomes a host-only cookie.
+ cookie.host = uri.host;
+ return true;
+ }
+
+ // A leading "." is not expected, but is tolerated if it's not the only
+ // character in the host. If there is one, start by stripping it off. We'll
+ // add a new one on success.
+ if (cookie.host.length > 1) {
+ cookie.host = cookie.host.replace(/^\./, "");
+ }
+ cookie.host = cookie.host.toLowerCase();
+
+ if (cookie.host != uri.host) {
+ // Not an exact match, so check for a valid subdomain.
+ let baseDomain;
+ try {
+ baseDomain = Services.eTLD.getBaseDomain(uri);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+ e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ // The cookie service uses these to determine whether the domain
+ // requires an exact match. We already know we don't have an exact
+ // match, so return false. In all other cases, re-raise the error.
+ return false;
+ }
+ throw e;
+ }
+
+ // The cookie domain must be a subdomain of the base domain. This prevents
+ // us from setting cookies for domains like ".co.uk".
+ // The domain of the requesting URL must likewise be a subdomain of the
+ // cookie domain. This prevents us from setting cookies for entirely
+ // unrelated domains.
+ if (!isSubdomain(cookie.host, baseDomain) ||
+ !isSubdomain(uri.host, cookie.host)) {
+ return false;
+ }
+
+ // RFC2109 suggests that we may only add cookies for sub-domains 1-level
+ // below us, but enforcing that would break the web, so we don't.
+ }
+
+ // An explicit domain was passed, so add a leading "." to make this a
+ // domain cookie.
+ cookie.host = "." + cookie.host;
+
+ // We don't do any significant checking of path permissions. RFC2109
+ // suggests we only allow sites to add cookies for sub-paths, similar to
+ // same origin policy enforcement, but no-one implements this.
+
+ return true;
+}
+
+function* query(detailsIn, props, context) {
+ // Different callers want to filter on different properties. |props|
+ // tells us which ones they're interested in.
+ let details = {};
+ props.forEach(property => {
+ if (detailsIn[property] !== null) {
+ details[property] = detailsIn[property];
+ }
+ });
+
+ if ("domain" in details) {
+ details.domain = details.domain.toLowerCase().replace(/^\./, "");
+ }
+
+ let userContextId = 0;
+ let isPrivate = context.incognito;
+ if (details.storeId) {
+ if (!global.isValidCookieStoreId(details.storeId)) {
+ return;
+ }
+
+ if (global.isDefaultCookieStoreId(details.storeId)) {
+ isPrivate = false;
+ } else if (global.isPrivateCookieStoreId(details.storeId)) {
+ isPrivate = true;
+ } else if (global.isContainerCookieStoreId(details.storeId)) {
+ isPrivate = false;
+ userContextId = global.getContainerForCookieStoreId(details.storeId);
+ if (!userContextId) {
+ return;
+ }
+ }
+ }
+
+ let storeId = DEFAULT_STORE;
+ if (isPrivate) {
+ storeId = PRIVATE_STORE;
+ } else if ("storeId" in details) {
+ storeId = details.storeId;
+ }
+
+ // We can use getCookiesFromHost for faster searching.
+ let enumerator;
+ let uri;
+ if ("url" in details) {
+ try {
+ uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL);
+ Services.cookies.usePrivateMode(isPrivate, () => {
+ enumerator = Services.cookies.getCookiesFromHost(uri.host, {userContextId});
+ });
+ } catch (ex) {
+ // This often happens for about: URLs
+ return;
+ }
+ } else if ("domain" in details) {
+ Services.cookies.usePrivateMode(isPrivate, () => {
+ enumerator = Services.cookies.getCookiesFromHost(details.domain, {userContextId});
+ });
+ } else {
+ Services.cookies.usePrivateMode(isPrivate, () => {
+ enumerator = Services.cookies.enumerator;
+ });
+ }
+
+ // Based on nsCookieService::GetCookieStringInternal
+ function matches(cookie) {
+ function domainMatches(host) {
+ return cookie.rawHost == host || (cookie.isDomain && host.endsWith(cookie.host));
+ }
+
+ function pathMatches(path) {
+ let cookiePath = cookie.path.replace(/\/$/, "");
+
+ if (!path.startsWith(cookiePath)) {
+ return false;
+ }
+
+ // path == cookiePath, but without the redundant string compare.
+ if (path.length == cookiePath.length) {
+ return true;
+ }
+
+ // URL path is a substring of the cookie path, so it matches if, and
+ // only if, the next character is a path delimiter.
+ let pathDelimiters = ["/", "?", "#", ";"];
+ return pathDelimiters.includes(path[cookiePath.length]);
+ }
+
+ // "Restricts the retrieved cookies to those that would match the given URL."
+ if (uri) {
+ if (!domainMatches(uri.host)) {
+ return false;
+ }
+
+ if (cookie.isSecure && uri.scheme != "https") {
+ return false;
+ }
+
+ if (!pathMatches(uri.path)) {
+ return false;
+ }
+ }
+
+ if ("name" in details && details.name != cookie.name) {
+ return false;
+ }
+
+ if (userContextId != cookie.originAttributes.userContextId) {
+ return false;
+ }
+
+ // "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."
+ if ("domain" in details && !isSubdomain(cookie.rawHost, details.domain)) {
+ return false;
+ }
+
+ // "Restricts the retrieved cookies to those whose path exactly matches this string.""
+ if ("path" in details && details.path != cookie.path) {
+ return false;
+ }
+
+ if ("secure" in details && details.secure != cookie.isSecure) {
+ return false;
+ }
+
+ if ("session" in details && details.session != cookie.isSession) {
+ return false;
+ }
+
+ // Check that the extension has permissions for this host.
+ if (!context.extension.whiteListedHosts.matchesCookie(cookie)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ while (enumerator.hasMoreElements()) {
+ let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+ if (matches(cookie)) {
+ yield {cookie, isPrivate, storeId};
+ }
+ }
+}
+
+extensions.registerSchemaAPI("cookies", "addon_parent", context => {
+ let {extension} = context;
+ let self = {
+ cookies: {
+ get: function(details) {
+ // FIXME: We don't sort by length of path and creation time.
+ for (let cookie of query(details, ["url", "name", "storeId"], context)) {
+ return Promise.resolve(convert(cookie));
+ }
+
+ // Found no match.
+ return Promise.resolve(null);
+ },
+
+ getAll: function(details) {
+ let allowed = ["url", "name", "domain", "path", "secure", "session", "storeId"];
+ let result = Array.from(query(details, allowed, context), convert);
+
+ return Promise.resolve(result);
+ },
+
+ set: function(details) {
+ let uri = NetUtil.newURI(details.url).QueryInterface(Ci.nsIURL);
+
+ let path;
+ if (details.path !== null) {
+ path = details.path;
+ } else {
+ // This interface essentially emulates the behavior of the
+ // Set-Cookie header. In the case of an omitted path, the cookie
+ // service uses the directory path of the requesting URL, ignoring
+ // any filename or query parameters.
+ path = uri.directory;
+ }
+
+ let name = details.name !== null ? details.name : "";
+ let value = details.value !== null ? details.value : "";
+ let secure = details.secure !== null ? details.secure : false;
+ let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
+ let isSession = details.expirationDate === null;
+ let expiry = isSession ? Number.MAX_SAFE_INTEGER : details.expirationDate;
+ let isPrivate = context.incognito;
+ let userContextId = 0;
+ if (global.isDefaultCookieStoreId(details.storeId)) {
+ isPrivate = false;
+ } else if (global.isPrivateCookieStoreId(details.storeId)) {
+ isPrivate = true;
+ } else if (global.isContainerCookieStoreId(details.storeId)) {
+ let containerId = global.getContainerForCookieStoreId(details.storeId);
+ if (containerId === null) {
+ return Promise.reject({message: `Illegal storeId: ${details.storeId}`});
+ }
+ isPrivate = false;
+ userContextId = containerId;
+ } else if (details.storeId !== null) {
+ return Promise.reject({message: "Unknown storeId"});
+ }
+
+ let cookieAttrs = {host: details.domain, path: path, isSecure: secure};
+ if (!checkSetCookiePermissions(extension, uri, cookieAttrs)) {
+ return Promise.reject({message: `Permission denied to set cookie ${JSON.stringify(details)}`});
+ }
+
+ // The permission check may have modified the domain, so use
+ // the new value instead.
+ Services.cookies.usePrivateMode(isPrivate, () => {
+ Services.cookies.add(cookieAttrs.host, path, name, value,
+ secure, httpOnly, isSession, expiry, {userContextId});
+ });
+
+ return self.cookies.get(details);
+ },
+
+ remove: function(details) {
+ for (let {cookie, isPrivate, storeId} of query(details, ["url", "name", "storeId"], context)) {
+ Services.cookies.usePrivateMode(isPrivate, () => {
+ Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+ });
+
+ // Todo: could there be multiple per subdomain?
+ return Promise.resolve({
+ url: details.url,
+ name: details.name,
+ storeId,
+ });
+ }
+
+ return Promise.resolve(null);
+ },
+
+ getAllCookieStores: function() {
+ let data = {};
+ for (let window of WindowListManager.browserWindows()) {
+ let tabs = TabManager.for(extension).getTabs(window);
+ for (let tab of tabs) {
+ if (!(tab.cookieStoreId in data)) {
+ data[tab.cookieStoreId] = [];
+ }
+ data[tab.cookieStoreId].push(tab);
+ }
+ }
+
+ let result = [];
+ for (let key in data) {
+ result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE});
+ }
+ return Promise.resolve(result);
+ },
+
+ onChanged: new EventManager(context, "cookies.onChanged", fire => {
+ let observer = (subject, topic, data) => {
+ let notify = (removed, cookie, cause) => {
+ cookie.QueryInterface(Ci.nsICookie2);
+
+ if (extension.whiteListedHosts.matchesCookie(cookie)) {
+ fire({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
+ }
+ };
+
+ // We do our best effort here to map the incompatible states.
+ switch (data) {
+ case "deleted":
+ notify(true, subject, "explicit");
+ break;
+ case "added":
+ notify(false, subject, "explicit");
+ break;
+ case "changed":
+ notify(true, subject, "overwrite");
+ notify(false, subject, "explicit");
+ break;
+ case "batch-deleted":
+ subject.QueryInterface(Ci.nsIArray);
+ for (let i = 0; i < subject.length; i++) {
+ let cookie = subject.queryElementAt(i, Ci.nsICookie2);
+ if (!cookie.isSession && cookie.expiry * 1000 <= Date.now()) {
+ notify(true, cookie, "expired");
+ } else {
+ notify(true, cookie, "evicted");
+ }
+ }
+ break;
+ }
+ };
+
+ Services.obs.addObserver(observer, "cookie-changed", false);
+ Services.obs.addObserver(observer, "private-cookie-changed", false);
+ return () => {
+ Services.obs.removeObserver(observer, "cookie-changed");
+ Services.obs.removeObserver(observer, "private-cookie-changed");
+ };
+ }).api(),
+ },
+ };
+
+ return self;
+});
diff --git a/toolkit/components/extensions/ext-downloads.js b/toolkit/components/extensions/ext-downloads.js
new file mode 100644
index 0000000000..132814ae4b
--- /dev/null
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -0,0 +1,799 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+const {
+ ignoreEvent,
+ normalizeTime,
+ runSafeSync,
+ SingletonEventManager,
+ PlatformInfo,
+} = ExtensionUtils;
+
+const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
+ "danger", "mime", "startTime", "endTime",
+ "estimatedEndTime", "state",
+ "paused", "canResume", "error",
+ "bytesReceived", "totalBytes",
+ "fileSize", "exists",
+ "byExtensionId", "byExtensionName"];
+
+// Fields that we generate onChanged events for.
+const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
+ "error", "exists"];
+
+// From https://fetch.spec.whatwg.org/#forbidden-header-name
+const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
+ "ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
+ "CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
+ "EXPECT", "HOST", "KEEP-ALIVE", "ORIGIN", "REFERER", "TE", "TRAILER",
+ "TRANSFER-ENCODING", "UPGRADE", "VIA"];
+
+const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
+
+class DownloadItem {
+ constructor(id, download, extension) {
+ this.id = id;
+ this.download = download;
+ this.extension = extension;
+ this.prechange = {};
+ }
+
+ get url() { return this.download.source.url; }
+ get referrer() { return this.download.source.referrer; }
+ get filename() { return this.download.target.path; }
+ get incognito() { return this.download.source.isPrivate; }
+ get danger() { return "safe"; } // TODO
+ get mime() { return this.download.contentType; }
+ get startTime() { return this.download.startTime; }
+ get endTime() { return null; } // TODO
+ get estimatedEndTime() { return null; } // TODO
+ get state() {
+ if (this.download.succeeded) {
+ return "complete";
+ }
+ if (this.download.canceled) {
+ return "interrupted";
+ }
+ return "in_progress";
+ }
+ get paused() {
+ return this.download.canceled && this.download.hasPartialData && !this.download.error;
+ }
+ get canResume() {
+ return (this.download.stopped || this.download.canceled) &&
+ this.download.hasPartialData && !this.download.error;
+ }
+ get error() {
+ if (!this.download.stopped || this.download.succeeded) {
+ return null;
+ }
+ // TODO store this instead of calculating it
+
+ if (this.download.error) {
+ if (this.download.error.becauseSourceFailed) {
+ return "NETWORK_FAILED"; // TODO
+ }
+ if (this.download.error.becauseTargetFailed) {
+ return "FILE_FAILED"; // TODO
+ }
+ return "CRASH";
+ }
+ return "USER_CANCELED";
+ }
+ get bytesReceived() {
+ return this.download.currentBytes;
+ }
+ get totalBytes() {
+ return this.download.hasProgress ? this.download.totalBytes : -1;
+ }
+ get fileSize() {
+ // todo: this is supposed to be post-compression
+ return this.download.succeeded ? this.download.target.size : -1;
+ }
+ get exists() { return this.download.target.exists; }
+ get byExtensionId() { return this.extension ? this.extension.id : undefined; }
+ get byExtensionName() { return this.extension ? this.extension.name : undefined; }
+
+ /**
+ * Create a cloneable version of this object by pulling all the
+ * fields into simple properties (instead of getters).
+ *
+ * @returns {object} A DownloadItem with flat properties,
+ * suitable for cloning.
+ */
+ serialize() {
+ let obj = {};
+ for (let field of DOWNLOAD_ITEM_FIELDS) {
+ obj[field] = this[field];
+ }
+ if (obj.startTime) {
+ obj.startTime = obj.startTime.toISOString();
+ }
+ return obj;
+ }
+
+ // When a change event fires, handlers can look at how an individual
+ // field changed by comparing item.fieldname with item.prechange.fieldname.
+ // After all handlers have been invoked, this gets called to store the
+ // current values of all fields ahead of the next event.
+ _change() {
+ for (let field of DOWNLOAD_ITEM_CHANGE_FIELDS) {
+ this.prechange[field] = this[field];
+ }
+ }
+}
+
+
+// DownloadMap maps back and forth betwen the numeric identifiers used in
+// the downloads WebExtension API and a Download object from the Downloads jsm.
+// todo: make id and extension info persistent (bug 1247794)
+const DownloadMap = {
+ currentId: 0,
+ loadPromise: null,
+
+ // Maps numeric id -> DownloadItem
+ byId: new Map(),
+
+ // Maps Download object -> DownloadItem
+ byDownload: new WeakMap(),
+
+ lazyInit() {
+ if (this.loadPromise == null) {
+ EventEmitter.decorate(this);
+ this.loadPromise = Downloads.getList(Downloads.ALL).then(list => {
+ let self = this;
+ return list.addView({
+ onDownloadAdded(download) {
+ const item = self.newFromDownload(download, null);
+ self.emit("create", item);
+ },
+
+ onDownloadRemoved(download) {
+ const item = self.byDownload.get(download);
+ if (item != null) {
+ self.emit("erase", item);
+ self.byDownload.delete(download);
+ self.byId.delete(item.id);
+ }
+ },
+
+ onDownloadChanged(download) {
+ const item = self.byDownload.get(download);
+ if (item == null) {
+ Cu.reportError("Got onDownloadChanged for unknown download object");
+ } else {
+ // We get the first one of these when the download is started.
+ // In this case, don't emit anything, just initialize prechange.
+ if (Object.keys(item.prechange).length > 0) {
+ self.emit("change", item);
+ }
+ item._change();
+ }
+ },
+ }).then(() => list.getAll())
+ .then(downloads => {
+ downloads.forEach(download => {
+ this.newFromDownload(download, null);
+ });
+ })
+ .then(() => list);
+ });
+ }
+ return this.loadPromise;
+ },
+
+ getDownloadList() {
+ return this.lazyInit();
+ },
+
+ getAll() {
+ return this.lazyInit().then(() => this.byId.values());
+ },
+
+ fromId(id) {
+ const download = this.byId.get(id);
+ if (!download) {
+ throw new Error(`Invalid download id ${id}`);
+ }
+ return download;
+ },
+
+ newFromDownload(download, extension) {
+ if (this.byDownload.has(download)) {
+ return this.byDownload.get(download);
+ }
+
+ const id = ++this.currentId;
+ let item = new DownloadItem(id, download, extension);
+ this.byId.set(id, item);
+ this.byDownload.set(download, item);
+ return item;
+ },
+
+ erase(item) {
+ // This will need to get more complicated for bug 1255507 but for now we
+ // only work with downloads in the DownloadList from getAll()
+ return this.getDownloadList().then(list => {
+ list.remove(item.download);
+ });
+ },
+};
+
+// Create a callable function that filters a DownloadItem based on a
+// query object of the type passed to search() or erase().
+function downloadQuery(query) {
+ let queryTerms = [];
+ let queryNegativeTerms = [];
+ if (query.query != null) {
+ for (let term of query.query) {
+ if (term[0] == "-") {
+ queryNegativeTerms.push(term.slice(1).toLowerCase());
+ } else {
+ queryTerms.push(term.toLowerCase());
+ }
+ }
+ }
+
+ function normalizeDownloadTime(arg, before) {
+ if (arg == null) {
+ return before ? Number.MAX_VALUE : 0;
+ }
+ return normalizeTime(arg).getTime();
+ }
+
+ const startedBefore = normalizeDownloadTime(query.startedBefore, true);
+ const startedAfter = normalizeDownloadTime(query.startedAfter, false);
+ // const endedBefore = normalizeDownloadTime(query.endedBefore, true);
+ // const endedAfter = normalizeDownloadTime(query.endedAfter, false);
+
+ const totalBytesGreater = query.totalBytesGreater || 0;
+ const totalBytesLess = (query.totalBytesLess != null)
+ ? query.totalBytesLess : Number.MAX_VALUE;
+
+ // Handle options for which we can have a regular expression and/or
+ // an explicit value to match.
+ function makeMatch(regex, value, field) {
+ if (value == null && regex == null) {
+ return input => true;
+ }
+
+ let re;
+ try {
+ re = new RegExp(regex || "", "i");
+ } catch (err) {
+ throw new Error(`Invalid ${field}Regex: ${err.message}`);
+ }
+ if (value == null) {
+ return input => re.test(input);
+ }
+
+ value = value.toLowerCase();
+ if (re.test(value)) {
+ return input => (value == input);
+ }
+ return input => false;
+ }
+
+ const matchFilename = makeMatch(query.filenameRegex, query.filename, "filename");
+ const matchUrl = makeMatch(query.urlRegex, query.url, "url");
+
+ return function(item) {
+ const url = item.url.toLowerCase();
+ const filename = item.filename.toLowerCase();
+
+ if (!queryTerms.every(term => url.includes(term) || filename.includes(term))) {
+ return false;
+ }
+
+ if (queryNegativeTerms.some(term => url.includes(term) || filename.includes(term))) {
+ return false;
+ }
+
+ if (!matchFilename(filename) || !matchUrl(url)) {
+ return false;
+ }
+
+ if (!item.startTime) {
+ if (query.startedBefore != null || query.startedAfter != null) {
+ return false;
+ }
+ } else if (item.startTime > startedBefore || item.startTime < startedAfter) {
+ return false;
+ }
+
+ // todo endedBefore, endedAfter
+
+ if (item.totalBytes == -1) {
+ if (query.totalBytesGreater != null || query.totalBytesLess != null) {
+ return false;
+ }
+ } else if (item.totalBytes <= totalBytesGreater || item.totalBytes >= totalBytesLess) {
+ return false;
+ }
+
+ // todo: include danger
+ const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state",
+ "paused", "error",
+ "bytesReceived", "totalBytes", "fileSize", "exists"];
+ for (let field of SIMPLE_ITEMS) {
+ if (query[field] != null && item[field] != query[field]) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+}
+
+function queryHelper(query) {
+ let matchFn;
+ try {
+ matchFn = downloadQuery(query);
+ } catch (err) {
+ return Promise.reject({message: err.message});
+ }
+
+ let compareFn;
+ if (query.orderBy != null) {
+ const fields = query.orderBy.map(field => field[0] == "-"
+ ? {reverse: true, name: field.slice(1)}
+ : {reverse: false, name: field});
+
+ for (let field of fields) {
+ if (!DOWNLOAD_ITEM_FIELDS.includes(field.name)) {
+ return Promise.reject({message: `Invalid orderBy field ${field.name}`});
+ }
+ }
+
+ compareFn = (dl1, dl2) => {
+ for (let field of fields) {
+ const val1 = dl1[field.name];
+ const val2 = dl2[field.name];
+
+ if (val1 < val2) {
+ return field.reverse ? 1 : -1;
+ } else if (val1 > val2) {
+ return field.reverse ? -1 : 1;
+ }
+ }
+ return 0;
+ };
+ }
+
+ return DownloadMap.getAll().then(downloads => {
+ if (compareFn) {
+ downloads = Array.from(downloads);
+ downloads.sort(compareFn);
+ }
+ let results = [];
+ for (let download of downloads) {
+ if (query.limit && results.length >= query.limit) {
+ break;
+ }
+ if (matchFn(download)) {
+ results.push(download);
+ }
+ }
+ return results;
+ });
+}
+
+extensions.registerSchemaAPI("downloads", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ downloads: {
+ download(options) {
+ let {filename} = options;
+ if (filename && PlatformInfo.os === "win") {
+ // cross platform javascript code uses "/"
+ filename = filename.replace(/\//g, "\\");
+ }
+
+ if (filename != null) {
+ if (filename.length == 0) {
+ return Promise.reject({message: "filename must not be empty"});
+ }
+
+ let path = OS.Path.split(filename);
+ if (path.absolute) {
+ return Promise.reject({message: "filename must not be an absolute path"});
+ }
+
+ if (path.components.some(component => component == "..")) {
+ return Promise.reject({message: "filename must not contain back-references (..)"});
+ }
+ }
+
+ if (options.conflictAction == "prompt") {
+ // TODO
+ return Promise.reject({message: "conflictAction prompt not yet implemented"});
+ }
+
+ if (options.headers) {
+ for (let {name} of options.headers) {
+ if (FORBIDDEN_HEADERS.includes(name.toUpperCase()) || name.match(FORBIDDEN_PREFIXES)) {
+ return Promise.reject({message: "Forbidden request header name"});
+ }
+ }
+ }
+
+ // Handle method, headers and body options.
+ function adjustChannel(channel) {
+ if (channel instanceof Ci.nsIHttpChannel) {
+ const method = options.method || "GET";
+ channel.requestMethod = method;
+
+ if (options.headers) {
+ for (let {name, value} of options.headers) {
+ channel.setRequestHeader(name, value, false);
+ }
+ }
+
+ if (options.body != null) {
+ const stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(options.body, options.body.length);
+
+ channel.QueryInterface(Ci.nsIUploadChannel2);
+ channel.explicitSetUploadStream(stream, null, -1, method, false);
+ }
+ }
+ return Promise.resolve();
+ }
+
+ function createTarget(downloadsDir) {
+ let target;
+ if (filename) {
+ target = OS.Path.join(downloadsDir, filename);
+ } else {
+ let uri = NetUtil.newURI(options.url);
+
+ let remote = "download";
+ if (uri instanceof Ci.nsIURL) {
+ remote = uri.fileName;
+ }
+ target = OS.Path.join(downloadsDir, remote);
+ }
+
+ // Create any needed subdirectories if required by filename.
+ const dir = OS.Path.dirname(target);
+ return OS.File.makeDir(dir, {from: downloadsDir}).then(() => {
+ return OS.File.exists(target);
+ }).then(exists => {
+ // This has a race, something else could come along and create
+ // the file between this test and them time the download code
+ // creates the target file. But we can't easily fix it without
+ // modifying DownloadCore so we live with it for now.
+ if (exists) {
+ switch (options.conflictAction) {
+ case "uniquify":
+ default:
+ target = DownloadPaths.createNiceUniqueFile(new FileUtils.File(target)).path;
+ break;
+
+ case "overwrite":
+ break;
+ }
+ }
+ }).then(() => {
+ if (!options.saveAs) {
+ return Promise.resolve(target);
+ }
+
+ // Setup the file picker Save As dialog.
+ const picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ const window = Services.wm.getMostRecentWindow("navigator:browser");
+ picker.init(window, null, Ci.nsIFilePicker.modeSave);
+ picker.displayDirectory = new FileUtils.File(dir);
+ picker.appendFilters(Ci.nsIFilePicker.filterAll);
+ picker.defaultString = OS.Path.basename(target);
+
+ // Open the dialog and resolve/reject with the result.
+ return new Promise((resolve, reject) => {
+ picker.open(result => {
+ if (result === Ci.nsIFilePicker.returnCancel) {
+ reject({message: "Download canceled by the user"});
+ } else {
+ resolve(picker.file.path);
+ }
+ });
+ });
+ });
+ }
+
+ let download;
+ return Downloads.getPreferredDownloadsDirectory()
+ .then(downloadsDir => createTarget(downloadsDir))
+ .then(target => {
+ const source = {
+ url: options.url,
+ };
+
+ if (options.method || options.headers || options.body) {
+ source.adjustChannel = adjustChannel;
+ }
+
+ return Downloads.createDownload({
+ source,
+ target: {
+ path: target,
+ partFilePath: target + ".part",
+ },
+ });
+ }).then(dl => {
+ download = dl;
+ return DownloadMap.getDownloadList();
+ }).then(list => {
+ list.add(download);
+
+ // This is necessary to make pause/resume work.
+ download.tryToKeepPartialData = true;
+ download.start();
+
+ const item = DownloadMap.newFromDownload(download, extension);
+ return item.id;
+ });
+ },
+
+ removeFile(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id);
+ } catch (err) {
+ return Promise.reject({message: `Invalid download id ${id}`});
+ }
+ if (item.state !== "complete") {
+ return Promise.reject({message: `Cannot remove incomplete download id ${id}`});
+ }
+ return OS.File.remove(item.filename, {ignoreAbsent: false}).catch((err) => {
+ return Promise.reject({message: `Could not remove download id ${item.id} because the file doesn't exist`});
+ });
+ });
+ },
+
+ search(query) {
+ return queryHelper(query)
+ .then(items => items.map(item => item.serialize()));
+ },
+
+ pause(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id);
+ } catch (err) {
+ return Promise.reject({message: `Invalid download id ${id}`});
+ }
+ if (item.state != "in_progress") {
+ return Promise.reject({message: `Download ${id} cannot be paused since it is in state ${item.state}`});
+ }
+
+ return item.download.cancel();
+ });
+ },
+
+ resume(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id);
+ } catch (err) {
+ return Promise.reject({message: `Invalid download id ${id}`});
+ }
+ if (!item.canResume) {
+ return Promise.reject({message: `Download ${id} cannot be resumed`});
+ }
+
+ return item.download.start();
+ });
+ },
+
+ cancel(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id);
+ } catch (err) {
+ return Promise.reject({message: `Invalid download id ${id}`});
+ }
+ if (item.download.succeeded) {
+ return Promise.reject({message: `Download ${id} is already complete`});
+ }
+ return item.download.finalize(true);
+ });
+ },
+
+ showDefaultFolder() {
+ Downloads.getPreferredDownloadsDirectory().then(dir => {
+ let dirobj = new FileUtils.File(dir);
+ if (dirobj.isDirectory()) {
+ dirobj.launch();
+ } else {
+ throw new Error(`Download directory ${dirobj.path} is not actually a directory`);
+ }
+ }).catch(Cu.reportError);
+ },
+
+ erase(query) {
+ return queryHelper(query).then(items => {
+ let results = [];
+ let promises = [];
+ for (let item of items) {
+ promises.push(DownloadMap.erase(item));
+ results.push(item.id);
+ }
+ return Promise.all(promises).then(() => results);
+ });
+ },
+
+ open(downloadId) {
+ return DownloadMap.lazyInit().then(() => {
+ let download = DownloadMap.fromId(downloadId).download;
+ if (download.succeeded) {
+ return download.launch();
+ }
+ return Promise.reject({message: "Download has not completed."});
+ }).catch((error) => {
+ return Promise.reject({message: error.message});
+ });
+ },
+
+ show(downloadId) {
+ return DownloadMap.lazyInit().then(() => {
+ let download = DownloadMap.fromId(downloadId);
+ return download.download.showContainingDirectory();
+ }).then(() => {
+ return true;
+ }).catch(error => {
+ return Promise.reject({message: error.message});
+ });
+ },
+
+ getFileIcon(downloadId, options) {
+ return DownloadMap.lazyInit().then(() => {
+ let size = options && options.size ? options.size : 32;
+ let download = DownloadMap.fromId(downloadId).download;
+ let pathPrefix = "";
+ let path;
+
+ if (download.succeeded) {
+ let file = FileUtils.File(download.target.path);
+ path = Services.io.newFileURI(file).spec;
+ } else {
+ path = OS.Path.basename(download.target.path);
+ pathPrefix = "//";
+ }
+
+ return new Promise((resolve, reject) => {
+ let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+ chromeWebNav
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
+
+ let img = chromeWebNav.document.createElement("img");
+ img.width = size;
+ img.height = size;
+
+ let handleLoad;
+ let handleError;
+ const cleanup = () => {
+ img.removeEventListener("load", handleLoad);
+ img.removeEventListener("error", handleError);
+ chromeWebNav.close();
+ chromeWebNav = null;
+ };
+
+ handleLoad = () => {
+ let canvas = chromeWebNav.document.createElement("canvas");
+ canvas.width = size;
+ canvas.height = size;
+ let context = canvas.getContext("2d");
+ context.drawImage(img, 0, 0, size, size);
+ let dataURL = canvas.toDataURL("image/png");
+ cleanup();
+ resolve(dataURL);
+ };
+
+ handleError = (error) => {
+ Cu.reportError(error);
+ cleanup();
+ reject(new Error("An unexpected error occurred"));
+ };
+
+ img.addEventListener("load", handleLoad);
+ img.addEventListener("error", handleError);
+ img.src = `moz-icon:${pathPrefix}${path}?size=${size}`;
+ });
+ }).catch((error) => {
+ return Promise.reject({message: error.message});
+ });
+ },
+
+ // When we do setShelfEnabled(), check for additional "downloads.shelf" permission.
+ // i.e.:
+ // setShelfEnabled(enabled) {
+ // if (!extension.hasPermission("downloads.shelf")) {
+ // throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
+ // }
+ // ...
+ // }
+
+ onChanged: new SingletonEventManager(context, "downloads.onChanged", fire => {
+ const handler = (what, item) => {
+ let changes = {};
+ const noundef = val => (val === undefined) ? null : val;
+ DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
+ if (item[fld] != item.prechange[fld]) {
+ changes[fld] = {
+ previous: noundef(item.prechange[fld]),
+ current: noundef(item[fld]),
+ };
+ }
+ });
+ if (Object.keys(changes).length > 0) {
+ changes.id = item.id;
+ runSafeSync(context, fire, changes);
+ }
+ };
+
+ let registerPromise = DownloadMap.getDownloadList().then(() => {
+ DownloadMap.on("change", handler);
+ });
+ return () => {
+ registerPromise.then(() => {
+ DownloadMap.off("change", handler);
+ });
+ };
+ }).api(),
+
+ onCreated: new SingletonEventManager(context, "downloads.onCreated", fire => {
+ const handler = (what, item) => {
+ runSafeSync(context, fire, item.serialize());
+ };
+ let registerPromise = DownloadMap.getDownloadList().then(() => {
+ DownloadMap.on("create", handler);
+ });
+ return () => {
+ registerPromise.then(() => {
+ DownloadMap.off("create", handler);
+ });
+ };
+ }).api(),
+
+ onErased: new SingletonEventManager(context, "downloads.onErased", fire => {
+ const handler = (what, item) => {
+ runSafeSync(context, fire, item.id);
+ };
+ let registerPromise = DownloadMap.getDownloadList().then(() => {
+ DownloadMap.on("erase", handler);
+ });
+ return () => {
+ registerPromise.then(() => {
+ DownloadMap.off("erase", handler);
+ });
+ };
+ }).api(),
+
+ onDeterminingFilename: ignoreEvent(context, "downloads.onDeterminingFilename"),
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-extension.js b/toolkit/components/extensions/ext-extension.js
new file mode 100644
index 0000000000..c4bdc8b630
--- /dev/null
+++ b/toolkit/components/extensions/ext-extension.js
@@ -0,0 +1,20 @@
+"use strict";
+
+extensions.registerSchemaAPI("extension", "addon_parent", context => {
+ return {
+ extension: {
+ get lastError() {
+ return context.lastError;
+ },
+
+ isAllowedIncognitoAccess() {
+ return Promise.resolve(true);
+ },
+
+ isAllowedFileSchemeAccess() {
+ return Promise.resolve(false);
+ },
+ },
+ };
+});
+
diff --git a/toolkit/components/extensions/ext-i18n.js b/toolkit/components/extensions/ext-i18n.js
new file mode 100644
index 0000000000..bb4bde4bd5
--- /dev/null
+++ b/toolkit/components/extensions/ext-i18n.js
@@ -0,0 +1,34 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ detectLanguage,
+} = ExtensionUtils;
+
+function i18nApiFactory(context) {
+ let {extension} = context;
+ return {
+ i18n: {
+ getMessage: function(messageName, substitutions) {
+ return extension.localizeMessage(messageName, substitutions, {cloneScope: context.cloneScope});
+ },
+
+ getAcceptLanguages: function() {
+ let result = extension.localeData.acceptLanguages;
+ return Promise.resolve(result);
+ },
+
+ getUILanguage: function() {
+ return extension.localeData.uiLocale;
+ },
+
+ detectLanguage: function(text) {
+ return detectLanguage(text);
+ },
+ },
+ };
+}
+extensions.registerSchemaAPI("i18n", "addon_child", i18nApiFactory);
+extensions.registerSchemaAPI("i18n", "content_child", i18nApiFactory);
diff --git a/toolkit/components/extensions/ext-idle.js b/toolkit/components/extensions/ext-idle.js
new file mode 100644
index 0000000000..c5be4b600e
--- /dev/null
+++ b/toolkit/components/extensions/ext-idle.js
@@ -0,0 +1,94 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+XPCOMUtils.defineLazyServiceGetter(this, "idleService",
+ "@mozilla.org/widget/idleservice;1",
+ "nsIIdleService");
+const {
+ SingletonEventManager,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Object]
+var observersMap = new WeakMap();
+
+function getObserverInfo(extension, context) {
+ let observerInfo = observersMap.get(extension);
+ if (!observerInfo) {
+ observerInfo = {
+ observer: null,
+ detectionInterval: 60,
+ };
+ observersMap.set(extension, observerInfo);
+ context.callOnClose({
+ close: () => {
+ let {observer, detectionInterval} = observersMap.get(extension);
+ if (observer) {
+ idleService.removeIdleObserver(observer, detectionInterval);
+ }
+ observersMap.delete(extension);
+ },
+ });
+ }
+ return observerInfo;
+}
+
+function getObserver(extension, context) {
+ let observerInfo = getObserverInfo(extension, context);
+ let {observer, detectionInterval} = observerInfo;
+ if (!observer) {
+ observer = {
+ observe: function(subject, topic, data) {
+ if (topic == "idle" || topic == "active") {
+ this.emit("stateChanged", topic);
+ }
+ },
+ };
+ EventEmitter.decorate(observer);
+ idleService.addIdleObserver(observer, detectionInterval);
+ observerInfo.observer = observer;
+ observerInfo.detectionInterval = detectionInterval;
+ }
+ return observer;
+}
+
+function setDetectionInterval(extension, context, newInterval) {
+ let observerInfo = getObserverInfo(extension, context);
+ let {observer, detectionInterval} = observerInfo;
+ if (observer) {
+ idleService.removeIdleObserver(observer, detectionInterval);
+ idleService.addIdleObserver(observer, newInterval);
+ }
+ observerInfo.detectionInterval = newInterval;
+}
+
+extensions.registerSchemaAPI("idle", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ idle: {
+ queryState: function(detectionIntervalInSeconds) {
+ if (idleService.idleTime < detectionIntervalInSeconds * 1000) {
+ return Promise.resolve("active");
+ }
+ return Promise.resolve("idle");
+ },
+ setDetectionInterval: function(detectionIntervalInSeconds) {
+ setDetectionInterval(extension, context, detectionIntervalInSeconds);
+ },
+ onStateChanged: new SingletonEventManager(context, "idle.onStateChanged", fire => {
+ let listener = (event, data) => {
+ context.runSafe(fire, data);
+ };
+
+ getObserver(extension, context).on("stateChanged", listener);
+ return () => {
+ getObserver(extension, context).off("stateChanged", listener);
+ };
+ }).api(),
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-management.js b/toolkit/components/extensions/ext-management.js
new file mode 100644
index 0000000000..59a7959d78
--- /dev/null
+++ b/toolkit/components/extensions/ext-management.js
@@ -0,0 +1,109 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
+ const stringSvc = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
+ return stringSvc.createBundle("chrome://global/locale/extensions.properties");
+});
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "promptService",
+ "@mozilla.org/embedcomp/prompt-service;1",
+ "nsIPromptService");
+
+function _(key, ...args) {
+ if (args.length) {
+ return strBundle.formatStringFromName(key, args, args.length);
+ }
+ return strBundle.GetStringFromName(key);
+}
+
+function installType(addon) {
+ if (addon.temporarilyInstalled) {
+ return "development";
+ } else if (addon.foreignInstall) {
+ return "sideload";
+ } else if (addon.isSystem) {
+ return "other";
+ }
+ return "normal";
+}
+
+extensions.registerSchemaAPI("management", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ management: {
+ getSelf: function() {
+ return new Promise((resolve, reject) => AddonManager.getAddonByID(extension.id, addon => {
+ try {
+ let m = extension.manifest;
+ let extInfo = {
+ id: extension.id,
+ name: addon.name,
+ shortName: m.short_name || "",
+ description: addon.description || "",
+ version: addon.version,
+ mayDisable: !!(addon.permissions & AddonManager.PERM_CAN_DISABLE),
+ enabled: addon.isActive,
+ optionsUrl: addon.optionsURL || "",
+ permissions: Array.from(extension.permissions).filter(perm => {
+ return !extension.whiteListedHosts.pat.includes(perm);
+ }),
+ hostPermissions: extension.whiteListedHosts.pat,
+ installType: installType(addon),
+ };
+ if (addon.homepageURL) {
+ extInfo.homepageUrl = addon.homepageURL;
+ }
+ if (addon.updateURL) {
+ extInfo.updateUrl = addon.updateURL;
+ }
+ if (m.icons) {
+ extInfo.icons = Object.keys(m.icons).map(key => {
+ return {size: Number(key), url: m.icons[key]};
+ });
+ }
+
+ resolve(extInfo);
+ } catch (err) {
+ reject(err);
+ }
+ }));
+ },
+
+ uninstallSelf: function(options) {
+ return new Promise((resolve, reject) => {
+ if (options && options.showConfirmDialog) {
+ let message = _("uninstall.confirmation.message", extension.name);
+ if (options.dialogMessage) {
+ message = `${options.dialogMessage}\n${message}`;
+ }
+ let title = _("uninstall.confirmation.title", extension.name);
+ let buttonFlags = promptService.BUTTON_POS_0 * promptService.BUTTON_TITLE_IS_STRING +
+ promptService.BUTTON_POS_1 * promptService.BUTTON_TITLE_IS_STRING;
+ let button0Title = _("uninstall.confirmation.button-0.label");
+ let button1Title = _("uninstall.confirmation.button-1.label");
+ let response = promptService.confirmEx(null, title, message, buttonFlags, button0Title, button1Title, null, null, {value: 0});
+ if (response == 1) {
+ return reject({message: "User cancelled uninstall of extension"});
+ }
+ }
+ AddonManager.getAddonByID(extension.id, addon => {
+ let canUninstall = Boolean(addon.permissions & AddonManager.PERM_CAN_UNINSTALL);
+ if (!canUninstall) {
+ return reject({message: "The add-on cannot be uninstalled"});
+ }
+ try {
+ addon.uninstall();
+ } catch (err) {
+ return reject(err);
+ }
+ });
+ });
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-notifications.js b/toolkit/components/extensions/ext-notifications.js
new file mode 100644
index 0000000000..1df96a2ace
--- /dev/null
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -0,0 +1,161 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+var {
+ EventManager,
+ ignoreEvent,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> Map[id -> Notification]]
+var notificationsMap = new WeakMap();
+
+// Manages a notification popup (notifications API) created by the extension.
+function Notification(extension, id, options) {
+ this.extension = extension;
+ this.id = id;
+ this.options = options;
+
+ let imageURL;
+ if (options.iconUrl) {
+ imageURL = this.extension.baseURI.resolve(options.iconUrl);
+ }
+
+ try {
+ let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ svc.showAlertNotification(imageURL,
+ options.title,
+ options.message,
+ true, // textClickable
+ this.id,
+ this,
+ this.id);
+ } catch (e) {
+ // This will fail if alerts aren't available on the system.
+ }
+}
+
+Notification.prototype = {
+ clear() {
+ try {
+ let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ svc.closeAlert(this.id);
+ } catch (e) {
+ // This will fail if the OS doesn't support this function.
+ }
+ notificationsMap.get(this.extension).delete(this.id);
+ },
+
+ observe(subject, topic, data) {
+ let notifications = notificationsMap.get(this.extension);
+
+ let emitAndDelete = event => {
+ notifications.emit(event, data);
+ notifications.delete(this.id);
+ };
+
+ // Don't try to emit events if the extension has been unloaded
+ if (!notifications) {
+ return;
+ }
+
+ if (topic === "alertclickcallback") {
+ emitAndDelete("clicked");
+ }
+ if (topic === "alertfinished") {
+ emitAndDelete("closed");
+ }
+ },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("startup", (type, extension) => {
+ let map = new Map();
+ EventEmitter.decorate(map);
+ notificationsMap.set(extension, map);
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (notificationsMap.has(extension)) {
+ for (let notification of notificationsMap.get(extension).values()) {
+ notification.clear();
+ }
+ notificationsMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+var nextId = 0;
+
+extensions.registerSchemaAPI("notifications", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ notifications: {
+ create: function(notificationId, options) {
+ if (!notificationId) {
+ notificationId = String(nextId++);
+ }
+
+ let notifications = notificationsMap.get(extension);
+ if (notifications.has(notificationId)) {
+ notifications.get(notificationId).clear();
+ }
+
+ // FIXME: Lots of options still aren't supported, especially
+ // buttons.
+ let notification = new Notification(extension, notificationId, options);
+ notificationsMap.get(extension).set(notificationId, notification);
+
+ return Promise.resolve(notificationId);
+ },
+
+ clear: function(notificationId) {
+ let notifications = notificationsMap.get(extension);
+ if (notifications.has(notificationId)) {
+ notifications.get(notificationId).clear();
+ return Promise.resolve(true);
+ }
+ return Promise.resolve(false);
+ },
+
+ getAll: function() {
+ let result = {};
+ notificationsMap.get(extension).forEach((value, key) => {
+ result[key] = value.options;
+ });
+ return Promise.resolve(result);
+ },
+
+ onClosed: new EventManager(context, "notifications.onClosed", fire => {
+ let listener = (event, notificationId) => {
+ // FIXME: Support the byUser argument.
+ fire(notificationId, true);
+ };
+
+ notificationsMap.get(extension).on("closed", listener);
+ return () => {
+ notificationsMap.get(extension).off("closed", listener);
+ };
+ }).api(),
+
+ onClicked: new EventManager(context, "notifications.onClicked", fire => {
+ let listener = (event, notificationId) => {
+ fire(notificationId, true);
+ };
+
+ notificationsMap.get(extension).on("clicked", listener);
+ return () => {
+ notificationsMap.get(extension).off("clicked", listener);
+ };
+ }).api(),
+
+ // Intend to implement this later: https://bugzilla.mozilla.org/show_bug.cgi?id=1190681
+ onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"),
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-runtime.js b/toolkit/components/extensions/ext-runtime.js
new file mode 100644
index 0000000000..aed3ffd4b0
--- /dev/null
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -0,0 +1,134 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+
+var {
+ SingletonEventManager,
+} = ExtensionUtils;
+
+extensions.registerSchemaAPI("runtime", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ runtime: {
+ onStartup: new SingletonEventManager(context, "runtime.onStartup", fire => {
+ if (context.incognito) {
+ // This event should not fire if we are operating in a private profile.
+ return () => {};
+ }
+ let listener = () => {
+ if (extension.startupReason === "APP_STARTUP") {
+ fire();
+ }
+ };
+ extension.on("startup", listener);
+ return () => {
+ extension.off("startup", listener);
+ };
+ }).api(),
+
+ onInstalled: new SingletonEventManager(context, "runtime.onInstalled", fire => {
+ let listener = () => {
+ switch (extension.startupReason) {
+ case "APP_STARTUP":
+ if (Extension.browserUpdated) {
+ fire({reason: "browser_update"});
+ }
+ break;
+ case "ADDON_INSTALL":
+ fire({reason: "install"});
+ break;
+ case "ADDON_UPGRADE":
+ fire({reason: "update"});
+ break;
+ }
+ };
+ extension.on("startup", listener);
+ return () => {
+ extension.off("startup", listener);
+ };
+ }).api(),
+
+ onUpdateAvailable: new SingletonEventManager(context, "runtime.onUpdateAvailable", fire => {
+ let instanceID = extension.addonData.instanceID;
+ AddonManager.addUpgradeListener(instanceID, upgrade => {
+ extension.upgrade = upgrade;
+ let details = {
+ version: upgrade.version,
+ };
+ context.runSafe(fire, details);
+ });
+ return () => {
+ AddonManager.removeUpgradeListener(instanceID);
+ };
+ }).api(),
+
+ reload: () => {
+ if (extension.upgrade) {
+ // If there is a pending update, install it now.
+ extension.upgrade.install();
+ } else {
+ // Otherwise, reload the current extension.
+ AddonManager.getAddonByID(extension.id, addon => {
+ addon.reload();
+ });
+ }
+ },
+
+ get lastError() {
+ // TODO(robwu): Figure out how to make sure that errors in the parent
+ // process are propagated to the child process.
+ // lastError should not be accessed from the parent.
+ return context.lastError;
+ },
+
+ getBrowserInfo: function() {
+ const {name, vendor, version, appBuildID} = Services.appinfo;
+ const info = {name, vendor, version, buildID: appBuildID};
+ return Promise.resolve(info);
+ },
+
+ getPlatformInfo: function() {
+ return Promise.resolve(ExtensionUtils.PlatformInfo);
+ },
+
+ openOptionsPage: function() {
+ if (!extension.manifest.options_ui) {
+ return Promise.reject({message: "No `options_ui` declared"});
+ }
+
+ return openOptionsPage(extension).then(() => {});
+ },
+
+ setUninstallURL: function(url) {
+ if (url.length == 0) {
+ return Promise.resolve();
+ }
+
+ let uri;
+ try {
+ uri = NetUtil.newURI(url);
+ } catch (e) {
+ return Promise.reject({message: `Invalid URL: ${JSON.stringify(url)}`});
+ }
+
+ if (uri.scheme != "http" && uri.scheme != "https") {
+ return Promise.reject({message: "url must have the scheme http or https"});
+ }
+
+ extension.uninstallURL = url;
+ return Promise.resolve();
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-storage.js b/toolkit/components/extensions/ext-storage.js
new file mode 100644
index 0000000000..46d4fe13c9
--- /dev/null
+++ b/toolkit/components/extensions/ext-storage.js
@@ -0,0 +1,68 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
+ "resource://gre/modules/ExtensionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
+ "resource://gre/modules/ExtensionStorageSync.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ EventManager,
+} = ExtensionUtils;
+
+function storageApiFactory(context) {
+ let {extension} = context;
+ return {
+ storage: {
+ local: {
+ get: function(spec) {
+ return ExtensionStorage.get(extension.id, spec);
+ },
+ set: function(items) {
+ return ExtensionStorage.set(extension.id, items, context);
+ },
+ remove: function(keys) {
+ return ExtensionStorage.remove(extension.id, keys);
+ },
+ clear: function() {
+ return ExtensionStorage.clear(extension.id);
+ },
+ },
+
+ sync: {
+ get: function(spec) {
+ return ExtensionStorageSync.get(extension, spec, context);
+ },
+ set: function(items) {
+ return ExtensionStorageSync.set(extension, items, context);
+ },
+ remove: function(keys) {
+ return ExtensionStorageSync.remove(extension, keys, context);
+ },
+ clear: function() {
+ return ExtensionStorageSync.clear(extension, context);
+ },
+ },
+
+ onChanged: new EventManager(context, "storage.onChanged", fire => {
+ let listenerLocal = changes => {
+ fire(changes, "local");
+ };
+ let listenerSync = changes => {
+ fire(changes, "sync");
+ };
+
+ ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
+ ExtensionStorageSync.addOnChangedListener(extension, listenerSync, context);
+ return () => {
+ ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
+ ExtensionStorageSync.removeOnChangedListener(extension, listenerSync);
+ };
+ }).api(),
+ },
+ };
+}
+extensions.registerSchemaAPI("storage", "addon_parent", storageApiFactory);
+extensions.registerSchemaAPI("storage", "content_parent", storageApiFactory);
diff --git a/toolkit/components/extensions/ext-topSites.js b/toolkit/components/extensions/ext-topSites.js
new file mode 100644
index 0000000000..a66ac85d99
--- /dev/null
+++ b/toolkit/components/extensions/ext-topSites.js
@@ -0,0 +1,24 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
+ "resource://gre/modules/NewTabUtils.jsm");
+
+extensions.registerSchemaAPI("topSites", "addon_parent", context => {
+ return {
+ topSites: {
+ get: function() {
+ let urls = NewTabUtils.links.getLinks()
+ .filter(link => !!link)
+ .map(link => {
+ return {
+ url: link.url,
+ title: link.title,
+ };
+ });
+ return Promise.resolve(urls);
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-webNavigation.js b/toolkit/components/extensions/ext-webNavigation.js
new file mode 100644
index 0000000000..904f3a4a78
--- /dev/null
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -0,0 +1,192 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MatchURLFilters",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebNavigation",
+ "resource://gre/modules/WebNavigation.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ SingletonEventManager,
+ ignoreEvent,
+} = ExtensionUtils;
+
+const defaultTransitionTypes = {
+ topFrame: "link",
+ subFrame: "auto_subframe",
+};
+
+const frameTransitions = {
+ anyFrame: {
+ qualifiers: ["server_redirect", "client_redirect", "forward_back"],
+ },
+ topFrame: {
+ types: ["reload", "form_submit"],
+ },
+};
+
+const tabTransitions = {
+ topFrame: {
+ qualifiers: ["from_address_bar"],
+ types: ["auto_bookmark", "typed", "keyword", "generated", "link"],
+ },
+ subFrame: {
+ types: ["manual_subframe"],
+ },
+};
+
+function isTopLevelFrame({frameId, parentFrameId}) {
+ return frameId == 0 && parentFrameId == -1;
+}
+
+function fillTransitionProperties(eventName, src, dst) {
+ if (eventName == "onCommitted" || eventName == "onHistoryStateUpdated") {
+ let frameTransitionData = src.frameTransitionData || {};
+ let tabTransitionData = src.tabTransitionData || {};
+
+ let transitionType, transitionQualifiers = [];
+
+ // Fill transition properties for any frame.
+ for (let qualifier of frameTransitions.anyFrame.qualifiers) {
+ if (frameTransitionData[qualifier]) {
+ transitionQualifiers.push(qualifier);
+ }
+ }
+
+ if (isTopLevelFrame(dst)) {
+ for (let type of frameTransitions.topFrame.types) {
+ if (frameTransitionData[type]) {
+ transitionType = type;
+ }
+ }
+
+ for (let qualifier of tabTransitions.topFrame.qualifiers) {
+ if (tabTransitionData[qualifier]) {
+ transitionQualifiers.push(qualifier);
+ }
+ }
+
+ for (let type of tabTransitions.topFrame.types) {
+ if (tabTransitionData[type]) {
+ transitionType = type;
+ }
+ }
+
+ // If transitionType is not defined, defaults it to "link".
+ if (!transitionType) {
+ transitionType = defaultTransitionTypes.topFrame;
+ }
+ } else {
+ // If it is sub-frame, transitionType defaults it to "auto_subframe",
+ // "manual_subframe" is set only in case of a recent user interaction.
+ transitionType = tabTransitionData.link ?
+ "manual_subframe" : defaultTransitionTypes.subFrame;
+ }
+
+ // Fill the transition properties in the webNavigation event object.
+ dst.transitionType = transitionType;
+ dst.transitionQualifiers = transitionQualifiers;
+ }
+}
+
+// Similar to WebRequestEventManager but for WebNavigation.
+function WebNavigationEventManager(context, eventName) {
+ let name = `webNavigation.${eventName}`;
+ let register = (callback, urlFilters) => {
+ // Don't create a MatchURLFilters instance if the listener does not include any filter.
+ let filters = urlFilters ?
+ new MatchURLFilters(urlFilters.url) : null;
+
+ let listener = data => {
+ if (!data.browser) {
+ return;
+ }
+
+ let data2 = {
+ url: data.url,
+ timeStamp: Date.now(),
+ frameId: ExtensionManagement.getFrameId(data.windowId),
+ parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
+ };
+
+ if (eventName == "onErrorOccurred") {
+ data2.error = data.error;
+ }
+
+ // Fills in tabId typically.
+ extensions.emit("fill-browser-data", data.browser, data2);
+ if (data2.tabId < 0) {
+ return;
+ }
+
+ fillTransitionProperties(eventName, data, data2);
+
+ context.runSafe(callback, data2);
+ };
+
+ WebNavigation[eventName].addListener(listener, filters);
+ return () => {
+ WebNavigation[eventName].removeListener(listener);
+ };
+ };
+
+ return SingletonEventManager.call(this, context, name, register);
+}
+
+WebNavigationEventManager.prototype = Object.create(SingletonEventManager.prototype);
+
+function convertGetFrameResult(tabId, data) {
+ return {
+ errorOccurred: data.errorOccurred,
+ url: data.url,
+ tabId,
+ frameId: ExtensionManagement.getFrameId(data.windowId),
+ parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
+ };
+}
+
+extensions.registerSchemaAPI("webNavigation", "addon_parent", context => {
+ return {
+ webNavigation: {
+ onTabReplaced: ignoreEvent(context, "webNavigation.onTabReplaced"),
+ onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
+ onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
+ onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
+ onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
+ onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
+ onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
+ onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
+ onCreatedNavigationTarget: ignoreEvent(context, "webNavigation.onCreatedNavigationTarget"),
+ getAllFrames(details) {
+ let tab = TabManager.getTab(details.tabId, context);
+
+ let {innerWindowID, messageManager} = tab.linkedBrowser;
+ let recipient = {innerWindowID};
+
+ return context.sendMessage(messageManager, "WebNavigation:GetAllFrames", {}, {recipient})
+ .then((results) => results.map(convertGetFrameResult.bind(null, details.tabId)));
+ },
+ getFrame(details) {
+ let tab = TabManager.getTab(details.tabId, context);
+
+ let recipient = {
+ innerWindowID: tab.linkedBrowser.innerWindowID,
+ };
+
+ let mm = tab.linkedBrowser.messageManager;
+ return context.sendMessage(mm, "WebNavigation:GetFrame", {options: details}, {recipient})
+ .then((result) => {
+ return result ?
+ convertGetFrameResult(details.tabId, result) :
+ Promise.reject({message: `No frame found with frameId: ${details.frameId}`});
+ });
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/ext-webRequest.js b/toolkit/components/extensions/ext-webRequest.js
new file mode 100644
index 0000000000..f92330131d
--- /dev/null
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -0,0 +1,115 @@
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
+ "resource://gre/modules/WebRequest.jsm");
+
+Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+var {
+ SingletonEventManager,
+} = ExtensionUtils;
+
+// EventManager-like class specifically for WebRequest. Inherits from
+// SingletonEventManager. Takes care of converting |details| parameter
+// when invoking listeners.
+function WebRequestEventManager(context, eventName) {
+ let name = `webRequest.${eventName}`;
+ let register = (callback, filter, info) => {
+ let listener = data => {
+ // Prevent listening in on requests originating from system principal to
+ // prevent tinkering with OCSP, app and addon updates, etc.
+ if (data.isSystemPrincipal) {
+ return;
+ }
+
+ let data2 = {
+ requestId: data.requestId,
+ url: data.url,
+ originUrl: data.originUrl,
+ method: data.method,
+ type: data.type,
+ timeStamp: Date.now(),
+ frameId: data.type == "main_frame" ? 0 : ExtensionManagement.getFrameId(data.windowId),
+ parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
+ };
+
+ const maybeCached = ["onResponseStarted", "onBeforeRedirect", "onCompleted", "onErrorOccurred"];
+ if (maybeCached.includes(eventName)) {
+ data2.fromCache = !!data.fromCache;
+ }
+
+ if ("ip" in data) {
+ data2.ip = data.ip;
+ }
+
+ extensions.emit("fill-browser-data", data.browser, data2);
+
+ let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
+ "requestBody"];
+ for (let opt of optional) {
+ if (opt in data) {
+ data2[opt] = data[opt];
+ }
+ }
+
+ return context.runSafe(callback, data2);
+ };
+
+ let filter2 = {};
+ filter2.urls = new MatchPattern(filter.urls);
+ if (filter.types) {
+ filter2.types = filter.types;
+ }
+ if (filter.tabId) {
+ filter2.tabId = filter.tabId;
+ }
+ if (filter.windowId) {
+ filter2.windowId = filter.windowId;
+ }
+
+ let info2 = [];
+ if (info) {
+ for (let desc of info) {
+ if (desc == "blocking" && !context.extension.hasPermission("webRequestBlocking")) {
+ Cu.reportError("Using webRequest.addListener with the blocking option " +
+ "requires the 'webRequestBlocking' permission.");
+ } else {
+ info2.push(desc);
+ }
+ }
+ }
+
+ WebRequest[eventName].addListener(listener, filter2, info2);
+ return () => {
+ WebRequest[eventName].removeListener(listener);
+ };
+ };
+
+ return SingletonEventManager.call(this, context, name, register);
+}
+
+WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);
+
+extensions.registerSchemaAPI("webRequest", "addon_parent", context => {
+ return {
+ webRequest: {
+ onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
+ onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
+ onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
+ onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
+ onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
+ onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
+ onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(),
+ onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
+ handlerBehaviorChanged: function() {
+ // TODO: Flush all caches.
+ },
+ },
+ };
+});
diff --git a/toolkit/components/extensions/extensions-toolkit.manifest b/toolkit/components/extensions/extensions-toolkit.manifest
new file mode 100644
index 0000000000..4ec65a9844
--- /dev/null
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -0,0 +1,49 @@
+# scripts
+category webextension-scripts alarms chrome://extensions/content/ext-alarms.js
+category webextension-scripts backgroundPage chrome://extensions/content/ext-backgroundPage.js
+category webextension-scripts cookies chrome://extensions/content/ext-cookies.js
+category webextension-scripts downloads chrome://extensions/content/ext-downloads.js
+category webextension-scripts management chrome://extensions/content/ext-management.js
+category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
+category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
+category webextension-scripts idle chrome://extensions/content/ext-idle.js
+category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
+category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
+category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
+category webextension-scripts extension chrome://extensions/content/ext-extension.js
+category webextension-scripts storage chrome://extensions/content/ext-storage.js
+category webextension-scripts topSites chrome://extensions/content/ext-topSites.js
+
+# scripts specific for content process.
+category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js
+category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js
+category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
+category webextension-scripts-content test chrome://extensions/content/ext-c-test.js
+category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js
+
+# scripts that must run in the same process as addon code.
+category webextension-scripts-addon backgroundPage chrome://extensions/content/ext-c-backgroundPage.js
+category webextension-scripts-addon extension chrome://extensions/content/ext-c-extension.js
+category webextension-scripts-addon i18n chrome://extensions/content/ext-i18n.js
+category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
+category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
+category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
+
+# schemas
+category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
+category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
+category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
+category webextension-schemas events chrome://extensions/content/schemas/events.json
+category webextension-schemas extension chrome://extensions/content/schemas/extension.json
+category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
+category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
+category webextension-schemas idle chrome://extensions/content/schemas/idle.json
+category webextension-schemas management chrome://extensions/content/schemas/management.json
+category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
+category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
+category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json
+category webextension-schemas storage chrome://extensions/content/schemas/storage.json
+category webextension-schemas test chrome://extensions/content/schemas/test.json
+category webextension-schemas top_sites chrome://extensions/content/schemas/top_sites.json
+category webextension-schemas web_navigation chrome://extensions/content/schemas/web_navigation.json
+category webextension-schemas web_request chrome://extensions/content/schemas/web_request.json
diff --git a/toolkit/components/extensions/jar.mn b/toolkit/components/extensions/jar.mn
new file mode 100644
index 0000000000..6d343e1b74
--- /dev/null
+++ b/toolkit/components/extensions/jar.mn
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 extensions %content/extensions/
+ content/extensions/ext-alarms.js
+ content/extensions/ext-backgroundPage.js
+ content/extensions/ext-browser-content.js
+ content/extensions/ext-cookies.js
+ content/extensions/ext-downloads.js
+ content/extensions/ext-management.js
+ content/extensions/ext-notifications.js
+ content/extensions/ext-i18n.js
+ content/extensions/ext-idle.js
+ content/extensions/ext-webRequest.js
+ content/extensions/ext-webNavigation.js
+ content/extensions/ext-runtime.js
+ content/extensions/ext-extension.js
+ content/extensions/ext-storage.js
+ content/extensions/ext-topSites.js
+ content/extensions/ext-c-backgroundPage.js
+ content/extensions/ext-c-extension.js
+ content/extensions/ext-c-runtime.js
+ content/extensions/ext-c-storage.js
+ content/extensions/ext-c-test.js
diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build
new file mode 100644
index 0000000000..f22a4b5d0c
--- /dev/null
+++ b/toolkit/components/extensions/moz.build
@@ -0,0 +1,42 @@
+# -*- 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_JS_MODULES += [
+ 'Extension.jsm',
+ 'ExtensionAPI.jsm',
+ 'ExtensionChild.jsm',
+ 'ExtensionCommon.jsm',
+ 'ExtensionContent.jsm',
+ 'ExtensionManagement.jsm',
+ 'ExtensionParent.jsm',
+ 'ExtensionStorage.jsm',
+ 'ExtensionStorageSync.jsm',
+ 'ExtensionUtils.jsm',
+ 'LegacyExtensionsUtils.jsm',
+ 'MessageChannel.jsm',
+ 'NativeMessaging.jsm',
+ 'Schemas.jsm',
+]
+
+EXTRA_COMPONENTS += [
+ 'extensions-toolkit.manifest',
+]
+
+TESTING_JS_MODULES += [
+ 'ExtensionTestCommon.jsm',
+ 'ExtensionXPCShellUtils.jsm',
+]
+
+DIRS += ['schemas']
+
+JAR_MANIFESTS += ['jar.mn']
+
+MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += [
+ 'test/xpcshell/native_messaging.ini',
+ 'test/xpcshell/xpcshell.ini',
+]
diff --git a/toolkit/components/extensions/schemas/LICENSE b/toolkit/components/extensions/schemas/LICENSE
new file mode 100644
index 0000000000..9314092fdc
--- /dev/null
+++ b/toolkit/components/extensions/schemas/LICENSE
@@ -0,0 +1,27 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/toolkit/components/extensions/schemas/alarms.json b/toolkit/components/extensions/schemas/alarms.json
new file mode 100644
index 0000000000..2a72a28425
--- /dev/null
+++ b/toolkit/components/extensions/schemas/alarms.json
@@ -0,0 +1,145 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "alarms",
+ "permissions": ["alarms"],
+ "types": [
+ {
+ "id": "Alarm",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of this alarm."
+ },
+ "scheduledTime": {
+ "type": "number",
+ "description": "Time when the alarm is scheduled to fire, in milliseconds past the epoch."
+ },
+ "periodInMinutes": {
+ "type": "number",
+ "optional": true,
+ "description": "When present, signals that the alarm triggers periodically after so many minutes."
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "description": "Creates an alarm. After the delay is expired, the onAlarm event is fired. If there is another alarm with the same name (or no name if none is specified), it will be cancelled and replaced by this alarm.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "name",
+ "optional": true,
+ "description": "Optional name to identify this alarm. Defaults to the empty string."
+ },
+ {
+ "type": "object",
+ "name": "alarmInfo",
+ "description": "Details about the alarm. The alarm first fires either at 'when' milliseconds past the epoch (if 'when' is provided), after 'delayInMinutes' minutes from the current time (if 'delayInMinutes' is provided instead), or after 'periodInMinutes' minutes from the current time (if only 'periodInMinutes' is provided). Users should never provide both 'when' and 'delayInMinutes'. If 'periodInMinutes' is provided, then the alarm recurs repeatedly after that many minutes.",
+ "properties": {
+ "when": {"type": "number", "optional": true,
+ "description": "Time when the alarm is scheduled to first fire, in milliseconds past the epoch."},
+ "delayInMinutes": {"type": "number", "optional": true,
+ "description": "Number of minutes from the current time after which the alarm should first fire."},
+ "periodInMinutes": {"type": "number", "optional": true,
+ "description": "Number of minutes after which the alarm should recur repeatedly."}
+ }
+ }
+ ]
+ },
+ {
+ "name": "get",
+ "type": "function",
+ "description": "Retrieves details about the specified alarm.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "name",
+ "optional": true,
+ "description": "The name of the alarm to get. Defaults to the empty string."
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ { "name": "alarm", "$ref": "Alarm" }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getAll",
+ "type": "function",
+ "description": "Gets an array of all the alarms.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ { "name": "alarms", "type": "array", "items": { "$ref": "Alarm" } }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "clear",
+ "type": "function",
+ "description": "Clears the alarm with the given name.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "name",
+ "optional": true,
+ "description": "The name of the alarm to clear. Defaults to the empty string."
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ { "name": "wasCleared", "type": "boolean", "description": "Whether an alarm of the given name was found to clear." }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "clearAll",
+ "type": "function",
+ "description": "Clears all alarms.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ { "name": "wasCleared", "type": "boolean", "description": "Whether any alarm was found to clear." }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onAlarm",
+ "type": "function",
+ "description": "Fired when an alarm has expired. Useful for transient background pages.",
+ "parameters": [
+ {
+ "name": "name",
+ "$ref": "Alarm",
+ "description": "The alarm that has expired."
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/cookies.json b/toolkit/components/extensions/schemas/cookies.json
new file mode 100644
index 0000000000..a7de6eb426
--- /dev/null
+++ b/toolkit/components/extensions/schemas/cookies.json
@@ -0,0 +1,224 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "cookies"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "cookies",
+ "description": "Use the <code>browser.cookies</code> API to query and modify cookies, and to be notified when they change.",
+ "permissions": ["cookies"],
+ "types": [
+ {
+ "id": "Cookie",
+ "type": "object",
+ "description": "Represents information about an HTTP cookie.",
+ "properties": {
+ "name": {"type": "string", "description": "The name of the cookie."},
+ "value": {"type": "string", "description": "The value of the cookie."},
+ "domain": {"type": "string", "description": "The domain of the cookie (e.g. \"www.google.com\", \"example.com\")."},
+ "hostOnly": {"type": "boolean", "description": "True if the cookie is a host-only cookie (i.e. a request's host must exactly match the domain of the cookie)."},
+ "path": {"type": "string", "description": "The path of the cookie."},
+ "secure": {"type": "boolean", "description": "True if the cookie is marked as Secure (i.e. its scope is limited to secure channels, typically HTTPS)."},
+ "httpOnly": {"type": "boolean", "description": "True if the cookie is marked as HttpOnly (i.e. the cookie is inaccessible to client-side scripts)."},
+ "session": {"type": "boolean", "description": "True if the cookie is a session cookie, as opposed to a persistent cookie with an expiration date."},
+ "expirationDate": {"type": "number", "optional": true, "description": "The expiration date of the cookie as the number of seconds since the UNIX epoch. Not provided for session cookies."},
+ "storeId": {"type": "string", "description": "The ID of the cookie store containing this cookie, as provided in getAllCookieStores()."}
+ }
+ },
+ {
+ "id": "CookieStore",
+ "type": "object",
+ "description": "Represents a cookie store in the browser. An incognito mode window, for instance, uses a separate cookie store from a non-incognito window.",
+ "properties": {
+ "id": {"type": "string", "description": "The unique identifier for the cookie store."},
+ "tabIds": {"type": "array", "items": {"type": "integer"}, "description": "Identifiers of all the browser tabs that share this cookie store."}
+ }
+ },
+ {
+ "id": "OnChangedCause",
+ "type": "string",
+ "enum": ["evicted", "expired", "explicit", "expired_overwrite", "overwrite"],
+ "description": "The underlying reason behind the cookie's change. If a cookie was inserted, or removed via an explicit call to $(ref:cookies.remove), \"cause\" will be \"explicit\". If a cookie was automatically removed due to expiry, \"cause\" will be \"expired\". If a cookie was removed due to being overwritten with an already-expired expiration date, \"cause\" will be set to \"expired_overwrite\". If a cookie was automatically removed due to garbage collection, \"cause\" will be \"evicted\". If a cookie was automatically removed due to a \"set\" call that overwrote it, \"cause\" will be \"overwrite\". Plan your response accordingly."
+ }
+ ],
+ "functions": [
+ {
+ "name": "get",
+ "type": "function",
+ "description": "Retrieves information about a single cookie. If more than one cookie of the same name exists for the given URL, the one with the longest path will be returned. For cookies with the same path length, the cookie with the earliest creation time will be returned.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "description": "Details to identify the cookie being retrieved.",
+ "properties": {
+ "url": {"type": "string", "description": "The URL with which the cookie to retrieve is associated. This argument may be a full URL, in which case any data following the URL path (e.g. the query string) is simply ignored. If host permissions for this URL are not specified in the manifest file, the API call will fail."},
+ "name": {"type": "string", "description": "The name of the cookie to retrieve."},
+ "storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store in which to look for the cookie. By default, the current execution context's cookie store will be used."}
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "cookie", "$ref": "Cookie", "optional": true, "description": "Contains details about the cookie. This parameter is null if no such cookie was found."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getAll",
+ "type": "function",
+ "description": "Retrieves all cookies from a single cookie store that match the given information. The cookies returned will be sorted, with those with the longest path first. If multiple cookies have the same path length, those with the earliest creation time will be first.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "description": "Information to filter the cookies being retrieved.",
+ "properties": {
+ "url": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those that would match the given URL."},
+ "name": {"type": "string", "optional": true, "description": "Filters the cookies by name."},
+ "domain": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."},
+ "path": {"type": "string", "optional": true, "description": "Restricts the retrieved cookies to those whose path exactly matches this string."},
+ "secure": {"type": "boolean", "optional": true, "description": "Filters the cookies by their Secure property."},
+ "session": {"type": "boolean", "optional": true, "description": "Filters out session vs. persistent cookies."},
+ "storeId": {"type": "string", "optional": true, "description": "The cookie store to retrieve cookies from. If omitted, the current execution context's cookie store will be used."}
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "cookies", "type": "array", "items": {"$ref": "Cookie"}, "description": "All the existing, unexpired cookies that match the given cookie info."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "set",
+ "type": "function",
+ "description": "Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "description": "Details about the cookie being set.",
+ "properties": {
+ "url": {"type": "string", "description": "The request-URI to associate with the setting of the cookie. This value can affect the default domain and path values of the created cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail."},
+ "name": {"type": "string", "optional": true, "description": "The name of the cookie. Empty by default if omitted."},
+ "value": {"type": "string", "optional": true, "description": "The value of the cookie. Empty by default if omitted."},
+ "domain": {"type": "string", "optional": true, "description": "The domain of the cookie. If omitted, the cookie becomes a host-only cookie."},
+ "path": {"type": "string", "optional": true, "description": "The path of the cookie. Defaults to the path portion of the url parameter."},
+ "secure": {"type": "boolean", "optional": true, "description": "Whether the cookie should be marked as Secure. Defaults to false."},
+ "httpOnly": {"type": "boolean", "optional": true, "description": "Whether the cookie should be marked as HttpOnly. Defaults to false."},
+ "expirationDate": {"type": "number", "optional": true, "description": "The expiration date of the cookie as the number of seconds since the UNIX epoch. If omitted, the cookie becomes a session cookie."},
+ "storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store in which to set the cookie. By default, the cookie is set in the current execution context's cookie store."}
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "cookie", "$ref": "Cookie", "optional": true, "description": "Contains details about the cookie that's been set. If setting failed for any reason, this will be \"null\", and $(ref:runtime.lastError) will be set."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "remove",
+ "type": "function",
+ "description": "Deletes a cookie by name.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "description": "Information to identify the cookie to remove.",
+ "properties": {
+ "url": {"type": "string", "description": "The URL associated with the cookie. If host permissions for this URL are not specified in the manifest file, the API call will fail."},
+ "name": {"type": "string", "description": "The name of the cookie to remove."},
+ "storeId": {"type": "string", "optional": true, "description": "The ID of the cookie store to look in for the cookie. If unspecified, the cookie is looked for by default in the current execution context's cookie store."}
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "description": "Contains details about the cookie that's been removed. If removal failed for any reason, this will be \"null\", and $(ref:runtime.lastError) will be set.",
+ "optional": true,
+ "properties": {
+ "url": {"type": "string", "description": "The URL associated with the cookie that's been removed."},
+ "name": {"type": "string", "description": "The name of the cookie that's been removed."},
+ "storeId": {"type": "string", "description": "The ID of the cookie store from which the cookie was removed."}
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getAllCookieStores",
+ "type": "function",
+ "description": "Lists all existing cookie stores.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "cookieStores", "type": "array", "items": {"$ref": "CookieStore"}, "description": "All the existing cookie stores."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onChanged",
+ "type": "function",
+ "description": "Fired when a cookie is set or removed. As a special case, note that updating a cookie's properties is implemented as a two step process: the cookie to be updated is first removed entirely, generating a notification with \"cause\" of \"overwrite\" . Afterwards, a new cookie is written with the updated values, generating a second notification with \"cause\" \"explicit\".",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "changeInfo",
+ "properties": {
+ "removed": {"type": "boolean", "description": "True if a cookie was removed."},
+ "cookie": {"$ref": "Cookie", "description": "Information about the cookie that was set or removed."},
+ "cause": {"$ref": "OnChangedCause", "description": "The underlying reason behind the cookie's change."}
+ }
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/downloads.json b/toolkit/components/extensions/schemas/downloads.json
new file mode 100644
index 0000000000..dcd43e4e15
--- /dev/null
+++ b/toolkit/components/extensions/schemas/downloads.json
@@ -0,0 +1,793 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "downloads",
+ "downloads.open",
+ "downloads.shelf"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "downloads",
+ "permissions": ["downloads"],
+ "types": [
+ {
+ "id": "FilenameConflictAction",
+ "type": "string",
+ "enum": [
+ "uniquify",
+ "overwrite",
+ "prompt"
+ ]
+ },
+ {
+ "id": "InterruptReason",
+ "type": "string",
+ "enum": [
+ "FILE_FAILED",
+ "FILE_ACCESS_DENIED",
+ "FILE_NO_SPACE",
+ "FILE_NAME_TOO_LONG",
+ "FILE_TOO_LARGE",
+ "FILE_VIRUS_INFECTED",
+ "FILE_TRANSIENT_ERROR",
+ "FILE_BLOCKED",
+ "FILE_SECURITY_CHECK_FAILED",
+ "FILE_TOO_SHORT",
+ "NETWORK_FAILED",
+ "NETWORK_TIMEOUT",
+ "NETWORK_DISCONNECTED",
+ "NETWORK_SERVER_DOWN",
+ "NETWORK_INVALID_REQUEST",
+ "SERVER_FAILED",
+ "SERVER_NO_RANGE",
+ "SERVER_BAD_CONTENT",
+ "SERVER_UNAUTHORIZED",
+ "SERVER_CERT_PROBLEM",
+ "SERVER_FORBIDDEN",
+ "USER_CANCELED",
+ "USER_SHUTDOWN",
+ "CRASH"
+ ]
+ },
+ {
+ "id": "DangerType",
+ "type": "string",
+ "enum": [
+ "file",
+ "url",
+ "content",
+ "uncommon",
+ "host",
+ "unwanted",
+ "safe",
+ "accepted"
+ ],
+ "description": "<dl><dt>file</dt><dd>The download's filename is suspicious.</dd><dt>url</dt><dd>The download's URL is known to be malicious.</dd><dt>content</dt><dd>The downloaded file is known to be malicious.</dd><dt>uncommon</dt><dd>The download's URL is not commonly downloaded and could be dangerous.</dd><dt>safe</dt><dd>The download presents no known danger to the user's computer.</dd></dl>These string constants will never change, however the set of DangerTypes may change."
+ },
+ {
+ "id": "State",
+ "type": "string",
+ "enum": [
+ "in_progress",
+ "interrupted",
+ "complete"
+ ],
+ "description": "<dl><dt>in_progress</dt><dd>The download is currently receiving data from the server.</dd><dt>interrupted</dt><dd>An error broke the connection with the file host.</dd><dt>complete</dt><dd>The download completed successfully.</dd></dl>These string constants will never change, however the set of States may change."
+ },
+ {
+ "id": "DownloadItem",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "An identifier that is persistent across browser sessions.",
+ "type": "integer"
+ },
+ "url": {
+ "description": "Absolute URL.",
+ "type": "string"
+ },
+ "referrer": {
+ "type": "string"
+ },
+ "filename": {
+ "description": "Absolute local path.",
+ "type": "string"
+ },
+ "incognito": {
+ "description": "False if this download is recorded in the history, true if it is not recorded.",
+ "type": "boolean"
+ },
+ "danger": {
+ "$ref": "DangerType",
+ "description": "Indication of whether this download is thought to be safe or known to be suspicious."
+ },
+ "mime": {
+ "description": "The file's MIME type.",
+ "type": "string"
+ },
+ "startTime": {
+ "description": "Number of milliseconds between the unix epoch and when this download began.",
+ "type": "string"
+ },
+ "endTime": {
+ "description": "Number of milliseconds between the unix epoch and when this download ended.",
+ "optional": true,
+ "type": "string"
+ },
+ "estimatedEndTime": {
+ "type": "string",
+ "optional": true
+ },
+ "state": {
+ "$ref": "State",
+ "description": "Indicates whether the download is progressing, interrupted, or complete."
+ },
+ "paused": {
+ "description": "True if the download has stopped reading data from the host, but kept the connection open.",
+ "type": "boolean"
+ },
+ "canResume": {
+ "type": "boolean"
+ },
+ "error": {
+ "description": "Number indicating why a download was interrupted.",
+ "optional": true,
+ "$ref": "InterruptReason"
+ },
+ "bytesReceived": {
+ "description": "Number of bytes received so far from the host, without considering file compression.",
+ "type": "number"
+ },
+ "totalBytes": {
+ "description": "Number of bytes in the whole file, without considering file compression, or -1 if unknown.",
+ "type": "number"
+ },
+ "fileSize": {
+ "description": "Number of bytes in the whole file post-decompression, or -1 if unknown.",
+ "type": "number"
+ },
+ "exists": {
+ "type": "boolean"
+ },
+ "byExtensionId": {
+ "type": "string",
+ "optional": true
+ },
+ "byExtensionName": {
+ "type": "string",
+ "optional": true
+ }
+ }
+ },
+ {
+ "id": "StringDelta",
+ "type": "object",
+ "properties": {
+ "current": {
+ "optional": true,
+ "type": "string"
+ },
+ "previous": {
+ "optional": true,
+ "type": "string"
+ }
+ }
+ },
+ {
+ "id": "DoubleDelta",
+ "type": "object",
+ "properties": {
+ "current": {
+ "optional": true,
+ "type": "number"
+ },
+ "previous": {
+ "optional": true,
+ "type": "number"
+ }
+ }
+ },
+ {
+ "id": "BooleanDelta",
+ "type": "object",
+ "properties": {
+ "current": {
+ "optional": true,
+ "type": "boolean"
+ },
+ "previous": {
+ "optional": true,
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "id": "DownloadTime",
+ "description": "A time specified as a Date object, a number or string representing milliseconds since the epoch, or an ISO 8601 string",
+ "choices": [
+ {
+ "type": "string",
+ "pattern": "^[1-9]\\d*$"
+ },
+ {
+ "$ref": "extensionTypes.Date"
+ }
+ ]
+ },
+ {
+ "id": "DownloadQuery",
+ "description": "Parameters that combine to specify a predicate that can be used to select a set of downloads. Used for example in search() and erase()",
+ "type": "object",
+ "properties": {
+ "query": {
+ "description": "This array of search terms limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> or <code>url</code> contain all of the search terms that do not begin with a dash '-' and none of the search terms that do begin with a dash.",
+ "optional": true,
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "startedBefore": {
+ "description": "Limits results to downloads that started before the given ms since the epoch.",
+ "optional": true,
+ "$ref": "DownloadTime"
+ },
+ "startedAfter": {
+ "description": "Limits results to downloads that started after the given ms since the epoch.",
+ "optional": true,
+ "$ref": "DownloadTime"
+ },
+ "endedBefore": {
+ "description": "Limits results to downloads that ended before the given ms since the epoch.",
+ "optional": true,
+ "$ref": "DownloadTime"
+ },
+ "endedAfter": {
+ "description": "Limits results to downloads that ended after the given ms since the epoch.",
+ "optional": true,
+ "$ref": "DownloadTime"
+ },
+ "totalBytesGreater": {
+ "description": "Limits results to downloads whose totalBytes is greater than the given integer.",
+ "optional": true,
+ "type": "number"
+ },
+ "totalBytesLess": {
+ "description": "Limits results to downloads whose totalBytes is less than the given integer.",
+ "optional": true,
+ "type": "number"
+ },
+ "filenameRegex": {
+ "description": "Limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> matches the given regular expression.",
+ "optional": true,
+ "type": "string"
+ },
+ "urlRegex": {
+ "description": "Limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>url</code> matches the given regular expression.",
+ "optional": true,
+ "type": "string"
+ },
+ "limit": {
+ "description": "Setting this integer limits the number of results. Otherwise, all matching <a href='#type-DownloadItem'>DownloadItems</a> will be returned.",
+ "optional": true,
+ "type": "integer"
+ },
+ "orderBy": {
+ "description": "Setting elements of this array to <a href='#type-DownloadItem'>DownloadItem</a> properties in order to sort the search results. For example, setting <code>orderBy='startTime'</code> sorts the <a href='#type-DownloadItem'>DownloadItems</a> by their start time in ascending order. To specify descending order, prefix <code>orderBy</code> with a hyphen: '-startTime'.",
+ "optional": true,
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "id": {
+ "type": "integer",
+ "optional": true
+ },
+ "url": {
+ "description": "Absolute URL.",
+ "optional": true,
+ "type": "string"
+ },
+ "filename": {
+ "description": "Absolute local path.",
+ "optional": true,
+ "type": "string"
+ },
+ "danger": {
+ "$ref": "DangerType",
+ "description": "Indication of whether this download is thought to be safe or known to be suspicious.",
+ "optional": true
+ },
+ "mime": {
+ "description": "The file's MIME type.",
+ "optional": true,
+ "type": "string"
+ },
+ "startTime": {
+ "optional": true,
+ "type": "string"
+ },
+ "endTime": {
+ "optional": true,
+ "type": "string"
+ },
+ "state": {
+ "$ref": "State",
+ "description": "Indicates whether the download is progressing, interrupted, or complete.",
+ "optional": true
+ },
+ "paused": {
+ "description": "True if the download has stopped reading data from the host, but kept the connection open.",
+ "optional": true,
+ "type": "boolean"
+ },
+ "error": {
+ "description": "Why a download was interrupted.",
+ "optional": true,
+ "$ref": "InterruptReason"
+ },
+ "bytesReceived": {
+ "description": "Number of bytes received so far from the host, without considering file compression.",
+ "optional": true,
+ "type": "number"
+ },
+ "totalBytes": {
+ "description": "Number of bytes in the whole file, without considering file compression, or -1 if unknown.",
+ "optional": true,
+ "type": "number"
+ },
+ "fileSize": {
+ "description": "Number of bytes in the whole file post-decompression, or -1 if unknown.",
+ "optional": true,
+ "type": "number"
+ },
+ "exists": {
+ "type": "boolean",
+ "optional": true
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "download",
+ "type": "function",
+ "async": "callback",
+ "description": "Download a URL. If the URL uses the HTTP[S] protocol, then the request will include all cookies currently set for its hostname. If both <code>filename</code> and <code>saveAs</code> are specified, then the Save As dialog will be displayed, pre-populated with the specified <code>filename</code>. If the download started successfully, <code>callback</code> will be called with the new <a href='#type-DownloadItem'>DownloadItem</a>'s <code>downloadId</code>. If there was an error starting the download, then <code>callback</code> will be called with <code>downloadId=undefined</code> and <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain a descriptive string. The error strings are not guaranteed to remain backwards compatible between releases. You must not parse it.",
+ "parameters": [
+ {
+ "description": "What to download and how.",
+ "name": "options",
+ "type": "object",
+ "properties": {
+ "url": {
+ "description": "The URL to download.",
+ "type": "string",
+ "format": "url"
+ },
+ "filename": {
+ "description": "A file path relative to the Downloads directory to contain the downloaded file.",
+ "optional": true,
+ "type": "string"
+ },
+ "conflictAction": {
+ "$ref": "FilenameConflictAction",
+ "optional": true
+ },
+ "saveAs": {
+ "description": "Use a file-chooser to allow the user to select a filename.",
+ "optional": true,
+ "type": "boolean"
+ },
+ "method": {
+ "description": "The HTTP method to use if the URL uses the HTTP[S] protocol.",
+ "enum": [
+ "GET",
+ "POST"
+ ],
+ "optional": true,
+ "type": "string"
+ },
+ "headers": {
+ "optional": true,
+ "type": "array",
+ "description": "Extra HTTP headers to send with the request if the URL uses the HTTP[s] protocol. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>, restricted to those allowed by XMLHttpRequest.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Name of the HTTP header.",
+ "type": "string"
+ },
+ "value": {
+ "description": "Value of the HTTP header.",
+ "type": "string"
+ }
+ }
+ }
+ },
+ "body": {
+ "description": "Post body.",
+ "optional": true,
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "downloadId",
+ "type": "integer"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "search",
+ "type": "function",
+ "async": "callback",
+ "description": "Find <a href='#type-DownloadItem'>DownloadItems</a>. Set <code>query</code> to the empty object to get all <a href='#type-DownloadItem'>DownloadItems</a>. To get a specific <a href='#type-DownloadItem'>DownloadItem</a>, set only the <code>id</code> field.",
+ "parameters": [
+ {
+ "name": "query",
+ "$ref": "DownloadQuery"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "items": {
+ "$ref": "DownloadItem"
+ },
+ "name": "results",
+ "type": "array"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "pause",
+ "type": "function",
+ "async": "callback",
+ "description": "Pause the download. If the request was successful the download is in a paused state. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.",
+ "parameters": [
+ {
+ "description": "The id of the download to pause.",
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "parameters": [],
+ "type": "function"
+ }
+ ]
+ },
+ {
+ "name": "resume",
+ "type": "function",
+ "async": "callback",
+ "description": "Resume a paused download. If the request was successful the download is in progress and unpaused. Otherwise <a href='extension.html#property-lastError'>chrome.extension.lastError</a> contains an error message. The request will fail if the download is not active.",
+ "parameters": [
+ {
+ "description": "The id of the download to resume.",
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "parameters": [],
+ "type": "function"
+ }
+ ]
+ },
+ {
+ "name": "cancel",
+ "type": "function",
+ "async": "callback",
+ "description": "Cancel a download. When <code>callback</code> is run, the download is cancelled, completed, interrupted or doesn't exist anymore.",
+ "parameters": [
+ {
+ "description": "The id of the download to cancel.",
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "parameters": [],
+ "type": "function"
+ }
+ ]
+ },
+ {
+ "name": "getFileIcon",
+ "type": "function",
+ "async": "callback",
+ "description": "Retrieve an icon for the specified download. For new downloads, file icons are available after the <a href='#event-onCreated'>onCreated</a> event has been received. The image returned by this function while a download is in progress may be different from the image returned after the download is complete. Icon retrieval is done by querying the underlying operating system or toolkit depending on the platform. The icon that is returned will therefore depend on a number of factors including state of the download, platform, registered file types and visual theme. If a file icon cannot be determined, <a href='extension.html#property-lastError'>chrome.extension.lastError</a> will contain an error message.",
+ "parameters": [
+ {
+ "description": "The identifier for the download.",
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "options",
+ "optional": true,
+ "properties": {
+ "size": {
+ "description": "The size of the icon. The returned icon will be square with dimensions size * size pixels. The default size for the icon is 32x32 pixels.",
+ "optional": true,
+ "minimum": 1,
+ "maximum": 127,
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "iconURL",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "type": "function"
+ }
+ ]
+ },
+ {
+ "name": "open",
+ "type": "function",
+ "async": "callback",
+ "description": "Open the downloaded file.",
+ "permissions": ["downloads.open"],
+ "parameters": [
+ {
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "show",
+ "type": "function",
+ "description": "Show the downloaded file in its folder in a file manager.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "success",
+ "type": "boolean"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "showDefaultFolder",
+ "type": "function",
+ "parameters": []
+ },
+ {
+ "name": "erase",
+ "type": "function",
+ "async": "callback",
+ "description": "Erase matching <a href='#type-DownloadItem'>DownloadItems</a> from history",
+ "parameters": [
+ {
+ "name": "query",
+ "$ref": "DownloadQuery"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [
+ {
+ "items": {
+ "type": "integer"
+ },
+ "name": "erasedIds",
+ "type": "array"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "removeFile",
+ "async": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [ ]
+ }
+ ]
+ },
+ {
+ "description": "Prompt the user to either accept or cancel a dangerous download. <code>acceptDanger()</code> does not automatically accept dangerous downloads.",
+ "name": "acceptDanger",
+ "unsupported": true,
+ "parameters": [
+ {
+ "name": "downloadId",
+ "type": "integer"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [ ]
+ }
+ ],
+ "type": "function"
+ },
+ {
+ "description": "Initiate dragging the file to another application.",
+ "name": "drag",
+ "unsupported": true,
+ "parameters": [
+ {
+ "name": "downloadId",
+ "type": "integer"
+ }
+ ],
+ "type": "function"
+ },
+ {
+ "name": "setShelfEnabled",
+ "type": "function",
+ "unsupported": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "description": "This event fires with the <a href='#type-DownloadItem'>DownloadItem</a> object when a download begins.",
+ "name": "onCreated",
+ "parameters": [
+ {
+ "$ref": "DownloadItem",
+ "name": "downloadItem"
+ }
+ ],
+ "type": "function"
+ },
+ {
+ "description": "Fires with the <code>downloadId</code> when a download is erased from history.",
+ "name": "onErased",
+ "parameters": [
+ {
+ "name": "downloadId",
+ "description": "The <code>id</code> of the <a href='#type-DownloadItem'>DownloadItem</a> that was erased.",
+ "type": "integer"
+ }
+ ],
+ "type": "function"
+ },
+ {
+ "name": "onChanged",
+ "description": "When any of a <a href='#type-DownloadItem'>DownloadItem</a>'s properties except <code>bytesReceived</code> changes, this event fires with the <code>downloadId</code> and an object containing the properties that changed.",
+ "parameters": [
+ {
+ "name": "downloadDelta",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "The <code>id</code> of the <a href='#type-DownloadItem'>DownloadItem</a> that changed.",
+ "type": "integer"
+ },
+ "url": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>url</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "filename": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>filename</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "danger": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>danger</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "mime": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>mime</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "startTime": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>startTime</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "endTime": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>endTime</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "state": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>state</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "canResume": {
+ "optional": true,
+ "$ref": "BooleanDelta"
+ },
+ "paused": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>paused</code>.",
+ "optional": true,
+ "$ref": "BooleanDelta"
+ },
+ "error": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>error</code>.",
+ "optional": true,
+ "$ref": "StringDelta"
+ },
+ "totalBytes": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>totalBytes</code>.",
+ "optional": true,
+ "$ref": "DoubleDelta"
+ },
+ "fileSize": {
+ "description": "Describes a change in a <a href='#type-DownloadItem'>DownloadItem</a>'s <code>fileSize</code>.",
+ "optional": true,
+ "$ref": "DoubleDelta"
+ },
+ "exists": {
+ "optional": true,
+ "$ref": "BooleanDelta"
+ }
+ }
+ }
+ ],
+ "type": "function"
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/events.json b/toolkit/components/extensions/schemas/events.json
new file mode 100644
index 0000000000..ea3cbb5d29
--- /dev/null
+++ b/toolkit/components/extensions/schemas/events.json
@@ -0,0 +1,322 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "events",
+ "description": "The <code>chrome.events</code> namespace contains common types used by APIs dispatching events to notify you when something interesting happens.",
+ "types": [
+ {
+ "id": "Rule",
+ "type": "object",
+ "description": "Description of a declarative rule for handling events.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "optional": true,
+ "description": "Optional identifier that allows referencing this rule."
+ },
+ "tags": {
+ "type": "array",
+ "items": {"type": "string"},
+ "optional": true,
+ "description": "Tags can be used to annotate rules and perform operations on sets of rules."
+ },
+ "conditions": {
+ "type": "array",
+ "items": {"type": "any"},
+ "description": "List of conditions that can trigger the actions."
+ },
+ "actions": {
+ "type": "array",
+ "items": {"type": "any"},
+ "description": "List of actions that are triggered if one of the condtions is fulfilled."
+ },
+ "priority": {
+ "type": "integer",
+ "optional": true,
+ "description": "Optional priority of this rule. Defaults to 100."
+ }
+ }
+ },
+ {
+ "id": "Event",
+ "type": "object",
+ "description": "An object which allows the addition and removal of listeners for a Chrome event.",
+ "functions": [
+ {
+ "name": "addListener",
+ "type": "function",
+ "description": "Registers an event listener <em>callback</em> to an event.",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Called when an event occurs. The parameters of this function depend on the type of event."
+ }
+ ]
+ },
+ {
+ "name": "removeListener",
+ "type": "function",
+ "description": "Deregisters an event listener <em>callback</em> from an event.",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Listener that shall be unregistered."
+ }
+ ]
+ },
+ {
+ "name": "hasListener",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Listener whose registration status shall be tested."
+ }
+ ],
+ "returns": {
+ "type": "boolean",
+ "description": "True if <em>callback</em> is registered to the event."
+ }
+ },
+ {
+ "name": "hasListeners",
+ "type": "function",
+ "parameters": [],
+ "returns": {
+ "type": "boolean",
+ "description": "True if any event listeners are registered to the event."
+ }
+ },
+ {
+ "name": "addRules",
+ "unsupported": true,
+ "type": "function",
+ "description": "Registers rules to handle events.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "type": "string",
+ "description": "Name of the event this function affects."
+ },
+ {
+ "name": "webViewInstanceId",
+ "type": "integer",
+ "description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call."
+ },
+ {
+ "name": "rules",
+ "type": "array",
+ "items": {"$ref": "Rule"},
+ "description": "Rules to be registered. These do not replace previously registered rules."
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "type": "function",
+ "parameters": [
+ {
+ "name": "rules",
+ "type": "array",
+ "items": {"$ref": "Rule"},
+ "description": "Rules that were registered, the optional parameters are filled with values."
+ }
+ ],
+ "description": "Called with registered rules."
+ }
+ ]
+ },
+ {
+ "name": "getRules",
+ "unsupported": true,
+ "type": "function",
+ "description": "Returns currently registered rules.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "type": "string",
+ "description": "Name of the event this function affects."
+ },
+ {
+ "name": "webViewInstanceId",
+ "type": "integer",
+ "description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call."
+ },
+ {
+ "name": "ruleIdentifiers",
+ "optional": true,
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "If an array is passed, only rules with identifiers contained in this array are returned."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "rules",
+ "type": "array",
+ "items": {"$ref": "Rule"},
+ "description": "Rules that were registered, the optional parameters are filled with values."
+ }
+ ],
+ "description": "Called with registered rules."
+ }
+ ]
+ },
+ {
+ "name": "removeRules",
+ "unsupported": true,
+ "type": "function",
+ "description": "Unregisters currently registered rules.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "type": "string",
+ "description": "Name of the event this function affects."
+ },
+ {
+ "name": "webViewInstanceId",
+ "type": "integer",
+ "description": "If provided, this is an integer that uniquely identfies the <webview> associated with this function call."
+ },
+ {
+ "name": "ruleIdentifiers",
+ "optional": true,
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "If an array is passed, only rules with identifiers contained in this array are unregistered."
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "type": "function",
+ "parameters": [],
+ "description": "Called when rules were unregistered."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "UrlFilter",
+ "type": "object",
+ "description": "Filters URLs for various criteria. See <a href='events#filtered'>event filtering</a>. All criteria are case sensitive.",
+ "properties": {
+ "hostContains": {
+ "type": "string",
+ "description": "Matches if the host name of the URL contains a specified string. To test whether a host name component has a prefix 'foo', use hostContains: '.foo'. This matches 'www.foobar.com' and 'foo.com', because an implicit dot is added at the beginning of the host name. Similarly, hostContains can be used to match against component suffix ('foo.') and to exactly match against components ('.foo.'). Suffix- and exact-matching for the last components need to be done separately using hostSuffix, because no implicit dot is added at the end of the host name.",
+ "optional": true
+ },
+ "hostEquals": {
+ "type": "string",
+ "description": "Matches if the host name of the URL is equal to a specified string.",
+ "optional": true
+ },
+ "hostPrefix": {
+ "type": "string",
+ "description": "Matches if the host name of the URL starts with a specified string.",
+ "optional": true
+ },
+ "hostSuffix": {
+ "type": "string",
+ "description": "Matches if the host name of the URL ends with a specified string.",
+ "optional": true
+ },
+ "pathContains": {
+ "type": "string",
+ "description": "Matches if the path segment of the URL contains a specified string.",
+ "optional": true
+ },
+ "pathEquals": {
+ "type": "string",
+ "description": "Matches if the path segment of the URL is equal to a specified string.",
+ "optional": true
+ },
+ "pathPrefix": {
+ "type": "string",
+ "description": "Matches if the path segment of the URL starts with a specified string.",
+ "optional": true
+ },
+ "pathSuffix": {
+ "type": "string",
+ "description": "Matches if the path segment of the URL ends with a specified string.",
+ "optional": true
+ },
+ "queryContains": {
+ "type": "string",
+ "description": "Matches if the query segment of the URL contains a specified string.",
+ "optional": true
+ },
+ "queryEquals": {
+ "type": "string",
+ "description": "Matches if the query segment of the URL is equal to a specified string.",
+ "optional": true
+ },
+ "queryPrefix": {
+ "type": "string",
+ "description": "Matches if the query segment of the URL starts with a specified string.",
+ "optional": true
+ },
+ "querySuffix": {
+ "type": "string",
+ "description": "Matches if the query segment of the URL ends with a specified string.",
+ "optional": true
+ },
+ "urlContains": {
+ "type": "string",
+ "description": "Matches if the URL (without fragment identifier) contains a specified string. Port numbers are stripped from the URL if they match the default port number.",
+ "optional": true
+ },
+ "urlEquals": {
+ "type": "string",
+ "description": "Matches if the URL (without fragment identifier) is equal to a specified string. Port numbers are stripped from the URL if they match the default port number.",
+ "optional": true
+ },
+ "urlMatches": {
+ "type": "string",
+ "description": "Matches if the URL (without fragment identifier) matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the <a href=\"https://github.com/google/re2/blob/master/doc/syntax.txt\">RE2 syntax</a>.",
+ "optional": true
+ },
+ "originAndPathMatches": {
+ "type": "string",
+ "description": "Matches if the URL without query segment and fragment identifier matches a specified regular expression. Port numbers are stripped from the URL if they match the default port number. The regular expressions use the <a href=\"https://github.com/google/re2/blob/master/doc/syntax.txt\">RE2 syntax</a>.",
+ "optional": true
+ },
+ "urlPrefix": {
+ "type": "string",
+ "description": "Matches if the URL (without fragment identifier) starts with a specified string. Port numbers are stripped from the URL if they match the default port number.",
+ "optional": true
+ },
+ "urlSuffix": {
+ "type": "string",
+ "description": "Matches if the URL (without fragment identifier) ends with a specified string. Port numbers are stripped from the URL if they match the default port number.",
+ "optional": true
+ },
+ "schemes": {
+ "type": "array",
+ "description": "Matches if the scheme of the URL is equal to any of the schemes specified in the array.",
+ "optional": true,
+ "items": { "type": "string" }
+ },
+ "ports": {
+ "type": "array",
+ "description": "Matches if the port of the URL is contained in any of the specified port lists. For example <code>[80, 443, [1000, 1200]]</code> matches all requests on port 80, 443 and in the range 1000-1200.",
+ "optional": true,
+ "items": {
+ "choices": [
+ {"type": "integer", "description": "A specific port."},
+ {"type": "array", "minItems": 2, "maxItems": 2, "items": {"type": "integer"}, "description": "A pair of integers identiying the start and end (both inclusive) of a port range."}
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/experiments.json b/toolkit/components/extensions/schemas/experiments.json
new file mode 100644
index 0000000000..c687173a94
--- /dev/null
+++ b/toolkit/components/extensions/schemas/experiments.json
@@ -0,0 +1,16 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [
+ {
+ "type": "string",
+ "pattern": "^experiments(\\.\\w+)+$"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/extension.json b/toolkit/components/extensions/schemas/extension.json
new file mode 100644
index 0000000000..5a1b6c9357
--- /dev/null
+++ b/toolkit/components/extensions/schemas/extension.json
@@ -0,0 +1,178 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "extension",
+ "allowedContexts": ["content"],
+ "description": "The <code>browser.extension</code> API has utilities that can be used by any extension page. It includes support for exchanging messages between an extension and its content scripts or between extensions, as described in detail in $(topic:messaging)[Message Passing].",
+ "properties": {
+ "lastError": {
+ "type": "object",
+ "optional": true,
+ "allowedContexts": ["content"],
+ "description": "Set for the lifetime of a callback if an ansychronous extension api has resulted in an error. If no error has occured lastError will be <var>undefined</var>.",
+ "properties": {
+ "message": { "type": "string", "description": "Description of the error that has taken place." }
+ },
+ "additionalProperties": {
+ "type": "any"
+ }
+ },
+ "inIncognitoContext": {
+ "type": "boolean",
+ "optional": true,
+ "allowedContexts": ["content"],
+ "description": "True for content scripts running inside incognito tabs, and for extension pages running inside an incognito process. The latter only applies to extensions with 'split' incognito_behavior."
+ }
+ },
+ "types": [
+ {
+ "id": "ViewType",
+ "type": "string",
+ "enum": ["tab", "notification", "popup"],
+ "description": "The type of extension view."
+ }
+ ],
+ "functions": [
+ {
+ "name": "getURL",
+ "type": "function",
+ "allowedContexts": ["content"],
+ "description": "Converts a relative path within an extension install directory to a fully-qualified URL.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "path",
+ "description": "A path to a resource within an extension expressed relative to its install directory."
+ }
+ ],
+ "returns": {
+ "type": "string",
+ "description": "The fully-qualified URL to the resource."
+ }
+ },
+ {
+ "name": "getViews",
+ "type": "function",
+ "description": "Returns an array of the JavaScript 'window' objects for each of the pages running inside the current extension.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "fetchProperties",
+ "optional": true,
+ "properties": {
+ "type": {
+ "$ref": "ViewType",
+ "optional": true,
+ "description": "The type of view to get. If omitted, returns all views (including background pages and tabs). Valid values: 'tab', 'notification', 'popup'."
+ },
+ "windowId": {
+ "type": "integer",
+ "optional": true,
+ "description": "The window to restrict the search to. If omitted, returns all views."
+ }
+ }
+ }
+ ],
+ "returns": {
+ "type": "array",
+ "description": "Array of global objects",
+ "items": {
+ "name": "viewGlobals",
+ "type": "object",
+ "isInstanceOf": "Window",
+ "additionalProperties": { "type": "any" }
+ }
+ }
+ },
+ {
+ "name": "getBackgroundPage",
+ "type": "function",
+ "description": "Returns the JavaScript 'window' object for the background page running inside the current extension. Returns null if the extension has no background page.",
+ "parameters": [],
+ "returns": {
+ "type": "object",
+ "optional": true,
+ "name": "backgroundPageGlobal",
+ "isInstanceOf": "Window",
+ "additionalProperties": { "type": "any" }
+ }
+ },
+ {
+ "name": "isAllowedIncognitoAccess",
+ "type": "function",
+ "description": "Retrieves the state of the extension's access to Incognito-mode (as determined by the user-controlled 'Allowed in Incognito' checkbox.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "isAllowedAccess",
+ "type": "boolean",
+ "description": "True if the extension has access to Incognito mode, false otherwise."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "isAllowedFileSchemeAccess",
+ "type": "function",
+ "description": "Retrieves the state of the extension's access to the 'file://' scheme (as determined by the user-controlled 'Allow access to File URLs' checkbox.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "isAllowedAccess",
+ "type": "boolean",
+ "description": "True if the extension can access the 'file://' scheme, false otherwise."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setUpdateUrlData",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets the value of the ap CGI parameter used in the extension's update URL. This value is ignored for extensions that are hosted in the browser vendor's store.",
+ "parameters": [
+ {"type": "string", "name": "data", "maxLength": 1024}
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onRequest",
+ "unsupported": true,
+ "deprecated": "Please use $(ref:runtime.onMessage).",
+ "type": "function",
+ "description": "Fired when a request is sent from either an extension process or a content script.",
+ "parameters": [
+ {"name": "request", "type": "any", "optional": true, "description": "The request sent by the calling script."},
+ {"name": "sender", "$ref": "runtime.MessageSender" },
+ {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object, or undefined if there is no response. If you have more than one <code>onRequest</code> listener in the same document, then only one may send a response." }
+ ]
+ },
+ {
+ "name": "onRequestExternal",
+ "unsupported": true,
+ "deprecated": "Please use $(ref:runtime.onMessageExternal).",
+ "type": "function",
+ "description": "Fired when a request is sent from another extension.",
+ "parameters": [
+ {"name": "request", "type": "any", "optional": true, "description": "The request sent by the calling script."},
+ {"name": "sender", "$ref": "runtime.MessageSender" },
+ {"name": "sendResponse", "type": "function", "description": "Function to call when you have a response. The argument should be any JSON-ifiable object, or undefined if there is no response." }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/extension_types.json b/toolkit/components/extensions/schemas/extension_types.json
new file mode 100644
index 0000000000..1a88e4e608
--- /dev/null
+++ b/toolkit/components/extensions/schemas/extension_types.json
@@ -0,0 +1,83 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "extensionTypes",
+ "description": "The <code>browser.extensionTypes</code> API contains type declarations for WebExtensions.",
+ "types": [
+ {
+ "id": "ImageFormat",
+ "type": "string",
+ "enum": ["jpeg", "png"],
+ "description": "The format of an image."
+ },
+ {
+ "id": "ImageDetails",
+ "type": "object",
+ "description": "Details about the format and quality of an image.",
+ "properties": {
+ "format": {
+ "$ref": "ImageFormat",
+ "optional": true,
+ "description": "The format of the resulting image. Default is <code>\"jpeg\"</code>."
+ },
+ "quality": {
+ "type": "integer",
+ "optional": true,
+ "minimum": 0,
+ "maximum": 100,
+ "description": "When format is <code>\"jpeg\"</code>, controls the quality of the resulting image. This value is ignored for PNG images. As quality is decreased, the resulting image will have more visual artifacts, and the number of bytes needed to store it will decrease."
+ }
+ }
+ },
+ {
+ "id": "RunAt",
+ "type": "string",
+ "enum": ["document_start", "document_end", "document_idle"],
+ "description": "The soonest that the JavaScript or CSS will be injected into the tab."
+ },
+ {
+ "id": "InjectDetails",
+ "type": "object",
+ "description": "Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time.",
+ "properties": {
+ "code": {"type": "string", "optional": true, "description": "JavaScript or CSS code to inject.<br><br><b>Warning:</b><br>Be careful using the <code>code</code> parameter. Incorrect use of it may open your extension to <a href=\"https://en.wikipedia.org/wiki/Cross-site_scripting\">cross site scripting</a> attacks."},
+ "file": {"type": "string", "optional": true, "description": "JavaScript or CSS file to inject."},
+ "allFrames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."},
+ "matchAboutBlank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."},
+ "frameId": {
+ "type": "integer",
+ "minimum": 0,
+ "optional": true,
+ "description": "The ID of the frame to inject the script into. This may not be used in combination with <code>allFrames</code>."
+ },
+ "runAt": {
+ "$ref": "RunAt",
+ "optional": true,
+ "description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"."
+ }
+ }
+ },
+ {
+ "id": "Date",
+ "choices": [
+ {
+ "type": "string",
+ "format": "date"
+ },
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "object",
+ "isInstanceOf": "Date",
+ "additionalProperties": { "type": "any" }
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/i18n.json b/toolkit/components/extensions/schemas/i18n.json
new file mode 100644
index 0000000000..12dc45dfc8
--- /dev/null
+++ b/toolkit/components/extensions/schemas/i18n.json
@@ -0,0 +1,132 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "WebExtensionManifest",
+ "properties": {
+ "default_locale": {
+ "type": "string",
+ "optional": "true"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "namespace": "i18n",
+ "allowedContexts": ["content"],
+ "defaultContexts": ["content"],
+ "description": "Use the <code>browser.i18n</code> infrastructure to implement internationalization across your whole app or extension.",
+ "types": [
+ {
+ "id": "LanguageCode",
+ "type": "string",
+ "description": "An ISO language code such as <code>en</code> or <code>fr</code>. For a complete list of languages supported by this method, see <a href='http://src.chromium.org/viewvc/chrome/trunk/src/third_party/cld/languages/internal/languages.cc'>kLanguageInfoTable</a>. For an unknown language, <code>und</code> will be returned, which means that [percentage] of the text is unknown to CLD"
+ }
+ ],
+ "functions": [
+ {
+ "name": "getAcceptLanguages",
+ "type": "function",
+ "description": "Gets the accept-languages of the browser. This is different from the locale used by the browser; to get the locale, use $(ref:i18n.getUILanguage).",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {"name": "languages", "type": "array", "items": {"$ref": "LanguageCode"}, "description": "Array of LanguageCode"}
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getMessage",
+ "type": "function",
+ "description": "Gets the localized string for the specified message. If the message is missing, this method returns an empty string (''). If the format of the <code>getMessage()</code> call is wrong &mdash; for example, <em>messageName</em> is not a string or the <em>substitutions</em> array has more than 9 elements &mdash; this method returns <code>undefined</code>.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "messageName",
+ "description": "The name of the message, as specified in the <code>$(topic:i18n-messages)[messages.json]</code> file."
+ },
+ {
+ "type": "any",
+ "name": "substitutions",
+ "optional": true,
+ "description": "Substitution strings, if the message requires any."
+ }
+ ],
+ "returns": {
+ "type": "string",
+ "description": "Message localized for current locale."
+ }
+ },
+ {
+ "name": "getUILanguage",
+ "type": "function",
+ "description": "Gets the browser UI language of the browser. This is different from $(ref:i18n.getAcceptLanguages) which returns the preferred user languages.",
+ "parameters": [],
+ "returns": {
+ "type": "string",
+ "description": "The browser UI language code such as en-US or fr-FR."
+ }
+ },
+ {
+ "name": "detectLanguage",
+ "type": "function",
+ "description": "Detects the language of the provided text using CLD.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "text",
+ "description": "User input string to be translated."
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "result",
+ "description": "LanguageDetectionResult object that holds detected langugae reliability and array of DetectedLanguage",
+ "properties": {
+ "isReliable": { "type": "boolean", "description": "CLD detected language reliability" },
+ "languages":
+ {
+ "type": "array",
+ "description": "array of detectedLanguage",
+ "items":
+ {
+ "type": "object",
+ "description": "DetectedLanguage object that holds detected ISO language code and its percentage in the input string",
+ "properties":
+ {
+ "language":
+ {
+ "$ref": "LanguageCode"
+ },
+ "percentage":
+ {
+ "type": "integer",
+ "description": "The percentage of the detected language"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": []
+ }
+]
diff --git a/toolkit/components/extensions/schemas/idle.json b/toolkit/components/extensions/schemas/idle.json
new file mode 100644
index 0000000000..e0b3b951ee
--- /dev/null
+++ b/toolkit/components/extensions/schemas/idle.json
@@ -0,0 +1,70 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "idle",
+ "description": "Use the <code>browser.idle</code> API to detect when the machine's idle state changes.",
+ "permissions": ["idle"],
+ "types": [
+ {
+ "id": "IdleState",
+ "type": "string",
+ "enum": ["active", "idle"]
+ }
+ ],
+ "functions": [
+ {
+ "name": "queryState",
+ "type": "function",
+ "description": "Returns \"idle\" if the user has not generated any input for a specified number of seconds, or \"active\" otherwise.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "detectionIntervalInSeconds",
+ "type": "integer",
+ "minimum": 15,
+ "description": "The system is considered idle if detectionIntervalInSeconds seconds have elapsed since the last user input detected."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "newState",
+ "$ref": "IdleState"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setDetectionInterval",
+ "type": "function",
+ "description": "Sets the interval, in seconds, used to determine when the system is in an idle state for onStateChanged events. The default interval is 60 seconds.",
+ "parameters": [
+ {
+ "name": "intervalInSeconds",
+ "type": "integer",
+ "minimum": 15,
+ "description": "Threshold, in seconds, used to determine when the system is in an idle state."
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onStateChanged",
+ "type": "function",
+ "description": "Fired when the system changes to an active or idle state. The event fires with \"idle\" if the the user has not generated any input for a specified number of seconds, and \"active\" when the user generates input on an idle system.",
+ "parameters": [
+ {
+ "name": "newState",
+ "$ref": "IdleState"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/jar.mn b/toolkit/components/extensions/schemas/jar.mn
new file mode 100644
index 0000000000..0bdf35b0df
--- /dev/null
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -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/.
+
+toolkit.jar:
+% content extensions %content/extensions/
+ content/extensions/schemas/alarms.json
+ content/extensions/schemas/cookies.json
+ content/extensions/schemas/downloads.json
+ content/extensions/schemas/events.json
+ content/extensions/schemas/experiments.json
+ content/extensions/schemas/extension.json
+ content/extensions/schemas/extension_types.json
+ content/extensions/schemas/i18n.json
+ content/extensions/schemas/idle.json
+ content/extensions/schemas/management.json
+ content/extensions/schemas/manifest.json
+ content/extensions/schemas/native_host_manifest.json
+ content/extensions/schemas/notifications.json
+ content/extensions/schemas/runtime.json
+ content/extensions/schemas/storage.json
+ content/extensions/schemas/test.json
+ content/extensions/schemas/top_sites.json
+ content/extensions/schemas/web_navigation.json
+ content/extensions/schemas/web_request.json
diff --git a/toolkit/components/extensions/schemas/management.json b/toolkit/components/extensions/schemas/management.json
new file mode 100644
index 0000000000..413ff1d0d9
--- /dev/null
+++ b/toolkit/components/extensions/schemas/management.json
@@ -0,0 +1,250 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "management"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace":"management",
+ "description": "The <code>browser.management</code> API provides ways to manage the list of extensions that are installed and running.",
+ "types": [
+ {
+ "id": "IconInfo",
+ "description": "Information about an icon belonging to an extension.",
+ "type": "object",
+ "properties": {
+ "size": {
+ "type": "integer",
+ "description": "A number representing the width and height of the icon. Likely values include (but are not limited to) 128, 48, 24, and 16."
+ },
+ "url": {
+ "type": "string",
+ "description": "The URL for this icon image. To display a grayscale version of the icon (to indicate that an extension is disabled, for example), append <code>?grayscale=true</code> to the URL."
+ }
+ }
+ },
+ {
+ "id": "ExtensionDisabledReason",
+ "description": "A reason the item is disabled.",
+ "type": "string",
+ "enum": ["unknown", "permissions_increase"]
+ },
+ {
+ "id": "ExtensionType",
+ "description": "The type of this extension. Will always be 'extension'.",
+ "type": "string",
+ "enum": ["extension"]
+ },
+ {
+ "id": "ExtensionInstallType",
+ "description": "How the extension was installed. One of<br><var>development</var>: The extension was loaded unpacked in developer mode,<br><var>normal</var>: The extension was installed normally via an .xpi file,<br><var>sideload</var>: The extension was installed by other software on the machine,<br><var>other</var>: The extension was installed by other means.",
+ "type": "string",
+ "enum": ["development", "normal", "sideload", "other"]
+ },
+ {
+ "id": "ExtensionInfo",
+ "description": "Information about an installed extension.",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "The extension's unique identifier.",
+ "type": "string"
+ },
+ "name": {
+ "description": "The name of this extension.",
+ "type": "string"
+ },
+ "shortName": {
+ "description": "A short version of the name of this extension.",
+ "type": "string"
+ },
+ "description": {
+ "description": "The description of this extension.",
+ "type": "string"
+ },
+ "version": {
+ "description": "The <a href='manifest/version'>version</a> of this extension.",
+ "type": "string"
+ },
+ "versionName": {
+ "description": "The <a href='manifest/version#version_name'>version name</a> of this extension if the manifest specified one.",
+ "type": "string",
+ "optional": true
+ },
+ "mayDisable": {
+ "description": "Whether this extension can be disabled or uninstalled by the user.",
+ "type": "boolean"
+ },
+ "enabled": {
+ "description": "Whether it is currently enabled or disabled.",
+ "type": "boolean"
+ },
+ "disabledReason": {
+ "description": "A reason the item is disabled.",
+ "$ref": "ExtensionDisabledReason",
+ "optional": true
+ },
+ "type": {
+ "description": "The type of this extension. Will always return 'extension'.",
+ "$ref": "ExtensionType"
+ },
+ "homepageUrl": {
+ "description": "The URL of the homepage of this extension.",
+ "type": "string",
+ "optional": true
+ },
+ "updateUrl": {
+ "description": "The update URL of this extension.",
+ "type": "string",
+ "optional": true
+ },
+ "optionsUrl": {
+ "description": "The url for the item's options page, if it has one.",
+ "type": "string"
+ },
+ "icons": {
+ "description": "A list of icon information. Note that this just reflects what was declared in the manifest, and the actual image at that url may be larger or smaller than what was declared, so you might consider using explicit width and height attributes on img tags referencing these images. See the <a href='manifest/icons'>manifest documentation on icons</a> for more details.",
+ "type": "array",
+ "optional": true,
+ "items": {
+ "$ref": "IconInfo"
+ }
+ },
+ "permissions": {
+ "description": "Returns a list of API based permissions.",
+ "type": "array",
+ "items" : {
+ "type": "string"
+ }
+ },
+ "hostPermissions": {
+ "description": "Returns a list of host based permissions.",
+ "type": "array",
+ "items" : {
+ "type": "string"
+ }
+ },
+ "installType": {
+ "description": "How the extension was installed.",
+ "$ref": "ExtensionInstallType"
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "getAll",
+ "type": "function",
+ "permissions": ["management"],
+ "unsupported": true,
+ "description": "Returns a list of information about installed extensions.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [
+ {
+ "type": "array",
+ "name": "result",
+ "items": {
+ "$ref": "ExtensionInfo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "get",
+ "type": "function",
+ "permissions": ["management"],
+ "unsupported": true,
+ "description": "Returns information about the installed extension that has the given ID.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "manifest.ExtensionID",
+ "description": "The ID from an item of $(ref:management.ExtensionInfo)."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "result",
+ "$ref": "ExtensionInfo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getSelf",
+ "type": "function",
+ "description": "Returns information about the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "result",
+ "$ref": "ExtensionInfo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "uninstallSelf",
+ "type": "function",
+ "description": "Uninstalls the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "options",
+ "optional": true,
+ "properties": {
+ "showConfirmDialog": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Whether or not a confirm-uninstall dialog should prompt the user. Defaults to false."
+ },
+ "dialogMessage": {
+ "type": "string",
+ "optional": true,
+ "description": "The message to display to a user when being asked to confirm removal of the extension."
+ }
+ }
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/manifest.json b/toolkit/components/extensions/schemas/manifest.json
new file mode 100644
index 0000000000..09e6b56fba
--- /dev/null
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -0,0 +1,377 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "id": "WebExtensionManifest",
+ "type": "object",
+ "description": "Represents a WebExtension manifest.json file",
+ "properties": {
+ "manifest_version": {
+ "type": "integer",
+ "minimum": 2,
+ "maximum": 2
+ },
+
+ "minimum_chrome_version":{
+ "type": "string",
+ "optional": true
+ },
+
+ "applications": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "gecko": {
+ "$ref": "FirefoxSpecificProperties",
+ "optional": true
+ }
+ }
+ },
+
+ "browser_specific_settings": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "gecko": {
+ "$ref": "FirefoxSpecificProperties",
+ "optional": true
+ }
+ }
+ },
+
+ "name": {
+ "type": "string",
+ "optional": false,
+ "preprocess": "localize"
+ },
+
+ "short_name": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "description": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "author": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize",
+ "onError": "warn"
+ },
+
+ "version": {
+ "type": "string",
+ "optional": false
+ },
+
+ "homepage_url": {
+ "type": "string",
+ "format": "url",
+ "optional": true,
+ "preprocess": "localize"
+ },
+
+ "icons": {
+ "type": "object",
+ "optional": true,
+ "patternProperties": {
+ "^[1-9]\\d*$": { "type": "string" }
+ }
+ },
+
+ "incognito": {
+ "type": "string",
+ "enum": ["spanning"],
+ "optional": true,
+ "onError": "warn"
+ },
+
+ "background": {
+ "choices": [
+ {
+ "type": "object",
+ "properties": {
+ "page": { "$ref": "ExtensionURL" },
+ "persistent": {
+ "optional": true,
+ "$ref": "PersistentBackgroundProperty"
+ }
+ },
+ "additionalProperties": { "$ref": "UnrecognizedProperty" }
+ },
+ {
+ "type": "object",
+ "properties": {
+ "scripts": {
+ "type": "array",
+ "items": { "$ref": "ExtensionURL" }
+ },
+ "persistent": {
+ "optional": true,
+ "$ref": "PersistentBackgroundProperty"
+ }
+ },
+ "additionalProperties": { "$ref": "UnrecognizedProperty" }
+ }
+ ],
+ "optional": true
+ },
+
+ "options_ui": {
+ "type": "object",
+
+ "optional": true,
+
+ "properties": {
+ "page": { "$ref": "ExtensionURL" },
+ "browser_style": {
+ "type": "boolean",
+ "optional": true
+ },
+ "chrome_style": {
+ "type": "boolean",
+ "optional": true
+ },
+ "open_in_tab": {
+ "type": "boolean",
+ "optional": true
+ }
+ },
+
+ "additionalProperties": {
+ "type": "any",
+ "deprecated": "An unexpected property was found in the WebExtension manifest"
+ }
+ },
+
+ "content_scripts": {
+ "type": "array",
+ "optional": true,
+ "items": { "$ref": "ContentScript" }
+ },
+
+ "content_security_policy": {
+ "type": "string",
+ "optional": true,
+ "format": "contentSecurityPolicy",
+ "onError": "warn"
+ },
+
+ "permissions": {
+ "type": "array",
+ "items": {
+ "choices": [
+ { "$ref": "Permission" },
+ {
+ "type": "string",
+ "deprecated": "Unknown permission ${value}"
+ }
+ ]
+ },
+ "optional": true
+ },
+
+ "web_accessible_resources": {
+ "type": "array",
+ "items": { "type": "string" },
+ "optional": true
+ },
+
+ "developer": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "url": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ }
+ }
+ }
+
+ },
+
+ "additionalProperties": { "$ref": "UnrecognizedProperty" }
+ },
+ {
+ "id": "Permission",
+ "choices": [
+ {
+ "type": "string",
+ "enum": [
+ "alarms",
+ "clipboardWrite",
+ "idle",
+ "notifications",
+ "storage"
+ ]
+ },
+ { "$ref": "MatchPattern" }
+ ]
+ },
+ {
+ "id": "ExtensionURL",
+ "type": "string",
+ "format": "strictRelativeUrl"
+ },
+ {
+ "id": "ExtensionID",
+ "choices": [
+ {
+ "type": "string",
+ "pattern": "(?i)^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$"
+ },
+ {
+ "type": "string",
+ "pattern": "(?i)^[a-z0-9-._]*@[a-z0-9-._]+$"
+ }
+ ]
+ },
+ {
+ "id": "FirefoxSpecificProperties",
+ "type": "object",
+ "properties": {
+ "id": {
+ "$ref": "ExtensionID",
+ "optional": true
+ },
+
+ "update_url": {
+ "type": "string",
+ "format": "url",
+ "optional": true
+ },
+
+ "strict_min_version": {
+ "type": "string",
+ "optional": true
+ },
+
+ "strict_max_version": {
+ "type": "string",
+ "optional": true
+ }
+ }
+ },
+ {
+ "id": "MatchPattern",
+ "choices": [
+ {
+ "type": "string",
+ "enum": ["<all_urls>"]
+ },
+ {
+ "type": "string",
+ "pattern": "^(https?|file|ftp|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+)/.*$"
+ },
+ {
+ "type": "string",
+ "pattern": "^file:///.*$"
+ }
+ ]
+ },
+ {
+ "id": "ContentScript",
+ "type": "object",
+ "description": "Details of the script or CSS to inject. Either the code or the file property must be set, but both may not be set at the same time. Based on InjectDetails, but using underscore rather than camel case naming conventions.",
+ "additionalProperties": { "$ref": "UnrecognizedProperty" },
+ "properties": {
+ "matches": {
+ "type": "array",
+ "optional": false,
+ "minItems": 1,
+ "items": { "$ref": "MatchPattern" }
+ },
+ "exclude_matches": {
+ "type": "array",
+ "optional": true,
+ "minItems": 1,
+ "items": { "$ref": "MatchPattern" }
+ },
+ "include_globs": {
+ "type": "array",
+ "optional": true,
+ "items": { "type": "string" }
+ },
+ "exclude_globs": {
+ "type": "array",
+ "optional": true,
+ "items": { "type": "string" }
+ },
+ "css": {
+ "type": "array",
+ "optional": true,
+ "description": "The list of CSS files to inject",
+ "items": { "$ref": "ExtensionURL" }
+ },
+ "js": {
+ "type": "array",
+ "optional": true,
+ "description": "The list of CSS files to inject",
+ "items": { "$ref": "ExtensionURL" }
+ },
+ "all_frames": {"type": "boolean", "optional": true, "description": "If allFrames is <code>true</code>, implies that the JavaScript or CSS should be injected into all frames of current page. By default, it's <code>false</code> and is only injected into the top frame."},
+ "match_about_blank": {"type": "boolean", "optional": true, "description": "If matchAboutBlank is true, then the code is also injected in about:blank and about:srcdoc frames if your extension has access to its parent document. Code cannot be inserted in top-level about:-frames. By default it is <code>false</code>."},
+ "run_at": {
+ "$ref": "extensionTypes.RunAt",
+ "optional": true,
+ "description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"."
+ }
+ }
+ },
+ {
+ "id": "IconPath",
+ "choices": [
+ {
+ "type": "object",
+ "patternProperties": {
+ "^[1-9]\\d*$": { "$ref": "ExtensionURL" }
+ },
+ "additionalProperties": false
+ },
+ { "$ref": "ExtensionURL" }
+ ]
+ },
+ {
+ "id": "IconImageData",
+ "choices": [
+ {
+ "type": "object",
+ "patternProperties": {
+ "^[1-9]\\d*$": { "$ref": "ImageData" }
+ },
+ "additionalProperties": false
+ },
+ { "$ref": "ImageData" }
+ ]
+ },
+ {
+ "id": "ImageData",
+ "type": "object",
+ "isInstanceOf": "ImageData",
+ "postprocess": "convertImageDataToURL"
+ },
+ {
+ "id": "UnrecognizedProperty",
+ "type": "any",
+ "deprecated": "An unexpected property was found in the WebExtension manifest."
+ },
+ {
+ "id": "PersistentBackgroundProperty",
+ "type": "boolean",
+ "deprecated": "Event pages are not currently supported. This will run as a persistent background page."
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/moz.build b/toolkit/components/extensions/schemas/moz.build
new file mode 100644
index 0000000000..aac3a838c4
--- /dev/null
+++ b/toolkit/components/extensions/schemas/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/toolkit/components/extensions/schemas/native_host_manifest.json b/toolkit/components/extensions/schemas/native_host_manifest.json
new file mode 100644
index 0000000000..4ad2ea7f16
--- /dev/null
+++ b/toolkit/components/extensions/schemas/native_host_manifest.json
@@ -0,0 +1,37 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "id": "NativeHostManifest",
+ "type": "object",
+ "description": "Represents a native host manifest file",
+ "properties": {
+ "name": {
+ "type": "string",
+ "pattern": "^\\w+(\\.\\w+)*$"
+ },
+ "description": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "stdio"
+ ]
+ },
+ "allowed_extensions": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "$ref": "manifest.ExtensionID"
+ }
+ }
+ }
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/notifications.json b/toolkit/components/extensions/schemas/notifications.json
new file mode 100644
index 0000000000..12878e8c8e
--- /dev/null
+++ b/toolkit/components/extensions/schemas/notifications.json
@@ -0,0 +1,416 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "notifications",
+ "permissions": ["notifications"],
+ "types": [
+ {
+ "id": "TemplateType",
+ "type": "string",
+ "enum": [
+ "basic",
+ "image",
+ "list",
+ "progress"
+ ]
+ },
+ {
+ "id": "PermissionLevel",
+ "type": "string",
+ "enum": [
+ "granted",
+ "denied"
+ ]
+ },
+ {
+ "id": "NotificationItem",
+ "type": "object",
+ "properties": {
+ "title": {
+ "description": "Title of one item of a list notification.",
+ "type": "string"
+ },
+ "message": {
+ "description": "Additional details about this item.",
+ "type": "string"
+ }
+ }
+ },
+ {
+ "id": "CreateNotificationOptions",
+ "type": "object",
+ "properties": {
+ "type": {
+ "description": "Which type of notification to display.",
+ "$ref": "TemplateType"
+ },
+ "iconUrl": {
+ "optional": true,
+ "description": "A URL to the sender's avatar, app icon, or a thumbnail for image notifications.",
+ "type": "string"
+ },
+ "appIconMaskUrl": {
+ "optional": true,
+ "description": "A URL to the app icon mask.",
+ "type": "string"
+ },
+ "title": {
+ "description": "Title of the notification (e.g. sender name for email).",
+ "type": "string"
+ },
+ "message": {
+ "description": "Main notification content.",
+ "type": "string"
+ },
+ "contextMessage": {
+ "optional": true,
+ "description": "Alternate notification content with a lower-weight font.",
+ "type": "string"
+ },
+ "priority": {
+ "optional": true,
+ "description": "Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default.",
+ "type": "integer",
+ "minimum": -2,
+ "maximum": 2
+ },
+ "eventTime": {
+ "optional": true,
+ "description": "A timestamp associated with the notification, in milliseconds past the epoch.",
+ "type": "number"
+ },
+ "buttons": {
+ "unsupported": true,
+ "optional": true,
+ "description": "Text and icons for up to two notification action buttons.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "iconUrl": {
+ "optional": true,
+ "type": "string"
+ }
+ }
+ }
+ },
+ "imageUrl": {
+ "optional": true,
+ "description": "A URL to the image thumbnail for image-type notifications.",
+ "type": "string"
+ },
+ "items": {
+ "optional": true,
+ "description": "Items for multi-item notifications.",
+ "type": "array",
+ "items": { "$ref": "NotificationItem" }
+ },
+ "progress": {
+ "optional": true,
+ "description": "Current progress ranges from 0 to 100.",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "isClickable": {
+ "optional": true,
+ "description": "Whether to show UI indicating that the app will visibly respond to clicks on the body of a notification.",
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "id": "UpdateNotificationOptions",
+ "type": "object",
+ "properties": {
+ "type": {
+ "optional": true,
+ "description": "Which type of notification to display.",
+ "$ref": "TemplateType"
+ },
+ "iconUrl": {
+ "optional": true,
+ "description": "A URL to the sender's avatar, app icon, or a thumbnail for image notifications.",
+ "type": "string"
+ },
+ "appIconMaskUrl": {
+ "optional": true,
+ "description": "A URL to the app icon mask.",
+ "type": "string"
+ },
+ "title": {
+ "optional": true,
+ "description": "Title of the notification (e.g. sender name for email).",
+ "type": "string"
+ },
+ "message": {
+ "optional": true,
+ "description": "Main notification content.",
+ "type": "string"
+ },
+ "contextMessage": {
+ "optional": true,
+ "description": "Alternate notification content with a lower-weight font.",
+ "type": "string"
+ },
+ "priority": {
+ "optional": true,
+ "description": "Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default.",
+ "type": "integer",
+ "minimum": -2,
+ "maximum": 2
+ },
+ "eventTime": {
+ "optional": true,
+ "description": "A timestamp associated with the notification, in milliseconds past the epoch.",
+ "type": "number"
+ },
+ "buttons": {
+ "unsupported": true,
+ "optional": true,
+ "description": "Text and icons for up to two notification action buttons.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "iconUrl": {
+ "optional": true,
+ "type": "string"
+ }
+ }
+ }
+ },
+ "imageUrl": {
+ "optional": true,
+ "description": "A URL to the image thumbnail for image-type notifications.",
+ "type": "string"
+ },
+ "items": {
+ "optional": true,
+ "description": "Items for multi-item notifications.",
+ "type": "array",
+ "items": { "$ref": "NotificationItem" }
+ },
+ "progress": {
+ "optional": true,
+ "description": "Current progress ranges from 0 to 100.",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "isClickable": {
+ "optional": true,
+ "description": "Whether to show UI indicating that the app will visibly respond to clicks on the body of a notification.",
+ "type": "boolean"
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "description": "Creates and displays a notification.",
+ "async": "callback",
+ "parameters": [
+ {
+ "optional": true,
+ "type": "string",
+ "name": "notificationId",
+ "description": "Identifier of the notification. If it is empty, this method generates an id. If it matches an existing notification, this method first clears that notification before proceeding with the create operation."
+ },
+ {
+ "$ref": "CreateNotificationOptions",
+ "name": "options",
+ "description": "Contents of the notification."
+ },
+ {
+ "optional": true,
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "notificationId",
+ "type": "string",
+ "description": "The notification id (either supplied or generated) that represents the created notification."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "update",
+ "unsupported": true,
+ "type": "function",
+ "description": "Updates an existing notification.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "notificationId",
+ "description": "The id of the notification to be updated."
+ },
+ {
+ "$ref": "UpdateNotificationOptions",
+ "name": "options",
+ "description": "Contents of the notification to update to."
+ },
+ {
+ "optional": true,
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "wasUpdated",
+ "type": "boolean",
+ "description": "Indicates whether a matching notification existed."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "clear",
+ "type": "function",
+ "description": "Clears an existing notification.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "notificationId",
+ "description": "The id of the notification to be updated."
+ },
+ {
+ "optional": true,
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "wasCleared",
+ "type": "boolean",
+ "description": "Indicates whether a matching notification existed."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getAll",
+ "type": "function",
+ "description": "Retrieves all the notifications.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "notifications",
+ "type": "object",
+ "description": "The set of notifications currently in the system."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getPermissionLevel",
+ "unsupported": true,
+ "type": "function",
+ "description": "Retrieves whether the user has enabled notifications from this app or extension.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "level",
+ "$ref": "PermissionLevel",
+ "description": "The current permission level."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onClosed",
+ "type": "function",
+ "description": "Fired when the notification closed, either by the system or by user action.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "notificationId",
+ "description": "The notificationId of the closed notification."
+ },
+ {
+ "type": "boolean",
+ "name": "byUser",
+ "description": "True if the notification was closed by the user."
+ }
+ ]
+ },
+ {
+ "name": "onClicked",
+ "type": "function",
+ "description": "Fired when the user clicked in a non-button area of the notification.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "notificationId",
+ "description": "The notificationId of the clicked notification."
+ }
+ ]
+ },
+ {
+ "name": "onButtonClicked",
+ "type": "function",
+ "description": "Fired when the user pressed a button in the notification.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "notificationId",
+ "description": "The notificationId of the clicked notification."
+ },
+ {
+ "type": "number",
+ "name": "buttonIndex",
+ "description": "The index of the button clicked by the user."
+ }
+ ]
+ },
+ {
+ "name": "onPermissionLevelChanged",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when the user changes the permission level.",
+ "parameters": [
+ {
+ "$ref": "PermissionLevel",
+ "name": "level",
+ "description": "The new permission level."
+ }
+ ]
+ },
+ {
+ "name": "onShowSettings",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when the user clicked on a link for the app's notification settings.",
+ "parameters": [
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/runtime.json b/toolkit/components/extensions/schemas/runtime.json
new file mode 100644
index 0000000000..b3f12a768b
--- /dev/null
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -0,0 +1,592 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "nativeMessaging"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "runtime",
+ "allowedContexts": ["content"],
+ "description": "Use the <code>browser.runtime</code> API to retrieve the background page, return details about the manifest, and listen for and respond to events in the app or extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.",
+ "types": [
+ {
+ "id": "Port",
+ "type": "object",
+ "allowedContexts": ["content"],
+ "description": "An object which allows two way communication with other pages.",
+ "properties": {
+ "name": {"type": "string"},
+ "disconnect": { "type": "function" },
+ "onDisconnect": { "$ref": "events.Event" },
+ "onMessage": { "$ref": "events.Event" },
+ "postMessage": {"type": "function"},
+ "sender": {
+ "$ref": "MessageSender",
+ "optional": true,
+ "description": "This property will <b>only</b> be present on ports passed to onConnect/onConnectExternal listeners."
+ }
+ },
+ "additionalProperties": { "type": "any"}
+ },
+ {
+ "id": "MessageSender",
+ "type": "object",
+ "allowedContexts": ["content"],
+ "description": "An object containing information about the script context that sent a message or request.",
+ "properties": {
+ "tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab) which opened the connection, if any. This property will <strong>only</strong> be present when the connection was opened from a tab (including content scripts), and <strong>only</strong> if the receiver is an extension, not an app."},
+ "frameId": {"type": "integer", "optional": true, "description": "The $(topic:frame_ids)[frame] that opened the connection. 0 for top-level frames, positive for child frames. This will only be set when <code>tab</code> is set."},
+ "id": {"type": "string", "optional": true, "description": "The ID of the extension or app that opened the connection, if any."},
+ "url": {"type": "string", "optional": true, "description": "The URL of the page or frame that opened the connection. If the sender is in an iframe, it will be iframe's URL not the URL of the page which hosts it."},
+ "tlsChannelId": {"unsupported": true, "type": "string", "optional": true, "description": "The TLS channel ID of the page or frame that opened the connection, if requested by the extension or app, and if available."}
+ }
+ },
+ {
+ "id": "PlatformOs",
+ "type": "string",
+ "allowedContexts": ["content"],
+ "description": "The operating system the browser is running on.",
+ "enum": ["mac", "win", "android", "cros", "linux", "openbsd"]
+ },
+ {
+ "id": "PlatformArch",
+ "type": "string",
+ "enum": ["arm", "x86-32", "x86-64"],
+ "allowedContexts": ["content"],
+ "description": "The machine's processor architecture."
+ },
+ {
+ "id": "PlatformInfo",
+ "type": "object",
+ "allowedContexts": ["content"],
+ "description": "An object containing information about the current platform.",
+ "properties": {
+ "os": {
+ "$ref": "PlatformOs",
+ "description": "The operating system the browser is running on."
+ },
+ "arch": {
+ "$ref": "PlatformArch",
+ "description": "The machine's processor architecture."
+ },
+ "nacl_arch" : {
+ "unsupported": true,
+ "description": "The native client architecture. This may be different from arch on some platforms.",
+ "$ref": "PlatformNaclArch"
+ }
+ }
+ },
+ {
+ "id": "BrowserInfo",
+ "type": "object",
+ "description": "An object containing information about the current browser.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of the browser, for example 'Firefox'."
+ },
+ "vendor": {
+ "type": "string",
+ "description": "The name of the browser vendor, for example 'Mozilla'."
+ },
+ "version": {
+ "type": "string",
+ "description": "The browser's version, for example '42.0.0' or '0.8.1pre'."
+ },
+ "buildID": {
+ "type": "string",
+ "description": "The browser's build ID/date, for example '20160101'."
+ }
+ }
+ },
+ {
+ "id": "RequestUpdateCheckStatus",
+ "type": "string",
+ "enum": ["throttled", "no_update", "update_available"],
+ "allowedContexts": ["content"],
+ "description": "Result of the update check."
+ },
+ {
+ "id": "OnInstalledReason",
+ "type": "string",
+ "enum": ["install", "update", "browser_update"],
+ "allowedContexts": ["content"],
+ "description": "The reason that this event is being dispatched."
+ },
+ {
+ "id": "OnRestartRequiredReason",
+ "type": "string",
+ "allowedContexts": ["content"],
+ "description": "The reason that the event is being dispatched. 'app_update' is used when the restart is needed because the application is updated to a newer version. 'os_update' is used when the restart is needed because the browser/OS is updated to a newer version. 'periodic' is used when the system runs for more than the permitted uptime set in the enterprise policy.",
+ "enum": ["app_update", "os_update", "periodic"]
+ }
+ ],
+ "properties": {
+ "lastError": {
+ "type": "object",
+ "optional": true,
+ "allowedContexts": ["content"],
+ "description": "This will be defined during an API method callback if there was an error",
+ "properties": {
+ "message": {
+ "optional": true,
+ "type": "string",
+ "description": "Details about the error which occurred."
+ }
+ },
+ "additionalProperties": {
+ "type": "any"
+ }
+ },
+ "id": {
+ "type": "string",
+ "allowedContexts": ["content"],
+ "description": "The ID of the extension/app."
+ }
+ },
+ "functions": [
+ {
+ "name": "getBackgroundPage",
+ "type": "function",
+ "description": "Retrieves the JavaScript 'window' object for the background page running inside the current extension/app. If the background page is an event page, the system will ensure it is loaded before calling the callback. If there is no background page, an error is set.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "backgroundPage",
+ "optional": true,
+ "type": "object",
+ "isInstanceOf": "Window",
+ "additionalProperties": { "type": "any" },
+ "description": "The JavaScript 'window' object for the background page."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "openOptionsPage",
+ "type": "function",
+ "description": "<p>Open your Extension's options page, if possible.</p><p>The precise behavior may depend on your manifest's <code>$(topic:optionsV2)[options_ui]</code> or <code>$(topic:options)[options_page]</code> key, or what the browser happens to support at the time.</p><p>If your Extension does not declare an options page, or the browser failed to create one for some other reason, the callback will set $(ref:lastError).</p>",
+ "async": "callback",
+ "parameters": [{
+ "type": "function",
+ "name": "callback",
+ "parameters": [],
+ "optional": true
+ }]
+ },
+ {
+ "name": "getManifest",
+ "allowedContexts": ["content"],
+ "description": "Returns details about the app or extension from the manifest. The object returned is a serialization of the full $(topic:manifest)[manifest file].",
+ "type": "function",
+ "parameters": [],
+ "returns": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": { "type": "any" },
+ "description": "The manifest details."
+ }
+ },
+ {
+ "name": "getURL",
+ "type": "function",
+ "allowedContexts": ["content"],
+ "description": "Converts a relative path within an app/extension install directory to a fully-qualified URL.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "path",
+ "description": "A path to a resource within an app/extension expressed relative to its install directory."
+ }
+ ],
+ "returns": {
+ "type": "string",
+ "description": "The fully-qualified URL to the resource."
+ }
+ },
+ {
+ "name": "setUninstallURL",
+ "type": "function",
+ "description": "Sets the URL to be visited upon uninstallation. This may be used to clean up server-side data, do analytics, and implement surveys. Maximum 255 characters.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "url",
+ "maxLength": 255,
+ "description": "URL to be opened after the extension is uninstalled. This URL must have an http: or https: scheme. Set an empty string to not open a new tab upon uninstallation."
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "description": "Called when the uninstall URL is set. If the given URL is invalid, $(ref:runtime.lastError) will be set.",
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "reload",
+ "description": "Reloads the app or extension.",
+ "type": "function",
+ "parameters": []
+ },
+ {
+ "name": "requestUpdateCheck",
+ "unsupported": true,
+ "type": "function",
+ "description": "Requests an update check for this app/extension.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "status",
+ "$ref": "RequestUpdateCheckStatus",
+ "description": "Result of the update check."
+ },
+ {
+ "name": "details",
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "version": {
+ "type": "string",
+ "description": "The version of the available update."
+ }
+ },
+ "description": "If an update is available, this contains more information about the available update."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "restart",
+ "unsupported": true,
+ "description": "Restart the device when the app runs in kiosk mode. Otherwise, it's no-op.",
+ "type": "function",
+ "parameters": []
+ },
+ {
+ "name": "connect",
+ "type": "function",
+ "allowedContexts": ["content"],
+ "description": "Attempts to connect to connect listeners within an extension/app (such as the background page), or other extensions/apps. This is useful for content scripts connecting to their extension processes, inter-app/extension communication, and $(topic:manifest/externally_connectable)[web messaging]. Note that this does not connect to any listeners in a content script. Extensions may connect to content scripts embedded in tabs via $(ref:tabs.connect).",
+ "parameters": [
+ {"type": "string", "name": "extensionId", "optional": true, "description": "The ID of the extension or app to connect to. If omitted, a connection will be attempted with your own extension. Required if sending messages from a web page for $(topic:manifest/externally_connectable)[web messaging]."},
+ {
+ "type": "object",
+ "name": "connectInfo",
+ "properties": {
+ "name": { "type": "string", "optional": true, "description": "Will be passed into onConnect for processes that are listening for the connection event." },
+ "includeTlsChannelId": { "type": "boolean", "optional": true, "description": "Whether the TLS channel ID will be passed into onConnectExternal for processes that are listening for the connection event." }
+ },
+ "optional": true
+ }
+ ],
+ "returns": {
+ "$ref": "Port",
+ "description": "Port through which messages can be sent and received. The port's $(ref:runtime.Port onDisconnect) event is fired if the extension/app does not exist. "
+ }
+ },
+ {
+ "name": "connectNative",
+ "type": "function",
+ "description": "Connects to a native application in the host machine.",
+ "permissions": ["nativeMessaging"],
+ "parameters": [
+ {
+ "type": "string",
+ "name": "application",
+ "description": "The name of the registered application to connect to."
+ }
+ ],
+ "returns": {
+ "$ref": "Port",
+ "description": "Port through which messages can be sent and received with the application"
+ }
+ },
+ {
+ "name": "sendMessage",
+ "type": "function",
+ "allowAmbiguousOptionalArguments": true,
+ "allowedContexts": ["content"],
+ "description": "Sends a single message to event listeners within your extension/app or a different extension/app. Similar to $(ref:runtime.connect) but only sends a single message, with an optional response. If sending to your extension, the $(ref:runtime.onMessage) event will be fired in each page, or $(ref:runtime.onMessageExternal), if a different extension. Note that extensions cannot send messages to content scripts using this method. To send messages to content scripts, use $(ref:tabs.sendMessage).",
+ "async": "responseCallback",
+ "parameters": [
+ {"type": "string", "name": "extensionId", "optional": true, "description": "The ID of the extension/app to send the message to. If omitted, the message will be sent to your own extension/app. Required if sending messages from a web page for $(topic:manifest/externally_connectable)[web messaging]."},
+ { "type": "any", "name": "message" },
+ {
+ "type": "object",
+ "name": "options",
+ "properties": {
+ "includeTlsChannelId": { "type": "boolean", "optional": true, "description": "Whether the TLS channel ID will be passed into onMessageExternal for processes that are listening for the connection event." }
+ },
+ "optional": true
+ },
+ {
+ "type": "function",
+ "name": "responseCallback",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "response",
+ "type": "any",
+ "description": "The JSON response object sent by the handler of the message. If an error occurs while connecting to the extension, the callback will be called with no arguments and $(ref:runtime.lastError) will be set to the error message."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "sendNativeMessage",
+ "type": "function",
+ "description": "Send a single message to a native application.",
+ "permissions": ["nativeMessaging"],
+ "async": "responseCallback",
+ "parameters": [
+ {
+ "name": "application",
+ "description": "The name of the native messaging host.",
+ "type": "string"
+ },
+ {
+ "name": "message",
+ "description": "The message that will be passed to the native messaging host.",
+ "type": "any"
+ },
+ {
+ "type": "function",
+ "name": "responseCallback",
+ "optional": true,
+ "parameters": [
+ {
+ "name": "response",
+ "type": "any",
+ "description": "The response message sent by the native messaging host. If an error occurs while connecting to the native messaging host, the callback will be called with no arguments and $(ref:runtime.lastError) will be set to the error message."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getBrowserInfo",
+ "type": "function",
+ "description": "Returns information about the current browser.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "description": "Called with results",
+ "parameters": [
+ {
+ "name": "browserInfo",
+ "$ref": "BrowserInfo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getPlatformInfo",
+ "type": "function",
+ "description": "Returns information about the current platform.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "description": "Called with results",
+ "parameters": [
+ {
+ "name": "platformInfo",
+ "$ref": "PlatformInfo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getPackageDirectoryEntry",
+ "unsupported": true,
+ "type": "function",
+ "description": "Returns a DirectoryEntry for the package directory.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "directoryEntry",
+ "type": "object",
+ "additionalProperties": { "type": "any" },
+ "isInstanceOf": "DirectoryEntry"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onStartup",
+ "type": "function",
+ "description": "Fired when a profile that has this extension installed first starts up. This event is not fired for incognito profiles."
+ },
+ {
+ "name": "onInstalled",
+ "type": "function",
+ "description": "Fired when the extension is first installed, when the extension is updated to a new version, and when the browser is updated to a new version.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "reason": {
+ "$ref": "OnInstalledReason",
+ "description": "The reason that this event is being dispatched."
+ },
+ "previousVersion": {
+ "type": "string",
+ "optional": true,
+ "unsupported": true,
+ "description": "Indicates the previous version of the extension, which has just been updated. This is present only if 'reason' is 'update'."
+ },
+ "id": {
+ "type": "string",
+ "optional": true,
+ "unsupported": true,
+ "description": "Indicates the ID of the imported shared module extension which updated. This is present only if 'reason' is 'shared_module_update'."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "onSuspend",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sent to the event page just before it is unloaded. This gives the extension opportunity to do some clean up. Note that since the page is unloading, any asynchronous operations started while handling this event are not guaranteed to complete. If more activity for the event page occurs before it gets unloaded the onSuspendCanceled event will be sent and the page won't be unloaded. "
+ },
+ {
+ "name": "onSuspendCanceled",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sent after onSuspend to indicate that the app won't be unloaded after all."
+ },
+ {
+ "name": "onUpdateAvailable",
+ "type": "function",
+ "description": "Fired when an update is available, but isn't installed immediately because the app is currently running. If you do nothing, the update will be installed the next time the background page gets unloaded, if you want it to be installed sooner you can explicitly call $(ref:runtime.reload). If your extension is using a persistent background page, the background page of course never gets unloaded, so unless you call $(ref:runtime.reload) manually in response to this event the update will not get installed until the next time the browser itself restarts. If no handlers are listening for this event, and your extension has a persistent background page, it behaves as if $(ref:runtime.reload) is called in response to this event.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "version": {
+ "type": "string",
+ "description": "The version number of the available update."
+ }
+ },
+ "additionalProperties": { "type": "any" },
+ "description": "The manifest details of the available update."
+ }
+ ]
+ },
+ {
+ "name": "onBrowserUpdateAvailable",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when an update for the browser is available, but isn't installed immediately because a browser restart is required.",
+ "deprecated": "Please use $(ref:runtime.onRestartRequired).",
+ "parameters": []
+ },
+ {
+ "name": "onConnect",
+ "type": "function",
+ "allowedContexts": ["content"],
+ "description": "Fired when a connection is made from either an extension process or a content script.",
+ "parameters": [
+ {"$ref": "Port", "name": "port"}
+ ]
+ },
+ {
+ "name": "onConnectExternal",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when a connection is made from another extension.",
+ "parameters": [
+ {"$ref": "Port", "name": "port"}
+ ]
+ },
+ {
+ "name": "onMessage",
+ "type": "function",
+ "allowedContexts": ["content"],
+ "description": "Fired when a message is sent from either an extension process or a content script.",
+ "parameters": [
+ {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."},
+ {"name": "sender", "$ref": "MessageSender" },
+ {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." }
+ ],
+ "returns": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns."
+ }
+ },
+ {
+ "name": "onMessageExternal",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when a message is sent from another extension/app. Cannot be used in a content script.",
+ "parameters": [
+ {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."},
+ {"name": "sender", "$ref": "MessageSender" },
+ {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." }
+ ],
+ "returns": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns."
+ }
+ },
+ {
+ "name": "onRestartRequired",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when an app or the device that it runs on needs to be restarted. The app should close all its windows at its earliest convenient time to let the restart to happen. If the app does nothing, a restart will be enforced after a 24-hour grace period has passed. Currently, this event is only fired for Chrome OS kiosk apps.",
+ "parameters": [
+ {
+ "$ref": "OnRestartRequiredReason",
+ "name": "reason",
+ "description": "The reason that the event is being dispatched."
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/storage.json b/toolkit/components/extensions/schemas/storage.json
new file mode 100644
index 0000000000..a54a209424
--- /dev/null
+++ b/toolkit/components/extensions/schemas/storage.json
@@ -0,0 +1,229 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "storage",
+ "allowedContexts": ["content"],
+ "defaultContexts": ["content"],
+ "description": "Use the <code>browser.storage</code> API to store, retrieve, and track changes to user data.",
+ "permissions": ["storage"],
+ "types": [
+ {
+ "id": "StorageChange",
+ "type": "object",
+ "properties": {
+ "oldValue": {
+ "type": "any",
+ "description": "The old value of the item, if there was an old value.",
+ "optional": true
+ },
+ "newValue": {
+ "type": "any",
+ "description": "The new value of the item, if there is a new value.",
+ "optional": true
+ }
+ }
+ },
+ {
+ "id": "StorageArea",
+ "type": "object",
+ "functions": [
+ {
+ "name": "get",
+ "type": "function",
+ "description": "Gets one or more items from storage.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "keys",
+ "choices": [
+ { "type": "string" },
+ { "type": "array", "items": { "type": "string" } },
+ {
+ "type": "object",
+ "description": "Storage items to return in the callback, where the values are replaced with those from storage if they exist.",
+ "additionalProperties": { "type": "any" }
+ }
+ ],
+ "description": "A single key to get, list of keys to get, or a dictionary specifying default values (see description of the object). An empty list or object will return an empty result object. Pass in <code>null</code> to get the entire contents of storage.",
+ "optional": true
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Callback with storage items, or on failure (in which case $(ref:runtime.lastError) will be set).",
+ "parameters": [
+ {
+ "name": "items",
+ "type": "object",
+ "additionalProperties": { "type": "any" },
+ "description": "Object with items in their key-value mappings."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getBytesInUse",
+ "unsupported": true,
+ "type": "function",
+ "description": "Gets the amount of space (in bytes) being used by one or more items.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "keys",
+ "choices": [
+ { "type": "string" },
+ { "type": "array", "items": { "type": "string" } }
+ ],
+ "description": "A single key or list of keys to get the total usage for. An empty list will return 0. Pass in <code>null</code> to get the total usage of all of storage.",
+ "optional": true
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Callback with the amount of space being used by storage, or on failure (in which case $(ref:runtime.lastError) will be set).",
+ "parameters": [
+ {
+ "name": "bytesInUse",
+ "type": "integer",
+ "description": "Amount of space being used in storage, in bytes."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "set",
+ "type": "function",
+ "description": "Sets multiple items.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "items",
+ "type": "object",
+ "additionalProperties": { "type": "any" },
+ "description": "<p>An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected.</p><p>Primitive values such as numbers will serialize as expected. Values with a <code>typeof</code> <code>\"object\"</code> and <code>\"function\"</code> will typically serialize to <code>{}</code>, with the exception of <code>Array</code> (serializes as expected), <code>Date</code>, and <code>Regex</code> (serialize using their <code>String</code> representation).</p>"
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Callback on success, or on failure (in which case $(ref:runtime.lastError) will be set).",
+ "parameters": [],
+ "optional": true
+ }
+ ]
+ },
+ {
+ "name": "remove",
+ "type": "function",
+ "description": "Removes one or more items from storage.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "keys",
+ "choices": [
+ {"type": "string"},
+ {"type": "array", "items": {"type": "string"}}
+ ],
+ "description": "A single key or a list of keys for items to remove."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Callback on success, or on failure (in which case $(ref:runtime.lastError) will be set).",
+ "parameters": [],
+ "optional": true
+ }
+ ]
+ },
+ {
+ "name": "clear",
+ "type": "function",
+ "description": "Removes all items from storage.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "description": "Callback on success, or on failure (in which case $(ref:runtime.lastError) will be set).",
+ "parameters": [],
+ "optional": true
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onChanged",
+ "type": "function",
+ "description": "Fired when one or more items change.",
+ "parameters": [
+ {
+ "name": "changes",
+ "type": "object",
+ "additionalProperties": { "$ref": "StorageChange" },
+ "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item."
+ },
+ {
+ "name": "areaName",
+ "type": "string",
+ "description": "The name of the storage area (<code>\"sync\"</code>, <code>\"local\"</code> or <code>\"managed\"</code>) the changes are for."
+ }
+ ]
+ }
+ ],
+ "properties": {
+ "sync": {
+ "$ref": "StorageArea",
+ "description": "Items in the <code>sync</code> storage area are synced by the browser.",
+ "properties": {
+ "QUOTA_BYTES": {
+ "value": 102400,
+ "description": "The maximum total amount (in bytes) of data that can be stored in sync storage, as measured by the JSON stringification of every value plus every key's length. Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError)."
+ },
+ "QUOTA_BYTES_PER_ITEM": {
+ "value": 8192,
+ "description": "The maximum size (in bytes) of each individual item in sync storage, as measured by the JSON stringification of its value plus its key length. Updates containing items larger than this limit will fail immediately and set $(ref:runtime.lastError)."
+ },
+ "MAX_ITEMS": {
+ "value": 512,
+ "description": "The maximum number of items that can be stored in sync storage. Updates that would cause this limit to be exceeded will fail immediately and set $(ref:runtime.lastError)."
+ },
+ "MAX_WRITE_OPERATIONS_PER_HOUR": {
+ "value": 1800,
+ "description": "<p>The maximum number of <code>set</code>, <code>remove</code>, or <code>clear</code> operations that can be performed each hour. This is 1 every 2 seconds, a lower ceiling than the short term higher writes-per-minute limit.</p><p>Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError).</p>"
+ },
+ "MAX_WRITE_OPERATIONS_PER_MINUTE": {
+ "value": 120,
+ "description": "<p>The maximum number of <code>set</code>, <code>remove</code>, or <code>clear</code> operations that can be performed each minute. This is 2 per second, providing higher throughput than writes-per-hour over a shorter period of time.</p><p>Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError).</p>"
+ },
+ "MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE": {
+ "value": 1000000,
+ "deprecated": "The storage.sync API no longer has a sustained write operation quota.",
+ "description": ""
+ }
+ }
+ },
+ "local": {
+ "$ref": "StorageArea",
+ "description": "Items in the <code>local</code> storage area are local to each machine.",
+ "properties": {
+ "QUOTA_BYTES": {
+ "value": 5242880,
+ "description": "The maximum amount (in bytes) of data that can be stored in local storage, as measured by the JSON stringification of every value plus every key's length. This value will be ignored if the extension has the <code>unlimitedStorage</code> permission. Updates that would cause this limit to be exceeded fail immediately and set $(ref:runtime.lastError)."
+ }
+ }
+ },
+ "managed": {
+ "unsupported": true,
+ "$ref": "StorageArea",
+ "description": "Items in the <code>managed</code> storage area are set by the domain administrator, and are read-only for the extension; trying to modify this namespace results in an error."
+ }
+ }
+ }
+]
diff --git a/toolkit/components/extensions/schemas/test.json b/toolkit/components/extensions/schemas/test.json
new file mode 100644
index 0000000000..25a62a96bd
--- /dev/null
+++ b/toolkit/components/extensions/schemas/test.json
@@ -0,0 +1,215 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "test",
+ "allowedContexts": ["content"],
+ "defaultContexts": ["content"],
+ "description": "none",
+ "functions": [
+ {
+ "name": "notifyFail",
+ "type": "function",
+ "description": "Notifies the browser process that test code running in the extension failed. This is only used for internal unit testing.",
+ "parameters": [
+ {"type": "string", "name": "message"}
+ ]
+ },
+ {
+ "name": "notifyPass",
+ "type": "function",
+ "description": "Notifies the browser process that test code running in the extension passed. This is only used for internal unit testing.",
+ "parameters": [
+ {"type": "string", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "log",
+ "type": "function",
+ "description": "Logs a message during internal unit testing.",
+ "parameters": [
+ {"type": "string", "name": "message"}
+ ]
+ },
+ {
+ "name": "sendMessage",
+ "type": "function",
+ "description": "Sends a string message to the browser process, generating a Notification that C++ test code can wait for.",
+ "allowAmbiguousOptionalArguments": true,
+ "parameters": [
+ {"type": "any", "name": "arg1", "optional": true},
+ {"type": "any", "name": "arg2", "optional": true}
+ ]
+ },
+ {
+ "name": "fail",
+ "type": "function",
+ "parameters": [
+ {"type": "any", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "succeed",
+ "type": "function",
+ "parameters": [
+ {"type": "any", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "assertTrue",
+ "type": "function",
+ "allowAmbiguousOptionalArguments": true,
+ "parameters": [
+ {"name": "test", "type": "any", "optional": true},
+ {"type": "string", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "assertFalse",
+ "type": "function",
+ "allowAmbiguousOptionalArguments": true,
+ "parameters": [
+ {"name": "test", "type": "any", "optional": true},
+ {"type": "string", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "assertBool",
+ "type": "function",
+ "unsupported": true,
+ "parameters": [
+ {
+ "name": "test",
+ "choices": [
+ {"type": "string"},
+ {"type": "boolean"}
+ ]
+ },
+ {"type": "boolean", "name": "expected"},
+ {"type": "string", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "checkDeepEq",
+ "type": "function",
+ "unsupported": true,
+ "allowAmbiguousOptionalArguments": true,
+ "parameters": [
+ {"type": "any", "name": "expected"},
+ {"type": "any", "name": "actual"}
+ ]
+ },
+ {
+ "name": "assertEq",
+ "type": "function",
+ "allowAmbiguousOptionalArguments": true,
+ "parameters": [
+ {"type": "any", "name": "expected", "optional": true},
+ {"type": "any", "name": "actual", "optional": true},
+ {"type": "string", "name": "message", "optional": true}
+ ]
+ },
+ {
+ "name": "assertNoLastError",
+ "type": "function",
+ "unsupported": true,
+ "parameters": []
+ },
+ {
+ "name": "assertLastError",
+ "type": "function",
+ "unsupported": true,
+ "parameters": [
+ {"type": "string", "name": "expectedError"}
+ ]
+ },
+ {
+ "name": "assertRejects",
+ "type": "function",
+ "async": true,
+ "parameters": [
+ {
+ "name": "promise",
+ "$ref": "Promise"
+ },
+ {
+ "name": "expectedError",
+ "$ref": "ExpectedError",
+ "optional": true
+ },
+ {
+ "name": "message",
+ "type": "string",
+ "optional": true
+ }
+ ]
+ },
+ {
+ "name": "assertThrows",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "func",
+ "type": "function"
+ },
+ {
+ "name": "expectedError",
+ "$ref": "ExpectedError",
+ "optional": true
+ },
+ {
+ "name": "message",
+ "type": "string",
+ "optional": true
+ }
+ ]
+ }
+ ],
+ "types": [
+ {
+ "id": "ExpectedError",
+ "choices": [
+ {"type": "string"},
+ {"type": "object", "isInstanceOf": "RegExp", "additionalProperties": true},
+ {"type": "function"}
+ ]
+ },
+ {
+ "id": "Promise",
+ "choices": [
+ {
+ "type": "object",
+ "properties": {
+ "then": {"type": "function"}
+ },
+ "additionalProperties": true
+ },
+ {
+ "type": "object",
+ "isInstanceOf": "Promise",
+ "additionalProperties": true
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onMessage",
+ "type": "function",
+ "description": "Used to test sending messages to extensions.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "message"
+ },
+ {
+ "type": "any",
+ "name": "argument"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/top_sites.json b/toolkit/components/extensions/schemas/top_sites.json
new file mode 100644
index 0000000000..fbfbc4b624
--- /dev/null
+++ b/toolkit/components/extensions/schemas/top_sites.json
@@ -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/. */
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "topSites"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "topSites",
+ "description": "Use the chrome.topSites API to access the top sites that are displayed on the new tab page. ",
+ "permissions": ["topSites"],
+ "types": [
+ {
+ "id": "MostVisitedURL",
+ "type": "object",
+ "description": "An object encapsulating a most visited URL, such as the URLs on the new tab page.",
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "The most visited URL."
+ },
+ "title": {
+ "type": "string",
+ "optional": true,
+ "description": "The title of the page."
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "get",
+ "type": "function",
+ "description": "Gets a list of top sites.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "results",
+ "type": "array",
+ "items": {
+ "$ref": "MostVisitedURL"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/web_navigation.json b/toolkit/components/extensions/schemas/web_navigation.json
new file mode 100644
index 0000000000..1e13b181ac
--- /dev/null
+++ b/toolkit/components/extensions/schemas/web_navigation.json
@@ -0,0 +1,387 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "webNavigation"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "webNavigation",
+ "description": "Use the <code>browser.webNavigation</code> API to receive notifications about the status of navigation requests in-flight.",
+ "permissions": ["webNavigation"],
+ "types": [
+ {
+ "id": "TransitionType",
+ "type": "string",
+ "enum": ["link", "typed", "auto_bookmark", "auto_subframe", "manual_subframe", "generated", "start_page", "form_submit", "reload", "keyword", "keyword_generated"],
+ "description": "Cause of the navigation. The same transition types as defined in the history API are used. These are the same transition types as defined in the $(topic:transition_types)[history API] except with <code>\"start_page\"</code> in place of <code>\"auto_toplevel\"</code> (for backwards compatibility)."
+ },
+ {
+ "id": "TransitionQualifier",
+ "type": "string",
+ "enum": ["client_redirect", "server_redirect", "forward_back", "from_address_bar"]
+ },
+ {
+ "id": "EventUrlFilters",
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "events.UrlFilter" }
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "getFrame",
+ "type": "function",
+ "description": "Retrieves information about the given frame. A frame refers to an &lt;iframe&gt; or a &lt;frame&gt; of a web page and is identified by a tab ID and a frame ID.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "description": "Information about the frame to retrieve information about.",
+ "properties": {
+ "tabId": { "type": "integer", "minimum": 0, "description": "The ID of the tab in which the frame is." },
+ "processId": {"optional": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": { "type": "integer", "minimum": 0, "description": "The ID of the frame in the given tab." }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "optional": true,
+ "description": "Information about the requested frame, null if the specified frame ID and/or tab ID are invalid.",
+ "properties": {
+ "errorOccurred": {
+ "unsupported": true,
+ "type": "boolean",
+ "description": "True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired."
+ },
+ "url": {
+ "type": "string",
+ "description": "The URL currently associated with this frame, if the frame identified by the frameId existed at one point in the given tab. The fact that an URL is associated with a given frameId does not imply that the corresponding frame still exists."
+ },
+ "parentFrameId": {
+ "type": "integer",
+ "description": "ID of frame that wraps the frame. Set to -1 of no parent frame exists."
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getAllFrames",
+ "type": "function",
+ "description": "Retrieves information about all frames of a given tab.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "description": "Information about the tab to retrieve all frames from.",
+ "properties": {
+ "tabId": { "type": "integer", "minimum": 0, "description": "The ID of the tab." }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "array",
+ "description": "A list of frames in the given tab, null if the specified tab ID is invalid.",
+ "optional": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "errorOccurred": {
+ "unsupported": true,
+ "type": "boolean",
+ "description": "True if the last navigation in this frame was interrupted by an error, i.e. the onErrorOccurred event fired."
+ },
+ "processId": {
+ "unsupported": true,
+ "type": "integer",
+ "description": "The ID of the process runs the renderer for this tab."
+ },
+ "frameId": {
+ "type": "integer",
+ "description": "The ID of the frame. 0 indicates that this is the main frame; a positive value indicates the ID of a subframe."
+ },
+ "parentFrameId": {
+ "type": "integer",
+ "description": "ID of frame that wraps the frame. Set to -1 of no parent frame exists."
+ },
+ "url": {
+ "type": "string",
+ "description": "The URL currently associated with this frame."
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onBeforeNavigate",
+ "type": "function",
+ "description": "Fired when a navigation is about to occur.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation is about to occur."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique for a given tab and process."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame. Set to -1 of no parent frame exists."},
+ "timeStamp": {"type": "number", "description": "The time when the browser was about to start the navigation, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onCommitted",
+ "type": "function",
+ "description": "Fired when a navigation is committed. The document (and the resources it refers to, such as images and subframes) might still be downloading, but at least part of the document has been received from the server and the browser has decided to switch to the new document.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."},
+ "transitionType": {"unsupported": true, "$ref": "TransitionType", "description": "Cause of the navigation."},
+ "transitionQualifiers": {"unsupported": true, "type": "array", "description": "A list of transition qualifiers.", "items": {"$ref": "TransitionQualifier"}},
+ "timeStamp": {"type": "number", "description": "The time when the navigation was committed, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onDOMContentLoaded",
+ "type": "function",
+ "description": "Fired when the page's DOM is fully constructed, but the referenced resources may not finish loading.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."},
+ "timeStamp": {"type": "number", "description": "The time when the page's DOM was fully constructed, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onCompleted",
+ "type": "function",
+ "description": "Fired when a document, including the resources it refers to, is completely loaded and initialized.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."},
+ "timeStamp": {"type": "number", "description": "The time when the document finished loading, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onErrorOccurred",
+ "type": "function",
+ "description": "Fired when an error occurs and the navigation is aborted. This can happen if either a network error occurred, or the user aborted the navigation.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."},
+ "error": {"unsupported": true, "type": "string", "description": "The error description."},
+ "timeStamp": {"type": "number", "description": "The time when the error occurred, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onCreatedNavigationTarget",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when a new window, or a new tab in an existing window, is created to host a navigation.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "sourceTabId": {"type": "integer", "description": "The ID of the tab in which the navigation is triggered."},
+ "sourceProcessId": {"type": "integer", "description": "The ID of the process runs the renderer for the source tab."},
+ "sourceFrameId": {"type": "integer", "description": "The ID of the frame with sourceTabId in which the navigation is triggered. 0 indicates the main frame."},
+ "url": {"type": "string", "description": "The URL to be opened in the new window."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the url is opened"},
+ "timeStamp": {"type": "number", "description": "The time when the browser was about to create a new view, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onReferenceFragmentUpdated",
+ "type": "function",
+ "description": "Fired when the reference fragment of a frame was updated. All future events for that frame will use the updated URL.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."},
+ "transitionType": {"unsupported": true, "$ref": "TransitionType", "description": "Cause of the navigation."},
+ "transitionQualifiers": {"unsupported": true, "type": "array", "description": "A list of transition qualifiers.", "items": {"$ref": "TransitionQualifier"}},
+ "timeStamp": {"type": "number", "description": "The time when the navigation was committed, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ },
+ {
+ "name": "onTabReplaced",
+ "type": "function",
+ "description": "Fired when the contents of the tab is replaced by a different (usually previously pre-rendered) tab.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "replacedTabId": {"type": "integer", "description": "The ID of the tab that was replaced."},
+ "tabId": {"type": "integer", "description": "The ID of the tab that replaced the old tab."},
+ "timeStamp": {"type": "number", "description": "The time when the replacement happened, in milliseconds since the epoch."}
+ }
+ }
+ ]
+ },
+ {
+ "name": "onHistoryStateUpdated",
+ "type": "function",
+ "description": "Fired when the frame's history was updated to a new URL. All future events for that frame will use the updated URL.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the navigation occurs."},
+ "url": {"type": "string"},
+ "processId": {"unsupported": true, "type": "integer", "description": "The ID of the process runs the renderer for this tab."},
+ "frameId": {"type": "integer", "description": "0 indicates the navigation happens in the tab content window; a positive value indicates navigation in a subframe. Frame IDs are unique within a tab."},
+ "transitionType": {"unsupported": true, "$ref": "TransitionType", "description": "Cause of the navigation."},
+ "transitionQualifiers": {"unsupported": true, "type": "array", "description": "A list of transition qualifiers.", "items": {"$ref": "TransitionQualifier"}},
+ "timeStamp": {"type": "number", "description": "The time when the navigation was committed, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "filters",
+ "optional": true,
+ "$ref": "EventUrlFilters",
+ "description": "Conditions that the URL being navigated to must satisfy. The 'schemes' and 'ports' fields of UrlFilter are ignored for this event."
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/schemas/web_request.json b/toolkit/components/extensions/schemas/web_request.json
new file mode 100644
index 0000000000..4035aea6e7
--- /dev/null
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -0,0 +1,616 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "webRequest",
+ "webRequestBlocking"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "webRequest",
+ "description": "Use the <code>browser.webRequest</code> API to observe and analyze traffic and to intercept, block, or modify requests in-flight.",
+ "permissions": ["webRequest"],
+ "properties": {
+ "MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES": {
+ "value": 20,
+ "description": "The maximum number of times that <code>handlerBehaviorChanged</code> can be called per 10 minute sustained interval. <code>handlerBehaviorChanged</code> is an expensive function call that shouldn't be called often."
+ }
+ },
+ "types": [
+ {
+ "id": "ResourceType",
+ "type": "string",
+ "enum": [
+ "main_frame",
+ "sub_frame",
+ "stylesheet",
+ "script",
+ "image",
+ "object",
+ "xmlhttprequest",
+ "xbl",
+ "xslt",
+ "ping",
+ "beacon",
+ "xml_dtd",
+ "font",
+ "media",
+ "websocket",
+ "csp_report",
+ "imageset",
+ "web_manifest",
+ "other"
+ ]
+ },
+ {
+ "id": "OnBeforeRequestOptions",
+ "type": "string",
+ "enum": ["blocking", "requestBody"]
+ },
+ {
+ "id": "OnBeforeSendHeadersOptions",
+ "type": "string",
+ "enum": ["requestHeaders", "blocking"]
+ },
+ {
+ "id": "OnSendHeadersOptions",
+ "type": "string",
+ "enum": ["requestHeaders"]
+ },
+ {
+ "id": "OnHeadersReceivedOptions",
+ "type": "string",
+ "enum": ["blocking", "responseHeaders"]
+ },
+ {
+ "id": "OnAuthRequiredOptions",
+ "type": "string",
+ "enum": ["responseHeaders", "blocking", "asyncBlocking"]
+ },
+ {
+ "id": "OnResponseStartedOptions",
+ "type": "string",
+ "enum": ["responseHeaders"]
+ },
+ {
+ "id": "OnBeforeRedirectOptions",
+ "type": "string",
+ "enum": ["responseHeaders"]
+ },
+ {
+ "id": "OnCompletedOptions",
+ "type": "string",
+ "enum": ["responseHeaders"]
+ },
+ {
+ "id": "RequestFilter",
+ "type": "object",
+ "description": "An object describing filters to apply to webRequest events.",
+ "properties": {
+ "urls": {
+ "type": "array",
+ "description": "A list of URLs or URL patterns. Requests that cannot match any of the URLs will be filtered out.",
+ "items": { "type": "string" }
+ },
+ "types": {
+ "type": "array",
+ "optional": true,
+ "description": "A list of request types. Requests that cannot match any of the types will be filtered out.",
+ "items": { "$ref": "ResourceType" }
+ },
+ "tabId": { "type": "integer", "optional": true },
+ "windowId": { "type": "integer", "optional": true }
+ }
+ },
+ {
+ "id": "HttpHeaders",
+ "type": "array",
+ "description": "An array of HTTP headers. Each header is represented as a dictionary containing the keys <code>name</code> and either <code>value</code> or <code>binaryValue</code>.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "Name of the HTTP header."},
+ "value": {"type": "string", "optional": true, "description": "Value of the HTTP header if it can be represented by UTF-8."},
+ "binaryValue": {
+ "type": "array",
+ "optional": true,
+ "description": "Value of the HTTP header if it cannot be represented by UTF-8, stored as individual byte values (0..255).",
+ "items": {"type": "integer"}
+ }
+ }
+ }
+ },
+ {
+ "id": "BlockingResponse",
+ "type": "object",
+ "description": "Returns value for event handlers that have the 'blocking' extraInfoSpec applied. Allows the event handler to modify network requests.",
+ "properties": {
+ "cancel": {
+ "type": "boolean",
+ "optional": true,
+ "description": "If true, the request is cancelled. Used in onBeforeRequest, this prevents the request from being sent."
+ },
+ "redirectUrl": {
+ "type": "string",
+ "optional": true,
+ "description": "Only used as a response to the onBeforeRequest and onHeadersReceived events. If set, the original request is prevented from being sent/completed and is instead redirected to the given URL. Redirections to non-HTTP schemes such as data: are allowed. Redirects initiated by a redirect action use the original request method for the redirect, with one exception: If the redirect is initiated at the onHeadersReceived stage, then the redirect will be issued using the GET method."
+ },
+ "requestHeaders": {
+ "$ref": "HttpHeaders",
+ "optional": true,
+ "description": "Only used as a response to the onBeforeSendHeaders event. If set, the request is made with these request headers instead."
+ },
+ "responseHeaders": {
+ "$ref": "HttpHeaders",
+ "optional": true,
+ "description": "Only used as a response to the onHeadersReceived event. If set, the server is assumed to have responded with these response headers instead. Only return <code>responseHeaders</code> if you really want to modify the headers in order to limit the number of conflicts (only one extension may modify <code>responseHeaders</code> for each request)."
+ },
+ "authCredentials": {
+ "type": "object",
+ "description": "Only used as a response to the onAuthRequired event. If set, the request is made using the supplied credentials.",
+ "optional": true,
+ "properties": {
+ "username": {"type": "string"},
+ "password": {"type": "string"}
+ }
+ }
+ }
+ },
+ {
+ "id": "UploadData",
+ "type": "object",
+ "properties": {
+ "bytes": {
+ "type": "any",
+ "optional": true,
+ "description": "An ArrayBuffer with a copy of the data."
+ },
+ "file": {
+ "type": "string",
+ "optional": true,
+ "description": "A string with the file's path and name."
+ }
+ },
+ "description": "Contains data uploaded in a URL request."
+ }
+ ],
+ "functions": [
+ {
+ "name": "handlerBehaviorChanged",
+ "type": "function",
+ "description": "Needs to be called when the behavior of the webRequest handlers has changed to prevent incorrect handling due to caching. This function call is expensive. Don't call it often.",
+ "async": "callback",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onBeforeRequest",
+ "type": "function",
+ "description": "Fired when a request is about to occur.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "requestBody": {
+ "type": "object",
+ "optional": true,
+ "description": "Contains the HTTP request body data. Only provided if extraInfoSpec contains 'requestBody'.",
+ "properties": {
+ "error": {"type": "string", "optional": true, "description": "Errors when obtaining request body data."},
+ "formData": {
+ "type": "object",
+ "optional": true,
+ "description": "If the request method is POST and the body is a sequence of key-value pairs encoded in UTF8, encoded as either multipart/form-data, or application/x-www-form-urlencoded, this dictionary is present and for each key contains the list of all values for that key. If the data is of another media type, or if it is malformed, the dictionary is not present. An example value of this dictionary is {'key': ['value1', 'value2']}.",
+ "properties": {},
+ "additionalProperties": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ },
+ "raw" : {
+ "type": "array",
+ "optional": true,
+ "items": {"$ref": "UploadData"},
+ "description": "If the request method is PUT or POST, and the body is not already parsed in formData, then the unparsed request body elements are contained in this array."
+ }
+ }
+ },
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnBeforeRequestOptions"
+ }
+ }
+ ],
+ "returns": {
+ "$ref": "BlockingResponse",
+ "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.",
+ "optional": true
+ }
+ },
+ {
+ "name": "onBeforeSendHeaders",
+ "type": "function",
+ "description": "Fired before sending an HTTP request, once the request headers are available. This may occur after a TCP connection is made to the server, but before any HTTP data is sent. ",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnBeforeSendHeadersOptions"
+ }
+ }
+ ],
+ "returns": {
+ "$ref": "BlockingResponse",
+ "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.",
+ "optional": true
+ }
+ },
+ {
+ "name": "onSendHeaders",
+ "type": "function",
+ "description": "Fired just before a request is going to be sent to the server (modifications of previous onBeforeSendHeaders callbacks are visible by the time onSendHeaders is fired).",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that have been sent out with this request."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnSendHeadersOptions"
+ }
+ }
+ ]
+ },
+ {
+ "name": "onHeadersReceived",
+ "type": "function",
+ "description": "Fired when HTTP response headers of a request have been received.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line)."},
+ "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that have been received with this response."},
+ "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnHeadersReceivedOptions"
+ }
+ }
+ ],
+ "returns": {
+ "$ref": "BlockingResponse",
+ "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.",
+ "optional": true
+ }
+ },
+ {
+ "name": "onAuthRequired",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when an authentication failure is received. The listener has three options: it can provide authentication credentials, it can cancel the request and display the error page, or it can take no action on the challenge. If bad user credentials are provided, this may be called multiple times for the same request.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "scheme": {"type": "string", "description": "The authentication scheme, e.g. Basic or Digest."},
+ "realm": {"type": "string", "description": "The authentication realm provided by the server, if there is one.", "optional": true},
+ "challenger": {"type": "object", "description": "The server requesting authentication.", "properties": {"host": {"type": "string"}, "port": {"type": "integer"}}},
+ "isProxy": {"type": "boolean", "description": "True for Proxy-Authenticate, false for WWW-Authenticate."},
+ "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."},
+ "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."},
+ "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}
+ }
+ },
+ {
+ "type": "function",
+ "optional": true,
+ "name": "callback",
+ "parameters": [
+ {"name": "response", "$ref": "BlockingResponse"}
+ ]
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnAuthRequiredOptions"
+ }
+ }
+ ],
+ "returns": {
+ "$ref": "BlockingResponse",
+ "description": "If \"blocking\" is specified in the \"extraInfoSpec\" parameter, the event listener should return an object of this type.",
+ "optional": true
+ }
+ },
+ {
+ "name": "onResponseStarted",
+ "type": "function",
+ "description": "Fired when the first byte of the response body is received. For HTTP requests, this means that the status line and response headers are available.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
+ "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
+ "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."},
+ "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."},
+ "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnResponseStartedOptions"
+ }
+ }
+ ]
+ },
+ {
+ "name": "onBeforeRedirect",
+ "type": "function",
+ "description": "Fired when a server-initiated redirect is about to occur.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
+ "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
+ "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."},
+ "redirectUrl": {"type": "string", "description": "The new URL."},
+ "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this redirect."},
+ "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnBeforeRedirectOptions"
+ }
+ }
+ ]
+ },
+ {
+ "name": "onCompleted",
+ "type": "function",
+ "description": "Fired when a request is completed.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
+ "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
+ "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."},
+ "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that were received along with this response."},
+ "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line) or an empty string if there are no headers."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "$ref": "OnCompletedOptions"
+ }
+ }
+ ]
+ },
+ {
+ "name": "onErrorOccurred",
+ "type": "function",
+ "description": "Fired when an error occurs.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
+ "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
+ "error": {"type": "string", "description": "The error description. This string is <em>not</em> guaranteed to remain backwards compatible between releases. You must not parse and act based upon its content."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/toolkit/components/extensions/test/mochitest/.eslintrc.js b/toolkit/components/extensions/test/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..53938410b7
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/.eslintrc.js
@@ -0,0 +1,35 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../../../../testing/mochitest/mochitest.eslintrc.js",
+
+ "env": {
+ "webextensions": true,
+ },
+
+ "globals": {
+ "ChromeWorker": false,
+ "onmessage": true,
+ "sendAsyncMessage": false,
+
+ "waitForLoad": true,
+ "promiseConsoleOutput": true,
+
+ "ExtensionTestUtils": false,
+ "NetUtil": true,
+ "webrequest_test": false,
+ "XPCOMUtils": true,
+
+ // head_webrequest.js symbols
+ "addStylesheet": true,
+ "addLink": true,
+ "addImage": true,
+ "addScript": true,
+ "addFrame": true,
+ "makeExtension": false,
+ },
+
+ "rules": {
+ "no-shadow": 0,
+ },
+};
diff --git a/toolkit/components/extensions/test/mochitest/chrome.ini b/toolkit/components/extensions/test/mochitest/chrome.ini
new file mode 100644
index 0000000000..26585cad7a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+support-files =
+ chrome_head.js
+ head.js
+ head_cookies.js
+ file_sample.html
+ webrequest_chromeworker.js
+ webrequest_test.jsm
+tags = webextensions
+
+[test_chrome_ext_background_debug_global.html]
+skip-if = (os == 'android') # android doesn't have devtools
+[test_chrome_ext_background_page.html]
+skip-if = (toolkit == 'android') # android doesn't have devtools
+[test_chrome_ext_eventpage_warning.html]
+[test_chrome_ext_contentscript_unrecognizedprop_warning.html]
+skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
+[test_chrome_ext_hybrid_addons.html]
+[test_chrome_ext_trustworthy_origin.html]
+[test_chrome_ext_webnavigation_resolved_urls.html]
+skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
+[test_chrome_ext_shutdown_cleanup.html]
+[test_chrome_native_messaging_paths.html]
+skip-if = os != "mac" && os != "linux"
+[test_ext_cookies_expiry.html]
+[test_ext_cookies_permissions_bad.html]
+[test_ext_cookies_permissions_good.html]
+[test_ext_cookies_containers.html]
+[test_ext_jsversion.html]
+[test_ext_schema.html]
+[test_chrome_ext_storage_cleanup.html]
+[test_chrome_ext_idle.html]
+[test_chrome_ext_downloads_saveAs.html]
+[test_chrome_ext_webrequest_background_events.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
diff --git a/toolkit/components/extensions/test/mochitest/chrome_head.js b/toolkit/components/extensions/test/mochitest/chrome_head.js
new file mode 100644
index 0000000000..da2f53a02b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/chrome_head.js
@@ -0,0 +1,12 @@
+"use strict";
+
+const {
+ classes: Cc,
+ interfaces: Ci,
+ utils: Cu,
+ results: Cr,
+} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html
new file mode 100644
index 0000000000..663ebc6112
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_WebNavigation_page2.html" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html
new file mode 100644
index 0000000000..cc1acc83d6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page2.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html
new file mode 100644
index 0000000000..a0a26a2e9d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_WebNavigation_page3.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<a id="elt" href="file_WebNavigation_page3.html#ref">click me</a>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html
new file mode 100644
index 0000000000..5807dd439f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+"use strict";
+window.close();
+</script>
+</head>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_csp.html b/toolkit/components/extensions/test/mochitest/file_csp.html
new file mode 100644
index 0000000000..206e443904
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_csp.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+<img id="bad-image" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" />
+<script id="bad-script" type="text/javascript" src="http://example.org/tests/toolkit/components/extensions/test/mochitest/file_script_bad.js"></script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_csp.html^headers^ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^
new file mode 100644
index 0000000000..4c6fa3c26a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_csp.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: default-src 'self'
diff --git a/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js b/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js
new file mode 100644
index 0000000000..06dfae65ec
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_ext_test_api_injection.js
@@ -0,0 +1,12 @@
+"use strict";
+
+var {interfaces: Ci} = Components;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+Services.console.registerListener(function listener(message) {
+ if (/WebExt Privilege Escalation/.test(message.message)) {
+ Services.console.unregisterListener(listener);
+ sendAsyncMessage("console-message", {message: message.message});
+ }
+});
diff --git a/toolkit/components/extensions/test/mochitest/file_image_bad.png b/toolkit/components/extensions/test/mochitest/file_image_bad.png
new file mode 100644
index 0000000000..4c3be50847
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_bad.png
Binary files differ
diff --git a/toolkit/components/extensions/test/mochitest/file_image_good.png b/toolkit/components/extensions/test/mochitest/file_image_good.png
new file mode 100644
index 0000000000..769c636340
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_good.png
Binary files differ
diff --git a/toolkit/components/extensions/test/mochitest/file_image_redirect.png b/toolkit/components/extensions/test/mochitest/file_image_redirect.png
new file mode 100644
index 0000000000..4c3be50847
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_image_redirect.png
Binary files differ
diff --git a/toolkit/components/extensions/test/mochitest/file_mixed.html b/toolkit/components/extensions/test/mochitest/file_mixed.html
new file mode 100644
index 0000000000..f3c7dda580
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_mixed.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+<img id="bad-image" src="http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png" />
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_permission_xhr.html b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html
new file mode 100644
index 0000000000..22a55f90d2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_permission_xhr.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<script>
+"use strict";
+
+/* globals privilegedFetch, privilegedXHR */
+/* eslint-disable mozilla/balanced-listeners */
+
+addEventListener("message", function rcv(event) {
+ removeEventListener("message", rcv, false);
+
+ function assertTrue(condition, description) {
+ postMessage({msg: "assertTrue", condition, description}, "*");
+ }
+
+ function passListener() {
+ assertTrue(true, "Content XHR has no elevated privileges");
+ postMessage({"msg": "finish"}, "*");
+ }
+
+ function failListener() {
+ assertTrue(false, "Content XHR has no elevated privileges");
+ postMessage({"msg": "finish"}, "*");
+ }
+
+ try {
+ new privilegedXHR();
+ assertTrue(false, "Content should not have access to privileged XHR constructor");
+ } catch (e) {
+ assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged XHR constructor");
+ }
+
+ try {
+ new privilegedFetch();
+ assertTrue(false, "Content should not have access to privileged fetch() constructor");
+ } catch (e) {
+ assertTrue(/Permission denied to access object/.test(e), "Content should not have access to privileged fetch() constructor");
+ }
+
+ let req = new XMLHttpRequest();
+ req.addEventListener("load", failListener);
+ req.addEventListener("error", passListener);
+ req.open("GET", "http://example.org/example.txt");
+ req.send();
+}, false);
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html b/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html
new file mode 100644
index 0000000000..258f7058d9
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_privilege_escalation.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+ <script type="text/javascript">
+ "use strict";
+ throw new Error(`WebExt Privilege Escalation: typeof(browser) = ${typeof(browser)}`);
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_sample.html b/toolkit/components/extensions/test/mochitest/file_sample.html
new file mode 100644
index 0000000000..a20e49a1f0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_sample.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div id="test">Sample text</div>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_script_bad.js b/toolkit/components/extensions/test/mochitest/file_script_bad.js
new file mode 100644
index 0000000000..c425122c71
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_bad.js
@@ -0,0 +1,3 @@
+"use strict";
+
+window.failure = true;
diff --git a/toolkit/components/extensions/test/mochitest/file_script_good.js b/toolkit/components/extensions/test/mochitest/file_script_good.js
new file mode 100644
index 0000000000..1848edf686
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_good.js
@@ -0,0 +1,3 @@
+"use strict";
+
+window.success = window.success ? window.success + 1 : 1;
diff --git a/toolkit/components/extensions/test/mochitest/file_script_redirect.js b/toolkit/components/extensions/test/mochitest/file_script_redirect.js
new file mode 100644
index 0000000000..c89a196c2a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_redirect.js
@@ -0,0 +1,4 @@
+"use strict";
+
+window.failure = true;
+
diff --git a/toolkit/components/extensions/test/mochitest/file_script_xhr.js b/toolkit/components/extensions/test/mochitest/file_script_xhr.js
new file mode 100644
index 0000000000..07f80eb2ea
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_script_xhr.js
@@ -0,0 +1,5 @@
+"use strict";
+
+var request = new XMLHttpRequest();
+request.open("get", "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/xhr_resource", false);
+request.send();
diff --git a/toolkit/components/extensions/test/mochitest/file_style_bad.css b/toolkit/components/extensions/test/mochitest/file_style_bad.css
new file mode 100644
index 0000000000..8dbc8dc7a4
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_style_bad.css
@@ -0,0 +1,3 @@
+#test {
+ color: green !important;
+}
diff --git a/toolkit/components/extensions/test/mochitest/file_style_good.css b/toolkit/components/extensions/test/mochitest/file_style_good.css
new file mode 100644
index 0000000000..46f9774b5f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_style_good.css
@@ -0,0 +1,3 @@
+#test {
+ color: red;
+}
diff --git a/toolkit/components/extensions/test/mochitest/file_style_redirect.css b/toolkit/components/extensions/test/mochitest/file_style_redirect.css
new file mode 100644
index 0000000000..8dbc8dc7a4
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_style_redirect.css
@@ -0,0 +1,3 @@
+#test {
+ color: green !important;
+}
diff --git a/toolkit/components/extensions/test/mochitest/file_teardown_test.js b/toolkit/components/extensions/test/mochitest/file_teardown_test.js
new file mode 100644
index 0000000000..7246012add
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_teardown_test.js
@@ -0,0 +1,24 @@
+"use strict";
+
+/* globals addMessageListener */
+let {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+let events = [];
+function record(type, extensionContext) {
+ let eventType = type == "proxy-context-load" ? "load" : "unload";
+ let url = extensionContext.uri.spec;
+ let extensionId = extensionContext.extension.id;
+ events.push({eventType, url, extensionId});
+}
+
+Management.on("proxy-context-load", record);
+Management.on("proxy-context-unload", record);
+addMessageListener("cleanup", () => {
+ Management.off("proxy-context-load", record);
+ Management.off("proxy-context-unload", record);
+});
+
+addMessageListener("get-context-events", extensionId => {
+ sendAsyncMessage("context-events", events);
+ events = [];
+});
+sendAsyncMessage("chromescript-startup");
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html
new file mode 100644
index 0000000000..cba3043f71
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+
+<html>
+ <head>
+ <meta http-equiv="refresh" content="1;dummy_page.html">
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html
new file mode 100644
index 0000000000..c5b436979f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+
+<html>
+ <head>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^
new file mode 100644
index 0000000000..574a392a15
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_clientRedirect_httpHeaders.html^headers^
@@ -0,0 +1 @@
+Refresh: 1;url=dummy_page.html
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html
new file mode 100644
index 0000000000..d360bcbb13
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameClientRedirect.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_webNavigation_clientRedirect.html" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html
new file mode 100644
index 0000000000..06dbd43741
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_frameRedirect.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="redirection.sjs" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html
new file mode 100644
index 0000000000..307990714b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<body>
+
+<iframe src="file_webNavigation_manualSubframe_page1.html" width="200" height="200"></iframe>
+
+<form>
+</form>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html
new file mode 100644
index 0000000000..55bb7aa6ae
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+
+<html>
+ <body>
+ <h1>page1</h1>
+ <a href="file_webNavigation_manualSubframe_page2.html">page2</a>
+ </body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html
new file mode 100644
index 0000000000..8f589f8bbd
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_webNavigation_manualSubframe_page2.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+ <body>
+ <h1>page2</h1>
+ </body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/file_with_about_blank.html b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html
new file mode 100644
index 0000000000..af51c2e52a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe id="a_b" src="about:blank"></iframe>
+ <iframe srcdoc="galactica actual" src="adama"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/head.js b/toolkit/components/extensions/test/mochitest/head.js
new file mode 100644
index 0000000000..1b1a294726
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head.js
@@ -0,0 +1,13 @@
+"use strict";
+
+/* exported waitForLoad */
+
+function waitForLoad(win) {
+ return new Promise(resolve => {
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, true);
+ resolve();
+ }, true);
+ });
+}
+
diff --git a/toolkit/components/extensions/test/mochitest/head_cookies.js b/toolkit/components/extensions/test/mochitest/head_cookies.js
new file mode 100644
index 0000000000..9f69665511
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_cookies.js
@@ -0,0 +1,167 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported testCookies */
+
+function* testCookies(options) {
+ // Changing the options object is a bit of a hack, but it allows us to easily
+ // pass an expiration date to the background script.
+ options.expiry = Date.now() / 1000 + 3600;
+
+ async function background(backgroundOptions) {
+ // Ask the parent scope to change some cookies we may or may not have
+ // permission for.
+ let awaitChanges = new Promise(resolve => {
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("cookies-changed", msg, "browser.test.onMessage");
+ resolve();
+ });
+ });
+
+ let changed = [];
+ browser.cookies.onChanged.addListener(event => {
+ changed.push(`${event.cookie.name}:${event.cause}`);
+ });
+ browser.test.sendMessage("change-cookies");
+
+
+ // Try to access some cookies in various ways.
+ let {url, domain, secure} = backgroundOptions;
+
+ let failures = 0;
+ let tallyFailure = error => {
+ failures++;
+ };
+
+ try {
+ await awaitChanges;
+
+ let cookie = await browser.cookies.get({url, name: "foo"});
+ browser.test.assertEq(backgroundOptions.shouldPass, cookie != null, "should pass == get cookie");
+
+ let cookies = await browser.cookies.getAll({domain});
+ if (backgroundOptions.shouldPass) {
+ browser.test.assertEq(2, cookies.length, "expected number of cookies");
+ } else {
+ browser.test.assertEq(0, cookies.length, "expected number of cookies");
+ }
+
+ await Promise.all([
+ browser.cookies.set({url, domain, secure, name: "foo", "value": "baz", expirationDate: backgroundOptions.expiry}).catch(tallyFailure),
+ browser.cookies.set({url, domain, secure, name: "bar", "value": "quux", expirationDate: backgroundOptions.expiry}).catch(tallyFailure),
+ browser.cookies.remove({url, name: "deleted"}),
+ ]);
+
+ if (backgroundOptions.shouldPass) {
+ // The order of eviction events isn't guaranteed, so just check that
+ // it's there somewhere.
+ let evicted = changed.indexOf("evicted:evicted");
+ if (evicted < 0) {
+ browser.test.fail("got no eviction event");
+ } else {
+ browser.test.succeed("got eviction event");
+ changed.splice(evicted, 1);
+ }
+
+ browser.test.assertEq("x:explicit,x:overwrite,x:explicit,x:explicit,foo:overwrite,foo:explicit,bar:explicit,deleted:explicit",
+ changed.join(","), "expected changes");
+ } else {
+ browser.test.assertEq("", changed.join(","), "expected no changes");
+ }
+
+ if (!(backgroundOptions.shouldPass || backgroundOptions.shouldWrite)) {
+ browser.test.assertEq(2, failures, "Expected failures");
+ } else {
+ browser.test.assertEq(0, failures, "Expected no failures");
+ }
+
+ browser.test.notifyPass("cookie-permissions");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("cookie-permissions");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": options.permissions,
+ },
+
+ background: `(${background})(${JSON.stringify(options)})`,
+ });
+
+
+ let cookieSvc = SpecialPowers.Services.cookies;
+
+ let domain = options.domain.replace(/^\.?/, ".");
+
+ // This will be evicted after we add a fourth cookie.
+ cookieSvc.add(domain, "/", "evicted", "bar", options.secure, false, false, options.expiry);
+ // This will be modified by the background script.
+ cookieSvc.add(domain, "/", "foo", "bar", options.secure, false, false, options.expiry);
+ // This will be deleted by the background script.
+ cookieSvc.add(domain, "/", "deleted", "bar", options.secure, false, false, options.expiry);
+
+
+ yield extension.startup();
+
+ yield extension.awaitMessage("change-cookies");
+ cookieSvc.add(domain, "/", "x", "y", options.secure, false, false, options.expiry);
+ cookieSvc.add(domain, "/", "x", "z", options.secure, false, false, options.expiry);
+ cookieSvc.remove(domain, "x", "/", false, {});
+ extension.sendMessage("cookies-changed");
+
+ yield extension.awaitFinish("cookie-permissions");
+ yield extension.unload();
+
+
+ function getCookies(host) {
+ let cookies = [];
+ let enum_ = cookieSvc.getCookiesFromHost(host, {});
+ while (enum_.hasMoreElements()) {
+ cookies.push(enum_.getNext().QueryInterface(SpecialPowers.Ci.nsICookie2));
+ }
+ return cookies.sort((a, b) => String.localeCompare(a.name, b.name));
+ }
+
+ let cookies = getCookies(options.domain);
+ info(`Cookies: ${cookies.map(c => `${c.name}=${c.value}`)}`);
+
+ if (options.shouldPass) {
+ is(cookies.length, 2, "expected two cookies for host");
+
+ is(cookies[0].name, "bar", "correct cookie name");
+ is(cookies[0].value, "quux", "correct cookie value");
+
+ is(cookies[1].name, "foo", "correct cookie name");
+ is(cookies[1].value, "baz", "correct cookie value");
+ } else if (options.shouldWrite) {
+ // Note: |shouldWrite| applies only when |shouldPass| is false.
+ // This is necessary because, unfortunately, websites (and therefore web
+ // extensions) are allowed to write some cookies which they're not allowed
+ // to read.
+ is(cookies.length, 3, "expected three cookies for host");
+
+ is(cookies[0].name, "bar", "correct cookie name");
+ is(cookies[0].value, "quux", "correct cookie value");
+
+ is(cookies[1].name, "deleted", "correct cookie name");
+
+ is(cookies[2].name, "foo", "correct cookie name");
+ is(cookies[2].value, "baz", "correct cookie value");
+ } else {
+ is(cookies.length, 2, "expected two cookies for host");
+
+ is(cookies[0].name, "deleted", "correct second cookie name");
+
+ is(cookies[1].name, "foo", "correct cookie name");
+ is(cookies[1].value, "bar", "correct cookie value");
+ }
+
+ for (let cookie of cookies) {
+ cookieSvc.remove(cookie.host, cookie.name, "/", false, {});
+ }
+ // Make sure we don't silently poison subsequent tests if something goes wrong.
+ is(getCookies(options.domain).length, 0, "cookies cleared");
+}
diff --git a/toolkit/components/extensions/test/mochitest/head_webrequest.js b/toolkit/components/extensions/test/mochitest/head_webrequest.js
new file mode 100644
index 0000000000..96924e505c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -0,0 +1,331 @@
+"use strict";
+
+let commonEvents = {
+ "onBeforeRequest": [{urls: ["<all_urls>"]}, ["blocking"]],
+ "onBeforeSendHeaders": [{urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]],
+ "onSendHeaders": [{urls: ["<all_urls>"]}, ["requestHeaders"]],
+ "onBeforeRedirect": [{urls: ["<all_urls>"]}],
+ "onHeadersReceived": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
+ "onResponseStarted": [{urls: ["<all_urls>"]}],
+ "onCompleted": [{urls: ["<all_urls>"]}, ["responseHeaders"]],
+ "onErrorOccurred": [{urls: ["<all_urls>"]}],
+};
+
+function background(events) {
+ let expect;
+ let ignore;
+ let defaultOrigin;
+
+ browser.test.onMessage.addListener((msg, expected) => {
+ if (msg !== "set-expected") {
+ return;
+ }
+ expect = expected.expect;
+ defaultOrigin = expected.origin;
+ ignore = expected.ignore;
+ let promises = [];
+ // Initialize some stuff we'll need in the tests.
+ for (let entry of Object.values(expect)) {
+ // a place for the test infrastructure to store some state.
+ entry.test = {};
+ // Each entry in expected gets a Promise that will be resolved in the
+ // last event for that entry. This will either be onCompleted, or the
+ // last entry if an events list was provided.
+ promises.push(new Promise(resolve => { entry.test.resolve = resolve; }));
+ // If events was left undefined, we're expecting all normal events we're
+ // listening for, exclude onBeforeRedirect and onErrorOccurred
+ if (entry.events === undefined) {
+ entry.events = Object.keys(events).filter(name => name != "onErrorOccurred" && name != "onBeforeRedirect");
+ }
+ if (entry.optional_events === undefined) {
+ entry.optional_events = [];
+ }
+ }
+ // When every expected entry has finished our test is done.
+ Promise.all(promises).then(() => {
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("continue");
+ });
+
+ // Retrieve the per-file/test expected values.
+ function getExpected(details) {
+ let url = new URL(details.url);
+ let filename;
+ if (url.protocol == "data:") {
+ // pathname is everything after protocol.
+ filename = url.pathname;
+ } else {
+ filename = url.pathname.split("/").pop();
+ }
+ if (ignore && ignore.includes(filename)) {
+ return;
+ }
+ let expected = expect[filename];
+ if (!expected) {
+ browser.test.fail(`unexpected request ${filename}`);
+ return;
+ }
+ // Save filename for redirect verification.
+ expected.test.filename = filename;
+ return expected;
+ }
+
+ // Process any test header modifications that can happen in request or response phases.
+ // If a test includes headers, it needs a complete header object, no undefined
+ // objects even if empty:
+ // request: {
+ // add: {"HeaderName": "value",},
+ // modify: {"HeaderName": "value",},
+ // remove: ["HeaderName",],
+ // },
+ // response: {
+ // add: {"HeaderName": "value",},
+ // modify: {"HeaderName": "value",},
+ // remove: ["HeaderName",],
+ // },
+ function processHeaders(phase, expected, details) {
+ // This should only happen once per phase [request|response].
+ browser.test.assertFalse(!!expected.test[phase], `First processing of headers for ${phase}`);
+ expected.test[phase] = true;
+
+ let headers = details[`${phase}Headers`];
+ browser.test.assertTrue(Array.isArray(headers), `${phase}Headers array present`);
+
+ let {add, modify, remove} = expected.headers[phase];
+
+ for (let name in add) {
+ browser.test.assertTrue(!headers.find(h => h.name === name), `header ${name} to be added not present yet in ${phase}Headers`);
+ let header = {name: name};
+ if (name.endsWith("-binary")) {
+ header.binaryValue = Array.from(add[name], c => c.charCodeAt(0));
+ } else {
+ header.value = add[name];
+ }
+ headers.push(header);
+ }
+
+ let modifiedAny = false;
+ for (let header of headers) {
+ if (header.name.toLowerCase() in modify) {
+ header.value = modify[header.name.toLowerCase()];
+ modifiedAny = true;
+ }
+ }
+ browser.test.assertTrue(modifiedAny, `at least one ${phase}Headers element to modify`);
+
+ let deletedAny = false;
+ for (let j = headers.length; j-- > 0;) {
+ if (remove.includes(headers[j].name.toLowerCase())) {
+ headers.splice(j, 1);
+ deletedAny = true;
+ }
+ }
+ browser.test.assertTrue(deletedAny, `at least one ${phase}Headers element to delete`);
+
+ return headers;
+ }
+
+ // phase is request or response.
+ function checkHeaders(phase, expected, details) {
+ if (!/^https?:/.test(details.url)) {
+ return;
+ }
+
+ let headers = details[`${phase}Headers`];
+ browser.test.assertTrue(Array.isArray(headers), `valid ${phase}Headers array`);
+
+ let {add, modify, remove} = expected.headers[phase];
+ for (let name in add) {
+ let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value;
+ browser.test.assertEq(value, add[name], `header ${name} correctly injected in ${phase}Headers`);
+ }
+
+ for (let name in modify) {
+ let value = headers.find(h => h.name.toLowerCase() === name.toLowerCase()).value;
+ browser.test.assertEq(value, modify[name], `header ${name} matches modified value`);
+ }
+
+ for (let name of remove) {
+ let found = headers.find(h => h.name.toLowerCase() === name.toLowerCase());
+ browser.test.assertFalse(!!found, `deleted header ${name} still found in ${phase}Headers`);
+ }
+ }
+
+ function getListener(name) {
+ return details => {
+ let result = {};
+ browser.test.log(`${name} ${details.requestId} ${details.url}`);
+ let expected = getExpected(details);
+ if (!expected) {
+ return result;
+ }
+ let expectedEvent = expected.events[0] == name;
+ if (expectedEvent) {
+ expected.events.shift();
+ } else {
+ expectedEvent = expected.optional_events[0] == name;
+ if (expectedEvent) {
+ expected.optional_events.shift();
+ }
+ }
+ browser.test.assertTrue(expectedEvent, `received ${name}`);
+ browser.test.assertEq(expected.type, details.type, "resource type is correct");
+ browser.test.assertEq(expected.origin || defaultOrigin, details.originUrl, "origin is correct");
+
+ if (name == "onBeforeRequest") {
+ // Save some values to test request consistency in later events.
+ browser.test.assertTrue(details.tabId !== undefined, `tabId ${details.tabId}`);
+ browser.test.assertTrue(details.requestId !== undefined, `requestId ${details.requestId}`);
+ // Validate requestId if it's already set, this happens with redirects.
+ if (expected.test.requestId !== undefined) {
+ browser.test.assertEq("string", typeof expected.test.requestId, `requestid ${expected.test.requestId} is string`);
+ browser.test.assertEq("string", typeof details.requestId, `requestid ${details.requestId} is string`);
+ browser.test.assertEq("number", typeof parseInt(details.requestId, 10), "parsed requestid is number");
+ browser.test.assertNotEq(expected.test.requestId, details.requestId,
+ `last requestId ${expected.test.requestId} different from this one ${details.requestId}`);
+ } else {
+ // Save any values we want to validate in later events.
+ expected.test.requestId = details.requestId;
+ expected.test.tabId = details.tabId;
+ }
+ // Tests we don't need to do every event.
+ browser.test.assertTrue(details.type.toUpperCase() in browser.webRequest.ResourceType, `valid resource type ${details.type}`);
+ if (details.type == "main_frame") {
+ browser.test.assertEq(0, details.frameId, "frameId is zero when type is main_frame bug 1329299");
+ }
+ } else {
+ // On events after onBeforeRequest, check the previous values.
+ browser.test.assertEq(expected.test.requestId, details.requestId, "correct requestId");
+ browser.test.assertEq(expected.test.tabId, details.tabId, "correct tabId");
+ }
+ if (name == "onBeforeSendHeaders") {
+ if (expected.headers && expected.headers.request) {
+ result.requestHeaders = processHeaders("request", expected, details);
+ }
+ if (expected.redirect) {
+ browser.test.log(`${name} redirect request`);
+ result.redirectUrl = details.url.replace(expected.test.filename, expected.redirect);
+ }
+ }
+ if (name == "onSendHeaders") {
+ if (expected.headers && expected.headers.request) {
+ checkHeaders("request", expected, details);
+ }
+ }
+ if (name == "onHeadersReceived") {
+ browser.test.assertEq(expected.status || 200, details.statusCode,
+ `expected HTTP status received for ${details.url}`);
+ if (expected.headers && expected.headers.response) {
+ result.responseHeaders = processHeaders("response", expected, details);
+ }
+ }
+ if (name == "onCompleted") {
+ // If we have already completed a GET request for this url,
+ // and it was found, we expect for the response to come fromCache.
+ // expected.cached may be undefined, force boolean.
+ let expectCached = !!expected.cached && details.method === "GET" && details.statusCode != 404;
+ browser.test.assertEq(expectCached, details.fromCache, "fromCache is correct");
+ // We can only tell IPs for non-cached HTTP requests.
+ if (!details.fromCache && /^https?:/.test(details.url)) {
+ browser.test.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`);
+ }
+ if (expected.headers && expected.headers.response) {
+ checkHeaders("response", expected, details);
+ }
+ }
+
+ if (expected.cancel && expected.cancel == name) {
+ browser.test.log(`${name} cancel request`);
+ browser.test.sendMessage("cancelled");
+ result.cancel = true;
+ }
+ // If we've used up all the events for this test, resolve the promise.
+ // If something wrong happens and more events come through, there will be
+ // failures.
+ if (expected.events.length <= 0) {
+ expected.test.resolve();
+ }
+ return result;
+ };
+ }
+
+ for (let [name, args] of Object.entries(events)) {
+ browser.test.log(`adding listener for ${name}`);
+ try {
+ browser.webRequest[name].addListener(getListener(name), ...args);
+ } catch (e) {
+ browser.test.assertTrue(/\brequestBody\b/.test(e.message),
+ "Request body is unsupported");
+
+ // RequestBody is disabled in release builds.
+ if (!/\brequestBody\b/.test(e.message)) {
+ throw e;
+ }
+
+ args.splice(args.indexOf("requestBody"), 1);
+ browser.webRequest[name].addListener(getListener(name), ...args);
+ }
+ }
+}
+
+/* exported makeExtension */
+
+function makeExtension(events = commonEvents) {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ },
+ background: `(${background})(${JSON.stringify(events)})`,
+ });
+}
+
+/* exported addStylesheet */
+
+function addStylesheet(file) {
+ let link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("href", file);
+ document.body.appendChild(link);
+}
+
+/* exported addLink */
+
+function addLink(file) {
+ let a = document.createElement("a");
+ a.setAttribute("href", file);
+ a.setAttribute("target", "_blank");
+ document.body.appendChild(a);
+ return a;
+}
+
+/* exported addImage */
+
+function addImage(file) {
+ let img = document.createElement("img");
+ img.setAttribute("src", file);
+ document.body.appendChild(img);
+}
+
+/* exported addScript */
+
+function addScript(file) {
+ let script = document.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("src", file);
+ document.getElementsByTagName("head").item(0).appendChild(script);
+}
+
+/* exported addFrame */
+
+function addFrame(file) {
+ let frame = document.createElement("iframe");
+ frame.setAttribute("width", "200");
+ frame.setAttribute("height", "200");
+ frame.setAttribute("src", file);
+ document.body.appendChild(frame);
+}
diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..45586237e6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -0,0 +1,114 @@
+[DEFAULT]
+support-files =
+ head.js
+ file_mixed.html
+ head_webrequest.js
+ file_csp.html
+ file_csp.html^headers^
+ file_WebRequest_page3.html
+ file_webNavigation_clientRedirect.html
+ file_webNavigation_clientRedirect_httpHeaders.html
+ file_webNavigation_clientRedirect_httpHeaders.html^headers^
+ file_webNavigation_frameClientRedirect.html
+ file_webNavigation_frameRedirect.html
+ file_webNavigation_manualSubframe.html
+ file_webNavigation_manualSubframe_page1.html
+ file_webNavigation_manualSubframe_page2.html
+ file_WebNavigation_page1.html
+ file_WebNavigation_page2.html
+ file_WebNavigation_page3.html
+ file_with_about_blank.html
+ file_image_good.png
+ file_image_bad.png
+ file_image_redirect.png
+ file_style_good.css
+ file_style_bad.css
+ file_style_redirect.css
+ file_script_good.js
+ file_script_bad.js
+ file_script_redirect.js
+ file_script_xhr.js
+ file_sample.html
+ redirection.sjs
+ file_privilege_escalation.html
+ file_ext_test_api_injection.js
+ file_permission_xhr.html
+ file_teardown_test.js
+ return_headers.sjs
+ webrequest_worker.js
+tags = webextensions
+
+[test_clipboard.html]
+# skip-if = # disabled test case with_permission_allow_copy, see inline comment.
+[test_ext_inIncognitoContext_window.html]
+skip-if = os == 'android' # Android does not currently support windows.
+[test_ext_geturl.html]
+[test_ext_background_canvas.html]
+[test_ext_content_security_policy.html]
+[test_ext_contentscript.html]
+[test_ext_contentscript_api_injection.html]
+[test_ext_contentscript_async_loading.html]
+[test_ext_contentscript_context.html]
+[test_ext_contentscript_create_iframe.html]
+[test_ext_contentscript_devtools_metadata.html]
+[test_ext_contentscript_exporthelpers.html]
+[test_ext_contentscript_css.html]
+[test_ext_contentscript_about_blank.html]
+[test_ext_contentscript_permission.html]
+skip-if = os == 'android' # Android does not support tabs API. Bug 1260250
+[test_ext_contentscript_teardown.html]
+skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
+[test_ext_exclude_include_globs.html]
+[test_ext_i18n_css.html]
+[test_ext_generate.html]
+[test_ext_notifications.html]
+[test_ext_permission_xhr.html]
+[test_ext_runtime_connect.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_runtime_connect_twoway.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_runtime_connect2.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_runtime_disconnect.html]
+[test_ext_runtime_id.html]
+[test_ext_sandbox_var.html]
+[test_ext_sendmessage_reply.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_sendmessage_reply2.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_sendmessage_doublereply.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_sendmessage_no_receiver.html]
+[test_ext_storage_content.html]
+[test_ext_storage_tab.html]
+skip-if = os == 'android' # Android does not currently support tabs.
+[test_ext_test.html]
+[test_ext_cookies.html]
+skip-if = os == 'android' # Bug 1258975 on android.
+[test_ext_background_api_injection.html]
+[test_ext_background_generated_url.html]
+[test_ext_background_teardown.html]
+[test_ext_tab_teardown.html]
+skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
+[test_ext_unload_frame.html]
+[test_ext_i18n.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
+[test_ext_listener_proxies.html]
+[test_ext_web_accessible_resources.html]
+skip-if = (os == 'android') # Bug 1258975 on android.
+[test_ext_webrequest_background_events.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webrequest_basic.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webrequest_suspend.html]
+skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webrequest_upload.html]
+skip-if = release_or_beta || os == 'android' # webrequest api unsupported (bug 1258975).
+[test_ext_webnavigation.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_webnavigation_filters.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_window_postMessage.html]
+[test_ext_subframes_privileges.html]
+skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
+[test_ext_xhr_capabilities.html]
diff --git a/toolkit/components/extensions/test/mochitest/redirection.sjs b/toolkit/components/extensions/test/mochitest/redirection.sjs
new file mode 100644
index 0000000000..370ecd213f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/redirection.sjs
@@ -0,0 +1,4 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+ aResponse.setHeader("Location", "./dummy_page.html");
+}
diff --git a/toolkit/components/extensions/test/mochitest/return_headers.sjs b/toolkit/components/extensions/test/mochitest/return_headers.sjs
new file mode 100644
index 0000000000..54e2e5fb4d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/return_headers.sjs
@@ -0,0 +1,20 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported handleRequest */
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ let headers = {};
+ // Why on earth is this a nsISimpleEnumerator...
+ let enumerator = request.headers;
+ while (enumerator.hasMoreElements()) {
+ let header = enumerator.getNext().data;
+ headers[header.toLowerCase()] = request.getHeader(header);
+ }
+
+ response.write(JSON.stringify(headers));
+}
+
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
new file mode 100644
index 0000000000..0edf5ea86a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+const {
+ XPIProvider,
+} = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+
+/**
+ * This test is asserting that ext-backgroundPage.js successfully sets its
+ * debug global in the AddonWrapper provided by XPIProvider.jsm
+ *
+ * It does _not_ test any functionality in devtools and does not guarantee
+ * debugging is actually working correctly end-to-end.
+ */
+
+function background() {
+ window.testThing = "test!";
+ browser.test.notifyPass("background script ran");
+}
+
+const ID = "debug@tests.mozilla.org";
+let extensionData = {
+ useAddonManager: "temporary",
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ },
+};
+
+add_task(function* () {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("background script ran");
+
+ yield new Promise(function(resolve) {
+ window.BrowserToolboxProcess.emit("connectionchange", "opened", {
+ setAddonOptions(id, options) {
+ if (id === ID) {
+ let context = Cu.waiveXrays(options.global);
+ ok(context.chrome, "global context has a chrome object");
+ ok(context.browser, "global context has a browser object");
+ is("test!", context.testThing, "global context is the background script context");
+ resolve();
+ }
+ },
+ });
+ });
+
+ let addon = yield new Promise((resolve, reject) => {
+ AddonManager.getAddonByID(ID, addon => addon ? resolve(addon) : reject());
+ });
+
+ ok(addon, `Got the addon wrapper for ${addon.id}`);
+
+ function waitForDebugGlobalChanges(times, initialAddonInstanceID) {
+ return new Promise((resolve) => {
+ AddonManager.addAddonListener({
+ count: 0,
+ notNullGlobalsCount: 0,
+ undefinedPrivateWrappersCount: 0,
+ lastAddonInstanceID: initialAddonInstanceID,
+ onPropertyChanged(newAddon, changedPropNames) {
+ if (newAddon.id != addon.id ||
+ !changedPropNames.includes("debugGlobal")) {
+ return;
+ }
+
+ ok(!(newAddon.setDebugGlobal) && !(newAddon.getDebugGlobal),
+ "The addon wrapper should not be a PrivateWrapper");
+
+ let activeAddon = XPIProvider.activeAddons.get(addon.id);
+
+ let addonInstanceID;
+
+ if (!activeAddon) {
+ // The addon has been disable, the preferred global should be null
+ addonInstanceID = this.lastAddonInstanceID;
+ delete this.lastAddonInstanceID;
+ } else {
+ addonInstanceID = activeAddon.instanceID;
+ this.lastAddonInstanceID = addonInstanceID;
+ }
+
+ ok(addonInstanceID, `Got the addon instanceID for ${addon.id}`);
+
+ AddonManager.getAddonByInstanceID(addonInstanceID).then((privateWrapper) => {
+ this.count += 1;
+
+ if (!privateWrapper) {
+ // The addon has been uninstalled
+ this.undefinedPrivateWrappersCount += 1;
+ } else {
+ ok((privateWrapper.getDebugGlobal), "Got the addon PrivateWrapper");
+
+ if (privateWrapper.getDebugGlobal()) {
+ this.notNullGlobalsCount += 1;
+ }
+ }
+
+ if (this.count == times) {
+ AddonManager.removeAddonListener(this);
+ resolve({
+ counters: {
+ count: this.count,
+ notNullGlobalsCount: this.notNullGlobalsCount,
+ undefinedPrivateWrappersCount: this.undefinedPrivateWrappersCount,
+ },
+ lastAddonInstanceID: this.lastAddonInstanceID,
+ });
+ }
+ });
+ },
+ });
+ });
+ }
+
+ // two calls expected, one for the shutdown and one for the startup
+ // of the background page.
+ let waitForDebugGlobalChangesOnReload = waitForDebugGlobalChanges(2);
+
+ info("Addon reload...");
+ yield addon.reload();
+
+ info("Addon completed startup after reload");
+
+ let {
+ counters: reloadCounters,
+ lastAddonInstanceID,
+ } = yield waitForDebugGlobalChangesOnReload;
+
+ isDeeply(reloadCounters, {count: 2, notNullGlobalsCount: 1, undefinedPrivateWrappersCount: 0},
+ "Got the expected number of onPropertyChanged calls on reload");
+
+ // one more call expected for the shutdown.
+ let waitForDebugGlobalChangesOnShutdown = waitForDebugGlobalChanges(1, lastAddonInstanceID);
+
+ info("extension unloading...");
+ yield extension.unload();
+ info("extension unloaded");
+
+ let {counters: unloadCounters} = yield waitForDebugGlobalChangesOnShutdown;
+
+ isDeeply(unloadCounters, {count: 1, notNullGlobalsCount: 0, undefinedPrivateWrappersCount: 1},
+ "Got the expected number of onPropertyChanged calls on shutdown");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
new file mode 100644
index 0000000000..3c47746527
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* testAlertNotShownInBackgroundWindow() {
+ ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(),
+ "Alerts should not be present at the start of the test.");
+
+ let consoleOpened = TestUtils.topicObserved("web-console-created");
+
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ browser.test.log("background script executed");
+
+ alert("I am an alert in the background.");
+
+ browser.test.notifyPass("alertCalled");
+ },
+ });
+
+ yield extension.startup();
+
+ info("startup complete loaded");
+
+ yield extension.awaitFinish("alertCalled");
+
+
+ let alertWindows = Services.wm.getEnumerator("alert:alert");
+ ok(!alertWindows.hasMoreElements(), "Should not show alert");
+
+
+ // Make sure the message we output to the console is seen.
+ // This message is in ext-backgroundPage.js
+ let events = Cc["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Ci.nsIConsoleAPIStorage).getEvents();
+
+ // This is the warning that is output after the first `alert()` call is made.
+ let alertWarningEvent = events[events.length - 2];
+ is(alertWarningEvent.arguments[0], "alert() is not supported in background windows; please use console.log instead.");
+
+ // This is the actual alert text that should be present in the console
+ // instead of as an `alert`.
+ let alertEvent = events[events.length - 1];
+ is(alertEvent.arguments[0], "I am an alert in the background.");
+
+
+ // Wait for the browser console window to open.
+ yield consoleOpened;
+
+ let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ require("devtools/client/framework/devtools-browser");
+ let hudservice = require("devtools/client/webconsole/hudservice");
+
+ // And then double check that we have an actual browser console.
+ let haveConsole = !!hudservice.getBrowserConsole();
+ ok(haveConsole, "Expected browser console to be open");
+
+ if (haveConsole) {
+ yield hudservice.toggleBrowserConsole();
+ }
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html
new file mode 100644
index 0000000000..e08121a8fb
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script unrecognized property on manifest</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
+
+add_task(function* test_contentscript() {
+ function background() {
+ browser.runtime.onMessage.addListener(async (msg) => {
+ if (msg == "loaded") {
+ // NOTE: we're removing the tab from here because doing a win.close()
+ // from the chrome test code is raising a "TypeError: can't access
+ // dead object" exception.
+ let tabs = await browser.tabs.query({active: true, currentWindow: true});
+ await browser.tabs.remove(tabs[0].id);
+
+ browser.test.notifyPass("content-script-loaded");
+ }
+ });
+ }
+
+ function contentScript() {
+ chrome.runtime.sendMessage("loaded");
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ "unrecognized_property": "with-a-random-value",
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{
+ message: /Reading manifest: Error processing content_scripts.*.unrecognized_property: An unexpected property was found/,
+ }]);
+ });
+
+ yield extension.startup();
+
+ window.open(`${BASE}/file_sample.html`);
+
+ yield Promise.all([extension.awaitFinish("content-script-loaded")]);
+ info("test page loaded");
+
+ yield extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html
new file mode 100644
index 0000000000..c1aaae0359
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_downloads_saveAs.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test downloads.download() saveAs option</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_downloads_saveAs() {
+ function background() {
+ const url = URL.createObjectURL(new Blob(["file content"]));
+ browser.test.onMessage.addListener(async () => {
+ try {
+ let id = await browser.downloads.download({url, saveAs: true});
+ browser.downloads.onChanged.addListener(delta => {
+ if (delta.state.current === "complete") {
+ browser.test.sendMessage("done", {ok: true, id});
+ }
+ });
+ } catch ({message}) {
+ browser.test.sendMessage("done", {ok: false, message});
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ const {MockFilePicker} = SpecialPowers;
+ const manifest = {background, manifest: {permissions: ["downloads"]}};
+ const extension = ExtensionTestUtils.loadExtension(manifest);
+
+ MockFilePicker.init(window);
+ MockFilePicker.useAnyFile();
+ const [file] = MockFilePicker.returnFiles;
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("download");
+ let result = yield extension.awaitMessage("done");
+
+ ok(result.ok, "downloads.download() works with saveAs");
+ is(file.fileSize, 12, "downloaded file is the correct size");
+ file.remove(false);
+
+ // Test the user canceling the save dialog.
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+ extension.sendMessage("download");
+ result = yield extension.awaitMessage("done");
+
+ ok(!result.ok, "download rejected if the user cancels the dialog");
+ is(result.message, "Download canceled by the user", "with the correct message");
+ ok(!file.exists(), "file was not downloaded");
+
+ yield extension.unload();
+ MockFilePicker.cleanup();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
new file mode 100644
index 0000000000..ecea8237ef
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebExtension EventPage Warning</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function createEventPageExtension(eventPage) {
+ function eventPageScript() {
+ browser.test.log("running event page as background script");
+ browser.test.sendMessage("running", 1);
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ "background": eventPage,
+ },
+ files: {
+ "event-page-script.js": eventPageScript,
+ "event-page.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="event-page-script.js"><\/script>
+ </head></html>`,
+ },
+ });
+}
+
+add_task(function* test_eventpages() {
+ // Used in other tests to prevent the monitorConsole to grip.
+ SimpleTest.waitForExplicitFinish();
+
+ let testCases = [
+ {
+ message: "testing event page running as a background page",
+ eventPage: {
+ "page": "event-page.html",
+ "persistent": false,
+ },
+ },
+ {
+ message: "testing event page scripts running as a background page",
+ eventPage: {
+ "scripts": ["event-page-script.js"],
+ "persistent": false,
+ },
+ },
+ ];
+
+ for (let {message, eventPage} of testCases) {
+ info(message);
+
+ // Wait for the expected logged warnings from the manifest validation.
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{message: /Event pages are not currently supported./}]);
+ });
+
+ let extension = createEventPageExtension(eventPage);
+
+ info("load complete");
+ let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("test complete");
+ yield extension.unload();
+ info("extension unloaded successfully");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+
+ waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{
+ message: /Reading manifest: Error processing background.nonExistentProp: An unexpected property was found/,
+ }]);
+ });
+
+ info("testing additional unrecognized properties on background page");
+
+ extension = createEventPageExtension({
+ "scripts": ["event-page-script.js"],
+ "nonExistentProp": true,
+ });
+
+ info("load complete");
+ [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("test complete");
+ yield extension.unload();
+ info("extension unloaded successfully");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+ }
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html
new file mode 100644
index 0000000000..a74c551f08
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_hybrid_addons.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for hybrid addons: SDK or bootstrap.js + embedded WebExtension</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * This test contains additional tests that ensure that an SDK hybrid addon
+ * which is using the new module loader can embed a webextension correctly:
+ *
+ * while the other tests related to the "Embedded WebExtension" are focused
+ * on unit testing a specific component, these tests are testing that a complete
+ * hybrid SDK addon works as expected.
+ *
+ * NOTE: this tests are also the only ones which tests an SDK hybrid addon that
+ * uses the new module loader (the one actually used in production by real world
+ * addons these days), while the Addon SDK "embedded-webextension" test addon
+ * uses the old deprecated module loader (as all the other Addon SDK test addons).
+ */
+
+function generateClassicExtensionFiles({id, files}) {
+ // The addon install.rdf file, as it would be generated by jpm from the addon
+ // package.json metadata.
+ files["install.rdf"] = `<?xml version="1.0" encoding="utf-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>${id}</em:id>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+ <em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
+ <em:unpack>false</em:unpack>
+ <em:version>0.1.0</em:version>
+ <em:name>Fake Hybrid Addon</em:name>
+ <em:description>A fake hybrid addon</em:description>
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>51.0a1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Fennec -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
+ <em:minVersion>51.0a1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </RDF>`;
+
+ // The addon package.json file.
+ files["package.json"] = `{
+ "id": "${id}",
+ "name": "hybrid-addon",
+ "version": "0.1.0",
+ "description": "A fake hybrid addon",
+ "main": "index.js",
+ "engines": {
+ "firefox": ">= 51.0a1",
+ "fennec": ">= 51.0a1"
+ },
+ "license": "MPL-2.0",
+ "hasEmbeddedWebExtension": true
+ }`;
+
+ // The bootstrap file that jpm bundle in any SDK addon built with it.
+ files["bootstrap.js"] = `
+ const { utils: Cu } = Components;
+ const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", "");
+ const COMMONJS_URI = "resource://gre/modules/commonjs";
+ const { require } = Cu.import(COMMONJS_URI + "/toolkit/require.js", {});
+ const { Bootstrap } = require(COMMONJS_URI + "/sdk/addon/bootstrap.js");
+ var { startup, shutdown, install, uninstall } = new Bootstrap(rootURI);
+ `;
+
+ return files;
+}
+
+add_task(function* test_sdk_hybrid_addon_with_jpm_module_loader() {
+ function backgroundScript() {
+ browser.runtime.sendMessage("background message", (reply) => {
+ browser.test.assertEq("sdk received message: background message", reply,
+ "Got the expected reply from the SDK context");
+ browser.test.notifyPass("sdk.webext-api.onmessage");
+ });
+ }
+
+ async function sdkMainScript() {
+ /* globals require */
+ const webext = require("sdk/webextension");
+ let {browser} = await webext.startup();
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ sendReply(`sdk received message: ${msg}`);
+ });
+ }
+
+ let id = "fake@sdk.hybrid.addon";
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ files: generateClassicExtensionFiles({
+ id,
+ files: {
+ "index.js": sdkMainScript,
+ "webextension/manifest.json": {
+ name: "embedded webextension name",
+ manifest_version: 2,
+ version: "0.1.0",
+ background: {
+ scripts: ["bg.js"],
+ },
+ },
+ "webextension/bg.js": backgroundScript,
+ },
+ }),
+ }, id);
+
+ extension.startup();
+
+ yield extension.awaitFinish("sdk.webext-api.onmessage");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html
new file mode 100644
index 0000000000..3c3063e67b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_idle.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(Ci.nsIIdleService);
+
+add_task(function* testWithRealIdleService() {
+ function background() {
+ browser.test.onMessage.addListener((msg, ...args) => {
+ let detectionInterval = args[0];
+ if (msg == "addListener") {
+ browser.idle.queryState(detectionInterval).then(status => {
+ browser.test.assertEq("active", status, "Idle status is active");
+ });
+ browser.idle.setDetectionInterval(detectionInterval);
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ } else if (msg == "checkState") {
+ browser.idle.queryState(detectionInterval).then(status => {
+ browser.test.assertEq("idle", status, "Idle status is idle");
+ browser.test.notifyPass("idle");
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ let idleTime = idleService.idleTime;
+ let detectionInterval = Math.max(Math.ceil(idleTime / 1000) + 2, 15);
+ info(`idleTime: ${idleTime}, detectionInterval: ${detectionInterval}`);
+ extension.sendMessage("addListener", detectionInterval);
+ info("Listener added");
+ yield extension.awaitMessage("listenerFired");
+ info("Listener fired");
+ extension.sendMessage("checkState", detectionInterval);
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
new file mode 100644
index 0000000000..e3098e6b1e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+const {GlobalManager} = Cu.import("resource://gre/modules/Extension.jsm");
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* testShutdownCleanup() {
+ is(GlobalManager.initialized, false,
+ "GlobalManager start as not initialized");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ browser.test.notifyPass("background page loaded");
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("background page loaded");
+
+ is(GlobalManager.initialized, true,
+ "GlobalManager has been initialized once an extension is started");
+
+ yield extension.unload();
+
+ is(GlobalManager.initialized, false,
+ "GlobalManager has been uninitialized once all the webextensions have been stopped");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
new file mode 100644
index 0000000000..010769500a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// Test that storage used by a webextension (through localStorage,
+// indexedDB, and browser.storage.local) gets cleaned up when the
+// extension is uninstalled.
+add_task(function* test_uninstall() {
+ function writeData() {
+ localStorage.setItem("hello", "world");
+
+ let idbPromise = new Promise((resolve, reject) => {
+ let req = indexedDB.open("test");
+ req.onerror = e => {
+ reject(new Error(`indexedDB open failed with ${e.errorCode}`));
+ };
+
+ req.onupgradeneeded = e => {
+ let db = e.target.result;
+ db.createObjectStore("store", {keyPath: "name"});
+ };
+
+ req.onsuccess = e => {
+ let db = e.target.result;
+ let transaction = db.transaction("store", "readwrite");
+ let addreq = transaction.objectStore("store")
+ .add({name: "hello", value: "world"});
+ addreq.onerror = addreqError => {
+ reject(new Error(`add to indexedDB failed with ${addreqError.errorCode}`));
+ };
+ addreq.onsuccess = () => {
+ resolve();
+ };
+ };
+ });
+
+ let browserStoragePromise = browser.storage.local.set({hello: "world"});
+
+ Promise.all([idbPromise, browserStoragePromise]).then(() => {
+ browser.test.sendMessage("finished");
+ });
+ }
+
+ function readData() {
+ let matchLocalStorage = (localStorage.getItem("hello") == "world");
+
+ let idbPromise = new Promise((resolve, reject) => {
+ let req = indexedDB.open("test");
+ req.onerror = e => {
+ reject(new Error(`indexedDB open failed with ${e.errorCode}`));
+ };
+
+ req.onupgradeneeded = e => {
+ // no database, data is not present
+ resolve(false);
+ };
+
+ req.onsuccess = e => {
+ let db = e.target.result;
+ let transaction = db.transaction("store", "readwrite");
+ let addreq = transaction.objectStore("store").get("hello");
+ addreq.onerror = addreqError => {
+ reject(new Error(`read from indexedDB failed with ${addreqError.errorCode}`));
+ };
+ addreq.onsuccess = () => {
+ let match = (addreq.result.value == "world");
+ resolve(match);
+ };
+ };
+ });
+
+ let browserStoragePromise = browser.storage.local.get("hello").then(result => {
+ return (Object.keys(result).length == 1 && result.hello == "world");
+ });
+
+ Promise.all([idbPromise, browserStoragePromise])
+ .then(([matchIDB, matchBrowserStorage]) => {
+ let result = {matchLocalStorage, matchIDB, matchBrowserStorage};
+ browser.test.sendMessage("results", result);
+ });
+ }
+
+ const ID = "storage.cleanup@tests.mozilla.org";
+
+ // Use a test-only pref to leave the addonid->uuid mapping around after
+ // uninstall so that we can re-attach to the same storage. Also set
+ // the pref to prevent cleaning up storage on uninstall so we can test
+ // that the "keep uuid" logic works correctly. Do the storage flag in
+ // a separate prefEnv so we can pop it below, leaving the uuid flag set.
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepUuidOnUninstall", true]],
+ });
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.keepStorageOnUninstall", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: writeData,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield extension.unload();
+
+ // Check that we can still see data we wrote to storage but clear the
+ // "leave storage" flag so our storaged gets cleared on uninstall.
+ // This effectively tests the keepUuidOnUninstall logic, which ensures
+ // that when we read storage again and check that it is cleared, that
+ // it is actually a meaningful test!
+ yield SpecialPowers.popPrefEnv();
+ extension = ExtensionTestUtils.loadExtension({
+ background: readData,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let results = yield extension.awaitMessage("results");
+ is(results.matchLocalStorage, true, "localStorage data is still present");
+ is(results.matchIDB, true, "indexedDB data is still present");
+ is(results.matchBrowserStorage, true, "browser.storage.local data is still present");
+
+ yield extension.unload();
+
+ // Read again. This time, our data should be gone.
+ extension = ExtensionTestUtils.loadExtension({
+ background: readData,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["storage"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ results = yield extension.awaitMessage("results");
+ is(results.matchLocalStorage, false, "localStorage data was cleared");
+ is(results.matchIDB, false, "indexedDB data was cleared");
+ is(results.matchBrowserStorage, false, "browser.storage.local data was cleared");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
new file mode 100644
index 0000000000..573c088065
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * This test is asserting that moz-extension: URLs are recognized as trustworthy local origins
+ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
+ "@mozilla.org/contentsecuritymanager;1",
+ "nsIContentSecurityManager");
+
+add_task(function* () {
+ function background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("/test.html"));
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "test.html": `<html><head></head><body></body></html>`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let url = yield extension.awaitMessage("ready");
+
+ let uri = NetUtil.newURI(url);
+ let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
+ is(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal), true);
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
new file mode 100644
index 0000000000..768eb31fd2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* webnav_unresolved_uri_on_expected_URI_scheme() {
+ function background() {
+ let checkURLs;
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ if (checkURLs.length > 0) {
+ let expectedURL = checkURLs.shift();
+ browser.test.assertEq(expectedURL, msg.url, "Got the expected URL");
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("next");
+ }
+ });
+
+ browser.test.onMessage.addListener((name, urls) => {
+ if (name == "checkURLs") {
+ checkURLs = urls;
+ }
+ });
+
+ browser.test.sendMessage("ready", browser.runtime.getURL("/tab.html"));
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ files: {
+ "tab.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ </html>
+ `,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let checkURLs = [
+ "resource://gre/modules/Services.jsm",
+ "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js",
+ "about:mozilla",
+ ];
+
+ let tabURL = yield extension.awaitMessage("ready");
+ checkURLs.push(tabURL);
+
+ extension.sendMessage("checkURLs", checkURLs);
+
+ for (let url of checkURLs) {
+ window.open(url);
+ yield extension.awaitMessage("next");
+ }
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html
new file mode 100644
index 0000000000..a13c4d4754
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_background_events.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import(SimpleTest.getTestFileURL("webrequest_test.jsm"));
+let {testFetch, testXHR} = webrequest_test;
+
+// Here we test that any requests originating from a system principal are not
+// accessible through WebRequest. text_ext_webrequest_background_events tests
+// non-system principal requests.
+
+let testExtension = {
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ let eventNames = [
+ "onBeforeRequest",
+ "onBeforeSendHeaders",
+ "onSendHeaders",
+ "onHeadersReceived",
+ "onResponseStarted",
+ "onCompleted",
+ ];
+
+ function listener(name, details) {
+ // If we get anything, we failed. Removing the system principal check
+ // in ext-webrequest triggers this failure.
+ browser.test.fail(`recieved ${name}`);
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+ },
+};
+
+add_task(function* test_webRequest_chromeworker_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ yield extension.startup();
+ yield new Promise(resolve => {
+ let worker = new ChromeWorker("webrequest_chromeworker.js");
+ worker.onmessage = event => {
+ ok("chrome worker fetch finished");
+ resolve();
+ };
+ worker.postMessage("go");
+ });
+ yield extension.unload();
+});
+
+add_task(function* test_webRequest_chromepage_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ yield extension.startup();
+ yield new Promise(resolve => {
+ fetch("https://example.com/example.txt").then(() => {
+ ok("test page loaded");
+ resolve();
+ });
+ });
+ yield extension.unload();
+});
+
+add_task(function* test_webRequest_jsm_events() {
+ let extension = ExtensionTestUtils.loadExtension(testExtension);
+ yield extension.startup();
+ yield testFetch("https://example.com/example.txt").then(() => {
+ ok("fetch page loaded");
+ });
+ yield testXHR("https://example.com/example.txt").then(() => {
+ ok("xhr page loaded");
+ });
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
new file mode 100644
index 0000000000..29a148063e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_native_messaging_paths.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* global OS */
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+// Test that the default paths searched for native host manifests
+// are the ones we expect.
+add_task(function* test_default_paths() {
+ let expectUser, expectGlobal;
+ switch (AppConstants.platform) {
+ case "macosx": {
+ expectUser = OS.Path.join(OS.Constants.Path.homeDir,
+ "Library/Application Support/Mozilla/NativeMessagingHosts");
+ expectGlobal = "/Library/Application Support/Mozilla/NativeMessagingHosts";
+
+ break;
+ }
+
+ case "linux": {
+ expectUser = OS.Path.join(OS.Constants.Path.homeDir, ".mozilla/native-messaging-hosts");
+
+ const libdir = AppConstants.HAVE_USR_LIB64_DIR ? "lib64" : "lib";
+ expectGlobal = OS.Path.join("/usr", libdir, "mozilla/native-messaging-hosts");
+ break;
+ }
+
+ default:
+ // Fixed filesystem paths are only defined for MacOS and Linux,
+ // there's nothing to test on other platforms.
+ ok(false, `This test does not apply on ${AppConstants.platform}`);
+ break;
+ }
+
+ let userDir = Services.dirsvc.get("XREUserNativeMessaging", Ci.nsIFile).path;
+ is(userDir, expectUser, "user-specific native messaging directory is correct");
+
+ let globalDir = Services.dirsvc.get("XRESysNativeMessaging", Ci.nsIFile).path;
+ is(globalDir, expectGlobal, "system-wide native messaing directory is correct");
+});
+
+</script>
+
+</body>
+</html>
+
diff --git a/toolkit/components/extensions/test/mochitest/test_clipboard.html b/toolkit/components/extensions/test/mochitest/test_clipboard.html
new file mode 100644
index 0000000000..900ee5f108
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_clipboard.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>clipboard permission test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+function doCopy(txt) {
+ let field = document.createElement("textarea");
+ document.body.appendChild(field);
+ field.value = txt;
+ field.select();
+ return document.execCommand("copy");
+}
+
+add_task(function* no_permission_deny_copy() {
+ function backgroundScript() {
+ browser.test.assertEq(false, doCopy("whatever"),
+ "copy should be denied without permission");
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ background: `${doCopy};(${backgroundScript})();`,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitMessage("ready");
+
+ yield extension.unload();
+});
+
+/** Selecting text in a bg page is not possible, skip test until it's fixed.
+add_task(function* with_permission_allow_copy() {
+ function backgroundScript() {
+ browser.test.onMessage.addListener(txt => {
+ browser.test.assertEq(true, doCopy(txt),
+ "copy should be allowed with permission");
+ });
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ background: `${doCopy};(${backgroundScript})();`,
+ manifest: {
+ permissions: [
+ "clipboardWrite",
+ ],
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ const DUMMY_STR = "dummy string to copy";
+ yield new Promise(resolve => {
+ SimpleTest.waitForClipboard(DUMMY_STR, () => {
+ extension.sendMessage(DUMMY_STR);
+ }, resolve, resolve);
+ });
+
+ yield extension.unload();
+}); */
+
+add_task(function* content_script_no_permission_deny_copy() {
+ function contentScript() {
+ browser.test.assertEq(false, doCopy("whatever"),
+ "copy should be denied without permission");
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": `${doCopy};(${contentScript})();`,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("ready");
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* content_script_with_permission_allow_copy() {
+ function contentScript() {
+ browser.test.onMessage.addListener(txt => {
+ browser.test.assertEq(true, doCopy(txt),
+ "copy should be allowed with permission");
+ });
+ browser.test.sendMessage("ready");
+ }
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ permissions: [
+ "clipboardWrite",
+ ],
+ },
+ files: {
+ "contentscript.js": `${doCopy};(${contentScript})();`,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("ready");
+
+ const DUMMY_STR = "dummy string to copy in content script";
+ yield new Promise(resolve => {
+ SimpleTest.waitForClipboard(DUMMY_STR, () => {
+ extension.sendMessage(DUMMY_STR);
+ }, resolve, resolve);
+ });
+
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
new file mode 100644
index 0000000000..0f617c37e6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -0,0 +1,158 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Tests whether not too many APIs are visible by default.
+// This file is used by test_ext_all_apis.html in browser/ and mobile/android/,
+// which may modify the following variables to add or remove expected APIs.
+/* globals expectedContentApisTargetSpecific */
+/* globals expectedBackgroundApisTargetSpecific */
+
+// Generates a list of expectations.
+function generateExpectations(list) {
+ return list.reduce((allApis, path) => {
+ return allApis.concat(`browser.${path}`, `chrome.${path}`);
+ }, []).sort();
+}
+
+let expectedCommonApis = [
+ "extension.getURL",
+ "extension.inIncognitoContext",
+ "extension.lastError",
+ "i18n.detectLanguage",
+ "i18n.getAcceptLanguages",
+ "i18n.getMessage",
+ "i18n.getUILanguage",
+ "runtime.OnInstalledReason",
+ "runtime.OnRestartRequiredReason",
+ "runtime.PlatformArch",
+ "runtime.PlatformOs",
+ "runtime.RequestUpdateCheckStatus",
+ "runtime.getManifest",
+ "runtime.connect",
+ "runtime.getURL",
+ "runtime.id",
+ "runtime.lastError",
+ "runtime.onConnect",
+ "runtime.onMessage",
+ "runtime.sendMessage",
+ // If you want to add a new powerful test API, please see bug 1287233.
+ "test.assertEq",
+ "test.assertFalse",
+ "test.assertRejects",
+ "test.assertThrows",
+ "test.assertTrue",
+ "test.fail",
+ "test.log",
+ "test.notifyFail",
+ "test.notifyPass",
+ "test.onMessage",
+ "test.sendMessage",
+ "test.succeed",
+];
+
+let expectedContentApis = [
+ ...expectedCommonApis,
+ ...expectedContentApisTargetSpecific,
+];
+
+let expectedBackgroundApis = [
+ ...expectedCommonApis,
+ ...expectedBackgroundApisTargetSpecific,
+ "extension.ViewType",
+ "extension.getBackgroundPage",
+ "extension.getViews",
+ "extension.isAllowedFileSchemeAccess",
+ "extension.isAllowedIncognitoAccess",
+ // Note: extensionTypes is not visible in Chrome.
+ "extensionTypes.ImageFormat",
+ "extensionTypes.RunAt",
+ "management.ExtensionDisabledReason",
+ "management.ExtensionInstallType",
+ "management.ExtensionType",
+ "management.getSelf",
+ "management.uninstallSelf",
+ "runtime.getBackgroundPage",
+ "runtime.getBrowserInfo",
+ "runtime.getPlatformInfo",
+ "runtime.onInstalled",
+ "runtime.onStartup",
+ "runtime.onUpdateAvailable",
+ "runtime.openOptionsPage",
+ "runtime.reload",
+ "runtime.setUninstallURL",
+];
+
+function sendAllApis() {
+ function isEvent(key, val) {
+ if (!/^on[A-Z]/.test(key)) {
+ return false;
+ }
+ let eventKeys = [];
+ for (let prop in val) {
+ eventKeys.push(prop);
+ }
+ eventKeys = eventKeys.sort().join();
+ return eventKeys === "addListener,hasListener,removeListener";
+ }
+ function mayRecurse(key, val) {
+ if (Object.keys(val).filter(k => !/^[A-Z\-0-9_]+$/.test(k)).length === 0) {
+ // Don't recurse on constants and empty objects.
+ return false;
+ }
+ return !isEvent(key, val);
+ }
+
+ let results = [];
+ function diveDeeper(path, obj) {
+ for (let key in obj) {
+ let val = obj[key];
+ if (typeof val == "object" && val !== null && mayRecurse(key, val)) {
+ diveDeeper(`${path}.${key}`, val);
+ } else if (val !== undefined) {
+ results.push(`${path}.${key}`);
+ }
+ }
+ }
+ diveDeeper("browser", browser);
+ diveDeeper("chrome", chrome);
+ browser.test.sendMessage("allApis", results.sort());
+}
+
+add_task(function* test_enumerate_content_script_apis() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["contentscript.js"],
+ run_at: "document_start",
+ }],
+ },
+ files: {
+ "contentscript.js": sendAllApis,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ let actualApis = yield extension.awaitMessage("allApis");
+ win.close();
+ let expectedApis = generateExpectations(expectedContentApis);
+ isDeeply(actualApis, expectedApis, "content script APIs");
+
+ yield extension.unload();
+});
+
+add_task(function* test_enumerate_background_script_apis() {
+ let extensionData = {
+ background: sendAllApis,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ let actualApis = yield extension.awaitMessage("allApis");
+ let expectedApis = generateExpectations(expectedBackgroundApis);
+ isDeeply(actualApis, expectedApis, "background script APIs");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html
new file mode 100644
index 0000000000..f43a59f816
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for privilege escalation into content pages</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* testBackgroundWindow() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+
+ browser.test.log("background script executed");
+ window.location = `${BASE}/file_privilege_escalation.html`;
+ },
+ });
+
+ let awaitConsole = new Promise(resolve => {
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_ext_test_api_injection.js"));
+
+ chromeScript.addMessageListener("console-message", resolve);
+ });
+
+ yield extension.startup();
+
+ let message = yield awaitConsole;
+
+ ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"),
+ "Document does not have `browser` APIs.");
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html
new file mode 100644
index 0000000000..bff7190cb2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for background page canvas rendering</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_background_canvas() {
+ function background() {
+ try {
+ let canvas = document.createElement("canvas");
+
+ let context = canvas.getContext("2d");
+
+ // This ensures that we have a working PresShell, and can successfully
+ // calculate font metrics.
+ context.font = "8pt fixed";
+
+ browser.test.notifyPass("background-canvas");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("background-canvas");
+ }
+ }
+
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+ yield extension.awaitFinish("background-canvas");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
new file mode 100644
index 0000000000..f4fcf3d341
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test _generated_background_page.html</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_url_of_generated_background_page() {
+ function backgroundScript() {
+ const EXPECTED_URL = browser.runtime.getURL("/_generated_background_page.html");
+ browser.test.assertEq(EXPECTED_URL, location.href);
+ browser.test.sendMessage("script done", EXPECTED_URL);
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ scripts: ["bg.js"],
+ },
+ web_accessible_resources: ["_generated_background_page.html"],
+ },
+ files: {
+ "bg.js": backgroundScript,
+ },
+ });
+
+ yield extension.startup();
+ const EXPECTED_URL = yield extension.awaitMessage("script done");
+
+ let win = window.open(EXPECTED_URL);
+ ok(win, "Should open new tab at URL: " + EXPECTED_URL);
+ yield extension.awaitMessage("script done");
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html
new file mode 100644
index 0000000000..bb6b2e9709
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for background script teardown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_background_reload_and_unload() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("reload-background", msg);
+ location.reload();
+ });
+ browser.test.sendMessage("background-url", location.href);
+ }
+
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_teardown_test.js"));
+ yield chromeScript.promiseOneMessage("chromescript-startup");
+
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ function* getContextEvents() {
+ chromeScript.sendAsyncMessage("get-context-events");
+ let contextEvents = yield chromeScript.promiseOneMessage("context-events");
+ return contextEvents.filter(event => event.extensionId == extension.id);
+ }
+ yield extension.startup();
+ let backgroundUrl = yield extension.awaitMessage("background-url");
+
+ let contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after loading an extension");
+ is(contextEvents[0].eventType, "load");
+ is(contextEvents[0].url, backgroundUrl,
+ "The ExtensionContext should be the background page");
+
+ extension.sendMessage("reload-background");
+ yield extension.awaitMessage("background-url");
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 2,
+ "ExtensionContext state changes after reloading the background page");
+ is(contextEvents[0].eventType, "unload",
+ "Unload ExtensionContext of background page");
+ is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background");
+ is(contextEvents[1].eventType, "load",
+ "Create new ExtensionContext for background page");
+ is(contextEvents[1].url, backgroundUrl, "ExtensionContext URL = background");
+ yield extension.unload();
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after unloading the extension");
+ is(contextEvents[0].eventType, "unload",
+ "Unload ExtensionContext for background page after extension unloads");
+ is(contextEvents[0].url, backgroundUrl, "ExtensionContext URL = background");
+
+ chromeScript.sendAsyncMessage("cleanup");
+ chromeScript.destroy();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html b/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html
new file mode 100644
index 0000000000..a36f295631
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_content_security_policy.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension CSP test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * Tests that content security policies for an add-on are actually applied to *
+ * documents that belong to it. This tests both the base policies and add-on
+ * specific policies, and ensures that the parsed policies applied to the
+ * document's principal match what was specified in the policy string.
+ *
+ * @param {object} [customCSP]
+ */
+function* testPolicy(customCSP = null) {
+ let baseURL;
+
+ let baseCSP = {
+ "object-src": ["blob:", "filesystem:", "https://*", "moz-extension:", "'self'"],
+ "script-src": ["'unsafe-eval'", "'unsafe-inline'", "blob:", "filesystem:", "https://*", "moz-extension:", "'self'"],
+ };
+
+ let addonCSP = {
+ "object-src": ["'self'"],
+ "script-src": ["'self'"],
+ };
+
+ let content_security_policy = null;
+
+ if (customCSP) {
+ for (let key of Object.keys(customCSP)) {
+ addonCSP[key] = customCSP[key].split(/\s+/);
+ }
+
+ content_security_policy = Object.keys(customCSP)
+ .map(key => `${key} ${customCSP[key]}`)
+ .join("; ");
+ }
+
+
+ function filterSelf(sources) {
+ return sources.map(src => src == "'self'" ? baseURL : src);
+ }
+
+ function checkSource(name, policy, expected) {
+ is(JSON.stringify(policy[name].sort()),
+ JSON.stringify(filterSelf(expected[name]).sort()),
+ `Expected value for ${name}`);
+ }
+
+ function checkCSP(csp, location) {
+ let policies = csp["csp-policies"];
+
+ info(`Base policy for ${location}`);
+
+ is(policies[0]["report-only"], false, "Policy is not report-only");
+ checkSource("object-src", policies[0], baseCSP);
+ checkSource("script-src", policies[0], baseCSP);
+
+ info(`Add-on policy for ${location}`);
+
+ is(policies[1]["report-only"], false, "Policy is not report-only");
+ checkSource("object-src", policies[1], addonCSP);
+ checkSource("script-src", policies[1], addonCSP);
+ }
+
+
+ function getCSP(window) {
+ let {cspJSON} = SpecialPowers.Cu.getObjectPrincipal(window);
+ return JSON.parse(cspJSON);
+ }
+
+ function background(getCSPFn) {
+ browser.test.sendMessage("base-url", browser.extension.getURL("").replace(/\/$/, ""));
+
+ browser.test.sendMessage("background-csp", getCSPFn(window));
+ }
+
+ function tabScript(getCSPFn) {
+ browser.test.sendMessage("tab-csp", getCSPFn(window));
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})(${getCSP})`,
+
+ files: {
+ "tab.html": `<html><head><meta charset="utf-8">
+ <script src="tab.js"></${"script"}></head></html>`,
+
+ "tab.js": `(${tabScript})(${getCSP})`,
+
+ "content.html": `<html><head><meta charset="utf-8"></head></html>`,
+ },
+
+ manifest: {
+ content_security_policy,
+
+ web_accessible_resources: ["content.html", "tab.html"],
+ },
+ });
+
+
+ info(`Testing CSP for policy: ${content_security_policy}`);
+
+ yield extension.startup();
+
+ baseURL = yield extension.awaitMessage("base-url");
+
+
+ let win1 = window.open(`${baseURL}/tab.html`);
+
+ let frame = document.createElement("iframe");
+ frame.src = `${baseURL}/content.html`;
+ document.body.appendChild(frame);
+
+ yield new Promise(resolve => {
+ frame.onload = resolve;
+ });
+
+
+ let backgroundCSP = yield extension.awaitMessage("background-csp");
+ checkCSP(backgroundCSP, "background page");
+
+ let tabCSP = yield extension.awaitMessage("tab-csp");
+ checkCSP(tabCSP, "tab page");
+
+ let contentCSP = getCSP(frame.contentWindow);
+ checkCSP(contentCSP, "content frame");
+
+
+ win1.close();
+ frame.remove();
+
+ yield extension.unload();
+}
+
+add_task(function* testCSP() {
+ yield testPolicy(null);
+
+ let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
+
+ yield testPolicy({
+ "object-src": "'self' https://*.example.com",
+ "script-src": `'self' https://*.example.com 'unsafe-eval' ${hash}`,
+ });
+
+ yield testPolicy({
+ "object-src": "'none'",
+ "script-src": `'self'`,
+ });
+});
+</script>
+</body>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
new file mode 100644
index 0000000000..39f1bfabd0
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript() {
+ function background() {
+ browser.runtime.onMessage.addListener(([msg, expectedStates, readyState], sender) => {
+ if (msg == "chrome-namespace-ok") {
+ browser.test.sendMessage(msg);
+ return;
+ }
+
+ browser.test.assertEq("script-run", msg, "message type is correct");
+ browser.test.assertTrue(expectedStates.includes(readyState),
+ `readyState "${readyState}" is one of [${expectedStates}]`);
+ browser.test.sendMessage("script-run-" + expectedStates[0]);
+ });
+ }
+
+ function contentScriptStart() {
+ browser.runtime.sendMessage(["script-run", ["loading"], document.readyState]);
+ }
+ function contentScriptEnd() {
+ browser.runtime.sendMessage(["script-run", ["interactive", "complete"], document.readyState]);
+ }
+ function contentScriptIdle() {
+ browser.runtime.sendMessage(["script-run", ["complete"], document.readyState]);
+ }
+
+ function contentScript() {
+ let manifest = browser.runtime.getManifest();
+ void manifest.applications.gecko.id;
+ chrome.runtime.sendMessage(["chrome-namespace-ok"]);
+ }
+
+ let extensionData = {
+ manifest: {
+ applications: {gecko: {id: "contentscript@tests.mozilla.org"}},
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script_start.js"],
+ "run_at": "document_start",
+ },
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script_end.js"],
+ "run_at": "document_end",
+ },
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script_idle.js"],
+ "run_at": "document_idle",
+ },
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script_start.js": contentScriptStart,
+ "content_script_end.js": contentScriptEnd,
+ "content_script_idle.js": contentScriptIdle,
+ "content_script.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let loadingCount = 0;
+ let interactiveCount = 0;
+ let completeCount = 0;
+ extension.onMessage("script-run-loading", () => { loadingCount++; });
+ extension.onMessage("script-run-interactive", () => { interactiveCount++; });
+
+ let completePromise = new Promise(resolve => {
+ extension.onMessage("script-run-complete", () => { completeCount++; resolve(); });
+ });
+
+ let chromeNamespacePromise = extension.awaitMessage("chrome-namespace-ok");
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), completePromise, chromeNamespacePromise]);
+ info("test page loaded");
+
+ win.close();
+
+ is(loadingCount, 1, "document_start script ran exactly once");
+ is(interactiveCount, 1, "document_end script ran exactly once");
+ is(completeCount, 1, "document_idle script ran exactly once");
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html
new file mode 100644
index 0000000000..3766678e71
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script match_about_blank option</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_about_blank() {
+ const manifest = {
+ content_scripts: [
+ {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html", "http://example.com/*"],
+ all_frames: true,
+ css: ["all.css"],
+ js: ["all.js"],
+ }, {
+ matches: ["http://mochi.test/*/file_with_about_blank.html"],
+ css: ["mochi_without.css"],
+ js: ["mochi_without.js"],
+ all_frames: true,
+ }, {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html"],
+ css: ["mochi_with.css"],
+ js: ["mochi_with.js"],
+ all_frames: true,
+ },
+ ],
+ };
+
+ const files = {
+ "all.js": function() {
+ browser.runtime.sendMessage("all");
+ },
+ "all.css": `
+ body { color: red; }
+ `,
+ "mochi_without.js": function() {
+ browser.runtime.sendMessage("mochi_without");
+ },
+ "mochi_without.css": `
+ body { background: yellow; }
+ `,
+ "mochi_with.js": function() {
+ browser.runtime.sendMessage("mochi_with");
+ },
+ "mochi_with.css": `
+ body { text-align: right; }
+ `,
+ };
+
+ function background() {
+ browser.runtime.onMessage.addListener((script, {url}) => {
+ const kind = url.startsWith("about:") ? url : "top";
+ browser.test.sendMessage("script", [script, kind, url]);
+ browser.test.sendMessage(`${script}:${kind}`);
+ });
+ }
+
+ const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html";
+ const extension = ExtensionTestUtils.loadExtension({manifest, files, background});
+ yield extension.startup();
+
+ let count = 0;
+ extension.onMessage("script", script => {
+ info(`script ran: ${script}`);
+ count++;
+ });
+
+ let win = window.open("http://example.com/" + PATH);
+ yield Promise.all([
+ extension.awaitMessage("all:top"),
+ extension.awaitMessage("all:about:blank"),
+ extension.awaitMessage("all:about:srcdoc"),
+ ]);
+ is(count, 3, "exactly 3 scripts ran");
+ win.close();
+
+ win = window.open("http://mochi.test:8888/" + PATH);
+ yield Promise.all([
+ extension.awaitMessage("all:top"),
+ extension.awaitMessage("all:about:blank"),
+ extension.awaitMessage("all:about:srcdoc"),
+ extension.awaitMessage("mochi_without:top"),
+ extension.awaitMessage("mochi_with:top"),
+ extension.awaitMessage("mochi_with:about:blank"),
+ extension.awaitMessage("mochi_with:about:srcdoc"),
+ ]);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.color, "rgb(255, 0, 0)", "top window text color is red");
+ is(style.backgroundColor, "rgb(255, 255, 0)", "top window background is yellow");
+ is(style.textAlign, "right", "top window text is right-aligned");
+
+ let a_b = win.document.getElementById("a_b");
+ style = a_b.contentWindow.getComputedStyle(a_b.contentDocument.body);
+ is(style.color, "rgb(255, 0, 0)", "about:blank iframe text color is red");
+ is(style.backgroundColor, "transparent", "about:blank iframe background is transparent");
+ is(style.textAlign, "right", "about:blank text is right-aligned");
+
+ is(count, 10, "exactly 7 more scripts ran");
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html
new file mode 100644
index 0000000000..abf3d349fb
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for privilege escalation into iframe with content script APIs</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it -->
+<textarea id="test-asset">
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="./content_script_iframe.js">
+ </script>
+ </head>
+ </html>
+</textarea>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_api_injection() {
+ function contentScript() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html"));
+ document.body.appendChild(iframe);
+ }
+
+ function contentScriptIframe() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ window.location = `${BASE}/file_privilege_escalation.html`;
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ "web_accessible_resources": [
+ "content_script_iframe.html",
+ ],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ "content_script_iframe.js": contentScriptIframe,
+ "content_script_iframe.html": document.querySelector("#test-asset").textContent,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let awaitConsole = new Promise(resolve => {
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_ext_test_api_injection.js"));
+
+ chromeScript.addMessageListener("console-message", resolve);
+ });
+
+ yield extension.startup();
+ info("extension loaded");
+
+ let win = window.open("file_sample.html");
+
+ let message = yield awaitConsole;
+
+ ok(message.message.includes("WebExt Privilege Escalation: typeof(browser) = undefined"),
+ "Document does not have `browser` APIs.");
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html
new file mode 100644
index 0000000000..d78f7ce02c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_async_loading.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script async loading</title>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+"use strict";
+
+add_task(function* test_async_loading() {
+ const adder = `(function add(a = 1) { this.count += a; })();\n`;
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ matches: ["https://example.org/"],
+ js: ["first.js", "second.js"],
+ }],
+ },
+ files: {
+ "first.js": `
+ this.count = 0;
+ ${adder.repeat(50000)}; // 2Mb
+ browser.test.assertEq(this.count, 50000, "A 50k line script");
+
+ this.order = (this.order || 0) + 1;
+ browser.test.sendMessage("first", this.order);
+ `,
+ "second.js": `
+ this.order = (this.order || 0) + 1;
+ browser.test.sendMessage("second", this.order);
+ `,
+ },
+ });
+
+ yield extension.startup();
+ const win = window.open("https://example.org/");
+
+ const [first, second] = yield Promise.all([
+ extension.awaitMessage("first"),
+ extension.awaitMessage("second"),
+ ]);
+
+ is(first, 1, "first.js finished execution first.");
+ is(second, 2, "second.js finished execution second.");
+
+ yield extension.unload();
+ win.close();
+});
+
+</script>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
new file mode 100644
index 0000000000..97b1645dd2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script contexts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_contentscript_context() {
+ function contentScript() {
+ browser.test.sendMessage("content-script-ready");
+
+ window.addEventListener("pagehide", () => {
+ browser.test.sendMessage("content-script-hide");
+ }, true);
+ window.addEventListener("pageshow", () => {
+ browser.test.sendMessage("content-script-show");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://example.com/"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("http://example.com/");
+ yield extension.awaitMessage("content-script-ready");
+ yield extension.awaitMessage("content-script-show");
+
+ // Get the content script context and check that it points to the correct window.
+
+ let {DocumentManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
+ let context = DocumentManager.getContentScriptContext(extension, win);
+ ok(context != null, "Got content script context");
+
+ is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct");
+
+ // Navigate so that the content page is hidden in the bfcache.
+
+ win.location = "http://example.org/";
+ yield extension.awaitMessage("content-script-hide");
+
+ is(context.contentWindow, null, "Context's contentWindow property is null");
+
+ // Navigate back so the content page is resurrected from the bfcache.
+
+ SpecialPowers.wrap(win).history.back();
+ yield extension.awaitMessage("content-script-show");
+
+ is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct");
+
+ win.close();
+
+ yield extension.awaitMessage("content-script-hide");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
new file mode 100644
index 0000000000..8aac3e213c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
@@ -0,0 +1,165 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<!-- WORKAROUND: this textarea hack is used to contain the html page source without escaping it -->
+<textarea id="test-asset">
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="content_script_iframe.js"></script>
+ </head>
+ </html>
+</textarea>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_create_iframe() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ let {name, availableAPIs, manifest, testGetManifest} = msg;
+ let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0;
+ let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0;
+
+ browser.test.assertFalse(hasExtTabsAPI, "the created iframe should not be able to use privileged APIs (tabs)");
+ browser.test.assertFalse(hasExtWindowsAPI, "the created iframe should not be able to use privileged APIs (windows)");
+
+ let {applications: {gecko: {id: expectedManifestGeckoId}}} = chrome.runtime.getManifest();
+ let {applications: {gecko: {id: actualManifestGeckoId}}} = manifest;
+
+ browser.test.assertEq(actualManifestGeckoId, expectedManifestGeckoId,
+ "the add-on manifest should be accessible from the created iframe"
+ );
+
+ let {applications: {gecko: {id: testGetManifestGeckoId}}} = testGetManifest;
+
+ browser.test.assertEq(testGetManifestGeckoId, expectedManifestGeckoId,
+ "GET_MANIFEST() returns manifest data before extension unload"
+ );
+
+ browser.test.sendMessage(name);
+ });
+ }
+
+ function contentScript() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", browser.runtime.getURL("content_script_iframe.html"));
+ document.body.appendChild(iframe);
+ }
+
+ function contentScriptIframe() {
+ window.GET_MANIFEST = browser.runtime.getManifest.bind(null);
+
+ window.testGetManifestException = () => {
+ try {
+ window.GET_MANIFEST();
+ } catch (exception) {
+ return String(exception);
+ }
+ };
+
+ let testGetManifest = window.GET_MANIFEST();
+
+ let manifest = browser.runtime.getManifest();
+ let availableAPIs = Object.keys(browser);
+
+ browser.runtime.sendMessage({
+ name: "content-script-iframe-loaded",
+ availableAPIs,
+ manifest,
+ testGetManifest,
+ });
+ }
+
+ const ID = "contentscript@tests.mozilla.org";
+ let extensionData = {
+ manifest: {
+ applications: {gecko: {id: ID}},
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ web_accessible_resources: [
+ "content_script_iframe.html",
+ ],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": contentScript,
+ "content_script_iframe.html": document.querySelector("#test-asset").textContent,
+ "content_script_iframe.js": contentScriptIframe,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let contentScriptIframeCreatedPromise = new Promise(resolve => {
+ extension.onMessage("content-script-iframe-loaded", () => { resolve(); });
+ });
+
+ yield extension.startup();
+ info("extension loaded");
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), contentScriptIframeCreatedPromise]);
+ info("content script privileged iframe loaded and executed");
+
+ info("testing APIs availability once the extension is unloaded...");
+
+ let iframeWindow = SpecialPowers.wrap(win)[0];
+
+ ok(iframeWindow, "content script enabled iframe found");
+ ok(/content_script_iframe\.html$/.test(iframeWindow.location), "the found iframe has the expected URL");
+
+ yield extension.unload();
+ info("extension unloaded");
+
+ info("test content script APIs not accessible from the frame once the extension is unloaded");
+
+ let ww = SpecialPowers.Cu.waiveXrays(iframeWindow);
+ let isDeadWrapper = SpecialPowers.Cu.isDeadWrapper(ww.browser);
+ ok(!isDeadWrapper, "the API object should not be a dead object");
+
+ let manifest;
+ let manifestException;
+
+ try {
+ manifest = ww.browser.runtime.getManifest();
+ } catch (e) {
+ manifestException = e;
+ }
+
+ ok(!manifest, "manifest should be undefined");
+
+ is(String(manifestException), "TypeError: ww.browser.runtime is undefined",
+ "expected exception received");
+
+ let getManifestException = ww.testGetManifestException();
+
+ is(getManifestException, "TypeError: can't access dead object",
+ "expected exception received");
+
+ win.close();
+
+ info("done");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html
new file mode 100644
index 0000000000..5630a1d680
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_css.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_content_script_css() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "css": ["content.css"],
+ }],
+ },
+
+ files: {
+ "content.css": "body { max-width: 42px; }",
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.maxWidth, "42px", "Stylesheet correctly applied");
+
+ yield extension.unload();
+
+ style = win.getComputedStyle(win.document.body);
+ is(style.maxWidth, "none", "Stylesheet correctly removed");
+
+ win.close();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
new file mode 100644
index 0000000000..137a3cda48
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Sandbox metadata on WebExtensions ContentScripts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_devtools_sandbox_metadata() {
+ function contentScript() {
+ browser.runtime.sendMessage("contentScript.executed");
+ }
+
+ function background() {
+ browser.runtime.onMessage.addListener((msg) => {
+ if (msg == "contentScript.executed") {
+ browser.test.notifyPass("contentScript.executed");
+ }
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+ },
+
+ background,
+ files: {
+ "content_script.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ let innerWindowID = SpecialPowers.wrap(win)
+ .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+
+ yield extension.awaitFinish("contentScript.executed");
+
+ const {ExtensionContent} = SpecialPowers.Cu.import(
+ "resource://gre/modules/ExtensionContent.jsm", {}
+ );
+
+ let res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+ is(res.length, 1, "Got the expected array of globals");
+ let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {};
+
+ is(metadata.addonId, extension.id, "Got the expected addonId");
+ is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id");
+
+ yield extension.unload();
+ info("extension unloaded");
+
+ res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+ is(res.length, 0, "No content scripts globals found once the extension is unloaded");
+
+ win.close();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
new file mode 100644
index 0000000000..f3414901d6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_contentscript_exportHelpers() {
+ function contentScript() {
+ browser.test.assertTrue(typeof cloneInto === "function");
+ browser.test.assertTrue(typeof createObjectIn === "function");
+ browser.test.assertTrue(typeof exportFunction === "function");
+
+ /* globals exportFunction, precisePi, reportPi */
+ let value = 3.14;
+ exportFunction(() => value, window, {defineAs: "precisePi"});
+
+ browser.test.assertEq("undefined", typeof precisePi,
+ "exportFunction should export to the page's scope only");
+
+ browser.test.assertEq("undefined", typeof window.precisePi,
+ "exportFunction should export to the page's scope only");
+
+ let results = [];
+ exportFunction(pi => results.push(pi), window, {defineAs: "reportPi"});
+
+ let s = document.createElement("script");
+ s.textContent = `(${function() {
+ let result1 = "unknown 1";
+ let result2 = "unknown 2";
+ try {
+ result1 = precisePi();
+ } catch (e) {
+ result1 = "err:" + e;
+ }
+ try {
+ result2 = window.precisePi();
+ } catch (e) {
+ result2 = "err:" + e;
+ }
+ reportPi(result1);
+ reportPi(result2);
+ }})();`;
+
+ document.documentElement.appendChild(s);
+ // Inline script ought to run synchronously.
+
+ browser.test.assertEq(3.14, results[0],
+ "exportFunction on window should define a global function");
+ browser.test.assertEq(3.14, results[1],
+ "exportFunction on window should export a property to window.");
+
+ browser.test.assertEq(2, results.length,
+ "Expecting the number of results to match the number of method calls");
+
+ browser.test.notifyPass("export helper test completed");
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ run_at: "document_start",
+ }],
+ },
+
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield extension.awaitFinish("export helper test completed");
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html
new file mode 100644
index 0000000000..a2f38dce65
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_incognito.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script private browsing ID</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_incognito() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ },
+ ],
+ },
+
+ background() {
+ let windowId;
+
+ browser.test.onMessage.addListener(([msg, url]) => {
+ if (msg === "open-window") {
+ browser.windows.create({url, incognito: true}).then(window => {
+ windowId = window.id;
+ });
+ } else if (msg === "close-window") {
+ browser.windows.remove(windowId).then(() => {
+ browser.test.sendMessage("done");
+ });
+ }
+ });
+ },
+
+ files: {
+ "content_script.js": async () => {
+ const COOKIE = "foo=florgheralzps";
+ document.cookie = COOKIE;
+
+ let url = new URL("return_headers.sjs", location.href);
+
+ let responses = [
+ new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => resolve(JSON.parse(xhr.responseText));
+ xhr.send();
+ }),
+
+ fetch(url, {credentials: "include"}).then(body => body.json()),
+ ];
+
+ try {
+ for (let response of await Promise.all(responses)) {
+ browser.test.assertEq(COOKIE, response.cookie, "Got expected cookie header");
+ }
+ browser.test.notifyPass("cookies");
+ } catch (e) {
+ browser.test.fail(`Error: ${e}`);
+ browser.test.notifyFail("cookies");
+ }
+ },
+ },
+ });
+
+ yield extension.startup();
+
+ extension.sendMessage(["open-window", SimpleTest.getTestFileURL("file_sample.html")]);
+
+ yield extension.awaitFinish("cookies");
+
+ extension.sendMessage(["close-window"]);
+ yield extension.awaitMessage("done");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
+
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html
new file mode 100644
index 0000000000..eaf8150926
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_permission.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript() {
+ function background() {
+ browser.test.onMessage.addListener(url => {
+ browser.tabs.create({url}).then(tab => {
+ return browser.tabs.executeScript(tab.id, {code: "true;"})
+ .then(() => {
+ browser.test.sendMessage("executed", true);
+ browser.tabs.remove([tab.id]);
+ }, err => {
+ browser.test.sendMessage("executed", false);
+ browser.tabs.remove([tab.id]);
+ });
+ });
+ });
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: ["<all_urls>"],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ extension.sendMessage("https://example.com");
+ let result = yield extension.awaitMessage("executed");
+ is(result, true, "Content script can be run in a page without mozAddonManager");
+
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+
+ extension.sendMessage("https://example.com");
+ result = yield extension.awaitMessage("executed");
+ is(result, false, "Content script cannot be run in a page with mozAddonManager");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html
new file mode 100644
index 0000000000..33a8c4ccc6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script teardown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_contentscript_reload_and_unload() {
+ function contentScript() {
+ browser.test.sendMessage("contentscript-run");
+ }
+ function background() {
+ let removedTabs = 0;
+ browser.tabs.onRemoved.addListener(() => {
+ browser.test.assertEq(1, ++removedTabs,
+ "Expected only one tab to be removed during the test");
+ browser.test.sendMessage("tab-closed");
+ });
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["contentscript.js"],
+ }],
+ },
+
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_teardown_test.js"));
+ yield chromeScript.promiseOneMessage("chromescript-startup");
+ function* getContextEvents() {
+ chromeScript.sendAsyncMessage("get-context-events");
+ let contextEvents = yield chromeScript.promiseOneMessage("context-events");
+ return contextEvents.filter(event => event.extensionId == extension.id);
+ }
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("contentscript-run");
+ let tabUrl = win.location.href;
+
+ let contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after loading a content script");
+ is(contextEvents[0].eventType, "load",
+ "Create ExtensionContext for content script");
+ is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page");
+
+ let promiseReload = extension.awaitMessage("contentscript-run");
+ win.location.reload();
+ yield promiseReload;
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 2,
+ "ExtensionContext state changes after reloading a content script");
+ is(contextEvents[0].eventType, "unload", "Unload old ExtensionContext");
+ is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page");
+ is(contextEvents[1].eventType, "load",
+ "Create new ExtensionContext for content script");
+ is(contextEvents[1].url, tabUrl, "ExtensionContext URL = page");
+
+ let tabClosePromise = extension.awaitMessage("tab-closed");
+ win.close();
+ yield tabClosePromise;
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1,
+ "ExtensionContext state change after unloading a content script");
+ is(contextEvents[0].eventType, "unload",
+ "Unload ExtensionContext after closing the tab with the content script");
+ is(contextEvents[0].url, tabUrl, "ExtensionContext URL = page");
+
+ chromeScript.sendAsyncMessage("cleanup");
+ chromeScript.destroy();
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
new file mode 100644
index 0000000000..d414a4e46e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
@@ -0,0 +1,234 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_cookies() {
+ async function background() {
+ function assertExpected(expected, cookie) {
+ for (let key of Object.keys(cookie)) {
+ browser.test.assertTrue(key in expected, `found property ${key}`);
+ browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`);
+ }
+ browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found");
+ }
+
+ const TEST_URL = "http://example.org/";
+ const TEST_SECURE_URL = "https://example.org/";
+ const THE_FUTURE = Date.now() + 5 * 60;
+ const TEST_PATH = "set_path";
+ const TEST_URL_WITH_PATH = TEST_URL + TEST_PATH;
+ const TEST_COOKIE_PATH = `/${TEST_PATH}`;
+ const STORE_ID = "firefox-default";
+ const PRIVATE_STORE_ID = "firefox-private";
+
+ let expected = {
+ name: "name1",
+ value: "value1",
+ domain: "example.org",
+ hostOnly: true,
+ path: "/",
+ secure: false,
+ httpOnly: false,
+ session: false,
+ expirationDate: THE_FUTURE,
+ storeId: STORE_ID,
+ };
+
+ let cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE});
+ assertExpected(expected, cookie);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ assertExpected(expected, cookie);
+
+ let cookies = await browser.cookies.getAll({name: "name1"});
+ browser.test.assertEq(cookies.length, 1, "one cookie found for matching name");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({domain: "example.org"});
+ browser.test.assertEq(cookies.length, 1, "one cookie found for matching domain");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({domain: "example.net"});
+ browser.test.assertEq(cookies.length, 0, "no cookies found for non-matching domain");
+
+ cookies = await browser.cookies.getAll({secure: false});
+ browser.test.assertEq(cookies.length, 1, "one non-secure cookie found");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({secure: true});
+ browser.test.assertEq(cookies.length, 0, "no secure cookies found");
+
+ cookies = await browser.cookies.getAll({storeId: STORE_ID});
+ browser.test.assertEq(cookies.length, 1, "one cookie found for valid storeId");
+ assertExpected(expected, cookies[0]);
+
+ cookies = await browser.cookies.getAll({storeId: "invalid_id"});
+ browser.test.assertEq(cookies.length, 0, "no cookies found for invalid storeId");
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ let stores = await browser.cookies.getAllCookieStores();
+ browser.test.assertEq(1, stores.length, "expected number of stores returned");
+ browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned");
+ browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store");
+
+ {
+ let privateWindow = await browser.windows.create({incognito: true});
+ let stores = await browser.cookies.getAllCookieStores();
+
+ browser.test.assertEq(2, stores.length, "expected number of stores returned");
+ browser.test.assertEq(STORE_ID, stores[0].id, "expected store id returned");
+ browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for store");
+ browser.test.assertEq(PRIVATE_STORE_ID, stores[1].id, "expected private store id returned");
+ browser.test.assertEq(1, stores[0].tabIds.length, "one tab returned for private store");
+
+ await browser.windows.remove(privateWindow.id);
+ }
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE});
+ browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name2"});
+ assertExpected({url: TEST_URL, name: "name2", storeId: STORE_ID}, details);
+
+ // Create a session cookie.
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"});
+ browser.test.assertEq(true, cookie.session, "session cookie set");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(true, cookie.session, "got session cookie");
+
+ cookies = await browser.cookies.getAll({session: true});
+ browser.test.assertEq(cookies.length, 1, "one session cookie found");
+ browser.test.assertEq(true, cookies[0].session, "found session cookie");
+
+ cookies = await browser.cookies.getAll({session: false});
+ browser.test.assertEq(cookies.length, 0, "no non-session cookies found");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ cookie = await browser.cookies.set({url: TEST_SECURE_URL, name: "name1", value: "value1", secure: true});
+ browser.test.assertEq(true, cookie.secure, "secure cookie set");
+
+ cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"});
+ browser.test.assertEq(true, cookie.session, "got secure cookie");
+
+ cookies = await browser.cookies.getAll({secure: true});
+ browser.test.assertEq(cookies.length, 1, "one secure cookie found");
+ browser.test.assertEq(true, cookies[0].secure, "found secure cookie");
+
+ cookies = await browser.cookies.getAll({secure: false});
+ browser.test.assertEq(cookies.length, 0, "no non-secure cookies found");
+
+ details = await browser.cookies.remove({url: TEST_SECURE_URL, name: "name1"});
+ assertExpected({url: TEST_SECURE_URL, name: "name1", storeId: STORE_ID}, details);
+
+ cookie = await browser.cookies.get({url: TEST_SECURE_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ cookie = await browser.cookies.set({url: TEST_URL_WITH_PATH, path: TEST_COOKIE_PATH, name: "name1", value: "value1", expirationDate: THE_FUTURE});
+ browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "created cookie with path");
+
+ cookie = await browser.cookies.get({url: TEST_URL_WITH_PATH, name: "name1"});
+ browser.test.assertEq(TEST_COOKIE_PATH, cookie.path, "got cookie with path");
+
+ cookies = await browser.cookies.getAll({path: TEST_COOKIE_PATH});
+ browser.test.assertEq(cookies.length, 1, "one cookie with path found");
+ browser.test.assertEq(TEST_COOKIE_PATH, cookies[0].path, "found cookie with path");
+
+ cookie = await browser.cookies.get({url: TEST_URL + "invalid_path", name: "name1"});
+ browser.test.assertEq(null, cookie, "get with invalid path returns null");
+
+ cookies = await browser.cookies.getAll({path: "/invalid_path"});
+ browser.test.assertEq(cookies.length, 0, "getAll with invalid path returns 0 cookies");
+
+ details = await browser.cookies.remove({url: TEST_URL_WITH_PATH, name: "name1"});
+ assertExpected({url: TEST_URL_WITH_PATH, name: "name1", storeId: STORE_ID}, details);
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: true});
+ browser.test.assertEq(true, cookie.httpOnly, "httpOnly cookie set");
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", httpOnly: false});
+ browser.test.assertEq(false, cookie.httpOnly, "non-httpOnly cookie set");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "name1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: STORE_ID}, details);
+
+ cookie = await browser.cookies.set({url: TEST_URL});
+ browser.test.assertEq("", cookie.name, "default name set");
+ browser.test.assertEq("", cookie.value, "default value set");
+ browser.test.assertEq(true, cookie.session, "no expiry date created session cookie");
+
+ {
+ let privateWindow = await browser.windows.create({incognito: true});
+
+ // Hacky work-around for bugzil.la/1309637
+ await new Promise(resolve => setTimeout(resolve, 700));
+
+ let cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "private", expirationDate: THE_FUTURE, storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq("private", cookie.value, "set the private cookie");
+
+ cookie = await browser.cookies.set({url: TEST_URL, name: "store", value: "default", expirationDate: THE_FUTURE, storeId: STORE_ID});
+ browser.test.assertEq("default", cookie.value, "set the default cookie");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq("private", cookie.value, "get the private cookie");
+ browser.test.assertEq(PRIVATE_STORE_ID, cookie.storeId, "get the private cookie storeId");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID});
+ browser.test.assertEq("default", cookie.value, "get the default cookie");
+ browser.test.assertEq(STORE_ID, cookie.storeId, "get the default cookie storeId");
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: STORE_ID});
+ assertExpected({url: TEST_URL, name: "store", storeId: STORE_ID}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: STORE_ID});
+ browser.test.assertEq(null, cookie, "deleted the default cookie");
+
+ details = await browser.cookies.remove({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ assertExpected({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "store", storeId: PRIVATE_STORE_ID});
+ browser.test.assertEq(null, cookie, "deleted the private cookie");
+
+ await browser.windows.remove(privateWindow.id);
+ }
+
+ browser.test.notifyPass("cookies");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["cookies", "*://example.org/"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("cookies");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html
new file mode 100644
index 0000000000..bc4994eec7
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_containers.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* setup() {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["privacy.userContext.enabled", true],
+ ]});
+});
+
+add_task(function* test_cookie_containers() {
+ async function background() {
+ function assertExpected(expected, cookie) {
+ for (let key of Object.keys(cookie)) {
+ browser.test.assertTrue(key in expected, `found property ${key}`);
+ browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`);
+ }
+ browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found");
+ }
+
+ const TEST_URL = "http://example.org/";
+ const THE_FUTURE = Date.now() + 5 * 60;
+
+ let expected = {
+ name: "name1",
+ value: "value1",
+ domain: "example.org",
+ hostOnly: true,
+ path: "/",
+ secure: false,
+ httpOnly: false,
+ session: false,
+ expirationDate: THE_FUTURE,
+ storeId: "firefox-container-1",
+ };
+
+ let cookie = await browser.cookies.set({
+ url: TEST_URL, name: "name1", value: "value1",
+ expirationDate: THE_FUTURE, storeId: "firefox-container-1",
+ });
+ browser.test.assertEq("firefox-container-1", cookie.storeId, "the cookie has the correct storeId");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1"});
+ browser.test.assertEq(null, cookie, "get() without storeId returns null");
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"});
+ assertExpected(expected, cookie);
+
+ let cookies = await browser.cookies.getAll({storeId: "firefox-default"});
+ browser.test.assertEq(0, cookies.length, "getAll() with default storeId returns an empty array");
+
+ cookies = await browser.cookies.getAll({storeId: "firefox-container-1"});
+ browser.test.assertEq(1, cookies.length, "one cookie found for matching domain");
+ assertExpected(expected, cookies[0]);
+
+ let details = await browser.cookies.remove({url: TEST_URL, name: "name1", storeId: "firefox-container-1"});
+ assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-container-1"}, details);
+
+ cookie = await browser.cookies.get({url: TEST_URL, name: "name1", storeId: "firefox-container-1"});
+ browser.test.assertEq(null, cookie, "removed cookie not found");
+
+ browser.test.notifyPass("cookies");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["cookies", "*://example.org/"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("cookies");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
new file mode 100644
index 0000000000..3927d9e94f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_cookies_expiry() {
+ function background() {
+ let expectedEvents = [];
+
+ browser.cookies.onChanged.addListener(event => {
+ expectedEvents.push(`${event.removed}:${event.cause}`);
+ if (expectedEvents.length === 1) {
+ browser.test.assertEq("true:expired", expectedEvents[0], "expired cookie removed");
+ browser.test.assertEq("first", event.cookie.name, "expired cookie has the expected name");
+ browser.test.assertEq("one", event.cookie.value, "expired cookie has the expected value");
+ } else {
+ browser.test.assertEq("false:explicit", expectedEvents[1], "new cookie added");
+ browser.test.assertEq("first", event.cookie.name, "new cookie has the expected name");
+ browser.test.assertEq("one-again", event.cookie.value, "new cookie has the expected value");
+ browser.test.notifyPass("cookie-expiry");
+ }
+ });
+
+ setTimeout(() => {
+ browser.test.sendMessage("change-cookies");
+ }, 1000);
+ }
+
+ let domain = ".example.com";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["http://example.com/", "cookies"],
+ },
+ background,
+ });
+
+ let cookieSvc = SpecialPowers.Services.cookies;
+
+ let cookie = {
+ host: domain,
+ name: "first",
+ path: "/",
+ };
+
+ do {
+ cookieSvc.add(cookie.host, cookie.path, cookie.name, "one", false, false, false, Date.now() / 1000 + 1);
+ } while (!cookieSvc.cookieExists(cookie));
+
+ yield extension.startup();
+ yield extension.awaitMessage("change-cookies");
+
+ cookieSvc.add(cookie.host, cookie.path, cookie.name, "one-again", false, false, false, Date.now() / 1000 + 10);
+
+ yield extension.awaitFinish("cookie-expiry");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html
new file mode 100644
index 0000000000..15a62855ad
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_bad.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_cookies.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* init() {
+ // We need to trigger a cookie eviction in order to test our batch delete
+ // observer.
+ SpecialPowers.setIntPref("network.cookie.maxPerHost", 3);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.maxPerHost");
+ });
+});
+
+add_task(function* test_bad_cookie_permissions() {
+ info("Test non-matching, non-secure domain with non-secure cookie");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.net/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching, secure domain with non-secure cookie");
+ yield testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.net/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching, secure domain with secure cookie");
+ yield testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.net/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test matching subdomain with superdomain privileges, secure cookie (http)");
+ yield testCookies({
+ permissions: ["http://foo.bar.example.com/", "cookies"],
+ url: "http://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: true,
+ });
+
+ info("Test matching, non-secure domain with secure cookie");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: true,
+ });
+
+ info("Test matching, non-secure host, secure URL");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test non-matching domain");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.net",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+
+ info("Test invalid scheme");
+ yield testCookies({
+ permissions: ["ftp://example.com/", "cookies"],
+ url: "ftp://example.com/",
+ domain: "example.com",
+ secure: false,
+ shouldPass: false,
+ shouldWrite: false,
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html
new file mode 100644
index 0000000000..31e83188c6
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_permissions_good.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_cookies.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* init() {
+ // We need to trigger a cookie eviction in order to test our batch delete
+ // observer.
+ SpecialPowers.setIntPref("network.cookie.maxPerHost", 3);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.maxPerHost");
+ });
+});
+
+add_task(function* test_good_cookie_permissions() {
+ info("Test matching, non-secure domain with non-secure cookie");
+ yield testCookies({
+ permissions: ["http://example.com/", "cookies"],
+ url: "http://example.com/",
+ domain: "example.com",
+ secure: false,
+ shouldPass: true,
+ });
+
+ info("Test matching, secure domain with non-secure cookie");
+ yield testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: false,
+ shouldPass: true,
+ });
+
+ info("Test matching, secure domain with secure cookie");
+ yield testCookies({
+ permissions: ["https://example.com/", "cookies"],
+ url: "https://example.com/",
+ domain: "example.com",
+ secure: true,
+ shouldPass: true,
+ });
+
+ info("Test matching subdomain with superdomain privileges, secure cookie (https)");
+ yield testCookies({
+ permissions: ["https://foo.bar.example.com/", "cookies"],
+ url: "https://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: true,
+ shouldPass: true,
+ });
+
+ info("Test matching subdomain with superdomain privileges, non-secure cookie (https)");
+ yield testCookies({
+ permissions: ["https://foo.bar.example.com/", "cookies"],
+ url: "https://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: false,
+ shouldPass: true,
+ });
+
+ info("Test matching subdomain with superdomain privileges, non-secure cookie (http)");
+ yield testCookies({
+ permissions: ["http://foo.bar.example.com/", "cookies"],
+ url: "http://foo.bar.example.com/",
+ domain: ".example.com",
+ secure: false,
+ shouldPass: true,
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html
new file mode 100644
index 0000000000..640522b40a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript() {
+ function background() {
+ browser.runtime.onMessage.addListener(([script], sender) => {
+ browser.test.sendMessage("run", {script});
+ browser.test.sendMessage("run-" + script);
+ });
+ browser.test.sendMessage("running");
+ }
+
+ function contentScriptAll() {
+ browser.runtime.sendMessage(["all"]);
+ }
+ function contentScriptIncludesTest1() {
+ browser.runtime.sendMessage(["includes-test1"]);
+ }
+ function contentScriptExcludesTest1() {
+ browser.runtime.sendMessage(["excludes-test1"]);
+ }
+
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://example.org/", "http://*.example.org/"],
+ "exclude_globs": [],
+ "include_globs": ["*"],
+ "js": ["content_script_all.js"],
+ },
+ {
+ "matches": ["http://example.org/", "http://*.example.org/"],
+ "include_globs": ["*test1*"],
+ "js": ["content_script_includes_test1.js"],
+ },
+ {
+ "matches": ["http://example.org/", "http://*.example.org/"],
+ "exclude_globs": ["*test1*"],
+ "js": ["content_script_excludes_test1.js"],
+ },
+ ],
+ },
+ background,
+
+ files: {
+ "content_script_all.js": contentScriptAll,
+ "content_script_includes_test1.js": contentScriptIncludesTest1,
+ "content_script_excludes_test1.js": contentScriptExcludesTest1,
+ },
+
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let ran = 0;
+ extension.onMessage("run", ({script}) => {
+ ran++;
+ });
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ info("extension loaded");
+
+ let win = window.open("http://example.org/");
+ yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-excludes-test1")]);
+ win.close();
+ is(ran, 2);
+
+ win = window.open("http://test1.example.org/");
+ yield Promise.all([extension.awaitMessage("run-all"), extension.awaitMessage("run-includes-test1")]);
+ win.close();
+ is(ran, 4);
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_generate.html b/toolkit/components/extensions/test/mochitest/test_ext_generate.html
new file mode 100644
index 0000000000..cfafcbad98
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for generating WebExtensions</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.test.log("running background script");
+
+ browser.test.onMessage.addListener((x, y) => {
+ browser.test.assertEq(x, 10, "x is 10");
+ browser.test.assertEq(y, 20, "y is 20");
+
+ browser.test.notifyPass("background test passed");
+ });
+
+ browser.test.sendMessage("running", 1);
+}
+
+let extensionData = {
+ background,
+};
+
+add_task(function* test_background() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ is(x, 1, "got correct value from extension");
+ info("startup complete");
+ extension.sendMessage(10, 20);
+ yield extension.awaitFinish();
+ info("test complete");
+ yield extension.unload();
+ info("extension unloaded successfully");
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_geturl.html b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
new file mode 100644
index 0000000000..6e39c2f5d1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onMessage.addListener(([url1, url2]) => {
+ let url3 = browser.runtime.getURL("test_file.html");
+ let url4 = browser.extension.getURL("test_file.html");
+
+ browser.test.assertTrue(url1 !== undefined, "url1 defined");
+
+ browser.test.assertTrue(url1.startsWith("moz-extension://"), "url1 has correct scheme");
+ browser.test.assertTrue(url1.endsWith("test_file.html"), "url1 has correct leaf name");
+
+ browser.test.assertEq(url1, url2, "url2 matches");
+ browser.test.assertEq(url1, url3, "url3 matches");
+ browser.test.assertEq(url1, url4, "url4 matches");
+
+ browser.test.notifyPass("geturl");
+ });
+}
+
+function contentScript() {
+ let url1 = browser.runtime.getURL("test_file.html");
+ let url2 = browser.extension.getURL("test_file.html");
+ browser.runtime.sendMessage([url1, url2]);
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("geturl")]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
new file mode 100644
index 0000000000..1f7330bbb1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -0,0 +1,432 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for WebExtension localization APIs</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("intl.accept_languages"); });
+SimpleTest.registerCleanupFunction(() => { SpecialPowers.clearUserPref("general.useragent.locale"); });
+
+add_task(function* test_i18n() {
+ function runTests(assertEq) {
+ let _ = browser.i18n.getMessage.bind(browser.i18n);
+
+ let url = browser.runtime.getURL("/");
+ assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message");
+
+ assertEq("Foo.", _("Foo"), "Simple message in selected locale.");
+
+ assertEq("(bar)", _("bar"), "Simple message fallback in default locale.");
+
+ assertEq("", _("some-unknown-locale-string"), "Unknown locale string.");
+
+ assertEq("", _("@@unknown_builtin_string"), "Unknown built-in string.");
+ assertEq("", _("@@bidi_unknown_builtin_string"), "Unknown built-in bidi string.");
+
+ assertEq("Føo.", _("Föo"), "Multi-byte message in selected locale.");
+
+ let substitutions = [];
+ substitutions[4] = "5";
+ substitutions[13] = "14";
+
+ assertEq("'$0' '14' '' '5' '$$$$' '$'.", _("basic_substitutions", substitutions),
+ "Basic numeric substitutions");
+
+ assertEq("'$0' '' 'just a string' '' '$$$$' '$'.", _("basic_substitutions", "just a string"),
+ "Basic numeric substitutions, with non-array value");
+
+ let values = _("named_placeholder_substitutions", ["(subst $1 $2)", "(2 $1 $2)"]).split("\n");
+
+ assertEq("_foo_ (subst $1 $2) _bar_", values[0], "Named and numeric substitution");
+
+ assertEq("(2 $1 $2)", values[1], "Numeric substitution amid named placeholders");
+
+ assertEq("$bad name$", values[2], "Named placeholder with invalid key");
+
+ assertEq("", values[3], "Named placeholder with an invalid value");
+
+ assertEq("Accepted, but shouldn't break.", values[4], "Named placeholder with a strange content value");
+
+ assertEq("$foo", values[5], "Non-placeholder token that should be ignored");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "default_locale": "jp",
+
+ content_scripts: [
+ {"matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content.js"]},
+ ],
+ },
+
+
+ files: {
+ "_locales/en_US/messages.json": {
+ "foo": {
+ "message": "Foo.",
+ "description": "foo",
+ },
+
+ "föo": {
+ "message": "Føo.",
+ "description": "foo",
+ },
+
+ "basic_substitutions": {
+ "message": "'$0' '$14' '$1' '$5' '$$$$$' '$$'.",
+ "description": "foo",
+ },
+
+ "Named_placeholder_substitutions": {
+ "message": "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo",
+ "description": "foo",
+ "placeholders": {
+ "foO": {
+ "content": "_foo_ $1 _bar_",
+ "description": "foo",
+ },
+
+ "bad name": {
+ "content": "Nope.",
+ "description": "bad name",
+ },
+
+ "bad_value": "Nope.",
+
+ "bad_content_value": {
+ "content": ["Accepted, but shouldn't break."],
+ "description": "bad value",
+ },
+ },
+ },
+
+ "broken_placeholders": {
+ "message": "$broken$",
+ "description": "broken placeholders",
+ "placeholders": "foo.",
+ },
+ },
+
+ "_locales/jp/messages.json": {
+ "foo": {
+ "message": "(foo)",
+ "description": "foo",
+ },
+
+ "bar": {
+ "message": "(bar)",
+ "description": "bar",
+ },
+ },
+
+ "content.js": "new " + function(runTestsFn) {
+ runTestsFn((...args) => {
+ browser.runtime.sendMessage(["assertEq", ...args]);
+ });
+
+ browser.runtime.sendMessage(["content-script-finished"]);
+ } + `(${runTests})`,
+ },
+
+ background: "new " + function(runTestsFn) {
+ browser.runtime.onMessage.addListener(([msg, ...args]) => {
+ if (msg == "assertEq") {
+ browser.test.assertEq(...args);
+ } else {
+ browser.test.sendMessage(msg, ...args);
+ }
+ });
+
+ runTestsFn(browser.test.assertEq.bind(browser.test));
+ } + `(${runTests})`,
+ });
+
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitMessage("content-script-finished");
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_get_accept_languages() {
+ function background() {
+ function checkResults(source, results, expected) {
+ browser.test.assertEq(
+ expected.length,
+ results.length,
+ `got expected number of languages in ${source}`);
+ results.forEach((lang, index) => {
+ browser.test.assertEq(
+ expected[index],
+ lang,
+ `got expected language in ${source}`);
+ });
+ }
+
+ let tabId;
+
+ browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ tabId = tabs[0].id;
+ browser.test.sendMessage("ready");
+ });
+
+ browser.test.onMessage.addListener(async ([msg, expected]) => {
+ let contentResults = await browser.tabs.sendMessage(tabId, "get-results");
+ let backgroundResults = await browser.i18n.getAcceptLanguages();
+
+ checkResults("contentScript", contentResults, expected);
+ checkResults("background", backgroundResults, expected);
+
+ browser.test.sendMessage("done");
+ });
+ }
+
+ function content() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ browser.i18n.getAcceptLanguages(respond);
+ return true;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": content,
+ },
+ });
+
+ let win = window.open("file_sample.html");
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ let expectedLangs = ["en-US", "en"];
+ extension.sendMessage(["expect-results", expectedLangs]);
+ yield extension.awaitMessage("done");
+
+ expectedLangs = ["en-US", "en", "fr-CA", "fr"];
+ SpecialPowers.setCharPref("intl.accept_languages", expectedLangs.toString());
+ extension.sendMessage(["expect-results", expectedLangs]);
+ yield extension.awaitMessage("done");
+ SpecialPowers.clearUserPref("intl.accept_languages");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_get_ui_language() {
+ function getResults() {
+ return {
+ getUILanguage: browser.i18n.getUILanguage(),
+ getMessage: browser.i18n.getMessage("@@ui_locale"),
+ };
+ }
+
+ function background(getResultsFn) {
+ function checkResults(source, results, expected) {
+ browser.test.assertEq(
+ expected,
+ results.getUILanguage,
+ `Got expected getUILanguage result in ${source}`
+ );
+ browser.test.assertEq(
+ expected,
+ results.getMessage,
+ `Got expected getMessage result in ${source}`
+ );
+ }
+
+ let tabId;
+
+ browser.test.onMessage.addListener(([msg, expected]) => {
+ browser.tabs.sendMessage(tabId, "get-results", result => {
+ checkResults("contentScript", result, expected);
+ checkResults("background", getResultsFn(), expected);
+
+ browser.test.sendMessage("done");
+ });
+ });
+
+ browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ tabId = tabs[0].id;
+ browser.test.sendMessage("ready");
+ });
+ }
+
+ function content(getResultsFn) {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ respond(getResultsFn());
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background: `(${background})(${getResults})`,
+
+ files: {
+ "content_script.js": `(${content})(${getResults})`,
+ },
+ });
+
+ let win = window.open("file_sample.html");
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage(["expect-results", "en_US"]);
+ yield extension.awaitMessage("done");
+
+ SpecialPowers.setCharPref("general.useragent.locale", "he");
+
+ extension.sendMessage(["expect-results", "he"]);
+ yield extension.awaitMessage("done");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+
+add_task(function* test_detect_language() {
+ const af_string = " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " +
+ "bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " +
+ "of winkels nie en slegs oornagbesoekers word toegelaat bateleur";
+ // String with intermixed French/English text
+ const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " +
+ "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " +
+ "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " +
+ "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." +
+ "Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumped over the lazy dog";
+
+ function background() {
+ function checkResult(source, result, expected) {
+ browser.test.assertEq(expected.isReliable, result.isReliable, "result.confident is true");
+ browser.test.assertEq(
+ expected.languages.length,
+ result.languages.length,
+ `result.languages contains the expected number of languages in ${source}`);
+ expected.languages.forEach((lang, index) => {
+ browser.test.assertEq(
+ lang.percentage,
+ result.languages[index].percentage,
+ `element ${index} of result.languages array has the expected percentage in ${source}`);
+ browser.test.assertEq(
+ lang.language,
+ result.languages[index].language,
+ `element ${index} of result.languages array has the expected language in ${source}`);
+ });
+ }
+
+ let tabId;
+
+ browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ tabId = tabs[0].id;
+ browser.test.sendMessage("ready");
+ });
+
+ browser.test.onMessage.addListener(async ([msg, expected]) => {
+ let backgroundResults = await browser.i18n.detectLanguage(msg);
+ let contentResults = await browser.tabs.sendMessage(tabId, msg);
+
+ checkResult("background", backgroundResults, expected);
+ checkResult("contentScript", contentResults, expected);
+
+ browser.test.sendMessage("done");
+ });
+ }
+
+ function content() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ browser.i18n.detectLanguage(msg, respond);
+ return true;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": content,
+ },
+ });
+
+ let win = window.open("file_sample.html");
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ let expected = {
+ isReliable: true,
+ languages: [
+ {
+ language: "fr",
+ percentage: 67,
+ },
+ {
+ language: "en",
+ percentage: 32,
+ },
+ ],
+ };
+ extension.sendMessage([fr_en_string, expected]);
+ yield extension.awaitMessage("done");
+
+ expected = {
+ isReliable: true,
+ languages: [
+ {
+ language: "af",
+ percentage: 99,
+ },
+ ],
+ };
+ extension.sendMessage([af_string, expected]);
+ yield extension.awaitMessage("done");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html
new file mode 100644
index 0000000000..7c6a8eeaaa
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_i18n_css() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function() {
+ function backgroundFetch(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => { resolve(xhr.responseText); };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+ }
+
+ Promise.all([backgroundFetch("foo.css"), backgroundFetch("bar.CsS?x#y"), backgroundFetch("foo.txt")]).then(results => {
+ browser.test.assertEq("body { max-width: 42px; }", results[0], "CSS file localized");
+ browser.test.assertEq("body { max-width: 42px; }", results[1], "CSS file localized");
+
+ browser.test.assertEq("body { __MSG_foo__; }", results[2], "Text file not localized");
+
+ browser.test.notifyPass("i18n-css");
+ });
+
+ browser.test.sendMessage("ready", browser.runtime.getURL("foo.css"));
+ },
+
+ manifest: {
+ "web_accessible_resources": ["foo.css", "foo.txt", "locale.css"],
+
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "css": ["foo.css"],
+ }],
+
+ "default_locale": "en",
+ },
+
+ files: {
+ "_locales/en/messages.json": JSON.stringify({
+ "foo": {
+ "message": "max-width: 42px",
+ "description": "foo",
+ },
+ }),
+
+ "foo.css": "body { __MSG_foo__; }",
+ "bar.CsS": "body { __MSG_foo__; }",
+ "foo.txt": "body { __MSG_foo__; }",
+ "locale.css": '* { content: "__MSG_@@ui_locale__ __MSG_@@bidi_dir__ __MSG_@@bidi_reversed_dir__ __MSG_@@bidi_start_edge__ __MSG_@@bidi_end_edge__" }',
+ },
+ });
+
+ yield extension.startup();
+ let cssURL = yield extension.awaitMessage("ready");
+
+ function fetch(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => { resolve(xhr.responseText); };
+ xhr.onerror = reject;
+ xhr.send();
+ });
+ }
+
+ let css = yield fetch(cssURL);
+
+ is(css, "body { max-width: 42px; }", "CSS file localized in mochitest scope");
+
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.maxWidth, "42px", "stylesheet correctly applied");
+ win.close();
+
+ cssURL = cssURL.replace(/foo.css$/, "locale.css");
+
+ css = yield fetch(cssURL);
+ is(css, '* { content: "en_US ltr rtl left right" }', "CSS file localized in mochitest scope");
+
+ const LOCALE = "general.useragent.locale";
+ const DIR = "intl.uidirection.en";
+
+ // We don't wind up actually switching the chrome registry locale, since we
+ // don't have a chrome package for Hebrew. So just override it.
+ SpecialPowers.setCharPref(LOCALE, "he");
+ SpecialPowers.setCharPref(DIR, "rtl");
+
+ css = yield fetch(cssURL);
+ is(css, '* { content: "he rtl ltr right left" }', "CSS file localized in mochitest scope");
+
+ SpecialPowers.clearUserPref(LOCALE);
+ SpecialPowers.clearUserPref(DIR);
+
+ yield extension.awaitFinish("i18n-css");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html
new file mode 100644
index 0000000000..675cbb2988
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_in_incognito_context_true() {
+ function background() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(true, msg, "inIncognitoContext is true");
+ browser.test.notifyPass("inIncognitoContext");
+ });
+
+ browser.windows.create({url: browser.runtime.getURL("/tab.html"), incognito: true});
+ }
+
+ function tabScript() {
+ browser.runtime.sendMessage(browser.extension.inIncognitoContext);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ files: {
+ "tab.js": tabScript,
+ "tab.html": `<!DOCTYPE html><html><head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head></html>`,
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("inIncognitoContext");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
new file mode 100644
index 0000000000..da0c355e0b
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_versioned_js() {
+ // We need to deal with escaping the close script tags.
+ // May as well consolidate it into one place.
+ let script = attrs => `<script ${attrs}><\/script>`;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "background": {"page": "background.html"},
+ },
+
+ files: {
+ "background.html": `
+ <meta charset="utf-8">
+ ${script('src="background.js" type="application/javascript"')}
+ ${script('src="background-1.js" type="application/javascript;version=1.8"')}
+ ${script('src="background-2.js" type="application/javascript;version=latest"')}
+ ${script('src="background-3.js" type="application/javascript"')}
+ `,
+
+ "background.js": function() {
+ window.reportResult = msg => {
+ browser.test.assertEq(
+ msg, "background-script-3",
+ "Expected a message only from the unversioned background script.");
+
+ browser.test.sendMessage("finished");
+ };
+ },
+
+ "background-1.js": function() {
+ window.reportResult("background-script-1");
+ },
+ "background-2.js": function() {
+ window.reportResult("background-script-2");
+ },
+ "background-3.js": function() {
+ window.reportResult("background-script-3");
+ },
+ },
+ });
+
+ let messages = [/Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/,
+ /Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/];
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ info("loading extension");
+
+ yield Promise.all([extension.startup(),
+ extension.awaitMessage("finished")]);
+
+ info("waiting for console");
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+
+ info("unloading extension");
+
+ yield extension.unload();
+
+ info("test complete");
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html
new file mode 100644
index 0000000000..ca8db873e4
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_listener_proxies.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_listener_proxies() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ "permissions": ["storage"],
+ },
+
+ async background() {
+ // Test that adding multiple listeners for the same event works as
+ // expected.
+
+ let awaitChanged = () => new Promise(resolve => {
+ browser.storage.onChanged.addListener(function listener() {
+ browser.storage.onChanged.removeListener(listener);
+ resolve();
+ });
+ });
+
+ let promises = [
+ awaitChanged(),
+ awaitChanged(),
+ ];
+
+ function removedListener() {}
+ browser.storage.onChanged.addListener(removedListener);
+ browser.storage.onChanged.removeListener(removedListener);
+
+ promises.push(awaitChanged(), awaitChanged());
+
+ browser.storage.local.set({foo: "bar"});
+
+ await Promise.all(promises);
+
+ browser.test.notifyPass("onchanged-listeners");
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("onchanged-listeners");
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
new file mode 100644
index 0000000000..d1b798cf96
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
@@ -0,0 +1,224 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for notifications</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+add_task(function* test_notification() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ let id = await browser.notifications.create(opts);
+
+ browser.test.sendMessage("running", id);
+ browser.test.notifyPass("background test passed");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ let x = yield extension.awaitMessage("running");
+ is(x, "0", "got correct id from notifications.create");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_notification_events() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ // Test an ignored listener.
+ browser.notifications.onButtonClicked.addListener(function() {});
+
+ // We cannot test onClicked listener without a mock
+ // but we can attempt to add a listener.
+ browser.notifications.onClicked.addListener(function() {});
+
+ // Test onClosed listener.
+ browser.notifications.onClosed.addListener(id => {
+ browser.test.sendMessage("closed", id);
+ browser.test.notifyPass("background test passed");
+ });
+
+ await browser.notifications.create("5", opts);
+ let id = await browser.notifications.create("5", opts);
+ browser.test.sendMessage("running", id);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ let x = yield extension.awaitMessage("closed");
+ is(x, "5", "got correct id from onClosed listener");
+ x = yield extension.awaitMessage("running");
+ is(x, "5", "got correct id from notifications.create");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_notification_clear() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ browser.notifications.onClosed.addListener(id => {
+ browser.test.sendMessage("closed", id);
+ });
+
+ let id = await browser.notifications.create("99", opts);
+
+ let wasCleared = await browser.notifications.clear(id);
+ browser.test.sendMessage("cleared", wasCleared);
+
+ browser.test.notifyPass("background test passed");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ let x = yield extension.awaitMessage("closed");
+ is(x, "99", "got correct id from onClosed listener");
+ x = yield extension.awaitMessage("cleared");
+ is(x, true, "got correct boolean from notifications.clear");
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_notifications_empty_getAll() {
+ async function background() {
+ let notifications = await browser.notifications.getAll();
+
+ browser.test.assertEq("object", typeof notifications, "getAll() returned an object");
+ browser.test.assertEq(0, Object.keys(notifications).length, "the object has no properties");
+ browser.test.notifyPass("getAll empty");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("getAll empty");
+ yield extension.unload();
+});
+
+add_task(function* test_notifications_populated_getAll() {
+ async function background() {
+ let opts = {
+ type: "basic",
+ iconUrl: "a.png",
+ title: "Testing Notification",
+ message: "Carry on",
+ };
+
+ await browser.notifications.create("p1", opts);
+ await browser.notifications.create("p2", opts);
+ let notifications = await browser.notifications.getAll();
+
+ browser.test.assertEq("object", typeof notifications, "getAll() returned an object");
+ browser.test.assertEq(2, Object.keys(notifications).length, "the object has 2 properties");
+
+ for (let notificationId of ["p1", "p2"]) {
+ for (let key of Object.keys(opts)) {
+ browser.test.assertEq(
+ opts[key],
+ notifications[notificationId][key],
+ `the notification has the expected value for option: ${key}`
+ );
+ }
+ }
+
+ browser.test.notifyPass("getAll populated");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ files: {
+ "a.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("getAll populated");
+ yield extension.unload();
+});
+
+add_task(function* test_buttons_unsupported() {
+ function background() {
+ let opts = {
+ type: "basic",
+ title: "Testing Notification",
+ message: "Carry on",
+ buttons: [{title: "Button title"}],
+ };
+
+ let exception = {};
+ try {
+ browser.notifications.create(opts);
+ } catch (e) {
+ exception = e;
+ }
+
+ browser.test.assertTrue(
+ String(exception).includes('Property "buttons" is unsupported by Firefox'),
+ "notifications.create with buttons option threw an expected exception"
+ );
+ browser.test.notifyPass("buttons-unsupported");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["notifications"],
+ },
+ background,
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("buttons-unsupported");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html
new file mode 100644
index 0000000000..07967d5d0e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_permission_xhr.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_simple() {
+ async function runTests(cx) {
+ function xhr(XMLHttpRequest) {
+ return (url) => {
+ return new Promise((resolve, reject) => {
+ let req = new XMLHttpRequest();
+ req.open("GET", url);
+ req.addEventListener("load", resolve);
+ req.addEventListener("error", reject);
+ req.send();
+ });
+ };
+ }
+
+ function run(shouldFail, fetch) {
+ function passListener() {
+ browser.test.succeed(`${cx}.${fetch.name} pass listener`);
+ }
+
+ function failListener() {
+ browser.test.fail(`${cx}.${fetch.name} fail listener`);
+ }
+
+ /* eslint-disable no-else-return */
+ if (shouldFail) {
+ return fetch("http://example.org/example.txt").then(failListener, passListener);
+ } else {
+ return fetch("http://example.com/example.txt").then(passListener, failListener);
+ }
+ /* eslint-enable no-else-return */
+ }
+
+ try {
+ await run(true, xhr(XMLHttpRequest));
+ await run(false, xhr(XMLHttpRequest));
+ await run(true, xhr(window.XMLHttpRequest));
+ await run(false, xhr(window.XMLHttpRequest));
+ await run(true, fetch);
+ await run(false, fetch);
+ await run(true, window.fetch);
+ await run(false, window.fetch);
+ } catch (err) {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("permission_xhr");
+ }
+ }
+
+ async function background(runTestsFn) {
+ await runTestsFn("bg");
+ browser.test.notifyPass("permission_xhr");
+ }
+
+ let extensionData = {
+ background: `(${background})(${runTests})`,
+ manifest: {
+ permissions: ["http://example.com/"],
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_permission_xhr.html"],
+ "js": ["content.js"],
+ }],
+ },
+ files: {
+ "content.js": `(${async runTestsFn => {
+ await runTestsFn("content");
+
+ window.wrappedJSObject.privilegedFetch = fetch;
+ window.wrappedJSObject.privilegedXHR = XMLHttpRequest;
+
+ window.addEventListener("message", function rcv({data}) {
+ switch (data.msg) {
+ case "test":
+ break;
+
+ case "assertTrue":
+ browser.test.assertTrue(data.condition, data.description);
+ break;
+
+ case "finish":
+ window.removeEventListener("message", rcv, false);
+ browser.test.sendMessage("content-script-finished");
+ break;
+ }
+ }, false);
+ window.postMessage("test", "*");
+ }})(${runTests})`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_permission_xhr.html");
+ yield extension.awaitMessage("content-script-finished");
+ win.close();
+
+ yield extension.awaitFinish("permission_xhr");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html
new file mode 100644
index 0000000000..60351eaee4
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.name, "ernie", "port name correct");
+ browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct");
+ browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct");
+
+ let expected = "message 1";
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, expected, "message is expected");
+ if (expected == "message 1") {
+ port.postMessage("message 2");
+ expected = "message 3";
+ } else if (expected == "message 3") {
+ expected = "disconnect";
+ browser.test.notifyPass("runtime.connect");
+ }
+ });
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(null, port.error, "No error because port is closed by disconnect() at other end");
+ browser.test.assertEq(expected, "disconnect", "got disconnection at right time");
+ });
+ });
+}
+
+function contentScript() {
+ let port = browser.runtime.connect({name: "ernie"});
+ port.postMessage("message 1");
+ port.onMessage.addListener(msg => {
+ if (msg == "message 2") {
+ port.postMessage("message 3");
+ port.disconnect();
+ }
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("runtime.connect")]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html
new file mode 100644
index 0000000000..dce12b21be
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect2.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function backgroundScript(token) {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "done");
+ browser.test.notifyPass("sendmessage_reply");
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "sender url correct");
+ browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ let tabId = port.sender.tab.id;
+ browser.tabs.connect(tabId, {name: token});
+
+ browser.test.assertEq(port.name, token, "token matches");
+ port.postMessage(token + "-done");
+ });
+
+ browser.test.sendMessage("background-ready");
+}
+
+function contentScript(token) {
+ let gotTabMessage = false;
+ let badTabMessage = false;
+ browser.runtime.onConnect.addListener(port => {
+ if (port.name == token) {
+ gotTabMessage = true;
+ } else {
+ badTabMessage = true;
+ }
+ port.disconnect();
+ });
+
+ let port = browser.runtime.connect(null, {name: token});
+ port.onMessage.addListener(function(msg) {
+ if (msg != token + "-done" || !gotTabMessage || badTabMessage) {
+ return; // test failed
+ }
+
+ // FIXME: Removing this line causes the test to fail:
+ // resource://gre/modules/ExtensionUtils.jsm, line 651: NS_ERROR_NOT_INITIALIZED
+ port.disconnect();
+ browser.runtime.sendMessage("done");
+ });
+}
+
+function makeExtension() {
+ let token = Math.random();
+ let extensionData = {
+ background: `(${backgroundScript})("${token}")`,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})("${token}")`,
+ },
+ };
+ return extensionData;
+}
+
+add_task(function* test_contentscript() {
+ let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
+ let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+ yield Promise.all([extension1.startup(), extension2.startup()]);
+
+ yield extension1.awaitMessage("background-ready");
+ yield extension2.awaitMessage("background-ready");
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win),
+ extension1.awaitFinish("sendmessage_reply"),
+ extension2.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ yield extension1.unload();
+ yield extension2.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html
new file mode 100644
index 0000000000..e84134eff8
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+add_task(function* test_connect_bidirectionally_and_postMessage() {
+ function background() {
+ let onConnectCount = 0;
+ browser.runtime.onConnect.addListener(port => {
+ // 3. onConnect by connect() from CS.
+ browser.test.assertEq("from-cs", port.name);
+ browser.test.assertEq(1, ++onConnectCount,
+ "BG onConnect should be called once");
+
+ let tabId = port.sender.tab.id;
+ browser.test.assertTrue(tabId, "content script must have a tab ID");
+
+ let port2;
+ let postMessageCount1 = 0;
+ port.onMessage.addListener(msg => {
+ // 11. port.onMessage by port.postMessage in CS.
+ browser.test.assertEq("from CS to port", msg);
+ browser.test.assertEq(1, ++postMessageCount1,
+ "BG port.onMessage should be called once");
+
+ // 12. should trigger port2.onMessage in CS.
+ port2.postMessage("from BG to port2");
+ });
+
+ // 4. Should trigger onConnect in CS.
+ port2 = browser.tabs.connect(tabId, {name: "from-bg"});
+ let postMessageCount2 = 0;
+ port2.onMessage.addListener(msg => {
+ // 7. onMessage by port2.postMessage in CS.
+ browser.test.assertEq("from CS to port2", msg);
+ browser.test.assertEq(1, ++postMessageCount2,
+ "BG port2.onMessage should be called once");
+
+ // 8. Should trigger port.onMessage in CS.
+ port.postMessage("from BG to port");
+ });
+ });
+
+ // 1. Notify test runner to create a new tab.
+ browser.test.sendMessage("ready");
+ }
+
+ function contentScript() {
+ let onConnectCount = 0;
+ let port;
+ browser.runtime.onConnect.addListener(port2 => {
+ // 5. onConnect by connect() from BG.
+ browser.test.assertEq("from-bg", port2.name);
+ browser.test.assertEq(1, ++onConnectCount,
+ "CS onConnect should be called once");
+
+ let postMessageCount2 = 0;
+ port2.onMessage.addListener(msg => {
+ // 12. port2.onMessage by port2.postMessage in BG.
+ browser.test.assertEq("from BG to port2", msg);
+ browser.test.assertEq(1, ++postMessageCount2,
+ "CS port2.onMessage should be called once");
+
+ // TODO(robwu): Do not explicitly disconnect, it should not be a problem
+ // if we keep the ports open. However, not closing the ports causes the
+ // test to fail with NS_ERROR_NOT_INITIALIZED in ExtensionUtils.jsm, in
+ // Port.prototype.disconnect (nsIMessageSender.sendAsyncMessage).
+ port.disconnect();
+ port2.disconnect();
+ browser.test.notifyPass("ping pong done");
+ });
+ // 6. should trigger port2.onMessage in BG.
+ port2.postMessage("from CS to port2");
+ });
+
+ // 2. should trigger onConnect in BG.
+ port = browser.runtime.connect({name: "from-cs"});
+ let postMessageCount1 = 0;
+ port.onMessage.addListener(msg => {
+ // 9. onMessage by port.postMessage in BG.
+ browser.test.assertEq("from BG to port", msg);
+ browser.test.assertEq(1, ++postMessageCount1,
+ "CS port.onMessage should be called once");
+
+ // 10. should trigger port.onMessage in BG.
+ port.postMessage("from CS to port");
+ });
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ info("extension loaded");
+
+ yield extension.awaitMessage("ready");
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitFinish("ping pong done");
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+</body>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
new file mode 100644
index 0000000000..5764d0a3c2
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.name, "ernie", "port name correct");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads");
+ // Closing an already-disconnected port is a no-op.
+ port.disconnect();
+ port.disconnect();
+ browser.test.sendMessage("disconnected");
+ });
+ browser.test.sendMessage("connected");
+ });
+}
+
+function contentScript() {
+ browser.runtime.connect({name: "ernie"});
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]);
+ win.close();
+ yield extension.awaitMessage("disconnected");
+
+ info("win.close() succeeded");
+
+ win = window.open("file_sample.html");
+ yield Promise.all([waitForLoad(win), extension.awaitMessage("connected")]);
+
+ // Add an "unload" listener so that we don't put the window in the
+ // bfcache. This way it gets destroyed immediately upon navigation.
+ win.addEventListener("unload", function() {}); // eslint-disable-line mozilla/balanced-listeners
+
+ win.location = "http://example.com";
+ yield extension.awaitMessage("disconnected");
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
new file mode 100644
index 0000000000..4cdefda410
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for browser.runtime.id</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_runtime_id() {
+ function background() {
+ browser.test.sendMessage("background-id", browser.runtime.id);
+ }
+
+ function content() {
+ browser.test.sendMessage("content-id", browser.runtime.id);
+ }
+
+ let uuidGenerator = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"].getService(SpecialPowers.Ci.nsIUUIDGenerator);
+ let id = uuidGenerator.generateUUID().number;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {gecko: {id}},
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": content,
+ },
+ });
+
+ yield extension.startup();
+
+ let backgroundId = yield extension.awaitMessage("background-id");
+ is(backgroundId, id, "runtime.id from background script is correct");
+ let win = window.open("file_sample.html");
+ let contentId = yield extension.awaitMessage("content-id");
+ is(contentId, id, "runtime.id from content script is correct");
+
+ win.close();
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
new file mode 100644
index 0000000000..426a71ac6a
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onMessage.addListener(result => {
+ browser.test.assertEq(result, 12, "x is 12");
+ browser.test.notifyPass("background test passed");
+ });
+}
+
+function contentScript() {
+ window.x = 12;
+ browser.runtime.onMessage.addListener(function() {});
+ browser.runtime.sendMessage(window.x);
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish()]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_schema.html b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
new file mode 100644
index 0000000000..8a0e11c56c
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for schema API creation</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* testEmptySchema() {
+ function background() {
+ browser.test.assertEq(undefined, browser.manifest, "browser.manifest is not defined");
+ browser.test.assertTrue("storage" in browser, "browser.storage should be defined");
+ browser.test.assertEq(undefined, browser.contextMenus, "browser.contextMenus should not be defined");
+ browser.test.notifyPass("schema");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("schema");
+ yield extension.unload();
+});
+
+add_task(function* testUnknownProperties() {
+ function background() {
+ browser.test.notifyPass("loaded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["unknownPermission"],
+
+ unknown_property: {},
+ },
+
+ background,
+ });
+
+ let messages = [
+ {message: /processing permissions\.0: Unknown permission "unknownPermission"/},
+ {message: /processing unknown_property: An unexpected property was found in the WebExtension manifest/},
+ ];
+
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, messages);
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("loaded");
+
+ yield extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
new file mode 100644
index 0000000000..a3ef37cadf
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ // Add two listeners that both send replies. We're supposed to ignore all but one
+ // of them. Which one is chosen is non-deterministic.
+
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "getreply") {
+ sendReply("reply1");
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "getreply") {
+ sendReply("reply2");
+ }
+ });
+
+ function sleep(callback, n = 10) {
+ if (n == 0) {
+ callback();
+ } else {
+ setTimeout(function() { sleep(callback, n - 1); }, 0);
+ }
+ }
+
+ let done_count = 0;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "done") {
+ done_count++;
+ browser.test.assertEq(done_count, 1, "got exactly one reply");
+
+ // Go through the event loop a few times to make sure we don't get multiple replies.
+ sleep(function() {
+ browser.test.notifyPass("sendmessage_doublereply");
+ });
+ }
+ });
+}
+
+function contentScript() {
+ browser.runtime.sendMessage("getreply", function(resp) {
+ if (resp != "reply1" && resp != "reply2") {
+ return; // test failed
+ }
+ browser.runtime.sendMessage("done");
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_doublereply")]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html
new file mode 100644
index 0000000000..96af6558e1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+
+function loadContentScriptExtension(contentScript) {
+ let extensionData = {
+ manifest: {
+ "content_scripts": [{
+ "js": ["contentscript.js"],
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ },
+ };
+ return ExtensionTestUtils.loadExtension(extensionData);
+}
+
+add_task(function* test_content_script_sendMessage_without_listener() {
+ async function contentScript() {
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("msg"),
+ "Could not establish connection. Receiving end does not exist.");
+
+ browser.test.notifyPass("sendMessage callback was invoked");
+ }
+
+ let extension = loadContentScriptExtension(contentScript);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitFinish("sendMessage callback was invoked");
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_content_script_chrome_sendMessage_without_listener() {
+ function contentScript() {
+ /* globals chrome */
+ browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call");
+ let retval = chrome.runtime.sendMessage("msg");
+ browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call");
+ // TODO(robwu): Fix the implementation and uncomment the next expectation.
+ // When content script APIs are schema-based (bugzil.la/1287007) this bug will be fixed for free.
+ // browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback");
+ browser.test.assertTrue(retval instanceof Promise, "TODO: chrome.runtime.sendMessage should return undefined, not a promise");
+
+ let isAsyncCall = false;
+ retval = chrome.runtime.sendMessage("msg", reply => {
+ browser.test.assertEq(undefined, reply, "no reply");
+ browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously");
+ browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback");
+ browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message);
+ browser.test.notifyPass("finished chrome.runtime.sendMessage");
+ });
+ isAsyncCall = true;
+ }
+
+ let extension = loadContentScriptExtension(contentScript);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+ yield extension.awaitFinish("finished chrome.runtime.sendMessage");
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
new file mode 100644
index 0000000000..a4ac708b28
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function background() {
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == 0) {
+ sendReply("reply1");
+ } else if (msg == 1) {
+ window.setTimeout(function() {
+ sendReply("reply2");
+ }, 0);
+ return true;
+ } else if (msg == 2) {
+ browser.test.notifyPass("sendmessage_reply");
+ }
+ });
+}
+
+function contentScript() {
+ browser.runtime.sendMessage(0, function(resp1) {
+ if (resp1 != "reply1") {
+ return; // test failed
+ }
+ browser.runtime.sendMessage(1, function(resp2) {
+ if (resp2 != "reply2") {
+ return; // test failed
+ }
+ browser.runtime.sendMessage(2);
+ });
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+ },
+
+ files: {
+ "content_script.js": contentScript,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win), extension.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ yield extension.unload();
+ info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
new file mode 100644
index 0000000000..1ebc1b40fd
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+function backgroundScript(token) {
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+
+ if (msg == "done") {
+ browser.test.notifyPass("sendmessage_reply");
+ return;
+ }
+
+ let tabId = sender.tab.id;
+ browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
+
+ browser.test.assertEq(msg, token, "token matches");
+ sendReply(`${token}-done`);
+ });
+}
+
+function contentScript(token) {
+ let gotTabMessage = false;
+ let badTabMessage = false;
+ browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+ if (msg == `${token}-tabMessage`) {
+ gotTabMessage = true;
+ } else {
+ badTabMessage = true;
+ }
+ });
+
+ browser.runtime.sendMessage(token, function(resp) {
+ if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
+ return; // test failed
+ }
+ browser.runtime.sendMessage("done");
+ });
+}
+
+function makeExtension() {
+ let token = Math.random();
+ let extensionData = {
+ background: `(${backgroundScript})(${token})`,
+ manifest: {
+ "permissions": ["tabs"],
+ "content_scripts": [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ }],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})(${token})`,
+ },
+ };
+ return extensionData;
+}
+
+add_task(function* test_contentscript() {
+ let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
+ let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+
+ yield Promise.all([extension1.startup(), extension2.startup()]);
+
+ let win = window.open("file_sample.html");
+
+ yield Promise.all([waitForLoad(win),
+ extension1.awaitFinish("sendmessage_reply"),
+ extension2.awaitFinish("sendmessage_reply")]);
+
+ win.close();
+
+ yield extension1.unload();
+ yield extension2.unload();
+ info("extensions unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html
new file mode 100644
index 0000000000..09a33814a8
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html
@@ -0,0 +1,330 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+"use strict";
+
+// Copied from toolkit/components/extensions/test/xpcshell/test_ext_storage.js.
+// The storage API in content scripts should behave identical to the storage API
+// in background pages.
+const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
+/**
+ * Utility function to ensure that all supported APIs for getting are
+ * tested.
+ *
+ * @param {string} areaName
+ * either "local" or "sync" according to what we want to test
+ * @param {string} prop
+ * "key" to look up using the storage API
+ * @param {Object} value
+ * "value" to compare against
+ */
+async function checkGetImpl(areaName, prop, value) {
+ let storage = browser.storage[areaName];
+
+ let data = await storage.get(null);
+ browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get(prop);
+ browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get([prop]);
+ browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get({[prop]: undefined});
+ browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`);
+}
+
+async function contentScript(checkGet) {
+ let globalChanges, gResolve;
+ function clearGlobalChanges() {
+ globalChanges = new Promise(resolve => { gResolve = resolve; });
+ }
+ clearGlobalChanges();
+ let expectedAreaName;
+
+ browser.storage.onChanged.addListener((changes, areaName) => {
+ browser.test.assertEq(expectedAreaName, areaName,
+ "Expected area name received by listener");
+ gResolve(changes);
+ });
+
+ async function checkChanges(areaName, changes, message) {
+ function checkSub(obj1, obj2) {
+ for (let prop in obj1) {
+ browser.test.assertTrue(obj1[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertTrue(obj2[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue,
+ `checkChanges ${areaName} ${prop} old (${message})`);
+ browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue,
+ `checkChanges ${areaName} ${prop} new (${message})`);
+ }
+ }
+
+ const recentChanges = await globalChanges;
+ checkSub(changes, recentChanges);
+ checkSub(recentChanges, changes);
+ clearGlobalChanges();
+ }
+
+ /* eslint-disable dot-notation */
+ async function runTests(areaName) {
+ expectedAreaName = areaName;
+ let storage = browser.storage[areaName];
+ // Set some data and then test getters.
+ try {
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ await checkChanges(areaName,
+ {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}},
+ "set (a)");
+
+ await checkGet(areaName, "test-prop1", "value1");
+ await checkGet(areaName, "test-prop2", "value2");
+
+ let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"});
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)");
+ browser.test.assertEq("default", data["other"], "other correct");
+
+ data = await storage.get(["test-prop1", "test-prop2", "other"]);
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)");
+ browser.test.assertFalse("other" in data, "other correct");
+
+ // Remove data in various ways.
+ await storage.remove("test-prop1");
+ await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)");
+ browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)");
+
+ await storage.set({"test-prop1": "value1"});
+ await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)");
+ browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)");
+
+ await storage.remove(["test-prop1", "test-prop2"]);
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "remove array");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)");
+
+ // test storage.clear
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ // Make sure that set() handler happened before we clear the
+ // promise again.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.clear();
+
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "clear");
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)");
+
+ // Make sure we can store complex JSON data.
+ // known previous values
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+
+ // Make sure the set() handler landed.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.set({
+ "test-prop1": {
+ str: "hello",
+ bool: true,
+ null: null,
+ undef: undefined,
+ obj: {},
+ arr: [1, 2],
+ date: new Date(0),
+ regexp: /regexp/,
+ func: function func() {},
+ window,
+ },
+ });
+
+ await storage.set({"test-prop2": function func() {}});
+ const recentChanges = await globalChanges;
+
+ browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct");
+ browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj");
+ clearGlobalChanges();
+
+ data = await storage.get({"test-prop1": undefined, "test-prop2": undefined});
+ let obj = data["test-prop1"];
+
+ browser.test.assertEq("hello", obj.str, "string part correct");
+ browser.test.assertEq(true, obj.bool, "bool part correct");
+ browser.test.assertEq(null, obj.null, "null part correct");
+ browser.test.assertEq(undefined, obj.undef, "undefined part correct");
+ browser.test.assertEq(undefined, obj.func, "function part correct");
+ browser.test.assertEq(undefined, obj.window, "window part correct");
+ browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct");
+ browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct");
+ browser.test.assertEq("object", typeof(obj.obj), "object part correct");
+ browser.test.assertTrue(Array.isArray(obj.arr), "array part present");
+ browser.test.assertEq(1, obj.arr[0], "arr[0] part correct");
+ browser.test.assertEq(2, obj.arr[1], "arr[1] part correct");
+ browser.test.assertEq(2, obj.arr.length, "arr.length part correct");
+
+ obj = data["test-prop2"];
+
+ browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object");
+ browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("storage");
+ }
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ let promise;
+ if (msg === "test-local") {
+ promise = runTests("local");
+ } else if (msg === "test-sync") {
+ promise = runTests("sync");
+ }
+ promise.then(() => browser.test.sendMessage("test-finished"));
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+let extensionData = {
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ }],
+
+ permissions: ["storage"],
+ },
+
+ files: {
+ "content_script.js": `(${contentScript})(${checkGetImpl})`,
+ },
+};
+
+add_task(function* test_contentscript() {
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ yield SpecialPowers.pushPrefEnv({
+ set: [[STORAGE_SYNC_PREF, true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ extension.sendMessage("test-local");
+ yield extension.awaitMessage("test-finished");
+
+ extension.sendMessage("test-sync");
+ yield extension.awaitMessage("test-finished");
+
+ yield SpecialPowers.popPrefEnv();
+ yield extension.unload();
+
+ win.close();
+});
+
+add_task(function* test_local_cache_invalidation() {
+ let win = window.open("file_sample.html");
+
+ function background(checkGet) {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "set-initial") {
+ await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"});
+ browser.test.sendMessage("set-initial-done");
+ } else if (msg === "check") {
+ await checkGet("local", "test-prop1", "value1");
+ await checkGet("local", "test-prop2", "value2");
+ browser.test.sendMessage("check-done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("set-initial");
+ yield extension.awaitMessage("set-initial-done");
+
+ SpecialPowers.invalidateExtensionStorageCache();
+
+ extension.sendMessage("check");
+ yield extension.awaitMessage("check-done");
+
+ yield extension.unload();
+ win.close();
+});
+
+add_task(function* test_config_flag_needed() {
+ let win = window.open("file_sample.html");
+ yield waitForLoad(win);
+
+ function background() {
+ let promises = [];
+ let apiTests = [
+ {method: "get", args: ["foo"]},
+ {method: "set", args: [{foo: "bar"}]},
+ {method: "remove", args: ["foo"]},
+ {method: "clear", args: []},
+ ];
+ apiTests.forEach(testDef => {
+ promises.push(browser.test.assertRejects(
+ browser.storage.sync[testDef.method](...testDef.args),
+ "Please set webextensions.storage.sync.enabled to true in about:config",
+ `storage.sync.${testDef.method} is behind a flag`));
+ });
+
+ Promise.all(promises).then(() => browser.test.notifyPass("flag needed"));
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("flag needed");
+ yield extension.unload();
+ win.close();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html
new file mode 100644
index 0000000000..32d8e6af0e
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_multiple_pages() {
+ async function background() {
+ let tabReady = new Promise(resolve => {
+ browser.runtime.onMessage.addListener(function listener(msg) {
+ browser.test.log("onMessage " + msg);
+ if (msg == "tab-ready") {
+ browser.runtime.onMessage.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+
+ let tabId;
+ let tabRemoved = new Promise(resolve => {
+ browser.tabs.onRemoved.addListener(function listener(removedId) {
+ if (removedId == tabId) {
+ browser.tabs.onRemoved.removeListener(listener);
+
+ // Delay long enough to be sure the inner window has been nuked.
+ setTimeout(resolve, 0);
+ }
+ });
+ });
+
+ try {
+ let storage = browser.storage.local;
+
+ browser.test.log("create");
+ let tab = await browser.tabs.create({url: "tab.html"});
+ tabId = tab.id;
+
+ await tabReady;
+
+ let result = await storage.get("key");
+ browser.test.assertEq(undefined, result.key, "Key should be undefined");
+
+ await browser.runtime.sendMessage("tab-set-key");
+
+ result = await storage.get("key");
+ browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}),
+ JSON.stringify(result.key),
+ "Key should be set to the value from the tab");
+
+ browser.test.log("Remove tab");
+
+ await Promise.all([
+ browser.tabs.remove(tabId),
+ tabRemoved,
+ ]);
+
+ result = await storage.get("key");
+ browser.test.assertEq(JSON.stringify({foo: {bar: "baz"}}),
+ JSON.stringify(result.key),
+ "Key should still be set to the value from the tab");
+
+ browser.test.notifyPass("storage-multiple");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("storage-multiple");
+ }
+ }
+
+ function tab() {
+ browser.test.log("tab");
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg == "tab-set-key") {
+ return browser.storage.local.set({key: {foo: {bar: "baz"}}});
+ }
+ });
+
+ browser.runtime.sendMessage("tab-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+
+ files: {
+ "tab.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="tab.js"><\/script>
+ </head>
+ </html>`,
+
+ "tab.js": tab,
+ },
+
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("storage-multiple");
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
new file mode 100644
index 0000000000..1f3a9a3c9f
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
@@ -0,0 +1,202 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_webext_tab_subframe_privileges() {
+ function background() {
+ browser.runtime.onMessage.addListener(async ({msg, success, tabId, error}) => {
+ if (msg == "webext-tab-subframe-privileges") {
+ if (success) {
+ await browser.tabs.remove(tabId);
+
+ browser.test.notifyPass(msg);
+ } else {
+ browser.test.log(`Got an unexpected error: ${error}`);
+
+ let tabs = await browser.tabs.query({active: true});
+ await browser.tabs.remove(tabs[0].id);
+
+ browser.test.notifyFail(msg);
+ }
+ }
+ });
+ browser.tabs.create({url: browser.runtime.getURL("/tab.html")});
+ }
+
+ async function tabSubframeScript() {
+ browser.test.assertTrue(browser.tabs != undefined,
+ "Subframe of a privileged page has access to privileged APIs");
+ if (browser.tabs) {
+ try {
+ let tab = await browser.tabs.getCurrent();
+ browser.runtime.sendMessage({
+ msg: "webext-tab-subframe-privileges",
+ success: true,
+ tabId: tab.id,
+ });
+ } catch (e) {
+ browser.runtime.sendMessage({msg: "webext-tab-subframe-privileges", success: false, error: `${e}`});
+ }
+ } else {
+ browser.runtime.sendMessage({
+ msg: "webext-tab-subframe-privileges",
+ success: false,
+ error: `Privileged APIs missing in WebExtension tab sub-frame`,
+ });
+ }
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "tab.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="tab-subframe.html"></iframe>
+ </body>
+ </html>`,
+ "tab-subframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="tab-subframe.js"><\/script>
+ </head>
+ </html>`,
+ "tab-subframe.js": tabSubframeScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("webext-tab-subframe-privileges");
+ yield extension.unload();
+});
+
+add_task(function* test_webext_background_subframe_privileges() {
+ function backgroundSubframeScript() {
+ browser.test.assertTrue(browser.tabs != undefined,
+ "Subframe of a background page has access to privileged APIs");
+ browser.test.notifyPass("webext-background-subframe-privileges");
+ }
+
+ let extensionData = {
+ manifest: {
+ background: {
+ page: "background.html",
+ },
+ },
+ files: {
+ "background.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="background-subframe.html"></iframe>
+ </body>
+ </html>`,
+ "background-subframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="background-subframe.js"><\/script>
+ </head>
+ </html>`,
+ "background-subframe.js": backgroundSubframeScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("webext-background-subframe-privileges");
+ yield extension.unload();
+});
+
+add_task(function* test_webext_contentscript_iframe_subframe_privileges() {
+ function background() {
+ browser.runtime.onMessage.addListener(({name, hasTabsAPI, hasStorageAPI}) => {
+ if (name == "contentscript-iframe-loaded") {
+ browser.test.assertFalse(hasTabsAPI,
+ "Subframe of a content script privileged iframes has no access to privileged APIs");
+ browser.test.assertTrue(hasStorageAPI,
+ "Subframe of a content script privileged iframes has access to content script APIs");
+
+ browser.test.notifyPass("webext-contentscript-subframe-privileges");
+ }
+ });
+ }
+
+ function subframeScript() {
+ browser.runtime.sendMessage({
+ name: "contentscript-iframe-loaded",
+ hasTabsAPI: browser.tabs != undefined,
+ hasStorageAPI: browser.storage != undefined,
+ });
+ }
+
+ function contentScript() {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", browser.runtime.getURL("/contentscript-iframe.html"));
+ document.body.appendChild(iframe);
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ "permissions": ["storage"],
+ "content_scripts": [{
+ "matches": ["http://example.com/*"],
+ "js": ["contentscript.js"],
+ }],
+ web_accessible_resources: [
+ "contentscript-iframe.html",
+ ],
+ },
+ files: {
+ "contentscript.js": contentScript,
+ "contentscript-iframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <iframe src="contentscript-iframe-subframe.html"></iframe>
+ </body>
+ </html>`,
+ "contentscript-iframe-subframe.html": `<!DOCTYPE>
+ <head>
+ <meta charset="utf-8">
+ <script src="contentscript-iframe-subframe.js"><\/script>
+ </head>
+ </html>`,
+ "contentscript-iframe-subframe.js": subframeScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let win = window.open("http://example.com");
+
+ yield extension.awaitFinish("webext-contentscript-subframe-privileges");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html
new file mode 100644
index 0000000000..dc351e48a1
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for extension tab teardown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+// Test for tabs opened using tabs.create and window.open
+function* runTabReloadAndCloseTest(extension) {
+ let chromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("file_teardown_test.js"));
+ yield chromeScript.promiseOneMessage("chromescript-startup");
+ function* getContextEvents() {
+ chromeScript.sendAsyncMessage("get-context-events");
+ let contextEvents = yield chromeScript.promiseOneMessage("context-events");
+ dump(JSON.stringify(contextEvents));
+ return contextEvents.filter(event => event.extensionId == extension.id);
+ }
+
+ extension.sendMessage("open extension page");
+ let extensionPageUrl = yield extension.awaitMessage("extension page loaded");
+
+ let contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1, "ExtensionContext change for opening a tab");
+ is(contextEvents[0].eventType, "load", "create ExtensionContext for tab");
+ is(contextEvents[0].url, extensionPageUrl,
+ "ExtensionContext URL after tab creation should be tab URL");
+
+ extension.sendMessage("reload extension page");
+ let extensionPageUrl2 = yield extension.awaitMessage("extension page loaded");
+
+ is(extensionPageUrl, extensionPageUrl2,
+ "The tab's URL is expected to not change after a page reload");
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 2, "ExtensionContext change after tab reload");
+ is(contextEvents[0].eventType, "unload", "unload old ExtensionContext");
+ is(contextEvents[0].url, extensionPageUrl,
+ "ExtensionContext URL before reload should be tab URL");
+ is(contextEvents[1].eventType, "load", "create new ExtensionContext for tab");
+ is(contextEvents[1].url, extensionPageUrl2,
+ "ExtensionContext URL after reload should be tab URL");
+
+ extension.sendMessage("close extension page");
+ yield extension.awaitMessage("closed extension page");
+
+ contextEvents = yield* getContextEvents();
+ is(contextEvents.length, 1, "ExtensionContext after closing tab");
+ is(contextEvents[0].eventType, "unload", "unload tab's ExtensionContext");
+ is(contextEvents[0].url, extensionPageUrl2,
+ "ExtensionContext URL at closing tab should be tab URL");
+
+ chromeScript.sendAsyncMessage("cleanup");
+ chromeScript.destroy();
+ yield extension.unload();
+}
+
+add_task(function* test_extension_page_tabs_create_reload_and_close() {
+ function background() {
+ let tabId;
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "open extension page") {
+ chrome.tabs.create({url: "page.html"}, tab => {
+ tabId = tab.id;
+ });
+ } else if (msg === "reload extension page") {
+ chrome.tabs.reload(tabId);
+ } else if (msg === "close extension page") {
+ chrome.tabs.remove(tabId, () => {
+ browser.test.sendMessage("closed extension page");
+ });
+ }
+ });
+ }
+
+ function pageScript() {
+ browser.test.sendMessage("extension page loaded", document.URL);
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`,
+ "page.js": pageScript,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield* runTabReloadAndCloseTest(extension);
+});
+
+add_task(function* test_extension_page_window_open_reload_and_close() {
+ // This tests whether a context that is opened via window.open is properly
+ // disposed when the tab closes.
+ // The background page cannot use window.open (bugzil.la/1282021), so we open
+ // another extension page that manages the window.open-tab for testing.
+ function background() {
+ chrome.tabs.create({url: "window.open.html"});
+ }
+
+ function windowOpenScript() {
+ let win;
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "open extension page") {
+ win = window.open("page.html");
+ } else if (msg === "reload extension page") {
+ win.location.reload();
+ } else if (msg === "close extension page") {
+ browser.tabs.onRemoved.addListener(function listener() {
+ browser.tabs.onRemoved.removeListener(listener);
+ browser.test.sendMessage("closed extension page");
+ });
+ win.close();
+ }
+ });
+ browser.test.sendMessage("setup-intermediate-tab");
+ }
+
+ function pageScript() {
+ browser.test.sendMessage("extension page loaded", document.URL);
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`,
+ "page.js": pageScript,
+ "window.open.html": `<!DOCTYPE html><meta charset="utf-8"><script src="window.open.js"><\/script>`,
+ "window.open.js": windowOpenScript,
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitMessage("setup-intermediate-tab");
+ yield* runTabReloadAndCloseTest(extension);
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_test.html b/toolkit/components/extensions/test/mochitest/test_ext_test.html
new file mode 100644
index 0000000000..fef31e0e21
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_test.html
@@ -0,0 +1,191 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Testing test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+function loadExtensionAndInterceptTest(extensionData) {
+ let results = [];
+ let testResolve;
+ let testDone = new Promise(resolve => { testResolve = resolve; });
+ let handler = {
+ testResult(...result) {
+ result.pop();
+ results.push(result);
+ SimpleTest.info(`Received test result: ${JSON.stringify(result)}`);
+ },
+
+ testMessage(msg, ...args) {
+ results.push(["test-message", msg, ...args]);
+ SimpleTest.info(`Received message: ${msg} ${JSON.stringify(args)}`);
+ if (msg === "This is the last browser.test call") {
+ testResolve();
+ }
+ },
+ };
+ let extension = SpecialPowers.loadExtension(extensionData, handler);
+ SimpleTest.registerCleanupFunction(() => {
+ if (extension.state == "pending" || extension.state == "running") {
+ SimpleTest.ok(false, "Extension left running at test shutdown");
+ return extension.unload();
+ } else if (extension.state == "unloading") {
+ SimpleTest.ok(false, "Extension not fully unloaded at test shutdown");
+ }
+ });
+ extension.awaitResults = () => testDone.then(() => results);
+ return extension;
+}
+
+function testScript() {
+ // Note: The result of these browser.test calls are intercepted by the test.
+ // See verifyTestResults for the expectations of each browser.test call.
+ browser.test.notifyPass("dot notifyPass");
+ browser.test.notifyFail("dot notifyFail");
+ browser.test.log("dot log");
+ browser.test.fail("dot fail");
+ browser.test.succeed("dot succeed");
+ browser.test.assertTrue(true);
+ browser.test.assertFalse(false);
+ browser.test.assertEq("", "");
+
+ let obj = {};
+ let arr = [];
+ let dom = document.createElement("body");
+ browser.test.assertTrue(obj, "Object truthy");
+ browser.test.assertTrue(arr, "Array truthy");
+ browser.test.assertTrue(dom, "Element truthy");
+ browser.test.assertTrue(true, "True truthy");
+ browser.test.assertTrue(false, "False truthy");
+ browser.test.assertTrue(null, "Null truthy");
+ browser.test.assertTrue(undefined, "Void truthy");
+ browser.test.assertTrue(false, document.createElement("html"));
+
+ browser.test.assertFalse(obj, "Object falsey");
+ browser.test.assertFalse(arr, "Array falsey");
+ browser.test.assertFalse(dom, "Element falsey");
+ browser.test.assertFalse(true, "True falsey");
+ browser.test.assertFalse(false, "False falsey");
+ browser.test.assertFalse(null, "Null falsey");
+ browser.test.assertFalse(undefined, "Void falsey");
+ browser.test.assertFalse(true, document.createElement("head"));
+
+ browser.test.assertEq(obj, obj, "Object equality");
+ browser.test.assertEq(arr, arr, "Array equality");
+ browser.test.assertEq(dom, dom, "Element equality");
+ browser.test.assertEq(null, null, "Null equality");
+ browser.test.assertEq(undefined, undefined, "Void equality");
+
+ browser.test.assertEq({}, {}, "Object reference ineqality");
+ browser.test.assertEq([], [], "Array reference ineqality");
+ browser.test.assertEq(dom, document.createElement("body"), "Element ineqality");
+ browser.test.assertEq(null, undefined, "Null and void ineqality");
+ browser.test.assertEq(true, false, document.createElement("div"));
+
+ obj = {
+ toString() {
+ return "Dynamic toString forbidden";
+ },
+ };
+ browser.test.assertEq(obj, obj, "obj with dynamic toString()");
+ browser.test.sendMessage("Ran test at", location.protocol);
+ browser.test.sendMessage("This is the last browser.test call");
+}
+
+function verifyTestResults(results, shortName, expectedProtocol) {
+ let expectations = [
+ ["test-done", true, "dot notifyPass"],
+ ["test-done", false, "dot notifyFail"],
+ ["test-log", true, "dot log"],
+ ["test-result", false, "dot fail"],
+ ["test-result", true, "dot succeed"],
+ ["test-result", true, "undefined"],
+ ["test-result", true, "undefined"],
+ ["test-eq", true, "undefined", "", ""],
+
+ ["test-result", true, "Object truthy"],
+ ["test-result", true, "Array truthy"],
+ ["test-result", true, "Element truthy"],
+ ["test-result", true, "True truthy"],
+ ["test-result", false, "False truthy"],
+ ["test-result", false, "Null truthy"],
+ ["test-result", false, "Void truthy"],
+ ["test-result", false, "[object HTMLHtmlElement]"],
+
+ ["test-result", false, "Object falsey"],
+ ["test-result", false, "Array falsey"],
+ ["test-result", false, "Element falsey"],
+ ["test-result", false, "True falsey"],
+ ["test-result", true, "False falsey"],
+ ["test-result", true, "Null falsey"],
+ ["test-result", true, "Void falsey"],
+ ["test-result", false, "[object HTMLHeadElement]"],
+
+ ["test-eq", true, "Object equality", "[object Object]", "[object Object]"],
+ ["test-eq", true, "Array equality", "", ""],
+ ["test-eq", true, "Element equality", "[object HTMLBodyElement]", "[object HTMLBodyElement]"],
+ ["test-eq", true, "Null equality", "null", "null"],
+ ["test-eq", true, "Void equality", "undefined", "undefined"],
+
+ ["test-eq", false, "Object reference ineqality", "[object Object]", "[object Object] (different)"],
+ ["test-eq", false, "Array reference ineqality", "", " (different)"],
+ ["test-eq", false, "Element ineqality", "[object HTMLBodyElement]", "[object HTMLBodyElement] (different)"],
+ ["test-eq", false, "Null and void ineqality", "null", "undefined"],
+ ["test-eq", false, "[object HTMLDivElement]", "true", "false"],
+
+ ["test-eq", true, "obj with dynamic toString()", "[object Object]", "[object Object]"],
+
+ ["test-message", "Ran test at", expectedProtocol],
+ ["test-message", "This is the last browser.test call"],
+ ];
+
+ expectations.forEach((expectation, i) => {
+ let msg = expectation.slice(2).join(" - ");
+ isDeeply(results[i], expectation, `${shortName} (${msg})`);
+ });
+ is(results[expectations.length], undefined, "No more results");
+}
+
+add_task(function* test_test_in_background() {
+ let extensionData = {
+ background: `(${testScript})()`,
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ yield extension.startup();
+ let results = yield extension.awaitResults();
+ verifyTestResults(results, "background page", "moz-extension:");
+ yield extension.unload();
+});
+
+add_task(function* test_test_in_content_script() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [{
+ matches: ["http://mochi.test/*/file_sample.html"],
+ js: ["contentscript.js"],
+ }],
+ },
+ files: {
+ "contentscript.js": `(${testScript})()`,
+ },
+ };
+
+ let extension = loadExtensionAndInterceptTest(extensionData);
+ yield extension.startup();
+ let win = window.open("file_sample.html");
+ let results = yield extension.awaitResults();
+ win.close();
+ verifyTestResults(results, "content script", "http:");
+ yield extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
new file mode 100644
index 0000000000..5572de2810
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtensions test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+/* globals delayedNotifyPass */ // Available in the background page of the test extensions.
+
+// Background and content script for testSendMessage_*
+function sendMessage_background() {
+ browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
+ browser.test.assertEq("from frame", msg, "Expected message from frame");
+ sendResponse("msg from back"); // Should not throw or anything like that.
+ delayedNotifyPass("Received sendMessage from closing frame");
+ });
+}
+function sendMessage_contentScript(testType) {
+ browser.runtime.sendMessage("from frame", reply => {
+ // The frame has been removed, so we should not get this callback!
+ browser.test.fail(`Unexpected reply: ${reply}`);
+ });
+ if (testType == "frame") {
+ frameElement.remove();
+ } else {
+ window.close();
+ }
+}
+
+// Background and content script for testConnect_*
+function connect_background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("port from frame", port.name);
+
+ let disconnected = false;
+ let hasMessage = false;
+ port.onDisconnect.addListener(() => {
+ browser.test.assertFalse(disconnected, "onDisconnect should fire once");
+ disconnected = true;
+ browser.test.assertTrue(hasMessage, "Expected onMessage before onDisconnect");
+ browser.test.assertEq(null, port.error, "The port is implicitly closed without errors when the other context unloads");
+ delayedNotifyPass("Received onDisconnect from closing frame");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.assertFalse(hasMessage, "onMessage should fire once");
+ hasMessage = true;
+ browser.test.assertFalse(disconnected, "Should get message before disconnect");
+ browser.test.assertEq("from frame", msg, "Expected message from frame");
+ });
+
+ port.postMessage("reply to closing frame");
+ });
+}
+function connect_contentScript(testType) {
+ let isUnloading = false;
+ addEventListener("pagehide", () => { isUnloading = true; }, {once: true});
+
+ let port = browser.runtime.connect({name: "port from frame"});
+ port.onMessage.addListener(msg => {
+ // The background page sends a reply as soon as we call runtime.connect().
+ // It is possible that the reply reaches this frame before the
+ // window.close() request has been processed.
+ if (!isUnloading) {
+ browser.test.log(`Ignorting unexpected reply ("${msg}") because the page is not being unloaded.`);
+ return;
+ }
+
+ // The frame has been removed, so we should not get a reply.
+ browser.test.fail(`Unexpected reply: ${msg}`);
+ });
+ port.postMessage("from frame");
+
+ // Removing the frame or window should disconnect the port.
+ if (testType == "frame") {
+ frameElement.remove();
+ } else {
+ window.close();
+ }
+}
+
+// `testType` is "window" or "frame".
+function createTestExtension(testType, backgroundScript, contentScript) {
+ // Make a roundtrip between the background page and the test runner (which is
+ // in the same process as the content script) to make sure that we record a
+ // failure in case the content script's sendMessage or onMessage handlers are
+ // called even after the frame or window was removed.
+ function delayedNotifyPass(msg) {
+ browser.test.onMessage.addListener((type, echoMsg) => {
+ if (type == "pong") {
+ browser.test.assertEq(msg, echoMsg, "Echoed reply should be the same");
+ browser.test.notifyPass(msg);
+ }
+ });
+ browser.test.log("Starting ping-pong to flush messages...");
+ browser.test.sendMessage("ping", msg);
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `${delayedNotifyPass};(${backgroundScript})();`,
+ manifest: {
+ content_scripts: [{
+ js: ["contentscript.js"],
+ all_frames: testType == "frame",
+ matches: ["http://mochi.test/*/file_sample.html"],
+ }],
+ },
+ files: {
+ "contentscript.js": `(${contentScript})("${testType}");`,
+ },
+ });
+ extension.awaitMessage("ping").then(msg => {
+ extension.sendMessage("pong", msg);
+ });
+ return extension;
+}
+
+add_task(function* testSendMessage_and_remove_frame() {
+ let extension = createTestExtension("frame", sendMessage_background, sendMessage_contentScript);
+ yield extension.startup();
+
+ let frame = document.createElement("iframe");
+ frame.src = "file_sample.html";
+ document.body.appendChild(frame);
+
+ yield extension.awaitFinish("Received sendMessage from closing frame");
+ yield extension.unload();
+});
+
+add_task(function* testConnect_and_remove_frame() {
+ let extension = createTestExtension("frame", connect_background, connect_contentScript);
+ yield extension.startup();
+
+ let frame = document.createElement("iframe");
+ frame.src = "file_sample.html";
+ document.body.appendChild(frame);
+
+ yield extension.awaitFinish("Received onDisconnect from closing frame");
+ yield extension.unload();
+});
+
+add_task(function* testSendMessage_and_remove_window() {
+ let extension = createTestExtension("window", sendMessage_background, sendMessage_contentScript);
+ yield extension.startup();
+
+ window.open("file_sample.html");
+
+ yield extension.awaitFinish("Received sendMessage from closing frame");
+ yield extension.unload();
+});
+
+add_task(function* testConnect_and_remove_window() {
+ let extension = createTestExtension("window", connect_background, connect_contentScript);
+ yield extension.startup();
+
+ window.open("file_sample.html");
+
+ yield extension.awaitFinish("Received onDisconnect from closing frame");
+ yield extension.unload();
+});
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
new file mode 100644
index 0000000000..fa32287394
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -0,0 +1,353 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the web_accessible_resources manifest directive</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("security.mixed_content.block_display_content");
+});
+
+let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+async function testImageLoading(src, expectedAction) {
+ let imageLoadingPromise = new Promise((resolve, reject) => {
+ let cleanupListeners;
+ let testImage = document.createElement("img");
+ testImage.setAttribute("src", src);
+
+ let loadListener = () => {
+ cleanupListeners();
+ resolve(expectedAction === "loaded");
+ };
+
+ let errorListener = () => {
+ cleanupListeners();
+ resolve(expectedAction === "blocked");
+ };
+
+ cleanupListeners = () => {
+ testImage.removeEventListener("load", loadListener);
+ testImage.removeEventListener("error", errorListener);
+ };
+
+ testImage.addEventListener("load", loadListener);
+ testImage.addEventListener("error", errorListener);
+
+ document.body.appendChild(testImage);
+ });
+
+ let success = await imageLoadingPromise;
+ browser.runtime.sendMessage({name: "image-loading", expectedAction, success});
+}
+
+add_task(function* test_web_accessible_resources() {
+ function background() {
+ let gotURL;
+ let tabId;
+
+ function loadFrame(url) {
+ return new Promise(resolve => {
+ browser.tabs.sendMessage(tabId, ["load-iframe", url], reply => {
+ resolve(reply);
+ });
+ });
+ }
+
+ let urls = [
+ [browser.extension.getURL("accessible.html"), true],
+ [browser.extension.getURL("accessible.html") + "?foo=bar", true],
+ [browser.extension.getURL("accessible.html") + "#!foo=bar", true],
+ [browser.extension.getURL("forbidden.html"), false],
+ [browser.extension.getURL("wild1.html"), true],
+ [browser.extension.getURL("wild2.htm"), false],
+ ];
+
+ async function runTests() {
+ for (let [url, shouldLoad] of urls) {
+ let success = await loadFrame(url);
+
+ browser.test.assertEq(shouldLoad, success, "Load was successful");
+ if (shouldLoad) {
+ browser.test.assertEq(url, gotURL, "Got expected url");
+ } else {
+ browser.test.assertEq(undefined, gotURL, "Got no url");
+ }
+ gotURL = undefined;
+ }
+
+ browser.test.notifyPass("web-accessible-resources");
+ }
+
+ browser.runtime.onMessage.addListener(([msg, url], sender) => {
+ if (msg == "content-script-ready") {
+ tabId = sender.tab.id;
+ runTests();
+ } else if (msg == "page-script") {
+ browser.test.assertEq(undefined, gotURL, "Should have gotten only one message");
+ browser.test.assertEq("string", typeof(url), "URL should be a string");
+ gotURL = url;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ function contentScript() {
+ browser.runtime.onMessage.addListener(([msg, url], sender, respond) => {
+ if (msg == "load-iframe") {
+ let iframe = document.createElement("iframe");
+ iframe.setAttribute("src", url);
+ iframe.addEventListener("load", () => { respond(true); });
+ iframe.addEventListener("error", () => { respond(false); });
+ document.body.appendChild(iframe);
+ return true;
+ }
+ });
+ browser.runtime.sendMessage(["content-script-ready"]);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://example.com/"],
+ "js": ["content_script.js"],
+ "run_at": "document_idle",
+ },
+ ],
+
+ "web_accessible_resources": [
+ "/accessible.html",
+ "wild*.html",
+ ],
+ },
+
+ background,
+
+ files: {
+ "content_script.js": contentScript,
+
+ "accessible.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="accessible.js"><\/script>
+ </head></html>`,
+
+ "accessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
+
+ "inaccessible.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="inaccessible.js"><\/script>
+ </head></html>`,
+
+ "inaccessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
+
+ "wild1.html": `<html><head>
+ <meta charset="utf-8">
+ <script src="wild.js"><\/script>
+ </head></html>`,
+
+ "wild2.htm": `<html><head>
+ <meta charset="utf-8">
+ <script src="wild.js"><\/script>
+ </head></html>`,
+
+ "wild.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
+ },
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitMessage("ready");
+
+ let win = window.open("http://example.com/");
+
+ yield extension.awaitFinish("web-accessible-resources");
+
+ win.close();
+
+ yield extension.unload();
+});
+
+add_task(function* test_web_accessible_resources_csp() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg.name === "image-loading") {
+ browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`);
+ browser.test.sendMessage(`image-${msg.expectedAction}`);
+ } else {
+ browser.test.sendMessage(msg);
+ }
+ });
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ function content() {
+ window.addEventListener("message", function rcv(event) {
+ browser.runtime.sendMessage("script-ran");
+ window.removeEventListener("message", rcv, false);
+ }, false);
+
+ testImageLoading(browser.extension.getURL("image.png"), "loaded");
+
+ let testScriptElement = document.createElement("script");
+ testScriptElement.setAttribute("src", browser.extension.getURL("test_script.js"));
+ document.head.appendChild(testScriptElement);
+ browser.runtime.sendMessage("script-loaded");
+ }
+
+ function testScript() {
+ window.postMessage("test-script-loaded", "*");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["http://example.com/*/file_csp.html"],
+ "run_at": "document_start",
+ "js": ["content_script_helper.js", "content_script.js"],
+ }],
+ "web_accessible_resources": [
+ "image.png",
+ "test_script.js",
+ ],
+ },
+ background,
+ files: {
+ "content_script_helper.js": `${testImageLoading}`,
+ "content_script.js": content,
+ "test_script.js": testScript,
+ "image.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+
+ // This is used to watch the blocked data bounce off CSP.
+ function examiner() {
+ SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+ }
+
+ let cspEventCount = 0;
+
+ examiner.prototype = {
+ observe: function(subject, topic, data) {
+ cspEventCount++;
+ let spec = SpecialPowers.wrap(subject).QueryInterface(SpecialPowers.Ci.nsIURI).spec;
+ ok(spec.includes("file_image_bad.png") || spec.includes("file_script_bad.js"),
+ `Expected file: ${spec} rejected by CSP`);
+ },
+
+ // We must eventually call this to remove the listener,
+ // or mochitests might get borked.
+ remove: function() {
+ SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+ },
+ };
+
+ let observer = new examiner();
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
+
+ let win = window.open("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_csp.html");
+
+ yield Promise.all([
+ extension.awaitMessage("image-loaded"),
+ extension.awaitMessage("script-loaded"),
+ extension.awaitMessage("script-ran"),
+ ]);
+ is(cspEventCount, 2, "Two items were rejected by CSP");
+ win.close();
+
+ observer.remove();
+ yield extension.unload();
+});
+
+add_task(function* test_web_accessible_resources_mixed_content() {
+ function background() {
+ browser.runtime.onMessage.addListener(msg => {
+ if (msg.name === "image-loading") {
+ browser.test.assertTrue(msg.success, `Image was ${msg.expectedAction}`);
+ browser.test.sendMessage(`image-${msg.expectedAction}`);
+ } else {
+ browser.test.sendMessage(msg);
+ if (msg === "accessible-script-loaded") {
+ browser.test.notifyPass("mixed-test");
+ }
+ }
+ });
+
+ browser.test.sendMessage("background-ready");
+ }
+
+ function content() {
+ testImageLoading("http://example.com/tests/toolkit/components/extensions/test/mochitest/file_image_bad.png", "blocked");
+ testImageLoading(browser.extension.getURL("image.png"), "loaded");
+
+ let testScriptElement = document.createElement("script");
+ testScriptElement.setAttribute("src", browser.extension.getURL("test_script.js"));
+ document.head.appendChild(testScriptElement);
+
+ window.addEventListener("message", event => {
+ browser.runtime.sendMessage(event.data);
+ });
+ }
+
+ function testScript() {
+ window.postMessage("accessible-script-loaded", "*");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "content_scripts": [{
+ "matches": ["https://example.com/*/file_mixed.html"],
+ "run_at": "document_start",
+ "js": ["content_script_helper.js", "content_script.js"],
+ }],
+ "web_accessible_resources": [
+ "image.png",
+ "test_script.js",
+ ],
+ },
+ background,
+ files: {
+ "content_script_helper.js": `${testImageLoading}`,
+ "content_script.js": content,
+ "test_script.js": testScript,
+ "image.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+
+ SpecialPowers.setBoolPref("security.mixed_content.block_display_content", true);
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
+
+ let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/file_mixed.html");
+
+ yield Promise.all([
+ extension.awaitMessage("image-blocked"),
+ extension.awaitMessage("image-loaded"),
+ extension.awaitMessage("accessible-script-loaded"),
+ ]);
+ yield extension.awaitFinish("mixed-test");
+ win.close();
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
new file mode 100644
index 0000000000..2287fd9b15
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -0,0 +1,559 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* globals sendMouseEvent */
+
+function backgroundScript() {
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ const URL = BASE + "/file_WebNavigation_page1.html";
+
+ const EVENTS = [
+ "onTabReplaced",
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ ];
+
+ let expectedTabId = -1;
+
+ function gotEvent(event, details) {
+ if (!details.url.startsWith(BASE)) {
+ return;
+ }
+ browser.test.log(`Got ${event} ${details.url} ${details.frameId} ${details.parentFrameId}`);
+
+ if (expectedTabId == -1) {
+ browser.test.assertTrue(details.tabId !== undefined, "tab ID defined");
+ expectedTabId = details.tabId;
+ }
+
+ browser.test.assertEq(details.tabId, expectedTabId, "correct tab");
+
+ browser.test.sendMessage("received", {url: details.url, event});
+
+ if (details.url == URL) {
+ browser.test.assertEq(details.frameId, 0, "root frame ID correct");
+ browser.test.assertEq(details.parentFrameId, -1, "root parent frame ID correct");
+ } else {
+ browser.test.assertEq(details.parentFrameId, 0, "parent frame ID correct");
+ browser.test.assertTrue(details.frameId != 0, "frame ID probably okay");
+ }
+
+ browser.test.assertTrue(details.frameId !== undefined);
+ browser.test.assertTrue(details.parentFrameId !== undefined);
+ }
+
+ let listeners = {};
+ for (let event of EVENTS) {
+ listeners[event] = gotEvent.bind(null, event);
+ browser.webNavigation[event].addListener(listeners[event]);
+ }
+
+ browser.test.sendMessage("ready");
+}
+
+const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+const URL = BASE + "/file_WebNavigation_page1.html";
+const FRAME = BASE + "/file_WebNavigation_page2.html";
+const FRAME2 = BASE + "/file_WebNavigation_page3.html";
+const FRAME_PUSHSTATE = BASE + "/file_WebNavigation_page3_pushState.html";
+const REDIRECT = BASE + "/redirection.sjs";
+const REDIRECTED = BASE + "/dummy_page.html";
+const CLIENT_REDIRECT = BASE + "/file_webNavigation_clientRedirect.html";
+const CLIENT_REDIRECT_HTTPHEADER = BASE + "/file_webNavigation_clientRedirect_httpHeaders.html";
+const FRAME_CLIENT_REDIRECT = BASE + "/file_webNavigation_frameClientRedirect.html";
+const FRAME_REDIRECT = BASE + "/file_webNavigation_frameRedirect.html";
+const FRAME_MANUAL = BASE + "/file_webNavigation_manualSubframe.html";
+const FRAME_MANUAL_PAGE1 = BASE + "/file_webNavigation_manualSubframe_page1.html";
+const FRAME_MANUAL_PAGE2 = BASE + "/file_webNavigation_manualSubframe_page2.html";
+const INVALID_PAGE = "https://invalid.localhost/";
+
+const REQUIRED = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+];
+
+var received = [];
+var completedResolve;
+var waitingURL, waitingEvent;
+
+function loadAndWait(win, event, url, script) {
+ received = [];
+ waitingEvent = event;
+ waitingURL = url;
+ dump(`RUN ${script}\n`);
+ script();
+ return new Promise(resolve => { completedResolve = resolve; });
+}
+
+add_task(function* webnav_transitions_props() {
+ function backgroundScriptTransitions() {
+ const EVENTS = [
+ "onCommitted",
+ "onCompleted",
+ ];
+
+ function gotEvent(event, details) {
+ browser.test.log(`Got ${event} ${details.url} ${details.transitionType} ${details.transitionQualifiers && JSON.stringify(details.transitionQualifiers)}`);
+
+ browser.test.sendMessage("received", {url: details.url, details, event});
+ }
+
+ let listeners = {};
+ for (let event of EVENTS) {
+ listeners[event] = gotEvent.bind(null, event);
+ browser.webNavigation[event].addListener(listeners[event]);
+ }
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background: backgroundScriptTransitions,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ extension.onMessage("received", ({url, event, details}) => {
+ received.push({url, event, details});
+
+ if (event == waitingEvent && url == waitingURL) {
+ completedResolve();
+ }
+ });
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
+
+ // transitionType: reload
+ received = [];
+ yield loadAndWait(win, "onCompleted", URL, () => { win.location.reload(); });
+
+ let found = received.find((data) => (data.event == "onCommitted" && data.url == URL));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "reload",
+ "Got the expected 'reload' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "transitionQualifiers found in the OnCommitted events");
+ }
+
+ // transitionType: auto_subframe
+ found = received.find((data) => (data.event == "onCommitted" && data.url == FRAME));
+
+ ok(found, "Got the sub-frame onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "transitionQualifiers found in the OnCommitted events");
+ }
+
+ // transitionType: form_submit
+ received = [];
+ yield loadAndWait(win, "onCompleted", URL, () => {
+ win.document.querySelector("form").submit();
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == URL));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "form_submit",
+ "Got the expected 'form_submit' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers),
+ "transitionQualifiers found in the OnCommitted events");
+ }
+
+ // transitionQualifier: server_redirect
+ received = [];
+ yield loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = REDIRECT; });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "server_redirect"),
+ "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: forward_back
+ received = [];
+ yield loadAndWait(win, "onCompleted", URL, () => { win.history.back(); });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == URL));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "forward_back"),
+ "Got the expected 'forward_back' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: client_redirect
+ // (from meta http-equiv tag)
+ received = [];
+ yield loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = CLIENT_REDIRECT;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "client_redirect"),
+ "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: client_redirect
+ // (from http headers)
+ received = [];
+ yield loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = CLIENT_REDIRECT_HTTPHEADER;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" &&
+ data.url == CLIENT_REDIRECT_HTTPHEADER));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "link",
+ "Got the expected 'link' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "client_redirect"),
+ "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: client_redirect (sub-frame)
+ // (from meta http-equiv tag)
+ received = [];
+ yield loadAndWait(win, "onCompleted", REDIRECTED, () => {
+ win.location = FRAME_CLIENT_REDIRECT;
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECTED));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ ok(Array.isArray(found.details.transitionQualifiers) &&
+ found.details.transitionQualifiers.find((q) => q == "client_redirect"),
+ "Got the expected 'client_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionQualifier: server_redirect (sub-frame)
+ received = [];
+ yield loadAndWait(win, "onCompleted", REDIRECTED, () => { win.location = FRAME_REDIRECT; });
+
+ found = received.find((data) => (data.event == "onCommitted" && data.url == REDIRECT));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ // BUG 1264936: currently the server_redirect is not detected in sub-frames
+ // once we fix it we can test it here:
+ //
+ // ok(Array.isArray(found.details.transitionQualifiers) &&
+ // found.details.transitionQualifiers.find((q) => q == "server_redirect"),
+ // "Got the expected 'server_redirect' transitionQualifiers in the OnCommitted events");
+ }
+
+ // transitionType: manual_subframe
+ received = [];
+ yield loadAndWait(win, "onCompleted", FRAME_MANUAL, () => { win.location = FRAME_MANUAL; });
+ found = received.find((data) => (data.event == "onCommitted" &&
+ data.url == FRAME_MANUAL_PAGE1));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'auto_subframe' transitionType in the OnCommitted event");
+ }
+
+ received = [];
+ yield loadAndWait(win, "onCompleted", FRAME_MANUAL_PAGE2, () => {
+ let el = win.document.querySelector("iframe")
+ .contentDocument.querySelector("a");
+ sendMouseEvent({type: "click"}, el, win);
+ });
+
+ found = received.find((data) => (data.event == "onCommitted" &&
+ data.url == FRAME_MANUAL_PAGE2));
+
+ ok(found, "Got the onCommitted event");
+
+ if (found) {
+ is(found.details.transitionType, "manual_subframe",
+ "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ }
+
+ // cleanup phase
+ win.close();
+
+ yield extension.unload();
+ info("webnavigation extension unloaded");
+});
+
+add_task(function* webnav_ordering() {
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background: backgroundScript,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ extension.onMessage("received", ({url, event}) => {
+ received.push({url, event});
+
+ if (event == waitingEvent && url == waitingURL) {
+ completedResolve();
+ }
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ yield loadAndWait(win, "onCompleted", URL, () => { win.location = URL; });
+
+ function checkRequired(url) {
+ for (let event of REQUIRED) {
+ let found = false;
+ for (let r of received) {
+ if (r.url == url && r.event == event) {
+ found = true;
+ }
+ }
+ ok(found, `Received event ${event} from ${url}`);
+ }
+ }
+
+ checkRequired(URL);
+ checkRequired(FRAME);
+
+ function checkBefore(action1, action2) {
+ function find(action) {
+ for (let i = 0; i < received.length; i++) {
+ if (received[i].url == action.url && received[i].event == action.event) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ let index1 = find(action1);
+ let index2 = find(action2);
+ ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`);
+ ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`);
+ ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`);
+ }
+
+ // As required in the webNavigation API documentation:
+ // If a navigating frame contains subframes, its onCommitted is fired before any
+ // of its children's onBeforeNavigate; while onCompleted is fired after
+ // all of its children's onCompleted.
+ checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"});
+ checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"});
+
+ // As required in the webNAvigation API documentation, check the event sequence:
+ // onBeforeNavigate -> onCommitted -> onDOMContentLoaded -> onCompleted
+ let expectedEventSequence = [
+ "onBeforeNavigate", "onCommitted", "onDOMContentLoaded", "onCompleted",
+ ];
+
+ for (let i = 1; i < expectedEventSequence.length; i++) {
+ let after = expectedEventSequence[i];
+ let before = expectedEventSequence[i - 1];
+ checkBefore({url: URL, event: before}, {url: URL, event: after});
+ checkBefore({url: FRAME, event: before}, {url: FRAME, event: after});
+ }
+
+ yield loadAndWait(win, "onCompleted", FRAME2, () => { win.frames[0].location = FRAME2; });
+
+ checkRequired(FRAME2);
+
+ let navigationSequence = [
+ {
+ action: () => { win.frames[0].document.getElementById("elt").click(); },
+ waitURL: `${FRAME2}#ref`,
+ expectedEvent: "onReferenceFragmentUpdated",
+ description: "clicked an anchor link",
+ },
+ {
+ action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+ waitURL: `${FRAME2}#ref2`,
+ expectedEvent: "onReferenceFragmentUpdated",
+ description: "history.pushState, same pathname, different hash",
+ },
+ {
+ action: () => { win.frames[0].history.pushState({}, "History PushState", `${FRAME2}#ref2`); },
+ waitURL: `${FRAME2}#ref2`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, same hash",
+ },
+ {
+ action: () => {
+ win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param1=value#ref2`);
+ },
+ waitURL: `${FRAME2}?query_param1=value#ref2`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, same hash, different query params",
+ },
+ {
+ action: () => {
+ win.frames[0].history.pushState({}, "History PushState", `${FRAME2}?query_param2=value#ref3`);
+ },
+ waitURL: `${FRAME2}?query_param2=value#ref3`,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, same pathname, different hash, different query params",
+ },
+ {
+ action: () => { win.frames[0].history.pushState(null, "History PushState", FRAME_PUSHSTATE); },
+ waitURL: FRAME_PUSHSTATE,
+ expectedEvent: "onHistoryStateUpdated",
+ description: "history.pushState, different pathname",
+ },
+ ];
+
+ for (let navigation of navigationSequence) {
+ let {expectedEvent, waitURL, action, description} = navigation;
+ info(`Waiting ${expectedEvent} from ${waitURL} - ${description}`);
+ yield loadAndWait(win, expectedEvent, waitURL, action);
+ info(`Received ${expectedEvent} from ${waitURL} - ${description}`);
+ }
+
+ for (let i = navigationSequence.length - 1; i > 0; i--) {
+ let {waitURL: fromURL, expectedEvent} = navigationSequence[i];
+ let {waitURL} = navigationSequence[i - 1];
+ info(`Waiting ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+ yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.back(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.back() from ${fromURL} to ${waitURL}`);
+ }
+
+ for (let i = 0; i < navigationSequence.length - 1; i++) {
+ let {waitURL: fromURL} = navigationSequence[i];
+ let {waitURL, expectedEvent} = navigationSequence[i + 1];
+ info(`Waiting ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ yield loadAndWait(win, expectedEvent, waitURL, () => { win.frames[0].history.forward(); });
+ info(`Received ${expectedEvent} from ${waitURL} - history.forward() from ${fromURL} to ${waitURL}`);
+ }
+
+ win.close();
+
+ yield extension.unload();
+ info("webnavigation extension unloaded");
+});
+
+add_task(function* webnav_error_event() {
+ function backgroundScriptErrorEvent() {
+ browser.webNavigation.onErrorOccurred.addListener((details) => {
+ browser.test.log(`Got onErrorOccurred ${details.url} ${details.error}`);
+
+ browser.test.sendMessage("received", {url: details.url, details, event: "onErrorOccurred"});
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background: backgroundScriptErrorEvent,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ extension.onMessage("received", ({url, event, details}) => {
+ received.push({url, event, details});
+
+ if (event == waitingEvent && url == waitingURL) {
+ completedResolve();
+ }
+ });
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+ info("webnavigation extension loaded");
+
+ let win = window.open();
+
+ received = [];
+ yield loadAndWait(win, "onErrorOccurred", INVALID_PAGE, () => { win.location = INVALID_PAGE; });
+
+ let found = received.find((data) => (data.event == "onErrorOccurred" &&
+ data.url == INVALID_PAGE));
+
+ ok(found, "Got the onErrorOccurred event");
+
+ if (found) {
+ ok(found.details.error.match(/Error code [0-9]+/),
+ "Got the expected error string in the onErrorOccurred event");
+ }
+
+ // cleanup phase
+ win.close();
+
+ yield extension.unload();
+ info("webnavigation extension unloaded");
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html
new file mode 100644
index 0000000000..a0de5e9e5d
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html
@@ -0,0 +1,308 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_webnav_unresolved_uri_on_expected_URI_scheme() {
+ function background() {
+ let lastTest;
+
+ function cleanupTestListeners() {
+ if (lastTest) {
+ let {event, okListener, failListener} = lastTest;
+ lastTest = null;
+ browser.test.log(`Cleanup previous test event listeners`);
+ browser.webNavigation[event].removeListener(okListener);
+ browser.webNavigation[event].removeListener(failListener);
+ }
+ }
+
+ function createTestListener(event, fail, urlFilter) {
+ function listener(details) {
+ let log = JSON.stringify({url: details.url, urlFilter});
+ if (fail) {
+ browser.test.fail(`Got an unexpected ${event} on the failure listener: ${log}`);
+ } else {
+ browser.test.succeed(`Got the expected ${event} on the success listener: ${log}`);
+ }
+
+ cleanupTestListeners();
+ browser.test.sendMessage("test-filter-next");
+ }
+
+ browser.webNavigation[event].addListener(listener, urlFilter);
+
+ return listener;
+ }
+
+ browser.test.onMessage.addListener((msg, event, okFilter, failFilter) => {
+ if (msg !== "test-filter") {
+ return;
+ }
+
+ lastTest = {
+ event,
+ // Register the failListener first, which should not be called
+ // and if it is called the test scenario is marked as a failure.
+ failListener: createTestListener(event, true, failFilter),
+ okListener: createTestListener(event, false, okFilter),
+ };
+
+ browser.test.sendMessage("test-filter-ready");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ yield extension.awaitMessage("ready");
+
+ let win = window.open();
+
+ let testFilterScenarios = [
+ {
+ url: "http://example.net/browser",
+ filters: [
+ // schemes
+ {
+ okFilter: [{schemes: ["http"]}],
+ failFilter: [{schemes: ["https"]}],
+ },
+ // ports
+ {
+ okFilter: [{ports: [80, 22, 443]}],
+ failFilter: [{ports: [81, 82, 83]}],
+ },
+ {
+ okFilter: [{ports: [22, 443, [10, 80]]}],
+ failFilter: [{ports: [22, 23, [81, 100]]}],
+ },
+ ],
+ },
+ {
+ url: "http://example.net/browser?param=1#ref",
+ filters: [
+ // host: Equals, Contains, Prefix, Suffix
+ {
+ okFilter: [{hostEquals: "example.net"}],
+ failFilter: [{hostEquals: "example.com"}],
+ },
+ {
+ okFilter: [{hostContains: ".example"}],
+ failFilter: [{hostContains: ".www"}],
+ },
+ {
+ okFilter: [{hostPrefix: "example"}],
+ failFilter: [{hostPrefix: "www"}],
+ },
+ {
+ okFilter: [{hostSuffix: "net"}],
+ failFilter: [{hostSuffix: "com"}],
+ },
+ // path: Equals, Contains, Prefix, Suffix
+ {
+ okFilter: [{pathEquals: "/browser"}],
+ failFilter: [{pathEquals: "/"}],
+ },
+ {
+ okFilter: [{pathContains: "brow"}],
+ failFilter: [{pathContains: "tool"}],
+ },
+ {
+ okFilter: [{pathPrefix: "/bro"}],
+ failFilter: [{pathPrefix: "/tool"}],
+ },
+ {
+ okFilter: [{pathSuffix: "wser"}],
+ failFilter: [{pathSuffix: "kit"}],
+ },
+ // query: Equals, Contains, Prefix, Suffix
+ {
+ okFilter: [{queryEquals: "param=1"}],
+ failFilter: [{queryEquals: "wrongparam=2"}],
+ },
+ {
+ okFilter: [{queryContains: "param"}],
+ failFilter: [{queryContains: "wrongparam"}],
+ },
+ {
+ okFilter: [{queryPrefix: "param="}],
+ failFilter: [{queryPrefix: "wrong"}],
+ },
+ {
+ okFilter: [{querySuffix: "=1"}],
+ failFilter: [{querySuffix: "=2"}],
+ },
+ // urlMatches, originAndPathMatches
+ {
+ okFilter: [{urlMatches: "example.net/.*\?param=1"}],
+ failFilter: [{urlMatches: "example.net/.*\?wrongparam=2"}],
+ },
+ {
+ okFilter: [{originAndPathMatches: "example.net\/browser"}],
+ failFilter: [{originAndPathMatches: "example.net/.*\?param=1"}],
+ },
+ ],
+ },
+ {
+ url: "http://example.net/browser",
+ filters: [
+ // multiple criteria in a single filter:
+ // if one of the critera is not verified, the event should not be received.
+ {
+ okFilter: [{schemes: ["http"], ports: [80, 22, 443]}],
+ failFilter: [{schemes: ["http"], ports: [81, 82, 83]}],
+ },
+ // multiple urlFilters on the same listener
+ // if at least one of the critera is verified, the event should be received.
+ {
+ okFilter: [{schemes: ["https"]}, {ports: [80, 22, 443]}],
+ failFilter: [{schemes: ["https"]}, {ports: [81, 82, 83]}],
+ },
+ ],
+ },
+ ];
+
+ function* runTestScenario(event, {url, filters}) {
+ for (let testFilters of filters) {
+ let {okFilter, failFilter} = testFilters;
+
+ info(`Prepare the new test scenario: ${event} ${url} ${JSON.stringify(testFilters)}`);
+ win.location = "about:blank";
+
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+ yield extension.awaitMessage("test-filter-ready");
+
+ info(`Loading the test url: ${url}`);
+ win.location = url;
+
+ yield extension.awaitMessage("test-filter-next");
+
+ info("Test scenario completed. Moving to the next test scenario.");
+ }
+ }
+
+ const BASE_WEBNAV_EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+
+ info("WebNavigation event filters test scenarios starting...");
+
+ for (let filterScenario of testFilterScenarios) {
+ for (let event of BASE_WEBNAV_EVENTS) {
+ yield runTestScenario(event, filterScenario);
+ }
+ }
+
+ info("WebNavigation event filters test onReferenceFragmentUpdated scenario starting...");
+
+ const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
+ let url = BASE + "/file_WebNavigation_page3.html";
+
+ let okFilter = [{urlContains: "_page3.html"}];
+ let failFilter = [{ports: [444]}];
+ let event = "onCompleted";
+
+ info(`Loading the initial test url: ${url}`);
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+
+ yield extension.awaitMessage("test-filter-ready");
+ win.location = url;
+ yield extension.awaitMessage("test-filter-next");
+
+ event = "onReferenceFragmentUpdated";
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+
+ yield extension.awaitMessage("test-filter-ready");
+ win.location = url + "#ref1";
+ yield extension.awaitMessage("test-filter-next");
+
+ info("WebNavigation event filters test onHistoryStateUpdated scenario starting...");
+
+ event = "onHistoryStateUpdated";
+ extension.sendMessage("test-filter", event, {url: okFilter}, {url: failFilter});
+ yield extension.awaitMessage("test-filter-ready");
+
+ win.history.pushState({}, "", BASE + "/pushState_page3.html");
+ yield extension.awaitMessage("test-filter-next");
+
+ // TODO: add additional specific tests for the other webNavigation events:
+ // onErrorOccurred (and onCreatedNavigationTarget on supported)
+
+ info("WebNavigation event filters test scenarios completed.");
+
+ yield extension.unload();
+
+ win.close();
+});
+
+add_task(function* test_webnav_empty_filter_validation_error() {
+ function background() {
+ let catchedException;
+
+ try {
+ browser.webNavigation.onCompleted.addListener(
+ // Empty callback (not really used)
+ () => {},
+ // Empty filter (which should raise a validation error exception).
+ {url: []}
+ );
+ } catch (e) {
+ catchedException = e;
+ browser.test.log(`Got an exception`);
+ }
+
+ if (catchedException &&
+ catchedException.message.includes("Type error for parameter filters") &&
+ catchedException.message.includes("Array requires at least 1 items; you have 0")) {
+ browser.test.notifyPass("webNav.emptyFilterValidationError");
+ } else {
+ browser.test.notifyFail("webNav.emptyFilterValidationError");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webNavigation",
+ ],
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("webNav.emptyFilterValidationError");
+
+ yield extension.unload();
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html
new file mode 100644
index 0000000000..78efeab350
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_background_events.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_webRequest_serviceworker_events() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.openWindow.enabled", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ let eventNames = new Set([
+ "onBeforeRequest",
+ "onBeforeSendHeaders",
+ "onSendHeaders",
+ "onHeadersReceived",
+ "onResponseStarted",
+ "onCompleted",
+ ]);
+
+ function listener(name, details) {
+ browser.test.assertTrue(eventNames.has(name), `recieved ${name}`);
+ eventNames.delete(name);
+ if (eventNames.size == 0) {
+ browser.test.sendMessage("done");
+ }
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+ },
+ });
+
+ yield extension.startup();
+ let registration = yield navigator.serviceWorker.register("webrequest_worker.js", {scope: "."});
+ yield extension.awaitMessage("done");
+ yield registration.unregister();
+ yield extension.unload();
+});
+
+add_task(function* test_webRequest_background_events() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "<all_urls>",
+ ],
+ },
+ background() {
+ let eventNames = new Set([
+ "onBeforeRequest",
+ "onBeforeSendHeaders",
+ "onSendHeaders",
+ "onHeadersReceived",
+ "onResponseStarted",
+ "onCompleted",
+ ]);
+
+ function listener(name, details) {
+ browser.test.assertTrue(eventNames.has(name), `recieved ${name}`);
+ eventNames.delete(name);
+
+ if (eventNames.size === 0) {
+ browser.test.assertEq(0, eventNames.size, "messages recieved");
+ browser.test.sendMessage("done");
+ }
+ }
+
+ for (let name of eventNames) {
+ browser.webRequest[name].addListener(
+ listener.bind(null, name),
+ {urls: ["https://example.com/*"]}
+ );
+ }
+
+ fetch("https://example.com/example.txt").then(() => {
+ browser.test.pass("Fetch succeeded.");
+ }, () => {
+ browser.test.fail("fetch recieved");
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
new file mode 100644
index 0000000000..ef77fee3be
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_basic.html
@@ -0,0 +1,327 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <script type="text/javascript" src="head_webrequest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+let extension;
+add_task(function* setup() {
+ // SelfSupport has a tendency to fire when running this test alone, without
+ // a good way to turn it off we just set the url to ""
+ yield SpecialPowers.pushPrefEnv({
+ set: [["browser.selfsupport.url", ""]],
+ });
+
+ // Clear the image cache, since it gets in the way otherwise.
+ let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
+ let cache = imgTools.getImgCacheForDocument(document);
+ cache.clearCache(false);
+
+ extension = makeExtension();
+ yield extension.startup();
+});
+
+// expect is a set of test values used by the background script.
+//
+// type: type of request action
+// events: optional, If defined only the events listed are expected for the
+// request. If undefined, all events except onErrorOccurred
+// and onBeforeRedirect are expected. Must be in order received.
+// redirect: url to redirect to during onBeforeSendHeaders
+// status: number expected status during onHeadersReceived, 200 default
+// cancel: event in which we return cancel=true. cancelled message is sent.
+// cached: expected fromCache value, default is false, checked in onCompletion
+// headers: request or response headers to modify
+
+add_task(function* test_webRequest_links() {
+ let expect = {
+ "file_style_bad.css": {
+ type: "stylesheet",
+ events: ["onBeforeRequest", "onErrorOccurred"],
+ cancel: "onBeforeRequest",
+ },
+ "file_style_redirect.css": {
+ type: "stylesheet",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+ optional_events: ["onHeadersReceived"],
+ redirect: "file_style_good.css",
+ },
+ "file_style_good.css": {
+ type: "stylesheet",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addStylesheet("file_style_bad.css");
+ yield extension.awaitMessage("cancelled");
+ // we redirect to style_good which completes the test
+ addStylesheet("file_style_redirect.css");
+ yield extension.awaitMessage("done");
+
+ let style = window.getComputedStyle(document.getElementById("test"), null);
+ is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded");
+});
+
+add_task(function* test_webRequest_images() {
+ let expect = {
+ "file_image_bad.png": {
+ type: "image",
+ events: ["onBeforeRequest", "onErrorOccurred"],
+ cancel: "onBeforeRequest",
+ },
+ "file_image_redirect.png": {
+ type: "image",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+ optional_events: ["onHeadersReceived"],
+ redirect: "file_image_good.png",
+ },
+ "file_image_good.png": {
+ type: "image",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addImage("file_image_bad.png");
+ yield extension.awaitMessage("cancelled");
+ // we redirect to image_good which completes the test
+ addImage("file_image_redirect.png");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_scripts() {
+ let expect = {
+ "file_script_bad.js": {
+ type: "script",
+ events: ["onBeforeRequest", "onErrorOccurred"],
+ cancel: "onBeforeRequest",
+ },
+ "file_script_redirect.js": {
+ type: "script",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onBeforeRedirect"],
+ optional_events: ["onHeadersReceived"],
+ redirect: "file_script_good.js",
+ },
+ "file_script_good.js": {
+ type: "script",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addScript("file_script_bad.js");
+ yield extension.awaitMessage("cancelled");
+ // we redirect to script_good which completes the test
+ addScript("file_script_redirect.js");
+ yield extension.awaitMessage("done");
+
+ is(window.success, 1, "Good script ran");
+ is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(function* test_webRequest_xhr_get() {
+ let expect = {
+ "file_script_xhr.js": {
+ type: "script",
+ },
+ "xhr_resource": {
+ status: 404,
+ type: "xmlhttprequest",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addScript("file_script_xhr.js");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_nonexistent() {
+ let expect = {
+ "nonexistent_script_url.js": {
+ status: 404,
+ type: "script",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addScript("nonexistent_script_url.js");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_checkCached() {
+ let expect = {
+ "file_image_good.png": {
+ type: "image",
+ cached: true,
+ },
+ "file_script_good.js": {
+ type: "script",
+ cached: true,
+ },
+ "file_style_good.css": {
+ type: "stylesheet",
+ cached: true,
+ },
+ "nonexistent_script_url.js": {
+ status: 404,
+ type: "script",
+ cached: false,
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addImage("file_image_good.png");
+ addScript("file_script_good.js");
+ addStylesheet("file_style_good.css");
+ addScript("nonexistent_script_url.js");
+ yield extension.awaitMessage("done");
+
+ is(window.success, 2, "Good script ran");
+ is(window.failure, undefined, "Failure script didn't run");
+});
+
+add_task(function* test_webRequest_headers() {
+ let expect = {
+ "file_script_nonexistent.js": {
+ type: "script",
+ status: 404,
+ headers: {
+ request: {
+ add: {
+ "X-WebRequest-request": "text",
+ "X-WebRequest-request-binary": "binary",
+ },
+ modify: {
+ "user-agent": "WebRequest",
+ },
+ remove: [
+ "referer",
+ ],
+ },
+ response: {
+ add: {
+ "X-WebRequest-response": "text",
+ "X-WebRequest-response-binary": "binary",
+ },
+ modify: {
+ "server": "WebRequest",
+ "content-type": "text/html; charset=utf-8",
+ },
+ remove: [
+ "connection",
+ ],
+ },
+ },
+ completion: "onCompleted",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addScript("file_script_nonexistent.js");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_tabId() {
+ let expect = {
+ "file_WebRequest_page3.html": {
+ type: "main_frame",
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ let a = addLink("file_WebRequest_page3.html?trigger=a");
+ a.click();
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* test_webRequest_tabId_browser() {
+ async function background(url) {
+ let tabId;
+ browser.test.onMessage.addListener(async (msg, expected) => {
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("done");
+ });
+
+ let tab = await browser.tabs.create({url});
+ tabId = tab.id;
+ }
+
+ let tabExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "tabs",
+ ],
+ },
+ background: `(${background})('${SimpleTest.getTestFileURL("file_sample.html")}?nocache=${Math.random()}')`,
+ });
+
+ let expect = {
+ "file_sample.html": {
+ type: "main_frame",
+ },
+ };
+ // expecting origin == undefined
+ extension.sendMessage("set-expected", {expect});
+ yield extension.awaitMessage("continue");
+
+ // open a tab from a system principal
+ yield tabExt.startup();
+
+ yield extension.awaitMessage("done");
+ tabExt.sendMessage("done");
+ yield tabExt.awaitMessage("done");
+ yield tabExt.unload();
+});
+
+add_task(function* test_webRequest_frames() {
+ let expect = {
+ "text/plain,webRequestTest": {
+ type: "sub_frame",
+ events: ["onBeforeRequest", "onCompleted"],
+ },
+ "text/plain,webRequestTest_bad": {
+ type: "sub_frame",
+ events: ["onBeforeRequest", "onCompleted"],
+ cancel: "onBeforeRequest",
+ },
+ "redirection.sjs": {
+ status: 302,
+ type: "sub_frame",
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived", "onBeforeRedirect"],
+ },
+ "dummy_page.html": {
+ type: "sub_frame",
+ status: 404,
+ },
+ "badrobot": {
+ type: "sub_frame",
+ status: 404,
+ events: ["onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders", "onErrorOccurred"],
+ },
+ };
+ extension.sendMessage("set-expected", {expect, origin: location.href});
+ yield extension.awaitMessage("continue");
+ addFrame("data:text/plain,webRequestTest");
+ addFrame("data:text/plain,webRequestTest_bad");
+ yield extension.awaitMessage("cancelled");
+ addFrame("redirection.sjs");
+ addFrame("https://invalid.localhost/badrobot");
+ yield extension.awaitMessage("done");
+});
+
+add_task(function* teardown() {
+ yield extension.unload();
+});
+</script>
+</head>
+<body>
+<div id="test">Sample text</div>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html
new file mode 100644
index 0000000000..c8423ec7cd
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_suspend.html
@@ -0,0 +1,216 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for simple WebExtension</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_suspend() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ ],
+ },
+
+ background() {
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ // Make sure that returning undefined or a promise that resolves to
+ // undefined does not break later handlers.
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking", "requestHeaders"]);
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ return Promise.resolve();
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking", "requestHeaders"]);
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ let requestHeaders = details.requestHeaders.concat({name: "Foo", value: "Bar"});
+
+ return new Promise(resolve => {
+ setTimeout(resolve, 500);
+ }).then(() => {
+ return {requestHeaders};
+ });
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking", "requestHeaders"]);
+ },
+ });
+
+ yield extension.startup();
+
+ let result = yield fetch(SimpleTest.getTestFileURL("return_headers.sjs"));
+
+ let headers = JSON.parse(yield result.text());
+
+ is(headers.foo, "Bar", "Request header was correctly set on suspended request");
+
+ yield extension.unload();
+});
+
+
+// Test that requests that were canceled while suspended for a blocking
+// listener are correctly resumed.
+add_task(function* test_error_resume() {
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let observer = channel => {
+ if (channel instanceof Ci.nsIHttpChannel && channel.URI.spec === "http://example.com/") {
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+
+ // Wait until the next tick to make sure this runs after WebRequest observers.
+ Promise.resolve().then(() => {
+ channel.cancel(Cr.NS_BINDING_ABORTED);
+ });
+ }
+ };
+
+ Services.obs.addObserver(observer, "http-on-modify-request", false);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ ],
+ },
+
+ background() {
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ details => {
+ browser.test.log(`onBeforeSendHeaders({url: ${details.url}})`);
+
+ if (details.url === "http://example.com/") {
+ browser.test.sendMessage("got-before-send-headers");
+ }
+ },
+ {urls: ["<all_urls>"]},
+ ["blocking"]);
+
+ browser.webRequest.onErrorOccurred.addListener(
+ details => {
+ browser.test.log(`onErrorOccurred({url: ${details.url}})`);
+
+ if (details.url === "http://example.com/") {
+ browser.test.sendMessage("got-error-occurred");
+ }
+ },
+ {urls: ["<all_urls>"]});
+ },
+ });
+
+ yield extension.startup();
+
+ try {
+ yield fetch("http://example.com/");
+ ok(false, "Fetch should have failed.");
+ } catch (e) {
+ ok(true, "Got expected error.");
+ }
+
+ yield extension.awaitMessage("got-before-send-headers");
+ yield extension.awaitMessage("got-error-occurred");
+
+ yield extension.unload();
+ chromeScript.destroy();
+});
+
+
+// Test that response header modifications take effect before onStartRequest fires.
+add_task(function* test_set_responseHeaders() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "http://example.com/",
+ ],
+ },
+
+ background() {
+ browser.webRequest.onHeadersReceived.addListener(
+ details => {
+ browser.test.log(`onHeadersReceived({url: ${details.url}})`);
+
+ details.responseHeaders.push({name: "foo", value: "bar"});
+
+ return {responseHeaders: details.responseHeaders};
+ },
+ {urls: ["http://example.com/?modify_headers"]},
+ ["blocking", "responseHeaders"]);
+ },
+ });
+
+ yield extension.startup();
+
+ yield new Promise(resolve => setTimeout(resolve, 0));
+
+ let chromeScript = SpecialPowers.loadChromeScript(() => {
+ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+ Cu.import("resource://gre/modules/NetUtil.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ let ssm = Services.scriptSecurityManager;
+
+ let channel = NetUtil.newChannel({
+ uri: "http://example.com/?modify_headers",
+ loadingPrincipal: ssm.createCodebasePrincipalFromOrigin("http://example.com"),
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ });
+
+ channel.asyncOpen2({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]),
+
+ onStartRequest(request, context) {
+ request.QueryInterface(Ci.nsIHttpChannel);
+
+ try {
+ sendAsyncMessage("response-header-foo", request.getResponseHeader("foo"));
+ } catch (e) {
+ sendAsyncMessage("response-header-foo", null);
+ }
+ request.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest() {
+ },
+
+ onDataAvailable() {
+ throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
+ },
+ });
+ });
+
+ let headerValue = yield chromeScript.promiseOneMessage("response-header-foo");
+ is(headerValue, "bar", "Expected Foo header value");
+
+ yield extension.unload();
+ chromeScript.destroy();
+});
+
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
new file mode 100644
index 0000000000..998ab98003
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_upload.html
@@ -0,0 +1,199 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ enctype="multipart/form-data"
+ >
+<input type="text" name="&quot;special&quot; chrs" value="spcial">
+<input type="file" name="testFile">
+<input type="file" name="emptyFile">
+<input type="text" name="textInput1" value="value1">
+</form>
+
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ enctype="multipart/form-data"
+ >
+<input type="text" name="textInput2" value="value2">
+<input type="file" name="testFile">
+<input type="file" name="emptyFile">
+</form>
+
+</form>
+<form method="post"
+ action="file_WebRequest_page3.html?trigger=form"
+ target="_blank"
+ >
+<input type="text" name="textInput" value="value1">
+<input type="text" name="textInput" value="value2">
+</form>
+<script>
+"use strict";
+
+let files, testFile, blob, file, uploads;
+add_task(function* test_setup() {
+ files = yield new Promise(resolve => {
+ SpecialPowers.createFiles([{name: "testFile.pdf", data: "Not really a PDF file :)", "type": "application/x-pdf"}], (result) => {
+ resolve(result);
+ });
+ });
+ testFile = files[0];
+ blob = {
+ name: "blobAsFile",
+ content: new Blob(["A blob sent as a file"], {type: "text/csv"}),
+ fileName: "blobAsFile.csv",
+ };
+ file = {
+ name: "testFile",
+ fileName: testFile.name,
+ };
+ uploads = {
+ [blob.name]: blob,
+ [file.name]: file,
+ };
+});
+
+function background() {
+ const FILTERS = {urls: ["<all_urls>"]};
+
+ let requestBodySupported = true;
+
+ function onUpload(details) {
+ let url = new URL(details.url);
+ let upload = url.searchParams.get("upload");
+ if (!upload || !requestBodySupported) {
+ return;
+ }
+ let requestBody = details.requestBody;
+ browser.test.log(`onBeforeRequest upload: ${details.url} ${JSON.stringify(details.requestBody)}`);
+ browser.test.assertTrue(!!requestBody, `Intercepted upload ${details.url} #${details.requestId} ${upload} have a requestBody`);
+ if (!requestBody) {
+ return;
+ }
+ let byteLength = parseInt(upload, 10);
+ if (byteLength) {
+ browser.test.assertTrue(!!requestBody.raw, `Binary upload ${details.url} #${details.requestId} ${upload} have a raw attribute`);
+ browser.test.assertEq(byteLength, requestBody.raw && requestBody.raw.map(r => r.bytes && r.bytes.byteLength || 0).reduce((a, b) => a + b), `Binary upload size matches`);
+ return;
+ }
+ if ("raw" in requestBody) {
+ browser.test.assertEq(upload, JSON.stringify(requestBody.raw).replace(/(\bfile: ")[^"]+/, "$1<file>"), `Upload ${details.url} #${details.requestId} matches raw data`);
+ } else {
+ browser.test.assertEq(upload, JSON.stringify(requestBody.formData), `Upload ${details.url} #${details.requestId} matches form data.`);
+ }
+ }
+
+ browser.webRequest.onCompleted.addListener(
+ details => {
+ browser.test.log(`onCompleted ${details.requestId} ${details.url}`);
+
+ browser.test.sendMessage("done");
+ },
+ FILTERS);
+
+ let onBeforeRequest = details => {
+ browser.test.log(`${name} ${details.requestId} ${details.url}`);
+
+ onUpload(details);
+ };
+
+ try {
+ browser.webRequest.onBeforeRequest.addListener(
+ onBeforeRequest, FILTERS, ["requestBody"]);
+ } catch (e) {
+ browser.test.assertTrue(/\brequestBody\b/.test(e.message),
+ "Request body is unsupported");
+
+ // requestBody is disabled in release builds
+ if (!/\brequestBody\b/.test(e.message)) {
+ throw e;
+ }
+
+ browser.webRequest.onBeforeRequest.addListener(
+ onBeforeRequest, FILTERS);
+ }
+}
+
+add_task(function* test_xhr_forms() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "<all_urls>",
+ ],
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ for (let form of document.forms) {
+ if (file.name in form.elements) {
+ SpecialPowers.wrap(form.elements[file.name]).mozSetFileArray(files);
+ }
+ let action = new URL(form.action);
+ let formData = new FormData(form);
+ let webRequestFD = {};
+
+ let updateActionURL = () => {
+ for (let name of formData.keys()) {
+ webRequestFD[name] = name in uploads ? [uploads[name].fileName] : formData.getAll(name);
+ }
+ action.searchParams.set("upload", JSON.stringify(webRequestFD));
+ action.searchParams.set("enctype", form.enctype);
+ };
+
+ updateActionURL();
+
+ form.action = action;
+ form.submit();
+ yield extension.awaitMessage("done");
+
+ if (form.enctype !== "multipart/form-data") {
+ continue;
+ }
+
+ let post = (data) => {
+ let xhr = new XMLHttpRequest();
+ action.searchParams.set("xhr", "1");
+ xhr.open("POST", action.href);
+ xhr.send(data);
+ action.searchParams.delete("xhr");
+ return extension.awaitMessage("done");
+ };
+
+ formData.append(blob.name, blob.content, blob.fileName);
+ formData.append("formDataField", "some value");
+ updateActionURL();
+ yield post(formData);
+
+ action.searchParams.set("upload", JSON.stringify([{file: "<file>"}]));
+ yield post(testFile);
+
+ action.searchParams.set("upload", `${blob.content.size} bytes`);
+ yield post(blob.content);
+
+ let byteLength = 16;
+ action.searchParams.set("upload", `${byteLength} bytes`);
+ yield post(new ArrayBuffer(byteLength));
+ }
+
+ yield extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html
new file mode 100644
index 0000000000..7d49d55baa
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_window_postMessage.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for content script</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_postMessage() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ "matches": ["http://mochi.test/*/file_sample.html"],
+ "js": ["content_script.js"],
+ "run_at": "document_start",
+ "all_frames": true,
+ },
+ ],
+
+ web_accessible_resources: ["iframe.html"],
+ },
+
+ background() {
+ browser.test.sendMessage("iframe-url", browser.runtime.getURL("iframe.html"));
+ },
+
+ files: {
+ "content_script.js": function() {
+ window.addEventListener("message", event => {
+ if (event.data == "ping") {
+ event.source.postMessage({pong: location.href},
+ event.origin);
+ }
+ });
+ },
+
+ "iframe.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="content_script.js"><\/script>
+ </head>
+ </html>`,
+ },
+ };
+
+ let createIframe = url => {
+ let iframe = document.createElement("iframe");
+ return new Promise(resolve => {
+ iframe.src = url;
+ iframe.onload = resolve;
+ document.body.appendChild(iframe);
+ }).then(() => {
+ return iframe;
+ });
+ };
+
+ let awaitMessage = () => {
+ return new Promise(resolve => {
+ let listener = event => {
+ if (event.data.pong) {
+ window.removeEventListener("message", listener);
+ resolve(event.data);
+ }
+ };
+ window.addEventListener("message", listener);
+ });
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ let iframeURL = yield extension.awaitMessage("iframe-url");
+ let testURL = SimpleTest.getTestFileURL("file_sample.html");
+
+ for (let url of [iframeURL, testURL]) {
+ info(`Testing URL ${url}`);
+
+ let iframe = yield createIframe(url);
+
+ iframe.contentWindow.postMessage(
+ "ping", url);
+
+ let pong = yield awaitMessage();
+ is(pong.pong, url, "Got expected pong");
+
+ iframe.remove();
+ }
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html b/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html
new file mode 100644
index 0000000000..1afdadb9fc
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_xhr_capabilities.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test XHR capabilities</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_xhr_capabilities() {
+ function backgroundScript() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", browser.extension.getURL("bad.xml"));
+
+ browser.test.sendMessage("result",
+ {name: "Background script XHRs should not be privileged",
+ result: xhr.channel === undefined});
+
+ xhr.onload = () => {
+ browser.test.sendMessage("result",
+ {name: "Background script XHRs should not yield <parsererrors>",
+ result: xhr.responseXML === null});
+ };
+ xhr.send();
+ }
+
+ function contentScript() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", browser.extension.getURL("bad.xml"));
+
+ browser.test.sendMessage("result",
+ {name: "Content script XHRs should not be privileged",
+ result: xhr.channel === undefined});
+
+ xhr.onload = () => {
+ browser.test.sendMessage("result",
+ {name: "Content script XHRs should not yield <parsererrors>",
+ result: xhr.responseXML === null});
+ };
+ xhr.send();
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ background: {
+ "scripts": ["background.js"],
+ },
+ content_scripts: [{
+ "matches": ["http://example.com/"],
+ "js": ["content_script.js"],
+ }],
+ web_accessible_resources: [
+ "bad.xml",
+ ],
+ },
+
+ files: {
+ "bad.xml": "<xml",
+ "background.js": `(${backgroundScript})()`,
+ "content_script.js": `(${contentScript})()`,
+ },
+ });
+
+ yield extension.startup();
+
+ let win = window.open("http://example.com/");
+
+ // We expect four test results from the content/background scripts.
+ for (let i = 0; i < 4; ++i) {
+ let result = yield extension.awaitMessage("result");
+ ok(result.result, result.name);
+ }
+
+ win.close();
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js
new file mode 100644
index 0000000000..ccfb2ac1fc
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_chromeworker.js
@@ -0,0 +1,8 @@
+"use strict";
+
+onmessage = function(event) {
+ fetch("https://example.com/example.txt").then(() => {
+ postMessage("Done!");
+ });
+};
+
diff --git a/toolkit/components/extensions/test/mochitest/webrequest_test.jsm b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm
new file mode 100644
index 0000000000..bfb1483018
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_test.jsm
@@ -0,0 +1,22 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["webrequest_test"];
+
+Components.utils.importGlobalProperties(["fetch", "XMLHttpRequest"]);
+
+this.webrequest_test = {
+ testFetch(url) {
+ return fetch(url);
+ },
+
+ testXHR(url) {
+ return new Promise(resolve => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("HEAD", url);
+ xhr.onload = () => {
+ resolve();
+ };
+ xhr.send();
+ });
+ },
+};
diff --git a/toolkit/components/extensions/test/mochitest/webrequest_worker.js b/toolkit/components/extensions/test/mochitest/webrequest_worker.js
new file mode 100644
index 0000000000..dcffd08578
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/webrequest_worker.js
@@ -0,0 +1,3 @@
+"use strict";
+
+fetch("https://example.com/example.txt");
diff --git a/toolkit/components/extensions/test/xpcshell/.eslintrc.js b/toolkit/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..3758537ef4
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../../../../testing/xpcshell/xpcshell.eslintrc.js",
+
+ "globals": {
+ "browser": false,
+ },
+};
diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.html b/toolkit/components/extensions/test/xpcshell/data/file_download.html
new file mode 100644
index 0000000000..d970c63259
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_download.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+<div>Download HTML File</div>
+
+</body>
+</html>
diff --git a/toolkit/components/extensions/test/xpcshell/data/file_download.txt b/toolkit/components/extensions/test/xpcshell/data/file_download.txt
new file mode 100644
index 0000000000..6293c7af79
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_download.txt
@@ -0,0 +1 @@
+This is a sample file used in download tests.
diff --git a/toolkit/components/extensions/test/xpcshell/head.js b/toolkit/components/extensions/test/xpcshell/head.js
new file mode 100644
index 0000000000..9e22be6da7
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,111 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+/* exported createHttpServer, promiseConsoleOutput, cleanupDir */
+
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Timer.jsm");
+Components.utils.import("resource://testing-common/AddonTestUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
+ "resource://testing-common/ExtensionXPCShellUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
+ "resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+ExtensionTestUtils.init(this);
+
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param {integer} [port]
+ * The port to listen on. If omitted, listen on a random
+ * port. The latter is the preferred behavior.
+ *
+ * @returns {HttpServer}
+ */
+function createHttpServer(port = -1) {
+ let server = new HttpServer();
+ server.start(port);
+
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+
+ return server;
+}
+
+var promiseConsoleOutput = Task.async(function* (task) {
+ const DONE = `=== console listener ${Math.random()} done ===`;
+
+ let listener;
+ let messages = [];
+ let awaitListener = new Promise(resolve => {
+ listener = msg => {
+ if (msg == DONE) {
+ resolve();
+ } else {
+ void (msg instanceof Ci.nsIConsoleMessage);
+ messages.push(msg);
+ }
+ };
+ });
+
+ Services.console.registerListener(listener);
+ try {
+ let result = yield task();
+
+ Services.console.logStringMessage(DONE);
+ yield awaitListener;
+
+ return {messages, result};
+ } finally {
+ Services.console.unregisterListener(listener);
+ }
+});
+
+// Attempt to remove a directory. If the Windows OS is still using the
+// file sometimes remove() will fail. So try repeatedly until we can
+// remove it or we give up.
+function cleanupDir(dir) {
+ let count = 0;
+ return new Promise((resolve, reject) => {
+ function tryToRemoveDir() {
+ count += 1;
+ try {
+ dir.remove(true);
+ } catch (e) {
+ // ignore
+ }
+ if (!dir.exists()) {
+ return resolve();
+ }
+ if (count >= 25) {
+ return reject(`Failed to cleanup directory: ${dir}`);
+ }
+ setTimeout(tryToRemoveDir, 100);
+ }
+ tryToRemoveDir();
+ });
+}
diff --git a/toolkit/components/extensions/test/xpcshell/head_native_messaging.js b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
new file mode 100644
index 0000000000..f7c619b76a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
@@ -0,0 +1,131 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals AppConstants, FileUtils */
+/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */
+
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
+ "resource://testing-common/MockRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
+
+
+// It's important that we use a space in this directory name to make sure we
+// correctly handle executing batch files with spaces in their path.
+let tmpDir = FileUtils.getDir("TmpD", ["Native Messaging"]);
+tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+do_register_cleanup(() => {
+ tmpDir.remove(true);
+});
+
+function getPath(filename) {
+ return OS.Path.join(tmpDir.path, filename);
+}
+
+const ID = "native@tests.mozilla.org";
+
+
+function* setupHosts(scripts) {
+ const PERMS = {unixMode: 0o755};
+
+ const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ const pythonPath = yield Subprocess.pathSearch(env.get("PYTHON"));
+
+ function* writeManifest(script, scriptPath, path) {
+ let body = `#!${pythonPath} -u\n${script.script}`;
+
+ yield OS.File.writeAtomic(scriptPath, body);
+ yield OS.File.setPermissions(scriptPath, PERMS);
+
+ let manifest = {
+ name: script.name,
+ description: script.description,
+ path,
+ type: "stdio",
+ allowed_extensions: [ID],
+ };
+
+ let manifestPath = getPath(`${script.name}.json`);
+ yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
+
+ return manifestPath;
+ }
+
+ switch (AppConstants.platform) {
+ case "macosx":
+ case "linux":
+ let dirProvider = {
+ getFile(property) {
+ if (property == "XREUserNativeMessaging") {
+ return tmpDir.clone();
+ } else if (property == "XRESysNativeMessaging") {
+ return tmpDir.clone();
+ }
+ return null;
+ },
+ };
+
+ Services.dirsvc.registerProvider(dirProvider);
+ do_register_cleanup(() => {
+ Services.dirsvc.unregisterProvider(dirProvider);
+ });
+
+ for (let script of scripts) {
+ let path = getPath(`${script.name}.py`);
+
+ yield writeManifest(script, path, path);
+ }
+ break;
+
+ case "win":
+ const REGKEY = String.raw`Software\Mozilla\NativeMessagingHosts`;
+
+ let registry = new MockRegistry();
+ do_register_cleanup(() => {
+ registry.shutdown();
+ });
+
+ for (let script of scripts) {
+ // It's important that we use a space in this filename. See directory
+ // name comment above.
+ let batPath = getPath(`batch ${script.name}.bat`);
+ let scriptPath = getPath(`${script.name}.py`);
+
+ let batBody = `@ECHO OFF\n${pythonPath} -u "${scriptPath}" %*\n`;
+ yield OS.File.writeAtomic(batPath, batBody);
+
+ // Create absolute and relative path versions of the entry.
+ for (let [name, path] of [[script.name, batPath],
+ [`relative.${script.name}`, OS.Path.basename(batPath)]]) {
+ script.name = name;
+ let manifestPath = yield writeManifest(script, scriptPath, path);
+
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGKEY}\\${script.name}`, "", manifestPath);
+ }
+ }
+ break;
+
+ default:
+ ok(false, `Native messaging is not supported on ${AppConstants.platform}`);
+ }
+}
+
+
+function getSubprocessCount() {
+ return SubprocessImpl.Process.getWorker().call("getProcesses", [])
+ .then(result => result.size);
+}
+function waitForSubprocessExit() {
+ return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []).then(() => {
+ // Return to the main event loop to give IO handlers enough time to consume
+ // their remaining buffered input.
+ return new Promise(resolve => setTimeout(resolve, 0));
+ });
+}
diff --git a/toolkit/components/extensions/test/xpcshell/head_sync.js b/toolkit/components/extensions/test/xpcshell/head_sync.js
new file mode 100644
index 0000000000..9b66b78e75
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/head_sync.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/* exported withSyncContext */
+
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm", this);
+
+var {
+ BaseContext,
+} = ExtensionCommon;
+
+class Context extends BaseContext {
+ constructor(principal) {
+ super();
+ Object.defineProperty(this, "principal", {
+ value: principal,
+ configurable: true,
+ });
+ this.sandbox = Components.utils.Sandbox(principal, {wantXrays: false});
+ this.extension = {id: "test@web.extension"};
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+/**
+ * Call the given function with a newly-constructed context.
+ * Unload the context on the way out.
+ *
+ * @param {function} f the function to call
+ */
+function* withContext(f) {
+ const ssm = Services.scriptSecurityManager;
+ const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
+ const context = new Context(PRINCIPAL1);
+ try {
+ yield* f(context);
+ } finally {
+ yield context.unload();
+ }
+}
+
+/**
+ * Like withContext(), but also turn on the "storage.sync" pref for
+ * the duration of the function.
+ * Calls to this function can be replaced with calls to withContext
+ * once the pref becomes on by default.
+ *
+ * @param {function} f the function to call
+ */
+function* withSyncContext(f) {
+ const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
+ let prefs = Services.prefs;
+
+ try {
+ prefs.setBoolPref(STORAGE_SYNC_PREF, true);
+ yield* withContext(f);
+ } finally {
+ prefs.clearUserPref(STORAGE_SYNC_PREF);
+ }
+}
diff --git a/toolkit/components/extensions/test/xpcshell/native_messaging.ini b/toolkit/components/extensions/test/xpcshell/native_messaging.ini
new file mode 100644
index 0000000000..d0e1da163d
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/native_messaging.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+head = head.js head_native_messaging.js
+tail =
+firefox-appdir = browser
+skip-if = appname == "thunderbird" || os == "android"
+subprocess = true
+support-files =
+ data/**
+tags = webextensions
+
+[test_ext_native_messaging.js]
+[test_ext_native_messaging_perf.js]
+[test_ext_native_messaging_unresponsive.js]
diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
new file mode 100644
index 0000000000..b6213baacb
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
@@ -0,0 +1,38 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const ADDON_ID = "test@web.extension";
+
+const aps = Cc["@mozilla.org/addons/policy-service;1"]
+ .getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+
+do_register_cleanup(() => {
+ aps.setAddonCSP(ADDON_ID, null);
+});
+
+add_task(function* test_addon_csp() {
+ equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"),
+ "Expected base CSP value");
+
+ equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"),
+ "Expected default CSP value");
+
+ equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
+ "CSP for unknown add-on ID should be the default CSP");
+
+
+ const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'";
+
+ aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY);
+
+ equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy");
+
+
+ aps.setAddonCSP(ADDON_ID, null);
+
+ equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
+ "CSP should revert to default when set to null");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_csp_validator.js b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js
new file mode 100644
index 0000000000..59a7322bc3
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_csp_validator.js
@@ -0,0 +1,85 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const cps = Cc["@mozilla.org/addons/content-policy;1"].getService(Ci.nsIAddonContentPolicy);
+
+add_task(function* test_csp_validator() {
+ let checkPolicy = (policy, expectedResult, message = null) => {
+ do_print(`Checking policy: ${policy}`);
+
+ let result = cps.validateAddonCSP(policy);
+ equal(result, expectedResult);
+ };
+
+ checkPolicy("script-src 'self'; object-src 'self';",
+ null);
+
+ let hash = "'sha256-NjZhMDQ1YjQ1MjEwMmM1OWQ4NDBlYzA5N2Q1OWQ5NDY3ZTEzYTNmMzRmNjQ5NGU1MzlmZmQzMmMxYmIzNWYxOCAgLQo='";
+
+ checkPolicy(`script-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash} 'unsafe-eval'; ` +
+ `object-src 'self' https://com https://*.example.com moz-extension://09abcdef blob: filesystem: ${hash}`,
+ null);
+
+ checkPolicy("",
+ "Policy is missing a required \u2018script-src\u2019 directive");
+
+ checkPolicy("object-src 'none';",
+ "Policy is missing a required \u2018script-src\u2019 directive");
+
+
+ checkPolicy("default-src 'self'", null,
+ "A valid default-src should count as a valid script-src or object-src");
+
+ checkPolicy("default-src 'self'; script-src 'self'", null,
+ "A valid default-src should count as a valid script-src or object-src");
+
+ checkPolicy("default-src 'self'; object-src 'self'", null,
+ "A valid default-src should count as a valid script-src or object-src");
+
+
+ checkPolicy("default-src 'self'; script-src http://example.com",
+ "\u2018script-src\u2019 directive contains a forbidden http: protocol source",
+ "A valid default-src should not allow an invalid script-src directive");
+
+ checkPolicy("default-src 'self'; object-src http://example.com",
+ "\u2018object-src\u2019 directive contains a forbidden http: protocol source",
+ "A valid default-src should not allow an invalid object-src directive");
+
+
+ checkPolicy("script-src 'self';",
+ "Policy is missing a required \u2018object-src\u2019 directive");
+
+ checkPolicy("script-src 'none'; object-src 'none'",
+ "\u2018script-src\u2019 must include the source 'self'");
+
+ checkPolicy("script-src 'self'; object-src 'none';",
+ null);
+
+ checkPolicy("script-src 'self' 'unsafe-inline'; object-src 'self';",
+ "\u2018script-src\u2019 directive contains a forbidden 'unsafe-inline' keyword");
+
+
+ let directives = ["script-src", "object-src"];
+
+ for (let [directive, other] of [directives, directives.slice().reverse()]) {
+ for (let src of ["https://*", "https://*.blogspot.com", "https://*"]) {
+ checkPolicy(`${directive} 'self' ${src}; ${other} 'self';`,
+ `https: wildcard sources in \u2018${directive}\u2019 directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)`);
+ }
+
+ checkPolicy(`${directive} 'self' https:; ${other} 'self';`,
+ `https: protocol requires a host in \u2018${directive}\u2019 directives`);
+
+ checkPolicy(`${directive} 'self' http://example.com; ${other} 'self';`,
+ `\u2018${directive}\u2019 directive contains a forbidden http: protocol source`);
+
+ for (let protocol of ["http", "ftp", "meh"]) {
+ checkPolicy(`${directive} 'self' ${protocol}:; ${other} 'self';`,
+ `\u2018${directive}\u2019 directive contains a forbidden ${protocol}: protocol source`);
+ }
+
+ checkPolicy(`${directive} 'self' 'nonce-01234'; ${other} 'self';`,
+ `\u2018${directive}\u2019 directive contains a forbidden 'nonce-*' keyword`);
+ }
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
new file mode 100644
index 0000000000..936c984c62
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
@@ -0,0 +1,210 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_alarm_without_permissions() {
+ function backgroundScript() {
+ browser.test.assertTrue(!browser.alarms,
+ "alarm API is not available when the alarm permission is not required");
+ browser.test.notifyPass("alarms_permission");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: [],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarms_permission");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_fires() {
+ function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+ let timer;
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the correct name");
+ clearTimeout(timer);
+ browser.test.notifyPass("alarm-fires");
+ });
+
+ browser.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
+
+ timer = setTimeout(async () => {
+ browser.test.fail("alarm fired within expected time");
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+ browser.test.notifyFail("alarm-fires");
+ }, 10000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-fires");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_fires_with_when() {
+ function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+ let timer;
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name");
+ clearTimeout(timer);
+ browser.test.notifyPass("alarm-when");
+ });
+
+ browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
+
+ timer = setTimeout(async () => {
+ browser.test.fail("alarm fired within expected time");
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+ browser.test.notifyFail("alarm-when");
+ }, 10000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-when");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_clear_non_matching_name() {
+ async function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+
+ browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000});
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME + "1");
+ browser.test.assertFalse(wasCleared, "alarm was not cleared");
+
+ let alarms = await browser.alarms.getAll();
+ browser.test.assertEq(1, alarms.length, "alarm was not removed");
+ browser.test.notifyPass("alarm-clear");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-clear");
+ yield extension.unload();
+});
+
+
+add_task(function* test_alarm_get_and_clear_single_argument() {
+ async function backgroundScript() {
+ browser.alarms.create({when: Date.now() + 2000});
+
+ let alarm = await browser.alarms.get();
+ browser.test.assertEq("", alarm.name, "expected alarm returned");
+
+ let wasCleared = await browser.alarms.clear();
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ let alarms = await browser.alarms.getAll();
+ browser.test.assertEq(0, alarms.length, "alarm was removed");
+
+ browser.test.notifyPass("alarm-single-arg");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-single-arg");
+ yield extension.unload();
+});
+
+
+add_task(function* test_get_get_all_clear_all_alarms() {
+ async function backgroundScript() {
+ const ALARM_NAME = "test_alarm";
+
+ let suffixes = [0, 1, 2];
+
+ for (let suffix of suffixes) {
+ browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000});
+ }
+
+ let alarms = await browser.alarms.getAll();
+ browser.test.assertEq(suffixes.length, alarms.length, "expected number of alarms were found");
+ alarms.forEach((alarm, index) => {
+ browser.test.assertEq(ALARM_NAME + index, alarm.name, "alarm has the expected name");
+ });
+
+
+ for (let suffix of suffixes) {
+ let alarm = await browser.alarms.get(ALARM_NAME + suffix);
+ browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "alarm has the expected name");
+ browser.test.sendMessage(`get-${suffix}`);
+ }
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME + suffixes[0]);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ alarms = await browser.alarms.getAll();
+ browser.test.assertEq(2, alarms.length, "alarm was removed");
+
+ let alarm = await browser.alarms.get(ALARM_NAME + suffixes[0]);
+ browser.test.assertEq(undefined, alarm, "non-existent alarm is undefined");
+ browser.test.sendMessage(`get-invalid`);
+
+ wasCleared = await browser.alarms.clearAll();
+ browser.test.assertTrue(wasCleared, "alarms were cleared");
+
+ alarms = await browser.alarms.getAll();
+ browser.test.assertEq(0, alarms.length, "no alarms exist");
+ browser.test.sendMessage("clearAll");
+ browser.test.sendMessage("clear");
+ browser.test.sendMessage("getAll");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield Promise.all([
+ extension.startup(),
+ extension.awaitMessage("getAll"),
+ extension.awaitMessage("get-0"),
+ extension.awaitMessage("get-1"),
+ extension.awaitMessage("get-2"),
+ extension.awaitMessage("clear"),
+ extension.awaitMessage("get-invalid"),
+ extension.awaitMessage("clearAll"),
+ ]);
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js
new file mode 100644
index 0000000000..11407b108a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_does_not_fire.js
@@ -0,0 +1,33 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_cleared_alarm_does_not_fire() {
+ async function backgroundScript() {
+ let ALARM_NAME = "test_ext_alarms";
+
+ browser.alarms.onAlarm.addListener(alarm => {
+ browser.test.fail("cleared alarm does not fire");
+ browser.test.notifyFail("alarm-cleared");
+ });
+ browser.alarms.create(ALARM_NAME, {when: Date.now() + 1000});
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ browser.test.notifyPass("alarm-cleared");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-cleared");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js
new file mode 100644
index 0000000000..6bcdf4e33a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_periodic.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_periodic_alarm_fires() {
+ function backgroundScript() {
+ const ALARM_NAME = "test_ext_alarms";
+ let count = 0;
+ let timer;
+
+ browser.alarms.onAlarm.addListener(async alarm => {
+ browser.test.assertEq(alarm.name, ALARM_NAME, "alarm has the expected name");
+ if (count++ === 3) {
+ clearTimeout(timer);
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ browser.test.notifyPass("alarm-periodic");
+ }
+ });
+
+ browser.alarms.create(ALARM_NAME, {periodInMinutes: 0.02});
+
+ timer = setTimeout(async () => {
+ browser.test.fail("alarm fired expected number of times");
+
+ let wasCleared = await browser.alarms.clear(ALARM_NAME);
+ browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+ browser.test.notifyFail("alarm-periodic");
+ }, 30000);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-periodic");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js
new file mode 100644
index 0000000000..96f61acb57
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms_replaces.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_duplicate_alarm_name_replaces_alarm() {
+ function backgroundScript() {
+ let count = 0;
+
+ browser.alarms.onAlarm.addListener(async alarm => {
+ if (alarm.name === "master alarm") {
+ browser.alarms.create("child alarm", {delayInMinutes: 0.05});
+ let results = await browser.alarms.getAll();
+
+ browser.test.assertEq(2, results.length, "exactly two alarms exist");
+ browser.test.assertEq("master alarm", results[0].name, "first alarm has the expected name");
+ browser.test.assertEq("child alarm", results[1].name, "second alarm has the expected name");
+
+ if (count++ === 3) {
+ await browser.alarms.clear("master alarm");
+ await browser.alarms.clear("child alarm");
+
+ browser.test.notifyPass("alarm-duplicate");
+ }
+ } else {
+ browser.test.fail("duplicate named alarm replaced existing alarm");
+ browser.test.notifyFail("alarm-duplicate");
+ }
+ });
+
+ browser.alarms.create("master alarm", {delayInMinutes: 0.025, periodInMinutes: 0.025});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["alarms"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("alarm-duplicate");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js
new file mode 100644
index 0000000000..d653d0e7aa
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_api_permissions.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+function getNextContext() {
+ return new Promise(resolve => {
+ Management.on("proxy-context-load", function listener(type, context) {
+ Management.off("proxy-context-load", listener);
+ resolve(context);
+ });
+ });
+}
+
+add_task(function* test_storage_api_without_permissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ // Force API initialization.
+ void browser.storage;
+ },
+
+ manifest: {
+ permissions: [],
+ },
+ });
+
+ let contextPromise = getNextContext();
+ yield extension.startup();
+
+ let context = yield contextPromise;
+
+ // Force API initialization.
+ void context.apiObj;
+
+ ok(!("storage" in context.apiObj),
+ "The storage API should not be initialized");
+
+ yield extension.unload();
+});
+
+add_task(function* test_storage_api_with_permissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ void browser.storage;
+ },
+
+ manifest: {
+ permissions: ["storage"],
+ },
+ });
+
+ let contextPromise = getNextContext();
+ yield extension.startup();
+
+ let context = yield contextPromise;
+
+ // Force API initialization.
+ void context.apiObj;
+
+ equal(typeof context.apiObj.storage, "object",
+ "The storage API should be initialized");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
new file mode 100644
index 0000000000..3f6672a11c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+
+const {
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+this.unknownvar = "Some module-global var";
+
+var gUniqueId = 0;
+
+// SchemaAPIManager's loadScript uses loadSubScript to load a script. This
+// requires a local (resource://) URL. So create such a temporary URL for
+// testing.
+function toLocalURI(code) {
+ let dataUrl = `data:charset=utf-8,${encodeURIComponent(code)}`;
+ let uniqueResPart = `need-a-local-uri-for-subscript-loading-${++gUniqueId}`;
+ Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler)
+ .setSubstitution(uniqueResPart, Services.io.newURI(dataUrl, null, null));
+ return `resource://${uniqueResPart}`;
+}
+
+add_task(function* test_global_isolation() {
+ let manA = new SchemaAPIManager("procA");
+ let manB = new SchemaAPIManager("procB");
+
+ // The "global" variable should be persistent and shared.
+ manA.loadScript(toLocalURI`global.globalVar = 1;`);
+ do_check_eq(manA.global.globalVar, 1);
+ do_check_eq(manA.global.unknownvar, undefined);
+ manA.loadScript(toLocalURI`global.canSeeGlobal = global.globalVar;`);
+ do_check_eq(manA.global.canSeeGlobal, 1);
+
+ // Each loadScript call should have their own scope, and global is shared.
+ manA.loadScript(toLocalURI`this.aVar = 1; global.thisScopeVar = aVar`);
+ do_check_eq(manA.global.aVar, undefined);
+ do_check_eq(manA.global.thisScopeVar, 1);
+ manA.loadScript(toLocalURI`global.differentScopeVar = this.aVar;`);
+ do_check_eq(manA.global.differentScopeVar, undefined);
+ manA.loadScript(toLocalURI`global.cantSeeOtherScope = typeof aVar;`);
+ do_check_eq(manA.global.cantSeeOtherScope, "undefined");
+
+ manB.loadScript(toLocalURI`global.tryReadOtherGlobal = global.tryagain;`);
+ do_check_eq(manA.global.tryReadOtherGlobal, undefined);
+
+ // Cu.import without second argument exports to the caller's global. Let's
+ // verify that it does not leak to the SchemaAPIManager's global.
+ do_check_eq(typeof ExtensionUtils, "undefined"); // Sanity check #1.
+ manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manA.global.hasExtUtils, "undefined"); // Sanity check #2
+
+ Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+ do_check_eq(typeof ExtensionUtils, "object"); // Sanity check #3.
+
+ manA.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manA.global.hasExtUtils, "undefined");
+ manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manB.global.hasExtUtils, "undefined");
+
+ // Confirm that Cu.import does not leak between SchemaAPIManager globals.
+ manA.loadScript(toLocalURI`
+ Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+ global.hasExtUtils = typeof ExtensionUtils;
+ `);
+ do_check_eq(manA.global.hasExtUtils, "object"); // Sanity check.
+ manB.loadScript(toLocalURI`global.hasExtUtils = typeof ExtensionUtils;`);
+ do_check_eq(manB.global.hasExtUtils, "undefined");
+
+ // Prototype modifications should be isolated.
+ manA.loadScript(toLocalURI`
+ Object.prototype.modifiedByA = "Prrft";
+ global.fromA = {};
+ `);
+ manA.loadScript(toLocalURI`
+ global.fromAagain = {};
+ `);
+ manB.loadScript(toLocalURI`
+ global.fromB = {};
+ `);
+ do_check_eq(manA.global.modifiedByA, "Prrft");
+ do_check_eq(manA.global.fromA.modifiedByA, "Prrft");
+ do_check_eq(manA.global.fromAagain.modifiedByA, "Prrft");
+ do_check_eq(manB.global.modifiedByA, undefined);
+ do_check_eq(manB.global.fromB.modifiedByA, undefined);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js
new file mode 100644
index 0000000000..26282fcb9a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_load_events.js
@@ -0,0 +1,23 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+
+add_task(function* test_DOMContentLoaded_in_generated_background_page() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ function reportListener(event) {
+ browser.test.sendMessage("eventname", event.type);
+ }
+ document.addEventListener("DOMContentLoaded", reportListener);
+ window.addEventListener("load", reportListener);
+ },
+ });
+
+ yield extension.startup();
+ equal("DOMContentLoaded", yield extension.awaitMessage("eventname"));
+ equal("load", yield extension.awaitMessage("eventname"));
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js
new file mode 100644
index 0000000000..4bf59b7989
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_generated_reload.js
@@ -0,0 +1,24 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_reload_generated_background_page() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ if (location.hash !== "#firstrun") {
+ browser.test.sendMessage("first run");
+ location.hash = "#firstrun";
+ browser.test.assertEq("#firstrun", location.hash);
+ location.reload();
+ } else {
+ browser.test.notifyPass("second run");
+ }
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("first run");
+ yield extension.awaitFinish("second run");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js
new file mode 100644
index 0000000000..092a9f5b39
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_global_history.js
@@ -0,0 +1,22 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
+
+add_task(function* test_global_history() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.sendMessage("background-loaded", location.href);
+ },
+ });
+
+ yield extension.startup();
+
+ let backgroundURL = yield extension.awaitMessage("background-loaded");
+
+ yield extension.unload();
+
+ let exists = yield PlacesTestUtils.isPageInDB(backgroundURL);
+ ok(!exists, "Background URL should not be in history database");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js
new file mode 100644
index 0000000000..8e8b5e0b08
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_private_browsing.js
@@ -0,0 +1,40 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+function* testBackgroundPage(expected) {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ browser.test.assertEq(window, browser.extension.getBackgroundPage(),
+ "Caller should be able to access itself as a background page");
+ browser.test.assertEq(window, await browser.runtime.getBackgroundPage(),
+ "Caller should be able to access itself as a background page");
+
+ browser.test.sendMessage("incognito", browser.extension.inIncognitoContext);
+ },
+ });
+
+ yield extension.startup();
+
+ let incognito = yield extension.awaitMessage("incognito");
+ equal(incognito, expected.incognito, "Expected incognito value");
+
+ yield extension.unload();
+}
+
+add_task(function* test_background_incognito() {
+ do_print("Test background page incognito value with permanent private browsing disabled");
+
+ yield testBackgroundPage({incognito: false});
+
+ do_print("Test background page incognito value with permanent private browsing enabled");
+
+ Preferences.set("browser.privatebrowsing.autostart", true);
+ do_register_cleanup(() => {
+ Preferences.reset("browser.privatebrowsing.autostart");
+ });
+
+ yield testBackgroundPage({incognito: true});
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js
new file mode 100644
index 0000000000..426833edd4
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_runtime_connect_params.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ let received_ports_number = 0;
+
+ const expected_received_ports_number = 1;
+
+ function countReceivedPorts(port) {
+ received_ports_number++;
+
+ if (port.name == "check-results") {
+ browser.runtime.onConnect.removeListener(countReceivedPorts);
+
+ browser.test.assertEq(expected_received_ports_number, received_ports_number, "invalid connect should not create a port");
+
+ browser.test.notifyPass("runtime.connect invalid params");
+ }
+ }
+
+ browser.runtime.onConnect.addListener(countReceivedPorts);
+
+ let childFrame = document.createElement("iframe");
+ childFrame.src = "extensionpage.html";
+ document.body.appendChild(childFrame);
+}
+
+function senderScript() {
+ let detected_invalid_connect_params = 0;
+
+ const invalid_connect_params = [
+ // too many params
+ ["fake-extensions-id", {name: "fake-conn-name"}, "unexpected third params"],
+ // invalid params format
+ [{}, {}],
+ ["fake-extensions-id", "invalid-connect-info-format"],
+ ];
+ const expected_detected_invalid_connect_params = invalid_connect_params.length;
+
+ function assertInvalidConnectParamsException(params) {
+ try {
+ browser.runtime.connect(...params);
+ } catch (e) {
+ detected_invalid_connect_params++;
+ browser.test.assertTrue(e.toString().indexOf("Incorrect argument types for runtime.connect.") >= 0, "exception message is correct");
+ }
+ }
+ for (let params of invalid_connect_params) {
+ assertInvalidConnectParamsException(params);
+ }
+ browser.test.assertEq(expected_detected_invalid_connect_params, detected_invalid_connect_params, "all invalid runtime.connect params detected");
+
+ browser.runtime.connect(browser.runtime.id, {name: "check-results"});
+}
+
+let extensionData = {
+ background: backgroundScript,
+ files: {
+ "senderScript.js": senderScript,
+ "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
+ },
+};
+
+add_task(function* test_backgroundRuntimeConnectParams() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("runtime.connect invalid params");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js
new file mode 100644
index 0000000000..c5f2f1332c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_sub_windows.js
@@ -0,0 +1,45 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testBackgroundWindow() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.log("background script executed");
+
+ browser.test.sendMessage("background-script-load");
+
+ let img = document.createElement("img");
+ img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
+ document.body.appendChild(img);
+
+ img.onload = () => {
+ browser.test.log("image loaded");
+
+ let iframe = document.createElement("iframe");
+ iframe.src = "about:blank?1";
+
+ iframe.onload = () => {
+ browser.test.log("iframe loaded");
+ setTimeout(() => {
+ browser.test.notifyPass("background sub-window test done");
+ }, 0);
+ };
+ document.body.appendChild(iframe);
+ };
+ },
+ });
+
+ let loadCount = 0;
+ extension.onMessage("background-script-load", () => {
+ loadCount++;
+ });
+
+ yield extension.startup();
+
+ yield extension.awaitFinish("background sub-window test done");
+
+ equal(loadCount, 1, "background script loaded only once");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js
new file mode 100644
index 0000000000..948e2913ea
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_background_window_properties.js
@@ -0,0 +1,34 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testBackgroundWindowProperties() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ let expectedValues = {
+ screenX: 0,
+ screenY: 0,
+ outerWidth: 0,
+ outerHeight: 0,
+ };
+
+ for (let k in window) {
+ try {
+ if (k in expectedValues) {
+ browser.test.assertEq(expectedValues[k], window[k],
+ `should return the expected value for window property: ${k}`);
+ } else {
+ void window[k];
+ }
+ } catch (e) {
+ browser.test.assertEq(null, e, `unexpected exception accessing window property: ${k}`);
+ }
+ }
+
+ browser.test.notifyPass("background.testWindowProperties.done");
+ },
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("background.testWindowProperties.done");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
new file mode 100644
index 0000000000..56a14e1898
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -0,0 +1,190 @@
+"use strict";
+
+const global = this;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ BaseContext,
+} = ExtensionCommon;
+
+var {
+ EventManager,
+ SingletonEventManager,
+} = ExtensionUtils;
+
+class StubContext extends BaseContext {
+ constructor() {
+ let fakeExtension = {id: "test@web.extension"};
+ super("testEnv", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+
+add_task(function* test_post_unload_promises() {
+ let context = new StubContext();
+
+ let fail = result => {
+ ok(false, `Unexpected callback: ${result}`);
+ };
+
+ // Make sure promises resolve normally prior to unload.
+ let promises = [
+ context.wrapPromise(Promise.resolve()),
+ context.wrapPromise(Promise.reject({message: ""})).catch(() => {}),
+ ];
+
+ yield Promise.all(promises);
+
+ // Make sure promises that resolve after unload do not trigger
+ // resolution handlers.
+
+ context.wrapPromise(Promise.resolve("resolved"))
+ .then(fail);
+
+ context.wrapPromise(Promise.reject({message: "rejected"}))
+ .then(fail, fail);
+
+ context.unload();
+
+ // The `setTimeout` ensures that we return to the event loop after
+ // promise resolution, which means we're guaranteed to return after
+ // any micro-tasks that get enqueued by the resolution handlers above.
+ yield new Promise(resolve => setTimeout(resolve, 0));
+});
+
+
+add_task(function* test_post_unload_listeners() {
+ let context = new StubContext();
+
+ let fireEvent;
+ let onEvent = new EventManager(context, "onEvent", fire => {
+ fireEvent = fire;
+ return () => {};
+ });
+
+ let fireSingleton;
+ let onSingleton = new SingletonEventManager(context, "onSingleton", callback => {
+ fireSingleton = () => {
+ Promise.resolve().then(callback);
+ };
+ return () => {};
+ });
+
+ let fail = event => {
+ ok(false, `Unexpected event: ${event}`);
+ };
+
+ // Check that event listeners aren't called after they've been removed.
+ onEvent.addListener(fail);
+ onSingleton.addListener(fail);
+
+ let promises = [
+ new Promise(resolve => onEvent.addListener(resolve)),
+ new Promise(resolve => onSingleton.addListener(resolve)),
+ ];
+
+ fireEvent("onEvent");
+ fireSingleton("onSingleton");
+
+ // Both `fireEvent` calls are dispatched asynchronously, so they won't
+ // have fired by this point. The `fail` listeners that we remove now
+ // should not be called, even though the events have already been
+ // enqueued.
+ onEvent.removeListener(fail);
+ onSingleton.removeListener(fail);
+
+ // Wait for the remaining listeners to be called, which should always
+ // happen after the `fail` listeners would normally be called.
+ yield Promise.all(promises);
+
+ // Check that event listeners aren't called after the context has
+ // unloaded.
+ onEvent.addListener(fail);
+ onSingleton.addListener(fail);
+
+ // The EventManager `fire` callback always dispatches events
+ // asynchronously, so we need to test that any pending event callbacks
+ // aren't fired after the context unloads. We also need to test that
+ // any `fire` calls that happen *after* the context is unloaded also
+ // do not trigger callbacks.
+ fireEvent("onEvent");
+ Promise.resolve("onEvent").then(fireEvent);
+
+ fireSingleton("onSingleton");
+ Promise.resolve("onSingleton").then(fireSingleton);
+
+ context.unload();
+
+ // The `setTimeout` ensures that we return to the event loop after
+ // promise resolution, which means we're guaranteed to return after
+ // any micro-tasks that get enqueued by the resolution handlers above.
+ yield new Promise(resolve => setTimeout(resolve, 0));
+});
+
+class Context extends BaseContext {
+ constructor(principal) {
+ let fakeExtension = {id: "test@web.extension"};
+ super("testEnv", fakeExtension);
+ Object.defineProperty(this, "principal", {
+ value: principal,
+ configurable: true,
+ });
+ this.sandbox = Cu.Sandbox(principal, {wantXrays: false});
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+let ssm = Services.scriptSecurityManager;
+const PRINCIPAL1 = ssm.createCodebasePrincipalFromOrigin("http://www.example.org");
+const PRINCIPAL2 = ssm.createCodebasePrincipalFromOrigin("http://www.somethingelse.org");
+
+// Test that toJSON() works in the json sandbox
+add_task(function* test_stringify_toJSON() {
+ let context = new Context(PRINCIPAL1);
+ let obj = Cu.evalInSandbox("({hidden: true, toJSON() { return {visible: true}; } })", context.sandbox);
+
+ let stringified = context.jsonStringify(obj);
+ let expected = JSON.stringify({visible: true});
+ equal(stringified, expected, "Stringified object with toJSON() method is as expected");
+});
+
+// Test that stringifying in inaccessible property throws
+add_task(function* test_stringify_inaccessible() {
+ let context = new Context(PRINCIPAL1);
+ let sandbox = context.sandbox;
+ let sandbox2 = Cu.Sandbox(PRINCIPAL2);
+
+ Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
+ let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
+ Assert.throws(() => {
+ context.jsonStringify(obj);
+ });
+});
+
+add_task(function* test_stringify_accessible() {
+ // Test that an accessible property from another global is included
+ let principal = Cu.getObjectPrincipal(Cu.Sandbox([PRINCIPAL1, PRINCIPAL2]));
+ let context = new Context(principal);
+ let sandbox = context.sandbox;
+ let sandbox2 = Cu.Sandbox(PRINCIPAL2);
+
+ Cu.waiveXrays(sandbox).subobj = Cu.evalInSandbox("({ subobject: true })", sandbox2);
+ let obj = Cu.evalInSandbox("({ local: true, nested: subobj })", sandbox);
+ let stringified = context.jsonStringify(obj);
+
+ let expected = JSON.stringify({local: true, nested: {subobject: true}});
+ equal(stringified, expected, "Stringified object with accessible property is as expected");
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js
new file mode 100644
index 0000000000..058b9b18cc
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads.js
@@ -0,0 +1,76 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_downloads_api_namespace_and_permissions() {
+ function backgroundScript() {
+ browser.test.assertTrue(!!browser.downloads, "`downloads` API is present.");
+ browser.test.assertTrue(!!browser.downloads.FilenameConflictAction,
+ "`downloads.FilenameConflictAction` enum is present.");
+ browser.test.assertTrue(!!browser.downloads.InterruptReason,
+ "`downloads.InterruptReason` enum is present.");
+ browser.test.assertTrue(!!browser.downloads.DangerType,
+ "`downloads.DangerType` enum is present.");
+ browser.test.assertTrue(!!browser.downloads.State,
+ "`downloads.State` enum is present.");
+ browser.test.notifyPass("downloads tests");
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads", "downloads.open", "downloads.shelf"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("downloads tests");
+ yield extension.unload();
+});
+
+add_task(function* test_downloads_open_permission() {
+ function backgroundScript() {
+ browser.test.assertFalse("open" in browser.downloads,
+ "`downloads.open` permission is required.");
+ browser.test.notifyPass("downloads tests");
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("downloads tests");
+ yield extension.unload();
+});
+
+add_task(function* test_downloads_open() {
+ async function backgroundScript() {
+ await browser.test.assertRejects(
+ browser.downloads.open(10),
+ "Invalid download id 10",
+ "The error is informative.");
+
+ browser.test.notifyPass("downloads tests");
+
+ // TODO: Once downloads.{pause,cancel,resume} lands (bug 1245602) test that this gives a good
+ // error when called with an incompleted download.
+ }
+
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads", "downloads.open"],
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("downloads tests");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
new file mode 100644
index 0000000000..37ddd4d7c1
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_download.js
@@ -0,0 +1,354 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* global OS */
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+
+const gServer = createHttpServer();
+gServer.registerDirectory("/data/", do_get_file("data"));
+
+const WINDOWS = AppConstants.platform == "win";
+
+const BASE = `http://localhost:${gServer.identity.primaryPort}/data`;
+const FILE_NAME = "file_download.txt";
+const FILE_URL = BASE + "/" + FILE_NAME;
+const FILE_NAME_UNIQUE = "file_download(1).txt";
+const FILE_LEN = 46;
+
+let downloadDir;
+
+function setup() {
+ downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_print(`Using download directory ${downloadDir.path}`);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
+
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+
+ let entries = downloadDir.directoryEntries;
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+ ok(false, `Leftover file ${entry.path} in download directory`);
+ entry.remove(false);
+ }
+
+ downloadDir.remove(false);
+ });
+}
+
+function backgroundScript() {
+ let blobUrl;
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == "download.request") {
+ let options = args[0];
+
+ if (options.blobme) {
+ let blob = new Blob(options.blobme);
+ delete options.blobme;
+ blobUrl = options.url = window.URL.createObjectURL(blob);
+ }
+
+ try {
+ let id = await browser.downloads.download(options);
+ browser.test.sendMessage("download.done", {status: "success", id});
+ } catch (error) {
+ browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
+ }
+ } else if (msg == "killTheBlob") {
+ window.URL.revokeObjectURL(blobUrl);
+ blobUrl = null;
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+// This function is a bit of a sledgehammer, it looks at every download
+// the browser knows about and waits for all active downloads to complete.
+// But we only start one at a time and only do a handful in total, so
+// this lets us test download() without depending on anything else.
+async function waitForDownloads() {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ let inprogress = downloads.filter(dl => !dl.stopped);
+ return Promise.all(inprogress.map(dl => dl.whenSucceeded()));
+}
+
+// Create a file in the downloads directory.
+function touch(filename) {
+ let file = downloadDir.clone();
+ file.append(filename);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+}
+
+// Remove a file in the downloads directory.
+function remove(filename, recursive = false) {
+ let file = downloadDir.clone();
+ file.append(filename);
+ file.remove(recursive);
+}
+
+add_task(function* test_downloads() {
+ setup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${backgroundScript})()`,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ function download(options) {
+ extension.sendMessage("download.request", options);
+ return extension.awaitMessage("download.done");
+ }
+
+ async function testDownload(options, localFile, expectedSize, description) {
+ let msg = await download(options);
+ equal(msg.status, "success", `downloads.download() works with ${description}`);
+
+ await waitForDownloads();
+
+ let localPath = downloadDir.clone();
+ let parts = Array.isArray(localFile) ? localFile : [localFile];
+
+ parts.map(p => localPath.append(p));
+ equal(localPath.fileSize, expectedSize, "Downloaded file has expected size");
+ localPath.remove(false);
+ }
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+ do_print("extension started");
+
+ // Call download() with just the url property.
+ yield testDownload({url: FILE_URL}, FILE_NAME, FILE_LEN, "just source");
+
+ // Call download() with a filename property.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "newpath.txt",
+ }, "newpath.txt", FILE_LEN, "source and filename");
+
+ // Call download() with a filename with subdirs.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "sub/dir/file",
+ }, ["sub", "dir", "file"], FILE_LEN, "source and filename with subdirs");
+
+ // Call download() with a filename with existing subdirs.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "sub/dir/file2",
+ }, ["sub", "dir", "file2"], FILE_LEN, "source and filename with existing subdirs");
+
+ // Only run Windows path separator test on Windows.
+ if (WINDOWS) {
+ // Call download() with a filename with Windows path separator.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "sub\\dir\\file3",
+ }, ["sub", "dir", "file3"], FILE_LEN, "filename with Windows path separator");
+ }
+ remove("sub", true);
+
+ // Call download(), filename with subdir, skipping parts.
+ yield testDownload({
+ url: FILE_URL,
+ filename: "skip//part",
+ }, ["skip", "part"], FILE_LEN, "source, filename, with subdir, skipping parts");
+ remove("skip", true);
+
+ // Check conflictAction of "uniquify".
+ touch(FILE_NAME);
+ yield testDownload({
+ url: FILE_URL,
+ conflictAction: "uniquify",
+ }, FILE_NAME_UNIQUE, FILE_LEN, "conflictAction=uniquify");
+ // todo check that preexisting file was not modified?
+ remove(FILE_NAME);
+
+ // Check conflictAction of "overwrite".
+ touch(FILE_NAME);
+ yield testDownload({
+ url: FILE_URL,
+ conflictAction: "overwrite",
+ }, FILE_NAME, FILE_LEN, "conflictAction=overwrite");
+
+ // Try to download in invalid url
+ yield download({url: "this is not a valid URL"}).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with invalid url");
+ ok(/not a valid URL/.test(msg.errmsg), "error message for invalid url is correct");
+ });
+
+ // Try to download to an empty path.
+ yield download({
+ url: FILE_URL,
+ filename: "",
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with empty filename");
+ equal(msg.errmsg, "filename must not be empty", "error message for empty filename is correct");
+ });
+
+ // Try to download to an absolute path.
+ const absolutePath = OS.Path.join(WINDOWS ? "\\tmp" : "/tmp", "file_download.txt");
+ yield download({
+ url: FILE_URL,
+ filename: absolutePath,
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with absolute filename");
+ equal(msg.errmsg, "filename must not be an absolute path", `error message for absolute path (${absolutePath}) is correct`);
+ });
+
+ if (WINDOWS) {
+ yield download({
+ url: FILE_URL,
+ filename: "C:\\file_download.txt",
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with absolute filename");
+ equal(msg.errmsg, "filename must not be an absolute path", "error message for absolute path with drive letter is correct");
+ });
+ }
+
+ // Try to download to a relative path containing ..
+ yield download({
+ url: FILE_URL,
+ filename: OS.Path.join("..", "file_download.txt"),
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with back-references");
+ equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
+ });
+
+ // Try to download to a long relative path containing ..
+ yield download({
+ url: FILE_URL,
+ filename: OS.Path.join("foo", "..", "..", "file_download.txt"),
+ }).then(msg => {
+ equal(msg.status, "error", "downloads.download() fails with back-references");
+ equal(msg.errmsg, "filename must not contain back-references (..)", "error message for back-references is correct");
+ });
+
+ // Try to download a blob url
+ const BLOB_STRING = "Hello, world";
+ yield testDownload({
+ blobme: [BLOB_STRING],
+ filename: FILE_NAME,
+ }, FILE_NAME, BLOB_STRING.length, "blob url");
+ extension.sendMessage("killTheBlob");
+
+ // Try to download a blob url without a given filename
+ yield testDownload({
+ blobme: [BLOB_STRING],
+ }, "download", BLOB_STRING.length, "blob url with no filename");
+ extension.sendMessage("killTheBlob");
+
+ yield extension.unload();
+});
+
+add_task(function* test_download_post() {
+ const server = createHttpServer();
+ const url = `http://localhost:${server.identity.primaryPort}/post-log`;
+
+ let received;
+ server.registerPathHandler("/post-log", request => {
+ received = request;
+ });
+
+ // Confirm received vs. expected values.
+ function confirm(method, headers = {}, body) {
+ equal(received.method, method, "method is correct");
+
+ for (let name in headers) {
+ ok(received.hasHeader(name), `header ${name} received`);
+ equal(received.getHeader(name), headers[name], `header ${name} is correct`);
+ }
+
+ if (body) {
+ const str = NetUtil.readInputStreamToString(received.bodyInputStream,
+ received.bodyInputStream.available());
+ equal(str, body, "body is correct");
+ }
+ }
+
+ function background() {
+ browser.test.onMessage.addListener(async options => {
+ try {
+ await browser.downloads.download(options);
+ } catch (err) {
+ browser.test.sendMessage("done", {err: err.message});
+ }
+ });
+ browser.downloads.onChanged.addListener(({state}) => {
+ if (state && state.current === "complete") {
+ browser.test.sendMessage("done", {ok: true});
+ }
+ });
+ }
+
+ const manifest = {permissions: ["downloads"]};
+ const extension = ExtensionTestUtils.loadExtension({background, manifest});
+ yield extension.startup();
+
+ function download(options) {
+ options.url = url;
+ options.conflictAction = "overwrite";
+
+ extension.sendMessage(options);
+ return extension.awaitMessage("done");
+ }
+
+ // Test method option.
+ let result = yield download({});
+ ok(result.ok, "download works without the method option, defaults to GET");
+ confirm("GET");
+
+ result = yield download({method: "PUT"});
+ ok(!result.ok, "download rejected with PUT method");
+ ok(/method: Invalid enumeration/.test(result.err), "descriptive error message");
+
+ result = yield download({method: "POST"});
+ ok(result.ok, "download works with POST method");
+ confirm("POST");
+
+ // Test body option values.
+ result = yield download({body: []});
+ ok(!result.ok, "download rejected because of non-string body");
+ ok(/body: Expected string/.test(result.err), "descriptive error message");
+
+ result = yield download({method: "POST", body: "of work"});
+ ok(result.ok, "download works with POST method and body");
+ confirm("POST", {"Content-Length": 7}, "of work");
+
+ // Test custom headers.
+ result = yield download({headers: [{name: "X-Custom"}]});
+ ok(!result.ok, "download rejected because of missing header value");
+ ok(/"value" is required/.test(result.err), "descriptive error message");
+
+ result = yield download({headers: [{name: "X-Custom", value: "13"}]});
+ ok(result.ok, "download works with a custom header");
+ confirm("GET", {"X-Custom": "13"});
+
+ // Test forbidden headers.
+ result = yield download({headers: [{name: "DNT", value: "1"}]});
+ ok(!result.ok, "download rejected because of forbidden header name DNT");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ result = yield download({headers: [{name: "Proxy-Connection", value: "keep"}]});
+ ok(!result.ok, "download rejected because of forbidden header name prefix Proxy-");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ result = yield download({headers: [{name: "Sec-ret", value: "13"}]});
+ ok(!result.ok, "download rejected because of forbidden header name prefix Sec-");
+ ok(/Forbidden request header/.test(result.err), "descriptive error message");
+
+ remove("post-log");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
new file mode 100644
index 0000000000..d08aab6665
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -0,0 +1,862 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const ROOT = `http://localhost:${server.identity.primaryPort}`;
+const BASE = `${ROOT}/data`;
+const TXT_FILE = "file_download.txt";
+const TXT_URL = BASE + "/" + TXT_FILE;
+
+// Keep these in sync with code in interruptible.sjs
+const INT_PARTIAL_LEN = 15;
+const INT_TOTAL_LEN = 31;
+
+const TEST_DATA = "This is 31 bytes of sample data";
+const TOTAL_LEN = TEST_DATA.length;
+const PARTIAL_LEN = 15;
+
+// A handler to let us systematically test pausing/resuming/canceling
+// of downloads. This target represents a small text file but a simple
+// GET will stall after sending part of the data, to give the test code
+// a chance to pause or do other operations on an in-progress download.
+// A resumed download (ie, a GET with a Range: header) will allow the
+// download to complete.
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+
+ if (request.hasHeader("Range")) {
+ let start, end;
+ let matches = request.getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ if (matches != null) {
+ start = matches[1] ? parseInt(matches[1], 10) : 0;
+ end = matches[2] ? parseInt(matches[2], 10) : (TOTAL_LEN - 1);
+ }
+
+ if (end == undefined || end >= TOTAL_LEN) {
+ response.setStatusLine(request.httpVersion, 416, "Requested Range Not Satisfiable");
+ response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
+ response.finish();
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
+ response.write(TEST_DATA.slice(start, end + 1));
+ } else {
+ response.processAsync();
+ response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
+ response.write(TEST_DATA.slice(0, PARTIAL_LEN));
+ }
+
+ do_register_cleanup(() => {
+ try {
+ response.finish();
+ } catch (e) {
+ // This will throw, but we don't care at this point.
+ }
+ });
+}
+
+server.registerPathHandler("/interruptible.html", handleRequest);
+
+let interruptibleCount = 0;
+function getInterruptibleUrl() {
+ let n = interruptibleCount++;
+ return `${ROOT}/interruptible.html?count=${n}`;
+}
+
+function backgroundScript() {
+ let events = new Set();
+ let eventWaiter = null;
+
+ browser.downloads.onCreated.addListener(data => {
+ events.add({type: "onCreated", data});
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ browser.downloads.onChanged.addListener(data => {
+ events.add({type: "onChanged", data});
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ browser.downloads.onErased.addListener(data => {
+ events.add({type: "onErased", data});
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+
+ // Returns a promise that will resolve when the given list of expected
+ // events have all been seen. By default, succeeds only if the exact list
+ // of expected events is seen in the given order. options.exact can be
+ // set to false to allow other events and options.inorder can be set to
+ // false to allow the events to arrive in any order.
+ function waitForEvents(expected, options = {}) {
+ function compare(a, b) {
+ if (typeof b == "object" && b != null) {
+ if (typeof a != "object") {
+ return false;
+ }
+ return Object.keys(b).every(fld => compare(a[fld], b[fld]));
+ }
+ return (a == b);
+ }
+
+ const exact = ("exact" in options) ? options.exact : true;
+ const inorder = ("inorder" in options) ? options.inorder : true;
+ return new Promise((resolve, reject) => {
+ function check() {
+ function fail(msg) {
+ browser.test.fail(msg);
+ reject(new Error(msg));
+ }
+ if (events.size < expected.length) {
+ return;
+ }
+ if (exact && expected.length < events.size) {
+ fail(`Got ${events.size} events but only expected ${expected.length}`);
+ return;
+ }
+
+ let remaining = new Set(events);
+ if (inorder) {
+ for (let event of events) {
+ if (compare(event, expected[0])) {
+ expected.shift();
+ remaining.delete(event);
+ }
+ }
+ } else {
+ expected = expected.filter(val => {
+ for (let remainingEvent of remaining) {
+ if (compare(remainingEvent, val)) {
+ remaining.delete(remainingEvent);
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+
+ // Events that did occur have been removed from expected so if
+ // expected is empty, we're done. If we didn't see all the
+ // expected events and we're not looking for an exact match,
+ // then we just may not have seen the event yet, so return without
+ // failing and check() will be called again when a new event arrives.
+ if (expected.length == 0) {
+ events = remaining;
+ eventWaiter = null;
+ resolve();
+ } else if (exact) {
+ fail(`Mismatched event: expecting ${JSON.stringify(expected[0])} but got ${JSON.stringify(Array.from(remaining)[0])}`);
+ }
+ }
+ eventWaiter = check;
+ check();
+ });
+ }
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ let match = msg.match(/(\w+).request$/);
+ if (!match) {
+ return;
+ }
+
+ let what = match[1];
+ if (what == "waitForEvents") {
+ try {
+ await waitForEvents(...args);
+ browser.test.sendMessage("waitForEvents.done", {status: "success"});
+ } catch (error) {
+ browser.test.sendMessage("waitForEvents.done", {status: "error", errmsg: error.message});
+ }
+ } else if (what == "clearEvents") {
+ events = new Set();
+ browser.test.sendMessage("clearEvents.done", {status: "success"});
+ } else {
+ try {
+ let result = await browser.downloads[what](...args);
+ browser.test.sendMessage(`${what}.done`, {status: "success", result});
+ } catch (error) {
+ browser.test.sendMessage(`${what}.done`, {status: "error", errmsg: error.message});
+ }
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+let downloadDir;
+let extension;
+
+async function clearDownloads(callback) {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ await Promise.all(downloads.map(download => list.remove(download)));
+
+ return downloads;
+}
+
+function runInExtension(what, ...args) {
+ extension.sendMessage(`${what}.request`, ...args);
+ return extension.awaitMessage(`${what}.done`);
+}
+
+// This is pretty simplistic, it looks for a progress update for a
+// download of the given url in which the total bytes are exactly equal
+// to the given value. Unless you know exactly how data will arrive from
+// the server (eg see interruptible.sjs), it probably isn't very useful.
+async function waitForProgress(url, bytes) {
+ let list = await Downloads.getList(Downloads.ALL);
+
+ return new Promise(resolve => {
+ const view = {
+ onDownloadChanged(download) {
+ if (download.source.url == url && download.currentBytes == bytes) {
+ list.removeView(view);
+ resolve();
+ }
+ },
+ };
+ list.addView(view);
+ });
+}
+
+add_task(function* setup() {
+ const nsIFile = Ci.nsIFile;
+ downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_print(`downloadDir ${downloadDir.path}`);
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
+
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ downloadDir.remove(true);
+
+ return clearDownloads();
+ });
+
+ yield clearDownloads().then(downloads => {
+ do_print(`removed ${downloads.length} pre-existing downloads from history`);
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+});
+
+add_task(function* test_events() {
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id, url: TXT_URL}},
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onCreated and onChanged events");
+});
+
+add_task(function* test_cancel() {
+ let url = getInterruptibleUrl();
+ do_print(url);
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("cancel", id);
+ equal(msg.status, "success", "cancel() succeeded");
+
+ // This sequence of events is bogus (bug 1256243)
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ paused: {
+ previous: true,
+ current: false,
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events corresponding to cancel()");
+
+ msg = yield runInExtension("search", {error: "USER_CANCELED"});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause a canceled download");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "error", "cannot resume a canceled download");
+});
+
+add_task(function* test_pauseresume() {
+ let url = getInterruptibleUrl();
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = yield runInExtension("search", {paused: true});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, true, "download.paused is correct");
+ equal(msg.result[0].canResume, true, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = yield runInExtension("search", {error: "USER_CANCELED"});
+ equal(msg.status, "success", "search() succeeded");
+ let found = msg.result.filter(item => item.id == id);
+ equal(found.length, 1, "search() by error found the paused download");
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause an already paused download");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "success", "resume() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "interrupted",
+ current: "in_progress",
+ },
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ error: {
+ previous: "USER_CANCELED",
+ current: null,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events for resume and complete");
+
+ msg = yield runInExtension("search", {id});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].state, "complete", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, null, "download.error is correct");
+ equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, true, "download.exists is correct");
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "error", "cannot pause a completed download");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "error", "cannot resume a completed download");
+});
+
+add_task(function* test_pausecancel() {
+ let url = getInterruptibleUrl();
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = yield runInExtension("search", {paused: true});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].id, id, "download.id is correct");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, true, "download.paused is correct");
+ equal(msg.result[0].canResume, true, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+
+ msg = yield runInExtension("search", {error: "USER_CANCELED"});
+ equal(msg.status, "success", "search() succeeded");
+ let found = msg.result.filter(item => item.id == id);
+ equal(found.length, 1, "search() by error found the paused download");
+
+ msg = yield runInExtension("cancel", id);
+ equal(msg.status, "success", "cancel() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged event for cancel");
+
+ msg = yield runInExtension("search", {id});
+ equal(msg.status, "success", "search() succeeded");
+ equal(msg.result.length, 1, "search() found 1 download");
+ equal(msg.result[0].state, "interrupted", "download.state is correct");
+ equal(msg.result[0].paused, false, "download.paused is correct");
+ equal(msg.result[0].canResume, false, "download.canResume is correct");
+ equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
+ equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
+ equal(msg.result[0].exists, false, "download.exists is correct");
+});
+
+add_task(function* test_pause_resume_cancel_badargs() {
+ let BAD_ID = 1000;
+
+ let msg = yield runInExtension("pause", BAD_ID);
+ equal(msg.status, "error", "pause() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+
+ msg = yield runInExtension("resume", BAD_ID);
+ equal(msg.status, "error", "resume() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+
+ msg = yield runInExtension("cancel", BAD_ID);
+ equal(msg.status, "error", "cancel() failed with a bad download id");
+ ok(/Invalid download id/.test(msg.errmsg), "error message is descriptive");
+});
+
+add_task(function* test_file_removal() {
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id, url: TXT_URL}},
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+
+ equal(msg.status, "success", "got onCreated and onChanged events");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "error", "removeFile() fails since the file was already removed.");
+ ok(/file doesn't exist/.test(msg.errmsg), "removeFile() failed on removed file.");
+
+ msg = yield runInExtension("removeFile", 1000);
+ ok(/Invalid download id/.test(msg.errmsg), "removeFile() failed due to non-existent id");
+});
+
+add_task(function* test_removal_of_incomplete_download() {
+ let url = getInterruptibleUrl();
+ let msg = yield runInExtension("download", {url});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id}},
+ ]);
+ equal(msg.status, "success", "got created and changed events");
+
+ yield progressPromise;
+ do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
+
+ msg = yield runInExtension("pause", id);
+ equal(msg.status, "success", "pause() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "interrupted",
+ },
+ paused: {
+ previous: false,
+ current: true,
+ },
+ canResume: {
+ previous: false,
+ current: true,
+ },
+ },
+ }, {
+ type: "onChanged",
+ data: {
+ id,
+ error: {
+ previous: null,
+ current: "USER_CANCELED",
+ },
+ },
+ }]);
+ equal(msg.status, "success", "got onChanged event corresponding to pause");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "error", "removeFile() on paused download failed");
+
+ ok(/Cannot remove incomplete download/.test(msg.errmsg), "removeFile() failed due to download being incomplete");
+
+ msg = yield runInExtension("resume", id);
+ equal(msg.status, "success", "resume() succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "interrupted",
+ current: "in_progress",
+ },
+ paused: {
+ previous: true,
+ current: false,
+ },
+ canResume: {
+ previous: true,
+ current: false,
+ },
+ error: {
+ previous: "USER_CANCELED",
+ current: null,
+ },
+ },
+ },
+ {
+ type: "onChanged",
+ data: {
+ id,
+ state: {
+ previous: "in_progress",
+ current: "complete",
+ },
+ },
+ },
+ ]);
+ equal(msg.status, "success", "got onChanged events for resume and complete");
+
+ msg = yield runInExtension("removeFile", id);
+ equal(msg.status, "success", "removeFile() succeeded following completion of resumed download.");
+});
+
+// Test erase(). We don't do elaborate testing of the query handling
+// since it uses the exact same engine as search() which is tested
+// more thoroughly in test_chrome_ext_downloads_search.html
+add_task(function* test_erase() {
+ yield clearDownloads();
+
+ yield runInExtension("clearEvents");
+
+ function* download() {
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download succeeded");
+ let id = msg.result;
+
+ msg = yield runInExtension("waitForEvents", [{
+ type: "onChanged", data: {id, state: {current: "complete"}},
+ }], {exact: false});
+ equal(msg.status, "success", "download finished");
+
+ return id;
+ }
+
+ let ids = {};
+ ids.dl1 = yield download();
+ ids.dl2 = yield download();
+ ids.dl3 = yield download();
+
+ let msg = yield runInExtension("search", {});
+ equal(msg.status, "success", "search succeded");
+ equal(msg.result.length, 3, "search found 3 downloads");
+
+ msg = yield runInExtension("clearEvents");
+
+ msg = yield runInExtension("erase", {id: ids.dl1});
+ equal(msg.status, "success", "erase by id succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onErased", data: ids.dl1},
+ ]);
+ equal(msg.status, "success", "received onErased event");
+
+ msg = yield runInExtension("search", {});
+ equal(msg.status, "success", "search succeded");
+ equal(msg.result.length, 2, "search found 2 downloads");
+
+ msg = yield runInExtension("erase", {});
+ equal(msg.status, "success", "erase everything succeeded");
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onErased", data: ids.dl2},
+ {type: "onErased", data: ids.dl3},
+ ], {inorder: false});
+ equal(msg.status, "success", "received 2 onErased events");
+
+ msg = yield runInExtension("search", {});
+ equal(msg.status, "success", "search succeded");
+ equal(msg.result.length, 0, "search found 0 downloads");
+});
+
+function loadImage(img, data) {
+ return new Promise((resolve) => {
+ img.src = data;
+ img.onload = resolve;
+ });
+}
+
+add_task(function* test_getFileIcon() {
+ let webNav = Services.appShell.createWindowlessBrowser(false);
+ let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ let system = Services.scriptSecurityManager.getSystemPrincipal();
+ docShell.createAboutBlankContentViewer(system);
+
+ let img = webNav.document.createElement("img");
+
+ let msg = yield runInExtension("download", {url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ const id = msg.result;
+
+ msg = yield runInExtension("getFileIcon", id);
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 32, "returns an icon with the right height");
+ equal(img.width, 32, "returns an icon with the right width");
+
+ msg = yield runInExtension("waitForEvents", [
+ {type: "onCreated", data: {id, url: TXT_URL}},
+ {type: "onChanged"},
+ ]);
+ equal(msg.status, "success", "got events");
+
+ msg = yield runInExtension("getFileIcon", id);
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 32, "returns an icon with the right height after download");
+ equal(img.width, 32, "returns an icon with the right width after download");
+
+ msg = yield runInExtension("getFileIcon", id + 100);
+ equal(msg.status, "error", "getFileIcon() failed");
+ ok(msg.errmsg.includes("Invalid download id"), "download id is invalid");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 127});
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 127, "returns an icon with the right custom height");
+ equal(img.width, 127, "returns an icon with the right custom width");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 1});
+ equal(msg.status, "success", "getFileIcon() succeeded");
+ yield loadImage(img, msg.result);
+ equal(img.height, 1, "returns an icon with the right custom height");
+ equal(img.width, 1, "returns an icon with the right custom width");
+
+ msg = yield runInExtension("getFileIcon", id, {size: "foo"});
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is not a number");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 0});
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is too small");
+
+ msg = yield runInExtension("getFileIcon", id, {size: 128});
+ equal(msg.status, "error", "getFileIcon() fails");
+ ok(msg.errmsg.includes("Error processing size"), "size is too big");
+
+ webNav.close();
+});
+
+add_task(function* cleanup() {
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
new file mode 100644
index 0000000000..4caa82456f
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_search.js
@@ -0,0 +1,402 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE = `http://localhost:${server.identity.primaryPort}/data`;
+const TXT_FILE = "file_download.txt";
+const TXT_URL = BASE + "/" + TXT_FILE;
+const TXT_LEN = 46;
+const HTML_FILE = "file_download.html";
+const HTML_URL = BASE + "/" + HTML_FILE;
+const HTML_LEN = 117;
+const BIG_LEN = 1000; // something bigger both TXT_LEN and HTML_LEN
+
+function backgroundScript() {
+ let complete = new Map();
+
+ function waitForComplete(id) {
+ if (complete.has(id)) {
+ return complete.get(id).promise;
+ }
+
+ let promise = new Promise(resolve => {
+ complete.set(id, {resolve});
+ });
+ complete.get(id).promise = promise;
+ return promise;
+ }
+
+ browser.downloads.onChanged.addListener(change => {
+ if (change.state && change.state.current == "complete") {
+ // Make sure we have a promise.
+ waitForComplete(change.id);
+ complete.get(change.id).resolve();
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == "download.request") {
+ try {
+ let id = await browser.downloads.download(args[0]);
+ browser.test.sendMessage("download.done", {status: "success", id});
+ } catch (error) {
+ browser.test.sendMessage("download.done", {status: "error", errmsg: error.message});
+ }
+ } else if (msg == "search.request") {
+ try {
+ let downloads = await browser.downloads.search(args[0]);
+ browser.test.sendMessage("search.done", {status: "success", downloads});
+ } catch (error) {
+ browser.test.sendMessage("search.done", {status: "error", errmsg: error.message});
+ }
+ } else if (msg == "waitForComplete.request") {
+ await waitForComplete(args[0]);
+ browser.test.sendMessage("waitForComplete.done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+}
+
+async function clearDownloads(callback) {
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+
+ await Promise.all(downloads.map(download => list.remove(download)));
+
+ return downloads;
+}
+
+add_task(function* test_search() {
+ const nsIFile = Ci.nsIFile;
+ let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
+ downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_print(`downloadDir ${downloadDir.path}`);
+
+ function downloadPath(filename) {
+ let path = downloadDir.clone();
+ path.append(filename);
+ return path.path;
+ }
+
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
+
+ do_register_cleanup(async () => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ await cleanupDir(downloadDir);
+ await clearDownloads();
+ });
+
+ yield clearDownloads().then(downloads => {
+ do_print(`removed ${downloads.length} pre-existing downloads from history`);
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["downloads"],
+ },
+ });
+
+ async function download(options) {
+ extension.sendMessage("download.request", options);
+ let result = await extension.awaitMessage("download.done");
+
+ if (result.status == "success") {
+ do_print(`wait for onChanged event to indicate ${result.id} is complete`);
+ extension.sendMessage("waitForComplete.request", result.id);
+
+ await extension.awaitMessage("waitForComplete.done");
+ }
+
+ return result;
+ }
+
+ function search(query) {
+ extension.sendMessage("search.request", query);
+ return extension.awaitMessage("search.done");
+ }
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ // Do some downloads...
+ const time1 = new Date();
+
+ let downloadIds = {};
+ let msg = yield download({url: TXT_URL});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt1 = msg.id;
+
+ const TXT_FILE2 = "NewFile.txt";
+ msg = yield download({url: TXT_URL, filename: TXT_FILE2});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.txt2 = msg.id;
+
+ const time2 = new Date();
+
+ msg = yield download({url: HTML_URL});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.html1 = msg.id;
+
+ const HTML_FILE2 = "renamed.html";
+ msg = yield download({url: HTML_URL, filename: HTML_FILE2});
+ equal(msg.status, "success", "download() succeeded");
+ downloadIds.html2 = msg.id;
+
+ const time3 = new Date();
+
+ // Search for each individual download and check
+ // the corresponding DownloadItem.
+ function* checkDownloadItem(id, expect) {
+ let item = yield search({id});
+ equal(item.status, "success", "search() succeeded");
+ equal(item.downloads.length, 1, "search() found exactly 1 download");
+
+ Object.keys(expect).forEach(function(field) {
+ equal(item.downloads[0][field], expect[field], `DownloadItem.${field} is correct"`);
+ });
+ }
+ yield checkDownloadItem(downloadIds.txt1, {
+ url: TXT_URL,
+ filename: downloadPath(TXT_FILE),
+ mime: "text/plain",
+ state: "complete",
+ bytesReceived: TXT_LEN,
+ totalBytes: TXT_LEN,
+ fileSize: TXT_LEN,
+ exists: true,
+ });
+
+ yield checkDownloadItem(downloadIds.txt2, {
+ url: TXT_URL,
+ filename: downloadPath(TXT_FILE2),
+ mime: "text/plain",
+ state: "complete",
+ bytesReceived: TXT_LEN,
+ totalBytes: TXT_LEN,
+ fileSize: TXT_LEN,
+ exists: true,
+ });
+
+ yield checkDownloadItem(downloadIds.html1, {
+ url: HTML_URL,
+ filename: downloadPath(HTML_FILE),
+ mime: "text/html",
+ state: "complete",
+ bytesReceived: HTML_LEN,
+ totalBytes: HTML_LEN,
+ fileSize: HTML_LEN,
+ exists: true,
+ });
+
+ yield checkDownloadItem(downloadIds.html2, {
+ url: HTML_URL,
+ filename: downloadPath(HTML_FILE2),
+ mime: "text/html",
+ state: "complete",
+ bytesReceived: HTML_LEN,
+ totalBytes: HTML_LEN,
+ fileSize: HTML_LEN,
+ exists: true,
+ });
+
+ function* checkSearch(query, expected, description, exact) {
+ let item = yield search(query);
+ equal(item.status, "success", "search() succeeded");
+ equal(item.downloads.length, expected.length, `search() for ${description} found exactly ${expected.length} downloads`);
+
+ let receivedIds = item.downloads.map(i => i.id);
+ if (exact) {
+ receivedIds.forEach((id, idx) => {
+ equal(id, downloadIds[expected[idx]], `search() for ${description} returned ${expected[idx]} in position ${idx}`);
+ });
+ } else {
+ Object.keys(downloadIds).forEach(key => {
+ const id = downloadIds[key];
+ const thisExpected = expected.includes(key);
+ equal(receivedIds.includes(id), thisExpected,
+ `search() for ${description} ${thisExpected ? "includes" : "does not include"} ${key}`);
+ });
+ }
+ }
+
+ // Check that search with an invalid id returns nothing.
+ // NB: for now ids are not persistent and we start numbering them at 1
+ // so a sufficiently large number will be unused.
+ const INVALID_ID = 1000;
+ yield checkSearch({id: INVALID_ID}, [], "invalid id");
+
+ // Check that search on url works.
+ yield checkSearch({url: TXT_URL}, ["txt1", "txt2"], "url");
+
+ // Check that regexp on url works.
+ const HTML_REGEX = "[downlad]{8}\.html+$";
+ yield checkSearch({urlRegex: HTML_REGEX}, ["html1", "html2"], "url regexp");
+
+ // Check that compatible url+regexp works
+ yield checkSearch({url: HTML_URL, urlRegex: HTML_REGEX}, ["html1", "html2"], "compatible url+urlRegex");
+
+ // Check that incompatible url+regexp works
+ yield checkSearch({url: TXT_URL, urlRegex: HTML_REGEX}, [], "incompatible url+urlRegex");
+
+ // Check that search on filename works.
+ yield checkSearch({filename: downloadPath(TXT_FILE)}, ["txt1"], "filename");
+
+ // Check that regexp on filename works.
+ yield checkSearch({filenameRegex: HTML_REGEX}, ["html1"], "filename regex");
+
+ // Check that compatible filename+regexp works
+ yield checkSearch({filename: downloadPath(HTML_FILE), filenameRegex: HTML_REGEX}, ["html1"], "compatible filename+filename regex");
+
+ // Check that incompatible filename+regexp works
+ yield checkSearch({filename: downloadPath(TXT_FILE), filenameRegex: HTML_REGEX}, [], "incompatible filename+filename regex");
+
+ // Check that simple positive search terms work.
+ yield checkSearch({query: ["file_download"]}, ["txt1", "txt2", "html1", "html2"],
+ "term file_download");
+ yield checkSearch({query: ["NewFile"]}, ["txt2"], "term NewFile");
+
+ // Check that positive search terms work case-insensitive.
+ yield checkSearch({query: ["nEwfILe"]}, ["txt2"], "term nEwfiLe");
+
+ // Check that negative search terms work.
+ yield checkSearch({query: ["-txt"]}, ["html1", "html2"], "term -txt");
+
+ // Check that positive and negative search terms together work.
+ yield checkSearch({query: ["html", "-renamed"]}, ["html1"], "postive and negative terms");
+
+ function* checkSearchWithDate(query, expected, description) {
+ const fields = Object.keys(query);
+ if (fields.length != 1 || !(query[fields[0]] instanceof Date)) {
+ throw new Error("checkSearchWithDate expects exactly one Date field");
+ }
+ const field = fields[0];
+ const date = query[field];
+
+ let newquery = {};
+
+ // Check as a Date
+ newquery[field] = date;
+ yield checkSearch(newquery, expected, `${description} as Date`);
+
+ // Check as numeric milliseconds
+ newquery[field] = date.valueOf();
+ yield checkSearch(newquery, expected, `${description} as numeric ms`);
+
+ // Check as stringified milliseconds
+ newquery[field] = date.valueOf().toString();
+ yield checkSearch(newquery, expected, `${description} as string ms`);
+
+ // Check as ISO string
+ newquery[field] = date.toISOString();
+ yield checkSearch(newquery, expected, `${description} as iso string`);
+ }
+
+ // Check startedBefore
+ yield checkSearchWithDate({startedBefore: time1}, [], "before time1");
+ yield checkSearchWithDate({startedBefore: time2}, ["txt1", "txt2"], "before time2");
+ yield checkSearchWithDate({startedBefore: time3}, ["txt1", "txt2", "html1", "html2"], "before time3");
+
+ // Check startedAfter
+ yield checkSearchWithDate({startedAfter: time1}, ["txt1", "txt2", "html1", "html2"], "after time1");
+ yield checkSearchWithDate({startedAfter: time2}, ["html1", "html2"], "after time2");
+ yield checkSearchWithDate({startedAfter: time3}, [], "after time3");
+
+ // Check simple search on totalBytes
+ yield checkSearch({totalBytes: TXT_LEN}, ["txt1", "txt2"], "totalBytes");
+ yield checkSearch({totalBytes: HTML_LEN}, ["html1", "html2"], "totalBytes");
+
+ // Check simple test on totalBytes{Greater,Less}
+ // (NB: TXT_LEN < HTML_LEN < BIG_LEN)
+ yield checkSearch({totalBytesGreater: 0}, ["txt1", "txt2", "html1", "html2"], "totalBytesGreater than 0");
+ yield checkSearch({totalBytesGreater: TXT_LEN}, ["html1", "html2"], `totalBytesGreater than ${TXT_LEN}`);
+ yield checkSearch({totalBytesGreater: HTML_LEN}, [], `totalBytesGreater than ${HTML_LEN}`);
+ yield checkSearch({totalBytesLess: TXT_LEN}, [], `totalBytesLess than ${TXT_LEN}`);
+ yield checkSearch({totalBytesLess: HTML_LEN}, ["txt1", "txt2"], `totalBytesLess than ${HTML_LEN}`);
+ yield checkSearch({totalBytesLess: BIG_LEN}, ["txt1", "txt2", "html1", "html2"], `totalBytesLess than ${BIG_LEN}`);
+
+ // Check good combinations of totalBytes*.
+ yield checkSearch({totalBytes: HTML_LEN, totalBytesGreater: TXT_LEN}, ["html1", "html2"], "totalBytes and totalBytesGreater");
+ yield checkSearch({totalBytes: TXT_LEN, totalBytesLess: HTML_LEN}, ["txt1", "txt2"], "totalBytes and totalBytesGreater");
+ yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: BIG_LEN, totalBytesGreater: 0}, ["html1", "html2"], "totalBytes and totalBytesLess and totalBytesGreater");
+
+ // Check bad combination of totalBytes*.
+ yield checkSearch({totalBytesLess: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytesLess, totalBytesGreater combination");
+ yield checkSearch({totalBytes: TXT_LEN, totalBytesGreater: HTML_LEN}, [], "bad totalBytes, totalBytesGreater combination");
+ yield checkSearch({totalBytes: HTML_LEN, totalBytesLess: TXT_LEN}, [], "bad totalBytes, totalBytesLess combination");
+
+ // Check mime.
+ yield checkSearch({mime: "text/plain"}, ["txt1", "txt2"], "mime text/plain");
+ yield checkSearch({mime: "text/html"}, ["html1", "html2"], "mime text/htmlplain");
+ yield checkSearch({mime: "video/webm"}, [], "mime video/webm");
+
+ // Check fileSize.
+ yield checkSearch({fileSize: TXT_LEN}, ["txt1", "txt2"], "fileSize");
+ yield checkSearch({fileSize: HTML_LEN}, ["html1", "html2"], "fileSize");
+
+ // Fields like bytesReceived, paused, state, exists are meaningful
+ // for downloads that are in progress but have not yet completed.
+ // todo: add tests for these when we have better support for in-progress
+ // downloads (e.g., after pause(), resume() and cancel() are implemented)
+
+ // Check multiple query properties.
+ // We could make this testing arbitrarily complicated...
+ // We already tested combining fields with obvious interactions above
+ // (e.g., filename and filenameRegex or startTime and startedBefore/After)
+ // so now just throw as many fields as we can at a single search and
+ // make sure a simple case still works.
+ yield checkSearch({
+ url: TXT_URL,
+ urlRegex: "download",
+ filename: downloadPath(TXT_FILE),
+ filenameRegex: "download",
+ query: ["download"],
+ startedAfter: time1.valueOf().toString(),
+ startedBefore: time2.valueOf().toString(),
+ totalBytes: TXT_LEN,
+ totalBytesGreater: 0,
+ totalBytesLess: BIG_LEN,
+ mime: "text/plain",
+ fileSize: TXT_LEN,
+ }, ["txt1"], "many properties");
+
+ // Check simple orderBy (forward and backward).
+ yield checkSearch({orderBy: ["startTime"]}, ["txt1", "txt2", "html1", "html2"], "orderBy startTime", true);
+ yield checkSearch({orderBy: ["-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy -startTime", true);
+
+ // Check orderBy with multiple fields.
+ // NB: TXT_URL and HTML_URL differ only in extension and .html precedes .txt
+ yield checkSearch({orderBy: ["url", "-startTime"]}, ["html2", "html1", "txt2", "txt1"], "orderBy with multiple fields", true);
+
+ // Check orderBy with limit.
+ yield checkSearch({orderBy: ["url"], limit: 1}, ["html1"], "orderBy with limit", true);
+
+ // Check bad arguments.
+ function* checkBadSearch(query, pattern, description) {
+ let item = yield search(query);
+ equal(item.status, "error", "search() failed");
+ ok(pattern.test(item.errmsg), `error message for ${description} was correct (${item.errmsg}).`);
+ }
+
+ yield checkBadSearch("myquery", /Incorrect argument type/, "query is not an object");
+ yield checkBadSearch({bogus: "boo"}, /Unexpected property/, "query contains an unknown field");
+ yield checkBadSearch({query: "query string"}, /Expected array/, "query.query is a string");
+ yield checkBadSearch({startedBefore: "i am not a time"}, /Type error/, "query.startedBefore is not a valid time");
+ yield checkBadSearch({startedAfter: "i am not a time"}, /Type error/, "query.startedAfter is not a valid time");
+ yield checkBadSearch({endedBefore: "i am not a time"}, /Type error/, "query.endedBefore is not a valid time");
+ yield checkBadSearch({endedAfter: "i am not a time"}, /Type error/, "query.endedAfter is not a valid time");
+ yield checkBadSearch({urlRegex: "["}, /Invalid urlRegex/, "query.urlRegexp is not a valid regular expression");
+ yield checkBadSearch({filenameRegex: "["}, /Invalid filenameRegex/, "query.filenameRegexp is not a valid regular expression");
+ yield checkBadSearch({orderBy: "startTime"}, /Expected array/, "query.orderBy is not an array");
+ yield checkBadSearch({orderBy: ["bogus"]}, /Invalid orderBy field/, "query.orderBy references a non-existent field");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
new file mode 100644
index 0000000000..bc6bfcd68b
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -0,0 +1,175 @@
+"use strict";
+
+/* globals browser */
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+function promiseAddonStartup() {
+ const {Management} = Cu.import("resource://gre/modules/Extension.jsm");
+
+ return new Promise(resolve => {
+ let listener = (evt, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+add_task(function* setup() {
+ yield ExtensionTestUtils.startAddonManager();
+});
+
+add_task(function* test_experiments_api() {
+ let apiAddonFile = Extension.generateZipFile({
+ "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="meh@experiments.addons.mozilla.org"
+ em:name="Meh Experiment"
+ em:type="256"
+ em:version="0.1"
+ em:description="Meh experiment"
+ em:creator="Mozilla">
+
+ <em:targetApplication>
+ <Description
+ em:id="xpcshell@tests.mozilla.org"
+ em:minVersion="48"
+ em:maxVersion="*"/>
+ </em:targetApplication>
+ </Description>
+ </RDF>
+ `,
+
+ "api.js": String.raw`
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ Services.obs.notifyObservers(null, "webext-api-loaded", "");
+
+ class API extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ meh: {
+ hello(text) {
+ Services.obs.notifyObservers(null, "webext-api-hello", text);
+ }
+ }
+ }
+ }
+ }
+ `,
+
+ "schema.json": [
+ {
+ "namespace": "meh",
+ "description": "All full of meh.",
+ "permissions": ["experiments.meh"],
+ "functions": [
+ {
+ "name": "hello",
+ "type": "function",
+ "description": "Hates you. This is all.",
+ "parameters": [
+ {"type": "string", "name": "text"},
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ let addonFile = Extension.generateXPI({
+ manifest: {
+ applications: {gecko: {id: "meh@web.extension"}},
+ permissions: ["experiments.meh"],
+ },
+
+ background() {
+ // The test code below checks that hello() is called at the right
+ // time with the string "Here I am". Verify that the api schema is
+ // being correctly interpreted by calling hello() with bad arguments
+ // and only calling hello() with the magic string if the call with
+ // bad arguments throws.
+ try {
+ browser.meh.hello("I should not see this", "since two arguments are bad");
+ } catch (err) {
+ browser.meh.hello("Here I am");
+ }
+ },
+ });
+
+ let boringAddonFile = Extension.generateXPI({
+ manifest: {
+ applications: {gecko: {id: "boring@web.extension"}},
+ },
+ background() {
+ if (browser.meh) {
+ browser.meh.hello("Here I should not be");
+ }
+ },
+ });
+
+ do_register_cleanup(() => {
+ for (let file of [apiAddonFile, addonFile, boringAddonFile]) {
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+ file.remove(false);
+ }
+ });
+
+
+ let resolveHello;
+ let observer = (subject, topic, data) => {
+ if (topic == "webext-api-loaded") {
+ ok(!!resolveHello, "Should not see API loaded until dependent extension loads");
+ } else if (topic == "webext-api-hello") {
+ resolveHello(data);
+ }
+ };
+
+ Services.obs.addObserver(observer, "webext-api-loaded", false);
+ Services.obs.addObserver(observer, "webext-api-hello", false);
+ do_register_cleanup(() => {
+ Services.obs.removeObserver(observer, "webext-api-loaded");
+ Services.obs.removeObserver(observer, "webext-api-hello");
+ });
+
+
+ // Install API add-on.
+ let apiAddon = yield AddonManager.installTemporaryAddon(apiAddonFile);
+
+ let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
+ ok(APIs.apis.has("meh"), "Should have meh API.");
+
+
+ // Install boring WebExtension add-on.
+ let boringAddon = yield AddonManager.installTemporaryAddon(boringAddonFile);
+ yield promiseAddonStartup();
+
+
+ // Install interesting WebExtension add-on.
+ let promise = new Promise(resolve => {
+ resolveHello = resolve;
+ });
+
+ let addon = yield AddonManager.installTemporaryAddon(addonFile);
+ yield promiseAddonStartup();
+
+ let hello = yield promise;
+ equal(hello, "Here I am", "Should get hello from add-on");
+
+ // Cleanup.
+ apiAddon.uninstall();
+
+ boringAddon.userDisabled = true;
+ yield new Promise(do_execute_soon);
+
+ equal(addon.appDisabled, true, "Add-on should be app-disabled after its dependency is removed.");
+
+ addon.uninstall();
+ boringAddon.uninstall();
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_extension.js b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js
new file mode 100644
index 0000000000..f18845f6a4
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js
@@ -0,0 +1,55 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_is_allowed_incognito_access() {
+ async function background() {
+ let allowed = await browser.extension.isAllowedIncognitoAccess();
+
+ browser.test.assertEq(true, allowed, "isAllowedIncognitoAccess is true");
+ browser.test.notifyPass("isAllowedIncognitoAccess");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("isAllowedIncognitoAccess");
+ yield extension.unload();
+});
+
+add_task(function* test_in_incognito_context_false() {
+ function background() {
+ browser.test.assertEq(false, browser.extension.inIncognitoContext, "inIncognitoContext returned false");
+ browser.test.notifyPass("inIncognitoContext");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("inIncognitoContext");
+ yield extension.unload();
+});
+
+add_task(function* test_is_allowed_file_scheme_access() {
+ async function background() {
+ let allowed = await browser.extension.isAllowedFileSchemeAccess();
+
+ browser.test.assertEq(false, allowed, "isAllowedFileSchemeAccess is false");
+ browser.test.notifyPass("isAllowedFileSchemeAccess");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("isAllowedFileSchemeAccess");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_idle.js b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
new file mode 100644
index 0000000000..89bcac2172
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
@@ -0,0 +1,202 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+let idleService = {
+ _observers: new Set(),
+ _activity: {
+ addCalls: [],
+ removeCalls: [],
+ observerFires: [],
+ },
+ _reset: function() {
+ this._observers.clear();
+ this._activity.addCalls = [];
+ this._activity.removeCalls = [];
+ this._activity.observerFires = [];
+ },
+ _fireObservers: function(state) {
+ for (let observer of this._observers.values()) {
+ observer.observe(observer, state, null);
+ this._activity.observerFires.push(state);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIIdleService]),
+ idleTime: 19999,
+ addIdleObserver: function(observer, time) {
+ this._observers.add(observer);
+ this._activity.addCalls.push(time);
+ },
+ removeIdleObserver: function(observer, time) {
+ this._observers.delete(observer);
+ this._activity.removeCalls.push(time);
+ },
+};
+
+function checkActivity(expectedActivity) {
+ let {expectedAdd, expectedRemove, expectedFires} = expectedActivity;
+ let {addCalls, removeCalls, observerFires} = idleService._activity;
+ equal(expectedAdd.length, addCalls.length, "idleService.addIdleObserver was called the expected number of times");
+ equal(expectedRemove.length, removeCalls.length, "idleService.removeIdleObserver was called the expected number of times");
+ equal(expectedFires.length, observerFires.length, "idle observer was fired the expected number of times");
+ deepEqual(addCalls, expectedAdd, "expected interval passed to idleService.addIdleObserver");
+ deepEqual(removeCalls, expectedRemove, "expected interval passed to idleService.removeIdleObserver");
+ deepEqual(observerFires, expectedFires, "expected topic passed to idle observer");
+}
+
+add_task(function* setup() {
+ let fakeIdleService = MockRegistrar.register("@mozilla.org/widget/idleservice;1", idleService);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(fakeIdleService);
+ });
+});
+
+add_task(function* testQueryStateActive() {
+ function background() {
+ browser.idle.queryState(20).then(status => {
+ browser.test.assertEq("active", status, "Idle status is active");
+ browser.test.notifyPass("idle");
+ },
+ err => {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("idle");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+add_task(function* testQueryStateIdle() {
+ function background() {
+ browser.idle.queryState(15).then(status => {
+ browser.test.assertEq("idle", status, "Idle status is idle");
+ browser.test.notifyPass("idle");
+ },
+ err => {
+ browser.test.fail(`Error: ${err} :: ${err.stack}`);
+ browser.test.notifyFail("idle");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("idle");
+ yield extension.unload();
+});
+
+add_task(function* testOnlySetDetectionInterval() {
+ function background() {
+ browser.idle.setDetectionInterval(99);
+ browser.test.sendMessage("detectionIntervalSet");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("detectionIntervalSet");
+ idleService._fireObservers("idle");
+ checkActivity({expectedAdd: [], expectedRemove: [], expectedFires: []});
+ yield extension.unload();
+});
+
+add_task(function* testSetDetectionIntervalBeforeAddingListener() {
+ function background() {
+ browser.idle.setDetectionInterval(99);
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("listenerAdded");
+ idleService._fireObservers("idle");
+ yield extension.awaitMessage("listenerFired");
+ checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]});
+ yield extension.unload();
+});
+
+add_task(function* testSetDetectionIntervalAfterAddingListener() {
+ function background() {
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("idle", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.idle.setDetectionInterval(99);
+ browser.test.sendMessage("detectionIntervalSet");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("detectionIntervalSet");
+ idleService._fireObservers("idle");
+ yield extension.awaitMessage("listenerFired");
+ checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]});
+ yield extension.unload();
+});
+
+add_task(function* testOnlyAddingListener() {
+ function background() {
+ browser.idle.onStateChanged.addListener(newState => {
+ browser.test.assertEq("active", newState, "listener fired with the expected state");
+ browser.test.sendMessage("listenerFired");
+ });
+ browser.test.sendMessage("listenerAdded");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["idle"],
+ },
+ });
+
+ idleService._reset();
+ yield extension.startup();
+ yield extension.awaitMessage("listenerAdded");
+ idleService._fireObservers("active");
+ yield extension.awaitMessage("listenerFired");
+ // check that "idle-daily" topic does not cause a listener to fire
+ idleService._fireObservers("idle-daily");
+ checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]});
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
new file mode 100644
index 0000000000..652f413156
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
@@ -0,0 +1,37 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_json_parser() {
+ const ID = "json@test.web.extension";
+
+ let xpi = Extension.generateXPI({
+ files: {
+ "manifest.json": String.raw`{
+ // This is a manifest.
+ "applications": {"gecko": {"id": "${ID}"}},
+ "name": "This \" is // not a comment",
+ "version": "0.1\\" // , "description": "This is not a description"
+ }`,
+ },
+ });
+
+ let expectedManifest = {
+ "applications": {"gecko": {"id": ID}},
+ "name": "This \" is // not a comment",
+ "version": "0.1\\",
+ };
+
+ let fileURI = Services.io.newFileURI(xpi);
+ let uri = NetUtil.newURI(`jar:${fileURI.spec}!/`);
+
+ let extension = new ExtensionData(uri);
+
+ yield extension.readManifest();
+
+ Assert.deepEqual(extension.rawManifest, expectedManifest,
+ "Manifest with correctly-filtered comments");
+
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
new file mode 100644
index 0000000000..63d5361a1e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
@@ -0,0 +1,168 @@
+"use strict";
+
+/* globals browser */
+
+Cu.import("resource://gre/modules/Extension.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {LegacyExtensionContext} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+/**
+ * This test case ensures that LegacyExtensionContext instances:
+ * - expose the expected API object and can join the messaging
+ * of a webextension given its addon id
+ * - the exposed API object can receive a port related to a `runtime.connect`
+ * Port created in the webextension's background page
+ * - the received Port instance can exchange messages with the background page
+ * - the received Port receive a disconnect event when the webextension is
+ * shutting down
+ */
+add_task(function* test_legacy_extension_context() {
+ function background() {
+ let bgURL = window.location.href;
+
+ let extensionInfo = {
+ bgURL,
+ // Extract the assigned uuid from the background page url.
+ uuid: window.location.hostname,
+ };
+
+ browser.test.sendMessage("webextension-ready", extensionInfo);
+
+ let port;
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "do-send-message") {
+ let reply = await browser.runtime.sendMessage("webextension -> legacy_extension message");
+
+ browser.test.assertEq("legacy_extension -> webextension reply", reply,
+ "Got the expected message from the LegacyExtensionContext");
+ browser.test.sendMessage("got-reply-message");
+ } else if (msg == "do-connect") {
+ port = browser.runtime.connect();
+
+ port.onMessage.addListener(portMsg => {
+ browser.test.assertEq("legacy_extension -> webextension port message", portMsg,
+ "Got the expected message from the LegacyExtensionContext");
+ port.postMessage("webextension -> legacy_extension port message");
+ });
+ } else if (msg == "do-disconnect") {
+ port.disconnect();
+ }
+ });
+ }
+
+ let extensionData = {
+ background,
+ };
+
+ let extension = Extension.generate(extensionData);
+
+ let waitForExtensionInfo = new Promise((resolve, reject) => {
+ extension.on("test-message", function testMessageListener(kind, msg, ...args) {
+ if (msg != "webextension-ready") {
+ reject(new Error(`Got an unexpected test-message: ${msg}`));
+ } else {
+ extension.off("test-message", testMessageListener);
+ resolve(args[0]);
+ }
+ });
+ });
+
+ // Connect to the target extension as an external context
+ // using the given custom sender info.
+ let legacyContext;
+
+ extension.on("startup", function onStartup() {
+ extension.off("startup", onStartup);
+ legacyContext = new LegacyExtensionContext(extension);
+ extension.callOnClose({
+ close: () => legacyContext.unload(),
+ });
+ });
+
+ yield extension.startup();
+
+ let extensionInfo = yield waitForExtensionInfo;
+
+ equal(legacyContext.envType, "legacy_extension",
+ "LegacyExtensionContext instance has the expected type");
+
+ ok(legacyContext.api, "Got the expected API object");
+ ok(legacyContext.api.browser, "Got the expected browser property");
+
+ let waitMessage = new Promise(resolve => {
+ const {browser} = legacyContext.api;
+ browser.runtime.onMessage.addListener((singleMsg, msgSender) => {
+ resolve({singleMsg, msgSender});
+
+ // Send a reply to the sender.
+ return Promise.resolve("legacy_extension -> webextension reply");
+ });
+ });
+
+ extension.testMessage("do-send-message");
+
+ let {singleMsg, msgSender} = yield waitMessage;
+ equal(singleMsg, "webextension -> legacy_extension message",
+ "Got the expected message");
+ ok(msgSender, "Got a message sender object");
+
+ equal(msgSender.id, extensionInfo.uuid, "The sender has the expected id property");
+ equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property");
+
+ // Wait confirmation that the reply has been received.
+ yield new Promise((resolve, reject) => {
+ extension.on("test-message", function testMessageListener(kind, msg, ...args) {
+ if (msg != "got-reply-message") {
+ reject(new Error(`Got an unexpected test-message: ${msg}`));
+ } else {
+ extension.off("test-message", testMessageListener);
+ resolve();
+ }
+ });
+ });
+
+ let waitConnectPort = new Promise(resolve => {
+ let {browser} = legacyContext.api;
+ browser.runtime.onConnect.addListener(port => {
+ resolve(port);
+ });
+ });
+
+ extension.testMessage("do-connect");
+
+ let port = yield waitConnectPort;
+
+ ok(port, "Got the Port API object");
+ ok(port.sender, "The port has a sender property");
+ equal(port.sender.id, extensionInfo.uuid,
+ "The port sender has the expected id property");
+ equal(port.sender.url, extensionInfo.bgURL,
+ "The port sender has the expected url property");
+
+ let waitPortMessage = new Promise(resolve => {
+ port.onMessage.addListener((msg) => {
+ resolve(msg);
+ });
+ });
+
+ port.postMessage("legacy_extension -> webextension port message");
+
+ let msg = yield waitPortMessage;
+
+ equal(msg, "webextension -> legacy_extension port message",
+ "LegacyExtensionContext received the expected message from the webextension");
+
+ let waitForDisconnect = new Promise(resolve => {
+ port.onDisconnect.addListener(resolve);
+ });
+
+ extension.testMessage("do-disconnect");
+
+ yield waitForDisconnect;
+
+ do_print("Got the disconnect event on unload");
+
+ yield extension.shutdown();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js
new file mode 100644
index 0000000000..ea5d785240
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_embedding.js
@@ -0,0 +1,188 @@
+"use strict";
+
+/* globals browser */
+
+Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+// Import EmbeddedExtensionManager to be able to check that the
+// tacked instances are cleared after the embedded extension shutdown.
+const {
+ EmbeddedExtensionManager,
+} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {});
+
+/**
+ * This test case ensures that the LegacyExtensionsUtils.EmbeddedExtension:
+ * - load the embedded webextension resources from a "/webextension/" dir
+ * inside the XPI.
+ * - EmbeddedExtension.prototype.api returns an API object which exposes
+ * a working `runtime.onConnect` event object (e.g. the API can receive a port
+ * when the embedded webextension is started and it can exchange messages
+ * with the background page).
+ * - EmbeddedExtension.prototype.startup/shutdown methods manage the embedded
+ * webextension lifecycle as expected.
+ */
+add_task(function* test_embedded_webextension_utils() {
+ function backgroundScript() {
+ let port = browser.runtime.connect();
+
+ port.onMessage.addListener((msg) => {
+ if (msg == "legacy_extension -> webextension") {
+ port.postMessage("webextension -> legacy_extension");
+ port.disconnect();
+ }
+ });
+ }
+
+ const id = "@test.embedded.web.extension";
+
+ // Extensions.generateXPI is used here (and in the other hybrid addons tests in this same
+ // test dir) to be able to generate an xpi with the directory layout that we expect from
+ // an hybrid legacy+webextension addon (where all the embedded webextension resources are
+ // loaded from a 'webextension/' directory).
+ let fakeHybridAddonFile = Extension.generateZipFile({
+ "webextension/manifest.json": {
+ applications: {gecko: {id}},
+ name: "embedded webextension name",
+ manifest_version: 2,
+ version: "1.0",
+ background: {
+ scripts: ["bg.js"],
+ },
+ },
+ "webextension/bg.js": `new ${backgroundScript}`,
+ });
+
+ // Remove the generated xpi file and flush the its jar cache
+ // on cleanup.
+ do_register_cleanup(() => {
+ Services.obs.notifyObservers(fakeHybridAddonFile, "flush-cache-entry", null);
+ fakeHybridAddonFile.remove(false);
+ });
+
+ let fileURI = Services.io.newFileURI(fakeHybridAddonFile);
+ let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null);
+
+ let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
+ id, resourceURI,
+ });
+
+ ok(embeddedExtension, "Got the embeddedExtension object");
+
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked embedded extension instances");
+
+ do_print("waiting embeddedExtension.startup");
+ let embeddedExtensionAPI = yield embeddedExtension.startup();
+ ok(embeddedExtensionAPI, "Got the embeddedExtensionAPI object");
+
+ let waitConnectPort = new Promise(resolve => {
+ let {browser} = embeddedExtensionAPI;
+ browser.runtime.onConnect.addListener(port => {
+ resolve(port);
+ });
+ });
+
+ let port = yield waitConnectPort;
+
+ ok(port, "Got the Port API object");
+
+ let waitPortMessage = new Promise(resolve => {
+ port.onMessage.addListener((msg) => {
+ resolve(msg);
+ });
+ });
+
+ port.postMessage("legacy_extension -> webextension");
+
+ let msg = yield waitPortMessage;
+
+ equal(msg, "webextension -> legacy_extension",
+ "LegacyExtensionContext received the expected message from the webextension");
+
+ let waitForDisconnect = new Promise(resolve => {
+ port.onDisconnect.addListener(resolve);
+ });
+
+ do_print("Wait for the disconnect port event");
+ yield waitForDisconnect;
+ do_print("Got the disconnect port event");
+
+ yield embeddedExtension.shutdown();
+
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "EmbeddedExtension instances has been untracked from the EmbeddedExtensionManager");
+});
+
+function* createManifestErrorTestCase(id, xpi, expectedError) {
+ // Remove the generated xpi file and flush the its jar cache
+ // on cleanup.
+ do_register_cleanup(() => {
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+ });
+
+ let fileURI = Services.io.newFileURI(xpi);
+ let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`, null, null);
+
+ let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
+ id, resourceURI,
+ });
+
+ yield Assert.rejects(embeddedExtension.startup(), expectedError,
+ "embedded extension startup rejected");
+
+ // Shutdown a "never-started" addon with an embedded webextension should not
+ // raise any exception, and if it does this test will fail.
+ yield embeddedExtension.shutdown();
+}
+
+add_task(function* test_startup_error_empty_manifest() {
+ const id = "empty-manifest@test.embedded.web.extension";
+ const files = {
+ "webextension/manifest.json": ``,
+ };
+ const expectedError = "(NS_BASE_STREAM_CLOSED)";
+
+ let fakeHybridAddonFile = Extension.generateZipFile(files);
+
+ yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
+});
+
+add_task(function* test_startup_error_invalid_json_manifest() {
+ const id = "invalid-json-manifest@test.embedded.web.extension";
+ const files = {
+ "webextension/manifest.json": `{ "name": }`,
+ };
+ const expectedError = "JSON.parse:";
+
+ let fakeHybridAddonFile = Extension.generateZipFile(files);
+
+ yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
+});
+
+add_task(function* test_startup_error_blocking_validation_errors() {
+ const id = "blocking-manifest-validation-error@test.embedded.web.extension";
+ const files = {
+ "webextension/manifest.json": {
+ name: "embedded webextension name",
+ manifest_version: 2,
+ version: "1.0",
+ background: {
+ scripts: {},
+ },
+ },
+ };
+
+ function expectedError(actual) {
+ if (actual.errors && actual.errors.length == 1 &&
+ actual.errors[0].startsWith("Reading manifest:")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ let fakeHybridAddonFile = Extension.generateZipFile(files);
+
+ yield createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
new file mode 100644
index 0000000000..0f0b410854
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
@@ -0,0 +1,50 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ let hasRun = localStorage.getItem("has-run");
+ let result;
+ if (!hasRun) {
+ localStorage.setItem("has-run", "yup");
+ localStorage.setItem("test-item", "item1");
+ result = "item1";
+ } else {
+ let data = localStorage.getItem("test-item");
+ if (data == "item1") {
+ localStorage.setItem("test-item", "item2");
+ result = "item2";
+ } else if (data == "item2") {
+ localStorage.removeItem("test-item");
+ result = "deleted";
+ } else if (!data) {
+ localStorage.clear();
+ result = "cleared";
+ }
+ }
+ browser.test.sendMessage("result", result);
+ browser.test.notifyPass("localStorage");
+}
+
+const ID = "test-webextension@mozilla.com";
+let extensionData = {
+ manifest: {applications: {gecko: {id: ID}}},
+ background: backgroundScript,
+};
+
+add_task(function* test_localStorage() {
+ const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"];
+
+ for (let expected of RESULTS) {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+
+ let actual = yield extension.awaitMessage("result");
+
+ yield extension.awaitFinish("localStorage");
+ yield extension.unload();
+
+ equal(actual, expected, "got expected localStorage data");
+ }
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_management.js b/toolkit/components/extensions/test/xpcshell/test_ext_management.js
new file mode 100644
index 0000000000..b19554a57e
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management.js
@@ -0,0 +1,20 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_management_schema() {
+ function background() {
+ browser.test.assertTrue(browser.management, "browser.management API exists");
+ browser.test.notifyPass("management-schema");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["management"],
+ },
+ background: `(${background})()`,
+ });
+ yield extension.startup();
+ yield extension.awaitFinish("management-schema");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
new file mode 100644
index 0000000000..7d80a9c239
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
@@ -0,0 +1,135 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://testing-common/AddonTestUtils.jsm");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+const {promiseAddonByID} = AddonTestUtils;
+const id = "uninstall_self_test@tests.mozilla.com";
+
+const manifest = {
+ applications: {
+ gecko: {
+ id,
+ },
+ },
+ name: "test extension name",
+ version: "1.0",
+};
+
+const waitForUninstalled = () => new Promise(resolve => {
+ const listener = {
+ onUninstalled: (addon) => {
+ equal(addon.id, id, "The expected add-on has been uninstalled");
+ AddonManager.getAddonByID(addon.id, checkedAddon => {
+ equal(checkedAddon, null, "Add-on no longer exists");
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ });
+ },
+ };
+ AddonManager.addAddonListener(listener);
+});
+
+let promptService = {
+ _response: null,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: function(...args) {
+ this._confirmExArgs = args;
+ return this._response;
+ },
+};
+
+add_task(function* setup() {
+ let fakePromptService = MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(fakePromptService);
+ });
+ yield ExtensionTestUtils.startAddonManager();
+});
+
+add_task(function* test_management_uninstall_no_prompt() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf();
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield waitForUninstalled();
+ yield extension.markUnloaded();
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
+
+add_task(function* test_management_uninstall_prompt_uninstall() {
+ promptService._response = 0;
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf({showConfirmDialog: true});
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield waitForUninstalled();
+ yield extension.markUnloaded();
+
+ // Test localization strings
+ equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`);
+ equal(promptService._confirmExArgs[2],
+ `The extension “${manifest.name}” is requesting to be uninstalled. What would you like to do?`);
+ equal(promptService._confirmExArgs[4], "Uninstall");
+ equal(promptService._confirmExArgs[5], "Keep Installed");
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
+
+add_task(function* test_management_uninstall_prompt_keep() {
+ promptService._response = 1;
+
+ function background() {
+ browser.test.onMessage.addListener(async msg => {
+ await browser.test.assertRejects(
+ browser.management.uninstallSelf({showConfirmDialog: true}),
+ "User cancelled uninstall of extension",
+ "Expected rejection when user declines uninstall");
+
+ browser.test.sendMessage("uninstall-rejected");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield extension.awaitMessage("uninstall-rejected");
+ addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on remains installed");
+ yield extension.unload();
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js
new file mode 100644
index 0000000000..2b0084980c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_content_security_policy.js
@@ -0,0 +1,30 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_manifest_csp() {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "content_security_policy": "script-src 'self'; object-src 'none'",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ equal(normalized.value.content_security_policy,
+ "script-src 'self'; object-src 'none'",
+ "Should have the expected poilcy string");
+
+
+ normalized = yield ExtensionTestUtils.normalizeManifest({
+ "content_security_policy": "object-src 'none'",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+
+ Assert.deepEqual(normalized.errors,
+ ["Error processing content_security_policy: SyntaxError: Policy is missing a required \u2018script-src\u2019 directive"],
+ "Should have the expected warning");
+
+ equal(normalized.value.content_security_policy, null,
+ "Invalid policy string should be omitted");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
new file mode 100644
index 0000000000..94649692e9
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js
@@ -0,0 +1,27 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_manifest_incognito() {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "incognito": "spanning",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ equal(normalized.value.incognito,
+ "spanning",
+ "Should have the expected incognito string");
+
+ normalized = yield ExtensionTestUtils.normalizeManifest({
+ "incognito": "split",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ Assert.deepEqual(normalized.errors,
+ ['Error processing incognito: Invalid enumeration value "split"'],
+ "Should have the expected warning");
+ equal(normalized.value.incognito, null,
+ "Invalid incognito string should be omitted");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js
new file mode 100644
index 0000000000..fad5661bba
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_minimum_chrome_version.js
@@ -0,0 +1,13 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+
+add_task(function* test_manifest_minimum_chrome_version() {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "minimum_chrome_version": "42",
+ });
+
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
new file mode 100644
index 0000000000..5a6b628f5c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -0,0 +1,514 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
+const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
+
+const ECHO_BODY = String.raw`
+ import struct
+ import sys
+
+ while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ sys.exit(0)
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+`;
+
+const INFO_BODY = String.raw`
+ import json
+ import os
+ import struct
+ import sys
+
+ msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
+ sys.stdout.write(struct.pack('@I', len(msg)))
+ sys.stdout.write(msg)
+ sys.exit(0)
+`;
+
+const STDERR_LINES = ["hello stderr", "this should be a separate line"];
+let STDERR_MSG = STDERR_LINES.join("\\n");
+
+const STDERR_BODY = String.raw`
+ import sys
+ sys.stderr.write("${STDERR_MSG}")
+`;
+
+const SCRIPTS = [
+ {
+ name: "echo",
+ description: "a native app that echoes back messages it receives",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "info",
+ description: "a native app that gives some info about how it was started",
+ script: INFO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "stderr",
+ description: "a native app that writes to stderr and then exits",
+ script: STDERR_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+add_task(function* setup() {
+ yield setupHosts(SCRIPTS);
+});
+
+// Test the basic operation of native messaging with a simple
+// script that echoes back whatever message is sent to it.
+add_task(function* test_happy_path() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("message", msg);
+ });
+ browser.test.onMessage.addListener((what, payload) => {
+ if (what == "send") {
+ if (payload._json) {
+ let json = payload._json;
+ payload.toJSON = () => json;
+ delete payload._json;
+ }
+ port.postMessage(payload);
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+ const tests = [
+ {
+ data: "this is a string",
+ what: "simple string",
+ },
+ {
+ data: "Это юникода",
+ what: "unicode string",
+ },
+ {
+ data: {test: "hello"},
+ what: "simple object",
+ },
+ {
+ data: {
+ what: "An object with a few properties",
+ number: 123,
+ bool: true,
+ nested: {what: "another object"},
+ },
+ what: "object with several properties",
+ },
+
+ {
+ data: {
+ ignoreme: true,
+ _json: {data: "i have a tojson method"},
+ },
+ expected: {data: "i have a tojson method"},
+ what: "object with toJSON() method",
+ },
+ ];
+ for (let test of tests) {
+ extension.sendMessage("send", test.data);
+ let response = yield extension.awaitMessage("message");
+ let expected = test.expected || test.data;
+ deepEqual(response, expected, `Echoed a message of type ${test.what}`);
+ }
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is still running");
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+});
+
+if (AppConstants.platform == "win") {
+ // "relative.echo" has a relative path in the host manifest.
+ add_task(function* test_relative_path() {
+ function background() {
+ let port = browser.runtime.connectNative("relative.echo");
+ let MSG = "test relative echo path";
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(MSG, msg, "Got expected message back");
+ browser.test.sendMessage("done");
+ });
+ port.postMessage(MSG);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is still running");
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+ });
+}
+
+// Test sendNativeMessage()
+add_task(function* test_sendNativeMessage() {
+ async function background() {
+ let MSG = {test: "hello world"};
+
+ // Check error handling
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage("nonexistent", MSG),
+ /Attempt to postMessage on disconnected port/,
+ "sendNativeMessage() to a nonexistent app failed");
+
+ // Check regular message exchange
+ let reply = await browser.runtime.sendNativeMessage("echo", MSG);
+
+ let expected = JSON.stringify(MSG);
+ let received = JSON.stringify(reply);
+ browser.test.assertEq(expected, received, "Received echoed native message");
+
+ browser.test.sendMessage("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+
+ // With sendNativeMessage(), the subprocess should be disconnected
+ // after exchanging a single message.
+ yield waitForSubprocessExit();
+
+ yield extension.unload();
+});
+
+// Test calling Port.disconnect()
+add_task(function* test_disconnect() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onMessage.addListener((msg, msgPort) => {
+ browser.test.assertEq(port, msgPort, "onMessage handler should receive the port as the second argument");
+ browser.test.sendMessage("message", msg);
+ });
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.fail("onDisconnect should not be called for disconnect()");
+ });
+ browser.test.onMessage.addListener((what, payload) => {
+ if (what == "send") {
+ if (payload._json) {
+ let json = payload._json;
+ payload.toJSON = () => json;
+ delete payload._json;
+ }
+ port.postMessage(payload);
+ } else if (what == "disconnect") {
+ try {
+ port.disconnect();
+ browser.test.sendMessage("disconnect-result", {success: true});
+ } catch (err) {
+ browser.test.sendMessage("disconnect-result", {
+ success: false,
+ errmsg: err.message,
+ });
+ }
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("send", "test");
+ let response = yield extension.awaitMessage("message");
+ equal(response, "test", "Echoed a string");
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is running");
+
+ extension.sendMessage("disconnect");
+ response = yield extension.awaitMessage("disconnect-result");
+ equal(response.success, true, "disconnect succeeded");
+
+ do_print("waiting for subprocess to exit");
+ yield waitForSubprocessExit();
+ procCount = yield getSubprocessCount();
+ equal(procCount, 0, "subprocess is no longer running");
+
+ extension.sendMessage("disconnect");
+ response = yield extension.awaitMessage("disconnect-result");
+ equal(response.success, true, "second call to disconnect silently ignored");
+
+ yield extension.unload();
+});
+
+// Test the limit on message size for writing
+add_task(function* test_write_limit() {
+ Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
+ function clearPref() {
+ Services.prefs.clearUserPref(PREF_MAX_WRITE);
+ }
+ do_register_cleanup(clearPref);
+
+ function background() {
+ const PAYLOAD = "0123456789A";
+ let port = browser.runtime.connectNative("echo");
+ try {
+ port.postMessage(PAYLOAD);
+ browser.test.sendMessage("result", null);
+ } catch (ex) {
+ browser.test.sendMessage("result", ex.message);
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let errmsg = yield extension.awaitMessage("result");
+ notEqual(errmsg, null, "native postMessage() failed for overly large message");
+
+ yield extension.unload();
+ yield waitForSubprocessExit();
+
+ clearPref();
+});
+
+// Test the limit on message size for reading
+add_task(function* test_read_limit() {
+ Services.prefs.setIntPref(PREF_MAX_READ, 10);
+ function clearPref() {
+ Services.prefs.clearUserPref(PREF_MAX_READ);
+ }
+ do_register_cleanup(clearPref);
+
+ function background() {
+ const PAYLOAD = "0123456789A";
+ let port = browser.runtime.connectNative("echo");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
+ browser.test.assertEq("Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.", port.error && port.error.message);
+ browser.test.sendMessage("result", "disconnected");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", "message");
+ });
+ port.postMessage(PAYLOAD);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let result = yield extension.awaitMessage("result");
+ equal(result, "disconnected", "native port disconnected on receiving large message");
+
+ yield extension.unload();
+ yield waitForSubprocessExit();
+
+ clearPref();
+});
+
+// Test that an extension without the nativeMessaging permission cannot
+// use native messaging.
+add_task(function* test_ext_permission() {
+ function background() {
+ browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission");
+ browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission");
+ browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+ browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+ browser.test.sendMessage("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield extension.unload();
+});
+
+// Test that an extension that is not listed in allowed_extensions for
+// a native application cannot use that application.
+add_task(function* test_app_permission() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
+ browser.test.assertEq("This extension does not have permission to use native application echo (or the application is not installed)", port.error && port.error.message);
+ browser.test.sendMessage("result", "disconnected");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", "message");
+ });
+ port.postMessage({test: "test"});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["nativeMessaging"],
+ },
+ }, "somethingelse@tests.mozilla.org");
+
+ yield extension.startup();
+
+ let result = yield extension.awaitMessage("result");
+ equal(result, "disconnected", "connectNative() failed without native app permission");
+
+ yield extension.unload();
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 0, "No child process was started");
+});
+
+// Test that the command-line arguments and working directory for the
+// native application are as expected.
+add_task(function* test_child_process() {
+ function background() {
+ let port = browser.runtime.connectNative("info");
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", msg);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let msg = yield extension.awaitMessage("result");
+ equal(msg.args.length, 2, "Received one command line argument");
+ equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
+ equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path,
+ "Working directory is the directory containing the native appliation");
+
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+});
+
+add_task(function* test_stderr() {
+ function background() {
+ let port = browser.runtime.connectNative("stderr");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(port, msgPort, "onDisconnect handler should receive the port as the first argument");
+ browser.test.assertEq(null, port.error, "Normal application exit is not an error");
+ browser.test.sendMessage("finished");
+ });
+ }
+
+ let {messages} = yield promiseConsoleOutput(function* () {
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("finished");
+ yield extension.unload();
+
+ yield waitForSubprocessExit();
+ });
+
+ let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line)));
+ notEqual(lines[0], -1, "Saw first line of stderr output on the console");
+ notEqual(lines[1], -1, "Saw second line of stderr output on the console");
+ notEqual(lines[0], lines[1], "Stderr output lines are separated in the console");
+});
+
+// Test that calling connectNative() multiple times works
+// (bug 1313980 was a previous regression in this area)
+add_task(function* test_multiple_connects() {
+ async function background() {
+ function once() {
+ return new Promise(resolve => {
+ let MSG = "hello";
+ let port = browser.runtime.connectNative("echo");
+
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(MSG, msg, "Got expected message back");
+ port.disconnect();
+ resolve();
+ });
+ port.postMessage(MSG);
+ });
+ }
+
+ await once();
+ await once();
+ browser.test.notifyPass("multiple-connect");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("multiple-connect");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
new file mode 100644
index 0000000000..693f67ddeb
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
+ "resource://testing-common/MockRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+Cu.import("resource://gre/modules/Subprocess.jsm");
+
+const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 36 : 18;
+const MAX_RETRIES = 5;
+
+
+const ECHO_BODY = String.raw`
+ import struct
+ import sys
+
+ while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ sys.exit(0)
+
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+`;
+
+const SCRIPTS = [
+ {
+ name: "echo",
+ description: "A native app that echoes back messages it receives",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+add_task(function* setup() {
+ yield setupHosts(SCRIPTS);
+});
+
+add_task(function* test_round_trip_perf() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.onMessage.addListener(msg => {
+ if (msg != "run-tests") {
+ return;
+ }
+
+ let port = browser.runtime.connectNative("echo");
+
+ function next() {
+ port.postMessage({
+ "Lorem": {
+ "ipsum": {
+ "dolor": [
+ "sit amet",
+ "consectetur adipiscing elit",
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ ],
+ "Ut enim": [
+ "ad minim veniam",
+ "quis nostrud exercitation ullamco",
+ "laboris nisi ut aliquip ex ea commodo consequat.",
+ ],
+ "Duis": [
+ "aute irure dolor in reprehenderit in",
+ "voluptate velit esse cillum dolore eu",
+ "fugiat nulla pariatur.",
+ ],
+ "Excepteur": [
+ "sint occaecat cupidatat non proident",
+ "sunt in culpa qui officia deserunt",
+ "mollit anim id est laborum.",
+ ],
+ },
+ },
+ });
+ }
+
+ const COUNT = 1000;
+ let now;
+ function finish() {
+ let roundTripTime = (Date.now() - now) / COUNT;
+
+ port.disconnect();
+ browser.test.sendMessage("result", roundTripTime);
+ }
+
+ let count = 0;
+ port.onMessage.addListener(() => {
+ if (count == 0) {
+ // Skip the first round, since it includes the time it takes
+ // the app to start up.
+ now = Date.now();
+ }
+
+ if (count++ <= COUNT) {
+ next();
+ } else {
+ finish();
+ }
+ });
+
+ next();
+ });
+ },
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+
+ let roundTripTime = Infinity;
+ for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) {
+ extension.sendMessage("run-tests");
+ roundTripTime = yield extension.awaitMessage("result");
+ }
+
+ yield extension.unload();
+
+ ok(roundTripTime <= MAX_ROUND_TRIP_TIME_MS,
+ `Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
new file mode 100644
index 0000000000..a75a1d49d1
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
@@ -0,0 +1,82 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const WONTDIE_BODY = String.raw`
+ import signal
+ import struct
+ import sys
+ import time
+
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
+
+ def spin():
+ while True:
+ try:
+ signal.pause()
+ except AttributeError:
+ time.sleep(5)
+
+ while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ spin()
+
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+`;
+
+const SCRIPTS = [
+ {
+ name: "wontdie",
+ description: "a native app that does not exit when stdin closes or on SIGTERM",
+ script: WONTDIE_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+add_task(function* setup() {
+ yield setupHosts(SCRIPTS);
+});
+
+
+// Test that an unresponsive native application still gets killed eventually
+add_task(function* test_unresponsive_native_app() {
+ // XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it
+ // just for this test?
+
+ function background() {
+ let port = browser.runtime.connectNative("wontdie");
+
+ const MSG = "echo me";
+ // bounce a message to make sure the process actually starts
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, MSG, "Received echoed message");
+ browser.test.sendMessage("ready");
+ });
+ port.postMessage(MSG);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ applications: {gecko: {id: ID}},
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ let procCount = yield getSubprocessCount();
+ equal(procCount, 1, "subprocess is running");
+
+ let exitPromise = waitForSubprocessExit();
+ yield extension.unload();
+ yield exitPromise;
+
+ procCount = yield getSubprocessCount();
+ equal(procCount, 0, "subprocess was succesfully killed");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js b/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js
new file mode 100644
index 0000000000..6f8b553fc6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_onmessage_removelistener.js
@@ -0,0 +1,30 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ function listener() {
+ browser.test.notifyFail("listener should not be invoked");
+ }
+
+ browser.runtime.onMessage.addListener(listener);
+ browser.runtime.onMessage.removeListener(listener);
+ browser.runtime.sendMessage("hello");
+
+ // Make sure that, if we somehow fail to remove the listener, then we'll run
+ // the listener before the test is marked as passing.
+ setTimeout(function() {
+ browser.test.notifyPass("onmessage_removelistener");
+ }, 0);
+}
+
+let extensionData = {
+ background: backgroundScript,
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("onmessage_removelistener");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js
new file mode 100644
index 0000000000..2a1342cde8
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_connect_no_receiver.js
@@ -0,0 +1,23 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_connect_without_listener() {
+ function background() {
+ let port = browser.runtime.connect();
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq("Could not establish connection. Receiving end does not exist.", port.error && port.error.message);
+ browser.test.notifyPass("port.onDisconnect was called");
+ });
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("port.onDisconnect was called");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js
new file mode 100644
index 0000000000..a280206fa4
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getBrowserInfo.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+add_task(function* setup() {
+ ExtensionTestUtils.mockAppInfo();
+});
+
+add_task(function* test_getBrowserInfo() {
+ async function background() {
+ let info = await browser.runtime.getBrowserInfo();
+
+ browser.test.assertEq(info.name, "XPCShell", "name is valid");
+ browser.test.assertEq(info.vendor, "Mozilla", "vendor is Mozilla");
+ browser.test.assertEq(info.version, "48", "version is correct");
+ browser.test.assertEq(info.buildID, "20160315", "buildID is correct");
+
+ browser.test.notifyPass("runtime.getBrowserInfo");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({background});
+ yield extension.startup();
+ yield extension.awaitFinish("runtime.getBrowserInfo");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js
new file mode 100644
index 0000000000..29bad0c108
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_getPlatformInfo.js
@@ -0,0 +1,25 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function backgroundScript() {
+ browser.runtime.getPlatformInfo(info => {
+ let validOSs = ["mac", "win", "android", "cros", "linux", "openbsd"];
+ let validArchs = ["arm", "x86-32", "x86-64"];
+
+ browser.test.assertTrue(validOSs.indexOf(info.os) != -1, "OS is valid");
+ browser.test.assertTrue(validArchs.indexOf(info.arch) != -1, "Architecture is valid");
+ browser.test.notifyPass("runtime.getPlatformInfo");
+ });
+}
+
+let extensionData = {
+ background: backgroundScript,
+};
+
+add_task(function* test_contentscript() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitFinish("runtime.getPlatformInfo");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
new file mode 100644
index 0000000000..fa6461412b
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_onInstalled_and_onStartup.js
@@ -0,0 +1,337 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+ const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ return Management;
+});
+
+const {
+ createAppInfo,
+ createTempWebExtensionFile,
+ promiseAddonByID,
+ promiseAddonEvent,
+ promiseCompleteAllInstalls,
+ promiseFindAddonUpdates,
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function awaitEvent(eventName) {
+ return new Promise(resolve => {
+ let listener = (_eventName, ...args) => {
+ if (_eventName === eventName) {
+ Management.off(eventName, listener);
+ resolve(...args);
+ }
+ };
+
+ Management.on(eventName, listener);
+ });
+}
+
+function background() {
+ let onInstalledDetails = null;
+ let onStartupFired = false;
+
+ browser.runtime.onInstalled.addListener(details => {
+ onInstalledDetails = details;
+ });
+
+ browser.runtime.onStartup.addListener(() => {
+ onStartupFired = true;
+ });
+
+ browser.test.onMessage.addListener(message => {
+ if (message === "get-on-installed-details") {
+ onInstalledDetails = onInstalledDetails || {fired: false};
+ browser.test.sendMessage("on-installed-details", onInstalledDetails);
+ } else if (message === "did-on-startup-fire") {
+ browser.test.sendMessage("on-startup-fired", onStartupFired);
+ } else if (message === "reload-extension") {
+ browser.runtime.reload();
+ }
+ });
+
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ browser.test.sendMessage("reloading");
+ browser.runtime.reload();
+ });
+}
+
+function* expectEvents(extension, {onStartupFired, onInstalledFired, onInstalledReason}) {
+ extension.sendMessage("get-on-installed-details");
+ let details = yield extension.awaitMessage("on-installed-details");
+ if (onInstalledFired) {
+ equal(details.reason, onInstalledReason, "runtime.onInstalled fired with the correct reason");
+ } else {
+ equal(details.fired, onInstalledFired, "runtime.onInstalled should not have fired");
+ }
+
+ extension.sendMessage("did-on-startup-fire");
+ let fired = yield extension.awaitMessage("on-startup-fired");
+ equal(fired, onStartupFired, `Expected runtime.onStartup to ${onStartupFired ? "" : "not "} fire`);
+}
+
+add_task(function* test_should_fire_on_addon_update() {
+ const EXTENSION_ID = "test_runtime_on_installed_addon_update@tests.mozilla.org";
+
+ const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+
+ // The test extension uses an insecure update url.
+ Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+ const testServer = createHttpServer();
+ const port = testServer.identity.primaryPort;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ "update_url": `http://localhost:${port}/test_update.json`,
+ },
+ },
+ },
+ background,
+ });
+
+ testServer.registerPathHandler("/test_update.json", (request, response) => {
+ response.write(`{
+ "addons": {
+ "${EXTENSION_ID}": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:${port}/addons/test_runtime_on_installed-2.0.xpi"
+ }
+ ]
+ }
+ }
+ }`);
+ });
+
+ let webExtensionFile = createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ testServer.registerFile("/addons/test_runtime_on_installed-2.0.xpi", webExtensionFile);
+
+ yield promiseStartupManager();
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let addon = yield promiseAddonByID(EXTENSION_ID);
+ equal(addon.version, "1.0", "The installed addon has the correct version");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ let promiseInstalled = promiseAddonEvent("onInstalled");
+ yield promiseCompleteAllInstalls([install]);
+
+ yield extension.awaitMessage("reloading");
+
+ let startupPromise = awaitEvent("ready");
+
+ let [updated_addon] = yield promiseInstalled;
+ equal(updated_addon.version, "2.0", "The updated addon has the correct version");
+
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "update",
+ });
+
+ yield extension.unload();
+
+ yield updated_addon.uninstall();
+ yield promiseShutdownManager();
+});
+
+add_task(function* test_should_fire_on_browser_update() {
+ const EXTENSION_ID = "test_runtime_on_installed_browser_update@tests.mozilla.org";
+
+ yield promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("1");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: false,
+ });
+
+ // Update the browser.
+ startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("2");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: true,
+ onInstalledReason: "browser_update",
+ });
+
+ // Restart the browser.
+ startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("2");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: false,
+ });
+
+ // Update the browser again.
+ startupPromise = awaitEvent("ready");
+ yield promiseRestartManager("3");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: true,
+ onInstalledFired: true,
+ onInstalledReason: "browser_update",
+ });
+
+ yield extension.unload();
+
+ yield promiseShutdownManager();
+});
+
+add_task(function* test_should_not_fire_on_reload() {
+ const EXTENSION_ID = "test_runtime_on_installed_reload@tests.mozilla.org";
+
+ yield promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let startupPromise = awaitEvent("ready");
+ extension.sendMessage("reload-extension");
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: false,
+ });
+
+ yield extension.unload();
+ yield promiseShutdownManager();
+});
+
+add_task(function* test_should_not_fire_on_restart() {
+ const EXTENSION_ID = "test_runtime_on_installed_restart@tests.mozilla.org";
+
+ yield promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": EXTENSION_ID,
+ },
+ },
+ },
+ background,
+ });
+
+ yield extension.startup();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: true,
+ onInstalledReason: "install",
+ });
+
+ let addon = yield promiseAddonByID(EXTENSION_ID);
+ addon.userDisabled = true;
+
+ let startupPromise = awaitEvent("ready");
+ addon.userDisabled = false;
+ extension.extension = yield startupPromise;
+ extension.attachListeners();
+
+ yield expectEvents(extension, {
+ onStartupFired: false,
+ onInstalledFired: false,
+ });
+
+ yield extension.markUnloaded();
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js
new file mode 100644
index 0000000000..fec8e13dd6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage.js
@@ -0,0 +1,79 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* tabsSendMessageReply() {
+ function background() {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond(msg);
+ } else if (msg == "respond-soon") {
+ setTimeout(() => { respond(msg); }, 0);
+ return true;
+ } else if (msg == "respond-promise") {
+ return Promise.resolve(msg);
+ } else if (msg == "respond-never") {
+ return;
+ } else if (msg == "respond-error") {
+ return Promise.reject(new Error(msg));
+ } else if (msg == "throw-error") {
+ throw new Error(msg);
+ }
+ });
+
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "respond-now") {
+ respond("hello");
+ } else if (msg == "respond-now-2") {
+ respond(msg);
+ }
+ });
+
+ let childFrame = document.createElement("iframe");
+ childFrame.src = "extensionpage.html";
+ document.body.appendChild(childFrame);
+ }
+
+ function senderScript() {
+ Promise.all([
+ browser.runtime.sendMessage("respond-now"),
+ browser.runtime.sendMessage("respond-now-2"),
+ new Promise(resolve => browser.runtime.sendMessage("respond-soon", resolve)),
+ browser.runtime.sendMessage("respond-promise"),
+ browser.runtime.sendMessage("respond-never"),
+ new Promise(resolve => {
+ browser.runtime.sendMessage("respond-never", response => { resolve(response); });
+ }),
+
+ browser.runtime.sendMessage("respond-error").catch(error => Promise.resolve({error})),
+ browser.runtime.sendMessage("throw-error").catch(error => Promise.resolve({error})),
+ ]).then(([respondNow, respondNow2, respondSoon, respondPromise, respondNever, respondNever2, respondError, throwError]) => {
+ browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response");
+ browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener");
+ browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response");
+ browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response");
+ browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution");
+ browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution");
+
+ browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response");
+ browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response");
+
+ browser.test.notifyPass("sendMessage");
+ }).catch(e => {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("sendMessage");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ files: {
+ "senderScript.js": senderScript,
+ "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="senderScript.js"></script>`,
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("sendMessage");
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js
new file mode 100644
index 0000000000..f1a8d5a368
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_errors.js
@@ -0,0 +1,59 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_sendMessage_error() {
+ async function background() {
+ let circ = {};
+ circ.circ = circ;
+ let testCases = [
+ // [arguments, expected error string],
+ [[], "runtime.sendMessage's message argument is missing"],
+ [[null, null, null, null], "runtime.sendMessage's last argument is not a function"],
+ [[null, null, 1], "runtime.sendMessage's options argument is invalid"],
+ [[1, null, null], "runtime.sendMessage's extensionId argument is invalid"],
+ [[null, null, null, null, null], "runtime.sendMessage received too many arguments"],
+
+ // Even when the parameters are accepted, we still expect an error
+ // because there is no onMessage listener.
+ [[null, null, null], "Could not establish connection. Receiving end does not exist."],
+
+ // Structural cloning doesn't work with DOM but we fall back
+ // JSON serialization, so we don't expect another error.
+ [[null, location, null], "Could not establish connection. Receiving end does not exist."],
+
+ // Structured cloning supports cyclic self-references.
+ [[null, [circ, location], null], "cyclic object value"],
+ // JSON serialization does not support cyclic references.
+ [[null, circ, null], "Could not establish connection. Receiving end does not exist."],
+ // (the last two tests shows whether sendMessage is implemented as structured cloning).
+ ];
+
+ // Repeat all tests with the undefined value instead of null.
+ for (let [args, expectedError] of testCases.slice()) {
+ args = args.map(arg => arg === null ? undefined : arg);
+ testCases.push([args, expectedError]);
+ }
+
+ for (let [args, expectedError] of testCases) {
+ let description = `runtime.sendMessage(${args.map(String).join(", ")})`;
+
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage(...args),
+ expectedError,
+ `expected error message for ${description}`);
+ }
+
+ browser.test.notifyPass("sendMessage parameter validation");
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("sendMessage parameter validation");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js
new file mode 100644
index 0000000000..f906333d21
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_no_receiver.js
@@ -0,0 +1,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_sendMessage_without_listener() {
+ async function background() {
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("msg"),
+ "Could not establish connection. Receiving end does not exist.",
+ "sendMessage callback was invoked");
+
+ browser.test.notifyPass("sendMessage callback was invoked");
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("sendMessage callback was invoked");
+
+ yield extension.unload();
+});
+
+add_task(function* test_chrome_sendMessage_without_listener() {
+ function background() {
+ /* globals chrome */
+ browser.test.assertEq(null, chrome.runtime.lastError, "no lastError before call");
+ let retval = chrome.runtime.sendMessage("msg");
+ browser.test.assertEq(null, chrome.runtime.lastError, "no lastError after call");
+ browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage without callback");
+
+ let isAsyncCall = false;
+ retval = chrome.runtime.sendMessage("msg", reply => {
+ browser.test.assertEq(undefined, reply, "no reply");
+ browser.test.assertTrue(isAsyncCall, "chrome.runtime.sendMessage's callback must be called asynchronously");
+ browser.test.assertEq(undefined, retval, "return value of chrome.runtime.sendMessage with callback");
+ browser.test.assertEq("Could not establish connection. Receiving end does not exist.", chrome.runtime.lastError.message);
+ browser.test.notifyPass("finished chrome.runtime.sendMessage");
+ });
+ isAsyncCall = true;
+ }
+ let extensionData = {
+ background,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitFinish("finished chrome.runtime.sendMessage");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js
new file mode 100644
index 0000000000..e4f5e951f6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_runtime_sendMessage_self.js
@@ -0,0 +1,51 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+add_task(function* test_sendMessage_to_self_should_not_trigger_onMessage() {
+ async function background() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq("msg from child", msg);
+ browser.test.notifyPass("sendMessage did not call same-frame onMessage");
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("sendMessage with a listener in another frame", msg);
+ browser.runtime.sendMessage("should only reach another frame");
+ });
+
+ await browser.test.assertRejects(
+ browser.runtime.sendMessage("should not trigger same-frame onMessage"),
+ "Could not establish connection. Receiving end does not exist.");
+
+ let anotherFrame = document.createElement("iframe");
+ anotherFrame.src = browser.extension.getURL("extensionpage.html");
+ document.body.appendChild(anotherFrame);
+ }
+
+ function lastScript() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq("should only reach another frame", msg);
+ browser.runtime.sendMessage("msg from child");
+ });
+ browser.test.sendMessage("sendMessage callback called");
+ }
+
+ let extensionData = {
+ background,
+ files: {
+ "lastScript.js": lastScript,
+ "extensionpage.html": `<!DOCTYPE html><meta charset="utf-8"><script src="lastScript.js"></script>`,
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+
+ yield extension.awaitMessage("sendMessage callback called");
+ extension.sendMessage("sendMessage with a listener in another frame");
+ yield extension.awaitFinish("sendMessage did not call same-frame onMessage");
+
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
new file mode 100644
index 0000000000..d838be5b51
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -0,0 +1,1427 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon;
+
+let json = [
+ {namespace: "testing",
+
+ properties: {
+ PROP1: {value: 20},
+ prop2: {type: "string"},
+ prop3: {
+ $ref: "submodule",
+ },
+ prop4: {
+ $ref: "submodule",
+ unsupported: true,
+ },
+ },
+
+ types: [
+ {
+ id: "type1",
+ type: "string",
+ "enum": ["value1", "value2", "value3"],
+ },
+
+ {
+ id: "type2",
+ type: "object",
+ properties: {
+ prop1: {type: "integer"},
+ prop2: {type: "array", items: {"$ref": "type1"}},
+ },
+ },
+
+ {
+ id: "basetype1",
+ type: "object",
+ properties: {
+ prop1: {type: "string"},
+ },
+ },
+
+ {
+ id: "basetype2",
+ choices: [
+ {type: "integer"},
+ ],
+ },
+
+ {
+ $extend: "basetype1",
+ properties: {
+ prop2: {type: "string"},
+ },
+ },
+
+ {
+ $extend: "basetype2",
+ choices: [
+ {type: "string"},
+ ],
+ },
+
+ {
+ id: "submodule",
+ type: "object",
+ functions: [
+ {
+ name: "sub_foo",
+ type: "function",
+ parameters: [],
+ returns: "integer",
+ },
+ ],
+ },
+ ],
+
+ functions: [
+ {
+ name: "foo",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "integer", optional: true, default: 99},
+ {name: "arg2", type: "boolean", optional: true},
+ ],
+ },
+
+ {
+ name: "bar",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "integer", optional: true},
+ {name: "arg2", type: "boolean"},
+ ],
+ },
+
+ {
+ name: "baz",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "object", properties: {
+ prop1: {type: "string"},
+ prop2: {type: "integer", optional: true},
+ prop3: {type: "integer", unsupported: true},
+ }},
+ ],
+ },
+
+ {
+ name: "qux",
+ type: "function",
+ parameters: [
+ {name: "arg1", "$ref": "type1"},
+ ],
+ },
+
+ {
+ name: "quack",
+ type: "function",
+ parameters: [
+ {name: "arg1", "$ref": "type2"},
+ ],
+ },
+
+ {
+ name: "quora",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "function"},
+ ],
+ },
+
+ {
+ name: "quileute",
+ type: "function",
+ parameters: [
+ {name: "arg1", type: "integer", optional: true},
+ {name: "arg2", type: "integer"},
+ ],
+ },
+
+ {
+ name: "queets",
+ type: "function",
+ unsupported: true,
+ parameters: [],
+ },
+
+ {
+ name: "quintuplets",
+ type: "function",
+ parameters: [
+ {name: "obj", type: "object", properties: [], additionalProperties: {type: "integer"}},
+ ],
+ },
+
+ {
+ name: "quasar",
+ type: "function",
+ parameters: [
+ {name: "abc", type: "object", properties: {
+ func: {type: "function", parameters: [
+ {name: "x", type: "integer"},
+ ]},
+ }},
+ ],
+ },
+
+ {
+ name: "quosimodo",
+ type: "function",
+ parameters: [
+ {name: "xyz", type: "object", additionalProperties: {type: "any"}},
+ ],
+ },
+
+ {
+ name: "patternprop",
+ type: "function",
+ parameters: [
+ {
+ name: "obj",
+ type: "object",
+ properties: {"prop1": {type: "string", pattern: "^\\d+$"}},
+ patternProperties: {
+ "(?i)^prop\\d+$": {type: "string"},
+ "^foo\\d+$": {type: "string"},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "pattern",
+ type: "function",
+ parameters: [
+ {name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"},
+ ],
+ },
+
+ {
+ name: "format",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ url: {type: "string", "format": "url", "optional": true},
+ relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
+ strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "formatDate",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ date: {type: "string", format: "date", optional: true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "deep",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ foo: {
+ type: "object",
+ properties: {
+ bar: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ baz: {
+ type: "object",
+ properties: {
+ required: {type: "integer"},
+ optional: {type: "string", optional: true},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+
+ {
+ name: "errors",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ warn: {
+ type: "string",
+ pattern: "^\\d+$",
+ optional: true,
+ onError: "warn",
+ },
+ ignore: {
+ type: "string",
+ pattern: "^\\d+$",
+ optional: true,
+ onError: "ignore",
+ },
+ default: {
+ type: "string",
+ pattern: "^\\d+$",
+ optional: true,
+ },
+ },
+ },
+ ],
+ },
+
+ {
+ name: "localize",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ foo: {type: "string", "preprocess": "localize", "optional": true},
+ bar: {type: "string", "optional": true},
+ url: {type: "string", "preprocess": "localize", "format": "url", "optional": true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "extended1",
+ type: "function",
+ parameters: [
+ {name: "val", $ref: "basetype1"},
+ ],
+ },
+
+ {
+ name: "extended2",
+ type: "function",
+ parameters: [
+ {name: "val", $ref: "basetype2"},
+ ],
+ },
+ ],
+
+ events: [
+ {
+ name: "onFoo",
+ type: "function",
+ },
+
+ {
+ name: "onBar",
+ type: "function",
+ extraParameters: [{
+ name: "filter",
+ type: "integer",
+ optional: true,
+ default: 1,
+ }],
+ },
+ ],
+ },
+ {
+ namespace: "foreign",
+ properties: {
+ foreignRef: {$ref: "testing.submodule"},
+ },
+ },
+ {
+ namespace: "inject",
+ properties: {
+ PROP1: {value: "should inject"},
+ },
+ },
+ {
+ namespace: "do-not-inject",
+ properties: {
+ PROP1: {value: "should not inject"},
+ },
+ },
+];
+
+let tallied = null;
+
+function tally(kind, ns, name, args) {
+ tallied = [kind, ns, name, args];
+}
+
+function verify(...args) {
+ do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
+ tallied = null;
+}
+
+let talliedErrors = [];
+
+function checkErrors(errors) {
+ do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors");
+ for (let [i, error] of errors.entries()) {
+ do_check_true(i in talliedErrors && talliedErrors[i].includes(error),
+ `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
+ }
+
+ talliedErrors.length = 0;
+}
+
+let permissions = new Set();
+
+class TallyingAPIImplementation extends SchemaAPIInterface {
+ constructor(namespace, name) {
+ super();
+ this.namespace = namespace;
+ this.name = name;
+ }
+
+ callFunction(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ callFunctionNoReturn(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ getProperty() {
+ tally("get", this.namespace, this.name);
+ }
+
+ setProperty(value) {
+ tally("set", this.namespace, this.name, value);
+ }
+
+ addListener(listener, args) {
+ tally("addListener", this.namespace, this.name, [listener, args]);
+ }
+
+ removeListener(listener) {
+ tally("removeListener", this.namespace, this.name, [listener]);
+ }
+
+ hasListener(listener) {
+ tally("hasListener", this.namespace, this.name, [listener]);
+ }
+}
+
+let wrapper = {
+ url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
+
+ checkLoadURL(url) {
+ return !url.startsWith("chrome:");
+ },
+
+ preprocessors: {
+ localize(value, context) {
+ return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
+ },
+ },
+
+ logError(message) {
+ talliedErrors.push(message);
+ },
+
+ hasPermission(permission) {
+ return permissions.has(permission);
+ },
+
+ shouldInject(ns) {
+ return ns != "do-not-inject";
+ },
+
+ getImplementation(namespace, name) {
+ return new TallyingAPIImplementation(namespace, name);
+ },
+};
+
+add_task(function* () {
+ let url = "data:," + JSON.stringify(json);
+ yield Schemas.load(url);
+
+ let root = {};
+ tallied = null;
+ Schemas.inject(root, wrapper);
+ do_check_eq(tallied, null);
+
+ do_check_eq(root.testing.PROP1, 20, "simple value property");
+ do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
+ do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
+
+ do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
+ do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
+
+ root.testing.foo(11, true);
+ verify("call", "testing", "foo", [11, true]);
+
+ root.testing.foo(true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(null, true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(undefined, true);
+ verify("call", "testing", "foo", [99, true]);
+
+ root.testing.foo(11);
+ verify("call", "testing", "foo", [11, null]);
+
+ Assert.throws(() => root.testing.bar(11),
+ /Incorrect argument types/,
+ "should throw without required arg");
+
+ Assert.throws(() => root.testing.bar(11, true, 10),
+ /Incorrect argument types/,
+ "should throw with too many arguments");
+
+ root.testing.bar(true);
+ verify("call", "testing", "bar", [null, true]);
+
+ root.testing.baz({prop1: "hello", prop2: 22});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: 22}]);
+
+ root.testing.baz({prop1: "hello"});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]);
+
+ root.testing.baz({prop1: "hello", prop2: null});
+ verify("call", "testing", "baz", [{prop1: "hello", prop2: null}]);
+
+ Assert.throws(() => root.testing.baz({prop2: 12}),
+ /Property "prop1" is required/,
+ "should throw without required property");
+
+ Assert.throws(() => root.testing.baz({prop1: "hi", prop3: 12}),
+ /Property "prop3" is unsupported by Firefox/,
+ "should throw with unsupported property");
+
+ Assert.throws(() => root.testing.baz({prop1: "hi", prop4: 12}),
+ /Unexpected property "prop4"/,
+ "should throw with unexpected property");
+
+ Assert.throws(() => root.testing.baz({prop1: 12}),
+ /Expected string instead of 12/,
+ "should throw with wrong type");
+
+ root.testing.qux("value2");
+ verify("call", "testing", "qux", ["value2"]);
+
+ Assert.throws(() => root.testing.qux("value4"),
+ /Invalid enumeration value "value4"/,
+ "should throw for invalid enum value");
+
+ root.testing.quack({prop1: 12, prop2: ["value1", "value3"]});
+ verify("call", "testing", "quack", [{prop1: 12, prop2: ["value1", "value3"]}]);
+
+ Assert.throws(() => root.testing.quack({prop1: 12, prop2: ["value1", "value3", "value4"]}),
+ /Invalid enumeration value "value4"/,
+ "should throw for invalid array type");
+
+ function f() {}
+ root.testing.quora(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ let g = () => 0;
+ root.testing.quora(g);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quora"]));
+ do_check_eq(tallied[3][0], g);
+ tallied = null;
+
+ root.testing.quileute(10);
+ verify("call", "testing", "quileute", [null, 10]);
+
+ Assert.throws(() => root.testing.queets(),
+ /queets is not a function/,
+ "should throw for unsupported functions");
+
+ root.testing.quintuplets({a: 10, b: 20, c: 30});
+ verify("call", "testing", "quintuplets", [{a: 10, b: 20, c: 30}]);
+
+ Assert.throws(() => root.testing.quintuplets({a: 10, b: 20, c: 30, d: "hi"}),
+ /Expected integer instead of "hi"/,
+ "should throw for wrong additionalProperties type");
+
+ root.testing.quasar({func: f});
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"]));
+ do_check_eq(tallied[3][0].func, f);
+ tallied = null;
+
+ root.testing.quosimodo({a: 10, b: 20, c: 30});
+ verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.quosimodo(10),
+ /Incorrect argument types/,
+ "should throw for wrong type");
+
+ root.testing.patternprop({prop1: "12", prop2: "42", Prop3: "43", foo1: "x"});
+ verify("call", "testing", "patternprop", [{prop1: "12", prop2: "42", Prop3: "43", foo1: "x"}]);
+ tallied = null;
+
+ root.testing.patternprop({prop1: "12"});
+ verify("call", "testing", "patternprop", [{prop1: "12"}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", foo1: null}),
+ /Expected string instead of null/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "xx", prop2: "yy"}),
+ /String "xx" must match \/\^\\d\+\$\//,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: 42}),
+ /Expected string instead of 42/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: null}),
+ /Expected string instead of null/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", propx: "42"}),
+ /Unexpected property "propx"/,
+ "should throw for unexpected property");
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", Foo1: "x"}),
+ /Unexpected property "Foo1"/,
+ "should throw for unexpected property");
+
+ root.testing.pattern("DEADbeef");
+ verify("call", "testing", "pattern", ["DEADbeef"]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.pattern("DEADcow"),
+ /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
+ "should throw for non-match");
+
+ root.testing.format({url: "http://foo/bar",
+ relativeUrl: "http://foo/bar"});
+ verify("call", "testing", "format", [{url: "http://foo/bar",
+ relativeUrl: "http://foo/bar",
+ strictRelativeUrl: null}]);
+ tallied = null;
+
+ root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"});
+ verify("call", "testing", "format", [{url: null,
+ relativeUrl: `${wrapper.url}foo.html`,
+ strictRelativeUrl: `${wrapper.url}foo.html`}]);
+ tallied = null;
+
+ for (let format of ["url", "relativeUrl"]) {
+ Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}),
+ /Access denied/,
+ "should throw for access denied");
+ }
+
+ for (let urlString of ["//foo.html", "http://foo/bar.html"]) {
+ Assert.throws(() => root.testing.format({strictRelativeUrl: urlString}),
+ /must be a relative URL/,
+ "should throw for non-relative URL");
+ }
+
+ const dates = [
+ "2016-03-04",
+ "2016-03-04T08:00:00Z",
+ "2016-03-04T08:00:00.000Z",
+ "2016-03-04T08:00:00-08:00",
+ "2016-03-04T08:00:00.000-08:00",
+ "2016-03-04T08:00:00+08:00",
+ "2016-03-04T08:00:00.000+08:00",
+ "2016-03-04T08:00:00+0800",
+ "2016-03-04T08:00:00-0800",
+ ];
+ dates.forEach(str => {
+ root.testing.formatDate({date: str});
+ verify("call", "testing", "formatDate", [{date: str}]);
+ });
+
+ // Make sure that a trivial change to a valid date invalidates it.
+ dates.forEach(str => {
+ Assert.throws(() => root.testing.formatDate({date: "0" + str}),
+ /Invalid date string/,
+ "should throw for invalid iso date string");
+ Assert.throws(() => root.testing.formatDate({date: str + "0"}),
+ /Invalid date string/,
+ "should throw for invalid iso date string");
+ });
+
+ const badDates = [
+ "I do not look anything like a date string",
+ "2016-99-99",
+ "2016-03-04T25:00:00Z",
+ ];
+ badDates.forEach(str => {
+ Assert.throws(() => root.testing.formatDate({date: str}),
+ /Invalid date string/,
+ "should throw for invalid iso date string");
+ });
+
+ root.testing.deep({foo: {bar: [{baz: {required: 12, optional: "42"}}]}});
+ verify("call", "testing", "deep", [{foo: {bar: [{baz: {required: 12, optional: "42"}}]}}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {optional: "42"}}]}}),
+ /Type error for parameter arg \(Error processing foo\.bar\.0\.baz: Property "required" is required\) for testing\.deep/,
+ "should throw with the correct object path");
+
+ Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {required: 12, optional: 42}}]}}),
+ /Type error for parameter arg \(Error processing foo\.bar\.0\.baz\.optional: Expected string instead of 42\) for testing\.deep/,
+ "should throw with the correct object path");
+
+
+ talliedErrors.length = 0;
+
+ root.testing.errors({warn: "0123", ignore: "0123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: "0123", ignore: "0123", default: "0123"}]);
+ checkErrors([]);
+
+ root.testing.errors({warn: "0123", ignore: "x123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: "0123", ignore: null, default: "0123"}]);
+ checkErrors([]);
+
+ root.testing.errors({warn: "x123", ignore: "0123", default: "0123"});
+ verify("call", "testing", "errors", [{warn: null, ignore: "0123", default: "0123"}]);
+ checkErrors([
+ 'String "x123" must match /^\\d+$/',
+ ]);
+
+
+ root.testing.onFoo.addListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([]));
+ tallied = null;
+
+ root.testing.onFoo.removeListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ root.testing.onFoo.hasListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["hasListener", "testing", "onFoo"]));
+ do_check_eq(tallied[3][0], f);
+ tallied = null;
+
+ Assert.throws(() => root.testing.onFoo.addListener(10),
+ /Invalid listener/,
+ "addListener with non-function should throw");
+
+ root.testing.onBar.addListener(f, 10);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([10]));
+ tallied = null;
+
+ root.testing.onBar.addListener(f);
+ do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onBar"]));
+ do_check_eq(tallied[3][0], f);
+ do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([1]));
+ tallied = null;
+
+ Assert.throws(() => root.testing.onBar.addListener(f, "hi"),
+ /Incorrect argument types/,
+ "addListener with wrong extra parameter should throw");
+
+ let target = {prop1: 12, prop2: ["value1", "value3"]};
+ let proxy = new Proxy(target, {});
+ Assert.throws(() => root.testing.quack(proxy),
+ /Expected a plain JavaScript object, got a Proxy/,
+ "should throw when passing a Proxy");
+
+ if (Symbol.toStringTag) {
+ let stringTarget = {prop1: 12, prop2: ["value1", "value3"]};
+ stringTarget[Symbol.toStringTag] = () => "[object Object]";
+ let stringProxy = new Proxy(stringTarget, {});
+ Assert.throws(() => root.testing.quack(stringProxy),
+ /Expected a plain JavaScript object, got a Proxy/,
+ "should throw when passing a Proxy");
+ }
+
+
+ root.testing.localize({foo: "__MSG_foo__", bar: "__MSG_foo__", url: "__MSG_http://example.com/__"});
+ verify("call", "testing", "localize", [{foo: "FOO", bar: "__MSG_foo__", url: "http://example.com/"}]);
+ tallied = null;
+
+
+ Assert.throws(() => root.testing.localize({url: "__MSG_/foo/bar__"}),
+ /\/FOO\/BAR is not a valid URL\./,
+ "should throw for invalid URL");
+
+
+ root.testing.extended1({prop1: "foo", prop2: "bar"});
+ verify("call", "testing", "extended1", [{prop1: "foo", prop2: "bar"}]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: 12}),
+ /Expected string instead of 12/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.extended1({prop1: "foo"}),
+ /Property "prop2" is required/,
+ "should throw for missing property");
+
+ Assert.throws(() => root.testing.extended1({prop1: "foo", prop2: "bar", prop3: "xxx"}),
+ /Unexpected property "prop3"/,
+ "should throw for extra property");
+
+
+ root.testing.extended2("foo");
+ verify("call", "testing", "extended2", ["foo"]);
+ tallied = null;
+
+ root.testing.extended2(12);
+ verify("call", "testing", "extended2", [12]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.extended2(true),
+ /Incorrect argument types/,
+ "should throw for wrong argument type");
+
+ root.testing.prop3.sub_foo();
+ verify("call", "testing.prop3", "sub_foo", []);
+ tallied = null;
+
+ Assert.throws(() => root.testing.prop4.sub_foo(),
+ /root.testing.prop4 is undefined/,
+ "should throw for unsupported submodule");
+
+ root.foreign.foreignRef.sub_foo();
+ verify("call", "foreign.foreignRef", "sub_foo", []);
+ tallied = null;
+});
+
+let deprecatedJson = [
+ {namespace: "deprecated",
+
+ properties: {
+ accessor: {
+ type: "string",
+ writable: true,
+ deprecated: "This is not the property you are looking for",
+ },
+ },
+
+ types: [
+ {
+ "id": "Type",
+ "type": "string",
+ },
+ ],
+
+ functions: [
+ {
+ name: "property",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ type: "object",
+ properties: {
+ foo: {
+ type: "string",
+ },
+ },
+ additionalProperties: {
+ type: "any",
+ deprecated: "Unknown property",
+ },
+ },
+ ],
+ },
+
+ {
+ name: "value",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "integer",
+ },
+ {
+ type: "string",
+ deprecated: "Please use an integer, not ${value}",
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "choices",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ deprecated: "You have no choices",
+ choices: [
+ {
+ type: "integer",
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "ref",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ $ref: "Type",
+ deprecated: "Deprecated alias",
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "method",
+ type: "function",
+ deprecated: "Do not call this method",
+ parameters: [
+ ],
+ },
+ ],
+
+ events: [
+ {
+ name: "onDeprecated",
+ type: "function",
+ deprecated: "This event does not work",
+ },
+ ],
+ },
+];
+
+add_task(function* testDeprecation() {
+ let url = "data:," + JSON.stringify(deprecatedJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+
+ root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"});
+ verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]);
+ checkErrors([
+ "Error processing xxx: Unknown property",
+ "Error processing yyy: Unknown property",
+ ]);
+
+ root.deprecated.value(12);
+ verify("call", "deprecated", "value", [12]);
+ checkErrors([]);
+
+ root.deprecated.value("12");
+ verify("call", "deprecated", "value", ["12"]);
+ checkErrors(["Please use an integer, not \"12\""]);
+
+ root.deprecated.choices(12);
+ verify("call", "deprecated", "choices", [12]);
+ checkErrors(["You have no choices"]);
+
+ root.deprecated.ref("12");
+ verify("call", "deprecated", "ref", ["12"]);
+ checkErrors(["Deprecated alias"]);
+
+ root.deprecated.method();
+ verify("call", "deprecated", "method", []);
+ checkErrors(["Do not call this method"]);
+
+
+ void root.deprecated.accessor;
+ verify("get", "deprecated", "accessor", null);
+ checkErrors(["This is not the property you are looking for"]);
+
+ root.deprecated.accessor = "x";
+ verify("set", "deprecated", "accessor", "x");
+ checkErrors(["This is not the property you are looking for"]);
+
+
+ root.deprecated.onDeprecated.addListener(() => {});
+ checkErrors(["This event does not work"]);
+
+ root.deprecated.onDeprecated.removeListener(() => {});
+ checkErrors(["This event does not work"]);
+
+ root.deprecated.onDeprecated.hasListener(() => {});
+ checkErrors(["This event does not work"]);
+});
+
+
+let choicesJson = [
+ {namespace: "choices",
+
+ types: [
+ ],
+
+ functions: [
+ {
+ name: "meh",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "string",
+ enum: ["foo", "bar", "baz"],
+ },
+ {
+ type: "string",
+ pattern: "florg.*meh",
+ },
+ {
+ type: "integer",
+ minimum: 12,
+ maximum: 42,
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "foo",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "object",
+ properties: {
+ blurg: {
+ type: "string",
+ unsupported: true,
+ optional: true,
+ },
+ },
+ additionalProperties: {
+ type: "string",
+ },
+ },
+ {
+ type: "string",
+ },
+ {
+ type: "array",
+ minItems: 2,
+ maxItems: 3,
+ items: {
+ type: "integer",
+ },
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ name: "bar",
+ type: "function",
+ parameters: [
+ {
+ name: "arg",
+ choices: [
+ {
+ type: "object",
+ properties: {
+ baz: {
+ type: "string",
+ },
+ },
+ },
+ {
+ type: "array",
+ items: {
+ type: "integer",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ]},
+];
+
+add_task(function* testChoices() {
+ let url = "data:," + JSON.stringify(choicesJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+ Assert.throws(() => root.choices.meh("frog"),
+ /Value must either: be one of \["foo", "bar", "baz"\], match the pattern \/florg\.\*meh\/, or be an integer value/);
+
+ Assert.throws(() => root.choices.meh(4),
+ /be a string value, or be at least 12/);
+
+ Assert.throws(() => root.choices.meh(43),
+ /be a string value, or be no greater than 42/);
+
+
+ Assert.throws(() => root.choices.foo([]),
+ /be an object value, be a string value, or have at least 2 items/);
+
+ Assert.throws(() => root.choices.foo([1, 2, 3, 4]),
+ /be an object value, be a string value, or have at most 3 items/);
+
+ Assert.throws(() => root.choices.foo({foo: 12}),
+ /.foo must be a string value, be a string value, or be an array value/);
+
+ Assert.throws(() => root.choices.foo({blurg: "foo"}),
+ /not contain an unsupported "blurg" property, be a string value, or be an array value/);
+
+
+ Assert.throws(() => root.choices.bar({}),
+ /contain the required "baz" property, or be an array value/);
+
+ Assert.throws(() => root.choices.bar({baz: "x", quux: "y"}),
+ /not contain an unexpected "quux" property, or be an array value/);
+
+ Assert.throws(() => root.choices.bar({baz: "x", quux: "y", foo: "z"}),
+ /not contain the unexpected properties \[foo, quux\], or be an array value/);
+});
+
+
+let permissionsJson = [
+ {namespace: "noPerms",
+
+ types: [],
+
+ functions: [
+ {
+ name: "noPerms",
+ type: "function",
+ parameters: [],
+ },
+
+ {
+ name: "fooPerm",
+ type: "function",
+ permissions: ["foo"],
+ parameters: [],
+ },
+ ]},
+
+ {namespace: "fooPerm",
+
+ permissions: ["foo"],
+
+ types: [],
+
+ functions: [
+ {
+ name: "noPerms",
+ type: "function",
+ parameters: [],
+ },
+
+ {
+ name: "fooBarPerm",
+ type: "function",
+ permissions: ["foo.bar"],
+ parameters: [],
+ },
+ ]},
+];
+
+add_task(function* testPermissions() {
+ let url = "data:," + JSON.stringify(permissionsJson);
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+
+ ok(!("fooPerm" in root.noPerms), "noPerms.fooPerm should not method exist");
+
+ ok(!("fooPerm" in root), "fooPerm namespace should not exist");
+
+
+ do_print('Add "foo" permission');
+ permissions.add("foo");
+
+ root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist");
+
+ equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
+ equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist");
+
+ ok(!("fooBarPerm" in root.fooPerm), "fooPerm.fooBarPerm method should not exist");
+
+
+ do_print('Add "foo.bar" permission');
+ permissions.add("foo.bar");
+
+ root = {};
+ Schemas.inject(root, wrapper);
+
+ equal(typeof root.noPerms, "object", "noPerms namespace should exist");
+ equal(typeof root.noPerms.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.noPerms.fooPerm, "function", "noPerms.fooPerm method should exist");
+
+ equal(typeof root.fooPerm, "object", "fooPerm namespace should exist");
+ equal(typeof root.fooPerm.noPerms, "function", "noPerms.noPerms method should exist");
+ equal(typeof root.fooPerm.fooBarPerm, "function", "noPerms.fooBarPerm method should exist");
+});
+
+let nestedNamespaceJson = [
+ {
+ "namespace": "nested.namespace",
+ "types": [
+ {
+ "id": "CustomType",
+ "type": "object",
+ "events": [
+ {
+ "name": "onEvent",
+ },
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ },
+ },
+ "functions": [
+ {
+ "name": "functionOnCustomType",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ "properties": {
+ "instanceOfCustomType": {
+ "$ref": "CustomType",
+ },
+ },
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ },
+ ],
+ },
+];
+
+add_task(function* testNestedNamespace() {
+ let url = "data:," + JSON.stringify(nestedNamespaceJson);
+
+ yield Schemas.load(url);
+
+ let root = {};
+ Schemas.inject(root, wrapper);
+
+ talliedErrors.length = 0;
+
+ ok(root.nested, "The root object contains the first namespace level");
+ ok(root.nested.namespace, "The first level object contains the second namespace level");
+
+ ok(root.nested.namespace.create, "Got the expected function in the nested namespace");
+ do_check_eq(typeof root.nested.namespace.create, "function",
+ "The property is a function as expected");
+
+ let {instanceOfCustomType} = root.nested.namespace;
+
+ ok(instanceOfCustomType,
+ "Got the expected instance of the CustomType defined in the schema");
+ ok(instanceOfCustomType.functionOnCustomType,
+ "Got the expected method in the CustomType instance");
+
+ // TODO: test support events and properties in a SubModuleType defined in the schema,
+ // once implemented, e.g.:
+ //
+ // ok(instanceOfCustomType.url,
+ // "Got the expected property defined in the CustomType instance)
+ //
+ // ok(instanceOfCustomType.onEvent &&
+ // instanceOfCustomType.onEvent.addListener &&
+ // typeof instanceOfCustomType.onEvent.addListener == "function",
+ // "Got the expected event defined in the CustomType instance");
+});
+
+add_task(function* testLocalAPIImplementation() {
+ let countGet2 = 0;
+ let countProp3 = 0;
+ let countProp3SubFoo = 0;
+
+ let testingApiObj = {
+ get PROP1() {
+ // PROP1 is a schema-defined constant.
+ throw new Error("Unexpected get PROP1");
+ },
+ get prop2() {
+ ++countGet2;
+ return "prop2 val";
+ },
+ get prop3() {
+ throw new Error("Unexpected get prop3");
+ },
+ set prop3(v) {
+ // prop3 is a submodule, defined as a function, so the API should not pass
+ // through assignment to prop3.
+ throw new Error("Unexpected set prop3");
+ },
+ };
+ let submoduleApiObj = {
+ get sub_foo() {
+ ++countProp3;
+ return () => {
+ return ++countProp3SubFoo;
+ };
+ },
+ };
+
+ let localWrapper = {
+ shouldInject(ns) {
+ return ns == "testing" || ns == "testing.prop3";
+ },
+ getImplementation(ns, name) {
+ do_check_true(ns == "testing" || ns == "testing.prop3");
+ if (ns == "testing.prop3" && name == "sub_foo") {
+ // It is fine to use `null` here because we don't call async functions.
+ return new LocalAPIImplementation(submoduleApiObj, name, null);
+ }
+ // It is fine to use `null` here because we don't call async functions.
+ return new LocalAPIImplementation(testingApiObj, name, null);
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+ do_check_eq(countGet2, 0);
+ do_check_eq(countProp3, 0);
+ do_check_eq(countProp3SubFoo, 0);
+
+ do_check_eq(root.testing.PROP1, 20);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 1);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 2);
+
+ do_print(JSON.stringify(root.testing));
+ do_check_eq(root.testing.prop3.sub_foo(), 1);
+ do_check_eq(countProp3, 1);
+ do_check_eq(countProp3SubFoo, 1);
+
+ do_check_eq(root.testing.prop3.sub_foo(), 2);
+ do_check_eq(countProp3, 2);
+ do_check_eq(countProp3SubFoo, 2);
+
+ root.testing.prop3.sub_foo = () => { return "overwritten"; };
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten");
+
+ root.testing.prop3 = {sub_foo() { return "overwritten again"; }};
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten again");
+ do_check_eq(countProp3SubFoo, 2);
+});
+
+
+let defaultsJson = [
+ {namespace: "defaultsJson",
+
+ types: [],
+
+ functions: [
+ {
+ name: "defaultFoo",
+ type: "function",
+ parameters: [
+ {name: "arg", type: "object", optional: true, properties: {
+ prop1: {type: "integer", optional: true},
+ }, default: {prop1: 1}},
+ ],
+ returns: {
+ type: "object",
+ },
+ },
+ ]},
+];
+
+add_task(function* testDefaults() {
+ let url = "data:," + JSON.stringify(defaultsJson);
+ yield Schemas.load(url);
+
+ let testingApiObj = {
+ defaultFoo: function(arg) {
+ if (Object.keys(arg) != "prop1") {
+ throw new Error(`Received the expected default object, default: ${JSON.stringify(arg)}`);
+ }
+ arg.newProp = 1;
+ return arg;
+ },
+ };
+
+ let localWrapper = {
+ shouldInject(ns) {
+ return true;
+ },
+ getImplementation(ns, name) {
+ return new LocalAPIImplementation(testingApiObj, name, null);
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+
+ deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1});
+ deepEqual(root.defaultsJson.defaultFoo({prop1: 2}), {prop1: 2, newProp: 1});
+ deepEqual(root.defaultsJson.defaultFoo(), {prop1: 1, newProp: 1});
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js
new file mode 100644
index 0000000000..606459764c
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_allowed_contexts.js
@@ -0,0 +1,147 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let schemaJson = [
+ {
+ namespace: "noAllowedContexts",
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_zero", "test_one"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_one"]},
+ },
+ },
+ {
+ namespace: "defaultContexts",
+ defaultContexts: ["test_two"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_three"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_two"]},
+ },
+ },
+ {
+ namespace: "withAllowedContexts",
+ allowedContexts: ["test_four"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_five"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_three"]},
+ },
+ },
+ {
+ namespace: "withAllowedContextsAndDefault",
+ allowedContexts: ["test_six"],
+ defaultContexts: ["test_seven"],
+ properties: {
+ prop1: {type: "object"},
+ prop2: {type: "object", allowedContexts: ["test_eight"]},
+ prop3: {type: "number", value: 1},
+ prop4: {type: "number", value: 1, allowedContexts: ["numeric_four"]},
+ },
+ },
+ {
+ namespace: "with_submodule",
+ defaultContexts: ["test_nine"],
+ types: [{
+ id: "subtype",
+ type: "object",
+ functions: [{
+ name: "noAllowedContexts",
+ type: "function",
+ parameters: [],
+ }, {
+ name: "allowedContexts",
+ allowedContexts: ["test_ten"],
+ type: "function",
+ parameters: [],
+ }],
+ }],
+ properties: {
+ prop1: {$ref: "subtype"},
+ prop2: {$ref: "subtype", allowedContexts: ["test_eleven"]},
+ },
+ },
+];
+add_task(function* testRestrictions() {
+ let url = "data:," + JSON.stringify(schemaJson);
+ yield Schemas.load(url);
+ let results = {};
+ let localWrapper = {
+ shouldInject(ns, name, allowedContexts) {
+ name = name === null ? ns : ns + "." + name;
+ results[name] = allowedContexts.join(",");
+ return true;
+ },
+ getImplementation() {
+ // The actual implementation is not significant for this test.
+ // Let's take this opportunity to see if schema generation is free of
+ // exceptions even when somehow getImplementation does not return an
+ // implementation.
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+
+ function verify(path, expected) {
+ let obj = root;
+ for (let thing of path.split(".")) {
+ try {
+ obj = obj[thing];
+ } catch (e) {
+ // Blech.
+ }
+ }
+
+ let result = results[path];
+ equal(result, expected);
+ }
+
+ verify("noAllowedContexts", "");
+ verify("noAllowedContexts.prop1", "");
+ verify("noAllowedContexts.prop2", "test_zero,test_one");
+ verify("noAllowedContexts.prop3", "");
+ verify("noAllowedContexts.prop4", "numeric_one");
+
+ verify("defaultContexts", "");
+ verify("defaultContexts.prop1", "test_two");
+ verify("defaultContexts.prop2", "test_three");
+ verify("defaultContexts.prop3", "test_two");
+ verify("defaultContexts.prop4", "numeric_two");
+
+ verify("withAllowedContexts", "test_four");
+ verify("withAllowedContexts.prop1", "");
+ verify("withAllowedContexts.prop2", "test_five");
+ verify("withAllowedContexts.prop3", "");
+ verify("withAllowedContexts.prop4", "numeric_three");
+
+ verify("withAllowedContextsAndDefault", "test_six");
+ verify("withAllowedContextsAndDefault.prop1", "test_seven");
+ verify("withAllowedContextsAndDefault.prop2", "test_eight");
+ verify("withAllowedContextsAndDefault.prop3", "test_seven");
+ verify("withAllowedContextsAndDefault.prop4", "numeric_four");
+
+ verify("with_submodule", "");
+ verify("with_submodule.prop1", "test_nine");
+ verify("with_submodule.prop1.noAllowedContexts", "test_nine");
+ verify("with_submodule.prop1.allowedContexts", "test_ten");
+ verify("with_submodule.prop2", "test_eleven");
+ // Note: test_nine inherits allowed contexts from the namespace, not from
+ // submodule. There is no "defaultContexts" for submodule types to not
+ // complicate things.
+ verify("with_submodule.prop1.noAllowedContexts", "test_nine");
+ verify("with_submodule.prop1.allowedContexts", "test_ten");
+
+ // This is a constant, so it does not matter that getImplementation does not
+ // return an implementation since the API injector should take care of it.
+ equal(root.noAllowedContexts.prop3, 1);
+
+ Assert.throws(() => root.noAllowedContexts.prop1,
+ /undefined/,
+ "Should throw when the implementation is absent.");
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
new file mode 100644
index 0000000000..36d88d7222
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
@@ -0,0 +1,102 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let {
+ BaseContext,
+ SchemaAPIManager,
+} = ExtensionCommon;
+
+let nestedNamespaceJson = [
+ {
+ "namespace": "backgroundAPI.testnamespace",
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ "returns": {
+ "type": "string",
+ },
+ },
+ ],
+ },
+ {
+ "namespace": "noBackgroundAPI.testnamespace",
+ "functions": [
+ {
+ "name": "create",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "title",
+ "type": "string",
+ },
+ ],
+ },
+ ],
+ },
+];
+
+let global = this;
+class StubContext extends BaseContext {
+ constructor() {
+ let fakeExtension = {id: "test@web.extension"};
+ super("addon_child", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ this.viewType = "background";
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+}
+
+add_task(function* testSchemaAPIInjection() {
+ let url = "data:," + JSON.stringify(nestedNamespaceJson);
+
+ // Load the schema of the fake APIs.
+ yield Schemas.load(url);
+
+ let apiManager = new SchemaAPIManager("addon");
+
+ // Register an API that will skip the background page.
+ apiManager.registerSchemaAPI("noBackgroundAPI.testnamespace", "addon_child", context => {
+ // This API should not be available in this context, return null so that
+ // the schema wrapper is removed as well.
+ return null;
+ });
+
+ // Register an API that will skip any but the background page.
+ apiManager.registerSchemaAPI("backgroundAPI.testnamespace", "addon_child", context => {
+ if (context.viewType === "background") {
+ return {
+ backgroundAPI: {
+ testnamespace: {
+ create(title) {
+ return title;
+ },
+ },
+ },
+ };
+ }
+
+ // This API should not be available in this context, return null so that
+ // the schema wrapper is removed as well.
+ return null;
+ });
+
+ let context = new StubContext();
+ let browserObj = {};
+ apiManager.generateAPIs(context, browserObj);
+
+ do_check_eq(browserObj.noBackgroundAPI, undefined);
+ const res = browserObj.backgroundAPI.testnamespace.create("param-value");
+ do_check_eq(res, "param-value");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
new file mode 100644
index 0000000000..6397d1f96a
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
@@ -0,0 +1,232 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+
+let {BaseContext, LocalAPIImplementation} = ExtensionCommon;
+
+let schemaJson = [
+ {
+ namespace: "testnamespace",
+ functions: [{
+ name: "one_required",
+ type: "function",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ }],
+ }, {
+ name: "one_optional",
+ type: "function",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ optional: true,
+ }],
+ }, {
+ name: "async_required",
+ type: "function",
+ async: "first",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ }],
+ }, {
+ name: "async_optional",
+ type: "function",
+ async: "first",
+ parameters: [{
+ name: "first",
+ type: "function",
+ parameters: [],
+ optional: true,
+ }],
+ }],
+ },
+];
+
+const global = this;
+class StubContext extends BaseContext {
+ constructor() {
+ let fakeExtension = {id: "test@web.extension"};
+ super("testEnv", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ }
+
+ get cloneScope() {
+ return this.sandbox;
+ }
+
+ get principal() {
+ return Cu.getObjectPrincipal(this.sandbox);
+ }
+}
+
+let context;
+
+function generateAPIs(extraWrapper, apiObj) {
+ context = new StubContext();
+ let localWrapper = {
+ shouldInject() {
+ return true;
+ },
+ getImplementation(namespace, name) {
+ return new LocalAPIImplementation(apiObj, name, context);
+ },
+ };
+ Object.assign(localWrapper, extraWrapper);
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+ return root.testnamespace;
+}
+
+add_task(function* testParameterValidation() {
+ yield Schemas.load("data:," + JSON.stringify(schemaJson));
+
+ let testnamespace;
+ function assertThrows(name, ...args) {
+ Assert.throws(() => testnamespace[name](...args),
+ /Incorrect argument types/,
+ `Expected testnamespace.${name}(${args.map(String).join(", ")}) to throw.`);
+ }
+ function assertNoThrows(name, ...args) {
+ try {
+ testnamespace[name](...args);
+ } catch (e) {
+ do_print(`testnamespace.${name}(${args.map(String).join(", ")}) unexpectedly threw.`);
+ throw new Error(e);
+ }
+ }
+ let cb = () => {};
+
+ for (let isChromeCompat of [true, false]) {
+ do_print(`Testing API validation with isChromeCompat=${isChromeCompat}`);
+ testnamespace = generateAPIs({
+ isChromeCompat,
+ }, {
+ one_required() {},
+ one_optional() {},
+ async_required() {},
+ async_optional() {},
+ });
+
+ assertThrows("one_required");
+ assertThrows("one_required", null);
+ assertNoThrows("one_required", cb);
+ assertThrows("one_required", cb, null);
+ assertThrows("one_required", cb, cb);
+
+ assertNoThrows("one_optional");
+ assertNoThrows("one_optional", null);
+ assertNoThrows("one_optional", cb);
+ assertThrows("one_optional", cb, null);
+ assertThrows("one_optional", cb, cb);
+
+ // Schema-based validation happens before an async method is called, so
+ // errors should be thrown synchronously.
+
+ // The parameter was declared as required, but there was also an "async"
+ // attribute with the same value as the parameter name, so the callback
+ // parameter is actually optional.
+ assertNoThrows("async_required");
+ assertNoThrows("async_required", null);
+ assertNoThrows("async_required", cb);
+ assertThrows("async_required", cb, null);
+ assertThrows("async_required", cb, cb);
+
+ assertNoThrows("async_optional");
+ assertNoThrows("async_optional", null);
+ assertNoThrows("async_optional", cb);
+ assertThrows("async_optional", cb, null);
+ assertThrows("async_optional", cb, cb);
+ }
+});
+
+add_task(function* testAsyncResults() {
+ yield Schemas.load("data:," + JSON.stringify(schemaJson));
+ function* runWithCallback(func) {
+ do_print(`Calling testnamespace.${func.name}, expecting callback with result`);
+ return yield new Promise(resolve => {
+ let result = "uninitialized value";
+ let returnValue = func(reply => {
+ result = reply;
+ resolve(result);
+ });
+ // When a callback is given, the return value must be missing.
+ do_check_eq(returnValue, undefined);
+ // Callback must be called asynchronously.
+ do_check_eq(result, "uninitialized value");
+ });
+ }
+
+ function* runFailCallback(func) {
+ do_print(`Calling testnamespace.${func.name}, expecting callback with error`);
+ return yield new Promise(resolve => {
+ func(reply => {
+ do_check_eq(reply, undefined);
+ resolve(context.lastError.message); // eslint-disable-line no-undef
+ });
+ });
+ }
+
+ for (let isChromeCompat of [true, false]) {
+ do_print(`Testing API invocation with isChromeCompat=${isChromeCompat}`);
+ let testnamespace = generateAPIs({
+ isChromeCompat,
+ }, {
+ async_required(cb) {
+ do_check_eq(cb, undefined);
+ return Promise.resolve(1);
+ },
+ async_optional(cb) {
+ do_check_eq(cb, undefined);
+ return Promise.resolve(2);
+ },
+ });
+ if (!isChromeCompat) { // No promises for chrome.
+ do_print("testnamespace.async_required should be a Promise");
+ let promise = testnamespace.async_required();
+ do_check_true(promise instanceof context.cloneScope.Promise);
+ do_check_eq(yield promise, 1);
+
+ do_print("testnamespace.async_optional should be a Promise");
+ promise = testnamespace.async_optional();
+ do_check_true(promise instanceof context.cloneScope.Promise);
+ do_check_eq(yield promise, 2);
+ }
+
+ do_check_eq(yield* runWithCallback(testnamespace.async_required), 1);
+ do_check_eq(yield* runWithCallback(testnamespace.async_optional), 2);
+
+ let otherSandbox = Cu.Sandbox(null, {});
+ let errorFactories = [
+ msg => { throw new context.cloneScope.Error(msg); },
+ msg => context.cloneScope.Promise.reject({message: msg}),
+ msg => Cu.evalInSandbox(`throw new Error("${msg}")`, otherSandbox),
+ msg => Cu.evalInSandbox(`Promise.reject({message: "${msg}"})`, otherSandbox),
+ ];
+ for (let makeError of errorFactories) {
+ do_print(`Testing callback/promise with error caused by: ${makeError}`);
+ testnamespace = generateAPIs({
+ isChromeCompat,
+ }, {
+ async_required() { return makeError("ONE"); },
+ async_optional() { return makeError("TWO"); },
+ });
+
+ if (!isChromeCompat) { // No promises for chrome.
+ yield Assert.rejects(testnamespace.async_required(), /ONE/,
+ "should reject testnamespace.async_required()").catch(() => {});
+ yield Assert.rejects(testnamespace.async_optional(), /TWO/,
+ "should reject testnamespace.async_optional()").catch(() => {});
+ }
+
+ do_check_eq(yield* runFailCallback(testnamespace.async_required), "ONE");
+ do_check_eq(yield* runFailCallback(testnamespace.async_optional), "TWO");
+ }
+ }
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_simple.js b/toolkit/components/extensions/test/xpcshell/test_ext_simple.js
new file mode 100644
index 0000000000..91b10354c6
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_simple.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_simple() {
+ let extensionData = {
+ manifest: {
+ "name": "Simple extension test",
+ "version": "1.0",
+ "manifest_version": 2,
+ "description": "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.unload();
+});
+
+add_task(function* test_background() {
+ function background() {
+ browser.test.log("running background script");
+
+ browser.test.onMessage.addListener((x, y) => {
+ browser.test.assertEq(x, 10, "x is 10");
+ browser.test.assertEq(y, 20, "y is 20");
+
+ browser.test.notifyPass("background test passed");
+ });
+
+ browser.test.sendMessage("running", 1);
+ }
+
+ let extensionData = {
+ background,
+ manifest: {
+ "name": "Simple extension test",
+ "version": "1.0",
+ "manifest_version": 2,
+ "description": "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
+ equal(x, 1, "got correct value from extension");
+
+ extension.sendMessage(10, 20);
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
+add_task(function* test_extensionTypes() {
+ let extensionData = {
+ background: function() {
+ browser.test.assertEq(typeof browser.extensionTypes, "object", "browser.extensionTypes exists");
+ browser.test.assertEq(typeof browser.extensionTypes.RunAt, "object", "browser.extensionTypes.RunAt exists");
+ browser.test.notifyPass("extentionTypes test passed");
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ yield extension.startup();
+ yield extension.awaitFinish();
+ yield extension.unload();
+});
+
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage.js
new file mode 100644
index 0000000000..df46dfb632
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage.js
@@ -0,0 +1,334 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const STORAGE_SYNC_PREF = "webextensions.storage.sync.enabled";
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+/**
+ * Utility function to ensure that all supported APIs for getting are
+ * tested.
+ *
+ * @param {string} areaName
+ * either "local" or "sync" according to what we want to test
+ * @param {string} prop
+ * "key" to look up using the storage API
+ * @param {Object} value
+ * "value" to compare against
+ */
+async function checkGetImpl(areaName, prop, value) {
+ let storage = browser.storage[areaName];
+
+ let data = await storage.get(null);
+ browser.test.assertEq(value, data[prop], `null getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get(prop);
+ browser.test.assertEq(value, data[prop], `string getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get([prop]);
+ browser.test.assertEq(value, data[prop], `array getter worked for ${prop} in ${areaName}`);
+
+ data = await storage.get({[prop]: undefined});
+ browser.test.assertEq(value, data[prop], `object getter worked for ${prop} in ${areaName}`);
+}
+
+add_task(function* test_local_cache_invalidation() {
+ function background(checkGet) {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "set-initial") {
+ await browser.storage.local.set({"test-prop1": "value1", "test-prop2": "value2"});
+ browser.test.sendMessage("set-initial-done");
+ } else if (msg === "check") {
+ await checkGet("local", "test-prop1", "value1");
+ await checkGet("local", "test-prop2", "value2");
+ browser.test.sendMessage("check-done");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("set-initial");
+ yield extension.awaitMessage("set-initial-done");
+
+ Services.obs.notifyObservers(null, "extension-invalidate-storage-cache", "");
+
+ extension.sendMessage("check");
+ yield extension.awaitMessage("check-done");
+
+ yield extension.unload();
+});
+
+add_task(function* test_config_flag_needed() {
+ function background() {
+ let promises = [];
+ let apiTests = [
+ {method: "get", args: ["foo"]},
+ {method: "set", args: [{foo: "bar"}]},
+ {method: "remove", args: ["foo"]},
+ {method: "clear", args: []},
+ ];
+ apiTests.forEach(testDef => {
+ promises.push(browser.test.assertRejects(
+ browser.storage.sync[testDef.method](...testDef.args),
+ "Please set webextensions.storage.sync.enabled to true in about:config",
+ `storage.sync.${testDef.method} is behind a flag`));
+ });
+
+ Promise.all(promises).then(() => browser.test.notifyPass("flag needed"));
+ }
+
+ ok(!Preferences.get(STORAGE_SYNC_PREF));
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})(${checkGetImpl})`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitFinish("flag needed");
+ yield extension.unload();
+});
+
+add_task(function* test_reloading_extensions_works() {
+ // Just some random extension ID that we can re-use
+ const extensionId = "my-extension-id@1";
+
+ function loadExtension() {
+ function background() {
+ browser.storage.sync.set({"a": "b"}).then(() => {
+ browser.test.notifyPass("set-works");
+ });
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["storage"],
+ },
+ background: `(${background})()`,
+ }, extensionId);
+ }
+
+ Preferences.set(STORAGE_SYNC_PREF, true);
+
+ let extension1 = loadExtension();
+
+ yield extension1.startup();
+ yield extension1.awaitFinish("set-works");
+ yield extension1.unload();
+
+ let extension2 = loadExtension();
+
+ yield extension2.startup();
+ yield extension2.awaitFinish("set-works");
+ yield extension2.unload();
+
+ Preferences.reset(STORAGE_SYNC_PREF);
+});
+
+do_register_cleanup(() => {
+ Preferences.reset(STORAGE_SYNC_PREF);
+});
+
+add_task(function* test_backgroundScript() {
+ async function backgroundScript(checkGet) {
+ let globalChanges, gResolve;
+ function clearGlobalChanges() {
+ globalChanges = new Promise(resolve => { gResolve = resolve; });
+ }
+ clearGlobalChanges();
+ let expectedAreaName;
+
+ browser.storage.onChanged.addListener((changes, areaName) => {
+ browser.test.assertEq(expectedAreaName, areaName,
+ "Expected area name received by listener");
+ gResolve(changes);
+ });
+
+ async function checkChanges(areaName, changes, message) {
+ function checkSub(obj1, obj2) {
+ for (let prop in obj1) {
+ browser.test.assertTrue(obj1[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertTrue(obj2[prop] !== undefined,
+ `checkChanges ${areaName} ${prop} is missing (${message})`);
+ browser.test.assertEq(obj1[prop].oldValue, obj2[prop].oldValue,
+ `checkChanges ${areaName} ${prop} old (${message})`);
+ browser.test.assertEq(obj1[prop].newValue, obj2[prop].newValue,
+ `checkChanges ${areaName} ${prop} new (${message})`);
+ }
+ }
+
+ const recentChanges = await globalChanges;
+ checkSub(changes, recentChanges);
+ checkSub(recentChanges, changes);
+ clearGlobalChanges();
+ }
+
+ /* eslint-disable dot-notation */
+ async function runTests(areaName) {
+ expectedAreaName = areaName;
+ let storage = browser.storage[areaName];
+ // Set some data and then test getters.
+ try {
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ await checkChanges(areaName,
+ {"test-prop1": {newValue: "value1"}, "test-prop2": {newValue: "value2"}},
+ "set (a)");
+
+ await checkGet(areaName, "test-prop1", "value1");
+ await checkGet(areaName, "test-prop2", "value2");
+
+ let data = await storage.get({"test-prop1": undefined, "test-prop2": undefined, "other": "default"});
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (a)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (a)");
+ browser.test.assertEq("default", data["other"], "other correct");
+
+ data = await storage.get(["test-prop1", "test-prop2", "other"]);
+ browser.test.assertEq("value1", data["test-prop1"], "prop1 correct (b)");
+ browser.test.assertEq("value2", data["test-prop2"], "prop2 correct (b)");
+ browser.test.assertFalse("other" in data, "other correct");
+
+ // Remove data in various ways.
+ await storage.remove("test-prop1");
+ await checkChanges(areaName, {"test-prop1": {oldValue: "value1"}}, "remove string");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove string)");
+ browser.test.assertTrue("test-prop2" in data, "prop2 present (remove string)");
+
+ await storage.set({"test-prop1": "value1"});
+ await checkChanges(areaName, {"test-prop1": {newValue: "value1"}}, "set (c)");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertEq(data["test-prop1"], "value1", "prop1 correct (c)");
+ browser.test.assertEq(data["test-prop2"], "value2", "prop2 correct (c)");
+
+ await storage.remove(["test-prop1", "test-prop2"]);
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "remove array");
+
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (remove array)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (remove array)");
+
+ // test storage.clear
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+ // Make sure that set() handler happened before we clear the
+ // promise again.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.clear();
+
+ await checkChanges(areaName,
+ {"test-prop1": {oldValue: "value1"}, "test-prop2": {oldValue: "value2"}},
+ "clear");
+ data = await storage.get(["test-prop1", "test-prop2"]);
+ browser.test.assertFalse("test-prop1" in data, "prop1 absent (clear)");
+ browser.test.assertFalse("test-prop2" in data, "prop2 absent (clear)");
+
+ // Make sure we can store complex JSON data.
+ // known previous values
+ await storage.set({"test-prop1": "value1", "test-prop2": "value2"});
+
+ // Make sure the set() handler landed.
+ await globalChanges;
+
+ clearGlobalChanges();
+ await storage.set({
+ "test-prop1": {
+ str: "hello",
+ bool: true,
+ null: null,
+ undef: undefined,
+ obj: {},
+ arr: [1, 2],
+ date: new Date(0),
+ regexp: /regexp/,
+ func: function func() {},
+ window,
+ },
+ });
+
+ await storage.set({"test-prop2": function func() {}});
+ const recentChanges = await globalChanges;
+
+ browser.test.assertEq("value1", recentChanges["test-prop1"].oldValue, "oldValue correct");
+ browser.test.assertEq("object", typeof(recentChanges["test-prop1"].newValue), "newValue is obj");
+ clearGlobalChanges();
+
+ data = await storage.get({"test-prop1": undefined, "test-prop2": undefined});
+ let obj = data["test-prop1"];
+
+ browser.test.assertEq("hello", obj.str, "string part correct");
+ browser.test.assertEq(true, obj.bool, "bool part correct");
+ browser.test.assertEq(null, obj.null, "null part correct");
+ browser.test.assertEq(undefined, obj.undef, "undefined part correct");
+ browser.test.assertEq(undefined, obj.func, "function part correct");
+ browser.test.assertEq(undefined, obj.window, "window part correct");
+ browser.test.assertEq("1970-01-01T00:00:00.000Z", obj.date, "date part correct");
+ browser.test.assertEq("/regexp/", obj.regexp, "regexp part correct");
+ browser.test.assertEq("object", typeof(obj.obj), "object part correct");
+ browser.test.assertTrue(Array.isArray(obj.arr), "array part present");
+ browser.test.assertEq(1, obj.arr[0], "arr[0] part correct");
+ browser.test.assertEq(2, obj.arr[1], "arr[1] part correct");
+ browser.test.assertEq(2, obj.arr.length, "arr.length part correct");
+
+ obj = data["test-prop2"];
+
+ browser.test.assertEq("[object Object]", {}.toString.call(obj), "function serialized as a plain object");
+ browser.test.assertEq(0, Object.keys(obj).length, "function serialized as an empty object");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("storage");
+ }
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ let promise;
+ if (msg === "test-local") {
+ promise = runTests("local");
+ } else if (msg === "test-sync") {
+ promise = runTests("sync");
+ }
+ promise.then(() => browser.test.sendMessage("test-finished"));
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extensionData = {
+ background: `(${backgroundScript})(${checkGetImpl})`,
+ manifest: {
+ permissions: ["storage"],
+ },
+ };
+
+ Preferences.set(STORAGE_SYNC_PREF, true);
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ yield extension.startup();
+ yield extension.awaitMessage("ready");
+
+ extension.sendMessage("test-local");
+ yield extension.awaitMessage("test-finished");
+
+ extension.sendMessage("test-sync");
+ yield extension.awaitMessage("test-finished");
+
+ Preferences.reset(STORAGE_SYNC_PREF);
+ yield extension.unload();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
new file mode 100644
index 0000000000..4258289e39
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -0,0 +1,1073 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+do_get_profile(); // so we can use FxAccounts
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
+const {
+ CollectionKeyEncryptionRemoteTransformer,
+ cryptoCollection,
+ idToKey,
+ extensionIdToCollectionId,
+ keyToId,
+} = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm");
+Cu.import("resource://services-sync/engines/extension-storage.js");
+Cu.import("resource://services-sync/keys.js");
+Cu.import("resource://services-sync/util.js");
+
+/* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */
+/* globals KeyRingEncryptionRemoteTransformer */
+/* globals Utils */
+
+function handleCannedResponse(cannedResponse, request, response) {
+ response.setStatusLine(null, cannedResponse.status.status,
+ cannedResponse.status.statusText);
+ // send the headers
+ for (let headerLine of cannedResponse.sampleHeaders) {
+ let headerElements = headerLine.split(":");
+ response.setHeader(headerElements[0], headerElements[1].trimLeft());
+ }
+ response.setHeader("Date", (new Date()).toUTCString());
+
+ response.write(cannedResponse.responseBody);
+}
+
+function collectionRecordsPath(collectionId) {
+ return `/buckets/default/collections/${collectionId}/records`;
+}
+
+class KintoServer {
+ constructor() {
+ // Set up an HTTP Server
+ this.httpServer = new HttpServer();
+ this.httpServer.start(-1);
+
+ // Map<CollectionId, Set<Object>> corresponding to the data in the
+ // Kinto server
+ this.collections = new Map();
+
+ // ETag to serve with responses
+ this.etag = 1;
+
+ this.port = this.httpServer.identity.primaryPort;
+ // POST requests we receive from the client go here
+ this.posts = [];
+ // DELETEd buckets will go here.
+ this.deletedBuckets = [];
+ // Anything in here will force the next POST to generate a conflict
+ this.conflicts = [];
+
+ this.installConfigPath();
+ this.installBatchPath();
+ this.installCatchAll();
+ }
+
+ clearPosts() {
+ this.posts = [];
+ }
+
+ getPosts() {
+ return this.posts;
+ }
+
+ getDeletedBuckets() {
+ return this.deletedBuckets;
+ }
+
+ installConfigPath() {
+ const configPath = "/v1/";
+ const responseBody = JSON.stringify({
+ "settings": {"batch_max_requests": 25},
+ "url": `http://localhost:${this.port}/v1/`,
+ "documentation": "https://kinto.readthedocs.org/",
+ "version": "1.5.1",
+ "commit": "cbc6f58",
+ "hello": "kinto",
+ });
+ const configResponse = {
+ "sampleHeaders": [
+ "Access-Control-Allow-Origin: *",
+ "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+ "Content-Type: application/json; charset=UTF-8",
+ "Server: waitress",
+ ],
+ "status": {status: 200, statusText: "OK"},
+ "responseBody": responseBody,
+ };
+
+ function handleGetConfig(request, response) {
+ if (request.method != "GET") {
+ dump(`ARGH, got ${request.method}\n`);
+ }
+ return handleCannedResponse(configResponse, request, response);
+ }
+
+ this.httpServer.registerPathHandler(configPath, handleGetConfig);
+ }
+
+ installBatchPath() {
+ const batchPath = "/v1/batch";
+
+ function handlePost(request, response) {
+ let bodyStr = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let body = JSON.parse(bodyStr);
+ let defaults = body.defaults;
+ for (let req of body.requests) {
+ let headers = Object.assign({}, defaults && defaults.headers || {}, req.headers);
+ // FIXME: assert auth is "Bearer ...token..."
+ this.posts.push(Object.assign({}, req, {headers}));
+ }
+
+ response.setStatusLine(null, 200, "OK");
+ response.setHeader("Content-Type", "application/json; charset=UTF-8");
+ response.setHeader("Date", (new Date()).toUTCString());
+
+ let postResponse = {
+ responses: body.requests.map(req => {
+ let oneBody;
+ if (req.method == "DELETE") {
+ let id = req.path.match(/^\/buckets\/default\/collections\/.+\/records\/(.+)$/)[1];
+ oneBody = {
+ "data": {
+ "deleted": true,
+ "id": id,
+ "last_modified": this.etag,
+ },
+ };
+ } else {
+ oneBody = {"data": Object.assign({}, req.body.data, {last_modified: this.etag}),
+ "permissions": []};
+ }
+
+ return {
+ path: req.path,
+ status: 201, // FIXME -- only for new posts??
+ headers: {"ETag": 3000}, // FIXME???
+ body: oneBody,
+ };
+ }),
+ };
+
+ if (this.conflicts.length > 0) {
+ const {collectionId, encrypted} = this.conflicts.shift();
+ this.collections.get(collectionId).add(encrypted);
+ dump(`responding with etag ${this.etag}\n`);
+ postResponse = {
+ responses: body.requests.map(req => {
+ return {
+ path: req.path,
+ status: 412,
+ headers: {"ETag": this.etag}, // is this correct??
+ body: {
+ details: {
+ existing: encrypted,
+ },
+ },
+ };
+ }),
+ };
+ }
+
+ response.write(JSON.stringify(postResponse));
+
+ // "sampleHeaders": [
+ // "Access-Control-Allow-Origin: *",
+ // "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+ // "Server: waitress",
+ // "Etag: \"4000\""
+ // ],
+ }
+
+ this.httpServer.registerPathHandler(batchPath, handlePost.bind(this));
+ }
+
+ installCatchAll() {
+ this.httpServer.registerPathHandler("/", (request, response) => {
+ dump(`got request: ${request.method}:${request.path}?${request.queryString}\n`);
+ dump(`${CommonUtils.readBytesFromInputStream(request.bodyInputStream)}\n`);
+ });
+ }
+
+ installCollection(collectionId) {
+ this.collections.set(collectionId, new Set());
+
+ const remoteRecordsPath = "/v1" + collectionRecordsPath(encodeURIComponent(collectionId));
+
+ function handleGetRecords(request, response) {
+ if (request.method != "GET") {
+ do_throw(`only GET is supported on ${remoteRecordsPath}`);
+ }
+
+ response.setStatusLine(null, 200, "OK");
+ response.setHeader("Content-Type", "application/json; charset=UTF-8");
+ response.setHeader("Date", (new Date()).toUTCString());
+ response.setHeader("ETag", this.etag.toString());
+
+ const records = this.collections.get(collectionId);
+ // Can't JSON a Set directly, so convert to Array
+ let data = Array.from(records);
+ if (request.queryString.includes("_since=")) {
+ data = data.filter(r => !(r._inPast || false));
+ }
+
+ // Remove records that we only needed to serve once.
+ // FIXME: come up with a more coherent idea of time here.
+ // See bug 1321570.
+ for (const record of records) {
+ if (record._onlyOnce) {
+ records.delete(record);
+ }
+ }
+
+ const body = JSON.stringify({
+ "data": data,
+ });
+ response.write(body);
+ }
+
+ this.httpServer.registerPathHandler(remoteRecordsPath, handleGetRecords.bind(this));
+ }
+
+ installDeleteBucket() {
+ this.httpServer.registerPrefixHandler("/v1/buckets/", (request, response) => {
+ if (request.method != "DELETE") {
+ dump(`got a non-delete action on bucket: ${request.method} ${request.path}\n`);
+ return;
+ }
+
+ const noPrefix = request.path.slice("/v1/buckets/".length);
+ const [bucket, afterBucket] = noPrefix.split("/", 1);
+ if (afterBucket && afterBucket != "") {
+ dump(`got a delete for a non-bucket: ${request.method} ${request.path}\n`);
+ }
+
+ this.deletedBuckets.push(bucket);
+ // Fake like this actually deletes the records.
+ for (const [, set] of this.collections) {
+ set.clear();
+ }
+
+ response.write(JSON.stringify({
+ data: {
+ deleted: true,
+ last_modified: 1475161309026,
+ id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME
+ },
+ }));
+ });
+ }
+
+ // Utility function to install a keyring at the start of a test.
+ installKeyRing(keysData, etag, {conflict = false} = {}) {
+ this.installCollection("storage-sync-crypto");
+ const keysRecord = {
+ "id": "keys",
+ "keys": keysData,
+ "last_modified": etag,
+ };
+ this.etag = etag;
+ const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord";
+ this[methodName](new KeyRingEncryptionRemoteTransformer(),
+ "storage-sync-crypto", keysRecord);
+ }
+
+ // Add an already-encrypted record.
+ addRecord(collectionId, record) {
+ this.collections.get(collectionId).add(record);
+ }
+
+ // Add a record that is only served if no `_since` is present.
+ //
+ // Since in real life, Kinto only serves a record as part of a
+ // changes feed if `_since` is before the record's modification
+ // time, this can be helpful to test certain kinds of syncing logic.
+ //
+ // FIXME: tracking of "time" in this mock server really needs to be
+ // implemented correctly rather than these hacks. See bug 1321570.
+ addRecordInPast(collectionId, record) {
+ record._inPast = true;
+ this.addRecord(collectionId, record);
+ }
+
+ encryptAndAddRecord(transformer, collectionId, record) {
+ return transformer.encode(record).then(encrypted => {
+ this.addRecord(collectionId, encrypted);
+ });
+ }
+
+ // Like encryptAndAddRecord, but add a flag that will only serve
+ // this record once.
+ //
+ // Since in real life, Kinto only serves a record as part of a changes feed
+ // once, this can be useful for testing complicated syncing logic.
+ //
+ // FIXME: This kind of logic really needs to be subsumed into some
+ // more-realistic tracking of "time" (simulated by etags). See bug 1321570.
+ encryptAndAddRecordOnlyOnce(transformer, collectionId, record) {
+ return transformer.encode(record).then(encrypted => {
+ encrypted._onlyOnce = true;
+ this.addRecord(collectionId, encrypted);
+ });
+ }
+
+ // Conflicts block the next push and then appear in the collection specified.
+ encryptAndAddRecordWithConflict(transformer, collectionId, record) {
+ return transformer.encode(record).then(encrypted => {
+ this.conflicts.push({collectionId, encrypted});
+ });
+ }
+
+ clearCollection(collectionId) {
+ this.collections.get(collectionId).clear();
+ }
+
+ stop() {
+ this.httpServer.stop(() => { });
+ }
+}
+
+// Run a block of code with access to a KintoServer.
+function* withServer(f) {
+ let server = new KintoServer();
+ // Point the sync.storage client to use the test server we've just started.
+ Services.prefs.setCharPref("webextensions.storage.sync.serverURL",
+ `http://localhost:${server.port}/v1`);
+ try {
+ yield* f(server);
+ } finally {
+ server.stop();
+ }
+}
+
+// Run a block of code with access to both a sync context and a
+// KintoServer. This is meant as a workaround for eslint's refusal to
+// let me have 5 nested callbacks.
+function* withContextAndServer(f) {
+ yield* withSyncContext(function* (context) {
+ yield* withServer(function* (server) {
+ yield* f(context, server);
+ });
+ });
+}
+
+// Run a block of code with fxa mocked out to return a specific user.
+function* withSignedInUser(user, f) {
+ const oldESSFxAccounts = ExtensionStorageSync._fxaService;
+ const oldERTFxAccounts = EncryptionRemoteTransformer.prototype._fxaService;
+ ExtensionStorageSync._fxaService = EncryptionRemoteTransformer.prototype._fxaService = {
+ getSignedInUser() {
+ return Promise.resolve(user);
+ },
+ getOAuthToken() {
+ return Promise.resolve("some-access-token");
+ },
+ sessionStatus() {
+ return Promise.resolve(true);
+ },
+ };
+
+ try {
+ yield* f();
+ } finally {
+ ExtensionStorageSync._fxaService = oldESSFxAccounts;
+ EncryptionRemoteTransformer.prototype._fxaService = oldERTFxAccounts;
+ }
+}
+
+// Some assertions that make it easier to write tests about what was
+// posted and when.
+
+// Assert that the request was made with the correct access token.
+// This should be true of all requests, so this is usually called from
+// another assertion.
+function assertAuthenticatedRequest(post) {
+ equal(post.headers.Authorization, "Bearer some-access-token");
+}
+
+// Assert that this post was made with the correct request headers to
+// create a new resource while protecting against someone else
+// creating it at the same time (in other words, "If-None-Match: *").
+// Also calls assertAuthenticatedRequest(post).
+function assertPostedNewRecord(post) {
+ assertAuthenticatedRequest(post);
+ equal(post.headers["If-None-Match"], "*");
+}
+
+// Assert that this post was made with the correct request headers to
+// update an existing resource while protecting against concurrent
+// modification (in other words, `If-Match: "${etag}"`).
+// Also calls assertAuthenticatedRequest(post).
+function assertPostedUpdatedRecord(post, since) {
+ assertAuthenticatedRequest(post);
+ equal(post.headers["If-Match"], `"${since}"`);
+}
+
+// Assert that this post was an encrypted keyring, and produce the
+// decrypted body. Sanity check the body while we're here.
+const assertPostedEncryptedKeys = Task.async(function* (post) {
+ equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys");
+
+ let body = yield new KeyRingEncryptionRemoteTransformer().decode(post.body.data);
+ ok(body.keys, `keys object should be present in decoded body`);
+ ok(body.keys.default, `keys object should have a default key`);
+ return body;
+});
+
+// assertEqual, but for keyring[extensionId] == key.
+function assertKeyRingKey(keyRing, extensionId, expectedKey, message) {
+ if (!message) {
+ message = `expected keyring's key for ${extensionId} to match ${expectedKey.keyPairB64}`;
+ }
+ ok(keyRing.hasKeysFor([extensionId]),
+ `expected keyring to have a key for ${extensionId}\n`);
+ deepEqual(keyRing.keyForCollection(extensionId).keyPairB64, expectedKey.keyPairB64,
+ message);
+}
+
+// Tests using this ID will share keys in local storage, so be careful.
+const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
+const defaultExtension = {id: defaultExtensionId};
+
+const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+const ANOTHER_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0";
+const loggedInUser = {
+ uid: "0123456789abcdef0123456789abcdef",
+ kB: BORING_KB,
+ oauthTokens: {
+ "sync:addon-storage": {
+ token: "some-access-token",
+ },
+ },
+};
+const defaultCollectionId = extensionIdToCollectionId(loggedInUser, defaultExtensionId);
+
+function uuid() {
+ const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ return uuidgen.generateUUID().toString();
+}
+
+add_task(function* test_key_to_id() {
+ equal(keyToId("foo"), "key-foo");
+ equal(keyToId("my-new-key"), "key-my_2D_new_2D_key");
+ equal(keyToId(""), "key-");
+ equal(keyToId("™"), "key-_2122_");
+ equal(keyToId("\b"), "key-_8_");
+ equal(keyToId("abc\ndef"), "key-abc_A_def");
+ equal(keyToId("Kinto's fancy_string"), "key-Kinto_27_s_20_fancy_5F_string");
+
+ const KEYS = ["foo", "my-new-key", "", "Kinto's fancy_string", "™", "\b"];
+ for (let key of KEYS) {
+ equal(idToKey(keyToId(key)), key);
+ }
+
+ equal(idToKey("hi"), null);
+ equal(idToKey("-key-hi"), null);
+ equal(idToKey("key--abcd"), null);
+ equal(idToKey("key-%"), null);
+ equal(idToKey("key-_HI"), null);
+ equal(idToKey("key-_HI_"), null);
+ equal(idToKey("key-"), "");
+ equal(idToKey("key-1"), "1");
+ equal(idToKey("key-_2D_"), "-");
+});
+
+add_task(function* test_extension_id_to_collection_id() {
+ const newKBUser = Object.assign(loggedInUser, {kB: ANOTHER_KB});
+ const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}";
+ const extensionId2 = "{9419cce6-5435-11e6-84bf-54ee758d6343}";
+
+ // "random" 32-char hex userid
+ equal(extensionIdToCollectionId(loggedInUser, extensionId),
+ "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d");
+ equal(extensionIdToCollectionId(loggedInUser, extensionId),
+ extensionIdToCollectionId(newKBUser, extensionId));
+ equal(extensionIdToCollectionId(loggedInUser, extensionId2),
+ "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8");
+});
+
+add_task(function* ensureKeysFor_posts_new_keys() {
+ const extensionId = uuid();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection("storage-sync-crypto");
+ server.etag = 1000;
+
+ let newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}`);
+
+ let posts = server.getPosts();
+ equal(posts.length, 1);
+ const post = posts[0];
+ assertPostedNewRecord(post);
+ const body = yield assertPostedEncryptedKeys(post);
+ ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
+
+ // Try adding another key to make sure that the first post was
+ // OK, even on a new profile.
+ yield cryptoCollection._clear();
+ server.clearPosts();
+ // Restore the first posted keyring
+ server.addRecordInPast("storage-sync-crypto", post.body.data);
+ const extensionId2 = uuid();
+ newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId2]);
+ ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`);
+ ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`);
+
+ posts = server.getPosts();
+ // FIXME: some kind of bug where we try to repush the
+ // server_wins version multiple times in a single sync. We
+ // actually push 5 times as of this writing.
+ // See bug 1321571.
+ // equal(posts.length, 1);
+ const newPost = posts[posts.length - 1];
+ const newBody = yield assertPostedEncryptedKeys(newPost);
+ ok(newBody.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
+ ok(newBody.keys.collections[extensionId2], `keys object should have a key for ${extensionId2}`);
+
+ });
+ });
+});
+
+add_task(function* ensureKeysFor_pulls_key() {
+ // ensureKeysFor is implemented by adding a key to our local record
+ // and doing a sync. This means that if the same key exists
+ // remotely, we get a "conflict". Ensure that we handle this
+ // correctly -- we keep the server key (since presumably it's
+ // already been used to encrypt records) and we don't wipe out other
+ // collections' keys.
+ const extensionId = uuid();
+ const extensionId2 = uuid();
+ const DEFAULT_KEY = new BulkKeyBundle("[default]");
+ DEFAULT_KEY.generateRandom();
+ const RANDOM_KEY = new BulkKeyBundle(extensionId);
+ RANDOM_KEY.generateRandom();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ const keysData = {
+ "default": DEFAULT_KEY.keyPairB64,
+ "collections": {
+ [extensionId]: RANDOM_KEY.keyPairB64,
+ },
+ };
+ server.installKeyRing(keysData, 999);
+
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY);
+
+ let posts = server.getPosts();
+ equal(posts.length, 0,
+ "ensureKeysFor shouldn't push when the server keyring has the right key");
+
+ // Another client generates a key for extensionId2
+ const newKey = new BulkKeyBundle(extensionId2);
+ newKey.generateRandom();
+ keysData.collections[extensionId2] = newKey.keyPairB64;
+ server.clearCollection("storage-sync-crypto");
+ server.installKeyRing(keysData, 1000);
+
+ let newCollectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId, extensionId2]);
+ assertKeyRingKey(newCollectionKeys, extensionId2, newKey);
+ assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY,
+ `ensureKeysFor shouldn't lose the old key for ${extensionId}`);
+
+ posts = server.getPosts();
+ equal(posts.length, 0, "ensureKeysFor shouldn't push when updating keys");
+ });
+ });
+});
+
+add_task(function* ensureKeysFor_handles_conflicts() {
+ // Syncing is done through a pull followed by a push of any merged
+ // changes. Accordingly, the only way to have a "true" conflict --
+ // i.e. with the server rejecting a change -- is if
+ // someone pushes changes between our pull and our push. Ensure that
+ // if this happens, we still behave sensibly (keep the remote key).
+ const extensionId = uuid();
+ const DEFAULT_KEY = new BulkKeyBundle("[default]");
+ DEFAULT_KEY.generateRandom();
+ const RANDOM_KEY = new BulkKeyBundle(extensionId);
+ RANDOM_KEY.generateRandom();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ const keysData = {
+ "default": DEFAULT_KEY.keyPairB64,
+ "collections": {
+ [extensionId]: RANDOM_KEY.keyPairB64,
+ },
+ };
+ server.installKeyRing(keysData, 765, {conflict: true});
+
+ yield cryptoCollection._clear();
+
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY,
+ `syncing keyring should keep the server key for ${extensionId}`);
+
+ let posts = server.getPosts();
+ equal(posts.length, 1,
+ "syncing keyring should have tried to post a keyring");
+ const failedPost = posts[0];
+ assertPostedNewRecord(failedPost);
+ let body = yield assertPostedEncryptedKeys(failedPost);
+ // This key will be the one the client generated locally, so
+ // we don't know what its value will be
+ ok(body.keys.collections[extensionId],
+ `decrypted failed post should have a key for ${extensionId}`);
+ notEqual(body.keys.collections[extensionId], RANDOM_KEY.keyPairB64,
+ `decrypted failed post should have a randomly-generated key for ${extensionId}`);
+ });
+ });
+});
+
+add_task(function* checkSyncKeyRing_reuploads_keys() {
+ // Verify that when keys are present, they are reuploaded with the
+ // new kB when we call touchKeys().
+ const extensionId = uuid();
+ let extensionKey;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection("storage-sync-crypto");
+ server.etag = 765;
+
+ yield cryptoCollection._clear();
+
+ // Do an `ensureKeysFor` to generate some keys.
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(collectionKeys.hasKeysFor([extensionId]),
+ `ensureKeysFor should return a keyring that has a key for ${extensionId}`);
+ extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
+ equal(server.getPosts().length, 1,
+ "generating a key that doesn't exist on the server should post it");
+ });
+
+ // The user changes their password. This is their new kB, with
+ // the last f changed to an e.
+ const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
+ const newUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+ let postedKeys;
+ yield* withSignedInUser(newUser, function* () {
+ yield ExtensionStorageSync.checkSyncKeyRing();
+
+ let posts = server.getPosts();
+ equal(posts.length, 2,
+ "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
+ postedKeys = posts[1];
+ assertPostedUpdatedRecord(postedKeys, 765);
+
+ let body = yield assertPostedEncryptedKeys(postedKeys);
+ deepEqual(body.keys.collections[extensionId], extensionKey,
+ `the posted keyring should have the same key for ${extensionId} as the old one`);
+ });
+
+ // Verify that with the old kB, we can't decrypt the record.
+ yield* withSignedInUser(loggedInUser, function* () {
+ let error;
+ try {
+ yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
+ } catch (e) {
+ error = e;
+ }
+ ok(error, "decrypting the keyring with the old kB should fail");
+ ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error),
+ "decrypting the keyring with the old kB should throw an HMAC mismatch");
+ });
+ });
+});
+
+add_task(function* checkSyncKeyRing_overwrites_on_conflict() {
+ // If there is already a record on the server that was encrypted
+ // with a different kB, we wipe the server, clear sync state, and
+ // overwrite it with our keys.
+ const extensionId = uuid();
+ const transformer = new KeyRingEncryptionRemoteTransformer();
+ let extensionKey;
+ yield* withSyncContext(function* (context) {
+ yield* withServer(function* (server) {
+ // The old device has this kB, which is very similar to the
+ // current kB but with the last f changed to an e.
+ const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
+ const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+ server.installCollection("storage-sync-crypto");
+ server.installDeleteBucket();
+ server.etag = 765;
+ yield* withSignedInUser(oldUser, function* () {
+ const FAKE_KEYRING = {
+ id: "keys",
+ keys: {},
+ uuid: "abcd",
+ kbHash: "abcd",
+ };
+ yield server.encryptAndAddRecord(transformer, "storage-sync-crypto", FAKE_KEYRING);
+ });
+
+ // Now we have this new user with a different kB.
+ yield* withSignedInUser(loggedInUser, function* () {
+ yield cryptoCollection._clear();
+
+ // Do an `ensureKeysFor` to generate some keys.
+ // This will try to sync, notice that the record is
+ // undecryptable, and clear the server.
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(collectionKeys.hasKeysFor([extensionId]),
+ `ensureKeysFor should always return a keyring with a key for ${extensionId}`);
+ extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
+
+ deepEqual(server.getDeletedBuckets(), ["default"],
+ "Kinto server should have been wiped when keyring was thrown away");
+
+ let posts = server.getPosts();
+ equal(posts.length, 1,
+ "new keyring should have been uploaded");
+ const postedKeys = posts[0];
+ // The POST was to an empty server, so etag shouldn't be respected
+ equal(postedKeys.headers.Authorization, "Bearer some-access-token",
+ "keyring upload should be authorized");
+ equal(postedKeys.headers["If-None-Match"], "*",
+ "keyring upload should be to empty Kinto server");
+ equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
+ "keyring upload should be to keyring path");
+
+ let body = yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
+ ok(body.uuid, "new keyring should have a UUID");
+ equal(typeof body.uuid, "string", "keyring UUIDs should be strings");
+ notEqual(body.uuid, "abcd",
+ "new keyring should not have the same UUID as previous keyring");
+ ok(body.keys,
+ "new keyring should have a keys attribute");
+ ok(body.keys.default, "new keyring should have a default key");
+ // We should keep the extension key that was in our uploaded version.
+ deepEqual(extensionKey, body.keys.collections[extensionId],
+ "ensureKeysFor should have returned keyring with the same key that was uploaded");
+
+ // This should be a no-op; the keys were uploaded as part of ensurekeysfor
+ yield ExtensionStorageSync.checkSyncKeyRing();
+ equal(server.getPosts().length, 1,
+ "checkSyncKeyRing should not need to post keys after they were reuploaded");
+ });
+ });
+ });
+});
+
+add_task(function* checkSyncKeyRing_flushes_on_uuid_change() {
+ // If we can decrypt the record, but the UUID has changed, that
+ // means another client has wiped the server and reuploaded a
+ // keyring, so reset sync state and reupload everything.
+ const extensionId = uuid();
+ const extension = {id: extensionId};
+ const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
+ const transformer = new KeyRingEncryptionRemoteTransformer();
+ yield* withSyncContext(function* (context) {
+ yield* withServer(function* (server) {
+ server.installCollection("storage-sync-crypto");
+ server.installCollection(collectionId);
+ server.installDeleteBucket();
+ yield* withSignedInUser(loggedInUser, function* () {
+ yield cryptoCollection._clear();
+
+ // Do an `ensureKeysFor` to get access to keys.
+ let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ ok(collectionKeys.hasKeysFor([extensionId]),
+ `ensureKeysFor should always return a keyring that has a key for ${extensionId}`);
+ const extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
+
+ // Set something to make sure that it gets re-uploaded when
+ // uuid changes.
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield ExtensionStorageSync.syncAll();
+
+ let posts = server.getPosts();
+ equal(posts.length, 2,
+ "should have posted a new keyring and an extension datum");
+ const postedKeys = posts[0];
+ equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
+ "should have posted keyring to /keys");
+
+ let body = yield transformer.decode(postedKeys.body.data);
+ ok(body.uuid,
+ "keyring should have a UUID");
+ ok(body.keys,
+ "keyring should have a keys attribute");
+ ok(body.keys.default,
+ "keyring should have a default key");
+ deepEqual(extensionKey, body.keys.collections[extensionId],
+ "new keyring should have the same key that we uploaded");
+
+ // Another client comes along and replaces the UUID.
+ // In real life, this would mean changing the keys too, but
+ // this test verifies that just changing the UUID is enough.
+ const newKeyRingData = Object.assign({}, body, {
+ uuid: "abcd",
+ // Technically, last_modified should be served outside the
+ // object, but the transformer will pass it through in
+ // either direction, so this is OK.
+ last_modified: 765,
+ });
+ server.clearCollection("storage-sync-crypto");
+ server.etag = 765;
+ yield server.encryptAndAddRecordOnlyOnce(transformer, "storage-sync-crypto", newKeyRingData);
+
+ // Fake adding another extension just so that the keyring will
+ // really get synced.
+ const newExtension = uuid();
+ const newKeyRing = yield ExtensionStorageSync.ensureKeysFor([newExtension]);
+
+ // This should have detected the UUID change and flushed everything.
+ // The keyring should, however, be the same, since we just
+ // changed the UUID of the previously POSTed one.
+ deepEqual(newKeyRing.keyForCollection(extensionId).keyPairB64, extensionKey,
+ "ensureKeysFor should have pulled down a new keyring with the same keys");
+
+ // Syncing should reupload the data for the extension.
+ yield ExtensionStorageSync.syncAll();
+ posts = server.getPosts();
+ equal(posts.length, 4,
+ "should have posted keyring for new extension and reuploaded extension data");
+
+ const finalKeyRingPost = posts[2];
+ const reuploadedPost = posts[3];
+
+ equal(finalKeyRingPost.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
+ "keyring for new extension should have been posted to /keys");
+ let finalKeyRing = yield transformer.decode(finalKeyRingPost.body.data);
+ equal(finalKeyRing.uuid, "abcd",
+ "newly uploaded keyring should preserve UUID from replacement keyring");
+
+ // Confirm that the data got reuploaded
+ equal(reuploadedPost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "extension data should be posted to path corresponding to its key");
+ let reuploadedData = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(reuploadedPost.body.data);
+ equal(reuploadedData.key, "my-key",
+ "extension data should have a key attribute corresponding to the extension data key");
+ equal(reuploadedData.data, 5,
+ "extension data should have a data attribute corresponding to the extension data value");
+ });
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pulls_changes() {
+ const extensionId = defaultExtensionId;
+ const collectionId = defaultCollectionId;
+ const extension = defaultExtension;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+
+ let calls = [];
+ yield ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield ExtensionStorageSync.ensureKeysFor([extensionId]);
+ yield server.encryptAndAddRecord(transformer, collectionId, {
+ "id": "key-remote_2D_key",
+ "key": "remote-key",
+ "data": 6,
+ });
+
+ yield ExtensionStorageSync.syncAll();
+ const remoteValue = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+ equal(remoteValue, 6,
+ "ExtensionStorageSync.get() returns value retrieved from sync");
+
+ equal(calls.length, 1,
+ "syncing calls on-changed listener");
+ deepEqual(calls[0][0], {"remote-key": {newValue: 6}});
+ calls = [];
+
+ // Syncing again doesn't do anything
+ yield ExtensionStorageSync.syncAll();
+
+ equal(calls.length, 0,
+ "syncing again shouldn't call on-changed listener");
+
+ // Updating the server causes us to pull down the new value
+ server.etag = 1000;
+ server.clearCollection(collectionId);
+ yield server.encryptAndAddRecord(transformer, collectionId, {
+ "id": "key-remote_2D_key",
+ "key": "remote-key",
+ "data": 7,
+ });
+
+ yield ExtensionStorageSync.syncAll();
+ const remoteValue2 = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+ equal(remoteValue2, 7,
+ "ExtensionStorageSync.get() returns value updated from sync");
+
+ equal(calls.length, 1,
+ "syncing calls on-changed listener on update");
+ deepEqual(calls[0][0], {"remote-key": {oldValue: 6, newValue: 7}});
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pushes_changes() {
+ const extensionId = defaultExtensionId;
+ const collectionId = defaultCollectionId;
+ const extension = defaultExtension;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+ server.etag = 1000;
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+
+ // install this AFTER we set the key to 5...
+ let calls = [];
+ ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield ExtensionStorageSync.syncAll();
+ const localValue = (yield ExtensionStorageSync.get(extension, "my-key", context))["my-key"];
+ equal(localValue, 5,
+ "pushing an ExtensionStorageSync value shouldn't change local value");
+
+ let posts = server.getPosts();
+ equal(posts.length, 1,
+ "pushing a value should cause a post to the server");
+ const post = posts[0];
+ assertPostedNewRecord(post);
+ equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "pushing a value should have a path corresponding to its id");
+
+ const encrypted = post.body.data;
+ ok(encrypted.ciphertext,
+ "pushing a value should post an encrypted record");
+ ok(!encrypted.data,
+ "pushing a value should not have any plaintext data");
+ equal(encrypted.id, "key-my_2D_key",
+ "pushing a value should use a kinto-friendly record ID");
+
+ const record = yield transformer.decode(encrypted);
+ equal(record.key, "my-key",
+ "when decrypted, a pushed value should have a key field corresponding to its storage.sync key");
+ equal(record.data, 5,
+ "when decrypted, a pushed value should have a data field corresponding to its storage.sync value");
+ equal(record.id, "key-my_2D_key",
+ "when decrypted, a pushed value should have an id field corresponding to its record ID");
+
+ equal(calls.length, 0,
+ "pushing a value shouldn't call the on-changed listener");
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 6}, context);
+ yield ExtensionStorageSync.syncAll();
+
+ // Doesn't push keys because keys were pushed by a previous test.
+ posts = server.getPosts();
+ equal(posts.length, 2,
+ "updating a value should trigger another push");
+ const updatePost = posts[1];
+ assertPostedUpdatedRecord(updatePost, 1000);
+ equal(updatePost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "pushing an updated value should go to the same path");
+
+ const updateEncrypted = updatePost.body.data;
+ ok(updateEncrypted.ciphertext,
+ "pushing an updated value should still be encrypted");
+ ok(!updateEncrypted.data,
+ "pushing an updated value should not have any plaintext visible");
+ equal(updateEncrypted.id, "key-my_2D_key",
+ "pushing an updated value should maintain the same ID");
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pulls_deletes() {
+ const collectionId = defaultCollectionId;
+ const extension = defaultExtension;
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield ExtensionStorageSync.syncAll();
+ server.clearPosts();
+
+ let calls = [];
+ yield ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield server.addRecord(collectionId, {
+ "id": "key-my_2D_key",
+ "deleted": true,
+ });
+
+ yield ExtensionStorageSync.syncAll();
+ const remoteValues = (yield ExtensionStorageSync.get(extension, "my-key", context));
+ ok(!remoteValues["my-key"],
+ "ExtensionStorageSync.get() shows value was deleted by sync");
+
+ equal(server.getPosts().length, 0,
+ "pulling the delete shouldn't cause posts");
+
+ equal(calls.length, 1,
+ "syncing calls on-changed listener");
+ deepEqual(calls[0][0], {"my-key": {oldValue: 5}});
+ calls = [];
+
+ // Syncing again doesn't do anything
+ yield ExtensionStorageSync.syncAll();
+
+ equal(calls.length, 0,
+ "syncing again shouldn't call on-changed listener");
+ });
+ });
+});
+
+add_task(function* test_storage_sync_pushes_deletes() {
+ const extensionId = uuid();
+ const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
+ const extension = {id: extensionId};
+ yield cryptoCollection._clear();
+ yield* withContextAndServer(function* (context, server) {
+ yield* withSignedInUser(loggedInUser, function* () {
+ server.installCollection(collectionId);
+ server.installCollection("storage-sync-crypto");
+ server.etag = 1000;
+
+ yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+
+ let calls = [];
+ ExtensionStorageSync.addOnChangedListener(extension, function() {
+ calls.push(arguments);
+ }, context);
+
+ yield ExtensionStorageSync.syncAll();
+ let posts = server.getPosts();
+ equal(posts.length, 2,
+ "pushing a non-deleted value should post keys and post the value to the server");
+
+ yield ExtensionStorageSync.remove(extension, ["my-key"], context);
+ equal(calls.length, 1,
+ "deleting a value should call the on-changed listener");
+
+ yield ExtensionStorageSync.syncAll();
+ equal(calls.length, 1,
+ "pushing a deleted value shouldn't call the on-changed listener");
+
+ // Doesn't push keys because keys were pushed by a previous test.
+ posts = server.getPosts();
+ equal(posts.length, 3,
+ "deleting a value should trigger another push");
+ const post = posts[2];
+ assertPostedUpdatedRecord(post, 1000);
+ equal(post.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
+ "pushing a deleted value should go to the same path");
+ ok(post.method, "DELETE");
+ ok(!post.body,
+ "deleting a value shouldn't have a body");
+ });
+ });
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js b/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js
new file mode 100644
index 0000000000..eb3f552ed5
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_topSites.js
@@ -0,0 +1,85 @@
+"use strict";
+
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
+
+
+function TestProvider(getLinksFn) {
+ this.getLinks = getLinksFn;
+ this._observers = new Set();
+}
+
+TestProvider.prototype = {
+ addObserver: function(observer) {
+ this._observers.add(observer);
+ },
+ notifyLinkChanged: function(link, index = -1, deleted = false) {
+ this._notifyObservers("onLinkChanged", link, index, deleted);
+ },
+ notifyManyLinksChanged: function() {
+ this._notifyObservers("onManyLinksChanged");
+ },
+ _notifyObservers: function(observerMethodName, ...args) {
+ args.unshift(this);
+ for (let obs of this._observers) {
+ if (obs[observerMethodName]) {
+ obs[observerMethodName].apply(NewTabUtils.links, args);
+ }
+ }
+ },
+};
+
+function makeLinks(links) {
+ // Important: To avoid test failures due to clock jitter on Windows XP, call
+ // Date.now() once here, not each time through the loop.
+ let frecency = 0;
+ let now = Date.now() * 1000;
+ let places = [];
+ links.map((link, i) => {
+ places.push({
+ url: link.url,
+ title: link.title,
+ lastVisitDate: now - i,
+ frecency: frecency++,
+ });
+ });
+ return places;
+}
+
+add_task(function* test_topSites() {
+ let expect = [{url: "http://example.com/", title: "site#-1"},
+ {url: "http://example0.com/", title: "site#0"},
+ {url: "http://example1.com/", title: "site#1"},
+ {url: "http://example2.com/", title: "site#2"},
+ {url: "http://example3.com/", title: "site#3"}];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": [
+ "topSites",
+ ],
+ },
+ background() {
+ browser.topSites.get(result => {
+ browser.test.sendMessage("done", result);
+ });
+ },
+ });
+
+
+ let expectedLinks = makeLinks(expect);
+ let provider = new TestProvider(done => done(expectedLinks));
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+
+ yield NewTabUtils.links.populateCache();
+
+ yield extension.startup();
+
+ let result = yield extension.awaitMessage("done");
+ Assert.deepEqual(expect, result, "got topSites");
+
+ yield extension.unload();
+
+ NewTabUtils.links.removeProvider(provider);
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js b/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js
new file mode 100644
index 0000000000..68741a6cca
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_getAPILevelForWindow.js
@@ -0,0 +1,55 @@
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function createWindowWithAddonId(addonId) {
+ let baseURI = Services.io.newURI("about:blank", null, null);
+ let originAttributes = {addonId};
+ let principal = Services.scriptSecurityManager
+ .createCodebasePrincipal(baseURI, originAttributes);
+ let chromeNav = Services.appShell.createWindowlessBrowser(true);
+ let interfaceRequestor = chromeNav.QueryInterface(Ci.nsIInterfaceRequestor);
+ let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+ docShell.createAboutBlankContentViewer(principal);
+
+ return {chromeNav, window: docShell.contentViewer.DOMDocument.defaultView};
+}
+
+add_task(function* test_eventpages() {
+ const {getAPILevelForWindow, getAddonIdForWindow} = ExtensionManagement;
+ const {NO_PRIVILEGES, FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
+ const FAKE_ADDON_ID = "fakeAddonId";
+ const OTHER_ADDON_ID = "otherFakeAddonId";
+ const EMPTY_ADDON_ID = "";
+
+ let fakeAddonId = createWindowWithAddonId(FAKE_ADDON_ID);
+ equal(getAddonIdForWindow(fakeAddonId.window), FAKE_ADDON_ID,
+ "the window has the expected addonId");
+
+ let apiLevel = getAPILevelForWindow(fakeAddonId.window, FAKE_ADDON_ID);
+ equal(apiLevel, FULL_PRIVILEGES,
+ "apiLevel for the window with the right addonId should be FULL_PRIVILEGES");
+
+ apiLevel = getAPILevelForWindow(fakeAddonId.window, OTHER_ADDON_ID);
+ equal(apiLevel, NO_PRIVILEGES,
+ "apiLevel for the window with a different addonId should be NO_PRIVILEGES");
+
+ fakeAddonId.chromeNav.close();
+
+ // NOTE: check that window with an empty addon Id (which are window that are
+ // not Extensions pages) always get no WebExtensions APIs.
+ let emptyAddonId = createWindowWithAddonId(EMPTY_ADDON_ID);
+ equal(getAddonIdForWindow(emptyAddonId.window), EMPTY_ADDON_ID,
+ "the window has the expected addonId");
+
+ apiLevel = getAPILevelForWindow(emptyAddonId.window, EMPTY_ADDON_ID);
+ equal(apiLevel, NO_PRIVILEGES,
+ "apiLevel for empty addonId should be NO_PRIVILEGES");
+
+ apiLevel = getAPILevelForWindow(emptyAddonId.window, OTHER_ADDON_ID);
+ equal(apiLevel, NO_PRIVILEGES,
+ "apiLevel for an 'empty addonId' window should be always NO_PRIVILEGES");
+
+ emptyAddonId.chromeNav.close();
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_locale_converter.js b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js
new file mode 100644
index 0000000000..c8b1ee92ba
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const convService = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+
+const UUID = "72b61ee3-aceb-476c-be1b-0822b036c9f1";
+const ADDON_ID = "test@web.extension";
+const URI = NetUtil.newURI(`moz-extension://${UUID}/file.css`);
+
+const FROM_TYPE = "application/vnd.mozilla.webext.unlocalized";
+const TO_TYPE = "text/css";
+
+
+function StringStream(string) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+
+ stream.data = string;
+ return stream;
+}
+
+
+// Initialize the policy service with a stub localizer for our
+// add-on ID.
+add_task(function* init() {
+ const aps = Cc["@mozilla.org/addons/policy-service;1"]
+ .getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+
+ let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => {
+ if (uri.host == UUID) {
+ return ADDON_ID;
+ }
+ });
+
+ aps.setAddonLocalizeCallback(ADDON_ID, string => {
+ return string.replace(/__MSG_(.*?)__/g, "<localized-$1>");
+ });
+
+ do_register_cleanup(() => {
+ aps.setExtensionURIToAddonIdCallback(oldCallback);
+ aps.setAddonLocalizeCallback(ADDON_ID, null);
+ });
+});
+
+
+// Test that the synchronous converter works as expected with a
+// simple string.
+add_task(function* testSynchronousConvert() {
+ let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
+
+ let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI);
+
+ let result = NetUtil.readInputStreamToString(resultStream, resultStream.available());
+
+ equal(result, "Foo <localized-xxx> bar <localized-yyy> baz");
+});
+
+
+// Test that the asynchronous converter works as expected with input
+// split into multiple chunks, and a boundary in the middle of a
+// replacement token.
+add_task(function* testAsyncConvert() {
+ let listener;
+ let awaitResult = new Promise((resolve, reject) => {
+ listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener]),
+
+ onDataAvailable(request, context, inputStream, offset, count) {
+ this.resultParts.push(NetUtil.readInputStreamToString(inputStream, count));
+ },
+
+ onStartRequest() {
+ ok(!("resultParts" in this));
+ this.resultParts = [];
+ },
+
+ onStopRequest(request, context, statusCode) {
+ if (!Components.isSuccessCode(statusCode)) {
+ reject(new Error(statusCode));
+ }
+
+ resolve(this.resultParts.join("\n"));
+ },
+ };
+ });
+
+ let parts = ["Foo __MSG_x", "xx__ bar __MSG_yyy__ baz"];
+
+ let converter = convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, URI);
+ converter.onStartRequest(null, null);
+
+ for (let part of parts) {
+ converter.onDataAvailable(null, null, StringStream(part), 0, part.length);
+ }
+
+ converter.onStopRequest(null, null, Cr.NS_OK);
+
+
+ let result = yield awaitResult;
+ equal(result, "Foo <localized-xxx> bar <localized-yyy> baz");
+});
+
+
+// Test that attempting to initialize a converter with the URI of a
+// nonexistent WebExtension fails.
+add_task(function* testInvalidUUID() {
+ let uri = NetUtil.newURI("moz-extension://eb4f3be8-41c9-4970-aa6d-b84d1ecc02b2/file.css");
+ let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
+
+ // Assert.throws raise a TypeError exception when the expected param
+ // is an arrow function. (See Bug 1237961 for rationale)
+ let expectInvalidContextException = function(e) {
+ return e.result === Cr.NS_ERROR_INVALID_ARG && /Invalid context/.test(e);
+ };
+
+ Assert.throws(() => {
+ convService.convert(stream, FROM_TYPE, TO_TYPE, uri);
+ }, expectInvalidContextException);
+
+ Assert.throws(() => {
+ let listener = {QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener])};
+
+ convService.asyncConvertData(FROM_TYPE, TO_TYPE, listener, uri);
+ }, expectInvalidContextException);
+});
+
+
+// Test that an empty stream does not throw an NS_ERROR_ILLEGAL_VALUE.
+add_task(function* testEmptyStream() {
+ let stream = StringStream("");
+ let resultStream = convService.convert(stream, FROM_TYPE, TO_TYPE, URI);
+ equal(resultStream.data, "");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_locale_data.js b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
new file mode 100644
index 0000000000..c3cd44e577
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
@@ -0,0 +1,130 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Extension.jsm");
+
+/* globals ExtensionData */
+
+const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+function* generateAddon(data) {
+ let id = uuidGenerator.generateUUID().number;
+
+ data = Object.assign({embedded: true}, data);
+ data.manifest = Object.assign({applications: {gecko: {id}}}, data.manifest);
+
+ let xpi = Extension.generateXPI(data);
+ do_register_cleanup(() => {
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+ });
+
+ let fileURI = Services.io.newFileURI(xpi);
+ let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/webextension/`);
+
+ let extension = new ExtensionData(jarURI);
+ yield extension.readManifest();
+
+ return extension;
+}
+
+add_task(function* testMissingDefaultLocale() {
+ let extension = yield generateAddon({
+ "files": {
+ "_locales/en_US/messages.json": {},
+ },
+ });
+
+ equal(extension.errors.length, 0, "No errors reported");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 1, "One error reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes('"default_locale" property is required'),
+ "Got missing default_locale error");
+});
+
+
+add_task(function* testInvalidDefaultLocale() {
+ let extension = yield generateAddon({
+ "manifest": {
+ "default_locale": "en",
+ },
+
+ "files": {
+ "_locales/en_US/messages.json": {},
+ },
+ });
+
+ equal(extension.errors.length, 1, "One error reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes("Loading locale file _locales/en/messages.json"),
+ "Got invalid default_locale error");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 2, "Two errors reported");
+
+ do_print(`Got error: ${extension.errors[1]}`);
+
+ ok(extension.errors[1].includes('"default_locale" property must correspond'),
+ "Got invalid default_locale error");
+});
+
+
+add_task(function* testUnexpectedDefaultLocale() {
+ let extension = yield generateAddon({
+ "manifest": {
+ "default_locale": "en_US",
+ },
+ });
+
+ equal(extension.errors.length, 1, "One error reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes("Loading locale file _locales/en-US/messages.json"),
+ "Got invalid default_locale error");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 2, "One error reported");
+
+ do_print(`Got error: ${extension.errors[1]}`);
+
+ ok(extension.errors[1].includes('"default_locale" property must correspond'),
+ "Got unexpected default_locale error");
+});
+
+
+add_task(function* testInvalidSyntax() {
+ let extension = yield generateAddon({
+ "manifest": {
+ "default_locale": "en_US",
+ },
+
+ "files": {
+ "_locales/en_US/messages.json": '{foo: {message: "bar", description: "baz"}}',
+ },
+ });
+
+ equal(extension.errors.length, 1, "No errors reported");
+
+ do_print(`Got error: ${extension.errors[0]}`);
+
+ ok(extension.errors[0].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
+ "Got syntax error");
+
+ yield extension.initAllLocales();
+
+ equal(extension.errors.length, 2, "One error reported");
+
+ do_print(`Got error: ${extension.errors[1]}`);
+
+ ok(extension.errors[1].includes("Loading locale file _locales\/en_US\/messages\.json: SyntaxError"),
+ "Got syntax error");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
new file mode 100644
index 0000000000..1fcb7799ed
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -0,0 +1,302 @@
+"use strict";
+
+/* global OS, HostManifestManager, NativeApp */
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Schemas.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
+Cu.import("resource://gre/modules/NativeMessaging.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+let registry = null;
+if (AppConstants.platform == "win") {
+ Cu.import("resource://testing-common/MockRegistry.jsm");
+ registry = new MockRegistry();
+ do_register_cleanup(() => {
+ registry.shutdown();
+ });
+}
+
+const REGPATH = "Software\\Mozilla\\NativeMessagingHosts";
+
+const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
+
+let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
+dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+let userDir = dir.clone();
+userDir.append("user");
+userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+let globalDir = dir.clone();
+globalDir.append("global");
+globalDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+let dirProvider = {
+ getFile(property) {
+ if (property == "XREUserNativeMessaging") {
+ return userDir.clone();
+ } else if (property == "XRESysNativeMessaging") {
+ return globalDir.clone();
+ }
+ return null;
+ },
+};
+
+Services.dirsvc.registerProvider(dirProvider);
+
+do_register_cleanup(() => {
+ Services.dirsvc.unregisterProvider(dirProvider);
+ dir.remove(true);
+});
+
+function writeManifest(path, manifest) {
+ if (typeof manifest != "string") {
+ manifest = JSON.stringify(manifest);
+ }
+ return OS.File.writeAtomic(path, manifest);
+}
+
+let PYTHON;
+add_task(function* setup() {
+ yield Schemas.load(BASE_SCHEMA);
+
+ PYTHON = yield Subprocess.pathSearch("python2.7");
+ if (PYTHON == null) {
+ PYTHON = yield Subprocess.pathSearch("python");
+ }
+ notEqual(PYTHON, null, "Found a suitable python interpreter");
+});
+
+let global = this;
+
+// Test of HostManifestManager.lookupApplication() begin here...
+let context = {
+ url: null,
+ jsonStringify(...args) { return JSON.stringify(...args); },
+ cloneScope: global,
+ logError() {},
+ preprocessors: {},
+ callOnClose: () => {},
+ forgetOnClose: () => {},
+};
+
+class MockContext extends ExtensionCommon.BaseContext {
+ constructor(extensionId) {
+ let fakeExtension = {id: extensionId};
+ super("testEnv", fakeExtension);
+ this.sandbox = Cu.Sandbox(global);
+ }
+
+ get cloneScope() {
+ return global;
+ }
+
+ get principal() {
+ return Cu.getObjectPrincipal(this.sandbox);
+ }
+}
+
+let templateManifest = {
+ name: "test",
+ description: "this is only a test",
+ path: "/bin/cat",
+ type: "stdio",
+ allowed_extensions: ["extension@tests.mozilla.org"],
+};
+
+add_task(function* test_nonexistent_manifest() {
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication returns null for non-existent application");
+});
+
+const USER_TEST_JSON = OS.Path.join(userDir.path, "test.json");
+
+add_task(function* test_good_manifest() {
+ yield writeManifest(USER_TEST_JSON, templateManifest);
+ if (registry) {
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\test`, "", USER_TEST_JSON);
+ }
+
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ notEqual(result, null, "lookupApplication finds a good manifest");
+ equal(result.path, USER_TEST_JSON, "lookupApplication returns the correct path");
+ deepEqual(result.manifest, templateManifest, "lookupApplication returns the manifest contents");
+});
+
+add_task(function* test_invalid_json() {
+ yield writeManifest(USER_TEST_JSON, "this is not valid json");
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores bad json");
+});
+
+add_task(function* test_invalid_name() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.name = "../test";
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores an invalid name");
+});
+
+add_task(function* test_name_mismatch() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.name = "not test";
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ let what = (AppConstants.platform == "win") ? "registry key" : "json filename";
+ equal(result, null, `lookupApplication ignores mistmatch between ${what} and name property`);
+});
+
+add_task(function* test_missing_props() {
+ const PROPS = [
+ "name",
+ "description",
+ "path",
+ "type",
+ "allowed_extensions",
+ ];
+ for (let prop of PROPS) {
+ let manifest = Object.assign({}, templateManifest);
+ delete manifest[prop];
+
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, `lookupApplication ignores missing ${prop}`);
+ }
+});
+
+add_task(function* test_invalid_type() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.type = "bogus";
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores invalid type");
+});
+
+add_task(function* test_no_allowed_extensions() {
+ let manifest = Object.assign({}, templateManifest);
+ manifest.allowed_extensions = [];
+ yield writeManifest(USER_TEST_JSON, manifest);
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ equal(result, null, "lookupApplication ignores manifest with no allowed_extensions");
+});
+
+const GLOBAL_TEST_JSON = OS.Path.join(globalDir.path, "test.json");
+let globalManifest = Object.assign({}, templateManifest);
+globalManifest.description = "This manifest is from the systemwide directory";
+
+add_task(function* good_manifest_system_dir() {
+ yield OS.File.remove(USER_TEST_JSON);
+ yield writeManifest(GLOBAL_TEST_JSON, globalManifest);
+ if (registry) {
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\test`, "", null);
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ `${REGPATH}\\test`, "", GLOBAL_TEST_JSON);
+ }
+
+ let where = (AppConstants.platform == "win") ? "registry location" : "directory";
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ notEqual(result, null, `lookupApplication finds a manifest in the system-wide ${where}`);
+ equal(result.path, GLOBAL_TEST_JSON, `lookupApplication returns path in the system-wide ${where}`);
+ deepEqual(result.manifest, globalManifest, `lookupApplication returns manifest contents from the system-wide ${where}`);
+});
+
+add_task(function* test_user_dir_precedence() {
+ yield writeManifest(USER_TEST_JSON, templateManifest);
+ if (registry) {
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\test`, "", USER_TEST_JSON);
+ }
+ // global test.json and LOCAL_MACHINE registry key on windows are
+ // still present from the previous test
+
+ let result = yield HostManifestManager.lookupApplication("test", context);
+ notEqual(result, null, "lookupApplication finds a manifest when entries exist in both user-specific and system-wide locations");
+ equal(result.path, USER_TEST_JSON, "lookupApplication returns the user-specific path when user-specific and system-wide entries both exist");
+ deepEqual(result.manifest, templateManifest, "lookupApplication returns user-specific manifest contents with user-specific and system-wide entries both exist");
+});
+
+// Test shutdown handling in NativeApp
+add_task(function* test_native_app_shutdown() {
+ const SCRIPT = String.raw`
+import signal
+import struct
+import sys
+
+signal.signal(signal.SIGTERM, signal.SIG_IGN)
+
+while True:
+ rawlen = sys.stdin.read(4)
+ if len(rawlen) == 0:
+ signal.pause()
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = sys.stdin.read(msglen)
+
+ sys.stdout.write(struct.pack('@I', msglen))
+ sys.stdout.write(msg)
+ `;
+
+ let scriptPath = OS.Path.join(userDir.path, "wontdie.py");
+ let manifestPath = OS.Path.join(userDir.path, "wontdie.json");
+
+ const ID = "native@tests.mozilla.org";
+ let manifest = {
+ name: "wontdie",
+ description: "test async shutdown of native apps",
+ type: "stdio",
+ allowed_extensions: [ID],
+ };
+
+ if (AppConstants.platform == "win") {
+ yield OS.File.writeAtomic(scriptPath, SCRIPT);
+
+ let batPath = OS.Path.join(userDir.path, "wontdie.bat");
+ let batBody = `@ECHO OFF\n${PYTHON} -u "${scriptPath}" %*\n`;
+ yield OS.File.writeAtomic(batPath, batBody);
+ yield OS.File.setPermissions(batPath, {unixMode: 0o755});
+
+ manifest.path = batPath;
+ yield writeManifest(manifestPath, manifest);
+
+ registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGPATH}\\wontdie`, "", manifestPath);
+ } else {
+ yield OS.File.writeAtomic(scriptPath, `#!${PYTHON} -u\n${SCRIPT}`);
+ yield OS.File.setPermissions(scriptPath, {unixMode: 0o755});
+ manifest.path = scriptPath;
+ yield writeManifest(manifestPath, manifest);
+ }
+
+ let mockContext = new MockContext(ID);
+ let app = new NativeApp(mockContext, "wontdie");
+
+ // send a message and wait for the reply to make sure the app is running
+ let MSG = "test";
+ let recvPromise = new Promise(resolve => {
+ let listener = (what, msg) => {
+ equal(msg, MSG, "Received test message");
+ app.off("message", listener);
+ resolve();
+ };
+ app.on("message", listener);
+ });
+
+ let buffer = NativeApp.encodeMessage(mockContext, MSG);
+ app.send(buffer);
+ yield recvPromise;
+
+ app._cleanup();
+
+ do_print("waiting for async shutdown");
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ AsyncShutdown.profileBeforeChange._trigger();
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+
+ let procs = yield SubprocessImpl.Process.getWorker().call("getProcesses", []);
+ equal(procs.size, 0, "native process exited");
+});
diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell.ini b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..3d0198ee93
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = appname == "thunderbird"
+support-files =
+ data/** head_sync.js
+tags = webextensions
+
+[test_csp_custom_policies.js]
+[test_csp_validator.js]
+[test_ext_alarms.js]
+[test_ext_alarms_does_not_fire.js]
+[test_ext_alarms_periodic.js]
+[test_ext_alarms_replaces.js]
+[test_ext_apimanager.js]
+[test_ext_api_permissions.js]
+[test_ext_background_generated_load_events.js]
+[test_ext_background_generated_reload.js]
+[test_ext_background_global_history.js]
+skip-if = os == "android" # Android does not use Places for history.
+[test_ext_background_private_browsing.js]
+[test_ext_background_runtime_connect_params.js]
+[test_ext_background_sub_windows.js]
+[test_ext_background_window_properties.js]
+skip-if = os == "android"
+[test_ext_contexts.js]
+[test_ext_downloads.js]
+[test_ext_downloads_download.js]
+skip-if = os == "android"
+[test_ext_downloads_misc.js]
+skip-if = os == "android"
+[test_ext_downloads_search.js]
+skip-if = os == "android"
+[test_ext_experiments.js]
+skip-if = release_or_beta
+[test_ext_extension.js]
+[test_ext_idle.js]
+[test_ext_json_parser.js]
+[test_ext_localStorage.js]
+[test_ext_management.js]
+[test_ext_management_uninstall_self.js]
+[test_ext_manifest_content_security_policy.js]
+[test_ext_manifest_incognito.js]
+[test_ext_manifest_minimum_chrome_version.js]
+[test_ext_onmessage_removelistener.js]
+[test_ext_runtime_connect_no_receiver.js]
+[test_ext_runtime_getBrowserInfo.js]
+[test_ext_runtime_getPlatformInfo.js]
+[test_ext_runtime_onInstalled_and_onStartup.js]
+[test_ext_runtime_sendMessage.js]
+[test_ext_runtime_sendMessage_errors.js]
+[test_ext_runtime_sendMessage_no_receiver.js]
+[test_ext_runtime_sendMessage_self.js]
+[test_ext_schemas.js]
+[test_ext_schemas_api_injection.js]
+[test_ext_schemas_async.js]
+[test_ext_schemas_allowed_contexts.js]
+[test_ext_simple.js]
+[test_ext_storage.js]
+[test_ext_storage_sync.js]
+head = head.js head_sync.js
+skip-if = os == "android"
+[test_ext_topSites.js]
+skip-if = os == "android"
+[test_getAPILevelForWindow.js]
+[test_ext_legacy_extension_context.js]
+[test_ext_legacy_extension_embedding.js]
+[test_locale_converter.js]
+[test_locale_data.js]
+[test_native_messaging.js]
+skip-if = os == "android"
diff --git a/toolkit/components/exthelper/extApplication.js b/toolkit/components/exthelper/extApplication.js
new file mode 100644
index 0000000000..a56a04c0ee
--- /dev/null
+++ b/toolkit/components/exthelper/extApplication.js
@@ -0,0 +1,719 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+// =================================================
+// Console constructor
+function Console() {
+ this._console = Components.classes["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+}
+
+// =================================================
+// Console implementation
+Console.prototype = {
+ log: function cs_log(aMsg) {
+ this._console.logStringMessage(aMsg);
+ },
+
+ open: function cs_open() {
+ var wMediator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Ci.nsIWindowMediator);
+ var console = wMediator.getMostRecentWindow("global:console");
+ if (!console) {
+ var wWatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Ci.nsIWindowWatcher);
+ wWatch.openWindow(null, "chrome://global/content/console.xul", "_blank",
+ "chrome,dialog=no,all", null);
+ } else {
+ // console was already open
+ console.focus();
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIConsole])
+};
+
+
+// =================================================
+// EventItem constructor
+function EventItem(aType, aData) {
+ this._type = aType;
+ this._data = aData;
+}
+
+// =================================================
+// EventItem implementation
+EventItem.prototype = {
+ _cancel: false,
+
+ get type() {
+ return this._type;
+ },
+
+ get data() {
+ return this._data;
+ },
+
+ preventDefault: function ei_pd() {
+ this._cancel = true;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIEventItem])
+};
+
+
+// =================================================
+// Events constructor
+function Events(notifier) {
+ this._listeners = [];
+ this._notifier = notifier;
+}
+
+// =================================================
+// Events implementation
+Events.prototype = {
+ addListener: function evts_al(aEvent, aListener) {
+ function hasFilter(element) {
+ return element.event == aEvent && element.listener == aListener;
+ }
+
+ if (this._listeners.some(hasFilter))
+ return;
+
+ this._listeners.push({
+ event: aEvent,
+ listener: aListener
+ });
+
+ if (this._notifier) {
+ this._notifier(aEvent, aListener);
+ }
+ },
+
+ removeListener: function evts_rl(aEvent, aListener) {
+ function hasFilter(element) {
+ return (element.event != aEvent) || (element.listener != aListener);
+ }
+
+ this._listeners = this._listeners.filter(hasFilter);
+ },
+
+ dispatch: function evts_dispatch(aEvent, aEventItem) {
+ var eventItem = new EventItem(aEvent, aEventItem);
+
+ this._listeners.forEach(function(key) {
+ if (key.event == aEvent) {
+ key.listener.handleEvent ?
+ key.listener.handleEvent(eventItem) :
+ key.listener(eventItem);
+ }
+ });
+
+ return !eventItem._cancel;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents])
+};
+
+// =================================================
+// PreferenceObserver (internal class)
+//
+// PreferenceObserver is a global singleton which watches the browser's
+// preferences and sends you events when things change.
+
+function PreferenceObserver() {
+ this._observersDict = {};
+}
+
+PreferenceObserver.prototype = {
+ /**
+ * Add a preference observer.
+ *
+ * @param aPrefs the nsIPrefBranch onto which we'll install our listener.
+ * @param aDomain the domain our listener will watch (a string).
+ * @param aEvent the event to listen to (you probably want "change").
+ * @param aListener the function to call back when the event fires. This
+ * function will receive an EventData argument.
+ */
+ addListener: function po_al(aPrefs, aDomain, aEvent, aListener) {
+ var root = aPrefs.root;
+ if (!this._observersDict[root]) {
+ this._observersDict[root] = {};
+ }
+ var observer = this._observersDict[root][aDomain];
+
+ if (!observer) {
+ observer = {
+ events: new Events(),
+ observe: function po_observer_obs(aSubject, aTopic, aData) {
+ this.events.dispatch("change", aData);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference])
+ };
+ observer.prefBranch = aPrefs;
+ observer.prefBranch.addObserver(aDomain, observer, /* ownsWeak = */ true);
+
+ // Notice that the prefBranch keeps a weak reference to the observer;
+ // it's this._observersDict which keeps the observer alive.
+ this._observersDict[root][aDomain] = observer;
+ }
+ observer.events.addListener(aEvent, aListener);
+ },
+
+ /**
+ * Remove a preference observer.
+ *
+ * This function's parameters are identical to addListener's.
+ */
+ removeListener: function po_rl(aPrefs, aDomain, aEvent, aListener) {
+ var root = aPrefs.root;
+ if (!this._observersDict[root] ||
+ !this._observersDict[root][aDomain]) {
+ return;
+ }
+ var observer = this._observersDict[root][aDomain];
+ observer.events.removeListener(aEvent, aListener);
+
+ if (observer.events._listeners.length == 0) {
+ // nsIPrefBranch objects are not singletons -- we can have two
+ // nsIPrefBranch'es for the same branch. There's no guarantee that
+ // aPrefs is the same object as observer.prefBranch, so we have to call
+ // removeObserver on observer.prefBranch.
+ observer.prefBranch.removeObserver(aDomain, observer);
+ delete this._observersDict[root][aDomain];
+ if (Object.keys(this._observersDict[root]).length == 0) {
+ delete this._observersDict[root];
+ }
+ }
+ }
+};
+
+// =================================================
+// PreferenceBranch constructor
+function PreferenceBranch(aBranch) {
+ if (!aBranch)
+ aBranch = "";
+
+ this._root = aBranch;
+ this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService)
+ .QueryInterface(Ci.nsIPrefBranch);
+
+ if (aBranch)
+ this._prefs = this._prefs.getBranch(aBranch);
+
+ let prefs = this._prefs;
+ this._events = {
+ addListener: function pb_al(aEvent, aListener) {
+ gPreferenceObserver.addListener(prefs, "", aEvent, aListener);
+ },
+ removeListener: function pb_rl(aEvent, aListener) {
+ gPreferenceObserver.removeListener(prefs, "", aEvent, aListener);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents])
+ };
+}
+
+// =================================================
+// PreferenceBranch implementation
+PreferenceBranch.prototype = {
+ get root() {
+ return this._root;
+ },
+
+ get all() {
+ return this.find({});
+ },
+
+ get events() {
+ return this._events;
+ },
+
+ // XXX: Disabled until we can figure out the wrapped object issues
+ // name: "name" or /name/
+ // path: "foo.bar." or "" or /fo+\.bar/
+ // type: Boolean, Number, String (getPrefType)
+ // locked: true, false (prefIsLocked)
+ // modified: true, false (prefHasUserValue)
+ find: function prefs_find(aOptions) {
+ var retVal = [];
+ var items = this._prefs.getChildList("");
+
+ for (var i = 0; i < items.length; i++) {
+ retVal.push(new Preference(items[i], this));
+ }
+
+ return retVal;
+ },
+
+ has: function prefs_has(aName) {
+ return (this._prefs.getPrefType(aName) != Ci.nsIPrefBranch.PREF_INVALID);
+ },
+
+ get: function prefs_get(aName) {
+ return this.has(aName) ? new Preference(aName, this) : null;
+ },
+
+ getValue: function prefs_gv(aName, aValue) {
+ var type = this._prefs.getPrefType(aName);
+
+ switch (type) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ aValue = this._prefs.getComplexValue(aName, Ci.nsISupportsString).data;
+ break;
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ aValue = this._prefs.getBoolPref(aName);
+ break;
+ case Ci.nsIPrefBranch.PREF_INT:
+ aValue = this._prefs.getIntPref(aName);
+ break;
+ }
+
+ return aValue;
+ },
+
+ setValue: function prefs_sv(aName, aValue) {
+ var type = aValue != null ? aValue.constructor.name : "";
+
+ switch (type) {
+ case "String":
+ var str = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ str.data = aValue;
+ this._prefs.setComplexValue(aName, Ci.nsISupportsString, str);
+ break;
+ case "Boolean":
+ this._prefs.setBoolPref(aName, aValue);
+ break;
+ case "Number":
+ this._prefs.setIntPref(aName, aValue);
+ break;
+ default:
+ throw ("Unknown preference value specified.");
+ }
+ },
+
+ reset: function prefs_reset() {
+ this._prefs.resetBranch("");
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIPreferenceBranch])
+};
+
+
+// =================================================
+// Preference constructor
+function Preference(aName, aBranch) {
+ this._name = aName;
+ this._branch = aBranch;
+
+ var self = this;
+ this._events = {
+ addListener: function pref_al(aEvent, aListener) {
+ gPreferenceObserver.addListener(self._branch._prefs, self._name, aEvent, aListener);
+ },
+ removeListener: function pref_rl(aEvent, aListener) {
+ gPreferenceObserver.removeListener(self._branch._prefs, self._name, aEvent, aListener);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents])
+ };
+}
+
+// =================================================
+// Preference implementation
+Preference.prototype = {
+ get name() {
+ return this._name;
+ },
+
+ get type() {
+ var value = "";
+ var type = this.branch._prefs.getPrefType(this._name);
+
+ switch (type) {
+ case Ci.nsIPrefBranch.PREF_STRING:
+ value = "String";
+ break;
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ value = "Boolean";
+ break;
+ case Ci.nsIPrefBranch.PREF_INT:
+ value = "Number";
+ break;
+ }
+
+ return value;
+ },
+
+ get value() {
+ return this.branch.getValue(this._name, null);
+ },
+
+ set value(aValue) {
+ return this.branch.setValue(this._name, aValue);
+ },
+
+ get locked() {
+ return this.branch._prefs.prefIsLocked(this.name);
+ },
+
+ set locked(aValue) {
+ this.branch._prefs[aValue ? "lockPref" : "unlockPref"](this.name);
+ },
+
+ get modified() {
+ return this.branch._prefs.prefHasUserValue(this.name);
+ },
+
+ get branch() {
+ return this._branch;
+ },
+
+ get events() {
+ return this._events;
+ },
+
+ reset: function pref_reset() {
+ this.branch._prefs.clearUserPref(this.name);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIPreference])
+};
+
+
+// =================================================
+// SessionStorage constructor
+function SessionStorage() {
+ this._storage = {};
+ this._events = new Events();
+}
+
+// =================================================
+// SessionStorage implementation
+SessionStorage.prototype = {
+ get events() {
+ return this._events;
+ },
+
+ has: function ss_has(aName) {
+ return this._storage.hasOwnProperty(aName);
+ },
+
+ set: function ss_set(aName, aValue) {
+ this._storage[aName] = aValue;
+ this._events.dispatch("change", aName);
+ },
+
+ get: function ss_get(aName, aDefaultValue) {
+ return this.has(aName) ? this._storage[aName] : aDefaultValue;
+ },
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.extISessionStorage])
+};
+
+// =================================================
+// ExtensionObserver constructor (internal class)
+//
+// ExtensionObserver is a global singleton which watches the browser's
+// extensions and sends you events when things change.
+
+function ExtensionObserver() {
+ this._eventsDict = {};
+
+ AddonManager.addAddonListener(this);
+ AddonManager.addInstallListener(this);
+}
+
+// =================================================
+// ExtensionObserver implementation (internal class)
+ExtensionObserver.prototype = {
+ onDisabling: function eo_onDisabling(addon, needsRestart) {
+ this._dispatchEvent(addon.id, "disable");
+ },
+
+ onEnabling: function eo_onEnabling(addon, needsRestart) {
+ this._dispatchEvent(addon.id, "enable");
+ },
+
+ onUninstalling: function eo_onUninstalling(addon, needsRestart) {
+ this._dispatchEvent(addon.id, "uninstall");
+ },
+
+ onOperationCancelled: function eo_onOperationCancelled(addon) {
+ this._dispatchEvent(addon.id, "cancel");
+ },
+
+ onInstallEnded: function eo_onInstallEnded(install, addon) {
+ this._dispatchEvent(addon.id, "upgrade");
+ },
+
+ addListener: function eo_al(aId, aEvent, aListener) {
+ var events = this._eventsDict[aId];
+ if (!events) {
+ events = new Events();
+ this._eventsDict[aId] = events;
+ }
+ events.addListener(aEvent, aListener);
+ },
+
+ removeListener: function eo_rl(aId, aEvent, aListener) {
+ var events = this._eventsDict[aId];
+ if (!events) {
+ return;
+ }
+ events.removeListener(aEvent, aListener);
+ if (events._listeners.length == 0) {
+ delete this._eventsDict[aId];
+ }
+ },
+
+ _dispatchEvent: function eo_dispatchEvent(aId, aEvent) {
+ var events = this._eventsDict[aId];
+ if (events) {
+ events.dispatch(aEvent, aId);
+ }
+ }
+};
+
+// =================================================
+// Extension constructor
+function Extension(aItem) {
+ this._item = aItem;
+ this._firstRun = false;
+ this._prefs = new PreferenceBranch("extensions." + this.id + ".");
+ this._storage = new SessionStorage();
+
+ let id = this.id;
+ this._events = {
+ addListener: function ext_events_al(aEvent, aListener) {
+ gExtensionObserver.addListener(id, aEvent, aListener);
+ },
+ removeListener: function ext_events_rl(aEvent, aListener) {
+ gExtensionObserver.addListener(id, aEvent, aListener);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents])
+ };
+
+ var installPref = "install-event-fired";
+ if (!this._prefs.has(installPref)) {
+ this._prefs.setValue(installPref, true);
+ this._firstRun = true;
+ }
+}
+
+// =================================================
+// Extension implementation
+Extension.prototype = {
+ get id() {
+ return this._item.id;
+ },
+
+ get name() {
+ return this._item.name;
+ },
+
+ get enabled() {
+ return this._item.isActive;
+ },
+
+ get version() {
+ return this._item.version;
+ },
+
+ get firstRun() {
+ return this._firstRun;
+ },
+
+ get storage() {
+ return this._storage;
+ },
+
+ get prefs() {
+ return this._prefs;
+ },
+
+ get events() {
+ return this._events;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIExtension])
+};
+
+
+// =================================================
+// Extensions constructor
+function Extensions(addons) {
+ this._cache = {};
+
+ addons.forEach(function (addon) {
+ this._cache[addon.id] = new Extension(addon);
+ }, this);
+}
+
+// =================================================
+// Extensions implementation
+Extensions.prototype = {
+ get all() {
+ return this.find({});
+ },
+
+ // XXX: Disabled until we can figure out the wrapped object issues
+ // id: "some@id" or /id/
+ // name: "name" or /name/
+ // version: "1.0.1"
+ // minVersion: "1.0"
+ // maxVersion: "2.0"
+ find: function exts_find(aOptions) {
+ return Object.keys(this._cache).map(id => this._cache[id]);
+ },
+
+ has: function exts_has(aId) {
+ return aId in this._cache;
+ },
+
+ get: function exts_get(aId) {
+ return this.has(aId) ? this._cache[aId] : null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIExtensions])
+};
+
+// =================================================
+// Application globals
+
+var gExtensionObserver = new ExtensionObserver();
+var gPreferenceObserver = new PreferenceObserver();
+
+// =================================================
+// extApplication constructor
+function extApplication() {
+}
+
+// =================================================
+// extApplication implementation
+extApplication.prototype = {
+ initToolkitHelpers: function extApp_initToolkitHelpers() {
+ XPCOMUtils.defineLazyServiceGetter(this, "_info",
+ "@mozilla.org/xre/app-info;1",
+ "nsIXULAppInfo");
+
+ this._obs = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ this._obs.addObserver(this, "xpcom-shutdown", /* ownsWeak = */ true);
+ this._registered = {"unload": true};
+ },
+
+ classInfo: XPCOMUtils.generateCI({interfaces: [Ci.extIApplication,
+ Ci.nsIObserver],
+ flags: Ci.nsIClassInfo.SINGLETON}),
+
+ // extIApplication
+ get id() {
+ return this._info.ID;
+ },
+
+ get name() {
+ return this._info.name;
+ },
+
+ get version() {
+ return this._info.version;
+ },
+
+ // for nsIObserver
+ observe: function app_observe(aSubject, aTopic, aData) {
+ if (aTopic == "app-startup") {
+ this.events.dispatch("load", "application");
+ }
+ else if (aTopic == "final-ui-startup") {
+ this.events.dispatch("ready", "application");
+ }
+ else if (aTopic == "quit-application-requested") {
+ // we can stop the quit by checking the return value
+ if (this.events.dispatch("quit", "application") == false)
+ aSubject.data = true;
+ }
+ else if (aTopic == "xpcom-shutdown") {
+ this.events.dispatch("unload", "application");
+ gExtensionObserver = null;
+ gPreferenceObserver = null;
+ }
+ },
+
+ get console() {
+ let console = new Console();
+ this.__defineGetter__("console", () => console);
+ return this.console;
+ },
+
+ get storage() {
+ let storage = new SessionStorage();
+ this.__defineGetter__("storage", () => storage);
+ return this.storage;
+ },
+
+ get prefs() {
+ let prefs = new PreferenceBranch("");
+ this.__defineGetter__("prefs", () => prefs);
+ return this.prefs;
+ },
+
+ getExtensions: function(callback) {
+ AddonManager.getAddonsByTypes(["extension"], function (addons) {
+ callback.callback(new Extensions(addons));
+ });
+ },
+
+ get events() {
+
+ // This ensures that FUEL only registers for notifications as needed
+ // by callers. Note that the unload (xpcom-shutdown) event is listened
+ // for by default, as it's needed for cleanup purposes.
+ var self = this;
+ function registerCheck(aEvent) {
+ var rmap = { "load": "app-startup",
+ "ready": "final-ui-startup",
+ "quit": "quit-application-requested"};
+ if (!(aEvent in rmap) || aEvent in self._registered)
+ return;
+
+ self._obs.addObserver(self, rmap[aEvent], /* ownsWeak = */ true);
+ self._registered[aEvent] = true;
+ }
+
+ let events = new Events(registerCheck);
+ this.__defineGetter__("events", () => events);
+ return this.events;
+ },
+
+ // helper method for correct quitting/restarting
+ _quitWithFlags: function app__quitWithFlags(aFlags) {
+ let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
+ let quitType = aFlags & Components.interfaces.nsIAppStartup.eRestart ? "restart" : null;
+ this._obs.notifyObservers(cancelQuit, "quit-application-requested", quitType);
+ if (cancelQuit.data)
+ return false; // somebody canceled our quit request
+
+ let appStartup = Components.classes['@mozilla.org/toolkit/app-startup;1']
+ .getService(Components.interfaces.nsIAppStartup);
+ appStartup.quit(aFlags);
+ return true;
+ },
+
+ quit: function app_quit() {
+ return this._quitWithFlags(Components.interfaces.nsIAppStartup.eAttemptQuit);
+ },
+
+ restart: function app_restart() {
+ return this._quitWithFlags(Components.interfaces.nsIAppStartup.eAttemptQuit |
+ Components.interfaces.nsIAppStartup.eRestart);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.extIApplication, Ci.nsISupportsWeakReference])
+};
diff --git a/toolkit/components/exthelper/extIApplication.idl b/toolkit/components/exthelper/extIApplication.idl
new file mode 100644
index 0000000000..51c17b436c
--- /dev/null
+++ b/toolkit/components/exthelper/extIApplication.idl
@@ -0,0 +1,416 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIVariant;
+interface extIPreference;
+interface extISessionStorage;
+
+
+/**
+ * Interface that gives simplified access to the console
+ */
+[scriptable, uuid(ae8482e0-aa5a-11db-abbd-0800200c9a66)]
+interface extIConsole : nsISupports
+{
+ /**
+ * Sends a given string to the console.
+ * @param aMsg
+ * The text to send to the console
+ */
+ void log(in AString aMsg);
+
+ /**
+ * Opens the error console window. The console window
+ * is focused if already open.
+ */
+ void open();
+};
+
+
+/**
+ * Interface holds information about an event.
+ */
+[scriptable, function, uuid(05281820-ab62-11db-abbd-0800200c9a66)]
+interface extIEventItem : nsISupports
+{
+ /**
+ * The name of the event
+ */
+ readonly attribute AString type;
+
+ /**
+ * Can hold extra details and data associated with the event. This
+ * is optional and event specific. If the event does not send extra
+ * details, this is null.
+ */
+ readonly attribute nsIVariant data;
+
+ /**
+ * Cancels the event if it is cancelable.
+ */
+ void preventDefault();
+};
+
+
+/**
+ * Interface used as a callback for listening to events.
+ */
+[scriptable, function, uuid(2dfe3a50-ab2f-11db-abbd-0800200c9a66)]
+interface extIEventListener : nsISupports
+{
+ /**
+ * This method is called whenever an event occurs of the type for which
+ * the extIEventListener interface was registered.
+ *
+ * @param aEvent
+ * The extIEventItem associated with the event.
+ */
+ void handleEvent(in extIEventItem aEvent);
+};
+
+
+/**
+ * Interface for supporting custom events.
+ */
+[scriptable, uuid(3a8ec9d0-ab19-11db-abbd-0800200c9a66)]
+interface extIEvents : nsISupports
+{
+ /**
+ * Adds an event listener to the list. If multiple identical event listeners
+ * are registered on the same event target with the same parameters the
+ * duplicate instances are discarded. They do not cause the EventListener
+ * to be called twice and since they are discarded they do not need to be
+ * removed with the removeListener method.
+ *
+ * @param aEvent
+ * The name of an event
+ * @param aListener
+ * The reference to a listener
+ */
+ void addListener(in AString aEvent, in extIEventListener aListener);
+
+ /**
+ * Removes an event listener from the list. Calling remove
+ * with arguments which do not identify any currently registered
+ * event listener has no effect.
+ * @param aEvent
+ * The name of an event
+ * @param aListener
+ * The reference to a listener
+ */
+ void removeListener(in AString aEvent, in extIEventListener aListener);
+};
+
+
+/**
+ * Interface for simplified access to preferences. The interface has a
+ * predefined root preference branch. The root branch is set based on the
+ * context of the owner. For example, an extension's preferences have a root
+ * of "extensions.<extensionid>.", while the application level preferences
+ * have an empty root. All preference "aName" parameters used in this interface
+ * are relative to the root branch.
+ */
+[scriptable, uuid(ce697d40-aa5a-11db-abbd-0800200c9a66)]
+interface extIPreferenceBranch : nsISupports
+{
+ /**
+ * The name of the branch root.
+ */
+ readonly attribute AString root;
+
+ /**
+ * Array of extIPreference listing all preferences in this branch.
+ */
+ readonly attribute nsIVariant all;
+
+ /**
+ * The events object for the preferences
+ * supports: "change"
+ */
+ readonly attribute extIEvents events;
+
+ /**
+ * Check to see if a preference exists.
+ * @param aName
+ * The name of preference
+ * @returns true if the preference exists, false if not
+ */
+ boolean has(in AString aName);
+
+ /**
+ * Gets an object representing a preference
+ * @param aName
+ * The name of preference
+ * @returns a preference object, or null if the preference does not exist
+ */
+ extIPreference get(in AString aName);
+
+ /**
+ * Gets the value of a preference. Returns a default value if
+ * the preference does not exist.
+ * @param aName
+ * The name of preference
+ * @param aDefaultValue
+ * The value to return if preference does not exist
+ * @returns value of the preference or the given default value if preference
+ * does not exists.
+ */
+ nsIVariant getValue(in AString aName, in nsIVariant aDefaultValue);
+
+ /**
+ * Sets the value of a storage item with the given name.
+ * @param aName
+ * The name of an item
+ * @param aValue
+ * The value to assign to the item
+ */
+ void setValue(in AString aName, in nsIVariant aValue);
+
+ /**
+ * Resets all preferences in a branch back to their default values.
+ */
+ void reset();
+};
+
+/**
+ * Interface for accessing a single preference. The data is not cached.
+ * All reads access the current state of the preference.
+ */
+[scriptable, uuid(2C7462E2-72C2-4473-9007-0E6AE71E23CA)]
+interface extIPreference : nsISupports
+{
+ /**
+ * The name of the preference.
+ */
+ readonly attribute AString name;
+
+ /**
+ * A string representing the type of preference (String, Boolean, or Number).
+ */
+ readonly attribute AString type;
+
+ /**
+ * Get/Set the value of the preference.
+ */
+ attribute nsIVariant value;
+
+ /**
+ * Get the locked state of the preference. Set to a boolean value to (un)lock it.
+ */
+ attribute boolean locked;
+
+ /**
+ * Check if a preference has been modified by the user, or not.
+ */
+ readonly attribute boolean modified;
+
+ /**
+ * The preference branch that contains this preference.
+ */
+ readonly attribute extIPreferenceBranch branch;
+
+ /**
+ * The events object for this preference.
+ * supports: "change"
+ */
+ readonly attribute extIEvents events;
+
+ /**
+ * Resets a preference back to its default values.
+ */
+ void reset();
+};
+
+
+/**
+ * Interface representing an extension
+ */
+[scriptable, uuid(10cee02c-f6e0-4d61-ab27-c16572b18c46)]
+interface extIExtension : nsISupports
+{
+ /**
+ * The id of the extension.
+ */
+ readonly attribute AString id;
+
+ /**
+ * The name of the extension.
+ */
+ readonly attribute AString name;
+
+ /**
+ * Check if the extension is currently enabled, or not.
+ */
+ readonly attribute boolean enabled;
+
+ /**
+ * The version number of the extension.
+ */
+ readonly attribute AString version;
+
+ /**
+ * Indicates whether this is the extension's first run after install
+ */
+ readonly attribute boolean firstRun;
+
+ /**
+ * The preferences object for the extension. Defaults to the
+ * "extensions.<extensionid>." branch.
+ */
+ readonly attribute extIPreferenceBranch prefs;
+
+ /**
+ * The storage object for the extension.
+ */
+ readonly attribute extISessionStorage storage;
+
+ /**
+ * The events object for the extension.
+ * supports: "uninstall"
+ */
+ readonly attribute extIEvents events;
+};
+
+/**
+ * Interface representing a list of all installed extensions
+ */
+[scriptable, uuid(de281930-aa5a-11db-abbd-0800200c9a66)]
+interface extIExtensions : nsISupports
+{
+ /**
+ * Array of extIExtension listing all extensions in the application.
+ */
+ readonly attribute nsIVariant all;
+
+ /**
+ * Determines if an extension exists with the given id.
+ * @param aId
+ * The id of an extension
+ * @returns true if an extension exists with the given id,
+ * false otherwise.
+ */
+ boolean has(in AString aId);
+
+ /**
+ * Gets a extIExtension object for an extension.
+ * @param aId
+ * The id of an extension
+ * @returns An extension object or null if no extension exists
+ * with the given id.
+ */
+ extIExtension get(in AString aId);
+};
+
+/**
+ * Interface representing a callback that receives an array of extIExtensions
+ */
+[scriptable, function, uuid(2571cbb5-550d-4400-8038-75df9b553f98)]
+interface extIExtensionsCallback : nsISupports
+{
+ void callback(in nsIVariant extensions);
+};
+
+/**
+ * Interface representing a simple storage system
+ */
+[scriptable, uuid(0787ac44-29b9-4889-b97f-13573aec6971)]
+interface extISessionStorage : nsISupports
+{
+ /**
+ * The events object for the storage
+ * supports: "change"
+ */
+ readonly attribute extIEvents events;
+
+ /**
+ * Determines if a storage item exists with the given name.
+ * @param aName
+ * The name of an item
+ * @returns true if an item exists with the given name,
+ * false otherwise.
+ */
+ boolean has(in AString aName);
+
+ /**
+ * Sets the value of a storage item with the given name.
+ * @param aName
+ * The name of an item
+ * @param aValue
+ * The value to assign to the item
+ */
+ void set(in AString aName, in nsIVariant aValue);
+
+ /**
+ * Gets the value of a storage item with the given name. Returns a
+ * default value if the item does not exist.
+ * @param aName
+ * The name of an item
+ * @param aDefaultValue
+ * The value to return if no item exists with the given name
+ * @returns value of the item or the given default value if no item
+ * exists with the given name.
+ */
+ nsIVariant get(in AString aName, in nsIVariant aDefaultValue);
+};
+
+[scriptable, uuid(2be87909-0817-4292-acfa-fc39be53be3f)]
+interface extIApplication : nsISupports
+{
+ /**
+ * The id of the application.
+ */
+ readonly attribute AString id;
+
+ /**
+ * The name of the application.
+ */
+ readonly attribute AString name;
+
+ /**
+ * The version number of the application.
+ */
+ readonly attribute AString version;
+
+ /**
+ * The console object for the application.
+ */
+ readonly attribute extIConsole console;
+
+ /**
+ * The extensions object for the application. Contains a list
+ * of all installed extensions.
+ */
+ void getExtensions(in extIExtensionsCallback aCallback);
+
+ /**
+ * The preferences object for the application. Defaults to an empty
+ * root branch.
+ */
+ readonly attribute extIPreferenceBranch prefs;
+
+ /**
+ * The storage object for the application.
+ */
+ readonly attribute extISessionStorage storage;
+
+ /**
+ * The events object for the application.
+ * supports: "load", "ready", "quit", "unload"
+ */
+ readonly attribute extIEvents events;
+
+ /**
+ * Quits the application (if nobody objects to quit-application-requested).
+ * @returns whether quitting will proceed
+ */
+ boolean quit();
+
+ /**
+ * Restarts the application (if nobody objects to quit-application-requested).
+ * @returns whether restarting will proceed
+ */
+ boolean restart();
+};
diff --git a/toolkit/components/exthelper/moz.build b/toolkit/components/exthelper/moz.build
new file mode 100644
index 0000000000..975030a353
--- /dev/null
+++ b/toolkit/components/exthelper/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/.
+
+XPIDL_SOURCES += [
+ 'extIApplication.idl',
+]
+
+XPIDL_MODULE = 'exthelper'
+
diff --git a/toolkit/components/feeds/FeedProcessor.js b/toolkit/components/feeds/FeedProcessor.js
new file mode 100644
index 0000000000..88d0ad6ed7
--- /dev/null
+++ b/toolkit/components/feeds/FeedProcessor.js
@@ -0,0 +1,1792 @@
+/* -*- 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 LOG(str) {
+ dump("*** " + str + "\n");
+}
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const FP_CONTRACTID = "@mozilla.org/feed-processor;1";
+const FP_CLASSID = Components.ID("{26acb1f0-28fc-43bc-867a-a46aabc85dd4}");
+const FP_CLASSNAME = "Feed Processor";
+const FR_CONTRACTID = "@mozilla.org/feed-result;1";
+const FR_CLASSID = Components.ID("{072a5c3d-30c6-4f07-b87f-9f63d51403f2}");
+const FR_CLASSNAME = "Feed Result";
+const FEED_CONTRACTID = "@mozilla.org/feed;1";
+const FEED_CLASSID = Components.ID("{5d0cfa97-69dd-4e5e-ac84-f253162e8f9a}");
+const FEED_CLASSNAME = "Feed";
+const ENTRY_CONTRACTID = "@mozilla.org/feed-entry;1";
+const ENTRY_CLASSID = Components.ID("{8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f}");
+const ENTRY_CLASSNAME = "Feed Entry";
+const TEXTCONSTRUCT_CONTRACTID = "@mozilla.org/feed-textconstruct;1";
+const TEXTCONSTRUCT_CLASSID =
+ Components.ID("{b992ddcd-3899-4320-9909-924b3e72c922}");
+const TEXTCONSTRUCT_CLASSNAME = "Feed Text Construct";
+const GENERATOR_CONTRACTID = "@mozilla.org/feed-generator;1";
+const GENERATOR_CLASSID =
+ Components.ID("{414af362-9ad8-4296-898e-62247f25a20e}");
+const GENERATOR_CLASSNAME = "Feed Generator";
+const PERSON_CONTRACTID = "@mozilla.org/feed-person;1";
+const PERSON_CLASSID = Components.ID("{95c963b7-20b2-11db-92f6-001422106990}");
+const PERSON_CLASSNAME = "Feed Person";
+
+const IO_CONTRACTID = "@mozilla.org/network/io-service;1"
+const BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1"
+const ARRAY_CONTRACTID = "@mozilla.org/array;1";
+const SAX_CONTRACTID = "@mozilla.org/saxparser/xmlreader;1";
+const PARSERUTILS_CONTRACTID = "@mozilla.org/parserutils;1";
+
+const gMimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+var gIoService = null;
+
+const XMLNS = "http://www.w3.org/XML/1998/namespace";
+const RSS090NS = "http://my.netscape.com/rdf/simple/0.9/";
+
+/** *** Some general utils *****/
+function strToURI(link, base) {
+ base = base || null;
+ if (!gIoService)
+ gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService);
+ try {
+ return gIoService.newURI(link, null, base);
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function isArray(a) {
+ return isObject(a) && a.constructor == Array;
+}
+
+function isObject(a) {
+ return (a && typeof a == "object") || isFunction(a);
+}
+
+function isFunction(a) {
+ return typeof a == "function";
+}
+
+function isIID(a, iid) {
+ var rv = false;
+ try {
+ a.QueryInterface(iid);
+ rv = true;
+ }
+ catch (e) {
+ }
+ return rv;
+}
+
+function isIArray(a) {
+ return isIID(a, Ci.nsIArray);
+}
+
+function isIFeedContainer(a) {
+ return isIID(a, Ci.nsIFeedContainer);
+}
+
+function stripTags(someHTML) {
+ return someHTML.replace(/<[^>]+>/g, "");
+}
+
+/**
+ * Searches through an array of links and returns a JS array
+ * of matching property bags.
+ */
+const IANA_URI = "http://www.iana.org/assignments/relation/";
+function findAtomLinks(rel, links) {
+ var rvLinks = [];
+ for (var i = 0; i < links.length; ++i) {
+ var linkElement = links.queryElementAt(i, Ci.nsIPropertyBag2);
+ // atom:link MUST have @href
+ if (bagHasKey(linkElement, "href")) {
+ var relAttribute = null;
+ if (bagHasKey(linkElement, "rel"))
+ relAttribute = linkElement.getPropertyAsAString("rel")
+ if ((!relAttribute && rel == "alternate") || relAttribute == rel) {
+ rvLinks.push(linkElement);
+ continue;
+ }
+ // catch relations specified by IANA URI
+ if (relAttribute == IANA_URI + rel) {
+ rvLinks.push(linkElement);
+ }
+ }
+ }
+ return rvLinks;
+}
+
+function xmlEscape(s) {
+ s = s.replace(/&/g, "&amp;");
+ s = s.replace(/>/g, "&gt;");
+ s = s.replace(/</g, "&lt;");
+ s = s.replace(/"/g, "&quot;");
+ s = s.replace(/'/g, "&apos;");
+ return s;
+}
+
+function arrayContains(array, element) {
+ for (var i = 0; i < array.length; ++i) {
+ if (array[i] == element) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// XXX add hasKey to nsIPropertyBag
+function bagHasKey(bag, key) {
+ try {
+ bag.getProperty(key);
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+}
+
+function makePropGetter(key) {
+ return function FeedPropGetter(bag) {
+ try {
+ return value = bag.getProperty(key);
+ }
+ catch (e) {
+ }
+ return null;
+ }
+}
+
+const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+// namespace map
+var gNamespaces = {
+ "http://webns.net/mvcb/":"admin",
+ "http://backend.userland.com/rss":"",
+ "http://blogs.law.harvard.edu/tech/rss":"",
+ "http://www.w3.org/2005/Atom":"atom",
+ "http://purl.org/atom/ns#":"atom03",
+ "http://purl.org/rss/1.0/modules/content/":"content",
+ "http://purl.org/dc/elements/1.1/":"dc",
+ "http://purl.org/dc/terms/":"dcterms",
+ "http://www.w3.org/1999/02/22-rdf-syntax-ns#":"rdf",
+ "http://purl.org/rss/1.0/":"rss1",
+ "http://my.netscape.com/rdf/simple/0.9/":"rss1",
+ "http://wellformedweb.org/CommentAPI/":"wfw",
+ "http://purl.org/rss/1.0/modules/wiki/":"wiki",
+ "http://www.w3.org/XML/1998/namespace":"xml",
+ "http://search.yahoo.com/mrss/":"media",
+ "http://search.yahoo.com/mrss":"media"
+}
+
+// We allow a very small set of namespaces in XHTML content,
+// for attributes only
+var gAllowedXHTMLNamespaces = {
+ "http://www.w3.org/XML/1998/namespace":"xml",
+ // if someone ns qualifies XHTML, we have to prefix it to avoid an
+ // attribute collision.
+ "http://www.w3.org/1999/xhtml":"xhtml"
+}
+
+function FeedResult() {}
+FeedResult.prototype = {
+ bozo: false,
+ doc: null,
+ version: null,
+ headers: null,
+ uri: null,
+ stylesheet: null,
+
+ registerExtensionPrefix: function FR_registerExtensionPrefix(ns, prefix) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ // XPCOM stuff
+ classID: FR_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResult])
+}
+
+function Feed() {
+ this.subtitle = null;
+ this.title = null;
+ this.items = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.link = null;
+ this.id = null;
+ this.generator = null;
+ this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.baseURI = null;
+ this.enclosureCount = 0;
+ this.type = Ci.nsIFeed.TYPE_FEED;
+}
+
+Feed.prototype = {
+ searchLists: {
+ title: ["title", "rss1:title", "atom03:title", "atom:title"],
+ subtitle: ["description", "dc:description", "rss1:description",
+ "atom03:tagline", "atom:subtitle"],
+ items: ["items", "atom03_entries", "entries"],
+ id: ["atom:id", "rdf:about"],
+ generator: ["generator"],
+ authors : ["authors"],
+ contributors: ["contributors"],
+ link: [["link", strToURI], ["rss1:link", strToURI]],
+ categories: ["categories", "dc:subject"],
+ rights: ["atom03:rights", "atom:rights"],
+ cloud: ["cloud"],
+ image: ["image", "rss1:image", "atom:logo"],
+ textInput: ["textInput", "rss1:textinput"],
+ skipDays: ["skipDays"],
+ skipHours: ["skipHours"],
+ updated: ["pubDate", "lastBuildDate", "atom03:modified", "dc:date",
+ "dcterms:modified", "atom:updated"]
+ },
+
+ normalize: function Feed_normalize() {
+ fieldsToObj(this, this.searchLists);
+ if (this.skipDays)
+ this.skipDays = this.skipDays.getProperty("days");
+ if (this.skipHours)
+ this.skipHours = this.skipHours.getProperty("hours");
+
+ if (this.updated)
+ this.updated = dateParse(this.updated);
+
+ // Assign Atom link if needed
+ if (bagHasKey(this.fields, "links"))
+ this._atomLinksToURI();
+
+ this._calcEnclosureCountAndFeedType();
+
+ // Resolve relative image links
+ if (this.image && bagHasKey(this.image, "url"))
+ this._resolveImageLink();
+
+ this._resetBagMembersToRawText([this.searchLists.subtitle,
+ this.searchLists.title]);
+ },
+
+ _calcEnclosureCountAndFeedType: function Feed_calcEnclosureCountAndFeedType() {
+ var entries_with_enclosures = 0;
+ var audio_count = 0;
+ var image_count = 0;
+ var video_count = 0;
+ var other_count = 0;
+
+ for (var i = 0; i < this.items.length; ++i) {
+ var entry = this.items.queryElementAt(i, Ci.nsIFeedEntry);
+ entry.QueryInterface(Ci.nsIFeedContainer);
+
+ if (entry.enclosures && entry.enclosures.length > 0) {
+ ++entries_with_enclosures;
+
+ for (var e = 0; e < entry.enclosures.length; ++e) {
+ var enc = entry.enclosures.queryElementAt(e, Ci.nsIWritablePropertyBag2);
+ if (enc.hasKey("type")) {
+ var enctype = enc.get("type");
+
+ if (/^audio/.test(enctype)) {
+ ++audio_count;
+ } else if (/^image/.test(enctype)) {
+ ++image_count;
+ } else if (/^video/.test(enctype)) {
+ ++video_count;
+ } else {
+ ++other_count;
+ }
+ } else {
+ ++other_count;
+ }
+ }
+ }
+ }
+
+ var feedtype = Ci.nsIFeed.TYPE_FEED;
+
+ // For a feed to be marked as TYPE_VIDEO, TYPE_AUDIO and TYPE_IMAGE,
+ // we enforce two things:
+ //
+ // 1. all entries must have at least one enclosure
+ // 2. all enclosures must be video for TYPE_VIDEO, audio for TYPE_AUDIO or image
+ // for TYPE_IMAGE
+ //
+ // Otherwise it's a TYPE_FEED.
+ if (entries_with_enclosures == this.items.length && other_count == 0) {
+ if (audio_count > 0 && !video_count && !image_count) {
+ feedtype = Ci.nsIFeed.TYPE_AUDIO;
+
+ } else if (image_count > 0 && !audio_count && !video_count) {
+ feedtype = Ci.nsIFeed.TYPE_IMAGE;
+
+ } else if (video_count > 0 && !audio_count && !image_count) {
+ feedtype = Ci.nsIFeed.TYPE_VIDEO;
+ }
+ }
+
+ this.type = feedtype;
+ this.enclosureCount = other_count + video_count + audio_count + image_count;
+ },
+
+ _atomLinksToURI: function Feed_linkToURI() {
+ var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
+ var alternates = findAtomLinks("alternate", links);
+ if (alternates.length > 0) {
+ var href = alternates[0].getPropertyAsAString("href");
+ var base;
+ if (bagHasKey(alternates[0], "xml:base"))
+ base = alternates[0].getPropertyAsAString("xml:base");
+ this.link = this._resolveURI(href, base);
+ }
+ },
+
+ _resolveImageLink: function Feed_resolveImageLink() {
+ var base;
+ if (bagHasKey(this.image, "xml:base"))
+ base = this.image.getPropertyAsAString("xml:base");
+ var url = this._resolveURI(this.image.getPropertyAsAString("url"), base);
+ if (url)
+ this.image.setPropertyAsAString("url", url.spec);
+ },
+
+ _resolveURI: function Feed_resolveURI(linkSpec, baseSpec) {
+ var uri = null;
+ try {
+ var base = baseSpec ? strToURI(baseSpec, this.baseURI) : this.baseURI;
+ uri = strToURI(linkSpec, base);
+ }
+ catch (e) {
+ LOG(e);
+ }
+
+ return uri;
+ },
+
+ // reset the bag to raw contents, not text constructs
+ _resetBagMembersToRawText: function Feed_resetBagMembers(fieldLists) {
+ for (var i=0; i<fieldLists.length; i++) {
+ for (var j=0; j<fieldLists[i].length; j++) {
+ if (bagHasKey(this.fields, fieldLists[i][j])) {
+ var textConstruct = this.fields.getProperty(fieldLists[i][j]);
+ this.fields.setPropertyAsAString(fieldLists[i][j],
+ textConstruct.text);
+ }
+ }
+ }
+ },
+
+ // XPCOM stuff
+ classID: FEED_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeed, Ci.nsIFeedContainer])
+}
+
+function Entry() {
+ this.summary = null;
+ this.content = null;
+ this.title = null;
+ this.fields = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag2);
+ this.link = null;
+ this.id = null;
+ this.baseURI = null;
+ this.updated = null;
+ this.published = null;
+ this.authors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.contributors = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+}
+
+Entry.prototype = {
+ fields: null,
+ enclosures: null,
+ mediaContent: null,
+
+ searchLists: {
+ title: ["title", "rss1:title", "atom03:title", "atom:title"],
+ link: [["link", strToURI], ["rss1:link", strToURI]],
+ id: [["guid", makePropGetter("guid")], "rdf:about",
+ "atom03:id", "atom:id"],
+ authors : ["authors"],
+ contributors: ["contributors"],
+ summary: ["description", "rss1:description", "dc:description",
+ "atom03:summary", "atom:summary"],
+ content: ["content:encoded", "atom03:content", "atom:content"],
+ rights: ["atom03:rights", "atom:rights"],
+ published: ["pubDate", "atom03:issued", "dcterms:issued", "atom:published"],
+ updated: ["pubDate", "atom03:modified", "dc:date", "dcterms:modified",
+ "atom:updated"]
+ },
+
+ normalize: function Entry_normalize() {
+ fieldsToObj(this, this.searchLists);
+
+ // Assign Atom link if needed
+ if (bagHasKey(this.fields, "links"))
+ this._atomLinksToURI();
+
+ // Populate enclosures array
+ this._populateEnclosures();
+
+ // The link might be a guid w/ permalink=true
+ if (!this.link && bagHasKey(this.fields, "guid")) {
+ var guid = this.fields.getProperty("guid");
+ var isPermaLink = true;
+
+ if (bagHasKey(guid, "isPermaLink"))
+ isPermaLink = guid.getProperty("isPermaLink").toLowerCase() != "false";
+
+ if (guid && isPermaLink)
+ this.link = strToURI(guid.getProperty("guid"));
+ }
+
+ if (this.updated)
+ this.updated = dateParse(this.updated);
+ if (this.published)
+ this.published = dateParse(this.published);
+
+ this._resetBagMembersToRawText([this.searchLists.content,
+ this.searchLists.summary,
+ this.searchLists.title]);
+ },
+
+ _populateEnclosures: function Entry_populateEnclosures() {
+ if (bagHasKey(this.fields, "links"))
+ this._atomLinksToEnclosures();
+
+ // Add RSS2 enclosure to enclosures
+ if (bagHasKey(this.fields, "enclosure"))
+ this._enclosureToEnclosures();
+
+ // Add media:content to enclosures
+ if (bagHasKey(this.fields, "mediacontent"))
+ this._mediaToEnclosures("mediacontent");
+
+ // Add media:thumbnail to enclosures
+ if (bagHasKey(this.fields, "mediathumbnail"))
+ this._mediaToEnclosures("mediathumbnail");
+
+ // Add media:content in media:group to enclosures
+ if (bagHasKey(this.fields, "mediagroup"))
+ this._mediaToEnclosures("mediagroup", "mediacontent");
+ },
+
+ __enclosure_map: null,
+
+ _addToEnclosures: function Entry_addToEnclosures(new_enc) {
+ // items we add to the enclosures array get displayed in the FeedWriter and
+ // they must have non-empty urls.
+ if (!bagHasKey(new_enc, "url") || new_enc.getPropertyAsAString("url") == "")
+ return;
+
+ if (this.__enclosure_map == null)
+ this.__enclosure_map = {};
+
+ var previous_enc = this.__enclosure_map[new_enc.getPropertyAsAString("url")];
+
+ if (previous_enc != undefined) {
+ previous_enc.QueryInterface(Ci.nsIWritablePropertyBag2);
+
+ if (!bagHasKey(previous_enc, "type") && bagHasKey(new_enc, "type")) {
+ previous_enc.setPropertyAsAString("type", new_enc.getPropertyAsAString("type"));
+ try {
+ let handlerInfoWrapper = gMimeService.getFromTypeAndExtension(new_enc.getPropertyAsAString("type"), null);
+ if (handlerInfoWrapper && handlerInfoWrapper.description) {
+ previous_enc.setPropertyAsAString("typeDesc", handlerInfoWrapper.description);
+ }
+ } catch (ext) {}
+ }
+
+ if (!bagHasKey(previous_enc, "length") && bagHasKey(new_enc, "length"))
+ previous_enc.setPropertyAsAString("length", new_enc.getPropertyAsAString("length"));
+
+ return;
+ }
+
+ if (this.enclosures == null) {
+ this.enclosures = Cc[ARRAY_CONTRACTID].createInstance(Ci.nsIMutableArray);
+ this.enclosures.QueryInterface(Ci.nsIMutableArray);
+ }
+
+ this.enclosures.appendElement(new_enc, false);
+ this.__enclosure_map[new_enc.getPropertyAsAString("url")] = new_enc;
+ },
+
+ _atomLinksToEnclosures: function Entry_linkToEnclosure() {
+ var links = this.fields.getPropertyAsInterface("links", Ci.nsIArray);
+ var enc_links = findAtomLinks("enclosure", links);
+ if (enc_links.length == 0)
+ return;
+
+ for (var i = 0; i < enc_links.length; ++i) {
+ var link = enc_links[i];
+
+ // an enclosure must have an href
+ if (!(link.getProperty("href")))
+ return;
+
+ var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+
+ // copy Atom bits over to equivalent enclosure bits
+ enc.setPropertyAsAString("url", link.getPropertyAsAString("href"));
+ if (bagHasKey(link, "type"))
+ enc.setPropertyAsAString("type", link.getPropertyAsAString("type"));
+ if (bagHasKey(link, "length"))
+ enc.setPropertyAsAString("length", link.getPropertyAsAString("length"));
+
+ this._addToEnclosures(enc);
+ }
+ },
+
+ _enclosureToEnclosures: function Entry_enclosureToEnclosures() {
+ var enc = this.fields.getPropertyAsInterface("enclosure", Ci.nsIPropertyBag2);
+
+ if (!(enc.getProperty("url")))
+ return;
+
+ this._addToEnclosures(enc);
+ },
+
+ _mediaToEnclosures: function Entry_mediaToEnclosures(mediaType, contentType) {
+ var content;
+
+ // If a contentType is specified, the mediaType is a simple propertybag,
+ // and the contentType is an array inside it.
+ if (contentType) {
+ var group = this.fields.getPropertyAsInterface(mediaType, Ci.nsIPropertyBag2);
+ content = group.getPropertyAsInterface(contentType, Ci.nsIArray);
+ } else {
+ content = this.fields.getPropertyAsInterface(mediaType, Ci.nsIArray);
+ }
+
+ for (var i = 0; i < content.length; ++i) {
+ var contentElement = content.queryElementAt(i, Ci.nsIWritablePropertyBag2);
+
+ // media:content don't require url, but if it's not there, we should
+ // skip it.
+ if (!bagHasKey(contentElement, "url"))
+ continue;
+
+ var enc = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+
+ // copy media:content bits over to equivalent enclosure bits
+ enc.setPropertyAsAString("url", contentElement.getPropertyAsAString("url"));
+ if (bagHasKey(contentElement, "type")) {
+ enc.setPropertyAsAString("type", contentElement.getPropertyAsAString("type"));
+ } else if (mediaType == "mediathumbnail") {
+ // thumbnails won't have a type, but default to image types
+ enc.setPropertyAsAString("type", "image/*");
+ enc.setPropertyAsBool("thumbnail", true);
+ }
+
+ if (bagHasKey(contentElement, "fileSize")) {
+ enc.setPropertyAsAString("length", contentElement.getPropertyAsAString("fileSize"));
+ }
+
+ this._addToEnclosures(enc);
+ }
+ },
+
+ // XPCOM stuff
+ classID: ENTRY_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIFeedEntry, Ci.nsIFeedContainer]
+ )
+}
+
+Entry.prototype._atomLinksToURI = Feed.prototype._atomLinksToURI;
+Entry.prototype._resolveURI = Feed.prototype._resolveURI;
+Entry.prototype._resetBagMembersToRawText =
+ Feed.prototype._resetBagMembersToRawText;
+
+// TextConstruct represents and element that could contain (X)HTML
+function TextConstruct() {
+ this.lang = null;
+ this.base = null;
+ this.type = "text";
+ this.text = null;
+ this.parserUtils = Cc[PARSERUTILS_CONTRACTID].getService(Ci.nsIParserUtils);
+}
+
+TextConstruct.prototype = {
+ plainText: function TC_plainText() {
+ if (this.type != "text") {
+ return this.parserUtils.convertToPlainText(stripTags(this.text),
+ Ci.nsIDocumentEncoder.OutputSelectionOnly |
+ Ci.nsIDocumentEncoder.OutputAbsoluteLinks,
+ 0);
+ }
+ return this.text;
+ },
+
+ createDocumentFragment: function TC_createDocumentFragment(element) {
+ if (this.type == "text") {
+ var doc = element.ownerDocument;
+ var docFragment = doc.createDocumentFragment();
+ var node = doc.createTextNode(this.text);
+ docFragment.appendChild(node);
+ return docFragment;
+ }
+ var isXML;
+ if (this.type == "xhtml")
+ isXML = true
+ else if (this.type == "html")
+ isXML = false;
+ else
+ return null;
+
+ return this.parserUtils.parseFragment(this.text, 0, isXML,
+ this.base, element);
+ },
+
+ // XPCOM stuff
+ classID: TEXTCONSTRUCT_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedTextConstruct])
+}
+
+// Generator represents the software that produced the feed
+function Generator() {
+ this.lang = null;
+ this.agent = null;
+ this.version = null;
+ this.uri = null;
+
+ // nsIFeedElementBase
+ this._attributes = null;
+ this.baseURI = null;
+}
+
+Generator.prototype = {
+
+ get attributes() {
+ return this._attributes;
+ },
+
+ set attributes(value) {
+ this._attributes = value;
+ this.version = this._attributes.getValueFromName("", "version");
+ var uriAttribute = this._attributes.getValueFromName("", "uri") ||
+ this._attributes.getValueFromName("", "url");
+ this.uri = strToURI(uriAttribute, this.baseURI);
+
+ // RSS1
+ uriAttribute = this._attributes.getValueFromName(RDF_NS, "resource");
+ if (uriAttribute) {
+ this.agent = uriAttribute;
+ this.uri = strToURI(uriAttribute, this.baseURI);
+ }
+ },
+
+ // XPCOM stuff
+ classID: GENERATOR_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIFeedGenerator, Ci.nsIFeedElementBase]
+ )
+}
+
+function Person() {
+ this.name = null;
+ this.uri = null;
+ this.email = null;
+
+ // nsIFeedElementBase
+ this.attributes = null;
+ this.baseURI = null;
+}
+
+Person.prototype = {
+ // XPCOM stuff
+ classID: PERSON_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIFeedPerson, Ci.nsIFeedElementBase]
+ )
+}
+
+/**
+ * Map a list of fields into properties on a container.
+ *
+ * @param container An nsIFeedContainer
+ * @param fields A list of fields to search for. List members can
+ * be a list, in which case the second member is
+ * transformation function (like parseInt).
+ */
+function fieldsToObj(container, fields) {
+ var props, prop, field, searchList;
+ for (var key in fields) {
+ searchList = fields[key];
+ for (var i=0; i < searchList.length; ++i) {
+ props = searchList[i];
+ prop = null;
+ field = isArray(props) ? props[0] : props;
+ try {
+ prop = container.fields.getProperty(field);
+ }
+ catch (e) {
+ }
+ if (prop) {
+ prop = isArray(props) ? props[1](prop) : prop;
+ container[key] = prop;
+ }
+ }
+ }
+}
+
+/**
+ * Lower cases an element's localName property
+ * @param element A DOM element.
+ *
+ * @returns The lower case localName property of the specified element
+ */
+function LC(element) {
+ return element.localName.toLowerCase();
+}
+
+// TODO move these post-processor functions
+// create a generator element
+function atomGenerator(s, generator) {
+ generator.QueryInterface(Ci.nsIFeedGenerator);
+ generator.agent = s.trim();
+ return generator;
+}
+
+// post-process atom:logo to create an RSS2-like structure
+function atomLogo(s, logo) {
+ logo.setPropertyAsAString("url", s.trim());
+}
+
+// post-process an RSS category, map it to the Atom fields.
+function rssCatTerm(s, cat) {
+ // add slash handling?
+ cat.setPropertyAsAString("term", s.trim());
+ return cat;
+}
+
+// post-process a GUID
+function rssGuid(s, guid) {
+ guid.setPropertyAsAString("guid", s.trim());
+ return guid;
+}
+
+// post-process an RSS author element
+//
+// It can contain a field like this:
+//
+// <author>lawyer@boyer.net (Lawyer Boyer)</author>
+//
+// or, delightfully, a field like this:
+//
+// <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+//
+// We want to split this up and assign it to corresponding Atom
+// fields.
+//
+function rssAuthor(s, author) {
+ author.QueryInterface(Ci.nsIFeedPerson);
+ // check for RSS2 string format
+ var chars = s.trim();
+ var matches = chars.match(/(.*)\((.*)\)/);
+ var emailCheck =
+ /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+ if (matches) {
+ var match1 = matches[1].trim();
+ var match2 = matches[2].trim();
+ if (match2.indexOf("mailto:") == 0)
+ match2 = match2.substring(7);
+ if (emailCheck.test(match1)) {
+ author.email = match1;
+ author.name = match2;
+ }
+ else if (emailCheck.test(match2)) {
+ author.email = match2;
+ author.name = match1;
+ }
+ else {
+ // put it back together
+ author.name = match1 + " (" + match2 + ")";
+ }
+ }
+ else {
+ author.name = chars;
+ if (chars.indexOf('@'))
+ author.email = chars;
+ }
+ return author;
+}
+
+//
+// skipHours and skipDays map to arrays, so we need to change the
+// string to an nsISupports in order to stick it in there.
+//
+function rssArrayElement(s) {
+ var str = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ str.data = s;
+ str.QueryInterface(Ci.nsISupportsString);
+ return str;
+}
+
+/**
+ * Tries parsing a string through the JavaScript Date object.
+ * @param aDateString
+ * A string that is supposedly an RFC822 or RFC3339 date.
+ * @return A Date.toUTCString, or null if the string can't be parsed.
+ */
+function dateParse(aDateString) {
+ let dateString = aDateString.trim();
+ // Without bug 682781 fixed, JS won't parse an RFC822 date with a Z for the
+ // timezone, so convert to -00:00 which works for any date format.
+ dateString = dateString.replace(/z$/i, "-00:00");
+ let date = new Date(dateString);
+ if (!isNaN(date)) {
+ return date.toUTCString();
+ }
+ return null;
+}
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+// The XHTMLHandler handles inline XHTML found in things like atom:summary
+function XHTMLHandler(processor, isAtom) {
+ this._buf = "";
+ this._processor = processor;
+ this._depth = 0;
+ this._isAtom = isAtom;
+ // a stack of lists tracking in-scope namespaces
+ this._inScopeNS = [];
+}
+
+// The fidelity can be improved here, to allow handling of stuff like
+// SVG and MathML. XXX
+XHTMLHandler.prototype = {
+
+ // look back up at the declared namespaces
+ // we always use the same prefixes for our safe stuff
+ _isInScope: function XH__isInScope(ns) {
+ for (var i in this._inScopeNS) {
+ for (var uri in this._inScopeNS[i]) {
+ if (this._inScopeNS[i][uri] == ns)
+ return true;
+ }
+ }
+ return false;
+ },
+
+ startDocument: function XH_startDocument() {
+ },
+ endDocument: function XH_endDocument() {
+ },
+ startElement: function XH_startElement(namespace, localName, qName, attributes) {
+ ++this._depth;
+ this._inScopeNS.push([]);
+
+ // RFC4287 requires XHTML to be wrapped in a div that is *not* part of
+ // the content. This prevents people from screwing up namespaces, but
+ // we need to skip it here.
+ if (this._isAtom && this._depth == 1 && localName == "div")
+ return;
+
+ // If it's an XHTML element, record it. Otherwise, it's ignored.
+ if (namespace == XHTML_NS) {
+ this._buf += "<" + localName;
+ var uri;
+ for (var i=0; i < attributes.length; ++i) {
+ uri = attributes.getURI(i);
+ // XHTML attributes aren't in a namespace
+ if (uri == "") {
+ this._buf += (" " + attributes.getLocalName(i) + "='" +
+ xmlEscape(attributes.getValue(i)) + "'");
+ } else {
+ // write a small set of allowed attribute namespaces
+ var prefix = gAllowedXHTMLNamespaces[uri];
+ if (prefix != null) {
+ // The attribute value we'll attempt to write
+ var attributeValue = xmlEscape(attributes.getValue(i));
+
+ // it's an allowed attribute NS.
+ // write the attribute
+ this._buf += (" " + prefix + ":" +
+ attributes.getLocalName(i) +
+ "='" + attributeValue + "'");
+
+ // write an xmlns declaration if necessary
+ if (prefix != "xml" && !this._isInScope(uri)) {
+ this._inScopeNS[this._inScopeNS.length - 1].push(uri);
+ this._buf += " xmlns:" + prefix + "='" + uri + "'";
+ }
+ }
+ }
+ }
+ this._buf += ">";
+ }
+ },
+ endElement: function XH_endElement(uri, localName, qName) {
+ --this._depth;
+ this._inScopeNS.pop();
+
+ // We need to skip outer divs in Atom. See comment in startElement.
+ if (this._isAtom && this._depth == 0 && localName == "div")
+ return;
+
+ // When we peek too far, go back to the main processor
+ if (this._depth < 0) {
+ this._processor.returnFromXHTMLHandler(this._buf.trim(),
+ uri, localName, qName);
+ return;
+ }
+ // If it's an XHTML element, record it. Otherwise, it's ignored.
+ if (uri == XHTML_NS) {
+ this._buf += "</" + localName + ">";
+ }
+ },
+ characters: function XH_characters(data) {
+ this._buf += xmlEscape(data);
+ },
+ startPrefixMapping: function XH_startPrefixMapping(prefix, uri) {
+ },
+ endPrefixMapping: function FP_endPrefixMapping(prefix) {
+ },
+ processingInstruction: function XH_processingInstruction() {
+ },
+}
+
+/**
+ * The ExtensionHandler deals with elements we haven't explicitly
+ * added to our transition table in the FeedProcessor.
+ */
+function ExtensionHandler(processor) {
+ this._buf = "";
+ this._depth = 0;
+ this._hasChildElements = false;
+
+ // The FeedProcessor
+ this._processor = processor;
+
+ // Fields of the outermost extension element.
+ this._localName = null;
+ this._uri = null;
+ this._qName = null;
+ this._attrs = null;
+}
+
+ExtensionHandler.prototype = {
+ startDocument: function EH_startDocument() {
+ },
+ endDocument: function EH_endDocument() {
+ },
+ startElement: function EH_startElement(uri, localName, qName, attrs) {
+ ++this._depth;
+
+ if (this._depth == 1) {
+ this._uri = uri;
+ this._localName = localName;
+ this._qName = qName;
+ this._attrs = attrs;
+ }
+
+ // if we descend into another element, we won't send text
+ this._hasChildElements = (this._depth > 1);
+
+ },
+ endElement: function EH_endElement(uri, localName, qName) {
+ --this._depth;
+ if (this._depth == 0) {
+ var text = this._hasChildElements ? null : this._buf.trim();
+ this._processor.returnFromExtHandler(this._uri, this._localName,
+ text, this._attrs);
+ }
+ },
+ characters: function EH_characters(data) {
+ if (!this._hasChildElements)
+ this._buf += data;
+ },
+ startPrefixMapping: function EH_startPrefixMapping() {
+ },
+ endPrefixMapping: function EH_endPrefixMapping() {
+ },
+ processingInstruction: function EH_processingInstruction() {
+ },
+};
+
+
+/**
+ * ElementInfo is a simple container object that describes
+ * some characteristics of a feed element. For example, it
+ * says whether an element can be expected to appear more
+ * than once inside a given entry or feed.
+ */
+function ElementInfo(fieldName, containerClass, closeFunc, isArray) {
+ this.fieldName = fieldName;
+ this.containerClass = containerClass;
+ this.closeFunc = closeFunc;
+ this.isArray = isArray;
+ this.isWrapper = false;
+}
+
+/**
+ * FeedElementInfo represents a feed element, usually the root.
+ */
+function FeedElementInfo(fieldName, feedVersion) {
+ this.isWrapper = false;
+ this.fieldName = fieldName;
+ this.feedVersion = feedVersion;
+}
+
+/**
+ * Some feed formats include vestigial wrapper elements that we don't
+ * want to include in our object model, but we do need to keep track
+ * of during parsing.
+ */
+function WrapperElementInfo(fieldName) {
+ this.isWrapper = true;
+ this.fieldName = fieldName;
+}
+
+/** *** The Processor *****/
+function FeedProcessor() {
+ this._reader = Cc[SAX_CONTRACTID].createInstance(Ci.nsISAXXMLReader);
+ this._buf = "";
+ this._feed = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+ this._handlerStack = [];
+ this._xmlBaseStack = []; // sparse array keyed to nesting depth
+ this._depth = 0;
+ this._state = "START";
+ this._result = null;
+ this._extensionHandler = null;
+ this._xhtmlHandler = null;
+ this._haveSentResult = false;
+
+ // The nsIFeedResultListener waiting for the parse results
+ this.listener = null;
+
+ // These elements can contain (X)HTML or plain text.
+ // We keep a table here that contains their default treatment
+ this._textConstructs = {"atom:title":"text",
+ "atom:summary":"text",
+ "atom:rights":"text",
+ "atom:content":"text",
+ "atom:subtitle":"text",
+ "description":"html",
+ "rss1:description":"html",
+ "dc:description":"html",
+ "content:encoded":"html",
+ "title":"text",
+ "rss1:title":"text",
+ "atom03:title":"text",
+ "atom03:tagline":"text",
+ "atom03:summary":"text",
+ "atom03:content":"text"};
+ this._stack = [];
+
+ this._trans = {
+ "START": {
+ // If we hit a root RSS element, treat as RSS2.
+ "rss": new FeedElementInfo("RSS2", "rss2"),
+
+ // If we hit an RDF element, if could be RSS1, but we can't
+ // verify that until we hit a rss1:channel element.
+ "rdf:RDF": new WrapperElementInfo("RDF"),
+
+ // If we hit a Atom 1.0 element, treat as Atom 1.0.
+ "atom:feed": new FeedElementInfo("Atom", "atom"),
+
+ // Treat as Atom 0.3
+ "atom03:feed": new FeedElementInfo("Atom03", "atom03"),
+ },
+
+ /** ******* RSS2 **********/
+ "IN_RSS2": {
+ "channel": new WrapperElementInfo("channel")
+ },
+
+ "IN_CHANNEL": {
+ "item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
+ "managingEditor": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "category": new ElementInfo("categories", null, rssCatTerm, true),
+ "cloud": new ElementInfo("cloud", null, null, false),
+ "image": new ElementInfo("image", null, null, false),
+ "textInput": new ElementInfo("textInput", null, null, false),
+ "skipDays": new ElementInfo("skipDays", null, null, false),
+ "skipHours": new ElementInfo("skipHours", null, null, false),
+ "generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
+ atomGenerator, false),
+ },
+
+ "IN_ITEMS": {
+ "author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "category": new ElementInfo("categories", null, rssCatTerm, true),
+ "enclosure": new ElementInfo("enclosure", null, null, false),
+ "media:content": new ElementInfo("mediacontent", null, null, true),
+ "media:group": new ElementInfo("mediagroup", null, null, false),
+ "media:thumbnail": new ElementInfo("mediathumbnail", null, null, true),
+ "guid": new ElementInfo("guid", null, rssGuid, false)
+ },
+
+ "IN_SKIPDAYS": {
+ "day": new ElementInfo("days", null, rssArrayElement, true)
+ },
+
+ "IN_SKIPHOURS":{
+ "hour": new ElementInfo("hours", null, rssArrayElement, true)
+ },
+
+ "IN_MEDIAGROUP": {
+ "media:content": new ElementInfo("mediacontent", null, null, true),
+ "media:thumbnail": new ElementInfo("mediathumbnail", null, null, true)
+ },
+
+ /** ******* RSS1 **********/
+ "IN_RDF": {
+ // If we hit a rss1:channel, we can verify that we have RSS1
+ "rss1:channel": new FeedElementInfo("rdf_channel", "rss1"),
+ "rss1:image": new ElementInfo("image", null, null, false),
+ "rss1:textinput": new ElementInfo("textInput", null, null, false),
+ "rss1:item": new ElementInfo("items", Cc[ENTRY_CONTRACTID], null, true),
+ },
+
+ "IN_RDF_CHANNEL": {
+ "admin:generatorAgent": new ElementInfo("generator",
+ Cc[GENERATOR_CONTRACTID],
+ null, false),
+ "dc:creator": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ "dc:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ rssAuthor, true),
+ },
+
+ /** ******* ATOM 1.0 **********/
+ "IN_ATOM": {
+ "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
+ atomGenerator, false),
+ "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:link": new ElementInfo("links", null, null, true),
+ "atom:logo": new ElementInfo("atom:logo", null, atomLogo, false),
+ "atom:entry": new ElementInfo("entries", Cc[ENTRY_CONTRACTID],
+ null, true)
+ },
+
+ "IN_ENTRIES": {
+ "atom:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:contributor": new ElementInfo("contributors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom:link": new ElementInfo("links", null, null, true),
+ },
+
+ /** ******* ATOM 0.3 **********/
+ "IN_ATOM03": {
+ "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:contributor": new ElementInfo("contributors",
+ Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:link": new ElementInfo("links", null, null, true),
+ "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
+ null, true),
+ "atom03:generator": new ElementInfo("generator", Cc[GENERATOR_CONTRACTID],
+ atomGenerator, false),
+ },
+
+ "IN_ATOM03_ENTRIES": {
+ "atom03:author": new ElementInfo("authors", Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:contributor": new ElementInfo("contributors",
+ Cc[PERSON_CONTRACTID],
+ null, true),
+ "atom03:link": new ElementInfo("links", null, null, true),
+ "atom03:entry": new ElementInfo("atom03_entries", Cc[ENTRY_CONTRACTID],
+ null, true)
+ }
+ }
+}
+
+// See startElement for a long description of how feeds are processed.
+FeedProcessor.prototype = {
+
+ // Set ourselves as the SAX handler, and set the base URI
+ _init: function FP_init(uri) {
+ this._reader.contentHandler = this;
+ this._reader.errorHandler = this;
+ this._result = Cc[FR_CONTRACTID].createInstance(Ci.nsIFeedResult);
+ if (uri) {
+ this._result.uri = uri;
+ this._reader.baseURI = uri;
+ this._xmlBaseStack[0] = uri;
+ }
+ },
+
+ // This function is called once we figure out what type of feed
+ // we're dealing with. Some feed types require digging a bit further
+ // than the root.
+ _docVerified: function FP_docVerified(version) {
+ this._result.doc = Cc[FEED_CONTRACTID].createInstance(Ci.nsIFeed);
+ this._result.doc.baseURI =
+ this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ this._result.doc.fields = this._feed;
+ this._result.version = version;
+ },
+
+ // When we're done with the feed, let the listener know what
+ // happened.
+ _sendResult: function FP_sendResult() {
+ this._haveSentResult = true;
+ try {
+ // Can be null when a non-feed is fed to us
+ if (this._result.doc)
+ this._result.doc.normalize();
+ }
+ catch (e) {
+ LOG("FIXME: " + e);
+ }
+
+ try {
+ if (this.listener != null)
+ this.listener.handleResult(this._result);
+ }
+ finally {
+ this._result = null;
+ }
+ },
+
+ // Parsing functions
+ parseFromStream: function FP_parseFromStream(stream, uri) {
+ this._init(uri);
+ this._reader.parseFromStream(stream, null, stream.available(),
+ "application/xml");
+ this._reader = null;
+ },
+
+ parseFromString: function FP_parseFromString(inputString, uri) {
+ this._init(uri);
+ this._reader.parseFromString(inputString, "application/xml");
+ this._reader = null;
+ },
+
+ parseAsync: function FP_parseAsync(requestObserver, uri) {
+ this._init(uri);
+ this._reader.parseAsync(requestObserver);
+ },
+
+ // nsIStreamListener
+
+ // The XMLReader will throw sensible exceptions if these get called
+ // out of order.
+ onStartRequest: function FP_onStartRequest(request, context) {
+ // this will throw if the request is not a channel, but so will nsParser.
+ var channel = request.QueryInterface(Ci.nsIChannel);
+ channel.contentType = "application/vnd.mozilla.maybe.feed";
+ this._reader.onStartRequest(request, context);
+ },
+
+ onStopRequest: function FP_onStopRequest(request, context, statusCode) {
+ try {
+ this._reader.onStopRequest(request, context, statusCode);
+ }
+ finally {
+ this._reader = null;
+ }
+ },
+
+ onDataAvailable:
+ function FP_onDataAvailable(request, context, inputStream, offset, count) {
+ this._reader.onDataAvailable(request, context, inputStream, offset, count);
+ },
+
+ // nsISAXErrorHandler
+
+ // We only care about fatal errors. When this happens, we may have
+ // parsed through the feed metadata and some number of entries. The
+ // listener can still show some of that data if it wants, and we'll
+ // set the bozo bit to indicate we were unable to parse all the way
+ // through.
+ fatalError: function FP_reportError() {
+ this._result.bozo = true;
+ // XXX need to QI to FeedProgressListener
+ if (!this._haveSentResult)
+ this._sendResult();
+ },
+
+ // nsISAXContentHandler
+
+ startDocument: function FP_startDocument() {
+ // LOG("----------");
+ },
+
+ endDocument: function FP_endDocument() {
+ if (!this._haveSentResult)
+ this._sendResult();
+ },
+
+ // The transitions defined above identify elements that contain more
+ // than just text. For example RSS items contain many fields, and so
+ // do Atom authors. The only commonly used elements that contain
+ // mixed content are Atom Text Constructs of type="xhtml", which we
+ // delegate to another handler for cleaning. That leaves a couple
+ // different types of elements to deal with: those that should occur
+ // only once, such as title elements, and those that can occur
+ // multiple times, such as the RSS category element and the Atom
+ // link element. Most of the RSS1/DC elements can occur multiple
+ // times in theory, but in practice, the only ones that do have
+ // analogues in Atom.
+ //
+ // Some elements are also groups of attributes or sub-elements,
+ // while others are simple text fields. For the most part, we don't
+ // have to pay explicit attention to the simple text elements,
+ // unless we want to post-process the resulting string to transform
+ // it into some richer object like a Date or URI.
+ //
+ // Elements that have more sophisticated content models still end up
+ // being dictionaries, whether they are based on attributes like RSS
+ // cloud, sub-elements like Atom author, or even items and
+ // entries. These elements are treated as "containers". It's
+ // theoretically possible for a container to have an attribute with
+ // the same universal name as a sub-element, but none of the feed
+ // formats allow this by default, and I don't of any extension that
+ // works this way.
+ //
+ startElement: function FP_startElement(uri, localName, qName, attributes) {
+ this._buf = "";
+ ++this._depth;
+ var elementInfo;
+
+ // LOG("<" + localName + ">");
+
+ // Check for xml:base
+ var base = attributes.getValueFromName(XMLNS, "base");
+ if (base) {
+ this._xmlBaseStack[this._depth] =
+ strToURI(base, this._xmlBaseStack[this._xmlBaseStack.length - 1]);
+ }
+
+ // To identify the element we're dealing with, we look up the
+ // namespace URI in our gNamespaces dictionary, which will give us
+ // a "canonical" prefix for a namespace URI. For example, this
+ // allows Dublin Core "creator" elements to be consistently mapped
+ // to "dc:creator", for easy field access by consumer code. This
+ // strategy also happens to shorten up our state table.
+ var key = this._prefixForNS(uri) + localName;
+
+ // Check to see if we need to hand this off to our XHTML handler.
+ // The elements we're dealing with will look like this:
+ //
+ // <title type="xhtml">
+ // <div xmlns="http://www.w3.org/1999/xhtml">
+ // A title with <b>bold</b> and <i>italics</i>.
+ // </div>
+ // </title>
+ //
+ // When it returns in returnFromXHTMLHandler, the handler should
+ // give us back a string like this:
+ //
+ // "A title with <b>bold</b> and <i>italics</i>."
+ //
+ // The Atom spec explicitly says the div is not part of the content,
+ // and explicitly allows whitespace collapsing.
+ //
+ if ((this._result.version == "atom" || this._result.version == "atom03") &&
+ this._textConstructs[key] != null) {
+ var type = attributes.getValueFromName("", "type");
+ if (type != null && type.indexOf("xhtml") >= 0) {
+ this._xhtmlHandler =
+ new XHTMLHandler(this, (this._result.version == "atom"));
+ this._reader.contentHandler = this._xhtmlHandler;
+ return;
+ }
+ }
+
+ // Check our current state, and see if that state has a defined
+ // transition. For example, this._trans["atom:entry"]["atom:author"]
+ // will have one, and it tells us to add an item to our authors array.
+ if (this._trans[this._state] && this._trans[this._state][key]) {
+ elementInfo = this._trans[this._state][key];
+ }
+ else {
+ // If we don't have a transition, hand off to extension handler
+ this._extensionHandler = new ExtensionHandler(this);
+ this._reader.contentHandler = this._extensionHandler;
+ this._extensionHandler.startElement(uri, localName, qName, attributes);
+ return;
+ }
+
+ // This distinguishes wrappers like 'channel' from elements
+ // we'd actually like to do something with (which will test true).
+ this._handlerStack[this._depth] = elementInfo;
+ if (elementInfo.isWrapper) {
+ this._state = "IN_" + elementInfo.fieldName.toUpperCase();
+ this._stack.push([this._feed, this._state]);
+ }
+ else if (elementInfo.feedVersion) {
+ this._state = "IN_" + elementInfo.fieldName.toUpperCase();
+
+ // Check for the older RSS2 variants
+ if (elementInfo.feedVersion == "rss2")
+ elementInfo.feedVersion = this._findRSSVersion(attributes);
+ else if (uri == RSS090NS)
+ elementInfo.feedVersion = "rss090";
+
+ this._docVerified(elementInfo.feedVersion);
+ this._stack.push([this._feed, this._state]);
+ this._mapAttributes(this._feed, attributes);
+ }
+ else {
+ this._state = this._processComplexElement(elementInfo, attributes);
+ }
+ },
+
+ // In the endElement handler, we decrement the stack and look
+ // for cleanup/transition functions to execute. The second part
+ // of the state transition works as above in startElement, but
+ // the state we're looking for is prefixed with an underscore
+ // to distinguish endElement events from startElement events.
+ endElement: function FP_endElement(uri, localName, qName) {
+ var elementInfo = this._handlerStack[this._depth];
+ // LOG("</" + localName + ">");
+ if (elementInfo && !elementInfo.isWrapper)
+ this._closeComplexElement(elementInfo);
+
+ // cut down xml:base context
+ if (this._xmlBaseStack.length == this._depth + 1)
+ this._xmlBaseStack = this._xmlBaseStack.slice(0, this._depth);
+
+ // our new state is whatever is at the top of the stack now
+ if (this._stack.length > 0)
+ this._state = this._stack[this._stack.length - 1][1];
+ this._handlerStack = this._handlerStack.slice(0, this._depth);
+ --this._depth;
+ },
+
+ // Buffer up character data. The buffer is cleared with every
+ // opening element.
+ characters: function FP_characters(data) {
+ this._buf += data;
+ },
+ // TODO: It would be nice to check new prefixes here, and if they
+ // don't conflict with the ones we've defined, throw them in a
+ // dictionary to check.
+ startPrefixMapping: function FP_startPrefixMapping(prefix, uri) {
+ },
+
+ endPrefixMapping: function FP_endPrefixMapping(prefix) {
+ },
+
+ processingInstruction: function FP_processingInstruction(target, data) {
+ if (target == "xml-stylesheet") {
+ var hrefAttribute = data.match(/href=[\"\'](.*?)[\"\']/);
+ if (hrefAttribute && hrefAttribute.length == 2)
+ this._result.stylesheet = strToURI(hrefAttribute[1], this._result.uri);
+ }
+ },
+
+ // end of nsISAXContentHandler
+
+ // Handle our more complicated elements--those that contain
+ // attributes and child elements.
+ _processComplexElement:
+ function FP__processComplexElement(elementInfo, attributes) {
+ var obj;
+
+ // If the container is an entry/item, it'll need to have its
+ // more esoteric properties put in the 'fields' property bag.
+ if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID]) {
+ obj = elementInfo.containerClass.createInstance(Ci.nsIFeedEntry);
+ obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ this._mapAttributes(obj.fields, attributes);
+ }
+ else if (elementInfo.containerClass) {
+ obj = elementInfo.containerClass.createInstance(Ci.nsIFeedElementBase);
+ obj.baseURI = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ obj.attributes = attributes; // just set the SAX attributes
+ }
+ else {
+ obj = Cc[BAG_CONTRACTID].createInstance(Ci.nsIWritablePropertyBag2);
+ this._mapAttributes(obj, attributes);
+ }
+
+ // We should have a container/propertyBag that's had its
+ // attributes processed. Now we need to attach it to its
+ // container.
+ var newProp;
+
+ // First we'll see what's on top of the stack.
+ var container = this._stack[this._stack.length - 1][0];
+
+ // Check to see if it has the property
+ var prop;
+ try {
+ prop = container.getProperty(elementInfo.fieldName);
+ }
+ catch (e) {
+ }
+
+ if (elementInfo.isArray) {
+ if (!prop) {
+ container.setPropertyAsInterface(elementInfo.fieldName,
+ Cc[ARRAY_CONTRACTID].
+ createInstance(Ci.nsIMutableArray));
+ }
+
+ newProp = container.getProperty(elementInfo.fieldName);
+ // XXX This QI should not be necessary, but XPConnect seems to fly
+ // off the handle in the browser, and loses track of the interface
+ // on large files. Bug 335638.
+ newProp.QueryInterface(Ci.nsIMutableArray);
+ newProp.appendElement(obj, false);
+
+ // If new object is an nsIFeedContainer, we want to deal with
+ // its member nsIPropertyBag instead.
+ if (isIFeedContainer(obj))
+ newProp = obj.fields;
+
+ }
+ else {
+ // If it doesn't, set it.
+ if (!prop) {
+ container.setPropertyAsInterface(elementInfo.fieldName, obj);
+ }
+ newProp = container.getProperty(elementInfo.fieldName);
+ }
+
+ // make our new state name, and push the property onto the stack
+ var newState = "IN_" + elementInfo.fieldName.toUpperCase();
+ this._stack.push([newProp, newState, obj]);
+ return newState;
+ },
+
+ // Sometimes we need reconcile the element content with the object
+ // model for a given feed. We use helper functions to do the
+ // munging, but we need to identify array types here, so the munging
+ // happens only to the last element of an array.
+ _closeComplexElement: function FP__closeComplexElement(elementInfo) {
+ var stateTuple = this._stack.pop();
+ var container = stateTuple[0];
+ var containerParent = stateTuple[2];
+ var element = null;
+ var isArray = isIArray(container);
+
+ // If it's an array and we have to post-process,
+ // grab the last element
+ if (isArray)
+ element = container.queryElementAt(container.length - 1, Ci.nsISupports);
+ else
+ element = container;
+
+ // Run the post-processing function if there is one.
+ if (elementInfo.closeFunc)
+ element = elementInfo.closeFunc(this._buf, element);
+
+ // If an nsIFeedContainer was on top of the stack,
+ // we need to normalize it
+ if (elementInfo.containerClass == Cc[ENTRY_CONTRACTID])
+ containerParent.normalize();
+
+ // If it's an array, re-set the last element
+ if (isArray)
+ container.replaceElementAt(element, container.length - 1, false);
+ },
+
+ _prefixForNS: function FP_prefixForNS(uri) {
+ if (!uri)
+ return "";
+ var prefix = gNamespaces[uri];
+ if (prefix)
+ return prefix + ":";
+ if (uri.toLowerCase().indexOf("http://backend.userland.com") == 0)
+ return "";
+ return null;
+ },
+
+ _mapAttributes: function FP__mapAttributes(bag, attributes) {
+ // Cycle through the attributes, and set our properties using the
+ // prefix:localNames we find in our namespace dictionary.
+ for (var i = 0; i < attributes.length; ++i) {
+ var key = this._prefixForNS(attributes.getURI(i)) + attributes.getLocalName(i);
+ var val = attributes.getValue(i);
+ bag.setPropertyAsAString(key, val);
+ }
+ },
+
+ // Only for RSS2esque formats
+ _findRSSVersion: function FP__findRSSVersion(attributes) {
+ var versionAttr = attributes.getValueFromName("", "version").trim();
+ var versions = { "0.91":"rss091",
+ "0.92":"rss092",
+ "0.93":"rss093",
+ "0.94":"rss094" }
+ if (versions[versionAttr])
+ return versions[versionAttr];
+ if (versionAttr.substr(0, 2) != "2.")
+ return "rssUnknown";
+ return "rss2";
+ },
+
+ // unknown element values are returned here. See startElement above
+ // for how this works.
+ returnFromExtHandler:
+ function FP_returnExt(uri, localName, chars, attributes) {
+ --this._depth;
+
+ // take control of the SAX events
+ this._reader.contentHandler = this;
+ if (localName == null && chars == null)
+ return;
+
+ // we don't take random elements inside rdf:RDF
+ if (this._state == "IN_RDF")
+ return;
+
+ // Grab the top of the stack
+ var top = this._stack[this._stack.length - 1];
+ if (!top)
+ return;
+
+ var container = top[0];
+ // Grab the last element if it's an array
+ if (isIArray(container)) {
+ var contract = this._handlerStack[this._depth].containerClass;
+ // check if it's something specific, but not an entry
+ if (contract && contract != Cc[ENTRY_CONTRACTID]) {
+ var el = container.queryElementAt(container.length - 1,
+ Ci.nsIFeedElementBase);
+ // XXX there must be a way to flatten these interfaces
+ if (contract == Cc[PERSON_CONTRACTID])
+ el.QueryInterface(Ci.nsIFeedPerson);
+ else
+ return; // don't know about this interface
+
+ let propName = localName;
+ var prefix = gNamespaces[uri];
+
+ // synonyms
+ if ((uri == "" ||
+ prefix &&
+ ((prefix.indexOf("atom") > -1) ||
+ (prefix.indexOf("rss") > -1))) &&
+ (propName == "url" || propName == "href"))
+ propName = "uri";
+
+ try {
+ if (el[propName] !== "undefined") {
+ var propValue = chars;
+ // convert URI-bearing values to an nsIURI
+ if (propName == "uri") {
+ var base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ propValue = strToURI(chars, base);
+ }
+ el[propName] = propValue;
+ }
+ }
+ catch (e) {
+ // ignore XPConnect errors
+ }
+ // the rest of the function deals with entry- and feed-level stuff
+ return;
+ }
+ container = container.queryElementAt(container.length - 1,
+ Ci.nsIWritablePropertyBag2);
+ }
+
+ // Make the buffer our new property
+ var propName = this._prefixForNS(uri) + localName;
+
+ // But, it could be something containing HTML. If so,
+ // we need to know about that.
+ if (this._textConstructs[propName] != null &&
+ this._handlerStack[this._depth].containerClass !== null) {
+ var newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
+ createInstance(Ci.nsIFeedTextConstruct);
+ newProp.text = chars;
+ // Look up the default type in our table
+ var type = this._textConstructs[propName];
+ var typeAttribute = attributes.getValueFromName("", "type");
+ if (this._result.version == "atom" && typeAttribute != null) {
+ type = typeAttribute;
+ }
+ else if (this._result.version == "atom03" && typeAttribute != null) {
+ if (typeAttribute.toLowerCase().indexOf("xhtml") >= 0) {
+ type = "xhtml";
+ }
+ else if (typeAttribute.toLowerCase().indexOf("html") >= 0) {
+ type = "html";
+ }
+ else if (typeAttribute.toLowerCase().indexOf("text") >= 0) {
+ type = "text";
+ }
+ }
+
+ // If it's rss feed-level description, it's not supposed to have html
+ if (this._result.version.indexOf("rss") >= 0 &&
+ this._handlerStack[this._depth].containerClass != ENTRY_CONTRACTID) {
+ type = "text";
+ }
+ newProp.type = type;
+ newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ container.setPropertyAsInterface(propName, newProp);
+ }
+ else {
+ container.setPropertyAsAString(propName, chars);
+ }
+ },
+
+ // Sometimes, we'll hand off SAX handling duties to an XHTMLHandler
+ // (see above) that will scrape out non-XHTML stuff, normalize
+ // namespaces, and remove the wrapper div from Atom 1.0. When the
+ // XHTMLHandler is done, it'll callback here.
+ returnFromXHTMLHandler:
+ function FP_returnFromXHTMLHandler(chars, uri, localName, qName) {
+ // retake control of the SAX content events
+ this._reader.contentHandler = this;
+
+ // Grab the top of the stack
+ var top = this._stack[this._stack.length - 1];
+ if (!top)
+ return;
+ var container = top[0];
+
+ // Assign the property
+ var newProp = newProp = Cc[TEXTCONSTRUCT_CONTRACTID].
+ createInstance(Ci.nsIFeedTextConstruct);
+ newProp.text = chars;
+ newProp.type = "xhtml";
+ newProp.base = this._xmlBaseStack[this._xmlBaseStack.length - 1];
+ container.setPropertyAsInterface(this._prefixForNS(uri) + localName,
+ newProp);
+
+ // XHTML will cause us to peek too far. The XHTML handler will
+ // send us an end element to call. RFC4287-valid feeds allow a
+ // more graceful way to handle this. Unfortunately, we can't count
+ // on compliance at this point.
+ this.endElement(uri, localName, qName);
+ },
+
+ // XPCOM stuff
+ classID: FP_CLASSID,
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIFeedProcessor, Ci.nsISAXContentHandler, Ci.nsISAXErrorHandler,
+ Ci.nsIStreamListener, Ci.nsIRequestObserver]
+ )
+}
+
+var components = [FeedProcessor, FeedResult, Feed, Entry,
+ TextConstruct, Generator, Person];
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/components/feeds/FeedProcessor.manifest b/toolkit/components/feeds/FeedProcessor.manifest
new file mode 100644
index 0000000000..5081d70c5d
--- /dev/null
+++ b/toolkit/components/feeds/FeedProcessor.manifest
@@ -0,0 +1,14 @@
+component {072a5c3d-30c6-4f07-b87f-9f63d51403f2} FeedProcessor.js
+contract @mozilla.org/feed-result;1 {072a5c3d-30c6-4f07-b87f-9f63d51403f2}
+component {5d0cfa97-69dd-4e5e-ac84-f253162e8f9a} FeedProcessor.js
+contract @mozilla.org/feed;1 {5d0cfa97-69dd-4e5e-ac84-f253162e8f9a}
+component {8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f} FeedProcessor.js
+contract @mozilla.org/feed-entry;1 {8e4444ff-8e99-4bdd-aa7f-fb3c1c77319f}
+component {b992ddcd-3899-4320-9909-924b3e72c922} FeedProcessor.js
+contract @mozilla.org/feed-textconstruct;1 {b992ddcd-3899-4320-9909-924b3e72c922}
+component {414af362-9ad8-4296-898e-62247f25a20e} FeedProcessor.js
+contract @mozilla.org/feed-generator;1 {414af362-9ad8-4296-898e-62247f25a20e}
+component {95c963b7-20b2-11db-92f6-001422106990} FeedProcessor.js
+contract @mozilla.org/feed-person;1 {95c963b7-20b2-11db-92f6-001422106990}
+component {26acb1f0-28fc-43bc-867a-a46aabc85dd4} FeedProcessor.js
+contract @mozilla.org/feed-processor;1 {26acb1f0-28fc-43bc-867a-a46aabc85dd4}
diff --git a/toolkit/components/feeds/moz.build b/toolkit/components/feeds/moz.build
new file mode 100644
index 0000000000..de5e0aa96c
--- /dev/null
+++ b/toolkit/components/feeds/moz.build
@@ -0,0 +1,31 @@
+# -*- 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 += ['test/chrome.ini']
+
+XPIDL_SOURCES += [
+ 'nsIFeed.idl',
+ 'nsIFeedContainer.idl',
+ 'nsIFeedElementBase.idl',
+ 'nsIFeedEntry.idl',
+ 'nsIFeedGenerator.idl',
+ 'nsIFeedListener.idl',
+ 'nsIFeedPerson.idl',
+ 'nsIFeedProcessor.idl',
+ 'nsIFeedResult.idl',
+ 'nsIFeedTextConstruct.idl',
+]
+
+XPIDL_MODULE = 'feeds'
+
+EXTRA_COMPONENTS += [
+ 'FeedProcessor.js',
+ 'FeedProcessor.manifest',
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'test/xpcshell.ini'
+]
diff --git a/toolkit/components/feeds/nsIFeed.idl b/toolkit/components/feeds/nsIFeed.idl
new file mode 100644
index 0000000000..ad87ad9d3e
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeed.idl
@@ -0,0 +1,86 @@
+/* -*- 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 "nsIFeedContainer.idl"
+
+interface nsIArray;
+interface nsIFeedGenerator;
+
+/**
+ * An nsIFeed represents a single Atom or RSS feed.
+ */
+[scriptable, uuid(3b8aae33-80e2-4efa-99c8-a6c5b99f76ea)]
+interface nsIFeed : nsIFeedContainer
+{
+ /**
+ * Uses description, subtitle, and extensions
+ * to generate a summary.
+ */
+ attribute nsIFeedTextConstruct subtitle;
+
+ // All content classifies as a "feed" - it is the transport.
+ const unsigned long TYPE_FEED = 0;
+ const unsigned long TYPE_AUDIO = 1;
+ const unsigned long TYPE_IMAGE = 2;
+ const unsigned long TYPE_VIDEO = 4;
+
+ /**
+ * The type of feed. For example, a podcast would be TYPE_AUDIO.
+ */
+ readonly attribute unsigned long type;
+
+ /**
+ * The total number of enclosures found in the feed.
+ */
+ attribute long enclosureCount;
+
+ /**
+ * The items or entries in feed.
+ */
+ attribute nsIArray items;
+
+ /**
+ * No one really knows what cloud is for.
+ *
+ * It supposedly enables some sort of interaction with an XML-RPC or
+ * SOAP service.
+ */
+ attribute nsIWritablePropertyBag2 cloud;
+
+ /**
+ * Information about the software that produced the feed.
+ */
+ attribute nsIFeedGenerator generator;
+
+ /**
+ * An image url and some metadata (as defined by RSS2).
+ *
+ */
+ attribute nsIWritablePropertyBag2 image;
+
+ /**
+ * No one really knows what textInput is for.
+ *
+ * See
+ * <http://www.cadenhead.org/workbench/news/2894/rss-joy-textinput>
+ * for more details.
+ */
+ attribute nsIWritablePropertyBag2 textInput;
+
+ /**
+ * Days to skip fetching. This field was supposed to designate
+ * intervals for feed fetching. It's not generally implemented. For
+ * example, if this array contained "Monday", aggregators should not
+ * fetch the feed on Mondays.
+ */
+ attribute nsIArray skipDays;
+
+ /**
+ * Hours to skip fetching. This field was supposed to designate
+ * intervals for feed fetching. It's not generally implemented. See
+ * <http://blogs.law.harvard.edu/tech/rss> for more information.
+ */
+ attribute nsIArray skipHours;
+};
diff --git a/toolkit/components/feeds/nsIFeedContainer.idl b/toolkit/components/feeds/nsIFeedContainer.idl
new file mode 100644
index 0000000000..58de494a51
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedContainer.idl
@@ -0,0 +1,85 @@
+/* -*- 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 "nsIFeedElementBase.idl"
+
+interface nsIURI;
+interface nsIWritablePropertyBag2;
+interface nsIArray;
+interface nsIFeedTextConstruct;
+
+/**
+ * A shared base for feeds and items, which are pretty similar,
+ * but they have some divergent attributes and require
+ * different convenience methods.
+ */
+[scriptable, uuid(577a1b4c-b3d4-4c76-9cf8-753e6606114f)]
+interface nsIFeedContainer : nsIFeedElementBase
+{
+ /**
+ * Many feeds contain an ID distinct from their URI, and
+ * entries have standard fields for this in all major formats.
+ */
+ attribute AString id;
+
+ /**
+ * The fields found in the document. Common Atom
+ * and RSS fields are normalized. This includes some namespaced
+ * extensions such as dc:subject and content:encoded.
+ * Consumers can avoid normalization by checking the feed type
+ * and accessing specific fields.
+ *
+ * Common namespaces are accessed using prefixes, like get("dc:subject");.
+ * See nsIFeedResult::registerExtensionPrefix.
+ */
+ attribute nsIWritablePropertyBag2 fields;
+
+ /**
+ * Sometimes there's no title, or the title contains markup, so take
+ * care in decoding the attribute.
+ */
+ attribute nsIFeedTextConstruct title;
+
+ /**
+ * Returns the primary link for the feed or entry.
+ */
+ attribute nsIURI link;
+
+ /**
+ * Returns all links for a feed or entry.
+ */
+ attribute nsIArray links;
+
+ /**
+ * Returns the categories found in a feed or entry.
+ */
+ attribute nsIArray categories;
+
+ /**
+ * The rights or license associated with a feed or entry.
+ */
+ attribute nsIFeedTextConstruct rights;
+
+ /**
+ * A list of nsIFeedPersons that authored the feed.
+ */
+ attribute nsIArray authors;
+
+ /**
+ * A list of nsIFeedPersons that contributed to the feed.
+ */
+ attribute nsIArray contributors;
+
+ /**
+ * The date the feed was updated, in RFC822 form. Parsable by JS
+ * and mail code.
+ */
+ attribute AString updated;
+
+ /**
+ * Syncs a container's fields with its convenience attributes.
+ */
+ void normalize();
+};
diff --git a/toolkit/components/feeds/nsIFeedElementBase.idl b/toolkit/components/feeds/nsIFeedElementBase.idl
new file mode 100644
index 0000000000..1b8975ae5a
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedElementBase.idl
@@ -0,0 +1,28 @@
+/* -*- 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 nsISAXAttributes;
+interface nsIURI;
+
+/**
+ * An nsIFeedGenerator represents the software used to create a feed.
+ */
+[scriptable, uuid(5215291e-fa0a-40c2-8ce7-e86cd1a1d3fa)]
+interface nsIFeedElementBase : nsISupports
+{
+ /**
+ * The attributes found on the element. Most interfaces provide convenience
+ * accessors for their standard fields, so this useful only when looking for
+ * an extension.
+ */
+ attribute nsISAXAttributes attributes;
+
+ /**
+ * The baseURI for the Entry or Feed.
+ */
+ attribute nsIURI baseURI;
+};
diff --git a/toolkit/components/feeds/nsIFeedEntry.idl b/toolkit/components/feeds/nsIFeedEntry.idl
new file mode 100644
index 0000000000..83646aadb6
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedEntry.idl
@@ -0,0 +1,46 @@
+/* -*- 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 "nsIFeedContainer.idl"
+interface nsIArray;
+
+/**
+ * An nsIFeedEntry represents an Atom or RSS entry/item. Summary
+ * and/or full-text content may be available, but callers will have to
+ * check both.
+ */
+[scriptable, uuid(31bfd5b4-8ff5-4bfd-a8cb-b3dfbd4f0a5b)]
+interface nsIFeedEntry : nsIFeedContainer {
+
+ /**
+ * Uses description, subtitle, summary, content and extensions
+ * to generate a summary.
+ *
+ */
+ attribute nsIFeedTextConstruct summary;
+
+ /**
+ * The date the entry was published, in RFC822 form. Parsable by JS
+ * and mail code.
+ */
+ attribute AString published;
+
+ /**
+ * Uses atom:content and content:encoded to provide
+ * a 'full text' view of an entry.
+ *
+ */
+ attribute nsIFeedTextConstruct content;
+
+ /**
+ * Enclosures are podcasts, photocasts, etc.
+ */
+ attribute nsIArray enclosures;
+
+ /**
+ * Enclosures, etc. that might be displayed inline.
+ */
+ attribute nsIArray mediaContent;
+};
diff --git a/toolkit/components/feeds/nsIFeedGenerator.idl b/toolkit/components/feeds/nsIFeedGenerator.idl
new file mode 100644
index 0000000000..3c23ca1424
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedGenerator.idl
@@ -0,0 +1,30 @@
+/* -*- 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 "nsIFeedElementBase.idl"
+
+interface nsIURI;
+
+/**
+ * An nsIFeedGenerator represents the software used to create a feed.
+ */
+[scriptable, uuid(0fecd56b-bd92-481b-a486-b8d489cdd385)]
+interface nsIFeedGenerator : nsIFeedElementBase
+{
+ /**
+ * The name of the software.
+ */
+ attribute AString agent;
+
+ /**
+ * The version of the software.
+ */
+ attribute AString version;
+
+ /**
+ * A URI associated with the software.
+ */
+ attribute nsIURI uri;
+};
diff --git a/toolkit/components/feeds/nsIFeedListener.idl b/toolkit/components/feeds/nsIFeedListener.idl
new file mode 100644
index 0000000000..6826d04a41
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedListener.idl
@@ -0,0 +1,87 @@
+/* -*- 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 nsIFeedResult;
+interface nsIFeedEntry;
+
+/**
+ * nsIFeedResultListener defines a callback used when feed processing
+ * completes.
+ */
+[scriptable, uuid(4d2ebe88-36eb-4e20-bcd1-997b3c1f24ce)]
+interface nsIFeedResultListener : nsISupports
+{
+ /**
+ * Always called, even after an error. There could be new feed-level
+ * data available at this point, if it followed or was interspersed
+ * with the items. Fire-and-Forget implementations only need this.
+ *
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata.
+ */
+ void handleResult(in nsIFeedResult result);
+};
+
+
+/**
+ * nsIFeedProgressListener defines callbacks used during feed
+ * processing.
+ */
+[scriptable, uuid(ebfd5de5-713c-40c0-ad7c-f095117fa580)]
+interface nsIFeedProgressListener : nsIFeedResultListener {
+
+ /**
+ * ReportError will be called in the event of fatal
+ * XML errors, or if the document is not a feed. The bozo
+ * bit will be set if the error was due to a fatal error.
+ *
+ * @param errorText
+ * A short description of the error.
+ * @param lineNumber
+ * The line on which the error occurred.
+ */
+ void reportError(in AString errorText, in long lineNumber,
+ in boolean bozo);
+
+ /**
+ * StartFeed will be called as soon as a reasonable start to
+ * a feed is detected.
+ *
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata. At this point, the result has version
+ * information.
+ */
+ void handleStartFeed(in nsIFeedResult result);
+
+ /**
+ * Called when the first entry/item is encountered. In Atom, all
+ * feed data is required to preceed the entries. In RSS, the data
+ * usually does. If the type is one of the entry/item-only types,
+ * this event will not be called.
+ *
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata. At this point, the result will likely have
+ * most of its feed-level metadata.
+ */
+ void handleFeedAtFirstEntry(in nsIFeedResult result);
+
+ /**
+ * Called after each entry/item. If the document is a standalone
+ * item or entry, this HandleFeedAtFirstEntry will not have been
+ * called. Also, this entry's parent field will be null.
+ *
+ * @param entry
+ * An object implementing nsIFeedEntry that represents the latest
+ * entry encountered.
+ * @param result
+ * An object implementing nsIFeedResult representing the feed
+ * and its metadata.
+ */
+ void handleEntry(in nsIFeedEntry entry, in nsIFeedResult result);
+};
diff --git a/toolkit/components/feeds/nsIFeedPerson.idl b/toolkit/components/feeds/nsIFeedPerson.idl
new file mode 100644
index 0000000000..d9d6eb77bf
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedPerson.idl
@@ -0,0 +1,30 @@
+/* -*- 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 "nsIFeedElementBase.idl"
+
+interface nsIURI;
+
+/**
+ * An nsIFeedPerson represents an author or contributor of a feed.
+ */
+[scriptable, uuid(29cbd45f-f2d3-4b28-b557-3ab7a61ecde4)]
+interface nsIFeedPerson : nsIFeedElementBase
+{
+ /**
+ * The name of the person.
+ */
+ attribute AString name;
+
+ /**
+ * An email address associated with the person.
+ */
+ attribute AString email;
+
+ /**
+ * A URI associated with the person (e.g. a homepage).
+ */
+ attribute nsIURI uri;
+};
diff --git a/toolkit/components/feeds/nsIFeedProcessor.idl b/toolkit/components/feeds/nsIFeedProcessor.idl
new file mode 100644
index 0000000000..d7205600d5
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedProcessor.idl
@@ -0,0 +1,57 @@
+/* -*- 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 "nsIStreamListener.idl"
+
+interface nsIURI;
+interface nsIFeedResultListener;
+interface nsIInputStream;
+
+/**
+ * An nsIFeedProcessor parses feeds, triggering callbacks based on
+ * their contents.
+ */
+[scriptable, uuid(8a0b2908-21b0-45d7-b14d-30df0f92afc7)]
+interface nsIFeedProcessor : nsIStreamListener {
+
+ /**
+ * The listener that will respond to feed events.
+ */
+ attribute nsIFeedResultListener listener;
+
+ // Level is where to listen for the extension, a constant: FEED,
+ // ENTRY, BOTH.
+ //
+ // XXX todo void registerExtensionHandler(in
+ // nsIFeedExtensionHandler, in long level);
+
+ /**
+ * Parse a feed from an nsIInputStream.
+ *
+ * @param stream The input stream.
+ * @param uri The base URI.
+ */
+ void parseFromStream(in nsIInputStream stream, in nsIURI uri);
+
+ /**
+ * Parse a feed from a string.
+ *
+ * @param str The string to parse.
+ * @param uri The base URI.
+ */
+ void parseFromString(in AString str, in nsIURI uri);
+
+ /**
+ * Parse a feed asynchronously. The caller must then call the
+ * nsIFeedProcessor's nsIStreamListener methods to drive the
+ * parse. Do not call the other parse methods during an asynchronous
+ * parse.
+ *
+ * @param requestObserver The observer to notify on start/stop. This
+ * argument can be null.
+ * @param uri The base URI.
+ */
+ void parseAsync(in nsIRequestObserver requestObserver, in nsIURI uri);
+};
diff --git a/toolkit/components/feeds/nsIFeedResult.idl b/toolkit/components/feeds/nsIFeedResult.idl
new file mode 100644
index 0000000000..4cfb0a13ea
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedResult.idl
@@ -0,0 +1,65 @@
+/* -*- 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 nsIFeedContainer;
+interface nsIProperties;
+interface nsIURI;
+
+/**
+ * The nsIFeedResult interface provides access to HTTP and parsing
+ * metadata for a feed or entry.
+ */
+[scriptable, uuid(7a180b78-0f46-4569-8c22-f3d720ea1c57)]
+interface nsIFeedResult : nsISupports {
+
+ /**
+ * The Feed parser will set the bozo bit when a feed triggers a fatal
+ * error during XML parsing. There may be entries and feed metadata
+ * that were parsed before the error. Thanks to Tim Bray for
+ * suggesting this terminology.
+ * <http://www.tbray.org/ongoing/When/200x/2004/01/11/PostelPilgrim>
+ */
+ attribute boolean bozo;
+
+ /**
+ * The parsed feed or entry.
+ *
+ * Will be null if a non-feed is processed.
+ */
+ attribute nsIFeedContainer doc;
+
+ /**
+ * The address from which the feed was fetched.
+ */
+ attribute nsIURI uri;
+
+ /**
+ * Feed Version:
+ * atom, rss2, rss09, rss091, rss091userland, rss092, rss1, atom03,
+ * atomEntry, rssItem
+ *
+ * Will be null if a non-feed is processed.
+ */
+ attribute AString version;
+
+ /**
+ * An XSLT stylesheet available to transform the source of the
+ * feed. Some feeds include this information in a processing
+ * instruction. It's generally intended for clients with specific
+ * feed capabilities.
+ */
+ attribute nsIURI stylesheet;
+
+ /**
+ * HTTP response headers that accompanied the feed.
+ */
+ attribute nsIProperties headers;
+
+ /**
+ * Registers a prefix used to access an extension in the feed/entry
+ */
+ void registerExtensionPrefix(in AString aNamespace, in AString aPrefix);
+};
diff --git a/toolkit/components/feeds/nsIFeedTextConstruct.idl b/toolkit/components/feeds/nsIFeedTextConstruct.idl
new file mode 100644
index 0000000000..070acb20a1
--- /dev/null
+++ b/toolkit/components/feeds/nsIFeedTextConstruct.idl
@@ -0,0 +1,57 @@
+/* -*- 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 nsIDOMElement;
+interface nsIDOMDocumentFragment;
+
+/**
+ * nsIFeedTextConstructs represent feed text fields that can contain
+ * one of text, HTML, or XHTML. Some extension elements also have "type"
+ * parameters, and this interface could be used there as well.
+ */
+[scriptable, uuid(fc97a2a9-d649-4494-931e-db81a156c873)]
+interface nsIFeedTextConstruct : nsISupports
+{
+ /**
+ * If the text construct contains (X)HTML, relative references in
+ * the content should be resolved against this base URI.
+ */
+ attribute nsIURI base;
+
+ /**
+ * The language of the text. For example, "en-US" for US English.
+ */
+ attribute AString lang;
+
+ /**
+ * One of "text", "html", or "xhtml". If the type is (x)html, a '<'
+ * character represents markup. To display that character, an escape
+ * such as &lt; must be used. If the type is "text", the '<'
+ * character represents the character itself, and such text should
+ * not be embedded in markup without escaping it first.
+ */
+ attribute AString type;
+
+ /**
+ * The content of the text construct.
+ */
+ attribute AString text;
+
+ /**
+ * Returns the text of the text construct, with all markup stripped
+ * and all entities decoded. If the type attribute's value is "text",
+ * this function returns the value of the text attribute unchanged.
+ */
+ AString plainText();
+
+ /**
+ * Return an nsIDocumentFragment containing the text and markup.
+ */
+ nsIDOMDocumentFragment createDocumentFragment(in nsIDOMElement element);
+};
+
diff --git a/toolkit/components/feeds/test/.eslintrc.js b/toolkit/components/feeds/test/.eslintrc.js
new file mode 100644
index 0000000000..89764b5510
--- /dev/null
+++ b/toolkit/components/feeds/test/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js",
+ "../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/feeds/test/chrome.ini b/toolkit/components/feeds/test/chrome.ini
new file mode 100644
index 0000000000..6745fa9a5e
--- /dev/null
+++ b/toolkit/components/feeds/test/chrome.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_bug675492.xul]
diff --git a/toolkit/components/feeds/test/head.js b/toolkit/components/feeds/test/head.js
new file mode 100644
index 0000000000..65aa64b948
--- /dev/null
+++ b/toolkit/components/feeds/test/head.js
@@ -0,0 +1,80 @@
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+function readTestData(testFile) {
+ var testcase = {};
+
+ // Got a feed file, now we need to parse out the Description and Expect headers.
+ var istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+ try {
+ istream.init(testFile, 0x01, parseInt("0444", 8), 0);
+ istream.QueryInterface(Ci.nsILineInputStream);
+
+ var hasmore = false;
+ do {
+ var line = {};
+ hasmore = istream.readLine(line);
+
+ if (line.value.indexOf('Description:') > -1) {
+ testcase.desc = line.value.substring(line.value.indexOf(':')+1).trim();
+ }
+
+ if (line.value.indexOf('Expect:') > -1) {
+ testcase.expect = line.value.substring(line.value.indexOf(':')+1).trim();
+ }
+
+ if (line.value.indexOf('Base:') > -1) {
+ testcase.base = NetUtil.newURI(line.value.substring(line.value.indexOf(':')+1).trim());
+ }
+
+ if (testcase.expect && testcase.desc) {
+ testcase.path = 'xml/' + testFile.parent.leafName + '/' + testFile.leafName;
+ testcase.file = testFile;
+ break;
+ }
+
+ } while (hasmore);
+
+ } catch (e) {
+ Assert.ok(false, "FAILED! Error reading testFile case in file " + testFile.leafName + " ---- " + e);
+ } finally {
+ istream.close();
+ }
+
+ return testcase;
+}
+
+function iterateDir(dir, recurse, callback) {
+ do_print("Iterate " + dir.leafName);
+ let entries = dir.directoryEntries;
+
+ // Loop over everything in this dir. If its a dir
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext();
+ entry.QueryInterface(Ci.nsILocalFile);
+
+ if (entry.isDirectory()) {
+ if (recurse) {
+ iterateDir(entry, recurse, callback);
+ }
+ } else {
+ callback(entry);
+ }
+ }
+}
+
+function isIID(a, iid) {
+ try {
+ a.QueryInterface(iid);
+ return true;
+ } catch (e) { }
+
+ return false;
+}
diff --git a/toolkit/components/feeds/test/test_bug675492.xul b/toolkit/components/feeds/test/test_bug675492.xul
new file mode 100644
index 0000000000..b1c52d11aa
--- /dev/null
+++ b/toolkit/components/feeds/test/test_bug675492.xul
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=675492
+-->
+<window title="Mozilla Bug 675492"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=675492"
+ target="_blank">Mozilla Bug 675492</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+
+ /** Test for Bug 675492 **/
+
+ Components
+ .classes["@mozilla.org/parserutils;1"]
+ .getService(Components.interfaces.nsIParserUtils)
+ .parseFragment("<p>test</p>", 0, false, null, document.createElementNS("http://www.w3.org/1999/xhtml", "body"));
+ ok(true, "No crash!");
+ ]]>
+ </script>
+</window>
diff --git a/toolkit/components/feeds/test/test_xml.js b/toolkit/components/feeds/test/test_xml.js
new file mode 100644
index 0000000000..5bc0d759d1
--- /dev/null
+++ b/toolkit/components/feeds/test/test_xml.js
@@ -0,0 +1,97 @@
+/* -*- 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/. */
+
+/* test_xml.js
+ * This file sets up the unit test environment by building an array of files
+ * to be tested. It assumes it lives in a folder adjacent to the a folder
+ * called 'xml', where the testcases live.
+ *
+ * The directory layout looks something like this:
+ *
+ * tests/test_xml.js*
+ * |
+ * - head.js
+ * |
+ * - xml/ -- rss1/...
+ * |
+ * -- rss2/...
+ * |
+ * -- atom/testcase.xml
+ *
+ * To add more tests, just include the file in the xml subfolder and add its name to xpcshell.ini
+ */
+
+"use strict";
+
+// Listens to feeds being loaded. Runs the tests built into the feed afterwards to veryify they
+// were parsed correctly.
+function FeedListener(testcase) {
+ this.testcase = testcase;
+}
+
+FeedListener.prototype = {
+ handleResult: function(result) {
+ var feed = result.doc;
+ try {
+ do_print("Testing feed " + this.testcase.file.path);
+ Assert.ok(isIID(feed, Ci.nsIFeed), "Has feed interface");
+
+ if (!eval(this.testcase.expect)) {
+ Assert.ok(false, "expect failed for " + this.testcase.desc);
+ } else {
+ Assert.ok(true, "expect passed for " + this.testcase.desc);
+ }
+ } catch (e) {
+ Assert.ok(false, "expect failed for " + this.testcase.desc + " ---- " + e.message);
+ }
+
+ run_next_test();
+ }
+}
+
+function createTest(data) {
+ return function() {
+ var uri;
+
+ if (data.base == null) {
+ uri = NetUtil.newURI('http://example.org/' + data.path);
+ } else {
+ uri = data.base;
+ }
+
+ do_print("Testing " + data.file.leafName);
+
+ var parser = Cc["@mozilla.org/feed-processor;1"].createInstance(Ci.nsIFeedProcessor);
+ var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+ stream.init(data.file, 0x01, parseInt("0444", 8), 0);
+ parser.listener = new FeedListener(data);
+
+ try {
+ parser.parseFromStream(stream, uri);
+ } catch (e) {
+ Assert.ok(false, "parse failed for " + data.file.leafName + " ---- " + e.message);
+ // If the parser failed, the listener won't be notified, run the next test here.
+ run_next_test();
+ } finally {
+ stream.close();
+ }
+ }
+}
+
+function run_test() {
+ // Get the 'xml' directory in here
+ var topDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ topDir.append("xml");
+
+ // Every file in the test dir contains an encapulated RSS "test". Iterate through
+ // them all and add them to the test runner.
+ iterateDir(topDir, true, file => {
+ var data = readTestData(file);
+ add_test(createTest(data));
+ });
+
+ // Now run!
+ run_next_test();
+}
diff --git a/toolkit/components/feeds/test/xml/rfc4287/author_namespaces.xml b/toolkit/components/feeds/test/xml/rfc4287/author_namespaces.xml
new file mode 100644
index 0000000000..3b2ad74ffc
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/author_namespaces.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Description: Ensure unknown NS element in atom:author doesn't cause exception
+Expect: var mCService = Components.classes['@mozilla.org/consoleservice;1'].getService(Components.interfaces.nsIConsoleService); var msg = mCService.getMessageArray()[0]; if(msg){msg = msg.message}; ((msg + "").indexOf("prefix has no properties") == -1);
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <author>
+ <name>Sam Ruby</name>
+ <method xmlns="http://www.intertwingly.net/blog/">excerpt</method>
+ <email>rubys@intertwingly.net</email>
+ <uri>.</uri>
+ </author>
+
+</feed>
+
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_author.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_author.xml
new file mode 100644
index 0000000000..2757148c4e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_author.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).name=='John Doe Entry';
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ <author>
+ <name>John Doe Entry</name>
+ </author>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_content.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_content.xml
new file mode 100644
index 0000000000..6bb9b210f5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_content.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry summary works
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).content.plainText() == "test content";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <content>test content</content>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_content_encoded.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_content_encoded.xml
new file mode 100644
index 0000000000..df09317f7e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_content_encoded.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry content:encoded and xhtml works
+Expect: var content = feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).content.plainText(); content == "should appear";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux"
+ xmlns:content="http://purl.org/rss/1.0/modules/content/">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <b>test</b> content
+ </div>
+ </summary>
+ <content:encoded>
+ should appear
+ </content:encoded>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_content_html.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_content_html.xml
new file mode 100644
index 0000000000..08974c35f0
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_content_html.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry content html works
+Expect: var content = feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).content.plainText(); content == "test content";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <content type="html">&lt;b>test&lt;/b> content</content>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml.xml
new file mode 100644
index 0000000000..dea4902bb6
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry content xhtml works
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).content.plainText() == "test content";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <b>test</b> content
+ </div>
+ </content>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml_with_markup.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml_with_markup.xml
new file mode 100644
index 0000000000..8cadef75e1
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_content_xhtml_with_markup.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry content xhtml works
+Expect: var content = feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).content.text; content == "<b>test</b> content";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <b>test</b> content
+ </div>
+ </content>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_contributor.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_contributor.xml
new file mode 100644
index 0000000000..bb85bf230b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_contributor.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).contributors.queryElementAt(0, Components.interfaces.nsIFeedPerson).name=='John Doe Entry';
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ <contributor>
+ <name>John Doe Entry</name>
+ </contributor>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_html_cdata.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_html_cdata.xml
new file mode 100644
index 0000000000..38a31ca104
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_html_cdata.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: HTML title w/ CDATA
+Expect: var title = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).title.plainText(); title == "<title>";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<id>http://atomtests.philringnalda.com/tests/item/title/html-cdata.atom</id>
+<title>Atom item title html cdata</title>
+<updated>2005-12-18T00:13:00Z</updated>
+<author>
+ <name>Phil Ringnalda</name>
+ <uri>http://weblog.philringnalda.com/</uri>
+</author>
+<link rel="self" href="http://atomtests.philringnalda.com/tests/item/title/html-cdata.atom"/>
+<entry>
+ <id>http://atomtests.philringnalda.com/tests/item/title/html-cdata.atom/1</id>
+ <title type="html"><![CDATA[&lt;title>]]></title>
+ <updated>2005-12-18T00:13:00Z</updated>
+ <summary>An item with a type="html" title consisting of a less-than
+character, the word 'title' and a greater-than character, where
+the character entity reference for the less-than is escaped by being
+in a CDATA section.</summary>
+ <link href="http://atomtests.philringnalda.com/alt/title-title.html"/>
+ <category term="item title"/>
+</entry>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_id.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_id.xml
new file mode 100644
index 0000000000..8513b6894d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_id.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry id
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).id == "http://foo.example.com/hmm/ok,2006,07,11";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <id>http://foo.example.com/hmm/ok,2006,07,11</id>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts.xml
new file mode 100644
index 0000000000..c7cebe4cd2
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+ <entry>
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/2</id>
+ <title>Two alternate links</title>
+ <updated>2005-01-18T15:00:02Z</updated>
+ <summary>The aggregator should pick either the second or third link below as the alternate</summary>
+
+ <link rel="ALTERNATE" href="http://www.snellspace.com/public/linktests/wrong" />
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link type="text/plain" href="http://www.snellspace.com/public/linktests/alternate2" />
+ <link rel="ALTERNATE" href="http://www.snellspace.com/public/linktests/wrong" />
+ </entry>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore.xml
new file mode 100644
index 0000000000..56675b1a9e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+ <entry>
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/3</id>
+ <title>One of each core link rel type</title>
+
+ <updated>2005-01-18T15:00:03Z</updated>
+ <summary>The aggregator should pick the first link as the alternate</summary>
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link rel="enclosure" href="http://www.snellspace.com/public/linktests/enclosure" length="19" />
+ <link rel="related" href="http://www.snellspace.com/public/linktests/related" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests/self" />
+ <link rel="via" href="http://www.snellspace.com/public/linktests/via" />
+ </entry>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore2.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore2.xml
new file mode 100644
index 0000000000..b5b73cfe02
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_2alts_allcore2.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+
+
+ <entry>
+
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/4</id>
+ <title>One of each core link rel type + An additional alternate link</title>
+ <updated>2005-01-18T15:00:04Z</updated>
+ <summary>The aggregator should pick either the first or last links as the alternate. First link is likely better.</summary>
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link rel="enclosure" href="http://www.snellspace.com/public/linktests/enclosure" length="19" />
+ <link rel="related" href="http://www.snellspace.com/public/linktests/related" />
+
+ <link rel="self" href="http://www.snellspace.com/public/linktests/self" />
+ <link rel="via" href="http://www.snellspace.com/public/linktests/via" />
+ <link rel="alternate" type="text/plain" href="http://www.snellspace.com/public/linktests/alternate2" />
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_IANA.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_IANA.xml
new file mode 100644
index 0000000000..af6563f2ea
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_IANA.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with IANA URI link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+ <entry>
+ <id>tag:example.org,2006:/linkreltest/1</id>
+ <title>Does your reader support http://www.iana.org/assignments/relation/alternate properly? </title>
+ <updated>2006-04-25T12:12:12Z</updated>
+ <link rel="http://example.org/random"
+ href="http://www.snellspace.com/public/random" />
+ <link rel="http://www.iana.org/assignments/relation/alternate"
+ href="http://www.snellspace.com/public/alternate" />
+ <link rel="http://example.org/random"
+ href="http://www.snellspace.com/public/random" />
+ <content>This entry uses link/@rel="http://www.iana.org/assignments/relation/alternate".</content>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_alt_extension.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_alt_extension.xml
new file mode 100644
index 0000000000..e37421864a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_alt_extension.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+ <entry>
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/5</id>
+ <title>Entry with a link relation registered by an extension</title>
+ <updated>2005-01-18T15:00:05Z</updated>
+ <summary>The aggregator should ignore the license link without throwing any errors. The first link should be picked as the alternate.</summary>
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link rel="payment" href="http://www.example.org/payment" />
+ </entry>
+
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure.xml
new file mode 100644
index 0000000000..b76c111c9d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item enclosure works
+Expect: var links = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('links'); links.QueryInterface(Components.interfaces.nsIArray); var link = links.queryElementAt(0, Components.interfaces.nsIPropertyBag2); ((link.getProperty('length') == '24986239') && (link.getProperty('type') == 'audio/mpeg') && (link.getProperty('href') == 'http://dallas.example.com/joebob_050689.mp3') && (feed.type == 1) && (feed.enclosureCount == 1));
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+
+
+ <entry>
+
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/4</id>
+ <title>One of each core link rel type + An additional alternate link</title>
+ <updated>2005-01-18T15:00:04Z</updated>
+ <summary>The aggregator should pick either the first or last links as the alternate. First link is likely better.</summary>
+ <link rel="enclosure" length="24986239" type="audio/mpeg" href="http://dallas.example.com/joebob_050689.mp3" />
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure_populate_enclosures.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure_populate_enclosures.xml
new file mode 100644
index 0000000000..8453c6e9cc
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_enclosure_populate_enclosures.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item enclosure added to enclosures
+Expect: var encs = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).enclosures; encs.QueryInterface(Components.interfaces.nsIArray); var enc = encs.queryElementAt(0, Components.interfaces.nsIPropertyBag2); ((enc.getProperty('length') == '24986239') && (enc.getProperty('type') == 'audio/mpeg') && (enc.getProperty('url') == 'http://dallas.example.com/joebob_050689.mp3'));
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+
+
+ <entry>
+
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/4</id>
+ <title>One of each core link rel type + An additional alternate link</title>
+ <updated>2005-01-18T15:00:04Z</updated>
+ <summary>The aggregator should pick either the first or last links as the alternate. First link is likely better.</summary>
+ <link rel="enclosure" length="24986239" type="audio/mpeg" href="http://dallas.example.com/joebob_050689.mp3" />
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_otherURI_alt.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_otherURI_alt.xml
new file mode 100644
index 0000000000..3dc0c8dd63
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_otherURI_alt.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+ <entry>
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/6</id>
+ <title>Entry with a link relation identified by URI</title>
+ <updated>2005-01-18T15:00:06Z</updated>
+ <summary>The aggregator should ignore the second link without throwing any errors. The first link should be picked as the alternate.</summary>
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link rel="http://example.org" href="http://www.snellspace.com/public/linktests/example" />
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_payment_alt.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_payment_alt.xml
new file mode 100644
index 0000000000..51e1524b4a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_payment_alt.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+
+ <entry>
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/5</id>
+ <title>Entry with a link relation registered by an extension</title>
+ <updated>2005-01-18T15:00:05Z</updated>
+ <summary>The aggregator should ignore the license link without throwing any errors. The first link should be picked as the alternate.</summary>
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link rel="payment" href="http://www.example.org/payment" />
+ </entry>
+
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_link_random.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_link_random.xml
new file mode 100644
index 0000000000..cb370e5168
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_link_random.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with random link relations
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.snellspace.com/public/linktests/alternate";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-01-18T15:10:00Z</updated>
+ <author><name>James Snell</name></author>
+ <link href="http://www.intertwingly.net/wiki/pie/LinkConformanceTests" />
+ <link rel="self" href="http://www.snellspace.com/public/linktests.xml" />
+
+ <entry>
+ <id>tag:snellspace.com,2006:/atom/conformance/linktest/1</id>
+ <title>Just a single Alternate Link</title>
+ <updated>2005-01-18T15:00:01Z</updated>
+ <summary>The aggregator should pick the second link as the alternate</summary>
+ <link rel="http://example.org/random"
+ href="http://www.snellspace.com/public/wrong" />
+ <link href="http://www.snellspace.com/public/linktests/alternate" />
+ <link rel="http://example.org/random"
+ href="http://www.snellspace.com/public/wrong" />
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_published.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_published.xml
new file mode 100644
index 0000000000..1112729e27
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_published.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom published works
+Expect: var entry = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry); entry.published == 'Tue, 09 Dec 2003 18:30:02 GMT'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <published>2003-12-09T18:30:02Z</published>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_rights_normalized.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_rights_normalized.xml
new file mode 100644
index 0000000000..1474e5f84c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_rights_normalized.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry rights works normalized
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).rights.plainText() == "test rights"
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <entry>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> rights</div>
+ </rights>
+ </entry>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_summary.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_summary.xml
new file mode 100644
index 0000000000..b245cb3808
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_summary.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry summary xhtml works
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).summary.plainText() == "test summary";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux"
+ xmlns:content="http://purl.org/rss/1.0/modules/content/">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <b>test</b> summary
+ </div>
+ </summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_title.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_title.xml
new file mode 100644
index 0000000000..c118e74727
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_title.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom feed and entry with random attributes works
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).title.text == "test";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_title_normalized.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_title_normalized.xml
new file mode 100644
index 0000000000..19e2ac1a6f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_title_normalized.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry title normalized
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).title.text == "test";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_updated.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_updated.xml
new file mode 100644
index 0000000000..4aed7e9c70
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_updated.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom updated works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).updated == 'Sat, 13 Dec 2003 18:30:02 GMT'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_w_content_encoded.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_w_content_encoded.xml
new file mode 100644
index 0000000000..58f94c6c1b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_w_content_encoded.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry content:encoded and xhtml works
+Expect: var content = feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).content.text; content == "<b>test</b> content";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux"
+ xmlns:content="http://purl.org/rss/1.0/modules/content/">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+
+ <entry></entry>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <b>test</b> content
+ </div>
+ </content>
+ <content:encoded>
+ shouldn't appear
+ </content:encoded>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_xhtml_baseURI_with_amp.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_xhtml_baseURI_with_amp.xml
new file mode 100644
index 0000000000..20819cecd3
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_xhtml_baseURI_with_amp.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry content xhtml works with a base URI that contains an ampersand.
+Base: http://www.travellerspoint.com/photo_gallery_feed.cfm?tags=Canada&onlyShowFeatured=true
+Expect: var frag = null; var content = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).content; var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"].createInstance(Components.interfaces.nsIDOMParser); var doc = parser.parseFromString("<div/>", "text/xml"); frag = content.createDocumentFragment(doc.documentElement); notEqual(frag, null, "frag is not null"); true;
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+ <entry foo:bar="baz">
+ <title>test</title>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <b>test</b> content
+ </div>
+ </content>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase.xml
new file mode 100644
index 0000000000..1656e21742
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with xml:base
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.example.org/foo";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id>tag:example.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-06-18T6:23:00Z</updated>
+ <link href="http://www.example.org" />
+
+ <entry xml:base="http://www.example.org">
+ <id>tag:example.org,2006:/linkreltest/1</id>
+ <title>Does your reader support xml:base properly? </title>
+ <updated>2006-06-23T12:12:12Z</updated>
+ <link href="foo"/>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase_on_link.xml b/toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase_on_link.xml
new file mode 100644
index 0000000000..0ead142558
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/entry_xmlBase_on_link.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry with xml:base
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == "http://www.example.org/bar/foo";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <id>tag:example.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-06-18T16:13:00Z</updated>
+ <link href="http://www.example.org" />
+
+ <entry xml:base="http://www.example.org">
+ <id>tag:example.org,2006:/linkreltest/1</id>
+ <title>Does your reader support xml:base properly? </title>
+ <updated>2006-06-23T12:12:12Z</updated>
+ <link xml:base="/bar/" href="foo"/>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_atom_rights_xhtml.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_atom_rights_xhtml.xml
new file mode 100644
index 0000000000..83add32651
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_atom_rights_xhtml.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom rights works with HTML
+Expect: feed.fields.getProperty('atom:rights') != null
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><div><div>test</div> summary</div></div>
+ </summary>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author.xml
new file mode 100644
index 0000000000..790027ee3c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author count works
+Expect: feed.authors.length == 1;
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+<author>
+</author>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author2.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author2.xml
new file mode 100644
index 0000000000..eae8292fa5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author2.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author count works
+Expect: feed.authors.length == 2
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+<author>
+</author>
+<author>
+</author>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author_email.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author_email.xml
new file mode 100644
index 0000000000..95abdbc8ef
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author_email.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).email=='hmm@example.com';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+</author>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author_email_2.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author_email_2.xml
new file mode 100644
index 0000000000..71cace7737
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author_email_2.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.authors.queryElementAt(1, Components.interfaces.nsIFeedPerson).email=='bar@example.com';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+</author>
+<author>
+<email>bar@example.com</email>
+<name>foo</name>
+</author>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author_name.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author_name.xml
new file mode 100644
index 0000000000..2df46f8d5e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author_name.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).name=='foo';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+<author>
+<name>foo</name>
+</author>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author_surrounded.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author_surrounded.xml
new file mode 100644
index 0000000000..b15278f91a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author_surrounded.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).name=='John Doe';
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_author_uri.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_author_uri.xml
new file mode 100644
index 0000000000..44149036cb
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_author_uri.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author uri works
+Expect: feed.authors.queryElementAt(1, Components.interfaces.nsIFeedPerson).uri.spec =='http://example.com/';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+<uri>http://example.org</uri>
+</author>
+<author>
+<email>bar@example.com</email>
+<name>foo</name>
+<uri>http://example.com/</uri>
+</author>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_comment_rss_extra_att.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_comment_rss_extra_att.xml
new file mode 100644
index 0000000000..795d3f6ad2
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_comment_rss_extra_att.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: wfw works with extra attribute
+Expect: feed.fields.getProperty('wfw:commentRss') == 'http://example.org'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://example.org"
+ xmlns:bla="http://wellformedweb.org/CommentAPI/">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id foo:bar="baz">urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+ <bla:commentRss hmm="yeah">http://example.org</bla:commentRss>
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_contributor.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_contributor.xml
new file mode 100644
index 0000000000..abb50c154c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_contributor.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom contributor uri works
+Expect: feed.contributors.queryElementAt(1, Components.interfaces.nsIFeedPerson).uri.spec=='http://example.com/';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>test title</title>
+ <contributor>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ <uri>http://example.org</uri>
+ </contributor>
+ <contributor>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ <uri>http://example.com</uri>
+ </contributor>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_entry_count.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_entry_count.xml
new file mode 100644
index 0000000000..53028d3d1d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_entry_count.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom entry count works
+Expect: feed.items.length == 3
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>test title</title>
+ <logo>http://example.org/logo.jpg</logo>
+ <entry></entry>
+ <entry></entry>
+ <entry></entry>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_generator.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_generator.xml
new file mode 100644
index 0000000000..5112775b9b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_generator.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom generator works
+Expect: feed.generator.agent == 'Hmm';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <generator>Hmm</generator>
+
+<author>
+<email>bar@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><div><div>test</div> summary</div></div>
+ </summary>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri.xml
new file mode 100644
index 0000000000..a4b8c735c8
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom generator works
+Expect: feed.generator.uri.spec == 'http://example.org/';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <generator uri="http://example.org">Hmm</generator>
+
+<author>
+<email>bar@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><div><div>test</div> summary</div></div>
+ </summary>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri_xmlbase.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri_xmlbase.xml
new file mode 100644
index 0000000000..54191b50ec
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_generator_uri_xmlbase.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom generator works
+Expect: feed.generator.uri.spec == 'http://example.org/gen/';
+
+-->
+<feed xml:base="http://example.org/" xmlns="http://www.w3.org/2005/Atom">
+
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <generator uri="/gen/">Hmm</generator>
+
+<author>
+<email>bar@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><div><div>test</div> summary</div></div>
+ </summary>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_generator_version.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_generator_version.xml
new file mode 100644
index 0000000000..845ce75040
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_generator_version.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom generator works
+Expect: feed.generator.version == "1.1"
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+<author>
+<email>hmm@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+
+<author>
+<email>bar@example.com</email>
+<name>foo</name>
+</author>
+
+
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+ <summary type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><div><div>test</div> summary</div></div>
+ </summary>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_icon.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_icon.xml
new file mode 100644
index 0000000000..017aafbad1
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_icon.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom icon works
+Expect: feed.fields.getProperty('atom:icon') == 'http://example.org/favicon.ico'
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>test title</title>
+ <icon>http://example.org/favicon.ico</icon>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_id.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_id.xml
new file mode 100644
index 0000000000..6c538e8015
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_id.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom author name works
+Expect: feed.fields.getProperty('atom:id') == 'urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6' && feed.id == 'urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_id_extra_att.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_id_extra_att.xml
new file mode 100644
index 0000000000..ef718463be
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_id_extra_att.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom feed id works with extra attribute
+Expect: feed.fields.getProperty('atom:id') == 'urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://example.org">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id foo:bar="baz">urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_logo.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_logo.xml
new file mode 100644
index 0000000000..2a90fe22b5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_logo.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom logo works
+Expect: feed.image.getProperty('url') == 'http://example.org/logo.jpg'
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>test title</title>
+ <logo xml:base="http://example.org/">logo.jpg</logo>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_random_attributes_on_feed_and_entry.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_random_attributes_on_feed_and_entry.xml
new file mode 100644
index 0000000000..a4c5633e27
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_random_attributes_on_feed_and_entry.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom feed and entry with random attributes works
+Expect: feed.title.text == "hmm" && feed.items.length == 2
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:foo="http://www.example.org"
+ foo:quux="quuux">
+
+ <title>hmm</title>
+
+ <author>
+ <email>hmm@example.com</email>
+ <name>foo</name>
+ </author>
+ <generator version="1.1" uri="http://example.org">Hmm</generator>
+ <author>
+ <email>bar@example.com</email>
+ <name>foo</name>
+ </author>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><i>test</i> rights</div>
+ </rights>
+
+ <entry foo:bar="baz"></entry>
+ <entry></entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_rights_normalized.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_rights_normalized.xml
new file mode 100644
index 0000000000..b7ef460478
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_rights_normalized.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom rights works normalized
+Expect: feed.rights.plainText() == "test rights"
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> rights</div>
+ </rights>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml.xml
new file mode 100644
index 0000000000..422c6fb492
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom rights works
+Expect: feed.fields.getProperty('atom:rights') != null
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> rights</div>
+ </rights>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml_nested_divs.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml_nested_divs.xml
new file mode 100644
index 0000000000..ebad24ac3b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_rights_xhtml_nested_divs.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom rights works with nested divs
+Expect: feed.fields.getProperty('atom:rights') != null
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+ <rights type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><div><div>test</div> rights</div></div>
+ </rights>
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_subtitle.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_subtitle.xml
new file mode 100644
index 0000000000..096061399e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_subtitle.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom subtitle works
+Expect: var sub = feed.subtitle.text; sub == '<b>test</b> subtitle';
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <subtitle type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> subtitle</div>
+ </subtitle>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_tantek_title.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_tantek_title.xml
new file mode 100644
index 0000000000..cba9a4918f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_tantek_title.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Description: XHTML title with apos
+Expect: feed.title.plainText() == "Tantek's Updates"
+-->
+<feed xml:lang="en-US"
+ xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">Tantek's Updates</div>
+ </title>
+ <link href="http://tantek.com/"
+ rel="alternate" title="Tantek's Posts" type="text/html"/>
+ <link href="http://tantek.com/updates.atom"
+ rel="self" />
+ <id>http://tantek.com/updates.atom</id>
+ <author>
+ <name>Tantek</name>
+ <uri>http://tantek.com/</uri>
+ </author>
+ <updated>2006-05-02T20:13:00-07:00</updated>
+ <entry>
+ <updated>2006-04-22T00:00:00-07:00</updated>
+ <published>2006-04-22T00:00:00-07:00</published>
+ <link href="http://www.makezine.com/faire/"
+ rel="alternate" title="Make Faire" type="text/html"/>
+ <id>http://www.makezine.com/faire/</id>
+ <title>Make Faire</title>
+ <content type="xhtml" xml:space="preserve">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <div class="vevent">
+ <a class="url" href="http://www.makezine.com/faire/">
+ <abbr class="dtstart" title="20060422">
+ 4/22</abbr>-<abbr class="dtend" title="20060424">23</abbr>
+
+ <span class="summary">
+ Make Faire
+ </span> @
+ <span class="location">
+ San Mateo Fairgrounds
+ </span>
+ </a>
+ </div>
+ </div>
+ </content>
+ </entry>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_title.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_title.xml
new file mode 100644
index 0000000000..6e0cea0039
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_title.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom title works
+Expect: feed.title.text == 'test title'
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>test title</title>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_title_full_feed.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_title_full_feed.xml
new file mode 100644
index 0000000000..cef3f84a3b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_title_full_feed.xml
@@ -0,0 +1,936 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!--
+Description: Feed title works with full entry
+Expect: feed.title.text == 'ongoing'
+-->
+<feed xmlns='http://www.w3.org/2005/Atom'
+ xml:base='http://www.tbray.org/ongoing/ongoing.atom'
+ xml:lang='en-us'>
+ <title>ongoing</title>
+ <id>http://www.tbray.org/ongoing/</id>
+ <link href='./' />
+ <link rel='self' href='' />
+ <logo>rsslogo.jpg</logo>
+ <icon>/favicon.ico</icon>
+ <updated>2006-04-26T20:10:25-08:00</updated>
+ <author><name>Tim Bray</name></author>
+ <subtitle>ongoing fragmented essay by Tim Bray</subtitle>
+ <rights>All content written by Tim Bray and photos by Tim Bray Copyright Tim Bray, some rights reserved, see /ongoing/misc/Copyright</rights>
+ <generator uri='/misc/Colophon'>Generated from XML source code using Perl, Expat, XML::Parser, Emacs, Mysql, and ImageMagick. Industrial strength technology, baby.</generator>
+
+<entry xml:base='When/200x/2006/04/26/'>
+ <title>Spring in White on White</title>
+ <link href='Spring-in-White-on-White' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/26/Spring-in-White-on-White</id>
+ <published>2006-04-26T13:00:00-08:00</published>
+ <updated>2006-04-26T20:10:16-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts/Photos' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Photos' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Garden' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Garden' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>Most people would generally prefer a climate where it&#x2019;s bright and warm most of the time. But for Canadians and others who live where it&#x2019;s not, there are compensations, and one is the experience of spring. I have a picture.</div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Most people would generally prefer a climate where it’s bright
+and warm most of the time. But for Canadians and others who live where it’s
+not, there are compensations, and one is the experience of
+spring. I have a picture.</p>
+<img src="IMGP3247.png" alt="Pear blossoms against cherry blossoms" />
+<div class="caption"><p>The blossoms are pear in the foreground, cherry behind.</p></div>
+<p>After all the months of 50° North Latitude winter—icy-sharp in most
+of Canada, wet and dark here in Vancouver—the soul, the spirit, and the
+libido all spring to life when the sun comes back. We’ve had a solid year of
+crappy weather, but this last Saturday through Monday were solidly summery,
+bright
+and warm; and in this season the days are already long and each gets
+longer so fast you can feel it.</p>
+<p>On the back porch, our pear tree’s branches were silhouetted against the
+neighbors’ big wild old cherry; the cherry yields no edible fruit but who
+cares, it’s beautiful
+tree any time of year.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/26/'>
+ <title>Scott</title>
+ <link href='Scott' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/26/Scott</id>
+ <published>2006-04-26T13:00:00-08:00</published>
+ <updated>2006-04-26T20:06:50-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Business/Sun' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Business' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Sun' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>I’ve been watching our internal leadership conference and spending quite a
+bit of time talking in the virtual hallways, and I’ve been surprised at
+the intensity of feeling about Mr. McNealy. Yes, there are those
+here saying “About bloody time, now we can make some progress” but there’s a
+much bigger group that is genuinely emotional about this transition.
+Maybe it’s a function of seniority: I never met nor corresponded with Scott, and
+he hasn’t been
+much of a presence in the company’s conversation in the time I’ve been here.
+But there are a lot of smart, seasoned, unsentimental people making it clear
+that
+he’s been a major force in their lives, at a more personal level than I’m
+used to hearing when people speak about executives. I guess also that to a
+lot of people, Sun’s vision, for which Scott gets some of the credit, was a
+radical and wonderful thing. I first used Unix in 1979 and quit a nice
+big-company job
+to become a VAX-bsd sysadmin in 1983, so I’ve always kind of
+lived inside that vision.
+But I’ll tell you one thing, what I’ve been hearing the last couple of days
+makes me really regret that I didn’t get to know Scott.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/26/'>
+ <title>Jacobs, Pictures, Spartans</title>
+ <link href='Jane-Jacobs' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/26/Jane-Jacobs</id>
+ <published>2006-04-26T13:00:00-08:00</published>
+ <updated>2006-04-26T17:28:59-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts/Photos' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Photos' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p><a href="http://en.wikipedia.org/wiki/Jane_Jacobs">Jane Jacobs</a> died;
+the city I live in, Vancouver, is pretty solidly Jacobsian both in its current
+shape and its planning dogma. By choosing to live here I’m empirically a
+fan. Oddly, few have remarked how great Jacobs
+<em>looked</em>; her face commanded the eye. Which leads me Alex
+Waterhouse-Hayward’s wonderful
+<a href="http://www.alexwaterhousehayward.com/blog/2006/04/jane-jacobs-viveca-lindfors_26.html">Jane Jacobs &amp; Viveca Lindfors</a>;
+surprising portraits and thoughts on decoration. W-H’s blog has become one of
+only two or three that I
+stab at excitedly whenever I see something new. For example, see
+<a href="http://www.alexwaterhousehayward.com/blog/archives/2006_04_01_archive.html#114476408248660848">Sex Crimes, Homicide and Drugs</a>
+and yes, that’s what it’s about.
+Staying with the death-and-betrayal theme, and apparently (but not really)
+shifting back 2&#xbd; millennia, see John Cowan’s
+<a href="http://recycledknowledge.blogspot.com/2006/04/war-after-simonides.html">The
+War (after Simonides)</a>, being careful to look closely at the links.
+I’ve
+<a href="http://www.tbray.org/ongoing/When/200x/2003/03/24/Herodotus">written</a>
+about those same wars.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/25/'>
+ <title>LAMP and MARS</title>
+ <link href='Scaling-Rails' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/25/Scaling-Rails</id>
+ <published>2006-04-25T13:00:00-08:00</published>
+ <updated>2006-04-26T07:24:06-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Web' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Web' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Sun' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Sun' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>At
+<a href="/ongoing/When/200x/2006/04/13/RoR">that Rails conference</a>, when I
+was
+<a href="http://blog.garbledygook.com/2006/04/17/ruby-on-rails-podcast-tim-bray-ruby-on-rails-podcast/">talking</a>
+to
+<a href="http://jroller.com/page/obie">Obie Fernandez</a>, he asked, more or
+less “How can Sun love us? We’re not Java” and I said, more or less, “Hey,
+you’re programmers, you write software and there have to be computers to run
+it, we sell computers, why wouldn’t we love you?” Anyhow, we touched on
+parallelism a bit and I talked up the
+<a href="http://www.sun.com/processors/UltraSPARC-T1/">T1</a>;
+Obie took that ball and
+<a href="http://jroller.com/page/obie?entry=will_ultrasparc_t1_emerge_as">ran with it</a>,
+saying all sorts of positive things about synergy between Rails’
+shared-nothing architecture and our multicore systems. Yeah, well, good in
+theory, but I’m too old to make that kind of prediction without running some
+tests. Hah, it turns out that
+<a href="http://joyent.com/">Joyent</a> has been
+<a href="http://scalewithrails.com/">doing that</a>, and have
+<a href="http://scalewithrails.com/downloads/ScaleWithRails-April2006.pdf">76
+PDF slides</a> on the subject.
+If you care about big-system scaling issues, read the whole thing; a little
+long, but amusing and with hardly any bullet lists. If you’re a Sun
+shareholder looking for a pick-me up, check out slides 40-41, 49, and 52-74.
+Oh, I gather that the T1, Solaris, and ZFS are OK for Java too.
+<i>[Update: The title was just “SAMR”, as in LAMP with two new letters.
+Enough people didn’t get it that I was forced to think about it, and MARS
+works better anyhow.]</i></p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/25/'>
+ <title>Real-Time Journalism</title>
+ <link href='Talk-With-Berlind' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/25/Talk-With-Berlind</id>
+ <published>2006-04-25T13:00:00-08:00</published>
+ <updated>2006-04-26T06:40:19-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Journalism' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Journalism' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Syndication' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Syndication' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>I got email late yesterday from
+<a href="http://blogs.zdnet.com/bio.php#berlind">David Berlind</a>: “Hey, can
+I call you for a minute?” He wanted commentary on
+<a href="http://blogs.zdnet.com/BTL/?p=2906">a story he was writing</a> that I
+think is about the potential for intellectual-property lock-ins on RSS and Atom
+extensions. I say “I think is about” because the headline is “Will or could
+RSS get forked?”. After a few minutes’ chat, David asked if he could record
+for a podcast, and even though I only had a cellphone, the audio came out OK.
+The conversation was rhythmic: David brought up a succession of potential
+issues and answered each along the lines of “Yes, it’s reasonable to worry
+about that, but in this
+case I don’t see any particular problems.”
+Plus I emitted a mercifully-brief rant on the difference between protocols,
+data, and software.
+On the one hand, I thought David could have been a
+little clearer that I was pushing back against the thrust of his story, but on
+the other hand he included the whole conversation right
+there in the piece, so anyone who actually cares can listen and find out what
+I actually said, not what I think I said nor what David reported I said.
+I find this raw barely-intermediated journalism (we
+talk on the phone this afternoon, it’s on the Web in hours) a little
+shocking still.
+On balance, it’s better than the way we used to do things.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/24/'>
+ <title>The Transition Explained</title>
+ <link href='CEO-Transition' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/24/CEO-Transition</id>
+ <published>2006-04-24T13:00:00-08:00</published>
+ <updated>2006-04-24T16:49:05-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Business/Sun' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Business' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Sun' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>It’s not that complicated, really.
+Bloggers are
+<a href="http://www.sun.com/2006-0418/js/index.jsp">taking over the world</a>.
+Resistance is futile; you will be assimilated.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/24/'>
+ <title>5&#x272d;&#x266b;: One More Cup of Coffee</title>
+ <link href='One-More-Cup-Of-Coffee' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/24/One-More-Cup-Of-Coffee</id>
+ <published>2006-04-24T13:00:00-08:00</published>
+ <updated>2006-04-24T13:00:00-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts/Music/Recordings' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Music' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Recordings' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts/Music/5 Stars' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='5 Stars' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>I&#x2019;m not really a <a href='http://en.wikipedia.org/wiki/Bob_Dylan'>Bob Dylan</a> fan. A voice like that, and a tunesmithing talent like that, come along only a few times per century, but he&#x2019;s still kind of irritating. That aside, the song <cite>One More Cup of Coffee</cite>, from the 1976 album <a href='http://en.wikipedia.org/wiki/Desire_%28album%29'>Desire</a>, can&#x2019;t be ignored; wonderful tune, wonderful orchestration, wonderful performance. <i>(&#x201c;5&#x272d;&#x266b;&#x201d; series introduction <a href='/ongoing/When/200x/2006/01/23/5-Star-Music'>here</a>; with <a href='/ongoing/When/200x/2006/01/23/5-Star-Music#p-1'>an explanation</a> of why the title may look broken.)</i></div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>I’m not really a
+<a href="http://en.wikipedia.org/wiki/Bob_Dylan">Bob Dylan</a> fan. A voice
+like that, and a tunesmithing talent like that, come along only a few times
+per century, but he’s still kind of irritating.
+That aside, the song <cite>One More Cup of Coffee</cite>, from the 1976 album
+<a href="http://en.wikipedia.org/wiki/Desire_%28album%29">Desire</a>, can’t be
+ignored; wonderful tune, wonderful orchestration, wonderful performance.
+<i>(“5✭♫” series introduction <a href="/ongoing/When/200x/2006/01/23/5-Star-Music">here</a>;
+with <a href="/ongoing/When/200x/2006/01/23/5-Star-Music#p-1">an
+explanation</a> of why the title may look broken.)</i></p>
+<img src="Desire.png" class="inline" alt="Desire, by Bob Dylan" />
+<h2 id='p-1'>The Context</h2>
+<p>Nothing I can possibly write will add any wisdom to the
+millions of words, some 90% of them in excess of needs, written on the subject
+of this particular person.</p>
+<p>A personal statement: Bob Dylan has long irritated me for, during the first
+thirty years or
+so of his career, never having given a straight answer to a straight question,
+and for writing songs with dozens of boring verses. But they’ll still be
+listening
+to lots of his performances long after I’m dead, and in recent years he’s
+become a better, more direct, interview.</p>
+<p>My taste in Dylan is a little unusual: once you get past <cite>One More Cup
+of Coffee</cite>, my favorites would be <cite>Baby Let Me Follow You
+Down</cite> (from the <cite>Last Waltz</cite> soundtrack) and
+<cite>Crash on the Levee (Down in the Flood)</cite> from
+<a href="http://en.wikipedia.org/wiki/The_Basement_Tapes">The Basement
+Tapes</a>.</p>
+<p><cite>Desire</cite>, the record, is hit and miss. <cite>Joey</cite>,
+glorification of the life of some mafioso, is flawed in concept
+and unlistenable in execution. <cite>Hurricane</cite>, whatever you think
+about
+<a href="http://en.wikipedia.org/wiki/Rubin_Carter">Mr. Carter</a>, that song
+rocks; and <cite>Isis</cite> hits pretty hard too.</p>
+<h2 id='p-2'>The Music</h2>
+<p>Is there anything in <cite>One More Cup of Coffee</cite> that’s not
+perfect? Well yes, in the verses, the
+lyrics on occasion drag (“He oversees his kingdom / So no stranger does
+intrude / His voice it trembles as he calls out / For another plate of food”).
+But apart from that, the sentiment is compelling,
+<a href="http://en.wikipedia.org/wiki/Scarlet_Rivera">Scarlet Rivera’s</a>
+violin is beautifully scored and played, the tune is to die for, and the
+backing vocals are by Emmylou Harris, who you can bet is going to be here in
+the 5-✭ series one of these days.
+And while there’s not much middle ground on the subject of Dylan’s singing, if
+you like it, you’ll <em>really</em> like this song.</p>
+<p>Listen to the choruses: Bob and Emmylou veer wildly around the rhythm, then
+coalesce on the beat when it matters, and they’re making it
+up as they go along, they’re wholly inhabiting the moment, and it’s
+quite, quite perfect.</p>
+<h2 id='p-3'>Sampling It</h2>
+<p>Oh yeah, it’s out there. And there’s a live version too; but the smart
+thing would be to go buy the un-compressed un-DRM’ed shiny round silver
+version of <cite>Desire</cite>; it’s a keeper.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/24/'>
+ <title>Atomic Monday</title>
+ <link href='Atomic-Monday' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/24/Atomic-Monday</id>
+ <published>2006-04-24T13:00:00-08:00</published>
+ <updated>2006-04-24T00:44:06-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Syndication' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Syndication' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Atom' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Atom' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>First of all, implementors of anything Atom-related need to spend some time
+<a href="http://golem.ph.utexas.edu/~distler/blog/archives/000793.html">chez
+Jacques Distler</a>; in particular, the conversation that plays out in the
+comments. Second, there’s this piece of software called
+<a href="http://www.planetplanet.org/">Planet Planet</a> that allows you to
+make an aggregate web page by reading lots of feeds; for example, see
+<a href="http://www.planetapache.org/">Planet Apache</a> or
+<a href="http://planetsun.org/">Planet Sun</a>.
+Sam Ruby decided that its Atom support needed some work, so
+<a href="http://www.intertwingly.net/blog/2006/04/23/Adding-Atom-support-to-PlanetPlanet">he did
+it</a>. Now, here’s the exciting part: he pinged me over the weekend and said
+“Hey, look at this” wanting to show me his cleverly-Atomized
+Planet Intertwingly feed.
+I looked at it in
+<a href="http://ranchero.com/netnewswire/">NetNewsWire</a> and was puzzled for
+a moment; some but not all of the
+things in the feed were highlighted as unread, even though this was the first
+time I’d seen it. Then the light went on.
+This
+is Atom doing <em>exactly what we went to all that trouble to make it do</em>.
+NetNewsWire has good Atom support and, because Atom entries all have unique
+IDs and timestamps, it can
+tell that it’s seen lots of those entries before in other feeds that I
+subscribe to. That’s how I found Jacques’ piece. This is huge; anyone who
+uses synthetic or aggregated feeds knows that dupes are a big problem, showing
+up all over the place.
+No longer, Atom makes that problem go away.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/22/'>
+ <title>Hyatt on the High-Res Web</title>
+ <link href='High-Res-Web' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/22/High-Res-Web</id>
+ <published>2006-04-22T13:00:00-08:00</published>
+ <updated>2006-04-23T17:12:18-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Web' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Web' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Presentation' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Presentation' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Check out Dave Hyatt’s
+<a href="http://webkit.opendarwin.org/blog/?p=55">excellent write-up</a> on
+designing and rendering Web pages so they take advantage of the
+higher-resolution screens that <em>may</em> be coming our way.
+I emphasize “may” because I’ve seen how slowly we’ve picked up pixels over
+the years. The first really substantial screen I ever worked on was a
+1988-vintage Sun workstation with about a million pixels. The Mac on my
+lap right now, which has 125 times as much memory as that workstation, has
+only 1.38 million pixels.
+Anyhow, Hyatt has some smart things to say on the issues,
+which are trickier than you might think. I suspect that sometime in a couple of
+years, if I still care about <span class='o'>ongoing</span>, I’m going to
+have to go back and reprocess all the images so that higher-res versions are
+available for those who have the screens and don’t mind downloading bigger
+files.
+Anyhow, Dave’s piece may be slightly misleading in that he talks about SVG
+as though
+it’s something coming in the future. Not so, check out
+<a href="http://zcorpan.1go.dk/sandbox/svg/atom/.xml">this nifty SVG Atom
+logo</a>, which works fine in all the Mozilla browsers I have here.
+Load it up, resize the window, and watch what happens. Then do a “view
+source”.
+<i>[Update:
+<a href="http://blog.codedread.com">Jeff Schiller</a> writes to tell me that
+Opera 9 does SVG (and Opera 8 “SVG Tiny”) too.]
+[<a href="http://www.freeke.org/ffg">Dave Walker</a> writes: Though the shipping version of Safari doesn’t support SVG,
+<a href="http://nightly.webkit.org/builds/Latest-WebKit-SVN.dmg">the
+nightlies</a> do.]
+[<a href="http://www.davelemen.com/archives/2006/04/is_it_time_for_jpeg_2000_to_go_mainstream.html">Dave Lemen</a>
+points to
+<a href="http://en.wikipedia.org/wiki/JPEG-2000">JPEG 2000</a> as possibly
+useful in a high-res context.]</i></p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/23/'>
+ <title>Wrong About the Infield Fly Rule</title>
+ <link href='Wrong-About-the-Infield-Fly-Rule' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/23/Wrong-About-the-Infield-Fly-Rule</id>
+ <published>2006-04-23T13:00:00-08:00</published>
+ <updated>2006-04-23T15:02:41-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Family' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Family' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>My brother
+<a href="http://takingalongview.blogspot.com/">Rob</a> is really taking to
+this blogging medium. Check out his recent
+<a href="http://takingalongview.blogspot.com/2006/04/credo.html">Credo</a>,
+and also the only instance I’ve seen of
+<a href="http://takingalongview.blogspot.com/2006/04/ode-to-96-chevy-lumina.html">Anglo-Saxon alliterative poetry</a>
+applied to a mini-van.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2004/12/12/'>
+ <title>Statistics</title>
+ <link href='BMS' />
+ <id>http://www.tbray.org/ongoing/When/200x/2004/12/12/BMS</id>
+ <published>2004-12-12T12:00:00-08:00</published>
+ <updated>2006-04-23T10:10:02-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Publishing' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Publishing' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Web' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Web' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>Almost every Sunday I grab the week&#x2019;s <span class="o">ongoing</span> logfiles and update my numbers. I find it interesting and maybe others will too, so this entry is now the charts&#x2019; permanent home. I&#x2019;ll update it most weeks, probably. <i>[Updated: 2006/04/23.]</i></div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Almost every Sunday I grab the week’s <span class='o'>ongoing</span>
+logfiles and update my numbers.
+I find it interesting
+and maybe others will too, so
+this entry is now the charts’ permanent home. I’ll update it most weeks,
+probably.
+<i>[Updated: 2006/04/23.]</i></p>
+<img src="Browser-Market-Share.png" alt="Browser market shares at ‘ongoing’" />
+<div class="caption"><p>Browsers visiting <span class='o'>ongoing</span>,
+percent.</p></div>
+<img src="Browsers-via-search.png" alt="Browser market shares at ‘ongoing’, visitors via search engines" />
+<div class="caption"><p>Browsers visiting <span class='o'>ongoing</span> via
+search engines, percent.</p></div>
+<img src="Search-Engines.png" alt="Search engine market shares at ‘ongoing’" />
+<div class="caption"><p>Search referrals to <span
+class='o'>ongoing</span> .</p></div>
+<img src="Feeds.png" alt="RSS and Atom feed fetches" />
+<div class="caption"><p>Fetches of the RSS 2.0 and Atom 1.0 feeds.</p></div>
+<p>The notes on usage and source code will return in coming weeks when I get
+the cycles to rewrite this whole article.</p>
+<h2 id='p-1'>What a “Hit” Means</h2>
+<p>I recently
+<a href="/ongoing/When/200x/2006/02/07/Thumbnail">updated</a> the
+<a href="/ongoing/misc/Colophon"><span class='o'>ongoing</span> software</a>
+(but haven’t updated the Colophon I see, oops).
+Anyhow, the <code>XMLHttpRequest</code> now issued by each page seems to be a
+pretty reliable counter of the number of actual browsers with humans behind
+them reading the pages. I checked against
+<a href="/ongoing/When/200x/2005/12/04/Google-Analytics">Google Analytics</a>
+and the numbers agreed to within a dozen or two on days with 5,000 to 10,000
+page views; interestingly, Google Analytics was always 10 or 20 views
+higher.</p>
+<p>Anyhow, do <em>not</em> conclude that now I know how many people are
+reading whatever it is I write here; because I publish lots of short pieces
+that are all there in my RSS feed, and anyone reading my Atom feed gets the
+full content of everything.
+I and I have <em>no #&amp;*!$ idea</em> how many people look at my feeds.</p>
+<p>By the way, this was the first time in weeks and weeks that I’d looked at the
+Analytics numbers, and they showed almost exactly zero change from the report
+linked above. So I’m going to turn them off; they’re a little too intrusive
+and I think may be slowing page loads.</p>
+<p>Anyhow, I ran some detailed statistics on the traffic for Wednesday,
+February 8th, 2006.</p>
+<table cellspacing="3" cellpadding="2" class="wltable">
+<tr valign="top"><td>Total connections to the server</td><td align="right">180,428</td></tr>
+<tr valign="top"><td>Total successful GET transactions</td><td align="right">155,507</td></tr>
+<tr valign="top"><td>Total fetches of the RSS and Atom feeds</td><td align="right">88,450</td></tr>
+<tr valign="top"><td>Total GET transactions that actually fetched data (i.e. status code
+200 as opposed to 304)</td><td align="right">87,271</td></tr>
+<tr valign="top"><td>Total GETs of actual ongoing pages (i.e. not CSS, js, or
+images)</td><td align="right">18,444</td></tr>
+<tr valign="top"><td>Actual human page-views</td><td align="right">6,348</td>
+</tr>
+</table>
+<p>So, there you have it. Doing a bit of rounding, if you take the 180K
+transactions and subtract the 90K feed fetches and the 6000 actual human page
+views, you’re left with 84,000 or so “Web overhead” transactions, mostly
+stylesheets and graphics and so on.
+For every human who viewed a page, it was fetched almost twice again by
+various kinds of robots and non-browser automated agents.</p>
+<p>It’s amazing that the whole thing works at all.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/18/'>
+ <title>XML Automaton</title>
+ <link href='XML-Grammar' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/18/XML-Grammar</id>
+ <published>2006-04-18T13:00:00-08:00</published>
+ <updated>2006-04-23T08:25:56-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/XML' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='XML' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Coding' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Coding' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>In December of 1996 I released a piece of software called <a href='http://www.textuality.com/Lark/'>Lark</a>, which was the world&#x2019;s first <a href='http://www.w3.org/TR/REC-xml/#dt-xml-proc'>XML Processor</a> (as the term is defined in the <a href='http://www.w3.org/TR/REC-xml/'>XML Specification</a>). It was successful, but I stopped maintaining it in 1998 because lots of other smart people, and some big companies like Microsoft, were shipping perfectly good processors. I never <em>quite</em> open-sourced it, holding back one clever bit in the moronic idea that I could make money out of Lark somehow. The magic sauce is a finite state machine that can be used to parse XML 1.0. Recently, someone out there needed one of those, so I thought I&#x2019;d publish it, with some commentary on Lark&#x2019;s construction and an amusing anecdote about the name. I doubt there are more than twelve people on the planet who care about this kind of parsing arcana. <i>[Rick Jelliffe <a href='http://www.oreillynet.com/xml/blog/2006/04/xml_in_xml.html'>has upgraded</a> the machine].</i></div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>In December of 1996 I released a piece of software called
+<a href="http://www.textuality.com/Lark/">Lark</a>, which was
+the world’s first
+<a href="http://www.w3.org/TR/REC-xml/#dt-xml-proc">XML Processor</a> (as the
+term is defined in the
+<a href="http://www.w3.org/TR/REC-xml/">XML Specification</a>).
+It was successful, but I stopped maintaining it in 1998 because lots of other
+smart people, and some big companies like Microsoft, were shipping perfectly
+good processors. I never <em>quite</em> open-sourced it, holding back one
+clever bit in the moronic idea that I could make money out of Lark somehow.
+The magic sauce is a finite state machine that can be used to parse XML 1.0.
+Recently, someone out there needed one of those, so I thought I’d publish
+it, with some commentary on Lark’s construction and an amusing anecdote about
+the name.
+I doubt there are more than twelve people on the planet who care about
+this kind of parsing arcana.
+<i>[Rick Jelliffe
+<a href="http://www.oreillynet.com/xml/blog/2006/04/xml_in_xml.html">has
+upgraded</a> the machine].</i></p>
+<h2 id='p-1'>Why “Lark”?</h2>
+<p><a href="http://www.laurenwood.org/anyway/">Lauren</a> and I went to
+Australia in late 1996 to visit her mother and to get married, which we
+did on November 30th. Forty-eight hours later, Lauren twisted her knee
+badly enough that she was pretty well
+confined to a sofa for the rest of our Australian vacation.</p>
+<p>So I broke out my computer and finished the work I’d already started on my
+XML processor, and decided to call it Lark for <b>La</b>uren’s <b>R</b>ight
+<b>K</b>nee.</p>
+<h2 id='p-2'>How Lark Worked</h2>
+<p>Lark was a pure
+<a href="http://en.wikipedia.org/wiki/Deterministic_finite_state_machine">deterministic
+finite automaton</a> (DFA)
+parser, with a little teeny state stack.
+Some of its transitions were labeled with named “events” that would provoke
+the parser to do something if, for example, it had just recognized a start tag
+or whatever.</p>
+<p>DFA-driven parsers are a common enough design pattern, although I think
+Lark is the only example in the XML space.
+There are well-known parser generators such as
+<a href="http://en.wikipedia.org/wiki/Yacc">yacc</a>,
+<a href="http://en.wikipedia.org/wiki/GNU_bison">GNU bison</a>, and
+<a href="https://javacc.dev.java.net/">javacc</a>,
+usually used in combination with lexical scanners such as
+<a href="http://en.wikipedia.org/wiki/Flex_lexical_analyser">flex</a> so that
+you can write your grammar in terms of tokens not characters.
+Also, they handle LALR langauges, so the parsing technique is quite a bit
+richer than a pure state machine.</p>
+<p>I thought I had a better idea. The grammar of XML is simple
+enough, and the syntax characters few enough, that I thought I could just
+write down the state machine by hand.
+So that’s what I did, inventing a special-purpose DFA-description
+language for the purpose.</p>
+<p>Then I had a file called <code>Lark.jin</code> which was really a Java
+program that used the state machine to parse XML. The transition “events”
+in the machine were mapped to <code>case</code> labels in a huge
+<code>switch</code> construct. Then there was a horrible, <em>horrible</em>
+Perl program that read the <code>Lark.jin</code> and the automaton,
+generated the DFA tables in Java syntax, inserted them into the code and
+produced <code>Lark.java</code>, which you actually compiled
+to make the parser.</p>
+<p>So while Java doesn’t have a preprocessor, Lark did, which made quite a few
+things easier.</p>
+<p>There were a lot of tricks; some of the state transitions
+weren’t on characters, they were on XML character classes such as
+<code>NameChar</code> and so on.
+This made the automaton easier to write, and in fact, to keep the class files
+small, the character-class transitions persisted into the Java form, and the
+real DFA was built at startup time.
+These days, quick startup might be more important than <code>.class</code>
+file size.</p>
+<h2 id='p-3'>What Was Good</h2>
+<p>It was <em>damn</em> fast. James Clark managed to hand-craft a
+Java-language XML parser called
+<a href="http://jclark.com/xml/xp/index.html">XP</a> that was a little faster
+than Lark, but he did that by clever I/O buffering, and I was determined to
+leapfrog him by improving my I/O.</p>
+<p>This was before the time of standardized XML APIs, but Lark had a stream API
+that influenced SAX, and a DOM-like tree API; both worked just fine.
+Lark is one of very few parsers ever to have survived the
+<a href="http://www.securityfocus.com/archive/1/303509/2002-12-13/2002-12-19/0">billion
+laughs attack</a>.</p>
+<p>Lark was put into production in quite a few deployments, and the flow of
+bug reports slowed to a trickle.
+Then in 1998 I noticed that IBM and Microsoft and BEA and everyone else
+were building XML Processors, so I decided that it wasn’t worthwhile
+maintaining mine.</p>
+<h2 id='p-4'>What Was Bad</h2>
+<p>I never got around to teaching it namespaces, which means it wouldn’t be
+real useful today.</p>
+<p>It had one serious bug that would have been real work to fix and since
+nobody ever encountered it in practice, I kept putting it off and never did.
+If you had an internal parsed entity reference in an attribute value and the
+replacement text included the attribute delimiter (<code>'</code> or
+<code>"</code>), it would scream and claim you had a busted XML document.</p>
+<h2 id='p-5'>That Automaton</h2>
+<p>What happened was,
+<a href="http://www.oreillynet.com/pub/au/1712">Rick Jelliffe</a>, who is a
+Good Person, was
+<a href="http://www.stylusstudio.com/xmldev/200604/post30110.html">looking for
+a FSM for XML</a> and I eventually noticed, and so I sent him mine.</p>
+<p>There’s no reason whatsoever to keep it a secret:
+<a href="/ongoing/code/lark/com/textuality/autom.txt">here it is</a>.
+Be warned: it’s ugly.</p>
+<p>Fortunately, there were only 227 states and 8732 transitions, so the state
+number fit into a
+byte; that and the associated event index pack into a short.
+To make things even tighter, the transitions were only keyed by characters up
+to 127, as in 7-bit ASCII.
+Characters higher than that can’t be XML syntax characters, so we’re only
+interested whether they fall into classes like <code>NameChar</code> and
+<code>NameStartChar</code> and so on. A 64K <code>byte[]</code> array takes
+care of that, each byte having a class bitmask.</p>
+<p>As a result of all this jiggery-pokery, the DFA ends up, believe it
+or not, constituting a <code>short[227][128]</code>.</p>
+<p>Here’s a typical chunk of the automaton:</p>
+<pre><code>1. # in Start tag GI
+2. State StagGI BustedMarkup {in element type}
+3. T $NameC StagGI
+4. T $S InStag !EndGI
+5. T > InDoc !EndGI !ReportSTag
+6. T / EmptyClose !EndGI</code></pre>
+<p>This state, called <code>StagGI</code>, is the state where we’re actually
+reading the name of a tag, we got here by seeing a <code>&lt;</code> followed
+by a <code>NameStart</code> character.<br/>
+Line 1 is a comment.<br/>
+In line 2 we name the state, and support error reporting, providing the name
+of another state to fall back into in case of error, and in the curly braces,
+some text to help build an error message.<br/>
+Line 3 says that if we see a valid XML Name character, we just stay in this
+state.<br/>
+Line 4 says that if we see an XML space character, we move to state
+<code>InStag</code> and process an <code>EndGI</code> event, which would stash
+the characters in the start tag.<br/>And so on.</p>
+<h2 id='p-6'>Other Hackery</h2>
+<p>An early cut of Lark used String and StringBuffer objects to hold all the
+bits and pieces of the XML. This might be a viable strategy today, but in
+1996’s Java it was painfully slow.
+So the code goes to heroic lengths to live in the land of character arrays at
+all times, making Strings only when a client program asks for one through the
+API. The performance difference was mind-boggling.</p>
+<h2 id='p-7'>An Evil Idea</h2>
+<p>If you look at the automaton, and the Lark code, at least half—I’d bet
+three quarters—is there to deal with parsing the DTD and then dealing with
+entity wrangling.
+A whole bunch more is there to support DOM-building and walking.</p>
+<p>I bet if I went through and simply removed support for anything coming out
+of the <code>&lt;!DOCTYPE></code>, including all entity processing,
+then discarded
+the DOM stuff, then added namespace support and SAX and StAX APIs, it would be
+less than half its current size.
+Then if I reworked the I/O, knowing what I know now and stealing some tricks
+that James Clark uses in
+<a href="http://expat.sourceforge.net/">expat</a>, I bet it would
+be the fastest Java XML parser on the planet for XML docs without a
+DOCTYPE; by a wide margin. It’s hard to beat a DFA.</p>
+<p>And it would still be fully XML 1.0 compliant. Because (snicker) this is
+Java, and your basic core Java now includes an XML parser, so I could simply
+instrument Larkette to buffer the prologue and if it saw a DOCTYPE with an
+internal subset, defer to Java’s built-in parser.</p>
+<p>I’ll probably never do it. But the thought brings a smile to my face.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/22/'>
+ <title>Just A Kid</title>
+ <link href='Just-a-Kid' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/22/Just-a-Kid</id>
+ <published>2006-04-22T13:00:00-08:00</published>
+ <updated>2006-04-22T13:37:58-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Food and Drink' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Food and Drink' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Last weekend, Lauren felt like cooking up home-made Easter eggs, so
+the shopping list included “chocolate chips (large bag)”. I was heading down
+the bulk-foods aisle and realized one of the vertical acrylic bins was full of
+them. Someone had been sloppy, and there was a little heap of chocolate chips
+on the shelf underneath it. For a second, I flashed into pure eight-year-old
+mode, thinking “Holy cow, there’s a <em>whole bin</em> full of chocolate
+chips, and more just lying there!” I popped a few in my mouth and they were
+excellent; semi-sweet, dark, strong, and firm. I was still in the state that
+Buddhists don’t mean when they say “Child’s Mind”, thinking “I
+can get as many as I want!” The list did say “large bag” after all, so I put
+a bag under the spout and gleefully jammed the lever <em>all the way
+over</em>. At home, Lauren said “You went overboard, a bit, didn’t you?”
+and now we have a plastic canister-full in the pantry which should last us
+into 2007. It’s a good feeling.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/22/'>
+ <title>Goddess</title>
+ <link href='Goddess' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/22/Goddess</id>
+ <published>2006-04-22T13:00:00-08:00</published>
+ <updated>2006-04-22T12:25:59-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Family' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Family' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Microsoft' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Microsoft' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>That would be my wife
+<a href="http://www.laurenwood.org/anyway/">Lauren</a>. After
+<a href="/ongoing/When/200x/2006/04/16/Mad-at-Microsoft">I b0rked</a> our
+Win2K gamebox, I tried re-installing the OS and eventually reduced it to
+complete brick-ness, it recognized neither the video adapter nor the network
+card. So Lauren brushed me aside and started wrestling with the problem, and
+to make a long story short, it almost completely works again. At one point
+she seemed nearly infinite in her capabilities, sitting in front of the
+computer wrangling software updates while knitting baby stuff and looking up
+words in a German dictionary for the kid’s homework. Some of the German nouns
+and muttered curses at the Windows install sounded remarkably like each other.
+Why would anyone not marry a geek? The only problem is that Win2K won’t
+auto-switch resolutions to play games any more, it gets the frequency wrong
+and the LCD goes pear-shaped, you have to hand-select the frequency and
+switch into the right resolution first. LazyWeb?</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/21/'>
+ <title>Routing Around Spotlight</title>
+ <link href='Routing-Around-Spotlight' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/21/Routing-Around-Spotlight</id>
+ <published>2006-04-21T13:00:00-08:00</published>
+ <updated>2006-04-21T23:16:25-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Mac OS X/Gripes' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Mac OS X' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Gripes' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>Herewith two hideously ugly little shell scripts for use when Spotlight refuses to search your mail. Spotlight is a flawed v1.0 implementation of a really good idea and will, I&#x2019;m sure, be debugged in a near-future release. <i>[Update: The LazyWeb is educating me... these are moving targets.]</i></div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Herewith two hideously ugly little shell scripts for use when Spotlight
+refuses to search your mail.
+Spotlight is a flawed v1.0 implementation of a really good idea and will, I’m
+sure, be debugged in a near-future release.
+<i>[Update: The LazyWeb is educating me... these are moving targets.]</i></p>
+<p>My problem is that whereas Mail.app will search my To/From/Subject
+lines (slowly, and with a
+<a href="/ongoing/When/200x/2005/11/20/UnTiger">really irritating GUI</a>),
+the “Entire Message” option just doesn’t work, it returns instantly with no
+results. Yes, I’ve read the hints about making Spotlight re-index,
+but it just flatly refuses to work for me. Mind you, I have a lot of
+email, but still, it should at least try.</p>
+<p>It turns out I had never really figured out the <code>-print0</code> and
+<code>-0</code> idioms that a lot of the shell-command stalwarts now have.
+Thanks to Malcolm Tredinnick for raising my consciousness.</p>
+<p>This lives in <code>$HOME/bin</code> under the name
+<code>mailgrep</code>:</p>
+<pre><code>#!/bin/sh
+find $HOME/Library/Mail/IMAP* -name '*.emlx' -print0 | \
+ xargs -0 fgrep -i $@</code></pre>
+<p>Isn’t <code>xargs</code> a funny command? I’ve discovered that it’s nearly
+impossible to describe what does, and then why what it does is necessary, but
+there are just a whole bunch of places where you’d be lost without it.</p>
+<p>This lives in <code>$HOME/bin/mailview</code>:</p>
+<pre><code>#!/bin/sh
+find $HOME/Library/Mail/IMAP* -name '*.emlx' -print0 | \
+ xargs -0 fgrep -i -l -Z $@ | \
+ xargs -0 open</code></pre>
+<p>The first cut of this dodged <code>xargs</code> and used an
+incredibly-inefficient and slow chain of <code>-exec</code> arguments to open
+the files one at a time with
+<code>view</code> (aka <code>vim</code>), to work around
+a well-known <code>vim</code> misfeature; it complained about the input
+not being a terminal and left my Terminal.app keystrokes borked.</p>
+<p>But Malcolm, confirming my belief in the broken-ness of <code>vim</code>,
+said “Oh, *that* ‘view’. I thought it was some sexy Mac ‘view my email’ app”.
+D’oh, of course; the magic OS X <code>open</code> command does just the right
+thing.
+Erm, you might want to run <code>mailgrep</code> before you run
+<code>mailview</code>; I’m not sure what would happen if you asked OS X to
+open three or four thousand email messages at once.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/21/'>
+ <title>FSS: Pink Flowers</title>
+ <link href='Dracon-Help' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/21/Dracon-Help</id>
+ <published>2006-04-21T13:00:00-08:00</published>
+ <updated>2006-04-21T17:19:27-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts/Photos' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Photos' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>Friday Slide Scan #28 is two Eighties florals, one interior, one exterior. With a confession.</div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Friday Slide Scan #28 is two Eighties florals, one interior, one
+exterior. With a confession.</p>
+<p>First some spring flowers fallen from a tree, just as now in our front
+yard, at dusk.</p>
+<img src="0506.png" alt="Fallen pink treeflowers on grass at dusk" />
+<p>I’m not sure what these are, but look at the light in the center. Rewards
+enlarging.</p>
+<img src="0713.png" alt="Flowers in shadow with light in background" />
+<p>Here’s the confession. Sometimes on Fridays when I’m feeling kinda
+burned-out, I knock off work and do these slide scans in the office, because
+this is where I have the
+<a href="http://www.tbray.org/ongoing/When/200x/2004/04/14/MineIsBigger">big
+screen</a>.
+Blowing these pictures up to mega-huge, picking away at the old-slide crud and
+scanning artifacts, tinkering with the colour balance, and listening; I never
+play music while I’m writing or coding seriously, but I play it real loud while
+photo-editing. It’s all pretty well pure pleasure; you just can’t imagine
+how good that second one above looks at near-native size.
+It reconstitutes the part of my mind that I earn my living with; that’s my
+story and I’m sticking to it.</p>
+<p>Images in the Friday Slide Scans are from 35mm slides taken between 1953
+and 2003 by (in rough chronological order)
+<a href="http://www.textuality.com/BillBray/">Bill Bray</a>,
+<a href="/ongoing/When/200x/2004/08/11/MomsGarden">Jean Bray</a>, Tim Bray, Cath
+Bray, and
+<a href="http://www.laurenwood.org/anyway/">Lauren Wood</a>; when I know
+exactly who took one, I’ll say; in this case, at least one is by Cath Bray.
+Most but not all of the slides were on Kodachrome; they were digitized using
+a Nikon CoolScan 4000 ED scanner and cleaned up by a combination of the Nikon
+scanning software and PhotoShop Elements.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/20/'>
+ <title>Spring Pix</title>
+ <link href='Spring-Pix' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/20/Spring-Pix</id>
+ <published>2006-04-20T13:00:00-08:00</published>
+ <updated>2006-04-20T23:07:10-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Places/Vancouver' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Places' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Vancouver' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts/Photos' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Arts' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Photos' />
+ <summary type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>Three pictures around Vancouver; one of a fresh green springtime tree, two of rotten old buildings being torn down.</div></summary>
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Three pictures around Vancouver; one of a fresh green springtime tree, two
+of rotten old buildings being torn down.</p>
+<p>There’s nothing quite as fresh as just-sprouted deciduous leaves;
+another few weeks and this tree will be just a tree.</p>
+<img src="IMG_4656.png" alt="Sunlit fresh young leaves" />
+<p>I have a thing about demolition.
+The first is a rotten dingy old one-story on Main Street near 23rd, the second
+is an unlovely grey mid-rise being torn down to build still more condos at
+Homer and Helmcken.</p>
+<img src="IMG_4665.png" alt="Demolition site on Main Street, Vancouver" />
+<img src="IMG_4671.png" alt="Demolition site at Homer and Helmcken, Vancouver" />
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/20/'>
+ <title>Totten&#x2019;s Trip</title>
+ <link href='Totten-on-Iraq' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/20/Totten-on-Iraq</id>
+ <published>2006-04-20T13:00:00-08:00</published>
+ <updated>2006-04-20T21:05:22-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Places/Middle East' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Places' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Middle East' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p><a href="http://www.michaeltotten.com/">Michael J. Totten</a> is a
+journalist and blogger who’s back and forth to the
+Middle East and writes about it, quite well in my opinion; he supports this by
+freelancing and with his blog’s tip jar. He gets lots of
+link love from the right-wing blogosphere, which is puzzling because Totten is
+balanced and clear-eyed and doesn’t seem to have any particular axe to grind.
+Recently, he and a friend were
+<a href="http://www.michaeltotten.com/archives/001117.html">having fun in
+Istanbul</a> and, on a random drive out into the country, decided on impulse to
+keep going, all the way across Turkey and into Iraq; into the Kurdish
+mini-state in Iraq’s north, to
+be precise. It makes a heck of a story, with lots of pictures, in six parts:
+<a href="http://www.michaeltotten.com/archives/001119.html">I</a>,
+<a href="http://www.michaeltotten.com/archives/001120.html">II</a>,
+<a href="http://www.michaeltotten.com/archives/001121.html">III</a>,
+<a href="http://www.michaeltotten.com/archives/001124.html">IV</a>,
+<a href="http://www.michaeltotten.com/archives/001126.html">V</a>, and
+<a href="http://www.michaeltotten.com/archives/001127.html">VI</a>.
+</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/19/'>
+ <title>The Cost of AJAX</title>
+ <link href='The-Cost-of-AJAX' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/19/The-Cost-of-AJAX</id>
+ <published>2006-04-19T13:00:00-08:00</published>
+ <updated>2006-04-20T00:37:46-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology/Web' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Technology' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Web' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>James Governor
+<a href="http://www.redmonk.com/jgovernor/archives/001526.html">relays a
+question</a> that sounds important
+but I think is actively dangerous: do AJAX apps present more of
+a server-side load? The question is dangerous because it’s meaningless and
+unanswerable. Your typical Web page will, in the process of
+loading, call back to the server for a bunch of stylesheets and graphics and
+scripts and so on: for example, this <span class='o'>ongoing</span> page calls
+out to three different graphics, one stylesheet, and one JavaScript file.
+It also has one “AJAXy” XMLHttpRequest call.
+From the server’s point of view, those are all just requests to dereference
+one URI or another. In the case
+of <span class='o'>ongoing</span>, the AJAX request is for a static file less
+than 200 bytes in size (i.e. cheap).
+On the other hand, it could have been for something that required a
+complex outer join on two ten-million-row tables (i.e. <em>very</em>
+expensive). And one of the virtues of
+the Web Architecture is that it hides those differences, the “U” in URI stands
+for “Uniform”, it’s a Uniform interface to a resource on the Web that could
+be, well, anything.
+So saying “AJAX is expensive” (or that it’s cheap) is like saying “A mountain
+bike is slower than a battle tank” (or that it’s faster).
+The truth depends on what you’re doing with it.
+In the case of web sites, it depends on how many fetches you do and
+where you have to go to get the data to satisfy them.
+<span class='o'>ongoing</span> is a pretty quick web site, even though it runs
+on a fairly modest server, but
+that has nothing to do with AJAX-or-not; it’s because of the particular way
+I’ve set up the Web resources that make the pages here.
+I’ve
+<a href="/ongoing/When/200x/2006/02/14/AJAX-Performance">argued elsewhere</a>
+that AJAX can be a performance win, system-wide; but that argument too is
+contingent on context, lots of context.</p>
+</div></content></entry>
+
+<entry xml:base='When/200x/2006/04/18/'>
+ <title>Hao Wu and Graham McMynn</title>
+ <link href='Hao-Wu' />
+ <id>http://www.tbray.org/ongoing/When/200x/2006/04/18/Hao-Wu</id>
+ <published>2006-04-18T13:00:00-08:00</published>
+ <updated>2006-04-18T22:00:40-08:00</updated>
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Places/China' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Places' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='China' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='The World/Politics' />
+ <category scheme='http://www.tbray.org/ongoing/What/' term='Politics' />
+<content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
+<p>Graham McMynn is a teenager who was kidnapped in Vancouver on April 4th and
+freed, in a large, noisy, and
+<a href="http://www.cbc.ca/story/news/national/2006/04/12/bcabduction060412.html">newsworthy</a>
+police operation, on April 12th.
+<a href="http://en.wikipedia.org/wiki/Hao_Wu">Hao Wu</a> is a Chinese
+film-maker and
+<a href="http://beijingorbust.blogspot.com/">blogger</a> who was kidnapped in
+Beijing on February 22nd in a
+small, quiet police operation not intended to be newsworthy, and who has not
+been freed.
+Read about it
+<a href="http://spaces.msn.com/wuhaofamily/">here</a>,
+<a href="http://ethanzuckerman.com/haowu/">here</a>, and
+<a href="http://rconversation.blogs.com/rconversation/freehaowu/index.html">here</a>.
+Making noise about it <em>might</em> influence the government of China to
+moderate its actions against Mr. Wu, and can’t do any harm.
+Mr. McMynn’s kidnappers were a gaggle of small-time hoodlums, one of whom was
+out on bail while awaiting trial for another kidnapping (!).
+Mr. Wu’s were police.
+In a civilized country, the function of the police force is to deter such
+people and arrest them. A nation where they are the same people? Nobody
+could call it “civilized”.</p>
+</div></content></entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml.xml
new file mode 100644
index 0000000000..0b4d21b969
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom title works
+Expect: feed.title.plainText() == 'test title'
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml"><b>test</b> title</div>
+ </title>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml_entities.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml_entities.xml
new file mode 100644
index 0000000000..aaf982acfa
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_title_xhtml_entities.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom summary with entities works
+Expect: feed.subtitle.text == '&quot;test&quot; &amp; &apos;title&apos; &amp; &lt;ok&gt;'
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+<subtitle type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ "test&quot; &amp; &apos;title' &amp; &lt;ok>
+ </div>
+</subtitle>
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_updated.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_updated.xml
new file mode 100644
index 0000000000..f425674f2b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_updated.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: raw atom updated works
+Expect: feed.fields.getProperty('atom:updated') == '2003-12-13T18:30:02Z'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_updated_invalid.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_updated_invalid.xml
new file mode 100644
index 0000000000..560d756b97
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_updated_invalid.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: non-date atom updated should produce null feed.updated
+Expect: feed.updated == null
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003I'mNotADate</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_updated_normalized.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_updated_normalized.xml
new file mode 100644
index 0000000000..71fa1d03d5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_updated_normalized.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom updated works
+Expect: feed.updated == 'Sat, 13 Dec 2003 18:30:02 GMT'
+
+-->
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_version.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_version.xml
new file mode 100644
index 0000000000..a703f506f5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_version.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 Version works
+Expect: result.version == 'atom'
+-->
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+</feed> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rfc4287/feed_xmlBase.xml b/toolkit/components/feeds/test/xml/rfc4287/feed_xmlBase.xml
new file mode 100644
index 0000000000..d5760e333e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rfc4287/feed_xmlBase.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: atom feed with xml:base
+Expect: feed.link.spec == "http://www.example.com/foo/bar/baz";
+
+-->
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xml:base="http://www.example.com/foo/bar/">
+
+ <id>tag:example.com,2006:/atom/conformance/linktest/</id>
+ <title>Atom Link Tests</title>
+ <updated>2005-06-18T16:13:00Z</updated>
+ <link href="baz" />
+
+ <entry xml:base="http://www.example.org">
+ <id>tag:example.org,2006:/linkreltest/1</id>
+ <title>Does your reader support xml:base properly? </title>
+ <updated>2006-06-23T12:12:12Z</updated>
+ <link xml:base="/bar/" href="foo"/>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/feeds/test/xml/rss09x/rss090.xml b/toolkit/components/feeds/test/xml/rss09x/rss090.xml
new file mode 100644
index 0000000000..783c018f4a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rss090.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+
+Description: RSS 0.90 works
+Expect: feed.title.plainText() == "Mozilla Dot Org" && result.version == "rss090"
+
+-->
+<!-- The very first RSS file, circa 1999 -->
+<rdf:RDF
+xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+xmlns="http://my.netscape.com/rdf/simple/0.9/">
+
+ <channel>
+ <title>Mozilla Dot Org</title>
+ <link>http://www.mozilla.org</link>
+ <description>the Mozilla Organization web site</description>
+ </channel>
+
+ <image>
+ <title>Mozilla</title>
+ <url>http://www.mozilla.org/images/moz.gif</url>
+ <link>http://www.mozilla.org</link>
+ </image>
+
+ <item>
+ <title>New Status Updates</title>
+ <link>http://www.mozilla.org/status/</link>
+ </item>
+
+ <item>
+ <title>Bugzilla Reorganized</title>
+ <link>http://www.mozilla.org/bugs/</link>
+ </item>
+
+ <item>
+ <title>Mozilla Party, 2.0!</title>
+ <link>http://www.mozilla.org/party/1999/</link>
+ </item>
+
+ <item>
+ <title>Unix Platform Parity</title>
+ <link>http://www.mozilla.org/build/unix.html</link>
+ </item>
+
+ <item>
+ <title>NPL 1.0M published</title>
+ <link>http://www.mozilla.org/NPL/NPL-1.0M.html</link>
+ </item>
+
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss09x/rss091.xml b/toolkit/components/feeds/test/xml/rss09x/rss091.xml
new file mode 100644
index 0000000000..6749fc49e3
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rss091.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
+<!--
+
+Description: RSS 0.91 works
+Expect: feed.title.plainText() == "Scripting News" && result.version == "rss091"
+
+-->
+<rss version="0.91">
+ <channel>
+ <language>en</language>
+ <description>
+ News and commentary from the cross-platform scripting community.
+ </description>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <image>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+ </image>
+ <item>
+ <title>stuff</title>
+ <link>http://bar.example.com</link>
+ <description>This is an article about some stuff</description>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss09x/rss091_withNS.xml b/toolkit/components/feeds/test/xml/rss09x/rss091_withNS.xml
new file mode 100644
index 0000000000..9ab2c3bf71
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rss091_withNS.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
+<!--
+
+Description: RSS 0.91 works
+Expect: feed.title.plainText() == "Scripting News" && result.version == "rss091"
+
+-->
+<rss version="0.91" xmlns="http://backend.userland.com/RsS2">
+ <channel>
+ <language>en</language>
+ <description>
+ News and commentary from the cross-platform scripting community.
+ </description>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <image>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+ </image>
+ <item>
+ <title>stuff</title>
+ <link>http://bar.example.com</link>
+ <description>This is an article about some stuff</description>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss09x/rss092.xml b/toolkit/components/feeds/test/xml/rss09x/rss092.xml
new file mode 100644
index 0000000000..ed68bd64f8
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rss092.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+
+Description: RSS 0.92 works
+Expect: feed.title.plainText() == "Scripting News" && result.version == "rss092"
+
+-->
+<rss version="0.92">
+ <channel>
+ <language>en</language>
+ <description>
+ News and commentary from the cross-platform scripting community.
+ </description>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <image>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+ </image>
+ <item>
+ <title>stuff</title>
+ <link>http://bar.example.com</link>
+ <description>This is an article about some stuff</description>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss09x/rss093.xml b/toolkit/components/feeds/test/xml/rss09x/rss093.xml
new file mode 100644
index 0000000000..f1bb81ddf9
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rss093.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+
+Description: RSS 0.93 works
+Expect: feed.title.plainText() == "Scripting News" && result.version == "rss093"
+
+-->
+<rss version="0.93">
+ <channel>
+ <language>en</language>
+ <description>
+ News and commentary from the cross-platform scripting community.
+ </description>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <image>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+ </image>
+ <item>
+ <title>stuff</title>
+ <link>http://bar.example.com</link>
+ <description>This is an article about some stuff</description>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss09x/rss094.xml b/toolkit/components/feeds/test/xml/rss09x/rss094.xml
new file mode 100644
index 0000000000..1c2b17a24d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rss094.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+
+Description: RSS 0.94 works
+Expect: feed.title.plainText() == "Scripting News" && result.version == "rss094"
+
+-->
+<rss version="0.94">
+ <channel>
+ <language>en</language>
+ <description>
+ News and commentary from the cross-platform scripting community.
+ </description>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <image>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+ </image>
+ <item>
+ <title>stuff</title>
+ <link>http://bar.example.com</link>
+ <description>This is an article about some stuff</description>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss09x/rssUnknown.xml b/toolkit/components/feeds/test/xml/rss09x/rssUnknown.xml
new file mode 100644
index 0000000000..653b574d0c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss09x/rssUnknown.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+
+Description: RSS unknown version works
+Expect: feed.title.plainText() == "Scripting News" && result.version == "rssUnknown"
+
+-->
+<rss version="0.95">
+ <channel>
+ <language>en</language>
+ <description>
+ News and commentary from the cross-platform scripting community.
+ </description>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <image>
+ <link>http://www.scripting.com/</link>
+ <title>Scripting News</title>
+ <url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
+ </image>
+ <item>
+ <title>stuff</title>
+ <link>http://bar.example.com</link>
+ <description>This is an article about some stuff</description>
+ </item>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_bogus_title.xml b/toolkit/components/feeds/test/xml/rss1/feed_bogus_title.xml
new file mode 100644
index 0000000000..a28952495b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_bogus_title.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ bogus title element
+Expect: feed.title.text == 'Correct Title'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+
+
+
+ <title>Bogus</title>
+
+
+
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Correct Title</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+
+
+
+ <title>Bogus</title>
+
+
+
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </dc:description>
+ </item>
+ <item>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_description.xml b/toolkit/components/feeds/test/xml/rss1/feed_description.xml
new file mode 100644
index 0000000000..109b8f9492
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_description.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed description works
+Expect: feed.fields.getProperty('rss1:description') == 'a description'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <description>a description</description>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_description_normalized.xml b/toolkit/components/feeds/test/xml/rss1/feed_description_normalized.xml
new file mode 100644
index 0000000000..44154b9f17
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_description_normalized.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed description works normalized
+Expect: feed.subtitle.text == 'a description'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <description>a description</description>
+ </channel>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_description_with_dc.xml b/toolkit/components/feeds/test/xml/rss1/feed_description_with_dc.xml
new file mode 100644
index 0000000000..4e5b1637a3
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_description_with_dc.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed description works normalized
+Expect: feed.subtitle.text == 'a description'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ <description>a description</description>
+ </channel>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_description_with_dc_only.xml b/toolkit/components/feeds/test/xml/rss1/feed_description_with_dc_only.xml
new file mode 100644
index 0000000000..fa1ecd59ad
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_description_with_dc_only.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed description works normalized
+Expect: feed.subtitle.plainText() == 'another description'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_generator.xml b/toolkit/components/feeds/test/xml/rss1/feed_generator.xml
new file mode 100644
index 0000000000..07ec853218
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_generator.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+
+Description: RSS1 generator
+Expect: feed.generator.agent == "http://Orchard.SourceForge.net/1.2/"
+
+-->
+
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:admin="http://webns.net/mvcb/"
+ xmlns="http://purl.org/rss/1.0/">
+
+ <channel rdf:about="http://meerkat.oreillynet.com/?_fl=rss1.0">
+ <title>Meerkat</title>
+ <link>http://meerkat.oreillynet.com</link>
+ <description>Meerkat: An Open Wire Service</description>
+ <admin:errorReportsTo rdf:resource="mailto:channel-owner@acme.orgs"/>
+ <admin:generatorAgent rdf:resource="http://Orchard.SourceForge.net/1.2/"/>
+
+ <image rdf:resource="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg" />
+
+ <items>
+ <rdf:Seq>
+ <rdf:li rdf:resource="http://c.moreover.com/click/here.pl?r123" />
+ </rdf:Seq>
+ </items>
+
+ <textinput rdf:resource="http://meerkat.oreillynet.com/" />
+
+ </channel>
+
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_id.xml b/toolkit/components/feeds/test/xml/rss1/feed_id.xml
new file mode 100644
index 0000000000..8f118102a8
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_id.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ feed rdf:about
+Expect: feed.id == 'http://www.xml.com/xml/news.rss'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item rdf:about="http://example.com/hmm">
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_image.xml b/toolkit/components/feeds/test/xml/rss1/feed_image.xml
new file mode 100644
index 0000000000..c293acc3ff
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_image.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ image
+Expect: ((feed.image.getProperty('rss1:link') == 'http://www.xml.com') && (feed.image.getProperty('rss1:url') == 'http://xml.com/universal/images/xml_tiny.gif'))
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ <textInput rdf:resource="http://www.google.com"/>
+ </channel>
+
+ <image rdf:about="http://xml.com/universal/images/xml_tiny.gif">
+ <title>XML.com</title>
+ <link>http://www.xml.com</link>
+ <url>http://xml.com/universal/images/xml_tiny.gif</url>
+ </image>
+
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>
+ </item>
+
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_items_length_zero.xml b/toolkit/components/feeds/test/xml/rss1/feed_items_length_zero.xml
new file mode 100644
index 0000000000..a935b9ad9f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_items_length_zero.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ zero items count
+Expect: feed.items.length == 0
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_link.xml b/toolkit/components/feeds/test/xml/rss1/feed_link.xml
new file mode 100644
index 0000000000..f304a63e82
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_link.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed title works normalized
+Expect: feed.fields.getProperty('rss1:link') == 'http://xml.com/pub'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_link_normalized.xml b/toolkit/components/feeds/test/xml/rss1/feed_link_normalized.xml
new file mode 100644
index 0000000000..ba9674746f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_link_normalized.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed link works normalized
+Expect: feed.link.spec == 'http://xml.com/pub'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_textInput.xml b/toolkit/components/feeds/test/xml/rss1/feed_textInput.xml
new file mode 100644
index 0000000000..ea56394778
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_textInput.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ textinput
+Expect: feed.textInput.getProperty('rdf:about') == 'http://search.xml.com'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+
+ <textinput rdf:about="http://search.xml.com">
+ <title>Search XML.com</title>
+ <description>Search XML.com's XML collection</description>
+ <name>s</name>
+ <link>http://search.xml.com</link>
+ </textinput>
+
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_title.xml b/toolkit/components/feeds/test/xml/rss1/feed_title.xml
new file mode 100644
index 0000000000..121545d380
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_title.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed title works
+Expect: feed.fields.getProperty('rss1:title') == 'Test'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_title_extra_att.xml b/toolkit/components/feeds/test/xml/rss1/feed_title_extra_att.xml
new file mode 100644
index 0000000000..b24550696f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_title_extra_att.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed title works
+Expect: feed.fields.getProperty('rss1:title') == 'Test'
+
+-->
+<rdf:RDF
+ xmlns:foo="http://example.org"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title foo:bar="baz">Test</title>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_title_normalized.xml b/toolkit/components/feeds/test/xml/rss1/feed_title_normalized.xml
new file mode 100644
index 0000000000..5bd6ccefd0
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_title_normalized.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed title works normalized
+Expect: feed.title.plainText() == 'Test'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ </channel>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_updated.xml b/toolkit/components/feeds/test/xml/rss1/feed_updated.xml
new file mode 100644
index 0000000000..4aed87e983
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_updated.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed updated
+Expect: feed.updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ <dc:date>Sat, 07 Sep 2002 00:00:01 GMT</dc:date>
+ </channel>
+ <item rdf:about="http://example.com/hmm">
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_updated_dctermsmodified.xml b/toolkit/components/feeds/test/xml/rss1/feed_updated_dctermsmodified.xml
new file mode 100644
index 0000000000..24b9fc92cc
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_updated_dctermsmodified.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed updated
+Expect: feed.updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:dcterms='http://purl.org/dc/terms/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ <dcterms:modified>Sat, 07 Sep 2002 00:00:01 GMT</dcterms:modified>
+ </channel>
+ <item rdf:about="http://example.com/hmm">
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/feed_version.xml b/toolkit/components/feeds/test/xml/rss1/feed_version.xml
new file mode 100644
index 0000000000..8147a970cd
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/feed_version.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 Version works
+Expect: result.version == 'rss1'
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ </channel>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/full_feed.xml b/toolkit/components/feeds/test/xml/rss1/full_feed.xml
new file mode 100644
index 0000000000..7dae34c857
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/full_feed.xml
@@ -0,0 +1,41 @@
+<!--
+
+Description: atom generator works
+Expect: result.bozo == true && feed.items.length == 1
+
+-->
+<rdf:RDF
+xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+xmlns:dc="http://purl.org/dc/elements/1.1/"
+xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+xmlns:admin="http://webns.net/mvcb/"
+xmlns:cc="http://web.resource.org/cc/"
+xmlns="http://purl.org/rss/1.0/">
+
+<channel rdf:about="http://example.org/">
+<title>fooo</title>
+<link>http://weblogs.example.org/</link>
+<description>fooooooo</description>
+<dc:language>en-us</dc:language>
+<dc:creator></dc:creator>
+<dc:date>2006-04-10T08:38:18-08:00</dc:date>
+<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.2" />
+
+
+<items>
+<rdf:Seq>
+<rdf:li rdf:resource="http://weblogs.example.org/archives/009698.html" />
+</rdf:Seq>
+
+</items>
+
+</channel>
+<item rdf:about="http://weblogs.example.org/archives/009698.html">
+<title>Come From?</title>
+<link>http://example.org/009698.html</link>
+<description><![CDATA[
+ %G–%@ much of the code
+]]></description>
+<dc:date>2006-02-06T10:19:03-08:00</dc:date>
+</item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/full_feed_not_bozo.xml b/toolkit/components/feeds/test/xml/rss1/full_feed_not_bozo.xml
new file mode 100644
index 0000000000..61ae8a2202
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/full_feed_not_bozo.xml
@@ -0,0 +1,354 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: Full RSS1 feed not bozo
+Expect: result.bozo == false
+
+-->
+<rdf:RDF
+xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+xmlns:dc="http://purl.org/dc/elements/1.1/"
+xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+xmlns:admin="http://webns.net/mvcb/"
+xmlns:cc="http://web.resource.org/cc/"
+xmlns="http://purl.org/rss/1.0/">
+
+<channel rdf:about="http://weblogs.mozillazine.org/ben/">
+<title>Inside Firefox</title>
+<link>http://weblogs.mozillazine.org/ben/</link>
+<description>The Inside Track on Firefox Development</description>
+<dc:language>en-us</dc:language>
+<dc:creator></dc:creator>
+<dc:date>2006-04-26T14:34:49-08:00</dc:date>
+<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.2" />
+
+
+<items>
+<rdf:Seq><rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010115.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010109.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010075.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010074.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010073.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010040.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010030.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/010011.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009965.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009964.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009943.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009924.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009914.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009804.html" />
+<rdf:li rdf:resource="http://weblogs.mozillazine.org/ben/archives/009774.html" />
+</rdf:Seq>
+</items>
+
+</channel>
+
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010115.html">
+<title>Firefox 2 Is Cool</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010115.html</link>
+<description><![CDATA[<p>A lot of people read my previous post and came to a very reasonable conclusion: "If you take Places out of Firefox 2, shouldn't it be called Firefox 1.6?"</p>
+
+<p>I don't agree that Places was the one and only thing that sold Firefox 2 though. I took a look through the <a href="http://wiki.mozilla.org/Firefox2/Requirements">Firefox 2 Requirements</a> page to look at some of the other stuff that's going on. Reading that document, I think I can see now why people are down in the dumps about no-places Firefox 2. I don't think that document necessarily does the best possible job of capturing the excitement I have about some of the Firefox 2 features we're pursuing. </p>
+
+<p>For the past week or so, I've been toting around a printout of another document, which I wrote because I wanted to convey some of the vision I have of the Firefox 2 product as a whole - a more holistic view as it were. </p>
+
+<h3>Safer, Faster, Better</h3>
+
+<p>If you take a look at the black buttons stacked in the right column of this page, you'll see that one of them reads "Safer, Faster, Better." I don't knowwho came up with that one but it's a good tag line. It has a certain cadence about it. People have attached lots of these to Firefox in the past - "Take Back the Web" was the one I came up with, there's "Rediscover the Web", the FirefoxFlicks project has yielded a few good ones too - I like "<a href="http://www.firefoxflicks.com/flick/index.php?sort=new&id=21122&c=false">Web For All</a>". But "Safer, Faster, Better" is not just a tag line, it can also map into a set of themes for product development. </p>
+
+<p>So, taking a look at the Requirements page, I attempted to do that. My document wasn't a comprehensive collection of everything on that page, I was focused more on the things immediately visible to most users. I guess my problem with the Requirements page has always been its very engineering/technical focus. The result of this is that the priority of items tend to reflect how difficult something is to implement, or where it lies in the development cycle, not necessarily the impact on the user. What I ended up with I guess is a sort of "Shadow PRD" that reflects what I personally thought was cool about Firefox 2, and what I wanted to get out of it. </p>
+
+<p>A copy of the document is <a href="http://www.bengoodger.com/software/mb/2.0/firefox2-vision.html">here</a>. <br />
+Some notes:</p>
+
+<ul>
+ <li>Assume the scratched out section for "Retracing Your Steps" will be part
+ of a future release.
+ <li>The priorities shown are my opinion, and relate to potential impact on
+ the user.
+ <li>The document does not represent all of the work being done, just a
+ readily marketable subset.
+</ul>
+
+<p>All in all, I think there's easily enough here to justify a "2" designation. That's just my opinion though. I also think whole numbers are probably easier for the general populace to understand than decimals. </p>
+
+<p>Firefox has never been about date driven development (within reason). The changes with Places should not be seen as a change in this sentiment. What we're about is high quality software development with real advantages to <br />
+users, and I think that with the updated plan we're still on a trajectory that supports and encourages that, perhaps more firmly now than before.</p>
+
+<p>And that's it from today's "Ben waits for the tinderboxen to clear" report. </p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-26T14:34:49-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010109.html">
+<title>Firefox 2 Content Update</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010109.html</link>
+<description><![CDATA[<p>When we began work on Firefox 2, we decided to focus on a development branch, a continuation of the Mozilla 1.8 branch. The reason for doing this was that there was to be a lot of significant architectural changes going on on the trunk, with the prospect of a new rendering back end, a rearchitecture of reflow in layout, and various other things. Shipping a Firefox 2 release in 2006 off of this code did not seem possible. </p>
+
+<p>As a result, we decided to pursue a release focused on application level improvements, on a separate branch. Going into it, we knew the perils of multi-branch development. We knew the divergences that would inevitably form between branch and trunk. We had experience from the painful development of Firefox 1.0 on the Aviary branch. We resolved to be more methodical about our commits, but we knew to expect some pain. The goal was to produce a high value release in short enough time so that we could all return to the trunk and help build new features that utilize the back end being developed there, to help shake them out. </p>
+
+<p>Late last year, we put together a list of things to pursue for the Firefox 2 release. A month or so ago, we got together as a group and formalized this more in a <a href="http://wiki.mozilla.org/Firefox2/Requirements">Firefox 2 PRD</a>. We had scheduled four major pre-release milestones, two alphas and two betas. We have already shipped one alpha. The intent of the second is to be "Feature Complete".</p>
+
+<p>The people driving the various sub-projects on the Requirements list get together weekly to check status. As the weeks have gone by, it has become clear to us that the most complex feature on the plan is Places. It is easily an order of magnitude more complex than anything else on the plan. Places is a great feature and it has been exciting watching its capabilities grow. We are looking forward to the capabilities that it will expose. What we have learned though is that the work required to complete Places is probably too substantial to gate the Firefox 2 release. It falls more into the "significant rearchitecture" category of feature that's generally been targeted at Firefox 3.</p>
+
+<p>What we have decided to do is as follows:</p>
+
+<ul>
+<li>We will disable places on the 1.8 branch, reverting the user interface and back end to Firefox 1.x functionality.
+<li>We will continue to aggressively develop the capabilities of Places on the 1.9 trunk. Places will remain enabled here.
+</ul>
+
+<p>We think this is a good decision for two reasons:</p>
+
+<ul>
+<li>It reduces the pressure on the Places team to deliver a lot of bug fixes and additional features on the very immediate timeframe required by the Firefox 2 testing releases. It is my opinion that doing so would impact the quality of the feature, if we did not add at least a couple more alpha cycles to the process. This decision provides us with an opportunity to really make the architecture and user interface of Places reach their full potential.
+<li>It allows us as a group to circle around and consider the content of the Firefox 2 release holistically, identify high impact at risk areas and spend some more time on them. One of those for me was Feed Handling.
+</ul>
+
+<p>Michael Schroepfer of the Mozilla Corporation has a <a href="http://groups.google.com/group/mozilla.dev.planning/browse_frm/thread/4b8e7bafecccbc10/8997efd5d5d5f03f">newsgroup posting</a> with additional information. His thread is also the most appropriate forum for discussion of this topic. </p>
+
+<p>I have been working on refining some of the messaging surrounding feature content and prioritization on the PRD. I will post the initial results of that here soon.</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-24T09:30:54-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010075.html">
+<title>Did I Mention...</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010075.html</link>
+<description><![CDATA[<p>... that I hate this computer?</p>
+
+<p>While I'm at it... the up arrow key cap fell off after about three weeks, in early 2004. About six months later I lost the little rubber membrane thing that made it slightly easier to push the arrow. Since then, I've been typing by pushing down on the little connection thingy on the keyboard tray. </p>
+
+<p>It's been shedding pieces of plastic too. I've never dropped the computer once, but pieces of the shell have begun to snap off. </p>
+
+<p>When I first got it, when the secondary battery was in place, when the primary drained the machine would hibernate, even though the secondary was present! Pretty awful bug to ship with. There was never a solution that I could find. Speaking of batteries, the primary battery is pretty much toast... it won't go for more than 5 minutes before shutting down. It began doing this at around the 12-18 month mark. And the battery light permanently flashes orange whenever the system is on. </p>
+
+<p>Why don't I call the hotline? I guess I'll have to, before my warranty runs out. I don't because it usually involves 45 minutes on hold or explaining to someone who only has a script to read from that the issue involving a missing up arrow doesn't require restarting Windows or running some stupid diagnostic tool. I could have paid more for "premium support" at build-time but I found that concept sort of insulting: why should I have to pay extra to speak to someone who is smart and doesn't think I'm a moron?</p>
+
+<p>And I don't want a Thinkpad either. I hate those computers. They have old-fashioned 4:3 displays, and the function key and left Ctrl key are reversed. I know I could map them differently but why would I? Why couldn't IBM just have designed the product correctly in the first place? Oh, and I'd sooner drink paint than run the awful IBM access connections software to connect to a wireless network, or deal with the fact that the Num Lock key seems to reset to ON every time the system is rebooted.</p>
+
+<p>Why doesn't someone make the perfect laptop? I'd be interested to hear from someone how long the compile times are for FirefoxDebug on a 2.16GHz MacBook Pro...</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-16T19:11:28-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010074.html">
+<title>I Hate This Computer</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010074.html</link>
+<description><![CDATA[<p>I have been fighting with this computer for the past few days to do a build with a few patches applied. </p>
+
+<p>First, I managed to get a certain distance with a branch build, compiling with Visual C++ 6.0. But soon I realized there were too many dependencies that were trunk specific, so I had to build trunk. About a quarter of the way through my build died, of course, compiling from the same shell, wrong version of VC6.0 for Cairo/Thebes. </p>
+
+<p>Starting over again with the VC7 tools, another failure towards the end. Some sort of cyclic dependency check error. Clobber and restart. Now I forgot one of my patches had a configure change, and the process begins anew, I have effectively clobbered. </p>
+
+<p><a href="http://weblogs.mozillazine.org/ben/archives/2003_12.html">When I bought this machine</a>, a Dell Precision M60 with a Pentium M 1.7GHz processor, a 7200rpm disk and a gig of RAM, it could compile Firefox start to stop in 21 minutes. Now it takes over an hour.</p>
+
+<p>The situation is better on my Google-supplied workstation, but for how long? Over time, Windows reaches a point of being completely useless for anything aside from the most basic activities. What's the effect? I had planned to work both days this weekend on Firefox 2 features. Instead I spent the whole time fighting one of the most frustrating fights possible, and have achieved nothing. I hate Windows. I hate this computer.</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-16T18:58:02-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010073.html">
+<title>Miscommunications</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010073.html</link>
+<description><![CDATA[<p>My laptop was running pretty slowly yesterday so I decided to scan the Add/Remove Programs list to clear out the cruft. Things were really chugging along. I sequentially uninstalled several pieces of software, and the process was very dissatisfying. I became more and more enfuriated at my computer as it proceeded. Here are some of the nuisances:</p>
+
+<ul>
+<li>I could only remove one thing at a time.
+<li>Many pieces of software used the Windows Installer system which seemed to take forever and report very inconsistent progress (I know, Firefox isn't the best at this in its installer, either)
+<li>Most annoyingly, the uninstaller apps all reported themselves as performing a variety of actions that I never requested, as explanations for what they were doing during long periods of inactivity and progress-bar freeze. Common excuses were "Windows is Configuring <blah>" and "InstallShield is preparing a report on <bleh>".
+</ul>
+
+<p>You know, I never <strong>asked</strong> for blah to be "configured." I never asked for a report on bleh (What am I, a manager? Where is the report anyway? Does it have the appropriate cover sheet?) <strong>I just want the software gone</strong>. I'm getting really tired of excuses from software like this. Windows software seems to be getting worse and worse. On Mac, the typical way to remove a program is to drag it into the trash can. I can even do that to several programs at once! I do however have to be able to afford a Mac (I can, I have one). Many folk aren't as fortunate as I. </p>
+
+<p>As a side note, I read an interesting article in Forbes a few weeks ago criticizing Microsoft for its delays shipping Vista, and asking why wouldn't you just side-step all the trouble and buy a Mac, since the odds were good many people would have to upgrade their PC anyway just to get the whiz-bang in Vista. The article side-swiped open source desktop initiatives, asking where the viable free alternative was. I think that was an interesting point, and especially so since the capabilities of Linux systems have come an awesome distance in the past few years but there have been few distributions or desktop environments that IMO make the most of all of those.</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-16T18:05:04-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010040.html">
+<title>Firefox Version Numbers</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010040.html</link>
+<description><![CDATA[<p>Mike Beltzner <a href="http://www.beltzner.ca/mike/archives/2006/04/10/when_3_is_less_than_2.html">explains</a> Firefox version numbering. i.e. Firefox 3 RTM is not "out".</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-10T08:38:18-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010030.html">
+<title>Our Next Challenge?</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010030.html</link>
+<description><![CDATA[<p>The past year or so has been interesting. In this time, I've been able to meet a lot of new people and learn a lot of new things. Most importantly is that for the first time in as long as I can remember, I have had a chance to see the Mozilla project from the outside, as it would appear to someone who was trying to build on Mozilla technology or contribute directly to a project, but was not part of Netscape "back in the day" or an employee of the Mozilla Foundation/Corporation itself. </p>
+
+<p>It's been an illuminating experience. From a technical perspective, it helped highlight APIs that I had developed without a clear understanding of how they would be used. The Extension Installation API was one example of this, and we were able to make some great improvements to it in 2005.</p>
+
+<p>But perhaps more importantly it has shed some light on how people perceive Mozilla as an open source project. These perceptions are not the sort of things people express explicitly. You have to notice them.</p>
+
+<h3>The Difficulty of Involvement</h3>
+
+<p>This is sort of the uber-perception. I think some of the reasons for this include the following:</p>
+
+<h3>Where is the Discussion?</h3>
+
+<p>Which newsgroup/mailing list/IRC channel/wiki/talk page/bug/forum page do I need to track in order to know what's going on in a specific area? The answer is unsatisfactorily complex.</p>
+
+<p>The traditional method of joining a project in the OSS world (where you join lists and IRC channels and lurk for a while, gradually ramping up your contributions) scales uneasily to a project the size of Mozilla. The amount of data a mere mortal would have to absorb in order to be productive quickly is staggering. I have in the past jocularly referred to it as the "learning wall". I wonder how many people just give up. </p>
+
+<h3>Madness to Method?</h3>
+
+<p>As a large project, Mozilla has thousands of source files across hundreds of directories. One of my coworkers here at Google commented that he tried to find something as simple as the browser window code a couple of years ago and couldn't, because it lived under the thoughtfully named "xpfe". </p>
+
+<p>There's not a huge amount of documentation - and I'm not just talking about public API docs. I'm talking about the much needed diagrams that show how the various building blocks fit together, and in-code documentation for pretty much anything that isn't intuitive (which is a lot). I've written as little of this as anyone else.</p>
+
+<h3>Tone</h3>
+
+<p>In the past, I have not done the best I could to establish a tone for discourse that is conducive to productive development. My tendency was to snap when provoked. I made two mistakes of judgement here, one was ignoring the effect that this sort of thing would have on those watching, aside from the victim. The other was to think that regardless of the tone set by my actions, we as a group could work through any negative effects. Any work we relied on others for we could do ourselves. Or we could hire through it.</p>
+
+<h3>The Joy of Code</h3>
+
+<p>The flaw with this is that when your project's contributions come solely from companies, for better or for worse the activities of those paid contributors will align in some way with the interests of those companies. What this does not always allow for is the pursuit of the sort of improvements that are outside the scope of these interests. Such things often include raising general code quality, speculative feature development, feature polish and detail etc. I don't mean to say that companies are <em>against</em> these things, but they're often not the primary concern during a release crunch. And what companies like to have is shipping software. </p>
+
+<p>Alternately, even in the absence of corporate support, if there is not enough redundancy that the same set of folk has to do the grunt work over and over, the risk of burnout is high.</p>
+
+<p>I feel this because I have been incredibly "plan" focused over the past few years, formally during my time at Netscape and less formally but no less importantly during the run up to Firefox 1.0 and 1.5. What I notice is that I no longer have time to work on the sort of interesting side projects that I used to enjoy doing when I was first starting out. </p>
+
+<p>For example, about six years ago I discovered a bug in the Bookmarks menu shortly after scrolling was implemented. When you moused into a submenu for a folder that was in the scrolled section, the sub menu popup was pushed off the bottom of the screen. I took a couple of days to learn the menu positioning code and fix the math error that was causing the bug. The exercise was good for me in a number of ways: I learned more about another section of the code, my general expertise was raised, and well.. I fixed the bug that was bothering<br />
+me.</p>
+
+<p>I think we need to have a project that is accessible to volunteers for this reason. We also need to provide a way to allow those volunteers to grow if they want to, so that if you're one of the folk at the center you can have a chance to step aside for a moment and take a breather and code for the pure joy of it. </p>
+
+<p>Full time paid contributors will always be a part of Open Source development. But I don't think release-focused agendas will ever be a substitute for the passion of folk who participate because of the joy of exploration and of contribution. </p>
+
+<h3>Looking Outward, Looking Forward</h3>
+
+<p>As a project, we have made overtures towards being a more inclusive lot. For some of the reasons I've listed here, I think as a project we're still more inward looking than outward. How many of us have thought about what we want to be doing in 5 years? Will we always be doing this? Will our roles remain the same? My opinion is that it's fast becoming time for us to start considering making personal sacrifices in our short term conveniences to make the project more accessible to new people. Do I know what we need to do? Not exactly. But I have some ideas: find ways to make our discussions, our public faces, and our code more accessible.</p>
+
+<p>With Firefox we did an excellent job of building a world class product that people wanted to use. We have a new challenge now, one that is larger and scope and in the long run in my opinion considerably more important because the long term success of products like Firefox depend on it. How will we grow a world class development community? How will we ensure that the freedoms we enjoy are protected, forever?</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-07T09:22:59-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/010011.html">
+<title>Congrats Relicensing Project!</title>
+<link>http://weblogs.mozillazine.org/ben/archives/010011.html</link>
+<description><![CDATA[<p><a href="http://weblogs.mozillazine.org/gerv/archives/2006/03/relicensing_complete.html">Gerv announces</a> that the Relicensing project is complete. Congrats Gerv and everyone else who doggedly pursued this over the years. Part of Mozilla's mission is preserving choice, and making our code available under a variety of flexible licenses helps ensure that by allowing other projects to make use of it. This was a formidable task and a great accomplishment. </p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-04-04T07:15:22-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009965.html">
+<title>Producing Open Source Software</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009965.html</link>
+<description><![CDATA[<p>I've been reading Karl Fogel's excellent <a href="http://www.producingoss.com/">Producing Open Source Software</a>. </p>
+
+<p>Karl's book is very well written, nice and compact (272 pages), and contains useful information for anyone doing anything in the Open Source world: both developers and managers. </p>
+
+<p>It will help people new to Open Source get a better understanding of how OSS projects are run. It will help people familiar with Open Source to get a better understanding of how to contribute more effectively.</p>
+
+<p>It's definitely a "must read."</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-03-26T15:03:52-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009964.html">
+<title>Writing for Busy People</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009964.html</link>
+<description><![CDATA[<p>Back when I was in <a href="http://www2.ece.auckland.ac.nz/">University</a>, many of the lecturers stressed time and time again the importance of succinct, well organized writing. They said over and over that this was the best way to have your thoughts read and understood by decision makers. In fact, they scared us by saying that 70% of us would become managers sooner or later!</p>
+
+<p>Well, I can tell you that's sage advice. It's great when people make contributions in the form of ideas and proposals, but it's even better when they're written for busy people. Here are some examples:</p>
+
+<ul>
+ <li>Making important points up front
+ <li>Clear taxonomy of headings, and lots of them
+ <li>Writing clearly and succinctly
+ <li>No long, unbroken paragraphs or tracts of text.
+ <li>Preferring bulleted lists with clear points to paragraphs.
+ <li>Use of emphasis in formatting to make important things clear
+</ul>
+
+<p>These days, I find I don't have a lot of time to read everything carefully, so the better structured a document is, the more I get out of it. I frequently find I miss entire subsections or points of documents, even when there's relatively little text, because of incomplete organization. My eyes definitely glaze over when i see a large block of unbroken text with few headings. At the very least, it'd be very helpful if folk would structure their thoughts into: "Problem" and "Proposed Solution". </p>
+
+<p>Before you post, stop and think if you've written something in a way that'll allow others to get the most out of it. Communicating your ideas effectively means you may get a clearer and quicker response from other people. </p>]]></description>
+<dc:subject>Editorial</dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-03-26T14:48:04-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009943.html">
+<title>Step 2: Ask Questions</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009943.html</link>
+<description><![CDATA[<p>A healthy project is one where active contributors are always evaluating the project's progress, making sure it is headed in the right direction (usually stated in the project mission or goals). </p>
+
+<p>I think we could be better at this in Mozilla. I'm not suggesting people be assholes or anything, but I think some more pan-project analysis would be useful. </p>
+
+<p>Historically, I can point at a couple of groups of people who have attempted to do something like this. The <code>drivers@</code> group is one that looked beyond individual modules within Gecko to make sure that the right thing for the shipping products as a whole happened. The Firefox team is another example. By taking a holistic view, user experience was enhanced. </p>
+
+<p>I think contributors should not be afraid to poke their nose in other parts of the project and see how things are going. Ask questions. Learn more. Get involved in governance and management. If things don't seem intuitive, or a little arbitrary, ask, rather than assume it's for a good reason. One of the benefits of having an open, referencable set of discussion forums means that once you've answered a question once on the public forums, when someone else asks you can just give them a URL. </p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-03-22T17:27:14-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009924.html">
+<title>Step 1: Public Discussions</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009924.html</link>
+<description><![CDATA[<p>The first step on the pathway to open source project happiness is to have our discussions in the public. </p>
+
+<p>One of the things people have (rightly) criticized about Firefox and Mozilla development in the past is that too much happens mysteriously, behind closed doors. This was for a number of reasons that sounded sufficient at the time - it was expedient, people were sitting within shouting distance, mental laziness, etc. </p>
+
+<p>What poor communication breeds is a lack of understanding of procedures, priorities and such like. A healthy project is one where the contributors understanding where things are headed, and what parts they can play. It is one where newcomers can visit the project website and within the space of a few minutes get a decent understanding of how things work, and find out opportunities for them to participate. </p>
+
+<p>People don't want to contribute to projects where things happen "magically". I've learned this lesson in the past. </p>
+
+<p>To this end, I've been encouraging everyone to have public discussions on the <a href="https://lists.mozilla.org/listinfo">Mozilla Newsgroups/Mailing Lists</a>. For Firefox, the list is dev-apps-firefox@, and the newsgroup is mozilla.dev.apps.firefox. They are mirrored through <a href="http://groups.google.com/group/mozilla.dev.apps.firefox">Google Groups</a> for ease of browsing. We're planning on improving the theme for Firefox2, and rather than pursue this effort in a walled garden like last time, we're going to proceed in dev-themes@/mozilla.dev.themes. Come on over and join in!</p>
+
+<p>At the same time, we've been encouraging other projects to use the newsgroups/lists too. Decisions made in private email, IRC (which isn't archived anywhere) even in public bugs etc make it very difficult for people who aren't central to the project to find out more or participate. I think we should strive to strike a better balance between convenience and accessibility/referencability. </p>
+
+<p>On top of this, there is a need to make the contact portions of the web site more accurate, relevant and easy to find, so people can easily find the list they want, and the person or group to contact. </p>
+
+<p>We've been having discussions about all of this in <a href="http://groups.google.com/group/mozilla.dev.general">mozilla.dev.general</a>, in <a href="http://groups.google.com/group/mozilla.dev.general/browse_thread/thread/899917f713861f06/4ae6d094ffee5ae7">these</a> <a href="http://groups.google.com/group/mozilla.dev.general/browse_thread/thread/5d1bf2bbc769919d/210246003f1f6fea">threads</a>. Rather than talk in a vacuum of only ourselves, I really hope that those of you that have experienced difficulty in the past in some of these areas will come forward and contribute to the discussion. </p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-03-20T09:23:24-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009914.html">
+<title>Reflection</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009914.html</link>
+<description><![CDATA[<p>I've been doing a bit of it lately. All manner of topics. We recently moved and things have been chaotic so it's been nice to take the time to think. I've had a chance to look at how far we've come, and form some ideas about what we might need to do to go the right places. </p>
+
+<p>The past couple of years have brought some immense highs, and some considerable angst. With success has come the realization that true now as ever: the spirit of open source is expressed through the creative freedom of the many. The surest way to navigate the murky waters of increased attention and marketshare and such like is, as Leslie has been saying for some time, to keep your karma clean. Do the right thing, not only in technical matters but also relationships. </p>
+
+<p>For the Mozilla project, what we need to do (I think) is:</p>
+
+<ul>
+ <li>Better define the things that are important to us. The things that define who we are. Impart the positive aspects of open development culture and practice on everyone involved because they're effective, and as a safeguard against recurrence of some of the <a href="http://weblogs.mozillazine.org/ben/archives/009698.html">troubles of the past</a>.
+ <li>Engage the community more effectively. Create and maintain an infrastructure of open communication to remove the "mystery" behind the decision making process. Organize our contributor materials better to make the project more accessible to newcomers. These are just a couple of examples.
+</ul>
+
+<p>For my part, I'm starting out this year by doing things <a href="http://www.producingoss.com/">a little differently</a>. I think we need to grow more as a project. I'm hopeful that I'll be able to achieve some positive change. </p>
+
+<p>I understand that this post might seem a little abstract. I think what I'm saying might become a bit more clear after I talk about some tangible efforts, which I will do in future entries.</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-03-19T02:00:07-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009804.html">
+<title>Bye Bye Blackberry?</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009804.html</link>
+<description><![CDATA[<p><a href="http://news.com.com/Bye-bye%2C+BlackBerry+-+page+2/2100-1047_3-6042308-2.html?tag=st.num">Bye Bye, Blackberry?</a></p>
+
+<p>I cannot believe people are discussing life without these things. It's like this: I have a patent on television. I don't plan on doing anything with it, but I'm going to shut TV down for all of you, and you're going to sit about and think about life without TV? What's wrong with people?! Is this the world we all want to live in, where people without the interest or capability to pursue technology can hold everyone else captive? That's not the world I want to live in. <br />
+</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-02-24T10:57:32-08:00</dc:date>
+</item>
+<item rdf:about="http://weblogs.mozillazine.org/ben/archives/009774.html">
+<title>More on Memory</title>
+<link>http://weblogs.mozillazine.org/ben/archives/009774.html</link>
+<description><![CDATA[<p>Firefox's caching behavior is just one area of memory usage. I'm really glad that there's been such a lot of discussion in the previous post I made, since many people have raised specific issues, bugs have been filed, and people are looking at the things people are reporting. This sort of feedback system is one of the things that makes the open development model great. Firefox 2 will be much better because of your help!</p>]]></description>
+<dc:subject></dc:subject>
+<dc:creator>ben</dc:creator>
+<dc:date>2006-02-17T23:44:03-08:00</dc:date>
+</item>
+
+
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/full_feed_unknown_extension.xml b/toolkit/components/feeds/test/xml/rss1/full_feed_unknown_extension.xml
new file mode 100644
index 0000000000..c2c875b14f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/full_feed_unknown_extension.xml
@@ -0,0 +1,55 @@
+<!--
+
+Description: rss1 unknown elements does not cause error
+Expect: feed.items.length == 1
+
+-->
+<rdf:RDF
+xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+xmlns:dc="http://purl.org/dc/elements/1.1/"
+xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+xmlns:admin="http://webns.net/mvcb/"
+xmlns:cc="http://web.resource.org/cc/"
+xmlns="http://purl.org/rss/1.0/">
+
+<channel rdf:about="http://example.org/">
+<title>fooo</title>
+<link>http://weblogs.example.org/</link>
+<description>fooooooo</description>
+<dc:language>en-us</dc:language>
+<dc:creator></dc:creator>
+<dc:date>2006-04-10T08:38:18-08:00</dc:date>
+<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.2" />
+
+
+
+
+
+
+
+<foo:bar xmlns:foo="http://example.org">baz</foo:bar>
+
+
+
+
+
+
+
+
+
+
+<items>
+<rdf:Seq>
+<rdf:li rdf:resource="http://weblogs.example.org/archives/009698.html" />
+</rdf:Seq>
+
+</items>
+
+</channel>
+<item rdf:about="http://weblogs.example.org/archives/009698.html">
+<title>Come From?</title>
+<link>http://example.org/009698.html</link>
+<description><![CDATA[much of the code]]></description>
+<dc:date>2006-02-06T10:19:03-08:00</dc:date>
+</item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/item_2_dc_description.xml b/toolkit/components/feeds/test/xml/rss1/item_2_dc_description.xml
new file mode 100644
index 0000000000..03a4bb548d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_2_dc_description.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item 2 dc:description
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).fields.getProperty('dc:description') == 'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </dc:description>
+ <!-- <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher.xml b/toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher.xml
new file mode 100644
index 0000000000..d21734d6ff
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item 2 dc:publisher
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).fields.getProperty('dc:publisher') == 'The O\'Reilly Network'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </dc:description>
+ <!--
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher_extra_att_invalid_rdf.xml b/toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher_extra_att_invalid_rdf.xml
new file mode 100644
index 0000000000..dac0d1dc84
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_2_dc_publisher_extra_att_invalid_rdf.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item 2 dc:publisher
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).fields.getProperty('dc:publisher') == 'The O\'Reilly Network'
+
+-->
+<rdf:RDF
+ xmlns:foo="http://example.org"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </dc:description>
+ <!--
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+ <dc:publisher foo:bar="baz">The O'Reilly Network</dc:publisher>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_count.xml b/toolkit/components/feeds/test/xml/rss1/item_count.xml
new file mode 100644
index 0000000000..69520fb143
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_count.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ 1 item count
+Expect: feed.items.length == 2
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <!-- <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_dc_creator.xml b/toolkit/components/feeds/test/xml/rss1/item_dc_creator.xml
new file mode 100644
index 0000000000..71ecd6d777
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_dc_creator.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item dc:description
+Expect: var author = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).authors.queryElementAt(0, Components.interfaces.nsIFeedPerson); (author.name == "Simon St.Laurent" && author.email == "simonstl@simonstl.com")
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </dc:description>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>
+ </item>
+ <item>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_dc_description.xml b/toolkit/components/feeds/test/xml/rss1/item_dc_description.xml
new file mode 100644
index 0000000000..3d7880e8ce
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_dc_description.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item dc:description
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('dc:description') == 'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </dc:description>
+ <!--
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_dc_description_normalized.xml b/toolkit/components/feeds/test/xml/rss1/item_dc_description_normalized.xml
new file mode 100644
index 0000000000..d57477a997
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_dc_description_normalized.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item dc:description
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.plainText() == 'XML is...'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <dc:description>XML is...</dc:description>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </description>
+
+ <!--
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet 2.
+ </dc:description>
+ <title>XML: A Disruptive Technology</title>
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/item_description.xml b/toolkit/components/feeds/test/xml/rss1/item_description.xml
new file mode 100644
index 0000000000..db4a0137ff
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_description.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item desc normalized
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.plainText() == 'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <description>
+ XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.
+ </description>
+ <!-- <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/item_id.xml b/toolkit/components/feeds/test/xml/rss1/item_id.xml
new file mode 100644
index 0000000000..50048b0a35
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_id.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item rdf:about
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).id == 'http://example.com/hmm'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item rdf:about="http://example.com/hmm">
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <!-- <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/item_link.xml b/toolkit/components/feeds/test/xml/rss1/item_link.xml
new file mode 100644
index 0000000000..8ead15fb6a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_link.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item link
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('rss1:link') == 'http://c.moreover.com/click/here.pl?r123'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <!-- <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_link_normalized.xml b/toolkit/components/feeds/test/xml/rss1/item_link_normalized.xml
new file mode 100644
index 0000000000..dbf9749ac3
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_link_normalized.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item link
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == 'http://c.moreover.com/click/here.pl?r123'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <!-- <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_title.xml b/toolkit/components/feeds/test/xml/rss1/item_title.xml
new file mode 100644
index 0000000000..b4d4b8e3eb
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_title.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item title
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('rss1:title') == 'XML: A Disruptive Technology'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <!-- <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss1/item_title_normalized.xml b/toolkit/components/feeds/test/xml/rss1/item_title_normalized.xml
new file mode 100644
index 0000000000..a9270982f3
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_title_normalized.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item title normalized
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).title.text == 'XML: A Disruptive Technology'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <!-- <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/item_updated_dcterms.xml b/toolkit/components/feeds/test/xml/rss1/item_updated_dcterms.xml
new file mode 100644
index 0000000000..d8f15ee32e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_updated_dcterms.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed updated
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:dcterms='http://purl.org/dc/terms/'>
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ <dcterms:modified>Sat, 07 Sep 2002 00:00:01 GMT</dcterms:modified>
+ </channel>
+ <item rdf:about="http://example.com/hmm">
+ <title>XML: A Disruptive Technology</title>
+ <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <dcterms:modified>Sat, 07 Sep 2002 00:00:01 GMT</dcterms:modified>
+ </item>
+</rdf:RDF>
diff --git a/toolkit/components/feeds/test/xml/rss1/item_wiki_importance_extra_att.xml b/toolkit/components/feeds/test/xml/rss1/item_wiki_importance_extra_att.xml
new file mode 100644
index 0000000000..49ba579ddb
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss1/item_wiki_importance_extra_att.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: RSS1 feed w/ item wiki:importance with extra attribute
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).fields.getProperty('wiki:importance') == 'major'
+
+-->
+<rdf:RDF
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://purl.org/rss/1.0/"
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:w="http://purl.org/rss/1.0/modules/wiki/"
+ xmlns:foo="http://example.org">
+ <channel rdf:about="http://www.xml.com/xml/news.rss">
+ <title>Test</title>
+ <link>http://xml.com/pub</link>
+ <dc:description>another description</dc:description>
+ </channel>
+ <item>
+ <title>XML: A Disruptive Technology</title>
+ <!-- <link>http://c.moreover.com/click/here.pl?r123</link>
+ <dc:description>
+ XML is placing increasingly heavy loads on the existing technical
+ infrastructure of the Internet.
+ </dc:description>
+ <dc:publisher>The O'Reilly Network</dc:publisher>
+ <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
+ <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
+ <dc:subject>XML</dc:subject>-->
+ </item>
+ <item>
+ <w:importance foo:bar="baz">major</w:importance>
+ </item>
+</rdf:RDF> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_category.xml b/toolkit/components/feeds/test/xml/rss2/feed_category.xml
new file mode 100644
index 0000000000..5538259957
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_category.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel category works
+Expect: feed.categories.queryElementAt(0, Components.interfaces.nsIPropertyBag).getProperty('term') == 'hmm'
+
+-->
+<rss version="2.0" >
+<channel>
+<category>hmm</category>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_category_count.xml b/toolkit/components/feeds/test/xml/rss2/feed_category_count.xml
new file mode 100644
index 0000000000..881c6e0d92
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_category_count.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel category works w/ domain
+Expect: feed.categories.length == 4
+
+-->
+<rss version="2.0" >
+<channel>
+<category>hmm0</category>
+<category>hmm1</category>
+<category domain="http://example.org">hmm2</category>
+<category>hmm3</category>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_cloud.xml b/toolkit/components/feeds/test/xml/rss2/feed_cloud.xml
new file mode 100644
index 0000000000..caa033548a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_cloud.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel cloud works
+Expect: ((feed.cloud.getProperty('domain')=="rpc.sys.com") && (feed.cloud.getProperty('port')=="80") && (feed.cloud.getProperty('path')=="/RPC2") && (feed.cloud.getProperty('registerProcedure')=="pingMe") && (feed.cloud.getProperty('protocol')=="soap"))
+
+-->
+<rss version="2.0" >
+<channel>
+<cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="pingMe" protocol="soap"/>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_copyright.xml b/toolkit/components/feeds/test/xml/rss2/feed_copyright.xml
new file mode 100644
index 0000000000..2d00e7a14f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_copyright.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.fields.getProperty('copyright') == 'copyright 2006'
+
+-->
+<rss version="2.0" >
+<channel>
+<copyright>copyright 2006</copyright>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_copyright_linebreak.xml b/toolkit/components/feeds/test/xml/rss2/feed_copyright_linebreak.xml
new file mode 100644
index 0000000000..4b9ca5e71f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_copyright_linebreak.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.fields.getProperty('copyright') == 'copyright 2005'
+
+-->
+<rss version="2.0" >
+<channel>
+<copyright>copyright 2005
+
+
+</copyright>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_data_outside_channel.xml b/toolkit/components/feeds/test/xml/rss2/feed_data_outside_channel.xml
new file mode 100644
index 0000000000..bf212760da
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_data_outside_channel.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.fields.getProperty('copyright') == 'copyright 2006'
+
+-->
+<rss version="2.0" >
+<dc:creator xmlns:dc="http://example.org">heynow</dc:creator>
+<channel>
+<copyright>copyright 2006</copyright>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_dc_contributor.xml b/toolkit/components/feeds/test/xml/rss2/feed_dc_contributor.xml
new file mode 100644
index 0000000000..cfb514ccd9
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_dc_contributor.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.contributors.queryElementAt(0, Components.interfaces.nsIFeedPerson).name == 'them';
+
+-->
+<rss version="2.0" >
+<channel xmlns:dc="http://purl.org/dc/elements/1.1/">
+<copyright>copyright 2006</copyright>
+<dc:creator>me</dc:creator>
+<dc:contributor>them</dc:contributor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_dc_creator.xml b/toolkit/components/feeds/test/xml/rss2/feed_dc_creator.xml
new file mode 100644
index 0000000000..8ed1aff7ff
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_dc_creator.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).name == 'me'
+
+-->
+<rss version="2.0" >
+<channel xmlns:dc="http://purl.org/dc/elements/1.1/">
+<copyright>copyright 2006</copyright>
+<dc:creator>me</dc:creator>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_description.xml b/toolkit/components/feeds/test/xml/rss2/feed_description.xml
new file mode 100644
index 0000000000..11463dfe04
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_description.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel description works
+Expect: var desc = feed.fields.getProperty('description'); desc == 'test';
+
+-->
+<rss version="2.0" >
+<channel>
+<description>test</description>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_description_html.xml b/toolkit/components/feeds/test/xml/rss2/feed_description_html.xml
new file mode 100644
index 0000000000..ecb544f6cf
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_description_html.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel description works w/html
+Expect: feed.fields.getProperty('description') == '<b>test</b>'
+
+-->
+<rss version="2.0" >
+<channel>
+<description>&lt;b>test&lt;/b></description>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_description_html_cdata.xml b/toolkit/components/feeds/test/xml/rss2/feed_description_html_cdata.xml
new file mode 100644
index 0000000000..45ee9e92be
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_description_html_cdata.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel description works w/ html & CDATA
+Expect: feed.fields.getProperty('description') == '<b>test</b>'
+
+-->
+<rss version="2.0" >
+<channel>
+<description><![CDATA[<b>test</b>]]></description>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_docs.xml b/toolkit/components/feeds/test/xml/rss2/feed_docs.xml
new file mode 100644
index 0000000000..20323b22f6
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_docs.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel docs works
+Expect: feed.fields.getProperty('docs') == 'http://example.org'
+
+-->
+<rss version="2.0" >
+<channel>
+<docs>http://example.org</docs>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_generator.xml b/toolkit/components/feeds/test/xml/rss2/feed_generator.xml
new file mode 100644
index 0000000000..91ba579b44
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_generator.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel generator works
+Expect: feed.generator.agent == 'a generator used to make feeds'
+
+-->
+<rss version="2.0" >
+<channel>
+<generator>a generator used to make feeds</generator>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_image_desc.xml b/toolkit/components/feeds/test/xml/rss2/feed_image_desc.xml
new file mode 100644
index 0000000000..7f665641b2
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_image_desc.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel image description and required fields work
+Expect: ((feed.image.getProperty('title') == 'A picture') && (feed.image.getProperty('link') == 'http://example.org') && (feed.image.getProperty('url') == 'http://example.org/a.jpg') && (feed.image.getProperty('description') == 'Yo!'))
+
+-->
+<rss version="2.0" >
+<channel>
+<image>
+ <link>http://example.org</link>
+ <title>A picture</title>
+ <url>http://example.org/a.jpg</url>
+ <description>Yo!</description>
+</image>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_image_desc_width_height.xml b/toolkit/components/feeds/test/xml/rss2/feed_image_desc_width_height.xml
new file mode 100644
index 0000000000..2be53e86cc
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_image_desc_width_height.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel image required fields work
+Expect: ((feed.image.getProperty('title') == 'A picture') && (feed.image.getProperty('link') == 'http://example.org') && (feed.image.getProperty('url') == 'http://example.org/a.jpg') && (feed.image.getProperty('description') == 'Yo!') && (feed.image.getProperty('width') == '42') && (feed.image.getProperty('height') == '43'))
+
+-->
+<rss version="2.0" >
+<channel>
+<image>
+ <link>http://example.org</link>
+ <title>A picture</title>
+ <url>http://example.org/a.jpg</url>
+ <description>Yo!</description>
+ <width>42</width>
+ <height>43</height>
+</image>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_image_required.xml b/toolkit/components/feeds/test/xml/rss2/feed_image_required.xml
new file mode 100644
index 0000000000..9035523a1a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_image_required.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel image required fields work
+Expect: ((feed.image.getProperty('title') == 'A picture') && (feed.image.getProperty('link') == 'http://example.org') && (feed.image.getProperty('url') == 'http://example.org/a.jpg'))
+
+-->
+<rss version="2.0" >
+<channel>
+<image>
+ <link>http://example.org</link>
+ <title>A picture</title>
+ <url>http://example.org/a.jpg</url>
+</image>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_language.xml b/toolkit/components/feeds/test/xml/rss2/feed_language.xml
new file mode 100644
index 0000000000..d4047a99fd
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_language.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel language works
+Expect: feed.fields.getProperty('language') == 'en-us'
+
+-->
+<rss version="2.0" >
+<channel>
+<language>en-us</language>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_lastBuildDate.xml b/toolkit/components/feeds/test/xml/rss2/feed_lastBuildDate.xml
new file mode 100644
index 0000000000..fee9ff5826
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_lastBuildDate.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel lastBuildDate works
+Expect: feed.fields.getProperty('lastBuildDate') == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<lastBuildDate>Sat, 07 Sep 2002 00:00:01 GMT</lastBuildDate>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_linebreak_link.xml b/toolkit/components/feeds/test/xml/rss2/feed_linebreak_link.xml
new file mode 100644
index 0000000000..1af6550f91
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_linebreak_link.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel link works w/ line break
+Expect: feed.link.spec == 'http://mozilla.org/'
+
+-->
+<rss version="2.0" >
+<channel>
+<link>
+http://mozilla.org/
+</link>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_link.xml b/toolkit/components/feeds/test/xml/rss2/feed_link.xml
new file mode 100644
index 0000000000..a4874e9cad
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_link.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel link works
+Expect: feed.link.spec == 'http://mozilla.org/'
+
+-->
+<rss version="2.0" >
+<channel>
+<link>http://mozilla.org/</link>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_link_cdata.xml b/toolkit/components/feeds/test/xml/rss2/feed_link_cdata.xml
new file mode 100644
index 0000000000..5031ac1c3c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_link_cdata.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel link works w/ CDATA
+Expect: feed.link.spec == 'http://mozilla.org/'
+
+-->
+<rss version="2.0" >
+<channel>
+<link>
+ <![CDATA[http://mozilla.org/]]>
+</link>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_managingEditor.xml b/toolkit/components/feeds/test/xml/rss2/feed_managingEditor.xml
new file mode 100644
index 0000000000..3f7060449e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_managingEditor.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel managingEditor works
+Expect: feed.authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).email == 'example@example.com'
+
+-->
+<rss version="2.0" >
+<channel>
+<managingEditor>example@example.com</managingEditor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_managingEditor_extra_att.xml b/toolkit/components/feeds/test/xml/rss2/feed_managingEditor_extra_att.xml
new file mode 100644
index 0000000000..017a4595f0
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_managingEditor_extra_att.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel managingEditor works
+Expect: feed.authors.queryElementAt(0, Components.interfaces.nsIFeedPerson).email == 'example@example.com'
+
+-->
+<rss version="2.0" >
+<channel>
+<managingEditor foo:bar="baz" xmlns:foo="http://example.org">example@example.com</managingEditor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_multiple_categories.xml b/toolkit/components/feeds/test/xml/rss2/feed_multiple_categories.xml
new file mode 100644
index 0000000000..538362b6ad
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_multiple_categories.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel category works
+Expect: feed.categories.queryElementAt(2, Components.interfaces.nsIPropertyBag).getProperty('term') == 'hmm2'
+
+-->
+<rss version="2.0" >
+<channel>
+<category>hmm0</category>
+<category>hmm1</category>
+<category>hmm2</category>
+<category>hmm3</category>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_pubDate.xml b/toolkit/components/feeds/test/xml/rss2/feed_pubDate.xml
new file mode 100644
index 0000000000..42cea68323
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_pubDate.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel pubDate works
+Expect: feed.fields.getProperty('pubDate') == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Sat, 07 Sep 2002 00:00:01 GMT</pubDate>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_pubDate_invalid.xml b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_invalid.xml
new file mode 100644
index 0000000000..48cce256ad
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_invalid.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: non-RFC822 date should produce null feed.updated
+Expect: feed.updated == null
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Satmonkey, 07 Sepmonkey 2002 00:00:01 GMT</pubDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_1.xml b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_1.xml
new file mode 100644
index 0000000000..846ef22a61
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_1.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: Test whether date parsing handles partly-abbreviated dates.
+Expect: feed.updated == 'Tue, 25 Apr 2006 08:00:00 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Tues, 25 Apri 2006 08:00:00 GMT</pubDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_2.xml b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_2.xml
new file mode 100644
index 0000000000..d1ee96f367
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_nonRFC822_2.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: Test whether date parsing handles unabbreviated dates.
+Expect: feed.updated == 'Sat, 25 Nov 2006 00:12:45 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Saturday 25 November 2006 10:12:45 +1000</pubDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_pubDate_timezoneZ.xml b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_timezoneZ.xml
new file mode 100644
index 0000000000..b31ed5c56c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_timezoneZ.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: Test whether date parsing handles "Z" as a timezone in RFC822.
+Expect: feed.updated == 'Tue, 25 Apr 2006 08:00:00 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Tue, 25 Apr 2006 08:00:00 Z</pubDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_pubDate_utc.xml b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_utc.xml
new file mode 100644
index 0000000000..8eff560460
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_pubDate_utc.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: Test whether date parsing handles 'UTC' as a timezone.
+Expect: feed.updated == 'Mon, 16 Apr 2007 03:12:45 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Monday, 16 April 2007 03:12:45 UTC</pubDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_rating.xml b/toolkit/components/feeds/test/xml/rss2/feed_rating.xml
new file mode 100644
index 0000000000..dd5f8f9e5e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_rating.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel rating works
+Expect: feed.fields.getProperty('rating') == '(PICS-1.1 "http://www.rsac.org/ratingsv01.html" l by "webmaster@example.com" on "2006.01.29T10:09-0800" r (n 0 s 0 v 0 l 0))'
+
+-->
+<rss version="2.0" >
+<channel>
+<rating>(PICS-1.1 "http://www.rsac.org/ratingsv01.html" l by "webmaster@example.com" on "2006.01.29T10:09-0800" r (n 0 s 0 v 0 l 0))</rating>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_single_quote_stylesheet_pi.xml b/toolkit/components/feeds/test/xml/rss2/feed_single_quote_stylesheet_pi.xml
new file mode 100644
index 0000000000..10141fe7ef
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_single_quote_stylesheet_pi.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+
+Description: stylesheets with single quoted attributes work. Bug 352549.
+Base: http://www.penny-arcade.com/rss.xml
+Expect: result.stylesheet.spec == "http://www.penny-arcade.com/stylesheets/rss-display.xsl"
+
+-->
+<?xml-stylesheet type='text/xsl' href='/stylesheets/rss-display.xsl' version='1.0'?>
+<rss version="2.0">
+<channel>
+<title>Penny-Arcade</title>
+<link>http://www.penny-arcade.com/</link>
+<language>en-us</language>
+
+<copyright>Copyright 1999 - 2006 Penny Arcade, Inc.</copyright>
+<image>
+<url>http://www.penny-arcade.com/images/rss-logo.png</url>
+<title>Penny-Arcade Logo</title>
+<link>http://www.penny-arcade.com/</link>
+<width>144</width>
+
+<height>82</height>
+</image>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_skipDays.xml b/toolkit/components/feeds/test/xml/rss2/feed_skipDays.xml
new file mode 100644
index 0000000000..8c6ffa9e46
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_skipDays.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel skipDays works
+Expect: ((feed.skipDays.queryElementAt(0, Components.interfaces.nsISupportsString) == 'Sunday') && (feed.skipDays.queryElementAt(1, Components.interfaces.nsISupportsString) == 'Monday'))
+
+-->
+<rss version="2.0" >
+<channel>
+<skipDays>
+ <day>Sunday</day>
+ <day>Monday</day>
+</skipDays>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_skipHours.xml b/toolkit/components/feeds/test/xml/rss2/feed_skipHours.xml
new file mode 100644
index 0000000000..a20372eda7
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_skipHours.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel skipHours works
+Expect: ((feed.skipHours.queryElementAt(0, Components.interfaces.nsISupportsString) == '0') && (feed.skipHours.queryElementAt(4, Components.interfaces.nsISupportsString) == '23'))
+
+-->
+<rss version="2.0" >
+<channel>
+<skipHours>
+ <hour>0</hour>
+ <hour>1</hour>
+ <hour>2</hour>
+ <hour>22</hour>
+ <hour>23</hour>
+</skipHours>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_subtitle.xml b/toolkit/components/feeds/test/xml/rss2/feed_subtitle.xml
new file mode 100644
index 0000000000..a98a3c19ed
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_subtitle.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel description works
+Expect: feed.subtitle.text == 'test'
+
+-->
+<rss version="2.0" >
+<channel>
+<description>test</description>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_subtitle_html.xml b/toolkit/components/feeds/test/xml/rss2/feed_subtitle_html.xml
new file mode 100644
index 0000000000..fca819d36d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_subtitle_html.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel description works
+Expect: feed.subtitle.plainText() == '<i><b>test</b></i>'
+
+-->
+<rss version="2.0" >
+<channel>
+<description><![CDATA[<i><b>test</b></i>]]></description>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_subtitle_markup_stripped.xml b/toolkit/components/feeds/test/xml/rss2/feed_subtitle_markup_stripped.xml
new file mode 100644
index 0000000000..54f6a623cb
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_subtitle_markup_stripped.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel description markup is not HTML
+Expect: feed.subtitle.plainText() == '<i><b>test</b></i>'
+
+-->
+<rss version="2.0" >
+<channel>
+<description><![CDATA[<i><b>test</b></i>]]></description>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_textinput.xml b/toolkit/components/feeds/test/xml/rss2/feed_textinput.xml
new file mode 100644
index 0000000000..4617d62bc0
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_textinput.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel textInput works
+Expect: ((feed.textInput.getProperty('description') == 'Your aggregator supports the textInput element. What software are you using?') && (feed.textInput.getProperty('link') == 'http://www.cadenhead.org/textinput.php') && (feed.textInput.getProperty('name') == 'query') && (feed.textInput.getProperty('title') == 'TextInput Inquiry'))
+
+-->
+<rss version="2.0" >
+<channel>
+<textInput>
+ <description>Your aggregator supports the textInput element. What software are you using?</description>
+ <link>http://www.cadenhead.org/textinput.php</link>
+ <name>query</name>
+ <title>TextInput Inquiry</title>
+</textInput>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_title.xml b/toolkit/components/feeds/test/xml/rss2/feed_title.xml
new file mode 100644
index 0000000000..1f7d63ad26
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_title.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel title works
+Expect: feed.title.plainText() == 'test title'
+
+-->
+<rss version="2.0" >
+<channel>
+<title>test title</title>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_title_cdata_mixed.xml b/toolkit/components/feeds/test/xml/rss2/feed_title_cdata_mixed.xml
new file mode 100644
index 0000000000..19ef62915d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_title_cdata_mixed.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel title works w/ funky CDATA title
+Expect: feed.title.plainText() == 'test title'
+
+-->
+<rss version="2.0" >
+<channel>
+<title>test t<![CDATA[it]]>le</title>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_title_nesting.xml b/toolkit/components/feeds/test/xml/rss2/feed_title_nesting.xml
new file mode 100644
index 0000000000..82492cef74
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_title_nesting.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel title works w/ nested title
+Expect: feed.title.text == 'test title'
+
+-->
+<rss version="2.0" >
+<channel>
+<title>test title</title>
+<bogus><title>bogus title</title></bogus>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_ttl.xml b/toolkit/components/feeds/test/xml/rss2/feed_ttl.xml
new file mode 100644
index 0000000000..57158aff4a
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_ttl.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel ttl works
+Expect: feed.fields.getProperty('ttl') == '60'
+
+-->
+<rss version="2.0" >
+<channel>
+<ttl>60</ttl>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_updated.xml b/toolkit/components/feeds/test/xml/rss2/feed_updated.xml
new file mode 100644
index 0000000000..c9e75e360f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_updated.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel pubDate works
+Expect: feed.updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Sat, 07 Sep 2002 00:00:01 GMT</pubDate>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_updated_dcdate.xml b/toolkit/components/feeds/test/xml/rss2/feed_updated_dcdate.xml
new file mode 100644
index 0000000000..aa69e7800b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_updated_dcdate.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel pubDate works
+Expect: feed.updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<channel>
+<dc:date>Sat, 07 Sep 2002 00:00:01 GMT</dc:date>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate.xml b/toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate.xml
new file mode 100644
index 0000000000..2fb24a7ec7
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel lastBuildDate works
+Expect: feed.updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<lastBuildDate>Sat, 07 Sep 2002 00:00:01 GMT</lastBuildDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate_priority.xml b/toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate_priority.xml
new file mode 100644
index 0000000000..24f024005b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_updated_lastBuildDate_priority.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel lastBuildDate trumps pubDate
+Expect: feed.updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" >
+<channel>
+<pubDate>Sun, 08 Sep 2002 00:00:01 GMT</pubDate>
+<lastBuildDate>Sat, 07 Sep 2002 00:00:01 GMT</lastBuildDate>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_webMaster.xml b/toolkit/components/feeds/test/xml/rss2/feed_webMaster.xml
new file mode 100644
index 0000000000..8878949830
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_webMaster.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel webMaster works
+Expect: feed.fields.getProperty('webMaster') == 'example@example.com'
+
+-->
+<rss version="2.0" >
+<channel>
+<webMaster>example@example.com</webMaster>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_wfw_commentapi.xml b/toolkit/components/feeds/test/xml/rss2/feed_wfw_commentapi.xml
new file mode 100644
index 0000000000..af725064e0
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_wfw_commentapi.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.fields.getProperty('wfw:comment') == 'http://example.org'
+
+-->
+<rss xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0" >
+<channel xmlns:dc="http://purl.org/dc/elements/1.1/">
+<wfw:comment>http://example.org</wfw:comment>
+<copyright>copyright 2006</copyright>
+<dc:creator>me</dc:creator>
+<dc:contributor>them</dc:contributor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_wfw_commentrss.xml b/toolkit/components/feeds/test/xml/rss2/feed_wfw_commentrss.xml
new file mode 100644
index 0000000000..fb1d896091
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_wfw_commentrss.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel copyright works
+Expect: feed.fields.getProperty('wfw:commentRss') == 'http://example.org'
+
+-->
+<rss xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0" >
+<channel xmlns:dc="http://purl.org/dc/elements/1.1/">
+<copyright>copyright 2006</copyright>
+<dc:creator>me</dc:creator>
+<wfw:commentRss>http://example.org</wfw:commentRss>
+<dc:contributor>them</dc:contributor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_wiki.xml b/toolkit/components/feeds/test/xml/rss2/feed_wiki.xml
new file mode 100644
index 0000000000..b8cb783b00
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_wiki.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel unusual prefixed ext works
+Expect: feed.fields.getProperty('wiki:version') == '1'
+
+-->
+<rss
+xmlns:w='http://purl.org/rss/1.0/modules/wiki/'
+xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0" >
+<channel xmlns:dc="http://purl.org/dc/elements/1.1/">
+<copyright>copyright 2006</copyright>
+<dc:creator>me</dc:creator>
+<w:version>1</w:version>
+<wfw:commentRss>http://example.org</wfw:commentRss>
+<dc:contributor>them</dc:contributor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/feed_wiki_unusual_prefix.xml b/toolkit/components/feeds/test/xml/rss2/feed_wiki_unusual_prefix.xml
new file mode 100644
index 0000000000..b8cb783b00
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/feed_wiki_unusual_prefix.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel unusual prefixed ext works
+Expect: feed.fields.getProperty('wiki:version') == '1'
+
+-->
+<rss
+xmlns:w='http://purl.org/rss/1.0/modules/wiki/'
+xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0" >
+<channel xmlns:dc="http://purl.org/dc/elements/1.1/">
+<copyright>copyright 2006</copyright>
+<dc:creator>me</dc:creator>
+<w:version>1</w:version>
+<wfw:commentRss>http://example.org</wfw:commentRss>
+<dc:contributor>them</dc:contributor>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/item_author.xml b/toolkit/components/feeds/test/xml/rss2/item_author.xml
new file mode 100644
index 0000000000..ad0c97e2a8
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_author.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item author works
+Expect: var authors = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).authors; var author = authors.queryElementAt(0, Components.interfaces.nsIFeedPerson); ((author.name == 'Joe Bob Briggs'));
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_category.xml b/toolkit/components/feeds/test/xml/rss2/item_category.xml
new file mode 100644
index 0000000000..4795e00bbe
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_category.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item category works
+Expect: var cats = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('categories'); cats.QueryInterface(Components.interfaces.nsIArray); var cat = cats.queryElementAt(0, Components.interfaces.nsIPropertyBag); ((cat.getProperty('domain') == 'foo') && (cat.getProperty('term') == 'bar'));
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<category domain="foo">bar</category>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_comments.xml b/toolkit/components/feeds/test/xml/rss2/item_comments.xml
new file mode 100644
index 0000000000..da1cccc821
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_comments.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item comments works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('comments') == 'http://example.org'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<title>test</title>
+<!--<author>jbb@dallas.example.com (Joe Bob Briggs)</author>-->
+<comments>http://example.org</comments>
+
+<category domain="foo">bar</category>
+
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_content_encoded.xml b/toolkit/components/feeds/test/xml/rss2/item_content_encoded.xml
new file mode 100644
index 0000000000..6611375b67
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_content_encoded.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item title works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).content.plainText() == 'foobar'
+
+-->
+<rss version="2.0" >
+<channel>
+<item xmlns:c="http://purl.org/rss/1.0/modules/content/">
+ <title>test</title>
+ <c:encoded>foobar</c:encoded>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_count.xml b/toolkit/components/feeds/test/xml/rss2/item_count.xml
new file mode 100644
index 0000000000..a69cbc9793
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_count.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: items length is correct
+Expect: feed.items.length == 1
+
+-->
+<rss version="2.0"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+
+<channel>
+<title>Items Test</title>
+<link>http://mozilla.org/</link>
+<description></description>
+
+<dc:language>en-us</dc:language>
+<dc:creator>sayrer@gmail.com</dc:creator>
+<dc:date>2005-12-07T14:48:03-05:00</dc:date>
+
+<item>
+<title>Is the date right?</title>
+<link>http://example.org/dc_date.html</link>
+</item>
+
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/item_count2.xml b/toolkit/components/feeds/test/xml/rss2/item_count2.xml
new file mode 100644
index 0000000000..27df0a932f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_count2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: items length is correct
+Expect: feed.items.length == 2
+
+-->
+<rss version="2.0"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+
+<channel>
+<title>Items Test</title>
+<link>http://mozilla.org/</link>
+<description></description>
+
+<dc:language>en-us</dc:language>
+<dc:creator>sayrer@gmail.com</dc:creator>
+<dc:date>2005-12-07T14:48:03-05:00</dc:date>
+
+<item>
+<title>Is the date right?</title>
+<link>http://example.org/dc_date.html</link>
+</item>
+<item>
+<title>Is the date right?</title>
+<link>http://example.org/dc_date.html</link>
+</item>
+
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/item_description.xml b/toolkit/components/feeds/test/xml/rss2/item_description.xml
new file mode 100644
index 0000000000..7416d48e6e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_description.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item desc encoded works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.text == 'I\'m headed for France. I wasn\'t gonna go this year, but then last week <a href="http://www.imdb.com/title/tt0086525/">Valley Girl</a> came out and I said to myself, Joe Bob, you gotta get out of the country for a while.'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<comments>http://example.org</comments>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_description_2.xml b/toolkit/components/feeds/test/xml/rss2/item_description_2.xml
new file mode 100644
index 0000000000..427726868f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_description_2.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item desc encoded works
+Expect: feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).summary.text == 'I\'m headed for France. I wasn\'t gonna go this year, but then last week <a href="http://www.imdb.com/title/tt0086525/">Valley Girl</a> came out and I said to myself, Joe Bob, you gotta get out of the country for a while.'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+ <description>hmmm</description>
+</item>
+<item>
+<comments>http://example.org</comments>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_description_cdata.xml b/toolkit/components/feeds/test/xml/rss2/item_description_cdata.xml
new file mode 100644
index 0000000000..ef3926f87e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_description_cdata.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item desc CDATA works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.text == 'I\'m headed for France. I wasn\'t gonna go this year, but then last week <a href="http://www.imdb.com/title/tt0086525/">Valley Girl</a> came out and I said to myself, Joe Bob, you gotta get out of the country for a while.'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+<description><![CDATA[I'm headed for France. I wasn't gonna go this year, but then last week <a href="http://www.imdb.com/title/tt0086525/">Valley Girl</a> came out and I said to myself, Joe Bob, you gotta get out of the country for a while.]]></description>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_description_decode_entities.xml b/toolkit/components/feeds/test/xml/rss2/item_description_decode_entities.xml
new file mode 100644
index 0000000000..e259354dcb
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_description_decode_entities.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+Description: item desc encoded, double-escaped entity
+Expect: var summary = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary; summary.plainText() == "test D\u00e9sol\u00e9e";
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<comments>http://example.org</comments>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<category domain="foo">bar</category>
+
+<description>
+ &lt;b>test D&amp;eacute;sol&amp;eacute;e&lt;/b>
+</description>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_description_normalized.xml b/toolkit/components/feeds/test/xml/rss2/item_description_normalized.xml
new file mode 100644
index 0000000000..9819deb36f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_description_normalized.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item desc encoded, normalied works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.text == 'I\'m headed for France. I wasn\'t gonna go this year, but then last week <a href="http://www.imdb.com/title/tt0086525/">Valley Girl</a> came out and I said to myself, Joe Bob, you gotta get out of the country for a while.'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<comments>http://example.org</comments>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_description_normalized_nohtml.xml b/toolkit/components/feeds/test/xml/rss2/item_description_normalized_nohtml.xml
new file mode 100644
index 0000000000..0a34a008d9
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_description_normalized_nohtml.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item desc encoded, normalized works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.plainText() == 'I\'m headed for France. I wasn\'t gonna go this year, but then last week Valley Girl came out and I said to myself, Joe Bob, you gotta get out of the country for a while.'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<comments>http://example.org</comments>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_enclosure.xml b/toolkit/components/feeds/test/xml/rss2/item_enclosure.xml
new file mode 100644
index 0000000000..2e38a370a1
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_enclosure.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item enclosure works
+Expect: var enc = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('enclosure'); enc.QueryInterface(Components.interfaces.nsIPropertyBag); ((enc.getProperty('length') == '24986239') && (enc.getProperty('type') == 'audio/mpeg') && (enc.getProperty('url') == 'http://dallas.example.com/joebob_050689.mp3') && (feed.type == 1) && (feed.enclosureCount == 1));
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates.xml b/toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates.xml
new file mode 100644
index 0000000000..a9bcd2cc9b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: feed with duplicate enclosures on a single item
+Expect: ((feed.type == 4) && (feed.enclosureCount == 1));
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+
+<item>
+<enclosure length="24986239" type="video/mpeg" url="http://dallas.example.com/joebob_050689.mpeg" />
+<media:content fileSize="24986239" type="video/mpeg" url="http://dallas.example.com/joebob_050689.mpeg" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test audio</title>
+
+<category domain="foo">bar</category>
+
+<description>Listen to the words that are coming out of my mouth.</description>
+</item>
+
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates2.xml b/toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates2.xml
new file mode 100644
index 0000000000..51f7caba9c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_enclosure_duplicates2.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: feed with duplicate enclosures on a single item with different data available
+Expect: var enc = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('enclosure'); enc.QueryInterface(Components.interfaces.nsIPropertyBag); ((enc.getProperty('length') == '24986239') && (enc.getProperty('type') == 'video/mpeg') && (feed.type == 4) && (feed.enclosureCount == 1) );
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+
+<item>
+<enclosure url="http://dallas.example.com/joebob_050689.mpeg" />
+<media:content fileSize="24986239" type="video/mpeg" url="http://dallas.example.com/joebob_050689.mpeg" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test audio</title>
+
+<category domain="foo">bar</category>
+
+<description>Listen to the words that are coming out of my mouth.</description>
+</item>
+
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed.xml b/toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed.xml
new file mode 100644
index 0000000000..7b49e4d742
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: feed with different kinds of enclosures that should be TYPE_FEED (0)
+Expect: ((feed.type == 0) && (feed.enclosureCount == 2));
+
+-->
+<rss version="2.0" >
+<channel>
+
+<item>
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test audio</title>
+
+<category domain="foo">bar</category>
+
+<description>Listen to the words that are coming out of my mouth.</description>
+</item>
+
+<item>
+<enclosure length="3000000" type="video/mpeg" url="http://dallas.example.com/joebob_pants.mpeg" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test video</title>
+
+<category domain="foo">bar</category>
+
+<description>Look into my eyes....</description>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed2.xml b/toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed2.xml
new file mode 100644
index 0000000000..a54a40559f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_enclosure_mixed2.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: feed that doesn't have at least one enclosure per entry should be TYPE_FEED (0)
+Expect: ((feed.type == 0) && (feed.enclosureCount == 1));
+
+-->
+<rss version="2.0" >
+<channel>
+
+<item>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>no video this week!</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm on a trip to the moon this week for this year's Spaceshot Vlogger
+conference. No video this week!</description>
+</item>
+
+<item>
+<enclosure length="3000000" type="video/mpeg" url="http://dallas.example.com/joebob_pants.mpeg" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>orange keyboard</title>
+
+<category domain="foo">bar</category>
+
+<description>Crazy things happen when you paint your keyboard orange.</description>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid.xml b/toolkit/components/feeds/test/xml/rss2/item_guid.xml
new file mode 100644
index 0000000000..2e7f551ef5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid works
+Expect: var guid = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('guid'); guid.QueryInterface(Components.interfaces.nsIPropertyBag2); guid.getProperty('guid') == 'asdf';
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+<guid>asdf</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_bogus_url.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_bogus_url.xml
new file mode 100644
index 0000000000..2f1b67cedf
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_bogus_url.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item copes with bogus guid
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link == null;
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+<guid isPermaLink="true">xorg</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink.xml
new file mode 100644
index 0000000000..c64341427c
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid works
+Expect: var link = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link; link.spec == 'http://www.example.org/';
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<guid isPermaLink="true">http://www.example.org/</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_default.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_default.xml
new file mode 100644
index 0000000000..7d6ae79584
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_default.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid works
+Expect: var link = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link; link.spec == 'http://www.example.org/';
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<guid>http://www.example.org/</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false.xml
new file mode 100644
index 0000000000..c2a1ad867d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid should not map to link when isPermaLink=false
+Expect: var link = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link; link == null;
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<guid isPermaLink="false">http://www.example.org/</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false_uppercase.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false_uppercase.xml
new file mode 100644
index 0000000000..9ff2505c48
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_false_uppercase.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid should not map to link when isPermaLink=FaLsE
+Expect: var link = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link; link == null;
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<guid isPermaLink="FaLsE">http://www.example.org/</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_true_uppercase.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_true_uppercase.xml
new file mode 100644
index 0000000000..bc4fdc0e94
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_true_uppercase.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid should map to link when isPermaLink=TrUe
+Expect: var link = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link; link.spec == "http://www.example.org/";
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<guid isPermaLink="TrUe">http://www.example.org/</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_unknown_value.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_unknown_value.xml
new file mode 100644
index 0000000000..4dce8c390f
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_isPermaLink_unknown_value.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid should map to link when isPermaLink=meatcake or other unknown values
+Expect: var link = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link; link.spec == "http://www.example.org/";
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<title>test</title>
+<guid isPermaLink="meatcake">http://www.example.org/</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_normalized.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_normalized.xml
new file mode 100644
index 0000000000..e4ac978620
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_normalized.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item guid works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).id == 'asdf';
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+<guid>asdf</guid>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_guid_with_link.xml b/toolkit/components/feeds/test/xml/rss2/item_guid_with_link.xml
new file mode 100644
index 0000000000..d2d3daca57
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_guid_with_link.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item prefers link to guid
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == 'http://link.example.org/';
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+<guid isPermaLink="true">http://www.example.org</guid>
+<link>http://link.example.org/</link>
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_link.xml b/toolkit/components/feeds/test/xml/rss2/item_link.xml
new file mode 100644
index 0000000000..c89f71e776
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_link.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item comments works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('link') == 'http://dallas.example.com/1983/05/06/joebob.htm'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<title>test</title>
+<!--<author>jbb@dallas.example.com (Joe Bob Briggs)</author>-->
+<comments>http://example.org</comments>
+<link>http://dallas.example.com/1983/05/06/joebob.htm</link>
+<category domain="foo">bar</category>
+
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_link_normalized.xml b/toolkit/components/feeds/test/xml/rss2/item_link_normalized.xml
new file mode 100644
index 0000000000..876d7613cf
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_link_normalized.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item link normalized works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).link.spec == 'http://dallas.example.com/1983/05/06/joebob.htm'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+<title>test</title>
+<!--<author>jbb@dallas.example.com (Joe Bob Briggs)</author>-->
+<comments>http://example.org</comments>
+<link>http://dallas.example.com/1983/05/06/joebob.htm</link>
+<category domain="foo">bar</category>
+
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_plain_desc.xml b/toolkit/components/feeds/test/xml/rss2/item_plain_desc.xml
new file mode 100644
index 0000000000..ffb4226f12
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_plain_desc.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item desc plain text works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).summary.text == "I'm headed for France. I wasn't gonna go this year, but then last week \"Valley Girl\" came out and I said to myself, Joe Bob, you gotta get out of the country for a while."
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+<description>I'm headed for France. I wasn't gonna go this year, but then last week "Valley Girl" came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_populated_enclosures.xml b/toolkit/components/feeds/test/xml/rss2/item_populated_enclosures.xml
new file mode 100644
index 0000000000..0a7d60df69
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_populated_enclosures.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item enclosure is added to enclosures array
+Expect: var encs = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).enclosures; encs.QueryInterface(Components.interfaces.nsIArray); var enc = encs.queryElementAt(0, Components.interfaces.nsIPropertyBag2); ((enc.getProperty('length') == '24986239') && (enc.getProperty('type') == 'audio/mpeg') && (enc.getProperty('url') == 'http://dallas.example.com/joebob_050689.mp3'));
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+
+<enclosure length="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_pubDate.xml b/toolkit/components/feeds/test/xml/rss2/item_pubDate.xml
new file mode 100644
index 0000000000..acb4abdcca
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_pubDate.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: entry pubDate works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).published == 'Tue, 02 Sep 2003 00:00:01 GMT'
+
+-->
+<rss version="2.0" xmlns:dcterms="http://purl.org/dc/terms/">
+<channel>
+<item>
+<pubDate>Tue, 02 Sep 2003 00:00:01 GMT</pubDate>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_published.xml b/toolkit/components/feeds/test/xml/rss2/item_published.xml
new file mode 100644
index 0000000000..86bf556027
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_published.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: entry published works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).published == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" xmlns:dcterms="http://purl.org/dc/terms/">
+<channel>
+<item>
+<dcterms:issued>Sat, 07 Sep 2002 00:00:01 GMT</dcterms:issued>
+</item>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/item_title.xml b/toolkit/components/feeds/test/xml/rss2/item_title.xml
new file mode 100644
index 0000000000..0126d7da09
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_title.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item title works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('title') == 'test'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+ <title>test</title>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_title_normalized.xml b/toolkit/components/feeds/test/xml/rss2/item_title_normalized.xml
new file mode 100644
index 0000000000..51de0f7e23
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_title_normalized.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: item title works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).title.text == 'test'
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+ <title>test</title>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/item_updated_dcdate.xml b/toolkit/components/feeds/test/xml/rss2/item_updated_dcdate.xml
new file mode 100644
index 0000000000..dc20cdbc0d
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/item_updated_dcdate.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: channel pubDate works
+Expect: feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).updated == 'Sat, 07 Sep 2002 00:00:01 GMT'
+
+-->
+<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<channel>
+<item>
+<dc:date>Sat, 07 Sep 2002 00:00:01 GMT</dc:date>
+</item>
+</channel>
+</rss> \ No newline at end of file
diff --git a/toolkit/components/feeds/test/xml/rss2/items_2_titles.xml b/toolkit/components/feeds/test/xml/rss2/items_2_titles.xml
new file mode 100644
index 0000000000..3a0d1eb67b
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/items_2_titles.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: 2 items title works
+Expect: ((feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).title.text == 'test') && (feed.items.queryElementAt(1, Components.interfaces.nsIFeedEntry).title.text == 'test #2'))
+
+-->
+<rss version="2.0" >
+<channel>
+<item>
+ <title>test</title>
+</item>
+<item>
+ <title>test #2</title>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_content.xml b/toolkit/components/feeds/test/xml/rss2/mrss_content.xml
new file mode 100644
index 0000000000..62a47cdef0
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_content.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: mrss content works
+Expect: var enc = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getPropertyAsInterface('mediacontent', Components.interfaces.nsIArray).queryElementAt(0, Components.interfaces.nsIPropertyBag); Assert.equal(enc.getProperty('fileSize'), '24986239', 'file size is correct'); Assert.equal(enc.getProperty('type'), 'video/mpeg', 'type is correct'); Assert.equal(enc.getProperty('url'), 'http://dallas.example.com/joebob_050689.mpeg', 'url is correct'); Assert.equal(feed.type, 4, 'Feed type is correct'); Assert.equal(feed.enclosureCount,1, 'Enclosure count is correct'); true;
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+<item>
+
+<media:content fileSize="24986239" type="video/mpeg" url="http://dallas.example.com/joebob_050689.mpeg" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_content2.xml b/toolkit/components/feeds/test/xml/rss2/mrss_content2.xml
new file mode 100644
index 0000000000..a0d740e699
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_content2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: mrss content with a thumbnail
+Expect: var enc = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getPropertyAsInterface('mediacontent', Components.interfaces.nsIArray).queryElementAt(0, Components.interfaces.nsIPropertyBag); Assert.equal(enc.getProperty('fileSize'), '24986239', 'file size is correct'); Assert.equal(enc.getProperty('type'), 'video/mpeg', 'type is correct'); Assert.equal(enc.getProperty('url'), 'http://dallas.example.com/joebob_050689.mpeg', 'url is correct'); Assert.equal(feed.type, 0, 'Feed type is correct'); Assert.equal(feed.enclosureCount,2, 'Enclosure count is correct'); true;
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+<item>
+
+<media:content fileSize="24986239" type="video/mpeg" url="http://dallas.example.com/joebob_050689.mpeg" />
+<media:thumbnail url="http://dallas.example.com/joebob_050689.jpg" width="75" height="50"/>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_content_429049.xml b/toolkit/components/feeds/test/xml/rss2/mrss_content_429049.xml
new file mode 100644
index 0000000000..d13efc94c5
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_content_429049.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: this tests bug 429049. the item with a valid url is added to the enclosures array and the item with an empty url does not.
+Expect: var encs = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).enclosures; encs.QueryInterface(Components.interfaces.nsIArray); (encs.length == 1);
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+
+<item>
+<media:content fileSize="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+<description>no description</description>
+</item>
+
+<item>
+<media:content url="" height="" width=""></media:content>
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test empty</title>
+
+<category domain="foo">bar</category>
+<description>no description</description>
+</item>
+
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_content_multiple.xml b/toolkit/components/feeds/test/xml/rss2/mrss_content_multiple.xml
new file mode 100644
index 0000000000..c391efdd2e
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_content_multiple.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: mrss content with multiple media:content items works
+Expect: var mcs = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('mediacontent'); mcs.QueryInterface(Components.interfaces.nsIArray); var enc1 = mcs.queryElementAt(0, Components.interfaces.nsIPropertyBag); var enc2 = mcs.queryElementAt(1, Components.interfaces.nsIPropertyBag); ((enc1.getProperty('fileSize') == '24986239') && (enc1.getProperty('type') == 'video/mpeg') && (enc1.getProperty('url') == 'http://dallas.example.com/joebob_1.mpeg') && (enc2.getProperty('fileSize') == '30000000') && (enc2.getProperty('type') == 'video/mpeg') && (enc2.getProperty('url') == 'http://dallas.example.com/joebob_2.mpeg') && (feed.type == 4) && (feed.enclosureCount == 2));
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+<item>
+
+<media:content fileSize="24986239" type="video/mpeg" url="http://dallas.example.com/joebob_1.mpeg" />
+<media:content fileSize="30000000" type="video/mpeg" url="http://dallas.example.com/joebob_2.mpeg" />
+
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while. Two videos of that.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_content_populate_enclosure.xml b/toolkit/components/feeds/test/xml/rss2/mrss_content_populate_enclosure.xml
new file mode 100644
index 0000000000..f0718655fb
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_content_populate_enclosure.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: mrss content added to enclosures array
+Expect: var encs = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).enclosures; encs.QueryInterface(Components.interfaces.nsIArray); var enc = encs.queryElementAt(0, Components.interfaces.nsIPropertyBag); ((enc.getProperty('length') == '24986239') && (enc.getProperty('type') == 'audio/mpeg') && (enc.getProperty('url') == 'http://dallas.example.com/joebob_050689.mp3') && (feed.type == 1) && (feed.enclosureCount == 1));
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+<item>
+
+<media:content fileSize="24986239" type="audio/mpeg" url="http://dallas.example.com/joebob_050689.mp3" />
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_group_content.xml b/toolkit/components/feeds/test/xml/rss2/mrss_group_content.xml
new file mode 100644
index 0000000000..da2ddb29c6
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_group_content.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: mrss group content works
+Expect: var mg = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).fields.getProperty('mediagroup'); mg.QueryInterface(Components.interfaces.nsIPropertyBag); var mcs = mg.getProperty("mediacontent"); mcs.QueryInterface(Components.interfaces.nsIArray); var mc1 = mcs.queryElementAt(0, Components.interfaces.nsIPropertyBag); var mc2 = mcs.queryElementAt(1, Components.interfaces.nsIPropertyBag); ((mc1.getProperty('fileSize') == '400') && (mc1.getProperty('type') == 'audio/mpeg') && (mc1.getProperty('url') == 'http://dallas.example.com/joebob_050689_2.mp3') && (mc2.getProperty('fileSize') == '200') && (mc2.getProperty('type') == 'audio/mpeg') && (mc2.getProperty('url') == 'http://dallas.example.com/joebob_050689_1.mp3'));
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+<item>
+
+<media:group>
+ <media:content fileSize="400" type="audio/mpeg" url="http://dallas.example.com/joebob_050689_2.mp3" />
+ <media:content fileSize="200" type="audio/mpeg" url="http://dallas.example.com/joebob_050689_1.mp3" />
+</media:group>
+
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xml/rss2/mrss_group_content_populate_enclosure.xml b/toolkit/components/feeds/test/xml/rss2/mrss_group_content_populate_enclosure.xml
new file mode 100644
index 0000000000..f7b9ebabfc
--- /dev/null
+++ b/toolkit/components/feeds/test/xml/rss2/mrss_group_content_populate_enclosure.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+
+Description: mrss group content works
+Expect: var encs = feed.items.queryElementAt(0, Components.interfaces.nsIFeedEntry).enclosures; encs.QueryInterface(Components.interfaces.nsIArray); var enc1 = encs.queryElementAt(0, Components.interfaces.nsIPropertyBag); var enc2 = encs.queryElementAt(1, Components.interfaces.nsIPropertyBag); ((enc1.getProperty('length') == '400') && (enc1.getProperty('type') == 'audio/mpeg') && (enc1.getProperty('url') == 'http://dallas.example.com/joebob_050689_2.mp3') && (enc2.getProperty('length') == '200') && (enc2.getProperty('type') == 'audio/mpeg') && (enc2.getProperty('url') == 'http://dallas.example.com/joebob_050689_1.mp3'));
+
+
+-->
+<rss xmlns:media="http://search.yahoo.com/mrss" version="2.0" >
+<channel>
+<item>
+
+<media:group>
+ <media:content fileSize="400" type="audio/mpeg" url="http://dallas.example.com/joebob_050689_2.mp3" />
+ <media:content fileSize="200" type="audio/mpeg" url="http://dallas.example.com/joebob_050689_1.mp3" />
+</media:group>
+
+<author>jbb@dallas.example.com (Joe Bob Briggs)</author>
+<comments>http://example.org</comments>
+<title>test</title>
+
+<category domain="foo">bar</category>
+
+<description>I'm headed for France. I wasn't gonna go this year, but then last week &lt;a href="http://www.imdb.com/title/tt0086525/"&gt;Valley Girl&lt;/a&gt; came out and I said to myself, Joe Bob, you gotta get out of the country for a while.</description></item>
+</channel>
+</rss>
diff --git a/toolkit/components/feeds/test/xpcshell.ini b/toolkit/components/feeds/test/xpcshell.ini
new file mode 100644
index 0000000000..fbed5e559f
--- /dev/null
+++ b/toolkit/components/feeds/test/xpcshell.ini
@@ -0,0 +1,209 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ xml/rfc4287/author_namespaces.xml
+ xml/rfc4287/entry_link_IANA.xml
+ xml/rfc4287/feed_author_email_2.xml
+ xml/rfc4287/feed_logo.xml
+ xml/rfc4287/entry_author.xml
+ xml/rfc4287/entry_link_otherURI_alt.xml
+ xml/rfc4287/feed_author_email.xml
+ xml/rfc4287/feed_random_attributes_on_feed_and_entry.xml
+ xml/rfc4287/entry_content_encoded.xml
+ xml/rfc4287/entry_link_payment_alt.xml
+ xml/rfc4287/feed_author_name.xml
+ xml/rfc4287/feed_rights_normalized.xml
+ xml/rfc4287/entry_content_html.xml
+ xml/rfc4287/entry_link_random.xml
+ xml/rfc4287/feed_author_surrounded.xml
+ xml/rfc4287/feed_rights_xhtml_nested_divs.xml
+ xml/rfc4287/entry_content_xhtml_with_markup.xml
+ xml/rfc4287/entry_published.xml
+ xml/rfc4287/feed_author_uri.xml
+ xml/rfc4287/feed_rights_xhtml.xml
+ xml/rfc4287/entry_content_xhtml.xml
+ xml/rfc4287/entry_rights_normalized.xml
+ xml/rfc4287/feed_author.xml
+ xml/rfc4287/feed_subtitle.xml
+ xml/rfc4287/entry_content.xml
+ xml/rfc4287/entry_summary.xml
+ xml/rfc4287/feed_comment_rss_extra_att.xml
+ xml/rfc4287/feed_tantek_title.xml
+ xml/rfc4287/entry_contributor.xml
+ xml/rfc4287/entry_title_normalized.xml
+ xml/rfc4287/feed_contributor.xml
+ xml/rfc4287/feed_title_full_feed.xml
+ xml/rfc4287/entry_html_cdata.xml
+ xml/rfc4287/entry_title.xml
+ xml/rfc4287/feed_entry_count.xml
+ xml/rfc4287/feed_title_xhtml_entities.xml
+ xml/rfc4287/entry_id.xml
+ xml/rfc4287/entry_updated.xml
+ xml/rfc4287/feed_generator_uri.xml
+ xml/rfc4287/feed_title_xhtml.xml
+ xml/rfc4287/entry_link_2alts_allcore2.xml
+ xml/rfc4287/entry_w_content_encoded.xml
+ xml/rfc4287/feed_generator_uri_xmlbase.xml
+ xml/rfc4287/feed_title.xml
+ xml/rfc4287/entry_link_2alts_allcore.xml
+ xml/rfc4287/entry_xhtml_baseURI_with_amp.xml
+ xml/rfc4287/feed_generator_version.xml
+ xml/rfc4287/feed_updated_invalid.xml
+ xml/rfc4287/entry_link_2alts.xml
+ xml/rfc4287/entry_xmlBase_on_link.xml
+ xml/rfc4287/feed_generator.xml
+ xml/rfc4287/feed_updated_normalized.xml
+ xml/rfc4287/entry_link_alt_extension.xml
+ xml/rfc4287/entry_xmlBase.xml
+ xml/rfc4287/feed_icon.xml
+ xml/rfc4287/feed_updated.xml
+ xml/rfc4287/entry_link_enclosure_populate_enclosures.xml
+ xml/rfc4287/feed_atom_rights_xhtml.xml
+ xml/rfc4287/feed_id_extra_att.xml
+ xml/rfc4287/feed_version.xml
+ xml/rfc4287/entry_link_enclosure.xml
+ xml/rfc4287/feed_author2.xml
+ xml/rfc4287/feed_id.xml
+ xml/rfc4287/feed_xmlBase.xml
+ xml/rss09x/rss090.xml
+ xml/rss09x/rss091_withNS.xml
+ xml/rss09x/rss091.xml
+ xml/rss09x/rss092.xml
+ xml/rss09x/rss093.xml
+ xml/rss09x/rss094.xml
+ xml/rss09x/rssUnknown.xml
+ xml/rss1/feed_bogus_title.xml
+ xml/rss1/feed_description_normalized.xml
+ xml/rss1/feed_description_with_dc_only.xml
+ xml/rss1/feed_description_with_dc.xml
+ xml/rss1/feed_description.xml
+ xml/rss1/feed_generator.xml
+ xml/rss1/feed_id.xml
+ xml/rss1/feed_image.xml
+ xml/rss1/feed_items_length_zero.xml
+ xml/rss1/feed_link_normalized.xml
+ xml/rss1/feed_link.xml
+ xml/rss1/feed_textInput.xml
+ xml/rss1/feed_title_extra_att.xml
+ xml/rss1/feed_title_normalized.xml
+ xml/rss1/feed_title.xml
+ xml/rss1/feed_updated_dctermsmodified.xml
+ xml/rss1/feed_updated.xml
+ xml/rss1/feed_version.xml
+ xml/rss1/full_feed_not_bozo.xml
+ xml/rss1/full_feed_unknown_extension.xml
+ xml/rss1/full_feed.xml
+ xml/rss1/item_2_dc_description.xml
+ xml/rss1/item_2_dc_publisher_extra_att_invalid_rdf.xml
+ xml/rss1/item_2_dc_publisher.xml
+ xml/rss1/item_count.xml
+ xml/rss1/item_dc_creator.xml
+ xml/rss1/item_dc_description_normalized.xml
+ xml/rss1/item_dc_description.xml
+ xml/rss1/item_description.xml
+ xml/rss1/item_id.xml
+ xml/rss1/item_link_normalized.xml
+ xml/rss1/item_link.xml
+ xml/rss1/item_title_normalized.xml
+ xml/rss1/item_title.xml
+ xml/rss1/item_updated_dcterms.xml
+ xml/rss1/item_wiki_importance_extra_att.xml
+ xml/rss2/feed_category_count.xml
+ xml/rss2/feed_category.xml
+ xml/rss2/feed_cloud.xml
+ xml/rss2/feed_copyright_linebreak.xml
+ xml/rss2/feed_copyright.xml
+ xml/rss2/feed_data_outside_channel.xml
+ xml/rss2/feed_dc_contributor.xml
+ xml/rss2/feed_dc_creator.xml
+ xml/rss2/feed_description_html_cdata.xml
+ xml/rss2/feed_description_html.xml
+ xml/rss2/feed_description.xml
+ xml/rss2/feed_docs.xml
+ xml/rss2/feed_generator.xml
+ xml/rss2/feed_image_desc_width_height.xml
+ xml/rss2/feed_image_desc.xml
+ xml/rss2/feed_image_required.xml
+ xml/rss2/feed_language.xml
+ xml/rss2/feed_lastBuildDate.xml
+ xml/rss2/feed_linebreak_link.xml
+ xml/rss2/feed_link_cdata.xml
+ xml/rss2/feed_link.xml
+ xml/rss2/feed_managingEditor_extra_att.xml
+ xml/rss2/feed_managingEditor.xml
+ xml/rss2/feed_multiple_categories.xml
+ xml/rss2/feed_pubDate_invalid.xml
+ xml/rss2/feed_pubDate_nonRFC822_1.xml
+ xml/rss2/feed_pubDate_nonRFC822_2.xml
+ xml/rss2/feed_pubDate_timezoneZ.xml
+ xml/rss2/feed_pubDate_utc.xml
+ xml/rss2/feed_pubDate.xml
+ xml/rss2/feed_rating.xml
+ xml/rss2/feed_single_quote_stylesheet_pi.xml
+ xml/rss2/feed_skipDays.xml
+ xml/rss2/feed_skipHours.xml
+ xml/rss2/feed_subtitle_html.xml
+ xml/rss2/feed_subtitle_markup_stripped.xml
+ xml/rss2/feed_subtitle.xml
+ xml/rss2/feed_textinput.xml
+ xml/rss2/feed_title_cdata_mixed.xml
+ xml/rss2/feed_title_nesting.xml
+ xml/rss2/feed_title.xml
+ xml/rss2/feed_ttl.xml
+ xml/rss2/feed_updated_dcdate.xml
+ xml/rss2/feed_updated_lastBuildDate_priority.xml
+ xml/rss2/feed_updated_lastBuildDate.xml
+ xml/rss2/feed_updated.xml
+ xml/rss2/feed_webMaster.xml
+ xml/rss2/feed_wfw_commentapi.xml
+ xml/rss2/feed_wfw_commentrss.xml
+ xml/rss2/feed_wiki_unusual_prefix.xml
+ xml/rss2/feed_wiki.xml
+ xml/rss2/item_author.xml
+ xml/rss2/item_category.xml
+ xml/rss2/item_comments.xml
+ xml/rss2/item_content_encoded.xml
+ xml/rss2/item_count2.xml
+ xml/rss2/item_count.xml
+ xml/rss2/item_description_2.xml
+ xml/rss2/item_description_cdata.xml
+ xml/rss2/item_description_decode_entities.xml
+ xml/rss2/item_description_normalized_nohtml.xml
+ xml/rss2/item_description_normalized.xml
+ xml/rss2/item_description.xml
+ xml/rss2/item_enclosure_duplicates2.xml
+ xml/rss2/item_enclosure_duplicates.xml
+ xml/rss2/item_enclosure_mixed2.xml
+ xml/rss2/item_enclosure_mixed.xml
+ xml/rss2/item_enclosure.xml
+ xml/rss2/item_guid_bogus_url.xml
+ xml/rss2/item_guid_isPermaLink_default.xml
+ xml/rss2/item_guid_isPermaLink_false_uppercase.xml
+ xml/rss2/item_guid_isPermaLink_false.xml
+ xml/rss2/item_guid_isPermaLink_true_uppercase.xml
+ xml/rss2/item_guid_isPermaLink_unknown_value.xml
+ xml/rss2/item_guid_isPermaLink.xml
+ xml/rss2/item_guid_normalized.xml
+ xml/rss2/item_guid_with_link.xml
+ xml/rss2/item_guid.xml
+ xml/rss2/item_link_normalized.xml
+ xml/rss2/item_link.xml
+ xml/rss2/item_plain_desc.xml
+ xml/rss2/item_populated_enclosures.xml
+ xml/rss2/item_pubDate.xml
+ xml/rss2/item_published.xml
+ xml/rss2/items_2_titles.xml
+ xml/rss2/item_title_normalized.xml
+ xml/rss2/item_title.xml
+ xml/rss2/item_updated_dcdate.xml
+ xml/rss2/mrss_content_429049.xml
+ xml/rss2/mrss_content_multiple.xml
+ xml/rss2/mrss_content_populate_enclosure.xml
+ xml/rss2/mrss_content.xml
+ xml/rss2/mrss_content2.xml
+ xml/rss2/mrss_group_content_populate_enclosure.xml
+ xml/rss2/mrss_group_content.xml
+
+[test_xml.js]
diff --git a/toolkit/components/filepicker/content/filepicker.js b/toolkit/components/filepicker/content/filepicker.js
new file mode 100644
index 0000000000..6f91066ba5
--- /dev/null
+++ b/toolkit/components/filepicker/content/filepicker.js
@@ -0,0 +1,833 @@
+// -*- 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 nsIFilePicker = Components.interfaces.nsIFilePicker;
+const nsIProperties = Components.interfaces.nsIProperties;
+const NS_DIRECTORYSERVICE_CONTRACTID = "@mozilla.org/file/directory_service;1";
+const NS_IOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
+const nsIFileView = Components.interfaces.nsIFileView;
+const NS_FILEVIEW_CONTRACTID = "@mozilla.org/filepicker/fileview;1";
+const nsITreeView = Components.interfaces.nsITreeView;
+const nsILocalFile = Components.interfaces.nsILocalFile;
+const nsIFile = Components.interfaces.nsIFile;
+const NS_LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+const NS_PROMPTSERVICE_CONTRACTID = "@mozilla.org/embedcomp/prompt-service;1";
+
+var sfile = Components.classes[NS_LOCAL_FILE_CONTRACTID].createInstance(nsILocalFile);
+var retvals;
+var filePickerMode;
+var homeDir;
+var treeView;
+var allowURLs;
+
+var textInput;
+var okButton;
+
+var gFilePickerBundle;
+
+// name of new directory entered by the user to be remembered
+// for next call of newDir() in case something goes wrong with creation
+var gNewDirName = { value: "" };
+
+function filepickerLoad() {
+ gFilePickerBundle = document.getElementById("bundle_filepicker");
+
+ textInput = document.getElementById("textInput");
+ okButton = document.documentElement.getButton("accept");
+ treeView = Components.classes[NS_FILEVIEW_CONTRACTID].createInstance(nsIFileView);
+
+ if (window.arguments) {
+ var o = window.arguments[0];
+ retvals = o.retvals; /* set this to a global var so we can set return values */
+ const title = o.title;
+ filePickerMode = o.mode;
+ if (o.displayDirectory) {
+ var directory = o.displayDirectory.path;
+ }
+
+ const initialText = o.defaultString;
+ var filterTitles = o.filters.titles;
+ var filterTypes = o.filters.types;
+ var numFilters = filterTitles.length;
+
+ document.title = title;
+ allowURLs = o.allowURLs;
+
+ if (initialText) {
+ textInput.value = initialText;
+ }
+ }
+
+ if (filePickerMode != nsIFilePicker.modeOpen && filePickerMode != nsIFilePicker.modeOpenMultiple) {
+ var newDirButton = document.getElementById("newDirButton");
+ newDirButton.removeAttribute("hidden");
+ }
+
+ if (filePickerMode == nsIFilePicker.modeGetFolder) {
+ var textInputLabel = document.getElementById("textInputLabel");
+ textInputLabel.value = gFilePickerBundle.getString("dirTextInputLabel");
+ textInputLabel.accessKey = gFilePickerBundle.getString("dirTextInputAccesskey");
+ }
+
+ if ((filePickerMode == nsIFilePicker.modeOpen) ||
+ (filePickerMode == nsIFilePicker.modeOpenMultiple) ||
+ (filePickerMode == nsIFilePicker.modeSave)) {
+
+ /* build filter popup */
+ var filterPopup = document.createElement("menupopup");
+
+ for (var i = 0; i < numFilters; i++) {
+ var menuItem = document.createElement("menuitem");
+ if (filterTypes[i] == "..apps")
+ menuItem.setAttribute("label", filterTitles[i]);
+ else
+ menuItem.setAttribute("label", filterTitles[i] + " (" + filterTypes[i] + ")");
+ menuItem.setAttribute("filters", filterTypes[i]);
+ filterPopup.appendChild(menuItem);
+ }
+
+ var filterMenuList = document.getElementById("filterMenuList");
+ filterMenuList.appendChild(filterPopup);
+ if (numFilters > 0)
+ filterMenuList.selectedIndex = 0;
+ var filterBox = document.getElementById("filterBox");
+ filterBox.removeAttribute("hidden");
+
+ filterMenuList.selectedIndex = o.filterIndex;
+
+ treeView.setFilter(filterTypes[o.filterIndex]);
+
+ } else if (filePickerMode == nsIFilePicker.modeGetFolder) {
+ treeView.showOnlyDirectories = true;
+ }
+
+ // The dialog defaults to an "open" icon, change it to "save" if applicable
+ if (filePickerMode == nsIFilePicker.modeSave)
+ okButton.setAttribute("icon", "save");
+
+ // start out with a filename sort
+ handleColumnClick("FilenameColumn");
+
+ try {
+ setOKAction();
+ } catch (exception) {
+ // keep it set to "OK"
+ }
+
+ // setup the dialogOverlay.xul button handlers
+ retvals.buttonStatus = nsIFilePicker.returnCancel;
+
+ var tree = document.getElementById("directoryTree");
+ if (filePickerMode == nsIFilePicker.modeOpenMultiple)
+ tree.removeAttribute("seltype");
+
+ tree.view = treeView;
+
+ // Start out with the ok button disabled since nothing will be
+ // selected and nothing will be in the text field.
+ okButton.disabled = filePickerMode != nsIFilePicker.modeGetFolder;
+
+ // This allows the window to show onscreen before we begin
+ // loading the file list
+
+ setTimeout(setInitialDirectory, 0, directory);
+}
+
+function setInitialDirectory(directory)
+{
+ // Start in the user's home directory
+ var dirService = Components.classes[NS_DIRECTORYSERVICE_CONTRACTID]
+ .getService(nsIProperties);
+ homeDir = dirService.get("Home", Components.interfaces.nsIFile);
+
+ if (directory) {
+ sfile.initWithPath(directory);
+ if (!sfile.exists() || !sfile.isDirectory())
+ directory = false;
+ }
+ if (!directory) {
+ sfile.initWithPath(homeDir.path);
+ }
+
+ gotoDirectory(sfile);
+}
+
+function onFilterChanged(target)
+{
+ // Do this on a timeout callback so the filter list can roll up
+ // and we don't keep the mouse grabbed while we are refiltering.
+
+ setTimeout(changeFilter, 0, target.getAttribute("filters"));
+}
+
+function changeFilter(filterTypes)
+{
+ window.setCursor("wait");
+ treeView.setFilter(filterTypes);
+ window.setCursor("auto");
+}
+
+function showErrorDialog(titleStrName, messageStrName, file)
+{
+ var errorTitle =
+ gFilePickerBundle.getFormattedString(titleStrName, [file.path]);
+ var errorMessage =
+ gFilePickerBundle.getFormattedString(messageStrName, [file.path]);
+ var promptService =
+ Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+
+ promptService.alert(window, errorTitle, errorMessage);
+}
+
+function openOnOK()
+{
+ var dir = treeView.selectedFiles.queryElementAt(0, nsIFile);
+ if (dir)
+ gotoDirectory(dir);
+
+ return false;
+}
+
+function selectOnOK()
+{
+ var errorTitle, errorMessage, promptService;
+ var ret = nsIFilePicker.returnOK;
+
+ var isDir = false;
+ var isFile = false;
+
+ retvals.filterIndex = document.getElementById("filterMenuList").selectedIndex;
+ retvals.fileURL = null;
+
+ if (allowURLs) {
+ try {
+ var ios = Components.classes[NS_IOSERVICE_CONTRACTID].getService(Components.interfaces.nsIIOService);
+ retvals.fileURL = ios.newURI(textInput.value, null, null);
+ let fileList = [];
+ if (retvals.fileURL instanceof Components.interfaces.nsIFileURL)
+ fileList.push(retvals.fileURL.file);
+ gFilesEnumerator.mFiles = fileList;
+ retvals.files = gFilesEnumerator;
+ retvals.buttonStatus = ret;
+
+ return true;
+ } catch (e) {
+ }
+ }
+
+ var fileList = processPath(textInput.value);
+ if (!fileList) {
+ // generic error message, should probably never happen
+ showErrorDialog("errorPathProblemTitle",
+ "errorPathProblemMessage",
+ textInput.value);
+ return false;
+ }
+
+ var curFileIndex;
+ for (curFileIndex = 0; curFileIndex < fileList.length &&
+ ret != nsIFilePicker.returnCancel; ++curFileIndex) {
+ var file = fileList[curFileIndex].QueryInterface(nsIFile);
+
+ // try to normalize - if this fails we will ignore the error
+ // because we will notice the
+ // error later and show a fitting error alert.
+ try {
+ file.normalize();
+ } catch (e) {
+ // promptService.alert(window, "Problem", "normalize failed, continuing");
+ }
+
+ var fileExists = file.exists();
+
+ if (!fileExists && (filePickerMode == nsIFilePicker.modeOpen ||
+ filePickerMode == nsIFilePicker.modeOpenMultiple)) {
+ showErrorDialog("errorOpenFileDoesntExistTitle",
+ "errorOpenFileDoesntExistMessage",
+ file);
+ return false;
+ }
+
+ if (!fileExists && filePickerMode == nsIFilePicker.modeGetFolder) {
+ showErrorDialog("errorDirDoesntExistTitle",
+ "errorDirDoesntExistMessage",
+ file);
+ return false;
+ }
+
+ if (fileExists) {
+ isDir = file.isDirectory();
+ isFile = file.isFile();
+ }
+
+ switch (filePickerMode) {
+ case nsIFilePicker.modeOpen:
+ case nsIFilePicker.modeOpenMultiple:
+ if (isFile) {
+ if (file.isReadable()) {
+ retvals.directory = file.parent.path;
+ } else {
+ showErrorDialog("errorOpeningFileTitle",
+ "openWithoutPermissionMessage_file",
+ file);
+ ret = nsIFilePicker.returnCancel;
+ }
+ } else if (isDir) {
+ if (!sfile.equals(file)) {
+ gotoDirectory(file);
+ }
+ textInput.value = "";
+ doEnabling();
+ ret = nsIFilePicker.returnCancel;
+ }
+ break;
+ case nsIFilePicker.modeSave:
+ if (isFile) { // can only be true if file.exists()
+ if (!file.isWritable()) {
+ showErrorDialog("errorSavingFileTitle",
+ "saveWithoutPermissionMessage_file",
+ file);
+ ret = nsIFilePicker.returnCancel;
+ } else {
+ // we need to pop up a dialog asking if you want to save
+ var confirmTitle = gFilePickerBundle.getString("confirmTitle");
+ var message =
+ gFilePickerBundle.getFormattedString("confirmFileReplacing",
+ [file.path]);
+
+ promptService = Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+ var rv = promptService.confirm(window, confirmTitle, message);
+ if (rv) {
+ ret = nsIFilePicker.returnReplace;
+ retvals.directory = file.parent.path;
+ } else {
+ ret = nsIFilePicker.returnCancel;
+ }
+ }
+ } else if (isDir) {
+ if (!sfile.equals(file)) {
+ gotoDirectory(file);
+ }
+ textInput.value = "";
+ doEnabling();
+ ret = nsIFilePicker.returnCancel;
+ } else {
+ var parent = file.parent;
+ if (parent.exists() && parent.isDirectory() && parent.isWritable()) {
+ retvals.directory = parent.path;
+ } else {
+ var oldParent = parent;
+ while (!parent.exists()) {
+ oldParent = parent;
+ parent = parent.parent;
+ }
+ errorTitle =
+ gFilePickerBundle.getFormattedString("errorSavingFileTitle",
+ [file.path]);
+ if (parent.isFile()) {
+ errorMessage =
+ gFilePickerBundle.getFormattedString("saveParentIsFileMessage",
+ [parent.path, file.path]);
+ } else {
+ errorMessage =
+ gFilePickerBundle.getFormattedString("saveParentDoesntExistMessage",
+ [oldParent.path, file.path]);
+ }
+ if (!parent.isWritable()) {
+ errorMessage =
+ gFilePickerBundle.getFormattedString("saveWithoutPermissionMessage_dir", [parent.path]);
+ }
+ promptService = Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+ promptService.alert(window, errorTitle, errorMessage);
+ ret = nsIFilePicker.returnCancel;
+ }
+ }
+ break;
+ case nsIFilePicker.modeGetFolder:
+ if (isDir) {
+ retvals.directory = file.parent.path;
+ } else { // if nothing selected, the current directory will be fine
+ retvals.directory = sfile.path;
+ }
+ break;
+ }
+ }
+
+ gFilesEnumerator.mFiles = fileList;
+
+ retvals.files = gFilesEnumerator;
+ retvals.buttonStatus = ret;
+
+ return (ret != nsIFilePicker.returnCancel);
+}
+
+var gFilesEnumerator = {
+ mFiles: null,
+ mIndex: 0,
+
+ hasMoreElements: function()
+ {
+ return (this.mIndex < this.mFiles.length);
+ },
+ getNext: function()
+ {
+ if (this.mIndex >= this.mFiles.length)
+ throw Components.results.NS_ERROR_FAILURE;
+ return this.mFiles[this.mIndex++];
+ }
+};
+
+function onCancel()
+{
+ // Close the window.
+ retvals.buttonStatus = nsIFilePicker.returnCancel;
+ retvals.file = null;
+ retvals.files = null;
+ return true;
+}
+
+function onDblClick(e) {
+ // we only care about button 0 (left click) events
+ if (e.button != 0) return;
+
+ var t = e.originalTarget;
+ if (t.localName != "treechildren")
+ return;
+
+ openSelectedFile();
+}
+
+function openSelectedFile() {
+ var fileList = treeView.selectedFiles;
+ if (fileList.length == 0)
+ return;
+
+ var file = fileList.queryElementAt(0, nsIFile);
+ if (file.isDirectory())
+ gotoDirectory(file);
+ else if (file.isFile())
+ document.documentElement.acceptDialog();
+}
+
+function onClick(e) {
+ var t = e.originalTarget;
+ if (t.localName == "treecol")
+ handleColumnClick(t.id);
+}
+
+function convertColumnIDtoSortType(columnID) {
+ var sortKey;
+
+ switch (columnID) {
+ case "FilenameColumn":
+ sortKey = nsIFileView.sortName;
+ break;
+ case "FileSizeColumn":
+ sortKey = nsIFileView.sortSize;
+ break;
+ case "LastModifiedColumn":
+ sortKey = nsIFileView.sortDate;
+ break;
+ default:
+ dump("unsupported sort column: " + columnID + "\n");
+ sortKey = 0;
+ break;
+ }
+
+ return sortKey;
+}
+
+function handleColumnClick(columnID) {
+ var sortType = convertColumnIDtoSortType(columnID);
+ var sortOrder = (treeView.sortType == sortType) ? !treeView.reverseSort : false;
+ treeView.sort(sortType, sortOrder);
+
+ // set the sort indicator on the column we are sorted by
+ var sortedColumn = document.getElementById(columnID);
+ if (treeView.reverseSort) {
+ sortedColumn.setAttribute("sortDirection", "descending");
+ } else {
+ sortedColumn.setAttribute("sortDirection", "ascending");
+ }
+
+ // remove the sort indicator from the rest of the columns
+ var currCol = sortedColumn.parentNode.firstChild;
+ while (currCol) {
+ if (currCol != sortedColumn && currCol.localName == "treecol")
+ currCol.removeAttribute("sortDirection");
+ currCol = currCol.nextSibling;
+ }
+}
+
+function onKeypress(e) {
+ if (e.keyCode == 8) /* backspace */
+ goUp();
+
+ /* enter is handled by the ondialogaccept handler */
+}
+
+function doEnabling() {
+ if (filePickerMode != nsIFilePicker.modeGetFolder)
+ // Maybe add check if textInput.value would resolve to an existing
+ // file or directory in .modeOpen. Too costly I think.
+ okButton.disabled = (textInput.value == "")
+}
+
+function onTreeFocus(event) {
+ // Reset the button label and enabled/disabled state.
+ onFileSelected(treeView.selectedFiles);
+}
+
+function setOKAction(file) {
+ var buttonLabel;
+ var buttonIcon = "open"; // used in all but one case
+
+ if (file && file.isDirectory()) {
+ document.documentElement.setAttribute("ondialogaccept", "return openOnOK();");
+ buttonLabel = gFilePickerBundle.getString("openButtonLabel");
+ }
+ else {
+ document.documentElement.setAttribute("ondialogaccept", "return selectOnOK();");
+ switch (filePickerMode) {
+ case nsIFilePicker.modeGetFolder:
+ buttonLabel = gFilePickerBundle.getString("selectFolderButtonLabel");
+ break;
+ case nsIFilePicker.modeOpen:
+ case nsIFilePicker.modeOpenMultiple:
+ buttonLabel = gFilePickerBundle.getString("openButtonLabel");
+ break;
+ case nsIFilePicker.modeSave:
+ buttonLabel = gFilePickerBundle.getString("saveButtonLabel");
+ buttonIcon = "save";
+ break;
+ }
+ }
+ okButton.setAttribute("label", buttonLabel);
+ okButton.setAttribute("icon", buttonIcon);
+}
+
+function onSelect(event) {
+ onFileSelected(treeView.selectedFiles);
+}
+
+function onFileSelected(/* nsIArray */ selectedFileList) {
+ var validFileSelected = false;
+ var invalidSelection = false;
+ var file;
+ var fileCount = selectedFileList.length;
+
+ for (var index = 0; index < fileCount; ++index) {
+ file = selectedFileList.queryElementAt(index, nsIFile);
+ if (file) {
+ var path = file.leafName;
+
+ if (path) {
+ var isDir = file.isDirectory();
+ if ((filePickerMode == nsIFilePicker.modeGetFolder) || !isDir) {
+ if (!validFileSelected)
+ textInput.value = "";
+ addToTextFieldValue(path);
+ }
+
+ if (isDir && fileCount > 1) {
+ // The user has selected multiple items, and one of them is
+ // a directory. This is not a valid state, so we'll disable
+ // the ok button.
+ invalidSelection = true;
+ }
+
+ validFileSelected = true;
+ }
+ }
+ }
+
+ if (validFileSelected) {
+ setOKAction(file);
+ okButton.disabled = invalidSelection;
+ } else if (filePickerMode != nsIFilePicker.modeGetFolder)
+ okButton.disabled = (textInput.value == "");
+}
+
+function addToTextFieldValue(path)
+{
+ var newValue = "";
+
+ if (textInput.value == "")
+ newValue = path.replace(/\"/g, "\\\"");
+ else {
+ // Quote the existing text if needed,
+ // then append the new filename (quoted and escaped)
+ if (textInput.value[0] != '"')
+ newValue = '"' + textInput.value.replace(/\"/g, "\\\"") + '"';
+ else
+ newValue = textInput.value;
+
+ newValue = newValue + ' "' + path.replace(/\"/g, "\\\"") + '"';
+ }
+
+ textInput.value = newValue;
+}
+
+function onTextFieldFocus() {
+ setOKAction(null);
+ doEnabling();
+}
+
+function onDirectoryChanged(target)
+{
+ var path = target.getAttribute("label");
+
+ var file = Components.classes[NS_LOCAL_FILE_CONTRACTID].createInstance(nsILocalFile);
+ file.initWithPath(path);
+
+ if (!sfile.equals(file)) {
+ // Do this on a timeout callback so the directory list can roll up
+ // and we don't keep the mouse grabbed while we are loading.
+
+ setTimeout(gotoDirectory, 0, file);
+ }
+}
+
+function populateAncestorList(directory) {
+ var menu = document.getElementById("lookInMenu");
+
+ while (menu.hasChildNodes()) {
+ menu.removeChild(menu.firstChild);
+ }
+
+ var menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("label", directory.path);
+ menuItem.setAttribute("crop", "start");
+ menu.appendChild(menuItem);
+
+ // .parent is _sometimes_ null, see bug 121489. Do a dance around that.
+ var parent = directory.parent;
+ while (parent && !parent.equals(directory)) {
+ menuItem = document.createElement("menuitem");
+ menuItem.setAttribute("label", parent.path);
+ menuItem.setAttribute("crop", "start");
+ menu.appendChild(menuItem);
+ directory = parent;
+ parent = directory.parent;
+ }
+
+ var menuList = document.getElementById("lookInMenuList");
+ menuList.selectedIndex = 0;
+}
+
+function goUp() {
+ try {
+ var parent = sfile.parent;
+ } catch (ex) { dump("can't get parent directory\n"); }
+
+ if (parent) {
+ gotoDirectory(parent);
+ }
+}
+
+function goHome() {
+ gotoDirectory(homeDir);
+}
+
+function newDir() {
+ var file;
+ var promptService =
+ Components.classes[NS_PROMPTSERVICE_CONTRACTID].getService(Components.interfaces.nsIPromptService);
+ var dialogTitle =
+ gFilePickerBundle.getString("promptNewDirTitle");
+ var dialogMsg =
+ gFilePickerBundle.getString("promptNewDirMessage");
+ var ret = promptService.prompt(window, dialogTitle, dialogMsg, gNewDirName, null, {value:0});
+
+ if (ret) {
+ file = processPath(gNewDirName.value);
+ if (!file) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirMessage",
+ file);
+ return false;
+ }
+
+ file = file[0].QueryInterface(nsIFile);
+ if (file.exists()) {
+ showErrorDialog("errorNewDirDoesExistTitle",
+ "errorNewDirDoesExistMessage",
+ file);
+ return false;
+ }
+
+ var parent = file.parent;
+ if (!(parent.exists() && parent.isDirectory() && parent.isWritable())) {
+ while (!parent.exists()) {
+ parent = parent.parent;
+ }
+ if (parent.isFile()) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirIsFileMessage",
+ parent);
+ return false;
+ }
+ if (!parent.isWritable()) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirPermissionMessage",
+ parent);
+ return false;
+ }
+ }
+
+ try {
+ file.create(nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (e) {
+ showErrorDialog("errorCreateNewDirTitle",
+ "errorCreateNewDirMessage",
+ file);
+ return false;
+ }
+ file.normalize(); // ... in case ".." was used in the path
+ gotoDirectory(file);
+ // we remember and reshow a dirname if something goes wrong
+ // so that errors can be corrected more easily. If all went well,
+ // reset the default value to blank
+ gNewDirName = { value: "" };
+ }
+ return true;
+}
+
+function gotoDirectory(directory) {
+ window.setCursor("wait");
+ try {
+ populateAncestorList(directory);
+ treeView.setDirectory(directory);
+ document.getElementById("errorShower").selectedIndex = 0;
+ } catch (ex) {
+ document.getElementById("errorShower").selectedIndex = 1;
+ }
+
+ window.setCursor("auto");
+
+ if (filePickerMode == nsIFilePicker.modeGetFolder) {
+ textInput.value = "";
+ }
+ textInput.focus();
+ textInput.setAttribute("autocompletesearchparam", directory.path);
+ sfile = directory;
+}
+
+function toggleShowHidden(event) {
+ treeView.showHiddenFiles = !treeView.showHiddenFiles;
+}
+
+// from the current directory and whatever was entered
+// in the entry field, try to make a new path. This
+// uses "/" as the directory separator, "~" as a shortcut
+// for the home directory (but only when seen at the start
+// of a path), and ".." to denote the parent directory.
+// returns an array of the files listed,
+// or false if an error occurred.
+function processPath(path)
+{
+ var fileArray = new Array();
+ var strLength = path.length;
+
+ if (path[0] == '"' && filePickerMode == nsIFilePicker.modeOpenMultiple &&
+ strLength > 1) {
+ // we have a quoted list of filenames, separated by spaces.
+ // iterate the list and process each file.
+
+ var curFileStart = 1;
+
+ while (1) {
+ var nextQuote;
+
+ // Look for an unescaped quote
+ var quoteSearchStart = curFileStart + 1;
+ do {
+ nextQuote = path.indexOf('"', quoteSearchStart);
+ quoteSearchStart = nextQuote + 1;
+ } while (nextQuote != -1 && path[nextQuote - 1] == '\\');
+
+ if (nextQuote == -1) {
+ // we have a filename with no trailing quote.
+ // just assume that the filename ends at the end of the string.
+
+ if (!processPathEntry(path.substring(curFileStart), fileArray))
+ return false;
+ break;
+ }
+
+ if (!processPathEntry(path.substring(curFileStart, nextQuote), fileArray))
+ return false;
+
+ curFileStart = path.indexOf('"', nextQuote + 1);
+ if (curFileStart == -1) {
+ // no more quotes, but if we're not at the end of the string,
+ // go ahead and process the remaining text.
+
+ if (nextQuote < strLength - 1)
+ if (!processPathEntry(path.substring(nextQuote + 1), fileArray))
+ return false;
+ break;
+ }
+ ++curFileStart;
+ }
+ } else if (!processPathEntry(path, fileArray)) {
+ // If we didn't start with a quote, assume we just have a single file.
+ return false;
+ }
+
+ return fileArray;
+}
+
+function processPathEntry(path, fileArray)
+{
+ var filePath;
+ var file;
+
+ try {
+ file = sfile.clone().QueryInterface(nsILocalFile);
+ } catch (e) {
+ dump("Couldn't clone\n"+e);
+ return false;
+ }
+
+ var tilde_file = file.clone();
+ tilde_file.append("~");
+ if (path[0] == '~' && // Expand ~ to $HOME, except:
+ !(path == "~" && tilde_file.exists()) && // If ~ was entered and such a file exists, don't expand
+ (path.length == 1 || path[1] == "/")) // We don't want to expand ~file to ${HOME}file
+ filePath = homeDir.path + path.substring(1);
+ else
+ filePath = path;
+
+ // Unescape quotes
+ filePath = filePath.replace(/\\\"/g, "\"");
+
+ if (filePath[0] == '/') /* an absolute path was entered */
+ file.initWithPath(filePath);
+ else if ((filePath.indexOf("/../") > 0) ||
+ (filePath.substr(-3) == "/..") ||
+ (filePath.substr(0, 3) == "../") ||
+ (filePath == "..")) {
+ /* appendRelativePath doesn't allow .. */
+ try {
+ file.initWithPath(file.path + "/" + filePath);
+ } catch (e) {
+ dump("Couldn't init path\n"+e);
+ return false;
+ }
+ }
+ else {
+ try {
+ file.appendRelativePath(filePath);
+ } catch (e) {
+ dump("Couldn't append path\n"+e);
+ return false;
+ }
+ }
+
+ fileArray[fileArray.length] = file;
+ return true;
+}
diff --git a/toolkit/components/filepicker/content/filepicker.xul b/toolkit/components/filepicker/content/filepicker.xul
new file mode 100644
index 0000000000..4bf3112307
--- /dev/null
+++ b/toolkit/components/filepicker/content/filepicker.xul
@@ -0,0 +1,80 @@
+<?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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/filepicker.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://global/locale/filepicker.dtd" >
+
+<dialog id="main-window"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="filepickerLoad();"
+ width="426" height="300"
+ ondialogaccept="return selectOnOK();"
+ ondialogcancel="return onCancel();"
+ persist="screenX screenY width height">
+
+<stringbundle id="bundle_filepicker" src="chrome://global/locale/filepicker.properties"/>
+<script type="application/javascript" src="chrome://global/content/filepicker.js"/>
+
+<hbox align="center">
+ <label value="&lookInMenuList.label;" control="lookInMenuList" accesskey="&lookInMenuList.accesskey;"/>
+ <menulist id="lookInMenuList" flex="1" oncommand="onDirectoryChanged(event.target);" crop="start">
+ <menupopup id="lookInMenu"/>
+ </menulist>
+ <button id="folderUpButton" class="up-button" tooltiptext="&folderUp.tooltiptext;" oncommand="goUp();"/>
+ <button id="homeButton" class="home-button" tooltiptext="&folderHome.tooltiptext;" oncommand="goHome();"/>
+ <button id="newDirButton" hidden="true" class="new-dir-button" tooltiptext="&folderNew.tooltiptext;" oncommand="newDir();"/>
+</hbox>
+
+<hbox flex="1">
+ <deck id="errorShower" flex="1">
+ <tree id="directoryTree" flex="1" class="focusring" seltype="single"
+ onclick="onClick(event);"
+ ondblclick="onDblClick(event);"
+ onkeypress="onKeypress(event);"
+ onfocus="onTreeFocus(event);"
+ onselect="onSelect(event);">
+ <treecols>
+ <treecol id="FilenameColumn" label="&name.label;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="FileSizeColumn" label="&size.label;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="LastModifiedColumn" label="&lastModified.label;" flex="1"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <label>&noPermissionError.label;</label>
+ </deck>
+</hbox>
+
+<grid style="margin-top: 5px">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <label value="&textInput.label;" id="textInputLabel" control="textInput" accesskey="&textInput.accesskey;"/>
+ <textbox id="textInput" flex="1" oninput="doEnabling()"
+ type="autocomplete" autocompletesearch="file"
+ onfocus="onTextFieldFocus();"/>
+ </row>
+ <row id="filterBox" hidden="true" align="center">
+ <label value="&filterMenuList.label;" control="filterMenuList" accesskey="&filterMenuList.accesskey;"/>
+ <menulist id="filterMenuList" flex="1" oncommand="onFilterChanged(event.target);"/>
+ </row>
+ </rows>
+</grid>
+<hbox class="dialog-button-box" align="center">
+ <checkbox label="&showHiddenFiles.label;" oncommand="toggleShowHidden();"
+ flex="1" accesskey="&showHiddenFiles.accesskey;"/>
+ <button dlgtype="cancel" icon="cancel" class="dialog-button"/>
+ <button dlgtype="accept" icon="open" class="dialog-button"/>
+</hbox>
+</dialog>
diff --git a/toolkit/components/filepicker/jar.mn b/toolkit/components/filepicker/jar.mn
new file mode 100644
index 0000000000..ba585f3a41
--- /dev/null
+++ b/toolkit/components/filepicker/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/.
+
+toolkit.jar:
+ content/global/filepicker.xul (content/filepicker.xul)
+ content/global/filepicker.js (content/filepicker.js)
+
diff --git a/toolkit/components/filepicker/moz.build b/toolkit/components/filepicker/moz.build
new file mode 100644
index 0000000000..0990cb00fd
--- /dev/null
+++ b/toolkit/components/filepicker/moz.build
@@ -0,0 +1,25 @@
+# -*- 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_MODULE = 'filepicker'
+XPIDL_SOURCES += [
+ 'nsIFileView.idl',
+]
+SOURCES += [
+ 'nsFileView.cpp',
+]
+EXTRA_COMPONENTS += [
+ 'nsFilePicker.js',
+]
+EXTRA_PP_COMPONENTS += [
+ 'nsFilePicker.manifest',
+]
+XPCSHELL_TESTS_MANIFESTS += [
+ 'test/unit/xpcshell.ini',
+]
+FINAL_LIBRARY = 'xul'
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/filepicker/nsFilePicker.js b/toolkit/components/filepicker/nsFilePicker.js
new file mode 100644
index 0000000000..8c92ff821b
--- /dev/null
+++ b/toolkit/components/filepicker/nsFilePicker.js
@@ -0,0 +1,319 @@
+/* -*- tab-width: 2; 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/. */
+
+/*
+ * 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.)
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const DEBUG = false; /* set to true to enable debug messages */
+
+const LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+const APPSHELL_SERV_CONTRACTID = "@mozilla.org/appshell/appShellService;1";
+const STRBUNDLE_SERV_CONTRACTID = "@mozilla.org/intl/stringbundle;1";
+
+const nsIAppShellService = Components.interfaces.nsIAppShellService;
+const nsILocalFile = Components.interfaces.nsILocalFile;
+const nsIFileURL = Components.interfaces.nsIFileURL;
+const nsISupports = Components.interfaces.nsISupports;
+const nsIFactory = Components.interfaces.nsIFactory;
+const nsIFilePicker = Components.interfaces.nsIFilePicker;
+const nsIInterfaceRequestor = Components.interfaces.nsIInterfaceRequestor;
+const nsIDOMWindow = Components.interfaces.nsIDOMWindow;
+const nsIStringBundleService = Components.interfaces.nsIStringBundleService;
+const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+const nsIDocShellTreeItem = Components.interfaces.nsIDocShellTreeItem;
+const nsIBaseWindow = Components.interfaces.nsIBaseWindow;
+
+var titleBundle = null;
+var filterBundle = null;
+var lastDirectory = null;
+
+function nsFilePicker()
+{
+ if (!titleBundle)
+ titleBundle = srGetStrBundle("chrome://global/locale/filepicker.properties");
+ if (!filterBundle)
+ filterBundle = srGetStrBundle("chrome://global/content/filepicker.properties");
+
+ /* attributes */
+ this.mDefaultString = "";
+ this.mFilterIndex = 0;
+ this.mFilterTitles = new Array();
+ this.mFilters = new Array();
+ this.mDisplayDirectory = null;
+ if (lastDirectory) {
+ try {
+ var dir = Components.classes[LOCAL_FILE_CONTRACTID].createInstance(nsILocalFile);
+ dir.initWithPath(lastDirectory);
+ this.mDisplayDirectory = dir;
+ } catch (e) {}
+ }
+}
+
+nsFilePicker.prototype = {
+ classID: Components.ID("{54ae32f8-1dd2-11b2-a209-df7c505370f8}"),
+
+ QueryInterface: function(iid) {
+ if (iid.equals(nsIFilePicker) ||
+ iid.equals(nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+
+ /* attribute nsILocalFile displayDirectory; */
+ set displayDirectory(a) {
+ this.mDisplayDirectory = a &&
+ a.clone().QueryInterface(nsILocalFile);
+ },
+ get displayDirectory() {
+ return this.mDisplayDirectory &&
+ this.mDisplayDirectory.clone()
+ .QueryInterface(nsILocalFile);
+ },
+
+ /* readonly attribute nsILocalFile file; */
+ get file() { return this.mFilesEnumerator.mFiles[0]; },
+
+ /* readonly attribute nsISimpleEnumerator files; */
+ get files() { return this.mFilesEnumerator; },
+
+ /* we don't support directories, yet */
+ get domFileOrDirectory() {
+ let enumerator = this.domFileOrDirectoryEnumerator;
+ return enumerator ? enumerator.mFiles[0] : null;
+ },
+
+ /* readonly attribute nsISimpleEnumerator domFileOrDirectoryEnumerator; */
+ get domFileOrDirectoryEnumerator() {
+ if (!this.mFilesEnumerator) {
+ return null;
+ }
+
+ if (!this.mDOMFilesEnumerator) {
+ this.mDOMFilesEnumerator = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISimpleEnumerator]),
+
+ mFiles: [],
+ mIndex: 0,
+
+ hasMoreElements: function() {
+ return (this.mIndex < this.mFiles.length);
+ },
+
+ getNext: function() {
+ if (this.mIndex >= this.mFiles.length) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+ return this.mFiles[this.mIndex++];
+ }
+ };
+
+ var utils = this.mParentWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+
+ for (var i = 0; i < this.mFilesEnumerator.mFiles.length; ++i) {
+ var file = utils.wrapDOMFile(this.mFilesEnumerator.mFiles[i]);
+ this.mDOMFilesEnumerator.mFiles.push(file);
+ }
+ }
+
+ return this.mDOMFilesEnumerator;
+ },
+
+ /* readonly attribute nsIURI fileURL; */
+ get fileURL() {
+ if (this.mFileURL)
+ return this.mFileURL;
+
+ if (!this.mFilesEnumerator)
+ return null;
+
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+
+ return this.mFileURL = ioService.newFileURI(this.file);
+ },
+
+ /* attribute wstring defaultString; */
+ set defaultString(a) { this.mDefaultString = a; },
+ get defaultString() { return this.mDefaultString; },
+
+ /* attribute wstring defaultExtension */
+ set defaultExtension(ext) { },
+ get defaultExtension() { return ""; },
+
+ /* attribute long filterIndex; */
+ set filterIndex(a) { this.mFilterIndex = a; },
+ get filterIndex() { return this.mFilterIndex; },
+
+ /* attribute boolean addToRecentDocs; */
+ set addToRecentDocs(a) {},
+ get addToRecentDocs() { return false; },
+
+ /* readonly attribute short mode; */
+ get mode() { return this.mMode; },
+
+ /* members */
+ mFilesEnumerator: undefined,
+ mDOMFilesEnumerator: undefined,
+ mParentWindow: null,
+
+ /* methods */
+ init: function(parent, title, mode) {
+ this.mParentWindow = parent;
+ this.mTitle = title;
+ this.mMode = mode;
+ },
+
+ appendFilters: function(filterMask) {
+ if (filterMask & nsIFilePicker.filterHTML) {
+ this.appendFilter(titleBundle.GetStringFromName("htmlTitle"),
+ filterBundle.GetStringFromName("htmlFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterText) {
+ this.appendFilter(titleBundle.GetStringFromName("textTitle"),
+ filterBundle.GetStringFromName("textFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterImages) {
+ this.appendFilter(titleBundle.GetStringFromName("imageTitle"),
+ filterBundle.GetStringFromName("imageFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterXML) {
+ this.appendFilter(titleBundle.GetStringFromName("xmlTitle"),
+ filterBundle.GetStringFromName("xmlFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterXUL) {
+ this.appendFilter(titleBundle.GetStringFromName("xulTitle"),
+ filterBundle.GetStringFromName("xulFilter"));
+ }
+ this.mAllowURLs = !!(filterMask & nsIFilePicker.filterAllowURLs);
+ if (filterMask & nsIFilePicker.filterApps) {
+ // We use "..apps" as a special filter for executable files
+ this.appendFilter(titleBundle.GetStringFromName("appsTitle"),
+ "..apps");
+ }
+ if (filterMask & nsIFilePicker.filterAudio) {
+ this.appendFilter(titleBundle.GetStringFromName("audioTitle"),
+ filterBundle.GetStringFromName("audioFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterVideo) {
+ this.appendFilter(titleBundle.GetStringFromName("videoTitle"),
+ filterBundle.GetStringFromName("videoFilter"));
+ }
+ if (filterMask & nsIFilePicker.filterAll) {
+ this.appendFilter(titleBundle.GetStringFromName("allTitle"),
+ filterBundle.GetStringFromName("allFilter"));
+ }
+ },
+
+ appendFilter: function(title, extensions) {
+ this.mFilterTitles.push(title);
+ this.mFilters.push(extensions);
+ },
+
+ open: function(aFilePickerShownCallback) {
+ var tm = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService(Components.interfaces.nsIThreadManager);
+ tm.mainThread.dispatch(function() {
+ let result = Components.interfaces.nsIFilePicker.returnCancel;
+ try {
+ result = this.show();
+ } catch (ex) {
+ }
+ if (aFilePickerShownCallback) {
+ aFilePickerShownCallback.done(result);
+ }
+ }.bind(this), Components.interfaces.nsIThread.DISPATCH_NORMAL);
+ },
+
+ show: function() {
+ var o = {};
+ o.title = this.mTitle;
+ o.mode = this.mMode;
+ o.displayDirectory = this.mDisplayDirectory;
+ o.defaultString = this.mDefaultString;
+ o.filterIndex = this.mFilterIndex;
+ o.filters = {};
+ o.filters.titles = this.mFilterTitles;
+ o.filters.types = this.mFilters;
+ o.allowURLs = this.mAllowURLs;
+ o.retvals = {};
+
+ var parent;
+ if (this.mParentWindow) {
+ parent = this.mParentWindow;
+ } else if (typeof(window) == "object" && window != null) {
+ parent = window;
+ } else {
+ try {
+ var appShellService = Components.classes[APPSHELL_SERV_CONTRACTID].getService(nsIAppShellService);
+ parent = appShellService.hiddenDOMWindow;
+ } catch (ex) {
+ debug("Can't get parent. xpconnect hates me so we can't get one from the appShellService.\n");
+ debug(ex + "\n");
+ }
+ }
+
+ try {
+ parent.openDialog("chrome://global/content/filepicker.xul",
+ "",
+ "chrome,modal,titlebar,resizable=yes,dependent=yes",
+ o);
+
+ this.mFilterIndex = o.retvals.filterIndex;
+ this.mFilesEnumerator = o.retvals.files;
+ this.mFileURL = o.retvals.fileURL;
+ lastDirectory = o.retvals.directory;
+ return o.retvals.buttonStatus;
+ } catch (ex) { dump("unable to open file picker\n" + ex + "\n"); }
+
+ return null;
+ }
+}
+
+if (DEBUG)
+ debug = function (s) { dump("-*- filepicker: " + s + "\n"); };
+else
+ debug = function (s) {};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsFilePicker]);
+
+/* crap from strres.js that I want to use for string bundles since I can't include another .js file.... */
+
+var strBundleService = null;
+
+function srGetStrBundle(path)
+{
+ var strBundle = null;
+
+ if (!strBundleService) {
+ try {
+ strBundleService = Components.classes[STRBUNDLE_SERV_CONTRACTID].getService(nsIStringBundleService);
+ } catch (ex) {
+ dump("\n--** strBundleService createInstance failed **--\n");
+ return null;
+ }
+ }
+
+ strBundle = strBundleService.createBundle(path);
+ if (!strBundle) {
+ dump("\n--** strBundle createInstance failed **--\n");
+ }
+ return strBundle;
+}
diff --git a/toolkit/components/filepicker/nsFilePicker.manifest b/toolkit/components/filepicker/nsFilePicker.manifest
new file mode 100644
index 0000000000..06c2e8956f
--- /dev/null
+++ b/toolkit/components/filepicker/nsFilePicker.manifest
@@ -0,0 +1,4 @@
+component {54ae32f8-1dd2-11b2-a209-df7c505370f8} nsFilePicker.js
+#ifndef MOZ_WIDGET_GTK
+contract @mozilla.org/filepicker;1 {54ae32f8-1dd2-11b2-a209-df7c505370f8}
+#endif
diff --git a/toolkit/components/filepicker/nsFileView.cpp b/toolkit/components/filepicker/nsFileView.cpp
new file mode 100644
index 0000000000..ad4471e862
--- /dev/null
+++ b/toolkit/components/filepicker/nsFileView.cpp
@@ -0,0 +1,982 @@
+/* -*- 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 "nsIFileView.h"
+#include "nsITreeView.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsITreeSelection.h"
+#include "nsITreeColumns.h"
+#include "nsITreeBoxObject.h"
+#include "nsIFile.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsCRT.h"
+#include "nsPrintfCString.h"
+#include "nsIDateTimeFormat.h"
+#include "nsQuickSort.h"
+#include "nsIAtom.h"
+#include "nsIAutoCompleteResult.h"
+#include "nsIAutoCompleteSearch.h"
+#include "nsISimpleEnumerator.h"
+#include "nsAutoPtr.h"
+#include "nsIMutableArray.h"
+#include "nsTArray.h"
+#include "mozilla/Attributes.h"
+
+#include "nsWildCard.h"
+
+class nsIDOMDataTransfer;
+
+#define NS_FILECOMPLETE_CID { 0xcb60980e, 0x18a5, 0x4a77, \
+ { 0x91, 0x10, 0x81, 0x46, 0x61, 0x4c, 0xa7, 0xf0 } }
+#define NS_FILECOMPLETE_CONTRACTID "@mozilla.org/autocomplete/search;1?name=file"
+
+class nsFileResult final : public nsIAutoCompleteResult
+{
+public:
+ // aSearchString is the text typed into the autocomplete widget
+ // aSearchParam is the picker's currently displayed directory
+ nsFileResult(const nsAString& aSearchString, const nsAString& aSearchParam);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOCOMPLETERESULT
+
+ nsTArray<nsString> mValues;
+ nsString mSearchString;
+ uint16_t mSearchResult;
+private:
+ ~nsFileResult() {}
+};
+
+NS_IMPL_ISUPPORTS(nsFileResult, nsIAutoCompleteResult)
+
+nsFileResult::nsFileResult(const nsAString& aSearchString,
+ const nsAString& aSearchParam):
+ mSearchString(aSearchString)
+{
+ if (aSearchString.IsEmpty())
+ mSearchResult = RESULT_IGNORED;
+ else {
+ int32_t slashPos = mSearchString.RFindChar('/');
+ mSearchResult = RESULT_FAILURE;
+ nsCOMPtr<nsIFile> directory;
+ nsDependentSubstring parent(Substring(mSearchString, 0, slashPos + 1));
+ if (!parent.IsEmpty() && parent.First() == '/')
+ NS_NewLocalFile(parent, true, getter_AddRefs(directory));
+ if (!directory) {
+ if (NS_FAILED(NS_NewLocalFile(aSearchParam, true, getter_AddRefs(directory))))
+ return;
+ if (slashPos > 0)
+ directory->AppendRelativePath(Substring(mSearchString, 0, slashPos));
+ }
+ nsCOMPtr<nsISimpleEnumerator> dirEntries;
+ if (NS_FAILED(directory->GetDirectoryEntries(getter_AddRefs(dirEntries))))
+ return;
+ mSearchResult = RESULT_NOMATCH;
+ bool hasMore = false;
+ nsDependentSubstring prefix(Substring(mSearchString, slashPos + 1));
+ while (NS_SUCCEEDED(dirEntries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> nextItem;
+ dirEntries->GetNext(getter_AddRefs(nextItem));
+ nsCOMPtr<nsIFile> nextFile(do_QueryInterface(nextItem));
+ nsAutoString fileName;
+ nextFile->GetLeafName(fileName);
+ if (StringBeginsWith(fileName, prefix)) {
+ fileName.Insert(parent, 0);
+ if (mSearchResult == RESULT_NOMATCH && fileName.Equals(mSearchString))
+ mSearchResult = RESULT_IGNORED;
+ else
+ mSearchResult = RESULT_SUCCESS;
+ bool isDirectory = false;
+ nextFile->IsDirectory(&isDirectory);
+ if (isDirectory)
+ fileName.Append('/');
+ mValues.AppendElement(fileName);
+ }
+ }
+ mValues.Sort();
+ }
+}
+
+NS_IMETHODIMP nsFileResult::GetSearchString(nsAString & aSearchString)
+{
+ aSearchString.Assign(mSearchString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetSearchResult(uint16_t *aSearchResult)
+{
+ NS_ENSURE_ARG_POINTER(aSearchResult);
+ *aSearchResult = mSearchResult;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetDefaultIndex(int32_t *aDefaultIndex)
+{
+ NS_ENSURE_ARG_POINTER(aDefaultIndex);
+ *aDefaultIndex = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetErrorDescription(nsAString & aErrorDescription)
+{
+ aErrorDescription.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetMatchCount(uint32_t *aMatchCount)
+{
+ NS_ENSURE_ARG_POINTER(aMatchCount);
+ *aMatchCount = mValues.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetValueAt(int32_t index, nsAString & aValue)
+{
+ aValue = mValues[index];
+ if (aValue.Last() == '/')
+ aValue.Truncate(aValue.Length() - 1);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetLabelAt(int32_t index, nsAString & aValue)
+{
+ return GetValueAt(index, aValue);
+}
+
+NS_IMETHODIMP nsFileResult::GetCommentAt(int32_t index, nsAString & aComment)
+{
+ aComment.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetStyleAt(int32_t index, nsAString & aStyle)
+{
+ if (mValues[index].Last() == '/')
+ aStyle.AssignLiteral("directory");
+ else
+ aStyle.AssignLiteral("file");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFileResult::GetImageAt(int32_t index, nsAString & aImage)
+{
+ aImage.Truncate();
+ return NS_OK;
+}
+NS_IMETHODIMP nsFileResult::GetFinalCompleteValueAt(int32_t index,
+ nsAString & aValue)
+{
+ return GetValueAt(index, aValue);
+}
+
+NS_IMETHODIMP nsFileResult::RemoveValueAt(int32_t rowIndex, bool removeFromDb)
+{
+ return NS_OK;
+}
+
+class nsFileComplete final : public nsIAutoCompleteSearch
+{
+ ~nsFileComplete() {}
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIAUTOCOMPLETESEARCH
+};
+
+NS_IMPL_ISUPPORTS(nsFileComplete, nsIAutoCompleteSearch)
+
+NS_IMETHODIMP
+nsFileComplete::StartSearch(const nsAString& aSearchString,
+ const nsAString& aSearchParam,
+ nsIAutoCompleteResult *aPreviousResult,
+ nsIAutoCompleteObserver *aListener)
+{
+ NS_ENSURE_ARG_POINTER(aListener);
+ RefPtr<nsFileResult> result = new nsFileResult(aSearchString, aSearchParam);
+ NS_ENSURE_TRUE(result, NS_ERROR_OUT_OF_MEMORY);
+ return aListener->OnSearchResult(this, result);
+}
+
+NS_IMETHODIMP
+nsFileComplete::StopSearch()
+{
+ return NS_OK;
+}
+
+#define NS_FILEVIEW_CID { 0xa5570462, 0x1dd1, 0x11b2, \
+ { 0x9d, 0x19, 0xdf, 0x30, 0xa2, 0x7f, 0xbd, 0xc4 } }
+
+class nsFileView : public nsIFileView,
+ public nsITreeView
+{
+public:
+ nsFileView();
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFILEVIEW
+ NS_DECL_NSITREEVIEW
+
+protected:
+ virtual ~nsFileView();
+
+ void FilterFiles();
+ void ReverseArray(nsTArray<nsCOMPtr<nsIFile> >& aArray);
+ void SortArray(nsTArray<nsCOMPtr<nsIFile> >& aArray);
+ void SortInternal();
+
+ nsTArray<nsCOMPtr<nsIFile> > mFileList;
+ nsTArray<nsCOMPtr<nsIFile> > mDirList;
+ nsTArray<nsCOMPtr<nsIFile> > mFilteredFiles;
+
+ nsCOMPtr<nsIFile> mDirectoryPath;
+ nsCOMPtr<nsITreeBoxObject> mTree;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ nsCOMPtr<nsIDateTimeFormat> mDateFormatter;
+
+ int16_t mSortType;
+ int32_t mTotalRows;
+
+ nsTArray<char16_t*> mCurrentFilters;
+
+ bool mShowHiddenFiles;
+ bool mDirectoryFilter;
+ bool mReverseSort;
+};
+
+// Factory constructor
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsFileComplete)
+NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsFileView, Init)
+NS_DEFINE_NAMED_CID(NS_FILECOMPLETE_CID);
+NS_DEFINE_NAMED_CID(NS_FILEVIEW_CID);
+
+static const mozilla::Module::CIDEntry kFileViewCIDs[] = {
+ { &kNS_FILECOMPLETE_CID, false, nullptr, nsFileCompleteConstructor },
+ { &kNS_FILEVIEW_CID, false, nullptr, nsFileViewConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kFileViewContracts[] = {
+ { NS_FILECOMPLETE_CONTRACTID, &kNS_FILECOMPLETE_CID },
+ { NS_FILEVIEW_CONTRACTID, &kNS_FILEVIEW_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kFileViewModule = {
+ mozilla::Module::kVersion,
+ kFileViewCIDs,
+ kFileViewContracts
+};
+
+NSMODULE_DEFN(nsFileViewModule) = &kFileViewModule;
+
+nsFileView::nsFileView() :
+ mSortType(-1),
+ mTotalRows(0),
+ mShowHiddenFiles(false),
+ mDirectoryFilter(false),
+ mReverseSort(false)
+{
+}
+
+nsFileView::~nsFileView()
+{
+ uint32_t count = mCurrentFilters.Length();
+ for (uint32_t i = 0; i < count; ++i)
+ free(mCurrentFilters[i]);
+}
+
+nsresult
+nsFileView::Init()
+{
+ mDateFormatter = nsIDateTimeFormat::Create();
+ if (!mDateFormatter)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+}
+
+// nsISupports implementation
+
+NS_IMPL_ISUPPORTS(nsFileView, nsITreeView, nsIFileView)
+
+// nsIFileView implementation
+
+NS_IMETHODIMP
+nsFileView::SetShowHiddenFiles(bool aShowHidden)
+{
+ if (aShowHidden != mShowHiddenFiles) {
+ mShowHiddenFiles = aShowHidden;
+
+ // This could be better optimized, but since the hidden
+ // file functionality is not currently used, this will be fine.
+ SetDirectory(mDirectoryPath);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetShowHiddenFiles(bool* aShowHidden)
+{
+ *aShowHidden = mShowHiddenFiles;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetShowOnlyDirectories(bool aOnlyDirs)
+{
+ if (aOnlyDirs == mDirectoryFilter)
+ return NS_OK;
+
+ mDirectoryFilter = aOnlyDirs;
+ uint32_t dirCount = mDirList.Length();
+ if (mDirectoryFilter) {
+ int32_t rowDiff = mTotalRows - dirCount;
+
+ mFilteredFiles.Clear();
+ mTotalRows = dirCount;
+ if (mTree)
+ mTree->RowCountChanged(mTotalRows, -rowDiff);
+ } else {
+ // Run the filter again to get the file list back
+ FilterFiles();
+
+ SortArray(mFilteredFiles);
+ if (mReverseSort)
+ ReverseArray(mFilteredFiles);
+
+ if (mTree)
+ mTree->RowCountChanged(dirCount, mTotalRows - dirCount);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetShowOnlyDirectories(bool* aOnlyDirs)
+{
+ *aOnlyDirs = mDirectoryFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetSortType(int16_t* aSortType)
+{
+ *aSortType = mSortType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetReverseSort(bool* aReverseSort)
+{
+ *aReverseSort = mReverseSort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::Sort(int16_t aSortType, bool aReverseSort)
+{
+ if (aSortType == mSortType) {
+ if (aReverseSort == mReverseSort)
+ return NS_OK;
+
+ mReverseSort = aReverseSort;
+ ReverseArray(mDirList);
+ ReverseArray(mFilteredFiles);
+ } else {
+ mSortType = aSortType;
+ mReverseSort = aReverseSort;
+ SortInternal();
+ }
+
+ if (mTree)
+ mTree->Invalidate();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetDirectory(nsIFile* aDirectory)
+{
+ NS_ENSURE_ARG_POINTER(aDirectory);
+
+ nsCOMPtr<nsISimpleEnumerator> dirEntries;
+ aDirectory->GetDirectoryEntries(getter_AddRefs(dirEntries));
+
+ if (!dirEntries) {
+ // Couldn't read in the directory, this can happen if the user does not
+ // have permission to list it.
+ return NS_ERROR_FAILURE;
+ }
+
+ mDirectoryPath = aDirectory;
+ mFileList.Clear();
+ mDirList.Clear();
+
+ bool hasMore = false;
+
+ while (NS_SUCCEEDED(dirEntries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> nextItem;
+ dirEntries->GetNext(getter_AddRefs(nextItem));
+ nsCOMPtr<nsIFile> theFile = do_QueryInterface(nextItem);
+
+ bool isDirectory = false;
+ if (theFile) {
+ theFile->IsDirectory(&isDirectory);
+
+ if (isDirectory) {
+ bool isHidden;
+ theFile->IsHidden(&isHidden);
+ if (mShowHiddenFiles || !isHidden) {
+ mDirList.AppendElement(theFile);
+ }
+ }
+ else {
+ mFileList.AppendElement(theFile);
+ }
+ }
+ }
+
+ if (mTree) {
+ mTree->BeginUpdateBatch();
+ mTree->RowCountChanged(0, -mTotalRows);
+ }
+
+ FilterFiles();
+ SortInternal();
+
+ if (mTree) {
+ mTree->EndUpdateBatch();
+ mTree->ScrollToRow(0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetFilter(const nsAString& aFilterString)
+{
+ uint32_t filterCount = mCurrentFilters.Length();
+ for (uint32_t i = 0; i < filterCount; ++i)
+ free(mCurrentFilters[i]);
+ mCurrentFilters.Clear();
+
+ nsAString::const_iterator start, iter, end;
+ aFilterString.BeginReading(iter);
+ aFilterString.EndReading(end);
+
+ while (true) {
+ // skip over delimiters
+ while (iter != end && (*iter == ';' || *iter == ' '))
+ ++iter;
+
+ if (iter == end)
+ break;
+
+ start = iter; // start of a filter
+
+ // we know this is neither ';' nor ' ', skip to next char
+ ++iter;
+
+ // find next delimiter or end of string
+ while (iter != end && (*iter != ';' && *iter != ' '))
+ ++iter;
+
+ char16_t* filter = ToNewUnicode(Substring(start, iter));
+ if (!filter)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ if (!mCurrentFilters.AppendElement(filter)) {
+ free(filter);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (iter == end)
+ break;
+
+ ++iter; // we know this is either ';' or ' ', skip to next char
+ }
+
+ if (mTree) {
+ mTree->BeginUpdateBatch();
+ uint32_t count = mDirList.Length();
+ mTree->RowCountChanged(count, count - mTotalRows);
+ }
+
+ mFilteredFiles.Clear();
+
+ FilterFiles();
+
+ SortArray(mFilteredFiles);
+ if (mReverseSort)
+ ReverseArray(mFilteredFiles);
+
+ if (mTree)
+ mTree->EndUpdateBatch();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetSelectedFiles(nsIArray** aFiles)
+{
+ *aFiles = nullptr;
+ if (!mSelection)
+ return NS_OK;
+
+ int32_t numRanges;
+ mSelection->GetRangeCount(&numRanges);
+
+ uint32_t dirCount = mDirList.Length();
+ nsCOMPtr<nsIMutableArray> fileArray =
+ do_CreateInstance(NS_ARRAY_CONTRACTID);
+ NS_ENSURE_STATE(fileArray);
+
+ for (int32_t range = 0; range < numRanges; ++range) {
+ int32_t rangeBegin, rangeEnd;
+ mSelection->GetRangeAt(range, &rangeBegin, &rangeEnd);
+
+ for (int32_t itemIndex = rangeBegin; itemIndex <= rangeEnd; ++itemIndex) {
+ nsIFile* curFile = nullptr;
+
+ if (itemIndex < (int32_t) dirCount)
+ curFile = mDirList[itemIndex];
+ else {
+ if (itemIndex < mTotalRows)
+ curFile = mFilteredFiles[itemIndex - dirCount];
+ }
+
+ if (curFile)
+ fileArray->AppendElement(curFile, false);
+ }
+ }
+
+ fileArray.forget(aFiles);
+ return NS_OK;
+}
+
+
+// nsITreeView implementation
+
+NS_IMETHODIMP
+nsFileView::GetRowCount(int32_t* aRowCount)
+{
+ *aRowCount = mTotalRows;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetSelection(nsITreeSelection** aSelection)
+{
+ *aSelection = mSelection;
+ NS_IF_ADDREF(*aSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetSelection(nsITreeSelection* aSelection)
+{
+ mSelection = aSelection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetRowProperties(int32_t aIndex, nsAString& aProps)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetCellProperties(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aProps)
+{
+ uint32_t dirCount = mDirList.Length();
+
+ if (aRow < (int32_t) dirCount)
+ aProps.AppendLiteral("directory");
+ else if (aRow < mTotalRows)
+ aProps.AppendLiteral("file");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetColumnProperties(nsITreeColumn* aCol, nsAString& aProps)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsContainer(int32_t aIndex, bool* aIsContainer)
+{
+ *aIsContainer = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsContainerOpen(int32_t aIndex, bool* aIsOpen)
+{
+ *aIsOpen = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsContainerEmpty(int32_t aIndex, bool* aIsEmpty)
+{
+ *aIsEmpty = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsSeparator(int32_t aIndex, bool* aIsSeparator)
+{
+ *aIsSeparator = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsSorted(bool* aIsSorted)
+{
+ *aIsSorted = (mSortType >= 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::CanDrop(int32_t aIndex, int32_t aOrientation,
+ nsIDOMDataTransfer* dataTransfer, bool* aCanDrop)
+{
+ *aCanDrop = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::Drop(int32_t aRow, int32_t aOrientation, nsIDOMDataTransfer* dataTransfer)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetParentIndex(int32_t aRowIndex, int32_t* aParentIndex)
+{
+ *aParentIndex = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
+ bool* aHasSibling)
+{
+ *aHasSibling = (aAfterIndex < (mTotalRows - 1));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetLevel(int32_t aIndex, int32_t* aLevel)
+{
+ *aLevel = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetImageSrc(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aImageSrc)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetProgressMode(int32_t aRow, nsITreeColumn* aCol,
+ int32_t* aProgressMode)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetCellValue(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aCellValue)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::GetCellText(int32_t aRow, nsITreeColumn* aCol,
+ nsAString& aCellText)
+{
+ uint32_t dirCount = mDirList.Length();
+ bool isDirectory;
+ nsIFile* curFile = nullptr;
+
+ if (aRow < (int32_t) dirCount) {
+ isDirectory = true;
+ curFile = mDirList[aRow];
+ } else if (aRow < mTotalRows) {
+ isDirectory = false;
+ curFile = mFilteredFiles[aRow - dirCount];
+ } else {
+ // invalid row
+ aCellText.SetCapacity(0);
+ return NS_OK;
+ }
+
+ const char16_t* colID;
+ aCol->GetIdConst(&colID);
+ if (NS_LITERAL_STRING("FilenameColumn").Equals(colID)) {
+ curFile->GetLeafName(aCellText);
+ } else if (NS_LITERAL_STRING("LastModifiedColumn").Equals(colID)) {
+ PRTime lastModTime;
+ curFile->GetLastModifiedTime(&lastModTime);
+ // XXX FormatPRTime could take an nsAString&
+ nsAutoString temp;
+ mDateFormatter->FormatPRTime(nullptr, kDateFormatShort, kTimeFormatSeconds,
+ lastModTime * 1000, temp);
+ aCellText = temp;
+ } else {
+ // file size
+ if (isDirectory)
+ aCellText.SetCapacity(0);
+ else {
+ int64_t fileSize;
+ curFile->GetFileSize(&fileSize);
+ CopyUTF8toUTF16(nsPrintfCString("%lld", fileSize), aCellText);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetTree(nsITreeBoxObject* aTree)
+{
+ mTree = aTree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::ToggleOpenState(int32_t aIndex)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::CycleHeader(nsITreeColumn* aCol)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SelectionChanged()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::CycleCell(int32_t aRow, nsITreeColumn* aCol)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsEditable(int32_t aRow, nsITreeColumn* aCol,
+ bool* aIsEditable)
+{
+ *aIsEditable = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::IsSelectable(int32_t aRow, nsITreeColumn* aCol,
+ bool* aIsSelectable)
+{
+ *aIsSelectable = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetCellValue(int32_t aRow, nsITreeColumn* aCol,
+ const nsAString& aValue)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::SetCellText(int32_t aRow, nsITreeColumn* aCol,
+ const nsAString& aValue)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::PerformAction(const char16_t* aAction)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::PerformActionOnRow(const char16_t* aAction, int32_t aRow)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFileView::PerformActionOnCell(const char16_t* aAction, int32_t aRow,
+ nsITreeColumn* aCol)
+{
+ return NS_OK;
+}
+
+// Private methods
+
+void
+nsFileView::FilterFiles()
+{
+ uint32_t count = mDirList.Length();
+ mTotalRows = count;
+ count = mFileList.Length();
+ mFilteredFiles.Clear();
+ uint32_t filterCount = mCurrentFilters.Length();
+
+ for (uint32_t i = 0; i < count; ++i) {
+ nsIFile* file = mFileList[i];
+ bool isHidden = false;
+ if (!mShowHiddenFiles)
+ file->IsHidden(&isHidden);
+
+ nsAutoString ucsLeafName;
+ if(NS_FAILED(file->GetLeafName(ucsLeafName))) {
+ // need to check return value for GetLeafName()
+ continue;
+ }
+
+ if (!isHidden) {
+ for (uint32_t j = 0; j < filterCount; ++j) {
+ bool matched = false;
+ if (!nsCRT::strcmp(mCurrentFilters.ElementAt(j),
+ u"..apps"))
+ {
+ file->IsExecutable(&matched);
+ } else
+ matched = (NS_WildCardMatch(ucsLeafName.get(),
+ mCurrentFilters.ElementAt(j),
+ true) == MATCH);
+
+ if (matched) {
+ mFilteredFiles.AppendElement(file);
+ ++mTotalRows;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void
+nsFileView::ReverseArray(nsTArray<nsCOMPtr<nsIFile> >& aArray)
+{
+ uint32_t count = aArray.Length();
+ for (uint32_t i = 0; i < count/2; ++i) {
+ // If we get references to the COMPtrs in the array, and then .swap() them
+ // we avoid AdRef() / Release() calls.
+ nsCOMPtr<nsIFile>& element = aArray[i];
+ nsCOMPtr<nsIFile>& element2 = aArray[count - i - 1];
+ element.swap(element2);
+ }
+}
+
+static int
+SortNameCallback(const void* aElement1, const void* aElement2, void* aContext)
+{
+ nsIFile* file1 = *static_cast<nsIFile* const *>(aElement1);
+ nsIFile* file2 = *static_cast<nsIFile* const *>(aElement2);
+
+ nsAutoString leafName1, leafName2;
+ file1->GetLeafName(leafName1);
+ file2->GetLeafName(leafName2);
+
+ return Compare(leafName1, leafName2);
+}
+
+static int
+SortSizeCallback(const void* aElement1, const void* aElement2, void* aContext)
+{
+ nsIFile* file1 = *static_cast<nsIFile* const *>(aElement1);
+ nsIFile* file2 = *static_cast<nsIFile* const *>(aElement2);
+
+ int64_t size1, size2;
+ file1->GetFileSize(&size1);
+ file2->GetFileSize(&size2);
+
+ if (size1 == size2)
+ return 0;
+
+ return size1 < size2 ? -1 : 1;
+}
+
+static int
+SortDateCallback(const void* aElement1, const void* aElement2, void* aContext)
+{
+ nsIFile* file1 = *static_cast<nsIFile* const *>(aElement1);
+ nsIFile* file2 = *static_cast<nsIFile* const *>(aElement2);
+
+ PRTime time1, time2;
+ file1->GetLastModifiedTime(&time1);
+ file2->GetLastModifiedTime(&time2);
+
+ if (time1 == time2)
+ return 0;
+
+ return time1 < time2 ? -1 : 1;
+}
+
+void
+nsFileView::SortArray(nsTArray<nsCOMPtr<nsIFile> >& aArray)
+{
+ // We assume the array to be in filesystem order, which
+ // for our purposes, is completely unordered.
+
+ int (*compareFunc)(const void*, const void*, void*);
+
+ switch (mSortType) {
+ case sortName:
+ compareFunc = SortNameCallback;
+ break;
+ case sortSize:
+ compareFunc = SortSizeCallback;
+ break;
+ case sortDate:
+ compareFunc = SortDateCallback;
+ break;
+ default:
+ return;
+ }
+
+ uint32_t count = aArray.Length();
+
+ nsIFile** array = new nsIFile*[count];
+ for (uint32_t i = 0; i < count; ++i) {
+ array[i] = aArray[i];
+ }
+
+ NS_QuickSort(array, count, sizeof(nsIFile*), compareFunc, nullptr);
+
+ for (uint32_t i = 0; i < count; ++i) {
+ // Use swap() to avoid refcounting.
+ aArray[i].swap(array[i]);
+ }
+
+ delete[] array;
+}
+
+void
+nsFileView::SortInternal()
+{
+ SortArray(mDirList);
+ SortArray(mFilteredFiles);
+
+ if (mReverseSort) {
+ ReverseArray(mDirList);
+ ReverseArray(mFilteredFiles);
+ }
+}
diff --git a/toolkit/components/filepicker/nsIFileView.idl b/toolkit/components/filepicker/nsIFileView.idl
new file mode 100644
index 0000000000..4862a594e0
--- /dev/null
+++ b/toolkit/components/filepicker/nsIFileView.idl
@@ -0,0 +1,34 @@
+/* -*- 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 nsIArray;
+interface nsIFile;
+
+[scriptable, uuid(60b320d2-1dd2-11b2-bd73-dc3575f78ddd)]
+interface nsIFileView : nsISupports
+{
+ const short sortName = 0;
+ const short sortSize = 1;
+ const short sortDate = 2;
+
+ attribute boolean showHiddenFiles;
+ attribute boolean showOnlyDirectories;
+ readonly attribute short sortType;
+ readonly attribute boolean reverseSort;
+
+ void sort(in short sortType, in boolean reverseSort);
+ void setDirectory(in nsIFile directory);
+ void setFilter(in AString filterString);
+
+ readonly attribute nsIArray selectedFiles;
+};
+
+%{C++
+
+#define NS_FILEVIEW_CONTRACTID "@mozilla.org/filepicker/fileview;1"
+
+%}
diff --git a/toolkit/components/filepicker/test/unit/.eslintrc.js b/toolkit/components/filepicker/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/filepicker/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/filepicker/test/unit/test_filecomplete.js b/toolkit/components/filepicker/test/unit/test_filecomplete.js
new file mode 100644
index 0000000000..d1e18d5337
--- /dev/null
+++ b/toolkit/components/filepicker/test/unit/test_filecomplete.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/. */
+
+// Start by getting an empty directory.
+var dir = do_get_profile();
+dir.append("temp");
+dir.create(dir.DIRECTORY_TYPE, -1);
+var path = dir.path + "/";
+
+// Now create some sample entries.
+var file = dir.clone();
+file.append("test_file");
+file.create(file.NORMAL_FILE_TYPE, -1);
+file = dir.clone();
+file.append("other_file");
+file.create(file.NORMAL_FILE_TYPE, -1);
+dir.append("test_dir");
+dir.create(dir.DIRECTORY_TYPE, -1);
+
+var gListener = {
+ onSearchResult: function(aSearch, aResult) {
+ // Check that we got same search string back.
+ do_check_eq(aResult.searchString, "test");
+ // Check that the search succeeded.
+ do_check_eq(aResult.searchResult, aResult.RESULT_SUCCESS);
+ // Check that we got two results.
+ do_check_eq(aResult.matchCount, 2);
+ // Check that the first result is the directory we created.
+ do_check_eq(aResult.getValueAt(0), "test_dir");
+ // Check that the first result has directory style.
+ do_check_eq(aResult.getStyleAt(0), "directory");
+ // Check that the second result is the file we created.
+ do_check_eq(aResult.getValueAt(1), "test_file");
+ // Check that the second result has file style.
+ do_check_eq(aResult.getStyleAt(1), "file");
+ }
+};
+
+function run_test()
+{
+ Components.classes["@mozilla.org/autocomplete/search;1?name=file"]
+ .getService(Components.interfaces.nsIAutoCompleteSearch)
+ .startSearch("test", path, null, gListener);
+}
diff --git a/toolkit/components/filepicker/test/unit/xpcshell.ini b/toolkit/components/filepicker/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..1a0a002dc9
--- /dev/null
+++ b/toolkit/components/filepicker/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+
+[test_filecomplete.js]
+skip-if = os != 'linux'
diff --git a/toolkit/components/filewatcher/NativeFileWatcherNotSupported.h b/toolkit/components/filewatcher/NativeFileWatcherNotSupported.h
new file mode 100644
index 0000000000..6b1aa97ba6
--- /dev/null
+++ b/toolkit/components/filewatcher/NativeFileWatcherNotSupported.h
@@ -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/. */
+
+#ifndef mozilla_nativefilewatcher_h__
+#define mozilla_nativefilewatcher_h__
+
+#include "nsINativeFileWatcher.h"
+
+namespace mozilla {
+
+class NativeFileWatcherService final : public nsINativeFileWatcherService
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ NativeFileWatcherService()
+ {
+ };
+
+ nsresult Init()
+ {
+ return NS_OK;
+ };
+
+ NS_IMETHOD AddPath(const nsAString& aPathToWatch,
+ nsINativeFileWatcherCallback* aOnChange,
+ nsINativeFileWatcherErrorCallback* aOnError,
+ nsINativeFileWatcherSuccessCallback* aOnSuccess) override
+ {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ };
+
+ NS_IMETHOD RemovePath(const nsAString& aPathToRemove,
+ nsINativeFileWatcherCallback* aOnChange,
+ nsINativeFileWatcherErrorCallback* aOnError,
+ nsINativeFileWatcherSuccessCallback* aOnSuccess) override
+ {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ };
+
+private:
+ ~NativeFileWatcherService() { };
+ NativeFileWatcherService(const NativeFileWatcherService& other) = delete;
+ void operator=(const NativeFileWatcherService& other) = delete;
+};
+
+NS_IMPL_ISUPPORTS(NativeFileWatcherService, nsINativeFileWatcherService);
+
+} // namespace mozilla
+
+#endif // mozilla_nativefilewatcher_h__
diff --git a/toolkit/components/filewatcher/NativeFileWatcherWin.cpp b/toolkit/components/filewatcher/NativeFileWatcherWin.cpp
new file mode 100644
index 0000000000..3ff69728a9
--- /dev/null
+++ b/toolkit/components/filewatcher/NativeFileWatcherWin.cpp
@@ -0,0 +1,1494 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Native implementation of Watcher operations.
+ */
+#include "NativeFileWatcherWin.h"
+
+#include "mozilla/Services.h"
+#include "mozilla/UniquePtr.h"
+#include "nsClassHashtable.h"
+#include "nsDataHashtable.h"
+#include "nsILocalFile.h"
+#include "nsIObserverService.h"
+#include "nsProxyRelease.h"
+#include "nsTArray.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+
+// Enclose everything which is not exported in an anonymous namespace.
+namespace {
+
+/**
+ * An event used to notify the main thread when an error happens.
+ */
+class WatchedErrorEvent final : public Runnable
+{
+public:
+ /**
+ * @param aOnError The passed error callback.
+ * @param aError The |nsresult| error value.
+ * @param osError The error returned by GetLastError().
+ */
+ WatchedErrorEvent(const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
+ const nsresult& anError, const DWORD& osError)
+ : mOnError(aOnError)
+ , mError(anError)
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Make sure we wrap a valid callback since it's not mandatory to provide
+ // one when watching a resource.
+ if (mOnError) {
+ (void)mOnError->Complete(mError, mOsError);
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> mOnError;
+ nsresult mError;
+ DWORD mOsError;
+};
+
+/**
+ * An event used to notify the main thread when an operation is successful.
+ */
+class WatchedSuccessEvent final : public Runnable
+{
+public:
+ /**
+ * @param aOnSuccess The passed success callback.
+ * @param aResourcePath
+ * The path of the resource for which this event was generated.
+ */
+ WatchedSuccessEvent(const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>& aOnSuccess,
+ const nsAString& aResourcePath)
+ : mOnSuccess(aOnSuccess)
+ , mResourcePath(aResourcePath)
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Make sure we wrap a valid callback since it's not mandatory to provide
+ // one when watching a resource.
+ if (mOnSuccess) {
+ (void)mOnSuccess->Complete(mResourcePath);
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> mOnSuccess;
+ nsString mResourcePath;
+};
+
+/**
+ * An event used to notify the main thread of a change in a watched
+ * resource.
+ */
+class WatchedChangeEvent final : public Runnable
+{
+public:
+ /**
+ * @param aOnChange The passed change callback.
+ * @param aChangedResource The name of the changed resource.
+ */
+ WatchedChangeEvent(const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
+ const nsAString& aChangedResource)
+ : mOnChange(aOnChange)
+ , mChangedResource(aChangedResource)
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // The second parameter is reserved for future uses: we use 0 as a placeholder.
+ (void)mOnChange->Changed(mChangedResource, 0);
+ return NS_OK;
+ }
+
+private:
+ nsMainThreadPtrHandle<nsINativeFileWatcherCallback> mOnChange;
+ nsString mChangedResource;
+};
+
+static mozilla::LazyLogModule gNativeWatcherPRLog("NativeFileWatcherService");
+#define FILEWATCHERLOG(...) MOZ_LOG(gNativeWatcherPRLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+// The number of notifications to store within WatchedResourceDescriptor:mNotificationBuffer.
+// If the buffer overflows, its contents are discarded and a change callback is dispatched
+// with "*" as changed path.
+const unsigned int WATCHED_RES_MAXIMUM_NOTIFICATIONS = 100;
+
+// The size, in bytes, of the notification buffer used to store the changes notifications
+// for each watched resource.
+const size_t NOTIFICATION_BUFFER_SIZE =
+ WATCHED_RES_MAXIMUM_NOTIFICATIONS * sizeof(FILE_NOTIFY_INFORMATION);
+
+/**
+ * AutoCloseHandle is a RAII wrapper for Windows |HANDLE|s
+ */
+struct AutoCloseHandleTraits
+{
+ typedef HANDLE type;
+ static type empty() { return INVALID_HANDLE_VALUE; }
+ static void release(type anHandle)
+ {
+ if (anHandle != INVALID_HANDLE_VALUE) {
+ // If CancelIo is called on an |HANDLE| not yet associated to a Completion I/O
+ // it simply does nothing.
+ (void)CancelIo(anHandle);
+ (void)CloseHandle(anHandle);
+ }
+ }
+};
+typedef Scoped<AutoCloseHandleTraits> AutoCloseHandle;
+
+// Define these callback array types to make the code easier to read.
+typedef nsTArray<nsMainThreadPtrHandle<nsINativeFileWatcherCallback>> ChangeCallbackArray;
+typedef nsTArray<nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>> ErrorCallbackArray;
+
+/**
+ * A structure to keep track of the information related to a
+ * watched resource.
+ */
+struct WatchedResourceDescriptor {
+ // The path on the file system of the watched resource.
+ nsString mPath;
+
+ // A buffer containing the latest notifications received for the resource.
+ // UniquePtr<FILE_NOTIFY_INFORMATION> cannot be used as the structure
+ // contains a variable length field (FileName).
+ UniquePtr<unsigned char> mNotificationBuffer;
+
+ // Used to hold information for the asynchronous ReadDirectoryChangesW call
+ // (does not need to be closed as it is not an |HANDLE|).
+ OVERLAPPED mOverlappedInfo;
+
+ // The OS handle to the watched resource.
+ AutoCloseHandle mResourceHandle;
+
+ WatchedResourceDescriptor(const nsAString& aPath, const HANDLE anHandle)
+ : mPath(aPath)
+ , mResourceHandle(anHandle)
+ {
+ memset(&mOverlappedInfo, 0, sizeof(OVERLAPPED));
+ mNotificationBuffer.reset(new unsigned char[NOTIFICATION_BUFFER_SIZE]);
+ }
+};
+
+/**
+ * A structure used to pass the callbacks to the AddPathRunnableMethod() and
+ * RemovePathRunnableMethod().
+ */
+struct PathRunnablesParametersWrapper {
+ nsString mPath;
+ nsMainThreadPtrHandle<nsINativeFileWatcherCallback> mChangeCallbackHandle;
+ nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> mErrorCallbackHandle;
+ nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> mSuccessCallbackHandle;
+
+ PathRunnablesParametersWrapper(
+ const nsAString& aPath,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>& aOnSuccess)
+ : mPath(aPath)
+ , mChangeCallbackHandle(aOnChange)
+ , mErrorCallbackHandle(aOnError)
+ , mSuccessCallbackHandle(aOnSuccess)
+ {
+ }
+};
+
+/**
+ * This runnable is dispatched to the main thread in order to safely
+ * shutdown the worker thread.
+ */
+class NativeWatcherIOShutdownTask : public Runnable
+{
+public:
+ NativeWatcherIOShutdownTask()
+ : mWorkerThread(do_GetCurrentThread())
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ mWorkerThread->Shutdown();
+ return NS_OK;
+ }
+
+private:
+ nsCOMPtr<nsIThread> mWorkerThread;
+};
+
+/**
+ * This runnable is dispatched from the main thread to get the notifications of the
+ * changes in the watched resources by continuously calling the blocking function
+ * GetQueuedCompletionStatus. This function queries the status of the Completion I/O
+ * port initialized in the main thread. The watched resources are registered to the
+ * completion I/O port when calling |addPath|.
+ *
+ * Instead of using a loop within the Run() method, the Runnable reschedules itself
+ * by issuing a NS_DispatchToCurrentThread(this) before exiting. This is done to allow
+ * the execution of other runnables enqueued within the thread task queue.
+ */
+class NativeFileWatcherIOTask : public Runnable
+{
+public:
+ NativeFileWatcherIOTask(HANDLE aIOCompletionPort)
+ : mIOCompletionPort(aIOCompletionPort)
+ , mShuttingDown(false)
+ {
+ }
+
+ NS_IMETHOD Run();
+ nsresult AddPathRunnableMethod(PathRunnablesParametersWrapper* aWrappedParameters);
+ nsresult RemovePathRunnableMethod(PathRunnablesParametersWrapper* aWrappedParameters);
+ nsresult DeactivateRunnableMethod();
+
+private:
+ // Maintain 2 indexes - one by resource path, one by resource |HANDLE|.
+ // Since |HANDLE| is basically a typedef to void*, we use nsVoidPtrHashKey to
+ // compute the hashing key. We need 2 indexes in order to quickly look up the
+ // changed resource in the Worker Thread.
+ // The objects are not ref counted and get destroyed by mWatchedResourcesByPath
+ // on NativeFileWatcherService::Destroy or in NativeFileWatcherService::RemovePath.
+ nsClassHashtable<nsStringHashKey, WatchedResourceDescriptor> mWatchedResourcesByPath;
+ nsDataHashtable<nsVoidPtrHashKey, WatchedResourceDescriptor*> mWatchedResourcesByHandle;
+
+ // The same callback can be associated to multiple watches so we need to keep
+ // them alive as long as there is a watch using them. We create two hashtables
+ // to map directory names to lists of nsMainThreadPtr<callbacks>.
+ nsClassHashtable<nsStringHashKey, ChangeCallbackArray> mChangeCallbacksTable;
+ nsClassHashtable<nsStringHashKey, ErrorCallbackArray> mErrorCallbacksTable;
+
+ // We hold a copy of the completion port |HANDLE|, which is owned by the main thread.
+ HANDLE mIOCompletionPort;
+
+ // Other methods need to know that a shutdown is in progress.
+ bool mShuttingDown;
+
+ nsresult RunInternal();
+
+ nsresult DispatchChangeCallbacks(WatchedResourceDescriptor* aResourceDescriptor,
+ const nsAString& aChangedResource);
+
+ nsresult ReportChange(
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
+ const nsAString& aChangedResource);
+
+ nsresult DispatchErrorCallbacks(WatchedResourceDescriptor* aResourceDescriptor,
+ nsresult anError, DWORD anOSError);
+
+ nsresult ReportError(
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
+ nsresult anError, DWORD anOSError);
+
+ nsresult ReportSuccess(
+ const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>& aOnSuccess,
+ const nsAString& aResourcePath);
+
+ nsresult AddDirectoryToWatchList(WatchedResourceDescriptor* aDirectoryDescriptor);
+
+ void AppendCallbacksToHashtables(
+ const nsAString& aPath,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError);
+
+ void RemoveCallbacksFromHashtables(
+ const nsAString& aPath,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError);
+
+ nsresult MakeResourcePath(
+ WatchedResourceDescriptor* changedDescriptor,
+ const nsAString& resourceName,
+ nsAString& nativeResourcePath);
+};
+
+/**
+ * The watching thread logic.
+ *
+ * @return NS_OK if the watcher loop must be rescheduled, a failure code
+ * if it must not.
+ */
+nsresult
+NativeFileWatcherIOTask::RunInternal()
+{
+ // Contains the address of the |OVERLAPPED| structure passed
+ // to ReadDirectoryChangesW (used to check for |HANDLE| closing).
+ OVERLAPPED* overlappedStructure;
+
+ // The number of bytes transferred by GetQueuedCompletionStatus
+ // (used to check for |HANDLE| closing).
+ DWORD transferredBytes = 0;
+
+ // Will hold the |HANDLE| to the watched resource returned by GetQueuedCompletionStatus
+ // which generated the change events.
+ ULONG_PTR changedResourceHandle = 0;
+
+ // Check for changes in the resource status by querying the |mIOCompletionPort|
+ // (blocking). GetQueuedCompletionStatus is always called before the first call
+ // to ReadDirectoryChangesW. This isn't a problem, since mIOCompletionPort is
+ // already a valid |HANDLE| even though it doesn't have any associated notification
+ // handles (through ReadDirectoryChangesW).
+ if (!GetQueuedCompletionStatus(mIOCompletionPort, &transferredBytes,
+ &changedResourceHandle, &overlappedStructure,
+ INFINITE)) {
+ // Ok, there was some error.
+ DWORD errCode = GetLastError();
+ switch (errCode) {
+ case ERROR_NOTIFY_ENUM_DIR: {
+ // There were too many changes and the notification buffer has overflowed.
+ // We dispatch the special value "*" and reschedule.
+ FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Notification buffer has overflowed");
+
+ WatchedResourceDescriptor* changedRes =
+ mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle);
+
+ nsresult rv = DispatchChangeCallbacks(changedRes, NS_LITERAL_STRING("*"));
+ if (NS_FAILED(rv)) {
+ // We failed to dispatch the error callbacks. Something very
+ // bad happened to the main thread, so we bail out from the watcher thread.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::Run - Failed to dispatch change callbacks (%x).",
+ rv);
+ return rv;
+ }
+
+ return NS_OK;
+ }
+ case ERROR_ABANDONED_WAIT_0:
+ case ERROR_INVALID_HANDLE: {
+ // If we reach this point, mIOCompletionPort was probably closed
+ // and we need to close this thread. This condition is identified
+ // by catching the ERROR_INVALID_HANDLE error.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::Run - The completion port was closed (%x).",
+ errCode);
+ return NS_ERROR_ABORT;
+ }
+ case ERROR_OPERATION_ABORTED: {
+ // Some path was unwatched! That's not really an error, now it is safe
+ // to free the memory for the resource and call GetQueuedCompletionStatus
+ // again.
+ FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Path unwatched (%x).", errCode);
+
+ WatchedResourceDescriptor* toFree =
+ mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle);
+
+ if (toFree) {
+ // Take care of removing the resource and freeing the memory
+
+ mWatchedResourcesByHandle.Remove((HANDLE)changedResourceHandle);
+
+ // This last call eventually frees the memory
+ mWatchedResourcesByPath.Remove(toFree->mPath);
+ }
+
+ return NS_OK;
+ }
+ default: {
+ // It should probably never get here, but it's better to be safe.
+ FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Unknown error (%x).", errCode);
+
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ // When an |HANDLE| associated to the completion I/O port is gracefully
+ // closed, GetQueuedCompletionStatus still may return a status update. Moreover,
+ // this can also be triggered when watching files on a network folder and losing
+ // the connection.
+ // That's an edge case we need to take care of for consistency by checking
+ // for (!transferredBytes && overlappedStructure). See http://xania.org/200807/iocp
+ if (!transferredBytes &&
+ (overlappedStructure ||
+ (!overlappedStructure && !changedResourceHandle))) {
+ // Note: if changedResourceHandle is nullptr as well, the wait on the Completion
+ // I/O was interrupted by a call to PostQueuedCompletionStatus with 0 transferred
+ // bytes and nullptr as |OVERLAPPED| and |HANDLE|. This is done to allow addPath
+ // and removePath to work on this thread.
+ return NS_OK;
+ }
+
+ // Check to see which resource is changedResourceHandle.
+ WatchedResourceDescriptor* changedRes =
+ mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle);
+ MOZ_ASSERT(changedRes, "Could not find the changed resource in the list of watched ones.");
+
+ // Parse the changes and notify the main thread.
+ const unsigned char* rawNotificationBuffer = changedRes->mNotificationBuffer.get();
+
+ while (true) {
+ FILE_NOTIFY_INFORMATION* notificationInfo =
+ (FILE_NOTIFY_INFORMATION*)rawNotificationBuffer;
+
+ // FileNameLength is in bytes, but we need FileName length
+ // in characters, so divide it by sizeof(WCHAR).
+ nsAutoString resourceName(notificationInfo->FileName,
+ notificationInfo->FileNameLength / sizeof(WCHAR));
+
+ // Handle path normalisation using nsILocalFile.
+ nsString resourcePath;
+ nsresult rv = MakeResourcePath(changedRes, resourceName, resourcePath);
+ if (NS_SUCCEEDED(rv)) {
+ rv = DispatchChangeCallbacks(changedRes, resourcePath);
+ if (NS_FAILED(rv)) {
+ // Log that we failed to dispatch the change callbacks.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::Run - Failed to dispatch change callbacks (%x).",
+ rv);
+ return rv;
+ }
+ }
+
+ if (!notificationInfo->NextEntryOffset) {
+ break;
+ }
+
+ rawNotificationBuffer += notificationInfo->NextEntryOffset;
+ }
+
+ // We need to keep watching for further changes.
+ nsresult rv = AddDirectoryToWatchList(changedRes);
+ if (NS_FAILED(rv)) {
+ // We failed to watch the folder.
+ if (rv == NS_ERROR_ABORT) {
+ // Log that we also failed to dispatch the error callbacks.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::Run - Failed to watch %s and"
+ " to dispatch the related error callbacks", changedRes->mPath.get());
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Wraps the watcher logic and takes care of rescheduling
+ * the watcher loop based on the return code of |RunInternal|
+ * in order to help with code readability.
+ *
+ * @return NS_OK or a failure error code from |NS_DispatchToCurrentThread|.
+ */
+NS_IMETHODIMP
+NativeFileWatcherIOTask::Run()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // We return immediately if |mShuttingDown| is true (see below for
+ // details about the shutdown protocol being followed).
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ nsresult rv = RunInternal();
+ if (NS_FAILED(rv)) {
+ // A critical error occurred in the watcher loop, don't reschedule.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::Run - Stopping the watcher loop (error %S)", rv);
+
+ // We log the error but return NS_OK instead: we don't want to
+ // propagate an exception through XPCOM.
+ return NS_OK;
+ }
+
+ // No error occurred, reschedule.
+ return NS_DispatchToCurrentThread(this);
+}
+
+/**
+ * Adds the resource to the watched list. This function is enqueued on the worker
+ * thread by NativeFileWatcherService::AddPath. All the errors are reported to the main
+ * thread using the error callback function mErrorCallback.
+ *
+ * @param pathToWatch
+ * The path of the resource to watch for changes.
+ *
+ * @return NS_ERROR_FILE_NOT_FOUND if the path is invalid or does not exist.
+ * Returns NS_ERROR_UNEXPECTED if OS |HANDLE|s are unexpectedly closed.
+ * If the ReadDirectoryChangesW call fails, returns NS_ERROR_FAILURE,
+ * otherwise NS_OK.
+ */
+nsresult
+NativeFileWatcherIOTask::AddPathRunnableMethod(
+ PathRunnablesParametersWrapper* aWrappedParameters)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
+
+ // We return immediately if |mShuttingDown| is true (see below for
+ // details about the shutdown protocol being followed).
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ if (!wrappedParameters ||
+ !wrappedParameters->mChangeCallbackHandle) {
+ FILEWATCHERLOG("NativeFileWatcherIOTask::AddPathRunnableMethod - Invalid arguments.");
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Is aPathToWatch already being watched?
+ WatchedResourceDescriptor* watchedResource =
+ mWatchedResourcesByPath.Get(wrappedParameters->mPath);
+ if (watchedResource) {
+ // Append it to the hash tables.
+ AppendCallbacksToHashtables(
+ watchedResource->mPath,
+ wrappedParameters->mChangeCallbackHandle,
+ wrappedParameters->mErrorCallbackHandle);
+
+ return NS_OK;
+ }
+
+ // Retrieve a file handle to associate with the completion port. Makes
+ // sure to request the appropriate rights (i.e. read files and list
+ // files contained in a folder). Note: the nullptr security flag prevents
+ // the |HANDLE| to be passed to child processes.
+ HANDLE resHandle = CreateFileW(wrappedParameters->mPath.get(),
+ FILE_LIST_DIRECTORY, // Access rights
+ FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, // Share
+ nullptr, // Security flags
+ OPEN_EXISTING, // Returns an handle only if the resource exists
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
+ nullptr); // Template file (only used when creating files)
+ if (resHandle == INVALID_HANDLE_VALUE) {
+ DWORD dwError = GetLastError();
+ nsresult rv;
+ if (dwError == ERROR_FILE_NOT_FOUND) {
+ rv = NS_ERROR_FILE_NOT_FOUND;
+ } else if (dwError == ERROR_ACCESS_DENIED) {
+ rv = NS_ERROR_FILE_ACCESS_DENIED;
+ } else {
+ rv = NS_ERROR_FAILURE;
+ }
+
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::AddPathRunnableMethod - CreateFileW failed (error %x) for %S.",
+ dwError, wrappedParameters->mPath.get());
+
+ rv = ReportError(wrappedParameters->mErrorCallbackHandle, rv, dwError);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::AddPathRunnableMethod - "
+ "Failed to dispatch the error callback (%x).",
+ rv);
+ return rv;
+ }
+
+ // Error has already been reported through mErrorCallback.
+ return NS_OK;
+ }
+
+ // Initialise the resource descriptor.
+ UniquePtr<WatchedResourceDescriptor> resourceDesc(
+ new WatchedResourceDescriptor(wrappedParameters->mPath, resHandle));
+
+ // Associate the file with the previously opened completion port.
+ if (!CreateIoCompletionPort(resourceDesc->mResourceHandle, mIOCompletionPort,
+ (ULONG_PTR)resourceDesc->mResourceHandle.get(), 0)) {
+ DWORD dwError = GetLastError();
+
+ FILEWATCHERLOG("NativeFileWatcherIOTask::AddPathRunnableMethod"
+ " - CreateIoCompletionPort failed (error %x) for %S.",
+ dwError, wrappedParameters->mPath.get());
+
+ // This could fail because passed parameters could be invalid |HANDLE|s
+ // i.e. mIOCompletionPort was unexpectedly closed or failed.
+ nsresult rv =
+ ReportError(wrappedParameters->mErrorCallbackHandle, NS_ERROR_UNEXPECTED, dwError);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::AddPathRunnableMethod - "
+ "Failed to dispatch the error callback (%x).",
+ rv);
+ return rv;
+ }
+
+ // Error has already been reported through mErrorCallback.
+ return NS_OK;
+ }
+
+ // Append the callbacks to the hash tables. We do this now since
+ // AddDirectoryToWatchList could use the error callback, but we
+ // need to make sure to remove them if AddDirectoryToWatchList fails.
+ AppendCallbacksToHashtables(
+ wrappedParameters->mPath,
+ wrappedParameters->mChangeCallbackHandle,
+ wrappedParameters->mErrorCallbackHandle);
+
+ // We finally watch the resource for changes.
+ nsresult rv = AddDirectoryToWatchList(resourceDesc.get());
+ if (NS_SUCCEEDED(rv)) {
+ // Add the resource pointer to both indexes.
+ WatchedResourceDescriptor* resource = resourceDesc.release();
+ mWatchedResourcesByPath.Put(wrappedParameters->mPath, resource);
+ mWatchedResourcesByHandle.Put(resHandle, resource);
+
+ // Dispatch the success callback.
+ nsresult rv =
+ ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::AddPathRunnableMethod - "
+ "Failed to dispatch the success callback (%x).",
+ rv);
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ // We failed to watch the folder. Remove the callbacks
+ // from the hash tables.
+ RemoveCallbacksFromHashtables(
+ wrappedParameters->mPath,
+ wrappedParameters->mChangeCallbackHandle,
+ wrappedParameters->mErrorCallbackHandle);
+
+ if (rv != NS_ERROR_ABORT) {
+ // Just don't add the descriptor to the watch list.
+ return NS_OK;
+ }
+
+ // We failed to dispatch the error callbacks as well.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::AddPathRunnableMethod - Failed to watch %s and"
+ " to dispatch the related error callbacks", resourceDesc->mPath.get());
+
+ return rv;
+}
+
+/**
+ * Removes the path from the list of watched resources. Silently ignores the request
+ * if the path was not being watched.
+ *
+ * Remove Protocol:
+ *
+ * 1. Find the resource to unwatch through the provided path.
+ * 2. Remove the error and change callbacks from the list of callbacks
+ * associated with the resource.
+ * 3. Remove the error and change callbacks from the callback hash maps.
+ * 4. If there are no more change callbacks for the resource, close
+ * its file |HANDLE|. We do not free the buffer memory just yet, it's
+ * still needed for the next call to GetQueuedCompletionStatus. That
+ * memory will be freed in NativeFileWatcherIOTask::Run.
+ *
+ * @param aWrappedParameters
+ * The structure containing the resource path, the error and change callback
+ * handles.
+ */
+nsresult
+NativeFileWatcherIOTask::RemovePathRunnableMethod(
+ PathRunnablesParametersWrapper* aWrappedParameters)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
+
+ // We return immediately if |mShuttingDown| is true (see below for
+ // details about the shutdown protocol being followed).
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ if (!wrappedParameters ||
+ !wrappedParameters->mChangeCallbackHandle) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ WatchedResourceDescriptor* toRemove =
+ mWatchedResourcesByPath.Get(wrappedParameters->mPath);
+ if (!toRemove) {
+ // We are trying to remove a path which wasn't being watched. Silently ignore
+ // and dispatch the success callback.
+ nsresult rv =
+ ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::RemovePathRunnableMethod - "
+ "Failed to dispatch the success callback (%x).",
+ rv);
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ ChangeCallbackArray* changeCallbackArray =
+ mChangeCallbacksTable.Get(toRemove->mPath);
+
+ // This should always be valid.
+ MOZ_ASSERT(changeCallbackArray);
+
+ bool removed =
+ changeCallbackArray->RemoveElement(wrappedParameters->mChangeCallbackHandle);
+ if (!removed) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::RemovePathRunnableMethod - Unable to remove the change "
+ "callback from the change callback hash map for %S.",
+ wrappedParameters->mPath.get());
+ MOZ_CRASH();
+ }
+
+ ErrorCallbackArray* errorCallbackArray =
+ mErrorCallbacksTable.Get(toRemove->mPath);
+
+ MOZ_ASSERT(errorCallbackArray);
+
+ removed =
+ errorCallbackArray->RemoveElement(wrappedParameters->mErrorCallbackHandle);
+ if (!removed) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::RemovePathRunnableMethod - Unable to remove the error "
+ "callback from the error callback hash map for %S.",
+ wrappedParameters->mPath.get());
+ MOZ_CRASH();
+ }
+
+ // If there are still callbacks left, keep the descriptor.
+ // We don't check for error callbacks since there's no point in keeping
+ // the descriptor if there are no change callbacks but some error callbacks.
+ if (changeCallbackArray->Length()) {
+ // Dispatch the success callback.
+ nsresult rv =
+ ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::RemovePathRunnableMethod - "
+ "Failed to dispatch the success callback (%x).",
+ rv);
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ // In this runnable, we just cancel callbacks (see above) and I/O (see below).
+ // Resources are freed by the worker thread when GetQueuedCompletionStatus
+ // detects that a resource was removed from the watch list.
+ // Since when closing |HANDLE|s relative to watched resources
+ // GetQueuedCompletionStatus is notified one last time, it would end
+ // up referring to deallocated memory if we were to free the memory here.
+ // This happens because the worker IO is scheduled to watch the resources
+ // again once we complete executing this function.
+
+ // Enforce CloseHandle/CancelIO by disposing the AutoCloseHandle. We don't
+ // remove the entry from mWatchedResourceBy* since the completion port might
+ // still be using the notification buffer. Entry remove is performed when
+ // handling ERROR_OPERATION_ABORTED in NativeFileWatcherIOTask::Run.
+ toRemove->mResourceHandle.dispose();
+
+ // Dispatch the success callback.
+ nsresult rv =
+ ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::RemovePathRunnableMethod - "
+ "Failed to dispatch the success callback (%x).",
+ rv);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Removes all the watched resources from the watch list and stops the
+ * watcher thread. Frees all the used resources.
+ */
+nsresult
+NativeFileWatcherIOTask::DeactivateRunnableMethod()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Remind users to manually remove the watches before quitting.
+ MOZ_ASSERT(!mWatchedResourcesByHandle.Count(),
+ "Clients of the nsINativeFileWatcher must remove "
+ "watches manually before quitting.");
+
+ // Log any pending watch.
+ for (auto it = mWatchedResourcesByHandle.Iter(); !it.Done(); it.Next()) {
+ FILEWATCHERLOG("NativeFileWatcherIOTask::DeactivateRunnableMethod - "
+ "%S is still being watched.", it.UserData()->mPath.get());
+
+ }
+
+ // We return immediately if |mShuttingDown| is true (see below for
+ // details about the shutdown protocol being followed).
+ if (mShuttingDown) {
+ // If this happens, we are in a strange situation.
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::DeactivateRunnableMethod - We are already shutting down.");
+ MOZ_CRASH();
+ return NS_OK;
+ }
+
+ // Deactivate all the non-shutdown methods of this object.
+ mShuttingDown = true;
+
+ // Remove all the elements from the index. Memory will be freed by
+ // calling Clear() on mWatchedResourcesByPath.
+ mWatchedResourcesByHandle.Clear();
+
+ // Clear frees the memory associated with each element and clears the table.
+ // Since we are using Scoped |HANDLE|s, they get automatically closed as well.
+ mWatchedResourcesByPath.Clear();
+
+ // Now that all the descriptors are closed, release the callback hahstables.
+ mChangeCallbacksTable.Clear();
+ mErrorCallbacksTable.Clear();
+
+ // Close the IO completion port, eventually making
+ // the watcher thread exit from the watching loop.
+ if (mIOCompletionPort) {
+ if (!CloseHandle(mIOCompletionPort)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::DeactivateRunnableMethod - "
+ "Failed to close the IO completion port HANDLE.");
+ }
+ }
+
+ // Now we just need to reschedule a final call to Shutdown() back to the main thread.
+ RefPtr<NativeWatcherIOShutdownTask> shutdownRunnable =
+ new NativeWatcherIOShutdownTask();
+
+ return NS_DispatchToMainThread(shutdownRunnable);
+}
+
+/**
+ * Helper function to dispatch a change notification to all the registered callbacks.
+ * @param aResourceDescriptor
+ * The resource descriptor.
+ * @param aChangedResource
+ * The path of the changed resource.
+ * @return NS_OK if all the callbacks are dispatched correctly, a |nsresult| error code
+ * otherwise.
+ */
+nsresult
+NativeFileWatcherIOTask::DispatchChangeCallbacks(
+ WatchedResourceDescriptor* aResourceDescriptor,
+ const nsAString& aChangedResource)
+{
+ MOZ_ASSERT(aResourceDescriptor);
+
+ // Retrieve the change callbacks array.
+ ChangeCallbackArray* changeCallbackArray =
+ mChangeCallbacksTable.Get(aResourceDescriptor->mPath);
+
+ // This should always be valid.
+ MOZ_ASSERT(changeCallbackArray);
+
+ for (size_t i = 0; i < changeCallbackArray->Length(); i++) {
+ nsresult rv =
+ ReportChange((*changeCallbackArray)[i], aChangedResource);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to post a change runnable to the main thread.
+ *
+ * @param aOnChange
+ * The change callback handle.
+ * @param aChangedResource
+ * The resource name to dispatch thorough the change callback.
+ *
+ * @return NS_OK if the callback is dispatched correctly.
+ */
+nsresult
+NativeFileWatcherIOTask::ReportChange(
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
+ const nsAString& aChangedResource)
+{
+ RefPtr<WatchedChangeEvent> changeRunnable =
+ new WatchedChangeEvent(aOnChange, aChangedResource);
+ return NS_DispatchToMainThread(changeRunnable);
+}
+
+/**
+ * Helper function to dispatch a error notification to all the registered callbacks.
+ * @param aResourceDescriptor
+ * The resource descriptor.
+ * @param anError
+ * The error to dispatch thorough the error callback.
+ * @param anOSError
+ * An OS specific error code to send with the callback.
+ * @return NS_OK if all the callbacks are dispatched correctly, a |nsresult| error code
+ * otherwise.
+ */
+nsresult
+NativeFileWatcherIOTask::DispatchErrorCallbacks(
+ WatchedResourceDescriptor* aResourceDescriptor,
+ nsresult anError, DWORD anOSError)
+{
+ MOZ_ASSERT(aResourceDescriptor);
+
+ // Retrieve the error callbacks array.
+ ErrorCallbackArray* errorCallbackArray =
+ mErrorCallbacksTable.Get(aResourceDescriptor->mPath);
+
+ // This must be valid.
+ MOZ_ASSERT(errorCallbackArray);
+
+ for (size_t i = 0; i < errorCallbackArray->Length(); i++) {
+ nsresult rv =
+ ReportError((*errorCallbackArray)[i], anError, anOSError);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to post an error runnable to the main thread.
+ *
+ * @param aOnError
+ * The error callback handle.
+ * @param anError
+ * The error to dispatch thorough the error callback.
+ * @param anOSError
+ * An OS specific error code to send with the callback.
+ *
+ * @return NS_OK if the callback is dispatched correctly.
+ */
+nsresult
+NativeFileWatcherIOTask::ReportError(
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
+ nsresult anError, DWORD anOSError)
+{
+ RefPtr<WatchedErrorEvent> errorRunnable =
+ new WatchedErrorEvent(aOnError, anError, anOSError);
+ return NS_DispatchToMainThread(errorRunnable);
+}
+
+/**
+ * Helper function to post a success runnable to the main thread.
+ *
+ * @param aOnSuccess
+ * The success callback handle.
+ * @param aResource
+ * The resource name to dispatch thorough the success callback.
+ *
+ * @return NS_OK if the callback is dispatched correctly.
+ */
+nsresult
+NativeFileWatcherIOTask::ReportSuccess(
+ const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>& aOnSuccess,
+ const nsAString& aResource)
+{
+ RefPtr<WatchedSuccessEvent> successRunnable =
+ new WatchedSuccessEvent(aOnSuccess, aResource);
+ return NS_DispatchToMainThread(successRunnable);
+}
+
+
+/**
+ * Instructs the OS to report the changes concerning the directory of interest.
+ *
+ * @param aDirectoryDescriptor
+ * A |WatchedResourceDescriptor| instance describing the directory to watch.
+ * @param aDispatchErrorCode
+ * If |ReadDirectoryChangesW| fails and dispatching an error callback to the
+ * main thread fails as well, the error code is stored here. If the OS API call
+ * does not fail, it gets set to NS_OK.
+ * @return |true| if |ReadDirectoryChangesW| returned no error, |false| otherwise.
+ */
+nsresult
+NativeFileWatcherIOTask::AddDirectoryToWatchList(
+ WatchedResourceDescriptor* aDirectoryDescriptor)
+{
+ MOZ_ASSERT(!mShuttingDown);
+
+ DWORD dwPlaceholder;
+ // Tells the OS to watch out on mResourceHandle for the changes specified
+ // with the FILE_NOTIFY_* flags. We monitor the creation, renaming and
+ // deletion of a file (FILE_NOTIFY_CHANGE_FILE_NAME), changes to the last
+ // modification time (FILE_NOTIFY_CHANGE_LAST_WRITE) and the creation and
+ // deletion of a folder (FILE_NOTIFY_CHANGE_DIR_NAME). Moreover, when you
+ // first call this function, the system allocates a buffer to store change
+ // information for the watched directory.
+ if (!ReadDirectoryChangesW(aDirectoryDescriptor->mResourceHandle,
+ aDirectoryDescriptor->mNotificationBuffer.get(),
+ NOTIFICATION_BUFFER_SIZE,
+ true, // watch subtree (recurse)
+ FILE_NOTIFY_CHANGE_LAST_WRITE
+ | FILE_NOTIFY_CHANGE_FILE_NAME
+ | FILE_NOTIFY_CHANGE_DIR_NAME,
+ &dwPlaceholder,
+ &aDirectoryDescriptor->mOverlappedInfo,
+ nullptr)) {
+ // NOTE: GetLastError() could return ERROR_INVALID_PARAMETER if the buffer length
+ // is greater than 64 KB and the application is monitoring a directory over the
+ // network. The same error could be returned when trying to watch a file instead
+ // of a directory.
+ // It could return ERROR_NOACCESS if the buffer is not aligned on a DWORD boundary.
+ DWORD dwError = GetLastError();
+
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::AddDirectoryToWatchList "
+ " - ReadDirectoryChangesW failed (error %x) for %S.",
+ dwError, aDirectoryDescriptor->mPath.get());
+
+ nsresult rv =
+ DispatchErrorCallbacks(aDirectoryDescriptor, NS_ERROR_FAILURE, dwError);
+ if (NS_FAILED(rv)) {
+ // That's really bad. We failed to watch the directory and failed to
+ // dispatch the error callbacks.
+ return NS_ERROR_ABORT;
+ }
+
+ // We failed to watch the directory, but we correctly dispatched the error callbacks.
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Appends the change and error callbacks to their respective hash tables.
+ * It also checks if the callbacks are already attached to them.
+ * @param aPath
+ * The watched directory path.
+ * @param aOnChangeHandle
+ * The callback to invoke when a change is detected.
+ * @param aOnErrorHandle
+ * The callback to invoke when an error is detected.
+ */
+void
+NativeFileWatcherIOTask::AppendCallbacksToHashtables(
+ const nsAString& aPath,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChangeHandle,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnErrorHandle)
+{
+ // First check to see if we've got an entry already.
+ ChangeCallbackArray* callbacksArray = mChangeCallbacksTable.Get(aPath);
+ if (!callbacksArray) {
+ // We don't have an entry. Create an array and put it into the hash table.
+ callbacksArray = new ChangeCallbackArray();
+ mChangeCallbacksTable.Put(aPath, callbacksArray);
+ }
+
+ // We do have an entry for that path. Check to see if the callback is
+ // already there.
+ ChangeCallbackArray::index_type changeCallbackIndex =
+ callbacksArray->IndexOf(aOnChangeHandle);
+
+ // If the callback is not attached to the descriptor, append it.
+ if (changeCallbackIndex == ChangeCallbackArray::NoIndex) {
+ callbacksArray->AppendElement(aOnChangeHandle);
+ }
+
+ // Same thing for the error callback.
+ ErrorCallbackArray* errorCallbacksArray = mErrorCallbacksTable.Get(aPath);
+ if (!errorCallbacksArray) {
+ // We don't have an entry. Create an array and put it into the hash table.
+ errorCallbacksArray = new ErrorCallbackArray();
+ mErrorCallbacksTable.Put(aPath, errorCallbacksArray);
+ }
+
+ ErrorCallbackArray::index_type errorCallbackIndex =
+ errorCallbacksArray->IndexOf(aOnErrorHandle);
+
+ if (errorCallbackIndex == ErrorCallbackArray::NoIndex) {
+ errorCallbacksArray->AppendElement(aOnErrorHandle);
+ }
+}
+
+/**
+ * Removes the change and error callbacks from their respective hash tables.
+ * @param aPath
+ * The watched directory path.
+ * @param aOnChangeHandle
+ * The change callback to remove.
+ * @param aOnErrorHandle
+ * The error callback to remove.
+ */
+void
+NativeFileWatcherIOTask::RemoveCallbacksFromHashtables(
+ const nsAString& aPath,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChangeHandle,
+ const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnErrorHandle)
+{
+ // Find the change callback array for |aPath|.
+ ChangeCallbackArray* callbacksArray = mChangeCallbacksTable.Get(aPath);
+ if (callbacksArray) {
+ // Remove the change callback.
+ callbacksArray->RemoveElement(aOnChangeHandle);
+ }
+
+ // Find the error callback array for |aPath|.
+ ErrorCallbackArray* errorCallbacksArray = mErrorCallbacksTable.Get(aPath);
+ if (errorCallbacksArray) {
+ // Remove the error callback.
+ errorCallbacksArray->RemoveElement(aOnErrorHandle);
+ }
+}
+
+/**
+ * Creates a string representing the native path for the changed resource.
+ * It appends the resource name to the path of the changed descriptor by
+ * using nsILocalFile.
+ * @param changedDescriptor
+ * The descriptor of the watched resource.
+ * @param resourceName
+ * The resource which triggered the change.
+ * @param nativeResourcePath
+ * The full path to the changed resource.
+ * @return NS_OK if nsILocalFile succeeded in building the path.
+ */
+nsresult
+NativeFileWatcherIOTask::MakeResourcePath(
+ WatchedResourceDescriptor* changedDescriptor,
+ const nsAString& resourceName,
+ nsAString& nativeResourcePath)
+{
+ nsCOMPtr<nsILocalFile>
+ localPath(do_CreateInstance("@mozilla.org/file/local;1"));
+ if (!localPath) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::MakeResourcePath - Failed to create a nsILocalFile instance.");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = localPath->InitWithPath(changedDescriptor->mPath);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::MakeResourcePath - Failed to init nsILocalFile with %S (%x).",
+ changedDescriptor->mPath.get(), rv);
+ return rv;
+ }
+
+ rv = localPath->AppendRelativePath(resourceName);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::MakeResourcePath - Failed to append to %S (%x).",
+ changedDescriptor->mPath.get(), rv);
+ return rv;
+ }
+
+ rv = localPath->GetPath(nativeResourcePath);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::MakeResourcePath - Failed to get native path from nsILocalFile (%x).",
+ rv);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace
+
+// The NativeFileWatcherService component
+
+NS_IMPL_ISUPPORTS(NativeFileWatcherService, nsINativeFileWatcherService, nsIObserver);
+
+NativeFileWatcherService::NativeFileWatcherService()
+{
+}
+
+NativeFileWatcherService::~NativeFileWatcherService()
+{
+}
+
+/**
+ * Sets the required resources and starts the watching IO thread.
+ *
+ * @return NS_OK if there was no error with thread creation and execution.
+ */
+nsresult
+NativeFileWatcherService::Init()
+{
+ // Creates an IO completion port and allows at most 2 thread to access it concurrently.
+ AutoCloseHandle completionPort(
+ CreateIoCompletionPort(INVALID_HANDLE_VALUE, // FileHandle
+ nullptr, // ExistingCompletionPort
+ 0, // CompletionKey
+ 2)); // NumberOfConcurrentThreads
+ if (!completionPort) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add an observer for the shutdown.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) {
+ return NS_ERROR_FAILURE;
+ }
+
+ observerService->AddObserver(this, "xpcom-shutdown-threads", false);
+
+ // Start the IO worker thread.
+ mWorkerIORunnable = new NativeFileWatcherIOTask(completionPort);
+ nsresult rv = NS_NewThread(getter_AddRefs(mIOThread), mWorkerIORunnable);
+ if (NS_FAILED(rv)) {
+ FILEWATCHERLOG(
+ "NativeFileWatcherIOTask::Init - Unable to create and dispatch the worker thread (%x).",
+ rv);
+ return rv;
+ }
+
+ // Set the name for the worker thread.
+ NS_SetThreadName(mIOThread, "FileWatcher IO");
+
+ mIOCompletionPort = completionPort.forget();
+
+ return NS_OK;
+}
+
+/**
+ * Watches a path for changes: monitors the creations, name changes and
+ * content changes to the files contained in the watched path.
+ *
+ * @param aPathToWatch
+ * The path of the resource to watch for changes.
+ * @param aOnChange
+ * The callback to invoke when a change is detected.
+ * @param aOnError
+ * The optional callback to invoke when there's an error.
+ * @param aOnSuccess
+ * The optional callback to invoke when the file watcher starts
+ * watching the resource for changes.
+ *
+ * @return NS_OK or NS_ERROR_NOT_INITIALIZED if the instance was not initialized.
+ * Other errors are reported by the error callback function.
+ */
+NS_IMETHODIMP
+NativeFileWatcherService::AddPath(const nsAString& aPathToWatch,
+ nsINativeFileWatcherCallback* aOnChange,
+ nsINativeFileWatcherErrorCallback* aOnError,
+ nsINativeFileWatcherSuccessCallback* aOnSuccess)
+{
+ // Make sure the instance was initialized.
+ if (!mIOThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Be sure a valid change callback was passed.
+ if (!aOnChange) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsMainThreadPtrHandle<nsINativeFileWatcherCallback> changeCallbackHandle(
+ new nsMainThreadPtrHolder<nsINativeFileWatcherCallback>(aOnChange));
+
+ nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> errorCallbackHandle(
+ new nsMainThreadPtrHolder<nsINativeFileWatcherErrorCallback>(aOnError));
+
+ nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> successCallbackHandle(
+ new nsMainThreadPtrHolder<nsINativeFileWatcherSuccessCallback>(aOnSuccess));
+
+ // Wrap the path and the callbacks in order to pass them using NewRunnableMethod.
+ UniquePtr<PathRunnablesParametersWrapper> wrappedCallbacks(
+ new PathRunnablesParametersWrapper(
+ aPathToWatch,
+ changeCallbackHandle,
+ errorCallbackHandle,
+ successCallbackHandle));
+
+ // Since this function does a bit of I/O stuff , run it in the IO thread.
+ nsresult rv =
+ mIOThread->Dispatch(
+ NewRunnableMethod<PathRunnablesParametersWrapper*>(
+ static_cast<NativeFileWatcherIOTask*>(mWorkerIORunnable.get()),
+ &NativeFileWatcherIOTask::AddPathRunnableMethod,
+ wrappedCallbacks.get()),
+ nsIEventTarget::DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Since the dispatch succeeded, we let the runnable own the pointer.
+ wrappedCallbacks.release();
+
+ WakeUpWorkerThread();
+
+ return NS_OK;
+}
+
+/**
+ * Removes the path from the list of watched resources. Silently ignores the request
+ * if the path was not being watched or the callbacks were not registered.
+ *
+ * @param aPathToRemove
+ * The path of the resource to remove from the watch list.
+ * @param aOnChange
+ * The callback to invoke when a change is detected.
+ * @param aOnError
+ * The optionally registered callback invoked when there's an error.
+ * @param aOnSuccess
+ * The optional callback to invoke when the file watcher stops
+ * watching the resource for changes.
+ *
+ * @return NS_OK or NS_ERROR_NOT_INITIALIZED if the instance was not initialized.
+ * Other errors are reported by the error callback function.
+ */
+NS_IMETHODIMP
+NativeFileWatcherService::RemovePath(const nsAString& aPathToRemove,
+ nsINativeFileWatcherCallback* aOnChange,
+ nsINativeFileWatcherErrorCallback* aOnError,
+ nsINativeFileWatcherSuccessCallback* aOnSuccess)
+{
+ // Make sure the instance was initialized.
+ if (!mIOThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Be sure a valid change callback was passed.
+ if (!aOnChange) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsMainThreadPtrHandle<nsINativeFileWatcherCallback> changeCallbackHandle(
+ new nsMainThreadPtrHolder<nsINativeFileWatcherCallback>(aOnChange));
+
+ nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> errorCallbackHandle(
+ new nsMainThreadPtrHolder<nsINativeFileWatcherErrorCallback>(aOnError));
+
+ nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> successCallbackHandle(
+ new nsMainThreadPtrHolder<nsINativeFileWatcherSuccessCallback>(aOnSuccess));
+
+ // Wrap the path and the callbacks in order to pass them using NewRunnableMethod.
+ UniquePtr<PathRunnablesParametersWrapper> wrappedCallbacks(
+ new PathRunnablesParametersWrapper(
+ aPathToRemove,
+ changeCallbackHandle,
+ errorCallbackHandle,
+ successCallbackHandle));
+
+ // Since this function does a bit of I/O stuff, run it in the IO thread.
+ nsresult rv =
+ mIOThread->Dispatch(
+ NewRunnableMethod<PathRunnablesParametersWrapper*>(
+ static_cast<NativeFileWatcherIOTask*>(mWorkerIORunnable.get()),
+ &NativeFileWatcherIOTask::RemovePathRunnableMethod,
+ wrappedCallbacks.get()),
+ nsIEventTarget::DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Since the dispatch succeeded, we let the runnable own the pointer.
+ wrappedCallbacks.release();
+
+ WakeUpWorkerThread();
+
+ return NS_OK;
+}
+
+/**
+ * Removes all the watched resources from the watch list and stops the
+ * watcher thread. Frees all the used resources.
+ *
+ * To avoid race conditions, we need a Shutdown Protocol:
+ *
+ * 1. [MainThread]
+ * When the "xpcom-shutdown-threads" event is detected, Uninit() gets called.
+ * 2. [MainThread]
+ * Uninit sends DeactivateRunnableMethod() to the WorkerThread.
+ * 3. [WorkerThread]
+ * DeactivateRunnableMethod makes it clear to other methods that shutdown is
+ * in progress, stops the IO completion port wait and schedules the rest of the
+ * deactivation for after every currently pending method call is complete.
+ */
+nsresult
+NativeFileWatcherService::Uninit()
+{
+ // Make sure the instance was initialized (and not de-initialized yet).
+ if (!mIOThread) {
+ return NS_OK;
+ }
+
+ // We need to be sure that there will be no calls to 'mIOThread' once we have entered
+ // 'Uninit()', even if we exit due to an error.
+ nsCOMPtr<nsIThread> ioThread;
+ ioThread.swap(mIOThread);
+
+ // Since this function does a bit of I/O stuff (close file handle), run it
+ // in the IO thread.
+ nsresult rv =
+ ioThread->Dispatch(
+ NewRunnableMethod(
+ static_cast<NativeFileWatcherIOTask*>(mWorkerIORunnable.get()),
+ &NativeFileWatcherIOTask::DeactivateRunnableMethod),
+ nsIEventTarget::DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ WakeUpWorkerThread();
+
+ return NS_OK;
+}
+
+/**
+ * Tells |NativeFileWatcherIOTask| to quit and to reschedule itself in order to
+ * execute the other runnables enqueued in the worker tread.
+ * This works by posting a bogus event to the blocking |GetQueuedCompletionStatus|
+ * call in |NativeFileWatcherIOTask::Run()|.
+ */
+void
+NativeFileWatcherService::WakeUpWorkerThread()
+{
+ // The last 3 parameters represent the number of transferred bytes, the changed
+ // resource |HANDLE| and the address of the |OVERLAPPED| structure passed to
+ // GetQueuedCompletionStatus: we set them to nullptr so that we can recognize
+ // that we requested an interruption from the Worker thread.
+ PostQueuedCompletionStatus(mIOCompletionPort, 0, 0, nullptr);
+}
+
+/**
+ * This method is used to catch the "xpcom-shutdown-threads" event in order
+ * to shutdown this service when closing the application.
+ */
+NS_IMETHODIMP
+NativeFileWatcherService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!strcmp("xpcom-shutdown-threads", aTopic)) {
+ nsresult rv = Uninit();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(false, "NativeFileWatcherService got an unexpected topic!");
+
+ return NS_ERROR_UNEXPECTED;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/filewatcher/NativeFileWatcherWin.h b/toolkit/components/filewatcher/NativeFileWatcherWin.h
new file mode 100644
index 0000000000..37dd97f84d
--- /dev/null
+++ b/toolkit/components/filewatcher/NativeFileWatcherWin.h
@@ -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/. */
+
+#ifndef mozilla_nativefilewatcher_h__
+#define mozilla_nativefilewatcher_h__
+
+#include "nsINativeFileWatcher.h"
+#include "nsIObserver.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsThreadUtils.h"
+
+// We need to include this header here for HANDLE definition.
+#include <windows.h>
+
+namespace mozilla {
+
+class NativeFileWatcherService final : public nsINativeFileWatcherService,
+ public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINATIVEFILEWATCHERSERVICE
+ NS_DECL_NSIOBSERVER
+
+ NativeFileWatcherService();
+
+ nsresult Init();
+
+private:
+ // The |HANDLE| to the I/O Completion Port, owned by the main thread.
+ HANDLE mIOCompletionPort;
+ nsCOMPtr<nsIThread> mIOThread;
+
+ // The instance of the runnable dealing with the I/O.
+ nsCOMPtr<nsIRunnable> mWorkerIORunnable;
+
+ nsresult Uninit();
+ void WakeUpWorkerThread();
+
+ // Make the dtor private to make this object only deleted via its ::Release() method.
+ ~NativeFileWatcherService();
+ NativeFileWatcherService(const NativeFileWatcherService& other) = delete;
+ void operator=(const NativeFileWatcherService& other) = delete;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_nativefilewatcher_h__
diff --git a/toolkit/components/filewatcher/moz.build b/toolkit/components/filewatcher/moz.build
new file mode 100644
index 0000000000..5ce94b5d26
--- /dev/null
+++ b/toolkit/components/filewatcher/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ EXPORTS += ['NativeFileWatcherWin.h']
+ UNIFIED_SOURCES += [
+ 'NativeFileWatcherWin.cpp',
+ ]
+else:
+ EXPORTS += ['NativeFileWatcherNotSupported.h']
+
+XPIDL_MODULE = 'toolkit_filewatcher'
+
+XPIDL_SOURCES += [
+ 'nsINativeFileWatcher.idl',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/filewatcher/nsINativeFileWatcher.idl b/toolkit/components/filewatcher/nsINativeFileWatcher.idl
new file mode 100644
index 0000000000..afbe684c4a
--- /dev/null
+++ b/toolkit/components/filewatcher/nsINativeFileWatcher.idl
@@ -0,0 +1,111 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=40: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+/**
+ * The interface for the callback invoked when there is an error.
+ */
+[scriptable, function, uuid(5DAEDDC3-FC94-4880-8A4F-26D910B92662)]
+interface nsINativeFileWatcherErrorCallback: nsISupports
+{
+ /**
+ * @param xpcomError The XPCOM error code.
+ * @param osError The native OS error (errno under Unix, GetLastError under Windows).
+ */
+ void complete(in nsresult xpcomError, in long osError);
+};
+
+/**
+ * The interface for the callback invoked when a change on a watched
+ * resource is detected.
+ */
+[scriptable, function, uuid(FE4D86C9-243F-4195-B544-AECE3DF4B86A)]
+interface nsINativeFileWatcherCallback: nsISupports
+{
+ /**
+ * @param resourcePath
+ * The path of the changed resource. If there were too many changes,
+ * the string "*" is passed.
+ * @param flags Reserved for future uses, not currently used.
+ */
+ void changed(in AString resourcePath, in int32_t flags);
+};
+
+/**
+ * The interface for the callback invoked when a file watcher operation
+ * successfully completes.
+ */
+[scriptable, function, uuid(C3D7F542-681B-4ABD-9D65-9D799B29A42B)]
+interface nsINativeFileWatcherSuccessCallback: nsISupports
+{
+ /**
+ * @param resourcePath
+ * The path of the resource for which the operation completes.
+ */
+ void complete(in AString resourcePath);
+};
+
+/**
+ * A service providing native implementations of path changes notification.
+ */
+[scriptable, builtinclass, uuid(B3A4E8D8-7DC8-47DB-A8B4-83736D7AC1AA)]
+interface nsINativeFileWatcherService: nsISupports
+{
+ /**
+ * Watches the passed path for changes. If it's a directory, every file
+ * it contains is watched. Recursively watches subdirectories. If the
+ * resource is already being watched, does nothing. If the passed path
+ * is a file, the behaviour is not specified.
+ *
+ * @param pathToWatch The path to watch for changes.
+ * @param onChange
+ * The callback invoked whenever a change on a watched
+ * resource is detected.
+ * @param onError
+ * The optional callback invoked whenever an error occurs.
+ * @param onSuccess
+ * The optional callback invoked when the file watcher starts
+ * watching the resource for changes.
+ */
+ void addPath(in AString pathToWatch,
+ in nsINativeFileWatcherCallback onChange,
+ [optional] in nsINativeFileWatcherErrorCallback onError,
+ [optional] in nsINativeFileWatcherSuccessCallback onSuccess);
+
+ /**
+ * Removes the provided path from the watched resources. If the path
+ * was not being watched or the callbacks were not registered, silently
+ * ignores the request.
+ * Please note that the file watcher only considers the onChange callbacks
+ * when deciding to close a watch on a resource. If there are no more onChange
+ * callbacks associated to the watch, it gets closed (even though it still has
+ * some error callbacks associated).
+ *
+ * @param pathToUnwatch The path to un-watch.
+ * @param onChange
+ * The registered callback invoked whenever a change on a watched
+ * resource is detected.
+ * @param onError
+ * The optionally registered callback invoked whenever an error
+ * occurs.
+ * @param onSuccess
+ * The optional callback invoked when the file watcher stops
+ * watching the resource for changes.
+ */
+ void removePath(in AString pathToUnwatch,
+ in nsINativeFileWatcherCallback onChange,
+ [optional] in nsINativeFileWatcherErrorCallback onError,
+ [optional] in nsINativeFileWatcherSuccessCallback onSuccess);
+};
+
+
+%{ C++
+
+#define NATIVE_FILEWATCHER_SERVICE_CID {0x6F488507, 0x469D, 0x4350, {0xA6, 0x8D, 0x99, 0xC8, 0x7, 0xBE, 0xA, 0x78}}
+#define NATIVE_FILEWATCHER_SERVICE_CONTRACTID "@mozilla.org/toolkit/filewatcher/native-file-watcher;1"
+
+%}
diff --git a/toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js b/toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/filewatcher/tests/xpcshell/head.js b/toolkit/components/filewatcher/tests/xpcshell/head.js
new file mode 100644
index 0000000000..73f8ac4f5f
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/head.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+
+function makeWatcher() {
+ let watcher =
+ Cc['@mozilla.org/toolkit/filewatcher/native-file-watcher;1']
+ .getService(Ci.nsINativeFileWatcherService);
+ return watcher;
+}
+
+function promiseAddPath(watcher, resource, onChange=null, onError=null) {
+ return new Promise(resolve =>
+ watcher.addPath(resource, onChange, onError, resolve)
+ );
+}
+
+function promiseRemovePath(watcher, resource, onChange=null, onError=null) {
+ return new Promise(resolve =>
+ watcher.removePath(resource, onChange, onError, resolve)
+ );
+}
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_arguments.js b/toolkit/components/filewatcher/tests/xpcshell/test_arguments.js
new file mode 100644
index 0000000000..7e62b1cb60
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_arguments.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Test for addPath usage with null arguments.
+ */
+add_task(function* test_null_args_addPath() {
+
+ let watcher = makeWatcher();
+ let testPath = 'someInvalidPath';
+
+ // Define a dummy callback function. In this test no callback is
+ // expected to be called.
+ let dummyFunc = function(changed) {
+ do_throw("Not expected in this test.");
+ };
+
+ // Check for error when passing a null first argument
+ try {
+ watcher.addPath(testPath, null, dummyFunc);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NULL_POINTER)
+ throw ex;
+ do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected.");
+ }
+
+ // Check for error when passing both null arguments
+ try {
+ watcher.addPath(testPath, null, null);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NULL_POINTER)
+ throw ex;
+ do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected.");
+ }
+});
+
+/**
+ * Test for removePath usage with null arguments.
+ */
+add_task(function* test_null_args_removePath() {
+
+ let watcher = makeWatcher();
+ let testPath = 'someInvalidPath';
+
+ // Define a dummy callback function. In this test no callback is
+ // expected to be called.
+ let dummyFunc = function(changed) {
+ do_throw("Not expected in this test.");
+ };
+
+ // Check for error when passing a null first argument
+ try {
+ watcher.removePath(testPath, null, dummyFunc);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NULL_POINTER)
+ throw ex;
+ do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected.");
+ }
+
+ // Check for error when passing both null arguments
+ try {
+ watcher.removePath(testPath, null, null);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NULL_POINTER)
+ throw ex;
+ do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected.");
+ }
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.js b/toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.js
new file mode 100644
index 0000000000..e5ceb33e55
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Test the component behaves correctly when no error callback is
+ * provided and an error occurs.
+ */
+add_task(function* test_error_with_no_error_callback() {
+
+ let watcher = makeWatcher();
+ let testPath = 'someInvalidPath';
+
+ // Define a dummy callback function. In this test no callback is
+ // expected to be called.
+ let dummyFunc = function(changed) {
+ do_throw("Not expected in this test.");
+ };
+
+ // We don't pass an error callback and try to watch an invalid
+ // path.
+ watcher.addPath(testPath, dummyFunc);
+});
+
+/**
+ * Test the component behaves correctly when no error callback is
+ * provided (no error should occur).
+ */
+add_task(function* test_watch_single_path_file_creation_no_error_cb() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ let tempFileName = "test_filecreation.tmp";
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Watch the profile directory but do not pass an error callback.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve);
+
+ // Create a file within the watched directory.
+ let tmpFilePath = OS.Path.join(watchedDir, tempFileName);
+ yield OS.File.writeAtomic(tmpFilePath, "some data");
+
+ // Wait until the watcher informs us that the file was created.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpFilePath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' to unregister).
+ watcher.removePath(watchedDir, deferred.resolve);
+
+ // Remove the test directory and all of its content.
+ yield OS.File.removeDir(watchedDir);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.js b/toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.js
new file mode 100644
index 0000000000..1375584a38
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Test removing non watched path
+ */
+add_task(function* test_remove_not_watched() {
+ let nonExistingDir =
+ OS.Path.join(OS.Constants.Path.profileDir, "absolutelyNotExisting");
+
+ // Instantiate the native watcher.
+ let watcher = makeWatcher();
+
+ // Try to un-watch a path which wasn't being watched.
+ watcher.removePath(
+ nonExistingDir,
+ function(changed) {
+ do_throw("No change is expected in this test.");
+ },
+ function(xpcomError, osError) {
+ // When removing a resource which wasn't being watched, it should silently
+ // ignore the request.
+ do_throw("Unexpected exception: "
+ + xpcomError + " (XPCOM) "
+ + osError + " (OS Error)");
+ }
+ );
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.js b/toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.js
new file mode 100644
index 0000000000..482ba6b8b1
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Test the watcher correctly handles two watches sharing the same
+ * change callback.
+ */
+add_task(function* test_watch_with_shared_callback() {
+
+ // Create and watch two sub-directories of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDirs =
+ [
+ OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"),
+ OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground2")
+ ];
+
+ yield OS.File.makeDir(watchedDirs[0]);
+ yield OS.File.makeDir(watchedDirs[1]);
+
+ let tempFileName = "test_filecreation.tmp";
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Watch both directories using the same callbacks.
+ yield promiseAddPath(watcher, watchedDirs[0], deferred.resolve, deferred.reject);
+ yield promiseAddPath(watcher, watchedDirs[1], deferred.resolve, deferred.reject);
+
+ // Remove the watch for the first directory, but keep watching
+ // for changes in the second: we need to make sure the callback
+ // survives the removal of the first watch.
+ watcher.removePath(watchedDirs[0], deferred.resolve, deferred.reject);
+
+ // Create a file within the watched directory.
+ let tmpFilePath = OS.Path.join(watchedDirs[1], tempFileName);
+ yield OS.File.writeAtomic(tmpFilePath, "some data");
+
+ // Wait until the watcher informs us that the file was created.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpFilePath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ watcher.removePath(watchedDirs[1], deferred.resolve, deferred.reject);
+
+ // Remove the test directories and all of their content.
+ yield OS.File.removeDir(watchedDirs[0]);
+ yield OS.File.removeDir(watchedDirs[1]);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.js
new file mode 100644
index 0000000000..a434ec751a
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files
+ do_get_profile();
+
+ // Start executing the tests
+ run_next_test();
+}
+
+/**
+ * Tests that the watcher correctly notifies of a directory creation when watching
+ * a single path.
+ */
+add_task(function* test_watch_single_path_directory_creation() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ let tmpDirPath = OS.Path.join(watchedDir, "testdir");
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Once ready, create a directory within the watched directory.
+ yield OS.File.makeDir(tmpDirPath);
+
+ // Wait until the watcher informs us that the file has changed.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpDirPath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Clean up the test directory.
+ yield OS.File.removeDir(watchedDir);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js
new file mode 100644
index 0000000000..2c74a93616
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js
@@ -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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Tests that the watcher correctly notifies of a directory deletion when watching
+ * a single path.
+ */
+add_task(function* test_watch_single_path_directory_deletion() {
+
+ let watchedDir = OS.Constants.Path.profileDir;
+ let tempDirName = "test";
+ let tmpDirPath = OS.Path.join(watchedDir, tempDirName);
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Create a directory within the watched directory.
+ yield OS.File.makeDir(tmpDirPath);
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching it.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Remove the directory.
+ OS.File.removeDir(tmpDirPath);
+
+ // Wait until the watcher informs us that the file has changed.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpDirPath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js
new file mode 100644
index 0000000000..9f87793f44
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Test the watcher correctly notifies of a file creation when watching
+ * a single path.
+ */
+add_task(function* test_watch_single_path_file_creation() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ let tempFileName = "test_filecreation.tmp";
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ let tmpFilePath = OS.Path.join(watchedDir, tempFileName);
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // create the file within the watched directory.
+ yield OS.File.writeAtomic(tmpFilePath, "some data");
+
+ // Wait until the watcher informs us that the file was created.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpFilePath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Remove the test directory and all of its content.
+ yield OS.File.removeDir(watchedDir);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js
new file mode 100644
index 0000000000..97d2d61bc2
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+/**
+ * Test the watcher correctly notifies of a file deletion when watching
+ * a single path.
+ */
+add_task(function* test_watch_single_path_file_deletion() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ let tempFileName = "test_filedeletion.tmp";
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Create a file within the directory to be watched. We do this
+ // before watching the directory so we do not get the creation notification.
+ let tmpFilePath = OS.Path.join(watchedDir, tempFileName);
+ yield OS.File.writeAtomic(tmpFilePath, "some data");
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching it.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Remove the file we created (should trigger a notification).
+ do_print('Removing ' + tmpFilePath);
+ yield OS.File.remove(tmpFilePath);
+
+ // Wait until the watcher informs us that the file was deleted.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpFilePath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Remove the test directory and all of its content.
+ yield OS.File.removeDir(watchedDir);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js
new file mode 100644
index 0000000000..ba25fdff63
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files
+ do_get_profile();
+
+ // Start executing the tests
+ run_next_test();
+}
+
+/**
+ * Tests that the watcher correctly notifies of a file modification when watching
+ * a single path.
+ */
+add_task(function* test_watch_single_path_file_modification() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ let tempFileName = "test_filemodification.tmp";
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Create a file within the directory to be watched. We do this
+ // before watching the directory so we do not get the creation notification.
+ let tmpFilePath = OS.Path.join(watchedDir, tempFileName);
+ yield OS.File.writeAtomic(tmpFilePath, "some data");
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching it.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Once ready, modify the file to trigger the notification.
+ yield OS.File.writeAtomic(tmpFilePath, "some new data");
+
+ // Wait until the watcher informs us that the file has changed.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpFilePath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Remove the test directory and all of its content.
+ yield OS.File.removeDir(watchedDir);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.js
new file mode 100644
index 0000000000..c236c6e1de
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files
+ do_get_profile();
+
+ // Start executing the tests
+ run_next_test();
+}
+
+/**
+ * Test that we correctly handle watching directories when hundreds of files
+ * change simultaneously.
+ */
+add_task(function* test_fill_notification_buffer() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ // The number of files to create.
+ let numberOfFiles = 100;
+ let fileNameBase = "testFile";
+
+ // This will be used to keep track of the number of changes within the watched
+ // directory.
+ let detectedChanges = 0;
+
+ // We expect at least the following notifications for each file:
+ // - File creation
+ // - File deletion
+ let expectedChanges = numberOfFiles * 2;
+
+ // Instantiate the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Initialise the change callback.
+ let changeCallback = function(changed) {
+ do_print(changed + " has changed.");
+
+ detectedChanges += 1;
+
+ // Resolve the promise if we get all the expected changes.
+ if (detectedChanges >= expectedChanges) {
+ deferred.resolve();
+ }
+ };
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching it.
+ yield promiseAddPath(watcher, watchedDir, changeCallback, deferred.reject);
+
+ // Create and then remove the files within the watched directory.
+ for (let i = 0; i < numberOfFiles; i++) {
+ let tmpFilePath = OS.Path.join(watchedDir, fileNameBase + i);
+ yield OS.File.writeAtomic(tmpFilePath, "test content");
+ yield OS.File.remove(tmpFilePath);
+ }
+
+ // Wait until the watcher informs us that all the files were
+ // created, modified and removed.
+ yield deferred.promise;
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'changeCallback' and 'errorCallback' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, changeCallback, deferred.reject);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js
new file mode 100644
index 0000000000..c55b262f19
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files
+ do_get_profile();
+
+ // Start executing the tests
+ run_next_test();
+}
+
+/**
+ * Tests the watcher by watching several resources.
+ * This test creates the specified number of directory inside the profile
+ * directory, adds each one of them to the watched list the creates
+ * a file in them in order to trigger the notification.
+ * The test keeps track of the number of times the changes callback is
+ * called in order to verify the success of the test.
+ */
+add_task(function* test_watch_multi_paths() {
+
+ // The number of resources to watch. We expect changes for
+ // creating a file within each directory.
+ let resourcesToWatch = 5;
+ let watchedDir = OS.Constants.Path.profileDir;
+
+ // The directories to be watched will be created with.
+ let tempDirNameBase = "FileWatcher_Test_";
+ let tempFileName = "test.tmp";
+
+ // Instantiate the native watcher.
+ let watcher = makeWatcher();
+
+ // This will be used to keep track of the number of changes within the watched
+ // resources.
+ let detectedChanges = 0;
+ let watchedResources = 0;
+ let unwatchedResources = 0;
+
+ let deferredChanges = Promise.defer();
+ let deferredSuccesses = Promise.defer();
+ let deferredShutdown = Promise.defer();
+
+ // Define the change callback function.
+ let changeCallback = function(changed) {
+ do_print(changed + " has changed.");
+
+ detectedChanges += 1;
+
+ // Resolve the promise if we get all the expected changes.
+ if (detectedChanges === resourcesToWatch) {
+ deferredChanges.resolve();
+ }
+ };
+
+ // Define the watch success callback function.
+ let watchSuccessCallback = function(resourcePath) {
+ do_print(resourcePath + " is being watched.");
+
+ watchedResources += 1;
+
+ // Resolve the promise when all the resources are being
+ // watched.
+ if (watchedResources === resourcesToWatch) {
+ deferredSuccesses.resolve();
+ }
+ };
+
+ // Define the watch success callback function.
+ let unwatchSuccessCallback = function(resourcePath) {
+ do_print(resourcePath + " is being un-watched.");
+
+ unwatchedResources += 1;
+
+ // Resolve the promise when all the resources are being
+ // watched.
+ if (unwatchedResources === resourcesToWatch) {
+ deferredShutdown.resolve();
+ }
+ };
+
+ // Create the directories and add them to the watched resources list.
+ for (let i = 0; i < resourcesToWatch; i++) {
+ let tmpSubDirPath = OS.Path.join(watchedDir, tempDirNameBase + i);
+ do_print("Creating the " + tmpSubDirPath + " directory.");
+ yield OS.File.makeDir(tmpSubDirPath);
+ watcher.addPath(tmpSubDirPath, changeCallback, deferredChanges.reject, watchSuccessCallback);
+ }
+
+ // Wait until the watcher informs us that all the desired resources
+ // are being watched.
+ yield deferredSuccesses.promise;
+
+ // Create a file within each watched directory.
+ for (let i = 0; i < resourcesToWatch; i++) {
+ let tmpFilePath = OS.Path.join(watchedDir, tempDirNameBase + i, tempFileName);
+ yield OS.File.writeAtomic(tmpFilePath, "test content");
+ }
+
+ // Wait until the watcher informs us that all the files were created.
+ yield deferredChanges.promise;
+
+ // Remove the directories we have created.
+ for (let i = 0; i < resourcesToWatch; i++) {
+ let tmpSubDirPath = OS.Path.join(watchedDir, tempDirNameBase + i);
+ watcher.removePath(tmpSubDirPath, changeCallback, deferredChanges.reject, unwatchSuccessCallback);
+ }
+
+ // Wait until the watcher un-watches the resources.
+ yield deferredShutdown.promise;
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.js
new file mode 100644
index 0000000000..13a3de8d36
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files.
+ do_get_profile();
+
+ // Start executing the tests.
+ run_next_test();
+}
+
+/**
+ * Test the watcher correctly notifies of a file creation in a subdirectory
+ * of the watched sub-directory (recursion).
+ */
+add_task(function* test_watch_recursively() {
+
+ // Create and watch a sub-directory of the profile directory so we don't
+ // catch notifications we're not interested in (i.e. "startupCache").
+ let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground");
+ yield OS.File.makeDir(watchedDir);
+
+ // We need at least 2 levels of directories to test recursion.
+ let subdirectory = OS.Path.join(watchedDir, "level1");
+ yield OS.File.makeDir(subdirectory);
+
+ let tempFileName = "test_filecreation.tmp";
+
+ // Instantiate and initialize the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ let tmpFilePath = OS.Path.join(subdirectory, tempFileName);
+
+ // Add the profile directory to the watch list and wait for the file watcher
+ // to start watching it.
+ yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Create a file within the subdirectory of the watched directory.
+ yield OS.File.writeAtomic(tmpFilePath, "some data");
+
+ // Wait until the watcher informs us that the file was created.
+ let changed = yield deferred.promise;
+ do_check_eq(changed, tmpFilePath);
+
+ // Remove the watch and free the associated memory (we need to
+ // reuse 'deferred.resolve' and 'deferred.reject' to unregister).
+ yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject);
+
+ // Remove the test directory and all of its content.
+ yield OS.File.removeDir(watchedDir);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js
new file mode 100644
index 0000000000..fffdff24b7
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js
@@ -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/. */
+
+"use strict";
+
+function run_test() {
+ // Set up profile. We will use profile path create some test files
+ do_get_profile();
+
+ // Start executing the tests
+ run_next_test();
+}
+
+/**
+ * Test watching non-existing path
+ */
+add_task(function* test_watching_non_existing() {
+ let notExistingDir =
+ OS.Path.join(OS.Constants.Path.profileDir, "absolutelyNotExisting");
+
+ // Instantiate the native watcher.
+ let watcher = makeWatcher();
+ let deferred = Promise.defer();
+
+ // Try watch a path which doesn't exist.
+ watcher.addPath(notExistingDir, deferred.reject, deferred.resolve);
+
+ // Wait until the watcher informs us that there was an error.
+ let error = yield deferred.promise;
+ do_check_eq(error, Components.results.NS_ERROR_FILE_NOT_FOUND);
+});
diff --git a/toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini b/toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..d6cc968ebd
--- /dev/null
+++ b/toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = os != "win"
+
+[test_arguments.js]
+[test_no_error_callback.js]
+[test_remove_non_watched.js]
+[test_shared_callback.js]
+[test_watch_file_creation_single.js]
+[test_watch_file_deletion_single.js]
+[test_watch_file_modification_single.js]
+[test_watch_directory_creation_single.js]
+[test_watch_directory_deletion_single.js]
+[test_watch_many_changes.js]
+[test_watch_multi_paths.js]
+[test_watch_recursively.js]
+[test_watch_resource.js]
diff --git a/toolkit/components/finalizationwitness/FinalizationWitnessService.cpp b/toolkit/components/finalizationwitness/FinalizationWitnessService.cpp
new file mode 100644
index 0000000000..0cf4ae52e4
--- /dev/null
+++ b/toolkit/components/finalizationwitness/FinalizationWitnessService.cpp
@@ -0,0 +1,247 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FinalizationWitnessService.h"
+
+#include "nsString.h"
+#include "jsapi.h"
+#include "js/CallNonGenericMethod.h"
+#include "mozJSComponentLoader.h"
+#include "nsZipArchive.h"
+
+#include "mozilla/Scoped.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+#include "nsThreadUtils.h"
+
+
+// Implementation of nsIFinalizationWitnessService
+
+static bool gShuttingDown = false;
+
+namespace mozilla {
+
+namespace {
+
+/**
+ * An event meant to be dispatched to the main thread upon finalization
+ * of a FinalizationWitness, unless method |forget()| has been called.
+ *
+ * Held as private data by each instance of FinalizationWitness.
+ * Important note: we maintain the invariant that these private data
+ * slots are already addrefed.
+ */
+class FinalizationEvent final: public Runnable
+{
+public:
+ FinalizationEvent(const char* aTopic,
+ const char16_t* aValue)
+ : mTopic(aTopic)
+ , mValue(aValue)
+ { }
+
+ NS_IMETHOD Run() override {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) {
+ // This is either too early or, more likely, too late for notifications.
+ // Bail out.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ (void)observerService->
+ NotifyObservers(nullptr, mTopic.get(), mValue.get());
+ return NS_OK;
+ }
+private:
+ /**
+ * The topic on which to broadcast the notification of finalization.
+ *
+ * Deallocated on the main thread.
+ */
+ const nsCString mTopic;
+
+ /**
+ * The result of converting the exception to a string.
+ *
+ * Deallocated on the main thread.
+ */
+ const nsString mValue;
+};
+
+enum {
+ WITNESS_SLOT_EVENT,
+ WITNESS_INSTANCES_SLOTS
+};
+
+/**
+ * Extract the FinalizationEvent from an instance of FinalizationWitness
+ * and clear the slot containing the FinalizationEvent.
+ */
+already_AddRefed<FinalizationEvent>
+ExtractFinalizationEvent(JSObject *objSelf)
+{
+ JS::Value slotEvent = JS_GetReservedSlot(objSelf, WITNESS_SLOT_EVENT);
+ if (slotEvent.isUndefined()) {
+ // Forget() has been called
+ return nullptr;
+ }
+
+ JS_SetReservedSlot(objSelf, WITNESS_SLOT_EVENT, JS::UndefinedValue());
+
+ return dont_AddRef(static_cast<FinalizationEvent*>(slotEvent.toPrivate()));
+}
+
+/**
+ * Finalizer for instances of FinalizationWitness.
+ *
+ * Unless method Forget() has been called, the finalizer displays an error
+ * message.
+ */
+void Finalize(JSFreeOp *fop, JSObject *objSelf)
+{
+ RefPtr<FinalizationEvent> event = ExtractFinalizationEvent(objSelf);
+ if (event == nullptr || gShuttingDown) {
+ // NB: event will be null if Forget() has been called
+ return;
+ }
+
+ // Notify observers. Since we are executed during garbage-collection,
+ // we need to dispatch the notification to the main thread.
+ nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+ if (mainThread) {
+ mainThread->Dispatch(event.forget(), NS_DISPATCH_NORMAL);
+ }
+ // We may fail at dispatching to the main thread if we arrive too late
+ // during shutdown. In that case, there is not much we can do.
+}
+
+static const JSClassOps sWitnessClassOps = {
+ nullptr /* addProperty */,
+ nullptr /* delProperty */,
+ nullptr /* getProperty */,
+ nullptr /* setProperty */,
+ nullptr /* enumerate */,
+ nullptr /* resolve */,
+ nullptr /* mayResolve */,
+ Finalize /* finalize */
+};
+
+static const JSClass sWitnessClass = {
+ "FinalizationWitness",
+ JSCLASS_HAS_RESERVED_SLOTS(WITNESS_INSTANCES_SLOTS) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &sWitnessClassOps
+};
+
+bool IsWitness(JS::Handle<JS::Value> v)
+{
+ return v.isObject() && JS_GetClass(&v.toObject()) == &sWitnessClass;
+}
+
+
+/**
+ * JS method |forget()|
+ *
+ * === JS documentation
+ *
+ * Neutralize the witness. Once this method is called, the witness will
+ * never report any error.
+ */
+bool ForgetImpl(JSContext* cx, const JS::CallArgs& args)
+{
+ if (args.length() != 0) {
+ JS_ReportErrorASCII(cx, "forget() takes no arguments");
+ return false;
+ }
+ JS::Rooted<JS::Value> valSelf(cx, args.thisv());
+ JS::Rooted<JSObject*> objSelf(cx, &valSelf.toObject());
+
+ RefPtr<FinalizationEvent> event = ExtractFinalizationEvent(objSelf);
+ if (event == nullptr) {
+ JS_ReportErrorASCII(cx, "forget() called twice");
+ return false;
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
+bool Forget(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+ return JS::CallNonGenericMethod<IsWitness, ForgetImpl>(cx, args);
+}
+
+static const JSFunctionSpec sWitnessClassFunctions[] = {
+ JS_FN("forget", Forget, 0, JSPROP_READONLY | JSPROP_PERMANENT),
+ JS_FS_END
+};
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(FinalizationWitnessService, nsIFinalizationWitnessService, nsIObserver)
+
+/**
+ * Create a new Finalization Witness.
+ *
+ * A finalization witness is an object whose sole role is to notify
+ * observers when it is gc-ed. Once the witness is created, call its
+ * method |forget()| to prevent the observers from being notified.
+ *
+ * @param aTopic The notification topic.
+ * @param aValue The notification value. Converted to a string.
+ *
+ * @constructor
+ */
+NS_IMETHODIMP
+FinalizationWitnessService::Make(const char* aTopic,
+ const char16_t* aValue,
+ JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval)
+{
+ JS::Rooted<JSObject*> objResult(aCx, JS_NewObject(aCx, &sWitnessClass));
+ if (!objResult) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (!JS_DefineFunctions(aCx, objResult, sWitnessClassFunctions)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FinalizationEvent> event = new FinalizationEvent(aTopic, aValue);
+
+ // Transfer ownership of the addrefed |event| to |objResult|.
+ JS_SetReservedSlot(objResult, WITNESS_SLOT_EVENT,
+ JS::PrivateValue(event.forget().take()));
+
+ aRetval.setObject(*objResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FinalizationWitnessService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aValue)
+{
+ MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0);
+ gShuttingDown = true;
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+FinalizationWitnessService::Init()
+{
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (!obs) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/finalizationwitness/FinalizationWitnessService.h b/toolkit/components/finalizationwitness/FinalizationWitnessService.h
new file mode 100644
index 0000000000..087cff2398
--- /dev/null
+++ b/toolkit/components/finalizationwitness/FinalizationWitnessService.h
@@ -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/. */
+
+#ifndef mozilla_finalizationwitnessservice_h__
+#define mozilla_finalizationwitnessservice_h__
+
+#include "nsIFinalizationWitnessService.h"
+#include "nsIObserver.h"
+
+namespace mozilla {
+
+/**
+ * XPConnect initializer, for use in the main thread.
+ */
+class FinalizationWitnessService final : public nsIFinalizationWitnessService,
+ public nsIObserver
+{
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFINALIZATIONWITNESSSERVICE
+ NS_DECL_NSIOBSERVER
+
+ nsresult Init();
+ private:
+ ~FinalizationWitnessService() {}
+ void operator=(const FinalizationWitnessService* other) = delete;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_finalizationwitnessservice_h__
diff --git a/toolkit/components/finalizationwitness/moz.build b/toolkit/components/finalizationwitness/moz.build
new file mode 100644
index 0000000000..85a3605347
--- /dev/null
+++ b/toolkit/components/finalizationwitness/moz.build
@@ -0,0 +1,25 @@
+# -*- 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 += [
+ 'FinalizationWitnessService.cpp',
+]
+
+XPIDL_SOURCES += [
+ 'nsIFinalizationWitnessService.idl',
+]
+
+XPIDL_MODULE = 'toolkit_finalizationwitness'
+
+EXPORTS.mozilla += [
+ 'FinalizationWitnessService.h',
+]
+
+LOCAL_INCLUDES += [
+ '/js/xpconnect/loader',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/finalizationwitness/nsIFinalizationWitnessService.idl b/toolkit/components/finalizationwitness/nsIFinalizationWitnessService.idl
new file mode 100644
index 0000000000..71b6400c46
--- /dev/null
+++ b/toolkit/components/finalizationwitness/nsIFinalizationWitnessService.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=40: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+
+[scriptable, uuid(15686f9d-483e-4361-98cd-37f1e8f1e61d)]
+interface nsIFinalizationWitnessService: nsISupports
+{
+ /**
+ * Create a new Finalization Witness.
+ *
+ * A finalization witness is an object whose sole role is to
+ * broadcast when it is garbage-collected. Once the witness is
+ * created, call method its method |forget()| to prevent the
+ * broadcast.
+ *
+ * @param aTopic The topic that the witness will broadcast using
+ * Services.obs.
+ * @param aString The string that the witness will broadcast.
+ * @return An object with a single method |forget()|.
+ */
+ [implicit_jscontext]
+ jsval make(in string aTopic, in wstring aString);
+};
+
+%{ C++
+
+#define FINALIZATIONWITNESSSERVICE_CID {0x15686f9d,0x483e,0x4361,{0x98,0xcd,0x37,0xf1,0xe8,0xf1,0xe6,0x1d}}
+#define FINALIZATIONWITNESSSERVICE_CONTRACTID "@mozilla.org/toolkit/finalizationwitness;1"
+
+%}
diff --git a/toolkit/components/find/moz.build b/toolkit/components/find/moz.build
new file mode 100644
index 0000000000..be28d0cc47
--- /dev/null
+++ b/toolkit/components/find/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/.
+
+XPIDL_SOURCES += [
+ 'nsIFindService.idl',
+]
+
+XPIDL_MODULE = 'mozfind'
+
+SOURCES += [
+ 'nsFindService.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/find/nsFindService.cpp b/toolkit/components/find/nsFindService.cpp
new file mode 100644
index 0000000000..3e80823ce0
--- /dev/null
+++ b/toolkit/components/find/nsFindService.cpp
@@ -0,0 +1,101 @@
+/* -*- 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/. */
+
+/*
+ * The sole purpose of the Find service is to store globally the
+ * last used Find settings
+ *
+ */
+
+
+#include "nsFindService.h"
+
+
+nsFindService::nsFindService()
+: mFindBackwards(false)
+, mWrapFind(true)
+, mEntireWord(false)
+, mMatchCase(false)
+{
+}
+
+
+nsFindService::~nsFindService()
+{
+}
+
+NS_IMPL_ISUPPORTS(nsFindService, nsIFindService)
+
+NS_IMETHODIMP nsFindService::GetSearchString(nsAString & aSearchString)
+{
+ aSearchString = mSearchString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFindService::SetSearchString(const nsAString & aSearchString)
+{
+ mSearchString = aSearchString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFindService::GetReplaceString(nsAString & aReplaceString)
+{
+ aReplaceString = mReplaceString;
+ return NS_OK;
+}
+NS_IMETHODIMP nsFindService::SetReplaceString(const nsAString & aReplaceString)
+{
+ mReplaceString = aReplaceString;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFindService::GetFindBackwards(bool *aFindBackwards)
+{
+ NS_ENSURE_ARG_POINTER(aFindBackwards);
+ *aFindBackwards = mFindBackwards;
+ return NS_OK;
+}
+NS_IMETHODIMP nsFindService::SetFindBackwards(bool aFindBackwards)
+{
+ mFindBackwards = aFindBackwards;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFindService::GetWrapFind(bool *aWrapFind)
+{
+ NS_ENSURE_ARG_POINTER(aWrapFind);
+ *aWrapFind = mWrapFind;
+ return NS_OK;
+}
+NS_IMETHODIMP nsFindService::SetWrapFind(bool aWrapFind)
+{
+ mWrapFind = aWrapFind;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFindService::GetEntireWord(bool *aEntireWord)
+{
+ NS_ENSURE_ARG_POINTER(aEntireWord);
+ *aEntireWord = mEntireWord;
+ return NS_OK;
+}
+NS_IMETHODIMP nsFindService::SetEntireWord(bool aEntireWord)
+{
+ mEntireWord = aEntireWord;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFindService::GetMatchCase(bool *aMatchCase)
+{
+ NS_ENSURE_ARG_POINTER(aMatchCase);
+ *aMatchCase = mMatchCase;
+ return NS_OK;
+}
+NS_IMETHODIMP nsFindService::SetMatchCase(bool aMatchCase)
+{
+ mMatchCase = aMatchCase;
+ return NS_OK;
+}
+
diff --git a/toolkit/components/find/nsFindService.h b/toolkit/components/find/nsFindService.h
new file mode 100644
index 0000000000..6b391d5bf4
--- /dev/null
+++ b/toolkit/components/find/nsFindService.h
@@ -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/. */
+
+/*
+ * The sole purpose of the Find service is to store globally the
+ * last used Find settings
+ *
+ */
+
+#include "nsString.h"
+
+#include "nsIFindService.h"
+
+
+// {5060b803-340e-11d5-be5b-b3e063ec6a3c}
+#define NS_FIND_SERVICE_CID \
+ {0x5060b803, 0x340e, 0x11d5, {0xbe, 0x5b, 0xb3, 0xe0, 0x63, 0xec, 0x6a, 0x3c}}
+
+
+#define NS_FIND_SERVICE_CONTRACTID \
+ "@mozilla.org/find/find_service;1"
+
+
+class nsFindService : public nsIFindService
+{
+public:
+
+ nsFindService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFINDSERVICE
+
+protected:
+
+ virtual ~nsFindService();
+
+ nsString mSearchString;
+ nsString mReplaceString;
+
+ bool mFindBackwards;
+ bool mWrapFind;
+ bool mEntireWord;
+ bool mMatchCase;
+};
diff --git a/toolkit/components/find/nsIFindService.idl b/toolkit/components/find/nsIFindService.idl
new file mode 100644
index 0000000000..c5fb96f763
--- /dev/null
+++ b/toolkit/components/find/nsIFindService.idl
@@ -0,0 +1,26 @@
+/* -*- 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"
+
+[scriptable, uuid(5060b801-340e-11d5-be5b-b3e063ec6a3c)]
+interface nsIFindService : nsISupports
+{
+
+ /*
+ * The sole purpose of the Find service is to store globally the
+ * last used Find settings
+ *
+ */
+
+ attribute AString searchString;
+ attribute AString replaceString;
+
+ attribute boolean findBackwards;
+ attribute boolean wrapFind;
+ attribute boolean entireWord;
+ attribute boolean matchCase;
+
+};
diff --git a/toolkit/components/formautofill/FormAutofill.jsm b/toolkit/components/formautofill/FormAutofill.jsm
new file mode 100644
index 0000000000..aae3a956ca
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofill.jsm
@@ -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/. */
+
+/*
+ * Main module handling references to objects living in the main process.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "FormAutofill",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+/**
+ * Main module handling references to objects living in the main process.
+ */
+this.FormAutofill = {
+ /**
+ * Registers new overrides for the FormAutofillIntegration methods. Example:
+ *
+ * FormAutofill.registerIntegration(base => ({
+ * createRequestAutocompleteUI: Task.async(function* () {
+ * yield base.createRequestAutocompleteUI.apply(this, arguments);
+ * }),
+ * }));
+ *
+ * @param aIntegrationFn
+ * Function returning an object defining the methods that should be
+ * overridden. Its only parameter is an object that contains the base
+ * implementation of all the available methods.
+ *
+ * @note The integration function is called every time the list of registered
+ * integration functions changes. Thus, it should not have any side
+ * effects or do any other initialization.
+ */
+ registerIntegration(aIntegrationFn) {
+ Integration.formAutofill.register(aIntegrationFn);
+ },
+
+ /**
+ * Removes a previously registered FormAutofillIntegration override.
+ *
+ * Overrides don't usually need to be unregistered, unless they are added by a
+ * restartless add-on, in which case they should be unregistered when the
+ * add-on is disabled or uninstalled.
+ *
+ * @param aIntegrationFn
+ * This must be the same function object passed to registerIntegration.
+ */
+ unregisterIntegration(aIntegrationFn) {
+ Integration.formAutofill.unregister(aIntegrationFn);
+ },
+
+ /**
+ * Processes a requestAutocomplete message asynchronously.
+ *
+ * @param aData
+ * Provided to FormAutofillIntegration.createRequestAutocompleteUI.
+ *
+ * @return {Promise}
+ * @resolves Structured data received from the requestAutocomplete UI.
+ */
+ processRequestAutocomplete: Task.async(function* (aData) {
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI(aData);
+ return yield ui.show();
+ }),
+};
+
+/**
+ * Dynamically generated object implementing the FormAutofillIntegration
+ * methods. Platform-specific code and add-ons can override methods of this
+ * object using the registerIntegration method.
+ */
+Integration.formAutofill.defineModuleGetter(
+ this.FormAutofill,
+ "integration",
+ "resource://gre/modules/FormAutofillIntegration.jsm",
+ "FormAutofillIntegration"
+);
diff --git a/toolkit/components/formautofill/FormAutofillContentService.js b/toolkit/components/formautofill/FormAutofillContentService.js
new file mode 100644
index 0000000000..ee8e978ad9
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillContentService.js
@@ -0,0 +1,272 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Implements a service used by DOM content to request Form Autofill, in
+ * particular when the requestAutocomplete method of Form objects is invoked.
+ *
+ * See the nsIFormAutofillContentService documentation for details.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * Handles requestAutocomplete for a DOM Form element.
+ */
+function FormHandler(aForm, aWindow) {
+ this.form = aForm;
+ this.window = aWindow;
+
+ this.fieldDetails = [];
+}
+
+FormHandler.prototype = {
+ /**
+ * DOM Form element to which this object is attached.
+ */
+ form: null,
+
+ /**
+ * nsIDOMWindow to which this object is attached.
+ */
+ window: null,
+
+ /**
+ * Array of collected data about relevant form fields. Each item is an object
+ * storing the identifying details of the field and a reference to the
+ * originally associated element from the form.
+ *
+ * The "section", "addressType", "contactType", and "fieldName" values are
+ * used to identify the exact field when the serializable data is received
+ * from the requestAutocomplete user interface. There cannot be multiple
+ * fields which have the same exact combination of these values.
+ *
+ * A direct reference to the associated element cannot be sent to the user
+ * interface because processing may be done in the parent process.
+ */
+ fieldDetails: null,
+
+ /**
+ * Handles requestAutocomplete and generates the DOM events when finished.
+ */
+ handleRequestAutocomplete: Task.async(function* () {
+ // Start processing the request asynchronously. At the end, the "reason"
+ // variable will contain the outcome of the operation, where an empty
+ // string indicates that an unexpected exception occurred.
+ let reason = "";
+ try {
+ reason = yield this.promiseRequestAutocomplete();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // The type of event depends on whether this is a success condition.
+ let event = (reason == "success")
+ ? new this.window.Event("autocomplete", { bubbles: true })
+ : new this.window.AutocompleteErrorEvent("autocompleteerror",
+ { bubbles: true,
+ reason: reason });
+ yield this.waitForTick();
+ this.form.dispatchEvent(event);
+ }),
+
+ /**
+ * Handles requestAutocomplete and returns the outcome when finished.
+ *
+ * @return {Promise}
+ * @resolves The "reason" value indicating the outcome of the
+ * requestAutocomplete operation, including "success" if the
+ * operation completed successfully.
+ */
+ promiseRequestAutocomplete: Task.async(function* () {
+ let data = this.collectFormFields();
+ if (!data) {
+ return "disabled";
+ }
+
+ // Access the frame message manager of the window starting the request.
+ let rootDocShell = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell);
+ let frameMM = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ // We need to set up a temporary message listener for our result before we
+ // send the request to the parent process. At present, there is no check
+ // for reentrancy (bug 1020459), thus it is possible that we'll receive a
+ // message for a different request, but this will not be normally allowed.
+ let promiseRequestAutocompleteResult = new Promise((resolve, reject) => {
+ frameMM.addMessageListener("FormAutofill:RequestAutocompleteResult",
+ function onResult(aMessage) {
+ frameMM.removeMessageListener(
+ "FormAutofill:RequestAutocompleteResult", onResult);
+ // Exceptions in the parent process are serialized and propagated in
+ // the response message that we received.
+ if ("exception" in aMessage.data) {
+ reject(aMessage.data.exception);
+ } else {
+ resolve(aMessage.data);
+ }
+ });
+ });
+
+ // Send the message to the parent process, and wait for the result. This
+ // will throw an exception if one occurred in the parent process.
+ frameMM.sendAsyncMessage("FormAutofill:RequestAutocomplete", data);
+ let result = yield promiseRequestAutocompleteResult;
+ if (result.canceled) {
+ return "cancel";
+ }
+
+ this.autofillFormFields(result);
+
+ return "success";
+ }),
+
+ /**
+ * Returns information from the form about fields that can be autofilled, and
+ * populates the fieldDetails array on this object accordingly.
+ *
+ * @returns Serializable data structure that can be sent to the user
+ * interface, or null if the operation failed because the constraints
+ * on the allowed fields were not honored.
+ */
+ collectFormFields: function () {
+ let autofillData = {
+ sections: [],
+ };
+
+ for (let element of this.form.elements) {
+ // Query the interface and exclude elements that cannot be autocompleted.
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
+ continue;
+ }
+
+ // Exclude elements to which no autocomplete field has been assigned.
+ let info = element.getAutocompleteInfo();
+ if (!info.fieldName || ["on", "off"].indexOf(info.fieldName) != -1) {
+ continue;
+ }
+
+ // Store the association between the field metadata and the element.
+ if (this.fieldDetails.some(f => f.section == info.section &&
+ f.addressType == info.addressType &&
+ f.contactType == info.contactType &&
+ f.fieldName == info.fieldName)) {
+ // A field with the same identifier already exists.
+ return null;
+ }
+ this.fieldDetails.push({
+ section: info.section,
+ addressType: info.addressType,
+ contactType: info.contactType,
+ fieldName: info.fieldName,
+ element: element,
+ });
+
+ // The first level is the custom section.
+ let section = autofillData.sections
+ .find(s => s.name == info.section);
+ if (!section) {
+ section = {
+ name: info.section,
+ addressSections: [],
+ };
+ autofillData.sections.push(section);
+ }
+
+ // The second level is the address section.
+ let addressSection = section.addressSections
+ .find(s => s.addressType == info.addressType);
+ if (!addressSection) {
+ addressSection = {
+ addressType: info.addressType,
+ fields: [],
+ };
+ section.addressSections.push(addressSection);
+ }
+
+ // The third level contains all the fields within the section.
+ let field = {
+ fieldName: info.fieldName,
+ contactType: info.contactType,
+ };
+ addressSection.fields.push(field);
+ }
+
+ return autofillData;
+ },
+
+ /**
+ * Processes form fields that can be autofilled, and populates them with the
+ * data provided by RequestAutocompleteUI.
+ *
+ * @param aAutofillResult
+ * Data returned by the user interface.
+ * {
+ * fields: [
+ * section: Value originally provided to the user interface.
+ * addressType: Value originally provided to the user interface.
+ * contactType: Value originally provided to the user interface.
+ * fieldName: Value originally provided to the user interface.
+ * value: String with which the field should be updated.
+ * ],
+ * }
+ */
+ autofillFormFields: function (aAutofillResult) {
+ for (let field of aAutofillResult.fields) {
+ // Get the field details, if it was processed by the user interface.
+ let fieldDetail = this.fieldDetails
+ .find(f => f.section == field.section &&
+ f.addressType == field.addressType &&
+ f.contactType == field.contactType &&
+ f.fieldName == field.fieldName);
+ if (!fieldDetail) {
+ continue;
+ }
+
+ fieldDetail.element.value = field.value;
+ }
+ },
+
+ /**
+ * Waits for one tick of the event loop before resolving the returned promise.
+ */
+ waitForTick: function () {
+ return new Promise(function (resolve) {
+ Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ },
+};
+
+/**
+ * Implements a service used by DOM content to request Form Autofill, in
+ * particular when the requestAutocomplete method of Form objects is invoked.
+ */
+function FormAutofillContentService() {
+}
+
+FormAutofillContentService.prototype = {
+ classID: Components.ID("{ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutofillContentService]),
+
+ // nsIFormAutofillContentService
+ requestAutocomplete: function (aForm, aWindow) {
+ new FormHandler(aForm, aWindow).handleRequestAutocomplete()
+ .catch(Cu.reportError);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillContentService]);
diff --git a/toolkit/components/formautofill/FormAutofillIntegration.jsm b/toolkit/components/formautofill/FormAutofillIntegration.jsm
new file mode 100644
index 0000000000..4b838a6abf
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillIntegration.jsm
@@ -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/. */
+
+/*
+ * This module defines the default implementation of platform-specific functions
+ * that can be overridden by the host application and by add-ons.
+ *
+ * This module should not be imported directly, but the "integration" getter of
+ * the FormAutofill module should be used to get a reference to the currently
+ * defined implementations of the methods.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "FormAutofillIntegration",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RequestAutocompleteUI",
+ "resource://gre/modules/RequestAutocompleteUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * This module defines the default implementation of platform-specific functions
+ * that can be overridden by the host application and by add-ons.
+ */
+this.FormAutofillIntegration = {
+ /**
+ * Creates a new RequestAutocompleteUI object.
+ *
+ * @param aAutofillData
+ * Provides the initial data required to display the user interface.
+ * {
+ * sections: [{
+ * name: User-specified section name, or empty string.
+ * addressSections: [{
+ * addressType: "shipping", "billing", or empty string.
+ * fields: [{
+ * fieldName: Type of information requested, like "email".
+ * contactType: For example "work", "home", or empty string.
+ * }],
+ * }],
+ * }],
+ * }
+ *
+ * @return {Promise}
+ * @resolves The newly created RequestAutocompleteUI object.
+ * @rejects JavaScript exception.
+ */
+ createRequestAutocompleteUI: Task.async(function* (aAutofillData) {
+ return new RequestAutocompleteUI(aAutofillData);
+ }),
+};
diff --git a/toolkit/components/formautofill/FormAutofillStartup.js b/toolkit/components/formautofill/FormAutofillStartup.js
new file mode 100644
index 0000000000..92887f8722
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillStartup.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/. */
+
+/*
+ * Handles startup in the parent process.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+
+/**
+ * Handles startup in the parent process.
+ */
+function FormAutofillStartup() {
+}
+
+FormAutofillStartup.prototype = {
+ classID: Components.ID("{51c95b3d-7431-467b-8d50-383f158ce9e5}"),
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIFrameMessageListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ ]),
+
+ // nsIObserver
+ observe: function (aSubject, aTopic, aData) {
+ // This method is called by the "profile-after-change" category on startup,
+ // which is called before any web page loads. At this time, we need to
+ // register a global message listener in the parent process preemptively,
+ // because we can receive requests from child processes at any time. For
+ // performance reasons, we use this object as a message listener, so that we
+ // don't have to load the FormAutoFill module at startup.
+ let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ globalMM.addMessageListener("FormAutofill:RequestAutocomplete", this);
+ },
+
+ // nsIFrameMessageListener
+ receiveMessage: function (aMessage) {
+ // Process the "FormAutofill:RequestAutocomplete" message. Any exception
+ // raised in the parent process is caught and serialized into the reply
+ // message that is sent to the requesting child process.
+ FormAutofill.processRequestAutocomplete(aMessage.data)
+ .catch(ex => { return { exception: ex } })
+ .then(result => {
+ // The browser message manager in the parent will send the reply to the
+ // associated frame message manager in the child.
+ let browserMM = aMessage.target.messageManager;
+ browserMM.sendAsyncMessage("FormAutofill:RequestAutocompleteResult",
+ result);
+ })
+ .catch(Cu.reportError);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutofillStartup]);
diff --git a/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
new file mode 100644
index 0000000000..74c4834ba6
--- /dev/null
+++ b/toolkit/components/formautofill/content/RequestAutocompleteUI.jsm
@@ -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/. */
+
+/*
+ * Handles the requestAutocomplete user interface.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "RequestAutocompleteUI",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * Handles the requestAutocomplete user interface.
+ */
+this.RequestAutocompleteUI = function (aAutofillData) {
+ this._autofillData = aAutofillData;
+}
+
+this.RequestAutocompleteUI.prototype = {
+ _autofillData: null,
+
+ show: Task.async(function* () {
+ // Create a new promise and store the function that will resolve it. This
+ // will be called by the UI once the selection has been made.
+ let resolveFn;
+ let uiPromise = new Promise(resolve => resolveFn = resolve);
+
+ // Wrap the callback function so that it survives XPCOM.
+ let args = {
+ resolveFn: resolveFn,
+ autofillData: this._autofillData,
+ };
+ args.wrappedJSObject = args;
+
+ // Open the window providing the function to call when it closes.
+ Services.ww.openWindow(null,
+ "chrome://formautofill/content/requestAutocomplete.xhtml",
+ "Toolkit:RequestAutocomplete",
+ "chrome,dialog=no,resizable",
+ args);
+
+ // Wait for the window to be closed and the operation confirmed.
+ return yield uiPromise;
+ }),
+};
diff --git a/toolkit/components/formautofill/content/requestAutocomplete.js b/toolkit/components/formautofill/content/requestAutocomplete.js
new file mode 100644
index 0000000000..47d5009646
--- /dev/null
+++ b/toolkit/components/formautofill/content/requestAutocomplete.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/. */
+
+/*
+ * Implementation of "requestAutocomplete.xhtml".
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const RequestAutocompleteDialog = {
+ resolveFn: null,
+ autofillData: null,
+
+ onLoad: function () {
+ Task.spawn(function* () {
+ let args = window.arguments[0].wrappedJSObject;
+ this.resolveFn = args.resolveFn;
+ this.autofillData = args.autofillData;
+
+ window.sizeToContent();
+
+ Services.obs.notifyObservers(window,
+ "formautofill-window-initialized", "");
+ }.bind(this)).catch(Cu.reportError);
+ },
+
+ onAccept: function () {
+ // TODO: Replace with autofill storage module (bug 1018304).
+ const dummyDB = {
+ "": {
+ "name": "Mozzy La",
+ "street-address": "331 E Evelyn Ave",
+ "address-level2": "Mountain View",
+ "address-level1": "CA",
+ "country": "US",
+ "postal-code": "94041",
+ "email": "email@example.org",
+ }
+ };
+
+ let result = { fields: [] };
+ for (let section of this.autofillData.sections) {
+ for (let addressSection of section.addressSections) {
+ let addressType = addressSection.addressType;
+ if (!(addressType in dummyDB)) {
+ continue;
+ }
+
+ for (let field of addressSection.fields) {
+ let fieldName = field.fieldName;
+ if (!(fieldName in dummyDB[addressType])) {
+ continue;
+ }
+
+ result.fields.push({
+ section: section.name,
+ addressType: addressType,
+ contactType: field.contactType,
+ fieldName: field.fieldName,
+ value: dummyDB[addressType][fieldName],
+ });
+ }
+ }
+ }
+
+ window.close();
+ this.resolveFn(result);
+ },
+
+ onCancel: function () {
+ window.close();
+ this.resolveFn({ canceled: true });
+ },
+};
diff --git a/toolkit/components/formautofill/content/requestAutocomplete.xhtml b/toolkit/components/formautofill/content/requestAutocomplete.xhtml
new file mode 100644
index 0000000000..269e55bd68
--- /dev/null
+++ b/toolkit/components/formautofill/content/requestAutocomplete.xhtml
@@ -0,0 +1,31 @@
+<?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 % requestAutocompleteDTD SYSTEM "chrome://formautofill/locale/requestAutocomplete.dtd">
+ %requestAutocompleteDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+ %globalDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>requestAutocomplete demo window</title>
+ <link rel="stylesheet"
+ href="chrome://mozapps/skin/formautofill/requestAutocomplete.css" />
+ <script type="application/javascript;version=1.7"
+ src="chrome://formautofill/content/requestAutocomplete.js" />
+ </head>
+ <body dir="&locale.dir;" onload="RequestAutocompleteDialog.onLoad();">
+ <h1>requestAutocomplete</h1>
+ <p>This is a demo window.</p>
+ <input id="accept" type="button" value="(OK)"
+ onclick="RequestAutocompleteDialog.onAccept();" />
+ <input id="cancel" type="button" value="(Cancel)"
+ onclick="RequestAutocompleteDialog.onCancel();" />
+ </body>
+</html>
diff --git a/toolkit/components/formautofill/formautofill.manifest b/toolkit/components/formautofill/formautofill.manifest
new file mode 100644
index 0000000000..880972edc2
--- /dev/null
+++ b/toolkit/components/formautofill/formautofill.manifest
@@ -0,0 +1,7 @@
+component {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6} FormAutofillContentService.js
+contract @mozilla.org/formautofill/content-service;1 {ed9c2c3c-3f86-4ae5-8e31-10f71b0f19e6}
+component {51c95b3d-7431-467b-8d50-383f158ce9e5} FormAutofillStartup.js
+contract @mozilla.org/formautofill/startup;1 {51c95b3d-7431-467b-8d50-383f158ce9e5}
+#ifdef NIGHTLY_BUILD
+category profile-after-change FormAutofillStartup @mozilla.org/formautofill/startup;1
+#endif
diff --git a/toolkit/components/formautofill/jar.mn b/toolkit/components/formautofill/jar.mn
new file mode 100644
index 0000000000..ebe869b58b
--- /dev/null
+++ b/toolkit/components/formautofill/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/.
+
+toolkit.jar:
+% content formautofill %content/formautofill/
+ content/formautofill/requestAutocomplete.js (content/requestAutocomplete.js)
+ content/formautofill/requestAutocomplete.xhtml (content/requestAutocomplete.xhtml)
diff --git a/toolkit/components/formautofill/moz.build b/toolkit/components/formautofill/moz.build
new file mode 100644
index 0000000000..2c2179f81e
--- /dev/null
+++ b/toolkit/components/formautofill/moz.build
@@ -0,0 +1,46 @@
+# -*- 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/.
+
+if CONFIG['NIGHTLY_BUILD']:
+ BROWSER_CHROME_MANIFESTS += [
+ 'test/browser/browser.ini',
+ ]
+
+ MOCHITEST_CHROME_MANIFESTS += [
+ 'test/chrome/chrome.ini',
+ ]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'test/xpcshell/xpcshell.ini',
+]
+
+XPIDL_SOURCES += [
+ 'nsIFormAutofillContentService.idl',
+]
+
+XPIDL_MODULE = 'toolkit_formautofill'
+
+EXTRA_COMPONENTS += [
+ 'FormAutofillContentService.js',
+ 'FormAutofillStartup.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'formautofill.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'content/RequestAutocompleteUI.jsm',
+ 'FormAutofill.jsm',
+ 'FormAutofillIntegration.jsm',
+]
+
+JAR_MANIFESTS += [
+ 'jar.mn',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Form Manager')
diff --git a/toolkit/components/formautofill/nsIFormAutofillContentService.idl b/toolkit/components/formautofill/nsIFormAutofillContentService.idl
new file mode 100644
index 0000000000..300645e741
--- /dev/null
+++ b/toolkit/components/formautofill/nsIFormAutofillContentService.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMHTMLFormElement;
+interface nsIDOMWindow;
+
+/**
+ * Defines a service used by DOM content to request Form Autofill, in particular
+ * when the requestAutocomplete method of Form objects is invoked.
+ *
+ * This service lives in the process that hosts the requesting DOM content.
+ * This means that, in a multi-process (e10s) environment, there can be an
+ * instance of the service for each content process, in addition to an instance
+ * for the chrome process.
+ *
+ * @remarks The service implementation uses a child-side message manager to
+ * communicate with a parent-side message manager living in the chrome
+ * process, where most of the processing is located.
+ */
+[scriptable, uuid(1db29340-99df-4845-9102-0c5d281b2fe8)]
+interface nsIFormAutofillContentService : nsISupports
+{
+ /**
+ * Invoked by the requestAutocomplete method of the DOM Form object.
+ *
+ * The application is expected to display a user interface asking for the
+ * details that are relevant to the form being filled in. The application
+ * should use the "autocomplete" attributes on the input elements as hints
+ * about which type of information is being requested.
+ *
+ * The processing will result in either an "autocomplete" simple DOM Event or
+ * an AutocompleteErrorEvent being fired on the form.
+ *
+ * @param aForm
+ * The form on which the requestAutocomplete method was invoked.
+ * @param aWindow
+ * The window where the form is located. This must be specified even
+ * for elements that are not in a document, and is used to generate the
+ * DOM events resulting from the operation.
+ */
+ void requestAutocomplete(in nsIDOMHTMLFormElement aForm,
+ in nsIDOMWindow aWindow);
+};
diff --git a/toolkit/components/formautofill/test/browser/.eslintrc.js b/toolkit/components/formautofill/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/formautofill/test/browser/browser.ini b/toolkit/components/formautofill/test/browser/browser.ini
new file mode 100644
index 0000000000..dff9c3381a
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+# The following files starting with ".." are installed in the current folder.
+support-files =
+ ../head_common.js
+ ../loader_common.js
+ head.js
+ loader.js
+
+[browser_infrastructure.js]
+[browser_ui_requestAutocomplete.js]
diff --git a/toolkit/components/formautofill/test/browser/browser_infrastructure.js b/toolkit/components/formautofill/test/browser/browser_infrastructure.js
new file mode 100644
index 0000000000..af27cfdb53
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/browser_infrastructure.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the local testing infrastructure.
+ */
+
+"use strict";
+
+/**
+ * Tests the truth assertion function.
+ */
+add_task(function* test_assert_truth() {
+ Assert.ok(1 != 2);
+});
+
+/**
+ * Tests the equality assertion function.
+ */
+add_task(function* test_assert_equality() {
+ Assert.equal(1 + 1, 2);
+});
+
+/**
+ * Uses some of the utility functions provided by the framework.
+ */
+add_task(function* test_utility_functions() {
+ // The "print" function is useful to log information that is not known before.
+ let randomString = "R" + Math.floor(Math.random() * 10);
+ Output.print("The random contents will be '" + randomString + "'.");
+
+ // Create the text file with the random contents.
+ let path = yield TestUtils.getTempFile("test-infrastructure.txt");
+ yield OS.File.writeAtomic(path, new TextEncoder().encode(randomString));
+
+ // Test a few utility functions.
+ yield TestUtils.waitForTick();
+ yield TestUtils.waitMs(50);
+
+ let promiseMyNotification = TestUtils.waitForNotification("my-topic");
+ Services.obs.notifyObservers(null, "my-topic", "");
+ yield promiseMyNotification;
+
+ // Check the file size. The file will be deleted automatically later.
+ Assert.equal((yield OS.File.stat(path)).size, randomString.length);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
new file mode 100644
index 0000000000..2a7b58f121
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/browser_ui_requestAutocomplete.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the requestAutocomplete user interface.
+ */
+
+"use strict";
+
+/**
+ * Open the requestAutocomplete UI and test that selecting a profile results in
+ * the correct data being sent back to the opener.
+ */
+add_task(function* test_select_profile() {
+ // Request an e-mail address.
+ let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(
+ TestData.requestEmailOnly);
+
+ // Accept the dialog.
+ let acceptButton = uiWindow.document.getElementById("accept");
+ EventUtils.synthesizeMouseAtCenter(acceptButton, {}, uiWindow);
+
+ let result = yield promiseResult;
+ Assert.equal(result.fields.length, 1);
+ Assert.equal(result.fields[0].section, "");
+ Assert.equal(result.fields[0].addressType, "");
+ Assert.equal(result.fields[0].contactType, "");
+ Assert.equal(result.fields[0].fieldName, "email");
+ Assert.equal(result.fields[0].value, "email@example.org");
+});
+
+/**
+ * Open the requestAutocomplete UI and cancel the dialog.
+ */
+add_task(function* test_cancel() {
+ // Request an e-mail address.
+ let { uiWindow, promiseResult } = yield FormAutofillTest.showUI(
+ TestData.requestEmailOnly);
+
+ // Cancel the dialog.
+ let acceptButton = uiWindow.document.getElementById("cancel");
+ EventUtils.synthesizeMouseAtCenter(acceptButton, {}, uiWindow);
+
+ let result = yield promiseResult;
+ Assert.ok(result.canceled);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/browser/head.js b/toolkit/components/formautofill/test/browser/head.js
new file mode 100644
index 0000000000..882f3fd5ec
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/head.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization specific to Form Autofill mochitest-browser tests.
+ */
+
+"use strict";
+
+// We cannot start initialization from "loader.js" like we do in the xpcshell
+// and mochitest-chrome frameworks, thus we load the script here.
+Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) + "loader.js",
+ this);
+
+// The testing framework is fully initialized at this point, you can add
+// mochitest-browser specific test initialization here. If you need shared
+// functions or initialization that are not specific to mochitest-browser,
+// consider adding them to "head_common.js" in the parent folder instead.
diff --git a/toolkit/components/formautofill/test/browser/loader.js b/toolkit/components/formautofill/test/browser/loader.js
new file mode 100644
index 0000000000..bfd5b9ee09
--- /dev/null
+++ b/toolkit/components/formautofill/test/browser/loader.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the mochitest-browser tests located in this folder.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) +
+ "loader_common.js", this);
+
+// Define output functions so they look the same across all frameworks.
+var Output = {
+ print: info,
+};
+
+// Define assertion functions so they look the same across all frameworks.
+var Assert = {
+ ok: _mochitestAssert.ok,
+ equal: _mochitestAssert.equal,
+};
+
+// Define task registration functions, see description in "loader_common.js".
+var add_task_in_parent_process = add_task;
+var add_task_in_child_process = function () {};
+var add_task_in_both_processes = add_task;
+
+Services.scriptloader.loadSubScript(getRootDirectory(gTestPath) +
+ "head_common.js", this);
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/chrome/.eslintrc.js b/toolkit/components/formautofill/test/chrome/.eslintrc.js
new file mode 100644
index 0000000000..8c0f4f574c
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/formautofill/test/chrome/chrome.ini b/toolkit/components/formautofill/test/chrome/chrome.ini
new file mode 100644
index 0000000000..67b7869afe
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/chrome.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+# The following files starting with ".." are installed in the current folder.
+support-files =
+ ../head_common.js
+ ../loader_common.js
+ head.js
+ test_infrastructure.js
+ test_requestAutocomplete_cancel.js
+ loader_parent.js
+ loader.js
+
+# For each test defined below, the associated JavaScript file must be declared
+# in the list above. This is required because a "support-files" declaration on
+# the individual test would override the global list instead of adding entries.
+
+[test_infrastructure.html]
+[test_requestAutocomplete_cancel.html]
diff --git a/toolkit/components/formautofill/test/chrome/head.js b/toolkit/components/formautofill/test/chrome/head.js
new file mode 100644
index 0000000000..4110d5e7c6
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/head.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization specific to Form Autofill mochitest-chrome tests.
+ *
+ * This file is loaded by "loader.js".
+ */
+
+"use strict";
+
+// The testing framework is fully initialized at this point, you can add
+// mochitest-chrome specific test initialization here. If you need shared
+// functions or initialization that are not specific to mochitest-chrome,
+// consider adding them to "head_common.js" in the parent folder instead.
diff --git a/toolkit/components/formautofill/test/chrome/loader.js b/toolkit/components/formautofill/test/chrome/loader.js
new file mode 100644
index 0000000000..25b0e6ea38
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/loader.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the mochitest-chrome tests located in this folder.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", this);
+
+var sharedUrl = SimpleTest.getTestFileURL("loader_common.js");
+Services.scriptloader.loadSubScript(sharedUrl, this);
+
+var parentScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("loader_parent.js"));
+
+// Replace the extension of the loaded HTML file with ".js"
+var testUrl = location.href.replace(/\.\w+$/, ".js");
+
+// Start loading the test script in the parent process.
+var promiseParentInitFinished = new Promise(function (resolve) {
+ parentScript.addMessageListener("finish_load_in_parent", resolve);
+});
+parentScript.sendAsyncMessage("start_load_in_parent", { testUrl: testUrl });
+
+// Define output functions so they look the same across all frameworks.
+var Output = {
+ print: info,
+};
+
+// Define assertion functions so they look the same across all frameworks.
+var Assert = {
+ ok: _mochitestAssert.ok,
+ equal: _mochitestAssert.equal,
+};
+
+var executeSoon = SimpleTest.executeSoon;
+
+var gTestTasks = [];
+
+// Define task registration functions, see description in "loader_common.js".
+function add_task(taskFn) {
+ gTestTasks.push([taskFn, "content", taskFn.name]);
+}
+function add_task_in_parent_process(taskFn, taskIdOverride) {
+ let taskId = taskIdOverride || getTaskId(Components.stack.caller);
+ gTestTasks.push([taskFn, "parent", taskId]);
+}
+function add_task_in_both_processes(taskFn) {
+ // We need to define a task ID based on our direct caller.
+ add_task_in_parent_process(taskFn, getTaskId(Components.stack.caller));
+ add_task(taskFn);
+}
+var add_task_in_child_process = add_task;
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+
+ Task.spawn(function* () {
+ try {
+ for (let [taskFn, taskType, taskId] of gTestTasks) {
+ if (taskType == "content") {
+ // This is a normal task executed in the current process.
+ info("Running " + taskFn.name);
+ yield Task.spawn(taskFn);
+ } else {
+ // This is a task executed in the parent process.
+ info("Running task in parent process: " + taskFn.name);
+ let promiseFinished = new Promise(function (resolve) {
+ parentScript.addMessageListener("finish_task_" + taskId, resolve);
+ });
+ parentScript.sendAsyncMessage("start_task_" + taskId);
+ yield promiseFinished;
+ info("Finished task in parent process: " + taskFn.name);
+ }
+ }
+ } catch (ex) {
+ ok(false, ex);
+ }
+
+ SimpleTest.finish();
+ });
+});
+
+// Wait for the test script to be loaded in the parent process. This means that
+// test tasks are registered and ready, but have not been executed yet.
+add_task(function* wait_loading_in_parent_process() {
+ yield promiseParentInitFinished;
+});
+
+var headUrl = SimpleTest.getTestFileURL("head_common.js");
+Services.scriptloader.loadSubScript(headUrl, this);
+
+Output.print("Loading test file: " + testUrl);
+Services.scriptloader.loadSubScript(testUrl, this);
+
+// Register the execution of termination tasks after all other tasks.
+add_task(terminationTaskFn);
+add_task_in_parent_process(terminationTaskFn, terminationTaskFn.name);
+
+SimpleTest.waitForExplicitFinish();
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/chrome/loader_parent.js b/toolkit/components/formautofill/test/chrome/loader_parent.js
new file mode 100644
index 0000000000..bf823218e6
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/loader_parent.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the mochitest-chrome tests located in this folder, always
+ * executed in the parent process.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+var sharedUrl = "chrome://mochitests/content/chrome/" +
+ "toolkit/components/formautofill/test/chrome/loader_common.js";
+Services.scriptloader.loadSubScript(sharedUrl, this);
+
+// Define output functions so they look the same across all frameworks. Since
+// we don't have an output function available here, we report as TEST-PASS.
+var Output = {
+ print: message => assert.ok(true, message),
+};
+
+// Define assertion functions so they look the same across all frameworks.
+var Assert = {
+ ok: assert.ok,
+ equal: assert.equal,
+};
+
+// Define task registration functions, see description in "loader_common.js".
+function add_task_in_parent_process(taskFn, taskIdOverride) {
+ let taskId = taskIdOverride || getTaskId(Components.stack.caller);
+ Output.print("Registering in the parent process: " + taskId);
+ addMessageListener("start_task_" + taskId, function () {
+ Task.spawn(function* () {
+ try {
+ Output.print("Running in the parent process " + taskId);
+ yield Task.spawn(taskFn);
+ } catch (ex) {
+ assert.ok(false, ex);
+ }
+
+ sendAsyncMessage("finish_task_" + taskId, {});
+ });
+ });
+}
+var add_task = function () {};
+var add_task_in_child_process = function () {};
+var add_task_in_both_processes = add_task_in_parent_process;
+
+// We need to wait for the child process to send us the path of the test file
+// to load before we can actually start loading it.
+var context = this;
+addMessageListener("start_load_in_parent", function (message) {
+ Output.print("Starting loading infrastructure in parent process.");
+ let headUrl = "chrome://mochitests/content/chrome/" +
+ "toolkit/components/formautofill/test/chrome/head_common.js";
+ Services.scriptloader.loadSubScript(headUrl, context);
+
+ Services.scriptloader.loadSubScript(message.testUrl, context);
+
+ // Register the execution of termination tasks after all other tasks.
+ add_task_in_parent_process(terminationTaskFn, terminationTaskFn.name);
+
+ Output.print("Finished loading infrastructure in parent process.");
+ sendAsyncMessage("finish_load_in_parent", {});
+});
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/chrome/test_infrastructure.html b/toolkit/components/formautofill/test/chrome/test_infrastructure.html
new file mode 100644
index 0000000000..54f417f779
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_infrastructure.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<script type="application/javascript;version=1.7" src="loader.js"></script>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<p id="paragraph">Paragraph contents.</p>
+
+</body></html>
diff --git a/toolkit/components/formautofill/test/chrome/test_infrastructure.js b/toolkit/components/formautofill/test/chrome/test_infrastructure.js
new file mode 100644
index 0000000000..c3b0b43ff4
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_infrastructure.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the local testing infrastructure.
+ */
+
+"use strict";
+
+/**
+ * Tests the truth assertion function.
+ */
+add_task(function* test_assert_truth() {
+ Assert.ok(1 != 2);
+});
+
+/**
+ * Tests the equality assertion function.
+ */
+add_task(function* test_assert_equality() {
+ Assert.equal(1 + 1, 2);
+});
+
+/**
+ * Uses some of the utility functions provided by the framework.
+ */
+add_task(function* test_utility_functions() {
+ // The "print" function is useful to log information that is not known before.
+ let randomString = "R" + Math.floor(Math.random() * 10);
+ Output.print("The random contents will be '" + randomString + "'.");
+
+ // Create the text file with the random contents.
+ let path = yield TestUtils.getTempFile("test-infrastructure.txt");
+ yield OS.File.writeAtomic(path, new TextEncoder().encode(randomString));
+
+ // Test a few utility functions.
+ yield TestUtils.waitForTick();
+ yield TestUtils.waitMs(50);
+
+ let promiseMyNotification = TestUtils.waitForNotification("my-topic");
+ Services.obs.notifyObservers(null, "my-topic", "");
+ yield promiseMyNotification;
+
+ // Check the file size. The file will be deleted automatically later.
+ Assert.equal((yield OS.File.stat(path)).size, randomString.length);
+});
+
+/**
+ * This type of test has access to the content declared above.
+ */
+add_task(function* test_content() {
+ Assert.equal($("paragraph").innerHTML, "Paragraph contents.");
+
+ let promiseMyEvent = TestUtils.waitForEvent($("paragraph"), "MyEvent");
+
+ let event = document.createEvent("CustomEvent");
+ event.initCustomEvent("MyEvent", true, false, {});
+ $("paragraph").dispatchEvent(event);
+
+ yield promiseMyEvent;
+});
diff --git a/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html
new file mode 100644
index 0000000000..8ae7ffd4bd
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<script type="application/javascript;version=1.7" src="loader.js"></script>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<form id="form">
+</form>
+
+</body></html>
diff --git a/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js
new file mode 100644
index 0000000000..1ee12bd9aa
--- /dev/null
+++ b/toolkit/components/formautofill/test/chrome/test_requestAutocomplete_cancel.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the response sent when requestAutocomplete is canceled by the user.
+ */
+
+"use strict";
+
+/**
+ * The requestAutocomplete UI will not be displayed during these tests.
+ */
+add_task_in_parent_process(function* test_cancel_init() {
+ FormAutofillTest.requestAutocompleteResponse = { canceled: true };
+});
+
+/**
+ * Tests the case where the feature is canceled.
+ */
+add_task(function* test_cancel() {
+ let promise = TestUtils.waitForEvent($("form"), "autocompleteerror");
+ $("form").requestAutocomplete();
+ let errorEvent = yield promise;
+
+ Assert.equal(errorEvent.reason, "cancel");
+});
diff --git a/toolkit/components/formautofill/test/head_common.js b/toolkit/components/formautofill/test/head_common.js
new file mode 100644
index 0000000000..82b87e4a6c
--- /dev/null
+++ b/toolkit/components/formautofill/test/head_common.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization of Form Autofill tests shared between all frameworks.
+ *
+ * A copy of this file is installed in each of the framework subfolders, this
+ * means it becomes a sibling of the test files in the final layout. This is
+ * determined by how manifest "support-files" installation works.
+ */
+
+"use strict";
+
+// The requestAutocomplete framework is available at this point, you can add
+// mochitest-chrome specific test initialization here. If you need shared
+// functions or initialization that are not specific to mochitest-chrome,
+// consider adding them to "head_common.js" in the parent folder instead.
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+ "resource://gre/modules/FormAutofill.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+/* --- Global helpers --- */
+
+// Some of these functions are already implemented in other parts of the source
+// tree, see bug 946708 about sharing more code.
+
+var TestUtils = {
+ /**
+ * Waits for at least one tick of the event loop. This means that all pending
+ * events at the time of this call will have been processed. Other events may
+ * be processed before the returned promise is resolved.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+ waitForTick: function () {
+ return new Promise(resolve => executeSoon(resolve));
+ },
+
+ /**
+ * Waits for the specified timeout.
+ *
+ * @param aTimeMs
+ * Minimum time to wait from the moment of this call, in milliseconds.
+ * The actual wait may be longer, due to system timer resolution and
+ * pending events being processed before the promise is resolved.
+ *
+ * @return {Promise}
+ * @resolves When the specified time has passed.
+ * @rejects Never.
+ */
+ waitMs: function (aTimeMs) {
+ return new Promise(resolve => setTimeout(resolve, aTimeMs));
+ },
+
+ /**
+ * 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.
+ */
+ waitForNotification: function (aTopic) {
+ Output.print("Waiting for notification: '" + aTopic + "'.");
+
+ return new Promise(resolve => Services.obs.addObserver(
+ function observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observe, aTopic);
+ resolve([aSubject, aData]);
+ }, aTopic, false));
+ },
+
+ /**
+ * Waits for a DOM event on the specified target.
+ *
+ * @param aTarget
+ * The DOM EventTarget on which addEventListener should be called.
+ * @param aEventName
+ * String with the name of the event.
+ * @param aUseCapture
+ * This parameter is passed to the addEventListener call.
+ *
+ * @return {Promise}
+ * @resolves The arguments from the observed event.
+ * @rejects Never.
+ */
+ waitForEvent: function (aTarget, aEventName, aUseCapture = false) {
+ Output.print("Waiting for event: '" + aEventName + "' on " + aTarget + ".");
+
+ return new Promise(resolve => aTarget.addEventListener(aEventName,
+ function onEvent(...aArgs) {
+ aTarget.removeEventListener(aEventName, onEvent, aUseCapture);
+ resolve(...aArgs);
+ }, aUseCapture));
+ },
+
+ // While the previous test file should have deleted all the temporary files it
+ // used, on Windows these might still be pending deletion on the physical file
+ // system. Thus, start from a new base number every time, to make a collision
+ // with a file that is still pending deletion highly unlikely.
+ _fileCounter: Math.floor(Math.random() * 1000000),
+
+ /**
+ * Returns a reference to a temporary file, that is guaranteed not to exist,
+ * and to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return {Promise}
+ * @resolves Path of a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the
+ * file after calling nsIFile.createUnique, because on Windows the
+ * delete operation in the file system may still be pending, preventing
+ * a new file with the same name to be created.
+ */
+ getTempFile: Task.async(function* (aLeafName) {
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + this._fileCounter + ext;
+ this._fileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, leafName);
+ Assert.ok(!(yield OS.File.exists(path)));
+
+ // Ensure the file is deleted whe the test terminates.
+ add_termination_task(function* () {
+ if (yield OS.File.exists(path)) {
+ yield OS.File.remove(path);
+ }
+ });
+
+ return path;
+ }),
+};
+
+/* --- Local helpers --- */
+
+var FormAutofillTest = {
+ /**
+ * Stores the response that the next call to the mock requestAutocomplete UI
+ * will return to the requester, or null to enable displaying the default UI.
+ */
+ requestAutocompleteResponse: null,
+
+ /**
+ * Displays the requestAutocomplete user interface using the specified data.
+ *
+ * @param aFormAutofillData
+ * Serializable object containing the set of requested fields.
+ *
+ * @return {Promise}
+ * @resolves An object with the following properties:
+ * {
+ * uiWindow: Reference to the initialized window.
+ * promiseResult: Promise resolved by the UI when it closes.
+ * }
+ */
+ showUI: Task.async(function* (aFormAutofillData) {
+ Output.print("Opening UI with data: " + JSON.stringify(aFormAutofillData));
+
+ // Wait for the initialization event before opening the window.
+ let promiseUIWindow =
+ TestUtils.waitForNotification("formautofill-window-initialized");
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI(
+ aFormAutofillData);
+ let promiseResult = ui.show();
+
+ // The window is the subject of the observer notification.
+ return {
+ uiWindow: (yield promiseUIWindow)[0],
+ promiseResult: promiseResult,
+ };
+ }),
+};
+
+var TestData = {
+ /**
+ * Autofill UI request for the e-mail field only.
+ */
+ get requestEmailOnly() {
+ return {
+ sections: [{
+ name: "",
+ addressSections: [{
+ addressType: "",
+ fields: [{
+ fieldName: "email",
+ contactType: "",
+ }],
+ }],
+ }],
+ };
+ },
+};
+
+/* --- Initialization and termination functions common to all tests --- */
+
+add_task_in_parent_process(function* () {
+ // If required, we return a mock response instead of displaying the UI.
+ let mockIntegrationFn = base => ({
+ createRequestAutocompleteUI: Task.async(function* () {
+ // Call the base method to display the UI if override is not requested.
+ if (FormAutofillTest.requestAutocompleteResponse === null) {
+ return yield base.createRequestAutocompleteUI.apply(this, arguments);
+ }
+
+ // Return a mock RequestAutocompleteUI object.
+ return {
+ show: Task.async(function* () {
+ let response = FormAutofillTest.requestAutocompleteResponse;
+ Output.print("Mock UI response: " + JSON.stringify(response));
+ return response;
+ }),
+ };
+ }),
+ });
+
+ FormAutofill.registerIntegration(mockIntegrationFn);
+ add_termination_task(function* () {
+ FormAutofill.unregisterIntegration(mockIntegrationFn);
+ });
+});
+
+add_task_in_both_processes(function* () {
+ // We must manually enable the feature while testing.
+ Services.prefs.setBoolPref("dom.forms.requestAutocomplete", true);
+ add_termination_task(function* () {
+ Services.prefs.clearUserPref("dom.forms.requestAutocomplete");
+ });
+});
diff --git a/toolkit/components/formautofill/test/loader_common.js b/toolkit/components/formautofill/test/loader_common.js
new file mode 100644
index 0000000000..340586b65b
--- /dev/null
+++ b/toolkit/components/formautofill/test/loader_common.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure common to the test frameworks located in subfolders.
+ *
+ * A copy of this file is installed in each of the framework subfolders, this
+ * means it becomes a sibling of the test files in the final layout. This is
+ * determined by how manifest "support-files" installation works.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or the "head.js" file of each
+ * framework for shared code.
+ */
+
+"use strict";
+
+/*
+ * --------------------
+ * FRAMEWORK OVERVIEW
+ * --------------------
+ *
+ * This framework is designed in such a way that test can be written in similar
+ * ways in the xpcshell, mochitest-chrome, and mochitest-browser frameworks,
+ * both when tests are running in the parent process or in a content process.
+ *
+ * There are some basic self-documenting assertion and output functions:
+ *
+ * Assert.ok(actualValue);
+ * Assert.is(actualValue, expectedValue);
+ * Output.print(string);
+ *
+ * Test cases and initialization functions are declared in shared head files
+ * ("head_common.js" and "head.js") as well as individual test files. When
+ * tests run in an Elecrolysis (e10s) environment, they are executed in both
+ * processes at first. Normally, at this point only the registration of test
+ * cases happen. When everything has finished loading, tests are started and
+ * appropriately synchronized between processes.
+ *
+ * Tests can be declared using the add_task syntax:
+ *
+ * add_task(function* test_something () { ... });
+ * This adds a test either in the parent process or child process:
+ * - Parent: xpcshell, mochitest-chrome --disable-e10s, mochitest-browser
+ * - Child: mochitest-chrome with e10s
+ * In the future, these might run in the child process for "xpcshell".
+ *
+ * add_task_in_parent_process(function* test_something () { ... });
+ * This test runs in the parent process, but the child process will wait for
+ * its completion before continuing with the next task. This wait currently
+ * happens only in mochitest-chrome with e10s, in other frameworks that run
+ * only in the parent process this is the same as a normal add_task.
+ *
+ * add_task_in_child_process(function* test_something () { ... });
+ * This test runs only in the child process. This means that the test is not
+ * run unless this is an e10s test, currently mochitest-chrome with e10s.
+ *
+ * add_task_in_both_processes(function* test_something () { ... });
+ * Useful for initialization that must be done both in the parent and the
+ * child, like setting preferences.
+ *
+ * add_termination_task(function* () { ... });
+ * Registers a new asynchronous termination task. This is executed after all
+ * test cases in the file finished, and always in the same process where the
+ * termination task is registered.
+ */
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+var gTerminationTasks = [];
+
+/**
+ * None of the testing frameworks support asynchronous termination functions, so
+ * this task must be registered later, after the other "add_task" calls.
+ *
+ * Even xpcshell doesn't support calling "add_task" in the "tail.js" file,
+ * because it registers the task but does not wait for its termination,
+ * potentially leading to intermittent failures in subsequent tests.
+ */
+function* terminationTaskFn() {
+ for (let taskFn of gTerminationTasks) {
+ try {
+ yield Task.spawn(taskFn);
+ } catch (ex) {
+ Output.print(ex);
+ Assert.ok(false);
+ }
+ }
+}
+
+function add_termination_task(taskFn) {
+ gTerminationTasks.push(taskFn);
+}
+
+/**
+ * Returns a unique identifier used for synchronizing the given test task
+ * between the parent and child processes.
+ */
+function getTaskId(stackFrame) {
+ return stackFrame.filename + ":" + stackFrame.lineNumber;
+}
+
+// This is a shared helper for mochitest-chrome and mochitest-browser.
+var _mochitestAssert = {
+ ok: function (actual) {
+ let stack = Components.stack.caller;
+ ok(actual, "[" + stack.name + " : " + stack.lineNumber + "] " + actual +
+ " == true");
+ },
+ equal: function (actual, expected) {
+ let stack = Components.stack.caller;
+ is(actual, expected, "[" + stack.name + " : " + stack.lineNumber + "] " +
+ actual + " == " + expected);
+ },
+};
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/xpcshell/.eslintrc.js b/toolkit/components/formautofill/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/formautofill/test/xpcshell/head.js b/toolkit/components/formautofill/test/xpcshell/head.js
new file mode 100644
index 0000000000..1cee023f21
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/head.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Initialization specific to Form Autofill xpcshell tests.
+ *
+ * This file is loaded by "loader.js".
+ */
+
+"use strict";
+
+// The testing framework is fully initialized at this point, you can add
+// xpcshell specific test initialization here. If you need shared functions or
+// initialization that are not specific to xpcshell, consider adding them to
+// "head_common.js" in the parent folder instead.
+
+add_task_in_parent_process(function* test_xpcshell_initialize_profile() {
+ // We need to send the profile-after-change notification manually to the
+ // startup component to ensure it has been initialized.
+ Cc["@mozilla.org/formautofill/startup;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "profile-after-change", "");
+});
diff --git a/toolkit/components/formautofill/test/xpcshell/loader.js b/toolkit/components/formautofill/test/xpcshell/loader.js
new file mode 100644
index 0000000000..449989c8ab
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/loader.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Infrastructure for the xpcshell tests located in this folder.
+ *
+ * See "loader_common.js" in the parent folder for a general overview.
+ *
+ * Unless you are adding new features to the framework, you shouldn't have to
+ * modify this file. Use "head_common.js" or "head.js" for shared code.
+ */
+
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("loader_common.js")).spec, this);
+
+// Define output functions so they look the same across all frameworks.
+var Output = {
+ print: do_print,
+};
+
+var executeSoon = do_execute_soon;
+var setTimeout = (fn, delay) => do_timeout(delay, fn);
+
+// Define task registration functions, see description in "loader_common.js".
+var add_task_in_parent_process = add_task;
+var add_task_in_child_process = function () {};
+var add_task_in_both_processes = add_task;
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("head_common.js")).spec, this);
+
+// Tests are always run asynchronously and with the profile loaded.
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+// Reminder: unless you are adding new features to the framework, you shouldn't
+// have to modify this file. Use "head_common.js" or "head.js" for shared code.
diff --git a/toolkit/components/formautofill/test/xpcshell/test_infrastructure.js b/toolkit/components/formautofill/test/xpcshell/test_infrastructure.js
new file mode 100644
index 0000000000..af27cfdb53
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/test_infrastructure.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the local testing infrastructure.
+ */
+
+"use strict";
+
+/**
+ * Tests the truth assertion function.
+ */
+add_task(function* test_assert_truth() {
+ Assert.ok(1 != 2);
+});
+
+/**
+ * Tests the equality assertion function.
+ */
+add_task(function* test_assert_equality() {
+ Assert.equal(1 + 1, 2);
+});
+
+/**
+ * Uses some of the utility functions provided by the framework.
+ */
+add_task(function* test_utility_functions() {
+ // The "print" function is useful to log information that is not known before.
+ let randomString = "R" + Math.floor(Math.random() * 10);
+ Output.print("The random contents will be '" + randomString + "'.");
+
+ // Create the text file with the random contents.
+ let path = yield TestUtils.getTempFile("test-infrastructure.txt");
+ yield OS.File.writeAtomic(path, new TextEncoder().encode(randomString));
+
+ // Test a few utility functions.
+ yield TestUtils.waitForTick();
+ yield TestUtils.waitMs(50);
+
+ let promiseMyNotification = TestUtils.waitForNotification("my-topic");
+ Services.obs.notifyObservers(null, "my-topic", "");
+ yield promiseMyNotification;
+
+ // Check the file size. The file will be deleted automatically later.
+ Assert.equal((yield OS.File.stat(path)).size, randomString.length);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/xpcshell/test_integration.js b/toolkit/components/formautofill/test/xpcshell/test_integration.js
new file mode 100644
index 0000000000..7707f3880f
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/test_integration.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests overriding the FormAutofillIntegration module functions.
+ */
+
+"use strict";
+
+/**
+ * The requestAutocomplete UI will not be displayed during these tests.
+ */
+add_task_in_parent_process(function* test_initialize() {
+ FormAutofillTest.requestAutocompleteResponse = { canceled: true };
+});
+
+/**
+ * Registers and unregisters an integration override function.
+ */
+add_task(function* test_integration_override() {
+ let overrideCalled = false;
+
+ let newIntegrationFn = base => ({
+ createRequestAutocompleteUI: Task.async(function* () {
+ overrideCalled = true;
+ return yield base.createRequestAutocompleteUI.apply(this, arguments);
+ }),
+ });
+
+ FormAutofill.registerIntegration(newIntegrationFn);
+ try {
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI({});
+ let result = yield ui.show();
+ Assert.ok(result.canceled);
+ } finally {
+ FormAutofill.unregisterIntegration(newIntegrationFn);
+ }
+
+ Assert.ok(overrideCalled);
+});
+
+/**
+ * Registers an integration override function that throws an exception, and
+ * ensures that this does not block other functions from being registered.
+ */
+add_task(function* test_integration_override_error() {
+ let overrideCalled = false;
+
+ let errorIntegrationFn = base => { throw "Expected error." };
+
+ let newIntegrationFn = base => ({
+ createRequestAutocompleteUI: Task.async(function* () {
+ overrideCalled = true;
+ return yield base.createRequestAutocompleteUI.apply(this, arguments);
+ }),
+ });
+
+ FormAutofill.registerIntegration(errorIntegrationFn);
+ FormAutofill.registerIntegration(newIntegrationFn);
+ try {
+ let ui = yield FormAutofill.integration.createRequestAutocompleteUI({});
+ let result = yield ui.show();
+ Assert.ok(result.canceled);
+ } finally {
+ FormAutofill.unregisterIntegration(errorIntegrationFn);
+ FormAutofill.unregisterIntegration(newIntegrationFn);
+ }
+
+ Assert.ok(overrideCalled);
+});
+
+add_task(terminationTaskFn);
diff --git a/toolkit/components/formautofill/test/xpcshell/xpcshell.ini b/toolkit/components/formautofill/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..711c03399f
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/xpcshell.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = loader.js head.js
+tail =
+skip-if = toolkit == 'android'
+# The following files starting with ".." are installed in the current folder.
+# However, they cannot be referenced directly in the "head" directive above.
+support-files =
+ ../head_common.js
+ ../loader_common.js
+
+[test_infrastructure.js]
+[test_integration.js]
diff --git a/toolkit/components/gfx/GfxSanityTest.manifest b/toolkit/components/gfx/GfxSanityTest.manifest
new file mode 100644
index 0000000000..b9febc4987
--- /dev/null
+++ b/toolkit/components/gfx/GfxSanityTest.manifest
@@ -0,0 +1,3 @@
+component {f3a8ca4d-4c83-456b-aee2-6a2cbf11e9bd} SanityTest.js process=main
+contract @mozilla.org/sanity-test;1 {f3a8ca4d-4c83-456b-aee2-6a2cbf11e9bd} process=main
+category profile-after-change SanityTest @mozilla.org/sanity-test;1 process=main
diff --git a/toolkit/components/gfx/SanityTest.js b/toolkit/components/gfx/SanityTest.js
new file mode 100644
index 0000000000..a563ec3619
--- /dev/null
+++ b/toolkit/components/gfx/SanityTest.js
@@ -0,0 +1,315 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const { utils: Cu, interfaces: Ci, classes: Cc, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/Preferences.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const FRAME_SCRIPT_URL = "chrome://gfxsanity/content/gfxFrameScript.js";
+
+const PAGE_WIDTH=92;
+const PAGE_HEIGHT=166;
+const DRIVER_PREF="sanity-test.driver-version";
+const DEVICE_PREF="sanity-test.device-id";
+const VERSION_PREF="sanity-test.version";
+const DISABLE_VIDEO_PREF="media.hardware-video-decoding.failed";
+const RUNNING_PREF="sanity-test.running";
+const TIMEOUT_SEC=20;
+
+// GRAPHICS_SANITY_TEST histogram enumeration values
+const TEST_PASSED=0;
+const TEST_FAILED_RENDER=1;
+const TEST_FAILED_VIDEO=2;
+const TEST_CRASHED=3;
+const TEST_TIMEOUT=4;
+
+// GRAPHICS_SANITY_TEST_REASON enumeration values.
+const REASON_FIRST_RUN=0;
+const REASON_FIREFOX_CHANGED=1;
+const REASON_DEVICE_CHANGED=2;
+const REASON_DRIVER_CHANGED=3;
+
+// GRAPHICS_SANITY_TEST_OS_SNAPSHOT histogram enumeration values
+const SNAPSHOT_VIDEO_OK=0;
+const SNAPSHOT_VIDEO_FAIL=1;
+const SNAPSHOT_ERROR=2;
+const SNAPSHOT_TIMEOUT=3;
+const SNAPSHOT_LAYERS_OK=4;
+const SNAPSHOT_LAYERS_FAIL=5;
+
+function testPixel(ctx, x, y, r, g, b, a, fuzz) {
+ var data = ctx.getImageData(x, y, 1, 1);
+
+ if (Math.abs(data.data[0] - r) <= fuzz &&
+ Math.abs(data.data[1] - g) <= fuzz &&
+ Math.abs(data.data[2] - b) <= fuzz &&
+ Math.abs(data.data[3] - a) <= fuzz) {
+ return true;
+ }
+ return false;
+}
+
+function reportResult(val) {
+ try {
+ let histogram = Services.telemetry.getHistogramById("GRAPHICS_SANITY_TEST");
+ histogram.add(val);
+ } catch (e) {}
+
+ Preferences.set(RUNNING_PREF, false);
+ Services.prefs.savePrefFile(null);
+}
+
+function reportTestReason(val) {
+ let histogram = Services.telemetry.getHistogramById("GRAPHICS_SANITY_TEST_REASON");
+ histogram.add(val);
+}
+
+function annotateCrashReport(value) {
+ try {
+ // "1" if we're annotating the crash report, "" to remove the annotation.
+ var crashReporter = Cc['@mozilla.org/toolkit/crash-reporter;1'].
+ getService(Ci.nsICrashReporter);
+ crashReporter.annotateCrashReport("GraphicsSanityTest", value ? "1" : "");
+ } catch (e) {
+ }
+}
+
+function setTimeout(aMs, aCallback) {
+ var timer = Cc['@mozilla.org/timer;1'].
+ createInstance(Ci.nsITimer);
+ timer.initWithCallback(aCallback, aMs, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+function takeWindowSnapshot(win, ctx) {
+ // TODO: drawWindow reads back from the gpu's backbuffer, which won't catch issues with presenting
+ // the front buffer via the window manager. Ideally we'd use an OS level API for reading back
+ // from the desktop itself to get a more accurate test.
+ var flags = ctx.DRAWWINDOW_DRAW_CARET | ctx.DRAWWINDOW_DRAW_VIEW | ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
+ ctx.drawWindow(win.ownerGlobal, 0, 0, PAGE_WIDTH, PAGE_HEIGHT, "rgb(255,255,255)", flags);
+}
+
+// Verify that all the 4 coloured squares of the video
+// render as expected (with a tolerance of 64 to allow for
+// yuv->rgb differences between platforms).
+//
+// The video is 64x64, and is split into quadrants of
+// different colours. The top left of the video is 8,72
+// and we test a pixel 10,10 into each quadrant to avoid
+// blending differences at the edges.
+//
+// We allow massive amounts of fuzz for the colours since
+// it can depend hugely on the yuv -> rgb conversion, and
+// we don't want to fail unnecessarily.
+function verifyVideoRendering(ctx) {
+ return testPixel(ctx, 18, 82, 255, 255, 255, 255, 64) &&
+ testPixel(ctx, 50, 82, 0, 255, 0, 255, 64) &&
+ testPixel(ctx, 18, 114, 0, 0, 255, 255, 64) &&
+ testPixel(ctx, 50, 114, 255, 0, 0, 255, 64);
+}
+
+// Verify that the middle of the layers test is the color we expect.
+// It's a red 64x64 square, test a pixel deep into the 64x64 square
+// to prevent fuzzing.
+function verifyLayersRendering(ctx) {
+ return testPixel(ctx, 18, 18, 255, 0, 0, 255, 64);
+}
+
+function testCompositor(win, ctx) {
+ takeWindowSnapshot(win, ctx);
+ var testPassed = true;
+
+ if (!verifyVideoRendering(ctx)) {
+ reportResult(TEST_FAILED_VIDEO);
+ Preferences.set(DISABLE_VIDEO_PREF, true);
+ testPassed = false;
+ }
+
+ if (!verifyLayersRendering(ctx)) {
+ reportResult(TEST_FAILED_RENDER);
+ testPassed = false;
+ }
+
+ if (testPassed) {
+ reportResult(TEST_PASSED);
+ }
+
+ return testPassed;
+}
+
+var listener = {
+ win: null,
+ utils: null,
+ canvas: null,
+ ctx: null,
+ mm: null,
+
+ messages: [
+ "gfxSanity:ContentLoaded",
+ ],
+
+ scheduleTest: function(win) {
+ this.win = win;
+ this.win.onload = this.onWindowLoaded.bind(this);
+ this.utils = this.win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ setTimeout(TIMEOUT_SEC * 1000, () => {
+ if (this.win) {
+ reportResult(TEST_TIMEOUT);
+ this.endTest();
+ }
+ });
+ },
+
+ runSanityTest: function() {
+ this.canvas = this.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ this.canvas.setAttribute("width", PAGE_WIDTH);
+ this.canvas.setAttribute("height", PAGE_HEIGHT);
+ this.ctx = this.canvas.getContext("2d");
+
+ // Perform the compositor backbuffer test, which currently we use for
+ // actually deciding whether to enable hardware media decoding.
+ testCompositor(this.win, this.ctx);
+
+ this.endTest();
+ },
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "gfxSanity:ContentLoaded":
+ this.runSanityTest();
+ break;
+ }
+ },
+
+ onWindowLoaded: function() {
+ let browser = this.win.document.createElementNS(XUL_NS, "browser");
+ browser.setAttribute("type", "content");
+
+ let remoteBrowser = Services.appinfo.browserTabsRemoteAutostart;
+ browser.setAttribute("remote", remoteBrowser);
+
+ browser.style.width = PAGE_WIDTH + "px";
+ browser.style.height = PAGE_HEIGHT + "px";
+
+ this.win.document.documentElement.appendChild(browser);
+ // Have to set the mm after we append the child
+ this.mm = browser.messageManager;
+
+ this.messages.forEach((msgName) => {
+ this.mm.addMessageListener(msgName, this);
+ });
+
+ this.mm.loadFrameScript(FRAME_SCRIPT_URL, false);
+ },
+
+ endTest: function() {
+ if (!this.win) {
+ return;
+ }
+
+ this.win.ownerGlobal.close();
+ this.win = null;
+ this.utils = null;
+ this.canvas = null;
+ this.ctx = null;
+
+ if (this.mm) {
+ // We don't have a MessageManager if onWindowLoaded never fired.
+ this.messages.forEach((msgName) => {
+ this.mm.removeMessageListener(msgName, this);
+ });
+
+ this.mm = null;
+ }
+
+ // Remove the annotation after we've cleaned everything up, to catch any
+ // incidental crashes from having performed the sanity test.
+ annotateCrashReport(false);
+ }
+};
+
+function SanityTest() {}
+SanityTest.prototype = {
+ classID: Components.ID("{f3a8ca4d-4c83-456b-aee2-6a2cbf11e9bd}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ shouldRunTest: function() {
+ // Only test gfx features if firefox has updated, or if the user has a new
+ // gpu or drivers.
+ var buildId = Services.appinfo.platformBuildID;
+ var gfxinfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ if (Preferences.get(RUNNING_PREF, false)) {
+ Preferences.set(DISABLE_VIDEO_PREF, true);
+ reportResult(TEST_CRASHED);
+ return false;
+ }
+
+ function checkPref(pref, value, reason) {
+ var prefValue = Preferences.get(pref, undefined);
+ if (prefValue == value) {
+ return true;
+ }
+ if (prefValue === undefined) {
+ reportTestReason(REASON_FIRST_RUN);
+ } else {
+ reportTestReason(reason);
+ }
+ return false;
+ }
+
+ // TODO: Handle dual GPU setups
+ if (checkPref(DRIVER_PREF, gfxinfo.adapterDriverVersion, REASON_DRIVER_CHANGED) &&
+ checkPref(DEVICE_PREF, gfxinfo.adapterDeviceID, REASON_DEVICE_CHANGED) &&
+ checkPref(VERSION_PREF, buildId, REASON_FIREFOX_CHANGED))
+ {
+ return false;
+ }
+
+ // Enable hardware decoding so we can test again
+ // and record the driver version to detect if the driver changes.
+ Preferences.set(DISABLE_VIDEO_PREF, false);
+ Preferences.set(DRIVER_PREF, gfxinfo.adapterDriverVersion);
+ Preferences.set(DEVICE_PREF, gfxinfo.adapterDeviceID);
+ Preferences.set(VERSION_PREF, buildId);
+
+ // Update the prefs so that this test doesn't run again until the next update.
+ Preferences.set(RUNNING_PREF, true);
+ Services.prefs.savePrefFile(null);
+ return true;
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic != "profile-after-change") return;
+
+ // profile-after-change fires only at startup, so we won't need
+ // to use the listener again.
+ let tester = listener;
+ listener = null;
+
+ if (!this.shouldRunTest()) return;
+
+ annotateCrashReport(true);
+
+ // Open a tiny window to render our test page, and notify us when it's loaded
+ var sanityTest = Services.ww.openWindow(null,
+ "chrome://gfxsanity/content/sanityparent.html",
+ "Test Page",
+ "width=" + PAGE_WIDTH + ",height=" + PAGE_HEIGHT + ",chrome,titlebar=0,scrollbars=0",
+ null);
+
+ // There's no clean way to have an invisible window and ensure it's always painted.
+ // Instead, move the window far offscreen so it doesn't show up during launch.
+ sanityTest.moveTo(100000000, 1000000000);
+ tester.scheduleTest(sanityTest);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SanityTest]);
diff --git a/toolkit/components/gfx/content/gfxFrameScript.js b/toolkit/components/gfx/content/gfxFrameScript.js
new file mode 100644
index 0000000000..d7f25d2efd
--- /dev/null
+++ b/toolkit/components/gfx/content/gfxFrameScript.js
@@ -0,0 +1,62 @@
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const gfxFrameScript = {
+ domUtils: null,
+
+ init: function() {
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+
+ this.domUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ webNav.loadURI("chrome://gfxsanity/content/sanitytest.html",
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "MozAfterPaint":
+ sendAsyncMessage('gfxSanity:ContentLoaded');
+ removeEventListener("MozAfterPaint", this);
+ break;
+ }
+ },
+
+ isSanityTest: function(aUri) {
+ if (!aUri) {
+ return false;
+ }
+
+ return aUri.endsWith("/sanitytest.html");
+ },
+
+ onStateChange: function (webProgress, req, flags, status) {
+ if (webProgress.isTopLevel &&
+ (flags & Ci.nsIWebProgressListener.STATE_STOP) &&
+ this.isSanityTest(req.name)) {
+
+ webProgress.removeProgressListener(this);
+
+ // If no paint is pending, then the test already painted
+ if (this.domUtils.isMozAfterPaintPending) {
+ addEventListener("MozAfterPaint", this);
+ } else {
+ sendAsyncMessage('gfxSanity:ContentLoaded');
+ }
+ }
+ },
+
+ // Needed to support web progress listener
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIObserver,
+ ]),
+};
+
+gfxFrameScript.init();
diff --git a/toolkit/components/gfx/content/sanityparent.html b/toolkit/components/gfx/content/sanityparent.html
new file mode 100644
index 0000000000..151214e683
--- /dev/null
+++ b/toolkit/components/gfx/content/sanityparent.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ </head>
+ <body>
+
+ </body>
+</html>
diff --git a/toolkit/components/gfx/content/sanitytest.html b/toolkit/components/gfx/content/sanitytest.html
new file mode 100644
index 0000000000..d60c479231
--- /dev/null
+++ b/toolkit/components/gfx/content/sanitytest.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ <div style="width:64px; height:64px; background-color:red;"></div>
+ <video src="videotest.mp4"></video>
+ </body>
+</html>
diff --git a/toolkit/components/gfx/content/videotest.mp4 b/toolkit/components/gfx/content/videotest.mp4
new file mode 100644
index 0000000000..42cf6e1aa6
--- /dev/null
+++ b/toolkit/components/gfx/content/videotest.mp4
Binary files differ
diff --git a/toolkit/components/gfx/jar.mn b/toolkit/components/gfx/jar.mn
new file mode 100644
index 0000000000..4794e7d3df
--- /dev/null
+++ b/toolkit/components/gfx/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/.
+
+toolkit.jar:
+% content gfxsanity %content/gfxsanity/
+ content/gfxsanity/gfxFrameScript.js (content/gfxFrameScript.js)
+ content/gfxsanity/sanityparent.html (content/sanityparent.html)
+ content/gfxsanity/sanitytest.html (content/sanitytest.html)
+ content/gfxsanity/videotest.mp4 (content/videotest.mp4)
diff --git a/toolkit/components/gfx/moz.build b/toolkit/components/gfx/moz.build
new file mode 100644
index 0000000000..3ecaeb6a32
--- /dev/null
+++ b/toolkit/components/gfx/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/.
+
+toolkit = CONFIG['MOZ_WIDGET_TOOLKIT']
+
+if toolkit == 'windows':
+ EXTRA_COMPONENTS += [
+ 'GfxSanityTest.manifest',
+ 'SanityTest.js',
+ ]
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/jsdownloads/moz.build b/toolkit/components/jsdownloads/moz.build
new file mode 100644
index 0000000000..62f08b160b
--- /dev/null
+++ b/toolkit/components/jsdownloads/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/.
+
+with Files('*'):
+ BUG_COMPONENT = ('Toolkit', 'Download Manager')
+
+DIRS += ['public', 'src']
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+
+TEST_HARNESS_FILES.xpcshell.toolkit.components.jsdownloads.test.data += [
+ 'test/data/empty.txt',
+ 'test/data/source.txt',
+]
diff --git a/toolkit/components/jsdownloads/public/moz.build b/toolkit/components/jsdownloads/public/moz.build
new file mode 100644
index 0000000000..6ea66bf5f2
--- /dev/null
+++ b/toolkit/components/jsdownloads/public/moz.build
@@ -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/.
+
+XPIDL_MODULE = 'jsdownloads'
+
+XPIDL_SOURCES += [
+ 'mozIDownloadPlatform.idl',
+]
diff --git a/toolkit/components/jsdownloads/public/mozIDownloadPlatform.idl b/toolkit/components/jsdownloads/public/mozIDownloadPlatform.idl
new file mode 100644
index 0000000000..d4f49bb4bb
--- /dev/null
+++ b/toolkit/components/jsdownloads/public/mozIDownloadPlatform.idl
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIFile;
+
+[scriptable, uuid(9f556e4a-d9b3-46c3-9f8f-d0db1ac6c8c1)]
+interface mozIDownloadPlatform : nsISupports
+{
+ /**
+ * Perform platform specific operations when a download is done.
+ *
+ * Windows:
+ * Add the download to the recent documents list
+ * Set the file to be indexed for searching
+ * Mac:
+ * Bounce the downloads dock icon
+ * GTK:
+ * Add the download to the recent documents list
+ * Save the source uri in the downloaded file's metadata
+ * Android:
+ * Scan media
+ *
+ * @param aSource
+ * Source URI of the download
+ * @param aReferrer
+ * Referrer URI of the download
+ * @param aTarget
+ * Downloaded file
+ * @param aContentType
+ * The source's content type
+ * @param aIsPrivate
+ * True for private downloads
+ * @return none
+ */
+ void downloadDone(in nsIURI aSource, in nsIURI aReferrer, in nsIFile aTarget,
+ in ACString aContentType, in boolean aIsPrivate);
+
+ /**
+ * Security Zone constants. Used by mapUrlToZone().
+ */
+ const unsigned long ZONE_MY_COMPUTER = 0;
+ const unsigned long ZONE_INTRANET = 1;
+ const unsigned long ZONE_TRUSTED = 2;
+ const unsigned long ZONE_INTERNET = 3;
+ const unsigned long ZONE_RESTRICTED = 4;
+
+ /**
+ * Proxy for IInternetSecurityManager::MapUrlToZone().
+ *
+ * Windows only.
+ *
+ * @param aURL
+ * URI of the download
+ * @return Security Zone corresponding to aURL.
+ */
+ unsigned long mapUrlToZone(in AString aURL);
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm
new file mode 100644
index 0000000000..d89dd58053
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -0,0 +1,2871 @@
+/* -*- 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/. */
+
+/**
+ * This file includes the following constructors and global objects:
+ *
+ * Download
+ * Represents a single download, with associated state and actions. This object
+ * is transient, though it can be included in a DownloadList so that it can be
+ * managed by the user interface and persisted across sessions.
+ *
+ * DownloadSource
+ * Represents the source of a download, for example a document or an URI.
+ *
+ * DownloadTarget
+ * Represents the target of a download, for example a file in the global
+ * downloads directory, or a file in the system temporary directory.
+ *
+ * DownloadError
+ * Provides detailed information about a download failure.
+ *
+ * DownloadSaver
+ * Template for an object that actually transfers the data for the download.
+ *
+ * DownloadCopySaver
+ * Saver object that simply copies the entire source file to the target.
+ *
+ * DownloadLegacySaver
+ * Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * DownloadPDFSaver
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "Download",
+ "DownloadSource",
+ "DownloadTarget",
+ "DownloadError",
+ "DownloadSaver",
+ "DownloadCopySaver",
+ "DownloadLegacySaver",
+ "DownloadPDFSaver",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
+ "@mozilla.org/browser/download-history;1",
+ Ci.nsIDownloadHistory);
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsPIExternalAppLauncher);
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsIExternalHelperAppService);
+XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService",
+ "@mozilla.org/gfx/printsettings-service;1",
+ Ci.nsIPrintSettingsService);
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+const BackgroundFileSaverStreamListener = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
+ "nsIBackgroundFileSaver");
+
+/**
+ * Returns true if the given value is a primitive string or a String object.
+ */
+function isString(aValue) {
+ // We cannot use the "instanceof" operator reliably across module boundaries.
+ return (typeof aValue == "string") ||
+ (typeof aValue == "object" && "charAt" in aValue);
+}
+
+/**
+ * Serialize the unknown properties of aObject into aSerializable.
+ */
+function serializeUnknownProperties(aObject, aSerializable)
+{
+ if (aObject._unknownProperties) {
+ for (let property in aObject._unknownProperties) {
+ aSerializable[property] = aObject._unknownProperties[property];
+ }
+ }
+}
+
+/**
+ * Check for any unknown properties in aSerializable and preserve those in the
+ * _unknownProperties field of aObject. aFilterFn is called for each property
+ * name of aObject and should return true only for unknown properties.
+ */
+function deserializeUnknownProperties(aObject, aSerializable, aFilterFn)
+{
+ for (let property in aSerializable) {
+ if (aFilterFn(property)) {
+ if (!aObject._unknownProperties) {
+ aObject._unknownProperties = { };
+ }
+
+ aObject._unknownProperties[property] = aSerializable[property];
+ }
+ }
+}
+
+/**
+ * This determines the minimum time interval between updates to the number of
+ * bytes transferred, and is a limiting factor to the sequence of readings used
+ * in calculating the speed of the download.
+ */
+const kProgressUpdateIntervalMs = 400;
+
+// Download
+
+/**
+ * Represents a single download, with associated state and actions. This object
+ * is transient, though it can be included in a DownloadList so that it can be
+ * managed by the user interface and persisted across sessions.
+ */
+this.Download = function ()
+{
+ this._deferSucceeded = Promise.defer();
+}
+
+this.Download.prototype = {
+ /**
+ * DownloadSource object associated with this download.
+ */
+ source: null,
+
+ /**
+ * DownloadTarget object associated with this download.
+ */
+ target: null,
+
+ /**
+ * DownloadSaver object associated with this download.
+ */
+ saver: null,
+
+ /**
+ * Indicates that the download never started, has been completed successfully,
+ * failed, or has been canceled. This property becomes false when a download
+ * is started for the first time, or when a failed or canceled download is
+ * restarted.
+ */
+ stopped: true,
+
+ /**
+ * Indicates that the download has been completed successfully.
+ */
+ succeeded: false,
+
+ /**
+ * Indicates that the download has been canceled. This property can become
+ * true, then it can be reset to false when a canceled download is restarted.
+ *
+ * This property becomes true as soon as the "cancel" method is called, though
+ * the "stopped" property might remain false until the cancellation request
+ * has been processed. Temporary files or part files may still exist even if
+ * they are expected to be deleted, until the "stopped" property becomes true.
+ */
+ canceled: false,
+
+ /**
+ * When the download fails, this is set to a DownloadError instance indicating
+ * the cause of the failure. If the download has been completed successfully
+ * or has been canceled, this property is null. This property is reset to
+ * null when a failed download is restarted.
+ */
+ error: null,
+
+ /**
+ * Indicates the start time of the download. When the download starts,
+ * this property is set to a valid Date object. The default value is null
+ * before the download starts.
+ */
+ startTime: null,
+
+ /**
+ * Indicates whether this download's "progress" property is able to report
+ * partial progress while the download proceeds, and whether the value in
+ * totalBytes is relevant. This depends on the saver and the download source.
+ */
+ hasProgress: false,
+
+ /**
+ * Progress percent, from 0 to 100. Intermediate values are reported only if
+ * hasProgress is true.
+ *
+ * @note You shouldn't rely on this property being equal to 100 to determine
+ * whether the download is completed. You should use the individual
+ * state properties instead.
+ */
+ progress: 0,
+
+ /**
+ * When hasProgress is true, indicates the total number of bytes to be
+ * transferred before the download finishes, that can be zero for empty files.
+ *
+ * When hasProgress is false, this property is always zero.
+ *
+ * @note This property may be different than the final file size on disk for
+ * downloads that are encoded during the network transfer. You can use
+ * the "size" property of the DownloadTarget object to get the actual
+ * size on disk once the download succeeds.
+ */
+ totalBytes: 0,
+
+ /**
+ * Number of bytes currently transferred. This value starts at zero, and may
+ * be updated regardless of the value of hasProgress.
+ *
+ * @note You shouldn't rely on this property being equal to totalBytes to
+ * determine whether the download is completed. You should use the
+ * individual state properties instead. This property may not be
+ * updated during the last part of the download.
+ */
+ currentBytes: 0,
+
+ /**
+ * Fractional number representing the speed of the download, in bytes per
+ * second. This value is zero when the download is stopped, and may be
+ * updated regardless of the value of hasProgress.
+ */
+ speed: 0,
+
+ /**
+ * Indicates whether, at this time, there is any partially downloaded data
+ * that can be used when restarting a failed or canceled download.
+ *
+ * Even if the download has partial data on disk, hasPartialData will be false
+ * if that data cannot be used to restart the download. In order to determine
+ * if a part file is being used which contains partial data the
+ * Download.target.partFilePath should be checked.
+ *
+ * This property is relevant while the download is in progress, and also if it
+ * failed or has been canceled. If the download has been completed
+ * successfully, this property is always false.
+ *
+ * Whether partial data can actually be retained depends on the saver and the
+ * download source, and may not be known before the download is started.
+ */
+ hasPartialData: false,
+
+ /**
+ * Indicates whether, at this time, there is any data that has been blocked.
+ * Since reputation blocking takes place after the download has fully
+ * completed a value of true also indicates 100% of the data is present.
+ */
+ hasBlockedData: false,
+
+ /**
+ * This can be set to a function that is called after other properties change.
+ */
+ onchange: null,
+
+ /**
+ * This tells if the user has chosen to open/run the downloaded file after
+ * download has completed.
+ */
+ launchWhenSucceeded: false,
+
+ /**
+ * This represents the MIME type of the download.
+ */
+ contentType: null,
+
+ /**
+ * This indicates the path of the application to be used to launch the file,
+ * or null if the file should be launched with the default application.
+ */
+ launcherPath: null,
+
+ /**
+ * Raises the onchange notification.
+ */
+ _notifyChange: function D_notifyChange() {
+ try {
+ if (this.onchange) {
+ this.onchange();
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ /**
+ * The download may be stopped and restarted multiple times before it
+ * completes successfully. This may happen if any of the download attempts is
+ * canceled or fails.
+ *
+ * This property contains a promise that is linked to the current attempt, or
+ * null if the download is either stopped or in the process of being canceled.
+ * If the download restarts, this property is replaced with a new promise.
+ *
+ * The promise is resolved if the attempt it represents finishes successfully,
+ * and rejected if the attempt fails.
+ */
+ _currentAttempt: null,
+
+ /**
+ * Starts the download for the first time, or restarts a download that failed
+ * or has been canceled.
+ *
+ * Calling this method when the download has been completed successfully has
+ * no effect, and the method returns a resolved promise. If the download is
+ * in progress, the method returns the same promise as the previous call.
+ *
+ * If the "cancel" method was called but the cancellation process has not
+ * finished yet, this method waits for the cancellation to finish, then
+ * restarts the download immediately.
+ *
+ * @note If you need to start a new download from the same source, rather than
+ * restarting a failed or canceled one, you should create a separate
+ * Download object with the same source as the current one.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+ start: function D_start()
+ {
+ // If the download succeeded, it's the final state, we have nothing to do.
+ if (this.succeeded) {
+ return Promise.resolve();
+ }
+
+ // If the download already started and hasn't failed or hasn't been
+ // canceled, return the same promise as the previous call, allowing the
+ // caller to wait for the current attempt to finish.
+ if (this._currentAttempt) {
+ return this._currentAttempt;
+ }
+
+ // While shutting down or disposing of this object, we prevent the download
+ // from returning to be in progress.
+ if (this._finalized) {
+ return Promise.reject(new DownloadError({
+ message: "Cannot start after finalization."}));
+ }
+
+ if (this.error && this.error.becauseBlockedByReputationCheck) {
+ return Promise.reject(new DownloadError({
+ message: "Cannot start after being blocked " +
+ "by a reputation check."}));
+ }
+
+ // Initialize all the status properties for a new or restarted download.
+ this.stopped = false;
+ this.canceled = false;
+ this.error = null;
+ this.hasProgress = false;
+ this.hasBlockedData = false;
+ this.progress = 0;
+ this.totalBytes = 0;
+ this.currentBytes = 0;
+ this.startTime = new Date();
+
+ // Create a new deferred object and an associated promise before starting
+ // the actual download. We store it on the download as the current attempt.
+ let deferAttempt = Promise.defer();
+ let currentAttempt = deferAttempt.promise;
+ this._currentAttempt = currentAttempt;
+
+ // Restart the progress and speed calculations from scratch.
+ this._lastProgressTimeMs = 0;
+
+ // This function propagates progress from the DownloadSaver object, unless
+ // it comes in late from a download attempt that was replaced by a new one.
+ // If the cancellation process for the download has started, then the update
+ // is ignored.
+ function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
+ {
+ if (this._currentAttempt == currentAttempt) {
+ this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
+ }
+ }
+
+ // This function propagates download properties from the DownloadSaver
+ // object, unless it comes in late from a download attempt that was
+ // replaced by a new one. If the cancellation process for the download has
+ // started, then the update is ignored.
+ function DS_setProperties(aOptions)
+ {
+ if (this._currentAttempt != currentAttempt) {
+ return;
+ }
+
+ let changeMade = false;
+
+ for (let property of ["contentType", "progress", "hasPartialData",
+ "hasBlockedData"]) {
+ if (property in aOptions && this[property] != aOptions[property]) {
+ this[property] = aOptions[property];
+ changeMade = true;
+ }
+ }
+
+ if (changeMade) {
+ this._notifyChange();
+ }
+ }
+
+ // Now that we stored the promise in the download object, we can start the
+ // task that will actually execute the download.
+ deferAttempt.resolve(Task.spawn(function* task_D_start() {
+ // Wait upon any pending operation before restarting.
+ if (this._promiseCanceled) {
+ yield this._promiseCanceled;
+ }
+ if (this._promiseRemovePartialData) {
+ try {
+ yield this._promiseRemovePartialData;
+ } catch (ex) {
+ // Ignore any errors, which are already reported by the original
+ // caller of the removePartialData method.
+ }
+ }
+
+ // In case the download was restarted while cancellation was in progress,
+ // but the previous attempt actually succeeded before cancellation could
+ // be processed, it is possible that the download has already finished.
+ if (this.succeeded) {
+ return;
+ }
+
+ try {
+ // Disallow download if parental controls service restricts it.
+ if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
+ throw new DownloadError({ becauseBlockedByParentalControls: true });
+ }
+
+ // Disallow download if needed runtime permissions have not been granted
+ // by user.
+ if (yield DownloadIntegration.shouldBlockForRuntimePermissions()) {
+ throw new DownloadError({ becauseBlockedByRuntimePermissions: true });
+ }
+
+ // We should check if we have been canceled in the meantime, after all
+ // the previous asynchronous operations have been executed and just
+ // before we call the "execute" method of the saver.
+ if (this._promiseCanceled) {
+ // The exception will become a cancellation in the "catch" block.
+ throw undefined;
+ }
+
+ // Execute the actual download through the saver object.
+ this._saverExecuting = true;
+ yield this.saver.execute(DS_setProgressBytes.bind(this),
+ DS_setProperties.bind(this));
+
+ // Now that the actual saving finished, read the actual file size on
+ // disk, that may be different from the amount of data transferred.
+ yield this.target.refresh();
+
+ // Check for the last time if the download has been canceled. This must
+ // be done right before setting the "stopped" property of the download,
+ // without any asynchronous operations in the middle, so that another
+ // cancellation request cannot start in the meantime and stay unhandled.
+ if (this._promiseCanceled) {
+ try {
+ yield OS.File.remove(this.target.path);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ this.target.exists = false;
+ this.target.size = 0;
+
+ // Cancellation exceptions will be changed in the catch block below.
+ throw new DownloadError();
+ }
+
+ // Update the status properties for a successful download.
+ this.progress = 100;
+ this.succeeded = true;
+ this.hasPartialData = false;
+ } catch (originalEx) {
+ // We may choose a different exception to propagate in the code below,
+ // or wrap the original one. We do this mutation in a different variable
+ // because of the "no-ex-assign" ESLint rule.
+ let ex = originalEx;
+
+ // Fail with a generic status code on cancellation, so that the caller
+ // is forced to actually check the status properties to see if the
+ // download was canceled or failed because of other reasons.
+ if (this._promiseCanceled) {
+ throw new DownloadError({ message: "Download canceled." });
+ }
+
+ // An HTTP 450 error code is used by Windows to indicate that a uri is
+ // blocked by parental controls. This will prevent the download from
+ // occuring, so an error needs to be raised. This is not performed
+ // during the parental controls check above as it requires the request
+ // to start.
+ if (this._blockedByParentalControls) {
+ ex = new DownloadError({ becauseBlockedByParentalControls: true });
+ }
+
+ // Update the download error, unless a new attempt already started. The
+ // change in the status property is notified in the finally block.
+ if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
+ if (!(ex instanceof DownloadError)) {
+ let properties = {innerException: ex};
+
+ if (ex.message) {
+ properties.message = ex.message;
+ }
+
+ ex = new DownloadError(properties);
+ }
+
+ this.error = ex;
+ }
+ throw ex;
+ } finally {
+ // Any cancellation request has now been processed.
+ this._saverExecuting = false;
+ this._promiseCanceled = null;
+
+ // Update the status properties, unless a new attempt already started.
+ if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
+ this._currentAttempt = null;
+ this.stopped = true;
+ this.speed = 0;
+ this._notifyChange();
+ if (this.succeeded) {
+ yield this._succeed();
+ }
+ }
+ }
+ }.bind(this)));
+
+ // Notify the new download state before returning.
+ this._notifyChange();
+ return currentAttempt;
+ },
+
+ /**
+ * Perform the actions necessary when a Download succeeds.
+ *
+ * @return {Promise}
+ * @resolves When the steps to take after success have completed.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ _succeed: Task.async(function* () {
+ yield DownloadIntegration.downloadDone(this);
+
+ this._deferSucceeded.resolve();
+
+ if (this.launchWhenSucceeded) {
+ this.launch().then(null, Cu.reportError);
+
+ // Always schedule files to be deleted at the end of the private browsing
+ // mode, regardless of the value of the pref.
+ if (this.source.isPrivate) {
+ gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
+ new FileUtils.File(this.target.path));
+ } else if (Services.prefs.getBoolPref(
+ "browser.helperApps.deleteTempFileOnExit")) {
+ gExternalAppLauncher.deleteTemporaryFileOnExit(
+ new FileUtils.File(this.target.path));
+ }
+ }
+ }),
+
+ /**
+ * When a request to unblock the download is received, contains a promise
+ * that will be resolved when the unblock request is completed. This property
+ * will then continue to hold the promise indefinitely.
+ */
+ _promiseUnblock: null,
+
+ /**
+ * When a request to confirm the block of the download is received, contains
+ * a promise that will be resolved when cleaning up the download has
+ * completed. This property will then continue to hold the promise
+ * indefinitely.
+ */
+ _promiseConfirmBlock: null,
+
+ /**
+ * Unblocks a download which had been blocked by reputation.
+ *
+ * The file will be moved out of quarantine and the download will be
+ * marked as succeeded.
+ *
+ * @return {Promise}
+ * @resolves When the Download has been unblocked and succeeded.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ unblock: function() {
+ if (this._promiseUnblock) {
+ return this._promiseUnblock;
+ }
+
+ if (this._promiseConfirmBlock) {
+ return Promise.reject(new Error(
+ "Download block has been confirmed, cannot unblock."));
+ }
+
+ if (!this.hasBlockedData) {
+ return Promise.reject(new Error(
+ "unblock may only be called on Downloads with blocked data."));
+ }
+
+ this._promiseUnblock = Task.spawn(function* () {
+ try {
+ yield OS.File.move(this.target.partFilePath, this.target.path);
+ yield this.target.refresh();
+ } catch (ex) {
+ yield this.refresh();
+ this._promiseUnblock = null;
+ throw ex;
+ }
+
+ this.succeeded = true;
+ this.hasBlockedData = false;
+ this._notifyChange();
+ yield this._succeed();
+ }.bind(this));
+
+ return this._promiseUnblock;
+ },
+
+ /**
+ * Confirms that a blocked download should be cleaned up.
+ *
+ * If a download was blocked but retained on disk this method can be used
+ * to remove the file.
+ *
+ * @return {Promise}
+ * @resolves When the Download's data has been removed.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ confirmBlock: function() {
+ if (this._promiseConfirmBlock) {
+ return this._promiseConfirmBlock;
+ }
+
+ if (this._promiseUnblock) {
+ return Promise.reject(new Error(
+ "Download is being unblocked, cannot confirmBlock."));
+ }
+
+ if (!this.hasBlockedData) {
+ return Promise.reject(new Error(
+ "confirmBlock may only be called on Downloads with blocked data."));
+ }
+
+ this._promiseConfirmBlock = Task.spawn(function* () {
+ try {
+ yield OS.File.remove(this.target.partFilePath);
+ } catch (ex) {
+ yield this.refresh();
+ this._promiseConfirmBlock = null;
+ throw ex;
+ }
+
+ this.hasBlockedData = false;
+ this._notifyChange();
+ }.bind(this));
+
+ return this._promiseConfirmBlock;
+ },
+
+ /*
+ * Launches the file after download has completed. This can open
+ * the file with the default application for the target MIME type
+ * or file extension, or with a custom application if launcherPath
+ * is set.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to launch the file has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the file is actually
+ * launched.
+ * @rejects JavaScript exception if there was an error trying to launch
+ * the file.
+ */
+ launch: function () {
+ if (!this.succeeded) {
+ return Promise.reject(
+ new Error("launch can only be called if the download succeeded")
+ );
+ }
+
+ return DownloadIntegration.launchDownload(this);
+ },
+
+ /*
+ * Shows the folder containing the target file, or where the target file
+ * will be saved. This may be called at any time, even if the download
+ * failed or is currently in progress.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to open the containing folder has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the folder is actually
+ * opened.
+ * @rejects JavaScript exception if there was an error trying to open
+ * the containing folder.
+ */
+ showContainingDirectory: function D_showContainingDirectory() {
+ return DownloadIntegration.showContainingDirectory(this.target.path);
+ },
+
+ /**
+ * When a request to cancel the download is received, contains a promise that
+ * will be resolved when the cancellation request is processed. When the
+ * request is processed, this property becomes null again.
+ */
+ _promiseCanceled: null,
+
+ /**
+ * True between the call to the "execute" method of the saver and the
+ * completion of the current download attempt.
+ */
+ _saverExecuting: false,
+
+ /**
+ * Cancels the download.
+ *
+ * The cancellation request is asynchronous. Until the cancellation process
+ * finishes, temporary files or part files may still exist even if they are
+ * expected to be deleted.
+ *
+ * In case the download completes successfully before the cancellation request
+ * could be processed, this method has no effect, and it returns a resolved
+ * promise. You should check the properties of the download at the time the
+ * returned promise is resolved to determine if the download was cancelled.
+ *
+ * Calling this method when the download has been completed successfully,
+ * failed, or has been canceled has no effect, and the method returns a
+ * resolved promise. This behavior is designed for the case where the call
+ * to "cancel" happens asynchronously, and is consistent with the case where
+ * the cancellation request could not be processed in time.
+ *
+ * @return {Promise}
+ * @resolves When the cancellation process has finished.
+ * @rejects Never.
+ */
+ cancel: function D_cancel()
+ {
+ // If the download is currently stopped, we have nothing to do.
+ if (this.stopped) {
+ return Promise.resolve();
+ }
+
+ if (!this._promiseCanceled) {
+ // Start a new cancellation request.
+ let deferCanceled = Promise.defer();
+ this._currentAttempt.then(() => deferCanceled.resolve(),
+ () => deferCanceled.resolve());
+ this._promiseCanceled = deferCanceled.promise;
+
+ // The download can already be restarted.
+ this._currentAttempt = null;
+
+ // Notify that the cancellation request was received.
+ this.canceled = true;
+ this._notifyChange();
+
+ // Execute the actual cancellation through the saver object, in case it
+ // has already started. Otherwise, the cancellation will be handled just
+ // before the saver is started.
+ if (this._saverExecuting) {
+ this.saver.cancel();
+ }
+ }
+
+ return this._promiseCanceled;
+ },
+
+ /**
+ * Indicates whether any partially downloaded data should be retained, to use
+ * when restarting a failed or canceled download. The default is false.
+ *
+ * Whether partial data can actually be retained depends on the saver and the
+ * download source, and may not be known before the download is started.
+ *
+ * To have any effect, this property must be set before starting the download.
+ * Resetting this property to false after the download has already started
+ * will not remove any partial data.
+ *
+ * If this property is set to true, care should be taken that partial data is
+ * removed before the reference to the download is discarded. This can be
+ * done using the removePartialData or the "finalize" methods.
+ */
+ tryToKeepPartialData: false,
+
+ /**
+ * When a request to remove partially downloaded data is received, contains a
+ * promise that will be resolved when the removal request is processed. When
+ * the request is processed, this property becomes null again.
+ */
+ _promiseRemovePartialData: null,
+
+ /**
+ * Removes any partial data kept as part of a canceled or failed download.
+ *
+ * If the download is not canceled or failed, this method has no effect, and
+ * it returns a resolved promise. If the "cancel" method was called but the
+ * cancellation process has not finished yet, this method waits for the
+ * cancellation to finish, then removes the partial data.
+ *
+ * After this method has been called, if the tryToKeepPartialData property is
+ * still true when the download is restarted, partial data will be retained
+ * during the new download attempt.
+ *
+ * @return {Promise}
+ * @resolves When the partial data has been successfully removed.
+ * @rejects JavaScript exception if the operation could not be completed.
+ */
+ removePartialData: function ()
+ {
+ if (!this.canceled && !this.error) {
+ return Promise.resolve();
+ }
+
+ let promiseRemovePartialData = this._promiseRemovePartialData;
+
+ if (!promiseRemovePartialData) {
+ let deferRemovePartialData = Promise.defer();
+ promiseRemovePartialData = deferRemovePartialData.promise;
+ this._promiseRemovePartialData = promiseRemovePartialData;
+
+ deferRemovePartialData.resolve(
+ Task.spawn(function* task_D_removePartialData() {
+ try {
+ // Wait upon any pending cancellation request.
+ if (this._promiseCanceled) {
+ yield this._promiseCanceled;
+ }
+ // Ask the saver object to remove any partial data.
+ yield this.saver.removePartialData();
+ // For completeness, clear the number of bytes transferred.
+ if (this.currentBytes != 0 || this.hasPartialData) {
+ this.currentBytes = 0;
+ this.hasPartialData = false;
+ this._notifyChange();
+ }
+ } finally {
+ this._promiseRemovePartialData = null;
+ }
+ }.bind(this)));
+ }
+
+ return promiseRemovePartialData;
+ },
+
+ /**
+ * This deferred object contains a promise that is resolved as soon as this
+ * download finishes successfully, and is never rejected. This property is
+ * initialized when the download is created, and never changes.
+ */
+ _deferSucceeded: null,
+
+ /**
+ * Returns a promise that is resolved as soon as this download finishes
+ * successfully, even if the download was stopped and restarted meanwhile.
+ *
+ * You can use this property for scheduling download completion actions in the
+ * current session, for downloads that are controlled interactively. If the
+ * download is not controlled interactively, you should use the promise
+ * returned by the "start" method instead, to check for success or failure.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects Never.
+ */
+ whenSucceeded: function D_whenSucceeded()
+ {
+ return this._deferSucceeded.promise;
+ },
+
+ /**
+ * Updates the state of a finished, failed, or canceled download based on the
+ * current state in the file system. If the download is in progress or it has
+ * been finalized, this method has no effect, and it returns a resolved
+ * promise.
+ *
+ * This allows the properties of the download to be updated in case the user
+ * moved or deleted the target file or its associated ".part" file.
+ *
+ * @return {Promise}
+ * @resolves When the operation has completed.
+ * @rejects Never.
+ */
+ refresh: function ()
+ {
+ return Task.spawn(function* () {
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ if (this.succeeded) {
+ let oldExists = this.target.exists;
+ let oldSize = this.target.size;
+ yield this.target.refresh();
+ if (oldExists != this.target.exists || oldSize != this.target.size) {
+ this._notifyChange();
+ }
+ return;
+ }
+
+ // Update the current progress from disk if we retained partial data.
+ if ((this.hasPartialData || this.hasBlockedData) &&
+ this.target.partFilePath) {
+
+ try {
+ let stat = yield OS.File.stat(this.target.partFilePath);
+
+ // Ignore the result if the state has changed meanwhile.
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ // Update the bytes transferred and the related progress properties.
+ this.currentBytes = stat.size;
+ if (this.totalBytes > 0) {
+ this.hasProgress = true;
+ this.progress = Math.floor(this.currentBytes /
+ this.totalBytes * 100);
+ }
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ // Ignore the result if the state has changed meanwhile.
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ this.hasBlockedData = false;
+ this.hasPartialData = false;
+ }
+
+ this._notifyChange();
+ }
+ }.bind(this)).then(null, Cu.reportError);
+ },
+
+ /**
+ * True if the "finalize" method has been called. This prevents the download
+ * from starting again after having been stopped.
+ */
+ _finalized: false,
+
+ /**
+ * Ensures that the download is stopped, and optionally removes any partial
+ * data kept as part of a canceled or failed download. After this method has
+ * been called, the download cannot be started again.
+ *
+ * This method should be used in place of "cancel" and removePartialData while
+ * shutting down or disposing of the download object, to prevent other callers
+ * from interfering with the operation. This is required because cancellation
+ * and other operations are asynchronous.
+ *
+ * @param aRemovePartialData
+ * Whether any partially downloaded data should be removed after the
+ * download has been stopped.
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished successfully.
+ * @rejects JavaScript exception if an error occurred while removing the
+ * partially downloaded data.
+ */
+ finalize: function (aRemovePartialData)
+ {
+ // Prevents the download from starting again after having been stopped.
+ this._finalized = true;
+
+ if (aRemovePartialData) {
+ // Cancel the download, in case it is currently in progress, then remove
+ // any partially downloaded data. The removal operation waits for
+ // cancellation to be completed before resolving the promise it returns.
+ this.cancel();
+ return this.removePartialData();
+ }
+ // Just cancel the download, in case it is currently in progress.
+ return this.cancel();
+ },
+
+ /**
+ * Indicates the time of the last progress notification, expressed as the
+ * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero
+ * until some bytes have actually been transferred.
+ */
+ _lastProgressTimeMs: 0,
+
+ /**
+ * Updates progress notifications based on the number of bytes transferred.
+ *
+ * The number of bytes transferred is not updated unless enough time passed
+ * since this function was last called. This limits the computation load, in
+ * particular when the listeners update the user interface in response.
+ *
+ * @param aCurrentBytes
+ * Number of bytes transferred until now.
+ * @param aTotalBytes
+ * Total number of bytes to be transferred, or -1 if unknown.
+ * @param aHasPartialData
+ * Indicates whether the partially downloaded data can be used when
+ * restarting the download if it fails or is canceled.
+ */
+ _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
+ let changeMade = (this.hasPartialData != aHasPartialData);
+ this.hasPartialData = aHasPartialData;
+
+ // Unless aTotalBytes is -1, we can report partial download progress. In
+ // this case, notify when the related properties changed since last time.
+ if (aTotalBytes != -1 && (!this.hasProgress ||
+ this.totalBytes != aTotalBytes)) {
+ this.hasProgress = true;
+ this.totalBytes = aTotalBytes;
+ changeMade = true;
+ }
+
+ // Updating the progress and computing the speed require that enough time
+ // passed since the last update, or that we haven't started throttling yet.
+ let currentTimeMs = Date.now();
+ let intervalMs = currentTimeMs - this._lastProgressTimeMs;
+ if (intervalMs >= kProgressUpdateIntervalMs) {
+ // Don't compute the speed unless we started throttling notifications.
+ if (this._lastProgressTimeMs != 0) {
+ // Calculate the speed in bytes per second.
+ let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000;
+ if (this.speed == 0) {
+ // When the previous speed is exactly zero instead of a fractional
+ // number, this can be considered the first element of the series.
+ this.speed = rawSpeed;
+ } else {
+ // Apply exponential smoothing, with a smoothing factor of 0.1.
+ this.speed = rawSpeed * 0.1 + this.speed * 0.9;
+ }
+ }
+
+ // Start throttling notifications only when we have actually received some
+ // bytes for the first time. The timing of the first part of the download
+ // is not reliable, due to possible latency in the initial notifications.
+ // This also allows automated tests to receive and verify the number of
+ // bytes initially transferred.
+ if (aCurrentBytes > 0) {
+ this._lastProgressTimeMs = currentTimeMs;
+
+ // Update the progress now that we don't need its previous value.
+ this.currentBytes = aCurrentBytes;
+ if (this.totalBytes > 0) {
+ this.progress = Math.floor(this.currentBytes / this.totalBytes * 100);
+ }
+ changeMade = true;
+ }
+ }
+
+ if (changeMade) {
+ this._notifyChange();
+ }
+ },
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ let serializable = {
+ source: this.source.toSerializable(),
+ target: this.target.toSerializable(),
+ };
+
+ let saver = this.saver.toSerializable();
+ if (!serializable.source || !saver) {
+ // If we are unable to serialize either the source or the saver,
+ // we won't persist the download.
+ return null;
+ }
+
+ // Simplify the representation for the most common saver type. If the saver
+ // is an object instead of a simple string, we can't simplify it because we
+ // need to persist all its properties, not only "type". This may happen for
+ // savers of type "copy" as well as other types.
+ if (saver !== "copy") {
+ serializable.saver = saver;
+ }
+
+ if (this.error) {
+ serializable.errorObj = this.error.toSerializable();
+ }
+
+ if (this.startTime) {
+ serializable.startTime = this.startTime.toJSON();
+ }
+
+ // These are serialized unless they are false, null, or empty strings.
+ for (let property of kPlainSerializableDownloadProperties) {
+ if (this[property]) {
+ serializable[property] = this[property];
+ }
+ }
+
+ serializeUnknownProperties(this, serializable);
+
+ return serializable;
+ },
+
+ /**
+ * Returns a value that changes only when one of the properties of a Download
+ * object that should be saved into a file also change. This excludes
+ * properties whose value doesn't usually change during the download lifetime.
+ *
+ * This function is used to determine whether the download should be
+ * serialized after a property change notification has been received.
+ *
+ * @return String representing the relevant download state.
+ */
+ getSerializationHash: function ()
+ {
+ // The "succeeded", "canceled", "error", and startTime properties are not
+ // taken into account because they all change before the "stopped" property
+ // changes, and are not altered in other cases.
+ return this.stopped + "," + this.totalBytes + "," + this.hasPartialData +
+ "," + this.contentType;
+ },
+};
+
+/**
+ * Defines which properties of the Download object are serializable.
+ */
+const kPlainSerializableDownloadProperties = [
+ "succeeded",
+ "canceled",
+ "totalBytes",
+ "hasPartialData",
+ "hasBlockedData",
+ "tryToKeepPartialData",
+ "launcherPath",
+ "launchWhenSucceeded",
+ "contentType",
+];
+
+/**
+ * Creates a new Download object from a serializable representation. This
+ * function is used by the createDownload method of Downloads.jsm when a new
+ * Download object is requested, thus some properties may refer to live objects
+ * in place of their serializable representations.
+ *
+ * @param aSerializable
+ * An object with the following fields:
+ * {
+ * source: DownloadSource object, or its serializable representation.
+ * See DownloadSource.fromSerializable for details.
+ * target: DownloadTarget object, or its serializable representation.
+ * See DownloadTarget.fromSerializable for details.
+ * saver: Serializable representation of a DownloadSaver object. See
+ * DownloadSaver.fromSerializable for details. If omitted,
+ * defaults to "copy".
+ * }
+ *
+ * @return The newly created Download object.
+ */
+Download.fromSerializable = function (aSerializable) {
+ let download = new Download();
+ if (aSerializable.source instanceof DownloadSource) {
+ download.source = aSerializable.source;
+ } else {
+ download.source = DownloadSource.fromSerializable(aSerializable.source);
+ }
+ if (aSerializable.target instanceof DownloadTarget) {
+ download.target = aSerializable.target;
+ } else {
+ download.target = DownloadTarget.fromSerializable(aSerializable.target);
+ }
+ if ("saver" in aSerializable) {
+ download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
+ } else {
+ download.saver = DownloadSaver.fromSerializable("copy");
+ }
+ download.saver.download = download;
+
+ if ("startTime" in aSerializable) {
+ let time = aSerializable.startTime.getTime
+ ? aSerializable.startTime.getTime()
+ : aSerializable.startTime;
+ download.startTime = new Date(time);
+ }
+
+ // If 'errorObj' is present it will take precedence over the 'error' property.
+ // 'error' is a legacy property only containing message, which is insufficient
+ // to represent all of the error information.
+ //
+ // Instead of just replacing 'error' we use a new 'errorObj' so that previous
+ // versions will keep it as an unknown property.
+ if ("errorObj" in aSerializable) {
+ download.error = DownloadError.fromSerializable(aSerializable.errorObj);
+ } else if ("error" in aSerializable) {
+ download.error = aSerializable.error;
+ }
+
+ for (let property of kPlainSerializableDownloadProperties) {
+ if (property in aSerializable) {
+ download[property] = aSerializable[property];
+ }
+ }
+
+ deserializeUnknownProperties(download, aSerializable, property =>
+ kPlainSerializableDownloadProperties.indexOf(property) == -1 &&
+ property != "startTime" &&
+ property != "source" &&
+ property != "target" &&
+ property != "error" &&
+ property != "saver");
+
+ return download;
+};
+
+// DownloadSource
+
+/**
+ * Represents the source of a download, for example a document or an URI.
+ */
+this.DownloadSource = function () {}
+
+this.DownloadSource.prototype = {
+ /**
+ * String containing the URI for the download source.
+ */
+ url: null,
+
+ /**
+ * Indicates whether the download originated from a private window. This
+ * determines the context of the network request that is made to retrieve the
+ * resource.
+ */
+ isPrivate: false,
+
+ /**
+ * String containing the referrer URI of the download source, or null if no
+ * referrer should be sent or the download source is not HTTP.
+ */
+ referrer: null,
+
+ /**
+ * For downloads handled by the (default) DownloadCopySaver, this function
+ * can adjust the network channel before it is opened, for example to change
+ * the HTTP headers or to upload a stream as POST data.
+ *
+ * @note If this is defined this object will not be serializable, thus the
+ * Download object will not be persisted across sessions.
+ *
+ * @param aChannel
+ * The nsIChannel to be adjusted.
+ *
+ * @return {Promise}
+ * @resolves When the channel has been adjusted and can be opened.
+ * @rejects JavaScript exception that will cause the download to fail.
+ */
+ adjustChannel: null,
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ if (this.adjustChannel) {
+ // If the callback was used, we can't reproduce this across sessions.
+ return null;
+ }
+
+ // Simplify the representation if we don't have other details.
+ if (!this.isPrivate && !this.referrer && !this._unknownProperties) {
+ return this.url;
+ }
+
+ let serializable = { url: this.url };
+ if (this.isPrivate) {
+ serializable.isPrivate = true;
+ }
+ if (this.referrer) {
+ serializable.referrer = this.referrer;
+ }
+
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+};
+
+/**
+ * Creates a new DownloadSource object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadSource object. This may be a
+ * string containing the URI for the download source, an nsIURI, or an
+ * object with the following properties:
+ * {
+ * url: String containing the URI for the download source.
+ * isPrivate: Indicates whether the download originated from a private
+ * window. If omitted, the download is public.
+ * referrer: String containing the referrer URI of the download source.
+ * Can be omitted or null if no referrer should be sent or
+ * the download source is not HTTP.
+ * adjustChannel: For downloads handled by (default) DownloadCopySaver,
+ * this function can adjust the network channel before
+ * it is opened, for example to change the HTTP headers
+ * or to upload a stream as POST data. Optional.
+ * }
+ *
+ * @return The newly created DownloadSource object.
+ */
+this.DownloadSource.fromSerializable = function (aSerializable) {
+ let source = new DownloadSource();
+ if (isString(aSerializable)) {
+ // Convert String objects to primitive strings at this point.
+ source.url = aSerializable.toString();
+ } else if (aSerializable instanceof Ci.nsIURI) {
+ source.url = aSerializable.spec;
+ } else if (aSerializable instanceof Ci.nsIDOMWindow) {
+ source.url = aSerializable.location.href;
+ source.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(aSerializable);
+ source.windowRef = Cu.getWeakReference(aSerializable);
+ } else {
+ // Convert String objects to primitive strings at this point.
+ source.url = aSerializable.url.toString();
+ if ("isPrivate" in aSerializable) {
+ source.isPrivate = aSerializable.isPrivate;
+ }
+ if ("referrer" in aSerializable) {
+ source.referrer = aSerializable.referrer;
+ }
+ if ("adjustChannel" in aSerializable) {
+ source.adjustChannel = aSerializable.adjustChannel;
+ }
+
+ deserializeUnknownProperties(source, aSerializable, property =>
+ property != "url" && property != "isPrivate" && property != "referrer");
+ }
+
+ return source;
+};
+
+// DownloadTarget
+
+/**
+ * Represents the target of a download, for example a file in the global
+ * downloads directory, or a file in the system temporary directory.
+ */
+this.DownloadTarget = function () {}
+
+this.DownloadTarget.prototype = {
+ /**
+ * String containing the path of the target file.
+ */
+ path: null,
+
+ /**
+ * String containing the path of the ".part" file containing the data
+ * downloaded so far, or null to disable the use of a ".part" file to keep
+ * partially downloaded data.
+ */
+ partFilePath: null,
+
+ /**
+ * Indicates whether the target file exists.
+ *
+ * This is a dynamic property updated when the download finishes or when the
+ * "refresh" method of the Download object is called. It can be used by the
+ * front-end to reduce I/O compared to checking the target file directly.
+ */
+ exists: false,
+
+ /**
+ * Size in bytes of the target file, or zero if the download has not finished.
+ *
+ * Even if the target file does not exist anymore, this property may still
+ * have a value taken from the download metadata. If the metadata has never
+ * been available in this session and the size cannot be obtained from the
+ * file because it has already been deleted, this property will be zero.
+ *
+ * For single-file downloads, this property will always match the actual file
+ * size on disk, while the totalBytes property of the Download object, when
+ * available, may represent the size of the encoded data instead.
+ *
+ * For downloads involving multiple files, like complete web pages saved to
+ * disk, the meaning of this value is undefined. It currently matches the size
+ * of the main file only rather than the sum of all the written data.
+ *
+ * This is a dynamic property updated when the download finishes or when the
+ * "refresh" method of the Download object is called. It can be used by the
+ * front-end to reduce I/O compared to checking the target file directly.
+ */
+ size: 0,
+
+ /**
+ * Sets the "exists" and "size" properties based on the actual file on disk.
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished successfully.
+ * @rejects JavaScript exception.
+ */
+ refresh: Task.async(function* () {
+ try {
+ this.size = (yield OS.File.stat(this.path)).size;
+ this.exists = true;
+ } catch (ex) {
+ // Report any error not caused by the file not being there. In any case,
+ // the size of the download is not updated and the known value is kept.
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ this.exists = false;
+ }
+ }),
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ // Simplify the representation if we don't have other details.
+ if (!this.partFilePath && !this._unknownProperties) {
+ return this.path;
+ }
+
+ let serializable = { path: this.path,
+ partFilePath: this.partFilePath };
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+};
+
+/**
+ * Creates a new DownloadTarget object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadTarget object. This may be a
+ * string containing the path of the target file, an nsIFile, or an
+ * object with the following properties:
+ * {
+ * path: String containing the path of the target file.
+ * partFilePath: optional string containing the part file path.
+ * }
+ *
+ * @return The newly created DownloadTarget object.
+ */
+this.DownloadTarget.fromSerializable = function (aSerializable) {
+ let target = new DownloadTarget();
+ if (isString(aSerializable)) {
+ // Convert String objects to primitive strings at this point.
+ target.path = aSerializable.toString();
+ } else if (aSerializable instanceof Ci.nsIFile) {
+ // Read the "path" property of nsIFile after checking the object type.
+ target.path = aSerializable.path;
+ } else {
+ // Read the "path" property of the serializable DownloadTarget
+ // representation, converting String objects to primitive strings.
+ target.path = aSerializable.path.toString();
+ if ("partFilePath" in aSerializable) {
+ target.partFilePath = aSerializable.partFilePath;
+ }
+
+ deserializeUnknownProperties(target, aSerializable, property =>
+ property != "path" && property != "partFilePath");
+ }
+ return target;
+};
+
+// DownloadError
+
+/**
+ * Provides detailed information about a download failure.
+ *
+ * @param aProperties
+ * Object which may contain any of the following properties:
+ * {
+ * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE
+ * message: String error message to be displayed, or null to use the
+ * message associated with the result code.
+ * inferCause: If true, attempts to determine if the cause of the
+ * download is a network failure or a local file failure,
+ * based on a set of known values of the result code.
+ * This is useful when the error is received by a
+ * component that handles both aspects of the download.
+ * }
+ * The properties object may also contain any of the DownloadError's
+ * because properties, which will be set accordingly in the error object.
+ */
+this.DownloadError = function (aProperties)
+{
+ const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
+ const NS_ERROR_MODULE_NETWORK = 6;
+ const NS_ERROR_MODULE_FILES = 13;
+
+ // Set the error name used by the Error object prototype first.
+ this.name = "DownloadError";
+ this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
+ if (aProperties.message) {
+ this.message = aProperties.message;
+ } else if (aProperties.becauseBlocked ||
+ aProperties.becauseBlockedByParentalControls ||
+ aProperties.becauseBlockedByReputationCheck ||
+ aProperties.becauseBlockedByRuntimePermissions) {
+ this.message = "Download blocked.";
+ } else {
+ let exception = new Components.Exception("", this.result);
+ this.message = exception.toString();
+ }
+ if (aProperties.inferCause) {
+ let module = ((this.result & 0x7FFF0000) >> 16) -
+ NS_ERROR_MODULE_BASE_OFFSET;
+ this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
+ this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
+ }
+ else {
+ if (aProperties.becauseSourceFailed) {
+ this.becauseSourceFailed = true;
+ }
+ if (aProperties.becauseTargetFailed) {
+ this.becauseTargetFailed = true;
+ }
+ }
+
+ if (aProperties.becauseBlockedByParentalControls) {
+ this.becauseBlocked = true;
+ this.becauseBlockedByParentalControls = true;
+ } else if (aProperties.becauseBlockedByReputationCheck) {
+ this.becauseBlocked = true;
+ this.becauseBlockedByReputationCheck = true;
+ this.reputationCheckVerdict = aProperties.reputationCheckVerdict || "";
+ } else if (aProperties.becauseBlockedByRuntimePermissions) {
+ this.becauseBlocked = true;
+ this.becauseBlockedByRuntimePermissions = true;
+ } else if (aProperties.becauseBlocked) {
+ this.becauseBlocked = true;
+ }
+
+ if (aProperties.innerException) {
+ this.innerException = aProperties.innerException;
+ }
+
+ this.stack = new Error().stack;
+}
+
+/**
+ * These constants are used by the reputationCheckVerdict property and indicate
+ * the detailed reason why a download is blocked.
+ *
+ * @note These values should not be changed because they can be serialized.
+ */
+this.DownloadError.BLOCK_VERDICT_MALWARE = "Malware";
+this.DownloadError.BLOCK_VERDICT_POTENTIALLY_UNWANTED = "PotentiallyUnwanted";
+this.DownloadError.BLOCK_VERDICT_UNCOMMON = "Uncommon";
+
+this.DownloadError.prototype = {
+ __proto__: Error.prototype,
+
+ /**
+ * The result code associated with this error.
+ */
+ result: false,
+
+ /**
+ * Indicates an error occurred while reading from the remote location.
+ */
+ becauseSourceFailed: false,
+
+ /**
+ * Indicates an error occurred while writing to the local target.
+ */
+ becauseTargetFailed: false,
+
+ /**
+ * Indicates the download failed because it was blocked. If the reason for
+ * blocking is known, the corresponding property will be also set.
+ */
+ becauseBlocked: false,
+
+ /**
+ * Indicates the download was blocked because downloads are globally
+ * disallowed by the Parental Controls or Family Safety features on Windows.
+ */
+ becauseBlockedByParentalControls: false,
+
+ /**
+ * Indicates the download was blocked because it failed the reputation check
+ * and may be malware.
+ */
+ becauseBlockedByReputationCheck: false,
+
+ /**
+ * Indicates the download was blocked because a runtime permission required to
+ * download files was not granted.
+ *
+ * This does not apply to all systems. On Android this flag is set to true if
+ * a needed runtime permission (storage) has not been granted by the user.
+ */
+ becauseBlockedByRuntimePermissions: false,
+
+ /**
+ * If becauseBlockedByReputationCheck is true, indicates the detailed reason
+ * why the download was blocked, according to the "BLOCK_VERDICT_" constants.
+ *
+ * If the download was not blocked or the reason for the block is unknown,
+ * this will be an empty string.
+ */
+ reputationCheckVerdict: "",
+
+ /**
+ * If this DownloadError was caused by an exception this property will
+ * contain the original exception. This will not be serialized when saving
+ * to the store.
+ */
+ innerException: null,
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ let serializable = {
+ result: this.result,
+ message: this.message,
+ becauseSourceFailed: this.becauseSourceFailed,
+ becauseTargetFailed: this.becauseTargetFailed,
+ becauseBlocked: this.becauseBlocked,
+ becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
+ becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
+ becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions,
+ reputationCheckVerdict: this.reputationCheckVerdict,
+ };
+
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+};
+
+/**
+ * Creates a new DownloadError object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadError object.
+ *
+ * @return The newly created DownloadError object.
+ */
+this.DownloadError.fromSerializable = function (aSerializable) {
+ let e = new DownloadError(aSerializable);
+ deserializeUnknownProperties(e, aSerializable, property =>
+ property != "result" &&
+ property != "message" &&
+ property != "becauseSourceFailed" &&
+ property != "becauseTargetFailed" &&
+ property != "becauseBlocked" &&
+ property != "becauseBlockedByParentalControls" &&
+ property != "becauseBlockedByReputationCheck" &&
+ property != "becauseBlockedByRuntimePermissions" &&
+ property != "reputationCheckVerdict");
+
+ return e;
+};
+
+// DownloadSaver
+
+/**
+ * Template for an object that actually transfers the data for the download.
+ */
+this.DownloadSaver = function () {}
+
+this.DownloadSaver.prototype = {
+ /**
+ * Download object for raising notifications and reading properties.
+ *
+ * If the tryToKeepPartialData property of the download object is false, the
+ * saver should never try to keep partially downloaded data if the download
+ * fails.
+ */
+ download: null,
+
+ /**
+ * Executes the download.
+ *
+ * @param aSetProgressBytesFn
+ * This function may be called by the saver to report progress. It
+ * takes three arguments: the first is the number of bytes transferred
+ * until now, the second is the total number of bytes to be
+ * transferred (or -1 if unknown), the third indicates whether the
+ * partially downloaded data can be used when restarting the download
+ * if it fails or is canceled.
+ * @param aSetPropertiesFn
+ * This function may be called by the saver to report information
+ * about new download properties discovered by the saver during the
+ * download process. It takes an object where the keys represents
+ * the names of the properties to set, and the value represents the
+ * value to set.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+ execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ throw new Error("Not implemented.");
+ },
+
+ /**
+ * Cancels the download.
+ */
+ cancel: function DS_cancel()
+ {
+ throw new Error("Not implemented.");
+ },
+
+ /**
+ * Removes any partial data kept as part of a canceled or failed download.
+ *
+ * This method is never called until the promise returned by "execute" is
+ * either resolved or rejected, and the "execute" method is not called again
+ * until the promise returned by this method is resolved or rejected.
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished successfully.
+ * @rejects JavaScript exception.
+ */
+ removePartialData: function DS_removePartialData()
+ {
+ return Promise.resolve();
+ },
+
+ /**
+ * This can be called by the saver implementation when the download is already
+ * started, to add it to the browsing history. This method has no effect if
+ * the download is private.
+ */
+ addToHistory: function ()
+ {
+ if (this.download.source.isPrivate) {
+ return;
+ }
+
+ let sourceUri = NetUtil.newURI(this.download.source.url);
+ let referrer = this.download.source.referrer;
+ let referrerUri = referrer ? NetUtil.newURI(referrer) : null;
+ let targetUri = NetUtil.newURI(new FileUtils.File(
+ this.download.target.path));
+
+ // The start time is always available when we reach this point.
+ let startPRTime = this.download.startTime.getTime() * 1000;
+
+ try {
+ gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
+ targetUri);
+ }
+ catch (ex) {
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ //
+ // Under normal operation the download history service may not
+ // be available. We don't want all downloads that are public to fail
+ // when this happens so we'll ignore this error and this error only!
+ //
+ }
+ },
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ throw new Error("Not implemented.");
+ },
+
+ /**
+ * Returns the SHA-256 hash of the downloaded file, if it exists.
+ */
+ getSha256Hash: function ()
+ {
+ throw new Error("Not implemented.");
+ },
+
+ getSignatureInfo: function ()
+ {
+ throw new Error("Not implemented.");
+ },
+}; // DownloadSaver
+
+/**
+ * Creates a new DownloadSaver object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadSaver object. If no initial
+ * state information for the saver object is needed, can be a string
+ * representing the class of the download operation, for example "copy".
+ *
+ * @return The newly created DownloadSaver object.
+ */
+this.DownloadSaver.fromSerializable = function (aSerializable) {
+ let serializable = isString(aSerializable) ? { type: aSerializable }
+ : aSerializable;
+ let saver;
+ switch (serializable.type) {
+ case "copy":
+ saver = DownloadCopySaver.fromSerializable(serializable);
+ break;
+ case "legacy":
+ saver = DownloadLegacySaver.fromSerializable(serializable);
+ break;
+ case "pdf":
+ saver = DownloadPDFSaver.fromSerializable(serializable);
+ break;
+ default:
+ throw new Error("Unrecoginzed download saver type.");
+ }
+ return saver;
+};
+
+// DownloadCopySaver
+
+/**
+ * Saver object that simply copies the entire source file to the target.
+ */
+this.DownloadCopySaver = function () {}
+
+this.DownloadCopySaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * BackgroundFileSaver object currently handling the download.
+ */
+ _backgroundFileSaver: null,
+
+ /**
+ * Indicates whether the "cancel" method has been called. This is used to
+ * prevent the request from starting in case the operation is canceled before
+ * the BackgroundFileSaver instance has been created.
+ */
+ _canceled: false,
+
+ /**
+ * Save the SHA-256 hash in raw bytes of the downloaded file. This is null
+ * unless BackgroundFileSaver has successfully completed saving the file.
+ */
+ _sha256Hash: null,
+
+ /**
+ * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
+ * if the file is signed. This is empty if the file is unsigned, and null
+ * unless BackgroundFileSaver has successfully completed saving the file.
+ */
+ _signatureInfo: null,
+
+ /**
+ * Save the redirects chain as an nsIArray of nsIPrincipal.
+ */
+ _redirects: null,
+
+ /**
+ * True if the associated download has already been added to browsing history.
+ */
+ alreadyAddedToHistory: false,
+
+ /**
+ * String corresponding to the entityID property of the nsIResumableChannel
+ * used to execute the download, or null if the channel was not resumable or
+ * the saver was instructed not to keep partially downloaded data.
+ */
+ entityID: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ let copySaver = this;
+
+ this._canceled = false;
+
+ let download = this.download;
+ let targetPath = download.target.path;
+ let partFilePath = download.target.partFilePath;
+ let keepPartialData = download.tryToKeepPartialData;
+
+ return Task.spawn(function* task_DCS_execute() {
+ // Add the download to history the first time it is started in this
+ // session. If the download is restarted in a different session, a new
+ // history visit will be added. We do this just to avoid the complexity
+ // of serializing this state between sessions, since adding a new visit
+ // does not have any noticeable side effect.
+ if (!this.alreadyAddedToHistory) {
+ this.addToHistory();
+ this.alreadyAddedToHistory = true;
+ }
+
+ // To reduce the chance that other downloads reuse the same final target
+ // file name, we should create a placeholder as soon as possible, before
+ // starting the network request. The placeholder is also required in case
+ // we are using a ".part" file instead of the final target while the
+ // download is in progress.
+ try {
+ // If the file already exists, don't delete its contents yet.
+ let file = yield OS.File.open(targetPath, { write: true });
+ yield file.close();
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error)) {
+ throw ex;
+ }
+ // Throw a DownloadError indicating that the operation failed because of
+ // the target file. We cannot translate this into a specific result
+ // code, but we preserve the original message using the toString method.
+ let error = new DownloadError({ message: ex.toString() });
+ error.becauseTargetFailed = true;
+ throw error;
+ }
+
+ try {
+ let deferSaveComplete = Promise.defer();
+
+ if (this._canceled) {
+ // Don't create the BackgroundFileSaver object if we have been
+ // canceled meanwhile.
+ throw new DownloadError({ message: "Saver canceled." });
+ }
+
+ // Create the object that will save the file in a background thread.
+ let backgroundFileSaver = new BackgroundFileSaverStreamListener();
+ try {
+ // When the operation completes, reflect the status in the promise
+ // returned by this download execution function.
+ backgroundFileSaver.observer = {
+ onTargetChange: function () { },
+ onSaveComplete: (aSaver, aStatus) => {
+ // Send notifications now that we can restart if needed.
+ if (Components.isSuccessCode(aStatus)) {
+ // Save the hash before freeing backgroundFileSaver.
+ this._sha256Hash = aSaver.sha256Hash;
+ this._signatureInfo = aSaver.signatureInfo;
+ this._redirects = aSaver.redirects;
+ deferSaveComplete.resolve();
+ } else {
+ // Infer the origin of the error from the failure code, because
+ // BackgroundFileSaver does not provide more specific data.
+ let properties = { result: aStatus, inferCause: true };
+ deferSaveComplete.reject(new DownloadError(properties));
+ }
+ // Free the reference cycle, to release resources earlier.
+ backgroundFileSaver.observer = null;
+ this._backgroundFileSaver = null;
+ },
+ };
+
+ // Create a channel from the source, and listen to progress
+ // notifications.
+ let channel = NetUtil.newChannel({
+ uri: download.source.url,
+ loadUsingSystemPrincipal: true,
+ });
+ if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
+ channel.setPrivate(download.source.isPrivate);
+ }
+ if (channel instanceof Ci.nsIHttpChannel &&
+ download.source.referrer) {
+ channel.referrer = NetUtil.newURI(download.source.referrer);
+ }
+
+ // If we have data that we can use to resume the download from where
+ // it stopped, try to use it.
+ let resumeAttempted = false;
+ let resumeFromBytes = 0;
+ if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
+ partFilePath && keepPartialData) {
+ try {
+ let stat = yield OS.File.stat(partFilePath);
+ channel.resumeAt(stat.size, this.entityID);
+ resumeAttempted = true;
+ resumeFromBytes = stat.size;
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ }
+ }
+
+ channel.notificationCallbacks = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
+ getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
+ onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
+ aProgressMax)
+ {
+ let currentBytes = resumeFromBytes + aProgress;
+ let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes +
+ aProgressMax);
+ aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 &&
+ partFilePath && keepPartialData);
+ },
+ onStatus: function () { },
+ };
+
+ // If the callback was set, handle it now before opening the channel.
+ if (download.source.adjustChannel) {
+ yield download.source.adjustChannel(channel);
+ }
+
+ // Open the channel, directing output to the background file saver.
+ backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
+ channel.asyncOpen2({
+ onStartRequest: function (aRequest, aContext) {
+ backgroundFileSaver.onStartRequest(aRequest, aContext);
+
+ // Check if the request's response has been blocked by Windows
+ // Parental Controls with an HTTP 450 error code.
+ if (aRequest instanceof Ci.nsIHttpChannel &&
+ aRequest.responseStatus == 450) {
+ // Set a flag that can be retrieved later when handling the
+ // cancellation so that the proper error can be thrown.
+ this.download._blockedByParentalControls = true;
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+
+ aSetPropertiesFn({ contentType: channel.contentType });
+
+ // Ensure we report the value of "Content-Length", if available,
+ // even if the download doesn't generate any progress events
+ // later.
+ if (channel.contentLength >= 0) {
+ aSetProgressBytesFn(0, channel.contentLength);
+ }
+
+ // If the URL we are downloading from includes a file extension
+ // that matches the "Content-Encoding" header, for example ".gz"
+ // with a "gzip" encoding, we should save the file in its encoded
+ // form. In all other cases, we decode the body while saving.
+ if (channel instanceof Ci.nsIEncodedChannel &&
+ channel.contentEncodings) {
+ let uri = channel.URI;
+ if (uri instanceof Ci.nsIURL && uri.fileExtension) {
+ // Only the first, outermost encoding is considered.
+ let encoding = channel.contentEncodings.getNext();
+ if (encoding) {
+ channel.applyConversion =
+ gExternalHelperAppService.applyDecodingForExtension(
+ uri.fileExtension, encoding);
+ }
+ }
+ }
+
+ if (keepPartialData) {
+ // If the source is not resumable, don't keep partial data even
+ // if we were asked to try and do it.
+ if (aRequest instanceof Ci.nsIResumableChannel) {
+ try {
+ // If reading the ID succeeds, the source is resumable.
+ this.entityID = aRequest.entityID;
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_ERROR_NOT_RESUMABLE) {
+ throw ex;
+ }
+ keepPartialData = false;
+ }
+ } else {
+ keepPartialData = false;
+ }
+ }
+
+ // Enable hashing and signature verification before setting the
+ // target.
+ backgroundFileSaver.enableSha256();
+ backgroundFileSaver.enableSignatureInfo();
+ if (partFilePath) {
+ // If we actually resumed a request, append to the partial data.
+ if (resumeAttempted) {
+ // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
+ backgroundFileSaver.enableAppend();
+ }
+
+ // Use a part file, determining if we should keep it on failure.
+ backgroundFileSaver.setTarget(new FileUtils.File(partFilePath),
+ keepPartialData);
+ } else {
+ // Set the final target file, and delete it on failure.
+ backgroundFileSaver.setTarget(new FileUtils.File(targetPath),
+ false);
+ }
+ }.bind(copySaver),
+
+ onStopRequest: function (aRequest, aContext, aStatusCode) {
+ try {
+ backgroundFileSaver.onStopRequest(aRequest, aContext,
+ aStatusCode);
+ } finally {
+ // If the data transfer completed successfully, indicate to the
+ // background file saver that the operation can finish. If the
+ // data transfer failed, the saver has been already stopped.
+ if (Components.isSuccessCode(aStatusCode)) {
+ backgroundFileSaver.finish(Cr.NS_OK);
+ }
+ }
+ }.bind(copySaver),
+
+ onDataAvailable: function (aRequest, aContext, aInputStream,
+ aOffset, aCount) {
+ backgroundFileSaver.onDataAvailable(aRequest, aContext,
+ aInputStream, aOffset,
+ aCount);
+ }.bind(copySaver),
+ });
+
+ // We should check if we have been canceled in the meantime, after
+ // all the previous asynchronous operations have been executed and
+ // just before we set the _backgroundFileSaver property.
+ if (this._canceled) {
+ throw new DownloadError({ message: "Saver canceled." });
+ }
+
+ // If the operation succeeded, store the object to allow cancellation.
+ this._backgroundFileSaver = backgroundFileSaver;
+ } catch (ex) {
+ // In case an error occurs while setting up the chain of objects for
+ // the download, ensure that we release the resources of the saver.
+ backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
+ // Since we're not going to handle deferSaveComplete.promise below,
+ // we need to make sure that the rejection is handled.
+ deferSaveComplete.promise.catch(() => {});
+ throw ex;
+ }
+
+ // We will wait on this promise in case no error occurred while setting
+ // up the chain of objects for the download.
+ yield deferSaveComplete.promise;
+
+ yield this._checkReputationAndMove(aSetPropertiesFn);
+ } catch (ex) {
+ // Ensure we always remove the placeholder for the final target file on
+ // failure, independently of which code path failed. In some cases, the
+ // background file saver may have already removed the file.
+ try {
+ yield OS.File.remove(targetPath);
+ } catch (e2) {
+ // If we failed during the operation, we report the error but use the
+ // original one as the failure reason of the download. Note that on
+ // Windows we may get an access denied error instead of a no such file
+ // error if the file existed before, and was recently deleted.
+ if (!(e2 instanceof OS.File.Error &&
+ (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
+ Cu.reportError(e2);
+ }
+ }
+ throw ex;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Perform the reputation check and cleanup the downloaded data if required.
+ * If the download passes the reputation check and is using a part file we
+ * will move it to the target path since reputation checking is the final
+ * step in the saver.
+ *
+ * @param aSetPropertiesFn
+ * Function provided to the "execute" method.
+ *
+ * @return {Promise}
+ * @resolves When the reputation check and cleanup is complete.
+ * @rejects DownloadError if the download should be blocked.
+ */
+ _checkReputationAndMove: Task.async(function* (aSetPropertiesFn) {
+ let download = this.download;
+ let targetPath = this.download.target.path;
+ let partFilePath = this.download.target.partFilePath;
+
+ let { shouldBlock, verdict } =
+ yield DownloadIntegration.shouldBlockForReputationCheck(download);
+ if (shouldBlock) {
+ let newProperties = { progress: 100, hasPartialData: false };
+
+ // We will remove the potentially dangerous file if instructed by
+ // DownloadIntegration. We will always remove the file when the
+ // download did not use a partial file path, meaning it
+ // currently has its final filename.
+ if (!DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
+ try {
+ yield OS.File.remove(partFilePath || targetPath);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ } else {
+ newProperties.hasBlockedData = true;
+ }
+
+ aSetPropertiesFn(newProperties);
+
+ throw new DownloadError({
+ becauseBlockedByReputationCheck: true,
+ reputationCheckVerdict: verdict,
+ });
+ }
+
+ if (partFilePath) {
+ yield OS.File.move(partFilePath, targetPath);
+ }
+ }),
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DCS_cancel()
+ {
+ this._canceled = true;
+ if (this._backgroundFileSaver) {
+ this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
+ this._backgroundFileSaver = null;
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.removePartialData".
+ */
+ removePartialData: function ()
+ {
+ return Task.spawn(function* task_DCS_removePartialData() {
+ if (this.download.target.partFilePath) {
+ try {
+ yield OS.File.remove(this.download.target.partFilePath);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ }
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ // Simplify the representation if we don't have other details.
+ if (!this.entityID && !this._unknownProperties) {
+ return "copy";
+ }
+
+ let serializable = { type: "copy",
+ entityID: this.entityID };
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+
+ /**
+ * Implements "DownloadSaver.getSha256Hash"
+ */
+ getSha256Hash: function ()
+ {
+ return this._sha256Hash;
+ },
+
+ /*
+ * Implements DownloadSaver.getSignatureInfo.
+ */
+ getSignatureInfo: function ()
+ {
+ return this._signatureInfo;
+ },
+
+ /*
+ * Implements DownloadSaver.getRedirects.
+ */
+ getRedirects: function ()
+ {
+ return this._redirects;
+ }
+};
+
+/**
+ * Creates a new DownloadCopySaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadCopySaver object.
+ *
+ * @return The newly created DownloadCopySaver object.
+ */
+this.DownloadCopySaver.fromSerializable = function (aSerializable) {
+ let saver = new DownloadCopySaver();
+ if ("entityID" in aSerializable) {
+ saver.entityID = aSerializable.entityID;
+ }
+
+ deserializeUnknownProperties(saver, aSerializable, property =>
+ property != "entityID" && property != "type");
+
+ return saver;
+};
+
+// DownloadLegacySaver
+
+/**
+ * Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * For more background on the process, see the DownloadLegacyTransfer object.
+ */
+this.DownloadLegacySaver = function ()
+{
+ this.deferExecuted = Promise.defer();
+ this.deferCanceled = Promise.defer();
+}
+
+this.DownloadLegacySaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * Save the SHA-256 hash in raw bytes of the downloaded file. This may be
+ * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not
+ * invoked.
+ */
+ _sha256Hash: null,
+
+ /**
+ * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
+ * if the file is signed. This is empty if the file is unsigned, and null
+ * unless BackgroundFileSaver has successfully completed saving the file.
+ */
+ _signatureInfo: null,
+
+ /**
+ * Save the redirect chain as an nsIArray of nsIPrincipal.
+ */
+ _redirects: null,
+
+ /**
+ * nsIRequest object associated to the status and progress updates we
+ * received. This object is null before we receive the first status and
+ * progress update, and is also reset to null when the download is stopped.
+ */
+ request: null,
+
+ /**
+ * This deferred object contains a promise that is resolved as soon as this
+ * download finishes successfully, and is rejected in case the download is
+ * canceled or receives a failure notification through nsITransfer.
+ */
+ deferExecuted: null,
+
+ /**
+ * This deferred object contains a promise that is resolved if the download
+ * receives a cancellation request through the "cancel" method, and is never
+ * rejected. The nsITransfer implementation will register a handler that
+ * actually causes the download cancellation.
+ */
+ deferCanceled: null,
+
+ /**
+ * This is populated with the value of the aSetProgressBytesFn argument of the
+ * "execute" method, and is null before the method is called.
+ */
+ setProgressBytesFn: null,
+
+ /**
+ * Called by the nsITransfer implementation while the download progresses.
+ *
+ * @param aCurrentBytes
+ * Number of bytes transferred until now.
+ * @param aTotalBytes
+ * Total number of bytes to be transferred, or -1 if unknown.
+ */
+ onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes)
+ {
+ this.progressWasNotified = true;
+
+ // Ignore progress notifications until we are ready to process them.
+ if (!this.setProgressBytesFn) {
+ // Keep the data from the last progress notification that was received.
+ this.currentBytes = aCurrentBytes;
+ this.totalBytes = aTotalBytes;
+ return;
+ }
+
+ let hasPartFile = !!this.download.target.partFilePath;
+
+ this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
+ aCurrentBytes > 0 && hasPartFile);
+ },
+
+ /**
+ * Whether the onProgressBytes function has been called at least once.
+ */
+ progressWasNotified: false,
+
+ /**
+ * Called by the nsITransfer implementation when the request has started.
+ *
+ * @param aRequest
+ * nsIRequest associated to the status update.
+ * @param aAlreadyAddedToHistory
+ * Indicates that the nsIExternalHelperAppService component already
+ * added the download to the browsing history, unless it was started
+ * from a private browsing window. When this parameter is false, the
+ * download is added to the browsing history here. Private downloads
+ * are never added to history even if this parameter is false.
+ */
+ onTransferStarted: function (aRequest, aAlreadyAddedToHistory)
+ {
+ // Store the entity ID to use for resuming if required.
+ if (this.download.tryToKeepPartialData &&
+ aRequest instanceof Ci.nsIResumableChannel) {
+ try {
+ // If reading the ID succeeds, the source is resumable.
+ this.entityID = aRequest.entityID;
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_ERROR_NOT_RESUMABLE) {
+ throw ex;
+ }
+ }
+ }
+
+ // For legacy downloads, we must update the referrer at this time.
+ if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) {
+ this.download.source.referrer = aRequest.referrer.spec;
+ }
+
+ if (!aAlreadyAddedToHistory) {
+ this.addToHistory();
+ }
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the request has finished.
+ *
+ * @param aRequest
+ * nsIRequest associated to the status update.
+ * @param aStatus
+ * Status code received by the nsITransfer implementation.
+ */
+ onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus)
+ {
+ // Store a reference to the request, used when handling completion.
+ this.request = aRequest;
+
+ if (Components.isSuccessCode(aStatus)) {
+ this.deferExecuted.resolve();
+ } else {
+ // Infer the origin of the error from the failure code, because more
+ // specific data is not available through the nsITransfer implementation.
+ let properties = { result: aStatus, inferCause: true };
+ this.deferExecuted.reject(new DownloadError(properties));
+ }
+ },
+
+ /**
+ * When the first execution of the download finished, it can be restarted by
+ * using a DownloadCopySaver object instead of the original legacy component
+ * that executed the download.
+ */
+ firstExecutionFinished: false,
+
+ /**
+ * In case the download is restarted after the first execution finished, this
+ * property contains a reference to the DownloadCopySaver that is executing
+ * the new download attempt.
+ */
+ copySaver: null,
+
+ /**
+ * String corresponding to the entityID property of the nsIResumableChannel
+ * used to execute the download, or null if the channel was not resumable or
+ * the saver was instructed not to keep partially downloaded data.
+ */
+ entityID: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function DLS_execute(aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ // Check if this is not the first execution of the download. The Download
+ // object guarantees that this function is not re-entered during execution.
+ if (this.firstExecutionFinished) {
+ if (!this.copySaver) {
+ this.copySaver = new DownloadCopySaver();
+ this.copySaver.download = this.download;
+ this.copySaver.entityID = this.entityID;
+ this.copySaver.alreadyAddedToHistory = true;
+ }
+ return this.copySaver.execute.apply(this.copySaver, arguments);
+ }
+
+ this.setProgressBytesFn = aSetProgressBytesFn;
+ if (this.progressWasNotified) {
+ this.onProgressBytes(this.currentBytes, this.totalBytes);
+ }
+
+ return Task.spawn(function* task_DLS_execute() {
+ try {
+ // Wait for the component that executes the download to finish.
+ yield this.deferExecuted.promise;
+
+ // At this point, the "request" property has been populated. Ensure we
+ // report the value of "Content-Length", if available, even if the
+ // download didn't generate any progress events.
+ if (!this.progressWasNotified &&
+ this.request instanceof Ci.nsIChannel &&
+ this.request.contentLength >= 0) {
+ aSetProgressBytesFn(0, this.request.contentLength);
+ }
+
+ // If the component executing the download provides the path of a
+ // ".part" file, it means that it expects the listener to move the file
+ // to its final target path when the download succeeds. In this case,
+ // an empty ".part" file is created even if no data was received from
+ // the source.
+ //
+ // When no ".part" file path is provided the download implementation may
+ // not have created the target file (if no data was received from the
+ // source). In this case, ensure that an empty file is created as
+ // expected.
+ if (!this.download.target.partFilePath) {
+ try {
+ // This atomic operation is more efficient than an existence check.
+ let file = yield OS.File.open(this.download.target.path,
+ { create: true });
+ yield file.close();
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseExists) {
+ throw ex;
+ }
+ }
+ }
+
+ yield this._checkReputationAndMove(aSetPropertiesFn);
+
+ } catch (ex) {
+ // Ensure we always remove the final target file on failure,
+ // independently of which code path failed. In some cases, the
+ // component executing the download may have already removed the file.
+ try {
+ yield OS.File.remove(this.download.target.path);
+ } catch (e2) {
+ // If we failed during the operation, we report the error but use the
+ // original one as the failure reason of the download. Note that on
+ // Windows we may get an access denied error instead of a no such file
+ // error if the file existed before, and was recently deleted.
+ if (!(e2 instanceof OS.File.Error &&
+ (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
+ Cu.reportError(e2);
+ }
+ }
+ // In case the operation failed, ensure we stop downloading data. Since
+ // we never re-enter this function, deferCanceled is always available.
+ this.deferCanceled.resolve();
+ throw ex;
+ } finally {
+ // We don't need the reference to the request anymore. We must also set
+ // deferCanceled to null in order to free any indirect references it
+ // may hold to the request.
+ this.request = null;
+ this.deferCanceled = null;
+ // Allow the download to restart through a DownloadCopySaver.
+ this.firstExecutionFinished = true;
+ }
+ }.bind(this));
+ },
+
+ _checkReputationAndMove: function () {
+ return DownloadCopySaver.prototype._checkReputationAndMove
+ .apply(this, arguments);
+ },
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DLS_cancel()
+ {
+ // We may be using a DownloadCopySaver to handle resuming.
+ if (this.copySaver) {
+ return this.copySaver.cancel.apply(this.copySaver, arguments);
+ }
+
+ // If the download hasn't stopped already, resolve deferCanceled so that the
+ // operation is canceled as soon as a cancellation handler is registered.
+ // Note that the handler might not have been registered yet.
+ if (this.deferCanceled) {
+ this.deferCanceled.resolve();
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.removePartialData".
+ */
+ removePartialData: function ()
+ {
+ // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing
+ // partially downloaded data, though this implementation isn't shared by
+ // other saver types, thus it isn't found on their shared prototype.
+ return DownloadCopySaver.prototype.removePartialData.call(this);
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ // This object depends on legacy components that are created externally,
+ // thus it cannot be rebuilt during deserialization. To support resuming
+ // across different browser sessions, this object is transformed into a
+ // DownloadCopySaver for the purpose of serialization.
+ return DownloadCopySaver.prototype.toSerializable.call(this);
+ },
+
+ /**
+ * Implements "DownloadSaver.getSha256Hash".
+ */
+ getSha256Hash: function ()
+ {
+ if (this.copySaver) {
+ return this.copySaver.getSha256Hash();
+ }
+ return this._sha256Hash;
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the hash is available.
+ */
+ setSha256Hash: function (hash)
+ {
+ this._sha256Hash = hash;
+ },
+
+ /**
+ * Implements "DownloadSaver.getSignatureInfo".
+ */
+ getSignatureInfo: function ()
+ {
+ if (this.copySaver) {
+ return this.copySaver.getSignatureInfo();
+ }
+ return this._signatureInfo;
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the hash is available.
+ */
+ setSignatureInfo: function (signatureInfo)
+ {
+ this._signatureInfo = signatureInfo;
+ },
+
+ /**
+ * Implements "DownloadSaver.getRedirects".
+ */
+ getRedirects: function ()
+ {
+ if (this.copySaver) {
+ return this.copySaver.getRedirects();
+ }
+ return this._redirects;
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the redirect chain is
+ * available.
+ */
+ setRedirects: function (redirects)
+ {
+ this._redirects = redirects;
+ },
+};
+
+/**
+ * Returns a new DownloadLegacySaver object. This saver type has a
+ * deserializable form only when creating a new object in memory, because it
+ * cannot be serialized to disk.
+ */
+this.DownloadLegacySaver.fromSerializable = function () {
+ return new DownloadLegacySaver();
+};
+
+// DownloadPDFSaver
+
+/**
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ *
+ * In order to prevent the download from saving a different document than the one
+ * originally loaded in the window, any attempt to restart the download will fail.
+ *
+ * Since this DownloadSaver type requires a live document as a source, it cannot
+ * be persisted across sessions, unless the download already succeeded.
+ */
+this.DownloadPDFSaver = function () {
+}
+
+this.DownloadPDFSaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * An nsIWebBrowserPrint instance for printing this page.
+ * This is null when saving has not started or has completed,
+ * or while the operation is being canceled.
+ */
+ _webBrowserPrint: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function (aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ return Task.spawn(function* task_DCS_execute() {
+ if (!this.download.source.windowRef) {
+ throw new DownloadError({
+ message: "PDF saver must be passed an open window, and cannot be restarted.",
+ becauseSourceFailed: true,
+ });
+ }
+
+ let win = this.download.source.windowRef.get();
+
+ // Set windowRef to null to avoid re-trying.
+ this.download.source.windowRef = null;
+
+ if (!win) {
+ throw new DownloadError({
+ message: "PDF saver can't save a window that has been closed.",
+ becauseSourceFailed: true,
+ });
+ }
+
+ this.addToHistory();
+
+ let targetPath = this.download.target.path;
+
+ // An empty target file must exist for the PDF printer to work correctly.
+ let file = yield OS.File.open(targetPath, { truncate: true });
+ yield file.close();
+
+ let printSettings = gPrintSettingsService.newPrintSettings;
+
+ printSettings.printToFile = true;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+ printSettings.toFileName = targetPath;
+
+ printSettings.printSilent = true;
+ printSettings.showPrintProgress = false;
+
+ printSettings.printBGImages = true;
+ printSettings.printBGColors = true;
+ printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+ printSettings.headerStrCenter = "";
+ printSettings.headerStrLeft = "";
+ printSettings.headerStrRight = "";
+ printSettings.footerStrCenter = "";
+ printSettings.footerStrLeft = "";
+ printSettings.footerStrRight = "";
+
+ this._webBrowserPrint = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserPrint);
+
+ try {
+ yield new Promise((resolve, reject) => {
+ this._webBrowserPrint.print(printSettings, {
+ onStateChange: function (webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ if (!Components.isSuccessCode(status)) {
+ reject(new DownloadError({ result: status,
+ inferCause: true }));
+ } else {
+ resolve();
+ }
+ }
+ },
+ onProgressChange: function (webProgress, request, curSelfProgress,
+ maxSelfProgress, curTotalProgress,
+ maxTotalProgress) {
+ aSetProgressBytesFn(curTotalProgress, maxTotalProgress, false);
+ },
+ onLocationChange: function () {},
+ onStatusChange: function () {},
+ onSecurityChange: function () {},
+ });
+ });
+ } finally {
+ // Remove the print object to avoid leaks
+ this._webBrowserPrint = null;
+ }
+
+ let fileInfo = yield OS.File.stat(targetPath);
+ aSetProgressBytesFn(fileInfo.size, fileInfo.size, false);
+ }.bind(this));
+ },
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DCS_cancel()
+ {
+ if (this._webBrowserPrint) {
+ this._webBrowserPrint.cancel();
+ this._webBrowserPrint = null;
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ if (this.download.succeeded) {
+ return DownloadCopySaver.prototype.toSerializable.call(this);
+ }
+
+ // This object needs a window to recreate itself. If it didn't succeded
+ // it will not be possible to restart. Returning null here will
+ // prevent us from serializing it at all.
+ return null;
+ },
+};
+
+/**
+ * Creates a new DownloadPDFSaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadPDFSaver object.
+ *
+ * @return The newly created DownloadPDFSaver object.
+ */
+this.DownloadPDFSaver.fromSerializable = function (aSerializable) {
+ return new DownloadPDFSaver();
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadImport.jsm b/toolkit/components/jsdownloads/src/DownloadImport.jsm
new file mode 100644
index 0000000000..5fb7fd0c73
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -0,0 +1,193 @@
+/* -*- 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";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadImport",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+/**
+ * These values come from the previous interface
+ * nsIDownloadManager, which has now been deprecated.
+ * These are the only types of download states that
+ * we will import.
+ */
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+// DownloadImport
+
+/**
+ * Provides an object that has a method to import downloads
+ * from the previous SQLite storage format.
+ *
+ * @param aList A DownloadList where each successfully
+ * imported download will be added.
+ * @param aPath The path to the database file.
+ */
+this.DownloadImport = function (aList, aPath)
+{
+ this.list = aList;
+ this.path = aPath;
+}
+
+this.DownloadImport.prototype = {
+ /**
+ * Imports unfinished downloads from the previous SQLite storage
+ * format (supporting schemas 7 and up), to the new Download object
+ * format. Each imported download will be added to the DownloadList
+ *
+ * @return {Promise}
+ * @resolves When the operation has completed (i.e., every download
+ * from the previous database has been read and added to
+ * the DownloadList)
+ */
+ import: function () {
+ return Task.spawn(function* task_DI_import() {
+ let connection = yield Sqlite.openConnection({ path: this.path });
+
+ try {
+ let schemaVersion = yield connection.getSchemaVersion();
+ // We don't support schemas older than version 7 (from 2007)
+ // - Version 7 added the columns mimeType, preferredApplication
+ // and preferredAction in 2007
+ // - Version 8 added the column autoResume in 2007
+ // (if we encounter version 7 we will treat autoResume = false)
+ // - Version 9 is the last known version, which added a unique
+ // GUID text column that is not used here
+ if (schemaVersion < 7) {
+ throw new Error("Unable to import in-progress downloads because "
+ + "the existing profile is too old.");
+ }
+
+ let rows = yield connection.execute("SELECT * FROM moz_downloads");
+
+ for (let row of rows) {
+ try {
+ // Get the DB row data
+ let source = row.getResultByName("source");
+ let target = row.getResultByName("target");
+ let tempPath = row.getResultByName("tempPath");
+ let startTime = row.getResultByName("startTime");
+ let state = row.getResultByName("state");
+ let referrer = row.getResultByName("referrer");
+ let maxBytes = row.getResultByName("maxBytes");
+ let mimeType = row.getResultByName("mimeType");
+ let preferredApplication = row.getResultByName("preferredApplication");
+ let preferredAction = row.getResultByName("preferredAction");
+ let entityID = row.getResultByName("entityID");
+
+ let autoResume = false;
+ try {
+ autoResume = (row.getResultByName("autoResume") == 1);
+ } catch (ex) {
+ // autoResume wasn't present in schema version 7
+ }
+
+ if (!source) {
+ throw new Error("Attempted to import a row with an empty " +
+ "source column.");
+ }
+
+ let resumeDownload = false;
+
+ switch (state) {
+ case DOWNLOAD_NOTSTARTED:
+ case DOWNLOAD_QUEUED:
+ case DOWNLOAD_DOWNLOADING:
+ resumeDownload = true;
+ break;
+
+ case DOWNLOAD_PAUSED:
+ resumeDownload = autoResume;
+ break;
+
+ default:
+ // We won't import downloads in other states
+ continue;
+ }
+
+ // Transform the data
+ let targetPath = NetUtil.newURI(target)
+ .QueryInterface(Ci.nsIFileURL).file.path;
+
+ let launchWhenSucceeded = (preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+
+ let downloadOptions = {
+ source: {
+ url: source,
+ referrer: referrer
+ },
+ target: {
+ path: targetPath,
+ partFilePath: tempPath,
+ },
+ saver: {
+ type: "copy",
+ entityID: entityID
+ },
+ startTime: new Date(startTime / 1000),
+ totalBytes: maxBytes,
+ hasPartialData: !!tempPath,
+ tryToKeepPartialData: true,
+ launchWhenSucceeded: launchWhenSucceeded,
+ contentType: mimeType,
+ launcherPath: preferredApplication
+ };
+
+ // Paused downloads that should not be auto-resumed are considered
+ // in a "canceled" state.
+ if (!resumeDownload) {
+ downloadOptions.canceled = true;
+ }
+
+ let download = yield Downloads.createDownload(downloadOptions);
+
+ yield this.list.add(download);
+
+ if (resumeDownload) {
+ download.start().catch(() => {});
+ } else {
+ yield download.refresh();
+ }
+
+ } catch (ex) {
+ Cu.reportError("Error importing download: " + ex);
+ }
+ }
+
+ } catch (ex) {
+ Cu.reportError(ex);
+ } finally {
+ yield connection.close();
+ }
+ }.bind(this));
+ }
+}
+
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
new file mode 100644
index 0000000000..5fed9212a5
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -0,0 +1,1273 @@
+/* -*- 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/. */
+
+/**
+ * Provides functions to integrate with the host application, handling for
+ * example the global prompts on shutdown.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadIntegration",
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
+ "resource://gre/modules/DownloadStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+ "resource://gre/modules/DownloadImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+#ifdef MOZ_PLACES
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+#endif
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform",
+ "@mozilla.org/toolkit/download-platform;1",
+ "mozIDownloadPlatform");
+XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment",
+ "@mozilla.org/process/environment;1",
+ "nsIEnvironment");
+XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService");
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService");
+#ifdef MOZ_WIDGET_ANDROID
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions",
+ "resource://gre/modules/RuntimePermissions.jsm");
+#endif
+
+XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ return Cc["@mozilla.org/parental-controls-service;1"]
+ .createInstance(Ci.nsIParentalControlsService);
+ }
+ return null;
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService",
+ "@mozilla.org/downloads/application-reputation-service;1",
+ Ci.nsIApplicationReputationService);
+
+XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
+ "@mozilla.org/telephony/volume-service;1",
+ "nsIVolumeService");
+
+// We have to use the gCombinedDownloadIntegration identifier because, in this
+// module only, the DownloadIntegration identifier refers to the base version.
+Integration.downloads.defineModuleGetter(this, "gCombinedDownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm",
+ "DownloadIntegration");
+
+const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
+ "initWithCallback");
+
+/**
+ * Indicates the delay between a change to the downloads data and the related
+ * save operation.
+ *
+ * For best efficiency, this value should be high enough that the input/output
+ * for opening or closing the target file does not overlap with the one for
+ * saving the list of downloads.
+ */
+const kSaveDelayMs = 1500;
+
+/**
+ * This pref indicates if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ */
+const kPrefImportedFromSqlite = "browser.download.importedFromSqlite";
+
+/**
+ * List of observers to listen against
+ */
+const kObserverTopics = [
+ "quit-application-requested",
+ "offline-requested",
+ "last-pb-context-exiting",
+ "last-pb-context-exited",
+ "sleep_notification",
+ "suspend_process_notification",
+ "wake_notification",
+ "resume_process_notification",
+ "network:offline-about-to-go-offline",
+ "network:offline-status-changed",
+ "xpcom-will-shutdown",
+];
+
+/**
+ * Maps nsIApplicationReputationService verdicts with the DownloadError ones.
+ */
+const kVerdictMap = {
+ [Ci.nsIApplicationReputationService.VERDICT_DANGEROUS]:
+ Downloads.Error.BLOCK_VERDICT_MALWARE,
+ [Ci.nsIApplicationReputationService.VERDICT_UNCOMMON]:
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ [Ci.nsIApplicationReputationService.VERDICT_POTENTIALLY_UNWANTED]:
+ Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+ [Ci.nsIApplicationReputationService.VERDICT_DANGEROUS_HOST]:
+ Downloads.Error.BLOCK_VERDICT_MALWARE,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadIntegration
+
+/**
+ * Provides functions to integrate with the host application, handling for
+ * example the global prompts on shutdown.
+ */
+this.DownloadIntegration = {
+ /**
+ * Main DownloadStore object for loading and saving the list of persistent
+ * downloads, or null if the download list was never requested and thus it
+ * doesn't need to be persisted.
+ */
+ _store: null,
+
+ /**
+ * Returns whether data for blocked downloads should be kept on disk.
+ * Implementations which support unblocking downloads may return true to
+ * keep the blocked download on disk until its fate is decided.
+ *
+ * If a download is blocked and the partial data is kept the Download's
+ * 'hasBlockedData' property will be true. In this state Download.unblock()
+ * or Download.confirmBlock() may be used to either unblock the download or
+ * remove the downloaded data respectively.
+ *
+ * Even if shouldKeepBlockedData returns true, if the download did not use a
+ * partFile the blocked data will be removed - preventing the complete
+ * download from existing on disk with its final filename.
+ *
+ * @return boolean True if data should be kept.
+ */
+ shouldKeepBlockedData() {
+ const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+ return Services.appinfo.ID == FIREFOX_ID;
+ },
+
+ /**
+ * Performs initialization of the list of persistent downloads, before its
+ * first use by the host application. This function may be called only once
+ * during the entire lifetime of the application.
+ *
+ * @param list
+ * DownloadList object to be initialized.
+ *
+ * @return {Promise}
+ * @resolves When the list has been initialized.
+ * @rejects JavaScript exception.
+ */
+ initializePublicDownloadList: Task.async(function* (list) {
+ try {
+ yield this.loadPublicDownloadListFromStore(list);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // After the list of persistent downloads has been loaded, we can add the
+ // history observers, even if the load operation failed. This object is kept
+ // alive by the history service.
+ new DownloadHistoryObserver(list);
+ }),
+
+ /**
+ * Called by initializePublicDownloadList to load the list of persistent
+ * downloads, before its first use by the host application. This function may
+ * be called only once during the entire lifetime of the application.
+ *
+ * @param list
+ * DownloadList object to be populated with the download objects
+ * serialized from the previous session. This list will be persisted
+ * to disk during the session lifetime.
+ *
+ * @return {Promise}
+ * @resolves When the list has been populated.
+ * @rejects JavaScript exception.
+ */
+ loadPublicDownloadListFromStore: Task.async(function* (list) {
+ if (this._store) {
+ throw new Error("Initialization may be performed only once.");
+ }
+
+ this._store = new DownloadStore(list, OS.Path.join(
+ OS.Constants.Path.profileDir,
+ "downloads.json"));
+ this._store.onsaveitem = this.shouldPersistDownload.bind(this);
+
+ try {
+ if (this._importedFromSqlite) {
+ yield this._store.load();
+ } else {
+ let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
+ "downloads.sqlite");
+
+ if (yield OS.File.exists(sqliteDBpath)) {
+ let sqliteImport = new DownloadImport(list, sqliteDBpath);
+ yield sqliteImport.import();
+
+ let importCount = (yield list.getAll()).length;
+ if (importCount > 0) {
+ try {
+ yield this._store.save();
+ } catch (ex) { }
+ }
+
+ // No need to wait for the file removal.
+ OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
+ }
+
+ Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
+
+ // Don't even report error here because this file is pre Firefox 3
+ // and most likely doesn't exist.
+ OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
+ "downloads.rdf")).catch(() => {});
+
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // Add the view used for detecting changes to downloads to be persisted.
+ // We must do this after the list of persistent downloads has been loaded,
+ // even if the load operation failed. We wait for a complete initialization
+ // so other callers cannot modify the list without being detected. The
+ // DownloadAutoSaveView is kept alive by the underlying DownloadList.
+ yield new DownloadAutoSaveView(list, this._store).initialize();
+ }),
+
+ /**
+ * Determines if a Download object from the list of persistent downloads
+ * should be saved into a file, so that it can be restored across sessions.
+ *
+ * This function allows filtering out downloads that the host application is
+ * not interested in persisting across sessions, for example downloads that
+ * finished successfully.
+ *
+ * @param aDownload
+ * The Download object to be inspected. This is originally taken from
+ * the global DownloadList object for downloads that were not started
+ * from a private browsing window. The item may have been removed
+ * from the list since the save operation started, though in this case
+ * the save operation will be repeated later.
+ *
+ * @return True to save the download, false otherwise.
+ */
+ shouldPersistDownload(aDownload) {
+ // On all platforms, we save all the downloads currently in progress, as
+ // well as stopped downloads for which we retained partially downloaded
+ // data or we have blocked data.
+ if (!aDownload.stopped || aDownload.hasPartialData ||
+ aDownload.hasBlockedData) {
+ return true;
+ }
+#ifdef MOZ_B2G
+ // On B2G we keep a few days of history.
+ let maxTime = Date.now() -
+ Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000;
+ return aDownload.startTime > maxTime;
+#elif defined(MOZ_WIDGET_ANDROID)
+ // On Android we store all history.
+ return true;
+#else
+ // On Desktop, stopped downloads for which we don't need to track the
+ // presence of a ".part" file are only retained in the browser history.
+ return false;
+#endif
+ },
+
+ /**
+ * Returns the system downloads directory asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getSystemDownloadsDirectory: Task.async(function* () {
+ if (this._downloadsDirectory) {
+ return this._downloadsDirectory;
+ }
+
+ let directoryPath = null;
+#ifdef XP_MACOSX
+ directoryPath = this._getDirectory("DfltDwnld");
+#elifdef XP_WIN
+ // For XP/2K, use My Documents/Downloads. Other version uses
+ // the default Downloads directory.
+ let version = parseFloat(Services.sysinfo.getProperty("version"));
+ if (version < 6) {
+ directoryPath = yield this._createDownloadsDirectory("Pers");
+ } else {
+ directoryPath = this._getDirectory("DfltDwnld");
+ }
+#elifdef XP_UNIX
+#ifdef MOZ_WIDGET_ANDROID
+ // Android doesn't have a $HOME directory, and by default we only have
+ // write access to /data/data/org.mozilla.{$APP} and /sdcard
+ directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
+ if (!directoryPath) {
+ throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
+ Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
+ }
+#else
+ // For Linux, use XDG download dir, with a fallback to Home/Downloads
+ // if the XDG user dirs are disabled.
+ try {
+ directoryPath = this._getDirectory("DfltDwnld");
+ } catch(e) {
+ directoryPath = yield this._createDownloadsDirectory("Home");
+ }
+#endif
+#else
+ directoryPath = yield this._createDownloadsDirectory("Home");
+#endif
+
+ this._downloadsDirectory = directoryPath;
+ return this._downloadsDirectory;
+ }),
+ _downloadsDirectory: null,
+
+ /**
+ * Returns the user downloads directory asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getPreferredDownloadsDirectory: Task.async(function* () {
+ let directoryPath = null;
+ let prefValue = 1;
+
+ try {
+ prefValue = Services.prefs.getIntPref("browser.download.folderList");
+ } catch(e) {}
+
+ switch(prefValue) {
+ case 0: // Desktop
+ directoryPath = this._getDirectory("Desk");
+ break;
+ case 1: // Downloads
+ directoryPath = yield this.getSystemDownloadsDirectory();
+ break;
+ case 2: // Custom
+ try {
+ let directory = Services.prefs.getComplexValue("browser.download.dir",
+ Ci.nsIFile);
+ directoryPath = directory.path;
+ yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
+ } catch(ex) {
+ // Either the preference isn't set or the directory cannot be created.
+ directoryPath = yield this.getSystemDownloadsDirectory();
+ }
+ break;
+ default:
+ directoryPath = yield this.getSystemDownloadsDirectory();
+ }
+ return directoryPath;
+ }),
+
+ /**
+ * Returns the temporary downloads directory asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getTemporaryDownloadsDirectory: Task.async(function* () {
+ let directoryPath = null;
+#ifdef XP_MACOSX
+ directoryPath = yield this.getPreferredDownloadsDirectory();
+#elifdef MOZ_WIDGET_ANDROID
+ directoryPath = yield this.getSystemDownloadsDirectory();
+#elifdef MOZ_WIDGET_GONK
+ directoryPath = yield this.getSystemDownloadsDirectory();
+#else
+ directoryPath = this._getDirectory("TmpD");
+#endif
+ return directoryPath;
+ }),
+
+ /**
+ * Checks to determine whether to block downloads for parental controls.
+ *
+ * aParam aDownload
+ * The download object.
+ *
+ * @return {Promise}
+ * @resolves The boolean indicates to block downloads or not.
+ */
+ shouldBlockForParentalControls(aDownload) {
+ let isEnabled = gParentalControlsService &&
+ gParentalControlsService.parentalControlsEnabled;
+ let shouldBlock = isEnabled &&
+ gParentalControlsService.blockFileDownloadsEnabled;
+
+ // Log the event if required by parental controls settings.
+ if (isEnabled && gParentalControlsService.loggingEnabled) {
+ gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
+ shouldBlock,
+ NetUtil.newURI(aDownload.source.url), null);
+ }
+
+ return Promise.resolve(shouldBlock);
+ },
+
+ /**
+ * Checks to determine whether to block downloads for not granted runtime permissions.
+ *
+ * @return {Promise}
+ * @resolves The boolean indicates to block downloads or not.
+ */
+ shouldBlockForRuntimePermissions() {
+#ifdef MOZ_WIDGET_ANDROID
+ return RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE)
+ .then(permissionGranted => !permissionGranted);
+#else
+ return Promise.resolve(false);
+#endif
+ },
+
+ /**
+ * Checks to determine whether to block downloads because they might be
+ * malware, based on application reputation checks.
+ *
+ * aParam aDownload
+ * The download object.
+ *
+ * @return {Promise}
+ * @resolves Object with the following properties:
+ * {
+ * shouldBlock: Whether the download should be blocked.
+ * verdict: Detailed reason for the block, according to the
+ * "Downloads.Error.BLOCK_VERDICT_" constants, or empty
+ * string if the reason is unknown.
+ * }
+ */
+ shouldBlockForReputationCheck(aDownload) {
+ let hash;
+ let sigInfo;
+ let channelRedirects;
+ try {
+ hash = aDownload.saver.getSha256Hash();
+ sigInfo = aDownload.saver.getSignatureInfo();
+ channelRedirects = aDownload.saver.getRedirects();
+ } catch (ex) {
+ // Bail if DownloadSaver doesn't have a hash or signature info.
+ return Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ });
+ }
+ if (!hash || !sigInfo) {
+ return Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ });
+ }
+ let deferred = Promise.defer();
+ let aReferrer = null;
+ if (aDownload.source.referrer) {
+ aReferrer = NetUtil.newURI(aDownload.source.referrer);
+ }
+ gApplicationReputationService.queryReputation({
+ sourceURI: NetUtil.newURI(aDownload.source.url),
+ referrerURI: aReferrer,
+ fileSize: aDownload.currentBytes,
+ sha256Hash: hash,
+ suggestedFileName: OS.Path.basename(aDownload.target.path),
+ signatureInfo: sigInfo,
+ redirects: channelRedirects },
+ function onComplete(aShouldBlock, aRv, aVerdict) {
+ deferred.resolve({
+ shouldBlock: aShouldBlock,
+ verdict: (aShouldBlock && kVerdictMap[aVerdict]) || "",
+ });
+ });
+ return deferred.promise;
+ },
+
+#ifdef XP_WIN
+ /**
+ * Checks whether downloaded files should be marked as coming from
+ * Internet Zone.
+ *
+ * @return true if files should be marked
+ */
+ _shouldSaveZoneInformation() {
+ let key = Cc["@mozilla.org/windows-registry-key;1"]
+ .createInstance(Ci.nsIWindowsRegKey);
+ try {
+ key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments",
+ Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE);
+ try {
+ return key.readIntValue("SaveZoneInformation") != 1;
+ } finally {
+ key.close();
+ }
+ } catch (ex) {
+ // If the key is not present, files should be marked by default.
+ return true;
+ }
+ },
+#endif
+
+ /**
+ * Performs platform-specific operations when a download is done.
+ *
+ * aParam aDownload
+ * The Download object.
+ *
+ * @return {Promise}
+ * @resolves When all the operations completed successfully.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ downloadDone: Task.async(function* (aDownload) {
+#ifdef XP_WIN
+ // On Windows, we mark any file saved to the NTFS file system as coming
+ // from the Internet security zone unless Group Policy disables the
+ // feature. We do this by writing to the "Zone.Identifier" Alternate
+ // Data Stream directly, because the Save method of the
+ // IAttachmentExecute interface would trigger operations that may cause
+ // the application to hang, or other performance issues.
+ // The stream created in this way is forward-compatible with all the
+ // current and future versions of Windows.
+ if (this._shouldSaveZoneInformation()) {
+ let zone;
+ try {
+ zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
+ } catch (e) {
+ // Default to Internet Zone if mapUrlToZone failed for
+ // whatever reason.
+ zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
+ }
+ try {
+ // Don't write zone IDs for Local, Intranet, or Trusted sites
+ // to match Windows behavior.
+ if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
+ let streamPath = aDownload.target.path + ":Zone.Identifier";
+ let stream = yield OS.File.open(
+ streamPath,
+ { create: true },
+ { winAllowLengthBeyondMaxPathWithCaveats: true }
+ );
+ try {
+ yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n"));
+ } finally {
+ yield stream.close();
+ }
+ }
+ } catch (ex) {
+ // If writing to the stream fails, we ignore the error and continue.
+ // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
+ // occur when working on a file system that does not support
+ // Alternate Data Streams, like FAT32, thus we don't report this
+ // specific error.
+ if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+#endif
+
+ // The file with the partially downloaded data has restrictive permissions
+ // that don't allow other users on the system to access it. Now that the
+ // download is completed, we need to adjust permissions based on whether
+ // this is a permanently downloaded file or a temporary download to be
+ // opened read-only with an external application.
+ try {
+ // The following logic to determine whether this is a temporary download
+ // is due to the fact that "deleteTempFileOnExit" is false on Mac, where
+ // downloads to be opened with external applications are preserved in
+ // the "Downloads" folder like normal downloads.
+ let isTemporaryDownload =
+ aDownload.launchWhenSucceeded && (aDownload.source.isPrivate ||
+ Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit"));
+ // Permanently downloaded files are made accessible by other users on
+ // this system, while temporary downloads are marked as read-only.
+ let options = {};
+ if (isTemporaryDownload) {
+ options.unixMode = 0o400;
+ options.winAttributes = {readOnly: true};
+ } else {
+ options.unixMode = 0o666;
+ }
+ // On Unix, the umask of the process is respected.
+ yield OS.File.setPermissions(aDownload.target.path, options);
+ } catch (ex) {
+ // We should report errors with making the permissions less restrictive
+ // or marking the file as read-only on Unix and Mac, but this should not
+ // prevent the download from completing.
+ // The setPermissions API error EPERM is expected to occur when working
+ // on a file system that does not support file permissions, like FAT32,
+ // thus we don't report this error.
+ if (!(ex instanceof OS.File.Error) || ex.unixErrno != OS.Constants.libc.EPERM) {
+ Cu.reportError(ex);
+ }
+ }
+
+ let aReferrer = null;
+ if (aDownload.source.referrer) {
+ aReferrer = NetUtil.newURI(aDownload.source.referrer);
+ }
+
+ gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
+ aReferrer,
+ new FileUtils.File(aDownload.target.path),
+ aDownload.contentType,
+ aDownload.source.isPrivate);
+ }),
+
+ /**
+ * Launches a file represented by the target of a download. This can
+ * open the file with the default application for the target MIME type
+ * or file extension, or with a custom application if
+ * aDownload.launcherPath is set.
+ *
+ * @param aDownload
+ * A Download object that contains the necessary information
+ * to launch the file. The relevant properties are: the target
+ * file, the contentType and the custom application chosen
+ * to launch it.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to launch the file has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the file is actually
+ * launched.
+ * @rejects JavaScript exception if there was an error trying to launch
+ * the file.
+ */
+ launchDownload: Task.async(function* (aDownload) {
+ let file = new FileUtils.File(aDownload.target.path);
+
+#ifndef XP_WIN
+ // Ask for confirmation if the file is executable, except on Windows where
+ // the operating system will show the prompt based on the security zone.
+ // We do this here, instead of letting the caller handle the prompt
+ // separately in the user interface layer, for two reasons. The first is
+ // because of its security nature, so that add-ons cannot forget to do
+ // this check. The second is that the system-level security prompt would
+ // be displayed at launch time in any case.
+ if (file.isExecutable() &&
+ !(yield this.confirmLaunchExecutable(file.path))) {
+ return;
+ }
+#endif
+
+ // In case of a double extension, like ".tar.gz", we only
+ // consider the last one, because the MIME service cannot
+ // handle multiple extensions.
+ let fileExtension = null, mimeInfo = null;
+ let match = file.leafName.match(/\.([^.]+)$/);
+ if (match) {
+ fileExtension = match[1];
+ }
+
+ try {
+ // The MIME service might throw if contentType == "" and it can't find
+ // a MIME type for the given extension, so we'll treat this case as
+ // an unknown mimetype.
+ mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
+ fileExtension);
+ } catch (e) { }
+
+ if (aDownload.launcherPath) {
+ if (!mimeInfo) {
+ // This should not happen on normal circumstances because launcherPath
+ // is only set when we had an instance of nsIMIMEInfo to retrieve
+ // the custom application chosen by the user.
+ throw new Error(
+ "Unable to create nsIMIMEInfo to launch a custom application");
+ }
+
+ // Custom application chosen
+ let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
+
+ mimeInfo.preferredApplicationHandler = localHandlerApp;
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+
+ this.launchFile(file, mimeInfo);
+ return;
+ }
+
+ // No custom application chosen, let's launch the file with the default
+ // handler. First, let's try to launch it through the MIME service.
+ if (mimeInfo) {
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
+
+ try {
+ this.launchFile(file, mimeInfo);
+ return;
+ } catch (ex) { }
+ }
+
+ // If it didn't work or if there was no MIME info available,
+ // let's try to directly launch the file.
+ try {
+ this.launchFile(file);
+ return;
+ } catch (ex) { }
+
+ // If our previous attempts failed, try sending it through
+ // the system's external "file:" URL handler.
+ gExternalProtocolService.loadUrl(NetUtil.newURI(file));
+ }),
+
+ /**
+ * Asks for confirmation for launching the specified executable file. This
+ * can be overridden by regression tests to avoid the interactive prompt.
+ */
+ confirmLaunchExecutable: Task.async(function* (path) {
+ // We don't anchor the prompt to a specific window intentionally, not
+ // only because this is the same behavior as the system-level prompt,
+ // but also because the most recently active window is the right choice
+ // in basically all cases.
+ return yield DownloadUIHelper.getPrompter().confirmLaunchExecutable(path);
+ }),
+
+ /**
+ * Launches the specified file, unless overridden by regression tests.
+ */
+ launchFile(file, mimeInfo) {
+ if (mimeInfo) {
+ mimeInfo.launchWithFile(file);
+ } else {
+ file.launch();
+ }
+ },
+
+ /**
+ * Shows the containing folder of a file.
+ *
+ * @param aFilePath
+ * The path to the file.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to open the containing folder has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the folder is actually
+ * opened.
+ * @rejects JavaScript exception if there was an error trying to open
+ * the containing folder.
+ */
+ showContainingDirectory: Task.async(function* (aFilePath) {
+ let file = new FileUtils.File(aFilePath);
+
+ try {
+ // Show the directory containing the file and select the file.
+ file.reveal();
+ return;
+ } 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 = file.parent;
+ if (!parent) {
+ throw new Error(
+ "Unexpected reference to a top-level directory instead of a file");
+ }
+
+ try {
+ // Open the parent directory to show where the file should be.
+ parent.launch();
+ return;
+ } catch (ex) { }
+
+ // If launch also fails (probably because it's not implemented), let
+ // the OS handler try to open the parent.
+ gExternalProtocolService.loadUrl(NetUtil.newURI(parent));
+ }),
+
+ /**
+ * Calls the directory service, create a downloads directory and returns an
+ * nsIFile for the downloads directory.
+ *
+ * @return {Promise}
+ * @resolves The directory string path.
+ */
+ _createDownloadsDirectory(aName) {
+ // We read the name of the directory from the list of translated strings
+ // that is kept by the UI helper module, even if this string is not strictly
+ // displayed in the user interface.
+ let directoryPath = OS.Path.join(this._getDirectory(aName),
+ DownloadUIHelper.strings.downloadsFolder);
+
+ // Create the Downloads folder and ignore if it already exists.
+ return OS.File.makeDir(directoryPath, { ignoreExisting: true })
+ .then(() => directoryPath);
+ },
+
+ /**
+ * Returns the string path for the given directory service location name. This
+ * can be overridden by regression tests to return the path of the system
+ * temporary directory in all cases.
+ */
+ _getDirectory(name) {
+ return Services.dirsvc.get(name, Ci.nsIFile).path;
+ },
+
+ /**
+ * Register the downloads interruption observers.
+ *
+ * @param aList
+ * The public or private downloads list.
+ * @param aIsPrivate
+ * True if the list is private, false otherwise.
+ *
+ * @return {Promise}
+ * @resolves When the views and observers are added.
+ */
+ addListObservers(aList, aIsPrivate) {
+ DownloadObserver.registerView(aList, aIsPrivate);
+ if (!DownloadObserver.observersAdded) {
+ DownloadObserver.observersAdded = true;
+ for (let topic of kObserverTopics) {
+ Services.obs.addObserver(DownloadObserver, topic, false);
+ }
+ }
+ return Promise.resolve();
+ },
+
+ /**
+ * Force a save on _store if it exists. Used to ensure downloads do not
+ * persist after being sanitized on Android.
+ *
+ * @return {Promise}
+ * @resolves When _store.save() completes.
+ */
+ forceSave() {
+ if (this._store) {
+ return this._store.save();
+ }
+ return Promise.resolve();
+ },
+
+ /**
+ * Checks if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ *
+ * @return boolean True if we the previous DB was imported.
+ */
+ get _importedFromSqlite() {
+ try {
+ return Services.prefs.getBoolPref(kPrefImportedFromSqlite);
+ } catch (ex) {
+ return false;
+ }
+ },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadObserver
+
+this.DownloadObserver = {
+ /**
+ * Flag to determine if the observers have been added previously.
+ */
+ observersAdded: false,
+
+ /**
+ * Timer used to delay restarting canceled downloads upon waking and returning
+ * online.
+ */
+ _wakeTimer: null,
+
+ /**
+ * Set that contains the in progress publics downloads.
+ * It's kept updated when a public download is added, removed or changes its
+ * properties.
+ */
+ _publicInProgressDownloads: new Set(),
+
+ /**
+ * Set that contains the in progress private downloads.
+ * It's kept updated when a private download is added, removed or changes its
+ * properties.
+ */
+ _privateInProgressDownloads: new Set(),
+
+ /**
+ * Set that contains the downloads that have been canceled when going offline
+ * or to sleep. These are started again when returning online or waking. This
+ * list is not persisted so when exiting and restarting, the downloads will not
+ * be started again.
+ */
+ _canceledOfflineDownloads: new Set(),
+
+ /**
+ * Registers a view that updates the corresponding downloads state set, based
+ * on the aIsPrivate argument. The set is updated when a download is added,
+ * removed or changes its properties.
+ *
+ * @param aList
+ * The public or private downloads list.
+ * @param aIsPrivate
+ * True if the list is private, false otherwise.
+ */
+ registerView: function DO_registerView(aList, aIsPrivate) {
+ let downloadsSet = aIsPrivate ? this._privateInProgressDownloads
+ : this._publicInProgressDownloads;
+ let downloadsView = {
+ onDownloadAdded: aDownload => {
+ if (!aDownload.stopped) {
+ downloadsSet.add(aDownload);
+ }
+ },
+ onDownloadChanged: aDownload => {
+ if (aDownload.stopped) {
+ downloadsSet.delete(aDownload);
+ } else {
+ downloadsSet.add(aDownload);
+ }
+ },
+ onDownloadRemoved: aDownload => {
+ downloadsSet.delete(aDownload);
+ // The download must also be removed from the canceled when offline set.
+ this._canceledOfflineDownloads.delete(aDownload);
+ }
+ };
+
+ // We register the view asynchronously.
+ aList.addView(downloadsView).then(null, Cu.reportError);
+ },
+
+ /**
+ * Wrapper that handles the test mode before calling the prompt that display
+ * a warning message box that informs that there are active downloads,
+ * and asks whether the user wants to cancel them or not.
+ *
+ * @param aCancel
+ * The observer notification subject.
+ * @param aDownloadsCount
+ * The current downloads count.
+ * @param aPrompter
+ * The prompter object that shows the confirm dialog.
+ * @param aPromptType
+ * The type of prompt notification depending on the observer.
+ */
+ _confirmCancelDownloads: function DO_confirmCancelDownload(
+ aCancel, aDownloadsCount, aPrompter, aPromptType) {
+ // If user has already dismissed the request, then do nothing.
+ if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
+ return;
+ }
+ // Handle test mode
+ if (gCombinedDownloadIntegration._testPromptDownloads) {
+ gCombinedDownloadIntegration._testPromptDownloads = aDownloadsCount;
+ return;
+ }
+
+ aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType);
+ },
+
+ /**
+ * Resume all downloads that were paused when going offline, used when waking
+ * from sleep or returning from being offline.
+ */
+ _resumeOfflineDownloads: function DO_resumeOfflineDownloads() {
+ this._wakeTimer = null;
+
+ for (let download of this._canceledOfflineDownloads) {
+ download.start().catch(() => {});
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+
+ observe: function DO_observe(aSubject, aTopic, aData) {
+ let downloadsCount;
+ let p = DownloadUIHelper.getPrompter();
+ switch (aTopic) {
+ case "quit-application-requested":
+ downloadsCount = this._publicInProgressDownloads.size +
+ this._privateInProgressDownloads.size;
+ this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_QUIT);
+ break;
+ case "offline-requested":
+ downloadsCount = this._publicInProgressDownloads.size +
+ this._privateInProgressDownloads.size;
+ this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE);
+ break;
+ case "last-pb-context-exiting":
+ downloadsCount = this._privateInProgressDownloads.size;
+ this._confirmCancelDownloads(aSubject, downloadsCount, p,
+ p.ON_LEAVE_PRIVATE_BROWSING);
+ break;
+ case "last-pb-context-exited":
+ let promise = Task.spawn(function() {
+ let list = yield Downloads.getList(Downloads.PRIVATE);
+ let downloads = yield list.getAll();
+
+ // We can remove the downloads and finalize them in parallel.
+ for (let download of downloads) {
+ list.remove(download).then(null, Cu.reportError);
+ download.finalize(true).then(null, Cu.reportError);
+ }
+ });
+ // Handle test mode
+ if (gCombinedDownloadIntegration._testResolveClearPrivateList) {
+ gCombinedDownloadIntegration._testResolveClearPrivateList(promise);
+ } else {
+ promise.catch(ex => Cu.reportError(ex));
+ }
+ break;
+ case "sleep_notification":
+ case "suspend_process_notification":
+ case "network:offline-about-to-go-offline":
+ for (let download of this._publicInProgressDownloads) {
+ download.cancel();
+ this._canceledOfflineDownloads.add(download);
+ }
+ for (let download of this._privateInProgressDownloads) {
+ download.cancel();
+ this._canceledOfflineDownloads.add(download);
+ }
+ break;
+ case "wake_notification":
+ case "resume_process_notification":
+ let wakeDelay = 10000;
+ try {
+ wakeDelay = Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay");
+ } catch(e) {}
+
+ if (wakeDelay >= 0) {
+ this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ break;
+ case "network:offline-status-changed":
+ if (aData == "online") {
+ this._resumeOfflineDownloads();
+ }
+ break;
+ // We need to unregister observers explicitly before we reach the
+ // "xpcom-shutdown" phase, otherwise observers may be notified when some
+ // required services are not available anymore. We can't unregister
+ // observers on "quit-application", because this module is also loaded
+ // during "make package" automation, and the quit notification is not sent
+ // in that execution environment (bug 973637).
+ case "xpcom-will-shutdown":
+ for (let topic of kObserverTopics) {
+ Services.obs.removeObserver(this, topic);
+ }
+ break;
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadHistoryObserver
+
+#ifdef MOZ_PLACES
+/**
+ * Registers a Places observer so that operations on download history are
+ * reflected on the provided list of downloads.
+ *
+ * You do not need to keep a reference to this object in order to keep it alive,
+ * because the history service already keeps a strong reference to it.
+ *
+ * @param aList
+ * DownloadList object linked to this observer.
+ */
+this.DownloadHistoryObserver = function (aList)
+{
+ this._list = aList;
+ PlacesUtils.history.addObserver(this, false);
+}
+
+this.DownloadHistoryObserver.prototype = {
+ /**
+ * DownloadList object linked to this observer.
+ */
+ _list: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsINavHistoryObserver
+
+ onDeleteURI: function DL_onDeleteURI(aURI, aGUID) {
+ this._list.removeFinished(download => aURI.equals(NetUtil.newURI(
+ download.source.url)));
+ },
+
+ onClearHistory: function DL_onClearHistory() {
+ this._list.removeFinished();
+ },
+
+ onTitleChanged: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+};
+#else
+/**
+ * Empty implementation when we have no Places support, for example on B2G.
+ */
+this.DownloadHistoryObserver = function (aList) {}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadAutoSaveView
+
+/**
+ * This view can be added to a DownloadList object to trigger a save operation
+ * in the given DownloadStore object when a relevant change occurs. You should
+ * call the "initialize" method in order to register the view and load the
+ * current state from disk.
+ *
+ * You do not need to keep a reference to this object in order to keep it alive,
+ * because the DownloadList object already keeps a strong reference to it.
+ *
+ * @param aList
+ * The DownloadList object on which the view should be registered.
+ * @param aStore
+ * The DownloadStore object used for saving.
+ */
+this.DownloadAutoSaveView = function (aList, aStore)
+{
+ this._list = aList;
+ this._store = aStore;
+ this._downloadsMap = new Map();
+ this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs);
+ AsyncShutdown.profileBeforeChange.addBlocker("DownloadAutoSaveView: writing data",
+ () => this._writer.finalize());
+}
+
+this.DownloadAutoSaveView.prototype = {
+ /**
+ * DownloadList object linked to this view.
+ */
+ _list: null,
+
+ /**
+ * The DownloadStore object used for saving.
+ */
+ _store: null,
+
+ /**
+ * True when the initial state of the downloads has been loaded.
+ */
+ _initialized: false,
+
+ /**
+ * Registers the view and loads the current state from disk.
+ *
+ * @return {Promise}
+ * @resolves When the view has been registered.
+ * @rejects JavaScript exception.
+ */
+ initialize: function ()
+ {
+ // We set _initialized to true after adding the view, so that
+ // onDownloadAdded doesn't cause a save to occur.
+ return this._list.addView(this).then(() => this._initialized = true);
+ },
+
+ /**
+ * This map contains only Download objects that should be saved to disk, and
+ * associates them with the result of their getSerializationHash function, for
+ * the purpose of detecting changes to the relevant properties.
+ */
+ _downloadsMap: null,
+
+ /**
+ * DeferredTask for the save operation.
+ */
+ _writer: null,
+
+ /**
+ * Called when the list of downloads changed, this triggers the asynchronous
+ * serialization of the list of downloads.
+ */
+ saveSoon: function ()
+ {
+ this._writer.arm();
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ if (gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
+ this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
+ if (this._initialized) {
+ this.saveSoon();
+ }
+ }
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ if (!gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
+ if (this._downloadsMap.has(aDownload)) {
+ this._downloadsMap.delete(aDownload);
+ this.saveSoon();
+ }
+ return;
+ }
+
+ let hash = aDownload.getSerializationHash();
+ if (this._downloadsMap.get(aDownload) != hash) {
+ this._downloadsMap.set(aDownload, hash);
+ this.saveSoon();
+ }
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ if (this._downloadsMap.has(aDownload)) {
+ this._downloadsMap.delete(aDownload);
+ this.saveSoon();
+ }
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadLegacy.js b/toolkit/components/jsdownloads/src/DownloadLegacy.js
new file mode 100644
index 0000000000..fc9fb35d22
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -0,0 +1,309 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 implements the XPCOM interfaces required for integration with
+ * the legacy download components.
+ *
+ * New code is expected to use the "Downloads.jsm" module directly, without
+ * going through the interfaces implemented in this XPCOM component. These
+ * interfaces are only maintained for backwards compatibility with components
+ * that still work synchronously on the main thread.
+ */
+
+"use strict";
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+// DownloadLegacyTransfer
+
+/**
+ * nsITransfer implementation that provides a bridge to a Download object.
+ *
+ * Legacy downloads work differently than the JavaScript implementation. In the
+ * latter, the caller only provides the properties for the Download object and
+ * the entire process is handled by the "start" method. In the legacy
+ * implementation, the caller must create a separate object to execute the
+ * download, and then make the download visible to the user by hooking it up to
+ * an nsITransfer instance.
+ *
+ * Since nsITransfer instances may be created before the download system is
+ * initialized, and initialization as well as other operations are asynchronous,
+ * this implementation is able to delay all progress and status notifications it
+ * receives until the associated Download object is finally created.
+ *
+ * Conversely, the DownloadLegacySaver object can also receive execution and
+ * cancellation requests asynchronously, before or after it is connected to
+ * this nsITransfer instance. For that reason, those requests are communicated
+ * in a potentially deferred way, using promise objects.
+ *
+ * The component that executes the download implements nsICancelable to receive
+ * cancellation requests, but after cancellation it cannot be reused again.
+ *
+ * Since the components that execute the download may be different and they
+ * don't always give consistent results, this bridge takes care of enforcing the
+ * expectations, for example by ensuring the target file exists when the
+ * download is successful, even if the source has a size of zero bytes.
+ */
+function DownloadLegacyTransfer()
+{
+ this._deferDownload = Promise.defer();
+}
+
+DownloadLegacyTransfer.prototype = {
+ classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsIWebProgressListener2,
+ Ci.nsITransfer]),
+
+ // nsIWebProgressListener
+
+ onStateChange: function DLT_onStateChange(aWebProgress, aRequest, aStateFlags,
+ aStatus)
+ {
+ if (!Components.isSuccessCode(aStatus)) {
+ this._componentFailed = true;
+ }
+
+ if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
+ (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+
+ let blockedByParentalControls = false;
+ // If it is a failed download, aRequest.responseStatus doesn't exist.
+ // (missing file on the server, network failure to download)
+ try {
+ // If the request's response has been blocked by Windows Parental Controls
+ // with an HTTP 450 error code, we must cancel the request synchronously.
+ blockedByParentalControls = aRequest instanceof Ci.nsIHttpChannel &&
+ aRequest.responseStatus == 450;
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+
+ if (blockedByParentalControls) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ }
+
+ // The main request has just started. Wait for the associated Download
+ // object to be available before notifying.
+ this._deferDownload.promise.then(download => {
+ // If the request was blocked, now that we have the download object we
+ // should set a flag that can be retrieved later when handling the
+ // cancellation so that the proper error can be thrown.
+ if (blockedByParentalControls) {
+ download._blockedByParentalControls = true;
+ }
+
+ download.saver.onTransferStarted(
+ aRequest,
+ this._cancelable instanceof Ci.nsIHelperAppLauncher);
+
+ // To handle asynchronous cancellation properly, we should hook up the
+ // handler only after we have been notified that the main request
+ // started. We will wait until the main request stopped before
+ // notifying that the download has been canceled. Since the request has
+ // not completed yet, deferCanceled is guaranteed to be set.
+ return download.saver.deferCanceled.promise.then(() => {
+ // Only cancel if the object executing the download is still running.
+ if (this._cancelable && !this._componentFailed) {
+ this._cancelable.cancel(Cr.NS_ERROR_ABORT);
+ if (this._cancelable instanceof Ci.nsIWebBrowserPersist) {
+ // This component will not send the STATE_STOP notification.
+ download.saver.onTransferFinished(aRequest, Cr.NS_ERROR_ABORT);
+ this._cancelable = null;
+ }
+ }
+ });
+ }).then(null, Cu.reportError);
+ } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
+ (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ // The last file has been received, or the download failed. Wait for the
+ // associated Download object to be available before notifying.
+ this._deferDownload.promise.then(download => {
+ // At this point, the hash has been set and we need to copy it to the
+ // DownloadSaver.
+ if (Components.isSuccessCode(aStatus)) {
+ download.saver.setSha256Hash(this._sha256Hash);
+ download.saver.setSignatureInfo(this._signatureInfo);
+ download.saver.setRedirects(this._redirects);
+ }
+ download.saver.onTransferFinished(aRequest, aStatus);
+ }).then(null, Cu.reportError);
+
+ // Release the reference to the component executing the download.
+ this._cancelable = null;
+ }
+ },
+
+ onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress)
+ {
+ this.onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ },
+
+ onLocationChange: function () { },
+
+ onStatusChange: function DLT_onStatusChange(aWebProgress, aRequest, aStatus,
+ aMessage)
+ {
+ // The status change may optionally be received in addition to the state
+ // change, but if no network request actually started, it is possible that
+ // we only receive a status change with an error status code.
+ if (!Components.isSuccessCode(aStatus)) {
+ this._componentFailed = true;
+
+ // Wait for the associated Download object to be available.
+ this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
+ aDownload.saver.onTransferFinished(aRequest, aStatus);
+ }).then(null, Cu.reportError);
+ }
+ },
+
+ onSecurityChange: function () { },
+
+ // nsIWebProgressListener2
+
+ onProgressChange64: function DLT_onProgressChange64(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress)
+ {
+ // Wait for the associated Download object to be available.
+ this._deferDownload.promise.then(function DLT_OPC64_onDownload(aDownload) {
+ aDownload.saver.onProgressBytes(aCurTotalProgress, aMaxTotalProgress);
+ }).then(null, Cu.reportError);
+ },
+
+ onRefreshAttempted: function DLT_onRefreshAttempted(aWebProgress, aRefreshURI,
+ aMillis, aSameURI)
+ {
+ // Indicate that refreshes and redirects are allowed by default. However,
+ // note that download components don't usually call this method at all.
+ return true;
+ },
+
+ // nsITransfer
+
+ init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
+ aTempFile, aCancelable, aIsPrivate)
+ {
+ this._cancelable = aCancelable;
+
+ let launchWhenSucceeded = false, contentType = null, launcherPath = null;
+
+ if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
+ launchWhenSucceeded =
+ aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
+ contentType = aMIMEInfo.type;
+
+ let appHandler = aMIMEInfo.preferredApplicationHandler;
+ if (aMIMEInfo.preferredAction == Ci.nsIMIMEInfo.useHelperApp &&
+ appHandler instanceof Ci.nsILocalHandlerApp) {
+ launcherPath = appHandler.executable.path;
+ }
+ }
+
+ // Create a new Download object associated to a DownloadLegacySaver, and
+ // wait for it to be available. This operation may cause the entire
+ // download system to initialize before the object is created.
+ Downloads.createDownload({
+ source: { url: aSource.spec, isPrivate: aIsPrivate },
+ target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
+ partFilePath: aTempFile && aTempFile.path },
+ saver: "legacy",
+ launchWhenSucceeded: launchWhenSucceeded,
+ contentType: contentType,
+ launcherPath: launcherPath
+ }).then(function DLT_I_onDownload(aDownload) {
+ // Legacy components keep partial data when they use a ".part" file.
+ if (aTempFile) {
+ aDownload.tryToKeepPartialData = true;
+ }
+
+ // Start the download before allowing it to be controlled. Ignore errors.
+ aDownload.start().catch(() => {});
+
+ // Start processing all the other events received through nsITransfer.
+ this._deferDownload.resolve(aDownload);
+
+ // Add the download to the list, allowing it to be seen and canceled.
+ return Downloads.getList(Downloads.ALL).then(list => list.add(aDownload));
+ }.bind(this)).then(null, Cu.reportError);
+ },
+
+ setSha256Hash: function (hash)
+ {
+ this._sha256Hash = hash;
+ },
+
+ setSignatureInfo: function (signatureInfo)
+ {
+ this._signatureInfo = signatureInfo;
+ },
+
+ setRedirects: function (redirects)
+ {
+ this._redirects = redirects;
+ },
+
+ // Private methods and properties
+
+ /**
+ * This deferred object contains a promise that is resolved with the Download
+ * object associated with this nsITransfer instance, when it is available.
+ */
+ _deferDownload: null,
+
+ /**
+ * Reference to the component that is executing the download. This component
+ * allows cancellation through its nsICancelable interface.
+ */
+ _cancelable: null,
+
+ /**
+ * Indicates that the component that executes the download has notified a
+ * failure condition. In this case, we should never use the component methods
+ * that cancel the download.
+ */
+ _componentFailed: false,
+
+ /**
+ * Save the SHA-256 hash in raw bytes of the downloaded file.
+ */
+ _sha256Hash: null,
+
+ /**
+ * Save the signature info in a serialized protobuf of the downloaded file.
+ */
+ _signatureInfo: null,
+};
+
+// Module
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadLegacyTransfer]);
diff --git a/toolkit/components/jsdownloads/src/DownloadList.jsm b/toolkit/components/jsdownloads/src/DownloadList.jsm
new file mode 100644
index 0000000000..f725bd3dea
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadList.jsm
@@ -0,0 +1,559 @@
+/* -*- 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/. */
+
+/**
+ * This file includes the following constructors and global objects:
+ *
+ * DownloadList
+ * Represents a collection of Download objects that can be viewed and managed by
+ * the user interface, and persisted across sessions.
+ *
+ * DownloadCombinedList
+ * Provides a unified, unordered list combining public and private downloads.
+ *
+ * DownloadSummary
+ * Provides an aggregated view on the contents of a DownloadList.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadList",
+ "DownloadCombinedList",
+ "DownloadSummary",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+// DownloadList
+
+/**
+ * Represents a collection of Download objects that can be viewed and managed by
+ * the user interface, and persisted across sessions.
+ */
+this.DownloadList = function ()
+{
+ this._downloads = [];
+ this._views = new Set();
+}
+
+this.DownloadList.prototype = {
+ /**
+ * Array of Download objects currently in the list.
+ */
+ _downloads: null,
+
+ /**
+ * Retrieves a snapshot of the downloads that are currently in the list. The
+ * returned array does not change when downloads are added or removed, though
+ * the Download objects it contains are still updated in real time.
+ *
+ * @return {Promise}
+ * @resolves An array of Download objects.
+ * @rejects JavaScript exception.
+ */
+ getAll: function DL_getAll() {
+ return Promise.resolve(Array.slice(this._downloads, 0));
+ },
+
+ /**
+ * Adds a new download to the end of the items list.
+ *
+ * @note When a download is added to the list, its "onchange" event is
+ * registered by the list, thus it cannot be used to monitor the
+ * download. To receive change notifications for downloads that are
+ * added to the list, use the addView method to register for
+ * onDownloadChanged notifications.
+ *
+ * @param aDownload
+ * The Download object to add.
+ *
+ * @return {Promise}
+ * @resolves When the download has been added.
+ * @rejects JavaScript exception.
+ */
+ add: function DL_add(aDownload) {
+ this._downloads.push(aDownload);
+ aDownload.onchange = this._change.bind(this, aDownload);
+ this._notifyAllViews("onDownloadAdded", aDownload);
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes a download from the list. If the download was already removed,
+ * this method has no effect.
+ *
+ * This method does not change the state of the download, to allow adding it
+ * to another list, or control it directly. If you want to dispose of the
+ * download object, you should cancel it afterwards, and remove any partially
+ * downloaded data if needed.
+ *
+ * @param aDownload
+ * The Download object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the download has been removed.
+ * @rejects JavaScript exception.
+ */
+ remove: function DL_remove(aDownload) {
+ let index = this._downloads.indexOf(aDownload);
+ if (index != -1) {
+ this._downloads.splice(index, 1);
+ aDownload.onchange = null;
+ this._notifyAllViews("onDownloadRemoved", aDownload);
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * This function is called when "onchange" events of downloads occur.
+ *
+ * @param aDownload
+ * The Download object that changed.
+ */
+ _change: function DL_change(aDownload) {
+ this._notifyAllViews("onDownloadChanged", aDownload);
+ },
+
+ /**
+ * Set of currently registered views.
+ */
+ _views: null,
+
+ /**
+ * Adds a view that will be notified of changes to downloads. The newly added
+ * view will receive onDownloadAdded notifications for all the downloads that
+ * are already in the list.
+ *
+ * @param aView
+ * The view object to add. The following methods may be defined:
+ * {
+ * onDownloadAdded: function (aDownload) {
+ * // Called after aDownload is added to the end of the list.
+ * },
+ * onDownloadChanged: function (aDownload) {
+ * // Called after the properties of aDownload change.
+ * },
+ * onDownloadRemoved: function (aDownload) {
+ * // Called after aDownload is removed from the list.
+ * },
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the view has been registered and all the onDownloadAdded
+ * notifications for the existing downloads have been sent.
+ * @rejects JavaScript exception.
+ */
+ addView: function DL_addView(aView)
+ {
+ this._views.add(aView);
+
+ if ("onDownloadAdded" in aView) {
+ for (let download of this._downloads) {
+ try {
+ aView.onDownloadAdded(download);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes a view that was previously added using addView.
+ *
+ * @param aView
+ * The view object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the view has been removed. At this point, the removed view
+ * will not receive any more notifications.
+ * @rejects JavaScript exception.
+ */
+ removeView: function DL_removeView(aView)
+ {
+ this._views.delete(aView);
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Notifies all the views of a download addition, change, or removal.
+ *
+ * @param aMethodName
+ * String containing the name of the method to call on the view.
+ * @param aDownload
+ * The Download object that changed.
+ */
+ _notifyAllViews: function (aMethodName, aDownload) {
+ for (let view of this._views) {
+ try {
+ if (aMethodName in view) {
+ view[aMethodName](aDownload);
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ /**
+ * Removes downloads from the list that have finished, have failed, or have
+ * been canceled without keeping partial data. A filter function may be
+ * specified to remove only a subset of those downloads.
+ *
+ * This method finalizes each removed download, ensuring that any partially
+ * downloaded data associated with it is also removed.
+ *
+ * @param aFilterFn
+ * The filter function is called with each download as its only
+ * argument, and should return true to remove the download and false
+ * to keep it. This parameter may be null or omitted to have no
+ * additional filter.
+ */
+ removeFinished: function DL_removeFinished(aFilterFn) {
+ Task.spawn(function* () {
+ let list = yield this.getAll();
+ for (let download of list) {
+ // Remove downloads that have been canceled, even if the cancellation
+ // operation hasn't completed yet so we don't check "stopped" here.
+ // Failed downloads with partial data are also removed.
+ if (download.stopped && (!download.hasPartialData || download.error) &&
+ (!aFilterFn || aFilterFn(download))) {
+ // Remove the download first, so that the views don't get the change
+ // notifications that may occur during finalization.
+ yield this.remove(download);
+ // Ensure that the download is stopped and no partial data is kept.
+ // This works even if the download state has changed meanwhile. We
+ // don't need to wait for the procedure to be complete before
+ // processing the other downloads in the list.
+ download.finalize(true).then(null, Cu.reportError);
+ }
+ }
+ }.bind(this)).then(null, Cu.reportError);
+ },
+};
+
+// DownloadCombinedList
+
+/**
+ * Provides a unified, unordered list combining public and private downloads.
+ *
+ * Download objects added to this list are also added to one of the two
+ * underlying lists, based on their "source.isPrivate" property. Views on this
+ * list will receive notifications for both public and private downloads.
+ *
+ * @param aPublicList
+ * Underlying DownloadList containing public downloads.
+ * @param aPrivateList
+ * Underlying DownloadList containing private downloads.
+ */
+this.DownloadCombinedList = function (aPublicList, aPrivateList)
+{
+ DownloadList.call(this);
+ this._publicList = aPublicList;
+ this._privateList = aPrivateList;
+ aPublicList.addView(this).then(null, Cu.reportError);
+ aPrivateList.addView(this).then(null, Cu.reportError);
+}
+
+this.DownloadCombinedList.prototype = {
+ __proto__: DownloadList.prototype,
+
+ /**
+ * Underlying DownloadList containing public downloads.
+ */
+ _publicList: null,
+
+ /**
+ * Underlying DownloadList containing private downloads.
+ */
+ _privateList: null,
+
+ /**
+ * Adds a new download to the end of the items list.
+ *
+ * @note When a download is added to the list, its "onchange" event is
+ * registered by the list, thus it cannot be used to monitor the
+ * download. To receive change notifications for downloads that are
+ * added to the list, use the addView method to register for
+ * onDownloadChanged notifications.
+ *
+ * @param aDownload
+ * The Download object to add.
+ *
+ * @return {Promise}
+ * @resolves When the download has been added.
+ * @rejects JavaScript exception.
+ */
+ add: function (aDownload)
+ {
+ if (aDownload.source.isPrivate) {
+ return this._privateList.add(aDownload);
+ }
+ return this._publicList.add(aDownload);
+ },
+
+ /**
+ * Removes a download from the list. If the download was already removed,
+ * this method has no effect.
+ *
+ * This method does not change the state of the download, to allow adding it
+ * to another list, or control it directly. If you want to dispose of the
+ * download object, you should cancel it afterwards, and remove any partially
+ * downloaded data if needed.
+ *
+ * @param aDownload
+ * The Download object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the download has been removed.
+ * @rejects JavaScript exception.
+ */
+ remove: function (aDownload)
+ {
+ if (aDownload.source.isPrivate) {
+ return this._privateList.remove(aDownload);
+ }
+ return this._publicList.remove(aDownload);
+ },
+
+ // DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ this._downloads.push(aDownload);
+ this._notifyAllViews("onDownloadAdded", aDownload);
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ this._notifyAllViews("onDownloadChanged", aDownload);
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ let index = this._downloads.indexOf(aDownload);
+ if (index != -1) {
+ this._downloads.splice(index, 1);
+ }
+ this._notifyAllViews("onDownloadRemoved", aDownload);
+ },
+};
+
+// DownloadSummary
+
+/**
+ * Provides an aggregated view on the contents of a DownloadList.
+ */
+this.DownloadSummary = function ()
+{
+ this._downloads = [];
+ this._views = new Set();
+}
+
+this.DownloadSummary.prototype = {
+ /**
+ * Array of Download objects that are currently part of the summary.
+ */
+ _downloads: null,
+
+ /**
+ * Underlying DownloadList whose contents should be summarized.
+ */
+ _list: null,
+
+ /**
+ * This method may be called once to bind this object to a DownloadList.
+ *
+ * Views on the summarized data can be registered before this object is bound
+ * to an actual list. This allows the summary to be used without requiring
+ * the initialization of the DownloadList first.
+ *
+ * @param aList
+ * Underlying DownloadList whose contents should be summarized.
+ *
+ * @return {Promise}
+ * @resolves When the view on the underlying list has been registered.
+ * @rejects JavaScript exception.
+ */
+ bindToList: function (aList)
+ {
+ if (this._list) {
+ throw new Error("bindToList may be called only once.");
+ }
+
+ return aList.addView(this).then(() => {
+ // Set the list reference only after addView has returned, so that we don't
+ // send a notification to our views for each download that is added.
+ this._list = aList;
+ this._onListChanged();
+ });
+ },
+
+ /**
+ * Set of currently registered views.
+ */
+ _views: null,
+
+ /**
+ * Adds a view that will be notified of changes to the summary. The newly
+ * added view will receive an initial onSummaryChanged notification.
+ *
+ * @param aView
+ * The view object to add. The following methods may be defined:
+ * {
+ * onSummaryChanged: function () {
+ * // Called after any property of the summary has changed.
+ * },
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the view has been registered and the onSummaryChanged
+ * notification has been sent.
+ * @rejects JavaScript exception.
+ */
+ addView: function (aView)
+ {
+ this._views.add(aView);
+
+ if ("onSummaryChanged" in aView) {
+ try {
+ aView.onSummaryChanged();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes a view that was previously added using addView.
+ *
+ * @param aView
+ * The view object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the view has been removed. At this point, the removed view
+ * will not receive any more notifications.
+ * @rejects JavaScript exception.
+ */
+ removeView: function (aView)
+ {
+ this._views.delete(aView);
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Indicates whether all the downloads are currently stopped.
+ */
+ allHaveStopped: true,
+
+ /**
+ * Indicates the total number of bytes to be transferred before completing all
+ * the downloads that are currently in progress.
+ *
+ * For downloads that do not have a known final size, the number of bytes
+ * currently transferred is reported as part of this property.
+ *
+ * This is zero if no downloads are currently in progress.
+ */
+ progressTotalBytes: 0,
+
+ /**
+ * Number of bytes currently transferred as part of all the downloads that are
+ * currently in progress.
+ *
+ * This is zero if no downloads are currently in progress.
+ */
+ progressCurrentBytes: 0,
+
+ /**
+ * This function is called when any change in the list of downloads occurs,
+ * and will recalculate the summary and notify the views in case the
+ * aggregated properties are different.
+ */
+ _onListChanged: function () {
+ let allHaveStopped = true;
+ let progressTotalBytes = 0;
+ let progressCurrentBytes = 0;
+
+ // Recalculate the aggregated state. See the description of the individual
+ // properties for an explanation of the summarization logic.
+ for (let download of this._downloads) {
+ if (!download.stopped) {
+ allHaveStopped = false;
+ progressTotalBytes += download.hasProgress ? download.totalBytes
+ : download.currentBytes;
+ progressCurrentBytes += download.currentBytes;
+ }
+ }
+
+ // Exit now if the properties did not change.
+ if (this.allHaveStopped == allHaveStopped &&
+ this.progressTotalBytes == progressTotalBytes &&
+ this.progressCurrentBytes == progressCurrentBytes) {
+ return;
+ }
+
+ this.allHaveStopped = allHaveStopped;
+ this.progressTotalBytes = progressTotalBytes;
+ this.progressCurrentBytes = progressCurrentBytes;
+
+ // Notify all the views that our properties changed.
+ for (let view of this._views) {
+ try {
+ if ("onSummaryChanged" in view) {
+ view.onSummaryChanged();
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ // DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ this._downloads.push(aDownload);
+ if (this._list) {
+ this._onListChanged();
+ }
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ this._onListChanged();
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ let index = this._downloads.indexOf(aDownload);
+ if (index != -1) {
+ this._downloads.splice(index, 1);
+ }
+ this._onListChanged();
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadPlatform.cpp b/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
new file mode 100644
index 0000000000..1506b7c30c
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
@@ -0,0 +1,275 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "DownloadPlatform.h"
+#include "nsAutoPtr.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsINestedURI.h"
+#include "nsIProtocolHandler.h"
+#include "nsIURI.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsDirectoryServiceDefs.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+
+#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs"
+
+#ifdef XP_WIN
+#include <shlobj.h>
+#include <urlmon.h>
+#include "nsILocalFileWin.h"
+#endif
+
+#ifdef XP_MACOSX
+#include <CoreFoundation/CoreFoundation.h>
+#include "../../../../xpcom/io/CocoaFileUtils.h"
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "FennecJNIWrappers.h"
+#endif
+
+#ifdef MOZ_WIDGET_GTK
+#include <gtk/gtk.h>
+#endif
+
+using namespace mozilla;
+
+DownloadPlatform *DownloadPlatform::gDownloadPlatformService = nullptr;
+
+NS_IMPL_ISUPPORTS(DownloadPlatform, mozIDownloadPlatform);
+
+DownloadPlatform* DownloadPlatform::GetDownloadPlatform()
+{
+ if (!gDownloadPlatformService) {
+ gDownloadPlatformService = new DownloadPlatform();
+ }
+
+ NS_ADDREF(gDownloadPlatformService);
+
+#if defined(MOZ_WIDGET_GTK)
+ g_type_init();
+#endif
+
+ return gDownloadPlatformService;
+}
+
+#ifdef MOZ_ENABLE_GIO
+static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data)
+{
+ GError *err = nullptr;
+ g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err);
+ if (err) {
+#ifdef DEBUG
+ NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__);
+#endif
+ g_error_free(err);
+ }
+}
+#endif
+
+#ifdef XP_MACOSX
+// Caller is responsible for freeing any result (CF Create Rule)
+CFURLRef CreateCFURLFromNSIURI(nsIURI *aURI) {
+ nsAutoCString spec;
+ if (aURI) {
+ aURI->GetSpec(spec);
+ }
+
+ CFStringRef urlStr = ::CFStringCreateWithCString(kCFAllocatorDefault,
+ spec.get(),
+ kCFStringEncodingUTF8);
+ if (!urlStr) {
+ return NULL;
+ }
+
+ CFURLRef url = ::CFURLCreateWithString(kCFAllocatorDefault,
+ urlStr,
+ NULL);
+
+ ::CFRelease(urlStr);
+
+ return url;
+}
+#endif
+
+nsresult DownloadPlatform::DownloadDone(nsIURI* aSource, nsIURI* aReferrer, nsIFile* aTarget,
+ const nsACString& aContentType, bool aIsPrivate)
+{
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) \
+ || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_GONK)
+
+ nsAutoString path;
+ if (aTarget && NS_SUCCEEDED(aTarget->GetPath(path))) {
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID)
+ // On Windows and Gtk, add the download to the system's "recent documents"
+ // list, with a pref to disable.
+ {
+ bool addToRecentDocs = Preferences::GetBool(PREF_BDM_ADDTORECENTDOCS);
+#ifdef MOZ_WIDGET_ANDROID
+ if (jni::IsFennec() && addToRecentDocs) {
+ java::DownloadsIntegration::ScanMedia(path, aContentType);
+ }
+#else
+ if (addToRecentDocs && !aIsPrivate) {
+#ifdef XP_WIN
+ ::SHAddToRecentDocs(SHARD_PATHW, path.get());
+#elif defined(MOZ_WIDGET_GTK)
+ GtkRecentManager* manager = gtk_recent_manager_get_default();
+
+ gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(),
+ nullptr, nullptr);
+ if (uri) {
+ gtk_recent_manager_add_item(manager, uri);
+ g_free(uri);
+ }
+#endif
+ }
+#endif
+#ifdef MOZ_ENABLE_GIO
+ // Use GIO to store the source URI for later display in the file manager.
+ GFile* gio_file = g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get());
+ nsCString source_uri;
+ nsresult rv = aSource->GetSpec(source_uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ GFileInfo *file_info = g_file_info_new();
+ g_file_info_set_attribute_string(file_info, "metadata::download-uri", source_uri.get());
+ g_file_set_attributes_async(gio_file,
+ file_info,
+ G_FILE_QUERY_INFO_NONE,
+ G_PRIORITY_DEFAULT,
+ nullptr, gio_set_metadata_done, nullptr);
+ g_object_unref(file_info);
+ g_object_unref(gio_file);
+#endif
+ }
+#endif
+
+#ifdef XP_MACOSX
+ // On OS X, make the downloads stack bounce.
+ CFStringRef observedObject = ::CFStringCreateWithCString(kCFAllocatorDefault,
+ NS_ConvertUTF16toUTF8(path).get(),
+ kCFStringEncodingUTF8);
+ CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
+ ::CFNotificationCenterPostNotification(center, CFSTR("com.apple.DownloadFileFinished"),
+ observedObject, nullptr, TRUE);
+ ::CFRelease(observedObject);
+
+ // Add OS X origin and referrer file metadata
+ CFStringRef pathCFStr = NULL;
+ if (!path.IsEmpty()) {
+ pathCFStr = ::CFStringCreateWithCharacters(kCFAllocatorDefault,
+ (const UniChar*)path.get(),
+ path.Length());
+ }
+ if (pathCFStr) {
+ bool isFromWeb = IsURLPossiblyFromWeb(aSource);
+
+ CFURLRef sourceCFURL = CreateCFURLFromNSIURI(aSource);
+ CFURLRef referrerCFURL = CreateCFURLFromNSIURI(aReferrer);
+
+ CocoaFileUtils::AddOriginMetadataToFile(pathCFStr,
+ sourceCFURL,
+ referrerCFURL);
+ CocoaFileUtils::AddQuarantineMetadataToFile(pathCFStr,
+ sourceCFURL,
+ referrerCFURL,
+ isFromWeb);
+
+ ::CFRelease(pathCFStr);
+ if (sourceCFURL) {
+ ::CFRelease(sourceCFURL);
+ }
+ if (referrerCFURL) {
+ ::CFRelease(referrerCFURL);
+ }
+ }
+#endif
+ }
+
+#endif
+
+ return NS_OK;
+}
+
+nsresult DownloadPlatform::MapUrlToZone(const nsAString& aURL,
+ uint32_t* aZone)
+{
+#ifdef XP_WIN
+ RefPtr<IInternetSecurityManager> inetSecMgr;
+ if (FAILED(CoCreateInstance(CLSID_InternetSecurityManager, NULL,
+ CLSCTX_ALL, IID_IInternetSecurityManager,
+ getter_AddRefs(inetSecMgr)))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ DWORD zone;
+ if (inetSecMgr->MapUrlToZone(PromiseFlatString(aURL).get(),
+ &zone, 0) != S_OK) {
+ return NS_ERROR_UNEXPECTED;
+ } else {
+ *aZone = zone;
+ }
+
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+// Check if a URI is likely to be web-based, by checking its URI flags.
+// If in doubt (e.g. if anything fails during the check) claims things
+// are from the web.
+bool DownloadPlatform::IsURLPossiblyFromWeb(nsIURI* aURI)
+{
+ nsCOMPtr<nsIIOService> ios = do_GetIOService();
+ nsCOMPtr<nsIURI> uri = aURI;
+ if (!ios) {
+ return true;
+ }
+
+ while (uri) {
+ // We're not using nsIIOService::ProtocolHasFlags because it doesn't
+ // take per-URI flags into account. We're also not using
+ // NS_URIChainHasFlags because we're checking for *any* of 3 flags
+ // to be present on *all* of the nested URIs, which it can't do.
+ nsAutoCString scheme;
+ nsresult rv = uri->GetScheme(scheme);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ nsCOMPtr<nsIProtocolHandler> ph;
+ rv = ios->GetProtocolHandler(scheme.get(), getter_AddRefs(ph));
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ uint32_t flags;
+ rv = ph->DoGetProtocolFlags(uri, &flags);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ // If not dangerous to load, not a UI resource and not a local file,
+ // assume this is from the web:
+ if (!(flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) &&
+ !(flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) &&
+ !(flags & nsIProtocolHandler::URI_IS_LOCAL_FILE)) {
+ return true;
+ }
+ // Otherwise, check if the URI is nested, and if so go through
+ // the loop again:
+ nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
+ uri = nullptr;
+ if (nestedURI) {
+ rv = nestedURI->GetInnerURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/toolkit/components/jsdownloads/src/DownloadPlatform.h b/toolkit/components/jsdownloads/src/DownloadPlatform.h
new file mode 100644
index 0000000000..ef3c7554ff
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadPlatform.h
@@ -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/. */
+
+#ifndef __DownloadPlatform_h__
+#define __DownloadPlatform_h__
+
+#include "mozIDownloadPlatform.h"
+
+#include "nsCOMPtr.h"
+class nsIURI;
+
+class DownloadPlatform : public mozIDownloadPlatform
+{
+protected:
+
+ virtual ~DownloadPlatform() { }
+
+public:
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIDOWNLOADPLATFORM
+
+ DownloadPlatform() { }
+
+ static DownloadPlatform *gDownloadPlatformService;
+
+ static DownloadPlatform* GetDownloadPlatform();
+
+private:
+ static bool IsURLPossiblyFromWeb(nsIURI* aURI);
+};
+
+#endif
diff --git a/toolkit/components/jsdownloads/src/DownloadStore.jsm b/toolkit/components/jsdownloads/src/DownloadStore.jsm
new file mode 100644
index 0000000000..765a45c5a0
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -0,0 +1,203 @@
+/* -*- 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/. */
+
+/**
+ * Handles serialization of Download objects and persistence into a file, so
+ * that the state of downloads can be restored across sessions.
+ *
+ * The file is stored in JSON format, without indentation. With indentation
+ * applied, the file would look like this:
+ *
+ * {
+ * "list": [
+ * {
+ * "source": "http://www.example.com/download.txt",
+ * "target": "/home/user/Downloads/download.txt"
+ * },
+ * {
+ * "source": {
+ * "url": "http://www.example.com/download.txt",
+ * "referrer": "http://www.example.com/referrer.html"
+ * },
+ * "target": "/home/user/Downloads/download-2.txt"
+ * }
+ * ]
+ * }
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadStore",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
+ return new TextDecoder();
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
+ return new TextEncoder();
+});
+
+// DownloadStore
+
+/**
+ * Handles serialization of Download objects and persistence into a file, so
+ * that the state of downloads can be restored across sessions.
+ *
+ * @param aList
+ * DownloadList object to be populated or serialized.
+ * @param aPath
+ * String containing the file path where data should be saved.
+ */
+this.DownloadStore = function (aList, aPath)
+{
+ this.list = aList;
+ this.path = aPath;
+}
+
+this.DownloadStore.prototype = {
+ /**
+ * DownloadList object to be populated or serialized.
+ */
+ list: null,
+
+ /**
+ * String containing the file path where data should be saved.
+ */
+ path: "",
+
+ /**
+ * This function is called with a Download object as its first argument, and
+ * should return true if the item should be saved.
+ */
+ onsaveitem: () => true,
+
+ /**
+ * Loads persistent downloads from the file to the list.
+ *
+ * @return {Promise}
+ * @resolves When the operation finished successfully.
+ * @rejects JavaScript exception.
+ */
+ load: function DS_load()
+ {
+ return Task.spawn(function* task_DS_load() {
+ let bytes;
+ try {
+ bytes = yield OS.File.read(this.path);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ // If the file does not exist, there are no downloads to load.
+ return;
+ }
+
+ let storeData = JSON.parse(gTextDecoder.decode(bytes));
+
+ // Create live downloads based on the static snapshot.
+ for (let downloadData of storeData.list) {
+ try {
+ let download = yield Downloads.createDownload(downloadData);
+ try {
+ if (!download.succeeded && !download.canceled && !download.error) {
+ // Try to restart the download if it was in progress during the
+ // previous session. Ignore errors.
+ download.start().catch(() => {});
+ } else {
+ // If the download was not in progress, try to update the current
+ // progress from disk. This is relevant in case we retained
+ // partially downloaded data.
+ yield download.refresh();
+ }
+ } finally {
+ // Add the download to the list if we succeeded in creating it,
+ // after we have updated its initial state.
+ yield this.list.add(download);
+ }
+ } catch (ex) {
+ // If an item is unrecognized, don't prevent others from being loaded.
+ Cu.reportError(ex);
+ }
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Saves persistent downloads from the list to the file.
+ *
+ * If an error occurs, the previous file is not deleted.
+ *
+ * @return {Promise}
+ * @resolves When the operation finished successfully.
+ * @rejects JavaScript exception.
+ */
+ save: function DS_save()
+ {
+ return Task.spawn(function* task_DS_save() {
+ let downloads = yield this.list.getAll();
+
+ // Take a static snapshot of the current state of all the downloads.
+ let storeData = { list: [] };
+ let atLeastOneDownload = false;
+ for (let download of downloads) {
+ try {
+ if (!this.onsaveitem(download)) {
+ continue;
+ }
+
+ let serializable = download.toSerializable();
+ if (!serializable) {
+ // This item cannot be persisted across sessions.
+ continue;
+ }
+ storeData.list.push(serializable);
+ atLeastOneDownload = true;
+ } catch (ex) {
+ // If an item cannot be converted to a serializable form, don't
+ // prevent others from being saved.
+ Cu.reportError(ex);
+ }
+ }
+
+ if (atLeastOneDownload) {
+ // Create or overwrite the file if there are downloads to save.
+ let bytes = gTextEncoder.encode(JSON.stringify(storeData));
+ yield OS.File.writeAtomic(this.path, bytes,
+ { tmpPath: this.path + ".tmp" });
+ } else {
+ // Remove the file if there are no downloads to save at all.
+ try {
+ yield OS.File.remove(this.path);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) ||
+ !(ex.becauseNoSuchFile || ex.becauseAccessDenied)) {
+ throw ex;
+ }
+ // On Windows, we may get an access denied error instead of a no such
+ // file error if the file existed before, and was recently deleted.
+ }
+ }
+ }.bind(this));
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
new file mode 100644
index 0000000000..f5102b4a89
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
@@ -0,0 +1,243 @@
+/* -*- 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/. */
+
+/**
+ * Provides functions to handle status and messages in the user interface.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadUIHelper",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+const kStringBundleUrl =
+ "chrome://mozapps/locale/downloads/downloads.properties";
+
+const kStringsRequiringFormatting = {
+ fileExecutableSecurityWarning: true,
+ cancelDownloadsOKTextMultiple: true,
+ quitCancelDownloadsAlertMsgMultiple: true,
+ quitCancelDownloadsAlertMsgMacMultiple: true,
+ offlineCancelDownloadsAlertMsgMultiple: true,
+ leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2: true
+};
+
+// DownloadUIHelper
+
+/**
+ * Provides functions to handle status and messages in the user interface.
+ */
+this.DownloadUIHelper = {
+ /**
+ * Returns an object that can be used to display prompts related to downloads.
+ *
+ * The prompts may be either anchored to a specified window, or anchored to
+ * the most recently active window, for example if the prompt is displayed in
+ * response to global notifications that are not associated with any window.
+ *
+ * @param aParent
+ * If specified, should reference the nsIDOMWindow to which the prompts
+ * should be attached. If omitted, the prompts will be attached to the
+ * most recently active window.
+ *
+ * @return A DownloadPrompter object.
+ */
+ getPrompter: function (aParent)
+ {
+ return new DownloadPrompter(aParent || null);
+ },
+};
+
+/**
+ * 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.
+ */
+XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () {
+ let strings = {};
+ let sb = Services.strings.createBundle(kStringBundleUrl);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ let stringName = string.key;
+ if (stringName in kStringsRequiringFormatting) {
+ strings[stringName] = function () {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ return strings;
+});
+
+// DownloadPrompter
+
+/**
+ * Allows displaying prompts related to downloads.
+ *
+ * @param aParent
+ * The nsIDOMWindow to which prompts should be attached, or null to
+ * attach prompts to the most recently active window.
+ */
+this.DownloadPrompter = function (aParent)
+{
+ if (AppConstants.MOZ_B2G) {
+ // On B2G there is no prompter implementation.
+ this._prompter = null;
+ } else {
+ this._prompter = Services.ww.getNewPrompter(aParent);
+ }
+}
+
+this.DownloadPrompter.prototype = {
+ /**
+ * Constants with the different type of prompts.
+ */
+ ON_QUIT: "prompt-on-quit",
+ ON_OFFLINE: "prompt-on-offline",
+ ON_LEAVE_PRIVATE_BROWSING: "prompt-on-leave-private-browsing",
+
+ /**
+ * nsIPrompt instance for displaying messages.
+ */
+ _prompter: null,
+
+ /**
+ * Displays a warning message box that informs that the specified file is
+ * executable, and asks whether the user wants to launch it. The user is
+ * given the option of disabling future instances of this warning.
+ *
+ * @param aPath
+ * String containing the full path to the file to be opened.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the launch operation can continue.
+ * @rejects JavaScript exception.
+ */
+ confirmLaunchExecutable: function (aPath)
+ {
+ const kPrefAlertOnEXEOpen = "browser.download.manager.alertOnEXEOpen";
+
+ try {
+ // Always launch in case we have no prompter implementation.
+ if (!this._prompter) {
+ return Promise.resolve(true);
+ }
+
+ try {
+ if (!Services.prefs.getBoolPref(kPrefAlertOnEXEOpen)) {
+ return Promise.resolve(true);
+ }
+ } catch (ex) {
+ // If the preference does not exist, continue with the prompt.
+ }
+
+ let leafName = OS.Path.basename(aPath);
+
+ let s = DownloadUIHelper.strings;
+ let checkState = { value: false };
+ let shouldLaunch = this._prompter.confirmCheck(
+ s.fileExecutableSecurityWarningTitle,
+ s.fileExecutableSecurityWarning(leafName, leafName),
+ s.fileExecutableSecurityWarningDontAsk,
+ checkState);
+
+ if (shouldLaunch) {
+ Services.prefs.setBoolPref(kPrefAlertOnEXEOpen, !checkState.value);
+ }
+
+ return Promise.resolve(shouldLaunch);
+ } catch (ex) {
+ return Promise.reject(ex);
+ }
+ },
+
+ /**
+ * Displays a warning message box that informs that there are active
+ * downloads, and asks whether the user wants to cancel them or not.
+ *
+ * @param aDownloadsCount
+ * The current downloads count.
+ * @param aPromptType
+ * The type of prompt notification depending on the observer.
+ *
+ * @return False to cancel the downloads and continue, true to abort the
+ * operation.
+ */
+ confirmCancelDownloads: function DP_confirmCancelDownload(aDownloadsCount,
+ aPromptType)
+ {
+ // Always continue in case we have no prompter implementation, or if there
+ // are no active downloads.
+ if (!this._prompter || aDownloadsCount <= 0) {
+ return false;
+ }
+
+ let s = DownloadUIHelper.strings;
+ let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
+ let okButton = aDownloadsCount > 1 ? s.cancelDownloadsOKTextMultiple(aDownloadsCount)
+ : s.cancelDownloadsOKText;
+ let title, message, cancelButton;
+
+ switch (aPromptType) {
+ case this.ON_QUIT:
+ title = s.quitCancelDownloadsAlertTitle;
+ if (AppConstants.platform != "macosx") {
+ message = aDownloadsCount > 1
+ ? s.quitCancelDownloadsAlertMsgMultiple(aDownloadsCount)
+ : s.quitCancelDownloadsAlertMsg;
+ cancelButton = s.dontQuitButtonWin;
+ } else {
+ message = aDownloadsCount > 1
+ ? s.quitCancelDownloadsAlertMsgMacMultiple(aDownloadsCount)
+ : s.quitCancelDownloadsAlertMsgMac;
+ cancelButton = s.dontQuitButtonMac;
+ }
+ break;
+ case this.ON_OFFLINE:
+ title = s.offlineCancelDownloadsAlertTitle;
+ message = aDownloadsCount > 1
+ ? s.offlineCancelDownloadsAlertMsgMultiple(aDownloadsCount)
+ : s.offlineCancelDownloadsAlertMsg;
+ cancelButton = s.dontGoOfflineButton;
+ break;
+ case this.ON_LEAVE_PRIVATE_BROWSING:
+ title = s.leavePrivateBrowsingCancelDownloadsAlertTitle;
+ message = aDownloadsCount > 1
+ ? s.leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2(aDownloadsCount)
+ : s.leavePrivateBrowsingWindowsCancelDownloadsAlertMsg2;
+ cancelButton = s.dontLeavePrivateBrowsingButton2;
+ break;
+ }
+
+ let rv = this._prompter.confirmEx(title, message, buttonFlags, okButton,
+ cancelButton, null, null, {});
+ return (rv == 1);
+ }
+};
diff --git a/toolkit/components/jsdownloads/src/Downloads.jsm b/toolkit/components/jsdownloads/src/Downloads.jsm
new file mode 100644
index 0000000000..9511dc4ca4
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -0,0 +1,305 @@
+/* -*- 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/. */
+
+/**
+ * Main entry point to get references to all the back-end objects.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "Downloads",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/DownloadCore.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadCombinedList",
+ "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadList",
+ "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadSummary",
+ "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+// Downloads
+
+/**
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides the only entry point to get references to back-end objects.
+ */
+this.Downloads = {
+ /**
+ * Work on downloads that were not started from a private browsing window.
+ */
+ get PUBLIC() {
+ return "{Downloads.PUBLIC}";
+ },
+ /**
+ * Work on downloads that were started from a private browsing window.
+ */
+ get PRIVATE() {
+ return "{Downloads.PRIVATE}";
+ },
+ /**
+ * Work on both Downloads.PRIVATE and Downloads.PUBLIC downloads.
+ */
+ get ALL() {
+ return "{Downloads.ALL}";
+ },
+
+ /**
+ * Creates a new Download object.
+ *
+ * @param aProperties
+ * Provides the initial properties for the newly created download.
+ * This matches the serializable representation of a Download object.
+ * Some of the most common properties in this object include:
+ * {
+ * source: String containing the URI for the download source.
+ * Alternatively, may be an nsIURI, a DownloadSource object,
+ * or an object with the following properties:
+ * {
+ * url: String containing the URI for the download source.
+ * isPrivate: Indicates whether the download originated from a
+ * private window. If omitted, the download is public.
+ * referrer: String containing the referrer URI of the download
+ * source. Can be omitted or null if no referrer should
+ * be sent or the download source is not HTTP.
+ * },
+ * target: String containing the path of the target file.
+ * Alternatively, may be an nsIFile, a DownloadTarget object,
+ * or an object with the following properties:
+ * {
+ * path: String containing the path of the target file.
+ * },
+ * saver: String representing the class of the download operation.
+ * If omitted, defaults to "copy". Alternatively, may be the
+ * serializable representation of a DownloadSaver object.
+ * }
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object.
+ * @rejects JavaScript exception.
+ */
+ createDownload: function D_createDownload(aProperties)
+ {
+ try {
+ return Promise.resolve(Download.fromSerializable(aProperties));
+ } catch (ex) {
+ return Promise.reject(ex);
+ }
+ },
+
+ /**
+ * Downloads data from a remote network location to a local file.
+ *
+ * This download method does not provide user interface, or the ability to
+ * cancel or restart the download programmatically. For that, you should
+ * obtain a reference to a Download object using the createDownload function.
+ *
+ * Since the download cannot be restarted, any partially downloaded data will
+ * not be kept in case the download fails.
+ *
+ * @param aSource
+ * String containing the URI for the download source. Alternatively,
+ * may be an nsIURI or a DownloadSource object.
+ * @param aTarget
+ * String containing the path of the target file. Alternatively, may
+ * be an nsIFile or a DownloadTarget object.
+ * @param aOptions
+ * An optional object used to control the behavior of this function.
+ * You may pass an object with a subset of the following fields:
+ * {
+ * isPrivate: Indicates whether the download originated from a
+ * private window.
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+ fetch: function (aSource, aTarget, aOptions) {
+ return this.createDownload({
+ source: aSource,
+ target: aTarget,
+ }).then(function D_SD_onSuccess(aDownload) {
+ if (aOptions && ("isPrivate" in aOptions)) {
+ aDownload.source.isPrivate = aOptions.isPrivate;
+ }
+ return aDownload.start();
+ });
+ },
+
+ /**
+ * Retrieves the specified type of DownloadList object. There is one download
+ * list for each type, and this method always retrieves a reference to the
+ * same download list when called with the same argument.
+ *
+ * Calling this function may cause the list of public downloads to be reloaded
+ * from the previous session, if it wasn't loaded already.
+ *
+ * @param aType
+ * This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
+ * Downloads added to the Downloads.PUBLIC and Downloads.PRIVATE lists
+ * are reflected in the Downloads.ALL list, and downloads added to the
+ * Downloads.ALL list are also added to either the Downloads.PUBLIC or
+ * the Downloads.PRIVATE list based on their properties.
+ *
+ * @return {Promise}
+ * @resolves The requested DownloadList or DownloadCombinedList object.
+ * @rejects JavaScript exception.
+ */
+ getList: function (aType)
+ {
+ if (!this._promiseListsInitialized) {
+ this._promiseListsInitialized = Task.spawn(function* () {
+ let publicList = new DownloadList();
+ let privateList = new DownloadList();
+ let combinedList = new DownloadCombinedList(publicList, privateList);
+
+ try {
+ yield DownloadIntegration.addListObservers(publicList, false);
+ yield DownloadIntegration.addListObservers(privateList, true);
+ yield DownloadIntegration.initializePublicDownloadList(publicList);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ let publicSummary = yield this.getSummary(Downloads.PUBLIC);
+ let privateSummary = yield this.getSummary(Downloads.PRIVATE);
+ let combinedSummary = yield this.getSummary(Downloads.ALL);
+
+ yield publicSummary.bindToList(publicList);
+ yield privateSummary.bindToList(privateList);
+ yield combinedSummary.bindToList(combinedList);
+
+ this._lists[Downloads.PUBLIC] = publicList;
+ this._lists[Downloads.PRIVATE] = privateList;
+ this._lists[Downloads.ALL] = combinedList;
+ }.bind(this));
+ }
+
+ return this._promiseListsInitialized.then(() => this._lists[aType]);
+ },
+
+ /**
+ * Promise resolved when the initialization of the download lists has
+ * completed, or null if initialization has never been requested.
+ */
+ _promiseListsInitialized: null,
+
+ /**
+ * After initialization, this object is populated with one key for each type
+ * of download list that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
+ * or Downloads.ALL). The values are the DownloadList objects.
+ */
+ _lists: {},
+
+ /**
+ * Retrieves the specified type of DownloadSummary object. There is one
+ * download summary for each type, and this method always retrieves a
+ * reference to the same download summary when called with the same argument.
+ *
+ * Calling this function does not cause the list of public downloads to be
+ * reloaded from the previous session. The summary will behave as if no
+ * downloads are present until the getList method is called.
+ *
+ * @param aType
+ * This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
+ *
+ * @return {Promise}
+ * @resolves The requested DownloadList or DownloadCombinedList object.
+ * @rejects JavaScript exception.
+ */
+ getSummary: function (aType)
+ {
+ if (aType != Downloads.PUBLIC && aType != Downloads.PRIVATE &&
+ aType != Downloads.ALL) {
+ throw new Error("Invalid aType argument.");
+ }
+
+ if (!(aType in this._summaries)) {
+ this._summaries[aType] = new DownloadSummary();
+ }
+
+ return Promise.resolve(this._summaries[aType]);
+ },
+
+ /**
+ * This object is populated by the getSummary method with one key for each
+ * type of object that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
+ * or Downloads.ALL). The values are the DownloadSummary objects.
+ */
+ _summaries: {},
+
+ /**
+ * Returns the system downloads directory asynchronously.
+ * Mac OSX:
+ * User downloads directory
+ * XP/2K:
+ * My Documents/Downloads
+ * Vista and others:
+ * User downloads directory
+ * Linux:
+ * XDG user dir spec, with a fallback to Home/Downloads
+ * Android:
+ * standard downloads directory i.e. /sdcard
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getSystemDownloadsDirectory: function D_getSystemDownloadsDirectory() {
+ return DownloadIntegration.getSystemDownloadsDirectory();
+ },
+
+ /**
+ * Returns the preferred downloads directory based on the user preferences
+ * in the current profile asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getPreferredDownloadsDirectory: function D_getPreferredDownloadsDirectory() {
+ return DownloadIntegration.getPreferredDownloadsDirectory();
+ },
+
+ /**
+ * Returns the temporary directory where downloads are placed before the
+ * final location is chosen, or while the document is opened temporarily
+ * with an external application. This may or may not be the system temporary
+ * directory, based on the platform asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getTemporaryDownloadsDirectory: function D_getTemporaryDownloadsDirectory() {
+ return DownloadIntegration.getTemporaryDownloadsDirectory();
+ },
+
+ /**
+ * Constructor for a DownloadError object. When you catch an exception during
+ * a download, you can use this to verify if "ex instanceof Downloads.Error",
+ * before reading the exception properties with the error details.
+ */
+ Error: DownloadError,
+};
diff --git a/toolkit/components/jsdownloads/src/Downloads.manifest b/toolkit/components/jsdownloads/src/Downloads.manifest
new file mode 100644
index 0000000000..03d4ed4a63
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/Downloads.manifest
@@ -0,0 +1,2 @@
+component {1b4c85df-cbdd-4bb6-b04e-613caece083c} DownloadLegacy.js
+contract @mozilla.org/transfer;1 {1b4c85df-cbdd-4bb6-b04e-613caece083c}
diff --git a/toolkit/components/jsdownloads/src/moz.build b/toolkit/components/jsdownloads/src/moz.build
new file mode 100644
index 0000000000..87abed62ef
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/moz.build
@@ -0,0 +1,31 @@
+# -*- 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 += [
+ 'DownloadPlatform.cpp',
+]
+
+EXTRA_COMPONENTS += [
+ 'DownloadLegacy.js',
+ 'Downloads.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'DownloadCore.jsm',
+ 'DownloadImport.jsm',
+ 'DownloadList.jsm',
+ 'Downloads.jsm',
+ 'DownloadStore.jsm',
+ 'DownloadUIHelper.jsm',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'DownloadIntegration.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+CXXFLAGS += CONFIG['TK_CFLAGS']
diff --git a/toolkit/components/jsdownloads/test/browser/.eslintrc.js b/toolkit/components/jsdownloads/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/jsdownloads/test/browser/browser.ini b/toolkit/components/jsdownloads/test/browser/browser.ini
new file mode 100644
index 0000000000..131fc4ec86
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+ testFile.html
+
+[browser_DownloadPDFSaver.js]
+skip-if = os != "win"
diff --git a/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
new file mode 100644
index 0000000000..80ed9665a7
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
@@ -0,0 +1,97 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the PDF download saver, and tests using a window as a
+ * source for the copy download saver.
+ */
+
+"use strict";
+
+/**
+ * Helper function to make sure a window reference exists on the download source.
+ */
+function* test_download_windowRef(aTab, aDownload) {
+ ok(aDownload.source.windowRef, "Download source had a window reference");
+ ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref");
+ is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test");
+}
+
+/**
+ * Helper function to check the state of a completed download.
+ */
+function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) {
+ ok(aDownload.source, "Download has a source");
+ is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url");
+ is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state");
+ ok(aDownload.stopped, "Download is stopped");
+ is(aCanceled, aDownload.canceled, "Download has correct canceled state");
+ is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state");
+ is(aDownload.error, null, "Download error is not defined");
+}
+
+function* test_createDownload_common(aPrivate, aType) {
+ let win = yield BrowserTestUtils.openNewBrowserWindow({ private : aPrivate});
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, getRootDirectory(gTestPath) + "testFile.html");
+ let download = yield Downloads.createDownload({
+ source: tab.linkedBrowser.contentWindow,
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+ saver: { type: aType }
+ });
+
+ yield test_download_windowRef(tab, download);
+ yield download.start();
+
+ yield test_download_state_complete(tab, download, aPrivate, false);
+ if (aType == "pdf") {
+ let signature = yield OS.File.read(download.target.path,
+ { bytes: 4, encoding: "us-ascii" });
+ is(signature, "%PDF", "File exists and signature matches");
+ } else {
+ ok((yield OS.File.exists(download.target.path)), "File exists");
+ }
+
+ win.gBrowser.removeTab(tab);
+ win.close()
+}
+
+add_task(function* test_createDownload_pdf_private() {
+ yield test_createDownload_common(true, "pdf");
+});
+add_task(function* test_createDownload_pdf_not_private() {
+ yield test_createDownload_common(false, "pdf");
+});
+
+// Even for the copy saver, using a window should produce valid results
+add_task(function* test_createDownload_copy_private() {
+ yield test_createDownload_common(true, "copy");
+});
+add_task(function* test_createDownload_copy_not_private() {
+ yield test_createDownload_common(false, "copy");
+});
+
+add_task(function* test_cancel_pdf_download() {
+ let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+
+ let download = yield Downloads.createDownload({
+ source: tab.linkedBrowser.contentWindow,
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+ saver: "pdf",
+ });
+
+ yield test_download_windowRef(tab, download);
+ download.start().catch(() => {});
+
+ // Immediately cancel the download to test that it is erased correctly.
+ yield download.cancel();
+ yield test_download_state_complete(tab, download, false, true);
+
+ let exists = yield OS.File.exists(download.target.path)
+ ok(!exists, "Target file does not exist");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/jsdownloads/test/browser/head.js b/toolkit/components/jsdownloads/test/browser/head.js
new file mode 100644
index 0000000000..769aaacb31
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/head.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf";
+
+// Support functions
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+var gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ ok(!file.exists(), "Temp file does not exist");
+
+ registerCleanupFunction(function () {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+function promiseBrowserLoaded(browser) {
+ return new Promise(resolve => {
+ browser.addEventListener("load", function onLoad(event) {
+ if (event.target == browser.contentDocument) {
+ browser.removeEventListener("load", onLoad, true);
+ resolve();
+ }
+ }, true);
+ });
+}
diff --git a/toolkit/components/jsdownloads/test/browser/testFile.html b/toolkit/components/jsdownloads/test/browser/testFile.html
new file mode 100644
index 0000000000..ee413514b1
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/testFile.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test Save as PDF</title>
+ </head>
+ <body>
+ <p>Save me as a PDF!</p>
+ </body>
+</html>
diff --git a/toolkit/components/jsdownloads/test/data/.eslintrc.js b/toolkit/components/jsdownloads/test/data/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/data/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/jsdownloads/test/data/empty.txt b/toolkit/components/jsdownloads/test/data/empty.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/data/empty.txt
diff --git a/toolkit/components/jsdownloads/test/data/source.txt b/toolkit/components/jsdownloads/test/data/source.txt
new file mode 100644
index 0000000000..2156cb8c03
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/data/source.txt
@@ -0,0 +1 @@
+This test string is downloaded. \ No newline at end of file
diff --git a/toolkit/components/jsdownloads/test/unit/.eslintrc.js b/toolkit/components/jsdownloads/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/jsdownloads/test/unit/common_test_Download.js b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
new file mode 100644
index 0000000000..42d4c56828
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -0,0 +1,2432 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This script is loaded by "test_DownloadCore.js" and "test_DownloadLegacy.js"
+ * with different values of the gUseLegacySaver variable, to apply tests to both
+ * the "copy" and "legacy" saver implementations.
+ */
+
+"use strict";
+
+// Globals
+
+const kDeleteTempFileOnExit = "browser.helperApps.deleteTempFileOnExit";
+
+/**
+ * Creates and starts a new download, using either DownloadCopySaver or
+ * DownloadLegacySaver based on the current test run.
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object. The download may be in progress
+ * or already finished. The promiseDownloadStopped function can be
+ * used to wait for completion.
+ * @rejects JavaScript exception.
+ */
+function promiseStartDownload(aSourceUrl) {
+ if (gUseLegacySaver) {
+ return promiseStartLegacyDownload(aSourceUrl);
+ }
+
+ return promiseNewDownload(aSourceUrl).then(download => {
+ download.start().catch(() => {});
+ return download;
+ });
+}
+
+/**
+ * Creates and starts a new download, configured to keep partial data, and
+ * returns only when the first part of "interruptible_resumable.txt" has been
+ * saved to disk. You must call "continueResponses" to allow the interruptible
+ * request to continue.
+ *
+ * This function uses either DownloadCopySaver or DownloadLegacySaver based on
+ * the current test run.
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object, still in progress.
+ * @rejects JavaScript exception.
+ */
+function promiseStartDownload_tryToKeepPartialData() {
+ return Task.spawn(function* () {
+ mustInterruptResponses();
+
+ // Start a new download and configure it to keep partially downloaded data.
+ let download;
+ if (!gUseLegacySaver) {
+ let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ download = yield Downloads.createDownload({
+ source: httpUrl("interruptible_resumable.txt"),
+ target: { path: targetFilePath,
+ partFilePath: targetFilePath + ".part" },
+ });
+ download.tryToKeepPartialData = true;
+ download.start().catch(() => {});
+ } else {
+ // Start a download using nsIExternalHelperAppService, that is configured
+ // to keep partially downloaded data by default.
+ download = yield promiseStartExternalHelperAppServiceDownload();
+ }
+
+ yield promiseDownloadMidway(download);
+ yield promisePartFileReady(download);
+
+ return download;
+ });
+}
+
+/**
+ * This function should be called after the progress notification for a download
+ * is received, and waits for the worker thread of BackgroundFileSaver to
+ * receive the data to be written to the ".part" file on disk.
+ *
+ * @return {Promise}
+ * @resolves When the ".part" file has been written to disk.
+ * @rejects JavaScript exception.
+ */
+function promisePartFileReady(aDownload) {
+ return Task.spawn(function* () {
+ // We don't have control over the file output code in BackgroundFileSaver.
+ // After we receive the download progress notification, we may only check
+ // that the ".part" file has been created, while its size cannot be
+ // determined because the file is currently open.
+ try {
+ do {
+ yield promiseTimeout(50);
+ } while (!(yield OS.File.exists(aDownload.target.partFilePath)));
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error)) {
+ throw ex;
+ }
+ // This indicates that the file has been created and cannot be accessed.
+ // The specific error might vary with the platform.
+ do_print("Expected exception while checking existence: " + ex.toString());
+ // Wait some more time to allow the write to complete.
+ yield promiseTimeout(100);
+ }
+ });
+}
+
+/**
+ * Checks that the actual data written to disk matches the expected data as well
+ * as the properties of the given DownloadTarget object.
+ *
+ * @param downloadTarget
+ * The DownloadTarget object whose details have to be verified.
+ * @param expectedContents
+ * String containing the octets that are expected in the file.
+ *
+ * @return {Promise}
+ * @resolves When the properties have been verified.
+ * @rejects JavaScript exception.
+ */
+var promiseVerifyTarget = Task.async(function* (downloadTarget,
+ expectedContents) {
+ yield promiseVerifyContents(downloadTarget.path, expectedContents);
+ do_check_true(downloadTarget.exists);
+ do_check_eq(downloadTarget.size, expectedContents.length);
+});
+
+/**
+ * Waits for an attempt to launch a file, and returns the nsIMIMEInfo used for
+ * the launch, or null if the file was launched with the default handler.
+ */
+function waitForFileLaunched() {
+ return new Promise(resolve => {
+ let waitFn = base => ({
+ launchFile(file, mimeInfo) {
+ Integration.downloads.unregister(waitFn);
+ if (!mimeInfo ||
+ mimeInfo.preferredAction == Ci.nsIMIMEInfo.useSystemDefault) {
+ resolve(null);
+ } else {
+ resolve(mimeInfo);
+ }
+ return Promise.resolve();
+ },
+ });
+ Integration.downloads.register(waitFn);
+ });
+}
+
+/**
+ * Waits for an attempt to show the directory where a file is located, and
+ * returns the path of the file.
+ */
+function waitForDirectoryShown() {
+ return new Promise(resolve => {
+ let waitFn = base => ({
+ showContainingDirectory(path) {
+ Integration.downloads.unregister(waitFn);
+ resolve(path);
+ return Promise.resolve();
+ },
+ });
+ Integration.downloads.register(waitFn);
+ });
+}
+
+// Tests
+
+/**
+ * Executes a download and checks its basic properties after construction.
+ * The download is started by constructing the simplest Download object with
+ * the "copy" saver, or using the legacy nsITransfer interface.
+ */
+add_task(function* test_basic()
+{
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can check its basic properties before it starts.
+ download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: { path: targetFile.path },
+ saver: { type: "copy" },
+ });
+
+ do_check_eq(download.source.url, httpUrl("source.txt"));
+ do_check_eq(download.target.path, targetFile.path);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we must check its basic properties while in progress.
+ download = yield promiseStartLegacyDownload(null,
+ { targetFile: targetFile });
+
+ do_check_eq(download.source.url, httpUrl("source.txt"));
+ do_check_eq(download.target.path, targetFile.path);
+
+ yield promiseDownloadStopped(download);
+ }
+
+ // Check additional properties on the finished download.
+ do_check_true(download.source.referrer === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Executes a download with the tryToKeepPartialData property set, and ensures
+ * that the file is saved correctly. When testing DownloadLegacySaver, the
+ * download is executed using the nsIExternalHelperAppService component.
+ */
+add_task(function* test_basic_tryToKeepPartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ continueResponses();
+ yield promiseDownloadStopped(download);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_eq(32, download.saver.getSha256Hash().length);
+});
+
+/**
+ * Tests the permissions of the final target file once the download finished.
+ */
+add_task(function* test_unix_permissions()
+{
+ // This test is only executed on some Desktop systems.
+ if (Services.appinfo.OS != "Darwin" && Services.appinfo.OS != "Linux" &&
+ Services.appinfo.OS != "WINNT") {
+ do_print("Skipping test.");
+ return;
+ }
+
+ let launcherPath = getTempFile("app-launcher").path;
+
+ for (let autoDelete of [false, true]) {
+ for (let isPrivate of [false, true]) {
+ for (let launchWhenSucceeded of [false, true]) {
+ do_print("Checking " + JSON.stringify({ autoDelete,
+ isPrivate,
+ launchWhenSucceeded }));
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, autoDelete);
+
+ let download;
+ if (!gUseLegacySaver) {
+ download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launchWhenSucceeded,
+ launcherPath,
+ });
+ yield download.start();
+ } else {
+ download = yield promiseStartLegacyDownload(httpUrl("source.txt"), {
+ isPrivate,
+ launchWhenSucceeded,
+ launcherPath: launchWhenSucceeded && launcherPath,
+ });
+ yield promiseDownloadStopped(download);
+ }
+
+ let isTemporary = launchWhenSucceeded && (autoDelete || isPrivate);
+ let stat = yield OS.File.stat(download.target.path);
+ if (Services.appinfo.OS == "WINNT") {
+ // On Windows
+ // Temporary downloads should be read-only
+ do_check_eq(stat.winAttributes.readOnly, isTemporary ? true : false);
+ } else {
+ // On Linux, Mac
+ // Temporary downloads should be read-only and not accessible to other
+ // users, while permanently downloaded files should be readable and
+ // writable as specified by the system umask.
+ do_check_eq(stat.unixMode,
+ isTemporary ? 0o400 : (0o666 & ~OS.Constants.Sys.umask));
+ }
+ }
+ }
+ }
+
+ // Clean up the changes to the preference.
+ Services.prefs.clearUserPref(kDeleteTempFileOnExit);
+});
+
+/**
+ * Checks the referrer for downloads.
+ */
+add_task(function* test_referrer()
+{
+ let sourcePath = "/test_referrer.txt";
+ let sourceUrl = httpUrl("test_referrer.txt");
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ do_check_true(aRequest.hasHeader("Referer"));
+ do_check_eq(aRequest.getHeader("Referer"), TEST_REFERRER_URL);
+ });
+ let download = yield Downloads.createDownload({
+ source: { url: sourceUrl, referrer: TEST_REFERRER_URL },
+ target: targetPath,
+ });
+ do_check_eq(download.source.referrer, TEST_REFERRER_URL);
+ yield download.start();
+
+ download = yield Downloads.createDownload({
+ source: { url: sourceUrl, referrer: TEST_REFERRER_URL,
+ isPrivate: true },
+ target: targetPath,
+ });
+ do_check_eq(download.source.referrer, TEST_REFERRER_URL);
+ yield download.start();
+
+ // Test the download still works for non-HTTP channel with referrer.
+ sourceUrl = "data:text/html,<html><body></body></html>";
+ download = yield Downloads.createDownload({
+ source: { url: sourceUrl, referrer: TEST_REFERRER_URL },
+ target: targetPath,
+ });
+ do_check_eq(download.source.referrer, TEST_REFERRER_URL);
+ yield download.start();
+
+ cleanup();
+});
+
+/**
+ * Checks the adjustChannel callback for downloads.
+ */
+add_task(function* test_adjustChannel()
+{
+ const sourcePath = "/test_post.txt";
+ const sourceUrl = httpUrl("test_post.txt");
+ const targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ const customHeader = { name: "X-Answer", value: "42" };
+ const postData = "Don't Panic";
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, aRequest => {
+ do_check_eq(aRequest.method, "POST");
+
+ do_check_true(aRequest.hasHeader(customHeader.name));
+ do_check_eq(aRequest.getHeader(customHeader.name), customHeader.value);
+
+ const stream = aRequest.bodyInputStream;
+ const body = NetUtil.readInputStreamToString(stream, stream.available());
+ do_check_eq(body, postData);
+ });
+
+ function adjustChannel(channel) {
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.setRequestHeader(customHeader.name, customHeader.value, false);
+
+ const stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(postData, postData.length);
+
+ channel.QueryInterface(Ci.nsIUploadChannel2);
+ channel.explicitSetUploadStream(stream, null, -1, "POST", false);
+
+ return Promise.resolve();
+ }
+
+ const download = yield Downloads.createDownload({
+ source: { url: sourceUrl, adjustChannel },
+ target: targetPath,
+ });
+ do_check_eq(download.source.adjustChannel, adjustChannel);
+ do_check_eq(download.toSerializable(), null);
+ yield download.start();
+
+ cleanup();
+});
+
+/**
+ * Checks initial and final state and progress for a successful download.
+ */
+add_task(function* test_initial_final_state()
+{
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can check its state before it starts.
+ download = yield promiseNewDownload();
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+ do_check_eq(download.progress, 0);
+ do_check_true(download.startTime === null);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we cannot check its initial state.
+ download = yield promiseStartLegacyDownload();
+ yield promiseDownloadStopped(download);
+ }
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+ do_check_eq(download.progress, 100);
+ do_check_true(isValidDate(download.startTime));
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, TEST_DATA_SHORT.length);
+});
+
+/**
+ * Checks the notification of the final download state.
+ */
+add_task(function* test_final_state_notified()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let onchangeNotified = false;
+ let lastNotifiedStopped;
+ let lastNotifiedProgress;
+ download.onchange = function () {
+ onchangeNotified = true;
+ lastNotifiedStopped = download.stopped;
+ lastNotifiedProgress = download.progress;
+ };
+
+ // Allow the download to complete.
+ let promiseAttempt = download.start();
+ continueResponses();
+ yield promiseAttempt;
+
+ // The view should have been notified before the download completes.
+ do_check_true(onchangeNotified);
+ do_check_true(lastNotifiedStopped);
+ do_check_eq(lastNotifiedProgress, 100);
+});
+
+/**
+ * Checks intermediate progress for a successful download.
+ */
+add_task(function* test_intermediate_progress()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ yield promiseDownloadMidway(download);
+
+ do_check_true(download.hasProgress);
+ do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
+
+ // The final file size should not be computed for in-progress downloads.
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // Continue after the first chunk of data is fully received.
+ continueResponses();
+ yield promiseDownloadStopped(download);
+
+ do_check_true(download.stopped);
+ do_check_eq(download.progress, 100);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Downloads a file with a "Content-Length" of 0 and checks the progress.
+ */
+add_task(function* test_empty_progress()
+{
+ let download = yield promiseStartDownload(httpUrl("empty.txt"));
+ yield promiseDownloadStopped(download);
+
+ do_check_true(download.stopped);
+ do_check_true(download.hasProgress);
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+
+ // We should have received the content type even for an empty file.
+ do_check_eq(download.contentType, "text/plain");
+
+ do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Downloads a file with a "Content-Length" of 0 with the tryToKeepPartialData
+ * property set, and ensures that the file is saved correctly.
+ */
+add_task(function* test_empty_progress_tryToKeepPartialData()
+{
+ // Start a new download and configure it to keep partially downloaded data.
+ let download;
+ if (!gUseLegacySaver) {
+ let targetFilePath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ download = yield Downloads.createDownload({
+ source: httpUrl("empty.txt"),
+ target: { path: targetFilePath,
+ partFilePath: targetFilePath + ".part" },
+ });
+ download.tryToKeepPartialData = true;
+ download.start().catch(() => {});
+ } else {
+ // Start a download using nsIExternalHelperAppService, that is configured
+ // to keep partially downloaded data by default.
+ download = yield promiseStartExternalHelperAppServiceDownload(
+ httpUrl("empty.txt"));
+ }
+ yield promiseDownloadStopped(download);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_eq(32, download.saver.getSha256Hash().length);
+});
+
+/**
+ * Downloads an empty file with no "Content-Length" and checks the progress.
+ */
+add_task(function* test_empty_noprogress()
+{
+ let sourcePath = "/test_empty_noprogress.txt";
+ let sourceUrl = httpUrl("test_empty_noprogress.txt");
+ let deferRequestReceived = Promise.defer();
+
+ // Register an interruptible handler that notifies us when the request occurs.
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ registerInterruptibleHandler(sourcePath,
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ deferRequestReceived.resolve();
+ }, function secondPart(aRequest, aResponse) { });
+
+ // Start the download, without allowing the request to finish.
+ mustInterruptResponses();
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can hook its onchange callback that will be notified when the
+ // download starts.
+ download = yield promiseNewDownload(sourceUrl);
+
+ download.onchange = function () {
+ if (!download.stopped) {
+ do_check_false(download.hasProgress);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+ }
+ };
+
+ download.start().catch(() => {});
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, and it may have already made all needed property change
+ // notifications, thus there is no point in checking the onchange callback.
+ download = yield promiseStartLegacyDownload(sourceUrl);
+ }
+
+ // Wait for the request to be received by the HTTP server, but don't allow the
+ // request to finish yet. Before checking the download state, wait for the
+ // events to be processed by the client.
+ yield deferRequestReceived.promise;
+ yield promiseExecuteSoon();
+
+ // Check that this download has no progress report.
+ do_check_false(download.stopped);
+ do_check_false(download.hasProgress);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+
+ // Now allow the response to finish.
+ continueResponses();
+ yield promiseDownloadStopped(download);
+
+ // We should have received the content type even if no progress is reported.
+ do_check_eq(download.contentType, "text/plain");
+
+ // Verify the state of the completed download.
+ do_check_true(download.stopped);
+ do_check_false(download.hasProgress);
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.currentBytes, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_true(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ do_check_eq((yield OS.File.stat(download.target.path)).size, 0);
+});
+
+/**
+ * Calls the "start" method two times before the download is finished.
+ */
+add_task(function* test_start_twice()
+{
+ mustInterruptResponses();
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can start the download later during the test.
+ download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created. Effectively, we are starting the download three times.
+ download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"));
+ }
+
+ // Call the start method two times.
+ let promiseAttempt1 = download.start();
+ let promiseAttempt2 = download.start();
+
+ // Allow the download to finish.
+ continueResponses();
+
+ // Both promises should now be resolved.
+ yield promiseAttempt1;
+ yield promiseAttempt2;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download and verifies that its state is reported correctly.
+ */
+add_task(function* test_cancel_midway()
+{
+ mustInterruptResponses();
+
+ // In this test case, we execute different checks that are only possible with
+ // DownloadCopySaver or DownloadLegacySaver respectively.
+ let download;
+ let options = {};
+ if (!gUseLegacySaver) {
+ download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ } else {
+ download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"),
+ options);
+ }
+
+ // Cancel the download after receiving the first part of the response.
+ let deferCancel = Promise.defer();
+ let onchange = function () {
+ if (!download.stopped && !download.canceled && download.progress == 50) {
+ // Cancel the download immediately during the notification.
+ deferCancel.resolve(download.cancel());
+
+ // The state change happens immediately after calling "cancel", but
+ // temporary files or part files may still exist at this point.
+ do_check_true(download.canceled);
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress. This may happen
+ // when using DownloadLegacySaver.
+ download.onchange = onchange;
+ onchange();
+
+ let promiseAttempt;
+ if (!gUseLegacySaver) {
+ promiseAttempt = download.start();
+ }
+
+ // Wait on the promise returned by the "cancel" method to ensure that the
+ // cancellation process finished and temporary files were removed.
+ yield deferCancel.promise;
+
+ if (gUseLegacySaver) {
+ // The nsIWebBrowserPersist instance should have been canceled now.
+ do_check_eq(options.outPersist.result, Cr.NS_ERROR_ABORT);
+ }
+
+ do_check_true(download.stopped);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ // Progress properties are not reset by canceling.
+ do_check_eq(download.progress, 50);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(download.currentBytes, TEST_DATA_SHORT.length);
+
+ if (!gUseLegacySaver) {
+ // The promise returned by "start" should have been rejected meanwhile.
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+ }
+});
+
+/**
+ * Cancels a download while keeping partially downloaded data, and verifies that
+ * both the target file and the ".part" file are deleted.
+ */
+add_task(function* test_cancel_midway_tryToKeepPartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+
+ do_check_true(yield OS.File.exists(download.target.path));
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ yield download.cancel();
+ yield download.removePartialData();
+
+ do_check_true(download.stopped);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download right after starting it.
+ */
+add_task(function* test_cancel_immediately()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseAttempt = download.start();
+ do_check_false(download.stopped);
+
+ let promiseCancel = download.cancel();
+ do_check_true(download.canceled);
+
+ // At this point, we don't know whether the download has already stopped or
+ // is still waiting for cancellation. We can wait on the promise returned
+ // by the "start" method to know for sure.
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ do_check_true(download.stopped);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ // Check that the promise returned by the "cancel" method has been resolved.
+ yield promiseCancel;
+});
+
+/**
+ * Cancels and restarts a download sequentially.
+ */
+add_task(function* test_cancel_midway_restart()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ // The first time, cancel the download midway.
+ yield promiseDownloadMidway(download);
+ yield download.cancel();
+
+ do_check_true(download.stopped);
+
+ // The second time, we'll provide the entire interruptible response.
+ continueResponses();
+ download.onchange = null;
+ let promiseAttempt = download.start();
+
+ // Download state should have already been reset.
+ do_check_false(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ // For the following test, we rely on the network layer reporting its progress
+ // asynchronously. Otherwise, there is nothing stopping the restarted
+ // download from reaching the same progress as the first request already.
+ do_check_eq(download.progress, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_eq(download.currentBytes, 0);
+
+ yield promiseAttempt;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download and restarts it from where it stopped.
+ */
+add_task(function* test_cancel_midway_restart_tryToKeepPartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.cancel();
+
+ do_check_true(download.stopped);
+ do_check_true(download.hasPartialData);
+
+ // The target file should not exist, but we should have kept the partial data.
+ do_check_false(yield OS.File.exists(download.target.path));
+ yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // Verify that the server sent the response from the start.
+ do_check_eq(gMostRecentFirstBytePos, 0);
+
+ // The second time, we'll request and obtain the second part of the response,
+ // but we still stop when half of the remaining progress is reached.
+ let deferMidway = Promise.defer();
+ download.onchange = function () {
+ if (!download.stopped && !download.canceled &&
+ download.currentBytes == Math.floor(TEST_DATA_SHORT.length * 3 / 2)) {
+ download.onchange = null;
+ deferMidway.resolve();
+ }
+ };
+
+ mustInterruptResponses();
+ let promiseAttempt = download.start();
+
+ // Continue when the number of bytes we received is correct, then check that
+ // progress is at about 75 percent. The exact figure may vary because of
+ // rounding issues, since the total number of bytes in the response might not
+ // be a multiple of four.
+ yield deferMidway.promise;
+ do_check_true(download.progress > 72 && download.progress < 78);
+
+ // Now we allow the download to finish.
+ continueResponses();
+ yield promiseAttempt;
+
+ // Check that the server now sent the second part only.
+ do_check_eq(gMostRecentFirstBytePos, TEST_DATA_SHORT.length);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download while keeping partially downloaded data, then removes the
+ * data and restarts the download from the beginning.
+ */
+add_task(function* test_cancel_midway_restart_removePartialData()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.cancel();
+
+ do_check_true(download.hasPartialData);
+ yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ yield download.removePartialData();
+
+ do_check_false(download.hasPartialData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // The second time, we'll request and obtain the entire response again.
+ continueResponses();
+ yield download.start();
+
+ // Verify that the server sent the response from the start.
+ do_check_eq(gMostRecentFirstBytePos, 0);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download while keeping partially downloaded data, then removes the
+ * data and restarts the download from the beginning without keeping the partial
+ * data anymore.
+ */
+add_task(function* test_cancel_midway_restart_tryToKeepPartialData_false()
+{
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.cancel();
+
+ download.tryToKeepPartialData = false;
+
+ // The above property change does not affect existing partial data.
+ do_check_true(download.hasPartialData);
+ yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
+
+ yield download.removePartialData();
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+
+ // Restart the download from the beginning.
+ mustInterruptResponses();
+ download.start().catch(() => {});
+
+ yield promiseDownloadMidway(download);
+ yield promisePartFileReady(download);
+
+ // While the download is in progress, we should still have a ".part" file.
+ do_check_false(download.hasPartialData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ // On Unix, verify that the file with the partially downloaded data is not
+ // accessible by other users on the system.
+ if (Services.appinfo.OS == "Darwin" || Services.appinfo.OS == "Linux") {
+ do_check_eq((yield OS.File.stat(download.target.partFilePath)).unixMode,
+ 0o600);
+ }
+
+ yield download.cancel();
+
+ // The ".part" file should be deleted now that the download is canceled.
+ do_check_false(download.hasPartialData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+
+ // The third time, we'll request and obtain the entire response again.
+ continueResponses();
+ yield download.start();
+
+ // Verify that the server sent the response from the start.
+ do_check_eq(gMostRecentFirstBytePos, 0);
+
+ // The target file should now have been created, and the ".part" file deleted.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Cancels a download right after starting it, then restarts it immediately.
+ */
+add_task(function* test_cancel_immediately_restart_immediately()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt = download.start();
+
+ do_check_false(download.stopped);
+
+ download.cancel();
+ do_check_true(download.canceled);
+
+ let promiseRestarted = download.start();
+ do_check_false(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ // For the following test, we rely on the network layer reporting its progress
+ // asynchronously. Otherwise, there is nothing stopping the restarted
+ // download from reaching the same progress as the first request already.
+ do_check_eq(download.hasProgress, false);
+ do_check_eq(download.progress, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_eq(download.currentBytes, 0);
+
+ // Ensure the next request is now allowed to complete, regardless of whether
+ // the canceled request was received by the server or not.
+ continueResponses();
+ try {
+ yield promiseAttempt;
+ // If we get here, it means that the first attempt actually succeeded. In
+ // fact, this could be a valid outcome, because the cancellation request may
+ // not have been processed in time before the download finished.
+ do_print("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ yield promiseRestarted;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Cancels a download midway, then restarts it immediately.
+ */
+add_task(function* test_cancel_midway_restart_immediately()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt = download.start();
+
+ // The first time, cancel the download midway.
+ yield promiseDownloadMidway(download);
+ download.cancel();
+ do_check_true(download.canceled);
+
+ let promiseRestarted = download.start();
+ do_check_false(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ // For the following test, we rely on the network layer reporting its progress
+ // asynchronously. Otherwise, there is nothing stopping the restarted
+ // download from reaching the same progress as the first request already.
+ do_check_eq(download.hasProgress, false);
+ do_check_eq(download.progress, 0);
+ do_check_eq(download.totalBytes, 0);
+ do_check_eq(download.currentBytes, 0);
+
+ // The second request is allowed to complete.
+ continueResponses();
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ yield promiseRestarted;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Calls the "cancel" method on a successful download.
+ */
+add_task(function* test_cancel_successful()
+{
+ let download = yield promiseStartDownload();
+ yield promiseDownloadStopped(download);
+
+ // The cancel method should succeed with no effect.
+ yield download.cancel();
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Calls the "cancel" method two times in a row.
+ */
+add_task(function* test_cancel_twice()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseAttempt = download.start();
+ do_check_false(download.stopped);
+
+ let promiseCancel1 = download.cancel();
+ do_check_true(download.canceled);
+ let promiseCancel2 = download.cancel();
+
+ try {
+ yield promiseAttempt;
+ do_throw("The download should have been canceled.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error)) {
+ throw ex;
+ }
+ do_check_false(ex.becauseSourceFailed);
+ do_check_false(ex.becauseTargetFailed);
+ }
+
+ // Both promises should now be resolved.
+ yield promiseCancel1;
+ yield promiseCancel2;
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Checks the "refresh" method for succeeded downloads.
+ */
+add_task(function* test_refresh_succeeded()
+{
+ let download = yield promiseStartDownload();
+ yield promiseDownloadStopped(download);
+
+ // The DownloadTarget properties should be the same after calling "refresh".
+ yield download.refresh();
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+
+ // If the file is removed, only the "exists" property should change, and the
+ // "size" property should keep its previous value.
+ yield OS.File.move(download.target.path, download.target.path + ".old");
+ yield download.refresh();
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, TEST_DATA_SHORT.length);
+
+ // The DownloadTarget properties should be restored when the file is put back.
+ yield OS.File.move(download.target.path + ".old", download.target.path);
+ yield download.refresh();
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Checks that a download cannot be restarted after the "finalize" method.
+ */
+add_task(function* test_finalize()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ let promiseFinalized = download.finalize();
+
+ try {
+ yield download.start();
+ do_throw("It should not be possible to restart after finalization.");
+ } catch (ex) { }
+
+ yield promiseFinalized;
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_true(download.canceled);
+ do_check_true(download.error === null);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Checks that the "finalize" method can remove partially downloaded data.
+ */
+add_task(function* test_finalize_tryToKeepPartialData()
+{
+ // Check finalization without removing partial data.
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.finalize();
+
+ do_check_true(download.hasPartialData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ // Clean up.
+ yield download.removePartialData();
+
+ // Check finalization while removing partial data.
+ download = yield promiseStartDownload_tryToKeepPartialData();
+ yield download.finalize(true);
+
+ do_check_false(download.hasPartialData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+});
+
+/**
+ * Checks that whenSucceeded returns a promise that is resolved after a restart.
+ */
+add_task(function* test_whenSucceeded_after_restart()
+{
+ mustInterruptResponses();
+
+ let promiseSucceeded;
+
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can verify getting a reference before the first download attempt.
+ download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ promiseSucceeded = download.whenSucceeded();
+ download.start().catch(() => {});
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when it
+ // is created, thus we cannot get the reference before the first attempt.
+ download = yield promiseStartLegacyDownload(httpUrl("interruptible.txt"));
+ promiseSucceeded = download.whenSucceeded();
+ }
+
+ // Cancel the first download attempt.
+ yield download.cancel();
+
+ // The second request is allowed to complete.
+ continueResponses();
+ download.start().catch(() => {});
+
+ // Wait for the download to finish by waiting on the whenSucceeded promise.
+ yield promiseSucceeded;
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+});
+
+/**
+ * Ensures download error details are reported on network failures.
+ */
+add_task(function* test_error_source()
+{
+ let serverSocket = startFakeServer();
+ try {
+ let sourceUrl = "http://localhost:" + serverSocket.port + "/source.txt";
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload(sourceUrl);
+
+ do_check_true(download.error === null);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when reading from the source fails.
+ }
+
+ // Check the properties now that the download stopped.
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+ } finally {
+ serverSocket.close();
+ }
+});
+
+/**
+ * Ensures a download error is reported when receiving less bytes than what was
+ * specified in the Content-Length header.
+ */
+add_task(function* test_error_source_partial()
+{
+ let sourceUrl = httpUrl("shorter-than-content-length-http-1-1.txt");
+
+ let enforcePref = Services.prefs.getBoolPref("network.http.enforce-framing.http1");
+ Services.prefs.setBoolPref("network.http.enforce-framing.http1", true);
+
+ function cleanup() {
+ Services.prefs.setBoolPref("network.http.enforce-framing.http1", enforcePref);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload(sourceUrl);
+
+ do_check_true(download.error === null);
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when reading from the source fails.
+ }
+
+ // Check the properties now that the download stopped.
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+ do_check_eq(download.error.result, Cr.NS_ERROR_NET_PARTIAL_TRANSFER);
+
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Ensures download error details are reported on local writing failures.
+ */
+add_task(function* test_error_target()
+{
+ // Create a file without write access permissions before downloading.
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
+ try {
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: targetFile,
+ });
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload(null,
+ { targetFile: targetFile });
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseTargetFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when writing to the target fails.
+ }
+
+ // Check the properties now that the download stopped.
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseTargetFailed);
+ do_check_false(download.error.becauseSourceFailed);
+ } finally {
+ // Restore the default permissions to allow deleting the file on Windows.
+ if (targetFile.exists()) {
+ targetFile.permissions = FileUtils.PERMS_FILE;
+ targetFile.remove(false);
+ }
+ }
+});
+
+/**
+ * Restarts a failed download.
+ */
+add_task(function* test_error_restart()
+{
+ let download;
+
+ // Create a file without write access permissions before downloading.
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
+ try {
+ // Use DownloadCopySaver or DownloadLegacySaver based on the test run,
+ // specifying the target file we created.
+ if (!gUseLegacySaver) {
+ download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: targetFile,
+ });
+ download.start().catch(() => {});
+ } else {
+ download = yield promiseStartLegacyDownload(null,
+ { targetFile: targetFile });
+ }
+ yield promiseDownloadStopped(download);
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseTargetFailed) {
+ throw ex;
+ }
+ // A specific error object is thrown when writing to the target fails.
+ } finally {
+ // Restore the default permissions to allow deleting the file on Windows.
+ if (targetFile.exists()) {
+ targetFile.permissions = FileUtils.PERMS_FILE;
+
+ // Also for Windows, rename the file before deleting. This makes the
+ // current file name available immediately for a new file, while deleting
+ // in place prevents creation of a file with the same name for some time.
+ targetFile.moveTo(null, targetFile.leafName + ".delete.tmp");
+ targetFile.remove(false);
+ }
+ }
+
+ // Restart the download and wait for completion.
+ yield download.start();
+
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.canceled);
+ do_check_true(download.error === null);
+ do_check_eq(download.progress, 100);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Executes download in both public and private modes.
+ */
+add_task(function* test_public_and_private()
+{
+ let sourcePath = "/test_public_and_private.txt";
+ let sourceUrl = httpUrl("test_public_and_private.txt");
+ let testCount = 0;
+
+ // Apply pref to allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ function cleanup() {
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+ Services.cookies.removeAll();
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ if (testCount == 0) {
+ // No cookies should exist for first public download.
+ do_check_false(aRequest.hasHeader("Cookie"));
+ aResponse.setHeader("Set-Cookie", "foobar=1", false);
+ testCount++;
+ } else if (testCount == 1) {
+ // The cookie should exists for second public download.
+ do_check_true(aRequest.hasHeader("Cookie"));
+ do_check_eq(aRequest.getHeader("Cookie"), "foobar=1");
+ testCount++;
+ } else if (testCount == 2) {
+ // No cookies should exist for first private download.
+ do_check_false(aRequest.hasHeader("Cookie"));
+ }
+ });
+
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ yield Downloads.fetch(sourceUrl, targetFile);
+ yield Downloads.fetch(sourceUrl, targetFile);
+
+ if (!gUseLegacySaver) {
+ let download = yield Downloads.createDownload({
+ source: { url: sourceUrl, isPrivate: true },
+ target: targetFile,
+ });
+ yield download.start();
+ } else {
+ let download = yield promiseStartLegacyDownload(sourceUrl,
+ { isPrivate: true });
+ yield promiseDownloadStopped(download);
+ }
+
+ cleanup();
+});
+
+/**
+ * Checks the startTime gets updated even after a restart.
+ */
+add_task(function* test_cancel_immediately_restart_and_check_startTime()
+{
+ let download = yield promiseStartDownload();
+
+ let startTime = download.startTime;
+ do_check_true(isValidDate(download.startTime));
+
+ yield download.cancel();
+ do_check_eq(download.startTime.getTime(), startTime.getTime());
+
+ // Wait for a timeout.
+ yield promiseTimeout(10);
+
+ yield download.start();
+ do_check_true(download.startTime.getTime() > startTime.getTime());
+});
+
+/**
+ * Executes download with content-encoding.
+ */
+add_task(function* test_with_content_encoding()
+{
+ let sourcePath = "/test_with_content_encoding.txt";
+ let sourceUrl = httpUrl("test_with_content_encoding.txt");
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Encoding", "gzip", false);
+ aResponse.setHeader("Content-Length",
+ "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
+ TEST_DATA_SHORT_GZIP_ENCODED.length);
+ });
+
+ let download = yield promiseStartDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ // Ensure the content matches the decoded test data.
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+
+ cleanup();
+});
+
+/**
+ * Checks that the file is not decoded if the extension matches the encoding.
+ */
+add_task(function* test_with_content_encoding_ignore_extension()
+{
+ let sourcePath = "/test_with_content_encoding_ignore_extension.gz";
+ let sourceUrl = httpUrl("test_with_content_encoding_ignore_extension.gz");
+
+ function cleanup() {
+ gHttpServer.registerPathHandler(sourcePath, null);
+ }
+ do_register_cleanup(cleanup);
+
+ gHttpServer.registerPathHandler(sourcePath, function (aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Encoding", "gzip", false);
+ aResponse.setHeader("Content-Length",
+ "" + TEST_DATA_SHORT_GZIP_ENCODED.length, false);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED,
+ TEST_DATA_SHORT_GZIP_ENCODED.length);
+ });
+
+ let download = yield promiseStartDownload(sourceUrl);
+ yield promiseDownloadStopped(download);
+
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+ do_check_eq(download.target.size, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ // Ensure the content matches the encoded test data. We convert the data to a
+ // string before executing the content check.
+ yield promiseVerifyTarget(download.target,
+ String.fromCharCode.apply(String, TEST_DATA_SHORT_GZIP_ENCODED));
+
+ cleanup();
+});
+
+/**
+ * Cancels and restarts a download sequentially with content-encoding.
+ */
+add_task(function* test_cancel_midway_restart_with_content_encoding()
+{
+ mustInterruptResponses();
+
+ let download = yield promiseStartDownload(httpUrl("interruptible_gzip.txt"));
+
+ // The first time, cancel the download midway.
+ let deferCancel = Promise.defer();
+ let onchange = function () {
+ if (!download.stopped && !download.canceled &&
+ download.currentBytes == TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length) {
+ deferCancel.resolve(download.cancel());
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ download.onchange = onchange;
+ onchange();
+
+ yield deferCancel.promise;
+
+ do_check_true(download.stopped);
+
+ // The second time, we'll provide the entire interruptible response.
+ continueResponses();
+ download.onchange = null;
+ yield download.start();
+
+ do_check_eq(download.progress, 100);
+ do_check_eq(download.totalBytes, TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT);
+});
+
+/**
+ * Download with parental controls enabled.
+ */
+add_task(function* test_blocked_parental_controls()
+{
+ let blockFn = base => ({
+ shouldBlockForParentalControls: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload();
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload();
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByParentalControls);
+ do_check_true(download.error.becauseBlockedByParentalControls);
+ }
+
+ // Now that the download stopped, the target file should not exist.
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ cleanup();
+});
+
+/**
+ * Test a download that will be blocked by Windows parental controls by
+ * resulting in an HTTP status code of 450.
+ */
+add_task(function* test_blocked_parental_controls_httpstatus450()
+{
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ download = yield promiseNewDownload(httpUrl("parentalblocked.zip"));
+ yield download.start();
+ }
+ else {
+ download = yield promiseStartLegacyDownload(httpUrl("parentalblocked.zip"));
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByParentalControls);
+ do_check_true(download.error.becauseBlockedByParentalControls);
+ do_check_true(download.stopped);
+ }
+
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Download with runtime permissions
+ */
+add_task(function* test_blocked_runtime_permissions()
+{
+ let blockFn = base => ({
+ shouldBlockForRuntimePermissions: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+ try {
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we want to check that the promise
+ // returned by the "start" method is rejected.
+ download = yield promiseNewDownload();
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, we cannot be sure whether we are
+ // testing the promise returned by the "start" method or we are testing
+ // the "error" property checked by promiseDownloadStopped. This happens
+ // because we don't have control over when the download is started.
+ download = yield promiseStartLegacyDownload();
+ yield promiseDownloadStopped(download);
+ }
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByRuntimePermissions);
+ do_check_true(download.error.becauseBlockedByRuntimePermissions);
+ }
+
+ // Now that the download stopped, the target file should not exist.
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ cleanup();
+});
+
+/**
+ * Check that DownloadCopySaver can always retrieve the hash.
+ * DownloadLegacySaver can only retrieve the hash when
+ * nsIExternalHelperAppService is invoked.
+ */
+add_task(function* test_getSha256Hash()
+{
+ if (!gUseLegacySaver) {
+ let download = yield promiseStartDownload(httpUrl("source.txt"));
+ yield promiseDownloadStopped(download);
+ do_check_true(download.stopped);
+ do_check_eq(32, download.saver.getSha256Hash().length);
+ }
+});
+
+/**
+ * Create a download which will be reputation blocked.
+ *
+ * @param options
+ * {
+ * keepPartialData: bool,
+ * keepBlockedData: bool,
+ * }
+ * @return {Promise}
+ * @resolves The reputation blocked download.
+ * @rejects JavaScript exception.
+ */
+var promiseBlockedDownload = Task.async(function* (options) {
+ let blockFn = base => ({
+ shouldBlockForReputationCheck: () => Promise.resolve({
+ shouldBlock: true,
+ verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ }),
+ shouldKeepBlockedData: () => Promise.resolve(options.keepBlockedData),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+
+ try {
+ if (options.keepPartialData) {
+ download = yield promiseStartDownload_tryToKeepPartialData();
+ continueResponses();
+ } else if (gUseLegacySaver) {
+ download = yield promiseStartLegacyDownload();
+ } else {
+ download = yield promiseNewDownload();
+ yield download.start();
+ do_throw("The download should have blocked.");
+ }
+
+ yield promiseDownloadStopped(download);
+ do_throw("The download should have blocked.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
+ throw ex;
+ }
+ do_check_true(ex.becauseBlockedByReputationCheck);
+ do_check_eq(ex.reputationCheckVerdict,
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON);
+ do_check_true(download.error.becauseBlockedByReputationCheck);
+ do_check_eq(download.error.reputationCheckVerdict,
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON);
+ }
+
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(yield OS.File.exists(download.target.path));
+
+ cleanup();
+ return download;
+});
+
+/**
+ * Checks that application reputation blocks the download and the target file
+ * does not exist.
+ */
+add_task(function* test_blocked_applicationReputation()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: false,
+ keepBlockedData: false,
+ });
+
+ // Now that the download is blocked, the target file should not exist.
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+
+ // There should also be no blocked data in this case
+ do_check_false(download.hasBlockedData);
+});
+
+/**
+ * Checks that if a download restarts while processing an application reputation
+ * request, the status is handled correctly.
+ */
+add_task(function* test_blocked_applicationReputation_race()
+{
+ let isFirstShouldBlockCall = true;
+
+ let blockFn = base => ({
+ shouldBlockForReputationCheck(download) {
+ if (isFirstShouldBlockCall) {
+ isFirstShouldBlockCall = false;
+
+ // 2. Cancel and restart the download before the first attempt has a
+ // chance to finish.
+ download.cancel();
+ download.removePartialData();
+ download.start();
+
+ // 3. Allow the first attempt to finish with a blocked response.
+ return Promise.resolve({
+ shouldBlock: true,
+ verdict: Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ });
+ }
+
+ // 4/5. Don't block the download the second time. The race condition would
+ // occur with the first attempt regardless of whether the second one
+ // is blocked, but not blocking here makes the test simpler.
+ return Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ });
+ },
+ shouldKeepBlockedData: () => Promise.resolve(true),
+ });
+
+ Integration.downloads.register(blockFn);
+ function cleanup() {
+ Integration.downloads.unregister(blockFn);
+ }
+ do_register_cleanup(cleanup);
+
+ let download;
+
+ try {
+ // 1. Start the download and get a reference to the promise asociated with
+ // the first attempt, before allowing the response to continue.
+ download = yield promiseStartDownload_tryToKeepPartialData();
+ let firstAttempt = promiseDownloadStopped(download);
+ continueResponses();
+
+ // 4/5. Wait for the first attempt to be completed. The result of this
+ // should appear as a cancellation.
+ yield firstAttempt;
+
+ do_throw("The first attempt should have been canceled.");
+ } catch (ex) {
+ // The "becauseBlocked" property should be false.
+ if (!(ex instanceof Downloads.Error) || ex.becauseBlocked) {
+ throw ex;
+ }
+ }
+
+ // 6. Wait for the second attempt to be completed.
+ yield promiseDownloadStopped(download);
+
+ // 7. At this point, "hasBlockedData" should be false.
+ do_check_false(download.hasBlockedData);
+
+ cleanup();
+});
+
+/**
+ * Checks that application reputation blocks the download but maintains the
+ * blocked data, which will be deleted when the block is confirmed.
+ */
+add_task(function* test_blocked_applicationReputation_confirmBlock()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ do_check_true(download.hasBlockedData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ yield download.confirmBlock();
+
+ // After confirming the block the download should be in a failed state and
+ // have no downloaded data left on disk.
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Checks that application reputation blocks the download but maintains the
+ * blocked data, which will be used to complete the download when unblocking.
+ */
+add_task(function* test_blocked_applicationReputation_unblock()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ do_check_true(download.hasBlockedData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ yield download.unblock();
+
+ // After unblocking the download should have succeeded and be
+ // present at the final path.
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ yield promiseVerifyTarget(download.target, TEST_DATA_SHORT + TEST_DATA_SHORT);
+
+ // The only indication the download was previously blocked is the
+ // existence of the error, so we make sure it's still set.
+ do_check_true(download.error instanceof Downloads.Error);
+ do_check_true(download.error.becauseBlocked);
+ do_check_true(download.error.becauseBlockedByReputationCheck);
+});
+
+/**
+ * Check that calling cancel on a blocked download will not cause errors
+ */
+add_task(function* test_blocked_applicationReputation_cancel()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ // This call should succeed on a blocked download.
+ yield download.cancel();
+
+ // Calling cancel should not have changed the current state, the download
+ // should still be blocked.
+ do_check_true(download.error.becauseBlockedByReputationCheck);
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_true(download.hasBlockedData);
+});
+
+/**
+ * Checks that unblock and confirmBlock cannot race on a blocked download
+ */
+add_task(function* test_blocked_applicationReputation_decisionRace()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ let unblockPromise = download.unblock();
+ let confirmBlockPromise = download.confirmBlock();
+
+ yield confirmBlockPromise.then(() => {
+ do_throw("confirmBlock should have failed.");
+ }, () => {});
+
+ yield unblockPromise;
+
+ // After unblocking the download should have succeeded and be
+ // present at the final path.
+ do_check_true(download.stopped);
+ do_check_true(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_true(yield OS.File.exists(download.target.path));
+
+ download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ confirmBlockPromise = download.confirmBlock();
+ unblockPromise = download.unblock();
+
+ yield unblockPromise.then(() => {
+ do_throw("unblock should have failed.");
+ }, () => {});
+
+ yield confirmBlockPromise;
+
+ // After confirming the block the download should be in a failed state and
+ // have no downloaded data left on disk.
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.hasBlockedData);
+ do_check_false(yield OS.File.exists(download.target.partFilePath));
+ do_check_false(yield OS.File.exists(download.target.path));
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * Checks that unblocking a blocked download fails if the blocked data has been
+ * removed.
+ */
+add_task(function* test_blocked_applicationReputation_unblock()
+{
+ let download = yield promiseBlockedDownload({
+ keepPartialData: true,
+ keepBlockedData: true,
+ });
+
+ do_check_true(download.hasBlockedData);
+ do_check_true(yield OS.File.exists(download.target.partFilePath));
+
+ // Remove the blocked data without telling the download.
+ yield OS.File.remove(download.target.partFilePath);
+
+ let unblockPromise = download.unblock();
+ yield unblockPromise.then(() => {
+ do_throw("unblock should have failed.");
+ }, () => {});
+
+ // Even though unblocking failed the download state should have been updated
+ // to reflect the lack of blocked data.
+ do_check_false(download.hasBlockedData);
+ do_check_true(download.stopped);
+ do_check_false(download.succeeded);
+ do_check_false(download.target.exists);
+ do_check_eq(download.target.size, 0);
+});
+
+/**
+ * download.showContainingDirectory() action
+ */
+add_task(function* test_showContainingDirectory() {
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ let download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: ""
+ });
+
+ let promiseDirectoryShown = waitForDirectoryShown();
+ yield download.showContainingDirectory();
+ let path = yield promiseDirectoryShown;
+ try {
+ new FileUtils.File(path);
+ do_throw("Should have failed because of an invalid path.");
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception)) {
+ throw ex;
+ }
+ // Invalid paths on Windows are reported with NS_ERROR_FAILURE,
+ // but with NS_ERROR_FILE_UNRECOGNIZED_PATH on Mac/Linux
+ let validResult = ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH ||
+ ex.result == Cr.NS_ERROR_FAILURE;
+ do_check_true(validResult);
+ }
+
+ download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: targetPath
+ });
+
+ promiseDirectoryShown = waitForDirectoryShown();
+ download.showContainingDirectory();
+ yield promiseDirectoryShown;
+});
+
+/**
+ * download.launch() action
+ */
+add_task(function* test_launch() {
+ let customLauncher = getTempFile("app-launcher");
+
+ // Test both with and without setting a custom application.
+ for (let launcherPath of [null, customLauncher.path]) {
+ let download;
+ if (!gUseLegacySaver) {
+ // When testing DownloadCopySaver, we have control over the download, thus
+ // we can test that file is not launched if download.succeeded is not set.
+ download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launcherPath: launcherPath,
+ launchWhenSucceeded: true
+ });
+
+ try {
+ yield download.launch();
+ do_throw("Can't launch download file as it has not completed yet");
+ } catch (ex) {
+ do_check_eq(ex.message,
+ "launch can only be called if the download succeeded");
+ }
+
+ yield download.start();
+ } else {
+ // When testing DownloadLegacySaver, the download is already started when
+ // it is created, thus we don't test calling "launch" before starting.
+ download = yield promiseStartLegacyDownload(
+ httpUrl("source.txt"),
+ { launcherPath: launcherPath,
+ launchWhenSucceeded: true });
+ yield promiseDownloadStopped(download);
+ }
+
+ do_check_true(download.launchWhenSucceeded);
+
+ let promiseFileLaunched = waitForFileLaunched();
+ download.launch();
+ let result = yield promiseFileLaunched;
+
+ // Verify that the results match the test case.
+ if (!launcherPath) {
+ // This indicates that the default handler has been chosen.
+ do_check_true(result === null);
+ } else {
+ // Check the nsIMIMEInfo instance that would have been used for launching.
+ do_check_eq(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
+ do_check_true(result.preferredApplicationHandler
+ .QueryInterface(Ci.nsILocalHandlerApp)
+ .executable.equals(customLauncher));
+ }
+ }
+});
+
+/**
+ * Test passing an invalid path as the launcherPath property.
+ */
+add_task(function* test_launcherPath_invalid() {
+ let download = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt") },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ launcherPath: " "
+ });
+
+ let promiseDownloadLaunched = new Promise(resolve => {
+ let waitFn = base => ({
+ __proto__: base,
+ launchDownload() {
+ Integration.downloads.unregister(waitFn);
+ let superPromise = super.launchDownload(...arguments);
+ resolve(superPromise);
+ return superPromise;
+ },
+ });
+ Integration.downloads.register(waitFn);
+ });
+
+ yield download.start();
+ try {
+ download.launch();
+ yield promiseDownloadLaunched;
+ do_throw("Can't launch file with invalid custom launcher")
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception)) {
+ throw ex;
+ }
+ // Invalid paths on Windows are reported with NS_ERROR_FAILURE,
+ // but with NS_ERROR_FILE_UNRECOGNIZED_PATH on Mac/Linux
+ let validResult = ex.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH ||
+ ex.result == Cr.NS_ERROR_FAILURE;
+ do_check_true(validResult);
+ }
+});
+
+/**
+ * Tests that download.launch() is automatically called after
+ * the download finishes if download.launchWhenSucceeded = true
+ */
+add_task(function* test_launchWhenSucceeded() {
+ let customLauncher = getTempFile("app-launcher");
+
+ // Test both with and without setting a custom application.
+ for (let launcherPath of [null, customLauncher.path]) {
+ let promiseFileLaunched = waitForFileLaunched();
+
+ if (!gUseLegacySaver) {
+ let download = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ launchWhenSucceeded: true,
+ launcherPath: launcherPath,
+ });
+ yield download.start();
+ } else {
+ let download = yield promiseStartLegacyDownload(
+ httpUrl("source.txt"),
+ { launcherPath: launcherPath,
+ launchWhenSucceeded: true });
+ yield promiseDownloadStopped(download);
+ }
+
+ let result = yield promiseFileLaunched;
+
+ // Verify that the results match the test case.
+ if (!launcherPath) {
+ // This indicates that the default handler has been chosen.
+ do_check_true(result === null);
+ } else {
+ // Check the nsIMIMEInfo instance that would have been used for launching.
+ do_check_eq(result.preferredAction, Ci.nsIMIMEInfo.useHelperApp);
+ do_check_true(result.preferredApplicationHandler
+ .QueryInterface(Ci.nsILocalHandlerApp)
+ .executable.equals(customLauncher));
+ }
+ }
+});
+
+/**
+ * Tests that the proper content type is set for a normal download.
+ */
+add_task(function* test_contentType() {
+ let download = yield promiseStartDownload(httpUrl("source.txt"));
+ yield promiseDownloadStopped(download);
+
+ do_check_eq("text/plain", download.contentType);
+});
+
+/**
+ * Tests that the serialization/deserialization of the startTime Date
+ * object works correctly.
+ */
+add_task(function* test_toSerializable_startTime()
+{
+ let download1 = yield promiseStartDownload(httpUrl("source.txt"));
+ yield promiseDownloadStopped(download1);
+
+ let serializable = download1.toSerializable();
+ let reserialized = JSON.parse(JSON.stringify(serializable));
+
+ let download2 = yield Downloads.createDownload(reserialized);
+
+ do_check_eq(download1.startTime.constructor.name, "Date");
+ do_check_eq(download2.startTime.constructor.name, "Date");
+ do_check_eq(download1.startTime.toJSON(), download2.startTime.toJSON());
+});
+
+/**
+ * Checks that downloads are added to browsing history when they start.
+ */
+add_task(function* test_history()
+{
+ mustInterruptResponses();
+
+ // We will wait for the visit to be notified during the download.
+ yield PlacesTestUtils.clearHistory();
+ let promiseVisit = promiseWaitForVisit(httpUrl("interruptible.txt"));
+
+ // Start a download that is not allowed to finish yet.
+ let download = yield promiseStartDownload(httpUrl("interruptible.txt"));
+
+ // The history notifications should be received before the download completes.
+ let [time, transitionType] = yield promiseVisit;
+ do_check_eq(time, download.startTime.getTime() * 1000);
+ do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+ // Restart and complete the download after clearing history.
+ yield PlacesTestUtils.clearHistory();
+ download.cancel();
+ continueResponses();
+ yield download.start();
+
+ // The restart should not have added a new history visit.
+ do_check_false(yield promiseIsURIVisited(httpUrl("interruptible.txt")));
+});
+
+/**
+ * Checks that downloads started by nsIHelperAppService are added to the
+ * browsing history when they start.
+ */
+add_task(function* test_history_tryToKeepPartialData()
+{
+ // We will wait for the visit to be notified during the download.
+ yield PlacesTestUtils.clearHistory();
+ let promiseVisit =
+ promiseWaitForVisit(httpUrl("interruptible_resumable.txt"));
+
+ // Start a download that is not allowed to finish yet.
+ let beforeStartTimeMs = Date.now();
+ let download = yield promiseStartDownload_tryToKeepPartialData();
+
+ // The history notifications should be received before the download completes.
+ let [time, transitionType] = yield promiseVisit;
+ do_check_eq(transitionType, Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+
+ // The time set by nsIHelperAppService may be different than the start time in
+ // the download object, thus we only check that it is a meaningful time. Note
+ // that we subtract one second from the earliest time to account for rounding.
+ do_check_true(time >= beforeStartTimeMs * 1000 - 1000000);
+
+ // Complete the download before finishing the test.
+ continueResponses();
+ yield promiseDownloadStopped(download);
+});
+
+/**
+ * Tests that the temp download files are removed on exit and exiting private
+ * mode after they have been launched.
+ */
+add_task(function* test_launchWhenSucceeded_deleteTempFileOnExit() {
+ let customLauncherPath = getTempFile("app-launcher").path;
+ let autoDeleteTargetPathOne = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let autoDeleteTargetPathTwo = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let noAutoDeleteTargetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+
+ let autoDeleteDownloadOne = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: autoDeleteTargetPathOne,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ yield autoDeleteDownloadOne.start();
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, true);
+ let autoDeleteDownloadTwo = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: autoDeleteTargetPathTwo,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ yield autoDeleteDownloadTwo.start();
+
+ Services.prefs.setBoolPref(kDeleteTempFileOnExit, false);
+ let noAutoDeleteDownload = yield Downloads.createDownload({
+ source: httpUrl("source.txt"),
+ target: noAutoDeleteTargetPath,
+ launchWhenSucceeded: true,
+ launcherPath: customLauncherPath,
+ });
+ yield noAutoDeleteDownload.start();
+
+ Services.prefs.clearUserPref(kDeleteTempFileOnExit);
+
+ do_check_true(yield OS.File.exists(autoDeleteTargetPathOne));
+ do_check_true(yield OS.File.exists(autoDeleteTargetPathTwo));
+ do_check_true(yield OS.File.exists(noAutoDeleteTargetPath));
+
+ // Simulate leaving private browsing mode
+ Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+ do_check_false(yield OS.File.exists(autoDeleteTargetPathOne));
+
+ // Simulate browser shutdown
+ let expire = Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Ci.nsIObserver);
+ expire.observe(null, "profile-before-change", null);
+ do_check_false(yield OS.File.exists(autoDeleteTargetPathTwo));
+ do_check_true(yield OS.File.exists(noAutoDeleteTargetPath));
+});
diff --git a/toolkit/components/jsdownloads/test/unit/head.js b/toolkit/components/jsdownloads/test/unit/head.js
new file mode 100644
index 0000000000..f322244c48
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -0,0 +1,843 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
+ "resource://testing-common/MockRegistrar.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsIExternalHelperAppService);
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+const ServerSocket = Components.Constructor(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init");
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream")
+
+XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService");
+
+const TEST_TARGET_FILE_NAME = "test-download.txt";
+const TEST_STORE_FILE_NAME = "test-downloads.json";
+
+const TEST_REFERRER_URL = "http://www.example.com/referrer.html";
+
+const TEST_DATA_SHORT = "This test string is downloaded.";
+// Generate using gzipCompressString in TelemetryController.jsm.
+const TEST_DATA_SHORT_GZIP_ENCODED_FIRST = [
+ 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 11, 201, 200, 44, 86, 40, 73, 45, 46, 81, 40, 46, 41, 202, 204
+];
+const TEST_DATA_SHORT_GZIP_ENCODED_SECOND = [
+ 75, 87, 0, 114, 83, 242, 203, 243, 114, 242, 19, 83, 82, 83, 244, 0, 151, 222, 109, 43, 31, 0, 0, 0
+];
+const TEST_DATA_SHORT_GZIP_ENCODED =
+ TEST_DATA_SHORT_GZIP_ENCODED_FIRST.concat(TEST_DATA_SHORT_GZIP_ENCODED_SECOND);
+
+/**
+ * All the tests are implemented with add_task, this starts them automatically.
+ */
+function run_test()
+{
+ do_get_profile();
+ run_next_test();
+}
+
+// Support functions
+
+/**
+ * HttpServer object initialized before tests start.
+ */
+var gHttpServer;
+
+/**
+ * Given a file name, returns a string containing an URI that points to the file
+ * on the currently running instance of the test HTTP server.
+ */
+function httpUrl(aFileName) {
+ return "http://localhost:" + gHttpServer.identity.primaryPort + "/" +
+ aFileName;
+}
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+var gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function () {
+ try {
+ file.remove(false)
+ } catch (e) {
+ if (!(e instanceof Components.Exception &&
+ (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
+ e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST ||
+ e.result == Cr.NS_ERROR_FILE_NOT_FOUND))) {
+ throw e;
+ }
+ // On Windows, we may get an access denied error if the file existed before,
+ // and was recently deleted.
+ // Don't bother checking file.exists() as that may also cause an access
+ // denied error.
+ }
+ });
+
+ return file;
+}
+
+/**
+ * Waits for pending events to be processed.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+function promiseExecuteSoon()
+{
+ let deferred = Promise.defer();
+ do_execute_soon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Waits for a pending events to be processed after a timeout.
+ *
+ * @return {Promise}
+ * @resolves When pending events have been processed.
+ * @rejects Never.
+ */
+function promiseTimeout(aTime)
+{
+ let deferred = Promise.defer();
+ do_timeout(aTime, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Waits for a new history visit to be notified for the specified URI.
+ *
+ * @param aUrl
+ * String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Array [aTime, aTransitionType] from nsINavHistoryObserver.onVisit.
+ * @rejects Never.
+ */
+function promiseWaitForVisit(aUrl)
+{
+ let deferred = Promise.defer();
+
+ let uri = NetUtil.newURI(aUrl);
+
+ PlacesUtils.history.addObserver({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType, aGUID, aHidden) {
+ if (aURI.equals(uri)) {
+ PlacesUtils.history.removeObserver(this);
+ deferred.resolve([aTime, aTransitionType]);
+ }
+ },
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ }, false);
+
+ return deferred.promise;
+}
+
+/**
+ * Check browsing history to see whether the given URI has been visited.
+ *
+ * @param aUrl
+ * String containing the URI that will be visited.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the URI has been visited.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aUrl) {
+ let deferred = Promise.defer();
+
+ PlacesUtils.asyncHistory.isURIVisited(NetUtil.newURI(aUrl),
+ function (aURI, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Creates a new Download object, setting a temporary file as the target.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object.
+ * @rejects JavaScript exception.
+ */
+function promiseNewDownload(aSourceUrl) {
+ return Downloads.createDownload({
+ source: aSourceUrl || httpUrl("source.txt"),
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ });
+}
+
+/**
+ * Starts a new download using the nsIWebBrowserPersist interface, and controls
+ * it using the legacy nsITransfer interface.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ * @param aOptions
+ * An optional object used to control the behavior of this function.
+ * You may pass an object with a subset of the following fields:
+ * {
+ * isPrivate: Boolean indicating whether the download originated from a
+ * private window.
+ * targetFile: nsIFile for the target, or null to use a temporary file.
+ * outPersist: Receives a reference to the created nsIWebBrowserPersist
+ * instance.
+ * launchWhenSucceeded: Boolean indicating whether the target should
+ * be launched when it has completed successfully.
+ * launcherPath: String containing the path of the custom executable to
+ * use to launch the target of the download.
+ * }
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ * download through the legacy nsITransfer interface.
+ * @rejects Never. The current test fails in case of exceptions.
+ */
+function promiseStartLegacyDownload(aSourceUrl, aOptions) {
+ let sourceURI = NetUtil.newURI(aSourceUrl || httpUrl("source.txt"));
+ let targetFile = (aOptions && aOptions.targetFile)
+ || getTempFile(TEST_TARGET_FILE_NAME);
+
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ if (aOptions) {
+ aOptions.outPersist = persist;
+ }
+
+ let fileExtension = null, mimeInfo = null;
+ let match = sourceURI.path.match(/\.([^.\/]+)$/);
+ if (match) {
+ fileExtension = match[1];
+ }
+
+ if (fileExtension) {
+ try {
+ mimeInfo = gMIMEService.getFromTypeAndExtension(null, fileExtension);
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+ } catch (ex) { }
+ }
+
+ if (aOptions && aOptions.launcherPath) {
+ do_check_true(mimeInfo != null);
+
+ let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.executable = new FileUtils.File(aOptions.launcherPath);
+
+ mimeInfo.preferredApplicationHandler = localHandlerApp;
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ }
+
+ if (aOptions && aOptions.launchWhenSucceeded) {
+ do_check_true(mimeInfo != null);
+
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ }
+
+ // Apply decoding if required by the "Content-Encoding" header.
+ persist.persistFlags &= ~Ci.nsIWebBrowserPersist.PERSIST_FLAGS_NO_CONVERSION;
+ persist.persistFlags |=
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ let transfer = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
+
+ let deferred = Promise.defer();
+
+ Downloads.getList(Downloads.ALL).then(function (aList) {
+ // Temporarily register a view that will get notified when the download we
+ // are controlling becomes visible in the list of downloads.
+ aList.addView({
+ onDownloadAdded: function (aDownload) {
+ aList.removeView(this).then(null, do_report_unexpected_exception);
+
+ // Remove the download to keep the list empty for the next test. This
+ // also allows the caller to register the "onchange" event directly.
+ let promise = aList.remove(aDownload);
+
+ // When the download object is ready, make it available to the caller.
+ promise.then(() => deferred.resolve(aDownload),
+ do_report_unexpected_exception);
+ },
+ }).then(null, do_report_unexpected_exception);
+
+ let isPrivate = aOptions && aOptions.isPrivate;
+
+ // Initialize the components so they reference each other. This will cause
+ // the Download object to be created and added to the public downloads.
+ transfer.init(sourceURI, NetUtil.newURI(targetFile), null, mimeInfo, null,
+ null, persist, isPrivate);
+ persist.progressListener = transfer;
+
+ // Start the actual download process.
+ persist.savePrivacyAwareURI(sourceURI, null, null, 0, null, null, targetFile,
+ isPrivate);
+ }.bind(this)).then(null, do_report_unexpected_exception);
+
+ return deferred.promise;
+}
+
+/**
+ * Starts a new download using the nsIHelperAppService interface, and controls
+ * it using the legacy nsITransfer interface. The source of the download will
+ * be "interruptible_resumable.txt" and partially downloaded data will be kept.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("interruptible_resumable.txt").
+ *
+ * @return {Promise}
+ * @resolves The Download object created as a consequence of controlling the
+ * download through the legacy nsITransfer interface.
+ * @rejects Never. The current test fails in case of exceptions.
+ */
+function promiseStartExternalHelperAppServiceDownload(aSourceUrl) {
+ let sourceURI = NetUtil.newURI(aSourceUrl ||
+ httpUrl("interruptible_resumable.txt"));
+
+ let deferred = Promise.defer();
+
+ Downloads.getList(Downloads.PUBLIC).then(function (aList) {
+ // Temporarily register a view that will get notified when the download we
+ // are controlling becomes visible in the list of downloads.
+ aList.addView({
+ onDownloadAdded: function (aDownload) {
+ aList.removeView(this).then(null, do_report_unexpected_exception);
+
+ // Remove the download to keep the list empty for the next test. This
+ // also allows the caller to register the "onchange" event directly.
+ let promise = aList.remove(aDownload);
+
+ // When the download object is ready, make it available to the caller.
+ promise.then(() => deferred.resolve(aDownload),
+ do_report_unexpected_exception);
+ },
+ }).then(null, do_report_unexpected_exception);
+
+ let channel = NetUtil.newChannel({
+ uri: sourceURI,
+ loadUsingSystemPrincipal: true
+ });
+
+ // Start the actual download process.
+ channel.asyncOpen2({
+ contentListener: null,
+
+ onStartRequest: function (aRequest, aContext)
+ {
+ let requestChannel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.contentListener = gExternalHelperAppService.doContent(
+ requestChannel.contentType, aRequest, null, true);
+ this.contentListener.onStartRequest(aRequest, aContext);
+ },
+
+ onStopRequest: function (aRequest, aContext, aStatusCode)
+ {
+ this.contentListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+
+ onDataAvailable: function (aRequest, aContext, aInputStream, aOffset,
+ aCount)
+ {
+ this.contentListener.onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount);
+ },
+ });
+ }.bind(this)).then(null, do_report_unexpected_exception);
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for a download to reach half of its progress, in case it has not
+ * reached the expected progress already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has reached half of its progress.
+ * @rejects Never.
+ */
+function promiseDownloadMidway(aDownload) {
+ let deferred = Promise.defer();
+
+ // Wait for the download to reach half of its progress.
+ let onchange = function () {
+ if (!aDownload.stopped && !aDownload.canceled && aDownload.progress == 50) {
+ aDownload.onchange = null;
+ deferred.resolve();
+ }
+ };
+
+ // Register for the notification, but also call the function directly in
+ // case the download already reached the expected progress.
+ aDownload.onchange = onchange;
+ onchange();
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ * The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+ if (!aDownload.stopped) {
+ // The download is in progress, wait for the current attempt to finish and
+ // report any errors that may occur.
+ return aDownload.start();
+ }
+
+ if (aDownload.succeeded) {
+ return Promise.resolve();
+ }
+
+ // The download failed or was canceled.
+ return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+/**
+ * Returns a new public or private DownloadList object.
+ *
+ * @param aIsPrivate
+ * True for the private list, false or undefined for the public list.
+ *
+ * @return {Promise}
+ * @resolves The newly created DownloadList object.
+ * @rejects JavaScript exception.
+ */
+function promiseNewList(aIsPrivate)
+{
+ // We need to clear all the internal state for the list and summary objects,
+ // since all the objects are interdependent internally.
+ Downloads._promiseListsInitialized = null;
+ Downloads._lists = {};
+ Downloads._summaries = {};
+
+ return Downloads.getList(aIsPrivate ? Downloads.PRIVATE : Downloads.PUBLIC);
+}
+
+/**
+ * Ensures that the given file contents are equal to the given string.
+ *
+ * @param aPath
+ * String containing the path of the file whose contents should be
+ * verified.
+ * @param aExpectedContents
+ * String containing the octets that are expected in the file.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects Never.
+ */
+function promiseVerifyContents(aPath, aExpectedContents)
+{
+ return Task.spawn(function* () {
+ let file = new FileUtils.File(aPath);
+
+ if (!(yield OS.File.exists(aPath))) {
+ do_throw("File does not exist: " + aPath);
+ }
+
+ if ((yield OS.File.stat(aPath)).size == 0) {
+ do_throw("File is empty: " + aPath);
+ }
+
+ let deferred = Promise.defer();
+ NetUtil.asyncFetch(
+ { uri: NetUtil.newURI(file), loadUsingSystemPrincipal: true },
+ function(aInputStream, aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ let contents = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ if (contents.length > TEST_DATA_SHORT.length * 2 ||
+ /[^\x20-\x7E]/.test(contents)) {
+ // Do not print the entire content string to the test log.
+ do_check_eq(contents.length, aExpectedContents.length);
+ do_check_true(contents == aExpectedContents);
+ } else {
+ // Print the string if it is short and made of printable characters.
+ do_check_eq(contents, aExpectedContents);
+ }
+ deferred.resolve();
+ });
+
+ yield deferred.promise;
+ });
+}
+
+/**
+ * Starts a socket listener that closes each incoming connection.
+ *
+ * @returns nsIServerSocket that listens for connections. Call its "close"
+ * method to stop listening and free the server port.
+ */
+function startFakeServer()
+{
+ let serverSocket = new ServerSocket(-1, true, -1);
+ serverSocket.asyncListen({
+ onSocketAccepted: function (aServ, aTransport) {
+ aTransport.close(Cr.NS_BINDING_ABORTED);
+ },
+ onStopListening: function () { },
+ });
+ return serverSocket;
+}
+
+/**
+ * This is an internal reference that should not be used directly by tests.
+ */
+var _gDeferResponses = Promise.defer();
+
+/**
+ * Ensures that all the interruptible requests started after this function is
+ * called won't complete until the continueResponses function is called.
+ *
+ * Normally, the internal HTTP server returns all the available data as soon as
+ * a request is received. In order for some requests to be served one part at a
+ * time, special interruptible handlers are registered on the HTTP server. This
+ * allows testing events or actions that need to happen in the middle of a
+ * download.
+ *
+ * For example, the handler accessible at the httpUri("interruptible.txt")
+ * address returns the TEST_DATA_SHORT text, then it may block until the
+ * continueResponses method is called. At this point, the handler sends the
+ * TEST_DATA_SHORT text again to complete the response.
+ *
+ * If an interruptible request is started before the function is called, it may
+ * or may not be blocked depending on the actual sequence of events.
+ */
+function mustInterruptResponses()
+{
+ // If there are pending blocked requests, allow them to complete. This is
+ // done to prevent requests from being blocked forever, but should not affect
+ // the test logic, since previously started requests should not be monitored
+ // on the client side anymore.
+ _gDeferResponses.resolve();
+
+ do_print("Interruptible responses will be blocked midway.");
+ _gDeferResponses = Promise.defer();
+}
+
+/**
+ * Allows all the current and future interruptible requests to complete.
+ */
+function continueResponses()
+{
+ do_print("Interruptible responses are now allowed to continue.");
+ _gDeferResponses.resolve();
+}
+
+/**
+ * Registers an interruptible response handler.
+ *
+ * @param aPath
+ * Path passed to nsIHttpServer.registerPathHandler.
+ * @param aFirstPartFn
+ * This function is called when the response is received, with the
+ * aRequest and aResponse arguments of the server.
+ * @param aSecondPartFn
+ * This function is called with the aRequest and aResponse arguments of
+ * the server, when the continueResponses function is called.
+ */
+function registerInterruptibleHandler(aPath, aFirstPartFn, aSecondPartFn)
+{
+ gHttpServer.registerPathHandler(aPath, function (aRequest, aResponse) {
+ do_print("Interruptible request started.");
+
+ // Process the first part of the response.
+ aResponse.processAsync();
+ aFirstPartFn(aRequest, aResponse);
+
+ // Wait on the current deferred object, then finish the request.
+ _gDeferResponses.promise.then(function RIH_onSuccess() {
+ aSecondPartFn(aRequest, aResponse);
+ aResponse.finish();
+ do_print("Interruptible request finished.");
+ }).then(null, Cu.reportError);
+ });
+}
+
+/**
+ * Ensure the given date object is valid.
+ *
+ * @param aDate
+ * The date object to be checked. This value can be null.
+ */
+function isValidDate(aDate) {
+ return aDate && aDate.getTime && !isNaN(aDate.getTime());
+}
+
+/**
+ * Position of the first byte served by the "interruptible_resumable.txt"
+ * handler during the most recent response.
+ */
+var gMostRecentFirstBytePos;
+
+// Initialization functions common to all tests
+
+add_task(function test_common_initialize()
+{
+ // Start the HTTP server.
+ gHttpServer = new HttpServer();
+ gHttpServer.registerDirectory("/", do_get_file("../data"));
+ gHttpServer.start(-1);
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ // Ensure all the pending HTTP requests have a chance to finish.
+ continueResponses();
+ // Stop the HTTP server, calling resolve when it's done.
+ gHttpServer.stop(resolve);
+ });
+ });
+
+ // Cache locks might prevent concurrent requests to the same resource, and
+ // this may block tests that use the interruptible handlers.
+ Services.prefs.setBoolPref("browser.cache.disk.enable", false);
+ Services.prefs.setBoolPref("browser.cache.memory.enable", false);
+ do_register_cleanup(function () {
+ Services.prefs.clearUserPref("browser.cache.disk.enable");
+ Services.prefs.clearUserPref("browser.cache.memory.enable");
+ });
+
+ registerInterruptibleHandler("/interruptible.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
+ false);
+ aResponse.write(TEST_DATA_SHORT);
+ }, function secondPart(aRequest, aResponse) {
+ aResponse.write(TEST_DATA_SHORT);
+ });
+
+ registerInterruptibleHandler("/interruptible_resumable.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+
+ // Determine if only part of the data should be sent.
+ let data = TEST_DATA_SHORT + TEST_DATA_SHORT;
+ if (aRequest.hasHeader("Range")) {
+ var matches = aRequest.getHeader("Range")
+ .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/);
+ var firstBytePos = (matches[1] === undefined) ? 0 : matches[1];
+ var lastBytePos = (matches[2] === undefined) ? data.length - 1
+ : matches[2];
+ if (firstBytePos >= data.length) {
+ aResponse.setStatusLine(aRequest.httpVersion, 416,
+ "Requested Range Not Satisfiable");
+ aResponse.setHeader("Content-Range", "*/" + data.length, false);
+ aResponse.finish();
+ return;
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 206, "Partial Content");
+ aResponse.setHeader("Content-Range", firstBytePos + "-" +
+ lastBytePos + "/" +
+ data.length, false);
+
+ data = data.substring(firstBytePos, lastBytePos + 1);
+
+ gMostRecentFirstBytePos = firstBytePos;
+ } else {
+ gMostRecentFirstBytePos = 0;
+ }
+
+ aResponse.setHeader("Content-Length", "" + data.length, false);
+
+ aResponse.write(data.substring(0, data.length / 2));
+
+ // Store the second part of the data on the response object, so that it
+ // can be used by the secondPart function.
+ aResponse.secondPartData = data.substring(data.length / 2);
+ }, function secondPart(aRequest, aResponse) {
+ aResponse.write(aResponse.secondPartData);
+ });
+
+ registerInterruptibleHandler("/interruptible_gzip.txt",
+ function firstPart(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Encoding", "gzip", false);
+ aResponse.setHeader("Content-Length", "" + TEST_DATA_SHORT_GZIP_ENCODED.length);
+
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_FIRST,
+ TEST_DATA_SHORT_GZIP_ENCODED_FIRST.length);
+ }, function secondPart(aRequest, aResponse) {
+ let bos = new BinaryOutputStream(aResponse.bodyOutputStream);
+ bos.writeByteArray(TEST_DATA_SHORT_GZIP_ENCODED_SECOND,
+ TEST_DATA_SHORT_GZIP_ENCODED_SECOND.length);
+ });
+
+ gHttpServer.registerPathHandler("/shorter-than-content-length-http-1-1.txt",
+ function (aRequest, aResponse) {
+ aResponse.processAsync();
+ aResponse.setStatusLine("1.1", 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain", false);
+ aResponse.setHeader("Content-Length", "" + (TEST_DATA_SHORT.length * 2),
+ false);
+ aResponse.write(TEST_DATA_SHORT);
+ aResponse.finish();
+ });
+
+ // This URL will emulate being blocked by Windows Parental controls
+ gHttpServer.registerPathHandler("/parentalblocked.zip",
+ function (aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 450,
+ "Blocked by Windows Parental Controls");
+ });
+
+ // During unit tests, most of the functions that require profile access or
+ // operating system features will be disabled. Individual tests may override
+ // them again to check for specific behaviors.
+ Integration.downloads.register(base => ({
+ __proto__: base,
+ loadPublicDownloadListFromStore: () => Promise.resolve(),
+ shouldKeepBlockedData: () => Promise.resolve(false),
+ shouldBlockForParentalControls: () => Promise.resolve(false),
+ shouldBlockForRuntimePermissions: () => Promise.resolve(false),
+ shouldBlockForReputationCheck: () => Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ }),
+ confirmLaunchExecutable: () => Promise.resolve(),
+ launchFile: () => Promise.resolve(),
+ showContainingDirectory: () => Promise.resolve(),
+ // This flag allows re-enabling the default observers during their tests.
+ allowObservers: false,
+ addListObservers() {
+ return this.allowObservers ? super.addListObservers(...arguments)
+ : Promise.resolve();
+ },
+ // This flag allows re-enabling the download directory logic for its tests.
+ _allowDirectories: false,
+ set allowDirectories(value) {
+ this._allowDirectories = value;
+ // We have to invalidate the previously computed directory path.
+ this._downloadsDirectory = null;
+ },
+ _getDirectory(name) {
+ return super._getDirectory(this._allowDirectories ? name : "TmpD");
+ },
+ }));
+
+ // Make sure that downloads started using nsIExternalHelperAppService are
+ // saved to disk without asking for a destination interactively.
+ let mock = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+ promptForSaveToFileAsync(aLauncher,
+ aWindowContext,
+ aDefaultFileName,
+ aSuggestedFileExtension,
+ aForcePrompt) {
+ // The dialog should create the empty placeholder file.
+ let file = getTempFile(TEST_TARGET_FILE_NAME);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ aLauncher.saveDestinationAvailable(file);
+ },
+ };
+
+ let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", mock);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(cid);
+ });
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
new file mode 100644
index 0000000000..6e32c63d32
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadCore.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the main download interfaces using DownloadCopySaver.
+ */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadError",
+ "resource://gre/modules/DownloadCore.jsm");
+
+// Execution of common tests
+
+var gUseLegacySaver = false;
+
+var scriptFile = do_get_file("common_test_Download.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
+
+// Tests
+
+/**
+ * Tests the DownloadError object.
+ */
+add_task(function test_DownloadError()
+{
+ let error = new DownloadError({ result: Cr.NS_ERROR_NOT_RESUMABLE,
+ message: "Not resumable."});
+ do_check_eq(error.result, Cr.NS_ERROR_NOT_RESUMABLE);
+ do_check_eq(error.message, "Not resumable.");
+ do_check_false(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_false(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ error = new DownloadError({ message: "Unknown error."});
+ do_check_eq(error.result, Cr.NS_ERROR_FAILURE);
+ do_check_eq(error.message, "Unknown error.");
+
+ error = new DownloadError({ result: Cr.NS_ERROR_NOT_RESUMABLE });
+ do_check_eq(error.result, Cr.NS_ERROR_NOT_RESUMABLE);
+ do_check_true(error.message.indexOf("Exception") > 0);
+
+ // becauseSourceFailed will be set, but not the unknown property.
+ error = new DownloadError({ message: "Unknown error.",
+ becauseSourceFailed: true,
+ becauseUnknown: true });
+ do_check_true(error.becauseSourceFailed);
+ do_check_false("becauseUnknown" in error);
+
+ error = new DownloadError({ result: Cr.NS_ERROR_MALFORMED_URI,
+ inferCause: true });
+ do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
+ do_check_true(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_false(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ // This test does not set inferCause, so becauseSourceFailed will not be set.
+ error = new DownloadError({ result: Cr.NS_ERROR_MALFORMED_URI });
+ do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
+ do_check_false(error.becauseSourceFailed);
+
+ error = new DownloadError({ result: Cr.NS_ERROR_FILE_INVALID_PATH,
+ inferCause: true });
+ do_check_eq(error.result, Cr.NS_ERROR_FILE_INVALID_PATH);
+ do_check_false(error.becauseSourceFailed);
+ do_check_true(error.becauseTargetFailed);
+ do_check_false(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ error = new DownloadError({ becauseBlocked: true });
+ do_check_eq(error.message, "Download blocked.");
+ do_check_false(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_true(error.becauseBlocked);
+ do_check_false(error.becauseBlockedByParentalControls);
+
+ error = new DownloadError({ becauseBlockedByParentalControls: true });
+ do_check_eq(error.message, "Download blocked.");
+ do_check_false(error.becauseSourceFailed);
+ do_check_false(error.becauseTargetFailed);
+ do_check_true(error.becauseBlocked);
+ do_check_true(error.becauseBlockedByParentalControls);
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js b/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
new file mode 100644
index 0000000000..388870f009
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
@@ -0,0 +1,701 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadImport object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+ "resource://gre/modules/DownloadImport.jsm");
+
+// Importable states
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+// Non importable states
+const DOWNLOAD_FAILED = 2;
+const DOWNLOAD_CANCELED = 3;
+const DOWNLOAD_BLOCKED_PARENTAL = 6;
+const DOWNLOAD_SCANNING = 7;
+const DOWNLOAD_DIRTY = 8;
+const DOWNLOAD_BLOCKED_POLICY = 9;
+
+// The TEST_DATA_TAINTED const is a version of TEST_DATA_SHORT in which the
+// beginning of the data was changed (with the TEST_DATA_REPLACEMENT value).
+// We use this to test that the entityID is properly imported and the download
+// can be resumed from where it was paused.
+// For simplification purposes, the test requires that TEST_DATA_SHORT and
+// TEST_DATA_TAINTED have the same length.
+const TEST_DATA_REPLACEMENT = "-changed- ";
+const TEST_DATA_TAINTED = TEST_DATA_REPLACEMENT +
+ TEST_DATA_SHORT.substr(TEST_DATA_REPLACEMENT.length);
+const TEST_DATA_LENGTH = TEST_DATA_SHORT.length;
+
+// The length of the partial file that we'll write to disk as an existing
+// ongoing download.
+const TEST_DATA_PARTIAL_LENGTH = TEST_DATA_REPLACEMENT.length;
+
+// The value of the "maxBytes" column stored in the DB about the downloads.
+// It's intentionally different than TEST_DATA_LENGTH to test that each value
+// is seen when expected.
+const MAXBYTES_IN_DB = TEST_DATA_LENGTH - 10;
+
+var gDownloadsRowToImport;
+var gDownloadsRowNonImportable;
+
+/**
+ * Creates a database with an empty moz_downloads table and leaves an
+ * open connection to it.
+ *
+ * @param aPath
+ * String containing the path of the database file to be created.
+ * @param aSchemaVersion
+ * Number with the version of the database schema to set.
+ *
+ * @return {Promise}
+ * @resolves The open connection to the database.
+ * @rejects If an error occurred during the database creation.
+ */
+function promiseEmptyDatabaseConnection({aPath, aSchemaVersion}) {
+ return Task.spawn(function* () {
+ let connection = yield Sqlite.openConnection({ path: aPath });
+
+ yield connection.execute("CREATE TABLE moz_downloads ("
+ + "id INTEGER PRIMARY KEY,"
+ + "name TEXT,"
+ + "source TEXT,"
+ + "target TEXT,"
+ + "tempPath TEXT,"
+ + "startTime INTEGER,"
+ + "endTime INTEGER,"
+ + "state INTEGER,"
+ + "referrer TEXT,"
+ + "entityID TEXT,"
+ + "currBytes INTEGER NOT NULL DEFAULT 0,"
+ + "maxBytes INTEGER NOT NULL DEFAULT -1,"
+ + "mimeType TEXT,"
+ + "preferredApplication TEXT,"
+ + "preferredAction INTEGER NOT NULL DEFAULT 0,"
+ + "autoResume INTEGER NOT NULL DEFAULT 0,"
+ + "guid TEXT)");
+
+ yield connection.setSchemaVersion(aSchemaVersion);
+
+ return connection;
+ });
+}
+
+/**
+ * Inserts a new entry in the database with the given columns' values.
+ *
+ * @param aConnection
+ * The database connection.
+ * @param aDownloadRow
+ * An object representing the values for each column of the row
+ * being inserted.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects If there's an error inserting the row.
+ */
+function promiseInsertRow(aConnection, aDownloadRow) {
+ // We can't use the aDownloadRow obj directly in the execute statement
+ // because the obj bind code in Sqlite.jsm doesn't allow objects
+ // with extra properties beyond those being binded. So we might as well
+ // use an array as it is simpler.
+ let values = [
+ aDownloadRow.source, aDownloadRow.target, aDownloadRow.tempPath,
+ aDownloadRow.startTime.getTime() * 1000, aDownloadRow.state,
+ aDownloadRow.referrer, aDownloadRow.entityID, aDownloadRow.maxBytes,
+ aDownloadRow.mimeType, aDownloadRow.preferredApplication,
+ aDownloadRow.preferredAction, aDownloadRow.autoResume
+ ];
+
+ return aConnection.execute("INSERT INTO moz_downloads ("
+ + "name, source, target, tempPath, startTime,"
+ + "endTime, state, referrer, entityID, currBytes,"
+ + "maxBytes, mimeType, preferredApplication,"
+ + "preferredAction, autoResume, guid)"
+ + "VALUES ("
+ + "'', ?, ?, ?, ?, " // name,
+ + "0, ?, ?, ?, 0, " // endTime, currBytes
+ + " ?, ?, ?, " //
+ + " ?, ?, '')", // and guid are not imported
+ values);
+}
+
+/**
+ * Retrieves the number of rows in the moz_downloads table of the
+ * database.
+ *
+ * @param aConnection
+ * The database connection.
+ *
+ * @return {Promise}
+ * @resolves With the number of rows.
+ * @rejects Never.
+ */
+function promiseTableCount(aConnection) {
+ return aConnection.execute("SELECT COUNT(*) FROM moz_downloads")
+ .then(res => res[0].getResultByName("COUNT(*)"))
+ .then(null, Cu.reportError);
+}
+
+/**
+ * Briefly opens a network channel to a given URL to retrieve
+ * the entityID of this url, as generated by the network code.
+ *
+ * @param aUrl
+ * The URL to retrieve the entityID.
+ *
+ * @return {Promise}
+ * @resolves The EntityID of the given URL.
+ * @rejects When there's a problem accessing the URL.
+ */
+function promiseEntityID(aUrl) {
+ let deferred = Promise.defer();
+ let entityID = "";
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(aUrl),
+ loadUsingSystemPrincipal: true
+ });
+
+ channel.asyncOpen2({
+ onStartRequest: function (aRequest) {
+ if (aRequest instanceof Ci.nsIResumableChannel) {
+ entityID = aRequest.entityID;
+ }
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ },
+
+ onStopRequest: function (aRequest, aContext, aStatusCode) {
+ if (aStatusCode == Cr.NS_BINDING_ABORTED) {
+ deferred.resolve(entityID);
+ } else {
+ deferred.reject("Unexpected status code received");
+ }
+ },
+
+ onDataAvailable: function () {}
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Gets a file path to a temporary writeable download target, in the
+ * correct format as expected to be stored in the downloads database,
+ * which is file:///absolute/path/to/file
+ *
+ * @param aLeafName
+ * A hint leaf name for the file.
+ *
+ * @return String The path to the download target.
+ */
+function getDownloadTarget(aLeafName) {
+ return NetUtil.newURI(getTempFile(aLeafName)).spec;
+}
+
+/**
+ * Generates a temporary partial file to use as an in-progress
+ * download. The file is written to disk with a part of the total expected
+ * download content pre-written.
+ *
+ * @param aLeafName
+ * A hint leaf name for the file.
+ * @param aTainted
+ * A boolean value. When true, the partial content of the file
+ * will be different from the expected content of the original source
+ * file. See the declaration of TEST_DATA_TAINTED for more information.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes, and returns a string with the path
+ * to the generated file.
+ * @rejects If there's an error writing the file.
+ */
+function getPartialFile(aLeafName, aTainted = false) {
+ let tempDownload = getTempFile(aLeafName);
+ let partialContent = aTainted
+ ? TEST_DATA_TAINTED.substr(0, TEST_DATA_PARTIAL_LENGTH)
+ : TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH);
+
+ return OS.File.writeAtomic(tempDownload.path, partialContent,
+ { tmpPath: tempDownload.path + ".tmp",
+ flush: true })
+ .then(() => tempDownload.path);
+}
+
+/**
+ * Generates a Date object to be used as the startTime for the download rows
+ * in the DB. A date that is obviously different from the current time is
+ * generated to make sure this stored data and a `new Date()` can't collide.
+ *
+ * @param aOffset
+ * A offset from the base generated date is used to differentiate each
+ * row in the database.
+ *
+ * @return A Date object.
+ */
+function getStartTime(aOffset) {
+ return new Date(1000000 + (aOffset * 10000));
+}
+
+/**
+ * Performs various checks on an imported Download object to make sure
+ * all properties are properly set as expected from the import procedure.
+ *
+ * @param aDownload
+ * The Download object to be checked.
+ * @param aDownloadRow
+ * An object that represents a row from the original database table,
+ * with extra properties describing expected values that are not
+ * explictly part of the database.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes
+ * @rejects Never
+ */
+function checkDownload(aDownload, aDownloadRow) {
+ return Task.spawn(function*() {
+ do_check_eq(aDownload.source.url, aDownloadRow.source);
+ do_check_eq(aDownload.source.referrer, aDownloadRow.referrer);
+
+ do_check_eq(aDownload.target.path,
+ NetUtil.newURI(aDownloadRow.target)
+ .QueryInterface(Ci.nsIFileURL).file.path);
+
+ do_check_eq(aDownload.target.partFilePath, aDownloadRow.tempPath);
+
+ if (aDownloadRow.expectedResume) {
+ do_check_true(!aDownload.stopped || aDownload.succeeded);
+ yield promiseDownloadStopped(aDownload);
+
+ do_check_true(aDownload.succeeded);
+ do_check_eq(aDownload.progress, 100);
+ // If the download has resumed, a new startTime will be set.
+ // By calling toJSON we're also testing that startTime is a Date object.
+ do_check_neq(aDownload.startTime.toJSON(),
+ aDownloadRow.startTime.toJSON());
+ } else {
+ do_check_false(aDownload.succeeded);
+ do_check_eq(aDownload.startTime.toJSON(),
+ aDownloadRow.startTime.toJSON());
+ }
+
+ do_check_eq(aDownload.stopped, true);
+
+ let serializedSaver = aDownload.saver.toSerializable();
+ if (typeof(serializedSaver) == "object") {
+ do_check_eq(serializedSaver.type, "copy");
+ } else {
+ do_check_eq(serializedSaver, "copy");
+ }
+
+ if (aDownloadRow.entityID) {
+ do_check_eq(aDownload.saver.entityID, aDownloadRow.entityID);
+ }
+
+ do_check_eq(aDownload.currentBytes, aDownloadRow.expectedCurrentBytes);
+ do_check_eq(aDownload.totalBytes, aDownloadRow.expectedTotalBytes);
+
+ if (aDownloadRow.expectedContent) {
+ let fileToCheck = aDownloadRow.expectedResume
+ ? aDownload.target.path
+ : aDownload.target.partFilePath;
+ yield promiseVerifyContents(fileToCheck, aDownloadRow.expectedContent);
+ }
+
+ do_check_eq(aDownload.contentType, aDownloadRow.expectedContentType);
+ do_check_eq(aDownload.launcherPath, aDownloadRow.preferredApplication);
+
+ do_check_eq(aDownload.launchWhenSucceeded,
+ aDownloadRow.preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+ });
+}
+
+// Preparation tasks
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * be imported by the import procedure.
+ */
+add_task(function* prepareDownloadsToImport() {
+
+ let sourceUrl = httpUrl("source.txt");
+ let sourceEntityId = yield promiseEntityID(sourceUrl);
+
+ gDownloadsRowToImport = [
+ // Paused download with autoResume and a partial file. By
+ // setting the correct entityID the download can resume from
+ // where it stopped, and to test that this works properly we
+ // intentionally set different data in the beginning of the
+ // partial file to make sure it was not replaced.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress1.txt"),
+ tempPath: yield getPartialFile("inprogress1.txt.part", true),
+ startTime: getStartTime(1),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer1"),
+ entityID: sourceEntityId,
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType1",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication1",
+ autoResume: 1,
+
+ // Even though the information stored in the DB said
+ // maxBytes was MAXBYTES_IN_DB, the download turned out to be
+ // a different length. Here we make sure the totalBytes property
+ // was correctly set with the actual value. The same consideration
+ // applies to the contentType.
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_TAINTED,
+ },
+
+ // Paused download with autoResume and a partial file,
+ // but missing entityID. This means that the download will
+ // start from beginning, and the entire original content of the
+ // source file should replace the different data that was stored
+ // in the partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress2.txt"),
+ tempPath: yield getPartialFile("inprogress2.txt.part", true),
+ startTime: getStartTime(2),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer2"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType2",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication2",
+ autoResume: 1,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Paused download with no autoResume and a partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress3.txt"),
+ tempPath: yield getPartialFile("inprogress3.txt.part"),
+ startTime: getStartTime(3),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer3"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType3",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication3",
+ autoResume: 0,
+
+ // Since this download has not been resumed, the actual data
+ // about its total size and content type is not known.
+ // Therefore, we're going by the information imported from the DB.
+ expectedCurrentBytes: TEST_DATA_PARTIAL_LENGTH,
+ expectedTotalBytes: MAXBYTES_IN_DB,
+ expectedResume: false,
+ expectedContentType: "mimeType3",
+ expectedContent: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH),
+ },
+
+ // Paused download with autoResume and no partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress4.txt"),
+ tempPath: "",
+ startTime: getStartTime(4),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer4"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+ preferredApplication: "prerredApplication4",
+ autoResume: 1,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Paused download with no autoResume and no partial file.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress5.txt"),
+ tempPath: "",
+ startTime: getStartTime(5),
+ state: DOWNLOAD_PAUSED,
+ referrer: httpUrl("referrer4"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useSystemDefault,
+ preferredApplication: "prerredApplication5",
+ autoResume: 0,
+
+ expectedCurrentBytes: 0,
+ expectedTotalBytes: MAXBYTES_IN_DB,
+ expectedResume: false,
+ expectedContentType: "text/plain",
+ },
+
+ // Queued download with no autoResume and no partial file.
+ // Even though autoResume=0, queued downloads always autoResume.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress6.txt"),
+ tempPath: "",
+ startTime: getStartTime(6),
+ state: DOWNLOAD_QUEUED,
+ referrer: httpUrl("referrer6"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+ preferredApplication: "prerredApplication6",
+ autoResume: 0,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Notstarted download with no autoResume and no partial file.
+ // Even though autoResume=0, notstarted downloads always autoResume.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress7.txt"),
+ tempPath: "",
+ startTime: getStartTime(7),
+ state: DOWNLOAD_NOTSTARTED,
+ referrer: httpUrl("referrer7"),
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+ preferredApplication: "prerredApplication7",
+ autoResume: 0,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_SHORT
+ },
+
+ // Downloading download with no autoResume and a partial file.
+ // Even though autoResume=0, downloading downloads always autoResume.
+ {
+ source: sourceUrl,
+ target: getDownloadTarget("inprogress8.txt"),
+ tempPath: yield getPartialFile("inprogress8.txt.part", true),
+ startTime: getStartTime(8),
+ state: DOWNLOAD_DOWNLOADING,
+ referrer: httpUrl("referrer8"),
+ entityID: sourceEntityId,
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "text/plain",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication8",
+ autoResume: 0,
+
+ expectedCurrentBytes: TEST_DATA_LENGTH,
+ expectedTotalBytes: TEST_DATA_LENGTH,
+ expectedResume: true,
+ expectedContentType: "text/plain",
+ expectedContent: TEST_DATA_TAINTED
+ },
+ ];
+});
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * *not* be imported by the import procedure.
+ */
+add_task(function* prepareNonImportableDownloads()
+{
+ gDownloadsRowNonImportable = [
+ // Download with no source (should never happen in normal circumstances).
+ {
+ source: "",
+ target: "nonimportable1.txt",
+ tempPath: "",
+ startTime: getStartTime(1),
+ state: DOWNLOAD_PAUSED,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType1",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication1",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_FAILED
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable2.txt",
+ tempPath: "",
+ startTime: getStartTime(2),
+ state: DOWNLOAD_FAILED,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType2",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication2",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_CANCELED
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable3.txt",
+ tempPath: "",
+ startTime: getStartTime(3),
+ state: DOWNLOAD_CANCELED,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType3",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication3",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_BLOCKED_PARENTAL
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable4.txt",
+ tempPath: "",
+ startTime: getStartTime(4),
+ state: DOWNLOAD_BLOCKED_PARENTAL,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType4",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication4",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_SCANNING
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable5.txt",
+ tempPath: "",
+ startTime: getStartTime(5),
+ state: DOWNLOAD_SCANNING,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType5",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication5",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_DIRTY
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable6.txt",
+ tempPath: "",
+ startTime: getStartTime(6),
+ state: DOWNLOAD_DIRTY,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType6",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication6",
+ autoResume: 1
+ },
+
+ // state = DOWNLOAD_BLOCKED_POLICY
+ {
+ source: httpUrl("source.txt"),
+ target: "nonimportable7.txt",
+ tempPath: "",
+ startTime: getStartTime(7),
+ state: DOWNLOAD_BLOCKED_POLICY,
+ referrer: "",
+ entityID: "",
+ maxBytes: MAXBYTES_IN_DB,
+ mimeType: "mimeType7",
+ preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+ preferredApplication: "prerredApplication7",
+ autoResume: 1
+ },
+ ];
+});
+
+// Test
+
+/**
+ * Creates a temporary Sqlite database with download data and perform an
+ * import of that data to the new Downloads API to verify that the import
+ * worked correctly.
+ */
+add_task(function* test_downloadImport()
+{
+ let connection = null;
+ let downloadsSqlite = getTempFile("downloads.sqlite").path;
+
+ try {
+ // Set up the database.
+ connection = yield promiseEmptyDatabaseConnection({
+ aPath: downloadsSqlite,
+ aSchemaVersion: 9
+ });
+
+ // Insert both the importable and non-importable
+ // downloads together.
+ for (let downloadRow of gDownloadsRowToImport) {
+ yield promiseInsertRow(connection, downloadRow);
+ }
+
+ for (let downloadRow of gDownloadsRowNonImportable) {
+ yield promiseInsertRow(connection, downloadRow);
+ }
+
+ // Check that every item was inserted.
+ do_check_eq((yield promiseTableCount(connection)),
+ gDownloadsRowToImport.length +
+ gDownloadsRowNonImportable.length);
+ } finally {
+ // Close the connection so that DownloadImport can open it.
+ yield connection.close();
+ }
+
+ // Import items.
+ let list = yield promiseNewList(false);
+ yield new DownloadImport(list, downloadsSqlite).import();
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, gDownloadsRowToImport.length);
+
+ for (let i = 0; i < gDownloadsRowToImport.length; i++) {
+ yield checkDownload(items[i], gDownloadsRowToImport[i]);
+ }
+})
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
new file mode 100644
index 0000000000..31dd7c7a4a
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
@@ -0,0 +1,432 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadIntegration object.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Notifies the prompt observers and verify the expected downloads count.
+ *
+ * @param aIsPrivate
+ * Flag to know is test private observers.
+ * @param aExpectedCount
+ * the expected downloads count for quit and offline observers.
+ * @param aExpectedPBCount
+ * the expected downloads count for private browsing observer.
+ */
+function notifyPromptObservers(aIsPrivate, aExpectedCount, aExpectedPBCount) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+
+ // Notify quit application requested observer.
+ DownloadIntegration._testPromptDownloads = -1;
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+ do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
+
+ // Notify offline requested observer.
+ DownloadIntegration._testPromptDownloads = -1;
+ Services.obs.notifyObservers(cancelQuit, "offline-requested", null);
+ do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedCount);
+
+ if (aIsPrivate) {
+ // Notify last private browsing requested observer.
+ DownloadIntegration._testPromptDownloads = -1;
+ Services.obs.notifyObservers(cancelQuit, "last-pb-context-exiting", null);
+ do_check_eq(DownloadIntegration._testPromptDownloads, aExpectedPBCount);
+ }
+
+ delete DownloadIntegration._testPromptDownloads;
+}
+
+// Tests
+
+/**
+ * Allows re-enabling the real download directory logic during one test.
+ */
+function allowDirectoriesInTest() {
+ DownloadIntegration.allowDirectories = true;
+ function cleanup() {
+ DownloadIntegration.allowDirectories = false;
+ }
+ do_register_cleanup(cleanup);
+ return cleanup;
+}
+
+XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://mozapps/locale/downloads/downloads.properties");
+});
+
+/**
+ * Tests that getSystemDownloadsDirectory returns an existing directory or
+ * creates a new directory depending on the platform. Instead of the real
+ * directory, this test is executed in the temporary directory so we can safely
+ * delete the created folder to check whether it is created again.
+ */
+add_task(function* test_getSystemDownloadsDirectory_exists_or_creates()
+{
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let downloadDir;
+
+ // OSX / Linux / Windows but not XP/2k
+ if (Services.appinfo.OS == "Darwin" ||
+ Services.appinfo.OS == "Linux" ||
+ (Services.appinfo.OS == "WINNT" &&
+ parseFloat(Services.sysinfo.getProperty("version")) >= 6)) {
+ downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ do_check_eq(downloadDir, tempDir.path);
+ do_check_true(yield OS.File.exists(downloadDir));
+
+ let info = yield OS.File.stat(downloadDir);
+ do_check_true(info.isDir);
+ } else {
+ let targetPath = OS.Path.join(tempDir.path,
+ gStringBundle.GetStringFromName("downloadsFolder"));
+ try {
+ yield OS.File.removeEmptyDir(targetPath);
+ } catch (e) {}
+ downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ do_check_eq(downloadDir, targetPath);
+ do_check_true(yield OS.File.exists(downloadDir));
+
+ let info = yield OS.File.stat(downloadDir);
+ do_check_true(info.isDir);
+ yield OS.File.removeEmptyDir(targetPath);
+ }
+});
+
+/**
+ * Tests that the real directory returned by getSystemDownloadsDirectory is not
+ * the one that is used during unit tests. Since this is the actual downloads
+ * directory of the operating system, we don't try to delete it afterwards.
+ */
+add_task(function* test_getSystemDownloadsDirectory_real()
+{
+ let fakeDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+
+ let cleanup = allowDirectoriesInTest();
+ let realDownloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ cleanup();
+
+ do_check_neq(fakeDownloadDir, realDownloadDir);
+});
+
+/**
+ * Tests that the getPreferredDownloadsDirectory returns a valid download
+ * directory string path.
+ */
+add_task(function* test_getPreferredDownloadsDirectory()
+{
+ let cleanupDirectories = allowDirectoriesInTest();
+
+ let folderListPrefName = "browser.download.folderList";
+ let dirPrefName = "browser.download.dir";
+ function cleanupPrefs() {
+ Services.prefs.clearUserPref(folderListPrefName);
+ Services.prefs.clearUserPref(dirPrefName);
+ }
+ do_register_cleanup(cleanupPrefs);
+
+ // Should return the system downloads directory.
+ Services.prefs.setIntPref(folderListPrefName, 1);
+ let systemDir = yield DownloadIntegration.getSystemDownloadsDirectory();
+ let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, systemDir);
+
+ // Should return the desktop directory.
+ Services.prefs.setIntPref(folderListPrefName, 0);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, Services.dirsvc.get("Desk", Ci.nsIFile).path);
+
+ // Should return the system downloads directory because the dir preference
+ // is not set.
+ Services.prefs.setIntPref(folderListPrefName, 2);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, systemDir);
+
+ // Should return the directory which is listed in the dir preference.
+ let time = (new Date()).getTime();
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append(time);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+ do_check_eq(downloadDir, tempDir.path);
+ do_check_true(yield OS.File.exists(downloadDir));
+ yield OS.File.removeEmptyDir(tempDir.path);
+
+ // Should return the system downloads directory beacause the path is invalid
+ // in the dir preference.
+ tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append("dir_not_exist");
+ tempDir.append(time);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_eq(downloadDir, systemDir);
+
+ // Should return the system downloads directory because the folderList
+ // preference is invalid
+ Services.prefs.setIntPref(folderListPrefName, 999);
+ downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_eq(downloadDir, systemDir);
+
+ cleanupPrefs();
+ cleanupDirectories();
+});
+
+/**
+ * Tests that the getTemporaryDownloadsDirectory returns a valid download
+ * directory string path.
+ */
+add_task(function* test_getTemporaryDownloadsDirectory()
+{
+ let cleanup = allowDirectoriesInTest();
+
+ let downloadDir = yield DownloadIntegration.getTemporaryDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+
+ if ("nsILocalFileMac" in Ci) {
+ let preferredDownloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
+ do_check_eq(downloadDir, preferredDownloadDir);
+ } else {
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ do_check_eq(downloadDir, tempDir.path);
+ }
+
+ cleanup();
+});
+
+// Tests DownloadObserver
+
+/**
+ * Re-enables the default observers for the following tests.
+ *
+ * This takes effect the first time a DownloadList object is created, and lasts
+ * until this test file has completed.
+ */
+add_task(function* test_observers_setup()
+{
+ DownloadIntegration.allowObservers = true;
+ do_register_cleanup(function () {
+ DownloadIntegration.allowObservers = false;
+ });
+});
+
+/**
+ * Tests notifications prompts when observers are notified if there are public
+ * and private active downloads.
+ */
+add_task(function* test_notifications()
+{
+ for (let isPrivate of [false, true]) {
+ mustInterruptResponses();
+
+ let list = yield promiseNewList(isPrivate);
+ let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt1 = download1.start();
+ let promiseAttempt2 = download2.start();
+ download3.start().catch(() => {});
+
+ // Add downloads to list.
+ yield list.add(download1);
+ yield list.add(download2);
+ yield list.add(download3);
+ // Cancel third download
+ yield download3.cancel();
+
+ notifyPromptObservers(isPrivate, 2, 2);
+
+ // Allow the downloads to complete.
+ continueResponses();
+ yield promiseAttempt1;
+ yield promiseAttempt2;
+
+ // Clean up.
+ yield list.remove(download1);
+ yield list.remove(download2);
+ yield list.remove(download3);
+ }
+});
+
+/**
+ * Tests that notifications prompts observers are not notified if there are no
+ * public or private active downloads.
+ */
+add_task(function* test_no_notifications()
+{
+ for (let isPrivate of [false, true]) {
+ let list = yield promiseNewList(isPrivate);
+ let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ download1.start().catch(() => {});
+ download2.start().catch(() => {});
+
+ // Add downloads to list.
+ yield list.add(download1);
+ yield list.add(download2);
+
+ yield download1.cancel();
+ yield download2.cancel();
+
+ notifyPromptObservers(isPrivate, 0, 0);
+
+ // Clean up.
+ yield list.remove(download1);
+ yield list.remove(download2);
+ }
+});
+
+/**
+ * Tests notifications prompts when observers are notified if there are public
+ * and private active downloads at the same time.
+ */
+add_task(function* test_mix_notifications()
+{
+ mustInterruptResponses();
+
+ let publicList = yield promiseNewList();
+ let privateList = yield Downloads.getList(Downloads.PRIVATE);
+ let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt1 = download1.start();
+ let promiseAttempt2 = download2.start();
+
+ // Add downloads to lists.
+ yield publicList.add(download1);
+ yield privateList.add(download2);
+
+ notifyPromptObservers(true, 2, 1);
+
+ // Allow the downloads to complete.
+ continueResponses();
+ yield promiseAttempt1;
+ yield promiseAttempt2;
+
+ // Clean up.
+ yield publicList.remove(download1);
+ yield privateList.remove(download2);
+});
+
+/**
+ * Tests suspending and resuming as well as going offline and then online again.
+ * The downloads should stop when suspending and start again when resuming.
+ */
+add_task(function* test_suspend_resume()
+{
+ // The default wake delay is 10 seconds, so set the wake delay to be much
+ // faster for these tests.
+ Services.prefs.setIntPref("browser.download.manager.resumeOnWakeDelay", 5);
+
+ let addDownload = function(list)
+ {
+ return Task.spawn(function* () {
+ let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ download.start().catch(() => {});
+ list.add(download);
+ return download;
+ });
+ }
+
+ let publicList = yield promiseNewList();
+ let privateList = yield promiseNewList(true);
+
+ let download1 = yield addDownload(publicList);
+ let download2 = yield addDownload(publicList);
+ let download3 = yield addDownload(privateList);
+ let download4 = yield addDownload(privateList);
+ let download5 = yield addDownload(publicList);
+
+ // First, check that the downloads are all canceled when going to sleep.
+ Services.obs.notifyObservers(null, "sleep_notification", null);
+ do_check_true(download1.canceled);
+ do_check_true(download2.canceled);
+ do_check_true(download3.canceled);
+ do_check_true(download4.canceled);
+ do_check_true(download5.canceled);
+
+ // Remove a download. It should not be started again.
+ publicList.remove(download5);
+ do_check_true(download5.canceled);
+
+ // When waking up again, the downloads start again after the wake delay. To be
+ // more robust, don't check after a delay but instead just wait for the
+ // downloads to finish.
+ Services.obs.notifyObservers(null, "wake_notification", null);
+ yield download1.whenSucceeded();
+ yield download2.whenSucceeded();
+ yield download3.whenSucceeded();
+ yield download4.whenSucceeded();
+
+ // Downloads should no longer be canceled. However, as download5 was removed
+ // from the public list, it will not be restarted.
+ do_check_false(download1.canceled);
+ do_check_true(download5.canceled);
+
+ // Create four new downloads and check for going offline and then online again.
+
+ download1 = yield addDownload(publicList);
+ download2 = yield addDownload(publicList);
+ download3 = yield addDownload(privateList);
+ download4 = yield addDownload(privateList);
+
+ // Going offline should cancel the downloads.
+ Services.obs.notifyObservers(null, "network:offline-about-to-go-offline", null);
+ do_check_true(download1.canceled);
+ do_check_true(download2.canceled);
+ do_check_true(download3.canceled);
+ do_check_true(download4.canceled);
+
+ // Going back online should start the downloads again.
+ Services.obs.notifyObservers(null, "network:offline-status-changed", "online");
+ yield download1.whenSucceeded();
+ yield download2.whenSucceeded();
+ yield download3.whenSucceeded();
+ yield download4.whenSucceeded();
+
+ Services.prefs.clearUserPref("browser.download.manager.resumeOnWakeDelay");
+});
+
+/**
+ * Tests both the downloads list and the in-progress downloads are clear when
+ * private browsing observer is notified.
+ */
+add_task(function* test_exit_private_browsing()
+{
+ mustInterruptResponses();
+
+ let privateList = yield promiseNewList(true);
+ let download1 = yield promiseNewDownload(httpUrl("source.txt"));
+ let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
+ let promiseAttempt1 = download1.start();
+ download2.start();
+
+ // Add downloads to list.
+ yield privateList.add(download1);
+ yield privateList.add(download2);
+
+ // Complete the download.
+ yield promiseAttempt1;
+
+ do_check_eq((yield privateList.getAll()).length, 2);
+
+ // Simulate exiting the private browsing.
+ yield new Promise(resolve => {
+ DownloadIntegration._testResolveClearPrivateList = resolve;
+ Services.obs.notifyObservers(null, "last-pb-context-exited", null);
+ });
+ delete DownloadIntegration._testResolveClearPrivateList;
+
+ do_check_eq((yield privateList.getAll()).length, 0);
+
+ continueResponses();
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
new file mode 100644
index 0000000000..dc6c186238
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadLegacy.js
@@ -0,0 +1,17 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the integration with legacy interfaces for downloads.
+ */
+
+"use strict";
+
+// Execution of common tests
+
+var gUseLegacySaver = true;
+
+var scriptFile = do_get_file("common_test_Download.js");
+Services.scriptloader.loadSubScript(NetUtil.newURI(scriptFile).spec);
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadList.js b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
new file mode 100644
index 0000000000..71e8807416
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadList.js
@@ -0,0 +1,564 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadList object.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * @note Expiration ignores any visit added in the last 7 days, but it's
+ * better be safe against DST issues, by going back one day more.
+ */
+function getExpirablePRTime()
+{
+ let dateObj = new Date();
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ dateObj = new Date(dateObj.getTime() - 8 * 86400000);
+ return dateObj.getTime() * 1000;
+}
+
+/**
+ * Adds an expirable history visit for a download.
+ *
+ * @param aSourceUrl
+ * String containing the URI for the download source, or null to use
+ * httpUrl("source.txt").
+ *
+ * @return {Promise}
+ * @rejects JavaScript exception.
+ */
+function promiseExpirableDownloadVisit(aSourceUrl)
+{
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.updatePlaces(
+ {
+ uri: NetUtil.newURI(aSourceUrl || httpUrl("source.txt")),
+ visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ visitDate: getExpirablePRTime(),
+ }]
+ },
+ {
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ let ex = new Components.Exception("Unexpected error in adding visits.",
+ aResultCode);
+ deferred.reject(ex);
+ },
+ handleResult: function () {},
+ handleCompletion: function handleCompletion() {
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
+
+// Tests
+
+/**
+ * Checks the testing mechanism used to build different download lists.
+ */
+add_task(function* test_construction()
+{
+ let downloadListOne = yield promiseNewList();
+ let downloadListTwo = yield promiseNewList();
+ let privateDownloadListOne = yield promiseNewList(true);
+ let privateDownloadListTwo = yield promiseNewList(true);
+
+ do_check_neq(downloadListOne, downloadListTwo);
+ do_check_neq(privateDownloadListOne, privateDownloadListTwo);
+ do_check_neq(downloadListOne, privateDownloadListOne);
+});
+
+/**
+ * Checks the methods to add and retrieve items from the list.
+ */
+add_task(function* test_add_getAll()
+{
+ let list = yield promiseNewList();
+
+ let downloadOne = yield promiseNewDownload();
+ yield list.add(downloadOne);
+
+ let itemsOne = yield list.getAll();
+ do_check_eq(itemsOne.length, 1);
+ do_check_eq(itemsOne[0], downloadOne);
+
+ let downloadTwo = yield promiseNewDownload();
+ yield list.add(downloadTwo);
+
+ let itemsTwo = yield list.getAll();
+ do_check_eq(itemsTwo.length, 2);
+ do_check_eq(itemsTwo[0], downloadOne);
+ do_check_eq(itemsTwo[1], downloadTwo);
+
+ // The first snapshot should not have been modified.
+ do_check_eq(itemsOne.length, 1);
+});
+
+/**
+ * Checks the method to remove items from the list.
+ */
+add_task(function* test_remove()
+{
+ let list = yield promiseNewList();
+
+ yield list.add(yield promiseNewDownload());
+ yield list.add(yield promiseNewDownload());
+
+ let items = yield list.getAll();
+ yield list.remove(items[0]);
+
+ // Removing an item that was never added should not raise an error.
+ yield list.remove(yield promiseNewDownload());
+
+ items = yield list.getAll();
+ do_check_eq(items.length, 1);
+});
+
+/**
+ * Tests that the "add", "remove", and "getAll" methods on the global
+ * DownloadCombinedList object combine the contents of the global DownloadList
+ * objects for public and private downloads.
+ */
+add_task(function* test_DownloadCombinedList_add_remove_getAll()
+{
+ let publicList = yield promiseNewList();
+ let privateList = yield Downloads.getList(Downloads.PRIVATE);
+ let combinedList = yield Downloads.getList(Downloads.ALL);
+
+ let publicDownload = yield promiseNewDownload();
+ let privateDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+
+ yield publicList.add(publicDownload);
+ yield privateList.add(privateDownload);
+
+ do_check_eq((yield combinedList.getAll()).length, 2);
+
+ yield combinedList.remove(publicDownload);
+ yield combinedList.remove(privateDownload);
+
+ do_check_eq((yield combinedList.getAll()).length, 0);
+
+ yield combinedList.add(publicDownload);
+ yield combinedList.add(privateDownload);
+
+ do_check_eq((yield publicList.getAll()).length, 1);
+ do_check_eq((yield privateList.getAll()).length, 1);
+ do_check_eq((yield combinedList.getAll()).length, 2);
+
+ yield publicList.remove(publicDownload);
+ yield privateList.remove(privateDownload);
+
+ do_check_eq((yield combinedList.getAll()).length, 0);
+});
+
+/**
+ * Checks that views receive the download add and remove notifications, and that
+ * adding and removing views works as expected, both for a normal and a combined
+ * list.
+ */
+add_task(function* test_notifications_add_remove()
+{
+ for (let isCombined of [false, true]) {
+ // Force creating a new list for both the public and combined cases.
+ let list = yield promiseNewList();
+ if (isCombined) {
+ list = yield Downloads.getList(Downloads.ALL);
+ }
+
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ // Check that we receive add notifications for existing elements.
+ let addNotifications = 0;
+ let viewOne = {
+ onDownloadAdded: function (aDownload) {
+ // The first download to be notified should be the first that was added.
+ if (addNotifications == 0) {
+ do_check_eq(aDownload, downloadOne);
+ } else if (addNotifications == 1) {
+ do_check_eq(aDownload, downloadTwo);
+ }
+ addNotifications++;
+ },
+ };
+ yield list.addView(viewOne);
+ do_check_eq(addNotifications, 2);
+
+ // Check that we receive add notifications for new elements.
+ yield list.add(yield promiseNewDownload());
+ do_check_eq(addNotifications, 3);
+
+ // Check that we receive remove notifications.
+ let removeNotifications = 0;
+ let viewTwo = {
+ onDownloadRemoved: function (aDownload) {
+ do_check_eq(aDownload, downloadOne);
+ removeNotifications++;
+ },
+ };
+ yield list.addView(viewTwo);
+ yield list.remove(downloadOne);
+ do_check_eq(removeNotifications, 1);
+
+ // We should not receive remove notifications after the view is removed.
+ yield list.removeView(viewTwo);
+ yield list.remove(downloadTwo);
+ do_check_eq(removeNotifications, 1);
+
+ // We should not receive add notifications after the view is removed.
+ yield list.removeView(viewOne);
+ yield list.add(yield promiseNewDownload());
+ do_check_eq(addNotifications, 3);
+ }
+});
+
+/**
+ * Checks that views receive the download change notifications, both for a
+ * normal and a combined list.
+ */
+add_task(function* test_notifications_change()
+{
+ for (let isCombined of [false, true]) {
+ // Force creating a new list for both the public and combined cases.
+ let list = yield promiseNewList();
+ if (isCombined) {
+ list = yield Downloads.getList(Downloads.ALL);
+ }
+
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield Downloads.createDownload({
+ source: { url: httpUrl("source.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ // Check that we receive change notifications.
+ let receivedOnDownloadChanged = false;
+ yield list.addView({
+ onDownloadChanged: function (aDownload) {
+ do_check_eq(aDownload, downloadOne);
+ receivedOnDownloadChanged = true;
+ },
+ });
+ yield downloadOne.start();
+ do_check_true(receivedOnDownloadChanged);
+
+ // We should not receive change notifications after a download is removed.
+ receivedOnDownloadChanged = false;
+ yield list.remove(downloadTwo);
+ yield downloadTwo.start();
+ do_check_false(receivedOnDownloadChanged);
+ }
+});
+
+/**
+ * Checks that the reference to "this" is correct in the view callbacks.
+ */
+add_task(function* test_notifications_this()
+{
+ let list = yield promiseNewList();
+
+ // Check that we receive change notifications.
+ let receivedOnDownloadAdded = false;
+ let receivedOnDownloadChanged = false;
+ let receivedOnDownloadRemoved = false;
+ let view = {
+ onDownloadAdded: function () {
+ do_check_eq(this, view);
+ receivedOnDownloadAdded = true;
+ },
+ onDownloadChanged: function () {
+ // Only do this check once.
+ if (!receivedOnDownloadChanged) {
+ do_check_eq(this, view);
+ receivedOnDownloadChanged = true;
+ }
+ },
+ onDownloadRemoved: function () {
+ do_check_eq(this, view);
+ receivedOnDownloadRemoved = true;
+ },
+ };
+ yield list.addView(view);
+
+ let download = yield promiseNewDownload();
+ yield list.add(download);
+ yield download.start();
+ yield list.remove(download);
+
+ // Verify that we executed the checks.
+ do_check_true(receivedOnDownloadAdded);
+ do_check_true(receivedOnDownloadChanged);
+ do_check_true(receivedOnDownloadRemoved);
+});
+
+/**
+ * Checks that download is removed on history expiration.
+ */
+add_task(function* test_history_expiration()
+{
+ mustInterruptResponses();
+
+ function cleanup() {
+ Services.prefs.clearUserPref("places.history.expiration.max_pages");
+ }
+ do_register_cleanup(cleanup);
+
+ // Set max pages to 0 to make the download expire.
+ Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
+
+ let list = yield promiseNewList();
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield promiseNewDownload(httpUrl("interruptible.txt"));
+
+ let deferred = Promise.defer();
+ let removeNotifications = 0;
+ let downloadView = {
+ onDownloadRemoved: function (aDownload) {
+ if (++removeNotifications == 2) {
+ deferred.resolve();
+ }
+ },
+ };
+ yield list.addView(downloadView);
+
+ // Work with one finished download and one canceled download.
+ yield downloadOne.start();
+ downloadTwo.start().catch(() => {});
+ yield downloadTwo.cancel();
+
+ // We must replace the visits added while executing the downloads with visits
+ // that are older than 7 days, otherwise they will not be expired.
+ yield PlacesTestUtils.clearHistory();
+ yield promiseExpirableDownloadVisit();
+ yield promiseExpirableDownloadVisit(httpUrl("interruptible.txt"));
+
+ // After clearing history, we can add the downloads to be removed to the list.
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ // Force a history expiration.
+ Cc["@mozilla.org/places/expiration;1"]
+ .getService(Ci.nsIObserver).observe(null, "places-debug-start-expiration", -1);
+
+ // Wait for both downloads to be removed.
+ yield deferred.promise;
+
+ cleanup();
+});
+
+/**
+ * Checks all downloads are removed after clearing history.
+ */
+add_task(function* test_history_clear()
+{
+ let list = yield promiseNewList();
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield promiseNewDownload();
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+
+ let deferred = Promise.defer();
+ let removeNotifications = 0;
+ let downloadView = {
+ onDownloadRemoved: function (aDownload) {
+ if (++removeNotifications == 2) {
+ deferred.resolve();
+ }
+ },
+ };
+ yield list.addView(downloadView);
+
+ yield downloadOne.start();
+ yield downloadTwo.start();
+
+ yield PlacesTestUtils.clearHistory();
+
+ // Wait for the removal notifications that may still be pending.
+ yield deferred.promise;
+});
+
+/**
+ * Tests the removeFinished method to ensure that it only removes
+ * finished downloads.
+ */
+add_task(function* test_removeFinished()
+{
+ let list = yield promiseNewList();
+ let downloadOne = yield promiseNewDownload();
+ let downloadTwo = yield promiseNewDownload();
+ let downloadThree = yield promiseNewDownload();
+ let downloadFour = yield promiseNewDownload();
+ yield list.add(downloadOne);
+ yield list.add(downloadTwo);
+ yield list.add(downloadThree);
+ yield list.add(downloadFour);
+
+ let deferred = Promise.defer();
+ let removeNotifications = 0;
+ let downloadView = {
+ onDownloadRemoved: function (aDownload) {
+ do_check_true(aDownload == downloadOne ||
+ aDownload == downloadTwo ||
+ aDownload == downloadThree);
+ do_check_true(removeNotifications < 3);
+ if (++removeNotifications == 3) {
+ deferred.resolve();
+ }
+ },
+ };
+ yield list.addView(downloadView);
+
+ // Start three of the downloads, but don't start downloadTwo, then set
+ // downloadFour to have partial data. All downloads except downloadFour
+ // should be removed.
+ yield downloadOne.start();
+ yield downloadThree.start();
+ yield downloadFour.start();
+ downloadFour.hasPartialData = true;
+
+ list.removeFinished();
+ yield deferred.promise;
+
+ let downloads = yield list.getAll()
+ do_check_eq(downloads.length, 1);
+});
+
+/**
+ * Tests the global DownloadSummary objects for the public, private, and
+ * combined download lists.
+ */
+add_task(function* test_DownloadSummary()
+{
+ mustInterruptResponses();
+
+ let publicList = yield promiseNewList();
+ let privateList = yield Downloads.getList(Downloads.PRIVATE);
+
+ let publicSummary = yield Downloads.getSummary(Downloads.PUBLIC);
+ let privateSummary = yield Downloads.getSummary(Downloads.PRIVATE);
+ let combinedSummary = yield Downloads.getSummary(Downloads.ALL);
+
+ // Add a public download that has succeeded.
+ let succeededPublicDownload = yield promiseNewDownload();
+ yield succeededPublicDownload.start();
+ yield publicList.add(succeededPublicDownload);
+
+ // Add a public download that has been canceled midway.
+ let canceledPublicDownload =
+ yield promiseNewDownload(httpUrl("interruptible.txt"));
+ canceledPublicDownload.start().catch(() => {});
+ yield promiseDownloadMidway(canceledPublicDownload);
+ yield canceledPublicDownload.cancel();
+ yield publicList.add(canceledPublicDownload);
+
+ // Add a public download that is in progress.
+ let inProgressPublicDownload =
+ yield promiseNewDownload(httpUrl("interruptible.txt"));
+ inProgressPublicDownload.start().catch(() => {});
+ yield promiseDownloadMidway(inProgressPublicDownload);
+ yield publicList.add(inProgressPublicDownload);
+
+ // Add a private download that is in progress.
+ let inProgressPrivateDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("interruptible.txt"), isPrivate: true },
+ target: getTempFile(TEST_TARGET_FILE_NAME).path,
+ });
+ inProgressPrivateDownload.start().catch(() => {});
+ yield promiseDownloadMidway(inProgressPrivateDownload);
+ yield privateList.add(inProgressPrivateDownload);
+
+ // Verify that the summary includes the total number of bytes and the
+ // currently transferred bytes only for the downloads that are not stopped.
+ // For simplicity, we assume that after a download is added to the list, its
+ // current state is immediately propagated to the summary object, which is
+ // true in the current implementation, though it is not guaranteed as all the
+ // download operations may happen asynchronously.
+ do_check_false(publicSummary.allHaveStopped);
+ do_check_eq(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ do_check_false(privateSummary.allHaveStopped);
+ do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ do_check_false(combinedSummary.allHaveStopped);
+ do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 4);
+ do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2);
+
+ yield inProgressPublicDownload.cancel();
+
+ // Stopping the download should have excluded it from the summary.
+ do_check_true(publicSummary.allHaveStopped);
+ do_check_eq(publicSummary.progressTotalBytes, 0);
+ do_check_eq(publicSummary.progressCurrentBytes, 0);
+
+ do_check_false(privateSummary.allHaveStopped);
+ do_check_eq(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ do_check_false(combinedSummary.allHaveStopped);
+ do_check_eq(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2);
+ do_check_eq(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length);
+
+ yield inProgressPrivateDownload.cancel();
+
+ // All the downloads should be stopped now.
+ do_check_true(publicSummary.allHaveStopped);
+ do_check_eq(publicSummary.progressTotalBytes, 0);
+ do_check_eq(publicSummary.progressCurrentBytes, 0);
+
+ do_check_true(privateSummary.allHaveStopped);
+ do_check_eq(privateSummary.progressTotalBytes, 0);
+ do_check_eq(privateSummary.progressCurrentBytes, 0);
+
+ do_check_true(combinedSummary.allHaveStopped);
+ do_check_eq(combinedSummary.progressTotalBytes, 0);
+ do_check_eq(combinedSummary.progressCurrentBytes, 0);
+});
+
+/**
+ * Checks that views receive the summary change notification. This is tested on
+ * the combined summary when adding a public download, as we assume that if we
+ * pass the test in this case we will also pass it in the others.
+ */
+add_task(function* test_DownloadSummary_notifications()
+{
+ let list = yield promiseNewList();
+ let summary = yield Downloads.getSummary(Downloads.ALL);
+
+ let download = yield promiseNewDownload();
+ yield list.add(download);
+
+ // Check that we receive change notifications.
+ let receivedOnSummaryChanged = false;
+ yield summary.addView({
+ onSummaryChanged: function () {
+ receivedOnSummaryChanged = true;
+ },
+ });
+ yield download.start();
+ do_check_true(receivedOnSummaryChanged);
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
new file mode 100644
index 0000000000..3a23dfbe36
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -0,0 +1,315 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadStore object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
+ "resource://gre/modules/DownloadStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+
+/**
+ * Returns a new DownloadList object with an associated DownloadStore.
+ *
+ * @param aStorePath
+ * String pointing to the file to be associated with the DownloadStore,
+ * or undefined to use a non-existing temporary file. In this case, the
+ * temporary file is deleted when the test file execution finishes.
+ *
+ * @return {Promise}
+ * @resolves Array [ Newly created DownloadList , associated DownloadStore ].
+ * @rejects JavaScript exception.
+ */
+function promiseNewListAndStore(aStorePath)
+{
+ return promiseNewList().then(function (aList) {
+ let path = aStorePath || getTempFile(TEST_STORE_FILE_NAME).path;
+ let store = new DownloadStore(aList, path);
+ return [aList, store];
+ });
+}
+
+// Tests
+
+/**
+ * Saves downloads to a file, then reloads them.
+ */
+add_task(function* test_save_reload()
+{
+ let [listForSave, storeForSave] = yield promiseNewListAndStore();
+ let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
+ storeForSave.path);
+
+ listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
+ listForSave.add(yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ }));
+
+ // This PDF download should not be serialized because it never succeeds.
+ let pdfDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ saver: "pdf",
+ });
+ listForSave.add(pdfDownload);
+
+ // If we used a callback to adjust the channel, the download should
+ // not be serialized because we can't recreate it across sessions.
+ let adjustedDownload = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ adjustChannel: () => Promise.resolve() },
+ target: getTempFile(TEST_TARGET_FILE_NAME),
+ });
+ listForSave.add(adjustedDownload);
+
+ let legacyDownload = yield promiseStartLegacyDownload();
+ yield legacyDownload.cancel();
+ listForSave.add(legacyDownload);
+
+ yield storeForSave.save();
+ yield storeForLoad.load();
+
+ // Remove the PDF and adjusted downloads because they should not appear here.
+ listForSave.remove(adjustedDownload);
+ listForSave.remove(pdfDownload);
+
+ let itemsForSave = yield listForSave.getAll();
+ let itemsForLoad = yield listForLoad.getAll();
+
+ do_check_eq(itemsForSave.length, itemsForLoad.length);
+
+ // Downloads should be reloaded in the same order.
+ for (let i = 0; i < itemsForSave.length; i++) {
+ // The reloaded downloads are different objects.
+ do_check_neq(itemsForSave[i], itemsForLoad[i]);
+
+ // The reloaded downloads have the same properties.
+ do_check_eq(itemsForSave[i].source.url,
+ itemsForLoad[i].source.url);
+ do_check_eq(itemsForSave[i].source.referrer,
+ itemsForLoad[i].source.referrer);
+ do_check_eq(itemsForSave[i].target.path,
+ itemsForLoad[i].target.path);
+ do_check_eq(itemsForSave[i].saver.toSerializable(),
+ itemsForLoad[i].saver.toSerializable());
+ }
+});
+
+/**
+ * Checks that saving an empty list deletes any existing file.
+ */
+add_task(function* test_save_empty()
+{
+ let [, store] = yield promiseNewListAndStore();
+
+ let createdFile = yield OS.File.open(store.path, { create: true });
+ yield createdFile.close();
+
+ yield store.save();
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ // If the file does not exist, saving should not generate exceptions.
+ yield store.save();
+});
+
+/**
+ * Checks that loading from a missing file results in an empty list.
+ */
+add_task(function* test_load_empty()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ yield store.load();
+
+ let items = yield list.getAll();
+ do_check_eq(items.length, 0);
+});
+
+/**
+ * Loads downloads from a string in a predefined format. The purpose of this
+ * test is to verify that the JSON format used in previous versions can be
+ * loaded, assuming the file is reloaded on the same platform.
+ */
+add_task(function* test_load_string_predefined()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ // The platform-dependent file name should be generated dynamically.
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let filePathLiteral = JSON.stringify(targetPath);
+ let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
+ let emptyUriLiteral = JSON.stringify(httpUrl("empty.txt"));
+ let referrerUriLiteral = JSON.stringify(TEST_REFERRER_URL);
+
+ let string = "{\"list\":[{\"source\":" + sourceUriLiteral + "," +
+ "\"target\":" + filePathLiteral + "}," +
+ "{\"source\":{\"url\":" + emptyUriLiteral + "," +
+ "\"referrer\":" + referrerUriLiteral + "}," +
+ "\"target\":" + filePathLiteral + "}]}";
+
+ yield OS.File.writeAtomic(store.path,
+ new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, 2);
+
+ do_check_eq(items[0].source.url, httpUrl("source.txt"));
+ do_check_eq(items[0].target.path, targetPath);
+
+ do_check_eq(items[1].source.url, httpUrl("empty.txt"));
+ do_check_eq(items[1].source.referrer, TEST_REFERRER_URL);
+ do_check_eq(items[1].target.path, targetPath);
+});
+
+/**
+ * Loads downloads from a well-formed JSON string containing unrecognized data.
+ */
+add_task(function* test_load_string_unrecognized()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ // The platform-dependent file name should be generated dynamically.
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let filePathLiteral = JSON.stringify(targetPath);
+ let sourceUriLiteral = JSON.stringify(httpUrl("source.txt"));
+
+ let string = "{\"list\":[{\"source\":null," +
+ "\"target\":null}," +
+ "{\"source\":{\"url\":" + sourceUriLiteral + "}," +
+ "\"target\":{\"path\":" + filePathLiteral + "}," +
+ "\"saver\":{\"type\":\"copy\"}}]}";
+
+ yield OS.File.writeAtomic(store.path,
+ new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, 1);
+
+ do_check_eq(items[0].source.url, httpUrl("source.txt"));
+ do_check_eq(items[0].target.path, targetPath);
+});
+
+/**
+ * Loads downloads from a malformed JSON string.
+ */
+add_task(function* test_load_string_malformed()
+{
+ let [list, store] = yield promiseNewListAndStore();
+
+ let string = "{\"list\":[{\"source\":null,\"target\":null}," +
+ "{\"source\":{\"url\":\"about:blank\"}}}";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ try {
+ yield store.load();
+ do_throw("Exception expected when JSON data is malformed.");
+ } catch (ex) {
+ if (ex.name != "SyntaxError") {
+ throw ex;
+ }
+ do_print("The expected SyntaxError exception was thrown.");
+ }
+
+ let items = yield list.getAll();
+
+ do_check_eq(items.length, 0);
+});
+
+/**
+ * Saves downloads with unknown properties to a file and then reloads
+ * them to ensure that these properties are preserved.
+ */
+add_task(function* test_save_reload_unknownProperties()
+{
+ let [listForSave, storeForSave] = yield promiseNewListAndStore();
+ let [listForLoad, storeForLoad] = yield promiseNewListAndStore(
+ storeForSave.path);
+
+ let download1 = yield promiseNewDownload(httpUrl("source.txt"));
+ // startTime should be ignored as it is a known property, and error
+ // is ignored by serialization
+ download1._unknownProperties = { peanut: "butter",
+ orange: "marmalade",
+ startTime: 77,
+ error: { message: "Passed" } };
+ listForSave.add(download1);
+
+ let download2 = yield promiseStartLegacyDownload();
+ yield download2.cancel();
+ download2._unknownProperties = { number: 5, object: { test: "string" } };
+ listForSave.add(download2);
+
+ let download3 = yield Downloads.createDownload({
+ source: { url: httpUrl("empty.txt"),
+ referrer: TEST_REFERRER_URL,
+ source1: "download3source1",
+ source2: "download3source2" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path,
+ target1: "download3target1",
+ target2: "download3target2" },
+ saver : { type: "copy",
+ saver1: "download3saver1",
+ saver2: "download3saver2" },
+ });
+ listForSave.add(download3);
+
+ yield storeForSave.save();
+ yield storeForLoad.load();
+
+ let itemsForSave = yield listForSave.getAll();
+ let itemsForLoad = yield listForLoad.getAll();
+
+ do_check_eq(itemsForSave.length, itemsForLoad.length);
+
+ do_check_eq(Object.keys(itemsForLoad[0]._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[0]._unknownProperties.peanut, "butter");
+ do_check_eq(itemsForLoad[0]._unknownProperties.orange, "marmalade");
+ do_check_false("startTime" in itemsForLoad[0]._unknownProperties);
+ do_check_false("error" in itemsForLoad[0]._unknownProperties);
+
+ do_check_eq(Object.keys(itemsForLoad[1]._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[1]._unknownProperties.number, 5);
+ do_check_eq(itemsForLoad[1]._unknownProperties.object.test, "string");
+
+ do_check_eq(Object.keys(itemsForLoad[2].source._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[2].source._unknownProperties.source1,
+ "download3source1");
+ do_check_eq(itemsForLoad[2].source._unknownProperties.source2,
+ "download3source2");
+
+ do_check_eq(Object.keys(itemsForLoad[2].target._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[2].target._unknownProperties.target1,
+ "download3target1");
+ do_check_eq(itemsForLoad[2].target._unknownProperties.target2,
+ "download3target2");
+
+ do_check_eq(Object.keys(itemsForLoad[2].saver._unknownProperties).length, 2);
+ do_check_eq(itemsForLoad[2].saver._unknownProperties.saver1,
+ "download3saver1");
+ do_check_eq(itemsForLoad[2].saver._unknownProperties.saver2,
+ "download3saver2");
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_Downloads.js b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
new file mode 100644
index 0000000000..2027beee14
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -0,0 +1,194 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the functions located directly in the "Downloads" object.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Tests that the createDownload function exists and can be called. More
+ * detailed tests are implemented separately for the DownloadCore module.
+ */
+add_task(function* test_createDownload()
+{
+ // Creates a simple Download object without starting the download.
+ yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "copy" },
+ });
+});
+
+/**
+ * Tests createDownload for private download.
+ */
+add_task(function* test_createDownload_private()
+{
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank", isPrivate: true },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "copy" }
+ });
+ do_check_true(download.source.isPrivate);
+});
+
+/**
+ * Tests createDownload for normal (public) download.
+ */
+add_task(function* test_createDownload_public()
+{
+ let tempPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank", isPrivate: false },
+ target: { path: tempPath },
+ saver: { type: "copy" }
+ });
+ do_check_false(download.source.isPrivate);
+
+ download = yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: tempPath },
+ saver: { type: "copy" }
+ });
+ do_check_false(download.source.isPrivate);
+});
+
+/**
+ * Tests createDownload for a pdf saver throws if only given a url.
+ */
+add_task(function* test_createDownload_pdf()
+{
+ let download = yield Downloads.createDownload({
+ source: { url: "about:blank" },
+ target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+ saver: { type: "pdf" },
+ });
+
+ try {
+ yield download.start();
+ do_throw("The download should have failed.");
+ } catch (ex) {
+ if (!(ex instanceof Downloads.Error) || !ex.becauseSourceFailed) {
+ throw ex;
+ }
+ }
+
+ do_check_false(download.succeeded);
+ do_check_true(download.stopped);
+ do_check_false(download.canceled);
+ do_check_true(download.error !== null);
+ do_check_true(download.error.becauseSourceFailed);
+ do_check_false(download.error.becauseTargetFailed);
+ do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
+ * Tests "fetch" with nsIURI and nsIFile as arguments.
+ */
+add_task(function* test_fetch_uri_file_arguments()
+{
+ let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
+ yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
+ yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
+});
+
+/**
+ * Tests "fetch" with DownloadSource and DownloadTarget as arguments.
+ */
+add_task(function* test_fetch_object_arguments()
+{
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ yield Downloads.fetch({ url: httpUrl("source.txt") }, { path: targetPath });
+ yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
+});
+
+/**
+ * Tests "fetch" with string arguments.
+ */
+add_task(function* test_fetch_string_arguments()
+{
+ let targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ yield Downloads.fetch(httpUrl("source.txt"), targetPath);
+ yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
+
+ targetPath = getTempFile(TEST_TARGET_FILE_NAME).path;
+ yield Downloads.fetch(new String(httpUrl("source.txt")),
+ new String(targetPath));
+ yield promiseVerifyContents(targetPath, TEST_DATA_SHORT);
+});
+
+/**
+ * Tests that the getList function returns the same list when called multiple
+ * times with the same argument, but returns different lists when called with
+ * different arguments. More detailed tests are implemented separately for the
+ * DownloadList module.
+ */
+add_task(function* test_getList()
+{
+ let publicListOne = yield Downloads.getList(Downloads.PUBLIC);
+ let privateListOne = yield Downloads.getList(Downloads.PRIVATE);
+
+ let publicListTwo = yield Downloads.getList(Downloads.PUBLIC);
+ let privateListTwo = yield Downloads.getList(Downloads.PRIVATE);
+
+ do_check_eq(publicListOne, publicListTwo);
+ do_check_eq(privateListOne, privateListTwo);
+
+ do_check_neq(publicListOne, privateListOne);
+});
+
+/**
+ * Tests that the getSummary function returns the same summary when called
+ * multiple times with the same argument, but returns different summaries when
+ * called with different arguments. More detailed tests are implemented
+ * separately for the DownloadSummary object in the DownloadList module.
+ */
+add_task(function* test_getSummary()
+{
+ let publicSummaryOne = yield Downloads.getSummary(Downloads.PUBLIC);
+ let privateSummaryOne = yield Downloads.getSummary(Downloads.PRIVATE);
+
+ let publicSummaryTwo = yield Downloads.getSummary(Downloads.PUBLIC);
+ let privateSummaryTwo = yield Downloads.getSummary(Downloads.PRIVATE);
+
+ do_check_eq(publicSummaryOne, publicSummaryTwo);
+ do_check_eq(privateSummaryOne, privateSummaryTwo);
+
+ do_check_neq(publicSummaryOne, privateSummaryOne);
+});
+
+/**
+ * Tests that the getSystemDownloadsDirectory returns a non-empty download
+ * directory string.
+ */
+add_task(function* test_getSystemDownloadsDirectory()
+{
+ let downloadDir = yield Downloads.getSystemDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+});
+
+/**
+ * Tests that the getPreferredDownloadsDirectory returns a non-empty download
+ * directory string.
+ */
+add_task(function* test_getPreferredDownloadsDirectory()
+{
+ let downloadDir = yield Downloads.getPreferredDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+});
+
+/**
+ * Tests that the getTemporaryDownloadsDirectory returns a non-empty download
+ * directory string.
+ */
+add_task(function* test_getTemporaryDownloadsDirectory()
+{
+ let downloadDir = yield Downloads.getTemporaryDownloadsDirectory();
+ do_check_neq(downloadDir, "");
+});
diff --git a/toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js b/toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js
new file mode 100644
index 0000000000..1308e97822
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_PrivateTemp.js
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * The temporary directory downloads saves to, should be only readable
+ * for the current user.
+ */
+add_task(function* test_private_temp() {
+
+ let download = yield promiseStartExternalHelperAppServiceDownload(
+ httpUrl("empty.txt"));
+
+ yield promiseDownloadStopped(download);
+
+ var targetFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);
+ targetFile.initWithPath(download.target.path);
+
+ // 488 is the decimal value of 0o700.
+ equal(targetFile.parent.permissions, 448);
+});
diff --git a/toolkit/components/jsdownloads/test/unit/xpcshell.ini b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..8de5545401
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+head = head.js
+tail =
+skip-if = toolkit == 'android'
+
+# Note: The "tail.js" file is not defined in the "tail" key because it calls
+# the "add_test_task" function, that does not work properly in tail files.
+support-files =
+ common_test_Download.js
+
+[test_DownloadCore.js]
+[test_DownloadImport.js]
+[test_DownloadIntegration.js]
+[test_DownloadLegacy.js]
+[test_DownloadList.js]
+[test_Downloads.js]
+[test_DownloadStore.js]
+[test_PrivateTemp.js]
+skip-if = os != 'linux'
diff --git a/toolkit/components/lz4/lz4.cpp b/toolkit/components/lz4/lz4.cpp
new file mode 100644
index 0000000000..34d5680252
--- /dev/null
+++ b/toolkit/components/lz4/lz4.cpp
@@ -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/. */
+
+#include "mozilla/Compression.h"
+
+/**
+ * LZ4 is a very fast byte-wise compression algorithm.
+ *
+ * Compared to Google's Snappy it is faster to compress and decompress and
+ * generally produces output of about the same size.
+ *
+ * Compared to zlib it compresses at about 10x the speed, decompresses at about
+ * 4x the speed and produces output of about 1.5x the size.
+ *
+ */
+
+using namespace mozilla::Compression;
+
+/**
+ * Compresses 'inputSize' bytes from 'source' into 'dest'.
+ * Destination buffer must be already allocated,
+ * and must be sized to handle worst cases situations (input data not compressible)
+ * Worst case size evaluation is provided by function LZ4_compressBound()
+ *
+ * @param inputSize is the input size. Max supported value is ~1.9GB
+ * @param return the number of bytes written in buffer dest
+ */
+extern "C" MOZ_EXPORT size_t
+workerlz4_compress(const char* source, size_t inputSize, char* dest) {
+ return LZ4::compress(source, inputSize, dest);
+}
+
+/**
+ * If the source stream is malformed, the function will stop decoding
+ * and return a negative result, indicating the byte position of the
+ * faulty instruction
+ *
+ * This function never writes outside of provided buffers, and never
+ * modifies input buffer.
+ *
+ * note : destination buffer must be already allocated.
+ * its size must be a minimum of 'outputSize' bytes.
+ * @param outputSize is the output size, therefore the original size
+ * @return true/false
+ */
+extern "C" MOZ_EXPORT int
+workerlz4_decompress(const char* source, size_t inputSize,
+ char* dest, size_t maxOutputSize,
+ size_t *bytesOutput) {
+ return LZ4::decompress(source, inputSize,
+ dest, maxOutputSize,
+ bytesOutput);
+}
+
+
+/*
+ Provides the maximum size that LZ4 may output in a "worst case"
+ scenario (input data not compressible) primarily useful for memory
+ allocation of output buffer.
+ note : this function is limited by "int" range (2^31-1)
+
+ @param inputSize is the input size. Max supported value is ~1.9GB
+ @return maximum output size in a "worst case" scenario
+*/
+extern "C" MOZ_EXPORT size_t
+workerlz4_maxCompressedSize(size_t inputSize)
+{
+ return LZ4::maxCompressedSize(inputSize);
+}
+
+
+
diff --git a/toolkit/components/lz4/lz4.js b/toolkit/components/lz4/lz4.js
new file mode 100644
index 0000000000..8d4ffcf8e5
--- /dev/null
+++ b/toolkit/components/lz4/lz4.js
@@ -0,0 +1,156 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 SharedAll;
+var Primitives;
+if (typeof Components != "undefined") {
+ let Cu = Components.utils;
+ SharedAll = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+ Cu.import("resource://gre/modules/lz4_internal.js");
+ Cu.import("resource://gre/modules/ctypes.jsm");
+
+ this.EXPORTED_SYMBOLS = [
+ "Lz4"
+ ];
+ this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+ SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ Primitives = require("resource://gre/modules/lz4_internal.js");
+} else {
+ throw new Error("Please load this module with Component.utils.import or with require()");
+}
+
+const MAGIC_NUMBER = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); // "mozLz4a\0"
+
+const BYTES_IN_SIZE_HEADER = ctypes.uint32_t.size;
+
+const HEADER_SIZE = MAGIC_NUMBER.byteLength + BYTES_IN_SIZE_HEADER;
+
+const EXPECTED_HEADER_TYPE = new ctypes.ArrayType(ctypes.uint8_t, HEADER_SIZE);
+const EXPECTED_SIZE_BUFFER_TYPE = new ctypes.ArrayType(ctypes.uint8_t, BYTES_IN_SIZE_HEADER);
+
+/**
+ * An error during (de)compression
+ *
+ * @param {string} operation The name of the operation ("compress", "decompress")
+ * @param {string} reason A reason to be used when matching errors. Must start
+ * with "because", e.g. "becauseInvalidContent".
+ * @param {string} message A human-readable message.
+ */
+function LZError(operation, reason, message) {
+ SharedAll.OSError.call(this);
+ this.operation = operation;
+ this[reason] = true;
+ this.message = message;
+}
+LZError.prototype = Object.create(SharedAll.OSError);
+LZError.prototype.toString = function toString() {
+ return this.message;
+};
+exports.Error = LZError;
+
+/**
+ * Compress a block to a form suitable for writing to disk.
+ *
+ * Compatibility note: For the moment, we are basing our code on lz4
+ * 1.3, which does not specify a *file* format. Therefore, we define
+ * our own format. Once lz4 defines a complete file format, we will
+ * migrate both |compressFileContent| and |decompressFileContent| to this file
+ * format. For backwards-compatibility, |decompressFileContent| will however
+ * keep the ability to decompress files provided with older versions of
+ * |compressFileContent|.
+ *
+ * Compressed files have the following layout:
+ *
+ * | MAGIC_NUMBER (8 bytes) | content size (uint32_t, little endian) | content, as obtained from lz4_compress |
+ *
+ * @param {TypedArray|void*} buffer The buffer to write to the disk.
+ * @param {object=} options An object that may contain the following fields:
+ * - {number} bytes The number of bytes to read from |buffer|. If |buffer|
+ * is an |ArrayBuffer|, |bytes| defaults to |buffer.byteLength|. If
+ * |buffer| is a |void*|, |bytes| MUST be provided.
+ * @return {Uint8Array} An array of bytes suitable for being written to the
+ * disk.
+ */
+function compressFileContent(array, options = {}) {
+ // Prepare the output array
+ let inputBytes;
+ if (SharedAll.isTypedArray(array) && !(options && "bytes" in options)) {
+ inputBytes = array.byteLength;
+ } else if (options && options.bytes) {
+ inputBytes = options.bytes;
+ } else {
+ throw new TypeError("compressFileContent requires a size");
+ }
+ let maxCompressedSize = Primitives.maxCompressedSize(inputBytes);
+ let outputArray = new Uint8Array(HEADER_SIZE + maxCompressedSize);
+
+ // Compress to output array
+ let payload = new Uint8Array(outputArray.buffer, outputArray.byteOffset + HEADER_SIZE);
+ let compressedSize = Primitives.compress(array, inputBytes, payload);
+
+ // Add headers
+ outputArray.set(MAGIC_NUMBER);
+ let view = new DataView(outputArray.buffer);
+ view.setUint32(MAGIC_NUMBER.byteLength, inputBytes, true);
+
+ return new Uint8Array(outputArray.buffer, 0, HEADER_SIZE + compressedSize);
+}
+exports.compressFileContent = compressFileContent;
+
+function decompressFileContent(array, options = {}) {
+ let bytes = SharedAll.normalizeBufferArgs(array, options.bytes || null);
+ if (bytes < HEADER_SIZE) {
+ throw new LZError("decompress", "becauseLZNoHeader",
+ `Buffer is too short (no header) - Data: ${ options.path || array }`);
+ }
+
+ // Read headers
+ let expectMagicNumber = new DataView(array.buffer, 0, MAGIC_NUMBER.byteLength);
+ for (let i = 0; i < MAGIC_NUMBER.byteLength; ++i) {
+ if (expectMagicNumber.getUint8(i) != MAGIC_NUMBER[i]) {
+ throw new LZError("decompress", "becauseLZWrongMagicNumber",
+ `Invalid header (no magic number) - Data: ${ options.path || array }`);
+ }
+ }
+
+ let sizeBuf = new DataView(array.buffer, MAGIC_NUMBER.byteLength, BYTES_IN_SIZE_HEADER);
+ let expectDecompressedSize =
+ sizeBuf.getUint8(0) +
+ (sizeBuf.getUint8(1) << 8) +
+ (sizeBuf.getUint8(2) << 16) +
+ (sizeBuf.getUint8(3) << 24);
+ if (expectDecompressedSize == 0) {
+ // The underlying algorithm cannot handle a size of 0
+ return new Uint8Array(0);
+ }
+
+ // Prepare the input buffer
+ let inputData = new DataView(array.buffer, HEADER_SIZE);
+
+ // Prepare the output buffer
+ let outputBuffer = new Uint8Array(expectDecompressedSize);
+ let decompressedBytes = (new SharedAll.Type.size_t.implementation(0));
+
+ // Decompress
+ let success = Primitives.decompress(inputData, bytes - HEADER_SIZE,
+ outputBuffer, outputBuffer.byteLength,
+ decompressedBytes.address());
+ if (!success) {
+ throw new LZError("decompress", "becauseLZInvalidContent",
+ `Invalid content: Decompression stopped at ${decompressedBytes.value} - Data: ${ options.path || array }`);
+ }
+ return new Uint8Array(outputBuffer.buffer, outputBuffer.byteOffset, decompressedBytes.value);
+}
+exports.decompressFileContent = decompressFileContent;
+
+if (typeof Components != "undefined") {
+ this.Lz4 = {
+ compressFileContent: compressFileContent,
+ decompressFileContent: decompressFileContent
+ };
+}
diff --git a/toolkit/components/lz4/lz4_internal.js b/toolkit/components/lz4/lz4_internal.js
new file mode 100644
index 0000000000..d1227da6c3
--- /dev/null
+++ b/toolkit/components/lz4/lz4_internal.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/. */
+
+"use strict";
+
+var Primitives = {};
+
+var SharedAll;
+if (typeof Components != "undefined") {
+ let Cu = Components.utils;
+ SharedAll = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+
+ this.EXPORTED_SYMBOLS = [
+ "Primitives"
+ ];
+ this.Primitives = Primitives;
+ this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+ SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+} else {
+ throw new Error("Please load this module with Component.utils.import or with require()");
+}
+
+var libxul = new SharedAll.Library("libxul", SharedAll.Constants.Path.libxul);
+var Type = SharedAll.Type;
+
+libxul.declareLazyFFI(Primitives, "compress",
+ "workerlz4_compress",
+ null,
+ /* return*/ Type.size_t,
+ /* const source*/ Type.void_t.in_ptr,
+ /* inputSize*/ Type.size_t,
+ /* dest*/ Type.void_t.out_ptr
+);
+
+libxul.declareLazyFFI(Primitives, "decompress",
+ "workerlz4_decompress",
+ null,
+ /* return*/ Type.int,
+ /* const source*/ Type.void_t.in_ptr,
+ /* inputSize*/ Type.size_t,
+ /* dest*/ Type.void_t.out_ptr,
+ /* maxOutputSize*/ Type.size_t,
+ /* actualOutputSize*/ Type.size_t.out_ptr
+);
+
+libxul.declareLazyFFI(Primitives, "maxCompressedSize",
+ "workerlz4_maxCompressedSize",
+ null,
+ /* return*/ Type.size_t,
+ /* inputSize*/ Type.size_t
+);
+
+if (typeof module != "undefined") {
+ module.exports = {
+ get compress() {
+ return Primitives.compress;
+ },
+ get decompress() {
+ return Primitives.decompress;
+ },
+ get maxCompressedSize() {
+ return Primitives.maxCompressedSize;
+ }
+ };
+}
diff --git a/toolkit/components/lz4/moz.build b/toolkit/components/lz4/moz.build
new file mode 100644
index 0000000000..a701859305
--- /dev/null
+++ b/toolkit/components/lz4/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+EXTRA_JS_MODULES += [
+ 'lz4.js',
+ 'lz4_internal.js',
+]
+
+SOURCES += [
+ 'lz4.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/lz4/tests/xpcshell/.eslintrc.js b/toolkit/components/lz4/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/lz4/tests/xpcshell/data/chrome.manifest b/toolkit/components/lz4/tests/xpcshell/data/chrome.manifest
new file mode 100644
index 0000000000..e2f9a9d8ef
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/data/chrome.manifest
@@ -0,0 +1 @@
+content test_lz4 ./
diff --git a/toolkit/components/lz4/tests/xpcshell/data/compression.lz b/toolkit/components/lz4/tests/xpcshell/data/compression.lz
new file mode 100644
index 0000000000..a354edc036
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/data/compression.lz
Binary files differ
diff --git a/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js b/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js
new file mode 100644
index 0000000000..47e3ea3695
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js
@@ -0,0 +1,146 @@
+importScripts("resource://gre/modules/workers/require.js");
+importScripts("resource://gre/modules/osfile.jsm");
+
+
+function do_print(x) {
+ // self.postMessage({kind: "do_print", args: [x]});
+ dump("TEST-INFO: " + x + "\n");
+}
+
+function do_check_true(x) {
+ self.postMessage({kind: "do_check_true", args: [!!x]});
+ if (x) {
+ dump("TEST-PASS: " + x + "\n");
+ } else {
+ throw new Error("do_check_true failed");
+ }
+}
+
+function do_check_eq(a, b) {
+ let result = a == b;
+ self.postMessage({kind: "do_check_true", args: [result]});
+ if (!result) {
+ throw new Error("do_check_eq failed " + a + " != " + b);
+ }
+}
+
+function do_test_complete() {
+ self.postMessage({kind: "do_test_complete", args:[]});
+}
+
+self.onmessage = function() {
+ try {
+ run_test();
+ } catch (ex) {
+ let {message, moduleStack, moduleName, lineNumber} = ex;
+ let error = new Error(message, moduleName, lineNumber);
+ error.stack = moduleStack;
+ dump("Uncaught error: " + error + "\n");
+ dump("Full stack: " + moduleStack + "\n");
+ throw error;
+ }
+};
+
+var Lz4;
+var Internals;
+function test_import() {
+ Lz4 = require("resource://gre/modules/lz4.js");
+ Internals = require("resource://gre/modules/lz4_internal.js");
+}
+
+function test_bound() {
+ for (let k of ["compress", "decompress", "maxCompressedSize"]) {
+ try {
+ do_print("Checking the existence of " + k + "\n");
+ do_check_true(!!Internals[k]);
+ do_print(k + " exists");
+ } catch (ex) {
+ // Ignore errors
+ do_print(k + " doesn't exist!");
+ }
+ }
+}
+
+function test_reference_file() {
+ do_print("Decompress reference file");
+ let path = OS.Path.join("data", "compression.lz");
+ let data = OS.File.read(path);
+ let decompressed = Lz4.decompressFileContent(data);
+ let text = (new TextDecoder()).decode(decompressed);
+ do_check_eq(text, "Hello, lz4");
+}
+
+function compare_arrays(a, b) {
+ return Array.prototype.join.call(a) == Array.prototype.join.call(a);
+}
+
+function run_rawcompression(name, array) {
+ do_print("Raw compression test " + name);
+ let length = array.byteLength;
+ let compressedArray = new Uint8Array(Internals.maxCompressedSize(length));
+ let compressedBytes = Internals.compress(array, length, compressedArray);
+ compressedArray = new Uint8Array(compressedArray.buffer, 0, compressedBytes);
+ do_print("Raw compressed: " + length + " into " + compressedBytes);
+
+ let decompressedArray = new Uint8Array(length);
+ let decompressedBytes = new ctypes.size_t();
+ let success = Internals.decompress(compressedArray, compressedBytes,
+ decompressedArray, length,
+ decompressedBytes.address());
+ do_print("Raw decompression success? " + success);
+ do_print("Raw decompression size: " + decompressedBytes.value);
+ do_check_true(compare_arrays(array, decompressedArray));
+}
+
+function run_filecompression(name, array) {
+ do_print("File compression test " + name);
+ let compressed = Lz4.compressFileContent(array);
+ do_print("Compressed " + array.byteLength + " bytes into " + compressed.byteLength);
+
+ let decompressed = Lz4.decompressFileContent(compressed);
+ do_print("Decompressed " + compressed.byteLength + " bytes into " + decompressed.byteLength);
+ do_check_true(compare_arrays(array, decompressed));
+}
+
+function run_faileddecompression(name, array) {
+ do_print("invalid decompression test " + name);
+
+ // Ensure that raw decompression doesn't segfault
+ let length = 1 << 14;
+ let decompressedArray = new Uint8Array(length);
+ let decompressedBytes = new ctypes.size_t();
+ Internals.decompress(array, array.byteLength,
+ decompressedArray, length,
+ decompressedBytes.address());
+
+ // File decompression should fail with an acceptable exception
+ let exn = null;
+ try {
+ Lz4.decompressFileContent(array);
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(exn);
+ if (array.byteLength < 10) {
+ do_check_true(exn.becauseLZNoHeader);
+ } else {
+ do_check_true(exn.becauseLZWrongMagicNumber);
+ }
+}
+
+function run_test() {
+ test_import();
+ test_bound();
+ test_reference_file();
+ for (let length of [0, 1, 1024]) {
+ let array = new Uint8Array(length);
+ for (let i = 0; i < length; ++i) {
+ array[i] = i % 256;
+ }
+ let name = length + " bytes";
+ run_rawcompression(name, array);
+ run_filecompression(name, array);
+ run_faileddecompression(name, array);
+ }
+ do_test_complete();
+}
diff --git a/toolkit/components/lz4/tests/xpcshell/test_lz4.js b/toolkit/components/lz4/tests/xpcshell/test_lz4.js
new file mode 100644
index 0000000000..8a8fc0b21a
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/test_lz4.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+var WORKER_SOURCE_URI = "chrome://test_lz4/content/worker_lz4.js";
+do_load_manifest("data/chrome.manifest");
+
+function run_test() {
+ run_next_test();
+}
+
+
+add_task(function() {
+ let worker = new ChromeWorker(WORKER_SOURCE_URI);
+ let deferred = Promise.defer();
+ worker.onmessage = function(event) {
+ let data = event.data;
+ switch (data.kind) {
+ case "do_check_true":
+ try {
+ do_check_true(data.args[0]);
+ } catch (ex) {
+ // Ignore errors
+ }
+ return;
+ case "do_test_complete":
+ deferred.resolve();
+ worker.terminate();
+ break;
+ case "do_print":
+ do_print(data.args[0]);
+ }
+ };
+ worker.onerror = function(event) {
+ let error = new Error(event.message, event.filename, event.lineno);
+ worker.terminate();
+ deferred.reject(error);
+ };
+ worker.postMessage("START");
+ return deferred.promise;
+});
+
diff --git a/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js b/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js
new file mode 100644
index 0000000000..61605373b7
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/lz4.js");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+function compare_arrays(a, b) {
+ return Array.prototype.join.call(a) == Array.prototype.join.call(a);
+}
+
+add_task(function*() {
+ let path = OS.Path.join("data", "compression.lz");
+ let data = yield OS.File.read(path);
+ let decompressed = Lz4.decompressFileContent(data);
+ let text = (new TextDecoder()).decode(decompressed);
+ do_check_eq(text, "Hello, lz4");
+});
+
+add_task(function*() {
+ for (let length of [0, 1, 1024]) {
+ let array = new Uint8Array(length);
+ for (let i = 0; i < length; ++i) {
+ array[i] = i % 256;
+ }
+
+ let compressed = Lz4.compressFileContent(array);
+ do_print("Compressed " + array.byteLength + " bytes into " +
+ compressed.byteLength);
+
+ let decompressed = Lz4.decompressFileContent(compressed);
+ do_print("Decompressed " + compressed.byteLength + " bytes into " +
+ decompressed.byteLength);
+
+ do_check_true(compare_arrays(array, decompressed));
+ }
+});
diff --git a/toolkit/components/lz4/tests/xpcshell/xpcshell.ini b/toolkit/components/lz4/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..e457f29b2b
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ data/worker_lz4.js
+ data/chrome.manifest
+ data/compression.lz
+
+[test_lz4.js]
+[test_lz4_sync.js]
diff --git a/toolkit/components/maintenanceservice/Makefile.in b/toolkit/components/maintenanceservice/Makefile.in
new file mode 100644
index 0000000000..b07afbb0a9
--- /dev/null
+++ b/toolkit/components/maintenanceservice/Makefile.in
@@ -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/.
+
+ifndef MOZ_WINCONSOLE
+ifdef MOZ_DEBUG
+MOZ_WINCONSOLE = 1
+else
+MOZ_WINCONSOLE = 0
+endif
+endif
+
+include $(topsrcdir)/config/rules.mk
diff --git a/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi
new file mode 100644
index 0000000000..9e831dc9c7
--- /dev/null
+++ b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi
@@ -0,0 +1,278 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+; Set verbosity to 3 (e.g. no script) to lessen the noise in the build logs
+!verbose 3
+
+; 7-Zip provides better compression than the lzma from NSIS so we add the files
+; uncompressed and use 7-Zip to create a SFX archive of it
+SetDatablockOptimize on
+SetCompress off
+CRCCheck on
+
+RequestExecutionLevel admin
+
+; The commands inside this ifdef require NSIS 3.0a2 or greater so the ifdef can
+; be removed after we require NSIS 3.0a2 or greater.
+!ifdef NSIS_PACKEDVERSION
+ Unicode true
+ ManifestSupportedOS all
+ ManifestDPIAware true
+!endif
+
+!addplugindir ./
+
+; Variables
+Var TempMaintServiceName
+Var BrandFullNameDA
+Var BrandFullName
+
+; Other included files may depend upon these includes!
+; The following includes are provided by NSIS.
+!include FileFunc.nsh
+!include LogicLib.nsh
+!include MUI.nsh
+!include WinMessages.nsh
+!include WinVer.nsh
+!include WordFunc.nsh
+
+!insertmacro GetOptions
+!insertmacro GetParameters
+!insertmacro GetSize
+
+; The test slaves use this fallback key to run tests.
+; And anyone that wants to run tests themselves should already have
+; this installed.
+!define FallbackKey \
+ "SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4"
+
+!define CompanyName "Mozilla Corporation"
+!define BrandFullNameInternal ""
+
+; The following includes are custom.
+!include defines.nsi
+; We keep defines.nsi defined so that we get other things like
+; the version number, but we redefine BrandFullName
+!define MaintFullName "Mozilla Maintenance Service"
+!undef BrandFullName
+!define BrandFullName "${MaintFullName}"
+
+!include common.nsh
+!include locales.nsi
+
+VIAddVersionKey "FileDescription" "${MaintFullName} Installer"
+VIAddVersionKey "OriginalFilename" "maintenanceservice_installer.exe"
+
+Name "${MaintFullName}"
+OutFile "maintenanceservice_installer.exe"
+
+; Get installation folder from registry if available
+InstallDirRegKey HKLM "Software\Mozilla\MaintenanceService" ""
+
+SetOverwrite on
+
+!define MaintUninstallKey \
+ "Software\Microsoft\Windows\CurrentVersion\Uninstall\MozillaMaintenanceService"
+
+; Always install into the 32-bit location even if we have a 64-bit build.
+; This is because we use only 1 service for all Firefox channels.
+; Allow either x86 and x64 builds to exist at this location, depending on
+; what is the latest build.
+InstallDir "$PROGRAMFILES32\${MaintFullName}\"
+ShowUnInstDetails nevershow
+
+################################################################################
+# Modern User Interface - MUI
+
+!define MUI_ICON setup.ico
+!define MUI_UNICON setup.ico
+!define MUI_WELCOMEPAGE_TITLE_3LINES
+!define MUI_UNWELCOMEFINISHPAGE_BITMAP wizWatermark.bmp
+
+;Interface Settings
+!define MUI_ABORTWARNING
+
+; Uninstaller Pages
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+
+################################################################################
+# Language
+
+!insertmacro MOZ_MUI_LANGUAGE 'baseLocale'
+!verbose push
+!verbose 3
+!include "overrideLocale.nsh"
+!include "customLocale.nsh"
+!verbose pop
+
+; Set this after the locale files to override it if it is in the locale
+; using " " for BrandingText will hide the "Nullsoft Install System..." branding
+BrandingText " "
+
+Function .onInit
+ ; Remove the current exe directory from the search order.
+ ; This only effects LoadLibrary calls and not implicitly loaded DLLs.
+ System::Call 'kernel32::SetDllDirectoryW(w "")'
+
+ SetSilent silent
+ ; On Windows 2000 we do not install the maintenance service.
+ ; We won't run this installer from the parent installer, but just in case
+ ; someone tries to execute it on Windows 2000...
+ ${Unless} ${AtLeastWinXP}
+ Abort
+ ${EndUnless}
+FunctionEnd
+
+Function un.onInit
+ ; Remove the current exe directory from the search order.
+ ; This only effects LoadLibrary calls and not implicitly loaded DLLs.
+ System::Call 'kernel32::SetDllDirectoryW(w "")'
+
+; The commands inside this ifndef are needed prior to NSIS 3.0a2 and can be
+; removed after we require NSIS 3.0a2 or greater.
+!ifndef NSIS_PACKEDVERSION
+ ${If} ${AtLeastWinVista}
+ System::Call 'user32::SetProcessDPIAware()'
+ ${EndIf}
+!endif
+
+ StrCpy $BrandFullNameDA "${MaintFullName}"
+ StrCpy $BrandFullName "${MaintFullName}"
+FunctionEnd
+
+Section "MaintenanceService"
+ AllowSkipFiles off
+
+ CreateDirectory $INSTDIR
+ SetOutPath $INSTDIR
+
+ ; If the service already exists, then it will be stopped when upgrading it
+ ; via the maintenanceservice_tmp.exe command executed below.
+ ; The maintenanceservice_tmp.exe command will rename the file to
+ ; maintenanceservice.exe if maintenanceservice_tmp.exe is newer.
+ ; If the service does not exist yet, we install it and drop the file on
+ ; disk as maintenanceservice.exe directly.
+ StrCpy $TempMaintServiceName "maintenanceservice.exe"
+ IfFileExists "$INSTDIR\maintenanceservice.exe" 0 skipAlreadyExists
+ StrCpy $TempMaintServiceName "maintenanceservice_tmp.exe"
+ skipAlreadyExists:
+
+ ; We always write out a copy and then decide whether to install it or
+ ; not via calling its 'install' cmdline which works by version comparison.
+ CopyFiles "$EXEDIR\maintenanceservice.exe" "$INSTDIR\$TempMaintServiceName"
+
+ ; The updater.ini file is only used when performing an install or upgrade,
+ ; and only if that install or upgrade is successful. If an old updater.ini
+ ; happened to be copied into the maintenance service installation directory
+ ; but the service was not newer, the updater.ini file would be unused.
+ ; It is used to fill the description of the service on success.
+ CopyFiles "$EXEDIR\updater.ini" "$INSTDIR\updater.ini"
+
+ ; Install the application maintenance service.
+ ; If a service already exists, the command line parameter will stop the
+ ; service and only install itself if it is newer than the already installed
+ ; service. If successful it will remove the old maintenanceservice.exe
+ ; and replace it with maintenanceservice_tmp.exe.
+ ClearErrors
+ ;${GetParameters} $0
+ ;${GetOptions} "$0" "/Upgrade" $0
+ ;${If} ${Errors}
+ ExecWait '"$INSTDIR\$TempMaintServiceName" forceinstall'
+ ;${Else}
+ ; The upgrade cmdline is the same as install except
+ ; It will fail if the service isn't already installed.
+ ; ExecWait '"$INSTDIR\$TempMaintServiceName" upgrade'
+ ;${EndIf}
+
+ WriteUninstaller "$INSTDIR\Uninstall.exe"
+ WriteRegStr HKLM "${MaintUninstallKey}" "DisplayName" "${MaintFullName}"
+ WriteRegStr HKLM "${MaintUninstallKey}" "UninstallString" \
+ '"$INSTDIR\uninstall.exe"'
+ WriteRegStr HKLM "${MaintUninstallKey}" "DisplayIcon" \
+ "$INSTDIR\Uninstall.exe,0"
+ WriteRegStr HKLM "${MaintUninstallKey}" "DisplayVersion" "${AppVersion}"
+ WriteRegStr HKLM "${MaintUninstallKey}" "Publisher" "Mozilla"
+ WriteRegStr HKLM "${MaintUninstallKey}" "Comments" "${BrandFullName}"
+ WriteRegDWORD HKLM "${MaintUninstallKey}" "NoModify" 1
+ ${GetSize} "$INSTDIR" "/S=0K" $R2 $R3 $R4
+ WriteRegDWORD HKLM "${MaintUninstallKey}" "EstimatedSize" $R2
+
+ ; Write out that a maintenance service was attempted.
+ ; We do this because on upgrades we will check this value and we only
+ ; want to install once on the first upgrade to maintenance service.
+ ; Also write out that we are currently installed, preferences will check
+ ; this value to determine if we should show the service update pref.
+ ; Since the Maintenance service can be installed either x86 or x64,
+ ; always use the 64-bit registry for checking if an attempt was made.
+ ${If} ${RunningX64}
+ SetRegView 64
+ ${EndIf}
+ WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Attempted" 1
+ WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Installed" 1
+ DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled"
+
+ ; Included here for debug purposes only.
+ ; These keys are used to bypass the installation dir is a valid installation
+ ; check from the service so that tests can be run.
+ WriteRegStr HKLM "${FallbackKey}\0" "name" "Mozilla Corporation"
+ WriteRegStr HKLM "${FallbackKey}\0" "issuer" "DigiCert SHA2 Assured ID Code Signing CA"
+ WriteRegStr HKLM "${FallbackKey}\1" "name" "Mozilla Fake SPC"
+ WriteRegStr HKLM "${FallbackKey}\1" "issuer" "Mozilla Fake CA"
+ ${If} ${RunningX64}
+ SetRegView lastused
+ ${EndIf}
+SectionEnd
+
+; By renaming before deleting we improve things slightly in case
+; there is a file in use error. In this case a new install can happen.
+Function un.RenameDelete
+ Pop $9
+ ; If the .moz-delete file already exists previously, delete it
+ ; If it doesn't exist, the call is ignored.
+ ; We don't need to pass /REBOOTOK here since it was already marked that way
+ ; if it exists.
+ Delete "$9.moz-delete"
+ Rename "$9" "$9.moz-delete"
+ ${If} ${Errors}
+ Delete /REBOOTOK "$9"
+ ${Else}
+ Delete /REBOOTOK "$9.moz-delete"
+ ${EndIf}
+ ClearErrors
+FunctionEnd
+
+Section "Uninstall"
+ ; Delete the service so that no updates will be attempted
+ ExecWait '"$INSTDIR\maintenanceservice.exe" uninstall'
+
+ Push "$INSTDIR\updater.ini"
+ Call un.RenameDelete
+ Push "$INSTDIR\maintenanceservice.exe"
+ Call un.RenameDelete
+ Push "$INSTDIR\maintenanceservice_tmp.exe"
+ Call un.RenameDelete
+ Push "$INSTDIR\maintenanceservice.old"
+ Call un.RenameDelete
+ Push "$INSTDIR\Uninstall.exe"
+ Call un.RenameDelete
+ Push "$INSTDIR\update\updater.ini"
+ Call un.RenameDelete
+ Push "$INSTDIR\update\updater.exe"
+ Call un.RenameDelete
+ RMDir /REBOOTOK "$INSTDIR\update"
+ RMDir /REBOOTOK "$INSTDIR"
+ DeleteRegKey HKLM "${MaintUninstallKey}"
+
+ ${If} ${RunningX64}
+ SetRegView 64
+ ${EndIf}
+ DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "Installed"
+ DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled"
+ DeleteRegKey HKLM "${FallbackKey}\"
+ ${If} ${RunningX64}
+ SetRegView lastused
+ ${EndIf}
+SectionEnd
+
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.cpp b/toolkit/components/maintenanceservice/maintenanceservice.cpp
new file mode 100644
index 0000000000..f1275b095f
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.cpp
@@ -0,0 +1,391 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <windows.h>
+#include <shlwapi.h>
+#include <stdio.h>
+#include <wchar.h>
+#include <shlobj.h>
+
+#include "serviceinstall.h"
+#include "maintenanceservice.h"
+#include "servicebase.h"
+#include "workmonitor.h"
+#include "uachelper.h"
+#include "updatehelper.h"
+#include "registrycertificates.h"
+
+// Link w/ subsystem window so we don't get a console when executing
+// this binary through the installer.
+#pragma comment(linker, "/SUBSYSTEM:windows")
+
+SERVICE_STATUS gSvcStatus = { 0 };
+SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr;
+HANDLE gWorkDoneEvent = nullptr;
+HANDLE gThread = nullptr;
+bool gServiceControlStopping = false;
+
+// logs are pretty small, about 20 lines, so 10 seems reasonable.
+#define LOGS_TO_KEEP 10
+
+BOOL GetLogDirectoryPath(WCHAR *path);
+
+int
+wmain(int argc, WCHAR **argv)
+{
+ // If command-line parameter is "install", install the service
+ // or upgrade if already installed
+ // If command line parameter is "forceinstall", install the service
+ // even if it is older than what is already installed.
+ // If command-line parameter is "upgrade", upgrade the service
+ // but do not install it if it is not already installed.
+ // If command line parameter is "uninstall", uninstall the service.
+ // Otherwise, the service is probably being started by the SCM.
+ bool forceInstall = !lstrcmpi(argv[1], L"forceinstall");
+ if (!lstrcmpi(argv[1], L"install") || forceInstall) {
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath)) {
+ LogInit(updatePath, L"maintenanceservice-install.log");
+ }
+
+ SvcInstallAction action = InstallSvc;
+ if (forceInstall) {
+ action = ForceInstallSvc;
+ LOG(("Installing service with force specified..."));
+ } else {
+ LOG(("Installing service..."));
+ }
+
+ bool ret = SvcInstall(action);
+ if (!ret) {
+ LOG_WARN(("Could not install service. (%d)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ LOG(("The service was installed successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"upgrade")) {
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath)) {
+ LogInit(updatePath, L"maintenanceservice-install.log");
+ }
+
+ LOG(("Upgrading service if installed..."));
+ if (!SvcInstall(UpgradeSvc)) {
+ LOG_WARN(("Could not upgrade service. (%d)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ LOG(("The service was upgraded successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"uninstall")) {
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath)) {
+ LogInit(updatePath, L"maintenanceservice-uninstall.log");
+ }
+ LOG(("Uninstalling service..."));
+ if (!SvcUninstall()) {
+ LOG_WARN(("Could not uninstall service. (%d)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+ LOG(("The service was uninstalled successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"check-cert") && argc > 2) {
+ return DoesBinaryMatchAllowedCertificates(argv[2], argv[3], FALSE) ? 0 : 1;
+ }
+
+ SERVICE_TABLE_ENTRYW DispatchTable[] = {
+ { SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain },
+ { nullptr, nullptr }
+ };
+
+ // This call returns when the service has stopped.
+ // The process should simply terminate when the call returns.
+ if (!StartServiceCtrlDispatcherW(DispatchTable)) {
+ LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError()));
+ }
+
+ return 0;
+}
+
+/**
+ * Obtains the base path where logs should be stored
+ *
+ * @param path The out buffer for the backup log path of size MAX_PATH + 1
+ * @return TRUE if successful.
+ */
+BOOL
+GetLogDirectoryPath(WCHAR *path)
+{
+ if (!GetModuleFileNameW(nullptr, path, MAX_PATH)) {
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(path)) {
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(path, L"logs")) {
+ return FALSE;
+ }
+ CreateDirectoryW(path, nullptr);
+ return TRUE;
+}
+
+/**
+ * Calculated a backup path based on the log number.
+ *
+ * @param path The out buffer to store the log path of size MAX_PATH + 1
+ * @param basePath The base directory where the calculated path should go
+ * @param logNumber The log number, 0 == updater.log
+ * @return TRUE if successful.
+ */
+BOOL
+GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber)
+{
+ WCHAR logName[64] = { L'\0' };
+ wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1);
+ if (logNumber <= 0) {
+ swprintf(logName, sizeof(logName) / sizeof(logName[0]),
+ L"maintenanceservice.log");
+ } else {
+ swprintf(logName, sizeof(logName) / sizeof(logName[0]),
+ L"maintenanceservice-%d.log", logNumber);
+ }
+ return PathAppendSafe(path, logName);
+}
+
+/**
+ * Moves the old log files out of the way before a new one is written.
+ * If you for example keep 3 logs, then this function will do:
+ * updater2.log -> updater3.log
+ * updater1.log -> updater2.log
+ * updater.log -> updater1.log
+ * Which clears room for a new updater.log in the basePath directory
+ *
+ * @param basePath The base directory path where log files are stored
+ * @param numLogsToKeep The number of logs to keep
+ */
+void
+BackupOldLogs(LPCWSTR basePath, int numLogsToKeep)
+{
+ WCHAR oldPath[MAX_PATH + 1];
+ WCHAR newPath[MAX_PATH + 1];
+ for (int i = numLogsToKeep; i >= 1; i--) {
+ if (!GetBackupLogPath(oldPath, basePath, i -1)) {
+ continue;
+ }
+
+ if (!GetBackupLogPath(newPath, basePath, i)) {
+ continue;
+ }
+
+ if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING)) {
+ continue;
+ }
+ }
+}
+
+/**
+ * Ensures the service is shutdown once all work is complete.
+ * There is an issue on XP SP2 and below where the service can hang
+ * in a stop pending state even though the SCM is notified of a stopped
+ * state. Control *should* be returned to StartServiceCtrlDispatcher from the
+ * call to SetServiceStatus on a stopped state in the wmain thread.
+ * Sometimes this is not the case though. This thread will terminate the process
+ * if it has been 5 seconds after all work is done and the process is still not
+ * terminated. This thread is only started once a stopped state was sent to the
+ * SCM. The stop pending hang can be reproduced intermittently even if you set
+ * a stopped state dirctly and never set a stop pending state. It is safe to
+ * forcefully terminate the process ourselves since all work is done once we
+ * start this thread.
+*/
+DWORD WINAPI
+EnsureProcessTerminatedThread(LPVOID)
+{
+ Sleep(5000);
+ exit(0);
+ return 0;
+}
+
+void
+StartTerminationThread()
+{
+ // If the process does not self terminate like it should, this thread
+ // will terminate the process after 5 seconds.
+ HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread,
+ nullptr, 0, nullptr);
+ if (thread) {
+ CloseHandle(thread);
+ }
+}
+
+/**
+ * Main entry point when running as a service.
+ */
+void WINAPI
+SvcMain(DWORD argc, LPWSTR *argv)
+{
+ // Setup logging, and backup the old logs
+ WCHAR updatePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(updatePath)) {
+ BackupOldLogs(updatePath, LOGS_TO_KEEP);
+ LogInit(updatePath, L"maintenanceservice.log");
+ }
+
+ // Disable every privilege we don't need. Processes started using
+ // CreateProcess will use the same token as this process.
+ UACHelper::DisablePrivileges(nullptr);
+
+ // Register the handler function for the service
+ gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler);
+ if (!gSvcStatusHandle) {
+ LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError()));
+ ExecuteServiceCommand(argc, argv);
+ LogFinish();
+ exit(1);
+ }
+
+ // These values will be re-used later in calls involving gSvcStatus
+ gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+ gSvcStatus.dwServiceSpecificExitCode = 0;
+
+ // Report initial status to the SCM
+ ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
+
+ // This event will be used to tell the SvcCtrlHandler when the work is
+ // done for when a stop comamnd is manually issued.
+ gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
+ if (!gWorkDoneEvent) {
+ ReportSvcStatus(SERVICE_STOPPED, 1, 0);
+ StartTerminationThread();
+ return;
+ }
+
+ // Initialization complete and we're about to start working on
+ // the actual command. Report the service state as running to the SCM.
+ ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
+
+ // The service command was executed, stop logging and set an event
+ // to indicate the work is done in case someone is waiting on a
+ // service stop operation.
+ ExecuteServiceCommand(argc, argv);
+ LogFinish();
+
+ SetEvent(gWorkDoneEvent);
+
+ // If we aren't already in a stopping state then tell the SCM we're stopped
+ // now. If we are already in a stopping state then the SERVICE_STOPPED state
+ // will be set by the SvcCtrlHandler.
+ if (!gServiceControlStopping) {
+ ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
+ StartTerminationThread();
+ }
+}
+
+/**
+ * Sets the current service status and reports it to the SCM.
+ *
+ * @param currentState The current state (see SERVICE_STATUS)
+ * @param exitCode The system error code
+ * @param waitHint Estimated time for pending operation in milliseconds
+ */
+void
+ReportSvcStatus(DWORD currentState,
+ DWORD exitCode,
+ DWORD waitHint)
+{
+ static DWORD dwCheckPoint = 1;
+
+ gSvcStatus.dwCurrentState = currentState;
+ gSvcStatus.dwWin32ExitCode = exitCode;
+ gSvcStatus.dwWaitHint = waitHint;
+
+ if (SERVICE_START_PENDING == currentState ||
+ SERVICE_STOP_PENDING == currentState) {
+ gSvcStatus.dwControlsAccepted = 0;
+ } else {
+ gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP |
+ SERVICE_ACCEPT_SHUTDOWN;
+ }
+
+ if ((SERVICE_RUNNING == currentState) ||
+ (SERVICE_STOPPED == currentState)) {
+ gSvcStatus.dwCheckPoint = 0;
+ } else {
+ gSvcStatus.dwCheckPoint = dwCheckPoint++;
+ }
+
+ // Report the status of the service to the SCM.
+ SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
+}
+
+/**
+ * Since the SvcCtrlHandler should only spend at most 30 seconds before
+ * returning, this function does the service stop work for the SvcCtrlHandler.
+*/
+DWORD WINAPI
+StopServiceAndWaitForCommandThread(LPVOID)
+{
+ do {
+ ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
+ } while(WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT);
+ CloseHandle(gWorkDoneEvent);
+ gWorkDoneEvent = nullptr;
+ ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
+ StartTerminationThread();
+ return 0;
+}
+
+/**
+ * Called by SCM whenever a control code is sent to the service
+ * using the ControlService function.
+ */
+void WINAPI
+SvcCtrlHandler(DWORD dwCtrl)
+{
+ // After a SERVICE_CONTROL_STOP there should be no more commands sent to
+ // the SvcCtrlHandler.
+ if (gServiceControlStopping) {
+ return;
+ }
+
+ // Handle the requested control code.
+ switch(dwCtrl) {
+ case SERVICE_CONTROL_SHUTDOWN:
+ case SERVICE_CONTROL_STOP: {
+ gServiceControlStopping = true;
+ ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
+
+ // The SvcCtrlHandler thread should not spend more than 30 seconds in
+ // shutdown so we spawn a new thread for stopping the service
+ HANDLE thread = CreateThread(nullptr, 0,
+ StopServiceAndWaitForCommandThread,
+ nullptr, 0, nullptr);
+ if (thread) {
+ CloseHandle(thread);
+ } else {
+ // Couldn't start the thread so just call the stop ourselves.
+ // If it happens to take longer than 30 seconds the caller will
+ // get an error.
+ StopServiceAndWaitForCommandThread(nullptr);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+}
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest
new file mode 100644
index 0000000000..cb317c47db
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+<assemblyIdentity
+ version="1.0.0.0"
+ processorArchitecture="*"
+ name="MaintenanceService"
+ type="win32"
+/>
+<description>MaintenanceService</description>
+<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:security>
+ <ms_asmv3:requestedPrivileges>
+ <ms_asmv3:requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
+ </ms_asmv3:requestedPrivileges>
+ </ms_asmv3:security>
+</ms_asmv3:trustInfo>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.h b/toolkit/components/maintenanceservice/maintenanceservice.h
new file mode 100644
index 0000000000..9e02914a0e
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.h
@@ -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/. */
+
+void WINAPI SvcMain(DWORD dwArgc, LPWSTR *lpszArgv);
+void SvcInit(DWORD dwArgc, LPWSTR *lpszArgv);
+void WINAPI SvcCtrlHandler(DWORD dwCtrl);
+void ReportSvcStatus(DWORD dwCurrentState,
+ DWORD dwWin32ExitCode,
+ DWORD dwWaitHint);
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.rc b/toolkit/components/maintenanceservice/maintenanceservice.rc
new file mode 100644
index 0000000000..ddd3e942b8
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.rc
@@ -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/. */
+
+// Microsoft Visual C++ generated resource script.
+//
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winresrc.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (U.S.) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+#ifdef _WIN32
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+#endif //_WIN32
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// RT_MANIFEST
+//
+
+1 RT_MANIFEST "maintenanceservice.exe.manifest"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+END
+#endif // APSTUDIO_INVOKED
+
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winresrc.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+#endif // English (U.S.) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/toolkit/components/maintenanceservice/moz.build b/toolkit/components/maintenanceservice/moz.build
new file mode 100644
index 0000000000..2c54c8c762
--- /dev/null
+++ b/toolkit/components/maintenanceservice/moz.build
@@ -0,0 +1,54 @@
+# -*- 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/.
+
+Program('maintenanceservice')
+
+SOURCES += [
+ 'maintenanceservice.cpp',
+ 'servicebase.cpp',
+ 'serviceinstall.cpp',
+ 'workmonitor.cpp',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ USE_LIBS += [
+ 'updatecommon-standalone',
+ ]
+else:
+ USE_LIBS += [
+ 'updatecommon',
+ ]
+
+# For debugging purposes only
+#DEFINES['DISABLE_UPDATER_AUTHENTICODE_CHECK'] = True
+
+DEFINES['UNICODE'] = True
+DEFINES['_UNICODE'] = True
+DEFINES['NS_NO_XPCOM'] = True
+
+# Pick up nsWindowsRestart.cpp
+LOCAL_INCLUDES += [
+ '/toolkit/mozapps/update/common',
+ '/toolkit/xre',
+]
+
+USE_STATIC_LIBS = True
+
+if CONFIG['_MSC_VER']:
+ WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup']
+
+RCINCLUDE = 'maintenanceservice.rc'
+
+DISABLE_STL_WRAPPING = True
+
+OS_LIBS += [
+ 'comctl32',
+ 'ws2_32',
+ 'shell32',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Application Update')
diff --git a/toolkit/components/maintenanceservice/resource.h b/toolkit/components/maintenanceservice/resource.h
new file mode 100644
index 0000000000..45619457c9
--- /dev/null
+++ b/toolkit/components/maintenanceservice/resource.h
@@ -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/. */
+
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by updater.rc
+//
+#define IDI_DIALOG 1003
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1003
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/toolkit/components/maintenanceservice/servicebase.cpp b/toolkit/components/maintenanceservice/servicebase.cpp
new file mode 100644
index 0000000000..a858c4537e
--- /dev/null
+++ b/toolkit/components/maintenanceservice/servicebase.cpp
@@ -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/. */
+
+#include "servicebase.h"
+#include "nsWindowsHelpers.h"
+
+// Shared code between applications and updater.exe
+#include "nsWindowsRestart.cpp"
+
+/**
+ * Verifies if 2 files are byte for byte equivalent.
+ *
+ * @param file1Path The first file to verify.
+ * @param file2Path The second file to verify.
+ * @param sameContent Out parameter, TRUE if the files are equal
+ * @return TRUE If there was no error checking the files.
+ */
+BOOL
+VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent)
+{
+ sameContent = FALSE;
+ nsAutoHandle file1(CreateFileW(file1Path, GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+ if (INVALID_HANDLE_VALUE == file1) {
+ return FALSE;
+ }
+ nsAutoHandle file2(CreateFileW(file2Path, GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+ if (INVALID_HANDLE_VALUE == file2) {
+ return FALSE;
+ }
+
+ DWORD fileSize1 = GetFileSize(file1, nullptr);
+ DWORD fileSize2 = GetFileSize(file2, nullptr);
+ if (INVALID_FILE_SIZE == fileSize1 || INVALID_FILE_SIZE == fileSize2) {
+ return FALSE;
+ }
+
+ if (fileSize1 != fileSize2) {
+ // sameContent is already set to FALSE
+ return TRUE;
+ }
+
+ char buf1[COMPARE_BLOCKSIZE];
+ char buf2[COMPARE_BLOCKSIZE];
+ DWORD numBlocks = fileSize1 / COMPARE_BLOCKSIZE;
+ DWORD leftOver = fileSize1 % COMPARE_BLOCKSIZE;
+ DWORD readAmount;
+ for (DWORD i = 0; i < numBlocks; i++) {
+ if (!ReadFile(file1, buf1, COMPARE_BLOCKSIZE, &readAmount, nullptr) ||
+ readAmount != COMPARE_BLOCKSIZE) {
+ return FALSE;
+ }
+
+ if (!ReadFile(file2, buf2, COMPARE_BLOCKSIZE, &readAmount, nullptr) ||
+ readAmount != COMPARE_BLOCKSIZE) {
+ return FALSE;
+ }
+
+ if (memcmp(buf1, buf2, COMPARE_BLOCKSIZE)) {
+ // sameContent is already set to FALSE
+ return TRUE;
+ }
+ }
+
+ if (leftOver) {
+ if (!ReadFile(file1, buf1, leftOver, &readAmount, nullptr) ||
+ readAmount != leftOver) {
+ return FALSE;
+ }
+
+ if (!ReadFile(file2, buf2, leftOver, &readAmount, nullptr) ||
+ readAmount != leftOver) {
+ return FALSE;
+ }
+
+ if (memcmp(buf1, buf2, leftOver)) {
+ // sameContent is already set to FALSE
+ return TRUE;
+ }
+ }
+
+ sameContent = TRUE;
+ return TRUE;
+}
diff --git a/toolkit/components/maintenanceservice/servicebase.h b/toolkit/components/maintenanceservice/servicebase.h
new file mode 100644
index 0000000000..4479814895
--- /dev/null
+++ b/toolkit/components/maintenanceservice/servicebase.h
@@ -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/. */
+
+#include <windows.h>
+#include "updatecommon.h"
+
+BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
+BOOL VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent);
+
+// 32KiB for comparing files at a time seems reasonable.
+// The bigger the better for speed, but this will be used
+// on the stack so I don't want it to be too big.
+#define COMPARE_BLOCKSIZE 32768
+
+// The following string resource value is used to uniquely identify the signed
+// Mozilla application as an updater. Before the maintenance service will
+// execute the updater it must have this updater identity string in its string
+// table. No other signed Mozilla product will have this string table value.
+#define UPDATER_IDENTITY_STRING \
+ "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49"
+#define IDS_UPDATER_IDENTITY 1006
diff --git a/toolkit/components/maintenanceservice/serviceinstall.cpp b/toolkit/components/maintenanceservice/serviceinstall.cpp
new file mode 100644
index 0000000000..3b9522d47c
--- /dev/null
+++ b/toolkit/components/maintenanceservice/serviceinstall.cpp
@@ -0,0 +1,759 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <windows.h>
+#include <aclapi.h>
+#include <stdlib.h>
+#include <shlwapi.h>
+
+// Used for DNLEN and UNLEN
+#include <lm.h>
+
+#include <nsWindowsHelpers.h>
+#include "mozilla/UniquePtr.h"
+
+#include "serviceinstall.h"
+#include "servicebase.h"
+#include "updatehelper.h"
+#include "shellapi.h"
+#include "readstrings.h"
+#include "errors.h"
+
+#pragma comment(lib, "version.lib")
+
+// This uninstall key is defined originally in maintenanceservice_installer.nsi
+#define MAINT_UNINSTALL_KEY L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MozillaMaintenanceService"
+
+static BOOL
+UpdateUninstallerVersionString(LPWSTR versionString)
+{
+ HKEY uninstallKey;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ MAINT_UNINSTALL_KEY, 0,
+ KEY_WRITE | KEY_WOW64_32KEY,
+ &uninstallKey) != ERROR_SUCCESS) {
+ return FALSE;
+ }
+
+ LONG rv = RegSetValueExW(uninstallKey, L"DisplayVersion", 0, REG_SZ,
+ reinterpret_cast<const BYTE *>(versionString),
+ (wcslen(versionString) + 1) * sizeof(WCHAR));
+ RegCloseKey(uninstallKey);
+ return rv == ERROR_SUCCESS;
+}
+
+/**
+ * A wrapper function to read strings for the maintenance service.
+ *
+ * @param path The path of the ini file to read from
+ * @param results The maintenance service strings that were read
+ * @return OK on success
+*/
+static int
+ReadMaintenanceServiceStrings(LPCWSTR path,
+ MaintenanceServiceStringTable *results)
+{
+ // Read in the maintenance service description string if specified.
+ const unsigned int kNumStrings = 1;
+ const char *kServiceKeys = "MozillaMaintenanceDescription\0";
+ char serviceStrings[kNumStrings][MAX_TEXT_LEN];
+ int result = ReadStrings(path, kServiceKeys,
+ kNumStrings, serviceStrings);
+ if (result != OK) {
+ serviceStrings[0][0] = '\0';
+ }
+ strncpy(results->serviceDescription,
+ serviceStrings[0], MAX_TEXT_LEN - 1);
+ results->serviceDescription[MAX_TEXT_LEN - 1] = '\0';
+ return result;
+}
+
+/**
+ * Obtains the version number from the specified PE file's version information
+ * Version Format: A.B.C.D (Example 10.0.0.300)
+ *
+ * @param path The path of the file to check the version on
+ * @param A The first part of the version number
+ * @param B The second part of the version number
+ * @param C The third part of the version number
+ * @param D The fourth part of the version number
+ * @return TRUE if successful
+ */
+static BOOL
+GetVersionNumberFromPath(LPWSTR path, DWORD &A, DWORD &B,
+ DWORD &C, DWORD &D)
+{
+ DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(path, 0);
+ mozilla::UniquePtr<char[]> fileVersionInfo(new char[fileVersionInfoSize]);
+ if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize,
+ fileVersionInfo.get())) {
+ LOG_WARN(("Could not obtain file info of old service. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ VS_FIXEDFILEINFO *fixedFileInfo =
+ reinterpret_cast<VS_FIXEDFILEINFO *>(fileVersionInfo.get());
+ UINT size;
+ if (!VerQueryValueW(fileVersionInfo.get(), L"\\",
+ reinterpret_cast<LPVOID*>(&fixedFileInfo), &size)) {
+ LOG_WARN(("Could not query file version info of old service. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ A = HIWORD(fixedFileInfo->dwFileVersionMS);
+ B = LOWORD(fixedFileInfo->dwFileVersionMS);
+ C = HIWORD(fixedFileInfo->dwFileVersionLS);
+ D = LOWORD(fixedFileInfo->dwFileVersionLS);
+ return TRUE;
+}
+
+/**
+ * Updates the service description with what is stored in updater.ini
+ * at the same path as the currently executing module binary.
+ *
+ * @param serviceHandle A handle to an opened service with
+ * SERVICE_CHANGE_CONFIG access right
+ * @param TRUE on succcess.
+*/
+BOOL
+UpdateServiceDescription(SC_HANDLE serviceHandle)
+{
+ WCHAR updaterINIPath[MAX_PATH + 1];
+ if (!GetModuleFileNameW(nullptr, updaterINIPath,
+ sizeof(updaterINIPath) /
+ sizeof(updaterINIPath[0]))) {
+ LOG_WARN(("Could not obtain module filename when attempting to "
+ "modify service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(updaterINIPath)) {
+ LOG_WARN(("Could not remove file spec when attempting to "
+ "modify service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(updaterINIPath, L"updater.ini")) {
+ LOG_WARN(("Could not append updater.ini filename when attempting to "
+ "modify service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) {
+ LOG_WARN(("updater.ini file does not exist, will not modify "
+ "service description. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ MaintenanceServiceStringTable serviceStrings;
+ int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings);
+ if (rv != OK || !strlen(serviceStrings.serviceDescription)) {
+ LOG_WARN(("updater.ini file does not contain a maintenance "
+ "service description."));
+ return FALSE;
+ }
+
+ WCHAR serviceDescription[MAX_TEXT_LEN];
+ if (!MultiByteToWideChar(CP_UTF8, 0,
+ serviceStrings.serviceDescription, -1,
+ serviceDescription,
+ sizeof(serviceDescription) /
+ sizeof(serviceDescription[0]))) {
+ LOG_WARN(("Could not convert description to wide string format. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ SERVICE_DESCRIPTIONW descriptionConfig;
+ descriptionConfig.lpDescription = serviceDescription;
+ if (!ChangeServiceConfig2W(serviceHandle,
+ SERVICE_CONFIG_DESCRIPTION,
+ &descriptionConfig)) {
+ LOG_WARN(("Could not change service config. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ LOG(("The service description was updated successfully."));
+ return TRUE;
+}
+
+/**
+ * Determines if the MozillaMaintenance service path needs to be updated
+ * and fixes it if it is wrong.
+ *
+ * @param service A handle to the service to fix.
+ * @param currentServicePath The current (possibly wrong) path that is used.
+ * @param servicePathWasWrong Out parameter set to TRUE if a fix was needed.
+ * @return TRUE if the service path is now correct.
+*/
+BOOL
+FixServicePath(SC_HANDLE service,
+ LPCWSTR currentServicePath,
+ BOOL &servicePathWasWrong)
+{
+ // When we originally upgraded the MozillaMaintenance service we
+ // would uninstall the service on each upgrade. This had an
+ // intermittent error which could cause the service to use the file
+ // maintenanceservice_tmp.exe as the install path. Only a small number
+ // of Nightly users would be affected by this, but we check for this
+ // state here and fix the user if they are affected.
+ //
+ // We also fix the path in the case of the path not being quoted.
+ size_t currentServicePathLen = wcslen(currentServicePath);
+ bool doesServiceHaveCorrectPath =
+ currentServicePathLen > 2 &&
+ !wcsstr(currentServicePath, L"maintenanceservice_tmp.exe") &&
+ currentServicePath[0] == L'\"' &&
+ currentServicePath[currentServicePathLen - 1] == L'\"';
+
+ if (doesServiceHaveCorrectPath) {
+ LOG(("The MozillaMaintenance service path is correct."));
+ servicePathWasWrong = FALSE;
+ return TRUE;
+ }
+ // This is a recoverable situation so not logging as a warning
+ LOG(("The MozillaMaintenance path is NOT correct. It was: %ls",
+ currentServicePath));
+
+ servicePathWasWrong = TRUE;
+ WCHAR fixedPath[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(fixedPath, currentServicePath, MAX_PATH);
+ PathUnquoteSpacesW(fixedPath);
+ if (!PathRemoveFileSpecW(fixedPath)) {
+ LOG_WARN(("Couldn't remove file spec. (%d)", GetLastError()));
+ return FALSE;
+ }
+ if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) {
+ LOG_WARN(("Couldn't append file spec. (%d)", GetLastError()));
+ return FALSE;
+ }
+ PathQuoteSpacesW(fixedPath);
+
+
+ if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
+ SERVICE_NO_CHANGE, fixedPath, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr)) {
+ LOG_WARN(("Could not fix service path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ LOG(("Fixed service path to: %ls.", fixedPath));
+ return TRUE;
+}
+
+/**
+ * Installs or upgrades the SVC_NAME service.
+ * If an existing service is already installed, we replace it with the
+ * currently running process.
+ *
+ * @param action The action to perform.
+ * @return TRUE if the service was installed/upgraded
+ */
+BOOL
+SvcInstall(SvcInstallAction action)
+{
+ // Get a handle to the local computer SCM database with full access rights.
+ nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS));
+ if (!schSCManager) {
+ LOG_WARN(("Could not open service manager. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ WCHAR newServiceBinaryPath[MAX_PATH + 1];
+ if (!GetModuleFileNameW(nullptr, newServiceBinaryPath,
+ sizeof(newServiceBinaryPath) /
+ sizeof(newServiceBinaryPath[0]))) {
+ LOG_WARN(("Could not obtain module filename when attempting to "
+ "install service. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Check if we already have the service installed.
+ nsAutoServiceHandle schService(OpenServiceW(schSCManager,
+ SVC_NAME,
+ SERVICE_ALL_ACCESS));
+ DWORD lastError = GetLastError();
+ if (!schService && ERROR_SERVICE_DOES_NOT_EXIST != lastError) {
+ // The service exists but we couldn't open it
+ LOG_WARN(("Could not open service. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (schService) {
+ // The service exists but it may not have the correct permissions.
+ // This could happen if the permissions were not set correctly originally
+ // or have been changed after the installation. This will reset the
+ // permissions back to allow limited user accounts.
+ if (!SetUserAccessServiceDACL(schService)) {
+ LOG_WARN(("Could not reset security ACE on service handle. It might not be "
+ "possible to start the service. This error should never "
+ "happen. (%d)", GetLastError()));
+ }
+
+ // The service exists and we opened it
+ DWORD bytesNeeded;
+ if (!QueryServiceConfigW(schService, nullptr, 0, &bytesNeeded) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+ LOG_WARN(("Could not determine buffer size for query service config. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Get the service config information, in particular we want the binary
+ // path of the service.
+ mozilla::UniquePtr<char[]> serviceConfigBuffer(new char[bytesNeeded]);
+ if (!QueryServiceConfigW(schService,
+ reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()),
+ bytesNeeded, &bytesNeeded)) {
+ LOG_WARN(("Could open service but could not query service config. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+ QUERY_SERVICE_CONFIGW &serviceConfig =
+ *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get());
+
+ // Check if we need to fix the service path
+ BOOL servicePathWasWrong;
+ static BOOL alreadyCheckedFixServicePath = FALSE;
+ if (!alreadyCheckedFixServicePath) {
+ if (!FixServicePath(schService, serviceConfig.lpBinaryPathName,
+ servicePathWasWrong)) {
+ LOG_WARN(("Could not fix service path. This should never happen. (%d)",
+ GetLastError()));
+ // True is returned because the service is pointing to
+ // maintenanceservice_tmp.exe so it actually was upgraded to the
+ // newest installed service.
+ return TRUE;
+ } else if (servicePathWasWrong) {
+ // Now that the path is fixed we should re-attempt the install.
+ // This current process' image path is maintenanceservice_tmp.exe.
+ // The service used to point to maintenanceservice_tmp.exe.
+ // The service was just fixed to point to maintenanceservice.exe.
+ // Re-attempting an install from scratch will work as normal.
+ alreadyCheckedFixServicePath = TRUE;
+ LOG(("Restarting install action: %d", action));
+ return SvcInstall(action);
+ }
+ }
+
+ // Ensure the service path is not quoted. We own this memory and know it to
+ // be large enough for the quoted path, so it is large enough for the
+ // unquoted path. This function cannot fail.
+ PathUnquoteSpacesW(serviceConfig.lpBinaryPathName);
+
+ // Obtain the existing maintenanceservice file's version number and
+ // the new file's version number. Versions are in the format of
+ // A.B.C.D.
+ DWORD existingA, existingB, existingC, existingD;
+ DWORD newA, newB, newC, newD;
+ BOOL obtainedExistingVersionInfo =
+ GetVersionNumberFromPath(serviceConfig.lpBinaryPathName,
+ existingA, existingB,
+ existingC, existingD);
+ if (!GetVersionNumberFromPath(newServiceBinaryPath, newA,
+ newB, newC, newD)) {
+ LOG_WARN(("Could not obtain version number from new path"));
+ return FALSE;
+ }
+
+ // Check if we need to replace the old binary with the new one
+ // If we couldn't get the old version info then we assume we should
+ // replace it.
+ if (ForceInstallSvc == action ||
+ !obtainedExistingVersionInfo ||
+ (existingA < newA) ||
+ (existingA == newA && existingB < newB) ||
+ (existingA == newA && existingB == newB &&
+ existingC < newC) ||
+ (existingA == newA && existingB == newB &&
+ existingC == newC && existingD < newD)) {
+
+ // We have a newer updater, so update the description from the INI file.
+ UpdateServiceDescription(schService);
+
+ schService.reset();
+ if (!StopService()) {
+ return FALSE;
+ }
+
+ if (!wcscmp(newServiceBinaryPath, serviceConfig.lpBinaryPathName)) {
+ LOG(("File is already in the correct location, no action needed for "
+ "upgrade. The path is: \"%ls\"", newServiceBinaryPath));
+ return TRUE;
+ }
+
+ BOOL result = TRUE;
+
+ // Attempt to copy the new binary over top the existing binary.
+ // If there is an error we try to move it out of the way and then
+ // copy it in. First try the safest / easiest way to overwrite the file.
+ if (!CopyFileW(newServiceBinaryPath,
+ serviceConfig.lpBinaryPathName, FALSE)) {
+ LOG_WARN(("Could not overwrite old service binary file. "
+ "This should never happen, but if it does the next "
+ "upgrade will fix it, the service is not a critical "
+ "component that needs to be installed for upgrades "
+ "to work. (%d)", GetLastError()));
+
+ // We rename the last 3 filename chars in an unsafe way. Manually
+ // verify there are more than 3 chars for safe failure in MoveFileExW.
+ const size_t len = wcslen(serviceConfig.lpBinaryPathName);
+ if (len > 3) {
+ // Calculate the temp file path that we're moving the file to. This
+ // is the same as the proper service path but with a .old extension.
+ LPWSTR oldServiceBinaryTempPath =
+ new WCHAR[len + 1];
+ memset(oldServiceBinaryTempPath, 0, (len + 1) * sizeof (WCHAR));
+ wcsncpy(oldServiceBinaryTempPath, serviceConfig.lpBinaryPathName, len);
+ // Rename the last 3 chars to 'old'
+ wcsncpy(oldServiceBinaryTempPath + len - 3, L"old", 3);
+
+ // Move the current (old) service file to the temp path.
+ if (MoveFileExW(serviceConfig.lpBinaryPathName,
+ oldServiceBinaryTempPath,
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
+ // The old binary is moved out of the way, copy in the new one.
+ if (!CopyFileW(newServiceBinaryPath,
+ serviceConfig.lpBinaryPathName, FALSE)) {
+ // It is best to leave the old service binary in this condition.
+ LOG_WARN(("The new service binary could not be copied in."
+ " The service will not be upgraded."));
+ result = FALSE;
+ } else {
+ LOG(("The new service binary was copied in by first moving the"
+ " old one out of the way."));
+ }
+
+ // Attempt to get rid of the old service temp path.
+ if (DeleteFileW(oldServiceBinaryTempPath)) {
+ LOG(("The old temp service path was deleted: %ls.",
+ oldServiceBinaryTempPath));
+ } else {
+ // The old temp path could not be removed. It will be removed
+ // the next time the user can't copy the binary in or on uninstall.
+ LOG_WARN(("The old temp service path was not deleted."));
+ }
+ } else {
+ // It is best to leave the old service binary in this condition.
+ LOG_WARN(("Could not move old service file out of the way from:"
+ " \"%ls\" to \"%ls\". Service will not be upgraded. (%d)",
+ serviceConfig.lpBinaryPathName,
+ oldServiceBinaryTempPath, GetLastError()));
+ result = FALSE;
+ }
+ delete[] oldServiceBinaryTempPath;
+ } else {
+ // It is best to leave the old service binary in this condition.
+ LOG_WARN(("Service binary path was less than 3, service will"
+ " not be updated. This should never happen."));
+ result = FALSE;
+ }
+ } else {
+ WCHAR versionStr[128] = { L'\0' };
+ swprintf(versionStr, 128, L"%d.%d.%d.%d", newA, newB, newC, newD);
+ if (!UpdateUninstallerVersionString(versionStr)) {
+ LOG(("The uninstaller version string could not be updated."));
+ }
+ LOG(("The new service binary was copied in."));
+ }
+
+ // We made a copy of ourselves to the existing location.
+ // The tmp file (the process of which we are executing right now) will be
+ // left over. Attempt to delete the file on the next reboot.
+ if (MoveFileExW(newServiceBinaryPath, nullptr,
+ MOVEFILE_DELAY_UNTIL_REBOOT)) {
+ LOG(("Deleting the old file path on the next reboot: %ls.",
+ newServiceBinaryPath));
+ } else {
+ LOG_WARN(("Call to delete the old file path failed: %ls.",
+ newServiceBinaryPath));
+ }
+
+ return result;
+ }
+
+ // We don't need to copy ourselves to the existing location.
+ // The tmp file (the process of which we are executing right now) will be
+ // left over. Attempt to delete the file on the next reboot.
+ MoveFileExW(newServiceBinaryPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);
+
+ // nothing to do, we already have a newer service installed
+ return TRUE;
+ }
+
+ // If the service does not exist and we are upgrading, don't install it.
+ if (UpgradeSvc == action) {
+ // The service does not exist and we are upgrading, so don't install it
+ return TRUE;
+ }
+
+ // Quote the path only if it contains spaces.
+ PathQuoteSpacesW(newServiceBinaryPath);
+ // The service does not already exist so create the service as on demand
+ schService.own(CreateServiceW(schSCManager, SVC_NAME, SVC_DISPLAY_NAME,
+ SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
+ SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
+ newServiceBinaryPath, nullptr, nullptr,
+ nullptr, nullptr, nullptr));
+ if (!schService) {
+ LOG_WARN(("Could not create Windows service. "
+ "This error should never happen since a service install "
+ "should only be called when elevated. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!SetUserAccessServiceDACL(schService)) {
+ LOG_WARN(("Could not set security ACE on service handle, the service will not "
+ "be able to be started from unelevated processes. "
+ "This error should never happen. (%d)",
+ GetLastError()));
+ }
+
+ UpdateServiceDescription(schService);
+
+ return TRUE;
+}
+
+/**
+ * Stops the Maintenance service.
+ *
+ * @return TRUE if successful.
+ */
+BOOL
+StopService()
+{
+ // Get a handle to the local computer SCM database with full access rights.
+ nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS));
+ if (!schSCManager) {
+ LOG_WARN(("Could not open service manager. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Open the service
+ nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME,
+ SERVICE_ALL_ACCESS));
+ if (!schService) {
+ LOG_WARN(("Could not open service. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ LOG(("Sending stop request..."));
+ SERVICE_STATUS status;
+ SetLastError(ERROR_SUCCESS);
+ if (!ControlService(schService, SERVICE_CONTROL_STOP, &status) &&
+ GetLastError() != ERROR_SERVICE_NOT_ACTIVE) {
+ LOG_WARN(("Error sending stop request. (%d)", GetLastError()));
+ }
+
+ schSCManager.reset();
+ schService.reset();
+
+ LOG(("Waiting for service stop..."));
+ DWORD lastState = WaitForServiceStop(SVC_NAME, 30);
+
+ // The service can be in a stopped state but the exe still in use
+ // so make sure the process is really gone before proceeding
+ WaitForProcessExit(L"maintenanceservice.exe", 30);
+ LOG(("Done waiting for service stop, last service state: %d", lastState));
+
+ return lastState == SERVICE_STOPPED;
+}
+
+/**
+ * Uninstalls the Maintenance service.
+ *
+ * @return TRUE if successful.
+ */
+BOOL
+SvcUninstall()
+{
+ // Get a handle to the local computer SCM database with full access rights.
+ nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS));
+ if (!schSCManager) {
+ LOG_WARN(("Could not open service manager. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Open the service
+ nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME,
+ SERVICE_ALL_ACCESS));
+ if (!schService) {
+ LOG_WARN(("Could not open service. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ //Stop the service so it deletes faster and so the uninstaller
+ // can actually delete its EXE.
+ DWORD totalWaitTime = 0;
+ SERVICE_STATUS status;
+ static const int maxWaitTime = 1000 * 60; // Never wait more than a minute
+ if (ControlService(schService, SERVICE_CONTROL_STOP, &status)) {
+ do {
+ Sleep(status.dwWaitHint);
+ totalWaitTime += (status.dwWaitHint + 10);
+ if (status.dwCurrentState == SERVICE_STOPPED) {
+ break;
+ } else if (totalWaitTime > maxWaitTime) {
+ break;
+ }
+ } while (QueryServiceStatus(schService, &status));
+ }
+
+ // Delete the service or mark it for deletion
+ BOOL deleted = DeleteService(schService);
+ if (!deleted) {
+ deleted = (GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE);
+ }
+
+ return deleted;
+}
+
+/**
+ * Sets the access control list for user access for the specified service.
+ *
+ * @param hService The service to set the access control list on
+ * @return TRUE if successful
+ */
+BOOL
+SetUserAccessServiceDACL(SC_HANDLE hService)
+{
+ PACL pNewAcl = nullptr;
+ PSECURITY_DESCRIPTOR psd = nullptr;
+ DWORD lastError = SetUserAccessServiceDACL(hService, pNewAcl, psd);
+ if (pNewAcl) {
+ LocalFree((HLOCAL)pNewAcl);
+ }
+ if (psd) {
+ LocalFree((LPVOID)psd);
+ }
+ return ERROR_SUCCESS == lastError;
+}
+
+/**
+ * Sets the access control list for user access for the specified service.
+ *
+ * @param hService The service to set the access control list on
+ * @param pNewAcl The out param ACL which should be freed by caller
+ * @param psd out param security descriptor, should be freed by caller
+ * @return ERROR_SUCCESS if successful
+ */
+DWORD
+SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl,
+ PSECURITY_DESCRIPTOR psd)
+{
+ // Get the current security descriptor needed size
+ DWORD needed = 0;
+ if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION,
+ &psd, 0, &needed)) {
+ if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+ LOG_WARN(("Could not query service object security size. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ DWORD size = needed;
+ psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size);
+ if (!psd) {
+ LOG_WARN(("Could not allocate security descriptor. (%d)",
+ GetLastError()));
+ return ERROR_INSUFFICIENT_BUFFER;
+ }
+
+ // Get the actual security descriptor now
+ if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION,
+ psd, size, &needed)) {
+ LOG_WARN(("Could not allocate security descriptor. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+ }
+
+ // Get the current DACL from the security descriptor.
+ PACL pacl = nullptr;
+ BOOL bDaclPresent = FALSE;
+ BOOL bDaclDefaulted = FALSE;
+ if ( !GetSecurityDescriptorDacl(psd, &bDaclPresent, &pacl,
+ &bDaclDefaulted)) {
+ LOG_WARN(("Could not obtain DACL. (%d)", GetLastError()));
+ return GetLastError();
+ }
+
+ PSID sid;
+ DWORD SIDSize = SECURITY_MAX_SID_SIZE;
+ sid = LocalAlloc(LMEM_FIXED, SIDSize);
+ if (!sid) {
+ LOG_WARN(("Could not allocate SID memory. (%d)", GetLastError()));
+ return GetLastError();
+ }
+
+ if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sid, &SIDSize)) {
+ DWORD lastError = GetLastError();
+ LOG_WARN(("Could not create well known SID. (%d)", lastError));
+ LocalFree(sid);
+ return lastError;
+ }
+
+ // Lookup the account name, the function fails if you don't pass in
+ // a buffer for the domain name but it's not used since we're using
+ // the built in account Sid.
+ SID_NAME_USE accountType;
+ WCHAR accountName[UNLEN + 1] = { L'\0' };
+ WCHAR domainName[DNLEN + 1] = { L'\0' };
+ DWORD accountNameSize = UNLEN + 1;
+ DWORD domainNameSize = DNLEN + 1;
+ if (!LookupAccountSidW(nullptr, sid, accountName,
+ &accountNameSize,
+ domainName, &domainNameSize, &accountType)) {
+ LOG_WARN(("Could not lookup account Sid, will try Users. (%d)",
+ GetLastError()));
+ wcsncpy(accountName, L"Users", UNLEN);
+ }
+
+ // We already have the group name so we can get rid of the SID
+ FreeSid(sid);
+ sid = nullptr;
+
+ // Build the ACE, BuildExplicitAccessWithName cannot fail so it is not logged.
+ EXPLICIT_ACCESS ea;
+ BuildExplicitAccessWithNameW(&ea, accountName,
+ SERVICE_START | SERVICE_STOP | GENERIC_READ,
+ SET_ACCESS, NO_INHERITANCE);
+ DWORD lastError = SetEntriesInAclW(1, (PEXPLICIT_ACCESS)&ea, pacl, &pNewAcl);
+ if (ERROR_SUCCESS != lastError) {
+ LOG_WARN(("Could not set entries in ACL. (%d)", lastError));
+ return lastError;
+ }
+
+ // Initialize a new security descriptor.
+ SECURITY_DESCRIPTOR sd;
+ if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
+ LOG_WARN(("Could not initialize security descriptor. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ // Set the new DACL in the security descriptor.
+ if (!SetSecurityDescriptorDacl(&sd, TRUE, pNewAcl, FALSE)) {
+ LOG_WARN(("Could not set security descriptor DACL. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ // Set the new security descriptor for the service object.
+ if (!SetServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &sd)) {
+ LOG_WARN(("Could not set object security. (%d)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ // Woohoo, raise the roof
+ LOG(("User access was set successfully on the service."));
+ return ERROR_SUCCESS;
+}
diff --git a/toolkit/components/maintenanceservice/serviceinstall.h b/toolkit/components/maintenanceservice/serviceinstall.h
new file mode 100644
index 0000000000..d8532a968e
--- /dev/null
+++ b/toolkit/components/maintenanceservice/serviceinstall.h
@@ -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/. */
+
+#include "readstrings.h"
+
+#define SVC_DISPLAY_NAME L"Mozilla Maintenance Service"
+
+enum SvcInstallAction { UpgradeSvc, InstallSvc, ForceInstallSvc };
+BOOL SvcInstall(SvcInstallAction action);
+BOOL SvcUninstall();
+BOOL StopService();
+BOOL SetUserAccessServiceDACL(SC_HANDLE hService);
+DWORD SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl,
+ PSECURITY_DESCRIPTOR psd);
+
+struct MaintenanceServiceStringTable
+{
+ char serviceDescription[MAX_TEXT_LEN];
+};
+
diff --git a/toolkit/components/maintenanceservice/workmonitor.cpp b/toolkit/components/maintenanceservice/workmonitor.cpp
new file mode 100644
index 0000000000..d06db3ca2b
--- /dev/null
+++ b/toolkit/components/maintenanceservice/workmonitor.cpp
@@ -0,0 +1,758 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <shlobj.h>
+#include <shlwapi.h>
+#include <wtsapi32.h>
+#include <userenv.h>
+#include <shellapi.h>
+
+#pragma comment(lib, "wtsapi32.lib")
+#pragma comment(lib, "userenv.lib")
+#pragma comment(lib, "shlwapi.lib")
+#pragma comment(lib, "ole32.lib")
+#pragma comment(lib, "rpcrt4.lib")
+
+#include "nsWindowsHelpers.h"
+
+#include "workmonitor.h"
+#include "serviceinstall.h"
+#include "servicebase.h"
+#include "registrycertificates.h"
+#include "uachelper.h"
+#include "updatehelper.h"
+#include "pathhash.h"
+#include "errors.h"
+
+// Wait 15 minutes for an update operation to run at most.
+// Updates usually take less than a minute so this seems like a
+// significantly large and safe amount of time to wait.
+static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000;
+wchar_t* MakeCommandLine(int argc, wchar_t** argv);
+BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode);
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
+ LPCWSTR newFileName);
+BOOL DoesFallbackKeyExist();
+
+/*
+ * Read the update.status file and sets isApplying to true if
+ * the status is set to applying.
+ *
+ * @param updateDirPath The directory where update.status is stored
+ * @param isApplying Out parameter for specifying if the status
+ * is set to applying or not.
+ * @return TRUE if the information was filled.
+*/
+static BOOL
+IsStatusApplying(LPCWSTR updateDirPath, BOOL &isApplying)
+{
+ isApplying = FALSE;
+ WCHAR updateStatusFilePath[MAX_PATH + 1] = {L'\0'};
+ wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH);
+ if (!PathAppendSafe(updateStatusFilePath, L"update.status")) {
+ LOG_WARN(("Could not append path for update.status file"));
+ return FALSE;
+ }
+
+ nsAutoHandle statusFile(CreateFileW(updateStatusFilePath, GENERIC_READ,
+ FILE_SHARE_READ |
+ FILE_SHARE_WRITE |
+ FILE_SHARE_DELETE,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+
+ if (INVALID_HANDLE_VALUE == statusFile) {
+ LOG_WARN(("Could not open update.status file"));
+ return FALSE;
+ }
+
+ char buf[32] = { 0 };
+ DWORD read;
+ if (!ReadFile(statusFile, buf, sizeof(buf), &read, nullptr)) {
+ LOG_WARN(("Could not read from update.status file"));
+ return FALSE;
+ }
+
+ const char kApplying[] = "applying";
+ isApplying = strncmp(buf, kApplying,
+ sizeof(kApplying) - 1) == 0;
+ return TRUE;
+}
+
+/**
+ * Determines whether we're staging an update.
+ *
+ * @param argc The argc value normally sent to updater.exe
+ * @param argv The argv value normally sent to updater.exe
+ * @return boolean True if we're staging an update
+ */
+static bool
+IsUpdateBeingStaged(int argc, LPWSTR *argv)
+{
+ // PID will be set to -1 if we're supposed to stage an update.
+ return argc == 4 && !wcscmp(argv[3], L"-1") ||
+ argc == 5 && !wcscmp(argv[4], L"-1");
+}
+
+/**
+ * Determines whether the param only contains digits.
+ *
+ * @param str The string to check
+ * @param boolean True if the param only contains digits
+ */
+static bool
+IsDigits(WCHAR *str)
+{
+ while (*str) {
+ if (!iswdigit(*str++)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Determines whether the command line contains just the directory to apply the
+ * update to (old command line) or if it contains the installation directory and
+ * the directory to apply the update to.
+ *
+ * @param argc The argc value normally sent to updater.exe
+ * @param argv The argv value normally sent to updater.exe
+ * @param boolean True if the command line contains just the directory to apply
+ * the update to
+ */
+static bool
+IsOldCommandline(int argc, LPWSTR *argv)
+{
+ return argc == 4 && !wcscmp(argv[3], L"-1") ||
+ argc >= 4 && (wcsstr(argv[3], L"/replace") || IsDigits(argv[3]));
+}
+
+/**
+ * Gets the installation directory from the arguments passed to updater.exe.
+ *
+ * @param argcTmp The argc value normally sent to updater.exe
+ * @param argvTmp The argv value normally sent to updater.exe
+ * @param aResultDir Buffer to hold the installation directory.
+ */
+static BOOL
+GetInstallationDir(int argcTmp, LPWSTR *argvTmp, WCHAR aResultDir[MAX_PATH + 1])
+{
+ int index = 3;
+ if (IsOldCommandline(argcTmp, argvTmp)) {
+ index = 2;
+ }
+
+ if (argcTmp < index) {
+ return FALSE;
+ }
+
+ wcsncpy(aResultDir, argvTmp[2], MAX_PATH);
+ WCHAR* backSlash = wcsrchr(aResultDir, L'\\');
+ // Make sure that the path does not include trailing backslashes
+ if (backSlash && (backSlash[1] == L'\0')) {
+ *backSlash = L'\0';
+ }
+
+ // The new command line's argv[2] is always the installation directory.
+ if (index == 2) {
+ bool backgroundUpdate = IsUpdateBeingStaged(argcTmp, argvTmp);
+ bool replaceRequest = (argcTmp >= 4 && wcsstr(argvTmp[3], L"/replace"));
+ if (backgroundUpdate || replaceRequest) {
+ return PathRemoveFileSpecW(aResultDir);
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Runs an update process as the service using the SYSTEM account.
+ *
+ * @param argc The number of arguments in argv
+ * @param argv The arguments normally passed to updater.exe
+ * argv[0] must be the path to updater.exe
+ * @param processStarted Set to TRUE if the process was started.
+ * @return TRUE if the update process was run had a return code of 0.
+ */
+BOOL
+StartUpdateProcess(int argc,
+ LPWSTR *argv,
+ LPCWSTR installDir,
+ BOOL &processStarted)
+{
+ LOG(("Starting update process as the service in session 0."));
+ STARTUPINFO si = {0};
+ si.cb = sizeof(STARTUPINFO);
+ si.lpDesktop = L"winsta0\\Default";
+ PROCESS_INFORMATION pi = {0};
+
+ // The updater command line is of the form:
+ // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]]
+ LPWSTR cmdLine = MakeCommandLine(argc, argv);
+
+ int index = 3;
+ if (IsOldCommandline(argc, argv)) {
+ index = 2;
+ }
+
+ // If we're about to start the update process from session 0,
+ // then we should not show a GUI. This only really needs to be done
+ // on Vista and higher, but it's better to keep everything consistent
+ // across all OS if it's of no harm.
+ if (argc >= index) {
+ // Setting the desktop to blank will ensure no GUI is displayed
+ si.lpDesktop = L"";
+ si.dwFlags |= STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_HIDE;
+ }
+
+ // Add an env var for MOZ_USING_SERVICE so the updater.exe can
+ // do anything special that it needs to do for service updates.
+ // Search in updater.cpp for more info on MOZ_USING_SERVICE.
+ putenv(const_cast<char*>("MOZ_USING_SERVICE=1"));
+ LOG(("Starting service with cmdline: %ls", cmdLine));
+ processStarted = CreateProcessW(argv[0], cmdLine,
+ nullptr, nullptr, FALSE,
+ CREATE_DEFAULT_ERROR_MODE,
+ nullptr,
+ nullptr, &si, &pi);
+
+ BOOL updateWasSuccessful = FALSE;
+ if (processStarted) {
+ BOOL processTerminated = FALSE;
+ BOOL noProcessExitCode = FALSE;
+ // Wait for the updater process to finish
+ LOG(("Process was started... waiting on result."));
+ DWORD waitRes = WaitForSingleObject(pi.hProcess, TIME_TO_WAIT_ON_UPDATER);
+ if (WAIT_TIMEOUT == waitRes) {
+ // We waited a long period of time for updater.exe and it never finished
+ // so kill it.
+ TerminateProcess(pi.hProcess, 1);
+ processTerminated = TRUE;
+ } else {
+ // Check the return code of updater.exe to make sure we get 0
+ DWORD returnCode;
+ if (GetExitCodeProcess(pi.hProcess, &returnCode)) {
+ LOG(("Process finished with return code %d.", returnCode));
+ // updater returns 0 if successful.
+ updateWasSuccessful = (returnCode == 0);
+ } else {
+ LOG_WARN(("Process finished but could not obtain return code."));
+ noProcessExitCode = TRUE;
+ }
+ }
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+
+ // Check just in case updater.exe didn't change the status from
+ // applying. If this is the case we report an error.
+ BOOL isApplying = FALSE;
+ if (IsStatusApplying(argv[1], isApplying) && isApplying) {
+ if (updateWasSuccessful) {
+ LOG(("update.status is still applying even though update was "
+ "successful."));
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_STILL_APPLYING_ON_SUCCESS)) {
+ LOG_WARN(("Could not write update.status still applying on "
+ "success error."));
+ }
+ // Since we still had applying we know updater.exe didn't do its
+ // job correctly.
+ updateWasSuccessful = FALSE;
+ } else {
+ LOG_WARN(("update.status is still applying and update was not successful."));
+ int failcode = SERVICE_STILL_APPLYING_ON_FAILURE;
+ if (noProcessExitCode) {
+ failcode = SERVICE_STILL_APPLYING_NO_EXIT_CODE;
+ } else if (processTerminated) {
+ failcode = SERVICE_STILL_APPLYING_TERMINATED;
+ }
+ if (!WriteStatusFailure(argv[1], failcode)) {
+ LOG_WARN(("Could not write update.status still applying on "
+ "failure error."));
+ }
+ }
+ }
+ } else {
+ DWORD lastError = GetLastError();
+ LOG_WARN(("Could not create process as current user, "
+ "updaterPath: %ls; cmdLine: %ls. (%d)",
+ argv[0], cmdLine, lastError));
+ }
+
+ // Empty value on putenv is how you remove an env variable in Windows
+ putenv(const_cast<char*>("MOZ_USING_SERVICE="));
+
+ free(cmdLine);
+ return updateWasSuccessful;
+}
+
+/**
+ * Validates a file as an official updater.
+ *
+ * @param updater Path to the updater to validate
+ * @param installDir Path to the application installation
+ * being updated
+ * @param updateDir Update applyTo direcotry,
+ * where logs will be written
+ *
+ * @return true if updater is the path to a valid updater
+ */
+static bool
+UpdaterIsValid(LPWSTR updater, LPWSTR installDir, LPWSTR updateDir)
+{
+ // Make sure the path to the updater to use for the update is local.
+ // We do this check to make sure that file locking is available for
+ // race condition security checks.
+ BOOL isLocal = FALSE;
+ if (!IsLocalFile(updater, isLocal) || !isLocal) {
+ LOG_WARN(("Filesystem in path %ls is not supported (%d)",
+ updater, GetLastError()));
+ if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_NOT_FIXED_DRIVE)) {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ return false;
+ }
+
+ nsAutoHandle noWriteLock(CreateFileW(updater, GENERIC_READ, FILE_SHARE_READ,
+ nullptr, OPEN_EXISTING, 0, nullptr));
+ if (INVALID_HANDLE_VALUE == noWriteLock) {
+ LOG_WARN(("Could not set no write sharing access on file. (%d)",
+ GetLastError()));
+ if (!WriteStatusFailure(updateDir, SERVICE_COULD_NOT_LOCK_UPDATER)) {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ return false;
+ }
+
+ // Verify that the updater.exe that we are executing is the same
+ // as the one in the installation directory which we are updating.
+ // The installation dir that we are installing to is installDir.
+ WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(installDirUpdater, installDir, MAX_PATH);
+ if (!PathAppendSafe(installDirUpdater, L"updater.exe")) {
+ LOG_WARN(("Install directory updater could not be determined."));
+ return false;
+ }
+
+ BOOL updaterIsCorrect;
+ if (!VerifySameFiles(updater, installDirUpdater, updaterIsCorrect)) {
+ LOG_WARN(("Error checking if the updaters are the same.\n"
+ "Path 1: %ls\nPath 2: %ls", updater, installDirUpdater));
+ return false;
+ }
+
+ if (!updaterIsCorrect) {
+ LOG_WARN(("The updaters do not match, updater will not run.\n"
+ "Path 1: %ls\nPath 2: %ls", updater, installDirUpdater));
+ if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_COMPARE_ERROR)) {
+ LOG_WARN(("Could not write update.status updater compare failure."));
+ }
+ return false;
+ }
+
+ LOG(("updater.exe was compared successfully to the installation directory"
+ " updater.exe."));
+
+ // Check to make sure the updater.exe module has the unique updater identity.
+ // This is a security measure to make sure that the signed executable that
+ // we will run is actually an updater.
+ bool result = true;
+ HMODULE updaterModule = LoadLibraryEx(updater, nullptr,
+ LOAD_LIBRARY_AS_DATAFILE);
+ if (!updaterModule) {
+ LOG_WARN(("updater.exe module could not be loaded. (%d)", GetLastError()));
+ result = false;
+ } else {
+ char updaterIdentity[64];
+ if (!LoadStringA(updaterModule, IDS_UPDATER_IDENTITY,
+ updaterIdentity, sizeof(updaterIdentity))) {
+ LOG_WARN(("The updater.exe application does not contain the Mozilla"
+ " updater identity."));
+ result = false;
+ }
+
+ if (strcmp(updaterIdentity, UPDATER_IDENTITY_STRING)) {
+ LOG_WARN(("The updater.exe identity string is not valid."));
+ result = false;
+ }
+ FreeLibrary(updaterModule);
+ }
+
+ if (result) {
+ LOG(("The updater.exe application contains the Mozilla"
+ " updater identity."));
+ } else {
+ if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_IDENTITY_ERROR)) {
+ LOG_WARN(("Could not write update.status no updater identity."));
+ }
+ return false;
+ }
+
+#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK
+ return DoesBinaryMatchAllowedCertificates(installDir, updater);
+#else
+ return true;
+#endif
+}
+
+/**
+ * Processes a software update command
+ *
+ * @param argc The number of arguments in argv
+ * @param argv The arguments normally passed to updater.exe
+ * argv[0] must be the path to updater.exe
+ * @return TRUE if the update was successful.
+ */
+BOOL
+ProcessSoftwareUpdateCommand(DWORD argc, LPWSTR *argv)
+{
+ BOOL result = TRUE;
+ if (argc < 3) {
+ LOG_WARN(("Not enough command line parameters specified. "
+ "Updating update.status."));
+
+ // We can only update update.status if argv[1] exists. argv[1] is
+ // the directory where the update.status file exists.
+ if (argc < 2 ||
+ !WriteStatusFailure(argv[1], SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS)) {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ return FALSE;
+ }
+
+ WCHAR installDir[MAX_PATH + 1] = {L'\0'};
+ if (!GetInstallationDir(argc, argv, installDir)) {
+ LOG_WARN(("Could not get the installation directory"));
+ if (!WriteStatusFailure(argv[1], SERVICE_INSTALLDIR_ERROR)) {
+ LOG_WARN(("Could not write update.status for GetInstallationDir failure."));
+ }
+ return FALSE;
+ }
+
+ if (UpdaterIsValid(argv[0], installDir, argv[1])) {
+ BOOL updateProcessWasStarted = FALSE;
+ if (StartUpdateProcess(argc, argv, installDir,
+ updateProcessWasStarted)) {
+ LOG(("updater.exe was launched and run successfully!"));
+ LogFlush();
+
+ // Don't attempt to update the service when the update is being staged.
+ if (!IsUpdateBeingStaged(argc, argv)) {
+ // We might not execute code after StartServiceUpdate because
+ // the service installer will stop the service if it is running.
+ StartServiceUpdate(installDir);
+ }
+ } else {
+ result = FALSE;
+ LOG_WARN(("Error running update process. Updating update.status (%d)",
+ GetLastError()));
+ LogFlush();
+
+ // If the update process was started, then updater.exe is responsible for
+ // setting the failure code. If it could not be started then we do the
+ // work. We set an error instead of directly setting status pending
+ // so that the app.update.service.errors pref can be updated when
+ // the callback app restarts.
+ if (!updateProcessWasStarted) {
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_COULD_NOT_BE_STARTED)) {
+ LOG_WARN(("Could not write update.status service update failure. (%d)",
+ GetLastError()));
+ }
+ }
+ }
+ } else {
+ result = FALSE;
+ LOG_WARN(("Could not start process due to certificate check error on "
+ "updater.exe. Updating update.status. (%d)", GetLastError()));
+
+ // When there is a certificate check error on the updater.exe application,
+ // we want to write out the error.
+ if (!WriteStatusFailure(argv[1],
+ SERVICE_UPDATER_SIGN_ERROR)) {
+ LOG_WARN(("Could not write pending state to update.status. (%d)",
+ GetLastError()));
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Obtains the updater path alongside a subdir of the service binary.
+ * The purpose of this function is to return a path that is likely high
+ * integrity and therefore more safe to execute code from.
+ *
+ * @param serviceUpdaterPath Out parameter for the path where the updater
+ * should be copied to.
+ * @return TRUE if a file path was obtained.
+ */
+BOOL
+GetSecureUpdaterPath(WCHAR serviceUpdaterPath[MAX_PATH + 1])
+{
+ if (!GetModuleFileNameW(nullptr, serviceUpdaterPath, MAX_PATH)) {
+ LOG_WARN(("Could not obtain module filename when attempting to "
+ "use a secure updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(serviceUpdaterPath)) {
+ LOG_WARN(("Couldn't remove file spec when attempting to use a secure "
+ "updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(serviceUpdaterPath, L"update")) {
+ LOG_WARN(("Couldn't append file spec when attempting to use a secure "
+ "updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ CreateDirectoryW(serviceUpdaterPath, nullptr);
+
+ if (!PathAppendSafe(serviceUpdaterPath, L"updater.exe")) {
+ LOG_WARN(("Couldn't append file spec when attempting to use a secure "
+ "updater path. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Deletes the passed in updater path and the associated updater.ini file.
+ *
+ * @param serviceUpdaterPath The path to delete.
+ * @return TRUE if a file was deleted.
+ */
+BOOL
+DeleteSecureUpdater(WCHAR serviceUpdaterPath[MAX_PATH + 1])
+{
+ BOOL result = FALSE;
+ if (serviceUpdaterPath[0]) {
+ result = DeleteFileW(serviceUpdaterPath);
+ if (!result && GetLastError() != ERROR_PATH_NOT_FOUND &&
+ GetLastError() != ERROR_FILE_NOT_FOUND) {
+ LOG_WARN(("Could not delete service updater path: '%ls'.",
+ serviceUpdaterPath));
+ }
+
+ WCHAR updaterINIPath[MAX_PATH + 1] = { L'\0' };
+ if (PathGetSiblingFilePath(updaterINIPath, serviceUpdaterPath,
+ L"updater.ini")) {
+ result = DeleteFileW(updaterINIPath);
+ if (!result && GetLastError() != ERROR_PATH_NOT_FOUND &&
+ GetLastError() != ERROR_FILE_NOT_FOUND) {
+ LOG_WARN(("Could not delete service updater INI path: '%ls'.",
+ updaterINIPath));
+ }
+ }
+ }
+ return result;
+}
+
+/**
+ * Executes a service command.
+ *
+ * @param argc The number of arguments in argv
+ * @param argv The service command line arguments, argv[0] and argv[1]
+ * and automatically included by Windows. argv[2] is the
+ * service command.
+ *
+ * @return FALSE if there was an error executing the service command.
+ */
+BOOL
+ExecuteServiceCommand(int argc, LPWSTR *argv)
+{
+ if (argc < 3) {
+ LOG_WARN(("Not enough command line arguments to execute a service command"));
+ return FALSE;
+ }
+
+ // The tests work by making sure the log has changed, so we put a
+ // unique ID in the log.
+ RPC_WSTR guidString = RPC_WSTR(L"");
+ GUID guid;
+ HRESULT hr = CoCreateGuid(&guid);
+ if (SUCCEEDED(hr)) {
+ UuidToString(&guid, &guidString);
+ }
+ LOG(("Executing service command %ls, ID: %ls",
+ argv[2], reinterpret_cast<LPCWSTR>(guidString)));
+ RpcStringFree(&guidString);
+
+ BOOL result = FALSE;
+ if (!lstrcmpi(argv[2], L"software-update")) {
+ // This check is also performed in updater.cpp and is performed here
+ // as well since the maintenance service can be called directly.
+ if (argc < 4 || !IsValidFullPath(argv[4])) {
+ // Since the status file is written to the patch directory and the patch
+ // directory is invalid don't write the status file.
+ LOG_WARN(("The patch directory path is not valid for this application."));
+ return FALSE;
+ }
+
+ // This check is also performed in updater.cpp and is performed here
+ // as well since the maintenance service can be called directly.
+ if (argc < 5 || !IsValidFullPath(argv[5])) {
+ LOG_WARN(("The install directory path is not valid for this application."));
+ if (!WriteStatusFailure(argv[4], SERVICE_INVALID_INSTALL_DIR_PATH_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+
+ if (!IsOldCommandline(argc - 3, argv + 3)) {
+ // This check is also performed in updater.cpp and is performed here
+ // as well since the maintenance service can be called directly.
+ if (argc < 6 || !IsValidFullPath(argv[6])) {
+ LOG_WARN(("The working directory path is not valid for this application."));
+ if (!WriteStatusFailure(argv[4], SERVICE_INVALID_WORKING_DIR_PATH_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+
+ // These checks are also performed in updater.cpp and is performed here
+ // as well since the maintenance service can be called directly.
+ if (_wcsnicmp(argv[6], argv[5], MAX_PATH) != 0) {
+ if (wcscmp(argv[7], L"-1") != 0 && !wcsstr(argv[7], L"/replace")) {
+ LOG_WARN(("Installation directory and working directory must be the "
+ "same for non-staged updates. Exiting."));
+ if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+
+ NS_tchar workingDirParent[MAX_PATH];
+ NS_tsnprintf(workingDirParent,
+ sizeof(workingDirParent) / sizeof(workingDirParent[0]),
+ NS_T("%s"), argv[6]);
+ if (!PathRemoveFileSpecW(workingDirParent)) {
+ LOG_WARN(("Couldn't remove file spec when attempting to verify the "
+ "working directory path. (%d)", GetLastError()));
+ if (!WriteStatusFailure(argv[4], REMOVE_FILE_SPEC_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+
+ if (_wcsnicmp(workingDirParent, argv[5], MAX_PATH) != 0) {
+ LOG_WARN(("The apply-to directory must be the same as or "
+ "a child of the installation directory! Exiting."));
+ if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+ }
+
+ }
+
+ // Use the passed in command line arguments for the update, except for the
+ // path to updater.exe. We always look for updater.exe in the installation
+ // directory, then we copy updater.exe to a the directory of the
+ // MozillaMaintenance service so that a low integrity process cannot
+ // replace the updater.exe at any point and use that for the update.
+ // It also makes DLL injection attacks harder.
+ WCHAR installDir[MAX_PATH + 1] = { L'\0' };
+ if (!GetInstallationDir(argc - 3, argv + 3, installDir)) {
+ LOG_WARN(("Could not get the installation directory"));
+ if (!WriteStatusFailure(argv[4], SERVICE_INSTALLDIR_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+
+ if (!DoesFallbackKeyExist()) {
+ WCHAR maintenanceServiceKey[MAX_PATH + 1];
+ if (CalculateRegistryPathFromFilePath(installDir, maintenanceServiceKey)) {
+ LOG(("Checking for Maintenance Service registry. key: '%ls'",
+ maintenanceServiceKey));
+ HKEY baseKey = nullptr;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ maintenanceServiceKey, 0,
+ KEY_READ | KEY_WOW64_64KEY,
+ &baseKey) != ERROR_SUCCESS) {
+ LOG_WARN(("The maintenance service registry key does not exist."));
+ if (!WriteStatusFailure(argv[4], SERVICE_INSTALL_DIR_REG_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+ RegCloseKey(baseKey);
+ } else {
+ if (!WriteStatusFailure(argv[4], SERVICE_CALC_REG_PATH_ERROR)) {
+ LOG_WARN(("Could not write update.status for previous failure."));
+ }
+ return FALSE;
+ }
+ }
+
+ WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(installDirUpdater, installDir, MAX_PATH);
+ if (!PathAppendSafe(installDirUpdater, L"updater.exe")) {
+ LOG_WARN(("Install directory updater could not be determined."));
+ result = FALSE;
+ }
+
+ result = UpdaterIsValid(installDirUpdater, installDir, argv[4]);
+
+ WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' };
+ if (result) {
+ result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging
+ }
+ if (result) {
+ LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.",
+ installDirUpdater, secureUpdaterPath));
+ DeleteSecureUpdater(secureUpdaterPath);
+ result = CopyFileW(installDirUpdater, secureUpdaterPath, FALSE);
+ }
+
+ if (!result) {
+ LOG_WARN(("Could not copy path to secure location. (%d)",
+ GetLastError()));
+ if (!WriteStatusFailure(argv[4], SERVICE_COULD_NOT_COPY_UPDATER)) {
+ LOG_WARN(("Could not write update.status could not copy updater error"));
+ }
+ } else {
+
+ // We obtained the path and copied it successfully, update the path to
+ // use for the service update.
+ argv[3] = secureUpdaterPath;
+
+ WCHAR installDirUpdaterINIPath[MAX_PATH + 1] = { L'\0' };
+ WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' };
+ if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath,
+ L"updater.ini") &&
+ PathGetSiblingFilePath(installDirUpdaterINIPath, installDirUpdater,
+ L"updater.ini")) {
+ // This is non fatal if it fails there is no real harm
+ if (!CopyFileW(installDirUpdaterINIPath, secureUpdaterINIPath, FALSE)) {
+ LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%d)",
+ installDirUpdaterINIPath, secureUpdaterINIPath, GetLastError()));
+ }
+ }
+
+ result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3);
+ DeleteSecureUpdater(secureUpdaterPath);
+ }
+
+ // We might not reach here if the service install succeeded
+ // because the service self updates itself and the service
+ // installer will stop the service.
+ LOG(("Service command %ls complete.", argv[2]));
+ } else {
+ LOG_WARN(("Service command not recognized: %ls.", argv[2]));
+ // result is already set to FALSE
+ }
+
+ LOG(("service command %ls complete with result: %ls.",
+ argv[1], (result ? L"Success" : L"Failure")));
+ return TRUE;
+}
diff --git a/toolkit/components/maintenanceservice/workmonitor.h b/toolkit/components/maintenanceservice/workmonitor.h
new file mode 100644
index 0000000000..ac4cd679bf
--- /dev/null
+++ b/toolkit/components/maintenanceservice/workmonitor.h
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+BOOL ExecuteServiceCommand(int argc, LPWSTR *argv);
diff --git a/toolkit/components/mediasniffer/moz.build b/toolkit/components/mediasniffer/moz.build
new file mode 100644
index 0000000000..d4f935f562
--- /dev/null
+++ b/toolkit/components/mediasniffer/moz.build
@@ -0,0 +1,22 @@
+# -*- 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 += ['test/unit/xpcshell.ini']
+
+EXPORTS += [
+ 'nsMediaSniffer.h',
+]
+
+UNIFIED_SOURCES += [
+ 'mp3sniff.c',
+ 'nsMediaSniffer.cpp',
+ 'nsMediaSnifferModule.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Core', 'Video/Audio')
diff --git a/toolkit/components/mediasniffer/mp3sniff.c b/toolkit/components/mediasniffer/mp3sniff.c
new file mode 100644
index 0000000000..a515d9c583
--- /dev/null
+++ b/toolkit/components/mediasniffer/mp3sniff.c
@@ -0,0 +1,156 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* MPEG format parsing */
+
+#include "mp3sniff.h"
+
+/* Maximum packet size is 320 kbits/s * 144 / 32 kHz + 1 padding byte */
+#define MP3_MAX_SIZE 1441
+
+typedef struct {
+ int version;
+ int layer;
+ int errp;
+ int bitrate;
+ int freq;
+ int pad;
+ int priv;
+ int mode;
+ int modex;
+ int copyright;
+ int original;
+ int emphasis;
+} mp3_header;
+
+/* Parse the 4-byte header in p and fill in the header struct. */
+static void mp3_parse(const uint8_t *p, mp3_header *header)
+{
+ const int bitrates[2][16] = {
+ /* MPEG version 1 layer 3 bitrates. */
+ {0, 32000, 40000, 48000, 56000, 64000, 80000, 96000,
+ 112000, 128000, 160000, 192000, 224000, 256000, 320000, 0},
+ /* MPEG Version 2 and 2.5 layer 3 bitrates */
+ {0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000,
+ 80000, 96000, 112000, 128000, 144000, 160000, 0} };
+ const int samplerates[4] = {44100, 48000, 32000, 0};
+
+ header->version = (p[1] & 0x18) >> 3;
+ header->layer = 4 - ((p[1] & 0x06) >> 1);
+ header->errp = (p[1] & 0x01);
+
+ header->bitrate = bitrates[(header->version & 1) ? 0 : 1][(p[2] & 0xf0) >> 4];
+ header->freq = samplerates[(p[2] & 0x0c) >> 2];
+ if (header->version == 2) header->freq >>= 1;
+ else if (header->version == 0) header->freq >>= 2;
+ header->pad = (p[2] & 0x02) >> 1;
+ header->priv = (p[2] & 0x01);
+
+ header->mode = (p[3] & 0xc0) >> 6;
+ header->modex = (p[3] & 0x30) >> 4;
+ header->copyright = (p[3] & 0x08) >> 3;
+ header->original = (p[3] & 0x04) >> 2;
+ header->emphasis = (p[3] & 0x03);
+}
+
+/* calculate the size of an mp3 frame from its header */
+static int mp3_framesize(mp3_header *header)
+{
+ int size;
+ int scale;
+
+ if ((header->version & 1) == 0) scale = 72;
+ else scale = 144;
+ size = header->bitrate * scale / header->freq;
+ if (header->pad) size += 1;
+
+ return size;
+}
+
+static int is_mp3(const uint8_t *p, long length) {
+ /* Do we have enough room to see a 4 byte header? */
+ if (length < 4) return 0;
+ /* Do we have a sync pattern? */
+ if (p[0] == 0xff && (p[1] & 0xe0) == 0xe0) {
+ /* Do we have any illegal field values? */
+ if (((p[1] & 0x06) >> 1) == 0) return 0; /* No layer 4 */
+ if (((p[2] & 0xf0) >> 4) == 15) return 0; /* Bitrate can't be 1111 */
+ if (((p[2] & 0x0c) >> 2) == 3) return 0; /* Samplerate can't be 11 */
+ /* Looks like a header. */
+ if ((4 - ((p[1] & 0x06) >> 1)) != 3) return 0; /* Only want level 3 */
+ return 1;
+ }
+ return 0;
+}
+
+/* Identify an ID3 tag based on its header. */
+/* http://id3.org/id3v2.4.0-structure */
+static int is_id3(const uint8_t *p, long length) {
+ /* Do we have enough room to see the header? */
+ if (length < 10) return 0;
+ /* Do we have a sync pattern? */
+ if (p[0] == 'I' && p[1] == 'D' && p[2] == '3') {
+ if (p[3] == 0xff || p[4] == 0xff) return 0; /* Illegal version. */
+ if (p[6] & 0x80 || p[7] & 0x80 ||
+ p[8] & 0x80) return 0; /* Bad length encoding. */
+ /* Looks like an id3 header. */
+ return 1;
+ }
+ return 0;
+}
+
+/* Calculate the size of an id3 tag structure from its header. */
+static int id3_framesize(const uint8_t *p, long length)
+{
+ int size;
+
+ /* Header is 10 bytes. */
+ if (length < 10) {
+ return 0;
+ }
+ /* Frame is header plus declared size. */
+ size = 10 + (p[9] | (p[8] << 7) | (p[7] << 14) | (p[6] << 21));
+
+ return size;
+}
+
+int mp3_sniff(const uint8_t *buf, long length)
+{
+ mp3_header header;
+ const uint8_t *p;
+ long skip;
+ long avail;
+
+ p = buf;
+ avail = length;
+ while (avail >= 4) {
+ if (is_id3(p, avail)) {
+ /* Skip over any id3 tags */
+ skip = id3_framesize(p, avail);
+ p += skip;
+ avail -= skip;
+ } else if (is_mp3(p, avail)) {
+ mp3_parse(p, &header);
+ skip = mp3_framesize(&header);
+ if (skip < 4 || skip + 4 >= avail) {
+ return 0;
+ }
+ p += skip;
+ avail -= skip;
+ /* Check for a second header at the expected offset. */
+ if (is_mp3(p, avail)) {
+ /* Looks like mp3. */
+ return 1;
+ } else {
+ /* No second header. Not mp3. */
+ return 0;
+ }
+ } else {
+ /* No id3 tag or mp3 header. Not mp3. */
+ return 0;
+ }
+ }
+
+ return 0;
+}
diff --git a/toolkit/components/mediasniffer/mp3sniff.h b/toolkit/components/mediasniffer/mp3sniff.h
new file mode 100644
index 0000000000..5b041a0a2d
--- /dev/null
+++ b/toolkit/components/mediasniffer/mp3sniff.h
@@ -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/. */
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int mp3_sniff(const uint8_t *buf, long length);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/toolkit/components/mediasniffer/nsMediaSniffer.cpp b/toolkit/components/mediasniffer/nsMediaSniffer.cpp
new file mode 100644
index 0000000000..29ba311e60
--- /dev/null
+++ b/toolkit/components/mediasniffer/nsMediaSniffer.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 tw=80 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsMediaSniffer.h"
+#include "nsIHttpChannel.h"
+#include "nsString.h"
+#include "nsMimeTypes.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/ModuleUtils.h"
+#include "mp3sniff.h"
+#include "nestegg/nestegg.h"
+#include "FlacDemuxer.h"
+
+#include "nsIClassInfoImpl.h"
+#include <algorithm>
+
+// The minimum number of bytes that are needed to attempt to sniff an mp4 file.
+static const unsigned MP4_MIN_BYTES_COUNT = 12;
+// The maximum number of bytes to consider when attempting to sniff a file.
+static const uint32_t MAX_BYTES_SNIFFED = 512;
+// The maximum number of bytes to consider when attempting to sniff for a mp3
+// bitstream.
+// This is 320kbps * 144 / 32kHz + 1 padding byte + 4 bytes of capture pattern.
+static const uint32_t MAX_BYTES_SNIFFED_MP3 = 320 * 144 / 32 + 1 + 4;
+
+NS_IMPL_ISUPPORTS(nsMediaSniffer, nsIContentSniffer)
+
+nsMediaSnifferEntry nsMediaSniffer::sSnifferEntries[] = {
+ // The string OggS, followed by the null byte.
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF\xFF", "OggS", APPLICATION_OGG),
+ // The string RIFF, followed by four bytes, followed by the string WAVE
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF", "RIFF\x00\x00\x00\x00WAVE", AUDIO_WAV),
+ // mp3 with ID3 tags, the string "ID3".
+ PATTERN_ENTRY("\xFF\xFF\xFF", "ID3", AUDIO_MP3),
+ // FLAC with standard header
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "fLaC", AUDIO_FLAC)
+};
+
+// For a complete list of file types, see http://www.ftyps.com/index.html
+nsMediaSnifferEntry sFtypEntries[] = {
+ PATTERN_ENTRY("\xFF\xFF\xFF", "mp4", VIDEO_MP4), // Could be mp41 or mp42.
+ PATTERN_ENTRY("\xFF\xFF\xFF", "avc", VIDEO_MP4), // Could be avc1, avc2, ...
+ PATTERN_ENTRY("\xFF\xFF\xFF", "3gp", VIDEO_3GPP), // Could be 3gp4, 3gp5, ...
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "M4A ", AUDIO_MP4),
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "M4P ", AUDIO_MP4),
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "qt ", VIDEO_QUICKTIME),
+ PATTERN_ENTRY("\xFF\xFF\xFF", "iso", VIDEO_MP4), // Could be isom or iso2.
+ PATTERN_ENTRY("\xFF\xFF\xFF\xFF", "mmp4", VIDEO_MP4),
+};
+
+static bool MatchesBrands(const uint8_t aData[4], nsACString& aSniffedType)
+{
+ for (size_t i = 0; i < mozilla::ArrayLength(sFtypEntries); ++i) {
+ const auto& currentEntry = sFtypEntries[i];
+ bool matched = true;
+ MOZ_ASSERT(currentEntry.mLength <= 4, "Pattern is too large to match brand strings.");
+ for (uint32_t j = 0; j < currentEntry.mLength; ++j) {
+ if ((currentEntry.mMask[j] & aData[j]) != currentEntry.mPattern[j]) {
+ matched = false;
+ break;
+ }
+ }
+ if (matched) {
+ aSniffedType.AssignASCII(currentEntry.mContentType);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// This function implements sniffing algorithm for MP4 family file types,
+// including MP4 (described at http://mimesniff.spec.whatwg.org/#signature-for-mp4),
+// M4A (Apple iTunes audio), and 3GPP.
+static bool MatchesMP4(const uint8_t* aData, const uint32_t aLength, nsACString& aSniffedType)
+{
+ if (aLength <= MP4_MIN_BYTES_COUNT) {
+ return false;
+ }
+ // Conversion from big endian to host byte order.
+ uint32_t boxSize = (uint32_t)(aData[3] | aData[2] << 8 | aData[1] << 16 | aData[0] << 24);
+
+ // Boxsize should be evenly divisible by 4.
+ if (boxSize % 4 || aLength < boxSize) {
+ return false;
+ }
+ // The string "ftyp".
+ if (aData[4] != 0x66 ||
+ aData[5] != 0x74 ||
+ aData[6] != 0x79 ||
+ aData[7] != 0x70) {
+ return false;
+ }
+ if (MatchesBrands(&aData[8], aSniffedType)) {
+ return true;
+ }
+ // Skip minor_version (bytes 12-15).
+ uint32_t bytesRead = 16;
+ while (bytesRead < boxSize) {
+ if (MatchesBrands(&aData[bytesRead], aSniffedType)) {
+ return true;
+ }
+ bytesRead += 4;
+ }
+
+ return false;
+}
+
+static bool MatchesWebM(const uint8_t* aData, const uint32_t aLength)
+{
+ return nestegg_sniff((uint8_t*)aData, aLength) ? true : false;
+}
+
+// This function implements mp3 sniffing based on parsing
+// packet headers and looking for expected boundaries.
+static bool MatchesMP3(const uint8_t* aData, const uint32_t aLength)
+{
+ return mp3_sniff(aData, (long)aLength);
+}
+
+static bool MatchesFLAC(const uint8_t* aData, const uint32_t aLength)
+{
+ return mozilla::FlacDemuxer::FlacSniffer(aData, aLength);
+}
+
+NS_IMETHODIMP
+nsMediaSniffer::GetMIMETypeFromContent(nsIRequest* aRequest,
+ const uint8_t* aData,
+ const uint32_t aLength,
+ nsACString& aSniffedType)
+{
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (channel) {
+ nsLoadFlags loadFlags = 0;
+ channel->GetLoadFlags(&loadFlags);
+ if (!(loadFlags & nsIChannel::LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE)) {
+ // For media, we want to sniff only if the Content-Type is unknown, or if it
+ // is application/octet-stream.
+ nsAutoCString contentType;
+ nsresult rv = channel->GetContentType(contentType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!contentType.IsEmpty() &&
+ !contentType.EqualsLiteral(APPLICATION_OCTET_STREAM) &&
+ !contentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ }
+
+ const uint32_t clampedLength = std::min(aLength, MAX_BYTES_SNIFFED);
+
+ for (size_t i = 0; i < mozilla::ArrayLength(sSnifferEntries); ++i) {
+ const nsMediaSnifferEntry& currentEntry = sSnifferEntries[i];
+ if (clampedLength < currentEntry.mLength || currentEntry.mLength == 0) {
+ continue;
+ }
+ bool matched = true;
+ for (uint32_t j = 0; j < currentEntry.mLength; ++j) {
+ if ((currentEntry.mMask[j] & aData[j]) != currentEntry.mPattern[j]) {
+ matched = false;
+ break;
+ }
+ }
+ if (matched) {
+ aSniffedType.AssignASCII(currentEntry.mContentType);
+ return NS_OK;
+ }
+ }
+
+ if (MatchesMP4(aData, clampedLength, aSniffedType)) {
+ return NS_OK;
+ }
+
+ if (MatchesWebM(aData, clampedLength)) {
+ aSniffedType.AssignLiteral(VIDEO_WEBM);
+ return NS_OK;
+ }
+
+ // Bug 950023: 512 bytes are often not enough to sniff for mp3.
+ if (MatchesMP3(aData, std::min(aLength, MAX_BYTES_SNIFFED_MP3))) {
+ aSniffedType.AssignLiteral(AUDIO_MP3);
+ return NS_OK;
+ }
+
+ // Flac frames are generally big, often in excess of 24kB.
+ // Using a size of MAX_BYTES_SNIFFED effectively means that we will only
+ // recognize flac content if it starts with a frame.
+ if (MatchesFLAC(aData, clampedLength)) {
+ aSniffedType.AssignLiteral(AUDIO_FLAC);
+ return NS_OK;
+ }
+
+ // Could not sniff the media type, we are required to set it to
+ // application/octet-stream.
+ aSniffedType.AssignLiteral(APPLICATION_OCTET_STREAM);
+ return NS_ERROR_NOT_AVAILABLE;
+}
diff --git a/toolkit/components/mediasniffer/nsMediaSniffer.h b/toolkit/components/mediasniffer/nsMediaSniffer.h
new file mode 100644
index 0000000000..45f6ac8544
--- /dev/null
+++ b/toolkit/components/mediasniffer/nsMediaSniffer.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 tw=80 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsMediaSniffer_h
+#define nsMediaSniffer_h
+
+#include "nsIModule.h"
+#include "nsIFactory.h"
+
+#include "nsIComponentManager.h"
+#include "nsIComponentRegistrar.h"
+#include "nsIContentSniffer.h"
+#include "mozilla/Attributes.h"
+
+// ed905ba3-c656-480e-934e-6bc35bd36aff
+#define NS_MEDIA_SNIFFER_CID \
+{0x3fdd6c28, 0x5b87, 0x4e3e, \
+{0x8b, 0x57, 0x8e, 0x83, 0xc2, 0x3c, 0x1a, 0x6d}}
+
+#define NS_MEDIA_SNIFFER_CONTRACTID "@mozilla.org/media/sniffer;1"
+
+#define PATTERN_ENTRY(mask, pattern, contentType) \
+ {(const uint8_t*)mask, (const uint8_t*)pattern, sizeof(mask) - 1, contentType}
+
+struct nsMediaSnifferEntry {
+ const uint8_t* mMask;
+ const uint8_t* mPattern;
+ const uint32_t mLength;
+ const char* mContentType;
+};
+
+class nsMediaSniffer final : public nsIContentSniffer
+{
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTSNIFFER
+
+ private:
+ ~nsMediaSniffer() {}
+
+ static nsMediaSnifferEntry sSnifferEntries[];
+};
+
+#endif
diff --git a/toolkit/components/mediasniffer/nsMediaSnifferModule.cpp b/toolkit/components/mediasniffer/nsMediaSnifferModule.cpp
new file mode 100644
index 0000000000..f95e610847
--- /dev/null
+++ b/toolkit/components/mediasniffer/nsMediaSnifferModule.cpp
@@ -0,0 +1,37 @@
+/* -*- 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 "nsMediaSniffer.h"
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsMediaSniffer)
+
+NS_DEFINE_NAMED_CID(NS_MEDIA_SNIFFER_CID);
+
+static const mozilla::Module::CIDEntry kMediaSnifferCIDs[] = {
+ { &kNS_MEDIA_SNIFFER_CID, false, nullptr, nsMediaSnifferConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kMediaSnifferContracts[] = {
+ { NS_MEDIA_SNIFFER_CONTRACTID, &kNS_MEDIA_SNIFFER_CID },
+ { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kMediaSnifferCategories[] = {
+ { "content-sniffing-services", NS_MEDIA_SNIFFER_CONTRACTID, NS_MEDIA_SNIFFER_CONTRACTID},
+ { "net-content-sniffers", NS_MEDIA_SNIFFER_CONTRACTID, NS_MEDIA_SNIFFER_CONTRACTID},
+ { nullptr }
+};
+
+static const mozilla::Module kMediaSnifferModule = {
+ mozilla::Module::kVersion,
+ kMediaSnifferCIDs,
+ kMediaSnifferContracts,
+ kMediaSnifferCategories
+};
+
+NSMODULE_DEFN(nsMediaSnifferModule) = &kMediaSnifferModule;
diff --git a/toolkit/components/mediasniffer/test/unit/.eslintrc.js b/toolkit/components/mediasniffer/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4 b/toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4
new file mode 100644
index 0000000000..f00731d7e2
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/bug1079747.mp4
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/detodos.mp3 b/toolkit/components/mediasniffer/test/unit/data/detodos.mp3
new file mode 100644
index 0000000000..12e3f89c20
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/detodos.mp3
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/ff-inst.exe b/toolkit/components/mediasniffer/test/unit/data/ff-inst.exe
new file mode 100644
index 0000000000..0f02f36e1a
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/ff-inst.exe
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/file.mkv b/toolkit/components/mediasniffer/test/unit/data/file.mkv
new file mode 100644
index 0000000000..4618cda032
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/file.mkv
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/file.webm b/toolkit/components/mediasniffer/test/unit/data/file.webm
new file mode 100644
index 0000000000..7bc738b8b4
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/file.webm
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/fl10.mp2 b/toolkit/components/mediasniffer/test/unit/data/fl10.mp2
new file mode 100644
index 0000000000..bf84d73675
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/fl10.mp2
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/he_free.mp3 b/toolkit/components/mediasniffer/test/unit/data/he_free.mp3
new file mode 100644
index 0000000000..e3da8e6a72
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/he_free.mp3
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/id3tags.mp3 b/toolkit/components/mediasniffer/test/unit/data/id3tags.mp3
new file mode 100644
index 0000000000..23091e6667
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/id3tags.mp3
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3 b/toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3
new file mode 100644
index 0000000000..5ad89786fa
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/notags-bad.mp3
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3 b/toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3
new file mode 100644
index 0000000000..949b7c4687
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/notags-scan.mp3
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/data/notags.mp3 b/toolkit/components/mediasniffer/test/unit/data/notags.mp3
new file mode 100644
index 0000000000..c7db943617
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/data/notags.mp3
Binary files differ
diff --git a/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js b/toolkit/components/mediasniffer/test/unit/test_mediasniffer.js
new file mode 100644
index 0000000000..b26d554a8e
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/test_mediasniffer.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/. */
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const PATH = "/file.meh";
+var httpserver = new HttpServer();
+
+// Each time, the data consist in a string that should be sniffed as Ogg.
+const data = "OggS\0meeeh.";
+var testRan = 0;
+
+// If the content-type is not present, or if it's application/octet-stream, it
+// should be sniffed to application/ogg by the media sniffer. Otherwise, it
+// should not be changed.
+const tests = [
+ // Those three first case are the case of a media loaded in a media element.
+ // All three should be sniffed.
+ { contentType: "",
+ expectedContentType: "application/ogg",
+ flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | Ci.nsIChannel.LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE },
+ { contentType: "application/octet-stream",
+ expectedContentType: "application/ogg",
+ flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | Ci.nsIChannel.LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE },
+ { contentType: "application/something",
+ expectedContentType: "application/ogg",
+ flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | Ci.nsIChannel.LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE },
+ // This last cases test the case of a channel opened while allowing content
+ // sniffers to override the content-type, like in the docshell.
+ { contentType: "application/octet-stream",
+ expectedContentType: "application/ogg",
+ flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS },
+ { contentType: "",
+ expectedContentType: "application/ogg",
+ flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS },
+ { contentType: "application/something",
+ expectedContentType: "application/something",
+ flags: Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS },
+];
+
+// A basic listener that reads checks the if we sniffed properly.
+var listener = {
+ onStartRequest: function(request, context) {
+ do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType,
+ tests[testRan].expectedContentType);
+ },
+
+ onDataAvailable: function(request, context, stream, offset, count) {
+ try {
+ var bis = Components.classes["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Components.interfaces.nsIBinaryInputStream);
+ bis.setInputStream(stream);
+ bis.readByteArray(bis.available());
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest: function(request, context, status) {
+ testRan++;
+ runNext();
+ }
+};
+
+function setupChannel(url, flags)
+{
+ let uri = "http://localhost:" +
+ httpserver.identity.primaryPort + url;
+ var chan = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_MEDIA
+ });
+ chan.loadFlags |= flags;
+ var httpChan = chan.QueryInterface(Components.interfaces.nsIHttpChannel);
+ return httpChan;
+}
+
+function runNext() {
+ if (testRan == tests.length) {
+ do_test_finished();
+ return;
+ }
+ var channel = setupChannel(PATH, tests[testRan].flags);
+ httpserver.registerPathHandler(PATH, function(request, response) {
+ response.setHeader("Content-Type", tests[testRan].contentType, false);
+ response.bodyOutputStream.write(data, data.length);
+ });
+ channel.asyncOpen2(listener);
+}
+
+function run_test() {
+ httpserver.start(-1);
+ do_test_pending();
+ try {
+ runNext();
+ } catch (e) {
+ print("ERROR - " + e + "\n");
+ }
+}
diff --git a/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js b/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.js
new file mode 100644
index 0000000000..ce30a5c1ba
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/test_mediasniffer_ext.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/. */
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cc = Components.classes;
+var CC = Components.Constructor;
+
+var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream");
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+var httpserver = new HttpServer();
+
+var testRan = 0;
+
+// The tests files we want to test, and the type we should have after sniffing.
+const tests = [
+ // Real webm and mkv files truncated to 512 bytes.
+ { path: "data/file.webm", expected: "video/webm" },
+ { path: "data/file.mkv", expected: "application/octet-stream" },
+ // MP3 files with and without id3 headers truncated to 512 bytes.
+ // NB these have 208/209 byte frames, but mp3 can require up to
+ // 1445 bytes to detect with our method.
+ { path: "data/id3tags.mp3", expected: "audio/mpeg" },
+ { path: "data/notags.mp3", expected: "audio/mpeg" },
+ // MPEG-2 mp3 files.
+ { path: "data/detodos.mp3", expected: "audio/mpeg" },
+ // Padding bit flipped in the first header: sniffing should fail.
+ { path: "data/notags-bad.mp3", expected: "application/octet-stream" },
+ // Garbage before header: sniffing should fail.
+ { path: "data/notags-scan.mp3", expected: "application/octet-stream" },
+ // VBR from the layer III test patterns. We can't sniff this.
+ { path: "data/he_free.mp3", expected: "application/octet-stream" },
+ // Make sure we reject mp2, which has a similar header.
+ { path: "data/fl10.mp2", expected: "application/octet-stream" },
+ // Truncated ff installer regression test for bug 875769.
+ { path: "data/ff-inst.exe", expected: "application/octet-stream" },
+ // MP4 with invalid box size (0) for "ftyp".
+ { path: "data/bug1079747.mp4", expected: "application/octet-stream" },
+];
+
+// A basic listener that reads checks the if we sniffed properly.
+var listener = {
+ onStartRequest: function(request, context) {
+ do_print("Sniffing " + tests[testRan].path);
+ do_check_eq(request.QueryInterface(Ci.nsIChannel).contentType, tests[testRan].expected);
+ },
+
+ onDataAvailable: function(request, context, stream, offset, count) {
+ try {
+ var bis = Components.classes["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Components.interfaces.nsIBinaryInputStream);
+ bis.setInputStream(stream);
+ bis.readByteArray(bis.available());
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest: function(request, context, status) {
+ testRan++;
+ runNext();
+ }
+};
+
+function setupChannel(url) {
+ var chan = NetUtil.newChannel({
+ uri: "http://localhost:" + httpserver.identity.primaryPort + url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_MEDIA
+ });
+ var httpChan = chan.QueryInterface(Components.interfaces.nsIHttpChannel);
+ return httpChan;
+}
+
+function runNext() {
+ if (testRan == tests.length) {
+ do_test_finished();
+ return;
+ }
+ var channel = setupChannel("/");
+ channel.asyncOpen2(listener);
+}
+
+function getFileContents(aFile) {
+ var fileStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ fileStream.init(aFile, 1, -1, null);
+ var bis = Components.classes["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Components.interfaces.nsIBinaryInputStream);
+ bis.setInputStream(fileStream);
+
+ var data = bis.readByteArray(bis.available());
+
+ return data;
+}
+
+function handler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ // Send an empty Content-Type, so we are guaranteed to sniff.
+ response.setHeader("Content-Type", "", false);
+ var body = getFileContents(do_get_file(tests[testRan].path));
+ var bos = new BinaryOutputStream(response.bodyOutputStream);
+ bos.writeByteArray(body, body.length);
+}
+
+function run_test() {
+ // We use a custom handler so we can change the header to force sniffing.
+ httpserver.registerPathHandler("/", handler);
+ httpserver.start(-1);
+ do_test_pending();
+ try {
+ runNext();
+ } catch (e) {
+ print("ERROR - " + e + "\n");
+ }
+}
diff --git a/toolkit/components/mediasniffer/test/unit/xpcshell.ini b/toolkit/components/mediasniffer/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..5ab4763f99
--- /dev/null
+++ b/toolkit/components/mediasniffer/test/unit/xpcshell.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ data/bug1079747.mp4
+ data/detodos.mp3
+ data/ff-inst.exe
+ data/file.mkv
+ data/file.webm
+ data/fl10.mp2
+ data/he_free.mp3
+ data/id3tags.mp3
+ data/notags-bad.mp3
+ data/notags-scan.mp3
+ data/notags.mp3
+
+[test_mediasniffer.js]
+[test_mediasniffer_ext.js]
diff --git a/toolkit/components/microformats/manifest.ini b/toolkit/components/microformats/manifest.ini
new file mode 100644
index 0000000000..24dbcb1ca7
--- /dev/null
+++ b/toolkit/components/microformats/manifest.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+run-if = buildapp == 'browser'
+
+[test/marionette/test_standards.py]
+[test/marionette/test_modules.py]
+[test/marionette/test_interface.py]
+
diff --git a/toolkit/components/microformats/microformat-shiv.js b/toolkit/components/microformats/microformat-shiv.js
new file mode 100644
index 0000000000..b81e10796c
--- /dev/null
+++ b/toolkit/components/microformats/microformat-shiv.js
@@ -0,0 +1,4523 @@
+/*
+ Modern
+ microformat-shiv - v1.4.0
+ Built: 2016-03-02 10:03 - http://microformat-shiv.com
+ Copyright (c) 2016 Glenn Jones
+ Licensed MIT
+*/
+
+
+var Microformats; // jshint ignore:line
+
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory();
+ } else {
+ root.Microformats = factory();
+ }
+}(this, function () {
+
+ var modules = {};
+
+
+ modules.version = '1.4.0';
+ modules.livingStandard = '2015-09-25T12:26:04Z';
+
+ /**
+ * constructor
+ *
+ */
+ modules.Parser = function () {
+ this.rootPrefix = 'h-';
+ this.propertyPrefixes = ['p-', 'dt-', 'u-', 'e-'];
+ this.excludeTags = ['br', 'hr'];
+ };
+
+
+ // create objects incase the v1 map modules don't load
+ modules.maps = (modules.maps)? modules.maps : {};
+ modules.rels = (modules.rels)? modules.rels : {};
+
+
+ modules.Parser.prototype = {
+
+ init: function() {
+ this.rootNode = null;
+ this.document = null;
+ this.options = {
+ 'baseUrl': '',
+ 'filters': [],
+ 'textFormat': 'whitespacetrimmed',
+ 'dateFormat': 'auto', // html5 for testing
+ 'overlappingVersions': false,
+ 'impliedPropertiesByVersion': true,
+ 'parseLatLonGeo': false
+ };
+ this.rootID = 0;
+ this.errors = [];
+ this.noContentErr = 'No options.node or options.html was provided and no document object could be found.';
+ },
+
+
+ /**
+ * internal parse function
+ *
+ * @param {Object} options
+ * @return {Object}
+ */
+ get: function(options) {
+ var out = this.formatEmpty(),
+ data = [],
+ rels;
+
+ this.init();
+ options = (options)? options : {};
+ this.mergeOptions(options);
+ this.getDOMContext( options );
+
+ // if we do not have any context create error
+ if (!this.rootNode || !this.document) {
+ this.errors.push(this.noContentErr);
+ } else {
+
+ // only parse h-* microformats if we need to
+ // this is added to speed up parsing
+ if (this.hasMicroformats(this.rootNode, options)) {
+ this.prepareDOM( options );
+
+ if (this.options.filters.length > 0) {
+ // parse flat list of items
+ var newRootNode = this.findFilterNodes(this.rootNode, this.options.filters);
+ data = this.walkRoot(newRootNode);
+ } else {
+ // parse whole document from root
+ data = this.walkRoot(this.rootNode);
+ }
+
+ out.items = data;
+ // don't clear-up DOM if it was cloned
+ if (modules.domUtils.canCloneDocument(this.document) === false) {
+ this.clearUpDom(this.rootNode);
+ }
+ }
+
+ // find any rels
+ if (this.findRels) {
+ rels = this.findRels(this.rootNode);
+ out.rels = rels.rels;
+ out['rel-urls'] = rels['rel-urls'];
+ }
+
+ }
+
+ if (this.errors.length > 0) {
+ return this.formatError();
+ }
+ return out;
+ },
+
+
+ /**
+ * parse to get parent microformat of passed node
+ *
+ * @param {DOM Node} node
+ * @param {Object} options
+ * @return {Object}
+ */
+ getParent: function(node, options) {
+ this.init();
+ options = (options)? options : {};
+
+ if (node) {
+ return this.getParentTreeWalk(node, options);
+ }
+ this.errors.push(this.noContentErr);
+ return this.formatError();
+ },
+
+
+ /**
+ * get the count of microformats
+ *
+ * @param {DOM Node} rootNode
+ * @return {Int}
+ */
+ count: function( options ) {
+ var out = {},
+ items,
+ classItems,
+ x,
+ i;
+
+ this.init();
+ options = (options)? options : {};
+ this.getDOMContext( options );
+
+ // if we do not have any context create error
+ if (!this.rootNode || !this.document) {
+ return {'errors': [this.noContentErr]};
+ }
+ items = this.findRootNodes( this.rootNode, true );
+ i = items.length;
+ while (i--) {
+ classItems = modules.domUtils.getAttributeList(items[i], 'class');
+ x = classItems.length;
+ while (x--) {
+ // find v2 names
+ if (modules.utils.startWith( classItems[x], 'h-' )) {
+ this.appendCount(classItems[x], 1, out);
+ }
+ // find v1 names
+ for (var key in modules.maps) {
+ // dont double count if v1 and v2 roots are present
+ if (modules.maps[key].root === classItems[x] && classItems.indexOf(key) === -1) {
+ this.appendCount(key, 1, out);
+ }
+ }
+ }
+ }
+ var relCount = this.countRels( this.rootNode );
+ if (relCount > 0) {
+ out.rels = relCount;
+ }
+
+ return out;
+ },
+
+
+ /**
+ * does a node have a class that marks it as a microformats root
+ *
+ * @param {DOM Node} node
+ * @param {Objecte} options
+ * @return {Boolean}
+ */
+ isMicroformat: function( node, options ) {
+ var classes,
+ i;
+
+ if (!node) {
+ return false;
+ }
+
+ // if documemt gets topmost node
+ node = modules.domUtils.getTopMostNode( node );
+
+ // look for h-* microformats
+ classes = this.getUfClassNames(node);
+ if (options && options.filters && modules.utils.isArray(options.filters)) {
+ i = options.filters.length;
+ while (i--) {
+ if (classes.root.indexOf(options.filters[i]) > -1) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return (classes.root.length > 0);
+ },
+
+
+ /**
+ * does a node or its children have microformats
+ *
+ * @param {DOM Node} node
+ * @param {Objecte} options
+ * @return {Boolean}
+ */
+ hasMicroformats: function( node, options ) {
+ var items,
+ i;
+
+ if (!node) {
+ return false;
+ }
+
+ // if browser based documemt get topmost node
+ node = modules.domUtils.getTopMostNode( node );
+
+ // returns all microformat roots
+ items = this.findRootNodes( node, true );
+ if (options && options.filters && modules.utils.isArray(options.filters)) {
+ i = items.length;
+ while (i--) {
+ if ( this.isMicroformat( items[i], options ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return (items.length > 0);
+ },
+
+
+ /**
+ * add a new v1 mapping object to parser
+ *
+ * @param {Array} maps
+ */
+ add: function( maps ) {
+ maps.forEach(function(map) {
+ if (map && map.root && map.name && map.properties) {
+ modules.maps[map.name] = JSON.parse(JSON.stringify(map));
+ }
+ });
+ },
+
+
+ /**
+ * internal parse to get parent microformats by walking up the tree
+ *
+ * @param {DOM Node} node
+ * @param {Object} options
+ * @param {Int} recursive
+ * @return {Object}
+ */
+ getParentTreeWalk: function (node, options, recursive) {
+ options = (options)? options : {};
+
+ // recursive calls
+ if (recursive === undefined) {
+ if (node.parentNode && node.nodeName !== 'HTML') {
+ return this.getParentTreeWalk(node.parentNode, options, true);
+ }
+ return this.formatEmpty();
+ }
+ if (node !== null && node !== undefined && node.parentNode) {
+ if (this.isMicroformat( node, options )) {
+ // if we have a match return microformat
+ options.node = node;
+ return this.get( options );
+ }
+ return this.getParentTreeWalk(node.parentNode, options, true);
+ }
+ return this.formatEmpty();
+ },
+
+
+
+ /**
+ * configures what are the base DOM objects for parsing
+ *
+ * @param {Object} options
+ */
+ getDOMContext: function( options ) {
+ var nodes = modules.domUtils.getDOMContext( options );
+ this.rootNode = nodes.rootNode;
+ this.document = nodes.document;
+ },
+
+
+ /**
+ * prepares DOM before the parse begins
+ *
+ * @param {Object} options
+ * @return {Boolean}
+ */
+ prepareDOM: function( options ) {
+ var baseTag,
+ href;
+
+ // use current document to define baseUrl, try/catch needed for IE10+ error
+ try {
+ if (!options.baseUrl && this.document && this.document.location) {
+ this.options.baseUrl = this.document.location.href;
+ }
+ } catch (e) {
+ // there is no alt action
+ }
+
+
+ // find base tag to set baseUrl
+ baseTag = modules.domUtils.querySelector(this.document, 'base');
+ if (baseTag) {
+ href = modules.domUtils.getAttribute(baseTag, 'href');
+ if (href) {
+ this.options.baseUrl = href;
+ }
+ }
+
+ // get path to rootNode
+ // then clone document
+ // then reset the rootNode to its cloned version in a new document
+ var path,
+ newDocument,
+ newRootNode;
+
+ path = modules.domUtils.getNodePath(this.rootNode);
+ newDocument = modules.domUtils.cloneDocument(this.document);
+ newRootNode = modules.domUtils.getNodeByPath(newDocument, path);
+
+ // check results as early IE fails
+ if (newDocument && newRootNode) {
+ this.document = newDocument;
+ this.rootNode = newRootNode;
+ }
+
+ // add includes
+ if (this.addIncludes) {
+ this.addIncludes( this.document );
+ }
+
+ return (this.rootNode && this.document);
+ },
+
+
+ /**
+ * returns an empty structure with errors
+ *
+ * @return {Object}
+ */
+ formatError: function() {
+ var out = this.formatEmpty();
+ out.errors = this.errors;
+ return out;
+ },
+
+
+ /**
+ * returns an empty structure
+ *
+ * @return {Object}
+ */
+ formatEmpty: function() {
+ return {
+ 'items': [],
+ 'rels': {},
+ 'rel-urls': {}
+ };
+ },
+
+
+ // find microformats of a given type and return node structures
+ findFilterNodes: function(rootNode, filters) {
+ if (modules.utils.isString(filters)) {
+ filters = [filters];
+ }
+ var newRootNode = modules.domUtils.createNode('div'),
+ items = this.findRootNodes(rootNode, true),
+ i = 0,
+ x = 0,
+ y = 0;
+
+ // add v1 names
+ y = filters.length;
+ while (y--) {
+ if (this.getMapping(filters[y])) {
+ var v1Name = this.getMapping(filters[y]).root;
+ filters.push(v1Name);
+ }
+ }
+
+ if (items) {
+ i = items.length;
+ while (x < i) {
+ // append matching nodes into newRootNode
+ y = filters.length;
+ while (y--) {
+ if (modules.domUtils.hasAttributeValue(items[x], 'class', filters[y])) {
+ var clone = modules.domUtils.clone(items[x]);
+ modules.domUtils.appendChild(newRootNode, clone);
+ break;
+ }
+ }
+ x++;
+ }
+ }
+
+ return newRootNode;
+ },
+
+
+ /**
+ * appends data to output object for count
+ *
+ * @param {string} name
+ * @param {Int} count
+ * @param {Object}
+ */
+ appendCount: function(name, count, out) {
+ if (out[name]) {
+ out[name] = out[name] + count;
+ } else {
+ out[name] = count;
+ }
+ },
+
+
+ /**
+ * is the microformats type in the filter list
+ *
+ * @param {Object} uf
+ * @param {Array} filters
+ * @return {Boolean}
+ */
+ shouldInclude: function(uf, filters) {
+ var i;
+
+ if (modules.utils.isArray(filters) && filters.length > 0) {
+ i = filters.length;
+ while (i--) {
+ if (uf.type[0] === filters[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ },
+
+
+ /**
+ * finds all microformat roots in a rootNode
+ *
+ * @param {DOM Node} rootNode
+ * @param {Boolean} includeRoot
+ * @return {Array}
+ */
+ findRootNodes: function(rootNode, includeRoot) {
+ var arr = null,
+ out = [],
+ classList = [],
+ items,
+ x,
+ i,
+ y,
+ key;
+
+
+ // build an array of v1 root names
+ for (key in modules.maps) {
+ if (modules.maps.hasOwnProperty(key)) {
+ classList.push(modules.maps[key].root);
+ }
+ }
+
+ // get all elements that have a class attribute
+ includeRoot = (includeRoot) ? includeRoot : false;
+ if (includeRoot && rootNode.parentNode) {
+ arr = modules.domUtils.getNodesByAttribute(rootNode.parentNode, 'class');
+ } else {
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'class');
+ }
+
+ // loop elements that have a class attribute
+ x = 0;
+ i = arr.length;
+ while (x < i) {
+
+ items = modules.domUtils.getAttributeList(arr[x], 'class');
+
+ // loop classes on an element
+ y = items.length;
+ while (y--) {
+ // match v1 root names
+ if (classList.indexOf(items[y]) > -1) {
+ out.push(arr[x]);
+ break;
+ }
+
+ // match v2 root name prefix
+ if (modules.utils.startWith(items[y], 'h-')) {
+ out.push(arr[x]);
+ break;
+ }
+ }
+
+ x++;
+ }
+ return out;
+ },
+
+
+ /**
+ * starts the tree walk to find microformats
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ walkRoot: function(node) {
+ var context = this,
+ children = [],
+ child,
+ classes,
+ items = [],
+ out = [];
+
+ classes = this.getUfClassNames(node);
+ // if it is a root microformat node
+ if (classes && classes.root.length > 0) {
+ items = this.walkTree(node);
+
+ if (items.length > 0) {
+ out = out.concat(items);
+ }
+ } else {
+ // check if there are children and one of the children has a root microformat
+ children = modules.domUtils.getChildren( node );
+ if (children && children.length > 0 && this.findRootNodes(node, true).length > -1) {
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ items = context.walkRoot(child);
+ if (items.length > 0) {
+ out = out.concat(items);
+ }
+ }
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * starts the tree walking for a single microformat
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ walkTree: function(node) {
+ var classes,
+ out = [],
+ obj,
+ itemRootID;
+
+ // loop roots found on one element
+ classes = this.getUfClassNames(node);
+ if (classes && classes.root.length && classes.root.length > 0) {
+
+ this.rootID++;
+ itemRootID = this.rootID;
+ obj = this.createUfObject(classes.root, classes.typeVersion);
+
+ this.walkChildren(node, obj, classes.root, itemRootID, classes);
+ if (this.impliedRules) {
+ this.impliedRules(node, obj, classes);
+ }
+ out.push( this.cleanUfObject(obj) );
+
+
+ }
+ return out;
+ },
+
+
+ /**
+ * finds child properties of microformat
+ *
+ * @param {DOM Node} node
+ * @param {Object} out
+ * @param {String} ufName
+ * @param {Int} rootID
+ * @param {Object} parentClasses
+ */
+ walkChildren: function(node, out, ufName, rootID, parentClasses) {
+ var context = this,
+ children = [],
+ rootItem,
+ itemRootID,
+ value,
+ propertyName,
+ propertyVersion,
+ i,
+ x,
+ y,
+ z,
+ child;
+
+ children = modules.domUtils.getChildren( node );
+
+ y = 0;
+ z = children.length;
+ while (y < z) {
+ child = children[y];
+
+ // get microformat classes for this single element
+ var classes = context.getUfClassNames(child, ufName);
+
+ // a property which is a microformat
+ if (classes.root.length > 0 && classes.properties.length > 0 && !child.addedAsRoot) {
+ // create object with type, property and value
+ rootItem = context.createUfObject(
+ classes.root,
+ classes.typeVersion,
+ modules.text.parse(this.document, child, context.options.textFormat)
+ );
+
+ // add the microformat as an array of properties
+ propertyName = context.removePropPrefix(classes.properties[0][0]);
+
+ // modifies value with "implied value rule"
+ if (parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1) {
+ if (context.impliedValueRule) {
+ out = context.impliedValueRule(out, parentClasses.properties[0][0], classes.properties[0][0], value);
+ }
+ }
+
+ if (out.properties[propertyName]) {
+ out.properties[propertyName].push(rootItem);
+ } else {
+ out.properties[propertyName] = [rootItem];
+ }
+
+ context.rootID++;
+ // used to stop duplication in heavily nested structures
+ child.addedAsRoot = true;
+
+
+ x = 0;
+ i = rootItem.type.length;
+ itemRootID = context.rootID;
+ while (x < i) {
+ context.walkChildren(child, rootItem, rootItem.type, itemRootID, classes);
+ x++;
+ }
+ if (this.impliedRules) {
+ context.impliedRules(child, rootItem, classes);
+ }
+ this.cleanUfObject(rootItem);
+
+ }
+
+ // a property which is NOT a microformat and has not been used for a given root element
+ if (classes.root.length === 0 && classes.properties.length > 0) {
+
+ x = 0;
+ i = classes.properties.length;
+ while (x < i) {
+
+ value = context.getValue(child, classes.properties[x][0], out);
+ propertyName = context.removePropPrefix(classes.properties[x][0]);
+ propertyVersion = classes.properties[x][1];
+
+ // modifies value with "implied value rule"
+ if (parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1) {
+ if (context.impliedValueRule) {
+ out = context.impliedValueRule(out, parentClasses.properties[0][0], classes.properties[x][0], value);
+ }
+ }
+
+ // if we have not added this value into a property with the same name already
+ if (!context.hasRootID(child, rootID, propertyName)) {
+ // check the root and property is the same version or if overlapping versions are allowed
+ if ( context.isAllowedPropertyVersion( out.typeVersion, propertyVersion ) ) {
+ // add the property as an array of properties
+ if (out.properties[propertyName]) {
+ out.properties[propertyName].push(value);
+ } else {
+ out.properties[propertyName] = [value];
+ }
+ // add rootid to node so we can track its use
+ context.appendRootID(child, rootID, propertyName);
+ }
+ }
+
+ x++;
+ }
+
+ context.walkChildren(child, out, ufName, rootID, classes);
+ }
+
+ // if the node has no microformat classes, see if its children have
+ if (classes.root.length === 0 && classes.properties.length === 0) {
+ context.walkChildren(child, out, ufName, rootID, classes);
+ }
+
+ // if the node is a child root add it to the children tree
+ if (classes.root.length > 0 && classes.properties.length === 0) {
+
+ // create object with type, property and value
+ rootItem = context.createUfObject(
+ classes.root,
+ classes.typeVersion,
+ modules.text.parse(this.document, child, context.options.textFormat)
+ );
+
+ // add the microformat as an array of properties
+ if (!out.children) {
+ out.children = [];
+ }
+
+ if (!context.hasRootID(child, rootID, 'child-root')) {
+ out.children.push( rootItem );
+ context.appendRootID(child, rootID, 'child-root');
+ context.rootID++;
+ }
+
+ x = 0;
+ i = rootItem.type.length;
+ itemRootID = context.rootID;
+ while (x < i) {
+ context.walkChildren(child, rootItem, rootItem.type, itemRootID, classes);
+ x++;
+ }
+ if (this.impliedRules) {
+ context.impliedRules(child, rootItem, classes);
+ }
+ context.cleanUfObject( rootItem );
+
+ }
+
+
+
+ y++;
+ }
+
+ },
+
+
+
+
+ /**
+ * gets the value of a property from a node
+ *
+ * @param {DOM Node} node
+ * @param {String} className
+ * @param {Object} uf
+ * @return {String || Object}
+ */
+ getValue: function(node, className, uf) {
+ var value = '';
+
+ if (modules.utils.startWith(className, 'p-')) {
+ value = this.getPValue(node, true);
+ }
+
+ if (modules.utils.startWith(className, 'e-')) {
+ value = this.getEValue(node);
+ }
+
+ if (modules.utils.startWith(className, 'u-')) {
+ value = this.getUValue(node, true);
+ }
+
+ if (modules.utils.startWith(className, 'dt-')) {
+ value = this.getDTValue(node, className, uf, true);
+ }
+ return value;
+ },
+
+
+ /**
+ * gets the value of a node which contains a 'p-' property
+ *
+ * @param {DOM Node} node
+ * @param {Boolean} valueParse
+ * @return {String}
+ */
+ getPValue: function(node, valueParse) {
+ var out = '';
+ if (valueParse) {
+ out = this.getValueClass(node, 'p');
+ }
+
+ if (!out && valueParse) {
+ out = this.getValueTitle(node);
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['data', 'input'], 'value');
+ }
+
+ if (node.name === 'br' || node.name === 'hr') {
+ out = '';
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['img', 'area'], 'alt');
+ }
+
+ if (!out) {
+ out = modules.text.parse(this.document, node, this.options.textFormat);
+ }
+
+ return (out) ? out : '';
+ },
+
+
+ /**
+ * gets the value of a node which contains the 'e-' property
+ *
+ * @param {DOM Node} node
+ * @return {Object}
+ */
+ getEValue: function(node) {
+
+ var out = {value: '', html: ''};
+
+ this.expandURLs(node, 'src', this.options.baseUrl);
+ this.expandURLs(node, 'href', this.options.baseUrl);
+
+ out.value = modules.text.parse(this.document, node, this.options.textFormat);
+ out.html = modules.html.parse(node);
+
+ return out;
+ },
+
+
+ /**
+ * gets the value of a node which contains the 'u-' property
+ *
+ * @param {DOM Node} node
+ * @param {Boolean} valueParse
+ * @return {String}
+ */
+ getUValue: function(node, valueParse) {
+ var out = '';
+ if (valueParse) {
+ out = this.getValueClass(node, 'u');
+ }
+
+ if (!out && valueParse) {
+ out = this.getValueTitle(node);
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['a', 'area'], 'href');
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['img', 'audio', 'video', 'source'], 'src');
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['object'], 'data');
+ }
+
+ // if we have no protocol separator, turn relative url to absolute url
+ if (out && out !== '' && out.indexOf('://') === -1) {
+ out = modules.url.resolve(out, this.options.baseUrl);
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['data', 'input'], 'value');
+ }
+
+ if (!out) {
+ out = modules.text.parse(this.document, node, this.options.textFormat);
+ }
+
+ return (out) ? out : '';
+ },
+
+
+ /**
+ * gets the value of a node which contains the 'dt-' property
+ *
+ * @param {DOM Node} node
+ * @param {String} className
+ * @param {Object} uf
+ * @param {Boolean} valueParse
+ * @return {String}
+ */
+ getDTValue: function(node, className, uf, valueParse) {
+ var out = '';
+
+ if (valueParse) {
+ out = this.getValueClass(node, 'dt');
+ }
+
+ if (!out && valueParse) {
+ out = this.getValueTitle(node);
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['time', 'ins', 'del'], 'datetime');
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+
+ if (!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['data', 'input'], 'value');
+ }
+
+ if (!out) {
+ out = modules.text.parse(this.document, node, this.options.textFormat);
+ }
+
+ if (out) {
+ if (modules.dates.isDuration(out)) {
+ // just duration
+ return out;
+ } else if (modules.dates.isTime(out)) {
+ // just time or time+timezone
+ if (uf) {
+ uf.times.push([className, modules.dates.parseAmPmTime(out, this.options.dateFormat)]);
+ }
+ return modules.dates.parseAmPmTime(out, this.options.dateFormat);
+ }
+ // returns a date - microformat profile
+ if (uf) {
+ uf.dates.push([className, new modules.ISODate(out).toString( this.options.dateFormat )]);
+ }
+ return new modules.ISODate(out).toString( this.options.dateFormat );
+ }
+ return '';
+ },
+
+
+ /**
+ * appends a new rootid to a given node
+ *
+ * @param {DOM Node} node
+ * @param {String} id
+ * @param {String} propertyName
+ */
+ appendRootID: function(node, id, propertyName) {
+ if (this.hasRootID(node, id, propertyName) === false) {
+ var rootids = [];
+ if (modules.domUtils.hasAttribute(node, 'rootids')) {
+ rootids = modules.domUtils.getAttributeList(node, 'rootids');
+ }
+ rootids.push('id' + id + '-' + propertyName);
+ modules.domUtils.setAttribute(node, 'rootids', rootids.join(' '));
+ }
+ },
+
+
+ /**
+ * does a given node already have a rootid
+ *
+ * @param {DOM Node} node
+ * @param {String} id
+ * @param {String} propertyName
+ * @return {Boolean}
+ */
+ hasRootID: function(node, id, propertyName) {
+ var rootids = [];
+ if (!modules.domUtils.hasAttribute(node, 'rootids')) {
+ return false;
+ }
+ rootids = modules.domUtils.getAttributeList(node, 'rootids');
+ return (rootids.indexOf('id' + id + '-' + propertyName) > -1);
+ },
+
+
+
+ /**
+ * gets the text of any child nodes with a class value
+ *
+ * @param {DOM Node} node
+ * @param {String} propertyName
+ * @return {String || null}
+ */
+ getValueClass: function(node, propertyType) {
+ var context = this,
+ children = [],
+ out = [],
+ child,
+ x,
+ i;
+
+ children = modules.domUtils.getChildren( node );
+
+ x = 0;
+ i = children.length;
+ while (x < i) {
+ child = children[x];
+ var value = null;
+ if (modules.domUtils.hasAttributeValue(child, 'class', 'value')) {
+ switch (propertyType) {
+ case 'p':
+ value = context.getPValue(child, false);
+ break;
+ case 'u':
+ value = context.getUValue(child, false);
+ break;
+ case 'dt':
+ value = context.getDTValue(child, '', null, false);
+ break;
+ }
+ if (value) {
+ out.push(modules.utils.trim(value));
+ }
+ }
+ x++;
+ }
+ if (out.length > 0) {
+ if (propertyType === 'p') {
+ return modules.text.parseText( this.document, out.join(' '), this.options.textFormat);
+ }
+ if (propertyType === 'u') {
+ return out.join('');
+ }
+ if (propertyType === 'dt') {
+ return modules.dates.concatFragments(out, this.options.dateFormat).toString(this.options.dateFormat);
+ }
+ return undefined;
+ }
+ return null;
+ },
+
+
+ /**
+ * returns a single string of the 'title' attr from all
+ * the child nodes with the class 'value-title'
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ getValueTitle: function(node) {
+ var out = [],
+ items,
+ i,
+ x;
+
+ items = modules.domUtils.getNodesByAttributeValue(node, 'class', 'value-title');
+ x = 0;
+ i = items.length;
+ while (x < i) {
+ if (modules.domUtils.hasAttribute(items[x], 'title')) {
+ out.push(modules.domUtils.getAttribute(items[x], 'title'));
+ }
+ x++;
+ }
+ return out.join('');
+ },
+
+
+ /**
+ * finds out whether a node has h-* class v1 and v2
+ *
+ * @param {DOM Node} node
+ * @return {Boolean}
+ */
+ hasHClass: function(node) {
+ var classes = this.getUfClassNames(node);
+ if (classes.root && classes.root.length > 0) {
+ return true;
+ }
+ return false;
+ },
+
+
+ /**
+ * get both the root and property class names from a node
+ *
+ * @param {DOM Node} node
+ * @param {Array} ufNameArr
+ * @return {Object}
+ */
+ getUfClassNames: function(node, ufNameArr) {
+ var context = this,
+ out = {
+ 'root': [],
+ 'properties': []
+ },
+ classNames,
+ key,
+ items,
+ item,
+ i,
+ x,
+ z,
+ y,
+ map,
+ prop,
+ propName,
+ v2Name,
+ impiedRel,
+ ufName;
+
+ // don't get classes from excluded list of tags
+ if (modules.domUtils.hasTagName(node, this.excludeTags) === false) {
+
+ // find classes for node
+ classNames = modules.domUtils.getAttribute(node, 'class');
+ if (classNames) {
+ items = classNames.split(' ');
+ x = 0;
+ i = items.length;
+ while (x < i) {
+
+ item = modules.utils.trim(items[x]);
+
+ // test for root prefix - v2
+ if (modules.utils.startWith(item, context.rootPrefix)) {
+ if (out.root.indexOf(item) === -1) {
+ out.root.push(item);
+ }
+ out.typeVersion = 'v2';
+ }
+
+ // test for property prefix - v2
+ z = context.propertyPrefixes.length;
+ while (z--) {
+ if (modules.utils.startWith(item, context.propertyPrefixes[z])) {
+ out.properties.push([item, 'v2']);
+ }
+ }
+
+ // test for mapped root classnames v1
+ for (key in modules.maps) {
+ if (modules.maps.hasOwnProperty(key)) {
+ // only add a root once
+ if (modules.maps[key].root === item && out.root.indexOf(key) === -1) {
+ // if root map has subTree set to true
+ // test to see if we should create a property or root
+ if (modules.maps[key].subTree) {
+ out.properties.push(['p-' + modules.maps[key].root, 'v1']);
+ } else {
+ out.root.push(key);
+ if (!out.typeVersion) {
+ out.typeVersion = 'v1';
+ }
+ }
+ }
+ }
+ }
+
+
+ // test for mapped property classnames v1
+ if (ufNameArr) {
+ for (var a = 0; a < ufNameArr.length; a++) {
+ ufName = ufNameArr[a];
+ // get mapped property v1 microformat
+ map = context.getMapping(ufName);
+ if (map) {
+ for (key in map.properties) {
+ if (map.properties.hasOwnProperty(key)) {
+
+ prop = map.properties[key];
+ propName = (prop.map) ? prop.map : 'p-' + key;
+
+ if (key === item) {
+ if (prop.uf) {
+ // loop all the classList make sure
+ // 1. this property is a root
+ // 2. that there is not already an equivalent v2 property i.e. url and u-url on the same element
+ y = 0;
+ while (y < i) {
+ v2Name = context.getV2RootName(items[y]);
+ // add new root
+ if (prop.uf.indexOf(v2Name) > -1 && out.root.indexOf(v2Name) === -1) {
+ out.root.push(v2Name);
+ out.typeVersion = 'v1';
+ }
+ y++;
+ }
+ // only add property once
+ if (out.properties.indexOf(propName) === -1) {
+ out.properties.push([propName, 'v1']);
+ }
+ } else if (out.properties.indexOf(propName) === -1) {
+ out.properties.push([propName, 'v1']);
+ }
+ }
+ }
+
+ }
+ }
+ }
+
+ }
+
+ x++;
+
+ }
+ }
+ }
+
+
+ // finds any alt rel=* mappings for a given node/microformat
+ if (ufNameArr && this.findRelImpied) {
+ for (var b = 0; b < ufNameArr.length; b++) {
+ ufName = ufNameArr[b];
+ impiedRel = this.findRelImpied(node, ufName);
+ if (impiedRel && out.properties.indexOf(impiedRel) === -1) {
+ out.properties.push([impiedRel, 'v1']);
+ }
+ }
+ }
+
+
+ // if(out.root.length === 1 && out.properties.length === 1) {
+ // if(out.root[0].replace('h-','') === this.removePropPrefix(out.properties[0][0])) {
+ // out.typeVersion = 'v2';
+ // }
+ // }
+
+ return out;
+ },
+
+
+ /**
+ * given a v1 or v2 root name, return mapping object
+ *
+ * @param {String} name
+ * @return {Object || null}
+ */
+ getMapping: function(name) {
+ var key;
+ for (key in modules.maps) {
+ if (modules.maps[key].root === name || key === name) {
+ return modules.maps[key];
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * given a v1 root name returns a v2 root name i.e. vcard >>> h-card
+ *
+ * @param {String} name
+ * @return {String || null}
+ */
+ getV2RootName: function(name) {
+ var key;
+ for (key in modules.maps) {
+ if (modules.maps[key].root === name) {
+ return key;
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * whether a property is the right microformats version for its root type
+ *
+ * @param {String} typeVersion
+ * @param {String} propertyVersion
+ * @return {Boolean}
+ */
+ isAllowedPropertyVersion: function(typeVersion, propertyVersion) {
+ if (this.options.overlappingVersions === true) {
+ return true;
+ }
+ return (typeVersion === propertyVersion);
+ },
+
+
+ /**
+ * creates a blank microformats object
+ *
+ * @param {String} name
+ * @param {String} value
+ * @return {Object}
+ */
+ createUfObject: function(names, typeVersion, value) {
+ var out = {};
+
+ // is more than just whitespace
+ if (value && modules.utils.isOnlyWhiteSpace(value) === false) {
+ out.value = value;
+ }
+ // add type i.e. ["h-card", "h-org"]
+ if (modules.utils.isArray(names)) {
+ out.type = names;
+ } else {
+ out.type = [names];
+ }
+ out.properties = {};
+ // metadata properties for parsing
+ out.typeVersion = typeVersion;
+ out.times = [];
+ out.dates = [];
+ out.altValue = null;
+
+ return out;
+ },
+
+
+ /**
+ * removes unwanted microformats property before output
+ *
+ * @param {Object} microformat
+ */
+ cleanUfObject: function( microformat ) {
+ delete microformat.times;
+ delete microformat.dates;
+ delete microformat.typeVersion;
+ delete microformat.altValue;
+ return microformat;
+ },
+
+
+
+ /**
+ * removes microformat property prefixes from text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ removePropPrefix: function(text) {
+ var i;
+
+ i = this.propertyPrefixes.length;
+ while (i--) {
+ var prefix = this.propertyPrefixes[i];
+ if (modules.utils.startWith(text, prefix)) {
+ text = text.substr(prefix.length);
+ }
+ }
+ return text;
+ },
+
+
+ /**
+ * expands all relative URLs to absolute ones where it can
+ *
+ * @param {DOM Node} node
+ * @param {String} attrName
+ * @param {String} baseUrl
+ */
+ expandURLs: function(node, attrName, baseUrl) {
+ var i,
+ nodes,
+ attr;
+
+ nodes = modules.domUtils.getNodesByAttribute(node, attrName);
+ i = nodes.length;
+ while (i--) {
+ try {
+ // the url parser can blow up if the format is not right
+ attr = modules.domUtils.getAttribute(nodes[i], attrName);
+ if (attr && attr !== '' && baseUrl !== '' && attr.indexOf('://') === -1) {
+ // attr = urlParser.resolve(baseUrl, attr);
+ attr = modules.url.resolve(attr, baseUrl);
+ modules.domUtils.setAttribute(nodes[i], attrName, attr);
+ }
+ } catch (err) {
+ // do nothing - convert only the urls we can, leave the rest as they are
+ }
+ }
+ },
+
+
+
+ /**
+ * merges passed and default options -single level clone of properties
+ *
+ * @param {Object} options
+ */
+ mergeOptions: function(options) {
+ var key;
+ for (key in options) {
+ if (options.hasOwnProperty(key)) {
+ this.options[key] = options[key];
+ }
+ }
+ },
+
+
+ /**
+ * removes all rootid attributes
+ *
+ * @param {DOM Node} rootNode
+ */
+ removeRootIds: function(rootNode) {
+ var arr,
+ i;
+
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'rootids');
+ i = arr.length;
+ while (i--) {
+ modules.domUtils.removeAttribute(arr[i], 'rootids');
+ }
+ },
+
+
+ /**
+ * removes all changes made to the DOM
+ *
+ * @param {DOM Node} rootNode
+ */
+ clearUpDom: function(rootNode) {
+ if (this.removeIncludes) {
+ this.removeIncludes(rootNode);
+ }
+ this.removeRootIds(rootNode);
+ }
+
+
+ };
+
+
+ modules.Parser.prototype.constructor = modules.Parser;
+
+
+ // check parser module is loaded
+ if (modules.Parser) {
+
+ /**
+ * applies "implied rules" microformat output structure i.e. feed-title, name, photo, url and date
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf (microformat output structure)
+ * @param {Object} parentClasses (classes structure)
+ * @param {Boolean} impliedPropertiesByVersion
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedRules = function(node, uf, parentClasses) {
+ var typeVersion = (uf.typeVersion)? uf.typeVersion: 'v2';
+
+ // TEMP: override to allow v1 implied properties while spec changes
+ if (this.options.impliedPropertiesByVersion === false) {
+ typeVersion = 'v2';
+ }
+
+ if (node && uf && uf.properties) {
+ uf = this.impliedBackwardComp( node, uf, parentClasses );
+ if (typeVersion === 'v2') {
+ uf = this.impliedhFeedTitle( uf );
+ uf = this.impliedName( node, uf );
+ uf = this.impliedPhoto( node, uf );
+ uf = this.impliedUrl( node, uf );
+ }
+ uf = this.impliedValue( node, uf, parentClasses );
+ uf = this.impliedDate( uf );
+
+ // TEMP: flagged while spec changes are put forward
+ if (this.options.parseLatLonGeo === true) {
+ uf = this.impliedGeo( uf );
+ }
+ }
+
+ return uf;
+ };
+
+
+ /**
+ * apply implied name rule
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedName = function(node, uf) {
+ // implied name rule
+ /*
+ img.h-x[alt] <img class="h-card" src="glenn.htm" alt="Glenn Jones"></a>
+ area.h-x[alt] <area class="h-card" href="glenn.htm" alt="Glenn Jones"></area>
+ abbr.h-x[title] <abbr class="h-card" title="Glenn Jones"GJ</abbr>
+
+ .h-x>img:only-child[alt]:not[.h-*] <div class="h-card"><a src="glenn.htm" alt="Glenn Jones"></a></div>
+ .h-x>area:only-child[alt]:not[.h-*] <div class="h-card"><area href="glenn.htm" alt="Glenn Jones"></area></div>
+ .h-x>abbr:only-child[title] <div class="h-card"><abbr title="Glenn Jones">GJ</abbr></div>
+
+ .h-x>:only-child>img:only-child[alt]:not[.h-*] <div class="h-card"><span><img src="jane.html" alt="Jane Doe"/></span></div>
+ .h-x>:only-child>area:only-child[alt]:not[.h-*] <div class="h-card"><span><area href="jane.html" alt="Jane Doe"></area></span></div>
+ .h-x>:only-child>abbr:only-child[title] <div class="h-card"><span><abbr title="Jane Doe">JD</abbr></span></div>
+ */
+ var name,
+ value;
+
+ if (!uf.properties.name) {
+ value = this.getImpliedProperty(node, ['img', 'area', 'abbr'], this.getNameAttr);
+ var textFormat = this.options.textFormat;
+ // if no value for tags/properties use text
+ if (!value) {
+ name = [modules.text.parse(this.document, node, textFormat)];
+ } else {
+ name = [modules.text.parseText(this.document, value, textFormat)];
+ }
+ if (name && name[0] !== '') {
+ uf.properties.name = name;
+ }
+ }
+
+ return uf;
+ };
+
+
+ /**
+ * apply implied photo rule
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedPhoto = function(node, uf) {
+ // implied photo rule
+ /*
+ img.h-x[src] <img class="h-card" alt="Jane Doe" src="jane.jpeg"/>
+ object.h-x[data] <object class="h-card" data="jane.jpeg"/>Jane Doe</object>
+ .h-x>img[src]:only-of-type:not[.h-*] <div class="h-card"><img alt="Jane Doe" src="jane.jpeg"/></div>
+ .h-x>object[data]:only-of-type:not[.h-*] <div class="h-card"><object data="jane.jpeg"/>Jane Doe</object></div>
+ .h-x>:only-child>img[src]:only-of-type:not[.h-*] <div class="h-card"><span><img alt="Jane Doe" src="jane.jpeg"/></span></div>
+ .h-x>:only-child>object[data]:only-of-type:not[.h-*] <div class="h-card"><span><object data="jane.jpeg"/>Jane Doe</object></span></div>
+ */
+ var value;
+ if (!uf.properties.photo) {
+ value = this.getImpliedProperty(node, ['img', 'object'], this.getPhotoAttr);
+ if (value) {
+ // relative to absolute URL
+ if (value && value !== '' && this.options.baseUrl !== '' && value.indexOf('://') === -1) {
+ value = modules.url.resolve(value, this.options.baseUrl);
+ }
+ uf.properties.photo = [modules.utils.trim(value)];
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * apply implied URL rule
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedUrl = function(node, uf) {
+ // implied URL rule
+ /*
+ a.h-x[href] <a class="h-card" href="glenn.html">Glenn</a>
+ area.h-x[href] <area class="h-card" href="glenn.html">Glenn</area>
+ .h-x>a[href]:only-of-type:not[.h-*] <div class="h-card" ><a href="glenn.html">Glenn</a><p>...</p></div>
+ .h-x>area[href]:only-of-type:not[.h-*] <div class="h-card" ><area href="glenn.html">Glenn</area><p>...</p></div>
+ */
+ var value;
+ if (!uf.properties.url) {
+ value = this.getImpliedProperty(node, ['a', 'area'], this.getURLAttr);
+ if (value) {
+ // relative to absolute URL
+ if (value && value !== '' && this.options.baseUrl !== '' && value.indexOf('://') === -1) {
+ value = modules.url.resolve(value, this.options.baseUrl);
+ }
+ uf.properties.url = [modules.utils.trim(value)];
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * apply implied date rule - if there is a time only property try to concat it with any date property
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedDate = function(uf) {
+ // implied date rule
+ // http://microformats.org/wiki/value-class-pattern#microformats2_parsers
+ // http://microformats.org/wiki/microformats2-parsing-issues#implied_date_for_dt_properties_both_mf2_and_backcompat
+ var newDate;
+ if (uf.times.length > 0 && uf.dates.length > 0) {
+ newDate = modules.dates.dateTimeUnion(uf.dates[0][1], uf.times[0][1], this.options.dateFormat);
+ uf.properties[this.removePropPrefix(uf.times[0][0])][0] = newDate.toString(this.options.dateFormat);
+ }
+ // clean-up object
+ delete uf.times;
+ delete uf.dates;
+ return uf;
+ };
+
+
+ /**
+ * get an implied property value from pre-defined tag/attriubte combinations
+ *
+ * @param {DOM Node} node
+ * @param {String} tagList (Array of tags from which an implied value can be pulled)
+ * @param {String} getAttrFunction (Function which can extract implied value)
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getImpliedProperty = function(node, tagList, getAttrFunction) {
+ // i.e. img.h-card
+ var value = getAttrFunction(node),
+ descendant,
+ child;
+
+ if (!value) {
+ // i.e. .h-card>img:only-of-type:not(.h-card)
+ descendant = modules.domUtils.getSingleDescendantOfType( node, tagList);
+ if (descendant && this.hasHClass(descendant) === false) {
+ value = getAttrFunction(descendant);
+ }
+ if (node.children.length > 0 ) {
+ // i.e. .h-card>:only-child>img:only-of-type:not(.h-card)
+ child = modules.domUtils.getSingleDescendant(node);
+ if (child && this.hasHClass(child) === false) {
+ descendant = modules.domUtils.getSingleDescendantOfType(child, tagList);
+ if (descendant && this.hasHClass(descendant) === false) {
+ value = getAttrFunction(descendant);
+ }
+ }
+ }
+ }
+
+ return value;
+ };
+
+
+ /**
+ * get an implied name value from a node
+ *
+ * @param {DOM Node} node
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getNameAttr = function(node) {
+ var value = modules.domUtils.getAttrValFromTagList(node, ['img', 'area'], 'alt');
+ if (!value) {
+ value = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+ return value;
+ };
+
+
+ /**
+ * get an implied photo value from a node
+ *
+ * @param {DOM Node} node
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getPhotoAttr = function(node) {
+ var value = modules.domUtils.getAttrValFromTagList(node, ['img'], 'src');
+ if (!value && modules.domUtils.hasAttributeValue(node, 'class', 'include') === false) {
+ value = modules.domUtils.getAttrValFromTagList(node, ['object'], 'data');
+ }
+ return value;
+ };
+
+
+ /**
+ * get an implied photo value from a node
+ *
+ * @param {DOM Node} node
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getURLAttr = function(node) {
+ var value = null;
+ if (modules.domUtils.hasAttributeValue(node, 'class', 'include') === false) {
+
+ value = modules.domUtils.getAttrValFromTagList(node, ['a'], 'href');
+ if (!value) {
+ value = modules.domUtils.getAttrValFromTagList(node, ['area'], 'href');
+ }
+
+ }
+ return value;
+ };
+
+
+ /**
+ *
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedValue = function(node, uf, parentClasses) {
+
+ // intersection of implied name and implied value rules
+ if (uf.properties.name) {
+ if (uf.value && parentClasses.root.length > 0 && parentClasses.properties.length === 1) {
+ uf = this.getAltValue(uf, parentClasses.properties[0][0], 'p-name', uf.properties.name[0]);
+ }
+ }
+
+ // intersection of implied URL and implied value rules
+ if (uf.properties.url) {
+ if (parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1) {
+ uf = this.getAltValue(uf, parentClasses.properties[0][0], 'u-url', uf.properties.url[0]);
+ }
+ }
+
+ // apply alt value
+ if (uf.altValue !== null) {
+ uf.value = uf.altValue.value;
+ }
+ delete uf.altValue;
+
+
+ return uf;
+ };
+
+
+ /**
+ * get alt value based on rules about parent property prefix
+ *
+ * @param {Object} uf
+ * @param {String} parentPropertyName
+ * @param {String} propertyName
+ * @param {String} value
+ * @return {Object}
+ */
+ modules.Parser.prototype.getAltValue = function(uf, parentPropertyName, propertyName, value) {
+ if (uf.value && !uf.altValue) {
+ // first p-name of the h-* child
+ if (modules.utils.startWith(parentPropertyName, 'p-') && propertyName === 'p-name') {
+ uf.altValue = {name: propertyName, value: value};
+ }
+ // if it's an e-* property element
+ if (modules.utils.startWith(parentPropertyName, 'e-') && modules.utils.startWith(propertyName, 'e-')) {
+ uf.altValue = {name: propertyName, value: value};
+ }
+ // if it's an u-* property element
+ if (modules.utils.startWith(parentPropertyName, 'u-') && propertyName === 'u-url') {
+ uf.altValue = {name: propertyName, value: value};
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * if a h-feed does not have a title use the title tag of a page
+ *
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedhFeedTitle = function( uf ) {
+ if (uf.type && uf.type.indexOf('h-feed') > -1) {
+ // has no name property
+ if (uf.properties.name === undefined || uf.properties.name[0] === '' ) {
+ // use the text from the title tag
+ var title = modules.domUtils.querySelector(this.document, 'title');
+ if (title) {
+ uf.properties.name = [modules.domUtils.textContent(title)];
+ }
+ }
+ }
+ return uf;
+ };
+
+
+
+ /**
+ * implied Geo from pattern <abbr class="p-geo" title="37.386013;-122.082932">
+ *
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedGeo = function( uf ) {
+ var geoPair,
+ parts,
+ longitude,
+ latitude,
+ valid = true;
+
+ if (uf.type && uf.type.indexOf('h-geo') > -1) {
+
+ // has no latitude or longitude property
+ if (uf.properties.latitude === undefined || uf.properties.longitude === undefined ) {
+
+ geoPair = (uf.properties.name)? uf.properties.name[0] : null;
+ geoPair = (!geoPair && uf.properties.value)? uf.properties.value : geoPair;
+
+ if (geoPair) {
+ // allow for the use of a ';' as in microformats and also ',' as in Geo URL
+ geoPair = geoPair.replace(';', ',');
+
+ // has sep char
+ if (geoPair.indexOf(',') > -1 ) {
+ parts = geoPair.split(',');
+
+ // only correct if we have two or more parts
+ if (parts.length > 1) {
+
+ // latitude no value outside the range -90 or 90
+ latitude = parseFloat( parts[0] );
+ if (modules.utils.isNumber(latitude) && latitude > 90 || latitude < -90) {
+ valid = false;
+ }
+
+ // longitude no value outside the range -180 to 180
+ longitude = parseFloat( parts[1] );
+ if (modules.utils.isNumber(longitude) && longitude > 180 || longitude < -180) {
+ valid = false;
+ }
+
+ if (valid) {
+ uf.properties.latitude = [latitude];
+ uf.properties.longitude = [longitude];
+ }
+ }
+
+ }
+ }
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * if a backwards compat built structure has no properties add name through this.impliedName
+ *
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedBackwardComp = function(node, uf, parentClasses) {
+
+ // look for pattern in parent classes like "p-geo h-geo"
+ // these are structures built from backwards compat parsing of geo
+ if (parentClasses.root.length === 1 && parentClasses.properties.length === 1) {
+ if (parentClasses.root[0].replace('h-', '') === this.removePropPrefix(parentClasses.properties[0][0])) {
+
+ // if microformat has no properties apply the impliedName rule to get value from containing node
+ // this will get value from html such as <abbr class="geo" title="30.267991;-97.739568">Brighton</abbr>
+ if ( modules.utils.hasProperties(uf.properties) === false ) {
+ uf = this.impliedName( node, uf );
+ }
+ }
+ }
+
+ return uf;
+ };
+
+
+
+ }
+
+
+ // check parser module is loaded
+ if (modules.Parser) {
+
+
+ /**
+ * appends clones of include Nodes into the DOM structure
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.addIncludes = function(rootNode) {
+ this.addAttributeIncludes(rootNode, 'itemref');
+ this.addAttributeIncludes(rootNode, 'headers');
+ this.addClassIncludes(rootNode);
+ };
+
+
+ /**
+ * appends clones of include Nodes into the DOM structure for attribute based includes
+ *
+ * @param {DOM node} rootNode
+ * @param {String} attributeName
+ */
+ modules.Parser.prototype.addAttributeIncludes = function(rootNode, attributeName) {
+ var arr,
+ idList,
+ i,
+ x,
+ z,
+ y;
+
+ arr = modules.domUtils.getNodesByAttribute(rootNode, attributeName);
+ x = 0;
+ i = arr.length;
+ while (x < i) {
+ idList = modules.domUtils.getAttributeList(arr[x], attributeName);
+ if (idList) {
+ z = 0;
+ y = idList.length;
+ while (z < y) {
+ this.apppendInclude(arr[x], idList[z]);
+ z++;
+ }
+ }
+ x++;
+ }
+ };
+
+
+ /**
+ * appends clones of include Nodes into the DOM structure for class based includes
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.addClassIncludes = function(rootNode) {
+ var id,
+ arr,
+ x = 0,
+ i;
+
+ arr = modules.domUtils.getNodesByAttributeValue(rootNode, 'class', 'include');
+ i = arr.length;
+ while (x < i) {
+ id = modules.domUtils.getAttrValFromTagList(arr[x], ['a'], 'href');
+ if (!id) {
+ id = modules.domUtils.getAttrValFromTagList(arr[x], ['object'], 'data');
+ }
+ this.apppendInclude(arr[x], id);
+ x++;
+ }
+ };
+
+
+ /**
+ * appends a clone of an include into another Node using Id
+ *
+ * @param {DOM node} rootNode
+ * @param {Stringe} id
+ */
+ modules.Parser.prototype.apppendInclude = function(node, id) {
+ var include,
+ clone;
+
+ id = modules.utils.trim(id.replace('#', ''));
+ include = modules.domUtils.getElementById(this.document, id);
+ if (include) {
+ clone = modules.domUtils.clone(include);
+ this.markIncludeChildren(clone);
+ modules.domUtils.appendChild(node, clone);
+ }
+ };
+
+
+ /**
+ * adds an attribute marker to all the child microformat roots
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.markIncludeChildren = function(rootNode) {
+ var arr,
+ x,
+ i;
+
+ // loop the array and add the attribute
+ arr = this.findRootNodes(rootNode);
+ x = 0;
+ i = arr.length;
+ modules.domUtils.setAttribute(rootNode, 'data-include', 'true');
+ modules.domUtils.setAttribute(rootNode, 'style', 'display:none');
+ while (x < i) {
+ modules.domUtils.setAttribute(arr[x], 'data-include', 'true');
+ x++;
+ }
+ };
+
+
+ /**
+ * removes all appended include clones from DOM
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.removeIncludes = function(rootNode) {
+ var arr,
+ i;
+
+ // remove all the items that were added as includes
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'data-include');
+ i = arr.length;
+ while (i--) {
+ modules.domUtils.removeChild(rootNode, arr[i]);
+ }
+ };
+
+
+ }
+
+
+ // check parser module is loaded
+ if (modules.Parser) {
+
+ /**
+ * finds rel=* structures
+ *
+ * @param {DOM node} rootNode
+ * @return {Object}
+ */
+ modules.Parser.prototype.findRels = function(rootNode) {
+ var out = {
+ 'items': [],
+ 'rels': {},
+ 'rel-urls': {}
+ },
+ x,
+ i,
+ y,
+ z,
+ relList,
+ items,
+ item,
+ value,
+ arr;
+
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'rel');
+ x = 0;
+ i = arr.length;
+ while (x < i) {
+ relList = modules.domUtils.getAttribute(arr[x], 'rel');
+
+ if (relList) {
+ items = relList.split(' ');
+
+
+ // add rels
+ z = 0;
+ y = items.length;
+ while (z < y) {
+ item = modules.utils.trim(items[z]);
+
+ // get rel value
+ value = modules.domUtils.getAttrValFromTagList(arr[x], ['a', 'area'], 'href');
+ if (!value) {
+ value = modules.domUtils.getAttrValFromTagList(arr[x], ['link'], 'href');
+ }
+
+ // create the key
+ if (!out.rels[item]) {
+ out.rels[item] = [];
+ }
+
+ if (typeof this.options.baseUrl === 'string' && typeof value === 'string') {
+
+ var resolved = modules.url.resolve(value, this.options.baseUrl);
+ // do not add duplicate rels - based on resolved URLs
+ if (out.rels[item].indexOf(resolved) === -1) {
+ out.rels[item].push( resolved );
+ }
+ }
+ z++;
+ }
+
+
+ var url = null;
+ if (modules.domUtils.hasAttribute(arr[x], 'href')) {
+ url = modules.domUtils.getAttribute(arr[x], 'href');
+ if (url) {
+ url = modules.url.resolve(url, this.options.baseUrl );
+ }
+ }
+
+
+ // add to rel-urls
+ var relUrl = this.getRelProperties(arr[x]);
+ relUrl.rels = items;
+ // do not add duplicate rel-urls - based on resolved URLs
+ if (url && out['rel-urls'][url] === undefined) {
+ out['rel-urls'][url] = relUrl;
+ }
+
+
+ }
+ x++;
+ }
+ return out;
+ };
+
+
+ /**
+ * gets the properties of a rel=*
+ *
+ * @param {DOM node} node
+ * @return {Object}
+ */
+ modules.Parser.prototype.getRelProperties = function(node) {
+ var obj = {};
+
+ if (modules.domUtils.hasAttribute(node, 'media')) {
+ obj.media = modules.domUtils.getAttribute(node, 'media');
+ }
+ if (modules.domUtils.hasAttribute(node, 'type')) {
+ obj.type = modules.domUtils.getAttribute(node, 'type');
+ }
+ if (modules.domUtils.hasAttribute(node, 'hreflang')) {
+ obj.hreflang = modules.domUtils.getAttribute(node, 'hreflang');
+ }
+ if (modules.domUtils.hasAttribute(node, 'title')) {
+ obj.title = modules.domUtils.getAttribute(node, 'title');
+ }
+ if (modules.utils.trim(this.getPValue(node, false)) !== '') {
+ obj.text = this.getPValue(node, false);
+ }
+
+ return obj;
+ };
+
+
+ /**
+ * finds any alt rel=* mappings for a given node/microformat
+ *
+ * @param {DOM node} node
+ * @param {String} ufName
+ * @return {String || undefined}
+ */
+ modules.Parser.prototype.findRelImpied = function(node, ufName) {
+ var out,
+ map,
+ i;
+
+ map = this.getMapping(ufName);
+ if (map) {
+ for (var key in map.properties) {
+ if (map.properties.hasOwnProperty(key)) {
+ var prop = map.properties[key],
+ propName = (prop.map) ? prop.map : 'p-' + key,
+ relCount = 0;
+
+ // is property an alt rel=* mapping
+ if (prop.relAlt && modules.domUtils.hasAttribute(node, 'rel')) {
+ i = prop.relAlt.length;
+ while (i--) {
+ if (modules.domUtils.hasAttributeValue(node, 'rel', prop.relAlt[i])) {
+ relCount++;
+ }
+ }
+ if (relCount === prop.relAlt.length) {
+ out = propName;
+ }
+ }
+ }
+ }
+ }
+ return out;
+ };
+
+
+ /**
+ * returns whether a node or its children has rel=* microformat
+ *
+ * @param {DOM node} node
+ * @return {Boolean}
+ */
+ modules.Parser.prototype.hasRel = function(node) {
+ return (this.countRels(node) > 0);
+ };
+
+
+ /**
+ * returns the number of rel=* microformats
+ *
+ * @param {DOM node} node
+ * @return {Int}
+ */
+ modules.Parser.prototype.countRels = function(node) {
+ if (node) {
+ return modules.domUtils.getNodesByAttribute(node, 'rel').length;
+ }
+ return 0;
+ };
+
+
+
+ }
+
+
+ modules.utils = {
+
+ /**
+ * is the object a string
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isString: function( obj ) {
+ return typeof( obj ) === 'string';
+ },
+
+ /**
+ * is the object a number
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isNumber: function( obj ) {
+ return !isNaN(parseFloat( obj )) && isFinite( obj );
+ },
+
+
+ /**
+ * is the object an array
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isArray: function( obj ) {
+ return obj && !( obj.propertyIsEnumerable( 'length' ) ) && typeof obj === 'object' && typeof obj.length === 'number';
+ },
+
+
+ /**
+ * is the object a function
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isFunction: function(obj) {
+ return !!(obj && obj.constructor && obj.call && obj.apply);
+ },
+
+
+ /**
+ * does the text start with a test string
+ *
+ * @param {String} text
+ * @param {String} test
+ * @return {Boolean}
+ */
+ startWith: function( text, test ) {
+ return (text.indexOf(test) === 0);
+ },
+
+
+ /**
+ * removes spaces at front and back of text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ trim: function( text ) {
+ if (text && this.isString(text)) {
+ return (text.trim())? text.trim() : text.replace(/^\s+|\s+$/g, '');
+ }
+ return '';
+ },
+
+
+ /**
+ * replaces a character in text
+ *
+ * @param {String} text
+ * @param {Int} index
+ * @param {String} character
+ * @return {String}
+ */
+ replaceCharAt: function( text, index, character ) {
+ if (text && text.length > index) {
+ return text.substr(0, index) + character + text.substr(index+character.length);
+ }
+ return text;
+ },
+
+
+ /**
+ * removes whitespace, tabs and returns from start and end of text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ trimWhitespace: function( text ) {
+ if (text && text.length) {
+ var i = text.length,
+ x = 0;
+
+ // turn all whitespace chars at end into spaces
+ while (i--) {
+ if (this.isOnlyWhiteSpace(text[i])) {
+ text = this.replaceCharAt( text, i, ' ' );
+ } else {
+ break;
+ }
+ }
+
+ // turn all whitespace chars at start into spaces
+ i = text.length;
+ while (x < i) {
+ if (this.isOnlyWhiteSpace(text[x])) {
+ text = this.replaceCharAt( text, i, ' ' );
+ } else {
+ break;
+ }
+ x++;
+ }
+ }
+ return this.trim(text);
+ },
+
+
+ /**
+ * does text only contain whitespace characters
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ isOnlyWhiteSpace: function( text ) {
+ return !(/[^\t\n\r ]/.test( text ));
+ },
+
+
+ /**
+ * removes whitespace from text (leaves a single space)
+ *
+ * @param {String} text
+ * @return {Sring}
+ */
+ collapseWhiteSpace: function( text ) {
+ return text.replace(/[\t\n\r ]+/g, ' ');
+ },
+
+
+ /**
+ * does an object have any of its own properties
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ hasProperties: function( obj ) {
+ var key;
+ for (key in obj) {
+ if ( obj.hasOwnProperty( key ) ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * a sort function - to sort objects in an array by a given property
+ *
+ * @param {String} property
+ * @param {Boolean} reverse
+ * @return {Int}
+ */
+ sortObjects: function(property, reverse) {
+ reverse = (reverse) ? -1 : 1;
+ return function (a, b) {
+ a = a[property];
+ b = b[property];
+ if (a < b) {
+ return reverse * -1;
+ }
+ if (a > b) {
+ return reverse * 1;
+ }
+ return 0;
+ };
+ }
+
+ };
+
+
+ modules.domUtils = {
+
+ // blank objects for DOM
+ document: null,
+ rootNode: null,
+
+
+ /**
+ * gets DOMParser object
+ *
+ * @return {Object || undefined}
+ */
+ getDOMParser: function () {
+ if (typeof DOMParser === "undefined") {
+ try {
+ return Components.classes["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Components.interfaces.nsIDOMParser);
+ } catch (e) {
+ return undefined;
+ }
+ } else {
+ return new DOMParser();
+ }
+ },
+
+
+ /**
+ * configures what are the base DOM objects for parsing
+ *
+ * @param {Object} options
+ * @return {DOM Node} node
+ */
+ getDOMContext: function( options ) {
+
+ // if a node is passed
+ if (options.node) {
+ this.rootNode = options.node;
+ }
+
+
+ // if a html string is passed
+ if (options.html) {
+ // var domParser = new DOMParser();
+ var domParser = this.getDOMParser();
+ this.rootNode = domParser.parseFromString( options.html, 'text/html' );
+ }
+
+
+ // find top level document from rootnode
+ if (this.rootNode !== null) {
+ if (this.rootNode.nodeType === 9) {
+ this.document = this.rootNode;
+ this.rootNode = modules.domUtils.querySelector(this.rootNode, 'html');
+ } else {
+ // if it's DOM node get parent DOM Document
+ this.document = modules.domUtils.ownerDocument(this.rootNode);
+ }
+ }
+
+
+ // use global document object
+ if (!this.rootNode && document) {
+ this.rootNode = modules.domUtils.querySelector(document, 'html');
+ this.document = document;
+ }
+
+
+ if (this.rootNode && this.document) {
+ return {document: this.document, rootNode: this.rootNode};
+ }
+
+ return {document: null, rootNode: null};
+ },
+
+
+
+ /**
+ * gets the first DOM node
+ *
+ * @param {Dom Document}
+ * @return {DOM Node} node
+ */
+ getTopMostNode: function( node ) {
+ // var doc = this.ownerDocument(node);
+ // if(doc && doc.nodeType && doc.nodeType === 9 && doc.documentElement){
+ // return doc.documentElement;
+ // }
+ return node;
+ },
+
+
+
+ /**
+ * abstracts DOM ownerDocument
+ *
+ * @param {DOM Node} node
+ * @return {Dom Document}
+ */
+ ownerDocument: function(node) {
+ return node.ownerDocument;
+ },
+
+
+ /**
+ * abstracts DOM textContent
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ textContent: function(node) {
+ if (node.textContent) {
+ return node.textContent;
+ } else if (node.innerText) {
+ return node.innerText;
+ }
+ return '';
+ },
+
+
+ /**
+ * abstracts DOM innerHTML
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ innerHTML: function(node) {
+ return node.innerHTML;
+ },
+
+
+ /**
+ * abstracts DOM hasAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {Boolean}
+ */
+ hasAttribute: function(node, attributeName) {
+ return node.hasAttribute(attributeName);
+ },
+
+
+ /**
+ * does an attribute contain a value
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @param {String} value
+ * @return {Boolean}
+ */
+ hasAttributeValue: function(node, attributeName, value) {
+ return (this.getAttributeList(node, attributeName).indexOf(value) > -1);
+ },
+
+
+ /**
+ * abstracts DOM getAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {String || null}
+ */
+ getAttribute: function(node, attributeName) {
+ return node.getAttribute(attributeName);
+ },
+
+
+ /**
+ * abstracts DOM setAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @param {String} attributeValue
+ */
+ setAttribute: function(node, attributeName, attributeValue) {
+ node.setAttribute(attributeName, attributeValue);
+ },
+
+
+ /**
+ * abstracts DOM removeAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ */
+ removeAttribute: function(node, attributeName) {
+ node.removeAttribute(attributeName);
+ },
+
+
+ /**
+ * abstracts DOM getElementById
+ *
+ * @param {DOM Node || DOM Document} node
+ * @param {String} id
+ * @return {DOM Node}
+ */
+ getElementById: function(docNode, id) {
+ return docNode.querySelector( '#' + id );
+ },
+
+
+ /**
+ * abstracts DOM querySelector
+ *
+ * @param {DOM Node || DOM Document} node
+ * @param {String} selector
+ * @return {DOM Node}
+ */
+ querySelector: function(docNode, selector) {
+ return docNode.querySelector( selector );
+ },
+
+
+ /**
+ * get value of a Node attribute as an array
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {Array}
+ */
+ getAttributeList: function(node, attributeName) {
+ var out = [],
+ attList;
+
+ attList = node.getAttribute(attributeName);
+ if (attList && attList !== '') {
+ if (attList.indexOf(' ') > -1) {
+ out = attList.split(' ');
+ } else {
+ out.push(attList);
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * gets all child nodes with a given attribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {NodeList}
+ */
+ getNodesByAttribute: function(node, attributeName) {
+ var selector = '[' + attributeName + ']';
+ return node.querySelectorAll(selector);
+ },
+
+
+ /**
+ * gets all child nodes with a given attribute containing a given value
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {DOM NodeList}
+ */
+ getNodesByAttributeValue: function(rootNode, name, value) {
+ var arr = [],
+ x = 0,
+ i,
+ out = [];
+
+ arr = this.getNodesByAttribute(rootNode, name);
+ if (arr) {
+ i = arr.length;
+ while (x < i) {
+ if (this.hasAttributeValue(arr[x], name, value)) {
+ out.push(arr[x]);
+ }
+ x++;
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * gets attribute value from controlled list of tags
+ *
+ * @param {Array} tagNames
+ * @param {String} attributeName
+ * @return {String || null}
+ */
+ getAttrValFromTagList: function(node, tagNames, attributeName) {
+ var i = tagNames.length;
+
+ while (i--) {
+ if (node.tagName.toLowerCase() === tagNames[i]) {
+ var attrValue = this.getAttribute(node, attributeName);
+ if (attrValue && attrValue !== '') {
+ return attrValue;
+ }
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * get node if it has no siblings. CSS equivalent is :only-child
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {DOM Node || null}
+ */
+ getSingleDescendant: function(node) {
+ return this.getDescendant( node, null, false );
+ },
+
+
+ /**
+ * get node if it has no siblings of the same type. CSS equivalent is :only-of-type
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {DOM Node || null}
+ */
+ getSingleDescendantOfType: function(node, tagNames) {
+ return this.getDescendant( node, tagNames, true );
+ },
+
+
+ /**
+ * get child node limited by presence of siblings - either CSS :only-of-type or :only-child
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {DOM Node || null}
+ */
+ getDescendant: function( node, tagNames, onlyOfType ) {
+ var i = node.children.length,
+ countAll = 0,
+ countOfType = 0,
+ child,
+ out = null;
+
+ while (i--) {
+ child = node.children[i];
+ if (child.nodeType === 1) {
+ if (tagNames) {
+ // count just only-of-type
+ if (this.hasTagName(child, tagNames)) {
+ out = child;
+ countOfType++;
+ }
+ } else {
+ // count all elements
+ out = child;
+ countAll++;
+ }
+ }
+ }
+ if (onlyOfType === true) {
+ return (countOfType === 1)? out : null;
+ }
+ return (countAll === 1)? out : null;
+ },
+
+
+ /**
+ * is a node one of a list of tags
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {Boolean}
+ */
+ hasTagName: function(node, tagNames) {
+ var i = tagNames.length;
+ while (i--) {
+ if (node.tagName.toLowerCase() === tagNames[i]) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * abstracts DOM appendChild
+ *
+ * @param {DOM Node} node
+ * @param {DOM Node} childNode
+ * @return {DOM Node}
+ */
+ appendChild: function(node, childNode) {
+ return node.appendChild(childNode);
+ },
+
+
+ /**
+ * abstracts DOM removeChild
+ *
+ * @param {DOM Node} childNode
+ * @return {DOM Node || null}
+ */
+ removeChild: function(childNode) {
+ if (childNode.parentNode) {
+ return childNode.parentNode.removeChild(childNode);
+ }
+ return null;
+ },
+
+
+ /**
+ * abstracts DOM cloneNode
+ *
+ * @param {DOM Node} node
+ * @return {DOM Node}
+ */
+ clone: function(node) {
+ var newNode = node.cloneNode(true);
+ newNode.removeAttribute('id');
+ return newNode;
+ },
+
+
+ /**
+ * gets the text of a node
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ getElementText: function( node ) {
+ if (node && node.data) {
+ return node.data;
+ }
+ return '';
+ },
+
+
+ /**
+ * gets the attributes of a node - ordered by sequence in html
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ getOrderedAttributes: function( node ) {
+ var nodeStr = node.outerHTML,
+ attrs = [];
+
+ for (var i = 0; i < node.attributes.length; i++) {
+ var attr = node.attributes[i];
+ attr.indexNum = nodeStr.indexOf(attr.name);
+
+ attrs.push( attr );
+ }
+ return attrs.sort( modules.utils.sortObjects( 'indexNum' ) );
+ },
+
+
+ /**
+ * decodes html entities in given text
+ *
+ * @param {DOM Document} doc
+ * @param String} text
+ * @return {String}
+ */
+ decodeEntities: function( doc, text ) {
+ // return text;
+ return doc.createTextNode( text ).nodeValue;
+ },
+
+
+ /**
+ * clones a DOM document
+ *
+ * @param {DOM Document} document
+ * @return {DOM Document}
+ */
+ cloneDocument: function( document ) {
+ var newNode,
+ newDocument = null;
+
+ if ( this.canCloneDocument( document )) {
+ newDocument = document.implementation.createHTMLDocument('');
+ newNode = newDocument.importNode( document.documentElement, true );
+ newDocument.replaceChild(newNode, newDocument.querySelector('html'));
+ }
+ return (newNode && newNode.nodeType && newNode.nodeType === 1)? newDocument : document;
+ },
+
+
+ /**
+ * can environment clone a DOM document
+ *
+ * @param {DOM Document} document
+ * @return {Boolean}
+ */
+ canCloneDocument: function( document ) {
+ return (document && document.importNode && document.implementation && document.implementation.createHTMLDocument);
+ },
+
+
+ /**
+ * get the child index of a node. Used to create a node path
+ *
+ * @param {DOM Node} node
+ * @return {Int}
+ */
+ getChildIndex: function (node) {
+ var parent = node.parentNode,
+ i = -1,
+ child;
+ while (parent && (child = parent.childNodes[++i])) {
+ if (child === node) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+
+ /**
+ * get a node's path
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ getNodePath: function (node) {
+ var parent = node.parentNode,
+ path = [],
+ index = this.getChildIndex(node);
+
+ if (parent && (path = this.getNodePath(parent))) {
+ if (index > -1) {
+ path.push(index);
+ }
+ }
+ return path;
+ },
+
+
+ /**
+ * get a node from a path.
+ *
+ * @param {DOM document} document
+ * @param {Array} path
+ * @return {DOM Node}
+ */
+ getNodeByPath: function (document, path) {
+ var node = document.documentElement,
+ i = 0,
+ index;
+ while ((index = path[++i]) > -1) {
+ node = node.childNodes[index];
+ }
+ return node;
+ },
+
+
+ /**
+ * get an array/nodeList of child nodes
+ *
+ * @param {DOM node} node
+ * @return {Array}
+ */
+ getChildren: function( node ) {
+ return node.children;
+ },
+
+
+ /**
+ * create a node
+ *
+ * @param {String} tagName
+ * @return {DOM node}
+ */
+ createNode: function( tagName ) {
+ return this.document.createElement(tagName);
+ },
+
+
+ /**
+ * create a node with text content
+ *
+ * @param {String} tagName
+ * @param {String} text
+ * @return {DOM node}
+ */
+ createNodeWithText: function( tagName, text ) {
+ var node = this.document.createElement(tagName);
+ node.innerHTML = text;
+ return node;
+ }
+
+
+
+ };
+
+
+ modules.url = {
+
+
+ /**
+ * creates DOM objects needed to resolve URLs
+ */
+ init: function() {
+ // this._domParser = new DOMParser();
+ this._domParser = modules.domUtils.getDOMParser();
+ // do not use a head tag it does not work with IE9
+ this._html = '<base id="base" href=""></base><a id="link" href=""></a>';
+ this._nodes = this._domParser.parseFromString( this._html, 'text/html' );
+ this._baseNode = modules.domUtils.getElementById(this._nodes, 'base');
+ this._linkNode = modules.domUtils.getElementById(this._nodes, 'link');
+ },
+
+
+ /**
+ * resolves url to absolute version using baseUrl
+ *
+ * @param {String} url
+ * @param {String} baseUrl
+ * @return {String}
+ */
+ resolve: function(url, baseUrl) {
+ // use modern URL web API where we can
+ if (modules.utils.isString(url) && modules.utils.isString(baseUrl) && url.indexOf('://') === -1) {
+ // this try catch is required as IE has an URL object but no constuctor support
+ // http://glennjones.net/articles/the-problem-with-window-url
+ try {
+ var resolved = new URL(url, baseUrl).toString();
+ // deal with early Webkit not throwing an error - for Safari
+ if (resolved === '[object URL]') {
+ resolved = URI.resolve(baseUrl, url);
+ }
+ return resolved;
+ } catch (e) {
+ // otherwise fallback to DOM
+ if (this._domParser === undefined) {
+ this.init();
+ }
+
+ // do not use setAttribute it does not work with IE9
+ this._baseNode.href = baseUrl;
+ this._linkNode.href = url;
+
+ // dont use getAttribute as it returns orginal value not resolved
+ return this._linkNode.href;
+ }
+ } else {
+ if (modules.utils.isString(url)) {
+ return url;
+ }
+ return '';
+ }
+ },
+
+ };
+
+
+ /**
+ * constructor
+ * parses text to find just the date element of an ISO date/time string i.e. 2008-05-01
+ *
+ * @param {String} dateString
+ * @param {String} format
+ * @return {String}
+ */
+ modules.ISODate = function ( dateString, format ) {
+ this.clear();
+
+ this.format = (format)? format : 'auto'; // auto or W3C or RFC3339 or HTML5
+ this.setFormatSep();
+
+ // optional should be full iso date/time string
+ if (arguments[0]) {
+ this.parse(dateString, format);
+ }
+ };
+
+
+ modules.ISODate.prototype = {
+
+
+ /**
+ * clear all states
+ *
+ */
+ clear: function() {
+ this.clearDate();
+ this.clearTime();
+ this.clearTimeZone();
+ this.setAutoProfileState();
+ },
+
+
+ /**
+ * clear date states
+ *
+ */
+ clearDate: function() {
+ this.dY = -1;
+ this.dM = -1;
+ this.dD = -1;
+ this.dDDD = -1;
+ },
+
+
+ /**
+ * clear time states
+ *
+ */
+ clearTime: function() {
+ this.tH = -1;
+ this.tM = -1;
+ this.tS = -1;
+ this.tD = -1;
+ },
+
+
+ /**
+ * clear timezone states
+ *
+ */
+ clearTimeZone: function() {
+ this.tzH = -1;
+ this.tzM = -1;
+ this.tzPN = '+';
+ this.z = false;
+ },
+
+
+ /**
+ * resets the auto profile state
+ *
+ */
+ setAutoProfileState: function() {
+ this.autoProfile = {
+ sep: 'T',
+ dsep: '-',
+ tsep: ':',
+ tzsep: ':',
+ tzZulu: 'Z'
+ };
+ },
+
+
+ /**
+ * parses text to find ISO date/time string i.e. 2008-05-01T15:45:19Z
+ *
+ * @param {String} dateString
+ * @param {String} format
+ * @return {String}
+ */
+ parse: function( dateString, format ) {
+ this.clear();
+
+ var parts = [],
+ tzArray = [],
+ position = 0,
+ datePart = '',
+ timePart = '',
+ timeZonePart = '';
+
+ if (format) {
+ this.format = format;
+ }
+
+
+
+ // discover date time separtor for auto profile
+ // Set to 'T' by default
+ if (dateString.indexOf('t') > -1) {
+ this.autoProfile.sep = 't';
+ }
+ if (dateString.indexOf('z') > -1) {
+ this.autoProfile.tzZulu = 'z';
+ }
+ if (dateString.indexOf('Z') > -1) {
+ this.autoProfile.tzZulu = 'Z';
+ }
+ if (dateString.toUpperCase().indexOf('T') === -1) {
+ this.autoProfile.sep = ' ';
+ }
+
+
+ dateString = dateString.toUpperCase().replace(' ', 'T');
+
+ // break on 'T' divider or space
+ if (dateString.indexOf('T') > -1) {
+ parts = dateString.split('T');
+ datePart = parts[0];
+ timePart = parts[1];
+
+ // zulu UTC
+ if (timePart.indexOf( 'Z' ) > -1) {
+ this.z = true;
+ }
+
+ // timezone
+ if (timePart.indexOf( '+' ) > -1 || timePart.indexOf( '-' ) > -1) {
+ tzArray = timePart.split( 'Z' ); // incase of incorrect use of Z
+ timePart = tzArray[0];
+ timeZonePart = tzArray[1];
+
+ // timezone
+ if (timePart.indexOf( '+' ) > -1 || timePart.indexOf( '-' ) > -1) {
+ position = 0;
+
+ if (timePart.indexOf( '+' ) > -1) {
+ position = timePart.indexOf( '+' );
+ } else {
+ position = timePart.indexOf( '-' );
+ }
+
+ timeZonePart = timePart.substring( position, timePart.length );
+ timePart = timePart.substring( 0, position );
+ }
+ }
+
+ } else {
+ datePart = dateString;
+ }
+
+ if (datePart !== '') {
+ this.parseDate( datePart );
+ if (timePart !== '') {
+ this.parseTime( timePart );
+ if (timeZonePart !== '') {
+ this.parseTimeZone( timeZonePart );
+ }
+ }
+ }
+ return this.toString( format );
+ },
+
+
+ /**
+ * parses text to find just the date element of an ISO date/time string i.e. 2008-05-01
+ *
+ * @param {String} dateString
+ * @param {String} format
+ * @return {String}
+ */
+ parseDate: function( dateString, format ) {
+ this.clearDate();
+
+ var parts = [];
+
+ // discover timezone separtor for auto profile // default is ':'
+ if (dateString.indexOf('-') === -1) {
+ this.autoProfile.tsep = '';
+ }
+
+ // YYYY-DDD
+ parts = dateString.match( /(\d\d\d\d)-(\d\d\d)/ );
+ if (parts) {
+ if (parts[1]) {
+ this.dY = parts[1];
+ }
+ if (parts[2]) {
+ this.dDDD = parts[2];
+ }
+ }
+
+ if (this.dDDD === -1) {
+ // YYYY-MM-DD ie 2008-05-01 and YYYYMMDD ie 20080501
+ parts = dateString.match( /(\d\d\d\d)?-?(\d\d)?-?(\d\d)?/ );
+ if (parts[1]) {
+ this.dY = parts[1];
+ }
+ if (parts[2]) {
+ this.dM = parts[2];
+ }
+ if (parts[3]) {
+ this.dD = parts[3];
+ }
+ }
+ return this.toString(format);
+ },
+
+
+ /**
+ * parses text to find just the time element of an ISO date/time string i.e. 13:30:45
+ *
+ * @param {String} timeString
+ * @param {String} format
+ * @return {String}
+ */
+ parseTime: function( timeString, format ) {
+ this.clearTime();
+ var parts = [];
+
+ // discover date separtor for auto profile // default is ':'
+ if (timeString.indexOf(':') === -1) {
+ this.autoProfile.tsep = '';
+ }
+
+ // finds timezone HH:MM:SS and HHMMSS ie 13:30:45, 133045 and 13:30:45.0135
+ parts = timeString.match( /(\d\d)?:?(\d\d)?:?(\d\d)?.?([0-9]+)?/ );
+ if (parts[1]) {
+ this.tH = parts[1];
+ }
+ if (parts[2]) {
+ this.tM = parts[2];
+ }
+ if (parts[3]) {
+ this.tS = parts[3];
+ }
+ if (parts[4]) {
+ this.tD = parts[4];
+ }
+ return this.toTimeString(format);
+ },
+
+
+ /**
+ * parses text to find just the time element of an ISO date/time string i.e. +08:00
+ *
+ * @param {String} timeString
+ * @param {String} format
+ * @return {String}
+ */
+ parseTimeZone: function( timeString, format ) {
+ this.clearTimeZone();
+ var parts = [];
+
+ if (timeString.toLowerCase() === 'z') {
+ this.z = true;
+ // set case for z
+ this.autoProfile.tzZulu = (timeString === 'z')? 'z' : 'Z';
+ } else {
+
+ // discover timezone separtor for auto profile // default is ':'
+ if (timeString.indexOf(':') === -1) {
+ this.autoProfile.tzsep = '';
+ }
+
+ // finds timezone +HH:MM and +HHMM ie +13:30 and +1330
+ parts = timeString.match( /([\-\+]{1})?(\d\d)?:?(\d\d)?/ );
+ if (parts[1]) {
+ this.tzPN = parts[1];
+ }
+ if (parts[2]) {
+ this.tzH = parts[2];
+ }
+ if (parts[3]) {
+ this.tzM = parts[3];
+ }
+
+
+ }
+ this.tzZulu = 'z';
+ return this.toTimeString( format );
+ },
+
+
+ /**
+ * returns ISO date/time string in W3C Note, RFC 3339, HTML5, or auto profile
+ *
+ * @param {String} format
+ * @return {String}
+ */
+ toString: function( format ) {
+ var output = '';
+
+ if (format) {
+ this.format = format;
+ }
+ this.setFormatSep();
+
+ if (this.dY > -1) {
+ output = this.dY;
+ if (this.dM > 0 && this.dM < 13) {
+ output += this.dsep + this.dM;
+ if (this.dD > 0 && this.dD < 32) {
+ output += this.dsep + this.dD;
+ if (this.tH > -1 && this.tH < 25) {
+ output += this.sep + this.toTimeString( format );
+ }
+ }
+ }
+ if (this.dDDD > -1) {
+ output += this.dsep + this.dDDD;
+ }
+ } else if (this.tH > -1) {
+ output += this.toTimeString( format );
+ }
+
+ return output;
+ },
+
+
+ /**
+ * returns just the time string element of an ISO date/time
+ * in W3C Note, RFC 3339, HTML5, or auto profile
+ *
+ * @param {String} format
+ * @return {String}
+ */
+ toTimeString: function( format ) {
+ var out = '';
+
+ if (format) {
+ this.format = format;
+ }
+ this.setFormatSep();
+
+ // time can only be created with a full date
+ if (this.tH) {
+ if (this.tH > -1 && this.tH < 25) {
+ out += this.tH;
+ if (this.tM > -1 && this.tM < 61) {
+ out += this.tsep + this.tM;
+ if (this.tS > -1 && this.tS < 61) {
+ out += this.tsep + this.tS;
+ if (this.tD > -1) {
+ out += '.' + this.tD;
+ }
+ }
+ }
+
+
+
+ // time zone offset
+ if (this.z) {
+ out += this.tzZulu;
+ } else if (this.tzH && this.tzH > -1 && this.tzH < 25) {
+ out += this.tzPN + this.tzH;
+ if (this.tzM > -1 && this.tzM < 61) {
+ out += this.tzsep + this.tzM;
+ }
+ }
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * set the current profile to W3C Note, RFC 3339, HTML5, or auto profile
+ *
+ */
+ setFormatSep: function() {
+ switch ( this.format.toLowerCase() ) {
+ case 'rfc3339':
+ this.sep = 'T';
+ this.dsep = '';
+ this.tsep = '';
+ this.tzsep = '';
+ this.tzZulu = 'Z';
+ break;
+ case 'w3c':
+ this.sep = 'T';
+ this.dsep = '-';
+ this.tsep = ':';
+ this.tzsep = ':';
+ this.tzZulu = 'Z';
+ break;
+ case 'html5':
+ this.sep = ' ';
+ this.dsep = '-';
+ this.tsep = ':';
+ this.tzsep = ':';
+ this.tzZulu = 'Z';
+ break;
+ default:
+ // auto - defined by format of input string
+ this.sep = this.autoProfile.sep;
+ this.dsep = this.autoProfile.dsep;
+ this.tsep = this.autoProfile.tsep;
+ this.tzsep = this.autoProfile.tzsep;
+ this.tzZulu = this.autoProfile.tzZulu;
+ }
+ },
+
+
+ /**
+ * does current data contain a full date i.e. 2015-03-23
+ *
+ * @return {Boolean}
+ */
+ hasFullDate: function() {
+ return (this.dY !== -1 && this.dM !== -1 && this.dD !== -1);
+ },
+
+
+ /**
+ * does current data contain a minimum date which is just a year number i.e. 2015
+ *
+ * @return {Boolean}
+ */
+ hasDate: function() {
+ return (this.dY !== -1);
+ },
+
+
+ /**
+ * does current data contain a minimum time which is just a hour number i.e. 13
+ *
+ * @return {Boolean}
+ */
+ hasTime: function() {
+ return (this.tH !== -1);
+ },
+
+ /**
+ * does current data contain a minimum timezone i.e. -1 || +1 || z
+ *
+ * @return {Boolean}
+ */
+ hasTimeZone: function() {
+ return (this.tzH !== -1);
+ }
+
+ };
+
+ modules.ISODate.prototype.constructor = modules.ISODate;
+
+
+ modules.dates = {
+
+
+ /**
+ * does text contain am
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ hasAM: function( text ) {
+ text = text.toLowerCase();
+ return (text.indexOf('am') > -1 || text.indexOf('a.m.') > -1);
+ },
+
+
+ /**
+ * does text contain pm
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ hasPM: function( text ) {
+ text = text.toLowerCase();
+ return (text.indexOf('pm') > -1 || text.indexOf('p.m.') > -1);
+ },
+
+
+ /**
+ * remove am and pm from text and return it
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ removeAMPM: function( text ) {
+ return text.replace('pm', '').replace('p.m.', '').replace('am', '').replace('a.m.', '');
+ },
+
+
+ /**
+ * simple test of whether ISO date string is a duration i.e. PY17M or PW12
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ isDuration: function( text ) {
+ if (modules.utils.isString( text )) {
+ text = text.toLowerCase();
+ if (modules.utils.startWith(text, 'p') ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * is text a time or timezone
+ * i.e. HH-MM-SS or z+-HH-MM-SS 08:43 | 15:23:00:0567 | 10:34pm | 10:34 p.m. | +01:00:00 | -02:00 | z15:00 | 0843
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ isTime: function( text ) {
+ if (modules.utils.isString(text)) {
+ text = text.toLowerCase();
+ text = modules.utils.trim( text );
+ // start with timezone char
+ if ( text.match(':') && ( modules.utils.startWith(text, 'z') || modules.utils.startWith(text, '-') || modules.utils.startWith(text, '+') )) {
+ return true;
+ }
+ // has ante meridiem or post meridiem
+ if ( text.match(/^[0-9]/) &&
+ ( this.hasAM(text) || this.hasPM(text) )) {
+ return true;
+ }
+ // contains time delimiter but not datetime delimiter
+ if ( text.match(':') && !text.match(/t|\s/) ) {
+ return true;
+ }
+
+ // if it's a number of 2, 4 or 6 chars
+ if (modules.utils.isNumber(text)) {
+ if (text.length === 2 || text.length === 4 || text.length === 6) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * parses a time from text and returns 24hr time string
+ * i.e. 5:34am = 05:34:00 and 1:52:04p.m. = 13:52:04
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ parseAmPmTime: function( text ) {
+ var out = text,
+ times = [];
+
+ // if the string has a text : or am or pm
+ if (modules.utils.isString(out)) {
+ // text = text.toLowerCase();
+ text = text.replace(/[ ]+/g, '');
+
+ if (text.match(':') || this.hasAM(text) || this.hasPM(text)) {
+
+ if (text.match(':')) {
+ times = text.split(':');
+ } else {
+ // single number text i.e. 5pm
+ times[0] = text;
+ times[0] = this.removeAMPM(times[0]);
+ }
+
+ // change pm hours to 24hr number
+ if (this.hasPM(text)) {
+ if (times[0] < 12) {
+ times[0] = parseInt(times[0], 10) + 12;
+ }
+ }
+
+ // add leading zero's where needed
+ if (times[0] && times[0].length === 1) {
+ times[0] = '0' + times[0];
+ }
+
+ // rejoin text elements together
+ if (times[0]) {
+ text = times.join(':');
+ }
+ }
+ }
+
+ // remove am/pm strings
+ return this.removeAMPM(text);
+ },
+
+
+ /**
+ * overlays a time on a date to return the union of the two
+ *
+ * @param {String} date
+ * @param {String} time
+ * @param {String} format ( Modules.ISODate profile format )
+ * @return {Object} Modules.ISODate
+ */
+ dateTimeUnion: function(date, time, format) {
+ var isodate = new modules.ISODate(date, format),
+ isotime = new modules.ISODate();
+
+ isotime.parseTime(this.parseAmPmTime(time), format);
+ if (isodate.hasFullDate() && isotime.hasTime()) {
+ isodate.tH = isotime.tH;
+ isodate.tM = isotime.tM;
+ isodate.tS = isotime.tS;
+ isodate.tD = isotime.tD;
+ return isodate;
+ }
+ if (isodate.hasFullDate()) {
+ return isodate;
+ }
+ return new modules.ISODate();
+ },
+
+
+ /**
+ * concatenate an array of date and time text fragments to create an ISODate object
+ * used for microformat value and value-title rules
+ *
+ * @param {Array} arr ( Array of Strings )
+ * @param {String} format ( Modules.ISODate profile format )
+ * @return {Object} Modules.ISODate
+ */
+ concatFragments: function (arr, format) {
+ var out = new modules.ISODate(),
+ i = 0,
+ value = '';
+
+ // if the fragment already contains a full date just return it once
+ if (arr[0].toUpperCase().match('T')) {
+ return new modules.ISODate(arr[0], format);
+ }
+ for (i = 0; i < arr.length; i++) {
+ value = arr[i];
+
+ // date pattern
+ if ( value.charAt(4) === '-' && out.hasFullDate() === false ) {
+ out.parseDate(value);
+ }
+
+ // time pattern
+ if ( (value.indexOf(':') > -1 || modules.utils.isNumber( this.parseAmPmTime(value) )) && out.hasTime() === false ) {
+ // split time and timezone
+ var items = this.splitTimeAndZone(value);
+ value = items[0];
+
+ // parse any use of am/pm
+ value = this.parseAmPmTime(value);
+ out.parseTime(value);
+
+ // parse any timezone
+ if (items.length > 1) {
+ out.parseTimeZone(items[1], format);
+ }
+ }
+
+ // timezone pattern
+ if (value.charAt(0) === '-' || value.charAt(0) === '+' || value.toUpperCase() === 'Z') {
+ if ( out.hasTimeZone() === false ) {
+ out.parseTimeZone(value);
+ }
+ }
+
+ }
+ return out;
+ },
+
+
+ /**
+ * parses text by splitting it into an array of time and timezone strings
+ *
+ * @param {String} text
+ * @return {Array} Modules.ISODate
+ */
+ splitTimeAndZone: function ( text ) {
+ var out = [text],
+ chars = ['-', '+', 'z', 'Z'],
+ i = chars.length;
+
+ while (i--) {
+ if (text.indexOf(chars[i]) > -1) {
+ out[0] = text.slice( 0, text.indexOf(chars[i]) );
+ out.push( text.slice( text.indexOf(chars[i]) ) );
+ break;
+ }
+ }
+ return out;
+ }
+
+ };
+
+
+ modules.text = {
+
+ // normalised or whitespace or whitespacetrimmed
+ textFormat: 'whitespacetrimmed',
+
+ // block level tags, used to add line returns
+ blockLevelTags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'hr', 'pre', 'table',
+ 'address', 'article', 'aside', 'blockquote', 'caption', 'col', 'colgroup', 'dd', 'div',
+ 'dt', 'dir', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'hr',
+ 'li', 'map', 'menu', 'nav', 'optgroup', 'option', 'section', 'tbody', 'testarea',
+ 'tfoot', 'th', 'thead', 'tr', 'td', 'ul', 'ol', 'dl', 'details'],
+
+ // tags to exclude
+ excludeTags: ['noframe', 'noscript', 'template', 'script', 'style', 'frames', 'frameset'],
+
+
+ /**
+ * parses the text from the DOM Node
+ *
+ * @param {DOM Node} node
+ * @param {String} textFormat
+ * @return {String}
+ */
+ parse: function(doc, node, textFormat) {
+ var out;
+ this.textFormat = (textFormat)? textFormat : this.textFormat;
+ if (this.textFormat === 'normalised') {
+ out = this.walkTreeForText( node );
+ if (out !== undefined) {
+ return this.normalise( doc, out );
+ }
+ return '';
+ }
+ return this.formatText( doc, modules.domUtils.textContent(node), this.textFormat );
+ },
+
+
+ /**
+ * parses the text from a html string
+ *
+ * @param {DOM Document} doc
+ * @param {String} text
+ * @param {String} textFormat
+ * @return {String}
+ */
+ parseText: function( doc, text, textFormat ) {
+ var node = modules.domUtils.createNodeWithText( 'div', text );
+ return this.parse( doc, node, textFormat );
+ },
+
+
+ /**
+ * parses the text from a html string - only for whitespace or whitespacetrimmed formats
+ *
+ * @param {String} text
+ * @param {String} textFormat
+ * @return {String}
+ */
+ formatText: function( doc, text, textFormat ) {
+ this.textFormat = (textFormat)? textFormat : this.textFormat;
+ if (text) {
+ var out = '',
+ regex = /(<([^>]+)>)/ig;
+
+ out = text.replace(regex, '');
+ if (this.textFormat === 'whitespacetrimmed') {
+ out = modules.utils.trimWhitespace( out );
+ }
+
+ // return entities.decode( out, 2 );
+ return modules.domUtils.decodeEntities( doc, out );
+ }
+ return '';
+ },
+
+
+ /**
+ * normalises whitespace in given text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ normalise: function( doc, text ) {
+ text = text.replace( /&nbsp;/g, ' ') ; // exchanges html entity for space into space char
+ text = modules.utils.collapseWhiteSpace( text ); // removes linefeeds, tabs and addtional spaces
+ text = modules.domUtils.decodeEntities( doc, text ); // decode HTML entities
+ text = text.replace( '–', '-' ); // correct dash decoding
+ return modules.utils.trim( text );
+ },
+
+
+ /**
+ * walks DOM tree parsing the text from DOM Nodes
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ walkTreeForText: function( node ) {
+ var out = '',
+ j = 0;
+
+ if (node.tagName && this.excludeTags.indexOf( node.tagName.toLowerCase() ) > -1) {
+ return out;
+ }
+
+ // if node is a text node get its text
+ if (node.nodeType && node.nodeType === 3) {
+ out += modules.domUtils.getElementText( node );
+ }
+
+ // get the text of the child nodes
+ if (node.childNodes && node.childNodes.length > 0) {
+ for (j = 0; j < node.childNodes.length; j++) {
+ var text = this.walkTreeForText( node.childNodes[j] );
+ if (text !== undefined) {
+ out += text;
+ }
+ }
+ }
+
+ // if it's a block level tag add an additional space at the end
+ if (node.tagName && this.blockLevelTags.indexOf( node.tagName.toLowerCase() ) !== -1) {
+ out += ' ';
+ }
+
+ return (out === '')? undefined : out ;
+ }
+
+ };
+
+
+ modules.html = {
+
+ // elements which are self-closing
+ selfClosingElt: ['area', 'base', 'br', 'col', 'hr', 'img', 'input', 'link', 'meta', 'param', 'command', 'keygen', 'source'],
+
+
+ /**
+ * parse the html string from DOM Node
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ parse: function( node ) {
+ var out = '',
+ j = 0;
+
+ // we do not want the outer container
+ if (node.childNodes && node.childNodes.length > 0) {
+ for (j = 0; j < node.childNodes.length; j++) {
+ var text = this.walkTreeForHtml( node.childNodes[j] );
+ if (text !== undefined) {
+ out += text;
+ }
+ }
+ }
+
+ return out;
+ },
+
+
+ /**
+ * walks the DOM tree parsing the html string from the nodes
+ *
+ * @param {DOM Document} doc
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ walkTreeForHtml: function( node ) {
+ var out = '',
+ j = 0;
+
+ // if node is a text node get its text
+ if (node.nodeType && node.nodeType === 3) {
+ out += modules.domUtils.getElementText( node );
+ }
+
+
+ // exclude text which has been added with include pattern -
+ if (node.nodeType && node.nodeType === 1 && modules.domUtils.hasAttribute(node, 'data-include') === false) {
+
+ // begin tag
+ out += '<' + node.tagName.toLowerCase();
+
+ // add attributes
+ var attrs = modules.domUtils.getOrderedAttributes(node);
+ for (j = 0; j < attrs.length; j++) {
+ out += ' ' + attrs[j].name + '=' + '"' + attrs[j].value + '"';
+ }
+
+ if (this.selfClosingElt.indexOf(node.tagName.toLowerCase()) === -1) {
+ out += '>';
+ }
+
+ // get the text of the child nodes
+ if (node.childNodes && node.childNodes.length > 0) {
+
+ for (j = 0; j < node.childNodes.length; j++) {
+ var text = this.walkTreeForHtml( node.childNodes[j] );
+ if (text !== undefined) {
+ out += text;
+ }
+ }
+ }
+
+ // end tag
+ if (this.selfClosingElt.indexOf(node.tagName.toLowerCase()) > -1) {
+ out += ' />';
+ } else {
+ out += '</' + node.tagName.toLowerCase() + '>';
+ }
+ }
+
+ return (out === '')? undefined : out;
+ }
+
+
+ };
+
+
+ modules.maps['h-adr'] = {
+ root: 'adr',
+ name: 'h-adr',
+ properties: {
+ 'post-office-box': {},
+ 'street-address': {},
+ 'extended-address': {},
+ 'locality': {},
+ 'region': {},
+ 'postal-code': {},
+ 'country-name': {}
+ }
+ };
+
+
+ modules.maps['h-card'] = {
+ root: 'vcard',
+ name: 'h-card',
+ properties: {
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'adr': {
+ 'map': 'p-adr',
+ 'uf': ['h-adr']
+ },
+ 'agent': {
+ 'uf': ['h-card']
+ },
+ 'bday': {
+ 'map': 'dt-bday'
+ },
+ 'class': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'email': {
+ 'map': 'u-email'
+ },
+ 'geo': {
+ 'map': 'p-geo',
+ 'uf': ['h-geo']
+ },
+ 'key': {
+ 'map': 'u-key'
+ },
+ 'label': {},
+ 'logo': {
+ 'map': 'u-logo'
+ },
+ 'mailer': {},
+ 'honorific-prefix': {},
+ 'given-name': {},
+ 'additional-name': {},
+ 'family-name': {},
+ 'honorific-suffix': {},
+ 'nickname': {},
+ 'note': {}, // could be html i.e. e-note
+ 'org': {},
+ 'p-organization-name': {},
+ 'p-organization-unit': {},
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ 'rev': {
+ 'map': 'dt-rev'
+ },
+ 'role': {},
+ 'sequence': {},
+ 'sort-string': {},
+ 'sound': {
+ 'map': 'u-sound'
+ },
+ 'title': {
+ 'map': 'p-job-title'
+ },
+ 'tel': {},
+ 'tz': {},
+ 'uid': {
+ 'map': 'u-uid'
+ },
+ 'url': {
+ 'map': 'u-url'
+ }
+ }
+ };
+
+
+ modules.maps['h-entry'] = {
+ root: 'hentry',
+ name: 'h-entry',
+ properties: {
+ 'entry-title': {
+ 'map': 'p-name'
+ },
+ 'entry-summary': {
+ 'map': 'p-summary'
+ },
+ 'entry-content': {
+ 'map': 'e-content'
+ },
+ 'published': {
+ 'map': 'dt-published'
+ },
+ 'updated': {
+ 'map': 'dt-updated'
+ },
+ 'author': {
+ 'uf': ['h-card']
+ },
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'geo': {
+ 'map': 'p-geo',
+ 'uf': ['h-geo']
+ },
+ 'latitude': {},
+ 'longitude': {},
+ 'url': {
+ 'map': 'u-url',
+ 'relAlt': ['bookmark']
+ }
+ }
+ };
+
+
+ modules.maps['h-event'] = {
+ root: 'vevent',
+ name: 'h-event',
+ properties: {
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'dtstart': {
+ 'map': 'dt-start'
+ },
+ 'dtend': {
+ 'map': 'dt-end'
+ },
+ 'description': {},
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'location': {
+ 'uf': ['h-card']
+ },
+ 'geo': {
+ 'uf': ['h-geo']
+ },
+ 'latitude': {},
+ 'longitude': {},
+ 'duration': {
+ 'map': 'dt-duration'
+ },
+ 'contact': {
+ 'uf': ['h-card']
+ },
+ 'organizer': {
+ 'uf': ['h-card']},
+ 'attendee': {
+ 'uf': ['h-card']},
+ 'uid': {
+ 'map': 'u-uid'
+ },
+ 'attach': {
+ 'map': 'u-attach'
+ },
+ 'status': {},
+ 'rdate': {},
+ 'rrule': {}
+ }
+ };
+
+
+ modules.maps['h-feed'] = {
+ root: 'hfeed',
+ name: 'h-feed',
+ properties: {
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'summary': {
+ 'map': 'p-summary'
+ },
+ 'author': {
+ 'uf': ['h-card']
+ },
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ }
+ };
+
+
+ modules.maps['h-geo'] = {
+ root: 'geo',
+ name: 'h-geo',
+ properties: {
+ 'latitude': {},
+ 'longitude': {}
+ }
+ };
+
+
+ modules.maps['h-item'] = {
+ root: 'item',
+ name: 'h-item',
+ subTree: false,
+ properties: {
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ }
+ }
+ };
+
+
+ modules.maps['h-listing'] = {
+ root: 'hlisting',
+ name: 'h-listing',
+ properties: {
+ 'version': {},
+ 'lister': {
+ 'uf': ['h-card']
+ },
+ 'dtlisted': {
+ 'map': 'dt-listed'
+ },
+ 'dtexpired': {
+ 'map': 'dt-expired'
+ },
+ 'location': {},
+ 'price': {},
+ 'item': {
+ 'uf': ['h-card', 'a-adr', 'h-geo']
+ },
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'description': {
+ 'map': 'e-description'
+ },
+ 'listing': {}
+ }
+ };
+
+
+ modules.maps['h-news'] = {
+ root: 'hnews',
+ name: 'h-news',
+ properties: {
+ 'entry': {
+ 'uf': ['h-entry']
+ },
+ 'geo': {
+ 'uf': ['h-geo']
+ },
+ 'latitude': {},
+ 'longitude': {},
+ 'source-org': {
+ 'uf': ['h-card']
+ },
+ 'dateline': {
+ 'uf': ['h-card']
+ },
+ 'item-license': {
+ 'map': 'u-item-license'
+ },
+ 'principles': {
+ 'map': 'u-principles',
+ 'relAlt': ['principles']
+ }
+ }
+ };
+
+
+ modules.maps['h-org'] = {
+ root: 'h-x-org', // drop this from v1 as it causes issue with fn org hcard pattern
+ name: 'h-org',
+ childStructure: true,
+ properties: {
+ 'organization-name': {},
+ 'organization-unit': {}
+ }
+ };
+
+
+ modules.maps['h-product'] = {
+ root: 'hproduct',
+ name: 'h-product',
+ properties: {
+ 'brand': {
+ 'uf': ['h-card']
+ },
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'price': {},
+ 'description': {
+ 'map': 'e-description'
+ },
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'review': {
+ 'uf': ['h-review', 'h-review-aggregate']
+ },
+ 'listing': {
+ 'uf': ['h-listing']
+ },
+ 'identifier': {
+ 'map': 'u-identifier'
+ }
+ }
+ };
+
+
+ modules.maps['h-recipe'] = {
+ root: 'hrecipe',
+ name: 'h-recipe',
+ properties: {
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'ingredient': {
+ 'map': 'e-ingredient'
+ },
+ 'yield': {},
+ 'instructions': {
+ 'map': 'e-instructions'
+ },
+ 'duration': {
+ 'map': 'dt-duration'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ 'summary': {},
+ 'author': {
+ 'uf': ['h-card']
+ },
+ 'published': {
+ 'map': 'dt-published'
+ },
+ 'nutrition': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ }
+ };
+
+
+ modules.maps['h-resume'] = {
+ root: 'hresume',
+ name: 'h-resume',
+ properties: {
+ 'summary': {},
+ 'contact': {
+ 'uf': ['h-card']
+ },
+ 'education': {
+ 'uf': ['h-card', 'h-event']
+ },
+ 'experience': {
+ 'uf': ['h-card', 'h-event']
+ },
+ 'skill': {},
+ 'affiliation': {
+ 'uf': ['h-card']
+ }
+ }
+ };
+
+
+ modules.maps['h-review-aggregate'] = {
+ root: 'hreview-aggregate',
+ name: 'h-review-aggregate',
+ properties: {
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'item': {
+ 'map': 'p-item',
+ 'uf': ['h-item', 'h-geo', 'h-adr', 'h-card', 'h-event', 'h-product']
+ },
+ 'rating': {},
+ 'average': {},
+ 'best': {},
+ 'worst': {},
+ 'count': {},
+ 'votes': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'url': {
+ 'map': 'u-url',
+ 'relAlt': ['self', 'bookmark']
+ }
+ }
+ };
+
+
+ modules.maps['h-review'] = {
+ root: 'hreview',
+ name: 'h-review',
+ properties: {
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'description': {
+ 'map': 'e-description'
+ },
+ 'item': {
+ 'map': 'p-item',
+ 'uf': ['h-item', 'h-geo', 'h-adr', 'h-card', 'h-event', 'h-product']
+ },
+ 'reviewer': {
+ 'uf': ['h-card']
+ },
+ 'dtreviewer': {
+ 'map': 'dt-reviewer'
+ },
+ 'rating': {},
+ 'best': {},
+ 'worst': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'url': {
+ 'map': 'u-url',
+ 'relAlt': ['self', 'bookmark']
+ }
+ }
+ };
+
+
+ modules.rels = {
+ // xfn
+ 'friend': [ 'yes', 'external'],
+ 'acquaintance': [ 'yes', 'external'],
+ 'contact': [ 'yes', 'external'],
+ 'met': [ 'yes', 'external'],
+ 'co-worker': [ 'yes', 'external'],
+ 'colleague': [ 'yes', 'external'],
+ 'co-resident': [ 'yes', 'external'],
+ 'neighbor': [ 'yes', 'external'],
+ 'child': [ 'yes', 'external'],
+ 'parent': [ 'yes', 'external'],
+ 'sibling': [ 'yes', 'external'],
+ 'spouse': [ 'yes', 'external'],
+ 'kin': [ 'yes', 'external'],
+ 'muse': [ 'yes', 'external'],
+ 'crush': [ 'yes', 'external'],
+ 'date': [ 'yes', 'external'],
+ 'sweetheart': [ 'yes', 'external'],
+ 'me': [ 'yes', 'external'],
+
+ // other rel=*
+ 'license': [ 'yes', 'yes'],
+ 'nofollow': [ 'no', 'external'],
+ 'tag': [ 'no', 'yes'],
+ 'self': [ 'no', 'external'],
+ 'bookmark': [ 'no', 'external'],
+ 'author': [ 'no', 'external'],
+ 'home': [ 'no', 'external'],
+ 'directory': [ 'no', 'external'],
+ 'enclosure': [ 'no', 'external'],
+ 'pronunciation': [ 'no', 'external'],
+ 'payment': [ 'no', 'external'],
+ 'principles': [ 'no', 'external']
+
+ };
+
+
+
+ var External = {
+ version: modules.version,
+ livingStandard: modules.livingStandard
+ };
+
+
+ External.get = function(options) {
+ var parser = new modules.Parser();
+ addV1(parser, options);
+ return parser.get( options );
+ };
+
+
+ External.getParent = function(node, options) {
+ var parser = new modules.Parser();
+ addV1(parser, options);
+ return parser.getParent( node, options );
+ };
+
+
+ External.count = function(options) {
+ var parser = new modules.Parser();
+ addV1(parser, options);
+ return parser.count( options );
+ };
+
+
+ External.isMicroformat = function( node, options ) {
+ var parser = new modules.Parser();
+ addV1(parser, options);
+ return parser.isMicroformat( node, options );
+ };
+
+
+ External.hasMicroformats = function( node, options ) {
+ var parser = new modules.Parser();
+ addV1(parser, options);
+ return parser.hasMicroformats( node, options );
+ };
+
+
+ function addV1(parser, options) {
+ if (options && options.maps) {
+ if (Array.isArray(options.maps)) {
+ parser.add(options.maps);
+ } else {
+ parser.add([options.maps]);
+ }
+ }
+ }
+
+
+ return External;
+
+
+}));
+try {
+ // mozilla jsm support
+ Components.utils.importGlobalProperties(["URL"]);
+} catch (e) {}
+this.EXPORTED_SYMBOLS = ['Microformats'];
diff --git a/toolkit/components/microformats/moz.build b/toolkit/components/microformats/moz.build
new file mode 100644
index 0000000000..39cefe4c87
--- /dev/null
+++ b/toolkit/components/microformats/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_JS_MODULES += [
+ 'microformat-shiv.js'
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Microformats')
diff --git a/toolkit/components/microformats/test/interface-tests/count-test.js b/toolkit/components/microformats/test/interface-tests/count-test.js
new file mode 100644
index 0000000000..baac56c2b4
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/count-test.js
@@ -0,0 +1,107 @@
+/*
+Unit test for count
+*/
+
+assert = chai.assert;
+
+
+describe('Microformat.count', function() {
+
+ it('count', function(){
+
+ var doc,
+ node,
+ result;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a><a class="h-card" href="http://janedoe.net"><span class="p-name">Jane</span></a><a class="h-event" href="http://janedoe.net"><span class="p-name">Event</span><span class="dt-start">2015-07-01</span></a>';
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ };
+
+ result = Microformats.count(options);
+ assert.deepEqual( result, {'h-event': 1,'h-card': 2} );
+
+ });
+
+
+ it('count rels', function(){
+
+ var doc,
+ node,
+ result;
+
+ var html = '<link href="http://glennjones.net/notes/atom" rel="notes alternate" title="Notes" type="application/atom+xml" /><a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a><a class="h-card" href="http://janedoe.net"><span class="p-name">Jane</span></a><a class="h-event" href="http://janedoe.net"><span class="p-name">Event</span><span class="dt-start">2015-07-01</span></a>';
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ };
+
+ result = Microformats.count(options);
+ assert.deepEqual( result, {'h-event': 1,'h-card': 2, 'rels': 1} );
+
+ });
+
+
+ it('count - no results', function(){
+
+ var doc,
+ node,
+ result;
+
+ var html = '<span class="p-name">Jane</span>';
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ };
+
+ result = Microformats.count(options);
+ assert.deepEqual( result, {} );
+
+ });
+
+
+
+ it('count - no options', function(){
+
+ var result;
+
+ result = Microformats.count({});
+ assert.deepEqual( result, {} );
+
+ });
+
+
+ it('count - options.html', function(){
+
+ var options = {},
+ result;
+
+ options.html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a><a class="h-card" href="http://janedoe.net"><span class="p-name">Jane</span></a><a class="h-event" href="http://janedoe.net"><span class="p-name">Event</span><span class="dt-start">2015-07-01</span></a>';
+
+ result = Microformats.count(options);
+ assert.deepEqual( result, {'h-event': 1,'h-card': 2} );
+
+ });
+
+
+
+ });
diff --git a/toolkit/components/microformats/test/interface-tests/experimental-test.js b/toolkit/components/microformats/test/interface-tests/experimental-test.js
new file mode 100644
index 0000000000..4d32b83c05
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/experimental-test.js
@@ -0,0 +1,37 @@
+/*
+Unit test for get
+*/
+
+assert = chai.assert;
+
+
+describe('experimental', function() {
+
+ it('h-geo - geo data writen as lat;lon', function(){
+
+ var expected = {
+ 'items': [{
+ 'type': ['h-geo'],
+ 'properties': {
+ 'name': ['30.267991;-97.739568'],
+ 'latitude': [30.267991],
+ 'longitude': [-97.739568]
+ }
+ }],
+ 'rels': {},
+ 'rel-urls': {}
+ },
+ options = {
+ 'html': '<div class="h-geo">30.267991;-97.739568</div>',
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5',
+ 'parseLatLonGeo': true
+ };
+
+ var result = Microformats.get(options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+});
diff --git a/toolkit/components/microformats/test/interface-tests/get-test.js b/toolkit/components/microformats/test/interface-tests/get-test.js
new file mode 100644
index 0000000000..098ff4e3d0
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/get-test.js
@@ -0,0 +1,605 @@
+/*
+Unit test for get
+*/
+
+assert = chai.assert;
+
+
+describe('Microformat.get', function() {
+
+
+ var expected = {
+ 'items': [{
+ 'type': ['h-card'],
+ 'properties': {
+ 'name': ['Glenn Jones'],
+ 'url': ['http://glennjones.net']
+ }
+ }],
+ 'rels': {
+ 'bookmark': ['http://glennjones.net'],
+ 'alternate': ['http://example.com/fr'],
+ 'home': ['http://example.com/fr']
+ },
+ 'rel-urls': {
+ 'http://glennjones.net': {
+ 'text': 'Glenn Jones',
+ 'rels': ['bookmark']
+ },
+ 'http://example.com/fr': {
+ 'media': 'handheld',
+ 'hreflang': 'fr',
+ 'text': 'French mobile homepage',
+ 'rels': ['alternate', 'home']
+ }
+ }
+ },
+ html = '<div class="h-card"><a class="p-name u-url" rel="bookmark" href="http://glennjones.net">Glenn Jones</a></div><a rel="alternate home" href="http://example.com/fr" media="handheld" hreflang="fr">French mobile homepage</a>';
+
+
+
+
+
+ it('get - no options.node parse this document', function(){
+ var result;
+
+ result = Microformats.get({});
+ assert.deepEqual( result.items, [] );
+ });
+
+
+ it('get - standard', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+ it('get - doc pass to node', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+ it('get - pass base tag', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html + '<base href="http://example.com">';
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+ it('get - pass no document', function(){
+
+ var doc,
+ node,
+ options,
+ parser,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = html + '<base href="http://example.com">';
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': doc,
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+ it('get - textFormat: normalised', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<a class="h-card" href="http://glennjones.net">\n';
+ altHTML += ' <span class="p-given-name">Glenn</span>\n';
+ altHTML += ' <span class="p-family-name">Jones</span>\n';
+ altHTML += '</a>\n';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'textFormat': 'normalised',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.name[0], 'Glenn Jones' );
+
+ });
+
+
+ it('get - textFormat: whitespace', function(){
+
+ var doc,
+ node,
+ options,
+ parser,
+ result;
+
+ var altHTML = '<a class="h-card" href="http://glennjones.net">\n';
+ altHTML += ' <span class="p-given-name">Glenn</span>\n';
+ altHTML += ' <span class="p-family-name">Jones</span>\n';
+ altHTML += '</a>\n';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'textFormat': 'whitespace',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.name[0], '\n Glenn\n Jones\n' );
+
+ });
+
+
+
+ it('get - textFormat: whitespacetrimmed', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<a class="h-card" href="http://glennjones.net">\n';
+ altHTML += ' <span class="p-given-name">Glenn</span>\n';
+ altHTML += ' <span class="p-family-name">Jones</span>\n';
+ altHTML += '</a>\n';
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'textFormat': 'whitespacetrimmed',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.name[0], 'Glenn\n Jones' );
+
+ });
+
+
+ it('get - dateFormat: auto', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'dateFormat': 'auto'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.start[0], '2015-07-01t17:30z' );
+
+ });
+
+
+ it('get - dateFormat: w3c', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'dateFormat': 'w3c'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.start[0], '2015-07-01T17:30Z' );
+
+ });
+
+
+ it('get - dateFormat: html5', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.start[0], '2015-07-01 17:30Z' );
+
+ });
+
+
+
+ it('get - dateFormat: rfc3339', function(){
+
+ var doc,
+ node,
+ options,
+
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'dateFormat': 'rfc3339'
+ };
+
+ result = Microformats.get(options);
+ assert.equal( result.items[0].properties.start[0], '20150701T1730Z' );
+
+ });
+
+
+
+ it('get - filters h-card', function(){
+
+ var doc,
+ node,
+ options,
+ parser,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div><a class="h-card" href="http://glennjones.net">Glenn Jones</a>';
+ var altExpected = {
+ 'items': [{
+ 'type': ['h-card'],
+ 'properties': {
+ 'name': ['Glenn Jones'],
+ 'url': ['http://glennjones.net']
+ }
+ }],
+ 'rels': {},
+ 'rel-urls': {}
+ }
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ // filter as an array
+ options ={
+ 'node': node,
+ 'filters': ['h-card'],
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+
+ // filter as an string
+ options ={
+ 'node': node,
+ 'filters': 'h-card',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+ });
+
+
+ it('get - filters h-event', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div><a class="h-card" href="http://glennjones.net">Glenn Jones</a>';
+ var altExpected = {
+ 'items': [{
+ 'type': ['h-event'],
+ 'properties': {
+ 'name': ['Pub'],
+ 'start': ['2015-07-01 17:30Z']
+ }
+ }],
+ 'rels': {},
+ 'rel-urls': {}
+ }
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'filters': ['h-event'],
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+ it('get - filters h-card and h-event', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div><a class="h-card" href="http://glennjones.net">Glenn Jones</a>';
+ var altExpected = {
+ 'items': [{
+ 'type': ['h-event'],
+ 'properties': {
+ 'name': ['Pub'],
+ 'start': ['2015-07-01 17:30Z']
+ }
+ },
+ {
+ 'type': ['h-card'],
+ 'properties': {
+ 'name': ['Glenn Jones'],
+ 'url': ['http://glennjones.net']
+ }
+ }],
+ 'rels': {},
+ 'rel-urls': {}
+ }
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'filter': ['h-event'],
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+ it('get - filters h-card no result', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div>';
+ var altExpected = {
+ 'items': [],
+ 'rels': {},
+ 'rel-urls': {}
+ }
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'filters': ['h-card'],
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+ it('get - filters h-card match v1 format', function(){
+
+ var doc,
+ node,
+ options,
+ parser,
+ result;
+
+ var altHTML = '<a class="vcard" href="http://glennjones.net"><span class="fn">Glenn Jones</span></a>';
+ var altExpected = {
+ 'items': [{
+ 'type': ['h-card'],
+ 'properties': {
+ 'name': ['Glenn Jones']
+ }
+ }],
+ 'rels': {},
+ 'rel-urls': {}
+ }
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'filter': ['h-card'],
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+ it('get - add new v1 format through options', function(){
+
+ var doc,
+ node,
+ options,
+ result;
+
+ var altHTML = '<div class="hpayment">£<span class="amount">36.78</span></div>';
+ var altExpected = {
+ 'items': [{
+ 'type': ['h-payment'],
+ 'properties': {
+ 'amount': ['36.78']
+ }
+ }],
+ 'rels': {},
+ 'rel-urls': {}
+ };
+ var v1Definition = {
+ root: 'hpayment',
+ name: 'h-payment',
+ properties: {
+ 'amount': {}
+ }
+ };
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node,
+ 'maps': v1Definition,
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+ it('get - options.html', function(){
+
+ var options,
+ result;
+
+ options ={
+ 'html': html,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+
+ result = Microformats.get(options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+
+
+
+});
diff --git a/toolkit/components/microformats/test/interface-tests/getParent-test.js b/toolkit/components/microformats/test/interface-tests/getParent-test.js
new file mode 100644
index 0000000000..56ccbb2ba7
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/getParent-test.js
@@ -0,0 +1,220 @@
+/*
+Unit test for getParent
+*/
+
+assert = chai.assert;
+
+
+describe('Microformat.getParent', function() {
+
+ var HTML = '<div class="h-event"><span class="p-name">Pub</span><span class="dt-start">2015-07-01t17:30z</span></div>';
+ var emptyExpected = {
+ "items": [],
+ "rels": {},
+ "rel-urls": {}
+ };
+ var expected = {
+ "items": [
+ {
+ "type": [
+ "h-event"
+ ],
+ "properties": {
+ "name": [
+ "Pub"
+ ],
+ "start": [
+ "2015-07-01 17:30Z"
+ ]
+ }
+ }
+ ],
+ "rels": {},
+ "rel-urls": {}
+ };
+ var options = {'dateFormat': 'html5'};
+
+
+
+
+ it('getParent with parent', function(){
+
+ var doc,
+ node,
+ span,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = HTML;
+ doc.body.appendChild(node);
+ span = doc.querySelector('.dt-start');
+
+ result = Microformats.getParent(span,options);
+ assert.deepEqual( result, expected );
+
+ });
+
+
+
+ it('getParent without parent', function(){
+
+ var doc,
+ node,
+ parser,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = HTML;
+ doc.body.appendChild(node);
+
+ result = Microformats.getParent(node,options);
+ assert.deepEqual( result, emptyExpected );
+
+ });
+
+
+ it('getParent found with option.filters', function(){
+
+ var doc,
+ node,
+ span,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = HTML;
+ doc.body.appendChild(node);
+ span = doc.querySelector('.dt-start');
+
+ result = Microformats.getParent( span, {'filters': ['h-event'], 'dateFormat': 'html5'} );
+ assert.deepEqual( result, expected );
+
+ });
+
+
+ it('getParent not found with option.filters', function(){
+
+ var doc,
+ node,
+ span,
+ result;
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = HTML;
+ doc.body.appendChild(node);
+ span = doc.querySelector('.dt-start');
+
+ result = Microformats.getParent( span, {'filters': ['h-card'], 'dateFormat': 'html5'} );
+ assert.deepEqual( result, emptyExpected );
+
+ });
+
+
+ it('getParent use option.filters to up through h-*', function(){
+
+ var doc,
+ node,
+ span,
+ result;
+
+ var altHTML = '<div class="h-entry"><h1 class="p-name">test</h1><div class="e-content">this</div><a class="p-author h-card" href="http://glennjones.net"><span class="p-name">Glenn Jones</span></a><span class="dt-publish">2015-07-01t17:30z</span></div>';
+ var altExpected = {
+ "items": [
+ {
+ "type": [
+ "h-entry"
+ ],
+ "properties": {
+ "name": [
+ "test"
+ ],
+ "content": [
+ {
+ "value": "this",
+ "html": "this"
+ }
+ ],
+ "author": [
+ {
+ "value": "Glenn Jones",
+ "type": [
+ "h-card"
+ ],
+ "properties": {
+ "name": [
+ "Glenn Jones"
+ ],
+ "url": [
+ "http://glennjones.net"
+ ]
+ }
+ }
+ ],
+ "publish": [
+ "2015-07-01 17:30Z"
+ ]
+ }
+ }
+ ],
+ "rels": {},
+ "rel-urls": {}
+ };
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+ span = doc.querySelector('.h-card .p-name');
+
+ result = Microformats.getParent( span, {'filters': ['h-entry'], 'dateFormat': 'html5'} );
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+ it('getParent stop at first h-* parent', function(){
+
+ var doc,
+ node,
+ span,
+ result;
+
+ var altHTML = '<div class="h-entry"><h1 class="p-name">test</h1><div class="e-content">this</div><a class="p-author h-card" href="http://glennjones.net"><span class="p-name">Glenn Jones</span></a><span class="dt-publish">2015-07-01t17:30z</span></div>';
+ var altExpected = {
+ "items": [
+ {
+ "type": [
+ "h-card"
+ ],
+ "properties": {
+ "name": [
+ "Glenn Jones"
+ ],
+ "url": [
+ "http://glennjones.net"
+ ]
+ }
+ }
+ ],
+ "rels": {},
+ "rel-urls": {}
+ };
+
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ node.innerHTML = altHTML;
+ doc.body.appendChild(node);
+ span = doc.querySelector('.h-card .p-name');
+
+ result = Microformats.getParent( span, options );
+ assert.deepEqual( result, altExpected );
+
+ });
+
+
+});
diff --git a/toolkit/components/microformats/test/interface-tests/hasMicroformats-test.js b/toolkit/components/microformats/test/interface-tests/hasMicroformats-test.js
new file mode 100644
index 0000000000..98c79a8551
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/hasMicroformats-test.js
@@ -0,0 +1,185 @@
+/*
+Unit test for hasMicroformat
+*/
+
+assert = chai.assert;
+
+
+describe('Microformat.hasMicroformats', function() {
+
+ it('true - v2 on node', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.hasMicroformats( node ) );
+
+ });
+
+
+ it('true - v1 on node', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="vcard" href="http://glennjones.net"><span class="fn">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.hasMicroformats( node ) );
+
+ });
+
+
+ it('true - v2 filter on node', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.hasMicroformats( node, {'filters': ['h-card']} ) );
+
+ });
+
+
+ it('true - v1 filter on node', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="vcard" href="http://glennjones.net"><span class="fn">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.hasMicroformats( node, {'filters': ['h-card']} ) );
+
+ });
+
+
+ it('false - v2 filter on node', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isFalse( Microformats.hasMicroformats( node, {'filters': ['h-entry']} ) );
+
+ });
+
+
+
+ it('false - property', function(){
+
+ var doc,
+ node,
+ parser;
+
+ var html = '<span class="p-name">Glenn</span>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'span' );
+
+ assert.isFalse( Microformats.hasMicroformats( node ) );
+
+ });
+
+
+ it('false - no class', function(){
+
+ var doc,
+ node,
+ parser;
+
+ var html = '<span>Glenn</span>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'span' );
+
+ assert.isFalse( Microformats.hasMicroformats( node ) );
+
+ });
+
+
+ it('false - no node', function(){
+ assert.isFalse( Microformats.hasMicroformats( ) );
+ });
+
+
+ it('false - undefined node', function(){
+ assert.isFalse( Microformats.hasMicroformats( undefined ) );
+ });
+
+
+ it('true - child', function(){
+
+ var doc,
+ node;
+
+ var html = '<section><div><a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a></div></section>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+
+ assert.isTrue( Microformats.hasMicroformats( node ) );
+
+ });
+
+
+
+ it('true - document', function(){
+
+ var doc,
+ node;
+
+ var html = '<html><head></head><body><section><div><a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a></div></section></body></html>';
+
+ var dom = new DOMParser();
+ doc = dom.parseFromString( html, 'text/html' );
+
+ assert.isTrue( Microformats.hasMicroformats( doc ) );
+
+ });
+
+
+
+
+
+ });
diff --git a/toolkit/components/microformats/test/interface-tests/index.html b/toolkit/components/microformats/test/interface-tests/index.html
new file mode 100644
index 0000000000..61759790ed
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/index.html
@@ -0,0 +1,69 @@
+<html><head><title>Mocha</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<link rel="stylesheet" href="../static/css/mocha.css" />
+<script src="../static/javascript/chai.js"></script>
+<script src="../static/javascript/mocha.js"></script>
+<link rel="stylesheet" href="../static/css/mocha-custom.css" />
+<link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
+
+<script src="../static/javascript/DOMParser.js"></script>
+
+<script data-cover src="../../microformat-shiv.js"></script>
+
+<script>
+var uncaughtError;
+
+window.addEventListener("error", function(error) {
+uncaughtError = error;
+});
+
+var consoleWarn = console.warn;
+var caughtWarnings = [];
+
+console.warn = function() {
+var args = Array.slice(arguments);
+caughtWarnings.push(args);
+consoleWarn.apply(console, args);
+};
+</script>
+
+<script>
+chai.config.includeStack = true;
+mocha.setup({ui: 'bdd', timeout: 10000});
+</script>
+
+
+<script src="../interface-tests/get-test.js"></script>
+<script src="../interface-tests/getParent-test.js"></script>
+<script src="../interface-tests/count-test.js"></script>
+<script src="../interface-tests/isMicroformat-test.js"></script>
+<script src="../interface-tests/hasMicroformats-test.js"></script>
+
+<script src="../interface-tests/experimental-test.js"></script>
+
+</head><body>
+<h3 class="capitalize">Microformats-shiv: interface tests</h3>
+<div id="mocha"></div>
+</body>
+<script>
+describe("Uncaught Error Check", function() {
+it("should load the tests without errors", function() {
+chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
+});
+});
+
+describe("Unexpected Warnings Check", function() {
+it("should long only the warnings we expect", function() {
+chai.expect(caughtWarnings.length).to.eql(0);
+});
+});
+
+mocha.run(function () {
+var completeNode = document.createElement("p");
+completeNode.setAttribute("id", "complete");
+completeNode.appendChild(document.createTextNode("Complete"));
+document.getElementById("mocha").appendChild(completeNode);
+});
+
+</script>
+</body></html>
diff --git a/toolkit/components/microformats/test/interface-tests/isMicroformat-test.js b/toolkit/components/microformats/test/interface-tests/isMicroformat-test.js
new file mode 100644
index 0000000000..7081b28804
--- /dev/null
+++ b/toolkit/components/microformats/test/interface-tests/isMicroformat-test.js
@@ -0,0 +1,146 @@
+/*
+Unit test for isMicroformat
+*/
+
+assert = chai.assert;
+
+
+describe('Microformat.isMicroformat', function() {
+
+ it('true - v2', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.isMicroformat( node ) );
+
+ });
+
+
+ it('true - v1', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="vcard" href="http://glennjones.net"><span class="fn">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.isMicroformat( node ) );
+
+ });
+
+
+ it('true - v2 filter', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.isMicroformat( node, {'filters': ['h-card']} ) );
+
+ });
+
+
+ it('true - v1 filter', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="vcard" href="http://glennjones.net"><span class="fn">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isTrue( Microformats.isMicroformat( node, {'filters': ['h-card']} ) );
+
+ });
+
+
+ it('false - v2 filter', function(){
+
+ var doc,
+ node;
+
+ var html = '<a class="h-card" href="http://glennjones.net"><span class="p-name">Glenn</span></a>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'a' );
+
+ assert.isFalse( Microformats.isMicroformat( node, {'filters': ['h-entry']} ) );
+
+ });
+
+
+
+ it('false - property', function(){
+
+ var doc,
+ node;
+
+ var html = '<span class="p-name">Glenn</span>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'span' );
+
+ assert.isFalse( Microformats.isMicroformat( node ) );
+
+ });
+
+
+ it('false - no class', function(){
+
+ var doc,
+ node;
+
+ var html = '<span>Glenn</span>';
+
+ doc = document.implementation.createHTMLDocument('New Document');
+ node = document.createElement('div');
+ doc.body.appendChild( node );
+ node.innerHTML = html;
+ node = doc.querySelector( 'span' );
+
+ assert.isFalse( Microformats.isMicroformat( node ) );
+
+ });
+
+
+ it('false - no node', function(){
+ assert.isFalse( Microformats.isMicroformat( ) );
+ });
+
+
+ it('false - undefined node', function(){
+ assert.isFalse( Microformats.isMicroformat( undefined ) );
+ });
+
+ });
diff --git a/toolkit/components/microformats/test/lib/dates.js b/toolkit/components/microformats/test/lib/dates.js
new file mode 100644
index 0000000000..6d6129b083
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/dates.js
@@ -0,0 +1,268 @@
+/*!
+ dates
+ These functions are based on microformats implied rules for parsing date fragments from text.
+ They are not generalist date utilities and should only be used with the isodate.js module of this library.
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies utilities.js, isodate.js
+*/
+
+
+var Modules = (function (modules) {
+
+ modules.dates = {
+
+
+ /**
+ * does text contain am
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ hasAM: function( text ) {
+ text = text.toLowerCase();
+ return(text.indexOf('am') > -1 || text.indexOf('a.m.') > -1);
+ },
+
+
+ /**
+ * does text contain pm
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ hasPM: function( text ) {
+ text = text.toLowerCase();
+ return(text.indexOf('pm') > -1 || text.indexOf('p.m.') > -1);
+ },
+
+
+ /**
+ * remove am and pm from text and return it
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ removeAMPM: function( text ) {
+ return text.replace('pm', '').replace('p.m.', '').replace('am', '').replace('a.m.', '');
+ },
+
+
+ /**
+ * simple test of whether ISO date string is a duration i.e. PY17M or PW12
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ isDuration: function( text ) {
+ if(modules.utils.isString( text )){
+ text = text.toLowerCase();
+ if(modules.utils.startWith(text, 'p') ){
+ return true;
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * is text a time or timezone
+ * i.e. HH-MM-SS or z+-HH-MM-SS 08:43 | 15:23:00:0567 | 10:34pm | 10:34 p.m. | +01:00:00 | -02:00 | z15:00 | 0843
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ isTime: function( text ) {
+ if(modules.utils.isString(text)){
+ text = text.toLowerCase();
+ text = modules.utils.trim( text );
+ // start with timezone char
+ if( text.match(':') && ( modules.utils.startWith(text, 'z') || modules.utils.startWith(text, '-') || modules.utils.startWith(text, '+') )) {
+ return true;
+ }
+ // has ante meridiem or post meridiem
+ if( text.match(/^[0-9]/) &&
+ ( this.hasAM(text) || this.hasPM(text) )) {
+ return true;
+ }
+ // contains time delimiter but not datetime delimiter
+ if( text.match(':') && !text.match(/t|\s/) ) {
+ return true;
+ }
+
+ // if it's a number of 2, 4 or 6 chars
+ if(modules.utils.isNumber(text)){
+ if(text.length === 2 || text.length === 4 || text.length === 6){
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * parses a time from text and returns 24hr time string
+ * i.e. 5:34am = 05:34:00 and 1:52:04p.m. = 13:52:04
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ parseAmPmTime: function( text ) {
+ var out = text,
+ times = [];
+
+ // if the string has a text : or am or pm
+ if(modules.utils.isString(out)) {
+ //text = text.toLowerCase();
+ text = text.replace(/[ ]+/g, '');
+
+ if(text.match(':') || this.hasAM(text) || this.hasPM(text)) {
+
+ if(text.match(':')) {
+ times = text.split(':');
+ } else {
+ // single number text i.e. 5pm
+ times[0] = text;
+ times[0] = this.removeAMPM(times[0]);
+ }
+
+ // change pm hours to 24hr number
+ if(this.hasPM(text)) {
+ if(times[0] < 12) {
+ times[0] = parseInt(times[0], 10) + 12;
+ }
+ }
+
+ // add leading zero's where needed
+ if(times[0] && times[0].length === 1) {
+ times[0] = '0' + times[0];
+ }
+
+ // rejoin text elements together
+ if(times[0]) {
+ text = times.join(':');
+ }
+ }
+ }
+
+ // remove am/pm strings
+ return this.removeAMPM(text);
+ },
+
+
+ /**
+ * overlays a time on a date to return the union of the two
+ *
+ * @param {String} date
+ * @param {String} time
+ * @param {String} format ( Modules.ISODate profile format )
+ * @return {Object} Modules.ISODate
+ */
+ dateTimeUnion: function(date, time, format) {
+ var isodate = new modules.ISODate(date, format),
+ isotime = new modules.ISODate();
+
+ isotime.parseTime(this.parseAmPmTime(time), format);
+ if(isodate.hasFullDate() && isotime.hasTime()) {
+ isodate.tH = isotime.tH;
+ isodate.tM = isotime.tM;
+ isodate.tS = isotime.tS;
+ isodate.tD = isotime.tD;
+ return isodate;
+ } else {
+ if(isodate.hasFullDate()){
+ return isodate;
+ }
+ return new modules.ISODate();
+ }
+ },
+
+
+ /**
+ * concatenate an array of date and time text fragments to create an ISODate object
+ * used for microformat value and value-title rules
+ *
+ * @param {Array} arr ( Array of Strings )
+ * @param {String} format ( Modules.ISODate profile format )
+ * @return {Object} Modules.ISODate
+ */
+ concatFragments: function (arr, format) {
+ var out = new modules.ISODate(),
+ i = 0,
+ value = '';
+
+ // if the fragment already contains a full date just return it once
+ if(arr[0].toUpperCase().match('T')) {
+ return new modules.ISODate(arr[0], format);
+ }else{
+ for(i = 0; i < arr.length; i++) {
+ value = arr[i];
+
+ // date pattern
+ if( value.charAt(4) === '-' && out.hasFullDate() === false ){
+ out.parseDate(value);
+ }
+
+ // time pattern
+ if( (value.indexOf(':') > -1 || modules.utils.isNumber( this.parseAmPmTime(value) )) && out.hasTime() === false ) {
+ // split time and timezone
+ var items = this.splitTimeAndZone(value);
+ value = items[0];
+
+ // parse any use of am/pm
+ value = this.parseAmPmTime(value);
+ out.parseTime(value);
+
+ // parse any timezone
+ if(items.length > 1){
+ out.parseTimeZone(items[1], format);
+ }
+ }
+
+ // timezone pattern
+ if(value.charAt(0) === '-' || value.charAt(0) === '+' || value.toUpperCase() === 'Z') {
+ if( out.hasTimeZone() === false ){
+ out.parseTimeZone(value);
+ }
+ }
+
+ }
+ return out;
+
+ }
+ },
+
+
+ /**
+ * parses text by splitting it into an array of time and timezone strings
+ *
+ * @param {String} text
+ * @return {Array} Modules.ISODate
+ */
+ splitTimeAndZone: function ( text ){
+ var out = [text],
+ chars = ['-','+','z','Z'],
+ i = chars.length;
+
+ while (i--) {
+ if(text.indexOf(chars[i]) > -1){
+ out[0] = text.slice( 0, text.indexOf(chars[i]) );
+ out.push( text.slice( text.indexOf(chars[i]) ) );
+ break;
+ }
+ }
+ return out;
+ }
+
+ };
+
+
+ return modules;
+
+} (Modules || {}));
+
+
+
+
diff --git a/toolkit/components/microformats/test/lib/domparser.js b/toolkit/components/microformats/test/lib/domparser.js
new file mode 100644
index 0000000000..bc342634ac
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/domparser.js
@@ -0,0 +1,103 @@
+
+// Based on https://gist.github.com/1129031 By Eli Grey, http://eligrey.com - Public domain.
+
+// DO NOT use https://developer.mozilla.org/en-US/docs/Web/API/DOMParser example polyfill
+// as it does not work with earlier versions of Chrome
+
+
+(function(DOMParser) {
+ 'use strict';
+
+ var DOMParser_proto;
+ var real_parseFromString;
+ var textHTML; // Flag for text/html support
+ var textXML; // Flag for text/xml support
+ var htmlElInnerHTML; // Flag for support for setting html element's innerHTML
+
+ // Stop here if DOMParser not defined
+ if (!DOMParser) {
+ return;
+ }
+
+ // Firefox, Opera and IE throw errors on unsupported types
+ try {
+ // WebKit returns null on unsupported types
+ textHTML = !!(new DOMParser()).parseFromString('', 'text/html');
+
+ } catch (er) {
+ textHTML = false;
+ }
+
+ // If text/html supported, don't need to do anything.
+ if (textHTML) {
+ return;
+ }
+
+ // Next try setting innerHTML of a created document
+ // IE 9 and lower will throw an error (can't set innerHTML of its HTML element)
+ try {
+ var doc = document.implementation.createHTMLDocument('');
+ doc.documentElement.innerHTML = '<title></title><div></div>';
+ htmlElInnerHTML = true;
+
+ } catch (er) {
+ htmlElInnerHTML = false;
+ }
+
+ // If if that failed, try text/xml
+ if (!htmlElInnerHTML) {
+
+ try {
+ textXML = !!(new DOMParser()).parseFromString('', 'text/xml');
+
+ } catch (er) {
+ textHTML = false;
+ }
+ }
+
+ // Mess with DOMParser.prototype (less than optimal...) if one of the above worked
+ // Assume can write to the prototype, if not, make this a stand alone function
+ if (DOMParser.prototype && (htmlElInnerHTML || textXML)) {
+ DOMParser_proto = DOMParser.prototype;
+ real_parseFromString = DOMParser_proto.parseFromString;
+
+ DOMParser_proto.parseFromString = function (markup, type) {
+
+ // Only do this if type is text/html
+ if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
+ var doc, doc_el, first_el;
+
+ // Use innerHTML if supported
+ if (htmlElInnerHTML) {
+ doc = document.implementation.createHTMLDocument('');
+ doc_el = doc.documentElement;
+ doc_el.innerHTML = markup;
+ first_el = doc_el.firstElementChild;
+
+ // Otherwise use XML method
+ } else if (textXML) {
+
+ // Make sure markup is wrapped in HTML tags
+ // Should probably allow for a DOCTYPE
+ if (!(/^<html.*html>$/i.test(markup))) {
+ markup = '<html>' + markup + '<\/html>';
+ }
+ doc = (new DOMParser()).parseFromString(markup, 'text/xml');
+ doc_el = doc.documentElement;
+ first_el = doc_el.firstElementChild;
+ }
+
+ // Is this an entire document or a fragment?
+ if (doc_el.childElementCount === 1 && first_el.localName.toLowerCase() === 'html') {
+ doc.replaceChild(first_el, doc_el);
+ }
+
+ return doc;
+
+ // If not text/html, send as-is to host method
+ } else {
+ return real_parseFromString.apply(this, arguments);
+ }
+ };
+ }
+}(DOMParser));
diff --git a/toolkit/components/microformats/test/lib/domutils.js b/toolkit/components/microformats/test/lib/domutils.js
new file mode 100644
index 0000000000..57269de978
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/domutils.js
@@ -0,0 +1,611 @@
+/*
+ dom utilities
+ The purpose of this module is to abstract DOM functions away from the main parsing modules of the library.
+ It was created so the file can be replaced in node.js environment to make use of different types of light weight node.js DOM's
+ such as 'cherrio.js'. It also contains a number of DOM utilities which are used throughout the parser such as: 'getDescendant'
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies utilities.js
+
+*/
+
+
+var Modules = (function (modules) {
+
+ modules.domUtils = {
+
+ // blank objects for DOM
+ document: null,
+ rootNode: null,
+
+
+ /**
+ * gets DOMParser object
+ *
+ * @return {Object || undefined}
+ */
+ getDOMParser: function () {
+ if (typeof DOMParser === undefined) {
+ try {
+ return Components.classes["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Components.interfaces.nsIDOMParser);
+ } catch (e) {
+ return undefined;
+ }
+ } else {
+ return new DOMParser();
+ }
+ },
+
+
+ /**
+ * configures what are the base DOM objects for parsing
+ *
+ * @param {Object} options
+ * @return {DOM Node} node
+ */
+ getDOMContext: function( options ){
+
+ // if a node is passed
+ if(options.node){
+ this.rootNode = options.node;
+ }
+
+
+ // if a html string is passed
+ if(options.html){
+ //var domParser = new DOMParser();
+ var domParser = this.getDOMParser();
+ this.rootNode = domParser.parseFromString( options.html, 'text/html' );
+ }
+
+
+ // find top level document from rootnode
+ if(this.rootNode !== null){
+ if(this.rootNode.nodeType === 9){
+ this.document = this.rootNode;
+ this.rootNode = modules.domUtils.querySelector(this.rootNode, 'html');
+ }else{
+ // if it's DOM node get parent DOM Document
+ this.document = modules.domUtils.ownerDocument(this.rootNode);
+ }
+ }
+
+
+ // use global document object
+ if(!this.rootNode && document){
+ this.rootNode = modules.domUtils.querySelector(document, 'html');
+ this.document = document;
+ }
+
+
+ if(this.rootNode && this.document){
+ return {document: this.document, rootNode: this.rootNode};
+ }
+
+ return {document: null, rootNode: null};
+ },
+
+
+
+ /**
+ * gets the first DOM node
+ *
+ * @param {Dom Document}
+ * @return {DOM Node} node
+ */
+ getTopMostNode: function( node ){
+ //var doc = this.ownerDocument(node);
+ //if(doc && doc.nodeType && doc.nodeType === 9 && doc.documentElement){
+ // return doc.documentElement;
+ //}
+ return node;
+ },
+
+
+
+ /**
+ * abstracts DOM ownerDocument
+ *
+ * @param {DOM Node} node
+ * @return {Dom Document}
+ */
+ ownerDocument: function(node){
+ return node.ownerDocument;
+ },
+
+
+ /**
+ * abstracts DOM textContent
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ textContent: function(node){
+ if(node.textContent){
+ return node.textContent;
+ }else if(node.innerText){
+ return node.innerText;
+ }
+ return '';
+ },
+
+
+ /**
+ * abstracts DOM innerHTML
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ innerHTML: function(node){
+ return node.innerHTML;
+ },
+
+
+ /**
+ * abstracts DOM hasAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {Boolean}
+ */
+ hasAttribute: function(node, attributeName) {
+ return node.hasAttribute(attributeName);
+ },
+
+
+ /**
+ * does an attribute contain a value
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @param {String} value
+ * @return {Boolean}
+ */
+ hasAttributeValue: function(node, attributeName, value) {
+ return (this.getAttributeList(node, attributeName).indexOf(value) > -1);
+ },
+
+
+ /**
+ * abstracts DOM getAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {String || null}
+ */
+ getAttribute: function(node, attributeName) {
+ return node.getAttribute(attributeName);
+ },
+
+
+ /**
+ * abstracts DOM setAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @param {String} attributeValue
+ */
+ setAttribute: function(node, attributeName, attributeValue){
+ node.setAttribute(attributeName, attributeValue);
+ },
+
+
+ /**
+ * abstracts DOM removeAttribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ */
+ removeAttribute: function(node, attributeName) {
+ node.removeAttribute(attributeName);
+ },
+
+
+ /**
+ * abstracts DOM getElementById
+ *
+ * @param {DOM Node || DOM Document} node
+ * @param {String} id
+ * @return {DOM Node}
+ */
+ getElementById: function(docNode, id) {
+ return docNode.querySelector( '#' + id );
+ },
+
+
+ /**
+ * abstracts DOM querySelector
+ *
+ * @param {DOM Node || DOM Document} node
+ * @param {String} selector
+ * @return {DOM Node}
+ */
+ querySelector: function(docNode, selector) {
+ return docNode.querySelector( selector );
+ },
+
+
+ /**
+ * get value of a Node attribute as an array
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {Array}
+ */
+ getAttributeList: function(node, attributeName) {
+ var out = [],
+ attList;
+
+ attList = node.getAttribute(attributeName);
+ if(attList && attList !== '') {
+ if(attList.indexOf(' ') > -1) {
+ out = attList.split(' ');
+ } else {
+ out.push(attList);
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * gets all child nodes with a given attribute
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {NodeList}
+ */
+ getNodesByAttribute: function(node, attributeName) {
+ var selector = '[' + attributeName + ']';
+ return node.querySelectorAll(selector);
+ },
+
+
+ /**
+ * gets all child nodes with a given attribute containing a given value
+ *
+ * @param {DOM Node} node
+ * @param {String} attributeName
+ * @return {DOM NodeList}
+ */
+ getNodesByAttributeValue: function(rootNode, name, value) {
+ var arr = [],
+ x = 0,
+ i,
+ out = [];
+
+ arr = this.getNodesByAttribute(rootNode, name);
+ if(arr) {
+ i = arr.length;
+ while(x < i) {
+ if(this.hasAttributeValue(arr[x], name, value)) {
+ out.push(arr[x]);
+ }
+ x++;
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * gets attribute value from controlled list of tags
+ *
+ * @param {Array} tagNames
+ * @param {String} attributeName
+ * @return {String || null}
+ */
+ getAttrValFromTagList: function(node, tagNames, attributeName) {
+ var i = tagNames.length;
+
+ while(i--) {
+ if(node.tagName.toLowerCase() === tagNames[i]) {
+ var attrValue = this.getAttribute(node, attributeName);
+ if(attrValue && attrValue !== '') {
+ return attrValue;
+ }
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * get node if it has no siblings. CSS equivalent is :only-child
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {DOM Node || null}
+ */
+ getSingleDescendant: function(node){
+ return this.getDescendant( node, null, false );
+ },
+
+
+ /**
+ * get node if it has no siblings of the same type. CSS equivalent is :only-of-type
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {DOM Node || null}
+ */
+ getSingleDescendantOfType: function(node, tagNames){
+ return this.getDescendant( node, tagNames, true );
+ },
+
+
+ /**
+ * get child node limited by presence of siblings - either CSS :only-of-type or :only-child
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {DOM Node || null}
+ */
+ getDescendant: function( node, tagNames, onlyOfType ){
+ var i = node.children.length,
+ countAll = 0,
+ countOfType = 0,
+ child,
+ out = null;
+
+ while(i--) {
+ child = node.children[i];
+ if(child.nodeType === 1) {
+ if(tagNames){
+ // count just only-of-type
+ if(this.hasTagName(child, tagNames)){
+ out = child;
+ countOfType++;
+ }
+ }else{
+ // count all elements
+ out = child;
+ countAll++;
+ }
+ }
+ }
+ if(onlyOfType === true){
+ return (countOfType === 1)? out : null;
+ }else{
+ return (countAll === 1)? out : null;
+ }
+ },
+
+
+ /**
+ * is a node one of a list of tags
+ *
+ * @param {DOM Node} rootNode
+ * @param {Array} tagNames
+ * @return {Boolean}
+ */
+ hasTagName: function(node, tagNames){
+ var i = tagNames.length;
+ while(i--) {
+ if(node.tagName.toLowerCase() === tagNames[i]) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * abstracts DOM appendChild
+ *
+ * @param {DOM Node} node
+ * @param {DOM Node} childNode
+ * @return {DOM Node}
+ */
+ appendChild: function(node, childNode){
+ return node.appendChild(childNode);
+ },
+
+
+ /**
+ * abstracts DOM removeChild
+ *
+ * @param {DOM Node} childNode
+ * @return {DOM Node || null}
+ */
+ removeChild: function(childNode){
+ if (childNode.parentNode) {
+ return childNode.parentNode.removeChild(childNode);
+ }else{
+ return null;
+ }
+ },
+
+
+ /**
+ * abstracts DOM cloneNode
+ *
+ * @param {DOM Node} node
+ * @return {DOM Node}
+ */
+ clone: function(node) {
+ var newNode = node.cloneNode(true);
+ newNode.removeAttribute('id');
+ return newNode;
+ },
+
+
+ /**
+ * gets the text of a node
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ getElementText: function( node ){
+ if(node && node.data){
+ return node.data;
+ }else{
+ return '';
+ }
+ },
+
+
+ /**
+ * gets the attributes of a node - ordered by sequence in html
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ getOrderedAttributes: function( node ){
+ var nodeStr = node.outerHTML,
+ attrs = [];
+
+ for (var i = 0; i < node.attributes.length; i++) {
+ var attr = node.attributes[i];
+ attr.indexNum = nodeStr.indexOf(attr.name);
+
+ attrs.push( attr );
+ }
+ return attrs.sort( modules.utils.sortObjects( 'indexNum' ) );
+ },
+
+
+ /**
+ * decodes html entities in given text
+ *
+ * @param {DOM Document} doc
+ * @param String} text
+ * @return {String}
+ */
+ decodeEntities: function( doc, text ){
+ //return text;
+ return doc.createTextNode( text ).nodeValue;
+ },
+
+
+ /**
+ * clones a DOM document
+ *
+ * @param {DOM Document} document
+ * @return {DOM Document}
+ */
+ cloneDocument: function( document ){
+ var newNode,
+ newDocument = null;
+
+ if( this.canCloneDocument( document )){
+ newDocument = document.implementation.createHTMLDocument('');
+ newNode = newDocument.importNode( document.documentElement, true );
+ newDocument.replaceChild(newNode, newDocument.querySelector('html'));
+ }
+ return (newNode && newNode.nodeType && newNode.nodeType === 1)? newDocument : document;
+ },
+
+
+ /**
+ * can environment clone a DOM document
+ *
+ * @param {DOM Document} document
+ * @return {Boolean}
+ */
+ canCloneDocument: function( document ){
+ return (document && document.importNode && document.implementation && document.implementation.createHTMLDocument);
+ },
+
+
+ /**
+ * get the child index of a node. Used to create a node path
+ *
+ * @param {DOM Node} node
+ * @return {Int}
+ */
+ getChildIndex: function (node) {
+ var parent = node.parentNode,
+ i = -1,
+ child;
+ while (parent && (child = parent.childNodes[++i])){
+ if (child === node){
+ return i;
+ }
+ }
+ return -1;
+ },
+
+
+ /**
+ * get a node's path
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ getNodePath: function (node) {
+ var parent = node.parentNode,
+ path = [],
+ index = this.getChildIndex(node);
+
+ if(parent && (path = this.getNodePath(parent))){
+ if(index > -1){
+ path.push(index);
+ }
+ }
+ return path;
+ },
+
+
+ /**
+ * get a node from a path.
+ *
+ * @param {DOM document} document
+ * @param {Array} path
+ * @return {DOM Node}
+ */
+ getNodeByPath: function (document, path) {
+ var node = document.documentElement,
+ i = 0,
+ index;
+ while ((index = path[++i]) > -1){
+ node = node.childNodes[index];
+ }
+ return node;
+ },
+
+
+ /**
+ * get an array/nodeList of child nodes
+ *
+ * @param {DOM node} node
+ * @return {Array}
+ */
+ getChildren: function( node ){
+ return node.children;
+ },
+
+
+ /**
+ * create a node
+ *
+ * @param {String} tagName
+ * @return {DOM node}
+ */
+ createNode: function( tagName ){
+ return this.document.createElement(tagName);
+ },
+
+
+ /**
+ * create a node with text content
+ *
+ * @param {String} tagName
+ * @param {String} text
+ * @return {DOM node}
+ */
+ createNodeWithText: function( tagName, text ){
+ var node = this.document.createElement(tagName);
+ node.innerHTML = text;
+ return node;
+ }
+
+
+
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/html.js b/toolkit/components/microformats/test/lib/html.js
new file mode 100644
index 0000000000..ab150d91e3
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/html.js
@@ -0,0 +1,107 @@
+/*
+ html
+ Extracts a HTML string from DOM nodes. Was created to get around the issue of not being able to exclude the content
+ of nodes with the 'data-include' attribute. DO NOT replace with functions such as innerHTML as it will break a
+ number of microformat include patterns.
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-node/master/license.txt
+ Dependencies utilities.js, domutils.js
+*/
+
+
+var Modules = (function (modules) {
+
+ modules.html = {
+
+ // elements which are self-closing
+ selfClosingElt: ['area', 'base', 'br', 'col', 'hr', 'img', 'input', 'link', 'meta', 'param', 'command', 'keygen', 'source'],
+
+
+ /**
+ * parse the html string from DOM Node
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ parse: function( node ){
+ var out = '',
+ j = 0;
+
+ // we do not want the outer container
+ if(node.childNodes && node.childNodes.length > 0){
+ for (j = 0; j < node.childNodes.length; j++) {
+ var text = this.walkTreeForHtml( node.childNodes[j] );
+ if(text !== undefined){
+ out += text;
+ }
+ }
+ }
+
+ return out;
+ },
+
+
+ /**
+ * walks the DOM tree parsing the html string from the nodes
+ *
+ * @param {DOM Document} doc
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ walkTreeForHtml: function( node ) {
+ var out = '',
+ j = 0;
+
+ // if node is a text node get its text
+ if(node.nodeType && node.nodeType === 3){
+ out += modules.domUtils.getElementText( node );
+ }
+
+
+ // exclude text which has been added with include pattern -
+ if(node.nodeType && node.nodeType === 1 && modules.domUtils.hasAttribute(node, 'data-include') === false){
+
+ // begin tag
+ out += '<' + node.tagName.toLowerCase();
+
+ // add attributes
+ var attrs = modules.domUtils.getOrderedAttributes(node);
+ for (j = 0; j < attrs.length; j++) {
+ out += ' ' + attrs[j].name + '=' + '"' + attrs[j].value + '"';
+ }
+
+ if(this.selfClosingElt.indexOf(node.tagName.toLowerCase()) === -1){
+ out += '>';
+ }
+
+ // get the text of the child nodes
+ if(node.childNodes && node.childNodes.length > 0){
+
+ for (j = 0; j < node.childNodes.length; j++) {
+ var text = this.walkTreeForHtml( node.childNodes[j] );
+ if(text !== undefined){
+ out += text;
+ }
+ }
+ }
+
+ // end tag
+ if(this.selfClosingElt.indexOf(node.tagName.toLowerCase()) > -1){
+ out += ' />';
+ }else{
+ out += '</' + node.tagName.toLowerCase() + '>';
+ }
+ }
+
+ return (out === '')? undefined : out;
+ }
+
+
+ };
+
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/isodate.js b/toolkit/components/microformats/test/lib/isodate.js
new file mode 100644
index 0000000000..30f35f35d4
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/isodate.js
@@ -0,0 +1,481 @@
+/*!
+ iso date
+ This module was built for the exact needs of parsing ISO dates to the microformats standard.
+
+ * Parses and builds ISO dates to the W3C note, HTML5 or RFC3339 profiles.
+ * Also allows for profile detection using 'auto'
+ * Outputs to the same level of specificity of date and time that was input
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ */
+
+
+
+var Modules = (function (modules) {
+
+
+ /**
+ * constructor
+ * parses text to find just the date element of an ISO date/time string i.e. 2008-05-01
+ *
+ * @param {String} dateString
+ * @param {String} format
+ * @return {String}
+ */
+ modules.ISODate = function ( dateString, format ) {
+ this.clear();
+
+ this.format = (format)? format : 'auto'; // auto or W3C or RFC3339 or HTML5
+ this.setFormatSep();
+
+ // optional should be full iso date/time string
+ if(arguments[0]) {
+ this.parse(dateString, format);
+ }
+ };
+
+
+ modules.ISODate.prototype = {
+
+
+ /**
+ * clear all states
+ *
+ */
+ clear: function(){
+ this.clearDate();
+ this.clearTime();
+ this.clearTimeZone();
+ this.setAutoProfileState();
+ },
+
+
+ /**
+ * clear date states
+ *
+ */
+ clearDate: function(){
+ this.dY = -1;
+ this.dM = -1;
+ this.dD = -1;
+ this.dDDD = -1;
+ },
+
+
+ /**
+ * clear time states
+ *
+ */
+ clearTime: function(){
+ this.tH = -1;
+ this.tM = -1;
+ this.tS = -1;
+ this.tD = -1;
+ },
+
+
+ /**
+ * clear timezone states
+ *
+ */
+ clearTimeZone: function(){
+ this.tzH = -1;
+ this.tzM = -1;
+ this.tzPN = '+';
+ this.z = false;
+ },
+
+
+ /**
+ * resets the auto profile state
+ *
+ */
+ setAutoProfileState: function(){
+ this.autoProfile = {
+ sep: 'T',
+ dsep: '-',
+ tsep: ':',
+ tzsep: ':',
+ tzZulu: 'Z'
+ };
+ },
+
+
+ /**
+ * parses text to find ISO date/time string i.e. 2008-05-01T15:45:19Z
+ *
+ * @param {String} dateString
+ * @param {String} format
+ * @return {String}
+ */
+ parse: function( dateString, format ) {
+ this.clear();
+
+ var parts = [],
+ tzArray = [],
+ position = 0,
+ datePart = '',
+ timePart = '',
+ timeZonePart = '';
+
+ if(format){
+ this.format = format;
+ }
+
+
+
+ // discover date time separtor for auto profile
+ // Set to 'T' by default
+ if(dateString.indexOf('t') > -1) {
+ this.autoProfile.sep = 't';
+ }
+ if(dateString.indexOf('z') > -1) {
+ this.autoProfile.tzZulu = 'z';
+ }
+ if(dateString.indexOf('Z') > -1) {
+ this.autoProfile.tzZulu = 'Z';
+ }
+ if(dateString.toUpperCase().indexOf('T') === -1) {
+ this.autoProfile.sep = ' ';
+ }
+
+
+ dateString = dateString.toUpperCase().replace(' ','T');
+
+ // break on 'T' divider or space
+ if(dateString.indexOf('T') > -1) {
+ parts = dateString.split('T');
+ datePart = parts[0];
+ timePart = parts[1];
+
+ // zulu UTC
+ if(timePart.indexOf( 'Z' ) > -1) {
+ this.z = true;
+ }
+
+ // timezone
+ if(timePart.indexOf( '+' ) > -1 || timePart.indexOf( '-' ) > -1) {
+ tzArray = timePart.split( 'Z' ); // incase of incorrect use of Z
+ timePart = tzArray[0];
+ timeZonePart = tzArray[1];
+
+ // timezone
+ if(timePart.indexOf( '+' ) > -1 || timePart.indexOf( '-' ) > -1) {
+ position = 0;
+
+ if(timePart.indexOf( '+' ) > -1) {
+ position = timePart.indexOf( '+' );
+ } else {
+ position = timePart.indexOf( '-' );
+ }
+
+ timeZonePart = timePart.substring( position, timePart.length );
+ timePart = timePart.substring( 0, position );
+ }
+ }
+
+ } else {
+ datePart = dateString;
+ }
+
+ if(datePart !== '') {
+ this.parseDate( datePart );
+ if(timePart !== '') {
+ this.parseTime( timePart );
+ if(timeZonePart !== '') {
+ this.parseTimeZone( timeZonePart );
+ }
+ }
+ }
+ return this.toString( format );
+ },
+
+
+ /**
+ * parses text to find just the date element of an ISO date/time string i.e. 2008-05-01
+ *
+ * @param {String} dateString
+ * @param {String} format
+ * @return {String}
+ */
+ parseDate: function( dateString, format ) {
+ this.clearDate();
+
+ var parts = [];
+
+ // discover timezone separtor for auto profile // default is ':'
+ if(dateString.indexOf('-') === -1) {
+ this.autoProfile.tsep = '';
+ }
+
+ // YYYY-DDD
+ parts = dateString.match( /(\d\d\d\d)-(\d\d\d)/ );
+ if(parts) {
+ if(parts[1]) {
+ this.dY = parts[1];
+ }
+ if(parts[2]) {
+ this.dDDD = parts[2];
+ }
+ }
+
+ if(this.dDDD === -1) {
+ // YYYY-MM-DD ie 2008-05-01 and YYYYMMDD ie 20080501
+ parts = dateString.match( /(\d\d\d\d)?-?(\d\d)?-?(\d\d)?/ );
+ if(parts[1]) {
+ this.dY = parts[1];
+ }
+ if(parts[2]) {
+ this.dM = parts[2];
+ }
+ if(parts[3]) {
+ this.dD = parts[3];
+ }
+ }
+ return this.toString(format);
+ },
+
+
+ /**
+ * parses text to find just the time element of an ISO date/time string i.e. 13:30:45
+ *
+ * @param {String} timeString
+ * @param {String} format
+ * @return {String}
+ */
+ parseTime: function( timeString, format ) {
+ this.clearTime();
+ var parts = [];
+
+ // discover date separtor for auto profile // default is ':'
+ if(timeString.indexOf(':') === -1) {
+ this.autoProfile.tsep = '';
+ }
+
+ // finds timezone HH:MM:SS and HHMMSS ie 13:30:45, 133045 and 13:30:45.0135
+ parts = timeString.match( /(\d\d)?:?(\d\d)?:?(\d\d)?.?([0-9]+)?/ );
+ if(parts[1]) {
+ this.tH = parts[1];
+ }
+ if(parts[2]) {
+ this.tM = parts[2];
+ }
+ if(parts[3]) {
+ this.tS = parts[3];
+ }
+ if(parts[4]) {
+ this.tD = parts[4];
+ }
+ return this.toTimeString(format);
+ },
+
+
+ /**
+ * parses text to find just the time element of an ISO date/time string i.e. +08:00
+ *
+ * @param {String} timeString
+ * @param {String} format
+ * @return {String}
+ */
+ parseTimeZone: function( timeString, format ) {
+ this.clearTimeZone();
+ var parts = [];
+
+ if(timeString.toLowerCase() === 'z'){
+ this.z = true;
+ // set case for z
+ this.autoProfile.tzZulu = (timeString === 'z')? 'z' : 'Z';
+ }else{
+
+ // discover timezone separtor for auto profile // default is ':'
+ if(timeString.indexOf(':') === -1) {
+ this.autoProfile.tzsep = '';
+ }
+
+ // finds timezone +HH:MM and +HHMM ie +13:30 and +1330
+ parts = timeString.match( /([\-\+]{1})?(\d\d)?:?(\d\d)?/ );
+ if(parts[1]) {
+ this.tzPN = parts[1];
+ }
+ if(parts[2]) {
+ this.tzH = parts[2];
+ }
+ if(parts[3]) {
+ this.tzM = parts[3];
+ }
+
+
+ }
+ this.tzZulu = 'z';
+ return this.toTimeString( format );
+ },
+
+
+ /**
+ * returns ISO date/time string in W3C Note, RFC 3339, HTML5, or auto profile
+ *
+ * @param {String} format
+ * @return {String}
+ */
+ toString: function( format ) {
+ var output = '';
+
+ if(format){
+ this.format = format;
+ }
+ this.setFormatSep();
+
+ if(this.dY > -1) {
+ output = this.dY;
+ if(this.dM > 0 && this.dM < 13) {
+ output += this.dsep + this.dM;
+ if(this.dD > 0 && this.dD < 32) {
+ output += this.dsep + this.dD;
+ if(this.tH > -1 && this.tH < 25) {
+ output += this.sep + this.toTimeString( format );
+ }
+ }
+ }
+ if(this.dDDD > -1) {
+ output += this.dsep + this.dDDD;
+ }
+ } else if(this.tH > -1) {
+ output += this.toTimeString( format );
+ }
+
+ return output;
+ },
+
+
+ /**
+ * returns just the time string element of an ISO date/time
+ * in W3C Note, RFC 3339, HTML5, or auto profile
+ *
+ * @param {String} format
+ * @return {String}
+ */
+ toTimeString: function( format ) {
+ var out = '';
+
+ if(format){
+ this.format = format;
+ }
+ this.setFormatSep();
+
+ // time can only be created with a full date
+ if(this.tH) {
+ if(this.tH > -1 && this.tH < 25) {
+ out += this.tH;
+ if(this.tM > -1 && this.tM < 61){
+ out += this.tsep + this.tM;
+ if(this.tS > -1 && this.tS < 61){
+ out += this.tsep + this.tS;
+ if(this.tD > -1){
+ out += '.' + this.tD;
+ }
+ }
+ }
+
+
+
+ // time zone offset
+ if(this.z) {
+ out += this.tzZulu;
+ } else {
+ if(this.tzH && this.tzH > -1 && this.tzH < 25) {
+ out += this.tzPN + this.tzH;
+ if(this.tzM > -1 && this.tzM < 61){
+ out += this.tzsep + this.tzM;
+ }
+ }
+ }
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * set the current profile to W3C Note, RFC 3339, HTML5, or auto profile
+ *
+ */
+ setFormatSep: function() {
+ switch( this.format.toLowerCase() ) {
+ case 'rfc3339':
+ this.sep = 'T';
+ this.dsep = '';
+ this.tsep = '';
+ this.tzsep = '';
+ this.tzZulu = 'Z';
+ break;
+ case 'w3c':
+ this.sep = 'T';
+ this.dsep = '-';
+ this.tsep = ':';
+ this.tzsep = ':';
+ this.tzZulu = 'Z';
+ break;
+ case 'html5':
+ this.sep = ' ';
+ this.dsep = '-';
+ this.tsep = ':';
+ this.tzsep = ':';
+ this.tzZulu = 'Z';
+ break;
+ default:
+ // auto - defined by format of input string
+ this.sep = this.autoProfile.sep;
+ this.dsep = this.autoProfile.dsep;
+ this.tsep = this.autoProfile.tsep;
+ this.tzsep = this.autoProfile.tzsep;
+ this.tzZulu = this.autoProfile.tzZulu;
+ }
+ },
+
+
+ /**
+ * does current data contain a full date i.e. 2015-03-23
+ *
+ * @return {Boolean}
+ */
+ hasFullDate: function() {
+ return(this.dY !== -1 && this.dM !== -1 && this.dD !== -1);
+ },
+
+
+ /**
+ * does current data contain a minimum date which is just a year number i.e. 2015
+ *
+ * @return {Boolean}
+ */
+ hasDate: function() {
+ return(this.dY !== -1);
+ },
+
+
+ /**
+ * does current data contain a minimum time which is just a hour number i.e. 13
+ *
+ * @return {Boolean}
+ */
+ hasTime: function() {
+ return(this.tH !== -1);
+ },
+
+ /**
+ * does current data contain a minimum timezone i.e. -1 || +1 || z
+ *
+ * @return {Boolean}
+ */
+ hasTimeZone: function() {
+ return(this.tzH !== -1);
+ }
+
+ };
+
+ modules.ISODate.prototype.constructor = modules.ISODate;
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/living-standard.js b/toolkit/components/microformats/test/lib/living-standard.js
new file mode 100644
index 0000000000..e2b0635733
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/living-standard.js
@@ -0,0 +1 @@
+ modules.livingStandard = '2015-09-25T12:26:04Z';
diff --git a/toolkit/components/microformats/test/lib/maps/h-adr.js b/toolkit/components/microformats/test/lib/maps/h-adr.js
new file mode 100644
index 0000000000..aa3a695c53
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-adr.js
@@ -0,0 +1,29 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-adr'] = {
+ root: 'adr',
+ name: 'h-adr',
+ properties: {
+ 'post-office-box': {},
+ 'street-address': {},
+ 'extended-address': {},
+ 'locality': {},
+ 'region': {},
+ 'postal-code': {},
+ 'country-name': {}
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
+
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-card.js b/toolkit/components/microformats/test/lib/maps/h-card.js
new file mode 100644
index 0000000000..124750a376
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-card.js
@@ -0,0 +1,85 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-card'] = {
+ root: 'vcard',
+ name: 'h-card',
+ properties: {
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'adr': {
+ 'map': 'p-adr',
+ 'uf': ['h-adr']
+ },
+ 'agent': {
+ 'uf': ['h-card']
+ },
+ 'bday': {
+ 'map': 'dt-bday'
+ },
+ 'class': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'email': {
+ 'map': 'u-email'
+ },
+ 'geo': {
+ 'map': 'p-geo',
+ 'uf': ['h-geo']
+ },
+ 'key': {
+ 'map': 'u-key'
+ },
+ 'label': {},
+ 'logo': {
+ 'map': 'u-logo'
+ },
+ 'mailer': {},
+ 'honorific-prefix': {},
+ 'given-name': {},
+ 'additional-name': {},
+ 'family-name': {},
+ 'honorific-suffix': {},
+ 'nickname': {},
+ 'note': {}, // could be html i.e. e-note
+ 'org': {},
+ 'p-organization-name': {},
+ 'p-organization-unit': {},
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ 'rev': {
+ 'map': 'dt-rev'
+ },
+ 'role': {},
+ 'sequence': {},
+ 'sort-string': {},
+ 'sound': {
+ 'map': 'u-sound'
+ },
+ 'title': {
+ 'map': 'p-job-title'
+ },
+ 'tel': {},
+ 'tz': {},
+ 'uid': {
+ 'map': 'u-uid'
+ },
+ 'url': {
+ 'map': 'u-url'
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-entry.js b/toolkit/components/microformats/test/lib/maps/h-entry.js
new file mode 100644
index 0000000000..b82c4c2d9c
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-entry.js
@@ -0,0 +1,52 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-entry'] = {
+ root: 'hentry',
+ name: 'h-entry',
+ properties: {
+ 'entry-title': {
+ 'map': 'p-name'
+ },
+ 'entry-summary': {
+ 'map': 'p-summary'
+ },
+ 'entry-content': {
+ 'map': 'e-content'
+ },
+ 'published': {
+ 'map': 'dt-published'
+ },
+ 'updated': {
+ 'map': 'dt-updated'
+ },
+ 'author': {
+ 'uf': ['h-card']
+ },
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'geo': {
+ 'map': 'p-geo',
+ 'uf': ['h-geo']
+ },
+ 'latitude': {},
+ 'longitude': {},
+ 'url': {
+ 'map': 'u-url',
+ 'relAlt': ['bookmark']
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-event.js b/toolkit/components/microformats/test/lib/maps/h-event.js
new file mode 100644
index 0000000000..6599d45495
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-event.js
@@ -0,0 +1,64 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-event'] = {
+ root: 'vevent',
+ name: 'h-event',
+ properties: {
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'dtstart': {
+ 'map': 'dt-start'
+ },
+ 'dtend': {
+ 'map': 'dt-end'
+ },
+ 'description': {},
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'location': {
+ 'uf': ['h-card']
+ },
+ 'geo': {
+ 'uf': ['h-geo']
+ },
+ 'latitude': {},
+ 'longitude': {},
+ 'duration': {
+ 'map': 'dt-duration'
+ },
+ 'contact': {
+ 'uf': ['h-card']
+ },
+ 'organizer': {
+ 'uf': ['h-card']},
+ 'attendee': {
+ 'uf': ['h-card']},
+ 'uid': {
+ 'map': 'u-uid'
+ },
+ 'attach': {
+ 'map': 'u-attach'
+ },
+ 'status': {},
+ 'rdate': {},
+ 'rrule': {}
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-feed.js b/toolkit/components/microformats/test/lib/maps/h-feed.js
new file mode 100644
index 0000000000..f680228567
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-feed.js
@@ -0,0 +1,36 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-feed'] = {
+ root: 'hfeed',
+ name: 'h-feed',
+ properties: {
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'summary': {
+ 'map': 'p-summary'
+ },
+ 'author': {
+ 'uf': ['h-card']
+ },
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-geo.js b/toolkit/components/microformats/test/lib/maps/h-geo.js
new file mode 100644
index 0000000000..fabb86f074
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-geo.js
@@ -0,0 +1,22 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-geo'] = {
+ root: 'geo',
+ name: 'h-geo',
+ properties: {
+ 'latitude': {},
+ 'longitude': {}
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-item.js b/toolkit/components/microformats/test/lib/maps/h-item.js
new file mode 100644
index 0000000000..471a8454e3
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-item.js
@@ -0,0 +1,30 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-item'] = {
+ root: 'item',
+ name: 'h-item',
+ subTree: false,
+ properties: {
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-listing.js b/toolkit/components/microformats/test/lib/maps/h-listing.js
new file mode 100644
index 0000000000..94783d9ee4
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-listing.js
@@ -0,0 +1,41 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-listing'] = {
+ root: 'hlisting',
+ name: 'h-listing',
+ properties: {
+ 'version': {},
+ 'lister': {
+ 'uf': ['h-card']
+ },
+ 'dtlisted': {
+ 'map': 'dt-listed'
+ },
+ 'dtexpired': {
+ 'map': 'dt-expired'
+ },
+ 'location': {},
+ 'price': {},
+ 'item': {
+ 'uf': ['h-card','a-adr','h-geo']
+ },
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'description': {
+ 'map': 'e-description'
+ },
+ 'listing': {}
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/maps/h-news.js b/toolkit/components/microformats/test/lib/maps/h-news.js
new file mode 100644
index 0000000000..362a5a5709
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-news.js
@@ -0,0 +1,42 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-news'] = {
+ root: 'hnews',
+ name: 'h-news',
+ properties: {
+ 'entry': {
+ 'uf': ['h-entry']
+ },
+ 'geo': {
+ 'uf': ['h-geo']
+ },
+ 'latitude': {},
+ 'longitude': {},
+ 'source-org': {
+ 'uf': ['h-card']
+ },
+ 'dateline': {
+ 'uf': ['h-card']
+ },
+ 'item-license': {
+ 'map': 'u-item-license'
+ },
+ 'principles': {
+ 'map': 'u-principles',
+ 'relAlt': ['principles']
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-org.js b/toolkit/components/microformats/test/lib/maps/h-org.js
new file mode 100644
index 0000000000..d1b4e82450
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-org.js
@@ -0,0 +1,24 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-org'] = {
+ root: 'h-x-org', // drop this from v1 as it causes issue with fn org hcard pattern
+ name: 'h-org',
+ childStructure: true,
+ properties: {
+ 'organization-name': {},
+ 'organization-unit': {}
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-product.js b/toolkit/components/microformats/test/lib/maps/h-product.js
new file mode 100644
index 0000000000..18f8eb51a7
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-product.js
@@ -0,0 +1,49 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-product'] = {
+ root: 'hproduct',
+ name: 'h-product',
+ properties: {
+ 'brand': {
+ 'uf': ['h-card']
+ },
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'price': {},
+ 'description': {
+ 'map': 'e-description'
+ },
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ 'url': {
+ 'map': 'u-url'
+ },
+ 'review': {
+ 'uf': ['h-review', 'h-review-aggregate']
+ },
+ 'listing': {
+ 'uf': ['h-listing']
+ },
+ 'identifier': {
+ 'map': 'u-identifier'
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-recipe.js b/toolkit/components/microformats/test/lib/maps/h-recipe.js
new file mode 100644
index 0000000000..e3901ea3e0
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-recipe.js
@@ -0,0 +1,47 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-recipe'] = {
+ root: 'hrecipe',
+ name: 'h-recipe',
+ properties: {
+ 'fn': {
+ 'map': 'p-name'
+ },
+ 'ingredient': {
+ 'map': 'e-ingredient'
+ },
+ 'yield': {},
+ 'instructions': {
+ 'map': 'e-instructions'
+ },
+ 'duration': {
+ 'map': 'dt-duration'
+ },
+ 'photo': {
+ 'map': 'u-photo'
+ },
+ 'summary': {},
+ 'author': {
+ 'uf': ['h-card']
+ },
+ 'published': {
+ 'map': 'dt-published'
+ },
+ 'nutrition': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/maps/h-resume.js b/toolkit/components/microformats/test/lib/maps/h-resume.js
new file mode 100644
index 0000000000..d6a46cc880
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-resume.js
@@ -0,0 +1,34 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-resume'] = {
+ root: 'hresume',
+ name: 'h-resume',
+ properties: {
+ 'summary': {},
+ 'contact': {
+ 'uf': ['h-card']
+ },
+ 'education': {
+ 'uf': ['h-card', 'h-event']
+ },
+ 'experience': {
+ 'uf': ['h-card', 'h-event']
+ },
+ 'skill': {},
+ 'affiliation': {
+ 'uf': ['h-card']
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
+
diff --git a/toolkit/components/microformats/test/lib/maps/h-review-aggregate.js b/toolkit/components/microformats/test/lib/maps/h-review-aggregate.js
new file mode 100644
index 0000000000..4b6027cbf7
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-review-aggregate.js
@@ -0,0 +1,40 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-review-aggregate'] = {
+ root: 'hreview-aggregate',
+ name: 'h-review-aggregate',
+ properties: {
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'item': {
+ 'map': 'p-item',
+ 'uf': ['h-item', 'h-geo', 'h-adr', 'h-card', 'h-event', 'h-product']
+ },
+ 'rating': {},
+ 'average': {},
+ 'best': {},
+ 'worst': {},
+ 'count': {},
+ 'votes': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'url': {
+ 'map': 'u-url',
+ 'relAlt': ['self', 'bookmark']
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/maps/h-review.js b/toolkit/components/microformats/test/lib/maps/h-review.js
new file mode 100644
index 0000000000..83f4c24bc3
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/h-review.js
@@ -0,0 +1,46 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.maps = (modules.maps)? modules.maps : {};
+
+ modules.maps['h-review'] = {
+ root: 'hreview',
+ name: 'h-review',
+ properties: {
+ 'summary': {
+ 'map': 'p-name'
+ },
+ 'description': {
+ 'map': 'e-description'
+ },
+ 'item': {
+ 'map': 'p-item',
+ 'uf': ['h-item', 'h-geo', 'h-adr', 'h-card', 'h-event', 'h-product']
+ },
+ 'reviewer': {
+ 'uf': ['h-card']
+ },
+ 'dtreviewer': {
+ 'map': 'dt-reviewer'
+ },
+ 'rating': {},
+ 'best': {},
+ 'worst': {},
+ 'category': {
+ 'map': 'p-category',
+ 'relAlt': ['tag']
+ },
+ 'url': {
+ 'map': 'u-url',
+ 'relAlt': ['self', 'bookmark']
+ }
+ }
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/maps/rel.js b/toolkit/components/microformats/test/lib/maps/rel.js
new file mode 100644
index 0000000000..8accf80090
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/maps/rel.js
@@ -0,0 +1,47 @@
+/*
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.rels = {
+ // xfn
+ 'friend': [ 'yes','external'],
+ 'acquaintance': [ 'yes','external'],
+ 'contact': [ 'yes','external'],
+ 'met': [ 'yes','external'],
+ 'co-worker': [ 'yes','external'],
+ 'colleague': [ 'yes','external'],
+ 'co-resident': [ 'yes','external'],
+ 'neighbor': [ 'yes','external'],
+ 'child': [ 'yes','external'],
+ 'parent': [ 'yes','external'],
+ 'sibling': [ 'yes','external'],
+ 'spouse': [ 'yes','external'],
+ 'kin': [ 'yes','external'],
+ 'muse': [ 'yes','external'],
+ 'crush': [ 'yes','external'],
+ 'date': [ 'yes','external'],
+ 'sweetheart': [ 'yes','external'],
+ 'me': [ 'yes','external'],
+
+ // other rel=*
+ 'license': [ 'yes','yes'],
+ 'nofollow': [ 'no','external'],
+ 'tag': [ 'no','yes'],
+ 'self': [ 'no','external'],
+ 'bookmark': [ 'no','external'],
+ 'author': [ 'no','external'],
+ 'home': [ 'no','external'],
+ 'directory': [ 'no','external'],
+ 'enclosure': [ 'no','external'],
+ 'pronunciation': [ 'no','external'],
+ 'payment': [ 'no','external'],
+ 'principles': [ 'no','external']
+
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/parser-implied.js b/toolkit/components/microformats/test/lib/parser-implied.js
new file mode 100644
index 0000000000..7f67a2ca1c
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/parser-implied.js
@@ -0,0 +1,439 @@
+/*!
+ Parser implied
+ All the functions that deal with microformats implied rules
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies dates.js, domutils.js, html.js, isodate,js, text.js, utilities.js, url.js
+*/
+
+var Modules = (function (modules) {
+
+ // check parser module is loaded
+ if(modules.Parser){
+
+ /**
+ * applies "implied rules" microformat output structure i.e. feed-title, name, photo, url and date
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf (microformat output structure)
+ * @param {Object} parentClasses (classes structure)
+ * @param {Boolean} impliedPropertiesByVersion
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedRules = function(node, uf, parentClasses) {
+ var typeVersion = (uf.typeVersion)? uf.typeVersion: 'v2';
+
+ // TEMP: override to allow v1 implied properties while spec changes
+ if(this.options.impliedPropertiesByVersion === false){
+ typeVersion = 'v2';
+ }
+
+ if(node && uf && uf.properties) {
+ uf = this.impliedBackwardComp( node, uf, parentClasses );
+ if(typeVersion === 'v2'){
+ uf = this.impliedhFeedTitle( uf );
+ uf = this.impliedName( node, uf );
+ uf = this.impliedPhoto( node, uf );
+ uf = this.impliedUrl( node, uf );
+ }
+ uf = this.impliedValue( node, uf, parentClasses );
+ uf = this.impliedDate( uf );
+
+ // TEMP: flagged while spec changes are put forward
+ if(this.options.parseLatLonGeo === true){
+ uf = this.impliedGeo( uf );
+ }
+ }
+
+ return uf;
+ };
+
+
+ /**
+ * apply implied name rule
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedName = function(node, uf) {
+ // implied name rule
+ /*
+ img.h-x[alt] <img class="h-card" src="glenn.htm" alt="Glenn Jones"></a>
+ area.h-x[alt] <area class="h-card" href="glenn.htm" alt="Glenn Jones"></area>
+ abbr.h-x[title] <abbr class="h-card" title="Glenn Jones"GJ</abbr>
+
+ .h-x>img:only-child[alt]:not[.h-*] <div class="h-card"><a src="glenn.htm" alt="Glenn Jones"></a></div>
+ .h-x>area:only-child[alt]:not[.h-*] <div class="h-card"><area href="glenn.htm" alt="Glenn Jones"></area></div>
+ .h-x>abbr:only-child[title] <div class="h-card"><abbr title="Glenn Jones">GJ</abbr></div>
+
+ .h-x>:only-child>img:only-child[alt]:not[.h-*] <div class="h-card"><span><img src="jane.html" alt="Jane Doe"/></span></div>
+ .h-x>:only-child>area:only-child[alt]:not[.h-*] <div class="h-card"><span><area href="jane.html" alt="Jane Doe"></area></span></div>
+ .h-x>:only-child>abbr:only-child[title] <div class="h-card"><span><abbr title="Jane Doe">JD</abbr></span></div>
+ */
+ var name,
+ value;
+
+ if(!uf.properties.name) {
+ value = this.getImpliedProperty(node, ['img', 'area', 'abbr'], this.getNameAttr);
+ var textFormat = this.options.textFormat;
+ // if no value for tags/properties use text
+ if(!value) {
+ name = [modules.text.parse(this.document, node, textFormat)];
+ }else{
+ name = [modules.text.parseText(this.document, value, textFormat)];
+ }
+ if(name && name[0] !== ''){
+ uf.properties.name = name;
+ }
+ }
+
+ return uf;
+ };
+
+
+ /**
+ * apply implied photo rule
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedPhoto = function(node, uf) {
+ // implied photo rule
+ /*
+ img.h-x[src] <img class="h-card" alt="Jane Doe" src="jane.jpeg"/>
+ object.h-x[data] <object class="h-card" data="jane.jpeg"/>Jane Doe</object>
+ .h-x>img[src]:only-of-type:not[.h-*] <div class="h-card"><img alt="Jane Doe" src="jane.jpeg"/></div>
+ .h-x>object[data]:only-of-type:not[.h-*] <div class="h-card"><object data="jane.jpeg"/>Jane Doe</object></div>
+ .h-x>:only-child>img[src]:only-of-type:not[.h-*] <div class="h-card"><span><img alt="Jane Doe" src="jane.jpeg"/></span></div>
+ .h-x>:only-child>object[data]:only-of-type:not[.h-*] <div class="h-card"><span><object data="jane.jpeg"/>Jane Doe</object></span></div>
+ */
+ var value;
+ if(!uf.properties.photo) {
+ value = this.getImpliedProperty(node, ['img', 'object'], this.getPhotoAttr);
+ if(value) {
+ // relative to absolute URL
+ if(value && value !== '' && this.options.baseUrl !== '' && value.indexOf('://') === -1) {
+ value = modules.url.resolve(value, this.options.baseUrl);
+ }
+ uf.properties.photo = [modules.utils.trim(value)];
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * apply implied URL rule
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedUrl = function(node, uf) {
+ // implied URL rule
+ /*
+ a.h-x[href] <a class="h-card" href="glenn.html">Glenn</a>
+ area.h-x[href] <area class="h-card" href="glenn.html">Glenn</area>
+ .h-x>a[href]:only-of-type:not[.h-*] <div class="h-card" ><a href="glenn.html">Glenn</a><p>...</p></div>
+ .h-x>area[href]:only-of-type:not[.h-*] <div class="h-card" ><area href="glenn.html">Glenn</area><p>...</p></div>
+ */
+ var value;
+ if(!uf.properties.url) {
+ value = this.getImpliedProperty(node, ['a', 'area'], this.getURLAttr);
+ if(value) {
+ // relative to absolute URL
+ if(value && value !== '' && this.options.baseUrl !== '' && value.indexOf('://') === -1) {
+ value = modules.url.resolve(value, this.options.baseUrl);
+ }
+ uf.properties.url = [modules.utils.trim(value)];
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * apply implied date rule - if there is a time only property try to concat it with any date property
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedDate = function(uf) {
+ // implied date rule
+ // http://microformats.org/wiki/value-class-pattern#microformats2_parsers
+ // http://microformats.org/wiki/microformats2-parsing-issues#implied_date_for_dt_properties_both_mf2_and_backcompat
+ var newDate;
+ if(uf.times.length > 0 && uf.dates.length > 0) {
+ newDate = modules.dates.dateTimeUnion(uf.dates[0][1], uf.times[0][1], this.options.dateFormat);
+ uf.properties[this.removePropPrefix(uf.times[0][0])][0] = newDate.toString(this.options.dateFormat);
+ }
+ // clean-up object
+ delete uf.times;
+ delete uf.dates;
+ return uf;
+ };
+
+
+ /**
+ * get an implied property value from pre-defined tag/attriubte combinations
+ *
+ * @param {DOM Node} node
+ * @param {String} tagList (Array of tags from which an implied value can be pulled)
+ * @param {String} getAttrFunction (Function which can extract implied value)
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getImpliedProperty = function(node, tagList, getAttrFunction) {
+ // i.e. img.h-card
+ var value = getAttrFunction(node),
+ descendant,
+ child;
+
+ if(!value) {
+ // i.e. .h-card>img:only-of-type:not(.h-card)
+ descendant = modules.domUtils.getSingleDescendantOfType( node, tagList);
+ if(descendant && this.hasHClass(descendant) === false){
+ value = getAttrFunction(descendant);
+ }
+ if(node.children.length > 0 ){
+ // i.e. .h-card>:only-child>img:only-of-type:not(.h-card)
+ child = modules.domUtils.getSingleDescendant(node);
+ if(child && this.hasHClass(child) === false){
+ descendant = modules.domUtils.getSingleDescendantOfType(child, tagList);
+ if(descendant && this.hasHClass(descendant) === false){
+ value = getAttrFunction(descendant);
+ }
+ }
+ }
+ }
+
+ return value;
+ };
+
+
+ /**
+ * get an implied name value from a node
+ *
+ * @param {DOM Node} node
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getNameAttr = function(node) {
+ var value = modules.domUtils.getAttrValFromTagList(node, ['img','area'], 'alt');
+ if(!value) {
+ value = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+ return value;
+ };
+
+
+ /**
+ * get an implied photo value from a node
+ *
+ * @param {DOM Node} node
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getPhotoAttr = function(node) {
+ var value = modules.domUtils.getAttrValFromTagList(node, ['img'], 'src');
+ if(!value && modules.domUtils.hasAttributeValue(node, 'class', 'include') === false) {
+ value = modules.domUtils.getAttrValFromTagList(node, ['object'], 'data');
+ }
+ return value;
+ };
+
+
+ /**
+ * get an implied photo value from a node
+ *
+ * @param {DOM Node} node
+ * @return {String || null}
+ */
+ modules.Parser.prototype.getURLAttr = function(node) {
+ var value = null;
+ if(modules.domUtils.hasAttributeValue(node, 'class', 'include') === false){
+
+ value = modules.domUtils.getAttrValFromTagList(node, ['a'], 'href');
+ if(!value) {
+ value = modules.domUtils.getAttrValFromTagList(node, ['area'], 'href');
+ }
+
+ }
+ return value;
+ };
+
+
+ /**
+ *
+ *
+ * @param {DOM Node} node
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedValue = function(node, uf, parentClasses){
+
+ // intersection of implied name and implied value rules
+ if(uf.properties.name) {
+ if(uf.value && parentClasses.root.length > 0 && parentClasses.properties.length === 1){
+ uf = this.getAltValue(uf, parentClasses.properties[0][0], 'p-name', uf.properties.name[0]);
+ }
+ }
+
+ // intersection of implied URL and implied value rules
+ if(uf.properties.url) {
+ if(parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1){
+ uf = this.getAltValue(uf, parentClasses.properties[0][0], 'u-url', uf.properties.url[0]);
+ }
+ }
+
+ // apply alt value
+ if(uf.altValue !== null){
+ uf.value = uf.altValue.value;
+ }
+ delete uf.altValue;
+
+
+ return uf;
+ };
+
+
+ /**
+ * get alt value based on rules about parent property prefix
+ *
+ * @param {Object} uf
+ * @param {String} parentPropertyName
+ * @param {String} propertyName
+ * @param {String} value
+ * @return {Object}
+ */
+ modules.Parser.prototype.getAltValue = function(uf, parentPropertyName, propertyName, value){
+ if(uf.value && !uf.altValue){
+ // first p-name of the h-* child
+ if(modules.utils.startWith(parentPropertyName,'p-') && propertyName === 'p-name'){
+ uf.altValue = {name: propertyName, value: value};
+ }
+ // if it's an e-* property element
+ if(modules.utils.startWith(parentPropertyName,'e-') && modules.utils.startWith(propertyName,'e-')){
+ uf.altValue = {name: propertyName, value: value};
+ }
+ // if it's an u-* property element
+ if(modules.utils.startWith(parentPropertyName,'u-') && propertyName === 'u-url'){
+ uf.altValue = {name: propertyName, value: value};
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * if a h-feed does not have a title use the title tag of a page
+ *
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedhFeedTitle = function( uf ){
+ if(uf.type && uf.type.indexOf('h-feed') > -1){
+ // has no name property
+ if(uf.properties.name === undefined || uf.properties.name[0] === '' ){
+ // use the text from the title tag
+ var title = modules.domUtils.querySelector(this.document, 'title');
+ if(title){
+ uf.properties.name = [modules.domUtils.textContent(title)];
+ }
+ }
+ }
+ return uf;
+ };
+
+
+
+ /**
+ * implied Geo from pattern <abbr class="p-geo" title="37.386013;-122.082932">
+ *
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedGeo = function( uf ){
+ var geoPair,
+ parts,
+ longitude,
+ latitude,
+ valid = true;
+
+ if(uf.type && uf.type.indexOf('h-geo') > -1){
+
+ // has no latitude or longitude property
+ if(uf.properties.latitude === undefined || uf.properties.longitude === undefined ){
+
+ geoPair = (uf.properties.name)? uf.properties.name[0] : null;
+ geoPair = (!geoPair && uf.properties.value)? uf.properties.value : geoPair;
+
+ if(geoPair){
+ // allow for the use of a ';' as in microformats and also ',' as in Geo URL
+ geoPair = geoPair.replace(';',',');
+
+ // has sep char
+ if(geoPair.indexOf(',') > -1 ){
+ parts = geoPair.split(',');
+
+ // only correct if we have two or more parts
+ if(parts.length > 1){
+
+ // latitude no value outside the range -90 or 90
+ latitude = parseFloat( parts[0] );
+ if(modules.utils.isNumber(latitude) && latitude > 90 || latitude < -90){
+ valid = false;
+ }
+
+ // longitude no value outside the range -180 to 180
+ longitude = parseFloat( parts[1] );
+ if(modules.utils.isNumber(longitude) && longitude > 180 || longitude < -180){
+ valid = false;
+ }
+
+ if(valid){
+ uf.properties.latitude = [latitude];
+ uf.properties.longitude = [longitude];
+ }
+ }
+
+ }
+ }
+ }
+ }
+ return uf;
+ };
+
+
+ /**
+ * if a backwards compat built structure has no properties add name through this.impliedName
+ *
+ * @param {Object} uf
+ * @return {Object}
+ */
+ modules.Parser.prototype.impliedBackwardComp = function(node, uf, parentClasses){
+
+ // look for pattern in parent classes like "p-geo h-geo"
+ // these are structures built from backwards compat parsing of geo
+ if(parentClasses.root.length === 1 && parentClasses.properties.length === 1) {
+ if(parentClasses.root[0].replace('h-','') === this.removePropPrefix(parentClasses.properties[0][0])) {
+
+ // if microformat has no properties apply the impliedName rule to get value from containing node
+ // this will get value from html such as <abbr class="geo" title="30.267991;-97.739568">Brighton</abbr>
+ if( modules.utils.hasProperties(uf.properties) === false ){
+ uf = this.impliedName( node, uf );
+ }
+ }
+ }
+
+ return uf;
+ };
+
+
+
+ }
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/parser-includes.js b/toolkit/components/microformats/test/lib/parser-includes.js
new file mode 100644
index 0000000000..f0967710d0
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/parser-includes.js
@@ -0,0 +1,150 @@
+/*!
+ Parser includes
+ All the functions that deal with microformats v1 include rules
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies dates.js, domutils.js, html.js, isodate,js, text.js, utilities.js
+*/
+
+
+var Modules = (function (modules) {
+
+ // check parser module is loaded
+ if(modules.Parser){
+
+
+ /**
+ * appends clones of include Nodes into the DOM structure
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.addIncludes = function(rootNode) {
+ this.addAttributeIncludes(rootNode, 'itemref');
+ this.addAttributeIncludes(rootNode, 'headers');
+ this.addClassIncludes(rootNode);
+ };
+
+
+ /**
+ * appends clones of include Nodes into the DOM structure for attribute based includes
+ *
+ * @param {DOM node} rootNode
+ * @param {String} attributeName
+ */
+ modules.Parser.prototype.addAttributeIncludes = function(rootNode, attributeName) {
+ var arr,
+ idList,
+ i,
+ x,
+ z,
+ y;
+
+ arr = modules.domUtils.getNodesByAttribute(rootNode, attributeName);
+ x = 0;
+ i = arr.length;
+ while(x < i) {
+ idList = modules.domUtils.getAttributeList(arr[x], attributeName);
+ if(idList) {
+ z = 0;
+ y = idList.length;
+ while(z < y) {
+ this.apppendInclude(arr[x], idList[z]);
+ z++;
+ }
+ }
+ x++;
+ }
+ };
+
+
+ /**
+ * appends clones of include Nodes into the DOM structure for class based includes
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.addClassIncludes = function(rootNode) {
+ var id,
+ arr,
+ x = 0,
+ i;
+
+ arr = modules.domUtils.getNodesByAttributeValue(rootNode, 'class', 'include');
+ i = arr.length;
+ while(x < i) {
+ id = modules.domUtils.getAttrValFromTagList(arr[x], ['a'], 'href');
+ if(!id) {
+ id = modules.domUtils.getAttrValFromTagList(arr[x], ['object'], 'data');
+ }
+ this.apppendInclude(arr[x], id);
+ x++;
+ }
+ };
+
+
+ /**
+ * appends a clone of an include into another Node using Id
+ *
+ * @param {DOM node} rootNode
+ * @param {Stringe} id
+ */
+ modules.Parser.prototype.apppendInclude = function(node, id){
+ var include,
+ clone;
+
+ id = modules.utils.trim(id.replace('#', ''));
+ include = modules.domUtils.getElementById(this.document, id);
+ if(include) {
+ clone = modules.domUtils.clone(include);
+ this.markIncludeChildren(clone);
+ modules.domUtils.appendChild(node, clone);
+ }
+ };
+
+
+ /**
+ * adds an attribute marker to all the child microformat roots
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.markIncludeChildren = function(rootNode) {
+ var arr,
+ x,
+ i;
+
+ // loop the array and add the attribute
+ arr = this.findRootNodes(rootNode);
+ x = 0;
+ i = arr.length;
+ modules.domUtils.setAttribute(rootNode, 'data-include', 'true');
+ modules.domUtils.setAttribute(rootNode, 'style', 'display:none');
+ while(x < i) {
+ modules.domUtils.setAttribute(arr[x], 'data-include', 'true');
+ x++;
+ }
+ };
+
+
+ /**
+ * removes all appended include clones from DOM
+ *
+ * @param {DOM node} rootNode
+ */
+ modules.Parser.prototype.removeIncludes = function(rootNode){
+ var arr,
+ i;
+
+ // remove all the items that were added as includes
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'data-include');
+ i = arr.length;
+ while(i--) {
+ modules.domUtils.removeChild(rootNode,arr[i]);
+ }
+ };
+
+
+ }
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/parser-rels.js b/toolkit/components/microformats/test/lib/parser-rels.js
new file mode 100644
index 0000000000..63ef674469
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/parser-rels.js
@@ -0,0 +1,200 @@
+/*!
+ Parser rels
+ All the functions that deal with microformats v2 rel structures
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies dates.js, domutils.js, html.js, isodate,js, text.js, utilities.js, url.js
+*/
+
+
+var Modules = (function (modules) {
+
+ // check parser module is loaded
+ if(modules.Parser){
+
+ /**
+ * finds rel=* structures
+ *
+ * @param {DOM node} rootNode
+ * @return {Object}
+ */
+ modules.Parser.prototype.findRels = function(rootNode) {
+ var out = {
+ 'items': [],
+ 'rels': {},
+ 'rel-urls': {}
+ },
+ x,
+ i,
+ y,
+ z,
+ relList,
+ items,
+ item,
+ value,
+ arr;
+
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'rel');
+ x = 0;
+ i = arr.length;
+ while(x < i) {
+ relList = modules.domUtils.getAttribute(arr[x], 'rel');
+
+ if(relList) {
+ items = relList.split(' ');
+
+
+ // add rels
+ z = 0;
+ y = items.length;
+ while(z < y) {
+ item = modules.utils.trim(items[z]);
+
+ // get rel value
+ value = modules.domUtils.getAttrValFromTagList(arr[x], ['a', 'area'], 'href');
+ if(!value) {
+ value = modules.domUtils.getAttrValFromTagList(arr[x], ['link'], 'href');
+ }
+
+ // create the key
+ if(!out.rels[item]) {
+ out.rels[item] = [];
+ }
+
+ if(typeof this.options.baseUrl === 'string' && typeof value === 'string') {
+
+ var resolved = modules.url.resolve(value, this.options.baseUrl);
+ // do not add duplicate rels - based on resolved URLs
+ if(out.rels[item].indexOf(resolved) === -1){
+ out.rels[item].push( resolved );
+ }
+ }
+ z++;
+ }
+
+
+ var url = null;
+ if(modules.domUtils.hasAttribute(arr[x], 'href')){
+ url = modules.domUtils.getAttribute(arr[x], 'href');
+ if(url){
+ url = modules.url.resolve(url, this.options.baseUrl );
+ }
+ }
+
+
+ // add to rel-urls
+ var relUrl = this.getRelProperties(arr[x]);
+ relUrl.rels = items;
+ // // do not add duplicate rel-urls - based on resolved URLs
+ if(url && out['rel-urls'][url] === undefined){
+ out['rel-urls'][url] = relUrl;
+ }
+
+
+ }
+ x++;
+ }
+ return out;
+ };
+
+
+ /**
+ * gets the properties of a rel=*
+ *
+ * @param {DOM node} node
+ * @return {Object}
+ */
+ modules.Parser.prototype.getRelProperties = function(node){
+ var obj = {};
+
+ if(modules.domUtils.hasAttribute(node, 'media')){
+ obj.media = modules.domUtils.getAttribute(node, 'media');
+ }
+ if(modules.domUtils.hasAttribute(node, 'type')){
+ obj.type = modules.domUtils.getAttribute(node, 'type');
+ }
+ if(modules.domUtils.hasAttribute(node, 'hreflang')){
+ obj.hreflang = modules.domUtils.getAttribute(node, 'hreflang');
+ }
+ if(modules.domUtils.hasAttribute(node, 'title')){
+ obj.title = modules.domUtils.getAttribute(node, 'title');
+ }
+ if(modules.utils.trim(this.getPValue(node, false)) !== ''){
+ obj.text = this.getPValue(node, false);
+ }
+
+ return obj;
+ };
+
+
+ /**
+ * finds any alt rel=* mappings for a given node/microformat
+ *
+ * @param {DOM node} node
+ * @param {String} ufName
+ * @return {String || undefined}
+ */
+ modules.Parser.prototype.findRelImpied = function(node, ufName) {
+ var out,
+ map,
+ i;
+
+ map = this.getMapping(ufName);
+ if(map) {
+ for(var key in map.properties) {
+ if (map.properties.hasOwnProperty(key)) {
+ var prop = map.properties[key],
+ propName = (prop.map) ? prop.map : 'p-' + key,
+ relCount = 0;
+
+ // is property an alt rel=* mapping
+ if(prop.relAlt && modules.domUtils.hasAttribute(node, 'rel')) {
+ i = prop.relAlt.length;
+ while(i--) {
+ if(modules.domUtils.hasAttributeValue(node, 'rel', prop.relAlt[i])) {
+ relCount++;
+ }
+ }
+ if(relCount === prop.relAlt.length) {
+ out = propName;
+ }
+ }
+ }
+ }
+ }
+ return out;
+ };
+
+
+ /**
+ * returns whether a node or its children has rel=* microformat
+ *
+ * @param {DOM node} node
+ * @return {Boolean}
+ */
+ modules.Parser.prototype.hasRel = function(node) {
+ return (this.countRels(node) > 0);
+ };
+
+
+ /**
+ * returns the number of rel=* microformats
+ *
+ * @param {DOM node} node
+ * @return {Int}
+ */
+ modules.Parser.prototype.countRels = function(node) {
+ if(node){
+ return modules.domUtils.getNodesByAttribute(node, 'rel').length;
+ }
+ return 0;
+ };
+
+
+
+ }
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/parser.js b/toolkit/components/microformats/test/lib/parser.js
new file mode 100644
index 0000000000..062ec9f0e0
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/parser.js
@@ -0,0 +1,1453 @@
+/*!
+ Parser
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies dates.js, domutils.js, html.js, isodate,js, text.js, utilities.js, url.js
+*/
+
+
+var Modules = (function (modules) {
+
+
+ /**
+ * constructor
+ *
+ */
+ modules.Parser = function () {
+ this.rootPrefix = 'h-';
+ this.propertyPrefixes = ['p-', 'dt-', 'u-', 'e-'];
+ this.excludeTags = ['br', 'hr'];
+ };
+
+
+ // create objects incase the v1 map modules don't load
+ modules.maps = (modules.maps)? modules.maps : {};
+ modules.rels = (modules.rels)? modules.rels : {};
+
+
+ modules.Parser.prototype = {
+
+ init: function(){
+ this.rootNode = null;
+ this.document = null;
+ this.options = {
+ 'baseUrl': '',
+ 'filters': [],
+ 'textFormat': 'whitespacetrimmed',
+ 'dateFormat': 'auto', // html5 for testing
+ 'overlappingVersions': false,
+ 'impliedPropertiesByVersion': true,
+ 'parseLatLonGeo': false
+ };
+ this.rootID = 0;
+ this.errors = [];
+ this.noContentErr = 'No options.node or options.html was provided and no document object could be found.';
+ },
+
+
+ /**
+ * internal parse function
+ *
+ * @param {Object} options
+ * @return {Object}
+ */
+ get: function(options) {
+ var out = this.formatEmpty(),
+ data = [],
+ rels;
+
+ this.init();
+ options = (options)? options : {};
+ this.mergeOptions(options);
+ this.getDOMContext( options );
+
+ // if we do not have any context create error
+ if(!this.rootNode || !this.document){
+ this.errors.push(this.noContentErr);
+ }else{
+
+ // only parse h-* microformats if we need to
+ // this is added to speed up parsing
+ if(this.hasMicroformats(this.rootNode, options)){
+ this.prepareDOM( options );
+
+ if(this.options.filters.length > 0){
+ // parse flat list of items
+ var newRootNode = this.findFilterNodes(this.rootNode, this.options.filters);
+ data = this.walkRoot(newRootNode);
+ }else{
+ // parse whole document from root
+ data = this.walkRoot(this.rootNode);
+ }
+
+ out.items = data;
+ // don't clear-up DOM if it was cloned
+ if(modules.domUtils.canCloneDocument(this.document) === false){
+ this.clearUpDom(this.rootNode);
+ }
+ }
+
+ // find any rels
+ if(this.findRels){
+ rels = this.findRels(this.rootNode);
+ out.rels = rels.rels;
+ out['rel-urls'] = rels['rel-urls'];
+ }
+
+ }
+
+ if(this.errors.length > 0){
+ return this.formatError();
+ }
+ return out;
+ },
+
+
+ /**
+ * parse to get parent microformat of passed node
+ *
+ * @param {DOM Node} node
+ * @param {Object} options
+ * @return {Object}
+ */
+ getParent: function(node, options) {
+ this.init();
+ options = (options)? options : {};
+
+ if(node){
+ return this.getParentTreeWalk(node, options);
+ }else{
+ this.errors.push(this.noContentErr);
+ return this.formatError();
+ }
+ },
+
+
+ /**
+ * get the count of microformats
+ *
+ * @param {DOM Node} rootNode
+ * @return {Int}
+ */
+ count: function( options ) {
+ var out = {},
+ items,
+ classItems,
+ x,
+ i;
+
+ this.init();
+ options = (options)? options : {};
+ this.getDOMContext( options );
+
+ // if we do not have any context create error
+ if(!this.rootNode || !this.document){
+ return {'errors': [this.noContentErr]};
+ }else{
+
+ items = this.findRootNodes( this.rootNode, true );
+ i = items.length;
+ while(i--) {
+ classItems = modules.domUtils.getAttributeList(items[i], 'class');
+ x = classItems.length;
+ while(x--) {
+ // find v2 names
+ if(modules.utils.startWith( classItems[x], 'h-' )){
+ this.appendCount(classItems[x], 1, out);
+ }
+ // find v1 names
+ for(var key in modules.maps) {
+ // dont double count if v1 and v2 roots are present
+ if(modules.maps[key].root === classItems[x] && classItems.indexOf(key) === -1) {
+ this.appendCount(key, 1, out);
+ }
+ }
+ }
+ }
+ var relCount = this.countRels( this.rootNode );
+ if(relCount > 0){
+ out.rels = relCount;
+ }
+
+ return out;
+ }
+ },
+
+
+ /**
+ * does a node have a class that marks it as a microformats root
+ *
+ * @param {DOM Node} node
+ * @param {Objecte} options
+ * @return {Boolean}
+ */
+ isMicroformat: function( node, options ) {
+ var classes,
+ i;
+
+ if(!node){
+ return false;
+ }
+
+ // if documemt gets topmost node
+ node = modules.domUtils.getTopMostNode( node );
+
+ // look for h-* microformats
+ classes = this.getUfClassNames(node);
+ if(options && options.filters && modules.utils.isArray(options.filters)){
+ i = options.filters.length;
+ while(i--) {
+ if(classes.root.indexOf(options.filters[i]) > -1){
+ return true;
+ }
+ }
+ return false;
+ }else{
+ return (classes.root.length > 0);
+ }
+ },
+
+
+ /**
+ * does a node or its children have microformats
+ *
+ * @param {DOM Node} node
+ * @param {Objecte} options
+ * @return {Boolean}
+ */
+ hasMicroformats: function( node, options ) {
+ var items,
+ i;
+
+ if(!node){
+ return false;
+ }
+
+ // if browser based documemt get topmost node
+ node = modules.domUtils.getTopMostNode( node );
+
+ // returns all microformat roots
+ items = this.findRootNodes( node, true );
+ if(options && options.filters && modules.utils.isArray(options.filters)){
+ i = items.length;
+ while(i--) {
+ if( this.isMicroformat( items[i], options ) ){
+ return true;
+ }
+ }
+ return false;
+ }else{
+ return (items.length > 0);
+ }
+ },
+
+
+ /**
+ * add a new v1 mapping object to parser
+ *
+ * @param {Array} maps
+ */
+ add: function( maps ){
+ maps.forEach(function(map){
+ if(map && map.root && map.name && map.properties){
+ modules.maps[map.name] = JSON.parse(JSON.stringify(map));
+ }
+ });
+ },
+
+
+ /**
+ * internal parse to get parent microformats by walking up the tree
+ *
+ * @param {DOM Node} node
+ * @param {Object} options
+ * @param {Int} recursive
+ * @return {Object}
+ */
+ getParentTreeWalk: function (node, options, recursive) {
+ options = (options)? options : {};
+
+ // recursive calls
+ if (recursive === undefined) {
+ if (node.parentNode && node.nodeName !== 'HTML'){
+ return this.getParentTreeWalk(node.parentNode, options, true);
+ }else{
+ return this.formatEmpty();
+ }
+ }
+ if (node !== null && node !== undefined && node.parentNode) {
+ if (this.isMicroformat( node, options )) {
+ // if we have a match return microformat
+ options.node = node;
+ return this.get( options );
+ }else{
+ return this.getParentTreeWalk(node.parentNode, options, true);
+ }
+ }else{
+ return this.formatEmpty();
+ }
+ },
+
+
+
+ /**
+ * configures what are the base DOM objects for parsing
+ *
+ * @param {Object} options
+ */
+ getDOMContext: function( options ){
+ var nodes = modules.domUtils.getDOMContext( options );
+ this.rootNode = nodes.rootNode;
+ this.document = nodes.document;
+ },
+
+
+ /**
+ * prepares DOM before the parse begins
+ *
+ * @param {Object} options
+ * @return {Boolean}
+ */
+ prepareDOM: function( options ){
+ var baseTag,
+ href;
+
+ // use current document to define baseUrl, try/catch needed for IE10+ error
+ try {
+ if (!options.baseUrl && this.document && this.document.location) {
+ this.options.baseUrl = this.document.location.href;
+ }
+ } catch (e) {
+ // there is no alt action
+ }
+
+
+ // find base tag to set baseUrl
+ baseTag = modules.domUtils.querySelector(this.document,'base');
+ if(baseTag) {
+ href = modules.domUtils.getAttribute(baseTag, 'href');
+ if(href){
+ this.options.baseUrl = href;
+ }
+ }
+
+ // get path to rootNode
+ // then clone document
+ // then reset the rootNode to its cloned version in a new document
+ var path,
+ newDocument,
+ newRootNode;
+
+ path = modules.domUtils.getNodePath(this.rootNode);
+ newDocument = modules.domUtils.cloneDocument(this.document);
+ newRootNode = modules.domUtils.getNodeByPath(newDocument, path);
+
+ // check results as early IE fails
+ if(newDocument && newRootNode){
+ this.document = newDocument;
+ this.rootNode = newRootNode;
+ }
+
+ // add includes
+ if(this.addIncludes){
+ this.addIncludes( this.document );
+ }
+
+ return (this.rootNode && this.document);
+ },
+
+
+ /**
+ * returns an empty structure with errors
+ *
+ * @return {Object}
+ */
+ formatError: function(){
+ var out = this.formatEmpty();
+ out.errors = this.errors;
+ return out;
+ },
+
+
+ /**
+ * returns an empty structure
+ *
+ * @return {Object}
+ */
+ formatEmpty: function(){
+ return {
+ 'items': [],
+ 'rels': {},
+ 'rel-urls': {}
+ };
+ },
+
+
+ // find microformats of a given type and return node structures
+ findFilterNodes: function(rootNode, filters) {
+ var newRootNode = modules.domUtils.createNode('div'),
+ items = this.findRootNodes(rootNode, true),
+ i = 0,
+ x = 0,
+ y = 0;
+
+ if(items){
+ i = items.length;
+ while(x < i) {
+ // add v1 names
+ y = filters.length;
+ while (y--) {
+ if(this.getMapping(filters[y])){
+ var v1Name = this.getMapping(filters[y]).root;
+ filters.push(v1Name);
+ }
+ }
+ // append matching nodes into newRootNode
+ y = filters.length;
+ while (y--) {
+ if(modules.domUtils.hasAttributeValue(items[x], 'class', filters[y])){
+ var clone = modules.domUtils.clone(items[x]);
+ modules.domUtils.appendChild(newRootNode, clone);
+ break;
+ }
+ }
+ x++;
+ }
+ }
+
+ return newRootNode;
+ },
+
+
+ /**
+ * appends data to output object for count
+ *
+ * @param {string} name
+ * @param {Int} count
+ * @param {Object}
+ */
+ appendCount: function(name, count, out){
+ if(out[name]){
+ out[name] = out[name] + count;
+ }else{
+ out[name] = count;
+ }
+ },
+
+
+ /**
+ * is the microformats type in the filter list
+ *
+ * @param {Object} uf
+ * @param {Array} filters
+ * @return {Boolean}
+ */
+ shouldInclude: function(uf, filters) {
+ var i;
+
+ if(modules.utils.isArray(filters) && filters.length > 0) {
+ i = filters.length;
+ while(i--) {
+ if(uf.type[0] === filters[i]) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return true;
+ }
+ },
+
+
+ /**
+ * finds all microformat roots in a rootNode
+ *
+ * @param {DOM Node} rootNode
+ * @param {Boolean} includeRoot
+ * @return {Array}
+ */
+ findRootNodes: function(rootNode, includeRoot) {
+ var arr = null,
+ out = [],
+ classList = [],
+ items,
+ x,
+ i,
+ y,
+ key;
+
+
+ // build an array of v1 root names
+ for(key in modules.maps) {
+ if (modules.maps.hasOwnProperty(key)) {
+ classList.push(modules.maps[key].root);
+ }
+ }
+
+ // get all elements that have a class attribute
+ includeRoot = (includeRoot) ? includeRoot : false;
+ if(includeRoot && rootNode.parentNode) {
+ arr = modules.domUtils.getNodesByAttribute(rootNode.parentNode, 'class');
+ } else {
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'class');
+ }
+
+ // loop elements that have a class attribute
+ x = 0;
+ i = arr.length;
+ while(x < i) {
+
+ items = modules.domUtils.getAttributeList(arr[x], 'class');
+
+ // loop classes on an element
+ y = items.length;
+ while(y--) {
+ // match v1 root names
+ if(classList.indexOf(items[y]) > -1) {
+ out.push(arr[x]);
+ break;
+ }
+
+ // match v2 root name prefix
+ if(modules.utils.startWith(items[y], 'h-')) {
+ out.push(arr[x]);
+ break;
+ }
+ }
+
+ x++;
+ }
+ return out;
+ },
+
+
+ /**
+ * starts the tree walk to find microformats
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ walkRoot: function(node){
+ var context = this,
+ children = [],
+ child,
+ classes,
+ items = [],
+ out = [];
+
+ classes = this.getUfClassNames(node);
+ // if it is a root microformat node
+ if(classes && classes.root.length > 0){
+ items = this.walkTree(node);
+
+ if(items.length > 0){
+ out = out.concat(items);
+ }
+ }else{
+ // check if there are children and one of the children has a root microformat
+ children = modules.domUtils.getChildren( node );
+ if(children && children.length > 0 && this.findRootNodes(node, true).length > -1){
+ for (var i = 0; i < children.length; i++) {
+ child = children[i];
+ items = context.walkRoot(child);
+ if(items.length > 0){
+ out = out.concat(items);
+ }
+ }
+ }
+ }
+ return out;
+ },
+
+
+ /**
+ * starts the tree walking for a single microformat
+ *
+ * @param {DOM Node} node
+ * @return {Array}
+ */
+ walkTree: function(node) {
+ var classes,
+ out = [],
+ obj,
+ itemRootID;
+
+ // loop roots found on one element
+ classes = this.getUfClassNames(node);
+ if(classes && classes.root.length && classes.root.length > 0){
+
+ this.rootID++;
+ itemRootID = this.rootID;
+ obj = this.createUfObject(classes.root, classes.typeVersion);
+
+ this.walkChildren(node, obj, classes.root, itemRootID, classes);
+ if(this.impliedRules){
+ this.impliedRules(node, obj, classes);
+ }
+ out.push( this.cleanUfObject(obj) );
+
+
+ }
+ return out;
+ },
+
+
+ /**
+ * finds child properties of microformat
+ *
+ * @param {DOM Node} node
+ * @param {Object} out
+ * @param {String} ufName
+ * @param {Int} rootID
+ * @param {Object} parentClasses
+ */
+ walkChildren: function(node, out, ufName, rootID, parentClasses) {
+ var context = this,
+ children = [],
+ rootItem,
+ itemRootID,
+ value,
+ propertyName,
+ propertyVersion,
+ i,
+ x,
+ y,
+ z,
+ child;
+
+ children = modules.domUtils.getChildren( node );
+
+ y = 0;
+ z = children.length;
+ while(y < z) {
+ child = children[y];
+
+ // get microformat classes for this single element
+ var classes = context.getUfClassNames(child, ufName);
+
+ // a property which is a microformat
+ if(classes.root.length > 0 && classes.properties.length > 0 && !child.addedAsRoot) {
+ // create object with type, property and value
+ rootItem = context.createUfObject(
+ classes.root,
+ classes.typeVersion,
+ modules.text.parse(this.document, child, context.options.textFormat)
+ );
+
+ // add the microformat as an array of properties
+ propertyName = context.removePropPrefix(classes.properties[0][0]);
+
+ // modifies value with "implied value rule"
+ if(parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1){
+ if(context.impliedValueRule){
+ out = context.impliedValueRule(out, parentClasses.properties[0][0], classes.properties[0][0], value);
+ }
+ }
+
+ if(out.properties[propertyName]) {
+ out.properties[propertyName].push(rootItem);
+ } else {
+ out.properties[propertyName] = [rootItem];
+ }
+
+ context.rootID++;
+ // used to stop duplication in heavily nested structures
+ child.addedAsRoot = true;
+
+
+ x = 0;
+ i = rootItem.type.length;
+ itemRootID = context.rootID;
+ while(x < i) {
+ context.walkChildren(child, rootItem, rootItem.type, itemRootID, classes);
+ x++;
+ }
+ if(this.impliedRules){
+ context.impliedRules(child, rootItem, classes);
+ }
+ this.cleanUfObject(rootItem);
+
+ }
+
+ // a property which is NOT a microformat and has not been used for a given root element
+ if(classes.root.length === 0 && classes.properties.length > 0) {
+
+ x = 0;
+ i = classes.properties.length;
+ while(x < i) {
+
+ value = context.getValue(child, classes.properties[x][0], out);
+ propertyName = context.removePropPrefix(classes.properties[x][0]);
+ propertyVersion = classes.properties[x][1];
+
+ // modifies value with "implied value rule"
+ if(parentClasses && parentClasses.root.length === 1 && parentClasses.properties.length === 1){
+ if(context.impliedValueRule){
+ out = context.impliedValueRule(out, parentClasses.properties[0][0], classes.properties[x][0], value);
+ }
+ }
+
+ // if we have not added this value into a property with the same name already
+ if(!context.hasRootID(child, rootID, propertyName)) {
+ // check the root and property is the same version or if overlapping versions are allowed
+ if( context.isAllowedPropertyVersion( out.typeVersion, propertyVersion ) ){
+ // add the property as an array of properties
+ if(out.properties[propertyName]) {
+ out.properties[propertyName].push(value);
+ } else {
+ out.properties[propertyName] = [value];
+ }
+ // add rootid to node so we can track its use
+ context.appendRootID(child, rootID, propertyName);
+ }
+ }
+
+ x++;
+ }
+
+ context.walkChildren(child, out, ufName, rootID, classes);
+ }
+
+ // if the node has no microformat classes, see if its children have
+ if(classes.root.length === 0 && classes.properties.length === 0) {
+ context.walkChildren(child, out, ufName, rootID, classes);
+ }
+
+ // if the node is a child root add it to the children tree
+ if(classes.root.length > 0 && classes.properties.length === 0) {
+
+ // create object with type, property and value
+ rootItem = context.createUfObject(
+ classes.root,
+ classes.typeVersion,
+ modules.text.parse(this.document, child, context.options.textFormat)
+ );
+
+ // add the microformat as an array of properties
+ if(!out.children){
+ out.children = [];
+ }
+
+ if(!context.hasRootID(child, rootID, 'child-root')) {
+ out.children.push( rootItem );
+ context.appendRootID(child, rootID, 'child-root');
+ context.rootID++;
+ }
+
+ x = 0;
+ i = rootItem.type.length;
+ itemRootID = context.rootID;
+ while(x < i) {
+ context.walkChildren(child, rootItem, rootItem.type, itemRootID, classes);
+ x++;
+ }
+ if(this.impliedRules){
+ context.impliedRules(child, rootItem, classes);
+ }
+ context.cleanUfObject( rootItem );
+
+ }
+
+
+
+ y++;
+ }
+
+ },
+
+
+
+
+ /**
+ * gets the value of a property from a node
+ *
+ * @param {DOM Node} node
+ * @param {String} className
+ * @param {Object} uf
+ * @return {String || Object}
+ */
+ getValue: function(node, className, uf) {
+ var value = '';
+
+ if(modules.utils.startWith(className, 'p-')) {
+ value = this.getPValue(node, true);
+ }
+
+ if(modules.utils.startWith(className, 'e-')) {
+ value = this.getEValue(node);
+ }
+
+ if(modules.utils.startWith(className, 'u-')) {
+ value = this.getUValue(node, true);
+ }
+
+ if(modules.utils.startWith(className, 'dt-')) {
+ value = this.getDTValue(node, className, uf, true);
+ }
+ return value;
+ },
+
+
+ /**
+ * gets the value of a node which contains a 'p-' property
+ *
+ * @param {DOM Node} node
+ * @param {Boolean} valueParse
+ * @return {String}
+ */
+ getPValue: function(node, valueParse) {
+ var out = '';
+ if(valueParse) {
+ out = this.getValueClass(node, 'p');
+ }
+
+ if(!out && valueParse) {
+ out = this.getValueTitle(node);
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['data','input'], 'value');
+ }
+
+ if(node.name === 'br' || node.name === 'hr') {
+ out = '';
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['img', 'area'], 'alt');
+ }
+
+ if(!out) {
+ out = modules.text.parse(this.document, node, this.options.textFormat);
+ }
+
+ return(out) ? out : '';
+ },
+
+
+ /**
+ * gets the value of a node which contains the 'e-' property
+ *
+ * @param {DOM Node} node
+ * @return {Object}
+ */
+ getEValue: function(node) {
+
+ var out = {value: '', html: ''};
+
+ this.expandURLs(node, 'src', this.options.baseUrl);
+ this.expandURLs(node, 'href', this.options.baseUrl);
+
+ out.value = modules.text.parse(this.document, node, this.options.textFormat);
+ out.html = modules.html.parse(node);
+
+ return out;
+ },
+
+
+ /**
+ * gets the value of a node which contains the 'u-' property
+ *
+ * @param {DOM Node} node
+ * @param {Boolean} valueParse
+ * @return {String}
+ */
+ getUValue: function(node, valueParse) {
+ var out = '';
+ if(valueParse) {
+ out = this.getValueClass(node, 'u');
+ }
+
+ if(!out && valueParse) {
+ out = this.getValueTitle(node);
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['a', 'area'], 'href');
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['img','audio','video','source'], 'src');
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['object'], 'data');
+ }
+
+ // if we have no protocol separator, turn relative url to absolute url
+ if(out && out !== '' && out.indexOf('://') === -1) {
+ out = modules.url.resolve(out, this.options.baseUrl);
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['data','input'], 'value');
+ }
+
+ if(!out) {
+ out = modules.text.parse(this.document, node, this.options.textFormat);
+ }
+
+ return(out) ? out : '';
+ },
+
+
+ /**
+ * gets the value of a node which contains the 'dt-' property
+ *
+ * @param {DOM Node} node
+ * @param {String} className
+ * @param {Object} uf
+ * @param {Boolean} valueParse
+ * @return {String}
+ */
+ getDTValue: function(node, className, uf, valueParse) {
+ var out = '';
+
+ if(valueParse) {
+ out = this.getValueClass(node, 'dt');
+ }
+
+ if(!out && valueParse) {
+ out = this.getValueTitle(node);
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['time', 'ins', 'del'], 'datetime');
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['abbr'], 'title');
+ }
+
+ if(!out) {
+ out = modules.domUtils.getAttrValFromTagList(node, ['data', 'input'], 'value');
+ }
+
+ if(!out) {
+ out = modules.text.parse(this.document, node, this.options.textFormat);
+ }
+
+ if(out) {
+ if(modules.dates.isDuration(out)) {
+ // just duration
+ return out;
+ } else if(modules.dates.isTime(out)) {
+ // just time or time+timezone
+ if(uf) {
+ uf.times.push([className, modules.dates.parseAmPmTime(out, this.options.dateFormat)]);
+ }
+ return modules.dates.parseAmPmTime(out, this.options.dateFormat);
+ } else {
+ // returns a date - microformat profile
+ if(uf) {
+ uf.dates.push([className, new modules.ISODate(out).toString( this.options.dateFormat )]);
+ }
+ return new modules.ISODate(out).toString( this.options.dateFormat );
+ }
+ } else {
+ return '';
+ }
+ },
+
+
+ /**
+ * appends a new rootid to a given node
+ *
+ * @param {DOM Node} node
+ * @param {String} id
+ * @param {String} propertyName
+ */
+ appendRootID: function(node, id, propertyName) {
+ if(this.hasRootID(node, id, propertyName) === false){
+ var rootids = [];
+ if(modules.domUtils.hasAttribute(node,'rootids')){
+ rootids = modules.domUtils.getAttributeList(node,'rootids');
+ }
+ rootids.push('id' + id + '-' + propertyName);
+ modules.domUtils.setAttribute(node, 'rootids', rootids.join(' '));
+ }
+ },
+
+
+ /**
+ * does a given node already have a rootid
+ *
+ * @param {DOM Node} node
+ * @param {String} id
+ * @param {String} propertyName
+ * @return {Boolean}
+ */
+ hasRootID: function(node, id, propertyName) {
+ var rootids = [];
+ if(!modules.domUtils.hasAttribute(node,'rootids')){
+ return false;
+ } else {
+ rootids = modules.domUtils.getAttributeList(node, 'rootids');
+ return (rootids.indexOf('id' + id + '-' + propertyName) > -1);
+ }
+ },
+
+
+
+ /**
+ * gets the text of any child nodes with a class value
+ *
+ * @param {DOM Node} node
+ * @param {String} propertyName
+ * @return {String || null}
+ */
+ getValueClass: function(node, propertyType) {
+ var context = this,
+ children = [],
+ out = [],
+ child,
+ x,
+ i;
+
+ children = modules.domUtils.getChildren( node );
+
+ x = 0;
+ i = children.length;
+ while(x < i) {
+ child = children[x];
+ var value = null;
+ if(modules.domUtils.hasAttributeValue(child, 'class', 'value')) {
+ switch(propertyType) {
+ case 'p':
+ value = context.getPValue(child, false);
+ break;
+ case 'u':
+ value = context.getUValue(child, false);
+ break;
+ case 'dt':
+ value = context.getDTValue(child, '', null, false);
+ break;
+ }
+ if(value) {
+ out.push(modules.utils.trim(value));
+ }
+ }
+ x++;
+ }
+ if(out.length > 0) {
+ if(propertyType === 'p') {
+ return modules.text.parseText( this.document, out.join(' '), this.options.textFormat);
+ }
+ if(propertyType === 'u') {
+ return out.join('');
+ }
+ if(propertyType === 'dt') {
+ return modules.dates.concatFragments(out,this.options.dateFormat).toString(this.options.dateFormat);
+ }
+ } else {
+ return null;
+ }
+ },
+
+
+ /**
+ * returns a single string of the 'title' attr from all
+ * the child nodes with the class 'value-title'
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ getValueTitle: function(node) {
+ var out = [],
+ items,
+ i,
+ x;
+
+ items = modules.domUtils.getNodesByAttributeValue(node, 'class', 'value-title');
+ x = 0;
+ i = items.length;
+ while(x < i) {
+ if(modules.domUtils.hasAttribute(items[x], 'title')) {
+ out.push(modules.domUtils.getAttribute(items[x], 'title'));
+ }
+ x++;
+ }
+ return out.join('');
+ },
+
+
+ /**
+ * finds out whether a node has h-* class v1 and v2
+ *
+ * @param {DOM Node} node
+ * @return {Boolean}
+ */
+ hasHClass: function(node){
+ var classes = this.getUfClassNames(node);
+ if(classes.root && classes.root.length > 0){
+ return true;
+ }else{
+ return false;
+ }
+ },
+
+
+ /**
+ * get both the root and property class names from a node
+ *
+ * @param {DOM Node} node
+ * @param {Array} ufNameArr
+ * @return {Object}
+ */
+ getUfClassNames: function(node, ufNameArr) {
+ var context = this,
+ out = {
+ 'root': [],
+ 'properties': []
+ },
+ classNames,
+ key,
+ items,
+ item,
+ i,
+ x,
+ z,
+ y,
+ map,
+ prop,
+ propName,
+ v2Name,
+ impiedRel,
+ ufName;
+
+ // don't get classes from excluded list of tags
+ if(modules.domUtils.hasTagName(node, this.excludeTags) === false){
+
+ // find classes for node
+ classNames = modules.domUtils.getAttribute(node, 'class');
+ if(classNames) {
+ items = classNames.split(' ');
+ x = 0;
+ i = items.length;
+ while(x < i) {
+
+ item = modules.utils.trim(items[x]);
+
+ // test for root prefix - v2
+ if(modules.utils.startWith(item, context.rootPrefix)) {
+ if(out.root.indexOf(item) === -1){
+ out.root.push(item);
+ }
+ out.typeVersion = 'v2';
+ }
+
+ // test for property prefix - v2
+ z = context.propertyPrefixes.length;
+ while(z--) {
+ if(modules.utils.startWith(item, context.propertyPrefixes[z])) {
+ out.properties.push([item,'v2']);
+ }
+ }
+
+ // test for mapped root classnames v1
+ for(key in modules.maps) {
+ if(modules.maps.hasOwnProperty(key)) {
+ // only add a root once
+ if(modules.maps[key].root === item && out.root.indexOf(key) === -1) {
+ // if root map has subTree set to true
+ // test to see if we should create a property or root
+ if(modules.maps[key].subTree) {
+ out.properties.push(['p-' + modules.maps[key].root, 'v1']);
+ } else {
+ out.root.push(key);
+ if(!out.typeVersion){
+ out.typeVersion = 'v1';
+ }
+ }
+ }
+ }
+ }
+
+
+ // test for mapped property classnames v1
+ if(ufNameArr){
+ for (var a = 0; a < ufNameArr.length; a++) {
+ ufName = ufNameArr[a];
+ // get mapped property v1 microformat
+ map = context.getMapping(ufName);
+ if(map) {
+ for(key in map.properties) {
+ if (map.properties.hasOwnProperty(key)) {
+
+ prop = map.properties[key];
+ propName = (prop.map) ? prop.map : 'p-' + key;
+
+ if(key === item) {
+ if(prop.uf) {
+ // loop all the classList make sure
+ // 1. this property is a root
+ // 2. that there is not already an equivalent v2 property i.e. url and u-url on the same element
+ y = 0;
+ while(y < i) {
+ v2Name = context.getV2RootName(items[y]);
+ // add new root
+ if(prop.uf.indexOf(v2Name) > -1 && out.root.indexOf(v2Name) === -1) {
+ out.root.push(v2Name);
+ out.typeVersion = 'v1';
+ }
+ y++;
+ }
+ //only add property once
+ if(out.properties.indexOf(propName) === -1) {
+ out.properties.push([propName,'v1']);
+ }
+ } else {
+ if(out.properties.indexOf(propName) === -1) {
+ out.properties.push([propName,'v1']);
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+
+ }
+
+ x++;
+
+ }
+ }
+ }
+
+
+ // finds any alt rel=* mappings for a given node/microformat
+ if(ufNameArr && this.findRelImpied){
+ for (var b = 0; b < ufNameArr.length; b++) {
+ ufName = ufNameArr[b];
+ impiedRel = this.findRelImpied(node, ufName);
+ if(impiedRel && out.properties.indexOf(impiedRel) === -1) {
+ out.properties.push([impiedRel, 'v1']);
+ }
+ }
+ }
+
+
+ //if(out.root.length === 1 && out.properties.length === 1) {
+ // if(out.root[0].replace('h-','') === this.removePropPrefix(out.properties[0][0])) {
+ // out.typeVersion = 'v2';
+ // }
+ //}
+
+ return out;
+ },
+
+
+ /**
+ * given a v1 or v2 root name, return mapping object
+ *
+ * @param {String} name
+ * @return {Object || null}
+ */
+ getMapping: function(name) {
+ var key;
+ for(key in modules.maps) {
+ if(modules.maps[key].root === name || key === name) {
+ return modules.maps[key];
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * given a v1 root name returns a v2 root name i.e. vcard >>> h-card
+ *
+ * @param {String} name
+ * @return {String || null}
+ */
+ getV2RootName: function(name) {
+ var key;
+ for(key in modules.maps) {
+ if(modules.maps[key].root === name) {
+ return key;
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * whether a property is the right microformats version for its root type
+ *
+ * @param {String} typeVersion
+ * @param {String} propertyVersion
+ * @return {Boolean}
+ */
+ isAllowedPropertyVersion: function(typeVersion, propertyVersion){
+ if(this.options.overlappingVersions === true){
+ return true;
+ }else{
+ return (typeVersion === propertyVersion);
+ }
+ },
+
+
+ /**
+ * creates a blank microformats object
+ *
+ * @param {String} name
+ * @param {String} value
+ * @return {Object}
+ */
+ createUfObject: function(names, typeVersion, value) {
+ var out = {};
+
+ // is more than just whitespace
+ if(value && modules.utils.isOnlyWhiteSpace(value) === false) {
+ out.value = value;
+ }
+ // add type i.e. ["h-card", "h-org"]
+ if(modules.utils.isArray(names)) {
+ out.type = names;
+ } else {
+ out.type = [names];
+ }
+ out.properties = {};
+ // metadata properties for parsing
+ out.typeVersion = typeVersion;
+ out.times = [];
+ out.dates = [];
+ out.altValue = null;
+
+ return out;
+ },
+
+
+ /**
+ * removes unwanted microformats property before output
+ *
+ * @param {Object} microformat
+ */
+ cleanUfObject: function( microformat ) {
+ delete microformat.times;
+ delete microformat.dates;
+ delete microformat.typeVersion;
+ delete microformat.altValue;
+ return microformat;
+ },
+
+
+
+ /**
+ * removes microformat property prefixes from text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ removePropPrefix: function(text) {
+ var i;
+
+ i = this.propertyPrefixes.length;
+ while(i--) {
+ var prefix = this.propertyPrefixes[i];
+ if(modules.utils.startWith(text, prefix)) {
+ text = text.substr(prefix.length);
+ }
+ }
+ return text;
+ },
+
+
+ /**
+ * expands all relative URLs to absolute ones where it can
+ *
+ * @param {DOM Node} node
+ * @param {String} attrName
+ * @param {String} baseUrl
+ */
+ expandURLs: function(node, attrName, baseUrl){
+ var i,
+ nodes,
+ attr;
+
+ nodes = modules.domUtils.getNodesByAttribute(node, attrName);
+ i = nodes.length;
+ while (i--) {
+ try{
+ // the url parser can blow up if the format is not right
+ attr = modules.domUtils.getAttribute(nodes[i], attrName);
+ if(attr && attr !== '' && baseUrl !== '' && attr.indexOf('://') === -1) {
+ //attr = urlParser.resolve(baseUrl, attr);
+ attr = modules.url.resolve(attr, baseUrl);
+ modules.domUtils.setAttribute(nodes[i], attrName, attr);
+ }
+ }catch(err){
+ // do nothing - convert only the urls we can, leave the rest as they are
+ }
+ }
+ },
+
+
+
+ /**
+ * merges passed and default options -single level clone of properties
+ *
+ * @param {Object} options
+ */
+ mergeOptions: function(options) {
+ var key;
+ for(key in options) {
+ if(options.hasOwnProperty(key)) {
+ this.options[key] = options[key];
+ }
+ }
+ },
+
+
+ /**
+ * removes all rootid attributes
+ *
+ * @param {DOM Node} rootNode
+ */
+ removeRootIds: function(rootNode){
+ var arr,
+ i;
+
+ arr = modules.domUtils.getNodesByAttribute(rootNode, 'rootids');
+ i = arr.length;
+ while(i--) {
+ modules.domUtils.removeAttribute(arr[i],'rootids');
+ }
+ },
+
+
+ /**
+ * removes all changes made to the DOM
+ *
+ * @param {DOM Node} rootNode
+ */
+ clearUpDom: function(rootNode){
+ if(this.removeIncludes){
+ this.removeIncludes(rootNode);
+ }
+ this.removeRootIds(rootNode);
+ }
+
+
+ };
+
+
+ modules.Parser.prototype.constructor = modules.Parser;
+
+ return modules;
+
+} (Modules || {}));
+
+
+
diff --git a/toolkit/components/microformats/test/lib/text.js b/toolkit/components/microformats/test/lib/text.js
new file mode 100644
index 0000000000..fe94dae0a3
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/text.js
@@ -0,0 +1,151 @@
+/*
+ text
+ Extracts text string from DOM nodes. Was created to extract text in a whitespace-normalized form.
+ It works like a none-CSS aware version of IE's innerText function. DO NOT replace this module
+ with functions such as textContent as it will reduce the quality of data provided to the API user.
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ Dependencies utilities.js, domutils.js
+*/
+
+
+var Modules = (function (modules) {
+
+
+ modules.text = {
+
+ // normalised or whitespace or whitespacetrimmed
+ textFormat: 'whitespacetrimmed',
+
+ // block level tags, used to add line returns
+ blockLevelTags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'hr', 'pre', 'table',
+ 'address', 'article', 'aside', 'blockquote', 'caption', 'col', 'colgroup', 'dd', 'div',
+ 'dt', 'dir', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'hr',
+ 'li', 'map', 'menu', 'nav', 'optgroup', 'option', 'section', 'tbody', 'testarea',
+ 'tfoot', 'th', 'thead', 'tr', 'td', 'ul', 'ol', 'dl', 'details'],
+
+ // tags to exclude
+ excludeTags: ['noframe', 'noscript', 'template', 'script', 'style', 'frames', 'frameset'],
+
+
+ /**
+ * parses the text from the DOM Node
+ *
+ * @param {DOM Node} node
+ * @param {String} textFormat
+ * @return {String}
+ */
+ parse: function(doc, node, textFormat){
+ var out;
+ this.textFormat = (textFormat)? textFormat : this.textFormat;
+ if(this.textFormat === 'normalised'){
+ out = this.walkTreeForText( node );
+ if(out !== undefined){
+ return this.normalise( doc, out );
+ }else{
+ return '';
+ }
+ }else{
+ return this.formatText( doc, modules.domUtils.textContent(node), this.textFormat );
+ }
+ },
+
+
+ /**
+ * parses the text from a html string
+ *
+ * @param {DOM Document} doc
+ * @param {String} text
+ * @param {String} textFormat
+ * @return {String}
+ */
+ parseText: function( doc, text, textFormat ){
+ var node = modules.domUtils.createNodeWithText( 'div', text );
+ return this.parse( doc, node, textFormat );
+ },
+
+
+ /**
+ * parses the text from a html string - only for whitespace or whitespacetrimmed formats
+ *
+ * @param {String} text
+ * @param {String} textFormat
+ * @return {String}
+ */
+ formatText: function( doc, text, textFormat ){
+ this.textFormat = (textFormat)? textFormat : this.textFormat;
+ if(text){
+ var out = '',
+ regex = /(<([^>]+)>)/ig;
+
+ out = text.replace(regex, '');
+ if(this.textFormat === 'whitespacetrimmed') {
+ out = modules.utils.trimWhitespace( out );
+ }
+
+ //return entities.decode( out, 2 );
+ return modules.domUtils.decodeEntities( doc, out );
+ }else{
+ return '';
+ }
+ },
+
+
+ /**
+ * normalises whitespace in given text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ normalise: function( doc, text ){
+ text = text.replace( /&nbsp;/g, ' ') ; // exchanges html entity for space into space char
+ text = modules.utils.collapseWhiteSpace( text ); // removes linefeeds, tabs and addtional spaces
+ text = modules.domUtils.decodeEntities( doc, text ); // decode HTML entities
+ text = text.replace( '–', '-' ); // correct dash decoding
+ return modules.utils.trim( text );
+ },
+
+
+ /**
+ * walks DOM tree parsing the text from DOM Nodes
+ *
+ * @param {DOM Node} node
+ * @return {String}
+ */
+ walkTreeForText: function( node ) {
+ var out = '',
+ j = 0;
+
+ if(node.tagName && this.excludeTags.indexOf( node.tagName.toLowerCase() ) > -1){
+ return out;
+ }
+
+ // if node is a text node get its text
+ if(node.nodeType && node.nodeType === 3){
+ out += modules.domUtils.getElementText( node );
+ }
+
+ // get the text of the child nodes
+ if(node.childNodes && node.childNodes.length > 0){
+ for (j = 0; j < node.childNodes.length; j++) {
+ var text = this.walkTreeForText( node.childNodes[j] );
+ if(text !== undefined){
+ out += text;
+ }
+ }
+ }
+
+ // if it's a block level tag add an additional space at the end
+ if(node.tagName && this.blockLevelTags.indexOf( node.tagName.toLowerCase() ) !== -1){
+ out += ' ';
+ }
+
+ return (out === '')? undefined : out ;
+ }
+
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/url.js b/toolkit/components/microformats/test/lib/url.js
new file mode 100644
index 0000000000..81ed9f29e5
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/url.js
@@ -0,0 +1,73 @@
+/*
+ url
+ Where possible use the modern window.URL API if its not available use the DOMParser method.
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+
+ modules.url = {
+
+
+ /**
+ * creates DOM objects needed to resolve URLs
+ */
+ init: function(){
+ //this._domParser = new DOMParser();
+ this._domParser = modules.domUtils.getDOMParser();
+ // do not use a head tag it does not work with IE9
+ this._html = '<base id="base" href=""></base><a id="link" href=""></a>';
+ this._nodes = this._domParser.parseFromString( this._html, 'text/html' );
+ this._baseNode = modules.domUtils.getElementById(this._nodes,'base');
+ this._linkNode = modules.domUtils.getElementById(this._nodes,'link');
+ },
+
+
+ /**
+ * resolves url to absolute version using baseUrl
+ *
+ * @param {String} url
+ * @param {String} baseUrl
+ * @return {String}
+ */
+ resolve: function(url, baseUrl) {
+ // use modern URL web API where we can
+ if(modules.utils.isString(url) && modules.utils.isString(baseUrl) && url.indexOf('://') === -1){
+ // this try catch is required as IE has an URL object but no constuctor support
+ // http://glennjones.net/articles/the-problem-with-window-url
+ try {
+ var resolved = new URL(url, baseUrl).toString();
+ // deal with early Webkit not throwing an error - for Safari
+ if(resolved === '[object URL]'){
+ resolved = URI.resolve(baseUrl, url);
+ }
+ return resolved;
+ }catch(e){
+ // otherwise fallback to DOM
+ if(this._domParser === undefined){
+ this.init();
+ }
+
+ // do not use setAttribute it does not work with IE9
+ this._baseNode.href = baseUrl;
+ this._linkNode.href = url;
+
+ // dont use getAttribute as it returns orginal value not resolved
+ return this._linkNode.href;
+ }
+ }else{
+ if(modules.utils.isString(url)){
+ return url;
+ }
+ return '';
+ }
+ },
+
+ };
+
+ return modules;
+
+} (Modules || {}));
diff --git a/toolkit/components/microformats/test/lib/utilities.js b/toolkit/components/microformats/test/lib/utilities.js
new file mode 100644
index 0000000000..c547148113
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/utilities.js
@@ -0,0 +1,206 @@
+/*
+ Utilities
+
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var Modules = (function (modules) {
+
+ modules.utils = {
+
+ /**
+ * is the object a string
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isString: function( obj ) {
+ return typeof( obj ) === 'string';
+ },
+
+ /**
+ * is the object a number
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isNumber: function( obj ) {
+ return !isNaN(parseFloat( obj )) && isFinite( obj );
+ },
+
+
+ /**
+ * is the object an array
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isArray: function( obj ) {
+ return obj && !( obj.propertyIsEnumerable( 'length' ) ) && typeof obj === 'object' && typeof obj.length === 'number';
+ },
+
+
+ /**
+ * is the object a function
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ isFunction: function(obj) {
+ return !!(obj && obj.constructor && obj.call && obj.apply);
+ },
+
+
+ /**
+ * does the text start with a test string
+ *
+ * @param {String} text
+ * @param {String} test
+ * @return {Boolean}
+ */
+ startWith: function( text, test ) {
+ return(text.indexOf(test) === 0);
+ },
+
+
+ /**
+ * removes spaces at front and back of text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ trim: function( text ) {
+ if(text && this.isString(text)){
+ return (text.trim())? text.trim() : text.replace(/^\s+|\s+$/g, '');
+ }else{
+ return '';
+ }
+ },
+
+
+ /**
+ * replaces a character in text
+ *
+ * @param {String} text
+ * @param {Int} index
+ * @param {String} character
+ * @return {String}
+ */
+ replaceCharAt: function( text, index, character ) {
+ if(text && text.length > index){
+ return text.substr(0, index) + character + text.substr(index+character.length);
+ }else{
+ return text;
+ }
+ },
+
+
+ /**
+ * removes whitespace, tabs and returns from start and end of text
+ *
+ * @param {String} text
+ * @return {String}
+ */
+ trimWhitespace: function( text ){
+ if(text && text.length){
+ var i = text.length,
+ x = 0;
+
+ // turn all whitespace chars at end into spaces
+ while (i--) {
+ if(this.isOnlyWhiteSpace(text[i])){
+ text = this.replaceCharAt( text, i, ' ' );
+ }else{
+ break;
+ }
+ }
+
+ // turn all whitespace chars at start into spaces
+ i = text.length;
+ while (x < i) {
+ if(this.isOnlyWhiteSpace(text[x])){
+ text = this.replaceCharAt( text, i, ' ' );
+ }else{
+ break;
+ }
+ x++;
+ }
+ }
+ return this.trim(text);
+ },
+
+
+ /**
+ * does text only contain whitespace characters
+ *
+ * @param {String} text
+ * @return {Boolean}
+ */
+ isOnlyWhiteSpace: function( text ){
+ return !(/[^\t\n\r ]/.test( text ));
+ },
+
+
+ /**
+ * removes whitespace from text (leaves a single space)
+ *
+ * @param {String} text
+ * @return {Sring}
+ */
+ collapseWhiteSpace: function( text ){
+ return text.replace(/[\t\n\r ]+/g, ' ');
+ },
+
+
+ /**
+ * does an object have any of its own properties
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ */
+ hasProperties: function( obj ) {
+ var key;
+ for(key in obj) {
+ if( obj.hasOwnProperty( key ) ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+
+ /**
+ * a sort function - to sort objects in an array by a given property
+ *
+ * @param {String} property
+ * @param {Boolean} reverse
+ * @return {Int}
+ */
+ sortObjects: function(property, reverse) {
+ reverse = (reverse) ? -1 : 1;
+ return function (a, b) {
+ a = a[property];
+ b = b[property];
+ if (a < b) {
+ return reverse * -1;
+ }
+ if (a > b) {
+ return reverse * 1;
+ }
+ return 0;
+ };
+ }
+
+ };
+
+ return modules;
+
+} (Modules || {}));
+
+
+
+
+
+
+
diff --git a/toolkit/components/microformats/test/lib/version.js b/toolkit/components/microformats/test/lib/version.js
new file mode 100644
index 0000000000..371272cff9
--- /dev/null
+++ b/toolkit/components/microformats/test/lib/version.js
@@ -0,0 +1 @@
+ modules.version = '1.4.0';
diff --git a/toolkit/components/microformats/test/marionette/microformats_tester.py b/toolkit/components/microformats/test/marionette/microformats_tester.py
new file mode 100644
index 0000000000..69fcb60ba6
--- /dev/null
+++ b/toolkit/components/microformats/test/marionette/microformats_tester.py
@@ -0,0 +1,170 @@
+import threading
+import SimpleHTTPServer
+import SocketServer
+import BaseHTTPServer
+import urllib
+import urlparse
+import os
+import posixpath
+
+from marionette_driver.errors import NoSuchElementException
+from marionette_harness import MarionetteTestCase
+
+DEBUG = True
+
+# Example taken from mozilla-central/browser/components/loop/
+
+# XXX Once we're on a branch with bug 993478 landed, we may want to get
+# rid of this HTTP server and just use the built-in one from Marionette,
+# since there will less code to maintain, and it will be faster. We'll
+# need to consider whether this code wants to be shared with WebDriver tests
+# for other browsers, though.
+
+class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
+ BaseHTTPServer.HTTPServer):
+ pass
+
+
+class HttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler, object):
+ def __init__(self, *args):
+ # set root to toolkit/components/microformats/
+ self.root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.normpath(__file__))))
+ super(HttpRequestHandler, self).__init__(*args)
+
+ def log_message(self, format, *args, **kwargs):
+ if DEBUG:
+ super(HttpRequestHandler, self).log_message(format, *args, **kwargs)
+
+ def translate_path(self, path):
+ """Translate a /-separated PATH to the local filename syntax.
+
+ Components that mean special things to the local file system
+ (e.g. drive or directory names) are ignored. (XXX They should
+ probably be diagnosed.)
+
+ """
+ # abandon query parameters
+ path = path.split('?',1)[0]
+ path = path.split('#',1)[0]
+ # Don't forget explicit trailing slash when normalizing. Issue17324
+ trailing_slash = path.rstrip().endswith('/')
+ path = posixpath.normpath(urllib.unquote(path))
+ words = path.split('/')
+ words = filter(None, words)
+ path = self.root
+ for word in words:
+ drive, word = os.path.splitdrive(word)
+ head, word = os.path.split(word)
+ if word in (os.curdir, os.pardir): continue
+ path = os.path.join(path, word)
+ if trailing_slash:
+ path += '/'
+ return path
+
+class BaseTestFrontendUnits(MarionetteTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(BaseTestFrontendUnits, cls).setUpClass()
+
+ # Port 0 means to select an arbitrary unused port
+ cls.server = ThreadingSimpleServer(('', 0), HttpRequestHandler)
+ cls.ip, cls.port = cls.server.server_address
+
+ cls.server_thread = threading.Thread(target=cls.server.serve_forever)
+ cls.server_thread.daemon = False
+ cls.server_thread.start()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.server.shutdown()
+ cls.server_thread.join()
+
+ # make sure everything gets GCed so it doesn't interfere with the next
+ # test class. Even though this is class-static, each subclass gets
+ # its own instance of this stuff.
+ cls.server_thread = None
+ cls.server = None
+
+ def setUp(self):
+ super(BaseTestFrontendUnits, self).setUp()
+
+ # Unfortunately, enforcing preferences currently comes with the side
+ # effect of launching and restarting the browser before running the
+ # real functional tests. Bug 1048554 has been filed to track this.
+ #
+ # Note: when e10s is enabled by default, this pref can go away. The automatic
+ # restart will also go away if this is still the only pref set here.
+ self.marionette.enforce_gecko_prefs({
+ "browser.tabs.remote.autostart": True
+ })
+
+ # This extends the timeout for find_element. We need this as the tests
+ # take an amount of time to run after loading, which we have to wait for.
+ self.marionette.timeout.implicit = 120
+
+ self.marionette.timeout.page_load = 120
+
+ # srcdir_path should be the directory relative to this file.
+ def set_server_prefix(self, srcdir_path):
+ self.server_prefix = urlparse.urljoin("http://localhost:" + str(self.port),
+ srcdir_path)
+
+ def check_page(self, page):
+
+ self.marionette.navigate(urlparse.urljoin(self.server_prefix, page))
+ try:
+ self.marionette.find_element("id", 'complete')
+ except NoSuchElementException:
+ fullPageUrl = urlparse.urljoin(self.relPath, page)
+
+ details = "%s: 1 failure encountered\n%s" % \
+ (fullPageUrl,
+ self.get_failure_summary(
+ fullPageUrl, "Waiting for Completion",
+ "Could not find the test complete indicator"))
+
+ raise AssertionError(details)
+
+ fail_node = self.marionette.find_element("css selector",
+ '.failures > em')
+ if fail_node.text == "0":
+ return
+
+ # This may want to be in a more general place triggerable by an env
+ # var some day if it ends up being something we need often:
+ #
+ # If you have browser-based unit tests which work when loaded manually
+ # but not from marionette, uncomment the two lines below to break
+ # on failing tests, so that the browsers won't be torn down, and you
+ # can use the browser debugging facilities to see what's going on.
+ #from ipdb import set_trace
+ #set_trace()
+
+ raise AssertionError(self.get_failure_details(page))
+
+ def get_failure_summary(self, fullPageUrl, testName, testError):
+ return "TEST-UNEXPECTED-FAIL | %s | %s - %s" % (fullPageUrl, testName, testError)
+
+ def get_failure_details(self, page):
+ fail_nodes = self.marionette.find_elements("css selector",
+ '.test.fail')
+ fullPageUrl = urlparse.urljoin(self.relPath, page)
+
+ details = ["%s: %d failure(s) encountered:" % (fullPageUrl, len(fail_nodes))]
+
+ for node in fail_nodes:
+ errorText = node.find_element("css selector", '.error').text
+
+ # We have to work our own failure message here, as we could be reporting multiple failures.
+ # XXX Ideally we'd also give the full test tree for <test name> - that requires walking
+ # up the DOM tree.
+
+ # Format: TEST-UNEXPECTED-FAIL | <filename> | <test name> - <test error>
+ details.append(
+ self.get_failure_summary(page,
+ node.find_element("tag name", 'h2').text.split("\n")[0],
+ errorText.split("\n")[0]))
+ details.append(
+ errorText)
+ return "\n".join(details)
diff --git a/toolkit/components/microformats/test/marionette/test_interface.py b/toolkit/components/microformats/test/marionette/test_interface.py
new file mode 100644
index 0000000000..aa34ef1c28
--- /dev/null
+++ b/toolkit/components/microformats/test/marionette/test_interface.py
@@ -0,0 +1,17 @@
+# Code example copied from mozilla-central/browser/components/loop/
+# need to get this dir in the path so that we make the import work
+import os
+import sys
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'marionette'))
+
+from microformats_tester import BaseTestFrontendUnits
+
+
+class TestInterfaceUnits(BaseTestFrontendUnits):
+
+ def setUp(self):
+ super(TestInterfaceUnits, self).setUp()
+ self.set_server_prefix("/test/interface-tests/")
+
+ def test_units(self):
+ self.check_page("index.html")
diff --git a/toolkit/components/microformats/test/marionette/test_modules.py b/toolkit/components/microformats/test/marionette/test_modules.py
new file mode 100644
index 0000000000..f2291259cf
--- /dev/null
+++ b/toolkit/components/microformats/test/marionette/test_modules.py
@@ -0,0 +1,17 @@
+# Code example copied from mozilla-central/browser/components/loop/
+# need to get this dir in the path so that we make the import work
+import os
+import sys
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'marionette'))
+
+from microformats_tester import BaseTestFrontendUnits
+
+
+class TestModulesUnits(BaseTestFrontendUnits):
+
+ def setUp(self):
+ super(TestModulesUnits, self).setUp()
+ self.set_server_prefix("/test/module-tests/")
+
+ def test_units(self):
+ self.check_page("index.html")
diff --git a/toolkit/components/microformats/test/marionette/test_standards.py b/toolkit/components/microformats/test/marionette/test_standards.py
new file mode 100644
index 0000000000..ec688fe78a
--- /dev/null
+++ b/toolkit/components/microformats/test/marionette/test_standards.py
@@ -0,0 +1,17 @@
+# Code example copied from mozilla-central/browser/components/loop/
+# need to get this dir in the path so that we make the import work
+import os
+import sys
+sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'marionette')))
+
+from microformats_tester import BaseTestFrontendUnits
+
+
+class TestStandardsUnits(BaseTestFrontendUnits):
+
+ def setUp(self):
+ super(TestStandardsUnits, self).setUp()
+ self.set_server_prefix("/test/standards-tests/")
+
+ def test_units(self):
+ self.check_page("index.html")
diff --git a/toolkit/components/microformats/test/module-tests/dates-test.js b/toolkit/components/microformats/test/module-tests/dates-test.js
new file mode 100644
index 0000000000..e5e034190e
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/dates-test.js
@@ -0,0 +1,113 @@
+/*
+Unit test for dates
+*/
+
+assert = chai.assert;
+
+// Tests the private Modules.dates object
+// Modules.dates is unit tested as it has an interface access by other modules
+
+
+describe('Modules.dates', function() {
+
+
+ it('hasAM', function(){
+ assert.isTrue( Modules.dates.hasAM( '5am' ) );
+ assert.isTrue( Modules.dates.hasAM( '5AM' ) );
+ assert.isTrue( Modules.dates.hasAM( '5 am' ) );
+ assert.isTrue( Modules.dates.hasAM( '5a.m.' ) );
+ assert.isTrue( Modules.dates.hasAM( '5:20 a.m.' ) );
+ assert.isFalse( Modules.dates.hasAM( '5pm' ) );
+ });
+
+
+ it('hasPM', function(){
+ assert.isTrue( Modules.dates.hasPM( '5pm' ) );
+ assert.isTrue( Modules.dates.hasPM( '5PM' ) );
+ assert.isTrue( Modules.dates.hasPM( '5 pm' ) );
+ assert.isTrue( Modules.dates.hasPM( '5p.m.' ) );
+ assert.isTrue( Modules.dates.hasPM( '5:20 p.m.' ) );
+ assert.isFalse( Modules.dates.hasPM( '5am' ) );
+ });
+
+
+ it('removeAMPM', function(){
+ assert.equal( Modules.dates.removeAMPM( '5pm' ), '5' );
+ assert.equal( Modules.dates.removeAMPM( '5 pm' ), '5 ' );
+ assert.equal( Modules.dates.removeAMPM( '5p.m.' ), '5' );
+ assert.equal( Modules.dates.removeAMPM( '5am' ), '5' );
+ assert.equal( Modules.dates.removeAMPM( '5a.m.' ), '5' );
+ assert.equal( Modules.dates.removeAMPM( '5' ), '5' );
+ });
+
+
+ it('isDuration', function(){
+ assert.isTrue( Modules.dates.isDuration( 'PY17M' ) );
+ assert.isTrue( Modules.dates.isDuration( 'PW12' ) );
+ assert.isTrue( Modules.dates.isDuration( 'P0.5Y' ) );
+ assert.isTrue( Modules.dates.isDuration( 'P3Y6M4DT12H30M5S' ) );
+ assert.isFalse( Modules.dates.isDuration( '2015-01-23T13:45' ) );
+ assert.isFalse( Modules.dates.isDuration( '2015-01-23 13:45' ) );
+ assert.isFalse( Modules.dates.isDuration( '20150123T1345' ) );
+ });
+
+
+ it('isTime', function(){
+ assert.isTrue( Modules.dates.isTime( '8:43' ) );
+ assert.isTrue( Modules.dates.isTime( '08:43' ) );
+ assert.isTrue( Modules.dates.isTime( '15:23:00:0567' ) );
+ assert.isTrue( Modules.dates.isTime( '10:34pm' ) );
+ assert.isTrue( Modules.dates.isTime( '10:34 p.m.' ) );
+ assert.isTrue( Modules.dates.isTime( '+01:00:00' ) );
+ assert.isTrue( Modules.dates.isTime( '-02:00' ) );
+ assert.isTrue( Modules.dates.isTime( 'z15:00' ) );
+ assert.isTrue( Modules.dates.isTime( '0843' ) );
+ assert.isFalse( Modules.dates.isTime( 'P3Y6M4DT12H30M5S' ) );
+ assert.isFalse( Modules.dates.isTime( '2015-01-23T13:45' ) );
+ assert.isFalse( Modules.dates.isTime( '2015-01-23 13:45' ) );
+ assert.isFalse( Modules.dates.isTime( '20150123T1345' ) );
+ assert.isFalse( Modules.dates.isTime( 'abc' ) );
+ assert.isFalse( Modules.dates.isTime( '12345' ) );
+ });
+
+
+ it('parseAmPmTime', function(){
+ assert.equal( Modules.dates.parseAmPmTime( '5am' ), '05' );
+ assert.equal( Modules.dates.parseAmPmTime( '12pm' ), '12' );
+ assert.equal( Modules.dates.parseAmPmTime( '5a.m.' ), '05' );
+ assert.equal( Modules.dates.parseAmPmTime( '5pm' ), '17' );
+ assert.equal( Modules.dates.parseAmPmTime( '5:34pm' ), '17:34' );
+ assert.equal( Modules.dates.parseAmPmTime( '5:04pm' ), '17:04' );
+ assert.equal( Modules.dates.parseAmPmTime( '05:34:00' ), '05:34:00' );
+ assert.equal( Modules.dates.parseAmPmTime( '05:34:00' ), '05:34:00' );
+ assert.equal( Modules.dates.parseAmPmTime( '1:52:04pm' ), '13:52:04' );
+ });
+
+
+ it('dateTimeUnion', function(){
+ assert.equal( Modules.dates.dateTimeUnion( '2015-01-23', '05:34:00', 'HTML5' ).toString('HTML5'), '2015-01-23 05:34:00' );
+ assert.equal( Modules.dates.dateTimeUnion( '2015-01-23', '05:34', 'HTML5' ).toString('HTML5'), '2015-01-23 05:34' );
+ assert.equal( Modules.dates.dateTimeUnion( '2015-01-23', '05', 'HTML5' ).toString('HTML5'), '2015-01-23 05' );
+ assert.equal( Modules.dates.dateTimeUnion( '2009-06-26T19:00', '2200', 'HTML5' ).toString('HTML5'), '2009-06-26 22:00' );
+
+ assert.equal( Modules.dates.dateTimeUnion( '2015-01-23', '', 'HTML5' ).toString('HTML5'), '2015-01-23' );
+ assert.equal( Modules.dates.dateTimeUnion( '', '', 'HTML5' ).toString('HTML5'), '' );
+ });
+
+
+ it('concatFragments', function(){
+ assert.equal( Modules.dates.concatFragments( ['2015-01-23', '05:34:00'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34:00' );
+ assert.equal( Modules.dates.concatFragments( ['05:34:00', '2015-01-23'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34:00' );
+ assert.equal( Modules.dates.concatFragments( ['2015-01-23T05:34:00'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34:00' );
+ assert.equal( Modules.dates.concatFragments( ['2015-01-23', '05:34:00', 'z'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34:00Z' );
+ assert.equal( Modules.dates.concatFragments( ['2015-01-23', '05:34', '-01'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34-01' );
+ assert.equal( Modules.dates.concatFragments( ['2015-01-23', '05:34', '-01:00'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34-01:00' );
+ assert.equal( Modules.dates.concatFragments( ['2015-01-23', '05:34-01:00'], 'HTML5' ).toString('HTML5'), '2015-01-23 05:34-01:00' );
+
+ });
+
+
+
+
+
+});
diff --git a/toolkit/components/microformats/test/module-tests/domutils-test.js b/toolkit/components/microformats/test/module-tests/domutils-test.js
new file mode 100644
index 0000000000..5d3f036a9f
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/domutils-test.js
@@ -0,0 +1,206 @@
+/*
+Unit test for domutils
+*/
+
+assert = chai.assert;
+
+
+// Tests the private Modules.domUtils object
+// Modules.domUtils is unit tested as it has an interface access by other modules
+
+
+describe('Modules.domutils', function() {
+
+
+ it('ownerDocument', function(){
+ var node = document.createElement('div');
+ assert.equal( Modules.domUtils.ownerDocument( node ).nodeType, 9);
+ });
+
+
+ it('innerHTML', function(){
+ var html = '<a href="http://glennjones.net">Glenn Jones</a>',
+ node = document.createElement('div');
+
+ node.innerHTML = html;
+ assert.equal( Modules.domUtils.innerHTML( node ), html );
+ });
+
+
+ it('hasAttribute', function(){
+ var node = document.createElement('a');
+
+ node.href = 'http://glennjones.net';
+ assert.isTrue( Modules.domUtils.hasAttribute( node, 'href' ) );
+ assert.isFalse( Modules.domUtils.hasAttribute( node, 'class' ) );
+ });
+
+
+ it('hasAttributeValue', function(){
+ var node = document.createElement('a');
+
+ node.href = 'http://glennjones.net';
+ assert.isTrue( Modules.domUtils.hasAttributeValue( node, 'href', 'http://glennjones.net' ) );
+ assert.isFalse( Modules.domUtils.hasAttributeValue( node, 'href', 'http://example.net' ) );
+ assert.isFalse( Modules.domUtils.hasAttributeValue( node, 'class', 'test' ) );
+ });
+
+
+ it('getAttribute', function(){
+ var node = document.createElement('a');
+
+ node.href = 'http://glennjones.net';
+ assert.equal( Modules.domUtils.getAttribute( node, 'href' ), 'http://glennjones.net' );
+ });
+
+
+ it('setAttribute', function(){
+ var node = document.createElement('a');
+
+ Modules.domUtils.setAttribute(node, 'href', 'http://glennjones.net')
+ assert.equal( Modules.domUtils.getAttribute( node, 'href' ), 'http://glennjones.net' );
+ });
+
+
+ it('removeAttribute', function(){
+ var node = document.createElement('a');
+
+ node.href = 'http://glennjones.net';
+ Modules.domUtils.removeAttribute(node, 'href')
+ assert.isFalse( Modules.domUtils.hasAttribute( node, 'href' ) );
+ });
+
+
+ it('getAttributeList', function(){
+ var node = document.createElement('a');
+
+ node.rel = 'next';
+ assert.deepEqual( Modules.domUtils.getAttributeList( node, 'rel'), ['next'] );
+ node.rel = 'next bookmark';
+ assert.deepEqual( Modules.domUtils.getAttributeList( node, 'rel'), ['next','bookmark'] );
+ });
+
+
+ it('hasAttributeValue', function(){
+ var node = document.createElement('a');
+
+ node.href = 'http://glennjones.net';
+ node.rel = 'next bookmark';
+ assert.isTrue( Modules.domUtils.hasAttributeValue( node, 'href', 'http://glennjones.net' ) );
+ assert.isFalse( Modules.domUtils.hasAttributeValue( node, 'href', 'http://codebits.glennjones.net' ) );
+ assert.isFalse( Modules.domUtils.hasAttributeValue( node, 'class', 'p-name' ) );
+ assert.isTrue( Modules.domUtils.hasAttributeValue( node, 'rel', 'bookmark' ) );
+ assert.isFalse( Modules.domUtils.hasAttributeValue( node, 'rel', 'previous' ) );
+ });
+
+
+ it('getNodesByAttribute', function(){
+ var node = document.createElement('ul');
+ node.innerHTML = '<li class="h-card">one</li><li>two</li><li class="h-card">three</li>';
+
+ assert.equal( Modules.domUtils.getNodesByAttribute( node, 'class' ).length, 2 );
+ assert.equal( Modules.domUtils.getNodesByAttribute( node, 'href' ).length, 0 );
+ });
+
+
+ it('getNodesByAttributeValue', function(){
+ var node = document.createElement('ul');
+ node.innerHTML = '<li class="h-card">one</li><li>two</li><li class="h-card">three</li><li class="p-name">four</li>';
+
+ assert.equal( Modules.domUtils.getNodesByAttributeValue( node, 'class', 'h-card' ).length, 2 );
+ assert.equal( Modules.domUtils.getNodesByAttributeValue( node, 'class', 'p-name' ).length, 1 );
+ assert.equal( Modules.domUtils.getNodesByAttributeValue( node, 'class', 'u-url' ).length, 0 );
+ });
+
+
+ it('getAttrValFromTagList', function(){
+ var node = document.createElement('a');
+
+ node.href = 'http://glennjones.net';
+
+ assert.equal( Modules.domUtils.getAttrValFromTagList( node, ['a','area'], 'href' ), 'http://glennjones.net' );
+ assert.equal( Modules.domUtils.getAttrValFromTagList( node, ['a','area'], 'class' ), null );
+ assert.equal( Modules.domUtils.getAttrValFromTagList( node, ['p'], 'href' ), null );
+ });
+
+
+ it('getSingleDescendant', function(){
+ var html = '<a class="u-url" href="http://glennjones.net">Glenn Jones</a>',
+ node = document.createElement('div');
+
+ node.innerHTML = html,
+
+ // one instance of a element
+ assert.equal( Modules.domUtils.getSingleDescendant( node ).outerHTML, html );
+
+ // two instances of a element
+ node.appendChild(document.createElement('a'));
+ assert.equal( Modules.domUtils.getSingleDescendant( node ), null );
+
+ });
+
+
+ it('getSingleDescendantOfType', function(){
+ var html = '<a class="u-url" href="http://glennjones.net">Glenn Jones</a>',
+ node = document.createElement('div');
+
+ node.innerHTML = html,
+
+ // one instance of a element
+ assert.equal( Modules.domUtils.getSingleDescendantOfType( node, ['a', 'link']).outerHTML, html );
+ assert.equal( Modules.domUtils.getSingleDescendantOfType( node, ['img','area']), null );
+
+ node.appendChild(document.createElement('p'));
+ assert.equal( Modules.domUtils.getSingleDescendantOfType( node, ['a', 'link']).outerHTML, html );
+
+ // two instances of a element
+ node.appendChild(document.createElement('a'));
+ assert.equal( Modules.domUtils.getSingleDescendantOfType( node, ['a', 'link']), null );
+
+ });
+
+
+ it('appendChild', function(){
+ var node = document.createElement('div'),
+ child = document.createElement('a');
+
+ Modules.domUtils.appendChild( node, child );
+ assert.equal( node.innerHTML, '<a></a>' );
+ });
+
+
+ it('removeChild', function(){
+ var node = document.createElement('div'),
+ child = document.createElement('a');
+
+ node.appendChild(child)
+
+ assert.equal( node.innerHTML, '<a></a>' );
+ Modules.domUtils.removeChild( child );
+ assert.equal( node.innerHTML, '' );
+ });
+
+
+ it('clone', function(){
+ var node = document.createElement('div');
+
+ node.innerHTML = 'text content';
+ assert.equal( Modules.domUtils.clone( node ).outerHTML, '<div>text content</div>' );
+ });
+
+
+ it('getElementText', function(){
+ assert.equal( Modules.domUtils.getElementText( {} ), '' );
+ });
+
+
+ it('getNodePath', function(){
+ var node = document.createElement('ul');
+ node.innerHTML = '<div><ul><li class="h-card">one</li><li>two</li><li class="h-card">three</li><li class="p-name">four</li></ul></div>';
+ var child = node.querySelector('.p-name');
+
+ assert.deepEqual( Modules.domUtils.getNodePath( child ), [0,0,3] );
+ });
+
+
+});
diff --git a/toolkit/components/microformats/test/module-tests/html-test.js b/toolkit/components/microformats/test/module-tests/html-test.js
new file mode 100644
index 0000000000..cd06c7b7f7
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/html-test.js
@@ -0,0 +1,50 @@
+/*
+Unit test for html
+*/
+
+assert = chai.assert;
+
+// Tests the private Modules.html object
+// Modules.html is unit tested as it has an interface access by other modules
+
+describe('Modules.html', function() {
+
+
+ it('parse', function(){
+ var html = '<a href="http://glennjones.net">Glenn Jones</a>',
+ bloghtml = '<section id="content" class="body"><ol id="posts-list" class="h-feed"><li><article class="h-entry"><header><h2 class="p-namee"><a href="#" rel="bookmark" title="Permalink to this POST TITLE">This be the title</a></h2></header><footer class="post-info"><abbr class="dt-published" title="2005-10-10T14:07:00-07:00">10th October 2005</abbr><address class="h-card p-author">By <a class="u-url p-name" href="#">Enrique Ramírez</a></address></footer><div class="e-content"><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque venenatis nunc vitae libero iaculis elementum. Nullam et justo <a href="#">non sapien</a> dapibus blandit nec et leo. Ut ut malesuada tellus.</p></div></article></li></ol></section>',
+ node = document.createElement('div');
+
+ node.innerHTML = html;
+ assert.equal(Modules.html.parse( node ), html );
+
+ // make sure excludes 'data-include' marked items
+ var child = document.createElement('p');
+ child.setAttribute('data-include', 'true');
+ node.appendChild(child);
+ assert.equal( Modules.html.parse( node ), html );
+
+ node = document.createElement('div');
+ node.innerHTML = bloghtml;
+ assert.equal( Modules.html.parse( node ), bloghtml );
+
+ node = document.createElement('div');
+ assert.equal( Modules.html.parse( node ), '' );
+
+ child = document.createElement('br');
+ node.appendChild(child);
+ assert.equal( Modules.html.parse( node ), '<br />' );
+
+ node = document.createComment('test comment');
+ assert.equal( Modules.html.parse( node ), '' );
+
+ });
+
+
+
+
+
+
+
+
+});
diff --git a/toolkit/components/microformats/test/module-tests/index.html b/toolkit/components/microformats/test/module-tests/index.html
new file mode 100644
index 0000000000..0eb3c20407
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/index.html
@@ -0,0 +1,76 @@
+<html><head><title>Mocha</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<link rel="stylesheet" href="../static/css/mocha.css" />
+<script src="../static/javascript/chai.js"></script>
+<script src="../static/javascript/mocha.js"></script>
+<link rel="stylesheet" href="../static/css/mocha-custom.css" />
+<link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
+<script src="../static/javascript/DOMParser.js"></script>
+
+<script data-cover src="../lib/utilities.js"></script>
+<script data-cover src="../lib/domutils.js"></script>
+<script data-cover src="../lib/url.js"></script>
+<script data-cover src="../lib/html.js"></script>
+<script data-cover src="../lib/text.js"></script>
+<script data-cover src="../lib/dates.js"></script>
+<script data-cover src="../lib/isodate.js"></script>
+
+<script>
+var uncaughtError;
+
+window.addEventListener("error", function(error) {
+uncaughtError = error;
+});
+
+var consoleWarn = console.warn;
+var caughtWarnings = [];
+
+console.warn = function() {
+var args = Array.slice(arguments);
+caughtWarnings.push(args);
+consoleWarn.apply(console, args);
+};
+</script>
+
+<script>
+chai.config.includeStack = true;
+mocha.setup({ui: 'bdd', timeout: 10000});
+</script>
+
+
+<script src="../module-tests/dates-test.js"></script>
+<script src="../module-tests/domUtils-test.js"></script>
+<script src="../module-tests/html-test.js"></script>
+<script src="../module-tests/isodate-test.js"></script>
+<script src="../module-tests/text-test.js"></script>
+
+<script src="../module-tests/url-test.js"></script>
+
+<script src="../module-tests/utilities-test.js"></script>
+
+</head><body>
+<h3 class="capitalize">Microformats-shiv: module tests</h3>
+<div id="mocha"></div>
+</body>
+<script>
+describe("Uncaught Error Check", function() {
+it("should load the tests without errors", function() {
+chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
+});
+});
+
+describe("Unexpected Warnings Check", function() {
+it("should long only the warnings we expect", function() {
+chai.expect(caughtWarnings.length).to.eql(0);
+});
+});
+
+mocha.run(function () {
+var completeNode = document.createElement("p");
+completeNode.setAttribute("id", "complete");
+completeNode.appendChild(document.createTextNode("Complete"));
+document.getElementById("mocha").appendChild(completeNode);
+});
+
+</script>
+</body></html>
diff --git a/toolkit/components/microformats/test/module-tests/isodate-test.js b/toolkit/components/microformats/test/module-tests/isodate-test.js
new file mode 100644
index 0000000000..5f081f81cc
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/isodate-test.js
@@ -0,0 +1,145 @@
+/*
+Unit test for dates
+*/
+
+assert = chai.assert;
+
+
+// Tests private Modules.ISODate object
+// Modules.ISODate is unit tested as it has an interface access by other modules
+
+
+describe('Modules.ISODates', function() {
+
+
+
+ it('constructor', function(){
+ assert.equal( new Modules.ISODate().toString('auto'), '' );
+ assert.equal( new Modules.ISODate('2015-01-23T05:34:00', 'html5').toString('html5'), '2015-01-23 05:34:00' );
+ assert.equal( new Modules.ISODate('2015-01-23T05:34:00', 'w3c').toString('w3c'), '2015-01-23T05:34:00' );
+ assert.equal( new Modules.ISODate('2015-01-23T05:34:00', 'html5').toString('rfc3339'), '20150123T053400' );
+ assert.equal( new Modules.ISODate('2015-01-23T05:34:00', 'auto').toString('auto'), '2015-01-23T05:34:00' );
+ });
+
+
+ it('parse', function(){
+ assert.equal( new Modules.ISODate().parse('2015-01-23T05:34:00', 'html5').toString('html5'), '2015-01-23 05:34:00' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23T05:34:00', 'auto').toString('auto'), '2015-01-23T05:34:00' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23t05:34:00', 'auto').toString('auto'), '2015-01-23t05:34:00' );
+
+ assert.equal( new Modules.ISODate().parse('2015-01-23t05:34:00Z', 'auto').toString('auto'), '2015-01-23t05:34:00Z' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23t05:34:00z', 'auto').toString('auto'), '2015-01-23t05:34:00z' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23 05:34:00Z', 'auto').toString('auto'), '2015-01-23 05:34:00Z' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23 05:34', 'auto').toString('auto'), '2015-01-23 05:34' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23 05', 'auto').toString('auto'), '2015-01-23 05' );
+
+ assert.equal( new Modules.ISODate().parse('2015-01-23 05:34+01:00', 'auto').toString('auto'), '2015-01-23 05:34+01:00' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23 05:34-01:00', 'auto').toString('auto'), '2015-01-23 05:34-01:00' );
+ assert.equal( new Modules.ISODate().parse('2015-01-23 05:34-01', 'auto').toString('auto'), '2015-01-23 05:34-01' );
+
+
+ assert.equal( new Modules.ISODate().parse('2015-01-23', 'auto').toString('auto'), '2015-01-23' );
+ // TODO support for importing rfc3339 profile dates
+ // assert.equal( new Modules.ISODate().parse('20150123t0534', 'auto').toString('auto'), '2015-01-23 05:34' );
+ });
+
+
+ it('parseDate', function(){
+ assert.equal( new Modules.ISODate().parseDate('2015-01-23T05:34:00', 'html5'), '2015-01-23' );
+ assert.equal( new Modules.ISODate().parseDate('2015-01-23', 'auto'), '2015-01-23' );
+ assert.equal( new Modules.ISODate().parseDate('2015-01', 'auto'), '2015-01' );
+ assert.equal( new Modules.ISODate().parseDate('2015', 'auto'), '2015' );
+ assert.equal( new Modules.ISODate().parseDate('2015-134', 'auto'), '2015-134' );
+ });
+
+
+ it('parseTime', function(){
+ assert.equal( new Modules.ISODate().parseTime('05:34:00.1267', 'html5'), '05:34:00.1267' );
+ assert.equal( new Modules.ISODate().parseTime('05:34:00', 'html5'), '05:34:00' );
+ assert.equal( new Modules.ISODate().parseTime('05:34', 'html5'), '05:34' );
+ assert.equal( new Modules.ISODate().parseTime('05', 'html5'), '05' );
+ });
+
+ it('parseTimeZone', function(){
+ var date = new Modules.ISODate();
+ date.parseTime('14:00');
+ assert.equal( date.parseTimeZone('-01:00', 'auto'), '14:00-01:00' );
+
+ date.clear();
+ date.parseTime('14:00');
+ assert.equal( date.parseTimeZone('-01', 'auto'), '14:00-01' );
+
+ date.clear();
+ date.parseTime('14:00');
+ assert.equal( date.parseTimeZone('+01:00', 'auto').toString('auto'), '14:00+01:00' );
+
+ date.clear();
+ date.parseTime('15:00');
+ assert.equal( date.parseTimeZone('Z', 'auto').toString('auto'), '15:00Z' );
+
+ date.clear();
+ date.parseTime('16:00');
+ assert.equal( date.parseTimeZone('z', 'auto'), '16:00z' );
+
+ });
+
+
+
+ it('toString', function(){
+ var date = new Modules.ISODate();
+ date.parseTime('05:34:00.1267');
+
+ assert.equal( date.toString('html5'), '05:34:00.1267' );
+ });
+
+
+ it('toTimeString', function(){
+ var date = new Modules.ISODate();
+ date.parseTime('05:34:00.1267');
+
+ assert.equal( date.toTimeString('html5'), '05:34:00.1267' );
+ });
+
+
+ it('hasFullDate', function(){
+ var dateEmpty = new Modules.ISODate(),
+ date = new Modules.ISODate('2015-01-23T05:34:00');
+
+ assert.isFalse( dateEmpty.hasFullDate() );
+ assert.isTrue( date.hasFullDate() );
+ });
+
+
+ it('hasDate', function(){
+ var dateEmpty = new Modules.ISODate(),
+ date = new Modules.ISODate('2015-01-23');
+
+ assert.isFalse( dateEmpty.hasDate() );
+ assert.isTrue( date.hasDate() );
+ });
+
+
+ it('hasTime', function(){
+ var dateEmpty = new Modules.ISODate(),
+ date = new Modules.ISODate();
+
+ date.parseTime('12:34');
+
+ assert.isFalse( dateEmpty.hasTime() );
+ assert.isTrue( date.hasTime() );
+ });
+
+
+ it('hasTimeZone', function(){
+ var dateEmpty = new Modules.ISODate(),
+ date = new Modules.ISODate();
+
+ date.parseTime('12:34'),
+ date.parseTimeZone('-01:00');
+
+ assert.isFalse( dateEmpty.hasTimeZone() );
+ assert.isTrue( date.hasTimeZone() );
+ });
+
+
+});
diff --git a/toolkit/components/microformats/test/module-tests/text-test.js b/toolkit/components/microformats/test/module-tests/text-test.js
new file mode 100644
index 0000000000..f1f2e775cf
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/text-test.js
@@ -0,0 +1,56 @@
+/*
+Unit test for text
+*/
+
+assert = chai.assert;
+
+// Tests the private Modules.text object
+// Modules.text is unit tested as it has an interface access by other modules
+
+
+describe('Modules.text', function() {
+
+
+ it('parse', function(){
+ var html = '\n <a href="http://glennjones.net">Glenn\n Jones \n</a> \n',
+ node = document.createElement('div');
+
+ node.innerHTML = html;
+ assert.equal( Modules.text.parse( document, node, 'whitespacetrimmed' ), 'Glenn\n Jones' );
+ assert.equal( Modules.text.parse( document, node, 'whitespace' ), '\n Glenn\n Jones \n \n' );
+ assert.equal( Modules.text.parse( document, node, 'normalised' ), 'Glenn Jones' );
+
+ // exclude tags
+ node.innerHTML = '<script>test</script>text';
+ assert.equal( Modules.text.parse( document, node, 'normalised' ), 'text' );
+
+ // block level
+ node.innerHTML = '<p>test</p>text';
+ //assert.equal( Modules.text.parse( document, node, 'normalised' ), 'test text' );
+
+ // node with no text data
+ node = document.createComment('test comment');
+ assert.equal( Modules.text.parse( document, node, 'normalised' ), '' );
+
+ });
+
+
+ it('parseText', function(){
+ var text = '\n <a href="http://glennjones.net">Glenn\n Jones \n</a> \n';
+
+ // create DOM context first
+ Modules.domUtils.getDOMContext( {} );
+
+ assert.equal( Modules.text.parseText( document, text, 'whitespacetrimmed' ), 'Glenn\n Jones' );
+ assert.equal( Modules.text.parseText( document, text, 'whitespace' ), '\n Glenn\n Jones \n \n' );
+ assert.equal( Modules.text.parseText( document, text, 'normalised' ), 'Glenn Jones' );
+ });
+
+
+ it('formatText', function(){
+ assert.equal( Modules.text.formatText( document, null, 'whitespacetrimmed' ), '' );
+ });
+
+
+
+});
diff --git a/toolkit/components/microformats/test/module-tests/url-test.js b/toolkit/components/microformats/test/module-tests/url-test.js
new file mode 100644
index 0000000000..788e8fdb51
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/url-test.js
@@ -0,0 +1,25 @@
+/*
+Unit test for url
+*/
+
+assert = chai.assert;
+
+
+// Tests the private Modules.url object
+// Modules.url is unit tested as it has an interface access by other modules
+
+
+describe('Modules.url', function() {
+
+ it('resolve', function(){
+ assert.equal( Modules.url.resolve( 'docs/index.html', 'http://example.org' ), 'http://example.org/docs/index.html' );
+ assert.equal( Modules.url.resolve( '../index.html', 'http://example.org/docs/' ), 'http://example.org/index.html' );
+ assert.equal( Modules.url.resolve( '/', 'http://example.org/' ), 'http://example.org/' );
+ assert.equal( Modules.url.resolve( 'http://glennjones.net/', 'http://example.org/' ), 'http://glennjones.net/' );
+
+ assert.equal( Modules.url.resolve( undefined, 'http://example.org/' ), '' );
+ assert.equal( Modules.url.resolve( undefined, undefined ), '' );
+ assert.equal( Modules.url.resolve( 'http://glennjones.net/', undefined ), 'http://glennjones.net/' );
+ });
+
+});
diff --git a/toolkit/components/microformats/test/module-tests/utilities-test.js b/toolkit/components/microformats/test/module-tests/utilities-test.js
new file mode 100644
index 0000000000..b37236a6bc
--- /dev/null
+++ b/toolkit/components/microformats/test/module-tests/utilities-test.js
@@ -0,0 +1,93 @@
+/*
+Unit test for utilities
+*/
+
+assert = chai.assert;
+
+// Tests the private Modules.utils object
+// Modules.utils is unit tested as it has an interface access by other modules
+
+
+describe('Modules.utilities', function() {
+
+
+ it('isString', function(){
+ assert.isTrue( Modules.utils.isString( 'abc' ) );
+ assert.isFalse( Modules.utils.isString( 123 ) );
+ assert.isFalse( Modules.utils.isString( 1.23 ) );
+ assert.isFalse( Modules.utils.isString( {'abc': 'abc'} ) );
+ assert.isFalse( Modules.utils.isString( ['abc'] ) );
+ assert.isFalse( Modules.utils.isString( true ) );
+ });
+
+
+ it('isArray', function(){
+ assert.isTrue( Modules.utils.isArray( ['abc'] ) );
+ assert.isFalse( Modules.utils.isArray( 123 ) );
+ assert.isFalse( Modules.utils.isArray( 1.23 ) );
+ assert.isFalse( Modules.utils.isArray( 'abc' ) );
+ assert.isFalse( Modules.utils.isArray( {'abc': 'abc'} ) );
+ assert.isFalse( Modules.utils.isArray( true ) );
+ });
+
+
+ it('isNumber', function(){
+ assert.isTrue( Modules.utils.isNumber( 123 ) );
+ assert.isTrue( Modules.utils.isNumber( 1.23 ) );
+ assert.isFalse( Modules.utils.isNumber( 'abc' ) );
+ assert.isFalse( Modules.utils.isNumber( {'abc': 'abc'} ) );
+ assert.isFalse( Modules.utils.isNumber( ['abc'] ) );
+ assert.isFalse( Modules.utils.isNumber( true ) );
+ });
+
+
+ it('startWith', function(){
+ assert.isTrue( Modules.utils.startWith( 'p-name', 'p-' ) );
+ assert.isFalse( Modules.utils.startWith( 'p-name', 'name' ) );
+ assert.isFalse( Modules.utils.startWith( 'p-name', 'u-' ) );
+ });
+
+
+ it('trim', function(){
+ assert.equal( Modules.utils.trim( ' Glenn Jones ' ), 'Glenn Jones' );
+ assert.equal( Modules.utils.trim( 'Glenn Jones' ), 'Glenn Jones' );
+ assert.equal( Modules.utils.trim( undefined ), '' );
+ });
+
+
+ it('replaceCharAt', function(){
+ assert.equal( Modules.utils.replaceCharAt( 'Glenn Jones', 5, '-' ), 'Glenn-Jones' );
+ assert.equal( Modules.utils.replaceCharAt( 'Glenn Jones', 50, '-' ), 'Glenn Jones' );
+ });
+
+
+ it('isOnlyWhiteSpace', function(){
+ assert.isTrue( Modules.utils.isOnlyWhiteSpace( ' ') );
+ assert.isTrue( Modules.utils.isOnlyWhiteSpace( ' \n\r') );
+ assert.isFalse( Modules.utils.isOnlyWhiteSpace( ' text\n\r') );
+ });
+
+
+ it('collapseWhiteSpace', function(){
+ assert.equal( Modules.utils.collapseWhiteSpace( ' '), ' ' );
+ assert.equal( Modules.utils.collapseWhiteSpace( ' \n\r'), ' ' );
+ assert.equal( Modules.utils.collapseWhiteSpace( ' text\n\r'), ' text ' );
+ });
+
+
+ it('hasProperties', function(){
+ assert.isTrue( Modules.utils.hasProperties( {name: 'glennjones'} ) );
+ assert.isFalse( Modules.utils.hasProperties( {} ) );
+ });
+
+
+ it('sortObjects', function(){
+ var arr = [{'name': 'one'},{'name': 'two'},{'name': 'three'},{'name': 'three'}];
+
+ assert.deepEqual( arr.sort( Modules.utils.sortObjects( 'name', true ) ), [{"name":"two"},{"name":"three"},{'name': 'three'},{"name":"one"}] );
+ assert.deepEqual( arr.sort( Modules.utils.sortObjects( 'name', false ) ), [{"name":"one"},{"name":"three"},{'name': 'three'},{"name":"two"}] );
+ });
+
+
+
+});
diff --git a/toolkit/components/microformats/test/standards-tests/index.html b/toolkit/components/microformats/test/standards-tests/index.html
new file mode 100644
index 0000000000..47f89f9886
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/index.html
@@ -0,0 +1,179 @@
+<html><head><title>Mocha</title>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<link rel="stylesheet" href="../static/css/mocha.css" />
+<script src="../static/javascript/chai.js"></script>
+<script src="../static/javascript/mocha.js"></script>
+<link rel="stylesheet" href="../static/css/mocha-custom.css" />
+<link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png"/>
+
+<script src="../static/javascript/DOMParser.js"></script>
+
+<script data-cover src="../../microformat-shiv.js"></script>
+
+<script>
+var uncaughtError;
+
+window.addEventListener("error", function(error) {
+uncaughtError = error;
+});
+
+var consoleWarn = console.warn;
+var caughtWarnings = [];
+
+console.warn = function() {
+var args = Array.slice(arguments);
+caughtWarnings.push(args);
+consoleWarn.apply(console, args);
+};
+</script>
+
+<script>
+chai.config.includeStack = true;
+mocha.setup({ui: 'bdd', timeout: 10000});
+</script>
+
+<script src="../standards-tests/mf-mixed-h-card-mixedpropertries.js"></script>
+<script src="../standards-tests/mf-mixed-h-card-tworoots.js"></script>
+<script src="../standards-tests/mf-mixed-h-entry-mixedroots.js"></script>
+<script src="../standards-tests/mf-mixed-h-resume-mixedroots.js"></script>
+<script src="../standards-tests/mf-v1-adr-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v1-geo-abbrpattern.js"></script>
+<script src="../standards-tests/mf-v1-geo-hidden.js"></script>
+<script src="../standards-tests/mf-v1-geo-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v1-geo-valuetitleclass.js"></script>
+<script src="../standards-tests/mf-v1-hcalendar-ampm.js"></script>
+<script src="../standards-tests/mf-v1-hcalendar-attendees.js"></script>
+<script src="../standards-tests/mf-v1-hcalendar-combining.js"></script>
+<script src="../standards-tests/mf-v1-hcalendar-concatenate.js"></script>
+<script src="../standards-tests/mf-v1-hcalendar-time.js"></script>
+<script src="../standards-tests/mf-v1-hcard-email.js"></script>
+<script src="../standards-tests/mf-v1-hcard-format.js"></script>
+<script src="../standards-tests/mf-v1-hcard-hyperlinkedphoto.js"></script>
+<script src="../standards-tests/mf-v1-hcard-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v1-hcard-justaname.js"></script>
+<script src="../standards-tests/mf-v1-hcard-multiple.js"></script>
+<script src="../standards-tests/mf-v1-hcard-name.js"></script>
+<script src="../standards-tests/mf-v1-hcard-single.js"></script>
+<script src="../standards-tests/mf-v1-hentry-summarycontent.js"></script>
+<script src="../standards-tests/mf-v1-hfeed-simple.js"></script>
+<script src="../standards-tests/mf-v1-hnews-all.js"></script>
+<script src="../standards-tests/mf-v1-hnews-minimum.js"></script>
+<script src="../standards-tests/mf-v1-hproduct-aggregate.js"></script>
+<script src="../standards-tests/mf-v1-hproduct-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v1-hresume-affiliation.js"></script>
+<script src="../standards-tests/mf-v1-hresume-contact.js"></script>
+<script src="../standards-tests/mf-v1-hresume-education.js"></script>
+<script src="../standards-tests/mf-v1-hresume-skill.js"></script>
+<script src="../standards-tests/mf-v1-hresume-work.js"></script>
+<script src="../standards-tests/mf-v1-hreview-item.js"></script>
+<script src="../standards-tests/mf-v1-hreview-vcard.js"></script>
+<script src="../standards-tests/mf-v1-hreview-aggregate-hcard.js"></script>
+<script src="../standards-tests/mf-v1-hreview-aggregate-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v1-hreview-aggregate-vevent.js"></script>
+<script src="../standards-tests/mf-v1-includes-hcarditemref.js"></script>
+<script src="../standards-tests/mf-v1-includes-heventitemref.js"></script>
+<script src="../standards-tests/mf-v1-includes-hyperlink.js"></script>
+<script src="../standards-tests/mf-v1-includes-object.js"></script>
+<script src="../standards-tests/mf-v1-includes-table.js"></script>
+<script src="../standards-tests/mf-v2-h-adr-geo.js"></script>
+<script src="../standards-tests/mf-v2-h-adr-geourl.js"></script>
+<script src="../standards-tests/mf-v2-h-adr-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-adr-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v2-h-as-note-note.js"></script>
+<script src="../standards-tests/mf-v2-h-card-baseurl.js"></script>
+<script src="../standards-tests/mf-v2-h-card-childimplied.js"></script>
+<script src="../standards-tests/mf-v2-h-card-extendeddescription.js"></script>
+<script src="../standards-tests/mf-v2-h-card-hcard.js"></script>
+<script src="../standards-tests/mf-v2-h-card-horghcard.js"></script>
+<script src="../standards-tests/mf-v2-h-card-hyperlinkedphoto.js"></script>
+<script src="../standards-tests/mf-v2-h-card-impliedname.js"></script>
+<script src="../standards-tests/mf-v2-h-card-impliedphoto.js"></script>
+<script src="../standards-tests/mf-v2-h-card-impliedurl.js"></script>
+<script src="../standards-tests/mf-v2-h-card-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-card-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-card-nested.js"></script>
+<script src="../standards-tests/mf-v2-h-card-p-property.js"></script>
+<script src="../standards-tests/mf-v2-h-card-relativeurls.js"></script>
+<script src="../standards-tests/mf-v2-h-entry-impliedvalue-nested.js"></script>
+<script src="../standards-tests/mf-v2-h-entry-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-entry-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-entry-summarycontent.js"></script>
+<script src="../standards-tests/mf-v2-h-entry-u-property.js"></script>
+<script src="../standards-tests/mf-v2-h-entry-urlincontent.js"></script>
+<script src="../standards-tests/mf-v2-h-event-ampm.js"></script>
+<script src="../standards-tests/mf-v2-h-event-attendees.js"></script>
+<script src="../standards-tests/mf-v2-h-event-combining.js"></script>
+<script src="../standards-tests/mf-v2-h-event-concatenate.js"></script>
+<script src="../standards-tests/mf-v2-h-event-dates.js"></script>
+<script src="../standards-tests/mf-v2-h-event-dt-property.js"></script>
+<script src="../standards-tests/mf-v2-h-event-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-event-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-event-time.js"></script>
+<script src="../standards-tests/mf-v2-h-feed-implied-title.js"></script>
+<script src="../standards-tests/mf-v2-h-feed-simple.js"></script>
+<script src="../standards-tests/mf-v2-h-geo-abbrpattern.js"></script>
+<script src="../standards-tests/mf-v2-h-geo-altitude.js"></script>
+<script src="../standards-tests/mf-v2-h-geo-hidden.js"></script>
+<script src="../standards-tests/mf-v2-h-geo-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-geo-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v2-h-geo-valuetitleclass.js"></script>
+<script src="../standards-tests/mf-v2-h-news-all.js"></script>
+<script src="../standards-tests/mf-v2-h-news-minimum.js"></script>
+<script src="../standards-tests/mf-v2-h-org-hyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-org-simple.js"></script>
+<script src="../standards-tests/mf-v2-h-org-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v2-h-product-aggregate.js"></script>
+<script src="../standards-tests/mf-v2-h-product-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-product-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-product-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v2-h-recipe-all.js"></script>
+<script src="../standards-tests/mf-v2-h-recipe-minimum.js"></script>
+<script src="../standards-tests/mf-v2-h-resume-affiliation.js"></script>
+<script src="../standards-tests/mf-v2-h-resume-contact.js"></script>
+<script src="../standards-tests/mf-v2-h-resume-education.js"></script>
+<script src="../standards-tests/mf-v2-h-resume-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-resume-skill.js"></script>
+<script src="../standards-tests/mf-v2-h-resume-work.js"></script>
+<script src="../standards-tests/mf-v2-h-review-hyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-review-implieditem.js"></script>
+<script src="../standards-tests/mf-v2-h-review-item.js"></script>
+<script src="../standards-tests/mf-v2-h-review-justaname.js"></script>
+<script src="../standards-tests/mf-v2-h-review-photo.js"></script>
+<script src="../standards-tests/mf-v2-h-review-vcard.js"></script>
+<script src="../standards-tests/mf-v2-h-review-aggregate-hevent.js"></script>
+<script src="../standards-tests/mf-v2-h-review-aggregate-justahyperlink.js"></script>
+<script src="../standards-tests/mf-v2-h-review-aggregate-simpleproperties.js"></script>
+<script src="../standards-tests/mf-v2-rel-duplicate-rels.js"></script>
+<script src="../standards-tests/mf-v2-rel-license.js"></script>
+<script src="../standards-tests/mf-v2-rel-nofollow.js"></script>
+<script src="../standards-tests/mf-v2-rel-rel-urls.js"></script>
+<script src="../standards-tests/mf-v2-rel-varying-text-duplicate-rels.js"></script>
+<script src="../standards-tests/mf-v2-rel-xfn-all.js"></script>
+<script src="../standards-tests/mf-v2-rel-xfn-elsewhere.js"></script>
+</head><body>
+<h3 class="capitalize">Microformats-shiv: standards tests</h3>
+<p>Standards tests built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST). Downloaded from github repo: microformats/tests version v0.1.24</p>
+<div id="mocha"></div>
+</body>
+<script>
+describe("Uncaught Error Check", function() {
+it("should load the tests without errors", function() {
+chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
+});
+});
+
+describe("Unexpected Warnings Check", function() {
+it("should long only the warnings we expect", function() {
+chai.expect(caughtWarnings.length).to.eql(0);
+});
+});
+
+mocha.run(function () {
+var completeNode = document.createElement("p");
+completeNode.setAttribute("id", "complete");
+completeNode.appendChild(document.createTextNode("Complete"));
+document.getElementById("mocha").appendChild(completeNode);
+});
+
+</script>
+</body></html>
diff --git a/toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-mixedpropertries.js b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-mixedpropertries.js
new file mode 100644
index 0000000000..db99dc92a2
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-mixedpropertries.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-mixed/h-card/mixedpropertries
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<div class=\"h-card\">\n <p>\n <a class=\"p-name p-org u-url\" href=\"http://mozilla.org/\">Mozilla Foundation</a>\n <img class=\"logo\" src=\"../logo.jpg\"/>\n </p>\n <p class=\"adr\">\n <span class=\"street-address\">665 3rd St.</span> \n <span class=\"extended-address\">Suite 207</span> \n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n <span class=\"postal-code\">94107</span> \n <span class=\"p-country-name\">U.S.A.</span> \n </p>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Mozilla Foundation"],"org":["Mozilla Foundation"],"url":["http://mozilla.org/"],"adr":[{"value":"665 3rd St. \n Suite 207 \n San Francisco, \n CA \n 94107 \n U.S.A.","type":["h-adr"],"properties":{"street-address":["665 3rd St."],"extended-address":["Suite 207"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94107"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('mixedpropertries', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-tworoots.js b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-tworoots.js
new file mode 100644
index 0000000000..be43abcd86
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-card-tworoots.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-mixed/h-card/tworoots
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<p class=\"h-card vcard\">Frances Berriman</p>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Frances Berriman"]}}],"rels":{},"rel-urls":{}};
+
+ it('tworoots', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-mixed-h-entry-mixedroots.js b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-entry-mixedroots.js
new file mode 100644
index 0000000000..705ffeebf2
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-entry-mixedroots.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-mixed/h-entry/mixedroots
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<!-- simplified version of http://aaronparecki.com/notes/2013/10/18/2/realtimeconf-mapattack -->\n<base href=\"http://aaronparecki.com/\" />\n\n<div class=\"h-entry\">\n <div class=\"h-card vcard author p-author\">\n <img class=\"photo logo u-photo u-logo\" src=\"https://aaronparecki.com/images/aaronpk.png\" alt=\"Aaron Parecki\"/>\n <a href=\"https://aaronparecki.com/\" rel=\"author\" class=\"u-url u-uid url\">aaronparecki.com</a>\n <a class=\"p-name fn value\" href=\"https://aaronparecki.com/\">Aaron Parecki</a>\n <a href=\"https://plus.google.com/117847912875913905493\" rel=\"author\" class=\"google-profile\">Aaron Parecki</a>\n </div>\n <div class=\"entry-content e-content p-name\">Did you play\n <a href=\"http://twitter.com/playmapattack\">@playmapattack</a>at\n <a href=\"/tag/realtimeconf\">#<span class=\"p-category\">realtimeconf</span></a>? Here is some more info about how we built it!\n <a href=\"http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/\"><span class=\"protocol\">http://</span>pdx.esri.com/blog/2013/10/17/introducting-mapattack/</a>\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"author":[{"value":"aaronparecki.com\n Aaron Parecki\n Aaron Parecki","type":["h-card"],"properties":{"photo":["https://aaronparecki.com/images/aaronpk.png"],"logo":["https://aaronparecki.com/images/aaronpk.png"],"url":["https://aaronparecki.com/"],"uid":["https://aaronparecki.com/"],"name":["Aaron Parecki"]}}],"content":[{"value":"Did you play\n @playmapattackat\n #realtimeconf? Here is some more info about how we built it!\n http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/","html":"Did you play\n <a href=\"http://twitter.com/playmapattack\">@playmapattack</a>at\n <a href=\"http://aaronparecki.com/tag/realtimeconf\">#<span class=\"p-category\">realtimeconf</span></a>? Here is some more info about how we built it!\n <a href=\"http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/\"><span class=\"protocol\">http://</span>pdx.esri.com/blog/2013/10/17/introducting-mapattack/</a>\n "}],"name":["Did you play\n @playmapattackat\n #realtimeconf? Here is some more info about how we built it!\n http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/"],"category":["realtimeconf"]}}],"rels":{"author":["https://aaronparecki.com/","https://plus.google.com/117847912875913905493"]},"rel-urls":{"https://aaronparecki.com/":{"text":"aaronparecki.com","rels":["author"]},"https://plus.google.com/117847912875913905493":{"text":"Aaron Parecki","rels":["author"]}}};
+
+ it('mixedroots', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-mixed-h-resume-mixedroots.js b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-resume-mixedroots.js
new file mode 100644
index 0000000000..5147866c6b
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-mixed-h-resume-mixedroots.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-mixed/h-resume/mixedroots
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"h-resume\">\n <div class=\"p-contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <div class=\"p-experience vevent vcard\">\n <p class=\"title\">Director</p>\n <p><a class=\"fn org summary url\" href=\"http://www.webfoundation.org/\">World Wide Web Foundation</a></p>\n <p>\n <time class=\"dtstart\" datetime=\"2009-01-18\">Jan 2009</time> – Present\n <time class=\"duration\" datetime=\"P2Y11M\">(2 years 11 month)</time>\n </p>\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"contact":[{"value":"Tim Berners-Lee","type":["h-card"],"properties":{"name":["Tim Berners-Lee"],"job-title":["Director of the World Wide Web Foundation"]}}],"summary":["Invented the World Wide Web."],"experience":[{"value":"World Wide Web Foundation","type":["h-event","h-card"],"properties":{"job-title":["Director"],"name":["World Wide Web Foundation"],"org":["World Wide Web Foundation"],"url":["http://www.webfoundation.org/"],"start":["2009-01-18"],"duration":["P2Y11M"]}}],"name":["Tim Berners-Lee\n Director of the World Wide Web Foundation\n \n Invented the World Wide Web.\n \n Director\n World Wide Web Foundation\n \n Jan 2009 – Present\n (2 years 11 month)"]}}],"rels":{},"rel-urls":{}};
+
+ it('mixedroots', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-adr-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v1-adr-simpleproperties.js
new file mode 100644
index 0000000000..09a346e0c4
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-adr-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/adr/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('adr', function() {
+ var htmlFragment = "<p class=\"adr\">\n <span class=\"street-address\">665 3rd St.</span> \n <span class=\"extended-address\">Suite 207</span> \n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n <span class=\"postal-code\">94107</span> \n <span class=\"country-name\">U.S.A.</span> \n</p>";
+ var expected = {"items":[{"type":["h-adr"],"properties":{"street-address":["665 3rd St."],"extended-address":["Suite 207"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94107"],"country-name":["U.S.A."]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-geo-abbrpattern.js b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-abbrpattern.js
new file mode 100644
index 0000000000..090e98bb99
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-abbrpattern.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/geo/abbrpattern
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('geo', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<p class=\"geo\">\n <abbr class=\"latitude\" title=\"37.408183\">N 37° 24.491</abbr>, \n <abbr class=\"longitude\" title=\"-122.13855\">W 122° 08.313</abbr>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["37.408183"],"longitude":["-122.13855"]}}],"rels":{},"rel-urls":{}};
+
+ it('abbrpattern', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-geo-hidden.js b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-hidden.js
new file mode 100644
index 0000000000..d67a03b4f3
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-hidden.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/geo/hidden
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('geo', function() {
+ var htmlFragment = "<p>\n <span class=\"geo\">The Bricklayer's Arms\n <span class=\"latitude\">\n <span class=\"value-title\" title=\"51.513458\"> </span> \n </span>\n <span class=\"longitude\">\n <span class=\"value-title\" title=\"-0.14812\"> </span>\n </span>\n </span>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["51.513458"],"longitude":["-0.14812"]}}],"rels":{},"rel-urls":{}};
+
+ it('hidden', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-geo-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-simpleproperties.js
new file mode 100644
index 0000000000..82cd7d3d92
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/geo/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('geo', function() {
+ var htmlFragment = "We are meeting at \n<span class=\"geo\"> \n <span>The Bricklayer's Arms</span>\n (Geo: <span class=\"latitude\">51.513458</span>:\n <span class=\"longitude\">-0.14812</span>)\n</span>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["51.513458"],"longitude":["-0.14812"]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-geo-valuetitleclass.js b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-valuetitleclass.js
new file mode 100644
index 0000000000..196e07f7dd
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-geo-valuetitleclass.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/geo/valuetitleclass
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('geo', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<p>\n <span class=\"geo\">\n <span class=\"latitude\">\n <span class=\"value-title\" title=\"51.513458\">N 51° 51.345</span>, \n </span>\n <span class=\"longitude\">\n <span class=\"value-title\" title=\"-0.14812\">W -0° 14.812</span>\n </span>\n </span>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["51.513458"],"longitude":["-0.14812"]}}],"rels":{},"rel-urls":{}};
+
+ it('valuetitleclass', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-ampm.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-ampm.js
new file mode 100644
index 0000000000..5da5fd7df9
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-ampm.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcalendar/ampm
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcalendar', function() {
+ var htmlFragment = "<div class=\"vevent\">\n <span class=\"summary\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00am \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00p.m. \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00PM \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00am \n </span></li>\n </ul>\n</div>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00:00","2009-06-26 07:00:00","2009-06-26 19:00","2009-06-26 19","2009-06-26 19","2009-06-26 19:00","2009-06-26 19:00","2009-06-26 19:00","2009-06-26 07:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('ampm', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-attendees.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-attendees.js
new file mode 100644
index 0000000000..ca28ad431c
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-attendees.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcalendar/attendees
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcalendar', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"vevent\">\n <span class=\"summary\">CPJ Online Press Freedom Summit</span>\n (<time class=\"dtstart\" datetime=\"2012-10-10\">10 Nov 2012</time>) in\n <span class=\"location\">San Francisco</span>.\n Attendees:\n <ul>\n <li class=\"attendee vcard\"><span class=\"fn\">Brian Warner</span></li>\n <li class=\"attendee vcard\"><span class=\"fn\">Kyle Machulis</span></li>\n <li class=\"attendee vcard\"><span class=\"fn\">Tantek Çelik</span></li>\n <li class=\"attendee vcard\"><span class=\"fn\">Sid Sutter</span></li>\n </ul>\n</div>\n";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["CPJ Online Press Freedom Summit"],"start":["2012-10-10"],"location":["San Francisco"],"attendee":[{"value":"Brian Warner","type":["h-card"],"properties":{"name":["Brian Warner"]}},{"value":"Kyle Machulis","type":["h-card"],"properties":{"name":["Kyle Machulis"]}},{"value":"Tantek Çelik","type":["h-card"],"properties":{"name":["Tantek Çelik"]}},{"value":"Sid Sutter","type":["h-card"],"properties":{"name":["Sid Sutter"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('attendees', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-combining.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-combining.js
new file mode 100644
index 0000000000..7e5a361b12
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-combining.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcalendar/combining
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcalendar', function() {
+ var htmlFragment = "<div class=\"vevent\">\n <a class=\"summary url\" href=\"http://indiewebcamp.com/2012\">\n IndieWebCamp 2012\n </a>\n from <time class=\"dtstart\">2012-06-30</time> \n to <time class=\"dtend\">2012-07-01</time> at \n <span class=\"location vcard\">\n <a class=\"fn org url\" href=\"http://geoloqi.com/\">Geoloqi</a>, \n <span class=\"adr\">\n <span class=\"street-address\">920 SW 3rd Ave. Suite 400</span>, \n <span class=\"locality\">Portland</span>, \n <abbr class=\"region\" title=\"Oregon\">OR</abbr>\n </span>\n </span>\n</div>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["IndieWebCamp 2012"],"url":["http://indiewebcamp.com/2012"],"start":["2012-06-30"],"end":["2012-07-01"],"location":[{"value":"Geoloqi","type":["h-card"],"properties":{"name":["Geoloqi"],"org":["Geoloqi"],"url":["http://geoloqi.com/"],"adr":[{"value":"920 SW 3rd Ave. Suite 400, \n Portland, \n OR","type":["h-adr"],"properties":{"street-address":["920 SW 3rd Ave. Suite 400"],"locality":["Portland"],"region":["Oregon"]}}]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('combining', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-concatenate.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-concatenate.js
new file mode 100644
index 0000000000..d17914e1c3
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-concatenate.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcalendar/concatenate
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcalendar', function() {
+ var htmlFragment = "<div class=\"vevent\">\n <span class=\"summary\">The 4th Microformat party</span> will be on \n <span class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time></span> to \n <span class=\"dtend\"><time class=\"value\">22:00</time></span>.\n</div>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00"],"end":["2009-06-26 22:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('concatenate', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-time.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-time.js
new file mode 100644
index 0000000000..edb26d6ad8
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcalendar-time.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcalendar/time
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcalendar', function() {
+ var htmlFragment = "<div class=\"vevent\">\n <span class=\"summary\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-08:00</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-0800</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00+0800</time> \n </li> \n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00Z</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00-08:00</time> \n </li> \n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00+08:00</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00z</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time> \n </li> \n <li>\n <time class=\"dtend\" datetime=\"2013-034\">3 February 2013</time>\n </li> \n </ul>\n</div>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00:00-08:00","2009-06-26 19:00:00-08:00","2009-06-26 19:00:00+08:00","2009-06-26 19:00:00Z","2009-06-26 19:00:00","2009-06-26 19:00-08:00","2009-06-26 19:00+08:00","2009-06-26 19:00Z","2009-06-26 19:00"],"end":["2013-034"]}}],"rels":{},"rel-urls":{}};
+
+ it('time', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-email.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-email.js
new file mode 100644
index 0000000000..48660ffb1b
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-email.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/email
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<div class=\"vcard\">\n <span class=\"fn\">John Doe</span> \n <ul>\n <li><a class=\"email\" href=\"mailto:john@example.com\">notthis@example.com</a></li>\n <li>\n <span class=\"email\">\n <span class=\"type\">internet</span> \n <a class=\"value\" href=\"mailto:john@example.com\">notthis@example.com</a>\n </span>\n </li> \n <li><a class=\"email\" href=\"mailto:john@example.com?subject=parser-test\">notthis@example.com</a></li>\n <li class=\"email\">john@example.com</li>\n </ul>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["John Doe"],"email":["mailto:john@example.com","mailto:john@example.com","mailto:john@example.com?subject=parser-test","john@example.com"]}}],"rels":{},"rel-urls":{}};
+
+ it('email', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-format.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-format.js
new file mode 100644
index 0000000000..eb539fd870
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-format.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/format
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<p class=\"vcard\">\n <span class=\"profile-name fn n\">\n <span class=\" given-name \">John</span> \n <span class=\"FAMILY-NAME\">Doe</span> \n </span>\n</p>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["John \n Doe"],"given-name":["John"]}}],"rels":{},"rel-urls":{}};
+
+ it('format', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-hyperlinkedphoto.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-hyperlinkedphoto.js
new file mode 100644
index 0000000000..7f348b4a80
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-hyperlinkedphoto.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/hyperlinkedphoto
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<a class=\"vcard\" href=\"http://rohit.khare.org/\">\n <img alt=\"Rohit Khare\" src=\"images/photo.gif\" />\n</a>";
+ var expected = {"items":[{"type":["h-card"],"properties":{}}],"rels":{},"rel-urls":{}};
+
+ it('hyperlinkedphoto', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justahyperlink.js
new file mode 100644
index 0000000000..e320f0f8af
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<a class=\"vcard\" href=\"http://benward.me/\">Ben Ward</a>";
+ var expected = {"items":[{"type":["h-card"],"properties":{}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justaname.js
new file mode 100644
index 0000000000..ba2a6d47dd
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<p class=\"vcard\">Frances Berriman</p>";
+ var expected = {"items":[{"type":["h-card"],"properties":{}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-multiple.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-multiple.js
new file mode 100644
index 0000000000..058e5e2aef
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-multiple.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/multiple
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<base href=\"http://example.com\">\n <div class=\"vcard\">\n \n <div class=\"fn n\"><span class=\"given-name\">John</span> <span class=\"family-name\">Doe</span></div>\n <a class=\"sound\" href=\"http://www.madgex.com/johndoe.mpeg\">Pronunciation of my name</a>\n <div><img class=\"photo\" src=\"images/photo.gif\" alt=\"Photo of John Doe\" /></div>\n\n <p>Nicknames:</p>\n <ul>\n <li class=\"nickname\">Man with no name</li>\n <li class=\"nickname\">Lost boy</li>\n </ul>\n\n <p>About:</p>\n <p class=\"note\">John Doe is one of those names you always have issues with.</p>\n <p class=\"note\">It can be a real problem booking a hotel room with the name John Doe.</p>\n\n <p>Companies:</p>\n <div>\n <img class=\"logo\" src=\"images/logo.gif\" alt=\"Madgex company logo\" />\n <img class=\"logo\" src=\"images/logo.gif\" alt=\"Web Feet Media company logo\" />\n </div>\n <ul>\n <li><a class=\"url org\" href=\"http://www.madgex.com/\">Madgex</a> <span class=\"title\">Creative Director</span></li>\n <li><a class=\"url org\" href=\"http://www.webfeetmedia.com/\">Web Feet Media Ltd</a> <span class=\"title\">Owner</span></li>\n </ul>\n \n <p>Tags: \n <a rel=\"tag\" class=\"category\" href=\"http://en.wikipedia.org/wiki/design\">design</a>, \n <a rel=\"tag\" class=\"category\" href=\"http://en.wikipedia.org/wiki/development\">development</a> and\n <a rel=\"tag\" class=\"category\" href=\"http://en.wikipedia.org/wiki/web\">web</a>\n </p>\n \n <p>Phone numbers:</p>\n <ul>\n <li class=\"tel\">\n <span class=\"type\">Work</span> (<span class=\"type\">pref</span>erred):\n <span class=\"value\">+1 415 555 100</span>\n </li>\n <li class=\"tel\"><span class=\"type\">Home</span>: <span class=\"value\">+1 415 555 200</span></li>\n <li class=\"tel\"><span class=\"type\">Postal</span>: <span class=\"value\">+1 415 555 300</span></li>\n </ul>\n \n <p>Emails:</p>\n <ul>\n <li><a class=\"email\" href=\"mailto:john.doe@madgex.com\">John Doe at Madgex</a></li>\n <li><a class=\"email\" href=\"mailto:john.doe@webfeetmedia.com\">John Doe at Web Feet Media</a></li>\n </ul>\n <p>John Doe uses <span class=\"mailer\">PigeonMail 2.1</span> or <span class=\"mailer\">Outlook 2007</span> for email.</p>\n\n <p>Addresses:</p>\n <ul>\n <li class=\"label\">\n <span class=\"adr\">\n <span class=\"type\">Work</span>: \n <span class=\"street-address\">North Street</span>, \n <span class=\"locality\">Brighton</span>, \n <span class=\"country-name\">United Kingdom</span>\n </span>\n \n </li>\n <li class=\"label\">\n <span class=\"adr\">\n <span class=\"type\">Home</span>: \n <span class=\"street-address\">West Street</span>, \n <span class=\"locality\">Brighton</span>, \n <span class=\"country-name\">United Kingdom</span>\n </span>\n </li>\n </ul>\n \n <p>In emergency contact: <span class=\"agent\">Jane Doe</span> or <span class=\"agent vcard\"><span class=\"fn\">Dave Doe</span></span>.</p>\n <p>Key: <span class=\"key\">hd02$Gfu*d%dh87KTa2=23934532479</span></p>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["John Doe"],"given-name":["John"],"family-name":["Doe"],"sound":["http://www.madgex.com/johndoe.mpeg"],"photo":["http://example.com/images/photo.gif"],"nickname":["Man with no name","Lost boy"],"note":["John Doe is one of those names you always have issues with.","It can be a real problem booking a hotel room with the name John Doe."],"logo":["http://example.com/images/logo.gif","http://example.com/images/logo.gif"],"url":["http://www.madgex.com/","http://www.webfeetmedia.com/"],"org":["Madgex","Web Feet Media Ltd"],"job-title":["Creative Director","Owner"],"category":["design","development","web"],"tel":["+1 415 555 100","+1 415 555 200","+1 415 555 300"],"email":["mailto:john.doe@madgex.com","mailto:john.doe@webfeetmedia.com"],"mailer":["PigeonMail 2.1","Outlook 2007"],"label":["Work: \n North Street, \n Brighton, \n United Kingdom","Home: \n West Street, \n Brighton, \n United Kingdom"],"adr":[{"value":"Work: \n North Street, \n Brighton, \n United Kingdom","type":["h-adr"],"properties":{"street-address":["North Street"],"locality":["Brighton"],"country-name":["United Kingdom"]}},{"value":"Home: \n West Street, \n Brighton, \n United Kingdom","type":["h-adr"],"properties":{"street-address":["West Street"],"locality":["Brighton"],"country-name":["United Kingdom"]}}],"agent":["Jane Doe",{"value":"Dave Doe","type":["h-card"],"properties":{"name":["Dave Doe"]}}],"key":["hd02$Gfu*d%dh87KTa2=23934532479"]}}],"rels":{"tag":["http://en.wikipedia.org/wiki/design","http://en.wikipedia.org/wiki/development","http://en.wikipedia.org/wiki/web"]},"rel-urls":{"http://en.wikipedia.org/wiki/design":{"text":"design","rels":["tag"]},"http://en.wikipedia.org/wiki/development":{"text":"development","rels":["tag"]},"http://en.wikipedia.org/wiki/web":{"text":"web","rels":["tag"]}}};
+
+ it('multiple', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-name.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-name.js
new file mode 100644
index 0000000000..ef75899cff
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-name.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/name
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<base href=\"http://example.com\">\n<div class=\"vcard\">\n <div class=\"name\">\n <span class=\"honorific-prefix\">Dr</span> \n <span class=\"given-name\">John</span> \n <abbr class=\"additional-name\" title=\"Peter\">P</abbr> \n <span class=\"family-name\">Doe</span> \n <data class=\"honorific-suffix\" value=\"MSc\"></data>\n <img class=\"photo honorific-suffix\" src=\"images/logo.gif\" alt=\"PHD\" />\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"honorific-prefix":["Dr"],"given-name":["John"],"additional-name":["Peter"],"family-name":["Doe"],"honorific-suffix":["MSc","PHD"],"photo":["http://example.com/images/logo.gif"]}}],"rels":{},"rel-urls":{}};
+
+ it('name', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-single.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-single.js
new file mode 100644
index 0000000000..a7ef7628b8
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hcard-single.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hcard/single
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hcard', function() {
+ var htmlFragment = "<div class=\"vcard\">\n \n <div class=\"fn n\"><span class=\"given-name sort-string\">John</span> Doe</div>\n <div>Birthday: <abbr class=\"bday\" title=\"2000-01-01T00:00:00-08:00\">January 1st, 2000</abbr></div>\n <div>Role: <span class=\"role\">Designer</span></div>\n <div>Location: <abbr class=\"geo\" title=\"30.267991;-97.739568\">Brighton</abbr></div>\n <div>Time zone: <abbr class=\"tz\" title=\"-05:00\">Eastern Standard Time</abbr></div>\n \n <div>Profile details:\n <div>Profile id: <span class=\"uid\">http://example.com/profiles/johndoe</span></div>\n <div>Details are: <span class=\"class\">Public</span></div>\n <div>Last updated: <abbr class=\"rev\" title=\"2008-01-01T13:45:00\">January 1st, 2008 - 13:45</abbr></div>\n </div>\n </div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["John Doe"],"given-name":["John"],"sort-string":["John"],"bday":["2000-01-01 00:00:00-08:00"],"role":["Designer"],"geo":[{"value":"30.267991;-97.739568","type":["h-geo"],"properties":{"name":["30.267991;-97.739568"]}}],"tz":["-05:00"],"uid":["http://example.com/profiles/johndoe"],"class":["Public"],"rev":["2008-01-01 13:45:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('single', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hentry-summarycontent.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hentry-summarycontent.js
new file mode 100644
index 0000000000..5280efb047
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hentry-summarycontent.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hentry/summarycontent
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hentry', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"hentry\">\n <h1><a class=\"entry-title\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"entry-content\">\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"name":["microformats.org at 7"],"content":[{"value":"Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service","html":"\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n "}],"summary":["Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities."],"updated":["2012-06-25 17:08:26"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('summarycontent', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hfeed-simple.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hfeed-simple.js
new file mode 100644
index 0000000000..4c8294d499
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hfeed-simple.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hfeed/simple
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hfeed', function() {
+ var htmlFragment = "<section class=\"hfeed\">\n\t<h1 class=\"name\">Microformats blog</h1>\n\t<span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n\t<a class=\"url\" href=\"http://microformats.org/blog\">permlink</a>\n\t<img class=\"photo\" src=\"photo.jpeg\"/>\n\t<p>\n\t\tTags: <a rel=\"tag\" href=\"tags/microformats\">microformats</a>, \n\t\t<a rel=\"tag\" href=\"tags/html\">html</a>\n\t</p>\n\t\n\t<div class=\"hentry\">\n\t <h1><a class=\"entry-title\" rel=\"bookmark\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n\t <div class=\"entry-content\">\n\t <p class=\"entry-summary\">Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.</p>\n\t\n\t <p>The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service </p>\n\t </div> \n\t <p>Updated \n\t <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time>\n\t </p>\n\t</div>\n\t\n</section>";
+ var expected = {"items":[{"type":["h-feed"],"properties":{"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}],"url":["http://microformats.org/blog"],"photo":["http://example.com/photo.jpeg"],"category":["microformats","html"]},"children":[{"value":"microformats.org at 7\n\t \n\t Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.\n\t\n\t The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t principles, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service \n\t \n\t Updated \n\t June 25th, 2012","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.\n\t\n\t The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t principles, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service","html":"\n\t <p class=\"entry-summary\">Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.</p>\n\t\n\t <p>The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service </p>\n\t "}],"summary":["Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities."],"updated":["2012-06-25 17:08:26"]}}]}],"rels":{"tag":["http://example.com/tags/microformats","http://example.com/tags/html"],"bookmark":["http://microformats.org/2012/06/25/microformats-org-at-7"]},"rel-urls":{"http://example.com/tags/microformats":{"text":"microformats","rels":["tag"]},"http://example.com/tags/html":{"text":"html","rels":["tag"]},"http://microformats.org/2012/06/25/microformats-org-at-7":{"text":"microformats.org at 7","rels":["bookmark"]}}};
+
+ it('simple', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hnews-all.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hnews-all.js
new file mode 100644
index 0000000000..82eb37b958
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hnews-all.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hnews/all
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hnews', function() {
+ var htmlFragment = "<div class=\"hnews\">\n <div class=\"entry hentry\">\n <h1><a class=\"entry-title\" rel=\"bookmark\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"entry-content\">\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n </p>\n </div>\n\n <p>\n <span class=\"dateline vcard\">\n <span class=\"adr\">\n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n </span>\n </span>\n (Geo: <span class=\"geo\">37.774921;-122.445202</span>) \n <span class=\"source-org vcard\">\n <a class=\"fn org url\" href=\"http://microformats.org/\">microformats.org</a>\n </span>\n </p>\n <p>\n <a rel=\"principles\" href=\"http://microformats.org/wiki/Category:public_domain_license\">Publishing policy</a>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-news"],"properties":{"entry":[{"value":"microformats.org at 7","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service","html":"\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n "}],"summary":["Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities."],"updated":["2012-06-25 17:08:26"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}]}}],"dateline":[{"value":"San Francisco, \n CA","type":["h-card"],"properties":{"adr":[{"value":"San Francisco, \n CA","type":["h-adr"],"properties":{"locality":["San Francisco"],"region":["CA"]}}]}}],"geo":[{"value":"37.774921;-122.445202","type":["h-geo"],"properties":{"name":["37.774921;-122.445202"]}}],"source-org":[{"value":"microformats.org","type":["h-card"],"properties":{"name":["microformats.org"],"org":["microformats.org"],"url":["http://microformats.org/"]}}],"principles":["http://microformats.org/wiki/Category:public_domain_license"]}}],"rels":{"bookmark":["http://microformats.org/2012/06/25/microformats-org-at-7"],"principles":["http://microformats.org/wiki/Category:public_domain_license"]},"rel-urls":{"http://microformats.org/2012/06/25/microformats-org-at-7":{"text":"microformats.org at 7","rels":["bookmark"]},"http://microformats.org/wiki/Category:public_domain_license":{"text":"Publishing policy","rels":["principles"]}}};
+
+ it('all', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hnews-minimum.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hnews-minimum.js
new file mode 100644
index 0000000000..5faf13d7b8
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hnews-minimum.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hnews/minimum
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hnews', function() {
+ var htmlFragment = "<div class=\"hnews\">\n <div class=\"entry hentry\">\n <h1><a class=\"entry-title\" rel=\"bookmark\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"entry-content\">\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n </p>\n </div>\n\n <p class=\"source-org vcard\">\n <a class=\"fn org url\" href=\"http://microformats.org/\">microformats.org</a> \n </p>\n</div>";
+ var expected = {"items":[{"type":["h-news"],"properties":{"entry":[{"value":"microformats.org at 7","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service","html":"\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n "}],"summary":["Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities."],"updated":["2012-06-25 17:08:26"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}]}}],"source-org":[{"value":"microformats.org","type":["h-card"],"properties":{"name":["microformats.org"],"org":["microformats.org"],"url":["http://microformats.org/"]}}]}}],"rels":{"bookmark":["http://microformats.org/2012/06/25/microformats-org-at-7"]},"rel-urls":{"http://microformats.org/2012/06/25/microformats-org-at-7":{"text":"microformats.org at 7","rels":["bookmark"]}}};
+
+ it('minimum', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-aggregate.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-aggregate.js
new file mode 100644
index 0000000000..7171bc7264
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-aggregate.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hproduct/aggregate
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hproduct', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"hproduct\">\n <h2 class=\"fn\">Raspberry Pi</h2>\n <img class=\"photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"price\">£29.95</p>\n <p class=\"review hreview-aggregate\">\n <span class=\"rating\">\n <span class=\"average value\">9.2</span> out of \n <span class=\"best\">10</span> \n based on <span class=\"count\">178</span> reviews\n </span>\n </p>\n <p>Categories: \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/computer\" class=\"category\">Computer</a>, \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/education\" class=\"category\">Education</a>\n </p>\n <p class=\"brand vcard\">From: \n <span class=\"fn org\">The Raspberry Pi Foundation</span> - \n <span class=\"adr\">\n <span class=\"locality\">Cambridge</span> \n <span class=\"country-name\">UK</span>\n </span>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-product"],"properties":{"name":["Raspberry Pi"],"photo":["http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg"],"description":[{"value":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.","html":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming."}],"url":["http://www.raspberrypi.org/"],"price":["£29.95"],"review":[{"value":"9.2 out of \n 10 \n based on 178 reviews","type":["h-review-aggregate"],"properties":{"rating":["9.2"],"average":["9.2"],"best":["10"],"count":["178"]}}],"category":["Computer","Education"],"brand":[{"value":"The Raspberry Pi Foundation","type":["h-card"],"properties":{"name":["The Raspberry Pi Foundation"],"org":["The Raspberry Pi Foundation"],"adr":[{"value":"Cambridge \n UK","type":["h-adr"],"properties":{"locality":["Cambridge"],"country-name":["UK"]}}]}}]}}],"rels":{"tag":["http://en.wikipedia.org/wiki/computer","http://en.wikipedia.org/wiki/education"]},"rel-urls":{"http://en.wikipedia.org/wiki/computer":{"text":"Computer","rels":["tag"]},"http://en.wikipedia.org/wiki/education":{"text":"Education","rels":["tag"]}}};
+
+ it('aggregate', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-simpleproperties.js
new file mode 100644
index 0000000000..7ec61f27b5
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hproduct-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hproduct/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hproduct', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"hproduct\">\n <h2 class=\"fn\">Raspberry Pi</h2>\n <img class=\"photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"price\">£29.95</p>\n <p class=\"review hreview\"><span class=\"rating\">4.5</span> out of 5</p>\n <p>Categories: \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/computer\" class=\"category\">Computer</a>, \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/education\" class=\"category\">Education</a>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-product"],"properties":{"name":["Raspberry Pi"],"photo":["http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg"],"description":[{"value":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.","html":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming."}],"url":["http://www.raspberrypi.org/"],"price":["£29.95"],"category":["Computer","Education"],"review":[{"value":"4.5 out of 5","type":["h-review"],"properties":{"rating":["4.5"]}}]}}],"rels":{"tag":["http://en.wikipedia.org/wiki/computer","http://en.wikipedia.org/wiki/education"]},"rel-urls":{"http://en.wikipedia.org/wiki/computer":{"text":"Computer","rels":["tag"]},"http://en.wikipedia.org/wiki/education":{"text":"Education","rels":["tag"]}}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-affiliation.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-affiliation.js
new file mode 100644
index 0000000000..d580d68dbb
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-affiliation.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hresume/affiliation
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hresume', function() {
+ var htmlFragment = "<div class=\"hresume\">\n <p>\n <span class=\"contact vcard\"><span class=\"fn\">Tim Berners-Lee</span></span>, \n <span class=\"summary\">invented the World Wide Web</span>.\n </p>\n Belongs to following groups:\n <p> \n <a class=\"affiliation vcard\" href=\"http://www.w3.org/\">\n <img class=\"fn photo\" alt=\"W3C\" src=\"http://www.w3.org/Icons/WWW/w3c_home_nb.png\" />\n </a>\n </p> \n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"contact":[{"value":"Tim Berners-Lee","type":["h-card"],"properties":{"name":["Tim Berners-Lee"]}}],"summary":["invented the World Wide Web"],"affiliation":[{"type":["h-card"],"properties":{"name":["W3C"],"photo":["http://www.w3.org/Icons/WWW/w3c_home_nb.png"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('affiliation', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-contact.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-contact.js
new file mode 100644
index 0000000000..595087af44
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-contact.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hresume/contact
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hresume', function() {
+ var htmlFragment = "<div class=\"hresume\">\n <div class=\"contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"org\">MIT</p>\n <p class=\"adr\">\n <span class=\"street-address\">32 Vassar Street</span>, \n <span class=\"extended-address\">Room 32-G524</span>, \n <span class=\"locality\">Cambridge</span>, \n <span class=\"region\">MA</span> \n <span class=\"postal-code\">02139</span>, \n <span class=\"country-name\">USA</span>. \n (<span class=\"type\">Work</span>)\n </p>\n <p>Tel:<span class=\"tel\">+1 (617) 253 5702</span></p>\n <p>Email:<a class=\"email\" href=\"mailto:timbl@w3.org\">timbl@w3.org</a></p>\n </div>\n <p class=\"summary\">Invented the World Wide Web.</p>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"contact":[{"value":"Tim Berners-Lee","type":["h-card"],"properties":{"name":["Tim Berners-Lee"],"org":["MIT"],"adr":[{"value":"32 Vassar Street, \n Room 32-G524, \n Cambridge, \n MA \n 02139, \n USA. \n (Work)","type":["h-adr"],"properties":{"street-address":["32 Vassar Street"],"extended-address":["Room 32-G524"],"locality":["Cambridge"],"region":["MA"],"postal-code":["02139"],"country-name":["USA"]}}],"tel":["+1 (617) 253 5702"],"email":["mailto:timbl@w3.org"]}}],"summary":["Invented the World Wide Web."]}}],"rels":{},"rel-urls":{}};
+
+ it('contact', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-education.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-education.js
new file mode 100644
index 0000000000..7a0114f538
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-education.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hresume/education
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hresume', function() {
+ var htmlFragment = "<div class=\"hresume\">\n <div class=\"contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"summary\">Invented the World Wide Web.</p><hr />\n <p class=\"education vevent vcard\">\n <span class=\"fn summary org\">The Queen's College, Oxford University</span>, \n <span class=\"description\">BA Hons (I) Physics</span> \n <time class=\"dtstart\" datetime=\"1973-09\">1973</time> –\n <time class=\"dtend\" datetime=\"1976-06\">1976</time>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"contact":[{"value":"Tim Berners-Lee","type":["h-card"],"properties":{"name":["Tim Berners-Lee"],"job-title":["Director of the World Wide Web Foundation"]}}],"summary":["Invented the World Wide Web."],"education":[{"value":"The Queen's College, Oxford University","type":["h-event","h-card"],"properties":{"name":["The Queen's College, Oxford University"],"org":["The Queen's College, Oxford University"],"description":["BA Hons (I) Physics"],"start":["1973-09"],"end":["1976-06"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('education', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-skill.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-skill.js
new file mode 100644
index 0000000000..b082567004
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-skill.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hresume/skill
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hresume', function() {
+ var htmlFragment = "<div class=\"hresume\"> \n <p>\n <span class=\"contact vcard\"><span class=\"fn\">Tim Berners-Lee</span></span>, \n <span class=\"summary\">invented the World Wide Web</span>.\n </p>\n Skills: \n <ul>\n <li><a class=\"skill\" rel=\"tag\" href=\"http://example.com/skills/informationsystems\">information systems</a></li>\n <li><a class=\"skill\" rel=\"tag\" href=\"http://example.com/skills/advocacy\">advocacy</a></li>\n <li><a class=\"skill\" rel=\"tag\" href=\"http://example.com/skills/leadership\">leadership</a></li>\n </ul>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"contact":[{"value":"Tim Berners-Lee","type":["h-card"],"properties":{"name":["Tim Berners-Lee"]}}],"summary":["invented the World Wide Web"],"skill":["information systems","advocacy","leadership"]}}],"rels":{"tag":["http://example.com/skills/informationsystems","http://example.com/skills/advocacy","http://example.com/skills/leadership"]},"rel-urls":{"http://example.com/skills/informationsystems":{"text":"information systems","rels":["tag"]},"http://example.com/skills/advocacy":{"text":"advocacy","rels":["tag"]},"http://example.com/skills/leadership":{"text":"leadership","rels":["tag"]}}};
+
+ it('skill', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-work.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-work.js
new file mode 100644
index 0000000000..4ece3a3890
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hresume-work.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hresume/work
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hresume', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"hresume\">\n <div class=\"contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"summary\">Invented the World Wide Web.</p><hr />\n <div class=\"experience vevent vcard\">\n <p class=\"title\">Director</p>\n <p><a class=\"fn summary org url\" href=\"http://www.webfoundation.org/\">World Wide Web Foundation</a></p>\n <p>\n <time class=\"dtstart\" datetime=\"2009-01-18\">Jan 2009</time> – Present\n <time class=\"duration\" datetime=\"P2Y11M\">(2 years 11 month)</time>\n </p>\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"contact":[{"value":"Tim Berners-Lee","type":["h-card"],"properties":{"name":["Tim Berners-Lee"],"job-title":["Director of the World Wide Web Foundation"]}}],"summary":["Invented the World Wide Web."],"experience":[{"value":"World Wide Web Foundation","type":["h-event","h-card"],"properties":{"job-title":["Director"],"name":["World Wide Web Foundation"],"org":["World Wide Web Foundation"],"url":["http://www.webfoundation.org/"],"start":["2009-01-18"],"duration":["P2Y11M"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('work', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-hcard.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-hcard.js
new file mode 100644
index 0000000000..6cdf65484f
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-hcard.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hreview-aggregate/hcard
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hreview-aggregate', function() {
+ var htmlFragment = "<div class=\"hreview-aggregate\">\n <div class=\"item vcard\">\n <h3 class=\"fn org\">Mediterranean Wraps</h3> \n <p>\n <span class=\"adr\">\n <span class=\"street-address\">433 S California Ave</span>, \n <span class=\"locality\">Palo Alto</span>, \n <span class=\"region\">CA</span></span> - \n \n <span class=\"tel\">(650) 321-8189</span>\n </p>\n </div> \n <p class=\"rating\">\n <span class=\"average value\">9.2</span> out of \n <span class=\"best\">10</span> \n based on <span class=\"count\">17</span> reviews\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-review-aggregate"],"properties":{"item":[{"value":"Mediterranean Wraps","type":["h-item","h-card"],"properties":{"name":["Mediterranean Wraps"],"org":["Mediterranean Wraps"],"adr":[{"value":"433 S California Ave, \n Palo Alto, \n CA","type":["h-adr"],"properties":{"street-address":["433 S California Ave"],"locality":["Palo Alto"],"region":["CA"]}}],"tel":["(650) 321-8189"]}}],"rating":["9.2"],"average":["9.2"],"best":["10"],"count":["17"]}}],"rels":{},"rel-urls":{}};
+
+ it('hcard', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-justahyperlink.js
new file mode 100644
index 0000000000..56d106fdbf
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hreview-aggregate/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hreview-aggregate', function() {
+ var htmlFragment = "<p class=\"hreview-aggregate\">\n <span class=\"item\">\n <a class=\"fn url\" href=\"http://example.com/mediterraneanwraps\">Mediterranean Wraps</a>\n </span> - Rated: \n <span class=\"rating\">4.5</span> out of 5 (<span class=\"count\">6</span> reviews)\n</p>";
+ var expected = {"items":[{"type":["h-review-aggregate"],"properties":{"item":[{"value":"Mediterranean Wraps","type":["h-item"],"properties":{"name":["Mediterranean Wraps"],"url":["http://example.com/mediterraneanwraps"]}}],"rating":["4.5"],"count":["6"]}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-vevent.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-vevent.js
new file mode 100644
index 0000000000..896bbdc1d3
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-aggregate-vevent.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hreview-aggregate/vevent
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hreview-aggregate', function() {
+ var htmlFragment = "<div class=\"hreview-aggregate\">\n <div class=\"item vevent\">\n <h3 class=\"summary\">Fullfrontal</h3>\n <p class=\"description\">A one day JavaScript Conference held in Brighton</p>\n <p><time class=\"dtstart\" datetime=\"2012-11-09\">9th November 2012</time></p> \n </div> \n \n <p class=\"rating\">\n <span class=\"average value\">9.9</span> out of \n <span class=\"best\">10</span> \n based on <span class=\"count\">62</span> reviews\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-review-aggregate"],"properties":{"item":[{"value":"Fullfrontal","type":["h-item","h-event"],"properties":{"name":["Fullfrontal"],"description":["A one day JavaScript Conference held in Brighton"],"start":["2012-11-09"]}}],"rating":["9.9"],"average":["9.9"],"best":["10"],"count":["62"]}}],"rels":{},"rel-urls":{}};
+
+ it('vevent', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-item.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-item.js
new file mode 100644
index 0000000000..4a00ac46a1
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-item.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hreview/item
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hreview', function() {
+ var htmlFragment = "<base href=\"http://example.com\">\n<div class=\"hreview\">\n <p class=\"item\">\n <img class=\"photo\" src=\"images/photo.gif\" />\n <a class=\"fn url\" href=\"http://example.com/crepeoncole\">Crepes on Cole</a>\n </p>\n <p><span class=\"rating\">5</span> out of 5 stars</p>\n</div>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"item":[{"value":"Crepes on Cole","type":["h-item"],"properties":{"photo":["http://example.com/images/photo.gif"],"name":["Crepes on Cole"],"url":["http://example.com/crepeoncole"]}}],"rating":["5"]}}],"rels":{},"rel-urls":{}};
+
+ it('item', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-vcard.js b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-vcard.js
new file mode 100644
index 0000000000..d59decb7fa
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-hreview-vcard.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/hreview/vcard
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('hreview', function() {
+ var htmlFragment = "<div class=\"hreview\">\n <span><span class=\"rating\">5</span> out of 5 stars</span>\n <h4 class=\"summary\">Crepes on Cole is awesome</h4>\n <span class=\"reviewer vcard\">\n Reviewer: <span class=\"fn\">Tantek</span> - \n </span>\n <time class=\"reviewed\" datetime=\"2005-04-18\">April 18, 2005</time>\n <div class=\"description\">\n <p class=\"item vcard\">\n <span class=\"fn org\">Crepes on Cole</span> is one of the best little \n creperies in <span class=\"adr\"><span class=\"locality\">San Francisco</span></span>.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.\n </p>\n </div>\n <p>Visit date: <span>April 2005</span></p>\n <p>Food eaten: <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/crepe\">crepe</a></p>\n <p>Permanent link for review: <a rel=\"self bookmark\" href=\"http://example.com/crepe\">http://example.com/crepe</a></p>\n <p><a rel=\"license\" href=\"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\">Creative Commons Attribution-ShareAlike License</a></p>\n</div>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"rating":["5"],"name":["Crepes on Cole is awesome"],"reviewer":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"]}}],"description":[{"value":"Crepes on Cole is one of the best little \n creperies in San Francisco.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.","html":"\n <p class=\"item vcard\">\n <span class=\"fn org\">Crepes on Cole</span> is one of the best little \n creperies in <span class=\"adr\"><span class=\"locality\">San Francisco</span></span>.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.\n </p>\n "}],"item":[{"value":"Crepes on Cole","type":["h-item","h-card"],"properties":{"name":["Crepes on Cole"],"org":["Crepes on Cole"],"adr":[{"value":"San Francisco","type":["h-adr"],"properties":{"locality":["San Francisco"]}}]}}],"category":["crepe"],"url":["http://example.com/crepe"]}}],"rels":{"tag":["http://en.wikipedia.org/wiki/crepe"],"self":["http://example.com/crepe"],"bookmark":["http://example.com/crepe"],"license":["http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License"]},"rel-urls":{"http://en.wikipedia.org/wiki/crepe":{"text":"crepe","rels":["tag"]},"http://example.com/crepe":{"text":"http://example.com/crepe","rels":["self","bookmark"]},"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License":{"text":"Creative Commons Attribution-ShareAlike License","rels":["license"]}}};
+
+ it('vcard', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-includes-hcarditemref.js b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-hcarditemref.js
new file mode 100644
index 0000000000..5ffa373d75
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-hcarditemref.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/includes/hcarditemref
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('includes', function() {
+ var htmlFragment = "<div class=\"vcard\" itemref=\"mozilla-org mozilla-adr\">\n <span class=\"name\">Brendan Eich</span>\n</div>\n<div class=\"vcard\" itemref=\"mozilla-org mozilla-adr\">\n <span class=\"name\">Mitchell Baker</span>\n</div>\n\n<p id=\"mozilla-org\" class=\"org\">Mozilla</p>\n<p id=\"mozilla-adr\" class=\"adr\">\n <span class=\"street-address\">665 3rd St.</span> \n <span class=\"extended-address\">Suite 207</span> \n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n <span class=\"postal-code\">94107</span> \n <span class=\"country-name\">U.S.A.</span> \n</p>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"org":["Mozilla"],"adr":[{"value":"665 3rd St. \n Suite 207 \n San Francisco, \n CA \n 94107 \n U.S.A.","type":["h-adr"],"properties":{"street-address":["665 3rd St."],"extended-address":["Suite 207"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94107"],"country-name":["U.S.A."]}}]}},{"type":["h-card"],"properties":{"org":["Mozilla"],"adr":[{"value":"665 3rd St. \n Suite 207 \n San Francisco, \n CA \n 94107 \n U.S.A.","type":["h-adr"],"properties":{"street-address":["665 3rd St."],"extended-address":["Suite 207"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94107"],"country-name":["U.S.A."]}}]}},{"type":["h-adr"],"properties":{"street-address":["665 3rd St."],"extended-address":["Suite 207"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94107"],"country-name":["U.S.A."]}}],"rels":{},"rel-urls":{}};
+
+ it('hcarditemref', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-includes-heventitemref.js b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-heventitemref.js
new file mode 100644
index 0000000000..b3a16025bb
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-heventitemref.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/includes/heventitemref
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('includes', function() {
+ var htmlFragment = "<div class=\"vevent\" itemref=\"io-session07\">\n <span class=\"name\">Monetizing Android Apps</span> - spaekers: \n <span class=\"speaker\">Chrix Finne</span>, \n <span class=\"speaker\">Kenneth Lui</span> - \n <span itemref=\"io-location\" class=\"location adr\">\n <span class=\"extended-address\">Room 10</span>\n </span> \n</div>\n<div class=\"vevent\" itemref=\"io-session07\">\n <span class=\"name\">New Low-Level Media APIs in Android</span> - spaekers: \n <span class=\"speaker\">Dave Burke</span> -\n <span itemref=\"io-location\" class=\"location adr\">\n <span class=\"extended-address\">Room 11</span>\n </span> \n</div>\n\n<p id=\"io-session07\">\n Session 01 is between: \n <time class=\"dtstart\" datetime=\"2012-06-27T15:45:00-0800\">3:45PM</time> to \n <time class=\"dtend\" datetime=\"2012-06-27T16:45:00-0800\">4:45PM</time> \n</p> \n<p id=\"io-location\">\n <span class=\"extended-address\">Moscone Center</span>, \n <span class=\"locality\">San Francisco</span> \n</p>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"location":[{"value":"Room 10\n \n Moscone Center, \n San Francisco","type":["h-adr"],"properties":{"extended-address":["Room 10","Moscone Center"],"locality":["San Francisco"]}}],"start":["2012-06-27 15:45:00-08:00"],"end":["2012-06-27 16:45:00-08:00"]}},{"type":["h-event"],"properties":{"location":[{"value":"Room 11\n \n Moscone Center, \n San Francisco","type":["h-adr"],"properties":{"extended-address":["Room 11","Moscone Center"],"locality":["San Francisco"]}}],"start":["2012-06-27 15:45:00-08:00"],"end":["2012-06-27 16:45:00-08:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('heventitemref', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-includes-hyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-hyperlink.js
new file mode 100644
index 0000000000..3a789bb1ba
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-hyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/includes/hyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('includes', function() {
+ var htmlFragment = "<div class=\"vcard\">\n <span class=\"name\">Ben Ward</span>\n <a class=\"include\" href=\"#twitter\">Twitter</a>\n</div>\n<div class=\"vcard\">\n <span class=\"name\">Dan Webb</span>\n <a class=\"include\" href=\"#twitter\">Twitter</a>\n</div>\n\n<div id=\"twitter\">\n <p class=\"org\">Twitter</p>\n <p class=\"adr\">\n <span class=\"street-address\">1355 Market St</span>,\n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span>\n <span class=\"postal-code\">94103</span>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"org":["Twitter"],"adr":[{"value":"1355 Market St,\n San Francisco, \n CA\n 94103","type":["h-adr"],"properties":{"street-address":["1355 Market St"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94103"]}}]}},{"type":["h-card"],"properties":{"org":["Twitter"],"adr":[{"value":"1355 Market St,\n San Francisco, \n CA\n 94103","type":["h-adr"],"properties":{"street-address":["1355 Market St"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94103"]}}]}},{"type":["h-adr"],"properties":{"street-address":["1355 Market St"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94103"]}}],"rels":{},"rel-urls":{}};
+
+ it('hyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-includes-object.js b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-object.js
new file mode 100644
index 0000000000..3bc15bd459
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-object.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/includes/object
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('includes', function() {
+ var htmlFragment = "<div class=\"vevent\">\n <span class=\"name\">HTML5 & CSS3 latest features in action!</span> - \n <span class=\"speaker\">David Rousset</span> -\n <time class=\"dtstart\" datetime=\"2012-10-30T11:45:00-08:00\">Tue 11:45am</time>\n <object data=\"#buildconf\" class=\"include\" type=\"text/html\" height=\"0\" width=\"0\"></object>\n</div>\n<div class=\"vevent\">\n <span class=\"name\">Building High-Performing JavaScript for Modern Engines</span> -\n <span class=\"speaker\">John-David Dalton</span> and \n <span class=\"speaker\">Amanda Silver</span> -\n <time class=\"dtstart\" datetime=\"2012-10-31T11:15:00-08:00\">Wed 11:15am</time>\n <object data=\"#buildconf\" class=\"include\" type=\"text/html\" height=\"0\" width=\"0\"></object>\n</div>\n\n\n<div id=\"buildconf\">\n <p class=\"summary\">Build Conference</p>\n <p class=\"location adr\">\n <span class=\"locality\">Redmond</span>, \n <span class=\"region\">Washington</span>, \n <span class=\"country-name\">USA</span>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"start":["2012-10-30 11:45:00-08:00"],"name":["Build Conference"],"location":[{"value":"Redmond, \n Washington, \n USA","type":["h-adr"],"properties":{"locality":["Redmond"],"region":["Washington"],"country-name":["USA"]}}]}},{"type":["h-event"],"properties":{"start":["2012-10-31 11:15:00-08:00"],"name":["Build Conference"],"location":[{"value":"Redmond, \n Washington, \n USA","type":["h-adr"],"properties":{"locality":["Redmond"],"region":["Washington"],"country-name":["USA"]}}]}},{"type":["h-adr"],"properties":{"locality":["Redmond"],"region":["Washington"],"country-name":["USA"]}}],"rels":{},"rel-urls":{}};
+
+ it('object', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v1-includes-table.js b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-table.js
new file mode 100644
index 0000000000..a0d3ef55c1
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v1-includes-table.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v1/includes/table
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('includes', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<table>\n <tr>\n <th id=\"org\"><a class=\"url org\" href=\"http://dev.opera.com/\">Opera</a></th>\n </tr>\n <tr>\n <td class=\"vcard\" headers=\"org\"><span class=\"fn\">Chris Mills</span></td>\n </tr>\n <tr>\n <td class=\"vcard\" headers=\"org\"><span class=\"fn\">Erik Möller</span></td>\n </tr>\n </table>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Chris Mills"],"url":["http://dev.opera.com/"],"org":["Opera"]}},{"type":["h-card"],"properties":{"name":["Erik Möller"],"url":["http://dev.opera.com/"],"org":["Opera"]}}],"rels":{},"rel-urls":{}};
+
+ it('table', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geo.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geo.js
new file mode 100644
index 0000000000..8ed7d747d4
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geo.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-adr/geo
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-adr', function() {
+ var htmlFragment = "<p class=\"h-adr\">\n <span class=\"p-name\">Bricklayer's Arms</span>\n <span class=\"p-label\"> \n <span class=\"p-street-address\">3 Charlotte Road</span>, \n <span class=\"p-locality\">City of London</span>, \n <span class=\"p-postal-code\">EC2A 3PE</span>, \n <span class=\"p-country-name\">UK</span> \n </span> – \n Geo:(<span class=\"p-geo\">51.526421;-0.081067</span>) \n</p>";
+ var expected = {"items":[{"type":["h-adr"],"properties":{"name":["Bricklayer's Arms"],"label":["3 Charlotte Road, \n City of London, \n EC2A 3PE, \n UK"],"street-address":["3 Charlotte Road"],"locality":["City of London"],"postal-code":["EC2A 3PE"],"country-name":["UK"],"geo":["51.526421;-0.081067"]}}],"rels":{},"rel-urls":{}};
+
+ it('geo', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geourl.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geourl.js
new file mode 100644
index 0000000000..b97e76f60a
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-geourl.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-adr/geourl
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-adr', function() {
+ var htmlFragment = "<p class=\"h-adr\">\n <a class=\"p-name u-geo\" href=\"geo:51.526421;-0.081067;crs=wgs84;u=40\">Bricklayer's Arms</a>, \n <span class=\"p-locality\">London</span> \n</p>";
+ var expected = {"items":[{"type":["h-adr"],"properties":{"name":["Bricklayer's Arms"],"geo":["geo:51.526421;-0.081067;crs=wgs84;u=40"],"locality":["London"],"url":["geo:51.526421;-0.081067;crs=wgs84;u=40"]}}],"rels":{},"rel-urls":{}};
+
+ it('geourl', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-justaname.js
new file mode 100644
index 0000000000..c943fbafce
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-adr/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-adr', function() {
+ var htmlFragment = "<p class=\"h-adr\">665 3rd St. Suite 207 San Francisco, CA 94107 U.S.A.</p>";
+ var expected = {"items":[{"type":["h-adr"],"properties":{"name":["665 3rd St. Suite 207 San Francisco, CA 94107 U.S.A."]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-simpleproperties.js
new file mode 100644
index 0000000000..084dac4405
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-adr-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-adr/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-adr', function() {
+ var htmlFragment = "<p class=\"h-adr\">\n <span class=\"p-street-address\">665 3rd St.</span> \n <span class=\"p-extended-address\">Suite 207</span> \n <span class=\"p-locality\">San Francisco</span>, \n <span class=\"p-region\">CA</span> \n <span class=\"p-postal-code\">94107</span> \n <span class=\"p-country-name\">U.S.A.</span> \n</p>";
+ var expected = {"items":[{"type":["h-adr"],"properties":{"street-address":["665 3rd St."],"extended-address":["Suite 207"],"locality":["San Francisco"],"region":["CA"],"postal-code":["94107"],"country-name":["U.S.A."],"name":["665 3rd St. \n Suite 207 \n San Francisco, \n CA \n 94107 \n U.S.A."]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-as-note-note.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-as-note-note.js
new file mode 100644
index 0000000000..7e0ac260c4
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-as-note-note.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-as-note/note
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-as-note', function() {
+ var htmlFragment = "<!-- http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy -->\n<base href=\"http://tantek.com/\" />\n\n<li class=\"h-entry hentry h-as-note\">\n <div>\n <ul>\n <li>\n <a href=\"152/t1/congrats-fellow-elected-w3cab-members\" id=\"previtem\" title=\"View the previous (older) item in the stream.\"\n rel=\"prev\"><abbr title=\"Previous\">←</abbr></a>\n </li>\n <li>\n <a href=\"152/t3/going-indiewebcamp-2015-portland\" id=\"nextitem\" title=\"View the next (newer) item in the stream\" rel=\"next\"><abbr title=\"Next\">→</abbr></a>\n </li>\n </ul>\n </div>\n <div>In reply to:\n <p>\n <a class=\"u-in-reply-to h-cite\" rel=\"in-reply-to\" href=\"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\">http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far</a>\n </p>\n <p>\n <a class=\"u-in-reply-to h-cite\" rel=\"in-reply-to\" href=\"https://twitter.com/benwerd/status/604733231284383744\">https://twitter.com/benwerd/status/604733231284383744</a>\n </p>\n <hr>\n </div>\n <a href=\"../\" class=\"p-author h-card\" rel=\"author\" title=\"Tantek Çelik\"><img src=\"/images/photo.gif\" alt=\"Tantek Çelik\"></a>\n <p class=\"p-name entry-title e-content entry-content article\">\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/benwerd\">@benwerd</a>\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/erinjo\">@erinjo</a>also proud of you &amp;\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/withknown\">@withknown</a>— so much #indieweb &amp; especially user empathy. Keep up the great work!</p>\n <span>\n <span class=\"dt-published published dt-updated updated\">\n <time class=\"value\" datetime=\"22:20-0700\">22:20</time>on\n <time class=\"value\">2015-06-01</time>\n </span>\n <span class=\"lt\">(ttk.me t4bT2)</span>using\n <span class=\"using\">BBEdit</span>\n </span>\n <div>\n <form action=\"http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy\">\n <div>\n <label>\n <span class=\"lt\">URL:</span>\n <input class=\"u-url url u-uid uid bookmark\" type=\"url\" size=\"70\" style=\"max-width:100%\" value=\"http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy\">\n </label>\n </div>\n </form>\n </div>\n <div>\n <a class=\"u-syndication\" rel=\"syndication\" style=\"float:right;\" href=\"https://twitter.com/t/status/605604965566906369\">\n <img src=\"/images/photo.gif\" style=\"vertical-align:-30%\" alt=\"\"> \n View \n Conversation\n on Twitter\n</a>\n </div>\n</li>";
+ var expected = {"items":[{"type":["h-entry","h-as-note"],"properties":{"in-reply-to":[{"value":"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far","type":["h-cite"],"properties":{"name":["http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far"],"url":["http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far"]}},{"value":"https://twitter.com/benwerd/status/604733231284383744","type":["h-cite"],"properties":{"name":["https://twitter.com/benwerd/status/604733231284383744"],"url":["https://twitter.com/benwerd/status/604733231284383744"]}}],"author":[{"type":["h-card"],"properties":{"name":["Tantek Çelik"],"photo":["http://tantek.com/images/photo.gif"],"url":["http://tantek.com/"]}}],"name":["@benwerd\n @erinjoalso proud of you &\n @withknown— so much #indieweb & especially user empathy. Keep up the great work!"],"content":[{"value":"@benwerd\n @erinjoalso proud of you &\n @withknown— so much #indieweb & especially user empathy. Keep up the great work!","html":"\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/benwerd\">@benwerd</a>\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/erinjo\">@erinjo</a>also proud of you &\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/withknown\">@withknown</a>— so much #indieweb & especially user empathy. Keep up the great work!"}],"published":["2015-06-01 22:20-07:00"],"updated":["2015-06-01 22:20-07:00"],"url":["http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy"],"uid":["http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy"],"syndication":["https://twitter.com/t/status/605604965566906369"]},"children":[{"value":"@benwerd","type":["h-x-username"],"properties":{"name":["@benwerd"],"url":["https://twitter.com/benwerd"]}},{"value":"@erinjo","type":["h-x-username"],"properties":{"name":["@erinjo"],"url":["https://twitter.com/erinjo"]}},{"value":"@withknown","type":["h-x-username"],"properties":{"name":["@withknown"],"url":["https://twitter.com/withknown"]}}]}],"rels":{"prev":["http://tantek.com/152/t1/congrats-fellow-elected-w3cab-members"],"next":["http://tantek.com/152/t3/going-indiewebcamp-2015-portland"],"in-reply-to":["http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far","https://twitter.com/benwerd/status/604733231284383744"],"author":["http://tantek.com/"],"syndication":["https://twitter.com/t/status/605604965566906369"]},"rel-urls":{"http://tantek.com/152/t1/congrats-fellow-elected-w3cab-members":{"title":"View the previous (older) item in the stream.","text":"←","rels":["prev"]},"http://tantek.com/152/t3/going-indiewebcamp-2015-portland":{"title":"View the next (newer) item in the stream","text":"→","rels":["next"]},"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far":{"text":"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far","rels":["in-reply-to"]},"https://twitter.com/benwerd/status/604733231284383744":{"text":"https://twitter.com/benwerd/status/604733231284383744","rels":["in-reply-to"]},"http://tantek.com/":{"title":"Tantek Çelik","rels":["author"]},"https://twitter.com/t/status/605604965566906369":{"text":"View \n Conversation\n on Twitter","rels":["syndication"]}}};
+
+ it('note', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-baseurl.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-baseurl.js
new file mode 100644
index 0000000000..d098db3926
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-baseurl.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/baseurl
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<base href=\"http://example.org\"/>\n<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card\" href=\"bios/mitchell-baker/\">Mozilla Foundation</a>)\n <img class=\"u-photo\" src=\"images/photo.gif\"/>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Mitchell Baker"],"url":["http://blog.lizardwrangler.com/"],"org":[{"value":"Mozilla Foundation","type":["h-card"],"properties":{"name":["Mozilla Foundation"],"url":["http://example.org/bios/mitchell-baker/"]}}],"photo":["http://example.org/images/photo.gif"]}}],"rels":{},"rel-urls":{}};
+
+ it('baseurl', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-childimplied.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-childimplied.js
new file mode 100644
index 0000000000..3ab1fa8ca6
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-childimplied.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/childimplied
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<a class=\"h-card\" href=\"http://people.opera.com/howcome/\" title=\"Håkon Wium Lie, CTO Opera\">\n <article>\n <h2 class=\"p-name\">Håkon Wium Lie</h2>\n <img src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/9/96/H%C3%A5kon-Wium-Lie-2009-03.jpg/215px-H%C3%A5kon-Wium-Lie-2009-03.jpg\" />\n </article>\n</a>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Håkon Wium Lie"],"photo":["http://upload.wikimedia.org/wikipedia/commons/thumb/9/96/H%C3%A5kon-Wium-Lie-2009-03.jpg/215px-H%C3%A5kon-Wium-Lie-2009-03.jpg"],"url":["http://people.opera.com/howcome/"]}}],"rels":{},"rel-urls":{}};
+
+ it('childimplied', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-extendeddescription.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-extendeddescription.js
new file mode 100644
index 0000000000..8ee35b022b
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-extendeddescription.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/extendeddescription
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<div class=\"h-card\">\n <img class=\"u-photo\" alt=\"photo of Mitchell\" src=\"http://blog.mozilla.org/press/files/2012/04/mitchell-baker.jpg\" />\n <p>\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a>\n (<a class=\"u-url\" href=\"https://twitter.com/MitchellBaker\">@MitchellBaker</a>)\n <span class=\"p-org\">Mozilla Foundation</span>\n </p>\n <p class=\"p-note\">Mitchell is responsible for setting the direction and scope of the Mozilla Foundation and its activities.</p>\n <p><span class=\"p-category\">Strategy</span> and <span class=\"p-category\">Leadership</span></p>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"photo":["http://blog.mozilla.org/press/files/2012/04/mitchell-baker.jpg"],"url":["http://blog.lizardwrangler.com/","https://twitter.com/MitchellBaker"],"name":["Mitchell Baker"],"org":["Mozilla Foundation"],"note":["Mitchell is responsible for setting the direction and scope of the Mozilla Foundation and its activities."],"category":["Strategy","Leadership"]}}],"rels":{},"rel-urls":{}};
+
+ it('extendeddescription', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hcard.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hcard.js
new file mode 100644
index 0000000000..9613816109
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hcard.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/hcard
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card\" href=\"http://mozilla.org/\">Mozilla Foundation</a>)\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"url":["http://blog.lizardwrangler.com/"],"name":["Mitchell Baker"],"org":[{"value":"Mozilla Foundation","type":["h-card"],"properties":{"name":["Mozilla Foundation"],"url":["http://mozilla.org/"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('hcard', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-horghcard.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-horghcard.js
new file mode 100644
index 0000000000..bc6329ae62
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-horghcard.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/horghcard
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card h-org\" href=\"http://mozilla.org/\">Mozilla Foundation</a>)\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Mitchell Baker"],"url":["http://blog.lizardwrangler.com/"],"org":[{"value":"Mozilla Foundation","type":["h-card","h-org"],"properties":{"name":["Mozilla Foundation"],"url":["http://mozilla.org/"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('horghcard', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hyperlinkedphoto.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hyperlinkedphoto.js
new file mode 100644
index 0000000000..70febcb844
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-hyperlinkedphoto.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/hyperlinkedphoto
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<a class=\"h-card\" href=\"http://rohit.khare.org/\">\n <img alt=\"Rohit Khare\" src=\"images/photo.gif\" />\n </a>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Rohit Khare"],"photo":["http://example.com/images/photo.gif"],"url":["http://rohit.khare.org/"]}}],"rels":{},"rel-urls":{}};
+
+ it('hyperlinkedphoto', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedname.js
new file mode 100644
index 0000000000..bcf45aecb6
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/impliedname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "\n<img class=\"h-card\" src=\"jane.html\" alt=\"Jane Doe\"/>\n<area class=\"h-card\" href=\"jane.html\" alt=\"Jane Doe\"></area>\n<abbr class=\"h-card\" title=\"Jane Doe\">JD</abbr>\n\n<div class=\"h-card\"><img src=\"jane.html\" alt=\"Jane Doe\"/></div>\n<div class=\"h-card\"><area href=\"jane.html\" alt=\"Jane Doe\"></area></div>\n<div class=\"h-card\"><abbr title=\"Jane Doe\">JD</abbr></div>\n\n<div class=\"h-card\"><span><img src=\"jane.html\" alt=\"Jane Doe\"/></span></div>\n<div class=\"h-card\"><span><area href=\"jane.html\" alt=\"Jane Doe\"></area></span></div>\n<div class=\"h-card\"><span><abbr title=\"Jane Doe\">JD</abbr></span></div>\n\n<div class=\"h-card\"><img class=\"h-card\" src=\"john.html\" alt=\"John Doe\"/>Name</div>\n<div class=\"h-card\"><span class=\"h-card\"><img src=\"john.html\" alt=\"John Doe\"/>Name</span></div>\n";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"]}},{"type":["h-card"],"properties":{"name":["Name"]},"children":[{"type":["h-card"],"properties":{"name":["John Doe"],"photo":["http://example.com/john.html"]}}]},{"type":["h-card"],"properties":{"name":["Name"]},"children":[{"value":"Name","type":["h-card"],"properties":{"name":["John Doe"],"photo":["http://example.com/john.html"]}}]}],"rels":{},"rel-urls":{}};
+
+ it('impliedname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedphoto.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedphoto.js
new file mode 100644
index 0000000000..3248a1d0fc
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedphoto.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/impliedphoto
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<img class=\"h-card\" alt=\"Jane Doe\" src=\"jane.jpeg\"/>\n<object class=\"h-card\" data=\"jane.jpeg\"/>Jane Doe</object>\n\n<div class=\"h-card\"><img alt=\"Jane Doe\" src=\"jane.jpeg\"/></div> \n<div class=\"h-card\"><object data=\"jane.jpeg\"/>Jane Doe</object></div> \n\n<div class=\"h-card\"><span><img alt=\"Jane Doe\" src=\"jane.jpeg\"/></span></div> \n<div class=\"h-card\"><span><object data=\"jane.jpeg\"/>Jane Doe</object></span></div> \n\n<div class=\"h-card\"><img class=\"h-card\" alt=\"Jane Doe\" src=\"jane.jpeg\"/>Jane Doe</div> \n<div class=\"h-card\"><span class=\"h-card\"><object data=\"jane.jpeg\"/>Jane Doe</object></span></div> ";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"]},"children":[{"type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}}]},{"type":["h-card"],"properties":{"name":["Jane Doe"]},"children":[{"value":"Jane Doe","type":["h-card"],"properties":{"name":["Jane Doe"],"photo":["http://example.com/jane.jpeg"]}}]}],"rels":{},"rel-urls":{}};
+
+ it('impliedphoto', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedurl.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedurl.js
new file mode 100644
index 0000000000..4034194ce9
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-impliedurl.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/impliedurl
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<a class=\"h-card\" href=\"jane.html\">Jane Doe</a>\n<area class=\"h-card\" href=\"jane.html\" alt=\"Jane Doe\"/ >\n<div class=\"h-card\" ><a href=\"jane.html\">Jane Doe</a><p></p></div> \n<div class=\"h-card\" ><area href=\"jane.html\">Jane Doe</area><p></p></div>\n<div class=\"h-card\" ><a class=\"h-card\" href=\"jane.html\">Jane Doe</a><p></p></div> ";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}},{"type":["h-card"],"properties":{"name":["Jane Doe"]},"children":[{"value":"Jane Doe","type":["h-card"],"properties":{"name":["Jane Doe"],"url":["http://example.com/jane.html"]}}]}],"rels":{},"rel-urls":{}};
+
+ it('impliedurl', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justahyperlink.js
new file mode 100644
index 0000000000..5911cc00fa
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<a class=\"h-card\" href=\"http://benward.me/\">Ben Ward</a>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Ben Ward"],"url":["http://benward.me/"]}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justaname.js
new file mode 100644
index 0000000000..4f239fab5c
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<p class=\"h-card\">Frances Berriman</p>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Frances Berriman"]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-nested.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-nested.js
new file mode 100644
index 0000000000..da2336e293
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-nested.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/nested
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"h-org h-card\" href=\"http://mozilla.org/\">Mozilla Foundation</a>)\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Mitchell Baker"],"url":["http://blog.lizardwrangler.com/"]},"children":[{"value":"Mozilla Foundation","type":["h-org","h-card"],"properties":{"name":["Mozilla Foundation"],"url":["http://mozilla.org/"]}}]}],"rels":{},"rel-urls":{}};
+
+ it('nested', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-p-property.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-p-property.js
new file mode 100644
index 0000000000..0a365e34b8
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-p-property.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/p-property
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<div class=\"h-card\">\n \n <span class=\"p-name\">\n <span class=\"p-given-name value\">John</span> \n <abbr class=\"p-additional-name\" title=\"Peter\">P</abbr> \n <span class=\"p-family-name value \">Doe</span> \n </span>\n <data class=\"p-honorific-suffix\" value=\"MSc\"></data>\n \n \n <br class=\"p-honorific-suffix\" />BSc<br />\n <hr class=\"p-honorific-suffix\" />BA\n \n \n <img class=\"p-honorific-suffix\" src=\"images/logo.gif\" alt=\"PHD\" />\n <img src=\"images/logo.gif\" alt=\"company logos\" usemap=\"#logomap\" />\n <map name=\"logomap\">\n <area class=\"p-org\" shape=\"rect\" coords=\"0,0,82,126\" href=\"madgex.htm\" alt=\"Madgex\" />\n <area class=\"p-org\" shape=\"circle\" coords=\"90,58,3\" href=\"mozilla.htm\" alt=\"Mozilla\" />\n </map>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["John Doe"],"given-name":["John"],"additional-name":["Peter"],"family-name":["Doe"],"honorific-suffix":["MSc","PHD"],"org":["Madgex","Mozilla"]}}],"rels":{},"rel-urls":{}};
+
+ it('p-property', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-relativeurls.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-relativeurls.js
new file mode 100644
index 0000000000..712a8cf722
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-card-relativeurls.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-card/relativeurls
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-card', function() {
+ var htmlFragment = "<base href=\"http://example.com\" >\n<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card\" href=\"bios/mitchell-baker/\">Mozilla Foundation</a>)\n <img class=\"u-photo\" src=\"bios/mitchell-baker/picture.jpeg\"/>\n</div>";
+ var expected = {"items":[{"type":["h-card"],"properties":{"name":["Mitchell Baker"],"url":["http://blog.lizardwrangler.com/"],"org":[{"value":"Mozilla Foundation","type":["h-card"],"properties":{"name":["Mozilla Foundation"],"url":["http://example.com/bios/mitchell-baker/"]}}],"photo":["http://example.com/bios/mitchell-baker/picture.jpeg"]}}],"rels":{},"rel-urls":{}};
+
+ it('relativeurls', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-impliedvalue-nested.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-impliedvalue-nested.js
new file mode 100644
index 0000000000..e729b48b0f
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-impliedvalue-nested.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-entry/impliedvalue-nested
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<div class=\"h-entry\">\n <div class=\"u-in-reply-to h-cite\">\n <span class=\"p-author h-card\">\n <span class=\"p-name\">Example Author</span>\n <a class=\"u-url\" href=\"http://example.com\">Home</a>\n </span>\n <a class=\"p-name u-url\" href=\"http://example.com/post\">Example Post</a>\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"in-reply-to":[{"type":["h-cite"],"properties":{"name":["Example Post"],"url":["http://example.com/post"],"author":[{"type":["h-card"],"properties":{"url":["http://example.com"],"name":["Example Author"]},"value":"Example Author"}]},"value":"http://example.com/post"}],"name":["Example Author\n Home\n \n Example Post"]}}],"rels":{},"rel-urls":{}};
+
+ it('impliedvalue-nested', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justahyperlink.js
new file mode 100644
index 0000000000..1e793e7273
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-entry/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<a class=\"h-entry\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"]}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justaname.js
new file mode 100644
index 0000000000..f4d31bf878
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-entry/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<p class=\"h-entry\">microformats.org at 7</p>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"name":["microformats.org at 7"]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-summarycontent.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-summarycontent.js
new file mode 100644
index 0000000000..b697f6c7c6
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-summarycontent.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-entry/summarycontent
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"h-entry\">\n <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"e-content\">\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"name":["microformats.org at 7"],"content":[{"value":"Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service","html":"\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n "}],"summary":["Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities."],"updated":["2012-06-25 17:08:26"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('summarycontent', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-u-property.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-u-property.js
new file mode 100644
index 0000000000..510f0aa90d
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-u-property.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-entry/u-property
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<base href=\"http://example.com\">\n<div class=\"h-entry\">\n <p class=\"p-name\">microformats.org at 7</p>\n\n \n <p class=\"u-url\">\n <span class=\"value-title\" title=\"http://microformats.org/\"> </span>\n Article permalink\n </p>\n <p class=\"u-url\">\n <span class=\"value\">http://microformats.org/</span> - \n <span class=\"value\">2012/06/25/microformats-org-at-7</span> \n </p> \n\n <p><a class=\"u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">Article permalink</a></p>\n\n <img src=\"images/logo.gif\" alt=\"company logos\" usemap=\"#logomap\" />\n <map name=\"logomap\">\n <area class=\"u-url\" shape=\"rect\" coords=\"0,0,82,126\" href=\"http://microformats.org/\" alt=\"microformats.org\" />\n </map>\n\n <img class=\"u-photo\" src=\"images/logo.gif\" alt=\"company logos\" />\n\n <object class=\"u-url\" data=\"http://microformats.org/wiki/microformats2-parsing\"></object>\n\n <abbr class=\"u-url\" title=\"http://microformats.org/wiki/value-class-pattern\">value-class-pattern</abbr> \n <data class=\"u-url\" value=\"http://microformats.org/wiki/\"></data>\n <p class=\"u-url\">http://microformats.org/discuss</p>\n</div>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/","http://microformats.org/2012/06/25/microformats-org-at-7","http://microformats.org/2012/06/25/microformats-org-at-7","http://microformats.org/","http://microformats.org/wiki/microformats2-parsing","http://microformats.org/wiki/value-class-pattern","http://microformats.org/wiki/","http://microformats.org/discuss"],"photo":["http://example.com/images/logo.gif"]}}],"rels":{},"rel-urls":{}};
+
+ it('u-property', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-urlincontent.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-urlincontent.js
new file mode 100644
index 0000000000..295ac9925a
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-entry-urlincontent.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-entry/urlincontent
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-entry', function() {
+ var htmlFragment = "<div class=\"h-entry\">\n <h1><a class=\"p-name\">Expanding URLs within HTML content</a></h1>\n <div class=\"e-content\">\n <ul>\n <li><a href=\"http://www.w3.org/\">Should not change: http://www.w3.org/</a></li>\n <li><a href=\"http://example.com/\">Should not change: http://example.com/</a></li>\n <li><a href=\"test.html\">File relative: test.html = http://example.com/test.html</a></li>\n <li><a href=\"/test/test.html\">Directory relative: /test/test.html = http://example.com/test/test.html</a></li>\n <li><a href=\"/test.html\">Relative to root: /test.html = http://example.com/test.html</a></li>\n </ul>\n <img src=\"images/photo.gif\" />\n </div> \n</div>";
+ var expected = {"items":[{"type":["h-entry"],"properties":{"name":["Expanding URLs within HTML content"],"content":[{"value":"Should not change: http://www.w3.org/\n Should not change: http://example.com/\n File relative: test.html = http://example.com/test.html\n Directory relative: /test/test.html = http://example.com/test/test.html\n Relative to root: /test.html = http://example.com/test.html","html":"\n <ul>\n <li><a href=\"http://www.w3.org/\">Should not change: http://www.w3.org/</a></li>\n <li><a href=\"http://example.com/\">Should not change: http://example.com/</a></li>\n <li><a href=\"http://example.com/test.html\">File relative: test.html = http://example.com/test.html</a></li>\n <li><a href=\"http://example.com/test/test.html\">Directory relative: /test/test.html = http://example.com/test/test.html</a></li>\n <li><a href=\"http://example.com/test.html\">Relative to root: /test.html = http://example.com/test.html</a></li>\n </ul>\n <img src=\"http://example.com/images/photo.gif\" />\n "}]}}],"rels":{},"rel-urls":{}};
+
+ it('urlincontent', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-ampm.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-ampm.js
new file mode 100644
index 0000000000..814c3c42ed
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-ampm.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/ampm
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<span class=\"h-event\">\n <span class=\"p-name\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00am \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00p.m. \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00PM \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00am \n </span></li>\n </ul>\n</span>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00:00","2009-06-26 07:00:00","2009-06-26 19:00","2009-06-26 19","2009-06-26 19","2009-06-26 19:00","2009-06-26 19:00","2009-06-26 19:00","2009-06-26 07:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('ampm', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-attendees.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-attendees.js
new file mode 100644
index 0000000000..2315dbe912
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-attendees.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/attendees
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"h-event\">\n <span class=\"p-name\">CPJ Online Press Freedom Summit</span>\n (<time class=\"dt-start\" datetime=\"2012-10-10\">10 Nov 2012</time>) in\n <span class=\"p-location\">San Francisco</span>.\n Attendees:\n <ul>\n <li class=\"p-attendee h-card\">Brian Warner</li>\n <li class=\"p-attendee h-card\">Kyle Machulis</li>\n <li class=\"p-attendee h-card\">Tantek Çelik</li>\n <li class=\"p-attendee h-card\">Sid Sutter</li>\n </ul>\n</div>\n";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["CPJ Online Press Freedom Summit"],"start":["2012-10-10"],"location":["San Francisco"],"attendee":[{"value":"Brian Warner","type":["h-card"],"properties":{"name":["Brian Warner"]}},{"value":"Kyle Machulis","type":["h-card"],"properties":{"name":["Kyle Machulis"]}},{"value":"Tantek Çelik","type":["h-card"],"properties":{"name":["Tantek Çelik"]}},{"value":"Sid Sutter","type":["h-card"],"properties":{"name":["Sid Sutter"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('attendees', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-combining.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-combining.js
new file mode 100644
index 0000000000..e91b381bab
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-combining.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/combining
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<div class=\"h-event\">\n <a class=\"p-name u-url\" href=\"http://indiewebcamp.com/2012\">\n IndieWebCamp 2012\n </a>\n from <time class=\"dt-start\">2012-06-30</time> \n to <time class=\"dt-end\">2012-07-01</time> at \n <span class=\"p-location h-card\">\n <a class=\"p-name p-org u-url\" href=\"http://geoloqi.com/\">Geoloqi</a>, \n <span class=\"p-street-address\">920 SW 3rd Ave. Suite 400</span>, \n <span class=\"p-locality\">Portland</span>, \n <abbr class=\"p-region\" title=\"Oregon\">OR</abbr>\n </span>\n</div>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["IndieWebCamp 2012"],"url":["http://indiewebcamp.com/2012"],"start":["2012-06-30"],"end":["2012-07-01"],"location":[{"value":"Geoloqi","type":["h-card"],"properties":{"name":["Geoloqi"],"org":["Geoloqi"],"url":["http://geoloqi.com/"],"street-address":["920 SW 3rd Ave. Suite 400"],"locality":["Portland"],"region":["Oregon"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('combining', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-concatenate.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-concatenate.js
new file mode 100644
index 0000000000..8972106745
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-concatenate.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/concatenate
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<span class=\"h-event\">\n <span class=\"p-name\">The 4th Microformat party</span> will be on \n <span class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time></span> to \n <span class=\"dt-end\"><time class=\"value\">22:00</time></span>.\n</span>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00"],"end":["2009-06-26 22:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('concatenate', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dates.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dates.js
new file mode 100644
index 0000000000..c26b5bccc2
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dates.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/dates
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<section class=\"h-event\">\n\t<p><span class=\"p-name\">The 4th Microformat party</span> will be on:</p>\n\t<ul>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00-08:00\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00-08\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00-0800\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00+0800\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00+08:00\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00Z\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26t19:00-08:00\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26 19:00:00-08:00\">26 July</time></li>\n\t</ul>\n</section>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00-08:00","2009-06-26 19:00-08","2009-06-26 19:00-08:00","2009-06-26 19:00+08:00","2009-06-26 19:00+08:00","2009-06-26 19:00Z","2009-06-26 19:00-08:00","2009-06-26 19:00:00-08:00"]}}],"rels":{},"rel-urls":{}};
+
+ it('dates', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dt-property.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dt-property.js
new file mode 100644
index 0000000000..eb97beccb5
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-dt-property.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/dt-property
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<span class=\"h-event\">\n <span class=\"p-name\">The party</span> will be on \n \n <p class=\"dt-start\">\n <span class=\"value-title\" title=\"2013-03-14\"> </span>\n March 14th 2013\n </p>\n <p class=\"dt-start\">\n <time class=\"value\" datetime=\"2013-06-25\">25 July</time>, from\n <span class=\"value\">07:00:00am \n </span></p> \n \n <p>\n <time class=\"dt-start\" datetime=\"2013-06-26\">26 June</time>\n \n <ins class=\"dt-start\" datetime=\"2013-06-27\">Just added</ins>, \n <del class=\"dt-start\" datetime=\"2013-06-28\">Removed</del>\n </p>\n <abbr class=\"dt-start\" title=\"2013-06-29\">June 29</abbr> \n <data class=\"dt-start\" value=\"2013-07-01\"></data>\n <p class=\"dt-start\">2013-07-02</p>\n \n</span>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The party"],"start":["2013-03-14","2013-06-25 07:00:00","2013-06-26","2013-06-27","2013-06-28","2013-06-29","2013-07-01","2013-07-02"]}}],"rels":{},"rel-urls":{}};
+
+ it('dt-property', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justahyperlink.js
new file mode 100644
index 0000000000..26c835863e
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<a class=\"h-event\" href=\"http://indiewebcamp.com/2012\">IndieWebCamp 2012</a>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["IndieWebCamp 2012"],"url":["http://indiewebcamp.com/2012"]}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justaname.js
new file mode 100644
index 0000000000..be3a5335db
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<p class=\"h-event\">IndieWebCamp 2012</p>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["IndieWebCamp 2012"]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-time.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-time.js
new file mode 100644
index 0000000000..243b518bfb
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-event-time.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-event/time
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-event', function() {
+ var htmlFragment = "<span class=\"h-event\">\n <span class=\"p-name\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-08:00</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-0800</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00+0800</time> \n </li> \n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00Z</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00-08:00</time> \n </li> \n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00+08:00</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00Z</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time> \n </li> \n <li>\n <time class=\"dt-end\" datetime=\"2013-034\">3 February 2013</time>\n </li>\n <li>\n <time class=\"dt-end\" datetime=\"2013-06-27 15:34\">26 July 2013</time>\n </li> \n </ul>\n</span>";
+ var expected = {"items":[{"type":["h-event"],"properties":{"name":["The 4th Microformat party"],"start":["2009-06-26 19:00:00-08:00","2009-06-26 19:00:00-08:00","2009-06-26 19:00:00+08:00","2009-06-26 19:00:00Z","2009-06-26 19:00:00","2009-06-26 19:00-08:00","2009-06-26 19:00+08:00","2009-06-26 19:00Z","2009-06-26 19:00"],"end":["2013-034","2013-06-27 15:34"]}}],"rels":{},"rel-urls":{}};
+
+ it('time', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-implied-title.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-implied-title.js
new file mode 100644
index 0000000000..30bbf52df0
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-implied-title.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-feed/implied-title
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-feed', function() {
+ var htmlFragment = "\n<html>\n\t<head>\n\t\t<title>microformats blog</title>\n\t</head>\n\t<body>\n\t<section class=\"h-feed\">\n\t\t\n\t\t<div class=\"h-entry\">\n\t\t <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n\t\t <div class=\"e-content\">\n\t\t <p class=\"p-summary\">Last week the microformats.org community \n\t\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t\t San Francisco and recognized accomplishments, challenges, and \n\t\t opportunities.</p>\n\t\t\n\t\t <p>The microformats tagline “humans first, machines second” \n\t\t forms the basis of many of our \n\t\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t\t in that regard, we’d like to recognize a few people and \n\t\t thank them for their years of volunteer service </p>\n\t\t </div> \n\t\t <p>Updated \n\t\t <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time>\n\t\t </p>\n\t\t</div>\n\t\t\n\t</section>\n\t</body>\n</html>";
+ var expected = {"items":[{"type":["h-feed"],"properties":{"name":["microformats blog"]},"children":[{"value":"microformats.org at 7\n\t\t \n\t\t Last week the microformats.org community \n\t\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t\t San Francisco and recognized accomplishments, challenges, and \n\t\t opportunities.\n\t\t\n\t\t The microformats tagline “humans first, machines second” \n\t\t forms the basis of many of our \n\t\t principles, and \n\t\t in that regard, we’d like to recognize a few people and \n\t\t thank them for their years of volunteer service \n\t\t \n\t\t Updated \n\t\t June 25th, 2012","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n\t\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t\t San Francisco and recognized accomplishments, challenges, and \n\t\t opportunities.\n\t\t\n\t\t The microformats tagline “humans first, machines second” \n\t\t forms the basis of many of our \n\t\t principles, and \n\t\t in that regard, we’d like to recognize a few people and \n\t\t thank them for their years of volunteer service","html":"\n\t\t <p class=\"p-summary\">Last week the microformats.org community \n\t\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t\t San Francisco and recognized accomplishments, challenges, and \n\t\t opportunities.</p>\n\t\t\n\t\t <p>The microformats tagline “humans first, machines second” \n\t\t forms the basis of many of our \n\t\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t\t in that regard, we’d like to recognize a few people and \n\t\t thank them for their years of volunteer service </p>\n\t\t "}],"summary":["Last week the microformats.org community \n\t\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t\t San Francisco and recognized accomplishments, challenges, and \n\t\t opportunities."],"updated":["2012-06-25 17:08:26"]}}]}],"rels":{},"rel-urls":{}};
+
+ it('implied-title', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-simple.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-simple.js
new file mode 100644
index 0000000000..c72b241404
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-feed-simple.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-feed/simple
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-feed', function() {
+ var htmlFragment = "<section class=\"h-feed\">\n\t<h1 class=\"p-name\">Microformats blog</h1>\n\t<a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n\t<a class=\"u-url\" href=\"http://microformats.org/blog\">permlink</a>\n\t<img class=\"u-photo\" src=\"photo.jpeg\"/>\n\t\n\t<div class=\"h-entry\">\n\t <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n\t <div class=\"e-content\">\n\t <p class=\"p-summary\">Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.</p>\n\t\n\t <p>The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service </p>\n\t </div> \n\t <p>Updated \n\t <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time>\n\t </p>\n\t</div>\n\t\n</section>";
+ var expected = {"items":[{"type":["h-feed"],"properties":{"name":["Microformats blog"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}],"url":["http://microformats.org/blog"],"photo":["http://example.com/photo.jpeg"]},"children":[{"value":"microformats.org at 7\n\t \n\t Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.\n\t\n\t The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t principles, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service \n\t \n\t Updated \n\t June 25th, 2012","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.\n\t\n\t The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t principles, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service","html":"\n\t <p class=\"p-summary\">Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.</p>\n\t\n\t <p>The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service </p>\n\t "}],"summary":["Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities."],"updated":["2012-06-25 17:08:26"]}}]}],"rels":{},"rel-urls":{}};
+
+ it('simple', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-abbrpattern.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-abbrpattern.js
new file mode 100644
index 0000000000..d26e9ed0d9
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-abbrpattern.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-geo/abbrpattern
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-geo', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<p class=\"h-geo\">\n <abbr class=\"p-latitude\" title=\"37.408183\">N 37° 24.491</abbr>, \n <abbr class=\"p-longitude\" title=\"-122.13855\">W 122° 08.313</abbr>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["37.408183"],"longitude":["-122.13855"],"name":["N 37° 24.491, \n W 122° 08.313"]}}],"rels":{},"rel-urls":{}};
+
+ it('abbrpattern', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-altitude.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-altitude.js
new file mode 100644
index 0000000000..45da683ff4
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-altitude.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-geo/altitude
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-geo', function() {
+ var htmlFragment = "<p>My favourite hill in the lakes is \n <span class=\"h-geo\">\n <span class=\"p-name\">Pen-y-ghent</span> \n (Geo: <span class=\"p-latitude\">54.155278</span>,\n <span class=\"p-longitude\">-2.249722</span>). It\n raises to <span class=\"p-altitude\">694</span>m.\n </span>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"name":["Pen-y-ghent"],"latitude":["54.155278"],"longitude":["-2.249722"],"altitude":["694"]}}],"rels":{},"rel-urls":{}};
+
+ it('altitude', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-hidden.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-hidden.js
new file mode 100644
index 0000000000..968ed12853
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-hidden.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-geo/hidden
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-geo', function() {
+ var htmlFragment = "<p>\n <span class=\"h-geo\">The Bricklayer's Arms\n <span class=\"p-latitude\">\n <span class=\"value-title\" title=\"51.513458\"> </span> \n </span>\n <span class=\"p-longitude\">\n <span class=\"value-title\" title=\"-0.14812\"> </span>\n </span>\n </span>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["51.513458"],"longitude":["-0.14812"],"name":["The Bricklayer's Arms"]}}],"rels":{},"rel-urls":{}};
+
+ it('hidden', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-justaname.js
new file mode 100644
index 0000000000..23c142462c
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-geo/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-geo', function() {
+ var htmlFragment = "<p>On my way to The Bricklayer's Arms\n (Geo: <span class=\"h-geo\">51.513458;-0.14812</span>)\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"name":["51.513458;-0.14812"]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-simpleproperties.js
new file mode 100644
index 0000000000..e9700a3e29
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-geo/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-geo', function() {
+ var htmlFragment = "<p class=\"h-geo\">We are meeting at \n <span class=\"p-name\">The Bricklayer's Arms</span>\n (Geo: <span class=\"p-latitude\">51.513458</span>:\n <span class=\"p-longitude\">-0.14812</span>)\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"name":["The Bricklayer's Arms"],"latitude":["51.513458"],"longitude":["-0.14812"]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-valuetitleclass.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-valuetitleclass.js
new file mode 100644
index 0000000000..813d215927
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-geo-valuetitleclass.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-geo/valuetitleclass
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-geo', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<p>\n <span class=\"h-geo\">\n <span class=\"p-latitude\">\n <span class=\"value-title\" title=\"51.513458\">N 51° 51.345</span>, \n </span>\n <span class=\"p-longitude\">\n <span class=\"value-title\" title=\"-0.14812\">W -0° 14.812</span>\n </span>\n </span>\n</p>";
+ var expected = {"items":[{"type":["h-geo"],"properties":{"latitude":["51.513458"],"longitude":["-0.14812"],"name":["N 51° 51.345, \n \n \n W -0° 14.812"]}}],"rels":{},"rel-urls":{}};
+
+ it('valuetitleclass', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-news-all.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-news-all.js
new file mode 100644
index 0000000000..a7deb3f2f0
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-news-all.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-news/all
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-news', function() {
+ var htmlFragment = "<div class=\"h-news\">\n <div class=\"p-entry h-entry\">\n <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"e-content\">\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n </p>\n </div>\n\n <p>\n <span class=\"p-dateline h-adr\">\n <span class=\"p-locality\">San Francisco</span>, \n <span class=\"p-region\">CA</span> \n </span>\n (Geo: <span class=\"p-geo\">37.774921;-122.445202</span>) \n <span class=\"p-source-org h-card\">\n <a class=\"p-name u-url\" href=\"http://microformats.org/\">microformats.org</a>\n </span>\n </p>\n <p>\n <a class=\"u-principles\" href=\"http://microformats.org/wiki/Category:public_domain_license\">Publishing policy</a>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-news"],"properties":{"entry":[{"value":"microformats.org at 7","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service","html":"\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n "}],"summary":["Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities."],"updated":["2012-06-25 17:08:26"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}]}}],"dateline":[{"value":"San Francisco, \n CA","type":["h-adr"],"properties":{"locality":["San Francisco"],"region":["CA"],"name":["San Francisco, \n CA"]}}],"geo":["37.774921;-122.445202"],"source-org":[{"value":"microformats.org","type":["h-card"],"properties":{"name":["microformats.org"],"url":["http://microformats.org/"]}}],"principles":["http://microformats.org/wiki/Category:public_domain_license"],"name":["microformats.org at 7\n \n Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service \n \n Updated \n June 25th, 2012 by\n Tantek\n \n \n\n \n \n San Francisco, \n CA \n \n (Geo: 37.774921;-122.445202) \n \n microformats.org\n \n \n \n Publishing policy"]}}],"rels":{},"rel-urls":{}};
+
+ it('all', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-news-minimum.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-news-minimum.js
new file mode 100644
index 0000000000..4494cb8ab3
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-news-minimum.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-news/minimum
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-news', function() {
+ var htmlFragment = "<div class=\"h-news\">\n <div class=\"p-entry h-entry\">\n <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"e-content\">\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n </p>\n </div>\n <p>\n <a class=\"p-source-org h-card\" href=\"http://microformats.org/\">microformats.org</a> \n </p>\n</div>";
+ var expected = {"items":[{"type":["h-news"],"properties":{"entry":[{"value":"microformats.org at 7","type":["h-entry"],"properties":{"name":["microformats.org at 7"],"url":["http://microformats.org/2012/06/25/microformats-org-at-7"],"content":[{"value":"Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service","html":"\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n "}],"summary":["Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities."],"updated":["2012-06-25 17:08:26"],"author":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"],"url":["http://tantek.com/"]}}]}}],"source-org":[{"value":"microformats.org","type":["h-card"],"properties":{"name":["microformats.org"],"url":["http://microformats.org/"]}}],"name":["microformats.org at 7\n \n Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.\n\n The microformats tagline “humans first, machines second” \n forms the basis of many of our \n principles, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service \n \n Updated \n June 25th, 2012 by\n Tantek\n \n \n \n microformats.org"]}}],"rels":{},"rel-urls":{}};
+
+ it('minimum', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-hyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-hyperlink.js
new file mode 100644
index 0000000000..b7150aae49
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-hyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-org/hyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-org', function() {
+ var htmlFragment = "<a class=\"h-org\" href=\"http://mozilla.org/\">Mozilla Foundation</a>";
+ var expected = {"items":[{"type":["h-org"],"properties":{"name":["Mozilla Foundation"],"url":["http://mozilla.org/"]}}],"rels":{},"rel-urls":{}};
+
+ it('hyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simple.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simple.js
new file mode 100644
index 0000000000..4f5a75e881
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simple.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-org/simple
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-org', function() {
+ var htmlFragment = "<span class=\"h-org\">Mozilla Foundation</span>";
+ var expected = {"items":[{"type":["h-org"],"properties":{"name":["Mozilla Foundation"]}}],"rels":{},"rel-urls":{}};
+
+ it('simple', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simpleproperties.js
new file mode 100644
index 0000000000..5c7e939e6e
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-org-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-org/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-org', function() {
+ var htmlFragment = "<p class=\"h-org\">\n <span class=\"p-organization-name\">W3C</span> - \n <span class=\"p-organization-unit\">CSS Working Group</span>\n</p>";
+ var expected = {"items":[{"type":["h-org"],"properties":{"organization-name":["W3C"],"organization-unit":["CSS Working Group"],"name":["W3C - \n CSS Working Group"]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-aggregate.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-aggregate.js
new file mode 100644
index 0000000000..b07d3f5477
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-aggregate.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-product/aggregate
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-product', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"h-product\">\n <h2 class=\"p-name\">Raspberry Pi</h2>\n <img class=\"u-photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"e-description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"u-url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"p-price\">£29.95</p>\n <p class=\"p-review h-review-aggregate\">\n <span class=\"p-rating h-rating\">\n <span class=\"p-average\">9.2</span> out of \n <span class=\"p-best\">10</span> \n based on <span class=\"p-count\">178</span> reviews\n </span>\n </p>\n <p>Categories: <span class=\"p-category\">Computer</span>, <span class=\"p-category\">Education</span></p>\n <p class=\"p-brand h-card\">From: \n <span class=\"p-name p-org\">The Raspberry Pi Foundation</span> - \n <span class=\"p-locality\">Cambridge</span> \n <span class=\"p-country-name\">UK</span>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-product"],"properties":{"name":["Raspberry Pi"],"photo":["http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg"],"description":[{"value":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.","html":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming."}],"url":["http://www.raspberrypi.org/"],"price":["£29.95"],"review":[{"value":"9.2 out of \n 10 \n based on 178 reviews","type":["h-review-aggregate"],"properties":{"rating":[{"value":"9.2 out of \n 10 \n based on 178 reviews","type":["h-rating"],"properties":{"average":["9.2"],"best":["10"],"count":["178"],"name":["9.2 out of \n 10 \n based on 178 reviews"]}}],"name":["9.2 out of \n 10 \n based on 178 reviews"]}}],"category":["Computer","Education"],"brand":[{"value":"The Raspberry Pi Foundation","type":["h-card"],"properties":{"name":["The Raspberry Pi Foundation"],"org":["The Raspberry Pi Foundation"],"locality":["Cambridge"],"country-name":["UK"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('aggregate', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justahyperlink.js
new file mode 100644
index 0000000000..cf2638e314
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-product/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-product', function() {
+ var htmlFragment = "<a class=\"h-product\" href=\"http://www.raspberrypi.org/\">Raspberry Pi</a>";
+ var expected = {"items":[{"type":["h-product"],"properties":{"name":["Raspberry Pi"],"url":["http://www.raspberrypi.org/"]}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justaname.js
new file mode 100644
index 0000000000..f946a10e06
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-product/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-product', function() {
+ var htmlFragment = "<p class=\"h-product\">Raspberry Pi</p>";
+ var expected = {"items":[{"type":["h-product"],"properties":{"name":["Raspberry Pi"]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-simpleproperties.js
new file mode 100644
index 0000000000..1c5467d519
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-product-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-product/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-product', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"h-product\">\n <h2 class=\"p-name\">Raspberry Pi</h2>\n <img class=\"u-photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"e-description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"u-url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"p-price\">£29.95</p>\n <p class=\"p-review h-review\"><span class=\"p-rating\">4.5</span> out of 5</p>\n <p>Categories: <span class=\"p-category\">Computer</span>, <span class=\"p-category\">Education</span></p>\n</div>";
+ var expected = {"items":[{"type":["h-product"],"properties":{"name":["Raspberry Pi"],"photo":["http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg"],"description":[{"value":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.","html":"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming."}],"url":["http://www.raspberrypi.org/"],"price":["£29.95"],"category":["Computer","Education"],"review":[{"value":"4.5 out of 5","type":["h-review"],"properties":{"rating":["4.5"],"name":["4.5 out of 5"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-all.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-all.js
new file mode 100644
index 0000000000..fa0e4cb376
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-all.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-recipe/all
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-recipe', function() {
+ var htmlFragment = "<section class=\"h-recipe\">\n <h1 class=\"p-name\">Yorkshire Puddings</h1> \n <p class=\"p-summary\">Makes <span class=\"p-yield\">6 good sized Yorkshire puddings</span>, the way my mum taught me</p>\n\n\n <p><img class=\"u-photo\" src=\"http://codebits.glennjones.net/semantic/yorkshire-puddings.jpg\" /></p>\n\n <span class=\"p-review h-review-aggregate\">\n <span class=\"p-rating\">\n <span class=\"p-average\">4.5</span> stars out 5 based on </span>\n <span class=\"p-count\">35</span> reviews</span>\n \n \n\n <div id=\"ingredients-container\">\n <h3>Ingredients</h3>\n <ul>\n <li class=\"e-ingredient\">1 egg</li>\n <li class=\"e-ingredient\">75g plain flour</li>\n <li class=\"e-ingredient\">70ml milk</li>\n <li class=\"e-ingredient\">60ml water</li>\n <li class=\"e-ingredient\">Pinch of salt</li>\n </ul>\n </div>\n\n <h3>Time</h3>\n <ul>\n <li class=\"prepTime\">Preparation <span class=\"value-title\" title=\"PT0H10M\">10 mins</span></li>\n <li class=\"cookTime\">Cook <span class=\"value-title\" title=\"PT0H25M\">25 mins</span></li>\n </ul> \n\n\n <h3>Instructions</h3>\n <div class=\"e-instructions\">\n <ol>\n <li>Pre-heat oven to 230C or gas mark 8. Pour the vegetable oil evenly into 2 x 4-hole \n Yorkshire pudding tins and place in the oven to heat through.</li> \n \n <li>To make the batter, add all the flour into a bowl and beat in the eggs until smooth. \n Gradually add the milk and water while beating the mixture. It should be smooth and \n without lumps. Finally add a pinch of salt.</li>\n \n <li>Make sure the oil is piping hot before pouring the batter evenly into the tins. \n Place in the oven for 20-25 minutes until pudding have risen and look golden brown</li>\n </ol>\n </div>\n\n <h3>Nutrition</h3>\n <ul id=\"nutrition-list\">\n <li class=\"p-nutrition\">Calories: <span class=\"calories\">125</span></li>\n <li class=\"p-nutrition\">Fat: <span class=\"fat\">3.2g</span></li>\n <li class=\"p-nutrition\">Cholesterol: <span class=\"cholesterol\">77mg</span></li>\n </ul>\n <p>(Amount per pudding)</p>\n\n <p>\n Published on <time class=\"dt-published\" datetime=\"2011-10-27\">27 Oct 2011</time> by \n <span class=\"p-author h-card\">\n <a class=\"p-name u-url\" href=\"http://glennjones.net\">Glenn Jones</a>\n </span>\n </p>\n <a href=\"http://www.flickr.com/photos/dithie/4106528495/\">Photo by dithie</a>\n </section>";
+ var expected = {"items":[{"type":["h-recipe"],"properties":{"name":["Yorkshire Puddings"],"summary":["Makes 6 good sized Yorkshire puddings, the way my mum taught me"],"yield":["6 good sized Yorkshire puddings"],"photo":["http://codebits.glennjones.net/semantic/yorkshire-puddings.jpg"],"review":[{"value":"4.5 stars out 5 based on \n 35 reviews","type":["h-review-aggregate"],"properties":{"rating":["4.5 stars out 5 based on"],"average":["4.5"],"count":["35"],"name":["4.5 stars out 5 based on \n 35 reviews"]}}],"ingredient":[{"value":"1 egg","html":"1 egg"},{"value":"75g plain flour","html":"75g plain flour"},{"value":"70ml milk","html":"70ml milk"},{"value":"60ml water","html":"60ml water"},{"value":"Pinch of salt","html":"Pinch of salt"}],"instructions":[{"value":"Pre-heat oven to 230C or gas mark 8. Pour the vegetable oil evenly into 2 x 4-hole \n Yorkshire pudding tins and place in the oven to heat through. \n \n To make the batter, add all the flour into a bowl and beat in the eggs until smooth. \n Gradually add the milk and water while beating the mixture. It should be smooth and \n without lumps. Finally add a pinch of salt.\n \n Make sure the oil is piping hot before pouring the batter evenly into the tins. \n Place in the oven for 20-25 minutes until pudding have risen and look golden brown","html":"\n <ol>\n <li>Pre-heat oven to 230C or gas mark 8. Pour the vegetable oil evenly into 2 x 4-hole \n Yorkshire pudding tins and place in the oven to heat through.</li> \n \n <li>To make the batter, add all the flour into a bowl and beat in the eggs until smooth. \n Gradually add the milk and water while beating the mixture. It should be smooth and \n without lumps. Finally add a pinch of salt.</li>\n \n <li>Make sure the oil is piping hot before pouring the batter evenly into the tins. \n Place in the oven for 20-25 minutes until pudding have risen and look golden brown</li>\n </ol>\n "}],"nutrition":["Calories: 125","Fat: 3.2g","Cholesterol: 77mg"],"published":["2011-10-27"],"author":[{"value":"Glenn Jones","type":["h-card"],"properties":{"name":["Glenn Jones"],"url":["http://glennjones.net"]}}],"url":["http://www.flickr.com/photos/dithie/4106528495/"]}}],"rels":{},"rel-urls":{}};
+
+ it('all', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-minimum.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-minimum.js
new file mode 100644
index 0000000000..ac3d91dc78
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-recipe-minimum.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-recipe/minimum
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-recipe', function() {
+ var htmlFragment = "<div class=\"h-recipe\"> \n <p class=\"p-name\">Toast</p>\n <ul>\n <li class=\"e-ingredient\">Slice of bread</li>\n <li class=\"e-ingredient\">Butter</li>\n </ul>\n</div>";
+ var expected = {"items":[{"type":["h-recipe"],"properties":{"name":["Toast"],"ingredient":[{"value":"Slice of bread","html":"Slice of bread"},{"value":"Butter","html":"Butter"}]}}],"rels":{},"rel-urls":{}};
+
+ it('minimum', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-affiliation.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-affiliation.js
new file mode 100644
index 0000000000..73329d46cb
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-affiliation.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-resume/affiliation
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<div class=\"h-resume\">\n <p>\n <span class=\"p-name\">Tim Berners-Lee</span>, \n <span class=\"p-summary\">invented the World Wide Web</span>. \n </p> \n Belongs to following groups:\n <p> \n <a class=\"p-affiliation h-card\" href=\"http://www.w3.org/\">\n <img class=\"p-name u-photo\" alt=\"W3C\" src=\"http://www.w3.org/Icons/WWW/w3c_home_nb.png\" />\n </a>\n </p> \n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"name":["Tim Berners-Lee"],"summary":["invented the World Wide Web"],"affiliation":[{"type":["h-card"],"properties":{"name":["W3C"],"photo":["http://www.w3.org/Icons/WWW/w3c_home_nb.png"],"url":["http://www.w3.org/"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('affiliation', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-contact.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-contact.js
new file mode 100644
index 0000000000..f2a1f76f7d
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-contact.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-resume/contact
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<div class=\"h-resume\">\n <p class=\"p-name\">Tim Berners-Lee</p>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <div class=\"p-contact h-card\">\n <p class=\"p-name\">MIT</p>\n <p>\n <span class=\"p-street-address\">32 Vassar Street</span>, \n <span class=\"p-extended-address\">Room 32-G524</span>, \n <span class=\"p-locality\">Cambridge</span>, \n <span class=\"p-region\">MA</span> \n <span class=\"p-postal-code\">02139</span>, \n <span class=\"p-country-name\">USA</span>.\n </p>\n <p>Tel:<span class=\"p-tel\">+1 (617) 253 5702</span></p>\n <p>Email:<a class=\"u-email\" href=\"mailto:timbl@w3.org\">timbl@w3.org</a></p>\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"name":["Tim Berners-Lee"],"summary":["Invented the World Wide Web."],"contact":[{"value":"MIT","type":["h-card"],"properties":{"name":["MIT"],"street-address":["32 Vassar Street"],"extended-address":["Room 32-G524"],"locality":["Cambridge"],"region":["MA"],"postal-code":["02139"],"country-name":["USA"],"tel":["+1 (617) 253 5702"],"email":["mailto:timbl@w3.org"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('contact', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-education.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-education.js
new file mode 100644
index 0000000000..5e38384344
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-education.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-resume/education
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<div class=\"h-resume\">\n <p class=\"p-name\">Tim Berners-Lee</p>\n <div class=\"p-contact h-card\">\n <p class=\"p-title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <p class=\"p-education h-event h-card\">\n <span class=\"p-name p-org\">The Queen's College, Oxford University</span>, \n <span class=\"p-description\">BA Hons (I) Physics</span> \n <time class=\"dt-start\" datetime=\"1973-09\">1973</time> –\n <time class=\"dt-end\" datetime=\"1976-06\">1976</time>\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"name":["Tim Berners-Lee"],"contact":[{"value":"Director of the World Wide Web Foundation","type":["h-card"],"properties":{"title":["Director of the World Wide Web Foundation"],"name":["Director of the World Wide Web Foundation"]}}],"summary":["Invented the World Wide Web."],"education":[{"value":"The Queen's College, Oxford University","type":["h-event","h-card"],"properties":{"name":["The Queen's College, Oxford University"],"org":["The Queen's College, Oxford University"],"description":["BA Hons (I) Physics"],"start":["1973-09"],"end":["1976-06"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('education', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-justaname.js
new file mode 100644
index 0000000000..2357bf1a22
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-resume/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<p class=\"h-resume\">Tim Berners-Lee, invented the World Wide Web.</p>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"name":["Tim Berners-Lee, invented the World Wide Web."]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-skill.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-skill.js
new file mode 100644
index 0000000000..60a983e04c
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-skill.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-resume/skill
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<div class=\"h-resume\">\n <p>\n <span class=\"p-name\">Tim Berners-Lee</span>, \n <span class=\"p-summary\">invented the World Wide Web</span>.\n </p>\n Skills: \n <ul>\n <li class=\"p-skill\">information systems</li>\n <li class=\"p-skill\">advocacy</li>\n <li class=\"p-skill\">leadership</li>\n <ul> \n</ul></ul></div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"name":["Tim Berners-Lee"],"summary":["invented the World Wide Web"],"skill":["information systems","advocacy","leadership"]}}],"rels":{},"rel-urls":{}};
+
+ it('skill', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-work.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-work.js
new file mode 100644
index 0000000000..d61ea3de56
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-resume-work.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-resume/work
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-resume', function() {
+ var htmlFragment = "<meta charset=\"utf-8\">\n<div class=\"h-resume\">\n <p class=\"p-name\">Tim Berners-Lee</p>\n <div class=\"p-contact h-card\">\n <p class=\"p-title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <div class=\"p-experience h-event h-card\">\n <p class=\"p-title\">Director</p>\n <p><a class=\"p-name p-org u-url\" href=\"http://www.webfoundation.org/\">World Wide Web Foundation</a></p>\n <p>\n <time class=\"dt-start\" datetime=\"2009-01-18\">Jan 2009</time> – Present\n <time class=\"dt-duration\" datetime=\"P2Y11M\">(2 years 11 month)</time>\n </p>\n </div>\n</div>";
+ var expected = {"items":[{"type":["h-resume"],"properties":{"name":["Tim Berners-Lee"],"contact":[{"value":"Director of the World Wide Web Foundation","type":["h-card"],"properties":{"title":["Director of the World Wide Web Foundation"],"name":["Director of the World Wide Web Foundation"]}}],"summary":["Invented the World Wide Web."],"experience":[{"value":"World Wide Web Foundation","type":["h-event","h-card"],"properties":{"title":["Director"],"name":["World Wide Web Foundation"],"org":["World Wide Web Foundation"],"url":["http://www.webfoundation.org/"],"start":["2009-01-18"],"duration":["P2Y11M"]}}]}}],"rels":{},"rel-urls":{}};
+
+ it('work', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-hevent.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-hevent.js
new file mode 100644
index 0000000000..e698ee37a9
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-hevent.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review-aggregate/hevent
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review-aggregate', function() {
+ var htmlFragment = "<div class=\"h-review-aggregate\">\n <div class=\"p-item h-event\">\n <h3 class=\"p-name\">Fullfrontal</h3>\n <p class=\"p-description\">A one day JavaScript Conference held in Brighton</p>\n <p><time class=\"dt-start\" datetime=\"2012-11-09\">9th November 2012</time></p> \n </div> \n \n <p class=\"p-rating\">\n <span class=\"p-average value\">9.9</span> out of \n <span class=\"p-best\">10</span> \n based on <span class=\"p-count\">62</span> reviews\n </p>\n</div>";
+ var expected = {"items":[{"type":["h-review-aggregate"],"properties":{"item":[{"value":"Fullfrontal","type":["h-event"],"properties":{"name":["Fullfrontal"],"description":["A one day JavaScript Conference held in Brighton"],"start":["2012-11-09"]}}],"rating":["9.9"],"average":["9.9"],"best":["10"],"count":["62"],"name":["Fullfrontal\n A one day JavaScript Conference held in Brighton\n 9th November 2012 \n \n \n \n 9.9 out of \n 10 \n based on 62 reviews"]}}],"rels":{},"rel-urls":{}};
+
+ it('hevent', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-justahyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-justahyperlink.js
new file mode 100644
index 0000000000..729fdfb2d3
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-justahyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review-aggregate/justahyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review-aggregate', function() {
+ var htmlFragment = "<div class=\"h-review-aggregate\">\n <h3 class=\"p-item h-item\">Mediterranean Wraps</h3>\n <span class=\"p-summary\">\n Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff.\n </span>\n <span class=\"p-rating\">4.5</span> out of 5 \n</div>";
+ var expected = {"items":[{"type":["h-review-aggregate"],"properties":{"item":[{"value":"Mediterranean Wraps","type":["h-item"],"properties":{"name":["Mediterranean Wraps"]}}],"summary":["Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff."],"rating":["4.5"],"name":["Mediterranean Wraps\n \n Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff.\n \n 4.5 out of 5"]}}],"rels":{},"rel-urls":{}};
+
+ it('justahyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-simpleproperties.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-simpleproperties.js
new file mode 100644
index 0000000000..d49cabb5fe
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-aggregate-simpleproperties.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review-aggregate/simpleproperties
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review-aggregate', function() {
+ var htmlFragment = "<div class=\"h-review-aggregate\">\n <div class=\"p-item h-card\">\n <h3 class=\"p-name\">Mediterranean Wraps</h3>\n <p>\n <span class=\"p-street-address\">433 S California Ave</span>, \n <span class=\"p-locality\">Palo Alto</span>, \n <span class=\"p-region\">CA</span> - \n <span class=\"p-tel\">(650) 321-8189</span>\n </p>\n </div> \n <span class=\"p-summary\">Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff.</span>\n <span class=\"p-rating\">\n <span class=\"p-average value\">9.2</span> out of \n <span class=\"p-best\">10</span> \n based on <span class=\"p-count\">17</span> reviews\n </span>\n</div>";
+ var expected = {"items":[{"type":["h-review-aggregate"],"properties":{"item":[{"value":"Mediterranean Wraps","type":["h-card"],"properties":{"name":["Mediterranean Wraps"],"street-address":["433 S California Ave"],"locality":["Palo Alto"],"region":["CA"],"tel":["(650) 321-8189"]}}],"summary":["Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff."],"rating":["9.2"],"average":["9.2"],"best":["10"],"count":["17"],"name":["Mediterranean Wraps\n \n 433 S California Ave, \n Palo Alto, \n CA - \n (650) 321-8189\n \n \n Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff.\n \n 9.2 out of \n 10 \n based on 17 reviews"]}}],"rels":{},"rel-urls":{}};
+
+ it('simpleproperties', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-hyperlink.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-hyperlink.js
new file mode 100644
index 0000000000..3f547d7a90
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-hyperlink.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review/hyperlink
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review', function() {
+ var htmlFragment = "<a class=\"h-review\" href=\"https://plus.google.com/116941523817079328322/about\">Crepes on Cole</a>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"name":["Crepes on Cole"],"url":["https://plus.google.com/116941523817079328322/about"]}}],"rels":{},"rel-urls":{}};
+
+ it('hyperlink', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-implieditem.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-implieditem.js
new file mode 100644
index 0000000000..ecde19277c
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-implieditem.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review/implieditem
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review', function() {
+ var htmlFragment = "<div class=\"h-review\">\n <a class=\"p-item h-item\" href=\"http://example.com/crepeoncole\">Crepes on Cole</a>\n <p><span class=\"p-rating\">4.7</span> out of 5 stars</p>\n</div>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"item":[{"value":"Crepes on Cole","type":["h-item"],"properties":{"name":["Crepes on Cole"],"url":["http://example.com/crepeoncole"]}}],"rating":["4.7"],"name":["Crepes on Cole\n 4.7 out of 5 stars"]}}],"rels":{},"rel-urls":{}};
+
+ it('implieditem', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-item.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-item.js
new file mode 100644
index 0000000000..d8aef51c85
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-item.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review/item
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review', function() {
+ var htmlFragment = "<base href=\"http://example.com\" >\n<div class=\"h-review\">\n <p class=\"p-item h-item\">\n <img class=\"u-photo\" src=\"images/photo.gif\" />\n <a class=\"p-name u-url\" href=\"http://example.com/crepeoncole\">Crepes on Cole</a>\n </p>\n <p><span class=\"p-rating\">5</span> out of 5 stars</p>\n</div>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"item":[{"value":"Crepes on Cole","type":["h-item"],"properties":{"photo":["http://example.com/images/photo.gif"],"name":["Crepes on Cole"],"url":["http://example.com/crepeoncole"]}}],"rating":["5"],"name":["Crepes on Cole\n \n 5 out of 5 stars"]}}],"rels":{},"rel-urls":{}};
+
+ it('item', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-justaname.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-justaname.js
new file mode 100644
index 0000000000..89523e909f
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-justaname.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review/justaname
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review', function() {
+ var htmlFragment = "<p class=\"h-review\">Crepes on Cole</p>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"name":["Crepes on Cole"]}}],"rels":{},"rel-urls":{}};
+
+ it('justaname', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-photo.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-photo.js
new file mode 100644
index 0000000000..ee0c41fe6c
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-photo.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review/photo
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review', function() {
+ var htmlFragment = "<base href=\"http://example.com\" ><img class=\"h-review\" src=\"images/photo.gif\" alt=\"Crepes on Cole\" />";
+ var expected = {"items":[{"type":["h-review"],"properties":{"name":["Crepes on Cole"],"photo":["http://example.com/images/photo.gif"]}}],"rels":{},"rel-urls":{}};
+
+ it('photo', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-vcard.js b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-vcard.js
new file mode 100644
index 0000000000..8411c4d2ad
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-h-review-vcard.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/h-review/vcard
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('h-review', function() {
+ var htmlFragment = "<div class=\"h-review\">\n <span><span class=\"p-rating\">5</span> out of 5 stars</span>\n <h4 class=\"p-name\">Crepes on Cole is awesome</h4>\n <span class=\"p-reviewer h-card\">\n Reviewer: <span class=\"p-name\">Tantek</span> - \n </span>\n <time class=\"dt-reviewed\" datetime=\"2005-04-18\">April 18, 2005</time>\n <div class=\"e-description\">\n <p class=\"p-item h-card\">\n <span class=\"p-name p-org\">Crepes on Cole</span> is one of the best little \n creperies in <span class=\"p-adr h-adr\"><span class=\"p-locality\">San Francisco</span></span>.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.\n </p>\n </div>\n <p>Visit date: <span>April 2005</span></p>\n <p>Food eaten: <a class=\"p-category\" href=\"http://en.wikipedia.org/wiki/crepe\">crepe</a></p>\n <p>Permanent link for review: <a class=\"u-url\" href=\"http://example.com/crepe\">http://example.com/crepe</a></p>\n <p><a rel=\"license\" href=\"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\">Creative Commons Attribution-ShareAlike License</a></p>\n</div>";
+ var expected = {"items":[{"type":["h-review"],"properties":{"rating":["5"],"name":["Crepes on Cole is awesome"],"reviewer":[{"value":"Tantek","type":["h-card"],"properties":{"name":["Tantek"]}}],"reviewed":["2005-04-18"],"description":[{"value":"Crepes on Cole is one of the best little \n creperies in San Francisco.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.","html":"\n <p class=\"p-item h-card\">\n <span class=\"p-name p-org\">Crepes on Cole</span> is one of the best little \n creperies in <span class=\"p-adr h-adr\"><span class=\"p-locality\">San Francisco</span></span>.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.\n </p>\n "}],"item":[{"value":"Crepes on Cole","type":["h-card"],"properties":{"name":["Crepes on Cole"],"org":["Crepes on Cole"],"adr":[{"value":"San Francisco","type":["h-adr"],"properties":{"locality":["San Francisco"],"name":["San Francisco"]}}]}}],"category":["crepe"],"url":["http://example.com/crepe"]}}],"rels":{"license":["http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License"]},"rel-urls":{"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License":{"text":"Creative Commons Attribution-ShareAlike License","rels":["license"]}}};
+
+ it('vcard', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-duplicate-rels.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-duplicate-rels.js
new file mode 100644
index 0000000000..d65dfdf8b8
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-duplicate-rels.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/duplicate-rels
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "<a href=\"http://ma.tt/2015/05/beethoven-mozart-bach/\" \n title=\"Permalink to Beethoven, Mozart, Bach\" rel=\"bookmark\">\n<time class=\"entry-date\" datetime=\"2015-05-31T22:42:00+00:00\">May 31, 2015</time></a></span>\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">Asides</a>\n<span class=\"author vcard\">\n<a class=\"url fn n\" href=\"http://ma.tt/author/saxmatt/\" \n title=\"View all posts by Matt\" rel=\"author\">Matt</a></span>\n<span class=\"date\"><a href=\"http://ma.tt/2015/06/jefferson-on-idleness/\" title=\"Permalink to Jefferson on Idleness\" rel=\"bookmark\"><time class=\"entry-date\" datetime=\"2015-06-02T21:26:00+00:00\">June 2, 2015</time></a></span>\n<span class=\"categories-links\"><a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">Asides</a></span>\n<span class=\"author vcard\"><a class=\"url fn n\" href=\"http://ma.tt/author/saxmatt/\" title=\"View all posts by Matt\" rel=\"author\">Matt</a></span>\n";
+ var expected = {"rels":{"bookmark":["http://ma.tt/2015/05/beethoven-mozart-bach/","http://ma.tt/2015/06/jefferson-on-idleness/"],"category":["http://ma.tt/category/asides/"],"tag":["http://ma.tt/category/asides/"],"author":["http://ma.tt/author/saxmatt/"]},"items":[{"type":["h-card"],"properties":{"url":["http://ma.tt/author/saxmatt/"],"name":["Matt"]}},{"type":["h-card"],"properties":{"url":["http://ma.tt/author/saxmatt/"],"name":["Matt"]}}],"rel-urls":{"http://ma.tt/category/asides/":{"rels":["category","tag"],"text":"Asides"},"http://ma.tt/author/saxmatt/":{"rels":["author"],"text":"Matt","title":"View all posts by Matt"},"http://ma.tt/2015/05/beethoven-mozart-bach/":{"rels":["bookmark"],"text":"May 31, 2015","title":"Permalink to Beethoven, Mozart, Bach"},"http://ma.tt/2015/06/jefferson-on-idleness/":{"rels":["bookmark"],"text":"June 2, 2015","title":"Permalink to Jefferson on Idleness"}}};
+
+ it('duplicate-rels', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-license.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-license.js
new file mode 100644
index 0000000000..d5606f5a3a
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-license.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/license
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "<a rel=\"license\" href=\"http://creativecommons.org/licenses/by/2.5/\">cc by 2.5</a>";
+ var expected = {"items":[],"rels":{"license":["http://creativecommons.org/licenses/by/2.5/"]},"rel-urls":{"http://creativecommons.org/licenses/by/2.5/":{"text":"cc by 2.5","rels":["license"]}}};
+
+ it('license', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-nofollow.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-nofollow.js
new file mode 100644
index 0000000000..4332d35723
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-nofollow.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/nofollow
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "<a rel=\"nofollow\" href=\"http://microformats.org/wiki/microformats:copyrights\">Copyrights</a>";
+ var expected = {"items":[],"rels":{"nofollow":["http://microformats.org/wiki/microformats:copyrights"]},"rel-urls":{"http://microformats.org/wiki/microformats:copyrights":{"text":"Copyrights","rels":["nofollow"]}}};
+
+ it('nofollow', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-rel-urls.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-rel-urls.js
new file mode 100644
index 0000000000..685532f448
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-rel-urls.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/rel-urls
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "<a rel=\"author\" href=\"http://example.com/a\">author a</a>\n<a rel=\"author\" href=\"http://example.com/b\">author b</a>\n<a rel=\"in-reply-to\" href=\"http://example.com/1\">post 1</a>\n<a rel=\"in-reply-to\" href=\"http://example.com/2\">post 2</a>\n<a rel=\"alternate home\"\n href=\"http://example.com/fr\"\n media=\"handheld\"\n hreflang=\"fr\">French mobile homepage</a>";
+ var expected = {"items":[],"rels":{"author":["http://example.com/a","http://example.com/b"],"in-reply-to":["http://example.com/1","http://example.com/2"],"home":["http://example.com/fr"],"alternate":["http://example.com/fr"]},"rel-urls":{"http://example.com/a":{"rels":["author"],"text":"author a"},"http://example.com/b":{"rels":["author"],"text":"author b"},"http://example.com/1":{"rels":["in-reply-to"],"text":"post 1"},"http://example.com/2":{"rels":["in-reply-to"],"text":"post 2"},"http://example.com/fr":{"rels":["alternate","home"],"media":"handheld","hreflang":"fr","text":"French mobile homepage"}}};
+
+ it('rel-urls', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-varying-text-duplicate-rels.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-varying-text-duplicate-rels.js
new file mode 100644
index 0000000000..3b1b72f447
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-varying-text-duplicate-rels.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/varying-text-duplicate-rels
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "This is a contrived example - not found links like this in the wild:\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">Asides</a>\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">B-sides</a>\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">seasides</a>";
+ var expected = {"rels":{"category":["http://ma.tt/category/asides/"],"tag":["http://ma.tt/category/asides/"]},"items":[],"rel-urls":{"http://ma.tt/category/asides/":{"rels":["category","tag"],"text":"Asides"}}};
+
+ it('varying-text-duplicate-rels', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-all.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-all.js
new file mode 100644
index 0000000000..3850ad5649
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-all.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/xfn-all
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "<ul>\n <li><a rel=\"friend\" href=\"http://example.com/profile/jane\">jane</a></li>\n <li><a rel=\"acquaintance\" href=\"http://example.com/profile/jeo\">jeo</a></li>\n <li><a rel=\"contact\" href=\"http://example.com/profile/lily\">lily</a></li>\n <li><a rel=\"met\" href=\"http://example.com/profile/oliver\">oliver</a></li>\n <li><a rel=\"co-worker\" href=\"http://example.com/profile/emily\">emily</a></li>\n <li><a rel=\"colleague\" href=\"http://example.com/profile/jack\">jack</a></li>\n <li><a rel=\"neighbor\" href=\"http://example.com/profile/isabella\">isabella</a></li>\n <li><a rel=\"child\" href=\"http://example.com/profile/harry\">harry</a></li>\n <li><a rel=\"parent\" href=\"http://example.com/profile/sophia\">sophia</a></li>\n <li><a rel=\"sibling\" href=\"http://example.com/profile/charlie\">charlie</a></li>\n <li><a rel=\"spouse\" href=\"http://example.com/profile/olivia\">olivia</a></li>\n <li><a rel=\"kin\" href=\"http://example.com/profile/james\">james</a></li>\n <li><a rel=\"muse\" href=\"http://example.com/profile/ava\">ava</a></li>\n <li><a rel=\"crush\" href=\"http://example.com/profile/joshua\">joshua</a></li>\n <li><a rel=\"date\" href=\"http://example.com/profile/chloe\">chloe</a></li>\n <li><a rel=\"sweetheart\" href=\"http://example.com/profile/alfie\">alfie</a></li>\n <li><a rel=\"me\" href=\"http://example.com/profile/isla\">isla</a></li>\n</ul>";
+ var expected = {"items":[],"rels":{"friend":["http://example.com/profile/jane"],"acquaintance":["http://example.com/profile/jeo"],"contact":["http://example.com/profile/lily"],"met":["http://example.com/profile/oliver"],"co-worker":["http://example.com/profile/emily"],"colleague":["http://example.com/profile/jack"],"neighbor":["http://example.com/profile/isabella"],"child":["http://example.com/profile/harry"],"parent":["http://example.com/profile/sophia"],"sibling":["http://example.com/profile/charlie"],"spouse":["http://example.com/profile/olivia"],"kin":["http://example.com/profile/james"],"muse":["http://example.com/profile/ava"],"crush":["http://example.com/profile/joshua"],"date":["http://example.com/profile/chloe"],"sweetheart":["http://example.com/profile/alfie"],"me":["http://example.com/profile/isla"]},"rel-urls":{"http://example.com/profile/jane":{"text":"jane","rels":["friend"]},"http://example.com/profile/jeo":{"text":"jeo","rels":["acquaintance"]},"http://example.com/profile/lily":{"text":"lily","rels":["contact"]},"http://example.com/profile/oliver":{"text":"oliver","rels":["met"]},"http://example.com/profile/emily":{"text":"emily","rels":["co-worker"]},"http://example.com/profile/jack":{"text":"jack","rels":["colleague"]},"http://example.com/profile/isabella":{"text":"isabella","rels":["neighbor"]},"http://example.com/profile/harry":{"text":"harry","rels":["child"]},"http://example.com/profile/sophia":{"text":"sophia","rels":["parent"]},"http://example.com/profile/charlie":{"text":"charlie","rels":["sibling"]},"http://example.com/profile/olivia":{"text":"olivia","rels":["spouse"]},"http://example.com/profile/james":{"text":"james","rels":["kin"]},"http://example.com/profile/ava":{"text":"ava","rels":["muse"]},"http://example.com/profile/joshua":{"text":"joshua","rels":["crush"]},"http://example.com/profile/chloe":{"text":"chloe","rels":["date"]},"http://example.com/profile/alfie":{"text":"alfie","rels":["sweetheart"]},"http://example.com/profile/isla":{"text":"isla","rels":["me"]}}};
+
+ it('xfn-all', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-elsewhere.js b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-elsewhere.js
new file mode 100644
index 0000000000..d982af5cc2
--- /dev/null
+++ b/toolkit/components/microformats/test/standards-tests/mf-v2-rel-xfn-elsewhere.js
@@ -0,0 +1,27 @@
+/*
+Microformats Test Suite - Downloaded from github repo: microformats/tests version v0.1.24
+Mocha integration test from: microformats-v2/rel/xfn-elsewhere
+The test was built on Fri Sep 25 2015 13:26:26 GMT+0100 (BST)
+*/
+
+assert = chai.assert;
+
+
+describe('rel', function() {
+ var htmlFragment = "<ul>\n <li><a rel=\"me\" href=\"http://twitter.com/glennjones\">twitter</a></li>\n <li><a rel=\"me\" href=\"http://delicious.com/glennjonesnet/\">delicious</a></li>\n <li><a rel=\"me\" href=\"https://plus.google.com/u/0/105161464208920272734/about\">google+</a></li>\n <li><a rel=\"me\" href=\"http://lanyrd.com/people/glennjones/\">lanyrd</a></li>\n <li><a rel=\"me\" href=\"http://github.com/glennjones\">github</a></li>\n <li><a rel=\"me\" href=\"http://www.flickr.com/photos/glennjonesnet/\">flickr</a></li>\n <li><a rel=\"me\" href=\"http://www.linkedin.com/in/glennjones\">linkedin</a></li>\n <li><a rel=\"me\" href=\"http://www.slideshare.net/glennjones/presentations\">slideshare</a></li>\n</ul>";
+ var expected = {"items":[],"rels":{"me":["http://twitter.com/glennjones","http://delicious.com/glennjonesnet/","https://plus.google.com/u/0/105161464208920272734/about","http://lanyrd.com/people/glennjones/","http://github.com/glennjones","http://www.flickr.com/photos/glennjonesnet/","http://www.linkedin.com/in/glennjones","http://www.slideshare.net/glennjones/presentations"]},"rel-urls":{"http://twitter.com/glennjones":{"text":"twitter","rels":["me"]},"http://delicious.com/glennjonesnet/":{"text":"delicious","rels":["me"]},"https://plus.google.com/u/0/105161464208920272734/about":{"text":"google+","rels":["me"]},"http://lanyrd.com/people/glennjones/":{"text":"lanyrd","rels":["me"]},"http://github.com/glennjones":{"text":"github","rels":["me"]},"http://www.flickr.com/photos/glennjonesnet/":{"text":"flickr","rels":["me"]},"http://www.linkedin.com/in/glennjones":{"text":"linkedin","rels":["me"]},"http://www.slideshare.net/glennjones/presentations":{"text":"slideshare","rels":["me"]}}};
+
+ it('xfn-elsewhere', function(){
+ var doc, dom, node, options, parser, found;
+ dom = new DOMParser();
+ doc = dom.parseFromString( htmlFragment, 'text/html' );
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5'
+ };
+ found = Microformats.get( options );
+ assert.deepEqual(found, expected);
+ });
+});
diff --git a/toolkit/components/microformats/test/static/count.html b/toolkit/components/microformats/test/static/count.html
new file mode 100644
index 0000000000..c367b29ead
--- /dev/null
+++ b/toolkit/components/microformats/test/static/count.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+ <meta charset="UTF-8">
+ <title>Count Microformats</title>
+ <meta name="description" content="microformat-shiv - A light-weight cross browser javascript microformats 2 parser" />
+
+ <link rel="stylesheet" href="css/testrunner.css">
+ <link rel="stylesheet" href="css/prettify.css">
+
+
+ <!-- loads Modules to help with debugging ie windows.Modules -->
+ <script src="../../lib/utilities.js"></script>
+ <script src="../../lib/domutils.js"></script>
+ <script src="../../lib/url.js"></script>
+ <script src="../../lib/html.js"></script>
+ <script src="../../lib/text.js"></script>
+ <script src="../../lib/dates.js"></script>
+ <script src="../../lib/isodate.js"></script>
+ <script src="../../lib/parser.js"></script>
+ <script src="../../lib/parser-implied.js"></script>
+ <script src="../../lib/parser-includes.js"></script>
+ <script src="../../lib/parser-rels.js"></script>
+
+ <script src="../../lib/maps/h-adr.js"></script>
+ <script src="../../lib/maps/h-card.js"></script>
+ <script src="../../lib/maps/h-entry.js"></script>
+ <script src="../../lib/maps/h-event.js"></script>
+ <script src="../../lib/maps/h-feed.js"></script>
+ <script src="../../lib/maps/h-geo.js"></script>
+ <script src="../../lib/maps/h-item.js"></script>
+ <script src="../../lib/maps/h-listing.js"></script>
+ <script src="../../lib/maps/h-news.js"></script>
+ <script src="../../lib/maps/h-org.js"></script>
+ <script src="../../lib/maps/h-product.js"></script>
+ <script src="../../lib/maps/h-recipe.js"></script>
+ <script src="../../lib/maps/h-resume.js"></script>
+ <script src="../../lib/maps/h-review-aggregate.js"></script>
+ <script src="../../lib/maps/h-review.js"></script>
+ <script src="../../lib/maps/rel.js"></script>
+
+
+ <script src="javascript/beautify.js"></script>
+ <script src="javascript/prettify.js"></script>
+
+ <script src="javascript/count.js"></script>
+
+ </head>
+
+ <body>
+
+ <p>microformat-shiv</p>
+ <h1>Count Microformats</h1>
+ <p>Type or copy and paste the HTML you want to parse into the box below.</p>
+
+ <form id="mf-form" class="tool-interface" method="get" action="">
+ <p>
+ <label for="html">HTML</label>
+<textarea id="html" name="html">&lt;a class="h-card" href="http://glennjones.net"&gt;
+ &lt;span class="p-given-name"&gt;Glenn&lt;/span&gt;
+ &lt;span class="p-family-name"&gt;Jones&lt;/span&gt;
+&lt;/a&gt;
+&lt;a class="h-card" href="http://janedoe.net"&gt;
+ &lt;span class="p-given-name"&gt;Jane&lt;/span&gt;
+ &lt;span class="p-family-name"&gt;Doe&lt;/span&gt;
+&lt;/a&gt;
+&lt;a class="h-event" href="http://janedoe.net"&gt;
+ &lt;span class="p-name"&gt;Event&lt;/span&gt;
+ &lt;span class="dt-start"&gt;2015-07-01&lt;/span&gt;
+&lt;/a&gt;
+</textarea>
+ </p>
+
+ <input class="button" value="Count" type="submit">
+ </form>
+
+ <h1>Parser JSON</h1>
+ <div id="parser-json"><pre class="prettyprint"><code class="language-json"></code></pre></div>
+
+
+
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/microformats/test/static/css/mocha-custom.css b/toolkit/components/microformats/test/static/css/mocha-custom.css
new file mode 100644
index 0000000000..30f07756b4
--- /dev/null
+++ b/toolkit/components/microformats/test/static/css/mocha-custom.css
@@ -0,0 +1,9 @@
+
+body {
+ font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
+ padding: 60px 50px;
+}
+
+h3.capitalize {
+ text-transform: capitalize;
+} \ No newline at end of file
diff --git a/toolkit/components/microformats/test/static/css/mocha.css b/toolkit/components/microformats/test/static/css/mocha.css
new file mode 100644
index 0000000000..42b9798fa4
--- /dev/null
+++ b/toolkit/components/microformats/test/static/css/mocha.css
@@ -0,0 +1,270 @@
+@charset "utf-8";
+
+body {
+ margin:0;
+}
+
+#mocha {
+ font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: 60px 50px;
+}
+
+#mocha ul,
+#mocha li {
+ margin: 0;
+ padding: 0;
+}
+
+#mocha ul {
+ list-style: none;
+}
+
+#mocha h1,
+#mocha h2 {
+ margin: 0;
+}
+
+#mocha h1 {
+ margin-top: 15px;
+ font-size: 1em;
+ font-weight: 200;
+}
+
+#mocha h1 a {
+ text-decoration: none;
+ color: inherit;
+}
+
+#mocha h1 a:hover {
+ text-decoration: underline;
+}
+
+#mocha .suite .suite h1 {
+ margin-top: 0;
+ font-size: .8em;
+}
+
+#mocha .hidden {
+ display: none;
+}
+
+#mocha h2 {
+ font-size: 12px;
+ font-weight: normal;
+ cursor: pointer;
+}
+
+#mocha .suite {
+ margin-left: 15px;
+}
+
+#mocha .test {
+ margin-left: 15px;
+ overflow: hidden;
+}
+
+#mocha .test.pending:hover h2::after {
+ content: '(pending)';
+ font-family: arial, sans-serif;
+}
+
+#mocha .test.pass.medium .duration {
+ background: #c09853;
+}
+
+#mocha .test.pass.slow .duration {
+ background: #b94a48;
+}
+
+#mocha .test.pass::before {
+ content: '✓';
+ font-size: 12px;
+ display: block;
+ float: left;
+ margin-right: 5px;
+ color: #00d6b2;
+}
+
+#mocha .test.pass .duration {
+ font-size: 9px;
+ margin-left: 5px;
+ padding: 2px 5px;
+ color: #fff;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+ -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ -ms-border-radius: 5px;
+ -o-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#mocha .test.pass.fast .duration {
+ display: none;
+}
+
+#mocha .test.pending {
+ color: #0b97c4;
+}
+
+#mocha .test.pending::before {
+ content: '◦';
+ color: #0b97c4;
+}
+
+#mocha .test.fail {
+ color: #c00;
+}
+
+#mocha .test.fail pre {
+ color: black;
+}
+
+#mocha .test.fail::before {
+ content: '✖';
+ font-size: 12px;
+ display: block;
+ float: left;
+ margin-right: 5px;
+ color: #c00;
+}
+
+#mocha .test pre.error {
+ color: #c00;
+ max-height: 300px;
+ overflow: auto;
+}
+
+/**
+ * (1): approximate for browsers not supporting calc
+ * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
+ * ^^ seriously
+ */
+#mocha .test pre {
+ display: block;
+ float: left;
+ clear: left;
+ font: 12px/1.5 monaco, monospace;
+ margin: 5px;
+ padding: 15px;
+ border: 1px solid #eee;
+ max-width: 85%; /*(1)*/
+ max-width: calc(100% - 42px); /*(2)*/
+ word-wrap: break-word;
+ border-bottom-color: #ddd;
+ -webkit-border-radius: 3px;
+ -webkit-box-shadow: 0 1px 3px #eee;
+ -moz-border-radius: 3px;
+ -moz-box-shadow: 0 1px 3px #eee;
+ border-radius: 3px;
+}
+
+#mocha .test h2 {
+ position: relative;
+}
+
+#mocha .test a.replay {
+ position: absolute;
+ top: 3px;
+ right: 0;
+ text-decoration: none;
+ vertical-align: middle;
+ display: block;
+ width: 15px;
+ height: 15px;
+ line-height: 15px;
+ text-align: center;
+ background: #eee;
+ font-size: 15px;
+ -moz-border-radius: 15px;
+ border-radius: 15px;
+ -webkit-transition: opacity 200ms;
+ -moz-transition: opacity 200ms;
+ transition: opacity 200ms;
+ opacity: 0.3;
+ color: #888;
+}
+
+#mocha .test:hover a.replay {
+ opacity: 1;
+}
+
+#mocha-report.pass .test.fail {
+ display: none;
+}
+
+#mocha-report.fail .test.pass {
+ display: none;
+}
+
+#mocha-report.pending .test.pass,
+#mocha-report.pending .test.fail {
+ display: none;
+}
+#mocha-report.pending .test.pass.pending {
+ display: block;
+}
+
+#mocha-error {
+ color: #c00;
+ font-size: 1.5em;
+ font-weight: 100;
+ letter-spacing: 1px;
+}
+
+#mocha-stats {
+ position: fixed;
+ top: 15px;
+ right: 10px;
+ font-size: 12px;
+ margin: 0;
+ color: #888;
+ z-index: 1;
+}
+
+#mocha-stats .progress {
+ float: right;
+ padding-top: 0;
+}
+
+#mocha-stats em {
+ color: black;
+}
+
+#mocha-stats a {
+ text-decoration: none;
+ color: inherit;
+}
+
+#mocha-stats a:hover {
+ border-bottom: 1px solid #eee;
+}
+
+#mocha-stats li {
+ display: inline-block;
+ margin: 0 5px;
+ list-style: none;
+ padding-top: 11px;
+}
+
+#mocha-stats canvas {
+ width: 40px;
+ height: 40px;
+}
+
+#mocha code .comment { color: #ddd; }
+#mocha code .init { color: #2f6fad; }
+#mocha code .string { color: #5890ad; }
+#mocha code .keyword { color: #8a6343; }
+#mocha code .number { color: #2f6fad; }
+
+@media screen and (max-device-width: 480px) {
+ #mocha {
+ margin: 60px 0px;
+ }
+
+ #mocha #stats {
+ position: absolute;
+ }
+}
diff --git a/toolkit/components/microformats/test/static/css/prettify.css b/toolkit/components/microformats/test/static/css/prettify.css
new file mode 100644
index 0000000000..843e903e7e
--- /dev/null
+++ b/toolkit/components/microformats/test/static/css/prettify.css
@@ -0,0 +1,65 @@
+/* Pretty printing styles. Used with prettify.js. */
+
+.str { color: #85C5DC; }
+.kwd { color: #EDF0D1; }
+.com { color: #878989; }
+.typ { color: #F5896F; }
+.lit { color: #FFB17A; }
+.pun { color: #FFFFFF; }
+.pln { color: #FFFFFF; }
+.tag { color: #F5896F; }
+.atn { color: #F5896F; }
+.atv { color: #85C5DC; }
+.dec { color: #878989; }
+
+pre.prettyprint {
+ background-color:#302F2D;
+ border: none;
+ line-height: normal;
+ font-size: 100%;
+ border-radius: 6px 6px 6px 6px;
+ font-family: consolas,​'andale mono',​'courier new',​monospace;
+ padding-top: 12px;
+ overflow: hidden;
+}
+
+code{
+ font-size: 13px;
+ line-height: normal;
+}
+
+/* Specify class=linenums on a pre to get line numbering */
+ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */
+li.L0,
+li.L1,
+li.L2,
+li.L3,
+li.L5,
+li.L6,
+li.L7,
+li.L8 { list-style-type: none }
+/* Alternate shading for lines */
+li.L1,
+li.L3,
+li.L5,
+li.L7,
+li.L9 { background: #eee }
+
+@media print {
+ .str { color: #060; }
+ .kwd { color: #006; font-weight: bold; }
+ .com { color: #600; font-style: italic; }
+ .typ { color: #404; font-weight: bold; }
+ .lit { color: #044; }
+ .pun { color: #440; }
+ .pln { color: #000; }
+ .tag { color: #006; font-weight: bold; }
+ .atn { color: #404; }
+ .atv { color: #060; }
+}
+
+
+/* correct additional line return at top of html diaplay*/
+code>:first-child{
+ display: none;
+} \ No newline at end of file
diff --git a/toolkit/components/microformats/test/static/css/testrunner.css b/toolkit/components/microformats/test/static/css/testrunner.css
new file mode 100644
index 0000000000..0064b139c6
--- /dev/null
+++ b/toolkit/components/microformats/test/static/css/testrunner.css
@@ -0,0 +1,367 @@
+/*
+All content and code is released into the public domain
+http://en.wikipedia.org/wiki/public_domain
+
+Contributors
+Glenn Jones - http://glennjones.net/
+*/
+
+
+
+
+@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700);
+
+body {
+ padding:50px;
+ font:1em "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color:#333;
+ font-weight:300;
+ border-top: 5px solid #302F2D;
+ margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color:#222;
+ font-weight: normal;
+}
+
+p, ul, ol, table, pre, dl {
+ margin:0 0 20px;
+}
+
+h1, h2, h3 {
+ margin-top: 50px;
+ margin-bottom: 10px;
+ font-family: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+h1 {
+ font-size: 2em;
+}
+
+h2, h3 {
+ font-size: 1.5em;
+}
+
+/* the first h1 in a page */
+h1:first-of-type{
+ margin-top: 0;
+ font-weight: bold;
+}
+
+a {
+ color: #39c;
+ font-weight: 300;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+a[name]{
+ color: #333;
+}
+
+a[name]:hover {
+ text-decoration: none;
+}
+
+blockquote {
+ border-left: 1px solid #e5e5e5;
+ margin: 0;
+ padding: 0 0 0 20px;
+ font-style: italic;
+}
+
+code, pre {
+ font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;
+ font-size: 12px;
+ margin: 0;
+}
+
+pre {
+ padding: 8px 15px;
+ border-radius: 5px;
+ border: 1px solid #e5e5e5;
+ overflow-x: auto;
+ color: #fff;
+ margin: 0;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+th, td {
+ text-align: left;
+ padding: 2px 10px;
+ border-bottom: 1px solid #e5e5e5;
+ font-weight: 300;
+}
+
+th{
+ background-color: #333;
+ color:#fff;
+}
+
+td a{
+ font-weight: 300;
+}
+
+dt {
+ color:#444;
+ font-weight:700;
+}
+
+img {
+ max-width:100%;
+}
+
+li{
+ padding: 0.25em 0 0.25em 0;
+}
+
+
+button, input[type="submit"], input[type="button"], .button {
+ display: inline-block;
+ padding-top: 0.4em;
+ padding-right: 1em;
+ padding-left: 1em;
+ padding-bottom: 0.5em;
+ margin-bottom: 0;
+ font-weight: 200;
+ font-size: 1rem;
+ text-align: center;
+ white-space: nowrap;
+ cursor: pointer;
+ background-image: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background-color: #33a0e8;
+ color: #fff;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+select {
+ height: 34px;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857143;
+ color: #555;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+ -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
+ -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+ transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+}
+
+option {
+ padding: 2em;
+}
+
+footer{
+ margin-top: 8em;
+ text-align: center;
+}
+
+
+/* The Magnificent Clearfix: Updated to prevent margin-collapsing on child elements.
+ j.mp/bestclearfix */
+.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
+.clearfix:after { clear: both; }
+/* Fix clearfix: blueprintcss.lighthouseapp.com/projects/15318/tickets/5-extra-margin-padding-bottom-of-page */
+.clearfix { zoom: 1; }
+
+.test-counts{
+ font-weight: 700;
+ margin-bottom: 4em;
+}
+
+.test-detail{
+ display:none;
+}
+
+.all-test-list, .test-result-list{
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: block;
+ float: left;
+}
+
+.all-test-list li .test-result-list li{
+ padding: 0.2em 0 0.2em 0;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.all-test-list li::hover, .test-result-list li::hover{
+ text-decoration: underline;
+}
+
+.all-test-list li.test-error a, .test-result-list li.test-error a{
+ color: red;
+ text-decoration: underline;
+}
+
+.flexbox-container {
+ display: -ms-flex;
+ display: -webkit-flex;
+ display: flex;
+}
+
+.flexbox-container > section {
+ margin-bottom: 3em;
+}
+
+.flexbox-container > section:first-child {
+ width: 20%;
+ margin-right: 20px;
+}
+
+.flexbox-container > section:nth-child(2) {
+ padding-left: 1em;
+ width: 80%;
+}
+
+
+@media (max-width: 1400px) {
+ .flexbox-container > section:first-child {
+ width: 25%;
+ }
+
+ .flexbox-container > section:nth-child(2) {
+ width: 75%;
+ }
+}
+
+@media (max-width: 1200px) {
+ .flexbox-container > section:first-child {
+ width: 30%;
+ }
+
+ .flexbox-container > section:nth-child(2) {
+ width: 70%;
+ }
+}
+
+
+@media (max-width: 1000px) {
+ .flexbox-container > section:first-child {
+ width: 35%;
+ }
+
+ .flexbox-container > section:nth-child(2) {
+ width: 65%;
+ }
+}
+
+
+
+
+
+
+#test-status {
+ margin-top: 0;
+}
+
+.test-passed{
+ border-left: 10px solid green;
+}
+
+.test-failed{
+ border-left: 10px solid red;
+}
+
+.differences-description{
+ margin-top: 2em;
+}
+
+.differences-description li{
+ padding: 0;
+}
+
+.failed{
+ color: red;
+}
+
+#test-list-by-version section{
+ width: 30%;
+ margin-right: 20px;
+}
+
+.test-container{
+ padding-left: 1em;
+}
+
+
+#textcontent-test textarea{
+ font-size: 1em;
+ min-height: 6em;
+ min-width: 40em;
+ padding: 0.5em;
+}
+
+#textcontent-test input{
+ font-size: 1em;
+}
+
+
+/* Tool interface */
+
+.tool-interface input[type="text"], input[type="url"] {
+ width: 20em;
+ padding: 0.4em;
+ border: 1px solid #999;
+ border-radius: 0.4em;
+ color: #333;
+ font-size: 1em
+}
+
+.tool-interface label {
+ display: inline-block;
+ width: 5em;
+}
+
+.tool-interface select {
+ padding: 0.2em;
+ font-family: open_sansregular, calibri, arial, helvetica, 'lucida grande', 'lucida sans unicode', verdana, sans-serif;
+}
+
+.tool-interface option{
+ padding: 0.2em;
+}
+
+.tool-interface textarea{
+ width: 100%;
+ height: 16em;
+ border: 1px solid #999;
+ border-radius: 0.4em;
+ font-size: 1em;
+ font-family: open_sansregular, calibri, arial, helvetica, 'lucida grande', 'lucida sans unicode', verdana, sans-serif;
+}
+
+.tool-interface .button{
+ margin-left: 5.2em;
+}
+
+.tool-interface label.checkbox-label{
+ width: auto;
+}
+
+.tool-interface .checkbox{
+ margin-left: 5.2em;
+}
+
+select.indent {
+ margin-left: 5em;
+}
diff --git a/toolkit/components/microformats/test/static/images/logo.gif b/toolkit/components/microformats/test/static/images/logo.gif
new file mode 100644
index 0000000000..96c965e702
--- /dev/null
+++ b/toolkit/components/microformats/test/static/images/logo.gif
Binary files differ
diff --git a/toolkit/components/microformats/test/static/images/photo.gif b/toolkit/components/microformats/test/static/images/photo.gif
new file mode 100644
index 0000000000..96c965e702
--- /dev/null
+++ b/toolkit/components/microformats/test/static/images/photo.gif
Binary files differ
diff --git a/toolkit/components/microformats/test/static/javascript/DOMParser.js b/toolkit/components/microformats/test/static/javascript/DOMParser.js
new file mode 100644
index 0000000000..fa26bcdfd9
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/DOMParser.js
@@ -0,0 +1,99 @@
+
+// Based on https://gist.github.com/1129031 By Eli Grey, http://eligrey.com - Public domain.
+
+// DO NOT use https://developer.mozilla.org/en-US/docs/Web/API/DOMParser example polyfill
+// as it does not work with earliar version of chrome
+
+
+(function(DOMParser) {
+ "use strict";
+
+ var DOMParser_proto;
+ var real_parseFromString;
+ var textHTML; // Flag for text/html support
+ var textXML; // Flag for text/xml support
+ var htmlElInnerHTML; // Flag for support for setting html element's innerHTML
+
+ // Stop here if DOMParser not defined
+ if (!DOMParser) return;
+
+ // Firefox, Opera and IE throw errors on unsupported types
+ try {
+ // WebKit returns null on unsupported types
+ textHTML = !!(new DOMParser).parseFromString('', 'text/html');
+
+ } catch (er) {
+ textHTML = false;
+ }
+
+ // If text/html supported, don't need to do anything.
+ if (textHTML) return;
+
+ // Next try setting innerHTML of a created document
+ // IE 9 and lower will throw an error (can't set innerHTML of its HTML element)
+ try {
+ var doc = document.implementation.createHTMLDocument('');
+ doc.documentElement.innerHTML = '<title></title><div></div>';
+ htmlElInnerHTML = true;
+
+ } catch (er) {
+ htmlElInnerHTML = false;
+ }
+
+ // If if that failed, try text/xml
+ if (!htmlElInnerHTML) {
+
+ try {
+ textXML = !!(new DOMParser).parseFromString('', 'text/xml');
+
+ } catch (er) {
+ textHTML = false;
+ }
+ }
+
+ // Mess with DOMParser.prototype (less than optimal...) if one of the above worked
+ // Assume can write to the prototype, if not, make this a stand alone function
+ if (DOMParser.prototype && (htmlElInnerHTML || textXML)) {
+ DOMParser_proto = DOMParser.prototype;
+ real_parseFromString = DOMParser_proto.parseFromString;
+
+ DOMParser_proto.parseFromString = function (markup, type) {
+
+ // Only do this if type is text/html
+ if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
+ var doc, doc_el, first_el;
+
+ // Use innerHTML if supported
+ if (htmlElInnerHTML) {
+ doc = document.implementation.createHTMLDocument("");
+ doc_el = doc.documentElement;
+ doc_el.innerHTML = markup;
+ first_el = doc_el.firstElementChild;
+
+ // Otherwise use XML method
+ } else if (textXML) {
+
+ // Make sure markup is wrapped in HTML tags
+ // Should probably allow for a DOCTYPE
+ if (!(/^<html.*html>$/i.test(markup))) {
+ markup = '<html>' + markup + '<\/html>';
+ }
+ doc = (new DOMParser).parseFromString(markup, 'text/xml');
+ doc_el = doc.documentElement;
+ first_el = doc_el.firstElementChild;
+ }
+
+ // Is this an entire document or a fragment?
+ if (doc_el.childElementCount == 1 && first_el.localName.toLowerCase() == 'html') {
+ doc.replaceChild(first_el, doc_el);
+ }
+
+ return doc;
+
+ // If not text/html, send as-is to host method
+ } else {
+ return real_parseFromString.apply(this, arguments);
+ }
+ };
+ }
+}(DOMParser));
diff --git a/toolkit/components/microformats/test/static/javascript/beautify.js b/toolkit/components/microformats/test/static/javascript/beautify.js
new file mode 100644
index 0000000000..55d06cb08b
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/beautify.js
@@ -0,0 +1,518 @@
+/*
+
+ JS Beautifier
+---------------
+
+ Written by Einars "elfz" Lielmanis, <elfz@laacz.lv>
+ http://elfz.laacz.lv/beautify/
+
+ Originally converted to javascript by Vital, <vital76@gmail.com>
+ http://my.opera.com/Vital/blog/2007/11/21/javascript-beautify-on-javascript-translated
+
+
+ You are free to use this in any way you want, in case you find this useful or working for you.
+
+ Usage:
+ js_beautify(js_source_text);
+
+
+*/
+
+
+function js_beautify(js_source_text, indent_size, indent_character)
+{
+
+ var input, output, token_text, last_type, current_mode, modes, indent_level, indent_string;
+ var whitespace, wordchar, punct;
+
+ indent_character = indent_character || ' ';
+ indent_size = indent_size || 4;
+
+ indent_string = '';
+ while (indent_size--) indent_string += indent_character;
+
+ input = js_source_text;
+
+ last_word = ''; // last 'TK_WORD' passed
+ last_type = 'TK_START_EXPR'; // last token type
+ last_text = ''; // last token text
+ output = '';
+
+ whitespace = "\n\r\t ".split('');
+ wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split('');
+ punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |='.split(' ');
+
+ // words which should always start on new line.
+ line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',');
+
+ // states showing if we are currently in expression (i.e. "if" case) - 'EXPRESSION', or in usual block (like, procedure), 'BLOCK'.
+ // some formatting depends on that.
+ current_mode = 'BLOCK';
+ modes = [current_mode];
+
+ indent_level = 0;
+ parser_pos = 0; // parser position
+ in_case = false; // flag for parser that case/default has been processed, and next colon needs special attention
+ while (true) {
+ var t = get_next_token(parser_pos);
+ token_text = t[0];
+ token_type = t[1];
+ if (token_type == 'TK_EOF') {
+ break;
+ }
+
+ switch (token_type) {
+
+ case 'TK_START_EXPR':
+
+ set_mode('EXPRESSION');
+ if (last_type == 'TK_END_EXPR' || last_type == 'TK_START_EXPR') {
+ // do nothing on (( and )( and ][ and ]( ..
+ } else if (last_type != 'TK_WORD' && last_type != 'TK_OPERATOR') {
+ print_space();
+ } else if (in_array(last_word, line_starters) && last_word != 'function') {
+ print_space();
+ }
+ print_token();
+ break;
+
+ case 'TK_END_EXPR':
+
+ print_token();
+ restore_mode();
+ break;
+
+ case 'TK_START_BLOCK':
+
+ set_mode('BLOCK');
+ if (last_type != 'TK_OPERATOR' && last_type != 'TK_START_EXPR') {
+ if (last_type == 'TK_START_BLOCK') {
+ print_newline();
+ } else {
+ print_space();
+ }
+ }
+ print_token();
+ indent();
+ break;
+
+ case 'TK_END_BLOCK':
+
+ if (last_type == 'TK_END_EXPR') {
+ unindent();
+ print_newline();
+ } else if (last_type == 'TK_END_BLOCK') {
+ unindent();
+ print_newline();
+ } else if (last_type == 'TK_START_BLOCK') {
+ // nothing
+ unindent();
+ } else {
+ unindent();
+ print_newline();
+ }
+ print_token();
+ restore_mode();
+ break;
+
+ case 'TK_WORD':
+
+ if (token_text == 'case' || token_text == 'default') {
+ if (last_text == ':') {
+ // switch cases following one another
+ remove_indent();
+ } else {
+ // case statement starts in the same line where switch
+ unindent();
+ print_newline();
+ indent();
+ }
+ print_token();
+ in_case = true;
+ break;
+ }
+
+ prefix = 'NONE';
+ if (last_type == 'TK_END_BLOCK') {
+ if (!in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) {
+ prefix = 'NEWLINE';
+ } else {
+ prefix = 'SPACE';
+ print_space();
+ }
+ } else if (last_type == 'TK_END_COMMAND' && current_mode == 'BLOCK') {
+ prefix = 'NEWLINE';
+ } else if (last_type == 'TK_END_COMMAND' && current_mode == 'EXPRESSION') {
+ prefix = 'SPACE';
+ } else if (last_type == 'TK_WORD') {
+ prefix = 'SPACE';
+ } else if (last_type == 'TK_START_BLOCK') {
+ prefix = 'NEWLINE';
+ } else if (last_type == 'TK_END_EXPR') {
+ print_space();
+ prefix = 'NEWLINE';
+ }
+
+ if (in_array(token_text, line_starters) || prefix == 'NEWLINE') {
+
+ if (last_text == 'else') {
+ // no need to force newline on else break
+ print_space();
+ } else if ((last_type == 'TK_START_EXPR' || last_text == '=') && token_text == 'function') {
+ // no need to force newline on 'function': (function
+ // DONOTHING
+ } else if (last_type == 'TK_WORD' && (last_text == 'return' || last_text == 'throw')) {
+ // no newline between 'return nnn'
+ print_space();
+ } else
+ if (last_type != 'TK_END_EXPR') {
+ if ((last_type != 'TK_START_EXPR' || token_text != 'var') && last_text != ':') {
+ // no need to force newline on 'var': for (var x = 0...)
+ if (token_text == 'if' && last_type == 'TK_WORD' && last_word == 'else') {
+ // no newline for } else if {
+ print_space();
+ } else {
+ print_newline();
+ }
+ }
+ }
+ } else if (prefix == 'SPACE') {
+ print_space();
+ }
+ print_token();
+ last_word = token_text;
+ break;
+
+ case 'TK_END_COMMAND':
+
+ print_token();
+ break;
+
+ case 'TK_STRING':
+
+ if (last_type == 'TK_START_BLOCK' || last_type == 'TK_END_BLOCK') {
+ print_newline();
+ } else if (last_type == 'TK_WORD') {
+ print_space();
+ }
+ print_token();
+ break;
+
+ case 'TK_OPERATOR':
+ start_delim = true;
+ end_delim = true;
+
+ if (token_text == ':' && in_case) {
+ print_token(); // colon really asks for separate treatment
+ print_newline();
+ break;
+ }
+
+ in_case = false;
+
+ if (token_text == ',') {
+ if (last_type == 'TK_END_BLOCK') {
+ print_token();
+ print_newline();
+ } else {
+ if (current_mode == 'BLOCK') {
+ print_token();
+ print_newline();
+ } else {
+ print_token();
+ print_space();
+ }
+ }
+ break;
+ } else if (token_text == '--' || token_text == '++') { // unary operators special case
+ if (last_text == ';') {
+ // space for (;; ++i)
+ start_delim = true;
+ end_delim = false;
+ } else {
+ start_delim = false;
+ end_delim = false;
+ }
+ } else if (token_text == '!' && last_type == 'TK_START_EXPR') {
+ // special case handling: if (!a)
+ start_delim = false;
+ end_delim = false;
+ } else if (last_type == 'TK_OPERATOR') {
+ start_delim = false;
+ end_delim = false;
+ } else if (last_type == 'TK_END_EXPR') {
+ start_delim = true;
+ end_delim = true;
+ } else if (token_text == '.') {
+ // decimal digits or object.property
+ start_delim = false;
+ end_delim = false;
+
+ } else if (token_text == ':') {
+ // zz: xx
+ // can't differentiate ternary op, so for now it's a ? b: c; without space before colon
+ start_delim = false;
+ }
+ if (start_delim) {
+ print_space();
+ }
+
+ print_token();
+
+ if (end_delim) {
+ print_space();
+ }
+ break;
+
+ case 'TK_BLOCK_COMMENT':
+
+ print_newline();
+ print_token();
+ print_newline();
+ break;
+
+ case 'TK_COMMENT':
+
+ // print_newline();
+ print_space();
+ print_token();
+ print_newline();
+ break;
+
+ case 'TK_UNKNOWN':
+ print_token();
+ break;
+ }
+
+ if (token_type != 'TK_COMMENT') {
+ last_type = token_type;
+ last_text = token_text;
+ }
+ }
+
+ return output;
+
+
+
+
+ function print_newline(ignore_repeated)
+ {
+ ignore_repeated = typeof ignore_repeated == 'undefined' ? true: ignore_repeated;
+ output = output.replace(/[ \t]+$/, ''); // remove possible indent
+ if (output == '') return; // no newline on start of file
+ if (output.substr(output.length - 1) != "\n" || !ignore_repeated) {
+ output += "\n";
+ }
+ for (var i = 0; i < indent_level; i++) {
+ output += indent_string;
+ }
+ }
+
+
+
+ function print_space()
+ {
+ if (output && output.substr(output.length - 1) != ' ' && output.substr(output.length - 1) != '\n') { // prevent occassional duplicate space
+ output += ' ';
+ }
+ }
+
+
+ function print_token()
+ {
+ output += token_text;
+ }
+
+ function indent()
+ {
+ indent_level++;
+ }
+
+
+ function unindent()
+ {
+ if (indent_level) {
+ indent_level--;
+ }
+ }
+
+
+ function remove_indent()
+ {
+ if (output.substr(output.length - indent_string.length) == indent_string) {
+ output = output.substr(0, output.length - indent_string.length);
+ }
+ }
+
+
+ function set_mode(mode)
+ {
+ modes.push(current_mode);
+ current_mode = mode;
+ }
+
+
+ function restore_mode()
+ {
+ current_mode = modes.pop();
+ }
+
+
+
+ function get_next_token()
+ {
+ var n_newlines = 0;
+ var c = '';
+
+ do {
+ if (parser_pos >= input.length) {
+ return ['', 'TK_EOF'];
+ }
+ c = input.charAt(parser_pos);
+
+ parser_pos += 1;
+ if (c == "\n") {
+ n_newlines += 1;
+ }
+ }
+ while (in_array(c, whitespace));
+
+ if (n_newlines > 1) {
+ for (var i = 0; i < n_newlines; i++) {
+ print_newline(i == 0);
+ }
+ }
+ var wanted_newline = n_newlines == 1;
+
+
+ if (in_array(c, wordchar)) {
+ if (parser_pos < input.length) {
+ while (in_array(input.charAt(parser_pos), wordchar)) {
+ c += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos == input.length) break;
+ }
+ }
+
+ // small and surprisingly unugly hack for 1E-10 representation
+ if (parser_pos != input.length && c.match(/^[0-9]+[Ee]$/) && input.charAt(parser_pos) == '-') {
+ parser_pos += 1;
+
+ var t = get_next_token(parser_pos);
+ next_word = t[0];
+ next_type = t[1];
+
+ c += '-' + next_word;
+ return [c, 'TK_WORD'];
+ }
+
+ if (c == 'in') { // hack for 'in' operator
+ return [c, 'TK_OPERATOR'];
+ }
+ return [c, 'TK_WORD'];
+ }
+
+ if (c == '(' || c == '[') {
+ return [c, 'TK_START_EXPR'];
+ }
+
+ if (c == ')' || c == ']') {
+ return [c, 'TK_END_EXPR'];
+ }
+
+ if (c == '{') {
+ return [c, 'TK_START_BLOCK'];
+ }
+
+ if (c == '}') {
+ return [c, 'TK_END_BLOCK'];
+ }
+
+ if (c == ';') {
+ return [c, 'TK_END_COMMAND'];
+ }
+
+ if (c == '/') {
+ // peek for comment /* ... */
+ if (input.charAt(parser_pos) == '*') {
+ comment = '';
+ parser_pos += 1;
+ if (parser_pos < input.length) {
+ while (! (input.charAt(parser_pos) == '*' && input.charAt(parser_pos + 1) && input.charAt(parser_pos + 1) == '/') && parser_pos < input.length) {
+ comment += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input.length) break;
+ }
+ }
+ parser_pos += 2;
+ return ['/*' + comment + '*/', 'TK_BLOCK_COMMENT'];
+ }
+ // peek for comment // ...
+ if (input.charAt(parser_pos) == '/') {
+ comment = c;
+ while (input.charAt(parser_pos) != "\x0d" && input.charAt(parser_pos) != "\x0a") {
+ comment += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input.length) break;
+ }
+ parser_pos += 1;
+ if (wanted_newline) {
+ print_newline();
+ }
+ return [comment, 'TK_COMMENT'];
+ }
+
+ }
+
+ if (c == "'" || // string
+ c == '"' || // string
+ (c == '/' &&
+ ((last_type == 'TK_WORD' && last_text == 'return') || (last_type == 'TK_START_EXPR' || last_type == 'TK_END_BLOCK' || last_type == 'TK_OPERATOR' || last_type == 'TK_EOF' || last_type == 'TK_END_COMMAND')))) { // regexp
+ sep = c;
+ c = '';
+ esc = false;
+
+ if (parser_pos < input.length) {
+
+ while (esc || input.charAt(parser_pos) != sep) {
+ c += input.charAt(parser_pos);
+ if (!esc) {
+ esc = input.charAt(parser_pos) == '\\';
+ } else {
+ esc = false;
+ }
+ parser_pos += 1;
+ if (parser_pos >= input.length) break;
+ }
+
+ }
+
+ parser_pos += 1;
+ if (last_type == 'TK_END_COMMAND') {
+ print_newline();
+ }
+ return [sep + c + sep, 'TK_STRING'];
+ }
+
+ if (in_array(c, punct)) {
+ while (parser_pos < input.length && in_array(c + input.charAt(parser_pos), punct)) {
+ c += input.charAt(parser_pos);
+ parser_pos += 1;
+ if (parser_pos >= input.length) break;
+ }
+ return [c, 'TK_OPERATOR'];
+ }
+
+ return [c, 'TK_UNKNOWN'];
+ }
+
+
+ function in_array(what, arr)
+ {
+ for (var i = 0; i < arr.length; i++)
+ {
+ if (arr[i] == what) return true;
+ }
+ return false;
+ }
+}
diff --git a/toolkit/components/microformats/test/static/javascript/chai.js b/toolkit/components/microformats/test/static/javascript/chai.js
new file mode 100644
index 0000000000..bbdbe907b8
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/chai.js
@@ -0,0 +1,5351 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.chai = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var used = [];
+exports = module.exports = {};
+
+/*!
+ * Chai version
+ */
+
+exports.version = '3.0.0';
+
+/*!
+ * Assertion Error
+ */
+
+exports.AssertionError = require('assertion-error');
+
+/*!
+ * Utils for plugins (not exported)
+ */
+
+var util = require('./chai/utils');
+
+/**
+ * # .use(function)
+ *
+ * Provides a way to extend the internals of Chai
+ *
+ * @param {Function}
+ * @returns {this} for chaining
+ * @api public
+ */
+
+exports.use = function (fn) {
+ if (!~used.indexOf(fn)) {
+ fn(this, util);
+ used.push(fn);
+ }
+
+ return this;
+};
+
+/*!
+ * Utility Functions
+ */
+
+exports.util = util;
+
+/*!
+ * Configuration
+ */
+
+var config = require('./chai/config');
+exports.config = config;
+
+/*!
+ * Primary `Assertion` prototype
+ */
+
+var assertion = require('./chai/assertion');
+exports.use(assertion);
+
+/*!
+ * Core Assertions
+ */
+
+var core = require('./chai/core/assertions');
+exports.use(core);
+
+/*!
+ * Expect interface
+ */
+
+var expect = require('./chai/interface/expect');
+exports.use(expect);
+
+/*!
+ * Should interface
+ */
+
+var should = require('./chai/interface/should');
+exports.use(should);
+
+/*!
+ * Assert interface
+ */
+
+var assert = require('./chai/interface/assert');
+exports.use(assert);
+
+},{"./chai/assertion":2,"./chai/config":3,"./chai/core/assertions":4,"./chai/interface/assert":5,"./chai/interface/expect":6,"./chai/interface/should":7,"./chai/utils":20,"assertion-error":28}],2:[function(require,module,exports){
+/*!
+ * chai
+ * http://chaijs.com
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var config = require('./config');
+
+module.exports = function (_chai, util) {
+ /*!
+ * Module dependencies.
+ */
+
+ var AssertionError = _chai.AssertionError
+ , flag = util.flag;
+
+ /*!
+ * Module export.
+ */
+
+ _chai.Assertion = Assertion;
+
+ /*!
+ * Assertion Constructor
+ *
+ * Creates object for chaining.
+ *
+ * @api private
+ */
+
+ function Assertion (obj, msg, stack) {
+ flag(this, 'ssfi', stack || arguments.callee);
+ flag(this, 'object', obj);
+ flag(this, 'message', msg);
+ }
+
+ Object.defineProperty(Assertion, 'includeStack', {
+ get: function() {
+ console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.');
+ return config.includeStack;
+ },
+ set: function(value) {
+ console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.');
+ config.includeStack = value;
+ }
+ });
+
+ Object.defineProperty(Assertion, 'showDiff', {
+ get: function() {
+ console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.');
+ return config.showDiff;
+ },
+ set: function(value) {
+ console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.');
+ config.showDiff = value;
+ }
+ });
+
+ Assertion.addProperty = function (name, fn) {
+ util.addProperty(this.prototype, name, fn);
+ };
+
+ Assertion.addMethod = function (name, fn) {
+ util.addMethod(this.prototype, name, fn);
+ };
+
+ Assertion.addChainableMethod = function (name, fn, chainingBehavior) {
+ util.addChainableMethod(this.prototype, name, fn, chainingBehavior);
+ };
+
+ Assertion.overwriteProperty = function (name, fn) {
+ util.overwriteProperty(this.prototype, name, fn);
+ };
+
+ Assertion.overwriteMethod = function (name, fn) {
+ util.overwriteMethod(this.prototype, name, fn);
+ };
+
+ Assertion.overwriteChainableMethod = function (name, fn, chainingBehavior) {
+ util.overwriteChainableMethod(this.prototype, name, fn, chainingBehavior);
+ };
+
+ /**
+ * ### .assert(expression, message, negateMessage, expected, actual, showDiff)
+ *
+ * Executes an expression and check expectations. Throws AssertionError for reporting if test doesn't pass.
+ *
+ * @name assert
+ * @param {Philosophical} expression to be tested
+ * @param {String or Function} message or function that returns message to display if expression fails
+ * @param {String or Function} negatedMessage or function that returns negatedMessage to display if negated expression fails
+ * @param {Mixed} expected value (remember to check for negation)
+ * @param {Mixed} actual (optional) will default to `this.obj`
+ * @param {Boolean} showDiff (optional) when set to `true`, assert will display a diff in addition to the message if expression fails
+ * @api private
+ */
+
+ Assertion.prototype.assert = function (expr, msg, negateMsg, expected, _actual, showDiff) {
+ var ok = util.test(this, arguments);
+ if (true !== showDiff) showDiff = false;
+ if (true !== config.showDiff) showDiff = false;
+
+ if (!ok) {
+ msg = util.getMessage(this, arguments)
+ var actual = util.getActual(this, arguments);
+ throw new AssertionError(msg, {
+ actual: actual
+ , expected: expected
+ , showDiff: showDiff
+ }, (config.includeStack) ? this.assert : flag(this, 'ssfi'));
+ }
+ };
+
+ /*!
+ * ### ._obj
+ *
+ * Quick reference to stored `actual` value for plugin developers.
+ *
+ * @api private
+ */
+
+ Object.defineProperty(Assertion.prototype, '_obj',
+ { get: function () {
+ return flag(this, 'object');
+ }
+ , set: function (val) {
+ flag(this, 'object', val);
+ }
+ });
+};
+
+},{"./config":3}],3:[function(require,module,exports){
+module.exports = {
+
+ /**
+ * ### config.includeStack
+ *
+ * User configurable property, influences whether stack trace
+ * is included in Assertion error message. Default of false
+ * suppresses stack trace in the error message.
+ *
+ * chai.config.includeStack = true; // enable stack on error
+ *
+ * @param {Boolean}
+ * @api public
+ */
+
+ includeStack: false,
+
+ /**
+ * ### config.showDiff
+ *
+ * User configurable property, influences whether or not
+ * the `showDiff` flag should be included in the thrown
+ * AssertionErrors. `false` will always be `false`; `true`
+ * will be true when the assertion has requested a diff
+ * be shown.
+ *
+ * @param {Boolean}
+ * @api public
+ */
+
+ showDiff: true,
+
+ /**
+ * ### config.truncateThreshold
+ *
+ * User configurable property, sets length threshold for actual and
+ * expected values in assertion errors. If this threshold is exceeded, for
+ * example for large data structures, the value is replaced with something
+ * like `[ Array(3) ]` or `{ Object (prop1, prop2) }`.
+ *
+ * Set it to zero if you want to disable truncating altogether.
+ *
+ * This is especially userful when doing assertions on arrays: having this
+ * set to a reasonable large value makes the failure messages readily
+ * inspectable.
+ *
+ * chai.config.truncateThreshold = 0; // disable truncating
+ *
+ * @param {Number}
+ * @api public
+ */
+
+ truncateThreshold: 40
+
+};
+
+},{}],4:[function(require,module,exports){
+/*!
+ * chai
+ * http://chaijs.com
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, _) {
+ var Assertion = chai.Assertion
+ , toString = Object.prototype.toString
+ , flag = _.flag;
+
+ /**
+ * ### Language Chains
+ *
+ * The following are provided as chainable getters to
+ * improve the readability of your assertions. They
+ * do not provide testing capabilities unless they
+ * have been overwritten by a plugin.
+ *
+ * **Chains**
+ *
+ * - to
+ * - be
+ * - been
+ * - is
+ * - that
+ * - which
+ * - and
+ * - has
+ * - have
+ * - with
+ * - at
+ * - of
+ * - same
+ *
+ * @name language chains
+ * @api public
+ */
+
+ [ 'to', 'be', 'been'
+ , 'is', 'and', 'has', 'have'
+ , 'with', 'that', 'which', 'at'
+ , 'of', 'same' ].forEach(function (chain) {
+ Assertion.addProperty(chain, function () {
+ return this;
+ });
+ });
+
+ /**
+ * ### .not
+ *
+ * Negates any of assertions following in the chain.
+ *
+ * expect(foo).to.not.equal('bar');
+ * expect(goodFn).to.not.throw(Error);
+ * expect({ foo: 'baz' }).to.have.property('foo')
+ * .and.not.equal('bar');
+ *
+ * @name not
+ * @api public
+ */
+
+ Assertion.addProperty('not', function () {
+ flag(this, 'negate', true);
+ });
+
+ /**
+ * ### .deep
+ *
+ * Sets the `deep` flag, later used by the `equal` and
+ * `property` assertions.
+ *
+ * expect(foo).to.deep.equal({ bar: 'baz' });
+ * expect({ foo: { bar: { baz: 'quux' } } })
+ * .to.have.deep.property('foo.bar.baz', 'quux');
+ *
+ * `.deep.property` special characters can be escaped
+ * by adding two slashes before the `.` or `[]`.
+ *
+ * var deepCss = { '.link': { '[target]': 42 }};
+ * expect(deepCss).to.have.deep.property('\\.link.\\[target\\]', 42);
+ *
+ * @name deep
+ * @api public
+ */
+
+ Assertion.addProperty('deep', function () {
+ flag(this, 'deep', true);
+ });
+
+ /**
+ * ### .any
+ *
+ * Sets the `any` flag, (opposite of the `all` flag)
+ * later used in the `keys` assertion.
+ *
+ * expect(foo).to.have.any.keys('bar', 'baz');
+ *
+ * @name any
+ * @api public
+ */
+
+ Assertion.addProperty('any', function () {
+ flag(this, 'any', true);
+ flag(this, 'all', false)
+ });
+
+
+ /**
+ * ### .all
+ *
+ * Sets the `all` flag (opposite of the `any` flag)
+ * later used by the `keys` assertion.
+ *
+ * expect(foo).to.have.all.keys('bar', 'baz');
+ *
+ * @name all
+ * @api public
+ */
+
+ Assertion.addProperty('all', function () {
+ flag(this, 'all', true);
+ flag(this, 'any', false);
+ });
+
+ /**
+ * ### .a(type)
+ *
+ * The `a` and `an` assertions are aliases that can be
+ * used either as language chains or to assert a value's
+ * type.
+ *
+ * // typeof
+ * expect('test').to.be.a('string');
+ * expect({ foo: 'bar' }).to.be.an('object');
+ * expect(null).to.be.a('null');
+ * expect(undefined).to.be.an('undefined');
+ * expect(new Promise).to.be.a('promise');
+ * expect(new Float32Array()).to.be.a('float32array');
+ * expect(Symbol()).to.be.a('symbol');
+ *
+ * // es6 overrides
+ * expect({[Symbol.toStringTag]:()=>'foo'}).to.be.a('foo');
+ *
+ * // language chain
+ * expect(foo).to.be.an.instanceof(Foo);
+ *
+ * @name a
+ * @alias an
+ * @param {String} type
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function an (type, msg) {
+ if (msg) flag(this, 'message', msg);
+ type = type.toLowerCase();
+ var obj = flag(this, 'object')
+ , article = ~[ 'a', 'e', 'i', 'o', 'u' ].indexOf(type.charAt(0)) ? 'an ' : 'a ';
+
+ this.assert(
+ type === _.type(obj)
+ , 'expected #{this} to be ' + article + type
+ , 'expected #{this} not to be ' + article + type
+ );
+ }
+
+ Assertion.addChainableMethod('an', an);
+ Assertion.addChainableMethod('a', an);
+
+ /**
+ * ### .include(value)
+ *
+ * The `include` and `contain` assertions can be used as either property
+ * based language chains or as methods to assert the inclusion of an object
+ * in an array or a substring in a string. When used as language chains,
+ * they toggle the `contains` flag for the `keys` assertion.
+ *
+ * expect([1,2,3]).to.include(2);
+ * expect('foobar').to.contain('foo');
+ * expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');
+ *
+ * @name include
+ * @alias contain
+ * @alias includes
+ * @alias contains
+ * @param {Object|String|Number} obj
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function includeChainingBehavior () {
+ flag(this, 'contains', true);
+ }
+
+ function include (val, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ var expected = false;
+ if (_.type(obj) === 'array' && _.type(val) === 'object') {
+ for (var i in obj) {
+ if (_.eql(obj[i], val)) {
+ expected = true;
+ break;
+ }
+ }
+ } else if (_.type(val) === 'object') {
+ if (!flag(this, 'negate')) {
+ for (var k in val) new Assertion(obj).property(k, val[k]);
+ return;
+ }
+ var subset = {};
+ for (k in val) subset[k] = obj[k];
+ expected = _.eql(subset, val);
+ } else {
+ expected = obj && ~obj.indexOf(val);
+ }
+ this.assert(
+ expected
+ , 'expected #{this} to include ' + _.inspect(val)
+ , 'expected #{this} to not include ' + _.inspect(val));
+ }
+
+ Assertion.addChainableMethod('include', include, includeChainingBehavior);
+ Assertion.addChainableMethod('contain', include, includeChainingBehavior);
+ Assertion.addChainableMethod('contains', include, includeChainingBehavior);
+ Assertion.addChainableMethod('includes', include, includeChainingBehavior);
+
+ /**
+ * ### .ok
+ *
+ * Asserts that the target is truthy.
+ *
+ * expect('everthing').to.be.ok;
+ * expect(1).to.be.ok;
+ * expect(false).to.not.be.ok;
+ * expect(undefined).to.not.be.ok;
+ * expect(null).to.not.be.ok;
+ *
+ * @name ok
+ * @api public
+ */
+
+ Assertion.addProperty('ok', function () {
+ this.assert(
+ flag(this, 'object')
+ , 'expected #{this} to be truthy'
+ , 'expected #{this} to be falsy');
+ });
+
+ /**
+ * ### .true
+ *
+ * Asserts that the target is `true`.
+ *
+ * expect(true).to.be.true;
+ * expect(1).to.not.be.true;
+ *
+ * @name true
+ * @api public
+ */
+
+ Assertion.addProperty('true', function () {
+ this.assert(
+ true === flag(this, 'object')
+ , 'expected #{this} to be true'
+ , 'expected #{this} to be false'
+ , this.negate ? false : true
+ );
+ });
+
+ /**
+ * ### .false
+ *
+ * Asserts that the target is `false`.
+ *
+ * expect(false).to.be.false;
+ * expect(0).to.not.be.false;
+ *
+ * @name false
+ * @api public
+ */
+
+ Assertion.addProperty('false', function () {
+ this.assert(
+ false === flag(this, 'object')
+ , 'expected #{this} to be false'
+ , 'expected #{this} to be true'
+ , this.negate ? true : false
+ );
+ });
+
+ /**
+ * ### .null
+ *
+ * Asserts that the target is `null`.
+ *
+ * expect(null).to.be.null;
+ * expect(undefined).to.not.be.null;
+ *
+ * @name null
+ * @api public
+ */
+
+ Assertion.addProperty('null', function () {
+ this.assert(
+ null === flag(this, 'object')
+ , 'expected #{this} to be null'
+ , 'expected #{this} not to be null'
+ );
+ });
+
+ /**
+ * ### .undefined
+ *
+ * Asserts that the target is `undefined`.
+ *
+ * expect(undefined).to.be.undefined;
+ * expect(null).to.not.be.undefined;
+ *
+ * @name undefined
+ * @api public
+ */
+
+ Assertion.addProperty('undefined', function () {
+ this.assert(
+ undefined === flag(this, 'object')
+ , 'expected #{this} to be undefined'
+ , 'expected #{this} not to be undefined'
+ );
+ });
+
+ /**
+ * ### .exist
+ *
+ * Asserts that the target is neither `null` nor `undefined`.
+ *
+ * var foo = 'hi'
+ * , bar = null
+ * , baz;
+ *
+ * expect(foo).to.exist;
+ * expect(bar).to.not.exist;
+ * expect(baz).to.not.exist;
+ *
+ * @name exist
+ * @api public
+ */
+
+ Assertion.addProperty('exist', function () {
+ this.assert(
+ null != flag(this, 'object')
+ , 'expected #{this} to exist'
+ , 'expected #{this} to not exist'
+ );
+ });
+
+
+ /**
+ * ### .empty
+ *
+ * Asserts that the target's length is `0`. For arrays and strings, it checks
+ * the `length` property. For objects, it gets the count of
+ * enumerable keys.
+ *
+ * expect([]).to.be.empty;
+ * expect('').to.be.empty;
+ * expect({}).to.be.empty;
+ *
+ * @name empty
+ * @api public
+ */
+
+ Assertion.addProperty('empty', function () {
+ var obj = flag(this, 'object')
+ , expected = obj;
+
+ if (Array.isArray(obj) || 'string' === typeof object) {
+ expected = obj.length;
+ } else if (typeof obj === 'object') {
+ expected = Object.keys(obj).length;
+ }
+
+ this.assert(
+ !expected
+ , 'expected #{this} to be empty'
+ , 'expected #{this} not to be empty'
+ );
+ });
+
+ /**
+ * ### .arguments
+ *
+ * Asserts that the target is an arguments object.
+ *
+ * function test () {
+ * expect(arguments).to.be.arguments;
+ * }
+ *
+ * @name arguments
+ * @alias Arguments
+ * @api public
+ */
+
+ function checkArguments () {
+ var obj = flag(this, 'object')
+ , type = Object.prototype.toString.call(obj);
+ this.assert(
+ '[object Arguments]' === type
+ , 'expected #{this} to be arguments but got ' + type
+ , 'expected #{this} to not be arguments'
+ );
+ }
+
+ Assertion.addProperty('arguments', checkArguments);
+ Assertion.addProperty('Arguments', checkArguments);
+
+ /**
+ * ### .equal(value)
+ *
+ * Asserts that the target is strictly equal (`===`) to `value`.
+ * Alternately, if the `deep` flag is set, asserts that
+ * the target is deeply equal to `value`.
+ *
+ * expect('hello').to.equal('hello');
+ * expect(42).to.equal(42);
+ * expect(1).to.not.equal(true);
+ * expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' });
+ * expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' });
+ *
+ * @name equal
+ * @alias equals
+ * @alias eq
+ * @alias deep.equal
+ * @param {Mixed} value
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertEqual (val, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ if (flag(this, 'deep')) {
+ return this.eql(val);
+ } else {
+ this.assert(
+ val === obj
+ , 'expected #{this} to equal #{exp}'
+ , 'expected #{this} to not equal #{exp}'
+ , val
+ , this._obj
+ , true
+ );
+ }
+ }
+
+ Assertion.addMethod('equal', assertEqual);
+ Assertion.addMethod('equals', assertEqual);
+ Assertion.addMethod('eq', assertEqual);
+
+ /**
+ * ### .eql(value)
+ *
+ * Asserts that the target is deeply equal to `value`.
+ *
+ * expect({ foo: 'bar' }).to.eql({ foo: 'bar' });
+ * expect([ 1, 2, 3 ]).to.eql([ 1, 2, 3 ]);
+ *
+ * @name eql
+ * @alias eqls
+ * @param {Mixed} value
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertEql(obj, msg) {
+ if (msg) flag(this, 'message', msg);
+ this.assert(
+ _.eql(obj, flag(this, 'object'))
+ , 'expected #{this} to deeply equal #{exp}'
+ , 'expected #{this} to not deeply equal #{exp}'
+ , obj
+ , this._obj
+ , true
+ );
+ }
+
+ Assertion.addMethod('eql', assertEql);
+ Assertion.addMethod('eqls', assertEql);
+
+ /**
+ * ### .above(value)
+ *
+ * Asserts that the target is greater than `value`.
+ *
+ * expect(10).to.be.above(5);
+ *
+ * Can also be used in conjunction with `length` to
+ * assert a minimum length. The benefit being a
+ * more informative error message than if the length
+ * was supplied directly.
+ *
+ * expect('foo').to.have.length.above(2);
+ * expect([ 1, 2, 3 ]).to.have.length.above(2);
+ *
+ * @name above
+ * @alias gt
+ * @alias greaterThan
+ * @param {Number} value
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertAbove (n, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ if (flag(this, 'doLength')) {
+ new Assertion(obj, msg).to.have.property('length');
+ var len = obj.length;
+ this.assert(
+ len > n
+ , 'expected #{this} to have a length above #{exp} but got #{act}'
+ , 'expected #{this} to not have a length above #{exp}'
+ , n
+ , len
+ );
+ } else {
+ this.assert(
+ obj > n
+ , 'expected #{this} to be above ' + n
+ , 'expected #{this} to be at most ' + n
+ );
+ }
+ }
+
+ Assertion.addMethod('above', assertAbove);
+ Assertion.addMethod('gt', assertAbove);
+ Assertion.addMethod('greaterThan', assertAbove);
+
+ /**
+ * ### .least(value)
+ *
+ * Asserts that the target is greater than or equal to `value`.
+ *
+ * expect(10).to.be.at.least(10);
+ *
+ * Can also be used in conjunction with `length` to
+ * assert a minimum length. The benefit being a
+ * more informative error message than if the length
+ * was supplied directly.
+ *
+ * expect('foo').to.have.length.of.at.least(2);
+ * expect([ 1, 2, 3 ]).to.have.length.of.at.least(3);
+ *
+ * @name least
+ * @alias gte
+ * @param {Number} value
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertLeast (n, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ if (flag(this, 'doLength')) {
+ new Assertion(obj, msg).to.have.property('length');
+ var len = obj.length;
+ this.assert(
+ len >= n
+ , 'expected #{this} to have a length at least #{exp} but got #{act}'
+ , 'expected #{this} to have a length below #{exp}'
+ , n
+ , len
+ );
+ } else {
+ this.assert(
+ obj >= n
+ , 'expected #{this} to be at least ' + n
+ , 'expected #{this} to be below ' + n
+ );
+ }
+ }
+
+ Assertion.addMethod('least', assertLeast);
+ Assertion.addMethod('gte', assertLeast);
+
+ /**
+ * ### .below(value)
+ *
+ * Asserts that the target is less than `value`.
+ *
+ * expect(5).to.be.below(10);
+ *
+ * Can also be used in conjunction with `length` to
+ * assert a maximum length. The benefit being a
+ * more informative error message than if the length
+ * was supplied directly.
+ *
+ * expect('foo').to.have.length.below(4);
+ * expect([ 1, 2, 3 ]).to.have.length.below(4);
+ *
+ * @name below
+ * @alias lt
+ * @alias lessThan
+ * @param {Number} value
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertBelow (n, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ if (flag(this, 'doLength')) {
+ new Assertion(obj, msg).to.have.property('length');
+ var len = obj.length;
+ this.assert(
+ len < n
+ , 'expected #{this} to have a length below #{exp} but got #{act}'
+ , 'expected #{this} to not have a length below #{exp}'
+ , n
+ , len
+ );
+ } else {
+ this.assert(
+ obj < n
+ , 'expected #{this} to be below ' + n
+ , 'expected #{this} to be at least ' + n
+ );
+ }
+ }
+
+ Assertion.addMethod('below', assertBelow);
+ Assertion.addMethod('lt', assertBelow);
+ Assertion.addMethod('lessThan', assertBelow);
+
+ /**
+ * ### .most(value)
+ *
+ * Asserts that the target is less than or equal to `value`.
+ *
+ * expect(5).to.be.at.most(5);
+ *
+ * Can also be used in conjunction with `length` to
+ * assert a maximum length. The benefit being a
+ * more informative error message than if the length
+ * was supplied directly.
+ *
+ * expect('foo').to.have.length.of.at.most(4);
+ * expect([ 1, 2, 3 ]).to.have.length.of.at.most(3);
+ *
+ * @name most
+ * @alias lte
+ * @param {Number} value
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertMost (n, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ if (flag(this, 'doLength')) {
+ new Assertion(obj, msg).to.have.property('length');
+ var len = obj.length;
+ this.assert(
+ len <= n
+ , 'expected #{this} to have a length at most #{exp} but got #{act}'
+ , 'expected #{this} to have a length above #{exp}'
+ , n
+ , len
+ );
+ } else {
+ this.assert(
+ obj <= n
+ , 'expected #{this} to be at most ' + n
+ , 'expected #{this} to be above ' + n
+ );
+ }
+ }
+
+ Assertion.addMethod('most', assertMost);
+ Assertion.addMethod('lte', assertMost);
+
+ /**
+ * ### .within(start, finish)
+ *
+ * Asserts that the target is within a range.
+ *
+ * expect(7).to.be.within(5,10);
+ *
+ * Can also be used in conjunction with `length` to
+ * assert a length range. The benefit being a
+ * more informative error message than if the length
+ * was supplied directly.
+ *
+ * expect('foo').to.have.length.within(2,4);
+ * expect([ 1, 2, 3 ]).to.have.length.within(2,4);
+ *
+ * @name within
+ * @param {Number} start lowerbound inclusive
+ * @param {Number} finish upperbound inclusive
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ Assertion.addMethod('within', function (start, finish, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object')
+ , range = start + '..' + finish;
+ if (flag(this, 'doLength')) {
+ new Assertion(obj, msg).to.have.property('length');
+ var len = obj.length;
+ this.assert(
+ len >= start && len <= finish
+ , 'expected #{this} to have a length within ' + range
+ , 'expected #{this} to not have a length within ' + range
+ );
+ } else {
+ this.assert(
+ obj >= start && obj <= finish
+ , 'expected #{this} to be within ' + range
+ , 'expected #{this} to not be within ' + range
+ );
+ }
+ });
+
+ /**
+ * ### .instanceof(constructor)
+ *
+ * Asserts that the target is an instance of `constructor`.
+ *
+ * var Tea = function (name) { this.name = name; }
+ * , Chai = new Tea('chai');
+ *
+ * expect(Chai).to.be.an.instanceof(Tea);
+ * expect([ 1, 2, 3 ]).to.be.instanceof(Array);
+ *
+ * @name instanceof
+ * @param {Constructor} constructor
+ * @param {String} message _optional_
+ * @alias instanceOf
+ * @api public
+ */
+
+ function assertInstanceOf (constructor, msg) {
+ if (msg) flag(this, 'message', msg);
+ var name = _.getName(constructor);
+ this.assert(
+ flag(this, 'object') instanceof constructor
+ , 'expected #{this} to be an instance of ' + name
+ , 'expected #{this} to not be an instance of ' + name
+ );
+ }
+
+ Assertion.addMethod('instanceof', assertInstanceOf);
+ Assertion.addMethod('instanceOf', assertInstanceOf);
+
+ /**
+ * ### .property(name, [value])
+ *
+ * Asserts that the target has a property `name`, optionally asserting that
+ * the value of that property is strictly equal to `value`.
+ * If the `deep` flag is set, you can use dot- and bracket-notation for deep
+ * references into objects and arrays.
+ *
+ * // simple referencing
+ * var obj = { foo: 'bar' };
+ * expect(obj).to.have.property('foo');
+ * expect(obj).to.have.property('foo', 'bar');
+ *
+ * // deep referencing
+ * var deepObj = {
+ * green: { tea: 'matcha' }
+ * , teas: [ 'chai', 'matcha', { tea: 'konacha' } ]
+ * };
+ *
+ * expect(deepObj).to.have.deep.property('green.tea', 'matcha');
+ * expect(deepObj).to.have.deep.property('teas[1]', 'matcha');
+ * expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha');
+ *
+ * You can also use an array as the starting point of a `deep.property`
+ * assertion, or traverse nested arrays.
+ *
+ * var arr = [
+ * [ 'chai', 'matcha', 'konacha' ]
+ * , [ { tea: 'chai' }
+ * , { tea: 'matcha' }
+ * , { tea: 'konacha' } ]
+ * ];
+ *
+ * expect(arr).to.have.deep.property('[0][1]', 'matcha');
+ * expect(arr).to.have.deep.property('[1][2].tea', 'konacha');
+ *
+ * Furthermore, `property` changes the subject of the assertion
+ * to be the value of that property from the original object. This
+ * permits for further chainable assertions on that property.
+ *
+ * expect(obj).to.have.property('foo')
+ * .that.is.a('string');
+ * expect(deepObj).to.have.property('green')
+ * .that.is.an('object')
+ * .that.deep.equals({ tea: 'matcha' });
+ * expect(deepObj).to.have.property('teas')
+ * .that.is.an('array')
+ * .with.deep.property('[2]')
+ * .that.deep.equals({ tea: 'konacha' });
+ *
+ * Note that dots and bracket in `name` must be backslash-escaped when
+ * the `deep` flag is set, while they must NOT be escaped when the `deep`
+ * flag is not set.
+ *
+ * // simple referencing
+ * var css = { '.link[target]': 42 };
+ * expect(css).to.have.property('.link[target]', 42);
+ *
+ * // deep referencing
+ * var deepCss = { '.link': { '[target]': 42 }};
+ * expect(deepCss).to.have.deep.property('\\.link.\\[target\\]', 42);
+ *
+ * @name property
+ * @alias deep.property
+ * @param {String} name
+ * @param {Mixed} value (optional)
+ * @param {String} message _optional_
+ * @returns value of property for chaining
+ * @api public
+ */
+
+ Assertion.addMethod('property', function (name, val, msg) {
+ if (msg) flag(this, 'message', msg);
+
+ var isDeep = !!flag(this, 'deep')
+ , descriptor = isDeep ? 'deep property ' : 'property '
+ , negate = flag(this, 'negate')
+ , obj = flag(this, 'object')
+ , pathInfo = isDeep ? _.getPathInfo(name, obj) : null
+ , hasProperty = isDeep
+ ? pathInfo.exists
+ : _.hasProperty(name, obj)
+ , value = isDeep
+ ? pathInfo.value
+ : obj[name];
+
+ if (negate && arguments.length > 1) {
+ if (undefined === value) {
+ msg = (msg != null) ? msg + ': ' : '';
+ throw new Error(msg + _.inspect(obj) + ' has no ' + descriptor + _.inspect(name));
+ }
+ } else {
+ this.assert(
+ hasProperty
+ , 'expected #{this} to have a ' + descriptor + _.inspect(name)
+ , 'expected #{this} to not have ' + descriptor + _.inspect(name));
+ }
+
+ if (arguments.length > 1) {
+ this.assert(
+ val === value
+ , 'expected #{this} to have a ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
+ , 'expected #{this} to not have a ' + descriptor + _.inspect(name) + ' of #{act}'
+ , val
+ , value
+ );
+ }
+
+ flag(this, 'object', value);
+ });
+
+
+ /**
+ * ### .ownProperty(name)
+ *
+ * Asserts that the target has an own property `name`.
+ *
+ * expect('test').to.have.ownProperty('length');
+ *
+ * @name ownProperty
+ * @alias haveOwnProperty
+ * @param {String} name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertOwnProperty (name, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ this.assert(
+ obj.hasOwnProperty(name)
+ , 'expected #{this} to have own property ' + _.inspect(name)
+ , 'expected #{this} to not have own property ' + _.inspect(name)
+ );
+ }
+
+ Assertion.addMethod('ownProperty', assertOwnProperty);
+ Assertion.addMethod('haveOwnProperty', assertOwnProperty);
+
+ /**
+ * ### .ownPropertyDescriptor(name[, descriptor[, message]])
+ *
+ * Asserts that the target has an own property descriptor `name`, that optionally matches `descriptor`.
+ *
+ * expect('test').to.have.ownPropertyDescriptor('length');
+ * expect('test').to.have.ownPropertyDescriptor('length', { enumerable: false, configurable: false, writable: false, value: 4 });
+ * expect('test').not.to.have.ownPropertyDescriptor('length', { enumerable: false, configurable: false, writable: false, value: 3 });
+ * expect('test').ownPropertyDescriptor('length').to.have.property('enumerable', false);
+ * expect('test').ownPropertyDescriptor('length').to.have.keys('value');
+ *
+ * @name ownPropertyDescriptor
+ * @alias haveOwnPropertyDescriptor
+ * @param {String} name
+ * @param {Object} descriptor _optional_
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertOwnPropertyDescriptor (name, descriptor, msg) {
+ if (typeof descriptor === 'string') {
+ msg = descriptor;
+ descriptor = null;
+ }
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ var actualDescriptor = Object.getOwnPropertyDescriptor(Object(obj), name);
+ if (actualDescriptor && descriptor) {
+ this.assert(
+ _.eql(descriptor, actualDescriptor)
+ , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to match ' + _.inspect(descriptor) + ', got ' + _.inspect(actualDescriptor)
+ , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to not match ' + _.inspect(descriptor)
+ , descriptor
+ , actualDescriptor
+ , true
+ );
+ } else {
+ this.assert(
+ actualDescriptor
+ , 'expected #{this} to have an own property descriptor for ' + _.inspect(name)
+ , 'expected #{this} to not have an own property descriptor for ' + _.inspect(name)
+ );
+ }
+ flag(this, 'object', actualDescriptor);
+ }
+
+ Assertion.addMethod('ownPropertyDescriptor', assertOwnPropertyDescriptor);
+ Assertion.addMethod('haveOwnPropertyDescriptor', assertOwnPropertyDescriptor);
+
+ /**
+ * ### .length
+ *
+ * Sets the `doLength` flag later used as a chain precursor to a value
+ * comparison for the `length` property.
+ *
+ * expect('foo').to.have.length.above(2);
+ * expect([ 1, 2, 3 ]).to.have.length.above(2);
+ * expect('foo').to.have.length.below(4);
+ * expect([ 1, 2, 3 ]).to.have.length.below(4);
+ * expect('foo').to.have.length.within(2,4);
+ * expect([ 1, 2, 3 ]).to.have.length.within(2,4);
+ *
+ * *Deprecation notice:* Using `length` as an assertion will be deprecated
+ * in version 2.4.0 and removed in 3.0.0. Code using the old style of
+ * asserting for `length` property value using `length(value)` should be
+ * switched to use `lengthOf(value)` instead.
+ *
+ * @name length
+ * @api public
+ */
+
+ /**
+ * ### .lengthOf(value[, message])
+ *
+ * Asserts that the target's `length` property has
+ * the expected value.
+ *
+ * expect([ 1, 2, 3]).to.have.lengthOf(3);
+ * expect('foobar').to.have.lengthOf(6);
+ *
+ * @name lengthOf
+ * @param {Number} length
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertLengthChain () {
+ flag(this, 'doLength', true);
+ }
+
+ function assertLength (n, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ new Assertion(obj, msg).to.have.property('length');
+ var len = obj.length;
+
+ this.assert(
+ len == n
+ , 'expected #{this} to have a length of #{exp} but got #{act}'
+ , 'expected #{this} to not have a length of #{act}'
+ , n
+ , len
+ );
+ }
+
+ Assertion.addChainableMethod('length', assertLength, assertLengthChain);
+ Assertion.addMethod('lengthOf', assertLength);
+
+ /**
+ * ### .match(regexp)
+ *
+ * Asserts that the target matches a regular expression.
+ *
+ * expect('foobar').to.match(/^foo/);
+ *
+ * @name match
+ * @alias matches
+ * @param {RegExp} RegularExpression
+ * @param {String} message _optional_
+ * @api public
+ */
+ function assertMatch(re, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ this.assert(
+ re.exec(obj)
+ , 'expected #{this} to match ' + re
+ , 'expected #{this} not to match ' + re
+ );
+ }
+
+ Assertion.addMethod('match', assertMatch);
+ Assertion.addMethod('matches', assertMatch);
+
+ /**
+ * ### .string(string)
+ *
+ * Asserts that the string target contains another string.
+ *
+ * expect('foobar').to.have.string('bar');
+ *
+ * @name string
+ * @param {String} string
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ Assertion.addMethod('string', function (str, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ new Assertion(obj, msg).is.a('string');
+
+ this.assert(
+ ~obj.indexOf(str)
+ , 'expected #{this} to contain ' + _.inspect(str)
+ , 'expected #{this} to not contain ' + _.inspect(str)
+ );
+ });
+
+
+ /**
+ * ### .keys(key1, [key2], [...])
+ *
+ * Asserts that the target contains any or all of the passed-in keys.
+ * Use in combination with `any`, `all`, `contains`, or `have` will affect
+ * what will pass.
+ *
+ * When used in conjunction with `any`, at least one key that is passed
+ * in must exist in the target object. This is regardless whether or not
+ * the `have` or `contain` qualifiers are used. Note, either `any` or `all`
+ * should be used in the assertion. If neither are used, the assertion is
+ * defaulted to `all`.
+ *
+ * When both `all` and `contain` are used, the target object must have at
+ * least all of the passed-in keys but may have more keys not listed.
+ *
+ * When both `all` and `have` are used, the target object must both contain
+ * all of the passed-in keys AND the number of keys in the target object must
+ * match the number of keys passed in (in other words, a target object must
+ * have all and only all of the passed-in keys).
+ *
+ * expect({ foo: 1, bar: 2 }).to.have.any.keys('foo', 'baz');
+ * expect({ foo: 1, bar: 2 }).to.have.any.keys('foo');
+ * expect({ foo: 1, bar: 2 }).to.contain.any.keys('bar', 'baz');
+ * expect({ foo: 1, bar: 2 }).to.contain.any.keys(['foo']);
+ * expect({ foo: 1, bar: 2 }).to.contain.any.keys({'foo': 6});
+ * expect({ foo: 1, bar: 2 }).to.have.all.keys(['bar', 'foo']);
+ * expect({ foo: 1, bar: 2 }).to.have.all.keys({'bar': 6, 'foo': 7});
+ * expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']);
+ * expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys({'bar': 6});
+ *
+ *
+ * @name keys
+ * @alias key
+ * @param {String...|Array|Object} keys
+ * @api public
+ */
+
+ function assertKeys (keys) {
+ var obj = flag(this, 'object')
+ , str
+ , ok = true
+ , mixedArgsMsg = 'keys must be given single argument of Array|Object|String, or multiple String arguments';
+
+ switch (_.type(keys)) {
+ case "array":
+ if (arguments.length > 1) throw (new Error(mixedArgsMsg));
+ break;
+ case "object":
+ if (arguments.length > 1) throw (new Error(mixedArgsMsg));
+ keys = Object.keys(keys);
+ break;
+ default:
+ keys = Array.prototype.slice.call(arguments);
+ }
+
+ if (!keys.length) throw new Error('keys required');
+
+ var actual = Object.keys(obj)
+ , expected = keys
+ , len = keys.length
+ , any = flag(this, 'any')
+ , all = flag(this, 'all');
+
+ if (!any && !all) {
+ all = true;
+ }
+
+ // Has any
+ if (any) {
+ var intersection = expected.filter(function(key) {
+ return ~actual.indexOf(key);
+ });
+ ok = intersection.length > 0;
+ }
+
+ // Has all
+ if (all) {
+ ok = keys.every(function(key){
+ return ~actual.indexOf(key);
+ });
+ if (!flag(this, 'negate') && !flag(this, 'contains')) {
+ ok = ok && keys.length == actual.length;
+ }
+ }
+
+ // Key string
+ if (len > 1) {
+ keys = keys.map(function(key){
+ return _.inspect(key);
+ });
+ var last = keys.pop();
+ if (all) {
+ str = keys.join(', ') + ', and ' + last;
+ }
+ if (any) {
+ str = keys.join(', ') + ', or ' + last;
+ }
+ } else {
+ str = _.inspect(keys[0]);
+ }
+
+ // Form
+ str = (len > 1 ? 'keys ' : 'key ') + str;
+
+ // Have / include
+ str = (flag(this, 'contains') ? 'contain ' : 'have ') + str;
+
+ // Assertion
+ this.assert(
+ ok
+ , 'expected #{this} to ' + str
+ , 'expected #{this} to not ' + str
+ , expected.slice(0).sort()
+ , actual.sort()
+ , true
+ );
+ }
+
+ Assertion.addMethod('keys', assertKeys);
+ Assertion.addMethod('key', assertKeys);
+
+ /**
+ * ### .throw(constructor)
+ *
+ * Asserts that the function target will throw a specific error, or specific type of error
+ * (as determined using `instanceof`), optionally with a RegExp or string inclusion test
+ * for the error's message.
+ *
+ * var err = new ReferenceError('This is a bad function.');
+ * var fn = function () { throw err; }
+ * expect(fn).to.throw(ReferenceError);
+ * expect(fn).to.throw(Error);
+ * expect(fn).to.throw(/bad function/);
+ * expect(fn).to.not.throw('good function');
+ * expect(fn).to.throw(ReferenceError, /bad function/);
+ * expect(fn).to.throw(err);
+ * expect(fn).to.not.throw(new RangeError('Out of range.'));
+ *
+ * Please note that when a throw expectation is negated, it will check each
+ * parameter independently, starting with error constructor type. The appropriate way
+ * to check for the existence of a type of error but for a message that does not match
+ * is to use `and`.
+ *
+ * expect(fn).to.throw(ReferenceError)
+ * .and.not.throw(/good function/);
+ *
+ * @name throw
+ * @alias throws
+ * @alias Throw
+ * @param {ErrorConstructor} constructor
+ * @param {String|RegExp} expected error message
+ * @param {String} message _optional_
+ * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+ * @returns error for chaining (null if no error)
+ * @api public
+ */
+
+ function assertThrows (constructor, errMsg, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ new Assertion(obj, msg).is.a('function');
+
+ var thrown = false
+ , desiredError = null
+ , name = null
+ , thrownError = null;
+
+ if (arguments.length === 0) {
+ errMsg = null;
+ constructor = null;
+ } else if (constructor && (constructor instanceof RegExp || 'string' === typeof constructor)) {
+ errMsg = constructor;
+ constructor = null;
+ } else if (constructor && constructor instanceof Error) {
+ desiredError = constructor;
+ constructor = null;
+ errMsg = null;
+ } else if (typeof constructor === 'function') {
+ name = constructor.prototype.name || constructor.name;
+ if (name === 'Error' && constructor !== Error) {
+ name = (new constructor()).name;
+ }
+ } else {
+ constructor = null;
+ }
+
+ try {
+ obj();
+ } catch (err) {
+ // first, check desired error
+ if (desiredError) {
+ this.assert(
+ err === desiredError
+ , 'expected #{this} to throw #{exp} but #{act} was thrown'
+ , 'expected #{this} to not throw #{exp}'
+ , (desiredError instanceof Error ? desiredError.toString() : desiredError)
+ , (err instanceof Error ? err.toString() : err)
+ );
+
+ flag(this, 'object', err);
+ return this;
+ }
+
+ // next, check constructor
+ if (constructor) {
+ this.assert(
+ err instanceof constructor
+ , 'expected #{this} to throw #{exp} but #{act} was thrown'
+ , 'expected #{this} to not throw #{exp} but #{act} was thrown'
+ , name
+ , (err instanceof Error ? err.toString() : err)
+ );
+
+ if (!errMsg) {
+ flag(this, 'object', err);
+ return this;
+ }
+ }
+
+ // next, check message
+ var message = 'error' === _.type(err) && "message" in err
+ ? err.message
+ : '' + err;
+
+ if ((message != null) && errMsg && errMsg instanceof RegExp) {
+ this.assert(
+ errMsg.exec(message)
+ , 'expected #{this} to throw error matching #{exp} but got #{act}'
+ , 'expected #{this} to throw error not matching #{exp}'
+ , errMsg
+ , message
+ );
+
+ flag(this, 'object', err);
+ return this;
+ } else if ((message != null) && errMsg && 'string' === typeof errMsg) {
+ this.assert(
+ ~message.indexOf(errMsg)
+ , 'expected #{this} to throw error including #{exp} but got #{act}'
+ , 'expected #{this} to throw error not including #{act}'
+ , errMsg
+ , message
+ );
+
+ flag(this, 'object', err);
+ return this;
+ } else {
+ thrown = true;
+ thrownError = err;
+ }
+ }
+
+ var actuallyGot = '';
+ var expectedThrown = 'an error';
+ if (name !== null) {
+ expectedThrown = name;
+ } else if (desiredError) {
+ expectedThrown = '#{exp}'; //_.inspect(desiredError)
+ }
+
+ if (thrown) {
+ actuallyGot = ' but #{act} was thrown'
+ }
+
+ this.assert(
+ thrown === true
+ , 'expected #{this} to throw ' + expectedThrown + actuallyGot
+ , 'expected #{this} to not throw ' + expectedThrown + actuallyGot
+ , (desiredError instanceof Error ? desiredError.toString() : desiredError)
+ , (thrownError instanceof Error ? thrownError.toString() : thrownError)
+ );
+
+ flag(this, 'object', thrownError);
+ }
+
+ Assertion.addMethod('throw', assertThrows);
+ Assertion.addMethod('throws', assertThrows);
+ Assertion.addMethod('Throw', assertThrows);
+
+ /**
+ * ### .respondTo(method)
+ *
+ * Asserts that the object or class target will respond to a method.
+ *
+ * Klass.prototype.bar = function(){};
+ * expect(Klass).to.respondTo('bar');
+ * expect(obj).to.respondTo('bar');
+ *
+ * To check if a constructor will respond to a static function,
+ * set the `itself` flag.
+ *
+ * Klass.baz = function(){};
+ * expect(Klass).itself.to.respondTo('baz');
+ *
+ * @name respondTo
+ * @param {String} method
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ Assertion.addMethod('respondTo', function (method, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object')
+ , itself = flag(this, 'itself')
+ , context = ('function' === _.type(obj) && !itself)
+ ? obj.prototype[method]
+ : obj[method];
+
+ this.assert(
+ 'function' === typeof context
+ , 'expected #{this} to respond to ' + _.inspect(method)
+ , 'expected #{this} to not respond to ' + _.inspect(method)
+ );
+ });
+
+ /**
+ * ### .itself
+ *
+ * Sets the `itself` flag, later used by the `respondTo` assertion.
+ *
+ * function Foo() {}
+ * Foo.bar = function() {}
+ * Foo.prototype.baz = function() {}
+ *
+ * expect(Foo).itself.to.respondTo('bar');
+ * expect(Foo).itself.not.to.respondTo('baz');
+ *
+ * @name itself
+ * @api public
+ */
+
+ Assertion.addProperty('itself', function () {
+ flag(this, 'itself', true);
+ });
+
+ /**
+ * ### .satisfy(method)
+ *
+ * Asserts that the target passes a given truth test.
+ *
+ * expect(1).to.satisfy(function(num) { return num > 0; });
+ *
+ * @name satisfy
+ * @param {Function} matcher
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ Assertion.addMethod('satisfy', function (matcher, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+ var result = matcher(obj);
+ this.assert(
+ result
+ , 'expected #{this} to satisfy ' + _.objDisplay(matcher)
+ , 'expected #{this} to not satisfy' + _.objDisplay(matcher)
+ , this.negate ? false : true
+ , result
+ );
+ });
+
+ /**
+ * ### .closeTo(expected, delta)
+ *
+ * Asserts that the target is equal `expected`, to within a +/- `delta` range.
+ *
+ * expect(1.5).to.be.closeTo(1, 0.5);
+ *
+ * @name closeTo
+ * @param {Number} expected
+ * @param {Number} delta
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ Assertion.addMethod('closeTo', function (expected, delta, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+
+ new Assertion(obj, msg).is.a('number');
+ if (_.type(expected) !== 'number' || _.type(delta) !== 'number') {
+ throw new Error('the arguments to closeTo must be numbers');
+ }
+
+ this.assert(
+ Math.abs(obj - expected) <= delta
+ , 'expected #{this} to be close to ' + expected + ' +/- ' + delta
+ , 'expected #{this} not to be close to ' + expected + ' +/- ' + delta
+ );
+ });
+
+ function isSubsetOf(subset, superset, cmp) {
+ return subset.every(function(elem) {
+ if (!cmp) return superset.indexOf(elem) !== -1;
+
+ return superset.some(function(elem2) {
+ return cmp(elem, elem2);
+ });
+ })
+ }
+
+ /**
+ * ### .members(set)
+ *
+ * Asserts that the target is a superset of `set`,
+ * or that the target and `set` have the same strictly-equal (===) members.
+ * Alternately, if the `deep` flag is set, set members are compared for deep
+ * equality.
+ *
+ * expect([1, 2, 3]).to.include.members([3, 2]);
+ * expect([1, 2, 3]).to.not.include.members([3, 2, 8]);
+ *
+ * expect([4, 2]).to.have.members([2, 4]);
+ * expect([5, 2]).to.not.have.members([5, 2, 1]);
+ *
+ * expect([{ id: 1 }]).to.deep.include.members([{ id: 1 }]);
+ *
+ * @name members
+ * @param {Array} set
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ Assertion.addMethod('members', function (subset, msg) {
+ if (msg) flag(this, 'message', msg);
+ var obj = flag(this, 'object');
+
+ new Assertion(obj).to.be.an('array');
+ new Assertion(subset).to.be.an('array');
+
+ var cmp = flag(this, 'deep') ? _.eql : undefined;
+
+ if (flag(this, 'contains')) {
+ return this.assert(
+ isSubsetOf(subset, obj, cmp)
+ , 'expected #{this} to be a superset of #{act}'
+ , 'expected #{this} to not be a superset of #{act}'
+ , obj
+ , subset
+ );
+ }
+
+ this.assert(
+ isSubsetOf(obj, subset, cmp) && isSubsetOf(subset, obj, cmp)
+ , 'expected #{this} to have the same members as #{act}'
+ , 'expected #{this} to not have the same members as #{act}'
+ , obj
+ , subset
+ );
+ });
+
+ /**
+ * ### .change(function)
+ *
+ * Asserts that a function changes an object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val += 3 };
+ * var noChangeFn = function() { return 'foo' + 'bar'; }
+ * expect(fn).to.change(obj, 'val');
+ * expect(noChangFn).to.not.change(obj, 'val')
+ *
+ * @name change
+ * @alias changes
+ * @alias Change
+ * @param {String} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertChanges (object, prop, msg) {
+ if (msg) flag(this, 'message', msg);
+ var fn = flag(this, 'object');
+ new Assertion(object, msg).to.have.property(prop);
+ new Assertion(fn).is.a('function');
+
+ var initial = object[prop];
+ fn();
+
+ this.assert(
+ initial !== object[prop]
+ , 'expected .' + prop + ' to change'
+ , 'expected .' + prop + ' to not change'
+ );
+ }
+
+ Assertion.addChainableMethod('change', assertChanges);
+ Assertion.addChainableMethod('changes', assertChanges);
+
+ /**
+ * ### .increase(function)
+ *
+ * Asserts that a function increases an object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 15 };
+ * expect(fn).to.increase(obj, 'val');
+ *
+ * @name increase
+ * @alias increases
+ * @alias Increase
+ * @param {String} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertIncreases (object, prop, msg) {
+ if (msg) flag(this, 'message', msg);
+ var fn = flag(this, 'object');
+ new Assertion(object, msg).to.have.property(prop);
+ new Assertion(fn).is.a('function');
+
+ var initial = object[prop];
+ fn();
+
+ this.assert(
+ object[prop] - initial > 0
+ , 'expected .' + prop + ' to increase'
+ , 'expected .' + prop + ' to not increase'
+ );
+ }
+
+ Assertion.addChainableMethod('increase', assertIncreases);
+ Assertion.addChainableMethod('increases', assertIncreases);
+
+ /**
+ * ### .decrease(function)
+ *
+ * Asserts that a function decreases an object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 5 };
+ * expect(fn).to.decrease(obj, 'val');
+ *
+ * @name decrease
+ * @alias decreases
+ * @alias Decrease
+ * @param {String} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ function assertDecreases (object, prop, msg) {
+ if (msg) flag(this, 'message', msg);
+ var fn = flag(this, 'object');
+ new Assertion(object, msg).to.have.property(prop);
+ new Assertion(fn).is.a('function');
+
+ var initial = object[prop];
+ fn();
+
+ this.assert(
+ object[prop] - initial < 0
+ , 'expected .' + prop + ' to decrease'
+ , 'expected .' + prop + ' to not decrease'
+ );
+ }
+
+ Assertion.addChainableMethod('decrease', assertDecreases);
+ Assertion.addChainableMethod('decreases', assertDecreases);
+
+};
+
+},{}],5:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+
+module.exports = function (chai, util) {
+
+ /*!
+ * Chai dependencies.
+ */
+
+ var Assertion = chai.Assertion
+ , flag = util.flag;
+
+ /*!
+ * Module export.
+ */
+
+ /**
+ * ### assert(expression, message)
+ *
+ * Write your own test expressions.
+ *
+ * assert('foo' !== 'bar', 'foo is not bar');
+ * assert(Array.isArray([]), 'empty arrays are arrays');
+ *
+ * @param {Mixed} expression to test for truthiness
+ * @param {String} message to display on error
+ * @name assert
+ * @api public
+ */
+
+ var assert = chai.assert = function (express, errmsg) {
+ var test = new Assertion(null, null, chai.assert);
+ test.assert(
+ express
+ , errmsg
+ , '[ negation message unavailable ]'
+ );
+ };
+
+ /**
+ * ### .fail(actual, expected, [message], [operator])
+ *
+ * Throw a failure. Node.js `assert` module-compatible.
+ *
+ * @name fail
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @param {String} operator
+ * @api public
+ */
+
+ assert.fail = function (actual, expected, message, operator) {
+ message = message || 'assert.fail()';
+ throw new chai.AssertionError(message, {
+ actual: actual
+ , expected: expected
+ , operator: operator
+ }, assert.fail);
+ };
+
+ /**
+ * ### .ok(object, [message])
+ *
+ * Asserts that `object` is truthy.
+ *
+ * assert.ok('everything', 'everything is ok');
+ * assert.ok(false, 'this will fail');
+ *
+ * @name ok
+ * @param {Mixed} object to test
+ * @param {String} message
+ * @api public
+ */
+
+ assert.ok = function (val, msg) {
+ new Assertion(val, msg).is.ok;
+ };
+
+ /**
+ * ### .notOk(object, [message])
+ *
+ * Asserts that `object` is falsy.
+ *
+ * assert.notOk('everything', 'this will fail');
+ * assert.notOk(false, 'this will pass');
+ *
+ * @name notOk
+ * @param {Mixed} object to test
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notOk = function (val, msg) {
+ new Assertion(val, msg).is.not.ok;
+ };
+
+ /**
+ * ### .equal(actual, expected, [message])
+ *
+ * Asserts non-strict equality (`==`) of `actual` and `expected`.
+ *
+ * assert.equal(3, '3', '== coerces values to strings');
+ *
+ * @name equal
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @api public
+ */
+
+ assert.equal = function (act, exp, msg) {
+ var test = new Assertion(act, msg, assert.equal);
+
+ test.assert(
+ exp == flag(test, 'object')
+ , 'expected #{this} to equal #{exp}'
+ , 'expected #{this} to not equal #{act}'
+ , exp
+ , act
+ );
+ };
+
+ /**
+ * ### .notEqual(actual, expected, [message])
+ *
+ * Asserts non-strict inequality (`!=`) of `actual` and `expected`.
+ *
+ * assert.notEqual(3, 4, 'these numbers are not equal');
+ *
+ * @name notEqual
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notEqual = function (act, exp, msg) {
+ var test = new Assertion(act, msg, assert.notEqual);
+
+ test.assert(
+ exp != flag(test, 'object')
+ , 'expected #{this} to not equal #{exp}'
+ , 'expected #{this} to equal #{act}'
+ , exp
+ , act
+ );
+ };
+
+ /**
+ * ### .strictEqual(actual, expected, [message])
+ *
+ * Asserts strict equality (`===`) of `actual` and `expected`.
+ *
+ * assert.strictEqual(true, true, 'these booleans are strictly equal');
+ *
+ * @name strictEqual
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @api public
+ */
+
+ assert.strictEqual = function (act, exp, msg) {
+ new Assertion(act, msg).to.equal(exp);
+ };
+
+ /**
+ * ### .notStrictEqual(actual, expected, [message])
+ *
+ * Asserts strict inequality (`!==`) of `actual` and `expected`.
+ *
+ * assert.notStrictEqual(3, '3', 'no coercion for strict equality');
+ *
+ * @name notStrictEqual
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notStrictEqual = function (act, exp, msg) {
+ new Assertion(act, msg).to.not.equal(exp);
+ };
+
+ /**
+ * ### .deepEqual(actual, expected, [message])
+ *
+ * Asserts that `actual` is deeply equal to `expected`.
+ *
+ * assert.deepEqual({ tea: 'green' }, { tea: 'green' });
+ *
+ * @name deepEqual
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @api public
+ */
+
+ assert.deepEqual = function (act, exp, msg) {
+ new Assertion(act, msg).to.eql(exp);
+ };
+
+ /**
+ * ### .notDeepEqual(actual, expected, [message])
+ *
+ * Assert that `actual` is not deeply equal to `expected`.
+ *
+ * assert.notDeepEqual({ tea: 'green' }, { tea: 'jasmine' });
+ *
+ * @name notDeepEqual
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notDeepEqual = function (act, exp, msg) {
+ new Assertion(act, msg).to.not.eql(exp);
+ };
+
+ /**
+ * ### .isTrue(value, [message])
+ *
+ * Asserts that `value` is true.
+ *
+ * var teaServed = true;
+ * assert.isTrue(teaServed, 'the tea has been served');
+ *
+ * @name isTrue
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isAbove = function (val, abv, msg) {
+ new Assertion(val, msg).to.be.above(abv);
+ };
+
+ /**
+ * ### .isAbove(valueToCheck, valueToBeAbove, [message])
+ *
+ * Asserts `valueToCheck` is strictly greater than (>) `valueToBeAbove`
+ *
+ * assert.isAbove(5, 2, '5 is strictly greater than 2');
+ *
+ * @name isAbove
+ * @param {Mixed} valueToCheck
+ * @param {Mixed} valueToBeAbove
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isBelow = function (val, blw, msg) {
+ new Assertion(val, msg).to.be.below(blw);
+ };
+
+ /**
+ * ### .isBelow(valueToCheck, valueToBeBelow, [message])
+ *
+ * Asserts `valueToCheck` is strictly less than (<) `valueToBeBelow`
+ *
+ * assert.isBelow(3, 6, '3 is strictly less than 6');
+ *
+ * @name isBelow
+ * @param {Mixed} valueToCheck
+ * @param {Mixed} valueToBeBelow
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isTrue = function (val, msg) {
+ new Assertion(val, msg).is['true'];
+ };
+
+ /**
+ * ### .isFalse(value, [message])
+ *
+ * Asserts that `value` is false.
+ *
+ * var teaServed = false;
+ * assert.isFalse(teaServed, 'no tea yet? hmm...');
+ *
+ * @name isFalse
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isFalse = function (val, msg) {
+ new Assertion(val, msg).is['false'];
+ };
+
+ /**
+ * ### .isNull(value, [message])
+ *
+ * Asserts that `value` is null.
+ *
+ * assert.isNull(err, 'there was no error');
+ *
+ * @name isNull
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNull = function (val, msg) {
+ new Assertion(val, msg).to.equal(null);
+ };
+
+ /**
+ * ### .isNotNull(value, [message])
+ *
+ * Asserts that `value` is not null.
+ *
+ * var tea = 'tasty chai';
+ * assert.isNotNull(tea, 'great, time for tea!');
+ *
+ * @name isNotNull
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotNull = function (val, msg) {
+ new Assertion(val, msg).to.not.equal(null);
+ };
+
+ /**
+ * ### .isUndefined(value, [message])
+ *
+ * Asserts that `value` is `undefined`.
+ *
+ * var tea;
+ * assert.isUndefined(tea, 'no tea defined');
+ *
+ * @name isUndefined
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isUndefined = function (val, msg) {
+ new Assertion(val, msg).to.equal(undefined);
+ };
+
+ /**
+ * ### .isDefined(value, [message])
+ *
+ * Asserts that `value` is not `undefined`.
+ *
+ * var tea = 'cup of chai';
+ * assert.isDefined(tea, 'tea has been defined');
+ *
+ * @name isDefined
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isDefined = function (val, msg) {
+ new Assertion(val, msg).to.not.equal(undefined);
+ };
+
+ /**
+ * ### .isFunction(value, [message])
+ *
+ * Asserts that `value` is a function.
+ *
+ * function serveTea() { return 'cup of tea'; };
+ * assert.isFunction(serveTea, 'great, we can have tea now');
+ *
+ * @name isFunction
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isFunction = function (val, msg) {
+ new Assertion(val, msg).to.be.a('function');
+ };
+
+ /**
+ * ### .isNotFunction(value, [message])
+ *
+ * Asserts that `value` is _not_ a function.
+ *
+ * var serveTea = [ 'heat', 'pour', 'sip' ];
+ * assert.isNotFunction(serveTea, 'great, we have listed the steps');
+ *
+ * @name isNotFunction
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotFunction = function (val, msg) {
+ new Assertion(val, msg).to.not.be.a('function');
+ };
+
+ /**
+ * ### .isObject(value, [message])
+ *
+ * Asserts that `value` is an object (as revealed by
+ * `Object.prototype.toString`).
+ *
+ * var selection = { name: 'Chai', serve: 'with spices' };
+ * assert.isObject(selection, 'tea selection is an object');
+ *
+ * @name isObject
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isObject = function (val, msg) {
+ new Assertion(val, msg).to.be.a('object');
+ };
+
+ /**
+ * ### .isNotObject(value, [message])
+ *
+ * Asserts that `value` is _not_ an object.
+ *
+ * var selection = 'chai'
+ * assert.isNotObject(selection, 'tea selection is not an object');
+ * assert.isNotObject(null, 'null is not an object');
+ *
+ * @name isNotObject
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotObject = function (val, msg) {
+ new Assertion(val, msg).to.not.be.a('object');
+ };
+
+ /**
+ * ### .isArray(value, [message])
+ *
+ * Asserts that `value` is an array.
+ *
+ * var menu = [ 'green', 'chai', 'oolong' ];
+ * assert.isArray(menu, 'what kind of tea do we want?');
+ *
+ * @name isArray
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isArray = function (val, msg) {
+ new Assertion(val, msg).to.be.an('array');
+ };
+
+ /**
+ * ### .isNotArray(value, [message])
+ *
+ * Asserts that `value` is _not_ an array.
+ *
+ * var menu = 'green|chai|oolong';
+ * assert.isNotArray(menu, 'what kind of tea do we want?');
+ *
+ * @name isNotArray
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotArray = function (val, msg) {
+ new Assertion(val, msg).to.not.be.an('array');
+ };
+
+ /**
+ * ### .isString(value, [message])
+ *
+ * Asserts that `value` is a string.
+ *
+ * var teaOrder = 'chai';
+ * assert.isString(teaOrder, 'order placed');
+ *
+ * @name isString
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isString = function (val, msg) {
+ new Assertion(val, msg).to.be.a('string');
+ };
+
+ /**
+ * ### .isNotString(value, [message])
+ *
+ * Asserts that `value` is _not_ a string.
+ *
+ * var teaOrder = 4;
+ * assert.isNotString(teaOrder, 'order placed');
+ *
+ * @name isNotString
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotString = function (val, msg) {
+ new Assertion(val, msg).to.not.be.a('string');
+ };
+
+ /**
+ * ### .isNumber(value, [message])
+ *
+ * Asserts that `value` is a number.
+ *
+ * var cups = 2;
+ * assert.isNumber(cups, 'how many cups');
+ *
+ * @name isNumber
+ * @param {Number} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNumber = function (val, msg) {
+ new Assertion(val, msg).to.be.a('number');
+ };
+
+ /**
+ * ### .isNotNumber(value, [message])
+ *
+ * Asserts that `value` is _not_ a number.
+ *
+ * var cups = '2 cups please';
+ * assert.isNotNumber(cups, 'how many cups');
+ *
+ * @name isNotNumber
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotNumber = function (val, msg) {
+ new Assertion(val, msg).to.not.be.a('number');
+ };
+
+ /**
+ * ### .isBoolean(value, [message])
+ *
+ * Asserts that `value` is a boolean.
+ *
+ * var teaReady = true
+ * , teaServed = false;
+ *
+ * assert.isBoolean(teaReady, 'is the tea ready');
+ * assert.isBoolean(teaServed, 'has tea been served');
+ *
+ * @name isBoolean
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isBoolean = function (val, msg) {
+ new Assertion(val, msg).to.be.a('boolean');
+ };
+
+ /**
+ * ### .isNotBoolean(value, [message])
+ *
+ * Asserts that `value` is _not_ a boolean.
+ *
+ * var teaReady = 'yep'
+ * , teaServed = 'nope';
+ *
+ * assert.isNotBoolean(teaReady, 'is the tea ready');
+ * assert.isNotBoolean(teaServed, 'has tea been served');
+ *
+ * @name isNotBoolean
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.isNotBoolean = function (val, msg) {
+ new Assertion(val, msg).to.not.be.a('boolean');
+ };
+
+ /**
+ * ### .typeOf(value, name, [message])
+ *
+ * Asserts that `value`'s type is `name`, as determined by
+ * `Object.prototype.toString`.
+ *
+ * assert.typeOf({ tea: 'chai' }, 'object', 'we have an object');
+ * assert.typeOf(['chai', 'jasmine'], 'array', 'we have an array');
+ * assert.typeOf('tea', 'string', 'we have a string');
+ * assert.typeOf(/tea/, 'regexp', 'we have a regular expression');
+ * assert.typeOf(null, 'null', 'we have a null');
+ * assert.typeOf(undefined, 'undefined', 'we have an undefined');
+ *
+ * @name typeOf
+ * @param {Mixed} value
+ * @param {String} name
+ * @param {String} message
+ * @api public
+ */
+
+ assert.typeOf = function (val, type, msg) {
+ new Assertion(val, msg).to.be.a(type);
+ };
+
+ /**
+ * ### .notTypeOf(value, name, [message])
+ *
+ * Asserts that `value`'s type is _not_ `name`, as determined by
+ * `Object.prototype.toString`.
+ *
+ * assert.notTypeOf('tea', 'number', 'strings are not numbers');
+ *
+ * @name notTypeOf
+ * @param {Mixed} value
+ * @param {String} typeof name
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notTypeOf = function (val, type, msg) {
+ new Assertion(val, msg).to.not.be.a(type);
+ };
+
+ /**
+ * ### .instanceOf(object, constructor, [message])
+ *
+ * Asserts that `value` is an instance of `constructor`.
+ *
+ * var Tea = function (name) { this.name = name; }
+ * , chai = new Tea('chai');
+ *
+ * assert.instanceOf(chai, Tea, 'chai is an instance of tea');
+ *
+ * @name instanceOf
+ * @param {Object} object
+ * @param {Constructor} constructor
+ * @param {String} message
+ * @api public
+ */
+
+ assert.instanceOf = function (val, type, msg) {
+ new Assertion(val, msg).to.be.instanceOf(type);
+ };
+
+ /**
+ * ### .notInstanceOf(object, constructor, [message])
+ *
+ * Asserts `value` is not an instance of `constructor`.
+ *
+ * var Tea = function (name) { this.name = name; }
+ * , chai = new String('chai');
+ *
+ * assert.notInstanceOf(chai, Tea, 'chai is not an instance of tea');
+ *
+ * @name notInstanceOf
+ * @param {Object} object
+ * @param {Constructor} constructor
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notInstanceOf = function (val, type, msg) {
+ new Assertion(val, msg).to.not.be.instanceOf(type);
+ };
+
+ /**
+ * ### .include(haystack, needle, [message])
+ *
+ * Asserts that `haystack` includes `needle`. Works
+ * for strings and arrays.
+ *
+ * assert.include('foobar', 'bar', 'foobar contains string "bar"');
+ * assert.include([ 1, 2, 3 ], 3, 'array contains value');
+ *
+ * @name include
+ * @param {Array|String} haystack
+ * @param {Mixed} needle
+ * @param {String} message
+ * @api public
+ */
+
+ assert.include = function (exp, inc, msg) {
+ new Assertion(exp, msg, assert.include).include(inc);
+ };
+
+ /**
+ * ### .notInclude(haystack, needle, [message])
+ *
+ * Asserts that `haystack` does not include `needle`. Works
+ * for strings and arrays.
+ *
+ * assert.notInclude('foobar', 'baz', 'string not include substring');
+ * assert.notInclude([ 1, 2, 3 ], 4, 'array not include contain value');
+ *
+ * @name notInclude
+ * @param {Array|String} haystack
+ * @param {Mixed} needle
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notInclude = function (exp, inc, msg) {
+ new Assertion(exp, msg, assert.notInclude).not.include(inc);
+ };
+
+ /**
+ * ### .match(value, regexp, [message])
+ *
+ * Asserts that `value` matches the regular expression `regexp`.
+ *
+ * assert.match('foobar', /^foo/, 'regexp matches');
+ *
+ * @name match
+ * @param {Mixed} value
+ * @param {RegExp} regexp
+ * @param {String} message
+ * @api public
+ */
+
+ assert.match = function (exp, re, msg) {
+ new Assertion(exp, msg).to.match(re);
+ };
+
+ /**
+ * ### .notMatch(value, regexp, [message])
+ *
+ * Asserts that `value` does not match the regular expression `regexp`.
+ *
+ * assert.notMatch('foobar', /^foo/, 'regexp does not match');
+ *
+ * @name notMatch
+ * @param {Mixed} value
+ * @param {RegExp} regexp
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notMatch = function (exp, re, msg) {
+ new Assertion(exp, msg).to.not.match(re);
+ };
+
+ /**
+ * ### .property(object, property, [message])
+ *
+ * Asserts that `object` has a property named by `property`.
+ *
+ * assert.property({ tea: { green: 'matcha' }}, 'tea');
+ *
+ * @name property
+ * @param {Object} object
+ * @param {String} property
+ * @param {String} message
+ * @api public
+ */
+
+ assert.property = function (obj, prop, msg) {
+ new Assertion(obj, msg).to.have.property(prop);
+ };
+
+ /**
+ * ### .notProperty(object, property, [message])
+ *
+ * Asserts that `object` does _not_ have a property named by `property`.
+ *
+ * assert.notProperty({ tea: { green: 'matcha' }}, 'coffee');
+ *
+ * @name notProperty
+ * @param {Object} object
+ * @param {String} property
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notProperty = function (obj, prop, msg) {
+ new Assertion(obj, msg).to.not.have.property(prop);
+ };
+
+ /**
+ * ### .deepProperty(object, property, [message])
+ *
+ * Asserts that `object` has a property named by `property`, which can be a
+ * string using dot- and bracket-notation for deep reference.
+ *
+ * assert.deepProperty({ tea: { green: 'matcha' }}, 'tea.green');
+ *
+ * @name deepProperty
+ * @param {Object} object
+ * @param {String} property
+ * @param {String} message
+ * @api public
+ */
+
+ assert.deepProperty = function (obj, prop, msg) {
+ new Assertion(obj, msg).to.have.deep.property(prop);
+ };
+
+ /**
+ * ### .notDeepProperty(object, property, [message])
+ *
+ * Asserts that `object` does _not_ have a property named by `property`, which
+ * can be a string using dot- and bracket-notation for deep reference.
+ *
+ * assert.notDeepProperty({ tea: { green: 'matcha' }}, 'tea.oolong');
+ *
+ * @name notDeepProperty
+ * @param {Object} object
+ * @param {String} property
+ * @param {String} message
+ * @api public
+ */
+
+ assert.notDeepProperty = function (obj, prop, msg) {
+ new Assertion(obj, msg).to.not.have.deep.property(prop);
+ };
+
+ /**
+ * ### .propertyVal(object, property, value, [message])
+ *
+ * Asserts that `object` has a property named by `property` with value given
+ * by `value`.
+ *
+ * assert.propertyVal({ tea: 'is good' }, 'tea', 'is good');
+ *
+ * @name propertyVal
+ * @param {Object} object
+ * @param {String} property
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.propertyVal = function (obj, prop, val, msg) {
+ new Assertion(obj, msg).to.have.property(prop, val);
+ };
+
+ /**
+ * ### .propertyNotVal(object, property, value, [message])
+ *
+ * Asserts that `object` has a property named by `property`, but with a value
+ * different from that given by `value`.
+ *
+ * assert.propertyNotVal({ tea: 'is good' }, 'tea', 'is bad');
+ *
+ * @name propertyNotVal
+ * @param {Object} object
+ * @param {String} property
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.propertyNotVal = function (obj, prop, val, msg) {
+ new Assertion(obj, msg).to.not.have.property(prop, val);
+ };
+
+ /**
+ * ### .deepPropertyVal(object, property, value, [message])
+ *
+ * Asserts that `object` has a property named by `property` with value given
+ * by `value`. `property` can use dot- and bracket-notation for deep
+ * reference.
+ *
+ * assert.deepPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'matcha');
+ *
+ * @name deepPropertyVal
+ * @param {Object} object
+ * @param {String} property
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.deepPropertyVal = function (obj, prop, val, msg) {
+ new Assertion(obj, msg).to.have.deep.property(prop, val);
+ };
+
+ /**
+ * ### .deepPropertyNotVal(object, property, value, [message])
+ *
+ * Asserts that `object` has a property named by `property`, but with a value
+ * different from that given by `value`. `property` can use dot- and
+ * bracket-notation for deep reference.
+ *
+ * assert.deepPropertyNotVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha');
+ *
+ * @name deepPropertyNotVal
+ * @param {Object} object
+ * @param {String} property
+ * @param {Mixed} value
+ * @param {String} message
+ * @api public
+ */
+
+ assert.deepPropertyNotVal = function (obj, prop, val, msg) {
+ new Assertion(obj, msg).to.not.have.deep.property(prop, val);
+ };
+
+ /**
+ * ### .lengthOf(object, length, [message])
+ *
+ * Asserts that `object` has a `length` property with the expected value.
+ *
+ * assert.lengthOf([1,2,3], 3, 'array has length of 3');
+ * assert.lengthOf('foobar', 5, 'string has length of 6');
+ *
+ * @name lengthOf
+ * @param {Mixed} object
+ * @param {Number} length
+ * @param {String} message
+ * @api public
+ */
+
+ assert.lengthOf = function (exp, len, msg) {
+ new Assertion(exp, msg).to.have.length(len);
+ };
+
+ /**
+ * ### .throws(function, [constructor/string/regexp], [string/regexp], [message])
+ *
+ * Asserts that `function` will throw an error that is an instance of
+ * `constructor`, or alternately that it will throw an error with message
+ * matching `regexp`.
+ *
+ * assert.throw(fn, 'function throws a reference error');
+ * assert.throw(fn, /function throws a reference error/);
+ * assert.throw(fn, ReferenceError);
+ * assert.throw(fn, ReferenceError, 'function throws a reference error');
+ * assert.throw(fn, ReferenceError, /function throws a reference error/);
+ *
+ * @name throws
+ * @alias throw
+ * @alias Throw
+ * @param {Function} function
+ * @param {ErrorConstructor} constructor
+ * @param {RegExp} regexp
+ * @param {String} message
+ * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+ * @api public
+ */
+
+ assert.Throw = function (fn, errt, errs, msg) {
+ if ('string' === typeof errt || errt instanceof RegExp) {
+ errs = errt;
+ errt = null;
+ }
+
+ var assertErr = new Assertion(fn, msg).to.Throw(errt, errs);
+ return flag(assertErr, 'object');
+ };
+
+ /**
+ * ### .doesNotThrow(function, [constructor/regexp], [message])
+ *
+ * Asserts that `function` will _not_ throw an error that is an instance of
+ * `constructor`, or alternately that it will not throw an error with message
+ * matching `regexp`.
+ *
+ * assert.doesNotThrow(fn, Error, 'function does not throw');
+ *
+ * @name doesNotThrow
+ * @param {Function} function
+ * @param {ErrorConstructor} constructor
+ * @param {RegExp} regexp
+ * @param {String} message
+ * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types
+ * @api public
+ */
+
+ assert.doesNotThrow = function (fn, type, msg) {
+ if ('string' === typeof type) {
+ msg = type;
+ type = null;
+ }
+
+ new Assertion(fn, msg).to.not.Throw(type);
+ };
+
+ /**
+ * ### .operator(val1, operator, val2, [message])
+ *
+ * Compares two values using `operator`.
+ *
+ * assert.operator(1, '<', 2, 'everything is ok');
+ * assert.operator(1, '>', 2, 'this will fail');
+ *
+ * @name operator
+ * @param {Mixed} val1
+ * @param {String} operator
+ * @param {Mixed} val2
+ * @param {String} message
+ * @api public
+ */
+
+ assert.operator = function (val, operator, val2, msg) {
+ var ok;
+ switch(operator) {
+ case '==':
+ ok = val == val2;
+ break;
+ case '===':
+ ok = val === val2;
+ break;
+ case '>':
+ ok = val > val2;
+ break;
+ case '>=':
+ ok = val >= val2;
+ break;
+ case '<':
+ ok = val < val2;
+ break;
+ case '<=':
+ ok = val <= val2;
+ break;
+ case '!=':
+ ok = val != val2;
+ break;
+ case '!==':
+ ok = val !== val2;
+ break;
+ default:
+ throw new Error('Invalid operator "' + operator + '"');
+ }
+ var test = new Assertion(ok, msg);
+ test.assert(
+ true === flag(test, 'object')
+ , 'expected ' + util.inspect(val) + ' to be ' + operator + ' ' + util.inspect(val2)
+ , 'expected ' + util.inspect(val) + ' to not be ' + operator + ' ' + util.inspect(val2) );
+ };
+
+ /**
+ * ### .closeTo(actual, expected, delta, [message])
+ *
+ * Asserts that the target is equal `expected`, to within a +/- `delta` range.
+ *
+ * assert.closeTo(1.5, 1, 0.5, 'numbers are close');
+ *
+ * @name closeTo
+ * @param {Number} actual
+ * @param {Number} expected
+ * @param {Number} delta
+ * @param {String} message
+ * @api public
+ */
+
+ assert.closeTo = function (act, exp, delta, msg) {
+ new Assertion(act, msg).to.be.closeTo(exp, delta);
+ };
+
+ /**
+ * ### .sameMembers(set1, set2, [message])
+ *
+ * Asserts that `set1` and `set2` have the same members.
+ * Order is not taken into account.
+ *
+ * assert.sameMembers([ 1, 2, 3 ], [ 2, 1, 3 ], 'same members');
+ *
+ * @name sameMembers
+ * @param {Array} set1
+ * @param {Array} set2
+ * @param {String} message
+ * @api public
+ */
+
+ assert.sameMembers = function (set1, set2, msg) {
+ new Assertion(set1, msg).to.have.same.members(set2);
+ }
+
+ /**
+ * ### .sameDeepMembers(set1, set2, [message])
+ *
+ * Asserts that `set1` and `set2` have the same members - using a deep equality checking.
+ * Order is not taken into account.
+ *
+ * assert.sameDeepMembers([ {b: 3}, {a: 2}, {c: 5} ], [ {c: 5}, {b: 3}, {a: 2} ], 'same deep members');
+ *
+ * @name sameDeepMembers
+ * @param {Array} set1
+ * @param {Array} set2
+ * @param {String} message
+ * @api public
+ */
+
+ assert.sameDeepMembers = function (set1, set2, msg) {
+ new Assertion(set1, msg).to.have.same.deep.members(set2);
+ }
+
+ /**
+ * ### .includeMembers(superset, subset, [message])
+ *
+ * Asserts that `subset` is included in `superset`.
+ * Order is not taken into account.
+ *
+ * assert.includeMembers([ 1, 2, 3 ], [ 2, 1 ], 'include members');
+ *
+ * @name includeMembers
+ * @param {Array} superset
+ * @param {Array} subset
+ * @param {String} message
+ * @api public
+ */
+
+ assert.includeMembers = function (superset, subset, msg) {
+ new Assertion(superset, msg).to.include.members(subset);
+ }
+
+ /**
+ * ### .changes(function, object, property)
+ *
+ * Asserts that a function changes the value of a property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 22 };
+ * assert.changes(fn, obj, 'val');
+ *
+ * @name changes
+ * @param {Function} modifier function
+ * @param {Object} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ assert.changes = function (fn, obj, prop) {
+ new Assertion(fn).to.change(obj, prop);
+ }
+
+ /**
+ * ### .doesNotChange(function, object, property)
+ *
+ * Asserts that a function does not changes the value of a property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { console.log('foo'); };
+ * assert.doesNotChange(fn, obj, 'val');
+ *
+ * @name doesNotChange
+ * @param {Function} modifier function
+ * @param {Object} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ assert.doesNotChange = function (fn, obj, prop) {
+ new Assertion(fn).to.not.change(obj, prop);
+ }
+
+ /**
+ * ### .increases(function, object, property)
+ *
+ * Asserts that a function increases an object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 13 };
+ * assert.increases(fn, obj, 'val');
+ *
+ * @name increases
+ * @param {Function} modifier function
+ * @param {Object} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ assert.increases = function (fn, obj, prop) {
+ new Assertion(fn).to.increase(obj, prop);
+ }
+
+ /**
+ * ### .doesNotIncrease(function, object, property)
+ *
+ * Asserts that a function does not increase object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 8 };
+ * assert.doesNotIncrease(fn, obj, 'val');
+ *
+ * @name doesNotIncrease
+ * @param {Function} modifier function
+ * @param {Object} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ assert.doesNotIncrease = function (fn, obj, prop) {
+ new Assertion(fn).to.not.increase(obj, prop);
+ }
+
+ /**
+ * ### .decreases(function, object, property)
+ *
+ * Asserts that a function decreases an object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 5 };
+ * assert.decreases(fn, obj, 'val');
+ *
+ * @name decreases
+ * @param {Function} modifier function
+ * @param {Object} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ assert.decreases = function (fn, obj, prop) {
+ new Assertion(fn).to.decrease(obj, prop);
+ }
+
+ /**
+ * ### .doesNotDecrease(function, object, property)
+ *
+ * Asserts that a function does not decreases an object property
+ *
+ * var obj = { val: 10 };
+ * var fn = function() { obj.val = 15 };
+ * assert.doesNotDecrease(fn, obj, 'val');
+ *
+ * @name doesNotDecrease
+ * @param {Function} modifier function
+ * @param {Object} object
+ * @param {String} property name
+ * @param {String} message _optional_
+ * @api public
+ */
+
+ assert.doesNotDecrease = function (fn, obj, prop) {
+ new Assertion(fn).to.not.decrease(obj, prop);
+ }
+
+ /*!
+ * ### .ifError(object)
+ *
+ * Asserts if value is not a false value, and throws if it is a true value.
+ * This is added to allow for chai to be a drop-in replacement for Node's
+ * assert class.
+ *
+ * var err = new Error('I am a custom error');
+ * assert.ifError(err); // Rethrows err!
+ *
+ * @name ifError
+ * @param {Object} object
+ * @api public
+ */
+
+ assert.ifError = function (val) {
+ if (val) {
+ throw(val);
+ }
+ };
+
+ /*!
+ * Aliases.
+ */
+
+ (function alias(name, as){
+ assert[as] = assert[name];
+ return alias;
+ })('Throw', 'throw')('Throw', 'throws');
+};
+
+},{}],6:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, util) {
+ chai.expect = function (val, message) {
+ return new chai.Assertion(val, message);
+ };
+
+ /**
+ * ### .fail(actual, expected, [message], [operator])
+ *
+ * Throw a failure.
+ *
+ * @name fail
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @param {String} operator
+ * @api public
+ */
+
+ chai.expect.fail = function (actual, expected, message, operator) {
+ message = message || 'expect.fail()';
+ throw new chai.AssertionError(message, {
+ actual: actual
+ , expected: expected
+ , operator: operator
+ }, chai.expect.fail);
+ };
+};
+
+},{}],7:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+module.exports = function (chai, util) {
+ var Assertion = chai.Assertion;
+
+ function loadShould () {
+ // explicitly define this method as function as to have it's name to include as `ssfi`
+ function shouldGetter() {
+ if (this instanceof String || this instanceof Number || this instanceof Boolean ) {
+ return new Assertion(this.valueOf(), null, shouldGetter);
+ }
+ return new Assertion(this, null, shouldGetter);
+ }
+ function shouldSetter(value) {
+ // See https://github.com/chaijs/chai/issues/86: this makes
+ // `whatever.should = someValue` actually set `someValue`, which is
+ // especially useful for `global.should = require('chai').should()`.
+ //
+ // Note that we have to use [[DefineProperty]] instead of [[Put]]
+ // since otherwise we would trigger this very setter!
+ Object.defineProperty(this, 'should', {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
+ }
+ // modify Object.prototype to have `should`
+ Object.defineProperty(Object.prototype, 'should', {
+ set: shouldSetter
+ , get: shouldGetter
+ , configurable: true
+ });
+
+ var should = {};
+
+ /**
+ * ### .fail(actual, expected, [message], [operator])
+ *
+ * Throw a failure.
+ *
+ * @name fail
+ * @param {Mixed} actual
+ * @param {Mixed} expected
+ * @param {String} message
+ * @param {String} operator
+ * @api public
+ */
+
+ should.fail = function (actual, expected, message, operator) {
+ message = message || 'should.fail()';
+ throw new chai.AssertionError(message, {
+ actual: actual
+ , expected: expected
+ , operator: operator
+ }, should.fail);
+ };
+
+ should.equal = function (val1, val2, msg) {
+ new Assertion(val1, msg).to.equal(val2);
+ };
+
+ should.Throw = function (fn, errt, errs, msg) {
+ new Assertion(fn, msg).to.Throw(errt, errs);
+ };
+
+ should.exist = function (val, msg) {
+ new Assertion(val, msg).to.exist;
+ }
+
+ // negation
+ should.not = {}
+
+ should.not.equal = function (val1, val2, msg) {
+ new Assertion(val1, msg).to.not.equal(val2);
+ };
+
+ should.not.Throw = function (fn, errt, errs, msg) {
+ new Assertion(fn, msg).to.not.Throw(errt, errs);
+ };
+
+ should.not.exist = function (val, msg) {
+ new Assertion(val, msg).to.not.exist;
+ }
+
+ should['throw'] = should['Throw'];
+ should.not['throw'] = should.not['Throw'];
+
+ return should;
+ }
+
+ chai.should = loadShould;
+ chai.Should = loadShould;
+};
+
+},{}],8:[function(require,module,exports){
+/*!
+ * Chai - addChainingMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var transferFlags = require('./transferFlags');
+var flag = require('./flag');
+var config = require('../config');
+
+/*!
+ * Module variables
+ */
+
+// Check whether `__proto__` is supported
+var hasProtoSupport = '__proto__' in Object;
+
+// Without `__proto__` support, this module will need to add properties to a function.
+// However, some Function.prototype methods cannot be overwritten,
+// and there seems no easy cross-platform way to detect them (@see chaijs/chai/issues/69).
+var excludeNames = /^(?:length|name|arguments|caller)$/;
+
+// Cache `Function` properties
+var call = Function.prototype.call,
+ apply = Function.prototype.apply;
+
+/**
+ * ### addChainableMethod (ctx, name, method, chainingBehavior)
+ *
+ * Adds a method to an object, such that the method can also be chained.
+ *
+ * utils.addChainableMethod(chai.Assertion.prototype, 'foo', function (str) {
+ * var obj = utils.flag(this, 'object');
+ * new chai.Assertion(obj).to.be.equal(str);
+ * });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ * chai.Assertion.addChainableMethod('foo', fn, chainingBehavior);
+ *
+ * The result can then be used as both a method assertion, executing both `method` and
+ * `chainingBehavior`, or as a language chain, which only executes `chainingBehavior`.
+ *
+ * expect(fooStr).to.be.foo('bar');
+ * expect(fooStr).to.be.foo.equal('foo');
+ *
+ * @param {Object} ctx object to which the method is added
+ * @param {String} name of method to add
+ * @param {Function} method function to be used for `name`, when called
+ * @param {Function} chainingBehavior function to be called every time the property is accessed
+ * @name addChainableMethod
+ * @api public
+ */
+
+module.exports = function (ctx, name, method, chainingBehavior) {
+ if (typeof chainingBehavior !== 'function') {
+ chainingBehavior = function () { };
+ }
+
+ var chainableBehavior = {
+ method: method
+ , chainingBehavior: chainingBehavior
+ };
+
+ // save the methods so we can overwrite them later, if we need to.
+ if (!ctx.__methods) {
+ ctx.__methods = {};
+ }
+ ctx.__methods[name] = chainableBehavior;
+
+ Object.defineProperty(ctx, name,
+ { get: function () {
+ chainableBehavior.chainingBehavior.call(this);
+
+ var assert = function assert() {
+ var old_ssfi = flag(this, 'ssfi');
+ if (old_ssfi && config.includeStack === false)
+ flag(this, 'ssfi', assert);
+ var result = chainableBehavior.method.apply(this, arguments);
+ return result === undefined ? this : result;
+ };
+
+ // Use `__proto__` if available
+ if (hasProtoSupport) {
+ // Inherit all properties from the object by replacing the `Function` prototype
+ var prototype = assert.__proto__ = Object.create(this);
+ // Restore the `call` and `apply` methods from `Function`
+ prototype.call = call;
+ prototype.apply = apply;
+ }
+ // Otherwise, redefine all properties (slow!)
+ else {
+ var asserterNames = Object.getOwnPropertyNames(ctx);
+ asserterNames.forEach(function (asserterName) {
+ if (!excludeNames.test(asserterName)) {
+ var pd = Object.getOwnPropertyDescriptor(ctx, asserterName);
+ Object.defineProperty(assert, asserterName, pd);
+ }
+ });
+ }
+
+ transferFlags(this, assert);
+ return assert;
+ }
+ , configurable: true
+ });
+};
+
+},{"../config":3,"./flag":11,"./transferFlags":27}],9:[function(require,module,exports){
+/*!
+ * Chai - addMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var config = require('../config');
+
+/**
+ * ### .addMethod (ctx, name, method)
+ *
+ * Adds a method to the prototype of an object.
+ *
+ * utils.addMethod(chai.Assertion.prototype, 'foo', function (str) {
+ * var obj = utils.flag(this, 'object');
+ * new chai.Assertion(obj).to.be.equal(str);
+ * });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ * chai.Assertion.addMethod('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ * expect(fooStr).to.be.foo('bar');
+ *
+ * @param {Object} ctx object to which the method is added
+ * @param {String} name of method to add
+ * @param {Function} method function to be used for name
+ * @name addMethod
+ * @api public
+ */
+var flag = require('./flag');
+
+module.exports = function (ctx, name, method) {
+ ctx[name] = function () {
+ var old_ssfi = flag(this, 'ssfi');
+ if (old_ssfi && config.includeStack === false)
+ flag(this, 'ssfi', ctx[name]);
+ var result = method.apply(this, arguments);
+ return result === undefined ? this : result;
+ };
+};
+
+},{"../config":3,"./flag":11}],10:[function(require,module,exports){
+/*!
+ * Chai - addProperty utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### addProperty (ctx, name, getter)
+ *
+ * Adds a property to the prototype of an object.
+ *
+ * utils.addProperty(chai.Assertion.prototype, 'foo', function () {
+ * var obj = utils.flag(this, 'object');
+ * new chai.Assertion(obj).to.be.instanceof(Foo);
+ * });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ * chai.Assertion.addProperty('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ * expect(myFoo).to.be.foo;
+ *
+ * @param {Object} ctx object to which the property is added
+ * @param {String} name of property to add
+ * @param {Function} getter function to be used for name
+ * @name addProperty
+ * @api public
+ */
+
+module.exports = function (ctx, name, getter) {
+ Object.defineProperty(ctx, name,
+ { get: function () {
+ var result = getter.call(this);
+ return result === undefined ? this : result;
+ }
+ , configurable: true
+ });
+};
+
+},{}],11:[function(require,module,exports){
+/*!
+ * Chai - flag utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### flag(object, key, [value])
+ *
+ * Get or set a flag value on an object. If a
+ * value is provided it will be set, else it will
+ * return the currently set value or `undefined` if
+ * the value is not set.
+ *
+ * utils.flag(this, 'foo', 'bar'); // setter
+ * utils.flag(this, 'foo'); // getter, returns `bar`
+ *
+ * @param {Object} object constructed Assertion
+ * @param {String} key
+ * @param {Mixed} value (optional)
+ * @name flag
+ * @api private
+ */
+
+module.exports = function (obj, key, value) {
+ var flags = obj.__flags || (obj.__flags = Object.create(null));
+ if (arguments.length === 3) {
+ flags[key] = value;
+ } else {
+ return flags[key];
+ }
+};
+
+},{}],12:[function(require,module,exports){
+/*!
+ * Chai - getActual utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * # getActual(object, [actual])
+ *
+ * Returns the `actual` value for an Assertion
+ *
+ * @param {Object} object (constructed Assertion)
+ * @param {Arguments} chai.Assertion.prototype.assert arguments
+ */
+
+module.exports = function (obj, args) {
+ return args.length > 4 ? args[4] : obj._obj;
+};
+
+},{}],13:[function(require,module,exports){
+/*!
+ * Chai - getEnumerableProperties utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getEnumerableProperties(object)
+ *
+ * This allows the retrieval of enumerable property names of an object,
+ * inherited or not.
+ *
+ * @param {Object} object
+ * @returns {Array}
+ * @name getEnumerableProperties
+ * @api public
+ */
+
+module.exports = function getEnumerableProperties(object) {
+ var result = [];
+ for (var name in object) {
+ result.push(name);
+ }
+ return result;
+};
+
+},{}],14:[function(require,module,exports){
+/*!
+ * Chai - message composition utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependancies
+ */
+
+var flag = require('./flag')
+ , getActual = require('./getActual')
+ , inspect = require('./inspect')
+ , objDisplay = require('./objDisplay');
+
+/**
+ * ### .getMessage(object, message, negateMessage)
+ *
+ * Construct the error message based on flags
+ * and template tags. Template tags will return
+ * a stringified inspection of the object referenced.
+ *
+ * Message template tags:
+ * - `#{this}` current asserted object
+ * - `#{act}` actual value
+ * - `#{exp}` expected value
+ *
+ * @param {Object} object (constructed Assertion)
+ * @param {Arguments} chai.Assertion.prototype.assert arguments
+ * @name getMessage
+ * @api public
+ */
+
+module.exports = function (obj, args) {
+ var negate = flag(obj, 'negate')
+ , val = flag(obj, 'object')
+ , expected = args[3]
+ , actual = getActual(obj, args)
+ , msg = negate ? args[2] : args[1]
+ , flagMsg = flag(obj, 'message');
+
+ if(typeof msg === "function") msg = msg();
+ msg = msg || '';
+ msg = msg
+ .replace(/#{this}/g, objDisplay(val))
+ .replace(/#{act}/g, objDisplay(actual))
+ .replace(/#{exp}/g, objDisplay(expected));
+
+ return flagMsg ? flagMsg + ': ' + msg : msg;
+};
+
+},{"./flag":11,"./getActual":12,"./inspect":21,"./objDisplay":22}],15:[function(require,module,exports){
+/*!
+ * Chai - getName utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * # getName(func)
+ *
+ * Gets the name of a function, in a cross-browser way.
+ *
+ * @param {Function} a function (usually a constructor)
+ */
+
+module.exports = function (func) {
+ if (func.name) return func.name;
+
+ var match = /^\s?function ([^(]*)\(/.exec(func);
+ return match && match[1] ? match[1] : "";
+};
+
+},{}],16:[function(require,module,exports){
+/*!
+ * Chai - getPathInfo utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var hasProperty = require('./hasProperty');
+
+/**
+ * ### .getPathInfo(path, object)
+ *
+ * This allows the retrieval of property info in an
+ * object given a string path.
+ *
+ * The path info consists of an object with the
+ * following properties:
+ *
+ * * parent - The parent object of the property referenced by `path`
+ * * name - The name of the final property, a number if it was an array indexer
+ * * value - The value of the property, if it exists, otherwise `undefined`
+ * * exists - Whether the property exists or not
+ *
+ * @param {String} path
+ * @param {Object} object
+ * @returns {Object} info
+ * @name getPathInfo
+ * @api public
+ */
+
+module.exports = function getPathInfo(path, obj) {
+ var parsed = parsePath(path),
+ last = parsed[parsed.length - 1];
+
+ var info = {
+ parent: parsed.length > 1 ? _getPathValue(parsed, obj, parsed.length - 1) : obj,
+ name: last.p || last.i,
+ value: _getPathValue(parsed, obj)
+ };
+ info.exists = hasProperty(info.name, info.parent);
+
+ return info;
+};
+
+
+/*!
+ * ## parsePath(path)
+ *
+ * Helper function used to parse string object
+ * paths. Use in conjunction with `_getPathValue`.
+ *
+ * var parsed = parsePath('myobject.property.subprop');
+ *
+ * ### Paths:
+ *
+ * * Can be as near infinitely deep and nested
+ * * Arrays are also valid using the formal `myobject.document[3].property`.
+ * * Literal dots and brackets (not delimiter) must be backslash-escaped.
+ *
+ * @param {String} path
+ * @returns {Object} parsed
+ * @api private
+ */
+
+function parsePath (path) {
+ var str = path.replace(/([^\\])\[/g, '$1.[')
+ , parts = str.match(/(\\\.|[^.]+?)+/g);
+ return parts.map(function (value) {
+ var re = /^\[(\d+)\]$/
+ , mArr = re.exec(value);
+ if (mArr) return { i: parseFloat(mArr[1]) };
+ else return { p: value.replace(/\\([.\[\]])/g, '$1') };
+ });
+}
+
+
+/*!
+ * ## _getPathValue(parsed, obj)
+ *
+ * Helper companion function for `.parsePath` that returns
+ * the value located at the parsed address.
+ *
+ * var value = getPathValue(parsed, obj);
+ *
+ * @param {Object} parsed definition from `parsePath`.
+ * @param {Object} object to search against
+ * @param {Number} object to search against
+ * @returns {Object|Undefined} value
+ * @api private
+ */
+
+function _getPathValue (parsed, obj, index) {
+ var tmp = obj
+ , res;
+
+ index = (index === undefined ? parsed.length : index);
+
+ for (var i = 0, l = index; i < l; i++) {
+ var part = parsed[i];
+ if (tmp) {
+ if ('undefined' !== typeof part.p)
+ tmp = tmp[part.p];
+ else if ('undefined' !== typeof part.i)
+ tmp = tmp[part.i];
+ if (i == (l - 1)) res = tmp;
+ } else {
+ res = undefined;
+ }
+ }
+ return res;
+}
+
+},{"./hasProperty":19}],17:[function(require,module,exports){
+/*!
+ * Chai - getPathValue utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * @see https://github.com/logicalparadox/filtr
+ * MIT Licensed
+ */
+
+var getPathInfo = require('./getPathInfo');
+
+/**
+ * ### .getPathValue(path, object)
+ *
+ * This allows the retrieval of values in an
+ * object given a string path.
+ *
+ * var obj = {
+ * prop1: {
+ * arr: ['a', 'b', 'c']
+ * , str: 'Hello'
+ * }
+ * , prop2: {
+ * arr: [ { nested: 'Universe' } ]
+ * , str: 'Hello again!'
+ * }
+ * }
+ *
+ * The following would be the results.
+ *
+ * getPathValue('prop1.str', obj); // Hello
+ * getPathValue('prop1.att[2]', obj); // b
+ * getPathValue('prop2.arr[0].nested', obj); // Universe
+ *
+ * @param {String} path
+ * @param {Object} object
+ * @returns {Object} value or `undefined`
+ * @name getPathValue
+ * @api public
+ */
+module.exports = function(path, obj) {
+ var info = getPathInfo(path, obj);
+ return info.value;
+};
+
+},{"./getPathInfo":16}],18:[function(require,module,exports){
+/*!
+ * Chai - getProperties utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### .getProperties(object)
+ *
+ * This allows the retrieval of property names of an object, enumerable or not,
+ * inherited or not.
+ *
+ * @param {Object} object
+ * @returns {Array}
+ * @name getProperties
+ * @api public
+ */
+
+module.exports = function getProperties(object) {
+ var result = Object.getOwnPropertyNames(subject);
+
+ function addProperty(property) {
+ if (result.indexOf(property) === -1) {
+ result.push(property);
+ }
+ }
+
+ var proto = Object.getPrototypeOf(subject);
+ while (proto !== null) {
+ Object.getOwnPropertyNames(proto).forEach(addProperty);
+ proto = Object.getPrototypeOf(proto);
+ }
+
+ return result;
+};
+
+},{}],19:[function(require,module,exports){
+/*!
+ * Chai - hasProperty utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+var type = require('type-detect');
+
+/**
+ * ### .hasProperty(object, name)
+ *
+ * This allows checking whether an object has
+ * named property or numeric array index.
+ *
+ * Basically does the same thing as the `in`
+ * operator but works properly with natives
+ * and null/undefined values.
+ *
+ * var obj = {
+ * arr: ['a', 'b', 'c']
+ * , str: 'Hello'
+ * }
+ *
+ * The following would be the results.
+ *
+ * hasProperty('str', obj); // true
+ * hasProperty('constructor', obj); // true
+ * hasProperty('bar', obj); // false
+ *
+ * hasProperty('length', obj.str); // true
+ * hasProperty(1, obj.str); // true
+ * hasProperty(5, obj.str); // false
+ *
+ * hasProperty('length', obj.arr); // true
+ * hasProperty(2, obj.arr); // true
+ * hasProperty(3, obj.arr); // false
+ *
+ * @param {Objuect} object
+ * @param {String|Number} name
+ * @returns {Boolean} whether it exists
+ * @name getPathInfo
+ * @api public
+ */
+
+var literals = {
+ 'number': Number
+ , 'string': String
+};
+
+module.exports = function hasProperty(name, obj) {
+ var ot = type(obj);
+
+ // Bad Object, obviously no props at all
+ if(ot === 'null' || ot === 'undefined')
+ return false;
+
+ // The `in` operator does not work with certain literals
+ // box these before the check
+ if(literals[ot] && typeof obj !== 'object')
+ obj = new literals[ot](obj);
+
+ return name in obj;
+};
+
+},{"type-detect":33}],20:[function(require,module,exports){
+/*!
+ * chai
+ * Copyright(c) 2011 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Main exports
+ */
+
+exports = module.exports = {};
+
+/*!
+ * test utility
+ */
+
+exports.test = require('./test');
+
+/*!
+ * type utility
+ */
+
+exports.type = require('type-detect');
+
+/*!
+ * message utility
+ */
+
+exports.getMessage = require('./getMessage');
+
+/*!
+ * actual utility
+ */
+
+exports.getActual = require('./getActual');
+
+/*!
+ * Inspect util
+ */
+
+exports.inspect = require('./inspect');
+
+/*!
+ * Object Display util
+ */
+
+exports.objDisplay = require('./objDisplay');
+
+/*!
+ * Flag utility
+ */
+
+exports.flag = require('./flag');
+
+/*!
+ * Flag transferring utility
+ */
+
+exports.transferFlags = require('./transferFlags');
+
+/*!
+ * Deep equal utility
+ */
+
+exports.eql = require('deep-eql');
+
+/*!
+ * Deep path value
+ */
+
+exports.getPathValue = require('./getPathValue');
+
+/*!
+ * Deep path info
+ */
+
+exports.getPathInfo = require('./getPathInfo');
+
+/*!
+ * Check if a property exists
+ */
+
+exports.hasProperty = require('./hasProperty');
+
+/*!
+ * Function name
+ */
+
+exports.getName = require('./getName');
+
+/*!
+ * add Property
+ */
+
+exports.addProperty = require('./addProperty');
+
+/*!
+ * add Method
+ */
+
+exports.addMethod = require('./addMethod');
+
+/*!
+ * overwrite Property
+ */
+
+exports.overwriteProperty = require('./overwriteProperty');
+
+/*!
+ * overwrite Method
+ */
+
+exports.overwriteMethod = require('./overwriteMethod');
+
+/*!
+ * Add a chainable method
+ */
+
+exports.addChainableMethod = require('./addChainableMethod');
+
+/*!
+ * Overwrite chainable method
+ */
+
+exports.overwriteChainableMethod = require('./overwriteChainableMethod');
+
+
+},{"./addChainableMethod":8,"./addMethod":9,"./addProperty":10,"./flag":11,"./getActual":12,"./getMessage":14,"./getName":15,"./getPathInfo":16,"./getPathValue":17,"./hasProperty":19,"./inspect":21,"./objDisplay":22,"./overwriteChainableMethod":23,"./overwriteMethod":24,"./overwriteProperty":25,"./test":26,"./transferFlags":27,"deep-eql":29,"type-detect":33}],21:[function(require,module,exports){
+// This is (almost) directly from Node.js utils
+// https://github.com/joyent/node/blob/f8c335d0caf47f16d31413f89aa28eda3878e3aa/lib/util.js
+
+var getName = require('./getName');
+var getProperties = require('./getProperties');
+var getEnumerableProperties = require('./getEnumerableProperties');
+
+module.exports = inspect;
+
+/**
+ * Echos the value of a value. Trys to print the value out
+ * in the best way possible given the different types.
+ *
+ * @param {Object} obj The object to print out.
+ * @param {Boolean} showHidden Flag that shows hidden (not enumerable)
+ * properties of objects.
+ * @param {Number} depth Depth in which to descend in object. Default is 2.
+ * @param {Boolean} colors Flag to turn on ANSI escape codes to color the
+ * output. Default is false (no coloring).
+ */
+function inspect(obj, showHidden, depth, colors) {
+ var ctx = {
+ showHidden: showHidden,
+ seen: [],
+ stylize: function (str) { return str; }
+ };
+ return formatValue(ctx, obj, (typeof depth === 'undefined' ? 2 : depth));
+}
+
+// Returns true if object is a DOM element.
+var isDOMElement = function (object) {
+ if (typeof HTMLElement === 'object') {
+ return object instanceof HTMLElement;
+ } else {
+ return object &&
+ typeof object === 'object' &&
+ object.nodeType === 1 &&
+ typeof object.nodeName === 'string';
+ }
+};
+
+function formatValue(ctx, value, recurseTimes) {
+ // Provide a hook for user-specified inspect functions.
+ // Check that value is an object with an inspect function on it
+ if (value && typeof value.inspect === 'function' &&
+ // Filter out the util module, it's inspect function is special
+ value.inspect !== exports.inspect &&
+ // Also filter out any prototype objects using the circular check.
+ !(value.constructor && value.constructor.prototype === value)) {
+ var ret = value.inspect(recurseTimes);
+ if (typeof ret !== 'string') {
+ ret = formatValue(ctx, ret, recurseTimes);
+ }
+ return ret;
+ }
+
+ // Primitive types cannot have properties
+ var primitive = formatPrimitive(ctx, value);
+ if (primitive) {
+ return primitive;
+ }
+
+ // If this is a DOM element, try to get the outer HTML.
+ if (isDOMElement(value)) {
+ if ('outerHTML' in value) {
+ return value.outerHTML;
+ // This value does not have an outerHTML attribute,
+ // it could still be an XML element
+ } else {
+ // Attempt to serialize it
+ try {
+ if (document.xmlVersion) {
+ var xmlSerializer = new XMLSerializer();
+ return xmlSerializer.serializeToString(value);
+ } else {
+ // Firefox 11- do not support outerHTML
+ // It does, however, support innerHTML
+ // Use the following to render the element
+ var ns = "http://www.w3.org/1999/xhtml";
+ var container = document.createElementNS(ns, '_');
+
+ container.appendChild(value.cloneNode(false));
+ html = container.innerHTML
+ .replace('><', '>' + value.innerHTML + '<');
+ container.innerHTML = '';
+ return html;
+ }
+ } catch (err) {
+ // This could be a non-native DOM implementation,
+ // continue with the normal flow:
+ // printing the element as if it is an object.
+ }
+ }
+ }
+
+ // Look up the keys of the object.
+ var visibleKeys = getEnumerableProperties(value);
+ var keys = ctx.showHidden ? getProperties(value) : visibleKeys;
+
+ // Some type of object without properties can be shortcutted.
+ // In IE, errors have a single `stack` property, or if they are vanilla `Error`,
+ // a `stack` plus `description` property; ignore those for consistency.
+ if (keys.length === 0 || (isError(value) && (
+ (keys.length === 1 && keys[0] === 'stack') ||
+ (keys.length === 2 && keys[0] === 'description' && keys[1] === 'stack')
+ ))) {
+ if (typeof value === 'function') {
+ var name = getName(value);
+ var nameSuffix = name ? ': ' + name : '';
+ return ctx.stylize('[Function' + nameSuffix + ']', 'special');
+ }
+ if (isRegExp(value)) {
+ return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
+ }
+ if (isDate(value)) {
+ return ctx.stylize(Date.prototype.toUTCString.call(value), 'date');
+ }
+ if (isError(value)) {
+ return formatError(value);
+ }
+ }
+
+ var base = '', array = false, braces = ['{', '}'];
+
+ // Make Array say that they are Array
+ if (isArray(value)) {
+ array = true;
+ braces = ['[', ']'];
+ }
+
+ // Make functions say that they are functions
+ if (typeof value === 'function') {
+ name = getName(value);
+ nameSuffix = name ? ': ' + name : '';
+ base = ' [Function' + nameSuffix + ']';
+ }
+
+ // Make RegExps say that they are RegExps
+ if (isRegExp(value)) {
+ base = ' ' + RegExp.prototype.toString.call(value);
+ }
+
+ // Make dates with properties first say the date
+ if (isDate(value)) {
+ base = ' ' + Date.prototype.toUTCString.call(value);
+ }
+
+ // Make error with message first say the error
+ if (isError(value)) {
+ return formatError(value);
+ }
+
+ if (keys.length === 0 && (!array || value.length == 0)) {
+ return braces[0] + base + braces[1];
+ }
+
+ if (recurseTimes < 0) {
+ if (isRegExp(value)) {
+ return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
+ } else {
+ return ctx.stylize('[Object]', 'special');
+ }
+ }
+
+ ctx.seen.push(value);
+
+ var output;
+ if (array) {
+ output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
+ } else {
+ output = keys.map(function(key) {
+ return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
+ });
+ }
+
+ ctx.seen.pop();
+
+ return reduceToSingleString(output, base, braces);
+}
+
+
+function formatPrimitive(ctx, value) {
+ switch (typeof value) {
+ case 'undefined':
+ return ctx.stylize('undefined', 'undefined');
+
+ case 'string':
+ var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
+ .replace(/'/g, "\\'")
+ .replace(/\\"/g, '"') + '\'';
+ return ctx.stylize(simple, 'string');
+
+ case 'number':
+ if (value === 0 && (1/value) === -Infinity) {
+ return ctx.stylize('-0', 'number');
+ }
+ return ctx.stylize('' + value, 'number');
+
+ case 'boolean':
+ return ctx.stylize('' + value, 'boolean');
+ }
+ // For some reason typeof null is "object", so special case here.
+ if (value === null) {
+ return ctx.stylize('null', 'null');
+ }
+}
+
+
+function formatError(value) {
+ return '[' + Error.prototype.toString.call(value) + ']';
+}
+
+
+function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
+ var output = [];
+ for (var i = 0, l = value.length; i < l; ++i) {
+ if (Object.prototype.hasOwnProperty.call(value, String(i))) {
+ output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
+ String(i), true));
+ } else {
+ output.push('');
+ }
+ }
+ keys.forEach(function(key) {
+ if (!key.match(/^\d+$/)) {
+ output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
+ key, true));
+ }
+ });
+ return output;
+}
+
+
+function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
+ var name, str;
+ if (value.__lookupGetter__) {
+ if (value.__lookupGetter__(key)) {
+ if (value.__lookupSetter__(key)) {
+ str = ctx.stylize('[Getter/Setter]', 'special');
+ } else {
+ str = ctx.stylize('[Getter]', 'special');
+ }
+ } else {
+ if (value.__lookupSetter__(key)) {
+ str = ctx.stylize('[Setter]', 'special');
+ }
+ }
+ }
+ if (visibleKeys.indexOf(key) < 0) {
+ name = '[' + key + ']';
+ }
+ if (!str) {
+ if (ctx.seen.indexOf(value[key]) < 0) {
+ if (recurseTimes === null) {
+ str = formatValue(ctx, value[key], null);
+ } else {
+ str = formatValue(ctx, value[key], recurseTimes - 1);
+ }
+ if (str.indexOf('\n') > -1) {
+ if (array) {
+ str = str.split('\n').map(function(line) {
+ return ' ' + line;
+ }).join('\n').substr(2);
+ } else {
+ str = '\n' + str.split('\n').map(function(line) {
+ return ' ' + line;
+ }).join('\n');
+ }
+ }
+ } else {
+ str = ctx.stylize('[Circular]', 'special');
+ }
+ }
+ if (typeof name === 'undefined') {
+ if (array && key.match(/^\d+$/)) {
+ return str;
+ }
+ name = JSON.stringify('' + key);
+ if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
+ name = name.substr(1, name.length - 2);
+ name = ctx.stylize(name, 'name');
+ } else {
+ name = name.replace(/'/g, "\\'")
+ .replace(/\\"/g, '"')
+ .replace(/(^"|"$)/g, "'");
+ name = ctx.stylize(name, 'string');
+ }
+ }
+
+ return name + ': ' + str;
+}
+
+
+function reduceToSingleString(output, base, braces) {
+ var numLinesEst = 0;
+ var length = output.reduce(function(prev, cur) {
+ numLinesEst++;
+ if (cur.indexOf('\n') >= 0) numLinesEst++;
+ return prev + cur.length + 1;
+ }, 0);
+
+ if (length > 60) {
+ return braces[0] +
+ (base === '' ? '' : base + '\n ') +
+ ' ' +
+ output.join(',\n ') +
+ ' ' +
+ braces[1];
+ }
+
+ return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
+}
+
+function isArray(ar) {
+ return Array.isArray(ar) ||
+ (typeof ar === 'object' && objectToString(ar) === '[object Array]');
+}
+
+function isRegExp(re) {
+ return typeof re === 'object' && objectToString(re) === '[object RegExp]';
+}
+
+function isDate(d) {
+ return typeof d === 'object' && objectToString(d) === '[object Date]';
+}
+
+function isError(e) {
+ return typeof e === 'object' && objectToString(e) === '[object Error]';
+}
+
+function objectToString(o) {
+ return Object.prototype.toString.call(o);
+}
+
+},{"./getEnumerableProperties":13,"./getName":15,"./getProperties":18}],22:[function(require,module,exports){
+/*!
+ * Chai - flag utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependancies
+ */
+
+var inspect = require('./inspect');
+var config = require('../config');
+
+/**
+ * ### .objDisplay (object)
+ *
+ * Determines if an object or an array matches
+ * criteria to be inspected in-line for error
+ * messages or should be truncated.
+ *
+ * @param {Mixed} javascript object to inspect
+ * @name objDisplay
+ * @api public
+ */
+
+module.exports = function (obj) {
+ var str = inspect(obj)
+ , type = Object.prototype.toString.call(obj);
+
+ if (config.truncateThreshold && str.length >= config.truncateThreshold) {
+ if (type === '[object Function]') {
+ return !obj.name || obj.name === ''
+ ? '[Function]'
+ : '[Function: ' + obj.name + ']';
+ } else if (type === '[object Array]') {
+ return '[ Array(' + obj.length + ') ]';
+ } else if (type === '[object Object]') {
+ var keys = Object.keys(obj)
+ , kstr = keys.length > 2
+ ? keys.splice(0, 2).join(', ') + ', ...'
+ : keys.join(', ');
+ return '{ Object (' + kstr + ') }';
+ } else {
+ return str;
+ }
+ } else {
+ return str;
+ }
+};
+
+},{"../config":3,"./inspect":21}],23:[function(require,module,exports){
+/*!
+ * Chai - overwriteChainableMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### overwriteChainableMethod (ctx, name, method, chainingBehavior)
+ *
+ * Overwites an already existing chainable method
+ * and provides access to the previous function or
+ * property. Must return functions to be used for
+ * name.
+ *
+ * utils.overwriteChainableMethod(chai.Assertion.prototype, 'length',
+ * function (_super) {
+ * }
+ * , function (_super) {
+ * }
+ * );
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ * chai.Assertion.overwriteChainableMethod('foo', fn, fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ * expect(myFoo).to.have.length(3);
+ * expect(myFoo).to.have.length.above(3);
+ *
+ * @param {Object} ctx object whose method / property is to be overwritten
+ * @param {String} name of method / property to overwrite
+ * @param {Function} method function that returns a function to be used for name
+ * @param {Function} chainingBehavior function that returns a function to be used for property
+ * @name overwriteChainableMethod
+ * @api public
+ */
+
+module.exports = function (ctx, name, method, chainingBehavior) {
+ var chainableBehavior = ctx.__methods[name];
+
+ var _chainingBehavior = chainableBehavior.chainingBehavior;
+ chainableBehavior.chainingBehavior = function () {
+ var result = chainingBehavior(_chainingBehavior).call(this);
+ return result === undefined ? this : result;
+ };
+
+ var _method = chainableBehavior.method;
+ chainableBehavior.method = function () {
+ var result = method(_method).apply(this, arguments);
+ return result === undefined ? this : result;
+ };
+};
+
+},{}],24:[function(require,module,exports){
+/*!
+ * Chai - overwriteMethod utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### overwriteMethod (ctx, name, fn)
+ *
+ * Overwites an already existing method and provides
+ * access to previous function. Must return function
+ * to be used for name.
+ *
+ * utils.overwriteMethod(chai.Assertion.prototype, 'equal', function (_super) {
+ * return function (str) {
+ * var obj = utils.flag(this, 'object');
+ * if (obj instanceof Foo) {
+ * new chai.Assertion(obj.value).to.equal(str);
+ * } else {
+ * _super.apply(this, arguments);
+ * }
+ * }
+ * });
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ * chai.Assertion.overwriteMethod('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ * expect(myFoo).to.equal('bar');
+ *
+ * @param {Object} ctx object whose method is to be overwritten
+ * @param {String} name of method to overwrite
+ * @param {Function} method function that returns a function to be used for name
+ * @name overwriteMethod
+ * @api public
+ */
+
+module.exports = function (ctx, name, method) {
+ var _method = ctx[name]
+ , _super = function () { return this; };
+
+ if (_method && 'function' === typeof _method)
+ _super = _method;
+
+ ctx[name] = function () {
+ var result = method(_super).apply(this, arguments);
+ return result === undefined ? this : result;
+ }
+};
+
+},{}],25:[function(require,module,exports){
+/*!
+ * Chai - overwriteProperty utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### overwriteProperty (ctx, name, fn)
+ *
+ * Overwites an already existing property getter and provides
+ * access to previous value. Must return function to use as getter.
+ *
+ * utils.overwriteProperty(chai.Assertion.prototype, 'ok', function (_super) {
+ * return function () {
+ * var obj = utils.flag(this, 'object');
+ * if (obj instanceof Foo) {
+ * new chai.Assertion(obj.name).to.equal('bar');
+ * } else {
+ * _super.call(this);
+ * }
+ * }
+ * });
+ *
+ *
+ * Can also be accessed directly from `chai.Assertion`.
+ *
+ * chai.Assertion.overwriteProperty('foo', fn);
+ *
+ * Then can be used as any other assertion.
+ *
+ * expect(myFoo).to.be.ok;
+ *
+ * @param {Object} ctx object whose property is to be overwritten
+ * @param {String} name of property to overwrite
+ * @param {Function} getter function that returns a getter function to be used for name
+ * @name overwriteProperty
+ * @api public
+ */
+
+module.exports = function (ctx, name, getter) {
+ var _get = Object.getOwnPropertyDescriptor(ctx, name)
+ , _super = function () {};
+
+ if (_get && 'function' === typeof _get.get)
+ _super = _get.get
+
+ Object.defineProperty(ctx, name,
+ { get: function () {
+ var result = getter(_super).call(this);
+ return result === undefined ? this : result;
+ }
+ , configurable: true
+ });
+};
+
+},{}],26:[function(require,module,exports){
+/*!
+ * Chai - test utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependancies
+ */
+
+var flag = require('./flag');
+
+/**
+ * # test(object, expression)
+ *
+ * Test and object for expression.
+ *
+ * @param {Object} object (constructed Assertion)
+ * @param {Arguments} chai.Assertion.prototype.assert arguments
+ */
+
+module.exports = function (obj, args) {
+ var negate = flag(obj, 'negate')
+ , expr = args[0];
+ return negate ? !expr : expr;
+};
+
+},{"./flag":11}],27:[function(require,module,exports){
+/*!
+ * Chai - transferFlags utility
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * ### transferFlags(assertion, object, includeAll = true)
+ *
+ * Transfer all the flags for `assertion` to `object`. If
+ * `includeAll` is set to `false`, then the base Chai
+ * assertion flags (namely `object`, `ssfi`, and `message`)
+ * will not be transferred.
+ *
+ *
+ * var newAssertion = new Assertion();
+ * utils.transferFlags(assertion, newAssertion);
+ *
+ * var anotherAsseriton = new Assertion(myObj);
+ * utils.transferFlags(assertion, anotherAssertion, false);
+ *
+ * @param {Assertion} assertion the assertion to transfer the flags from
+ * @param {Object} object the object to transfer the flags to; usually a new assertion
+ * @param {Boolean} includeAll
+ * @name transferFlags
+ * @api private
+ */
+
+module.exports = function (assertion, object, includeAll) {
+ var flags = assertion.__flags || (assertion.__flags = Object.create(null));
+
+ if (!object.__flags) {
+ object.__flags = Object.create(null);
+ }
+
+ includeAll = arguments.length === 3 ? includeAll : true;
+
+ for (var flag in flags) {
+ if (includeAll ||
+ (flag !== 'object' && flag !== 'ssfi' && flag != 'message')) {
+ object.__flags[flag] = flags[flag];
+ }
+ }
+};
+
+},{}],28:[function(require,module,exports){
+/*!
+ * assertion-error
+ * Copyright(c) 2013 Jake Luer <jake@qualiancy.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Return a function that will copy properties from
+ * one object to another excluding any originally
+ * listed. Returned function will create a new `{}`.
+ *
+ * @param {String} excluded properties ...
+ * @return {Function}
+ */
+
+function exclude () {
+ var excludes = [].slice.call(arguments);
+
+ function excludeProps (res, obj) {
+ Object.keys(obj).forEach(function (key) {
+ if (!~excludes.indexOf(key)) res[key] = obj[key];
+ });
+ }
+
+ return function extendExclude () {
+ var args = [].slice.call(arguments)
+ , i = 0
+ , res = {};
+
+ for (; i < args.length; i++) {
+ excludeProps(res, args[i]);
+ }
+
+ return res;
+ };
+}
+
+/*!
+ * Primary Exports
+ */
+
+module.exports = AssertionError;
+
+/**
+ * ### AssertionError
+ *
+ * An extension of the JavaScript `Error` constructor for
+ * assertion and validation scenarios.
+ *
+ * @param {String} message
+ * @param {Object} properties to include (optional)
+ * @param {callee} start stack function (optional)
+ */
+
+function AssertionError (message, _props, ssf) {
+ var extend = exclude('name', 'message', 'stack', 'constructor', 'toJSON')
+ , props = extend(_props || {});
+
+ // default values
+ this.message = message || 'Unspecified AssertionError';
+ this.showDiff = false;
+
+ // copy from properties
+ for (var key in props) {
+ this[key] = props[key];
+ }
+
+ // capture stack trace
+ ssf = ssf || arguments.callee;
+ if (ssf && Error.captureStackTrace) {
+ Error.captureStackTrace(this, ssf);
+ } else {
+ this.stack = new Error().stack;
+ }
+}
+
+/*!
+ * Inherit from Error.prototype
+ */
+
+AssertionError.prototype = Object.create(Error.prototype);
+
+/*!
+ * Statically set name
+ */
+
+AssertionError.prototype.name = 'AssertionError';
+
+/*!
+ * Ensure correct constructor
+ */
+
+AssertionError.prototype.constructor = AssertionError;
+
+/**
+ * Allow errors to be converted to JSON for static transfer.
+ *
+ * @param {Boolean} include stack (default: `true`)
+ * @return {Object} object that can be `JSON.stringify`
+ */
+
+AssertionError.prototype.toJSON = function (stack) {
+ var extend = exclude('constructor', 'toJSON', 'stack')
+ , props = extend({ name: this.name }, this);
+
+ // include stack if exists and not turned off
+ if (false !== stack && this.stack) {
+ props.stack = this.stack;
+ }
+
+ return props;
+};
+
+},{}],29:[function(require,module,exports){
+module.exports = require('./lib/eql');
+
+},{"./lib/eql":30}],30:[function(require,module,exports){
+/*!
+ * deep-eql
+ * Copyright(c) 2013 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Module dependencies
+ */
+
+var type = require('type-detect');
+
+/*!
+ * Buffer.isBuffer browser shim
+ */
+
+var Buffer;
+try { Buffer = require('buffer').Buffer; }
+catch(ex) {
+ Buffer = {};
+ Buffer.isBuffer = function() { return false; }
+}
+
+/*!
+ * Primary Export
+ */
+
+module.exports = deepEqual;
+
+/**
+ * Assert super-strict (egal) equality between
+ * two objects of any type.
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @param {Array} memoised (optional)
+ * @return {Boolean} equal match
+ */
+
+function deepEqual(a, b, m) {
+ if (sameValue(a, b)) {
+ return true;
+ } else if ('date' === type(a)) {
+ return dateEqual(a, b);
+ } else if ('regexp' === type(a)) {
+ return regexpEqual(a, b);
+ } else if (Buffer.isBuffer(a)) {
+ return bufferEqual(a, b);
+ } else if ('arguments' === type(a)) {
+ return argumentsEqual(a, b, m);
+ } else if (!typeEqual(a, b)) {
+ return false;
+ } else if (('object' !== type(a) && 'object' !== type(b))
+ && ('array' !== type(a) && 'array' !== type(b))) {
+ return sameValue(a, b);
+ } else {
+ return objectEqual(a, b, m);
+ }
+}
+
+/*!
+ * Strict (egal) equality test. Ensures that NaN always
+ * equals NaN and `-0` does not equal `+0`.
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @return {Boolean} equal match
+ */
+
+function sameValue(a, b) {
+ if (a === b) return a !== 0 || 1 / a === 1 / b;
+ return isNaN(a) && isNaN(b);
+}
+
+/*!
+ * Compare the types of two given objects and
+ * return if they are equal. Note that an Array
+ * has a type of `array` (not `object`) and arguments
+ * have a type of `arguments` (not `array`/`object`).
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @return {Boolean} result
+ */
+
+function typeEqual(a, b) {
+ return type(a) === type(b);
+}
+
+/*!
+ * Compare two Date objects by asserting that
+ * the time values are equal using `saveValue`.
+ *
+ * @param {Date} a
+ * @param {Date} b
+ * @return {Boolean} result
+ */
+
+function dateEqual(a, b) {
+ if ('date' !== type(b)) return false;
+ return sameValue(a.getTime(), b.getTime());
+}
+
+/*!
+ * Compare two regular expressions by converting them
+ * to string and checking for `sameValue`.
+ *
+ * @param {RegExp} a
+ * @param {RegExp} b
+ * @return {Boolean} result
+ */
+
+function regexpEqual(a, b) {
+ if ('regexp' !== type(b)) return false;
+ return sameValue(a.toString(), b.toString());
+}
+
+/*!
+ * Assert deep equality of two `arguments` objects.
+ * Unfortunately, these must be sliced to arrays
+ * prior to test to ensure no bad behavior.
+ *
+ * @param {Arguments} a
+ * @param {Arguments} b
+ * @param {Array} memoize (optional)
+ * @return {Boolean} result
+ */
+
+function argumentsEqual(a, b, m) {
+ if ('arguments' !== type(b)) return false;
+ a = [].slice.call(a);
+ b = [].slice.call(b);
+ return deepEqual(a, b, m);
+}
+
+/*!
+ * Get enumerable properties of a given object.
+ *
+ * @param {Object} a
+ * @return {Array} property names
+ */
+
+function enumerable(a) {
+ var res = [];
+ for (var key in a) res.push(key);
+ return res;
+}
+
+/*!
+ * Simple equality for flat iterable objects
+ * such as Arrays or Node.js buffers.
+ *
+ * @param {Iterable} a
+ * @param {Iterable} b
+ * @return {Boolean} result
+ */
+
+function iterableEqual(a, b) {
+ if (a.length !== b.length) return false;
+
+ var i = 0;
+ var match = true;
+
+ for (; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ match = false;
+ break;
+ }
+ }
+
+ return match;
+}
+
+/*!
+ * Extension to `iterableEqual` specifically
+ * for Node.js Buffers.
+ *
+ * @param {Buffer} a
+ * @param {Mixed} b
+ * @return {Boolean} result
+ */
+
+function bufferEqual(a, b) {
+ if (!Buffer.isBuffer(b)) return false;
+ return iterableEqual(a, b);
+}
+
+/*!
+ * Block for `objectEqual` ensuring non-existing
+ * values don't get in.
+ *
+ * @param {Mixed} object
+ * @return {Boolean} result
+ */
+
+function isValue(a) {
+ return a !== null && a !== undefined;
+}
+
+/*!
+ * Recursively check the equality of two objects.
+ * Once basic sameness has been established it will
+ * defer to `deepEqual` for each enumerable key
+ * in the object.
+ *
+ * @param {Mixed} a
+ * @param {Mixed} b
+ * @return {Boolean} result
+ */
+
+function objectEqual(a, b, m) {
+ if (!isValue(a) || !isValue(b)) {
+ return false;
+ }
+
+ if (a.prototype !== b.prototype) {
+ return false;
+ }
+
+ var i;
+ if (m) {
+ for (i = 0; i < m.length; i++) {
+ if ((m[i][0] === a && m[i][1] === b)
+ || (m[i][0] === b && m[i][1] === a)) {
+ return true;
+ }
+ }
+ } else {
+ m = [];
+ }
+
+ try {
+ var ka = enumerable(a);
+ var kb = enumerable(b);
+ } catch (ex) {
+ return false;
+ }
+
+ ka.sort();
+ kb.sort();
+
+ if (!iterableEqual(ka, kb)) {
+ return false;
+ }
+
+ m.push([ a, b ]);
+
+ var key;
+ for (i = ka.length - 1; i >= 0; i--) {
+ key = ka[i];
+ if (!deepEqual(a[key], b[key], m)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+},{"buffer":undefined,"type-detect":31}],31:[function(require,module,exports){
+module.exports = require('./lib/type');
+
+},{"./lib/type":32}],32:[function(require,module,exports){
+/*!
+ * type-detect
+ * Copyright(c) 2013 jake luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Primary Exports
+ */
+
+exports = module.exports = getType;
+
+/*!
+ * Detectable javascript natives
+ */
+
+var natives = {
+ '[object Array]': 'array'
+ , '[object RegExp]': 'regexp'
+ , '[object Function]': 'function'
+ , '[object Arguments]': 'arguments'
+ , '[object Date]': 'date'
+};
+
+/**
+ * ### typeOf (obj)
+ *
+ * Use several different techniques to determine
+ * the type of object being tested.
+ *
+ *
+ * @param {Mixed} object
+ * @return {String} object type
+ * @api public
+ */
+
+function getType (obj) {
+ var str = Object.prototype.toString.call(obj);
+ if (natives[str]) return natives[str];
+ if (obj === null) return 'null';
+ if (obj === undefined) return 'undefined';
+ if (obj === Object(obj)) return 'object';
+ return typeof obj;
+}
+
+exports.Library = Library;
+
+/**
+ * ### Library
+ *
+ * Create a repository for custom type detection.
+ *
+ * ```js
+ * var lib = new type.Library;
+ * ```
+ *
+ */
+
+function Library () {
+ this.tests = {};
+}
+
+/**
+ * #### .of (obj)
+ *
+ * Expose replacement `typeof` detection to the library.
+ *
+ * ```js
+ * if ('string' === lib.of('hello world')) {
+ * // ...
+ * }
+ * ```
+ *
+ * @param {Mixed} object to test
+ * @return {String} type
+ */
+
+Library.prototype.of = getType;
+
+/**
+ * #### .define (type, test)
+ *
+ * Add a test to for the `.test()` assertion.
+ *
+ * Can be defined as a regular expression:
+ *
+ * ```js
+ * lib.define('int', /^[0-9]+$/);
+ * ```
+ *
+ * ... or as a function:
+ *
+ * ```js
+ * lib.define('bln', function (obj) {
+ * if ('boolean' === lib.of(obj)) return true;
+ * var blns = [ 'yes', 'no', 'true', 'false', 1, 0 ];
+ * if ('string' === lib.of(obj)) obj = obj.toLowerCase();
+ * return !! ~blns.indexOf(obj);
+ * });
+ * ```
+ *
+ * @param {String} type
+ * @param {RegExp|Function} test
+ * @api public
+ */
+
+Library.prototype.define = function (type, test) {
+ if (arguments.length === 1) return this.tests[type];
+ this.tests[type] = test;
+ return this;
+};
+
+/**
+ * #### .test (obj, test)
+ *
+ * Assert that an object is of type. Will first
+ * check natives, and if that does not pass it will
+ * use the user defined custom tests.
+ *
+ * ```js
+ * assert(lib.test('1', 'int'));
+ * assert(lib.test('yes', 'bln'));
+ * ```
+ *
+ * @param {Mixed} object
+ * @param {String} type
+ * @return {Boolean} result
+ * @api public
+ */
+
+Library.prototype.test = function (obj, type) {
+ if (type === getType(obj)) return true;
+ var test = this.tests[type];
+
+ if (test && 'regexp' === getType(test)) {
+ return test.test(obj);
+ } else if (test && 'function' === getType(test)) {
+ return test(obj);
+ } else {
+ throw new ReferenceError('Type test "' + type + '" not defined or invalid.');
+ }
+};
+
+},{}],33:[function(require,module,exports){
+arguments[4][31][0].apply(exports,arguments)
+},{"./lib/type":34,"dup":31}],34:[function(require,module,exports){
+/*!
+ * type-detect
+ * Copyright(c) 2013 jake luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/*!
+ * Primary Exports
+ */
+
+exports = module.exports = getType;
+
+/**
+ * ### typeOf (obj)
+ *
+ * Use several different techniques to determine
+ * the type of object being tested.
+ *
+ *
+ * @param {Mixed} object
+ * @return {String} object type
+ * @api public
+ */
+var objectTypeRegexp = /^\[object (.*)\]$/;
+
+function getType(obj) {
+ var type = Object.prototype.toString.call(obj).match(objectTypeRegexp)[1].toLowerCase();
+ // Let "new String('')" return 'object'
+ if (typeof Promise === 'function' && obj instanceof Promise) return 'promise';
+ // PhantomJS has type "DOMWindow" for null
+ if (obj === null) return 'null';
+ // PhantomJS has type "DOMWindow" for undefined
+ if (obj === undefined) return 'undefined';
+ return type;
+}
+
+exports.Library = Library;
+
+/**
+ * ### Library
+ *
+ * Create a repository for custom type detection.
+ *
+ * ```js
+ * var lib = new type.Library;
+ * ```
+ *
+ */
+
+function Library() {
+ if (!(this instanceof Library)) return new Library();
+ this.tests = {};
+}
+
+/**
+ * #### .of (obj)
+ *
+ * Expose replacement `typeof` detection to the library.
+ *
+ * ```js
+ * if ('string' === lib.of('hello world')) {
+ * // ...
+ * }
+ * ```
+ *
+ * @param {Mixed} object to test
+ * @return {String} type
+ */
+
+Library.prototype.of = getType;
+
+/**
+ * #### .define (type, test)
+ *
+ * Add a test to for the `.test()` assertion.
+ *
+ * Can be defined as a regular expression:
+ *
+ * ```js
+ * lib.define('int', /^[0-9]+$/);
+ * ```
+ *
+ * ... or as a function:
+ *
+ * ```js
+ * lib.define('bln', function (obj) {
+ * if ('boolean' === lib.of(obj)) return true;
+ * var blns = [ 'yes', 'no', 'true', 'false', 1, 0 ];
+ * if ('string' === lib.of(obj)) obj = obj.toLowerCase();
+ * return !! ~blns.indexOf(obj);
+ * });
+ * ```
+ *
+ * @param {String} type
+ * @param {RegExp|Function} test
+ * @api public
+ */
+
+Library.prototype.define = function(type, test) {
+ if (arguments.length === 1) return this.tests[type];
+ this.tests[type] = test;
+ return this;
+};
+
+/**
+ * #### .test (obj, test)
+ *
+ * Assert that an object is of type. Will first
+ * check natives, and if that does not pass it will
+ * use the user defined custom tests.
+ *
+ * ```js
+ * assert(lib.test('1', 'int'));
+ * assert(lib.test('yes', 'bln'));
+ * ```
+ *
+ * @param {Mixed} object
+ * @param {String} type
+ * @return {Boolean} result
+ * @api public
+ */
+
+Library.prototype.test = function(obj, type) {
+ if (type === getType(obj)) return true;
+ var test = this.tests[type];
+
+ if (test && 'regexp' === getType(test)) {
+ return test.test(obj);
+ } else if (test && 'function' === getType(test)) {
+ return test(obj);
+ } else {
+ throw new ReferenceError('Type test "' + type + '" not defined or invalid.');
+ }
+};
+
+},{}],35:[function(require,module,exports){
+module.exports = require('./lib/chai');
+
+},{"./lib/chai":1}]},{},[35])(35)
+});
diff --git a/toolkit/components/microformats/test/static/javascript/count.js b/toolkit/components/microformats/test/static/javascript/count.js
new file mode 100644
index 0000000000..56a64c05ea
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/count.js
@@ -0,0 +1,62 @@
+/*!
+ parse
+ Used by http://localhost:3000/
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+window.onload = function() {
+
+ var form;
+ form= document.getElementById('mf-form');
+
+ form.onsubmit = function(e){
+ e.preventDefault();
+
+ var html,
+ doc,
+ node,
+ options,
+ mfJSON,
+ parserJSONElt;
+
+ // get data from html
+ html = document.getElementById('html').value;
+ parserJSONElt = document.querySelector('#parser-json pre code')
+
+ // createHTMLDocument is not well support below ie9
+ doc = document.implementation.createHTMLDocument("New Document");
+ node = document.createElement('div');
+ node.innerHTML = html;
+ doc.body.appendChild(node);
+
+ options ={
+ 'node': node
+ };
+
+ // parse direct into Modules to help debugging
+ if(window.Modules){
+ var parser = new Modules.Parser();
+ mfJSON = parser.count(options);
+ }else if(window.Microformats){
+ mfJSON = Microformats.count(options);
+ }
+
+
+ // format output
+ parserJSONElt.innerHTML = htmlEscape( js_beautify( JSON.stringify(mfJSON) ) );
+ //prettyPrint();
+
+ }
+
+ function htmlEscape(str) {
+ return String(str)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ }
+
+
+};
diff --git a/toolkit/components/microformats/test/static/javascript/data.js b/toolkit/components/microformats/test/static/javascript/data.js
new file mode 100644
index 0000000000..3f725c6db2
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/data.js
@@ -0,0 +1 @@
+var testData = {"date":"2015-09-25T12:26:26.421Z","repo":"microformats/tests","version":"0.1.24","data":[{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"org\": [\"Mozilla Foundation\"],\n \"url\": [\"http://mozilla.org/\"],\n \"adr\": [{\n \"value\": \"665 3rd St. \\n Suite 207 \\n San Francisco, \\n CA \\n 94107 \\n U.S.A.\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"665 3rd St.\"],\n \"extended-address\": [\"Suite 207\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94107\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-card\">\n <p>\n <a class=\"p-name p-org u-url\" href=\"http://mozilla.org/\">Mozilla Foundation</a>\n <img class=\"logo\" src=\"../logo.jpg\"/>\n </p>\n <p class=\"adr\">\n <span class=\"street-address\">665 3rd St.</span> \n <span class=\"extended-address\">Suite 207</span> \n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n <span class=\"postal-code\">94107</span> \n <span class=\"p-country-name\">U.S.A.</span> \n </p>\n</div>","name":"mf-mixed-h-card-mixedpropertries"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Frances Berriman\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-card vcard\">Frances Berriman</p>","name":"mf-mixed-h-card-tworoots"},{"json":"\n{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"author\": [{\n \"value\": \"aaronparecki.com\\n Aaron Parecki\\n Aaron Parecki\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"photo\": [\"https://aaronparecki.com/images/aaronpk.png\"],\n \"logo\": [\"https://aaronparecki.com/images/aaronpk.png\"],\n \"url\": [\"https://aaronparecki.com/\"],\n \"uid\": [\"https://aaronparecki.com/\"],\n \"name\": [\"Aaron Parecki\"]\n }\n }],\n \"content\": [{\n \"value\": \"Did you play\\n @playmapattackat\\n #realtimeconf? Here is some more info about how we built it!\\n http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/\",\n \"html\": \"Did you play\\n <a href=\\\"http://twitter.com/playmapattack\\\">@playmapattack</a>at\\n <a href=\\\"http://aaronparecki.com/tag/realtimeconf\\\">#<span class=\\\"p-category\\\">realtimeconf</span></a>? Here is some more info about how we built it!\\n <a href=\\\"http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/\\\"><span class=\\\"protocol\\\">http://</span>pdx.esri.com/blog/2013/10/17/introducting-mapattack/</a>\\n \"\n }],\n \"name\": [\"Did you play\\n @playmapattackat\\n #realtimeconf? Here is some more info about how we built it!\\n http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/\"],\n \"category\": [\"realtimeconf\"]\n }\n }],\n \"rels\": {\n \"author\": [\"https://aaronparecki.com/\", \"https://plus.google.com/117847912875913905493\"]\n },\n \"rel-urls\": {\n \"https://aaronparecki.com/\": {\n \"text\": \"aaronparecki.com\",\n \"rels\": [\"author\"]\n },\n \"https://plus.google.com/117847912875913905493\": {\n \"text\": \"Aaron Parecki\",\n \"rels\": [\"author\"]\n }\n }\n}","html":"<!-- simplified version of http://aaronparecki.com/notes/2013/10/18/2/realtimeconf-mapattack -->\n<base href=\"http://aaronparecki.com/\" />\n\n<div class=\"h-entry\">\n <div class=\"h-card vcard author p-author\">\n <img class=\"photo logo u-photo u-logo\" src=\"https://aaronparecki.com/images/aaronpk.png\" alt=\"Aaron Parecki\"/>\n <a href=\"https://aaronparecki.com/\" rel=\"author\" class=\"u-url u-uid url\">aaronparecki.com</a>\n <a class=\"p-name fn value\" href=\"https://aaronparecki.com/\">Aaron Parecki</a>\n <a href=\"https://plus.google.com/117847912875913905493\" rel=\"author\" class=\"google-profile\">Aaron Parecki</a>\n </div>\n <div class=\"entry-content e-content p-name\">Did you play\n <a href=\"http://twitter.com/playmapattack\">@playmapattack</a>at\n <a href=\"/tag/realtimeconf\">#<span class=\"p-category\">realtimeconf</span></a>? Here is some more info about how we built it!\n <a href=\"http://pdx.esri.com/blog/2013/10/17/introducting-mapattack/\"><span class=\"protocol\">http://</span>pdx.esri.com/blog/2013/10/17/introducting-mapattack/</a>\n </div>\n</div>","name":"mf-mixed-h-entry-mixedroots"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"contact\": [{\n \"value\": \"Tim Berners-Lee\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"job-title\": [\"Director of the World Wide Web Foundation\"]\n }\n }],\n \"summary\": [\"Invented the World Wide Web.\"],\n \"experience\": [{\n \"value\": \"World Wide Web Foundation\",\n \"type\": [\"h-event\", \"h-card\"],\n \"properties\": {\n \"job-title\": [\"Director\"],\n \"name\": [\"World Wide Web Foundation\"],\n \"org\": [\"World Wide Web Foundation\"],\n \"url\": [\"http://www.webfoundation.org/\"],\n \"start\": [\"2009-01-18\"],\n \"duration\": [\"P2Y11M\"]\n }\n }],\n \"name\": [\"Tim Berners-Lee\\n Director of the World Wide Web Foundation\\n \\n Invented the World Wide Web.\\n \\n Director\\n World Wide Web Foundation\\n \\n Jan 2009 – Present\\n (2 years 11 month)\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"h-resume\">\n <div class=\"p-contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <div class=\"p-experience vevent vcard\">\n <p class=\"title\">Director</p>\n <p><a class=\"fn org summary url\" href=\"http://www.webfoundation.org/\">World Wide Web Foundation</a></p>\n <p>\n <time class=\"dtstart\" datetime=\"2009-01-18\">Jan 2009</time> – Present\n <time class=\"duration\" datetime=\"P2Y11M\">(2 years 11 month)</time>\n </p>\n </div>\n</div>","name":"mf-mixed-h-resume-mixedroots"},{"json":"{\n \"items\": [{\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"665 3rd St.\"],\n \"extended-address\": [\"Suite 207\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94107\"],\n \"country-name\": [\"U.S.A.\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"adr\">\n <span class=\"street-address\">665 3rd St.</span> \n <span class=\"extended-address\">Suite 207</span> \n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n <span class=\"postal-code\">94107</span> \n <span class=\"country-name\">U.S.A.</span> \n</p>","name":"mf-v1-adr-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"37.408183\"],\n \"longitude\": [\"-122.13855\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<p class=\"geo\">\n <abbr class=\"latitude\" title=\"37.408183\">N 37° 24.491</abbr>, \n <abbr class=\"longitude\" title=\"-122.13855\">W 122° 08.313</abbr>\n</p>","name":"mf-v1-geo-abbrpattern"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"51.513458\"],\n \"longitude\": [\"-0.14812\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p>\n <span class=\"geo\">The Bricklayer's Arms\n <span class=\"latitude\">\n <span class=\"value-title\" title=\"51.513458\"> </span> \n </span>\n <span class=\"longitude\">\n <span class=\"value-title\" title=\"-0.14812\"> </span>\n </span>\n </span>\n</p>","name":"mf-v1-geo-hidden"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"51.513458\"],\n \"longitude\": [\"-0.14812\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"We are meeting at \n<span class=\"geo\"> \n <span>The Bricklayer's Arms</span>\n (Geo: <span class=\"latitude\">51.513458</span>:\n <span class=\"longitude\">-0.14812</span>)\n</span>","name":"mf-v1-geo-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"51.513458\"],\n \"longitude\": [\"-0.14812\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<p>\n <span class=\"geo\">\n <span class=\"latitude\">\n <span class=\"value-title\" title=\"51.513458\">N 51° 51.345</span>, \n </span>\n <span class=\"longitude\">\n <span class=\"value-title\" title=\"-0.14812\">W -0° 14.812</span>\n </span>\n </span>\n</p>","name":"mf-v1-geo-valuetitleclass"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The 4th Microformat party\"],\n \"start\": [\n \"2009-06-26 19:00:00\", \n \"2009-06-26 07:00:00\", \n \"2009-06-26 19:00\", \n \"2009-06-26 19\", \n \"2009-06-26 19\", \n \"2009-06-26 19:00\", \n \"2009-06-26 19:00\", \n \"2009-06-26 19:00\", \n \"2009-06-26 07:00\"\n ]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vevent\">\n <span class=\"summary\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00am \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00pm \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00p.m. \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00PM \n </span></li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00am \n </span></li>\n </ul>\n</div>","name":"mf-v1-hcalendar-ampm"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"CPJ Online Press Freedom Summit\"],\n \"start\": [\"2012-10-10\"],\n \"location\": [\"San Francisco\"],\n \"attendee\": [{\n \"value\": \"Brian Warner\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Brian Warner\"]\n }\n }, {\n \"value\": \"Kyle Machulis\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Kyle Machulis\"]\n }\n }, {\n \"value\": \"Tantek Çelik\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek Çelik\"]\n }\n }, {\n \"value\": \"Sid Sutter\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Sid Sutter\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"vevent\">\n <span class=\"summary\">CPJ Online Press Freedom Summit</span>\n (<time class=\"dtstart\" datetime=\"2012-10-10\">10 Nov 2012</time>) in\n <span class=\"location\">San Francisco</span>.\n Attendees:\n <ul>\n <li class=\"attendee vcard\"><span class=\"fn\">Brian Warner</span></li>\n <li class=\"attendee vcard\"><span class=\"fn\">Kyle Machulis</span></li>\n <li class=\"attendee vcard\"><span class=\"fn\">Tantek Çelik</span></li>\n <li class=\"attendee vcard\"><span class=\"fn\">Sid Sutter</span></li>\n </ul>\n</div>\n","name":"mf-v1-hcalendar-attendees"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"IndieWebCamp 2012\"],\n \"url\": [\"http://indiewebcamp.com/2012\"],\n \"start\": [\"2012-06-30\"],\n \"end\": [\"2012-07-01\"],\n \"location\": [{\n \"value\": \"Geoloqi\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Geoloqi\"],\n \"org\": [\"Geoloqi\"],\n \"url\": [\"http://geoloqi.com/\"],\n \"adr\": [{\n \"value\": \"920 SW 3rd Ave. Suite 400, \\n Portland, \\n OR\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"920 SW 3rd Ave. Suite 400\"],\n \"locality\": [\"Portland\"],\n \"region\": [\"Oregon\"]\n }\n }]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vevent\">\n <a class=\"summary url\" href=\"http://indiewebcamp.com/2012\">\n IndieWebCamp 2012\n </a>\n from <time class=\"dtstart\">2012-06-30</time> \n to <time class=\"dtend\">2012-07-01</time> at \n <span class=\"location vcard\">\n <a class=\"fn org url\" href=\"http://geoloqi.com/\">Geoloqi</a>, \n <span class=\"adr\">\n <span class=\"street-address\">920 SW 3rd Ave. Suite 400</span>, \n <span class=\"locality\">Portland</span>, \n <abbr class=\"region\" title=\"Oregon\">OR</abbr>\n </span>\n </span>\n</div>","name":"mf-v1-hcalendar-combining"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The 4th Microformat party\"],\n \"start\": [\"2009-06-26 19:00\"],\n \"end\": [\"2009-06-26 22:00\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vevent\">\n <span class=\"summary\">The 4th Microformat party</span> will be on \n <span class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time></span> to \n <span class=\"dtend\"><time class=\"value\">22:00</time></span>.\n</div>","name":"mf-v1-hcalendar-concatenate"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The 4th Microformat party\"],\n \"start\": [\n \"2009-06-26 19:00:00-08:00\", \n \"2009-06-26 19:00:00-08:00\", \n \"2009-06-26 19:00:00+08:00\", \n \"2009-06-26 19:00:00Z\", \n \"2009-06-26 19:00:00\", \n \"2009-06-26 19:00-08:00\", \n \"2009-06-26 19:00+08:00\", \n \"2009-06-26 19:00Z\", \n \"2009-06-26 19:00\"\n ],\n \"end\": [\"2013-034\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vevent\">\n <span class=\"summary\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-08:00</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-0800</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00+0800</time> \n </li> \n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00Z</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00-08:00</time> \n </li> \n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00+08:00</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00z</time> \n </li>\n <li class=\"dtstart\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time> \n </li> \n <li>\n <time class=\"dtend\" datetime=\"2013-034\">3 February 2013</time>\n </li> \n </ul>\n</div>","name":"mf-v1-hcalendar-time"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John Doe\"],\n \"email\": [\"mailto:john@example.com\", \"mailto:john@example.com\", \"mailto:john@example.com?subject=parser-test\", \"john@example.com\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vcard\">\n <span class=\"fn\">John Doe</span> \n <ul>\n <li><a class=\"email\" href=\"mailto:john@example.com\">notthis@example.com</a></li>\n <li>\n <span class=\"email\">\n <span class=\"type\">internet</span> \n <a class=\"value\" href=\"mailto:john@example.com\">notthis@example.com</a>\n </span>\n </li> \n <li><a class=\"email\" href=\"mailto:john@example.com?subject=parser-test\">notthis@example.com</a></li>\n <li class=\"email\">john@example.com</li>\n </ul>\n</div>","name":"mf-v1-hcard-email"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John \\n Doe\"],\n \"given-name\": [\"John\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"vcard\">\n <span class=\"profile-name fn n\">\n <span class=\" given-name \">John</span> \n <span class=\"FAMILY-NAME\">Doe</span> \n </span>\n</p>","name":"mf-v1-hcard-format"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {}\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"vcard\" href=\"http://rohit.khare.org/\">\n <img alt=\"Rohit Khare\" src=\"images/photo.gif\" />\n</a>","name":"mf-v1-hcard-hyperlinkedphoto"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {}\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"vcard\" href=\"http://benward.me/\">Ben Ward</a>","name":"mf-v1-hcard-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {}\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"vcard\">Frances Berriman</p>","name":"mf-v1-hcard-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John Doe\"],\n \"given-name\": [\"John\"],\n \"family-name\": [\"Doe\"],\n \"sound\": [\"http://www.madgex.com/johndoe.mpeg\"],\n \"photo\": [\"http://example.com/images/photo.gif\"],\n \"nickname\": [\"Man with no name\", \"Lost boy\"],\n \"note\": [\"John Doe is one of those names you always have issues with.\", \"It can be a real problem booking a hotel room with the name John Doe.\"],\n \"logo\": [\"http://example.com/images/logo.gif\", \"http://example.com/images/logo.gif\"],\n \"url\": [\"http://www.madgex.com/\", \"http://www.webfeetmedia.com/\"],\n \"org\": [\"Madgex\", \"Web Feet Media Ltd\"],\n \"job-title\": [\"Creative Director\", \"Owner\"],\n \"category\": [\"design\", \"development\", \"web\"],\n \"tel\": [\"+1 415 555 100\", \"+1 415 555 200\", \"+1 415 555 300\"],\n \"email\": [\"mailto:john.doe@madgex.com\", \"mailto:john.doe@webfeetmedia.com\"],\n \"mailer\": [\"PigeonMail 2.1\", \"Outlook 2007\"],\n \"label\": [\"Work: \\n North Street, \\n Brighton, \\n United Kingdom\", \"Home: \\n West Street, \\n Brighton, \\n United Kingdom\"],\n \"adr\": [{\n \"value\": \"Work: \\n North Street, \\n Brighton, \\n United Kingdom\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"North Street\"],\n \"locality\": [\"Brighton\"],\n \"country-name\": [\"United Kingdom\"]\n }\n }, {\n \"value\": \"Home: \\n West Street, \\n Brighton, \\n United Kingdom\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"West Street\"],\n \"locality\": [\"Brighton\"],\n \"country-name\": [\"United Kingdom\"]\n }\n }],\n \"agent\": [\"Jane Doe\", {\n \"value\": \"Dave Doe\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Dave Doe\"]\n }\n }],\n \"key\": [\"hd02$Gfu*d%dh87KTa2=23934532479\"]\n }\n }],\n \"rels\": {\n \"tag\": [\"http://en.wikipedia.org/wiki/design\", \"http://en.wikipedia.org/wiki/development\", \"http://en.wikipedia.org/wiki/web\"]\n },\n \"rel-urls\": {\n \"http://en.wikipedia.org/wiki/design\": {\n \"text\": \"design\",\n \"rels\": [\"tag\"]\n },\n \"http://en.wikipedia.org/wiki/development\": {\n \"text\": \"development\",\n \"rels\": [\"tag\"]\n },\n \"http://en.wikipedia.org/wiki/web\": {\n \"text\": \"web\",\n \"rels\": [\"tag\"]\n }\n }\n}","html":"<base href=\"http://example.com\">\n <div class=\"vcard\">\n \n <div class=\"fn n\"><span class=\"given-name\">John</span> <span class=\"family-name\">Doe</span></div>\n <a class=\"sound\" href=\"http://www.madgex.com/johndoe.mpeg\">Pronunciation of my name</a>\n <div><img class=\"photo\" src=\"images/photo.gif\" alt=\"Photo of John Doe\" /></div>\n\n <p>Nicknames:</p>\n <ul>\n <li class=\"nickname\">Man with no name</li>\n <li class=\"nickname\">Lost boy</li>\n </ul>\n\n <p>About:</p>\n <p class=\"note\">John Doe is one of those names you always have issues with.</p>\n <p class=\"note\">It can be a real problem booking a hotel room with the name John Doe.</p>\n\n <p>Companies:</p>\n <div>\n <img class=\"logo\" src=\"images/logo.gif\" alt=\"Madgex company logo\" />\n <img class=\"logo\" src=\"images/logo.gif\" alt=\"Web Feet Media company logo\" />\n </div>\n <ul>\n <li><a class=\"url org\" href=\"http://www.madgex.com/\">Madgex</a> <span class=\"title\">Creative Director</span></li>\n <li><a class=\"url org\" href=\"http://www.webfeetmedia.com/\">Web Feet Media Ltd</a> <span class=\"title\">Owner</span></li>\n </ul>\n \n <p>Tags: \n <a rel=\"tag\" class=\"category\" href=\"http://en.wikipedia.org/wiki/design\">design</a>, \n <a rel=\"tag\" class=\"category\" href=\"http://en.wikipedia.org/wiki/development\">development</a> and\n <a rel=\"tag\" class=\"category\" href=\"http://en.wikipedia.org/wiki/web\">web</a>\n </p>\n \n <p>Phone numbers:</p>\n <ul>\n <li class=\"tel\">\n <span class=\"type\">Work</span> (<span class=\"type\">pref</span>erred):\n <span class=\"value\">+1 415 555 100</span>\n </li>\n <li class=\"tel\"><span class=\"type\">Home</span>: <span class=\"value\">+1 415 555 200</span></li>\n <li class=\"tel\"><span class=\"type\">Postal</span>: <span class=\"value\">+1 415 555 300</span></li>\n </ul>\n \n <p>Emails:</p>\n <ul>\n <li><a class=\"email\" href=\"mailto:john.doe@madgex.com\">John Doe at Madgex</a></li>\n <li><a class=\"email\" href=\"mailto:john.doe@webfeetmedia.com\">John Doe at Web Feet Media</a></li>\n </ul>\n <p>John Doe uses <span class=\"mailer\">PigeonMail 2.1</span> or <span class=\"mailer\">Outlook 2007</span> for email.</p>\n\n <p>Addresses:</p>\n <ul>\n <li class=\"label\">\n <span class=\"adr\">\n <span class=\"type\">Work</span>: \n <span class=\"street-address\">North Street</span>, \n <span class=\"locality\">Brighton</span>, \n <span class=\"country-name\">United Kingdom</span>\n </span>\n \n </li>\n <li class=\"label\">\n <span class=\"adr\">\n <span class=\"type\">Home</span>: \n <span class=\"street-address\">West Street</span>, \n <span class=\"locality\">Brighton</span>, \n <span class=\"country-name\">United Kingdom</span>\n </span>\n </li>\n </ul>\n \n <p>In emergency contact: <span class=\"agent\">Jane Doe</span> or <span class=\"agent vcard\"><span class=\"fn\">Dave Doe</span></span>.</p>\n <p>Key: <span class=\"key\">hd02$Gfu*d%dh87KTa2=23934532479</span></p>\n</div>","name":"mf-v1-hcard-multiple"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"honorific-prefix\": [\"Dr\"],\n \"given-name\": [\"John\"],\n \"additional-name\": [\"Peter\"],\n \"family-name\": [\"Doe\"],\n \"honorific-suffix\": [\"MSc\", \"PHD\"],\n \"photo\": [\"http://example.com/images/logo.gif\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<base href=\"http://example.com\">\n<div class=\"vcard\">\n <div class=\"name\">\n <span class=\"honorific-prefix\">Dr</span> \n <span class=\"given-name\">John</span> \n <abbr class=\"additional-name\" title=\"Peter\">P</abbr> \n <span class=\"family-name\">Doe</span> \n <data class=\"honorific-suffix\" value=\"MSc\"></data>\n <img class=\"photo honorific-suffix\" src=\"images/logo.gif\" alt=\"PHD\" />\n </div>\n</div>","name":"mf-v1-hcard-name"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John Doe\"],\n \"given-name\": [\"John\"],\n \"sort-string\": [\"John\"],\n \"bday\": [\"2000-01-01 00:00:00-08:00\"],\n \"role\": [\"Designer\"],\n \"geo\": [{\n \"value\": \"30.267991;-97.739568\",\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"name\": [\"30.267991;-97.739568\"]\n }\n }],\n \"tz\": [\"-05:00\"],\n \"uid\": [\"http://example.com/profiles/johndoe\"],\n \"class\": [\"Public\"],\n \"rev\": [\"2008-01-01 13:45:00\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vcard\">\n \n <div class=\"fn n\"><span class=\"given-name sort-string\">John</span> Doe</div>\n <div>Birthday: <abbr class=\"bday\" title=\"2000-01-01T00:00:00-08:00\">January 1st, 2000</abbr></div>\n <div>Role: <span class=\"role\">Designer</span></div>\n <div>Location: <abbr class=\"geo\" title=\"30.267991;-97.739568\">Brighton</abbr></div>\n <div>Time zone: <abbr class=\"tz\" title=\"-05:00\">Eastern Standard Time</abbr></div>\n \n <div>Profile details:\n <div>Profile id: <span class=\"uid\">http://example.com/profiles/johndoe</span></div>\n <div>Details are: <span class=\"class\">Public</span></div>\n <div>Last updated: <abbr class=\"rev\" title=\"2008-01-01T13:45:00\">January 1st, 2008 - 13:45</abbr></div>\n </div>\n </div>","name":"mf-v1-hcard-single"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service\",\n \"html\": \"\\n <p class=\\\"entry-summary\\\">Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.</p>\\n\\n <p>The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service </p>\\n \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"hentry\">\n <h1><a class=\"entry-title\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"entry-content\">\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n </p>\n</div>","name":"mf-v1-hentry-summarycontent"},{"json":"{\n \"items\": [{\n \"type\": [\"h-feed\"],\n \"properties\": {\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }],\n \"url\": [\"http://microformats.org/blog\"],\n \"photo\": [\"http://example.com/photo.jpeg\"],\n \"category\": [\"microformats\", \"html\"]\n },\n \"children\": [{\n \"value\": \"microformats.org at 7\\n\\t \\n\\t Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.\\n\\t\\n\\t The microformats tagline “humans first, machines second” \\n\\t forms the basis of many of our \\n\\t principles, and \\n\\t in that regard, we’d like to recognize a few people and \\n\\t thank them for their years of volunteer service \\n\\t \\n\\t Updated \\n\\t June 25th, 2012\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.\\n\\t\\n\\t The microformats tagline “humans first, machines second” \\n\\t forms the basis of many of our \\n\\t principles, and \\n\\t in that regard, we’d like to recognize a few people and \\n\\t thank them for their years of volunteer service\",\n \"html\": \"\\n\\t <p class=\\\"entry-summary\\\">Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.</p>\\n\\t\\n\\t <p>The microformats tagline “humans first, machines second” \\n\\t forms the basis of many of our \\n\\t <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n\\t in that regard, we’d like to recognize a few people and \\n\\t thank them for their years of volunteer service </p>\\n\\t \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"]\n }\n }]\n }],\n \"rels\": {\n \"tag\": [\"http://example.com/tags/microformats\", \"http://example.com/tags/html\"],\n \"bookmark\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"]\n },\n \"rel-urls\": {\n \"http://example.com/tags/microformats\": {\n \"text\": \"microformats\",\n \"rels\": [\"tag\"]\n },\n \"http://example.com/tags/html\": {\n \"text\": \"html\",\n \"rels\": [\"tag\"]\n },\n \"http://microformats.org/2012/06/25/microformats-org-at-7\": {\n \"text\": \"microformats.org at 7\",\n \"rels\": [\"bookmark\"]\n }\n }\n}","html":"<section class=\"hfeed\">\n\t<h1 class=\"name\">Microformats blog</h1>\n\t<span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n\t<a class=\"url\" href=\"http://microformats.org/blog\">permlink</a>\n\t<img class=\"photo\" src=\"photo.jpeg\"/>\n\t<p>\n\t\tTags: <a rel=\"tag\" href=\"tags/microformats\">microformats</a>, \n\t\t<a rel=\"tag\" href=\"tags/html\">html</a>\n\t</p>\n\t\n\t<div class=\"hentry\">\n\t <h1><a class=\"entry-title\" rel=\"bookmark\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n\t <div class=\"entry-content\">\n\t <p class=\"entry-summary\">Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.</p>\n\t\n\t <p>The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service </p>\n\t </div> \n\t <p>Updated \n\t <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time>\n\t </p>\n\t</div>\n\t\n</section>","name":"mf-v1-hfeed-simple"},{"json":"\n{\n \"items\": [{\n \"type\": [\"h-news\"],\n \"properties\": {\n \"entry\": [{\n \"value\": \"microformats.org at 7\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service\",\n \"html\": \"\\n <p class=\\\"entry-summary\\\">Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.</p>\\n\\n <p>The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service </p>\\n \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }]\n }\n }],\n \"dateline\": [{\n \"value\": \"San Francisco, \\n CA\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"adr\": [{\n \"value\": \"San Francisco, \\n CA\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"]\n }\n }]\n }\n }],\n \"geo\": [{\n \"value\": \"37.774921;-122.445202\",\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"name\": [\"37.774921;-122.445202\"]\n }\n }],\n \"source-org\": [{\n \"value\": \"microformats.org\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"microformats.org\"],\n \"org\": [\"microformats.org\"],\n \"url\": [\"http://microformats.org/\"]\n }\n }],\n \"principles\": [\"http://microformats.org/wiki/Category:public_domain_license\"]\n }\n }],\n \"rels\": {\n \"bookmark\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"principles\": [\"http://microformats.org/wiki/Category:public_domain_license\"]\n },\n \"rel-urls\": {\n \"http://microformats.org/2012/06/25/microformats-org-at-7\": {\n \"text\": \"microformats.org at 7\",\n \"rels\": [\"bookmark\"]\n },\n \"http://microformats.org/wiki/Category:public_domain_license\": {\n \"text\": \"Publishing policy\",\n \"rels\": [\"principles\"]\n }\n }\n}","html":"<div class=\"hnews\">\n <div class=\"entry hentry\">\n <h1><a class=\"entry-title\" rel=\"bookmark\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"entry-content\">\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n </p>\n </div>\n\n <p>\n <span class=\"dateline vcard\">\n <span class=\"adr\">\n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n </span>\n </span>\n (Geo: <span class=\"geo\">37.774921;-122.445202</span>) \n <span class=\"source-org vcard\">\n <a class=\"fn org url\" href=\"http://microformats.org/\">microformats.org</a>\n </span>\n </p>\n <p>\n <a rel=\"principles\" href=\"http://microformats.org/wiki/Category:public_domain_license\">Publishing policy</a>\n </p>\n</div>","name":"mf-v1-hnews-all"},{"json":"\n{\n \"items\": [{\n \"type\": [\"h-news\"],\n \"properties\": {\n \"entry\": [{\n \"value\": \"microformats.org at 7\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service\",\n \"html\": \"\\n <p class=\\\"entry-summary\\\">Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.</p>\\n\\n <p>The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service </p>\\n \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }]\n }\n }],\n \"source-org\": [{\n \"value\": \"microformats.org\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"microformats.org\"],\n \"org\": [\"microformats.org\"],\n \"url\": [\"http://microformats.org/\"]\n }\n }]\n }\n }],\n \"rels\": {\n \"bookmark\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"]\n },\n \"rel-urls\": {\n \"http://microformats.org/2012/06/25/microformats-org-at-7\": {\n \"text\": \"microformats.org at 7\",\n \"rels\": [\"bookmark\"]\n }\n }\n}","html":"<div class=\"hnews\">\n <div class=\"entry hentry\">\n <h1><a class=\"entry-title\" rel=\"bookmark\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"entry-content\">\n <p class=\"entry-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <span class=\"author vcard\"><a class=\"fn url\" href=\"http://tantek.com/\">Tantek</a></span>\n </p>\n </div>\n\n <p class=\"source-org vcard\">\n <a class=\"fn org url\" href=\"http://microformats.org/\">microformats.org</a> \n </p>\n</div>","name":"mf-v1-hnews-minimum"},{"json":"{\n \"items\": [{\n \"type\": [\"h-product\"],\n \"properties\": {\n \"name\": [\"Raspberry Pi\"],\n \"photo\": [\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\"],\n \"description\": [{\n \"value\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\",\n \"html\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\"\n }],\n \"url\": [\"http://www.raspberrypi.org/\"],\n \"price\": [\"£29.95\"],\n \"review\": [{\n \"value\": \"9.2 out of \\n 10 \\n based on 178 reviews\",\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"rating\": [\"9.2\"],\n \"average\": [\"9.2\"],\n \"best\": [\"10\"],\n \"count\": [\"178\"]\n }\n }],\n \"category\": [\"Computer\", \"Education\"],\n \"brand\": [{\n \"value\": \"The Raspberry Pi Foundation\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"The Raspberry Pi Foundation\"],\n \"org\": [\"The Raspberry Pi Foundation\"],\n \"adr\": [{\n \"value\": \"Cambridge \\n UK\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"Cambridge\"],\n \"country-name\": [\"UK\"]\n }\n }]\n }\n }]\n }\n }],\n \"rels\": {\n \"tag\": [\"http://en.wikipedia.org/wiki/computer\", \"http://en.wikipedia.org/wiki/education\"]\n },\n \"rel-urls\": {\n \"http://en.wikipedia.org/wiki/computer\": {\n \"text\": \"Computer\",\n \"rels\": [\"tag\"]\n },\n \"http://en.wikipedia.org/wiki/education\": {\n \"text\": \"Education\",\n \"rels\": [\"tag\"]\n }\n }\n}","html":"<meta charset=\"utf-8\">\n<div class=\"hproduct\">\n <h2 class=\"fn\">Raspberry Pi</h2>\n <img class=\"photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"price\">£29.95</p>\n <p class=\"review hreview-aggregate\">\n <span class=\"rating\">\n <span class=\"average value\">9.2</span> out of \n <span class=\"best\">10</span> \n based on <span class=\"count\">178</span> reviews\n </span>\n </p>\n <p>Categories: \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/computer\" class=\"category\">Computer</a>, \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/education\" class=\"category\">Education</a>\n </p>\n <p class=\"brand vcard\">From: \n <span class=\"fn org\">The Raspberry Pi Foundation</span> - \n <span class=\"adr\">\n <span class=\"locality\">Cambridge</span> \n <span class=\"country-name\">UK</span>\n </span>\n </p>\n</div>","name":"mf-v1-hproduct-aggregate"},{"json":"{\n \"items\": [{\n \"type\": [\"h-product\"],\n \"properties\": {\n \"name\": [\"Raspberry Pi\"],\n \"photo\": [\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\"],\n \"description\": [{\n \"value\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\",\n \"html\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\"\n }],\n \"url\": [\"http://www.raspberrypi.org/\"],\n \"price\": [\"£29.95\"],\n \"category\": [\"Computer\", \"Education\"],\n \"review\": [{\n \"value\": \"4.5 out of 5\",\n \"type\": [\"h-review\"],\n \"properties\": {\n \"rating\": [\"4.5\"]\n }\n }]\n }\n }],\n \"rels\": {\n \"tag\": [\"http://en.wikipedia.org/wiki/computer\", \"http://en.wikipedia.org/wiki/education\"]\n },\n \"rel-urls\": {\n \"http://en.wikipedia.org/wiki/computer\": {\n \"text\": \"Computer\",\n \"rels\": [\"tag\"]\n },\n \"http://en.wikipedia.org/wiki/education\": {\n \"text\": \"Education\",\n \"rels\": [\"tag\"]\n }\n }\n}","html":"<meta charset=\"utf-8\">\n<div class=\"hproduct\">\n <h2 class=\"fn\">Raspberry Pi</h2>\n <img class=\"photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"price\">£29.95</p>\n <p class=\"review hreview\"><span class=\"rating\">4.5</span> out of 5</p>\n <p>Categories: \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/computer\" class=\"category\">Computer</a>, \n <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/education\" class=\"category\">Education</a>\n </p>\n</div>","name":"mf-v1-hproduct-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"contact\": [{\n \"value\": \"Tim Berners-Lee\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"]\n }\n }],\n \"summary\": [\"invented the World Wide Web\"],\n \"affiliation\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"W3C\"],\n \"photo\": [\"http://www.w3.org/Icons/WWW/w3c_home_nb.png\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"hresume\">\n <p>\n <span class=\"contact vcard\"><span class=\"fn\">Tim Berners-Lee</span></span>, \n <span class=\"summary\">invented the World Wide Web</span>.\n </p>\n Belongs to following groups:\n <p> \n <a class=\"affiliation vcard\" href=\"http://www.w3.org/\">\n <img class=\"fn photo\" alt=\"W3C\" src=\"http://www.w3.org/Icons/WWW/w3c_home_nb.png\" />\n </a>\n </p> \n</div>","name":"mf-v1-hresume-affiliation"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"contact\": [{\n \"value\": \"Tim Berners-Lee\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"org\": [\"MIT\"],\n \"adr\": [{\n \"value\": \"32 Vassar Street, \\n Room 32-G524, \\n Cambridge, \\n MA \\n 02139, \\n USA. \\n (Work)\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"32 Vassar Street\"],\n \"extended-address\": [\"Room 32-G524\"],\n \"locality\": [\"Cambridge\"],\n \"region\": [\"MA\"],\n \"postal-code\": [\"02139\"],\n \"country-name\": [\"USA\"]\n }\n }],\n \"tel\": [\"+1 (617) 253 5702\"],\n \"email\": [\"mailto:timbl@w3.org\"]\n }\n }],\n \"summary\": [\"Invented the World Wide Web.\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"hresume\">\n <div class=\"contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"org\">MIT</p>\n <p class=\"adr\">\n <span class=\"street-address\">32 Vassar Street</span>, \n <span class=\"extended-address\">Room 32-G524</span>, \n <span class=\"locality\">Cambridge</span>, \n <span class=\"region\">MA</span> \n <span class=\"postal-code\">02139</span>, \n <span class=\"country-name\">USA</span>. \n (<span class=\"type\">Work</span>)\n </p>\n <p>Tel:<span class=\"tel\">+1 (617) 253 5702</span></p>\n <p>Email:<a class=\"email\" href=\"mailto:timbl@w3.org\">timbl@w3.org</a></p>\n </div>\n <p class=\"summary\">Invented the World Wide Web.</p>\n</div>","name":"mf-v1-hresume-contact"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"contact\": [{\n \"value\": \"Tim Berners-Lee\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"job-title\": [\"Director of the World Wide Web Foundation\"]\n }\n }],\n \"summary\": [\"Invented the World Wide Web.\"],\n \"education\": [{\n \"value\": \"The Queen's College, Oxford University\",\n \"type\": [\"h-event\", \"h-card\"],\n \"properties\": {\n \"name\": [\"The Queen's College, Oxford University\"],\n \"org\": [\"The Queen's College, Oxford University\"],\n \"description\": [\"BA Hons (I) Physics\"],\n \"start\": [\"1973-09\"],\n \"end\": [\"1976-06\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"hresume\">\n <div class=\"contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"summary\">Invented the World Wide Web.</p><hr />\n <p class=\"education vevent vcard\">\n <span class=\"fn summary org\">The Queen's College, Oxford University</span>, \n <span class=\"description\">BA Hons (I) Physics</span> \n <time class=\"dtstart\" datetime=\"1973-09\">1973</time> –\n <time class=\"dtend\" datetime=\"1976-06\">1976</time>\n </p>\n</div>","name":"mf-v1-hresume-education"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"contact\": [{\n \"value\": \"Tim Berners-Lee\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"]\n }\n }],\n \"summary\": [\"invented the World Wide Web\"],\n \"skill\": [\"information systems\", \"advocacy\", \"leadership\"]\n }\n }],\n \"rels\": {\n \"tag\": [\"http://example.com/skills/informationsystems\", \"http://example.com/skills/advocacy\", \"http://example.com/skills/leadership\"]\n },\n \"rel-urls\": {\n \"http://example.com/skills/informationsystems\": {\n \"text\": \"information systems\",\n \"rels\": [\"tag\"]\n },\n \"http://example.com/skills/advocacy\": {\n \"text\": \"advocacy\",\n \"rels\": [\"tag\"]\n },\n \"http://example.com/skills/leadership\": {\n \"text\": \"leadership\",\n \"rels\": [\"tag\"]\n }\n }\n}","html":"<div class=\"hresume\"> \n <p>\n <span class=\"contact vcard\"><span class=\"fn\">Tim Berners-Lee</span></span>, \n <span class=\"summary\">invented the World Wide Web</span>.\n </p>\n Skills: \n <ul>\n <li><a class=\"skill\" rel=\"tag\" href=\"http://example.com/skills/informationsystems\">information systems</a></li>\n <li><a class=\"skill\" rel=\"tag\" href=\"http://example.com/skills/advocacy\">advocacy</a></li>\n <li><a class=\"skill\" rel=\"tag\" href=\"http://example.com/skills/leadership\">leadership</a></li>\n </ul>\n</div>","name":"mf-v1-hresume-skill"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"contact\": [{\n \"value\": \"Tim Berners-Lee\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"job-title\": [\"Director of the World Wide Web Foundation\"]\n }\n }],\n \"summary\": [\"Invented the World Wide Web.\"],\n \"experience\": [{\n \"value\": \"World Wide Web Foundation\",\n \"type\": [\"h-event\", \"h-card\"],\n \"properties\": {\n \"job-title\": [\"Director\"],\n \"name\": [\"World Wide Web Foundation\"],\n \"org\": [\"World Wide Web Foundation\"],\n \"url\": [\"http://www.webfoundation.org/\"],\n \"start\": [\"2009-01-18\"],\n \"duration\": [\"P2Y11M\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"hresume\">\n <div class=\"contact vcard\">\n <p class=\"fn\">Tim Berners-Lee</p>\n <p class=\"title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"summary\">Invented the World Wide Web.</p><hr />\n <div class=\"experience vevent vcard\">\n <p class=\"title\">Director</p>\n <p><a class=\"fn summary org url\" href=\"http://www.webfoundation.org/\">World Wide Web Foundation</a></p>\n <p>\n <time class=\"dtstart\" datetime=\"2009-01-18\">Jan 2009</time> – Present\n <time class=\"duration\" datetime=\"P2Y11M\">(2 years 11 month)</time>\n </p>\n </div>\n</div>","name":"mf-v1-hresume-work"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Crepes on Cole\",\n \"type\": [\"h-item\"],\n \"properties\": {\n \"photo\": [\"http://example.com/images/photo.gif\"],\n \"name\": [\"Crepes on Cole\"],\n \"url\": [\"http://example.com/crepeoncole\"]\n }\n }],\n \"rating\": [\"5\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<base href=\"http://example.com\">\n<div class=\"hreview\">\n <p class=\"item\">\n <img class=\"photo\" src=\"images/photo.gif\" />\n <a class=\"fn url\" href=\"http://example.com/crepeoncole\">Crepes on Cole</a>\n </p>\n <p><span class=\"rating\">5</span> out of 5 stars</p>\n</div>","name":"mf-v1-hreview-item"},{"json":"\n{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"rating\": [\"5\"],\n \"name\": [\"Crepes on Cole is awesome\"],\n \"reviewer\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"]\n }\n }],\n \"description\": [{\n \"value\": \"Crepes on Cole is one of the best little \\n creperies in San Francisco.\\n Excellent food and service. Plenty of tables in a variety of sizes \\n for parties large and small. Window seating makes for excellent \\n people watching to/from the N-Judah which stops right outside. \\n I've had many fun social gatherings here, as well as gotten \\n plenty of work done thanks to neighborhood WiFi.\",\n \"html\": \"\\n <p class=\\\"item vcard\\\">\\n <span class=\\\"fn org\\\">Crepes on Cole</span> is one of the best little \\n creperies in <span class=\\\"adr\\\"><span class=\\\"locality\\\">San Francisco</span></span>.\\n Excellent food and service. Plenty of tables in a variety of sizes \\n for parties large and small. Window seating makes for excellent \\n people watching to/from the N-Judah which stops right outside. \\n I've had many fun social gatherings here, as well as gotten \\n plenty of work done thanks to neighborhood WiFi.\\n </p>\\n \"\n }],\n \"item\": [{\n \"value\": \"Crepes on Cole\",\n \"type\": [\"h-item\", \"h-card\"],\n \"properties\": {\n \"name\": [\"Crepes on Cole\"],\n \"org\": [\"Crepes on Cole\"],\n \"adr\": [{\n \"value\": \"San Francisco\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"San Francisco\"]\n }\n }]\n }\n }],\n \"category\": [\"crepe\"],\n \"url\": [\"http://example.com/crepe\"]\n }\n }],\n \"rels\": {\n \"tag\": [\"http://en.wikipedia.org/wiki/crepe\"],\n \"self\": [\"http://example.com/crepe\"],\n \"bookmark\": [\"http://example.com/crepe\"],\n \"license\": [\"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\"]\n },\n \"rel-urls\": {\n \"http://en.wikipedia.org/wiki/crepe\": {\n \"text\": \"crepe\",\n \"rels\": [\"tag\"]\n },\n \"http://example.com/crepe\": {\n \"text\": \"http://example.com/crepe\",\n \"rels\": [\"self\", \"bookmark\"]\n },\n \"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\": {\n \"text\": \"Creative Commons Attribution-ShareAlike License\",\n \"rels\": [\"license\"]\n }\n }\n}","html":"<div class=\"hreview\">\n <span><span class=\"rating\">5</span> out of 5 stars</span>\n <h4 class=\"summary\">Crepes on Cole is awesome</h4>\n <span class=\"reviewer vcard\">\n Reviewer: <span class=\"fn\">Tantek</span> - \n </span>\n <time class=\"reviewed\" datetime=\"2005-04-18\">April 18, 2005</time>\n <div class=\"description\">\n <p class=\"item vcard\">\n <span class=\"fn org\">Crepes on Cole</span> is one of the best little \n creperies in <span class=\"adr\"><span class=\"locality\">San Francisco</span></span>.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.\n </p>\n </div>\n <p>Visit date: <span>April 2005</span></p>\n <p>Food eaten: <a rel=\"tag\" href=\"http://en.wikipedia.org/wiki/crepe\">crepe</a></p>\n <p>Permanent link for review: <a rel=\"self bookmark\" href=\"http://example.com/crepe\">http://example.com/crepe</a></p>\n <p><a rel=\"license\" href=\"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\">Creative Commons Attribution-ShareAlike License</a></p>\n</div>","name":"mf-v1-hreview-vcard"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Mediterranean Wraps\",\n \"type\": [\"h-item\", \"h-card\"],\n \"properties\": {\n \"name\": [\"Mediterranean Wraps\"],\n \"org\": [\"Mediterranean Wraps\"],\n \"adr\": [{\n \"value\": \"433 S California Ave, \\n Palo Alto, \\n CA\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"433 S California Ave\"],\n \"locality\": [\"Palo Alto\"],\n \"region\": [\"CA\"]\n }\n }],\n \"tel\": [\"(650) 321-8189\"]\n }\n }],\n \"rating\": [\"9.2\"],\n \"average\": [\"9.2\"],\n \"best\": [\"10\"],\n \"count\": [\"17\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"hreview-aggregate\">\n <div class=\"item vcard\">\n <h3 class=\"fn org\">Mediterranean Wraps</h3> \n <p>\n <span class=\"adr\">\n <span class=\"street-address\">433 S California Ave</span>, \n <span class=\"locality\">Palo Alto</span>, \n <span class=\"region\">CA</span></span> - \n \n <span class=\"tel\">(650) 321-8189</span>\n </p>\n </div> \n <p class=\"rating\">\n <span class=\"average value\">9.2</span> out of \n <span class=\"best\">10</span> \n based on <span class=\"count\">17</span> reviews\n </p>\n</div>","name":"mf-v1-hreview-aggregate-hcard"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Mediterranean Wraps\",\n \"type\": [\"h-item\"],\n \"properties\": {\n \"name\": [\"Mediterranean Wraps\"],\n \"url\": [\"http://example.com/mediterraneanwraps\"]\n }\n }],\n \"rating\": [\"4.5\"],\n \"count\": [\"6\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}\n","html":"<p class=\"hreview-aggregate\">\n <span class=\"item\">\n <a class=\"fn url\" href=\"http://example.com/mediterraneanwraps\">Mediterranean Wraps</a>\n </span> - Rated: \n <span class=\"rating\">4.5</span> out of 5 (<span class=\"count\">6</span> reviews)\n</p>","name":"mf-v1-hreview-aggregate-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Fullfrontal\",\n \"type\": [\"h-item\", \"h-event\"],\n \"properties\": {\n \"name\": [\"Fullfrontal\"],\n \"description\": [\"A one day JavaScript Conference held in Brighton\"],\n \"start\": [\"2012-11-09\"]\n }\n }],\n \"rating\": [\"9.9\"],\n \"average\": [\"9.9\"],\n \"best\": [\"10\"],\n \"count\": [\"62\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"hreview-aggregate\">\n <div class=\"item vevent\">\n <h3 class=\"summary\">Fullfrontal</h3>\n <p class=\"description\">A one day JavaScript Conference held in Brighton</p>\n <p><time class=\"dtstart\" datetime=\"2012-11-09\">9th November 2012</time></p> \n </div> \n \n <p class=\"rating\">\n <span class=\"average value\">9.9</span> out of \n <span class=\"best\">10</span> \n based on <span class=\"count\">62</span> reviews\n </p>\n</div>","name":"mf-v1-hreview-aggregate-vevent"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"org\": [\"Mozilla\"],\n \"adr\": [{\n \"value\": \"665 3rd St. \\n Suite 207 \\n San Francisco, \\n CA \\n 94107 \\n U.S.A.\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"665 3rd St.\"],\n \"extended-address\": [\"Suite 207\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94107\"],\n \"country-name\": [\"U.S.A.\"]\n }\n }]\n }\n }, {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"org\": [\"Mozilla\"],\n \"adr\": [{\n \"value\": \"665 3rd St. \\n Suite 207 \\n San Francisco, \\n CA \\n 94107 \\n U.S.A.\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"665 3rd St.\"],\n \"extended-address\": [\"Suite 207\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94107\"],\n \"country-name\": [\"U.S.A.\"]\n }\n }]\n }\n }, {\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"665 3rd St.\"],\n \"extended-address\": [\"Suite 207\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94107\"],\n \"country-name\": [\"U.S.A.\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vcard\" itemref=\"mozilla-org mozilla-adr\">\n <span class=\"name\">Brendan Eich</span>\n</div>\n<div class=\"vcard\" itemref=\"mozilla-org mozilla-adr\">\n <span class=\"name\">Mitchell Baker</span>\n</div>\n\n<p id=\"mozilla-org\" class=\"org\">Mozilla</p>\n<p id=\"mozilla-adr\" class=\"adr\">\n <span class=\"street-address\">665 3rd St.</span> \n <span class=\"extended-address\">Suite 207</span> \n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span> \n <span class=\"postal-code\">94107</span> \n <span class=\"country-name\">U.S.A.</span> \n</p>","name":"mf-v1-includes-hcarditemref"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"location\": [{\n \"value\": \"Room 10\\n \\n Moscone Center, \\n San Francisco\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"extended-address\": [\"Room 10\", \"Moscone Center\"],\n \"locality\": [\"San Francisco\"]\n }\n }],\n \"start\": [\"2012-06-27 15:45:00-08:00\"],\n \"end\": [\"2012-06-27 16:45:00-08:00\"]\n }\n }, {\n \"type\": [\"h-event\"],\n \"properties\": {\n \"location\": [{\n \"value\": \"Room 11\\n \\n Moscone Center, \\n San Francisco\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"extended-address\": [\"Room 11\", \"Moscone Center\"],\n \"locality\": [\"San Francisco\"]\n }\n }],\n \"start\": [\"2012-06-27 15:45:00-08:00\"],\n \"end\": [\"2012-06-27 16:45:00-08:00\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vevent\" itemref=\"io-session07\">\n <span class=\"name\">Monetizing Android Apps</span> - spaekers: \n <span class=\"speaker\">Chrix Finne</span>, \n <span class=\"speaker\">Kenneth Lui</span> - \n <span itemref=\"io-location\" class=\"location adr\">\n <span class=\"extended-address\">Room 10</span>\n </span> \n</div>\n<div class=\"vevent\" itemref=\"io-session07\">\n <span class=\"name\">New Low-Level Media APIs in Android</span> - spaekers: \n <span class=\"speaker\">Dave Burke</span> -\n <span itemref=\"io-location\" class=\"location adr\">\n <span class=\"extended-address\">Room 11</span>\n </span> \n</div>\n\n<p id=\"io-session07\">\n Session 01 is between: \n <time class=\"dtstart\" datetime=\"2012-06-27T15:45:00-0800\">3:45PM</time> to \n <time class=\"dtend\" datetime=\"2012-06-27T16:45:00-0800\">4:45PM</time> \n</p> \n<p id=\"io-location\">\n <span class=\"extended-address\">Moscone Center</span>, \n <span class=\"locality\">San Francisco</span> \n</p>","name":"mf-v1-includes-heventitemref"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"org\": [\"Twitter\"],\n \"adr\": [{\n \"value\": \"1355 Market St,\\n San Francisco, \\n CA\\n 94103\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"1355 Market St\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94103\"]\n }\n }]\n }\n }, {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"org\": [\"Twitter\"],\n \"adr\": [{\n \"value\": \"1355 Market St,\\n San Francisco, \\n CA\\n 94103\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"1355 Market St\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94103\"]\n }\n }]\n }\n }, {\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"1355 Market St\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94103\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vcard\">\n <span class=\"name\">Ben Ward</span>\n <a class=\"include\" href=\"#twitter\">Twitter</a>\n</div>\n<div class=\"vcard\">\n <span class=\"name\">Dan Webb</span>\n <a class=\"include\" href=\"#twitter\">Twitter</a>\n</div>\n\n<div id=\"twitter\">\n <p class=\"org\">Twitter</p>\n <p class=\"adr\">\n <span class=\"street-address\">1355 Market St</span>,\n <span class=\"locality\">San Francisco</span>, \n <span class=\"region\">CA</span>\n <span class=\"postal-code\">94103</span>\n </p>\n</div>","name":"mf-v1-includes-hyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"start\": [\"2012-10-30 11:45:00-08:00\"],\n \"name\": [\"Build Conference\"],\n \"location\": [{\n \"value\": \"Redmond, \\n Washington, \\n USA\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"Redmond\"],\n \"region\": [\"Washington\"],\n \"country-name\": [\"USA\"]\n }\n }]\n }\n }, {\n \"type\": [\"h-event\"],\n \"properties\": {\n \"start\": [\"2012-10-31 11:15:00-08:00\"],\n \"name\": [\"Build Conference\"],\n \"location\": [{\n \"value\": \"Redmond, \\n Washington, \\n USA\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"Redmond\"],\n \"region\": [\"Washington\"],\n \"country-name\": [\"USA\"]\n }\n }]\n }\n }, {\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"Redmond\"],\n \"region\": [\"Washington\"],\n \"country-name\": [\"USA\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"vevent\">\n <span class=\"name\">HTML5 & CSS3 latest features in action!</span> - \n <span class=\"speaker\">David Rousset</span> -\n <time class=\"dtstart\" datetime=\"2012-10-30T11:45:00-08:00\">Tue 11:45am</time>\n <object data=\"#buildconf\" class=\"include\" type=\"text/html\" height=\"0\" width=\"0\"></object>\n</div>\n<div class=\"vevent\">\n <span class=\"name\">Building High-Performing JavaScript for Modern Engines</span> -\n <span class=\"speaker\">John-David Dalton</span> and \n <span class=\"speaker\">Amanda Silver</span> -\n <time class=\"dtstart\" datetime=\"2012-10-31T11:15:00-08:00\">Wed 11:15am</time>\n <object data=\"#buildconf\" class=\"include\" type=\"text/html\" height=\"0\" width=\"0\"></object>\n</div>\n\n\n<div id=\"buildconf\">\n <p class=\"summary\">Build Conference</p>\n <p class=\"location adr\">\n <span class=\"locality\">Redmond</span>, \n <span class=\"region\">Washington</span>, \n <span class=\"country-name\">USA</span>\n </p>\n</div>","name":"mf-v1-includes-object"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Chris Mills\"],\n \"url\": [\"http://dev.opera.com/\"],\n \"org\": [\"Opera\"]\n }\n }, {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Erik Möller\"],\n \"url\": [\"http://dev.opera.com/\"],\n \"org\": [\"Opera\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<table>\n <tr>\n <th id=\"org\"><a class=\"url org\" href=\"http://dev.opera.com/\">Opera</a></th>\n </tr>\n <tr>\n <td class=\"vcard\" headers=\"org\"><span class=\"fn\">Chris Mills</span></td>\n </tr>\n <tr>\n <td class=\"vcard\" headers=\"org\"><span class=\"fn\">Erik Möller</span></td>\n </tr>\n </table>","name":"mf-v1-includes-table"},{"json":"{\n \"items\": [{\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"name\": [\"Bricklayer's Arms\"],\n \"label\": [\"3 Charlotte Road, \\n City of London, \\n EC2A 3PE, \\n UK\"],\n \"street-address\": [\"3 Charlotte Road\"],\n \"locality\": [\"City of London\"],\n \"postal-code\": [\"EC2A 3PE\"],\n \"country-name\": [\"UK\"],\n \"geo\": [\"51.526421;-0.081067\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-adr\">\n <span class=\"p-name\">Bricklayer's Arms</span>\n <span class=\"p-label\"> \n <span class=\"p-street-address\">3 Charlotte Road</span>, \n <span class=\"p-locality\">City of London</span>, \n <span class=\"p-postal-code\">EC2A 3PE</span>, \n <span class=\"p-country-name\">UK</span> \n </span> – \n Geo:(<span class=\"p-geo\">51.526421;-0.081067</span>) \n</p>","name":"mf-v2-h-adr-geo"},{"json":"{\n \"items\": [{\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"name\": [\"Bricklayer's Arms\"],\n \"geo\": [\"geo:51.526421;-0.081067;crs=wgs84;u=40\"],\n \"locality\": [\"London\"],\n \"url\": [\"geo:51.526421;-0.081067;crs=wgs84;u=40\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-adr\">\n <a class=\"p-name u-geo\" href=\"geo:51.526421;-0.081067;crs=wgs84;u=40\">Bricklayer's Arms</a>, \n <span class=\"p-locality\">London</span> \n</p>","name":"mf-v2-h-adr-geourl"},{"json":"{\n \"items\": [{\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"name\": [\"665 3rd St. Suite 207 San Francisco, CA 94107 U.S.A.\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-adr\">665 3rd St. Suite 207 San Francisco, CA 94107 U.S.A.</p>","name":"mf-v2-h-adr-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"street-address\": [\"665 3rd St.\"],\n \"extended-address\": [\"Suite 207\"],\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"postal-code\": [\"94107\"],\n \"country-name\": [\"U.S.A.\"],\n \"name\": [\"665 3rd St. \\n Suite 207 \\n San Francisco, \\n CA \\n 94107 \\n U.S.A.\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-adr\">\n <span class=\"p-street-address\">665 3rd St.</span> \n <span class=\"p-extended-address\">Suite 207</span> \n <span class=\"p-locality\">San Francisco</span>, \n <span class=\"p-region\">CA</span> \n <span class=\"p-postal-code\">94107</span> \n <span class=\"p-country-name\">U.S.A.</span> \n</p>","name":"mf-v2-h-adr-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\", \"h-as-note\"],\n \"properties\": {\n \"in-reply-to\": [{\n \"value\": \"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\",\n \"type\": [\"h-cite\"],\n \"properties\": {\n \"name\": [\"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\"],\n \"url\": [\"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\"]\n }\n },\n {\n \"value\": \"https://twitter.com/benwerd/status/604733231284383744\",\n \"type\": [\"h-cite\"],\n \"properties\": {\n \"name\": [\"https://twitter.com/benwerd/status/604733231284383744\"],\n \"url\": [\"https://twitter.com/benwerd/status/604733231284383744\"]\n }\n }],\n \"author\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek Çelik\"],\n \"photo\": [\"http://tantek.com/images/photo.gif\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }],\n \"name\": [\"@benwerd\\n @erinjoalso proud of you &\\n @withknown— so much #indieweb & especially user empathy. Keep up the great work!\"],\n \"content\": [{\n \"value\": \"@benwerd\\n @erinjoalso proud of you &\\n @withknown— so much #indieweb & especially user empathy. Keep up the great work!\",\n \"html\": \"\\n <a class=\\\"auto-link h-x-username\\\" href=\\\"https://twitter.com/benwerd\\\">@benwerd</a>\\n <a class=\\\"auto-link h-x-username\\\" href=\\\"https://twitter.com/erinjo\\\">@erinjo</a>also proud of you &\\n <a class=\\\"auto-link h-x-username\\\" href=\\\"https://twitter.com/withknown\\\">@withknown</a>— so much #indieweb & especially user empathy. Keep up the great work!\"\n }],\n \"published\": [\"2015-06-01 22:20-07:00\"],\n \"updated\": [\"2015-06-01 22:20-07:00\"],\n \"url\": [\"http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy\"],\n \"uid\": [\"http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy\"],\n \"syndication\": [\"https://twitter.com/t/status/605604965566906369\"]\n },\n \"children\": [{\n \"value\": \"@benwerd\",\n \"type\": [\"h-x-username\"],\n \"properties\": {\n \"name\": [\"@benwerd\"],\n \"url\": [\"https://twitter.com/benwerd\"]\n }\n },\n {\n \"value\": \"@erinjo\",\n \"type\": [\"h-x-username\"],\n \"properties\": {\n \"name\": [\"@erinjo\"],\n \"url\": [\"https://twitter.com/erinjo\"]\n }\n },\n {\n \"value\": \"@withknown\",\n \"type\": [\"h-x-username\"],\n \"properties\": {\n \"name\": [\"@withknown\"],\n \"url\": [\"https://twitter.com/withknown\"]\n }\n }]\n }],\n \"rels\": {\n \"prev\": [\"http://tantek.com/152/t1/congrats-fellow-elected-w3cab-members\"],\n \"next\": [\"http://tantek.com/152/t3/going-indiewebcamp-2015-portland\"],\n \"in-reply-to\": [\"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\", \"https://twitter.com/benwerd/status/604733231284383744\"],\n \"author\": [\"http://tantek.com/\"],\n \"syndication\": [\"https://twitter.com/t/status/605604965566906369\"]\n },\n \"rel-urls\": {\n \"http://tantek.com/152/t1/congrats-fellow-elected-w3cab-members\": {\n \"title\": \"View the previous (older) item in the stream.\",\n \"text\": \"←\",\n \"rels\": [\"prev\"]\n },\n \"http://tantek.com/152/t3/going-indiewebcamp-2015-portland\": {\n \"title\": \"View the next (newer) item in the stream\",\n \"text\": \"→\",\n \"rels\": [\"next\"]\n },\n \"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\": {\n \"text\": \"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\",\n \"rels\": [\"in-reply-to\"]\n },\n \"https://twitter.com/benwerd/status/604733231284383744\": {\n \"text\": \"https://twitter.com/benwerd/status/604733231284383744\",\n \"rels\": [\"in-reply-to\"]\n },\n \"http://tantek.com/\": {\n \"title\": \"Tantek Çelik\",\n \"rels\": [\"author\"]\n },\n \"https://twitter.com/t/status/605604965566906369\": {\n \"text\": \"View \\n Conversation\\n on Twitter\",\n \"rels\": [\"syndication\"]\n }\n }\n}","html":"<!-- http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy -->\n<base href=\"http://tantek.com/\" />\n\n<li class=\"h-entry hentry h-as-note\">\n <div>\n <ul>\n <li>\n <a href=\"152/t1/congrats-fellow-elected-w3cab-members\" id=\"previtem\" title=\"View the previous (older) item in the stream.\"\n rel=\"prev\"><abbr title=\"Previous\">←</abbr></a>\n </li>\n <li>\n <a href=\"152/t3/going-indiewebcamp-2015-portland\" id=\"nextitem\" title=\"View the next (newer) item in the stream\" rel=\"next\"><abbr title=\"Next\">→</abbr></a>\n </li>\n </ul>\n </div>\n <div>In reply to:\n <p>\n <a class=\"u-in-reply-to h-cite\" rel=\"in-reply-to\" href=\"http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far\">http://werd.io/2015/im-super-proud-of-everything-weve-done-on-withknown-so-far</a>\n </p>\n <p>\n <a class=\"u-in-reply-to h-cite\" rel=\"in-reply-to\" href=\"https://twitter.com/benwerd/status/604733231284383744\">https://twitter.com/benwerd/status/604733231284383744</a>\n </p>\n <hr>\n </div>\n <a href=\"../\" class=\"p-author h-card\" rel=\"author\" title=\"Tantek Çelik\"><img src=\"/images/photo.gif\" alt=\"Tantek Çelik\"></a>\n <p class=\"p-name entry-title e-content entry-content article\">\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/benwerd\">@benwerd</a>\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/erinjo\">@erinjo</a>also proud of you &amp;\n <a class=\"auto-link h-x-username\" href=\"https://twitter.com/withknown\">@withknown</a>— so much #indieweb &amp; especially user empathy. Keep up the great work!</p>\n <span>\n <span class=\"dt-published published dt-updated updated\">\n <time class=\"value\" datetime=\"22:20-0700\">22:20</time>on\n <time class=\"value\">2015-06-01</time>\n </span>\n <span class=\"lt\">(ttk.me t4bT2)</span>using\n <span class=\"using\">BBEdit</span>\n </span>\n <div>\n <form action=\"http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy\">\n <div>\n <label>\n <span class=\"lt\">URL:</span>\n <input class=\"u-url url u-uid uid bookmark\" type=\"url\" size=\"70\" style=\"max-width:100%\" value=\"http://tantek.com/2015/152/t2/proud-withknown-indieweb-user-empathy\">\n </label>\n </div>\n </form>\n </div>\n <div>\n <a class=\"u-syndication\" rel=\"syndication\" style=\"float:right;\" href=\"https://twitter.com/t/status/605604965566906369\">\n <img src=\"/images/photo.gif\" style=\"vertical-align:-30%\" alt=\"\"> \n View \n Conversation\n on Twitter\n</a>\n </div>\n</li>","name":"mf-v2-h-as-note-note"},{"json":" {\n\t\"items\": [{\n \"type\": [\n \"h-card\"\n ],\n \"properties\": {\n \"name\": [\n \"Mitchell Baker\"\n ],\n \"url\": [\"http://blog.lizardwrangler.com/\"],\n \"org\": [{\n \"value\": \"Mozilla Foundation\",\n \"type\": [\n \"h-card\"\n ],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"url\": [\"http://example.org/bios/mitchell-baker/\"]\n }\n }],\n \"photo\": [\"http://example.org/images/photo.gif\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}\n","html":"<base href=\"http://example.org\"/>\n<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card\" href=\"bios/mitchell-baker/\">Mozilla Foundation</a>)\n <img class=\"u-photo\" src=\"images/photo.gif\"/>\n</div>","name":"mf-v2-h-card-baseurl"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Håkon Wium Lie\"],\n \"photo\": [\"http://upload.wikimedia.org/wikipedia/commons/thumb/9/96/H%C3%A5kon-Wium-Lie-2009-03.jpg/215px-H%C3%A5kon-Wium-Lie-2009-03.jpg\"],\n \"url\": [\"http://people.opera.com/howcome/\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<a class=\"h-card\" href=\"http://people.opera.com/howcome/\" title=\"Håkon Wium Lie, CTO Opera\">\n <article>\n <h2 class=\"p-name\">Håkon Wium Lie</h2>\n <img src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/9/96/H%C3%A5kon-Wium-Lie-2009-03.jpg/215px-H%C3%A5kon-Wium-Lie-2009-03.jpg\" />\n </article>\n</a>","name":"mf-v2-h-card-childimplied"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"photo\": [\"http://blog.mozilla.org/press/files/2012/04/mitchell-baker.jpg\"],\n \"url\": [\"http://blog.lizardwrangler.com/\", \"https://twitter.com/MitchellBaker\"],\n \"name\": [\"Mitchell Baker\"],\n \"org\": [\"Mozilla Foundation\"],\n \"note\": [\"Mitchell is responsible for setting the direction and scope of the Mozilla Foundation and its activities.\"],\n \"category\": [\"Strategy\", \"Leadership\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-card\">\n <img class=\"u-photo\" alt=\"photo of Mitchell\" src=\"http://blog.mozilla.org/press/files/2012/04/mitchell-baker.jpg\" />\n <p>\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a>\n (<a class=\"u-url\" href=\"https://twitter.com/MitchellBaker\">@MitchellBaker</a>)\n <span class=\"p-org\">Mozilla Foundation</span>\n </p>\n <p class=\"p-note\">Mitchell is responsible for setting the direction and scope of the Mozilla Foundation and its activities.</p>\n <p><span class=\"p-category\">Strategy</span> and <span class=\"p-category\">Leadership</span></p>\n</div>","name":"mf-v2-h-card-extendeddescription"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"url\": [\"http://blog.lizardwrangler.com/\"],\n \"name\": [\"Mitchell Baker\"],\n \"org\": [{\n \"value\": \"Mozilla Foundation\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"url\": [\"http://mozilla.org/\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card\" href=\"http://mozilla.org/\">Mozilla Foundation</a>)\n</div>","name":"mf-v2-h-card-hcard"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Mitchell Baker\"],\n \"url\": [\"http://blog.lizardwrangler.com/\"],\n \"org\": [{\n \"value\": \"Mozilla Foundation\",\n \"type\": [\"h-card\", \"h-org\"],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"url\": [\"http://mozilla.org/\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card h-org\" href=\"http://mozilla.org/\">Mozilla Foundation</a>)\n</div>","name":"mf-v2-h-card-horghcard"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Rohit Khare\"],\n \"photo\": [\"http://example.com/images/photo.gif\"],\n \"url\": [\"http://rohit.khare.org/\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-card\" href=\"http://rohit.khare.org/\">\n <img alt=\"Rohit Khare\" src=\"images/photo.gif\" />\n </a>","name":"mf-v2-h-card-hyperlinkedphoto"},{"json":"{ \n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Name\"]\n },\n \"children\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John Doe\"],\n \"photo\": [\"http://example.com/john.html\"]\n }\n }]\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Name\"]\n },\n \"children\": [{\n \"value\": \"Name\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John Doe\"],\n \"photo\": [\"http://example.com/john.html\"]\n }\n }]\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"\n<img class=\"h-card\" src=\"jane.html\" alt=\"Jane Doe\"/>\n<area class=\"h-card\" href=\"jane.html\" alt=\"Jane Doe\"></area>\n<abbr class=\"h-card\" title=\"Jane Doe\">JD</abbr>\n\n<div class=\"h-card\"><img src=\"jane.html\" alt=\"Jane Doe\"/></div>\n<div class=\"h-card\"><area href=\"jane.html\" alt=\"Jane Doe\"></area></div>\n<div class=\"h-card\"><abbr title=\"Jane Doe\">JD</abbr></div>\n\n<div class=\"h-card\"><span><img src=\"jane.html\" alt=\"Jane Doe\"/></span></div>\n<div class=\"h-card\"><span><area href=\"jane.html\" alt=\"Jane Doe\"></area></span></div>\n<div class=\"h-card\"><span><abbr title=\"Jane Doe\">JD</abbr></span></div>\n\n<div class=\"h-card\"><img class=\"h-card\" src=\"john.html\" alt=\"John Doe\"/>Name</div>\n<div class=\"h-card\"><span class=\"h-card\"><img src=\"john.html\" alt=\"John Doe\"/>Name</span></div>\n","name":"mf-v2-h-card-impliedname"},{"json":" {\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"]\n },\n \"children\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n }]\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"]\n },\n \"children\": [{\n \"value\": \"Jane Doe\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"photo\": [\"http://example.com/jane.jpeg\"]\n }\n }]\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<img class=\"h-card\" alt=\"Jane Doe\" src=\"jane.jpeg\"/>\n<object class=\"h-card\" data=\"jane.jpeg\"/>Jane Doe</object>\n\n<div class=\"h-card\"><img alt=\"Jane Doe\" src=\"jane.jpeg\"/></div> \n<div class=\"h-card\"><object data=\"jane.jpeg\"/>Jane Doe</object></div> \n\n<div class=\"h-card\"><span><img alt=\"Jane Doe\" src=\"jane.jpeg\"/></span></div> \n<div class=\"h-card\"><span><object data=\"jane.jpeg\"/>Jane Doe</object></span></div> \n\n<div class=\"h-card\"><img class=\"h-card\" alt=\"Jane Doe\" src=\"jane.jpeg\"/>Jane Doe</div> \n<div class=\"h-card\"><span class=\"h-card\"><object data=\"jane.jpeg\"/>Jane Doe</object></span></div> ","name":"mf-v2-h-card-impliedphoto"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n },\n {\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"]\n },\n \"children\": [{\n \"value\": \"Jane Doe\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Jane Doe\"],\n \"url\": [\"http://example.com/jane.html\"]\n }\n }]\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-card\" href=\"jane.html\">Jane Doe</a>\n<area class=\"h-card\" href=\"jane.html\" alt=\"Jane Doe\"/ >\n<div class=\"h-card\" ><a href=\"jane.html\">Jane Doe</a><p></p></div> \n<div class=\"h-card\" ><area href=\"jane.html\">Jane Doe</area><p></p></div>\n<div class=\"h-card\" ><a class=\"h-card\" href=\"jane.html\">Jane Doe</a><p></p></div> ","name":"mf-v2-h-card-impliedurl"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Ben Ward\"],\n \"url\": [\"http://benward.me/\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-card\" href=\"http://benward.me/\">Ben Ward</a>","name":"mf-v2-h-card-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Frances Berriman\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-card\">Frances Berriman</p>","name":"mf-v2-h-card-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Mitchell Baker\"],\n \"url\": [\"http://blog.lizardwrangler.com/\"]\n },\n \"children\": [{\n \"value\": \"Mozilla Foundation\",\n \"type\": [\"h-org\", \"h-card\"],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"url\": [\"http://mozilla.org/\"]\n }\n }]\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"h-org h-card\" href=\"http://mozilla.org/\">Mozilla Foundation</a>)\n</div>","name":"mf-v2-h-card-nested"},{"json":"{\n \"items\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"John Doe\"],\n \"given-name\": [\"John\"],\n \"additional-name\": [\"Peter\"],\n \"family-name\": [\"Doe\"],\n \"honorific-suffix\": [\"MSc\", \"PHD\"],\n \"org\": [\"Madgex\", \"Mozilla\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-card\">\n \n <span class=\"p-name\">\n <span class=\"p-given-name value\">John</span> \n <abbr class=\"p-additional-name\" title=\"Peter\">P</abbr> \n <span class=\"p-family-name value \">Doe</span> \n </span>\n <data class=\"p-honorific-suffix\" value=\"MSc\"></data>\n \n \n <br class=\"p-honorific-suffix\" />BSc<br />\n <hr class=\"p-honorific-suffix\" />BA\n \n \n <img class=\"p-honorific-suffix\" src=\"images/logo.gif\" alt=\"PHD\" />\n <img src=\"images/logo.gif\" alt=\"company logos\" usemap=\"#logomap\" />\n <map name=\"logomap\">\n <area class=\"p-org\" shape=\"rect\" coords=\"0,0,82,126\" href=\"madgex.htm\" alt=\"Madgex\" />\n <area class=\"p-org\" shape=\"circle\" coords=\"90,58,3\" href=\"mozilla.htm\" alt=\"Mozilla\" />\n </map>\n</div>","name":"mf-v2-h-card-p-property"},{"json":"\n {\n\t\"items\": [{\n \"type\": [\n \"h-card\"\n ],\n \"properties\": {\n \"name\": [\n \"Mitchell Baker\"\n ],\n \"url\": [\"http://blog.lizardwrangler.com/\"],\n \"org\": [\n {\n \"value\": \"Mozilla Foundation\",\n \"type\": [\n \"h-card\"\n ],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"url\": [\"http://example.com/bios/mitchell-baker/\"]\n }\n }\n ],\n \"photo\": [\"http://example.com/bios/mitchell-baker/picture.jpeg\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}\n","html":"<base href=\"http://example.com\" >\n<div class=\"h-card\">\n <a class=\"p-name u-url\" href=\"http://blog.lizardwrangler.com/\">Mitchell Baker</a> \n (<a class=\"p-org h-card\" href=\"bios/mitchell-baker/\">Mozilla Foundation</a>)\n <img class=\"u-photo\" src=\"bios/mitchell-baker/picture.jpeg\"/>\n</div>","name":"mf-v2-h-card-relativeurls"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"in-reply-to\": [{\n \"type\": [\"h-cite\"],\n \"properties\": {\n \"name\": [\"Example Post\"],\n \"url\": [\"http://example.com/post\"],\n\n \"author\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"url\": [\"http://example.com\"],\n \"name\": [\"Example Author\"]\n },\n \"value\": \"Example Author\"\n }]\n },\n \"value\": \"http://example.com/post\"\n }],\n \"name\": [\"Example Author\\n Home\\n \\n Example Post\"]\n }\n }],\n\t\"rels\": {},\n\t\"rel-urls\": {}\n}","html":"<div class=\"h-entry\">\n <div class=\"u-in-reply-to h-cite\">\n <span class=\"p-author h-card\">\n <span class=\"p-name\">Example Author</span>\n <a class=\"u-url\" href=\"http://example.com\">Home</a>\n </span>\n <a class=\"p-name u-url\" href=\"http://example.com/post\">Example Post</a>\n </div>\n</div>","name":"mf-v2-h-entry-impliedvalue-nested"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-entry\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a>","name":"mf-v2-h-entry-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-entry\">microformats.org at 7</p>","name":"mf-v2-h-entry-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"name\": [\"microformats.org at 7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service\",\n \"html\": \"\\n <p class=\\\"p-summary\\\">Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.</p>\\n\\n <p>The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service </p>\\n \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"h-entry\">\n <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"e-content\">\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n </p>\n</div>","name":"mf-v2-h-entry-summarycontent"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/\", \"http://microformats.org/2012/06/25/microformats-org-at-7\", \"http://microformats.org/2012/06/25/microformats-org-at-7\", \"http://microformats.org/\", \"http://microformats.org/wiki/microformats2-parsing\", \"http://microformats.org/wiki/value-class-pattern\", \"http://microformats.org/wiki/\", \"http://microformats.org/discuss\"],\n \"photo\": [\"http://example.com/images/logo.gif\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<base href=\"http://example.com\">\n<div class=\"h-entry\">\n <p class=\"p-name\">microformats.org at 7</p>\n\n \n <p class=\"u-url\">\n <span class=\"value-title\" title=\"http://microformats.org/\"> </span>\n Article permalink\n </p>\n <p class=\"u-url\">\n <span class=\"value\">http://microformats.org/</span> - \n <span class=\"value\">2012/06/25/microformats-org-at-7</span> \n </p> \n\n <p><a class=\"u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">Article permalink</a></p>\n\n <img src=\"images/logo.gif\" alt=\"company logos\" usemap=\"#logomap\" />\n <map name=\"logomap\">\n <area class=\"u-url\" shape=\"rect\" coords=\"0,0,82,126\" href=\"http://microformats.org/\" alt=\"microformats.org\" />\n </map>\n\n <img class=\"u-photo\" src=\"images/logo.gif\" alt=\"company logos\" />\n\n <object class=\"u-url\" data=\"http://microformats.org/wiki/microformats2-parsing\"></object>\n\n <abbr class=\"u-url\" title=\"http://microformats.org/wiki/value-class-pattern\">value-class-pattern</abbr> \n <data class=\"u-url\" value=\"http://microformats.org/wiki/\"></data>\n <p class=\"u-url\">http://microformats.org/discuss</p>\n</div>","name":"mf-v2-h-entry-u-property"},{"json":"{\n \"items\": [{\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"Expanding URLs within HTML content\"],\n \"content\": [{\n \"value\": \"Should not change: http://www.w3.org/\\n Should not change: http://example.com/\\n File relative: test.html = http://example.com/test.html\\n Directory relative: /test/test.html = http://example.com/test/test.html\\n Relative to root: /test.html = http://example.com/test.html\",\n \"html\": \"\\n <ul>\\n <li><a href=\\\"http://www.w3.org/\\\">Should not change: http://www.w3.org/</a></li>\\n <li><a href=\\\"http://example.com/\\\">Should not change: http://example.com/</a></li>\\n <li><a href=\\\"http://example.com/test.html\\\">File relative: test.html = http://example.com/test.html</a></li>\\n <li><a href=\\\"http://example.com/test/test.html\\\">Directory relative: /test/test.html = http://example.com/test/test.html</a></li>\\n <li><a href=\\\"http://example.com/test.html\\\">Relative to root: /test.html = http://example.com/test.html</a></li>\\n </ul>\\n <img src=\\\"http://example.com/images/photo.gif\\\" />\\n \"\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-entry\">\n <h1><a class=\"p-name\">Expanding URLs within HTML content</a></h1>\n <div class=\"e-content\">\n <ul>\n <li><a href=\"http://www.w3.org/\">Should not change: http://www.w3.org/</a></li>\n <li><a href=\"http://example.com/\">Should not change: http://example.com/</a></li>\n <li><a href=\"test.html\">File relative: test.html = http://example.com/test.html</a></li>\n <li><a href=\"/test/test.html\">Directory relative: /test/test.html = http://example.com/test/test.html</a></li>\n <li><a href=\"/test.html\">Relative to root: /test.html = http://example.com/test.html</a></li>\n </ul>\n <img src=\"images/photo.gif\" />\n </div> \n</div>","name":"mf-v2-h-entry-urlincontent"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The 4th Microformat party\"],\n \"start\": [\n \"2009-06-26 19:00:00\", \n \"2009-06-26 07:00:00\", \n \"2009-06-26 19:00\", \n \"2009-06-26 19\", \n \"2009-06-26 19\", \n \"2009-06-26 19:00\", \n \"2009-06-26 19:00\", \n \"2009-06-26 19:00\", \n \"2009-06-26 07:00\"\n ]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<span class=\"h-event\">\n <span class=\"p-name\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00:00am \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00pm \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00p.m. \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">07:00PM \n </span></li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <span class=\"value\">7:00am \n </span></li>\n </ul>\n</span>","name":"mf-v2-h-event-ampm"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"CPJ Online Press Freedom Summit\"],\n \"start\": [\"2012-10-10\"],\n \"location\": [\"San Francisco\"],\n \"attendee\": [{\n \"value\": \"Brian Warner\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Brian Warner\"]\n }\n }, {\n \"value\": \"Kyle Machulis\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Kyle Machulis\"]\n }\n }, {\n \"value\": \"Tantek Çelik\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek Çelik\"]\n }\n }, {\n \"value\": \"Sid Sutter\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Sid Sutter\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"h-event\">\n <span class=\"p-name\">CPJ Online Press Freedom Summit</span>\n (<time class=\"dt-start\" datetime=\"2012-10-10\">10 Nov 2012</time>) in\n <span class=\"p-location\">San Francisco</span>.\n Attendees:\n <ul>\n <li class=\"p-attendee h-card\">Brian Warner</li>\n <li class=\"p-attendee h-card\">Kyle Machulis</li>\n <li class=\"p-attendee h-card\">Tantek Çelik</li>\n <li class=\"p-attendee h-card\">Sid Sutter</li>\n </ul>\n</div>\n","name":"mf-v2-h-event-attendees"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"IndieWebCamp 2012\"],\n \"url\": [\"http://indiewebcamp.com/2012\"],\n \"start\": [\"2012-06-30\"],\n \"end\": [\"2012-07-01\"],\n \"location\": [{\n \"value\": \"Geoloqi\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Geoloqi\"],\n \"org\": [\"Geoloqi\"],\n \"url\": [\"http://geoloqi.com/\"],\n \"street-address\": [\"920 SW 3rd Ave. Suite 400\"],\n \"locality\": [\"Portland\"],\n \"region\": [\"Oregon\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-event\">\n <a class=\"p-name u-url\" href=\"http://indiewebcamp.com/2012\">\n IndieWebCamp 2012\n </a>\n from <time class=\"dt-start\">2012-06-30</time> \n to <time class=\"dt-end\">2012-07-01</time> at \n <span class=\"p-location h-card\">\n <a class=\"p-name p-org u-url\" href=\"http://geoloqi.com/\">Geoloqi</a>, \n <span class=\"p-street-address\">920 SW 3rd Ave. Suite 400</span>, \n <span class=\"p-locality\">Portland</span>, \n <abbr class=\"p-region\" title=\"Oregon\">OR</abbr>\n </span>\n</div>","name":"mf-v2-h-event-combining"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The 4th Microformat party\"],\n \"start\": [\"2009-06-26 19:00\"],\n \"end\": [\"2009-06-26 22:00\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<span class=\"h-event\">\n <span class=\"p-name\">The 4th Microformat party</span> will be on \n <span class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time></span> to \n <span class=\"dt-end\"><time class=\"value\">22:00</time></span>.\n</span>","name":"mf-v2-h-event-concatenate"},{"json":"{\n \"items\": [\n {\n \"type\": [\n \"h-event\"\n ],\n \"properties\": {\n \"name\": [\n \"The 4th Microformat party\"\n ],\n \"start\": [\n \"2009-06-26 19:00-08:00\",\n \"2009-06-26 19:00-08\",\n \"2009-06-26 19:00-08:00\",\n \"2009-06-26 19:00+08:00\",\n \"2009-06-26 19:00+08:00\",\n \"2009-06-26 19:00Z\",\n \"2009-06-26 19:00-08:00\",\n \"2009-06-26 19:00:00-08:00\"\n ]\n }\n }\n ],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<section class=\"h-event\">\n\t<p><span class=\"p-name\">The 4th Microformat party</span> will be on:</p>\n\t<ul>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00-08:00\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00-08\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00-0800\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00+0800\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00+08:00\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26T19:00Z\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26t19:00-08:00\">26 July</time></li>\n\t\t<li><time class=\"dt-start\" datetime=\"2009-06-26 19:00:00-08:00\">26 July</time></li>\n\t</ul>\n</section>","name":"mf-v2-h-event-dates"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The party\"],\n \"start\": [\n \"2013-03-14\", \n \"2013-06-25 07:00:00\", \n \"2013-06-26\", \n \"2013-06-27\", \n \"2013-06-28\", \n \"2013-06-29\", \n \"2013-07-01\", \n \"2013-07-02\"\n ]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<span class=\"h-event\">\n <span class=\"p-name\">The party</span> will be on \n \n <p class=\"dt-start\">\n <span class=\"value-title\" title=\"2013-03-14\"> </span>\n March 14th 2013\n </p>\n <p class=\"dt-start\">\n <time class=\"value\" datetime=\"2013-06-25\">25 July</time>, from\n <span class=\"value\">07:00:00am \n </span></p> \n \n <p>\n <time class=\"dt-start\" datetime=\"2013-06-26\">26 June</time>\n \n <ins class=\"dt-start\" datetime=\"2013-06-27\">Just added</ins>, \n <del class=\"dt-start\" datetime=\"2013-06-28\">Removed</del>\n </p>\n <abbr class=\"dt-start\" title=\"2013-06-29\">June 29</abbr> \n <data class=\"dt-start\" value=\"2013-07-01\"></data>\n <p class=\"dt-start\">2013-07-02</p>\n \n</span>","name":"mf-v2-h-event-dt-property"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"IndieWebCamp 2012\"],\n \"url\": [\"http://indiewebcamp.com/2012\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-event\" href=\"http://indiewebcamp.com/2012\">IndieWebCamp 2012</a>","name":"mf-v2-h-event-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"IndieWebCamp 2012\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-event\">IndieWebCamp 2012</p>","name":"mf-v2-h-event-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"The 4th Microformat party\"],\n \"start\": [\n \"2009-06-26 19:00:00-08:00\", \n \"2009-06-26 19:00:00-08:00\", \n \"2009-06-26 19:00:00+08:00\", \n \"2009-06-26 19:00:00Z\", \n \"2009-06-26 19:00:00\", \n \"2009-06-26 19:00-08:00\", \n \"2009-06-26 19:00+08:00\", \n \"2009-06-26 19:00Z\", \n \"2009-06-26 19:00\"\n ],\n \"end\": [\n \"2013-034\", \n \"2013-06-27 15:34\"\n ]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<span class=\"h-event\">\n <span class=\"p-name\">The 4th Microformat party</span> will be on \n <ul>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-08:00</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00-0800</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00+0800</time> \n </li> \n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00Z</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00:00</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00-08:00</time> \n </li> \n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00+08:00</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00Z</time> \n </li>\n <li class=\"dt-start\">\n <time class=\"value\" datetime=\"2009-06-26\">26 July</time>, from\n <time class=\"value\">19:00</time> \n </li> \n <li>\n <time class=\"dt-end\" datetime=\"2013-034\">3 February 2013</time>\n </li>\n <li>\n <time class=\"dt-end\" datetime=\"2013-06-27 15:34\">26 July 2013</time>\n </li> \n </ul>\n</span>","name":"mf-v2-h-event-time"},{"json":"{\n \"items\": [{\n \"type\": [\"h-feed\"],\n \"properties\": {\n \"name\": [\"microformats blog\"]\n },\n \"children\": [{\n \"value\": \"microformats.org at 7\\n\\t\\t \\n\\t\\t Last week the microformats.org community \\n\\t\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t\\t San Francisco and recognized accomplishments, challenges, and \\n\\t\\t opportunities.\\n\\t\\t\\n\\t\\t The microformats tagline “humans first, machines second” \\n\\t\\t forms the basis of many of our \\n\\t\\t principles, and \\n\\t\\t in that regard, we’d like to recognize a few people and \\n\\t\\t thank them for their years of volunteer service \\n\\t\\t \\n\\t\\t Updated \\n\\t\\t June 25th, 2012\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n\\t\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t\\t San Francisco and recognized accomplishments, challenges, and \\n\\t\\t opportunities.\\n\\t\\t\\n\\t\\t The microformats tagline “humans first, machines second” \\n\\t\\t forms the basis of many of our \\n\\t\\t principles, and \\n\\t\\t in that regard, we’d like to recognize a few people and \\n\\t\\t thank them for their years of volunteer service\",\n \"html\": \"\\n\\t\\t <p class=\\\"p-summary\\\">Last week the microformats.org community \\n\\t\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t\\t San Francisco and recognized accomplishments, challenges, and \\n\\t\\t opportunities.</p>\\n\\t\\t\\n\\t\\t <p>The microformats tagline “humans first, machines second” \\n\\t\\t forms the basis of many of our \\n\\t\\t <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n\\t\\t in that regard, we’d like to recognize a few people and \\n\\t\\t thank them for their years of volunteer service </p>\\n\\t\\t \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n\\t\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t\\t San Francisco and recognized accomplishments, challenges, and \\n\\t\\t opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"]\n }\n }]\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"\n<html>\n\t<head>\n\t\t<title>microformats blog</title>\n\t</head>\n\t<body>\n\t<section class=\"h-feed\">\n\t\t\n\t\t<div class=\"h-entry\">\n\t\t <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n\t\t <div class=\"e-content\">\n\t\t <p class=\"p-summary\">Last week the microformats.org community \n\t\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t\t San Francisco and recognized accomplishments, challenges, and \n\t\t opportunities.</p>\n\t\t\n\t\t <p>The microformats tagline “humans first, machines second” \n\t\t forms the basis of many of our \n\t\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t\t in that regard, we’d like to recognize a few people and \n\t\t thank them for their years of volunteer service </p>\n\t\t </div> \n\t\t <p>Updated \n\t\t <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time>\n\t\t </p>\n\t\t</div>\n\t\t\n\t</section>\n\t</body>\n</html>","name":"mf-v2-h-feed-implied-title"},{"json":"{\n \"items\": [{\n \"type\": [\"h-feed\"],\n \"properties\": {\n \"name\": [\"Microformats blog\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }],\n \"url\": [\"http://microformats.org/blog\"],\n \"photo\": [\"http://example.com/photo.jpeg\"]\n },\n \"children\": [{\n \"value\": \"microformats.org at 7\\n\\t \\n\\t Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.\\n\\t\\n\\t The microformats tagline “humans first, machines second” \\n\\t forms the basis of many of our \\n\\t principles, and \\n\\t in that regard, we’d like to recognize a few people and \\n\\t thank them for their years of volunteer service \\n\\t \\n\\t Updated \\n\\t June 25th, 2012\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.\\n\\t\\n\\t The microformats tagline “humans first, machines second” \\n\\t forms the basis of many of our \\n\\t principles, and \\n\\t in that regard, we’d like to recognize a few people and \\n\\t thank them for their years of volunteer service\",\n \"html\": \"\\n\\t <p class=\\\"p-summary\\\">Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.</p>\\n\\t\\n\\t <p>The microformats tagline “humans first, machines second” \\n\\t forms the basis of many of our \\n\\t <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n\\t in that regard, we’d like to recognize a few people and \\n\\t thank them for their years of volunteer service </p>\\n\\t \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n\\t celebrated its 7th birthday at a gathering hosted by Mozilla in \\n\\t San Francisco and recognized accomplishments, challenges, and \\n\\t opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"]\n }\n }]\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<section class=\"h-feed\">\n\t<h1 class=\"p-name\">Microformats blog</h1>\n\t<a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n\t<a class=\"u-url\" href=\"http://microformats.org/blog\">permlink</a>\n\t<img class=\"u-photo\" src=\"photo.jpeg\"/>\n\t\n\t<div class=\"h-entry\">\n\t <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n\t <div class=\"e-content\">\n\t <p class=\"p-summary\">Last week the microformats.org community \n\t celebrated its 7th birthday at a gathering hosted by Mozilla in \n\t San Francisco and recognized accomplishments, challenges, and \n\t opportunities.</p>\n\t\n\t <p>The microformats tagline “humans first, machines second” \n\t forms the basis of many of our \n\t <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n\t in that regard, we’d like to recognize a few people and \n\t thank them for their years of volunteer service </p>\n\t </div> \n\t <p>Updated \n\t <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time>\n\t </p>\n\t</div>\n\t\n</section>","name":"mf-v2-h-feed-simple"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"37.408183\"],\n \"longitude\": [\"-122.13855\"],\n \"name\": [\"N 37° 24.491, \\n W 122° 08.313\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<p class=\"h-geo\">\n <abbr class=\"p-latitude\" title=\"37.408183\">N 37° 24.491</abbr>, \n <abbr class=\"p-longitude\" title=\"-122.13855\">W 122° 08.313</abbr>\n</p>","name":"mf-v2-h-geo-abbrpattern"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"name\": [\"Pen-y-ghent\"],\n \"latitude\": [\"54.155278\"],\n \"longitude\": [\"-2.249722\"],\n \"altitude\": [\"694\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p>My favourite hill in the lakes is \n <span class=\"h-geo\">\n <span class=\"p-name\">Pen-y-ghent</span> \n (Geo: <span class=\"p-latitude\">54.155278</span>,\n <span class=\"p-longitude\">-2.249722</span>). It\n raises to <span class=\"p-altitude\">694</span>m.\n </span>\n</p>","name":"mf-v2-h-geo-altitude"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"51.513458\"],\n \"longitude\": [\"-0.14812\"],\n \"name\": [\"The Bricklayer's Arms\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p>\n <span class=\"h-geo\">The Bricklayer's Arms\n <span class=\"p-latitude\">\n <span class=\"value-title\" title=\"51.513458\"> </span> \n </span>\n <span class=\"p-longitude\">\n <span class=\"value-title\" title=\"-0.14812\"> </span>\n </span>\n </span>\n</p>","name":"mf-v2-h-geo-hidden"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"name\": [\"51.513458;-0.14812\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p>On my way to The Bricklayer's Arms\n (Geo: <span class=\"h-geo\">51.513458;-0.14812</span>)\n</p>","name":"mf-v2-h-geo-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"name\": [\"The Bricklayer's Arms\"],\n \"latitude\": [\"51.513458\"],\n \"longitude\": [\"-0.14812\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-geo\">We are meeting at \n <span class=\"p-name\">The Bricklayer's Arms</span>\n (Geo: <span class=\"p-latitude\">51.513458</span>:\n <span class=\"p-longitude\">-0.14812</span>)\n</p>","name":"mf-v2-h-geo-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-geo\"],\n \"properties\": {\n \"latitude\": [\"51.513458\"],\n \"longitude\": [\"-0.14812\"],\n \"name\": [\"N 51° 51.345, \\n \\n \\n W -0° 14.812\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<p>\n <span class=\"h-geo\">\n <span class=\"p-latitude\">\n <span class=\"value-title\" title=\"51.513458\">N 51° 51.345</span>, \n </span>\n <span class=\"p-longitude\">\n <span class=\"value-title\" title=\"-0.14812\">W -0° 14.812</span>\n </span>\n </span>\n</p>","name":"mf-v2-h-geo-valuetitleclass"},{"json":"{\n \"items\": [{\n \"type\": [\"h-news\"],\n \"properties\": {\n \"entry\": [{\n \"value\": \"microformats.org at 7\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service\",\n \"html\": \"\\n <p class=\\\"p-summary\\\">Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.</p>\\n\\n <p>The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service </p>\\n \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }]\n }\n }],\n \"dateline\": [{\n \"value\": \"San Francisco, \\n CA\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"San Francisco\"],\n \"region\": [\"CA\"],\n \"name\": [\"San Francisco, \\n CA\"]\n }\n }],\n \"geo\": [\"37.774921;-122.445202\"],\n \"source-org\": [{\n \"value\": \"microformats.org\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"microformats.org\"],\n \"url\": [\"http://microformats.org/\"]\n }\n }],\n \"principles\": [\"http://microformats.org/wiki/Category:public_domain_license\"],\n \"name\": [\"microformats.org at 7\\n \\n Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service \\n \\n Updated \\n June 25th, 2012 by\\n Tantek\\n \\n \\n\\n \\n \\n San Francisco, \\n CA \\n \\n (Geo: 37.774921;-122.445202) \\n \\n microformats.org\\n \\n \\n \\n Publishing policy\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-news\">\n <div class=\"p-entry h-entry\">\n <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"e-content\">\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n </p>\n </div>\n\n <p>\n <span class=\"p-dateline h-adr\">\n <span class=\"p-locality\">San Francisco</span>, \n <span class=\"p-region\">CA</span> \n </span>\n (Geo: <span class=\"p-geo\">37.774921;-122.445202</span>) \n <span class=\"p-source-org h-card\">\n <a class=\"p-name u-url\" href=\"http://microformats.org/\">microformats.org</a>\n </span>\n </p>\n <p>\n <a class=\"u-principles\" href=\"http://microformats.org/wiki/Category:public_domain_license\">Publishing policy</a>\n </p>\n</div>","name":"mf-v2-h-news-all"},{"json":"{\n \"items\": [{\n \"type\": [\"h-news\"],\n \"properties\": {\n \"entry\": [{\n \"value\": \"microformats.org at 7\",\n \"type\": [\"h-entry\"],\n \"properties\": {\n \"name\": [\"microformats.org at 7\"],\n \"url\": [\"http://microformats.org/2012/06/25/microformats-org-at-7\"],\n \"content\": [{\n \"value\": \"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service\",\n \"html\": \"\\n <p class=\\\"p-summary\\\">Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.</p>\\n\\n <p>The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n <a href=\\\"http://microformats.org/wiki/principles\\\">principles</a>, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service </p>\\n \"\n }],\n \"summary\": [\"Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\"],\n \"updated\": [\"2012-06-25 17:08:26\"],\n \"author\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"],\n \"url\": [\"http://tantek.com/\"]\n }\n }]\n }\n }],\n \"source-org\": [{\n \"value\": \"microformats.org\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"microformats.org\"],\n \"url\": [\"http://microformats.org/\"]\n }\n }],\n \"name\": [\"microformats.org at 7\\n \\n Last week the microformats.org community \\n celebrated its 7th birthday at a gathering hosted by Mozilla in \\n San Francisco and recognized accomplishments, challenges, and \\n opportunities.\\n\\n The microformats tagline “humans first, machines second” \\n forms the basis of many of our \\n principles, and \\n in that regard, we’d like to recognize a few people and \\n thank them for their years of volunteer service \\n \\n Updated \\n June 25th, 2012 by\\n Tantek\\n \\n \\n \\n microformats.org\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-news\">\n <div class=\"p-entry h-entry\">\n <h1><a class=\"p-name u-url\" href=\"http://microformats.org/2012/06/25/microformats-org-at-7\">microformats.org at 7</a></h1>\n <div class=\"e-content\">\n <p class=\"p-summary\">Last week the microformats.org community \n celebrated its 7th birthday at a gathering hosted by Mozilla in \n San Francisco and recognized accomplishments, challenges, and \n opportunities.</p>\n\n <p>The microformats tagline “humans first, machines second” \n forms the basis of many of our \n <a href=\"http://microformats.org/wiki/principles\">principles</a>, and \n in that regard, we’d like to recognize a few people and \n thank them for their years of volunteer service </p>\n </div> \n <p>Updated \n <time class=\"dt-updated\" datetime=\"2012-06-25T17:08:26\">June 25th, 2012</time> by\n <a class=\"p-author h-card\" href=\"http://tantek.com/\">Tantek</a>\n </p>\n </div>\n <p>\n <a class=\"p-source-org h-card\" href=\"http://microformats.org/\">microformats.org</a> \n </p>\n</div>","name":"mf-v2-h-news-minimum"},{"json":"{\n \"items\": [{\n \"type\": [\"h-org\"],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"],\n \"url\": [\"http://mozilla.org/\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-org\" href=\"http://mozilla.org/\">Mozilla Foundation</a>","name":"mf-v2-h-org-hyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-org\"],\n \"properties\": {\n \"name\": [\"Mozilla Foundation\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<span class=\"h-org\">Mozilla Foundation</span>","name":"mf-v2-h-org-simple"},{"json":"{\n \"items\": [{\n \"type\": [\"h-org\"],\n \"properties\": {\n \"organization-name\": [\"W3C\"],\n \"organization-unit\": [\"CSS Working Group\"],\n \"name\": [\"W3C - \\n CSS Working Group\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-org\">\n <span class=\"p-organization-name\">W3C</span> - \n <span class=\"p-organization-unit\">CSS Working Group</span>\n</p>","name":"mf-v2-h-org-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-product\"],\n \"properties\": {\n \"name\": [\"Raspberry Pi\"],\n \"photo\": [\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\"],\n \"description\": [{\n \"value\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\",\n \"html\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\"\n }],\n \"url\": [\"http://www.raspberrypi.org/\"],\n \"price\": [\"£29.95\"],\n \"review\": [{\n \"value\": \"9.2 out of \\n 10 \\n based on 178 reviews\",\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"rating\": [{\n \"value\": \"9.2 out of \\n 10 \\n based on 178 reviews\",\n \"type\": [\"h-rating\"],\n \"properties\": {\n \"average\": [\"9.2\"],\n \"best\": [\"10\"],\n \"count\": [\"178\"],\n \"name\": [\"9.2 out of \\n 10 \\n based on 178 reviews\"]\n }\n }],\n \"name\": [\"9.2 out of \\n 10 \\n based on 178 reviews\"]\n }\n }],\n \"category\": [\"Computer\", \"Education\"],\n \"brand\": [{\n \"value\": \"The Raspberry Pi Foundation\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"The Raspberry Pi Foundation\"],\n \"org\": [\"The Raspberry Pi Foundation\"],\n \"locality\": [\"Cambridge\"],\n \"country-name\": [\"UK\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"h-product\">\n <h2 class=\"p-name\">Raspberry Pi</h2>\n <img class=\"u-photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"e-description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"u-url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"p-price\">£29.95</p>\n <p class=\"p-review h-review-aggregate\">\n <span class=\"p-rating h-rating\">\n <span class=\"p-average\">9.2</span> out of \n <span class=\"p-best\">10</span> \n based on <span class=\"p-count\">178</span> reviews\n </span>\n </p>\n <p>Categories: <span class=\"p-category\">Computer</span>, <span class=\"p-category\">Education</span></p>\n <p class=\"p-brand h-card\">From: \n <span class=\"p-name p-org\">The Raspberry Pi Foundation</span> - \n <span class=\"p-locality\">Cambridge</span> \n <span class=\"p-country-name\">UK</span>\n </p>\n</div>","name":"mf-v2-h-product-aggregate"},{"json":"{\n \"items\": [{\n \"type\": [\"h-product\"],\n \"properties\": {\n \"name\": [\"Raspberry Pi\"],\n \"url\": [\"http://www.raspberrypi.org/\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-product\" href=\"http://www.raspberrypi.org/\">Raspberry Pi</a>","name":"mf-v2-h-product-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-product\"],\n \"properties\": {\n \"name\": [\"Raspberry Pi\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-product\">Raspberry Pi</p>","name":"mf-v2-h-product-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-product\"],\n \"properties\": {\n \"name\": [\"Raspberry Pi\"],\n \"photo\": [\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\"],\n \"description\": [{\n \"value\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\",\n \"html\": \"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.\"\n }],\n \"url\": [\"http://www.raspberrypi.org/\"],\n \"price\": [\"£29.95\"],\n \"category\": [\"Computer\", \"Education\"],\n \"review\": [{\n \"value\": \"4.5 out of 5\",\n \"type\": [\"h-review\"],\n \"properties\": {\n \"rating\": [\"4.5\"],\n \"name\": [\"4.5 out of 5\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"h-product\">\n <h2 class=\"p-name\">Raspberry Pi</h2>\n <img class=\"u-photo\" src=\"http://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/RaspberryPi.jpg/320px-RaspberryPi.jpg\" />\n <p class=\"e-description\">The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.</p>\n <a class=\"u-url\" href=\"http://www.raspberrypi.org/\">More info about the Raspberry Pi</a>\n <p class=\"p-price\">£29.95</p>\n <p class=\"p-review h-review\"><span class=\"p-rating\">4.5</span> out of 5</p>\n <p>Categories: <span class=\"p-category\">Computer</span>, <span class=\"p-category\">Education</span></p>\n</div>","name":"mf-v2-h-product-simpleproperties"},{"json":"{\n \"items\": [{\n \"type\": [\"h-recipe\"],\n \"properties\": {\n \"name\": [\"Yorkshire Puddings\"],\n \"summary\": [\"Makes 6 good sized Yorkshire puddings, the way my mum taught me\"],\n \"yield\": [\"6 good sized Yorkshire puddings\"],\n \"photo\": [\"http://codebits.glennjones.net/semantic/yorkshire-puddings.jpg\"],\n \"review\": [{\n \"value\": \"4.5 stars out 5 based on \\n 35 reviews\",\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"rating\": [\"4.5 stars out 5 based on\"],\n \"average\": [\"4.5\"],\n \"count\": [\"35\"],\n \"name\": [\"4.5 stars out 5 based on \\n 35 reviews\"]\n }\n }],\n \"ingredient\": [{\n \"value\": \"1 egg\",\n \"html\": \"1 egg\"\n }, {\n \"value\": \"75g plain flour\",\n \"html\": \"75g plain flour\"\n }, {\n \"value\": \"70ml milk\",\n \"html\": \"70ml milk\"\n }, {\n \"value\": \"60ml water\",\n \"html\": \"60ml water\"\n }, {\n \"value\": \"Pinch of salt\",\n \"html\": \"Pinch of salt\"\n }],\n \"instructions\": [{\n \"value\": \"Pre-heat oven to 230C or gas mark 8. Pour the vegetable oil evenly into 2 x 4-hole \\n Yorkshire pudding tins and place in the oven to heat through. \\n \\n To make the batter, add all the flour into a bowl and beat in the eggs until smooth. \\n Gradually add the milk and water while beating the mixture. It should be smooth and \\n without lumps. Finally add a pinch of salt.\\n \\n Make sure the oil is piping hot before pouring the batter evenly into the tins. \\n Place in the oven for 20-25 minutes until pudding have risen and look golden brown\",\n \"html\": \"\\n <ol>\\n <li>Pre-heat oven to 230C or gas mark 8. Pour the vegetable oil evenly into 2 x 4-hole \\n Yorkshire pudding tins and place in the oven to heat through.</li> \\n \\n <li>To make the batter, add all the flour into a bowl and beat in the eggs until smooth. \\n Gradually add the milk and water while beating the mixture. It should be smooth and \\n without lumps. Finally add a pinch of salt.</li>\\n \\n <li>Make sure the oil is piping hot before pouring the batter evenly into the tins. \\n Place in the oven for 20-25 minutes until pudding have risen and look golden brown</li>\\n </ol>\\n \"\n }],\n \"nutrition\": [\"Calories: 125\", \"Fat: 3.2g\", \"Cholesterol: 77mg\"],\n \"published\": [\"2011-10-27\"],\n \"author\": [{\n \"value\": \"Glenn Jones\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Glenn Jones\"],\n \"url\": [\"http://glennjones.net\"]\n }\n }],\n \"url\": [\"http://www.flickr.com/photos/dithie/4106528495/\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<section class=\"h-recipe\">\n <h1 class=\"p-name\">Yorkshire Puddings</h1> \n <p class=\"p-summary\">Makes <span class=\"p-yield\">6 good sized Yorkshire puddings</span>, the way my mum taught me</p>\n\n\n <p><img class=\"u-photo\" src=\"http://codebits.glennjones.net/semantic/yorkshire-puddings.jpg\" /></p>\n\n <span class=\"p-review h-review-aggregate\">\n <span class=\"p-rating\">\n <span class=\"p-average\">4.5</span> stars out 5 based on </span>\n <span class=\"p-count\">35</span> reviews</span>\n \n \n\n <div id=\"ingredients-container\">\n <h3>Ingredients</h3>\n <ul>\n <li class=\"e-ingredient\">1 egg</li>\n <li class=\"e-ingredient\">75g plain flour</li>\n <li class=\"e-ingredient\">70ml milk</li>\n <li class=\"e-ingredient\">60ml water</li>\n <li class=\"e-ingredient\">Pinch of salt</li>\n </ul>\n </div>\n\n <h3>Time</h3>\n <ul>\n <li class=\"prepTime\">Preparation <span class=\"value-title\" title=\"PT0H10M\">10 mins</span></li>\n <li class=\"cookTime\">Cook <span class=\"value-title\" title=\"PT0H25M\">25 mins</span></li>\n </ul> \n\n\n <h3>Instructions</h3>\n <div class=\"e-instructions\">\n <ol>\n <li>Pre-heat oven to 230C or gas mark 8. Pour the vegetable oil evenly into 2 x 4-hole \n Yorkshire pudding tins and place in the oven to heat through.</li> \n \n <li>To make the batter, add all the flour into a bowl and beat in the eggs until smooth. \n Gradually add the milk and water while beating the mixture. It should be smooth and \n without lumps. Finally add a pinch of salt.</li>\n \n <li>Make sure the oil is piping hot before pouring the batter evenly into the tins. \n Place in the oven for 20-25 minutes until pudding have risen and look golden brown</li>\n </ol>\n </div>\n\n <h3>Nutrition</h3>\n <ul id=\"nutrition-list\">\n <li class=\"p-nutrition\">Calories: <span class=\"calories\">125</span></li>\n <li class=\"p-nutrition\">Fat: <span class=\"fat\">3.2g</span></li>\n <li class=\"p-nutrition\">Cholesterol: <span class=\"cholesterol\">77mg</span></li>\n </ul>\n <p>(Amount per pudding)</p>\n\n <p>\n Published on <time class=\"dt-published\" datetime=\"2011-10-27\">27 Oct 2011</time> by \n <span class=\"p-author h-card\">\n <a class=\"p-name u-url\" href=\"http://glennjones.net\">Glenn Jones</a>\n </span>\n </p>\n <a href=\"http://www.flickr.com/photos/dithie/4106528495/\">Photo by dithie</a>\n </section>","name":"mf-v2-h-recipe-all"},{"json":"{\n \"items\": [{\n \"type\": [\"h-recipe\"],\n \"properties\": {\n \"name\": [\"Toast\"],\n \"ingredient\": [{\n \"value\": \"Slice of bread\",\n \"html\": \"Slice of bread\"\n }, {\n \"value\": \"Butter\",\n \"html\": \"Butter\"\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-recipe\"> \n <p class=\"p-name\">Toast</p>\n <ul>\n <li class=\"e-ingredient\">Slice of bread</li>\n <li class=\"e-ingredient\">Butter</li>\n </ul>\n</div>","name":"mf-v2-h-recipe-minimum"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"summary\": [\"invented the World Wide Web\"],\n \"affiliation\": [{\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"W3C\"],\n \"photo\": [\"http://www.w3.org/Icons/WWW/w3c_home_nb.png\"],\n \"url\": [\"http://www.w3.org/\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-resume\">\n <p>\n <span class=\"p-name\">Tim Berners-Lee</span>, \n <span class=\"p-summary\">invented the World Wide Web</span>. \n </p> \n Belongs to following groups:\n <p> \n <a class=\"p-affiliation h-card\" href=\"http://www.w3.org/\">\n <img class=\"p-name u-photo\" alt=\"W3C\" src=\"http://www.w3.org/Icons/WWW/w3c_home_nb.png\" />\n </a>\n </p> \n</div>","name":"mf-v2-h-resume-affiliation"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"summary\": [\"Invented the World Wide Web.\"],\n \"contact\": [{\n \"value\": \"MIT\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"MIT\"],\n \"street-address\": [\"32 Vassar Street\"],\n \"extended-address\": [\"Room 32-G524\"],\n \"locality\": [\"Cambridge\"],\n \"region\": [\"MA\"],\n \"postal-code\": [\"02139\"],\n \"country-name\": [\"USA\"],\n \"tel\": [\"+1 (617) 253 5702\"],\n \"email\": [\"mailto:timbl@w3.org\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-resume\">\n <p class=\"p-name\">Tim Berners-Lee</p>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <div class=\"p-contact h-card\">\n <p class=\"p-name\">MIT</p>\n <p>\n <span class=\"p-street-address\">32 Vassar Street</span>, \n <span class=\"p-extended-address\">Room 32-G524</span>, \n <span class=\"p-locality\">Cambridge</span>, \n <span class=\"p-region\">MA</span> \n <span class=\"p-postal-code\">02139</span>, \n <span class=\"p-country-name\">USA</span>.\n </p>\n <p>Tel:<span class=\"p-tel\">+1 (617) 253 5702</span></p>\n <p>Email:<a class=\"u-email\" href=\"mailto:timbl@w3.org\">timbl@w3.org</a></p>\n </div>\n</div>","name":"mf-v2-h-resume-contact"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"contact\": [{\n \"value\": \"Director of the World Wide Web Foundation\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"title\": [\"Director of the World Wide Web Foundation\"],\n \"name\": [\"Director of the World Wide Web Foundation\"]\n }\n }],\n \"summary\": [\"Invented the World Wide Web.\"],\n \"education\": [{\n \"value\": \"The Queen's College, Oxford University\",\n \"type\": [\"h-event\", \"h-card\"],\n \"properties\": {\n \"name\": [\"The Queen's College, Oxford University\"],\n \"org\": [\"The Queen's College, Oxford University\"],\n \"description\": [\"BA Hons (I) Physics\"],\n \"start\": [\"1973-09\"],\n \"end\": [\"1976-06\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-resume\">\n <p class=\"p-name\">Tim Berners-Lee</p>\n <div class=\"p-contact h-card\">\n <p class=\"p-title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <p class=\"p-education h-event h-card\">\n <span class=\"p-name p-org\">The Queen's College, Oxford University</span>, \n <span class=\"p-description\">BA Hons (I) Physics</span> \n <time class=\"dt-start\" datetime=\"1973-09\">1973</time> –\n <time class=\"dt-end\" datetime=\"1976-06\">1976</time>\n </p>\n</div>","name":"mf-v2-h-resume-education"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee, invented the World Wide Web.\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-resume\">Tim Berners-Lee, invented the World Wide Web.</p>","name":"mf-v2-h-resume-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"summary\": [\"invented the World Wide Web\"],\n \"skill\": [\"information systems\", \"advocacy\", \"leadership\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-resume\">\n <p>\n <span class=\"p-name\">Tim Berners-Lee</span>, \n <span class=\"p-summary\">invented the World Wide Web</span>.\n </p>\n Skills: \n <ul>\n <li class=\"p-skill\">information systems</li>\n <li class=\"p-skill\">advocacy</li>\n <li class=\"p-skill\">leadership</li>\n <ul> \n</ul></ul></div>","name":"mf-v2-h-resume-skill"},{"json":"{\n \"items\": [{\n \"type\": [\"h-resume\"],\n \"properties\": {\n \"name\": [\"Tim Berners-Lee\"],\n \"contact\": [{\n \"value\": \"Director of the World Wide Web Foundation\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"title\": [\"Director of the World Wide Web Foundation\"],\n \"name\": [\"Director of the World Wide Web Foundation\"]\n }\n }],\n \"summary\": [\"Invented the World Wide Web.\"],\n \"experience\": [{\n \"value\": \"World Wide Web Foundation\",\n \"type\": [\"h-event\", \"h-card\"],\n \"properties\": {\n \"title\": [\"Director\"],\n \"name\": [\"World Wide Web Foundation\"],\n \"org\": [\"World Wide Web Foundation\"],\n \"url\": [\"http://www.webfoundation.org/\"],\n \"start\": [\"2009-01-18\"],\n \"duration\": [\"P2Y11M\"]\n }\n }]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<meta charset=\"utf-8\">\n<div class=\"h-resume\">\n <p class=\"p-name\">Tim Berners-Lee</p>\n <div class=\"p-contact h-card\">\n <p class=\"p-title\">Director of the World Wide Web Foundation</p>\n </div>\n <p class=\"p-summary\">Invented the World Wide Web.</p><hr />\n <div class=\"p-experience h-event h-card\">\n <p class=\"p-title\">Director</p>\n <p><a class=\"p-name p-org u-url\" href=\"http://www.webfoundation.org/\">World Wide Web Foundation</a></p>\n <p>\n <time class=\"dt-start\" datetime=\"2009-01-18\">Jan 2009</time> – Present\n <time class=\"dt-duration\" datetime=\"P2Y11M\">(2 years 11 month)</time>\n </p>\n </div>\n</div>","name":"mf-v2-h-resume-work"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"name\": [\"Crepes on Cole\"],\n \"url\": [\"https://plus.google.com/116941523817079328322/about\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<a class=\"h-review\" href=\"https://plus.google.com/116941523817079328322/about\">Crepes on Cole</a>","name":"mf-v2-h-review-hyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Crepes on Cole\",\n \"type\": [\"h-item\"],\n \"properties\": {\n \"name\": [\"Crepes on Cole\"],\n \"url\": [\"http://example.com/crepeoncole\"]\n }\n }],\n \"rating\": [\"4.7\"],\n \"name\": [\"Crepes on Cole\\n 4.7 out of 5 stars\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-review\">\n <a class=\"p-item h-item\" href=\"http://example.com/crepeoncole\">Crepes on Cole</a>\n <p><span class=\"p-rating\">4.7</span> out of 5 stars</p>\n</div>","name":"mf-v2-h-review-implieditem"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Crepes on Cole\",\n \"type\": [\"h-item\"],\n \"properties\": {\n \"photo\": [\"http://example.com/images/photo.gif\"],\n \"name\": [\"Crepes on Cole\"],\n \"url\": [\"http://example.com/crepeoncole\"]\n }\n }],\n \"rating\": [\"5\"],\n \"name\": [\"Crepes on Cole\\n \\n 5 out of 5 stars\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<base href=\"http://example.com\" >\n<div class=\"h-review\">\n <p class=\"p-item h-item\">\n <img class=\"u-photo\" src=\"images/photo.gif\" />\n <a class=\"p-name u-url\" href=\"http://example.com/crepeoncole\">Crepes on Cole</a>\n </p>\n <p><span class=\"p-rating\">5</span> out of 5 stars</p>\n</div>","name":"mf-v2-h-review-item"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"name\": [\"Crepes on Cole\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<p class=\"h-review\">Crepes on Cole</p>","name":"mf-v2-h-review-justaname"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"name\": [\"Crepes on Cole\"],\n \"photo\": [\"http://example.com/images/photo.gif\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<base href=\"http://example.com\" ><img class=\"h-review\" src=\"images/photo.gif\" alt=\"Crepes on Cole\" />","name":"mf-v2-h-review-photo"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review\"],\n \"properties\": {\n \"rating\": [\"5\"],\n \"name\": [\"Crepes on Cole is awesome\"],\n \"reviewer\": [{\n \"value\": \"Tantek\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Tantek\"]\n }\n }],\n \"reviewed\": [\"2005-04-18\"],\n \"description\": [{\n \"value\": \"Crepes on Cole is one of the best little \\n creperies in San Francisco.\\n Excellent food and service. Plenty of tables in a variety of sizes \\n for parties large and small. Window seating makes for excellent \\n people watching to/from the N-Judah which stops right outside. \\n I've had many fun social gatherings here, as well as gotten \\n plenty of work done thanks to neighborhood WiFi.\",\n \"html\": \"\\n <p class=\\\"p-item h-card\\\">\\n <span class=\\\"p-name p-org\\\">Crepes on Cole</span> is one of the best little \\n creperies in <span class=\\\"p-adr h-adr\\\"><span class=\\\"p-locality\\\">San Francisco</span></span>.\\n Excellent food and service. Plenty of tables in a variety of sizes \\n for parties large and small. Window seating makes for excellent \\n people watching to/from the N-Judah which stops right outside. \\n I've had many fun social gatherings here, as well as gotten \\n plenty of work done thanks to neighborhood WiFi.\\n </p>\\n \"\n }],\n \"item\": [{\n \"value\": \"Crepes on Cole\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Crepes on Cole\"],\n \"org\": [\"Crepes on Cole\"],\n \"adr\": [{\n \"value\": \"San Francisco\",\n \"type\": [\"h-adr\"],\n \"properties\": {\n \"locality\": [\"San Francisco\"],\n \"name\": [\"San Francisco\"]\n }\n }]\n }\n }],\n \"category\": [\"crepe\"],\n \"url\": [\"http://example.com/crepe\"]\n }\n }],\n \"rels\": {\n \"license\": [\"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\"]\n },\n \"rel-urls\": {\n \"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\": {\n \"text\": \"Creative Commons Attribution-ShareAlike License\",\n \"rels\": [\"license\"]\n }\n }\n}","html":"<div class=\"h-review\">\n <span><span class=\"p-rating\">5</span> out of 5 stars</span>\n <h4 class=\"p-name\">Crepes on Cole is awesome</h4>\n <span class=\"p-reviewer h-card\">\n Reviewer: <span class=\"p-name\">Tantek</span> - \n </span>\n <time class=\"dt-reviewed\" datetime=\"2005-04-18\">April 18, 2005</time>\n <div class=\"e-description\">\n <p class=\"p-item h-card\">\n <span class=\"p-name p-org\">Crepes on Cole</span> is one of the best little \n creperies in <span class=\"p-adr h-adr\"><span class=\"p-locality\">San Francisco</span></span>.\n Excellent food and service. Plenty of tables in a variety of sizes \n for parties large and small. Window seating makes for excellent \n people watching to/from the N-Judah which stops right outside. \n I've had many fun social gatherings here, as well as gotten \n plenty of work done thanks to neighborhood WiFi.\n </p>\n </div>\n <p>Visit date: <span>April 2005</span></p>\n <p>Food eaten: <a class=\"p-category\" href=\"http://en.wikipedia.org/wiki/crepe\">crepe</a></p>\n <p>Permanent link for review: <a class=\"u-url\" href=\"http://example.com/crepe\">http://example.com/crepe</a></p>\n <p><a rel=\"license\" href=\"http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License\">Creative Commons Attribution-ShareAlike License</a></p>\n</div>","name":"mf-v2-h-review-vcard"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Fullfrontal\",\n \"type\": [\"h-event\"],\n \"properties\": {\n \"name\": [\"Fullfrontal\"],\n \"description\": [\"A one day JavaScript Conference held in Brighton\"],\n \"start\": [\"2012-11-09\"]\n }\n }],\n \"rating\": [\"9.9\"],\n \"average\": [\"9.9\"],\n \"best\": [\"10\"],\n \"count\": [\"62\"],\n \"name\": [\"Fullfrontal\\n A one day JavaScript Conference held in Brighton\\n 9th November 2012 \\n \\n \\n \\n 9.9 out of \\n 10 \\n based on 62 reviews\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-review-aggregate\">\n <div class=\"p-item h-event\">\n <h3 class=\"p-name\">Fullfrontal</h3>\n <p class=\"p-description\">A one day JavaScript Conference held in Brighton</p>\n <p><time class=\"dt-start\" datetime=\"2012-11-09\">9th November 2012</time></p> \n </div> \n \n <p class=\"p-rating\">\n <span class=\"p-average value\">9.9</span> out of \n <span class=\"p-best\">10</span> \n based on <span class=\"p-count\">62</span> reviews\n </p>\n</div>","name":"mf-v2-h-review-aggregate-hevent"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Mediterranean Wraps\",\n \"type\": [\"h-item\"],\n \"properties\": {\n \"name\": [\"Mediterranean Wraps\"]\n }\n }],\n \"summary\": [\"Customers flock to this small restaurant for their \\n tasty falafel and shawerma wraps and welcoming staff.\"],\n \"rating\": [\"4.5\"],\n \"name\": [\"Mediterranean Wraps\\n \\n Customers flock to this small restaurant for their \\n tasty falafel and shawerma wraps and welcoming staff.\\n \\n 4.5 out of 5\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-review-aggregate\">\n <h3 class=\"p-item h-item\">Mediterranean Wraps</h3>\n <span class=\"p-summary\">\n Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff.\n </span>\n <span class=\"p-rating\">4.5</span> out of 5 \n</div>","name":"mf-v2-h-review-aggregate-justahyperlink"},{"json":"{\n \"items\": [{\n \"type\": [\"h-review-aggregate\"],\n \"properties\": {\n \"item\": [{\n \"value\": \"Mediterranean Wraps\",\n \"type\": [\"h-card\"],\n \"properties\": {\n \"name\": [\"Mediterranean Wraps\"],\n \"street-address\": [\"433 S California Ave\"],\n \"locality\": [\"Palo Alto\"],\n \"region\": [\"CA\"],\n \"tel\": [\"(650) 321-8189\"]\n }\n }],\n \"summary\": [\"Customers flock to this small restaurant for their \\n tasty falafel and shawerma wraps and welcoming staff.\"],\n \"rating\": [\"9.2\"],\n \"average\": [\"9.2\"],\n \"best\": [\"10\"],\n \"count\": [\"17\"],\n \"name\": [\"Mediterranean Wraps\\n \\n 433 S California Ave, \\n Palo Alto, \\n CA - \\n (650) 321-8189\\n \\n \\n Customers flock to this small restaurant for their \\n tasty falafel and shawerma wraps and welcoming staff.\\n \\n 9.2 out of \\n 10 \\n based on 17 reviews\"]\n }\n }],\n \"rels\": {},\n \"rel-urls\": {}\n}","html":"<div class=\"h-review-aggregate\">\n <div class=\"p-item h-card\">\n <h3 class=\"p-name\">Mediterranean Wraps</h3>\n <p>\n <span class=\"p-street-address\">433 S California Ave</span>, \n <span class=\"p-locality\">Palo Alto</span>, \n <span class=\"p-region\">CA</span> - \n <span class=\"p-tel\">(650) 321-8189</span>\n </p>\n </div> \n <span class=\"p-summary\">Customers flock to this small restaurant for their \n tasty falafel and shawerma wraps and welcoming staff.</span>\n <span class=\"p-rating\">\n <span class=\"p-average value\">9.2</span> out of \n <span class=\"p-best\">10</span> \n based on <span class=\"p-count\">17</span> reviews\n </span>\n</div>","name":"mf-v2-h-review-aggregate-simpleproperties"},{"json":"{\n \"rels\": {\n \"bookmark\": [\n \"http://ma.tt/2015/05/beethoven-mozart-bach/\",\n \"http://ma.tt/2015/06/jefferson-on-idleness/\" \n ], \n \"category\": [\n \"http://ma.tt/category/asides/\"\n ], \n \"tag\": [\n \"http://ma.tt/category/asides/\"\n ], \n \"author\": [\n \"http://ma.tt/author/saxmatt/\"\n ]\n }, \n \"items\": [\n {\n \"type\": [\n \"h-card\"\n ], \n \"properties\": {\n \"url\": [\n \"http://ma.tt/author/saxmatt/\"\n ], \n \"name\": [\n \"Matt\"\n ]\n }\n }, \n {\n \"type\": [\n \"h-card\"\n ], \n \"properties\": {\n \"url\": [\n \"http://ma.tt/author/saxmatt/\"\n ], \n \"name\": [\n \"Matt\"\n ]\n }\n }\n ], \n \"rel-urls\": {\n \"http://ma.tt/category/asides/\": {\n \"rels\": [\n \"category\", \n \"tag\"\n ], \n \"text\": \"Asides\"\n }, \n \"http://ma.tt/author/saxmatt/\": {\n \"rels\": [\n \"author\"\n ], \n \"text\": \"Matt\", \n \"title\": \"View all posts by Matt\"\n }, \n \"http://ma.tt/2015/05/beethoven-mozart-bach/\": {\n \"rels\": [\n \"bookmark\"\n ], \n \"text\": \"May 31, 2015\", \n \"title\": \"Permalink to Beethoven, Mozart, Bach\"\n }, \n \"http://ma.tt/2015/06/jefferson-on-idleness/\": {\n \"rels\": [\n \"bookmark\"\n ], \n \"text\": \"June 2, 2015\", \n \"title\": \"Permalink to Jefferson on Idleness\"\n }\n }\n}","html":"<a href=\"http://ma.tt/2015/05/beethoven-mozart-bach/\" \n title=\"Permalink to Beethoven, Mozart, Bach\" rel=\"bookmark\">\n<time class=\"entry-date\" datetime=\"2015-05-31T22:42:00+00:00\">May 31, 2015</time></a></span>\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">Asides</a>\n<span class=\"author vcard\">\n<a class=\"url fn n\" href=\"http://ma.tt/author/saxmatt/\" \n title=\"View all posts by Matt\" rel=\"author\">Matt</a></span>\n<span class=\"date\"><a href=\"http://ma.tt/2015/06/jefferson-on-idleness/\" title=\"Permalink to Jefferson on Idleness\" rel=\"bookmark\"><time class=\"entry-date\" datetime=\"2015-06-02T21:26:00+00:00\">June 2, 2015</time></a></span>\n<span class=\"categories-links\"><a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">Asides</a></span>\n<span class=\"author vcard\"><a class=\"url fn n\" href=\"http://ma.tt/author/saxmatt/\" title=\"View all posts by Matt\" rel=\"author\">Matt</a></span>\n","name":"mf-v2-rel-duplicate-rels"},{"json":"{\n \"items\": [],\n \"rels\": {\n \"license\": [\"http://creativecommons.org/licenses/by/2.5/\"]\n },\n \"rel-urls\": {\n \"http://creativecommons.org/licenses/by/2.5/\": {\n \"text\": \"cc by 2.5\",\n \"rels\": [\"license\"]\n }\n }\n}","html":"<a rel=\"license\" href=\"http://creativecommons.org/licenses/by/2.5/\">cc by 2.5</a>","name":"mf-v2-rel-license"},{"json":"{\n \"items\": [],\n \"rels\": {\n \"nofollow\": [\"http://microformats.org/wiki/microformats:copyrights\"]\n },\n \"rel-urls\": {\n \"http://microformats.org/wiki/microformats:copyrights\": {\n \"text\": \"Copyrights\",\n \"rels\": [\"nofollow\"]\n }\n }\n}","html":"<a rel=\"nofollow\" href=\"http://microformats.org/wiki/microformats:copyrights\">Copyrights</a>","name":"mf-v2-rel-nofollow"},{"json":"{\n \"items\": [],\n \"rels\": { \n \"author\": [ \"http://example.com/a\", \"http://example.com/b\" ],\n \"in-reply-to\": [ \"http://example.com/1\", \"http://example.com/2\" ], \n \"home\": [ \"http://example.com/fr\" ], \n \"alternate\": [ \"http://example.com/fr\" ] \n },\n \"rel-urls\": {\n \"http://example.com/a\": {\n \"rels\": [\"author\"], \n \"text\": \"author a\"\n },\n \"http://example.com/b\": {\n \"rels\": [\"author\"], \n \"text\": \"author b\"\n },\n \"http://example.com/1\": {\n \"rels\": [\"in-reply-to\"], \n \"text\": \"post 1\"\n },\n \"http://example.com/2\": {\n \"rels\": [\"in-reply-to\"], \n \"text\": \"post 2\"\n },\n \"http://example.com/fr\": {\n \"rels\": [\"alternate\", \"home\"],\n \"media\": \"handheld\", \n \"hreflang\": \"fr\", \n \"text\": \"French mobile homepage\"\n }\n }\n}","html":"<a rel=\"author\" href=\"http://example.com/a\">author a</a>\n<a rel=\"author\" href=\"http://example.com/b\">author b</a>\n<a rel=\"in-reply-to\" href=\"http://example.com/1\">post 1</a>\n<a rel=\"in-reply-to\" href=\"http://example.com/2\">post 2</a>\n<a rel=\"alternate home\"\n href=\"http://example.com/fr\"\n media=\"handheld\"\n hreflang=\"fr\">French mobile homepage</a>","name":"mf-v2-rel-rel-urls"},{"json":"{\n \"rels\": {\n \"category\": [\n \"http://ma.tt/category/asides/\"\n ], \n \"tag\": [\n \"http://ma.tt/category/asides/\"\n ]\n }, \n \"items\": [], \n \"rel-urls\": {\n \"http://ma.tt/category/asides/\": {\n \"rels\": [\n \"category\", \n \"tag\"\n ], \n \"text\": \"Asides\"\n }\n }\n}\n","html":"This is a contrived example - not found links like this in the wild:\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">Asides</a>\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">B-sides</a>\n<a href=\"http://ma.tt/category/asides/\" rel=\"category tag\">seasides</a>","name":"mf-v2-rel-varying-text-duplicate-rels"},{"json":"{\n \"items\": [],\n \"rels\": {\n \"friend\": [\"http://example.com/profile/jane\"],\n \"acquaintance\": [\"http://example.com/profile/jeo\"],\n \"contact\": [\"http://example.com/profile/lily\"],\n \"met\": [\"http://example.com/profile/oliver\"],\n \"co-worker\": [\"http://example.com/profile/emily\"],\n \"colleague\": [\"http://example.com/profile/jack\"],\n \"neighbor\": [\"http://example.com/profile/isabella\"],\n \"child\": [\"http://example.com/profile/harry\"],\n \"parent\": [\"http://example.com/profile/sophia\"],\n \"sibling\": [\"http://example.com/profile/charlie\"],\n \"spouse\": [\"http://example.com/profile/olivia\"],\n \"kin\": [\"http://example.com/profile/james\"],\n \"muse\": [\"http://example.com/profile/ava\"],\n \"crush\": [\"http://example.com/profile/joshua\"],\n \"date\": [\"http://example.com/profile/chloe\"],\n \"sweetheart\": [\"http://example.com/profile/alfie\"],\n \"me\": [\"http://example.com/profile/isla\"]\n },\n \"rel-urls\": {\n \"http://example.com/profile/jane\": {\n \"text\": \"jane\",\n \"rels\": [\"friend\"]\n },\n \"http://example.com/profile/jeo\": {\n \"text\": \"jeo\",\n \"rels\": [\"acquaintance\"]\n },\n \"http://example.com/profile/lily\": {\n \"text\": \"lily\",\n \"rels\": [\"contact\"]\n },\n \"http://example.com/profile/oliver\": {\n \"text\": \"oliver\",\n \"rels\": [\"met\"]\n },\n \"http://example.com/profile/emily\": {\n \"text\": \"emily\",\n \"rels\": [\"co-worker\"]\n },\n \"http://example.com/profile/jack\": {\n \"text\": \"jack\",\n \"rels\": [\"colleague\"]\n },\n \"http://example.com/profile/isabella\": {\n \"text\": \"isabella\",\n \"rels\": [\"neighbor\"]\n },\n \"http://example.com/profile/harry\": {\n \"text\": \"harry\",\n \"rels\": [\"child\"]\n },\n \"http://example.com/profile/sophia\": {\n \"text\": \"sophia\",\n \"rels\": [\"parent\"]\n },\n \"http://example.com/profile/charlie\": {\n \"text\": \"charlie\",\n \"rels\": [\"sibling\"]\n },\n \"http://example.com/profile/olivia\": {\n \"text\": \"olivia\",\n \"rels\": [\"spouse\"]\n },\n \"http://example.com/profile/james\": {\n \"text\": \"james\",\n \"rels\": [\"kin\"]\n },\n \"http://example.com/profile/ava\": {\n \"text\": \"ava\",\n \"rels\": [\"muse\"]\n },\n \"http://example.com/profile/joshua\": {\n \"text\": \"joshua\",\n \"rels\": [\"crush\"]\n },\n \"http://example.com/profile/chloe\": {\n \"text\": \"chloe\",\n \"rels\": [\"date\"]\n },\n \"http://example.com/profile/alfie\": {\n \"text\": \"alfie\",\n \"rels\": [\"sweetheart\"]\n },\n \"http://example.com/profile/isla\": {\n \"text\": \"isla\",\n \"rels\": [\"me\"]\n }\n }\n}\n","html":"<ul>\n <li><a rel=\"friend\" href=\"http://example.com/profile/jane\">jane</a></li>\n <li><a rel=\"acquaintance\" href=\"http://example.com/profile/jeo\">jeo</a></li>\n <li><a rel=\"contact\" href=\"http://example.com/profile/lily\">lily</a></li>\n <li><a rel=\"met\" href=\"http://example.com/profile/oliver\">oliver</a></li>\n <li><a rel=\"co-worker\" href=\"http://example.com/profile/emily\">emily</a></li>\n <li><a rel=\"colleague\" href=\"http://example.com/profile/jack\">jack</a></li>\n <li><a rel=\"neighbor\" href=\"http://example.com/profile/isabella\">isabella</a></li>\n <li><a rel=\"child\" href=\"http://example.com/profile/harry\">harry</a></li>\n <li><a rel=\"parent\" href=\"http://example.com/profile/sophia\">sophia</a></li>\n <li><a rel=\"sibling\" href=\"http://example.com/profile/charlie\">charlie</a></li>\n <li><a rel=\"spouse\" href=\"http://example.com/profile/olivia\">olivia</a></li>\n <li><a rel=\"kin\" href=\"http://example.com/profile/james\">james</a></li>\n <li><a rel=\"muse\" href=\"http://example.com/profile/ava\">ava</a></li>\n <li><a rel=\"crush\" href=\"http://example.com/profile/joshua\">joshua</a></li>\n <li><a rel=\"date\" href=\"http://example.com/profile/chloe\">chloe</a></li>\n <li><a rel=\"sweetheart\" href=\"http://example.com/profile/alfie\">alfie</a></li>\n <li><a rel=\"me\" href=\"http://example.com/profile/isla\">isla</a></li>\n</ul>","name":"mf-v2-rel-xfn-all"},{"json":"{\n \"items\": [],\n \"rels\": {\n \"me\": [\"http://twitter.com/glennjones\", \"http://delicious.com/glennjonesnet/\", \"https://plus.google.com/u/0/105161464208920272734/about\", \"http://lanyrd.com/people/glennjones/\", \"http://github.com/glennjones\", \"http://www.flickr.com/photos/glennjonesnet/\", \"http://www.linkedin.com/in/glennjones\", \"http://www.slideshare.net/glennjones/presentations\"]\n },\n \"rel-urls\": {\n \"http://twitter.com/glennjones\": {\n \"text\": \"twitter\",\n \"rels\": [\"me\"]\n },\n \"http://delicious.com/glennjonesnet/\": {\n \"text\": \"delicious\",\n \"rels\": [\"me\"]\n },\n \"https://plus.google.com/u/0/105161464208920272734/about\": {\n \"text\": \"google+\",\n \"rels\": [\"me\"]\n },\n \"http://lanyrd.com/people/glennjones/\": {\n \"text\": \"lanyrd\",\n \"rels\": [\"me\"]\n },\n \"http://github.com/glennjones\": {\n \"text\": \"github\",\n \"rels\": [\"me\"]\n },\n \"http://www.flickr.com/photos/glennjonesnet/\": {\n \"text\": \"flickr\",\n \"rels\": [\"me\"]\n },\n \"http://www.linkedin.com/in/glennjones\": {\n \"text\": \"linkedin\",\n \"rels\": [\"me\"]\n },\n \"http://www.slideshare.net/glennjones/presentations\": {\n \"text\": \"slideshare\",\n \"rels\": [\"me\"]\n }\n }\n}\n","html":"<ul>\n <li><a rel=\"me\" href=\"http://twitter.com/glennjones\">twitter</a></li>\n <li><a rel=\"me\" href=\"http://delicious.com/glennjonesnet/\">delicious</a></li>\n <li><a rel=\"me\" href=\"https://plus.google.com/u/0/105161464208920272734/about\">google+</a></li>\n <li><a rel=\"me\" href=\"http://lanyrd.com/people/glennjones/\">lanyrd</a></li>\n <li><a rel=\"me\" href=\"http://github.com/glennjones\">github</a></li>\n <li><a rel=\"me\" href=\"http://www.flickr.com/photos/glennjonesnet/\">flickr</a></li>\n <li><a rel=\"me\" href=\"http://www.linkedin.com/in/glennjones\">linkedin</a></li>\n <li><a rel=\"me\" href=\"http://www.slideshare.net/glennjones/presentations\">slideshare</a></li>\n</ul>","name":"mf-v2-rel-xfn-elsewhere"}]}
diff --git a/toolkit/components/microformats/test/static/javascript/deep-diff-0.3.1.min.js b/toolkit/components/microformats/test/static/javascript/deep-diff-0.3.1.min.js
new file mode 100644
index 0000000000..e200019c0f
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/deep-diff-0.3.1.min.js
@@ -0,0 +1,5 @@
+/*!
+ * deep-diff.
+ * Licensed under the MIT License.
+ */
+(function(e,t){"use strict";if(typeof define==="function"&&define.amd){define([],t)}else if(typeof exports==="object"){module.exports=t()}else{e.DeepDiff=t()}})(this,function(e){"use strict";var t,n,r=[];if(typeof global==="object"&&global){t=global}else if(typeof window!=="undefined"){t=window}else{t={}}n=t.DeepDiff;if(n){r.push(function(){if("undefined"!==typeof n&&t.DeepDiff===p){t.DeepDiff=n;n=e}})}function a(e,t){e.super_=t;e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:false,writable:true,configurable:true}})}function i(e,t){Object.defineProperty(this,"kind",{value:e,enumerable:true});if(t&&t.length){Object.defineProperty(this,"path",{value:t,enumerable:true})}}function f(e,t,n){f.super_.call(this,"E",e);Object.defineProperty(this,"lhs",{value:t,enumerable:true});Object.defineProperty(this,"rhs",{value:n,enumerable:true})}a(f,i);function u(e,t){u.super_.call(this,"N",e);Object.defineProperty(this,"rhs",{value:t,enumerable:true})}a(u,i);function l(e,t){l.super_.call(this,"D",e);Object.defineProperty(this,"lhs",{value:t,enumerable:true})}a(l,i);function s(e,t,n){s.super_.call(this,"A",e);Object.defineProperty(this,"index",{value:t,enumerable:true});Object.defineProperty(this,"item",{value:n,enumerable:true})}a(s,i);function h(e,t,n){var r=e.slice((n||t)+1||e.length);e.length=t<0?e.length+t:t;e.push.apply(e,r);return e}function c(e){var t=typeof e;if(t!=="object"){return t}if(e===Math){return"math"}else if(e===null){return"null"}else if(Array.isArray(e)){return"array"}else if(e instanceof Date){return"date"}else if(/^\/.*\//.test(e.toString())){return"regexp"}return"object"}function o(t,n,r,a,i,p,b){i=i||[];var d=i.slice(0);if(typeof p!=="undefined"){if(a&&a(d,p,{lhs:t,rhs:n})){return}d.push(p)}var v=typeof t;var y=typeof n;if(v==="undefined"){if(y!=="undefined"){r(new u(d,n))}}else if(y==="undefined"){r(new l(d,t))}else if(c(t)!==c(n)){r(new f(d,t,n))}else if(t instanceof Date&&n instanceof Date&&t-n!==0){r(new f(d,t,n))}else if(v==="object"&&t!==null&&n!==null){b=b||[];if(b.indexOf(t)<0){b.push(t);if(Array.isArray(t)){var k,m=t.length;for(k=0;k<t.length;k++){if(k>=n.length){r(new s(d,k,new l(e,t[k])))}else{o(t[k],n[k],r,a,d,k,b)}}while(k<n.length){r(new s(d,k,new u(e,n[k++])))}}else{var g=Object.keys(t);var w=Object.keys(n);g.forEach(function(i,f){var u=w.indexOf(i);if(u>=0){o(t[i],n[i],r,a,d,i,b);w=h(w,u)}else{o(t[i],e,r,a,d,i,b)}});w.forEach(function(t){o(e,n[t],r,a,d,t,b)})}b.length=b.length-1}}else if(t!==n){if(!(v==="number"&&isNaN(t)&&isNaN(n))){r(new f(d,t,n))}}}function p(t,n,r,a){a=a||[];o(t,n,function(e){if(e){a.push(e)}},r);return a.length?a:e}function b(e,t,n){if(n.path&&n.path.length){var r=e[t],a,i=n.path.length-1;for(a=0;a<i;a++){r=r[n.path[a]]}switch(n.kind){case"A":b(r[n.path[a]],n.index,n.item);break;case"D":delete r[n.path[a]];break;case"E":case"N":r[n.path[a]]=n.rhs;break}}else{switch(n.kind){case"A":b(e[t],n.index,n.item);break;case"D":e=h(e,t);break;case"E":case"N":e[t]=n.rhs;break}}return e}function d(e,t,n){if(e&&t&&n&&n.kind){var r=e,a=-1,i=n.path.length-1;while(++a<i){if(typeof r[n.path[a]]==="undefined"){r[n.path[a]]=typeof n.path[a]==="number"?[]:{}}r=r[n.path[a]]}switch(n.kind){case"A":b(r[n.path[a]],n.index,n.item);break;case"D":delete r[n.path[a]];break;case"E":case"N":r[n.path[a]]=n.rhs;break}}}function v(e,t,n){if(n.path&&n.path.length){var r=e[t],a,i=n.path.length-1;for(a=0;a<i;a++){r=r[n.path[a]]}switch(n.kind){case"A":v(r[n.path[a]],n.index,n.item);break;case"D":r[n.path[a]]=n.lhs;break;case"E":r[n.path[a]]=n.lhs;break;case"N":delete r[n.path[a]];break}}else{switch(n.kind){case"A":v(e[t],n.index,n.item);break;case"D":e[t]=n.lhs;break;case"E":e[t]=n.lhs;break;case"N":e=h(e,t);break}}return e}function y(e,t,n){if(e&&t&&n&&n.kind){var r=e,a,i;i=n.path.length-1;for(a=0;a<i;a++){if(typeof r[n.path[a]]==="undefined"){r[n.path[a]]={}}r=r[n.path[a]]}switch(n.kind){case"A":v(r[n.path[a]],n.index,n.item);break;case"D":r[n.path[a]]=n.lhs;break;case"E":r[n.path[a]]=n.lhs;break;case"N":delete r[n.path[a]];break}}}function k(e,t,n){if(e&&t){var r=function(r){if(!n||n(e,t,r)){d(e,t,r)}};o(e,t,r)}}Object.defineProperties(p,{diff:{value:p,enumerable:true},observableDiff:{value:o,enumerable:true},applyDiff:{value:k,enumerable:true},applyChange:{value:d,enumerable:true},revertChange:{value:y,enumerable:true},isConflict:{value:function(){return"undefined"!==typeof n},enumerable:true},noConflict:{value:function(){if(r){r.forEach(function(e){e()});r=null}return p},enumerable:true}});return p});
diff --git a/toolkit/components/microformats/test/static/javascript/mocha.js b/toolkit/components/microformats/test/static/javascript/mocha.js
new file mode 100644
index 0000000000..100850e5d3
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/mocha.js
@@ -0,0 +1,6573 @@
+(function(){
+
+// CommonJS require()
+
+function require(p){
+ var path = require.resolve(p)
+ , mod = require.modules[path];
+ if (!mod) throw new Error('failed to require "' + p + '"');
+ if (!mod.exports) {
+ mod.exports = {};
+ mod.call(mod.exports, mod, mod.exports, require.relative(path));
+ }
+ return mod.exports;
+ }
+
+require.modules = {};
+
+require.resolve = function (path){
+ var orig = path
+ , reg = path + '.js'
+ , index = path + '/index.js';
+ return require.modules[reg] && reg
+ || require.modules[index] && index
+ || orig;
+ };
+
+require.register = function (path, fn){
+ require.modules[path] = fn;
+ };
+
+require.relative = function (parent) {
+ return function(p){
+ if ('.' != p.charAt(0)) return require(p);
+
+ var path = parent.split('/')
+ , segs = p.split('/');
+ path.pop();
+
+ for (var i = 0; i < segs.length; i++) {
+ var seg = segs[i];
+ if ('..' == seg) path.pop();
+ else if ('.' != seg) path.push(seg);
+ }
+
+ return require(path.join('/'));
+ };
+ };
+
+
+require.register("browser/debug.js", function(module, exports, require){
+module.exports = function(type){
+ return function(){
+ }
+};
+
+}); // module: browser/debug.js
+
+require.register("browser/diff.js", function(module, exports, require){
+/* See LICENSE file for terms of use */
+
+/*
+ * Text diff implementation.
+ *
+ * This library supports the following APIS:
+ * JsDiff.diffChars: Character by character diff
+ * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace
+ * JsDiff.diffLines: Line based diff
+ *
+ * JsDiff.diffCss: Diff targeted at CSS content
+ *
+ * These methods are based on the implementation proposed in
+ * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986).
+ * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927
+ */
+var JsDiff = (function() {
+ /*jshint maxparams: 5*/
+ function clonePath(path) {
+ return { newPos: path.newPos, components: path.components.slice(0) };
+ }
+ function removeEmpty(array) {
+ var ret = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array[i]) {
+ ret.push(array[i]);
+ }
+ }
+ return ret;
+ }
+ function escapeHTML(s) {
+ var n = s;
+ n = n.replace(/&/g, '&amp;');
+ n = n.replace(/</g, '&lt;');
+ n = n.replace(/>/g, '&gt;');
+ n = n.replace(/"/g, '&quot;');
+
+ return n;
+ }
+
+ var Diff = function(ignoreWhitespace) {
+ this.ignoreWhitespace = ignoreWhitespace;
+ };
+ Diff.prototype = {
+ diff: function(oldString, newString) {
+ // Handle the identity case (this is due to unrolling editLength == 0
+ if (newString === oldString) {
+ return [{ value: newString }];
+ }
+ if (!newString) {
+ return [{ value: oldString, removed: true }];
+ }
+ if (!oldString) {
+ return [{ value: newString, added: true }];
+ }
+
+ newString = this.tokenize(newString);
+ oldString = this.tokenize(oldString);
+
+ var newLen = newString.length, oldLen = oldString.length;
+ var maxEditLength = newLen + oldLen;
+ var bestPath = [{ newPos: -1, components: [] }];
+
+ // Seed editLength = 0
+ var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0);
+ if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) {
+ return bestPath[0].components;
+ }
+
+ for (var editLength = 1; editLength <= maxEditLength; editLength++) {
+ for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) {
+ var basePath;
+ var addPath = bestPath[diagonalPath-1],
+ removePath = bestPath[diagonalPath+1];
+ oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
+ if (addPath) {
+ // No one else is going to attempt to use this value, clear it
+ bestPath[diagonalPath-1] = undefined;
+ }
+
+ var canAdd = addPath && addPath.newPos+1 < newLen;
+ var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
+ if (!canAdd && !canRemove) {
+ bestPath[diagonalPath] = undefined;
+ continue;
+ }
+
+ // Select the diagonal that we want to branch from. We select the prior
+ // path whose position in the new string is the farthest from the origin
+ // and does not pass the bounds of the diff graph
+ if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) {
+ basePath = clonePath(removePath);
+ this.pushComponent(basePath.components, oldString[oldPos], undefined, true);
+ } else {
+ basePath = clonePath(addPath);
+ basePath.newPos++;
+ this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined);
+ }
+
+ oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath);
+
+ if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) {
+ return basePath.components;
+ } else {
+ bestPath[diagonalPath] = basePath;
+ }
+ }
+ }
+ },
+
+ pushComponent: function(components, value, added, removed) {
+ var last = components[components.length-1];
+ if (last && last.added === added && last.removed === removed) {
+ // We need to clone here as the component clone operation is just
+ // as shallow array clone
+ components[components.length-1] =
+ {value: this.join(last.value, value), added: added, removed: removed };
+ } else {
+ components.push({value: value, added: added, removed: removed });
+ }
+ },
+ extractCommon: function(basePath, newString, oldString, diagonalPath) {
+ var newLen = newString.length,
+ oldLen = oldString.length,
+ newPos = basePath.newPos,
+ oldPos = newPos - diagonalPath;
+ while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) {
+ newPos++;
+ oldPos++;
+
+ this.pushComponent(basePath.components, newString[newPos], undefined, undefined);
+ }
+ basePath.newPos = newPos;
+ return oldPos;
+ },
+
+ equals: function(left, right) {
+ var reWhitespace = /\S/;
+ if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) {
+ return true;
+ } else {
+ return left === right;
+ }
+ },
+ join: function(left, right) {
+ return left + right;
+ },
+ tokenize: function(value) {
+ return value;
+ }
+ };
+
+ var CharDiff = new Diff();
+
+ var WordDiff = new Diff(true);
+ var WordWithSpaceDiff = new Diff();
+ WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) {
+ return removeEmpty(value.split(/(\s+|\b)/));
+ };
+
+ var CssDiff = new Diff(true);
+ CssDiff.tokenize = function(value) {
+ return removeEmpty(value.split(/([{}:;,]|\s+)/));
+ };
+
+ var LineDiff = new Diff();
+ LineDiff.tokenize = function(value) {
+ var retLines = [],
+ lines = value.split(/^/m);
+
+ for(var i = 0; i < lines.length; i++) {
+ var line = lines[i],
+ lastLine = lines[i - 1];
+
+ // Merge lines that may contain windows new lines
+ if (line == '\n' && lastLine && lastLine[lastLine.length - 1] === '\r') {
+ retLines[retLines.length - 1] += '\n';
+ } else if (line) {
+ retLines.push(line);
+ }
+ }
+
+ return retLines;
+ };
+
+ return {
+ Diff: Diff,
+
+ diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); },
+ diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); },
+ diffWordsWithSpace: function(oldStr, newStr) { return WordWithSpaceDiff.diff(oldStr, newStr); },
+ diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); },
+
+ diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); },
+
+ createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) {
+ var ret = [];
+
+ ret.push('Index: ' + fileName);
+ ret.push('===================================================================');
+ ret.push('--- ' + fileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader));
+ ret.push('+++ ' + fileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader));
+
+ var diff = LineDiff.diff(oldStr, newStr);
+ if (!diff[diff.length-1].value) {
+ diff.pop(); // Remove trailing newline add
+ }
+ diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier
+
+ function contextLines(lines) {
+ return lines.map(function(entry) { return ' ' + entry; });
+ }
+ function eofNL(curRange, i, current) {
+ var last = diff[diff.length-2],
+ isLast = i === diff.length-2,
+ isLastOfType = i === diff.length-3 && (current.added !== last.added || current.removed !== last.removed);
+
+ // Figure out if this is the last line for the given file and missing NL
+ if (!/\n$/.test(current.value) && (isLast || isLastOfType)) {
+ curRange.push('\\ No newline at end of file');
+ }
+ }
+
+ var oldRangeStart = 0, newRangeStart = 0, curRange = [],
+ oldLine = 1, newLine = 1;
+ for (var i = 0; i < diff.length; i++) {
+ var current = diff[i],
+ lines = current.lines || current.value.replace(/\n$/, '').split('\n');
+ current.lines = lines;
+
+ if (current.added || current.removed) {
+ if (!oldRangeStart) {
+ var prev = diff[i-1];
+ oldRangeStart = oldLine;
+ newRangeStart = newLine;
+
+ if (prev) {
+ curRange = contextLines(prev.lines.slice(-4));
+ oldRangeStart -= curRange.length;
+ newRangeStart -= curRange.length;
+ }
+ }
+ curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?'+':'-') + entry; }));
+ eofNL(curRange, i, current);
+
+ if (current.added) {
+ newLine += lines.length;
+ } else {
+ oldLine += lines.length;
+ }
+ } else {
+ if (oldRangeStart) {
+ // Close out any changes that have been output (or join overlapping)
+ if (lines.length <= 8 && i < diff.length-2) {
+ // Overlapping
+ curRange.push.apply(curRange, contextLines(lines));
+ } else {
+ // end the range and output
+ var contextSize = Math.min(lines.length, 4);
+ ret.push(
+ '@@ -' + oldRangeStart + ',' + (oldLine-oldRangeStart+contextSize)
+ + ' +' + newRangeStart + ',' + (newLine-newRangeStart+contextSize)
+ + ' @@');
+ ret.push.apply(ret, curRange);
+ ret.push.apply(ret, contextLines(lines.slice(0, contextSize)));
+ if (lines.length <= 4) {
+ eofNL(ret, i, current);
+ }
+
+ oldRangeStart = 0; newRangeStart = 0; curRange = [];
+ }
+ }
+ oldLine += lines.length;
+ newLine += lines.length;
+ }
+ }
+
+ return ret.join('\n') + '\n';
+ },
+
+ applyPatch: function(oldStr, uniDiff) {
+ var diffstr = uniDiff.split('\n');
+ var diff = [];
+ var remEOFNL = false,
+ addEOFNL = false;
+
+ for (var i = (diffstr[0][0]==='I'?4:0); i < diffstr.length; i++) {
+ if(diffstr[i][0] === '@') {
+ var meh = diffstr[i].split(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/);
+ diff.unshift({
+ start:meh[3],
+ oldlength:meh[2],
+ oldlines:[],
+ newlength:meh[4],
+ newlines:[]
+ });
+ } else if(diffstr[i][0] === '+') {
+ diff[0].newlines.push(diffstr[i].substr(1));
+ } else if(diffstr[i][0] === '-') {
+ diff[0].oldlines.push(diffstr[i].substr(1));
+ } else if(diffstr[i][0] === ' ') {
+ diff[0].newlines.push(diffstr[i].substr(1));
+ diff[0].oldlines.push(diffstr[i].substr(1));
+ } else if(diffstr[i][0] === '\\') {
+ if (diffstr[i-1][0] === '+') {
+ remEOFNL = true;
+ } else if(diffstr[i-1][0] === '-') {
+ addEOFNL = true;
+ }
+ }
+ }
+
+ var str = oldStr.split('\n');
+ for (i = diff.length - 1; i >= 0; i--) {
+ var d = diff[i];
+ for (var j = 0; j < d.oldlength; j++) {
+ if(str[d.start-1+j] !== d.oldlines[j]) {
+ return false;
+ }
+ }
+ Array.prototype.splice.apply(str,[d.start-1,+d.oldlength].concat(d.newlines));
+ }
+
+ if (remEOFNL) {
+ while (!str[str.length-1]) {
+ str.pop();
+ }
+ } else if (addEOFNL) {
+ str.push('');
+ }
+ return str.join('\n');
+ },
+
+ convertChangesToXML: function(changes){
+ var ret = [];
+ for ( var i = 0; i < changes.length; i++) {
+ var change = changes[i];
+ if (change.added) {
+ ret.push('<ins>');
+ } else if (change.removed) {
+ ret.push('<del>');
+ }
+
+ ret.push(escapeHTML(change.value));
+
+ if (change.added) {
+ ret.push('</ins>');
+ } else if (change.removed) {
+ ret.push('</del>');
+ }
+ }
+ return ret.join('');
+ },
+
+ // See: http://code.google.com/p/google-diff-match-patch/wiki/API
+ convertChangesToDMP: function(changes){
+ var ret = [], change;
+ for ( var i = 0; i < changes.length; i++) {
+ change = changes[i];
+ var order = 0;
+ if (change.added) {
+ order = 1;
+ } else if (change.removed) {
+ order = -1;
+ }
+ ret.push([order, change.value]);
+ }
+ return ret;
+ }
+ };
+})();
+
+if (typeof module !== 'undefined') {
+ module.exports = JsDiff;
+}
+
+}); // module: browser/diff.js
+
+require.register("browser/escape-string-regexp.js", function(module, exports, require){
+'use strict';
+
+var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
+
+module.exports = function (str) {
+ if (typeof str !== 'string') {
+ throw new TypeError('Expected a string');
+ }
+
+ return str.replace(matchOperatorsRe, '\\$&');
+};
+
+}); // module: browser/escape-string-regexp.js
+
+require.register("browser/events.js", function(module, exports, require){
+/**
+ * Module exports.
+ */
+
+exports.EventEmitter = EventEmitter;
+
+/**
+ * Check if `obj` is an array.
+ */
+
+function isArray(obj) {
+ return '[object Array]' == {}.toString.call(obj);
+}
+
+/**
+ * Event emitter constructor.
+ *
+ * @api public
+ */
+
+function EventEmitter(){}
+
+/**
+ * Adds a listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.on = function (name, fn) {
+ if (!this.$events) {
+ this.$events = {};
+ }
+
+ if (!this.$events[name]) {
+ this.$events[name] = fn;
+ } else if (isArray(this.$events[name])) {
+ this.$events[name].push(fn);
+ } else {
+ this.$events[name] = [this.$events[name], fn];
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.addListener = EventEmitter.prototype.on;
+
+/**
+ * Adds a volatile listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.once = function (name, fn) {
+ var self = this;
+
+ function on () {
+ self.removeListener(name, on);
+ fn.apply(this, arguments);
+ }
+
+ on.listener = fn;
+ this.on(name, on);
+
+ return this;
+};
+
+/**
+ * Removes a listener.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.removeListener = function (name, fn) {
+ if (this.$events && this.$events[name]) {
+ var list = this.$events[name];
+
+ if (isArray(list)) {
+ var pos = -1;
+
+ for (var i = 0, l = list.length; i < l; i++) {
+ if (list[i] === fn || (list[i].listener && list[i].listener === fn)) {
+ pos = i;
+ break;
+ }
+ }
+
+ if (pos < 0) {
+ return this;
+ }
+
+ list.splice(pos, 1);
+
+ if (!list.length) {
+ delete this.$events[name];
+ }
+ } else if (list === fn || (list.listener && list.listener === fn)) {
+ delete this.$events[name];
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Removes all listeners for an event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.removeAllListeners = function (name) {
+ if (name === undefined) {
+ this.$events = {};
+ return this;
+ }
+
+ if (this.$events && this.$events[name]) {
+ this.$events[name] = null;
+ }
+
+ return this;
+};
+
+/**
+ * Gets all listeners for a certain event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.listeners = function (name) {
+ if (!this.$events) {
+ this.$events = {};
+ }
+
+ if (!this.$events[name]) {
+ this.$events[name] = [];
+ }
+
+ if (!isArray(this.$events[name])) {
+ this.$events[name] = [this.$events[name]];
+ }
+
+ return this.$events[name];
+};
+
+/**
+ * Emits an event.
+ *
+ * @api public
+ */
+
+EventEmitter.prototype.emit = function (name) {
+ if (!this.$events) {
+ return false;
+ }
+
+ var handler = this.$events[name];
+
+ if (!handler) {
+ return false;
+ }
+
+ var args = [].slice.call(arguments, 1);
+
+ if ('function' == typeof handler) {
+ handler.apply(this, args);
+ } else if (isArray(handler)) {
+ var listeners = handler.slice();
+
+ for (var i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].apply(this, args);
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+};
+
+}); // module: browser/events.js
+
+require.register("browser/fs.js", function(module, exports, require){
+
+}); // module: browser/fs.js
+
+require.register("browser/glob.js", function(module, exports, require){
+
+}); // module: browser/glob.js
+
+require.register("browser/path.js", function(module, exports, require){
+
+}); // module: browser/path.js
+
+require.register("browser/progress.js", function(module, exports, require){
+/**
+ * Expose `Progress`.
+ */
+
+module.exports = Progress;
+
+/**
+ * Initialize a new `Progress` indicator.
+ */
+
+function Progress() {
+ this.percent = 0;
+ this.size(0);
+ this.fontSize(11);
+ this.font('helvetica, arial, sans-serif');
+}
+
+/**
+ * Set progress size to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.size = function(n){
+ this._size = n;
+ return this;
+};
+
+/**
+ * Set text to `str`.
+ *
+ * @param {String} str
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.text = function(str){
+ this._text = str;
+ return this;
+};
+
+/**
+ * Set font size to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ * @api public
+ */
+
+Progress.prototype.fontSize = function(n){
+ this._fontSize = n;
+ return this;
+};
+
+/**
+ * Set font `family`.
+ *
+ * @param {String} family
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.font = function(family){
+ this._font = family;
+ return this;
+};
+
+/**
+ * Update percentage to `n`.
+ *
+ * @param {Number} n
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.update = function(n){
+ this.percent = n;
+ return this;
+};
+
+/**
+ * Draw on `ctx`.
+ *
+ * @param {CanvasRenderingContext2d} ctx
+ * @return {Progress} for chaining
+ */
+
+Progress.prototype.draw = function(ctx){
+ try {
+ var percent = Math.min(this.percent, 100)
+ , size = this._size
+ , half = size / 2
+ , x = half
+ , y = half
+ , rad = half - 1
+ , fontSize = this._fontSize;
+
+ ctx.font = fontSize + 'px ' + this._font;
+
+ var angle = Math.PI * 2 * (percent / 100);
+ ctx.clearRect(0, 0, size, size);
+
+ // outer circle
+ ctx.strokeStyle = '#9f9f9f';
+ ctx.beginPath();
+ ctx.arc(x, y, rad, 0, angle, false);
+ ctx.stroke();
+
+ // inner circle
+ ctx.strokeStyle = '#eee';
+ ctx.beginPath();
+ ctx.arc(x, y, rad - 1, 0, angle, true);
+ ctx.stroke();
+
+ // text
+ var text = this._text || (percent | 0) + '%'
+ , w = ctx.measureText(text).width;
+
+ ctx.fillText(
+ text
+ , x - w / 2 + 1
+ , y + fontSize / 2 - 1);
+ } catch (ex) {} //don't fail if we can't render progress
+ return this;
+};
+
+}); // module: browser/progress.js
+
+require.register("browser/tty.js", function(module, exports, require){
+exports.isatty = function(){
+ return true;
+};
+
+exports.getWindowSize = function(){
+ if ('innerHeight' in global) {
+ return [global.innerHeight, global.innerWidth];
+ } else {
+ // In a Web Worker, the DOM Window is not available.
+ return [640, 480];
+ }
+};
+
+}); // module: browser/tty.js
+
+require.register("context.js", function(module, exports, require){
+/**
+ * Expose `Context`.
+ */
+
+module.exports = Context;
+
+/**
+ * Initialize a new `Context`.
+ *
+ * @api private
+ */
+
+function Context(){}
+
+/**
+ * Set or get the context `Runnable` to `runnable`.
+ *
+ * @param {Runnable} runnable
+ * @return {Context}
+ * @api private
+ */
+
+Context.prototype.runnable = function(runnable){
+ if (0 == arguments.length) return this._runnable;
+ this.test = this._runnable = runnable;
+ return this;
+};
+
+/**
+ * Set test timeout `ms`.
+ *
+ * @param {Number} ms
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.timeout = function(ms){
+ if (arguments.length === 0) return this.runnable().timeout();
+ this.runnable().timeout(ms);
+ return this;
+};
+
+/**
+ * Set test timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.enableTimeouts = function (enabled) {
+ this.runnable().enableTimeouts(enabled);
+ return this;
+};
+
+
+/**
+ * Set test slowness threshold `ms`.
+ *
+ * @param {Number} ms
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.slow = function(ms){
+ this.runnable().slow(ms);
+ return this;
+};
+
+/**
+ * Mark a test as skipped.
+ *
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.skip = function(){
+ this.runnable().skip();
+ return this;
+};
+
+/**
+ * Inspect the context void of `._runnable`.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Context.prototype.inspect = function(){
+ return JSON.stringify(this, function(key, val){
+ if ('_runnable' == key) return undefined;
+ if ('test' == key) return undefined;
+ return val;
+ }, 2);
+};
+
+}); // module: context.js
+
+require.register("hook.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Runnable = require('./runnable');
+
+/**
+ * Expose `Hook`.
+ */
+
+module.exports = Hook;
+
+/**
+ * Initialize a new `Hook` with the given `title` and callback `fn`.
+ *
+ * @param {String} title
+ * @param {Function} fn
+ * @api private
+ */
+
+function Hook(title, fn) {
+ Runnable.call(this, title, fn);
+ this.type = 'hook';
+}
+
+/**
+ * Inherit from `Runnable.prototype`.
+ */
+
+function F(){}
+F.prototype = Runnable.prototype;
+Hook.prototype = new F;
+Hook.prototype.constructor = Hook;
+
+
+/**
+ * Get or set the test `err`.
+ *
+ * @param {Error} err
+ * @return {Error}
+ * @api public
+ */
+
+Hook.prototype.error = function(err){
+ if (0 == arguments.length) {
+ err = this._error;
+ this._error = null;
+ return err;
+ }
+
+ this._error = err;
+};
+
+}); // module: hook.js
+
+require.register("interfaces/bdd.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test')
+ , utils = require('../utils')
+ , escapeRe = require('browser/escape-string-regexp');
+
+/**
+ * BDD-style interface:
+ *
+ * describe('Array', function(){
+ * describe('#indexOf()', function(){
+ * it('should return -1 when not present', function(){
+ *
+ * });
+ *
+ * it('should return the index when present', function(){
+ *
+ * });
+ * });
+ * });
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('pre-require', function(context, file, mocha){
+
+ var common = require('./common')(suites, context);
+
+ context.before = common.before;
+ context.after = common.after;
+ context.beforeEach = common.beforeEach;
+ context.afterEach = common.afterEach;
+ context.run = mocha.options.delay && common.runWithSuite(suite);
+ /**
+ * Describe a "suite" with the given `title`
+ * and callback `fn` containing nested suites
+ * and/or tests.
+ */
+
+ context.describe = context.context = function(title, fn){
+ var suite = Suite.create(suites[0], title);
+ suite.file = file;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ return suite;
+ };
+
+ /**
+ * Pending describe.
+ */
+
+ context.xdescribe =
+ context.xcontext =
+ context.describe.skip = function(title, fn){
+ var suite = Suite.create(suites[0], title);
+ suite.pending = true;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ };
+
+ /**
+ * Exclusive suite.
+ */
+
+ context.describe.only = function(title, fn){
+ var suite = context.describe(title, fn);
+ mocha.grep(suite.fullTitle());
+ return suite;
+ };
+
+ /**
+ * Describe a specification or test-case
+ * with the given `title` and callback `fn`
+ * acting as a thunk.
+ */
+
+ context.it = context.specify = function(title, fn){
+ var suite = suites[0];
+ if (suite.pending) fn = null;
+ var test = new Test(title, fn);
+ test.file = file;
+ suite.addTest(test);
+ return test;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.it.only = function(title, fn){
+ var test = context.it(title, fn);
+ var reString = '^' + escapeRe(test.fullTitle()) + '$';
+ mocha.grep(new RegExp(reString));
+ return test;
+ };
+
+ /**
+ * Pending test case.
+ */
+
+ context.xit =
+ context.xspecify =
+ context.it.skip = function(title){
+ context.it(title);
+ };
+
+ });
+};
+
+}); // module: interfaces/bdd.js
+
+require.register("interfaces/common.js", function(module, exports, require){
+/**
+ * Functions common to more than one interface
+ * @module lib/interfaces/common
+ */
+
+'use strict';
+
+module.exports = function (suites, context) {
+
+ return {
+ /**
+ * This is only present if flag --delay is passed into Mocha. It triggers
+ * root suite execution. Returns a function which runs the root suite.
+ */
+ runWithSuite: function runWithSuite(suite) {
+ return function run() {
+ suite.run();
+ };
+ },
+
+ /**
+ * Execute before running tests.
+ */
+ before: function (name, fn) {
+ suites[0].beforeAll(name, fn);
+ },
+
+ /**
+ * Execute after running tests.
+ */
+ after: function (name, fn) {
+ suites[0].afterAll(name, fn);
+ },
+
+ /**
+ * Execute before each test case.
+ */
+ beforeEach: function (name, fn) {
+ suites[0].beforeEach(name, fn);
+ },
+
+ /**
+ * Execute after each test case.
+ */
+ afterEach: function (name, fn) {
+ suites[0].afterEach(name, fn);
+ },
+
+ test: {
+ /**
+ * Pending test case.
+ */
+ skip: function (title) {
+ context.test(title);
+ }
+ }
+ }
+};
+
+}); // module: interfaces/common.js
+
+require.register("interfaces/exports.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test');
+
+/**
+ * TDD-style interface:
+ *
+ * exports.Array = {
+ * '#indexOf()': {
+ * 'should return -1 when the value is not present': function(){
+ *
+ * },
+ *
+ * 'should return the correct index when the value is present': function(){
+ *
+ * }
+ * }
+ * };
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('require', visit);
+
+ function visit(obj, file) {
+ var suite;
+ for (var key in obj) {
+ if ('function' == typeof obj[key]) {
+ var fn = obj[key];
+ switch (key) {
+ case 'before':
+ suites[0].beforeAll(fn);
+ break;
+ case 'after':
+ suites[0].afterAll(fn);
+ break;
+ case 'beforeEach':
+ suites[0].beforeEach(fn);
+ break;
+ case 'afterEach':
+ suites[0].afterEach(fn);
+ break;
+ default:
+ var test = new Test(key, fn);
+ test.file = file;
+ suites[0].addTest(test);
+ }
+ } else {
+ suite = Suite.create(suites[0], key);
+ suites.unshift(suite);
+ visit(obj[key]);
+ suites.shift();
+ }
+ }
+ }
+};
+
+}); // module: interfaces/exports.js
+
+require.register("interfaces/index.js", function(module, exports, require){
+exports.bdd = require('./bdd');
+exports.tdd = require('./tdd');
+exports.qunit = require('./qunit');
+exports.exports = require('./exports');
+
+}); // module: interfaces/index.js
+
+require.register("interfaces/qunit.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test')
+ , escapeRe = require('browser/escape-string-regexp')
+ , utils = require('../utils');
+
+/**
+ * QUnit-style interface:
+ *
+ * suite('Array');
+ *
+ * test('#length', function(){
+ * var arr = [1,2,3];
+ * ok(arr.length == 3);
+ * });
+ *
+ * test('#indexOf()', function(){
+ * var arr = [1,2,3];
+ * ok(arr.indexOf(1) == 0);
+ * ok(arr.indexOf(2) == 1);
+ * ok(arr.indexOf(3) == 2);
+ * });
+ *
+ * suite('String');
+ *
+ * test('#length', function(){
+ * ok('foo'.length == 3);
+ * });
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('pre-require', function(context, file, mocha){
+
+ var common = require('./common')(suites, context);
+
+ context.before = common.before;
+ context.after = common.after;
+ context.beforeEach = common.beforeEach;
+ context.afterEach = common.afterEach;
+ context.run = mocha.options.delay && common.runWithSuite(suite);
+ /**
+ * Describe a "suite" with the given `title`.
+ */
+
+ context.suite = function(title){
+ if (suites.length > 1) suites.shift();
+ var suite = Suite.create(suites[0], title);
+ suite.file = file;
+ suites.unshift(suite);
+ return suite;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.suite.only = function(title, fn){
+ var suite = context.suite(title, fn);
+ mocha.grep(suite.fullTitle());
+ };
+
+ /**
+ * Describe a specification or test-case
+ * with the given `title` and callback `fn`
+ * acting as a thunk.
+ */
+
+ context.test = function(title, fn){
+ var test = new Test(title, fn);
+ test.file = file;
+ suites[0].addTest(test);
+ return test;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.test.only = function(title, fn){
+ var test = context.test(title, fn);
+ var reString = '^' + escapeRe(test.fullTitle()) + '$';
+ mocha.grep(new RegExp(reString));
+ };
+
+ context.test.skip = common.test.skip;
+
+ });
+};
+
+}); // module: interfaces/qunit.js
+
+require.register("interfaces/tdd.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Suite = require('../suite')
+ , Test = require('../test')
+ , escapeRe = require('browser/escape-string-regexp')
+ , utils = require('../utils');
+
+/**
+ * TDD-style interface:
+ *
+ * suite('Array', function(){
+ * suite('#indexOf()', function(){
+ * suiteSetup(function(){
+ *
+ * });
+ *
+ * test('should return -1 when not present', function(){
+ *
+ * });
+ *
+ * test('should return the index when present', function(){
+ *
+ * });
+ *
+ * suiteTeardown(function(){
+ *
+ * });
+ * });
+ * });
+ *
+ */
+
+module.exports = function(suite){
+ var suites = [suite];
+
+ suite.on('pre-require', function(context, file, mocha){
+
+ var common = require('./common')(suites, context);
+
+ context.setup = common.beforeEach;
+ context.teardown = common.afterEach;
+ context.suiteSetup = common.before;
+ context.suiteTeardown = common.after;
+ context.run = mocha.options.delay && common.runWithSuite(suite);
+ /**
+ * Describe a "suite" with the given `title`
+ * and callback `fn` containing nested suites
+ * and/or tests.
+ */
+
+ context.suite = function(title, fn){
+ var suite = Suite.create(suites[0], title);
+ suite.file = file;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ return suite;
+ };
+
+ /**
+ * Pending suite.
+ */
+ context.suite.skip = function(title, fn) {
+ var suite = Suite.create(suites[0], title);
+ suite.pending = true;
+ suites.unshift(suite);
+ fn.call(suite);
+ suites.shift();
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.suite.only = function(title, fn){
+ var suite = context.suite(title, fn);
+ mocha.grep(suite.fullTitle());
+ };
+
+ /**
+ * Describe a specification or test-case
+ * with the given `title` and callback `fn`
+ * acting as a thunk.
+ */
+
+ context.test = function(title, fn){
+ var suite = suites[0];
+ if (suite.pending) fn = null;
+ var test = new Test(title, fn);
+ test.file = file;
+ suite.addTest(test);
+ return test;
+ };
+
+ /**
+ * Exclusive test-case.
+ */
+
+ context.test.only = function(title, fn){
+ var test = context.test(title, fn);
+ var reString = '^' + escapeRe(test.fullTitle()) + '$';
+ mocha.grep(new RegExp(reString));
+ };
+
+ context.test.skip = common.test.skip;
+ });
+};
+
+}); // module: interfaces/tdd.js
+
+require.register("mocha.js", function(module, exports, require){
+/*!
+ * mocha
+ * Copyright(c) 2011 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var path = require('browser/path')
+ , escapeRe = require('browser/escape-string-regexp')
+ , utils = require('./utils');
+
+/**
+ * Expose `Mocha`.
+ */
+
+exports = module.exports = Mocha;
+
+/**
+ * To require local UIs and reporters when running in node.
+ */
+
+if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
+ var join = path.join
+ , cwd = process.cwd();
+ module.paths.push(cwd, join(cwd, 'node_modules'));
+}
+
+/**
+ * Expose internals.
+ */
+
+exports.utils = utils;
+exports.interfaces = require('./interfaces');
+exports.reporters = require('./reporters');
+exports.Runnable = require('./runnable');
+exports.Context = require('./context');
+exports.Runner = require('./runner');
+exports.Suite = require('./suite');
+exports.Hook = require('./hook');
+exports.Test = require('./test');
+
+/**
+ * Return image `name` path.
+ *
+ * @param {String} name
+ * @return {String}
+ * @api private
+ */
+
+function image(name) {
+ return __dirname + '/../images/' + name + '.png';
+}
+
+/**
+ * Setup mocha with `options`.
+ *
+ * Options:
+ *
+ * - `ui` name "bdd", "tdd", "exports" etc
+ * - `reporter` reporter instance, defaults to `mocha.reporters.spec`
+ * - `globals` array of accepted globals
+ * - `timeout` timeout in milliseconds
+ * - `bail` bail on the first test failure
+ * - `slow` milliseconds to wait before considering a test slow
+ * - `ignoreLeaks` ignore global leaks
+ * - `fullTrace` display the full stack-trace on failing
+ * - `grep` string or regexp to filter tests with
+ *
+ * @param {Object} options
+ * @api public
+ */
+
+function Mocha(options) {
+ options = options || {};
+ this.files = [];
+ this.options = options;
+ if (options.grep) this.grep(new RegExp(options.grep));
+ if (options.fgrep) this.grep(options.fgrep);
+ this.suite = new exports.Suite('', new exports.Context);
+ this.ui(options.ui);
+ this.bail(options.bail);
+ this.reporter(options.reporter, options.reporterOptions);
+ if (null != options.timeout) this.timeout(options.timeout);
+ this.useColors(options.useColors);
+ if (options.enableTimeouts !== null) this.enableTimeouts(options.enableTimeouts);
+ if (options.slow) this.slow(options.slow);
+
+ this.suite.on('pre-require', function (context) {
+ exports.afterEach = context.afterEach || context.teardown;
+ exports.after = context.after || context.suiteTeardown;
+ exports.beforeEach = context.beforeEach || context.setup;
+ exports.before = context.before || context.suiteSetup;
+ exports.describe = context.describe || context.suite;
+ exports.it = context.it || context.test;
+ exports.setup = context.setup || context.beforeEach;
+ exports.suiteSetup = context.suiteSetup || context.before;
+ exports.suiteTeardown = context.suiteTeardown || context.after;
+ exports.suite = context.suite || context.describe;
+ exports.teardown = context.teardown || context.afterEach;
+ exports.test = context.test || context.it;
+ exports.run = context.run;
+ });
+}
+
+/**
+ * Enable or disable bailing on the first failure.
+ *
+ * @param {Boolean} [bail]
+ * @api public
+ */
+
+Mocha.prototype.bail = function(bail){
+ if (0 == arguments.length) bail = true;
+ this.suite.bail(bail);
+ return this;
+};
+
+/**
+ * Add test `file`.
+ *
+ * @param {String} file
+ * @api public
+ */
+
+Mocha.prototype.addFile = function(file){
+ this.files.push(file);
+ return this;
+};
+
+/**
+ * Set reporter to `reporter`, defaults to "spec".
+ *
+ * @param {String|Function} reporter name or constructor
+ * @param {Object} reporterOptions optional options
+ * @api public
+ */
+Mocha.prototype.reporter = function(reporter, reporterOptions){
+ if ('function' == typeof reporter) {
+ this._reporter = reporter;
+ } else {
+ reporter = reporter || 'spec';
+ var _reporter;
+ try { _reporter = require('./reporters/' + reporter); } catch (err) {}
+ if (!_reporter) try { _reporter = require(reporter); } catch (err) {
+ err.message.indexOf('Cannot find module') !== -1
+ ? console.warn('"' + reporter + '" reporter not found')
+ : console.warn('"' + reporter + '" reporter blew up with error:\n' + err.stack);
+ }
+ if (!_reporter && reporter === 'teamcity')
+ console.warn('The Teamcity reporter was moved to a package named ' +
+ 'mocha-teamcity-reporter ' +
+ '(https://npmjs.org/package/mocha-teamcity-reporter).');
+ if (!_reporter) throw new Error('invalid reporter "' + reporter + '"');
+ this._reporter = _reporter;
+ }
+ this.options.reporterOptions = reporterOptions;
+ return this;
+};
+
+/**
+ * Set test UI `name`, defaults to "bdd".
+ *
+ * @param {String} bdd
+ * @api public
+ */
+
+Mocha.prototype.ui = function(name){
+ name = name || 'bdd';
+ this._ui = exports.interfaces[name];
+ if (!this._ui) try { this._ui = require(name); } catch (err) {}
+ if (!this._ui) throw new Error('invalid interface "' + name + '"');
+ this._ui = this._ui(this.suite);
+ return this;
+};
+
+/**
+ * Load registered files.
+ *
+ * @api private
+ */
+
+Mocha.prototype.loadFiles = function(fn){
+ var self = this;
+ var suite = this.suite;
+ var pending = this.files.length;
+ this.files.forEach(function(file){
+ file = path.resolve(file);
+ suite.emit('pre-require', global, file, self);
+ suite.emit('require', require(file), file, self);
+ suite.emit('post-require', global, file, self);
+ --pending || (fn && fn());
+ });
+};
+
+/**
+ * Enable growl support.
+ *
+ * @api private
+ */
+
+Mocha.prototype._growl = function(runner, reporter) {
+ var notify = require('growl');
+
+ runner.on('end', function(){
+ var stats = reporter.stats;
+ if (stats.failures) {
+ var msg = stats.failures + ' of ' + runner.total + ' tests failed';
+ notify(msg, { name: 'mocha', title: 'Failed', image: image('error') });
+ } else {
+ notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', {
+ name: 'mocha'
+ , title: 'Passed'
+ , image: image('ok')
+ });
+ }
+ });
+};
+
+/**
+ * Add regexp to grep, if `re` is a string it is escaped.
+ *
+ * @param {RegExp|String} re
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.grep = function(re){
+ this.options.grep = 'string' == typeof re
+ ? new RegExp(escapeRe(re))
+ : re;
+ return this;
+};
+
+/**
+ * Invert `.grep()` matches.
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.invert = function(){
+ this.options.invert = true;
+ return this;
+};
+
+/**
+ * Ignore global leaks.
+ *
+ * @param {Boolean} ignore
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.ignoreLeaks = function(ignore){
+ this.options.ignoreLeaks = !!ignore;
+ return this;
+};
+
+/**
+ * Enable global leak checking.
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.checkLeaks = function(){
+ this.options.ignoreLeaks = false;
+ return this;
+};
+
+/**
+ * Display long stack-trace on failing
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.fullTrace = function() {
+ this.options.fullStackTrace = true;
+ return this;
+};
+
+/**
+ * Enable growl support.
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.growl = function(){
+ this.options.growl = true;
+ return this;
+};
+
+/**
+ * Ignore `globals` array or string.
+ *
+ * @param {Array|String} globals
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.globals = function(globals){
+ this.options.globals = (this.options.globals || []).concat(globals);
+ return this;
+};
+
+/**
+ * Emit color output.
+ *
+ * @param {Boolean} colors
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.useColors = function(colors){
+ if (colors !== undefined) {
+ this.options.useColors = colors;
+ }
+ return this;
+};
+
+/**
+ * Use inline diffs rather than +/-.
+ *
+ * @param {Boolean} inlineDiffs
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.useInlineDiffs = function(inlineDiffs) {
+ this.options.useInlineDiffs = arguments.length && inlineDiffs != undefined
+ ? inlineDiffs
+ : false;
+ return this;
+};
+
+/**
+ * Set the timeout in milliseconds.
+ *
+ * @param {Number} timeout
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.timeout = function(timeout){
+ this.suite.timeout(timeout);
+ return this;
+};
+
+/**
+ * Set slowness threshold in milliseconds.
+ *
+ * @param {Number} slow
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.slow = function(slow){
+ this.suite.slow(slow);
+ return this;
+};
+
+/**
+ * Enable timeouts.
+ *
+ * @param {Boolean} enabled
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.enableTimeouts = function(enabled) {
+ this.suite.enableTimeouts(arguments.length && enabled !== undefined
+ ? enabled
+ : true);
+ return this
+};
+
+/**
+ * Makes all tests async (accepting a callback)
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.asyncOnly = function(){
+ this.options.asyncOnly = true;
+ return this;
+};
+
+/**
+ * Disable syntax highlighting (in browser).
+ * @returns {Mocha}
+ * @api public
+ */
+Mocha.prototype.noHighlighting = function() {
+ this.options.noHighlighting = true;
+ return this;
+};
+
+/**
+ * Delay root suite execution.
+ * @returns {Mocha}
+ * @api public
+ */
+Mocha.prototype.delay = function delay() {
+ this.options.delay = true;
+ return this;
+};
+
+/**
+ * Run tests and invoke `fn()` when complete.
+ *
+ * @param {Function} fn
+ * @return {Runner}
+ * @api public
+ */
+Mocha.prototype.run = function(fn){
+ if (this.files.length) this.loadFiles();
+ var suite = this.suite;
+ var options = this.options;
+ options.files = this.files;
+ var runner = new exports.Runner(suite, options.delay);
+ var reporter = new this._reporter(runner, options);
+ runner.ignoreLeaks = false !== options.ignoreLeaks;
+ runner.fullStackTrace = options.fullStackTrace;
+ runner.asyncOnly = options.asyncOnly;
+ if (options.grep) runner.grep(options.grep, options.invert);
+ if (options.globals) runner.globals(options.globals);
+ if (options.growl) this._growl(runner, reporter);
+ if (options.useColors !== undefined) {
+ exports.reporters.Base.useColors = options.useColors;
+ }
+ exports.reporters.Base.inlineDiffs = options.useInlineDiffs;
+
+ function done(failures) {
+ if (reporter.done) {
+ reporter.done(failures, fn);
+ } else fn && fn(failures);
+ }
+
+ return runner.run(done);
+};
+
+}); // module: mocha.js
+
+require.register("ms.js", function(module, exports, require){
+/**
+ * Helpers.
+ */
+
+var s = 1000;
+var m = s * 60;
+var h = m * 60;
+var d = h * 24;
+var y = d * 365.25;
+
+/**
+ * Parse or format the given `val`.
+ *
+ * Options:
+ *
+ * - `long` verbose formatting [false]
+ *
+ * @param {String|Number} val
+ * @param {Object} options
+ * @return {String|Number}
+ * @api public
+ */
+
+module.exports = function(val, options){
+ options = options || {};
+ if ('string' == typeof val) return parse(val);
+ return options['long'] ? longFormat(val) : shortFormat(val);
+};
+
+/**
+ * Parse the given `str` and return milliseconds.
+ *
+ * @param {String} str
+ * @return {Number}
+ * @api private
+ */
+
+function parse(str) {
+ var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str);
+ if (!match) return undefined;
+ var n = parseFloat(match[1]);
+ var type = (match[2] || 'ms').toLowerCase();
+ switch (type) {
+ case 'years':
+ case 'year':
+ case 'y':
+ return n * y;
+ case 'days':
+ case 'day':
+ case 'd':
+ return n * d;
+ case 'hours':
+ case 'hour':
+ case 'h':
+ return n * h;
+ case 'minutes':
+ case 'minute':
+ case 'm':
+ return n * m;
+ case 'seconds':
+ case 'second':
+ case 's':
+ return n * s;
+ case 'ms':
+ return n;
+ }
+}
+
+/**
+ * Short format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function shortFormat(ms) {
+ if (ms >= d) return Math.round(ms / d) + 'd';
+ if (ms >= h) return Math.round(ms / h) + 'h';
+ if (ms >= m) return Math.round(ms / m) + 'm';
+ if (ms >= s) return Math.round(ms / s) + 's';
+ return ms + 'ms';
+}
+
+/**
+ * Long format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function longFormat(ms) {
+ return plural(ms, d, 'day')
+ || plural(ms, h, 'hour')
+ || plural(ms, m, 'minute')
+ || plural(ms, s, 'second')
+ || ms + ' ms';
+}
+
+/**
+ * Pluralization helper.
+ */
+
+function plural(ms, n, name) {
+ if (ms < n) return undefined;
+ if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
+ return Math.ceil(ms / n) + ' ' + name + 's';
+}
+
+}); // module: ms.js
+
+require.register("pending.js", function(module, exports, require){
+
+/**
+ * Expose `Pending`.
+ */
+
+module.exports = Pending;
+
+/**
+ * Initialize a new `Pending` error with the given message.
+ *
+ * @param {String} message
+ */
+
+function Pending(message) {
+ this.message = message;
+}
+
+}); // module: pending.js
+
+require.register("reporters/base.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var tty = require('browser/tty')
+ , diff = require('browser/diff')
+ , ms = require('../ms')
+ , utils = require('../utils')
+ , supportsColor = process.env ? require('supports-color') : null;
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Check if both stdio streams are associated with a tty.
+ */
+
+var isatty = tty.isatty(1) && tty.isatty(2);
+
+/**
+ * Expose `Base`.
+ */
+
+exports = module.exports = Base;
+
+/**
+ * Enable coloring by default, except in the browser interface.
+ */
+
+exports.useColors = process.env
+ ? (supportsColor || (process.env.MOCHA_COLORS !== undefined))
+ : false;
+
+/**
+ * Inline diffs instead of +/-
+ */
+
+exports.inlineDiffs = false;
+
+/**
+ * Default color map.
+ */
+
+exports.colors = {
+ 'pass': 90
+ , 'fail': 31
+ , 'bright pass': 92
+ , 'bright fail': 91
+ , 'bright yellow': 93
+ , 'pending': 36
+ , 'suite': 0
+ , 'error title': 0
+ , 'error message': 31
+ , 'error stack': 90
+ , 'checkmark': 32
+ , 'fast': 90
+ , 'medium': 33
+ , 'slow': 31
+ , 'green': 32
+ , 'light': 90
+ , 'diff gutter': 90
+ , 'diff added': 42
+ , 'diff removed': 41
+};
+
+/**
+ * Default symbol map.
+ */
+
+exports.symbols = {
+ ok: '✓',
+ err: '✖',
+ dot: '․'
+};
+
+// With node.js on Windows: use symbols available in terminal default fonts
+if ('win32' == process.platform) {
+ exports.symbols.ok = '\u221A';
+ exports.symbols.err = '\u00D7';
+ exports.symbols.dot = '.';
+}
+
+/**
+ * Color `str` with the given `type`,
+ * allowing colors to be disabled,
+ * as well as user-defined color
+ * schemes.
+ *
+ * @param {String} type
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+var color = exports.color = function(type, str) {
+ if (!exports.useColors) return String(str);
+ return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m';
+};
+
+/**
+ * Expose term window size, with some
+ * defaults for when stderr is not a tty.
+ */
+
+exports.window = {
+ width: 75
+};
+if (isatty) {
+ exports.window.width = process.stdout.getWindowSize
+ ? process.stdout.getWindowSize(1)[0]
+ : tty.getWindowSize()[1];
+}
+
+/**
+ * Expose some basic cursor interactions
+ * that are common among reporters.
+ */
+
+exports.cursor = {
+ hide: function(){
+ isatty && process.stdout.write('\u001b[?25l');
+ },
+
+ show: function(){
+ isatty && process.stdout.write('\u001b[?25h');
+ },
+
+ deleteLine: function(){
+ isatty && process.stdout.write('\u001b[2K');
+ },
+
+ beginningOfLine: function(){
+ isatty && process.stdout.write('\u001b[0G');
+ },
+
+ CR: function(){
+ if (isatty) {
+ exports.cursor.deleteLine();
+ exports.cursor.beginningOfLine();
+ } else {
+ process.stdout.write('\r');
+ }
+ }
+};
+
+/**
+ * Outut the given `failures` as a list.
+ *
+ * @param {Array} failures
+ * @api public
+ */
+
+exports.list = function(failures){
+ console.log();
+ failures.forEach(function(test, i){
+ // format
+ var fmt = color('error title', ' %s) %s:\n')
+ + color('error message', ' %s')
+ + color('error stack', '\n%s\n');
+
+ // msg
+ var err = test.err
+ , message = err.message || ''
+ , stack = err.stack || message
+ , index = stack.indexOf(message)
+ , actual = err.actual
+ , expected = err.expected
+ , escape = true;
+ if (index === -1) {
+ msg = message;
+ } else {
+ index += message.length;
+ msg = stack.slice(0, index);
+ // remove msg from stack
+ stack = stack.slice(index + 1);
+ }
+
+ // uncaught
+ if (err.uncaught) {
+ msg = 'Uncaught ' + msg;
+ }
+ // explicitly show diff
+ if (err.showDiff !== false && sameType(actual, expected)
+ && expected !== undefined) {
+
+ if ('string' !== typeof actual) {
+ escape = false;
+ err.actual = actual = utils.stringify(actual);
+ err.expected = expected = utils.stringify(expected);
+ }
+
+ fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n');
+ var match = message.match(/^([^:]+): expected/);
+ msg = '\n ' + color('error message', match ? match[1] : msg);
+
+ if (exports.inlineDiffs) {
+ msg += inlineDiff(err, escape);
+ } else {
+ msg += unifiedDiff(err, escape);
+ }
+ }
+
+ // indent stack trace
+ stack = stack.replace(/^/gm, ' ');
+
+ console.log(fmt, (i + 1), test.fullTitle(), msg, stack);
+ });
+};
+
+/**
+ * Initialize a new `Base` reporter.
+ *
+ * All other reporters generally
+ * inherit from this reporter, providing
+ * stats such as test duration, number
+ * of tests passed / failed etc.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Base(runner) {
+ var self = this
+ , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 }
+ , failures = this.failures = [];
+
+ if (!runner) return;
+ this.runner = runner;
+
+ runner.stats = stats;
+
+ runner.on('start', function(){
+ stats.start = new Date;
+ });
+
+ runner.on('suite', function(suite){
+ stats.suites = stats.suites || 0;
+ suite.root || stats.suites++;
+ });
+
+ runner.on('test end', function(test){
+ stats.tests = stats.tests || 0;
+ stats.tests++;
+ });
+
+ runner.on('pass', function(test){
+ stats.passes = stats.passes || 0;
+
+ var medium = test.slow() / 2;
+ if (test.duration > test.slow()) {
+ test.speed = 'slow';
+ } else if (test.duration > medium) {
+ test.speed = 'medium';
+ } else {
+ test.speed = 'fast';
+ }
+
+ stats.passes++;
+ });
+
+ runner.on('fail', function(test, err){
+ stats.failures = stats.failures || 0;
+ stats.failures++;
+ test.err = err;
+ failures.push(test);
+ });
+
+ runner.on('end', function(){
+ stats.end = new Date;
+ stats.duration = new Date - stats.start;
+ });
+
+ runner.on('pending', function(){
+ stats.pending++;
+ });
+}
+
+/**
+ * Output common epilogue used by many of
+ * the bundled reporters.
+ *
+ * @api public
+ */
+
+Base.prototype.epilogue = function(){
+ var stats = this.stats;
+ var tests;
+ var fmt;
+
+ console.log();
+
+ // passes
+ fmt = color('bright pass', ' ')
+ + color('green', ' %d passing')
+ + color('light', ' (%s)');
+
+ console.log(fmt,
+ stats.passes || 0,
+ ms(stats.duration));
+
+ // pending
+ if (stats.pending) {
+ fmt = color('pending', ' ')
+ + color('pending', ' %d pending');
+
+ console.log(fmt, stats.pending);
+ }
+
+ // failures
+ if (stats.failures) {
+ fmt = color('fail', ' %d failing');
+
+ console.log(fmt, stats.failures);
+
+ Base.list(this.failures);
+ console.log();
+ }
+
+ console.log();
+};
+
+/**
+ * Pad the given `str` to `len`.
+ *
+ * @param {String} str
+ * @param {String} len
+ * @return {String}
+ * @api private
+ */
+
+function pad(str, len) {
+ str = String(str);
+ return Array(len - str.length + 1).join(' ') + str;
+}
+
+
+/**
+ * Returns an inline diff between 2 strings with coloured ANSI output
+ *
+ * @param {Error} Error with actual/expected
+ * @return {String} Diff
+ * @api private
+ */
+
+function inlineDiff(err, escape) {
+ var msg = errorDiff(err, 'WordsWithSpace', escape);
+
+ // linenos
+ var lines = msg.split('\n');
+ if (lines.length > 4) {
+ var width = String(lines.length).length;
+ msg = lines.map(function(str, i){
+ return pad(++i, width) + ' |' + ' ' + str;
+ }).join('\n');
+ }
+
+ // legend
+ msg = '\n'
+ + color('diff removed', 'actual')
+ + ' '
+ + color('diff added', 'expected')
+ + '\n\n'
+ + msg
+ + '\n';
+
+ // indent
+ msg = msg.replace(/^/gm, ' ');
+ return msg;
+}
+
+/**
+ * Returns a unified diff between 2 strings
+ *
+ * @param {Error} Error with actual/expected
+ * @return {String} Diff
+ * @api private
+ */
+
+function unifiedDiff(err, escape) {
+ var indent = ' ';
+ function cleanUp(line) {
+ if (escape) {
+ line = escapeInvisibles(line);
+ }
+ if (line[0] === '+') return indent + colorLines('diff added', line);
+ if (line[0] === '-') return indent + colorLines('diff removed', line);
+ if (line.match(/\@\@/)) return null;
+ if (line.match(/\\ No newline/)) return null;
+ else return indent + line;
+ }
+ function notBlank(line) {
+ return line != null;
+ }
+ var msg = diff.createPatch('string', err.actual, err.expected);
+ var lines = msg.split('\n').splice(4);
+ return '\n '
+ + colorLines('diff added', '+ expected') + ' '
+ + colorLines('diff removed', '- actual')
+ + '\n\n'
+ + lines.map(cleanUp).filter(notBlank).join('\n');
+}
+
+/**
+ * Return a character diff for `err`.
+ *
+ * @param {Error} err
+ * @return {String}
+ * @api private
+ */
+
+function errorDiff(err, type, escape) {
+ var actual = escape ? escapeInvisibles(err.actual) : err.actual;
+ var expected = escape ? escapeInvisibles(err.expected) : err.expected;
+ return diff['diff' + type](actual, expected).map(function(str){
+ if (str.added) return colorLines('diff added', str.value);
+ if (str.removed) return colorLines('diff removed', str.value);
+ return str.value;
+ }).join('');
+}
+
+/**
+ * Returns a string with all invisible characters in plain text
+ *
+ * @param {String} line
+ * @return {String}
+ * @api private
+ */
+function escapeInvisibles(line) {
+ return line.replace(/\t/g, '<tab>')
+ .replace(/\r/g, '<CR>')
+ .replace(/\n/g, '<LF>\n');
+}
+
+/**
+ * Color lines for `str`, using the color `name`.
+ *
+ * @param {String} name
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+function colorLines(name, str) {
+ return str.split('\n').map(function(str){
+ return color(name, str);
+ }).join('\n');
+}
+
+/**
+ * Check that a / b have the same type.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Boolean}
+ * @api private
+ */
+
+function sameType(a, b) {
+ a = Object.prototype.toString.call(a);
+ b = Object.prototype.toString.call(b);
+ return a == b;
+}
+
+}); // module: reporters/base.js
+
+require.register("reporters/doc.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils');
+
+/**
+ * Expose `Doc`.
+ */
+
+exports = module.exports = Doc;
+
+/**
+ * Initialize a new `Doc` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Doc(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , total = runner.total
+ , indents = 2;
+
+ function indent() {
+ return Array(indents).join(' ');
+ }
+
+ runner.on('suite', function(suite){
+ if (suite.root) return;
+ ++indents;
+ console.log('%s<section class="suite">', indent());
+ ++indents;
+ console.log('%s<h1>%s</h1>', indent(), utils.escape(suite.title));
+ console.log('%s<dl>', indent());
+ });
+
+ runner.on('suite end', function(suite){
+ if (suite.root) return;
+ console.log('%s</dl>', indent());
+ --indents;
+ console.log('%s</section>', indent());
+ --indents;
+ });
+
+ runner.on('pass', function(test){
+ console.log('%s <dt>%s</dt>', indent(), utils.escape(test.title));
+ var code = utils.escape(utils.clean(test.fn.toString()));
+ console.log('%s <dd><pre><code>%s</code></pre></dd>', indent(), code);
+ });
+
+ runner.on('fail', function(test, err){
+ console.log('%s <dt class="error">%s</dt>', indent(), utils.escape(test.title));
+ var code = utils.escape(utils.clean(test.fn.toString()));
+ console.log('%s <dd class="error"><pre><code>%s</code></pre></dd>', indent(), code);
+ console.log('%s <dd class="error">%s</dd>', indent(), utils.escape(err));
+ });
+}
+
+}); // module: reporters/doc.js
+
+require.register("reporters/dot.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , color = Base.color;
+
+/**
+ * Expose `Dot`.
+ */
+
+exports = module.exports = Dot;
+
+/**
+ * Initialize a new `Dot` matrix test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Dot(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .75 | 0
+ , n = -1;
+
+ runner.on('start', function(){
+ process.stdout.write('\n');
+ });
+
+ runner.on('pending', function(test){
+ if (++n % width == 0) process.stdout.write('\n ');
+ process.stdout.write(color('pending', Base.symbols.dot));
+ });
+
+ runner.on('pass', function(test){
+ if (++n % width == 0) process.stdout.write('\n ');
+ if ('slow' == test.speed) {
+ process.stdout.write(color('bright yellow', Base.symbols.dot));
+ } else {
+ process.stdout.write(color(test.speed, Base.symbols.dot));
+ }
+ });
+
+ runner.on('fail', function(test, err){
+ if (++n % width == 0) process.stdout.write('\n ');
+ process.stdout.write(color('fail', Base.symbols.dot));
+ });
+
+ runner.on('end', function(){
+ console.log();
+ self.epilogue();
+ });
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+Dot.prototype = new F;
+Dot.prototype.constructor = Dot;
+
+
+}); // module: reporters/dot.js
+
+require.register("reporters/html-cov.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var JSONCov = require('./json-cov')
+ , fs = require('browser/fs');
+
+/**
+ * Expose `HTMLCov`.
+ */
+
+exports = module.exports = HTMLCov;
+
+/**
+ * Initialize a new `JsCoverage` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function HTMLCov(runner) {
+ var jade = require('jade')
+ , file = __dirname + '/templates/coverage.jade'
+ , str = fs.readFileSync(file, 'utf8')
+ , fn = jade.compile(str, { filename: file })
+ , self = this;
+
+ JSONCov.call(this, runner, false);
+
+ runner.on('end', function(){
+ process.stdout.write(fn({
+ cov: self.cov
+ , coverageClass: coverageClass
+ }));
+ });
+}
+
+/**
+ * Return coverage class for `n`.
+ *
+ * @return {String}
+ * @api private
+ */
+
+function coverageClass(n) {
+ if (n >= 75) return 'high';
+ if (n >= 50) return 'medium';
+ if (n >= 25) return 'low';
+ return 'terrible';
+}
+
+}); // module: reporters/html-cov.js
+
+require.register("reporters/html.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils')
+ , Progress = require('../browser/progress')
+ , escape = utils.escape;
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Expose `HTML`.
+ */
+
+exports = module.exports = HTML;
+
+/**
+ * Stats template.
+ */
+
+var statsTemplate = '<ul id="mocha-stats">'
+ + '<li class="progress"><canvas width="40" height="40"></canvas></li>'
+ + '<li class="passes"><a href="#">passes:</a> <em>0</em></li>'
+ + '<li class="failures"><a href="#">failures:</a> <em>0</em></li>'
+ + '<li class="duration">duration: <em>0</em>s</li>'
+ + '</ul>';
+
+/**
+ * Initialize a new `HTML` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function HTML(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , total = runner.total
+ , stat = fragment(statsTemplate)
+ , items = stat.getElementsByTagName('li')
+ , passes = items[1].getElementsByTagName('em')[0]
+ , passesLink = items[1].getElementsByTagName('a')[0]
+ , failures = items[2].getElementsByTagName('em')[0]
+ , failuresLink = items[2].getElementsByTagName('a')[0]
+ , duration = items[3].getElementsByTagName('em')[0]
+ , canvas = stat.getElementsByTagName('canvas')[0]
+ , report = fragment('<ul id="mocha-report"></ul>')
+ , stack = [report]
+ , progress
+ , ctx
+ , root = document.getElementById('mocha');
+
+ if (canvas.getContext) {
+ var ratio = window.devicePixelRatio || 1;
+ canvas.style.width = canvas.width;
+ canvas.style.height = canvas.height;
+ canvas.width *= ratio;
+ canvas.height *= ratio;
+ ctx = canvas.getContext('2d');
+ ctx.scale(ratio, ratio);
+ progress = new Progress;
+ }
+
+ if (!root) return error('#mocha div missing, add it to your document');
+
+ // pass toggle
+ on(passesLink, 'click', function(){
+ unhide();
+ var name = /pass/.test(report.className) ? '' : ' pass';
+ report.className = report.className.replace(/fail|pass/g, '') + name;
+ if (report.className.trim()) hideSuitesWithout('test pass');
+ });
+
+ // failure toggle
+ on(failuresLink, 'click', function(){
+ unhide();
+ var name = /fail/.test(report.className) ? '' : ' fail';
+ report.className = report.className.replace(/fail|pass/g, '') + name;
+ if (report.className.trim()) hideSuitesWithout('test fail');
+ });
+
+ root.appendChild(stat);
+ root.appendChild(report);
+
+ if (progress) progress.size(40);
+
+ runner.on('suite', function(suite){
+ if (suite.root) return;
+
+ // suite
+ var url = self.suiteURL(suite);
+ var el = fragment('<li class="suite"><h1><a href="%s">%s</a></h1></li>', url, escape(suite.title));
+
+ // container
+ stack[0].appendChild(el);
+ stack.unshift(document.createElement('ul'));
+ el.appendChild(stack[0]);
+ });
+
+ runner.on('suite end', function(suite){
+ if (suite.root) return;
+ stack.shift();
+ });
+
+ runner.on('fail', function(test, err){
+ if ('hook' == test.type) runner.emit('test end', test);
+ });
+
+ runner.on('test end', function(test){
+ // TODO: add to stats
+ var percent = stats.tests / this.total * 100 | 0;
+ if (progress) progress.update(percent).draw(ctx);
+
+ // update stats
+ var ms = new Date - stats.start;
+ text(passes, stats.passes);
+ text(failures, stats.failures);
+ text(duration, (ms / 1000).toFixed(2));
+
+ // test
+ if ('passed' == test.state) {
+ var url = self.testURL(test);
+ var el = fragment('<li class="test pass %e"><h2>%e<span class="duration">%ems</span> <a href="%s" class="replay">‣</a></h2></li>', test.speed, test.title, test.duration, url);
+ } else if (test.pending) {
+ el = fragment('<li class="test pass pending"><h2>%e</h2></li>', test.title);
+ } else {
+ el = fragment('<li class="test fail"><h2>%e <a href="%e" class="replay">‣</a></h2></li>', test.title, self.testURL(test));
+ var str = test.err.stack || test.err.toString();
+
+ // FF / Opera do not add the message
+ if (!~str.indexOf(test.err.message)) {
+ str = test.err.message + '\n' + str;
+ }
+
+ // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
+ // check for the result of the stringifying.
+ if ('[object Error]' == str) str = test.err.message;
+
+ // Safari doesn't give you a stack. Let's at least provide a source line.
+ if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) {
+ str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")";
+ }
+
+ el.appendChild(fragment('<pre class="error">%e</pre>', str));
+ }
+
+ // toggle code
+ // TODO: defer
+ if (!test.pending) {
+ var h2 = el.getElementsByTagName('h2')[0];
+
+ on(h2, 'click', function(){
+ pre.style.display = 'none' == pre.style.display
+ ? 'block'
+ : 'none';
+ });
+
+ var pre = fragment('<pre><code>%e</code></pre>', utils.clean(test.fn.toString()));
+ el.appendChild(pre);
+ pre.style.display = 'none';
+ }
+
+ // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack.
+ if (stack[0]) stack[0].appendChild(el);
+ });
+}
+
+/**
+ * Makes a URL, preserving querystring ("search") parameters.
+ * @param {string} s
+ * @returns {string} your new URL
+ */
+var makeUrl = function makeUrl(s) {
+ var search = window.location.search;
+
+ // Remove previous grep query parameter if present
+ if (search) {
+ search = search.replace(/[?&]grep=[^&\s]*/g, '').replace(/^&/, '?');
+ }
+
+ return window.location.pathname + (search ? search + '&' : '?' ) + 'grep=' + encodeURIComponent(s);
+};
+
+/**
+ * Provide suite URL
+ *
+ * @param {Object} [suite]
+ */
+HTML.prototype.suiteURL = function(suite){
+ return makeUrl(suite.fullTitle());
+};
+
+/**
+ * Provide test URL
+ *
+ * @param {Object} [test]
+ */
+
+HTML.prototype.testURL = function(test){
+ return makeUrl(test.fullTitle());
+};
+
+/**
+ * Display error `msg`.
+ */
+
+function error(msg) {
+ document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
+}
+
+/**
+ * Return a DOM fragment from `html`.
+ */
+
+function fragment(html) {
+ var args = arguments
+ , div = document.createElement('div')
+ , i = 1;
+
+ div.innerHTML = html.replace(/%([se])/g, function(_, type){
+ switch (type) {
+ case 's': return String(args[i++]);
+ case 'e': return escape(args[i++]);
+ }
+ });
+
+ return div.firstChild;
+}
+
+/**
+ * Check for suites that do not have elements
+ * with `classname`, and hide them.
+ */
+
+function hideSuitesWithout(classname) {
+ var suites = document.getElementsByClassName('suite');
+ for (var i = 0; i < suites.length; i++) {
+ var els = suites[i].getElementsByClassName(classname);
+ if (0 == els.length) suites[i].className += ' hidden';
+ }
+}
+
+/**
+ * Unhide .hidden suites.
+ */
+
+function unhide() {
+ var els = document.getElementsByClassName('suite hidden');
+ for (var i = 0; i < els.length; ++i) {
+ els[i].className = els[i].className.replace('suite hidden', 'suite');
+ }
+}
+
+/**
+ * Set `el` text to `str`.
+ */
+
+function text(el, str) {
+ if (el.textContent) {
+ el.textContent = str;
+ } else {
+ el.innerText = str;
+ }
+}
+
+/**
+ * Listen on `event` with callback `fn`.
+ */
+
+function on(el, event, fn) {
+ if (el.addEventListener) {
+ el.addEventListener(event, fn, false);
+ } else {
+ el.attachEvent('on' + event, fn);
+ }
+}
+
+}); // module: reporters/html.js
+
+require.register("reporters/index.js", function(module, exports, require){
+exports.Base = require('./base');
+exports.Dot = require('./dot');
+exports.Doc = require('./doc');
+exports.TAP = require('./tap');
+exports.JSON = require('./json');
+exports.HTML = require('./html');
+exports.List = require('./list');
+exports.Min = require('./min');
+exports.Spec = require('./spec');
+exports.Nyan = require('./nyan');
+exports.XUnit = require('./xunit');
+exports.Markdown = require('./markdown');
+exports.Progress = require('./progress');
+exports.Landing = require('./landing');
+exports.JSONCov = require('./json-cov');
+exports.HTMLCov = require('./html-cov');
+exports.JSONStream = require('./json-stream');
+
+}); // module: reporters/index.js
+
+require.register("reporters/json-cov.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base');
+
+/**
+ * Expose `JSONCov`.
+ */
+
+exports = module.exports = JSONCov;
+
+/**
+ * Initialize a new `JsCoverage` reporter.
+ *
+ * @param {Runner} runner
+ * @param {Boolean} output
+ * @api public
+ */
+
+function JSONCov(runner, output) {
+ var self = this;
+ output = 1 == arguments.length ? true : output;
+
+ Base.call(this, runner);
+
+ var tests = []
+ , failures = []
+ , passes = [];
+
+ runner.on('test end', function(test){
+ tests.push(test);
+ });
+
+ runner.on('pass', function(test){
+ passes.push(test);
+ });
+
+ runner.on('fail', function(test){
+ failures.push(test);
+ });
+
+ runner.on('end', function(){
+ var cov = global._$jscoverage || {};
+ var result = self.cov = map(cov);
+ result.stats = self.stats;
+ result.tests = tests.map(clean);
+ result.failures = failures.map(clean);
+ result.passes = passes.map(clean);
+ if (!output) return;
+ process.stdout.write(JSON.stringify(result, null, 2 ));
+ });
+}
+
+/**
+ * Map jscoverage data to a JSON structure
+ * suitable for reporting.
+ *
+ * @param {Object} cov
+ * @return {Object}
+ * @api private
+ */
+
+function map(cov) {
+ var ret = {
+ instrumentation: 'node-jscoverage'
+ , sloc: 0
+ , hits: 0
+ , misses: 0
+ , coverage: 0
+ , files: []
+ };
+
+ for (var filename in cov) {
+ var data = coverage(filename, cov[filename]);
+ ret.files.push(data);
+ ret.hits += data.hits;
+ ret.misses += data.misses;
+ ret.sloc += data.sloc;
+ }
+
+ ret.files.sort(function(a, b) {
+ return a.filename.localeCompare(b.filename);
+ });
+
+ if (ret.sloc > 0) {
+ ret.coverage = (ret.hits / ret.sloc) * 100;
+ }
+
+ return ret;
+}
+
+/**
+ * Map jscoverage data for a single source file
+ * to a JSON structure suitable for reporting.
+ *
+ * @param {String} filename name of the source file
+ * @param {Object} data jscoverage coverage data
+ * @return {Object}
+ * @api private
+ */
+
+function coverage(filename, data) {
+ var ret = {
+ filename: filename,
+ coverage: 0,
+ hits: 0,
+ misses: 0,
+ sloc: 0,
+ source: {}
+ };
+
+ data.source.forEach(function(line, num){
+ num++;
+
+ if (data[num] === 0) {
+ ret.misses++;
+ ret.sloc++;
+ } else if (data[num] !== undefined) {
+ ret.hits++;
+ ret.sloc++;
+ }
+
+ ret.source[num] = {
+ source: line
+ , coverage: data[num] === undefined
+ ? ''
+ : data[num]
+ };
+ });
+
+ ret.coverage = ret.hits / ret.sloc * 100;
+
+ return ret;
+}
+
+/**
+ * Return a plain-object representation of `test`
+ * free of cyclic properties etc.
+ *
+ * @param {Object} test
+ * @return {Object}
+ * @api private
+ */
+
+function clean(test) {
+ return {
+ title: test.title
+ , fullTitle: test.fullTitle()
+ , duration: test.duration
+ }
+}
+
+}); // module: reporters/json-cov.js
+
+require.register("reporters/json-stream.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , color = Base.color;
+
+/**
+ * Expose `List`.
+ */
+
+exports = module.exports = List;
+
+/**
+ * Initialize a new `List` test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function List(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , total = runner.total;
+
+ runner.on('start', function(){
+ console.log(JSON.stringify(['start', { total: total }]));
+ });
+
+ runner.on('pass', function(test){
+ console.log(JSON.stringify(['pass', clean(test)]));
+ });
+
+ runner.on('fail', function(test, err){
+ test = clean(test);
+ test.err = err.message;
+ console.log(JSON.stringify(['fail', test]));
+ });
+
+ runner.on('end', function(){
+ process.stdout.write(JSON.stringify(['end', self.stats]));
+ });
+}
+
+/**
+ * Return a plain-object representation of `test`
+ * free of cyclic properties etc.
+ *
+ * @param {Object} test
+ * @return {Object}
+ * @api private
+ */
+
+function clean(test) {
+ return {
+ title: test.title
+ , fullTitle: test.fullTitle()
+ , duration: test.duration
+ }
+}
+
+}); // module: reporters/json-stream.js
+
+require.register("reporters/json.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `JSON`.
+ */
+
+exports = module.exports = JSONReporter;
+
+/**
+ * Initialize a new `JSON` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function JSONReporter(runner) {
+ var self = this;
+ Base.call(this, runner);
+
+ var tests = []
+ , pending = []
+ , failures = []
+ , passes = [];
+
+ runner.on('test end', function(test){
+ tests.push(test);
+ });
+
+ runner.on('pass', function(test){
+ passes.push(test);
+ });
+
+ runner.on('fail', function(test){
+ failures.push(test);
+ });
+
+ runner.on('pending', function(test){
+ pending.push(test);
+ });
+
+ runner.on('end', function(){
+ var obj = {
+ stats: self.stats,
+ tests: tests.map(clean),
+ pending: pending.map(clean),
+ failures: failures.map(clean),
+ passes: passes.map(clean)
+ };
+
+ runner.testResults = obj;
+
+ process.stdout.write(JSON.stringify(obj, null, 2));
+ });
+}
+
+/**
+ * Return a plain-object representation of `test`
+ * free of cyclic properties etc.
+ *
+ * @param {Object} test
+ * @return {Object}
+ * @api private
+ */
+
+function clean(test) {
+ return {
+ title: test.title,
+ fullTitle: test.fullTitle(),
+ duration: test.duration,
+ err: errorJSON(test.err || {})
+ }
+}
+
+/**
+ * Transform `error` into a JSON object.
+ * @param {Error} err
+ * @return {Object}
+ */
+
+function errorJSON(err) {
+ var res = {};
+ Object.getOwnPropertyNames(err).forEach(function(key) {
+ res[key] = err[key];
+ }, err);
+ return res;
+}
+
+}); // module: reporters/json.js
+
+require.register("reporters/landing.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `Landing`.
+ */
+
+exports = module.exports = Landing;
+
+/**
+ * Airplane color.
+ */
+
+Base.colors.plane = 0;
+
+/**
+ * Airplane crash color.
+ */
+
+Base.colors['plane crash'] = 31;
+
+/**
+ * Runway color.
+ */
+
+Base.colors.runway = 90;
+
+/**
+ * Initialize a new `Landing` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Landing(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .75 | 0
+ , total = runner.total
+ , stream = process.stdout
+ , plane = color('plane', '✈')
+ , crashed = -1
+ , n = 0;
+
+ function runway() {
+ var buf = Array(width).join('-');
+ return ' ' + color('runway', buf);
+ }
+
+ runner.on('start', function(){
+ stream.write('\n\n\n ');
+ cursor.hide();
+ });
+
+ runner.on('test end', function(test){
+ // check if the plane crashed
+ var col = -1 == crashed
+ ? width * ++n / total | 0
+ : crashed;
+
+ // show the crash
+ if ('failed' == test.state) {
+ plane = color('plane crash', '✈');
+ crashed = col;
+ }
+
+ // render landing strip
+ stream.write('\u001b['+(width+1)+'D\u001b[2A');
+ stream.write(runway());
+ stream.write('\n ');
+ stream.write(color('runway', Array(col).join('⋅')));
+ stream.write(plane)
+ stream.write(color('runway', Array(width - col).join('⋅') + '\n'));
+ stream.write(runway());
+ stream.write('\u001b[0m');
+ });
+
+ runner.on('end', function(){
+ cursor.show();
+ console.log();
+ self.epilogue();
+ });
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+Landing.prototype = new F;
+Landing.prototype.constructor = Landing;
+
+
+}); // module: reporters/landing.js
+
+require.register("reporters/list.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `List`.
+ */
+
+exports = module.exports = List;
+
+/**
+ * Initialize a new `List` test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function List(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , n = 0;
+
+ runner.on('start', function(){
+ console.log();
+ });
+
+ runner.on('test', function(test){
+ process.stdout.write(color('pass', ' ' + test.fullTitle() + ': '));
+ });
+
+ runner.on('pending', function(test){
+ var fmt = color('checkmark', ' -')
+ + color('pending', ' %s');
+ console.log(fmt, test.fullTitle());
+ });
+
+ runner.on('pass', function(test){
+ var fmt = color('checkmark', ' '+Base.symbols.dot)
+ + color('pass', ' %s: ')
+ + color(test.speed, '%dms');
+ cursor.CR();
+ console.log(fmt, test.fullTitle(), test.duration);
+ });
+
+ runner.on('fail', function(test, err){
+ cursor.CR();
+ console.log(color('fail', ' %d) %s'), ++n, test.fullTitle());
+ });
+
+ runner.on('end', self.epilogue.bind(self));
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+List.prototype = new F;
+List.prototype.constructor = List;
+
+
+}); // module: reporters/list.js
+
+require.register("reporters/markdown.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils');
+
+/**
+ * Constants
+ */
+
+var SUITE_PREFIX = '$';
+
+/**
+ * Expose `Markdown`.
+ */
+
+exports = module.exports = Markdown;
+
+/**
+ * Initialize a new `Markdown` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Markdown(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , level = 0
+ , buf = '';
+
+ function title(str) {
+ return Array(level).join('#') + ' ' + str;
+ }
+
+ function indent() {
+ return Array(level).join(' ');
+ }
+
+ function mapTOC(suite, obj) {
+ var ret = obj,
+ key = SUITE_PREFIX + suite.title;
+ obj = obj[key] = obj[key] || { suite: suite };
+ suite.suites.forEach(function(suite){
+ mapTOC(suite, obj);
+ });
+ return ret;
+ }
+
+ function stringifyTOC(obj, level) {
+ ++level;
+ var buf = '';
+ var link;
+ for (var key in obj) {
+ if ('suite' == key) continue;
+ if (key !== SUITE_PREFIX) {
+ link = ' - [' + key.substring(1) + ']';
+ link += '(#' + utils.slug(obj[key].suite.fullTitle()) + ')\n';
+ buf += Array(level).join(' ') + link;
+ }
+ buf += stringifyTOC(obj[key], level);
+ }
+ return buf;
+ }
+
+ function generateTOC(suite) {
+ var obj = mapTOC(suite, {});
+ return stringifyTOC(obj, 0);
+ }
+
+ generateTOC(runner.suite);
+
+ runner.on('suite', function(suite){
+ ++level;
+ var slug = utils.slug(suite.fullTitle());
+ buf += '<a name="' + slug + '"></a>' + '\n';
+ buf += title(suite.title) + '\n';
+ });
+
+ runner.on('suite end', function(suite){
+ --level;
+ });
+
+ runner.on('pass', function(test){
+ var code = utils.clean(test.fn.toString());
+ buf += test.title + '.\n';
+ buf += '\n```js\n';
+ buf += code + '\n';
+ buf += '```\n\n';
+ });
+
+ runner.on('end', function(){
+ process.stdout.write('# TOC\n');
+ process.stdout.write(generateTOC(runner.suite));
+ process.stdout.write(buf);
+ });
+}
+
+}); // module: reporters/markdown.js
+
+require.register("reporters/min.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base');
+
+/**
+ * Expose `Min`.
+ */
+
+exports = module.exports = Min;
+
+/**
+ * Initialize a new `Min` minimal test reporter (best used with --watch).
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Min(runner) {
+ Base.call(this, runner);
+
+ runner.on('start', function(){
+ // clear screen
+ process.stdout.write('\u001b[2J');
+ // set cursor position
+ process.stdout.write('\u001b[1;3H');
+ });
+
+ runner.on('end', this.epilogue.bind(this));
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+Min.prototype = new F;
+Min.prototype.constructor = Min;
+
+
+}); // module: reporters/min.js
+
+require.register("reporters/nyan.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base');
+
+/**
+ * Expose `Dot`.
+ */
+
+exports = module.exports = NyanCat;
+
+/**
+ * Initialize a new `Dot` matrix test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function NyanCat(runner) {
+ Base.call(this, runner);
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .75 | 0
+ , rainbowColors = this.rainbowColors = self.generateColors()
+ , colorIndex = this.colorIndex = 0
+ , numerOfLines = this.numberOfLines = 4
+ , trajectories = this.trajectories = [[], [], [], []]
+ , nyanCatWidth = this.nyanCatWidth = 11
+ , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth)
+ , scoreboardWidth = this.scoreboardWidth = 5
+ , tick = this.tick = 0
+ , n = 0;
+
+ runner.on('start', function(){
+ Base.cursor.hide();
+ self.draw();
+ });
+
+ runner.on('pending', function(test){
+ self.draw();
+ });
+
+ runner.on('pass', function(test){
+ self.draw();
+ });
+
+ runner.on('fail', function(test, err){
+ self.draw();
+ });
+
+ runner.on('end', function(){
+ Base.cursor.show();
+ for (var i = 0; i < self.numberOfLines; i++) write('\n');
+ self.epilogue();
+ });
+}
+
+/**
+ * Draw the nyan cat
+ *
+ * @api private
+ */
+
+NyanCat.prototype.draw = function(){
+ this.appendRainbow();
+ this.drawScoreboard();
+ this.drawRainbow();
+ this.drawNyanCat();
+ this.tick = !this.tick;
+};
+
+/**
+ * Draw the "scoreboard" showing the number
+ * of passes, failures and pending tests.
+ *
+ * @api private
+ */
+
+NyanCat.prototype.drawScoreboard = function(){
+ var stats = this.stats;
+
+ function draw(type, n) {
+ write(' ');
+ write(Base.color(type, n));
+ write('\n');
+ }
+
+ draw('green', stats.passes);
+ draw('fail', stats.failures);
+ draw('pending', stats.pending);
+ write('\n');
+
+ this.cursorUp(this.numberOfLines);
+};
+
+/**
+ * Append the rainbow.
+ *
+ * @api private
+ */
+
+NyanCat.prototype.appendRainbow = function(){
+ var segment = this.tick ? '_' : '-';
+ var rainbowified = this.rainbowify(segment);
+
+ for (var index = 0; index < this.numberOfLines; index++) {
+ var trajectory = this.trajectories[index];
+ if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift();
+ trajectory.push(rainbowified);
+ }
+};
+
+/**
+ * Draw the rainbow.
+ *
+ * @api private
+ */
+
+NyanCat.prototype.drawRainbow = function(){
+ var self = this;
+
+ this.trajectories.forEach(function(line, index) {
+ write('\u001b[' + self.scoreboardWidth + 'C');
+ write(line.join(''));
+ write('\n');
+ });
+
+ this.cursorUp(this.numberOfLines);
+};
+
+/**
+ * Draw the nyan cat
+ *
+ * @api private
+ */
+
+NyanCat.prototype.drawNyanCat = function() {
+ var self = this;
+ var startWidth = this.scoreboardWidth + this.trajectories[0].length;
+ var dist = '\u001b[' + startWidth + 'C';
+ var padding = '';
+
+ write(dist);
+ write('_,------,');
+ write('\n');
+
+ write(dist);
+ padding = self.tick ? ' ' : ' ';
+ write('_|' + padding + '/\\_/\\ ');
+ write('\n');
+
+ write(dist);
+ padding = self.tick ? '_' : '__';
+ var tail = self.tick ? '~' : '^';
+ var face;
+ write(tail + '|' + padding + this.face() + ' ');
+ write('\n');
+
+ write(dist);
+ padding = self.tick ? ' ' : ' ';
+ write(padding + '"" "" ');
+ write('\n');
+
+ this.cursorUp(this.numberOfLines);
+};
+
+/**
+ * Draw nyan cat face.
+ *
+ * @return {String}
+ * @api private
+ */
+
+NyanCat.prototype.face = function() {
+ var stats = this.stats;
+ if (stats.failures) {
+ return '( x .x)';
+ } else if (stats.pending) {
+ return '( o .o)';
+ } else if(stats.passes) {
+ return '( ^ .^)';
+ } else {
+ return '( - .-)';
+ }
+};
+
+/**
+ * Move cursor up `n`.
+ *
+ * @param {Number} n
+ * @api private
+ */
+
+NyanCat.prototype.cursorUp = function(n) {
+ write('\u001b[' + n + 'A');
+};
+
+/**
+ * Move cursor down `n`.
+ *
+ * @param {Number} n
+ * @api private
+ */
+
+NyanCat.prototype.cursorDown = function(n) {
+ write('\u001b[' + n + 'B');
+};
+
+/**
+ * Generate rainbow colors.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+NyanCat.prototype.generateColors = function(){
+ var colors = [];
+
+ for (var i = 0; i < (6 * 7); i++) {
+ var pi3 = Math.floor(Math.PI / 3);
+ var n = (i * (1.0 / 6));
+ var r = Math.floor(3 * Math.sin(n) + 3);
+ var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3);
+ var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3);
+ colors.push(36 * r + 6 * g + b + 16);
+ }
+
+ return colors;
+};
+
+/**
+ * Apply rainbow to the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+NyanCat.prototype.rainbowify = function(str){
+ if (!Base.useColors)
+ return str;
+ var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length];
+ this.colorIndex += 1;
+ return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m';
+};
+
+/**
+ * Stdout helper.
+ */
+
+function write(string) {
+ process.stdout.write(string);
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+NyanCat.prototype = new F;
+NyanCat.prototype.constructor = NyanCat;
+
+
+}); // module: reporters/nyan.js
+
+require.register("reporters/progress.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `Progress`.
+ */
+
+exports = module.exports = Progress;
+
+/**
+ * General progress bar color.
+ */
+
+Base.colors.progress = 90;
+
+/**
+ * Initialize a new `Progress` bar test reporter.
+ *
+ * @param {Runner} runner
+ * @param {Object} options
+ * @api public
+ */
+
+function Progress(runner, options) {
+ Base.call(this, runner);
+
+ options = options || {}
+ var self = this
+ , stats = this.stats
+ , width = Base.window.width * .50 | 0
+ , total = runner.total
+ , complete = 0
+ , max = Math.max
+ , lastN = -1;
+
+ // default chars
+ options.open = options.open || '[';
+ options.complete = options.complete || '▬';
+ options.incomplete = options.incomplete || Base.symbols.dot;
+ options.close = options.close || ']';
+ options.verbose = false;
+
+ // tests started
+ runner.on('start', function(){
+ console.log();
+ cursor.hide();
+ });
+
+ // tests complete
+ runner.on('test end', function(){
+ complete++;
+ var incomplete = total - complete
+ , percent = complete / total
+ , n = width * percent | 0
+ , i = width - n;
+
+ if (lastN === n && !options.verbose) {
+ // Don't re-render the line if it hasn't changed
+ return;
+ }
+ lastN = n;
+
+ cursor.CR();
+ process.stdout.write('\u001b[J');
+ process.stdout.write(color('progress', ' ' + options.open));
+ process.stdout.write(Array(n).join(options.complete));
+ process.stdout.write(Array(i).join(options.incomplete));
+ process.stdout.write(color('progress', options.close));
+ if (options.verbose) {
+ process.stdout.write(color('progress', ' ' + complete + ' of ' + total));
+ }
+ });
+
+ // tests are complete, output some stats
+ // and the failures if any
+ runner.on('end', function(){
+ cursor.show();
+ console.log();
+ self.epilogue();
+ });
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+Progress.prototype = new F;
+Progress.prototype.constructor = Progress;
+
+
+}); // module: reporters/progress.js
+
+require.register("reporters/spec.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `Spec`.
+ */
+
+exports = module.exports = Spec;
+
+/**
+ * Initialize a new `Spec` test reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function Spec(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , indents = 0
+ , n = 0;
+
+ function indent() {
+ return Array(indents).join(' ')
+ }
+
+ runner.on('start', function(){
+ console.log();
+ });
+
+ runner.on('suite', function(suite){
+ ++indents;
+ console.log(color('suite', '%s%s'), indent(), suite.title);
+ });
+
+ runner.on('suite end', function(suite){
+ --indents;
+ if (1 == indents) console.log();
+ });
+
+ runner.on('pending', function(test){
+ var fmt = indent() + color('pending', ' - %s');
+ console.log(fmt, test.title);
+ });
+
+ runner.on('pass', function(test){
+ if ('fast' == test.speed) {
+ var fmt = indent()
+ + color('checkmark', ' ' + Base.symbols.ok)
+ + color('pass', ' %s');
+ cursor.CR();
+ console.log(fmt, test.title);
+ } else {
+ fmt = indent()
+ + color('checkmark', ' ' + Base.symbols.ok)
+ + color('pass', ' %s')
+ + color(test.speed, ' (%dms)');
+ cursor.CR();
+ console.log(fmt, test.title, test.duration);
+ }
+ });
+
+ runner.on('fail', function(test, err){
+ cursor.CR();
+ console.log(indent() + color('fail', ' %d) %s'), ++n, test.title);
+ });
+
+ runner.on('end', self.epilogue.bind(self));
+}
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+Spec.prototype = new F;
+Spec.prototype.constructor = Spec;
+
+
+}); // module: reporters/spec.js
+
+require.register("reporters/tap.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , cursor = Base.cursor
+ , color = Base.color;
+
+/**
+ * Expose `TAP`.
+ */
+
+exports = module.exports = TAP;
+
+/**
+ * Initialize a new `TAP` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function TAP(runner) {
+ Base.call(this, runner);
+
+ var self = this
+ , stats = this.stats
+ , n = 1
+ , passes = 0
+ , failures = 0;
+
+ runner.on('start', function(){
+ var total = runner.grepTotal(runner.suite);
+ console.log('%d..%d', 1, total);
+ });
+
+ runner.on('test end', function(){
+ ++n;
+ });
+
+ runner.on('pending', function(test){
+ console.log('ok %d %s # SKIP -', n, title(test));
+ });
+
+ runner.on('pass', function(test){
+ passes++;
+ console.log('ok %d %s', n, title(test));
+ });
+
+ runner.on('fail', function(test, err){
+ failures++;
+ console.log('not ok %d %s', n, title(test));
+ if (err.stack) console.log(err.stack.replace(/^/gm, ' '));
+ });
+
+ runner.on('end', function(){
+ console.log('# tests ' + (passes + failures));
+ console.log('# pass ' + passes);
+ console.log('# fail ' + failures);
+ });
+}
+
+/**
+ * Return a TAP-safe title of `test`
+ *
+ * @param {Object} test
+ * @return {String}
+ * @api private
+ */
+
+function title(test) {
+ return test.fullTitle().replace(/#/g, '');
+}
+
+}); // module: reporters/tap.js
+
+require.register("reporters/xunit.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Base = require('./base')
+ , utils = require('../utils')
+ , fs = require('browser/fs')
+ , escape = utils.escape;
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Expose `XUnit`.
+ */
+
+exports = module.exports = XUnit;
+
+/**
+ * Initialize a new `XUnit` reporter.
+ *
+ * @param {Runner} runner
+ * @api public
+ */
+
+function XUnit(runner, options) {
+ Base.call(this, runner);
+ var stats = this.stats
+ , tests = []
+ , self = this;
+
+ if (options.reporterOptions && options.reporterOptions.output) {
+ if (! fs.createWriteStream) {
+ throw new Error('file output not supported in browser');
+ }
+ self.fileStream = fs.createWriteStream(options.reporterOptions.output);
+ }
+
+ runner.on('pending', function(test){
+ tests.push(test);
+ });
+
+ runner.on('pass', function(test){
+ tests.push(test);
+ });
+
+ runner.on('fail', function(test){
+ tests.push(test);
+ });
+
+ runner.on('end', function(){
+ self.write(tag('testsuite', {
+ name: 'Mocha Tests'
+ , tests: stats.tests
+ , failures: stats.failures
+ , errors: stats.failures
+ , skipped: stats.tests - stats.failures - stats.passes
+ , timestamp: (new Date).toUTCString()
+ , time: (stats.duration / 1000) || 0
+ }, false));
+
+ tests.forEach(function(t) { self.test(t); });
+ self.write('</testsuite>');
+ });
+}
+
+/**
+ * Override done to close the stream (if it's a file).
+ */
+XUnit.prototype.done = function(failures, fn) {
+ if (this.fileStream) {
+ this.fileStream.end(function() {
+ fn(failures);
+ });
+ } else {
+ fn(failures);
+ }
+};
+
+/**
+ * Inherit from `Base.prototype`.
+ */
+
+function F(){}
+F.prototype = Base.prototype;
+XUnit.prototype = new F;
+XUnit.prototype.constructor = XUnit;
+
+
+/**
+ * Write out the given line
+ */
+XUnit.prototype.write = function(line) {
+ if (this.fileStream) {
+ this.fileStream.write(line + '\n');
+ } else {
+ console.log(line);
+ }
+};
+
+/**
+ * Output tag for the given `test.`
+ */
+
+XUnit.prototype.test = function(test, ostream) {
+ var attrs = {
+ classname: test.parent.fullTitle()
+ , name: test.title
+ , time: (test.duration / 1000) || 0
+ };
+
+ if ('failed' == test.state) {
+ var err = test.err;
+ this.write(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack))));
+ } else if (test.pending) {
+ this.write(tag('testcase', attrs, false, tag('skipped', {}, true)));
+ } else {
+ this.write(tag('testcase', attrs, true) );
+ }
+};
+
+/**
+ * HTML tag helper.
+ */
+
+function tag(name, attrs, close, content) {
+ var end = close ? '/>' : '>'
+ , pairs = []
+ , tag;
+
+ for (var key in attrs) {
+ pairs.push(key + '="' + escape(attrs[key]) + '"');
+ }
+
+ tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end;
+ if (content) tag += content + '</' + name + end;
+ return tag;
+}
+
+/**
+ * Return cdata escaped CDATA `str`.
+ */
+
+function cdata(str) {
+ return '<![CDATA[' + escape(str) + ']]>';
+}
+
+}); // module: reporters/xunit.js
+
+require.register("runnable.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('browser/events').EventEmitter
+ , debug = require('browser/debug')('mocha:runnable')
+ , Pending = require('./pending')
+ , milliseconds = require('./ms')
+ , utils = require('./utils');
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date
+ , setTimeout = global.setTimeout
+ , setInterval = global.setInterval
+ , clearTimeout = global.clearTimeout
+ , clearInterval = global.clearInterval;
+
+/**
+ * Object#toString().
+ */
+
+var toString = Object.prototype.toString;
+
+/**
+ * Expose `Runnable`.
+ */
+
+module.exports = Runnable;
+
+/**
+ * Initialize a new `Runnable` with the given `title` and callback `fn`.
+ *
+ * @param {String} title
+ * @param {Function} fn
+ * @api private
+ */
+
+function Runnable(title, fn) {
+ this.title = title;
+ this.fn = fn;
+ this.async = fn && fn.length;
+ this.sync = ! this.async;
+ this._timeout = 2000;
+ this._slow = 75;
+ this._enableTimeouts = true;
+ this.timedOut = false;
+ this._trace = new Error('done() called multiple times')
+}
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+function F(){}
+F.prototype = EventEmitter.prototype;
+Runnable.prototype = new F;
+Runnable.prototype.constructor = Runnable;
+
+
+/**
+ * Set & get timeout `ms`.
+ *
+ * @param {Number|String} ms
+ * @return {Runnable|Number} ms or self
+ * @api private
+ */
+
+Runnable.prototype.timeout = function(ms){
+ if (0 == arguments.length) return this._timeout;
+ if (ms === 0) this._enableTimeouts = false;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('timeout %d', ms);
+ this._timeout = ms;
+ if (this.timer) this.resetTimeout();
+ return this;
+};
+
+/**
+ * Set & get slow `ms`.
+ *
+ * @param {Number|String} ms
+ * @return {Runnable|Number} ms or self
+ * @api private
+ */
+
+Runnable.prototype.slow = function(ms){
+ if (0 === arguments.length) return this._slow;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('timeout %d', ms);
+ this._slow = ms;
+ return this;
+};
+
+/**
+ * Set and & get timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Runnable|Boolean} enabled or self
+ * @api private
+ */
+
+Runnable.prototype.enableTimeouts = function(enabled){
+ if (arguments.length === 0) return this._enableTimeouts;
+ debug('enableTimeouts %s', enabled);
+ this._enableTimeouts = enabled;
+ return this;
+};
+
+/**
+ * Halt and mark as pending.
+ *
+ * @api private
+ */
+
+Runnable.prototype.skip = function(){
+ throw new Pending();
+};
+
+/**
+ * Return the full title generated by recursively
+ * concatenating the parent's full title.
+ *
+ * @return {String}
+ * @api public
+ */
+
+Runnable.prototype.fullTitle = function(){
+ return this.parent.fullTitle() + ' ' + this.title;
+};
+
+/**
+ * Clear the timeout.
+ *
+ * @api private
+ */
+
+Runnable.prototype.clearTimeout = function(){
+ clearTimeout(this.timer);
+};
+
+/**
+ * Inspect the runnable void of private properties.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Runnable.prototype.inspect = function(){
+ return JSON.stringify(this, function(key, val){
+ if ('_' == key[0]) return undefined;
+ if ('parent' == key) return '#<Suite>';
+ if ('ctx' == key) return '#<Context>';
+ return val;
+ }, 2);
+};
+
+/**
+ * Reset the timeout.
+ *
+ * @api private
+ */
+
+Runnable.prototype.resetTimeout = function(){
+ var self = this;
+ var ms = this.timeout() || 1e9;
+
+ if (!this._enableTimeouts) return;
+ this.clearTimeout();
+ this.timer = setTimeout(function(){
+ if (!self._enableTimeouts) return;
+ self.callback(new Error('timeout of ' + ms + 'ms exceeded. Ensure the done() callback is being called in this test.'));
+ self.timedOut = true;
+ }, ms);
+};
+
+/**
+ * Whitelist these globals for this test run
+ *
+ * @api private
+ */
+Runnable.prototype.globals = function(arr){
+ var self = this;
+ this._allowedGlobals = arr;
+};
+
+/**
+ * Run the test and invoke `fn(err)`.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+Runnable.prototype.run = function(fn){
+ var self = this
+ , start = new Date
+ , ctx = this.ctx
+ , finished
+ , emitted;
+
+ // Some times the ctx exists but it is not runnable
+ if (ctx && ctx.runnable) ctx.runnable(this);
+
+ // called multiple times
+ function multiple(err) {
+ if (emitted) return;
+ emitted = true;
+ self.emit('error', err || new Error('done() called multiple times; stacktrace may be inaccurate'));
+ }
+
+ // finished
+ function done(err) {
+ var ms = self.timeout();
+ if (self.timedOut) return undefined;
+ if (finished) return multiple(err || self._trace);
+
+ // Discard the resolution if this test has already failed asynchronously
+ if (self.state) return undefined;
+
+ self.clearTimeout();
+ self.duration = new Date - start;
+ finished = true;
+ if (!err && self.duration > ms && self._enableTimeouts) err = new Error('timeout of ' + ms + 'ms exceeded. Ensure the done() callback is being called in this test.');
+ fn(err);
+ }
+
+ // for .resetTimeout()
+ this.callback = done;
+
+ // explicit async with `done` argument
+ if (this.async) {
+ this.resetTimeout();
+
+ try {
+ this.fn.call(ctx, function(err){
+ if (err instanceof Error || toString.call(err) === "[object Error]") return done(err);
+ if (null != err) {
+ if (Object.prototype.toString.call(err) === '[object Object]') {
+ return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err)));
+ } else {
+ return done(new Error('done() invoked with non-Error: ' + err));
+ }
+ }
+ done();
+ });
+ } catch (err) {
+ done(utils.getError(err));
+ }
+ return undefined;
+ }
+
+ if (this.asyncOnly) {
+ return done(new Error('--async-only option in use without declaring `done()`'));
+ }
+
+ // sync or promise-returning
+ try {
+ if (this.pending) {
+ done();
+ } else {
+ callFn(this.fn);
+ }
+ } catch (err) {
+ done(utils.getError(err));
+ }
+
+ function callFn(fn) {
+ var result = fn.call(ctx);
+ if (result && typeof result.then === 'function') {
+ self.resetTimeout();
+ result
+ .then(function() {
+ done()
+ },
+ function(reason) {
+ done(reason || new Error('Promise rejected with no or falsy reason'))
+ });
+ } else {
+ done();
+ }
+ }
+};
+
+}); // module: runnable.js
+
+require.register("runner.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('browser/events').EventEmitter
+ , debug = require('browser/debug')('mocha:runner')
+ , Pending = require('./pending')
+ , Test = require('./test')
+ , utils = require('./utils')
+ , filter = utils.filter
+ , keys = utils.keys
+ , type = utils.type
+ , stringify = utils.stringify
+ , stackFilter = utils.stackTraceFilter();
+
+/**
+ * Non-enumerable globals.
+ */
+
+var globals = [
+ 'setTimeout',
+ 'clearTimeout',
+ 'setInterval',
+ 'clearInterval',
+ 'XMLHttpRequest',
+ 'Date',
+ 'setImmediate',
+ 'clearImmediate'
+];
+
+/**
+ * Expose `Runner`.
+ */
+
+module.exports = Runner;
+
+/**
+ * Initialize a `Runner` for the given `suite`.
+ *
+ * Events:
+ *
+ * - `start` execution started
+ * - `end` execution complete
+ * - `suite` (suite) test suite execution started
+ * - `suite end` (suite) all tests (and sub-suites) have finished
+ * - `test` (test) test execution started
+ * - `test end` (test) test completed
+ * - `hook` (hook) hook execution started
+ * - `hook end` (hook) hook complete
+ * - `pass` (test) test passed
+ * - `fail` (test, err) test failed
+ * - `pending` (test) test pending
+ *
+ * @param {Suite} suite Root suite
+ * @param {boolean} [delay] Whether or not to delay execution of root suite
+ * until ready.
+ * @api public
+ */
+
+function Runner(suite, delay) {
+ var self = this;
+ this._globals = [];
+ this._abort = false;
+ this._delay = delay;
+ this.suite = suite;
+ this.total = suite.total();
+ this.failures = 0;
+ this.on('test end', function(test){ self.checkGlobals(test); });
+ this.on('hook end', function(hook){ self.checkGlobals(hook); });
+ this.grep(/.*/);
+ this.globals(this.globalProps().concat(extraGlobals()));
+}
+
+/**
+ * Wrapper for setImmediate, process.nextTick, or browser polyfill.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.immediately = global.setImmediate || process.nextTick;
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+function F(){}
+F.prototype = EventEmitter.prototype;
+Runner.prototype = new F;
+Runner.prototype.constructor = Runner;
+
+
+/**
+ * Run tests with full titles matching `re`. Updates runner.total
+ * with number of tests matched.
+ *
+ * @param {RegExp} re
+ * @param {Boolean} invert
+ * @return {Runner} for chaining
+ * @api public
+ */
+
+Runner.prototype.grep = function(re, invert){
+ debug('grep %s', re);
+ this._grep = re;
+ this._invert = invert;
+ this.total = this.grepTotal(this.suite);
+ return this;
+};
+
+/**
+ * Returns the number of tests matching the grep search for the
+ * given suite.
+ *
+ * @param {Suite} suite
+ * @return {Number}
+ * @api public
+ */
+
+Runner.prototype.grepTotal = function(suite) {
+ var self = this;
+ var total = 0;
+
+ suite.eachTest(function(test){
+ var match = self._grep.test(test.fullTitle());
+ if (self._invert) match = !match;
+ if (match) total++;
+ });
+
+ return total;
+};
+
+/**
+ * Return a list of global properties.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+Runner.prototype.globalProps = function() {
+ var props = utils.keys(global);
+
+ // non-enumerables
+ for (var i = 0; i < globals.length; ++i) {
+ if (~utils.indexOf(props, globals[i])) continue;
+ props.push(globals[i]);
+ }
+
+ return props;
+};
+
+/**
+ * Allow the given `arr` of globals.
+ *
+ * @param {Array} arr
+ * @return {Runner} for chaining
+ * @api public
+ */
+
+Runner.prototype.globals = function(arr){
+ if (0 == arguments.length) return this._globals;
+ debug('globals %j', arr);
+ this._globals = this._globals.concat(arr);
+ return this;
+};
+
+/**
+ * Check for global variable leaks.
+ *
+ * @api private
+ */
+
+Runner.prototype.checkGlobals = function(test){
+ if (this.ignoreLeaks) return;
+ var ok = this._globals;
+
+ var globals = this.globalProps();
+ var leaks;
+
+ if (test) {
+ ok = ok.concat(test._allowedGlobals || []);
+ }
+
+ if(this.prevGlobalsLength == globals.length) return;
+ this.prevGlobalsLength = globals.length;
+
+ leaks = filterLeaks(ok, globals);
+ this._globals = this._globals.concat(leaks);
+
+ if (leaks.length > 1) {
+ this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + ''));
+ } else if (leaks.length) {
+ this.fail(test, new Error('global leak detected: ' + leaks[0]));
+ }
+};
+
+/**
+ * Fail the given `test`.
+ *
+ * @param {Test} test
+ * @param {Error} err
+ * @api private
+ */
+
+Runner.prototype.fail = function(test, err) {
+ ++this.failures;
+ test.state = 'failed';
+
+ if (!(err instanceof Error)) {
+ err = new Error('the ' + type(err) + ' ' + stringify(err) + ' was thrown, throw an Error :)');
+ }
+
+ err.stack = (this.fullStackTrace || !err.stack)
+ ? err.stack
+ : stackFilter(err.stack);
+
+ this.emit('fail', test, err);
+};
+
+/**
+ * Fail the given `hook` with `err`.
+ *
+ * Hook failures work in the following pattern:
+ * - If bail, then exit
+ * - Failed `before` hook skips all tests in a suite and subsuites,
+ * but jumps to corresponding `after` hook
+ * - Failed `before each` hook skips remaining tests in a
+ * suite and jumps to corresponding `after each` hook,
+ * which is run only once
+ * - Failed `after` hook does not alter
+ * execution order
+ * - Failed `after each` hook skips remaining tests in a
+ * suite and subsuites, but executes other `after each`
+ * hooks
+ *
+ * @param {Hook} hook
+ * @param {Error} err
+ * @api private
+ */
+
+Runner.prototype.failHook = function(hook, err){
+ this.fail(hook, err);
+ if (this.suite.bail()) {
+ this.emit('end');
+ }
+};
+
+/**
+ * Run hook `name` callbacks and then invoke `fn()`.
+ *
+ * @param {String} name
+ * @param {Function} function
+ * @api private
+ */
+
+Runner.prototype.hook = function(name, fn){
+ var suite = this.suite
+ , hooks = suite['_' + name]
+ , self = this
+ , timer;
+
+ function next(i) {
+ var hook = hooks[i];
+ if (!hook) return fn();
+ self.currentRunnable = hook;
+
+ hook.ctx.currentTest = self.test;
+
+ self.emit('hook', hook);
+
+ hook.on('error', function(err){
+ self.failHook(hook, err);
+ });
+
+ hook.run(function(err){
+ hook.removeAllListeners('error');
+ var testError = hook.error();
+ if (testError) self.fail(self.test, testError);
+ if (err) {
+ if (err instanceof Pending) {
+ suite.pending = true;
+ } else {
+ self.failHook(hook, err);
+
+ // stop executing hooks, notify callee of hook err
+ return fn(err);
+ }
+ }
+ self.emit('hook end', hook);
+ delete hook.ctx.currentTest;
+ next(++i);
+ });
+ }
+
+ Runner.immediately(function(){
+ next(0);
+ });
+};
+
+/**
+ * Run hook `name` for the given array of `suites`
+ * in order, and callback `fn(err, errSuite)`.
+ *
+ * @param {String} name
+ * @param {Array} suites
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.hooks = function(name, suites, fn){
+ var self = this
+ , orig = this.suite;
+
+ function next(suite) {
+ self.suite = suite;
+
+ if (!suite) {
+ self.suite = orig;
+ return fn();
+ }
+
+ self.hook(name, function(err){
+ if (err) {
+ var errSuite = self.suite;
+ self.suite = orig;
+ return fn(err, errSuite);
+ }
+
+ next(suites.pop());
+ });
+ }
+
+ next(suites.pop());
+};
+
+/**
+ * Run hooks from the top level down.
+ *
+ * @param {String} name
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.hookUp = function(name, fn){
+ var suites = [this.suite].concat(this.parents()).reverse();
+ this.hooks(name, suites, fn);
+};
+
+/**
+ * Run hooks from the bottom up.
+ *
+ * @param {String} name
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.hookDown = function(name, fn){
+ var suites = [this.suite].concat(this.parents());
+ this.hooks(name, suites, fn);
+};
+
+/**
+ * Return an array of parent Suites from
+ * closest to furthest.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+Runner.prototype.parents = function(){
+ var suite = this.suite
+ , suites = [];
+ while (suite = suite.parent) suites.push(suite);
+ return suites;
+};
+
+/**
+ * Run the current test and callback `fn(err)`.
+ *
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.runTest = function(fn){
+ var test = this.test
+ , self = this;
+
+ if (this.asyncOnly) test.asyncOnly = true;
+
+ try {
+ test.on('error', function(err){
+ self.fail(test, err);
+ });
+ test.run(fn);
+ } catch (err) {
+ fn(err);
+ }
+};
+
+/**
+ * Run tests in the given `suite` and invoke
+ * the callback `fn()` when complete.
+ *
+ * @param {Suite} suite
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.runTests = function(suite, fn){
+ var self = this
+ , tests = suite.tests.slice()
+ , test;
+
+
+ function hookErr(err, errSuite, after) {
+ // before/after Each hook for errSuite failed:
+ var orig = self.suite;
+
+ // for failed 'after each' hook start from errSuite parent,
+ // otherwise start from errSuite itself
+ self.suite = after ? errSuite.parent : errSuite;
+
+ if (self.suite) {
+ // call hookUp afterEach
+ self.hookUp('afterEach', function(err2, errSuite2) {
+ self.suite = orig;
+ // some hooks may fail even now
+ if (err2) return hookErr(err2, errSuite2, true);
+ // report error suite
+ fn(errSuite);
+ });
+ } else {
+ // there is no need calling other 'after each' hooks
+ self.suite = orig;
+ fn(errSuite);
+ }
+ }
+
+ function next(err, errSuite) {
+ // if we bail after first err
+ if (self.failures && suite._bail) return fn();
+
+ if (self._abort) return fn();
+
+ if (err) return hookErr(err, errSuite, true);
+
+ // next test
+ test = tests.shift();
+
+ // all done
+ if (!test) return fn();
+
+ // grep
+ var match = self._grep.test(test.fullTitle());
+ if (self._invert) match = !match;
+ if (!match) return next();
+
+ // pending
+ if (test.pending) {
+ self.emit('pending', test);
+ self.emit('test end', test);
+ return next();
+ }
+
+ // execute test and hook(s)
+ self.emit('test', self.test = test);
+ self.hookDown('beforeEach', function(err, errSuite){
+
+ if (suite.pending) {
+ self.emit('pending', test);
+ self.emit('test end', test);
+ return next();
+ }
+ if (err) return hookErr(err, errSuite, false);
+
+ self.currentRunnable = self.test;
+ self.runTest(function(err){
+ test = self.test;
+
+ if (err) {
+ if (err instanceof Pending) {
+ self.emit('pending', test);
+ } else {
+ self.fail(test, err);
+ }
+ self.emit('test end', test);
+
+ if (err instanceof Pending) {
+ return next();
+ }
+
+ return self.hookUp('afterEach', next);
+ }
+
+ test.state = 'passed';
+ self.emit('pass', test);
+ self.emit('test end', test);
+ self.hookUp('afterEach', next);
+ });
+ });
+ }
+
+ this.next = next;
+ next();
+};
+
+/**
+ * Run the given `suite` and invoke the
+ * callback `fn()` when complete.
+ *
+ * @param {Suite} suite
+ * @param {Function} fn
+ * @api private
+ */
+
+Runner.prototype.runSuite = function(suite, fn){
+ var total = this.grepTotal(suite)
+ , self = this
+ , i = 0;
+
+ debug('run suite %s', suite.fullTitle());
+
+ if (!total) return fn();
+
+ this.emit('suite', this.suite = suite);
+
+ function next(errSuite) {
+ if (errSuite) {
+ // current suite failed on a hook from errSuite
+ if (errSuite == suite) {
+ // if errSuite is current suite
+ // continue to the next sibling suite
+ return done();
+ } else {
+ // errSuite is among the parents of current suite
+ // stop execution of errSuite and all sub-suites
+ return done(errSuite);
+ }
+ }
+
+ if (self._abort) return done();
+
+ var curr = suite.suites[i++];
+ if (!curr) return done();
+ self.runSuite(curr, next);
+ }
+
+ function done(errSuite) {
+ self.suite = suite;
+ self.hook('afterAll', function(){
+ self.emit('suite end', suite);
+ fn(errSuite);
+ });
+ }
+
+ this.hook('beforeAll', function(err){
+ if (err) return done();
+ self.runTests(suite, next);
+ });
+};
+
+/**
+ * Handle uncaught exceptions.
+ *
+ * @param {Error} err
+ * @api private
+ */
+
+Runner.prototype.uncaught = function(err){
+ if (err) {
+ debug('uncaught exception %s', err !== function () {
+ return this;
+ }.call(err) ? err : ( err.message || err ));
+ } else {
+ debug('uncaught undefined exception');
+ err = utils.undefinedError();
+ }
+ err.uncaught = true;
+
+ var runnable = this.currentRunnable;
+ if (!runnable) return;
+
+ runnable.clearTimeout();
+
+ // Ignore errors if complete
+ if (runnable.state) return;
+ this.fail(runnable, err);
+
+ // recover from test
+ if ('test' == runnable.type) {
+ this.emit('test end', runnable);
+ this.hookUp('afterEach', this.next);
+ return;
+ }
+
+ // bail on hooks
+ this.emit('end');
+};
+
+/**
+ * Run the root suite and invoke `fn(failures)`
+ * on completion.
+ *
+ * @param {Function} fn
+ * @return {Runner} for chaining
+ * @api public
+ */
+
+Runner.prototype.run = function(fn){
+ var self = this,
+ rootSuite = this.suite;
+
+ fn = fn || function(){};
+
+ function uncaught(err){
+ self.uncaught(err);
+ }
+
+ function start() {
+ self.emit('start');
+ self.runSuite(rootSuite, function(){
+ debug('finished running');
+ self.emit('end');
+ });
+ }
+
+ debug('start');
+
+ // callback
+ this.on('end', function(){
+ debug('end');
+ process.removeListener('uncaughtException', uncaught);
+ fn(self.failures);
+ });
+
+ // uncaught exception
+ process.on('uncaughtException', uncaught);
+
+ if (this._delay) {
+ // for reporters, I guess.
+ // might be nice to debounce some dots while we wait.
+ this.emit('waiting', rootSuite);
+ rootSuite.once('run', start);
+ }
+ else {
+ start();
+ }
+
+ return this;
+};
+
+/**
+ * Cleanly abort execution
+ *
+ * @return {Runner} for chaining
+ * @api public
+ */
+Runner.prototype.abort = function(){
+ debug('aborting');
+ this._abort = true;
+};
+
+/**
+ * Filter leaks with the given globals flagged as `ok`.
+ *
+ * @param {Array} ok
+ * @param {Array} globals
+ * @return {Array}
+ * @api private
+ */
+
+function filterLeaks(ok, globals) {
+ return filter(globals, function(key){
+ // Firefox and Chrome exposes iframes as index inside the window object
+ if (/^d+/.test(key)) return false;
+
+ // in firefox
+ // if runner runs in an iframe, this iframe's window.getInterface method not init at first
+ // it is assigned in some seconds
+ if (global.navigator && /^getInterface/.test(key)) return false;
+
+ // an iframe could be approached by window[iframeIndex]
+ // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak
+ if (global.navigator && /^\d+/.test(key)) return false;
+
+ // Opera and IE expose global variables for HTML element IDs (issue #243)
+ if (/^mocha-/.test(key)) return false;
+
+ var matched = filter(ok, function(ok){
+ if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]);
+ return key == ok;
+ });
+ return matched.length == 0 && (!global.navigator || 'onerror' !== key);
+ });
+}
+
+/**
+ * Array of globals dependent on the environment.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+function extraGlobals() {
+ if (typeof(process) === 'object' &&
+ typeof(process.version) === 'string') {
+
+ var nodeVersion = process.version.split('.').reduce(function(a, v) {
+ return a << 8 | v;
+ });
+
+ // 'errno' was renamed to process._errno in v0.9.11.
+
+ if (nodeVersion < 0x00090B) {
+ return ['errno'];
+ }
+ }
+
+ return [];
+}
+
+}); // module: runner.js
+
+require.register("suite.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('browser/events').EventEmitter
+ , debug = require('browser/debug')('mocha:suite')
+ , milliseconds = require('./ms')
+ , utils = require('./utils')
+ , Hook = require('./hook');
+
+/**
+ * Expose `Suite`.
+ */
+
+exports = module.exports = Suite;
+
+/**
+ * Create a new `Suite` with the given `title`
+ * and parent `Suite`. When a suite with the
+ * same title is already present, that suite
+ * is returned to provide nicer reporter
+ * and more flexible meta-testing.
+ *
+ * @param {Suite} parent
+ * @param {String} title
+ * @return {Suite}
+ * @api public
+ */
+
+exports.create = function(parent, title){
+ var suite = new Suite(title, parent.ctx);
+ suite.parent = parent;
+ if (parent.pending) suite.pending = true;
+ title = suite.fullTitle();
+ parent.addSuite(suite);
+ return suite;
+};
+
+/**
+ * Initialize a new `Suite` with the given
+ * `title` and `ctx`.
+ *
+ * @param {String} title
+ * @param {Context} ctx
+ * @api private
+ */
+
+function Suite(title, parentContext) {
+ this.title = title;
+ var context = function() {};
+ context.prototype = parentContext;
+ this.ctx = new context();
+ this.suites = [];
+ this.tests = [];
+ this.pending = false;
+ this._beforeEach = [];
+ this._beforeAll = [];
+ this._afterEach = [];
+ this._afterAll = [];
+ this.root = !title;
+ this._timeout = 2000;
+ this._enableTimeouts = true;
+ this._slow = 75;
+ this._bail = false;
+ this.delayed = false;
+}
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+function F(){}
+F.prototype = EventEmitter.prototype;
+Suite.prototype = new F;
+Suite.prototype.constructor = Suite;
+
+
+/**
+ * Return a clone of this `Suite`.
+ *
+ * @return {Suite}
+ * @api private
+ */
+
+Suite.prototype.clone = function(){
+ var suite = new Suite(this.title);
+ debug('clone');
+ suite.ctx = this.ctx;
+ suite.timeout(this.timeout());
+ suite.enableTimeouts(this.enableTimeouts());
+ suite.slow(this.slow());
+ suite.bail(this.bail());
+ return suite;
+};
+
+/**
+ * Set timeout `ms` or short-hand such as "2s".
+ *
+ * @param {Number|String} ms
+ * @return {Suite|Number} for chaining
+ * @api private
+ */
+
+Suite.prototype.timeout = function(ms){
+ if (0 == arguments.length) return this._timeout;
+ if (ms.toString() === '0') this._enableTimeouts = false;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('timeout %d', ms);
+ this._timeout = parseInt(ms, 10);
+ return this;
+};
+
+/**
+ * Set timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Suite|Boolean} self or enabled
+ * @api private
+ */
+
+Suite.prototype.enableTimeouts = function(enabled){
+ if (arguments.length === 0) return this._enableTimeouts;
+ debug('enableTimeouts %s', enabled);
+ this._enableTimeouts = enabled;
+ return this;
+};
+
+/**
+ * Set slow `ms` or short-hand such as "2s".
+ *
+ * @param {Number|String} ms
+ * @return {Suite|Number} for chaining
+ * @api private
+ */
+
+Suite.prototype.slow = function(ms){
+ if (0 === arguments.length) return this._slow;
+ if ('string' == typeof ms) ms = milliseconds(ms);
+ debug('slow %d', ms);
+ this._slow = ms;
+ return this;
+};
+
+/**
+ * Sets whether to bail after first error.
+ *
+ * @param {Boolean} bail
+ * @return {Suite|Number} for chaining
+ * @api private
+ */
+
+Suite.prototype.bail = function(bail){
+ if (0 == arguments.length) return this._bail;
+ debug('bail %s', bail);
+ this._bail = bail;
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` before running tests.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.beforeAll = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"before all" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._beforeAll.push(hook);
+ this.emit('beforeAll', hook);
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` after running tests.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.afterAll = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"after all" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._afterAll.push(hook);
+ this.emit('afterAll', hook);
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` before each test case.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.beforeEach = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"before each" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._beforeEach.push(hook);
+ this.emit('beforeEach', hook);
+ return this;
+};
+
+/**
+ * Run `fn(test[, done])` after each test case.
+ *
+ * @param {Function} fn
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.afterEach = function(title, fn){
+ if (this.pending) return this;
+ if ('function' === typeof title) {
+ fn = title;
+ title = fn.name;
+ }
+ title = '"after each" hook' + (title ? ': ' + title : '');
+
+ var hook = new Hook(title, fn);
+ hook.parent = this;
+ hook.timeout(this.timeout());
+ hook.enableTimeouts(this.enableTimeouts());
+ hook.slow(this.slow());
+ hook.ctx = this.ctx;
+ this._afterEach.push(hook);
+ this.emit('afterEach', hook);
+ return this;
+};
+
+/**
+ * Add a test `suite`.
+ *
+ * @param {Suite} suite
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.addSuite = function(suite){
+ suite.parent = this;
+ suite.timeout(this.timeout());
+ suite.enableTimeouts(this.enableTimeouts());
+ suite.slow(this.slow());
+ suite.bail(this.bail());
+ this.suites.push(suite);
+ this.emit('suite', suite);
+ return this;
+};
+
+/**
+ * Add a `test` to this suite.
+ *
+ * @param {Test} test
+ * @return {Suite} for chaining
+ * @api private
+ */
+
+Suite.prototype.addTest = function(test){
+ test.parent = this;
+ test.timeout(this.timeout());
+ test.enableTimeouts(this.enableTimeouts());
+ test.slow(this.slow());
+ test.ctx = this.ctx;
+ this.tests.push(test);
+ this.emit('test', test);
+ return this;
+};
+
+/**
+ * Return the full title generated by recursively
+ * concatenating the parent's full title.
+ *
+ * @return {String}
+ * @api public
+ */
+
+Suite.prototype.fullTitle = function(){
+ if (this.parent) {
+ var full = this.parent.fullTitle();
+ if (full) return full + ' ' + this.title;
+ }
+ return this.title;
+};
+
+/**
+ * Return the total number of tests.
+ *
+ * @return {Number}
+ * @api public
+ */
+
+Suite.prototype.total = function(){
+ return utils.reduce(this.suites, function(sum, suite){
+ return sum + suite.total();
+ }, 0) + this.tests.length;
+};
+
+/**
+ * Iterates through each suite recursively to find
+ * all tests. Applies a function in the format
+ * `fn(test)`.
+ *
+ * @param {Function} fn
+ * @return {Suite}
+ * @api private
+ */
+
+Suite.prototype.eachTest = function(fn){
+ utils.forEach(this.tests, fn);
+ utils.forEach(this.suites, function(suite){
+ suite.eachTest(fn);
+ });
+ return this;
+};
+
+/**
+ * This will run the root suite if we happen to be running in delayed mode.
+ */
+Suite.prototype.run = function run() {
+ if (this.root) {
+ this.emit('run');
+ }
+};
+
+}); // module: suite.js
+
+require.register("test.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var Runnable = require('./runnable');
+
+/**
+ * Expose `Test`.
+ */
+
+module.exports = Test;
+
+/**
+ * Initialize a new `Test` with the given `title` and callback `fn`.
+ *
+ * @param {String} title
+ * @param {Function} fn
+ * @api private
+ */
+
+function Test(title, fn) {
+ Runnable.call(this, title, fn);
+ this.pending = !fn;
+ this.type = 'test';
+}
+
+/**
+ * Inherit from `Runnable.prototype`.
+ */
+
+function F(){}
+F.prototype = Runnable.prototype;
+Test.prototype = new F;
+Test.prototype.constructor = Test;
+
+
+}); // module: test.js
+
+require.register("utils.js", function(module, exports, require){
+/**
+ * Module dependencies.
+ */
+
+var fs = require('browser/fs')
+ , path = require('browser/path')
+ , basename = path.basename
+ , exists = fs.existsSync || path.existsSync
+ , glob = require('browser/glob')
+ , join = path.join
+ , debug = require('browser/debug')('mocha:watch');
+
+/**
+ * Ignored directories.
+ */
+
+var ignore = ['node_modules', '.git'];
+
+/**
+ * Escape special characters in the given string of html.
+ *
+ * @param {String} html
+ * @return {String}
+ * @api private
+ */
+
+exports.escape = function(html){
+ return String(html)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+};
+
+/**
+ * Array#forEach (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} scope
+ * @api private
+ */
+
+exports.forEach = function(arr, fn, scope){
+ for (var i = 0, l = arr.length; i < l; i++)
+ fn.call(scope, arr[i], i);
+};
+
+/**
+ * Array#map (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} scope
+ * @api private
+ */
+
+exports.map = function(arr, fn, scope){
+ var result = [];
+ for (var i = 0, l = arr.length; i < l; i++)
+ result.push(fn.call(scope, arr[i], i, arr));
+ return result;
+};
+
+/**
+ * Array#indexOf (<=IE8)
+ *
+ * @parma {Array} arr
+ * @param {Object} obj to find index of
+ * @param {Number} start
+ * @api private
+ */
+
+exports.indexOf = function(arr, obj, start){
+ for (var i = start || 0, l = arr.length; i < l; i++) {
+ if (arr[i] === obj)
+ return i;
+ }
+ return -1;
+};
+
+/**
+ * Array#reduce (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @param {Object} initial value
+ * @api private
+ */
+
+exports.reduce = function(arr, fn, val){
+ var rval = val;
+
+ for (var i = 0, l = arr.length; i < l; i++) {
+ rval = fn(rval, arr[i], i, arr);
+ }
+
+ return rval;
+};
+
+/**
+ * Array#filter (<=IE8)
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.filter = function(arr, fn){
+ var ret = [];
+
+ for (var i = 0, l = arr.length; i < l; i++) {
+ var val = arr[i];
+ if (fn(val, i, arr)) ret.push(val);
+ }
+
+ return ret;
+};
+
+/**
+ * Object.keys (<=IE8)
+ *
+ * @param {Object} obj
+ * @return {Array} keys
+ * @api private
+ */
+
+exports.keys = Object.keys || function(obj) {
+ var keys = []
+ , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8
+
+ for (var key in obj) {
+ if (has.call(obj, key)) {
+ keys.push(key);
+ }
+ }
+
+ return keys;
+};
+
+/**
+ * Watch the given `files` for changes
+ * and invoke `fn(file)` on modification.
+ *
+ * @param {Array} files
+ * @param {Function} fn
+ * @api private
+ */
+
+exports.watch = function(files, fn){
+ var options = { interval: 100 };
+ files.forEach(function(file){
+ debug('file %s', file);
+ fs.watchFile(file, options, function(curr, prev){
+ if (prev.mtime < curr.mtime) fn(file);
+ });
+ });
+};
+
+/**
+ * Array.isArray (<=IE8)
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api private
+ */
+var isArray = Array.isArray || function (obj) {
+ return '[object Array]' == {}.toString.call(obj);
+};
+
+/**
+ * @description
+ * Buffer.prototype.toJSON polyfill
+ * @type {Function}
+ */
+if(typeof Buffer !== 'undefined' && Buffer.prototype) {
+ Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () {
+ return Array.prototype.slice.call(this, 0);
+ };
+}
+
+/**
+ * Ignored files.
+ */
+
+function ignored(path){
+ return !~ignore.indexOf(path);
+}
+
+/**
+ * Lookup files in the given `dir`.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+exports.files = function(dir, ext, ret){
+ ret = ret || [];
+ ext = ext || ['js'];
+
+ var re = new RegExp('\\.(' + ext.join('|') + ')$');
+
+ fs.readdirSync(dir)
+ .filter(ignored)
+ .forEach(function(path){
+ path = join(dir, path);
+ if (fs.statSync(path).isDirectory()) {
+ exports.files(path, ext, ret);
+ } else if (path.match(re)) {
+ ret.push(path);
+ }
+ });
+
+ return ret;
+};
+
+/**
+ * Compute a slug from the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+exports.slug = function(str){
+ return str
+ .toLowerCase()
+ .replace(/ +/g, '-')
+ .replace(/[^-\w]/g, '');
+};
+
+/**
+ * Strip the function definition from `str`,
+ * and re-indent for pre whitespace.
+ */
+
+exports.clean = function(str) {
+ str = str
+ .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '')
+ .replace(/^function *\(.*\)\s*{|\(.*\) *=> *{?/, '')
+ .replace(/\s+\}$/, '');
+
+ var spaces = str.match(/^\n?( *)/)[1].length
+ , tabs = str.match(/^\n?(\t*)/)[1].length
+ , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm');
+
+ str = str.replace(re, '');
+
+ return exports.trim(str);
+};
+
+/**
+ * Trim the given `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+exports.trim = function(str){
+ return str.replace(/^\s+|\s+$/g, '');
+};
+
+/**
+ * Parse the given `qs`.
+ *
+ * @param {String} qs
+ * @return {Object}
+ * @api private
+ */
+
+exports.parseQuery = function(qs){
+ return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){
+ var i = pair.indexOf('=')
+ , key = pair.slice(0, i)
+ , val = pair.slice(++i);
+
+ obj[key] = decodeURIComponent(val);
+ return obj;
+ }, {});
+};
+
+/**
+ * Highlight the given string of `js`.
+ *
+ * @param {String} js
+ * @return {String}
+ * @api private
+ */
+
+function highlight(js) {
+ return js
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
+ .replace(/('.*?')/gm, '<span class="string">$1</span>')
+ .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
+ .replace(/(\d+)/gm, '<span class="number">$1</span>')
+ .replace(/\bnew[ \t]+(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
+ .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '<span class="keyword">$1</span>')
+}
+
+/**
+ * Highlight the contents of tag `name`.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+exports.highlightTags = function(name) {
+ var code = document.getElementById('mocha').getElementsByTagName(name);
+ for (var i = 0, len = code.length; i < len; ++i) {
+ code[i].innerHTML = highlight(code[i].innerHTML);
+ }
+};
+
+/**
+ * If a value could have properties, and has none, this function is called, which returns
+ * a string representation of the empty value.
+ *
+ * Functions w/ no properties return `'[Function]'`
+ * Arrays w/ length === 0 return `'[]'`
+ * Objects w/ no properties return `'{}'`
+ * All else: return result of `value.toString()`
+ *
+ * @param {*} value Value to inspect
+ * @param {string} [type] The type of the value, if known.
+ * @returns {string}
+ */
+var emptyRepresentation = function emptyRepresentation(value, type) {
+ type = type || exports.type(value);
+
+ switch(type) {
+ case 'function':
+ return '[Function]';
+ case 'object':
+ return '{}';
+ case 'array':
+ return '[]';
+ default:
+ return value.toString();
+ }
+};
+
+/**
+ * Takes some variable and asks `{}.toString()` what it thinks it is.
+ * @param {*} value Anything
+ * @example
+ * type({}) // 'object'
+ * type([]) // 'array'
+ * type(1) // 'number'
+ * type(false) // 'boolean'
+ * type(Infinity) // 'number'
+ * type(null) // 'null'
+ * type(new Date()) // 'date'
+ * type(/foo/) // 'regexp'
+ * type('type') // 'string'
+ * type(global) // 'global'
+ * @api private
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
+ * @returns {string}
+ */
+exports.type = function type(value) {
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
+ return 'buffer';
+ }
+ return Object.prototype.toString.call(value)
+ .replace(/^\[.+\s(.+?)\]$/, '$1')
+ .toLowerCase();
+};
+
+/**
+ * @summary Stringify `value`.
+ * @description Different behavior depending on type of value.
+ * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively.
+ * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes.
+ * - If `value` is an *empty* object, function, or array, return result of function
+ * {@link emptyRepresentation}.
+ * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of
+ * JSON.stringify().
+ *
+ * @see exports.type
+ * @param {*} value
+ * @return {string}
+ * @api private
+ */
+
+exports.stringify = function(value) {
+ var type = exports.type(value);
+
+ if (!~exports.indexOf(['object', 'array', 'function'], type)) {
+ if(type != 'buffer') {
+ return jsonStringify(value);
+ }
+ var json = value.toJSON();
+ // Based on the toJSON result
+ return jsonStringify(json.data && json.type ? json.data : json, 2)
+ .replace(/,(\n|$)/g, '$1');
+ }
+
+ for (var prop in value) {
+ if (Object.prototype.hasOwnProperty.call(value, prop)) {
+ return jsonStringify(exports.canonicalize(value), 2).replace(/,(\n|$)/g, '$1');
+ }
+ }
+
+ return emptyRepresentation(value, type);
+};
+
+/**
+ * @description
+ * like JSON.stringify but more sense.
+ * @param {Object} object
+ * @param {Number=} spaces
+ * @param {number=} depth
+ * @returns {*}
+ * @private
+ */
+function jsonStringify(object, spaces, depth) {
+ if(typeof spaces == 'undefined') return _stringify(object); // primitive types
+
+ depth = depth || 1;
+ var space = spaces * depth
+ , str = isArray(object) ? '[' : '{'
+ , end = isArray(object) ? ']' : '}'
+ , length = object.length || exports.keys(object).length
+ , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill
+
+ function _stringify(val) {
+ switch (exports.type(val)) {
+ case 'null':
+ case 'undefined':
+ val = '[' + val + ']';
+ break;
+ case 'array':
+ case 'object':
+ val = jsonStringify(val, spaces, depth + 1);
+ break;
+ case 'boolean':
+ case 'regexp':
+ case 'number':
+ val = val === 0 && (1/val) === -Infinity // `-0`
+ ? '-0'
+ : val.toString();
+ break;
+ case 'date':
+ val = '[Date: ' + val.toISOString() + ']';
+ break;
+ case 'buffer':
+ var json = val.toJSON();
+ // Based on the toJSON result
+ json = json.data && json.type ? json.data : json;
+ val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']';
+ break;
+ default:
+ val = (val == '[Function]' || val == '[Circular]')
+ ? val
+ : '"' + val + '"'; //string
+ }
+ return val;
+ }
+
+ for(var i in object) {
+ if(!object.hasOwnProperty(i)) continue; // not my business
+ --length;
+ str += '\n ' + repeat(' ', space)
+ + (isArray(object) ? '' : '"' + i + '": ') // key
+ + _stringify(object[i]) // value
+ + (length ? ',' : ''); // comma
+ }
+
+ return str + (str.length != 1 // [], {}
+ ? '\n' + repeat(' ', --space) + end
+ : end);
+}
+
+/**
+ * Return if obj is a Buffer
+ * @param {Object} arg
+ * @return {Boolean}
+ * @api private
+ */
+exports.isBuffer = function (arg) {
+ return typeof Buffer !== 'undefined' && Buffer.isBuffer(arg);
+};
+
+/**
+ * @summary Return a new Thing that has the keys in sorted order. Recursive.
+ * @description If the Thing...
+ * - has already been seen, return string `'[Circular]'`
+ * - is `undefined`, return string `'[undefined]'`
+ * - is `null`, return value `null`
+ * - is some other primitive, return the value
+ * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method
+ * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again.
+ * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()`
+ *
+ * @param {*} value Thing to inspect. May or may not have properties.
+ * @param {Array} [stack=[]] Stack of seen values
+ * @return {(Object|Array|Function|string|undefined)}
+ * @see {@link exports.stringify}
+ * @api private
+ */
+
+exports.canonicalize = function(value, stack) {
+ var canonicalizedObj,
+ type = exports.type(value),
+ prop,
+ withStack = function withStack(value, fn) {
+ stack.push(value);
+ fn();
+ stack.pop();
+ };
+
+ stack = stack || [];
+
+ if (exports.indexOf(stack, value) !== -1) {
+ return '[Circular]';
+ }
+
+ switch(type) {
+ case 'undefined':
+ case 'buffer':
+ case 'null':
+ canonicalizedObj = value;
+ break;
+ case 'array':
+ withStack(value, function () {
+ canonicalizedObj = exports.map(value, function (item) {
+ return exports.canonicalize(item, stack);
+ });
+ });
+ break;
+ case 'function':
+ for (prop in value) {
+ canonicalizedObj = {};
+ break;
+ }
+ if (!canonicalizedObj) {
+ canonicalizedObj = emptyRepresentation(value, type);
+ break;
+ }
+ /* falls through */
+ case 'object':
+ canonicalizedObj = canonicalizedObj || {};
+ withStack(value, function () {
+ exports.forEach(exports.keys(value).sort(), function (key) {
+ canonicalizedObj[key] = exports.canonicalize(value[key], stack);
+ });
+ });
+ break;
+ case 'date':
+ case 'number':
+ case 'regexp':
+ case 'boolean':
+ canonicalizedObj = value;
+ break;
+ default:
+ canonicalizedObj = value.toString();
+ }
+
+ return canonicalizedObj;
+};
+
+/**
+ * Lookup file names at the given `path`.
+ */
+exports.lookupFiles = function lookupFiles(path, extensions, recursive) {
+ var files = [];
+ var re = new RegExp('\\.(' + extensions.join('|') + ')$');
+
+ if (!exists(path)) {
+ if (exists(path + '.js')) {
+ path += '.js';
+ } else {
+ files = glob.sync(path);
+ if (!files.length) throw new Error("cannot resolve path (or pattern) '" + path + "'");
+ return files;
+ }
+ }
+
+ try {
+ var stat = fs.statSync(path);
+ if (stat.isFile()) return path;
+ }
+ catch (ignored) {
+ return undefined;
+ }
+
+ fs.readdirSync(path).forEach(function(file) {
+ file = join(path, file);
+ try {
+ var stat = fs.statSync(file);
+ if (stat.isDirectory()) {
+ if (recursive) {
+ files = files.concat(lookupFiles(file, extensions, recursive));
+ }
+ return;
+ }
+ }
+ catch (ignored) {
+ return;
+ }
+ if (!stat.isFile() || !re.test(file) || basename(file)[0] === '.') return;
+ files.push(file);
+ });
+
+ return files;
+};
+
+/**
+ * Generate an undefined error with a message warning the user.
+ *
+ * @return {Error}
+ */
+
+exports.undefinedError = function() {
+ return new Error('Caught undefined error, did you throw without specifying what?');
+};
+
+/**
+ * Generate an undefined error if `err` is not defined.
+ *
+ * @param {Error} err
+ * @return {Error}
+ */
+
+exports.getError = function(err) {
+ return err || exports.undefinedError();
+};
+
+
+/**
+ * @summary
+ * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`)
+ * @description
+ * When invoking this function you get a filter function that get the Error.stack as an input,
+ * and return a prettify output.
+ * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace).
+ * @returns {Function}
+ */
+
+exports.stackTraceFilter = function() {
+ var slash = '/'
+ , is = typeof document === 'undefined'
+ ? { node: true }
+ : { browser: true }
+ , cwd = is.node
+ ? process.cwd() + slash
+ : location.href.replace(/\/[^\/]*$/, '/');
+
+ function isNodeModule (line) {
+ return (~line.indexOf('node_modules'));
+ }
+
+ function isMochaInternal (line) {
+ return (~line.indexOf('node_modules' + slash + 'mocha')) ||
+ (~line.indexOf('components' + slash + 'mochajs')) ||
+ (~line.indexOf('components' + slash + 'mocha'));
+ }
+
+ // node_modules, bower, componentJS
+ function isBrowserModule(line) {
+ return (~line.indexOf('node_modules')) ||
+ (~line.indexOf('components'));
+ }
+
+ function isNodeInternal (line) {
+ return (~line.indexOf('(timers.js:')) ||
+ (~line.indexOf('(events.js:')) ||
+ (~line.indexOf('(node.js:')) ||
+ (~line.indexOf('(module.js:')) ||
+ (~line.indexOf('GeneratorFunctionPrototype.next (native)')) ||
+ false
+ }
+
+ return function(stack) {
+ stack = stack.split('\n');
+
+ stack = exports.reduce(stack, function(list, line) {
+ if (is.node && (isNodeModule(line) ||
+ isMochaInternal(line) ||
+ isNodeInternal(line)))
+ return list;
+
+ if (is.browser && (isBrowserModule(line)))
+ return list;
+
+ // Clean up cwd(absolute)
+ list.push(line.replace(cwd, ''));
+ return list;
+ }, []);
+
+ return stack.join('\n');
+ }
+};
+}); // module: utils.js
+// The global object is "self" in Web Workers.
+var global = (function() { return this; })();
+
+/**
+ * Save timer references to avoid Sinon interfering (see GH-237).
+ */
+
+var Date = global.Date;
+var setTimeout = global.setTimeout;
+var setInterval = global.setInterval;
+var clearTimeout = global.clearTimeout;
+var clearInterval = global.clearInterval;
+
+/**
+ * Node shims.
+ *
+ * These are meant only to allow
+ * mocha.js to run untouched, not
+ * to allow running node code in
+ * the browser.
+ */
+
+var process = {};
+process.exit = function(status){};
+process.stdout = {};
+
+var uncaughtExceptionHandlers = [];
+
+var originalOnerrorHandler = global.onerror;
+
+/**
+ * Remove uncaughtException listener.
+ * Revert to original onerror handler if previously defined.
+ */
+
+process.removeListener = function(e, fn){
+ if ('uncaughtException' == e) {
+ if (originalOnerrorHandler) {
+ global.onerror = originalOnerrorHandler;
+ } else {
+ global.onerror = function() {};
+ }
+ var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn);
+ if (i != -1) { uncaughtExceptionHandlers.splice(i, 1); }
+ }
+};
+
+/**
+ * Implements uncaughtException listener.
+ */
+
+process.on = function(e, fn){
+ if ('uncaughtException' == e) {
+ global.onerror = function(err, url, line){
+ fn(new Error(err + ' (' + url + ':' + line + ')'));
+ return true;
+ };
+ uncaughtExceptionHandlers.push(fn);
+ }
+};
+
+/**
+ * Expose mocha.
+ */
+
+var Mocha = global.Mocha = require('mocha'),
+ mocha = global.mocha = new Mocha({ reporter: 'html' });
+
+// The BDD UI is registered by default, but no UI will be functional in the
+// browser without an explicit call to the overridden `mocha.ui` (see below).
+// Ensure that this default UI does not expose its methods to the global scope.
+mocha.suite.removeAllListeners('pre-require');
+
+var immediateQueue = []
+ , immediateTimeout;
+
+function timeslice() {
+ var immediateStart = new Date().getTime();
+ while (immediateQueue.length && (new Date().getTime() - immediateStart) < 100) {
+ immediateQueue.shift()();
+ }
+ if (immediateQueue.length) {
+ immediateTimeout = setTimeout(timeslice, 0);
+ } else {
+ immediateTimeout = null;
+ }
+}
+
+/**
+ * High-performance override of Runner.immediately.
+ */
+
+Mocha.Runner.immediately = function(callback) {
+ immediateQueue.push(callback);
+ if (!immediateTimeout) {
+ immediateTimeout = setTimeout(timeslice, 0);
+ }
+};
+
+/**
+ * Function to allow assertion libraries to throw errors directly into mocha.
+ * This is useful when running tests in a browser because window.onerror will
+ * only receive the 'message' attribute of the Error.
+ */
+mocha.throwError = function(err) {
+ Mocha.utils.forEach(uncaughtExceptionHandlers, function (fn) {
+ fn(err);
+ });
+ throw err;
+};
+
+/**
+ * Override ui to ensure that the ui functions are initialized.
+ * Normally this would happen in Mocha.prototype.loadFiles.
+ */
+
+mocha.ui = function(ui){
+ Mocha.prototype.ui.call(this, ui);
+ this.suite.emit('pre-require', global, null, this);
+ return this;
+};
+
+/**
+ * Setup mocha with the given setting options.
+ */
+
+mocha.setup = function(opts){
+ if ('string' == typeof opts) opts = { ui: opts };
+ for (var opt in opts) this[opt](opts[opt]);
+ return this;
+};
+
+/**
+ * Run mocha, returning the Runner.
+ */
+
+mocha.run = function(fn){
+ var options = mocha.options;
+ mocha.globals('location');
+
+ var query = Mocha.utils.parseQuery(global.location.search || '');
+ if (query.grep) mocha.grep(new RegExp(query.grep));
+ if (query.fgrep) mocha.grep(query.fgrep);
+ if (query.invert) mocha.invert();
+
+ return Mocha.prototype.run.call(mocha, function(err){
+ // The DOM Document is not available in Web Workers.
+ var document = global.document;
+ if (document && document.getElementById('mocha') && options.noHighlighting !== true) {
+ Mocha.utils.highlightTags('code');
+ }
+ if (fn) fn(err);
+ });
+};
+
+/**
+ * Expose the process shim.
+ */
+
+Mocha.process = process;
+})();
diff --git a/toolkit/components/microformats/test/static/javascript/parse.js b/toolkit/components/microformats/test/static/javascript/parse.js
new file mode 100644
index 0000000000..588e403eef
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/parse.js
@@ -0,0 +1,133 @@
+/*!
+ parse
+ Used by http://localhost:3000/
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+window.onload = function() {
+
+ var form;
+ form= document.getElementById('mf-form');
+
+ form.onsubmit = function(e){
+ e = (e)? e : window.event;
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+
+
+ var html,
+ baseUrl,
+ filter,
+ collapsewhitespace,
+ overlappingversions,
+ impliedPropertiesByVersion,
+ dateformatElt,
+ dateformat,
+ doc,
+ node,
+ options,
+ mfJSON,
+ parserJSONElt;
+
+ // get data from html
+ html = document.getElementById('html').value;
+ baseUrl = document.getElementById('baseurl').value;
+ filters = document.getElementById('filters').value;
+ collapsewhitespace = document.getElementById('collapsewhitespace').checked;
+ //overlappingversions = document.getElementById('overlappingversions').checked;
+ //impliedPropertiesByVersion = document.getElementById('impliedPropertiesByVersion').checked;
+ parseLatLonGeo = document.getElementById('parseLatLonGeo').checked;
+ dateformatElt = document.getElementById("dateformat");
+ dateformat = dateformatElt.options[dateformatElt.selectedIndex].value;
+ parserJSONElt = document.querySelector('#parser-json pre code')
+
+
+ var dom = new DOMParser();
+ doc = dom.parseFromString( html, 'text/html' );
+
+ options ={
+ 'document': doc,
+ 'node': doc,
+ 'dateFormat': dateformat,
+ 'parseLatLonGeo': false
+ };
+
+ if(baseUrl.trim() !== ''){
+ options.baseUrl = baseUrl;
+ }
+
+ if(filters.trim() !== ''){
+ if(filters.indexOf(',') > -1){
+ options.filters = trimArrayItems(filters.split(','));
+ }else{
+ options.filters = [filters.trim()];
+ }
+ }
+
+ if(collapsewhitespace === true){
+ options.textFormat = 'normalised';
+ }
+
+ /*
+ if(overlappingversions === true){
+ options.overlappingVersions = false;
+ }
+
+ if(impliedPropertiesByVersion === true){
+ options.impliedPropertiesByVersion = true;
+ }
+ */
+
+ if(parseLatLonGeo === true){
+ options.parseLatLonGeo = true
+ }
+
+ if(options.baseUrl){
+ html = '<base href="' + baseUrl+ '">' + html;
+ }
+
+
+
+ // parse direct into Modules to help debugging
+ if(window.Modules){
+ var parser = new Modules.Parser();
+ mfJSON = parser.get(options);
+ }else if(window.Microformats){
+ mfJSON = Microformats.get(options);
+ }
+
+
+ // format output
+ parserJSONElt.innerHTML = htmlEscape( js_beautify( JSON.stringify(mfJSON) ) );
+ //prettyPrint();
+
+ }
+
+
+};
+
+
+
+
+
+function htmlEscape(str) {
+ return String(str)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+}
+
+
+function trimArrayItems( arr ){
+ return arr.map(function(item){
+ return item.trim();
+ })
+}
+
diff --git a/toolkit/components/microformats/test/static/javascript/prettify.js b/toolkit/components/microformats/test/static/javascript/prettify.js
new file mode 100644
index 0000000000..879dfd60fc
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/prettify.js
@@ -0,0 +1,1479 @@
+// Copyright (C) 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview
+ * some functions for browser-side pretty printing of code contained in html.
+ *
+ * <p>
+ * For a fairly comprehensive set of languages see the
+ * <a href="http://google-code-prettify.googlecode.com/svn/trunk/README.html#langs">README</a>
+ * file that came with this source. At a minimum, the lexer should work on a
+ * number of languages including C and friends, Java, Python, Bash, SQL, HTML,
+ * XML, CSS, Javascript, and Makefiles. It works passably on Ruby, PHP and Awk
+ * and a subset of Perl, but, because of commenting conventions, doesn't work on
+ * Smalltalk, Lisp-like, or CAML-like languages without an explicit lang class.
+ * <p>
+ * Usage: <ol>
+ * <li> include this source file in an html page via
+ * {@code <script type="text/javascript" src="/path/to/prettify.js"></script>}
+ * <li> define style rules. See the example page for examples.
+ * <li> mark the {@code <pre>} and {@code <code>} tags in your source with
+ * {@code class=prettyprint.}
+ * You can also use the (html deprecated) {@code <xmp>} tag, but the pretty
+ * printer needs to do more substantial DOM manipulations to support that, so
+ * some css styles may not be preserved.
+ * </ol>
+ * That's it. I wanted to keep the API as simple as possible, so there's no
+ * need to specify which language the code is in, but if you wish, you can add
+ * another class to the {@code <pre>} or {@code <code>} element to specify the
+ * language, as in {@code <pre class="prettyprint lang-java">}. Any class that
+ * starts with "lang-" followed by a file extension, specifies the file type.
+ * See the "lang-*.js" files in this directory for code that implements
+ * per-language file handlers.
+ * <p>
+ * Change log:<br>
+ * cbeust, 2006/08/22
+ * <blockquote>
+ * Java annotations (start with "@") are now captured as literals ("lit")
+ * </blockquote>
+ * @requires console
+ */
+
+// JSLint declarations
+/*global console, document, navigator, setTimeout, window */
+
+/**
+ * Split {@code prettyPrint} into multiple timeouts so as not to interfere with
+ * UI events.
+ * If set to {@code false}, {@code prettyPrint()} is synchronous.
+ */
+window['PR_SHOULD_USE_CONTINUATION'] = true;
+
+(function () {
+ // Keyword lists for various languages.
+ // We use things that coerce to strings to make them compact when minified
+ // and to defeat aggressive optimizers that fold large string constants.
+ var FLOW_CONTROL_KEYWORDS = ["break,continue,do,else,for,if,return,while"];
+ var C_KEYWORDS = [FLOW_CONTROL_KEYWORDS,"auto,case,char,const,default," +
+ "double,enum,extern,float,goto,int,long,register,short,signed,sizeof," +
+ "static,struct,switch,typedef,union,unsigned,void,volatile"];
+ var COMMON_KEYWORDS = [C_KEYWORDS,"catch,class,delete,false,import," +
+ "new,operator,private,protected,public,this,throw,true,try,typeof"];
+ var CPP_KEYWORDS = [COMMON_KEYWORDS,"alignof,align_union,asm,axiom,bool," +
+ "concept,concept_map,const_cast,constexpr,decltype," +
+ "dynamic_cast,explicit,export,friend,inline,late_check," +
+ "mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast," +
+ "template,typeid,typename,using,virtual,where"];
+ var JAVA_KEYWORDS = [COMMON_KEYWORDS,
+ "abstract,boolean,byte,extends,final,finally,implements,import," +
+ "instanceof,null,native,package,strictfp,super,synchronized,throws," +
+ "transient"];
+ var CSHARP_KEYWORDS = [JAVA_KEYWORDS,
+ "as,base,by,checked,decimal,delegate,descending,dynamic,event," +
+ "fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock," +
+ "object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed," +
+ "stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];
+ var COFFEE_KEYWORDS = "all,and,by,catch,class,else,extends,false,finally," +
+ "for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then," +
+ "true,try,unless,until,when,while,yes";
+ var JSCRIPT_KEYWORDS = [COMMON_KEYWORDS,
+ "debugger,eval,export,function,get,null,set,undefined,var,with," +
+ "Infinity,NaN"];
+ var PERL_KEYWORDS = "caller,delete,die,do,dump,elsif,eval,exit,foreach,for," +
+ "goto,if,import,last,local,my,next,no,our,print,package,redo,require," +
+ "sub,undef,unless,until,use,wantarray,while,BEGIN,END";
+ var PYTHON_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "and,as,assert,class,def,del," +
+ "elif,except,exec,finally,from,global,import,in,is,lambda," +
+ "nonlocal,not,or,pass,print,raise,try,with,yield," +
+ "False,True,None"];
+ var RUBY_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "alias,and,begin,case,class," +
+ "def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo," +
+ "rescue,retry,self,super,then,true,undef,unless,until,when,yield," +
+ "BEGIN,END"];
+ var SH_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "case,done,elif,esac,eval,fi," +
+ "function,in,local,set,then,until"];
+ var ALL_KEYWORDS = [
+ CPP_KEYWORDS, CSHARP_KEYWORDS, JSCRIPT_KEYWORDS, PERL_KEYWORDS +
+ PYTHON_KEYWORDS, RUBY_KEYWORDS, SH_KEYWORDS];
+ var C_TYPES = /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;
+
+ // token style names. correspond to css classes
+ /**
+ * token style for a string literal
+ * @const
+ */
+ var PR_STRING = 'str';
+ /**
+ * token style for a keyword
+ * @const
+ */
+ var PR_KEYWORD = 'kwd';
+ /**
+ * token style for a comment
+ * @const
+ */
+ var PR_COMMENT = 'com';
+ /**
+ * token style for a type
+ * @const
+ */
+ var PR_TYPE = 'typ';
+ /**
+ * token style for a literal value. e.g. 1, null, true.
+ * @const
+ */
+ var PR_LITERAL = 'lit';
+ /**
+ * token style for a punctuation string.
+ * @const
+ */
+ var PR_PUNCTUATION = 'pun';
+ /**
+ * token style for a punctuation string.
+ * @const
+ */
+ var PR_PLAIN = 'pln';
+
+ /**
+ * token style for an sgml tag.
+ * @const
+ */
+ var PR_TAG = 'tag';
+ /**
+ * token style for a markup declaration such as a DOCTYPE.
+ * @const
+ */
+ var PR_DECLARATION = 'dec';
+ /**
+ * token style for embedded source.
+ * @const
+ */
+ var PR_SOURCE = 'src';
+ /**
+ * token style for an sgml attribute name.
+ * @const
+ */
+ var PR_ATTRIB_NAME = 'atn';
+ /**
+ * token style for an sgml attribute value.
+ * @const
+ */
+ var PR_ATTRIB_VALUE = 'atv';
+
+ /**
+ * A class that indicates a section of markup that is not code, e.g. to allow
+ * embedding of line numbers within code listings.
+ * @const
+ */
+ var PR_NOCODE = 'nocode';
+
+
+
+/**
+ * A set of tokens that can precede a regular expression literal in
+ * javascript
+ * http://web.archive.org/web/20070717142515/http://www.mozilla.org/js/language/js20/rationale/syntax.html
+ * has the full list, but I've removed ones that might be problematic when
+ * seen in languages that don't support regular expression literals.
+ *
+ * <p>Specifically, I've removed any keywords that can't precede a regexp
+ * literal in a syntactically legal javascript program, and I've removed the
+ * "in" keyword since it's not a keyword in many languages, and might be used
+ * as a count of inches.
+ *
+ * <p>The link a above does not accurately describe EcmaScript rules since
+ * it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
+ * very well in practice.
+ *
+ * @private
+ * @const
+ */
+var REGEXP_PRECEDER_PATTERN = '(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*';
+
+// CAVEAT: this does not properly handle the case where a regular
+// expression immediately follows another since a regular expression may
+// have flags for case-sensitivity and the like. Having regexp tokens
+// adjacent is not valid in any language I'm aware of, so I'm punting.
+// TODO: maybe style special characters inside a regexp as punctuation.
+
+
+ /**
+ * Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
+ * matches the union of the sets of strings matched by the input RegExp.
+ * Since it matches globally, if the input strings have a start-of-input
+ * anchor (/^.../), it is ignored for the purposes of unioning.
+ * @param {Array.<RegExp>} regexs non multiline, non-global regexs.
+ * @return {RegExp} a global regex.
+ */
+ function combinePrefixPatterns(regexs) {
+ var capturedGroupIndex = 0;
+
+ var needToFoldCase = false;
+ var ignoreCase = false;
+ for (var i = 0, n = regexs.length; i < n; ++i) {
+ var regex = regexs[i];
+ if (regex.ignoreCase) {
+ ignoreCase = true;
+ } else if (/[a-z]/i.test(regex.source.replace(
+ /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
+ needToFoldCase = true;
+ ignoreCase = false;
+ break;
+ }
+ }
+
+ var escapeCharToCodeUnit = {
+ 'b': 8,
+ 't': 9,
+ 'n': 0xa,
+ 'v': 0xb,
+ 'f': 0xc,
+ 'r': 0xd
+ };
+
+ function decodeEscape(charsetPart) {
+ var cc0 = charsetPart.charCodeAt(0);
+ if (cc0 !== 92 /* \\ */) {
+ return cc0;
+ }
+ var c1 = charsetPart.charAt(1);
+ cc0 = escapeCharToCodeUnit[c1];
+ if (cc0) {
+ return cc0;
+ } else if ('0' <= c1 && c1 <= '7') {
+ return parseInt(charsetPart.substring(1), 8);
+ } else if (c1 === 'u' || c1 === 'x') {
+ return parseInt(charsetPart.substring(2), 16);
+ } else {
+ return charsetPart.charCodeAt(1);
+ }
+ }
+
+ function encodeEscape(charCode) {
+ if (charCode < 0x20) {
+ return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
+ }
+ var ch = String.fromCharCode(charCode);
+ if (ch === '\\' || ch === '-' || ch === '[' || ch === ']') {
+ ch = '\\' + ch;
+ }
+ return ch;
+ }
+
+ function caseFoldCharset(charSet) {
+ var charsetParts = charSet.substring(1, charSet.length - 1).match(
+ new RegExp(
+ '\\\\u[0-9A-Fa-f]{4}'
+ + '|\\\\x[0-9A-Fa-f]{2}'
+ + '|\\\\[0-3][0-7]{0,2}'
+ + '|\\\\[0-7]{1,2}'
+ + '|\\\\[\\s\\S]'
+ + '|-'
+ + '|[^-\\\\]',
+ 'g'));
+ var groups = [];
+ var ranges = [];
+ var inverse = charsetParts[0] === '^';
+ for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
+ var p = charsetParts[i];
+ if (/\\[bdsw]/i.test(p)) { // Don't muck with named groups.
+ groups.push(p);
+ } else {
+ var start = decodeEscape(p);
+ var end;
+ if (i + 2 < n && '-' === charsetParts[i + 1]) {
+ end = decodeEscape(charsetParts[i + 2]);
+ i += 2;
+ } else {
+ end = start;
+ }
+ ranges.push([start, end]);
+ // If the range might intersect letters, then expand it.
+ // This case handling is too simplistic.
+ // It does not deal with non-latin case folding.
+ // It works for latin source code identifiers though.
+ if (!(end < 65 || start > 122)) {
+ if (!(end < 65 || start > 90)) {
+ ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
+ }
+ if (!(end < 97 || start > 122)) {
+ ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
+ }
+ }
+ }
+ }
+
+ // [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
+ // -> [[1, 12], [14, 14], [16, 17]]
+ ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1] - a[1]); });
+ var consolidatedRanges = [];
+ var lastRange = [NaN, NaN];
+ for (i = 0; i < ranges.length; ++i) {
+ var range = ranges[i];
+ if (range[0] <= lastRange[1] + 1) {
+ lastRange[1] = Math.max(lastRange[1], range[1]);
+ } else {
+ consolidatedRanges.push(lastRange = range);
+ }
+ }
+
+ var out = ['['];
+ if (inverse) { out.push('^'); }
+ out.push.apply(out, groups);
+ for (i = 0; i < consolidatedRanges.length; ++i) {
+ range = consolidatedRanges[i];
+ out.push(encodeEscape(range[0]));
+ if (range[1] > range[0]) {
+ if (range[1] + 1 > range[0]) { out.push('-'); }
+ out.push(encodeEscape(range[1]));
+ }
+ }
+ out.push(']');
+ return out.join('');
+ }
+
+ function allowAnywhereFoldCaseAndRenumberGroups(regex) {
+ // Split into character sets, escape sequences, punctuation strings
+ // like ('(', '(?:', ')', '^'), and runs of characters that do not
+ // include any of the above.
+ var parts = regex.source.match(
+ new RegExp(
+ '(?:'
+ + '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]' // a character set
+ + '|\\\\u[A-Fa-f0-9]{4}' // a unicode escape
+ + '|\\\\x[A-Fa-f0-9]{2}' // a hex escape
+ + '|\\\\[0-9]+' // a back-reference or octal escape
+ + '|\\\\[^ux0-9]' // other escape sequence
+ + '|\\(\\?[:!=]' // start of a non-capturing group
+ + '|[\\(\\)\\^]' // start/emd of a group, or line start
+ + '|[^\\x5B\\x5C\\(\\)\\^]+' // run of other characters
+ + ')',
+ 'g'));
+ var n = parts.length;
+
+ // Maps captured group numbers to the number they will occupy in
+ // the output or to -1 if that has not been determined, or to
+ // undefined if they need not be capturing in the output.
+ var capturedGroups = [];
+
+ // Walk over and identify back references to build the capturedGroups
+ // mapping.
+ for (var i = 0, groupIndex = 0; i < n; ++i) {
+ var p = parts[i];
+ if (p === '(') {
+ // groups are 1-indexed, so max group index is count of '('
+ ++groupIndex;
+ } else if ('\\' === p.charAt(0)) {
+ var decimalValue = +p.substring(1);
+ if (decimalValue && decimalValue <= groupIndex) {
+ capturedGroups[decimalValue] = -1;
+ }
+ }
+ }
+
+ // Renumber groups and reduce capturing groups to non-capturing groups
+ // where possible.
+ for (i = 1; i < capturedGroups.length; ++i) {
+ if (-1 === capturedGroups[i]) {
+ capturedGroups[i] = ++capturedGroupIndex;
+ }
+ }
+ for (i = 0, groupIndex = 0; i < n; ++i) {
+ p = parts[i];
+ if (p === '(') {
+ ++groupIndex;
+ if (capturedGroups[groupIndex] === undefined) {
+ parts[i] = '(?:';
+ }
+ } else if ('\\' === p.charAt(0)) {
+ decimalValue = +p.substring(1);
+ if (decimalValue && decimalValue <= groupIndex) {
+ parts[i] = '\\' + capturedGroups[groupIndex];
+ }
+ }
+ }
+
+ // Remove any prefix anchors so that the output will match anywhere.
+ // ^^ really does mean an anchored match though.
+ for (i = 0, groupIndex = 0; i < n; ++i) {
+ if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
+ }
+
+ // Expand letters to groups to handle mixing of case-sensitive and
+ // case-insensitive patterns if necessary.
+ if (regex.ignoreCase && needToFoldCase) {
+ for (i = 0; i < n; ++i) {
+ p = parts[i];
+ var ch0 = p.charAt(0);
+ if (p.length >= 2 && ch0 === '[') {
+ parts[i] = caseFoldCharset(p);
+ } else if (ch0 !== '\\') {
+ // TODO: handle letters in numeric escapes.
+ parts[i] = p.replace(
+ /[a-zA-Z]/g,
+ function (ch) {
+ var cc = ch.charCodeAt(0);
+ return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
+ });
+ }
+ }
+ }
+
+ return parts.join('');
+ }
+
+ var rewritten = [];
+ for (i = 0, n = regexs.length; i < n; ++i) {
+ regex = regexs[i];
+ if (regex.global || regex.multiline) { throw new Error('' + regex); }
+ rewritten.push(
+ '(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
+ }
+
+ return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
+ }
+
+
+ /**
+ * Split markup into a string of source code and an array mapping ranges in
+ * that string to the text nodes in which they appear.
+ *
+ * <p>
+ * The HTML DOM structure:</p>
+ * <pre>
+ * (Element "p"
+ * (Element "b"
+ * (Text "print ")) ; #1
+ * (Text "'Hello '") ; #2
+ * (Element "br") ; #3
+ * (Text " + 'World';")) ; #4
+ * </pre>
+ * <p>
+ * corresponds to the HTML
+ * {@code <p><b>print </b>'Hello '<br> + 'World';</p>}.</p>
+ *
+ * <p>
+ * It will produce the output:</p>
+ * <pre>
+ * {
+ * sourceCode: "print 'Hello '\n + 'World';",
+ * // 1 2
+ * // 012345678901234 5678901234567
+ * spans: [0, #1, 6, #2, 14, #3, 15, #4]
+ * }
+ * </pre>
+ * <p>
+ * where #1 is a reference to the {@code "print "} text node above, and so
+ * on for the other text nodes.
+ * </p>
+ *
+ * <p>
+ * The {@code} spans array is an array of pairs. Even elements are the start
+ * indices of substrings, and odd elements are the text nodes (or BR elements)
+ * that contain the text for those substrings.
+ * Substrings continue until the next index or the end of the source.
+ * </p>
+ *
+ * @param {Node} node an HTML DOM subtree containing source-code.
+ * @return {Object} source code and the text nodes in which they occur.
+ */
+ function extractSourceSpans(node) {
+ var nocode = /(?:^|\s)nocode(?:\s|$)/;
+
+ var chunks = [];
+ var length = 0;
+ var spans = [];
+ var k = 0;
+
+ var whitespace;
+ if (node.currentStyle) {
+ whitespace = node.currentStyle.whiteSpace;
+ } else if (window.getComputedStyle) {
+ whitespace = document.defaultView.getComputedStyle(node, null)
+ .getPropertyValue('white-space');
+ }
+ var isPreformatted = whitespace && 'pre' === whitespace.substring(0, 3);
+
+ function walk(node) {
+ switch (node.nodeType) {
+ case 1: // Element
+ if (nocode.test(node.className)) { return; }
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ walk(child);
+ }
+ var nodeName = node.nodeName;
+ if ('BR' === nodeName || 'LI' === nodeName) {
+ chunks[k] = '\n';
+ spans[k << 1] = length++;
+ spans[(k++ << 1) | 1] = node;
+ }
+ break;
+ case 3: case 4: // Text
+ var text = node.nodeValue;
+ if (text.length) {
+ if (!isPreformatted) {
+ text = text.replace(/[ \t\r\n]+/g, ' ');
+ } else {
+ text = text.replace(/\r\n?/g, '\n'); // Normalize newlines.
+ }
+ // TODO: handle tabs here?
+ chunks[k] = text;
+ spans[k << 1] = length;
+ length += text.length;
+ spans[(k++ << 1) | 1] = node;
+ }
+ break;
+ }
+ }
+
+ walk(node);
+
+ return {
+ sourceCode: chunks.join('').replace(/\n$/, ''),
+ spans: spans
+ };
+ }
+
+
+ /**
+ * Apply the given language handler to sourceCode and add the resulting
+ * decorations to out.
+ * @param {number} basePos the index of sourceCode within the chunk of source
+ * whose decorations are already present on out.
+ */
+ function appendDecorations(basePos, sourceCode, langHandler, out) {
+ if (!sourceCode) { return; }
+ var job = {
+ sourceCode: sourceCode,
+ basePos: basePos
+ };
+ langHandler(job);
+ out.push.apply(out, job.decorations);
+ }
+
+ var notWs = /\S/;
+
+ /**
+ * Given an element, if it contains only one child element and any text nodes
+ * it contains contain only space characters, return the sole child element.
+ * Otherwise returns undefined.
+ * <p>
+ * This is meant to return the CODE element in {@code <pre><code ...>} when
+ * there is a single child element that contains all the non-space textual
+ * content, but not to return anything where there are multiple child elements
+ * as in {@code <pre><code>...</code><code>...</code></pre>} or when there
+ * is textual content.
+ */
+ function childContentWrapper(element) {
+ var wrapper = undefined;
+ for (var c = element.firstChild; c; c = c.nextSibling) {
+ var type = c.nodeType;
+ if (type === 1) {
+ wrapper = wrapper ? element : c;
+ } else if (type === 3) {
+ wrapper = notWs.test(c.nodeValue) ? element : wrapper;
+ }
+ }
+ return wrapper === element ? undefined : wrapper;
+ }
+
+ /** Given triples of [style, pattern, context] returns a lexing function,
+ * The lexing function interprets the patterns to find token boundaries and
+ * returns a decoration list of the form
+ * [index_0, style_0, index_1, style_1, ..., index_n, style_n]
+ * where index_n is an index into the sourceCode, and style_n is a style
+ * constant like PR_PLAIN. index_n-1 <= index_n, and style_n-1 applies to
+ * all characters in sourceCode[index_n-1:index_n].
+ *
+ * The stylePatterns is a list whose elements have the form
+ * [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
+ *
+ * Style is a style constant like PR_PLAIN, or can be a string of the
+ * form 'lang-FOO', where FOO is a language extension describing the
+ * language of the portion of the token in $1 after pattern executes.
+ * E.g., if style is 'lang-lisp', and group 1 contains the text
+ * '(hello (world))', then that portion of the token will be passed to the
+ * registered lisp handler for formatting.
+ * The text before and after group 1 will be restyled using this decorator
+ * so decorators should take care that this doesn't result in infinite
+ * recursion. For example, the HTML lexer rule for SCRIPT elements looks
+ * something like ['lang-js', /<[s]cript>(.+?)<\/script>/]. This may match
+ * '<script>foo()<\/script>', which would cause the current decorator to
+ * be called with '<script>' which would not match the same rule since
+ * group 1 must not be empty, so it would be instead styled as PR_TAG by
+ * the generic tag rule. The handler registered for the 'js' extension would
+ * then be called with 'foo()', and finally, the current decorator would
+ * be called with '<\/script>' which would not match the original rule and
+ * so the generic tag rule would identify it as a tag.
+ *
+ * Pattern must only match prefixes, and if it matches a prefix, then that
+ * match is considered a token with the same style.
+ *
+ * Context is applied to the last non-whitespace, non-comment token
+ * recognized.
+ *
+ * Shortcut is an optional string of characters, any of which, if the first
+ * character, gurantee that this pattern and only this pattern matches.
+ *
+ * @param {Array} shortcutStylePatterns patterns that always start with
+ * a known character. Must have a shortcut string.
+ * @param {Array} fallthroughStylePatterns patterns that will be tried in
+ * order if the shortcut ones fail. May have shortcuts.
+ *
+ * @return {function (Object)} a
+ * function that takes source code and returns a list of decorations.
+ */
+ function createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns) {
+ var shortcuts = {};
+ var tokenizer;
+ (function () {
+ var allPatterns = shortcutStylePatterns.concat(fallthroughStylePatterns);
+ var allRegexs = [];
+ var regexKeys = {};
+ for (var i = 0, n = allPatterns.length; i < n; ++i) {
+ var patternParts = allPatterns[i];
+ var shortcutChars = patternParts[3];
+ if (shortcutChars) {
+ for (var c = shortcutChars.length; --c >= 0;) {
+ shortcuts[shortcutChars.charAt(c)] = patternParts;
+ }
+ }
+ var regex = patternParts[1];
+ var k = '' + regex;
+ if (!regexKeys.hasOwnProperty(k)) {
+ allRegexs.push(regex);
+ regexKeys[k] = null;
+ }
+ }
+ allRegexs.push(/[\0-\uffff]/);
+ tokenizer = combinePrefixPatterns(allRegexs);
+ })();
+
+ var nPatterns = fallthroughStylePatterns.length;
+
+ /**
+ * Lexes job.sourceCode and produces an output array job.decorations of
+ * style classes preceded by the position at which they start in
+ * job.sourceCode in order.
+ *
+ * @param {Object} job an object like <pre>{
+ * sourceCode: {string} sourceText plain text,
+ * basePos: {int} position of job.sourceCode in the larger chunk of
+ * sourceCode.
+ * }</pre>
+ */
+ var decorate = function (job) {
+ var sourceCode = job.sourceCode, basePos = job.basePos;
+ /** Even entries are positions in source in ascending order. Odd enties
+ * are style markers (e.g., PR_COMMENT) that run from that position until
+ * the end.
+ * @type {Array.<number|string>}
+ */
+ var decorations = [basePos, PR_PLAIN];
+ var pos = 0; // index into sourceCode
+ var tokens = sourceCode.match(tokenizer) || [];
+ var styleCache = {};
+
+ for (var ti = 0, nTokens = tokens.length; ti < nTokens; ++ti) {
+ var token = tokens[ti];
+ var style = styleCache[token];
+ var match = void 0;
+
+ var isEmbedded;
+ if (typeof style === 'string') {
+ isEmbedded = false;
+ } else {
+ var patternParts = shortcuts[token.charAt(0)];
+ if (patternParts) {
+ match = token.match(patternParts[1]);
+ style = patternParts[0];
+ } else {
+ for (var i = 0; i < nPatterns; ++i) {
+ patternParts = fallthroughStylePatterns[i];
+ match = token.match(patternParts[1]);
+ if (match) {
+ style = patternParts[0];
+ break;
+ }
+ }
+
+ if (!match) { // make sure that we make progress
+ style = PR_PLAIN;
+ }
+ }
+
+ isEmbedded = style.length >= 5 && 'lang-' === style.substring(0, 5);
+ if (isEmbedded && !(match && typeof match[1] === 'string')) {
+ isEmbedded = false;
+ style = PR_SOURCE;
+ }
+
+ if (!isEmbedded) { styleCache[token] = style; }
+ }
+
+ var tokenStart = pos;
+ pos += token.length;
+
+ if (!isEmbedded) {
+ decorations.push(basePos + tokenStart, style);
+ } else { // Treat group 1 as an embedded block of source code.
+ var embeddedSource = match[1];
+ var embeddedSourceStart = token.indexOf(embeddedSource);
+ var embeddedSourceEnd = embeddedSourceStart + embeddedSource.length;
+ if (match[2]) {
+ // If embeddedSource can be blank, then it would match at the
+ // beginning which would cause us to infinitely recurse on the
+ // entire token, so we catch the right context in match[2].
+ embeddedSourceEnd = token.length - match[2].length;
+ embeddedSourceStart = embeddedSourceEnd - embeddedSource.length;
+ }
+ var lang = style.substring(5);
+ // Decorate the left of the embedded source
+ appendDecorations(
+ basePos + tokenStart,
+ token.substring(0, embeddedSourceStart),
+ decorate, decorations);
+ // Decorate the embedded source
+ appendDecorations(
+ basePos + tokenStart + embeddedSourceStart,
+ embeddedSource,
+ langHandlerForExtension(lang, embeddedSource),
+ decorations);
+ // Decorate the right of the embedded section
+ appendDecorations(
+ basePos + tokenStart + embeddedSourceEnd,
+ token.substring(embeddedSourceEnd),
+ decorate, decorations);
+ }
+ }
+ job.decorations = decorations;
+ };
+ return decorate;
+ }
+
+ /** returns a function that produces a list of decorations from source text.
+ *
+ * This code treats ", ', and ` as string delimiters, and \ as a string
+ * escape. It does not recognize perl's qq() style strings.
+ * It has no special handling for double delimiter escapes as in basic, or
+ * the tripled delimiters used in python, but should work on those regardless
+ * although in those cases a single string literal may be broken up into
+ * multiple adjacent string literals.
+ *
+ * It recognizes C, C++, and shell style comments.
+ *
+ * @param {Object} options a set of optional parameters.
+ * @return {function (Object)} a function that examines the source code
+ * in the input job and builds the decoration list.
+ */
+ function sourceDecorator(options) {
+ var shortcutStylePatterns = [], fallthroughStylePatterns = [];
+ if (options['tripleQuotedStrings']) {
+ // '''multi-line-string''', 'single-line-string', and double-quoted
+ shortcutStylePatterns.push(
+ [PR_STRING, /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
+ null, '\'"']);
+ } else if (options['multiLineStrings']) {
+ // 'multi-line-string', "multi-line-string"
+ shortcutStylePatterns.push(
+ [PR_STRING, /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,
+ null, '\'"`']);
+ } else {
+ // 'single-line-string', "single-line-string"
+ shortcutStylePatterns.push(
+ [PR_STRING,
+ /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,
+ null, '"\'']);
+ }
+ if (options['verbatimStrings']) {
+ // verbatim-string-literal production from the C# grammar. See issue 93.
+ fallthroughStylePatterns.push(
+ [PR_STRING, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]);
+ }
+ var hc = options['hashComments'];
+ if (hc) {
+ if (options['cStyleComments']) {
+ if (hc > 1) { // multiline hash comments
+ shortcutStylePatterns.push(
+ [PR_COMMENT, /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, null, '#']);
+ } else {
+ // Stop C preprocessor declarations at an unclosed open comment
+ shortcutStylePatterns.push(
+ [PR_COMMENT, /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,
+ null, '#']);
+ }
+ fallthroughStylePatterns.push(
+ [PR_STRING,
+ /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,
+ null]);
+ } else {
+ shortcutStylePatterns.push([PR_COMMENT, /^#[^\r\n]*/, null, '#']);
+ }
+ }
+ if (options['cStyleComments']) {
+ fallthroughStylePatterns.push([PR_COMMENT, /^\/\/[^\r\n]*/, null]);
+ fallthroughStylePatterns.push(
+ [PR_COMMENT, /^\/\*[\s\S]*?(?:\*\/|$)/, null]);
+ }
+ if (options['regexLiterals']) {
+ /**
+ * @const
+ */
+ var REGEX_LITERAL = (
+ // A regular expression literal starts with a slash that is
+ // not followed by * or / so that it is not confused with
+ // comments.
+ '/(?=[^/*])'
+ // and then contains any number of raw characters,
+ + '(?:[^/\\x5B\\x5C]'
+ // escape sequences (\x5C),
+ + '|\\x5C[\\s\\S]'
+ // or non-nesting character sets (\x5B\x5D);
+ + '|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+'
+ // finally closed by a /.
+ + '/');
+ fallthroughStylePatterns.push(
+ ['lang-regex',
+ new RegExp('^' + REGEXP_PRECEDER_PATTERN + '(' + REGEX_LITERAL + ')')
+ ]);
+ }
+
+ var types = options['types'];
+ if (types) {
+ fallthroughStylePatterns.push([PR_TYPE, types]);
+ }
+
+ var keywords = ("" + options['keywords']).replace(/^ | $/g, '');
+ if (keywords.length) {
+ fallthroughStylePatterns.push(
+ [PR_KEYWORD,
+ new RegExp('^(?:' + keywords.replace(/[\s,]+/g, '|') + ')\\b'),
+ null]);
+ }
+
+ shortcutStylePatterns.push([PR_PLAIN, /^\s+/, null, ' \r\n\t\xA0']);
+ fallthroughStylePatterns.push(
+ // TODO(mikesamuel): recognize non-latin letters and numerals in idents
+ [PR_LITERAL, /^@[a-z_$][a-z_$@0-9]*/i, null],
+ [PR_TYPE, /^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/, null],
+ [PR_PLAIN, /^[a-z_$][a-z_$@0-9]*/i, null],
+ [PR_LITERAL,
+ new RegExp(
+ '^(?:'
+ // A hex number
+ + '0x[a-f0-9]+'
+ // or an octal or decimal number,
+ + '|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)'
+ // possibly in scientific notation
+ + '(?:e[+\\-]?\\d+)?'
+ + ')'
+ // with an optional modifier like UL for unsigned long
+ + '[a-z]*', 'i'),
+ null, '0123456789'],
+ // Don't treat escaped quotes in bash as starting strings. See issue 144.
+ [PR_PLAIN, /^\\[\s\S]?/, null],
+ [PR_PUNCTUATION, /^.[^\s\w\.$@\'\"\`\/\#\\]*/, null]);
+
+ return createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns);
+ }
+
+ var decorateSource = sourceDecorator({
+ 'keywords': ALL_KEYWORDS,
+ 'hashComments': true,
+ 'cStyleComments': true,
+ 'multiLineStrings': true,
+ 'regexLiterals': true
+ });
+
+ /**
+ * Given a DOM subtree, wraps it in a list, and puts each line into its own
+ * list item.
+ *
+ * @param {Node} node modified in place. Its content is pulled into an
+ * HTMLOListElement, and each line is moved into a separate list item.
+ * This requires cloning elements, so the input might not have unique
+ * IDs after numbering.
+ */
+ function numberLines(node, opt_startLineNum) {
+ var nocode = /(?:^|\s)nocode(?:\s|$)/;
+ var lineBreak = /\r\n?|\n/;
+
+ var document = node.ownerDocument;
+
+ var whitespace;
+ if (node.currentStyle) {
+ whitespace = node.currentStyle.whiteSpace;
+ } else if (window.getComputedStyle) {
+ whitespace = document.defaultView.getComputedStyle(node, null)
+ .getPropertyValue('white-space');
+ }
+ // If it's preformatted, then we need to split lines on line breaks
+ // in addition to <BR>s.
+ var isPreformatted = whitespace && 'pre' === whitespace.substring(0, 3);
+
+ var li = document.createElement('LI');
+ while (node.firstChild) {
+ li.appendChild(node.firstChild);
+ }
+ // An array of lines. We split below, so this is initialized to one
+ // un-split line.
+ var listItems = [li];
+
+ function walk(node) {
+ switch (node.nodeType) {
+ case 1: // Element
+ if (nocode.test(node.className)) { break; }
+ if ('BR' === node.nodeName) {
+ breakAfter(node);
+ // Discard the <BR> since it is now flush against a </LI>.
+ if (node.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+ } else {
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ walk(child);
+ }
+ }
+ break;
+ case 3: case 4: // Text
+ if (isPreformatted) {
+ var text = node.nodeValue;
+ var match = text.match(lineBreak);
+ if (match) {
+ var firstLine = text.substring(0, match.index);
+ node.nodeValue = firstLine;
+ var tail = text.substring(match.index + match[0].length);
+ if (tail) {
+ var parent = node.parentNode;
+ parent.insertBefore(
+ document.createTextNode(tail), node.nextSibling);
+ }
+ breakAfter(node);
+ if (!firstLine) {
+ // Don't leave blank text nodes in the DOM.
+ node.parentNode.removeChild(node);
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ // Split a line after the given node.
+ function breakAfter(lineEndNode) {
+ // If there's nothing to the right, then we can skip ending the line
+ // here, and move root-wards since splitting just before an end-tag
+ // would require us to create a bunch of empty copies.
+ while (!lineEndNode.nextSibling) {
+ lineEndNode = lineEndNode.parentNode;
+ if (!lineEndNode) { return; }
+ }
+
+ function breakLeftOf(limit, copy) {
+ // Clone shallowly if this node needs to be on both sides of the break.
+ var rightSide = copy ? limit.cloneNode(false) : limit;
+ var parent = limit.parentNode;
+ if (parent) {
+ // We clone the parent chain.
+ // This helps us resurrect important styling elements that cross lines.
+ // E.g. in <i>Foo<br>Bar</i>
+ // should be rewritten to <li><i>Foo</i></li><li><i>Bar</i></li>.
+ var parentClone = breakLeftOf(parent, 1);
+ // Move the clone and everything to the right of the original
+ // onto the cloned parent.
+ var next = limit.nextSibling;
+ parentClone.appendChild(rightSide);
+ for (var sibling = next; sibling; sibling = next) {
+ next = sibling.nextSibling;
+ parentClone.appendChild(sibling);
+ }
+ }
+ return rightSide;
+ }
+
+ var copiedListItem = breakLeftOf(lineEndNode.nextSibling, 0);
+
+ // Walk the parent chain until we reach an unattached LI.
+ for (var parent;
+ // Check nodeType since IE invents document fragments.
+ (parent = copiedListItem.parentNode) && parent.nodeType === 1;) {
+ copiedListItem = parent;
+ }
+ // Put it on the list of lines for later processing.
+ listItems.push(copiedListItem);
+ }
+
+ // Split lines while there are lines left to split.
+ for (var i = 0; // Number of lines that have been split so far.
+ i < listItems.length; // length updated by breakAfter calls.
+ ++i) {
+ walk(listItems[i]);
+ }
+
+ // Make sure numeric indices show correctly.
+ if (opt_startLineNum === (opt_startLineNum|0)) {
+ listItems[0].setAttribute('value', opt_startLineNum);
+ }
+
+ var ol = document.createElement('OL');
+ ol.className = 'linenums';
+ var offset = Math.max(0, ((opt_startLineNum - 1 /* zero index */)) | 0) || 0;
+ for (i = 0, n = listItems.length; i < n; ++i) {
+ li = listItems[i];
+ // Stick a class on the LIs so that stylesheets can
+ // color odd/even rows, or any other row pattern that
+ // is co-prime with 10.
+ li.className = 'L' + ((i + offset) % 10);
+ if (!li.firstChild) {
+ li.appendChild(document.createTextNode('\xA0'));
+ }
+ ol.appendChild(li);
+ }
+
+ node.appendChild(ol);
+ }
+
+ /**
+ * Breaks {@code job.sourceCode} around style boundaries in
+ * {@code job.decorations} and modifies {@code job.sourceNode} in place.
+ * @param {Object} job like <pre>{
+ * sourceCode: {string} source as plain text,
+ * spans: {Array.<number|Node>} alternating span start indices into source
+ * and the text node or element (e.g. {@code <BR>}) corresponding to that
+ * span.
+ * decorations: {Array.<number|string} an array of style classes preceded
+ * by the position at which they start in job.sourceCode in order
+ * }</pre>
+ * @private
+ */
+ function recombineTagsAndDecorations(job) {
+ var isIE = /\bMSIE\b/.test(navigator.userAgent);
+ var newlineRe = /\n/g;
+
+ var source = job.sourceCode;
+ var sourceLength = source.length;
+ // Index into source after the last code-unit recombined.
+ var sourceIndex = 0;
+
+ var spans = job.spans;
+ var nSpans = spans.length;
+ // Index into spans after the last span which ends at or before sourceIndex.
+ var spanIndex = 0;
+
+ var decorations = job.decorations;
+ var nDecorations = decorations.length;
+ // Index into decorations after the last decoration which ends at or before
+ // sourceIndex.
+ var decorationIndex = 0;
+
+ // Remove all zero-length decorations.
+ decorations[nDecorations] = sourceLength;
+ var decPos, i;
+ for (i = decPos = 0; i < nDecorations;) {
+ if (decorations[i] !== decorations[i + 2]) {
+ decorations[decPos++] = decorations[i++];
+ decorations[decPos++] = decorations[i++];
+ } else {
+ i += 2;
+ }
+ }
+ nDecorations = decPos;
+
+ // Simplify decorations.
+ for (i = decPos = 0; i < nDecorations;) {
+ var startPos = decorations[i];
+ // Conflate all adjacent decorations that use the same style.
+ var startDec = decorations[i + 1];
+ var end = i + 2;
+ while (end + 2 <= nDecorations && decorations[end + 1] === startDec) {
+ end += 2;
+ }
+ decorations[decPos++] = startPos;
+ decorations[decPos++] = startDec;
+ i = end;
+ }
+
+ nDecorations = decorations.length = decPos;
+
+ var decoration = null;
+ while (spanIndex < nSpans) {
+ var spanStart = spans[spanIndex];
+ var spanEnd = spans[spanIndex + 2] || sourceLength;
+
+ var decStart = decorations[decorationIndex];
+ var decEnd = decorations[decorationIndex + 2] || sourceLength;
+
+ end = Math.min(spanEnd, decEnd);
+
+ var textNode = spans[spanIndex + 1];
+ var styledText;
+ if (textNode.nodeType !== 1 // Don't muck with <BR>s or <LI>s
+ // Don't introduce spans around empty text nodes.
+ && (styledText = source.substring(sourceIndex, end))) {
+ // This may seem bizarre, and it is. Emitting LF on IE causes the
+ // code to display with spaces instead of line breaks.
+ // Emitting Windows standard issue linebreaks (CRLF) causes a blank
+ // space to appear at the beginning of every line but the first.
+ // Emitting an old Mac OS 9 line separator makes everything spiffy.
+ if (isIE) { styledText = styledText.replace(newlineRe, '\r'); }
+ textNode.nodeValue = styledText;
+ var document = textNode.ownerDocument;
+ var span = document.createElement('SPAN');
+ span.className = decorations[decorationIndex + 1];
+ var parentNode = textNode.parentNode;
+ parentNode.replaceChild(span, textNode);
+ span.appendChild(textNode);
+ if (sourceIndex < spanEnd) { // Split off a text node.
+ spans[spanIndex + 1] = textNode
+ // TODO: Possibly optimize by using '' if there's no flicker.
+ = document.createTextNode(source.substring(end, spanEnd));
+ parentNode.insertBefore(textNode, span.nextSibling);
+ }
+ }
+
+ sourceIndex = end;
+
+ if (sourceIndex >= spanEnd) {
+ spanIndex += 2;
+ }
+ if (sourceIndex >= decEnd) {
+ decorationIndex += 2;
+ }
+ }
+ }
+
+
+ /** Maps language-specific file extensions to handlers. */
+ var langHandlerRegistry = {};
+ /** Register a language handler for the given file extensions.
+ * @param {function (Object)} handler a function from source code to a list
+ * of decorations. Takes a single argument job which describes the
+ * state of the computation. The single parameter has the form
+ * {@code {
+ * sourceCode: {string} as plain text.
+ * decorations: {Array.<number|string>} an array of style classes
+ * preceded by the position at which they start in
+ * job.sourceCode in order.
+ * The language handler should assigned this field.
+ * basePos: {int} the position of source in the larger source chunk.
+ * All positions in the output decorations array are relative
+ * to the larger source chunk.
+ * } }
+ * @param {Array.<string>} fileExtensions
+ */
+ function registerLangHandler(handler, fileExtensions) {
+ for (var i = fileExtensions.length; --i >= 0;) {
+ var ext = fileExtensions[i];
+ if (!langHandlerRegistry.hasOwnProperty(ext)) {
+ langHandlerRegistry[ext] = handler;
+ } else if (window['console']) {
+ console['warn']('cannot override language handler %s', ext);
+ }
+ }
+ }
+ function langHandlerForExtension(extension, source) {
+ if (!(extension && langHandlerRegistry.hasOwnProperty(extension))) {
+ // Treat it as markup if the first non whitespace character is a < and
+ // the last non-whitespace character is a >.
+ extension = /^\s*</.test(source)
+ ? 'default-markup'
+ : 'default-code';
+ }
+ return langHandlerRegistry[extension];
+ }
+ registerLangHandler(decorateSource, ['default-code']);
+ registerLangHandler(
+ createSimpleLexer(
+ [],
+ [
+ [PR_PLAIN, /^[^<?]+/],
+ [PR_DECLARATION, /^<!\w[^>]*(?:>|$)/],
+ [PR_COMMENT, /^<\!--[\s\S]*?(?:-\->|$)/],
+ // Unescaped content in an unknown language
+ ['lang-', /^<\?([\s\S]+?)(?:\?>|$)/],
+ ['lang-', /^<%([\s\S]+?)(?:%>|$)/],
+ [PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/],
+ ['lang-', /^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],
+ // Unescaped content in javascript. (Or possibly vbscript).
+ ['lang-js', /^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],
+ // Contains unescaped stylesheet content
+ ['lang-css', /^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],
+ ['lang-in.tag', /^(<\/?[a-z][^<>]*>)/i]
+ ]),
+ ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']);
+ registerLangHandler(
+ createSimpleLexer(
+ [
+ [PR_PLAIN, /^[\s]+/, null, ' \t\r\n'],
+ [PR_ATTRIB_VALUE, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '\"\'']
+ ],
+ [
+ [PR_TAG, /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],
+ [PR_ATTRIB_NAME, /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],
+ ['lang-uq.val', /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],
+ [PR_PUNCTUATION, /^[=<>\/]+/],
+ ['lang-js', /^on\w+\s*=\s*\"([^\"]+)\"/i],
+ ['lang-js', /^on\w+\s*=\s*\'([^\']+)\'/i],
+ ['lang-js', /^on\w+\s*=\s*([^\"\'>\s]+)/i],
+ ['lang-css', /^style\s*=\s*\"([^\"]+)\"/i],
+ ['lang-css', /^style\s*=\s*\'([^\']+)\'/i],
+ ['lang-css', /^style\s*=\s*([^\"\'>\s]+)/i]
+ ]),
+ ['in.tag']);
+ registerLangHandler(
+ createSimpleLexer([], [[PR_ATTRIB_VALUE, /^[\s\S]+/]]), ['uq.val']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': CPP_KEYWORDS,
+ 'hashComments': true,
+ 'cStyleComments': true,
+ 'types': C_TYPES
+ }), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': 'null,true,false'
+ }), ['json']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': CSHARP_KEYWORDS,
+ 'hashComments': true,
+ 'cStyleComments': true,
+ 'verbatimStrings': true,
+ 'types': C_TYPES
+ }), ['cs']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': JAVA_KEYWORDS,
+ 'cStyleComments': true
+ }), ['java']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': SH_KEYWORDS,
+ 'hashComments': true,
+ 'multiLineStrings': true
+ }), ['bsh', 'csh', 'sh']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': PYTHON_KEYWORDS,
+ 'hashComments': true,
+ 'multiLineStrings': true,
+ 'tripleQuotedStrings': true
+ }), ['cv', 'py']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': PERL_KEYWORDS,
+ 'hashComments': true,
+ 'multiLineStrings': true,
+ 'regexLiterals': true
+ }), ['perl', 'pl', 'pm']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': RUBY_KEYWORDS,
+ 'hashComments': true,
+ 'multiLineStrings': true,
+ 'regexLiterals': true
+ }), ['rb']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': JSCRIPT_KEYWORDS,
+ 'cStyleComments': true,
+ 'regexLiterals': true
+ }), ['js']);
+ registerLangHandler(sourceDecorator({
+ 'keywords': COFFEE_KEYWORDS,
+ 'hashComments': 3, // ### style block comments
+ 'cStyleComments': true,
+ 'multilineStrings': true,
+ 'tripleQuotedStrings': true,
+ 'regexLiterals': true
+ }), ['coffee']);
+ registerLangHandler(createSimpleLexer([], [[PR_STRING, /^[\s\S]+/]]), ['regex']);
+
+ function applyDecorator(job) {
+ var opt_langExtension = job.langExtension;
+
+ try {
+ // Extract tags, and convert the source code to plain text.
+ var sourceAndSpans = extractSourceSpans(job.sourceNode);
+ /** Plain text. @type {string} */
+ var source = sourceAndSpans.sourceCode;
+ job.sourceCode = source;
+ job.spans = sourceAndSpans.spans;
+ job.basePos = 0;
+
+ // Apply the appropriate language handler
+ langHandlerForExtension(opt_langExtension, source)(job);
+
+ // Integrate the decorations and tags back into the source code,
+ // modifying the sourceNode in place.
+ recombineTagsAndDecorations(job);
+ } catch (e) {
+ if ('console' in window) {
+ console['log'](e && e['stack'] ? e['stack'] : e);
+ }
+ }
+ }
+
+ /**
+ * @param sourceCodeHtml {string} The HTML to pretty print.
+ * @param opt_langExtension {string} The language name to use.
+ * Typically, a filename extension like 'cpp' or 'java'.
+ * @param opt_numberLines {number|boolean} True to number lines,
+ * or the 1-indexed number of the first line in sourceCodeHtml.
+ */
+ function prettyPrintOne(sourceCodeHtml, opt_langExtension, opt_numberLines) {
+ var container = document.createElement('PRE');
+ // This could cause images to load and onload listeners to fire.
+ // E.g. <img onerror="alert(1337)" src="nosuchimage.png">.
+ // We assume that the inner HTML is from a trusted source.
+ container.innerHTML = sourceCodeHtml;
+ if (opt_numberLines) {
+ numberLines(container, opt_numberLines);
+ }
+
+ var job = {
+ langExtension: opt_langExtension,
+ numberLines: opt_numberLines,
+ sourceNode: container
+ };
+ applyDecorator(job);
+ return container.innerHTML;
+ }
+
+ function prettyPrint(opt_whenDone) {
+ function byTagName(tn) { return document.getElementsByTagName(tn); }
+ // fetch a list of nodes to rewrite
+ var codeSegments = [byTagName('pre'), byTagName('code'), byTagName('xmp')];
+ var elements = [];
+ for (var i = 0; i < codeSegments.length; ++i) {
+ for (var j = 0, n = codeSegments[i].length; j < n; ++j) {
+ elements.push(codeSegments[i][j]);
+ }
+ }
+ codeSegments = null;
+
+ var clock = Date;
+ if (!clock['now']) {
+ clock = { 'now': function () { return +(new Date); } };
+ }
+
+ // The loop is broken into a series of continuations to make sure that we
+ // don't make the browser unresponsive when rewriting a large page.
+ var k = 0;
+ var prettyPrintingJob;
+
+ var langExtensionRe = /\blang(?:uage)?-([\w.]+)(?!\S)/;
+ var prettyPrintRe = /\bprettyprint\b/;
+
+ function doWork() {
+ var endTime = (window['PR_SHOULD_USE_CONTINUATION'] ?
+ clock['now']() + 250 /* ms */ :
+ Infinity);
+ for (; k < elements.length && clock['now']() < endTime; k++) {
+ var cs = elements[k];
+ var className = cs.className;
+ if (className.indexOf('prettyprint') >= 0) {
+ // If the classes includes a language extensions, use it.
+ // Language extensions can be specified like
+ // <pre class="prettyprint lang-cpp">
+ // the language extension "cpp" is used to find a language handler as
+ // passed to PR.registerLangHandler.
+ // HTML5 recommends that a language be specified using "language-"
+ // as the prefix instead. Google Code Prettify supports both.
+ // http://dev.w3.org/html5/spec-author-view/the-code-element.html
+ var langExtension = className.match(langExtensionRe);
+ // Support <pre class="prettyprint"><code class="language-c">
+ var wrapper;
+ if (!langExtension && (wrapper = childContentWrapper(cs))
+ && "CODE" === wrapper.tagName) {
+ langExtension = wrapper.className.match(langExtensionRe);
+ }
+
+ if (langExtension) {
+ langExtension = langExtension[1];
+ }
+
+ // make sure this is not nested in an already prettified element
+ var nested = false;
+ for (var p = cs.parentNode; p; p = p.parentNode) {
+ if ((p.tagName === 'pre' || p.tagName === 'code' ||
+ p.tagName === 'xmp') &&
+ p.className && p.className.indexOf('prettyprint') >= 0) {
+ nested = true;
+ break;
+ }
+ }
+ if (!nested) {
+ // Look for a class like linenums or linenums:<n> where <n> is the
+ // 1-indexed number of the first line.
+ var lineNums = cs.className.match(/\blinenums\b(?::(\d+))?/);
+ if (lineNums) {
+ lineNums = lineNums[1] && lineNums[1].length ? +lineNums[1] : true;
+ } else {
+ lineNums = false;
+ }
+ if (lineNums) { numberLines(cs, lineNums); }
+
+ // do the pretty printing
+ prettyPrintingJob = {
+ langExtension: langExtension,
+ sourceNode: cs,
+ numberLines: lineNums
+ };
+ applyDecorator(prettyPrintingJob);
+ }
+ }
+ }
+ if (k < elements.length) {
+ // finish up in a continuation
+ setTimeout(doWork, 250);
+ } else if (opt_whenDone) {
+ opt_whenDone();
+ }
+ }
+
+ doWork();
+ }
+
+ /**
+ * Find all the {@code <pre>} and {@code <code>} tags in the DOM with
+ * {@code class=prettyprint} and prettify them.
+ *
+ * @param {Function?} opt_whenDone if specified, called when the last entry
+ * has been finished.
+ */
+ window['prettyPrintOne'] = prettyPrintOne;
+ /**
+ * Pretty print a chunk of code.
+ *
+ * @param {string} sourceCodeHtml code as html
+ * @return {string} code as html, but prettier
+ */
+ window['prettyPrint'] = prettyPrint;
+ /**
+ * Contains functions for creating and registering new language handlers.
+ * @type {Object}
+ */
+ window['PR'] = {
+ 'createSimpleLexer': createSimpleLexer,
+ 'registerLangHandler': registerLangHandler,
+ 'sourceDecorator': sourceDecorator,
+ 'PR_ATTRIB_NAME': PR_ATTRIB_NAME,
+ 'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE,
+ 'PR_COMMENT': PR_COMMENT,
+ 'PR_DECLARATION': PR_DECLARATION,
+ 'PR_KEYWORD': PR_KEYWORD,
+ 'PR_LITERAL': PR_LITERAL,
+ 'PR_NOCODE': PR_NOCODE,
+ 'PR_PLAIN': PR_PLAIN,
+ 'PR_PUNCTUATION': PR_PUNCTUATION,
+ 'PR_SOURCE': PR_SOURCE,
+ 'PR_STRING': PR_STRING,
+ 'PR_TAG': PR_TAG,
+ 'PR_TYPE': PR_TYPE
+ };
+})();
diff --git a/toolkit/components/microformats/test/static/javascript/testrunner.js b/toolkit/components/microformats/test/static/javascript/testrunner.js
new file mode 100644
index 0000000000..db8db492e4
--- /dev/null
+++ b/toolkit/components/microformats/test/static/javascript/testrunner.js
@@ -0,0 +1,179 @@
+/*!
+ testrunner
+ Used by http://localhost:3000/testrunner.html
+ Copyright (C) 2010 - 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+*/
+
+var options = {
+ 'baseUrl': 'http://example.com',
+ 'dateFormat': 'html5',
+ 'parseLatLonGeo': false
+ };
+
+window.onload = function() {
+ var test = testData.data[0],
+ versionElt = document.querySelector('#version');
+
+ versionElt.innerHTML = 'v' + testData.version;
+
+ buildTest( test );
+ buildList( testData );
+}
+
+
+function displayTest(e){
+ var label = e.target.innerHTML;
+ var i = testData.data.length;
+ while (i--) {
+ if(testData.data[i].name === label){
+ buildTest( testData.data[i] );
+ break;
+ }
+ }
+}
+
+
+function buildTest( test ){
+ var testDetailElt = document.querySelector('.test-detail'),
+ nameElt = document.querySelector('#test-name'),
+ htmlElt = document.querySelector('#test-html pre code'),
+ jsonElt = document.querySelector('#test-json pre code'),
+ parserElt = document.querySelector('#parser-json pre code'),
+ diffElt = document.querySelector('#test-diff pre code');
+
+ nameElt.innerHTML = test.name;
+ htmlElt.innerHTML = htmlEscape( test.html );
+ jsonElt.innerHTML = htmlEscape( test.json );
+
+ var dom = new DOMParser();
+ doc = dom.parseFromString( test.html, 'text/html' );
+
+ options.node = doc;
+ var mfJSON = Microformats.get( options );
+ parserElt.innerHTML = htmlEscape( js_beautify( JSON.stringify(mfJSON) ) );
+
+ // diff json
+ var diff = DeepDiff(JSON.parse(test.json), mfJSON);
+ if(diff !== undefined){
+ diffElt.innerHTML = htmlEscape( js_beautify( JSON.stringify(diff) ) );
+ }else{
+ diffElt.innerHTML = '';
+ }
+
+ console.log(diff)
+ if(diff !== undefined){
+ addClass(nameElt, 'failed');
+ addClass(testDetailElt, 'test-failed');
+ removeClass(testDetailElt, 'test-passed');
+ }else{
+ removeClass(nameElt, 'failed');
+ removeClass(testDetailElt, 'test-failed');
+ addClass(testDetailElt, 'test-passed');
+ }
+
+ testDetailElt.style.display = 'block';
+
+ //prettyPrint();
+}
+
+
+
+function passTest( test ){
+ var dom = new DOMParser(),
+ doc = dom.parseFromString( test.html, 'text/html' );
+
+ options.node = doc;
+ var mfJSON = Microformats.get( options );
+
+ // diff json
+ var diff = DeepDiff(JSON.parse(test.json), mfJSON);
+ return (diff === undefined);
+}
+
+
+
+
+function buildList( tests ){
+ var total = tests.data.length,
+ passed = 0,
+ testResultListElt = document.querySelector('.test-result-list');
+
+ tests.data.forEach(function(item){
+ var li = document.createElement('li');
+ li.innerHTML = item.name;
+ testResultListElt.appendChild(li);
+
+ if( passTest( item ) === false ){
+ //li.classList.add('failed')
+ addClass(li, 'failed');
+ }else{
+ passed ++;
+ }
+
+ li.addEventListener('click', function(e){
+ e.preventDefault();
+ displayTest(e);
+ });
+
+ });
+ updateCounts( {
+ 'total': total,
+ 'passed': passed,
+ 'percentPassed': ((100/total) * passed).toFixed(1)
+ } )
+}
+
+
+function updateCounts( data ){
+ var testCountsElt = document.querySelector('.test-counts');
+ testCountsElt.innerHTML = 'Passed: ' + data.passed + '/' + data.total + ' - ' + data.percentPassed + '% of microformats tests';
+}
+
+
+function htmlEscape(str) {
+ return String(str)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+}
+
+// I needed the opposite function today, so adding here too:
+function htmlUnescape(value){
+ return String(value)
+ .replace(/&quot;/g, '"')
+ .replace(/&#39;/g, "'")
+ .replace(/&lt;/g, '<')
+ .replace(/&gt;/g, '>')
+ .replace(/&amp;/g, '&');
+}
+
+
+// Does the node have a class
+function hasClass(node, className) {
+ if (node.className) {
+ return node.className.match(
+ new RegExp('(\\s|^)' + className + '(\\s|$)'));
+ } else {
+ return false;
+ }
+}
+
+
+// Add a class to an node
+function addClass(node, className) {
+ if (!hasClass(node, className)) {
+ node.className += " " + className;
+ }
+}
+
+
+// Removes a class from an node
+function removeClass(node, className) {
+ if (hasClass(node, className)) {
+ var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
+ node.className = node.className.replace(reg, ' ');
+ }
+}
diff --git a/toolkit/components/microformats/test/static/parse-umd.html b/toolkit/components/microformats/test/static/parse-umd.html
new file mode 100644
index 0000000000..ec9a0c0711
--- /dev/null
+++ b/toolkit/components/microformats/test/static/parse-umd.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+ <meta charset="UTF-8">
+ <title>Parse Microformats</title>
+ <meta name="description" content="microformat-shiv - A light-weight cross browser javascript microformats 2 parser" />
+
+ <link rel="stylesheet" href="css/testrunner.css">
+ <link rel="stylesheet" href="css/prettify.css">
+
+
+ <!-- Loads full umd version -->
+ <script src="../../thirdparty/es5-shim.min.js"></script>
+ <script src="../../microformat-shiv.js"></script>
+
+
+ <script src="javascript/beautify.js"></script>
+ <script src="javascript/prettify.js"></script>
+ <script src="javascript/DOMParser.js"></script>
+ <script src="javascript/parse.js"></script>
+
+ </head>
+
+ <body>
+
+ <p>microformat-shiv</p>
+ <h1>Parse Microformats - UMD</h1>
+ <p>Type or copy and paste the HTML you want to parse into the box below.</p>
+
+ <form id="mf-form" class="tool-interface" method="get" action="">
+ <p>
+ <label for="html">HTML</label>
+<textarea id="html" name="html">&lt;a class="h-card" href="http://glennjones.net"&gt;
+ &lt;span class="p-given-name"&gt;Glenn&lt;/span&gt;
+ &lt;span class="p-family-name"&gt;Jones&lt;/span&gt;
+&lt;/a&gt;
+</textarea>
+ </p>
+
+
+ <p>
+ <label for="baseurl">BaseURL</label>
+ <input id="baseurl" name="baseurl" placeholder="Optional URL to help resolve relative links" value="http://example.com" type="text">
+ </p>
+
+
+ <p>
+ <label for="filters">Filters</label>
+ <input id="filters" name="filters" placeholder="Optional comma separted list of formats to filter by" type="text">
+ </p>
+
+
+ <p class="checkbox">
+ <input id="collapsewhitespace" name="collapsewhitespace" id="textformat2" type="checkbox">
+ <label class="checkbox-label" for="textformat2"><strong>Experimental</strong> ‐ Text white-space collapsing</label>
+ </p>
+
+ <p class="checkbox">
+ <input id="parseLatLonGeo" name="parseLatLonGeo" id="parseLatLonGeo" type="checkbox">
+ <label class="checkbox-label" for="parseLatLonGeo"><strong>Experimental</strong> ‐ Parse geo data writen as latlon i.e. 30.267991;-97.739568</label>
+ </p>
+
+ <p>
+
+ <select id="dateformat" class="indent" name="dateformat" id="dateformat2">
+ <option value="auto" selected="selected">auto</option>
+ <option value="W3C">w3c</option>
+ <option value="HTML5">html5</option>
+ <option value="RFC3339">rfc3339</option>
+ </select>
+
+ <label class="checkbox-label" for="dateformat2"><strong>Experimental</strong> ‐ ISO date profile</label>
+ </p>
+
+ <input class="button" value="Parse" type="submit">
+ </form>
+
+ <h1>Parser JSON</h1>
+ <div id="parser-json"><pre class="prettyprint"><code class="language-json"></code></pre></div>
+
+
+
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/microformats/test/static/parse.html b/toolkit/components/microformats/test/static/parse.html
new file mode 100644
index 0000000000..c8b929fcba
--- /dev/null
+++ b/toolkit/components/microformats/test/static/parse.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+ <meta charset="UTF-8">
+ <title>Parse Microformats</title>
+ <meta name="description" content="microformat-shiv - A light-weight cross browser javascript microformats 2 parser" />
+
+ <link rel="stylesheet" href="css/testrunner.css">
+ <link rel="stylesheet" href="css/prettify.css">
+
+ <script src="..././thirdparty/es5-shim.min.js"></script>
+
+ <!-- loads Modules to help with debugging ie windows.Modules -->
+ <script src="../../lib/utilities.js"></script>
+ <script src="../../lib/domutils.js"></script>
+ <script src="../../lib/url.js"></script>
+ <script src="../../lib/html.js"></script>
+ <script src="../../lib/text.js"></script>
+ <script src="../../lib/dates.js"></script>
+ <script src="../../lib/isodate.js"></script>
+ <script src="../../lib/parser.js"></script>
+ <script src="../../lib/parser-implied.js"></script>
+ <script src="../../lib/parser-includes.js"></script>
+ <script src="../../lib/parser-rels.js"></script>
+
+ <script src="../../lib/maps/h-adr.js"></script>
+ <script src="../../lib/maps/h-card.js"></script>
+ <script src="../../lib/maps/h-entry.js"></script>
+ <script src="../../lib/maps/h-event.js"></script>
+ <script src="../../lib/maps/h-feed.js"></script>
+ <script src="../../lib/maps/h-geo.js"></script>
+ <script src="../../lib/maps/h-item.js"></script>
+ <script src="../../lib/maps/h-listing.js"></script>
+ <script src="../../lib/maps/h-news.js"></script>
+ <script src="../../lib/maps/h-org.js"></script>
+ <script src="../../lib/maps/h-product.js"></script>
+ <script src="../../lib/maps/h-recipe.js"></script>
+ <script src="../../lib/maps/h-resume.js"></script>
+ <script src="../../lib/maps/h-review-aggregate.js"></script>
+ <script src="../../lib/maps/h-review.js"></script>
+ <script src="../../lib/maps/rel.js"></script>
+
+
+ <script src="javascript/beautify.js"></script>
+ <script src="javascript/prettify.js"></script>
+
+ <script src="javascript/DOMParser.js"></script>
+ <script src="javascript/parse.js"></script>
+
+ </head>
+
+ <body>
+
+ <p>microformat-shiv</p>
+ <h1>Parse Microformats - Modules</h1>
+ <p>Type or copy and paste the HTML you want to parse into the box below.</p>
+
+ <form id="mf-form" class="tool-interface" method="get" action="">
+ <p>
+ <label for="html">HTML</label>
+<textarea id="html" name="html">&lt;a class="h-card" href="glenn.html"&gt;
+ &lt;span class="p-given-name"&gt;Glenn&lt;/span&gt;
+ &lt;span class="p-family-name"&gt;Jones&lt;/span&gt;
+&lt;/a&gt;
+</textarea>
+ </p>
+
+
+ <p>
+ <label for="baseurl">BaseURL</label>
+ <input id="baseurl" name="baseurl" placeholder="Optional URL to help resolve relative links" value="http://example.com" type="text">
+ </p>
+
+ <p>
+ <label for="filters">Filters</label>
+ <input id="filters" name="filters" placeholder="Optional comma separted list of formats to filter by" type="text">
+ </p>
+
+
+
+ <p class="checkbox">
+ <input id="collapsewhitespace" name="collapsewhitespace" id="textformat2" type="checkbox">
+ <label class="checkbox-label" for="textformat2"><strong>Experimental</strong> ‐ Text white-space collapsing</label>
+ </p>
+
+ <!--
+ <p class="checkbox">
+ <input id="overlappingversions" name="overlappingversions" id="overlappingversions" type="checkbox">
+ <label class="checkbox-label" for="overlappingversions"><strong>Experimental</strong> ‐ Block overlapping properties from different microformat versions</label>
+ </p>
+
+ <p class="checkbox">
+ <input id="impliedPropertiesByVersion" name="impliedPropertiesByVersion" id="impliedPropertiesByVersion" type="checkbox">
+ <label class="checkbox-label" for="impliedPropertiesByVersion"><strong>Experimental</strong> ‐ Set implied properties by microformat version</label>
+ </p>
+ -->
+
+ <p class="checkbox">
+ <input id="parseLatLonGeo" name="parseLatLonGeo" id="parseLatLonGeo" type="checkbox">
+ <label class="checkbox-label" for="parseLatLonGeo"><strong>Experimental</strong> ‐ Parse geo data writen as latlon i.e. 30.267991;-97.739568</label>
+ </p>
+
+
+
+ <p>
+
+ <select id="dateformat" class="indent" name="dateformat" id="dateformat2">
+ <option value="auto" selected="selected">auto</option>
+ <option value="W3C">w3c</option>
+ <option value="HTML5">html5</option>
+ <option value="RFC3339">rfc3339</option>
+ </select>
+
+ <label class="checkbox-label" for="dateformat2"><strong>Experimental</strong> ‐ Fixed ISO date profile for output</label>
+ </p>
+
+ <input class="button" value="Parse" type="submit">
+ </form>
+
+ <h1>Parser JSON</h1>
+ <div id="parser-json"><pre class="prettyprint"><code class="language-json"></code></pre></div>
+
+
+
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/microformats/test/static/testrunner.html b/toolkit/components/microformats/test/static/testrunner.html
new file mode 100644
index 0000000000..54e8ceb846
--- /dev/null
+++ b/toolkit/components/microformats/test/static/testrunner.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Microformats Test Suite</title>
+
+ <link rel="stylesheet" href="css/testrunner.css">
+ <link rel="stylesheet" href="css/prettify.css">
+
+
+ <script src="../../thirdparty/es5-shim.min.js"></script>
+ <script src="../../microformat-shiv.js"></script>
+
+
+ <script src="javascript/DOMParser.js"></script>
+ <script src="javascript/data.js"></script>
+ <script src="javascript/beautify.js"></script>
+ <script src="javascript/prettify.js"></script>
+ <script src="javascript/deep-diff-0.3.1.min.js"></script>
+ <script src="javascript/testrunner.js"></script>
+
+
+</head>
+<body >
+
+ <p>microformat-shiv</p>
+ <h1>Microformats Tests <span id="version"></span></h1>
+
+ <h2 class="test-counts"></h2>
+
+
+ <p>Inspect the details of any test from the list</p>
+ <section class="flexbox-container">
+
+ <section class="test-results">
+ <ul class="test-result-list">
+ </ul>
+ </section>
+
+
+ <section class="test-detail">
+
+
+ <h2 id="test-name"></h2>
+
+ <h1>Test</h1>
+ <div id="test-html"><pre class="prettyprint"><code class="language-html"></code></pre></div>
+
+ <h1>Expected JSON</h1>
+ <div id="test-json"><pre class="prettyprint"><code class="language-json"></code></pre></div>
+
+ <h1>Parser JSON</h1>
+ <div id="parser-json"><pre class="prettyprint"><code class="language-json"></code></pre></div>
+
+ <div class="differences">
+ <h1>Differences</h1>
+ <div id="test-diff"><pre class="prettyprint"><code class="language-json"></code></pre></div>
+ </div>
+
+ </section>
+
+ </section>
+
+ <footer>
+
+ </footer>
+</body>
+
+</html> \ No newline at end of file
diff --git a/toolkit/components/microformats/update/package.json b/toolkit/components/microformats/update/package.json
new file mode 100644
index 0000000000..8f829439f1
--- /dev/null
+++ b/toolkit/components/microformats/update/package.json
@@ -0,0 +1,21 @@
+{
+ "author": "Glenn Jones",
+ "name": "microformat-shiv-updater",
+ "description": "A script for updating microformat-shiv in mozilla-central from source repo",
+ "version": "1.0.0",
+ "license": "MIT",
+ "homepage": "http://microformat-shiv.com",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/glennjones/microformat-shiv-updater.git"
+ },
+ "main": "update.js",
+ "scripts": {
+ "start": "update"
+ },
+ "dependencies": {
+ "download-github-repo": "0.1.x",
+ "fs-extra": "0.19.x",
+ "request": "2.58.x"
+ }
+} \ No newline at end of file
diff --git a/toolkit/components/microformats/update/readme.txt b/toolkit/components/microformats/update/readme.txt
new file mode 100644
index 0000000000..0e41447a89
--- /dev/null
+++ b/toolkit/components/microformats/update/readme.txt
@@ -0,0 +1,33 @@
+/*!
+ update.js
+
+ This node.js script downloads latest version of microformat-shiv and it tests form the authors github repo.
+
+ Make sure your have an uptodate copy of node.js on your machine then using a command line navigate the
+ directory containing the update.js and run the following commands:
+
+ $ npm install
+ $ node unpdate.js
+
+ The script will
+
+ 1. Checks the current build status of the project.
+ 2. Checks the date of the last commit
+ 3. Downloads and updates the following directories and files:
+ * microformat-shiv.js
+ * test/lib
+ * test/interface-tests
+ * test/module-tests
+ * test/standards-tests
+ * test/static
+ 4. Adds the EXPORTED_SYMBOLS to the bottom of microformat-shiv.js
+ 5. Repath the links in test/module-tests/index.html file
+
+
+ This will update the microformats parser and all the related tests.
+
+
+
+ Copyright (C) 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ */ \ No newline at end of file
diff --git a/toolkit/components/microformats/update/update.js b/toolkit/components/microformats/update/update.js
new file mode 100644
index 0000000000..80795d523c
--- /dev/null
+++ b/toolkit/components/microformats/update/update.js
@@ -0,0 +1,266 @@
+/* !
+ update.js
+
+ run $ npm install
+ run $ node unpdate.js
+
+ Downloads latest version of microformat-shiv and it tests form github repo
+ Files downloaded:
+ * microformat-shiv.js (note: modern version)
+ * lib
+ * test/interface-tests
+ * test/module-tests
+ * test/standards-tests
+ * test/static
+
+ Copyright (C) 2015 Glenn Jones. All Rights Reserved.
+ MIT License: https://raw.github.com/glennjones/microformat-shiv/master/license.txt
+ */
+
+// configuration
+var deployDir = '../'
+ exportedSymbol = 'try {\n // mozilla jsm support\n Components.utils.importGlobalProperties(["URL"]);\n} catch(e) {}\nthis.EXPORTED_SYMBOLS = [\'Microformats\'];';
+
+
+
+var path = require('path'),
+ request = require('request'),
+ fs = require('fs-extra'),
+ download = require('download-github-repo');
+
+
+var repo = 'glennjones/microformat-shiv',
+ tempDir = path.resolve(__dirname, 'temp-repo'),
+ deployDirResolved = path.resolve(__dirname, deployDir),
+ pathList = [
+ ['/modern/microformat-shiv-modern.js', '/microformat-shiv.js'],
+ ['/lib', '/test/lib'],
+ ['/test/interface-tests', '/test/interface-tests'],
+ ['/test/module-tests', '/test/module-tests'],
+ ['/test/standards-tests', '/test/standards-tests'],
+ ['/test/static', '/test/static']
+ ];
+
+
+
+getLastBuildState( repo, function( err, buildState) {
+ if (buildState) {
+ console.log('last build state:', buildState);
+
+ if (buildState === 'passed') {
+
+ console.log('downloading git repo', repo);
+ getLastCommitDate( repo, function( err, date) {
+ if (date) {
+ console.log( 'last commit:', new Date(date).toString() );
+ }
+ });
+ updateFromRepo();
+
+ } else {
+ console.log('not updating because of build state is failing please contact Glenn Jones glennjones@gmail.com');
+ }
+
+ } else {
+ console.log('could not get build state from travis-ci:', err);
+ }
+});
+
+
+/**
+ * updates from directories and files from repo
+ *
+ */
+function updateFromRepo() {
+ download(repo, tempDir, function(err, data) {
+
+ // the err and data from download-github-repo give false negatives
+ if ( fs.existsSync( tempDir ) ) {
+
+ var version = getRepoVersion();
+ removeCurrentFiles( pathList, deployDirResolved );
+ addNewFiles( pathList, deployDirResolved );
+ fs.removeSync(tempDir);
+
+ // changes files for firefox
+ replaceInFile('/test/module-tests/index.html', /..\/..\/lib\//g, '../lib/' );
+ addExportedSymbol( '/microformat-shiv.js' );
+
+ console.log('microformat-shiv is now uptodate to v' + version);
+
+ } else {
+ console.log('error getting repo', err);
+ }
+
+ });
+}
+
+
+/**
+ * removes old version of delpoyed directories and files
+ *
+ * @param {Array} pathList
+ * @param {String} deployDirResolved
+ */
+function removeCurrentFiles( pathList, deployDirResolved ) {
+ pathList.forEach( function( path ) {
+ console.log('removed:', deployDirResolved + path[1]);
+ fs.removeSync(deployDirResolved + path[1]);
+ });
+}
+
+
+/**
+ * copies over required directories and files into deployed path
+ *
+ * @param {Array} pathList
+ * @param {String} deployDirResolved
+ */
+function addNewFiles( pathList, deployDirResolved ) {
+ pathList.forEach( function( path ) {
+ console.log('added:', deployDirResolved + path[1]);
+ fs.copySync(tempDir + path[0], deployDirResolved + path[1]);
+ });
+
+}
+
+
+/**
+ * gets the repo version number
+ *
+ * @return {String}
+ */
+function getRepoVersion() {
+ var pack = fs.readFileSync(path.resolve(tempDir, 'package.json'), {encoding: 'utf8'});
+ if (pack) {
+ pack = JSON.parse(pack)
+ if (pack && pack.version) {
+ return pack.version;
+ }
+ }
+ return '';
+}
+
+
+/**
+ * get the last commit date from github repo
+ *
+ * @param {String} repo
+ * @param {Function} callback
+ */
+function getLastCommitDate( repo, callback ) {
+
+ var options = {
+ url: 'https://api.github.com/repos/' + repo + '/commits?per_page=1',
+ headers: {
+ 'User-Agent': 'request'
+ }
+ };
+
+ request(options, function (error, response, body) {
+ if (!error && response.statusCode == 200) {
+ var date = null,
+ json = JSON.parse(body);
+ if (json && json.length && json[0].commit && json[0].commit.author ) {
+ date = json[0].commit.author.date;
+ }
+ callback(null, date);
+ } else {
+ console.log(error, response, body);
+ callback('fail to get last commit date', null);
+ }
+ });
+}
+
+
+/**
+ * get the last build state from travis-ci
+ *
+ * @param {String} repo
+ * @param {Function} callback
+ */
+function getLastBuildState( repo, callback ) {
+
+ var options = {
+ url: 'https://api.travis-ci.org/repos/' + repo,
+ headers: {
+ 'User-Agent': 'request',
+ 'Accept': 'application/vnd.travis-ci.2+json'
+ }
+ };
+
+ request(options, function (error, response, body) {
+ if (!error && response.statusCode == 200) {
+ var buildState = null,
+ json = JSON.parse(body);
+ if (json && json.repo && json.repo.last_build_state ) {
+ buildState = json.repo.last_build_state;
+ }
+ callback(null, buildState);
+ } else {
+ console.log(error, response, body);
+ callback('fail to get last build state', null);
+ }
+ });
+}
+
+
+/**
+ * adds exported symbol to microformat-shiv.js file
+ *
+ * @param {String} path
+ * @param {String} content
+ */
+function addExportedSymbol( path ) {
+ if (path === '/microformat-shiv.js') {
+ fs.appendFileSync(deployDirResolved + '/microformat-shiv.js', '\r\n' + exportedSymbol + '\r\n');
+ console.log('appended exported symbol to microformat-shiv.js');
+ }
+}
+
+
+/**
+ * adds exported symbol to microformat-shiv.js file
+ *
+ * @param {String} path
+ * @param {String} content
+ */
+function replaceInFile( path, findStr, replaceStr ) {
+ readFile(deployDirResolved + path, function(err, fileStr) {
+ if (fileStr) {
+ fileStr = fileStr.replace(findStr, replaceStr)
+ writeFile(deployDirResolved + path, fileStr);
+ console.log('replaced ' + findStr + ' with ' + replaceStr + ' in ' + path);
+ } else {
+ console.log('error replaced strings in ' + path);
+ }
+ })
+}
+
+
+/**
+ * write a file
+ *
+ * @param {String} path
+ * @param {String} content
+ */
+function writeFile(path, content) {
+ fs.writeFile(path, content, 'utf8', function(err) {
+ if (err) {
+ console.log(err);
+ } else {
+ console.log('The file: ' + path + ' was saved');
+ }
+ });
+}
+
+
+/**
+ * read a file
+ *
+ * @param {String} path
+ * @param {Function} callback
+ */
+function readFile(path, callback) {
+ fs.readFile(path, 'utf8', callback);
+}
diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build
new file mode 100644
index 0000000000..74f3ad7f83
--- /dev/null
+++ b/toolkit/components/moz.build
@@ -0,0 +1,108 @@
+# -*- 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/.
+
+# These component dirs are built for all apps (including suite)
+if CONFIG['MOZ_ENABLE_XREMOTE']:
+ DIRS += ['remote']
+
+DIRS += [
+ 'aboutcache',
+ 'aboutcheckerboard',
+ 'aboutmemory',
+ 'aboutperformance',
+ 'addoncompat',
+ 'alerts',
+ 'apppicker',
+ 'asyncshutdown',
+ 'commandlines',
+ 'contentprefs',
+ 'contextualidentity',
+ 'cookie',
+ 'crashmonitor',
+ 'diskspacewatcher',
+ 'downloads',
+ 'extensions',
+ 'exthelper',
+ 'filewatcher',
+ 'finalizationwitness',
+ 'formautofill',
+ 'find',
+ 'gfx',
+ 'jsdownloads',
+ 'lz4',
+ 'mediasniffer',
+ 'microformats',
+ 'mozprotocol',
+ 'osfile',
+ 'parentalcontrols',
+ 'passwordmgr',
+ 'perf',
+ 'perfmonitoring',
+ 'places',
+ 'privatebrowsing',
+ 'processsingleton',
+ 'promiseworker',
+ 'prompts',
+ 'protobuf',
+ 'reader',
+ 'remotebrowserutils',
+ 'reflect',
+ 'securityreporter',
+ 'sqlite',
+ 'startup',
+ 'statusfilter',
+ 'telemetry',
+ 'thumbnails',
+ 'timermanager',
+ 'tooltiptext',
+ 'typeaheadfind',
+ 'utils',
+ 'url-classifier',
+ 'urlformatter',
+ 'viewconfig',
+ 'workerloader',
+ 'xulstore'
+]
+
+if CONFIG['ENABLE_INTL_API']:
+ DIRS += ['mozintl']
+
+if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
+ DIRS += ['narrate', 'viewsource'];
+
+ if CONFIG['NS_PRINTING']:
+ DIRS += ['printing']
+
+if CONFIG['MOZ_CRASHREPORTER']:
+ DIRS += ['crashes']
+
+if CONFIG['BUILD_CTYPES']:
+ DIRS += ['ctypes']
+
+if CONFIG['MOZ_FEEDS']:
+ DIRS += ['feeds']
+
+if CONFIG['MOZ_XUL']:
+ DIRS += ['autocomplete', 'satchel']
+
+if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
+ DIRS += ['filepicker']
+
+if CONFIG['MOZ_TOOLKIT_SEARCH']:
+ DIRS += ['search']
+
+DIRS += ['captivedetect']
+
+if CONFIG['OS_TARGET'] != 'Android':
+ DIRS += ['terminator']
+
+DIRS += ['build']
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
+ EXTRA_COMPONENTS += [
+ 'nsDefaultCLH.js',
+ 'nsDefaultCLH.manifest',
+ ]
diff --git a/toolkit/components/mozintl/MozIntl.cpp b/toolkit/components/mozintl/MozIntl.cpp
new file mode 100644
index 0000000000..9c393c2960
--- /dev/null
+++ b/toolkit/components/mozintl/MozIntl.cpp
@@ -0,0 +1,74 @@
+/* -*- 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 "MozIntl.h"
+#include "jswrapper.h"
+#include "mozilla/ModuleUtils.h"
+
+#define MOZ_MOZINTL_CID \
+ { 0x83f8f991, 0x6b81, 0x4dd8, { 0xa0, 0x93, 0x72, 0x0b, 0xfb, 0x67, 0x4d, 0x38 } }
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(MozIntl, mozIMozIntl)
+
+MozIntl::MozIntl()
+{
+}
+
+MozIntl::~MozIntl()
+{
+}
+
+NS_IMETHODIMP
+MozIntl::AddGetCalendarInfo(JS::Handle<JS::Value> val, JSContext* cx)
+{
+ if (!val.isObject()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JSObject*> realIntlObj(cx, js::CheckedUnwrap(&val.toObject()));
+ if (!realIntlObj) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JSAutoCompartment ac(cx, realIntlObj);
+
+ static const JSFunctionSpec funcs[] = {
+ JS_SELF_HOSTED_FN("getCalendarInfo", "Intl_getCalendarInfo", 1, 0),
+ JS_FS_END
+ };
+
+ if (!JS_DefineFunctions(cx, realIntlObj, funcs)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(MozIntl)
+NS_DEFINE_NAMED_CID(MOZ_MOZINTL_CID);
+
+static const Module::CIDEntry kMozIntlCIDs[] = {
+ { &kMOZ_MOZINTL_CID, false, nullptr, MozIntlConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kMozIntlContracts[] = {
+ { "@mozilla.org/mozintl;1", &kMOZ_MOZINTL_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kMozIntlModule = {
+ mozilla::Module::kVersion,
+ kMozIntlCIDs,
+ kMozIntlContracts,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr
+};
+
+NSMODULE_DEFN(mozMozIntlModule) = &kMozIntlModule;
diff --git a/toolkit/components/mozintl/MozIntl.h b/toolkit/components/mozintl/MozIntl.h
new file mode 100644
index 0000000000..00c10ed191
--- /dev/null
+++ b/toolkit/components/mozintl/MozIntl.h
@@ -0,0 +1,22 @@
+/* -*- 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 "mozIMozIntl.h"
+
+namespace mozilla {
+
+class MozIntl final : public mozIMozIntl
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIMOZINTL
+
+ MozIntl();
+
+private:
+ ~MozIntl();
+};
+
+} // namespace mozilla
diff --git a/toolkit/components/mozintl/moz.build b/toolkit/components/mozintl/moz.build
new file mode 100644
index 0000000000..a92ed50b71
--- /dev/null
+++ b/toolkit/components/mozintl/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'mozIMozIntl.idl',
+]
+
+XPIDL_MODULE = 'mozintl'
+
+SOURCES += [
+ 'MozIntl.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/mozintl/mozIMozIntl.idl b/toolkit/components/mozintl/mozIMozIntl.idl
new file mode 100644
index 0000000000..67be184d4e
--- /dev/null
+++ b/toolkit/components/mozintl/mozIMozIntl.idl
@@ -0,0 +1,12 @@
+/* -*- 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"
+
+[scriptable, uuid(9f9bc42e-54f4-11e6-9aed-4b1429ac0ba0)]
+interface mozIMozIntl : nsISupports
+{
+ [implicit_jscontext] void addGetCalendarInfo(in jsval intlObject);
+};
diff --git a/toolkit/components/mozintl/test/test_mozintl.js b/toolkit/components/mozintl/test/test_mozintl.js
new file mode 100644
index 0000000000..0eca2c67e5
--- /dev/null
+++ b/toolkit/components/mozintl/test/test_mozintl.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ const mozIntl = Components.classes["@mozilla.org/mozintl;1"]
+ .getService(Components.interfaces.mozIMozIntl);
+
+ test_this_global(mozIntl);
+ test_cross_global(mozIntl);
+
+ ok(true);
+}
+
+function test_this_global(mozIntl) {
+ let x = {};
+
+ mozIntl.addGetCalendarInfo(x);
+ equal(x.getCalendarInfo instanceof Function, true);
+ equal(x.getCalendarInfo() instanceof Object, true);
+}
+
+function test_cross_global(mozIntl) {
+ var global = new Components.utils.Sandbox("https://example.com/");
+ var x = global.Object();
+
+ mozIntl.addGetCalendarInfo(x);
+ var waivedX = Components.utils.waiveXrays(x);
+ equal(waivedX.getCalendarInfo instanceof Function, false);
+ equal(waivedX.getCalendarInfo instanceof global.Function, true);
+ equal(waivedX.getCalendarInfo() instanceof Object, false);
+ equal(waivedX.getCalendarInfo() instanceof global.Object, true);
+}
diff --git a/toolkit/components/mozintl/test/xpcshell.ini b/toolkit/components/mozintl/test/xpcshell.ini
new file mode 100644
index 0000000000..8fecd39334
--- /dev/null
+++ b/toolkit/components/mozintl/test/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_mozintl.js]
diff --git a/toolkit/components/mozprotocol/moz.build b/toolkit/components/mozprotocol/moz.build
new file mode 100644
index 0000000000..fa09ade5a4
--- /dev/null
+++ b/toolkit/components/mozprotocol/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/.
+
+EXTRA_COMPONENTS += [
+ 'mozProtocolHandler.js',
+ 'mozProtocolHandler.manifest',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'tests/browser.ini'
+]
diff --git a/toolkit/components/mozprotocol/mozProtocolHandler.js b/toolkit/components/mozprotocol/mozProtocolHandler.js
new file mode 100644
index 0000000000..a92483f6a2
--- /dev/null
+++ b/toolkit/components/mozprotocol/mozProtocolHandler.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/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+function mozProtocolHandler() {
+ XPCOMUtils.defineLazyPreferenceGetter(this, "urlToLoad", "toolkit.mozprotocol.url",
+ "https://www.mozilla.org/protocol");
+}
+
+mozProtocolHandler.prototype = {
+ scheme: "moz",
+ defaultPort: -1,
+ protocolFlags: Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD,
+
+ newURI(spec, charset, base) {
+ let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
+ if (base) {
+ uri.spec = base.resolve(spec);
+ } else {
+ uri.spec = spec;
+ }
+ return uri;
+ },
+
+ newChannel2(uri, loadInfo) {
+ let realURL = NetUtil.newURI(this.urlToLoad);
+ let channel = Services.io.newChannelFromURIWithLoadInfo(realURL, loadInfo)
+ channel.loadFlags |= Ci.nsIChannel.LOAD_REPLACE;
+ return channel;
+ },
+
+ newChannel(uri) {
+ return this.newChannel(uri, null);
+ },
+
+ classID: Components.ID("{47a45e5f-691e-4799-8686-14f8d3fc0f8c}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([mozProtocolHandler]);
diff --git a/toolkit/components/mozprotocol/mozProtocolHandler.manifest b/toolkit/components/mozprotocol/mozProtocolHandler.manifest
new file mode 100644
index 0000000000..bbfdf780af
--- /dev/null
+++ b/toolkit/components/mozprotocol/mozProtocolHandler.manifest
@@ -0,0 +1,2 @@
+component {47a45e5f-691e-4799-8686-14f8d3fc0f8c} mozProtocolHandler.js
+contract @mozilla.org/network/protocol;1?name=moz {47a45e5f-691e-4799-8686-14f8d3fc0f8c}
diff --git a/toolkit/components/mozprotocol/tests/browser.ini b/toolkit/components/mozprotocol/tests/browser.ini
new file mode 100644
index 0000000000..168882a671
--- /dev/null
+++ b/toolkit/components/mozprotocol/tests/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+[browser_mozprotocol.js]
+support-files =
+ mozprotocol.html
diff --git a/toolkit/components/mozprotocol/tests/browser_mozprotocol.js b/toolkit/components/mozprotocol/tests/browser_mozprotocol.js
new file mode 100644
index 0000000000..795ce8a08c
--- /dev/null
+++ b/toolkit/components/mozprotocol/tests/browser_mozprotocol.js
@@ -0,0 +1,14 @@
+// Check that entering moz://a into the address bar directs us to a new url
+add_task(function*() {
+ let path = getRootDirectory(gTestPath).substring("chrome://mochitests/content/".length);
+ yield SpecialPowers.pushPrefEnv({
+ set: [["toolkit.mozprotocol.url", `https://example.com/${path}mozprotocol.html`]],
+ });
+
+ yield BrowserTestUtils.withNewTab("about:blank", function*() {
+ gBrowser.loadURI("moz://a");
+ yield BrowserTestUtils.waitForLocationChange(gBrowser,
+ `https://example.com/${path}mozprotocol.html`);
+ ok(true, "Made it to the expected page");
+ });
+});
diff --git a/toolkit/components/mozprotocol/tests/mozprotocol.html b/toolkit/components/mozprotocol/tests/mozprotocol.html
new file mode 100644
index 0000000000..3d6549e00f
--- /dev/null
+++ b/toolkit/components/mozprotocol/tests/mozprotocol.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p><a id="link" href="moz://a">Test</a></p>
+</body>
+</html>
diff --git a/toolkit/components/narrate/.eslintrc.js b/toolkit/components/narrate/.eslintrc.js
new file mode 100644
index 0000000000..b2d443575c
--- /dev/null
+++ b/toolkit/components/narrate/.eslintrc.js
@@ -0,0 +1,94 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../.eslintrc.js"
+ ],
+
+ "globals": {
+ "Components": true,
+ "dump": true,
+ "Iterator": true
+ },
+
+ "env": { "browser": true },
+
+ "rules": {
+ // Mozilla stuff
+ "mozilla/no-aArgs": "warn",
+ "mozilla/reject-importGlobalProperties": "warn",
+ "mozilla/var-only-at-top-level": "warn",
+
+ "block-scoped-var": "error",
+ "brace-style": ["warn", "1tbs", {"allowSingleLine": false}],
+ "camelcase": "warn",
+ "comma-dangle": "off",
+ "comma-spacing": ["warn", {"before": false, "after": true}],
+ "comma-style": ["warn", "last"],
+ "complexity": "warn",
+ "consistent-return": "error",
+ "curly": "error",
+ "dot-location": ["warn", "property"],
+ "dot-notation": "error",
+ "eol-last": "error",
+ "generator-star-spacing": ["warn", "after"],
+ "indent": ["warn", 2, {"SwitchCase": 1}],
+ "key-spacing": ["warn", {"beforeColon": false, "afterColon": true}],
+ "keyword-spacing": "warn",
+ "max-len": ["warn", 80, 2, {"ignoreUrls": true}],
+ "max-nested-callbacks": ["error", 3],
+ "new-cap": ["error", {"capIsNew": false}],
+ "new-parens": "error",
+ "no-array-constructor": "error",
+ "no-cond-assign": "error",
+ "no-control-regex": "error",
+ "no-debugger": "error",
+ "no-delete-var": "error",
+ "no-dupe-args": "error",
+ "no-dupe-keys": "error",
+ "no-duplicate-case": "error",
+ "no-else-return": "error",
+ "no-eval": "error",
+ "no-extend-native": "error",
+ "no-extra-bind": "error",
+ "no-extra-boolean-cast": "error",
+ "no-extra-semi": "warn",
+ "no-fallthrough": "error",
+ "no-inline-comments": "warn",
+ "no-lonely-if": "error",
+ "no-mixed-spaces-and-tabs": "error",
+ "no-multi-spaces": "warn",
+ "no-multi-str": "warn",
+ "no-multiple-empty-lines": ["warn", {"max": 1}],
+ "no-native-reassign": "error",
+ "no-nested-ternary": "error",
+ "no-redeclare": "error",
+ "no-return-assign": "error",
+ "no-self-compare": "error",
+ "no-sequences": "error",
+ "no-shadow": "warn",
+ "no-shadow-restricted-names": "error",
+ "no-spaced-func": "warn",
+ "no-throw-literal": "error",
+ "no-trailing-spaces": "error",
+ "no-undef": "error",
+ "no-unneeded-ternary": "error",
+ "no-unreachable": "error",
+ "no-unused-vars": "error",
+ "no-with": "error",
+ "padded-blocks": ["warn", "never"],
+ "quotes": ["warn", "double", "avoid-escape"],
+ "semi": ["warn", "always"],
+ "semi-spacing": ["warn", {"before": false, "after": true}],
+ "space-before-blocks": ["warn", "always"],
+ "space-before-function-paren": ["warn", "never"],
+ "space-in-parens": ["warn", "never"],
+ "space-infix-ops": ["warn", {"int32Hint": true}],
+ "space-unary-ops": ["warn", { "words": true, "nonwords": false }],
+ "spaced-comment": ["warn", "always"],
+ "strict": ["error", "global"],
+ "use-isnan": "error",
+ "valid-typeof": "error",
+ "yoda": "error"
+ }
+};
diff --git a/toolkit/components/narrate/NarrateControls.jsm b/toolkit/components/narrate/NarrateControls.jsm
new file mode 100644
index 0000000000..7d8794b18f
--- /dev/null
+++ b/toolkit/components/narrate/NarrateControls.jsm
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/narrate/VoiceSelect.jsm");
+Cu.import("resource://gre/modules/narrate/Narrator.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AsyncPrefs.jsm");
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
+
+this.EXPORTED_SYMBOLS = ["NarrateControls"];
+
+var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties");
+
+function NarrateControls(mm, win) {
+ this._mm = mm;
+ this._winRef = Cu.getWeakReference(win);
+
+ win.addEventListener("unload", this);
+
+ // Append content style sheet in document head
+ let style = win.document.createElement("link");
+ style.rel = "stylesheet";
+ style.href = "chrome://global/skin/narrate.css";
+ win.document.head.appendChild(style);
+
+ function localize(pieces, ...substitutions) {
+ let result = pieces[0];
+ for (let i = 0; i < substitutions.length; ++i) {
+ result += gStrings.GetStringFromName(substitutions[i]) + pieces[i + 1];
+ }
+ return result;
+ }
+
+ let dropdown = win.document.createElement("ul");
+ dropdown.className = "dropdown";
+ dropdown.id = "narrate-dropdown";
+ // We need inline svg here for the animation to work (bug 908634 & 1190881).
+ // The style animation can't be scoped (bug 830056).
+ dropdown.innerHTML =
+ localize`<style scoped>
+ @import url("chrome://global/skin/narrateControls.css");
+ </style>
+ <li>
+ <button class="dropdown-toggle button" id="narrate-toggle"
+ title="${"narrate"}" hidden>
+ <svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="24" height="24" viewBox="0 0 24 24">
+ <style>
+ @keyframes grow {
+ 0% { transform: scaleY(1); }
+ 15% { transform: scaleY(1.5); }
+ 15% { transform: scaleY(1.5); }
+ 30% { transform: scaleY(1); }
+ 100% { transform: scaleY(1); }
+ }
+
+ #waveform > rect {
+ fill: #808080;
+ }
+
+ .speaking #waveform > rect {
+ fill: #58bf43;
+ transform-box: fill-box;
+ transform-origin: 50% 50%;
+ animation-name: grow;
+ animation-duration: 1750ms;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+ }
+
+ #waveform > rect:nth-child(2) { animation-delay: 250ms; }
+ #waveform > rect:nth-child(3) { animation-delay: 500ms; }
+ #waveform > rect:nth-child(4) { animation-delay: 750ms; }
+ #waveform > rect:nth-child(5) { animation-delay: 1000ms; }
+ #waveform > rect:nth-child(6) { animation-delay: 1250ms; }
+ #waveform > rect:nth-child(7) { animation-delay: 1500ms; }
+
+ </style>
+ <g id="waveform">
+ <rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" />
+ <rect x="4" y="5" width="2" height="14" rx=".5" ry=".5" />
+ <rect x="7" y="8" width="2" height="8" rx=".5" ry=".5" />
+ <rect x="10" y="4" width="2" height="16" rx=".5" ry=".5" />
+ <rect x="13" y="2" width="2" height="20" rx=".5" ry=".5" />
+ <rect x="16" y="4" width="2" height="16" rx=".5" ry=".5" />
+ <rect x="19" y="7" width="2" height="10" rx=".5" ry=".5" />
+ </g>
+ </svg>
+ </button>
+ </li>
+ <li class="dropdown-popup">
+ <div id="narrate-control" class="narrate-row">
+ <button disabled id="narrate-skip-previous"
+ title="${"back"}"></button>
+ <button id="narrate-start-stop" title="${"start"}"></button>
+ <button disabled id="narrate-skip-next"
+ title="${"forward"}"></button>
+ </div>
+ <div id="narrate-rate" class="narrate-row">
+ <input id="narrate-rate-input" value="0" title="${"speed"}"
+ step="5" max="100" min="-100" type="range">
+ </div>
+ <div id="narrate-voices" class="narrate-row"></div>
+ <div class="dropdown-arrow"></div>
+ </li>`;
+
+ this.narrator = new Narrator(win);
+
+ let branch = Services.prefs.getBranch("narrate.");
+ let selectLabel = gStrings.GetStringFromName("selectvoicelabel");
+ this.voiceSelect = new VoiceSelect(win, selectLabel);
+ this.voiceSelect.element.addEventListener("change", this);
+ this.voiceSelect.element.id = "voice-select";
+ win.speechSynthesis.addEventListener("voiceschanged", this);
+ dropdown.querySelector("#narrate-voices").appendChild(
+ this.voiceSelect.element);
+
+ dropdown.addEventListener("click", this, true);
+
+ let rateRange = dropdown.querySelector("#narrate-rate > input");
+ rateRange.addEventListener("change", this);
+
+ // The rate is stored as an integer.
+ rateRange.value = branch.getIntPref("rate");
+
+ this._setupVoices();
+
+ let tb = win.document.getElementById("reader-toolbar");
+ tb.appendChild(dropdown);
+}
+
+NarrateControls.prototype = {
+ handleEvent: function(evt) {
+ switch (evt.type) {
+ case "change":
+ if (evt.target.id == "narrate-rate-input") {
+ this._onRateInput(evt);
+ } else {
+ this._onVoiceChange();
+ }
+ break;
+ case "click":
+ this._onButtonClick(evt);
+ break;
+ case "voiceschanged":
+ this._setupVoices();
+ break;
+ case "unload":
+ if (this.narrator.speaking) {
+ TelemetryStopwatch.finish("NARRATE_CONTENT_SPEAKTIME_MS", this);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Returns true if synth voices are available.
+ */
+ _setupVoices: function() {
+ return this.narrator.languagePromise.then(language => {
+ this.voiceSelect.clear();
+ let win = this._win;
+ let voicePrefs = this._getVoicePref();
+ let selectedVoice = voicePrefs[language || "default"];
+ let comparer = win.Intl ?
+ (new Intl.Collator()).compare : (a, b) => a.localeCompare(b);
+ let filter = !Services.prefs.getBoolPref("narrate.filter-voices");
+ let options = win.speechSynthesis.getVoices().filter(v => {
+ return filter || !language || v.lang.split("-")[0] == language;
+ }).map(v => {
+ return {
+ label: this._createVoiceLabel(v),
+ value: v.voiceURI,
+ selected: selectedVoice == v.voiceURI
+ };
+ }).sort((a, b) => comparer(a.label, b.label));
+
+ if (options.length) {
+ options.unshift({
+ label: gStrings.GetStringFromName("defaultvoice"),
+ value: "automatic",
+ selected: selectedVoice == "automatic"
+ });
+ this.voiceSelect.addOptions(options);
+ }
+
+ let narrateToggle = win.document.getElementById("narrate-toggle");
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "NARRATE_CONTENT_BY_LANGUAGE_2");
+ let initial = !this._voicesInitialized;
+ this._voicesInitialized = true;
+
+ if (initial) {
+ histogram.add(language, 0);
+ }
+
+ if (options.length && narrateToggle.hidden) {
+ // About to show for the first time..
+ histogram.add(language, 1);
+ }
+
+ // We disable this entire feature if there are no available voices.
+ narrateToggle.hidden = !options.length;
+ });
+ },
+
+ _getVoicePref: function() {
+ let voicePref = Services.prefs.getCharPref("narrate.voice");
+ try {
+ return JSON.parse(voicePref);
+ } catch (e) {
+ return { default: voicePref };
+ }
+ },
+
+ _onRateInput: function(evt) {
+ AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10));
+ this.narrator.setRate(this._convertRate(evt.target.value));
+ },
+
+ _onVoiceChange: function() {
+ let voice = this.voice;
+ this.narrator.setVoice(voice);
+ this.narrator.languagePromise.then(language => {
+ if (language) {
+ let voicePref = this._getVoicePref();
+ voicePref[language || "default"] = voice;
+ AsyncPrefs.set("narrate.voice", JSON.stringify(voicePref));
+ }
+ });
+ },
+
+ _onButtonClick: function(evt) {
+ switch (evt.target.id) {
+ case "narrate-skip-previous":
+ this.narrator.skipPrevious();
+ break;
+ case "narrate-skip-next":
+ this.narrator.skipNext();
+ break;
+ case "narrate-start-stop":
+ if (this.narrator.speaking) {
+ this.narrator.stop();
+ } else {
+ this._updateSpeechControls(true);
+ let options = { rate: this.rate, voice: this.voice };
+ this.narrator.start(options).then(() => {
+ this._updateSpeechControls(false);
+ }, err => {
+ Cu.reportError(`Narrate failed: ${err}.`);
+ this._updateSpeechControls(false);
+ });
+ }
+ break;
+ }
+ },
+
+ _updateSpeechControls: function(speaking) {
+ let dropdown = this._doc.getElementById("narrate-dropdown");
+ dropdown.classList.toggle("keep-open", speaking);
+ dropdown.classList.toggle("speaking", speaking);
+
+ let startStopButton = this._doc.getElementById("narrate-start-stop");
+ startStopButton.title =
+ gStrings.GetStringFromName(speaking ? "stop" : "start");
+
+ this._doc.getElementById("narrate-skip-previous").disabled = !speaking;
+ this._doc.getElementById("narrate-skip-next").disabled = !speaking;
+
+ if (speaking) {
+ TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this);
+ } else {
+ TelemetryStopwatch.finish("NARRATE_CONTENT_SPEAKTIME_MS", this);
+ }
+ },
+
+ _createVoiceLabel: function(voice) {
+ // This is a highly imperfect method of making human-readable labels
+ // for system voices. Because each platform has a different naming scheme
+ // for voices, we use a different method for each platform.
+ switch (Services.appinfo.OS) {
+ case "WINNT":
+ // On windows the language is included in the name, so just use the name
+ return voice.name;
+ case "Linux":
+ // On Linux, the name is usually the unlocalized language name.
+ // Use a localized language name, and have the language tag in
+ // parenthisis. This is to avoid six languages called "English".
+ return gStrings.formatStringFromName("voiceLabel",
+ [this._getLanguageName(voice.lang) || voice.name, voice.lang], 2);
+ default:
+ // On Mac the language is not included in the name, find a localized
+ // language name or show the tag if none exists.
+ // This is the ideal naming scheme so it is also the "default".
+ return gStrings.formatStringFromName("voiceLabel",
+ [voice.name, this._getLanguageName(voice.lang) || voice.lang], 2);
+ }
+ },
+
+ _getLanguageName: function(lang) {
+ if (!this._langStrings) {
+ this._langStrings = Services.strings.createBundle(
+ "chrome://global/locale/languageNames.properties ");
+ }
+
+ try {
+ // language tags will be lower case ascii between 2 and 3 characters long.
+ return this._langStrings.GetStringFromName(lang.match(/^[a-z]{2,3}/)[0]);
+ } catch (e) {
+ return "";
+ }
+ },
+
+ _convertRate: function(rate) {
+ // We need to convert a relative percentage value to a fraction rate value.
+ // eg. -100 is half the speed, 100 is twice the speed in percentage,
+ // 0.5 is half the speed and 2 is twice the speed in fractions.
+ return Math.pow(Math.abs(rate / 100) + 1, rate < 0 ? -1 : 1);
+ },
+
+ get _win() {
+ return this._winRef.get();
+ },
+
+ get _doc() {
+ return this._win.document;
+ },
+
+ get rate() {
+ return this._convertRate(
+ this._doc.getElementById("narrate-rate-input").value);
+ },
+
+ get voice() {
+ return this.voiceSelect.value;
+ }
+};
diff --git a/toolkit/components/narrate/Narrator.jsm b/toolkit/components/narrate/Narrator.jsm
new file mode 100644
index 0000000000..ade06510e0
--- /dev/null
+++ b/toolkit/components/narrate/Narrator.jsm
@@ -0,0 +1,464 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
+ "resource:///modules/translation/LanguageDetector.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "Narrator" ];
+
+// Maximum time into paragraph when pressing "skip previous" will go
+// to previous paragraph and not the start of current one.
+const PREV_THRESHOLD = 2000;
+// All text-related style rules that we should copy over to the highlight node.
+const kTextStylesRules = ["font-family", "font-kerning", "font-size",
+ "font-size-adjust", "font-stretch", "font-variant", "font-weight",
+ "line-height", "letter-spacing", "text-orientation",
+ "text-transform", "word-spacing"];
+
+function Narrator(win) {
+ this._winRef = Cu.getWeakReference(win);
+ this._inTest = Services.prefs.getBoolPref("narrate.test");
+ this._speechOptions = {};
+ this._startTime = 0;
+ this._stopped = false;
+
+ this.languagePromise = new Promise(resolve => {
+ let detect = () => {
+ win.document.removeEventListener("AboutReaderContentReady", detect);
+ let sampleText = this._doc.getElementById(
+ "moz-reader-content").textContent.substring(0, 60 * 1024);
+ LanguageDetector.detectLanguage(sampleText).then(result => {
+ resolve(result.confident ? result.language : null);
+ });
+ };
+
+ if (win.document.body.classList.contains("loaded")) {
+ detect();
+ } else {
+ win.document.addEventListener("AboutReaderContentReady", detect);
+ }
+ });
+}
+
+Narrator.prototype = {
+ get _doc() {
+ return this._winRef.get().document;
+ },
+
+ get _win() {
+ return this._winRef.get();
+ },
+
+ get _treeWalker() {
+ if (!this._treeWalkerRef) {
+ let wu = this._win.QueryInterface(
+ Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ let nf = this._win.NodeFilter;
+
+ let filter = {
+ _matches: new Set(),
+
+ // We want high-level elements that have non-empty text nodes.
+ // For example, paragraphs. But nested anchors and other elements
+ // are not interesting since their text already appears in their
+ // parent's textContent.
+ acceptNode: function(node) {
+ if (this._matches.has(node.parentNode)) {
+ // Reject sub-trees of accepted nodes.
+ return nf.FILTER_REJECT;
+ }
+
+ if (!/\S/.test(node.textContent)) {
+ // Reject nodes with no text.
+ return nf.FILTER_REJECT;
+ }
+
+ let bb = wu.getBoundsWithoutFlushing(node);
+ if (!bb.width || !bb.height) {
+ // Skip non-rendered nodes. We don't reject because a zero-sized
+ // container can still have visible, "overflowed", content.
+ return nf.FILTER_SKIP;
+ }
+
+ for (let c = node.firstChild; c; c = c.nextSibling) {
+ if (c.nodeType == c.TEXT_NODE && /\S/.test(c.textContent)) {
+ // If node has a non-empty text child accept it.
+ this._matches.add(node);
+ return nf.FILTER_ACCEPT;
+ }
+ }
+
+ return nf.FILTER_SKIP;
+ }
+ };
+
+ this._treeWalkerRef = new WeakMap();
+
+ // We can't hold a weak reference on the treewalker, because there
+ // are no other strong references, and it will be GC'ed. Instead,
+ // we rely on the window's lifetime and use it as a weak reference.
+ this._treeWalkerRef.set(this._win,
+ this._doc.createTreeWalker(this._doc.getElementById("container"),
+ nf.SHOW_ELEMENT, filter, false));
+ }
+
+ return this._treeWalkerRef.get(this._win);
+ },
+
+ get _timeIntoParagraph() {
+ let rv = Date.now() - this._startTime;
+ return rv;
+ },
+
+ get speaking() {
+ return this._win.speechSynthesis.speaking ||
+ this._win.speechSynthesis.pending;
+ },
+
+ _getVoice: function(voiceURI) {
+ if (!this._voiceMap || !this._voiceMap.has(voiceURI)) {
+ this._voiceMap = new Map(
+ this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v]));
+ }
+
+ return this._voiceMap.get(voiceURI);
+ },
+
+ _isParagraphInView: function(paragraph) {
+ if (!paragraph) {
+ return false;
+ }
+
+ let bb = paragraph.getBoundingClientRect();
+ return bb.top >= 0 && bb.top < this._win.innerHeight;
+ },
+
+ _sendTestEvent: function(eventType, detail) {
+ let win = this._win;
+ win.dispatchEvent(new win.CustomEvent(eventType,
+ { detail: Cu.cloneInto(detail, win.document) }));
+ },
+
+ _speakInner: function() {
+ this._win.speechSynthesis.cancel();
+ let tw = this._treeWalker;
+ let paragraph = tw.currentNode;
+ if (paragraph == tw.root) {
+ this._sendTestEvent("paragraphsdone", {});
+ return Promise.resolve();
+ }
+
+ let utterance = new this._win.SpeechSynthesisUtterance(
+ paragraph.textContent);
+ utterance.rate = this._speechOptions.rate;
+ if (this._speechOptions.voice) {
+ utterance.voice = this._speechOptions.voice;
+ } else {
+ utterance.lang = this._speechOptions.lang;
+ }
+
+ this._startTime = Date.now();
+
+ let highlighter = new Highlighter(paragraph);
+
+ if (this._inTest) {
+ let onTestSynthEvent = e => {
+ if (e.detail.type == "boundary") {
+ let args = Object.assign({ utterance }, e.detail.args);
+ let evt = new this._win.SpeechSynthesisEvent(e.detail.type, args);
+ utterance.dispatchEvent(evt);
+ }
+ };
+
+ let removeListeners = () => {
+ this._win.removeEventListener("testsynthevent", onTestSynthEvent);
+ };
+
+ this._win.addEventListener("testsynthevent", onTestSynthEvent);
+ utterance.addEventListener("end", removeListeners);
+ utterance.addEventListener("error", removeListeners);
+ }
+
+ return new Promise((resolve, reject) => {
+ utterance.addEventListener("start", () => {
+ paragraph.classList.add("narrating");
+ let bb = paragraph.getBoundingClientRect();
+ if (bb.top < 0 || bb.bottom > this._win.innerHeight) {
+ paragraph.scrollIntoView({ behavior: "smooth", block: "start"});
+ }
+
+ if (this._inTest) {
+ this._sendTestEvent("paragraphstart", {
+ voice: utterance.chosenVoiceURI,
+ rate: utterance.rate,
+ paragraph: paragraph.textContent,
+ tag: paragraph.localName
+ });
+ }
+ });
+
+ utterance.addEventListener("end", () => {
+ if (!this._win) {
+ // page got unloaded, don't do anything.
+ return;
+ }
+
+ highlighter.remove();
+ paragraph.classList.remove("narrating");
+ this._startTime = 0;
+ if (this._inTest) {
+ this._sendTestEvent("paragraphend", {});
+ }
+
+ if (this._stopped) {
+ // User pressed stopped.
+ resolve();
+ } else {
+ tw.currentNode = tw.nextNode() || tw.root;
+ this._speakInner().then(resolve, reject);
+ }
+ });
+
+ utterance.addEventListener("error", () => {
+ reject("speech synthesis failed");
+ });
+
+ utterance.addEventListener("boundary", e => {
+ if (e.name != "word") {
+ // We are only interested in word boundaries for now.
+ return;
+ }
+
+ // Match non-whitespace. This isn't perfect, but the most universal
+ // solution for now.
+ let reWordBoundary = /\S+/g;
+ // Match the first word from the boundary event offset.
+ reWordBoundary.lastIndex = e.charIndex;
+ let firstIndex = reWordBoundary.exec(paragraph.textContent);
+ if (firstIndex) {
+ highlighter.highlight(firstIndex.index, reWordBoundary.lastIndex);
+ if (this._inTest) {
+ this._sendTestEvent("wordhighlight", {
+ start: firstIndex.index,
+ end: reWordBoundary.lastIndex
+ });
+ }
+ }
+ });
+
+ this._win.speechSynthesis.speak(utterance);
+ });
+ },
+
+ start: function(speechOptions) {
+ this._speechOptions = {
+ rate: speechOptions.rate,
+ voice: this._getVoice(speechOptions.voice)
+ };
+
+ this._stopped = false;
+ return this.languagePromise.then(language => {
+ if (!this._speechOptions.voice) {
+ this._speechOptions.lang = language;
+ }
+
+ let tw = this._treeWalker;
+ if (!this._isParagraphInView(tw.currentNode)) {
+ tw.currentNode = tw.root;
+ while (tw.nextNode()) {
+ if (this._isParagraphInView(tw.currentNode)) {
+ break;
+ }
+ }
+ }
+ if (tw.currentNode == tw.root) {
+ tw.nextNode();
+ }
+
+ return this._speakInner();
+ });
+ },
+
+ stop: function() {
+ this._stopped = true;
+ this._win.speechSynthesis.cancel();
+ },
+
+ skipNext: function() {
+ this._win.speechSynthesis.cancel();
+ },
+
+ skipPrevious: function() {
+ this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1);
+ },
+
+ setRate: function(rate) {
+ this._speechOptions.rate = rate;
+ /* repeat current paragraph */
+ this._goBackParagraphs(1);
+ },
+
+ setVoice: function(voice) {
+ this._speechOptions.voice = this._getVoice(voice);
+ /* repeat current paragraph */
+ this._goBackParagraphs(1);
+ },
+
+ _goBackParagraphs: function(count) {
+ let tw = this._treeWalker;
+ for (let i = 0; i < count; i++) {
+ if (!tw.previousNode()) {
+ tw.currentNode = tw.root;
+ }
+ }
+ this._win.speechSynthesis.cancel();
+ }
+};
+
+/**
+ * The Highlighter class is used to highlight a range of text in a container.
+ *
+ * @param {nsIDOMElement} container a text container
+ */
+function Highlighter(container) {
+ this.container = container;
+}
+
+Highlighter.prototype = {
+ /**
+ * Highlight the range within offsets relative to the container.
+ *
+ * @param {Number} startOffset the start offset
+ * @param {Number} endOffset the end offset
+ */
+ highlight: function(startOffset, endOffset) {
+ let containerRect = this.container.getBoundingClientRect();
+ let range = this._getRange(startOffset, endOffset);
+ let rangeRects = range.getClientRects();
+ let win = this.container.ownerDocument.defaultView;
+ let computedStyle = win.getComputedStyle(range.endContainer.parentNode);
+ let nodes = this._getFreshHighlightNodes(rangeRects.length);
+
+ let textStyle = {};
+ for (let textStyleRule of kTextStylesRules) {
+ textStyle[textStyleRule] = computedStyle[textStyleRule];
+ }
+
+ for (let i = 0; i < rangeRects.length; i++) {
+ let r = rangeRects[i];
+ let node = nodes[i];
+
+ let style = Object.assign({
+ "top": `${r.top - containerRect.top + r.height / 2}px`,
+ "left": `${r.left - containerRect.left + r.width / 2}px`,
+ "width": `${r.width}px`,
+ "height": `${r.height}px`
+ }, textStyle);
+
+ // Enables us to vary the CSS transition on a line change.
+ node.classList.toggle("newline", style.top != node.dataset.top);
+ node.dataset.top = style.top;
+
+ // Enables CSS animations.
+ node.classList.remove("animate");
+ win.requestAnimationFrame(() => {
+ node.classList.add("animate");
+ });
+
+ // Enables alternative word display with a CSS pseudo-element.
+ node.dataset.word = range.toString();
+
+ // Apply style
+ node.style = Object.entries(style).map(
+ s => `${s[0]}: ${s[1]};`).join(" ");
+ }
+ },
+
+ /**
+ * Releases reference to container and removes all highlight nodes.
+ */
+ remove: function() {
+ for (let node of this._nodes) {
+ node.remove();
+ }
+
+ this.container = null;
+ },
+
+ /**
+ * Returns specified amount of highlight nodes. Creates new ones if necessary
+ * and purges any additional nodes that are not needed.
+ *
+ * @param {Number} count number of nodes needed
+ */
+ _getFreshHighlightNodes: function(count) {
+ let doc = this.container.ownerDocument;
+ let nodes = Array.from(this._nodes);
+
+ // Remove nodes we don't need anymore (nodes.length - count > 0).
+ for (let toRemove = 0; toRemove < nodes.length - count; toRemove++) {
+ nodes.shift().remove();
+ }
+
+ // Add additional nodes if we need them (count - nodes.length > 0).
+ for (let toAdd = 0; toAdd < count - nodes.length; toAdd++) {
+ let node = doc.createElement("div");
+ node.className = "narrate-word-highlight";
+ this.container.appendChild(node);
+ nodes.push(node);
+ }
+
+ return nodes;
+ },
+
+ /**
+ * Create and return a range object with the start and end offsets relative
+ * to the container node.
+ *
+ * @param {Number} startOffset the start offset
+ * @param {Number} endOffset the end offset
+ */
+ _getRange: function(startOffset, endOffset) {
+ let doc = this.container.ownerDocument;
+ let i = 0;
+ let treeWalker = doc.createTreeWalker(
+ this.container, doc.defaultView.NodeFilter.SHOW_TEXT);
+ let node = treeWalker.nextNode();
+
+ function _findNodeAndOffset(offset) {
+ do {
+ let length = node.data.length;
+ if (offset >= i && offset <= i + length) {
+ return [node, offset - i];
+ }
+ i += length;
+ } while ((node = treeWalker.nextNode()));
+
+ // Offset is out of bounds, return last offset of last node.
+ node = treeWalker.lastChild();
+ return [node, node.data.length];
+ }
+
+ let range = doc.createRange();
+ range.setStart(..._findNodeAndOffset(startOffset));
+ range.setEnd(..._findNodeAndOffset(endOffset));
+
+ return range;
+ },
+
+ /*
+ * Get all existing highlight nodes for container.
+ */
+ get _nodes() {
+ return this.container.querySelectorAll(".narrate-word-highlight");
+ }
+};
diff --git a/toolkit/components/narrate/VoiceSelect.jsm b/toolkit/components/narrate/VoiceSelect.jsm
new file mode 100644
index 0000000000..b283a06b39
--- /dev/null
+++ b/toolkit/components/narrate/VoiceSelect.jsm
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["VoiceSelect"];
+
+function VoiceSelect(win, label) {
+ this._winRef = Cu.getWeakReference(win);
+
+ let element = win.document.createElement("div");
+ element.classList.add("voiceselect");
+ element.innerHTML =
+ `<button class="select-toggle" aria-controls="voice-options">
+ <span class="label">${label}</span> <span class="current-voice"></span>
+ </button>
+ <div class="options" id="voice-options" role="listbox"></div>`;
+
+ this._elementRef = Cu.getWeakReference(element);
+
+ let button = this.selectToggle;
+ button.addEventListener("click", this);
+ button.addEventListener("keypress", this);
+
+ let listbox = this.listbox;
+ listbox.addEventListener("click", this);
+ listbox.addEventListener("mousemove", this);
+ listbox.addEventListener("keypress", this);
+ listbox.addEventListener("wheel", this, true);
+
+ win.addEventListener("resize", () => {
+ this._updateDropdownHeight();
+ });
+}
+
+VoiceSelect.prototype = {
+ add: function(label, value) {
+ let option = this._doc.createElement("button");
+ option.dataset.value = value;
+ option.classList.add("option");
+ option.tabIndex = "-1";
+ option.setAttribute("role", "option");
+ option.textContent = label;
+ this.listbox.appendChild(option);
+ return option;
+ },
+
+ addOptions: function(options) {
+ let selected = null;
+ for (let option of options) {
+ if (option.selected) {
+ selected = this.add(option.label, option.value);
+ } else {
+ this.add(option.label, option.value);
+ }
+ }
+
+ this._select(selected || this.options[0], true);
+ },
+
+ clear: function() {
+ this.listbox.innerHTML = "";
+ },
+
+ toggleList: function(force, focus = true) {
+ if (this.element.classList.toggle("open", force)) {
+ if (focus) {
+ (this.selected || this.options[0]).focus();
+ }
+
+ this._updateDropdownHeight(true);
+ this.listbox.setAttribute("aria-expanded", true);
+ this._win.addEventListener("focus", this, true);
+ } else {
+ if (focus) {
+ this.element.querySelector(".select-toggle").focus();
+ }
+
+ this.listbox.setAttribute("aria-expanded", false);
+ this._win.removeEventListener("focus", this, true);
+ }
+ },
+
+ handleEvent: function(evt) {
+ let target = evt.target;
+
+ switch (evt.type) {
+ case "click":
+ if (target.classList.contains("option")) {
+ if (!target.classList.contains("selected")) {
+ this.selected = target;
+ }
+
+ this.toggleList(false);
+ } else if (target.classList.contains("select-toggle")) {
+ this.toggleList();
+ }
+ break;
+
+ case "mousemove":
+ this.listbox.classList.add("hovering");
+ break;
+
+ case "keypress":
+ if (target.classList.contains("select-toggle")) {
+ if (evt.altKey) {
+ this.toggleList(true);
+ } else {
+ this._keyPressedButton(evt);
+ }
+ } else {
+ this.listbox.classList.remove("hovering");
+ this._keyPressedInBox(evt);
+ }
+ break;
+
+ case "wheel":
+ // Don't let wheel events bubble to document. It will scroll the page
+ // and close the entire narrate dialog.
+ evt.stopPropagation();
+ break;
+
+ case "focus":
+ if (!target.closest(".voiceselect")) {
+ this.toggleList(false, false);
+ }
+ break;
+ }
+ },
+
+ _getPagedOption: function(option, up) {
+ let height = elem => elem.getBoundingClientRect().height;
+ let listboxHeight = height(this.listbox);
+
+ let next = option;
+ for (let delta = 0; delta < listboxHeight; delta += height(next)) {
+ let sibling = up ? next.previousElementSibling : next.nextElementSibling;
+ if (!sibling) {
+ break;
+ }
+
+ next = sibling;
+ }
+
+ return next;
+ },
+
+ _keyPressedButton: function(evt) {
+ if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
+ this.toggleList(true);
+ return;
+ }
+
+ let toSelect;
+ switch (evt.key) {
+ case "PageUp":
+ case "ArrowUp":
+ toSelect = this.selected.previousElementSibling;
+ break;
+ case "PageDown":
+ case "ArrowDown":
+ toSelect = this.selected.nextElementSibling;
+ break;
+ case "Home":
+ toSelect = this.selected.parentNode.firstElementChild;
+ break;
+ case "End":
+ toSelect = this.selected.parentNode.lastElementChild;
+ break;
+ }
+
+ if (toSelect && toSelect.classList.contains("option")) {
+ evt.preventDefault();
+ this.selected = toSelect;
+ }
+ },
+
+ _keyPressedInBox: function(evt) {
+ let toFocus;
+ let cur = this._doc.activeElement;
+
+ switch (evt.key) {
+ case "ArrowUp":
+ toFocus = cur.previousElementSibling || this.listbox.lastElementChild;
+ break;
+ case "ArrowDown":
+ toFocus = cur.nextElementSibling || this.listbox.firstElementChild;
+ break;
+ case "PageUp":
+ toFocus = this._getPagedOption(cur, true);
+ break;
+ case "PageDown":
+ toFocus = this._getPagedOption(cur, false);
+ break;
+ case "Home":
+ toFocus = cur.parentNode.firstElementChild;
+ break;
+ case "End":
+ toFocus = cur.parentNode.lastElementChild;
+ break;
+ case "Escape":
+ this.toggleList(false);
+ break;
+ }
+
+ if (toFocus && toFocus.classList.contains("option")) {
+ evt.preventDefault();
+ toFocus.focus();
+ }
+ },
+
+ _select: function(option, suppressEvent = false) {
+ let oldSelected = this.selected;
+ if (oldSelected) {
+ oldSelected.removeAttribute("aria-selected");
+ oldSelected.classList.remove("selected");
+ }
+
+ if (option) {
+ option.setAttribute("aria-selected", true);
+ option.classList.add("selected");
+ this.element.querySelector(".current-voice").textContent =
+ option.textContent;
+ }
+
+ if (!suppressEvent) {
+ let evt = this.element.ownerDocument.createEvent("Event");
+ evt.initEvent("change", true, true);
+ this.element.dispatchEvent(evt);
+ }
+ },
+
+ _updateDropdownHeight: function(now) {
+ let updateInner = () => {
+ let winHeight = this._win.innerHeight;
+ let listbox = this.listbox;
+ let listboxTop = listbox.getBoundingClientRect().top;
+ listbox.style.maxHeight = (winHeight - listboxTop - 10) + "px";
+ };
+
+ if (now) {
+ updateInner();
+ } else if (!this._pendingDropdownUpdate) {
+ this._pendingDropdownUpdate = true;
+ this._win.requestAnimationFrame(() => {
+ updateInner();
+ delete this._pendingDropdownUpdate;
+ });
+ }
+ },
+
+ _getOptionFromValue: function(value) {
+ return Array.from(this.options).find(o => o.dataset.value === value);
+ },
+
+ get element() {
+ return this._elementRef.get();
+ },
+
+ get listbox() {
+ return this._elementRef.get().querySelector(".options");
+ },
+
+ get selectToggle() {
+ return this._elementRef.get().querySelector(".select-toggle");
+ },
+
+ get _win() {
+ return this._winRef.get();
+ },
+
+ get _doc() {
+ return this._win.document;
+ },
+
+ set selected(option) {
+ this._select(option);
+ },
+
+ get selected() {
+ return this.element.querySelector(".options > .option.selected");
+ },
+
+ get options() {
+ return this.element.querySelectorAll(".options > .option");
+ },
+
+ set value(value) {
+ this._select(this._getOptionFromValue(value));
+ },
+
+ get value() {
+ let selected = this.selected;
+ return selected ? selected.dataset.value : "";
+ }
+};
diff --git a/toolkit/components/narrate/moz.build b/toolkit/components/narrate/moz.build
new file mode 100644
index 0000000000..c5597c3691
--- /dev/null
+++ b/toolkit/components/narrate/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/.
+
+EXTRA_JS_MODULES.narrate = [
+ 'NarrateControls.jsm',
+ 'Narrator.jsm',
+ 'VoiceSelect.jsm'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/toolkit/components/narrate/test/.eslintrc.js b/toolkit/components/narrate/test/.eslintrc.js
new file mode 100644
index 0000000000..5ff0bae7ee
--- /dev/null
+++ b/toolkit/components/narrate/test/.eslintrc.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../.eslintrc.js"
+ ],
+
+ "globals": {
+ "is": true,
+ "isnot": true,
+ "ok": true,
+ "NarrateTestUtils": true,
+ "content": true,
+ "ContentTaskUtils": true,
+ "ContentTask": true,
+ "BrowserTestUtils": true,
+ "gBrowser": true,
+ },
+
+ "rules": {
+ "mozilla/import-headjs-globals": "warn"
+ }
+};
diff --git a/toolkit/components/narrate/test/NarrateTestUtils.jsm b/toolkit/components/narrate/test/NarrateTestUtils.jsm
new file mode 100644
index 0000000000..b782f66c9e
--- /dev/null
+++ b/toolkit/components/narrate/test/NarrateTestUtils.jsm
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://testing-common/ContentTaskUtils.jsm");
+
+this.EXPORTED_SYMBOLS = [ "NarrateTestUtils" ];
+
+this.NarrateTestUtils = {
+ TOGGLE: "#narrate-toggle",
+ POPUP: "#narrate-dropdown .dropdown-popup",
+ VOICE_SELECT: "#narrate-voices .select-toggle",
+ VOICE_OPTIONS: "#narrate-voices .options",
+ VOICE_SELECTED: "#narrate-voices .options .option.selected",
+ VOICE_SELECT_LABEL: "#narrate-voices .select-toggle .current-voice",
+ RATE: "#narrate-rate-input",
+ START: "#narrate-dropdown:not(.speaking) #narrate-start-stop",
+ STOP: "#narrate-dropdown.speaking #narrate-start-stop",
+ BACK: "#narrate-skip-previous",
+ FORWARD: "#narrate-skip-next",
+
+ isVisible: function(element) {
+ let style = element.ownerDocument.defaultView.getComputedStyle(element, "");
+ if (style.display == "none") {
+ return false;
+ } else if (style.visibility != "visible") {
+ return false;
+ } else if (style.display == "-moz-popup" && element.state != "open") {
+ return false;
+ }
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument) {
+ return this.isVisible(element.parentNode);
+ }
+
+ return true;
+ },
+
+ isStoppedState: function(window, ok) {
+ let $ = window.document.querySelector.bind(window.document);
+ ok($(this.BACK).disabled, "back button is disabled");
+ ok($(this.FORWARD).disabled, "forward button is disabled");
+ ok(!!$(this.START), "start button is showing");
+ ok(!$(this.STOP), "stop button is hidden");
+ // This checks for a localized label. Not the best...
+ ok($(this.START).title == "Start", "Button tooltip is correct");
+ },
+
+ isStartedState: function(window, ok) {
+ let $ = window.document.querySelector.bind(window.document);
+ ok(!$(this.BACK).disabled, "back button is enabled");
+ ok(!$(this.FORWARD).disabled, "forward button is enabled");
+ ok(!$(this.START), "start button is hidden");
+ ok(!!$(this.STOP), "stop button is showing");
+ // This checks for a localized label. Not the best...
+ ok($(this.STOP).title == "Stop", "Button tooltip is correct");
+ },
+
+ selectVoice: function(window, voiceUri) {
+ if (!this.isVisible(window.document.querySelector(this.VOICE_OPTIONS))) {
+ window.document.querySelector(this.VOICE_SELECT).click();
+ }
+
+ let voiceOption = window.document.querySelector(
+ `#narrate-voices .option[data-value="${voiceUri}"]`);
+
+ voiceOption.focus();
+ voiceOption.click();
+
+ return voiceOption.classList.contains("selected");
+ },
+
+ getEventUtils: function(window) {
+ let eventUtils = {
+ "_EU_Ci": Components.interfaces,
+ "_EU_Cc": Components.classes,
+ window: window,
+ parent: window,
+ navigator: window.navigator,
+ KeyboardEvent: window.KeyboardEvent,
+ KeyEvent: window.KeyEvent
+ };
+ Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", eventUtils);
+ return eventUtils;
+ },
+
+ getReaderReadyPromise: function(window) {
+ return new Promise(resolve => {
+ function observeReady(subject, topic) {
+ if (subject == window) {
+ Services.obs.removeObserver(observeReady, topic);
+ resolve();
+ }
+ }
+
+ if (window.document.body.classList.contains("loaded")) {
+ resolve();
+ } else {
+ Services.obs.addObserver(observeReady, "AboutReader:Ready", false);
+ }
+ });
+ },
+
+ waitForNarrateToggle: function(window) {
+ let toggle = window.document.querySelector(this.TOGGLE);
+ return ContentTaskUtils.waitForCondition(
+ () => !toggle.hidden, "");
+ },
+
+ waitForPrefChange: function(pref) {
+ return new Promise(resolve => {
+ function observeChange() {
+ Services.prefs.removeObserver(pref, observeChange);
+ resolve(Preferences.get(pref));
+ }
+
+ Services.prefs.addObserver(pref, observeChange, false);
+ });
+ },
+
+ sendBoundaryEvent: function(window, name, charIndex) {
+ let detail = { type: "boundary", args: { name, charIndex } };
+ window.dispatchEvent(new window.CustomEvent("testsynthevent",
+ { detail: detail }));
+ },
+
+ isWordHighlightGone: function(window, ok) {
+ let $ = window.document.querySelector.bind(window.document);
+ ok(!$(".narrate-word-highlight"), "No more word highlights exist");
+ },
+
+ getWordHighlights: function(window) {
+ let $$ = window.document.querySelectorAll.bind(window.document);
+ let nodes = Array.from($$(".narrate-word-highlight"));
+ return nodes.map(node => {
+ return { word: node.dataset.word,
+ left: Number(node.style.left.replace(/px$/, "")),
+ top: Number(node.style.top.replace(/px$/, ""))};
+ });
+ }
+};
diff --git a/toolkit/components/narrate/test/browser.ini b/toolkit/components/narrate/test/browser.ini
new file mode 100644
index 0000000000..0f5d694ac2
--- /dev/null
+++ b/toolkit/components/narrate/test/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files =
+ head.js
+ NarrateTestUtils.jsm
+ moby_dick.html
+
+[browser_narrate.js]
+[browser_narrate_disable.js]
+[browser_narrate_language.js]
+support-files = inferno.html
+[browser_voiceselect.js]
+[browser_word_highlight.js]
diff --git a/toolkit/components/narrate/test/browser_narrate.js b/toolkit/components/narrate/test/browser_narrate.js
new file mode 100644
index 0000000000..b4951ef9f6
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_narrate.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals is, isnot, registerCleanupFunction, add_task */
+
+"use strict";
+
+registerCleanupFunction(teardown);
+
+add_task(function* testNarrate() {
+ setup();
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+ let TEST_VOICE = "urn:moz-tts:fake-indirect:teresa";
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ let popup = $(NarrateTestUtils.POPUP);
+ ok(!NarrateTestUtils.isVisible(popup), "popup is initially hidden");
+
+ let toggle = $(NarrateTestUtils.TOGGLE);
+ toggle.click();
+
+ ok(NarrateTestUtils.isVisible(popup), "popup toggled");
+
+ let voiceOptions = $(NarrateTestUtils.VOICE_OPTIONS);
+ ok(!NarrateTestUtils.isVisible(voiceOptions),
+ "voice options are initially hidden");
+
+ $(NarrateTestUtils.VOICE_SELECT).click();
+ ok(NarrateTestUtils.isVisible(voiceOptions), "voice options pop up");
+
+ let prefChanged = NarrateTestUtils.waitForPrefChange("narrate.voice");
+ ok(NarrateTestUtils.selectVoice(content, TEST_VOICE),
+ "test voice selected");
+ yield prefChanged;
+
+ ok(!NarrateTestUtils.isVisible(voiceOptions), "voice options hidden again");
+
+ NarrateTestUtils.isStoppedState(content, ok);
+
+ let promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.START).click();
+ let speechinfo = (yield promiseEvent).detail;
+ is(speechinfo.voice, TEST_VOICE, "correct voice is being used");
+ let paragraph = speechinfo.paragraph;
+
+ NarrateTestUtils.isStartedState(content, ok);
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.FORWARD).click();
+ speechinfo = (yield promiseEvent).detail;
+ is(speechinfo.voice, TEST_VOICE, "same voice is used");
+ isnot(speechinfo.paragraph, paragraph, "next paragraph is being spoken");
+
+ NarrateTestUtils.isStartedState(content, ok);
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.BACK).click();
+ speechinfo = (yield promiseEvent).detail;
+ is(speechinfo.paragraph, paragraph, "first paragraph being spoken");
+
+ NarrateTestUtils.isStartedState(content, ok);
+
+ paragraph = speechinfo.paragraph;
+ $(NarrateTestUtils.STOP).click();
+ yield ContentTaskUtils.waitForCondition(
+ () => !$(NarrateTestUtils.STOP), "transitioned to stopped state");
+ NarrateTestUtils.isStoppedState(content, ok);
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.START).click();
+ speechinfo = (yield promiseEvent).detail;
+ is(speechinfo.paragraph, paragraph, "read same paragraph again");
+
+ NarrateTestUtils.isStartedState(content, ok);
+
+ let eventUtils = NarrateTestUtils.getEventUtils(content);
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ prefChanged = NarrateTestUtils.waitForPrefChange("narrate.rate");
+ $(NarrateTestUtils.RATE).focus();
+ eventUtils.sendKey("UP", content);
+ let newspeechinfo = (yield promiseEvent).detail;
+ is(newspeechinfo.paragraph, speechinfo.paragraph, "same paragraph");
+ isnot(newspeechinfo.rate, speechinfo.rate, "rate changed");
+ yield prefChanged;
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphend");
+ $(NarrateTestUtils.STOP).click();
+ yield promiseEvent;
+
+ yield ContentTaskUtils.waitForCondition(
+ () => !$(NarrateTestUtils.STOP), "transitioned to stopped state");
+ NarrateTestUtils.isStoppedState(content, ok);
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "scroll");
+ content.scrollBy(0, 10);
+ yield promiseEvent;
+ ok(!NarrateTestUtils.isVisible(popup), "popup is hidden after scroll");
+
+ toggle.click();
+ ok(NarrateTestUtils.isVisible(popup), "popup is toggled again");
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.START).click();
+ yield promiseEvent;
+ NarrateTestUtils.isStartedState(content, ok);
+
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "scroll");
+ content.scrollBy(0, -10);
+ yield promiseEvent;
+ ok(NarrateTestUtils.isVisible(popup), "popup stays visible after scroll");
+
+ toggle.click();
+ ok(!NarrateTestUtils.isVisible(popup), "popup is dismissed while speaking");
+ NarrateTestUtils.isStartedState(content, ok);
+
+ // Go forward all the way to the end of the article. We should eventually
+ // stop.
+ do {
+ promiseEvent = Promise.race([
+ ContentTaskUtils.waitForEvent(content, "paragraphstart"),
+ ContentTaskUtils.waitForEvent(content, "paragraphsdone")]);
+ $(NarrateTestUtils.FORWARD).click();
+ } while ((yield promiseEvent).type == "paragraphstart");
+
+ // This is to make sure we are not actively scrolling when the tab closes.
+ content.scroll(0, 0);
+
+ yield ContentTaskUtils.waitForCondition(
+ () => !$(NarrateTestUtils.STOP), "transitioned to stopped state");
+ NarrateTestUtils.isStoppedState(content, ok);
+ });
+});
diff --git a/toolkit/components/narrate/test/browser_narrate_disable.js b/toolkit/components/narrate/test/browser_narrate_disable.js
new file mode 100644
index 0000000000..264815fd1e
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_narrate_disable.js
@@ -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/. */
+
+/* globals registerCleanupFunction, add_task */
+
+"use strict";
+
+const ENABLE_PREF = "narrate.enabled";
+
+registerCleanupFunction(() => {
+ clearUserPref(ENABLE_PREF);
+ teardown();
+});
+
+add_task(function* testNarratePref() {
+ setup();
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function() {
+ is(content.document.querySelectorAll(NarrateTestUtils.TOGGLE).length, 1,
+ "narrate is inserted by default");
+ });
+
+ setBoolPref(ENABLE_PREF, false);
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function() {
+ ok(!content.document.querySelector(NarrateTestUtils.TOGGLE),
+ "narrate is disabled and is not in reader mode");
+ });
+
+ setBoolPref(ENABLE_PREF, true);
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function() {
+ is(content.document.querySelectorAll(NarrateTestUtils.TOGGLE).length, 1,
+ "narrate is re-enabled and appears only once");
+ });
+});
diff --git a/toolkit/components/narrate/test/browser_narrate_language.js b/toolkit/components/narrate/test/browser_narrate_language.js
new file mode 100644
index 0000000000..2542a87d6d
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_narrate_language.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/. */
+
+/* globals is, isnot, registerCleanupFunction, add_task */
+
+"use strict";
+
+registerCleanupFunction(teardown);
+
+add_task(function* testVoiceselectDropdownAutoclose() {
+ setup("automatic", true);
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ ok(!!$(".option[data-value='urn:moz-tts:fake-direct:bob']"),
+ "Jamaican English voice available");
+ ok(!!$(".option[data-value='urn:moz-tts:fake-direct:lenny']"),
+ "Canadian English voice available");
+ ok(!!$(".option[data-value='urn:moz-tts:fake-direct:amy']"),
+ "British English voice available");
+
+ ok(!$(".option[data-value='urn:moz-tts:fake-direct:celine']"),
+ "Canadian French voice unavailable");
+ ok(!$(".option[data-value='urn:moz-tts:fake-direct:julie']"),
+ "Mexican Spanish voice unavailable");
+
+ $(NarrateTestUtils.TOGGLE).click();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "popup is toggled");
+
+ let prefChanged = NarrateTestUtils.waitForPrefChange(
+ "narrate.voice", "getCharPref");
+ NarrateTestUtils.selectVoice(content, "urn:moz-tts:fake-direct:lenny");
+ let voicePref = JSON.parse(yield prefChanged);
+ is(voicePref.en, "urn:moz-tts:fake-direct:lenny", "pref set correctly");
+ });
+});
+
+add_task(function* testVoiceselectDropdownAutoclose() {
+ setup("automatic", true);
+
+ yield spawnInNewReaderTab(TEST_ITALIAN_ARTICLE, function* () {
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ ok(!!$(".option[data-value='urn:moz-tts:fake-indirect:zanetta']"),
+ "Italian voice available");
+ ok(!!$(".option[data-value='urn:moz-tts:fake-indirect:margherita']"),
+ "Italian voice available");
+
+ ok(!$(".option[data-value='urn:moz-tts:fake-direct:bob']"),
+ "Jamaican English voice available");
+ ok(!$(".option[data-value='urn:moz-tts:fake-direct:celine']"),
+ "Canadian French voice unavailable");
+ ok(!$(".option[data-value='urn:moz-tts:fake-direct:julie']"),
+ "Mexican Spanish voice unavailable");
+
+ $(NarrateTestUtils.TOGGLE).click();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "popup is toggled");
+
+ let prefChanged = NarrateTestUtils.waitForPrefChange(
+ "narrate.voice", "getCharPref");
+ NarrateTestUtils.selectVoice(content, "urn:moz-tts:fake-indirect:zanetta");
+ let voicePref = JSON.parse(yield prefChanged);
+ is(voicePref.it, "urn:moz-tts:fake-indirect:zanetta", "pref set correctly");
+ });
+});
diff --git a/toolkit/components/narrate/test/browser_voiceselect.js b/toolkit/components/narrate/test/browser_voiceselect.js
new file mode 100644
index 0000000000..0de6528dd4
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_voiceselect.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals registerCleanupFunction, add_task, is, isnot */
+
+"use strict";
+
+registerCleanupFunction(teardown);
+
+add_task(function* testVoiceselectDropdownAutoclose() {
+ setup();
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ $(NarrateTestUtils.TOGGLE).click();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "popup is toggled");
+
+ ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+ "voice options are initially hidden");
+
+ $(NarrateTestUtils.VOICE_SELECT).click();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+ "voice options are toggled");
+
+ $(NarrateTestUtils.TOGGLE).click();
+ // A focus will follow a real click.
+ $(NarrateTestUtils.TOGGLE).focus();
+ ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "narrate popup is dismissed");
+
+ $(NarrateTestUtils.TOGGLE).click();
+ // A focus will follow a real click.
+ $(NarrateTestUtils.TOGGLE).focus();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "narrate popup is showing again");
+ ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+ "voice options are hidden after popup comes back");
+ });
+});
+
+add_task(function* testVoiceselectLabelChange() {
+ setup();
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ $(NarrateTestUtils.TOGGLE).click();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "popup is toggled");
+
+ ok(NarrateTestUtils.selectVoice(content, "urn:moz-tts:fake-direct:lenny"),
+ "voice selected");
+
+ let selectedOption = $(NarrateTestUtils.VOICE_SELECTED);
+ let selectLabel = $(NarrateTestUtils.VOICE_SELECT_LABEL);
+
+ is(selectedOption.textContent, selectLabel.textContent,
+ "new label matches selected voice");
+ });
+});
+
+add_task(function* testVoiceselectKeyboard() {
+ setup();
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ $(NarrateTestUtils.TOGGLE).click();
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.POPUP)),
+ "popup is toggled");
+
+ let eventUtils = NarrateTestUtils.getEventUtils(content);
+
+ let firstValue = $(NarrateTestUtils.VOICE_SELECTED).dataset.value;
+
+ ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+ "voice options initially are hidden");
+
+ $(NarrateTestUtils.VOICE_SELECT).focus();
+
+ eventUtils.sendKey("DOWN", content);
+
+ yield ContentTaskUtils.waitForCondition(
+ () => $(NarrateTestUtils.VOICE_SELECTED).dataset.value != firstValue,
+ "value changed after pressing DOWN key");
+
+ eventUtils.sendKey("RETURN", content);
+
+ ok(NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+ "voice options showing after pressing RETURN");
+
+ eventUtils.sendKey("UP", content);
+
+ eventUtils.sendKey("RETURN", content);
+
+ ok(!NarrateTestUtils.isVisible($(NarrateTestUtils.VOICE_OPTIONS)),
+ "voice options hidden after pressing RETURN");
+
+ yield ContentTaskUtils.waitForCondition(
+ () => $(NarrateTestUtils.VOICE_SELECTED).dataset.value == firstValue,
+ "value changed back to original after pressing RETURN");
+ });
+});
diff --git a/toolkit/components/narrate/test/browser_word_highlight.js b/toolkit/components/narrate/test/browser_word_highlight.js
new file mode 100644
index 0000000000..bfdbcf48e7
--- /dev/null
+++ b/toolkit/components/narrate/test/browser_word_highlight.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/. */
+
+/* globals is, isnot, registerCleanupFunction, add_task */
+
+"use strict";
+
+registerCleanupFunction(teardown);
+
+add_task(function* testNarrate() {
+ setup("urn:moz-tts:fake-indirect:teresa");
+
+ yield spawnInNewReaderTab(TEST_ARTICLE, function* () {
+ let $ = content.document.querySelector.bind(content.document);
+
+ yield NarrateTestUtils.waitForNarrateToggle(content);
+
+ let popup = $(NarrateTestUtils.POPUP);
+ ok(!NarrateTestUtils.isVisible(popup), "popup is initially hidden");
+
+ let toggle = $(NarrateTestUtils.TOGGLE);
+ toggle.click();
+
+ ok(NarrateTestUtils.isVisible(popup), "popup toggled");
+
+ NarrateTestUtils.isStoppedState(content, ok);
+
+ let promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.START).click();
+ let voice = (yield promiseEvent).detail.voice;
+ is(voice, "urn:moz-tts:fake-indirect:teresa", "double-check voice");
+
+ // Skip forward to first paragraph.
+ let details;
+ do {
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "paragraphstart");
+ $(NarrateTestUtils.FORWARD).click();
+ details = (yield promiseEvent).detail;
+ } while (details.tag != "p");
+
+ let boundaryPat = /(\s+)\S/g;
+ let position = { left: 0, top: 0 };
+ let text = details.paragraph;
+ for (let res = boundaryPat.exec(text); res; res = boundaryPat.exec(text)) {
+ promiseEvent = ContentTaskUtils.waitForEvent(content, "wordhighlight");
+ NarrateTestUtils.sendBoundaryEvent(content, "word", res.index);
+ let { start, end } = (yield promiseEvent).detail;
+ let nodes = NarrateTestUtils.getWordHighlights(content);
+ for (let node of nodes) {
+ // Since this is English we can assume each word is to the right or
+ // below the previous one.
+ ok(node.left > position.left || node.top > position.top,
+ "highlight position is moving");
+ position = { left: node.left, top: node.top };
+ }
+ let wordFromOffset = text.substring(start, end);
+ // XXX: Each node should contain the part of the word it highlights.
+ // Right now, each node contains the entire word.
+ let wordFromHighlight = nodes[0].word;
+ is(wordFromOffset, wordFromHighlight, "Correct word is highlighted");
+ }
+
+ $(NarrateTestUtils.STOP).click();
+ yield ContentTaskUtils.waitForCondition(
+ () => !$(NarrateTestUtils.STOP), "transitioned to stopped state");
+ NarrateTestUtils.isWordHighlightGone(content, ok);
+ });
+});
diff --git a/toolkit/components/narrate/test/head.js b/toolkit/components/narrate/test/head.js
new file mode 100644
index 0000000000..491a3da8d4
--- /dev/null
+++ b/toolkit/components/narrate/test/head.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/. */
+
+/* exported teardown, setup, toggleExtension,
+ spawnInNewReaderTab, TEST_ARTICLE, TEST_ITALIAN_ARTICLE */
+
+"use strict";
+
+const TEST_ARTICLE =
+ "http://example.com/browser/toolkit/components/narrate/test/moby_dick.html";
+
+const TEST_ITALIAN_ARTICLE =
+ "http://example.com/browser/toolkit/components/narrate/test/inferno.html";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+const TEST_PREFS = {
+ "reader.parse-on-load.enabled": true,
+ "media.webspeech.synth.enabled": true,
+ "media.webspeech.synth.test": true,
+ "narrate.enabled": true,
+ "narrate.test": true,
+ "narrate.voice": null,
+ "narrate.filter-voices": false,
+};
+
+function setup(voiceUri = "automatic", filterVoices = false) {
+ let prefs = Object.assign({}, TEST_PREFS, {
+ "narrate.filter-voices": filterVoices,
+ "narrate.voice": JSON.stringify({ en: voiceUri })
+ });
+
+ // Set required test prefs.
+ Object.entries(prefs).forEach(([name, value]) => {
+ switch (typeof value) {
+ case "boolean":
+ setBoolPref(name, value);
+ break;
+ case "string":
+ setCharPref(name, value);
+ break;
+ }
+ });
+}
+
+function teardown() {
+ // Reset test prefs.
+ Object.entries(TEST_PREFS).forEach(pref => {
+ clearUserPref(pref[0]);
+ });
+}
+
+function spawnInNewReaderTab(url, func) {
+ return BrowserTestUtils.withNewTab(
+ { gBrowser,
+ url: `about:reader?url=${encodeURIComponent(url)}` },
+ function* (browser) {
+ yield ContentTask.spawn(browser, null, function* () {
+ Components.utils.import("chrome://mochitests/content/browser/" +
+ "toolkit/components/narrate/test/NarrateTestUtils.jsm");
+
+ yield NarrateTestUtils.getReaderReadyPromise(content);
+ });
+
+ yield ContentTask.spawn(browser, null, func);
+ });
+}
+
+function setBoolPref(name, value) {
+ Services.prefs.setBoolPref(name, value);
+}
+
+function setCharPref(name, value) {
+ Services.prefs.setCharPref(name, value);
+}
+
+function clearUserPref(name) {
+ Services.prefs.clearUserPref(name);
+}
diff --git a/toolkit/components/narrate/test/inferno.html b/toolkit/components/narrate/test/inferno.html
new file mode 100644
index 0000000000..58dfd24df1
--- /dev/null
+++ b/toolkit/components/narrate/test/inferno.html
@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Inferno - Canto I</title>
+</head>
+<body>
+ <h1>Inferno</h1>
+ <h2>Canto I: Dante nella selva oscura</h2>
+ <p>
+ Nel mezzo del cammin di nostra vita<br>
+ mi ritrovai per una selva oscura,<br>
+ ché la diritta via era smarrita.
+ </p>
+ <p>
+ Ahi quanto a dir qual era è cosa dura<br>
+ esta selva selvaggia e aspra e forte<br>
+ che nel pensier rinova la paura!
+ </p>
+ <p>
+ Tant' è amara che poco è più morte;<br>
+ ma per trattar del ben ch'i' vi trovai,<br>
+ dirò de l'altre cose ch'i' v'ho scorte.
+ </p>
+ <p>
+ Io non so ben ridir com' i' v'intrai,<br>
+ tant' era pien di sonno a quel punto<br>
+ che la verace via abbandonai.
+ </p>
+ <p>
+ Ma poi ch'i' fui al piè d'un colle giunto,<br>
+ là dove terminava quella valle<br>
+ che m'avea di paura il cor compunto,
+ </p>
+ <p>
+ guardai in alto e vidi le sue spalle<br>
+ vestite già de' raggi del pianeta<br>
+ che mena dritto altrui per ogne calle.
+ </p>
+ <p>
+ Allor fu la paura un poco queta,<br>
+ che nel lago del cor m'era durata<br>
+ la notte ch'i' passai con tanta pieta.
+ </p>
+ <p>
+ E come quei che con lena affannata,<br>
+ uscito fuor del pelago a la riva,<br>
+ si volge a l'acqua perigliosa e guata,
+ </p>
+ <p>
+ così l'animo mio, ch'ancor fuggiva,<br>
+ si volse a retro a rimirar lo passo<br>
+ che non lasciò già mai persona viva.
+ </p>
+ <p>
+ Poi ch'èi posato un poco il corpo lasso,<br>
+ ripresi via per la piaggia diserta,<br>
+ sì che 'l piè fermo sempre era 'l più basso.
+ </p>
+ <p>
+ Ed ecco, quasi al cominciar de l'erta,<br>
+ una lonza leggiera e presta molto,<br>
+ che di pel macolato era coverta;
+ </p>
+ <p>
+ e non mi si partia dinanzi al volto,<br>
+ anzi 'mpediva tanto il mio cammino,<br>
+ ch'i' fui per ritornar più volte vòlto.
+ </p>
+ <p>
+ Temp' era dal principio del mattino,<br>
+ e 'l sol montava 'n sù con quelle stelle<br>
+ ch'eran con lui quando l'amor divino
+ </p>
+ <p>
+ mosse di prima quelle cose belle;<br>
+ sì ch'a bene sperar m'era cagione<br>
+ di quella fiera a la gaetta pelle
+ </p>
+ <p>
+ l'ora del tempo e la dolce stagione;<br>
+ ma non sì che paura non mi desse<br>
+ la vista che m'apparve d'un leone.
+ </p>
+ <p>
+ Questi parea che contra me venisse<br>
+ con la test' alta e con rabbiosa fame,<br>
+ sì che parea che l'aere ne tremesse.
+ </p>
+ <p>
+ Ed una lupa, che di tutte brame<br>
+ sembiava carca ne la sua magrezza,<br>
+ e molte genti fé già viver grame,
+ </p>
+ <p>
+ questa mi porse tanto di gravezza<br>
+ con la paura ch'uscia di sua vista,<br>
+ ch'io perdei la speranza de l'altezza.
+ </p>
+ <p>
+ E qual è quei che volontieri acquista,<br>
+ e giugne 'l tempo che perder lo face,<br>
+ che 'n tutti suoi pensier piange e s'attrista;
+ </p>
+ <p>
+ tal mi fece la bestia sanza pace,<br>
+ che, venendomi 'ncontro, a poco a poco<br>
+ mi ripigneva là dove 'l sol tace.
+ </p>
+ <p>
+ Mentre ch'i' rovinava in basso loco,<br>
+ dinanzi a li occhi mi si fu offerto<br>
+ chi per lungo silenzio parea fioco.
+ </p>
+ <p>
+ Quando vidi costui nel gran diserto,<br>
+ «<em>Miserere</em> di me», gridai a lui,<br>
+ «qual che tu sii, od ombra od omo certo!».
+ </p>
+ <p>
+ Rispuosemi: «Non omo, omo già fui,<br>
+ e li parenti miei furon lombardi,<br>
+ mantoani per patrïa ambedui.
+ </p>
+ <p>
+ Nacqui <em>sub Iulio</em>, ancor che fosse tardi,<br>
+ e vissi a Roma sotto 'l buono Augusto<br>
+ nel tempo de li dèi falsi e bugiardi.
+ </p>
+ <p>
+ Poeta fui, e cantai di quel giusto<br>
+ figliuol d'Anchise che venne di Troia,<br>
+ poi che 'l superbo Ilïón fu combusto.
+ </p>
+ <p>
+ Ma tu perché ritorni a tanta noia?<br>
+ perché non sali il dilettoso monte<br>
+ ch'è principio e cagion di tutta gioia?».
+ </p>
+ <p>
+ «Or se' tu quel Virgilio e quella fonte<br>
+ che spandi di parlar sì largo fiume?»,<br>
+ rispuos' io lui con vergognosa fronte.
+ </p>
+ <p>
+ «O de li altri poeti onore e lume,<br>
+ vagliami 'l lungo studio e 'l grande amore<br>
+ che m'ha fatto cercar lo tuo volume.
+ </p>
+ <p>
+ Tu se' lo mio maestro e 'l mio autore,<br>
+ tu se' solo colui da cu' io tolsi<br>
+ lo bello stilo che m'ha fatto onore.
+ </p>
+ <p>
+ Vedi la bestia per cu' io mi volsi;<br>
+ aiutami da lei, famoso saggio,<br>
+ ch'ella mi fa tremar le vene e i polsi».
+ </p>
+ <p>
+ «A te convien tenere altro vïaggio»,<br>
+ rispuose, poi che lagrimar mi vide,<br>
+ «se vuo' campar d'esto loco selvaggio;
+ </p>
+ <p>
+ ché questa bestia, per la qual tu gride,<br>
+ non lascia altrui passar per la sua via,<br>
+ ma tanto lo 'mpedisce che l'uccide;
+ </p>
+ <p>
+ e ha natura sì malvagia e ria,<br>
+ che mai non empie la bramosa voglia,<br>
+ e dopo 'l pasto ha più fame che pria.
+ </p>
+ <p>
+ Molti son li animali a cui s'ammoglia,<br>
+ e più saranno ancora, infin che 'l veltro<br>
+ verrà, che la farà morir con doglia.
+ </p>
+ <p>
+ Questi non ciberà terra né peltro,<br>
+ ma sapïenza, amore e virtute,<br>
+ e sua nazion sarà tra feltro e feltro.
+ </p>
+ <p>
+ Di quella umile Italia fia salute<br>
+ per cui morì la vergine Cammilla,<br>
+ Eurialo e Turno e Niso di ferute.
+ </p>
+ <p>
+ Questi la caccerà per ogne villa,<br>
+ fin che l'avrà rimessa ne lo 'nferno,<br>
+ là onde 'nvidia prima dipartilla.
+ </p>
+ <p>
+ Ond' io per lo tuo me' penso e discerno<br>
+ che tu mi segui, e io sarò tua guida,<br>
+ e trarrotti di qui per loco etterno;
+ </p>
+ <p>
+ ove udirai le disperate strida,<br>
+ vedrai li antichi spiriti dolenti,<br>
+ ch'a la seconda morte ciascun grida;
+ </p>
+ <p>
+ e vederai color che son contenti<br>
+ nel foco, perché speran di venire<br>
+ quando che sia a le beate genti.
+ </p>
+ <p>
+ A le quai poi se tu vorrai salire,<br>
+ anima fia a ciò più di me degna:<br>
+ con lei ti lascerò nel mio partire;
+ </p>
+ <p>
+ ché quello imperador che là sù regna,<br>
+ perch' i' fu' ribellante a la sua legge,<br>
+ non vuol che 'n sua città per me si vegna.
+ </p>
+ <p>
+ In tutte parti impera e quivi regge;<br>
+ quivi è la sua città e l'alto seggio:<br>
+ oh felice colui cu' ivi elegge!».
+ </p>
+ <p>
+ E io a lui: «Poeta, io ti richeggio<br>
+ per quello Dio che tu non conoscesti,<br>
+ a ciò ch'io fugga questo male e peggio,
+ </p>
+ <p>
+ che tu mi meni là dov' or dicesti,<br>
+ sì ch'io veggia la porta di san Pietro<br>
+ e color cui tu fai cotanto mesti».
+ </p>
+ <p>
+ Allor si mosse, e io li tenni dietro.
+ </p>
+</body>
+</html>
diff --git a/toolkit/components/narrate/test/moby_dick.html b/toolkit/components/narrate/test/moby_dick.html
new file mode 100644
index 0000000000..0beaa20fd1
--- /dev/null
+++ b/toolkit/components/narrate/test/moby_dick.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Moby Dick - Chapter 1. Loomings</title>
+</head>
+<body>
+ <h1>Moby Dick</h1>
+ <h2>Chapter 1. Loomings</h2>
+ <p>
+ Call me Ishmael. <span>Some <span>years</span></span> ago—never mind how
+ long precisely—having little or no money in my purse, and nothing particular
+ to interest me on shore, I thought I would sail about a little and see the
+ watery part of the world. It is a way I have of driving off the spleen and
+ regulating the circulation. Whenever I find myself growing grim about the
+ mouth; whenever it is a damp, drizzly November in my soul; whenever I find
+ myself involuntarily pausing before coffin warehouses, and bringing up the
+ rear of every funeral I meet; and especially whenever my hypos get such an
+ upper hand of me, that it requires a strong moral principle to prevent me
+ from deliberately stepping into the street, and methodically knocking
+ people's hats off—then, I account it high time to get to sea as soon as I
+ can. This is my substitute for pistol and ball. With a philosophical
+ flourish Cato throws himself upon his sword; I quietly take to the ship.
+ There is nothing surprising in this. If they but knew it, almost all men in
+ their degree, some time or other, cherish very nearly the same feelings
+ towards the ocean with me.
+ </p>
+ <p>
+ There now is your insular city of the Manhattoes, belted round by wharves
+ as Indian isles by coral reefs—commerce surrounds it with her surf.
+ Right and left, the streets take you waterward. Its extreme downtown is
+ the battery, where that noble mole is washed by waves, and cooled by
+ breezes, which a few hours previous were out of sight of land. Look at the
+ crowds of water-gazers there.
+ </p>
+ <p>
+ Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears
+ Hook to Coenties Slip, and from thence, by Whitehall, northward. What do
+ you see?—Posted like silent sentinels all around the town, stand
+ thousands upon thousands of mortal men fixed in ocean reveries. Some
+ leaning against the spiles; some seated upon the pier-heads; some looking
+ over the bulwarks of ships from China; some high aloft in the rigging, as
+ if striving to get a still better seaward peep. But these are all
+ landsmen; of week days pent up in lath and plaster—tied to counters,
+ nailed to benches, clinched to desks. How then is this? Are the green
+ fields gone? What do they here?
+ </p>
+ <p>
+ But look! here come more crowds, pacing straight for the water, and
+ seemingly bound for a dive. Strange! Nothing will content them but the
+ extremest limit of the land; loitering under the shady lee of yonder
+ warehouses will not suffice. No. They must get just as nigh the water as
+ they possibly can without falling in. And there they stand—miles of
+ them—leagues. Inlanders all, they come from lanes and alleys,
+ streets and avenues—north, east, south, and west. Yet here they all
+ unite. Tell me, does the magnetic virtue of the needles of the compasses
+ of all those ships attract them thither?
+ </p>
+ <p>
+ Once more. Say you are in the country; in some high land of lakes. Take
+ almost any path you please, and ten to one it carries you down in a dale,
+ and leaves you there by a pool in the stream. There is magic in it. Let
+ the most absent-minded of men be plunged in his deepest reveries—stand
+ that man on his legs, set his feet a-going, and he will infallibly lead
+ you to water, if water there be in all that region. Should you ever be
+ athirst in the great American desert, try this experiment, if your caravan
+ happen to be supplied with a metaphysical professor. Yes, as every one
+ knows, meditation and water are wedded for ever.
+ </p>
+ <p>
+ But here is an artist. He desires to paint you the dreamiest, shadiest,
+ quietest, most enchanting bit of romantic landscape in all the valley of
+ the Saco. What is the chief element he employs? There stand his trees,
+ each with a hollow trunk, as if a hermit and a crucifix were within; and
+ here sleeps his meadow, and there sleep his cattle; and up from yonder
+ cottage goes a sleepy smoke. Deep into distant woodlands winds a mazy way,
+ reaching to overlapping spurs of mountains bathed in their hill-side blue.
+ But though the picture lies thus tranced, and though this pine-tree shakes
+ down its sighs like leaves upon this shepherd's head, yet all were vain,
+ unless the shepherd's eye were fixed upon the magic stream before him. Go
+ visit the Prairies in June, when for scores on scores of miles you wade
+ knee-deep among Tiger-lilies—what is the one charm wanting?—Water—there
+ is not a drop of water there! Were Niagara but a cataract of sand, would
+ you travel your thousand miles to see it? Why did the poor poet of
+ Tennessee, upon suddenly receiving two handfuls of silver, deliberate
+ whether to buy him a coat, which he sadly needed, or invest his money in a
+ pedestrian trip to Rockaway Beach? Why is almost every robust healthy boy
+ with a robust healthy soul in him, at some time or other crazy to go to
+ sea? Why upon your first voyage as a passenger, did you yourself feel such
+ a mystical vibration, when first told that you and your ship were now out
+ of sight of land? Why did the old Persians hold the sea holy? Why did the
+ Greeks give it a separate deity, and own brother of Jove? Surely all this
+ is not without meaning. And still deeper the meaning of that story of
+ Narcissus, who because he could not grasp the tormenting, mild image he
+ saw in the fountain, plunged into it and was drowned. But that same image,
+ we ourselves see in all rivers and oceans. It is the image of the
+ ungraspable phantom of life; and this is the key to it all.
+ </p>
+ <p>
+ Now, when I say that I am in the habit of going to sea whenever I begin to
+ grow hazy about the eyes, and begin to be over conscious of my lungs, I do
+ not mean to have it inferred that I ever go to sea as a passenger. For to
+ go as a passenger you must needs have a purse, and a purse is but a rag
+ unless you have something in it. Besides, passengers get sea-sick—grow
+ quarrelsome—don't sleep of nights—do not enjoy themselves
+ much, as a general thing;—no, I never go as a passenger; nor, though
+ I am something of a salt, do I ever go to sea as a Commodore, or a
+ Captain, or a Cook. I abandon the glory and distinction of such offices to
+ those who like them. For my part, I abominate all honourable respectable
+ toils, trials, and tribulations of every kind whatsoever. It is quite as
+ much as I can do to take care of myself, without taking care of ships,
+ barques, brigs, schooners, and what not. And as for going as cook,—though
+ I confess there is considerable glory in that, a cook being a sort of
+ officer on ship-board—yet, somehow, I never fancied broiling fowls;—though
+ once broiled, judiciously buttered, and judgmatically salted and peppered,
+ there is no one who will speak more respectfully, not to say
+ reverentially, of a broiled fowl than I will. It is out of the idolatrous
+ dotings of the old Egyptians upon broiled ibis and roasted river horse,
+ that you see the mummies of those creatures in their huge bake-houses the
+ pyramids.
+ </p>
+ <p>
+ No, when I go to sea, I go as a simple sailor, right before the mast,
+ plumb down into the forecastle, aloft there to the royal mast-head. True,
+ they rather order me about some, and make me jump from spar to spar, like
+ a grasshopper in a May meadow. And at first, this sort of thing is
+ unpleasant enough. It touches one's sense of honour, particularly if you
+ come of an old established family in the land, the Van Rensselaers, or
+ Randolphs, or Hardicanutes. And more than all, if just previous to putting
+ your hand into the tar-pot, you have been lording it as a country
+ schoolmaster, making the tallest boys stand in awe of you. The transition
+ is a keen one, I assure you, from a schoolmaster to a sailor, and requires
+ a strong decoction of Seneca and the Stoics to enable you to grin and bear
+ it. But even this wears off in time.
+ </p>
+ <p>
+ What of it, if some old hunks of a sea-captain orders me to get a broom
+ and sweep down the decks? What does that indignity amount to, weighed, I
+ mean, in the scales of the New Testament? Do you think the archangel
+ Gabriel thinks anything the less of me, because I promptly and
+ respectfully obey that old hunks in that particular instance? Who ain't a
+ slave? Tell me that. Well, then, however the old sea-captains may order me
+ about—however they may thump and punch me about, I have the
+ satisfaction of knowing that it is all right; that everybody else is one
+ way or other served in much the same way—either in a physical or
+ metaphysical point of view, that is; and so the universal thump is passed
+ round, and all hands should rub each other's shoulder-blades, and be
+ content.
+ </p>
+ <p>
+ Again, I always go to sea as a sailor, because they make a point of paying
+ me for my trouble, whereas they never pay passengers a single penny that I
+ ever heard of. On the contrary, passengers themselves must pay. And there
+ is all the difference in the world between paying and being paid. The act
+ of paying is perhaps the most uncomfortable infliction that the two
+ orchard thieves entailed upon us. But <i>being paid</i>,—what will compare
+ with it? The urbane activity with which a man receives money is really
+ marvellous, considering that we so earnestly believe money to be the root
+ of all earthly ills, and that on no account can a monied man enter heaven.
+ Ah! how cheerfully we consign ourselves to perdition!
+ </p>
+ <p>
+ Finally, I always go to sea as a sailor, because of the wholesome exercise
+ and pure air of the fore-castle deck. For as in this world, head winds are
+ far more prevalent than winds from astern (that is, if you never violate
+ the Pythagorean maxim), so for the most part the Commodore on the
+ quarter-deck gets his atmosphere at second hand from the sailors on the
+ forecastle. He thinks he breathes it first; but not so. In much the same
+ way do the commonalty lead their leaders in many other things, at the same
+ time that the leaders little suspect it. But wherefore it was that after
+ having repeatedly smelt the sea as a merchant sailor, I should now take it
+ into my head to go on a whaling voyage; this the invisible police officer
+ of the Fates, who has the constant surveillance of me, and secretly dogs
+ me, and influences me in some unaccountable way—he can better answer
+ than any one else. And, doubtless, my going on this whaling voyage, formed
+ part of the grand programme of Providence that was drawn up a long time
+ ago. It came in as a sort of brief interlude and solo between more
+ extensive performances. I take it that this part of the bill must have run
+ something like this:
+ </p>
+ <p>
+ "<i>Grand Contested Election for the Presidency of the United States.</i>
+ "WHALING VOYAGE BY ONE ISHMAEL. "BLOODY BATTLE IN AFFGHANISTAN."
+ </p>
+ <p>
+ Though I cannot tell why it was exactly that those stage managers, the
+ Fates, put me down for this shabby part of a whaling voyage, when others
+ were set down for magnificent parts in high tragedies, and short and easy
+ parts in genteel comedies, and jolly parts in farces—though I cannot
+ tell why this was exactly; yet, now that I recall all the circumstances, I
+ think I can see a little into the springs and motives which being
+ cunningly presented to me under various disguises, induced me to set about
+ performing the part I did, besides cajoling me into the delusion that it
+ was a choice resulting from my own unbiased freewill and discriminating
+ judgment.
+ </p>
+ <p>
+ Chief among these motives was the overwhelming idea of the great whale
+ himself. Such a portentous and mysterious monster roused all my curiosity.
+ Then the wild and distant seas where he rolled his island bulk; the
+ undeliverable, nameless perils of the whale; these, with all the attending
+ marvels of a thousand Patagonian sights and sounds, helped to sway me to
+ my wish. With other men, perhaps, such things would not have been
+ inducements; but as for me, I am tormented with an everlasting itch for
+ things remote. I love to sail forbidden seas, and land on barbarous
+ coasts. Not ignoring what is good, I am quick to perceive a horror, and
+ could still be social with it—would they let me—since it is
+ but well to be on friendly terms with all the inmates of the place one
+ lodges in.
+ </p>
+ <p>
+ By reason of these things, then, the whaling voyage was welcome; the great
+ flood-gates of the wonder-world swung open, and in the wild conceits that
+ swayed me to my purpose, two and two there floated into my inmost soul,
+ endless processions of the whale, and, mid most of them all, one grand
+ hooded phantom, like a snow hill in the air.
+ </p>
+</body>
+</html>
diff --git a/toolkit/components/nsDefaultCLH.js b/toolkit/components/nsDefaultCLH.js
new file mode 100644
index 0000000000..a081bae498
--- /dev/null
+++ b/toolkit/components/nsDefaultCLH.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const nsISupports = Components.interfaces.nsISupports;
+
+const nsICommandLine = Components.interfaces.nsICommandLine;
+const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
+const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+const nsISupportsString = Components.interfaces.nsISupportsString;
+const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
+const nsIProperties = Components.interfaces.nsIProperties;
+const nsIFile = Components.interfaces.nsIFile;
+const nsISimpleEnumerator = Components.interfaces.nsISimpleEnumerator;
+
+/**
+ * This file provides a generic default command-line handler.
+ *
+ * It opens the chrome window specified by the pref "toolkit.defaultChromeURI"
+ * with the flags specified by the pref "toolkit.defaultChromeFeatures"
+ * or "chrome,dialog=no,all" is it is not available.
+ * The arguments passed to the window are the nsICommandLine instance.
+ *
+ * It doesn't do anything if the pref "toolkit.defaultChromeURI" is unset.
+ */
+
+function getDirectoryService()
+{
+ return Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(nsIProperties);
+}
+
+function nsDefaultCLH() { }
+nsDefaultCLH.prototype = {
+ classID: Components.ID("{6ebc941a-f2ff-4d56-b3b6-f7d0b9d73344}"),
+
+ /* nsISupports */
+
+ QueryInterface : XPCOMUtils.generateQI([nsICommandLineHandler]),
+
+ /* nsICommandLineHandler */
+
+ handle : function clh_handle(cmdLine) {
+ var printDir;
+ while ((printDir = cmdLine.handleFlagWithParam("print-xpcom-dir", false))) {
+ var out = "print-xpcom-dir(\"" + printDir + "\"): ";
+ try {
+ out += getDirectoryService().get(printDir, nsIFile).path;
+ }
+ catch (e) {
+ out += "<Not Provided>";
+ }
+
+ dump(out + "\n");
+ Components.utils.reportError(out);
+ }
+
+ var printDirList;
+ while ((printDirList = cmdLine.handleFlagWithParam("print-xpcom-dirlist",
+ false))) {
+ out = "print-xpcom-dirlist(\"" + printDirList + "\"): ";
+ try {
+ var list = getDirectoryService().get(printDirList,
+ nsISimpleEnumerator);
+ while (list.hasMoreElements())
+ out += list.getNext().QueryInterface(nsIFile).path + ";";
+ }
+ catch (e) {
+ out += "<Not Provided>";
+ }
+
+ dump(out + "\n");
+ Components.utils.reportError(out);
+ }
+
+ if (cmdLine.handleFlag("silent", false)) {
+ cmdLine.preventDefault = true;
+ }
+
+ if (cmdLine.preventDefault)
+ return;
+
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(nsIPrefBranch);
+
+ try {
+ var singletonWindowType =
+ prefs.getCharPref("toolkit.singletonWindowType");
+ var windowMediator =
+ Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+
+ var win = windowMediator.getMostRecentWindow(singletonWindowType);
+ if (win) {
+ win.focus();
+ cmdLine.preventDefault = true;
+ return;
+ }
+ }
+ catch (e) { }
+
+ // if the pref is missing, ignore the exception
+ try {
+ var chromeURI = prefs.getCharPref("toolkit.defaultChromeURI");
+
+ var flags = "chrome,dialog=no,all";
+ try {
+ flags = prefs.getCharPref("toolkit.defaultChromeFeatures");
+ }
+ catch (e) { }
+
+ var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(nsIWindowWatcher);
+ wwatch.openWindow(null, chromeURI, "_blank",
+ flags, cmdLine);
+ }
+ catch (e) { }
+ },
+
+ helpInfo : "",
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsDefaultCLH]);
diff --git a/toolkit/components/nsDefaultCLH.manifest b/toolkit/components/nsDefaultCLH.manifest
new file mode 100644
index 0000000000..075c8c416b
--- /dev/null
+++ b/toolkit/components/nsDefaultCLH.manifest
@@ -0,0 +1,3 @@
+component {6ebc941a-f2ff-4d56-b3b6-f7d0b9d73344} nsDefaultCLH.js
+contract @mozilla.org/toolkit/default-clh;1 {6ebc941a-f2ff-4d56-b3b6-f7d0b9d73344}
+category command-line-handler y-default @mozilla.org/toolkit/default-clh;1
diff --git a/toolkit/components/osfile/NativeOSFileInternals.cpp b/toolkit/components/osfile/NativeOSFileInternals.cpp
new file mode 100644
index 0000000000..e4725d390d
--- /dev/null
+++ b/toolkit/components/osfile/NativeOSFileInternals.cpp
@@ -0,0 +1,916 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Native implementation of some OS.File operations.
+ */
+
+#include "nsString.h"
+#include "nsNetCID.h"
+#include "nsThreadUtils.h"
+#include "nsXPCOMCID.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsServiceManagerUtils.h"
+#include "nsProxyRelease.h"
+
+#include "nsINativeOSFileInternals.h"
+#include "NativeOSFileInternals.h"
+#include "mozilla/dom/NativeOSFileInternalsBinding.h"
+
+#include "nsIUnicodeDecoder.h"
+#include "nsIEventTarget.h"
+
+#include "mozilla/dom/EncodingUtils.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Scoped.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/TimeStamp.h"
+
+#include "prio.h"
+#include "prerror.h"
+#include "private/pprio.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Utility.h"
+#include "xpcpublic.h"
+
+#include <algorithm>
+#if defined(XP_UNIX)
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#endif // defined (XP_UNIX)
+
+#if defined(XP_WIN)
+#include <windows.h>
+#endif // defined (XP_WIN)
+
+namespace mozilla {
+
+MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc, PR_Close)
+
+namespace {
+
+// Utilities for safely manipulating ArrayBuffer contents even in the
+// absence of a JSContext.
+
+/**
+ * The C buffer underlying to an ArrayBuffer. Throughout the code, we manipulate
+ * this instead of a void* buffer, as this lets us transfer data across threads
+ * and into JavaScript without copy.
+ */
+struct ArrayBufferContents {
+ /**
+ * The data of the ArrayBuffer. This is the pointer manipulated to
+ * read/write the contents of the buffer.
+ */
+ uint8_t* data;
+ /**
+ * The number of bytes in the ArrayBuffer.
+ */
+ size_t nbytes;
+};
+
+/**
+ * RAII for ArrayBufferContents.
+ */
+struct ScopedArrayBufferContentsTraits {
+ typedef ArrayBufferContents type;
+ const static type empty() {
+ type result = {0, 0};
+ return result;
+ }
+ static void release(type ptr) {
+ js_free(ptr.data);
+ ptr.data = nullptr;
+ ptr.nbytes = 0;
+ }
+};
+
+struct MOZ_NON_TEMPORARY_CLASS ScopedArrayBufferContents: public Scoped<ScopedArrayBufferContentsTraits> {
+ explicit ScopedArrayBufferContents(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM):
+ Scoped<ScopedArrayBufferContentsTraits>(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_TO_PARENT)
+ { }
+ explicit ScopedArrayBufferContents(const ArrayBufferContents& v
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM):
+ Scoped<ScopedArrayBufferContentsTraits>(v MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT)
+ { }
+ ScopedArrayBufferContents& operator=(ArrayBufferContents ptr) {
+ Scoped<ScopedArrayBufferContentsTraits>::operator=(ptr);
+ return *this;
+ }
+
+ /**
+ * Request memory for this ArrayBufferContent. This memory may later
+ * be used to create an ArrayBuffer object (possibly on another
+ * thread) without copy.
+ *
+ * @return true In case of success, false otherwise.
+ */
+ bool Allocate(uint32_t length) {
+ dispose();
+ ArrayBufferContents& value = rwget();
+ void *ptr = js_calloc(1, length);
+ if (ptr) {
+ value.data = (uint8_t *) ptr;
+ value.nbytes = length;
+ return true;
+ }
+ return false;
+ }
+private:
+ explicit ScopedArrayBufferContents(ScopedArrayBufferContents& source) = delete;
+ ScopedArrayBufferContents& operator=(ScopedArrayBufferContents& source) = delete;
+};
+
+///////// Cross-platform issues
+
+// Platform specific constants. As OS.File always uses OS-level
+// errors, we need to map a few high-level errors to OS-level
+// constants.
+#if defined(XP_UNIX)
+#define OS_ERROR_NOMEM ENOMEM
+#define OS_ERROR_INVAL EINVAL
+#define OS_ERROR_TOO_LARGE EFBIG
+#define OS_ERROR_RACE EIO
+#elif defined(XP_WIN)
+#define OS_ERROR_NOMEM ERROR_NOT_ENOUGH_MEMORY
+#define OS_ERROR_INVAL ERROR_BAD_ARGUMENTS
+#define OS_ERROR_TOO_LARGE ERROR_FILE_TOO_LARGE
+#define OS_ERROR_RACE ERROR_SHARING_VIOLATION
+#else
+#error "We do not have platform-specific constants for this platform"
+#endif
+
+///////// Results of OS.File operations
+
+/**
+ * Base class for results passed to the callbacks.
+ *
+ * This base class implements caching of JS values returned to the client.
+ * We make use of this caching in derived classes e.g. to avoid accidents
+ * when we transfer data allocated on another thread into JS. Note that
+ * this caching can lead to cycles (e.g. if a client adds a back-reference
+ * in the JS value), so we implement all Cycle Collector primitives in
+ * AbstractResult.
+ */
+class AbstractResult: public nsINativeOSFileResult {
+public:
+ NS_DECL_NSINATIVEOSFILERESULT
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AbstractResult)
+
+ /**
+ * Construct the result object. Must be called on the main thread
+ * as the AbstractResult is cycle-collected.
+ *
+ * @param aStartDate The instant at which the operation was
+ * requested. Used to collect Telemetry statistics.
+ */
+ explicit AbstractResult(TimeStamp aStartDate)
+ : mStartDate(aStartDate)
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::HoldJSObjects(this);
+ }
+
+ /**
+ * Setup the AbstractResult once data is available.
+ *
+ * @param aDispatchDate The instant at which the IO thread received
+ * the operation request. Used to collect Telemetry statistics.
+ * @param aExecutionDuration The duration of the operation on the
+ * IO thread.
+ */
+ void Init(TimeStamp aDispatchDate,
+ TimeDuration aExecutionDuration) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ mDispatchDuration = (aDispatchDate - mStartDate);
+ mExecutionDuration = aExecutionDuration;
+ }
+
+ /**
+ * Drop any data that could lead to a cycle.
+ */
+ void DropJSData() {
+ mCachedResult = JS::UndefinedValue();
+ }
+
+protected:
+ virtual ~AbstractResult() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DropJSData();
+ mozilla::DropJSObjects(this);
+ }
+
+ virtual nsresult GetCacheableResult(JSContext *cx, JS::MutableHandleValue aResult) = 0;
+
+private:
+ TimeStamp mStartDate;
+ TimeDuration mDispatchDuration;
+ TimeDuration mExecutionDuration;
+ JS::Heap<JS::Value> mCachedResult;
+};
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AbstractResult)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AbstractResult)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AbstractResult)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbstractResult)
+ NS_INTERFACE_MAP_ENTRY(nsINativeOSFileResult)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AbstractResult)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedResult)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractResult)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AbstractResult)
+ tmp->DropJSData();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMETHODIMP
+AbstractResult::GetDispatchDurationMS(double *aDispatchDuration)
+{
+ *aDispatchDuration = mDispatchDuration.ToMilliseconds();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AbstractResult::GetExecutionDurationMS(double *aExecutionDuration)
+{
+ *aExecutionDuration = mExecutionDuration.ToMilliseconds();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AbstractResult::GetResult(JSContext *cx, JS::MutableHandleValue aResult)
+{
+ if (mCachedResult.isUndefined()) {
+ nsresult rv = GetCacheableResult(cx, aResult);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mCachedResult = aResult;
+ return NS_OK;
+ }
+ aResult.set(mCachedResult);
+ return NS_OK;
+}
+
+/**
+ * Return a result as a string.
+ *
+ * In this implementation, attribute |result| is a string. Strings are
+ * passed to JS without copy.
+ */
+class StringResult final : public AbstractResult
+{
+public:
+ explicit StringResult(TimeStamp aStartDate)
+ : AbstractResult(aStartDate)
+ {
+ }
+
+ /**
+ * Initialize the object once the contents of the result as available.
+ *
+ * @param aContents The string to pass to JavaScript. Ownership of the
+ * string and its contents is passed to StringResult. The string must
+ * be valid UTF-16.
+ */
+ void Init(TimeStamp aDispatchDate,
+ TimeDuration aExecutionDuration,
+ nsString& aContents) {
+ AbstractResult::Init(aDispatchDate, aExecutionDuration);
+ mContents = aContents;
+ }
+
+protected:
+ nsresult GetCacheableResult(JSContext* cx, JS::MutableHandleValue aResult) override;
+
+private:
+ nsString mContents;
+};
+
+nsresult
+StringResult::GetCacheableResult(JSContext* cx, JS::MutableHandleValue aResult)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mContents.get());
+
+ // Convert mContents to a js string without copy. Note that this
+ // may have the side-effect of stealing the contents of the string
+ // from XPCOM and into JS.
+ if (!xpc::StringToJsval(cx, mContents, aResult)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+
+/**
+ * Return a result as a Uint8Array.
+ *
+ * In this implementation, attribute |result| is a Uint8Array. The array
+ * is passed to JS without memory copy.
+ */
+class TypedArrayResult final : public AbstractResult
+{
+public:
+ explicit TypedArrayResult(TimeStamp aStartDate)
+ : AbstractResult(aStartDate)
+ {
+ }
+
+ /**
+ * @param aContents The contents to pass to JS. Calling this method.
+ * transmits ownership of the ArrayBufferContents to the TypedArrayResult.
+ * Do not reuse this value anywhere else.
+ */
+ void Init(TimeStamp aDispatchDate,
+ TimeDuration aExecutionDuration,
+ ArrayBufferContents aContents) {
+ AbstractResult::Init(aDispatchDate, aExecutionDuration);
+ mContents = aContents;
+ }
+
+protected:
+ nsresult GetCacheableResult(JSContext* cx, JS::MutableHandleValue aResult) override;
+private:
+ ScopedArrayBufferContents mContents;
+};
+
+nsresult
+TypedArrayResult::GetCacheableResult(JSContext* cx, JS::MutableHandle<JS::Value> aResult)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // We cannot simply construct a typed array using contents.data as
+ // this would allow us to have several otherwise unrelated
+ // ArrayBuffers with the same underlying C buffer. As this would be
+ // very unsafe, we need to cache the result once we have it.
+
+ const ArrayBufferContents& contents = mContents.get();
+ MOZ_ASSERT(contents.data);
+
+ JS::Rooted<JSObject*>
+ arrayBuffer(cx, JS_NewArrayBufferWithContents(cx, contents.nbytes, contents.data));
+ if (!arrayBuffer) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS::Rooted<JSObject*>
+ result(cx, JS_NewUint8ArrayWithBuffer(cx, arrayBuffer,
+ 0, contents.nbytes));
+ if (!result) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ // The memory of contents has been allocated on a thread that
+ // doesn't have a JSRuntime, hence without a context. Now that we
+ // have a context, attach the memory to where it belongs.
+ JS_updateMallocCounter(cx, contents.nbytes);
+ mContents.forget();
+
+ aResult.setObject(*result);
+ return NS_OK;
+}
+
+//////// Callback events
+
+/**
+ * An event used to notify asynchronously of an error.
+ */
+class ErrorEvent final : public Runnable {
+public:
+ /**
+ * @param aOnSuccess The success callback.
+ * @param aOnError The error callback.
+ * @param aDiscardedResult The discarded result.
+ * @param aOperation The name of the operation, used for error reporting.
+ * @param aOSError The OS error of the operation, as returned by errno/
+ * GetLastError().
+ *
+ * Note that we pass both the success callback and the error
+ * callback, as well as the discarded result to ensure that they are
+ * all released on the main thread, rather than on the IO thread
+ * (which would hopefully segfault). Also, we pass the callbacks as
+ * alread_AddRefed to ensure that we do not manipulate main-thread
+ * only refcounters off the main thread.
+ */
+ ErrorEvent(nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError,
+ already_AddRefed<AbstractResult>& aDiscardedResult,
+ const nsACString& aOperation,
+ int32_t aOSError)
+ : mOnSuccess(aOnSuccess)
+ , mOnError(aOnError)
+ , mDiscardedResult(aDiscardedResult)
+ , mOSError(aOSError)
+ , mOperation(aOperation)
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ (void)mOnError->Complete(mOperation, mOSError);
+
+ // Ensure that the callbacks are released on the main thread.
+ mOnSuccess = nullptr;
+ mOnError = nullptr;
+ mDiscardedResult = nullptr;
+
+ return NS_OK;
+ }
+ private:
+ // The callbacks. Maintained as nsMainThreadPtrHandle as they are generally
+ // xpconnect values, which cannot be manipulated with nsCOMPtr off
+ // the main thread. We store both the success callback and the
+ // error callback to ensure that they are safely released on the
+ // main thread.
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> mOnSuccess;
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> mOnError;
+ RefPtr<AbstractResult> mDiscardedResult;
+ int32_t mOSError;
+ nsCString mOperation;
+};
+
+/**
+ * An event used to notify of a success.
+ */
+class SuccessEvent final : public Runnable {
+public:
+ /**
+ * @param aOnSuccess The success callback.
+ * @param aOnError The error callback.
+ *
+ * Note that we pass both the success callback and the error
+ * callback to ensure that they are both released on the main
+ * thread, rather than on the IO thread (which would hopefully
+ * segfault). Also, we pass them as alread_AddRefed to ensure that
+ * we do not manipulate xpconnect refcounters off the main thread
+ * (which is illegal).
+ */
+ SuccessEvent(nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError,
+ already_AddRefed<nsINativeOSFileResult>& aResult)
+ : mOnSuccess(aOnSuccess)
+ , mOnError(aOnError)
+ , mResult(aResult)
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ (void)mOnSuccess->Complete(mResult);
+
+ // Ensure that the callbacks are released on the main thread.
+ mOnSuccess = nullptr;
+ mOnError = nullptr;
+ mResult = nullptr;
+
+ return NS_OK;
+ }
+ private:
+ // The callbacks. Maintained as nsMainThreadPtrHandle as they are generally
+ // xpconnect values, which cannot be manipulated with nsCOMPtr off
+ // the main thread. We store both the success callback and the
+ // error callback to ensure that they are safely released on the
+ // main thread.
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> mOnSuccess;
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> mOnError;
+ RefPtr<nsINativeOSFileResult> mResult;
+};
+
+
+//////// Action events
+
+/**
+ * Base class shared by actions.
+ */
+class AbstractDoEvent: public Runnable {
+public:
+ AbstractDoEvent(nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError)
+ : mOnSuccess(aOnSuccess)
+ , mOnError(aOnError)
+#if defined(DEBUG)
+ , mResolved(false)
+#endif // defined(DEBUG)
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ /**
+ * Fail, asynchronously.
+ */
+ void Fail(const nsACString& aOperation,
+ already_AddRefed<AbstractResult>&& aDiscardedResult,
+ int32_t aOSError = 0) {
+ Resolve();
+ RefPtr<ErrorEvent> event = new ErrorEvent(mOnSuccess,
+ mOnError,
+ aDiscardedResult,
+ aOperation,
+ aOSError);
+ nsresult rv = NS_DispatchToMainThread(event);
+ if (NS_FAILED(rv)) {
+ // Last ditch attempt to release on the main thread - some of
+ // the members of event are not thread-safe, so letting the
+ // pointer go out of scope would cause a crash.
+ NS_ReleaseOnMainThread(event.forget());
+ }
+ }
+
+ /**
+ * Succeed, asynchronously.
+ */
+ void Succeed(already_AddRefed<nsINativeOSFileResult>&& aResult) {
+ Resolve();
+ RefPtr<SuccessEvent> event = new SuccessEvent(mOnSuccess,
+ mOnError,
+ aResult);
+ nsresult rv = NS_DispatchToMainThread(event);
+ if (NS_FAILED(rv)) {
+ // Last ditch attempt to release on the main thread - some of
+ // the members of event are not thread-safe, so letting the
+ // pointer go out of scope would cause a crash.
+ NS_ReleaseOnMainThread(event.forget());
+ }
+
+ }
+
+private:
+
+ /**
+ * Mark the event as complete, for debugging purposes.
+ */
+ void Resolve() {
+#if defined(DEBUG)
+ MOZ_ASSERT(!mResolved);
+ mResolved = true;
+#endif // defined(DEBUG)
+ }
+
+private:
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> mOnSuccess;
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> mOnError;
+#if defined(DEBUG)
+ // |true| once the action is complete
+ bool mResolved;
+#endif // defined(DEBUG)
+};
+
+/**
+ * An abstract event implementing reading from a file.
+ *
+ * Concrete subclasses are responsible for handling the
+ * data obtained from the file and possibly post-processing it.
+ */
+class AbstractReadEvent: public AbstractDoEvent {
+public:
+ /**
+ * @param aPath The path of the file.
+ */
+ AbstractReadEvent(const nsAString& aPath,
+ const uint64_t aBytes,
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError)
+ : AbstractDoEvent(aOnSuccess, aOnError)
+ , mPath(aPath)
+ , mBytes(aBytes)
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ TimeStamp dispatchDate = TimeStamp::Now();
+
+ nsresult rv = BeforeRead();
+ if (NS_FAILED(rv)) {
+ // Error reporting is handled by BeforeRead();
+ return NS_OK;
+ }
+
+ ScopedArrayBufferContents buffer;
+ rv = Read(buffer);
+ if (NS_FAILED(rv)) {
+ // Error reporting is handled by Read();
+ return NS_OK;
+ }
+
+ AfterRead(dispatchDate, buffer);
+ return NS_OK;
+ }
+
+ private:
+ /**
+ * Read synchronously.
+ *
+ * Must be called off the main thread.
+ *
+ * @param aBuffer The destination buffer.
+ */
+ nsresult Read(ScopedArrayBufferContents& aBuffer)
+ {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ ScopedPRFileDesc file;
+#if defined(XP_WIN)
+ // On Windows, we can't use PR_OpenFile because it doesn't
+ // handle UTF-16 encoding, which is pretty bad. In addition,
+ // PR_OpenFile opens files without sharing, which is not the
+ // general semantics of OS.File.
+ HANDLE handle =
+ ::CreateFileW(mPath.get(),
+ GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ /*Security attributes*/nullptr,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
+ /*Template file*/ nullptr);
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ Fail(NS_LITERAL_CSTRING("open"), nullptr, ::GetLastError());
+ return NS_ERROR_FAILURE;
+ }
+
+ file = PR_ImportFile((PROsfd)handle);
+ if (!file) {
+ // |file| is closed by PR_ImportFile
+ Fail(NS_LITERAL_CSTRING("ImportFile"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+
+#else
+ // On other platforms, PR_OpenFile will do.
+ NS_ConvertUTF16toUTF8 path(mPath);
+ file = PR_OpenFile(path.get(), PR_RDONLY, 0);
+ if (!file) {
+ Fail(NS_LITERAL_CSTRING("open"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+
+#endif // defined(XP_XIN)
+
+ PRFileInfo64 stat;
+ if (PR_GetOpenFileInfo64(file, &stat) != PR_SUCCESS) {
+ Fail(NS_LITERAL_CSTRING("stat"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+
+ uint64_t bytes = std::min((uint64_t)stat.size, mBytes);
+ if (bytes > UINT32_MAX) {
+ Fail(NS_LITERAL_CSTRING("Arithmetics"), nullptr, OS_ERROR_INVAL);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!aBuffer.Allocate(bytes)) {
+ Fail(NS_LITERAL_CSTRING("allocate"), nullptr, OS_ERROR_NOMEM);
+ return NS_ERROR_FAILURE;
+ }
+
+ uint64_t total_read = 0;
+ int32_t just_read = 0;
+ char* dest_chars = reinterpret_cast<char*>(aBuffer.rwget().data);
+ do {
+ just_read = PR_Read(file, dest_chars + total_read,
+ std::min(uint64_t(PR_INT32_MAX), bytes - total_read));
+ if (just_read == -1) {
+ Fail(NS_LITERAL_CSTRING("read"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+ total_read += just_read;
+ } while (just_read != 0 && total_read < bytes);
+ if (total_read != bytes) {
+ // We seem to have a race condition here.
+ Fail(NS_LITERAL_CSTRING("read"), nullptr, OS_ERROR_RACE);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+ }
+
+protected:
+ /**
+ * Any steps that need to be taken before reading.
+ *
+ * In case of error, this method should call Fail() and return
+ * a failure code.
+ */
+ virtual
+ nsresult BeforeRead() {
+ return NS_OK;
+ }
+
+ /**
+ * Proceed after reading.
+ */
+ virtual
+ void AfterRead(TimeStamp aDispatchDate, ScopedArrayBufferContents& aBuffer) = 0;
+
+ protected:
+ const nsString mPath;
+ const uint64_t mBytes;
+};
+
+/**
+ * An implementation of a Read event that provides the data
+ * as a TypedArray.
+ */
+class DoReadToTypedArrayEvent final : public AbstractReadEvent {
+public:
+ DoReadToTypedArrayEvent(const nsAString& aPath,
+ const uint32_t aBytes,
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError)
+ : AbstractReadEvent(aPath, aBytes,
+ aOnSuccess, aOnError)
+ , mResult(new TypedArrayResult(TimeStamp::Now()))
+ { }
+
+ ~DoReadToTypedArrayEvent() {
+ // If AbstractReadEvent::Run() has bailed out, we may need to cleanup
+ // mResult, which is main-thread only data
+ if (!mResult) {
+ return;
+ }
+ NS_ReleaseOnMainThread(mResult.forget());
+ }
+
+protected:
+ void AfterRead(TimeStamp aDispatchDate,
+ ScopedArrayBufferContents& aBuffer) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+ mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, aBuffer.forget());
+ Succeed(mResult.forget());
+ }
+
+ private:
+ RefPtr<TypedArrayResult> mResult;
+};
+
+/**
+ * An implementation of a Read event that provides the data
+ * as a JavaScript string.
+ */
+class DoReadToStringEvent final : public AbstractReadEvent {
+public:
+ DoReadToStringEvent(const nsAString& aPath,
+ const nsACString& aEncoding,
+ const uint32_t aBytes,
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError)
+ : AbstractReadEvent(aPath, aBytes, aOnSuccess, aOnError)
+ , mEncoding(aEncoding)
+ , mResult(new StringResult(TimeStamp::Now()))
+ { }
+
+ ~DoReadToStringEvent() {
+ // If AbstraactReadEvent::Run() has bailed out, we may need to cleanup
+ // mResult, which is main-thread only data
+ if (!mResult) {
+ return;
+ }
+ NS_ReleaseOnMainThread(mResult.forget());
+ }
+
+protected:
+ nsresult BeforeRead() override {
+ // Obtain the decoder. We do this before reading to avoid doing
+ // any unnecessary I/O in case the name of the encoding is incorrect.
+ MOZ_ASSERT(!NS_IsMainThread());
+ nsAutoCString encodingName;
+ if (!dom::EncodingUtils::FindEncodingForLabel(mEncoding, encodingName)) {
+ Fail(NS_LITERAL_CSTRING("Decode"), mResult.forget(), OS_ERROR_INVAL);
+ return NS_ERROR_FAILURE;
+ }
+ mDecoder = dom::EncodingUtils::DecoderForEncoding(encodingName);
+ if (!mDecoder) {
+ Fail(NS_LITERAL_CSTRING("DecoderForEncoding"), mResult.forget(), OS_ERROR_INVAL);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+ }
+
+ void AfterRead(TimeStamp aDispatchDate,
+ ScopedArrayBufferContents& aBuffer) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ int32_t maxChars;
+ const char* sourceChars = reinterpret_cast<const char*>(aBuffer.get().data);
+ int32_t sourceBytes = aBuffer.get().nbytes;
+ if (sourceBytes < 0) {
+ Fail(NS_LITERAL_CSTRING("arithmetics"), mResult.forget(), OS_ERROR_TOO_LARGE);
+ return;
+ }
+
+ nsresult rv = mDecoder->GetMaxLength(sourceChars, sourceBytes, &maxChars);
+ if (NS_FAILED(rv)) {
+ Fail(NS_LITERAL_CSTRING("GetMaxLength"), mResult.forget(), OS_ERROR_INVAL);
+ return;
+ }
+
+ if (maxChars < 0) {
+ Fail(NS_LITERAL_CSTRING("arithmetics"), mResult.forget(), OS_ERROR_TOO_LARGE);
+ return;
+ }
+
+ nsString resultString;
+ resultString.SetLength(maxChars);
+ if (resultString.Length() != (nsString::size_type)maxChars) {
+ Fail(NS_LITERAL_CSTRING("allocation"), mResult.forget(), OS_ERROR_TOO_LARGE);
+ return;
+ }
+
+
+ rv = mDecoder->Convert(sourceChars, &sourceBytes,
+ resultString.BeginWriting(), &maxChars);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ resultString.SetLength(maxChars);
+
+ mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, resultString);
+ Succeed(mResult.forget());
+ }
+
+ private:
+ nsCString mEncoding;
+ nsCOMPtr<nsIUnicodeDecoder> mDecoder;
+ RefPtr<StringResult> mResult;
+};
+
+} // namespace
+
+// The OS.File service
+
+NS_IMPL_ISUPPORTS(NativeOSFileInternalsService, nsINativeOSFileInternalsService);
+
+NS_IMETHODIMP
+NativeOSFileInternalsService::Read(const nsAString& aPath,
+ JS::HandleValue aOptions,
+ nsINativeOSFileSuccessCallback *aOnSuccess,
+ nsINativeOSFileErrorCallback *aOnError,
+ JSContext* cx)
+{
+ // Extract options
+ nsCString encoding;
+ uint64_t bytes = UINT64_MAX;
+
+ if (aOptions.isObject()) {
+ dom::NativeOSFileReadOptions dict;
+ if (!dict.Init(cx, aOptions)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (dict.mEncoding.WasPassed()) {
+ CopyUTF16toUTF8(dict.mEncoding.Value(), encoding);
+ }
+
+ if (dict.mBytes.WasPassed() && !dict.mBytes.Value().IsNull()) {
+ bytes = dict.mBytes.Value().Value();
+ }
+ }
+
+ // Prepare the off main thread event and dispatch it
+ nsCOMPtr<nsINativeOSFileSuccessCallback> onSuccess(aOnSuccess);
+ nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> onSuccessHandle(
+ new nsMainThreadPtrHolder<nsINativeOSFileSuccessCallback>(onSuccess));
+ nsCOMPtr<nsINativeOSFileErrorCallback> onError(aOnError);
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> onErrorHandle(
+ new nsMainThreadPtrHolder<nsINativeOSFileErrorCallback>(onError));
+
+ RefPtr<AbstractDoEvent> event;
+ if (encoding.IsEmpty()) {
+ event = new DoReadToTypedArrayEvent(aPath, bytes,
+ onSuccessHandle,
+ onErrorHandle);
+ } else {
+ event = new DoReadToStringEvent(aPath, encoding, bytes,
+ onSuccessHandle,
+ onErrorHandle);
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return target->Dispatch(event, NS_DISPATCH_NORMAL);
+}
+
+} // namespace mozilla
+
diff --git a/toolkit/components/osfile/NativeOSFileInternals.h b/toolkit/components/osfile/NativeOSFileInternals.h
new file mode 100644
index 0000000000..2da9293578
--- /dev/null
+++ b/toolkit/components/osfile/NativeOSFileInternals.h
@@ -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/. */
+
+#ifndef mozilla_nativeosfileinternalservice_h__
+#define mozilla_nativeosfileinternalservice_h__
+
+#include "nsINativeOSFileInternals.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+
+class NativeOSFileInternalsService final : public nsINativeOSFileInternalsService {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINATIVEOSFILEINTERNALSSERVICE
+private:
+ ~NativeOSFileInternalsService() {}
+ // Avoid accidental use of built-in operator=
+ void operator=(const NativeOSFileInternalsService& other) = delete;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_finalizationwitnessservice_h__
diff --git a/toolkit/components/osfile/modules/moz.build b/toolkit/components/osfile/modules/moz.build
new file mode 100644
index 0000000000..7a0580ca38
--- /dev/null
+++ b/toolkit/components/osfile/modules/moz.build
@@ -0,0 +1,22 @@
+# -*- 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_JS_MODULES.osfile += [
+ 'osfile_async_front.jsm',
+ 'osfile_async_worker.js',
+ 'osfile_native.jsm',
+ 'osfile_shared_allthreads.jsm',
+ 'osfile_shared_front.jsm',
+ 'osfile_unix_allthreads.jsm',
+ 'osfile_unix_back.jsm',
+ 'osfile_unix_front.jsm',
+ 'osfile_win_allthreads.jsm',
+ 'osfile_win_back.jsm',
+ 'osfile_win_front.jsm',
+ 'ospath.jsm',
+ 'ospath_unix.jsm',
+ 'ospath_win.jsm',
+]
diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm
new file mode 100644
index 0000000000..181471cd81
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -0,0 +1,1533 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Asynchronous front-end for OS.File.
+ *
+ * This front-end is meant to be imported from the main thread. In turn,
+ * it spawns one worker (perhaps more in the future) and delegates all
+ * disk I/O to this worker.
+ *
+ * Documentation note: most of the functions and methods in this module
+ * return promises. For clarity, we denote as follows a promise that may resolve
+ * with type |A| and some value |value| or reject with type |B| and some
+ * reason |reason|
+ * @resolves {A} value
+ * @rejects {B} reason
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["OS"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+var SharedAll = {};
+Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+
+// Boilerplate, to simplify the transition to require()
+var LOG = SharedAll.LOG.bind(SharedAll, "Controller");
+var isTypedArray = SharedAll.isTypedArray;
+
+// The constructor for file errors.
+var SysAll = {};
+if (SharedAll.Constants.Win) {
+ Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll);
+} else if (SharedAll.Constants.libc) {
+ Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll);
+} else {
+ throw new Error("I am neither under Windows nor under a Posix system");
+}
+var OSError = SysAll.Error;
+var Type = SysAll.Type;
+
+var Path = {};
+Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
+
+// The library of promises.
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+// The implementation of communications
+Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
+Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
+var Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {});
+
+
+// It's possible for osfile.jsm to get imported before the profile is
+// set up. In this case, some path constants aren't yet available.
+// Here, we make them lazy loaders.
+
+function lazyPathGetter(constProp, dirKey) {
+ return function () {
+ let path;
+ try {
+ path = Services.dirsvc.get(dirKey, Ci.nsIFile).path;
+ delete SharedAll.Constants.Path[constProp];
+ SharedAll.Constants.Path[constProp] = path;
+ } catch (ex) {
+ // Ignore errors if the value still isn't available. Hopefully
+ // the next access will return it.
+ }
+
+ return path;
+ };
+}
+
+for (let [constProp, dirKey] of [
+ ["localProfileDir", "ProfLD"],
+ ["profileDir", "ProfD"],
+ ["userApplicationDataDir", "UAppData"],
+ ["winAppDataDir", "AppData"],
+ ["winStartMenuProgsDir", "Progs"],
+ ]) {
+
+ if (constProp in SharedAll.Constants.Path) {
+ continue;
+ }
+
+ LOG("Installing lazy getter for OS.Constants.Path." + constProp +
+ " because it isn't defined and profile may not be loaded.");
+ Object.defineProperty(SharedAll.Constants.Path, constProp, {
+ get: lazyPathGetter(constProp, dirKey),
+ });
+}
+
+/**
+ * Return a shallow clone of the enumerable properties of an object.
+ */
+var clone = SharedAll.clone;
+
+/**
+ * Extract a shortened version of an object, fit for logging.
+ *
+ * This function returns a copy of the original object in which all
+ * long strings, Arrays, TypedArrays, ArrayBuffers are removed and
+ * replaced with placeholders. Use this function to sanitize objects
+ * if you wish to log them or to keep them in memory.
+ *
+ * @param {*} obj The obj to shorten.
+ * @return {*} array A shorter object, fit for logging.
+ */
+function summarizeObject(obj) {
+ if (!obj) {
+ return null;
+ }
+ if (typeof obj == "string") {
+ if (obj.length > 1024) {
+ return {"Long string": obj.length};
+ }
+ return obj;
+ }
+ if (typeof obj == "object") {
+ if (Array.isArray(obj)) {
+ if (obj.length > 32) {
+ return {"Long array": obj.length};
+ }
+ return obj.map(summarizeObject);
+ }
+ if ("byteLength" in obj) {
+ // Assume TypedArray or ArrayBuffer
+ return {"Binary Data": obj.byteLength};
+ }
+ let result = {};
+ for (let k of Object.keys(obj)) {
+ result[k] = summarizeObject(obj[k]);
+ }
+ return result;
+ }
+ return obj;
+}
+
+// In order to expose Scheduler to the unfiltered Cu.import return value variant
+// on B2G we need to save it to `this`. This does not make it public;
+// EXPORTED_SYMBOLS still controls that in all cases.
+var Scheduler = this.Scheduler = {
+
+ /**
+ * |true| once we have sent at least one message to the worker.
+ * This field is unaffected by resetting the worker.
+ */
+ launched: false,
+
+ /**
+ * |true| once shutdown has begun i.e. we should reject any
+ * message, including resets.
+ */
+ shutdown: false,
+
+ /**
+ * A promise resolved once all currently pending operations are complete.
+ *
+ * This promise is never rejected and the result is always undefined.
+ */
+ queue: Promise.resolve(),
+
+ /**
+ * A promise resolved once all currently pending `kill` operations
+ * are complete.
+ *
+ * This promise is never rejected and the result is always undefined.
+ */
+ _killQueue: Promise.resolve(),
+
+ /**
+ * Miscellaneous debugging information
+ */
+ Debugging: {
+ /**
+ * The latest message sent and still waiting for a reply.
+ */
+ latestSent: undefined,
+
+ /**
+ * The latest reply received, or null if we are waiting for a reply.
+ */
+ latestReceived: undefined,
+
+ /**
+ * Number of messages sent to the worker. This includes the
+ * initial SET_DEBUG, if applicable.
+ */
+ messagesSent: 0,
+
+ /**
+ * Total number of messages ever queued, including the messages
+ * sent.
+ */
+ messagesQueued: 0,
+
+ /**
+ * Number of messages received from the worker.
+ */
+ messagesReceived: 0,
+ },
+
+ /**
+ * A timer used to automatically shut down the worker after some time.
+ */
+ resetTimer: null,
+
+ /**
+ * The worker to which to send requests.
+ *
+ * If the worker has never been created or has been reset, this is a
+ * fresh worker, initialized with osfile_async_worker.js.
+ *
+ * @type {PromiseWorker}
+ */
+ get worker() {
+ if (!this._worker) {
+ // Either the worker has never been created or it has been
+ // reset. In either case, it is time to instantiate the worker.
+ this._worker = new BasePromiseWorker("resource://gre/modules/osfile/osfile_async_worker.js");
+ this._worker.log = LOG;
+ this._worker.ExceptionHandlers["OS.File.Error"] = OSError.fromMsg;
+ }
+ return this._worker;
+ },
+
+ _worker: null,
+
+ /**
+ * Prepare to kill the OS.File worker after a few seconds.
+ */
+ restartTimer: function(arg) {
+ let delay;
+ try {
+ delay = Services.prefs.getIntPref("osfile.reset_worker_delay");
+ } catch(e) {
+ // Don't auto-shutdown if we don't have a delay preference set.
+ return;
+ }
+
+ if (this.resetTimer) {
+ clearTimeout(this.resetTimer);
+ }
+ this.resetTimer = setTimeout(
+ () => Scheduler.kill({reset: true, shutdown: false}),
+ delay
+ );
+ },
+
+ /**
+ * Shutdown OS.File.
+ *
+ * @param {*} options
+ * - {boolean} shutdown If |true|, reject any further request. Otherwise,
+ * further requests will resurrect the worker.
+ * - {boolean} reset If |true|, instruct the worker to shutdown if this
+ * would not cause leaks. Otherwise, assume that the worker will be shutdown
+ * through some other mean.
+ */
+ kill: function({shutdown, reset}) {
+ // Grab the kill queue to make sure that we
+ // cannot be interrupted by another call to `kill`.
+ let killQueue = this._killQueue;
+
+ // Deactivate the queue, to ensure that no message is sent
+ // to an obsolete worker (we reactivate it in the `finally`).
+ // This needs to be done right now so that we maintain relative
+ // ordering with calls to post(), etc.
+ let deferred = Promise.defer();
+ let savedQueue = this.queue;
+ this.queue = deferred.promise;
+
+ return this._killQueue = Task.spawn(function*() {
+
+ yield killQueue;
+ // From this point, and until the end of the Task, we are the
+ // only call to `kill`, regardless of any `yield`.
+
+ yield savedQueue;
+
+ try {
+ // Enter critical section: no yield in this block
+ // (we want to make sure that we remain the only
+ // request in the queue).
+
+ if (!this.launched || this.shutdown || !this._worker) {
+ // Nothing to kill
+ this.shutdown = this.shutdown || shutdown;
+ this._worker = null;
+ return null;
+ }
+
+ // Exit critical section
+
+ let message = ["Meta_shutdown", [reset]];
+
+ Scheduler.latestReceived = [];
+ Scheduler.latestSent = [Date.now(),
+ Task.Debugging.generateReadableStack(new Error().stack),
+ ...message];
+
+ // Wait for result
+ let resources;
+ try {
+ resources = yield this._worker.post(...message);
+
+ Scheduler.latestReceived = [Date.now(), message];
+ } catch (ex) {
+ LOG("Could not dispatch Meta_reset", ex);
+ // It's most likely a programmer error, but we'll assume that
+ // the worker has been shutdown, as it's less risky than the
+ // opposite stance.
+ resources = {openedFiles: [], openedDirectoryIterators: [], killed: true};
+
+ Scheduler.latestReceived = [Date.now(), message, ex];
+ }
+
+ let {openedFiles, openedDirectoryIterators, killed} = resources;
+ if (!reset
+ && (openedFiles && openedFiles.length
+ || ( openedDirectoryIterators && openedDirectoryIterators.length))) {
+ // The worker still holds resources. Report them.
+
+ let msg = "";
+ if (openedFiles.length > 0) {
+ msg += "The following files are still open:\n" +
+ openedFiles.join("\n");
+ }
+ if (openedDirectoryIterators.length > 0) {
+ msg += "The following directory iterators are still open:\n" +
+ openedDirectoryIterators.join("\n");
+ }
+
+ LOG("WARNING: File descriptors leaks detected.\n" + msg);
+ }
+
+ // Make sure that we do not leave an invalid |worker| around.
+ if (killed || shutdown) {
+ this._worker = null;
+ }
+
+ this.shutdown = shutdown;
+
+ return resources;
+
+ } finally {
+ // Resume accepting messages. If we have set |shutdown| to |true|,
+ // any pending/future request will be rejected. Otherwise, any
+ // pending/future request will spawn a new worker if necessary.
+ deferred.resolve();
+ }
+
+ }.bind(this));
+ },
+
+ /**
+ * Push a task at the end of the queue.
+ *
+ * @param {function} code A function returning a Promise.
+ * This function will be executed once all the previously
+ * pushed tasks have completed.
+ * @return {Promise} A promise with the same behavior as
+ * the promise returned by |code|.
+ */
+ push: function(code) {
+ let promise = this.queue.then(code);
+ // By definition, |this.queue| can never reject.
+ this.queue = promise.then(null, () => undefined);
+ // Fork |promise| to ensure that uncaught errors are reported
+ return promise.then(null, null);
+ },
+
+ /**
+ * Post a message to the worker thread.
+ *
+ * @param {string} method The name of the method to call.
+ * @param {...} args The arguments to pass to the method. These arguments
+ * must be clonable.
+ * @return {Promise} A promise conveying the result/error caused by
+ * calling |method| with arguments |args|.
+ */
+ post: function post(method, args = undefined, closure = undefined) {
+ if (this.shutdown) {
+ LOG("OS.File is not available anymore. The following request has been rejected.",
+ method, args);
+ return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method));
+ }
+ let firstLaunch = !this.launched;
+ this.launched = true;
+
+ if (firstLaunch && SharedAll.Config.DEBUG) {
+ // If we have delayed sending SET_DEBUG, do it now.
+ this.worker.post("SET_DEBUG", [true]);
+ Scheduler.Debugging.messagesSent++;
+ }
+
+ Scheduler.Debugging.messagesQueued++;
+ return this.push(Task.async(function*() {
+ if (this.shutdown) {
+ LOG("OS.File is not available anymore. The following request has been rejected.",
+ method, args);
+ throw new Error("OS.File has been shut down. Rejecting request to " + method);
+ }
+
+ // Update debugging information. As |args| may be quite
+ // expensive, we only keep a shortened version of it.
+ Scheduler.Debugging.latestReceived = null;
+ Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(args)];
+
+ // Don't kill the worker just yet
+ Scheduler.restartTimer();
+
+
+ let reply;
+ try {
+ try {
+ Scheduler.Debugging.messagesSent++;
+ Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2);
+ reply = yield this.worker.post(method, args, closure);
+ Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)];
+ return reply;
+ } finally {
+ Scheduler.Debugging.messagesReceived++;
+ }
+ } catch (error) {
+ Scheduler.Debugging.latestReceived = [Date.now(), error.message, error.fileName, error.lineNumber];
+ throw error;
+ } finally {
+ if (firstLaunch) {
+ Scheduler._updateTelemetry();
+ }
+ Scheduler.restartTimer();
+ }
+ }.bind(this)));
+ },
+
+ /**
+ * Post Telemetry statistics.
+ *
+ * This is only useful on first launch.
+ */
+ _updateTelemetry: function() {
+ let worker = this.worker;
+ let workerTimeStamps = worker.workerTimeStamps;
+ if (!workerTimeStamps) {
+ // If the first call to OS.File results in an uncaught errors,
+ // the timestamps are absent. As this case is a developer error,
+ // let's not waste time attempting to extract telemetry from it.
+ return;
+ }
+ let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS");
+ HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp);
+
+ let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS");
+ HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp);
+ }
+};
+
+const PREF_OSFILE_LOG = "toolkit.osfile.log";
+const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect";
+
+/**
+ * Safely read a PREF_OSFILE_LOG preference.
+ * Returns a value read or, in case of an error, oldPref or false.
+ *
+ * @param bool oldPref
+ * An optional value that the DEBUG flag was set to previously.
+ */
+function readDebugPref(prefName, oldPref = false) {
+ let pref = oldPref;
+ try {
+ pref = Services.prefs.getBoolPref(prefName);
+ } catch (x) {
+ // In case of an error when reading a pref keep it as is.
+ }
+ // If neither pref nor oldPref were set, default it to false.
+ return pref;
+};
+
+/**
+ * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag
+ * appropriately.
+ */
+Services.prefs.addObserver(PREF_OSFILE_LOG,
+ function prefObserver(aSubject, aTopic, aData) {
+ SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG);
+ if (Scheduler.launched) {
+ // Don't start the worker just to set this preference.
+ Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]);
+ }
+ }, false);
+SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false);
+
+Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT,
+ function prefObserver(aSubject, aTopic, aData) {
+ SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST);
+ }, false);
+SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false);
+
+
+/**
+ * If |true|, use the native implementaiton of OS.File methods
+ * whenever possible. Otherwise, force the use of the JS version.
+ */
+var nativeWheneverAvailable = true;
+const PREF_OSFILE_NATIVE = "toolkit.osfile.native";
+Services.prefs.addObserver(PREF_OSFILE_NATIVE,
+ function prefObserver(aSubject, aTopic, aData) {
+ nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable);
+ }, false);
+
+
+// Update worker's DEBUG flag if it's true.
+// Don't start the worker just for this, though.
+if (SharedAll.Config.DEBUG && Scheduler.launched) {
+ Scheduler.post("SET_DEBUG", [true]);
+}
+
+// Observer topics used for monitoring shutdown
+const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown";
+
+// Preference used to configure test shutdown observer.
+const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER =
+ "toolkit.osfile.test.shutdown.observer";
+
+AsyncShutdown.webWorkersShutdown.addBlocker(
+ "OS.File: flush pending requests, warn about unclosed files, shut down service.",
+ Task.async(function*() {
+ // Give clients a last chance to enqueue requests.
+ yield Barriers.shutdown.wait({crashAfterMS: null});
+
+ // Wait until all requests are complete and kill the worker.
+ yield Scheduler.kill({reset: false, shutdown: true});
+ }),
+ () => {
+ let details = Barriers.getDetails();
+ details.clients = Barriers.shutdown.state;
+ return details;
+ }
+);
+
+
+// Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or
+// disable the test shutdown event observer.
+// Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset.
+// Note: This is meant to be used for testing purposes only.
+Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER,
+ function prefObserver() {
+ // The temporary phase topic used to trigger the unclosed
+ // phase warning.
+ let TOPIC = null;
+ try {
+ TOPIC = Services.prefs.getCharPref(
+ PREF_OSFILE_TEST_SHUTDOWN_OBSERVER);
+ } catch (x) {
+ }
+ if (TOPIC) {
+ // Generate a phase, add a blocker.
+ // Note that this can work only if AsyncShutdown itself has been
+ // configured for testing by the testsuite.
+ let phase = AsyncShutdown._getPhase(TOPIC);
+ phase.addBlocker(
+ "(for testing purposes) OS.File: warn about unclosed files",
+ () => Scheduler.kill({shutdown: false, reset: false})
+ );
+ }
+ }, false);
+
+/**
+ * Representation of a file, with asynchronous methods.
+ *
+ * @param {*} fdmsg The _message_ representing the platform-specific file
+ * handle.
+ *
+ * @constructor
+ */
+var File = function File(fdmsg) {
+ // FIXME: At the moment, |File| does not close on finalize
+ // (see bug 777715)
+ this._fdmsg = fdmsg;
+ this._closeResult = null;
+ this._closed = null;
+};
+
+
+File.prototype = {
+ /**
+ * Close a file asynchronously.
+ *
+ * This method is idempotent.
+ *
+ * @return {promise}
+ * @resolves {null}
+ * @rejects {OS.File.Error}
+ */
+ close: function close() {
+ if (this._fdmsg != null) {
+ let msg = this._fdmsg;
+ this._fdmsg = null;
+ return this._closeResult =
+ Scheduler.post("File_prototype_close", [msg], this);
+ }
+ return this._closeResult;
+ },
+
+ /**
+ * Fetch information about the file.
+ *
+ * @return {promise}
+ * @resolves {OS.File.Info} The latest information about the file.
+ * @rejects {OS.File.Error}
+ */
+ stat: function stat() {
+ return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then(
+ File.Info.fromMsg
+ );
+ },
+
+ /**
+ * Write bytes from a buffer to this file.
+ *
+ * Note that, by default, this function may perform several I/O
+ * operations to ensure that the buffer is fully written.
+ *
+ * @param {Typed array | C pointer} buffer The buffer in which the
+ * the bytes are stored. The buffer must be large enough to
+ * accomodate |bytes| bytes. Using the buffer before the operation
+ * is complete is a BAD IDEA.
+ * @param {*=} options Optionally, an object that may contain the
+ * following fields:
+ * - {number} bytes The number of |bytes| to write from the buffer. If
+ * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
+ * if |buffer| is a C pointer.
+ *
+ * @return {number} The number of bytes actually written.
+ */
+ write: function write(buffer, options = {}) {
+ // If |buffer| is a typed array and there is no |bytes| options,
+ // we need to extract the |byteLength| now, as it will be lost
+ // by communication.
+ // Options might be a nullish value, so better check for that before using
+ // the |in| operator.
+ if (isTypedArray(buffer) && !(options && "bytes" in options)) {
+ // Preserve reference to option |outExecutionDuration|, if it is passed.
+ options = clone(options, ["outExecutionDuration"]);
+ options.bytes = buffer.byteLength;
+ }
+ return Scheduler.post("File_prototype_write",
+ [this._fdmsg,
+ Type.void_t.in_ptr.toMsg(buffer),
+ options],
+ buffer/*Ensure that |buffer| is not gc-ed*/);
+ },
+
+ /**
+ * Read bytes from this file to a new buffer.
+ *
+ * @param {number=} bytes If unspecified, read all the remaining bytes from
+ * this file. If specified, read |bytes| bytes, or less if the file does not
+ * contain that many bytes.
+ * @param {JSON} options
+ * @return {promise}
+ * @resolves {Uint8Array} An array containing the bytes read.
+ */
+ read: function read(nbytes, options = {}) {
+ let promise = Scheduler.post("File_prototype_read",
+ [this._fdmsg,
+ nbytes, options]);
+ return promise.then(
+ function onSuccess(data) {
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ });
+ },
+
+ /**
+ * Return the current position in the file, as bytes.
+ *
+ * @return {promise}
+ * @resolves {number} The current position in the file,
+ * as a number of bytes since the start of the file.
+ */
+ getPosition: function getPosition() {
+ return Scheduler.post("File_prototype_getPosition",
+ [this._fdmsg]);
+ },
+
+ /**
+ * Set the current position in the file, as bytes.
+ *
+ * @param {number} pos A number of bytes.
+ * @param {number} whence The reference position in the file,
+ * which may be either POS_START (from the start of the file),
+ * POS_END (from the end of the file) or POS_CUR (from the
+ * current position in the file).
+ *
+ * @return {promise}
+ */
+ setPosition: function setPosition(pos, whence) {
+ return Scheduler.post("File_prototype_setPosition",
+ [this._fdmsg, pos, whence]);
+ },
+
+ /**
+ * Flushes the file's buffers and causes all buffered data
+ * to be written.
+ * Disk flushes are very expensive and therefore should be used carefully,
+ * sparingly and only in scenarios where it is vital that data survives
+ * system crashes. Even though the function will be executed off the
+ * main-thread, it might still affect the overall performance of any running
+ * application.
+ *
+ * @return {promise}
+ */
+ flush: function flush() {
+ return Scheduler.post("File_prototype_flush",
+ [this._fdmsg]);
+ },
+
+ /**
+ * Set the file's access permissions. This does nothing on Windows.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+ setPermissions: function setPermissions(options = {}) {
+ return Scheduler.post("File_prototype_setPermissions",
+ [this._fdmsg, options]);
+ }
+};
+
+
+if (SharedAll.Constants.Sys.Name != "Android" && SharedAll.Constants.Sys.Name != "Gonk") {
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * WARNING: This method is not implemented on Android/B2G. On Android/B2G,
+ * you should use File.setDates instead.
+ *
+ * @return {promise}
+ * @rejects {TypeError}
+ * @rejects {OS.File.Error}
+ */
+ File.prototype.setDates = function(accessDate, modificationDate) {
+ return Scheduler.post("File_prototype_setDates",
+ [this._fdmsg, accessDate, modificationDate], this);
+ };
+}
+
+
+/**
+ * Open a file asynchronously.
+ *
+ * @return {promise}
+ * @resolves {OS.File}
+ * @rejects {OS.Error}
+ */
+File.open = function open(path, mode, options) {
+ return Scheduler.post(
+ "open", [Type.path.toMsg(path), mode, options],
+ path
+ ).then(
+ function onSuccess(msg) {
+ return new File(msg);
+ }
+ );
+};
+
+/**
+ * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name.
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
+ * If |false| use HEX numbers ie: filename-A65BC0.ext
+ * - {number} maxReadableNumber Used to limit the amount of tries after a failed
+ * file creation. Default is 20.
+ *
+ * @return {Object} contains A file object{file} and the path{path}.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+File.openUnique = function openUnique(path, options) {
+ return Scheduler.post(
+ "openUnique", [Type.path.toMsg(path), options],
+ path
+ ).then(
+ function onSuccess(msg) {
+ return {
+ path: msg.path,
+ file: new File(msg.file)
+ };
+ }
+ );
+};
+
+/**
+ * Get the information on the file.
+ *
+ * @return {promise}
+ * @resolves {OS.File.Info}
+ * @rejects {OS.Error}
+ */
+File.stat = function stat(path, options) {
+ return Scheduler.post(
+ "stat", [Type.path.toMsg(path), options],
+ path).then(File.Info.fromMsg);
+};
+
+
+/**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * @return {promise}
+ * @rejects {TypeError}
+ * @rejects {OS.File.Error}
+ */
+File.setDates = function setDates(path, accessDate, modificationDate) {
+ return Scheduler.post("setDates",
+ [Type.path.toMsg(path), accessDate, modificationDate],
+ this);
+};
+
+/**
+ * Set the file's access permissions. This does nothing on Windows.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+File.setPermissions = function setPermissions(path, options = {}) {
+ return Scheduler.post("setPermissions",
+ [Type.path.toMsg(path), options]);
+};
+
+/**
+ * Fetch the current directory
+ *
+ * @return {promise}
+ * @resolves {string} The current directory, as a path usable with OS.Path
+ * @rejects {OS.Error}
+ */
+File.getCurrentDirectory = function getCurrentDirectory() {
+ return Scheduler.post(
+ "getCurrentDirectory"
+ ).then(Type.path.fromMsg);
+};
+
+/**
+ * Change the current directory
+ *
+ * @param {string} path The OS-specific path to the current directory.
+ * You should use the methods of OS.Path and the constants of OS.Constants.Path
+ * to build OS-specific paths in a portable manner.
+ *
+ * @return {promise}
+ * @resolves {null}
+ * @rejects {OS.Error}
+ */
+File.setCurrentDirectory = function setCurrentDirectory(path) {
+ return Scheduler.post(
+ "setCurrentDirectory", [Type.path.toMsg(path)], path
+ );
+};
+
+/**
+ * Copy a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be copied.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If true, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @rejects {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be copied with the file. The
+ * behavior may not be the same across all platforms.
+*/
+File.copy = function copy(sourcePath, destPath, options) {
+ return Scheduler.post("copy", [Type.path.toMsg(sourcePath),
+ Type.path.toMsg(destPath), options], [sourcePath, destPath]);
+};
+
+/**
+ * Move a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be moved.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @returns {Promise}
+ * @rejects {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be moved with the file. The
+ * behavior may not be the same across all platforms.
+ */
+File.move = function move(sourcePath, destPath, options) {
+ return Scheduler.post("move", [Type.path.toMsg(sourcePath),
+ Type.path.toMsg(destPath), options], [sourcePath, destPath]);
+};
+
+/**
+ * Create a symbolic link to a source.
+ *
+ * @param {string} sourcePath The platform-specific path to which
+ * the symbolic link should point.
+ * @param {string} destPath The platform-specific path at which the
+ * symbolic link should be created.
+ *
+ * @returns {Promise}
+ * @rejects {OS.File.Error} In case of any error.
+ */
+if (!SharedAll.Constants.Win) {
+ File.unixSymLink = function unixSymLink(sourcePath, destPath) {
+ return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath),
+ Type.path.toMsg(destPath)], [sourcePath, destPath]);
+ };
+}
+
+/**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} Platform-specific path to a directory on the disk to
+ * query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) {
+ return Scheduler.post("getAvailableFreeSpace",
+ [Type.path.toMsg(sourcePath)], sourcePath
+ ).then(Type.uint64_t.fromMsg);
+};
+
+/**
+ * Remove an empty directory.
+ *
+ * @param {string} path The name of the directory to remove.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |true|, do not fail if the
+ * directory does not exist yet.
+ */
+File.removeEmptyDir = function removeEmptyDir(path, options) {
+ return Scheduler.post("removeEmptyDir",
+ [Type.path.toMsg(path), options], path);
+};
+
+/**
+ * Remove an existing file.
+ *
+ * @param {string} path The name of the file.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the file does
+ * not exist. |true| by default.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+File.remove = function remove(path, options) {
+ return Scheduler.post("remove",
+ [Type.path.toMsg(path), options], path);
+};
+
+
+
+/**
+ * Create a directory and, optionally, its parent directories.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ *
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |path|
+ * must be a descendant of |from|, and that |from| and its existing
+ * subdirectories present in |path| must be user-writeable.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ * - {bool} ignoreExisting If |false|, throw an error if the directory
+ * already exists. |true| by default. Ignored if |from| is specified.
+ * - {number} unixMode Under Unix, if specified, a file creation mode,
+ * as per libc function |mkdir|. If unspecified, dirs are
+ * created with a default mode of 0700 (dir is private to
+ * the user, the user can read, write and execute). Ignored under Windows
+ * or if the file system does not support file creation modes.
+ * - {C pointer} winSecurity Under Windows, if specified, security
+ * attributes as per winapi function |CreateDirectory|. If
+ * unspecified, use the default security descriptor, inherited from
+ * the parent directory. Ignored under Unix or if the file system
+ * does not support security descriptors.
+ */
+File.makeDir = function makeDir(path, options) {
+ return Scheduler.post("makeDir",
+ [Type.path.toMsg(path), options], path);
+};
+
+/**
+ * Return the contents of a file
+ *
+ * @param {string} path The path to the file.
+ * @param {number=} bytes Optionally, an upper bound to the number of bytes
+ * to read. DEPRECATED - please use options.bytes instead.
+ * @param {JSON} options Additional options.
+ * - {boolean} sequential A flag that triggers a population of the page cache
+ * with data from a file so that subsequent reads from that file would not
+ * block on disk I/O. If |true| or unspecified, inform the system that the
+ * contents of the file will be read in order. Otherwise, make no such
+ * assumption. |true| by default.
+ * - {number} bytes An upper bound to the number of bytes to read.
+ * - {string} compression If "lz4" and if the file is compressed using the lz4
+ * compression algorithm, decompress the file contents on the fly.
+ *
+ * @resolves {Uint8Array} A buffer holding the bytes
+ * read from the file.
+ */
+File.read = function read(path, bytes, options = {}) {
+ if (typeof bytes == "object") {
+ // Passing |bytes| as an argument is deprecated.
+ // We should now be passing it as a field of |options|.
+ options = bytes || {};
+ } else {
+ options = clone(options, ["outExecutionDuration"]);
+ if (typeof bytes != "undefined") {
+ options.bytes = bytes;
+ }
+ }
+
+ if (options.compression || !nativeWheneverAvailable) {
+ // We need to use the JS implementation.
+ let promise = Scheduler.post("read",
+ [Type.path.toMsg(path), bytes, options], path);
+ return promise.then(
+ function onSuccess(data) {
+ if (typeof data == "string") {
+ return data;
+ }
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ });
+ }
+
+ // Otherwise, use the native implementation.
+ return Scheduler.push(() => Native.read(path, options));
+};
+
+/**
+ * Find outs if a file exists.
+ *
+ * @param {string} path The path to the file.
+ *
+ * @return {bool} true if the file exists, false otherwise.
+ */
+File.exists = function exists(path) {
+ return Scheduler.post("exists",
+ [Type.path.toMsg(path)], path);
+};
+
+/**
+ * Write a file, atomically.
+ *
+ * By opposition to a regular |write|, this operation ensures that,
+ * until the contents are fully written, the destination file is
+ * not modified.
+ *
+ * Limitation: In a few extreme cases (hardware failure during the
+ * write, user unplugging disk during the write, etc.), data may be
+ * corrupted. If your data is user-critical (e.g. preferences,
+ * application data, etc.), you may wish to consider adding options
+ * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
+ * detailed below. Note that no combination of options can be
+ * guaranteed to totally eliminate the risk of corruption.
+ *
+ * @param {string} path The path of the file to modify.
+ * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
+ * @param {*=} options Optionally, an object determining the behavior
+ * of this function. This object may contain the following fields:
+ * - {number} bytes The number of bytes to write. If unspecified,
+ * |buffer.byteLength|. Required if |buffer| is a C pointer.
+ * - {string} tmpPath If |null| or unspecified, write all data directly
+ * to |path|. If specified, write all data to a temporary file called
+ * |tmpPath| and, once this write is complete, rename the file to
+ * replace |path|. Performing this additional operation is a little
+ * slower but also a little safer.
+ * - {bool} noOverwrite - If set, this function will fail if a file already
+ * exists at |path|.
+ * - {bool} flush - If |false| or unspecified, return immediately once the
+ * write is complete. If |true|, before writing, force the operating system
+ * to write its internal disk buffers to the disk. This is considerably slower
+ * (not just for the application but for the whole system) but also safer:
+ * if the system shuts down improperly (typically due to a kernel freeze
+ * or a power failure) or if the device is disconnected before the buffer
+ * is flushed, the file has more chances of not being corrupted.
+ * - {string} backupTo - If specified, backup the destination file as |backupTo|.
+ * Note that this function renames the destination file before overwriting it.
+ * If the process or the operating system freezes or crashes
+ * during the short window between these operations,
+ * the destination file will have been moved to its backup.
+ *
+ * @return {promise}
+ * @resolves {number} The number of bytes actually written.
+ */
+File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
+ // Copy |options| to avoid modifying the original object but preserve the
+ // reference to |outExecutionDuration| option if it is passed.
+ options = clone(options, ["outExecutionDuration"]);
+ // As options.tmpPath is a path, we need to encode it as |Type.path| message
+ if ("tmpPath" in options) {
+ options.tmpPath = Type.path.toMsg(options.tmpPath);
+ };
+ if (isTypedArray(buffer) && (!("bytes" in options))) {
+ options.bytes = buffer.byteLength;
+ };
+ let refObj = {};
+ TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj);
+ let promise = Scheduler.post("writeAtomic",
+ [Type.path.toMsg(path),
+ Type.void_t.in_ptr.toMsg(buffer),
+ options], [options, buffer, path]);
+ TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj);
+ return promise;
+};
+
+File.removeDir = function(path, options = {}) {
+ return Scheduler.post("removeDir",
+ [Type.path.toMsg(path), options], path);
+};
+
+/**
+ * Information on a file, as returned by OS.File.stat or
+ * OS.File.prototype.stat
+ *
+ * @constructor
+ */
+File.Info = function Info(value) {
+ // Note that we can't just do this[k] = value[k] because our
+ // prototype defines getters for all of these fields.
+ for (let k in value) {
+ if (k != "creationDate") {
+ Object.defineProperty(this, k, {value: value[k]});
+ }
+ }
+ Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]});
+};
+File.Info.prototype = SysAll.AbstractInfo.prototype;
+
+// Deprecated
+Object.defineProperty(File.Info.prototype, "creationDate", {
+ get: function creationDate() {
+ let {Deprecated} = Cu.import("resource://gre/modules/Deprecated.jsm", {});
+ Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes");
+ return this._deprecatedCreationDate;
+ }
+});
+
+File.Info.fromMsg = function fromMsg(value) {
+ return new File.Info(value);
+};
+
+/**
+ * Get worker's current DEBUG flag.
+ * Note: This is used for testing purposes.
+ */
+File.GET_DEBUG = function GET_DEBUG() {
+ return Scheduler.post("GET_DEBUG");
+};
+
+/**
+ * Iterate asynchronously through a directory
+ *
+ * @constructor
+ */
+var DirectoryIterator = function DirectoryIterator(path, options) {
+ /**
+ * Open the iterator on the worker thread
+ *
+ * @type {Promise}
+ * @resolves {*} A message accepted by the methods of DirectoryIterator
+ * in the worker thread
+ * @rejects {StopIteration} If all entries have already been visited
+ * or the iterator has been closed.
+ */
+ this.__itmsg = Scheduler.post(
+ "new_DirectoryIterator", [Type.path.toMsg(path), options],
+ path
+ );
+ this._isClosed = false;
+};
+DirectoryIterator.prototype = {
+ iterator: function () {
+ return this;
+ },
+ __iterator__: function () {
+ return this;
+ },
+
+ // Once close() is called, _itmsg should reject with a
+ // StopIteration. However, we don't want to create the promise until
+ // it's needed because it might never be used. In that case, we
+ // would get a warning on the console.
+ get _itmsg() {
+ if (!this.__itmsg) {
+ this.__itmsg = Promise.reject(StopIteration);
+ }
+ return this.__itmsg;
+ },
+
+ /**
+ * Determine whether the directory exists.
+ *
+ * @resolves {boolean}
+ */
+ exists: function exists() {
+ return this._itmsg.then(
+ function onSuccess(iterator) {
+ return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]);
+ }
+ );
+ },
+ /**
+ * Get the next entry in the directory.
+ *
+ * @return {Promise}
+ * @resolves {OS.File.Entry}
+ * @rejects {StopIteration} If all entries have already been visited.
+ */
+ next: function next() {
+ let self = this;
+ let promise = this._itmsg;
+
+ // Get the iterator, call _next
+ promise = promise.then(
+ function withIterator(iterator) {
+ return self._next(iterator);
+ });
+
+ return promise;
+ },
+ /**
+ * Get several entries at once.
+ *
+ * @param {number=} length If specified, the number of entries
+ * to return. If unspecified, return all remaining entries.
+ * @return {Promise}
+ * @resolves {Array} An array containing the |length| next entries.
+ */
+ nextBatch: function nextBatch(size) {
+ if (this._isClosed) {
+ return Promise.resolve([]);
+ }
+ let promise = this._itmsg;
+ promise = promise.then(
+ function withIterator(iterator) {
+ return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
+ });
+ promise = promise.then(
+ function withEntries(array) {
+ return array.map(DirectoryIterator.Entry.fromMsg);
+ });
+ return promise;
+ },
+ /**
+ * Apply a function to all elements of the directory sequentially.
+ *
+ * @param {Function} cb This function will be applied to all entries
+ * of the directory. It receives as arguments
+ * - the OS.File.Entry corresponding to the entry;
+ * - the index of the entry in the enumeration;
+ * - the iterator itself - return |iterator.close()| to stop the loop.
+ *
+ * If the callback returns a promise, iteration waits until the
+ * promise is resolved before proceeding.
+ *
+ * @return {Promise} A promise resolved once the loop has reached
+ * its end.
+ */
+ forEach: function forEach(cb, options) {
+ if (this._isClosed) {
+ return Promise.resolve();
+ }
+
+ let self = this;
+ let position = 0;
+ let iterator;
+
+ // Grab iterator
+ let promise = this._itmsg.then(
+ function(aIterator) {
+ iterator = aIterator;
+ }
+ );
+
+ // Then iterate
+ let loop = function loop() {
+ if (self._isClosed) {
+ return Promise.resolve();
+ }
+ return self._next(iterator).then(
+ function onSuccess(value) {
+ return Promise.resolve(cb(value, position++, self)).then(loop);
+ },
+ function onFailure(reason) {
+ if (reason == StopIteration) {
+ return;
+ }
+ throw reason;
+ }
+ );
+ };
+
+ return promise.then(loop);
+ },
+ /**
+ * Auxiliary method: fetch the next item
+ *
+ * @rejects {StopIteration} If all entries have already been visited
+ * or the iterator has been closed.
+ */
+ _next: function _next(iterator) {
+ if (this._isClosed) {
+ return this._itmsg;
+ }
+ let self = this;
+ let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
+ promise = promise.then(
+ DirectoryIterator.Entry.fromMsg,
+ function onReject(reason) {
+ if (reason == StopIteration) {
+ self.close();
+ throw StopIteration;
+ }
+ throw reason;
+ });
+ return promise;
+ },
+ /**
+ * Close the iterator
+ */
+ close: function close() {
+ if (this._isClosed) {
+ return Promise.resolve();
+ }
+ this._isClosed = true;
+ let self = this;
+ return this._itmsg.then(
+ function withIterator(iterator) {
+ // Set __itmsg to null so that the _itmsg getter returns a
+ // rejected StopIteration promise if it's ever used.
+ self.__itmsg = null;
+ return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
+ }
+ );
+ }
+};
+
+DirectoryIterator.Entry = function Entry(value) {
+ return value;
+};
+DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
+
+DirectoryIterator.Entry.fromMsg = function fromMsg(value) {
+ return new DirectoryIterator.Entry(value);
+};
+
+File.resetWorker = function() {
+ return Task.spawn(function*() {
+ let resources = yield Scheduler.kill({shutdown: false, reset: true});
+ if (resources && !resources.killed) {
+ throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources));
+ }
+ });
+};
+
+// Constants
+File.POS_START = SysAll.POS_START;
+File.POS_CURRENT = SysAll.POS_CURRENT;
+File.POS_END = SysAll.POS_END;
+
+// Exports
+File.Error = OSError;
+File.DirectoryIterator = DirectoryIterator;
+
+this.OS = {};
+this.OS.File = File;
+this.OS.Constants = SharedAll.Constants;
+this.OS.Shared = {
+ LOG: SharedAll.LOG,
+ Type: SysAll.Type,
+ get DEBUG() {
+ return SharedAll.Config.DEBUG;
+ },
+ set DEBUG(x) {
+ return SharedAll.Config.DEBUG = x;
+ }
+};
+Object.freeze(this.OS.Shared);
+this.OS.Path = Path;
+
+// Returns a resolved promise when all the queued operation have been completed.
+Object.defineProperty(OS.File, "queue", {
+ get: function() {
+ return Scheduler.queue;
+ }
+});
+
+// `true` if this is a content process, `false` otherwise.
+// It would be nicer to go through `Services.appInfo`, but some tests need to be
+// able to replace that field with a custom implementation before it is first
+// called.
+const isContent = Components.classes["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
+/**
+ * Shutdown barriers, to let clients register to be informed during shutdown.
+ */
+var Barriers = {
+ shutdown: new AsyncShutdown.Barrier("OS.File: Waiting for clients before full shutdown"),
+ /**
+ * Return the shutdown state of OS.File
+ */
+ getDetails: function() {
+ let result = {
+ launched: Scheduler.launched,
+ shutdown: Scheduler.shutdown,
+ worker: !!Scheduler._worker,
+ pendingReset: !!Scheduler.resetTimer,
+ latestSent: Scheduler.Debugging.latestSent,
+ latestReceived: Scheduler.Debugging.latestReceived,
+ messagesSent: Scheduler.Debugging.messagesSent,
+ messagesReceived: Scheduler.Debugging.messagesReceived,
+ messagesQueued: Scheduler.Debugging.messagesQueued,
+ DEBUG: SharedAll.Config.DEBUG,
+ };
+ // Convert dates to strings for better readability
+ for (let key of ["latestSent", "latestReceived"]) {
+ if (result[key] && typeof result[key][0] == "number") {
+ result[key][0] = Date(result[key][0]);
+ }
+ }
+ return result;
+ }
+};
+
+function setupShutdown(phaseName) {
+ Barriers[phaseName] = new AsyncShutdown.Barrier(`OS.File: Waiting for clients before ${phaseName}`),
+ File[phaseName] = Barriers[phaseName].client;
+
+ // Auto-flush OS.File during `phaseName`. This ensures that any I/O
+ // that has been queued *before* `phaseName` is properly completed.
+ // To ensure that I/O queued *during* `phaseName` change is completed,
+ // clients should register using AsyncShutdown.addBlocker.
+ AsyncShutdown[phaseName].addBlocker(
+ `OS.File: flush I/O queued before ${phaseName}`,
+ Task.async(function*() {
+ // Give clients a last chance to enqueue requests.
+ yield Barriers[phaseName].wait({crashAfterMS: null});
+
+ // Wait until all currently enqueued requests are completed.
+ yield Scheduler.queue;
+ }),
+ () => {
+ let details = Barriers.getDetails();
+ details.clients = Barriers[phaseName].state;
+ return details;
+ }
+ );
+}
+
+// profile-before-change only exists in the parent, and OS.File should
+// not be used in the child process anyways.
+if (!isContent) {
+ setupShutdown("profileBeforeChange")
+}
+File.shutdown = Barriers.shutdown.client;
diff --git a/toolkit/components/osfile/modules/osfile_async_worker.js b/toolkit/components/osfile/modules/osfile_async_worker.js
new file mode 100644
index 0000000000..84287c75e7
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_async_worker.js
@@ -0,0 +1,407 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+if (this.Components) {
+ throw new Error("This worker can only be loaded from a worker thread");
+}
+
+// Worker thread for osfile asynchronous front-end
+
+(function(exports) {
+ "use strict";
+
+ // Timestamps, for use in Telemetry.
+ // The object is set to |null| once it has been sent
+ // to the main thread.
+ let timeStamps = {
+ entered: Date.now(),
+ loaded: null
+ };
+
+ importScripts("resource://gre/modules/osfile.jsm");
+
+ let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let LOG = SharedAll.LOG.bind(SharedAll, "Agent");
+
+ let worker = new PromiseWorker.AbstractWorker();
+ worker.dispatch = function(method, args = []) {
+ return Agent[method](...args);
+ },
+ worker.log = LOG;
+ worker.postMessage = function(message, ...transfers) {
+ if (timeStamps) {
+ message.timeStamps = timeStamps;
+ timeStamps = null;
+ }
+ self.postMessage(message, ...transfers);
+ };
+ worker.close = function() {
+ self.close();
+ };
+ let Meta = PromiseWorker.Meta;
+
+ self.addEventListener("message", msg => worker.handleMessage(msg));
+
+ /**
+ * A data structure used to track opened resources
+ */
+ let ResourceTracker = function ResourceTracker() {
+ // A number used to generate ids
+ this._idgen = 0;
+ // A map from id to resource
+ this._map = new Map();
+ };
+ ResourceTracker.prototype = {
+ /**
+ * Get a resource from its unique identifier.
+ */
+ get: function(id) {
+ let result = this._map.get(id);
+ if (result == null) {
+ return result;
+ }
+ return result.resource;
+ },
+ /**
+ * Remove a resource from its unique identifier.
+ */
+ remove: function(id) {
+ if (!this._map.has(id)) {
+ throw new Error("Cannot find resource id " + id);
+ }
+ this._map.delete(id);
+ },
+ /**
+ * Add a resource, return a new unique identifier
+ *
+ * @param {*} resource A resource.
+ * @param {*=} info Optional information. For debugging purposes.
+ *
+ * @return {*} A unique identifier. For the moment, this is a number,
+ * but this might not remain the case forever.
+ */
+ add: function(resource, info) {
+ let id = this._idgen++;
+ this._map.set(id, {resource:resource, info:info});
+ return id;
+ },
+ /**
+ * Return a list of all open resources i.e. the ones still present in
+ * ResourceTracker's _map.
+ */
+ listOpenedResources: function listOpenedResources() {
+ return Array.from(this._map, ([id, resource]) => resource.info.path);
+ }
+ };
+
+ /**
+ * A map of unique identifiers to opened files.
+ */
+ let OpenedFiles = new ResourceTracker();
+
+ /**
+ * Execute a function in the context of a given file.
+ *
+ * @param {*} id A unique identifier, as used by |OpenFiles|.
+ * @param {Function} f A function to call.
+ * @param {boolean} ignoreAbsent If |true|, the error is ignored. Otherwise, the error causes an exception.
+ * @return The return value of |f()|
+ *
+ * This function attempts to get the file matching |id|. If
+ * the file exists, it executes |f| within the |this| set
+ * to the corresponding file. Otherwise, it throws an error.
+ */
+ let withFile = function withFile(id, f, ignoreAbsent) {
+ let file = OpenedFiles.get(id);
+ if (file == null) {
+ if (!ignoreAbsent) {
+ throw OS.File.Error.closed("accessing file");
+ }
+ return undefined;
+ }
+ return f.call(file);
+ };
+
+ let OpenedDirectoryIterators = new ResourceTracker();
+ let withDir = function withDir(fd, f, ignoreAbsent) {
+ let file = OpenedDirectoryIterators.get(fd);
+ if (file == null) {
+ if (!ignoreAbsent) {
+ throw OS.File.Error.closed("accessing directory");
+ }
+ return undefined;
+ }
+ if (!(file instanceof File.DirectoryIterator)) {
+ throw new Error("file is not a directory iterator " + file.__proto__.toSource());
+ }
+ return f.call(file);
+ };
+
+ let Type = exports.OS.Shared.Type;
+
+ let File = exports.OS.File;
+
+ /**
+ * The agent.
+ *
+ * It is in charge of performing method-specific deserialization
+ * of messages, calling the function/method of OS.File and serializing
+ * back the results.
+ */
+ let Agent = {
+ // Update worker's OS.Shared.DEBUG flag message from controller.
+ SET_DEBUG: function(aDEBUG) {
+ SharedAll.Config.DEBUG = aDEBUG;
+ },
+ // Return worker's current OS.Shared.DEBUG value to controller.
+ // Note: This is used for testing purposes.
+ GET_DEBUG: function() {
+ return SharedAll.Config.DEBUG;
+ },
+ /**
+ * Execute shutdown sequence, returning data on leaked file descriptors.
+ *
+ * @param {bool} If |true|, kill the worker if this would not cause
+ * leaks.
+ */
+ Meta_shutdown: function(kill) {
+ let result = {
+ openedFiles: OpenedFiles.listOpenedResources(),
+ openedDirectoryIterators: OpenedDirectoryIterators.listOpenedResources(),
+ killed: false // Placeholder
+ };
+
+ // Is it safe to kill the worker?
+ let safe = result.openedFiles.length == 0
+ && result.openedDirectoryIterators.length == 0;
+ result.killed = safe && kill;
+
+ return new Meta(result, {shutdown: result.killed});
+ },
+ // Functions of OS.File
+ stat: function stat(path, options) {
+ return exports.OS.File.Info.toMsg(
+ exports.OS.File.stat(Type.path.fromMsg(path), options));
+ },
+ setPermissions: function setPermissions(path, options = {}) {
+ return exports.OS.File.setPermissions(Type.path.fromMsg(path), options);
+ },
+ setDates: function setDates(path, accessDate, modificationDate) {
+ return exports.OS.File.setDates(Type.path.fromMsg(path), accessDate,
+ modificationDate);
+ },
+ getCurrentDirectory: function getCurrentDirectory() {
+ return exports.OS.Shared.Type.path.toMsg(File.getCurrentDirectory());
+ },
+ setCurrentDirectory: function setCurrentDirectory(path) {
+ File.setCurrentDirectory(exports.OS.Shared.Type.path.fromMsg(path));
+ },
+ copy: function copy(sourcePath, destPath, options) {
+ return File.copy(Type.path.fromMsg(sourcePath),
+ Type.path.fromMsg(destPath), options);
+ },
+ move: function move(sourcePath, destPath, options) {
+ return File.move(Type.path.fromMsg(sourcePath),
+ Type.path.fromMsg(destPath), options);
+ },
+ getAvailableFreeSpace: function getAvailableFreeSpace(sourcePath) {
+ return Type.uint64_t.toMsg(
+ File.getAvailableFreeSpace(Type.path.fromMsg(sourcePath)));
+ },
+ makeDir: function makeDir(path, options) {
+ return File.makeDir(Type.path.fromMsg(path), options);
+ },
+ removeEmptyDir: function removeEmptyDir(path, options) {
+ return File.removeEmptyDir(Type.path.fromMsg(path), options);
+ },
+ remove: function remove(path, options) {
+ return File.remove(Type.path.fromMsg(path), options);
+ },
+ open: function open(path, mode, options) {
+ let filePath = Type.path.fromMsg(path);
+ let file = File.open(filePath, mode, options);
+ return OpenedFiles.add(file, {
+ // Adding path information to keep track of opened files
+ // to report leaks when debugging.
+ path: filePath
+ });
+ },
+ openUnique: function openUnique(path, options) {
+ let filePath = Type.path.fromMsg(path);
+ let openedFile = OS.Shared.AbstractFile.openUnique(filePath, options);
+ let resourceId = OpenedFiles.add(openedFile.file, {
+ // Adding path information to keep track of opened files
+ // to report leaks when debugging.
+ path: openedFile.path
+ });
+
+ return {
+ path: openedFile.path,
+ file: resourceId
+ };
+ },
+ read: function read(path, bytes, options) {
+ let data = File.read(Type.path.fromMsg(path), bytes, options);
+ if (typeof data == "string") {
+ return data;
+ }
+ return new Meta({
+ buffer: data.buffer,
+ byteOffset: data.byteOffset,
+ byteLength: data.byteLength
+ }, {
+ transfers: [data.buffer]
+ });
+ },
+ exists: function exists(path) {
+ return File.exists(Type.path.fromMsg(path));
+ },
+ writeAtomic: function writeAtomic(path, buffer, options) {
+ if (options.tmpPath) {
+ options.tmpPath = Type.path.fromMsg(options.tmpPath);
+ }
+ return File.writeAtomic(Type.path.fromMsg(path),
+ Type.voidptr_t.fromMsg(buffer),
+ options
+ );
+ },
+ removeDir: function(path, options) {
+ return File.removeDir(Type.path.fromMsg(path), options);
+ },
+ new_DirectoryIterator: function new_DirectoryIterator(path, options) {
+ let directoryPath = Type.path.fromMsg(path);
+ let iterator = new File.DirectoryIterator(directoryPath, options);
+ return OpenedDirectoryIterators.add(iterator, {
+ // Adding path information to keep track of opened directory
+ // iterators to report leaks when debugging.
+ path: directoryPath
+ });
+ },
+ // Methods of OS.File
+ File_prototype_close: function close(fd) {
+ return withFile(fd,
+ function do_close() {
+ try {
+ return this.close();
+ } finally {
+ OpenedFiles.remove(fd);
+ }
+ });
+ },
+ File_prototype_stat: function stat(fd) {
+ return withFile(fd,
+ function do_stat() {
+ return exports.OS.File.Info.toMsg(this.stat());
+ });
+ },
+ File_prototype_setPermissions: function setPermissions(fd, options = {}) {
+ return withFile(fd,
+ function do_setPermissions() {
+ return this.setPermissions(options);
+ });
+ },
+ File_prototype_setDates: function setDates(fd, accessTime, modificationTime) {
+ return withFile(fd,
+ function do_setDates() {
+ return this.setDates(accessTime, modificationTime);
+ });
+ },
+ File_prototype_read: function read(fd, nbytes, options) {
+ return withFile(fd,
+ function do_read() {
+ let data = this.read(nbytes, options);
+ return new Meta({
+ buffer: data.buffer,
+ byteOffset: data.byteOffset,
+ byteLength: data.byteLength
+ }, {
+ transfers: [data.buffer]
+ });
+ }
+ );
+ },
+ File_prototype_readTo: function readTo(fd, buffer, options) {
+ return withFile(fd,
+ function do_readTo() {
+ return this.readTo(exports.OS.Shared.Type.voidptr_t.fromMsg(buffer),
+ options);
+ });
+ },
+ File_prototype_write: function write(fd, buffer, options) {
+ return withFile(fd,
+ function do_write() {
+ return this.write(exports.OS.Shared.Type.voidptr_t.fromMsg(buffer),
+ options);
+ });
+ },
+ File_prototype_setPosition: function setPosition(fd, pos, whence) {
+ return withFile(fd,
+ function do_setPosition() {
+ return this.setPosition(pos, whence);
+ });
+ },
+ File_prototype_getPosition: function getPosition(fd) {
+ return withFile(fd,
+ function do_getPosition() {
+ return this.getPosition();
+ });
+ },
+ File_prototype_flush: function flush(fd) {
+ return withFile(fd,
+ function do_flush() {
+ return this.flush();
+ });
+ },
+ // Methods of OS.File.DirectoryIterator
+ DirectoryIterator_prototype_next: function next(dir) {
+ return withDir(dir,
+ function do_next() {
+ try {
+ return File.DirectoryIterator.Entry.toMsg(this.next());
+ } catch (x) {
+ if (x == StopIteration) {
+ OpenedDirectoryIterators.remove(dir);
+ }
+ throw x;
+ }
+ }, false);
+ },
+ DirectoryIterator_prototype_nextBatch: function nextBatch(dir, size) {
+ return withDir(dir,
+ function do_nextBatch() {
+ let result;
+ try {
+ result = this.nextBatch(size);
+ } catch (x) {
+ OpenedDirectoryIterators.remove(dir);
+ throw x;
+ }
+ return result.map(File.DirectoryIterator.Entry.toMsg);
+ }, false);
+ },
+ DirectoryIterator_prototype_close: function close(dir) {
+ return withDir(dir,
+ function do_close() {
+ this.close();
+ OpenedDirectoryIterators.remove(dir);
+ }, true);// ignore error to support double-closing |DirectoryIterator|
+ },
+ DirectoryIterator_prototype_exists: function exists(dir) {
+ return withDir(dir,
+ function do_exists() {
+ return this.exists();
+ });
+ }
+ };
+ if (!SharedAll.Constants.Win) {
+ Agent.unixSymLink = function unixSymLink(sourcePath, destPath) {
+ return File.unixSymLink(Type.path.fromMsg(sourcePath),
+ Type.path.fromMsg(destPath));
+ };
+ }
+
+ timeStamps.loaded = Date.now();
+})(this);
diff --git a/toolkit/components/osfile/modules/osfile_native.jsm b/toolkit/components/osfile/modules/osfile_native.jsm
new file mode 100644
index 0000000000..16cd3c92a1
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_native.jsm
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Native (xpcom) implementation of key OS.File functions
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["read"];
+
+var {results: Cr, utils: Cu, interfaces: Ci} = Components;
+
+var SharedAll = Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", {});
+
+var SysAll = {};
+if (SharedAll.Constants.Win) {
+ Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll);
+} else if (SharedAll.Constants.libc) {
+ Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll);
+} else {
+ throw new Error("I am neither under Windows nor under a Posix system");
+}
+var {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+/**
+ * The native service holding the implementation of the functions.
+ */
+XPCOMUtils.defineLazyServiceGetter(this,
+ "Internals",
+ "@mozilla.org/toolkit/osfile/native-internals;1",
+ "nsINativeOSFileInternalsService");
+
+/**
+ * Native implementation of OS.File.read
+ *
+ * This implementation does not handle option |compression|.
+ */
+this.read = function(path, options = {}) {
+ // Sanity check on types of options
+ if ("encoding" in options && typeof options.encoding != "string") {
+ return Promise.reject(new TypeError("Invalid type for option encoding"));
+ }
+ if ("compression" in options && typeof options.compression != "string") {
+ return Promise.reject(new TypeError("Invalid type for option compression"));
+ }
+ if ("bytes" in options && typeof options.bytes != "number") {
+ return Promise.reject(new TypeError("Invalid type for option bytes"));
+ }
+
+ let deferred = Promise.defer();
+ Internals.read(path,
+ options,
+ function onSuccess(success) {
+ success.QueryInterface(Ci.nsINativeOSFileResult);
+ if ("outExecutionDuration" in options) {
+ options.outExecutionDuration =
+ success.executionDurationMS +
+ (options.outExecutionDuration || 0);
+ }
+ deferred.resolve(success.result);
+ },
+ function onError(operation, oserror) {
+ deferred.reject(new SysAll.Error(operation, oserror, path));
+ }
+ );
+ return deferred.promise;
+};
diff --git a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm
new file mode 100644
index 0000000000..c5c5051026
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm
@@ -0,0 +1,1315 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * OS.File utilities used by all threads.
+ *
+ * This module defines:
+ * - logging;
+ * - the base constants;
+ * - base types and primitives for declaring new types;
+ * - primitives for importing C functions;
+ * - primitives for dealing with integers, pointers, typed arrays;
+ * - the base class OSError;
+ * - a few additional utilities.
+ */
+
+// Boilerplate used to be able to import this module both from the main
+// thread and from worker threads.
+
+// Since const is lexically scoped, hoist the
+// conditionally-useful definition ourselves.
+const Cu = typeof Components != "undefined" ? Components.utils : undefined;
+const Ci = typeof Components != "undefined" ? Components.interfaces : undefined;
+const Cc = typeof Components != "undefined" ? Components.classes : undefined;
+
+/**
+ * A constructor for messages that require transfers instead of copies.
+ *
+ * See BasePromiseWorker.Meta.
+ *
+ * @constructor
+ */
+var Meta;
+if (typeof Components != "undefined") {
+ // Global definition of |exports|, to keep everybody happy.
+ // In non-main thread, |exports| is provided by the module
+ // loader.
+ this.exports = {};
+
+ Cu.import("resource://gre/modules/Services.jsm", this);
+ Meta = Cu.import("resource://gre/modules/PromiseWorker.jsm", {}).BasePromiseWorker.Meta;
+} else {
+ importScripts("resource://gre/modules/workers/require.js");
+ Meta = require("resource://gre/modules/workers/PromiseWorker.js").Meta;
+}
+
+var EXPORTED_SYMBOLS = [
+ "LOG",
+ "clone",
+ "Config",
+ "Constants",
+ "Type",
+ "HollowStructure",
+ "OSError",
+ "Library",
+ "declareFFI",
+ "declareLazy",
+ "declareLazyFFI",
+ "normalizeBufferArgs",
+ "projectValue",
+ "isArrayBuffer",
+ "isTypedArray",
+ "defineLazyGetter",
+ "OS" // Warning: this exported symbol will disappear
+];
+
+////////////////////// Configuration of OS.File
+
+var Config = {
+ /**
+ * If |true|, calls to |LOG| are shown. Otherwise, they are hidden.
+ *
+ * This configuration option is controlled by preference "toolkit.osfile.log".
+ */
+ DEBUG: false,
+
+ /**
+ * TEST
+ */
+ TEST: false
+};
+exports.Config = Config;
+
+////////////////////// OS Constants
+
+if (typeof Components != "undefined") {
+ // On the main thread, OS.Constants is defined by a xpcom
+ // component. On other threads, it is available automatically
+ Cu.import("resource://gre/modules/ctypes.jsm");
+ Cc["@mozilla.org/net/osfileconstantsservice;1"].
+ getService(Ci.nsIOSFileConstantsService).init();
+}
+
+exports.Constants = OS.Constants;
+
+///////////////////// Utilities
+
+// Define a lazy getter for a property
+var defineLazyGetter = function defineLazyGetter(object, name, getter) {
+ Object.defineProperty(object, name, {
+ configurable: true,
+ get: function lazy() {
+ delete this[name];
+ let value = getter.call(this);
+ Object.defineProperty(object, name, {
+ value: value
+ });
+ return value;
+ }
+ });
+};
+exports.defineLazyGetter = defineLazyGetter;
+
+
+///////////////////// Logging
+
+/**
+ * The default implementation of the logger.
+ *
+ * The choice of logger can be overridden with Config.TEST.
+ */
+var gLogger;
+if (typeof window != "undefined" && window.console && console.log) {
+ gLogger = console.log.bind(console, "OS");
+} else {
+ gLogger = function(...args) {
+ dump("OS " + args.join(" ") + "\n");
+ };
+}
+
+/**
+ * Attempt to stringify an argument into something useful for
+ * debugging purposes, by using |.toString()| or |JSON.stringify|
+ * if available.
+ *
+ * @param {*} arg An argument to be stringified if possible.
+ * @return {string} A stringified version of |arg|.
+ */
+var stringifyArg = function stringifyArg(arg) {
+ if (typeof arg === "string") {
+ return arg;
+ }
+ if (arg && typeof arg === "object") {
+ let argToString = "" + arg;
+
+ /**
+ * The only way to detect whether this object has a non-default
+ * implementation of |toString| is to check whether it returns
+ * '[object Object]'. Unfortunately, we cannot simply compare |arg.toString|
+ * and |Object.prototype.toString| as |arg| typically comes from another
+ * compartment.
+ */
+ if (argToString === "[object Object]") {
+ return JSON.stringify(arg, function(key, value) {
+ if (isTypedArray(value)) {
+ return "["+ value.constructor.name + " " + value.byteOffset + " " + value.byteLength + "]";
+ }
+ if (isArrayBuffer(arg)) {
+ return "[" + value.constructor.name + " " + value.byteLength + "]";
+ }
+ return value;
+ });
+ } else {
+ return argToString;
+ }
+ }
+ return arg;
+};
+
+var LOG = function (...args) {
+ if (!Config.DEBUG) {
+ // If logging is deactivated, don't log
+ return;
+ }
+
+ let logFunc = gLogger;
+ if (Config.TEST && typeof Components != "undefined") {
+ // If _TESTING_LOGGING is set, and if we are on the main thread,
+ // redirect logs to Services.console, for testing purposes
+ logFunc = function logFunc(...args) {
+ let message = ["TEST", "OS"].concat(args).join(" ");
+ Services.console.logStringMessage(message + "\n");
+ };
+ }
+ logFunc.apply(null, args.map(stringifyArg));
+};
+
+exports.LOG = LOG;
+
+/**
+ * Return a shallow clone of the enumerable properties of an object.
+ *
+ * Utility used whenever normalizing options requires making (shallow)
+ * changes to an option object. The copy ensures that we do not modify
+ * a client-provided object by accident.
+ *
+ * Note: to reference and not copy specific fields, provide an optional
+ * |refs| argument containing their names.
+ *
+ * @param {JSON} object Options to be cloned.
+ * @param {Array} refs An optional array of field names to be passed by
+ * reference instead of copying.
+ */
+var clone = function (object, refs = []) {
+ let result = {};
+ // Make a reference between result[key] and object[key].
+ let refer = function refer(result, key, object) {
+ Object.defineProperty(result, key, {
+ enumerable: true,
+ get: function() {
+ return object[key];
+ },
+ set: function(value) {
+ object[key] = value;
+ }
+ });
+ };
+ for (let k in object) {
+ if (refs.indexOf(k) < 0) {
+ result[k] = object[k];
+ } else {
+ refer(result, k, object);
+ }
+ }
+ return result;
+};
+
+exports.clone = clone;
+
+///////////////////// Abstractions above js-ctypes
+
+/**
+ * Abstraction above js-ctypes types.
+ *
+ * Use values of this type to register FFI functions. In addition to the
+ * usual features of js-ctypes, values of this type perform the necessary
+ * transformations to ensure that C errors are handled nicely, to connect
+ * resources with their finalizer, etc.
+ *
+ * @param {string} name The name of the type. Must be unique.
+ * @param {CType} implementation The js-ctypes implementation of the type.
+ *
+ * @constructor
+ */
+function Type(name, implementation) {
+ if (!(typeof name == "string")) {
+ throw new TypeError("Type expects as first argument a name, got: "
+ + name);
+ }
+ if (!(implementation instanceof ctypes.CType)) {
+ throw new TypeError("Type expects as second argument a ctypes.CType"+
+ ", got: " + implementation);
+ }
+ Object.defineProperty(this, "name", { value: name });
+ Object.defineProperty(this, "implementation", { value: implementation });
+}
+Type.prototype = {
+ /**
+ * Serialize a value of |this| |Type| into a format that can
+ * be transmitted as a message (not necessarily a string).
+ *
+ * In the default implementation, the method returns the
+ * value unchanged.
+ */
+ toMsg: function default_toMsg(value) {
+ return value;
+ },
+ /**
+ * Deserialize a message to a value of |this| |Type|.
+ *
+ * In the default implementation, the method returns the
+ * message unchanged.
+ */
+ fromMsg: function default_fromMsg(msg) {
+ return msg;
+ },
+ /**
+ * Import a value from C.
+ *
+ * In this default implementation, return the value
+ * unchanged.
+ */
+ importFromC: function default_importFromC(value) {
+ return value;
+ },
+
+ /**
+ * A pointer/array used to pass data to the foreign function.
+ */
+ get in_ptr() {
+ delete this.in_ptr;
+ let ptr_t = new PtrType(
+ "[in] " + this.name + "*",
+ this.implementation.ptr,
+ this);
+ Object.defineProperty(this, "in_ptr",
+ {
+ get: function() {
+ return ptr_t;
+ }
+ });
+ return ptr_t;
+ },
+
+ /**
+ * A pointer/array used to receive data from the foreign function.
+ */
+ get out_ptr() {
+ delete this.out_ptr;
+ let ptr_t = new PtrType(
+ "[out] " + this.name + "*",
+ this.implementation.ptr,
+ this);
+ Object.defineProperty(this, "out_ptr",
+ {
+ get: function() {
+ return ptr_t;
+ }
+ });
+ return ptr_t;
+ },
+
+ /**
+ * A pointer/array used to both pass data to the foreign function
+ * and receive data from the foreign function.
+ *
+ * Whenever possible, prefer using |in_ptr| or |out_ptr|, which
+ * are generally faster.
+ */
+ get inout_ptr() {
+ delete this.inout_ptr;
+ let ptr_t = new PtrType(
+ "[inout] " + this.name + "*",
+ this.implementation.ptr,
+ this);
+ Object.defineProperty(this, "inout_ptr",
+ {
+ get: function() {
+ return ptr_t;
+ }
+ });
+ return ptr_t;
+ },
+
+ /**
+ * Attach a finalizer to a type.
+ */
+ releaseWith: function releaseWith(finalizer) {
+ let parent = this;
+ let type = this.withName("[auto " + this.name + ", " + finalizer + "] ");
+ type.importFromC = function importFromC(value, operation) {
+ return ctypes.CDataFinalizer(
+ parent.importFromC(value, operation),
+ finalizer);
+ };
+ return type;
+ },
+
+ /**
+ * Lazy variant of releaseWith.
+ * Attach a finalizer lazily to a type.
+ *
+ * @param {function} getFinalizer The function that
+ * returns finalizer lazily.
+ */
+ releaseWithLazy: function releaseWithLazy(getFinalizer) {
+ let parent = this;
+ let type = this.withName("[auto " + this.name + ", (lazy)] ");
+ type.importFromC = function importFromC(value, operation) {
+ return ctypes.CDataFinalizer(
+ parent.importFromC(value, operation),
+ getFinalizer());
+ };
+ return type;
+ },
+
+ /**
+ * Return an alias to a type with a different name.
+ */
+ withName: function withName(name) {
+ return Object.create(this, {name: {value: name}});
+ },
+
+ /**
+ * Cast a C value to |this| type.
+ *
+ * Throw an error if the value cannot be casted.
+ */
+ cast: function cast(value) {
+ return ctypes.cast(value, this.implementation);
+ },
+
+ /**
+ * Return the number of bytes in a value of |this| type.
+ *
+ * This may not be defined, e.g. for |void_t|, array types
+ * without length, etc.
+ */
+ get size() {
+ return this.implementation.size;
+ }
+};
+
+/**
+ * Utility function used to determine whether an object is a typed array
+ */
+var isTypedArray = function isTypedArray(obj) {
+ return obj != null && typeof obj == "object"
+ && "byteOffset" in obj;
+};
+exports.isTypedArray = isTypedArray;
+
+/**
+ * Utility function used to determine whether an object is an ArrayBuffer.
+ */
+var isArrayBuffer = function(obj) {
+ return obj != null && typeof obj == "object" &&
+ obj.constructor.name == "ArrayBuffer";
+};
+exports.isArrayBuffer = isArrayBuffer;
+
+/**
+ * A |Type| of pointers.
+ *
+ * @param {string} name The name of this type.
+ * @param {CType} implementation The type of this pointer.
+ * @param {Type} targetType The target type.
+ */
+function PtrType(name, implementation, targetType) {
+ Type.call(this, name, implementation);
+ if (targetType == null || !targetType instanceof Type) {
+ throw new TypeError("targetType must be an instance of Type");
+ }
+ /**
+ * The type of values targeted by this pointer type.
+ */
+ Object.defineProperty(this, "targetType", {
+ value: targetType
+ });
+}
+PtrType.prototype = Object.create(Type.prototype);
+
+/**
+ * Convert a value to a pointer.
+ *
+ * Protocol:
+ * - |null| returns |null|
+ * - a string returns |{string: value}|
+ * - a typed array returns |{ptr: address_of_buffer}|
+ * - a C array returns |{ptr: address_of_buffer}|
+ * everything else raises an error
+ */
+PtrType.prototype.toMsg = function ptr_toMsg(value) {
+ if (value == null) {
+ return null;
+ }
+ if (typeof value == "string") {
+ return { string: value };
+ }
+ if (isTypedArray(value)) {
+ // Automatically transfer typed arrays
+ return new Meta({data: value}, {transfers: [value.buffer]});
+ }
+ if (isArrayBuffer(value)) {
+ // Automatically transfer array buffers
+ return new Meta({data: value}, {transfers: [value]});
+ }
+ let normalized;
+ if ("addressOfElement" in value) { // C array
+ normalized = value.addressOfElement(0);
+ } else if ("isNull" in value) { // C pointer
+ normalized = value;
+ } else {
+ throw new TypeError("Value " + value +
+ " cannot be converted to a pointer");
+ }
+ let cast = Type.uintptr_t.cast(normalized);
+ return {ptr: cast.value.toString()};
+};
+
+/**
+ * Convert a message back to a pointer.
+ */
+PtrType.prototype.fromMsg = function ptr_fromMsg(msg) {
+ if (msg == null) {
+ return null;
+ }
+ if ("string" in msg) {
+ return msg.string;
+ }
+ if ("data" in msg) {
+ return msg.data;
+ }
+ if ("ptr" in msg) {
+ let address = ctypes.uintptr_t(msg.ptr);
+ return this.cast(address);
+ }
+ throw new TypeError("Message " + msg.toSource() +
+ " does not represent a pointer");
+};
+
+exports.Type = Type;
+
+
+/*
+ * Some values are large integers on 64 bit platforms. Unfortunately,
+ * in practice, 64 bit integers cannot be manipulated in JS. We
+ * therefore project them to regular numbers whenever possible.
+ */
+
+var projectLargeInt = function projectLargeInt(x) {
+ let str = x.toString();
+ let rv = parseInt(str, 10);
+ if (rv.toString() !== str) {
+ throw new TypeError("Number " + str + " cannot be projected to a double");
+ }
+ return rv;
+};
+var projectLargeUInt = function projectLargeUInt(x) {
+ return projectLargeInt(x);
+};
+var projectValue = function projectValue(x) {
+ if (!(x instanceof ctypes.CData)) {
+ return x;
+ }
+ if (!("value" in x)) { // Sanity check
+ throw new TypeError("Number " + x.toSource() + " has no field |value|");
+ }
+ return x.value;
+};
+
+function projector(type, signed) {
+ LOG("Determining best projection for", type,
+ "(size: ", type.size, ")", signed?"signed":"unsigned");
+ if (type instanceof Type) {
+ type = type.implementation;
+ }
+ if (!type.size) {
+ throw new TypeError("Argument is not a proper C type");
+ }
+ // Determine if type is projected to Int64/Uint64
+ if (type.size == 8 // Usual case
+ // The following cases have special treatment in js-ctypes
+ // Regardless of their size, the value getter returns
+ // a Int64/Uint64
+ || type == ctypes.size_t // Special cases
+ || type == ctypes.ssize_t
+ || type == ctypes.intptr_t
+ || type == ctypes.uintptr_t
+ || type == ctypes.off_t) {
+ if (signed) {
+ LOG("Projected as a large signed integer");
+ return projectLargeInt;
+ } else {
+ LOG("Projected as a large unsigned integer");
+ return projectLargeUInt;
+ }
+ }
+ LOG("Projected as a regular number");
+ return projectValue;
+};
+exports.projectValue = projectValue;
+
+/**
+ * Get the appropriate type for an unsigned int of the given size.
+ *
+ * This function is useful to define types such as |mode_t| whose
+ * actual width depends on the OS/platform.
+ *
+ * @param {number} size The number of bytes requested.
+ */
+Type.uintn_t = function uintn_t(size) {
+ switch (size) {
+ case 1: return Type.uint8_t;
+ case 2: return Type.uint16_t;
+ case 4: return Type.uint32_t;
+ case 8: return Type.uint64_t;
+ default:
+ throw new Error("Cannot represent unsigned integers of " + size + " bytes");
+ }
+};
+
+/**
+ * Get the appropriate type for an signed int of the given size.
+ *
+ * This function is useful to define types such as |mode_t| whose
+ * actual width depends on the OS/platform.
+ *
+ * @param {number} size The number of bytes requested.
+ */
+Type.intn_t = function intn_t(size) {
+ switch (size) {
+ case 1: return Type.int8_t;
+ case 2: return Type.int16_t;
+ case 4: return Type.int32_t;
+ case 8: return Type.int64_t;
+ default:
+ throw new Error("Cannot represent integers of " + size + " bytes");
+ }
+};
+
+/**
+ * Actual implementation of common C types.
+ */
+
+/**
+ * The void value.
+ */
+Type.void_t =
+ new Type("void",
+ ctypes.void_t);
+
+/**
+ * Shortcut for |void*|.
+ */
+Type.voidptr_t =
+ new PtrType("void*",
+ ctypes.voidptr_t,
+ Type.void_t);
+
+// void* is a special case as we can cast any pointer to/from it
+// so we have to shortcut |in_ptr|/|out_ptr|/|inout_ptr| and
+// ensure that js-ctypes' casting mechanism is invoked directly
+["in_ptr", "out_ptr", "inout_ptr"].forEach(function(key) {
+ Object.defineProperty(Type.void_t, key,
+ {
+ value: Type.voidptr_t
+ });
+});
+
+/**
+ * A Type of integers.
+ *
+ * @param {string} name The name of this type.
+ * @param {CType} implementation The underlying js-ctypes implementation.
+ * @param {bool} signed |true| if this is a type of signed integers,
+ * |false| otherwise.
+ *
+ * @constructor
+ */
+function IntType(name, implementation, signed) {
+ Type.call(this, name, implementation);
+ this.importFromC = projector(implementation, signed);
+ this.project = this.importFromC;
+};
+IntType.prototype = Object.create(Type.prototype);
+IntType.prototype.toMsg = function toMsg(value) {
+ if (typeof value == "number") {
+ return value;
+ }
+ return this.project(value);
+};
+
+/**
+ * A C char (one byte)
+ */
+Type.char =
+ new Type("char",
+ ctypes.char);
+
+/**
+ * A C wide char (two bytes)
+ */
+Type.char16_t =
+ new Type("char16_t",
+ ctypes.char16_t);
+
+ /**
+ * Base string types.
+ */
+Type.cstring = Type.char.in_ptr.withName("[in] C string");
+Type.wstring = Type.char16_t.in_ptr.withName("[in] wide string");
+Type.out_cstring = Type.char.out_ptr.withName("[out] C string");
+Type.out_wstring = Type.char16_t.out_ptr.withName("[out] wide string");
+
+/**
+ * A C integer (8-bits).
+ */
+Type.int8_t =
+ new IntType("int8_t", ctypes.int8_t, true);
+
+Type.uint8_t =
+ new IntType("uint8_t", ctypes.uint8_t, false);
+
+/**
+ * A C integer (16-bits).
+ *
+ * Also known as WORD under Windows.
+ */
+Type.int16_t =
+ new IntType("int16_t", ctypes.int16_t, true);
+
+Type.uint16_t =
+ new IntType("uint16_t", ctypes.uint16_t, false);
+
+/**
+ * A C integer (32-bits).
+ *
+ * Also known as DWORD under Windows.
+ */
+Type.int32_t =
+ new IntType("int32_t", ctypes.int32_t, true);
+
+Type.uint32_t =
+ new IntType("uint32_t", ctypes.uint32_t, false);
+
+/**
+ * A C integer (64-bits).
+ */
+Type.int64_t =
+ new IntType("int64_t", ctypes.int64_t, true);
+
+Type.uint64_t =
+ new IntType("uint64_t", ctypes.uint64_t, false);
+
+ /**
+ * A C integer
+ *
+ * Size depends on the platform.
+ */
+Type.int = Type.intn_t(ctypes.int.size).
+ withName("int");
+
+Type.unsigned_int = Type.intn_t(ctypes.unsigned_int.size).
+ withName("unsigned int");
+
+/**
+ * A C long integer.
+ *
+ * Size depends on the platform.
+ */
+Type.long =
+ Type.intn_t(ctypes.long.size).withName("long");
+
+Type.unsigned_long =
+ Type.intn_t(ctypes.unsigned_long.size).withName("unsigned long");
+
+/**
+ * An unsigned integer with the same size as a pointer.
+ *
+ * Used to cast a pointer to an integer, whenever necessary.
+ */
+Type.uintptr_t =
+ Type.uintn_t(ctypes.uintptr_t.size).withName("uintptr_t");
+
+/**
+ * A boolean.
+ * Implemented as a C integer.
+ */
+Type.bool = Type.int.withName("bool");
+Type.bool.importFromC = function projectBool(x) {
+ return !!(x.value);
+};
+
+/**
+ * A user identifier.
+ *
+ * Implemented as a C integer.
+ */
+Type.uid_t =
+ Type.int.withName("uid_t");
+
+/**
+ * A group identifier.
+ *
+ * Implemented as a C integer.
+ */
+Type.gid_t =
+ Type.int.withName("gid_t");
+
+/**
+ * An offset (positive or negative).
+ *
+ * Implemented as a C integer.
+ */
+Type.off_t =
+ new IntType("off_t", ctypes.off_t, true);
+
+/**
+ * A size (positive).
+ *
+ * Implemented as a C size_t.
+ */
+Type.size_t =
+ new IntType("size_t", ctypes.size_t, false);
+
+/**
+ * An offset (positive or negative).
+ * Implemented as a C integer.
+ */
+Type.ssize_t =
+ new IntType("ssize_t", ctypes.ssize_t, true);
+
+/**
+ * Encoding/decoding strings
+ */
+Type.uencoder =
+ new Type("uencoder", ctypes.StructType("uencoder"));
+Type.udecoder =
+ new Type("udecoder", ctypes.StructType("udecoder"));
+
+/**
+ * Utility class, used to build a |struct| type
+ * from a set of field names, types and offsets.
+ *
+ * @param {string} name The name of the |struct| type.
+ * @param {number} size The total size of the |struct| type in bytes.
+ */
+function HollowStructure(name, size) {
+ if (!name) {
+ throw new TypeError("HollowStructure expects a name");
+ }
+ if (!size || size < 0) {
+ throw new TypeError("HollowStructure expects a (positive) size");
+ }
+
+ // A mapping from offsets in the struct to name/type pairs
+ // (or nothing if no field starts at that offset).
+ this.offset_to_field_info = [];
+
+ // The name of the struct
+ this.name = name;
+
+ // The size of the struct, in bytes
+ this.size = size;
+
+ // The number of paddings inserted so far.
+ // Used to give distinct names to padding fields.
+ this._paddings = 0;
+}
+HollowStructure.prototype = {
+ /**
+ * Add a field at a given offset.
+ *
+ * @param {number} offset The offset at which to insert the field.
+ * @param {string} name The name of the field.
+ * @param {CType|Type} type The type of the field.
+ */
+ add_field_at: function add_field_at(offset, name, type) {
+ if (offset == null) {
+ throw new TypeError("add_field_at requires a non-null offset");
+ }
+ if (!name) {
+ throw new TypeError("add_field_at requires a non-null name");
+ }
+ if (!type) {
+ throw new TypeError("add_field_at requires a non-null type");
+ }
+ if (type instanceof Type) {
+ type = type.implementation;
+ }
+ if (this.offset_to_field_info[offset]) {
+ throw new Error("HollowStructure " + this.name +
+ " already has a field at offset " + offset);
+ }
+ if (offset + type.size > this.size) {
+ throw new Error("HollowStructure " + this.name +
+ " cannot place a value of type " + type +
+ " at offset " + offset +
+ " without exceeding its size of " + this.size);
+ }
+ let field = {name: name, type:type};
+ this.offset_to_field_info[offset] = field;
+ },
+
+ /**
+ * Create a pseudo-field that will only serve as padding.
+ *
+ * @param {number} size The number of bytes in the field.
+ * @return {Object} An association field-name => field-type,
+ * as expected by |ctypes.StructType|.
+ */
+ _makePaddingField: function makePaddingField(size) {
+ let field = ({});
+ field["padding_" + this._paddings] =
+ ctypes.ArrayType(ctypes.uint8_t, size);
+ this._paddings++;
+ return field;
+ },
+
+ /**
+ * Convert this |HollowStructure| into a |Type|.
+ */
+ getType: function getType() {
+ // Contents of the structure, in the format expected
+ // by ctypes.StructType.
+ let struct = [];
+
+ let i = 0;
+ while (i < this.size) {
+ let currentField = this.offset_to_field_info[i];
+ if (!currentField) {
+ // No field was specified at this offset, we need to
+ // introduce some padding.
+
+ // Firstly, determine how many bytes of padding
+ let padding_length = 1;
+ while (i + padding_length < this.size
+ && !this.offset_to_field_info[i + padding_length]) {
+ ++padding_length;
+ }
+
+ // Then add the padding
+ struct.push(this._makePaddingField(padding_length));
+
+ // And proceed
+ i += padding_length;
+ } else {
+ // We have a field at this offset.
+
+ // Firstly, ensure that we do not have two overlapping fields
+ for (let j = 1; j < currentField.type.size; ++j) {
+ let candidateField = this.offset_to_field_info[i + j];
+ if (candidateField) {
+ throw new Error("Fields " + currentField.name +
+ " and " + candidateField.name +
+ " overlap at position " + (i + j));
+ }
+ }
+
+ // Then add the field
+ let field = ({});
+ field[currentField.name] = currentField.type;
+ struct.push(field);
+
+ // And proceed
+ i += currentField.type.size;
+ }
+ }
+ let result = new Type(this.name, ctypes.StructType(this.name, struct));
+ if (result.implementation.size != this.size) {
+ throw new Error("Wrong size for type " + this.name +
+ ": expected " + this.size +
+ ", found " + result.implementation.size +
+ " (" + result.implementation.toSource() + ")");
+ }
+ return result;
+ }
+};
+exports.HollowStructure = HollowStructure;
+
+/**
+ * Representation of a native library.
+ *
+ * The native library is opened lazily, during the first call to its
+ * field |library| or whenever accessing one of the methods imported
+ * with declareLazyFFI.
+ *
+ * @param {string} name A human-readable name for the library. Used
+ * for debugging and error reporting.
+ * @param {string...} candidates A list of system libraries that may
+ * represent this library. Used e.g. to try different library names
+ * on distinct operating systems ("libxul", "XUL", etc.).
+ *
+ * @constructor
+ */
+function Library(name, ...candidates) {
+ this.name = name;
+ this._candidates = candidates;
+};
+Library.prototype = Object.freeze({
+ /**
+ * The native library as a js-ctypes object.
+ *
+ * @throws {Error} If none of the candidate libraries could be opened.
+ */
+ get library() {
+ let library;
+ delete this.library;
+ for (let candidate of this._candidates) {
+ try {
+ library = ctypes.open(candidate);
+ break;
+ } catch (ex) {
+ LOG("Could not open library", candidate, ex);
+ }
+ }
+ this._candidates = null;
+ if (library) {
+ Object.defineProperty(this, "library", {
+ value: library
+ });
+ Object.freeze(this);
+ return library;
+ }
+ let error = new Error("Could not open library " + this.name);
+ Object.defineProperty(this, "library", {
+ get: function() {
+ throw error;
+ }
+ });
+ Object.freeze(this);
+ throw error;
+ },
+
+ /**
+ * Declare a function, lazily.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {Type} returnType The type of values returned by the function.
+ * @param {...Type} argTypes The type of arguments to the function.
+ */
+ declareLazyFFI: function(object, field, ...args) {
+ let lib = this;
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ let ffi = declareFFI(lib.library, ...args);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ /**
+ * Define a js-ctypes function lazily using ctypes method declare.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {ctypes.CType} returnType The type of values returned by the function.
+ * @param {...ctypes.CType} argTypes The type of arguments to the function.
+ */
+ declareLazy: function(object, field, ...args) {
+ let lib = this;
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ let ffi = lib.library.declare(...args);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ /**
+ * Define a js-ctypes function lazily using ctypes method declare,
+ * with a fallback library to use if this library can't be opened
+ * or the function cannot be declared.
+ *
+ * @param {fallbacklibrary} The fallback Library object.
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {ctypes.CType} returnType The type of values returned by the function.
+ * @param {...ctypes.CType} argTypes The type of arguments to the function.
+ */
+ declareLazyWithFallback: function(fallbacklibrary, object, field, ...args) {
+ let lib = this;
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ try {
+ let ffi = lib.library.declare(...args);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ } catch (ex) {
+ // Use the fallback library and get the symbol from there.
+ fallbacklibrary.declareLazy(object, field, ...args);
+ return object[field];
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ toString: function() {
+ return "[Library " + this.name + "]";
+ }
+});
+exports.Library = Library;
+
+/**
+ * Declare a function through js-ctypes
+ *
+ * @param {ctypes.library} lib The ctypes library holding the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {Type} returnType The type of values returned by the function.
+ * @param {...Type} argTypes The type of arguments to the function.
+ *
+ * @return null if the function could not be defined (generally because
+ * it does not exist), or a JavaScript wrapper performing the call to C
+ * and any type conversion required.
+ */
+var declareFFI = function declareFFI(lib, symbol, abi,
+ returnType /*, argTypes ...*/) {
+ LOG("Attempting to declare FFI ", symbol);
+ // We guard agressively, to avoid any late surprise
+ if (typeof symbol != "string") {
+ throw new TypeError("declareFFI expects as first argument a string");
+ }
+ abi = abi || ctypes.default_abi;
+ if (Object.prototype.toString.call(abi) != "[object CABI]") {
+ // Note: This is the only known manner of checking whether an object
+ // is an abi.
+ throw new TypeError("declareFFI expects as second argument an abi or null");
+ }
+ if (!returnType.importFromC) {
+ throw new TypeError("declareFFI expects as third argument an instance of Type");
+ }
+ let signature = [symbol, abi];
+ let argtypes = [];
+ for (let i = 3; i < arguments.length; ++i) {
+ let current = arguments[i];
+ if (!current) {
+ throw new TypeError("Missing type for argument " + ( i - 3 ) +
+ " of symbol " + symbol);
+ }
+ if (!current.implementation) {
+ throw new TypeError("Missing implementation for argument " + (i - 3)
+ + " of symbol " + symbol
+ + " ( " + current.name + " )" );
+ }
+ signature.push(current.implementation);
+ }
+ try {
+ let fun = lib.declare.apply(lib, signature);
+ let result = function ffi(...args) {
+ for (let i = 0; i < args.length; i++) {
+ if (typeof args[i] == "undefined") {
+ throw new TypeError("Argument " + i + " of " + symbol + " is undefined");
+ }
+ }
+ let result = fun.apply(fun, args);
+ return returnType.importFromC(result, symbol);
+ };
+ LOG("Function", symbol, "declared");
+ return result;
+ } catch (x) {
+ // Note: Not being able to declare a function is normal.
+ // Some functions are OS (or OS version)-specific.
+ LOG("Could not declare function ", symbol, x);
+ return null;
+ }
+};
+exports.declareFFI = declareFFI;
+
+/**
+ * Define a lazy getter to a js-ctypes function using declareFFI.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {ctypes.library} lib The ctypes library holding the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {Type} returnType The type of values returned by the function.
+ * @param {...Type} argTypes The type of arguments to the function.
+ */
+function declareLazyFFI(object, field, ...declareFFIArgs) {
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ let ffi = declareFFI(...declareFFIArgs);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+}
+exports.declareLazyFFI = declareLazyFFI;
+
+/**
+ * Define a lazy getter to a js-ctypes function using ctypes method declare.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {ctypes.library} lib The ctypes library holding the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {ctypes.CType} returnType The type of values returned by the function.
+ * @param {...ctypes.CType} argTypes The type of arguments to the function.
+ */
+function declareLazy(object, field, lib, ...declareArgs) {
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ try {
+ let ffi = lib.declare(...declareArgs);
+ return this[field] = ffi;
+ } catch (ex) {
+ // The symbol doesn't exist
+ return undefined;
+ }
+ },
+ configurable: true
+ });
+}
+exports.declareLazy = declareLazy;
+
+/**
+ * Utility function used to sanity check buffer and length arguments. The
+ * buffer must be a Typed Array.
+ *
+ * @param {Typed array} candidate The buffer.
+ * @param {number} bytes The number of bytes that |candidate| should contain.
+ *
+ * @return number The bytes argument clamped to the length of the buffer.
+ */
+function normalizeBufferArgs(candidate, bytes) {
+ if (!candidate) {
+ throw new TypeError("Expecting a Typed Array");
+ }
+ if (!isTypedArray(candidate)) {
+ throw new TypeError("Expecting a Typed Array");
+ }
+ if (bytes == null) {
+ bytes = candidate.byteLength;
+ } else if (candidate.byteLength < bytes) {
+ throw new TypeError("Buffer is too short. I need at least " +
+ bytes +
+ " bytes but I have only " +
+ candidate.byteLength +
+ "bytes");
+ }
+ return bytes;
+};
+exports.normalizeBufferArgs = normalizeBufferArgs;
+
+///////////////////// OS interactions
+
+/**
+ * An OS error.
+ *
+ * This class is provided mostly for type-matching. If you need more
+ * details about an error, you should use the platform-specific error
+ * codes provided by subclasses of |OS.Shared.Error|.
+ *
+ * @param {string} operation The operation that failed.
+ * @param {string=} path The path of the file on which the operation failed,
+ * or nothing if there was no file involved in the failure.
+ *
+ * @constructor
+ */
+function OSError(operation, path = "") {
+ Error.call(this);
+ this.operation = operation;
+ this.path = path;
+}
+OSError.prototype = Object.create(Error.prototype);
+exports.OSError = OSError;
+
+
+///////////////////// Temporary boilerplate
+// Boilerplate, to simplify the transition to require()
+// Do not rely upon this symbol, it will disappear with
+// bug 883050.
+exports.OS = {
+ Constants: exports.Constants,
+ Shared: {
+ LOG: LOG,
+ clone: clone,
+ Type: Type,
+ HollowStructure: HollowStructure,
+ Error: OSError,
+ declareFFI: declareFFI,
+ projectValue: projectValue,
+ isTypedArray: isTypedArray,
+ defineLazyGetter: defineLazyGetter
+ }
+};
+
+Object.defineProperty(exports.OS.Shared, "DEBUG", {
+ get: function() {
+ return Config.DEBUG;
+ },
+ set: function(x) {
+ return Config.DEBUG = x;
+ }
+});
+Object.defineProperty(exports.OS.Shared, "TEST", {
+ get: function() {
+ return Config.TEST;
+ },
+ set: function(x) {
+ return Config.TEST = x;
+ }
+});
+
+
+///////////////////// Permanent boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/osfile_shared_front.jsm b/toolkit/components/osfile/modules/osfile_shared_front.jsm
new file mode 100644
index 0000000000..a2971991d4
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm
@@ -0,0 +1,567 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Code shared by OS.File front-ends.
+ *
+ * This code is meant to be included by another library. It is also meant to
+ * be executed only on a worker thread.
+ */
+
+if (typeof Components != "undefined") {
+ throw new Error("osfile_shared_front.jsm cannot be used from the main thread");
+}
+(function(exports) {
+
+var SharedAll =
+ require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+var Path = require("resource://gre/modules/osfile/ospath.jsm");
+var Lz4 =
+ require("resource://gre/modules/lz4.js");
+var LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
+var clone = SharedAll.clone;
+
+/**
+ * Code shared by implementations of File.
+ *
+ * @param {*} fd An OS-specific file handle.
+ * @param {string} path File path of the file handle, used for error-reporting.
+ * @constructor
+ */
+var AbstractFile = function AbstractFile(fd, path) {
+ this._fd = fd;
+ if (!path) {
+ throw new TypeError("path is expected");
+ }
+ this._path = path;
+};
+
+AbstractFile.prototype = {
+ /**
+ * Return the file handle.
+ *
+ * @throw OS.File.Error if the file has been closed.
+ */
+ get fd() {
+ if (this._fd) {
+ return this._fd;
+ }
+ throw OS.File.Error.closed("accessing file", this._path);
+ },
+ /**
+ * Read bytes from this file to a new buffer.
+ *
+ * @param {number=} maybeBytes (deprecated, please use options.bytes)
+ * @param {JSON} options
+ * @return {Uint8Array} An array containing the bytes read.
+ */
+ read: function read(maybeBytes, options = {}) {
+ if (typeof maybeBytes === "object") {
+ // Caller has skipped `maybeBytes` and provided an options object.
+ options = clone(maybeBytes);
+ maybeBytes = null;
+ } else {
+ options = options || {};
+ }
+ let bytes = options.bytes || undefined;
+ if (bytes === undefined) {
+ bytes = maybeBytes == null ? this.stat().size : maybeBytes;
+ }
+ let buffer = new Uint8Array(bytes);
+ let pos = 0;
+ while (pos < bytes) {
+ let length = bytes - pos;
+ let view = new DataView(buffer.buffer, pos, length);
+ let chunkSize = this._read(view, length, options);
+ if (chunkSize == 0) {
+ break;
+ }
+ pos += chunkSize;
+ }
+ if (pos == bytes) {
+ return buffer;
+ } else {
+ return buffer.subarray(0, pos);
+ }
+ },
+
+ /**
+ * Write bytes from a buffer to this file.
+ *
+ * Note that, by default, this function may perform several I/O
+ * operations to ensure that the buffer is fully written.
+ *
+ * @param {Typed array} buffer The buffer in which the the bytes are
+ * stored. The buffer must be large enough to accomodate |bytes| bytes.
+ * @param {*=} options Optionally, an object that may contain the
+ * following fields:
+ * - {number} bytes The number of |bytes| to write from the buffer. If
+ * unspecified, this is |buffer.byteLength|.
+ *
+ * @return {number} The number of bytes actually written.
+ */
+ write: function write(buffer, options = {}) {
+ let bytes =
+ SharedAll.normalizeBufferArgs(buffer, ("bytes" in options) ? options.bytes : undefined);
+ let pos = 0;
+ while (pos < bytes) {
+ let length = bytes - pos;
+ let view = new DataView(buffer.buffer, buffer.byteOffset + pos, length);
+ let chunkSize = this._write(view, length, options);
+ pos += chunkSize;
+ }
+ return pos;
+ }
+};
+
+/**
+ * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name.
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
+ * If |false| use HEX numbers ie: filename-A65BC0.ext
+ * - {number} maxReadableNumber Used to limit the amount of tries after a failed
+ * file creation. Default is 20.
+ *
+ * @return {Object} contains A file object{file} and the path{path}.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+AbstractFile.openUnique = function openUnique(path, options = {}) {
+ let mode = {
+ create : true
+ };
+
+ let dirName = Path.dirname(path);
+ let leafName = Path.basename(path);
+ let lastDotCharacter = leafName.lastIndexOf('.');
+ let fileName = leafName.substring(0, lastDotCharacter != -1 ? lastDotCharacter : leafName.length);
+ let suffix = (lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : "");
+ let uniquePath = "";
+ let maxAttempts = options.maxAttempts || 99;
+ let humanReadable = !!options.humanReadable;
+ const HEX_RADIX = 16;
+ // We produce HEX numbers between 0 and 2^24 - 1.
+ const MAX_HEX_NUMBER = 16777215;
+
+ try {
+ return {
+ path: path,
+ file: OS.File.open(path, mode)
+ };
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
+ for (let i = 0; i < maxAttempts; ++i) {
+ try {
+ if (humanReadable) {
+ uniquePath = Path.join(dirName, fileName + "-" + (i + 1) + suffix);
+ } else {
+ let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX);
+ uniquePath = Path.join(dirName, fileName + "-" + hexNumber + suffix);
+ }
+ return {
+ path: uniquePath,
+ file: OS.File.open(uniquePath, mode)
+ };
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
+ // keep trying ...
+ }
+ }
+ throw OS.File.Error.exists("could not find an unused file name.", path);
+ }
+};
+
+/**
+ * Code shared by iterators.
+ */
+AbstractFile.AbstractIterator = function AbstractIterator() {
+};
+AbstractFile.AbstractIterator.prototype = {
+ /**
+ * Allow iterating with |for|
+ */
+ __iterator__: function __iterator__() {
+ return this;
+ },
+ /**
+ * Apply a function to all elements of the directory sequentially.
+ *
+ * @param {Function} cb This function will be applied to all entries
+ * of the directory. It receives as arguments
+ * - the OS.File.Entry corresponding to the entry;
+ * - the index of the entry in the enumeration;
+ * - the iterator itself - calling |close| on the iterator stops
+ * the loop.
+ */
+ forEach: function forEach(cb) {
+ let index = 0;
+ for (let entry in this) {
+ cb(entry, index++, this);
+ }
+ },
+ /**
+ * Return several entries at once.
+ *
+ * Entries are returned in the same order as a walk with |forEach| or
+ * |for(...)|.
+ *
+ * @param {number=} length If specified, the number of entries
+ * to return. If unspecified, return all remaining entries.
+ * @return {Array} An array containing the next |length| entries, or
+ * less if the iteration contains less than |length| entries left.
+ */
+ nextBatch: function nextBatch(length) {
+ let array = [];
+ let i = 0;
+ for (let entry in this) {
+ array.push(entry);
+ if (++i >= length) {
+ return array;
+ }
+ }
+ return array;
+ }
+};
+
+/**
+ * Utility function shared by implementations of |OS.File.open|:
+ * extract read/write/trunc/create/existing flags from a |mode|
+ * object.
+ *
+ * @param {*=} mode An object that may contain fields |read|,
+ * |write|, |truncate|, |create|, |existing|. These fields
+ * are interpreted only if true-ish.
+ * @return {{read:bool, write:bool, trunc:bool, create:bool,
+ * existing:bool}} an object recapitulating the options set
+ * by |mode|.
+ * @throws {TypeError} If |mode| contains other fields, or
+ * if it contains both |create| and |truncate|, or |create|
+ * and |existing|.
+ */
+AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
+ let result = {
+ read: false,
+ write: false,
+ trunc: false,
+ create: false,
+ existing: false,
+ append: true
+ };
+ for (let key in mode) {
+ let val = !!mode[key]; // bool cast.
+ switch (key) {
+ case "read":
+ result.read = val;
+ break;
+ case "write":
+ result.write = val;
+ break;
+ case "truncate": // fallthrough
+ case "trunc":
+ result.trunc = val;
+ result.write |= val;
+ break;
+ case "create":
+ result.create = val;
+ result.write |= val;
+ break;
+ case "existing": // fallthrough
+ case "exist":
+ result.existing = val;
+ break;
+ case "append":
+ result.append = val;
+ break;
+ default:
+ throw new TypeError("Mode " + key + " not understood");
+ }
+ }
+ // Reject opposite modes
+ if (result.existing && result.create) {
+ throw new TypeError("Cannot specify both existing:true and create:true");
+ }
+ if (result.trunc && result.create) {
+ throw new TypeError("Cannot specify both trunc:true and create:true");
+ }
+ // Handle read/write
+ if (!result.write) {
+ result.read = true;
+ }
+ return result;
+};
+
+/**
+ * Return the contents of a file.
+ *
+ * @param {string} path The path to the file.
+ * @param {number=} bytes Optionally, an upper bound to the number of bytes
+ * to read. DEPRECATED - please use options.bytes instead.
+ * @param {object=} options Optionally, an object with some of the following
+ * fields:
+ * - {number} bytes An upper bound to the number of bytes to read.
+ * - {string} compression If "lz4" and if the file is compressed using the lz4
+ * compression algorithm, decompress the file contents on the fly.
+ *
+ * @return {Uint8Array} A buffer holding the bytes
+ * and the number of bytes read from the file.
+ */
+AbstractFile.read = function read(path, bytes, options = {}) {
+ if (bytes && typeof bytes == "object") {
+ options = bytes;
+ bytes = options.bytes || null;
+ }
+ if ("encoding" in options && typeof options.encoding != "string") {
+ throw new TypeError("Invalid type for option encoding");
+ }
+ if ("compression" in options && typeof options.compression != "string") {
+ throw new TypeError("Invalid type for option compression: " + options.compression);
+ }
+ if ("bytes" in options && typeof options.bytes != "number") {
+ throw new TypeError("Invalid type for option bytes");
+ }
+ let file = exports.OS.File.open(path);
+ try {
+ let buffer = file.read(bytes, options);
+ if ("compression" in options) {
+ if (options.compression == "lz4") {
+ options = Object.create(options);
+ options.path = path;
+ buffer = Lz4.decompressFileContent(buffer, options);
+ } else {
+ throw OS.File.Error.invalidArgument("Compression");
+ }
+ }
+ if (!("encoding" in options)) {
+ return buffer;
+ }
+ let decoder;
+ try {
+ decoder = new TextDecoder(options.encoding);
+ } catch (ex if ex instanceof RangeError) {
+ throw OS.File.Error.invalidArgument("Decode");
+ }
+ return decoder.decode(buffer);
+ } finally {
+ file.close();
+ }
+};
+
+/**
+ * Write a file, atomically.
+ *
+ * By opposition to a regular |write|, this operation ensures that,
+ * until the contents are fully written, the destination file is
+ * not modified.
+ *
+ * Limitation: In a few extreme cases (hardware failure during the
+ * write, user unplugging disk during the write, etc.), data may be
+ * corrupted. If your data is user-critical (e.g. preferences,
+ * application data, etc.), you may wish to consider adding options
+ * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
+ * detailed below. Note that no combination of options can be
+ * guaranteed to totally eliminate the risk of corruption.
+ *
+ * @param {string} path The path of the file to modify.
+ * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
+ * @param {*=} options Optionally, an object determining the behavior
+ * of this function. This object may contain the following fields:
+ * - {number} bytes The number of bytes to write. If unspecified,
+ * |buffer.byteLength|. Required if |buffer| is a C pointer.
+ * - {string} tmpPath If |null| or unspecified, write all data directly
+ * to |path|. If specified, write all data to a temporary file called
+ * |tmpPath| and, once this write is complete, rename the file to
+ * replace |path|. Performing this additional operation is a little
+ * slower but also a little safer.
+ * - {bool} noOverwrite - If set, this function will fail if a file already
+ * exists at |path|.
+ * - {bool} flush - If |false| or unspecified, return immediately once the
+ * write is complete. If |true|, before writing, force the operating system
+ * to write its internal disk buffers to the disk. This is considerably slower
+ * (not just for the application but for the whole system) but also safer:
+ * if the system shuts down improperly (typically due to a kernel freeze
+ * or a power failure) or if the device is disconnected before the buffer
+ * is flushed, the file has more chances of not being corrupted.
+ * - {string} compression - If empty or unspecified, do not compress the file.
+ * If "lz4", compress the contents of the file atomically using lz4. For the
+ * time being, the container format is specific to Mozilla and cannot be read
+ * by means other than OS.File.read(..., { compression: "lz4"})
+ * - {string} backupTo - If specified, backup the destination file as |backupTo|.
+ * Note that this function renames the destination file before overwriting it.
+ * If the process or the operating system freezes or crashes
+ * during the short window between these operations,
+ * the destination file will have been moved to its backup.
+ *
+ * @return {number} The number of bytes actually written.
+ */
+AbstractFile.writeAtomic =
+ function writeAtomic(path, buffer, options = {}) {
+
+ // Verify that path is defined and of the correct type
+ if (typeof path != "string" || path == "") {
+ throw new TypeError("File path should be a (non-empty) string");
+ }
+ let noOverwrite = options.noOverwrite;
+ if (noOverwrite && OS.File.exists(path)) {
+ throw OS.File.Error.exists("writeAtomic", path);
+ }
+
+ if (typeof buffer == "string") {
+ // Normalize buffer to a C buffer by encoding it
+ let encoding = options.encoding || "utf-8";
+ buffer = new TextEncoder(encoding).encode(buffer);
+ }
+
+ if ("compression" in options && options.compression == "lz4") {
+ buffer = Lz4.compressFileContent(buffer, options);
+ options = Object.create(options);
+ options.bytes = buffer.byteLength;
+ }
+
+ let bytesWritten = 0;
+
+ if (!options.tmpPath) {
+ if (options.backupTo) {
+ try {
+ OS.File.move(path, options.backupTo, {noCopy: true});
+ } catch (ex if ex.becauseNoSuchFile) {
+ // The file doesn't exist, nothing to backup.
+ }
+ }
+ // Just write, without any renaming trick
+ let dest = OS.File.open(path, {write: true, truncate: true});
+ try {
+ bytesWritten = dest.write(buffer, options);
+ if (options.flush) {
+ dest.flush();
+ }
+ } finally {
+ dest.close();
+ }
+ return bytesWritten;
+ }
+
+ let tmpFile = OS.File.open(options.tmpPath, {write: true, truncate: true});
+ try {
+ bytesWritten = tmpFile.write(buffer, options);
+ if (options.flush) {
+ tmpFile.flush();
+ }
+ } catch (x) {
+ OS.File.remove(options.tmpPath);
+ throw x;
+ } finally {
+ tmpFile.close();
+ }
+
+ if (options.backupTo) {
+ try {
+ OS.File.move(path, options.backupTo, {noCopy: true});
+ } catch (ex if ex.becauseNoSuchFile) {
+ // The file doesn't exist, nothing to backup.
+ }
+ }
+
+ OS.File.move(options.tmpPath, path, {noCopy: true});
+ return bytesWritten;
+};
+
+/**
+ * This function is used by removeDir to avoid calling lstat for each
+ * files under the specified directory. External callers should not call
+ * this function directly.
+ */
+AbstractFile.removeRecursive = function(path, options = {}) {
+ let iterator = new OS.File.DirectoryIterator(path);
+ if (!iterator.exists()) {
+ if (!("ignoreAbsent" in options) || options.ignoreAbsent) {
+ return;
+ }
+ }
+
+ try {
+ for (let entry in iterator) {
+ if (entry.isDir) {
+ if (entry.isLink) {
+ // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
+ // directories are directories themselves. OS.File.remove()
+ // will not work for them.
+ OS.File.removeEmptyDir(entry.path, options);
+ } else {
+ // Normal directories.
+ AbstractFile.removeRecursive(entry.path, options);
+ }
+ } else {
+ // NTFS symlinks to files, Unix symlinks, or regular files.
+ OS.File.remove(entry.path, options);
+ }
+ }
+ } finally {
+ iterator.close();
+ }
+
+ OS.File.removeEmptyDir(path);
+};
+
+/**
+ * Create a directory and, optionally, its parent directories.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ *
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |path|
+ * must be a descendant of |from|, and that |from| and its existing
+ * subdirectories present in |path| must be user-writeable.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ * - {bool} ignoreExisting If |false|, throw an error if the directory
+ * already exists. |true| by default. Ignored if |from| is specified.
+ * - {number} unixMode Under Unix, if specified, a file creation mode,
+ * as per libc function |mkdir|. If unspecified, dirs are
+ * created with a default mode of 0700 (dir is private to
+ * the user, the user can read, write and execute). Ignored under Windows
+ * or if the file system does not support file creation modes.
+ * - {C pointer} winSecurity Under Windows, if specified, security
+ * attributes as per winapi function |CreateDirectory|. If
+ * unspecified, use the default security descriptor, inherited from
+ * the parent directory. Ignored under Unix or if the file system
+ * does not support security descriptors.
+ */
+AbstractFile.makeDir = function(path, options = {}) {
+ let from = options.from;
+ if (!from) {
+ OS.File._makeDir(path, options);
+ return;
+ }
+ if (!path.startsWith(from)) {
+ // Apparently, `from` is not a parent of `path`. However, we may
+ // have false negatives due to non-normalized paths, e.g.
+ // "foo//bar" is a parent of "foo/bar/sna".
+ path = Path.normalize(path);
+ from = Path.normalize(from);
+ if (!path.startsWith(from)) {
+ throw new Error("Incorrect use of option |from|: " + path + " is not a descendant of " + from);
+ }
+ }
+ let innerOptions = Object.create(options, {
+ ignoreExisting: {
+ value: true
+ }
+ });
+ // Compute the elements that appear in |path| but not in |from|.
+ let items = Path.split(path).components.slice(Path.split(from).components.length);
+ let current = from;
+ for (let item of items) {
+ current = Path.join(current, item);
+ OS.File._makeDir(current, innerOptions);
+ }
+};
+
+if (!exports.OS.Shared) {
+ exports.OS.Shared = {};
+}
+exports.OS.Shared.AbstractFile = AbstractFile;
+})(this);
diff --git a/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
new file mode 100644
index 0000000000..632f9c40be
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
@@ -0,0 +1,375 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 module defines the thread-agnostic components of the Unix version
+ * of OS.File. It depends on the thread-agnostic cross-platform components
+ * of OS.File.
+ *
+ * It serves the following purposes:
+ * - open libc;
+ * - define OS.Unix.Error;
+ * - define a few constants and types that need to be defined on all platforms.
+ *
+ * This module can be:
+ * - opened from the main thread as a jsm module;
+ * - opened from a chrome worker through require().
+ */
+
+"use strict";
+
+var SharedAll;
+if (typeof Components != "undefined") {
+ let Cu = Components.utils;
+ // Module is opened as a jsm module
+ Cu.import("resource://gre/modules/ctypes.jsm", this);
+
+ SharedAll = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+ this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+ // Module is loaded with require()
+ SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+} else {
+ throw new Error("Please open this module with Component.utils.import or with require()");
+}
+
+var LOG = SharedAll.LOG.bind(SharedAll, "Unix", "allthreads");
+var Const = SharedAll.Constants.libc;
+
+// Open libc
+var libc = new SharedAll.Library("libc",
+ "libc.so", "libSystem.B.dylib", "a.out");
+exports.libc = libc;
+
+// Define declareFFI
+var declareFFI = SharedAll.declareFFI.bind(null, libc);
+exports.declareFFI = declareFFI;
+
+// Define lazy binding
+var LazyBindings = {};
+libc.declareLazy(LazyBindings, "strerror",
+ "strerror", ctypes.default_abi,
+ /*return*/ ctypes.char.ptr,
+ /*errnum*/ ctypes.int);
+
+/**
+ * A File-related error.
+ *
+ * To obtain a human-readable error message, use method |toString|.
+ * To determine the cause of the error, use the various |becauseX|
+ * getters. To determine the operation that failed, use field
+ * |operation|.
+ *
+ * Additionally, this implementation offers a field
+ * |unixErrno|, which holds the OS-specific error
+ * constant. If you need this level of detail, you may match the value
+ * of this field against the error constants of |OS.Constants.libc|.
+ *
+ * @param {string=} operation The operation that failed. If unspecified,
+ * the name of the calling function is taken to be the operation that
+ * failed.
+ * @param {number=} lastError The OS-specific constant detailing the
+ * reason of the error. If unspecified, this is fetched from the system
+ * status.
+ * @param {string=} path The file path that manipulated. If unspecified,
+ * assign the empty string.
+ *
+ * @constructor
+ * @extends {OS.Shared.Error}
+ */
+var OSError = function OSError(operation = "unknown operation",
+ errno = ctypes.errno, path = "") {
+ SharedAll.OSError.call(this, operation, path);
+ this.unixErrno = errno;
+};
+OSError.prototype = Object.create(SharedAll.OSError.prototype);
+OSError.prototype.toString = function toString() {
+ return "Unix error " + this.unixErrno +
+ " during operation " + this.operation +
+ (this.path? " on file " + this.path : "") +
+ " (" + LazyBindings.strerror(this.unixErrno).readString() + ")";
+};
+OSError.prototype.toMsg = function toMsg() {
+ return OSError.toMsg(this);
+};
+
+/**
+ * |true| if the error was raised because a file or directory
+ * already exists, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseExists", {
+ get: function becauseExists() {
+ return this.unixErrno == Const.EEXIST;
+ }
+});
+/**
+ * |true| if the error was raised because a file or directory
+ * does not exist, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseNoSuchFile", {
+ get: function becauseNoSuchFile() {
+ return this.unixErrno == Const.ENOENT;
+ }
+});
+
+/**
+ * |true| if the error was raised because a directory is not empty
+ * does not exist, |false| otherwise.
+ */
+ Object.defineProperty(OSError.prototype, "becauseNotEmpty", {
+ get: function becauseNotEmpty() {
+ return this.unixErrno == Const.ENOTEMPTY;
+ }
+ });
+/**
+ * |true| if the error was raised because a file or directory
+ * is closed, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseClosed", {
+ get: function becauseClosed() {
+ return this.unixErrno == Const.EBADF;
+ }
+});
+/**
+ * |true| if the error was raised because permission is denied to
+ * access a file or directory, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseAccessDenied", {
+ get: function becauseAccessDenied() {
+ return this.unixErrno == Const.EACCES;
+ }
+});
+/**
+ * |true| if the error was raised because some invalid argument was passed,
+ * |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseInvalidArgument", {
+ get: function becauseInvalidArgument() {
+ return this.unixErrno == Const.EINVAL;
+ }
+});
+
+/**
+ * Serialize an instance of OSError to something that can be
+ * transmitted across threads (not necessarily a string).
+ */
+OSError.toMsg = function toMsg(error) {
+ return {
+ exn: "OS.File.Error",
+ fileName: error.moduleName,
+ lineNumber: error.lineNumber,
+ stack: error.moduleStack,
+ operation: error.operation,
+ unixErrno: error.unixErrno,
+ path: error.path
+ };
+};
+
+/**
+ * Deserialize a message back to an instance of OSError
+ */
+OSError.fromMsg = function fromMsg(msg) {
+ let error = new OSError(msg.operation, msg.unixErrno, msg.path);
+ error.stack = msg.stack;
+ error.fileName = msg.fileName;
+ error.lineNumber = msg.lineNumber;
+ return error;
+};
+exports.Error = OSError;
+
+/**
+ * Code shared by implementations of File.Info on Unix
+ *
+ * @constructor
+*/
+var AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size, lastAccessDate,
+ lastModificationDate, unixLastStatusChangeDate,
+ unixOwner, unixGroup, unixMode) {
+ this._path = path;
+ this._isDir = isDir;
+ this._isSymlLink = isSymLink;
+ this._size = size;
+ this._lastAccessDate = lastAccessDate;
+ this._lastModificationDate = lastModificationDate;
+ this._unixLastStatusChangeDate = unixLastStatusChangeDate;
+ this._unixOwner = unixOwner;
+ this._unixGroup = unixGroup;
+ this._unixMode = unixMode;
+};
+
+AbstractInfo.prototype = {
+ /**
+ * The path of the file, used for error-reporting.
+ *
+ * @type {string}
+ */
+ get path() {
+ return this._path;
+ },
+ /**
+ * |true| if this file is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if this file is a symbolink link, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymlLink;
+ },
+ /**
+ * The size of the file, in bytes.
+ *
+ * Note that the result may be |NaN| if the size of the file cannot be
+ * represented in JavaScript.
+ *
+ * @type {number}
+ */
+ get size() {
+ return this._size;
+ },
+ /**
+ * The date of last access to this file.
+ *
+ * Note that the definition of last access may depend on the
+ * underlying operating system and file system.
+ *
+ * @type {Date}
+ */
+ get lastAccessDate() {
+ return this._lastAccessDate;
+ },
+ /**
+ * Return the date of last modification of this file.
+ */
+ get lastModificationDate() {
+ return this._lastModificationDate;
+ },
+ /**
+ * Return the date at which the status of this file was last modified
+ * (this is the date of the latest write/renaming/mode change/...
+ * of the file)
+ */
+ get unixLastStatusChangeDate() {
+ return this._unixLastStatusChangeDate;
+ },
+ /*
+ * Return the Unix owner of this file
+ */
+ get unixOwner() {
+ return this._unixOwner;
+ },
+ /*
+ * Return the Unix group of this file
+ */
+ get unixGroup() {
+ return this._unixGroup;
+ },
+ /*
+ * Return the Unix group of this file
+ */
+ get unixMode() {
+ return this._unixMode;
+ }
+};
+exports.AbstractInfo = AbstractInfo;
+
+/**
+ * Code shared by implementations of File.DirectoryIterator.Entry on Unix
+ *
+ * @constructor
+*/
+var AbstractEntry = function AbstractEntry(isDir, isSymLink, name, path) {
+ this._isDir = isDir;
+ this._isSymlLink = isSymLink;
+ this._name = name;
+ this._path = path;
+};
+
+AbstractEntry.prototype = {
+ /**
+ * |true| if the entry is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if the entry is a directory, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymlLink;
+ },
+ /**
+ * The name of the entry
+ * @type {string}
+ */
+ get name() {
+ return this._name;
+ },
+ /**
+ * The full path to the entry
+ */
+ get path() {
+ return this._path;
+ }
+};
+exports.AbstractEntry = AbstractEntry;
+
+// Special constants that need to be defined on all platforms
+
+exports.POS_START = Const.SEEK_SET;
+exports.POS_CURRENT = Const.SEEK_CUR;
+exports.POS_END = Const.SEEK_END;
+
+// Special types that need to be defined for communication
+// between threads
+var Type = Object.create(SharedAll.Type);
+exports.Type = Type;
+
+/**
+ * Native paths
+ *
+ * Under Unix, expressed as C strings
+ */
+Type.path = Type.cstring.withName("[in] path");
+Type.out_path = Type.out_cstring.withName("[out] path");
+
+// Special constructors that need to be defined on all threads
+OSError.closed = function closed(operation, path) {
+ return new OSError(operation, Const.EBADF, path);
+};
+
+OSError.exists = function exists(operation, path) {
+ return new OSError(operation, Const.EEXIST, path);
+};
+
+OSError.noSuchFile = function noSuchFile(operation, path) {
+ return new OSError(operation, Const.ENOENT, path);
+};
+
+OSError.invalidArgument = function invalidArgument(operation) {
+ return new OSError(operation, Const.EINVAL);
+};
+
+var EXPORTED_SYMBOLS = [
+ "declareFFI",
+ "libc",
+ "Error",
+ "AbstractInfo",
+ "AbstractEntry",
+ "Type",
+ "POS_START",
+ "POS_CURRENT",
+ "POS_END"
+];
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/osfile_unix_back.jsm b/toolkit/components/osfile/modules/osfile_unix_back.jsm
new file mode 100644
index 0000000000..a028dda7d5
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_unix_back.jsm
@@ -0,0 +1,735 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_unix_back.jsm to be used directly as a main thread
+ // module yet. When time comes, it will be loaded by a combination of
+ // a main thread front-end/worker thread implementation that makes sure
+ // that we are not executing synchronous IO code in the main thread.
+
+ throw new Error("osfile_unix_back.jsm cannot be used from the main thread yet");
+ }
+ (function(exports) {
+ "use strict";
+ if (exports.OS && exports.OS.Unix && exports.OS.Unix.File) {
+ return; // Avoid double initialization
+ }
+
+ let SharedAll =
+ require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let SysAll =
+ require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm");
+ let LOG = SharedAll.LOG.bind(SharedAll, "Unix", "back");
+ let libc = SysAll.libc;
+ let Const = SharedAll.Constants.libc;
+
+ /**
+ * Initialize the Unix module.
+ *
+ * @param {function=} declareFFI
+ */
+ // FIXME: Both |init| and |aDeclareFFI| are deprecated, we should remove them
+ let init = function init(aDeclareFFI) {
+ let declareFFI;
+ if (aDeclareFFI) {
+ declareFFI = aDeclareFFI.bind(null, libc);
+ } else {
+ declareFFI = SysAll.declareFFI;
+ }
+ let declareLazyFFI = SharedAll.declareLazyFFI;
+
+ // Initialize types that require additional OS-specific
+ // support - either finalization or matching against
+ // OS-specific constants.
+ let Type = Object.create(SysAll.Type);
+ let SysFile = exports.OS.Unix.File = { Type: Type };
+
+ /**
+ * A file descriptor.
+ */
+ Type.fd = Type.int.withName("fd");
+ Type.fd.importFromC = function importFromC(fd_int) {
+ return ctypes.CDataFinalizer(fd_int, SysFile._close);
+ };
+
+
+ /**
+ * A C integer holding -1 in case of error or a file descriptor
+ * in case of success.
+ */
+ Type.negativeone_or_fd = Type.fd.withName("negativeone_or_fd");
+ Type.negativeone_or_fd.importFromC =
+ function importFromC(fd_int) {
+ if (fd_int == -1) {
+ return -1;
+ }
+ return ctypes.CDataFinalizer(fd_int, SysFile._close);
+ };
+
+ /**
+ * A C integer holding -1 in case of error or a meaningless value
+ * in case of success.
+ */
+ Type.negativeone_or_nothing =
+ Type.int.withName("negativeone_or_nothing");
+
+ /**
+ * A C integer holding -1 in case of error or a positive integer
+ * in case of success.
+ */
+ Type.negativeone_or_ssize_t =
+ Type.ssize_t.withName("negativeone_or_ssize_t");
+
+ /**
+ * Various libc integer types
+ */
+ Type.mode_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_MODE_T).withName("mode_t");
+ Type.uid_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_UID_T).withName("uid_t");
+ Type.gid_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_GID_T).withName("gid_t");
+
+ /**
+ * Type |time_t|
+ */
+ Type.time_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_TIME_T).withName("time_t");
+
+ // Structure |dirent|
+ // Building this type is rather complicated, as its layout varies between
+ // variants of Unix. For this reason, we rely on a number of constants
+ // (computed in C from the C data structures) that give us the layout.
+ // The structure we compute looks like
+ // { int8_t[...] before_d_type; // ignored content
+ // int8_t d_type ;
+ // int8_t[...] before_d_name; // ignored content
+ // char[...] d_name;
+ // };
+ {
+ let d_name_extra_size = 0;
+ if (Const.OSFILE_SIZEOF_DIRENT_D_NAME < 8) {
+ // d_name is defined like "char d_name[1];" on some platforms
+ // (e.g. Solaris), we need to give it more size for our structure.
+ d_name_extra_size = 256;
+ }
+
+ let dirent = new SharedAll.HollowStructure("dirent",
+ Const.OSFILE_SIZEOF_DIRENT + d_name_extra_size);
+ if (Const.OSFILE_OFFSETOF_DIRENT_D_TYPE != undefined) {
+ // |dirent| doesn't have d_type on some platforms (e.g. Solaris).
+ dirent.add_field_at(Const.OSFILE_OFFSETOF_DIRENT_D_TYPE,
+ "d_type", ctypes.uint8_t);
+ }
+ dirent.add_field_at(Const.OSFILE_OFFSETOF_DIRENT_D_NAME,
+ "d_name", ctypes.ArrayType(ctypes.char,
+ Const.OSFILE_SIZEOF_DIRENT_D_NAME + d_name_extra_size));
+
+ // We now have built |dirent|.
+ Type.dirent = dirent.getType();
+ }
+ Type.null_or_dirent_ptr =
+ new SharedAll.Type("null_of_dirent",
+ Type.dirent.out_ptr.implementation);
+
+ // Structure |stat|
+ // Same technique
+ {
+ let stat = new SharedAll.HollowStructure("stat",
+ Const.OSFILE_SIZEOF_STAT);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_MODE,
+ "st_mode", Type.mode_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_UID,
+ "st_uid", Type.uid_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_GID,
+ "st_gid", Type.gid_t.implementation);
+
+ // Here, things get complicated with different data structures.
+ // Some platforms have |time_t st_atime| and some platforms have
+ // |timespec st_atimespec|. However, since |timespec| starts with
+ // a |time_t|, followed by nanoseconds, we just cheat and pretend
+ // that everybody has |time_t st_atime|, possibly followed by padding
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_ATIME,
+ "st_atime", Type.time_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_MTIME,
+ "st_mtime", Type.time_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_CTIME,
+ "st_ctime", Type.time_t.implementation);
+
+ // To complicate further, MacOS and some BSDs have a field |birthtime|
+ if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) {
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_BIRTHTIME,
+ "st_birthtime", Type.time_t.implementation);
+ }
+
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_SIZE,
+ "st_size", Type.off_t.implementation);
+ Type.stat = stat.getType();
+ }
+
+ // Structure |DIR|
+ if ("OSFILE_SIZEOF_DIR" in Const) {
+ // On platforms for which we need to access the fields of DIR
+ // directly (e.g. because certain functions are implemented
+ // as macros), we need to define DIR as a hollow structure.
+ let DIR = new SharedAll.HollowStructure(
+ "DIR",
+ Const.OSFILE_SIZEOF_DIR);
+
+ DIR.add_field_at(
+ Const.OSFILE_OFFSETOF_DIR_DD_FD,
+ "dd_fd",
+ Type.fd.implementation);
+
+ Type.DIR = DIR.getType();
+ } else {
+ // On other platforms, we keep DIR as a blackbox
+ Type.DIR =
+ new SharedAll.Type("DIR",
+ ctypes.StructType("DIR"));
+ }
+
+ Type.null_or_DIR_ptr =
+ Type.DIR.out_ptr.withName("null_or_DIR*");
+ Type.null_or_DIR_ptr.importFromC = function importFromC(dir) {
+ if (dir == null || dir.isNull()) {
+ return null;
+ }
+ return ctypes.CDataFinalizer(dir, SysFile._close_dir);
+ };
+
+ // Structure |timeval|
+ {
+ let timeval = new SharedAll.HollowStructure(
+ "timeval",
+ Const.OSFILE_SIZEOF_TIMEVAL);
+ timeval.add_field_at(
+ Const.OSFILE_OFFSETOF_TIMEVAL_TV_SEC,
+ "tv_sec",
+ Type.long.implementation);
+ timeval.add_field_at(
+ Const.OSFILE_OFFSETOF_TIMEVAL_TV_USEC,
+ "tv_usec",
+ Type.long.implementation);
+ Type.timeval = timeval.getType();
+ Type.timevals = new SharedAll.Type("two timevals",
+ ctypes.ArrayType(Type.timeval.implementation, 2));
+ }
+
+ // Types fsblkcnt_t and fsfilcnt_t, used by structure |statvfs|
+ Type.fsblkcnt_t =
+ Type.uintn_t(Const.OSFILE_SIZEOF_FSBLKCNT_T).withName("fsblkcnt_t");
+
+ // Structure |statvfs|
+ // Use an hollow structure
+ {
+ let statvfs = new SharedAll.HollowStructure("statvfs",
+ Const.OSFILE_SIZEOF_STATVFS);
+
+ statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BSIZE,
+ "f_bsize", Type.unsigned_long.implementation);
+ statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BAVAIL,
+ "f_bavail", Type.fsblkcnt_t.implementation);
+
+ Type.statvfs = statvfs.getType();
+ }
+
+ // Declare libc functions as functions of |OS.Unix.File|
+
+ // Finalizer-related functions
+ libc.declareLazy(SysFile, "_close",
+ "close", ctypes.default_abi,
+ /*return */ctypes.int,
+ /*fd*/ ctypes.int);
+
+ SysFile.close = function close(fd) {
+ // Detach the finalizer and call |_close|.
+ return fd.dispose();
+ };
+
+ libc.declareLazy(SysFile, "_close_dir",
+ "closedir", ctypes.default_abi,
+ /*return */ctypes.int,
+ /*dirp*/ Type.DIR.in_ptr.implementation);
+
+ SysFile.closedir = function closedir(fd) {
+ // Detach the finalizer and call |_close_dir|.
+ return fd.dispose();
+ };
+
+ {
+ // Symbol free() is special.
+ // We override the definition of free() on several platforms.
+ let default_lib = new SharedAll.Library("default_lib",
+ "a.out");
+
+ // On platforms for which we override free(), nspr defines
+ // a special library name "a.out" that will resolve to the
+ // correct implementation free().
+ // If it turns out we don't have an a.out library or a.out
+ // doesn't contain free, use the ordinary libc free.
+
+ default_lib.declareLazyWithFallback(libc, SysFile, "free",
+ "free", ctypes.default_abi,
+ /*return*/ ctypes.void_t,
+ /*ptr*/ ctypes.voidptr_t);
+ }
+
+
+ // Other functions
+ libc.declareLazyFFI(SysFile, "access",
+ "access", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*mode*/ Type.int);
+
+ libc.declareLazyFFI(SysFile, "chdir",
+ "chdir", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "chmod",
+ "chmod", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*mode*/ Type.mode_t);
+
+ libc.declareLazyFFI(SysFile, "chown",
+ "chown", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*uid*/ Type.uid_t,
+ /*gid*/ Type.gid_t);
+
+ libc.declareLazyFFI(SysFile, "copyfile",
+ "copyfile", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*source*/ Type.path,
+ /*dest*/ Type.path,
+ /*state*/ Type.void_t.in_ptr, // Ignored atm
+ /*flags*/ Type.uint32_t);
+
+ libc.declareLazyFFI(SysFile, "dup",
+ "dup", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_fd,
+ /*fd*/ Type.fd);
+
+ if ("OSFILE_SIZEOF_DIR" in Const) {
+ // On platforms for which |dirfd| is a macro
+ SysFile.dirfd =
+ function dirfd(DIRp) {
+ return Type.DIR.in_ptr.implementation(DIRp).contents.dd_fd;
+ };
+ } else {
+ // On platforms for which |dirfd| is a function
+ libc.declareLazyFFI(SysFile, "dirfd",
+ "dirfd", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_fd,
+ /*dir*/ Type.DIR.in_ptr);
+ }
+
+ libc.declareLazyFFI(SysFile, "chdir",
+ "chdir", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "fchdir",
+ "fchdir", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd);
+
+ libc.declareLazyFFI(SysFile, "fchmod",
+ "fchmod", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*mode*/ Type.mode_t);
+
+ libc.declareLazyFFI(SysFile, "fchown",
+ "fchown", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*uid_t*/ Type.uid_t,
+ /*gid_t*/ Type.gid_t);
+
+ libc.declareLazyFFI(SysFile, "fsync",
+ "fsync", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd);
+
+ libc.declareLazyFFI(SysFile, "getcwd",
+ "getcwd", ctypes.default_abi,
+ /*return*/ Type.out_path,
+ /*buf*/ Type.out_path,
+ /*size*/ Type.size_t);
+
+ libc.declareLazyFFI(SysFile, "getwd",
+ "getwd", ctypes.default_abi,
+ /*return*/ Type.out_path,
+ /*buf*/ Type.out_path);
+
+ // Two variants of |getwd| which allocate the memory
+ // dynamically.
+
+ // Linux/Android version
+ libc.declareLazyFFI(SysFile, "get_current_dir_name",
+ "get_current_dir_name", ctypes.default_abi,
+ /*return*/ Type.out_path.releaseWithLazy(() =>
+ SysFile.free
+ ));
+
+ // MacOS/BSD version (will return NULL on Linux/Android)
+ libc.declareLazyFFI(SysFile, "getwd_auto",
+ "getwd", ctypes.default_abi,
+ /*return*/ Type.out_path.releaseWithLazy(() =>
+ SysFile.free
+ ),
+ /*buf*/ Type.void_t.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "fdatasync",
+ "fdatasync", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd); // Note: MacOS/BSD-specific
+
+ libc.declareLazyFFI(SysFile, "ftruncate",
+ "ftruncate", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*length*/ Type.off_t);
+
+
+ libc.declareLazyFFI(SysFile, "lchown",
+ "lchown", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*uid_t*/ Type.uid_t,
+ /*gid_t*/ Type.gid_t);
+
+ libc.declareLazyFFI(SysFile, "link",
+ "link", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*source*/ Type.path,
+ /*dest*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "lseek",
+ "lseek", ctypes.default_abi,
+ /*return*/ Type.off_t,
+ /*fd*/ Type.fd,
+ /*offset*/ Type.off_t,
+ /*whence*/ Type.int);
+
+ libc.declareLazyFFI(SysFile, "mkdir",
+ "mkdir", ctypes.default_abi,
+ /*return*/ Type.int,
+ /*path*/ Type.path,
+ /*mode*/ Type.int);
+
+ libc.declareLazyFFI(SysFile, "mkstemp",
+ "mkstemp", ctypes.default_abi,
+ /*return*/ Type.fd,
+ /*template*/ Type.out_path);
+
+ libc.declareLazyFFI(SysFile, "open",
+ "open", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_fd,
+ /*path*/ Type.path,
+ /*oflags*/ Type.int,
+ /*mode*/ Type.int);
+
+ if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "opendir",
+ "__opendir30", ctypes.default_abi,
+ /*return*/ Type.null_or_DIR_ptr,
+ /*path*/ Type.path);
+ } else {
+ libc.declareLazyFFI(SysFile, "opendir",
+ "opendir", ctypes.default_abi,
+ /*return*/ Type.null_or_DIR_ptr,
+ /*path*/ Type.path);
+ }
+
+ libc.declareLazyFFI(SysFile, "pread",
+ "pread", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.out_ptr,
+ /*nbytes*/ Type.size_t,
+ /*offset*/ Type.off_t);
+
+ libc.declareLazyFFI(SysFile, "pwrite",
+ "pwrite", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.in_ptr,
+ /*nbytes*/ Type.size_t,
+ /*offset*/ Type.off_t);
+
+ libc.declareLazyFFI(SysFile, "read",
+ "read", ctypes.default_abi,
+ /*return*/Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.out_ptr,
+ /*nbytes*/Type.size_t);
+
+ libc.declareLazyFFI(SysFile, "posix_fadvise",
+ "posix_fadvise", ctypes.default_abi,
+ /*return*/ Type.int,
+ /*fd*/ Type.fd,
+ /*offset*/ Type.off_t,
+ /*len*/ Type.off_t,
+ /*advise*/ Type.int);
+
+ if (Const._DARWIN_FEATURE_64_BIT_INODE) {
+ // Special case for MacOS X 10.5+
+ // Symbol name "readdir" still exists but is used for a
+ // deprecated function that does not match the
+ // constants of |Const|.
+ libc.declareLazyFFI(SysFile, "readdir",
+ "readdir$INODE64", ctypes.default_abi,
+ /*return*/ Type.null_or_dirent_ptr,
+ /*dir*/ Type.DIR.in_ptr); // For MacOS X
+ } else if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "readdir",
+ "__readdir30", ctypes.default_abi,
+ /*return*/Type.null_or_dirent_ptr,
+ /*dir*/ Type.DIR.in_ptr); // Other Unices
+ } else {
+ libc.declareLazyFFI(SysFile, "readdir",
+ "readdir", ctypes.default_abi,
+ /*return*/Type.null_or_dirent_ptr,
+ /*dir*/ Type.DIR.in_ptr); // Other Unices
+ }
+
+ libc.declareLazyFFI(SysFile, "rename",
+ "rename", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*old*/ Type.path,
+ /*new*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "rmdir",
+ "rmdir", ctypes.default_abi,
+ /*return*/ Type.int,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "splice",
+ "splice", ctypes.default_abi,
+ /*return*/ Type.long,
+ /*fd_in*/ Type.fd,
+ /*off_in*/ Type.off_t.in_ptr,
+ /*fd_out*/ Type.fd,
+ /*off_out*/Type.off_t.in_ptr,
+ /*len*/ Type.size_t,
+ /*flags*/ Type.unsigned_int); // Linux/Android-specific
+
+ libc.declareLazyFFI(SysFile, "statfs",
+ "statfs", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.statvfs.out_ptr); // Android,B2G
+
+ libc.declareLazyFFI(SysFile, "statvfs",
+ "statvfs", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.statvfs.out_ptr); // Other platforms
+
+ libc.declareLazyFFI(SysFile, "symlink",
+ "symlink", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*source*/ Type.path,
+ /*dest*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "truncate",
+ "truncate", ctypes.default_abi,
+ /*return*/Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*length*/ Type.off_t);
+
+ libc.declareLazyFFI(SysFile, "unlink",
+ "unlink", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "write",
+ "write", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.in_ptr,
+ /*nbytes*/ Type.size_t);
+
+ // Weird cases that require special treatment
+
+ // OSes use a variety of hacks to differentiate between
+ // 32-bits and 64-bits versions of |stat|, |lstat|, |fstat|.
+ if (Const._DARWIN_FEATURE_64_BIT_INODE) {
+ // MacOS X 64-bits
+ libc.declareLazyFFI(SysFile, "stat",
+ "stat$INODE64", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "lstat",
+ "lstat$INODE64", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "fstat",
+ "fstat$INODE64", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr
+ );
+ } else if (Const._STAT_VER != undefined) {
+ const ver = Const._STAT_VER;
+ let xstat_name, lxstat_name, fxstat_name;
+ if (OS.Constants.Sys.Name == "SunOS") {
+ // Solaris
+ xstat_name = "_xstat";
+ lxstat_name = "_lxstat";
+ fxstat_name = "_fxstat";
+ } else {
+ // Linux, all widths
+ xstat_name = "__xstat";
+ lxstat_name = "__lxstat";
+ fxstat_name = "__fxstat";
+ }
+
+ let Stat = {};
+ libc.declareLazyFFI(Stat, "xstat",
+ xstat_name, ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*_stat_ver*/ Type.int,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr);
+ libc.declareLazyFFI(Stat, "lxstat",
+ lxstat_name, ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*_stat_ver*/ Type.int,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr);
+ libc.declareLazyFFI(Stat, "fxstat",
+ fxstat_name, ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*_stat_ver*/ Type.int,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr);
+
+
+ SysFile.stat = function stat(path, buf) {
+ return Stat.xstat(ver, path, buf);
+ };
+
+ SysFile.lstat = function lstat(path, buf) {
+ return Stat.lxstat(ver, path, buf);
+ };
+
+ SysFile.fstat = function fstat(fd, buf) {
+ return Stat.fxstat(ver, fd, buf);
+ };
+ } else if (OS.Constants.Sys.Name == "NetBSD") {
+ // NetBSD 5.0 and newer
+ libc.declareLazyFFI(SysFile, "stat",
+ "__stat50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "lstat",
+ "__lstat50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "fstat",
+ "__fstat50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr
+ );
+ } else {
+ // Mac OS X 32-bits, other Unix
+ libc.declareLazyFFI(SysFile, "stat",
+ "stat", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "lstat",
+ "lstat", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "fstat",
+ "fstat", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr
+ );
+ }
+
+ // We cannot make a C array of CDataFinalizer, so
+ // pipe cannot be directly defined as a C function.
+
+ let Pipe = {};
+ libc.declareLazyFFI(Pipe, "_pipe",
+ "pipe", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fds*/ new SharedAll.Type("two file descriptors",
+ ctypes.ArrayType(ctypes.int, 2)));
+
+ // A shared per-thread buffer used to communicate with |pipe|
+ let _pipebuf = new (ctypes.ArrayType(ctypes.int, 2))();
+
+ SysFile.pipe = function pipe(array) {
+ let result = Pipe._pipe(_pipebuf);
+ if (result == -1) {
+ return result;
+ }
+ array[0] = ctypes.CDataFinalizer(_pipebuf[0], SysFile._close);
+ array[1] = ctypes.CDataFinalizer(_pipebuf[1], SysFile._close);
+ return result;
+ };
+
+ if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "utimes",
+ "__utimes50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ } else {
+ libc.declareLazyFFI(SysFile, "utimes",
+ "utimes", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ }
+ if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "futimes",
+ "__futimes50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ } else {
+ libc.declareLazyFFI(SysFile, "futimes",
+ "futimes", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ }
+ };
+
+ exports.OS.Unix = {
+ File: {
+ _init: init
+ }
+ };
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/osfile_unix_front.jsm b/toolkit/components/osfile/modules/osfile_unix_front.jsm
new file mode 100644
index 0000000000..19a27ae1aa
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm
@@ -0,0 +1,1193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Synchronous front-end for the JavaScript OS.File library.
+ * Unix implementation.
+ *
+ * This front-end is meant to be imported by a worker thread.
+ */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_unix_front.jsm to be used directly as a main thread
+ // module yet.
+
+ throw new Error("osfile_unix_front.jsm cannot be used from the main thread yet");
+ }
+ (function(exports) {
+ "use strict";
+
+ // exports.OS.Unix is created by osfile_unix_back.jsm
+ if (exports.OS && exports.OS.File) {
+ return; // Avoid double-initialization
+ }
+
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let Path = require("resource://gre/modules/osfile/ospath.jsm");
+ let SysAll = require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm");
+ exports.OS.Unix.File._init();
+ let LOG = SharedAll.LOG.bind(SharedAll, "Unix front-end");
+ let Const = SharedAll.Constants.libc;
+ let UnixFile = exports.OS.Unix.File;
+ let Type = UnixFile.Type;
+
+ /**
+ * Representation of a file.
+ *
+ * You generally do not need to call this constructor yourself. Rather,
+ * to open a file, use function |OS.File.open|.
+ *
+ * @param fd A OS-specific file descriptor.
+ * @param {string} path File path of the file handle, used for error-reporting.
+ * @constructor
+ */
+ let File = function File(fd, path) {
+ exports.OS.Shared.AbstractFile.call(this, fd, path);
+ this._closeResult = null;
+ };
+ File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype);
+
+ /**
+ * Close the file.
+ *
+ * This method has no effect if the file is already closed. However,
+ * if the first call to |close| has thrown an error, further calls
+ * will throw the same error.
+ *
+ * @throws File.Error If closing the file revealed an error that could
+ * not be reported earlier.
+ */
+ File.prototype.close = function close() {
+ if (this._fd) {
+ let fd = this._fd;
+ this._fd = null;
+ // Call |close(fd)|, detach finalizer if any
+ // (|fd| may not be a CDataFinalizer if it has been
+ // instantiated from a controller thread).
+ let result = UnixFile._close(fd);
+ if (typeof fd == "object" && "forget" in fd) {
+ fd.forget();
+ }
+ if (result == -1) {
+ this._closeResult = new File.Error("close", ctypes.errno, this._path);
+ }
+ }
+ if (this._closeResult) {
+ throw this._closeResult;
+ }
+ return;
+ };
+
+ /**
+ * Read some bytes from a file.
+ *
+ * @param {C pointer} buffer A buffer for holding the data
+ * once it is read.
+ * @param {number} nbytes The number of bytes to read. It must not
+ * exceed the size of |buffer| in bytes but it may exceed the number
+ * of bytes unread in the file.
+ * @param {*=} options Additional options for reading. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively read. If zero,
+ * the end of the file has been reached.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._read = function _read(buffer, nbytes, options = {}) {
+ // Populate the page cache with data from a file so the subsequent reads
+ // from that file will not block on disk I/O.
+ if (typeof(UnixFile.posix_fadvise) === 'function' &&
+ (options.sequential || !("sequential" in options))) {
+ UnixFile.posix_fadvise(this.fd, 0, nbytes,
+ OS.Constants.libc.POSIX_FADV_SEQUENTIAL);
+ }
+ return throw_on_negative("read",
+ UnixFile.read(this.fd, buffer, nbytes),
+ this._path
+ );
+ };
+
+ /**
+ * Write some bytes to a file.
+ *
+ * @param {Typed array} buffer A buffer holding the data that must be
+ * written.
+ * @param {number} nbytes The number of bytes to write. It must not
+ * exceed the size of |buffer| in bytes.
+ * @param {*=} options Additional options for writing. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively written.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._write = function _write(buffer, nbytes, options = {}) {
+ return throw_on_negative("write",
+ UnixFile.write(this.fd, buffer, nbytes),
+ this._path
+ );
+ };
+
+ /**
+ * Return the current position in the file.
+ */
+ File.prototype.getPosition = function getPosition(pos) {
+ return this.setPosition(0, File.POS_CURRENT);
+ };
+
+ /**
+ * Change the current position in the file.
+ *
+ * @param {number} pos The new position. Whether this position
+ * is considered from the current position, from the start of
+ * the file or from the end of the file is determined by
+ * argument |whence|. Note that |pos| may exceed the length of
+ * the file.
+ * @param {number=} whence The reference position. If omitted
+ * or |OS.File.POS_START|, |pos| is relative to the start of the
+ * file. If |OS.File.POS_CURRENT|, |pos| is relative to the
+ * current position in the file. If |OS.File.POS_END|, |pos| is
+ * relative to the end of the file.
+ *
+ * @return The new position in the file.
+ */
+ File.prototype.setPosition = function setPosition(pos, whence) {
+ if (whence === undefined) {
+ whence = Const.SEEK_SET;
+ }
+ return throw_on_negative("setPosition",
+ UnixFile.lseek(this.fd, pos, whence),
+ this._path
+ );
+ };
+
+ /**
+ * Fetch the information on the file.
+ *
+ * @return File.Info The information on |this| file.
+ */
+ File.prototype.stat = function stat() {
+ throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr),
+ this._path);
+ return new File.Info(gStatData, this._path);
+ };
+
+ /**
+ * Set the file's access permissions.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+ File.prototype.setPermissions = function setPermissions(options = {}) {
+ throw_on_negative("setPermissions",
+ UnixFile.fchmod(this.fd, unixMode(options)),
+ this._path);
+ };
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * WARNING: This method is not implemented on Android/B2G. On Android/B2G,
+ * you should use File.setDates instead.
+ *
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid parameters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ if (SharedAll.Constants.Sys.Name != "Android") {
+ File.prototype.setDates = function(accessDate, modificationDate) {
+ let {value, ptr} = datesToTimevals(accessDate, modificationDate);
+ throw_on_negative("setDates",
+ UnixFile.futimes(this.fd, ptr),
+ this._path);
+ };
+ }
+
+ /**
+ * Flushes the file's buffers and causes all buffered data
+ * to be written.
+ * Disk flushes are very expensive and therefore should be used carefully,
+ * sparingly and only in scenarios where it is vital that data survives
+ * system crashes. Even though the function will be executed off the
+ * main-thread, it might still affect the overall performance of any
+ * running application.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype.flush = function flush() {
+ throw_on_negative("flush", UnixFile.fsync(this.fd), this._path);
+ };
+
+ // The default unix mode for opening (0600)
+ const DEFAULT_UNIX_MODE = 384;
+
+ /**
+ * Open a file
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} mode The opening mode for the file, as
+ * an object that may contain the following fields:
+ *
+ * - {bool} truncate If |true|, the file will be opened
+ * for writing. If the file does not exist, it will be
+ * created. If the file exists, its contents will be
+ * erased. Cannot be specified with |create|.
+ * - {bool} create If |true|, the file will be opened
+ * for writing. If the file exists, this function fails.
+ * If the file does not exist, it will be created. Cannot
+ * be specified with |truncate| or |existing|.
+ * - {bool} existing. If the file does not exist, this function
+ * fails. Cannot be specified with |create|.
+ * - {bool} read If |true|, the file will be opened for
+ * reading. The file may also be opened for writing, depending
+ * on the other fields of |mode|.
+ * - {bool} write If |true|, the file will be opened for
+ * writing. The file may also be opened for reading, depending
+ * on the other fields of |mode|.
+ * - {bool} append If |true|, the file will be opened for appending,
+ * meaning the equivalent of |.setPosition(0, POS_END)| is executed
+ * before each write. The default is |true|, i.e. opening a file for
+ * appending. Specify |append: false| to open the file in regular mode.
+ *
+ * If neither |truncate|, |create| or |write| is specified, the file
+ * is opened for reading.
+ *
+ * Note that |false|, |null| or |undefined| flags are simply ignored.
+ *
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} unixFlags If specified, file opening flags, as
+ * per libc function |open|. Replaces |mode|.
+ * - {number} unixMode If specified, a file creation mode,
+ * as per libc function |open|. If unspecified, files are
+ * created with a default mode of 0600 (file is private to the
+ * user, the user can read and write).
+ *
+ * @return {File} A file object.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+ File.open = function Unix_open(path, mode, options = {}) {
+ // We don't need to filter for the umask because "open" does this for us.
+ let omode = options.unixMode !== undefined ?
+ options.unixMode : DEFAULT_UNIX_MODE;
+ let flags;
+ if (options.unixFlags !== undefined) {
+ flags = options.unixFlags;
+ } else {
+ mode = OS.Shared.AbstractFile.normalizeOpenMode(mode);
+ // Handle read/write
+ if (!mode.write) {
+ flags = Const.O_RDONLY;
+ } else if (mode.read) {
+ flags = Const.O_RDWR;
+ } else {
+ flags = Const.O_WRONLY;
+ }
+ // Finally, handle create/existing/trunc
+ if (mode.trunc) {
+ if (mode.existing) {
+ flags |= Const.O_TRUNC;
+ } else {
+ flags |= Const.O_CREAT | Const.O_TRUNC;
+ }
+ } else if (mode.create) {
+ flags |= Const.O_CREAT | Const.O_EXCL;
+ } else if (mode.read && !mode.write) {
+ // flags are sufficient
+ } else if (!mode.existing) {
+ flags |= Const.O_CREAT;
+ }
+ if (mode.append) {
+ flags |= Const.O_APPEND;
+ }
+ }
+ return error_or_file(UnixFile.open(path, flags, omode), path);
+ };
+
+ /**
+ * Checks if a file exists
+ *
+ * @param {string} path The path to the file.
+ *
+ * @return {bool} true if the file exists, false otherwise.
+ */
+ File.exists = function Unix_exists(path) {
+ if (UnixFile.access(path, Const.F_OK) == -1) {
+ return false;
+ } else {
+ return true;
+ }
+ };
+
+ /**
+ * Remove an existing file.
+ *
+ * @param {string} path The name of the file.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the file does
+ * not exist. |true| by default.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.remove = function remove(path, options = {}) {
+ let result = UnixFile.unlink(path);
+ if (result == -1) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.errno == Const.ENOENT) {
+ return;
+ }
+ throw new File.Error("remove", ctypes.errno, path);
+ }
+ };
+
+ /**
+ * Remove an empty directory.
+ *
+ * @param {string} path The name of the directory to remove.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory
+ * does not exist. |true| by default
+ */
+ File.removeEmptyDir = function removeEmptyDir(path, options = {}) {
+ let result = UnixFile.rmdir(path);
+ if (result == -1) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.errno == Const.ENOENT) {
+ return;
+ }
+ throw new File.Error("removeEmptyDir", ctypes.errno, path);
+ }
+ };
+
+ /**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} sourcePath Platform-specific path to a directory on
+ * the disk to query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+ File.getAvailableFreeSpace = function Unix_getAvailableFreeSpace(sourcePath) {
+ let fileSystemInfo = new Type.statvfs.implementation();
+ let fileSystemInfoPtr = fileSystemInfo.address();
+
+ throw_on_negative("statvfs", (UnixFile.statvfs || UnixFile.statfs)(sourcePath, fileSystemInfoPtr));
+
+ let bytes = new Type.uint64_t.implementation(
+ fileSystemInfo.f_bsize * fileSystemInfo.f_bavail);
+
+ return bytes.value;
+ };
+
+ /**
+ * Default mode for opening directories.
+ */
+ const DEFAULT_UNIX_MODE_DIR = Const.S_IRWXU;
+
+ /**
+ * Create a directory.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options. This
+ * implementation interprets the following fields:
+ *
+ * - {number} unixMode If specified, a file creation mode,
+ * as per libc function |mkdir|. If unspecified, dirs are
+ * created with a default mode of 0700 (dir is private to
+ * the user, the user can read, write and execute).
+ * - {bool} ignoreExisting If |false|, throw error if the directory
+ * already exists. |true| by default
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |from|
+ * and its existing descendants must be user-writeable and that |path|
+ * must be a descendant of |from|.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ */
+ File._makeDir = function makeDir(path, options = {}) {
+ let omode = options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE_DIR;
+ let result = UnixFile.mkdir(path, omode);
+ if (result == -1) {
+ if ((!("ignoreExisting" in options) || options.ignoreExisting) &&
+ (ctypes.errno == Const.EEXIST || ctypes.errno == Const.EISDIR)) {
+ return;
+ }
+ throw new File.Error("makeDir", ctypes.errno, path);
+ }
+ };
+
+ /**
+ * Copy a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be copied.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be copied with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.copy = null;
+
+ /**
+ * Move a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be moved.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ * @option {bool} noCopy - If set, this function will fail if the
+ * operation is more sophisticated than a simple renaming, i.e. if
+ * |sourcePath| and |destPath| are not situated on the same device.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be moved with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.move = null;
+
+ if (UnixFile.copyfile) {
+ // This implementation uses |copyfile(3)|, from the BSD library.
+ // Adding copying of hierarchies and/or attributes is just a flag
+ // away.
+ File.copy = function copyfile(sourcePath, destPath, options = {}) {
+ let flags = Const.COPYFILE_DATA;
+ if (options.noOverwrite) {
+ flags |= Const.COPYFILE_EXCL;
+ }
+ throw_on_negative("copy",
+ UnixFile.copyfile(sourcePath, destPath, null, flags),
+ sourcePath
+ );
+ };
+ } else {
+ // If the OS does not implement file copying for us, we need to
+ // implement it ourselves. For this purpose, we need to define
+ // a pumping function.
+
+ /**
+ * Copy bytes from one file to another one.
+ *
+ * @param {File} source The file containing the data to be copied. It
+ * should be opened for reading.
+ * @param {File} dest The file to which the data should be written. It
+ * should be opened for writing.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {number} nbytes The maximal number of bytes to
+ * copy. If unspecified, copy everything from the current
+ * position.
+ * @option {number} bufSize A hint regarding the size of the
+ * buffer to use for copying. The implementation may decide to
+ * ignore this hint.
+ * @option {bool} unixUserland Will force the copy operation to be
+ * caried out in user land, instead of using optimized syscalls such
+ * as splice(2).
+ *
+ * @throws {OS.File.Error} In case of error.
+ */
+ let pump;
+
+ // A buffer used by |pump_userland|
+ let pump_buffer = null;
+
+ // An implementation of |pump| using |read|/|write|
+ let pump_userland = function pump_userland(source, dest, options = {}) {
+ let bufSize = options.bufSize > 0 ? options.bufSize : 4096;
+ let nbytes = options.nbytes > 0 ? options.nbytes : Infinity;
+ if (!pump_buffer || pump_buffer.length < bufSize) {
+ pump_buffer = new (ctypes.ArrayType(ctypes.char))(bufSize);
+ }
+ let read = source._read.bind(source);
+ let write = dest._write.bind(dest);
+ // Perform actual copy
+ let total_read = 0;
+ while (true) {
+ let chunk_size = Math.min(nbytes, bufSize);
+ let bytes_just_read = read(pump_buffer, bufSize);
+ if (bytes_just_read == 0) {
+ return total_read;
+ }
+ total_read += bytes_just_read;
+ let bytes_written = 0;
+ do {
+ bytes_written += write(
+ pump_buffer.addressOfElement(bytes_written),
+ bytes_just_read - bytes_written
+ );
+ } while (bytes_written < bytes_just_read);
+ nbytes -= bytes_written;
+ if (nbytes <= 0) {
+ return total_read;
+ }
+ }
+ };
+
+ // Fortunately, under Linux, that pumping function can be optimized.
+ if (UnixFile.splice) {
+ const BUFSIZE = 1 << 17;
+
+ // An implementation of |pump| using |splice| (for Linux/Android)
+ pump = function pump_splice(source, dest, options = {}) {
+ let nbytes = options.nbytes > 0 ? options.nbytes : Infinity;
+ let pipe = [];
+ throw_on_negative("pump", UnixFile.pipe(pipe));
+ let pipe_read = pipe[0];
+ let pipe_write = pipe[1];
+ let source_fd = source.fd;
+ let dest_fd = dest.fd;
+ let total_read = 0;
+ let total_written = 0;
+ try {
+ while (true) {
+ let chunk_size = Math.min(nbytes, BUFSIZE);
+ let bytes_read = throw_on_negative("pump",
+ UnixFile.splice(source_fd, null,
+ pipe_write, null, chunk_size, 0)
+ );
+ if (!bytes_read) {
+ break;
+ }
+ total_read += bytes_read;
+ let bytes_written = throw_on_negative(
+ "pump",
+ UnixFile.splice(pipe_read, null,
+ dest_fd, null, bytes_read,
+ (bytes_read == chunk_size)?Const.SPLICE_F_MORE:0
+ ));
+ if (!bytes_written) {
+ // This should never happen
+ throw new Error("Internal error: pipe disconnected");
+ }
+ total_written += bytes_written;
+ nbytes -= bytes_read;
+ if (!nbytes) {
+ break;
+ }
+ }
+ return total_written;
+ } catch (x) {
+ if (x.unixErrno == Const.EINVAL) {
+ // We *might* be on a file system that does not support splice.
+ // Try again with a fallback pump.
+ if (total_read) {
+ source.setPosition(-total_read, File.POS_CURRENT);
+ }
+ if (total_written) {
+ dest.setPosition(-total_written, File.POS_CURRENT);
+ }
+ return pump_userland(source, dest, options);
+ }
+ throw x;
+ } finally {
+ pipe_read.dispose();
+ pipe_write.dispose();
+ }
+ };
+ } else {
+ // Fallback implementation of pump for other Unix platforms.
+ pump = pump_userland;
+ }
+
+ // Implement |copy| using |pump|.
+ // This implementation would require some work before being able to
+ // copy directories
+ File.copy = function copy(sourcePath, destPath, options = {}) {
+ let source, dest;
+ let result;
+ try {
+ source = File.open(sourcePath);
+ // Need to open the output file with |append:false|, or else |splice|
+ // won't work.
+ if (options.noOverwrite) {
+ dest = File.open(destPath, {create:true, append:false});
+ } else {
+ dest = File.open(destPath, {trunc:true, append:false});
+ }
+ if (options.unixUserland) {
+ result = pump_userland(source, dest, options);
+ } else {
+ result = pump(source, dest, options);
+ }
+ } catch (x) {
+ if (dest) {
+ dest.close();
+ }
+ if (source) {
+ source.close();
+ }
+ throw x;
+ }
+ };
+ } // End of definition of copy
+
+ // Implement |move| using |rename| (wherever possible) or |copy|
+ // (if files are on distinct devices).
+ File.move = function move(sourcePath, destPath, options = {}) {
+ // An implementation using |rename| whenever possible or
+ // |File.pump| when required, for other Unices.
+ // It can move directories on one file system, not
+ // across file systems
+
+ // If necessary, fail if the destination file exists
+ if (options.noOverwrite) {
+ let fd = UnixFile.open(destPath, Const.O_RDONLY, 0);
+ if (fd != -1) {
+ fd.dispose();
+ // The file exists and we have access
+ throw new File.Error("move", Const.EEXIST, sourcePath);
+ } else if (ctypes.errno == Const.EACCESS) {
+ // The file exists and we don't have access
+ throw new File.Error("move", Const.EEXIST, sourcePath);
+ }
+ }
+
+ // If we can, rename the file
+ let result = UnixFile.rename(sourcePath, destPath);
+ if (result != -1)
+ return;
+
+ // If the error is not EXDEV ("not on the same device"),
+ // or if the error is EXDEV and we have passed an option
+ // that prevents us from crossing devices, throw the
+ // error.
+ if (ctypes.errno != Const.EXDEV || options.noCopy) {
+ throw new File.Error("move", ctypes.errno, sourcePath);
+ }
+
+ // Otherwise, copy and remove.
+ File.copy(sourcePath, destPath, options);
+ // FIXME: Clean-up in case of copy error?
+ File.remove(sourcePath);
+ };
+
+ File.unixSymLink = function unixSymLink(sourcePath, destPath) {
+ throw_on_negative("symlink", UnixFile.symlink(sourcePath, destPath),
+ sourcePath);
+ };
+
+ /**
+ * Iterate on one directory.
+ *
+ * This iterator will not enter subdirectories.
+ *
+ * @param {string} path The directory upon which to iterate.
+ * @param {*=} options Ignored in this implementation.
+ *
+ * @throws {File.Error} If |path| does not represent a directory or
+ * if the directory cannot be iterated.
+ * @constructor
+ */
+ File.DirectoryIterator = function DirectoryIterator(path, options) {
+ exports.OS.Shared.AbstractFile.AbstractIterator.call(this);
+ this._path = path;
+ this._dir = UnixFile.opendir(this._path);
+ if (this._dir == null) {
+ let error = ctypes.errno;
+ if (error != Const.ENOENT) {
+ throw new File.Error("DirectoryIterator", error, path);
+ }
+ this._exists = false;
+ this._closed = true;
+ } else {
+ this._exists = true;
+ this._closed = false;
+ }
+ };
+ File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype);
+
+ /**
+ * Return the next entry in the directory, if any such entry is
+ * available.
+ *
+ * Skip special directories "." and "..".
+ *
+ * @return {File.Entry} The next entry in the directory.
+ * @throws {StopIteration} Once all files in the directory have been
+ * encountered.
+ */
+ File.DirectoryIterator.prototype.next = function next() {
+ if (!this._exists) {
+ throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
+ }
+ if (this._closed) {
+ throw StopIteration;
+ }
+ for (let entry = UnixFile.readdir(this._dir);
+ entry != null && !entry.isNull();
+ entry = UnixFile.readdir(this._dir)) {
+ let contents = entry.contents;
+ let name = contents.d_name.readString();
+ if (name == "." || name == "..") {
+ continue;
+ }
+
+ let isDir, isSymLink;
+ if (!("d_type" in contents)) {
+ // |dirent| doesn't have d_type on some platforms (e.g. Solaris).
+ let path = Path.join(this._path, name);
+ throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr), this._path);
+ isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR;
+ isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK;
+ } else {
+ isDir = contents.d_type == Const.DT_DIR;
+ isSymLink = contents.d_type == Const.DT_LNK;
+ }
+
+ return new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path);
+ }
+ this.close();
+ throw StopIteration;
+ };
+
+ /**
+ * Close the iterator and recover all resources.
+ * You should call this once you have finished iterating on a directory.
+ */
+ File.DirectoryIterator.prototype.close = function close() {
+ if (this._closed) return;
+ this._closed = true;
+ UnixFile.closedir(this._dir);
+ this._dir = null;
+ };
+
+ /**
+ * Determine whether the directory exists.
+ *
+ * @return {boolean}
+ */
+ File.DirectoryIterator.prototype.exists = function exists() {
+ return this._exists;
+ };
+
+ /**
+ * Return directory as |File|
+ */
+ File.DirectoryIterator.prototype.unixAsFile = function unixAsFile() {
+ if (!this._dir) throw File.Error.closed("unixAsFile", this._path);
+ return error_or_file(UnixFile.dirfd(this._dir), this._path);
+ };
+
+ /**
+ * An entry in a directory.
+ */
+ File.DirectoryIterator.Entry = function Entry(isDir, isSymLink, name, parent) {
+ // Copy the relevant part of |unix_entry| to ensure that
+ // our data is not overwritten prematurely.
+ this._parent = parent;
+ let path = Path.join(this._parent, name);
+
+ SysAll.AbstractEntry.call(this, isDir, isSymLink, name, path);
+ };
+ File.DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
+
+ /**
+ * Return a version of an instance of
+ * File.DirectoryIterator.Entry that can be sent from a worker
+ * thread to the main thread. Note that deserialization is
+ * asymmetric and returns an object with a different
+ * implementation.
+ */
+ File.DirectoryIterator.Entry.toMsg = function toMsg(value) {
+ if (!value instanceof File.DirectoryIterator.Entry) {
+ throw new TypeError("parameter of " +
+ "File.DirectoryIterator.Entry.toMsg must be a " +
+ "File.DirectoryIterator.Entry");
+ }
+ let serialized = {};
+ for (let key in File.DirectoryIterator.Entry.prototype) {
+ serialized[key] = value[key];
+ }
+ return serialized;
+ };
+
+ let gStatData = new Type.stat.implementation();
+ let gStatDataPtr = gStatData.address();
+
+ let MODE_MASK = 4095 /*= 07777*/;
+ File.Info = function Info(stat, path) {
+ let isDir = (stat.st_mode & Const.S_IFMT) == Const.S_IFDIR;
+ let isSymLink = (stat.st_mode & Const.S_IFMT) == Const.S_IFLNK;
+ let size = Type.off_t.importFromC(stat.st_size);
+
+ let lastAccessDate = new Date(stat.st_atime * 1000);
+ let lastModificationDate = new Date(stat.st_mtime * 1000);
+ let unixLastStatusChangeDate = new Date(stat.st_ctime * 1000);
+
+ let unixOwner = Type.uid_t.importFromC(stat.st_uid);
+ let unixGroup = Type.gid_t.importFromC(stat.st_gid);
+ let unixMode = Type.mode_t.importFromC(stat.st_mode & MODE_MASK);
+
+ SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size,
+ lastAccessDate, lastModificationDate, unixLastStatusChangeDate,
+ unixOwner, unixGroup, unixMode);
+
+ // Some platforms (e.g. MacOS X, some BSDs) store a file creation date
+ if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) {
+ let date = new Date(stat.st_birthtime * 1000);
+
+ /**
+ * The date of creation of this file.
+ *
+ * Note that the date returned by this method is not always
+ * reliable. Not all file systems are able to provide this
+ * information.
+ *
+ * @type {Date}
+ */
+ this.macBirthDate = date;
+ }
+ };
+ File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype);
+
+ // Deprecated, use macBirthDate/winBirthDate instead
+ Object.defineProperty(File.Info.prototype, "creationDate", {
+ get: function creationDate() {
+ // On the Macintosh, returns the birth date if available.
+ // On other Unix, as the birth date is not available,
+ // returns the epoch.
+ return this.macBirthDate || new Date(0);
+ }
+ });
+
+ /**
+ * Return a version of an instance of File.Info that can be sent
+ * from a worker thread to the main thread. Note that deserialization
+ * is asymmetric and returns an object with a different implementation.
+ */
+ File.Info.toMsg = function toMsg(stat) {
+ if (!stat instanceof File.Info) {
+ throw new TypeError("parameter of File.Info.toMsg must be a File.Info");
+ }
+ let serialized = {};
+ for (let key in File.Info.prototype) {
+ serialized[key] = stat[key];
+ }
+ return serialized;
+ };
+
+ /**
+ * Fetch the information on a file.
+ *
+ * @param {string} path The full name of the file to open.
+ * @param {*=} options Additional options. In this implementation:
+ *
+ * - {bool} unixNoFollowingLinks If set and |true|, if |path|
+ * represents a symbolic link, the call will return the information
+ * of the link itself, rather than that of the target file.
+ *
+ * @return {File.Information}
+ */
+ File.stat = function stat(path, options = {}) {
+ if (options.unixNoFollowingLinks) {
+ throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr), path);
+ } else {
+ throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr), path);
+ }
+ return new File.Info(gStatData, path);
+ };
+
+ /**
+ * Set the file's access permissions.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {string} path The name of the file to reset the permissions of.
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+ File.setPermissions = function setPermissions(path, options = {}) {
+ throw_on_negative("setPermissions",
+ UnixFile.chmod(path, unixMode(options)),
+ path);
+ };
+
+ /**
+ * Convert an access date and a modification date to an array
+ * of two |timeval|.
+ */
+ function datesToTimevals(accessDate, modificationDate) {
+ accessDate = normalizeDate("File.setDates", accessDate);
+ modificationDate = normalizeDate("File.setDates", modificationDate);
+
+ let timevals = new Type.timevals.implementation();
+ let timevalsPtr = timevals.address();
+
+ timevals[0].tv_sec = (accessDate / 1000) | 0;
+ timevals[0].tv_usec = 0;
+ timevals[1].tv_sec = (modificationDate / 1000) | 0;
+ timevals[1].tv_usec = 0;
+
+ return { value: timevals, ptr: timevalsPtr };
+ }
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * @param {string} path The full name of the file to set the dates for.
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid paramters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.setDates = function setDates(path, accessDate, modificationDate) {
+ let {value, ptr} = datesToTimevals(accessDate, modificationDate);
+ throw_on_negative("setDates",
+ UnixFile.utimes(path, ptr),
+ path);
+ };
+
+ File.read = exports.OS.Shared.AbstractFile.read;
+ File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
+ File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
+ File.makeDir = exports.OS.Shared.AbstractFile.makeDir;
+
+ /**
+ * Remove an existing directory and its contents.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't
+ * exist. |true| by default.
+ * - {boolean} ignorePermissions If |true|, remove the file even when lacking write
+ * permission.
+ *
+ * @throws {OS.File.Error} In case of I/O error, in particular if |path| is
+ * not a directory.
+ *
+ * Note: This function will remove a symlink even if it points a directory.
+ */
+ File.removeDir = function(path, options = {}) {
+ let isSymLink;
+ try {
+ let info = File.stat(path, {unixNoFollowingLinks: true});
+ isSymLink = info.isSymLink;
+ } catch (e) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.errno == Const.ENOENT) {
+ return;
+ }
+ throw e;
+ }
+ if (isSymLink) {
+ // A Unix symlink itself is not a directory even if it points
+ // a directory.
+ File.remove(path, options);
+ return;
+ }
+ exports.OS.Shared.AbstractFile.removeRecursive(path, options);
+ };
+
+ /**
+ * Get the current directory by getCurrentDirectory.
+ */
+ File.getCurrentDirectory = function getCurrentDirectory() {
+ let path, buf;
+ if (UnixFile.get_current_dir_name) {
+ path = UnixFile.get_current_dir_name();
+ } else if (UnixFile.getwd_auto) {
+ path = UnixFile.getwd_auto(null);
+ } else {
+ for (let length = Const.PATH_MAX; !path; length *= 2) {
+ buf = new (ctypes.char.array(length));
+ path = UnixFile.getcwd(buf, length);
+ };
+ }
+ throw_on_null("getCurrentDirectory", path);
+ return path.readString();
+ };
+
+ /**
+ * Set the current directory by setCurrentDirectory.
+ */
+ File.setCurrentDirectory = function setCurrentDirectory(path) {
+ throw_on_negative("setCurrentDirectory",
+ UnixFile.chdir(path),
+ path
+ );
+ };
+
+ /**
+ * Get/set the current directory.
+ */
+ Object.defineProperty(File, "curDir", {
+ set: function(path) {
+ this.setCurrentDirectory(path);
+ },
+ get: function() {
+ return this.getCurrentDirectory();
+ }
+ }
+ );
+
+ // Utility functions
+
+ /**
+ * Turn the result of |open| into an Error or a File
+ * @param {number} maybe The result of the |open| operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.errno, otherwise it returns the opened file.
+ * @param {string=} path The path of the file.
+ */
+ function error_or_file(maybe, path) {
+ if (maybe == -1) {
+ throw new File.Error("open", ctypes.errno, path);
+ }
+ return new File(maybe, path);
+ }
+
+ /**
+ * Utility function to sort errors represented as "-1" from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {number} result The result of the operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.errno, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_negative(operation, result, path) {
+ if (result < 0) {
+ throw new File.Error(operation, ctypes.errno, path);
+ }
+ return result;
+ }
+
+ /**
+ * Utility function to sort errors represented as |null| from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {pointer} result The result of the operation that may
+ * represent either an error or a success. If |null|, this function raises
+ * an error holding ctypes.errno, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_null(operation, result, path) {
+ if (result == null || (result.isNull && result.isNull())) {
+ throw new File.Error(operation, ctypes.errno, path);
+ }
+ return result;
+ }
+
+ /**
+ * Normalize and verify a Date or numeric date value.
+ *
+ * @param {string} fn Function name of the calling function.
+ * @param {Date,number} date The date to normalize. If omitted or null,
+ * then the current date will be used.
+ *
+ * @throws {TypeError} Invalid date provided.
+ *
+ * @return {number} Sanitized, numeric date in milliseconds since epoch.
+ */
+ function normalizeDate(fn, date) {
+ if (typeof date !== "number" && !date) {
+ // |date| was Omitted or null.
+ date = Date.now();
+ } else if (typeof date.getTime === "function") {
+ // Input might be a date or date-like object.
+ date = date.getTime();
+ }
+
+ if (typeof date !== "number" || Number.isNaN(date)) {
+ throw new TypeError("|date| parameter of " + fn + " must be a " +
+ "|Date| instance or number");
+ }
+ return date;
+ };
+
+ /**
+ * Helper used by both versions of setPermissions.
+ */
+ function unixMode(options) {
+ let mode = options.unixMode !== undefined ?
+ options.unixMode : DEFAULT_UNIX_MODE;
+ let unixHonorUmask = true;
+ if ("unixHonorUmask" in options) {
+ unixHonorUmask = options.unixHonorUmask;
+ }
+ if (unixHonorUmask) {
+ mode &= ~SharedAll.Constants.Sys.umask;
+ }
+ return mode;
+ }
+
+ File.Unix = exports.OS.Unix.File;
+ File.Error = SysAll.Error;
+ exports.OS.File = File;
+ exports.OS.Shared.Type = Type;
+
+ Object.defineProperty(File, "POS_START", { value: SysAll.POS_START });
+ Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT });
+ Object.defineProperty(File, "POS_END", { value: SysAll.POS_END });
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/osfile_win_allthreads.jsm b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
new file mode 100644
index 0000000000..b059d4e127
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
@@ -0,0 +1,425 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 module defines the thread-agnostic components of the Win version
+ * of OS.File. It depends on the thread-agnostic cross-platform components
+ * of OS.File.
+ *
+ * It serves the following purposes:
+ * - open kernel32;
+ * - define OS.Shared.Win.Error;
+ * - define a few constants and types that need to be defined on all platforms.
+ *
+ * This module can be:
+ * - opened from the main thread as a jsm module;
+ * - opened from a chrome worker through require().
+ */
+
+"use strict";
+
+var SharedAll;
+if (typeof Components != "undefined") {
+ let Cu = Components.utils;
+ // Module is opened as a jsm module
+ Cu.import("resource://gre/modules/ctypes.jsm", this);
+
+ SharedAll = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+ this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+ // Module is loaded with require()
+ SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+} else {
+ throw new Error("Please open this module with Component.utils.import or with require()");
+}
+
+var LOG = SharedAll.LOG.bind(SharedAll, "Win", "allthreads");
+var Const = SharedAll.Constants.Win;
+
+// Open libc
+var libc = new SharedAll.Library("libc", "kernel32.dll");
+exports.libc = libc;
+
+// Define declareFFI
+var declareFFI = SharedAll.declareFFI.bind(null, libc);
+exports.declareFFI = declareFFI;
+
+var Scope = {};
+
+// Define Error
+libc.declareLazy(Scope, "FormatMessage",
+ "FormatMessageW", ctypes.winapi_abi,
+ /*return*/ ctypes.uint32_t,
+ /*flags*/ ctypes.uint32_t,
+ /*source*/ ctypes.voidptr_t,
+ /*msgid*/ ctypes.uint32_t,
+ /*langid*/ ctypes.uint32_t,
+ /*buf*/ ctypes.char16_t.ptr,
+ /*size*/ ctypes.uint32_t,
+ /*Arguments*/ctypes.voidptr_t);
+
+/**
+ * A File-related error.
+ *
+ * To obtain a human-readable error message, use method |toString|.
+ * To determine the cause of the error, use the various |becauseX|
+ * getters. To determine the operation that failed, use field
+ * |operation|.
+ *
+ * Additionally, this implementation offers a field
+ * |winLastError|, which holds the OS-specific error
+ * constant. If you need this level of detail, you may match the value
+ * of this field against the error constants of |OS.Constants.Win|.
+ *
+ * @param {string=} operation The operation that failed. If unspecified,
+ * the name of the calling function is taken to be the operation that
+ * failed.
+ * @param {number=} lastError The OS-specific constant detailing the
+ * reason of the error. If unspecified, this is fetched from the system
+ * status.
+ * @param {string=} path The file path that manipulated. If unspecified,
+ * assign the empty string.
+ *
+ * @constructor
+ * @extends {OS.Shared.Error}
+ */
+var OSError = function OSError(operation = "unknown operation",
+ lastError = ctypes.winLastError, path = "") {
+ operation = operation;
+ SharedAll.OSError.call(this, operation, path);
+ this.winLastError = lastError;
+};
+OSError.prototype = Object.create(SharedAll.OSError.prototype);
+OSError.prototype.toString = function toString() {
+ let buf = new (ctypes.ArrayType(ctypes.char16_t, 1024))();
+ let result = Scope.FormatMessage(
+ Const.FORMAT_MESSAGE_FROM_SYSTEM |
+ Const.FORMAT_MESSAGE_IGNORE_INSERTS,
+ null,
+ /* The error number */ this.winLastError,
+ /* Default language */ 0,
+ /* Output buffer*/ buf,
+ /* Minimum size of buffer */ 1024,
+ /* Format args*/ null
+ );
+ if (!result) {
+ buf = "additional error " +
+ ctypes.winLastError +
+ " while fetching system error message";
+ }
+ return "Win error " + this.winLastError + " during operation "
+ + this.operation + (this.path? " on file " + this.path : "") +
+ " (" + buf.readString() + ")";
+};
+OSError.prototype.toMsg = function toMsg() {
+ return OSError.toMsg(this);
+};
+
+/**
+ * |true| if the error was raised because a file or directory
+ * already exists, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseExists", {
+ get: function becauseExists() {
+ return this.winLastError == Const.ERROR_FILE_EXISTS ||
+ this.winLastError == Const.ERROR_ALREADY_EXISTS;
+ }
+});
+/**
+ * |true| if the error was raised because a file or directory
+ * does not exist, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseNoSuchFile", {
+ get: function becauseNoSuchFile() {
+ return this.winLastError == Const.ERROR_FILE_NOT_FOUND ||
+ this.winLastError == Const.ERROR_PATH_NOT_FOUND;
+ }
+});
+/**
+ * |true| if the error was raised because a directory is not empty
+ * does not exist, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseNotEmpty", {
+ get: function becauseNotEmpty() {
+ return this.winLastError == Const.ERROR_DIR_NOT_EMPTY;
+ }
+});
+/**
+ * |true| if the error was raised because a file or directory
+ * is closed, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseClosed", {
+ get: function becauseClosed() {
+ return this.winLastError == Const.ERROR_INVALID_HANDLE;
+ }
+});
+/**
+ * |true| if the error was raised because permission is denied to
+ * access a file or directory, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseAccessDenied", {
+ get: function becauseAccessDenied() {
+ return this.winLastError == Const.ERROR_ACCESS_DENIED;
+ }
+});
+/**
+ * |true| if the error was raised because some invalid argument was passed,
+ * |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseInvalidArgument", {
+ get: function becauseInvalidArgument() {
+ return this.winLastError == Const.ERROR_NOT_SUPPORTED ||
+ this.winLastError == Const.ERROR_BAD_ARGUMENTS;
+ }
+});
+
+/**
+ * Serialize an instance of OSError to something that can be
+ * transmitted across threads (not necessarily a string).
+ */
+OSError.toMsg = function toMsg(error) {
+ return {
+ exn: "OS.File.Error",
+ fileName: error.moduleName,
+ lineNumber: error.lineNumber,
+ stack: error.moduleStack,
+ operation: error.operation,
+ winLastError: error.winLastError,
+ path: error.path
+ };
+};
+
+/**
+ * Deserialize a message back to an instance of OSError
+ */
+OSError.fromMsg = function fromMsg(msg) {
+ let error = new OSError(msg.operation, msg.winLastError, msg.path);
+ error.stack = msg.stack;
+ error.fileName = msg.fileName;
+ error.lineNumber = msg.lineNumber;
+ return error;
+};
+exports.Error = OSError;
+
+/**
+ * Code shared by implementation of File.Info on Windows
+ *
+ * @constructor
+ */
+var AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size,
+ winBirthDate,
+ lastAccessDate, lastWriteDate,
+ winAttributes) {
+ this._path = path;
+ this._isDir = isDir;
+ this._isSymLink = isSymLink;
+ this._size = size;
+ this._winBirthDate = winBirthDate;
+ this._lastAccessDate = lastAccessDate;
+ this._lastModificationDate = lastWriteDate;
+ this._winAttributes = winAttributes;
+};
+
+AbstractInfo.prototype = {
+ /**
+ * The path of the file, used for error-reporting.
+ *
+ * @type {string}
+ */
+ get path() {
+ return this._path;
+ },
+ /**
+ * |true| if this file is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if this file is a symbolic link, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymLink;
+ },
+ /**
+ * The size of the file, in bytes.
+ *
+ * Note that the result may be |NaN| if the size of the file cannot be
+ * represented in JavaScript.
+ *
+ * @type {number}
+ */
+ get size() {
+ return this._size;
+ },
+ // Deprecated
+ get creationDate() {
+ return this._winBirthDate;
+ },
+ /**
+ * The date of creation of this file.
+ *
+ * @type {Date}
+ */
+ get winBirthDate() {
+ return this._winBirthDate;
+ },
+ /**
+ * The date of last access to this file.
+ *
+ * Note that the definition of last access may depend on the underlying
+ * operating system and file system.
+ *
+ * @type {Date}
+ */
+ get lastAccessDate() {
+ return this._lastAccessDate;
+ },
+ /**
+ * The date of last modification of this file.
+ *
+ * Note that the definition of last access may depend on the underlying
+ * operating system and file system.
+ *
+ * @type {Date}
+ */
+ get lastModificationDate() {
+ return this._lastModificationDate;
+ },
+ /**
+ * The Object with following boolean properties of this file.
+ * {readOnly, system, hidden}
+ *
+ * @type {object}
+ */
+ get winAttributes() {
+ return this._winAttributes;
+ }
+};
+exports.AbstractInfo = AbstractInfo;
+
+/**
+ * Code shared by implementation of File.DirectoryIterator.Entry on Windows
+ *
+ * @constructor
+ */
+var AbstractEntry = function AbstractEntry(isDir, isSymLink, name,
+ winCreationDate, winLastWriteDate,
+ winLastAccessDate, path) {
+ this._isDir = isDir;
+ this._isSymLink = isSymLink;
+ this._name = name;
+ this._winCreationDate = winCreationDate;
+ this._winLastWriteDate = winLastWriteDate;
+ this._winLastAccessDate = winLastAccessDate;
+ this._path = path;
+};
+
+AbstractEntry.prototype = {
+ /**
+ * |true| if the entry is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if the entry is a symbolic link, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymLink;
+ },
+ /**
+ * The name of the entry.
+ * @type {string}
+ */
+ get name() {
+ return this._name;
+ },
+ /**
+ * The creation time of this file.
+ * @type {Date}
+ */
+ get winCreationDate() {
+ return this._winCreationDate;
+ },
+ /**
+ * The last modification time of this file.
+ * @type {Date}
+ */
+ get winLastWriteDate() {
+ return this._winLastWriteDate;
+ },
+ /**
+ * The last access time of this file.
+ * @type {Date}
+ */
+ get winLastAccessDate() {
+ return this._winLastAccessDate;
+ },
+ /**
+ * The full path of the entry
+ * @type {string}
+ */
+ get path() {
+ return this._path;
+ }
+};
+exports.AbstractEntry = AbstractEntry;
+
+// Special constants that need to be defined on all platforms
+
+exports.POS_START = Const.FILE_BEGIN;
+exports.POS_CURRENT = Const.FILE_CURRENT;
+exports.POS_END = Const.FILE_END;
+
+// Special types that need to be defined for communication
+// between threads
+var Type = Object.create(SharedAll.Type);
+exports.Type = Type;
+
+/**
+ * Native paths
+ *
+ * Under Windows, expressed as wide strings
+ */
+Type.path = Type.wstring.withName("[in] path");
+Type.out_path = Type.out_wstring.withName("[out] path");
+
+// Special constructors that need to be defined on all threads
+OSError.closed = function closed(operation, path) {
+ return new OSError(operation, Const.ERROR_INVALID_HANDLE, path);
+};
+
+OSError.exists = function exists(operation, path) {
+ return new OSError(operation, Const.ERROR_FILE_EXISTS, path);
+};
+
+OSError.noSuchFile = function noSuchFile(operation, path) {
+ return new OSError(operation, Const.ERROR_FILE_NOT_FOUND, path);
+};
+
+OSError.invalidArgument = function invalidArgument(operation) {
+ return new OSError(operation, Const.ERROR_NOT_SUPPORTED);
+};
+
+var EXPORTED_SYMBOLS = [
+ "declareFFI",
+ "libc",
+ "Error",
+ "AbstractInfo",
+ "AbstractEntry",
+ "Type",
+ "POS_START",
+ "POS_CURRENT",
+ "POS_END"
+];
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/osfile_win_back.jsm b/toolkit/components/osfile/modules/osfile_win_back.jsm
new file mode 100644
index 0000000000..21172b6bfe
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_win_back.jsm
@@ -0,0 +1,437 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file can be used in the following contexts:
+ *
+ * 1. included from a non-osfile worker thread using importScript
+ * (it serves to define a synchronous API for that worker thread)
+ * (bug 707681)
+ *
+ * 2. included from the main thread using Components.utils.import
+ * (it serves to define the asynchronous API, whose implementation
+ * resides in the worker thread)
+ * (bug 729057)
+ *
+ * 3. included from the osfile worker thread using importScript
+ * (it serves to define the implementation of the asynchronous API)
+ * (bug 729057)
+ */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_win.jsm to be used directly as a main thread
+ // module yet. When time comes, it will be loaded by a combination of
+ // a main thread front-end/worker thread implementation that makes sure
+ // that we are not executing synchronous IO code in the main thread.
+
+ throw new Error("osfile_win.jsm cannot be used from the main thread yet");
+ }
+
+ (function(exports) {
+ "use strict";
+ if (exports.OS && exports.OS.Win && exports.OS.Win.File) {
+ return; // Avoid double initialization
+ }
+
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let SysAll = require("resource://gre/modules/osfile/osfile_win_allthreads.jsm");
+ let LOG = SharedAll.LOG.bind(SharedAll, "Unix", "back");
+ let libc = SysAll.libc;
+ let advapi32 = new SharedAll.Library("advapi32", "advapi32.dll");
+ let Const = SharedAll.Constants.Win;
+
+ /**
+ * Initialize the Windows module.
+ *
+ * @param {function=} declareFFI
+ */
+ // FIXME: Both |init| and |aDeclareFFI| are deprecated, we should remove them
+ let init = function init(aDeclareFFI) {
+ let declareFFI;
+ if (aDeclareFFI) {
+ declareFFI = aDeclareFFI.bind(null, libc);
+ } else {
+ declareFFI = SysAll.declareFFI;
+ }
+ let declareLazyFFI = SharedAll.declareLazyFFI;
+
+ // Initialize types that require additional OS-specific
+ // support - either finalization or matching against
+ // OS-specific constants.
+ let Type = Object.create(SysAll.Type);
+ let SysFile = exports.OS.Win.File = { Type: Type };
+
+ // Initialize types
+
+ /**
+ * A C integer holding INVALID_HANDLE_VALUE in case of error or
+ * a file descriptor in case of success.
+ */
+ Type.HANDLE =
+ Type.voidptr_t.withName("HANDLE");
+ Type.HANDLE.importFromC = function importFromC(maybe) {
+ if (Type.int.cast(maybe).value == INVALID_HANDLE) {
+ // Ensure that API clients can effectively compare against
+ // Const.INVALID_HANDLE_VALUE. Without this cast,
+ // == would always return |false|.
+ return INVALID_HANDLE;
+ }
+ return ctypes.CDataFinalizer(maybe, this.finalizeHANDLE);
+ };
+ Type.HANDLE.finalizeHANDLE = function placeholder() {
+ throw new Error("finalizeHANDLE should be implemented");
+ };
+ let INVALID_HANDLE = Const.INVALID_HANDLE_VALUE;
+
+ Type.file_HANDLE = Type.HANDLE.withName("file HANDLE");
+ SharedAll.defineLazyGetter(Type.file_HANDLE,
+ "finalizeHANDLE",
+ function() {
+ return SysFile._CloseHandle;
+ });
+
+ Type.find_HANDLE = Type.HANDLE.withName("find HANDLE");
+ SharedAll.defineLazyGetter(Type.find_HANDLE,
+ "finalizeHANDLE",
+ function() {
+ return SysFile._FindClose;
+ });
+
+ Type.DWORD = Type.uint32_t.withName("DWORD");
+
+ /* A special type used to represent flags passed as DWORDs to a function.
+ * In JavaScript, bitwise manipulation of numbers, such as or-ing flags,
+ * can produce negative numbers. Since DWORD is unsigned, these negative
+ * numbers simply cannot be converted to DWORD. For this reason, whenever
+ * bit manipulation is called for, you should rather use DWORD_FLAGS,
+ * which is represented as a signed integer, hence has the correct
+ * semantics.
+ */
+ Type.DWORD_FLAGS = Type.int32_t.withName("DWORD_FLAGS");
+
+ /**
+ * A C integer holding 0 in case of error or a positive integer
+ * in case of success.
+ */
+ Type.zero_or_DWORD =
+ Type.DWORD.withName("zero_or_DWORD");
+
+ /**
+ * A C integer holding 0 in case of error, any other value in
+ * case of success.
+ */
+ Type.zero_or_nothing =
+ Type.int.withName("zero_or_nothing");
+
+ /**
+ * A C integer holding flags related to NTFS security.
+ */
+ Type.SECURITY_ATTRIBUTES =
+ Type.void_t.withName("SECURITY_ATTRIBUTES");
+
+ /**
+ * A C integer holding pointers related to NTFS security.
+ */
+ Type.PSID =
+ Type.voidptr_t.withName("PSID");
+
+ Type.PACL =
+ Type.voidptr_t.withName("PACL");
+
+ Type.PSECURITY_DESCRIPTOR =
+ Type.voidptr_t.withName("PSECURITY_DESCRIPTOR");
+
+ /**
+ * A C integer holding Win32 local memory handle.
+ */
+ Type.HLOCAL =
+ Type.voidptr_t.withName("HLOCAL");
+
+ Type.FILETIME =
+ new SharedAll.Type("FILETIME",
+ ctypes.StructType("FILETIME", [
+ { lo: Type.DWORD.implementation },
+ { hi: Type.DWORD.implementation }]));
+
+ Type.FindData =
+ new SharedAll.Type("FIND_DATA",
+ ctypes.StructType("FIND_DATA", [
+ { dwFileAttributes: ctypes.uint32_t },
+ { ftCreationTime: Type.FILETIME.implementation },
+ { ftLastAccessTime: Type.FILETIME.implementation },
+ { ftLastWriteTime: Type.FILETIME.implementation },
+ { nFileSizeHigh: Type.DWORD.implementation },
+ { nFileSizeLow: Type.DWORD.implementation },
+ { dwReserved0: Type.DWORD.implementation },
+ { dwReserved1: Type.DWORD.implementation },
+ { cFileName: ctypes.ArrayType(ctypes.char16_t, Const.MAX_PATH) },
+ { cAlternateFileName: ctypes.ArrayType(ctypes.char16_t, 14) }
+ ]));
+
+ Type.FILE_INFORMATION =
+ new SharedAll.Type("FILE_INFORMATION",
+ ctypes.StructType("FILE_INFORMATION", [
+ { dwFileAttributes: ctypes.uint32_t },
+ { ftCreationTime: Type.FILETIME.implementation },
+ { ftLastAccessTime: Type.FILETIME.implementation },
+ { ftLastWriteTime: Type.FILETIME.implementation },
+ { dwVolumeSerialNumber: ctypes.uint32_t },
+ { nFileSizeHigh: Type.DWORD.implementation },
+ { nFileSizeLow: Type.DWORD.implementation },
+ { nNumberOfLinks: ctypes.uint32_t },
+ { nFileIndex: ctypes.uint64_t }
+ ]));
+
+ Type.SystemTime =
+ new SharedAll.Type("SystemTime",
+ ctypes.StructType("SystemTime", [
+ { wYear: ctypes.int16_t },
+ { wMonth: ctypes.int16_t },
+ { wDayOfWeek: ctypes.int16_t },
+ { wDay: ctypes.int16_t },
+ { wHour: ctypes.int16_t },
+ { wMinute: ctypes.int16_t },
+ { wSecond: ctypes.int16_t },
+ { wMilliSeconds: ctypes.int16_t }
+ ]));
+
+ // Special case: these functions are used by the
+ // finalizer
+ libc.declareLazy(SysFile, "_CloseHandle",
+ "CloseHandle", ctypes.winapi_abi,
+ /*return */ctypes.bool,
+ /*handle*/ ctypes.voidptr_t);
+
+ SysFile.CloseHandle = function(fd) {
+ if (fd == INVALID_HANDLE) {
+ return true;
+ } else {
+ return fd.dispose(); // Returns the value of |CloseHandle|.
+ }
+ };
+
+ libc.declareLazy(SysFile, "_FindClose",
+ "FindClose", ctypes.winapi_abi,
+ /*return */ctypes.bool,
+ /*handle*/ ctypes.voidptr_t);
+
+ SysFile.FindClose = function(handle) {
+ if (handle == INVALID_HANDLE) {
+ return true;
+ } else {
+ return handle.dispose(); // Returns the value of |FindClose|.
+ }
+ };
+
+ // Declare libc functions as functions of |OS.Win.File|
+
+ libc.declareLazyFFI(SysFile, "CopyFile",
+ "CopyFileW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*sourcePath*/ Type.path,
+ /*destPath*/ Type.path,
+ /*bailIfExist*/Type.bool);
+
+ libc.declareLazyFFI(SysFile, "CreateDirectory",
+ "CreateDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*name*/ Type.char16_t.in_ptr,
+ /*security*/Type.SECURITY_ATTRIBUTES.in_ptr);
+
+ libc.declareLazyFFI(SysFile, "CreateFile",
+ "CreateFileW", ctypes.winapi_abi,
+ /*return*/ Type.file_HANDLE,
+ /*name*/ Type.path,
+ /*access*/ Type.DWORD_FLAGS,
+ /*share*/ Type.DWORD_FLAGS,
+ /*security*/Type.SECURITY_ATTRIBUTES.in_ptr,
+ /*creation*/Type.DWORD_FLAGS,
+ /*flags*/ Type.DWORD_FLAGS,
+ /*template*/Type.HANDLE);
+
+ libc.declareLazyFFI(SysFile, "DeleteFile",
+ "DeleteFileW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "FileTimeToSystemTime",
+ "FileTimeToSystemTime", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*filetime*/Type.FILETIME.in_ptr,
+ /*systime*/ Type.SystemTime.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "SystemTimeToFileTime",
+ "SystemTimeToFileTime", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*systime*/ Type.SystemTime.in_ptr,
+ /*filetime*/ Type.FILETIME.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "FindFirstFile",
+ "FindFirstFileW", ctypes.winapi_abi,
+ /*return*/ Type.find_HANDLE,
+ /*pattern*/Type.path,
+ /*data*/ Type.FindData.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "FindNextFile",
+ "FindNextFileW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*prev*/ Type.find_HANDLE,
+ /*data*/ Type.FindData.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "FormatMessage",
+ "FormatMessageW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*flags*/ Type.DWORD_FLAGS,
+ /*source*/ Type.void_t.in_ptr,
+ /*msgid*/ Type.DWORD_FLAGS,
+ /*langid*/ Type.DWORD_FLAGS,
+ /*buf*/ Type.out_wstring,
+ /*size*/ Type.DWORD,
+ /*Arguments*/Type.void_t.in_ptr
+ );
+
+ libc.declareLazyFFI(SysFile, "GetCurrentDirectory",
+ "GetCurrentDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_DWORD,
+ /*length*/ Type.DWORD,
+ /*buf*/ Type.out_path
+ );
+
+ libc.declareLazyFFI(SysFile, "GetFullPathName",
+ "GetFullPathNameW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_DWORD,
+ /*fileName*/ Type.path,
+ /*length*/ Type.DWORD,
+ /*buf*/ Type.out_path,
+ /*filePart*/ Type.DWORD
+ );
+
+ libc.declareLazyFFI(SysFile, "GetDiskFreeSpaceEx",
+ "GetDiskFreeSpaceExW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*directoryName*/ Type.path,
+ /*freeBytesForUser*/ Type.uint64_t.out_ptr,
+ /*totalBytesForUser*/ Type.uint64_t.out_ptr,
+ /*freeTotalBytesOnDrive*/ Type.uint64_t.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "GetFileInformationByHandle",
+ "GetFileInformationByHandle", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*handle*/ Type.HANDLE,
+ /*info*/ Type.FILE_INFORMATION.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "MoveFileEx",
+ "MoveFileExW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*sourcePath*/ Type.path,
+ /*destPath*/ Type.path,
+ /*flags*/ Type.DWORD
+ );
+
+ libc.declareLazyFFI(SysFile, "ReadFile",
+ "ReadFile", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE,
+ /*buffer*/ Type.voidptr_t,
+ /*nbytes*/ Type.DWORD,
+ /*nbytes_read*/Type.DWORD.out_ptr,
+ /*overlapped*/Type.void_t.inout_ptr // FIXME: Implement?
+ );
+
+ libc.declareLazyFFI(SysFile, "RemoveDirectory",
+ "RemoveDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "SetCurrentDirectory",
+ "SetCurrentDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*path*/ Type.path
+ );
+
+ libc.declareLazyFFI(SysFile, "SetEndOfFile",
+ "SetEndOfFile", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE);
+
+ libc.declareLazyFFI(SysFile, "SetFilePointer",
+ "SetFilePointer", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*file*/ Type.HANDLE,
+ /*distlow*/Type.long,
+ /*disthi*/ Type.long.in_ptr,
+ /*method*/ Type.DWORD);
+
+ libc.declareLazyFFI(SysFile, "SetFileTime",
+ "SetFileTime", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE,
+ /*creation*/ Type.FILETIME.in_ptr,
+ /*access*/ Type.FILETIME.in_ptr,
+ /*write*/ Type.FILETIME.in_ptr);
+
+
+ libc.declareLazyFFI(SysFile, "WriteFile",
+ "WriteFile", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE,
+ /*buffer*/ Type.voidptr_t,
+ /*nbytes*/ Type.DWORD,
+ /*nbytes_wr*/Type.DWORD.out_ptr,
+ /*overlapped*/Type.void_t.inout_ptr // FIXME: Implement?
+ );
+
+ libc.declareLazyFFI(SysFile, "FlushFileBuffers",
+ "FlushFileBuffers", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE);
+
+ libc.declareLazyFFI(SysFile, "GetFileAttributes",
+ "GetFileAttributesW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD_FLAGS,
+ /*fileName*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "SetFileAttributes",
+ "SetFileAttributesW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*fileName*/ Type.path,
+ /*fileAttributes*/ Type.DWORD_FLAGS);
+
+ advapi32.declareLazyFFI(SysFile, "GetNamedSecurityInfo",
+ "GetNamedSecurityInfoW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*objectName*/ Type.path,
+ /*objectType*/ Type.DWORD,
+ /*securityInfo*/ Type.DWORD,
+ /*sidOwner*/ Type.PSID.out_ptr,
+ /*sidGroup*/ Type.PSID.out_ptr,
+ /*dacl*/ Type.PACL.out_ptr,
+ /*sacl*/ Type.PACL.out_ptr,
+ /*securityDesc*/ Type.PSECURITY_DESCRIPTOR.out_ptr);
+
+ advapi32.declareLazyFFI(SysFile, "SetNamedSecurityInfo",
+ "SetNamedSecurityInfoW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*objectName*/ Type.path,
+ /*objectType*/ Type.DWORD,
+ /*securityInfo*/ Type.DWORD,
+ /*sidOwner*/ Type.PSID,
+ /*sidGroup*/ Type.PSID,
+ /*dacl*/ Type.PACL,
+ /*sacl*/ Type.PACL);
+
+ libc.declareLazyFFI(SysFile, "LocalFree",
+ "LocalFree", ctypes.winapi_abi,
+ /*return*/ Type.HLOCAL,
+ /*mem*/ Type.HLOCAL);
+ };
+
+ exports.OS.Win = {
+ File: {
+ _init: init
+ }
+ };
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/osfile_win_front.jsm b/toolkit/components/osfile/modules/osfile_win_front.jsm
new file mode 100644
index 0000000000..387dd08b56
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_win_front.jsm
@@ -0,0 +1,1266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Synchronous front-end for the JavaScript OS.File library.
+ * Windows implementation.
+ *
+ * This front-end is meant to be imported by a worker thread.
+ */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_win_front.jsm to be used directly as a main thread
+ // module yet.
+ throw new Error("osfile_win_front.jsm cannot be used from the main thread yet");
+ }
+
+ (function(exports) {
+ "use strict";
+
+
+ // exports.OS.Win is created by osfile_win_back.jsm
+ if (exports.OS && exports.OS.File) {
+ return; // Avoid double-initialization
+ }
+
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let Path = require("resource://gre/modules/osfile/ospath.jsm");
+ let SysAll = require("resource://gre/modules/osfile/osfile_win_allthreads.jsm");
+ exports.OS.Win.File._init();
+ let Const = exports.OS.Constants.Win;
+ let WinFile = exports.OS.Win.File;
+ let Type = WinFile.Type;
+
+ // Mutable thread-global data
+ // In the Windows implementation, methods |read| and |write|
+ // require passing a pointer to an uint32 to determine how many
+ // bytes have been read/written. In C, this is a benigne operation,
+ // but in js-ctypes, this has a cost. Rather than re-allocating a
+ // C uint32 and a C uint32* for each |read|/|write|, we take advantage
+ // of the fact that the state is thread-private -- hence that two
+ // |read|/|write| operations cannot take place at the same time --
+ // and we use the following global mutable values:
+ let gBytesRead = new ctypes.uint32_t(0);
+ let gBytesReadPtr = gBytesRead.address();
+ let gBytesWritten = new ctypes.uint32_t(0);
+ let gBytesWrittenPtr = gBytesWritten.address();
+
+ // Same story for GetFileInformationByHandle
+ let gFileInfo = new Type.FILE_INFORMATION.implementation();
+ let gFileInfoPtr = gFileInfo.address();
+
+ /**
+ * Representation of a file.
+ *
+ * You generally do not need to call this constructor yourself. Rather,
+ * to open a file, use function |OS.File.open|.
+ *
+ * @param fd A OS-specific file descriptor.
+ * @param {string} path File path of the file handle, used for error-reporting.
+ * @constructor
+ */
+ let File = function File(fd, path) {
+ exports.OS.Shared.AbstractFile.call(this, fd, path);
+ this._closeResult = null;
+ };
+ File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype);
+
+ /**
+ * Close the file.
+ *
+ * This method has no effect if the file is already closed. However,
+ * if the first call to |close| has thrown an error, further calls
+ * will throw the same error.
+ *
+ * @throws File.Error If closing the file revealed an error that could
+ * not be reported earlier.
+ */
+ File.prototype.close = function close() {
+ if (this._fd) {
+ let fd = this._fd;
+ this._fd = null;
+ // Call |close(fd)|, detach finalizer if any
+ // (|fd| may not be a CDataFinalizer if it has been
+ // instantiated from a controller thread).
+ let result = WinFile._CloseHandle(fd);
+ if (typeof fd == "object" && "forget" in fd) {
+ fd.forget();
+ }
+ if (result == -1) {
+ this._closeResult = new File.Error("close", ctypes.winLastError, this._path);
+ }
+ }
+ if (this._closeResult) {
+ throw this._closeResult;
+ }
+ return;
+ };
+
+ /**
+ * Read some bytes from a file.
+ *
+ * @param {C pointer} buffer A buffer for holding the data
+ * once it is read.
+ * @param {number} nbytes The number of bytes to read. It must not
+ * exceed the size of |buffer| in bytes but it may exceed the number
+ * of bytes unread in the file.
+ * @param {*=} options Additional options for reading. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively read. If zero,
+ * the end of the file has been reached.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._read = function _read(buffer, nbytes, options) {
+ // |gBytesReadPtr| is a pointer to |gBytesRead|.
+ throw_on_zero("read",
+ WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null),
+ this._path
+ );
+ return gBytesRead.value;
+ };
+
+ /**
+ * Write some bytes to a file.
+ *
+ * @param {Typed array} buffer A buffer holding the data that must be
+ * written.
+ * @param {number} nbytes The number of bytes to write. It must not
+ * exceed the size of |buffer| in bytes.
+ * @param {*=} options Additional options for writing. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively written.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._write = function _write(buffer, nbytes, options) {
+ if (this._appendMode) {
+ // Need to manually seek on Windows, as O_APPEND is not supported.
+ // This is, of course, a race, but there is no real way around this.
+ this.setPosition(0, File.POS_END);
+ }
+ // |gBytesWrittenPtr| is a pointer to |gBytesWritten|.
+ throw_on_zero("write",
+ WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null),
+ this._path
+ );
+ return gBytesWritten.value;
+ };
+
+ /**
+ * Return the current position in the file.
+ */
+ File.prototype.getPosition = function getPosition(pos) {
+ return this.setPosition(0, File.POS_CURRENT);
+ };
+
+ /**
+ * Change the current position in the file.
+ *
+ * @param {number} pos The new position. Whether this position
+ * is considered from the current position, from the start of
+ * the file or from the end of the file is determined by
+ * argument |whence|. Note that |pos| may exceed the length of
+ * the file.
+ * @param {number=} whence The reference position. If omitted
+ * or |OS.File.POS_START|, |pos| is relative to the start of the
+ * file. If |OS.File.POS_CURRENT|, |pos| is relative to the
+ * current position in the file. If |OS.File.POS_END|, |pos| is
+ * relative to the end of the file.
+ *
+ * @return The new position in the file.
+ */
+ File.prototype.setPosition = function setPosition(pos, whence) {
+ if (whence === undefined) {
+ whence = Const.FILE_BEGIN;
+ }
+ let pos64 = ctypes.Int64(pos);
+ // Per MSDN, while |lDistanceToMove| (low) is declared as int32_t, when
+ // providing |lDistanceToMoveHigh| as well, it should countain the
+ // bottom 32 bits of the 64-bit integer. Hence the following |posLo|
+ // cast is OK.
+ let posLo = new ctypes.uint32_t(ctypes.Int64.lo(pos64));
+ posLo = ctypes.cast(posLo, ctypes.int32_t);
+ let posHi = new ctypes.int32_t(ctypes.Int64.hi(pos64));
+ let result = WinFile.SetFilePointer(
+ this.fd, posLo.value, posHi.address(), whence);
+ // INVALID_SET_FILE_POINTER might be still a valid result, as it
+ // represents the lower 32 bit of the int64 result. MSDN says to check
+ // both, INVALID_SET_FILE_POINTER and a non-zero winLastError.
+ if (result == Const.INVALID_SET_FILE_POINTER && ctypes.winLastError) {
+ throw new File.Error("setPosition", ctypes.winLastError, this._path);
+ }
+ pos64 = ctypes.Int64.join(posHi.value, result);
+ return Type.int64_t.project(pos64);
+ };
+
+ /**
+ * Fetch the information on the file.
+ *
+ * @return File.Info The information on |this| file.
+ */
+ File.prototype.stat = function stat() {
+ throw_on_zero("stat",
+ WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr),
+ this._path);
+ return new File.Info(gFileInfo, this._path);
+ };
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid parameters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype.setDates = function setDates(accessDate, modificationDate) {
+ accessDate = Date_to_FILETIME("File.prototype.setDates", accessDate, this._path);
+ modificationDate = Date_to_FILETIME("File.prototype.setDates",
+ modificationDate,
+ this._path);
+ throw_on_zero("setDates",
+ WinFile.SetFileTime(this.fd, null, accessDate.address(),
+ modificationDate.address()),
+ this._path);
+ };
+
+ /**
+ * Set the file's access permission bits.
+ */
+ File.prototype.setPermissions = function setPermissions(options = {}) {
+ if (!("winAttributes" in options)) {
+ return;
+ }
+ let oldAttributes = WinFile.GetFileAttributes(this._path);
+ if (oldAttributes == Const.INVALID_FILE_ATTRIBUTES) {
+ throw new File.Error("setPermissions", ctypes.winLastError, this._path);
+ }
+ let newAttributes = toFileAttributes(options.winAttributes, oldAttributes);
+ throw_on_zero("setPermissions",
+ WinFile.SetFileAttributes(this._path, newAttributes),
+ this._path);
+ };
+
+ /**
+ * Flushes the file's buffers and causes all buffered data
+ * to be written.
+ * Disk flushes are very expensive and therefore should be used carefully,
+ * sparingly and only in scenarios where it is vital that data survives
+ * system crashes. Even though the function will be executed off the
+ * main-thread, it might still affect the overall performance of any
+ * running application.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype.flush = function flush() {
+ throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd), this._path);
+ };
+
+ // The default sharing mode for opening files: files are not
+ // locked against being reopened for reading/writing or against
+ // being deleted by the same process or another process.
+ // This is consistent with the default Unix policy.
+ const DEFAULT_SHARE = Const.FILE_SHARE_READ |
+ Const.FILE_SHARE_WRITE | Const.FILE_SHARE_DELETE;
+
+ // The default flags for opening files.
+ const DEFAULT_FLAGS = Const.FILE_ATTRIBUTE_NORMAL;
+
+ /**
+ * Open a file
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} mode The opening mode for the file, as
+ * an object that may contain the following fields:
+ *
+ * - {bool} truncate If |true|, the file will be opened
+ * for writing. If the file does not exist, it will be
+ * created. If the file exists, its contents will be
+ * erased. Cannot be specified with |create|.
+ * - {bool} create If |true|, the file will be opened
+ * for writing. If the file exists, this function fails.
+ * If the file does not exist, it will be created. Cannot
+ * be specified with |truncate| or |existing|.
+ * - {bool} existing. If the file does not exist, this function
+ * fails. Cannot be specified with |create|.
+ * - {bool} read If |true|, the file will be opened for
+ * reading. The file may also be opened for writing, depending
+ * on the other fields of |mode|.
+ * - {bool} write If |true|, the file will be opened for
+ * writing. The file may also be opened for reading, depending
+ * on the other fields of |mode|.
+ * - {bool} append If |true|, the file will be opened for appending,
+ * meaning the equivalent of |.setPosition(0, POS_END)| is executed
+ * before each write. The default is |true|, i.e. opening a file for
+ * appending. Specify |append: false| to open the file in regular mode.
+ *
+ * If neither |truncate|, |create| or |write| is specified, the file
+ * is opened for reading.
+ *
+ * Note that |false|, |null| or |undefined| flags are simply ignored.
+ *
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} winShare If specified, a share mode, as per
+ * Windows function |CreateFile|. You can build it from
+ * constants |OS.Constants.Win.FILE_SHARE_*|. If unspecified,
+ * the file uses the default sharing policy: it can be opened
+ * for reading and/or writing and it can be removed by other
+ * processes and by the same process.
+ * - {number} winSecurity If specified, Windows security
+ * attributes, as per Windows function |CreateFile|. If unspecified,
+ * no security attributes.
+ * - {number} winAccess If specified, Windows access mode, as
+ * per Windows function |CreateFile|. This also requires option
+ * |winDisposition| and this replaces argument |mode|. If unspecified,
+ * uses the string |mode|.
+ * - {number} winDisposition If specified, Windows disposition mode,
+ * as per Windows function |CreateFile|. This also requires option
+ * |winAccess| and this replaces argument |mode|. If unspecified,
+ * uses the string |mode|.
+ *
+ * @return {File} A file object.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+ File.open = function Win_open(path, mode = {}, options = {}) {
+ let share = options.winShare !== undefined ? options.winShare : DEFAULT_SHARE;
+ let security = options.winSecurity || null;
+ let flags = options.winFlags !== undefined ? options.winFlags : DEFAULT_FLAGS;
+ let template = options.winTemplate ? options.winTemplate._fd : null;
+ let access;
+ let disposition;
+
+ mode = OS.Shared.AbstractFile.normalizeOpenMode(mode);
+
+ // The following option isn't a generic implementation of access to paths
+ // of arbitrary lengths. It allows for the specific case of writing to an
+ // Alternate Data Stream on a file whose path length is already close to
+ // MAX_PATH. This implementation is safe with a full path as input, if
+ // the first part of the path comes from local configuration and the
+ // file without the ADS was successfully opened before, so we know the
+ // path is valid.
+ if (options.winAllowLengthBeyondMaxPathWithCaveats) {
+ // Use the \\?\ syntax to allow lengths beyond MAX_PATH. This limited
+ // implementation only supports a DOS local path or UNC path as input.
+ let isUNC = path.length >= 2 && (path[0] == "\\" || path[0] == "/") &&
+ (path[1] == "\\" || path[1] == "/");
+ let pathToUse = "\\\\?\\" + (isUNC ? "UNC\\" + path.slice(2) : path);
+ // Use GetFullPathName to normalize slashes into backslashes. This is
+ // required because CreateFile won't do this for the \\?\ syntax.
+ let buffer_size = 512;
+ let array = new (ctypes.ArrayType(ctypes.char16_t, buffer_size))();
+ let expected_size = throw_on_zero("open",
+ WinFile.GetFullPathName(pathToUse, buffer_size, array, 0)
+ );
+ if (expected_size > buffer_size) {
+ // We don't need to allow an arbitrary path length for now.
+ throw new File.Error("open", ctypes.winLastError, path);
+ }
+ path = array.readString();
+ }
+
+ if ("winAccess" in options && "winDisposition" in options) {
+ access = options.winAccess;
+ disposition = options.winDisposition;
+ } else if (("winAccess" in options && !("winDisposition" in options))
+ ||(!("winAccess" in options) && "winDisposition" in options)) {
+ throw new TypeError("OS.File.open requires either both options " +
+ "winAccess and winDisposition or neither");
+ } else {
+ if (mode.read) {
+ access |= Const.GENERIC_READ;
+ }
+ if (mode.write) {
+ access |= Const.GENERIC_WRITE;
+ }
+ // Finally, handle create/existing/trunc
+ if (mode.trunc) {
+ if (mode.existing) {
+ // It seems that Const.TRUNCATE_EXISTING is broken
+ // in presence of links (source, anyone?). We need
+ // to open normally, then perform truncation manually.
+ disposition = Const.OPEN_EXISTING;
+ } else {
+ disposition = Const.CREATE_ALWAYS;
+ }
+ } else if (mode.create) {
+ disposition = Const.CREATE_NEW;
+ } else if (mode.read && !mode.write) {
+ disposition = Const.OPEN_EXISTING;
+ } else if (mode.existing) {
+ disposition = Const.OPEN_EXISTING;
+ } else {
+ disposition = Const.OPEN_ALWAYS;
+ }
+ }
+
+ let file = error_or_file(WinFile.CreateFile(path,
+ access, share, security, disposition, flags, template), path);
+
+ file._appendMode = !!mode.append;
+
+ if (!(mode.trunc && mode.existing)) {
+ return file;
+ }
+ // Now, perform manual truncation
+ file.setPosition(0, File.POS_START);
+ throw_on_zero("open",
+ WinFile.SetEndOfFile(file.fd),
+ path);
+ return file;
+ };
+
+ /**
+ * Checks if a file or directory exists
+ *
+ * @param {string} path The path to the file.
+ *
+ * @return {bool} true if the file exists, false otherwise.
+ */
+ File.exists = function Win_exists(path) {
+ try {
+ let file = File.open(path, FILE_STAT_MODE, FILE_STAT_OPTIONS);
+ file.close();
+ return true;
+ } catch (x) {
+ return false;
+ }
+ };
+
+ /**
+ * Remove an existing file.
+ *
+ * @param {string} path The name of the file.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the file does
+ * not exist. |true| by default.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.remove = function remove(path, options = {}) {
+ if (WinFile.DeleteFile(path)) {
+ return;
+ }
+
+ if (ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND ||
+ ctypes.winLastError == Const.ERROR_PATH_NOT_FOUND) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent)) {
+ return;
+ }
+ } else if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED) {
+ // Save winLastError before another ctypes call.
+ let lastError = ctypes.winLastError;
+ let attributes = WinFile.GetFileAttributes(path);
+ if (attributes != Const.INVALID_FILE_ATTRIBUTES) {
+ if (!(attributes & Const.FILE_ATTRIBUTE_READONLY)) {
+ throw new File.Error("remove", lastError, path);
+ }
+ let newAttributes = attributes & ~Const.FILE_ATTRIBUTE_READONLY;
+ if (WinFile.SetFileAttributes(path, newAttributes) &&
+ WinFile.DeleteFile(path)) {
+ return;
+ }
+ }
+ }
+
+ throw new File.Error("remove", ctypes.winLastError, path);
+ };
+
+ /**
+ * Remove an empty directory.
+ *
+ * @param {string} path The name of the directory to remove.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory
+ * does not exist. |true| by default
+ */
+ File.removeEmptyDir = function removeEmptyDir(path, options = {}) {
+ let result = WinFile.RemoveDirectory(path);
+ if (!result) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ throw new File.Error("removeEmptyDir", ctypes.winLastError, path);
+ }
+ };
+
+ /**
+ * Create a directory and, optionally, its parent directories.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options. This
+ * implementation interprets the following fields:
+ *
+ * - {C pointer} winSecurity If specified, security attributes
+ * as per winapi function |CreateDirectory|. If unspecified,
+ * use the default security descriptor, inherited from the
+ * parent directory.
+ * - {bool} ignoreExisting If |false|, throw an error if the directory
+ * already exists. |true| by default
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |from|
+ * and its existing descendants must be user-writeable and that |path|
+ * must be a descendant of |from|.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ */
+ File._makeDir = function makeDir(path, options = {}) {
+ let security = options.winSecurity || null;
+ let result = WinFile.CreateDirectory(path, security);
+
+ if (result) {
+ return;
+ }
+
+ if (("ignoreExisting" in options) && !options.ignoreExisting) {
+ throw new File.Error("makeDir", ctypes.winLastError, path);
+ }
+
+ if (ctypes.winLastError == Const.ERROR_ALREADY_EXISTS) {
+ return;
+ }
+
+ // If the user has no access, but it's a root directory, no error should be thrown
+ let splitPath = OS.Path.split(path);
+ // Removing last component if it's empty
+ // An empty last component is caused by trailing slashes in path
+ // This is always the case with root directories
+ if( splitPath.components[splitPath.components.length - 1].length === 0 ) {
+ splitPath.components.pop();
+ }
+ // One component consisting of a drive letter implies a directory root.
+ if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED &&
+ splitPath.winDrive &&
+ splitPath.components.length === 1 ) {
+ return;
+ }
+
+ throw new File.Error("makeDir", ctypes.winLastError, path);
+ };
+
+ /**
+ * Copy a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be copied.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If true, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be copied with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.copy = function copy(sourcePath, destPath, options = {}) {
+ throw_on_zero("copy",
+ WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false),
+ sourcePath
+ );
+ };
+
+ /**
+ * Move a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be moved.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ * @option {bool} noCopy - If set, this function will fail if the
+ * operation is more sophisticated than a simple renaming, i.e. if
+ * |sourcePath| and |destPath| are not situated on the same drive.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be moved with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.move = function move(sourcePath, destPath, options = {}) {
+ let flags = 0;
+ if (!options.noCopy) {
+ flags = Const.MOVEFILE_COPY_ALLOWED;
+ }
+ if (!options.noOverwrite) {
+ flags = flags | Const.MOVEFILE_REPLACE_EXISTING;
+ }
+ throw_on_zero("move",
+ WinFile.MoveFileEx(sourcePath, destPath, flags),
+ sourcePath
+ );
+
+ // Inherit NTFS permissions from the destination directory
+ // if possible.
+ if (Path.dirname(sourcePath) === Path.dirname(destPath)) {
+ // Skip if the move operation was the simple rename,
+ return;
+ }
+ // The function may fail for various reasons (e.g. not all
+ // filesystems support NTFS permissions or the user may not
+ // have the enough rights to read/write permissions).
+ // However we can safely ignore errors. The file was already
+ // moved. Setting permissions is not mandatory.
+ let dacl = new ctypes.voidptr_t();
+ let sd = new ctypes.voidptr_t();
+ WinFile.GetNamedSecurityInfo(destPath, Const.SE_FILE_OBJECT,
+ Const.DACL_SECURITY_INFORMATION,
+ null /*sidOwner*/, null /*sidGroup*/,
+ dacl.address(), null /*sacl*/,
+ sd.address());
+ // dacl will be set only if the function succeeds.
+ if (!dacl.isNull()) {
+ WinFile.SetNamedSecurityInfo(destPath, Const.SE_FILE_OBJECT,
+ Const.DACL_SECURITY_INFORMATION |
+ Const.UNPROTECTED_DACL_SECURITY_INFORMATION,
+ null /*sidOwner*/, null /*sidGroup*/,
+ dacl, null /*sacl*/);
+ }
+ // sd will be set only if the function succeeds.
+ if (!sd.isNull()) {
+ WinFile.LocalFree(Type.HLOCAL.cast(sd));
+ }
+ };
+
+ /**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} sourcePath Platform-specific path to a directory on
+ * the disk to query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+ File.getAvailableFreeSpace = function Win_getAvailableFreeSpace(sourcePath) {
+ let freeBytesAvailableToUser = new Type.uint64_t.implementation(0);
+ let freeBytesAvailableToUserPtr = freeBytesAvailableToUser.address();
+
+ throw_on_zero("getAvailableFreeSpace",
+ WinFile.GetDiskFreeSpaceEx(sourcePath, freeBytesAvailableToUserPtr, null, null)
+ );
+
+ return freeBytesAvailableToUser.value;
+ };
+
+ /**
+ * A global value used to receive data during time conversions.
+ */
+ let gSystemTime = new Type.SystemTime.implementation();
+ let gSystemTimePtr = gSystemTime.address();
+
+ /**
+ * Utility function: convert a FILETIME to a JavaScript Date.
+ */
+ let FILETIME_to_Date = function FILETIME_to_Date(fileTime, path) {
+ if (fileTime == null) {
+ throw new TypeError("Expecting a non-null filetime");
+ }
+ throw_on_zero("FILETIME_to_Date",
+ WinFile.FileTimeToSystemTime(fileTime.address(),
+ gSystemTimePtr),
+ path);
+ // Windows counts hours, minutes, seconds from UTC,
+ // JS counts from local time, so we need to go through UTC.
+ let utc = Date.UTC(gSystemTime.wYear,
+ gSystemTime.wMonth - 1
+ /*Windows counts months from 1, JS from 0*/,
+ gSystemTime.wDay, gSystemTime.wHour,
+ gSystemTime.wMinute, gSystemTime.wSecond,
+ gSystemTime.wMilliSeconds);
+ return new Date(utc);
+ };
+
+ /**
+ * Utility function: convert Javascript Date to FileTime.
+ *
+ * @param {string} fn Name of the calling function.
+ * @param {Date,number} date The date to be converted. If omitted or null,
+ * then the current date will be used. If numeric, assumed to be the date
+ * in milliseconds since epoch.
+ */
+ let Date_to_FILETIME = function Date_to_FILETIME(fn, date, path) {
+ if (typeof date === "number") {
+ date = new Date(date);
+ } else if (!date) {
+ date = new Date();
+ } else if (typeof date.getUTCFullYear !== "function") {
+ throw new TypeError("|date| parameter of " + fn + " must be a " +
+ "|Date| instance or number");
+ }
+ gSystemTime.wYear = date.getUTCFullYear();
+ // Windows counts months from 1, JS from 0.
+ gSystemTime.wMonth = date.getUTCMonth() + 1;
+ gSystemTime.wDay = date.getUTCDate();
+ gSystemTime.wHour = date.getUTCHours();
+ gSystemTime.wMinute = date.getUTCMinutes();
+ gSystemTime.wSecond = date.getUTCSeconds();
+ gSystemTime.wMilliseconds = date.getUTCMilliseconds();
+ let result = new OS.Shared.Type.FILETIME.implementation();
+ throw_on_zero("Date_to_FILETIME",
+ WinFile.SystemTimeToFileTime(gSystemTimePtr,
+ result.address()),
+ path);
+ return result;
+ };
+
+ /**
+ * Iterate on one directory.
+ *
+ * This iterator will not enter subdirectories.
+ *
+ * @param {string} path The directory upon which to iterate.
+ * @param {*=} options An object that may contain the following field:
+ * @option {string} winPattern Windows file name pattern; if set,
+ * only files matching this pattern are returned.
+ *
+ * @throws {File.Error} If |path| does not represent a directory or
+ * if the directory cannot be iterated.
+ * @constructor
+ */
+ File.DirectoryIterator = function DirectoryIterator(path, options) {
+ exports.OS.Shared.AbstractFile.AbstractIterator.call(this);
+ if (options && options.winPattern) {
+ this._pattern = path + "\\" + options.winPattern;
+ } else {
+ this._pattern = path + "\\*";
+ }
+ this._path = path;
+
+ // Pre-open the first item.
+ this._first = true;
+ this._findData = new Type.FindData.implementation();
+ this._findDataPtr = this._findData.address();
+ this._handle = WinFile.FindFirstFile(this._pattern, this._findDataPtr);
+ if (this._handle == Const.INVALID_HANDLE_VALUE) {
+ let error = ctypes.winLastError;
+ this._findData = null;
+ this._findDataPtr = null;
+ if (error == Const.ERROR_FILE_NOT_FOUND) {
+ // Directory is empty, let's behave as if it were closed
+ SharedAll.LOG("Directory is empty");
+ this._closed = true;
+ this._exists = true;
+ } else if (error == Const.ERROR_PATH_NOT_FOUND) {
+ // Directory does not exist, let's throw if we attempt to walk it
+ SharedAll.LOG("Directory does not exist");
+ this._closed = true;
+ this._exists = false;
+ } else {
+ throw new File.Error("DirectoryIterator", error, this._path);
+ }
+ } else {
+ this._closed = false;
+ this._exists = true;
+ }
+ };
+
+ File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype);
+
+
+ /**
+ * Fetch the next entry in the directory.
+ *
+ * @return null If we have reached the end of the directory.
+ */
+ File.DirectoryIterator.prototype._next = function _next() {
+ // Bailout if the directory does not exist
+ if (!this._exists) {
+ throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
+ }
+ // Bailout if the iterator is closed.
+ if (this._closed) {
+ return null;
+ }
+ // If this is the first entry, we have obtained it already
+ // during construction.
+ if (this._first) {
+ this._first = false;
+ return this._findData;
+ }
+
+ if (WinFile.FindNextFile(this._handle, this._findDataPtr)) {
+ return this._findData;
+ } else {
+ let error = ctypes.winLastError;
+ this.close();
+ if (error == Const.ERROR_NO_MORE_FILES) {
+ return null;
+ } else {
+ throw new File.Error("iter (FindNextFile)", error, this._path);
+ }
+ }
+ },
+
+ /**
+ * Return the next entry in the directory, if any such entry is
+ * available.
+ *
+ * Skip special directories "." and "..".
+ *
+ * @return {File.Entry} The next entry in the directory.
+ * @throws {StopIteration} Once all files in the directory have been
+ * encountered.
+ */
+ File.DirectoryIterator.prototype.next = function next() {
+ // FIXME: If we start supporting "\\?\"-prefixed paths, do not forget
+ // that "." and ".." are absolutely normal file names if _path starts
+ // with such prefix
+ for (let entry = this._next(); entry != null; entry = this._next()) {
+ let name = entry.cFileName.readString();
+ if (name == "." || name == "..") {
+ continue;
+ }
+ return new File.DirectoryIterator.Entry(entry, this._path);
+ }
+ throw StopIteration;
+ };
+
+ File.DirectoryIterator.prototype.close = function close() {
+ if (this._closed) {
+ return;
+ }
+ this._closed = true;
+ if (this._handle) {
+ // We might not have a handle if the iterator is closed
+ // before being used.
+ throw_on_zero("FindClose",
+ WinFile.FindClose(this._handle),
+ this._path);
+ this._handle = null;
+ }
+ };
+
+ /**
+ * Determine whether the directory exists.
+ *
+ * @return {boolean}
+ */
+ File.DirectoryIterator.prototype.exists = function exists() {
+ return this._exists;
+ };
+
+ File.DirectoryIterator.Entry = function Entry(win_entry, parent) {
+ if (!win_entry.dwFileAttributes || !win_entry.ftCreationTime ||
+ !win_entry.ftLastAccessTime || !win_entry.ftLastWriteTime)
+ throw new TypeError();
+
+ // Copy the relevant part of |win_entry| to ensure that
+ // our data is not overwritten prematurely.
+ let isDir = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY);
+ let isSymLink = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT);
+
+ let winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime, this._path);
+ let winLastWriteDate = FILETIME_to_Date(win_entry.ftLastWriteTime, this._path);
+ let winLastAccessDate = FILETIME_to_Date(win_entry.ftLastAccessTime, this._path);
+
+ let name = win_entry.cFileName.readString();
+ if (!name) {
+ throw new TypeError("Empty name");
+ }
+
+ if (!parent) {
+ throw new TypeError("Empty parent");
+ }
+ this._parent = parent;
+
+ let path = Path.join(this._parent, name);
+
+ SysAll.AbstractEntry.call(this, isDir, isSymLink, name,
+ winCreationDate, winLastWriteDate,
+ winLastAccessDate, path);
+ };
+ File.DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
+
+ /**
+ * Return a version of an instance of
+ * File.DirectoryIterator.Entry that can be sent from a worker
+ * thread to the main thread. Note that deserialization is
+ * asymmetric and returns an object with a different
+ * implementation.
+ */
+ File.DirectoryIterator.Entry.toMsg = function toMsg(value) {
+ if (!value instanceof File.DirectoryIterator.Entry) {
+ throw new TypeError("parameter of " +
+ "File.DirectoryIterator.Entry.toMsg must be a " +
+ "File.DirectoryIterator.Entry");
+ }
+ let serialized = {};
+ for (let key in File.DirectoryIterator.Entry.prototype) {
+ serialized[key] = value[key];
+ }
+ return serialized;
+ };
+
+
+ /**
+ * Information on a file.
+ *
+ * To obtain the latest information on a file, use |File.stat|
+ * (for an unopened file) or |File.prototype.stat| (for an
+ * already opened file).
+ *
+ * @constructor
+ */
+ File.Info = function Info(stat, path) {
+ let isDir = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY);
+ let isSymLink = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT);
+
+ let winBirthDate = FILETIME_to_Date(stat.ftCreationTime, this._path);
+ let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime, this._path);
+ let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime, this._path);
+
+ let value = ctypes.UInt64.join(stat.nFileSizeHigh, stat.nFileSizeLow);
+ let size = Type.uint64_t.importFromC(value);
+ let winAttributes = {
+ readOnly: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_READONLY),
+ system: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_SYSTEM),
+ hidden: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_HIDDEN),
+ };
+
+ SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size,
+ winBirthDate, lastAccessDate, lastWriteDate, winAttributes);
+ };
+ File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype);
+
+ /**
+ * Return a version of an instance of File.Info that can be sent
+ * from a worker thread to the main thread. Note that deserialization
+ * is asymmetric and returns an object with a different implementation.
+ */
+ File.Info.toMsg = function toMsg(stat) {
+ if (!stat instanceof File.Info) {
+ throw new TypeError("parameter of File.Info.toMsg must be a File.Info");
+ }
+ let serialized = {};
+ for (let key in File.Info.prototype) {
+ serialized[key] = stat[key];
+ }
+ return serialized;
+ };
+
+
+ /**
+ * Fetch the information on a file.
+ *
+ * Performance note: if you have opened the file already,
+ * method |File.prototype.stat| is generally much faster
+ * than method |File.stat|.
+ *
+ * Platform-specific note: under Windows, if the file is
+ * already opened without sharing of the read capability,
+ * this function will fail.
+ *
+ * @return {File.Information}
+ */
+ File.stat = function stat(path) {
+ let file = File.open(path, FILE_STAT_MODE, FILE_STAT_OPTIONS);
+ try {
+ return file.stat();
+ } finally {
+ file.close();
+ }
+ };
+ // All of the following is required to ensure that File.stat
+ // also works on directories.
+ const FILE_STAT_MODE = {
+ read: true
+ };
+ const FILE_STAT_OPTIONS = {
+ // Directories can be opened neither for reading(!) nor for writing
+ winAccess: 0,
+ // Directories can only be opened with backup semantics(!)
+ winFlags: Const.FILE_FLAG_BACKUP_SEMANTICS,
+ winDisposition: Const.OPEN_EXISTING
+ };
+
+ /**
+ * Set the file's access permission bits.
+ */
+ File.setPermissions = function setPermissions(path, options = {}) {
+ if (!("winAttributes" in options)) {
+ return;
+ }
+ let oldAttributes = WinFile.GetFileAttributes(path);
+ if (oldAttributes == Const.INVALID_FILE_ATTRIBUTES) {
+ throw new File.Error("setPermissions", ctypes.winLastError, path);
+ }
+ let newAttributes = toFileAttributes(options.winAttributes, oldAttributes);
+ throw_on_zero("setPermissions",
+ WinFile.SetFileAttributes(path, newAttributes),
+ path);
+ };
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * Performance note: if you have opened the file already in write mode,
+ * method |File.prototype.stat| is generally much faster
+ * than method |File.stat|.
+ *
+ * Platform-specific note: under Windows, if the file is
+ * already opened without sharing of the write capability,
+ * this function will fail.
+ *
+ * @param {string} path The full name of the file to set the dates for.
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid paramters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.setDates = function setDates(path, accessDate, modificationDate) {
+ let file = File.open(path, FILE_SETDATES_MODE, FILE_SETDATES_OPTIONS);
+ try {
+ return file.setDates(accessDate, modificationDate);
+ } finally {
+ file.close();
+ }
+ };
+ // All of the following is required to ensure that File.setDates
+ // also works on directories.
+ const FILE_SETDATES_MODE = {
+ write: true
+ };
+ const FILE_SETDATES_OPTIONS = {
+ winAccess: Const.GENERIC_WRITE,
+ // Directories can only be opened with backup semantics(!)
+ winFlags: Const.FILE_FLAG_BACKUP_SEMANTICS,
+ winDisposition: Const.OPEN_EXISTING
+ };
+
+ File.read = exports.OS.Shared.AbstractFile.read;
+ File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
+ File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
+ File.makeDir = exports.OS.Shared.AbstractFile.makeDir;
+
+ /**
+ * Remove an existing directory and its contents.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't
+ * exist. |true| by default.
+ * - {boolean} ignorePermissions If |true|, remove the file even when lacking write
+ * permission.
+ *
+ * @throws {OS.File.Error} In case of I/O error, in particular if |path| is
+ * not a directory.
+ */
+ File.removeDir = function(path, options = {}) {
+ // We can't use File.stat here because it will follow the symlink.
+ let attributes = WinFile.GetFileAttributes(path);
+ if (attributes == Const.INVALID_FILE_ATTRIBUTES) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ throw new File.Error("removeEmptyDir", ctypes.winLastError, path);
+ }
+ if (attributes & Const.FILE_ATTRIBUTE_REPARSE_POINT) {
+ // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
+ // directories are directories themselves. OS.File.remove()
+ // will not work for them.
+ OS.File.removeEmptyDir(path, options);
+ return;
+ }
+ exports.OS.Shared.AbstractFile.removeRecursive(path, options);
+ };
+
+ /**
+ * Get the current directory by getCurrentDirectory.
+ */
+ File.getCurrentDirectory = function getCurrentDirectory() {
+ // This function is more complicated than one could hope.
+ //
+ // This is due to two facts:
+ // - the maximal length of a path under Windows is not completely
+ // specified (there is a constant MAX_PATH, but it is quite possible
+ // to create paths that are much larger, see bug 744413);
+ // - if we attempt to call |GetCurrentDirectory| with a buffer that
+ // is too short, it returns the length of the current directory, but
+ // this length might be insufficient by the time we can call again
+ // the function with a larger buffer, in the (unlikely but possible)
+ // case in which the process changes directory to a directory with
+ // a longer name between both calls.
+ //
+ let buffer_size = 4096;
+ while (true) {
+ let array = new (ctypes.ArrayType(ctypes.char16_t, buffer_size))();
+ let expected_size = throw_on_zero("getCurrentDirectory",
+ WinFile.GetCurrentDirectory(buffer_size, array)
+ );
+ if (expected_size <= buffer_size) {
+ return array.readString();
+ }
+ // At this point, we are in a case in which our buffer was not
+ // large enough to hold the name of the current directory.
+ // Consequently, we need to increase the size of the buffer.
+ // Note that, even in crazy scenarios, the loop will eventually
+ // converge, as the length of the paths cannot increase infinitely.
+ buffer_size = expected_size + 1 /* to store \0 */;
+ }
+ };
+
+ /**
+ * Set the current directory by setCurrentDirectory.
+ */
+ File.setCurrentDirectory = function setCurrentDirectory(path) {
+ throw_on_zero("setCurrentDirectory",
+ WinFile.SetCurrentDirectory(path),
+ path);
+ };
+
+ /**
+ * Get/set the current directory by |curDir|.
+ */
+ Object.defineProperty(File, "curDir", {
+ set: function(path) {
+ this.setCurrentDirectory(path);
+ },
+ get: function() {
+ return this.getCurrentDirectory();
+ }
+ }
+ );
+
+ // Utility functions, used for error-handling
+
+ /**
+ * Turn the result of |open| into an Error or a File
+ * @param {number} maybe The result of the |open| operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns the opened file.
+ * @param {string=} path The path of the file.
+ */
+ function error_or_file(maybe, path) {
+ if (maybe == Const.INVALID_HANDLE_VALUE) {
+ throw new File.Error("open", ctypes.winLastError, path);
+ }
+ return new File(maybe, path);
+ }
+
+ /**
+ * Utility function to sort errors represented as "0" from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {number} result The result of the operation that may
+ * represent either an error or a success. If 0, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_zero(operation, result, path) {
+ if (result == 0) {
+ throw new File.Error(operation, ctypes.winLastError, path);
+ }
+ return result;
+ }
+
+ /**
+ * Utility function to sort errors represented as "-1" from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {number} result The result of the operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_negative(operation, result, path) {
+ if (result < 0) {
+ throw new File.Error(operation, ctypes.winLastError, path);
+ }
+ return result;
+ }
+
+ /**
+ * Utility function to sort errors represented as |null| from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {pointer} result The result of the operation that may
+ * represent either an error or a success. If |null|, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_null(operation, result, path) {
+ if (result == null || (result.isNull && result.isNull())) {
+ throw new File.Error(operation, ctypes.winLastError, path);
+ }
+ return result;
+ }
+
+ /**
+ * Helper used by both versions of setPermissions
+ */
+ function toFileAttributes(winAttributes, oldDwAttrs) {
+ if ("readOnly" in winAttributes) {
+ if (winAttributes.readOnly) {
+ oldDwAttrs |= Const.FILE_ATTRIBUTE_READONLY;
+ } else {
+ oldDwAttrs &= ~Const.FILE_ATTRIBUTE_READONLY;
+ }
+ }
+ if ("system" in winAttributes) {
+ if (winAttributes.system) {
+ oldDwAttrs |= Const.FILE_ATTRIBUTE_SYSTEM;
+ } else {
+ oldDwAttrs &= ~Const.FILE_ATTRIBUTE_SYSTEM;
+ }
+ }
+ if ("hidden" in winAttributes) {
+ if (winAttributes.hidden) {
+ oldDwAttrs |= Const.FILE_ATTRIBUTE_HIDDEN;
+ } else {
+ oldDwAttrs &= ~Const.FILE_ATTRIBUTE_HIDDEN;
+ }
+ }
+ return oldDwAttrs;
+ }
+
+ File.Win = exports.OS.Win.File;
+ File.Error = SysAll.Error;
+ exports.OS.File = File;
+ exports.OS.Shared.Type = Type;
+
+ Object.defineProperty(File, "POS_START", { value: SysAll.POS_START });
+ Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT });
+ Object.defineProperty(File, "POS_END", { value: SysAll.POS_END });
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/ospath.jsm b/toolkit/components/osfile/modules/ospath.jsm
new file mode 100644
index 0000000000..68bbe43454
--- /dev/null
+++ b/toolkit/components/osfile/modules/ospath.jsm
@@ -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/. */
+
+/**
+ * Handling native paths.
+ *
+ * This module contains a number of functions destined to simplify
+ * working with native paths through a cross-platform API. Functions
+ * of this module will only work with the following assumptions:
+ *
+ * - paths are valid;
+ * - paths are defined with one of the grammars that this module can
+ * parse (see later);
+ * - all path concatenations go through function |join|.
+ */
+
+"use strict";
+
+if (typeof Components == "undefined") {
+ let Path;
+ if (OS.Constants.Win) {
+ Path = require("resource://gre/modules/osfile/ospath_win.jsm");
+ } else {
+ Path = require("resource://gre/modules/osfile/ospath_unix.jsm");
+ }
+ module.exports = Path;
+} else {
+ let Cu = Components.utils;
+ let Scope = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", Scope);
+
+ let Path = {};
+ if (Scope.OS.Constants.Win) {
+ Cu.import("resource://gre/modules/osfile/ospath_win.jsm", Path);
+ } else {
+ Cu.import("resource://gre/modules/osfile/ospath_unix.jsm", Path);
+ }
+
+ this.EXPORTED_SYMBOLS = [];
+ for (let k in Path) {
+ this.EXPORTED_SYMBOLS.push(k);
+ this[k] = Path[k];
+ }
+}
diff --git a/toolkit/components/osfile/modules/ospath_unix.jsm b/toolkit/components/osfile/modules/ospath_unix.jsm
new file mode 100644
index 0000000000..1d574baed2
--- /dev/null
+++ b/toolkit/components/osfile/modules/ospath_unix.jsm
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Handling native paths.
+ *
+ * This module contains a number of functions destined to simplify
+ * working with native paths through a cross-platform API. Functions
+ * of this module will only work with the following assumptions:
+ *
+ * - paths are valid;
+ * - paths are defined with one of the grammars that this module can
+ * parse (see later);
+ * - all path concatenations go through function |join|.
+ */
+
+"use strict";
+
+// Boilerplate used to be able to import this module both from the main
+// thread and from worker threads.
+if (typeof Components != "undefined") {
+ Components.utils.importGlobalProperties(["URL"]);
+ // Global definition of |exports|, to keep everybody happy.
+ // In non-main thread, |exports| is provided by the module
+ // loader.
+ this.exports = {};
+} else if (typeof module == "undefined" || typeof exports == "undefined") {
+ throw new Error("Please load this module using require()");
+}
+
+var EXPORTED_SYMBOLS = [
+ "basename",
+ "dirname",
+ "join",
+ "normalize",
+ "split",
+ "toFileURI",
+ "fromFileURI",
+];
+
+/**
+ * Return the final part of the path.
+ * The final part of the path is everything after the last "/".
+ */
+var basename = function(path) {
+ return path.slice(path.lastIndexOf("/") + 1);
+};
+exports.basename = basename;
+
+/**
+ * Return the directory part of the path.
+ * The directory part of the path is everything before the last
+ * "/". If the last few characters of this part are also "/",
+ * they are ignored.
+ *
+ * If the path contains no directory, return ".".
+ */
+var dirname = function(path) {
+ let index = path.lastIndexOf("/");
+ if (index == -1) {
+ return ".";
+ }
+ while (index >= 0 && path[index] == "/") {
+ --index;
+ }
+ return path.slice(0, index + 1);
+};
+exports.dirname = dirname;
+
+/**
+ * Join path components.
+ * This is the recommended manner of getting the path of a file/subdirectory
+ * in a directory.
+ *
+ * Example: Obtaining $TMP/foo/bar in an OS-independent manner
+ * var tmpDir = OS.Constants.Path.tmpDir;
+ * var path = OS.Path.join(tmpDir, "foo", "bar");
+ *
+ * Under Unix, this will return "/tmp/foo/bar".
+ *
+ * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the
+ * same as `OS.Path.join("foo", "bar")`.
+ */
+var join = function(...path) {
+ // If there is a path that starts with a "/", eliminate everything before
+ let paths = [];
+ for (let subpath of path) {
+ if (subpath == null) {
+ throw new TypeError("invalid path component");
+ }
+ if (subpath.length == 0) {
+ continue;
+ } else if (subpath[0] == "/") {
+ paths = [subpath];
+ } else {
+ paths.push(subpath);
+ }
+ }
+ return paths.join("/");
+};
+exports.join = join;
+
+/**
+ * Normalize a path by removing any unneeded ".", "..", "//".
+ */
+var normalize = function(path) {
+ let stack = [];
+ let absolute;
+ if (path.length >= 0 && path[0] == "/") {
+ absolute = true;
+ } else {
+ absolute = false;
+ }
+ path.split("/").forEach(function(v) {
+ switch (v) {
+ case "": case ".":// fallthrough
+ break;
+ case "..":
+ if (stack.length == 0) {
+ if (absolute) {
+ throw new Error("Path is ill-formed: attempting to go past root");
+ } else {
+ stack.push("..");
+ }
+ } else {
+ if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ }
+ break;
+ default:
+ stack.push(v);
+ }
+ });
+ let string = stack.join("/");
+ return absolute ? "/" + string : string;
+};
+exports.normalize = normalize;
+
+/**
+ * Return the components of a path.
+ * You should generally apply this function to a normalized path.
+ *
+ * @return {{
+ * {bool} absolute |true| if the path is absolute, |false| otherwise
+ * {array} components the string components of the path
+ * }}
+ *
+ * Other implementations may add additional OS-specific informations.
+ */
+var split = function(path) {
+ return {
+ absolute: path.length && path[0] == "/",
+ components: path.split("/")
+ };
+};
+exports.split = split;
+
+/**
+ * Returns the file:// URI file path of the given local file path.
+ */
+// The case of %3b is designed to match Services.io, but fundamentally doesn't matter.
+var toFileURIExtraEncodings = {';': '%3b', '?': '%3F', '#': '%23'};
+var toFileURI = function toFileURI(path) {
+ // Per https://url.spec.whatwg.org we should not encode [] in the path
+ let dontNeedEscaping = {'%5B': '[', '%5D': ']'};
+ let uri = encodeURI(this.normalize(path)).replace(/%(5B|5D)/gi,
+ match => dontNeedEscaping[match]);
+
+ // add a prefix, and encodeURI doesn't escape a few characters that we do
+ // want to escape, so fix that up
+ let prefix = "file://";
+ uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]);
+
+ return uri;
+};
+exports.toFileURI = toFileURI;
+
+/**
+ * Returns the local file path from a given file URI.
+ */
+var fromFileURI = function fromFileURI(uri) {
+ let url = new URL(uri);
+ if (url.protocol != 'file:') {
+ throw new Error("fromFileURI expects a file URI");
+ }
+ let path = this.normalize(decodeURIComponent(url.pathname));
+ return path;
+};
+exports.fromFileURI = fromFileURI;
+
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/ospath_win.jsm b/toolkit/components/osfile/modules/ospath_win.jsm
new file mode 100644
index 0000000000..31a87b1157
--- /dev/null
+++ b/toolkit/components/osfile/modules/ospath_win.jsm
@@ -0,0 +1,373 @@
+/**
+ * Handling native paths.
+ *
+ * This module contains a number of functions destined to simplify
+ * working with native paths through a cross-platform API. Functions
+ * of this module will only work with the following assumptions:
+ *
+ * - paths are valid;
+ * - paths are defined with one of the grammars that this module can
+ * parse (see later);
+ * - all path concatenations go through function |join|.
+ *
+ * Limitations of this implementation.
+ *
+ * Windows supports 6 distinct grammars for paths. For the moment, this
+ * implementation supports the following subset:
+ *
+ * - drivename:backslash-separated components
+ * - backslash-separated components
+ * - \\drivename\ followed by backslash-separated components
+ *
+ * Additionally, |normalize| can convert a path containing slash-
+ * separated components to a path containing backslash-separated
+ * components.
+ */
+
+"use strict";
+
+// Boilerplate used to be able to import this module both from the main
+// thread and from worker threads.
+if (typeof Components != "undefined") {
+ Components.utils.importGlobalProperties(["URL"]);
+ // Global definition of |exports|, to keep everybody happy.
+ // In non-main thread, |exports| is provided by the module
+ // loader.
+ this.exports = {};
+} else if (typeof module == "undefined" || typeof exports == "undefined") {
+ throw new Error("Please load this module using require()");
+}
+
+var EXPORTED_SYMBOLS = [
+ "basename",
+ "dirname",
+ "join",
+ "normalize",
+ "split",
+ "winGetDrive",
+ "winIsAbsolute",
+ "toFileURI",
+ "fromFileURI",
+];
+
+/**
+ * Return the final part of the path.
+ * The final part of the path is everything after the last "\\".
+ */
+var basename = function(path) {
+ if (path.startsWith("\\\\")) {
+ // UNC-style path
+ let index = path.lastIndexOf("\\");
+ if (index != 1) {
+ return path.slice(index + 1);
+ }
+ return ""; // Degenerate case
+ }
+ return path.slice(Math.max(path.lastIndexOf("\\"),
+ path.lastIndexOf(":")) + 1);
+};
+exports.basename = basename;
+
+/**
+ * Return the directory part of the path.
+ *
+ * If the path contains no directory, return the drive letter,
+ * or "." if the path contains no drive letter or if option
+ * |winNoDrive| is set.
+ *
+ * Otherwise, return everything before the last backslash,
+ * including the drive/server name.
+ *
+ *
+ * @param {string} path The path.
+ * @param {*=} options Platform-specific options controlling the behavior
+ * of this function. This implementation supports the following options:
+ * - |winNoDrive| If |true|, also remove the letter from the path name.
+ */
+var dirname = function(path, options) {
+ let noDrive = (options && options.winNoDrive);
+
+ // Find the last occurrence of "\\"
+ let index = path.lastIndexOf("\\");
+ if (index == -1) {
+ // If there is no directory component...
+ if (!noDrive) {
+ // Return the drive path if possible, falling back to "."
+ return this.winGetDrive(path) || ".";
+ } else {
+ // Or just "."
+ return ".";
+ }
+ }
+
+ if (index == 1 && path.charAt(0) == "\\") {
+ // The path is reduced to a UNC drive
+ if (noDrive) {
+ return ".";
+ } else {
+ return path;
+ }
+ }
+
+ // Ignore any occurrence of "\\: immediately before that one
+ while (index >= 0 && path[index] == "\\") {
+ --index;
+ }
+
+ // Compute what is left, removing the drive name if necessary
+ let start;
+ if (noDrive) {
+ start = (this.winGetDrive(path) || "").length;
+ } else {
+ start = 0;
+ }
+ return path.slice(start, index + 1);
+};
+exports.dirname = dirname;
+
+/**
+ * Join path components.
+ * This is the recommended manner of getting the path of a file/subdirectory
+ * in a directory.
+ *
+ * Example: Obtaining $TMP/foo/bar in an OS-independent manner
+ * var tmpDir = OS.Constants.Path.tmpDir;
+ * var path = OS.Path.join(tmpDir, "foo", "bar");
+ *
+ * Under Windows, this will return "$TMP\foo\bar".
+ *
+ * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the
+ * same as `OS.Path.join("foo", "bar")`.
+ */
+var join = function(...path) {
+ let paths = [];
+ let root;
+ let absolute = false;
+ for (let subpath of path) {
+ if (subpath == null) {
+ throw new TypeError("invalid path component");
+ }
+ if (subpath == "") {
+ continue;
+ }
+ let drive = this.winGetDrive(subpath);
+ if (drive) {
+ root = drive;
+ let component = trimBackslashes(subpath.slice(drive.length));
+ if (component) {
+ paths = [component];
+ } else {
+ paths = [];
+ }
+ absolute = true;
+ } else if (this.winIsAbsolute(subpath)) {
+ paths = [trimBackslashes(subpath)];
+ absolute = true;
+ } else {
+ paths.push(trimBackslashes(subpath));
+ }
+ }
+ let result = "";
+ if (root) {
+ result += root;
+ }
+ if (absolute) {
+ result += "\\";
+ }
+ result += paths.join("\\");
+ return result;
+};
+exports.join = join;
+
+/**
+ * Return the drive name of a path, or |null| if the path does
+ * not contain a drive name.
+ *
+ * Drive name appear either as "DriveName:..." (the return drive
+ * name includes the ":") or "\\\\DriveName..." (the returned drive name
+ * includes "\\\\").
+ */
+var winGetDrive = function(path) {
+ if (path == null) {
+ throw new TypeError("path is invalid");
+ }
+
+ if (path.startsWith("\\\\")) {
+ // UNC path
+ if (path.length == 2) {
+ return null;
+ }
+ let index = path.indexOf("\\", 2);
+ if (index == -1) {
+ return path;
+ }
+ return path.slice(0, index);
+ }
+ // Non-UNC path
+ let index = path.indexOf(":");
+ if (index <= 0) return null;
+ return path.slice(0, index + 1);
+};
+exports.winGetDrive = winGetDrive;
+
+/**
+ * Return |true| if the path is absolute, |false| otherwise.
+ *
+ * We consider that a path is absolute if it starts with "\\"
+ * or "driveletter:\\".
+ */
+var winIsAbsolute = function(path) {
+ let index = path.indexOf(":");
+ return path.length > index + 1 && path[index + 1] == "\\";
+};
+exports.winIsAbsolute = winIsAbsolute;
+
+/**
+ * Normalize a path by removing any unneeded ".", "..", "\\".
+ * Also convert any "/" to a "\\".
+ */
+var normalize = function(path) {
+ let stack = [];
+
+ if (!path.startsWith("\\\\")) {
+ // Normalize "/" to "\\"
+ path = path.replace(/\//g, "\\");
+ }
+
+ // Remove the drive (we will put it back at the end)
+ let root = this.winGetDrive(path);
+ if (root) {
+ path = path.slice(root.length);
+ }
+
+ // Remember whether we need to restore a leading "\\" or drive name.
+ let absolute = this.winIsAbsolute(path);
+
+ // And now, fill |stack| from the components,
+ // popping whenever there is a ".."
+ path.split("\\").forEach(function loop(v) {
+ switch (v) {
+ case "": case ".": // Ignore
+ break;
+ case "..":
+ if (stack.length == 0) {
+ if (absolute) {
+ throw new Error("Path is ill-formed: attempting to go past root");
+ } else {
+ stack.push("..");
+ }
+ } else {
+ if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ }
+ break;
+ default:
+ stack.push(v);
+ }
+ });
+
+ // Put everything back together
+ let result = stack.join("\\");
+ if (absolute || root) {
+ result = "\\" + result;
+ }
+ if (root) {
+ result = root + result;
+ }
+ return result;
+};
+exports.normalize = normalize;
+
+/**
+ * Return the components of a path.
+ * You should generally apply this function to a normalized path.
+ *
+ * @return {{
+ * {bool} absolute |true| if the path is absolute, |false| otherwise
+ * {array} components the string components of the path
+ * {string?} winDrive the drive or server for this path
+ * }}
+ *
+ * Other implementations may add additional OS-specific informations.
+ */
+var split = function(path) {
+ return {
+ absolute: this.winIsAbsolute(path),
+ winDrive: this.winGetDrive(path),
+ components: path.split("\\")
+ };
+};
+exports.split = split;
+
+/**
+ * Return the file:// URI file path of the given local file path.
+ */
+// The case of %3b is designed to match Services.io, but fundamentally doesn't matter.
+var toFileURIExtraEncodings = {';': '%3b', '?': '%3F', '#': '%23'};
+var toFileURI = function toFileURI(path) {
+ // URI-escape forward slashes and convert backward slashes to forward
+ path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F');
+ // Per https://url.spec.whatwg.org we should not encode [] in the path
+ let dontNeedEscaping = {'%5B': '[', '%5D': ']'};
+ let uri = encodeURI(path).replace(/%(5B|5D)/gi,
+ match => dontNeedEscaping[match]);
+
+ // add a prefix, and encodeURI doesn't escape a few characters that we do
+ // want to escape, so fix that up
+ let prefix = "file:///";
+ uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]);
+
+ // turn e.g., file:///C: into file:///C:/
+ if (uri.charAt(uri.length - 1) === ':') {
+ uri += "/"
+ }
+
+ return uri;
+};
+exports.toFileURI = toFileURI;
+
+/**
+ * Returns the local file path from a given file URI.
+ */
+var fromFileURI = function fromFileURI(uri) {
+ let url = new URL(uri);
+ if (url.protocol != 'file:') {
+ throw new Error("fromFileURI expects a file URI");
+ }
+
+ // strip leading slash, since Windows paths don't start with one
+ uri = url.pathname.substr(1);
+
+ let path = decodeURI(uri);
+ // decode a few characters where URL's parsing is overzealous
+ path = path.replace(/%(3b|3f|23)/gi,
+ match => decodeURIComponent(match));
+ path = this.normalize(path);
+
+ // this.normalize() does not remove the trailing slash if the path
+ // component is a drive letter. eg. 'C:\'' will not get normalized.
+ if (path.endsWith(":\\")) {
+ path = path.substr(0, path.length - 1);
+ }
+ return this.normalize(path);
+};
+exports.fromFileURI = fromFileURI;
+
+/**
+* Utility function: Remove any leading/trailing backslashes
+* from a string.
+*/
+var trimBackslashes = function trimBackslashes(string) {
+ return string.replace(/^\\+|\\+$/g,'');
+};
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/moz.build b/toolkit/components/osfile/moz.build
new file mode 100644
index 0000000000..d17da2d60f
--- /dev/null
+++ b/toolkit/components/osfile/moz.build
@@ -0,0 +1,35 @@
+# -*- 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 += [
+ 'modules',
+]
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/mochi/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+SOURCES += [
+ 'NativeOSFileInternals.cpp',
+]
+
+XPIDL_MODULE = 'toolkit_osfile'
+
+XPIDL_SOURCES += [
+ 'nsINativeOSFileInternals.idl',
+]
+
+EXPORTS.mozilla += [
+ 'NativeOSFileInternals.h',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'osfile.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'OS.File')
diff --git a/toolkit/components/osfile/nsINativeOSFileInternals.idl b/toolkit/components/osfile/nsINativeOSFileInternals.idl
new file mode 100644
index 0000000000..c1bf8be147
--- /dev/null
+++ b/toolkit/components/osfile/nsINativeOSFileInternals.idl
@@ -0,0 +1,93 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=40: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+/**
+ * The result of a successful asynchronous operation.
+ */
+[scriptable, builtinclass, uuid(08B4CF29-3D65-4E79-B522-A694C322ED07)]
+interface nsINativeOSFileResult: nsISupports
+{
+ /**
+ * The actual value produced by the operation.
+ *
+ * Actual type of this value depends on the options passed to the
+ * operation.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval result;
+
+ /**
+ * Delay between when the operation was requested on the main thread and
+ * when the operation was started off main thread.
+ */
+ readonly attribute double dispatchDurationMS;
+
+ /**
+ * Duration of the off main thread execution.
+ */
+ readonly attribute double executionDurationMS;
+};
+
+/**
+ * A callback invoked in case of success.
+ */
+[scriptable, function, uuid(2C1922CA-CA1B-4099-8B61-EC23CFF49412)]
+interface nsINativeOSFileSuccessCallback: nsISupports
+{
+ void complete(in nsINativeOSFileResult result);
+};
+
+/**
+ * A callback invoked in case of error.
+ */
+[scriptable, function, uuid(F612E0FC-6736-4D24-AA50-FD661B3B40B6)]
+interface nsINativeOSFileErrorCallback: nsISupports
+{
+ /**
+ * @param operation The name of the failed operation. Provided to aid
+ * debugging only, may change without notice.
+ * @param OSstatus The OS status of the operation (errno under Unix,
+ * GetLastError under Windows).
+ */
+ void complete(in ACString operation, in long OSstatus);
+};
+
+/**
+ * A service providing native implementations of some of the features
+ * of OS.File.
+ */
+[scriptable, builtinclass, uuid(913362AD-1526-4623-9E6B-A2EB08AFBBB9)]
+interface nsINativeOSFileInternalsService: nsISupports
+{
+ /**
+ * Implementation of OS.File.read
+ *
+ * @param path The absolute path to the file to read.
+ * @param options An object that may contain some of the following fields
+ * - {number} bytes The maximal number of bytes to read.
+ * - {string} encoding If provided, return the result as a string, decoded
+ * using this encoding. Otherwise, pass the result as an ArrayBuffer.
+ * Invalid encodings cause onError to be called with the platform-specific
+ * "invalid argument" constant.
+ * - {string} compression Unimplemented at the moment.
+ * @param onSuccess The success callback.
+ * @param onError The error callback.
+ */
+ [implicit_jscontext]
+ void read(in AString path, in jsval options,
+ in nsINativeOSFileSuccessCallback onSuccess,
+ in nsINativeOSFileErrorCallback onError);
+};
+
+
+%{ C++
+
+#define NATIVE_OSFILE_INTERNALS_SERVICE_CID {0x63A69303,0x8A64,0x45A9,{0x84, 0x8C, 0xD4, 0xE2, 0x79, 0x27, 0x94, 0xE6}}
+#define NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID "@mozilla.org/toolkit/osfile/native-internals;1"
+
+%}
diff --git a/toolkit/components/osfile/osfile.jsm b/toolkit/components/osfile/osfile.jsm
new file mode 100644
index 0000000000..20f21591ad
--- /dev/null
+++ b/toolkit/components/osfile/osfile.jsm
@@ -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/. */
+
+/**
+ * Common front for various implementations of OS.File
+ */
+
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = ["OS"];
+ Components.utils.import("resource://gre/modules/osfile/osfile_async_front.jsm", this);
+} else {
+ // At this stage, we need to import all sources at once to avoid
+ // a unique failure on tbpl + talos that seems caused by a
+ // what looks like a nested event loop bug (see bug 794091).
+#ifdef XP_WIN
+ importScripts(
+ "resource://gre/modules/workers/require.js",
+ "resource://gre/modules/osfile/osfile_win_back.jsm",
+ "resource://gre/modules/osfile/osfile_shared_front.jsm",
+ "resource://gre/modules/osfile/osfile_win_front.jsm"
+ );
+#else
+ importScripts(
+ "resource://gre/modules/workers/require.js",
+ "resource://gre/modules/osfile/osfile_unix_back.jsm",
+ "resource://gre/modules/osfile/osfile_shared_front.jsm",
+ "resource://gre/modules/osfile/osfile_unix_front.jsm"
+ );
+#endif
+ OS.Path = require("resource://gre/modules/osfile/ospath.jsm");
+}
diff --git a/toolkit/components/osfile/tests/mochi/.eslintrc.js b/toolkit/components/osfile/tests/mochi/.eslintrc.js
new file mode 100644
index 0000000000..8c0f4f574c
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/osfile/tests/mochi/chrome.ini b/toolkit/components/osfile/tests/mochi/chrome.ini
new file mode 100644
index 0000000000..1da463316e
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ main_test_osfile_async.js
+ worker_handler.js
+ worker_test_osfile_comms.js
+ worker_test_osfile_front.js
+ worker_test_osfile_shared.js
+ worker_test_osfile_unix.js
+ worker_test_osfile_win.js
+
+[test_osfile_async.xul]
+[test_osfile_back.xul]
+[test_osfile_comms.xul]
+[test_osfile_front.xul]
diff --git a/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
new file mode 100644
index 0000000000..b940a032a2
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
@@ -0,0 +1,443 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/AsyncShutdown.jsm");
+
+// The following are used to compare against a well-tested reference
+// implementation of file I/O.
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var myok = ok;
+var myis = is;
+var myinfo = info;
+var myisnot = isnot;
+
+var isPromise = function ispromise(value) {
+ return value != null && typeof value == "object" && "then" in value;
+};
+
+var maketest = function(prefix, test) {
+ let utils = {
+ ok: function ok(t, m) {
+ myok(t, prefix + ": " + m);
+ },
+ is: function is(l, r, m) {
+ myis(l, r, prefix + ": " + m);
+ },
+ isnot: function isnot(l, r, m) {
+ myisnot(l, r, prefix + ": " + m);
+ },
+ info: function info(m) {
+ myinfo(prefix + ": " + m);
+ },
+ fail: function fail(m) {
+ utils.ok(false, m);
+ },
+ okpromise: function okpromise(t, m) {
+ return t.then(
+ function onSuccess() {
+ util.ok(true, m);
+ },
+ function onFailure() {
+ util.ok(false, m);
+ }
+ );
+ }
+ };
+ return function runtest() {
+ utils.info("Entering");
+ try {
+ let result = test.call(this, utils);
+ if (!isPromise(result)) {
+ throw new TypeError("The test did not return a promise");
+ }
+ utils.info("This was a promise");
+ // The test returns a promise
+ result = result.then(function test_complete() {
+ utils.info("Complete");
+ }, function catch_uncaught_errors(err) {
+ utils.fail("Uncaught error " + err);
+ if (err && typeof err == "object" && "message" in err) {
+ utils.fail("(" + err.message + ")");
+ }
+ if (err && typeof err == "object" && "stack" in err) {
+ utils.fail("at " + err.stack);
+ }
+ });
+ return result;
+ } catch (x) {
+ utils.fail("Error " + x + " at " + x.stack);
+ return null;
+ }
+ };
+};
+
+/**
+ * Fetch asynchronously the contents of a file using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} path The _absolute_ path to the file.
+ * @return {promise}
+ * @resolves {string} The contents of the file.
+ */
+var reference_fetch_file = function reference_fetch_file(path, test) {
+ test.info("Fetching file " + path);
+ let promise = Promise.defer();
+ let file = new FileUtils.File(path);
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function(stream, status) {
+ if (!Components.isSuccessCode(status)) {
+ promise.reject(status);
+ return;
+ }
+ let result, reject;
+ try {
+ result = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (x) {
+ reject = x;
+ }
+ stream.close();
+ if (reject) {
+ promise.reject(reject);
+ } else {
+ promise.resolve(result);
+ }
+ });
+
+ return promise.promise;
+};
+
+/**
+ * Compare asynchronously the contents two files using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} a The _absolute_ path to the first file.
+ * @param {string} b The _absolute_ path to the second file.
+ *
+ * @resolves {null}
+ */
+var reference_compare_files = function reference_compare_files(a, b, test) {
+ test.info("Comparing files " + a + " and " + b);
+ let a_contents = yield reference_fetch_file(a, test);
+ let b_contents = yield reference_fetch_file(b, test);
+ is(a_contents, b_contents, "Contents of files " + a + " and " + b + " match");
+};
+
+var reference_dir_contents = function reference_dir_contents(path) {
+ let result = [];
+ let entries = new FileUtils.File(path).directoryEntries;
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Components.interfaces.nsILocalFile);
+ result.push(entry.path);
+ }
+ return result;
+};
+
+// Set/Unset OS.Shared.DEBUG, OS.Shared.TEST and a console listener.
+function toggleDebugTest (pref, consoleListener) {
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref);
+ Services.console[pref ? "registerListener" : "unregisterListener"](
+ consoleListener);
+}
+
+var test = maketest("Main", function main(test) {
+ return Task.spawn(function() {
+ SimpleTest.waitForExplicitFinish();
+ yield test_stat();
+ yield test_debug();
+ yield test_info_features_detect();
+ yield test_position();
+ yield test_iter();
+ yield test_exists();
+ yield test_debug_test();
+ info("Test is over");
+ SimpleTest.finish();
+ });
+});
+
+/**
+ * A file that we know exists and that can be used for reading.
+ */
+var EXISTING_FILE = OS.Path.join("chrome", "toolkit", "components",
+ "osfile", "tests", "mochi", "main_test_osfile_async.js");
+
+/**
+ * Test OS.File.stat and OS.File.prototype.stat
+ */
+var test_stat = maketest("stat", function stat(test) {
+ return Task.spawn(function() {
+ // Open a file and stat it
+ let file = yield OS.File.open(EXISTING_FILE);
+ let stat1;
+
+ try {
+ test.info("Stating file");
+ stat1 = yield file.stat();
+ test.ok(true, "stat has worked " + stat1);
+ test.ok(stat1, "stat is not empty");
+ } finally {
+ yield file.close();
+ }
+
+ // Stat the same file without opening it
+ test.info("Stating a file without opening it");
+ let stat2 = yield OS.File.stat(EXISTING_FILE);
+ test.ok(true, "stat 2 has worked " + stat2);
+ test.ok(stat2, "stat 2 is not empty");
+ for (let key in stat2) {
+ test.is("" + stat1[key], "" + stat2[key], "Stat field " + key + "is the same");
+ }
+ });
+});
+
+/**
+ * Test feature detection using OS.File.Info.prototype on main thread
+ */
+var test_info_features_detect = maketest("features_detect", function features_detect(test) {
+ return Task.spawn(function() {
+ if (OS.Constants.Win) {
+ // see if winBirthDate is defined
+ if ("winBirthDate" in OS.File.Info.prototype) {
+ test.ok(true, "winBirthDate is defined");
+ } else {
+ test.fail("winBirthDate not defined though we are under Windows");
+ }
+ } else if (OS.Constants.libc) {
+ // see if unixGroup is defined
+ if ("unixGroup" in OS.File.Info.prototype) {
+ test.ok(true, "unixGroup is defined");
+ } else {
+ test.fail("unixGroup is not defined though we are under Unix");
+ }
+ }
+ });
+});
+
+/**
+ * Test file.{getPosition, setPosition}
+ */
+var test_position = maketest("position", function position(test) {
+ return Task.spawn(function() {
+ let file = yield OS.File.open(EXISTING_FILE);
+
+ try {
+ let view = yield file.read();
+ test.info("First batch of content read");
+ let CHUNK_SIZE = 178;// An arbitrary number of bytes to read from the file
+ let pos = yield file.getPosition();
+ test.info("Obtained position");
+ test.is(pos, view.byteLength, "getPosition returned the end of the file");
+ pos = yield file.setPosition(-CHUNK_SIZE, OS.File.POS_END);
+ test.info("Changed position");
+ test.is(pos, view.byteLength - CHUNK_SIZE, "setPosition returned the correct position");
+
+ let view2 = yield file.read();
+ test.info("Read the end of the file");
+ for (let i = 0; i < CHUNK_SIZE; ++i) {
+ if (view2[i] != view[i + view.byteLength - CHUNK_SIZE]) {
+ test.is(view2[i], view[i], "setPosition put us in the right position");
+ }
+ }
+ } finally {
+ yield file.close();
+ }
+ });
+});
+
+/**
+ * Test OS.File.prototype.{DirectoryIterator}
+ */
+var test_iter = maketest("iter", function iter(test) {
+ return Task.spawn(function() {
+ let currentDir = yield OS.File.getCurrentDirectory();
+
+ // Trivial walks through the directory
+ test.info("Preparing iteration");
+ let iterator = new OS.File.DirectoryIterator(currentDir);
+ let temporary_file_name = OS.Path.join(currentDir, "empty-temporary-file.tmp");
+ try {
+ yield OS.File.remove(temporary_file_name);
+ } catch (err) {
+ // Ignore errors removing file
+ }
+ let allFiles1 = yield iterator.nextBatch();
+ test.info("Obtained all files through nextBatch");
+ test.isnot(allFiles1.length, 0, "There is at least one file");
+ test.isnot(allFiles1[0].path, null, "Files have a path");
+
+ // Ensure that we have the same entries with |reference_dir_contents|
+ let referenceEntries = new Set();
+ for (let entry of reference_dir_contents(currentDir)) {
+ referenceEntries.add(entry);
+ }
+ test.is(referenceEntries.size, allFiles1.length, "All the entries in the directory have been listed");
+ for (let entry of allFiles1) {
+ test.ok(referenceEntries.has(entry.path), "File " + entry.path + " effectively exists");
+ // Ensure that we have correct isDir and isSymLink
+ // Current directory is {objdir}/_tests/testing/mochitest/, assume it has some dirs and symlinks.
+ var f = new FileUtils.File(entry.path);
+ test.is(entry.isDir, f.isDirectory(), "Get file " + entry.path + " isDir correctly");
+ test.is(entry.isSymLink, f.isSymlink(), "Get file " + entry.path + " isSymLink correctly");
+ }
+
+ yield iterator.close();
+ test.info("Closed iterator");
+
+ test.info("Double closing DirectoryIterator");
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ yield iterator.close();
+ yield iterator.close(); //double closing |DirectoryIterator|
+ test.ok(true, "|DirectoryIterator| was closed twice successfully");
+
+ let allFiles2 = [];
+ let i = 0;
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ yield iterator.forEach(function(entry, index) {
+ test.is(i++, index, "Getting the correct index");
+ allFiles2.push(entry);
+ });
+ test.info("Obtained all files through forEach");
+ is(allFiles1.length, allFiles2.length, "Both runs returned the same number of files");
+ for (let i = 0; i < allFiles1.length; ++i) {
+ if (allFiles1[i].path != allFiles2[i].path) {
+ test.is(allFiles1[i].path, allFiles2[i].path, "Both runs return the same files");
+ break;
+ }
+ }
+
+ // Testing batch iteration + whether an iteration can be stopped early
+ let BATCH_LENGTH = 10;
+ test.info("Getting some files through nextBatch");
+ yield iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ let someFiles1 = yield iterator.nextBatch(BATCH_LENGTH);
+ let someFiles2 = yield iterator.nextBatch(BATCH_LENGTH);
+ yield iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ yield iterator.forEach(function cb(entry, index, iterator) {
+ if (index < BATCH_LENGTH) {
+ test.is(entry.path, someFiles1[index].path, "Both runs return the same files (part 1)");
+ } else if (index < 2*BATCH_LENGTH) {
+ test.is(entry.path, someFiles2[index - BATCH_LENGTH].path, "Both runs return the same files (part 2)");
+ } else if (index == 2 * BATCH_LENGTH) {
+ test.info("Attempting to stop asynchronous forEach");
+ return iterator.close();
+ } else {
+ test.fail("Can we stop an asynchronous forEach? " + index);
+ }
+ return null;
+ });
+ yield iterator.close();
+
+ // Ensuring that we find new files if they appear
+ let file = yield OS.File.open(temporary_file_name, { write: true } );
+ file.close();
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ try {
+ let files = yield iterator.nextBatch();
+ is(files.length, allFiles1.length + 1, "The directory iterator has noticed the new file");
+ let exists = yield iterator.exists();
+ test.ok(exists, "After nextBatch, iterator detects that the directory exists");
+ } finally {
+ yield iterator.close();
+ }
+
+ // Ensuring that opening a non-existing directory fails consistently
+ // once iteration starts.
+ try {
+ iterator = null;
+ iterator = new OS.File.DirectoryIterator("/I do not exist");
+ let exists = yield iterator.exists();
+ test.ok(!exists, "Before any iteration, iterator detects that the directory doesn't exist");
+ let exn = null;
+ try {
+ yield iterator.next();
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ exn = ex;
+ let exists = yield iterator.exists();
+ test.ok(!exists, "After one iteration, iterator detects that the directory doesn't exist");
+ }
+ test.ok(exn, "Iterating through a directory that does not exist has failed with becauseNoSuchFile");
+ } finally {
+ if (iterator) {
+ iterator.close();
+ }
+ }
+ test.ok(!!iterator, "The directory iterator for a non-existing directory was correctly created");
+ });
+});
+
+/**
+ * Test OS.File.prototype.{exists}
+ */
+var test_exists = maketest("exists", function exists(test) {
+ return Task.spawn(function() {
+ let fileExists = yield OS.File.exists(EXISTING_FILE);
+ test.ok(fileExists, "file exists");
+ fileExists = yield OS.File.exists(EXISTING_FILE + ".tmp");
+ test.ok(!fileExists, "file does not exists");
+ });
+});
+
+/**
+ * Test changes to OS.Shared.DEBUG flag.
+ */
+var test_debug = maketest("debug", function debug(test) {
+ return Task.spawn(function() {
+ function testSetDebugPref (pref) {
+ try {
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ } catch (x) {
+ test.fail("Setting OS.Shared.DEBUG to " + pref +
+ " should not cause error.");
+ } finally {
+ test.is(OS.Shared.DEBUG, pref, "OS.Shared.DEBUG is set correctly.");
+ }
+ }
+ testSetDebugPref(true);
+ let workerDEBUG = yield OS.File.GET_DEBUG();
+ test.is(workerDEBUG, true, "Worker's DEBUG is set.");
+ testSetDebugPref(false);
+ workerDEBUG = yield OS.File.GET_DEBUG();
+ test.is(workerDEBUG, false, "Worker's DEBUG is unset.");
+ });
+});
+
+/**
+ * Test logging in the main thread with set OS.Shared.DEBUG and
+ * OS.Shared.TEST flags.
+ */
+var test_debug_test = maketest("debug_test", function debug_test(test) {
+ return Task.spawn(function () {
+ // Create a console listener.
+ let consoleListener = {
+ observe: function (aMessage) {
+ // Ignore unexpected messages.
+ if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
+ return;
+ }
+ if (aMessage.message.indexOf("TEST OS") < 0) {
+ return;
+ }
+ test.ok(true, "DEBUG TEST messages are logged correctly.");
+ }
+ };
+ toggleDebugTest(true, consoleListener);
+ // Execution of OS.File.exist method will trigger OS.File.LOG several times.
+ let fileExists = yield OS.File.exists(EXISTING_FILE);
+ toggleDebugTest(false, consoleListener);
+ });
+});
+
+
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_async.xul b/toolkit/components/osfile/tests/mochi/test_osfile_async.xul
new file mode 100644
index 0000000000..1ef103f02c
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_async.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing async I/O with OS.File"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="main_test_osfile_async.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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_back.xul b/toolkit/components/osfile/tests/mochi/test_osfile_back.xul
new file mode 100644
index 0000000000..b6af6303f2
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_back.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="worker_handler.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+
+function test() {
+ ok(true, "test_osfile.xul: Starting test");
+ if (navigator.platform.indexOf("Win") != -1) {
+ ok(true, "test_osfile.xul: Using Windows test suite");
+ worker = new ChromeWorker("worker_test_osfile_win.js");
+ } else {
+ ok(true, "test_osfile.xul: Using Unix test suite");
+ worker = new ChromeWorker("worker_test_osfile_unix.js");
+ }
+ SimpleTest.waitForExplicitFinish();
+ ok(true, "test_osfile.xul: Chrome worker created");
+ dump("MAIN: go\n");
+ worker_handler(worker);
+ worker.postMessage(0);
+ ok(true, "test_osfile.xul: Test in progress");
+};
+]]>
+ </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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_comms.xul b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xul
new file mode 100644
index 0000000000..88e474ce2b
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xul
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+"use strict";
+
+let worker;
+
+let test = function test() {
+ SimpleTest.info("test_osfile_comms.xul: Starting test");
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+ Components.utils.import("resource://gre/modules/osfile.jsm");
+ worker = new ChromeWorker("worker_test_osfile_comms.js");
+ SimpleTest.waitForExplicitFinish();
+ try {
+ worker.onerror = function onerror(error) {
+ SimpleTest.ok(false, "received error "+error);
+ }
+ worker.onmessage = function onmessage(msg) {
+ Components.utils.forceShrinkingGC();
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ " ("+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ " ("+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ case "value":
+ SimpleTest.ok(true, "test_osfile_comms.xul: Received value " + JSON.stringify(msg.data.value));
+ let type = eval(msg.data.typename);
+ let check = eval(msg.data.check);
+ let value = msg.data.value;
+ let deserialized = type.fromMsg(value);
+ check(deserialized, "Main thread test: ");
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile_comms.xul: wrong message "+JSON.stringify(msg.data));
+ return;
+ }
+ };
+ worker.postMessage(0)
+ ok(true, "Worker launched");
+ } catch(x) {
+ // As we have set |waitForExplicitFinish|, we add this fallback
+ // in case of uncaught error, to ensure that the test does not
+ // just freeze.
+ ok(false, "Caught exception " + x + " at " + x.stack);
+ SimpleTest.finish();
+ }
+};
+
+]]>
+ </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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_front.xul b/toolkit/components/osfile/tests/mochi/test_osfile_front.xul
new file mode 100644
index 0000000000..10d3fbd014
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_front.xul
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="worker_handler.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+let main = this;
+
+function test() {
+ info("test_osfile_front.xul: Starting test");
+
+ // Test the OS.File worker
+
+ worker = new ChromeWorker("worker_test_osfile_front.js");
+ SimpleTest.waitForExplicitFinish();
+ info("test_osfile_front.xul: Chrome worker created");
+ dump("MAIN: go\n");
+ worker_handler(worker);
+ worker.postMessage(0);
+ ok(true, "test_osfile_front.xul: Test in progress");
+};
+]]>
+ </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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/worker_handler.js b/toolkit/components/osfile/tests/mochi/worker_handler.js
new file mode 100644
index 0000000000..6c513e6ba6
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_handler.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function worker_handler(worker) {
+ worker.onerror = function(error) {
+ error.preventDefault();
+ ok(false, "Worker error " + error.message);
+ }
+ worker.onmessage = function(msg) {
+ ok(true, "MAIN: onmessage " + JSON.stringify(msg.data));
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile.xul: wrong message " + JSON.stringify(msg.data));
+ return;
+ }
+ };
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js
new file mode 100644
index 0000000000..5e8bdd9caa
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+importScripts('worker_test_osfile_shared.js');
+
+// The set of samples for communications test. Declare as a global
+// variable to prevent this from being garbage-collected too early.
+var samples;
+
+self.onmessage = function(msg) {
+ info("Initializing");
+ self.onmessage = function on_unexpected_message(msg) {
+ throw new Error("Unexpected message " + JSON.stringify(msg.data));
+ };
+ importScripts("resource://gre/modules/osfile.jsm");
+ info("Initialization complete");
+
+ samples = [
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "String",
+ value: "This is a test",
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_string(candidate, prefix) {
+ is(candidate, "This is a test", prefix);
+ }},
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "Typed array",
+ value: (function() {
+ let view = new Uint8Array(15);
+ for (let i = 0; i < 15; ++i) {
+ view[i] = i;
+ }
+ return view;
+ })(),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_ArrayBuffer(candidate, prefix) {
+ for (let i = 0; i < 15; ++i) {
+ is(candidate[i], i % 256, prefix + "Checking that the contents of the ArrayBuffer were preserved");
+ }
+ }},
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "Pointer",
+ value: new OS.Shared.Type.char.in_ptr.implementation(1),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_ptr(candidate, prefix) {
+ let address = ctypes.cast(candidate, ctypes.uintptr_t).value.toString();
+ is(address, "1", prefix + "Checking that the pointer address was preserved");
+ }},
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "C array",
+ value: (function() {
+ let buf = new (ctypes.ArrayType(ctypes.uint8_t, 15))();
+ for (let i = 0; i < 15; ++i) {
+ buf[i] = i % 256;
+ }
+ return buf;
+ })(),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_array(candidate, prefix) {
+ let cast = ctypes.cast(candidate, ctypes.uint8_t.ptr);
+ for (let i = 0; i < 15; ++i) {
+ is(cast.contents, i % 256, prefix + "Checking that the contents of the C array were preserved, index " + i);
+ cast = cast.increment();
+ }
+ }
+ },
+ { typename: "OS.File.Error",
+ valuedescr: "OS Error",
+ type: OS.File.Error,
+ value: new OS.File.Error("foo", 1),
+ check: function check_error(candidate, prefix) {
+ ok(candidate instanceof OS.File.Error,
+ prefix + "Error is an OS.File.Error");
+ ok(candidate.unixErrno == 1 || candidate.winLastError == 1,
+ prefix + "Error code is correct");
+ try {
+ let string = candidate.toString();
+ info(prefix + ".toString() works " + string);
+ } catch (x) {
+ ok(false, prefix + ".toString() fails " + x);
+ }
+ }
+ }
+ ];
+ samples.forEach(function test(sample) {
+ let type = sample.type;
+ let value = sample.value;
+ let check = sample.check;
+ info("Testing handling of type " + sample.typename + " communicating " + sample.valuedescr);
+
+ // 1. Test serialization
+ let serialized;
+ let exn;
+ try {
+ serialized = type.toMsg(value);
+ } catch (ex) {
+ exn = ex;
+ }
+ is(exn, null, "Can I serialize the following value? " + value +
+ " aka " + JSON.stringify(value));
+ if (exn) {
+ return;
+ }
+
+ if ("data" in serialized) {
+ // Unwrap from `Meta`
+ serialized = serialized.data;
+ }
+
+ // 2. Test deserialization
+ let deserialized;
+ try {
+ deserialized = type.fromMsg(serialized);
+ } catch (ex) {
+ exn = ex;
+ }
+ is(exn, null, "Can I deserialize the following message? " + serialized
+ + " aka " + JSON.stringify(serialized));
+ if (exn) {
+ return;
+ }
+
+ // 3. Local test deserialized value
+ info("Running test on deserialized value " + deserialized +
+ " aka " + JSON.stringify(deserialized));
+ check(deserialized, "Local test: ");
+
+ // 4. Test sending serialized
+ info("Attempting to send message");
+ try {
+ self.postMessage({kind:"value",
+ typename: sample.typename,
+ value: serialized,
+ check: check.toSource()});
+ } catch (ex) {
+ exn = ex;
+ }
+ is(exn, null, "Can I send the following message? " + serialized
+ + " aka " + JSON.stringify(serialized));
+ });
+
+ finish();
+ };
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js
new file mode 100644
index 0000000000..29e6135101
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js
@@ -0,0 +1,566 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts('worker_test_osfile_shared.js');
+importScripts("resource://gre/modules/workers/require.js");
+
+var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+SharedAll.Config.DEBUG = true;
+
+function should_throw(f) {
+ try {
+ f();
+ } catch (x) {
+ return x;
+ }
+ return null;
+}
+
+self.onmessage = function onmessage_start(msg) {
+ self.onmessage = function onmessage_ignored(msg) {
+ log("ignored message " + JSON.stringify(msg.data));
+ };
+ try {
+ test_init();
+ test_open_existing_file();
+ test_open_non_existing_file();
+ test_flush_open_file();
+ test_copy_existing_file();
+ test_position();
+ test_move_file();
+ test_iter_dir();
+ test_info();
+ test_path();
+ test_exists_file();
+ test_remove_file();
+ } catch (x) {
+ log("Catching error: " + x);
+ log("Stack: " + x.stack);
+ log("Source: " + x.toSource());
+ ok(false, x.toString() + "\n" + x.stack);
+ }
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+/**
+ * Test that we can open an existing file.
+ */
+function test_open_existing_file()
+{
+ info("Starting test_open_existing");
+ let file = OS.File.open("chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js");
+ file.close();
+}
+
+/**
+ * Test that opening a file that does not exist fails with the right error.
+ */
+function test_open_non_existing_file()
+{
+ info("Starting test_open_non_existing");
+ let exn;
+ try {
+ let file = OS.File.open("/I do not exist");
+ } catch (x) {
+ exn = x;
+ info("test_open_non_existing_file: Exception detail " + exn);
+ }
+ ok(!!exn, "test_open_non_existing_file: Exception was raised ");
+ ok(exn instanceof OS.File.Error, "test_open_non_existing_file: Exception was a OS.File.Error");
+ ok(exn.becauseNoSuchFile, "test_open_non_existing_file: Exception confirms that the file does not exist");
+}
+
+/**
+ * Test that to ensure that |foo.flush()| does not
+ * cause an error, where |foo| is an open file.
+ */
+function test_flush_open_file()
+{
+ info("Starting test_flush_open_file");
+ let tmp = "test_flush.tmp";
+ let file = OS.File.open(tmp, {create: true, write: true});
+ file.flush();
+ file.close();
+ OS.File.remove(tmp);
+}
+
+/**
+ * Utility function for comparing two files (or a prefix of two files).
+ *
+ * This function returns nothing but fails of both files (or prefixes)
+ * are not identical.
+ *
+ * @param {string} test The name of the test (used for logging).
+ * @param {string} sourcePath The name of the first file.
+ * @param {string} destPath The name of the second file.
+ * @param {number=} prefix If specified, only compare the |prefix|
+ * first bytes of |sourcePath| and |destPath|.
+ */
+function compare_files(test, sourcePath, destPath, prefix)
+{
+ info(test + ": Comparing " + sourcePath + " and " + destPath);
+ let source = OS.File.open(sourcePath);
+ let dest = OS.File.open(destPath);
+ info("Files are open");
+ let sourceResult, destResult;
+ try {
+ if (prefix != undefined) {
+ sourceResult = source.read(prefix);
+ destResult = dest.read(prefix);
+ } else {
+ sourceResult = source.read();
+ destResult = dest.read();
+ }
+ is(sourceResult.length, destResult.length, test + ": Both files have the same size");
+ for (let i = 0; i < sourceResult.length; ++i) {
+ if (sourceResult[i] != destResult[i]) {
+ is(sourceResult[i] != destResult[i], test + ": Comparing char " + i);
+ break;
+ }
+ }
+ } finally {
+ source.close();
+ dest.close();
+ }
+ info(test + ": Comparison complete");
+}
+
+/**
+ * Test that copying a file using |copy| works.
+ */
+function test_copy_existing_file()
+{
+ let src_file_name =
+ OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi",
+ "worker_test_osfile_front.js");
+ let tmp_file_name = "test_osfile_front.tmp";
+ info("Starting test_copy_existing");
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ info("test_copy_existing: Copy complete");
+ compare_files("test_copy_existing", src_file_name, tmp_file_name);
+
+ // Create a bogus file with arbitrary content, then attempt to overwrite
+ // it with |copy|.
+ let dest = OS.File.open(tmp_file_name, {trunc: true});
+ let buf = new Uint8Array(50);
+ dest.write(buf);
+ dest.close();
+
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ compare_files("test_copy_existing 2", src_file_name, tmp_file_name);
+
+ // Attempt to overwrite with noOverwrite
+ let exn;
+ try {
+ OS.File.copy(src_file_name, tmp_file_name, {noOverwrite: true});
+ } catch(x) {
+ exn = x;
+ }
+ ok(!!exn, "test_copy_existing: noOverwrite prevents overwriting existing files");
+
+ info("test_copy_existing: Cleaning up");
+ OS.File.remove(tmp_file_name);
+}
+
+/**
+ * Test that moving a file works.
+ */
+function test_move_file()
+{
+ info("test_move_file: Starting");
+ // 1. Copy file into a temporary file
+ let src_file_name =
+ OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi",
+ "worker_test_osfile_front.js");
+ let tmp_file_name = "test_osfile_front.tmp";
+ let tmp2_file_name = "test_osfile_front.tmp2";
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ info("test_move_file: Copy complete");
+
+ // 2. Move
+ OS.File.move(tmp_file_name, tmp2_file_name);
+
+ info("test_move_file: Move complete");
+
+ // 3. Check that destination exists
+ compare_files("test_move_file", src_file_name, tmp2_file_name);
+
+ // 4. Check that original file does not exist anymore
+ let exn;
+ try {
+ OS.File.open(tmp_file_name);
+ } catch (x) {
+ exn = x;
+ }
+ ok(!!exn, "test_move_file: Original file has been removed");
+
+ info("test_move_file: Cleaning up");
+ OS.File.remove(tmp2_file_name);
+}
+
+function test_iter_dir()
+{
+ info("test_iter_dir: Starting");
+
+ // Create a file, to be sure that it exists
+ let tmp_file_name = "test_osfile_front.tmp";
+ let tmp_file = OS.File.open(tmp_file_name, {write: true, trunc:true});
+ tmp_file.close();
+
+ let parent = OS.File.getCurrentDirectory();
+ info("test_iter_dir: directory " + parent);
+ let iterator = new OS.File.DirectoryIterator(parent);
+ info("test_iter_dir: iterator created");
+ let encountered_tmp_file = false;
+ for (let entry in iterator) {
+ // Checking that |name| can be decoded properly
+ info("test_iter_dir: encountering entry " + entry.name);
+
+ if (entry.name == tmp_file_name) {
+ encountered_tmp_file = true;
+ isnot(entry.isDir, "test_iter_dir: The temporary file is not a directory");
+ isnot(entry.isSymLink, "test_iter_dir: The temporary file is not a link");
+ }
+
+ let file;
+ let success = true;
+ try {
+ file = OS.File.open(entry.path);
+ } catch (x) {
+ if (x.becauseNoSuchFile) {
+ success = false;
+ }
+ }
+ if (file) {
+ file.close();
+ }
+ ok(success, "test_iter_dir: Entry " + entry.path + " exists");
+
+ if (OS.Win) {
+ // We assume that the files are at least as recent as 2009.
+ // Since this test was written in 2011 and some of our packaging
+ // sets dates arbitrarily to 2010, this should be safe.
+ let year = new Date().getFullYear();
+ let creation = entry.winCreationDate;
+ ok(creation, "test_iter_dir: Windows creation date exists: " + creation);
+ ok(creation.getFullYear() >= 2009 && creation.getFullYear() <= year, "test_iter_dir: consistent creation date");
+
+ let lastWrite = entry.winLastWriteDate;
+ ok(lastWrite, "test_iter_dir: Windows lastWrite date exists: " + lastWrite);
+ ok(lastWrite.getFullYear() >= 2009 && lastWrite.getFullYear() <= year, "test_iter_dir: consistent lastWrite date");
+
+ let lastAccess = entry.winLastAccessDate;
+ ok(lastAccess, "test_iter_dir: Windows lastAccess date exists: " + lastAccess);
+ ok(lastAccess.getFullYear() >= 2009 && lastAccess.getFullYear() <= year, "test_iter_dir: consistent lastAccess date");
+ }
+
+ }
+ ok(encountered_tmp_file, "test_iter_dir: We have found the temporary file");
+
+ info("test_iter_dir: Cleaning up");
+ iterator.close();
+
+ // Testing nextBatch()
+ iterator = new OS.File.DirectoryIterator(parent);
+ let allentries = [];
+ for (let x in iterator) {
+ allentries.push(x);
+ }
+ iterator.close();
+
+ ok(allentries.length >= 14, "test_iter_dir: Meta-check: the test directory should contain at least 14 items");
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ let firstten = iterator.nextBatch(10);
+ is(firstten.length, 10, "test_iter_dir: nextBatch(10) returns 10 items");
+ for (let i = 0; i < firstten.length; ++i) {
+ is(allentries[i].path, firstten[i].path, "test_iter_dir: Checking that batch returns the correct entries");
+ }
+ let nextthree = iterator.nextBatch(3);
+ is(nextthree.length, 3, "test_iter_dir: nextBatch(3) returns 3 items");
+ for (let i = 0; i < nextthree.length; ++i) {
+ is(allentries[i + firstten.length].path, nextthree[i].path, "test_iter_dir: Checking that batch 2 returns the correct entries");
+ }
+ let everythingelse = iterator.nextBatch();
+ ok(everythingelse.length >= 1, "test_iter_dir: nextBatch() returns at least one item");
+ for (let i = 0; i < everythingelse.length; ++i) {
+ is(allentries[i + firstten.length + nextthree.length].path, everythingelse[i].path, "test_iter_dir: Checking that batch 3 returns the correct entries");
+ }
+ is(iterator.nextBatch().length, 0, "test_iter_dir: Once there is nothing left, nextBatch returns an empty array");
+ iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ iterator.close();
+ is(iterator.nextBatch().length, 0, "test_iter_dir: nextBatch on closed iterator returns an empty array");
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ let allentries2 = iterator.nextBatch();
+ is(allentries.length, allentries2.length, "test_iter_dir: Checking that getBatch(null) returns the right number of entries");
+ for (let i = 0; i < allentries.length; ++i) {
+ is(allentries[i].path, allentries2[i].path, "test_iter_dir: Checking that getBatch(null) returns everything in the right order");
+ }
+ iterator.close();
+
+ // Test forEach
+ iterator = new OS.File.DirectoryIterator(parent);
+ let index = 0;
+ iterator.forEach(
+ function cb(entry, aIndex, aIterator) {
+ is(index, aIndex, "test_iter_dir: Checking that forEach index is correct");
+ ok(iterator == aIterator, "test_iter_dir: Checking that right iterator is passed");
+ if (index < 10) {
+ is(allentries[index].path, entry.path, "test_iter_dir: Checking that forEach entry is correct");
+ } else if (index == 10) {
+ iterator.close();
+ } else {
+ ok(false, "test_iter_dir: Checking that forEach can be stopped early");
+ }
+ ++index;
+ });
+ iterator.close();
+
+ //test for prototype |OS.File.DirectoryIterator.unixAsFile|
+ if ("unixAsFile" in OS.File.DirectoryIterator.prototype) {
+ info("testing property unixAsFile");
+ let path = OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi");
+ iterator = new OS.File.DirectoryIterator(path);
+
+ let dir_file = iterator.unixAsFile();// return |File|
+ let stat0 = dir_file.stat();
+ let stat1 = OS.File.stat(path);
+
+ let unix_info_to_string = function unix_info_to_string(info) {
+ return "| " + info.unixMode + " | " + info.unixOwner + " | " + info.unixGroup + " | " + info.creationDate + " | " + info.lastModificationDate + " | " + info.lastAccessDate + " | " + info.size + " |";
+ };
+
+ let s0_string = unix_info_to_string(stat0);
+ let s1_string = unix_info_to_string(stat1);
+
+ ok(stat0.isDir, "unixAsFile returned a directory");
+ is(s0_string, s1_string, "unixAsFile returned the correct file");
+ dir_file.close();
+ iterator.close();
+ }
+ info("test_iter_dir: Complete");
+}
+
+function test_position() {
+ info("test_position: Starting");
+
+ ok("POS_START" in OS.File, "test_position: POS_START exists");
+ ok("POS_CURRENT" in OS.File, "test_position: POS_CURRENT exists");
+ ok("POS_END" in OS.File, "test_position: POS_END exists");
+
+ let ARBITRARY_POSITION = 321;
+ let src_file_name =
+ OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi",
+ "worker_test_osfile_front.js");
+
+ let file = OS.File.open(src_file_name);
+ is(file.getPosition(), 0, "test_position: Initial position is 0");
+
+ let size = 0 + file.stat().size; // Hack: We can remove this 0 + once 776259 has landed
+
+ file.setPosition(ARBITRARY_POSITION, OS.File.POS_START);
+ is(file.getPosition(), ARBITRARY_POSITION, "test_position: Setting position from start");
+
+ file.setPosition(0, OS.File.POS_START);
+ is(file.getPosition(), 0, "test_position: Setting position from start back to 0");
+
+ file.setPosition(ARBITRARY_POSITION);
+ is(file.getPosition(), ARBITRARY_POSITION, "test_position: Setting position without argument");
+
+ file.setPosition(-ARBITRARY_POSITION, OS.File.POS_END);
+ is(file.getPosition(), size - ARBITRARY_POSITION, "test_position: Setting position from end");
+
+ file.setPosition(ARBITRARY_POSITION, OS.File.POS_CURRENT);
+ is(file.getPosition(), size, "test_position: Setting position from current");
+
+ file.close();
+ info("test_position: Complete");
+}
+
+function test_info() {
+ info("test_info: Starting");
+
+ let filename = "test_info.tmp";
+ let size = 261;// An arbitrary file length
+ let start = new Date();
+
+ // Cleanup any leftover from previous tests
+ try {
+ OS.File.remove(filename);
+ info("test_info: Cleaned up previous garbage");
+ } catch (x) {
+ if (!x.becauseNoSuchFile) {
+ throw x;
+ }
+ info("test_info: No previous garbage");
+ }
+
+ let file = OS.File.open(filename, {trunc: true});
+ let buf = new ArrayBuffer(size);
+ file._write(buf, size);
+ file.close();
+
+ // Test OS.File.stat on new file
+ let stat = OS.File.stat(filename);
+ ok(!!stat, "test_info: info acquired");
+ ok(!stat.isDir, "test_info: file is not a directory");
+ is(stat.isSymLink, false, "test_info: file is not a link");
+ is(stat.size.toString(), size, "test_info: correct size");
+
+ let stop = new Date();
+
+ // We round down/up by 1s as file system precision is lower than
+ // Date precision (no clear specifications about that, but it seems
+ // that this can be a little over 1 second under ext3 and 2 seconds
+ // under FAT).
+ let SLOPPY_FILE_SYSTEM_ADJUSTMENT = 3000;
+ let startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ let stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ info("Testing stat with bounds [ " + startMs + ", " + stopMs +" ]");
+
+ (function() {
+ let birth;
+ if ("winBirthDate" in stat) {
+ birth = stat.winBirthDate;
+ } else if ("macBirthDate" in stat) {
+ birth = stat.macBirthDate;
+ } else {
+ ok(true, "Skipping birthdate test");
+ return;
+ }
+ ok(birth.getTime() <= stopMs,
+ "test_info: platformBirthDate is consistent");
+ // Note: Previous versions of this test checked whether the file
+ // has been created after the start of the test. Unfortunately,
+ // this sometimes failed under Windows, in specific circumstances:
+ // if the file has been removed at the start of the test and
+ // recreated immediately, the Windows file system detects this and
+ // decides that the file was actually truncated rather than
+ // recreated, hence that it should keep its previous creation
+ // date. Debugging hilarity ensues.
+ });
+
+ let change = stat.lastModificationDate;
+ info("Testing lastModificationDate: " + change);
+ ok(change.getTime() >= startMs && change.getTime() <= stopMs,
+ "test_info: lastModificationDate is consistent");
+
+ // Test OS.File.prototype.stat on new file
+ file = OS.File.open(filename);
+ try {
+ stat = file.stat();
+ } finally {
+ file.close();
+ }
+
+ ok(!!stat, "test_info: info acquired 2");
+ ok(!stat.isDir, "test_info: file is not a directory 2");
+ ok(!stat.isSymLink, "test_info: file is not a link 2");
+ is(stat.size.toString(), size, "test_info: correct size 2");
+
+ stop = new Date();
+
+ // Round up/down as above
+ startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ info("Testing stat 2 with bounds [ " + startMs + ", " + stopMs +" ]");
+
+ let access = stat.lastAccessDate;
+ info("Testing lastAccessDate: " + access);
+ ok(access.getTime() >= startMs && access.getTime() <= stopMs,
+ "test_info: lastAccessDate is consistent");
+
+ change = stat.lastModificationDate;
+ info("Testing lastModificationDate 2: " + change);
+ ok(change.getTime() >= startMs && change.getTime() <= stopMs,
+ "test_info: lastModificationDate 2 is consistent");
+
+ // Test OS.File.stat on directory
+ stat = OS.File.stat(OS.File.getCurrentDirectory());
+ ok(!!stat, "test_info: info on directory acquired");
+ ok(stat.isDir, "test_info: directory is a directory");
+
+ info("test_info: Complete");
+}
+
+// Note that most of the features of path are tested in
+// worker_test_osfile_{unix, win}.js
+function test_path()
+{
+ info("test_path: starting");
+ let abcd = OS.Path.join("a", "b", "c", "d");
+ is(OS.Path.basename(abcd), "d", "basename of a/b/c/d");
+
+ let abc = OS.Path.join("a", "b", "c");
+ is(OS.Path.dirname(abcd), abc, "dirname of a/b/c/d");
+
+ let abdotsc = OS.Path.join("a", "b", "..", "c");
+ is(OS.Path.normalize(abdotsc), OS.Path.join("a", "c"), "normalize a/b/../c");
+
+ let adotsdotsdots = OS.Path.join("a", "..", "..", "..");
+ is(OS.Path.normalize(adotsdotsdots), OS.Path.join("..", ".."), "normalize a/../../..");
+
+ info("test_path: Complete");
+}
+
+/**
+ * Test the file |exists| method.
+ */
+function test_exists_file()
+{
+ let file_name = OS.Path.join("chrome", "toolkit", "components" ,"osfile",
+ "tests", "mochi", "test_osfile_front.xul");
+ info("test_exists_file: starting");
+ ok(OS.File.exists(file_name), "test_exists_file: file exists (OS.File.exists)");
+ ok(!OS.File.exists(file_name + ".tmp"), "test_exists_file: file does not exists (OS.File.exists)");
+
+ let dir_name = OS.Path.join("chrome", "toolkit", "components" ,"osfile",
+ "tests", "mochi");
+ ok(OS.File.exists(dir_name), "test_exists_file: directory exists");
+ ok(!OS.File.exists(dir_name) + ".tmp", "test_exists_file: directory does not exist");
+
+ info("test_exists_file: complete");
+}
+
+/**
+ * Test the file |remove| method.
+ */
+function test_remove_file()
+{
+ let absent_file_name = "test_osfile_front_absent.tmp";
+
+ // Check that removing absent files is handled correctly
+ let exn = should_throw(function() {
+ OS.File.remove(absent_file_name, {ignoreAbsent: false});
+ });
+ ok(!!exn, "test_remove_file: throws if there is no such file");
+
+ exn = should_throw(function() {
+ OS.File.remove(absent_file_name, {ignoreAbsent: true});
+ OS.File.remove(absent_file_name);
+ });
+ ok(!exn, "test_remove_file: ignoreAbsent works");
+
+ if (OS.Win) {
+ let file_name = "test_osfile_front_file_to_remove.tmp";
+ let file = OS.File.open(file_name, {write: true});
+ file.close();
+ ok(OS.File.exists(file_name), "test_remove_file: test file exists");
+ OS.Win.File.SetFileAttributes(file_name,
+ OS.Constants.Win.FILE_ATTRIBUTE_READONLY);
+ OS.File.remove(file_name);
+ ok(!OS.File.exists(file_name),
+ "test_remove_file: test file has been removed");
+ }
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js
new file mode 100644
index 0000000000..da82d4b0ab
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function log(text) {
+ dump("WORKER " + text + "\n");
+}
+
+function send(message) {
+ self.postMessage(message);
+}
+
+function finish() {
+ send({kind: "finish"});
+}
+
+function ok(condition, description) {
+ send({kind: "ok", condition: !!condition, description: "" + description});
+}
+
+function is(a, b, description) {
+ let outcome = a == b; // Need to decide outcome here, as not everything can be serialized
+ send({kind: "is", outcome: outcome, description: "" + description, a: "" + a, b: "" + b});
+}
+
+function isnot(a, b, description) {
+ let outcome = a != b; // Need to decide outcome here, as not everything can be serialized
+ send({kind: "isnot", outcome: outcome, description: "" + description, a: "" + a, b: "" + b});
+}
+
+function info(description) {
+ send({kind: "info", description: "" + description});
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js
new file mode 100644
index 0000000000..9fe2d0b4e6
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts('worker_test_osfile_shared.js');
+
+self.onmessage = function(msg) {
+ log("received message "+JSON.stringify(msg.data));
+ self.onmessage = function(msg) {
+ log("ignored message "+JSON.stringify(msg.data));
+ };
+ test_init();
+ test_getcwd();
+ test_open_close();
+ test_create_file();
+ test_access();
+ test_read_write();
+ test_passing_undefined();
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+function test_open_close() {
+ info("Starting test_open_close");
+ is(typeof OS.Unix.File.open, "function", "OS.Unix.File.open is a function");
+ let file = OS.Unix.File.open("chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js", OS.Constants.libc.O_RDONLY, 0);
+ isnot(file, -1, "test_open_close: opening succeeded");
+ info("Close: "+OS.Unix.File.close.toSource());
+ let result = OS.Unix.File.close(file);
+ is(result, 0, "test_open_close: close succeeded");
+
+ file = OS.Unix.File.open("/i do not exist", OS.Constants.libc.O_RDONLY, 0);
+ is(file, -1, "test_open_close: opening of non-existing file failed");
+ is(ctypes.errno, OS.Constants.libc.ENOENT, "test_open_close: error is ENOENT");
+}
+
+function test_create_file()
+{
+ info("Starting test_create_file");
+ let file = OS.Unix.File.open("test.tmp", OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ isnot(file, -1, "test_create_file: file created");
+ OS.Unix.File.close(file);
+}
+
+function test_access()
+{
+ info("Starting test_access");
+ let file = OS.Unix.File.open("test1.tmp", OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ let result = OS.Unix.File.access("test1.tmp", OS.Constants.libc.R_OK | OS.Constants.libc.W_OK | OS.Constants.libc.X_OK | OS.Constants.libc.F_OK);
+ is(result, 0, "first call to access() succeeded");
+ OS.Unix.File.close(file);
+
+ file = OS.Unix.File.open("test1.tmp", OS.Constants.libc.O_WRONLY
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IWUSR);
+
+ info("test_access: preparing second call to access()");
+ result = OS.Unix.File.access("test2.tmp", OS.Constants.libc.R_OK
+ | OS.Constants.libc.W_OK
+ | OS.Constants.libc.X_OK
+ | OS.Constants.libc.F_OK);
+ is(result, -1, "test_access: second call to access() failed as expected");
+ is(ctypes.errno, OS.Constants.libc.ENOENT, "This is the correct error");
+ OS.Unix.File.close(file);
+}
+
+function test_getcwd()
+{
+ let array = new (ctypes.ArrayType(ctypes.char, 32768))();
+ let path = OS.Unix.File.getcwd(array, array.length);
+ if (ctypes.char.ptr(path).isNull()) {
+ ok(false, "test_get_cwd: getcwd returned null, errno: " + ctypes.errno);
+ }
+ let path2;
+ if (OS.Unix.File.get_current_dir_name) {
+ path2 = OS.Unix.File.get_current_dir_name();
+ } else {
+ path2 = OS.Unix.File.getwd_auto(null);
+ }
+ if (ctypes.char.ptr(path2).isNull()) {
+ ok(false, "test_get_cwd: getwd_auto/get_current_dir_name returned null, errno: " + ctypes.errno);
+ }
+ is(path.readString(), path2.readString(), "test_get_cwd: getcwd and getwd return the same path");
+}
+
+function test_read_write()
+{
+ let output_name = "osfile_copy.tmp";
+ // Copy file
+ let input = OS.Unix.File.open(
+ "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js",
+ OS.Constants.libc.O_RDONLY, 0);
+ isnot(input, -1, "test_read_write: input file opened");
+ let output = OS.Unix.File.open("osfile_copy.tmp", OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ isnot(output, -1, "test_read_write: output file opened");
+
+ let array = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes = -1;
+ let total = 0;
+ while (true) {
+ bytes = OS.Unix.File.read(input, array, 4096);
+ ok(bytes != undefined, "test_read_write: bytes is defined");
+ isnot(bytes, -1, "test_read_write: no read error");
+ let write_from = 0;
+ if (bytes == 0) {
+ break;
+ }
+ while (bytes > 0) {
+ let ptr = array.addressOfElement(write_from);
+ // Note: |write| launches an exception in case of error
+ let written = OS.Unix.File.write(output, array, bytes);
+ isnot(written, -1, "test_read_write: no write error");
+ write_from += written;
+ bytes -= written;
+ }
+ total += write_from;
+ }
+ info("test_read_write: copy complete " + total);
+
+ // Compare files
+ let result;
+ info("SEEK_SET: " + OS.Constants.libc.SEEK_SET);
+ info("Input: " + input + "(" + input.toSource() + ")");
+ info("Output: " + output + "(" + output.toSource() + ")");
+ result = OS.Unix.File.lseek(input, 0, OS.Constants.libc.SEEK_SET);
+ info("Result of lseek: " + result);
+ isnot(result, -1, "test_read_write: input seek succeeded " + ctypes.errno);
+ result = OS.Unix.File.lseek(output, 0, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: output seek succeeded " + ctypes.errno);
+
+ let array2 = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes2 = -1;
+ let pos = 0;
+ while (true) {
+ bytes = OS.Unix.File.read(input, array, 4096);
+ isnot(bytes, -1, "test_read_write: input read succeeded");
+ bytes2 = OS.Unix.File.read(output, array2, 4096);
+ isnot(bytes, -1, "test_read_write: output read succeeded");
+ is(bytes > 0, bytes2 > 0, "Both files contain data or neither does "+bytes+", "+bytes2);
+ if (bytes == 0) {
+ break;
+ }
+ if (bytes != bytes2) {
+ // This would be surprising, but theoretically possible with a
+ // remote file system, I believe.
+ bytes = Math.min(bytes, bytes2);
+ pos += bytes;
+ result = OS.Unix.File.lseek(input, pos, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: input seek succeeded");
+ result = OS.Unix.File.lseek(output, pos, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: output seek succeeded");
+ } else {
+ pos += bytes;
+ }
+ for (let i = 0; i < bytes; ++i) {
+ if (array[i] != array2[i]) {
+ ok(false, "Files do not match at position " + i
+ + " ("+array[i] + "/"+array2[i] + ")");
+ }
+ }
+ }
+ info("test_read_write test complete");
+ result = OS.Unix.File.close(input);
+ isnot(result, -1, "test_read_write: input close succeeded");
+ result = OS.Unix.File.close(output);
+ isnot(result, -1, "test_read_write: output close succeeded");
+ result = OS.Unix.File.unlink(output_name);
+ isnot(result, -1, "test_read_write: input remove succeeded");
+ info("test_read_write cleanup complete");
+}
+
+function test_passing_undefined()
+{
+ info("Testing that an exception gets thrown when an FFI function is passed undefined");
+ let exceptionRaised = false;
+
+ try {
+ let file = OS.Unix.File.open(undefined, OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ } catch(e if e instanceof TypeError && e.message.indexOf("open") > -1) {
+ exceptionRaised = true;
+ }
+
+ ok(exceptionRaised, "test_passing_undefined: exception gets thrown")
+}
+
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js
new file mode 100644
index 0000000000..f41fdecfea
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js
@@ -0,0 +1,211 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts('worker_test_osfile_shared.js');
+
+self.onmessage = function(msg) {
+ self.onmessage = function(msg) {
+ log("ignored message "+JSON.stringify(msg.data));
+ };
+
+ test_init();
+ test_GetCurrentDirectory();
+ test_OpenClose();
+ test_CreateFile();
+ test_ReadWrite();
+ test_passing_undefined();
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+function test_OpenClose() {
+ info("Starting test_OpenClose");
+ is(typeof OS.Win.File.CreateFile, "function", "OS.Win.File.CreateFile is a function");
+ is(OS.Win.File.CloseHandle(OS.Constants.Win.INVALID_HANDLE_VALUE), true, "CloseHandle returns true given the invalid handle");
+ is(OS.Win.File.FindClose(OS.Constants.Win.INVALID_HANDLE_VALUE), true, "FindClose returns true given the invalid handle");
+ isnot(OS.Constants.Win.GENERIC_READ, undefined, "GENERIC_READ exists");
+ isnot(OS.Constants.Win.FILE_SHARE_READ, undefined, "FILE_SHARE_READ exists");
+ isnot(OS.Constants.Win.FILE_ATTRIBUTE_NORMAL, undefined, "FILE_ATTRIBUTE_NORMAL exists");
+ let file = OS.Win.File.CreateFile(
+ "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js",
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null);
+ info("test_OpenClose: Passed open");
+ isnot(file, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_OpenClose: file opened");
+ let result = OS.Win.File.CloseHandle(file);
+ isnot(result, 0, "test_OpenClose: close succeeded");
+
+ file = OS.Win.File.CreateFile(
+ "\\I do not exist",
+ OS.Constants.Win.GENERIC_READ,
+ OS.Constants.Win.FILE_SHARE_READ,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null);
+ is(file, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_OpenClose: cannot open non-existing file");
+ is(ctypes.winLastError, OS.Constants.Win.ERROR_FILE_NOT_FOUND, "test_OpenClose: error is ERROR_FILE_NOT_FOUND");
+}
+
+function test_CreateFile()
+{
+ info("Starting test_CreateFile");
+ let file = OS.Win.File.CreateFile(
+ "test.tmp",
+ OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE,
+ OS.Constants.Win.FILE_SHARE_READ | OS.Constants.FILE_SHARE_WRITE,
+ null,
+ OS.Constants.Win.CREATE_ALWAYS,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null);
+ isnot(file, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_CreateFile: opening succeeded");
+ let result = OS.Win.File.CloseHandle(file);
+ isnot(result, 0, "test_CreateFile: close succeeded");
+}
+
+function test_GetCurrentDirectory()
+{
+ let array = new (ctypes.ArrayType(ctypes.char16_t, 4096))();
+ let result = OS.Win.File.GetCurrentDirectory(4096, array);
+ ok(result < array.length, "test_GetCurrentDirectory: length sufficient");
+ ok(result > 0, "test_GetCurrentDirectory: length != 0");
+}
+
+function test_ReadWrite()
+{
+ info("Starting test_ReadWrite");
+ let output_name = "osfile_copy.tmp";
+ // Copy file
+ let input = OS.Win.File.CreateFile(
+ "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js",
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null);
+ isnot(input, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_ReadWrite: input file opened");
+ let output = OS.Win.File.CreateFile(
+ "osfile_copy.tmp",
+ OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE,
+ 0,
+ null,
+ OS.Constants.Win.CREATE_ALWAYS,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null);
+ isnot(output, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_ReadWrite: output file opened");
+ let array = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes_read = new ctypes.uint32_t(0);
+ let bytes_read_ptr = bytes_read.address();
+ log("We have a pointer for bytes read: "+bytes_read_ptr);
+ let bytes_written = new ctypes.uint32_t(0);
+ let bytes_written_ptr = bytes_written.address();
+ log("We have a pointer for bytes written: "+bytes_written_ptr);
+ log("test_ReadWrite: buffer and pointers ready");
+ let result;
+ while (true) {
+ log("test_ReadWrite: reading");
+ result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null);
+ isnot (result, 0, "test_ReadWrite: read success");
+ let write_from = 0;
+ let bytes_left = bytes_read;
+ log("test_ReadWrite: read chunk complete " + bytes_left.value);
+ if (bytes_left.value == 0) {
+ break;
+ }
+ while (bytes_left.value > 0) {
+ log("test_ReadWrite: writing "+bytes_left.value);
+ let ptr = array.addressOfElement(write_from);
+ // Note: |WriteFile| launches an exception in case of error
+ result = OS.Win.File.WriteFile(output, array, bytes_left, bytes_written_ptr, null);
+ isnot (result, 0, "test_ReadWrite: write success");
+ write_from += bytes_written;
+ bytes_left -= bytes_written;
+ }
+ }
+ info("test_ReadWrite: copy complete");
+
+ // Compare files
+ result = OS.Win.File.SetFilePointer(input, 0, null, OS.Constants.Win.FILE_BEGIN);
+ isnot (result, OS.Constants.Win.INVALID_SET_FILE_POINTER, "test_ReadWrite: input reset");
+
+ result = OS.Win.File.SetFilePointer(output, 0, null, OS.Constants.Win.FILE_BEGIN);
+ isnot (result, OS.Constants.Win.INVALID_SET_FILE_POINTER, "test_ReadWrite: output reset");
+
+ let array2 = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes_read2 = new ctypes.uint32_t(0);
+ let bytes_read2_ptr = bytes_read2.address();
+ let pos = 0;
+ while (true) {
+ result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null);
+ isnot(result, 0, "test_ReadWrite: input read succeeded");
+
+ result = OS.Win.File.ReadFile(output, array2, 4096, bytes_read2_ptr, null);
+ isnot(result, 0, "test_ReadWrite: output read succeeded");
+
+ is(bytes_read.value > 0, bytes_read2.value > 0,
+ "Both files contain data or neither does " + bytes_read.value + ", " + bytes_read2.value);
+ if (bytes_read.value == 0) {
+ break;
+ }
+ let bytes;
+ if (bytes_read.value != bytes_read2.value) {
+ // This would be surprising, but theoretically possible with a
+ // remote file system, I believe.
+ bytes = Math.min(bytes_read.value, bytes_read2.value);
+ pos += bytes;
+ result = OS.Win.File.SetFilePointer(input, pos, null, OS.Constants.Win.FILE_BEGIN);
+ isnot(result, 0, "test_ReadWrite: input seek succeeded");
+
+ result = OS.Win.File.SetFilePointer(output, pos, null, OS.Constants.Win.FILE_BEGIN);
+ isnot(result, 0, "test_ReadWrite: output seek succeeded");
+
+ } else {
+ bytes = bytes_read.value;
+ pos += bytes;
+ }
+ for (let i = 0; i < bytes; ++i) {
+ if (array[i] != array2[i]) {
+ ok(false, "Files do not match at position " + i
+ + " ("+array[i] + "/"+array2[i] + ")");
+ }
+ }
+ }
+ info("test_ReadWrite test complete");
+ result = OS.Win.File.CloseHandle(input);
+ isnot(result, 0, "test_ReadWrite: inpout close succeeded");
+ result = OS.Win.File.CloseHandle(output);
+ isnot(result, 0, "test_ReadWrite: outpout close succeeded");
+ result = OS.Win.File.DeleteFile(output_name);
+ isnot(result, 0, "test_ReadWrite: output remove succeeded");
+ info("test_ReadWrite cleanup complete");
+}
+
+function test_passing_undefined()
+{
+ info("Testing that an exception gets thrown when an FFI function is passed undefined");
+ let exceptionRaised = false;
+
+ try {
+ let file = OS.Win.File.CreateFile(
+ undefined,
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null);
+ } catch(e if e instanceof TypeError && e.message.indexOf("CreateFile") > -1) {
+ exceptionRaised = true;
+ }
+
+ ok(exceptionRaised, "test_passing_undefined: exception gets thrown")
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/.eslintrc.js b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/osfile/tests/xpcshell/head.js b/toolkit/components/osfile/tests/xpcshell/head.js
new file mode 100644
index 0000000000..eef29962af
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/head.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu, interfaces: Ci} = Components;
+
+var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+// Bug 1014484 can only be reproduced by loading OS.File first from the
+// CommonJS loader, so we do not want OS.File to be loaded eagerly for
+// all the tests in this directory.
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+var {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+
+Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+/**
+ * As add_task, but execute the test both with native operations and
+ * without.
+ */
+function add_test_pair(generator) {
+ add_task(function*() {
+ do_print("Executing test " + generator.name + " with native operations");
+ Services.prefs.setBoolPref("toolkit.osfile.native", true);
+ return Task.spawn(generator);
+ });
+ add_task(function*() {
+ do_print("Executing test " + generator.name + " without native operations");
+ Services.prefs.setBoolPref("toolkit.osfile.native", false);
+ return Task.spawn(generator);
+ });
+}
+
+/**
+ * Fetch asynchronously the contents of a file using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} path The _absolute_ path to the file.
+ * @return {promise}
+ * @resolves {string} The contents of the file.
+ */
+function reference_fetch_file(path, test) {
+ do_print("Fetching file " + path);
+ let deferred = Promise.defer();
+ let file = new FileUtils.File(path);
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function(stream, status) {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(status);
+ return;
+ }
+ let result, reject;
+ try {
+ result = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (x) {
+ reject = x;
+ }
+ stream.close();
+ if (reject) {
+ deferred.reject(reject);
+ } else {
+ deferred.resolve(result);
+ }
+ });
+
+ return deferred.promise;
+};
+
+/**
+ * Compare asynchronously the contents two files using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} a The _absolute_ path to the first file.
+ * @param {string} b The _absolute_ path to the second file.
+ *
+ * @resolves {null}
+ */
+function reference_compare_files(a, b, test) {
+ return Task.spawn(function*() {
+ do_print("Comparing files " + a + " and " + b);
+ let a_contents = yield reference_fetch_file(a, test);
+ let b_contents = yield reference_fetch_file(b, test);
+ do_check_eq(a_contents, b_contents);
+ });
+};
diff --git a/toolkit/components/osfile/tests/xpcshell/test_available_free_space.js b/toolkit/components/osfile/tests/xpcshell/test_available_free_space.js
new file mode 100644
index 0000000000..08e67763b5
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_available_free_space.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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+ run_next_test();
+}
+
+/**
+ * Test OS.File.getAvailableFreeSpace
+ */
+add_task(function() {
+ // Set up profile. We will use profile path to query for available free
+ // space.
+ do_get_profile();
+
+ let dir = OS.Constants.Path.profileDir;
+
+ // Sanity checking for the test
+ do_check_true((yield OS.File.exists(dir)));
+
+ // Query for available bytes for user
+ let availableBytes = yield OS.File.getAvailableFreeSpace(dir);
+
+ do_check_true(!!availableBytes);
+ do_check_true(availableBytes > 0);
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_compression.js b/toolkit/components/osfile/tests/xpcshell/test_compression.js
new file mode 100644
index 0000000000..b40235615e
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_compression.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+add_task(function test_compress_lz4() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "compression.lz");
+ let length = 1024;
+ let array = new Uint8Array(length);
+ for (let i = 0; i < array.byteLength; ++i) {
+ array[i] = i;
+ }
+ let arrayAsString = Array.prototype.join.call(array);
+
+ do_print("Writing data with lz4 compression");
+ let bytes = yield OS.File.writeAtomic(path, array, { compression: "lz4" });
+ do_print("Compressed " + length + " bytes into " + bytes);
+
+ do_print("Reading back with lz4 decompression");
+ let decompressed = yield OS.File.read(path, { compression: "lz4" });
+ do_print("Decompressed into " + decompressed.byteLength + " bytes");
+ do_check_eq(arrayAsString, Array.prototype.join.call(decompressed));
+});
+
+add_task(function test_uncompressed() {
+ do_print("Writing data without compression");
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_compression.tmp");
+ let array = new Uint8Array(1024);
+ for (let i = 0; i < array.byteLength; ++i) {
+ array[i] = i;
+ }
+ let bytes = yield OS.File.writeAtomic(path, array); // No compression
+
+ let exn;
+ // Force decompression, reading should fail
+ try {
+ yield OS.File.read(path, { compression: "lz4" });
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ // Check the exception message (and that it contains the file name)
+ do_check_true(exn.message.indexOf(`Invalid header (no magic number) - Data: ${ path }`) != -1);
+});
+
+add_task(function test_no_header() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_header.tmp");
+ let array = new Uint8Array(8).fill(0,0); // Small array with no header
+
+ do_print("Writing data with no header");
+
+ let bytes = yield OS.File.writeAtomic(path, array); // No compression
+ let exn;
+ // Force decompression, reading should fail
+ try {
+ yield OS.File.read(path, { compression: "lz4" });
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ // Check the exception message (and that it contains the file name)
+ do_check_true(exn.message.indexOf(`Buffer is too short (no header) - Data: ${ path }`) != -1);
+});
+
+add_task(function test_invalid_content() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "invalid_content.tmp");
+ let arr1 = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]);
+ let arr2 = new Uint8Array(248).fill(1,0);
+
+ let array = new Uint8Array(arr1.length + arr2.length);
+ array.set(arr1);
+ array.set(arr2, arr1.length);
+
+ do_print("Writing invalid data (with a valid header and only ones after that)");
+
+ let bytes = yield OS.File.writeAtomic(path, array); // No compression
+ let exn;
+ // Force decompression, reading should fail
+ try {
+ yield OS.File.read(path, { compression: "lz4" });
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ // Check the exception message (and that it contains the file name)
+ do_check_true(exn.message.indexOf(`Invalid content: Decompression stopped at 0 - Data: ${ path }`) != -1);
+});
+
+add_task(function() {
+ do_test_finished();
+}); \ No newline at end of file
diff --git a/toolkit/components/osfile/tests/xpcshell/test_constants.js b/toolkit/components/osfile/tests/xpcshell/test_constants.js
new file mode 100644
index 0000000000..e92f33ab83
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_constants.js
@@ -0,0 +1,31 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+
+function run_test() {
+ run_next_test();
+}
+
+// Test that OS.Constants is defined correctly.
+add_task(function* check_definition() {
+ do_check_true(OS.Constants!=null);
+ do_check_true(!!OS.Constants.Win || !!OS.Constants.libc);
+ do_check_true(OS.Constants.Path!=null);
+ do_check_true(OS.Constants.Sys!=null);
+ //check system name
+ if (OS.Constants.Sys.Name == "Gonk") {
+ // Services.appinfo.OS doesn't know the difference between Gonk and Android
+ do_check_eq(Services.appinfo.OS, "Android");
+ } else {
+ do_check_eq(Services.appinfo.OS, OS.Constants.Sys.Name);
+ }
+
+ //check if using DEBUG build
+ if (Components.classes["@mozilla.org/xpcom/debug;1"].getService(Components.interfaces.nsIDebug2).isDebugBuild == true) {
+ do_check_true(OS.Constants.Sys.DEBUG);
+ } else {
+ do_check_true(typeof(OS.Constants.Sys.DEBUG) == 'undefined');
+ }
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_creationDate.js b/toolkit/components/osfile/tests/xpcshell/test_creationDate.js
new file mode 100644
index 0000000000..9c4fa1dfc4
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_creationDate.js
@@ -0,0 +1,31 @@
+"use strict";
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * Test to ensure that deprecation warning is issued on use
+ * of creationDate.
+ */
+add_task(function test_deprecatedCreationDate () {
+ let deferred = Promise.defer();
+ let consoleListener = {
+ observe: function (aMessage) {
+ if(aMessage.message.indexOf("Field 'creationDate' is deprecated.") > -1) {
+ do_print("Deprecation message printed");
+ do_check_true(true);
+ Services.console.unregisterListener(consoleListener);
+ deferred.resolve();
+ }
+ }
+ };
+ let currentDir = yield OS.File.getCurrentDirectory();
+ let path = OS.Path.join(currentDir, "test_creationDate.js");
+
+ Services.console.registerListener(consoleListener);
+ (yield OS.File.stat(path)).creationDate;
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_duration.js b/toolkit/components/osfile/tests/xpcshell/test_duration.js
new file mode 100644
index 0000000000..305c03da86
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_duration.js
@@ -0,0 +1,91 @@
+var {OS} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+var {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
+
+/**
+ * Test optional duration reporting that can be used for telemetry.
+ */
+add_task(function* duration() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+ // Options structure passed to a OS.File copy method.
+ let copyOptions = {
+ // This field should be overridden with the actual duration
+ // measurement.
+ outExecutionDuration: null
+ };
+ let currentDir = yield OS.File.getCurrentDirectory();
+ let pathSource = OS.Path.join(currentDir, "test_duration.js");
+ let copyFile = pathSource + ".bak";
+ function testOptions(options, name) {
+ do_print("Checking outExecutionDuration for operation: " + name);
+ do_print(name + ": Gathered method duration time: " +
+ options.outExecutionDuration + "ms");
+ // Making sure that duration was updated.
+ do_check_eq(typeof options.outExecutionDuration, "number");
+ do_check_true(options.outExecutionDuration >= 0);
+ };
+ // Testing duration of OS.File.copy.
+ yield OS.File.copy(pathSource, copyFile, copyOptions);
+ testOptions(copyOptions, "OS.File.copy");
+ yield OS.File.remove(copyFile);
+
+ // Trying an operation where options are cloned.
+ let pathDest = OS.Path.join(OS.Constants.Path.tmpDir,
+ "osfile async test read writeAtomic.tmp");
+ let tmpPath = pathDest + ".tmp";
+ let readOptions = {
+ outExecutionDuration: null
+ };
+ let contents = yield OS.File.read(pathSource, undefined, readOptions);
+ testOptions(readOptions, "OS.File.read");
+ // Options structure passed to a OS.File writeAtomic method.
+ let writeAtomicOptions = {
+ // This field should be first initialized with the actual
+ // duration measurement then progressively incremented.
+ outExecutionDuration: null,
+ tmpPath: tmpPath
+ };
+ yield OS.File.writeAtomic(pathDest, contents, writeAtomicOptions);
+ testOptions(writeAtomicOptions, "OS.File.writeAtomic");
+ yield OS.File.remove(pathDest);
+
+ do_print("Ensuring that we can use outExecutionDuration to accumulate durations");
+
+ let ARBITRARY_BASE_DURATION = 5;
+ copyOptions = {
+ // This field should now be incremented with the actual duration
+ // measurement.
+ outExecutionDuration: ARBITRARY_BASE_DURATION
+ };
+ let backupDuration = ARBITRARY_BASE_DURATION;
+ // Testing duration of OS.File.copy.
+ yield OS.File.copy(pathSource, copyFile, copyOptions);
+
+ do_check_true(copyOptions.outExecutionDuration >= backupDuration);
+
+ backupDuration = copyOptions.outExecutionDuration;
+ yield OS.File.remove(copyFile, copyOptions);
+ do_check_true(copyOptions.outExecutionDuration >= backupDuration);
+
+ // Trying an operation where options are cloned.
+ // Options structure passed to a OS.File writeAtomic method.
+ writeAtomicOptions = {
+ // This field should be overridden with the actual duration
+ // measurement.
+ outExecutionDuration: copyOptions.outExecutionDuration,
+ tmpPath: tmpPath
+ };
+ backupDuration = writeAtomicOptions.outExecutionDuration;
+
+ yield OS.File.writeAtomic(pathDest, contents, writeAtomicOptions);
+ do_check_true(copyOptions.outExecutionDuration >= backupDuration);
+ OS.File.remove(pathDest);
+
+ // Testing an operation that doesn't take arguments at all
+ let file = yield OS.File.open(pathSource);
+ yield file.stat();
+ yield file.close();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_exception.js b/toolkit/components/osfile/tests/xpcshell/test_exception.js
new file mode 100644
index 0000000000..1282adb3e1
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_exception.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that functions throw the appropriate exceptions.
+ */
+
+"use strict";
+
+var EXISTING_FILE = do_get_file("xpcshell.ini").path;
+
+
+// Tests on |open|
+
+add_test_pair(function test_typeerror() {
+ let exn;
+ try {
+ let fd = yield OS.File.open("/tmp", {no_such_key: 1});
+ do_print("Fd: " + fd);
+ } catch (ex) {
+ exn = ex;
+ }
+ do_print("Exception: " + exn);
+ do_check_true(exn.constructor.name == "TypeError");
+});
+
+// Tests on |read|
+
+add_test_pair(function* test_bad_encoding() {
+ do_print("Testing with a wrong encoding");
+ try {
+ yield OS.File.read(EXISTING_FILE, { encoding: "baby-speak-encoded" });
+ do_throw("Should have thrown with an ex.becauseInvalidArgument");
+ } catch (ex if ex.becauseInvalidArgument) {
+ do_print("Wrong encoding caused the correct exception");
+ }
+
+ try {
+ yield OS.File.read(EXISTING_FILE, { encoding: 4 });
+ do_throw("Should have thrown a TypeError");
+ } catch (ex if ex.constructor.name == "TypeError") {
+ // Note that TypeError doesn't carry across compartments
+ do_print("Non-string encoding caused the correct exception");
+ }
+ });
+
+add_test_pair(function* test_bad_compression() {
+ do_print("Testing with a non-existing compression");
+ try {
+ yield OS.File.read(EXISTING_FILE, { compression: "mmmh-crunchy" });
+ do_throw("Should have thrown with an ex.becauseInvalidArgument");
+ } catch (ex if ex.becauseInvalidArgument) {
+ do_print("Wrong encoding caused the correct exception");
+ }
+
+ do_print("Testing with a bad type for option compression");
+ try {
+ yield OS.File.read(EXISTING_FILE, { compression: 5 });
+ do_throw("Should have thrown a TypeError");
+ } catch (ex if ex.constructor.name == "TypeError") {
+ // Note that TypeError doesn't carry across compartments
+ do_print("Non-string encoding caused the correct exception");
+ }
+});
+
+add_test_pair(function* test_bad_bytes() {
+ do_print("Testing with a bad type for option bytes");
+ try {
+ yield OS.File.read(EXISTING_FILE, { bytes: "five" });
+ do_throw("Should have thrown a TypeError");
+ } catch (ex if ex.constructor.name == "TypeError") {
+ // Note that TypeError doesn't carry across compartments
+ do_print("Non-number bytes caused the correct exception");
+ }
+});
+
+add_test_pair(function* read_non_existent() {
+ do_print("Testing with a non-existent file");
+ try {
+ yield OS.File.read("I/do/not/exist");
+ do_throw("Should have thrown with an ex.becauseNoSuchFile");
+ } catch (ex if ex.becauseNoSuchFile) {
+ do_print("Correct exceptions");
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js
new file mode 100644
index 0000000000..3ec42065bd
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/osfile.jsm");
+ Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+ let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
+
+ // Test cases for filePathToURI
+ let paths = isWindows ? [
+ 'C:\\',
+ 'C:\\test',
+ 'C:\\test\\',
+ 'C:\\test%2f',
+ 'C:\\test\\test\\test',
+ 'C:\\test;+%',
+ 'C:\\test?action=index\\',
+ 'C:\\test\ test',
+ '\\\\C:\\a\\b\\c',
+ '\\\\Server\\a\\b\\c',
+
+ // note that per http://support.microsoft.com/kb/177506 (under more info),
+ // the following characters are allowed on Windows:
+ 'C:\\char^',
+ 'C:\\char&',
+ 'C:\\char\'',
+ 'C:\\char@',
+ 'C:\\char{',
+ 'C:\\char}',
+ 'C:\\char[',
+ 'C:\\char]',
+ 'C:\\char,',
+ 'C:\\char$',
+ 'C:\\char=',
+ 'C:\\char!',
+ 'C:\\char-',
+ 'C:\\char#',
+ 'C:\\char(',
+ 'C:\\char)',
+ 'C:\\char%',
+ 'C:\\char.',
+ 'C:\\char+',
+ 'C:\\char~',
+ 'C:\\char_'
+ ] : [
+ '/',
+ '/test',
+ '/test/',
+ '/test%2f',
+ '/test/test/test',
+ '/test;+%',
+ '/test?action=index/',
+ '/test\ test',
+ '/punctuation/;,/?:@&=+$-_.!~*\'()[]"#',
+ '/CasePreserving'
+ ];
+
+ // some additional URIs to test, beyond those generated from paths
+ let uris = isWindows ? [
+ 'file:///C:/test/',
+ 'file://localhost/C:/test',
+ 'file:///c:/test/test.txt',
+ //'file:///C:/foo%2f', // trailing, encoded slash
+ 'file:///C:/%3f%3F',
+ 'file:///C:/%3b%3B',
+ 'file:///C:/%3c%3C', // not one of the special-cased ? or ;
+ 'file:///C:/%78', // 'x', not usually uri encoded
+ 'file:///C:/test#frag', // a fragment identifier
+ 'file:///C:/test?action=index' // an actual query component
+ ] : [
+ 'file:///test/',
+ 'file://localhost/test',
+ 'file:///test/test.txt',
+ 'file:///foo%2f', // trailing, encoded slash
+ 'file:///%3f%3F',
+ 'file:///%3b%3B',
+ 'file:///%3c%3C', // not one of the special-cased ? or ;
+ 'file:///%78', // 'x', not usually uri encoded
+ 'file:///test#frag', // a fragment identifier
+ 'file:///test?action=index' // an actual query component
+ ];
+
+ for (let path of paths) {
+ // convert that to a uri using FileUtils and Services, which toFileURI is trying to model
+ let file = FileUtils.File(path);
+ let uri = Services.io.newFileURI(file).spec;
+ do_check_eq(uri, OS.Path.toFileURI(path));
+
+ // keep the resulting URI to try the reverse, except for "C:\" for which the
+ // behavior of nsIFileURL and OS.File is inconsistent
+ if (path != "C:\\") {
+ uris.push(uri);
+ }
+ }
+
+ for (let uri of uris) {
+ // convert URIs to paths with nsIFileURI, which fromFileURI is trying to model
+ let path = Services.io.newURI(uri, null, null).QueryInterface(Components.interfaces.nsIFileURL).file.path;
+ do_check_eq(path, OS.Path.fromFileURI(uri));
+ }
+
+ // check that non-file URLs aren't allowed
+ let thrown = false;
+ try {
+ OS.Path.fromFileURI('http://test.com')
+ } catch (e) {
+ do_check_eq(e.message, "fromFileURI expects a file URI");
+ thrown = true;
+ }
+ do_check_true(thrown);
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_loader.js b/toolkit/components/osfile/tests/xpcshell/test_loader.js
new file mode 100644
index 0000000000..dcfa819be8
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_loader.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that OS.File can be loaded using the CommonJS loader.
+ */
+
+var { Loader } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
+
+function run_test() {
+ run_next_test();
+}
+
+
+add_task(function*() {
+ let dataDir = Services.io.newFileURI(do_get_file("test_loader/", true)).spec + "/";
+ let loader = Loader.Loader({
+ paths: {'': dataDir }
+ });
+
+ let require = Loader.Require(loader, Loader.Module('module_test_loader', 'foo'));
+ do_print("Require is ready");
+ try {
+ require('module_test_loader');
+ } catch (error) {
+ dump('Bootstrap error: ' +
+ (error.message ? error.message : String(error)) + '\n' +
+ (error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
+
+ throw error;
+ }
+
+ do_print("Require has worked");
+});
+
diff --git a/toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js b/toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js
new file mode 100644
index 0000000000..18356d6ad8
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load OS.File from a module loaded with the CommonJS/addon-sdk loader
+
+var {Cu} = require("chrome");
+Cu.import('resource://gre/modules/osfile.jsm');
diff --git a/toolkit/components/osfile/tests/xpcshell/test_logging.js b/toolkit/components/osfile/tests/xpcshell/test_logging.js
new file mode 100644
index 0000000000..133909e0be
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_logging.js
@@ -0,0 +1,74 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests logging by passing OS.Shared.LOG both an object with its own
+ * toString method, and one with the default.
+ */
+function run_test() {
+ do_test_pending();
+ let messageCount = 0;
+
+ do_print("Test starting");
+
+ // Create a console listener.
+ let consoleListener = {
+ observe: function (aMessage) {
+ //Ignore unexpected messages.
+ if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
+ return;
+ }
+ // This is required, as printing to the |Services.console|
+ // while in the observe function causes an exception.
+ do_execute_soon(function() {
+ do_print("Observing message " + aMessage.message);
+ if (aMessage.message.indexOf("TEST OS") < 0) {
+ return;
+ }
+
+ ++messageCount;
+ if(messageCount == 1) {
+ do_check_eq(aMessage.message, "TEST OS {\"name\":\"test\"}\n");
+ }
+ if(messageCount == 2) {
+ do_check_eq(aMessage.message, "TEST OS name is test\n");
+ toggleConsoleListener(false);
+ do_test_finished();
+ }
+ });
+ }
+ };
+
+ // Set/Unset the console listener.
+ function toggleConsoleListener (pref) {
+ do_print("Setting console listener: " + pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref);
+ Services.console[pref ? "registerListener" : "unregisterListener"](
+ consoleListener);
+ }
+
+ toggleConsoleListener(true);
+
+ let objectDefault = {name: "test"};
+ let CustomToString = function() {
+ this.name = "test";
+ };
+ CustomToString.prototype.toString = function() {
+ return "name is " + this.name;
+ };
+ let objectCustom = new CustomToString();
+
+ do_print(OS.Shared.LOG.toSource());
+
+ do_print("Logging 1");
+ OS.Shared.LOG(objectDefault);
+
+ do_print("Logging 2");
+ OS.Shared.LOG(objectCustom);
+ // Once both messages are observed OS.Shared.DEBUG, and OS.Shared.TEST
+ // are reset to false.
+}
+
diff --git a/toolkit/components/osfile/tests/xpcshell/test_makeDir.js b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js
new file mode 100644
index 0000000000..5b9740a7ff
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_makeDir.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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Path = OS.Path;
+var profileDir;
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * Test OS.File.makeDir
+ */
+
+add_task(function init() {
+ // Set up profile. We create the directory in the profile, because the profile
+ // is removed after every test run.
+ do_get_profile();
+ profileDir = OS.Constants.Path.profileDir;
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+});
+
+/**
+ * Basic use
+ */
+
+add_task(function* test_basic() {
+ let dir = Path.join(profileDir, "directory");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Make a directory
+ yield OS.File.makeDir(dir);
+
+ //check if the directory exists
+ yield OS.File.stat(dir);
+
+ // Make a directory that already exists, this should succeed
+ yield OS.File.makeDir(dir);
+
+ // Make a directory with ignoreExisting
+ yield OS.File.makeDir(dir, {ignoreExisting: true});
+
+ // Make a directory with ignoreExisting false
+ let exception = null;
+ try {
+ yield OS.File.makeDir(dir, {ignoreExisting: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseExists);
+});
+
+// Make a root directory that already exists
+add_task(function* test_root() {
+ if (OS.Constants.Win) {
+ yield OS.File.makeDir("C:");
+ yield OS.File.makeDir("C:\\");
+ } else {
+ yield OS.File.makeDir("/");
+ }
+});
+
+/**
+ * Creating subdirectories
+ */
+add_task(function test_option_from() {
+ let dir = Path.join(profileDir, "a", "b", "c");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Make a directory
+ yield OS.File.makeDir(dir, {from: profileDir});
+
+ //check if the directory exists
+ yield OS.File.stat(dir);
+
+ // Make a directory that already exists, this should succeed
+ yield OS.File.makeDir(dir);
+
+ // Make a directory with ignoreExisting
+ yield OS.File.makeDir(dir, {ignoreExisting: true});
+
+ // Make a directory with ignoreExisting false
+ let exception = null;
+ try {
+ yield OS.File.makeDir(dir, {ignoreExisting: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseExists);
+
+ // Make a directory without |from| and fail
+ let dir2 = Path.join(profileDir, "g", "h", "i");
+ exception = null;
+ try {
+ yield OS.File.makeDir(dir2);
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseNoSuchFile);
+
+ // Test edge cases on paths
+
+ let dir3 = Path.join(profileDir, "d", "", "e", "f");
+ do_check_false((yield OS.File.exists(dir3)));
+ yield OS.File.makeDir(dir3, {from: profileDir});
+ do_check_true((yield OS.File.exists(dir3)));
+
+ let dir4;
+ if (OS.Constants.Win) {
+ // Test that we can create a directory recursively even
+ // if we have too many "\\".
+ dir4 = profileDir + "\\\\g";
+ } else {
+ dir4 = profileDir + "////g";
+ }
+ do_check_false((yield OS.File.exists(dir4)));
+ yield OS.File.makeDir(dir4, {from: profileDir});
+ do_check_true((yield OS.File.exists(dir4)));
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_open.js b/toolkit/components/osfile/tests/xpcshell/test_open.js
new file mode 100644
index 0000000000..78772ad09a
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_open.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * Test OS.File.open for reading:
+ * - with an existing file (should succeed);
+ * - with a non-existing file (should fail);
+ * - with inconsistent arguments (should fail).
+ */
+add_task(function() {
+ // Attempt to open a file that does not exist, ensure that it yields the
+ // appropriate error.
+ try {
+ let fd = yield OS.File.open(OS.Path.join(".", "This file does not exist"));
+ do_check_true(false, "File opening 1 succeeded (it should fail)");
+ } catch (err if err instanceof OS.File.Error && err.becauseNoSuchFile) {
+ do_print("File opening 1 failed " + err);
+ }
+
+ // Attempt to open a file with the wrong args, so that it fails before
+ // serialization, ensure that it yields the appropriate error.
+ do_print("Attempting to open a file with wrong arguments");
+ try {
+ let fd = yield OS.File.open(1, 2, 3);
+ do_check_true(false, "File opening 2 succeeded (it should fail)" + fd);
+ } catch (err) {
+ do_print("File opening 2 failed " + err);
+ do_check_false(err instanceof OS.File.Error,
+ "File opening 2 returned something that is not a file error");
+ do_check_true(err.constructor.name == "TypeError",
+ "File opening 2 returned a TypeError");
+ }
+
+ // Attempt to open a file correctly
+ do_print("Attempting to open a file correctly");
+ let openedFile = yield OS.File.open(OS.Path.join(do_get_cwd().path, "test_open.js"));
+ do_print("File opened correctly");
+
+ do_print("Attempting to close a file correctly");
+ yield openedFile.close();
+
+ do_print("Attempting to close a file again");
+ yield openedFile.close();
+});
+
+/**
+ * Test the error thrown by OS.File.open when attempting to open a directory
+ * that does not exist.
+ */
+add_task(function test_error_attributes () {
+
+ let dir = OS.Path.join(do_get_profile().path, "test_osfileErrorAttrs");
+ let fpath = OS.Path.join(dir, "test_error_attributes.txt");
+
+ try {
+ yield OS.File.open(fpath, {truncate: true}, {});
+ do_check_true(false, "Opening path suceeded (it should fail) " + fpath);
+ } catch (err) {
+ do_check_true(err instanceof OS.File.Error);
+ do_check_true(err.becauseNoSuchFile);
+ }
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js
new file mode 100644
index 0000000000..0f86b2ea8b
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js
@@ -0,0 +1,16 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+/**
+ * A trivial test ensuring that we can call osfile from xpcshell.
+ * (see bug 808161)
+ */
+
+function run_test() {
+ do_test_pending();
+ OS.File.getCurrentDirectory().then(
+ do_test_finished,
+ do_test_finished
+ );
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js
new file mode 100644
index 0000000000..0aef2c58af
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js
@@ -0,0 +1,122 @@
+"use strict";
+
+do_print("starting tests");
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * A test to check that the |append| mode flag is correctly implemented.
+ * (see bug 925865)
+ */
+
+function setup_mode(mode) {
+ // Complete mode.
+ let realMode = {
+ read: true,
+ write: true
+ };
+ for (let k in mode) {
+ realMode[k] = mode[k];
+ }
+ return realMode;
+}
+
+// Test append mode.
+function test_append(mode) {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_append.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ mode = setup_mode(mode);
+ mode.append = true;
+ if (mode.trunc) {
+ // Pre-fill file with some data to see if |trunc| actually works.
+ yield OS.File.writeAtomic(path, new Uint8Array(500));
+ }
+ let file = yield OS.File.open(path, mode);
+ try {
+ yield file.write(new Uint8Array(1000));
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.read(100);
+ // Should be at offset 100, length 1000 now.
+ yield file.write(new Uint8Array(100));
+ // Should be at offset 1100, length 1100 now.
+ let stat = yield file.stat();
+ do_check_eq(1100, stat.size);
+ } finally {
+ yield file.close();
+ }
+ } catch(ex) {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ }
+}
+
+// Test no-append mode.
+function test_no_append(mode) {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_noappend.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ mode = setup_mode(mode);
+ mode.append = false;
+ if (mode.trunc) {
+ // Pre-fill file with some data to see if |trunc| actually works.
+ yield OS.File.writeAtomic(path, new Uint8Array(500));
+ }
+ let file = yield OS.File.open(path, mode);
+ try {
+ yield file.write(new Uint8Array(1000));
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.read(100);
+ // Should be at offset 100, length 1000 now.
+ yield file.write(new Uint8Array(100));
+ // Should be at offset 200, length 1000 now.
+ let stat = yield file.stat();
+ do_check_eq(1000, stat.size);
+ } finally {
+ yield file.close();
+ }
+ } finally {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ }
+}
+
+var test_flags = [
+ {},
+ {create:true},
+ {trunc:true}
+];
+function run_test() {
+ do_test_pending();
+
+ for (let t of test_flags) {
+ add_task(test_append.bind(null, t));
+ add_task(test_no_append.bind(null, t));
+ }
+ add_task(do_test_finished);
+
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js
new file mode 100644
index 0000000000..68fa9152cd
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js
@@ -0,0 +1,39 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * Test to ensure that {bytes:} in options to |write| is correctly
+ * preserved.
+ */
+add_task(function* test_bytes() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_bytes.tmp");
+ let file = yield OS.File.open(path, {trunc: true, read: true, write: true});
+ try {
+ try {
+ // 1. Test write, by supplying {bytes:} options smaller than the actual
+ // buffer.
+ yield file.write(new Uint8Array(2048), {bytes: 1024});
+ do_check_eq((yield file.stat()).size, 1024);
+
+ // 2. Test that passing nullish values for |options| still works.
+ yield file.setPosition(0, OS.File.POS_END);
+ yield file.write(new Uint8Array(1024), null);
+ yield file.write(new Uint8Array(1024), undefined);
+ do_check_eq((yield file.stat()).size, 3072);
+ } finally {
+ yield file.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js
new file mode 100644
index 0000000000..9c52c8a80d
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js
@@ -0,0 +1,113 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * A file that we know exists and that can be used for reading.
+ */
+var EXISTING_FILE = "test_osfile_async_copy.js";
+
+/**
+ * Fetch asynchronously the contents of a file using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} path The _absolute_ path to the file.
+ * @return {promise}
+ * @resolves {string} The contents of the file.
+ */
+var reference_fetch_file = function reference_fetch_file(path) {
+ let promise = Promise.defer();
+ let file = new FileUtils.File(path);
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function(stream, status) {
+ if (!Components.isSuccessCode(status)) {
+ promise.reject(status);
+ return;
+ }
+ let result, reject;
+ try {
+ result = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (x) {
+ reject = x;
+ }
+ stream.close();
+ if (reject) {
+ promise.reject(reject);
+ } else {
+ promise.resolve(result);
+ }
+ });
+
+ return promise.promise;
+};
+
+/**
+ * Compare asynchronously the contents two files using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} a The _absolute_ path to the first file.
+ * @param {string} b The _absolute_ path to the second file.
+ *
+ * @resolves {null}
+ */
+var reference_compare_files = function reference_compare_files(a, b) {
+ let a_contents = yield reference_fetch_file(a);
+ let b_contents = yield reference_fetch_file(b);
+ // Not using do_check_eq to avoid dumping the whole file to the log.
+ // It is OK to === compare here, as both variables contain a string.
+ do_check_true(a_contents === b_contents);
+};
+
+/**
+ * Test to ensure that OS.File.copy works.
+ */
+function test_copymove(options = {}) {
+ let source = OS.Path.join((yield OS.File.getCurrentDirectory()),
+ EXISTING_FILE);
+ let dest = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_copy_dest.tmp");
+ let dest2 = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_copy_dest2.tmp");
+ try {
+ // 1. Test copy.
+ yield OS.File.copy(source, dest, options);
+ yield reference_compare_files(source, dest);
+ // 2. Test subsequent move.
+ yield OS.File.move(dest, dest2);
+ yield reference_compare_files(source, dest2);
+ // 3. Check that the moved file was really moved.
+ do_check_eq((yield OS.File.exists(dest)), false);
+ } finally {
+ try {
+ yield OS.File.remove(dest);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+ try {
+ yield OS.File.remove(dest2);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+ }
+}
+
+// Regular copy test.
+add_task(test_copymove);
+// Userland copy test.
+add_task(test_copymove.bind(null, {unixUserland: true}));
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js
new file mode 100644
index 0000000000..9ed087f4e9
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js
@@ -0,0 +1,30 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * Test to ensure that |File.prototype.flush| is available in the async API.
+ */
+
+add_task(function test_flush() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_flush.tmp");
+ let file = yield OS.File.open(path, {trunc: true, write: true});
+ try {
+ try {
+ yield file.flush();
+ } finally {
+ yield file.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js
new file mode 100644
index 0000000000..a9ac776b0b
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/ctypes.jsm");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * A test to check that .getPosition/.setPosition work with large files.
+ * (see bug 952997)
+ */
+
+// Test setPosition/getPosition.
+function test_setPosition(forward, current, backward) {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_largefiles.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ let file = yield OS.File.open(path, {write:true, append:false});
+ try {
+ let pos = 0;
+
+ // 1. seek forward from start
+ do_print("Moving forward: " + forward);
+ yield file.setPosition(forward, OS.File.POS_START);
+ pos += forward;
+ do_check_eq((yield file.getPosition()), pos);
+
+ // 2. seek forward from current position
+ do_print("Moving current: " + current);
+ yield file.setPosition(current, OS.File.POS_CURRENT);
+ pos += current;
+ do_check_eq((yield file.getPosition()), pos);
+
+ // 3. seek backward from current position
+ do_print("Moving current backward: " + backward);
+ yield file.setPosition(-backward, OS.File.POS_CURRENT);
+ pos -= backward;
+ do_check_eq((yield file.getPosition()), pos);
+
+ } finally {
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.close();
+ }
+ } catch(ex) {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ do_throw(ex);
+ }
+}
+
+// Test setPosition/getPosition expected failures.
+function test_setPosition_failures() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_largefiles.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ let file = yield OS.File.open(path, {write:true, append:false});
+ try {
+ let pos = 0;
+
+ // 1. Use an invalid position value
+ try {
+ yield file.setPosition(0.5, OS.File.POS_START);
+ do_throw("Shouldn't have succeeded");
+ } catch (ex) {
+ do_check_true(ex.toString().includes("can't pass"));
+ }
+ // Since setPosition should have bailed, it shouldn't have moved the
+ // file pointer at all.
+ do_check_eq((yield file.getPosition()), 0);
+
+ // 2. Use an invalid position value
+ try {
+ yield file.setPosition(0xffffffff + 0.5, OS.File.POS_START);
+ do_throw("Shouldn't have succeeded");
+ } catch (ex) {
+ do_check_true(ex.toString().includes("can't pass"));
+ }
+ // Since setPosition should have bailed, it shouldn't have moved the
+ // file pointer at all.
+ do_check_eq((yield file.getPosition()), 0);
+
+ // 3. Use a position that cannot be represented as a double
+ try {
+ // Not all numbers after 9007199254740992 can be represented as a
+ // double. E.g. in js 9007199254740992 + 1 == 9007199254740992
+ yield file.setPosition(9007199254740992, OS.File.POS_START);
+ yield file.setPosition(1, OS.File.POS_CURRENT);
+ do_throw("Shouldn't have succeeded");
+ } catch (ex) {
+ do_print(ex.toString());
+ do_check_true(!!ex);
+ }
+
+ } finally {
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.close();
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ }
+ } catch(ex) {
+ do_throw(ex);
+ }
+}
+
+function run_test() {
+ // First verify stuff works for small values.
+ add_task(test_setPosition.bind(null, 0, 100, 50));
+ add_task(test_setPosition.bind(null, 1000, 100, 50));
+ add_task(test_setPosition.bind(null, 1000, -100, -50));
+
+ if (OS.Constants.Win || ctypes.off_t.size >= 8) {
+ // Now verify stuff still works for large values.
+ // 1. Multiple small seeks, which add up to > MAXINT32
+ add_task(test_setPosition.bind(null, 0x7fffffff, 0x7fffffff, 0));
+ // 2. Plain large seek, that should end up at 0 again.
+ // 0xffffffff also happens to be the INVALID_SET_FILE_POINTER value on
+ // Windows, so this also tests the error handling
+ add_task(test_setPosition.bind(null, 0, 0xffffffff, 0xffffffff));
+ // 3. Multiple large seeks that should end up > MAXINT32.
+ add_task(test_setPosition.bind(null, 0xffffffff, 0xffffffff, 0xffffffff));
+ // 5. Multiple large seeks with negative offsets.
+ add_task(test_setPosition.bind(null, 0xffffffff, -0x7fffffff, 0x7fffffff));
+
+ // 6. Check failures
+ add_task(test_setPosition_failures);
+ }
+
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
new file mode 100644
index 0000000000..17d3afa7c1
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
@@ -0,0 +1,211 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * A test to ensure that OS.File.setDates and OS.File.prototype.setDates are
+ * working correctly.
+ * (see bug 924916)
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+// Non-prototypical tests, operating on path names.
+add_task(function* test_nonproto() {
+ // First, create a file we can mess with.
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setDates_nonproto.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ // 1. Try to set some well known dates.
+ // We choose multiples of 2000ms, because the time stamp resolution of
+ // the underlying OS might not support something more precise.
+ const accDate = 2000;
+ const modDate = 4000;
+ {
+ yield OS.File.setDates(path, accDate, modDate);
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.1 Try to omit modificationDate (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield OS.File.setDates(path, accDate);
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.2 Try to omit accessDate as well (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield OS.File.setDates(path);
+ let stat = yield OS.File.stat(path);
+ do_check_neq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 3. Repeat 1., but with Date objects this time
+ {
+ yield OS.File.setDates(path, new Date(accDate), new Date(modDate));
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 4. Check that invalid params will cause an exception/rejection.
+ {
+ for (let p of ["invalid", new Uint8Array(1), NaN]) {
+ try {
+ yield OS.File.setDates(path, p, modDate);
+ do_throw("Invalid access date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield OS.File.setDates(path, accDate, p);
+ do_throw("Invalid modification date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield OS.File.setDates(path, p, p);
+ do_throw("Invalid dates should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ }
+ }
+ } finally {
+ // Remove the temp file again
+ yield OS.File.remove(path);
+ }
+});
+
+// Prototypical tests, operating on |File| handles.
+add_task(function* test_proto() {
+ if (OS.Constants.Sys.Name == "Android" || OS.Constants.Sys.Name == "Gonk") {
+ do_print("File.prototype.setDates is not implemented for Android/B2G");
+ do_check_eq(OS.File.prototype.setDates, undefined);
+ return;
+ }
+
+ // First, create a file we can mess with.
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setDates_proto.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, {write: true});
+
+ try {
+ // 1. Try to set some well known dates.
+ // We choose multiples of 2000ms, because the time stamp resolution of
+ // the underlying OS might not support something more precise.
+ const accDate = 2000;
+ const modDate = 4000;
+ {
+ yield fd.setDates(accDate, modDate);
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.1 Try to omit modificationDate (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield fd.setDates(accDate);
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.2 Try to omit accessDate as well (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield fd.setDates();
+ let stat = yield fd.stat();
+ do_check_neq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 3. Repeat 1., but with Date objects this time
+ {
+ yield fd.setDates(new Date(accDate), new Date(modDate));
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 4. Check that invalid params will cause an exception/rejection.
+ {
+ for (let p of ["invalid", new Uint8Array(1), NaN]) {
+ try {
+ yield fd.setDates(p, modDate);
+ do_throw("Invalid access date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield fd.setDates(accDate, p);
+ do_throw("Invalid modification date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield fd.setDates(p, p);
+ do_throw("Invalid dates should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ }
+ }
+ } finally {
+ yield fd.close();
+ }
+ } finally {
+ // Remove the temp file again
+ yield OS.File.remove(path);
+ }
+});
+
+// Tests setting dates on directories.
+add_task(function* test_dirs() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setDates_dir");
+ yield OS.File.makeDir(path);
+
+ try {
+ // 1. Try to set some well known dates.
+ // We choose multiples of 2000ms, because the time stamp resolution of
+ // the underlying OS might not support something more precise.
+ const accDate = 2000;
+ const modDate = 4000;
+ {
+ yield OS.File.setDates(path, accDate, modDate);
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ } finally {
+ yield OS.File.removeEmptyDir(path);
+ }
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js
new file mode 100644
index 0000000000..ab8bf7dd9b
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * A test to ensure that OS.File.setPermissions and
+ * OS.File.prototype.setPermissions are all working correctly.
+ * (see bug 1001849)
+ * These functions are currently Unix-specific. The manifest skips
+ * the test on Windows.
+ */
+
+/**
+ * Helper function for test logging: prints a POSIX file permission mode as an
+ * octal number, with a leading '0' per C (not JS) convention. When the
+ * numeric value is 0o777 or lower, it is padded on the left with zeroes to
+ * four digits wide.
+ * Sample outputs: 0022, 0644, 04755.
+ */
+function format_mode(mode) {
+ if (mode <= 0o777) {
+ return ("0000" + mode.toString(8)).slice(-4);
+ } else {
+ return "0" + mode.toString(8);
+ }
+}
+
+const _umask = OS.Constants.Sys.umask;
+do_print("umask: " + format_mode(_umask));
+
+/**
+ * Compute the mode that a file should have after applying the umask,
+ * whatever it happens to be.
+ */
+function apply_umask(mode) {
+ return mode & ~_umask;
+}
+
+// Sequence of setPermission parameters and expected file mode. The first test
+// checks the permissions when the file is first created.
+var testSequence = [
+ [null, apply_umask(0o600)],
+ [{ unixMode: 0o4777 }, apply_umask(0o4777)],
+ [{ unixMode: 0o4777, unixHonorUmask: false }, 0o4777],
+ [{ unixMode: 0o4777, unixHonorUmask: true }, apply_umask(0o4777)],
+ [undefined, apply_umask(0o600)],
+ [{ unixMode: 0o666 }, apply_umask(0o666)],
+ [{ unixMode: 0o600 }, apply_umask(0o600)],
+ [{ unixMode: 0 }, 0],
+ [{}, apply_umask(0o600)],
+];
+
+// Test application to paths.
+add_task(function* test_path_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setPermissions_path.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ for (let [options, expectedMode] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield OS.File.setPermissions(path, options);
+ }
+
+ let stat = yield OS.File.stat(path);
+ do_check_eq(format_mode(stat.unixMode), format_mode(expectedMode));
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+// Test application to open files.
+add_task(function* test_file_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setPermissions_file.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, { write: true });
+ try {
+ for (let [options, expectedMode] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield fd.setPermissions(options);
+ }
+
+ let stat = yield fd.stat();
+ do_check_eq(format_mode(stat.unixMode), format_mode(expectedMode));
+ }
+ } finally {
+ yield fd.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js
new file mode 100644
index 0000000000..5740f7f1ad
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js
@@ -0,0 +1,48 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+add_task(function test_closed() {
+ OS.Shared.DEBUG = true;
+ let currentDir = yield OS.File.getCurrentDirectory();
+ do_print("Open a file, ensure that we can call stat()");
+ let path = OS.Path.join(currentDir, "test_osfile_closed.js");
+ let file = yield OS.File.open(path);
+ yield file.stat();
+ do_check_true(true);
+
+ yield file.close();
+
+ do_print("Ensure that we cannot stat() on closed file");
+ let exn;
+ try {
+ yield file.stat();
+ } catch (ex) {
+ exn = ex;
+ }
+ do_print("Ensure that this raises the correct error");
+ do_check_true(!!exn);
+ do_check_true(exn instanceof OS.File.Error);
+ do_check_true(exn.becauseClosed);
+
+ do_print("Ensure that we cannot read() on closed file");
+ exn = null;
+ try {
+ yield file.read();
+ } catch (ex) {
+ exn = ex;
+ }
+ do_print("Ensure that this raises the correct error");
+ do_check_true(!!exn);
+ do_check_true(exn instanceof OS.File.Error);
+ do_check_true(exn.becauseClosed);
+
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js
new file mode 100644
index 0000000000..a1c319eca7
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* testFileError_with_writeAtomic() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "testFileError.tmp");
+ yield File.remove(path);
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ let exception;
+ try {
+ yield File.writeAtomic(path, DEFAULT_CONTENTS, { noOverwrite: true });
+ } catch (ex) {
+ exception = ex;
+ }
+ do_check_true(exception instanceof File.Error);
+ do_check_true(exception.path == path);
+});
+
+add_task(function* testFileError_with_makeDir() {
+ let path = Path.join(Constants.Path.tmpDir,
+ "directory");
+ yield File.removeDir(path);
+ yield File.makeDir(path);
+ let exception;
+ try {
+ yield File.makeDir(path, { ignoreExisting: false });
+ } catch (ex) {
+ exception = ex;
+ }
+ do_check_true(exception instanceof File.Error);
+ do_check_true(exception.path == path);
+});
+
+add_task(function* testFileError_with_move() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let sourcePath = Path.join(Constants.Path.tmpDir,
+ "src.tmp");
+ let destPath = Path.join(Constants.Path.tmpDir,
+ "dest.tmp");
+ yield File.remove(sourcePath);
+ yield File.remove(destPath);
+ yield File.writeAtomic(sourcePath, DEFAULT_CONTENTS);
+ yield File.writeAtomic(destPath, DEFAULT_CONTENTS);
+ let exception;
+ try {
+ yield File.move(sourcePath, destPath, { noOverwrite: true });
+ } catch (ex) {
+ exception = ex;
+ }
+ do_print(exception);
+ do_check_true(exception instanceof File.Error);
+ do_check_true(exception.path == sourcePath);
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js
new file mode 100644
index 0000000000..e32c37224f
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js
@@ -0,0 +1,100 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+// We want the actual global to get at the internals since Scheduler is not
+// exported.
+var AsyncFrontGlobal = Components.utils.import(
+ "resource://gre/modules/osfile/osfile_async_front.jsm",
+ null);
+var Scheduler = AsyncFrontGlobal.Scheduler;
+
+/**
+ * Verify that Scheduler.kill() interacts with other OS.File requests correctly,
+ * and that no requests are lost. This is relevant because on B2G we
+ * auto-kill the worker periodically, making it very possible for valid requests
+ * to be interleaved with the automatic kill().
+ *
+ * This test is being created with the fix for Bug 1125989 where `kill` queue
+ * management was found to be buggy. It is a glass-box test that explicitly
+ * re-creates the observed failure situation; it is not guaranteed to prevent
+ * all future regressions. The following is a detailed explanation of the test
+ * for your benefit if this test ever breaks or you are wondering what was the
+ * point of all this. You might want to skim the code below first.
+ *
+ * OS.File maintains a `queue` of operations to be performed. This queue is
+ * nominally implemented as a chain of promises. Every time a new job is
+ * OS.File.push()ed, it effectively becomes the new `queue` promise. (An
+ * extra promise is interposed with a rejection handler to avoid the rejection
+ * cascading, but that does not matter for our purposes.)
+ *
+ * The flaw in `kill` was that it would wait for the `queue` to complete before
+ * replacing `queue`. As a result, another OS.File operation could use `push`
+ * (by way of OS.File.post()) to also use .then() on the same `queue` promise.
+ * Accordingly, assuming that promise was not yet resolved (due to a pending
+ * OS.File request), when it was resolved, both the task scheduled in `kill`
+ * and in `post` would be triggered. Both of those tasks would run until
+ * encountering a call to worker.post().
+ *
+ * Re-creating this race is not entirely trivial because of the large number of
+ * promises used by the code causing control flow to repeatedly be deferred. In
+ * a slightly simpler world we could run the follwing in the same turn of the
+ * event loop and trigger the problem.
+ * - any OS.File request
+ * - Scheduler.kill()
+ * - any OS.File request
+ *
+ * However, we need the Scheduler.kill task to reach the point where it is
+ * waiting on the same `queue` that another task has been scheduled against.
+ * Since the `kill` task yields on the `killQueue` promise prior to yielding
+ * on `queue`, however, some turns of the event loop are required. Happily,
+ * for us, as discussed above, the problem triggers when we have two promises
+ * scheduled on the `queue`, so we can just wait to schedule the second OS.File
+ * request on the queue. (Note that because of the additional then() added to
+ * eat rejections, there is an important difference between the value of
+ * `queue` and the value returned by the first OS.File request.)
+ */
+add_task(function* test_kill_race() {
+ // Ensure the worker has been created and that SET_DEBUG has taken effect.
+ // We have chosen OS.File.exists for our tests because it does not trigger
+ // a rejection and we absolutely do not care what the operation is other
+ // than it does not invoke a native fast-path.
+ yield OS.File.exists('foo.foo');
+
+ do_print('issuing first request');
+ let firstRequest = OS.File.exists('foo.bar');
+ let secondRequest;
+ let secondResolved = false;
+
+ // As noted in our big block comment, we want to wait to schedule the
+ // second request so that it races `kill`'s call to `worker.post`. Having
+ // ourselves wait on the same promise, `queue`, and registering ourselves
+ // before we issue the kill request means we will get run before the `kill`
+ // task resumes and allow us to precisely create the desired race.
+ Scheduler.queue.then(function() {
+ do_print('issuing second request');
+ secondRequest = OS.File.exists('foo.baz');
+ secondRequest.then(function() {
+ secondResolved = true;
+ });
+ });
+
+ do_print('issuing kill request');
+ let killRequest = Scheduler.kill({ reset: true, shutdown: false });
+
+ // Wait on the killRequest so that we can schedule a new OS.File request
+ // after it completes...
+ yield killRequest;
+ // ...because our ordering guarantee ensures that there is at most one
+ // worker (and this usage here should not be vulnerable even with the
+ // bug present), so when this completes the secondRequest has either been
+ // resolved or lost.
+ yield OS.File.exists('foo.goz');
+
+ ok(secondResolved,
+ 'The second request was resolved so we avoided the bug. Victory!');
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js
new file mode 100644
index 0000000000..990d722f5c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * A test to ensure that OS.File.setPermissions and
+ * OS.File.prototype.setPermissions are all working correctly.
+ * (see bug 1022816)
+ * The manifest tests on Windows.
+ */
+
+// Sequence of setPermission parameters.
+var testSequence = [
+ [ { winAttributes: { readOnly: true, system: true, hidden: true } },
+ { readOnly: true, system: true, hidden: true } ],
+ [ { winAttributes: { readOnly: false } },
+ { readOnly: false, system: true, hidden: true } ],
+ [ { winAttributes: { system: false } },
+ { readOnly: false, system: false, hidden: true } ],
+ [ { winAttributes: { hidden: false } },
+ { readOnly: false, system: false, hidden: false } ],
+ [ { winAttributes: {readOnly: true, system: false, hidden: false} },
+ { readOnly: true, system: false, hidden: false } ],
+ [ { winAttributes: {readOnly: false, system: true, hidden: false} },
+ { readOnly: false, system: true, hidden: false } ],
+ [ { winAttributes: {readOnly: false, system: false, hidden: true} },
+ { readOnly: false, system: false, hidden: true } ],
+];
+
+// Test application to paths.
+add_task(function* test_path_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_path.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ for (let [options, attributesExpected] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield OS.File.setPermissions(path, options);
+ }
+
+ let stat = yield OS.File.stat(path);
+ do_print("Got stat winAttributes: " + JSON.stringify(stat.winAttributes));
+
+ do_check_eq(stat.winAttributes.readOnly, attributesExpected.readOnly);
+ do_check_eq(stat.winAttributes.system, attributesExpected.system);
+ do_check_eq(stat.winAttributes.hidden, attributesExpected.hidden);
+
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+// Test application to open files.
+add_task(function* test_file_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_file.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, { write: true });
+ try {
+ for (let [options, attributesExpected] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield fd.setPermissions(options);
+ }
+
+ let stat = yield fd.stat();
+ do_print("Got stat winAttributes: " + JSON.stringify(stat.winAttributes));
+ do_check_eq(stat.winAttributes.readOnly, attributesExpected.readOnly);
+ do_check_eq(stat.winAttributes.system, attributesExpected.system);
+ do_check_eq(stat.winAttributes.hidden, attributesExpected.hidden);
+ }
+ } finally {
+ yield fd.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+// Test application to Check setPermissions on a non-existant file path.
+add_task(function* test_non_existant_file_path_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_path.tmp");
+ Assert.rejects(OS.File.setPermissions(path, {winAttributes: {readOnly: true}}),
+ /The system cannot find the file specified/,
+ "setPermissions failed as expected on a non-existant file path");
+});
+
+// Test application to Check setPermissions on a invalid file handle.
+add_task(function* test_closed_file_handle_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_path.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, { write: true });
+ yield fd.close();
+ Assert.rejects(fd.setPermissions(path, {winAttributes: {readOnly: true}}),
+ /The handle is invalid/,
+ "setPermissions failed as expected on a invalid file handle");
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js
new file mode 100644
index 0000000000..adf345b0c6
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * Remove all temporary files and back up files, including
+ * test_backupTo_option_with_tmpPath.tmp
+ * test_backupTo_option_with_tmpPath.tmp.backup
+ * test_backupTo_option_without_tmpPath.tmp
+ * test_backupTo_option_without_tmpPath.tmp.backup
+ * test_non_backupTo_option.tmp
+ * test_non_backupTo_option.tmp.backup
+ * test_backupTo_option_without_destination_file.tmp
+ * test_backupTo_option_without_destination_file.tmp.backup
+ * test_backupTo_option_with_backup_file.tmp
+ * test_backupTo_option_with_backup_file.tmp.backup
+ */
+function clearFiles() {
+ return Task.spawn(function () {
+ let files = ["test_backupTo_option_with_tmpPath.tmp",
+ "test_backupTo_option_without_tmpPath.tmp",
+ "test_non_backupTo_option.tmp",
+ "test_backupTo_option_without_destination_file.tmp",
+ "test_backupTo_option_with_backup_file.tmp"];
+ for (let file of files) {
+ let path = Path.join(Constants.Path.tmpDir, file);
+ yield File.remove(path);
+ yield File.remove(path + ".backup");
+ }
+ });
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* init() {
+ yield clearFiles();
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| specified
+ * destination file exists
+ * @result destination file will be backed up
+ */
+add_task(function* test_backupTo_option_with_tmpPath() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_with_tmpPath.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ yield File.writeAtomic(path, WRITE_CONTENTS, { tmpPath: path + ".tmp",
+ backupTo: path + ".backup" });
+ do_check_true((yield File.exists(path + ".backup")));
+ let contents = yield File.read(path + ".backup");
+ do_check_eq(DEFAULT_CONTENTS, (new TextDecoder()).decode(contents));
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| not specified
+ * destination file exists
+ * @result destination file will be backed up
+ */
+add_task(function* test_backupTo_option_without_tmpPath() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_without_tmpPath.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ yield File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" });
+ do_check_true((yield File.exists(path + ".backup")));
+ let contents = yield File.read(path + ".backup");
+ do_check_eq(DEFAULT_CONTENTS, (new TextDecoder()).decode(contents));
+});
+
+/**
+ * test when
+ * |backupTo| not specified
+ * |tmpPath| not specified
+ * destination file exists
+ * @result destination file will not be backed up
+ */
+add_task(function* test_non_backupTo_option() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_non_backupTo_option.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ yield File.writeAtomic(path, WRITE_CONTENTS);
+ do_check_false((yield File.exists(path + ".backup")));
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| not specified
+ * destination file not exists
+ * @result no back up file exists
+ */
+add_task(function* test_backupTo_option_without_destination_file() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_without_destination_file.tmp");
+ yield File.remove(path);
+ yield File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" });
+ do_check_false((yield File.exists(path + ".backup")));
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| not specified
+ * backup file exists
+ * destination file exists
+ * @result destination file will be backed up
+ */
+add_task(function* test_backupTo_option_with_backup_file() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_with_backup_file.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+
+ yield File.writeAtomic(path + ".backup", new Uint8Array(1000));
+
+ yield File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" });
+ do_check_true((yield File.exists(path + ".backup")));
+ let contents = yield File.read(path + ".backup");
+ do_check_eq(DEFAULT_CONTENTS, (new TextDecoder()).decode(contents));
+});
+
+add_task(function* cleanup() {
+ yield clearFiles();
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js
new file mode 100644
index 0000000000..a32a690e6f
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+var SHARED_PATH;
+
+add_task(function* init() {
+ do_get_profile();
+ SHARED_PATH = OS.Path.join(OS.Constants.Path.profileDir, "test_osfile_write_zerobytes.tmp");
+});
+
+add_test_pair(function* test_osfile_writeAtomic_zerobytes() {
+ let encoder = new TextEncoder();
+ let string1 = "";
+ let outbin = encoder.encode(string1);
+ yield OS.File.writeAtomic(SHARED_PATH, outbin);
+
+ let decoder = new TextDecoder();
+ let bin = yield OS.File.read(SHARED_PATH);
+ let string2 = decoder.decode(bin);
+ // Checking if writeAtomic supports writing encoded zero-byte strings
+ Assert.equal(string2, string1, "Read the expected (empty) string.");
+});
+
+function run_test() {
+ run_next_test();
+} \ No newline at end of file
diff --git a/toolkit/components/osfile/tests/xpcshell/test_path.js b/toolkit/components/osfile/tests/xpcshell/test_path.js
new file mode 100644
index 0000000000..76a507ee38
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_path.js
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+Services.prefs.setBoolPref("toolkit.osfile.test.syslib_necessary", false);
+ // We don't need libc/kernel32.dll for this test
+
+var ImportWin = {};
+var ImportUnix = {};
+Components.utils.import("resource://gre/modules/osfile/ospath_win.jsm", ImportWin);
+Components.utils.import("resource://gre/modules/osfile/ospath_unix.jsm", ImportUnix);
+
+var Win = ImportWin;
+var Unix = ImportUnix;
+
+function do_check_fail(f)
+{
+ try {
+ let result = f();
+ do_print("Failed do_check_fail: " + result);
+ do_check_true(false);
+ } catch (ex) {
+ do_check_true(true);
+ }
+};
+
+function run_test()
+{
+ do_print("Testing Windows paths");
+
+ do_print("Backslash-separated, no drive");
+ do_check_eq(Win.basename("a\\b"), "b");
+ do_check_eq(Win.basename("a\\b\\"), "");
+ do_check_eq(Win.basename("abc"), "abc");
+ do_check_eq(Win.dirname("a\\b"), "a");
+ do_check_eq(Win.dirname("a\\b\\"), "a\\b");
+ do_check_eq(Win.dirname("a\\\\\\\\b"), "a");
+ do_check_eq(Win.dirname("abc"), ".");
+ do_check_eq(Win.normalize("\\a\\b\\c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f");
+ do_check_eq(Win.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f");
+ do_check_fail(() => Win.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
+
+ do_check_eq(Win.join("\\tmp", "foo", "bar"), "\\tmp\\foo\\bar", "join \\tmp,foo,bar");
+ do_check_eq(Win.join("\\tmp", "\\foo", "bar"), "\\foo\\bar", "join \\tmp,\\foo,bar");
+ do_check_eq(Win.winGetDrive("\\tmp"), null);
+ do_check_eq(Win.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null);
+ do_check_eq(Win.winGetDrive("\\"), null);
+
+
+ do_print("Backslash-separated, with a drive");
+ do_check_eq(Win.basename("c:a\\b"), "b");
+ do_check_eq(Win.basename("c:a\\b\\"), "");
+ do_check_eq(Win.basename("c:abc"), "abc");
+ do_check_eq(Win.dirname("c:a\\b"), "c:a");
+ do_check_eq(Win.dirname("c:a\\b\\"), "c:a\\b");
+ do_check_eq(Win.dirname("c:a\\\\\\\\b"), "c:a");
+ do_check_eq(Win.dirname("c:abc"), "c:");
+ let options = {
+ winNoDrive: true
+ };
+ do_check_eq(Win.dirname("c:a\\b", options), "a");
+ do_check_eq(Win.dirname("c:a\\b\\", options), "a\\b");
+ do_check_eq(Win.dirname("c:a\\\\\\\\b", options), "a");
+ do_check_eq(Win.dirname("c:abc", options), ".");
+ do_check_eq(Win.join("c:", "abc"), "c:\\abc", "join c:,abc");
+
+ do_check_eq(Win.normalize("c:"), "c:\\");
+ do_check_eq(Win.normalize("c:\\"), "c:\\");
+ do_check_eq(Win.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
+ do_check_eq(Win.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
+ do_check_fail(() => Win.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
+
+ do_check_eq(Win.join("c:\\", "foo"), "c:\\foo", "join c:\,foo");
+ do_check_eq(Win.join("c:\\tmp", "foo", "bar"), "c:\\tmp\\foo\\bar", "join c:\\tmp,foo,bar");
+ do_check_eq(Win.join("c:\\tmp", "\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,\\foo,bar");
+ do_check_eq(Win.join("c:\\tmp", "c:\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:\\foo,bar");
+ do_check_eq(Win.join("c:\\tmp", "c:foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:foo,bar");
+ do_check_eq(Win.winGetDrive("c:"), "c:");
+ do_check_eq(Win.winGetDrive("c:\\"), "c:");
+ do_check_eq(Win.winGetDrive("c:abc"), "c:");
+ do_check_eq(Win.winGetDrive("c:abc\\d\\e\\f\\g"), "c:");
+ do_check_eq(Win.winGetDrive("c:\\abc"), "c:");
+ do_check_eq(Win.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:");
+
+ do_print("Forwardslash-separated, no drive");
+ do_check_eq(Win.normalize("/a/b/c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("/a/b////c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("/a/b/c///"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("/a/b/c/../../../d/e/f"), "\\d\\e\\f");
+ do_check_eq(Win.normalize("a/b/c/../../../d/e/f"), "d\\e\\f");
+
+ do_print("Forwardslash-separated, with a drive");
+ do_check_eq(Win.normalize("c:/"), "c:\\");
+ do_check_eq(Win.normalize("c:/a/b/c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:/a/b////c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:////a/b/c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:/a/b/c///"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:/a/b/c/../../../d/e/f"), "c:\\d\\e\\f");
+ do_check_eq(Win.normalize("c:a/b/c/../../../d/e/f"), "c:\\d\\e\\f");
+
+ do_print("Backslash-separated, UNC-style");
+ do_check_eq(Win.basename("\\\\a\\b"), "b");
+ do_check_eq(Win.basename("\\\\a\\b\\"), "");
+ do_check_eq(Win.basename("\\\\abc"), "");
+ do_check_eq(Win.dirname("\\\\a\\b"), "\\\\a");
+ do_check_eq(Win.dirname("\\\\a\\b\\"), "\\\\a\\b");
+ do_check_eq(Win.dirname("\\\\a\\\\\\\\b"), "\\\\a");
+ do_check_eq(Win.dirname("\\\\abc"), "\\\\abc");
+ do_check_eq(Win.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c");
+ do_check_eq(Win.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c");
+ do_check_eq(Win.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c");
+ do_check_eq(Win.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f");
+ do_check_fail(() => Win.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f"));
+
+ do_check_eq(Win.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar");
+ do_check_eq(Win.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar");
+ do_check_eq(Win.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar");
+ do_check_eq(Win.winGetDrive("\\\\"), null);
+ do_check_eq(Win.winGetDrive("\\\\c"), "\\\\c");
+ do_check_eq(Win.winGetDrive("\\\\c\\abc"), "\\\\c");
+
+ do_print("Testing unix paths");
+ do_check_eq(Unix.basename("a/b"), "b");
+ do_check_eq(Unix.basename("a/b/"), "");
+ do_check_eq(Unix.basename("abc"), "abc");
+ do_check_eq(Unix.dirname("a/b"), "a");
+ do_check_eq(Unix.dirname("a/b/"), "a/b");
+ do_check_eq(Unix.dirname("a////b"), "a");
+ do_check_eq(Unix.dirname("abc"), ".");
+ do_check_eq(Unix.normalize("/a/b/c"), "/a/b/c");
+ do_check_eq(Unix.normalize("/a/b////c"), "/a/b/c");
+ do_check_eq(Unix.normalize("////a/b/c"), "/a/b/c");
+ do_check_eq(Unix.normalize("/a/b/c///"), "/a/b/c");
+ do_check_eq(Unix.normalize("/a/b/c/../../../d/e/f"), "/d/e/f");
+ do_check_eq(Unix.normalize("a/b/c/../../../d/e/f"), "d/e/f");
+ do_check_fail(() => Unix.normalize("/a/b/c/../../../../d/e/f"));
+
+ do_check_eq(Unix.join("/tmp", "foo", "bar"), "/tmp/foo/bar", "join /tmp,foo,bar");
+ do_check_eq(Unix.join("/tmp", "/foo", "bar"), "/foo/bar", "join /tmp,/foo,bar");
+
+ do_print("Testing the presence of ospath.jsm");
+ let Scope = {};
+ try {
+ Components.utils.import("resource://gre/modules/osfile/ospath.jsm", Scope);
+ } catch (ex) {
+ // Can't load ospath
+ }
+ do_check_true(!!Scope.basename);
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
new file mode 100644
index 0000000000..c0057c7509
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+Cu.import("resource://gre/modules/ctypes.jsm", this);
+Cu.import("resource://testing-common/AppData.jsm", this);
+
+
+function run_test() {
+ run_next_test();
+}
+
+function compare_paths(ospath, key) {
+ let file;
+ try {
+ file = Services.dirsvc.get(key, Components.interfaces.nsIFile);
+ } catch(ex) {}
+
+ if (file) {
+ do_check_true(!!ospath);
+ do_check_eq(ospath, file.path);
+ } else {
+ do_print("WARNING: " + key + " is not defined. Test may not be testing anything!");
+ do_check_false(!!ospath);
+ }
+}
+
+// Some path constants aren't set up until the profile is available. This
+// test verifies that behavior.
+add_task(function* test_before_after_profile() {
+ do_check_null(OS.Constants.Path.profileDir);
+ do_check_null(OS.Constants.Path.localProfileDir);
+ do_check_null(OS.Constants.Path.userApplicationDataDir);
+
+ do_get_profile();
+ do_check_true(!!OS.Constants.Path.profileDir);
+ do_check_true(!!OS.Constants.Path.localProfileDir);
+
+ // UAppData is still null because the xpcshell profile doesn't set it up.
+ // This test is mostly here to fail in case behavior of do_get_profile() ever
+ // changes. We want to know if our assumptions no longer hold!
+ do_check_null(OS.Constants.Path.userApplicationDataDir);
+
+ yield makeFakeAppDir();
+ do_check_true(!!OS.Constants.Path.userApplicationDataDir);
+
+ // FUTURE: verify AppData too (bug 964291).
+});
+
+// Test simple paths
+add_task(function* test_simple_paths() {
+ do_check_true(!!OS.Constants.Path.tmpDir);
+ compare_paths(OS.Constants.Path.tmpDir, "TmpD");
+
+});
+
+// Test presence of paths that only exist on Desktop platforms
+add_task(function* test_desktop_paths() {
+ if (OS.Constants.Sys.Name == "Android" || OS.Constants.Sys.Name == "Gonk") {
+ return;
+ }
+ do_check_true(!!OS.Constants.Path.desktopDir);
+ do_check_true(!!OS.Constants.Path.homeDir);
+
+ compare_paths(OS.Constants.Path.homeDir, "Home");
+ compare_paths(OS.Constants.Path.desktopDir, "Desk");
+ compare_paths(OS.Constants.Path.userApplicationDataDir, "UAppData");
+
+ compare_paths(OS.Constants.Path.winAppDataDir, "AppData");
+ compare_paths(OS.Constants.Path.winStartMenuProgsDir, "Progs");
+
+ compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir");
+ compare_paths(OS.Constants.Path.macLocalApplicationsDir, "LocApp");
+ compare_paths(OS.Constants.Path.macTrashDir, "Trsh");
+});
+
+// Open libxul
+add_task(function* test_libxul() {
+ ctypes.open(OS.Constants.Path.libxul);
+ do_print("Linked to libxul");
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_queue.js b/toolkit/components/osfile/tests/xpcshell/test_queue.js
new file mode 100644
index 0000000000..c9d23eabc4
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_queue.js
@@ -0,0 +1,38 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+// Check if Scheduler.queue returned by OS.File.queue is resolved initially.
+add_task(function* check_init() {
+ yield OS.File.queue;
+ do_print("Function resolved");
+});
+
+// Check if Scheduler.queue returned by OS.File.queue is resolved
+// after an operation is successful.
+add_task(function* check_success() {
+ do_print("Attempting to open a file correctly");
+ let openedFile = yield OS.File.open(OS.Path.join(do_get_cwd().path, "test_queue.js"));
+ do_print("File opened correctly");
+ yield OS.File.queue;
+ do_print("Function resolved");
+});
+
+// Check if Scheduler.queue returned by OS.File.queue is resolved
+// after an operation fails.
+add_task(function* check_failure() {
+ let exception;
+ try {
+ do_print("Attempting to open a non existing file");
+ yield OS.File.open(OS.Path.join(".", "Bigfoot"));
+ } catch (err) {
+ exception = err;
+ yield OS.File.queue;
+ }
+ do_check_true(exception!=null);
+ do_print("Function resolved");
+}); \ No newline at end of file
diff --git a/toolkit/components/osfile/tests/xpcshell/test_read_write.js b/toolkit/components/osfile/tests/xpcshell/test_read_write.js
new file mode 100644
index 0000000000..00235ed8c5
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_read_write.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu} = Components;
+
+var SHARED_PATH;
+
+var EXISTING_FILE = do_get_file("xpcshell.ini").path;
+
+add_task(function* init() {
+ do_get_profile();
+ SHARED_PATH = OS.Path.join(OS.Constants.Path.profileDir, "test_osfile_read.tmp");
+});
+
+
+// Check that OS.File.read() is executed after the previous operation
+add_test_pair(function* ordering() {
+ let string1 = "Initial state " + Math.random();
+ let string2 = "After writing " + Math.random();
+ yield OS.File.writeAtomic(SHARED_PATH, string1);
+ OS.File.writeAtomic(SHARED_PATH, string2);
+ let string3 = yield OS.File.read(SHARED_PATH, { encoding: "utf-8" });
+ do_check_eq(string3, string2);
+});
+
+add_test_pair(function* read_write_all() {
+ let DEST_PATH = SHARED_PATH + Math.random();
+ let TMP_PATH = DEST_PATH + ".tmp";
+
+ let test_with_options = function(options, suffix) {
+ return Task.spawn(function*() {
+ do_print("Running test read_write_all with options " + JSON.stringify(options));
+ let TEST = "read_write_all " + suffix;
+
+ let optionsBackup = JSON.parse(JSON.stringify(options));
+
+ // Check that read + writeAtomic performs a correct copy
+ let currentDir = yield OS.File.getCurrentDirectory();
+ let pathSource = OS.Path.join(currentDir, EXISTING_FILE);
+ let contents = yield OS.File.read(pathSource);
+ do_check_true(!!contents); // Content is not empty
+ let bytesRead = contents.byteLength;
+
+ let bytesWritten = yield OS.File.writeAtomic(DEST_PATH, contents, options);
+ do_check_eq(bytesRead, bytesWritten); // Correct number of bytes written
+
+ // Check that options are not altered
+ do_check_eq(JSON.stringify(options), JSON.stringify(optionsBackup));
+ yield reference_compare_files(pathSource, DEST_PATH, TEST);
+
+ // Check that temporary file was removed or never created exist
+ do_check_false(new FileUtils.File(TMP_PATH).exists());
+
+ // Check that writeAtomic fails if noOverwrite is true and the destination
+ // file already exists!
+ contents = new Uint8Array(300);
+ let view = new Uint8Array(contents.buffer, 10, 200);
+ try {
+ let opt = JSON.parse(JSON.stringify(options));
+ opt.noOverwrite = true;
+ yield OS.File.writeAtomic(DEST_PATH, view, opt);
+ do_throw("With noOverwrite, writeAtomic should have refused to overwrite file (" + suffix + ")");
+ } catch (err if err instanceof OS.File.Error && err.becauseExists) {
+ do_print("With noOverwrite, writeAtomic correctly failed (" + suffix + ")");
+ }
+ yield reference_compare_files(pathSource, DEST_PATH, TEST);
+
+ // Check that temporary file was removed or never created
+ do_check_false(new FileUtils.File(TMP_PATH).exists());
+
+ // Now write a subset
+ let START = 10;
+ let LENGTH = 100;
+ contents = new Uint8Array(300);
+ for (var i = 0; i < contents.byteLength; i++)
+ contents[i] = i % 256;
+ view = new Uint8Array(contents.buffer, START, LENGTH);
+ bytesWritten = yield OS.File.writeAtomic(DEST_PATH, view, options);
+ do_check_eq(bytesWritten, LENGTH);
+
+ let array2 = yield OS.File.read(DEST_PATH);
+ do_check_eq(LENGTH, array2.length);
+ for (var i = 0; i < LENGTH; i++)
+ do_check_eq(array2[i], (i + START) % 256);
+
+ // Cleanup.
+ yield OS.File.remove(DEST_PATH);
+ yield OS.File.remove(TMP_PATH);
+ });
+ };
+
+ yield test_with_options({tmpPath: TMP_PATH}, "Renaming, not flushing");
+ yield test_with_options({tmpPath: TMP_PATH, flush: true}, "Renaming, flushing");
+ yield test_with_options({}, "Not renaming, not flushing");
+ yield test_with_options({flush: true}, "Not renaming, flushing");
+});
+
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_remove.js b/toolkit/components/osfile/tests/xpcshell/test_remove.js
new file mode 100644
index 0000000000..c8dc330545
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_remove.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+ run_next_test();
+}
+
+add_task(function* test_ignoreAbsent() {
+ let absent_file_name = "test_osfile_front_absent.tmp";
+
+ // Removing absent files should throw if "ignoreAbsent" is true.
+ yield Assert.rejects(OS.File.remove(absent_file_name, {ignoreAbsent: false}),
+ "OS.File.remove throws if there is no such file.");
+
+ // Removing absent files should not throw if "ignoreAbsent" is true or not
+ // defined.
+ let exception = null;
+ try {
+ yield OS.File.remove(absent_file_name, {ignoreAbsent: true});
+ yield OS.File.remove(absent_file_name);
+ } catch (ex) {
+ exception = ex;
+ }
+ Assert.ok(!exception, "OS.File.remove should not throw when not requested.");
+});
+
+add_task(function* test_ignoreAbsent_directory_missing() {
+ let absent_file_name = OS.Path.join("absent_parent", "test.tmp");
+
+ // Removing absent files should throw if "ignoreAbsent" is true.
+ yield Assert.rejects(OS.File.remove(absent_file_name, {ignoreAbsent: false}),
+ "OS.File.remove throws if there is no such file.");
+
+ // Removing files from absent directories should not throw if "ignoreAbsent"
+ // is true or not defined.
+ let exception = null;
+ try {
+ yield OS.File.remove(absent_file_name, {ignoreAbsent: true});
+ yield OS.File.remove(absent_file_name);
+ } catch (ex) {
+ exception = ex;
+ }
+ Assert.ok(!exception, "OS.File.remove should not throw when not requested.");
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js
new file mode 100644
index 0000000000..41ad0eb8ce
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js
@@ -0,0 +1,177 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+ run_next_test();
+}
+
+add_task(function() {
+ // Set up profile. We create the directory in the profile, because the profile
+ // is removed after every test run.
+ do_get_profile();
+
+ let file = OS.Path.join(OS.Constants.Path.profileDir, "file");
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
+ let file1 = OS.Path.join(dir, "file1");
+ let file2 = OS.Path.join(dir, "file2");
+ let subDir = OS.Path.join(dir, "subdir");
+ let fileInSubDir = OS.Path.join(subDir, "file");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove non-existent directory
+ let exception = null;
+ try {
+ yield OS.File.removeDir(dir, {ignoreAbsent: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+
+ // Remove non-existent directory with ignoreAbsent
+ yield OS.File.removeDir(dir, {ignoreAbsent: true});
+ yield OS.File.removeDir(dir);
+
+ // Remove file with ignoreAbsent: false
+ yield OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" });
+ exception = null;
+ try {
+ yield OS.File.removeDir(file, {ignoreAbsent: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+
+ // Remove empty directory
+ yield OS.File.makeDir(dir);
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove directory that contains one file
+ yield OS.File.makeDir(dir);
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove directory that contains multiple files
+ yield OS.File.makeDir(dir);
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ yield OS.File.writeAtomic(file2, "content", { tmpPath: file2 + ".tmp" });
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove directory that contains a file and a directory
+ yield OS.File.makeDir(dir);
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ yield OS.File.makeDir(subDir);
+ yield OS.File.writeAtomic(fileInSubDir, "content", { tmpPath: fileInSubDir + ".tmp" });
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+});
+
+add_task(function* test_unix_symlink() {
+ // Windows does not implement OS.File.unixSymLink()
+ if (OS.Constants.Win) {
+ return;
+ }
+
+ // Android / B2G file systems typically don't support symlinks.
+ if (OS.Constants.Sys.Name == "Android") {
+ return;
+ }
+
+ let file = OS.Path.join(OS.Constants.Path.profileDir, "file");
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
+ let file1 = OS.Path.join(dir, "file1");
+
+ // This test will create the following directory structure:
+ // <profileDir>/file (regular file)
+ // <profileDir>/file.link => file (symlink)
+ // <profileDir>/directory (directory)
+ // <profileDir>/linkdir => directory (directory)
+ // <profileDir>/directory/file1 (regular file)
+ // <profileDir>/directory3 (directory)
+ // <profileDir>/directory3/file3 (directory)
+ // <profileDir>/directory/link2 => ../directory3 (regular file)
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ yield OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" });
+ do_check_true((yield OS.File.exists(file)));
+ let info = yield OS.File.stat(file, {unixNoFollowingLinks: true});
+ do_check_false(info.isDir);
+ do_check_false(info.isSymLink);
+
+ yield OS.File.unixSymLink(file, file + ".link");
+ do_check_true((yield OS.File.exists(file + ".link")));
+ info = yield OS.File.stat(file + ".link", {unixNoFollowingLinks: true});
+ do_check_false(info.isDir);
+ do_check_true(info.isSymLink);
+ info = yield OS.File.stat(file + ".link");
+ do_check_false(info.isDir);
+ do_check_false(info.isSymLink);
+ yield OS.File.remove(file + ".link");
+ do_check_false((yield OS.File.exists(file + ".link")));
+
+ yield OS.File.makeDir(dir);
+ do_check_true((yield OS.File.exists(dir)));
+ info = yield OS.File.stat(dir, {unixNoFollowingLinks: true});
+ do_check_true(info.isDir);
+ do_check_false(info.isSymLink);
+
+ let link = OS.Path.join(OS.Constants.Path.profileDir, "linkdir");
+
+ yield OS.File.unixSymLink(dir, link);
+ do_check_true((yield OS.File.exists(link)));
+ info = yield OS.File.stat(link, {unixNoFollowingLinks: true});
+ do_check_false(info.isDir);
+ do_check_true(info.isSymLink);
+ info = yield OS.File.stat(link);
+ do_check_true(info.isDir);
+ do_check_false(info.isSymLink);
+
+ let dir3 = OS.Path.join(OS.Constants.Path.profileDir, "directory3");
+ let file3 = OS.Path.join(dir3, "file3");
+ let link2 = OS.Path.join(dir, "link2");
+
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ do_check_true((yield OS.File.exists(file1)));
+ yield OS.File.makeDir(dir3);
+ do_check_true((yield OS.File.exists(dir3)));
+ yield OS.File.writeAtomic(file3, "content", { tmpPath: file3 + ".tmp" });
+ do_check_true((yield OS.File.exists(file3)));
+ yield OS.File.unixSymLink("../directory3", link2);
+ do_check_true((yield OS.File.exists(link2)));
+
+ yield OS.File.removeDir(link);
+ do_check_false((yield OS.File.exists(link)));
+ do_check_true((yield OS.File.exists(file1)));
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+ do_check_true((yield OS.File.exists(file3)));
+ yield OS.File.removeDir(dir3);
+ do_check_false((yield OS.File.exists(dir3)));
+
+ // This task will be executed only on Unix-like systems.
+ // Please do not add tests independent to operating systems here
+ // or implement symlink() on Windows.
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js
new file mode 100644
index 0000000000..95f8d5cd1d
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+ run_next_test();
+}
+
+/**
+ * Test OS.File.removeEmptyDir
+ */
+add_task(function() {
+ // Set up profile. We create the directory in the profile, because the profile
+ // is removed after every test run.
+ do_get_profile();
+
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove non-existent directory
+ yield OS.File.removeEmptyDir(dir);
+
+ // Remove non-existent directory with ignoreAbsent
+ yield OS.File.removeEmptyDir(dir, {ignoreAbsent: true});
+
+ // Remove non-existent directory with ignoreAbsent false
+ let exception = null;
+ try {
+ yield OS.File.removeEmptyDir(dir, {ignoreAbsent: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseNoSuchFile);
+
+ // Remove empty directory
+ yield OS.File.makeDir(dir);
+ yield OS.File.removeEmptyDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_reset.js b/toolkit/components/osfile/tests/xpcshell/test_reset.js
new file mode 100644
index 0000000000..f1e1b14d14
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_reset.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var Path = OS.Constants.Path;
+
+add_task(function* init() {
+ do_get_profile();
+});
+
+add_task(function* reset_before_launching() {
+ do_print("Reset without launching OS.File, it shouldn't break");
+ yield OS.File.resetWorker();
+});
+
+add_task(function* transparent_reset() {
+ for (let i = 1; i < 3; ++i) {
+ do_print("Do stome stuff before and after " + i + " reset(s), " +
+ "it shouldn't break");
+ let CONTENT = "some content " + i;
+ let path = OS.Path.join(Path.profileDir, "tmp");
+ yield OS.File.writeAtomic(path, CONTENT);
+ for (let j = 0; j < i; ++j) {
+ yield OS.File.resetWorker();
+ }
+ let data = yield OS.File.read(path);
+ let string = (new TextDecoder()).decode(data);
+ do_check_eq(string, CONTENT);
+ }
+});
+
+add_task(function* file_open_cannot_reset() {
+ let TEST_FILE = OS.Path.join(Path.profileDir, "tmp-" + Math.random());
+ do_print("Leaking file descriptor " + TEST_FILE + ", we shouldn't be able to reset");
+ let openedFile = yield OS.File.open(TEST_FILE, { create: true} );
+ let thrown = false;
+ try {
+ yield OS.File.resetWorker();
+ } catch (ex if ex.message.indexOf(OS.Path.basename(TEST_FILE)) != -1 ) {
+ thrown = true;
+ }
+ do_check_true(thrown);
+
+ do_print("Closing the file, we should now be able to reset");
+ yield openedFile.close();
+ yield OS.File.resetWorker();
+});
+
+add_task(function* dir_open_cannot_reset() {
+ let TEST_DIR = yield OS.File.getCurrentDirectory();
+ do_print("Leaking directory " + TEST_DIR + ", we shouldn't be able to reset");
+ let iterator = new OS.File.DirectoryIterator(TEST_DIR);
+ let thrown = false;
+ try {
+ yield OS.File.resetWorker();
+ } catch (ex if ex.message.indexOf(OS.Path.basename(TEST_DIR)) != -1 ) {
+ thrown = true;
+ }
+ do_check_true(thrown);
+
+ do_print("Closing the directory, we should now be able to reset");
+ yield iterator.close();
+ yield OS.File.resetWorker();
+});
+
+add_task(function* race_against_itself() {
+ do_print("Attempt to get resetWorker() to race against itself");
+ // Arbitrary operation, just to wake up the worker
+ try {
+ yield OS.File.read("/foo");
+ } catch (ex) {
+ }
+
+ let all = [];
+ for (let i = 0; i < 100; ++i) {
+ all.push(OS.File.resetWorker());
+ }
+
+ yield Promise.all(all);
+});
+
+
+add_task(function* finish_with_a_reset() {
+ do_print("Reset without waiting for the result");
+ // Arbitrary operation, just to wake up the worker
+ try {
+ yield OS.File.read("/foo");
+ } catch (ex) {
+ }
+ // Now reset
+ /*don't yield*/ OS.File.resetWorker();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_shutdown.js b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js
new file mode 100644
index 0000000000..667965d9ee
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js
@@ -0,0 +1,98 @@
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+Components.utils.import("resource://gre/modules/Promise.jsm", this);
+Components.utils.import("resource://gre/modules/Task.jsm", this);
+Components.utils.import("resource://gre/modules/osfile.jsm", this);
+
+add_task(function init() {
+ do_get_profile();
+});
+
+/**
+ * Test logging of file descriptors leaks.
+ */
+add_task(function system_shutdown() {
+
+ // Test that unclosed files cause warnings
+ // Test that unclosed directories cause warnings
+ // Test that closed files do not cause warnings
+ // Test that closed directories do not cause warnings
+ function testLeaksOf(resource, topic) {
+ return Task.spawn(function() {
+ let deferred = Promise.defer();
+
+ // Register observer
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", true);
+ Services.prefs.setCharPref("toolkit.osfile.test.shutdown.observer", topic);
+
+ let observer = function(aMessage) {
+ try {
+ do_print("Got message: " + aMessage);
+ if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
+ return;
+ }
+ let message = aMessage.message;
+ do_print("Got message: " + message);
+ if (message.indexOf("TEST OS Controller WARNING") < 0) {
+ return;
+ }
+ do_print("Got message: " + message + ", looking for resource " + resource);
+ if (message.indexOf(resource) < 0) {
+ return;
+ }
+ do_print("Resource: " + resource + " found");
+ do_execute_soon(deferred.resolve);
+ } catch (ex) {
+ do_execute_soon(function() {
+ deferred.reject(ex);
+ });
+ }
+ };
+ Services.console.registerListener(observer);
+ Services.obs.notifyObservers(null, topic, null);
+ do_timeout(1000, function() {
+ do_print("Timeout while waiting for resource: " + resource);
+ deferred.reject("timeout");
+ });
+
+ let resolved = false;
+ try {
+ yield deferred.promise;
+ resolved = true;
+ } catch (ex if ex == "timeout") {
+ resolved = false;
+ }
+ Services.console.unregisterListener(observer);
+ Services.prefs.clearUserPref("toolkit.osfile.log");
+ Services.prefs.clearUserPref("toolkit.osfile.log.redirect");
+ Services.prefs.clearUserPref("toolkit.osfile.test.shutdown.observer");
+ Services.prefs.clearUserPref("toolkit.async_shutdown.testing", true);
+
+ throw new Task.Result(resolved);
+ });
+ }
+
+ let TEST_DIR = OS.Path.join((yield OS.File.getCurrentDirectory()), "..");
+ do_print("Testing for leaks of directory iterator " + TEST_DIR);
+ let iterator = new OS.File.DirectoryIterator(TEST_DIR);
+ do_print("At this stage, we leak the directory");
+ do_check_true((yield testLeaksOf(TEST_DIR, "test.shutdown.dir.leak")));
+ yield iterator.close();
+ do_print("At this stage, we don't leak the directory anymore");
+ do_check_false((yield testLeaksOf(TEST_DIR, "test.shutdown.dir.noleak")));
+
+ let TEST_FILE = OS.Path.join(OS.Constants.Path.profileDir, "test");
+ do_print("Testing for leaks of file descriptor: " + TEST_FILE);
+ let openedFile = yield OS.File.open(TEST_FILE, { create: true} );
+ do_print("At this stage, we leak the file");
+ do_check_true((yield testLeaksOf(TEST_FILE, "test.shutdown.file.leak")));
+ yield openedFile.close();
+ do_print("At this stage, we don't leak the file anymore");
+ do_check_false((yield testLeaksOf(TEST_FILE, "test.shutdown.file.leak.2")));
+});
+
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_telemetry.js b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js
new file mode 100644
index 0000000000..dc5104443d
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js
@@ -0,0 +1,63 @@
+"use strict";
+
+var {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+var {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
+
+// Ensure that we have a profile but that the OS.File worker is not launched
+add_task(function* init() {
+ do_get_profile();
+ yield File.resetWorker();
+});
+
+function getCount(histogram) {
+ if (histogram == null) {
+ return 0;
+ }
+
+ let total = 0;
+ for (let i of histogram.counts) {
+ total += i;
+ }
+ return total;
+}
+
+// Ensure that launching the OS.File worker adds data to the relevant
+// histograms
+add_task(function* test_startup() {
+ let LAUNCH = "OSFILE_WORKER_LAUNCH_MS";
+ let READY = "OSFILE_WORKER_READY_MS";
+
+ let before = Services.telemetry.histogramSnapshots;
+
+ // Launch the OS.File worker
+ yield File.getCurrentDirectory();
+
+ let after = Services.telemetry.histogramSnapshots;
+
+
+ do_print("Ensuring that we have recorded measures for histograms");
+ do_check_eq(getCount(after[LAUNCH]), getCount(before[LAUNCH]) + 1);
+ do_check_eq(getCount(after[READY]), getCount(before[READY]) + 1);
+
+ do_print("Ensuring that launh <= ready");
+ do_check_true(after[LAUNCH].sum <= after[READY].sum);
+});
+
+// Ensure that calling writeAtomic adds data to the relevant histograms
+add_task(function* test_writeAtomic() {
+ let LABEL = "OSFILE_WRITEATOMIC_JANK_MS";
+
+ let before = Services.telemetry.histogramSnapshots;
+
+ // Perform a write.
+ let path = Path.join(Constants.Path.profileDir, "test_osfile_telemetry.tmp");
+ yield File.writeAtomic(path, LABEL, { tmpPath: path + ".tmp" } );
+
+ let after = Services.telemetry.histogramSnapshots;
+
+ do_check_eq(getCount(after[LABEL]), getCount(before[LABEL]) + 1);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_unique.js b/toolkit/components/osfile/tests/xpcshell/test_unique.js
new file mode 100644
index 0000000000..8aa81b8034
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_unique.js
@@ -0,0 +1,88 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+function testFiles(filename) {
+ return Task.spawn(function() {
+ const MAX_TRIES = 10;
+ let profileDir = OS.Constants.Path.profileDir;
+ let path = OS.Path.join(profileDir, filename);
+
+ // Ensure that openUnique() uses the file name if there is no file with that name already.
+ let openedFile = yield OS.File.openUnique(path);
+ do_print("\nCreate new file: " + openedFile.path);
+ yield openedFile.file.close();
+ let exists = yield OS.File.exists(openedFile.path);
+ do_check_true(exists);
+ do_check_eq(path, openedFile.path);
+ let fileInfo = yield OS.File.stat(openedFile.path);
+ do_check_true(fileInfo.size == 0);
+
+ // Ensure that openUnique() creates a new file name using a HEX number, as the original name is already taken.
+ openedFile = yield OS.File.openUnique(path);
+ do_print("\nCreate unique HEX file: " + openedFile.path);
+ yield openedFile.file.close();
+ exists = yield OS.File.exists(openedFile.path);
+ do_check_true(exists);
+ fileInfo = yield OS.File.stat(openedFile.path);
+ do_check_true(fileInfo.size == 0);
+
+ // Ensure that openUnique() generates different file names each time, using the HEX number algorithm
+ let filenames = new Set();
+ for (let i=0; i < MAX_TRIES; i++) {
+ openedFile = yield OS.File.openUnique(path);
+ yield openedFile.file.close();
+ filenames.add(openedFile.path);
+ }
+
+ do_check_eq(filenames.size, MAX_TRIES);
+
+ // Ensure that openUnique() creates a new human readable file name using, as the original name is already taken.
+ openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+ do_print("\nCreate unique Human Readable file: " + openedFile.path);
+ yield openedFile.file.close();
+ exists = yield OS.File.exists(openedFile.path);
+ do_check_true(exists);
+ fileInfo = yield OS.File.stat(openedFile.path);
+ do_check_true(fileInfo.size == 0);
+
+ // Ensure that openUnique() generates different human readable file names each time
+ filenames = new Set();
+ for (let i=0; i < MAX_TRIES; i++) {
+ openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+ yield openedFile.file.close();
+ filenames.add(openedFile.path);
+ }
+
+ do_check_eq(filenames.size, MAX_TRIES);
+
+ let exn;
+ try {
+ for (let i=0; i < 100; i++) {
+ openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+ yield openedFile.file.close();
+ }
+ } catch (ex) {
+ exn = ex;
+ }
+
+ do_print("Ensure that this raises the correct error");
+ do_check_true(!!exn);
+ do_check_true(exn instanceof OS.File.Error);
+ do_check_true(exn.becauseExists);
+ });
+}
+
+add_task(function test_unique() {
+ OS.Shared.DEBUG = true;
+ // Tests files with extension
+ yield testFiles("dummy_unique_file.txt");
+ // Tests files with no extension
+ yield testFiles("dummy_unique_file_no_ext");
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..58b106d3df
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+head = head.js
+tail =
+
+support-files =
+ test_loader/module_test_loader.js
+
+[test_available_free_space.js]
+[test_compression.js]
+[test_constants.js]
+[test_creationDate.js]
+[test_duration.js]
+[test_exception.js]
+[test_file_URL_conversion.js]
+[test_loader.js]
+[test_logging.js]
+[test_makeDir.js]
+[test_open.js]
+[test_osfile_async.js]
+[test_osfile_async_append.js]
+[test_osfile_async_bytes.js]
+[test_osfile_async_copy.js]
+[test_osfile_async_flush.js]
+[test_osfile_async_largefiles.js]
+[test_osfile_async_setDates.js]
+# Unimplemented on Windows (bug 1022816).
+# Spurious failure on Android test farm due to non-POSIX behavior of
+# filesystem backing /mnt/sdcard (not worth trying to fix).
+[test_osfile_async_setPermissions.js]
+skip-if = os == "win" || os == "android"
+[test_osfile_closed.js]
+[test_osfile_error.js]
+[test_osfile_kill.js]
+# Windows test
+[test_osfile_win_async_setPermissions.js]
+skip-if = os != "win"
+[test_osfile_writeAtomic_backupTo_option.js]
+[test_osfile_writeAtomic_zerobytes.js]
+[test_path.js]
+[test_path_constants.js]
+[test_queue.js]
+[test_read_write.js]
+requesttimeoutfactor = 4
+[test_remove.js]
+[test_removeDir.js]
+requesttimeoutfactor = 4
+[test_removeEmptyDir.js]
+[test_reset.js]
+[test_shutdown.js]
+[test_telemetry.js]
+[test_unique.js]
diff --git a/toolkit/components/parentalcontrols/moz.build b/toolkit/components/parentalcontrols/moz.build
new file mode 100644
index 0000000000..083312fefb
--- /dev/null
+++ b/toolkit/components/parentalcontrols/moz.build
@@ -0,0 +1,31 @@
+# -*- 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 += [
+ 'nsIParentalControlsService.idl',
+]
+
+XPIDL_MODULE = 'parentalcontrols'
+
+if not CONFIG['MOZ_DISABLE_PARENTAL_CONTROLS']:
+ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ SOURCES += [
+ 'nsParentalControlsServiceWin.cpp',
+ ]
+ elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+ UNIFIED_SOURCES += [
+ 'nsParentalControlsServiceCocoa.mm',
+ ]
+ elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
+ UNIFIED_SOURCES += [
+ 'nsParentalControlsServiceAndroid.cpp',
+ ]
+ else:
+ SOURCES += [
+ 'nsParentalControlsServiceDefault.cpp',
+ ]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/parentalcontrols/nsIParentalControlsService.idl b/toolkit/components/parentalcontrols/nsIParentalControlsService.idl
new file mode 100644
index 0000000000..45d349addc
--- /dev/null
+++ b/toolkit/components/parentalcontrols/nsIParentalControlsService.idl
@@ -0,0 +1,104 @@
+/* -*- 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 nsIURI;
+interface nsIFile;
+interface nsIInterfaceRequestor;
+interface nsIArray;
+
+[scriptable, uuid(2e97e5dd-467b-4aea-a1bb-6773c0f2beb0)]
+interface nsIParentalControlsService : nsISupports
+{
+ /**
+ * Action types that can be blocked for users.
+ */
+ const short DOWNLOAD = 1; // Downloading files
+ const short INSTALL_EXTENSION = 2; // Installing extensions
+ const short INSTALL_APP = 3; // Installing webapps
+ const short BROWSE = 4; // Opening specific urls
+ const short SHARE = 5; // Sharing
+ const short BOOKMARK = 6; // Creating bookmarks
+ const short ADD_CONTACT = 7; // Add contacts to the system database
+ const short SET_IMAGE = 8; // Setting images as wall paper
+ const short MODIFY_ACCOUNTS = 9; // Modifying system accounts
+ const short REMOTE_DEBUGGING = 10; // Remote debugging
+ const short IMPORT_SETTINGS = 11; // Importing settings from other apps
+ const short PRIVATE_BROWSING = 12; // Disallow usage of private browsing
+ const short DATA_CHOICES = 13; // Choose whether or not to send usage information
+ const short CLEAR_HISTORY = 14; // Clear browsing history
+ const short MASTER_PASSWORD = 15; // Setting master password for logins
+ const short GUEST_BROWSING = 16; // Disallow usage of guest browsing
+ const short ADVANCED_SETTINGS = 17; // Advanced settings
+ const short CAMERA_MICROPHONE = 18; // Camera and microphone (WebRTC)
+ const short BLOCK_LIST = 19; // Block websites that include sensitive content
+ const short TELEMETRY = 20; // Submit telemetry data
+ const short HEALTH_REPORT = 21; // Submit FHR data
+ const short DEFAULT_THEME = 22; // Use default theme or a special parental controls theme
+
+ /**
+ * @returns true if the current user account has parental controls
+ * restrictions enabled.
+ */
+ readonly attribute boolean parentalControlsEnabled;
+
+ /**
+ * @returns true if the current user account parental controls
+ * restrictions include the blocking of all file downloads.
+ */
+ readonly attribute boolean blockFileDownloadsEnabled;
+
+ /**
+ * Check if the user can do the prescibed action for this uri.
+ *
+ * @param aAction Action being performed
+ * @param aUri The uri requesting this action
+ * @param aWindow The window generating this event.
+ */
+ boolean isAllowed(in short aAction, [optional] in nsIURI aUri);
+
+ /**
+ * Request that blocked URI(s) be allowed through parental
+ * control filters. Returns true if the URI was successfully
+ * overriden. Note, may block while native UI is shown.
+ *
+ * @param aTarget(s) URI to be overridden. In the case of
+ * multiple URI, the first URI in the array
+ * should be the root URI of the site.
+ * @param window Window that generates the event.
+ */
+ boolean requestURIOverride(in nsIURI aTarget, [optional] in nsIInterfaceRequestor aWindowContext);
+ boolean requestURIOverrides(in nsIArray aTargets, [optional] in nsIInterfaceRequestor aWindowContext);
+
+ /**
+ * @returns true if the current user account has parental controls
+ * logging enabled. If true, applications should log relevent events
+ * using 'log'.
+ */
+ readonly attribute boolean loggingEnabled;
+
+ /**
+ * Log entry types. Additional types can be defined and implemented
+ * as needed. Other possible event types might include email events,
+ * media related events, and IM events.
+ */
+ const short ePCLog_URIVisit = 1; /* Web content */
+ const short ePCLog_FileDownload = 2; /* File downloads */
+
+ /**
+ * Log an application specific parental controls
+ * event.
+ *
+ * @param aEntryType Constant defining the type of event.
+ * @param aFlag A flag indicating if the subject content
+ * was blocked.
+ * @param aSource The URI source of the subject content.
+ * @param aTarget The location the content was saved to if
+ * no blocking occurred.
+ */
+ void log(in short aEntryType, in boolean aFlag, in nsIURI aSource, [optional] in nsIFile aTarget);
+};
diff --git a/toolkit/components/parentalcontrols/nsParentalControlsService.h b/toolkit/components/parentalcontrols/nsParentalControlsService.h
new file mode 100644
index 0000000000..a0dc9c2db0
--- /dev/null
+++ b/toolkit/components/parentalcontrols/nsParentalControlsService.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 nsParentalControlsService_h__
+#define nsParentalControlsService_h__
+
+#include "nsIParentalControlsService.h"
+#include "nsCOMPtr.h"
+#include "nsAutoPtr.h"
+#include "nsIURI.h"
+
+#if defined(XP_WIN)
+// wpcevents.h requires this be elevated
+#if (WINVER < 0x0600)
+# undef WINVER
+# define WINVER 0x0600
+#endif
+#include <wpcapi.h>
+#include <wpcevent.h>
+#endif
+
+class nsParentalControlsService : public nsIParentalControlsService
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPARENTALCONTROLSSERVICE
+
+ nsParentalControlsService();
+
+protected:
+ virtual ~nsParentalControlsService();
+
+private:
+ bool mEnabled;
+#if defined(XP_WIN)
+ REGHANDLE mProvider;
+ IWindowsParentalControls * mPC;
+ void LogFileDownload(bool blocked, nsIURI *aSource, nsIFile *aTarget);
+#endif
+};
+
+#endif /* nsParentalControlsService_h__ */
diff --git a/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp b/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp
new file mode 100644
index 0000000000..3647490002
--- /dev/null
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp
@@ -0,0 +1,103 @@
+/* -*- 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 "nsParentalControlsService.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "FennecJNIWrappers.h"
+
+namespace java = mozilla::java;
+
+NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService)
+
+nsParentalControlsService::nsParentalControlsService() :
+ mEnabled(false)
+{
+ if (mozilla::jni::IsFennec()) {
+ mEnabled = java::Restrictions::IsUserRestricted();
+ }
+}
+
+nsParentalControlsService::~nsParentalControlsService()
+{
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetParentalControlsEnabled(bool *aResult)
+{
+ *aResult = mEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult)
+{
+ // NOTE: isAllowed returns the opposite intention, so we need to flip it
+ bool res;
+ IsAllowed(nsIParentalControlsService::DOWNLOAD, NULL, &res);
+ *aResult = !res;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetLoggingEnabled(bool *aResult)
+{
+ // Android doesn't currently have any method of logging restricted actions.
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::Log(int16_t aEntryType,
+ bool aBlocked,
+ nsIURI *aSource,
+ nsIFile *aTarget)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverride(nsIURI *aTarget,
+ nsIInterfaceRequestor *aWindowContext,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
+ nsIInterfaceRequestor *aWindowContext,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+ nsIURI *aUri,
+ bool *_retval)
+{
+ nsresult rv = NS_OK;
+ *_retval = true;
+
+ if (!mEnabled) {
+ return rv;
+ }
+
+ if (mozilla::jni::IsFennec()) {
+ nsAutoCString url;
+ if (aUri) {
+ rv = aUri->GetSpec(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *_retval = java::Restrictions::IsAllowed(aAction,
+ NS_ConvertUTF8toUTF16(url));
+ return rv;
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
diff --git a/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm b/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm
new file mode 100644
index 0000000000..0eb0184001
--- /dev/null
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm
@@ -0,0 +1,79 @@
+/* -*- 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 "nsParentalControlsService.h"
+#include "nsString.h"
+#include "nsIFile.h"
+
+#import <Cocoa/Cocoa.h>
+
+NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService)
+
+nsParentalControlsService::nsParentalControlsService() :
+ mEnabled(false)
+{
+ mEnabled = CFPreferencesAppValueIsForced(CFSTR("restrictWeb"),
+ CFSTR("com.apple.familycontrols.contentfilter"));
+}
+
+nsParentalControlsService::~nsParentalControlsService()
+{
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetParentalControlsEnabled(bool *aResult)
+{
+ *aResult = mEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult)
+{
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetLoggingEnabled(bool *aResult)
+{
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::Log(int16_t aEntryType,
+ bool blocked,
+ nsIURI *aSource,
+ nsIFile *aTarget)
+{
+ // silently drop on the floor
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverride(nsIURI *aTarget,
+ nsIInterfaceRequestor *aWindowContext,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
+ nsIInterfaceRequestor *aWindowContext,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+ nsIURI *aUri,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
diff --git a/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp b/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp
new file mode 100644
index 0000000000..5d97b6f1b8
--- /dev/null
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp
@@ -0,0 +1,73 @@
+/* -*- 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 "nsParentalControlsService.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "mozilla/Unused.h"
+
+NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService)
+
+nsParentalControlsService::nsParentalControlsService() :
+ mEnabled(false)
+{
+ mozilla::Unused << mEnabled;
+}
+
+nsParentalControlsService::~nsParentalControlsService()
+{
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetParentalControlsEnabled(bool *aResult)
+{
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetLoggingEnabled(bool *aResult)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::Log(int16_t aEntryType,
+ bool blocked,
+ nsIURI *aSource,
+ nsIFile *aTarget)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverride(nsIURI *aTarget,
+ nsIInterfaceRequestor *aWindowContext,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
+ nsIInterfaceRequestor *aWindowContext,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+ nsIURI *aUri,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
diff --git a/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp b/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp
new file mode 100644
index 0000000000..e73bb097aa
--- /dev/null
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp
@@ -0,0 +1,347 @@
+/* -*- 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 "nsParentalControlsService.h"
+#include "nsString.h"
+#include "nsIArray.h"
+#include "nsIWidget.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIFile.h"
+#include "nsILocalFileWin.h"
+#include "nsArrayUtils.h"
+#include "nsIXULAppInfo.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsVersion.h"
+
+using namespace mozilla;
+
+static const CLSID CLSID_WinParentalControls = {0xE77CC89B,0x7401,0x4C04,{0x8C,0xED,0x14,0x9D,0xB3,0x5A,0xDD,0x04}};
+static const IID IID_IWinParentalControls = {0x28B4D88B,0xE072,0x49E6,{0x80,0x4D,0x26,0xED,0xBE,0x21,0xA7,0xB9}};
+
+NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService)
+
+static HINSTANCE gAdvAPIDLLInst = nullptr;
+
+decltype(EventWrite)* gEventWrite = nullptr;
+decltype(EventRegister)* gEventRegister = nullptr;
+decltype(EventUnregister)* gEventUnregister = nullptr;
+
+nsParentalControlsService::nsParentalControlsService() :
+ mEnabled(false)
+, mProvider(0)
+, mPC(nullptr)
+{
+ HRESULT hr;
+ CoInitialize(nullptr);
+ hr = CoCreateInstance(CLSID_WinParentalControls, nullptr, CLSCTX_INPROC,
+ IID_IWinParentalControls, (void**)&mPC);
+ if (FAILED(hr))
+ return;
+
+ RefPtr<IWPCSettings> wpcs;
+ if (FAILED(mPC->GetUserSettings(nullptr, getter_AddRefs(wpcs)))) {
+ // Not available on this os or not enabled for this user account or we're running as admin
+ mPC->Release();
+ mPC = nullptr;
+ return;
+ }
+
+ DWORD settings = 0;
+ wpcs->GetRestrictions(&settings);
+
+ // If we can't determine specifically whether Web Filtering is on/off (i.e.
+ // we're on Windows < 8), then assume it's on unless no restrictions are set.
+ bool enable = IsWin8OrLater() ? settings & WPCFLAG_WEB_FILTERED
+ : settings != WPCFLAG_NO_RESTRICTION;
+
+ if (enable) {
+ gAdvAPIDLLInst = ::LoadLibrary("Advapi32.dll");
+ if(gAdvAPIDLLInst)
+ {
+ gEventWrite = (decltype(EventWrite)*) GetProcAddress(gAdvAPIDLLInst, "EventWrite");
+ gEventRegister = (decltype(EventRegister)*) GetProcAddress(gAdvAPIDLLInst, "EventRegister");
+ gEventUnregister = (decltype(EventUnregister)*) GetProcAddress(gAdvAPIDLLInst, "EventUnregister");
+ }
+ mEnabled = true;
+ }
+}
+
+nsParentalControlsService::~nsParentalControlsService()
+{
+ if (mPC)
+ mPC->Release();
+
+ if (gEventUnregister && mProvider)
+ gEventUnregister(mProvider);
+
+ if (gAdvAPIDLLInst)
+ ::FreeLibrary(gAdvAPIDLLInst);
+}
+
+//------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsParentalControlsService::GetParentalControlsEnabled(bool *aResult)
+{
+ *aResult = false;
+
+ if (mEnabled)
+ *aResult = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult)
+{
+ *aResult = false;
+
+ if (!mEnabled)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ RefPtr<IWPCWebSettings> wpcws;
+ if (SUCCEEDED(mPC->GetWebSettings(nullptr, getter_AddRefs(wpcws)))) {
+ DWORD settings = 0;
+ wpcws->GetSettings(&settings);
+ if (settings == WPCFLAG_WEB_SETTING_DOWNLOADSBLOCKED)
+ *aResult = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::GetLoggingEnabled(bool *aResult)
+{
+ *aResult = false;
+
+ if (!mEnabled)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ // Check the general purpose logging flag
+ RefPtr<IWPCSettings> wpcs;
+ if (SUCCEEDED(mPC->GetUserSettings(nullptr, getter_AddRefs(wpcs)))) {
+ BOOL enabled = FALSE;
+ wpcs->IsLoggingRequired(&enabled);
+ if (enabled)
+ *aResult = true;
+ }
+
+ return NS_OK;
+}
+
+// Post a log event to the system
+NS_IMETHODIMP
+nsParentalControlsService::Log(int16_t aEntryType, bool blocked, nsIURI *aSource, nsIFile *aTarget)
+{
+ if (!mEnabled)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ENSURE_ARG_POINTER(aSource);
+
+ // Confirm we should be logging
+ bool enabled;
+ GetLoggingEnabled(&enabled);
+ if (!enabled)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ // Register a Vista log event provider associated with the parental controls channel.
+ if (!mProvider) {
+ if (!gEventRegister)
+ return NS_ERROR_NOT_AVAILABLE;
+ if (gEventRegister(&WPCPROV, nullptr, nullptr, &mProvider) != ERROR_SUCCESS)
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ switch(aEntryType) {
+ case ePCLog_URIVisit:
+ // Not needed, Vista's web content filter handles this for us
+ break;
+ case ePCLog_FileDownload:
+ LogFileDownload(blocked, aSource, aTarget);
+ break;
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+// Override a single URI
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverride(nsIURI *aTarget, nsIInterfaceRequestor *aWindowContext, bool *_retval)
+{
+ *_retval = false;
+
+ if (!mEnabled)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ENSURE_ARG_POINTER(aTarget);
+
+ nsAutoCString spec;
+ aTarget->GetSpec(spec);
+ if (spec.IsEmpty())
+ return NS_ERROR_INVALID_ARG;
+
+ HWND hWnd = nullptr;
+ // If we have a native window, use its handle instead
+ nsCOMPtr<nsIWidget> widget(do_GetInterface(aWindowContext));
+ if (widget)
+ hWnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (hWnd == nullptr)
+ hWnd = GetDesktopWindow();
+
+ BOOL ret;
+ RefPtr<IWPCWebSettings> wpcws;
+ if (SUCCEEDED(mPC->GetWebSettings(nullptr, getter_AddRefs(wpcws)))) {
+ wpcws->RequestURLOverride(hWnd, NS_ConvertUTF8toUTF16(spec).get(),
+ 0, nullptr, &ret);
+ *_retval = ret;
+ }
+
+
+ return NS_OK;
+}
+
+// Override a web page
+NS_IMETHODIMP
+nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets, nsIInterfaceRequestor *aWindowContext, bool *_retval)
+{
+ *_retval = false;
+
+ if (!mEnabled)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ENSURE_ARG_POINTER(aTargets);
+
+ uint32_t arrayLength = 0;
+ aTargets->GetLength(&arrayLength);
+ if (!arrayLength)
+ return NS_ERROR_INVALID_ARG;
+
+ if (arrayLength == 1) {
+ nsCOMPtr<nsIURI> uri = do_QueryElementAt(aTargets, 0);
+ if (!uri)
+ return NS_ERROR_INVALID_ARG;
+ return RequestURIOverride(uri, aWindowContext, _retval);
+ }
+
+ HWND hWnd = nullptr;
+ // If we have a native window, use its handle instead
+ nsCOMPtr<nsIWidget> widget(do_GetInterface(aWindowContext));
+ if (widget)
+ hWnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+ if (hWnd == nullptr)
+ hWnd = GetDesktopWindow();
+
+ // The first entry should be the root uri
+ nsAutoCString rootSpec;
+ nsCOMPtr<nsIURI> rootURI = do_QueryElementAt(aTargets, 0);
+ if (!rootURI)
+ return NS_ERROR_INVALID_ARG;
+
+ rootURI->GetSpec(rootSpec);
+ if (rootSpec.IsEmpty())
+ return NS_ERROR_INVALID_ARG;
+
+ // Allocate an array of sub uri
+ int32_t count = arrayLength - 1;
+ auto arrUrls = MakeUnique<LPCWSTR[]>(count);
+
+ uint32_t uriIdx = 0, idx;
+ for (idx = 1; idx < arrayLength; idx++)
+ {
+ nsCOMPtr<nsIURI> uri = do_QueryElementAt(aTargets, idx);
+ if (!uri)
+ continue;
+
+ nsAutoCString subURI;
+ if (NS_FAILED(uri->GetSpec(subURI)))
+ continue;
+
+ arrUrls[uriIdx] = (LPCWSTR)UTF8ToNewUnicode(subURI); // allocation
+ if (!arrUrls[uriIdx])
+ continue;
+
+ uriIdx++;
+ }
+
+ if (!uriIdx)
+ return NS_ERROR_INVALID_ARG;
+
+ BOOL ret;
+ RefPtr<IWPCWebSettings> wpcws;
+ if (SUCCEEDED(mPC->GetWebSettings(nullptr, getter_AddRefs(wpcws)))) {
+ wpcws->RequestURLOverride(hWnd, NS_ConvertUTF8toUTF16(rootSpec).get(),
+ uriIdx, (LPCWSTR*)arrUrls.get(), &ret);
+ *_retval = ret;
+ }
+
+ // Free up the allocated strings in our array
+ for (idx = 0; idx < uriIdx; idx++)
+ free((void*)arrUrls[idx]);
+
+ return NS_OK;
+}
+
+//------------------------------------------------------------------------
+
+// Sends a file download event to the Vista Event Log
+void
+nsParentalControlsService::LogFileDownload(bool blocked, nsIURI *aSource, nsIFile *aTarget)
+{
+ nsAutoCString curi;
+
+ if (!gEventWrite)
+ return;
+
+ // Note, EventDataDescCreate is a macro defined in the headers, not a function
+
+ aSource->GetSpec(curi);
+ nsAutoString uri = NS_ConvertUTF8toUTF16(curi);
+
+ // Get the name of the currently running process
+ nsCOMPtr<nsIXULAppInfo> appInfo = do_GetService("@mozilla.org/xre/app-info;1");
+ nsAutoCString asciiAppName;
+ if (appInfo)
+ appInfo->GetName(asciiAppName);
+ nsAutoString appName = NS_ConvertUTF8toUTF16(asciiAppName);
+
+ static const WCHAR fill[] = L"";
+
+ // See wpcevent.h and msdn for event formats
+ EVENT_DATA_DESCRIPTOR eventData[WPC_ARGS_FILEDOWNLOADEVENT_CARGS];
+ DWORD dwBlocked = blocked;
+
+ EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_URL], (const void*)uri.get(),
+ ((ULONG)uri.Length()+1)*sizeof(WCHAR));
+ EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_APPNAME], (const void*)appName.get(),
+ ((ULONG)appName.Length()+1)*sizeof(WCHAR));
+ EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_VERSION], (const void*)fill, sizeof(fill));
+ EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_BLOCKED], (const void*)&dwBlocked,
+ sizeof(dwBlocked));
+
+ nsCOMPtr<nsILocalFileWin> local(do_QueryInterface(aTarget)); // May be null
+ if (local) {
+ nsAutoString path;
+ local->GetCanonicalPath(path);
+ EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_PATH], (const void*)path.get(),
+ ((ULONG)path.Length()+1)*sizeof(WCHAR));
+ }
+ else {
+ EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_PATH], (const void*)fill, sizeof(fill));
+ }
+
+ gEventWrite(mProvider, &WPCEVENT_WEB_FILEDOWNLOAD, ARRAYSIZE(eventData), eventData);
+}
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+ nsIURI *aUri,
+ bool *_retval)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
diff --git a/toolkit/components/passwordmgr/.eslintrc.js b/toolkit/components/passwordmgr/.eslintrc.js
new file mode 100644
index 0000000000..188f7eeff9
--- /dev/null
+++ b/toolkit/components/passwordmgr/.eslintrc.js
@@ -0,0 +1,36 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": "../../.eslintrc.js",
+ "rules": {
+ // Require spacing around =>
+ "arrow-spacing": "error",
+
+ // No newline before open brace for a block
+ "brace-style": ["error", "1tbs", {"allowSingleLine": true}],
+
+ // No space before always a space after a comma
+ "comma-spacing": ["error", {"before": false, "after": true}],
+
+ // Commas at the end of the line not the start
+ "comma-style": "error",
+
+ // Use [] instead of Array()
+ "no-array-constructor": "error",
+
+ // Use {} instead of new Object()
+ "no-new-object": "error",
+
+ // No using undeclared variables
+ "no-undef": "error",
+
+ // Don't allow unused local variables unless they match the pattern
+ "no-unused-vars": ["error", {"args": "none", "vars": "local", "varsIgnorePattern": "^(ids|ignored|unused)$"}],
+
+ // Always require semicolon at end of statement
+ "semi": ["error", "always"],
+
+ // Require spaces around operators
+ "space-infix-ops": "error",
+ }
+};
diff --git a/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
new file mode 100644
index 0000000000..5351e45b23
--- /dev/null
+++ b/toolkit/components/passwordmgr/InsecurePasswordUtils.jsm
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = [ "InsecurePasswordUtils" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const STRINGS_URI = "chrome://global/locale/security/security.properties";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+ "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
+ "@mozilla.org/contentsecuritymanager;1",
+ "nsIContentSecurityManager");
+XPCOMUtils.defineLazyServiceGetter(this, "gScriptSecurityManager",
+ "@mozilla.org/scriptsecuritymanager;1",
+ "nsIScriptSecurityManager");
+XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", () => {
+ return this.devtools.require("devtools/server/actors/utils/webconsole-utils").Utils;
+});
+
+/*
+ * A module that provides utility functions for form security.
+ *
+ * Note:
+ * This module uses isSecureContextIfOpenerIgnored instead of isSecureContext.
+ *
+ * We don't want to expose JavaScript APIs in a non-Secure Context even if
+ * the context is only insecure because the windows has an insecure opener.
+ * Doing so prevents sites from implementing postMessage workarounds to enable
+ * an insecure opener to gain access to Secure Context-only APIs. However,
+ * in the case of form fields such as password fields we don't need to worry
+ * about whether the opener is secure or not. In fact to flag a password
+ * field as insecure in such circumstances would unnecessarily confuse our
+ * users.
+ */
+this.InsecurePasswordUtils = {
+ _formRootsWarned: new WeakMap(),
+ _sendWebConsoleMessage(messageTag, domDoc) {
+ let windowId = WebConsoleUtils.getInnerWindowId(domDoc.defaultView);
+ let category = "Insecure Password Field";
+ // All web console messages are warnings for now.
+ let flag = Ci.nsIScriptError.warningFlag;
+ let bundle = Services.strings.createBundle(STRINGS_URI);
+ let message = bundle.GetStringFromName(messageTag);
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+ consoleMsg.initWithWindowID(message, domDoc.location.href, 0, 0, 0, flag, category, windowId);
+
+ Services.console.logMessage(consoleMsg);
+ },
+
+ /**
+ * Gets the security state of the passed form.
+ *
+ * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
+ *
+ * @returns {Object} An object with the following boolean values:
+ * isFormSubmitHTTP: if the submit action is an http:// URL
+ * isFormSubmitSecure: if the submit action URL is secure,
+ * either because it is HTTPS or because its origin is considered trustworthy
+ */
+ _checkFormSecurity(aForm) {
+ let isFormSubmitHTTP = false, isFormSubmitSecure = false;
+ if (aForm.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
+ let uri = Services.io.newURI(aForm.rootElement.action || aForm.rootElement.baseURI,
+ null, null);
+ let principal = gScriptSecurityManager.getCodebasePrincipal(uri);
+
+ if (uri.schemeIs("http")) {
+ isFormSubmitHTTP = true;
+ if (gContentSecurityManager.isOriginPotentiallyTrustworthy(principal)) {
+ isFormSubmitSecure = true;
+ }
+ } else {
+ isFormSubmitSecure = true;
+ }
+ }
+
+ return { isFormSubmitHTTP, isFormSubmitSecure };
+ },
+
+ /**
+ * Checks if there are insecure password fields present on the form's document
+ * i.e. passwords inside forms with http action, inside iframes with http src,
+ * or on insecure web pages.
+ *
+ * @param {FormLike} aForm A form-like object. @See {LoginFormFactory}
+ * @return {boolean} whether the form is secure
+ */
+ isFormSecure(aForm) {
+ // Ignores window.opener, see top level documentation.
+ let isSafePage = aForm.ownerDocument.defaultView.isSecureContextIfOpenerIgnored;
+ let { isFormSubmitSecure, isFormSubmitHTTP } = this._checkFormSecurity(aForm);
+
+ return isSafePage && (isFormSubmitSecure || !isFormSubmitHTTP);
+ },
+
+ /**
+ * Report insecure password fields in a form to the web console to warn developers.
+ *
+ * @param {FormLike} aForm A form-like object. @See {FormLikeFactory}
+ */
+ reportInsecurePasswords(aForm) {
+ if (this._formRootsWarned.has(aForm.rootElement) ||
+ this._formRootsWarned.get(aForm.rootElement)) {
+ return;
+ }
+
+ let domDoc = aForm.ownerDocument;
+ // Ignores window.opener, see top level documentation.
+ let isSafePage = domDoc.defaultView.isSecureContextIfOpenerIgnored;
+
+ let { isFormSubmitHTTP, isFormSubmitSecure } = this._checkFormSecurity(aForm);
+
+ if (!isSafePage) {
+ if (domDoc.defaultView == domDoc.defaultView.parent) {
+ this._sendWebConsoleMessage("InsecurePasswordsPresentOnPage", domDoc);
+ } else {
+ this._sendWebConsoleMessage("InsecurePasswordsPresentOnIframe", domDoc);
+ }
+ this._formRootsWarned.set(aForm.rootElement, true);
+ } else if (isFormSubmitHTTP && !isFormSubmitSecure) {
+ this._sendWebConsoleMessage("InsecureFormActionPasswordsPresent", domDoc);
+ this._formRootsWarned.set(aForm.rootElement, true);
+ }
+
+ // The safety of a password field determined by the form action and the page protocol
+ let passwordSafety;
+ if (isSafePage) {
+ if (isFormSubmitSecure) {
+ passwordSafety = 0;
+ } else if (isFormSubmitHTTP) {
+ passwordSafety = 1;
+ } else {
+ passwordSafety = 2;
+ }
+ } else if (isFormSubmitSecure) {
+ passwordSafety = 3;
+ } else if (isFormSubmitHTTP) {
+ passwordSafety = 4;
+ } else {
+ passwordSafety = 5;
+ }
+
+ Services.telemetry.getHistogramById("PWMGR_LOGIN_PAGE_SAFETY").add(passwordSafety);
+ },
+};
diff --git a/toolkit/components/passwordmgr/LoginHelper.jsm b/toolkit/components/passwordmgr/LoginHelper.jsm
new file mode 100644
index 0000000000..e0c4d872b6
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -0,0 +1,725 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Contains functions shared by different Login Manager components.
+ *
+ * This JavaScript module exists in order to share code between the different
+ * XPCOM components that constitute the Login Manager, including implementations
+ * of nsILoginManager and nsILoginManagerStorage.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginHelper",
+];
+
+// Globals
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// LoginHelper
+
+/**
+ * Contains functions shared by different Login Manager components.
+ */
+this.LoginHelper = {
+ /**
+ * Warning: these only update if a logger was created.
+ */
+ debug: Services.prefs.getBoolPref("signon.debug"),
+ formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ insecureAutofill: Services.prefs.getBoolPref("signon.autofillForms.http"),
+ showInsecureFieldWarning: Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled"),
+
+ createLogger(aLogPrefix) {
+ let getMaxLogLevel = () => {
+ return this.debug ? "debug" : "warn";
+ };
+
+ // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
+ let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
+ let consoleOptions = {
+ maxLogLevel: getMaxLogLevel(),
+ prefix: aLogPrefix,
+ };
+ let logger = new ConsoleAPI(consoleOptions);
+
+ // Watch for pref changes and update this.debug and the maxLogLevel for created loggers
+ Services.prefs.addObserver("signon.", () => {
+ this.debug = Services.prefs.getBoolPref("signon.debug");
+ this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
+ this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
+ this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
+ logger.maxLogLevel = getMaxLogLevel();
+ }, false);
+
+ Services.prefs.addObserver("security.insecure_field_warning.", () => {
+ this.showInsecureFieldWarning = Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled");
+ }, false);
+
+ return logger;
+ },
+
+ /**
+ * Due to the way the signons2.txt file is formatted, we need to make
+ * sure certain field values or characters do not cause the file to
+ * be parsed incorrectly. Reject hostnames that we can't store correctly.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ checkHostnameValue(aHostname) {
+ // Nulls are invalid, as they don't round-trip well. Newlines are also
+ // invalid for any field stored as plaintext, and a hostname made of a
+ // single dot cannot be stored in the legacy format.
+ if (aHostname == "." ||
+ aHostname.indexOf("\r") != -1 ||
+ aHostname.indexOf("\n") != -1 ||
+ aHostname.indexOf("\0") != -1) {
+ throw new Error("Invalid hostname");
+ }
+ },
+
+ /**
+ * Due to the way the signons2.txt file is formatted, we need to make
+ * sure certain field values or characters do not cause the file to
+ * be parsed incorrectly. Reject logins that we can't store correctly.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ checkLoginValues(aLogin) {
+ function badCharacterPresent(l, c) {
+ return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
+ (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
+ l.hostname.indexOf(c) != -1 ||
+ l.usernameField.indexOf(c) != -1 ||
+ l.passwordField.indexOf(c) != -1);
+ }
+
+ // Nulls are invalid, as they don't round-trip well.
+ // Mostly not a formatting problem, although ".\0" can be quirky.
+ if (badCharacterPresent(aLogin, "\0")) {
+ throw new Error("login values can't contain nulls");
+ }
+
+ // In theory these nulls should just be rolled up into the encrypted
+ // values, but nsISecretDecoderRing doesn't use nsStrings, so the
+ // nulls cause truncation. Check for them here just to avoid
+ // unexpected round-trip surprises.
+ if (aLogin.username.indexOf("\0") != -1 ||
+ aLogin.password.indexOf("\0") != -1) {
+ throw new Error("login values can't contain nulls");
+ }
+
+ // Newlines are invalid for any field stored as plaintext.
+ if (badCharacterPresent(aLogin, "\r") ||
+ badCharacterPresent(aLogin, "\n")) {
+ throw new Error("login values can't contain newlines");
+ }
+
+ // A line with just a "." can have special meaning.
+ if (aLogin.usernameField == "." ||
+ aLogin.formSubmitURL == ".") {
+ throw new Error("login values can't be periods");
+ }
+
+ // A hostname with "\ \(" won't roundtrip.
+ // eg host="foo (", realm="bar" --> "foo ( (bar)"
+ // vs host="foo", realm=" (bar" --> "foo ( (bar)"
+ if (aLogin.hostname.indexOf(" (") != -1) {
+ throw new Error("bad parens in hostname");
+ }
+ },
+
+ /**
+ * Returns a new XPCOM property bag with the provided properties.
+ *
+ * @param {Object} aProperties
+ * Each property of this object is copied to the property bag. This
+ * parameter can be omitted to return an empty property bag.
+ *
+ * @return A new property bag, that is an instance of nsIWritablePropertyBag,
+ * nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
+ */
+ newPropertyBag(aProperties) {
+ let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
+ .createInstance(Ci.nsIWritablePropertyBag);
+ if (aProperties) {
+ for (let [name, value] of Object.entries(aProperties)) {
+ propertyBag.setProperty(name, value);
+ }
+ }
+ return propertyBag.QueryInterface(Ci.nsIPropertyBag)
+ .QueryInterface(Ci.nsIPropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag2);
+ },
+
+ /**
+ * Helper to avoid the `count` argument and property bags when calling
+ * Services.logins.searchLogins from JS.
+ *
+ * @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
+ * @return {nsILoginInfo[]} - The result of calling searchLogins.
+ */
+ searchLoginsWithObject(aSearchOptions) {
+ return Services.logins.searchLogins({}, this.newPropertyBag(aSearchOptions));
+ },
+
+ /**
+ * @param {String} aLoginOrigin - An origin value from a stored login's
+ * hostname or formSubmitURL properties.
+ * @param {String} aSearchOrigin - The origin that was are looking to match
+ * with aLoginOrigin. This would normally come
+ * from a form or page that we are considering.
+ * @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
+ * from the login (aLoginOrigin) is a
+ * match for the origin we're looking
+ * for (aSearchOrigin).
+ */
+ isOriginMatching(aLoginOrigin, aSearchOrigin, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ if (aLoginOrigin == aSearchOrigin) {
+ return true;
+ }
+
+ if (!aOptions) {
+ return false;
+ }
+
+ if (aOptions.schemeUpgrades) {
+ try {
+ let loginURI = Services.io.newURI(aLoginOrigin, null, null);
+ let searchURI = Services.io.newURI(aSearchOrigin, null, null);
+ if (loginURI.scheme == "http" && searchURI.scheme == "https" &&
+ loginURI.hostPort == searchURI.hostPort) {
+ return true;
+ }
+ } catch (ex) {
+ // newURI will throw for some values e.g. chrome://FirefoxAccounts
+ return false;
+ }
+ }
+
+ return false;
+ },
+
+ doLoginsMatch(aLogin1, aLogin2, {
+ ignorePassword = false,
+ ignoreSchemes = false,
+ }) {
+ if (aLogin1.httpRealm != aLogin2.httpRealm ||
+ aLogin1.username != aLogin2.username)
+ return false;
+
+ if (!ignorePassword && aLogin1.password != aLogin2.password)
+ return false;
+
+ if (ignoreSchemes) {
+ let hostname1URI = Services.io.newURI(aLogin1.hostname, null, null);
+ let hostname2URI = Services.io.newURI(aLogin2.hostname, null, null);
+ if (hostname1URI.hostPort != hostname2URI.hostPort)
+ return false;
+
+ if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
+ Services.io.newURI(aLogin1.formSubmitURL, null, null).hostPort !=
+ Services.io.newURI(aLogin2.formSubmitURL, null, null).hostPort)
+ return false;
+ } else {
+ if (aLogin1.hostname != aLogin2.hostname)
+ return false;
+
+ // If either formSubmitURL is blank (but not null), then match.
+ if (aLogin1.formSubmitURL != "" && aLogin2.formSubmitURL != "" &&
+ aLogin1.formSubmitURL != aLogin2.formSubmitURL)
+ return false;
+ }
+
+ // The .usernameField and .passwordField values are ignored.
+
+ return true;
+ },
+
+ /**
+ * Creates a new login object that results by modifying the given object with
+ * the provided data.
+ *
+ * @param aOldStoredLogin
+ * Existing nsILoginInfo object to modify.
+ * @param aNewLoginData
+ * The new login values, either as nsILoginInfo or nsIProperyBag.
+ *
+ * @return The newly created nsILoginInfo object.
+ *
+ * @throws String with English message in case validation failed.
+ */
+ buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
+ function bagHasProperty(aPropName) {
+ try {
+ aNewLoginData.getProperty(aPropName);
+ return true;
+ } catch (ex) { }
+ return false;
+ }
+
+ aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ let newLogin;
+ if (aNewLoginData instanceof Ci.nsILoginInfo) {
+ // Clone the existing login to get its nsILoginMetaInfo, then init it
+ // with the replacement nsILoginInfo data from the new login.
+ newLogin = aOldStoredLogin.clone();
+ newLogin.init(aNewLoginData.hostname,
+ aNewLoginData.formSubmitURL, aNewLoginData.httpRealm,
+ aNewLoginData.username, aNewLoginData.password,
+ aNewLoginData.usernameField, aNewLoginData.passwordField);
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // Automatically update metainfo when password is changed.
+ if (newLogin.password != aOldStoredLogin.password) {
+ newLogin.timePasswordChanged = Date.now();
+ }
+ } else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
+ // Clone the existing login, along with all its properties.
+ newLogin = aOldStoredLogin.clone();
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // Automatically update metainfo when password is changed.
+ // (Done before the main property updates, lest the caller be
+ // explicitly updating both .password and .timePasswordChanged)
+ if (bagHasProperty("password")) {
+ let newPassword = aNewLoginData.getProperty("password");
+ if (newPassword != aOldStoredLogin.password) {
+ newLogin.timePasswordChanged = Date.now();
+ }
+ }
+
+ let propEnum = aNewLoginData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // nsILoginInfo
+ case "hostname":
+ case "httpRealm":
+ case "formSubmitURL":
+ case "username":
+ case "password":
+ case "usernameField":
+ case "passwordField":
+ // nsILoginMetaInfo
+ case "guid":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ newLogin[prop.name] = prop.value;
+ break;
+
+ // Fake property, allows easy incrementing.
+ case "timesUsedIncrement":
+ newLogin.timesUsed += prop.value;
+ break;
+
+ // Fail if caller requests setting an unknown property.
+ default:
+ throw new Error("Unexpected propertybag item: " + prop.name);
+ }
+ }
+ } else {
+ throw new Error("newLoginData needs an expected interface!");
+ }
+
+ // Sanity check the login
+ if (newLogin.hostname == null || newLogin.hostname.length == 0) {
+ throw new Error("Can't add a login with a null or empty hostname.");
+ }
+
+ // For logins w/o a username, set to "", not null.
+ if (newLogin.username == null) {
+ throw new Error("Can't add a login with a null username.");
+ }
+
+ if (newLogin.password == null || newLogin.password.length == 0) {
+ throw new Error("Can't add a login with a null or empty password.");
+ }
+
+ if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (newLogin.httpRealm != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else if (newLogin.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (newLogin.formSubmitURL != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else {
+ // Need one or the other!
+ throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
+ }
+
+ // Throws if there are bogus values.
+ this.checkLoginValues(newLogin);
+
+ return newLogin;
+ },
+
+ /**
+ * Removes duplicates from a list of logins while preserving the sort order.
+ *
+ * @param {nsILoginInfo[]} logins
+ * A list of logins we want to deduplicate.
+ * @param {string[]} [uniqueKeys = ["username", "password"]]
+ * A list of login attributes to use as unique keys for the deduplication.
+ * @param {string[]} [resolveBy = ["timeLastUsed"]]
+ * Ordered array of keyword strings used to decide which of the
+ * duplicates should be used. "scheme" would prefer the login that has
+ * a scheme matching `preferredOrigin`'s if there are two logins with
+ * the same `uniqueKeys`. The default preference to distinguish two
+ * logins is `timeLastUsed`. If there is no preference between two
+ * logins, the first one found wins.
+ * @param {string} [preferredOrigin = undefined]
+ * String representing the origin to use for preferring one login over
+ * another when they are dupes. This is used with "scheme" for
+ * `resolveBy` so the scheme from this origin will be preferred.
+ *
+ * @returns {nsILoginInfo[]} list of unique logins.
+ */
+ dedupeLogins(logins, uniqueKeys = ["username", "password"],
+ resolveBy = ["timeLastUsed"],
+ preferredOrigin = undefined) {
+ const KEY_DELIMITER = ":";
+
+ if (!preferredOrigin && resolveBy.includes("scheme")) {
+ throw new Error("dedupeLogins: `preferredOrigin` is required in order to " +
+ "prefer schemes which match it.");
+ }
+
+ let preferredOriginScheme;
+ if (preferredOrigin) {
+ try {
+ preferredOriginScheme = Services.io.newURI(preferredOrigin, null, null).scheme;
+ } catch (ex) {
+ // Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts
+ }
+ }
+
+ if (!preferredOriginScheme && resolveBy.includes("scheme")) {
+ log.warn("dedupeLogins: Deduping with a scheme preference but couldn't " +
+ "get the preferred origin scheme.");
+ }
+
+ // We use a Map to easily lookup logins by their unique keys.
+ let loginsByKeys = new Map();
+
+ // Generate a unique key string from a login.
+ function getKey(login, uniqueKeys) {
+ return uniqueKeys.reduce((prev, key) => prev + KEY_DELIMITER + login[key], "");
+ }
+
+ /**
+ * @return {bool} whether `login` is preferred over its duplicate (considering `uniqueKeys`)
+ * `existingLogin`.
+ *
+ * `resolveBy` is a sorted array so we can return true the first time `login` is preferred
+ * over the existingLogin.
+ */
+ function isLoginPreferred(existingLogin, login) {
+ if (!resolveBy || resolveBy.length == 0) {
+ // If there is no preference, prefer the existing login.
+ return false;
+ }
+
+ for (let preference of resolveBy) {
+ switch (preference) {
+ case "scheme": {
+ if (!preferredOriginScheme) {
+ break;
+ }
+
+ try {
+ // Only `hostname` is currently considered
+ let existingLoginURI = Services.io.newURI(existingLogin.hostname, null, null);
+ let loginURI = Services.io.newURI(login.hostname, null, null);
+ // If the schemes of the two logins are the same or neither match the
+ // preferredOriginScheme then we have no preference and look at the next resolveBy.
+ if (loginURI.scheme == existingLoginURI.scheme ||
+ (loginURI.scheme != preferredOriginScheme &&
+ existingLoginURI.scheme != preferredOriginScheme)) {
+ break;
+ }
+
+ return loginURI.scheme == preferredOriginScheme;
+ } catch (ex) {
+ // Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts)
+ log.debug("dedupeLogins/shouldReplaceExisting: Error comparing schemes:",
+ existingLogin.hostname, login.hostname,
+ "preferredOrigin:", preferredOrigin, ex);
+ }
+ break;
+ }
+ case "timeLastUsed":
+ case "timePasswordChanged": {
+ // If we find a more recent login for the same key, replace the existing one.
+ let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo)[preference];
+ let storedLoginDate = existingLogin.QueryInterface(Ci.nsILoginMetaInfo)[preference];
+ if (loginDate == storedLoginDate) {
+ break;
+ }
+
+ return loginDate > storedLoginDate;
+ }
+ default: {
+ throw new Error("dedupeLogins: Invalid resolveBy preference: " + preference);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ for (let login of logins) {
+ let key = getKey(login, uniqueKeys);
+
+ if (loginsByKeys.has(key)) {
+ if (!isLoginPreferred(loginsByKeys.get(key), login)) {
+ // If there is no preference for the new login, use the existing one.
+ continue;
+ }
+ }
+ loginsByKeys.set(key, login);
+ }
+
+ // Return the map values in the form of an array.
+ return [...loginsByKeys.values()];
+ },
+
+ /**
+ * Open the password manager window.
+ *
+ * @param {Window} window
+ * the window from where we want to open the dialog
+ *
+ * @param {string} [filterString=""]
+ * the filterString parameter to pass to the login manager dialog
+ */
+ openPasswordManager(window, filterString = "") {
+ let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
+ if (win) {
+ win.setFilter(filterString);
+ win.focus();
+ } else {
+ window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
+ "Toolkit:PasswordManager", "",
+ {filterString : filterString});
+ }
+ },
+
+ /**
+ * Checks if a field type is username compatible.
+ *
+ * @param {Element} element
+ * the field we want to check.
+ *
+ * @returns {Boolean} true if the field type is one
+ * of the username types.
+ */
+ isUsernameFieldType(element) {
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement))
+ return false;
+
+ let fieldType = (element.hasAttribute("type") ?
+ element.getAttribute("type").toLowerCase() :
+ element.type);
+ if (fieldType == "text" ||
+ fieldType == "email" ||
+ fieldType == "url" ||
+ fieldType == "tel" ||
+ fieldType == "number") {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Add the login to the password manager if a similar one doesn't already exist. Merge it
+ * otherwise with the similar existing ones.
+ * @param {Object} loginData - the data about the login that needs to be added.
+ * @returns {nsILoginInfo} the newly added login, or null if no login was added.
+ * Note that we will also return null if an existing login
+ * was modified.
+ */
+ maybeImportLogin(loginData) {
+ // create a new login
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ login.init(loginData.hostname,
+ loginData.formSubmitURL || (typeof(loginData.httpRealm) == "string" ? null : ""),
+ typeof(loginData.httpRealm) == "string" ? loginData.httpRealm : null,
+ loginData.username,
+ loginData.password,
+ loginData.usernameElement || "",
+ loginData.passwordElement || "");
+
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.timeCreated = loginData.timeCreated;
+ login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
+ login.timePasswordChanged = loginData.timePasswordChanged || loginData.timeCreated;
+ login.timesUsed = loginData.timesUsed || 1;
+ // While here we're passing formSubmitURL and httpRealm, they could be empty/null and get
+ // ignored in that case, leading to multiple logins for the same username.
+ let existingLogins = Services.logins.findLogins({}, login.hostname,
+ login.formSubmitURL,
+ login.httpRealm);
+ // Check for an existing login that matches *including* the password.
+ // If such a login exists, we do not need to add a new login.
+ if (existingLogins.some(l => login.matches(l, false /* ignorePassword */))) {
+ return null;
+ }
+ // Now check for a login with the same username, where it may be that we have an
+ // updated password.
+ let foundMatchingLogin = false;
+ for (let existingLogin of existingLogins) {
+ if (login.username == existingLogin.username) {
+ foundMatchingLogin = true;
+ existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ if (login.password != existingLogin.password &
+ login.timePasswordChanged > existingLogin.timePasswordChanged) {
+ // if a login with the same username and different password already exists and it's older
+ // than the current one, update its password and timestamp.
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ propBag.setProperty("password", login.password);
+ propBag.setProperty("timePasswordChanged", login.timePasswordChanged);
+ Services.logins.modifyLogin(existingLogin, propBag);
+ }
+ }
+ }
+ // if the new login is an update or is older than an exiting login, don't add it.
+ if (foundMatchingLogin) {
+ return null;
+ }
+ return Services.logins.addLogin(login);
+ },
+
+ /**
+ * Convert an array of nsILoginInfo to vanilla JS objects suitable for
+ * sending over IPC.
+ *
+ * NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
+ */
+ loginsToVanillaObjects(logins) {
+ return logins.map(this.loginToVanillaObject);
+ },
+
+ /**
+ * Same as above, but for a single login.
+ */
+ loginToVanillaObject(login) {
+ let obj = {};
+ for (let i in login.QueryInterface(Ci.nsILoginMetaInfo)) {
+ if (typeof login[i] !== 'function') {
+ obj[i] = login[i];
+ }
+ }
+
+ return obj;
+ },
+
+ /**
+ * Convert an object received from IPC into an nsILoginInfo (with guid).
+ */
+ vanillaObjectToLogin(login) {
+ let formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ formLogin.init(login.hostname, login.formSubmitURL,
+ login.httpRealm, login.username,
+ login.password, login.usernameField,
+ login.passwordField);
+
+ formLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ for (let prop of ["guid", "timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
+ formLogin[prop] = login[prop];
+ }
+ return formLogin;
+ },
+
+ /**
+ * As above, but for an array of objects.
+ */
+ vanillaObjectsToLogins(logins) {
+ return logins.map(this.vanillaObjectToLogin);
+ },
+
+ removeLegacySignonFiles() {
+ const {Constants, Path, File} = Cu.import("resource://gre/modules/osfile.jsm").OS;
+
+ const profileDir = Constants.Path.profileDir;
+ const defaultSignonFilePrefs = new Map([
+ ["signon.SignonFileName", "signons.txt"],
+ ["signon.SignonFileName2", "signons2.txt"],
+ ["signon.SignonFileName3", "signons3.txt"]
+ ]);
+ const toDeletes = new Set();
+
+ for (let [pref, val] of defaultSignonFilePrefs.entries()) {
+ toDeletes.add(Path.join(profileDir, val));
+
+ try {
+ let signonFile = Services.prefs.getCharPref(pref);
+
+ toDeletes.add(Path.join(profileDir, signonFile));
+ Services.prefs.clearUserPref(pref);
+ } catch (e) {}
+ }
+
+ for (let file of toDeletes) {
+ File.remove(file);
+ }
+ },
+
+ /**
+ * Returns true if the user has a master password set and false otherwise.
+ */
+ isMasterPasswordSet() {
+ let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].
+ getService(Ci.nsIPKCS11ModuleDB);
+ let slot = secmodDB.findSlotByName("");
+ if (!slot) {
+ return false;
+ }
+ let hasMP = slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
+ slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
+ return hasMP;
+ },
+
+ /**
+ * Send a notification when stored data is changed.
+ */
+ notifyStorageChanged(changeType, data) {
+ let dataObject = data;
+ // Can't pass a raw JS string or array though notifyObservers(). :-(
+ if (Array.isArray(data)) {
+ dataObject = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ for (let i = 0; i < data.length; i++) {
+ dataObject.appendElement(data[i], false);
+ }
+ } else if (typeof(data) == "string") {
+ dataObject = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ dataObject.data = data;
+ }
+ Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
+ }
+};
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginHelper");
+ return logger;
+});
diff --git a/toolkit/components/passwordmgr/LoginImport.jsm b/toolkit/components/passwordmgr/LoginImport.jsm
new file mode 100644
index 0000000000..a1d5c988a7
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginImport.jsm
@@ -0,0 +1,173 @@
+/* -*- 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/. */
+
+/**
+ * Provides an object that has a method to import login-related data from the
+ * previous SQLite storage format.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginImport",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+// LoginImport
+
+/**
+ * Provides an object that has a method to import login-related data from the
+ * previous SQLite storage format.
+ *
+ * @param aStore
+ * LoginStore object where imported data will be added.
+ * @param aPath
+ * String containing the file path of the SQLite login database.
+ */
+this.LoginImport = function (aStore, aPath) {
+ this.store = aStore;
+ this.path = aPath;
+};
+
+this.LoginImport.prototype = {
+ /**
+ * LoginStore object where imported data will be added.
+ */
+ store: null,
+
+ /**
+ * String containing the file path of the SQLite login database.
+ */
+ path: null,
+
+ /**
+ * Imports login-related data from the previous SQLite storage format.
+ */
+ import: Task.async(function* () {
+ // We currently migrate data directly from the database to the JSON store at
+ // first run, then we set a preference to prevent repeating the import.
+ // Thus, merging with existing data is not a use case we support. This
+ // restriction might be removed to support re-importing passwords set by an
+ // old version by flipping the import preference and restarting.
+ if (this.store.data.logins.length > 0 ||
+ this.store.data.disabledHosts.length > 0) {
+ throw new Error("Unable to import saved passwords because some data " +
+ "has already been imported or saved.");
+ }
+
+ // When a timestamp is not specified, we will use the same reference time.
+ let referenceTimeMs = Date.now();
+
+ let connection = yield Sqlite.openConnection({ path: this.path });
+ try {
+ let schemaVersion = yield connection.getSchemaVersion();
+
+ // We support importing database schema versions from 3 onwards.
+ // Version 3 was implemented in bug 316084 (Firefox 3.6, March 2009).
+ // Version 4 was implemented in bug 465636 (Firefox 4, March 2010).
+ // Version 5 was implemented in bug 718817 (Firefox 13, February 2012).
+ if (schemaVersion < 3) {
+ throw new Error("Unable to import saved passwords because " +
+ "the existing profile is too old.");
+ }
+
+ let rows = yield connection.execute("SELECT * FROM moz_logins");
+ for (let row of rows) {
+ try {
+ let hostname = row.getResultByName("hostname");
+ let httpRealm = row.getResultByName("httpRealm");
+ let formSubmitURL = row.getResultByName("formSubmitURL");
+ let usernameField = row.getResultByName("usernameField");
+ let passwordField = row.getResultByName("passwordField");
+ let encryptedUsername = row.getResultByName("encryptedUsername");
+ let encryptedPassword = row.getResultByName("encryptedPassword");
+
+ // The "guid" field was introduced in schema version 2, and the
+ // "enctype" field was introduced in schema version 3. We don't
+ // support upgrading from older versions of the database.
+ let guid = row.getResultByName("guid");
+ let encType = row.getResultByName("encType");
+
+ // The time and count fields were introduced in schema version 4.
+ let timeCreated = null;
+ let timeLastUsed = null;
+ let timePasswordChanged = null;
+ let timesUsed = null;
+ try {
+ timeCreated = row.getResultByName("timeCreated");
+ timeLastUsed = row.getResultByName("timeLastUsed");
+ timePasswordChanged = row.getResultByName("timePasswordChanged");
+ timesUsed = row.getResultByName("timesUsed");
+ } catch (ex) { }
+
+ // These columns may be null either because they were not present in
+ // the database or because the record was created on a new schema
+ // version by an old application version.
+ if (!timeCreated) {
+ timeCreated = referenceTimeMs;
+ }
+ if (!timeLastUsed) {
+ timeLastUsed = referenceTimeMs;
+ }
+ if (!timePasswordChanged) {
+ timePasswordChanged = referenceTimeMs;
+ }
+ if (!timesUsed) {
+ timesUsed = 1;
+ }
+
+ this.store.data.logins.push({
+ id: this.store.data.nextId++,
+ hostname: hostname,
+ httpRealm: httpRealm,
+ formSubmitURL: formSubmitURL,
+ usernameField: usernameField,
+ passwordField: passwordField,
+ encryptedUsername: encryptedUsername,
+ encryptedPassword: encryptedPassword,
+ guid: guid,
+ encType: encType,
+ timeCreated: timeCreated,
+ timeLastUsed: timeLastUsed,
+ timePasswordChanged: timePasswordChanged,
+ timesUsed: timesUsed,
+ });
+ } catch (ex) {
+ Cu.reportError("Error importing login: " + ex);
+ }
+ }
+
+ rows = yield connection.execute("SELECT * FROM moz_disabledHosts");
+ for (let row of rows) {
+ try {
+ let hostname = row.getResultByName("hostname");
+
+ this.store.data.disabledHosts.push(hostname);
+ } catch (ex) {
+ Cu.reportError("Error importing disabled host: " + ex);
+ }
+ }
+ } finally {
+ yield connection.close();
+ }
+ }),
+};
diff --git a/toolkit/components/passwordmgr/LoginManagerContent.jsm b/toolkit/components/passwordmgr/LoginManagerContent.jsm
new file mode 100644
index 0000000000..60805530d3
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -0,0 +1,1619 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
+ "LoginFormFactory",
+ "UserAutoCompleteResult" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
+const AUTOCOMPLETE_AFTER_CONTEXTMENU_THRESHOLD_MS = 250;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Cu.import("resource://gre/modules/InsecurePasswordUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
+ "resource://gre/modules/FormLikeFactory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
+ "resource://gre/modules/LoginRecipes.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+ "resource://gre/modules/InsecurePasswordUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gNetUtil",
+ "@mozilla.org/network/util;1",
+ "nsINetUtil");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginManagerContent");
+ return logger.log.bind(logger);
+});
+
+// These mirror signon.* prefs.
+var gEnabled, gAutofillForms, gStoreWhenAutocompleteOff;
+var gLastContextMenuEventTimeStamp = Number.NEGATIVE_INFINITY;
+
+var observer = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIFormSubmitObserver,
+ Ci.nsIWebProgressListener,
+ Ci.nsIDOMEventListener,
+ Ci.nsISupportsWeakReference]),
+
+ // nsIFormSubmitObserver
+ notify(formElement, aWindow, actionURI) {
+ log("observer notified for form submission.");
+
+ // We're invoked before the content's |onsubmit| handlers, so we
+ // can grab form data before it might be modified (see bug 257781).
+
+ try {
+ let formLike = LoginFormFactory.createFromForm(formElement);
+ LoginManagerContent._onFormSubmit(formLike);
+ } catch (e) {
+ log("Caught error in onFormSubmit(", e.lineNumber, "):", e.message);
+ Cu.reportError(e);
+ }
+
+ return true; // Always return true, or form submit will be canceled.
+ },
+
+ onPrefChange() {
+ gEnabled = Services.prefs.getBoolPref("signon.rememberSignons");
+ gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
+ gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
+ },
+
+ // nsIWebProgressListener
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // Only handle pushState/replaceState here.
+ if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
+ !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
+ return;
+ }
+
+ log("onLocationChange handled:", aLocation.spec, aWebProgress.DOMWindow.document);
+
+ LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
+ },
+
+ onStateChange(aWebProgress, aRequest, aState, aStatus) {
+ if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
+ return;
+ }
+
+ // We only care about when a page triggered a load, not the user. For example:
+ // clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't
+ // likely to be when a user wants to save a login.
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ if (triggeringPrincipal.isNullPrincipal ||
+ triggeringPrincipal.equals(Services.scriptSecurityManager.getSystemPrincipal())) {
+ return;
+ }
+
+ // Don't handle history navigation, reload, or pushState not triggered via chrome UI.
+ // e.g. history.go(-1), location.reload(), history.replaceState()
+ if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
+ log("onStateChange: loadType isn't LOAD_CMD_NORMAL:", aWebProgress.loadType);
+ return;
+ }
+
+ log("onStateChange handled:", channel);
+ LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
+ },
+
+ handleEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ return;
+ }
+
+ if (!gEnabled) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ // Only used for username fields.
+ case "focus": {
+ LoginManagerContent._onUsernameFocus(aEvent);
+ break;
+ }
+
+ case "contextmenu": {
+ gLastContextMenuEventTimeStamp = Date.now();
+ break;
+ }
+
+ default: {
+ throw new Error("Unexpected event");
+ }
+ }
+ },
+};
+
+Services.obs.addObserver(observer, "earlyformsubmit", false);
+var prefBranch = Services.prefs.getBranch("signon.");
+prefBranch.addObserver("", observer.onPrefChange, false);
+
+observer.onPrefChange(); // read initial values
+
+
+function messageManagerFromWindow(win) {
+ return win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+}
+
+// This object maps to the "child" process (even in the single-process case).
+var LoginManagerContent = {
+
+ __formFillService : null, // FormFillController, for username autocompleting
+ get _formFillService() {
+ if (!this.__formFillService)
+ this.__formFillService =
+ Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(Ci.nsIFormFillController);
+ return this.__formFillService;
+ },
+
+ _getRandomId() {
+ return Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ },
+
+ _messages: [ "RemoteLogins:loginsFound",
+ "RemoteLogins:loginsAutoCompleted" ],
+
+ /**
+ * WeakMap of the root element of a FormLike to the FormLike representing its fields.
+ *
+ * This is used to be able to lookup an existing FormLike for a given root element since multiple
+ * calls to LoginFormFactory won't give the exact same object. When batching fills we don't always
+ * want to use the most recent list of elements for a FormLike since we may end up doing multiple
+ * fills for the same set of elements when a field gets added between arming and running the
+ * DeferredTask.
+ *
+ * @type {WeakMap}
+ */
+ _formLikeByRootElement: new WeakMap(),
+
+ /**
+ * WeakMap of the root element of a WeakMap to the DeferredTask to fill its fields.
+ *
+ * This is used to be able to throttle fills for a FormLike since onDOMInputPasswordAdded gets
+ * dispatched for each password field added to a document but we only want to fill once per
+ * FormLike when multiple fields are added at once.
+ *
+ * @type {WeakMap}
+ */
+ _deferredPasswordAddedTasksByRootElement: new WeakMap(),
+
+ // Map from form login requests to information about that request.
+ _requests: new Map(),
+
+ // Number of outstanding requests to each manager.
+ _managers: new Map(),
+
+ _takeRequest(msg) {
+ let data = msg.data;
+ let request = this._requests.get(data.requestId);
+
+ this._requests.delete(data.requestId);
+
+ let count = this._managers.get(msg.target);
+ if (--count === 0) {
+ this._managers.delete(msg.target);
+
+ for (let message of this._messages)
+ msg.target.removeMessageListener(message, this);
+ } else {
+ this._managers.set(msg.target, count);
+ }
+
+ return request;
+ },
+
+ _sendRequest(messageManager, requestData,
+ name, messageData) {
+ let count;
+ if (!(count = this._managers.get(messageManager))) {
+ this._managers.set(messageManager, 1);
+
+ for (let message of this._messages)
+ messageManager.addMessageListener(message, this);
+ } else {
+ this._managers.set(messageManager, ++count);
+ }
+
+ let requestId = this._getRandomId();
+ messageData.requestId = requestId;
+
+ messageManager.sendAsyncMessage(name, messageData);
+
+ let deferred = Promise.defer();
+ requestData.promise = deferred;
+ this._requests.set(requestId, requestData);
+ return deferred.promise;
+ },
+
+ receiveMessage(msg, window) {
+ if (msg.name == "RemoteLogins:fillForm") {
+ this.fillForm({
+ topDocument: window.document,
+ loginFormOrigin: msg.data.loginFormOrigin,
+ loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
+ recipes: msg.data.recipes,
+ inputElement: msg.objects.inputElement,
+ });
+ return;
+ }
+
+ let request = this._takeRequest(msg);
+ switch (msg.name) {
+ case "RemoteLogins:loginsFound": {
+ let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
+ request.promise.resolve({
+ form: request.form,
+ loginsFound: loginsFound,
+ recipes: msg.data.recipes,
+ });
+ break;
+ }
+
+ case "RemoteLogins:loginsAutoCompleted": {
+ let loginsFound =
+ LoginHelper.vanillaObjectsToLogins(msg.data.logins);
+ // If we're in the parent process, don't pass a message manager so our
+ // autocomplete result objects know they can remove the login from the
+ // login manager directly.
+ let messageManager =
+ (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) ?
+ msg.target : undefined;
+ request.promise.resolve({ logins: loginsFound, messageManager });
+ break;
+ }
+ }
+ },
+
+ /**
+ * Get relevant logins and recipes from the parent
+ *
+ * @param {HTMLFormElement} form - form to get login data for
+ * @param {Object} options
+ * @param {boolean} options.showMasterPassword - whether to show a master password prompt
+ */
+ _getLoginDataFromParent(form, options) {
+ let doc = form.ownerDocument;
+ let win = doc.defaultView;
+
+ let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
+ if (!formOrigin) {
+ return Promise.reject("_getLoginDataFromParent: A form origin is required");
+ }
+ let actionOrigin = LoginUtils._getActionOrigin(form);
+
+ let messageManager = messageManagerFromWindow(win);
+
+ // XXX Weak??
+ let requestData = { form: form };
+ let messageData = { formOrigin: formOrigin,
+ actionOrigin: actionOrigin,
+ options: options };
+
+ return this._sendRequest(messageManager, requestData,
+ "RemoteLogins:findLogins",
+ messageData);
+ },
+
+ _autoCompleteSearchAsync(aSearchString, aPreviousResult,
+ aElement, aRect) {
+ let doc = aElement.ownerDocument;
+ let form = LoginFormFactory.createFromField(aElement);
+ let win = doc.defaultView;
+
+ let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
+ let actionOrigin = LoginUtils._getActionOrigin(form);
+
+ let messageManager = messageManagerFromWindow(win);
+
+ let remote = (Services.appinfo.processType ===
+ Services.appinfo.PROCESS_TYPE_CONTENT);
+
+ let previousResult = aPreviousResult ?
+ { searchString: aPreviousResult.searchString,
+ logins: LoginHelper.loginsToVanillaObjects(aPreviousResult.logins) } :
+ null;
+
+ let requestData = {};
+ let messageData = { formOrigin: formOrigin,
+ actionOrigin: actionOrigin,
+ searchString: aSearchString,
+ previousResult: previousResult,
+ rect: aRect,
+ isSecure: InsecurePasswordUtils.isFormSecure(form),
+ isPasswordField: aElement.type == "password",
+ remote: remote };
+
+ return this._sendRequest(messageManager, requestData,
+ "RemoteLogins:autoCompleteLogins",
+ messageData);
+ },
+
+ setupProgressListener(window) {
+ if (!LoginHelper.formlessCaptureEnabled) {
+ return;
+ }
+
+ try {
+ let webProgress = window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShell).
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(observer,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+ } catch (ex) {
+ // Ignore NS_ERROR_FAILURE if the progress listener was already added
+ }
+ },
+
+ onDOMFormHasPassword(event, window) {
+ if (!event.isTrusted) {
+ return;
+ }
+
+ let form = event.target;
+ let formLike = LoginFormFactory.createFromForm(form);
+ log("onDOMFormHasPassword:", form, formLike);
+ this._fetchLoginsFromParentAndFillForm(formLike, window);
+ },
+
+ onDOMInputPasswordAdded(event, window) {
+ if (!event.isTrusted) {
+ return;
+ }
+
+ let pwField = event.target;
+ if (pwField.form) {
+ // Fill is handled by onDOMFormHasPassword which is already throttled.
+ return;
+ }
+
+ // Only setup the listener for formless inputs.
+ // Capture within a <form> but without a submit event is bug 1287202.
+ this.setupProgressListener(window);
+
+ let formLike = LoginFormFactory.createFromField(pwField);
+ log("onDOMInputPasswordAdded:", pwField, formLike);
+
+ let deferredTask = this._deferredPasswordAddedTasksByRootElement.get(formLike.rootElement);
+ if (!deferredTask) {
+ log("Creating a DeferredTask to call _fetchLoginsFromParentAndFillForm soon");
+ this._formLikeByRootElement.set(formLike.rootElement, formLike);
+
+ deferredTask = new DeferredTask(function* deferredInputProcessing() {
+ // Get the updated formLike instead of the one at the time of creating the DeferredTask via
+ // a closure since it could be stale since FormLike.elements isn't live.
+ let formLike2 = this._formLikeByRootElement.get(formLike.rootElement);
+ log("Running deferred processing of onDOMInputPasswordAdded", formLike2);
+ this._deferredPasswordAddedTasksByRootElement.delete(formLike2.rootElement);
+ this._fetchLoginsFromParentAndFillForm(formLike2, window);
+ }.bind(this), PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS);
+
+ this._deferredPasswordAddedTasksByRootElement.set(formLike.rootElement, deferredTask);
+ }
+
+ if (deferredTask.isArmed) {
+ log("DeferredTask is already armed so just updating the FormLike");
+ // We update the FormLike so it (most important .elements) is fresh when the task eventually
+ // runs since changes to the elements could affect our field heuristics.
+ this._formLikeByRootElement.set(formLike.rootElement, formLike);
+ } else if (window.document.readyState == "complete") {
+ log("Arming the DeferredTask we just created since document.readyState == 'complete'");
+ deferredTask.arm();
+ } else {
+ window.addEventListener("DOMContentLoaded", function armPasswordAddedTask() {
+ window.removeEventListener("DOMContentLoaded", armPasswordAddedTask);
+ log("Arming the onDOMInputPasswordAdded DeferredTask due to DOMContentLoaded");
+ deferredTask.arm();
+ });
+ }
+ },
+
+ /**
+ * Fetch logins from the parent for a given form and then attempt to fill it.
+ *
+ * @param {FormLike} form to fetch the logins for then try autofill.
+ * @param {Window} window
+ */
+ _fetchLoginsFromParentAndFillForm(form, window) {
+ this._detectInsecureFormLikes(window);
+
+ let messageManager = messageManagerFromWindow(window);
+ messageManager.sendAsyncMessage("LoginStats:LoginEncountered");
+
+ if (!gEnabled) {
+ return;
+ }
+
+ this._getLoginDataFromParent(form, { showMasterPassword: true })
+ .then(this.loginsFound.bind(this))
+ .then(null, Cu.reportError);
+ },
+
+ onPageShow(event, window) {
+ this._detectInsecureFormLikes(window);
+ },
+
+ /**
+ * Maps all DOM content documents in this content process, including those in
+ * frames, to the current state used by the Login Manager.
+ */
+ loginFormStateByDocument: new WeakMap(),
+
+ /**
+ * Retrieves a reference to the state object associated with the given
+ * document. This is initialized to an object with default values.
+ */
+ stateForDocument(document) {
+ let loginFormState = this.loginFormStateByDocument.get(document);
+ if (!loginFormState) {
+ loginFormState = {
+ /**
+ * Keeps track of filled fields and values.
+ */
+ fillsByRootElement: new WeakMap(),
+ loginFormRootElements: new Set(),
+ };
+ this.loginFormStateByDocument.set(document, loginFormState);
+ }
+ return loginFormState;
+ },
+
+ /**
+ * Compute whether there is an insecure login form on any frame of the current page, and
+ * notify the parent process. This is used to control whether insecure password UI appears.
+ */
+ _detectInsecureFormLikes(topWindow) {
+ log("_detectInsecureFormLikes", topWindow.location.href);
+
+ // Returns true if this window or any subframes have insecure login forms.
+ let hasInsecureLoginForms = (thisWindow) => {
+ let doc = thisWindow.document;
+ let hasLoginForm = this.stateForDocument(doc).loginFormRootElements.size > 0;
+ // Ignores window.opener, because it's not relevant for indicating
+ // form security. See InsecurePasswordUtils docs for details.
+ return (hasLoginForm && !thisWindow.isSecureContextIfOpenerIgnored) ||
+ Array.some(thisWindow.frames,
+ frame => hasInsecureLoginForms(frame));
+ };
+
+ let messageManager = messageManagerFromWindow(topWindow);
+ messageManager.sendAsyncMessage("RemoteLogins:insecureLoginFormPresent", {
+ hasInsecureLoginForms: hasInsecureLoginForms(topWindow),
+ });
+ },
+
+ /**
+ * Perform a password fill upon user request coming from the parent process.
+ * The fill will be in the form previously identified during page navigation.
+ *
+ * @param An object with the following properties:
+ * {
+ * topDocument:
+ * DOM document currently associated to the the top-level window
+ * for which the fill is requested. This may be different from the
+ * document that originally caused the login UI to be displayed.
+ * loginFormOrigin:
+ * String with the origin for which the login UI was displayed.
+ * This must match the origin of the form used for the fill.
+ * loginsFound:
+ * Array containing the login to fill. While other messages may
+ * have more logins, for this use case this is expected to have
+ * exactly one element. The origin of the login may be different
+ * from the origin of the form used for the fill.
+ * recipes:
+ * Fill recipes transmitted together with the original message.
+ * inputElement:
+ * Username or password input element from the form we want to fill.
+ * }
+ */
+ fillForm({ topDocument, loginFormOrigin, loginsFound, recipes, inputElement }) {
+ if (!inputElement) {
+ log("fillForm: No input element specified");
+ return;
+ }
+ if (LoginUtils._getPasswordOrigin(topDocument.documentURI) != loginFormOrigin) {
+ if (!inputElement ||
+ LoginUtils._getPasswordOrigin(inputElement.ownerDocument.documentURI) != loginFormOrigin) {
+ log("fillForm: The requested origin doesn't match the one form the",
+ "document. This may mean we navigated to a document from a different",
+ "site before we had a chance to indicate this change in the user",
+ "interface.");
+ return;
+ }
+ }
+
+ let clobberUsername = true;
+ let options = {
+ inputElement,
+ };
+
+ let form = LoginFormFactory.createFromField(inputElement);
+ if (inputElement.type == "password") {
+ clobberUsername = false;
+ }
+ this._fillForm(form, true, clobberUsername, true, true, loginsFound, recipes, options);
+ },
+
+ loginsFound({ form, loginsFound, recipes }) {
+ let doc = form.ownerDocument;
+ let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
+
+ this._fillForm(form, autofillForm, false, false, false, loginsFound, recipes);
+ },
+
+ /**
+ * Focus event handler for username fields to decide whether to show autocomplete.
+ * @param {FocusEvent} event
+ */
+ _onUsernameFocus(event) {
+ let focusedField = event.target;
+ if (!focusedField.mozIsTextField(true) || focusedField.readOnly) {
+ return;
+ }
+
+ if (this._isLoginAlreadyFilled(focusedField)) {
+ log("_onUsernameFocus: Already filled");
+ return;
+ }
+
+ /*
+ * A `focus` event is fired before a `contextmenu` event if a user right-clicks into an
+ * unfocused field. In that case we don't want to show both autocomplete and a context menu
+ * overlapping so we spin the event loop to see if a `contextmenu` event is coming next. If no
+ * `contextmenu` event was seen and the focused field is still focused by the form fill
+ * controller then show the autocomplete popup.
+ */
+ let timestamp = Date.now();
+ setTimeout(function maybeOpenAutocompleteAfterFocus() {
+ // Even though the `focus` event happens first, its .timeStamp is greater in
+ // testing and I don't want to rely on that so the absolute value is used.
+ let timeDiff = Math.abs(gLastContextMenuEventTimeStamp - timestamp);
+ if (timeDiff < AUTOCOMPLETE_AFTER_CONTEXTMENU_THRESHOLD_MS) {
+ log("Not opening autocomplete after focus since a context menu was opened within",
+ timeDiff, "ms");
+ return;
+ }
+
+ if (this._formFillService.focusedInput == focusedField) {
+ log("maybeOpenAutocompleteAfterFocus: Opening the autocomplete popup. Time diff:", timeDiff);
+ this._formFillService.showPopup();
+ } else {
+ log("maybeOpenAutocompleteAfterFocus: FormFillController has a different focused input");
+ }
+ }.bind(this), 0);
+ },
+
+ /**
+ * Listens for DOMAutoComplete and blur events on an input field.
+ */
+ onUsernameInput(event) {
+ if (!event.isTrusted)
+ return;
+
+ if (!gEnabled)
+ return;
+
+ var acInputField = event.target;
+
+ // This is probably a bit over-conservatative.
+ if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument))
+ return;
+
+ if (!LoginHelper.isUsernameFieldType(acInputField))
+ return;
+
+ var acForm = LoginFormFactory.createFromField(acInputField);
+ if (!acForm)
+ return;
+
+ // If the username is blank, bail out now -- we don't want
+ // fillForm() to try filling in a login without a username
+ // to filter on (bug 471906).
+ if (!acInputField.value)
+ return;
+
+ log("onUsernameInput from", event.type);
+
+ let doc = acForm.ownerDocument;
+ let messageManager = messageManagerFromWindow(doc.defaultView);
+ let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+ formOrigin: LoginUtils._getPasswordOrigin(doc.documentURI),
+ })[0];
+
+ // Make sure the username field fillForm will use is the
+ // same field as the autocomplete was activated on.
+ var [usernameField, passwordField, ignored] =
+ this._getFormFields(acForm, false, recipes);
+ if (usernameField == acInputField && passwordField) {
+ this._getLoginDataFromParent(acForm, { showMasterPassword: false })
+ .then(({ form, loginsFound, recipes }) => {
+ this._fillForm(form, true, false, true, true, loginsFound, recipes);
+ })
+ .then(null, Cu.reportError);
+ } else {
+ // Ignore the event, it's for some input we don't care about.
+ }
+ },
+
+ /**
+ * @param {FormLike} form - the FormLike to look for password fields in.
+ * @param {bool} [skipEmptyFields=false] - Whether to ignore password fields with no value.
+ * Used at capture time since saving empty values isn't
+ * useful.
+ * @return {Array|null} Array of password field elements for the specified form.
+ * If no pw fields are found, or if more than 3 are found, then null
+ * is returned.
+ */
+ _getPasswordFields(form, skipEmptyFields = false) {
+ // Locate the password fields in the form.
+ let pwFields = [];
+ for (let i = 0; i < form.elements.length; i++) {
+ let element = form.elements[i];
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
+ element.type != "password") {
+ continue;
+ }
+
+ if (skipEmptyFields && !element.value.trim()) {
+ continue;
+ }
+
+ pwFields[pwFields.length] = {
+ index : i,
+ element : element
+ };
+ }
+
+ // If too few or too many fields, bail out.
+ if (pwFields.length == 0) {
+ log("(form ignored -- no password fields.)");
+ return null;
+ } else if (pwFields.length > 3) {
+ log("(form ignored -- too many password fields. [ got ", pwFields.length, "])");
+ return null;
+ }
+
+ return pwFields;
+ },
+
+ /**
+ * Returns the username and password fields found in the form.
+ * Can handle complex forms by trying to figure out what the
+ * relevant fields are.
+ *
+ * @param {FormLike} form
+ * @param {bool} isSubmission
+ * @param {Set} recipes
+ * @return {Array} [usernameField, newPasswordField, oldPasswordField]
+ *
+ * usernameField may be null.
+ * newPasswordField will always be non-null.
+ * oldPasswordField may be null. If null, newPasswordField is just
+ * "theLoginField". If not null, the form is apparently a
+ * change-password field, with oldPasswordField containing the password
+ * that is being changed.
+ *
+ * Note that even though we can create a FormLike from a text field,
+ * this method will only return a non-null usernameField if the
+ * FormLike has a password field.
+ */
+ _getFormFields(form, isSubmission, recipes) {
+ var usernameField = null;
+ var pwFields = null;
+ var fieldOverrideRecipe = LoginRecipesContent.getFieldOverrides(recipes, form);
+ if (fieldOverrideRecipe) {
+ var pwOverrideField = LoginRecipesContent.queryLoginField(
+ form,
+ fieldOverrideRecipe.passwordSelector
+ );
+ if (pwOverrideField) {
+ // The field from the password override may be in a different FormLike.
+ let formLike = LoginFormFactory.createFromField(pwOverrideField);
+ pwFields = [{
+ index : [...formLike.elements].indexOf(pwOverrideField),
+ element : pwOverrideField,
+ }];
+ }
+
+ var usernameOverrideField = LoginRecipesContent.queryLoginField(
+ form,
+ fieldOverrideRecipe.usernameSelector
+ );
+ if (usernameOverrideField) {
+ usernameField = usernameOverrideField;
+ }
+ }
+
+ if (!pwFields) {
+ // Locate the password field(s) in the form. Up to 3 supported.
+ // If there's no password field, there's nothing for us to do.
+ pwFields = this._getPasswordFields(form, isSubmission);
+ }
+
+ if (!pwFields) {
+ return [null, null, null];
+ }
+
+ if (!usernameField) {
+ // Locate the username field in the form by searching backwards
+ // from the first password field, assume the first text field is the
+ // username. We might not find a username field if the user is
+ // already logged in to the site.
+ for (var i = pwFields[0].index - 1; i >= 0; i--) {
+ var element = form.elements[i];
+ if (!LoginHelper.isUsernameFieldType(element)) {
+ continue;
+ }
+
+ if (fieldOverrideRecipe && fieldOverrideRecipe.notUsernameSelector &&
+ element.matches(fieldOverrideRecipe.notUsernameSelector)) {
+ continue;
+ }
+
+ usernameField = element;
+ break;
+ }
+ }
+
+ if (!usernameField)
+ log("(form -- no username field found)");
+ else
+ log("Username field ", usernameField, "has name/value:",
+ usernameField.name, "/", usernameField.value);
+
+ // If we're not submitting a form (it's a page load), there are no
+ // password field values for us to use for identifying fields. So,
+ // just assume the first password field is the one to be filled in.
+ if (!isSubmission || pwFields.length == 1) {
+ var passwordField = pwFields[0].element;
+ log("Password field", passwordField, "has name: ", passwordField.name);
+ return [usernameField, passwordField, null];
+ }
+
+
+ // Try to figure out WTF is in the form based on the password values.
+ var oldPasswordField, newPasswordField;
+ var pw1 = pwFields[0].element.value;
+ var pw2 = pwFields[1].element.value;
+ var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
+
+ if (pwFields.length == 3) {
+ // Look for two identical passwords, that's the new password
+
+ if (pw1 == pw2 && pw2 == pw3) {
+ // All 3 passwords the same? Weird! Treat as if 1 pw field.
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = null;
+ } else if (pw1 == pw2) {
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = pwFields[2].element;
+ } else if (pw2 == pw3) {
+ oldPasswordField = pwFields[0].element;
+ newPasswordField = pwFields[2].element;
+ } else if (pw1 == pw3) {
+ // A bit odd, but could make sense with the right page layout.
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = pwFields[1].element;
+ } else {
+ // We can't tell which of the 3 passwords should be saved.
+ log("(form ignored -- all 3 pw fields differ)");
+ return [null, null, null];
+ }
+ } else if (pw1 == pw2) {
+ // pwFields.length == 2
+ // Treat as if 1 pw field
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = null;
+ } else {
+ // Just assume that the 2nd password is the new password
+ oldPasswordField = pwFields[0].element;
+ newPasswordField = pwFields[1].element;
+ }
+
+ log("Password field (new) id/name is: ", newPasswordField.id, " / ", newPasswordField.name);
+ if (oldPasswordField) {
+ log("Password field (old) id/name is: ", oldPasswordField.id, " / ", oldPasswordField.name);
+ } else {
+ log("Password field (old):", oldPasswordField);
+ }
+ return [usernameField, newPasswordField, oldPasswordField];
+ },
+
+
+ /**
+ * @return true if the page requests autocomplete be disabled for the
+ * specified element.
+ */
+ _isAutocompleteDisabled(element) {
+ return element && element.autocomplete == "off";
+ },
+
+ /**
+ * Trigger capture on any relevant FormLikes due to a navigation alone (not
+ * necessarily due to an actual form submission). This method is used to
+ * capture logins for cases where form submit events are not used.
+ *
+ * To avoid multiple notifications for the same FormLike, this currently
+ * avoids capturing when dealing with a real <form> which are ideally already
+ * using a submit event.
+ *
+ * @param {Document} document being navigated
+ */
+ _onNavigation(aDocument) {
+ let state = this.stateForDocument(aDocument);
+ let loginFormRootElements = state.loginFormRootElements;
+ log("_onNavigation: state:", state, "loginFormRootElements size:", loginFormRootElements.size,
+ "document:", aDocument);
+
+ for (let formRoot of state.loginFormRootElements) {
+ if (formRoot instanceof Ci.nsIDOMHTMLFormElement) {
+ // For now only perform capture upon navigation for FormLike's without
+ // a <form> to avoid capture from both an earlyformsubmit and
+ // navigation for the same "form".
+ log("Ignoring navigation for the form root to avoid multiple prompts " +
+ "since it was for a real <form>");
+ continue;
+ }
+ let formLike = this._formLikeByRootElement.get(formRoot);
+ this._onFormSubmit(formLike);
+ }
+ },
+
+ /**
+ * Called by our observer when notified of a form submission.
+ * [Note that this happens before any DOM onsubmit handlers are invoked.]
+ * Looks for a password change in the submitted form, so we can update
+ * our stored password.
+ *
+ * @param {FormLike} form
+ */
+ _onFormSubmit(form) {
+ log("_onFormSubmit", form);
+ var doc = form.ownerDocument;
+ var win = doc.defaultView;
+
+ if (PrivateBrowsingUtils.isContentWindowPrivate(win)) {
+ // We won't do anything in private browsing mode anyway,
+ // so there's no need to perform further checks.
+ log("(form submission ignored in private browsing mode)");
+ return;
+ }
+
+ // If password saving is disabled (globally or for host), bail out now.
+ if (!gEnabled)
+ return;
+
+ var hostname = LoginUtils._getPasswordOrigin(doc.documentURI);
+ if (!hostname) {
+ log("(form submission ignored -- invalid hostname)");
+ return;
+ }
+
+ let formSubmitURL = LoginUtils._getActionOrigin(form);
+ let messageManager = messageManagerFromWindow(win);
+
+ let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+ formOrigin: hostname,
+ })[0];
+
+ // Get the appropriate fields from the form.
+ var [usernameField, newPasswordField, oldPasswordField] =
+ this._getFormFields(form, true, recipes);
+
+ // Need at least 1 valid password field to do anything.
+ if (newPasswordField == null)
+ return;
+
+ // Check for autocomplete=off attribute. We don't use it to prevent
+ // autofilling (for existing logins), but won't save logins when it's
+ // present and the storeWhenAutocompleteOff pref is false.
+ // XXX spin out a bug that we don't update timeLastUsed in this case?
+ if ((this._isAutocompleteDisabled(form) ||
+ this._isAutocompleteDisabled(usernameField) ||
+ this._isAutocompleteDisabled(newPasswordField) ||
+ this._isAutocompleteDisabled(oldPasswordField)) &&
+ !gStoreWhenAutocompleteOff) {
+ log("(form submission ignored -- autocomplete=off found)");
+ return;
+ }
+
+ // Don't try to send DOM nodes over IPC.
+ let mockUsername = usernameField ?
+ { name: usernameField.name,
+ value: usernameField.value } :
+ null;
+ let mockPassword = { name: newPasswordField.name,
+ value: newPasswordField.value };
+ let mockOldPassword = oldPasswordField ?
+ { name: oldPasswordField.name,
+ value: oldPasswordField.value } :
+ null;
+
+ // Make sure to pass the opener's top in case it was in a frame.
+ let openerTopWindow = win.opener ? win.opener.top : null;
+
+ messageManager.sendAsyncMessage("RemoteLogins:onFormSubmit",
+ { hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ usernameField: mockUsername,
+ newPasswordField: mockPassword,
+ oldPasswordField: mockOldPassword },
+ { openerTopWindow });
+ },
+
+ /**
+ * Attempt to find the username and password fields in a form, and fill them
+ * in using the provided logins and recipes.
+ *
+ * @param {LoginForm} form
+ * @param {bool} autofillForm denotes if we should fill the form in automatically
+ * @param {bool} clobberUsername controls if an existing username can be overwritten.
+ * If this is false and an inputElement of type password
+ * is also passed, the username field will be ignored.
+ * If this is false and no inputElement is passed, if the username
+ * field value is not found in foundLogins, it will not fill the password.
+ * @param {bool} clobberPassword controls if an existing password value can be
+ * overwritten
+ * @param {bool} userTriggered is an indication of whether this filling was triggered by
+ * the user
+ * @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
+ * @param {Set} recipes that could be used to affect how the form is filled
+ * @param {Object} [options = {}] is a list of options for this method.
+ - [inputElement] is an optional target input element we want to fill
+ */
+ _fillForm(form, autofillForm, clobberUsername, clobberPassword,
+ userTriggered, foundLogins, recipes, {inputElement} = {}) {
+ if (form instanceof Ci.nsIDOMHTMLFormElement) {
+ throw new Error("_fillForm should only be called with FormLike objects");
+ }
+
+ log("_fillForm", form.elements);
+ let ignoreAutocomplete = true;
+ // Will be set to one of AUTOFILL_RESULT in the `try` block.
+ let autofillResult = -1;
+ const AUTOFILL_RESULT = {
+ FILLED: 0,
+ NO_PASSWORD_FIELD: 1,
+ PASSWORD_DISABLED_READONLY: 2,
+ NO_LOGINS_FIT: 3,
+ NO_SAVED_LOGINS: 4,
+ EXISTING_PASSWORD: 5,
+ EXISTING_USERNAME: 6,
+ MULTIPLE_LOGINS: 7,
+ NO_AUTOFILL_FORMS: 8,
+ AUTOCOMPLETE_OFF: 9,
+ INSECURE: 10,
+ };
+
+ try {
+ // Nothing to do if we have no matching logins available,
+ // and there isn't a need to show the insecure form warning.
+ if (foundLogins.length == 0 &&
+ (InsecurePasswordUtils.isFormSecure(form) ||
+ !LoginHelper.showInsecureFieldWarning)) {
+ // We don't log() here since this is a very common case.
+ autofillResult = AUTOFILL_RESULT.NO_SAVED_LOGINS;
+ return;
+ }
+
+ // Heuristically determine what the user/pass fields are
+ // We do this before checking to see if logins are stored,
+ // so that the user isn't prompted for a master password
+ // without need.
+ var [usernameField, passwordField, ignored] =
+ this._getFormFields(form, false, recipes);
+
+ // If we have a password inputElement parameter and it's not
+ // the same as the one heuristically found, use the parameter
+ // one instead.
+ if (inputElement) {
+ if (inputElement.type == "password") {
+ passwordField = inputElement;
+ if (!clobberUsername) {
+ usernameField = null;
+ }
+ } else if (LoginHelper.isUsernameFieldType(inputElement)) {
+ usernameField = inputElement;
+ } else {
+ throw new Error("Unexpected input element type.");
+ }
+ }
+
+ // Need a valid password field to do anything.
+ if (passwordField == null) {
+ log("not filling form, no password field found");
+ autofillResult = AUTOFILL_RESULT.NO_PASSWORD_FIELD;
+ return;
+ }
+
+ // If the password field is disabled or read-only, there's nothing to do.
+ if (passwordField.disabled || passwordField.readOnly) {
+ log("not filling form, password field disabled or read-only");
+ autofillResult = AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY;
+ return;
+ }
+
+ // Attach autocomplete stuff to the username field, if we have
+ // one. This is normally used to select from multiple accounts,
+ // but even with one account we should refill if the user edits.
+ // We would also need this attached to show the insecure login
+ // warning, regardless of saved login.
+ if (usernameField) {
+ this._formFillService.markAsLoginManagerField(usernameField);
+ }
+
+ // Nothing to do if we have no matching logins available.
+ // Only insecure pages reach this block and logs the same
+ // telemetry flag.
+ if (foundLogins.length == 0) {
+ // We don't log() here since this is a very common case.
+ autofillResult = AUTOFILL_RESULT.NO_SAVED_LOGINS;
+ return;
+ }
+
+ // Prevent autofilling insecure forms.
+ if (!userTriggered && !LoginHelper.insecureAutofill &&
+ !InsecurePasswordUtils.isFormSecure(form)) {
+ log("not filling form since it's insecure");
+ autofillResult = AUTOFILL_RESULT.INSECURE;
+ return;
+ }
+
+ var isAutocompleteOff = false;
+ if (this._isAutocompleteDisabled(form) ||
+ this._isAutocompleteDisabled(usernameField) ||
+ this._isAutocompleteDisabled(passwordField)) {
+ isAutocompleteOff = true;
+ }
+
+ // Discard logins which have username/password values that don't
+ // fit into the fields (as specified by the maxlength attribute).
+ // The user couldn't enter these values anyway, and it helps
+ // with sites that have an extra PIN to be entered (bug 391514)
+ var maxUsernameLen = Number.MAX_VALUE;
+ var maxPasswordLen = Number.MAX_VALUE;
+
+ // If attribute wasn't set, default is -1.
+ if (usernameField && usernameField.maxLength >= 0)
+ maxUsernameLen = usernameField.maxLength;
+ if (passwordField.maxLength >= 0)
+ maxPasswordLen = passwordField.maxLength;
+
+ var logins = foundLogins.filter(function (l) {
+ var fit = (l.username.length <= maxUsernameLen &&
+ l.password.length <= maxPasswordLen);
+ if (!fit)
+ log("Ignored", l.username, "login: won't fit");
+
+ return fit;
+ }, this);
+
+ if (logins.length == 0) {
+ log("form not filled, none of the logins fit in the field");
+ autofillResult = AUTOFILL_RESULT.NO_LOGINS_FIT;
+ return;
+ }
+
+ // Don't clobber an existing password.
+ if (passwordField.value && !clobberPassword) {
+ log("form not filled, the password field was already filled");
+ autofillResult = AUTOFILL_RESULT.EXISTING_PASSWORD;
+ return;
+ }
+
+ // Select a login to use for filling in the form.
+ var selectedLogin;
+ if (!clobberUsername && usernameField && (usernameField.value ||
+ usernameField.disabled ||
+ usernameField.readOnly)) {
+ // If username was specified in the field, it's disabled or it's readOnly, only fill in the
+ // password if we find a matching login.
+ var username = usernameField.value.toLowerCase();
+
+ let matchingLogins = logins.filter(l =>
+ l.username.toLowerCase() == username);
+ if (matchingLogins.length == 0) {
+ log("Password not filled. None of the stored logins match the username already present.");
+ autofillResult = AUTOFILL_RESULT.EXISTING_USERNAME;
+ return;
+ }
+
+ // If there are multiple, and one matches case, use it
+ for (let l of matchingLogins) {
+ if (l.username == usernameField.value) {
+ selectedLogin = l;
+ }
+ }
+ // Otherwise just use the first
+ if (!selectedLogin) {
+ selectedLogin = matchingLogins[0];
+ }
+ } else if (logins.length == 1) {
+ selectedLogin = logins[0];
+ } else {
+ // We have multiple logins. Handle a special case here, for sites
+ // which have a normal user+pass login *and* a password-only login
+ // (eg, a PIN). Prefer the login that matches the type of the form
+ // (user+pass or pass-only) when there's exactly one that matches.
+ let matchingLogins;
+ if (usernameField)
+ matchingLogins = logins.filter(l => l.username);
+ else
+ matchingLogins = logins.filter(l => !l.username);
+
+ if (matchingLogins.length != 1) {
+ log("Multiple logins for form, so not filling any.");
+ autofillResult = AUTOFILL_RESULT.MULTIPLE_LOGINS;
+ return;
+ }
+
+ selectedLogin = matchingLogins[0];
+ }
+
+ // We will always have a selectedLogin at this point.
+
+ if (!autofillForm) {
+ log("autofillForms=false but form can be filled");
+ autofillResult = AUTOFILL_RESULT.NO_AUTOFILL_FORMS;
+ return;
+ }
+
+ if (isAutocompleteOff && !ignoreAutocomplete) {
+ log("Not filling the login because we're respecting autocomplete=off");
+ autofillResult = AUTOFILL_RESULT.AUTOCOMPLETE_OFF;
+ return;
+ }
+
+ // Fill the form
+
+ if (usernameField) {
+ // Don't modify the username field if it's disabled or readOnly so we preserve its case.
+ let disabledOrReadOnly = usernameField.disabled || usernameField.readOnly;
+
+ let userNameDiffers = selectedLogin.username != usernameField.value;
+ // Don't replace the username if it differs only in case, and the user triggered
+ // this autocomplete. We assume that if it was user-triggered the entered text
+ // is desired.
+ let userEnteredDifferentCase = userTriggered && userNameDiffers &&
+ usernameField.value.toLowerCase() == selectedLogin.username.toLowerCase();
+
+ if (!disabledOrReadOnly && !userEnteredDifferentCase && userNameDiffers) {
+ usernameField.setUserInput(selectedLogin.username);
+ }
+ }
+
+ let doc = form.ownerDocument;
+ if (passwordField.value != selectedLogin.password) {
+ passwordField.setUserInput(selectedLogin.password);
+ let autoFilledLogin = {
+ guid: selectedLogin.QueryInterface(Ci.nsILoginMetaInfo).guid,
+ username: selectedLogin.username,
+ usernameField: usernameField ? Cu.getWeakReference(usernameField) : null,
+ password: selectedLogin.password,
+ passwordField: Cu.getWeakReference(passwordField),
+ };
+ log("Saving autoFilledLogin", autoFilledLogin.guid, "for", form.rootElement);
+ this.stateForDocument(doc).fillsByRootElement.set(form.rootElement, autoFilledLogin);
+ }
+
+ log("_fillForm succeeded");
+ autofillResult = AUTOFILL_RESULT.FILLED;
+
+ let win = doc.defaultView;
+ let messageManager = messageManagerFromWindow(win);
+ messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
+ } finally {
+ if (autofillResult == -1) {
+ // eslint-disable-next-line no-unsafe-finally
+ throw new Error("_fillForm: autofillResult must be specified");
+ }
+
+ if (!userTriggered) {
+ // Ignore fills as a result of user action for this probe.
+ Services.telemetry.getHistogramById("PWMGR_FORM_AUTOFILL_RESULT").add(autofillResult);
+
+ if (usernameField) {
+ let focusedElement = this._formFillService.focusedInput;
+ if (usernameField == focusedElement &&
+ autofillResult !== AUTOFILL_RESULT.FILLED) {
+ log("_fillForm: Opening username autocomplete popup since the form wasn't autofilled");
+ this._formFillService.showPopup();
+ }
+ }
+ }
+
+ if (usernameField) {
+ log("_fillForm: Attaching event listeners to usernameField");
+ usernameField.addEventListener("focus", observer);
+ usernameField.addEventListener("contextmenu", observer);
+ }
+
+ Services.obs.notifyObservers(form.rootElement, "passwordmgr-processed-form", null);
+ }
+ },
+
+ /**
+ * Given a field, determine whether that field was last filled as a username
+ * field AND whether the username is still filled in with the username AND
+ * whether the associated password field has the matching password.
+ *
+ * @note This could possibly be unified with getFieldContext but they have
+ * slightly different use cases. getFieldContext looks up recipes whereas this
+ * method doesn't need to since it's only returning a boolean based upon the
+ * recipes used for the last fill (in _fillForm).
+ *
+ * @param {HTMLInputElement} aUsernameField element contained in a FormLike
+ * cached in _formLikeByRootElement.
+ * @returns {Boolean} whether the username and password fields still have the
+ * last-filled values, if previously filled.
+ */
+ _isLoginAlreadyFilled(aUsernameField) {
+ let formLikeRoot = FormLikeFactory.findRootForField(aUsernameField);
+ // Look for the existing FormLike.
+ let existingFormLike = this._formLikeByRootElement.get(formLikeRoot);
+ if (!existingFormLike) {
+ throw new Error("_isLoginAlreadyFilled called with a username field with " +
+ "no rootElement FormLike");
+ }
+
+ log("_isLoginAlreadyFilled: existingFormLike", existingFormLike);
+ let filledLogin = this.stateForDocument(aUsernameField.ownerDocument).fillsByRootElement.get(formLikeRoot);
+ if (!filledLogin) {
+ return false;
+ }
+
+ // Unpack the weak references.
+ let autoFilledUsernameField = filledLogin.usernameField ? filledLogin.usernameField.get() : null;
+ let autoFilledPasswordField = filledLogin.passwordField.get();
+
+ // Check username and password values match what was filled.
+ if (!autoFilledUsernameField ||
+ autoFilledUsernameField != aUsernameField ||
+ autoFilledUsernameField.value != filledLogin.username ||
+ !autoFilledPasswordField ||
+ autoFilledPasswordField.value != filledLogin.password) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Verify if a field is a valid login form field and
+ * returns some information about it's FormLike.
+ *
+ * @param {Element} aField
+ * A form field we want to verify.
+ *
+ * @returns {Object} an object with information about the
+ * FormLike username and password field
+ * or null if the passed field is invalid.
+ */
+ getFieldContext(aField) {
+ // If the element is not a proper form field, return null.
+ if (!(aField instanceof Ci.nsIDOMHTMLInputElement) ||
+ (aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
+ !aField.ownerDocument) {
+ return null;
+ }
+ let form = LoginFormFactory.createFromField(aField);
+
+ let doc = aField.ownerDocument;
+ let messageManager = messageManagerFromWindow(doc.defaultView);
+ let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+ formOrigin: LoginUtils._getPasswordOrigin(doc.documentURI),
+ })[0];
+
+ let [usernameField, newPasswordField] =
+ this._getFormFields(form, false, recipes);
+
+ // If we are not verifying a password field, we want
+ // to use aField as the username field.
+ if (aField.type != "password") {
+ usernameField = aField;
+ }
+
+ return {
+ usernameField: {
+ found: !!usernameField,
+ disabled: usernameField && (usernameField.disabled || usernameField.readOnly),
+ },
+ passwordField: {
+ found: !!newPasswordField,
+ disabled: newPasswordField && (newPasswordField.disabled || newPasswordField.readOnly),
+ },
+ };
+ },
+};
+
+var LoginUtils = {
+ /**
+ * Get the parts of the URL we want for identification.
+ * Strip out things like the userPass portion
+ */
+ _getPasswordOrigin(uriString, allowJS) {
+ var realm = "";
+ try {
+ var uri = Services.io.newURI(uriString, null, null);
+
+ if (allowJS && uri.scheme == "javascript")
+ return "javascript:";
+
+ // Build this manually instead of using prePath to avoid including the userPass portion.
+ realm = uri.scheme + "://" + uri.hostPort;
+ } catch (e) {
+ // bug 159484 - disallow url types that don't support a hostPort.
+ // (although we handle "javascript:..." as a special case above.)
+ log("Couldn't parse origin for", uriString, e);
+ realm = null;
+ }
+
+ return realm;
+ },
+
+ _getActionOrigin(form) {
+ var uriString = form.action;
+
+ // A blank or missing action submits to where it came from.
+ if (uriString == "")
+ uriString = form.baseURI; // ala bug 297761
+
+ return this._getPasswordOrigin(uriString, true);
+ },
+};
+
+// nsIAutoCompleteResult implementation
+function UserAutoCompleteResult(aSearchString, matchingLogins, {isSecure, messageManager, isPasswordField}) {
+ function loginSort(a, b) {
+ var userA = a.username.toLowerCase();
+ var userB = b.username.toLowerCase();
+
+ if (userA < userB)
+ return -1;
+
+ if (userA > userB)
+ return 1;
+
+ return 0;
+ }
+
+ function findDuplicates(loginList) {
+ let seen = new Set();
+ let duplicates = new Set();
+ for (let login of loginList) {
+ if (seen.has(login.username)) {
+ duplicates.add(login.username);
+ }
+ seen.add(login.username);
+ }
+ return duplicates;
+ }
+
+ this._showInsecureFieldWarning = (!isSecure && LoginHelper.showInsecureFieldWarning) ? 1 : 0;
+ this.searchString = aSearchString;
+ this.logins = matchingLogins.sort(loginSort);
+ this.matchCount = matchingLogins.length + this._showInsecureFieldWarning;
+ this._messageManager = messageManager;
+ this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+ this._dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+
+ this._isPasswordField = isPasswordField;
+
+ this._duplicateUsernames = findDuplicates(matchingLogins);
+
+ if (this.matchCount > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ this.defaultIndex = 0;
+ }
+}
+
+UserAutoCompleteResult.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
+ Ci.nsISupportsWeakReference]),
+
+ // private
+ logins : null,
+
+ // Allow autoCompleteSearch to get at the JS object so it can
+ // modify some readonly properties for internal use.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Interfaces from idl...
+ searchString : null,
+ searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+ defaultIndex : -1,
+ errorDescription : "",
+ matchCount : 0,
+
+ getValueAt(index) {
+ if (index < 0 || index >= this.matchCount) {
+ throw new Error("Index out of range.");
+ }
+
+ if (this._showInsecureFieldWarning && index === 0) {
+ return "";
+ }
+
+ let selectedLogin = this.logins[index - this._showInsecureFieldWarning];
+
+ return this._isPasswordField ? selectedLogin.password : selectedLogin.username;
+ },
+
+ getLabelAt(index) {
+ if (index < 0 || index >= this.matchCount) {
+ throw new Error("Index out of range.");
+ }
+
+ if (this._showInsecureFieldWarning && index === 0) {
+ return this._stringBundle.GetStringFromName("insecureFieldWarningDescription") + " " +
+ this._stringBundle.GetStringFromName("insecureFieldWarningLearnMore");
+ }
+
+ let that = this;
+
+ function getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return that._stringBundle.formatStringFromName(key, formatArgs, formatArgs.length);
+ }
+ return that._stringBundle.GetStringFromName(key);
+ }
+
+ let login = this.logins[index - this._showInsecureFieldWarning];
+ let username = login.username;
+ // If login is empty or duplicated we want to append a modification date to it.
+ if (!username || this._duplicateUsernames.has(username)) {
+ if (!username) {
+ username = getLocalizedString("noUsername");
+ }
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ let time = this._dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+ username = getLocalizedString("loginHostAge", [username, time]);
+ }
+
+ return username;
+ },
+
+ getCommentAt(index) {
+ return "";
+ },
+
+ getStyleAt(index) {
+ if (index == 0 && this._showInsecureFieldWarning) {
+ return "insecureWarning";
+ }
+
+ return "login";
+ },
+
+ getImageAt(index) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(index) {
+ return this.getValueAt(index);
+ },
+
+ removeValueAt(index, removeFromDB) {
+ if (index < 0 || index >= this.matchCount) {
+ throw new Error("Index out of range.");
+ }
+
+ if (this._showInsecureFieldWarning && index === 0) {
+ // Ignore the warning message item.
+ return;
+ }
+ if (this._showInsecureFieldWarning) {
+ index--;
+ }
+
+ var [removedLogin] = this.logins.splice(index, 1);
+
+ this.matchCount--;
+ if (this.defaultIndex > this.logins.length)
+ this.defaultIndex--;
+
+ if (removeFromDB) {
+ if (this._messageManager) {
+ let vanilla = LoginHelper.loginToVanillaObject(removedLogin);
+ this._messageManager.sendAsyncMessage("RemoteLogins:removeLogin",
+ { login: vanilla });
+ } else {
+ Services.logins.removeLogin(removedLogin);
+ }
+ }
+ }
+};
+
+/**
+ * A factory to generate FormLike objects that represent a set of login fields
+ * which aren't necessarily marked up with a <form> element.
+ */
+var LoginFormFactory = {
+ /**
+ * Create a LoginForm object from a <form>.
+ *
+ * @param {HTMLFormElement} aForm
+ * @return {LoginForm}
+ * @throws Error if aForm isn't an HTMLFormElement
+ */
+ createFromForm(aForm) {
+ let formLike = FormLikeFactory.createFromForm(aForm);
+ formLike.action = LoginUtils._getActionOrigin(aForm);
+
+ let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
+ state.loginFormRootElements.add(formLike.rootElement);
+ log("adding", formLike.rootElement, "to loginFormRootElements for", formLike.ownerDocument);
+
+ LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
+ return formLike;
+ },
+
+ /**
+ * Create a LoginForm object from a password or username field.
+ *
+ * If the field is in a <form>, construct the LoginForm from the form.
+ * Otherwise, create a LoginForm with a rootElement (wrapper) according to
+ * heuristics. Currently all <input> not in a <form> are one LoginForm but this
+ * shouldn't be relied upon as the heuristics may change to detect multiple
+ * "forms" (e.g. registration and login) on one page with a <form>.
+ *
+ * Note that two LoginForms created from the same field won't return the same LoginForm object.
+ * Use the `rootElement` property on the LoginForm as a key instead.
+ *
+ * @param {HTMLInputElement} aField - a password or username field in a document
+ * @return {LoginForm}
+ * @throws Error if aField isn't a password or username field in a document
+ */
+ createFromField(aField) {
+ if (!(aField instanceof Ci.nsIDOMHTMLInputElement) ||
+ (aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
+ !aField.ownerDocument) {
+ throw new Error("createFromField requires a password or username field in a document");
+ }
+
+ if (aField.form) {
+ return this.createFromForm(aField.form);
+ }
+
+ let formLike = FormLikeFactory.createFromField(aField);
+ formLike.action = LoginUtils._getPasswordOrigin(aField.ownerDocument.baseURI);
+ log("Created non-form FormLike for rootElement:", aField.ownerDocument.documentElement);
+
+ let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
+ state.loginFormRootElements.add(formLike.rootElement);
+ log("adding", formLike.rootElement, "to loginFormRootElements for", formLike.ownerDocument);
+
+
+ LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
+
+ return formLike;
+ },
+};
diff --git a/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm b/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
new file mode 100644
index 0000000000..5c88687bf1
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["LoginManagerContextMenu"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+ "resource://gre/modules/LoginManagerParent.jsm");
+
+/*
+ * Password manager object for the browser contextual menu.
+ */
+var LoginManagerContextMenu = {
+ /**
+ * Look for login items and add them to the contextual menu.
+ *
+ * @param {HTMLInputElement} inputElement
+ * The target input element of the context menu click.
+ * @param {xul:browser} browser
+ * The browser for the document the context menu was open on.
+ * @param {nsIURI} documentURI
+ * The URI of the document that the context menu was activated from.
+ * This isn't the same as the browser's top-level document URI
+ * when subframes are involved.
+ * @returns {DocumentFragment} a document fragment with all the login items.
+ */
+ addLoginsToMenu(inputElement, browser, documentURI) {
+ let foundLogins = this._findLogins(documentURI);
+
+ if (!foundLogins.length) {
+ return null;
+ }
+
+ let fragment = browser.ownerDocument.createDocumentFragment();
+ let duplicateUsernames = this._findDuplicates(foundLogins);
+ for (let login of foundLogins) {
+ let item = fragment.ownerDocument.createElement("menuitem");
+
+ let username = login.username;
+ // If login is empty or duplicated we want to append a modification date to it.
+ if (!username || duplicateUsernames.has(username)) {
+ if (!username) {
+ username = this._getLocalizedString("noUsername");
+ }
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ let time = this.dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+ username = this._getLocalizedString("loginHostAge", [username, time]);
+ }
+ item.setAttribute("label", username);
+ item.setAttribute("class", "context-login-item");
+
+ // login is bound so we can keep the reference to each object.
+ item.addEventListener("command", function(login, event) {
+ this._fillTargetField(login, inputElement, browser, documentURI);
+ }.bind(this, login));
+
+ fragment.appendChild(item);
+ }
+
+ return fragment;
+ },
+
+ /**
+ * Undoes the work of addLoginsToMenu for the same menu.
+ *
+ * @param {Document}
+ * The context menu owner document.
+ */
+ clearLoginsFromMenu(document) {
+ let loginItems = document.getElementsByClassName("context-login-item");
+ while (loginItems.item(0)) {
+ loginItems.item(0).remove();
+ }
+ },
+
+ /**
+ * Find logins for the current URI.
+ *
+ * @param {nsIURI} documentURI
+ * URI object with the hostname of the logins we want to find.
+ * This isn't the same as the browser's top-level document URI
+ * when subframes are involved.
+ *
+ * @returns {nsILoginInfo[]} a login list
+ */
+ _findLogins(documentURI) {
+ let searchParams = {
+ hostname: documentURI.prePath,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ };
+ let logins = LoginHelper.searchLoginsWithObject(searchParams);
+ let resolveBy = [
+ "scheme",
+ "timePasswordChanged",
+ ];
+ logins = LoginHelper.dedupeLogins(logins, ["username", "password"], resolveBy, documentURI.prePath);
+
+ // Sort logins in alphabetical order and by date.
+ logins.sort((loginA, loginB) => {
+ // Sort alphabetically
+ let result = loginA.username.localeCompare(loginB.username);
+ if (result) {
+ // Forces empty logins to be at the end
+ if (!loginA.username) {
+ return 1;
+ }
+ if (!loginB.username) {
+ return -1;
+ }
+ return result;
+ }
+
+ // Same username logins are sorted by last change date
+ let metaA = loginA.QueryInterface(Ci.nsILoginMetaInfo);
+ let metaB = loginB.QueryInterface(Ci.nsILoginMetaInfo);
+ return metaB.timePasswordChanged - metaA.timePasswordChanged;
+ });
+
+ return logins;
+ },
+
+ /**
+ * Find duplicate usernames in a login list.
+ *
+ * @param {nsILoginInfo[]} loginList
+ * A list of logins we want to look for duplicate usernames.
+ *
+ * @returns {Set} a set with the duplicate usernames.
+ */
+ _findDuplicates(loginList) {
+ let seen = new Set();
+ let duplicates = new Set();
+ for (let login of loginList) {
+ if (seen.has(login.username)) {
+ duplicates.add(login.username);
+ }
+ seen.add(login.username);
+ }
+ return duplicates;
+ },
+
+ /**
+ * @param {nsILoginInfo} login
+ * The login we want to fill the form with.
+ * @param {Element} inputElement
+ * The target input element we want to fill.
+ * @param {xul:browser} browser
+ * The target tab browser.
+ * @param {nsIURI} documentURI
+ * URI of the document owning the form we want to fill.
+ * This isn't the same as the browser's top-level
+ * document URI when subframes are involved.
+ */
+ _fillTargetField(login, inputElement, browser, documentURI) {
+ LoginManagerParent.fillForm({
+ browser: browser,
+ loginFormOrigin: documentURI.prePath,
+ login: login,
+ inputElement: inputElement,
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * @param {string} key
+ * The localized string key
+ * @param {string[]} formatArgs
+ * An array of formatting argument string
+ *
+ * @returns {string} the localized string for the specified key,
+ * formatted with arguments if required.
+ */
+ _getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return this._stringBundle.formatStringFromName(key, formatArgs, formatArgs.length);
+ }
+ return this._stringBundle.GetStringFromName(key);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(LoginManagerContextMenu, "_stringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+XPCOMUtils.defineLazyGetter(LoginManagerContextMenu, "dateAndTimeFormatter", function() {
+ return new Intl.DateTimeFormat(undefined, {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ });
+});
diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm
new file mode 100644
index 0000000000..e472fb61c7
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -0,0 +1,511 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UserAutoCompleteResult",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AutoCompletePopup",
+ "resource://gre/modules/AutoCompletePopup.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("LoginManagerParent");
+ return logger.log.bind(logger);
+});
+
+this.EXPORTED_SYMBOLS = [ "LoginManagerParent" ];
+
+var LoginManagerParent = {
+ /**
+ * Reference to the default LoginRecipesParent (instead of the initialization promise) for
+ * synchronous access. This is a temporary hack and new consumers should yield on
+ * recipeParentPromise instead.
+ *
+ * @type LoginRecipesParent
+ * @deprecated
+ */
+ _recipeManager: null,
+
+ // Tracks the last time the user cancelled the master password prompt,
+ // to avoid spamming master password prompts on autocomplete searches.
+ _lastMPLoginCancelled: Math.NEGATIVE_INFINITY,
+
+ _searchAndDedupeLogins: function (formOrigin, actionOrigin) {
+ let logins;
+ try {
+ logins = LoginHelper.searchLoginsWithObject({
+ hostname: formOrigin,
+ formSubmitURL: actionOrigin,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+ } catch (e) {
+ // Record the last time the user cancelled the MP prompt
+ // to avoid spamming them with MP prompts for autocomplete.
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ log("User cancelled master password prompt.");
+ this._lastMPLoginCancelled = Date.now();
+ return [];
+ }
+ throw e;
+ }
+
+ // Dedupe so the length checks below still make sense with scheme upgrades.
+ let resolveBy = [
+ "scheme",
+ "timePasswordChanged",
+ ];
+ return LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin);
+ },
+
+ init: function() {
+ let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+ mm.addMessageListener("RemoteLogins:findLogins", this);
+ mm.addMessageListener("RemoteLogins:findRecipes", this);
+ mm.addMessageListener("RemoteLogins:onFormSubmit", this);
+ mm.addMessageListener("RemoteLogins:autoCompleteLogins", this);
+ mm.addMessageListener("RemoteLogins:removeLogin", this);
+ mm.addMessageListener("RemoteLogins:insecureLoginFormPresent", this);
+
+ XPCOMUtils.defineLazyGetter(this, "recipeParentPromise", () => {
+ const { LoginRecipesParent } = Cu.import("resource://gre/modules/LoginRecipes.jsm", {});
+ this._recipeManager = new LoginRecipesParent({
+ defaults: Services.prefs.getComplexValue("signon.recipes.path", Ci.nsISupportsString).data,
+ });
+ return this._recipeManager.initializationPromise;
+ });
+ },
+
+ receiveMessage: function (msg) {
+ let data = msg.data;
+ switch (msg.name) {
+ case "RemoteLogins:findLogins": {
+ // TODO Verify msg.target's principals against the formOrigin?
+ this.sendLoginDataToChild(data.options.showMasterPassword,
+ data.formOrigin,
+ data.actionOrigin,
+ data.requestId,
+ msg.target.messageManager);
+ break;
+ }
+
+ case "RemoteLogins:findRecipes": {
+ let formHost = (new URL(data.formOrigin)).host;
+ return this._recipeManager.getRecipesForHost(formHost);
+ }
+
+ case "RemoteLogins:onFormSubmit": {
+ // TODO Verify msg.target's principals against the formOrigin?
+ this.onFormSubmit(data.hostname,
+ data.formSubmitURL,
+ data.usernameField,
+ data.newPasswordField,
+ data.oldPasswordField,
+ msg.objects.openerTopWindow,
+ msg.target);
+ break;
+ }
+
+ case "RemoteLogins:insecureLoginFormPresent": {
+ this.setHasInsecureLoginForms(msg.target, data.hasInsecureLoginForms);
+ break;
+ }
+
+ case "RemoteLogins:autoCompleteLogins": {
+ this.doAutocompleteSearch(data, msg.target);
+ break;
+ }
+
+ case "RemoteLogins:removeLogin": {
+ let login = LoginHelper.vanillaObjectToLogin(data.login);
+ AutoCompletePopup.removeLogin(login);
+ break;
+ }
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Trigger a login form fill and send relevant data (e.g. logins and recipes)
+ * to the child process (LoginManagerContent).
+ */
+ fillForm: Task.async(function* ({ browser, loginFormOrigin, login, inputElement }) {
+ let recipes = [];
+ if (loginFormOrigin) {
+ let formHost;
+ try {
+ formHost = (new URL(loginFormOrigin)).host;
+ let recipeManager = yield this.recipeParentPromise;
+ recipes = recipeManager.getRecipesForHost(formHost);
+ } catch (ex) {
+ // Some schemes e.g. chrome aren't supported by URL
+ }
+ }
+
+ // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
+ // doesn't support structured cloning.
+ let jsLogins = [LoginHelper.loginToVanillaObject(login)];
+
+ let objects = inputElement ? {inputElement} : null;
+ browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", {
+ loginFormOrigin,
+ logins: jsLogins,
+ recipes,
+ }, objects);
+ }),
+
+ /**
+ * Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent).
+ */
+ sendLoginDataToChild: Task.async(function*(showMasterPassword, formOrigin, actionOrigin,
+ requestId, target) {
+ let recipes = [];
+ if (formOrigin) {
+ let formHost;
+ try {
+ formHost = (new URL(formOrigin)).host;
+ let recipeManager = yield this.recipeParentPromise;
+ recipes = recipeManager.getRecipesForHost(formHost);
+ } catch (ex) {
+ // Some schemes e.g. chrome aren't supported by URL
+ }
+ }
+
+ if (!showMasterPassword && !Services.logins.isLoggedIn) {
+ try {
+ target.sendAsyncMessage("RemoteLogins:loginsFound", {
+ requestId: requestId,
+ logins: [],
+ recipes,
+ });
+ } catch (e) {
+ log("error sending message to target", e);
+ }
+ return;
+ }
+
+ // If we're currently displaying a master password prompt, defer
+ // processing this form until the user handles the prompt.
+ if (Services.logins.uiBusy) {
+ log("deferring sendLoginDataToChild for", formOrigin);
+ let self = this;
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ observe: function (subject, topic, data) {
+ log("Got deferred sendLoginDataToChild notification:", topic);
+ // Only run observer once.
+ Services.obs.removeObserver(this, "passwordmgr-crypto-login");
+ Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
+ if (topic == "passwordmgr-crypto-loginCanceled") {
+ target.sendAsyncMessage("RemoteLogins:loginsFound", {
+ requestId: requestId,
+ logins: [],
+ recipes,
+ });
+ return;
+ }
+
+ self.sendLoginDataToChild(showMasterPassword, formOrigin, actionOrigin,
+ requestId, target);
+ },
+ };
+
+ // Possible leak: it's possible that neither of these notifications
+ // will fire, and if that happens, we'll leak the observer (and
+ // never return). We should guarantee that at least one of these
+ // will fire.
+ // See bug XXX.
+ Services.obs.addObserver(observer, "passwordmgr-crypto-login", false);
+ Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", false);
+ return;
+ }
+
+ let logins = this._searchAndDedupeLogins(formOrigin, actionOrigin);
+
+ log("sendLoginDataToChild:", logins.length, "deduped logins");
+ // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
+ // doesn't support structured cloning.
+ var jsLogins = LoginHelper.loginsToVanillaObjects(logins);
+ target.sendAsyncMessage("RemoteLogins:loginsFound", {
+ requestId: requestId,
+ logins: jsLogins,
+ recipes,
+ });
+ }),
+
+ doAutocompleteSearch: function({ formOrigin, actionOrigin,
+ searchString, previousResult,
+ rect, requestId, isSecure, isPasswordField,
+ remote }, target) {
+ // Note: previousResult is a regular object, not an
+ // nsIAutoCompleteResult.
+
+ // Cancel if we unsuccessfully prompted for the master password too recently.
+ if (!Services.logins.isLoggedIn) {
+ let timeDiff = Date.now() - this._lastMPLoginCancelled;
+ if (timeDiff < this._repromptTimeout) {
+ log("Not searching logins for autocomplete since the master password " +
+ `prompt was last cancelled ${Math.round(timeDiff / 1000)} seconds ago.`);
+ // Send an empty array to make LoginManagerContent clear the
+ // outstanding request it has temporarily saved.
+ target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
+ requestId,
+ logins: [],
+ });
+ return;
+ }
+ }
+
+ let searchStringLower = searchString.toLowerCase();
+ let logins;
+ if (previousResult &&
+ searchStringLower.startsWith(previousResult.searchString.toLowerCase())) {
+ log("Using previous autocomplete result");
+
+ // We have a list of results for a shorter search string, so just
+ // filter them further based on the new search string.
+ logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins);
+ } else {
+ log("Creating new autocomplete search result.");
+
+ logins = this._searchAndDedupeLogins(formOrigin, actionOrigin);
+ }
+
+ let matchingLogins = logins.filter(function(fullMatch) {
+ let match = fullMatch.username;
+
+ // Remove results that are too short, or have different prefix.
+ // Also don't offer empty usernames as possible results except
+ // for password field.
+ if (isPasswordField) {
+ return true;
+ }
+ return match && match.toLowerCase().startsWith(searchStringLower);
+ });
+
+ // XXX In the E10S case, we're responsible for showing our own
+ // autocomplete popup here because the autocomplete protocol hasn't
+ // been e10s-ized yet. In the non-e10s case, our caller is responsible
+ // for showing the autocomplete popup (via the regular
+ // nsAutoCompleteController).
+ if (remote) {
+ let results = new UserAutoCompleteResult(searchString, matchingLogins, {isSecure});
+ AutoCompletePopup.showPopupWithResults({ browser: target.ownerDocument.defaultView, rect, results });
+ }
+
+ // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
+ // doesn't support structured cloning.
+ var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
+ target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
+ requestId: requestId,
+ logins: jsLogins,
+ });
+ },
+
+ onFormSubmit: function(hostname, formSubmitURL,
+ usernameField, newPasswordField,
+ oldPasswordField, openerTopWindow,
+ target) {
+ function getPrompter() {
+ var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
+ createInstance(Ci.nsILoginManagerPrompter);
+ prompterSvc.init(target.ownerDocument.defaultView);
+ prompterSvc.browser = target;
+ prompterSvc.opener = openerTopWindow;
+ return prompterSvc;
+ }
+
+ function recordLoginUse(login) {
+ // Update the lastUsed timestamp and increment the use count.
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ propBag.setProperty("timeLastUsed", Date.now());
+ propBag.setProperty("timesUsedIncrement", 1);
+ Services.logins.modifyLogin(login, propBag);
+ }
+
+ if (!Services.logins.getLoginSavingEnabled(hostname)) {
+ log("(form submission ignored -- saving is disabled for:", hostname, ")");
+ return;
+ }
+
+ var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ formLogin.init(hostname, formSubmitURL, null,
+ (usernameField ? usernameField.value : ""),
+ newPasswordField.value,
+ (usernameField ? usernameField.name : ""),
+ newPasswordField.name);
+
+ // Below here we have one login per hostPort + action + username with the
+ // matching scheme being preferred.
+ let logins = this._searchAndDedupeLogins(hostname, formSubmitURL);
+
+ // If we didn't find a username field, but seem to be changing a
+ // password, allow the user to select from a list of applicable
+ // logins to update the password for.
+ if (!usernameField && oldPasswordField && logins.length > 0) {
+ var prompter = getPrompter();
+
+ if (logins.length == 1) {
+ var oldLogin = logins[0];
+
+ if (oldLogin.password == formLogin.password) {
+ recordLoginUse(oldLogin);
+ log("(Not prompting to save/change since we have no username and the " +
+ "only saved password matches the new password)");
+ return;
+ }
+
+ formLogin.username = oldLogin.username;
+ formLogin.usernameField = oldLogin.usernameField;
+
+ prompter.promptToChangePassword(oldLogin, formLogin);
+ } else {
+ // Note: It's possible that that we already have the correct u+p saved
+ // but since we don't have the username, we don't know if the user is
+ // changing a second account to the new password so we ask anyways.
+
+ prompter.promptToChangePasswordWithUsernames(
+ logins, logins.length, formLogin);
+ }
+
+ return;
+ }
+
+
+ var existingLogin = null;
+ // Look for an existing login that matches the form login.
+ for (let login of logins) {
+ let same;
+
+ // If one login has a username but the other doesn't, ignore
+ // the username when comparing and only match if they have the
+ // same password. Otherwise, compare the logins and match even
+ // if the passwords differ.
+ if (!login.username && formLogin.username) {
+ var restoreMe = formLogin.username;
+ formLogin.username = "";
+ same = LoginHelper.doLoginsMatch(formLogin, login, {
+ ignorePassword: false,
+ ignoreSchemes: LoginHelper.schemeUpgrades,
+ });
+ formLogin.username = restoreMe;
+ } else if (!formLogin.username && login.username) {
+ formLogin.username = login.username;
+ same = LoginHelper.doLoginsMatch(formLogin, login, {
+ ignorePassword: false,
+ ignoreSchemes: LoginHelper.schemeUpgrades,
+ });
+ formLogin.username = ""; // we know it's always blank.
+ } else {
+ same = LoginHelper.doLoginsMatch(formLogin, login, {
+ ignorePassword: true,
+ ignoreSchemes: LoginHelper.schemeUpgrades,
+ });
+ }
+
+ if (same) {
+ existingLogin = login;
+ break;
+ }
+ }
+
+ if (existingLogin) {
+ log("Found an existing login matching this form submission");
+
+ // Change password if needed.
+ if (existingLogin.password != formLogin.password) {
+ log("...passwords differ, prompting to change.");
+ prompter = getPrompter();
+ prompter.promptToChangePassword(existingLogin, formLogin);
+ } else if (!existingLogin.username && formLogin.username) {
+ log("...empty username update, prompting to change.");
+ prompter = getPrompter();
+ prompter.promptToChangePassword(existingLogin, formLogin);
+ } else {
+ recordLoginUse(existingLogin);
+ }
+
+ return;
+ }
+
+
+ // Prompt user to save login (via dialog or notification bar)
+ prompter = getPrompter();
+ prompter.promptToSavePassword(formLogin);
+ },
+
+ /**
+ * Maps all the <browser> elements for tabs in the parent process to the
+ * current state used to display tab-specific UI.
+ *
+ * This mapping is not updated in case a web page is moved to a different
+ * chrome window by the swapDocShells method. In this case, it is possible
+ * that a UI update just requested for the login fill doorhanger and then
+ * delayed by a few hundred milliseconds will be lost. Later requests would
+ * use the new browser reference instead.
+ *
+ * Given that the case above is rare, and it would not cause any origin
+ * mismatch at the time of filling because the origin is checked later in the
+ * content process, this case is left unhandled.
+ */
+ loginFormStateByBrowser: new WeakMap(),
+
+ /**
+ * Retrieves a reference to the state object associated with the given
+ * browser. This is initialized to an empty object.
+ */
+ stateForBrowser(browser) {
+ let loginFormState = this.loginFormStateByBrowser.get(browser);
+ if (!loginFormState) {
+ loginFormState = {};
+ this.loginFormStateByBrowser.set(browser, loginFormState);
+ }
+ return loginFormState;
+ },
+
+ /**
+ * Returns true if the page currently loaded in the given browser element has
+ * insecure login forms. This state may be updated asynchronously, in which
+ * case a custom event named InsecureLoginFormsStateChange will be dispatched
+ * on the browser element.
+ */
+ hasInsecureLoginForms(browser) {
+ return !!this.stateForBrowser(browser).hasInsecureLoginForms;
+ },
+
+ /**
+ * Called to indicate whether an insecure password field is present so
+ * insecure password UI can know when to show.
+ */
+ setHasInsecureLoginForms(browser, hasInsecureLoginForms) {
+ let state = this.stateForBrowser(browser);
+
+ // Update the data to use to the latest known values. Since messages are
+ // processed in order, this will always be the latest version to use.
+ state.hasInsecureLoginForms = hasInsecureLoginForms;
+
+ // Report the insecure login form state immediately.
+ browser.dispatchEvent(new browser.ownerDocument.defaultView
+ .CustomEvent("InsecureLoginFormsStateChange"));
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(LoginManagerParent, "_repromptTimeout",
+ "signon.masterPasswordReprompt.timeout_ms", 900000); // 15 Minutes
diff --git a/toolkit/components/passwordmgr/LoginRecipes.jsm b/toolkit/components/passwordmgr/LoginRecipes.jsm
new file mode 100644
index 0000000000..4a8124bbca
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginRecipes.jsm
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["LoginRecipesContent", "LoginRecipesParent"];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const REQUIRED_KEYS = ["hosts"];
+const OPTIONAL_KEYS = ["description", "notUsernameSelector", "passwordSelector", "pathRegex", "usernameSelector"];
+const SUPPORTED_KEYS = REQUIRED_KEYS.concat(OPTIONAL_KEYS);
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => LoginHelper.createLogger("LoginRecipes"));
+
+/**
+ * Create an instance of the object to manage recipes in the parent process.
+ * Consumers should wait until {@link initializationPromise} resolves before
+ * calling methods on the object.
+ *
+ * @constructor
+ * @param {String} [aOptions.defaults=null] the URI to load the recipes from.
+ * If it's null, nothing is loaded.
+ *
+*/
+function LoginRecipesParent(aOptions = { defaults: null }) {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ throw new Error("LoginRecipesParent should only be used from the main process");
+ }
+ this._defaults = aOptions.defaults;
+ this.reset();
+}
+
+LoginRecipesParent.prototype = {
+ /**
+ * Promise resolved with an instance of itself when the module is ready.
+ *
+ * @type {Promise}
+ */
+ initializationPromise: null,
+
+ /**
+ * @type {bool} Whether default recipes were loaded at construction time.
+ */
+ _defaults: null,
+
+ /**
+ * @type {Map} Map of hosts (including non-default port numbers) to Sets of recipes.
+ * e.g. "example.com:8080" => Set({...})
+ */
+ _recipesByHost: null,
+
+ /**
+ * @param {Object} aRecipes an object containing recipes to load for use. The object
+ * should be compatible with JSON (e.g. no RegExp).
+ * @return {Promise} resolving when the recipes are loaded
+ */
+ load(aRecipes) {
+ let recipeErrors = 0;
+ for (let rawRecipe of aRecipes.siteRecipes) {
+ try {
+ rawRecipe.pathRegex = rawRecipe.pathRegex ? new RegExp(rawRecipe.pathRegex) : undefined;
+ this.add(rawRecipe);
+ } catch (ex) {
+ recipeErrors++;
+ log.error("Error loading recipe", rawRecipe, ex);
+ }
+ }
+
+ if (recipeErrors) {
+ return Promise.reject(`There were ${recipeErrors} recipe error(s)`);
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Reset the set of recipes to the ones from the time of construction.
+ */
+ reset() {
+ log.debug("Resetting recipes with defaults:", this._defaults);
+ this._recipesByHost = new Map();
+
+ if (this._defaults) {
+ let channel = NetUtil.newChannel({uri: NetUtil.newURI(this._defaults, "UTF-8"),
+ loadUsingSystemPrincipal: true});
+ channel.contentType = "application/json";
+
+ try {
+ this.initializationPromise = new Promise(function(resolve) {
+ NetUtil.asyncFetch(channel, function (stream, result) {
+ if (!Components.isSuccessCode(result)) {
+ throw new Error("Error fetching recipe file:" + result);
+ }
+ let count = stream.available();
+ let data = NetUtil.readInputStreamToString(stream, count, { charset: "UTF-8" });
+ resolve(JSON.parse(data));
+ });
+ }).then(recipes => {
+ return this.load(recipes);
+ }).then(resolve => {
+ return this;
+ });
+ } catch (e) {
+ throw new Error("Error reading recipe file:" + e);
+ }
+ } else {
+ this.initializationPromise = Promise.resolve(this);
+ }
+ },
+
+ /**
+ * Validate the recipe is sane and then add it to the set of recipes.
+ *
+ * @param {Object} recipe
+ */
+ add(recipe) {
+ log.debug("Adding recipe:", recipe);
+ let recipeKeys = Object.keys(recipe);
+ let unknownKeys = recipeKeys.filter(key => SUPPORTED_KEYS.indexOf(key) == -1);
+ if (unknownKeys.length > 0) {
+ throw new Error("The following recipe keys aren't supported: " + unknownKeys.join(", "));
+ }
+
+ let missingRequiredKeys = REQUIRED_KEYS.filter(key => recipeKeys.indexOf(key) == -1);
+ if (missingRequiredKeys.length > 0) {
+ throw new Error("The following required recipe keys are missing: " + missingRequiredKeys.join(", "));
+ }
+
+ if (!Array.isArray(recipe.hosts)) {
+ throw new Error("'hosts' must be a array");
+ }
+
+ if (!recipe.hosts.length) {
+ throw new Error("'hosts' must be a non-empty array");
+ }
+
+ if (recipe.pathRegex && recipe.pathRegex.constructor.name != "RegExp") {
+ throw new Error("'pathRegex' must be a regular expression");
+ }
+
+ const OPTIONAL_STRING_PROPS = ["description", "passwordSelector", "usernameSelector"];
+ for (let prop of OPTIONAL_STRING_PROPS) {
+ if (recipe[prop] && typeof(recipe[prop]) != "string") {
+ throw new Error(`'${prop}' must be a string`);
+ }
+ }
+
+ // Add the recipe to the map for each host
+ for (let host of recipe.hosts) {
+ if (!this._recipesByHost.has(host)) {
+ this._recipesByHost.set(host, new Set());
+ }
+ this._recipesByHost.get(host).add(recipe);
+ }
+ },
+
+ /**
+ * Currently only exact host matches are returned but this will eventually handle parent domains.
+ *
+ * @param {String} aHost (e.g. example.com:8080 [non-default port] or sub.example.com)
+ * @return {Set} of recipes that apply to the host ordered by host priority
+ */
+ getRecipesForHost(aHost) {
+ let hostRecipes = this._recipesByHost.get(aHost);
+ if (!hostRecipes) {
+ return new Set();
+ }
+
+ return hostRecipes;
+ },
+};
+
+
+var LoginRecipesContent = {
+ /**
+ * @param {Set} aRecipes - Possible recipes that could apply to the form
+ * @param {FormLike} aForm - We use a form instead of just a URL so we can later apply
+ * tests to the page contents.
+ * @return {Set} a subset of recipes that apply to the form with the order preserved
+ */
+ _filterRecipesForForm(aRecipes, aForm) {
+ let formDocURL = aForm.ownerDocument.location;
+ let hostRecipes = aRecipes;
+ let recipes = new Set();
+ log.debug("_filterRecipesForForm", aRecipes);
+ if (!hostRecipes) {
+ return recipes;
+ }
+
+ for (let hostRecipe of hostRecipes) {
+ if (hostRecipe.pathRegex && !hostRecipe.pathRegex.test(formDocURL.pathname)) {
+ continue;
+ }
+ recipes.add(hostRecipe);
+ }
+
+ return recipes;
+ },
+
+ /**
+ * Given a set of recipes that apply to the host, choose the one most applicable for
+ * overriding login fields in the form.
+ *
+ * @param {Set} aRecipes The set of recipes to consider for the form
+ * @param {FormLike} aForm The form where login fields exist.
+ * @return {Object} The recipe that is most applicable for the form.
+ */
+ getFieldOverrides(aRecipes, aForm) {
+ let recipes = this._filterRecipesForForm(aRecipes, aForm);
+ log.debug("getFieldOverrides: filtered recipes:", recipes);
+ if (!recipes.size) {
+ return null;
+ }
+
+ let chosenRecipe = null;
+ // Find the first (most-specific recipe that involves field overrides).
+ for (let recipe of recipes) {
+ if (!recipe.usernameSelector && !recipe.passwordSelector &&
+ !recipe.notUsernameSelector) {
+ continue;
+ }
+
+ chosenRecipe = recipe;
+ break;
+ }
+
+ return chosenRecipe;
+ },
+
+ /**
+ * @param {HTMLElement} aParent the element to query for the selector from.
+ * @param {CSSSelector} aSelector the CSS selector to query for the login field.
+ * @return {HTMLElement|null}
+ */
+ queryLoginField(aParent, aSelector) {
+ if (!aSelector) {
+ return null;
+ }
+ let field = aParent.ownerDocument.querySelector(aSelector);
+ if (!field) {
+ log.debug("Login field selector wasn't matched:", aSelector);
+ return null;
+ }
+ if (!(field instanceof aParent.ownerDocument.defaultView.HTMLInputElement)) {
+ log.warn("Login field isn't an <input> so ignoring it:", aSelector);
+ return null;
+ }
+ return field;
+ },
+};
diff --git a/toolkit/components/passwordmgr/LoginStore.jsm b/toolkit/components/passwordmgr/LoginStore.jsm
new file mode 100644
index 0000000000..9fa6e7dff9
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginStore.jsm
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Handles serialization of the data and persistence into a file.
+ *
+ * The file is stored in JSON format, without indentation, using UTF-8 encoding.
+ * With indentation applied, the file would look like this:
+ *
+ * {
+ * "logins": [
+ * {
+ * "id": 2,
+ * "hostname": "http://www.example.com",
+ * "httpRealm": null,
+ * "formSubmitURL": "http://www.example.com/submit-url",
+ * "usernameField": "username_field",
+ * "passwordField": "password_field",
+ * "encryptedUsername": "...",
+ * "encryptedPassword": "...",
+ * "guid": "...",
+ * "encType": 1,
+ * "timeCreated": 1262304000000,
+ * "timeLastUsed": 1262304000000,
+ * "timePasswordChanged": 1262476800000,
+ * "timesUsed": 1
+ * },
+ * {
+ * "id": 4,
+ * (...)
+ * }
+ * ],
+ * "disabledHosts": [
+ * "http://www.example.org",
+ * "http://www.example.net"
+ * ],
+ * "nextId": 10,
+ * "version": 1
+ * }
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginStore",
+];
+
+// Globals
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+ "resource://gre/modules/JSONFile.jsm");
+
+/**
+ * Current data version assigned by the code that last touched the data.
+ *
+ * This number should be updated only when it is important to understand whether
+ * an old version of the code has touched the data, for example to execute an
+ * update logic. In most cases, this number should not be changed, in
+ * particular when no special one-time update logic is needed.
+ *
+ * For example, this number should NOT be changed when a new optional field is
+ * added to a login entry.
+ */
+const kDataVersion = 2;
+
+// The permission type we store in the permission manager.
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+// LoginStore
+
+/**
+ * Inherits from JSONFile and handles serialization of login-related data and
+ * persistence into a file.
+ *
+ * @param aPath
+ * String containing the file path where data should be saved.
+ */
+function LoginStore(aPath) {
+ JSONFile.call(this, {
+ path: aPath,
+ dataPostProcessor: this._dataPostProcessor.bind(this)
+ });
+}
+
+LoginStore.prototype = Object.create(JSONFile.prototype);
+LoginStore.prototype.constructor = LoginStore;
+
+/**
+ * Synchronously work on the data just loaded into memory.
+ */
+LoginStore.prototype._dataPostProcessor = function(data) {
+ if (data.nextId === undefined) {
+ data.nextId = 1;
+ }
+
+ // Create any arrays that are not present in the saved file.
+ if (!data.logins) {
+ data.logins = [];
+ }
+
+ // Stub needed for login imports before data has been migrated.
+ if (!data.disabledHosts) {
+ data.disabledHosts = [];
+ }
+
+ if (data.version === 1) {
+ this._migrateDisabledHosts(data);
+ }
+
+ // Indicate that the current version of the code has touched the file.
+ data.version = kDataVersion;
+
+ return data;
+};
+
+/**
+ * Migrates disabled hosts to the permission manager.
+ */
+LoginStore.prototype._migrateDisabledHosts = function (data) {
+ for (let host of data.disabledHosts) {
+ try {
+ let uri = Services.io.newURI(host, null, null);
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ delete data.disabledHosts;
+};
diff --git a/toolkit/components/passwordmgr/OSCrypto.jsm b/toolkit/components/passwordmgr/OSCrypto.jsm
new file mode 100644
index 0000000000..04254f66f5
--- /dev/null
+++ b/toolkit/components/passwordmgr/OSCrypto.jsm
@@ -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/. */
+
+/**
+ * Common front for various implementations of OSCrypto
+ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["OSCrypto"];
+
+var OSCrypto = {};
+
+if (AppConstants.platform == "win") {
+ Services.scriptloader.loadSubScript("resource://gre/modules/OSCrypto_win.js", this);
+} else {
+ throw new Error("OSCrypto.jsm isn't supported on this platform");
+}
diff --git a/toolkit/components/passwordmgr/OSCrypto_win.js b/toolkit/components/passwordmgr/OSCrypto_win.js
new file mode 100644
index 0000000000..0f52f42692
--- /dev/null
+++ b/toolkit/components/passwordmgr/OSCrypto_win.js
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes", "resource://gre/modules/ctypes.jsm");
+
+const FLAGS_NOT_SET = 0;
+
+const wintypes = {
+ BOOL: ctypes.bool,
+ BYTE: ctypes.uint8_t,
+ DWORD: ctypes.uint32_t,
+ PBYTE: ctypes.unsigned_char.ptr,
+ PCHAR: ctypes.char.ptr,
+ PDWORD: ctypes.uint32_t.ptr,
+ PVOID: ctypes.voidptr_t,
+ WORD: ctypes.uint16_t,
+};
+
+function OSCrypto() {
+ this._structs = {};
+ this._functions = new Map();
+ this._libs = new Map();
+ this._structs.DATA_BLOB = new ctypes.StructType("DATA_BLOB",
+ [
+ {cbData: wintypes.DWORD},
+ {pbData: wintypes.PVOID}
+ ]);
+
+ try {
+
+ this._libs.set("crypt32", ctypes.open("Crypt32"));
+ this._libs.set("kernel32", ctypes.open("Kernel32"));
+
+ this._functions.set("CryptProtectData",
+ this._libs.get("crypt32").declare("CryptProtectData",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr));
+ this._functions.set("CryptUnprotectData",
+ this._libs.get("crypt32").declare("CryptUnprotectData",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr));
+ this._functions.set("LocalFree",
+ this._libs.get("kernel32").declare("LocalFree",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ wintypes.PVOID));
+ } catch (ex) {
+ Cu.reportError(ex);
+ this.finalize();
+ throw ex;
+ }
+}
+OSCrypto.prototype = {
+ /**
+ * Convert an array containing only two bytes unsigned numbers to a string.
+ * @param {number[]} arr - the array that needs to be converted.
+ * @returns {string} the string representation of the array.
+ */
+ arrayToString(arr) {
+ let str = "";
+ for (let i = 0; i < arr.length; i++) {
+ str += String.fromCharCode(arr[i]);
+ }
+ return str;
+ },
+
+ /**
+ * Convert a string to an array.
+ * @param {string} str - the string that needs to be converted.
+ * @returns {number[]} the array representation of the string.
+ */
+ stringToArray(str) {
+ let arr = [];
+ for (let i = 0; i < str.length; i++) {
+ arr.push(str.charCodeAt(i));
+ }
+ return arr;
+ },
+
+ /**
+ * Calculate the hash value used by IE as the name of the registry value where login details are
+ * stored.
+ * @param {string} data - the string value that needs to be hashed.
+ * @returns {string} the hash value of the string.
+ */
+ getIELoginHash(data) {
+ // return the two-digit hexadecimal code for a byte
+ function toHexString(charCode) {
+ return ("00" + charCode.toString(16)).slice(-2);
+ }
+
+ // the data needs to be encoded in null terminated UTF-16
+ data += "\0";
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-16";
+ // result is an out parameter,
+ // result.value will contain the array length
+ let result = {};
+ // dataArray is an array of bytes
+ let dataArray = converter.convertToByteArray(data, result);
+ // calculation of SHA1 hash value
+ let cryptoHash = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ cryptoHash.init(cryptoHash.SHA1);
+ cryptoHash.update(dataArray, dataArray.length);
+ let hash = cryptoHash.finish(false);
+
+ let tail = 0; // variable to calculate value for the last 2 bytes
+ // convert to a character string in hexadecimal notation
+ for (let c of hash) {
+ tail += c.charCodeAt(0);
+ }
+ hash += String.fromCharCode(tail % 256);
+
+ // convert the binary hash data to a hex string.
+ let hashStr = Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
+ return hashStr.toUpperCase();
+ },
+
+ /**
+ * Decrypt a string using the windows CryptUnprotectData API.
+ * @param {string} data - the encrypted string that needs to be decrypted.
+ * @param {?string} entropy - the entropy value of the decryption (could be null). Its value must
+ * be the same as the one used when the data was encrypted.
+ * @returns {string} the decryption of the string.
+ */
+ decryptData(data, entropy = null) {
+ let array = this.stringToArray(data);
+ let decryptedData = "";
+ let encryptedData = wintypes.BYTE.array(array.length)(array);
+ let inData = new this._structs.DATA_BLOB(encryptedData.length, encryptedData);
+ let outData = new this._structs.DATA_BLOB();
+ let entropyParam;
+ if (entropy) {
+ let entropyArray = this.stringToArray(entropy);
+ entropyArray.push(0);
+ let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
+ let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
+ entropyData);
+ entropyParam = optionalEntropy.address();
+ } else {
+ entropyParam = null;
+ }
+
+ let status = this._functions.get("CryptUnprotectData")(inData.address(), null,
+ entropyParam,
+ null, null, FLAGS_NOT_SET,
+ outData.address());
+ if (status === 0) {
+ throw new Error("decryptData failed: " + status);
+ }
+
+ // convert byte array to JS string.
+ let len = outData.cbData;
+ let decrypted = ctypes.cast(outData.pbData,
+ wintypes.BYTE.array(len).ptr).contents;
+ for (let i = 0; i < decrypted.length; i++) {
+ decryptedData += String.fromCharCode(decrypted[i]);
+ }
+
+ this._functions.get("LocalFree")(outData.pbData);
+ return decryptedData;
+ },
+
+ /**
+ * Encrypt a string using the windows CryptProtectData API.
+ * @param {string} data - the string that is going to be encrypted.
+ * @param {?string} entropy - the entropy value of the encryption (could be null). Its value must
+ * be the same as the one that is going to be used for the decryption.
+ * @returns {string} the encrypted string.
+ */
+ encryptData(data, entropy = null) {
+ let encryptedData = "";
+ let decryptedData = wintypes.BYTE.array(data.length)(this.stringToArray(data));
+
+ let inData = new this._structs.DATA_BLOB(data.length, decryptedData);
+ let outData = new this._structs.DATA_BLOB();
+ let entropyParam;
+ if (!entropy) {
+ entropyParam = null;
+ } else {
+ let entropyArray = this.stringToArray(entropy);
+ entropyArray.push(0);
+ let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
+ let optionalEntropy = new this._structs.DATA_BLOB(entropyData.length * 2,
+ entropyData);
+ entropyParam = optionalEntropy.address();
+ }
+
+ let status = this._functions.get("CryptProtectData")(inData.address(), null,
+ entropyParam,
+ null, null, FLAGS_NOT_SET,
+ outData.address());
+ if (status === 0) {
+ throw new Error("encryptData failed: " + status);
+ }
+
+ // convert byte array to JS string.
+ let len = outData.cbData;
+ let encrypted = ctypes.cast(outData.pbData,
+ wintypes.BYTE.array(len).ptr).contents;
+ encryptedData = this.arrayToString(encrypted);
+ this._functions.get("LocalFree")(outData.pbData);
+ return encryptedData;
+ },
+
+ /**
+ * Must be invoked once after last use of any of the provided helpers.
+ */
+ finalize() {
+ this._structs = {};
+ this._functions.clear();
+ for (let lib of this._libs.values()) {
+ try {
+ lib.close();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ this._libs.clear();
+ },
+};
diff --git a/toolkit/components/passwordmgr/content/passwordManager.js b/toolkit/components/passwordmgr/content/passwordManager.js
new file mode 100644
index 0000000000..333dc1d245
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -0,0 +1,728 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** * =================== SAVED SIGNONS CODE =================== ***/
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+let kSignonBundle;
+
+// Default value for signon table sorting
+let lastSignonSortColumn = "hostname";
+let lastSignonSortAscending = true;
+
+let showingPasswords = false;
+
+// password-manager lists
+let signons = [];
+let deletedSignons = [];
+
+// Elements that would be used frequently
+let filterField;
+let togglePasswordsButton;
+let signonsIntro;
+let removeButton;
+let removeAllButton;
+let signonsTree;
+
+let signonReloadDisplay = {
+ observe: function(subject, topic, data) {
+ if (topic == "passwordmgr-storage-changed") {
+ switch (data) {
+ case "addLogin":
+ case "modifyLogin":
+ case "removeLogin":
+ case "removeAllLogins":
+ if (!signonsTree) {
+ return;
+ }
+ signons.length = 0;
+ LoadSignons();
+ // apply the filter if needed
+ if (filterField && filterField.value != "") {
+ FilterPasswords();
+ }
+ break;
+ }
+ Services.obs.notifyObservers(null, "passwordmgr-dialog-updated", null);
+ }
+ }
+};
+
+// Formatter for localization.
+let dateFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric",
+ hour: "numeric", minute: "numeric" });
+
+function Startup() {
+ // be prepared to reload the display if anything changes
+ Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed", false);
+
+ signonsTree = document.getElementById("signonsTree");
+ kSignonBundle = document.getElementById("signonBundle");
+ filterField = document.getElementById("filter");
+ togglePasswordsButton = document.getElementById("togglePasswords");
+ signonsIntro = document.getElementById("signonsIntro");
+ removeButton = document.getElementById("removeSignon");
+ removeAllButton = document.getElementById("removeAllSignons");
+
+ togglePasswordsButton.label = kSignonBundle.getString("showPasswords");
+ togglePasswordsButton.accessKey = kSignonBundle.getString("showPasswordsAccessKey");
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionAll");
+ document.getElementsByTagName("treecols")[0].addEventListener("click", (event) => {
+ let { target, button } = event;
+ let sortField = target.getAttribute("data-field-name");
+
+ if (target.nodeName != "treecol" || button != 0 || !sortField) {
+ return;
+ }
+
+ SignonColumnSort(sortField);
+ Services.telemetry.getKeyedHistogramById("PWMGR_MANAGE_SORTED").add(sortField);
+ });
+
+ LoadSignons();
+
+ // filter the table if requested by caller
+ if (window.arguments &&
+ window.arguments[0] &&
+ window.arguments[0].filterString) {
+ setFilter(window.arguments[0].filterString);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_OPENED").add(1);
+ } else {
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_OPENED").add(0);
+ }
+
+ FocusFilterBox();
+}
+
+function Shutdown() {
+ Services.obs.removeObserver(signonReloadDisplay, "passwordmgr-storage-changed");
+}
+
+function setFilter(aFilterString) {
+ filterField.value = aFilterString;
+ FilterPasswords();
+}
+
+let signonsTreeView = {
+ // Keep track of which favicons we've fetched or started fetching.
+ // Maps a login origin to a favicon URL.
+ _faviconMap: new Map(),
+ _filterSet: [],
+ // Coalesce invalidations to avoid repeated flickering.
+ _invalidateTask: new DeferredTask(() => {
+ signonsTree.treeBoxObject.invalidateColumn(signonsTree.columns.siteCol);
+ }, 10),
+ _lastSelectedRanges: [],
+ selection: null,
+
+ rowCount: 0,
+ setTree(tree) {},
+ getImageSrc(row, column) {
+ if (column.element.getAttribute("id") !== "siteCol") {
+ return "";
+ }
+
+ const signon = this._filterSet.length ? this._filterSet[row] : signons[row];
+
+ // We already have the favicon URL or we started to fetch (value is null).
+ if (this._faviconMap.has(signon.hostname)) {
+ return this._faviconMap.get(signon.hostname);
+ }
+
+ // Record the fact that we already starting fetching a favicon for this
+ // origin in order to avoid multiple requests for the same origin.
+ this._faviconMap.set(signon.hostname, null);
+
+ PlacesUtils.promiseFaviconLinkUrl(signon.hostname)
+ .then(faviconURI => {
+ this._faviconMap.set(signon.hostname, faviconURI.spec);
+ this._invalidateTask.arm();
+ }).catch(Cu.reportError);
+
+ return "";
+ },
+ getProgressMode(row, column) {},
+ getCellValue(row, column) {},
+ getCellText(row, column) {
+ let time;
+ let signon = this._filterSet.length ? this._filterSet[row] : signons[row];
+ switch (column.id) {
+ case "siteCol":
+ return signon.httpRealm ?
+ (signon.hostname + " (" + signon.httpRealm + ")") :
+ signon.hostname;
+ case "userCol":
+ return signon.username || "";
+ case "passwordCol":
+ return signon.password || "";
+ case "timeCreatedCol":
+ time = new Date(signon.timeCreated);
+ return dateFormatter.format(time);
+ case "timeLastUsedCol":
+ time = new Date(signon.timeLastUsed);
+ return dateAndTimeFormatter.format(time);
+ case "timePasswordChangedCol":
+ time = new Date(signon.timePasswordChanged);
+ return dateFormatter.format(time);
+ case "timesUsedCol":
+ return signon.timesUsed;
+ default:
+ return "";
+ }
+ },
+ isEditable(row, col) {
+ if (col.id == "userCol" || col.id == "passwordCol") {
+ return true;
+ }
+ return false;
+ },
+ isSeparator(index) { return false; },
+ isSorted() { return false; },
+ isContainer(index) { return false; },
+ cycleHeader(column) {},
+ getRowProperties(row) { return ""; },
+ getColumnProperties(column) { return ""; },
+ getCellProperties(row, column) {
+ if (column.element.getAttribute("id") == "siteCol")
+ return "ltr";
+
+ return "";
+ },
+ setCellText(row, col, value) {
+ // If there is a filter, _filterSet needs to be used, otherwise signons is used.
+ let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+ function _editLogin(field) {
+ if (value == table[row][field]) {
+ return;
+ }
+ let existingLogin = table[row].clone();
+ table[row][field] = value;
+ table[row].timePasswordChanged = Date.now();
+ Services.logins.modifyLogin(existingLogin, table[row]);
+ signonsTree.treeBoxObject.invalidateRow(row);
+ }
+
+ if (col.id == "userCol") {
+ _editLogin("username");
+
+ } else if (col.id == "passwordCol") {
+ if (!value) {
+ return;
+ }
+ _editLogin("password");
+ }
+ },
+};
+
+function SortTree(column, ascending) {
+ let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+ // remember which item was selected so we can restore it after the sort
+ let selections = GetTreeSelections();
+ let selectedNumber = selections.length ? table[selections[0]].number : -1;
+
+ function compareFunc(a, b) {
+ let valA, valB;
+ switch (column) {
+ case "hostname":
+ let realmA = a.httpRealm;
+ let realmB = b.httpRealm;
+ realmA = realmA == null ? "" : realmA.toLowerCase();
+ realmB = realmB == null ? "" : realmB.toLowerCase();
+
+ valA = a[column].toLowerCase() + realmA;
+ valB = b[column].toLowerCase() + realmB;
+ break;
+ case "username":
+ case "password":
+ valA = a[column].toLowerCase();
+ valB = b[column].toLowerCase();
+ break;
+
+ default:
+ valA = a[column];
+ valB = b[column];
+ }
+
+ if (valA < valB)
+ return -1;
+ if (valA > valB)
+ return 1;
+ return 0;
+ }
+
+ // do the sort
+ table.sort(compareFunc);
+ if (!ascending) {
+ table.reverse();
+ }
+
+ // restore the selection
+ let selectedRow = -1;
+ if (selectedNumber >= 0 && false) {
+ for (let s = 0; s < table.length; s++) {
+ if (table[s].number == selectedNumber) {
+ // update selection
+ // note: we need to deselect before reselecting in order to trigger ...Selected()
+ signonsTree.view.selection.select(-1);
+ signonsTree.view.selection.select(s);
+ selectedRow = s;
+ break;
+ }
+ }
+ }
+
+ // display the results
+ signonsTree.treeBoxObject.invalidate();
+ if (selectedRow >= 0) {
+ signonsTree.treeBoxObject.ensureRowIsVisible(selectedRow);
+ }
+}
+
+function LoadSignons() {
+ // loads signons into table
+ try {
+ signons = Services.logins.getAllLogins();
+ } catch (e) {
+ signons = [];
+ }
+ signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
+ signonsTreeView.rowCount = signons.length;
+
+ // sort and display the table
+ signonsTree.view = signonsTreeView;
+ // The sort column didn't change. SortTree (called by
+ // SignonColumnSort) assumes we want to toggle the sort
+ // direction but here we don't so we have to trick it
+ lastSignonSortAscending = !lastSignonSortAscending;
+ SignonColumnSort(lastSignonSortColumn);
+
+ // disable "remove all signons" button if there are no signons
+ if (signons.length == 0) {
+ removeAllButton.setAttribute("disabled", "true");
+ togglePasswordsButton.setAttribute("disabled", "true");
+ } else {
+ removeAllButton.removeAttribute("disabled");
+ togglePasswordsButton.removeAttribute("disabled");
+ }
+
+ return true;
+}
+
+function GetTreeSelections() {
+ let selections = [];
+ let select = signonsTree.view.selection;
+ if (select) {
+ 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[selections.length] = k;
+ }
+ }
+ }
+ }
+ return selections;
+}
+
+function SignonSelected() {
+ let selections = GetTreeSelections();
+ if (selections.length) {
+ removeButton.removeAttribute("disabled");
+ } else {
+ removeButton.setAttribute("disabled", true);
+ }
+}
+
+function DeleteSignon() {
+ let filterSet = signonsTreeView._filterSet;
+ let syncNeeded = (filterSet.length != 0);
+ let tree = signonsTree;
+ let view = signonsTreeView;
+ let table = filterSet.length ? filterSet : signons;
+
+ // Turn off tree selection notifications during the deletion
+ tree.view.selection.selectEventsSuppressed = true;
+
+ // remove selected items from list (by setting them to null) and place in deleted list
+ let selections = GetTreeSelections();
+ for (let s = selections.length - 1; s >= 0; s--) {
+ let i = selections[s];
+ deletedSignons.push(table[i]);
+ table[i] = null;
+ }
+
+ // collapse list by removing all the null entries
+ for (let j = 0; j < table.length; j++) {
+ if (table[j] == null) {
+ let k = j;
+ while ((k < table.length) && (table[k] == null)) {
+ k++;
+ }
+ table.splice(j, k - j);
+ view.rowCount -= k - j;
+ tree.treeBoxObject.rowCountChanged(j, j - k);
+ }
+ }
+
+ // update selection and/or buttons
+ if (table.length) {
+ // update selection
+ let nextSelection = (selections[0] < table.length) ? selections[0] : table.length - 1;
+ tree.view.selection.select(nextSelection);
+ tree.treeBoxObject.ensureRowIsVisible(nextSelection);
+ } else {
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ }
+ tree.view.selection.selectEventsSuppressed = false;
+ FinalizeSignonDeletions(syncNeeded);
+}
+
+function DeleteAllSignons() {
+ let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Ci.nsIPromptService);
+
+ // Confirm the user wants to remove all passwords
+ let dummy = { value: false };
+ if (prompter.confirmEx(window,
+ kSignonBundle.getString("removeAllPasswordsTitle"),
+ kSignonBundle.getString("removeAllPasswordsPrompt"),
+ prompter.STD_YES_NO_BUTTONS + prompter.BUTTON_POS_1_DEFAULT,
+ null, null, null, null, dummy) == 1) // 1 == "No" button
+ return;
+
+ let filterSet = signonsTreeView._filterSet;
+ let syncNeeded = (filterSet.length != 0);
+ let view = signonsTreeView;
+ let table = filterSet.length ? filterSet : signons;
+
+ // remove all items from table and place in deleted table
+ for (let i = 0; i < table.length; i++) {
+ deletedSignons.push(table[i]);
+ }
+ table.length = 0;
+
+ // clear out selections
+ view.selection.select(-1);
+
+ // update the tree view and notify the tree
+ view.rowCount = 0;
+
+ let box = signonsTree.treeBoxObject;
+ box.rowCountChanged(0, -deletedSignons.length);
+ box.invalidate();
+
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ FinalizeSignonDeletions(syncNeeded);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED_ALL").add(1);
+}
+
+function TogglePasswordVisible() {
+ if (showingPasswords || masterPasswordLogin(AskUserShowPasswords)) {
+ showingPasswords = !showingPasswords;
+ togglePasswordsButton.label = kSignonBundle.getString(showingPasswords ? "hidePasswords" : "showPasswords");
+ togglePasswordsButton.accessKey = kSignonBundle.getString(showingPasswords ? "hidePasswordsAccessKey" : "showPasswordsAccessKey");
+ document.getElementById("passwordCol").hidden = !showingPasswords;
+ FilterPasswords();
+ }
+
+ // Notify observers that the password visibility toggling is
+ // completed. (Mostly useful for tests)
+ Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete", null);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_VISIBILITY_TOGGLED").add(showingPasswords);
+}
+
+function AskUserShowPasswords() {
+ let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
+ let dummy = { value: false };
+
+ // Confirm the user wants to display passwords
+ return prompter.confirmEx(window,
+ null,
+ kSignonBundle.getString("noMasterPasswordPrompt"), prompter.STD_YES_NO_BUTTONS,
+ null, null, null, null, dummy) == 0; // 0=="Yes" button
+}
+
+function FinalizeSignonDeletions(syncNeeded) {
+ for (let s = 0; s < deletedSignons.length; s++) {
+ Services.logins.removeLogin(deletedSignons[s]);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED").add(1);
+ }
+ // If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table.
+ // See bug 405389.
+ if (syncNeeded) {
+ try {
+ signons = Services.logins.getAllLogins();
+ } catch (e) {
+ signons = [];
+ }
+ }
+ deletedSignons.length = 0;
+}
+
+function HandleSignonKeyPress(e) {
+ // If editing is currently performed, don't do anything.
+ if (signonsTree.getAttribute("editing")) {
+ return;
+ }
+ if (e.keyCode == KeyboardEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE)) {
+ DeleteSignon();
+ }
+}
+
+function getColumnByName(column) {
+ switch (column) {
+ case "hostname":
+ return document.getElementById("siteCol");
+ case "username":
+ return document.getElementById("userCol");
+ case "password":
+ return document.getElementById("passwordCol");
+ case "timeCreated":
+ return document.getElementById("timeCreatedCol");
+ case "timeLastUsed":
+ return document.getElementById("timeLastUsedCol");
+ case "timePasswordChanged":
+ return document.getElementById("timePasswordChangedCol");
+ case "timesUsed":
+ return document.getElementById("timesUsedCol");
+ }
+ return undefined;
+}
+
+function SignonColumnSort(column) {
+ let sortedCol = getColumnByName(column);
+ let lastSortedCol = getColumnByName(lastSignonSortColumn);
+
+ // clear out the sortDirection attribute on the old column
+ lastSortedCol.removeAttribute("sortDirection");
+
+ // determine if sort is to be ascending or descending
+ lastSignonSortAscending = (column == lastSignonSortColumn) ? !lastSignonSortAscending : true;
+
+ // sort
+ lastSignonSortColumn = column;
+ SortTree(lastSignonSortColumn, lastSignonSortAscending);
+
+ // set the sortDirection attribute to get the styling going
+ // first we need to get the right element
+ sortedCol.setAttribute("sortDirection", lastSignonSortAscending ?
+ "ascending" : "descending");
+}
+
+function SignonClearFilter() {
+ let singleSelection = (signonsTreeView.selection.count == 1);
+
+ // Clear the Tree Display
+ signonsTreeView.rowCount = 0;
+ signonsTree.treeBoxObject.rowCountChanged(0, -signonsTreeView._filterSet.length);
+ signonsTreeView._filterSet = [];
+
+ // Just reload the list to make sure deletions are respected
+ LoadSignons();
+
+ // Restore selection
+ if (singleSelection) {
+ signonsTreeView.selection.clearSelection();
+ for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) {
+ let range = signonsTreeView._lastSelectedRanges[i];
+ signonsTreeView.selection.rangedSelect(range.min, range.max, true);
+ }
+ } else {
+ signonsTreeView.selection.select(0);
+ }
+ signonsTreeView._lastSelectedRanges = [];
+
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionAll");
+}
+
+function FocusFilterBox() {
+ if (filterField.getAttribute("focused") != "true") {
+ filterField.focus();
+ }
+}
+
+function SignonMatchesFilter(aSignon, aFilterValue) {
+ if (aSignon.hostname.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (aSignon.username &&
+ aSignon.username.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (aSignon.httpRealm &&
+ aSignon.httpRealm.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (showingPasswords && aSignon.password &&
+ aSignon.password.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+
+ return false;
+}
+
+function _filterPasswords(aFilterValue, view) {
+ aFilterValue = aFilterValue.toLowerCase();
+ return signons.filter(s => SignonMatchesFilter(s, aFilterValue));
+}
+
+function SignonSaveState() {
+ // Save selection
+ let seln = signonsTreeView.selection;
+ signonsTreeView._lastSelectedRanges = [];
+ let rangeCount = seln.getRangeCount();
+ for (let i = 0; i < rangeCount; ++i) {
+ let min = {}; let max = {};
+ seln.getRangeAt(i, min, max);
+ signonsTreeView._lastSelectedRanges.push({ min: min.value, max: max.value });
+ }
+}
+
+function FilterPasswords() {
+ if (filterField.value == "") {
+ SignonClearFilter();
+ return;
+ }
+
+ let newFilterSet = _filterPasswords(filterField.value, signonsTreeView);
+ if (!signonsTreeView._filterSet.length) {
+ // Save Display Info for the Non-Filtered mode when we first
+ // enter Filtered mode.
+ SignonSaveState();
+ }
+ signonsTreeView._filterSet = newFilterSet;
+
+ // Clear the display
+ let oldRowCount = signonsTreeView.rowCount;
+ signonsTreeView.rowCount = 0;
+ signonsTree.treeBoxObject.rowCountChanged(0, -oldRowCount);
+ // Set up the filtered display
+ signonsTreeView.rowCount = signonsTreeView._filterSet.length;
+ signonsTree.treeBoxObject.rowCountChanged(0, signonsTreeView.rowCount);
+
+ // if the view is not empty then select the first item
+ if (signonsTreeView.rowCount > 0)
+ signonsTreeView.selection.select(0);
+
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionFiltered");
+}
+
+function CopyPassword() {
+ // Don't copy passwords if we aren't already showing the passwords & a master
+ // password hasn't been entered.
+ if (!showingPasswords && !masterPasswordLogin())
+ return;
+ // Copy selected signon's password to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ let row = signonsTree.currentIndex;
+ let password = signonsTreeView.getCellText(row, {id : "passwordCol" });
+ clipboard.copyString(password);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_PASSWORD").add(1);
+}
+
+function CopyUsername() {
+ // Copy selected signon's username to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+ let row = signonsTree.currentIndex;
+ let username = signonsTreeView.getCellText(row, {id : "userCol" });
+ clipboard.copyString(username);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_USERNAME").add(1);
+}
+
+function EditCellInSelectedRow(columnName) {
+ let row = signonsTree.currentIndex;
+ let columnElement = getColumnByName(columnName);
+ signonsTree.startEditing(row, signonsTree.columns.getColumnFor(columnElement));
+}
+
+function UpdateContextMenu() {
+ let singleSelection = (signonsTreeView.selection.count == 1);
+ let menuItems = new Map();
+ let menupopup = document.getElementById("signonsTreeContextMenu");
+ for (let menuItem of menupopup.querySelectorAll("menuitem")) {
+ menuItems.set(menuItem.id, menuItem);
+ }
+
+ if (!singleSelection) {
+ for (let menuItem of menuItems.values()) {
+ menuItem.setAttribute("disabled", "true");
+ }
+ return;
+ }
+
+ let selectedRow = signonsTree.currentIndex;
+
+ // Disable "Copy Username" if the username is empty.
+ if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") {
+ menuItems.get("context-copyusername").removeAttribute("disabled");
+ } else {
+ menuItems.get("context-copyusername").setAttribute("disabled", "true");
+ }
+
+ menuItems.get("context-editusername").removeAttribute("disabled");
+ menuItems.get("context-copypassword").removeAttribute("disabled");
+
+ // Disable "Edit Password" if the password column isn't showing.
+ if (!document.getElementById("passwordCol").hidden) {
+ menuItems.get("context-editpassword").removeAttribute("disabled");
+ } else {
+ menuItems.get("context-editpassword").setAttribute("disabled", "true");
+ }
+}
+
+function masterPasswordLogin(noPasswordCallback) {
+ // 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 noPasswordCallback ? noPasswordCallback() : true;
+
+ // 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();
+}
+
+function escapeKeyHandler() {
+ // If editing is currently performed, don't do anything.
+ if (signonsTree.getAttribute("editing")) {
+ return;
+ }
+ window.close();
+}
+
+function OpenMigrator() {
+ const { MigrationUtils } = Cu.import("resource:///modules/MigrationUtils.jsm", {});
+ // We pass in the type of source we're using for use in telemetry:
+ MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_PASSWORDS]);
+}
diff --git a/toolkit/components/passwordmgr/content/passwordManager.xul b/toolkit/components/passwordmgr/content/passwordManager.xul
new file mode 100644
index 0000000000..d248283b67
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -0,0 +1,134 @@
+<?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/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/passwordmgr.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://passwordmgr/locale/passwordManager.dtd" >
+
+<window id="SignonViewerDialog"
+ windowtype="Toolkit:PasswordManager"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="Startup();"
+ onunload="Shutdown();"
+ title="&savedLogins.title;"
+ style="width: 45em;"
+ persist="width height screenX screenY">
+
+ <script type="application/javascript" src="chrome://passwordmgr/content/passwordManager.js"/>
+
+ <stringbundle id="signonBundle"
+ src="chrome://passwordmgr/locale/passwordmgr.properties"/>
+
+ <keyset>
+ <key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();"/>
+ <key key="&windowClose.key;" modifiers="accel" oncommand="escapeKeyHandler();"/>
+ <key key="&focusSearch1.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
+ <key key="&focusSearch2.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
+ </keyset>
+
+ <popupset id="signonsTreeContextSet">
+ <menupopup id="signonsTreeContextMenu"
+ onpopupshowing="UpdateContextMenu()">
+ <menuitem id="context-copyusername"
+ label="&copyUsernameCmd.label;"
+ accesskey="&copyUsernameCmd.accesskey;"
+ oncommand="CopyUsername()"/>
+ <menuitem id="context-editusername"
+ label="&editUsernameCmd.label;"
+ accesskey="&editUsernameCmd.accesskey;"
+ oncommand="EditCellInSelectedRow('username')"/>
+ <menuseparator/>
+ <menuitem id="context-copypassword"
+ label="&copyPasswordCmd.label;"
+ accesskey="&copyPasswordCmd.accesskey;"
+ oncommand="CopyPassword()"/>
+ <menuitem id="context-editpassword"
+ label="&editPasswordCmd.label;"
+ accesskey="&editPasswordCmd.accesskey;"
+ oncommand="EditCellInSelectedRow('password')"/>
+ </menupopup>
+ </popupset>
+
+ <!-- saved signons -->
+ <vbox id="savedsignons" class="contentPane" flex="1">
+ <!-- filter -->
+ <hbox align="center">
+ <label accesskey="&filter.accesskey;" control="filter">&filter.label;</label>
+ <textbox id="filter" flex="1" type="search"
+ aria-controls="signonsTree"
+ oncommand="FilterPasswords();"/>
+ </hbox>
+
+ <label control="signonsTree" id="signonsIntro"/>
+ <separator class="thin"/>
+ <tree id="signonsTree" flex="1"
+ width="750"
+ style="height: 20em;"
+ onkeypress="HandleSignonKeyPress(event)"
+ onselect="SignonSelected();"
+ editable="true"
+ context="signonsTreeContextMenu">
+ <treecols>
+ <treecol id="siteCol" label="&treehead.site.label;" flex="40"
+ data-field-name="hostname" persist="width"
+ ignoreincolumnpicker="true"
+ sortDirection="ascending"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="userCol" label="&treehead.username.label;" flex="25"
+ ignoreincolumnpicker="true"
+ data-field-name="username" persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="passwordCol" label="&treehead.password.label;" flex="15"
+ ignoreincolumnpicker="true"
+ data-field-name="password" persist="width"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timeCreatedCol" label="&treehead.timeCreated.label;" flex="10"
+ data-field-name="timeCreated" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timeLastUsedCol" label="&treehead.timeLastUsed.label;" flex="20"
+ data-field-name="timeLastUsed" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timePasswordChangedCol" label="&treehead.timePasswordChanged.label;" flex="10"
+ data-field-name="timePasswordChanged" persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timesUsedCol" label="&treehead.timesUsed.label;" flex="1"
+ data-field-name="timesUsed" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <separator class="thin"/>
+ <hbox id="SignonViewerButtons">
+ <button id="removeSignon" disabled="true" icon="remove"
+ label="&remove.label;" accesskey="&remove.accesskey;"
+ oncommand="DeleteSignon();"/>
+ <button id="removeAllSignons" icon="clear"
+ label="&removeall.label;" accesskey="&removeall.accesskey;"
+ oncommand="DeleteAllSignons();"/>
+ <spacer flex="1"/>
+#if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(XP_WIN)
+ <button accesskey="&import.accesskey;"
+ label="&import.label;"
+ oncommand="OpenMigrator();"/>
+#endif
+ <button id="togglePasswords"
+ oncommand="TogglePasswordVisible();"/>
+ </hbox>
+ </vbox>
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <spacer flex="1"/>
+#ifndef XP_MACOSX
+ <button oncommand="close();" icon="close"
+ label="&closebutton.label;" accesskey="&closebutton.accesskey;"/>
+#endif
+ </hbox>
+ </hbox>
+</window>
diff --git a/toolkit/components/passwordmgr/content/recipes.json b/toolkit/components/passwordmgr/content/recipes.json
new file mode 100644
index 0000000000..fc747219b4
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/recipes.json
@@ -0,0 +1,31 @@
+{
+ "siteRecipes": [
+ {
+ "description": "okta uses a hidden password field to disable filling",
+ "hosts": ["mozilla.okta.com"],
+ "passwordSelector": "#pass-signin"
+ },
+ {
+ "description": "anthem uses a hidden password and username field to disable filling",
+ "hosts": ["www.anthem.com"],
+ "passwordSelector": "#LoginContent_txtLoginPass"
+ },
+ {
+ "description": "An ephemeral password-shim field is incorrectly selected as the username field.",
+ "hosts": ["www.discover.com"],
+ "usernameSelector": "#login-account"
+ },
+ {
+ "description": "Tibia uses type=password for its username field and puts the email address before the password field during registration",
+ "hosts": ["secure.tibia.com"],
+ "usernameSelector": "#accountname, input[name='loginname']",
+ "passwordSelector": "#password1, input[name='loginpassword']",
+ "pathRegex": "^\/account\/"
+ },
+ {
+ "description": "Username field will be incorrectly captured in the change password form (bug 1243722)",
+ "hosts": ["www.facebook.com"],
+ "notUsernameSelector": "#password_strength"
+ }
+ ]
+}
diff --git a/toolkit/components/passwordmgr/crypto-SDR.js b/toolkit/components/passwordmgr/crypto-SDR.js
new file mode 100644
index 0000000000..b0916eb291
--- /dev/null
+++ b/toolkit/components/passwordmgr/crypto-SDR.js
@@ -0,0 +1,207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+function LoginManagerCrypto_SDR() {
+ this.init();
+}
+
+LoginManagerCrypto_SDR.prototype = {
+
+ classID : Components.ID("{dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerCrypto]),
+
+ __sdrSlot : null, // PKCS#11 slot being used by the SDR.
+ get _sdrSlot() {
+ if (!this.__sdrSlot) {
+ let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"].
+ getService(Ci.nsIPKCS11ModuleDB);
+ this.__sdrSlot = modules.findSlotByName("");
+ }
+ return this.__sdrSlot;
+ },
+
+ __decoderRing : null, // nsSecretDecoderRing service
+ get _decoderRing() {
+ if (!this.__decoderRing)
+ this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
+ getService(Ci.nsISecretDecoderRing);
+ return this.__decoderRing;
+ },
+
+ __utfConverter : null, // UCS2 <--> UTF8 string conversion
+ get _utfConverter() {
+ if (!this.__utfConverter) {
+ this.__utfConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ this.__utfConverter.charset = "UTF-8";
+ }
+ return this.__utfConverter;
+ },
+
+ _utfConverterReset : function() {
+ this.__utfConverter = null;
+ },
+
+ _uiBusy : false,
+
+
+ init : function () {
+ // Check to see if the internal PKCS#11 token has been initialized.
+ // If not, set a blank password.
+ let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
+ getService(Ci.nsIPK11TokenDB);
+
+ let token = tokenDB.getInternalKeyToken();
+ if (token.needsUserInit) {
+ this.log("Initializing key3.db with default blank password.");
+ token.initPassword("");
+ }
+ },
+
+
+ /*
+ * encrypt
+ *
+ * Encrypts the specified string, using the SecretDecoderRing.
+ *
+ * Returns the encrypted string, or throws an exception if there was a
+ * problem.
+ */
+ encrypt : function (plainText) {
+ let cipherText = null;
+
+ let wasLoggedIn = this.isLoggedIn;
+ let canceledMP = false;
+
+ this._uiBusy = true;
+ try {
+ let plainOctet = this._utfConverter.ConvertFromUnicode(plainText);
+ plainOctet += this._utfConverter.Finish();
+ cipherText = this._decoderRing.encryptString(plainOctet);
+ } catch (e) {
+ this.log("Failed to encrypt string. (" + e.name + ")");
+ // If the user clicks Cancel, we get NS_ERROR_FAILURE.
+ // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
+ if (e.result == Cr.NS_ERROR_FAILURE) {
+ canceledMP = true;
+ throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ } else {
+ throw Components.Exception("Couldn't encrypt string", Cr.NS_ERROR_FAILURE);
+ }
+ } finally {
+ this._uiBusy = false;
+ // If we triggered a master password prompt, notify observers.
+ if (!wasLoggedIn && this.isLoggedIn)
+ this._notifyObservers("passwordmgr-crypto-login");
+ else if (canceledMP)
+ this._notifyObservers("passwordmgr-crypto-loginCanceled");
+ }
+ return cipherText;
+ },
+
+
+ /*
+ * decrypt
+ *
+ * Decrypts the specified string, using the SecretDecoderRing.
+ *
+ * Returns the decrypted string, or throws an exception if there was a
+ * problem.
+ */
+ decrypt : function (cipherText) {
+ let plainText = null;
+
+ let wasLoggedIn = this.isLoggedIn;
+ let canceledMP = false;
+
+ this._uiBusy = true;
+ try {
+ let plainOctet;
+ plainOctet = this._decoderRing.decryptString(cipherText);
+ plainText = this._utfConverter.ConvertToUnicode(plainOctet);
+ } catch (e) {
+ this.log("Failed to decrypt string: " + cipherText +
+ " (" + e.name + ")");
+
+ // In the unlikely event the converter threw, reset it.
+ this._utfConverterReset();
+
+ // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
+ // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
+ // Wrong passwords are handled by the decoderRing reprompting;
+ // we get no notification.
+ if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ canceledMP = true;
+ throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ } else {
+ throw Components.Exception("Couldn't decrypt string", Cr.NS_ERROR_FAILURE);
+ }
+ } finally {
+ this._uiBusy = false;
+ // If we triggered a master password prompt, notify observers.
+ if (!wasLoggedIn && this.isLoggedIn)
+ this._notifyObservers("passwordmgr-crypto-login");
+ else if (canceledMP)
+ this._notifyObservers("passwordmgr-crypto-loginCanceled");
+ }
+
+ return plainText;
+ },
+
+
+ /*
+ * uiBusy
+ */
+ get uiBusy() {
+ return this._uiBusy;
+ },
+
+
+ /*
+ * isLoggedIn
+ */
+ get isLoggedIn() {
+ let status = this._sdrSlot.status;
+ this.log("SDR slot status is " + status);
+ if (status == Ci.nsIPKCS11Slot.SLOT_READY ||
+ status == Ci.nsIPKCS11Slot.SLOT_LOGGED_IN)
+ return true;
+ if (status == Ci.nsIPKCS11Slot.SLOT_NOT_LOGGED_IN)
+ return false;
+ throw Components.Exception("unexpected slot status: " + status, Cr.NS_ERROR_FAILURE);
+ },
+
+
+ /*
+ * defaultEncType
+ */
+ get defaultEncType() {
+ return Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
+ },
+
+
+ /*
+ * _notifyObservers
+ */
+ _notifyObservers : function(topic) {
+ this.log("Prompted for a master password, notifying for " + topic);
+ Services.obs.notifyObservers(null, topic, null);
+ },
+}; // end of nsLoginManagerCrypto_SDR implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerCrypto_SDR.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login crypto");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerCrypto_SDR];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/passwordmgr/jar.mn b/toolkit/components/passwordmgr/jar.mn
new file mode 100644
index 0000000000..9fa574e493
--- /dev/null
+++ b/toolkit/components/passwordmgr/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 passwordmgr %content/passwordmgr/
+* content/passwordmgr/passwordManager.xul (content/passwordManager.xul)
+ content/passwordmgr/passwordManager.js (content/passwordManager.js)
+ content/passwordmgr/recipes.json (content/recipes.json)
diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build
new file mode 100644
index 0000000000..72c8c70a47
--- /dev/null
+++ b/toolkit/components/passwordmgr/moz.build
@@ -0,0 +1,78 @@
+# -*- 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/.
+
+if CONFIG['MOZ_BUILD_APP'] == 'browser':
+ DEFINES['MOZ_BUILD_APP_IS_BROWSER'] = True
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini', 'test/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+TESTING_JS_MODULES += [
+ # Make this file available from the "resource:" URI of the test environment.
+ 'test/browser/form_basic.html',
+ 'test/LoginTestUtils.jsm',
+]
+
+XPIDL_SOURCES += [
+ 'nsILoginInfo.idl',
+ 'nsILoginManager.idl',
+ 'nsILoginManagerCrypto.idl',
+ 'nsILoginManagerPrompter.idl',
+ 'nsILoginManagerStorage.idl',
+ 'nsILoginMetaInfo.idl',
+]
+
+XPIDL_MODULE = 'loginmgr'
+
+EXTRA_COMPONENTS += [
+ 'crypto-SDR.js',
+ 'nsLoginInfo.js',
+ 'nsLoginManager.js',
+ 'nsLoginManagerPrompter.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'passwordmgr.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'InsecurePasswordUtils.jsm',
+ 'LoginHelper.jsm',
+ 'LoginManagerContent.jsm',
+ 'LoginManagerParent.jsm',
+ 'LoginRecipes.jsm',
+ 'OSCrypto.jsm',
+]
+
+if CONFIG['OS_TARGET'] == 'Android':
+ EXTRA_COMPONENTS += [
+ 'storage-mozStorage.js',
+ ]
+else:
+ EXTRA_COMPONENTS += [
+ 'storage-json.js',
+ ]
+ EXTRA_JS_MODULES += [
+ 'LoginImport.jsm',
+ 'LoginStore.jsm',
+ ]
+
+if CONFIG['OS_TARGET'] == 'WINNT':
+ EXTRA_JS_MODULES += [
+ 'OSCrypto_win.js',
+ ]
+
+if CONFIG['MOZ_BUILD_APP'] == 'browser':
+ EXTRA_JS_MODULES += [
+ 'LoginManagerContextMenu.jsm',
+ ]
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Password Manager')
diff --git a/toolkit/components/passwordmgr/nsILoginInfo.idl b/toolkit/components/passwordmgr/nsILoginInfo.idl
new file mode 100644
index 0000000000..7dce9033df
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginInfo.idl
@@ -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/. */
+
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(c41b7dff-6b9b-42fe-b78d-113051facb05)]
+
+/**
+ * An object containing information for a login stored by the
+ * password manager.
+ */
+interface nsILoginInfo : nsISupports {
+ /**
+ * The hostname the login applies to.
+ *
+ * The hostname should be formatted as an URL. For example,
+ * "https://site.com", "http://site.com:1234", "ftp://ftp.site.com".
+ */
+ attribute AString hostname;
+
+ /**
+ * The URL a form-based login was submitted to.
+ *
+ * For logins obtained from HTML forms, this field is the |action|
+ * attribute from the |form| element, with the path removed. For
+ * example "http://www.site.com". [Forms with no |action| attribute
+ * default to submitting to their origin URL, so we store that.]
+ *
+ * For logins obtained from a HTTP or FTP protocol authentication,
+ * this field is NULL.
+ */
+ attribute AString formSubmitURL;
+
+ /**
+ * The HTTP Realm a login was requested for.
+ *
+ * When an HTTP server sends a 401 result, the WWW-Authenticate
+ * header includes a realm to identify the "protection space." See
+ * RFC2617. If the response sent has a missing or blank realm, the
+ * hostname is used instead.
+ *
+ * For logins obtained from HTML forms, this field is NULL.
+ */
+ attribute AString httpRealm;
+
+ /**
+ * The username for the login.
+ */
+ attribute AString username;
+
+ /**
+ * The |name| attribute for the username input field.
+ *
+ * For logins obtained from a HTTP or FTP protocol authentication,
+ * this field is an empty string.
+ */
+ attribute AString usernameField;
+
+ /**
+ * The password for the login.
+ */
+ attribute AString password;
+
+ /**
+ * The |name| attribute for the password input field.
+ *
+ * For logins obtained from a HTTP or FTP protocol authentication,
+ * this field is an empty string.
+ */
+ attribute AString passwordField;
+
+ /**
+ * Initialize a newly created nsLoginInfo object.
+ *
+ * The arguments are the fields for the new object.
+ */
+ void init(in AString aHostname,
+ in AString aFormSubmitURL, in AString aHttpRealm,
+ in AString aUsername, in AString aPassword,
+ in AString aUsernameField, in AString aPasswordField);
+
+ /**
+ * Test for strict equality with another nsILoginInfo object.
+ *
+ * @param aLoginInfo
+ * The other object to test.
+ */
+ boolean equals(in nsILoginInfo aLoginInfo);
+
+ /**
+ * Test for loose equivalency with another nsILoginInfo object. The
+ * passwordField and usernameField values are ignored, and the password
+ * values may be optionally ignored. If one login's formSubmitURL is an
+ * empty string (but not null), it will be treated as a wildcard. [The
+ * blank value indicates the login was stored before bug 360493 was fixed.]
+ *
+ * @param aLoginInfo
+ * The other object to test.
+ * @param ignorePassword
+ * If true, ignore the password when checking for match.
+ */
+ boolean matches(in nsILoginInfo aLoginInfo, in boolean ignorePassword);
+
+ /**
+ * Create an identical copy of the login, duplicating all of the login's
+ * nsILoginInfo and nsILoginMetaInfo properties.
+ *
+ * This allows code to be forwards-compatible, when additional properties
+ * are added to nsILoginMetaInfo (or nsILoginInfo) in the future.
+ */
+ nsILoginInfo clone();
+};
+
+%{C++
+
+#define NS_LOGININFO_CONTRACTID "@mozilla.org/login-manager/loginInfo;1"
+
+%}
diff --git a/toolkit/components/passwordmgr/nsILoginManager.idl b/toolkit/components/passwordmgr/nsILoginManager.idl
new file mode 100644
index 0000000000..30b5a0449d
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManager.idl
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsILoginInfo;
+interface nsIAutoCompleteResult;
+interface nsIFormAutoCompleteObserver;
+interface nsIDOMHTMLInputElement;
+interface nsIDOMHTMLFormElement;
+interface nsIPropertyBag;
+
+[scriptable, uuid(38c7f6af-7df9-49c7-b558-2776b24e6cc1)]
+interface nsILoginManager : nsISupports {
+ /**
+ * This promise is resolved when initialization is complete, and is rejected
+ * in case initialization failed. This includes the initial loading of the
+ * login data as well as any migration from previous versions.
+ *
+ * Calling any method of nsILoginManager before this promise is resolved
+ * might trigger the synchronous initialization fallback.
+ */
+ readonly attribute jsval initializationPromise;
+
+
+ /**
+ * Store a new login in the login manager.
+ *
+ * @param aLogin
+ * The login to be added.
+ * @return a clone of the login info with the guid set (even if it was not provided)
+ *
+ * Default values for the login's nsILoginMetaInfo properties will be
+ * created. However, if the caller specifies non-default values, they will
+ * be used instead.
+ */
+ nsILoginInfo addLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Remove a login from the login manager.
+ *
+ * @param aLogin
+ * The login to be removed.
+ *
+ * The specified login must exactly match a stored login. However, the
+ * values of any nsILoginMetaInfo properties are ignored.
+ */
+ void removeLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Modify an existing login in the login manager.
+ *
+ * @param oldLogin
+ * The login to be modified.
+ * @param newLoginData
+ * The new login values (either a nsILoginInfo or nsIProperyBag)
+ *
+ * If newLoginData is a nsILoginInfo, all of the old login's nsILoginInfo
+ * properties are changed to the values from newLoginData (but the old
+ * login's nsILoginMetaInfo properties are unmodified).
+ *
+ * If newLoginData is a nsIPropertyBag, only the specified properties
+ * will be changed. The nsILoginMetaInfo properties of oldLogin can be
+ * changed in this manner.
+ *
+ * If the propertybag contains an item named "timesUsedIncrement", the
+ * login's timesUsed property will be incremented by the item's value.
+ */
+ void modifyLogin(in nsILoginInfo oldLogin, in nsISupports newLoginData);
+
+
+ /**
+ * Remove all logins known to login manager.
+ *
+ * The browser sanitization feature allows the user to clear any stored
+ * passwords. This interface allows that to be done without getting each
+ * login first (which might require knowing the master password).
+ *
+ */
+ void removeAllLogins();
+
+
+ /**
+ * Fetch all logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property and omit this param.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.getAllLogins();
+ * (|logins| is an array).
+ */
+ void getAllLogins([optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Obtain a list of all hosts for which password saving is disabled.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property and omit this param.
+ * @param hostnames
+ * An array of hostname strings, in origin URL format without a
+ * pathname. For example: "https://www.site.com".
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.getDisabledAllLogins();
+ */
+ void getAllDisabledHosts([optional] out unsigned long count,
+ [retval, array, size_is(count)] out wstring hostnames);
+
+
+ /**
+ * Check to see if saving logins has been disabled for a host.
+ *
+ * @param aHost
+ * The hostname to check. This argument should be in the origin
+ * URL format, without a pathname. For example: "http://foo.com".
+ */
+ boolean getLoginSavingEnabled(in AString aHost);
+
+
+ /**
+ * Disable (or enable) storing logins for the specified host. When
+ * disabled, the login manager will not prompt to store logins for
+ * that host. Existing logins are not affected.
+ *
+ * @param aHost
+ * The hostname to set. This argument should be in the origin
+ * URL format, without a pathname. For example: "http://foo.com".
+ * @param isEnabled
+ * Specify if saving logins should be enabled (true) or
+ * disabled (false)
+ */
+ void setLoginSavingEnabled(in AString aHost, in boolean isEnabled);
+
+
+ /**
+ * Search for logins matching the specified criteria. Called when looking
+ * for logins that might be applicable to a form or authentication request.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |findLogins({}, hostname, ...)|
+ * @param aHostname
+ * The hostname to restrict searches to, in URL format. For
+ * example: "http://www.site.com".
+ * To find logins for a given nsIURI, you would typically pass in
+ * its prePath.
+ * @param aActionURL
+ * For form logins, this argument should be the URL to which the
+ * form will be submitted. For protocol logins, specify null.
+ * An empty string ("") will match any value (except null).
+ * @param aHttpRealm
+ * For protocol logins, this argument should be the HTTP Realm
+ * for which the login applies. This is obtained from the
+ * WWW-Authenticate header. See RFC2617. For form logins,
+ * specify null.
+ * An empty string ("") will match any value (except null).
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.findLogins({}, hostname, ...);
+ *
+ */
+ void findLogins(out unsigned long count, in AString aHostname,
+ in AString aActionURL, in AString aHttpRealm,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins matching the specified criteria, as with
+ * findLogins(). This interface only returns the number of matching
+ * logins (and not the logins themselves), which allows a caller to
+ * check for logins without causing the user to be prompted for a master
+ * password to decrypt the logins.
+ *
+ * @param aHostname
+ * The hostname to restrict searches to. Specify an empty string
+ * to match all hosts. A null value will not match any logins, and
+ * will thus always return a count of 0.
+ * @param aActionURL
+ * The URL to which a form login will be submitted. To match any
+ * form login, specify an empty string. To not match any form
+ * login, specify null.
+ * @param aHttpRealm
+ * The HTTP Realm for which the login applies. To match logins for
+ * any realm, specify an empty string. To not match logins for any
+ * realm, specify null.
+ */
+ unsigned long countLogins(in AString aHostname, in AString aActionURL,
+ in AString aHttpRealm);
+
+
+ /**
+ * Generate results for a userfield autocomplete menu.
+ *
+ * NOTE: This interface is provided for use only by the FormFillController,
+ * which calls it directly. This isn't really ideal, it should
+ * probably be callback registered through the FFC.
+ */
+ void autoCompleteSearchAsync(in AString aSearchString,
+ in nsIAutoCompleteResult aPreviousResult,
+ in nsIDOMHTMLInputElement aElement,
+ in nsIFormAutoCompleteObserver aListener);
+
+ /**
+ * Stop a previously-started async search.
+ */
+ void stopSearch();
+
+ /**
+ * Search for logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |searchLogins({}, matchData)|
+ * @param matchData
+ * The data used to search. This does not follow the same
+ * requirements as findLogins for those fields. Wildcard matches are
+ * simply not specified.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.searchLogins({}, matchData);
+ * (|logins| is an array).
+ */
+ void searchLogins(out unsigned long count, in nsIPropertyBag matchData,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+ /**
+ * True when a master password prompt is being displayed.
+ */
+ readonly attribute boolean uiBusy;
+
+ /**
+ * True when the master password has already been entered, and so a caller
+ * can ask for decrypted logins without triggering a prompt.
+ */
+ readonly attribute boolean isLoggedIn;
+};
+
+%{C++
+
+#define NS_LOGINMANAGER_CONTRACTID "@mozilla.org/login-manager;1"
+
+%}
diff --git a/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl b/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
new file mode 100644
index 0000000000..8af36a258b
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManagerCrypto.idl
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+[scriptable, uuid(2030770e-542e-40cd-8061-cd9d4ad4227f)]
+
+interface nsILoginManagerCrypto : nsISupports {
+
+ const unsigned long ENCTYPE_BASE64 = 0; // obsolete
+ const unsigned long ENCTYPE_SDR = 1;
+
+ /**
+ * encrypt
+ *
+ * @param plainText
+ * The string to be encrypted.
+ *
+ * Encrypts the specified string, returning the ciphertext value.
+ *
+ * NOTE: The current implemention of this inferface simply uses NSS/PSM's
+ * "Secret Decoder Ring" service. It is not recommended for general
+ * purpose encryption/decryption.
+ *
+ * Can throw if the user cancels entry of their master password.
+ */
+ AString encrypt(in AString plainText);
+
+ /**
+ * decrypt
+ *
+ * @param cipherText
+ * The string to be decrypted.
+ *
+ * Decrypts the specified string, returning the plaintext value.
+ *
+ * Can throw if the user cancels entry of their master password, or if the
+ * cipherText value can not be successfully decrypted (eg, if it was
+ * encrypted with some other key).
+ */
+ AString decrypt(in AString cipherText);
+
+ /**
+ * uiBusy
+ *
+ * True when a master password prompt is being displayed.
+ */
+ readonly attribute boolean uiBusy;
+
+ /**
+ * isLoggedIn
+ *
+ * Current login state of the token used for encryption. If the user is
+ * not logged in, performing a crypto operation will result in a master
+ * password prompt.
+ */
+ readonly attribute boolean isLoggedIn;
+
+ /**
+ * defaultEncType
+ *
+ * Default encryption type used by an implementation of this interface.
+ */
+ readonly attribute unsigned long defaultEncType;
+};
diff --git a/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl b/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
new file mode 100644
index 0000000000..c673154d17
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManagerPrompter.idl
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsILoginInfo;
+interface nsIDOMElement;
+interface nsIDOMWindow;
+
+[scriptable, uuid(425f73b9-b2db-4e8a-88c5-9ac2512934ce)]
+interface nsILoginManagerPrompter : nsISupports {
+ /**
+ * Initialize the prompter. Must be called before using other interfaces.
+ *
+ * @param aWindow
+ * The window in which the user is doing some login-related action that's
+ * resulting in a need to prompt them for something. The prompt
+ * will be associated with this window (or, if a notification bar
+ * is being used, topmost opener in some cases).
+ *
+ * aWindow can be null if there is no associated window, e.g. in a JSM
+ * or Sandbox. In this case there will be no checkbox to save the login
+ * since the window is needed to know if this is a private context.
+ *
+ * If this window is a content window, the corresponding window and browser
+ * elements will be calculated. If this window is a chrome window, the
+ * corresponding browser element needs to be set using setBrowser.
+ */
+ void init(in nsIDOMWindow aWindow);
+
+ /**
+ * The browser this prompter is being created for.
+ * This is required if the init function received a chrome window as argument.
+ */
+ attribute nsIDOMElement browser;
+
+ /**
+ * The opener that was used to open the window passed to init.
+ * The opener can be used to determine in which window the prompt
+ * should be shown. Must be a content window that is not a frame window,
+ * make sure to pass the top window using e.g. window.top.
+ */
+ attribute nsIDOMWindow opener;
+
+ /**
+ * Ask the user if they want to save a login (Yes, Never, Not Now)
+ *
+ * @param aLogin
+ * The login to be saved.
+ */
+ void promptToSavePassword(in nsILoginInfo aLogin);
+
+ /**
+ * Ask the user if they want to change a login's password or username.
+ * If the user consents, modifyLogin() will be called.
+ *
+ * @param aOldLogin
+ * The existing login (with the old password).
+ * @param aNewLogin
+ * The new login.
+ */
+ void promptToChangePassword(in nsILoginInfo aOldLogin,
+ in nsILoginInfo aNewLogin);
+
+ /**
+ * Ask the user if they want to change the password for one of
+ * multiple logins, when the caller can't determine exactly which
+ * login should be changed. If the user consents, modifyLogin() will
+ * be called.
+ *
+ * @param logins
+ * An array of existing logins.
+ * @param count
+ * (length of the array)
+ * @param aNewLogin
+ * The new login.
+ *
+ * Note: Because the caller does not know the username of the login
+ * to be changed, aNewLogin.username and aNewLogin.usernameField
+ * will be set (using the user's selection) before modifyLogin()
+ * is called.
+ */
+ void promptToChangePasswordWithUsernames(
+ [array, size_is(count)] in nsILoginInfo logins,
+ in uint32_t count,
+ in nsILoginInfo aNewLogin);
+};
+%{C++
+
+#define NS_LOGINMANAGERPROMPTER_CONTRACTID "@mozilla.org/login-manager/prompter/;1"
+
+%}
diff --git a/toolkit/components/passwordmgr/nsILoginManagerStorage.idl b/toolkit/components/passwordmgr/nsILoginManagerStorage.idl
new file mode 100644
index 0000000000..4ad3dbfe9a
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginManagerStorage.idl
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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;
+interface nsILoginInfo;
+interface nsIPropertyBag;
+
+[scriptable, uuid(5df81a93-25e6-4b45-a696-089479e15c7d)]
+
+/*
+ * NOTE: This interface is intended to be implemented by modules
+ * providing storage mechanisms for the login manager.
+ * Other code should use the login manager's interfaces
+ * (nsILoginManager), and should not call storage modules
+ * directly.
+ */
+interface nsILoginManagerStorage : nsISupports {
+ /**
+ * Initialize the component.
+ *
+ * At present, other methods of this interface may be called before the
+ * returned promise is resolved or rejected.
+ *
+ * @return {Promise}
+ * @resolves When initialization is complete.
+ * @rejects JavaScript exception.
+ */
+ jsval initialize();
+
+
+ /**
+ * Ensures that all data has been written to disk and all files are closed.
+ *
+ * At present, this method is called by regression tests only. Finalization
+ * on shutdown is done by observers within the component.
+ *
+ * @return {Promise}
+ * @resolves When finalization is complete.
+ * @rejects JavaScript exception.
+ */
+ jsval terminate();
+
+
+ /**
+ * Store a new login in the storage module.
+ *
+ * @param aLogin
+ * The login to be added.
+ * @return a clone of the login info with the guid set (even if it was not provided).
+ *
+ * Default values for the login's nsILoginMetaInfo properties will be
+ * created. However, if the caller specifies non-default values, they will
+ * be used instead.
+ */
+ nsILoginInfo addLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Remove a login from the storage module.
+ *
+ * @param aLogin
+ * The login to be removed.
+ *
+ * The specified login must exactly match a stored login. However, the
+ * values of any nsILoginMetaInfo properties are ignored.
+ */
+ void removeLogin(in nsILoginInfo aLogin);
+
+
+ /**
+ * Modify an existing login in the storage module.
+ *
+ * @param oldLogin
+ * The login to be modified.
+ * @param newLoginData
+ * The new login values (either a nsILoginInfo or nsIProperyBag)
+ *
+ * If newLoginData is a nsILoginInfo, all of the old login's nsILoginInfo
+ * properties are changed to the values from newLoginData (but the old
+ * login's nsILoginMetaInfo properties are unmodified).
+ *
+ * If newLoginData is a nsIPropertyBag, only the specified properties
+ * will be changed. The nsILoginMetaInfo properties of oldLogin can be
+ * changed in this manner.
+ *
+ * If the propertybag contains an item named "timesUsedIncrement", the
+ * login's timesUsed property will be incremented by the item's value.
+ */
+ void modifyLogin(in nsILoginInfo oldLogin, in nsISupports newLoginData);
+
+
+ /**
+ * Remove all stored logins.
+ *
+ * The browser sanitization feature allows the user to clear any stored
+ * passwords. This interface allows that to be done without getting each
+ * login first (which might require knowing the master password).
+ *
+ */
+ void removeAllLogins();
+
+
+ /**
+ * Fetch all logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property and omit this param.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.getAllLogins();
+ * (|logins| is an array).
+ */
+ void getAllLogins([optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins in the login manager. An array is always returned;
+ * if there are no logins the array is empty.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |searchLogins({}, matchData)|
+ * @param matchData
+ * The data used to search. This does not follow the same
+ * requirements as findLogins for those fields. Wildcard matches are
+ * simply not specified.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.searchLogins({}, matchData);
+ * (|logins| is an array).
+ */
+ void searchLogins(out unsigned long count, in nsIPropertyBag matchData,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins matching the specified criteria. Called when looking
+ * for logins that might be applicable to a form or authentication request.
+ *
+ * @param count
+ * The number of elements in the array. JS callers can simply use
+ * the array's .length property, and supply an dummy object for
+ * this out param. For example: |findLogins({}, hostname, ...)|
+ * @param aHostname
+ * The hostname to restrict searches to, in URL format. For
+ * example: "http://www.site.com".
+ * @param aActionURL
+ * For form logins, this argument should be the URL to which the
+ * form will be submitted. For protocol logins, specify null.
+ * @param aHttpRealm
+ * For protocol logins, this argument should be the HTTP Realm
+ * for which the login applies. This is obtained from the
+ * WWW-Authenticate header. See RFC2617. For form logins,
+ * specify null.
+ * @param logins
+ * An array of nsILoginInfo objects.
+ *
+ * NOTE: This can be called from JS as:
+ * var logins = pwmgr.findLogins({}, hostname, ...);
+ *
+ */
+ void findLogins(out unsigned long count, in AString aHostname,
+ in AString aActionURL, in AString aHttpRealm,
+ [retval, array, size_is(count)] out nsILoginInfo logins);
+
+
+ /**
+ * Search for logins matching the specified criteria, as with
+ * findLogins(). This interface only returns the number of matching
+ * logins (and not the logins themselves), which allows a caller to
+ * check for logins without causing the user to be prompted for a master
+ * password to decrypt the logins.
+ *
+ * @param aHostname
+ * The hostname to restrict searches to. Specify an empty string
+ * to match all hosts. A null value will not match any logins, and
+ * will thus always return a count of 0.
+ * @param aActionURL
+ * The URL to which a form login will be submitted. To match any
+ * form login, specify an empty string. To not match any form
+ * login, specify null.
+ * @param aHttpRealm
+ * The HTTP Realm for which the login applies. To match logins for
+ * any realm, specify an empty string. To not match logins for any
+ * realm, specify null.
+ */
+ unsigned long countLogins(in AString aHostname, in AString aActionURL,
+ in AString aHttpRealm);
+ /**
+ * True when a master password prompt is being shown.
+ */
+ readonly attribute boolean uiBusy;
+
+ /**
+ * True when the master password has already been entered, and so a caller
+ * can ask for decrypted logins without triggering a prompt.
+ */
+ readonly attribute boolean isLoggedIn;
+};
diff --git a/toolkit/components/passwordmgr/nsILoginMetaInfo.idl b/toolkit/components/passwordmgr/nsILoginMetaInfo.idl
new file mode 100644
index 0000000000..92d8f2bc87
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsILoginMetaInfo.idl
@@ -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/. */
+
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(20d8eb40-c494-497f-b2a6-aaa32f807ebd)]
+
+/**
+ * An object containing metainfo for a login stored by the login manager.
+ *
+ * Code using login manager can generally ignore this interface. When adding
+ * logins, default value will be created. When modifying logins, these
+ * properties will be unchanged unless a change is explicitly requested [by
+ * using modifyLogin() with a nsIPropertyBag]. When deleting a login or
+ * comparing logins, these properties are ignored.
+ */
+interface nsILoginMetaInfo : nsISupports {
+ /**
+ * The GUID to uniquely identify the login. This can be any arbitrary
+ * string, but a format as created by nsIUUIDGenerator is recommended.
+ * For example, "{d4e1a1f6-5ea0-40ee-bff5-da57982f21cf}"
+ *
+ * addLogin will generate a random value unless a value is provided.
+ *
+ * addLogin and modifyLogin will throw if the GUID already exists.
+ */
+ attribute AString guid;
+
+ /**
+ * The time, in Unix Epoch milliseconds, when the login was first created.
+ */
+ attribute unsigned long long timeCreated;
+
+ /**
+ * The time, in Unix Epoch milliseconds, when the login was last submitted
+ * in a form or used to begin an HTTP auth session.
+ */
+ attribute unsigned long long timeLastUsed;
+
+ /**
+ * The time, in Unix Epoch milliseconds, when the login was last modified.
+ *
+ * Contrary to what the name may suggest, this attribute takes into account
+ * not only the password but also the username attribute.
+ */
+ attribute unsigned long long timePasswordChanged;
+
+ /**
+ * The number of times the login was submitted in a form or used to begin
+ * an HTTP auth session.
+ */
+ attribute unsigned long timesUsed;
+};
diff --git a/toolkit/components/passwordmgr/nsLoginInfo.js b/toolkit/components/passwordmgr/nsLoginInfo.js
new file mode 100644
index 0000000000..d6ea86446f
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginInfo.js
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+
+function nsLoginInfo() {}
+
+nsLoginInfo.prototype = {
+
+ classID : Components.ID("{0f2f347c-1e4f-40cc-8efd-792dea70a85e}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginInfo, Ci.nsILoginMetaInfo]),
+
+ //
+ // nsILoginInfo interfaces...
+ //
+
+ hostname : null,
+ formSubmitURL : null,
+ httpRealm : null,
+ username : null,
+ password : null,
+ usernameField : null,
+ passwordField : null,
+
+ init : function (aHostname, aFormSubmitURL, aHttpRealm,
+ aUsername, aPassword,
+ aUsernameField, aPasswordField) {
+ this.hostname = aHostname;
+ this.formSubmitURL = aFormSubmitURL;
+ this.httpRealm = aHttpRealm;
+ this.username = aUsername;
+ this.password = aPassword;
+ this.usernameField = aUsernameField;
+ this.passwordField = aPasswordField;
+ },
+
+ matches(aLogin, ignorePassword) {
+ return LoginHelper.doLoginsMatch(this, aLogin, {
+ ignorePassword,
+ });
+ },
+
+ equals : function (aLogin) {
+ if (this.hostname != aLogin.hostname ||
+ this.formSubmitURL != aLogin.formSubmitURL ||
+ this.httpRealm != aLogin.httpRealm ||
+ this.username != aLogin.username ||
+ this.password != aLogin.password ||
+ this.usernameField != aLogin.usernameField ||
+ this.passwordField != aLogin.passwordField)
+ return false;
+
+ return true;
+ },
+
+ clone : function() {
+ let clone = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ clone.init(this.hostname, this.formSubmitURL, this.httpRealm,
+ this.username, this.password,
+ this.usernameField, this.passwordField);
+
+ // Copy nsILoginMetaInfo props
+ clone.QueryInterface(Ci.nsILoginMetaInfo);
+ clone.guid = this.guid;
+ clone.timeCreated = this.timeCreated;
+ clone.timeLastUsed = this.timeLastUsed;
+ clone.timePasswordChanged = this.timePasswordChanged;
+ clone.timesUsed = this.timesUsed;
+
+ return clone;
+ },
+
+ //
+ // nsILoginMetaInfo interfaces...
+ //
+
+ guid : null,
+ timeCreated : null,
+ timeLastUsed : null,
+ timePasswordChanged : null,
+ timesUsed : null
+
+}; // end of nsLoginInfo implementation
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsLoginInfo]);
diff --git a/toolkit/components/passwordmgr/nsLoginManager.js b/toolkit/components/passwordmgr/nsLoginManager.js
new file mode 100644
index 0000000000..514351fa52
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -0,0 +1,541 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/LoginManagerContent.jsm"); /* global UserAutoCompleteResult */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
+ "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+ "resource://gre/modules/InsecurePasswordUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let logger = LoginHelper.createLogger("nsLoginManager");
+ return logger;
+});
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+function LoginManager() {
+ this.init();
+}
+
+LoginManager.prototype = {
+
+ classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManager,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIInterfaceRequestor]),
+ getInterface(aIID) {
+ if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
+ let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
+ return ir.getInterface(aIID);
+ }
+
+ if (aIID.equals(Ci.nsIVariant)) {
+ // Allows unwrapping the JavaScript object for regression tests.
+ return this;
+ }
+
+ throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+
+ /* ---------- private members ---------- */
+
+
+ __formFillService: null, // FormFillController, for username autocompleting
+ get _formFillService() {
+ if (!this.__formFillService) {
+ this.__formFillService = Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(Ci.nsIFormFillController);
+ }
+ return this.__formFillService;
+ },
+
+
+ _storage: null, // Storage component which contains the saved logins
+ _prefBranch: null, // Preferences service
+ _remember: true, // mirrors signon.rememberSignons preference
+
+
+ /**
+ * Initialize the Login Manager. Automatically called when service
+ * is created.
+ *
+ * Note: Service created in /browser/base/content/browser.js,
+ * delayedStartup()
+ */
+ init() {
+
+ // Cache references to current |this| in utility objects
+ this._observer._pwmgr = this;
+
+ // Preferences. Add observer so we get notified of changes.
+ this._prefBranch = Services.prefs.getBranch("signon.");
+ this._prefBranch.addObserver("rememberSignons", this._observer, false);
+
+ this._remember = this._prefBranch.getBoolPref("rememberSignons");
+ this._autoCompleteLookupPromise = null;
+
+ // Form submit observer checks forms for new logins and pw changes.
+ Services.obs.addObserver(this._observer, "xpcom-shutdown", false);
+
+ if (Services.appinfo.processType ===
+ Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.obs.addObserver(this._observer, "passwordmgr-storage-replace",
+ false);
+
+ // Initialize storage so that asynchronous data loading can start.
+ this._initStorage();
+ }
+
+ Services.obs.addObserver(this._observer, "gather-telemetry", false);
+ },
+
+
+ _initStorage() {
+ let contractID;
+ if (AppConstants.platform == "android") {
+ contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
+ } else {
+ contractID = "@mozilla.org/login-manager/storage/json;1";
+ }
+ try {
+ let catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ contractID = catMan.getCategoryEntry("login-manager-storage",
+ "nsILoginManagerStorage");
+ log.debug("Found alternate nsILoginManagerStorage with contract ID:", contractID);
+ } catch (e) {
+ log.debug("No alternate nsILoginManagerStorage registered");
+ }
+
+ this._storage = Cc[contractID].
+ createInstance(Ci.nsILoginManagerStorage);
+ this.initializationPromise = this._storage.initialize();
+ },
+
+
+ /* ---------- Utility objects ---------- */
+
+
+ /**
+ * Internal utility object, implements the nsIObserver interface.
+ * Used to receive notification for: form submission, preference changes.
+ */
+ _observer: {
+ _pwmgr: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ // nsIObserver
+ observe(subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ var prefName = data;
+ log.debug("got change to", prefName, "preference");
+
+ if (prefName == "rememberSignons") {
+ this._pwmgr._remember =
+ this._pwmgr._prefBranch.getBoolPref("rememberSignons");
+ } else {
+ log.debug("Oops! Pref not handled, change ignored.");
+ }
+ } else if (topic == "xpcom-shutdown") {
+ delete this._pwmgr.__formFillService;
+ delete this._pwmgr._storage;
+ delete this._pwmgr._prefBranch;
+ this._pwmgr = null;
+ } else if (topic == "passwordmgr-storage-replace") {
+ Task.spawn(function* () {
+ yield this._pwmgr._storage.terminate();
+ this._pwmgr._initStorage();
+ yield this._pwmgr.initializationPromise;
+ Services.obs.notifyObservers(null,
+ "passwordmgr-storage-replace-complete", null);
+ }.bind(this));
+ } else if (topic == "gather-telemetry") {
+ // When testing, the "data" parameter is a string containing the
+ // reference time in milliseconds for time-based statistics.
+ this._pwmgr._gatherTelemetry(data ? parseInt(data)
+ : new Date().getTime());
+ } else {
+ log.debug("Oops! Unexpected notification:", topic);
+ }
+ }
+ },
+
+ /**
+ * Collects statistics about the current logins and settings. The telemetry
+ * histograms used here are not accumulated, but are reset each time this
+ * function is called, since it can be called multiple times in a session.
+ *
+ * This function might also not be called at all in the current session.
+ *
+ * @param referenceTimeMs
+ * Current time used to calculate time-based statistics, expressed as
+ * the number of milliseconds since January 1, 1970, 00:00:00 UTC.
+ * This is set to a fake value during unit testing.
+ */
+ _gatherTelemetry(referenceTimeMs) {
+ function clearAndGetHistogram(histogramId) {
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ histogram.clear();
+ return histogram;
+ }
+
+ clearAndGetHistogram("PWMGR_BLOCKLIST_NUM_SITES").add(
+ this.getAllDisabledHosts({}).length
+ );
+ clearAndGetHistogram("PWMGR_NUM_SAVED_PASSWORDS").add(
+ this.countLogins("", "", "")
+ );
+ clearAndGetHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS").add(
+ this.countLogins("", null, "")
+ );
+
+ // This is a boolean histogram, and not a flag, because we don't want to
+ // record any value if _gatherTelemetry is not called.
+ clearAndGetHistogram("PWMGR_SAVING_ENABLED").add(this._remember);
+
+ // Don't try to get logins if MP is enabled, since we don't want to show a MP prompt.
+ if (!this.isLoggedIn) {
+ return;
+ }
+
+ let logins = this.getAllLogins({});
+
+ let usernamePresentHistogram = clearAndGetHistogram("PWMGR_USERNAME_PRESENT");
+ let loginLastUsedDaysHistogram = clearAndGetHistogram("PWMGR_LOGIN_LAST_USED_DAYS");
+
+ let hostnameCount = new Map();
+ for (let login of logins) {
+ usernamePresentHistogram.add(!!login.username);
+
+ let hostname = login.hostname;
+ hostnameCount.set(hostname, (hostnameCount.get(hostname) || 0 ) + 1);
+
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ let timeLastUsedAgeMs = referenceTimeMs - login.timeLastUsed;
+ if (timeLastUsedAgeMs > 0) {
+ loginLastUsedDaysHistogram.add(
+ Math.floor(timeLastUsedAgeMs / MS_PER_DAY)
+ );
+ }
+ }
+
+ let passwordsCountHistogram = clearAndGetHistogram("PWMGR_NUM_PASSWORDS_PER_HOSTNAME");
+ for (let count of hostnameCount.values()) {
+ passwordsCountHistogram.add(count);
+ }
+ },
+
+
+
+
+
+ /* ---------- Primary Public interfaces ---------- */
+
+
+
+
+ /**
+ * @type Promise
+ * This promise is resolved when initialization is complete, and is rejected
+ * in case the asynchronous part of initialization failed.
+ */
+ initializationPromise: null,
+
+
+ /**
+ * Add a new login to login storage.
+ */
+ addLogin(login) {
+ // Sanity check the login
+ if (login.hostname == null || login.hostname.length == 0) {
+ throw new Error("Can't add a login with a null or empty hostname.");
+ }
+
+ // For logins w/o a username, set to "", not null.
+ if (login.username == null) {
+ throw new Error("Can't add a login with a null username.");
+ }
+
+ if (login.password == null || login.password.length == 0) {
+ throw new Error("Can't add a login with a null or empty password.");
+ }
+
+ if (login.formSubmitURL || login.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (login.httpRealm != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else if (login.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (login.formSubmitURL != null) {
+ throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
+ }
+ } else {
+ // Need one or the other!
+ throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
+ }
+
+
+ // Look for an existing entry.
+ var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
+ login.httpRealm);
+
+ if (logins.some(l => login.matches(l, true))) {
+ throw new Error("This login already exists.");
+ }
+
+ log.debug("Adding login");
+ return this._storage.addLogin(login);
+ },
+
+ /**
+ * Remove the specified login from the stored logins.
+ */
+ removeLogin(login) {
+ log.debug("Removing login");
+ return this._storage.removeLogin(login);
+ },
+
+
+ /**
+ * Change the specified login to match the new login.
+ */
+ modifyLogin(oldLogin, newLogin) {
+ log.debug("Modifying login");
+ return this._storage.modifyLogin(oldLogin, newLogin);
+ },
+
+
+ /**
+ * Get a dump of all stored logins. Used by the login manager UI.
+ *
+ * @param count - only needed for XPCOM.
+ * @return {nsILoginInfo[]} - If there are no logins, the array is empty.
+ */
+ getAllLogins(count) {
+ log.debug("Getting a list of all logins");
+ return this._storage.getAllLogins(count);
+ },
+
+
+ /**
+ * Remove all stored logins.
+ */
+ removeAllLogins() {
+ log.debug("Removing all logins");
+ this._storage.removeAllLogins();
+ },
+
+ /**
+ * Get a list of all origins for which logins are disabled.
+ *
+ * @param {Number} count - only needed for XPCOM.
+ *
+ * @return {String[]} of disabled origins. If there are no disabled origins,
+ * the array is empty.
+ */
+ getAllDisabledHosts(count) {
+ log.debug("Getting a list of all disabled origins");
+
+ let disabledHosts = [];
+ let enumerator = Services.perms.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let perm = enumerator.getNext();
+ if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) {
+ disabledHosts.push(perm.principal.URI.prePath);
+ }
+ }
+
+ if (count)
+ count.value = disabledHosts.length; // needed for XPCOM
+
+ log.debug("getAllDisabledHosts: returning", disabledHosts.length, "disabled hosts.");
+ return disabledHosts;
+ },
+
+
+ /**
+ * Search for the known logins for entries matching the specified criteria.
+ */
+ findLogins(count, origin, formActionOrigin, httpRealm) {
+ log.debug("Searching for logins matching origin:", origin,
+ "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm);
+
+ return this._storage.findLogins(count, origin, formActionOrigin,
+ httpRealm);
+ },
+
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins(count, matchData) {
+ log.debug("Searching for logins");
+
+ matchData.QueryInterface(Ci.nsIPropertyBag2);
+ if (!matchData.hasKey("guid")) {
+ if (!matchData.hasKey("hostname")) {
+ log.warn("searchLogins: A `hostname` is recommended");
+ }
+
+ if (!matchData.hasKey("formSubmitURL") && !matchData.hasKey("httpRealm")) {
+ log.warn("searchLogins: `formSubmitURL` or `httpRealm` is recommended");
+ }
+ }
+
+ return this._storage.searchLogins(count, matchData);
+ },
+
+
+ /**
+ * Search for the known logins for entries matching the specified criteria,
+ * returns only the count.
+ */
+ countLogins(origin, formActionOrigin, httpRealm) {
+ log.debug("Counting logins matching origin:", origin,
+ "formActionOrigin:", formActionOrigin, "httpRealm:", httpRealm);
+
+ return this._storage.countLogins(origin, formActionOrigin, httpRealm);
+ },
+
+
+ get uiBusy() {
+ return this._storage.uiBusy;
+ },
+
+
+ get isLoggedIn() {
+ return this._storage.isLoggedIn;
+ },
+
+
+ /**
+ * Check to see if user has disabled saving logins for the origin.
+ */
+ getLoginSavingEnabled(origin) {
+ log.debug("Checking if logins to", origin, "can be saved.");
+ if (!this._remember) {
+ return false;
+ }
+
+ let uri = Services.io.newURI(origin, null, null);
+ return Services.perms.testPermission(uri, PERMISSION_SAVE_LOGINS) != Services.perms.DENY_ACTION;
+ },
+
+
+ /**
+ * Enable or disable storing logins for the specified origin.
+ */
+ setLoginSavingEnabled(origin, enabled) {
+ // Throws if there are bogus values.
+ LoginHelper.checkHostnameValue(origin);
+
+ let uri = Services.io.newURI(origin, null, null);
+ if (enabled) {
+ Services.perms.remove(uri, PERMISSION_SAVE_LOGINS);
+ } else {
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ }
+
+ log.debug("Login saving for", origin, "now enabled?", enabled);
+ LoginHelper.notifyStorageChanged(enabled ? "hostSavingEnabled" : "hostSavingDisabled", origin);
+ },
+
+ /**
+ * Yuck. This is called directly by satchel:
+ * nsFormFillController::StartSearch()
+ * [toolkit/components/satchel/nsFormFillController.cpp]
+ *
+ * We really ought to have a simple way for code to register an
+ * auto-complete provider, and not have satchel calling pwmgr directly.
+ */
+ autoCompleteSearchAsync(aSearchString, aPreviousResult,
+ aElement, aCallback) {
+ // aPreviousResult is an nsIAutoCompleteResult, aElement is
+ // nsIDOMHTMLInputElement
+
+ let form = LoginFormFactory.createFromField(aElement);
+ let isSecure = InsecurePasswordUtils.isFormSecure(form);
+ let isPasswordField = aElement.type == "password";
+
+ let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => {
+ // If the search was canceled before we got our
+ // results, don't bother reporting them.
+ if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
+ return;
+ }
+
+ this._autoCompleteLookupPromise = null;
+ let results = new UserAutoCompleteResult(aSearchString, logins, {
+ messageManager,
+ isSecure,
+ isPasswordField,
+ });
+ aCallback.onSearchCompletion(results);
+ };
+
+ if (isPasswordField && aSearchString) {
+ // Return empty result on password fields with password already filled.
+ let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] });
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
+ return;
+ }
+
+ if (!this._remember) {
+ let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] });
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise));
+ return;
+ }
+
+ log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
+
+ let previousResult;
+ if (aPreviousResult) {
+ previousResult = { searchString: aPreviousResult.searchString,
+ logins: aPreviousResult.wrappedJSObject.logins };
+ } else {
+ previousResult = null;
+ }
+
+ let rect = BrowserUtils.getElementBoundingScreenRect(aElement);
+ let acLookupPromise = this._autoCompleteLookupPromise =
+ LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult,
+ aElement, rect);
+ acLookupPromise.then(completeSearch.bind(this, acLookupPromise))
+ .then(null, Cu.reportError);
+ },
+
+ stopSearch() {
+ this._autoCompleteLookupPromise = null;
+ },
+}; // end of LoginManager implementation
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);
diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
new file mode 100644
index 0000000000..b66489234a
--- /dev/null
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -0,0 +1,1701 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+const { PromptUtils } = Cu.import("resource://gre/modules/SharedPromptUtils.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+const LoginInfo =
+ Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
+
+/**
+ * Constants for password prompt telemetry.
+ * Mirrored in mobile/android/components/LoginManagerPrompter.js */
+const PROMPT_DISPLAYED = 0;
+
+const PROMPT_ADD_OR_UPDATE = 1;
+const PROMPT_NOTNOW = 2;
+const PROMPT_NEVER = 3;
+
+/**
+ * Implements nsIPromptFactory
+ *
+ * Invoked by [toolkit/components/prompts/src/nsPrompter.js]
+ */
+function LoginManagerPromptFactory() {
+ Services.obs.addObserver(this, "quit-application-granted", true);
+ Services.obs.addObserver(this, "passwordmgr-crypto-login", true);
+ Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true);
+}
+
+LoginManagerPromptFactory.prototype = {
+
+ classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ _asyncPrompts : {},
+ _asyncPromptInProgress : false,
+
+ observe : function (subject, topic, data) {
+ this.log("Observed: " + topic);
+ if (topic == "quit-application-granted") {
+ this._cancelPendingPrompts();
+ } else if (topic == "passwordmgr-crypto-login") {
+ // Start processing the deferred prompters.
+ this._doAsyncPrompt();
+ } else if (topic == "passwordmgr-crypto-loginCanceled") {
+ // User canceled a Master Password prompt, so go ahead and cancel
+ // all pending auth prompts to avoid nagging over and over.
+ this._cancelPendingPrompts();
+ }
+ },
+
+ getPrompt : function (aWindow, aIID) {
+ var prompt = new LoginManagerPrompter().QueryInterface(aIID);
+ prompt.init(aWindow, this);
+ return prompt;
+ },
+
+ _doAsyncPrompt : function() {
+ if (this._asyncPromptInProgress) {
+ this.log("_doAsyncPrompt bypassed, already in progress");
+ return;
+ }
+
+ // Find the first prompt key we have in the queue
+ var hashKey = null;
+ for (hashKey in this._asyncPrompts)
+ break;
+
+ if (!hashKey) {
+ this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
+ return;
+ }
+
+ // If login manger has logins for this host, defer prompting if we're
+ // already waiting on a master password entry.
+ var prompt = this._asyncPrompts[hashKey];
+ var prompter = prompt.prompter;
+ var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo);
+ var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0);
+ if (!hasLogins && LoginHelper.schemeUpgrades && hostname.startsWith("https://")) {
+ let httpHostname = hostname.replace(/^https:\/\//, "http://");
+ hasLogins = (prompter._pwmgr.countLogins(httpHostname, null, httpRealm) > 0);
+ }
+ if (hasLogins && prompter._pwmgr.uiBusy) {
+ this.log("_doAsyncPrompt:run bypassed, master password UI busy");
+ return;
+ }
+
+ // Allow only a limited number of authentication dialogs when they are all
+ // canceled by the user.
+ var cancelationCounter = (prompter._browser && prompter._browser.canceledAuthenticationPromptCounter) || { count: 0, id: 0 };
+ if (prompt.channel) {
+ var httpChannel = prompt.channel.QueryInterface(Ci.nsIHttpChannel);
+ if (httpChannel) {
+ var windowId = httpChannel.topLevelContentWindowId;
+ if (windowId != cancelationCounter.id) {
+ // window has been reloaded or navigated, reset the counter
+ cancelationCounter = { count: 0, id: windowId };
+ }
+ }
+ }
+
+ var self = this;
+
+ var runnable = {
+ cancel: false,
+ run : function() {
+ var ok = false;
+ if (!this.cancel) {
+ try {
+ self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
+ ok = prompter.promptAuth(prompt.channel,
+ prompt.level,
+ prompt.authInfo);
+ } catch (e) {
+ if (e instanceof Components.Exception &&
+ e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ self.log("_doAsyncPrompt:run bypassed, UI is not available in this context");
+ } else {
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "_doAsyncPrompt:run: " + e + "\n");
+ }
+ }
+
+ delete self._asyncPrompts[hashKey];
+ prompt.inProgress = false;
+ self._asyncPromptInProgress = false;
+
+ if (ok) {
+ cancelationCounter.count = 0;
+ } else {
+ cancelationCounter.count++;
+ }
+ if (prompter._browser) {
+ prompter._browser.canceledAuthenticationPromptCounter = cancelationCounter;
+ }
+ }
+
+ for (var consumer of prompt.consumers) {
+ if (!consumer.callback)
+ // Not having a callback means that consumer didn't provide it
+ // or canceled the notification
+ continue;
+
+ self.log("Calling back to " + consumer.callback + " ok=" + ok);
+ try {
+ if (ok) {
+ consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
+ } else {
+ consumer.callback.onAuthCancelled(consumer.context, !this.cancel);
+ }
+ } catch (e) { /* Throw away exceptions caused by callback */ }
+ }
+ self._doAsyncPrompt();
+ }
+ };
+
+ var cancelDialogLimit = Services.prefs.getIntPref("prompts.authentication_dialog_abuse_limit");
+
+ this.log("cancelationCounter =", cancelationCounter);
+ if (cancelDialogLimit && cancelationCounter.count >= cancelDialogLimit) {
+ this.log("Blocking auth dialog, due to exceeding dialog bloat limit");
+ delete this._asyncPrompts[hashKey];
+
+ // just make the runnable cancel all consumers
+ runnable.cancel = true;
+ } else {
+ this._asyncPromptInProgress = true;
+ prompt.inProgress = true;
+ }
+
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ this.log("_doAsyncPrompt:run dispatched");
+ },
+
+
+ _cancelPendingPrompts : function() {
+ this.log("Canceling all pending prompts...");
+ var asyncPrompts = this._asyncPrompts;
+ this.__proto__._asyncPrompts = {};
+
+ for (var hashKey in asyncPrompts) {
+ let prompt = asyncPrompts[hashKey];
+ // Watch out! If this prompt is currently prompting, let it handle
+ // notifying the callbacks of success/failure, since it's already
+ // asking the user for input. Reusing a callback can be crashy.
+ if (prompt.inProgress) {
+ this.log("skipping a prompt in progress");
+ continue;
+ }
+
+ for (var consumer of prompt.consumers) {
+ if (!consumer.callback)
+ continue;
+
+ this.log("Canceling async auth prompt callback " + consumer.callback);
+ try {
+ consumer.callback.onAuthCancelled(consumer.context, true);
+ } catch (e) { /* Just ignore exceptions from the callback */ }
+ }
+ }
+ },
+}; // end of LoginManagerPromptFactory implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerPromptFactory.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login PromptFactory");
+ return logger.log.bind(logger);
+});
+
+
+
+
+/* ==================== LoginManagerPrompter ==================== */
+
+
+
+
+/**
+ * Implements interfaces for prompting the user to enter/save/change auth info.
+ *
+ * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
+ *
+ * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
+ * (eg HTTP Authenticate, FTP login).
+ *
+ * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
+ * found in HTML forms.
+ */
+function LoginManagerPrompter() {}
+
+LoginManagerPrompter.prototype = {
+
+ classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
+ Ci.nsIAuthPrompt2,
+ Ci.nsILoginManagerPrompter]),
+
+ _factory : null,
+ _chromeWindow : null,
+ _browser : null,
+ _opener : null,
+
+ __pwmgr : null, // Password Manager service
+ get _pwmgr() {
+ if (!this.__pwmgr)
+ this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ return this.__pwmgr;
+ },
+
+ __promptService : null, // Prompt service for user interaction
+ get _promptService() {
+ if (!this.__promptService)
+ this.__promptService =
+ Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+ return this.__promptService;
+ },
+
+
+ __strBundle : null, // String bundle for L10N
+ get _strBundle() {
+ if (!this.__strBundle) {
+ var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ this.__strBundle = bunService.createBundle(
+ "chrome://passwordmgr/locale/passwordmgr.properties");
+ if (!this.__strBundle)
+ throw new Error("String bundle for Login Manager not present!");
+ }
+
+ return this.__strBundle;
+ },
+
+
+ __ellipsis : null,
+ get _ellipsis() {
+ if (!this.__ellipsis) {
+ this.__ellipsis = "\u2026";
+ try {
+ this.__ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+ }
+ return this.__ellipsis;
+ },
+
+
+ // Whether we are in private browsing mode
+ get _inPrivateBrowsing() {
+ if (this._chromeWindow) {
+ return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow);
+ }
+ // If we don't that we're in private browsing mode if the caller did
+ // not provide a window. The callers which really care about this
+ // will indeed pass down a window to us, and for those who don't,
+ // we can just assume that we don't want to save the entered login
+ // information.
+ this.log("We have no chromeWindow so assume we're in a private context");
+ return true;
+ },
+
+
+
+
+ /* ---------- nsIAuthPrompt prompts ---------- */
+
+
+ /**
+ * Wrapper around the prompt service prompt. Saving random fields here
+ * doesn't really make sense and therefore isn't implemented.
+ */
+ prompt : function (aDialogTitle, aText, aPasswordRealm,
+ aSavePassword, aDefaultText, aResult) {
+ if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
+ throw new Components.Exception("prompt only supports SAVE_PASSWORD_NEVER",
+ Cr.NS_ERROR_NOT_IMPLEMENTED);
+
+ this.log("===== prompt() called =====");
+
+ if (aDefaultText) {
+ aResult.value = aDefaultText;
+ }
+
+ return this._promptService.prompt(this._chromeWindow,
+ aDialogTitle, aText, aResult, null, {});
+ },
+
+
+ /**
+ * Looks up a username and password in the database. Will prompt the user
+ * with a dialog, even if a username and password are found.
+ */
+ promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm,
+ aSavePassword, aUsername, aPassword) {
+ this.log("===== promptUsernameAndPassword() called =====");
+
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
+ throw new Components.Exception("promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED);
+
+ var selectedLogin = null;
+ var checkBox = { value : false };
+ var checkBoxLabel = null;
+ var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
+
+ // If hostname is null, we can't save this login.
+ if (hostname) {
+ var canRememberLogin;
+ if (this._inPrivateBrowsing)
+ canRememberLogin = false;
+ else
+ canRememberLogin = (aSavePassword ==
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
+ this._pwmgr.getLoginSavingEnabled(hostname);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin)
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+
+ // Look for existing logins.
+ var foundLogins = this._pwmgr.findLogins({}, hostname, null,
+ realm);
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection. (bug 227632)
+ if (foundLogins.length > 0) {
+ selectedLogin = foundLogins[0];
+
+ // If the caller provided a username, try to use it. If they
+ // provided only a password, this will try to find a password-only
+ // login (or return null if none exists).
+ if (aUsername.value)
+ selectedLogin = this._repickSelectedLogin(foundLogins,
+ aUsername.value);
+
+ if (selectedLogin) {
+ checkBox.value = true;
+ aUsername.value = selectedLogin.username;
+ // If the caller provided a password, prefer it.
+ if (!aPassword.value)
+ aPassword.value = selectedLogin.password;
+ }
+ }
+ }
+
+ var ok = this._promptService.promptUsernameAndPassword(this._chromeWindow,
+ aDialogTitle, aText, aUsername, aPassword,
+ checkBoxLabel, checkBox);
+
+ if (!ok || !checkBox.value || !hostname)
+ return ok;
+
+ if (!aPassword.value) {
+ this.log("No password entered, so won't offer to save.");
+ return ok;
+ }
+
+ // XXX We can't prompt with multiple logins yet (bug 227632), so
+ // the entered login might correspond to an existing login
+ // other than the one we originally selected.
+ selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLogin.init(hostname, null, realm,
+ aUsername.value, aPassword.value, "", "");
+ if (!selectedLogin) {
+ // add as new
+ this.log("New login seen for " + realm);
+ this._pwmgr.addLogin(newLogin);
+ } else if (aPassword.value != selectedLogin.password) {
+ // update password
+ this.log("Updating password for " + realm);
+ this._updateLogin(selectedLogin, newLogin);
+ } else {
+ this.log("Login unchanged, no further action needed.");
+ this._updateLogin(selectedLogin);
+ }
+
+ return ok;
+ },
+
+
+ /**
+ * If a password is found in the database for the password realm, it is
+ * returned straight away without displaying a dialog.
+ *
+ * If a password is not found in the database, the user will be prompted
+ * with a dialog with a text field and ok/cancel buttons. If the user
+ * allows it, then the password will be saved in the database.
+ */
+ promptPassword : function (aDialogTitle, aText, aPasswordRealm,
+ aSavePassword, aPassword) {
+ this.log("===== promptPassword called() =====");
+
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
+ throw new Components.Exception("promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED);
+
+ var checkBox = { value : false };
+ var checkBoxLabel = null;
+ var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ // If hostname is null, we can't save this login.
+ if (hostname && !this._inPrivateBrowsing) {
+ var canRememberLogin = (aSavePassword ==
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
+ this._pwmgr.getLoginSavingEnabled(hostname);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin)
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+
+ if (!aPassword.value) {
+ // Look for existing logins.
+ var foundLogins = this._pwmgr.findLogins({}, hostname, null,
+ realm);
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection (bug 227632). We can deal with finding the
+ // account based on the supplied username - but in this case we'll
+ // just return the first match.
+ for (var i = 0; i < foundLogins.length; ++i) {
+ if (foundLogins[i].username == username) {
+ aPassword.value = foundLogins[i].password;
+ // wallet returned straight away, so this mimics that code
+ return true;
+ }
+ }
+ }
+ }
+
+ var ok = this._promptService.promptPassword(this._chromeWindow, aDialogTitle,
+ aText, aPassword,
+ checkBoxLabel, checkBox);
+
+ if (ok && checkBox.value && hostname && aPassword.value) {
+ var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLogin.init(hostname, null, realm, username,
+ aPassword.value, "", "");
+
+ this.log("New login seen for " + realm);
+
+ this._pwmgr.addLogin(newLogin);
+ }
+
+ return ok;
+ },
+
+ /* ---------- nsIAuthPrompt helpers ---------- */
+
+
+ /**
+ * Given aRealmString, such as "http://user@example.com/foo", returns an
+ * array of:
+ * - the formatted hostname
+ * - the realm (hostname + path)
+ * - the username, if present
+ *
+ * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
+ * channels, e.g. "example.com:80 (httprealm)", null is returned for all
+ * arguments to let callers know the login can't be saved because we don't
+ * know whether it's http or https.
+ */
+ _getRealmInfo : function (aRealmString) {
+ var httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString))
+ return [null, null, null];
+
+ var uri = Services.io.newURI(aRealmString, null, null);
+ var pathname = "";
+
+ if (uri.path != "/")
+ pathname = uri.path;
+
+ var formattedHostname = this._getFormattedHostname(uri);
+
+ return [formattedHostname, formattedHostname + pathname, uri.username];
+ },
+
+ /* ---------- nsIAuthPrompt2 prompts ---------- */
+
+
+
+
+ /**
+ * Implementation of nsIAuthPrompt2.
+ *
+ * @param {nsIChannel} aChannel
+ * @param {int} aLevel
+ * @param {nsIAuthInformation} aAuthInfo
+ */
+ promptAuth : function (aChannel, aLevel, aAuthInfo) {
+ var selectedLogin = null;
+ var checkbox = { value : false };
+ var checkboxLabel = null;
+ var epicfail = false;
+ var canAutologin = false;
+ var notifyObj;
+ var foundLogins;
+
+ try {
+ this.log("===== promptAuth called =====");
+
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ this._removeLoginNotifications();
+
+ var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
+
+ // Looks for existing logins to prefill the prompt with.
+ foundLogins = LoginHelper.searchLoginsWithObject({
+ hostname,
+ httpRealm,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+ this.log("found", foundLogins.length, "matching logins.");
+ let resolveBy = [
+ "scheme",
+ "timePasswordChanged",
+ ];
+ foundLogins = LoginHelper.dedupeLogins(foundLogins, ["username"], resolveBy, hostname);
+ this.log(foundLogins.length, "matching logins remain after deduping");
+
+ // XXX Can't select from multiple accounts yet. (bug 227632)
+ if (foundLogins.length > 0) {
+ selectedLogin = foundLogins[0];
+ this._SetAuthInfo(aAuthInfo, selectedLogin.username,
+ selectedLogin.password);
+
+ // Allow automatic proxy login
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
+ !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
+ Services.prefs.getBoolPref("signon.autologin.proxy") &&
+ !this._inPrivateBrowsing) {
+
+ this.log("Autologin enabled, skipping auth prompt.");
+ canAutologin = true;
+ }
+
+ checkbox.value = true;
+ }
+
+ var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
+ if (this._inPrivateBrowsing)
+ canRememberLogin = false;
+
+ // if checkboxLabel is null, the checkbox won't be shown at all.
+ notifyObj = this._getPopupNote() || this._getNotifyBox();
+ if (canRememberLogin && !notifyObj)
+ checkboxLabel = this._getLocalizedString("rememberPassword");
+ } catch (e) {
+ // Ignore any errors and display the prompt anyway.
+ epicfail = true;
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "Epic fail in promptAuth: " + e + "\n");
+ }
+
+ var ok = canAutologin;
+ if (!ok) {
+ if (this._chromeWindow)
+ PromptUtils.fireDialogEvent(this._chromeWindow, "DOMWillOpenModalDialog", this._browser);
+ ok = this._promptService.promptAuth(this._chromeWindow,
+ aChannel, aLevel, aAuthInfo,
+ checkboxLabel, checkbox);
+ }
+
+ // If there's a notification box, use it to allow the user to
+ // determine if the login should be saved. If there isn't a
+ // notification box, only save the login if the user set the
+ // checkbox to do so.
+ var rememberLogin = notifyObj ? canRememberLogin : checkbox.value;
+ if (!ok || !rememberLogin || epicfail)
+ return ok;
+
+ try {
+ var [username, password] = this._GetAuthInfo(aAuthInfo);
+
+ if (!password) {
+ this.log("No password entered, so won't offer to save.");
+ return ok;
+ }
+
+ // XXX We can't prompt with multiple logins yet (bug 227632), so
+ // the entered login might correspond to an existing login
+ // other than the one we originally selected.
+ selectedLogin = this._repickSelectedLogin(foundLogins, username);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLogin.init(hostname, null, httpRealm,
+ username, password, "", "");
+ if (!selectedLogin) {
+ this.log("New login seen for " + username +
+ " @ " + hostname + " (" + httpRealm + ")");
+
+ if (notifyObj)
+ this._showSaveLoginNotification(notifyObj, newLogin);
+ else
+ this._pwmgr.addLogin(newLogin);
+ } else if (password != selectedLogin.password) {
+ this.log("Updating password for " + username +
+ " @ " + hostname + " (" + httpRealm + ")");
+ if (notifyObj)
+ this._showChangeLoginNotification(notifyObj,
+ selectedLogin, newLogin);
+ else
+ this._updateLogin(selectedLogin, newLogin);
+ } else {
+ this.log("Login unchanged, no further action needed.");
+ this._updateLogin(selectedLogin);
+ }
+ } catch (e) {
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "Fail2 in promptAuth: " + e + "\n");
+ }
+
+ return ok;
+ },
+
+ asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) {
+ var cancelable = null;
+
+ try {
+ this.log("===== asyncPromptAuth called =====");
+
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ this._removeLoginNotifications();
+
+ cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
+
+ var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
+
+ var hashKey = aLevel + "|" + hostname + "|" + httpRealm;
+ this.log("Async prompt key = " + hashKey);
+ var asyncPrompt = this._factory._asyncPrompts[hashKey];
+ if (asyncPrompt) {
+ this.log("Prompt bound to an existing one in the queue, callback = " + aCallback);
+ asyncPrompt.consumers.push(cancelable);
+ return cancelable;
+ }
+
+ this.log("Adding new prompt to the queue, callback = " + aCallback);
+ asyncPrompt = {
+ consumers: [cancelable],
+ channel: aChannel,
+ authInfo: aAuthInfo,
+ level: aLevel,
+ inProgress : false,
+ prompter: this
+ };
+
+ this._factory._asyncPrompts[hashKey] = asyncPrompt;
+ this._factory._doAsyncPrompt();
+ } catch (e) {
+ Components.utils.reportError("LoginManagerPrompter: " +
+ "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n");
+ // Fail the prompt operation to let the consumer fall back
+ // to synchronous promptAuth method
+ throw e;
+ }
+
+ return cancelable;
+ },
+
+
+
+
+ /* ---------- nsILoginManagerPrompter prompts ---------- */
+
+
+ init : function (aWindow = null, aFactory = null) {
+ if (!aWindow) {
+ // There may be no applicable window e.g. in a Sandbox or JSM.
+ this._chromeWindow = null;
+ this._browser = null;
+ } else if (aWindow instanceof Ci.nsIDOMChromeWindow) {
+ this._chromeWindow = aWindow;
+ // needs to be set explicitly using setBrowser
+ this._browser = null;
+ } else {
+ let {win, browser} = this._getChromeWindow(aWindow);
+ this._chromeWindow = win;
+ this._browser = browser;
+ }
+ this._opener = null;
+ this._factory = aFactory || null;
+
+ this.log("===== initialized =====");
+ },
+
+ set browser(aBrowser) {
+ this._browser = aBrowser;
+ },
+
+ set opener(aOpener) {
+ this._opener = aOpener;
+ },
+
+ promptToSavePassword : function (aLogin) {
+ this.log("promptToSavePassword");
+ var notifyObj = this._getPopupNote() || this._getNotifyBox();
+ if (notifyObj)
+ this._showSaveLoginNotification(notifyObj, aLogin);
+ else
+ this._showSaveLoginDialog(aLogin);
+ },
+
+ /**
+ * Displays a notification bar.
+ */
+ _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
+ var oldBar = aNotifyBox.getNotificationWithValue(aName);
+ const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
+
+ this.log("Adding new " + aName + " notification bar");
+ var newBar = aNotifyBox.appendNotification(
+ aText, aName, "",
+ priority, aButtons);
+
+ // The page we're going to hasn't loaded yet, so we want to persist
+ // across the first location change.
+ newBar.persistence++;
+
+ // Sites like Gmail perform a funky redirect dance before you end up
+ // at the post-authentication page. I don't see a good way to
+ // heuristically determine when to ignore such location changes, so
+ // we'll try ignoring location changes based on a time interval.
+ newBar.timeout = Date.now() + 20000; // 20 seconds
+
+ if (oldBar) {
+ this.log("(...and removing old " + aName + " notification bar)");
+ aNotifyBox.removeNotification(oldBar);
+ }
+ },
+
+ /**
+ * Displays the PopupNotifications.jsm doorhanger for password save or change.
+ *
+ * @param {nsILoginInfo} login
+ * Login to save or change. For changes, this login should contain the
+ * new password.
+ * @param {string} type
+ * This is "password-save" or "password-change" depending on the
+ * original notification type. This is used for telemetry and tests.
+ */
+ _showLoginCaptureDoorhanger(login, type) {
+ let { browser } = this._getNotifyWindow();
+
+ let saveMsgNames = {
+ prompt: login.username === "" ? "rememberLoginMsgNoUser"
+ : "rememberLoginMsg",
+ buttonLabel: "rememberLoginButtonText",
+ buttonAccessKey: "rememberLoginButtonAccessKey",
+ };
+
+ let changeMsgNames = {
+ prompt: login.username === "" ? "updateLoginMsgNoUser"
+ : "updateLoginMsg",
+ buttonLabel: "updateLoginButtonText",
+ buttonAccessKey: "updateLoginButtonAccessKey",
+ };
+
+ let initialMsgNames = type == "password-save" ? saveMsgNames
+ : changeMsgNames;
+
+ let brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
+ let brandShortName = brandBundle.GetStringFromName("brandShortName");
+ let promptMsg = type == "password-save" ? this._getLocalizedString(saveMsgNames.prompt, [brandShortName])
+ : this._getLocalizedString(changeMsgNames.prompt);
+
+ let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
+ : "PWMGR_PROMPT_UPDATE_ACTION";
+ let histogram = Services.telemetry.getHistogramById(histogramName);
+ histogram.add(PROMPT_DISPLAYED);
+
+ let chromeDoc = browser.ownerDocument;
+
+ let currentNotification;
+
+ let updateButtonStatus = (element) => {
+ let mainActionButton = chromeDoc.getAnonymousElementByAttribute(element.button, "anonid", "button");
+ // Disable the main button inside the menu-button if the password field is empty.
+ if (login.password.length == 0) {
+ mainActionButton.setAttribute("disabled", true);
+ chromeDoc.getElementById("password-notification-password")
+ .classList.add("popup-notification-invalid-input");
+ } else {
+ mainActionButton.removeAttribute("disabled");
+ chromeDoc.getElementById("password-notification-password")
+ .classList.remove("popup-notification-invalid-input");
+ }
+ };
+
+ let updateButtonLabel = () => {
+ let foundLogins = LoginHelper.searchLoginsWithObject({
+ formSubmitURL: login.formSubmitURL,
+ hostname: login.hostname,
+ httpRealm: login.httpRealm,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+
+ let logins = this._filterUpdatableLogins(login, foundLogins);
+ let msgNames = (logins.length == 0) ? saveMsgNames : changeMsgNames;
+
+ // Update the label based on whether this will be a new login or not.
+ let label = this._getLocalizedString(msgNames.buttonLabel);
+ let accessKey = this._getLocalizedString(msgNames.buttonAccessKey);
+
+ // Update the labels for the next time the panel is opened.
+ currentNotification.mainAction.label = label;
+ currentNotification.mainAction.accessKey = accessKey;
+
+ // Update the labels in real time if the notification is displayed.
+ let element = [...currentNotification.owner.panel.childNodes]
+ .find(n => n.notification == currentNotification);
+ if (element) {
+ element.setAttribute("buttonlabel", label);
+ element.setAttribute("buttonaccesskey", accessKey);
+ updateButtonStatus(element);
+ }
+ };
+
+ let writeDataToUI = () => {
+ // setAttribute is used since the <textbox> binding may not be attached yet.
+ chromeDoc.getElementById("password-notification-username")
+ .setAttribute("placeholder", usernamePlaceholder);
+ chromeDoc.getElementById("password-notification-username")
+ .setAttribute("value", login.username);
+
+ let toggleCheckbox = chromeDoc.getElementById("password-notification-visibilityToggle");
+ toggleCheckbox.removeAttribute("checked");
+ let passwordField = chromeDoc.getElementById("password-notification-password");
+ // Ensure the type is reset so the field is masked.
+ passwordField.setAttribute("type", "password");
+ passwordField.setAttribute("value", login.password);
+ updateButtonLabel();
+ };
+
+ let readDataFromUI = () => {
+ login.username =
+ chromeDoc.getElementById("password-notification-username").value;
+ login.password =
+ chromeDoc.getElementById("password-notification-password").value;
+ };
+
+ let onInput = () => {
+ readDataFromUI();
+ updateButtonLabel();
+ };
+
+ let onVisibilityToggle = (commandEvent) => {
+ let passwordField = chromeDoc.getElementById("password-notification-password");
+ // Gets the caret position before changing the type of the textbox
+ let selectionStart = passwordField.selectionStart;
+ let selectionEnd = passwordField.selectionEnd;
+ passwordField.setAttribute("type", commandEvent.target.checked ? "" : "password");
+ if (!passwordField.hasAttribute("focused")) {
+ return;
+ }
+ passwordField.selectionStart = selectionStart;
+ passwordField.selectionEnd = selectionEnd;
+ };
+
+ let persistData = () => {
+ let foundLogins = LoginHelper.searchLoginsWithObject({
+ formSubmitURL: login.formSubmitURL,
+ hostname: login.hostname,
+ httpRealm: login.httpRealm,
+ schemeUpgrades: LoginHelper.schemeUpgrades,
+ });
+
+ let logins = this._filterUpdatableLogins(login, foundLogins);
+
+ if (logins.length == 0) {
+ // The original login we have been provided with might have its own
+ // metadata, but we don't want it propagated to the newly created one.
+ Services.logins.addLogin(new LoginInfo(login.hostname,
+ login.formSubmitURL,
+ login.httpRealm,
+ login.username,
+ login.password,
+ login.usernameField,
+ login.passwordField));
+ } else if (logins.length == 1) {
+ if (logins[0].password == login.password &&
+ logins[0].username == login.username) {
+ // We only want to touch the login's use count and last used time.
+ this._updateLogin(logins[0]);
+ } else {
+ this._updateLogin(logins[0], login);
+ }
+ } else {
+ Cu.reportError("Unexpected match of multiple logins.");
+ }
+ };
+
+ // The main action is the "Remember" or "Update" button.
+ let mainAction = {
+ label: this._getLocalizedString(initialMsgNames.buttonLabel),
+ accessKey: this._getLocalizedString(initialMsgNames.buttonAccessKey),
+ callback: () => {
+ histogram.add(PROMPT_ADD_OR_UPDATE);
+ if (histogramName == "PWMGR_PROMPT_REMEMBER_ACTION") {
+ Services.obs.notifyObservers(null, 'LoginStats:NewSavedPassword', null);
+ }
+ readDataFromUI();
+ persistData();
+ browser.focus();
+ }
+ };
+
+ // Include a "Never for this site" button when saving a new password.
+ let secondaryActions = type == "password-save" ? [{
+ label: this._getLocalizedString("notifyBarNeverRememberButtonText"),
+ accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"),
+ callback: () => {
+ histogram.add(PROMPT_NEVER);
+ Services.logins.setLoginSavingEnabled(login.hostname, false);
+ browser.focus();
+ }
+ }] : null;
+
+ let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
+ let togglePasswordLabel = this._getLocalizedString("togglePasswordLabel");
+ let togglePasswordAccessKey = this._getLocalizedString("togglePasswordAccessKey");
+
+ this._getPopupNote().show(
+ browser,
+ "password",
+ promptMsg,
+ "password-notification-icon",
+ mainAction,
+ secondaryActions,
+ {
+ timeout: Date.now() + 10000,
+ displayURI: Services.io.newURI(login.hostname, null, null),
+ persistWhileVisible: true,
+ passwordNotificationType: type,
+ eventCallback: function (topic) {
+ switch (topic) {
+ case "showing":
+ currentNotification = this;
+ chromeDoc.getElementById("password-notification-password")
+ .removeAttribute("focused");
+ chromeDoc.getElementById("password-notification-username")
+ .removeAttribute("focused");
+ chromeDoc.getElementById("password-notification-username")
+ .addEventListener("input", onInput);
+ chromeDoc.getElementById("password-notification-password")
+ .addEventListener("input", onInput);
+ let toggleBtn = chromeDoc.getElementById("password-notification-visibilityToggle");
+
+ if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) {
+ toggleBtn.addEventListener("command", onVisibilityToggle);
+ toggleBtn.setAttribute("label", togglePasswordLabel);
+ toggleBtn.setAttribute("accesskey", togglePasswordAccessKey);
+ toggleBtn.setAttribute("hidden", LoginHelper.isMasterPasswordSet());
+ }
+ if (this.wasDismissed) {
+ chromeDoc.getElementById("password-notification-visibilityToggle")
+ .setAttribute("hidden", true);
+ }
+ break;
+ case "shown":
+ writeDataToUI();
+ break;
+ case "dismissed":
+ this.wasDismissed = true;
+ readDataFromUI();
+ // Fall through.
+ case "removed":
+ currentNotification = null;
+ chromeDoc.getElementById("password-notification-username")
+ .removeEventListener("input", onInput);
+ chromeDoc.getElementById("password-notification-password")
+ .removeEventListener("input", onInput);
+ chromeDoc.getElementById("password-notification-visibilityToggle")
+ .removeEventListener("command", onVisibilityToggle);
+ break;
+ }
+ return false;
+ },
+ }
+ );
+ },
+
+ /**
+ * Displays a notification bar or a popup notification, to allow the user
+ * to save the specified login. This allows the user to see the results of
+ * their login, and only save a login which they know worked.
+ *
+ * @param aNotifyObj
+ * A notification box or a popup notification.
+ * @param aLogin
+ * The login captured from the form.
+ */
+ _showSaveLoginNotification : function (aNotifyObj, aLogin) {
+ // Ugh. We can't use the strings from the popup window, because they
+ // have the access key marked in the string (eg "Mo&zilla"), along
+ // with some weird rules for handling access keys that do not occur
+ // in the string, for L10N. See commonDialog.js's setLabelForNode().
+ var neverButtonText =
+ this._getLocalizedString("notifyBarNeverRememberButtonText");
+ var neverButtonAccessKey =
+ this._getLocalizedString("notifyBarNeverRememberButtonAccessKey");
+ var rememberButtonText =
+ this._getLocalizedString("notifyBarRememberPasswordButtonText");
+ var rememberButtonAccessKey =
+ this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
+
+ var displayHost = this._getShortDisplayHost(aLogin.hostname);
+ var notificationText = this._getLocalizedString(
+ "rememberPasswordMsgNoUsername",
+ [displayHost]);
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var pwmgr = this._pwmgr;
+
+ // Notification is a PopupNotification
+ if (aNotifyObj == this._getPopupNote()) {
+ this._showLoginCaptureDoorhanger(aLogin, "password-save");
+ } else {
+ var notNowButtonText =
+ this._getLocalizedString("notifyBarNotNowButtonText");
+ var notNowButtonAccessKey =
+ this._getLocalizedString("notifyBarNotNowButtonAccessKey");
+ var buttons = [
+ // "Remember" button
+ {
+ label: rememberButtonText,
+ accessKey: rememberButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ pwmgr.addLogin(aLogin);
+ }
+ },
+
+ // "Never for this site" button
+ {
+ label: neverButtonText,
+ accessKey: neverButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ }
+ },
+
+ // "Not now" button
+ {
+ label: notNowButtonText,
+ accessKey: notNowButtonAccessKey,
+ popup: null,
+ callback: function() { /* NOP */ }
+ }
+ ];
+
+ this._showLoginNotification(aNotifyObj, "password-save",
+ notificationText, buttons);
+ }
+
+ Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
+ },
+
+ _removeLoginNotifications : function () {
+ var popupNote = this._getPopupNote();
+ if (popupNote)
+ popupNote = popupNote.getNotification("password");
+ if (popupNote)
+ popupNote.remove();
+
+ var notifyBox = this._getNotifyBox();
+ if (notifyBox) {
+ var oldBar = notifyBox.getNotificationWithValue("password-save");
+ if (oldBar) {
+ this.log("Removing save-password notification bar.");
+ notifyBox.removeNotification(oldBar);
+ }
+
+ oldBar = notifyBox.getNotificationWithValue("password-change");
+ if (oldBar) {
+ this.log("Removing change-password notification bar.");
+ notifyBox.removeNotification(oldBar);
+ }
+ }
+ },
+
+
+ /**
+ * Called when we detect a new login in a form submission,
+ * asks the user what to do.
+ */
+ _showSaveLoginDialog : function (aLogin) {
+ const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
+
+ var displayHost = this._getShortDisplayHost(aLogin.hostname);
+
+ var dialogText;
+ if (aLogin.username) {
+ var displayUser = this._sanitizeUsername(aLogin.username);
+ dialogText = this._getLocalizedString(
+ "rememberPasswordMsg",
+ [displayUser, displayHost]);
+ } else {
+ dialogText = this._getLocalizedString(
+ "rememberPasswordMsgNoUsername",
+ [displayHost]);
+
+ }
+ var dialogTitle = this._getLocalizedString(
+ "savePasswordTitle");
+ var neverButtonText = this._getLocalizedString(
+ "neverForSiteButtonText");
+ var rememberButtonText = this._getLocalizedString(
+ "rememberButtonText");
+ var notNowButtonText = this._getLocalizedString(
+ "notNowButtonText");
+
+ this.log("Prompting user to save/ignore login");
+ var userChoice = this._promptService.confirmEx(this._chromeWindow,
+ dialogTitle, dialogText,
+ buttonFlags, rememberButtonText,
+ notNowButtonText, neverButtonText,
+ null, {});
+ // Returns:
+ // 0 - Save the login
+ // 1 - Ignore the login this time
+ // 2 - Never save logins for this site
+ if (userChoice == 2) {
+ this.log("Disabling " + aLogin.hostname + " logins by request.");
+ this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ } else if (userChoice == 0) {
+ this.log("Saving login for " + aLogin.hostname);
+ this._pwmgr.addLogin(aLogin);
+ } else {
+ // userChoice == 1 --> just ignore the login.
+ this.log("Ignoring login.");
+ }
+
+ Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
+ },
+
+
+ /**
+ * Called when we think we detect a password or username change for
+ * an existing login, when the form being submitted contains multiple
+ * password fields.
+ *
+ * @param {nsILoginInfo} aOldLogin
+ * The old login we may want to update.
+ * @param {nsILoginInfo} aNewLogin
+ * The new login from the page form.
+ */
+ promptToChangePassword(aOldLogin, aNewLogin) {
+ this.log("promptToChangePassword");
+ let notifyObj = this._getPopupNote() || this._getNotifyBox();
+
+ if (notifyObj) {
+ this._showChangeLoginNotification(notifyObj, aOldLogin,
+ aNewLogin);
+ } else {
+ this._showChangeLoginDialog(aOldLogin, aNewLogin);
+ }
+ },
+
+ /**
+ * Shows the Change Password notification bar or popup notification.
+ *
+ * @param aNotifyObj
+ * A notification box or a popup notification.
+ *
+ * @param aOldLogin
+ * The stored login we want to update.
+ *
+ * @param aNewLogin
+ * The login object with the changes we want to make.
+ */
+ _showChangeLoginNotification(aNotifyObj, aOldLogin, aNewLogin) {
+ var changeButtonText =
+ this._getLocalizedString("notifyBarUpdateButtonText");
+ var changeButtonAccessKey =
+ this._getLocalizedString("notifyBarUpdateButtonAccessKey");
+
+ // We reuse the existing message, even if it expects a username, until we
+ // switch to the final terminology in bug 1144856.
+ var displayHost = this._getShortDisplayHost(aOldLogin.hostname);
+ var notificationText = this._getLocalizedString("updatePasswordMsg",
+ [displayHost]);
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var self = this;
+
+ // Notification is a PopupNotification
+ if (aNotifyObj == this._getPopupNote()) {
+ aOldLogin.hostname = aNewLogin.hostname;
+ aOldLogin.formSubmitURL = aNewLogin.formSubmitURL;
+ aOldLogin.password = aNewLogin.password;
+ aOldLogin.username = aNewLogin.username;
+ this._showLoginCaptureDoorhanger(aOldLogin, "password-change");
+ } else {
+ var dontChangeButtonText =
+ this._getLocalizedString("notifyBarDontChangeButtonText");
+ var dontChangeButtonAccessKey =
+ this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
+ var buttons = [
+ // "Yes" button
+ {
+ label: changeButtonText,
+ accessKey: changeButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ self._updateLogin(aOldLogin, aNewLogin);
+ }
+ },
+
+ // "No" button
+ {
+ label: dontChangeButtonText,
+ accessKey: dontChangeButtonAccessKey,
+ popup: null,
+ callback: function(aNotifyObj, aButton) {
+ // do nothing
+ }
+ }
+ ];
+
+ this._showLoginNotification(aNotifyObj, "password-change",
+ notificationText, buttons);
+ }
+
+ let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+ Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
+ },
+
+
+ /**
+ * Shows the Change Password dialog.
+ */
+ _showChangeLoginDialog(aOldLogin, aNewLogin) {
+ const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
+
+ var dialogText;
+ if (aOldLogin.username)
+ dialogText = this._getLocalizedString(
+ "updatePasswordMsg",
+ [aOldLogin.username]);
+ else
+ dialogText = this._getLocalizedString(
+ "updatePasswordMsgNoUser");
+
+ var dialogTitle = this._getLocalizedString(
+ "passwordChangeTitle");
+
+ // returns 0 for yes, 1 for no.
+ var ok = !this._promptService.confirmEx(this._chromeWindow,
+ dialogTitle, dialogText, buttonFlags,
+ null, null, null,
+ null, {});
+ if (ok) {
+ this.log("Updating password for user " + aOldLogin.username);
+ this._updateLogin(aOldLogin, aNewLogin);
+ }
+
+ let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+ Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
+ },
+
+
+ /**
+ * Called when we detect a password change in a form submission, but we
+ * don't know which existing login (username) it's for. Asks the user
+ * to select a username and confirm the password change.
+ *
+ * Note: The caller doesn't know the username for aNewLogin, so this
+ * function fills in .username and .usernameField with the values
+ * from the login selected by the user.
+ *
+ * Note; XPCOM stupidity: |count| is just |logins.length|.
+ */
+ promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
+ this.log("promptToChangePasswordWithUsernames with count:", count);
+
+ var usernames = logins.map(l => l.username);
+ var dialogText = this._getLocalizedString("userSelectText");
+ var dialogTitle = this._getLocalizedString("passwordChangeTitle");
+ var selectedIndex = { value: null };
+
+ // If user selects ok, outparam.value is set to the index
+ // of the selected username.
+ var ok = this._promptService.select(this._chromeWindow,
+ dialogTitle, dialogText,
+ usernames.length, usernames,
+ selectedIndex);
+ if (ok) {
+ // Now that we know which login to use, modify its password.
+ var selectedLogin = logins[selectedIndex.value];
+ this.log("Updating password for user " + selectedLogin.username);
+ var newLoginWithUsername = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ newLoginWithUsername.init(aNewLogin.hostname,
+ aNewLogin.formSubmitURL, aNewLogin.httpRealm,
+ selectedLogin.username, aNewLogin.password,
+ selectedLogin.userNameField, aNewLogin.passwordField);
+ this._updateLogin(selectedLogin, newLoginWithUsername);
+ }
+ },
+
+
+
+
+ /* ---------- Internal Methods ---------- */
+
+
+
+
+ _updateLogin(login, aNewLogin = null) {
+ var now = Date.now();
+ var propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ if (aNewLogin) {
+ propBag.setProperty("formSubmitURL", aNewLogin.formSubmitURL);
+ propBag.setProperty("hostname", aNewLogin.hostname);
+ propBag.setProperty("password", aNewLogin.password);
+ propBag.setProperty("username", aNewLogin.username);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+ this._pwmgr.modifyLogin(login, propBag);
+ },
+
+ /**
+ * Given a content DOM window, returns the chrome window and browser it's in.
+ */
+ _getChromeWindow: function (aWindow) {
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ let browser = win.gBrowser.getBrowserForContentWindow(aWindow);
+ if (browser) {
+ return { win, browser };
+ }
+ }
+ return null;
+ },
+
+ _getNotifyWindow: function () {
+ // Some sites pop up a temporary login window, which disappears
+ // upon submission of credentials. We want to put the notification
+ // bar in the opener window if this seems to be happening.
+ if (this._opener) {
+ let chromeDoc = this._chromeWindow.document.documentElement;
+
+ // Check to see if the current window was opened with chrome
+ // disabled, and if so use the opener window. But if the window
+ // has been used to visit other pages (ie, has a history),
+ // assume it'll stick around and *don't* use the opener.
+ if (chromeDoc.getAttribute("chromehidden") && !this._browser.canGoBack) {
+ this.log("Using opener window for notification bar.");
+ return this._getChromeWindow(this._opener);
+ }
+ }
+
+ return { win: this._chromeWindow, browser: this._browser };
+ },
+
+ /**
+ * Returns the popup notification to this prompter,
+ * or null if there isn't one available.
+ */
+ _getPopupNote : function () {
+ let popupNote = null;
+
+ try {
+ let { win: notifyWin } = this._getNotifyWindow();
+
+ // .wrappedJSObject needed here -- see bug 422974 comment 5.
+ popupNote = notifyWin.wrappedJSObject.PopupNotifications;
+ } catch (e) {
+ this.log("Popup notifications not available on window");
+ }
+
+ return popupNote;
+ },
+
+
+ /**
+ * Returns the notification box to this prompter, or null if there isn't
+ * a notification box available.
+ */
+ _getNotifyBox : function () {
+ let notifyBox = null;
+
+ try {
+ let { win: notifyWin } = this._getNotifyWindow();
+
+ // .wrappedJSObject needed here -- see bug 422974 comment 5.
+ notifyBox = notifyWin.wrappedJSObject.getNotificationBox(notifyWin);
+ } catch (e) {
+ this.log("Notification bars not available on window");
+ }
+
+ return notifyBox;
+ },
+
+
+ /**
+ * The user might enter a login that isn't the one we prefilled, but
+ * is the same as some other existing login. So, pick a login with a
+ * matching username, or return null.
+ */
+ _repickSelectedLogin : function (foundLogins, username) {
+ for (var i = 0; i < foundLogins.length; i++)
+ if (foundLogins[i].username == username)
+ return foundLogins[i];
+ return null;
+ },
+
+
+ /**
+ * Can be called as:
+ * _getLocalizedString("key1");
+ * _getLocalizedString("key2", ["arg1"]);
+ * _getLocalizedString("key3", ["arg1", "arg2"]);
+ * (etc)
+ *
+ * Returns the localized string for the specified key,
+ * formatted if required.
+ *
+ */
+ _getLocalizedString : function (key, formatArgs) {
+ if (formatArgs)
+ return this._strBundle.formatStringFromName(
+ key, formatArgs, formatArgs.length);
+ return this._strBundle.GetStringFromName(key);
+ },
+
+
+ /**
+ * Sanitizes the specified username, by stripping quotes and truncating if
+ * it's too long. This helps prevent an evil site from messing with the
+ * "save password?" prompt too much.
+ */
+ _sanitizeUsername : function (username) {
+ if (username.length > 30) {
+ username = username.substring(0, 30);
+ username += this._ellipsis;
+ }
+ return username.replace(/['"]/g, "");
+ },
+
+
+ /**
+ * The aURI parameter may either be a string uri, or an nsIURI instance.
+ *
+ * Returns the hostname to use in a nsILoginInfo object (for example,
+ * "http://example.com").
+ */
+ _getFormattedHostname : function (aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI, null, null);
+ }
+
+ return uri.scheme + "://" + uri.hostPort;
+ },
+
+
+ /**
+ * Converts a login's hostname field (a URL) to a short string for
+ * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
+ * "ftp://www.site.co.uk" --> "site.co.uk".
+ */
+ _getShortDisplayHost: function (aURIString) {
+ var displayHost;
+
+ var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
+ getService(Ci.nsIEffectiveTLDService);
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+ try {
+ var uri = Services.io.newURI(aURIString, null, null);
+ var baseDomain = eTLDService.getBaseDomain(uri);
+ displayHost = idnService.convertToDisplayIDN(baseDomain, {});
+ } catch (e) {
+ this.log("_getShortDisplayHost couldn't process " + aURIString);
+ }
+
+ if (!displayHost)
+ displayHost = aURIString;
+
+ return displayHost;
+ },
+
+
+ /**
+ * Returns the hostname and realm for which authentication is being
+ * requested, in the format expected to be used with nsILoginInfo.
+ */
+ _getAuthTarget : function (aChannel, aAuthInfo) {
+ var hostname, realm;
+
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ this.log("getAuthTarget is for proxy auth");
+ if (!(aChannel instanceof Ci.nsIProxiedChannel))
+ throw new Error("proxy auth needs nsIProxiedChannel");
+
+ var info = aChannel.proxyInfo;
+ if (!info)
+ throw new Error("proxy auth needs nsIProxyInfo");
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+ hostname = "moz-proxy://" +
+ idnService.convertUTF8toACE(info.host) +
+ ":" + info.port;
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ }
+
+ hostname = this._getFormattedHostname(aChannel.URI);
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ },
+
+
+ /**
+ * Returns [username, password] as extracted from aAuthInfo (which
+ * holds this info after having prompted the user).
+ *
+ * If the authentication was for a Windows domain, we'll prepend the
+ * return username with the domain. (eg, "domain\user")
+ */
+ _GetAuthInfo : function (aAuthInfo) {
+ var username, password;
+
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
+ username = aAuthInfo.domain + "\\" + aAuthInfo.username;
+ else
+ username = aAuthInfo.username;
+
+ password = aAuthInfo.password;
+
+ return [username, password];
+ },
+
+
+ /**
+ * Given a username (possibly in DOMAIN\user form) and password, parses the
+ * domain out of the username if necessary and sets domain, username and
+ * password on the auth information object.
+ */
+ _SetAuthInfo : function (aAuthInfo, username, password) {
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ var idx = username.indexOf("\\");
+ if (idx == -1) {
+ aAuthInfo.username = username;
+ } else {
+ aAuthInfo.domain = username.substring(0, idx);
+ aAuthInfo.username = username.substring(idx + 1);
+ }
+ } else {
+ aAuthInfo.username = username;
+ }
+ aAuthInfo.password = password;
+ },
+
+ _newAsyncPromptConsumer : function(aCallback, aContext) {
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ callback: aCallback,
+ context: aContext,
+ cancel: function() {
+ this.callback.onAuthCancelled(this.context, false);
+ this.callback = null;
+ this.context = null;
+ }
+ };
+ },
+
+ /**
+ * This function looks for existing logins that can be updated
+ * to match a submitted login, instead of creating a new one.
+ *
+ * Given a login and a loginList, it filters the login list
+ * to find every login with either the same username as aLogin
+ * or with the same password as aLogin and an empty username
+ * so the user can add a username.
+ *
+ * @param {nsILoginInfo} aLogin
+ * login to use as filter.
+ * @param {nsILoginInfo[]} aLoginList
+ * Array of logins to filter.
+ * @returns {nsILoginInfo[]} the filtered array of logins.
+ */
+ _filterUpdatableLogins(aLogin, aLoginList) {
+ return aLoginList.filter(l => l.username == aLogin.username ||
+ (l.password == aLogin.password &&
+ !l.username));
+ },
+
+}; // end of LoginManagerPrompter implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerPrompter.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("LoginManagerPrompter");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerPromptFactory, LoginManagerPrompter];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/passwordmgr/passwordmgr.manifest b/toolkit/components/passwordmgr/passwordmgr.manifest
new file mode 100644
index 0000000000..72e9ccffb8
--- /dev/null
+++ b/toolkit/components/passwordmgr/passwordmgr.manifest
@@ -0,0 +1,17 @@
+component {cb9e0de8-3598-4ed7-857b-827f011ad5d8} nsLoginManager.js
+contract @mozilla.org/login-manager;1 {cb9e0de8-3598-4ed7-857b-827f011ad5d8}
+component {749e62f4-60ae-4569-a8a2-de78b649660e} nsLoginManagerPrompter.js
+contract @mozilla.org/passwordmanager/authpromptfactory;1 {749e62f4-60ae-4569-a8a2-de78b649660e}
+component {8aa66d77-1bbb-45a6-991e-b8f47751c291} nsLoginManagerPrompter.js
+contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
+component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
+contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
+#ifdef ANDROID
+component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
+contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
+#else
+component {c00c432d-a0c9-46d7-bef6-9c45b4d07341} storage-json.js
+contract @mozilla.org/login-manager/storage/json;1 {c00c432d-a0c9-46d7-bef6-9c45b4d07341}
+#endif
+component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
+contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} \ No newline at end of file
diff --git a/toolkit/components/passwordmgr/storage-json.js b/toolkit/components/passwordmgr/storage-json.js
new file mode 100644
index 0000000000..20834d45b5
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-json.js
@@ -0,0 +1,514 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * nsILoginManagerStorage implementation for the JSON back-end.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
+ "resource://gre/modules/LoginImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+this.LoginManagerStorage_json = function () {};
+
+this.LoginManagerStorage_json.prototype = {
+ classID: Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
+
+ __crypto: null, // nsILoginManagerCrypto service
+ get _crypto() {
+ if (!this.__crypto)
+ this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
+ getService(Ci.nsILoginManagerCrypto);
+ return this.__crypto;
+ },
+
+ initialize() {
+ try {
+ // Force initialization of the crypto module.
+ // See bug 717490 comment 17.
+ this._crypto;
+
+ // Set the reference to LoginStore synchronously.
+ let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "logins.json");
+ this._store = new LoginStore(jsonPath);
+
+ return Task.spawn(function* () {
+ // Load the data asynchronously.
+ this.log("Opening database at", this._store.path);
+ yield this._store.load();
+
+ // The import from previous versions operates the first time
+ // that this built-in storage back-end is used. This may be
+ // later than expected, in case add-ons have registered an
+ // alternate storage that disabled the default one.
+ try {
+ if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
+ return;
+ }
+ } catch (ex) {
+ // If the preference does not exist, we need to import.
+ }
+
+ // Import only happens asynchronously.
+ let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
+ "signons.sqlite");
+ if (yield OS.File.exists(sqlitePath)) {
+ let loginImport = new LoginImport(this._store, sqlitePath);
+ // Failures during import, for example due to a corrupt
+ // file or a schema version that is too old, will not
+ // prevent us from marking the operation as completed.
+ // At the next startup, we will not try the import again.
+ yield loginImport.import().catch(Cu.reportError);
+ this._store.saveSoon();
+ }
+
+ // We won't attempt import again on next startup.
+ Services.prefs.setBoolPref("signon.importedFromSqlite", true);
+ }.bind(this)).catch(Cu.reportError);
+ } catch (e) {
+ this.log("Initialization failed:", e);
+ throw new Error("Initialization failed");
+ }
+ },
+
+ /**
+ * Internal method used by regression tests only. It is called before
+ * replacing this storage module with a new instance.
+ */
+ terminate() {
+ this._store._saver.disarm();
+ return this._store._save();
+ },
+
+ addLogin(login) {
+ this._store.ensureDataReady();
+
+ // Throws if there are bogus values.
+ LoginHelper.checkLoginValues(login);
+
+ let [encUsername, encPassword, encType] = this._encryptLogin(login);
+
+ // Clone the login, so we don't modify the caller's object.
+ let loginClone = login.clone();
+
+ // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
+ loginClone.QueryInterface(Ci.nsILoginMetaInfo);
+ if (loginClone.guid) {
+ if (!this._isGuidUnique(loginClone.guid))
+ throw new Error("specified GUID already exists");
+ } else {
+ loginClone.guid = gUUIDGenerator.generateUUID().toString();
+ }
+
+ // Set timestamps
+ let currentTime = Date.now();
+ if (!loginClone.timeCreated)
+ loginClone.timeCreated = currentTime;
+ if (!loginClone.timeLastUsed)
+ loginClone.timeLastUsed = currentTime;
+ if (!loginClone.timePasswordChanged)
+ loginClone.timePasswordChanged = currentTime;
+ if (!loginClone.timesUsed)
+ loginClone.timesUsed = 1;
+
+ this._store.data.logins.push({
+ id: this._store.data.nextId++,
+ hostname: loginClone.hostname,
+ httpRealm: loginClone.httpRealm,
+ formSubmitURL: loginClone.formSubmitURL,
+ usernameField: loginClone.usernameField,
+ passwordField: loginClone.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: loginClone.guid,
+ encType: encType,
+ timeCreated: loginClone.timeCreated,
+ timeLastUsed: loginClone.timeLastUsed,
+ timePasswordChanged: loginClone.timePasswordChanged,
+ timesUsed: loginClone.timesUsed
+ });
+ this._store.saveSoon();
+
+ // Send a notification that a login was added.
+ LoginHelper.notifyStorageChanged("addLogin", loginClone);
+ return loginClone;
+ },
+
+ removeLogin(login) {
+ this._store.ensureDataReady();
+
+ let [idToDelete, storedLogin] = this._getIdForLogin(login);
+ if (!idToDelete)
+ throw new Error("No matching logins");
+
+ let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
+ if (foundIndex != -1) {
+ this._store.data.logins.splice(foundIndex, 1);
+ this._store.saveSoon();
+ }
+
+ LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
+ },
+
+ modifyLogin(oldLogin, newLoginData) {
+ this._store.ensureDataReady();
+
+ let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
+ if (!idToModify)
+ throw new Error("No matching logins");
+
+ let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
+
+ // Check if the new GUID is duplicate.
+ if (newLogin.guid != oldStoredLogin.guid &&
+ !this._isGuidUnique(newLogin.guid)) {
+ throw new Error("specified GUID already exists");
+ }
+
+ // Look for an existing entry in case key properties changed.
+ if (!newLogin.matches(oldLogin, true)) {
+ let logins = this.findLogins({}, newLogin.hostname,
+ newLogin.formSubmitURL,
+ newLogin.httpRealm);
+
+ if (logins.some(login => newLogin.matches(login, true)))
+ throw new Error("This login already exists.");
+ }
+
+ // Get the encrypted value of the username and password.
+ let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
+
+ for (let loginItem of this._store.data.logins) {
+ if (loginItem.id == idToModify) {
+ loginItem.hostname = newLogin.hostname;
+ loginItem.httpRealm = newLogin.httpRealm;
+ loginItem.formSubmitURL = newLogin.formSubmitURL;
+ loginItem.usernameField = newLogin.usernameField;
+ loginItem.passwordField = newLogin.passwordField;
+ loginItem.encryptedUsername = encUsername;
+ loginItem.encryptedPassword = encPassword;
+ loginItem.guid = newLogin.guid;
+ loginItem.encType = encType;
+ loginItem.timeCreated = newLogin.timeCreated;
+ loginItem.timeLastUsed = newLogin.timeLastUsed;
+ loginItem.timePasswordChanged = newLogin.timePasswordChanged;
+ loginItem.timesUsed = newLogin.timesUsed;
+ this._store.saveSoon();
+ break;
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
+ },
+
+ /**
+ * @return {nsILoginInfo[]}
+ */
+ getAllLogins(count) {
+ let [logins, ids] = this._searchLogins({});
+
+ // decrypt entries for caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_getAllLogins: returning", logins.length, "logins.");
+ if (count)
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins(count, matchData) {
+ let realMatchData = {};
+ let options = {};
+ // Convert nsIPropertyBag to normal JS object
+ let propEnum = matchData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
+ }
+
+ let [logins, ids] = this._searchLogins(realMatchData, options);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ /**
+ * Private method to perform arbitrary searches on any field. Decryption is
+ * left to the caller.
+ *
+ * Returns [logins, ids] for logins that match the arguments, where logins
+ * is an array of encrypted nsLoginInfo and ids is an array of associated
+ * ids in the database.
+ */
+ _searchLogins(matchData, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ this._store.ensureDataReady();
+
+ function match(aLogin) {
+ for (let field in matchData) {
+ let wantedValue = matchData[field];
+ switch (field) {
+ case "formSubmitURL":
+ if (wantedValue != null) {
+ // Historical compatibility requires this special case
+ if (aLogin.formSubmitURL == "") {
+ break;
+ }
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
+ case "hostname":
+ if (wantedValue != null) { // needed for formSubmitURL fall through
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
+ // Normal cases.
+ case "httpRealm":
+ case "id":
+ case "usernameField":
+ case "passwordField":
+ case "encryptedUsername":
+ case "encryptedPassword":
+ case "guid":
+ case "encType":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ if (wantedValue == null && aLogin[field]) {
+ return false;
+ } else if (aLogin[field] != wantedValue) {
+ return false;
+ }
+ break;
+ // Fail if caller requests an unknown property.
+ default:
+ throw new Error("Unexpected field: " + field);
+ }
+ }
+ return true;
+ }
+
+ let foundLogins = [], foundIds = [];
+ for (let loginItem of this._store.data.logins) {
+ if (match(loginItem)) {
+ // Create the new nsLoginInfo object, push to array
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init(loginItem.hostname, loginItem.formSubmitURL,
+ loginItem.httpRealm, loginItem.encryptedUsername,
+ loginItem.encryptedPassword, loginItem.usernameField,
+ loginItem.passwordField);
+ // set nsILoginMetaInfo values
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.guid = loginItem.guid;
+ login.timeCreated = loginItem.timeCreated;
+ login.timeLastUsed = loginItem.timeLastUsed;
+ login.timePasswordChanged = loginItem.timePasswordChanged;
+ login.timesUsed = loginItem.timesUsed;
+ foundLogins.push(login);
+ foundIds.push(loginItem.id);
+ }
+ }
+
+ this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData,
+ "with options", aOptions);
+ return [foundLogins, foundIds];
+ },
+
+ /**
+ * Removes all logins from storage.
+ */
+ removeAllLogins() {
+ this._store.ensureDataReady();
+
+ this.log("Removing all logins");
+ this._store.data.logins = [];
+ this._store.saveSoon();
+
+ LoginHelper.notifyStorageChanged("removeAllLogins", null);
+ },
+
+ findLogins(count, hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_findLogins: returning", logins.length, "logins");
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+ countLogins(hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ this.log("_countLogins: counted logins:", logins.length);
+ return logins.length;
+ },
+
+ get uiBusy() {
+ return this._crypto.uiBusy;
+ },
+
+ get isLoggedIn() {
+ return this._crypto.isLoggedIn;
+ },
+
+ /**
+ * Returns an array with two items: [id, login]. If the login was not
+ * found, both items will be null. The returned login contains the actual
+ * stored login (useful for looking at the actual nsILoginMetaInfo values).
+ */
+ _getIdForLogin(login) {
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (login[field] != '')
+ matchData[field] = login[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ let id = null;
+ let foundLogin = null;
+
+ // The specified login isn't encrypted, so we need to ensure
+ // the logins we're comparing with are decrypted. We decrypt one entry
+ // at a time, lest _decryptLogins return fewer entries and screw up
+ // indices between the two.
+ for (let i = 0; i < logins.length; i++) {
+ let [decryptedLogin] = this._decryptLogins([logins[i]]);
+
+ if (!decryptedLogin || !decryptedLogin.equals(login))
+ continue;
+
+ // We've found a match, set id and break
+ foundLogin = decryptedLogin;
+ id = ids[i];
+ break;
+ }
+
+ return [id, foundLogin];
+ },
+
+ /**
+ * Checks to see if the specified GUID already exists.
+ */
+ _isGuidUnique(guid) {
+ this._store.ensureDataReady();
+
+ return this._store.data.logins.every(l => l.guid != guid);
+ },
+
+ /**
+ * Returns the encrypted username, password, and encrypton type for the specified
+ * login. Can throw if the user cancels a master password entry.
+ */
+ _encryptLogin(login) {
+ let encUsername = this._crypto.encrypt(login.username);
+ let encPassword = this._crypto.encrypt(login.password);
+ let encType = this._crypto.defaultEncType;
+
+ return [encUsername, encPassword, encType];
+ },
+
+ /**
+ * Decrypts username and password fields in the provided array of
+ * logins.
+ *
+ * The entries specified by the array will be decrypted, if possible.
+ * An array of successfully decrypted logins will be returned. The return
+ * value should be given to external callers (since still-encrypted
+ * entries are useless), whereas internal callers generally don't want
+ * to lose unencrypted entries (eg, because the user clicked Cancel
+ * instead of entering their master password)
+ */
+ _decryptLogins(logins) {
+ let result = [];
+
+ for (let login of logins) {
+ try {
+ login.username = this._crypto.decrypt(login.username);
+ login.password = this._crypto.decrypt(login.password);
+ } catch (e) {
+ // If decryption failed (corrupt entry?), just skip it.
+ // Rethrow other errors (like canceling entry of a master pw)
+ if (e.result == Cr.NS_ERROR_FAILURE)
+ continue;
+ throw e;
+ }
+ result.push(login);
+ }
+
+ return result;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_json.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);
diff --git a/toolkit/components/passwordmgr/storage-mozStorage.js b/toolkit/components/passwordmgr/storage-mozStorage.js
new file mode 100644
index 0000000000..7fc9e57fd5
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-mozStorage.js
@@ -0,0 +1,1262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const DB_VERSION = 6; // The database schema version
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+/**
+ * Object that manages a database transaction properly so consumers don't have
+ * to worry about it throwing.
+ *
+ * @param aDatabase
+ * The mozIStorageConnection to start a transaction on.
+ */
+function Transaction(aDatabase) {
+ this._db = aDatabase;
+
+ this._hasTransaction = false;
+ try {
+ this._db.beginTransaction();
+ this._hasTransaction = true;
+ } catch (e) { /* om nom nom exceptions */ }
+}
+
+Transaction.prototype = {
+ commit : function() {
+ if (this._hasTransaction)
+ this._db.commitTransaction();
+ },
+
+ rollback : function() {
+ if (this._hasTransaction)
+ this._db.rollbackTransaction();
+ },
+};
+
+
+function LoginManagerStorage_mozStorage() { }
+
+LoginManagerStorage_mozStorage.prototype = {
+
+ classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
+ Ci.nsIInterfaceRequestor]),
+ getInterface : function(aIID) {
+ if (aIID.equals(Ci.nsIVariant)) {
+ // Allows unwrapping the JavaScript object for regression tests.
+ return this;
+ }
+
+ if (aIID.equals(Ci.mozIStorageConnection)) {
+ return this._dbConnection;
+ }
+
+ throw new Components.Exception("Interface not available", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ __crypto : null, // nsILoginManagerCrypto service
+ get _crypto() {
+ if (!this.__crypto)
+ this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
+ getService(Ci.nsILoginManagerCrypto);
+ return this.__crypto;
+ },
+
+ __profileDir: null, // nsIFile for the user's profile dir
+ get _profileDir() {
+ if (!this.__profileDir)
+ this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ return this.__profileDir;
+ },
+
+ __storageService: null, // Storage service for using mozStorage
+ get _storageService() {
+ if (!this.__storageService)
+ this.__storageService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ return this.__storageService;
+ },
+
+ __uuidService: null,
+ get _uuidService() {
+ if (!this.__uuidService)
+ this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ return this.__uuidService;
+ },
+
+
+ // The current database schema.
+ _dbSchema: {
+ tables: {
+ moz_logins: "id INTEGER PRIMARY KEY," +
+ "hostname TEXT NOT NULL," +
+ "httpRealm TEXT," +
+ "formSubmitURL TEXT," +
+ "usernameField TEXT NOT NULL," +
+ "passwordField TEXT NOT NULL," +
+ "encryptedUsername TEXT NOT NULL," +
+ "encryptedPassword TEXT NOT NULL," +
+ "guid TEXT," +
+ "encType INTEGER," +
+ "timeCreated INTEGER," +
+ "timeLastUsed INTEGER," +
+ "timePasswordChanged INTEGER," +
+ "timesUsed INTEGER",
+ // Changes must be reflected in this._dbAreExpectedColumnsPresent(),
+ // this._searchLogins(), and this.modifyLogin().
+
+ moz_disabledHosts: "id INTEGER PRIMARY KEY," +
+ "hostname TEXT UNIQUE ON CONFLICT REPLACE",
+
+ moz_deleted_logins: "id INTEGER PRIMARY KEY," +
+ "guid TEXT," +
+ "timeDeleted INTEGER",
+ },
+ indices: {
+ moz_logins_hostname_index: {
+ table: "moz_logins",
+ columns: ["hostname"]
+ },
+ moz_logins_hostname_formSubmitURL_index: {
+ table: "moz_logins",
+ columns: ["hostname", "formSubmitURL"]
+ },
+ moz_logins_hostname_httpRealm_index: {
+ table: "moz_logins",
+ columns: ["hostname", "httpRealm"]
+ },
+ moz_logins_guid_index: {
+ table: "moz_logins",
+ columns: ["guid"]
+ },
+ moz_logins_encType_index: {
+ table: "moz_logins",
+ columns: ["encType"]
+ }
+ }
+ },
+ _dbConnection : null, // The database connection
+ _dbStmts : null, // Database statements for memoization
+
+ _signonsFile : null, // nsIFile for "signons.sqlite"
+
+
+ /*
+ * Internal method used by regression tests only. It overrides the default
+ * database location.
+ */
+ initWithFile : function(aDBFile) {
+ if (aDBFile)
+ this._signonsFile = aDBFile;
+
+ this.initialize();
+ },
+
+
+ initialize : function () {
+ this._dbStmts = {};
+
+ let isFirstRun;
+ try {
+ // Force initialization of the crypto module.
+ // See bug 717490 comment 17.
+ this._crypto;
+
+ // If initWithFile is calling us, _signonsFile may already be set.
+ if (!this._signonsFile) {
+ // Initialize signons.sqlite
+ this._signonsFile = this._profileDir.clone();
+ this._signonsFile.append("signons.sqlite");
+ }
+ this.log("Opening database at " + this._signonsFile.path);
+
+ // Initialize the database (create, migrate as necessary)
+ isFirstRun = this._dbInit();
+
+ this._initialized = true;
+
+ return Promise.resolve();
+ } catch (e) {
+ this.log("Initialization failed: " + e);
+ // If the import fails on first run, we want to delete the db
+ if (isFirstRun && e == "Import failed")
+ this._dbCleanup(false);
+ throw new Error("Initialization failed");
+ }
+ },
+
+
+ /**
+ * Internal method used by regression tests only. It is called before
+ * replacing this storage module with a new instance.
+ */
+ terminate : function () {
+ return Promise.resolve();
+ },
+
+
+ addLogin : function (login) {
+ // Throws if there are bogus values.
+ LoginHelper.checkLoginValues(login);
+
+ let [encUsername, encPassword, encType] = this._encryptLogin(login);
+
+ // Clone the login, so we don't modify the caller's object.
+ let loginClone = login.clone();
+
+ // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
+ loginClone.QueryInterface(Ci.nsILoginMetaInfo);
+ if (loginClone.guid) {
+ if (!this._isGuidUnique(loginClone.guid))
+ throw new Error("specified GUID already exists");
+ } else {
+ loginClone.guid = this._uuidService.generateUUID().toString();
+ }
+
+ // Set timestamps
+ let currentTime = Date.now();
+ if (!loginClone.timeCreated)
+ loginClone.timeCreated = currentTime;
+ if (!loginClone.timeLastUsed)
+ loginClone.timeLastUsed = currentTime;
+ if (!loginClone.timePasswordChanged)
+ loginClone.timePasswordChanged = currentTime;
+ if (!loginClone.timesUsed)
+ loginClone.timesUsed = 1;
+
+ let query =
+ "INSERT INTO moz_logins " +
+ "(hostname, httpRealm, formSubmitURL, usernameField, " +
+ "passwordField, encryptedUsername, encryptedPassword, " +
+ "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " +
+ "timesUsed) " +
+ "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
+ ":passwordField, :encryptedUsername, :encryptedPassword, " +
+ ":guid, :encType, :timeCreated, :timeLastUsed, " +
+ ":timePasswordChanged, :timesUsed)";
+
+ let params = {
+ hostname: loginClone.hostname,
+ httpRealm: loginClone.httpRealm,
+ formSubmitURL: loginClone.formSubmitURL,
+ usernameField: loginClone.usernameField,
+ passwordField: loginClone.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: loginClone.guid,
+ encType: encType,
+ timeCreated: loginClone.timeCreated,
+ timeLastUsed: loginClone.timeLastUsed,
+ timePasswordChanged: loginClone.timePasswordChanged,
+ timesUsed: loginClone.timesUsed
+ };
+
+ let stmt;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("addLogin failed: " + e.name + " : " + e.message);
+ throw new Error("Couldn't write to database, login not added.");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Send a notification that a login was added.
+ LoginHelper.notifyStorageChanged("addLogin", loginClone);
+ return loginClone;
+ },
+
+
+ removeLogin : function (login) {
+ let [idToDelete, storedLogin] = this._getIdForLogin(login);
+ if (!idToDelete)
+ throw new Error("No matching logins");
+
+ // Execute the statement & remove from DB
+ let query = "DELETE FROM moz_logins WHERE id = :id";
+ let params = { id: idToDelete };
+ let stmt;
+ let transaction = new Transaction(this._dbConnection);
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ this.storeDeletedLogin(storedLogin);
+ transaction.commit();
+ } catch (e) {
+ this.log("_removeLogin failed: " + e.name + " : " + e.message);
+ transaction.rollback();
+ throw new Error("Couldn't write to database, login not removed.");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
+ },
+
+ modifyLogin : function (oldLogin, newLoginData) {
+ let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
+ if (!idToModify)
+ throw new Error("No matching logins");
+
+ let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
+
+ // Check if the new GUID is duplicate.
+ if (newLogin.guid != oldStoredLogin.guid &&
+ !this._isGuidUnique(newLogin.guid)) {
+ throw new Error("specified GUID already exists");
+ }
+
+ // Look for an existing entry in case key properties changed.
+ if (!newLogin.matches(oldLogin, true)) {
+ let logins = this.findLogins({}, newLogin.hostname,
+ newLogin.formSubmitURL,
+ newLogin.httpRealm);
+
+ if (logins.some(login => newLogin.matches(login, true)))
+ throw new Error("This login already exists.");
+ }
+
+ // Get the encrypted value of the username and password.
+ let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
+
+ let query =
+ "UPDATE moz_logins " +
+ "SET hostname = :hostname, " +
+ "httpRealm = :httpRealm, " +
+ "formSubmitURL = :formSubmitURL, " +
+ "usernameField = :usernameField, " +
+ "passwordField = :passwordField, " +
+ "encryptedUsername = :encryptedUsername, " +
+ "encryptedPassword = :encryptedPassword, " +
+ "guid = :guid, " +
+ "encType = :encType, " +
+ "timeCreated = :timeCreated, " +
+ "timeLastUsed = :timeLastUsed, " +
+ "timePasswordChanged = :timePasswordChanged, " +
+ "timesUsed = :timesUsed " +
+ "WHERE id = :id";
+
+ let params = {
+ id: idToModify,
+ hostname: newLogin.hostname,
+ httpRealm: newLogin.httpRealm,
+ formSubmitURL: newLogin.formSubmitURL,
+ usernameField: newLogin.usernameField,
+ passwordField: newLogin.passwordField,
+ encryptedUsername: encUsername,
+ encryptedPassword: encPassword,
+ guid: newLogin.guid,
+ encType: encType,
+ timeCreated: newLogin.timeCreated,
+ timeLastUsed: newLogin.timeLastUsed,
+ timePasswordChanged: newLogin.timePasswordChanged,
+ timesUsed: newLogin.timesUsed
+ };
+
+ let stmt;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("modifyLogin failed: " + e.name + " : " + e.message);
+ throw new Error("Couldn't write to database, login not modified.");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
+ },
+
+
+ /**
+ * Returns an array of nsILoginInfo.
+ */
+ getAllLogins : function (count) {
+ let [logins, ids] = this._searchLogins({});
+
+ // decrypt entries for caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_getAllLogins: returning " + logins.length + " logins.");
+ if (count)
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ /**
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * @return {nsILoginInfo[]} which are decrypted.
+ */
+ searchLogins : function(count, matchData) {
+ let realMatchData = {};
+ let options = {};
+ // Convert nsIPropertyBag to normal JS object
+ let propEnum = matchData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
+ }
+
+ let [logins, ids] = this._searchLogins(realMatchData, options);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ /**
+ * Private method to perform arbitrary searches on any field. Decryption is
+ * left to the caller.
+ *
+ * Returns [logins, ids] for logins that match the arguments, where logins
+ * is an array of encrypted nsLoginInfo and ids is an array of associated
+ * ids in the database.
+ */
+ _searchLogins : function (matchData, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ let conditions = [], params = {};
+
+ for (let field in matchData) {
+ let value = matchData[field];
+ let condition = "";
+ switch (field) {
+ case "formSubmitURL":
+ if (value != null) {
+ // Historical compatibility requires this special case
+ condition = "formSubmitURL = '' OR ";
+ }
+ // Fall through
+ case "hostname":
+ if (value != null) {
+ condition += `${field} = :${field}`;
+ params[field] = value;
+ let valueURI;
+ try {
+ if (aOptions.schemeUpgrades && (valueURI = Services.io.newURI(value, null, null)) &&
+ valueURI.scheme == "https") {
+ condition += ` OR ${field} = :http${field}`;
+ params["http" + field] = "http://" + valueURI.hostPort;
+ }
+ } catch (ex) {
+ // newURI will throw for some values (e.g. chrome://FirefoxAccounts)
+ // but those URLs wouldn't support upgrades anyways.
+ }
+ break;
+ }
+ // Fall through
+ // Normal cases.
+ case "httpRealm":
+ case "id":
+ case "usernameField":
+ case "passwordField":
+ case "encryptedUsername":
+ case "encryptedPassword":
+ case "guid":
+ case "encType":
+ case "timeCreated":
+ case "timeLastUsed":
+ case "timePasswordChanged":
+ case "timesUsed":
+ if (value == null) {
+ condition = field + " isnull";
+ } else {
+ condition = field + " = :" + field;
+ params[field] = value;
+ }
+ break;
+ // Fail if caller requests an unknown property.
+ default:
+ throw new Error("Unexpected field: " + field);
+ }
+ if (condition) {
+ conditions.push(condition);
+ }
+ }
+
+ // Build query
+ let query = "SELECT * FROM moz_logins";
+ if (conditions.length) {
+ conditions = conditions.map(c => "(" + c + ")");
+ query += " WHERE " + conditions.join(" AND ");
+ }
+
+ let stmt;
+ let logins = [], ids = [];
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ // We can't execute as usual here, since we're iterating over rows
+ while (stmt.executeStep()) {
+ // Create the new nsLoginInfo object, push to array
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init(stmt.row.hostname, stmt.row.formSubmitURL,
+ stmt.row.httpRealm, stmt.row.encryptedUsername,
+ stmt.row.encryptedPassword, stmt.row.usernameField,
+ stmt.row.passwordField);
+ // set nsILoginMetaInfo values
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.guid = stmt.row.guid;
+ login.timeCreated = stmt.row.timeCreated;
+ login.timeLastUsed = stmt.row.timeLastUsed;
+ login.timePasswordChanged = stmt.row.timePasswordChanged;
+ login.timesUsed = stmt.row.timesUsed;
+ logins.push(login);
+ ids.push(stmt.row.id);
+ }
+ } catch (e) {
+ this.log("_searchLogins failed: " + e.name + " : " + e.message);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ this.log("_searchLogins: returning " + logins.length + " logins");
+ return [logins, ids];
+ },
+
+ /**
+ * Moves a login to the deleted logins table
+ */
+ storeDeletedLogin : function(aLogin) {
+ let stmt = null;
+ try {
+ this.log("Storing " + aLogin.guid + " in deleted passwords\n");
+ let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)";
+ let params = { guid: aLogin.guid,
+ timeDeleted: Date.now() };
+ let stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (ex) {
+ throw ex;
+ } finally {
+ if (stmt)
+ stmt.reset();
+ }
+ },
+
+
+ /**
+ * Removes all logins from storage.
+ */
+ removeAllLogins : function () {
+ this.log("Removing all logins");
+ let query;
+ let stmt;
+ let transaction = new Transaction(this._dbConnection);
+
+ // Disabled hosts kept, as one presumably doesn't want to erase those.
+ // TODO: Add these items to the deleted items table once we've sorted
+ // out the issues from bug 756701
+ query = "DELETE FROM moz_logins";
+ try {
+ stmt = this._dbCreateStatement(query);
+ stmt.execute();
+ transaction.commit();
+ } catch (e) {
+ this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
+ transaction.rollback();
+ throw new Error("Couldn't write to database");
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ LoginHelper.notifyStorageChanged("removeAllLogins", null);
+ },
+
+
+ findLogins : function (count, hostname, formSubmitURL, httpRealm) {
+ let loginData = {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (loginData[field] != '')
+ matchData[field] = loginData[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ // Decrypt entries found for the caller.
+ logins = this._decryptLogins(logins);
+
+ this.log("_findLogins: returning " + logins.length + " logins");
+ count.value = logins.length; // needed for XPCOM
+ return logins;
+ },
+
+
+ countLogins : function (hostname, formSubmitURL, httpRealm) {
+
+ let _countLoginsHelper = (hostname, formSubmitURL, httpRealm) => {
+ // Do checks for null and empty strings, adjust conditions and params
+ let [conditions, params] =
+ this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
+
+ let query = "SELECT COUNT(1) AS numLogins FROM moz_logins";
+ if (conditions.length) {
+ conditions = conditions.map(c => "(" + c + ")");
+ query += " WHERE " + conditions.join(" AND ");
+ }
+
+ let stmt, numLogins;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.executeStep();
+ numLogins = stmt.row.numLogins;
+ } catch (e) {
+ this.log("_countLogins failed: " + e.name + " : " + e.message);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ return numLogins;
+ };
+
+ let resultLogins = _countLoginsHelper(hostname, formSubmitURL, httpRealm);
+ this.log("_countLogins: counted logins: " + resultLogins);
+ return resultLogins;
+ },
+
+
+ get uiBusy() {
+ return this._crypto.uiBusy;
+ },
+
+
+ get isLoggedIn() {
+ return this._crypto.isLoggedIn;
+ },
+
+
+ /**
+ * Returns an array with two items: [id, login]. If the login was not
+ * found, both items will be null. The returned login contains the actual
+ * stored login (useful for looking at the actual nsILoginMetaInfo values).
+ */
+ _getIdForLogin : function (login) {
+ let matchData = { };
+ for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+ if (login[field] != '')
+ matchData[field] = login[field];
+ let [logins, ids] = this._searchLogins(matchData);
+
+ let id = null;
+ let foundLogin = null;
+
+ // The specified login isn't encrypted, so we need to ensure
+ // the logins we're comparing with are decrypted. We decrypt one entry
+ // at a time, lest _decryptLogins return fewer entries and screw up
+ // indices between the two.
+ for (let i = 0; i < logins.length; i++) {
+ let [decryptedLogin] = this._decryptLogins([logins[i]]);
+
+ if (!decryptedLogin || !decryptedLogin.equals(login))
+ continue;
+
+ // We've found a match, set id and break
+ foundLogin = decryptedLogin;
+ id = ids[i];
+ break;
+ }
+
+ return [id, foundLogin];
+ },
+
+
+ /**
+ * Adjusts the WHERE conditions and parameters for statements prior to the
+ * statement being created. This fixes the cases where nulls are involved
+ * and the empty string is supposed to be a wildcard match
+ */
+ _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) {
+ let conditions = [], params = {};
+
+ if (hostname == null) {
+ conditions.push("hostname isnull");
+ } else if (hostname != '') {
+ conditions.push("hostname = :hostname");
+ params["hostname"] = hostname;
+ }
+
+ if (formSubmitURL == null) {
+ conditions.push("formSubmitURL isnull");
+ } else if (formSubmitURL != '') {
+ conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
+ params["formSubmitURL"] = formSubmitURL;
+ }
+
+ if (httpRealm == null) {
+ conditions.push("httpRealm isnull");
+ } else if (httpRealm != '') {
+ conditions.push("httpRealm = :httpRealm");
+ params["httpRealm"] = httpRealm;
+ }
+
+ return [conditions, params];
+ },
+
+
+ /**
+ * Checks to see if the specified GUID already exists.
+ */
+ _isGuidUnique : function (guid) {
+ let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid";
+ let params = { guid: guid };
+
+ let stmt, numLogins;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.executeStep();
+ numLogins = stmt.row.numLogins;
+ } catch (e) {
+ this.log("_isGuidUnique failed: " + e.name + " : " + e.message);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ return (numLogins == 0);
+ },
+
+
+ /**
+ * Returns the encrypted username, password, and encrypton type for the specified
+ * login. Can throw if the user cancels a master password entry.
+ */
+ _encryptLogin : function (login) {
+ let encUsername = this._crypto.encrypt(login.username);
+ let encPassword = this._crypto.encrypt(login.password);
+ let encType = this._crypto.defaultEncType;
+
+ return [encUsername, encPassword, encType];
+ },
+
+
+ /**
+ * Decrypts username and password fields in the provided array of
+ * logins.
+ *
+ * The entries specified by the array will be decrypted, if possible.
+ * An array of successfully decrypted logins will be returned. The return
+ * value should be given to external callers (since still-encrypted
+ * entries are useless), whereas internal callers generally don't want
+ * to lose unencrypted entries (eg, because the user clicked Cancel
+ * instead of entering their master password)
+ */
+ _decryptLogins : function (logins) {
+ let result = [];
+
+ for (let login of logins) {
+ try {
+ login.username = this._crypto.decrypt(login.username);
+ login.password = this._crypto.decrypt(login.password);
+ } catch (e) {
+ // If decryption failed (corrupt entry?), just skip it.
+ // Rethrow other errors (like canceling entry of a master pw)
+ if (e.result == Cr.NS_ERROR_FAILURE)
+ continue;
+ throw e;
+ }
+ result.push(login);
+ }
+
+ return result;
+ },
+
+
+ // Database Creation & Access
+
+ /**
+ * Creates a statement, wraps it, and then does parameter replacement
+ * Returns the wrapped statement for execution. Will use memoization
+ * so that statements can be reused.
+ */
+ _dbCreateStatement : function (query, params) {
+ let wrappedStmt = this._dbStmts[query];
+ // Memoize the statements
+ if (!wrappedStmt) {
+ this.log("Creating new statement for query: " + query);
+ wrappedStmt = this._dbConnection.createStatement(query);
+ this._dbStmts[query] = wrappedStmt;
+ }
+ // Replace parameters, must be done 1 at a time
+ if (params)
+ for (let i in params)
+ wrappedStmt.params[i] = params[i];
+ return wrappedStmt;
+ },
+
+
+ /**
+ * Attempts to initialize the database. This creates the file if it doesn't
+ * exist, performs any migrations, etc. Return if this is the first run.
+ */
+ _dbInit : function () {
+ this.log("Initializing Database");
+ let isFirstRun = false;
+ try {
+ this._dbConnection = this._storageService.openDatabase(this._signonsFile);
+ // Get the version of the schema in the file. It will be 0 if the
+ // database has not been created yet.
+ let version = this._dbConnection.schemaVersion;
+ if (version == 0) {
+ this._dbCreate();
+ isFirstRun = true;
+ } else if (version != DB_VERSION) {
+ this._dbMigrate(version);
+ }
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+ // Database is corrupted, so we backup the database, then throw
+ // causing initialization to fail and a new db to be created next use
+ this._dbCleanup(true);
+ }
+ throw e;
+ }
+
+ Services.obs.addObserver(this, "profile-before-change", false);
+ return isFirstRun;
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "profile-before-change":
+ Services.obs.removeObserver(this, "profile-before-change");
+ this._dbClose();
+ break;
+ }
+ },
+
+ _dbCreate: function () {
+ this.log("Creating Database");
+ this._dbCreateSchema();
+ this._dbConnection.schemaVersion = DB_VERSION;
+ },
+
+
+ _dbCreateSchema : function () {
+ this._dbCreateTables();
+ this._dbCreateIndices();
+ },
+
+
+ _dbCreateTables : function () {
+ this.log("Creating Tables");
+ for (let name in this._dbSchema.tables)
+ this._dbConnection.createTable(name, this._dbSchema.tables[name]);
+ },
+
+
+ _dbCreateIndices : function () {
+ this.log("Creating Indices");
+ for (let name in this._dbSchema.indices) {
+ let index = this._dbSchema.indices[name];
+ let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
+ "(" + index.columns.join(", ") + ")";
+ this._dbConnection.executeSimpleSQL(statement);
+ }
+ },
+
+
+ _dbMigrate : function (oldVersion) {
+ this.log("Attempting to migrate from version " + oldVersion);
+
+ if (oldVersion > DB_VERSION) {
+ this.log("Downgrading to version " + DB_VERSION);
+ // User's DB is newer. Sanity check that our expected columns are
+ // present, and if so mark the lower version and merrily continue
+ // on. If the columns are borked, something is wrong so blow away
+ // the DB and start from scratch. [Future incompatible upgrades
+ // should swtich to a different table or file.]
+
+ if (!this._dbAreExpectedColumnsPresent())
+ throw Components.Exception("DB is missing expected columns",
+ Cr.NS_ERROR_FILE_CORRUPTED);
+
+ // Change the stored version to the current version. If the user
+ // runs the newer code again, it will see the lower version number
+ // and re-upgrade (to fixup any entries the old code added).
+ this._dbConnection.schemaVersion = DB_VERSION;
+ return;
+ }
+
+ // Upgrade to newer version...
+
+ let transaction = new Transaction(this._dbConnection);
+
+ try {
+ for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
+ this.log("Upgrading to version " + v + "...");
+ let migrateFunction = "_dbMigrateToVersion" + v;
+ this[migrateFunction]();
+ }
+ } catch (e) {
+ this.log("Migration failed: " + e);
+ transaction.rollback();
+ throw e;
+ }
+
+ this._dbConnection.schemaVersion = DB_VERSION;
+ transaction.commit();
+ this.log("DB migration completed.");
+ },
+
+
+ /**
+ * Version 2 adds a GUID column. Existing logins are assigned a random GUID.
+ */
+ _dbMigrateToVersion2 : function () {
+ // Check to see if GUID column already exists, add if needed
+ let query;
+ if (!this._dbColumnExists("guid")) {
+ query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT";
+ this._dbConnection.executeSimpleSQL(query);
+
+ query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)";
+ this._dbConnection.executeSimpleSQL(query);
+ }
+
+ // Get a list of IDs for existing logins
+ let ids = [];
+ query = "SELECT id FROM moz_logins WHERE guid isnull";
+ let stmt;
+ try {
+ stmt = this._dbCreateStatement(query);
+ while (stmt.executeStep())
+ ids.push(stmt.row.id);
+ } catch (e) {
+ this.log("Failed getting IDs: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Generate a GUID for each login and update the DB.
+ query = "UPDATE moz_logins SET guid = :guid WHERE id = :id";
+ for (let id of ids) {
+ let params = {
+ id: id,
+ guid: this._uuidService.generateUUID().toString()
+ };
+
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting GUID: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Version 3 adds a encType column.
+ */
+ _dbMigrateToVersion3 : function () {
+ // Check to see if encType column already exists, add if needed
+ let query;
+ if (!this._dbColumnExists("encType")) {
+ query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER";
+ this._dbConnection.executeSimpleSQL(query);
+
+ query = "CREATE INDEX IF NOT EXISTS " +
+ "moz_logins_encType_index ON moz_logins (encType)";
+ this._dbConnection.executeSimpleSQL(query);
+ }
+
+ // Get a list of existing logins
+ let logins = [];
+ let stmt;
+ query = "SELECT id, encryptedUsername, encryptedPassword " +
+ "FROM moz_logins WHERE encType isnull";
+ try {
+ stmt = this._dbCreateStatement(query);
+ while (stmt.executeStep()) {
+ let params = { id: stmt.row.id };
+ // We will tag base64 logins correctly, but no longer support their use.
+ if (stmt.row.encryptedUsername.charAt(0) == '~' ||
+ stmt.row.encryptedPassword.charAt(0) == '~')
+ params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64;
+ else
+ params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
+ logins.push(params);
+ }
+ } catch (e) {
+ this.log("Failed getting logins: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Determine encryption type for each login and update the DB.
+ query = "UPDATE moz_logins SET encType = :encType WHERE id = :id";
+ for (let params of logins) {
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting encType: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged,
+ * and timesUsed columns
+ */
+ _dbMigrateToVersion4 : function () {
+ let query;
+ // Add the new columns, if needed.
+ for (let column of ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
+ if (!this._dbColumnExists(column)) {
+ query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER";
+ this._dbConnection.executeSimpleSQL(query);
+ }
+ }
+
+ // Get a list of IDs for existing logins.
+ let ids = [];
+ let stmt;
+ query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " +
+ "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull";
+ try {
+ stmt = this._dbCreateStatement(query);
+ while (stmt.executeStep())
+ ids.push(stmt.row.id);
+ } catch (e) {
+ this.log("Failed getting IDs: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Initialize logins with current time.
+ query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " +
+ "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id";
+ let params = {
+ id: null,
+ initTime: Date.now()
+ };
+ for (let id of ids) {
+ params.id = id;
+ try {
+ stmt = this._dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting timestamps: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Version 5 adds the moz_deleted_logins table
+ */
+ _dbMigrateToVersion5 : function () {
+ if (!this._dbConnection.tableExists("moz_deleted_logins")) {
+ this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins);
+ }
+ },
+
+ /**
+ * Version 6 migrates all the hosts from
+ * moz_disabledHosts to the permission manager.
+ */
+ _dbMigrateToVersion6 : function () {
+ let disabledHosts = [];
+ let query = "SELECT hostname FROM moz_disabledHosts";
+ let stmt;
+
+ try {
+ stmt = this._dbCreateStatement(query);
+
+ while (stmt.executeStep()) {
+ disabledHosts.push(stmt.row.hostname);
+ }
+
+ for (let host of disabledHosts) {
+ try {
+ let uri = Services.io.newURI(host, null, null);
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ } catch (e) {
+ this.log(`_dbMigrateToVersion6 failed: ${e.name} : ${e.message}`);
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ query = "DELETE FROM moz_disabledHosts";
+ this._dbConnection.executeSimpleSQL(query);
+ },
+
+ /**
+ * Sanity check to ensure that the columns this version of the code expects
+ * are present in the DB we're using.
+ */
+ _dbAreExpectedColumnsPresent : function () {
+ let query = "SELECT " +
+ "id, " +
+ "hostname, " +
+ "httpRealm, " +
+ "formSubmitURL, " +
+ "usernameField, " +
+ "passwordField, " +
+ "encryptedUsername, " +
+ "encryptedPassword, " +
+ "guid, " +
+ "encType, " +
+ "timeCreated, " +
+ "timeLastUsed, " +
+ "timePasswordChanged, " +
+ "timesUsed " +
+ "FROM moz_logins";
+ try {
+ let stmt = this._dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ } catch (e) {
+ return false;
+ }
+
+ query = "SELECT " +
+ "id, " +
+ "hostname " +
+ "FROM moz_disabledHosts";
+ try {
+ let stmt = this._dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ } catch (e) {
+ return false;
+ }
+
+ this.log("verified that expected columns are present in DB.");
+ return true;
+ },
+
+
+ /**
+ * Checks to see if the named column already exists.
+ */
+ _dbColumnExists : function (columnName) {
+ let query = "SELECT " + columnName + " FROM moz_logins";
+ try {
+ let stmt = this._dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ _dbClose : function () {
+ this.log("Closing the DB connection.");
+ // Finalize all statements to free memory, avoid errors later
+ for (let query in this._dbStmts) {
+ let stmt = this._dbStmts[query];
+ stmt.finalize();
+ }
+ this._dbStmts = {};
+
+ if (this._dbConnection !== null) {
+ try {
+ this._dbConnection.close();
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+ this._dbConnection = null;
+ },
+
+ /**
+ * Called when database creation fails. Finalizes database statements,
+ * closes the database connection, deletes the database file.
+ */
+ _dbCleanup : function (backup) {
+ this.log("Cleaning up DB file - close & remove & backup=" + backup);
+
+ // Create backup file
+ if (backup) {
+ let backupFile = this._signonsFile.leafName + ".corrupt";
+ this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
+ }
+
+ this._dbClose();
+ this._signonsFile.remove(false);
+ }
+
+}; // end of nsLoginManagerStorage_mozStorage implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_mozStorage.prototype, "log", () => {
+ let logger = LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});
+
+var component = [LoginManagerStorage_mozStorage];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/passwordmgr/test/.eslintrc.js b/toolkit/components/passwordmgr/test/.eslintrc.js
new file mode 100644
index 0000000000..ca626f31ce
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/.eslintrc.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js",
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ],
+ "rules": {
+ "brace-style": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ },
+};
diff --git a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
new file mode 100644
index 0000000000..2fd8a31a3f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm
@@ -0,0 +1,295 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Shared functions generally available for testing login components.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "LoginTestUtils",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.import("resource://testing-common/TestUtils.jsm");
+
+const LoginInfo =
+ Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+// For now, we need consumers to provide a reference to Assert.jsm.
+var Assert = null;
+
+this.LoginTestUtils = {
+ set Assert(assert) {
+ Assert = assert; // eslint-disable-line no-native-reassign
+ },
+
+ /**
+ * Forces the storage module to save all data, and the Login Manager service
+ * to replace the storage module with a newly initialized instance.
+ */
+ * reloadData() {
+ Services.obs.notifyObservers(null, "passwordmgr-storage-replace", null);
+ yield TestUtils.topicObserved("passwordmgr-storage-replace-complete");
+ },
+
+ /**
+ * Erases all the data stored by the Login Manager service.
+ */
+ clearData() {
+ Services.logins.removeAllLogins();
+ for (let hostname of Services.logins.getAllDisabledHosts()) {
+ Services.logins.setLoginSavingEnabled(hostname, true);
+ }
+ },
+
+ /**
+ * Checks that the currently stored list of nsILoginInfo matches the provided
+ * array. The comparison uses the "equals" method of nsILoginInfo, that does
+ * not include nsILoginMetaInfo properties in the test.
+ */
+ checkLogins(expectedLogins) {
+ this.assertLoginListsEqual(Services.logins.getAllLogins(), expectedLogins);
+ },
+
+ /**
+ * Checks that the two provided arrays of nsILoginInfo have the same length,
+ * and every login in "expected" is also found in "actual". The comparison
+ * uses the "equals" method of nsILoginInfo, that does not include
+ * nsILoginMetaInfo properties in the test.
+ */
+ assertLoginListsEqual(actual, expected) {
+ Assert.equal(expected.length, actual.length);
+ Assert.ok(expected.every(e => actual.some(a => a.equals(e))));
+ },
+
+ /**
+ * Checks that the two provided arrays of strings contain the same values,
+ * maybe in a different order, case-sensitively.
+ */
+ assertDisabledHostsEqual(actual, expected) {
+ Assert.deepEqual(actual.sort(), expected.sort());
+ },
+
+ /**
+ * Checks whether the given time, expressed as the number of milliseconds
+ * since January 1, 1970, 00:00:00 UTC, falls within 30 seconds of now.
+ */
+ assertTimeIsAboutNow(timeMs) {
+ Assert.ok(Math.abs(timeMs - Date.now()) < 30000);
+ },
+};
+
+/**
+ * This object contains functions that return new instances of nsILoginInfo for
+ * every call. The returned instances can be compared using their "equals" or
+ * "matches" methods, or modified for the needs of the specific test being run.
+ *
+ * Any modification to the test data requires updating the tests accordingly, in
+ * particular the search tests.
+ */
+this.LoginTestUtils.testData = {
+ /**
+ * Returns a new nsILoginInfo for use with form submits.
+ *
+ * @param modifications
+ * Each property of this object replaces the property of the same name
+ * in the returned nsILoginInfo or nsILoginMetaInfo.
+ */
+ formLogin(modifications) {
+ let loginInfo = new LoginInfo("http://www3.example.com",
+ "http://www.example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password");
+ loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ if (modifications) {
+ for (let [name, value] of Object.entries(modifications)) {
+ loginInfo[name] = value;
+ }
+ }
+ return loginInfo;
+ },
+
+ /**
+ * Returns a new nsILoginInfo for use with HTTP authentication.
+ *
+ * @param modifications
+ * Each property of this object replaces the property of the same name
+ * in the returned nsILoginInfo or nsILoginMetaInfo.
+ */
+ authLogin(modifications) {
+ let loginInfo = new LoginInfo("http://www.example.org", null,
+ "The HTTP Realm", "the username",
+ "the password", "", "");
+ loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ if (modifications) {
+ for (let [name, value] of Object.entries(modifications)) {
+ loginInfo[name] = value;
+ }
+ }
+ return loginInfo;
+ },
+
+ /**
+ * Returns an array of typical nsILoginInfo that could be stored in the
+ * database.
+ */
+ loginList() {
+ return [
+ // --- Examples of form logins (subdomains of example.com) ---
+
+ // Simple form login with named fields for username and password.
+ new LoginInfo("http://www.example.com", "http://www.example.com", null,
+ "the username", "the password for www.example.com",
+ "form_field_username", "form_field_password"),
+
+ // Different schemes are treated as completely different sites.
+ new LoginInfo("https://www.example.com", "https://www.example.com", null,
+ "the username", "the password for https",
+ "form_field_username", "form_field_password"),
+
+ // Subdomains are treated as completely different sites.
+ new LoginInfo("https://example.com", "https://example.com", null,
+ "the username", "the password for example.com",
+ "form_field_username", "form_field_password"),
+
+ // Forms found on the same host, but with different hostnames in the
+ // "action" attribute, are handled independently.
+ new LoginInfo("http://www3.example.com", "http://www.example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www3.example.com", "https://www.example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www3.example.com", "http://example.com", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+
+ // It is not possible to store multiple passwords for the same username,
+ // however multiple passwords can be stored when the usernames differ.
+ // An empty username is a valid case and different from the others.
+ new LoginInfo("http://www4.example.com", "http://www4.example.com", null,
+ "username one", "password one",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www4.example.com", "http://www4.example.com", null,
+ "username two", "password two",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www4.example.com", "http://www4.example.com", null,
+ "", "password three",
+ "form_field_username", "form_field_password"),
+
+ // Username and passwords fields in forms may have no "name" attribute.
+ new LoginInfo("http://www5.example.com", "http://www5.example.com", null,
+ "multi username", "multi password", "", ""),
+
+ // Forms with PIN-type authentication will typically have no username.
+ new LoginInfo("http://www6.example.com", "http://www6.example.com", null,
+ "", "12345", "", "form_field_password"),
+
+ // --- Examples of authentication logins (subdomains of example.org) ---
+
+ // Simple HTTP authentication login.
+ new LoginInfo("http://www.example.org", null, "The HTTP Realm",
+ "the username", "the password", "", ""),
+
+ // Simple FTP authentication login.
+ new LoginInfo("ftp://ftp.example.org", null, "ftp://ftp.example.org",
+ "the username", "the password", "", ""),
+
+ // Multiple HTTP authentication logins can be stored for different realms.
+ new LoginInfo("http://www2.example.org", null, "The HTTP Realm",
+ "the username", "the password", "", ""),
+ new LoginInfo("http://www2.example.org", null, "The HTTP Realm Other",
+ "the username other", "the password other", "", ""),
+
+ // --- Both form and authentication logins (example.net) ---
+
+ new LoginInfo("http://example.net", "http://example.net", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://example.net", "http://www.example.net", null,
+ "the username", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://example.net", "http://www.example.net", null,
+ "username two", "the password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://example.net", null, "The HTTP Realm",
+ "the username", "the password", "", ""),
+ new LoginInfo("http://example.net", null, "The HTTP Realm Other",
+ "username two", "the password", "", ""),
+ new LoginInfo("ftp://example.net", null, "ftp://example.net",
+ "the username", "the password", "", ""),
+
+ // --- Examples of logins added by extensions (chrome scheme) ---
+
+ new LoginInfo("chrome://example_extension", null, "Example Login One",
+ "the username", "the password one", "", ""),
+ new LoginInfo("chrome://example_extension", null, "Example Login Two",
+ "the username", "the password two", "", ""),
+ ];
+ },
+};
+
+this.LoginTestUtils.recipes = {
+ getRecipeParent() {
+ let { LoginManagerParent } = Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
+ if (!LoginManagerParent.recipeParentPromise) {
+ return null;
+ }
+ return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
+ return recipeParent;
+ });
+ },
+};
+
+this.LoginTestUtils.masterPassword = {
+ masterPassword: "omgsecret!",
+
+ _set(enable) {
+ let oldPW, newPW;
+ if (enable) {
+ oldPW = "";
+ newPW = this.masterPassword;
+ } else {
+ oldPW = this.masterPassword;
+ newPW = "";
+ }
+
+ let secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"]
+ .getService(Ci.nsIPKCS11ModuleDB);
+ let slot = secmodDB.findSlotByName("");
+ if (!slot) {
+ throw new Error("Can't find slot");
+ }
+
+ // Set master password. Note that this does not log you in, so the next
+ // invocation of pwmgr can trigger a MP prompt.
+ let pk11db = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB);
+ let token = pk11db.findTokenByName("");
+ if (slot.status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED) {
+ dump("MP initialized to " + newPW + "\n");
+ token.initPassword(newPW);
+ } else {
+ token.checkPassword(oldPW);
+ dump("MP change from " + oldPW + " to " + newPW + "\n");
+ token.changePassword(oldPW, newPW);
+ }
+ },
+
+ enable() {
+ this._set(true);
+ },
+
+ disable() {
+ this._set(false);
+ },
+};
diff --git a/toolkit/components/passwordmgr/test/authenticate.sjs b/toolkit/components/passwordmgr/test/authenticate.sjs
new file mode 100644
index 0000000000..42edc32203
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/authenticate.sjs
@@ -0,0 +1,228 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false, formauth = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // formauth=1
+ match = /formauth=1/.exec(query);
+ if (match)
+ formauth = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ if (formauth && authPresent)
+ response.setStatusLine("1.0", 403, "Form authentication required");
+ else
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/passwordmgr/test/blank.html b/toolkit/components/passwordmgr/test/blank.html
new file mode 100644
index 0000000000..81ddc2235b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/blank.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/.eslintrc.js b/toolkit/components/passwordmgr/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/passwordmgr/test/browser/authenticate.sjs b/toolkit/components/passwordmgr/test/browser/authenticate.sjs
new file mode 100644
index 0000000000..fe2d2423cb
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/authenticate.sjs
@@ -0,0 +1,110 @@
+function handleRequest(request, response)
+{
+ var match;
+ var requestAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "test", expected_pass = "testpass", realm = "mochitest";
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+
+ if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini
new file mode 100644
index 0000000000..b175914367
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+support-files =
+ ../formsubmit.sjs
+ authenticate.sjs
+ form_basic.html
+ form_basic_iframe.html
+ formless_basic.html
+ form_same_origin_action.html
+ form_cross_origin_secure_action.html
+ head.js
+ insecure_test.html
+ insecure_test_subframe.html
+ multiple_forms.html
+ streamConverter_content.sjs
+
+[browser_autocomplete_insecure_warning.js]
+support-files =
+ form_cross_origin_insecure_action.html
+[browser_capture_doorhanger.js]
+support-files =
+ subtst_notifications_1.html
+ subtst_notifications_2.html
+ subtst_notifications_2pw_0un.html
+ subtst_notifications_2pw_1un_1text.html
+ subtst_notifications_3.html
+ subtst_notifications_4.html
+ subtst_notifications_5.html
+ subtst_notifications_6.html
+ subtst_notifications_8.html
+ subtst_notifications_9.html
+ subtst_notifications_10.html
+ subtst_notifications_change_p.html
+[browser_capture_doorhanger_httpsUpgrade.js]
+support-files =
+ subtst_notifications_1.html
+ subtst_notifications_8.html
+[browser_capture_doorhanger_window_open.js]
+support-files =
+ subtst_notifications_11.html
+ subtst_notifications_11_popup.html
+skip-if = os == "linux" # Bug 1312981, bug 1313136
+[browser_context_menu_autocomplete_interaction.js]
+[browser_username_select_dialog.js]
+support-files =
+ subtst_notifications_change_p.html
+[browser_DOMFormHasPassword.js]
+[browser_DOMInputPasswordAdded.js]
+[browser_exceptions_dialog.js]
+[browser_formless_submit_chrome.js]
+[browser_hasInsecureLoginForms.js]
+[browser_hasInsecureLoginForms_streamConverter.js]
+[browser_http_autofill.js]
+[browser_insecurePasswordConsoleWarning.js]
+support-files =
+ form_cross_origin_insecure_action.html
+[browser_master_password_autocomplete.js]
+[browser_notifications.js]
+[browser_notifications_username.js]
+[browser_notifications_password.js]
+[browser_notifications_2.js]
+skip-if = os == "linux" # Bug 1272849 Main action button disabled state intermittent
+[browser_passwordmgr_editing.js]
+skip-if = os == "linux"
+[browser_context_menu.js]
+[browser_context_menu_iframe.js]
+[browser_passwordmgr_contextmenu.js]
+subsuite = clipboard
+[browser_passwordmgr_fields.js]
+[browser_passwordmgr_observers.js]
+[browser_passwordmgr_sort.js]
+[browser_passwordmgr_switchtab.js]
+[browser_passwordmgrdlg.js]
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js
new file mode 100644
index 0000000000..80a0dd903e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js
@@ -0,0 +1,94 @@
+const ids = {
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+};
+
+function task(contentIds) {
+ let resolve;
+ let promise = new Promise(r => { resolve = r; });
+
+ function unexpectedContentEvent(evt) {
+ ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ if (content.location.href == "about:blank")
+ return;
+ removeEventListener("load", tabLoad, true);
+
+ gDoc = content.document;
+ gDoc.addEventListener("DOMFormHasPassword", unexpectedContentEvent, false);
+ gDoc.defaultView.setTimeout(test_inputAdd, 0);
+ }
+
+ function test_inputAdd() {
+ addEventListener("DOMFormHasPassword", test_inputAddHandler, false);
+ let input = gDoc.createElementNS("http://www.w3.org/1999/xhtml", "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentIds.INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
+ }
+
+ function test_inputAddHandler(evt) {
+ removeEventListener(evt.type, test_inputAddHandler, false);
+ is(evt.target.id, contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (added password element)");
+ gDoc.defaultView.setTimeout(test_inputChangeForm, 0);
+ }
+
+ function test_inputChangeForm() {
+ addEventListener("DOMFormHasPassword", test_inputChangeFormHandler, false);
+ let input = gDoc.getElementById(contentIds.INPUT_ID);
+ input.setAttribute("form", contentIds.FORM2_ID);
+ }
+
+ function test_inputChangeFormHandler(evt) {
+ removeEventListener(evt.type, test_inputChangeFormHandler, false);
+ is(evt.target.id, contentIds.FORM2_ID,
+ evt.type + " event targets correct form element (changed form)");
+ gDoc.defaultView.setTimeout(test_inputChangesType, 0);
+ }
+
+ function test_inputChangesType() {
+ addEventListener("DOMFormHasPassword", test_inputChangesTypeHandler, false);
+ let input = gDoc.getElementById(contentIds.CHANGE_INPUT_ID);
+ input.setAttribute("type", "password");
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ removeEventListener(evt.type, test_inputChangesTypeHandler, false);
+ is(evt.target.id, contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (changed type)");
+ gDoc.defaultView.setTimeout(finish, 0);
+ }
+
+ function finish() {
+ gDoc.removeEventListener("DOMFormHasPassword", unexpectedContentEvent, false);
+ resolve();
+ }
+
+ return promise;
+}
+
+add_task(function* () {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+
+ let promise = ContentTask.spawn(tab.linkedBrowser, ids, task);
+ tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," +
+ "<html><body>" +
+ "<form id='" + ids.FORM1_ID + "'>" +
+ "<input id='" + ids.CHANGE_INPUT_ID + "'></form>" +
+ "<form id='" + ids.FORM2_ID + "'></form>" +
+ "</body></html>");
+ yield promise;
+
+ ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js
new file mode 100644
index 0000000000..f54892e190
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js
@@ -0,0 +1,99 @@
+const consts = {
+ HTML_NS: "http://www.w3.org/1999/xhtml",
+
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+ BODY_INPUT_ID: "input3",
+};
+
+function task(contentConsts) {
+ let resolve;
+ let promise = new Promise(r => { resolve = r; });
+
+ function unexpectedContentEvent(evt) {
+ Assert.ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ removeEventListener("load", tabLoad, true);
+ gDoc = content.document;
+ // These events shouldn't escape to content.
+ gDoc.addEventListener("DOMInputPasswordAdded", unexpectedContentEvent, false);
+ gDoc.defaultView.setTimeout(test_inputAdd, 0);
+ }
+
+ function test_inputAdd() {
+ addEventListener("DOMInputPasswordAdded", test_inputAddHandler, false);
+ let input = gDoc.createElementNS(contentConsts.HTML_NS, "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentConsts.INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.getElementById(contentConsts.FORM1_ID).appendChild(input);
+ info("Done appending the input element");
+ }
+
+ function test_inputAddHandler(evt) {
+ removeEventListener(evt.type, test_inputAddHandler, false);
+ Assert.equal(evt.target.id, contentConsts.INPUT_ID,
+ evt.type + " event targets correct input element (added password element)");
+ gDoc.defaultView.setTimeout(test_inputAddOutsideForm, 0);
+ }
+
+ function test_inputAddOutsideForm() {
+ addEventListener("DOMInputPasswordAdded", test_inputAddOutsideFormHandler, false);
+ let input = gDoc.createElementNS(contentConsts.HTML_NS, "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentConsts.BODY_INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.body.appendChild(input);
+ info("Done appending the input element to the body");
+ }
+
+ function test_inputAddOutsideFormHandler(evt) {
+ removeEventListener(evt.type, test_inputAddOutsideFormHandler, false);
+ Assert.equal(evt.target.id, contentConsts.BODY_INPUT_ID,
+ evt.type + " event targets correct input element (added password element outside form)");
+ gDoc.defaultView.setTimeout(test_inputChangesType, 0);
+ }
+
+ function test_inputChangesType() {
+ addEventListener("DOMInputPasswordAdded", test_inputChangesTypeHandler, false);
+ let input = gDoc.getElementById(contentConsts.CHANGE_INPUT_ID);
+ input.setAttribute("type", "password");
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ removeEventListener(evt.type, test_inputChangesTypeHandler, false);
+ Assert.equal(evt.target.id, contentConsts.CHANGE_INPUT_ID,
+ evt.type + " event targets correct input element (changed type)");
+ gDoc.defaultView.setTimeout(completeTest, 0);
+ }
+
+ function completeTest() {
+ Assert.ok(true, "Test completed");
+ gDoc.removeEventListener("DOMInputPasswordAdded", unexpectedContentEvent, false);
+ resolve();
+ }
+
+ return promise;
+}
+
+add_task(function* () {
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ let promise = ContentTask.spawn(tab.linkedBrowser, consts, task);
+ tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," +
+ "<html><body>" +
+ "<form id='" + consts.FORM1_ID + "'>" +
+ "<input id='" + consts.CHANGE_INPUT_ID + "'></form>" +
+ "<form id='" + consts.FORM2_ID + "'></form>" +
+ "</body></html>");
+ yield promise;
+ gBrowser.removeCurrentTab();
+});
+
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
new file mode 100644
index 0000000000..6aa8e5cf7e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const EXPECTED_SUPPORT_URL = Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "insecure-password";
+
+add_task(function* test_clickInsecureFieldWarning() {
+ let url = "https://example.com" + DIRECTORY_PATH + "form_cross_origin_insecure_action.html";
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url,
+ }, function*(browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+ ok(popup, "Got popup");
+
+ let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ yield SimpleTest.promiseFocus(browser);
+ info("content window focused");
+
+ // Focus the username field to open the popup.
+ yield ContentTask.spawn(browser, null, function openAutocomplete() {
+ content.document.getElementById("form-basic-username").focus();
+ });
+
+ yield promiseShown;
+ ok(promiseShown, "autocomplete shown");
+
+ let warningItem = document.getAnonymousElementByAttribute(popup, "type", "insecureWarning");
+ ok(warningItem, "Got warning richlistitem");
+
+ yield BrowserTestUtils.waitForCondition(() => !warningItem.collapsed, "Wait for warning to show");
+
+ info("Clicking on warning");
+ let supportTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, EXPECTED_SUPPORT_URL);
+ EventUtils.synthesizeMouseAtCenter(warningItem, {});
+ let supportTab = yield supportTabPromise;
+ ok(supportTab, "Support tab opened");
+ yield BrowserTestUtils.removeTab(supportTab);
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
new file mode 100644
index 0000000000..b6bfdbf504
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
@@ -0,0 +1,600 @@
+/*
+ * Test capture popup notifications
+ */
+
+const BRAND_BUNDLE = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+const BRAND_SHORT_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login2 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "", "notifyp1", "", "pass");
+let login1B = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1B", "notifyp1B", "user", "pass");
+let login2B = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "", "notifyp1B", "", "pass");
+
+requestLongerTimeout(2);
+
+add_task(function* setup() {
+ // Load recipes for this test.
+ let recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.load({
+ siteRecipes: [{
+ hosts: ["example.org"],
+ usernameSelector: "#user",
+ passwordSelector: "#pass",
+ }],
+ });
+});
+
+add_task(function* test_remember_opens() {
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+});
+
+add_task(function* test_clickNever() {
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ is(true, Services.logins.getLoginSavingEnabled("http://example.com"),
+ "Checking for login saving enabled");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, NEVER_BUTTON);
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+
+ info("Make sure Never took effect");
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ is(false, Services.logins.getLoginSavingEnabled("http://example.com"),
+ "Checking for login saving disabled");
+ Services.logins.setLoginSavingEnabled("http://example.com", true);
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_clickRemember() {
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username used on the new entry");
+ is(login.password, "notifyp1", "Check the password used on the new entry");
+ is(login.timesUsed, 1, "Check times used on new entry");
+
+ info("Make sure Remember took effect and we don't prompt for an existing login");
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username used");
+ is(login.password, "notifyp1", "Check the password used");
+ is(login.timesUsed, 2, "Check times used incremented");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ // remove that login
+ Services.logins.removeLogin(login1);
+});
+
+/* signons.rememberSignons pref tests... */
+
+add_task(function* test_rememberSignonsFalse() {
+ info("Make sure we don't prompt with rememberSignons=false");
+ Services.prefs.setBoolPref("signon.rememberSignons", false);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_rememberSignonsTrue() {
+ info("Make sure we prompt with rememberSignons=true");
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+/* autocomplete=off tests... */
+
+add_task(function* test_autocompleteOffUsername() {
+ info("Check for notification popup when autocomplete=off present on username");
+
+ yield testSubmittingLoginForm("subtst_notifications_2.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_autocompleteOffPassword() {
+ info("Check for notification popup when autocomplete=off present on password");
+
+ yield testSubmittingLoginForm("subtst_notifications_3.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_autocompleteOffForm() {
+ info("Check for notification popup when autocomplete=off present on form");
+
+ yield testSubmittingLoginForm("subtst_notifications_4.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+
+add_task(function* test_noPasswordField() {
+ info("Check for no notification popup when no password field present");
+
+ yield testSubmittingLoginForm("subtst_notifications_5.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "null", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_pwOnlyLoginMatchesForm() {
+ info("Check for update popup when existing pw-only login matches form.");
+ Services.logins.addLogin(login2);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "checking for notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username");
+ is(login.password, "notifyp1", "Check the password");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(function* test_pwOnlyFormMatchesLogin() {
+ info("Check for no notification popup when pw-only form matches existing login.");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username");
+ is(login.password, "notifyp1", "Check the password");
+ is(login.timesUsed, 2, "Check times used");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(function* test_pwOnlyFormDoesntMatchExisting() {
+ info("Check for notification popup when pw-only form doesn't match existing login.");
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(function* test_changeUPLoginOnUPForm_dont() {
+ info("Check for change-password popup, u+p login on u+p form. (not changed)");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(function* test_changeUPLoginOnUPForm_change() {
+ info("Check for change-password popup, u+p login on u+p form.");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(function* test_changePLoginOnUPForm() {
+ info("Check for change-password popup, p-only login on u+p form.");
+ Services.logins.addLogin(login2);
+
+ yield testSubmittingLoginForm("subtst_notifications_9.html", function*(fieldValues) {
+ is(fieldValues.username, "", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ // no cleanup -- saved password to be used in the next test.
+});
+
+add_task(function* test_changePLoginOnPForm() {
+ info("Check for change-password popup, p-only login on p-only form.");
+
+ yield testSubmittingLoginForm("subtst_notifications_10.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("", "notifyp1");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password changed");
+ is(login.timesUsed, 3, "Check times used");
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(function* test_checkUPSaveText() {
+ info("Check text on a user+pass notification popup");
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ // Check the text, which comes from the localized saveLoginText string.
+ let notificationText = notif.message;
+ let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this login?";
+ is(expectedText, notificationText, "Checking text: " + notificationText);
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_checkPSaveText() {
+ info("Check text on a pass-only notification popup");
+
+ yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ // Check the text, which comes from the localized saveLoginTextNoUser string.
+ let notificationText = notif.message;
+ let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this password?";
+ is(expectedText, notificationText, "Checking text: " + notificationText);
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_capture2pw0un() {
+ info("Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there are no saved logins.");
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+});
+
+add_task(function* test_change2pw0unExistingDifferentUP() {
+ info("Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with a username and different password.");
+
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(function* test_change2pw0unExistingDifferentP() {
+ info("Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with no username and different password.");
+
+ Services.logins.addLogin(login2B);
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+ notif.remove();
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login2B);
+});
+
+add_task(function* test_change2pw0unExistingWithSameP() {
+ info("Check for no notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with a username and the same password.");
+
+ Services.logins.addLogin(login2);
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 2, "Check times used incremented");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(function* test_changeUPLoginOnPUpdateForm() {
+ info("Check for change-password popup, u+p login on password update form.");
+ Services.logins.addLogin(login1);
+
+ yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "got notification popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(function* test_recipeCaptureFields_NewLogin() {
+ info("Check that we capture the proper fields when a field recipe is in use.");
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+
+ // Sanity check, no logins should exist yet.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 0, "Should not have any logins yet");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+
+ }, "http://example.org"); // The recipe is for example.org
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+});
+
+add_task(function* test_recipeCaptureFields_ExistingLogin() {
+ info("Check that we capture the proper fields when a field recipe is in use " +
+ "and there is a matching login");
+
+ yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ }, "http://example.org");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 2, "Check times used incremented");
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function* test_noShowPasswordOnDismissal() {
+ info("Check for no Show Password field when the doorhanger is dismissed");
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ info("Opening popup");
+ let notif = getCaptureDoorhanger("password-save");
+ let { panel } = PopupNotifications;
+
+ info("Hiding popup.");
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ yield promiseHidden;
+
+ info("Clicking on anchor to reshow popup.");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ notif.anchorElement.click();
+ yield promiseShown;
+
+ let passwordVisiblityToggle = panel.querySelector("#password-notification-visibilityToggle");
+ is(passwordVisiblityToggle.hidden, true, "Check that the Show Password field is Hidden");
+ });
+});
+
+// TODO:
+// * existing login test, form has different password --> change password, no save prompt
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js
new file mode 100644
index 0000000000..9be0aa6316
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js
@@ -0,0 +1,123 @@
+/*
+ * Test capture popup notifications with HTTPS upgrades
+ */
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login1HTTPS = new nsLoginInfo("https://example.com", "https://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+
+add_task(function* test_httpsUpgradeCaptureFields_noChange() {
+ info("Check that we don't prompt to remember when capturing an upgraded login with no change");
+ Services.logins.addLogin(login1);
+ // Sanity check the HTTP login exists.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should have the HTTP login");
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP
+
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login still");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.hostname, "http://example.com", "Check the hostname is unchanged");
+ is(login.username, "notifyu1", "Check the username is unchanged");
+ is(login.password, "notifyp1", "Check the password is unchanged");
+ is(login.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(function* test_httpsUpgradeCaptureFields_changePW() {
+ info("Check that we prompt to change when capturing an upgraded login with a new PW");
+ Services.logins.addLogin(login1);
+ // Sanity check the HTTP login exists.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should have the HTTP login");
+
+ yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-change");
+ ok(notif, "checking for a change popup");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
+ }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login still");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.hostname, "https://example.com", "Check the hostname is upgraded");
+ is(login.formSubmitURL, "https://example.com", "Check the formSubmitURL is upgraded");
+ is(login.username, "notifyu1", "Check the username is unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function* test_httpsUpgradeCaptureFields_captureMatchingHTTP() {
+ info("Capture a new HTTP login which matches a stored HTTPS one.");
+ Services.logins.addLogin(login1HTTPS);
+
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(notif, "got notification popup");
+
+ is(Services.logins.getAllLogins().length, 1, "Should only have the HTTPS login");
+
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have both HTTP and HTTPS logins");
+ for (let login of logins) {
+ login = login.QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username used on the new entry");
+ is(login.password, "notifyp1", "Check the password used on the new entry");
+ is(login.timesUsed, 1, "Check times used on entry");
+ }
+
+ info("Make sure Remember took effect and we don't prompt for an existing HTTP login");
+ yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) {
+ is(fieldValues.username, "notifyu1", "Checking submitted username");
+ is(fieldValues.password, "notifyp1", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ ok(!notif, "checking for no notification popup");
+ });
+
+ logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have both HTTP and HTTPS still");
+
+ let httpsLogins = LoginHelper.searchLoginsWithObject({
+ hostname: "https://example.com",
+ });
+ is(httpsLogins.length, 1, "Check https logins count");
+ let httpsLogin = httpsLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ ok(httpsLogin.equals(login1HTTPS), "Check HTTPS login didn't change");
+ is(httpsLogin.timesUsed, 1, "Check times used");
+
+ let httpLogins = LoginHelper.searchLoginsWithObject({
+ hostname: "http://example.com",
+ });
+ is(httpLogins.length, 1, "Check http logins count");
+ let httpLogin = httpLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ ok(httpLogin.equals(login1), "Check HTTP login is as expected");
+ is(httpLogin.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeLogin(login1);
+ Services.logins.removeLogin(login1HTTPS);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
new file mode 100644
index 0000000000..1bcfec5eb1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
@@ -0,0 +1,144 @@
+/*
+ * Test capture popup notifications in content opened by window.open
+ */
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login2 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "notifyu2", "notifyp2", "user", "pass");
+
+
+function withTestTabUntilStorageChange(aPageFile, aTaskFn) {
+ function storageChangedObserved(subject, data) {
+ // Watch for actions triggered from a doorhanger (not cleanup tasks with removeLogin)
+ if (data == "removeLogin") {
+ return false;
+ }
+ return true;
+ }
+
+ let storageChangedPromised = TestUtils.topicObserved("passwordmgr-storage-changed",
+ storageChangedObserved);
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "http://mochi.test:8888" + DIRECTORY_PATH + aPageFile,
+ }, function*(browser) {
+ ok(true, "loaded " + aPageFile);
+ info("running test case task");
+ yield* aTaskFn();
+ info("waiting for storage change");
+ yield storageChangedPromised;
+ });
+}
+
+add_task(function* setup() {
+ yield SimpleTest.promiseFocus(window);
+});
+
+add_task(function* test_saveChromeHiddenAutoClose() {
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ // query arguments are: username, password, features, auto-close (delimited by '|')
+ let url = "subtst_notifications_11.html?notifyu1|notifyp1|" +
+ "menubar=no,toolbar=no,location=no|autoclose";
+ yield withTestTabUntilStorageChange(url, function*() {
+ info("waiting for popupshown");
+ yield notifShownPromise;
+ // the popup closes and the doorhanger should appear in the opener
+ let popup = getCaptureDoorhanger("password-save");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ // Sanity check, no logins should exist yet.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 0, "Should not have any logins yet");
+
+ clickDoorhangerButton(popup, REMEMBER_BUTTON);
+ });
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.timesUsed, 1, "Check times used on new entry");
+ is(login.username, "notifyu1", "Check the username used on the new entry");
+ is(login.password, "notifyp1", "Check the password used on the new entry");
+});
+
+add_task(function* test_changeChromeHiddenAutoClose() {
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ let url = "subtst_notifications_11.html?notifyu1|pass2|menubar=no,toolbar=no,location=no|autoclose";
+ yield withTestTabUntilStorageChange(url, function*() {
+ info("waiting for popupshown");
+ yield notifShownPromise;
+ let popup = getCaptureDoorhanger("password-change");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(popup, CHANGE_BUTTON);
+ });
+
+ // Check to make sure we updated the password, timestamps and use count for
+ // the login being changed with this form.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username");
+ is(login.password, "pass2", "Check password changed");
+ is(login.timesUsed, 2, "check .timesUsed incremented on change");
+ ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped");
+ ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged");
+
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(function* test_saveChromeVisibleSameWindow() {
+ // This test actually opens a new tab in the same window with default browser settings.
+ let url = "subtst_notifications_11.html?notifyu2|notifyp2||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ yield withTestTabUntilStorageChange(url, function*() {
+ yield notifShownPromise;
+ let popup = getCaptureDoorhanger("password-save");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu2", "notifyp2");
+ clickDoorhangerButton(popup, REMEMBER_BUTTON);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login now");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu2", "Check the username used on the new entry");
+ is(login.password, "notifyp2", "Check the password used on the new entry");
+ is(login.timesUsed, 1, "Check times used on new entry");
+});
+
+add_task(function* test_changeChromeVisibleSameWindow() {
+ let url = "subtst_notifications_11.html?notifyu2|pass2||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ yield withTestTabUntilStorageChange(url, function*() {
+ yield notifShownPromise;
+ let popup = getCaptureDoorhanger("password-change");
+ ok(popup, "got notification popup");
+ yield* checkDoorhangerUsernamePassword("notifyu2", "pass2");
+ clickDoorhangerButton(popup, CHANGE_BUTTON);
+ yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Check to make sure we updated the password, timestamps and use count for
+ // the login being changed with this form.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu2", "Check the username");
+ is(login.password, "pass2", "Check password changed");
+ is(login.timesUsed, 2, "check .timesUsed incremented on change");
+ ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped");
+ ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged");
+
+ // cleanup
+ login2.password = "pass2";
+ Services.logins.removeLogin(login2);
+ login2.password = "notifyp2";
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
new file mode 100644
index 0000000000..6cfcaa7c24
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
@@ -0,0 +1,432 @@
+/*
+ * Test the password manager context menu.
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The hostname for the test URIs.
+const TEST_HOSTNAME = "https://example.com";
+const MULTIPLE_FORMS_PAGE_PATH = "/browser/toolkit/components/passwordmgr/test/browser/multiple_forms.html";
+
+const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
+const POPUP_HEADER = document.getElementById("fill-login");
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(function* test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ });
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(function* test_context_menu_populate_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(function* test_context_menu_populate_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(function* test_context_menu_populate_username_with_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-username-2");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(function* test_context_menu_populate_username_with_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ }, function* (browser) {
+ yield openPasswordContextMenu(browser, "#test-username-2");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ CONTEXT_MENU.hidePopup();
+ });
+});
+
+/**
+ * Check if the password field is correctly filled when one
+ * login menuitem is clicked.
+ */
+add_task(function* test_context_menu_password_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+ let formDescriptions = yield ContentTask.spawn(browser, {}, function*() {
+ let forms = Array.from(content.document.getElementsByClassName("test-form"));
+ return forms.map((f) => f.getAttribute("description"));
+ });
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+
+ let passwordInputIds = yield ContentTask.spawn(browser, {description}, function*({description}) {
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ let passwords = Array.from(formElement.querySelectorAll("input[type='password']"));
+ return passwords.map((p) => p.id);
+ });
+
+ for (let inputId of passwordInputIds) {
+ info("Testing password field: " + inputId);
+
+ // Synthesize a right mouse click over the username input element.
+ yield openPasswordContextMenu(browser, "#" + inputId, function*() {
+ let inputDisabled = yield ContentTask
+ .spawn(browser, {inputId}, function*({inputId}) {
+ let input = content.document.getElementById(inputId);
+ return input.disabled || input.readOnly;
+ });
+
+ // If the password field is disabled or read-only, we want to see
+ // the disabled Fill Password popup header.
+ if (inputDisabled) {
+ Assert.ok(!POPUP_HEADER.hidden, "Popup menu is not hidden.");
+ Assert.ok(POPUP_HEADER.disabled, "Popup menu is disabled.");
+ CONTEXT_MENU.hidePopup();
+ }
+
+ return !inputDisabled;
+ });
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ // The only field affected by the password fill
+ // should be the target password field itself.
+ yield assertContextMenuFill(browser, description, null, inputId, 1);
+ yield ContentTask.spawn(browser, {inputId}, function*({inputId}) {
+ let passwordField = content.document.getElementById(inputId);
+ Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used");
+ });
+
+ CONTEXT_MENU.hidePopup();
+ }
+ }
+ });
+});
+
+/**
+ * Check if the form is correctly filled when one
+ * username context menu login menuitem is clicked.
+ */
+add_task(function* test_context_menu_username_login_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH,
+ }, function* (browser) {
+
+ let formDescriptions = yield ContentTask.spawn(browser, {}, function*() {
+ let forms = Array.from(content.document.getElementsByClassName("test-form"));
+ return forms.map((f) => f.getAttribute("description"));
+ });
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+ let usernameInputIds = yield ContentTask
+ .spawn(browser, {description}, function*({description}) {
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ let inputs = Array.from(formElement.querySelectorAll("input[type='text']"));
+ return inputs.map((p) => p.id);
+ });
+
+ for (let inputId of usernameInputIds) {
+ info("Testing username field: " + inputId);
+
+ // Synthesize a right mouse click over the username input element.
+ yield openPasswordContextMenu(browser, "#" + inputId, function*() {
+ let headerHidden = POPUP_HEADER.hidden;
+ let headerDisabled = POPUP_HEADER.disabled;
+
+ let data = {description, inputId, headerHidden, headerDisabled};
+ let shouldContinue = yield ContentTask.spawn(browser, data, function*(data) {
+ let {description, inputId, headerHidden, headerDisabled} = data;
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ let usernameField = content.document.getElementById(inputId);
+ // We always want to check if the first password field is filled,
+ // since this is the current behavior from the _fillForm function.
+ let passwordField = formElement.querySelector("input[type='password']");
+
+ // If we don't want to see the actual popup menu,
+ // check if the popup is hidden or disabled.
+ if (!passwordField || usernameField.disabled || usernameField.readOnly ||
+ passwordField.disabled || passwordField.readOnly) {
+ if (!passwordField) {
+ Assert.ok(headerHidden, "Popup menu is hidden.");
+ } else {
+ Assert.ok(!headerHidden, "Popup menu is not hidden.");
+ Assert.ok(headerDisabled, "Popup menu is disabled.");
+ }
+ return false;
+ }
+ return true;
+ });
+
+ if (!shouldContinue) {
+ CONTEXT_MENU.hidePopup();
+ }
+
+ return shouldContinue;
+ });
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ let passwordFieldId = yield ContentTask
+ .spawn(browser, {description}, function*({description}) {
+ let formElement = content.document.querySelector(`[description="${description}"]`);
+ return formElement.querySelector("input[type='password']").id;
+ });
+
+ // We shouldn't change any field that's not the target username field or the first password field
+ yield assertContextMenuFill(browser, description, inputId, passwordFieldId, 1);
+
+ yield ContentTask.spawn(browser, {passwordFieldId}, function*({passwordFieldId}) {
+ let passwordField = content.document.getElementById(passwordFieldId);
+ if (!passwordField.hasAttribute("expectedFail")) {
+ Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used");
+ }
+ });
+
+ CONTEXT_MENU.hidePopup();
+ }
+ }
+ });
+});
+
+/**
+ * Synthesize mouse clicks to open the password manager context menu popup
+ * for a target password input element.
+ *
+ * assertCallback should return true if we should continue or else false.
+ */
+function* openPasswordContextMenu(browser, passwordInput, assertCallback = null) {
+ // Synthesize a right mouse click over the password input element.
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(CONTEXT_MENU, "popupshown");
+ let eventDetails = {type: "contextmenu", button: 2};
+ BrowserTestUtils.synthesizeMouseAtCenter(passwordInput, eventDetails, browser);
+ yield contextMenuShownPromise;
+
+ if (assertCallback) {
+ let shouldContinue = yield assertCallback();
+ if (!shouldContinue) {
+ return;
+ }
+ }
+
+ // Synthesize a mouse click over the fill login menu header.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(POPUP_HEADER, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(POPUP_HEADER, {});
+ yield popupShownPromise;
+}
+
+/**
+ * Verify that only the expected form fields are filled.
+ */
+function* assertContextMenuFill(browser, formId, usernameFieldId, passwordFieldId, loginIndex) {
+ let popupMenu = document.getElementById("fill-login-popup");
+ let unchangedSelector = `[description="${formId}"] input:not(#${passwordFieldId})`;
+
+ if (usernameFieldId) {
+ unchangedSelector += `:not(#${usernameFieldId})`;
+ }
+
+ yield ContentTask.spawn(browser, {unchangedSelector}, function*({unchangedSelector}) {
+ let unchangedFields = content.document.querySelectorAll(unchangedSelector);
+
+ // Store the value of fields that should remain unchanged.
+ if (unchangedFields.length) {
+ for (let field of unchangedFields) {
+ field.setAttribute("original-value", field.value);
+ }
+ }
+ });
+
+ // Execute the default command of the specified login menuitem found in the context menu.
+ let loginItem = popupMenu.getElementsByClassName("context-login-item")[loginIndex];
+
+ // Find the used login by it's username (Use only unique usernames in this test).
+ let {username, password} = getLoginFromUsername(loginItem.label);
+
+ let data = {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector};
+ let continuePromise = ContentTask.spawn(browser, data, function*(data) {
+ let {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector} = data;
+ let form = content.document.querySelector(`[description="${formId}"]`);
+ yield ContentTaskUtils.waitForEvent(form, "input", "Username input value changed");
+
+ if (usernameFieldId) {
+ let usernameField = content.document.getElementById(usernameFieldId);
+
+ // If we have an username field, check if it's correctly filled
+ if (usernameField.getAttribute("expectedFail") == null) {
+ Assert.equal(username, usernameField.value, "Username filled and correct.");
+ }
+ }
+
+ if (passwordFieldId) {
+ let passwordField = content.document.getElementById(passwordFieldId);
+
+ // If we have a password field, check if it's correctly filled
+ if (passwordField && passwordField.getAttribute("expectedFail") == null) {
+ Assert.equal(password, passwordField.value, "Password filled and correct.");
+ }
+ }
+
+ let unchangedFields = content.document.querySelectorAll(unchangedSelector);
+
+ // Check that all fields that should not change have the same value as before.
+ if (unchangedFields.length) {
+ Assert.ok(() => {
+ for (let field of unchangedFields) {
+ if (field.value != field.getAttribute("original-value")) {
+ return false;
+ }
+ }
+ return true;
+ }, "Other fields were not changed.");
+ }
+ });
+
+ loginItem.doCommand();
+
+ return continuePromise;
+}
+
+/**
+ * Check if every login that matches the page hostname are available at the context menu.
+ * @param {Element} contextMenu
+ * @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure
+* we continue testing something useful.
+ */
+function checkMenu(contextMenu, expectedCount) {
+ let logins = loginList().filter(login => {
+ return LoginHelper.isOriginMatching(login.hostname, TEST_HOSTNAME, {
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ });
+ });
+ // Make an array of menuitems for easier comparison.
+ let menuitems = [...CONTEXT_MENU.getElementsByClassName("context-login-item")];
+ Assert.equal(menuitems.length, expectedCount, "Expected number of menu items");
+ Assert.ok(logins.every(l => menuitems.some(m => l.username == m.label)), "Every login have an item at the menu.");
+}
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/hostname combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
new file mode 100644
index 0000000000..1b37e3f791
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
@@ -0,0 +1,99 @@
+/*
+ * Test the password manager context menu interaction with autocomplete.
+ */
+
+"use strict";
+
+const TEST_HOSTNAME = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+
+var gUnexpectedIsTODO = false;
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(function* test_initialize() {
+ let autocompletePopup = document.getElementById("PopupAutoComplete");
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ autocompletePopup.removeEventListener("popupshowing", autocompleteUnexpectedPopupShowing);
+ });
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+ autocompletePopup.addEventListener("popupshowing", autocompleteUnexpectedPopupShowing);
+});
+
+add_task(function* test_context_menu_username() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH,
+ }, function* (browser) {
+ yield openContextMenu(browser, "#form-basic-username");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ Assert.equal(contextMenu.state, "open", "Context menu opened");
+ contextMenu.hidePopup();
+ });
+});
+
+add_task(function* test_context_menu_password() {
+ gUnexpectedIsTODO = true;
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH,
+ }, function* (browser) {
+ yield openContextMenu(browser, "#form-basic-password");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ Assert.equal(contextMenu.state, "open", "Context menu opened");
+ contextMenu.hidePopup();
+ });
+});
+
+function autocompleteUnexpectedPopupShowing(event) {
+ if (gUnexpectedIsTODO) {
+ todo(false, "Autocomplete shouldn't appear");
+ } else {
+ Assert.ok(false, "Autocomplete shouldn't appear");
+ }
+ event.target.hidePopup();
+}
+
+/**
+ * Synthesize mouse clicks to open the context menu popup
+ * for a target login input element.
+ */
+function* openContextMenu(browser, loginInput) {
+ // First synthesize a mousedown. We need this to get the focus event with the "contextmenu" event.
+ let eventDetails1 = {type: "mousedown", button: 2};
+ BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails1, browser);
+
+ // Then synthesize the contextmenu click over the input element.
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown");
+ let eventDetails = {type: "contextmenu", button: 2};
+ BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails, browser);
+ yield contextMenuShownPromise;
+
+ // Wait to see which popups are shown.
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+}
+
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js
new file mode 100644
index 0000000000..c5219789d9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js
@@ -0,0 +1,144 @@
+/*
+ * Test the password manager context menu.
+ */
+
+"use strict";
+
+const TEST_HOSTNAME = "https://example.com";
+
+// Test with a page that only has a form within an iframe, not in the top-level document
+const IFRAME_PAGE_PATH = "/browser/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html";
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(function* test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ });
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Check if the password field is correctly filled when it's in an iframe.
+ */
+add_task(function* test_context_menu_iframe_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_HOSTNAME + IFRAME_PAGE_PATH
+ }, function* (browser) {
+ function getPasswordInput() {
+ let frame = content.document.getElementById("test-iframe");
+ return frame.contentDocument.getElementById("form-basic-password");
+ }
+
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown");
+ let eventDetails = {type: "contextmenu", button: 2};
+
+ // To click at the right point we have to take into account the iframe offset.
+ // Synthesize a right mouse click over the password input element.
+ BrowserTestUtils.synthesizeMouseAtCenter(getPasswordInput, eventDetails, browser);
+ yield contextMenuShownPromise;
+
+ // Synthesize a mouse click over the fill login menu header.
+ let popupHeader = document.getElementById("fill-login");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(popupHeader, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(popupHeader, {});
+ yield popupShownPromise;
+
+ let popupMenu = document.getElementById("fill-login-popup");
+
+ // Stores the original value of username
+ function promiseFrameInputValue(name) {
+ return ContentTask.spawn(browser, name, function(inputname) {
+ let iframe = content.document.getElementById("test-iframe");
+ let input = iframe.contentDocument.getElementById(inputname);
+ return input.value;
+ });
+ }
+ let usernameOriginalValue = yield promiseFrameInputValue("form-basic-username");
+
+ // Execute the command of the first login menuitem found at the context menu.
+ let passwordChangedPromise = ContentTask.spawn(browser, null, function* () {
+ let frame = content.document.getElementById("test-iframe");
+ let passwordInput = frame.contentDocument.getElementById("form-basic-password");
+ yield ContentTaskUtils.waitForEvent(passwordInput, "input");
+ });
+
+ let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0];
+ firstLoginItem.doCommand();
+
+ yield passwordChangedPromise;
+
+ // Find the used login by it's username.
+ let login = getLoginFromUsername(firstLoginItem.label);
+ let passwordValue = yield promiseFrameInputValue("form-basic-password");
+ is(login.password, passwordValue, "Password filled and correct.");
+
+ let usernameNewValue = yield promiseFrameInputValue("form-basic-username");
+ is(usernameOriginalValue,
+ usernameNewValue,
+ "Username value was not changed.");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.hidePopup();
+ });
+});
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/hostname combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.com",
+ formSubmitURL: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js
new file mode 100644
index 0000000000..09fbe0eea7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js
@@ -0,0 +1,56 @@
+
+"use strict";
+
+const LOGIN_HOST = "http://example.com";
+
+function openExceptionsDialog() {
+ return window.openDialog(
+ "chrome://browser/content/preferences/permissions.xul",
+ "Toolkit:PasswordManagerExceptions", "",
+ {
+ blockVisible: true,
+ sessionVisible: false,
+ allowVisible: false,
+ hideStatusColumn: true,
+ prefilledHost: "",
+ permissionType: "login-saving"
+ }
+ );
+}
+
+function countDisabledHosts(dialog) {
+ let doc = dialog.document;
+ let rejectsTree = doc.getElementById("permissionsTree");
+
+ return rejectsTree.view.rowCount;
+}
+
+function promiseStorageChanged(expectedData) {
+ function observer(subject, data) {
+ return data == expectedData && subject.QueryInterface(Ci.nsISupportsString).data == LOGIN_HOST;
+ }
+
+ return TestUtils.topicObserved("passwordmgr-storage-changed", observer);
+}
+
+add_task(function* test_disable() {
+ let dialog = openExceptionsDialog();
+ let promiseChanged = promiseStorageChanged("hostSavingDisabled");
+
+ yield BrowserTestUtils.waitForEvent(dialog, "load");
+ Services.logins.setLoginSavingEnabled(LOGIN_HOST, false);
+ yield promiseChanged;
+ is(countDisabledHosts(dialog), 1, "Verify disabled host added");
+ yield BrowserTestUtils.closeWindow(dialog);
+});
+
+add_task(function* test_enable() {
+ let dialog = openExceptionsDialog();
+ let promiseChanged = promiseStorageChanged("hostSavingEnabled");
+
+ yield BrowserTestUtils.waitForEvent(dialog, "load");
+ Services.logins.setLoginSavingEnabled(LOGIN_HOST, true);
+ yield promiseChanged;
+ is(countDisabledHosts(dialog), 0, "Verify disabled host removed");
+ yield BrowserTestUtils.closeWindow(dialog);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
new file mode 100644
index 0000000000..c6d9ce50a1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
@@ -0,0 +1,126 @@
+/*
+ * Test that browser chrome UI interactions don't trigger a capture doorhanger.
+ */
+
+"use strict";
+
+function* fillTestPage(aBrowser) {
+ yield ContentTask.spawn(aBrowser, null, function*() {
+ content.document.getElementById("form-basic-username").value = "my_username";
+ content.document.getElementById("form-basic-password").value = "my_password";
+ });
+ info("fields filled");
+}
+
+function* withTestPage(aTaskFn) {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "formless_basic.html",
+ }, function*(aBrowser) {
+ info("tab opened");
+ yield fillTestPage(aBrowser);
+ yield* aTaskFn(aBrowser);
+
+ // Give a chance for the doorhanger to appear
+ yield new Promise(resolve => SimpleTest.executeSoon(resolve));
+ ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
+ });
+}
+
+add_task(function* setup() {
+ yield SimpleTest.promiseFocus(window);
+});
+
+add_task(function* test_urlbar_new_URL() {
+ yield withTestPage(function*(aBrowser) {
+ gURLBar.value = "";
+ let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ gURLBar.focus();
+ yield focusPromise;
+ info("focused");
+ EventUtils.sendString("http://mochi.test:8888/");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ yield BrowserTestUtils.browserLoaded(aBrowser, false, "http://mochi.test:8888/");
+ });
+});
+
+add_task(function* test_urlbar_fragment_enter() {
+ yield withTestPage(function*(aBrowser) {
+ gURLBar.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", {});
+ EventUtils.sendString("#fragment");
+ EventUtils.synthesizeKey("VK_RETURN", {});
+ });
+});
+
+add_task(function* test_backButton_forwardButton() {
+ yield withTestPage(function*(aBrowser) {
+ // Load a new page in the tab so we can test going back
+ aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second");
+ yield BrowserTestUtils.browserLoaded(aBrowser, false,
+ "https://example.com" + DIRECTORY_PATH +
+ "formless_basic.html?second");
+ yield fillTestPage(aBrowser);
+
+ let forwardButton = document.getElementById("forward-button");
+ // We need to wait for the forward button transition to complete before we
+ // can click it, so we hook up a listener to wait for it to be ready.
+ let forwardTransitionPromise = BrowserTestUtils.waitForEvent(forwardButton, "transitionend");
+
+ let backPromise = BrowserTestUtils.browserStopped(aBrowser);
+ EventUtils.synthesizeMouseAtCenter(document.getElementById("back-button"), {});
+ yield backPromise;
+
+ // Give a chance for the doorhanger to appear
+ yield new Promise(resolve => SimpleTest.executeSoon(resolve));
+ ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
+
+ // Now go forward again after filling
+ yield fillTestPage(aBrowser);
+
+ yield forwardTransitionPromise;
+ info("transition done");
+ yield BrowserTestUtils.waitForCondition(() => {
+ return forwardButton.disabled == false;
+ });
+ let forwardPromise = BrowserTestUtils.browserStopped(aBrowser);
+ info("click the forward button");
+ EventUtils.synthesizeMouseAtCenter(forwardButton, {});
+ yield forwardPromise;
+ });
+});
+
+
+add_task(function* test_reloadButton() {
+ yield withTestPage(function*(aBrowser) {
+ let reloadButton = document.getElementById("urlbar-reload-button");
+ let loadPromise = BrowserTestUtils.browserLoaded(aBrowser, false,
+ "https://example.com" + DIRECTORY_PATH +
+ "formless_basic.html");
+
+ yield BrowserTestUtils.waitForCondition(() => {
+ return reloadButton.disabled == false;
+ });
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {});
+ yield loadPromise;
+ });
+});
+
+add_task(function* test_back_keyboard_shortcut() {
+ if (Services.prefs.getIntPref("browser.backspace_action") != 0) {
+ ok(true, "Skipped testing backspace to go back since it's disabled");
+ return;
+ }
+ yield withTestPage(function*(aBrowser) {
+ // Load a new page in the tab so we can test going back
+ aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second");
+ yield BrowserTestUtils.browserLoaded(aBrowser, false,
+ "https://example.com" + DIRECTORY_PATH +
+ "formless_basic.html?second");
+ yield fillTestPage(aBrowser);
+
+ let backPromise = BrowserTestUtils.browserStopped(aBrowser);
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ yield backPromise;
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js
new file mode 100644
index 0000000000..039312b7d1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/LoginManagerParent.jsm", this);
+
+const testUrlPath =
+ "://example.com/browser/toolkit/components/passwordmgr/test/browser/";
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+ return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+ false, () => --count == 0);
+}
+
+/**
+ * Checks that hasInsecureLoginForms is true for a simple HTTP page and false
+ * for a simple HTTPS page.
+ */
+add_task(function* test_simple() {
+ for (let scheme of ["http", "https"]) {
+ let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+
+ Assert.equal(LoginManagerParent.hasInsecureLoginForms(browser),
+ scheme == "http");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+/**
+ * Checks that hasInsecureLoginForms is true if a password field is present in
+ * an HTTP page loaded as a subframe of a top-level HTTPS page, when mixed
+ * active content blocking is disabled.
+ *
+ * When the subframe is navigated to an HTTPS page, hasInsecureLoginForms should
+ * be set to false.
+ *
+ * Moving back in history should set hasInsecureLoginForms to true again.
+ */
+add_task(function* test_subframe_navigation() {
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({
+ "set": [["security.mixed_content.block_active_content", false]],
+ }, resolve));
+
+ // Load the page with the subframe in a new tab.
+ let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // Two events are triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 3),
+ ]);
+
+ Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser));
+
+ // Navigate the subframe to a secure page.
+ let promiseSubframeReady = Promise.all([
+ BrowserTestUtils.browserLoaded(browser, true),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("test-iframe")
+ .contentDocument.getElementById("test-link").click();
+ });
+ yield promiseSubframeReady;
+
+ Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser));
+
+ // Navigate back to the insecure page. We only have to wait for the
+ // InsecureLoginFormsStateChange event that is triggered by pageshow.
+ let promise = waitForInsecureLoginFormsStateChange(browser, 1);
+ yield ContentTask.spawn(browser, null, function* () {
+ content.document.getElementById("test-iframe")
+ .contentWindow.history.back();
+ });
+ yield promise;
+
+ Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser));
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js
new file mode 100644
index 0000000000..2dbffb9cc9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/LoginManagerParent.jsm", this);
+
+function* registerConverter() {
+ Cu.import("resource://gre/modules/Services.jsm", this);
+ Cu.import("resource://gre/modules/NetUtil.jsm", this);
+
+ /**
+ * Converts the "test/content" MIME type, served by the test over HTTP, to an
+ * HTML viewer page containing the "form_basic.html" code. The viewer is
+ * served from a "resource:" URI while keeping the "resource:" principal.
+ */
+ function TestStreamConverter() {}
+
+ TestStreamConverter.prototype = {
+ classID: Components.ID("{5f01d6ef-c090-45a4-b3e5-940d64713eb7}"),
+ contractID: "@mozilla.org/streamconv;1?from=test/content&to=*/*",
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIRequestObserver,
+ Ci.nsIStreamListener,
+ Ci.nsIStreamConverter,
+ ]),
+
+ // nsIStreamConverter
+ convert() {},
+
+ // nsIStreamConverter
+ asyncConvertData(aFromType, aToType, aListener, aCtxt) {
+ this.listener = aListener;
+ },
+
+ // nsIRequestObserver
+ onStartRequest(aRequest, aContext) {
+ let channel = NetUtil.newChannel({
+ uri: "resource://testing-common/form_basic.html",
+ loadUsingSystemPrincipal: true,
+ });
+ channel.originalURI = aRequest.QueryInterface(Ci.nsIChannel).URI;
+ channel.loadGroup = aRequest.loadGroup;
+ channel.owner = Services.scriptSecurityManager
+ .createCodebasePrincipal(channel.URI, {});
+ // In this test, we pass the new channel to the listener but don't fire a
+ // redirect notification, even if it would be required. This keeps the
+ // test code simpler and doesn't impact the principal check we're testing.
+ channel.asyncOpen2(this.listener);
+ },
+
+ // nsIRequestObserver
+ onStopRequest() {},
+
+ // nsIStreamListener
+ onDataAvailable() {},
+ };
+
+ let factory = XPCOMUtils._getFactory(TestStreamConverter);
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(TestStreamConverter.prototype.classID, "",
+ TestStreamConverter.prototype.contractID, factory);
+ this.cleanupFunction = function () {
+ registrar.unregisterFactory(TestStreamConverter.prototype.classID, factory);
+ };
+}
+
+/**
+ * Waits for the given number of occurrences of InsecureLoginFormsStateChange
+ * on the given browser element.
+ */
+function waitForInsecureLoginFormsStateChange(browser, count) {
+ return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
+ false, () => --count == 0);
+}
+
+/**
+ * Checks that hasInsecureLoginForms is false for a viewer served internally
+ * using a "resource:" URI.
+ */
+add_task(function* test_streamConverter() {
+ let originalBrowser = gBrowser.selectedTab.linkedBrowser;
+
+ yield ContentTask.spawn(originalBrowser, null, registerConverter);
+
+ let tab = gBrowser.addTab("http://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/streamConverter_content.sjs",
+ { relatedBrowser: originalBrowser.linkedBrowser });
+ let browser = tab.linkedBrowser;
+ yield Promise.all([
+ BrowserTestUtils.switchTab(gBrowser, tab),
+ BrowserTestUtils.browserLoaded(browser),
+ // One event is triggered by pageshow and one by DOMFormHasPassword.
+ waitForInsecureLoginFormsStateChange(browser, 2),
+ ]);
+
+ Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser));
+
+ yield BrowserTestUtils.removeTab(tab);
+
+ yield ContentTask.spawn(originalBrowser, null, function* () {
+ this.cleanupFunction();
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js
new file mode 100644
index 0000000000..beb928a34e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js
@@ -0,0 +1,78 @@
+const TEST_URL_PATH = "://example.org/browser/toolkit/components/passwordmgr/test/browser/";
+
+add_task(function* setup() {
+ let login = LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://example.org",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ login = LoginTestUtils.testData.formLogin({
+ hostname: "http://example.org",
+ formSubmitURL: "http://another.domain",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ yield SpecialPowers.pushPrefEnv({ "set": [["signon.autofillForms.http", false]] });
+});
+
+add_task(function* test_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic.html`);
+
+ let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let contentUsername = doc.getElementById("form-basic-username").value;
+ let contentPassword = doc.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ });
+
+ is(username, scheme == "http" ? "" : "username", "Username filled correctly");
+ is(password, scheme == "http" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(function* test_iframe_in_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic_iframe.html`);
+
+ let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let iframe = doc.getElementById("test-iframe");
+ let contentUsername = iframe.contentWindow.document.getElementById("form-basic-username").value;
+ let contentPassword = iframe.contentWindow.document.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ });
+
+ is(username, scheme == "http" ? "" : "username", "Username filled correctly");
+ is(password, scheme == "http" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(function* test_http_action_autofill() {
+ for (let type of ["insecure", "secure"]) {
+ let tab = yield BrowserTestUtils
+ .openNewForegroundTab(gBrowser, `https${TEST_URL_PATH}form_cross_origin_${type}_action.html`);
+
+ let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
+ let doc = content.document;
+ let contentUsername = doc.getElementById("form-basic-username").value;
+ let contentPassword = doc.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ });
+
+ is(username, type == "insecure" ? "" : "username", "Username filled correctly");
+ is(password, type == "insecure" ? "" : "password", "Password filled correctly");
+
+ gBrowser.removeTab(tab);
+ }
+});
+
diff --git a/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js
new file mode 100644
index 0000000000..f16ae1b986
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js
@@ -0,0 +1,94 @@
+"use strict";
+
+const WARNING_PATTERN = [{
+ key: "INSECURE_FORM_ACTION",
+ msg: 'JavaScript Warning: "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen."'
+}, {
+ key: "INSECURE_PAGE",
+ msg: 'JavaScript Warning: "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."'
+}];
+
+add_task(function* testInsecurePasswordWarning() {
+ let warningPatternHandler;
+
+ function messageHandler(msgObj) {
+ function findWarningPattern(msg) {
+ return WARNING_PATTERN.find(patternPair => {
+ return msg.indexOf(patternPair.msg) !== -1;
+ });
+ }
+
+ let warning = findWarningPattern(msgObj.message);
+
+ // Only handle the insecure password related warning messages.
+ if (warning) {
+ // Prevent any unexpected or redundant matched warning message coming after
+ // the test case is ended.
+ ok(warningPatternHandler, "Invoke a valid warning message handler");
+ warningPatternHandler(warning, msgObj.message);
+ }
+ }
+ Services.console.registerListener(messageHandler);
+ registerCleanupFunction(function() {
+ Services.console.unregisterListener(messageHandler);
+ });
+
+ for (let [origin, testFile, expectWarnings] of [
+ ["http://127.0.0.1", "form_basic.html", []],
+ ["http://127.0.0.1", "formless_basic.html", []],
+ ["http://example.com", "form_basic.html", ["INSECURE_PAGE"]],
+ ["http://example.com", "formless_basic.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_basic.html", []],
+ ["https://example.com", "formless_basic.html", []],
+
+ // For a form with customized action link in the same origin.
+ ["http://127.0.0.1", "form_same_origin_action.html", []],
+ ["http://example.com", "form_same_origin_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_same_origin_action.html", []],
+
+ // For a form with an insecure (http) customized action link.
+ ["http://127.0.0.1", "form_cross_origin_insecure_action.html", ["INSECURE_FORM_ACTION"]],
+ ["http://example.com", "form_cross_origin_insecure_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_cross_origin_insecure_action.html", ["INSECURE_FORM_ACTION"]],
+
+ // For a form with a secure (https) customized action link.
+ ["http://127.0.0.1", "form_cross_origin_secure_action.html", []],
+ ["http://example.com", "form_cross_origin_secure_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_cross_origin_secure_action.html", []],
+ ]) {
+ let testURL = origin + DIRECTORY_PATH + testFile;
+ let promiseConsoleMessages = new Promise(resolve => {
+ warningPatternHandler = function (warning, originMessage) {
+ ok(warning, "Handling a warning pattern");
+ let fullMessage = `[${warning.msg} {file: "${testURL}" line: 0 column: 0 source: "0"}]`;
+ is(originMessage, fullMessage, "Message full matched:" + originMessage);
+
+ let index = expectWarnings.indexOf(warning.key);
+ isnot(index, -1, "Found warning: " + warning.key + " for URL:" + testURL);
+ if (index !== -1) {
+ // Remove the shown message.
+ expectWarnings.splice(index, 1);
+ }
+ if (expectWarnings.length === 0) {
+ info("All warnings are shown for URL:" + testURL);
+ resolve();
+ }
+ };
+ });
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: testURL
+ }, function*() {
+ if (expectWarnings.length === 0) {
+ info("All warnings are shown for URL:" + testURL);
+ return Promise.resolve();
+ }
+ return promiseConsoleMessages;
+ });
+
+ // Remove warningPatternHandler to stop handling the matched warning pattern
+ // and the task should not get any warning anymore.
+ warningPatternHandler = null;
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js
new file mode 100644
index 0000000000..f3bc62b0a4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js
@@ -0,0 +1,59 @@
+const HOST = "https://example.com";
+const URL = HOST + "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
+const TIMEOUT_PREF = "signon.masterPasswordReprompt.timeout_ms";
+
+// Waits for the master password prompt and cancels it.
+function waitForDialog() {
+ let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
+ return dialogShown.then(function([subject]) {
+ let dialog = subject.Dialog;
+ is(dialog.args.title, "Password Required");
+ dialog.ui.button1.click();
+ });
+}
+
+// Test that autocomplete does not trigger a master password prompt
+// for a certain time after it was cancelled.
+add_task(function* test_mpAutocompleteTimeout() {
+ let login = LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: "username",
+ password: "password",
+ });
+ Services.logins.addLogin(login);
+ LoginTestUtils.masterPassword.enable();
+
+ registerCleanupFunction(function() {
+ LoginTestUtils.masterPassword.disable();
+ Services.logins.removeAllLogins();
+ });
+
+ // Set master password prompt timeout to 3s.
+ // If this test goes intermittent, you likely have to increase this value.
+ yield SpecialPowers.pushPrefEnv({set: [[TIMEOUT_PREF, 3000]]});
+
+ // Wait for initial master password dialog after opening the tab.
+ let dialogShown = waitForDialog();
+
+ yield BrowserTestUtils.withNewTab(URL, function*(browser) {
+ yield dialogShown;
+
+ yield ContentTask.spawn(browser, null, function*() {
+ // Focus the password field to trigger autocompletion.
+ content.document.getElementById("form-basic-password").focus();
+ });
+
+ // Wait 4s, dialog should not have been shown
+ // (otherwise the code below will not work).
+ yield new Promise((c) => setTimeout(c, 4000));
+
+ dialogShown = waitForDialog();
+ yield ContentTask.spawn(browser, null, function*() {
+ // Re-focus the password field to trigger autocompletion.
+ content.document.getElementById("form-basic-username").focus();
+ content.document.getElementById("form-basic-password").focus();
+ });
+ yield dialogShown;
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications.js b/toolkit/components/passwordmgr/test/browser/browser_notifications.js
new file mode 100644
index 0000000000..4fb012f149
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications.js
@@ -0,0 +1,81 @@
+/**
+ * Test that the doorhanger notification for password saving is populated with
+ * the correct values in various password capture cases.
+ */
+add_task(function* test_save_change() {
+ let testCases = [{
+ username: "username",
+ password: "password",
+ }, {
+ username: "",
+ password: "password",
+ }, {
+ username: "username",
+ oldPassword: "password",
+ password: "newPassword",
+ }, {
+ username: "",
+ oldPassword: "password",
+ password: "newPassword",
+ }];
+
+ for (let { username, oldPassword, password } of testCases) {
+ // Add a login for the origin of the form if testing a change notification.
+ if (oldPassword) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username,
+ password: oldPassword,
+ }));
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, [username, password],
+ function* ([contentUsername, contentPassword]) {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = contentUsername;
+ doc.getElementById("form-basic-password").value = contentPassword;
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ // Style flush to make sure binding is attached
+ notificationElement.querySelector("#password-notification-password").clientTop;
+
+ // Check the actual content of the popup notification.
+ Assert.equal(notificationElement.querySelector("#password-notification-username")
+ .value, username);
+ Assert.equal(notificationElement.querySelector("#password-notification-password")
+ .value, password);
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = oldPassword ? "modifyLogin" : "addLogin";
+ let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification);
+ notificationElement.button.doCommand();
+ let [result] = yield promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = oldPassword ? result.QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+ Assert.equal(login.username, username);
+ Assert.equal(login.password, password);
+ });
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
new file mode 100644
index 0000000000..48c73b0e68
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js
@@ -0,0 +1,125 @@
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["signon.rememberSignons.visibilityToggle", true]
+ ]});
+});
+
+/**
+ * Test that the doorhanger main action button is disabled
+ * when the password field is empty.
+ *
+ * Also checks that submiting an empty password throws an error.
+ */
+add_task(function* test_empty_password() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, null,
+ function* () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = "username";
+ doc.getElementById("form-basic-password").value = "p";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+ let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+ // Synthesize input to empty the field
+ passwordTextbox.focus();
+ yield EventUtils.synthesizeKey("VK_RIGHT", {});
+ yield EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+
+ let mainActionButton = document.getAnonymousElementByAttribute(notificationElement.button, "anonid", "button");
+ Assert.ok(mainActionButton.disabled, "Main action button is disabled");
+
+ // Makes sure submiting an empty password throws an error
+ Assert.throws(notificationElement.button.doCommand(),
+ "Can't add a login with a null or empty password.",
+ "Should fail for an empty password");
+ });
+});
+
+/**
+ * Test that the doorhanger password field shows plain or * text
+ * when the checkbox is checked.
+ */
+add_task(function* test_toggle_password() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, null,
+ function* () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = "username";
+ doc.getElementById("form-basic-password").value = "p";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+ let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+ yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+ Assert.ok(toggleCheckbox.checked);
+ Assert.equal(passwordTextbox.type, "", "Password textbox changed to plain text");
+
+ yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+ Assert.ok(!toggleCheckbox.checked);
+ Assert.equal(passwordTextbox.type, "password", "Password textbox changed to * text");
+ });
+});
+
+/**
+ * Test that the doorhanger password toggle checkbox is disabled
+ * when the master password is set.
+ */
+add_task(function* test_checkbox_disabled_if_has_master_password() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+
+ LoginTestUtils.masterPassword.enable();
+
+ yield ContentTask.spawn(browser, null, function* () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = "username";
+ doc.getElementById("form-basic-password").value = "p";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector("#password-notification-password");
+ let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle");
+
+ Assert.equal(passwordTextbox.type, "password", "Password textbox should show * text");
+ Assert.ok(toggleCheckbox.getAttribute("hidden"), "checkbox is hidden when master password is set");
+ });
+
+ LoginTestUtils.masterPassword.disable();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js
new file mode 100644
index 0000000000..8ac49dac55
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js
@@ -0,0 +1,145 @@
+/**
+ * Test changing the password inside the doorhanger notification for passwords.
+ *
+ * We check the following cases:
+ * - Editing the password of a new login.
+ * - Editing the password of an existing login.
+ * - Changing both username and password to an existing login.
+ * - Changing the username to an existing login.
+ * - Editing username to an empty one and a new password.
+ *
+ * If both the username and password matches an already existing login, we should not
+ * update it's password, but only it's usage timestamp and count.
+ */
+add_task(function* test_edit_password() {
+ let testCases = [{
+ usernameInPage: "username",
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 1,
+ }, {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ passwordInPage: "password",
+ passwordInStorage: "oldPassword",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ }, {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ }, {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "password",
+ timesUsed: 2,
+ checkPasswordNotUpdated: true,
+ }, {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ }];
+
+ for (let testCase of testCases) {
+ info("Test case: " + JSON.stringify(testCase));
+
+ // Create the pre-existing logins when needed.
+ if (testCase.usernameInPageExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameInPage,
+ password: testCase.passwordInStorage,
+ }));
+ }
+
+ if (testCase.usernameChangedToExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameChangedTo,
+ password: testCase.passwordChangedTo,
+ }));
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, testCase,
+ function* (contentTestCase) {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = contentTestCase.usernameInPage;
+ doc.getElementById("form-basic-password").value = contentTestCase.passwordInPage;
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ // Style flush to make sure binding is attached
+ notificationElement.querySelector("#password-notification-password").clientTop;
+
+ // Modify the username in the dialog if requested.
+ if (testCase.usernameChangedTo) {
+ notificationElement.querySelector("#password-notification-username")
+ .value = testCase.usernameChangedTo;
+ }
+
+ // Modify the password in the dialog if requested.
+ if (testCase.passwordChangedTo) {
+ notificationElement.querySelector("#password-notification-password")
+ .value = testCase.passwordChangedTo;
+ }
+
+ // We expect a modifyLogin notification if the final username used by the
+ // dialog exists in the logins database, otherwise an addLogin one.
+ let expectModifyLogin = typeof testCase.usernameChangedTo !== "undefined"
+ ? testCase.usernameChangedToExists
+ : testCase.usernameInPageExists;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
+ let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification);
+ notificationElement.button.doCommand();
+ let [result] = yield promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+
+ Assert.equal(login.username, testCase.usernameChangedTo ||
+ testCase.usernameInPage);
+ Assert.equal(login.password, testCase.passwordChangedTo ||
+ testCase.passwordInPage);
+
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(meta.timesUsed, testCase.timesUsed);
+
+ // Check that the password was not updated if the user is empty
+ if (testCase.checkPasswordNotUpdated) {
+ Assert.ok(meta.timeLastUsed > meta.timeCreated);
+ Assert.ok(meta.timeCreated == meta.timePasswordChanged);
+ }
+ });
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js
new file mode 100644
index 0000000000..2c9ea26070
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js
@@ -0,0 +1,119 @@
+/**
+ * Test changing the username inside the doorhanger notification for passwords.
+ *
+ * We have to test combination of existing and non-existing logins both for
+ * the original one from the webpage and the final one used by the dialog.
+ *
+ * We also check switching to and from empty usernames.
+ */
+add_task(function* test_edit_username() {
+ let testCases = [{
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ }, {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ usernameChangedTo: "newUsername",
+ }, {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ }, {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ }, {
+ usernameInPage: "",
+ usernameChangedTo: "newUsername",
+ }, {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ }, {
+ usernameInPage: "",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ }, {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ usernameChangedToExists: true,
+ }];
+
+ for (let testCase of testCases) {
+ info("Test case: " + JSON.stringify(testCase));
+
+ // Create the pre-existing logins when needed.
+ if (testCase.usernameInPageExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameInPage,
+ password: "old password",
+ }));
+ }
+
+ if (testCase.usernameChangedToExists) {
+ Services.logins.addLogin(LoginTestUtils.testData.formLogin({
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com",
+ username: testCase.usernameChangedTo,
+ password: "old password",
+ }));
+ }
+
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ }, function* (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+ "popupshown",
+ (event) => event.target == PopupNotifications.panel);
+ yield ContentTask.spawn(browser, testCase.usernameInPage,
+ function* (usernameInPage) {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").value = usernameInPage;
+ doc.getElementById("form-basic-password").value = "password";
+ doc.getElementById("form-basic").submit();
+ });
+ yield promiseShown;
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ // Style flush to make sure binding is attached
+ notificationElement.querySelector("#password-notification-password").clientTop;
+
+ // Modify the username in the dialog if requested.
+ if (testCase.usernameChangedTo) {
+ notificationElement.querySelector("#password-notification-username")
+ .value = testCase.usernameChangedTo;
+ }
+
+ // We expect a modifyLogin notification if the final username used by the
+ // dialog exists in the logins database, otherwise an addLogin one.
+ let expectModifyLogin = testCase.usernameChangedTo
+ ? testCase.usernameChangedToExists
+ : testCase.usernameInPageExists;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = expectModifyLogin ? "modifyLogin" : "addLogin";
+ let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification);
+ notificationElement.button.doCommand();
+ let [result] = yield promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = expectModifyLogin ? result.QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+ Assert.equal(login.username, testCase.usernameChangedTo ||
+ testCase.usernameInPage);
+ Assert.equal(login.password, "password");
+ });
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js
new file mode 100644
index 0000000000..ece2b731f5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.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/. */
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.logins.removeAllLogins();
+
+ // Add some initial logins
+ let urls = [
+ "http://example.com/",
+ "http://mozilla.org/",
+ "http://spreadfirefox.com/",
+ "https://support.mozilla.org/",
+ "http://hg.mozilla.org/"
+ ];
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let logins = [
+ new nsLoginInfo(urls[0], urls[0], null, "", "o hai", "u1", "p1"),
+ new nsLoginInfo(urls[1], urls[1], null, "ehsan", "coded", "u2", "p2"),
+ new nsLoginInfo(urls[2], urls[2], null, "this", "awesome", "u3", "p3"),
+ new nsLoginInfo(urls[3], urls[3], null, "array of", "logins", "u4", "p4"),
+ new nsLoginInfo(urls[4], urls[4], null, "then", "i wrote the test", "u5", "p5")
+ ];
+ logins.forEach(login => Services.logins.addLogin(login));
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ // Test if "Copy Username" and "Copy Password" works
+ function doTest() {
+ let doc = pwmgrdlg.document;
+ let selection = doc.getElementById("signonsTree").view.selection;
+ let menuitem = doc.getElementById("context-copyusername");
+
+ function copyField() {
+ info("Select all");
+ selection.selectAll();
+ assertMenuitemEnabled("copyusername", false);
+ assertMenuitemEnabled("editusername", false);
+ assertMenuitemEnabled("copypassword", false);
+ assertMenuitemEnabled("editpassword", false);
+
+ info("Select the first row (with an empty username)");
+ selection.select(0);
+ assertMenuitemEnabled("copyusername", false, "empty username");
+ assertMenuitemEnabled("editusername", true);
+ assertMenuitemEnabled("copypassword", true);
+ assertMenuitemEnabled("editpassword", false, "password column hidden");
+
+ info("Clear the selection");
+ selection.clearSelection();
+ assertMenuitemEnabled("copyusername", false);
+ assertMenuitemEnabled("editusername", false);
+ assertMenuitemEnabled("copypassword", false);
+ assertMenuitemEnabled("editpassword", false);
+
+ info("Select the third row and making the password column visible");
+ selection.select(2);
+ doc.getElementById("passwordCol").hidden = false;
+ assertMenuitemEnabled("copyusername", true);
+ assertMenuitemEnabled("editusername", true);
+ assertMenuitemEnabled("copypassword", true);
+ assertMenuitemEnabled("editpassword", true, "password column visible");
+ menuitem.doCommand();
+ }
+
+ function assertMenuitemEnabled(idSuffix, expected, reason = "") {
+ doc.defaultView.UpdateContextMenu();
+ let actual = !doc.getElementById("context-" + idSuffix).getAttribute("disabled");
+ is(actual, expected, idSuffix + " should be " + (expected ? "enabled" : "disabled") +
+ (reason ? ": " + reason : ""));
+ }
+
+ function cleanUp() {
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ Services.ww.unregisterNotification(arguments.callee);
+ Services.logins.removeAllLogins();
+ doc.getElementById("passwordCol").hidden = true;
+ finish();
+ });
+ pwmgrdlg.close();
+ }
+
+ function testPassword() {
+ info("Testing Copy Password");
+ waitForClipboard("coded", function copyPassword() {
+ menuitem = doc.getElementById("context-copypassword");
+ menuitem.doCommand();
+ }, cleanUp, cleanUp);
+ }
+
+ info("Testing Copy Username");
+ waitForClipboard("ehsan", copyField, testPassword, testPassword);
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
new file mode 100644
index 0000000000..2b2e422733
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
@@ -0,0 +1,126 @@
+const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
+const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+
+var doc;
+var pwmgr;
+var pwmgrdlg;
+var signonsTree;
+
+function addLogin(site, username, password) {
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let login = new nsLoginInfo(site, site, null, username, password, "u", "p");
+ Services.logins.addLogin(login);
+}
+
+function getUsername(row) {
+ return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("userCol"));
+}
+
+function getPassword(row) {
+ return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("passwordCol"));
+}
+
+function synthesizeDblClickOnCell(aTree, column, row) {
+ let tbo = aTree.treeBoxObject;
+ let rect = tbo.getCoordsForCellItem(row, aTree.columns[column], "text");
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+ // Simulate the double click.
+ EventUtils.synthesizeMouse(aTree.body, x, y, { clickCount: 2 },
+ aTree.ownerDocument.defaultView);
+}
+
+function* togglePasswords() {
+ pwmgrdlg.document.querySelector("#togglePasswords").doCommand();
+ yield new Promise(resolve => waitForFocus(resolve, pwmgrdlg));
+}
+
+function* editUsernamePromises(site, oldUsername, newUsername) {
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
+ let login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.username, oldUsername, "Correct username saved");
+ is(getUsername(0), oldUsername, "Correct username shown");
+ synthesizeDblClickOnCell(signonsTree, 1, 0);
+ yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
+ "Waiting for editing");
+
+ EventUtils.sendString(newUsername, pwmgrdlg);
+ let signonsIntro = doc.querySelector("#signonsIntro");
+ EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
+ yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
+ "Waiting for editing to stop");
+
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
+ login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.username, newUsername, "Correct username updated");
+ is(getUsername(0), newUsername, "Correct username shown after the update");
+}
+
+function* editPasswordPromises(site, oldPassword, newPassword) {
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
+ let login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.password, oldPassword, "Correct password saved");
+ is(getPassword(0), oldPassword, "Correct password shown");
+
+ synthesizeDblClickOnCell(signonsTree, 2, 0);
+ yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
+ "Waiting for editing");
+
+ EventUtils.sendString(newPassword, pwmgrdlg);
+ let signonsIntro = doc.querySelector("#signonsIntro");
+ EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
+ yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
+ "Waiting for editing to stop");
+
+ is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
+ login = Services.logins.findLogins({}, site, "", "")[0];
+ is(login.password, newPassword, "Correct password updated");
+ is(getPassword(0), newPassword, "Correct password shown after the update");
+}
+
+add_task(function* test_setup() {
+ registerCleanupFunction(function() {
+ Services.logins.removeAllLogins();
+ });
+
+ Services.logins.removeAllLogins();
+ // Open the password manager dialog.
+ pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ SimpleTest.waitForFocus(function() {
+ EventUtils.sendKey("RETURN", win);
+ }, win);
+ } else if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") {
+ // Unregister ourself.
+ Services.ww.unregisterNotification(arguments.callee);
+ }
+ });
+
+ yield new Promise((resolve) => {
+ SimpleTest.waitForFocus(() => {
+ doc = pwmgrdlg.document;
+ signonsTree = doc.querySelector("#signonsTree");
+ resolve();
+ }, pwmgrdlg);
+ });
+});
+
+add_task(function* test_edit_multiple_logins() {
+ function* testLoginChange(site, oldUsername, oldPassword, newUsername, newPassword) {
+ addLogin(site, oldUsername, oldPassword);
+ yield* editUsernamePromises(site, oldUsername, newUsername);
+ yield* togglePasswords();
+ yield* editPasswordPromises(site, oldPassword, newPassword);
+ yield* togglePasswords();
+ }
+
+ yield* testLoginChange("http://c.tn/", "userC", "passC", "usernameC", "passwordC");
+ yield* testLoginChange("http://b.tn/", "userB", "passB", "usernameB", "passwordB");
+ yield* testLoginChange("http://a.tn/", "userA", "passA", "usernameA", "passwordA");
+
+ pwmgrdlg.close();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js
new file mode 100644
index 0000000000..95bcee9ed2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+ waitForExplicitFinish();
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+
+ // add login data
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let login = new nsLoginInfo("http://example.com/", "http://example.com/", null,
+ "user", "password", "u1", "p1");
+ pwmgr.addLogin(login);
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ function doTest() {
+ let doc = pwmgrdlg.document;
+
+ let signonsTree = doc.querySelector("#signonsTree");
+ is(signonsTree.view.rowCount, 1, "One entry in the passwords list");
+
+ is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("siteCol")),
+ "http://example.com/",
+ "Correct website saved");
+
+ is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("userCol")),
+ "user",
+ "Correct user saved");
+
+ let timeCreatedCol = doc.getElementById("timeCreatedCol");
+ is(timeCreatedCol.getAttribute("hidden"), "true",
+ "Time created column is not displayed");
+
+
+ let timeLastUsedCol = doc.getElementById("timeLastUsedCol");
+ is(timeLastUsedCol.getAttribute("hidden"), "true",
+ "Last Used column is not displayed");
+
+ let timePasswordChangedCol = doc.getElementById("timePasswordChangedCol");
+ is(timePasswordChangedCol.getAttribute("hidden"), "",
+ "Last Changed column is displayed");
+
+ // cleanup
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") {
+ // unregister ourself
+ Services.ww.unregisterNotification(arguments.callee);
+
+ pwmgr.removeAllLogins();
+
+ finish();
+ }
+ });
+
+ pwmgrdlg.close();
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js
new file mode 100644
index 0000000000..1dc7076aad
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+ waitForExplicitFinish();
+
+ const LOGIN_HOST = "http://example.com";
+ const LOGIN_COUNT = 5;
+
+ let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
+ let pmDialog = window.openDialog(
+ "chrome://passwordmgr/content/passwordManager.xul",
+ "Toolkit:PasswordManager", "");
+
+ let logins = [];
+ let loginCounter = 0;
+ let loginOrder = null;
+ let modifiedLogin;
+ let testNumber = 0;
+ let testObserver = {
+ observe: function (subject, topic, data) {
+ if (topic == "passwordmgr-dialog-updated") {
+ switch (testNumber) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ is(countLogins(), loginCounter, "Verify login added");
+ ok(getLoginOrder().startsWith(loginOrder), "Verify login order");
+ runNextTest();
+ break;
+ case 6:
+ is(countLogins(), loginCounter, "Verify login count");
+ is(getLoginOrder(), loginOrder, "Verify login order");
+ is(getLoginPassword(), "newpassword0", "Verify login modified");
+ runNextTest();
+ break;
+ case 7:
+ is(countLogins(), loginCounter, "Verify login removed");
+ ok(loginOrder.endsWith(getLoginOrder()), "Verify login order");
+ runNextTest();
+ break;
+ case 8:
+ is(countLogins(), 0, "Verify all logins removed");
+ runNextTest();
+ break;
+ }
+ }
+ }
+ };
+
+ SimpleTest.waitForFocus(startTest, pmDialog);
+
+ function createLogins() {
+ let login;
+ for (let i = 0; i < LOGIN_COUNT; i++) {
+ login = new nsLoginInfo(LOGIN_HOST + "?n=" + i, LOGIN_HOST + "?n=" + i,
+ null, "user" + i, "password" + i, "u" + i, "p" + i);
+ logins.push(login);
+ }
+ modifiedLogin = new nsLoginInfo(LOGIN_HOST + "?n=0", LOGIN_HOST + "?n=0",
+ null, "user0", "newpassword0", "u0", "p0");
+ is(logins.length, LOGIN_COUNT, "Verify logins created");
+ }
+
+ function countLogins() {
+ let doc = pmDialog.document;
+ let signonsTree = doc.getElementById("signonsTree");
+ return signonsTree.view.rowCount;
+ }
+
+ function getLoginOrder() {
+ let doc = pmDialog.document;
+ let signonsTree = doc.getElementById("signonsTree");
+ let column = signonsTree.columns[0]; // host column
+ let order = [];
+ for (let i = 0; i < signonsTree.view.rowCount; i++) {
+ order.push(signonsTree.view.getCellText(i, column));
+ }
+ return order.join(',');
+ }
+
+ function getLoginPassword() {
+ let doc = pmDialog.document;
+ let loginsTree = doc.getElementById("signonsTree");
+ let column = loginsTree.columns[2]; // password column
+ return loginsTree.view.getCellText(0, column);
+ }
+
+ function startTest() {
+ Services.obs.addObserver(
+ testObserver, "passwordmgr-dialog-updated", false);
+ is(countLogins(), 0, "Verify starts with 0 logins");
+ createLogins();
+ runNextTest();
+ }
+
+ function runNextTest() {
+ switch (++testNumber) {
+ case 1: // add the logins
+ for (let i = 0; i < logins.length; i++) {
+ loginCounter++;
+ loginOrder = getLoginOrder();
+ Services.logins.addLogin(logins[i]);
+ }
+ break;
+ case 6: // modify a login
+ loginOrder = getLoginOrder();
+ Services.logins.modifyLogin(logins[0], modifiedLogin);
+ break;
+ case 7: // remove a login
+ loginCounter--;
+ loginOrder = getLoginOrder();
+ Services.logins.removeLogin(modifiedLogin);
+ break;
+ case 8: // remove all logins
+ Services.logins.removeAllLogins();
+ break;
+ case 9: // finish
+ Services.obs.removeObserver(
+ testObserver, "passwordmgr-dialog-updated", false);
+ pmDialog.close();
+ finish();
+ break;
+ }
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
new file mode 100644
index 0000000000..83272a9c41
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+ waitForExplicitFinish();
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+
+ // Add some initial logins
+ let urls = [
+ "http://example.com/",
+ "http://example.org/",
+ "http://mozilla.com/",
+ "http://mozilla.org/",
+ "http://spreadfirefox.com/",
+ "http://planet.mozilla.org/",
+ "https://developer.mozilla.org/",
+ "http://hg.mozilla.org/",
+ "http://dxr.mozilla.org/",
+ "http://feeds.mozilla.org/",
+ ];
+ let users = [
+ "user",
+ "username",
+ "ehsan",
+ "ehsan",
+ "john",
+ "what?",
+ "really?",
+ "you sure?",
+ "my user name",
+ "my username",
+ ];
+ let pwds = [
+ "password",
+ "password",
+ "mypass",
+ "mypass",
+ "smith",
+ "very secret",
+ "super secret",
+ "absolutely",
+ "mozilla",
+ "mozilla.com",
+ ];
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ for (let i = 0; i < 10; i++)
+ pwmgr.addLogin(new nsLoginInfo(urls[i], urls[i], null, users[i], pwds[i],
+ "u" + (i + 1), "p" + (i + 1)));
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ // the meat of the test
+ function doTest() {
+ let doc = pwmgrdlg.document;
+ let win = doc.defaultView;
+ let sTree = doc.getElementById("signonsTree");
+ let filter = doc.getElementById("filter");
+ let siteCol = doc.getElementById("siteCol");
+ let userCol = doc.getElementById("userCol");
+ let passwordCol = doc.getElementById("passwordCol");
+
+ let toggleCalls = 0;
+ function toggleShowPasswords(func) {
+ let toggleButton = doc.getElementById("togglePasswords");
+ let showMode = (toggleCalls++ % 2) == 0;
+
+ // only watch for a confirmation dialog every other time being called
+ if (showMode) {
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowclosed")
+ Services.ww.unregisterNotification(arguments.callee);
+ else if (aTopic == "domwindowopened") {
+ let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ SimpleTest.waitForFocus(function() {
+ EventUtils.sendKey("RETURN", targetWin);
+ }, targetWin);
+ }
+ });
+ }
+
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ if (aTopic == "passwordmgr-password-toggle-complete") {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ func();
+ }
+ }, "passwordmgr-password-toggle-complete", false);
+
+ EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win);
+ }
+
+ function clickCol(col) {
+ EventUtils.synthesizeMouse(col, 20, 1, {}, win);
+ setTimeout(runNextTest, 0);
+ }
+
+ function setFilter(string) {
+ filter.value = string;
+ filter.doCommand();
+ setTimeout(runNextTest, 0);
+ }
+
+ function checkSortMarkers(activeCol) {
+ let isOk = true;
+ let col = null;
+ let hasAttr = false;
+ let treecols = activeCol.parentNode;
+ for (let i = 0; i < treecols.childNodes.length; i++) {
+ col = treecols.childNodes[i];
+ if (col.nodeName != "treecol")
+ continue;
+ hasAttr = col.hasAttribute("sortDirection");
+ isOk &= col == activeCol ? hasAttr : !hasAttr;
+ }
+ ok(isOk, "Only " + activeCol.id + " has a sort marker");
+ }
+
+ function checkSortDirection(col, ascending) {
+ checkSortMarkers(col);
+ let direction = ascending ? "ascending" : "descending";
+ is(col.getAttribute("sortDirection"), direction,
+ col.id + ": sort direction is " + direction);
+ }
+
+ function checkColumnEntries(aCol, expectedValues) {
+ let actualValues = getColumnEntries(aCol);
+ is(actualValues.length, expectedValues.length, "Checking length of expected column");
+ for (let i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], "Checking column entry #" + i);
+ }
+
+ function getColumnEntries(aCol) {
+ let entries = [];
+ let column = sTree.columns[aCol];
+ let numRows = sTree.view.rowCount;
+ for (let i = 0; i < numRows; i++)
+ entries.push(sTree.view.getCellText(i, column));
+ return entries;
+ }
+
+ let testCounter = 0;
+ let expectedValues;
+ function runNextTest() {
+ switch (testCounter++) {
+ case 0:
+ expectedValues = urls.slice().sort();
+ checkColumnEntries(0, expectedValues);
+ checkSortDirection(siteCol, true);
+ // Toggle sort direction on Host column
+ clickCol(siteCol);
+ break;
+ case 1:
+ expectedValues.reverse();
+ checkColumnEntries(0, expectedValues);
+ checkSortDirection(siteCol, false);
+ // Sort by Username
+ clickCol(userCol);
+ break;
+ case 2:
+ expectedValues = users.slice().sort();
+ checkColumnEntries(1, expectedValues);
+ checkSortDirection(userCol, true);
+ // Sort by Password
+ clickCol(passwordCol);
+ break;
+ case 3:
+ expectedValues = pwds.slice().sort();
+ checkColumnEntries(2, expectedValues);
+ checkSortDirection(passwordCol, true);
+ // Set filter
+ setFilter("moz");
+ break;
+ case 4:
+ expectedValues = [ "absolutely", "mozilla", "mozilla.com",
+ "mypass", "mypass", "super secret",
+ "very secret" ];
+ checkColumnEntries(2, expectedValues);
+ checkSortDirection(passwordCol, true);
+ // Reset filter
+ setFilter("");
+ break;
+ case 5:
+ expectedValues = pwds.slice().sort();
+ checkColumnEntries(2, expectedValues);
+ checkSortDirection(passwordCol, true);
+ // cleanup
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ // unregister ourself
+ Services.ww.unregisterNotification(arguments.callee);
+
+ pwmgr.removeAllLogins();
+ finish();
+ });
+ pwmgrdlg.close();
+ }
+ }
+
+ // Toggle Show Passwords to display Password column, then start tests
+ toggleShowPasswords(runNextTest);
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js
new file mode 100644
index 0000000000..bd4f265b51
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.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/. */
+
+const PROMPT_URL = "chrome://global/content/commonDialog.xul";
+var { interfaces: Ci } = Components;
+
+function test() {
+ waitForExplicitFinish();
+
+ let tab = gBrowser.addTab();
+ isnot(tab, gBrowser.selectedTab, "New tab shouldn't be selected");
+
+ let listener = {
+ onOpenWindow: function(window) {
+ var domwindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ waitForFocus(() => {
+ is(domwindow.document.location.href, PROMPT_URL, "Should have seen a prompt window");
+ is(domwindow.args.promptType, "promptUserAndPass", "Should be an authenticate prompt");
+
+ is(gBrowser.selectedTab, tab, "Should have selected the new tab");
+
+ domwindow.document.documentElement.cancelDialog();
+ }, domwindow);
+ },
+
+ onCloseWindow: function() {
+ }
+ };
+
+ Services.wm.addListener(listener);
+ registerCleanupFunction(() => {
+ Services.wm.removeListener(listener);
+ gBrowser.removeTab(tab);
+ });
+
+ tab.linkedBrowser.addEventListener("load", () => {
+ finish();
+ }, true);
+ tab.linkedBrowser.loadURI("http://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs");
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
new file mode 100644
index 0000000000..57cfa9f83c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+ waitForExplicitFinish();
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+
+ // Add some initial logins
+ let urls = [
+ "http://example.com/",
+ "http://example.org/",
+ "http://mozilla.com/",
+ "http://mozilla.org/",
+ "http://spreadfirefox.com/",
+ "http://planet.mozilla.org/",
+ "https://developer.mozilla.org/",
+ "http://hg.mozilla.org/",
+ "http://dxr.mozilla.org/",
+ "http://feeds.mozilla.org/",
+ ];
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ let logins = [
+ new nsLoginInfo(urls[0], urls[0], null, "user", "password", "u1", "p1"),
+ new nsLoginInfo(urls[1], urls[1], null, "username", "password", "u2", "p2"),
+ new nsLoginInfo(urls[2], urls[2], null, "ehsan", "mypass", "u3", "p3"),
+ new nsLoginInfo(urls[3], urls[3], null, "ehsan", "mypass", "u4", "p4"),
+ new nsLoginInfo(urls[4], urls[4], null, "john", "smith", "u5", "p5"),
+ new nsLoginInfo(urls[5], urls[5], null, "what?", "very secret", "u6", "p6"),
+ new nsLoginInfo(urls[6], urls[6], null, "really?", "super secret", "u7", "p7"),
+ new nsLoginInfo(urls[7], urls[7], null, "you sure?", "absolutely", "u8", "p8"),
+ new nsLoginInfo(urls[8], urls[8], null, "my user name", "mozilla", "u9", "p9"),
+ new nsLoginInfo(urls[9], urls[9], null, "my username", "mozilla.com", "u10", "p10"),
+ ];
+ logins.forEach(login => pwmgr.addLogin(login));
+
+ // Open the password manager dialog
+ const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
+ let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", "");
+ SimpleTest.waitForFocus(doTest, pwmgrdlg);
+
+ // the meat of the test
+ function doTest() {
+ let doc = pwmgrdlg.document;
+ let win = doc.defaultView;
+ let filter = doc.getElementById("filter");
+ let tree = doc.getElementById("signonsTree");
+ let view = tree.view;
+
+ is(filter.value, "", "Filter box should initially be empty");
+ is(view.rowCount, 10, "There should be 10 passwords initially");
+
+ // Prepare a set of tests
+ // filter: the text entered in the filter search box
+ // count: the number of logins which should match the respective filter
+ // count2: the number of logins which should match the respective filter
+ // if the passwords are being shown as well
+ // Note: if a test doesn't have count2 set, count is used instead.
+ let tests = [
+ {filter: "pass", count: 0, count2: 4},
+ {filter: "", count: 10}, // test clearing the filter
+ {filter: "moz", count: 7},
+ {filter: "mozi", count: 7},
+ {filter: "mozil", count: 7},
+ {filter: "mozill", count: 7},
+ {filter: "mozilla", count: 7},
+ {filter: "mozilla.com", count: 1, count2: 2},
+ {filter: "user", count: 4},
+ {filter: "user ", count: 1},
+ {filter: " user", count: 2},
+ {filter: "http", count: 10},
+ {filter: "https", count: 1},
+ {filter: "secret", count: 0, count2: 2},
+ {filter: "secret!", count: 0},
+ ];
+
+ let toggleCalls = 0;
+ function toggleShowPasswords(func) {
+ let toggleButton = doc.getElementById("togglePasswords");
+ let showMode = (toggleCalls++ % 2) == 0;
+
+ // only watch for a confirmation dialog every other time being called
+ if (showMode) {
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ if (aTopic == "domwindowclosed")
+ Services.ww.unregisterNotification(arguments.callee);
+ else if (aTopic == "domwindowopened") {
+ let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ SimpleTest.waitForFocus(function() {
+ EventUtils.sendKey("RETURN", targetWin);
+ }, targetWin);
+ }
+ });
+ }
+
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ if (aTopic == "passwordmgr-password-toggle-complete") {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ func();
+ }
+ }, "passwordmgr-password-toggle-complete", false);
+
+ EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win);
+ }
+
+ function runTests(mode, endFunction) {
+ let testCounter = 0;
+
+ function setFilter(string) {
+ filter.value = string;
+ filter.doCommand();
+ }
+
+ function runOneTest(testCase) {
+ function tester() {
+ is(view.rowCount, expected, expected + " logins should match '" + testCase.filter + "'");
+ }
+
+ let expected;
+ switch (mode) {
+ case 1: // without showing passwords
+ expected = testCase.count;
+ break;
+ case 2: // showing passwords
+ expected = ("count2" in testCase) ? testCase.count2 : testCase.count;
+ break;
+ case 3: // toggle
+ expected = testCase.count;
+ tester();
+ toggleShowPasswords(function () {
+ expected = ("count2" in testCase) ? testCase.count2 : testCase.count;
+ tester();
+ toggleShowPasswords(proceed);
+ });
+ return;
+ }
+ tester();
+ proceed();
+ }
+
+ function proceed() {
+ // run the next test if necessary or proceed with the tests
+ if (testCounter != tests.length)
+ runNextTest();
+ else
+ endFunction();
+ }
+
+ function runNextTest() {
+ let testCase = tests[testCounter++];
+ setFilter(testCase.filter);
+ setTimeout(runOneTest, 0, testCase);
+ }
+
+ runNextTest();
+ }
+
+ function step1() {
+ runTests(1, step2);
+ }
+
+ function step2() {
+ toggleShowPasswords(function() {
+ runTests(2, step3);
+ });
+ }
+
+ function step3() {
+ toggleShowPasswords(function() {
+ runTests(3, lastStep);
+ });
+ }
+
+ function lastStep() {
+ // cleanup
+ Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+ // unregister ourself
+ Services.ww.unregisterNotification(arguments.callee);
+
+ pwmgr.removeAllLogins();
+ finish();
+ });
+ pwmgrdlg.close();
+ }
+
+ step1();
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js
new file mode 100644
index 0000000000..8df89b5105
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js
@@ -0,0 +1,144 @@
+/*
+ * Test username selection dialog, on password update from a p-only form,
+ * when there are multiple saved logins on the domain.
+ */
+
+// Copied from prompt_common.js. TODO: share the code.
+function getSelectDialogDoc() {
+ // Trudge through all the open windows, until we find the one
+ // that has selectDialog.xul loaded.
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ // var enumerator = wm.getEnumerator("navigator:browser");
+ var enumerator = wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ var containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ var childDocShell = containedDocShells.getNext();
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+ continue;
+ var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ if (childDoc.location.href == "chrome://global/content/selectDialog.xul")
+ return childDoc;
+ }
+ }
+
+ return null;
+}
+
+let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+let login1 = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1", "notifyp1", "user", "pass");
+let login1B = new nsLoginInfo("http://example.com", "http://example.com", null,
+ "notifyu1B", "notifyp1B", "user", "pass");
+
+add_task(function* test_changeUPLoginOnPUpdateForm_accept() {
+ info("Select an u+p login from multiple logins, on password update form, and accept.");
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return getSelectDialogDoc();
+ }, "Wait for selection dialog to be accessible.");
+
+ let doc = getSelectDialogDoc();
+ let dialog = doc.getElementsByTagName("dialog")[0];
+ let listbox = doc.getElementById("list");
+
+ is(listbox.selectedIndex, 0, "Checking selected index");
+ is(listbox.itemCount, 2, "Checking selected length");
+ ['notifyu1', 'notifyu1B'].forEach((username, i) => {
+ is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog");
+ });
+
+ dialog.acceptDialog();
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return !getSelectDialogDoc();
+ }, "Wait for selection dialog to disappear.");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have 2 logins");
+
+ let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "pass2", "Check the password changed");
+ is(login.timesUsed, 2, "Check times used");
+
+ login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(function* test_changeUPLoginOnPUpdateForm_cancel() {
+ info("Select an u+p login from multiple logins, on password update form, and cancel.");
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login1B);
+
+ yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) {
+ is(fieldValues.username, "null", "Checking submitted username");
+ is(fieldValues.password, "pass2", "Checking submitted password");
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return getSelectDialogDoc();
+ }, "Wait for selection dialog to be accessible.");
+
+ let doc = getSelectDialogDoc();
+ let dialog = doc.getElementsByTagName("dialog")[0];
+ let listbox = doc.getElementById("list");
+
+ is(listbox.selectedIndex, 0, "Checking selected index");
+ is(listbox.itemCount, 2, "Checking selected length");
+ ['notifyu1', 'notifyu1B'].forEach((username, i) => {
+ is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog");
+ });
+
+ dialog.cancelDialog();
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return !getSelectDialogDoc();
+ }, "Wait for selection dialog to disappear.");
+ });
+
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 2, "Should have 2 logins");
+
+ let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1", "Check the username unchanged");
+ is(login.password, "notifyp1", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo);
+ is(login.username, "notifyu1B", "Check the username unchanged");
+ is(login.password, "notifyp1B", "Check the password unchanged");
+ is(login.timesUsed, 1, "Check times used");
+
+ // cleanup
+ Services.logins.removeLogin(login1);
+ Services.logins.removeLogin(login1B);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html
new file mode 100644
index 0000000000..76056e3751
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head>
+<body onload="document.getElementById('form-basic-username').focus();">
+<!-- Username field is focused by js onload -->
+<form id="form-basic">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic.html b/toolkit/components/passwordmgr/test/browser/form_basic.html
new file mode 100644
index 0000000000..df2083a93c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html
new file mode 100644
index 0000000000..616f56947a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+</head>
+
+<body>
+ <!-- Form in an iframe -->
+ <iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
+</body>
+
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html
new file mode 100644
index 0000000000..e8aa8b2153
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="http://another.domain/custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html
new file mode 100644
index 0000000000..892a9f6f68
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="https://another.domain/custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html
new file mode 100644
index 0000000000..8f0c9a14e2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="./custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/formless_basic.html b/toolkit/components/passwordmgr/test/browser/formless_basic.html
new file mode 100644
index 0000000000..2f4c5de527
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/formless_basic.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+
+<!-- Simplest form with username and password fields. -->
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+
+ <button id="add">Add input[type=password]</button>
+
+ <script>
+ document.getElementById("add").addEventListener("click", function () {
+ var node = document.createElement("input");
+ node.setAttribute("type", "password");
+ document.querySelector("body").appendChild(node);
+ });
+ </script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js
new file mode 100644
index 0000000000..926cb66168
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/head.js
@@ -0,0 +1,137 @@
+const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+
+Cu.import("resource://testing-common/LoginTestUtils.jsm", this);
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+registerCleanupFunction(function* cleanup_removeAllLoginsAndResetRecipes() {
+ Services.logins.removeAllLogins();
+
+ let recipeParent = LoginTestUtils.recipes.getRecipeParent();
+ if (!recipeParent) {
+ // No need to reset the recipes if the recipe module wasn't even loaded.
+ return;
+ }
+ yield recipeParent.then(recipeParentResult => recipeParentResult.reset());
+});
+
+/**
+ * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a
+ * promise resolving with the field values when the optional `aTaskFn` is done.
+ *
+ * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs
+ * @param {Function} aTaskFn - task which can be run before the tab closes.
+ * @param {String} [aOrigin="http://example.com"] - origin of the server to use
+ * to load `aPageFile`.
+ */
+function testSubmittingLoginForm(aPageFile, aTaskFn, aOrigin = "http://example.com") {
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: aOrigin + DIRECTORY_PATH + aPageFile,
+ }, function*(browser) {
+ ok(true, "loaded " + aPageFile);
+ let fieldValues = yield ContentTask.spawn(browser, undefined, function*() {
+ yield ContentTaskUtils.waitForCondition(() => {
+ return content.location.pathname.endsWith("/formsubmit.sjs") &&
+ content.document.readyState == "complete";
+ }, "Wait for form submission load (formsubmit.sjs)");
+ let username = content.document.getElementById("user").textContent;
+ let password = content.document.getElementById("pass").textContent;
+ return {
+ username,
+ password,
+ };
+ });
+ ok(true, "form submission loaded");
+ if (aTaskFn) {
+ yield* aTaskFn(fieldValues);
+ }
+ return fieldValues;
+ });
+}
+
+function checkOnlyLoginWasUsedTwice({ justChanged }) {
+ // Check to make sure we updated the timestamps and use count on the
+ // existing login that was submitted for the test.
+ let logins = Services.logins.getAllLogins();
+ is(logins.length, 1, "Should only have 1 login");
+ ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI");
+ is(logins[0].timesUsed, 2, "check .timesUsed for existing login submission");
+ ok(logins[0].timeCreated < logins[0].timeLastUsed, "timeLastUsed bumped");
+ if (justChanged) {
+ is(logins[0].timeLastUsed, logins[0].timePasswordChanged, "timeLastUsed == timePasswordChanged");
+ } else {
+ is(logins[0].timeCreated, logins[0].timePasswordChanged, "timeChanged not updated");
+ }
+}
+
+// Begin popup notification (doorhanger) functions //
+
+const REMEMBER_BUTTON = 0;
+const NEVER_BUTTON = 1;
+
+const CHANGE_BUTTON = 0;
+const DONT_CHANGE_BUTTON = 1;
+
+/**
+ * Checks if we have a password capture popup notification
+ * of the right type and with the right label.
+ *
+ * @param {String} aKind The desired `passwordNotificationType`
+ * @param {Object} [popupNotifications = PopupNotifications]
+ * @return the found password popup notification.
+ */
+function getCaptureDoorhanger(aKind, popupNotifications = PopupNotifications) {
+ ok(true, "Looking for " + aKind + " popup notification");
+ let notification = popupNotifications.getNotification("password");
+ if (notification) {
+ is(notification.options.passwordNotificationType, aKind, "Notification type matches.");
+ if (aKind == "password-change") {
+ is(notification.mainAction.label, "Update", "Main action label matches update doorhanger.");
+ } else if (aKind == "password-save") {
+ is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger.");
+ }
+ }
+ return notification;
+}
+
+/**
+ * Clicks the specified popup notification button.
+ *
+ * @param {Element} aPopup Popup Notification element
+ * @param {Number} aButtonIndex Number indicating which button to click.
+ * See the constants in this file.
+ */
+function clickDoorhangerButton(aPopup, aButtonIndex) {
+ ok(true, "Looking for action at index " + aButtonIndex);
+
+ let notifications = aPopup.owner.panel.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ ok(true, notifications.length + " notification(s)");
+ let notification = notifications[0];
+
+ if (aButtonIndex == 0) {
+ ok(true, "Triggering main action");
+ notification.button.doCommand();
+ } else if (aButtonIndex <= aPopup.secondaryActions.length) {
+ ok(true, "Triggering secondary action " + aButtonIndex);
+ notification.childNodes[aButtonIndex].doCommand();
+ }
+}
+
+/**
+ * Checks the doorhanger's username and password.
+ *
+ * @param {String} username The username.
+ * @param {String} password The password.
+ */
+function* checkDoorhangerUsernamePassword(username, password) {
+ yield BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById("password-notification-username").value == username;
+ }, "Wait for nsLoginManagerPrompter writeDataToUI()");
+ is(document.getElementById("password-notification-username").value, username,
+ "Check doorhanger username");
+ is(document.getElementById("password-notification-password").value, password,
+ "Check doorhanger password");
+}
+
+// End popup notification (doorhanger) functions //
diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test.html b/toolkit/components/passwordmgr/test/browser/insecure_test.html
new file mode 100644
index 0000000000..fedea1428e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- This frame is initially loaded over HTTP. -->
+<iframe id="test-iframe"
+ src="http://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html"/>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
new file mode 100644
index 0000000000..3f01e36a6f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<form>
+ <input name="password" type="password">
+</form>
+
+<!-- Link to reload this page over HTTPS. -->
+<a id="test-link"
+ href="https://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html">HTTPS</a>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/multiple_forms.html b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
new file mode 100644
index 0000000000..3f64f89930
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+
+<form class="test-form"
+ description="Password only form">
+ <input id='test-password-1' type='password' name='pname' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+
+<form class="test-form"
+ description="Username only form">
+ <input id='test-username-1' type='text' name='uname' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form">
+ <input id='test-username-2' type='text' name='uname' value=''>
+ <input id='test-password-2' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password form, prefilled username">
+ <input id='test-username-3' type='text' name='uname' value='testuser'>
+ <input id='test-password-3' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password form, prefilled username and password">
+ <input id='test-username-4' type='text' name='uname' value='testuser'>
+ <input id='test-password-4' type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="One username and two passwords empty form">
+ <input id='test-username-5' type='text' name='uname'>
+ <input id='test-password-5' type='password' name='pname'>
+ <input id='test-password2-5' type='password' name='pname2'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="One username and two passwords form, fields prefiled">
+ <input id='test-username-6' type='text' name='uname' value="testuser">
+ <input id='test-password-6' type='password' name='pname' value="testpass">
+ <input id='test-password2-6' type='password' name='pname2' value="testpass">
+ <button type='submit'>Submit</button>
+</form>
+
+
+<div class="test-form"
+ description="Username and password fields with no form">
+ <input id='test-username-7' type='text' name='uname' value="testuser">
+ <input id='test-password-7' type='password' name='pname' value="testpass">
+</div>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with disabled password">
+ <input id='test-username-8' type='text' name='uname' value=''>
+ <input id='test-password-8' type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with disabled username">
+ <input id='test-username-9' type='text' name='uname' value='' disabled>
+ <input id='test-password-9' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with readonly password">
+ <input id='test-username-10' type='text' name='uname' value=''>
+ <input id='test-password-10' type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with readonly username">
+ <input id='test-username-11' type='text' name='uname' value='' readonly>
+ <input id='test-password-11' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Two username and one passwords form, fields prefiled">
+ <input id='test-username-12' type='text' name='uname' value="testuser">
+ <input id='test-username2-12' type='text' name='uname2' value="testuser">
+ <input id='test-password-12' type='password' name='pname' value="testpass">
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Two username and one passwords form, one disabled username field">
+ <input id='test-username-13' type='text' name='uname'>
+ <input id='test-username2-13' type='text' name='uname2' disabled>
+ <input id='test-password-13' type='password' name='pname'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<div class="test-form"
+ description="Second username and password fields with no form">
+ <input id='test-username-14' type='text' name='uname'>
+ <input id='test-password-14' type='password' name='pname' expectedFail>
+</div>
+
+<!-- Form in an iframe -->
+<iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
+
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs
new file mode 100644
index 0000000000..84c75437e4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "test/content", false);
+}
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html
new file mode 100644
index 0000000000..b96faf2eef
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Basic 1un 1pw</title>
+</head>
+<body>
+<h2>Subtest 1</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html
new file mode 100644
index 0000000000..2dc96b4fdb
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 10</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
new file mode 100644
index 0000000000..cf3df5275f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Popup Windows</title>
+</head>
+<body>
+<h2>Subtest 11 (popup windows)</h2>
+<script>
+
+// Ignore the '?' and split on |
+[username, password, features, autoClose] = window.location.search.substring(1).split('|');
+
+var url = "subtst_notifications_11_popup.html?" + username + "|" + password;
+var popupWin = window.open(url, "subtst_11", features);
+
+// Popup window will call this function on form submission.
+function formSubmitted() {
+ if (autoClose)
+ popupWin.close();
+}
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
new file mode 100644
index 0000000000..2e8e4135c9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 11</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ // Get the password from the query string (exclude '?').
+ [username, password] = window.location.search.substring(1).split('|');
+ userField.value = username;
+ passField.value = password;
+ form.submit();
+ window.opener.formSubmitted();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html
new file mode 100644
index 0000000000..72651d6c12
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - autocomplete=off on the username field</title>
+</head>
+<body>
+<h2>Subtest 2</h2>
+(username autocomplete=off)
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" autocomplete="off">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html
new file mode 100644
index 0000000000..7ddbf0851f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications with 2 password fields and no username</title>
+</head>
+<body>
+<h2>Subtest 24</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass1" name="pass1" type="password" value="staticpw">
+ <input id="pass" name="pass" type="password">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ pass.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var pass = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html
new file mode 100644
index 0000000000..893f18724a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications with 2 password fields and 1 username field and one other text field before the first password field</title>
+</head>
+<body>
+<h2>1 username field followed by a text field followed by 2 username fields</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" value="staticpw">
+ <input id="city" name="city" value="city">
+ <input id="pass" name="pass" type="password">
+ <input id="pin" name="pin" type="password" value="static-pin">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html
new file mode 100644
index 0000000000..291e735d0c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - autocomplete=off on the password field</title>
+</head>
+<body>
+<h2>Subtest 3</h2>
+(password autocomplete=off)
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password" autocomplete="off">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html
new file mode 100644
index 0000000000..63df3a42da
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" >
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 4</h2>
+(form autocomplete=off)
+<form id="form" action="formsubmit.sjs" autocomplete="off">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html
new file mode 100644
index 0000000000..72a3df95f4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Form with only a username field</title>
+</head>
+<body>
+<h2>Subtest 5</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html
new file mode 100644
index 0000000000..47e23e9728
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 6</h2>
+(password-only form)
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html
new file mode 100644
index 0000000000..abeea42620
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 8</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "pass2";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html
new file mode 100644
index 0000000000..c6f741068b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 9</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "";
+ passField.value = "pass2";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html
new file mode 100644
index 0000000000..d74f3bcdff
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Change password</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass_current" name="pass_current" type="password" value="notifyp1">
+ <input id="pass" name="pass" type="password">
+ <input id="pass_confirm" name="pass_confirm" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "pass2";
+ passConfirmField.value = "pass2";
+
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+var passConfirmField = document.getElementById("pass_confirm");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/chrome.ini b/toolkit/components/passwordmgr/test/chrome/chrome.ini
new file mode 100644
index 0000000000..093b87b7d0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/chrome.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+skip-if = os == 'android'
+
+[test_privbrowsing_perwindowpb.html]
+skip-if = true # Bug 1173337
+support-files =
+ ../formsubmit.sjs
+ notification_common.js
+ privbrowsing_perwindowpb_iframe.html
+ subtst_privbrowsing_1.html
+ subtst_privbrowsing_2.html
+ subtst_privbrowsing_3.html
+ subtst_privbrowsing_4.html
diff --git a/toolkit/components/passwordmgr/test/chrome/notification_common.js b/toolkit/components/passwordmgr/test/chrome/notification_common.js
new file mode 100644
index 0000000000..e8a52929de
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/notification_common.js
@@ -0,0 +1,111 @@
+/*
+ * Initialization: for each test, remove any prior notifications.
+ */
+function cleanUpPopupNotifications() {
+ var container = getPopupNotifications(window.top);
+ var notes = container._currentNotifications;
+ info(true, "Removing " + notes.length + " popup notifications.");
+ for (var i = notes.length - 1; i >= 0; i--) {
+ notes[i].remove();
+ }
+}
+cleanUpPopupNotifications();
+
+/*
+ * getPopupNotifications
+ *
+ * Fetches the popup notification for the specified window.
+ */
+function getPopupNotifications(aWindow) {
+ var Ci = SpecialPowers.Ci;
+ var Cc = SpecialPowers.Cc;
+ ok(Ci != null, "Access Ci");
+ ok(Cc != null, "Access Cc");
+
+ var chromeWin = SpecialPowers.wrap(aWindow)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument.defaultView;
+
+ var popupNotifications = chromeWin.PopupNotifications;
+ return popupNotifications;
+}
+
+
+/**
+ * Checks if we have a password popup notification
+ * of the right type and with the right label.
+ *
+ * @deprecated Write a browser-chrome test instead and use the fork of this method there.
+ * @returns the found password popup notification.
+ */
+function getPopup(aPopupNote, aKind) {
+ ok(true, "Looking for " + aKind + " popup notification");
+ var notification = aPopupNote.getNotification("password");
+ if (notification) {
+ is(notification.options.passwordNotificationType, aKind, "Notification type matches.");
+ if (aKind == "password-change") {
+ is(notification.mainAction.label, "Update", "Main action label matches update doorhanger.");
+ } else if (aKind == "password-save") {
+ is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger.");
+ }
+ }
+ return notification;
+}
+
+
+/**
+ * @deprecated - Use a browser chrome test instead.
+ *
+ * Clicks the specified popup notification button.
+ */
+function clickPopupButton(aPopup, aButtonIndex) {
+ ok(true, "Looking for action at index " + aButtonIndex);
+
+ var notifications = SpecialPowers.wrap(aPopup.owner).panel.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ ok(true, notifications.length + " notifications");
+ var notification = notifications[0];
+
+ if (aButtonIndex == 0) {
+ ok(true, "Triggering main action");
+ notification.button.doCommand();
+ } else if (aButtonIndex <= aPopup.secondaryActions.length) {
+ var index = aButtonIndex;
+ ok(true, "Triggering secondary action " + index);
+ notification.childNodes[index].doCommand();
+ }
+}
+
+const kRememberButton = 0;
+const kNeverButton = 1;
+
+const kChangeButton = 0;
+const kDontChangeButton = 1;
+
+function dumpNotifications() {
+ try {
+ // PopupNotifications
+ var container = getPopupNotifications(window.top);
+ ok(true, "is popup panel open? " + container.isPanelOpen);
+ var notes = container._currentNotifications;
+ ok(true, "Found " + notes.length + " popup notifications.");
+ for (let i = 0; i < notes.length; i++) {
+ ok(true, "#" + i + ": " + notes[i].id);
+ }
+
+ // Notification bars
+ var chromeWin = SpecialPowers.wrap(window.top)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument.defaultView;
+ var nb = chromeWin.getNotificationBox(window.top);
+ notes = nb.allNotifications;
+ ok(true, "Found " + notes.length + " notification bars.");
+ for (let i = 0; i < notes.length; i++) {
+ ok(true, "#" + i + ": " + notes[i].getAttribute("value"));
+ }
+ } catch (e) { todo(false, "WOAH! " + e); }
+}
diff --git a/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html b/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html
new file mode 100644
index 0000000000..2efdab2654
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<iframe id="iframe"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html
new file mode 100644
index 0000000000..8c7202dd0f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_1.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+</head>
+<body>
+<h2>Subtest 1</h2>
+<!--
+ Make sure that the password-save notification appears outside of
+ the private mode, but not inside it.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ userField.value = "notifyu1";
+ passField.value = "notifyp1";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html
new file mode 100644
index 0000000000..bf3b851595
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_2.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+</head>
+<body>
+<h2>Subtest 2</h2>
+<!--
+ Make sure that the password-change notification appears outside of
+ the private mode, but not inside it.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <input id="newpass" name="newpass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ passField.value = "notifyp1";
+ passField2.value = "notifyp2";
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var passField = document.getElementById("pass");
+var passField2 = document.getElementById("newpass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html
new file mode 100644
index 0000000000..e88a302e0c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_3.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+</head>
+<body>
+<h2>Subtest 3</h2>
+<!--
+ Make sure that the user/pass fields are auto-filled outside of
+ the private mode, but not inside it.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ form.submit();
+}
+
+var form = document.getElementById("form");
+window.addEventListener('message', () => { submitForm(); });
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html
new file mode 100644
index 0000000000..184142743d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/subtst_privbrowsing_4.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications (private browsing)</title>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+</head>
+<body>
+<h2>Subtest 4</h2>
+<!--
+ Make sure that the user/pass fields have manual filling enabled
+ in private mode.
+-->
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function startAutocomplete() {
+ userField.focus();
+ doKey("down");
+ setTimeout(submitForm, 100);
+}
+
+function submitForm() {
+ doKey("down");
+ doKey("return");
+ setTimeout(function() { form.submit(); }, 100);
+}
+
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+
+window.addEventListener('message', () => { startAutocomplete(); });
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html b/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html
new file mode 100644
index 0000000000..6b7d4abb3a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome/test_privbrowsing_perwindowpb.html
@@ -0,0 +1,322 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=248970
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Private Browsing</title>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="notification_common.js"></script>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=248970">Mozilla Bug 248970</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 248970 **/
+// based on test_notifications.html
+
+const Ci = SpecialPowers.Ci;
+const Cc = SpecialPowers.Cc;
+const Cr = SpecialPowers.Cr;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var testpath = "/chrome/toolkit/components/passwordmgr/test/chrome/";
+var prefix = "http://test2.example.com" + testpath;
+var subtests = [
+ "subtst_privbrowsing_1.html", // 1
+ "subtst_privbrowsing_1.html", // 2
+ "subtst_privbrowsing_1.html", // 3
+ "subtst_privbrowsing_2.html", // 4
+ "subtst_privbrowsing_2.html", // 5
+ "subtst_privbrowsing_2.html", // 6
+ "subtst_privbrowsing_3.html", // 7
+ "subtst_privbrowsing_3.html", // 8
+ "subtst_privbrowsing_4.html", // 9
+ "subtst_privbrowsing_3.html" // 10
+ ];
+var observer;
+
+var testNum = 0;
+function loadNextTest() {
+ // run the initialization code for each test
+ switch (++ testNum) {
+ case 1:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ case 2:
+ popupNotifications = privateWindowPopupNotifications;
+ iframe = privateWindowIframe;
+ break;
+
+ case 3:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ case 4:
+ pwmgr.addLogin(login);
+ break;
+
+ case 5:
+ popupNotifications = privateWindowPopupNotifications;
+ iframe = privateWindowIframe;
+ break;
+
+ case 6:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ case 7:
+ pwmgr.addLogin(login);
+ break;
+
+ case 8:
+ popupNotifications = privateWindowPopupNotifications;
+ iframe = privateWindowIframe;
+ break;
+
+ case 9:
+ break;
+
+ case 10:
+ popupNotifications = normalWindowPopupNotifications;
+ iframe = normalWindowIframe;
+ break;
+
+ default:
+ ok(false, "Unexpected call to loadNextTest for test #" + testNum);
+ }
+
+ if (testNum === 7) {
+ observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
+ SimpleTest.executeSoon(() => { iframe.contentWindow.postMessage("go", "*"); });
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+ }
+
+ ok(true, "Starting test #" + testNum);
+ iframe.src = prefix + subtests[testNum - 1];
+}
+
+function checkTest() {
+ var popup;
+ var gotUser;
+ var gotPass;
+
+ switch (testNum) {
+ case 1:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-save");
+ ok(popup, "got popup notification");
+ popup.remove();
+ break;
+
+ case 2:
+ // run inside of private mode, popup notification should not appear
+ popup = getPopup(popupNotifications, "password-save");
+ ok(!popup, "checking for no popup notification");
+ break;
+
+ case 3:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-save");
+ ok(popup, "got popup notification");
+ popup.remove();
+ break;
+
+ case 4:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-change");
+ ok(popup, "got popup notification");
+ popup.remove();
+ break;
+
+ case 5:
+ // run inside of private mode, popup notification should not appear
+ popup = getPopup(popupNotifications, "password-change");
+ ok(!popup, "checking for no popup notification");
+ break;
+
+ case 6:
+ // run outside of private mode, popup notification should appear
+ popup = getPopup(popupNotifications, "password-change");
+ ok(popup, "got popup notification");
+ popup.remove();
+ pwmgr.removeLogin(login);
+ break;
+
+ case 7:
+ // verify that the user/pass pair was autofilled
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "notifyu1", "Checking submitted username");
+ is(gotPass, "notifyp1", "Checking submitted password");
+ break;
+
+ case 8:
+ // verify that the user/pass pair was not autofilled
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "", "Checking submitted username");
+ is(gotPass, "", "Checking submitted password");
+ break;
+
+ case 9:
+ // verify that the user/pass pair was available for autocomplete
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "notifyu1", "Checking submitted username");
+ is(gotPass, "notifyp1", "Checking submitted password");
+ break;
+
+ case 10:
+ // verify that the user/pass pair was autofilled
+ gotUser = iframe.contentDocument.getElementById("user").textContent;
+ gotPass = iframe.contentDocument.getElementById("pass").textContent;
+ is(gotUser, "notifyu1", "Checking submitted username");
+ is(gotPass, "notifyp1", "Checking submitted password");
+ pwmgr.removeLogin(login);
+ break;
+
+ default:
+ ok(false, "Unexpected call to checkTest for test #" + testNum);
+
+ }
+}
+
+var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+var contentPage = "http://mochi.test:8888/chrome/toolkit/components/passwordmgr/test/chrome/privbrowsing_perwindowpb_iframe.html";
+var testWindows = [];
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function obs(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(obs, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function testOnWindow(aIsPrivate, aCallback) {
+ var win = mainWindow.OpenBrowserWindow({private: aIsPrivate});
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(contentPage);
+ return;
+ }
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+
+ win.content.addEventListener('load', function innerLoad2() {
+ win.content.removeEventListener('load', innerLoad2, false);
+ testWindows.push(win);
+ SimpleTest.executeSoon(function() { aCallback(win); });
+ }, false, true);
+ }, true);
+ SimpleTest.executeSoon(function() { win.gBrowser.loadURI(contentPage); });
+ });
+ }, true);
+}
+
+var ignoreLoad = false;
+function handleLoad(aEvent) {
+ // ignore every other load event ... We get one for loading the subtest (which
+ // we want to ignore), and another when the subtest's form submits itself
+ // (which we want to handle, to start the next test).
+ ignoreLoad = !ignoreLoad;
+ if (ignoreLoad) {
+ ok(true, "Ignoring load of subtest #" + testNum);
+ return;
+ }
+ ok(true, "Processing submission of subtest #" + testNum);
+
+ checkTest();
+
+ if (testNum < subtests.length) {
+ loadNextTest();
+ } else {
+ ok(true, "private browsing notification tests finished.");
+
+ testWindows.forEach(function(aWin) {
+ aWin.close();
+ });
+
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ SimpleTest.finish();
+ }
+}
+
+var pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ok(pwmgr != null, "Access pwmgr");
+
+// We need to make sure no logins have been stored by previous tests
+// for forms in |url|, otherwise the change password notification
+// would turn into a prompt, and the test will fail.
+var url = "http://test2.example.com";
+is(pwmgr.countLogins(url, "", null), 0, "No logins should be stored for " + url);
+
+var nsLoginInfo = new SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+var login = new nsLoginInfo(url, url, null, "notifyu1", "notifyp1", "user", "pass");
+
+var normalWindow;
+var privateWindow;
+
+var iframe;
+var normalWindowIframe;
+var privateWindowIframe;
+
+var popupNotifications;
+var normalWindowPopupNotifications;
+var privateWindowPopupNotifications;
+
+testOnWindow(false, function(aWin) {
+ var selectedBrowser = aWin.gBrowser.selectedBrowser;
+ normalWindowIframe = selectedBrowser.contentDocument.getElementById("iframe");
+ normalWindowIframe.onload = handleLoad;
+ selectedBrowser.focus();
+
+ normalWindowPopupNotifications = getPopupNotifications(selectedBrowser.contentWindow.top);
+ ok(normalWindowPopupNotifications, "Got popupNotifications in normal window");
+ // ignore the first load for this window;
+ ignoreLoad = false;
+
+ testOnWindow(true, function(aPrivateWin) {
+ selectedBrowser = aPrivateWin.gBrowser.selectedBrowser;
+ privateWindowIframe = selectedBrowser.contentDocument.getElementById("iframe");
+ privateWindowIframe.onload = handleLoad;
+ selectedBrowser.focus();
+
+ privateWindowPopupNotifications = getPopupNotifications(selectedBrowser.contentWindow.top);
+ ok(privateWindowPopupNotifications, "Got popupNotifications in private window");
+ // ignore the first load for this window;
+ ignoreLoad = false;
+
+ SimpleTest.executeSoon(loadNextTest);
+ });
+});
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/chrome_timeout.js b/toolkit/components/passwordmgr/test/chrome_timeout.js
new file mode 100644
index 0000000000..9049d0beaa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/chrome_timeout.js
@@ -0,0 +1,11 @@
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+addMessageListener('setTimeout', msg => {
+ let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
+ timer.init(_ => {
+ sendAsyncMessage('timeout');
+ }, msg.delay, Ci.nsITimer.TYPE_ONE_SHOT);
+});
+
+sendAsyncMessage('ready');
diff --git a/toolkit/components/passwordmgr/test/formsubmit.sjs b/toolkit/components/passwordmgr/test/formsubmit.sjs
new file mode 100644
index 0000000000..4b4a387f7b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/formsubmit.sjs
@@ -0,0 +1,37 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true;
+
+ // XXX I bet this doesn't work for POST requests.
+ var query = request.queryString;
+
+ var user = null, pass = null;
+ // user=xxx
+ match = /user=([^&]*)/.exec(query);
+ if (match)
+ user = match[1];
+
+ // pass=xxx
+ match = /pass=([^&]*)/.exec(query);
+ if (match)
+ pass = match[1];
+
+ response.setStatusLine("1.0", 200, "OK");
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>User: <span id='user'>" + user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + pass + "</span></p>\n");
+ response.write("</html>");
+}
diff --git a/toolkit/components/passwordmgr/test/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest.ini
new file mode 100644
index 0000000000..640f5c2563
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+skip-if = e10s
+support-files =
+ authenticate.sjs
+ blank.html
+ formsubmit.sjs
+ prompt_common.js
+ pwmgr_common.js
+ subtst_master_pass.html
+ subtst_prompt_async.html
+ chrome_timeout.js
+
+[test_master_password.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_prompt_async.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_xhr.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_xml_load.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
diff --git a/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs
new file mode 100644
index 0000000000..d2f6500131
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs
@@ -0,0 +1,220 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw new Error("Couldn't parse auth header: " + authHeader);
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw new Error("Couldn't decode auth header: " + userpass);
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..a4170d7e0e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+support-files =
+ ../../../prompts/test/chromeScript.js
+ ../../../prompts/test/prompt_common.js
+ ../../../satchel/test/parent_utils.js
+ ../../../satchel/test/satchel_common.js
+ ../authenticate.sjs
+ ../blank.html
+ ../browser/form_autofocus_js.html
+ ../browser/form_basic.html
+ ../browser/form_cross_origin_secure_action.html
+ ../pwmgr_common.js
+ auth2/authenticate.sjs
+
+[test_autocomplete_https_upgrade.html]
+skip-if = toolkit == 'android' # autocomplete
+[test_autofill_https_upgrade.html]
+skip-if = toolkit == 'android' # Bug 1259768
+[test_autofill_password-only.html]
+[test_autofocus_js.html]
+skip-if = toolkit == 'android' # autocomplete
+[test_basic_form.html]
+[test_basic_form_0pw.html]
+[test_basic_form_1pw.html]
+[test_basic_form_1pw_2.html]
+[test_basic_form_2pw_1.html]
+[test_basic_form_2pw_2.html]
+[test_basic_form_3pw_1.html]
+[test_basic_form_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_insecure_form_field_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_password_field_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_insecure_form_field_no_saved_login.html]
+skip-if = toolkit == 'android' || os == 'linux' # android:autocomplete., linux: bug 1325778
+[test_basic_form_html5.html]
+[test_basic_form_pwevent.html]
+[test_basic_form_pwonly.html]
+[test_bug_627616.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_bug_776171.html]
+[test_case_differences.html]
+skip-if = toolkit == 'android' # autocomplete
+[test_form_action_1.html]
+[test_form_action_2.html]
+[test_form_action_javascript.html]
+[test_formless_autofill.html]
+[test_formless_submit.html]
+[test_formless_submit_navigation.html]
+[test_formless_submit_navigation_negative.html]
+[test_input_events.html]
+[test_input_events_for_identical_values.html]
+[test_maxlength.html]
+[test_passwords_in_type_password.html]
+[test_prompt.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_http.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_noWindow.html]
+skip-if = e10s || toolkit == 'android' # Tests desktop prompts. e10s: bug 1217876
+[test_prompt_promptAuth.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_promptAuth_proxy.html]
+skip-if = e10s || os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_recipe_login_fields.html]
+[test_username_focus.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_xhr_2.html]
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
new file mode 100644
index 0000000000..7d57253225
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
@@ -0,0 +1,218 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const chromeScript = runChecksAfterCommonInit(false);
+
+runInParent(function addLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ let nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+ // We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true.
+ let login0 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name", "pass", "uname", "pword");
+
+ let login1 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name1", "pass1", "uname", "pword");
+
+ // Same as above but HTTP instead of HTTPS (to test de-duping)
+ let login2 = new nsLoginInfo("http://example.org", "http://example.org", null,
+ "name1", "passHTTP", "uname", "pword");
+
+ // Different HTTP login to upgrade with secure formSubmitURL
+ let login3 = new nsLoginInfo("http://example.org", "https://example.org", null,
+ "name2", "passHTTPtoHTTPS", "uname", "pword");
+
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe src="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+let iframeDoc;
+let uname;
+let pword;
+
+// Restore the form to the default state.
+function restoreForm() {
+ pword.focus();
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ let formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username");
+ is(pword.value, expectedPassword, "Checking " + formID + " password");
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
+
+ yield new Promise(resolve => {
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ resolve();
+ });
+ });
+
+ iframeDoc = iframe.contentDocument;
+ uname = iframeDoc.getElementById("form-basic-username");
+ pword = iframeDoc.getElementById("form-basic-password");
+});
+
+add_task(function* test_empty_first_entry() {
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ // Trigger autocomplete popup
+ restoreForm();
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ let shownPromise = promiseACShown();
+ doKey("down");
+ let results = yield shownPromise;
+ popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkArrayValues(results, ["name", "name1", "name2"], "initial");
+
+ // Check first entry
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name", "pass");
+});
+
+add_task(function* test_empty_second_entry() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name1", "pass1");
+});
+
+add_task(function* test_search() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ // We need to blur for the autocomplete controller to notice the forced value below.
+ uname.blur();
+ uname.value = "name";
+ uname.focus();
+ sendChar("1");
+ doKey("down"); // open
+ let results = yield shownPromise;
+ checkArrayValues(results, ["name1"], "check result deduping for 'name1'");
+ doKey("down"); // first
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name1", "pass1");
+
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is now closed");
+});
+
+add_task(function* test_delete_first_entry() {
+ restoreForm();
+ uname.focus();
+ let shownPromise = promiseACShown();
+ doKey("down");
+ yield shownPromise;
+
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+
+ let deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+ checkACForm("", "");
+
+ let results = yield notifyMenuChanged(2, "name1");
+
+ checkArrayValues(results, ["name1", "name2"], "two should remain after deleting the first");
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup stays open after deleting");
+ doKey("escape");
+ popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup closed upon ESC");
+});
+
+add_task(function* test_delete_duplicate_entry() {
+ restoreForm();
+ uname.focus();
+ let shownPromise = promiseACShown();
+ doKey("down");
+ yield shownPromise;
+
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+
+ let deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+ checkACForm("", "");
+
+ is(LoginManager.countLogins("http://example.org", "http://example.org", null), 1,
+ "Check that the HTTP login remains");
+ is(LoginManager.countLogins("https://example.org", "https://example.org", null), 0,
+ "Check that the HTTPS login was deleted");
+
+ // Two menu items should remain as the HTTPS login should have been deleted but
+ // the HTTP would remain.
+ let results = yield notifyMenuChanged(1, "name2");
+
+ checkArrayValues(results, ["name2"], "one should remain after deleting the HTTPS name1");
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup stays open after deleting");
+ doKey("escape");
+ popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup closed upon ESC");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
new file mode 100644
index 0000000000..ee1424002b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
+const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html";
+
+const chromeScript = runChecksAfterCommonInit(false);
+
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+SpecialPowers.Ci.nsILoginInfo,
+"init");
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ let iframeDoc = iframe.contentDocument;
+ let uname = iframeDoc.getElementById("form-basic-username");
+ let pword = iframeDoc.getElementById("form-basic-password");
+ let formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username");
+ is(pword.value, expectedPassword, "Checking " + formID + " password");
+}
+function* prepareLoginsAndProcessForm(url, logins = []) {
+ LoginManager.removeAllLogins();
+
+ let dates = Date.now();
+ for (let login of logins) {
+ SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
+ // Force all dates to be the same so they don't affect things like deduping.
+ login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
+ LoginManager.addLogin(login);
+ }
+
+ iframe.src = url;
+ yield promiseFormsProcessed();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
+});
+
+add_task(function* test_simpleNoDupesNoAction() {
+ yield prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
+ new nsLoginInfo("http://example.com", "http://example.com", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_simpleNoDupesUpgradeOriginAndAction() {
+ yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("http://example.com", "http://another.domain", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_simpleNoDupesUpgradeOriginOnly() {
+ yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("http://example.com", "https://another.domain", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_simpleNoDupesUpgradeActionOnly() {
+ yield prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("https://example.com", "http://another.domain", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ checkACForm("name2", "pass2");
+});
+
+add_task(function* test_dedupe() {
+ yield prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
+ new nsLoginInfo("https://example.com", "https://example.com", null,
+ "name1", "passHTTPStoHTTPS", "uname", "pword"),
+ new nsLoginInfo("http://example.com", "http://example.com", null,
+ "name1", "passHTTPtoHTTP", "uname", "pword"),
+ new nsLoginInfo("http://example.com", "https://example.com", null,
+ "name1", "passHTTPtoHTTPS", "uname", "pword"),
+ new nsLoginInfo("https://example.com", "http://example.com", null,
+ "name1", "passHTTPStoHTTP", "uname", "pword"),
+ ]);
+
+ checkACForm("name1", "passHTTPStoHTTPS");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
new file mode 100644
index 0000000000..983356371a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test password-only forms should prefer a password-only login when present</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 444968
+<script>
+let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+pwmgrCommonScript.sendSyncMessage("setupParent", { selfFilling: true });
+
+SimpleTest.waitForExplicitFinish();
+
+let chromeScript = runInParent(function chromeSetup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+
+ let login1A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login1B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1A.init("http://mochi.test:8888", "http://bug444968-1", null,
+ "testuser1A", "testpass1A", "", "");
+ login1B.init("http://mochi.test:8888", "http://bug444968-1", null,
+ "", "testpass1B", "", "");
+
+ login2A.init("http://mochi.test:8888", "http://bug444968-2", null,
+ "testuser2A", "testpass2A", "", "");
+ login2B.init("http://mochi.test:8888", "http://bug444968-2", null,
+ "", "testpass2B", "", "");
+ login2C.init("http://mochi.test:8888", "http://bug444968-2", null,
+ "testuser2C", "testpass2C", "", "");
+
+ pwmgr.addLogin(login1A);
+ pwmgr.addLogin(login1B);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+ pwmgr.addLogin(login2C);
+
+ addMessageListener("removeLogins", function removeLogins() {
+ pwmgr.removeLogin(login1A);
+ pwmgr.removeLogin(login1B);
+ pwmgr.removeLogin(login2A);
+ pwmgr.removeLogin(login2B);
+ pwmgr.removeLogin(login2C);
+ });
+});
+
+SimpleTest.registerCleanupFunction(() => chromeScript.sendSyncMessage("removeLogins"));
+
+registerRunTests();
+</script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- first 3 forms have matching user+pass and pass-only logins -->
+
+ <!-- user+pass form. -->
+ <form id="form1" action="http://bug444968-1">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password-only form. -->
+ <form id="form2" action="http://bug444968-1">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form3" action="http://bug444968-1">
+ <input type="text" name="uname" value="testuser1A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+ <!-- next 4 forms have matching user+pass (2x) and pass-only (1x) logins -->
+
+ <!-- user+pass form. -->
+ <form id="form4" action="http://bug444968-2">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password-only form. -->
+ <form id="form5" action="http://bug444968-2">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form6" action="http://bug444968-2">
+ <input type="text" name="uname" value="testuser2A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form7" action="http://bug444968-2">
+ <input type="text" name="uname" value="testuser2C">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 444968 (password-only forms should prefer a
+ * password-only login when present )
+ */
+function startTest() {
+ checkForm(1, "testuser1A", "testpass1A");
+ checkForm(2, "testpass1B");
+ checkForm(3, "testuser1A", "testpass1A");
+
+ checkUnmodifiedForm(4); // 2 logins match
+ checkForm(5, "testpass2B");
+ checkForm(6, "testuser2A", "testpass2A");
+ checkForm(7, "testuser2C", "testpass2C");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("runTests", startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
new file mode 100644
index 0000000000..2ce3293dda
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test login autocomplete is activated when focused by js on load</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const chromeScript = runChecksAfterCommonInit(false);
+
+runInParent(function addLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ let nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+ let login0 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name", "pass", "uname", "pword");
+
+ let login1 = new nsLoginInfo("https://example.org", "https://example.org", null,
+ "name1", "pass1", "uname", "pword");
+
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+});
+</script>
+<p id="display"></p>
+
+<div id="content">
+ <iframe src="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/form_autofocus_js.html"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+let iframeDoc;
+
+add_task(function* setup() {
+ yield new Promise(resolve => {
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ resolve();
+ });
+ });
+
+ iframeDoc = iframe.contentDocument;
+
+ SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur");
+});
+
+add_task(function* test_initial_focus() {
+ let results = yield notifyMenuChanged(2, "name");
+ checkArrayValues(results, ["name", "name1"], "Two results");
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ is(iframeDoc.getElementById("form-basic-password").value, "pass", "Check first password filled");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is now closed");
+});
+
+// This depends on the filling from the previous test.
+add_task(function* test_not_reopened_if_filled() {
+ listenForUnexpectedPopupShown();
+ let usernameField = iframeDoc.getElementById("form-basic-username");
+ usernameField.focus();
+ info("Waiting to see if a popupshown occurs");
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+
+ // cleanup
+ gPopupShownExpected = true;
+ iframeDoc.getElementById("form-basic-submit").focus();
+});
+
+add_task(function* test_reopened_after_edit_not_matching_saved() {
+ let usernameField = iframeDoc.getElementById("form-basic-username");
+ usernameField.value = "nam";
+ let shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+ iframeDoc.getElementById("form-basic-submit").focus();
+});
+
+add_task(function* test_not_reopened_after_selecting() {
+ let formFillController = SpecialPowers.Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(SpecialPowers.Ci.nsIFormFillController);
+ let usernameField = iframeDoc.getElementById("form-basic-username");
+ usernameField.value = "";
+ iframeDoc.getElementById("form-basic-password").value = "";
+ listenForUnexpectedPopupShown();
+ formFillController.markAsLoginManagerField(usernameField);
+ info("Waiting to see if a popupshown occurs");
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Cleanup
+ gPopupShownExpected = true;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html
new file mode 100644
index 0000000000..3c38343a5d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: simple form fill
+
+<script>
+runChecksAfterCommonInit(startTest);
+
+/** Test for Login Manager: form fill, multiple forms. **/
+
+function startTest() {
+ is($_(1, "uname").value, "testuser", "Checking for filled username");
+ is($_(1, "pword").value, "testpass", "Checking for filled password");
+
+ SimpleTest.finish();
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
new file mode 100644
index 0000000000..0b416673b3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with no password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with no password fields
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <!-- Form with no user field or password field -->
+ <form id="form1" action="formtest.js">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with no user field or password field, but one other field -->
+ <form id="form2" action="formtest.js">
+ <input type="checkbox">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with no user field or password field, but one other field -->
+ <form id="form3" action="formtest.js">
+ <input type="checkbox" name="uname" value="">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with a text field, but no password field -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="yyyyy">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with a user field, but no password field -->
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, no password fields. **/
+
+function startTest() {
+ is($_(3, "uname").value, "", "Checking for unfilled checkbox (form 3)");
+ is($_(4, "yyyyy").value, "", "Checking for unfilled text field (form 4)");
+ is($_(5, "uname").value, "", "Checking for unfilled text field (form 5)");
+
+ SimpleTest.finish();
+}
+
+runChecksAfterCommonInit(startTest);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html
new file mode 100644
index 0000000000..3937fad4bd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 1 password field</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 1 password field
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+<!-- no username fields -->
+
+<form id='form1' action='formtest.js'> 1
+ <!-- Blank, so fill in the password -->
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form2' action='formtest.js'> 2
+ <!-- Already contains the password, so nothing to do. -->
+ <input type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form3' action='formtest.js'> 3
+ <!-- Contains unknown password, so don't change it -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<!-- username fields -->
+
+<form id='form4' action='formtest.js'> 4
+ <!-- Blanks, so fill in login -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form5' action='formtest.js'> 5
+ <!-- Username already set, so fill in password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form6' action='formtest.js'> 6
+ <!-- Unknown username, so don't fill in password -->
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form7' action='formtest.js'> 7
+ <!-- Password already set, could fill in username but that's weird so we don't -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form8' action='formtest.js'> 8
+ <!-- Unknown password, so don't fill in a username -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+
+<!-- extra text fields -->
+
+<form id='form9' action='formtest.js'> 9
+ <!-- text field _after_ password should never be treated as a username field -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form10' action='formtest.js'> 10
+ <!-- only the first text field before the password should be for username -->
+ <input type='text' name='other' value=''>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form11' action='formtest.js'> 11
+ <!-- variation just to make sure extra text field is still ignored -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='text' name='other' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+
+<!-- same as last bunch, but with xxxx in the extra field. -->
+
+<form id='form12' action='formtest.js'> 12
+ <!-- text field _after_ password should never be treated as a username field -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form13' action='formtest.js'> 13
+ <!-- only the first text field before the password should be for username -->
+ <input type='text' name='other' value='xxxxxxxx'>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form14' action='formtest.js'> 14
+ <!-- variation just to make sure extra text field is still ignored -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='text' name='other' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill **/
+
+function startTest() {
+ var f = 1;
+
+ // 1-3
+ checkForm(f++, "testpass");
+ checkForm(f++, "testpass");
+ checkForm(f++, "xxxxxxxx");
+
+ // 4-8
+ checkForm(f++, "testuser", "testpass");
+ checkForm(f++, "testuser", "testpass");
+ checkForm(f++, "xxxxxxxx", "");
+ checkForm(f++, "", "testpass");
+ checkForm(f++, "", "xxxxxxxx");
+
+ // 9-14
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "", "testuser", "testpass");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "testpass", "xxxxxxxx");
+ checkForm(f++, "xxxxxxxx", "testuser", "testpass");
+ checkForm(f++, "testuser", "testpass", "xxxxxxxx");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html
new file mode 100644
index 0000000000..0f6566b9c2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with 1 password field, part 2</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 1 password field, part 2
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+<form id='form1' action='formtest.js'> 1
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form2' action='formtest.js'> 2
+ <input type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form3' action='formtest.js'> 3
+ <input type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form4' action='formtest.js'> 4
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form5' action='formtest.js'> 5
+ <input type='text' name='uname' value='' disabled>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form6' action='formtest.js'> 6
+ <input type='text' name='uname' value='' readonly>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form7' action='formtest.js'> 7
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form8' action='formtest.js'> 8
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form9' action='formtest.js'> 9
+ <input type='text' name='uname' value='TESTUSER'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form10' action='formtest.js'> 10
+ <input type='text' name='uname' value='TESTUSER' readonly>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form11' action='formtest.js'> 11
+ <input type='text' name='uname' value='TESTUSER' disabled>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill, part 2 **/
+
+function startTest() {
+ var f;
+
+ // Test various combinations of disabled/readonly inputs
+ checkForm(1, "testpass"); // control
+ checkUnmodifiedForm(2);
+ checkUnmodifiedForm(3);
+ checkForm(4, "testuser", "testpass"); // control
+ for (f = 5; f <= 8; f++) { checkUnmodifiedForm(f); }
+ // Test case-insensitive comparison of username field
+ checkForm(9, "testuser", "testpass");
+ checkForm(10, "TESTUSER", "testpass");
+ checkForm(11, "TESTUSER", "testpass");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html
new file mode 100644
index 0000000000..128ffca7c7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 2 password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 2 password fields
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+<!-- no username fields -->
+
+<form id='form1' action='formtest.js'> 1
+ <!-- simple form, fill in first pw -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form2' action='formtest.js'> 2
+ <!-- same but reverse pname and qname, field names are ignored. -->
+ <input type='password' name='qname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form3' action='formtest.js'> 3
+ <!-- text field after password fields should be ignored, no username. -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <input type='text' name='uname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form4' action='formtest.js'> 4
+ <!-- nothing to do, password already present -->
+ <input type='password' name='pname' value='testpass'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form5' action='formtest.js'> 5
+ <!-- don't clobber an existing unrecognized password -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form6' action='formtest.js'> 6
+ <!-- fill in first field, 2nd field shouldn't be touched anyway. -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+
+<!-- with username fields -->
+
+
+
+<form id='form7' action='formtest.js'> 7
+ <!-- simple form, should fill in username and first pw -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form8' action='formtest.js'> 8
+ <!-- reverse pname and qname, field names are ignored. -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form9' action='formtest.js'> 9
+ <!-- username already filled, so just fill first password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form10' action='formtest.js'> 10
+ <!-- unknown username, don't fill in a password -->
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form11' action='formtest.js'> 11
+ <!-- don't clobber unknown password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form12' action='formtest.js'> 12
+ <!-- fill in 1st pass, don't clobber 2nd pass -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form13' action='formtest.js'> 13
+ <!-- nothing to do, user and pass prefilled. life is easy. -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value='testpass'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form14' action='formtest.js'> 14
+ <!-- shouldn't fill in username because 1st pw field is unknown. -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form15' action='formtest.js'> 15
+ <!-- textfield in the middle of pw fields should be ignored -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+<form id='form16' action='formtest.js'> 16
+ <!-- same, and don't clobber existing unknown password -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill **/
+
+function startTest() {
+ var f = 1;
+
+ // 1-6 no username
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "testpass", "", "");
+ checkForm(f++, "testpass", "");
+ checkForm(f++, "xxxxxxxx", "");
+ checkForm(f++, "testpass", "xxxxxxxx");
+
+ // 7-15 with username
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "xxxxxxxx", "", "");
+ checkForm(f++, "testuser", "xxxxxxxx", "");
+ checkForm(f++, "testuser", "testpass", "xxxxxxxx");
+ checkForm(f++, "testuser", "testpass", "");
+ checkForm(f++, "", "xxxxxxxx", "testpass");
+ checkForm(f++, "testpass", "", "");
+ checkForm(f++, "xxxxxxxx", "", "");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
new file mode 100644
index 0000000000..eba811cf94
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for form fill with 2 password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: form fill, 2 password fields
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, 2 password fields **/
+
+/*
+ * If a form has two password fields, other things may be going on....
+ *
+ * 1 - The user might be creating a new login (2nd field for typo checking)
+ * 2 - The user is changing a password (old and new password each have field)
+ *
+ * This test is for case #1.
+ */
+
+var numSubmittedForms = 0;
+var numStartingLogins = 0;
+
+function startTest() {
+ // Check for unfilled forms
+ is($_(1, "uname").value, "", "Checking username 1");
+ is($_(1, "pword").value, "", "Checking password 1A");
+ is($_(1, "qword").value, "", "Checking password 1B");
+
+ // Fill in the username and password fields, for account creation.
+ // Form 1
+ $_(1, "uname").value = "newuser1";
+ $_(1, "pword").value = "newpass1";
+ $_(1, "qword").value = "newpass1";
+
+ var button = getFormSubmitButton(1);
+
+ todo(false, "form submission disabled, can't auto-accept dialog yet");
+ SimpleTest.finish();
+}
+
+
+// Called by each form's onsubmit handler.
+function checkSubmit(formNum) {
+ numSubmittedForms++;
+
+ // End the test at the last form.
+ if (formNum == 999) {
+ is(numSubmittedForms, 999, "Ensuring all forms submitted for testing.");
+
+ var numEndingLogins = LoginManager.countLogins("", "", "");
+
+ ok(numEndingLogins > 0, "counting logins at end");
+ is(numStartingLogins, numEndingLogins + 222, "counting logins at end");
+
+ SimpleTest.finish();
+ return false; // return false to cancel current form submission
+ }
+
+ // submit the next form.
+ var button = getFormSubmitButton(formNum + 1);
+ button.click();
+
+ return false; // return false to cancel current form submission
+}
+
+
+function getFormSubmitButton(formNum) {
+ var form = $("form" + formNum); // by id, not name
+ ok(form != null, "getting form " + formNum);
+
+ // we can't just call form.submit(), because that doesn't seem to
+ // invoke the form onsubmit handler.
+ var button = form.firstChild;
+ while (button && button.type != "submit") { button = button.nextSibling; }
+ ok(button != null, "getting form submit button");
+
+ return button;
+}
+
+runChecksAfterCommonInit(startTest);
+
+</script>
+</pre>
+<div id="content" style="display: none">
+ <form id="form1" onsubmit="return checkSubmit(1)" action="http://newuser.com">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html
new file mode 100644
index 0000000000..30b5a319f6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 3 password fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 3 password fields (form filling)
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <p>The next three forms are <b>user/pass/passB/passC</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form2" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword" value="testpass">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+
+ <p>The next three forms are <b>user/passB/pass/passC</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="qword">
+ <input type="password" name="pword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="pword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="pword" value="testpass">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <p>The next three forms are <b>user/passB/passC/pass</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form8" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form9" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword" value="testpass">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, 3 password fields **/
+
+// Test to make sure 3-password forms are filled properly.
+
+function startTest() {
+ // Check form 1
+ is($_(1, "uname").value, "testuser", "Checking username 1");
+ is($_(1, "pword").value, "testpass", "Checking password 1");
+ is($_(1, "qword").value, "", "Checking password 1 (q)");
+ is($_(1, "rword").value, "", "Checking password 1 (r)");
+ // Check form 2
+ is($_(2, "uname").value, "testuser", "Checking username 2");
+ is($_(2, "pword").value, "testpass", "Checking password 2");
+ is($_(2, "qword").value, "", "Checking password 2 (q)");
+ is($_(2, "rword").value, "", "Checking password 2 (r)");
+ // Check form 3
+ is($_(3, "uname").value, "testuser", "Checking username 3");
+ is($_(3, "pword").value, "testpass", "Checking password 3");
+ is($_(3, "qword").value, "", "Checking password 3 (q)");
+ is($_(3, "rword").value, "", "Checking password 3 (r)");
+
+ // Check form 4
+ is($_(4, "uname").value, "testuser", "Checking username 4");
+ todo_is($_(4, "qword").value, "", "Checking password 4 (q)");
+ todo_is($_(4, "pword").value, "testpass", "Checking password 4");
+ is($_(4, "rword").value, "", "Checking password 4 (r)");
+ // Check form 5
+ is($_(5, "uname").value, "testuser", "Checking username 5");
+ todo_is($_(5, "qword").value, "", "Checking password 5 (q)");
+ todo_is($_(5, "pword").value, "testpass", "Checking password 5");
+ is($_(5, "rword").value, "", "Checking password 5 (r)");
+ // Check form 6
+ is($_(6, "uname").value, "testuser", "Checking username 6");
+ todo_is($_(6, "qword").value, "", "Checking password 6 (q)");
+ is($_(6, "pword").value, "testpass", "Checking password 6");
+ is($_(6, "rword").value, "", "Checking password 6 (r)");
+
+ // Check form 7
+ is($_(7, "uname").value, "testuser", "Checking username 7");
+ todo_is($_(7, "qword").value, "", "Checking password 7 (q)");
+ is($_(7, "rword").value, "", "Checking password 7 (r)");
+ todo_is($_(7, "pword").value, "testpass", "Checking password 7");
+ // Check form 8
+ is($_(8, "uname").value, "testuser", "Checking username 8");
+ todo_is($_(8, "qword").value, "", "Checking password 8 (q)");
+ is($_(8, "rword").value, "", "Checking password 8 (r)");
+ todo_is($_(8, "pword").value, "testpass", "Checking password 8");
+ // Check form 9
+ is($_(9, "uname").value, "testuser", "Checking username 9");
+ todo_is($_(9, "qword").value, "", "Checking password 9 (q)");
+ is($_(9, "rword").value, "", "Checking password 9 (r)");
+ is($_(9, "pword").value, "testpass", "Checking password 9");
+
+ // TODO: as with the 2-password cases, add tests to check for creating new
+ // logins and changing passwords.
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
new file mode 100644
index 0000000000..0eee8e696b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -0,0 +1,859 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: multiple login autocomplete
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "user0pass", "", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword");
+
+ var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword");
+
+ var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword");
+
+ // login 5 only used in the single-user forms
+ var login5 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete2", null,
+ "singleuser5", "singlepass5", "uname", "pword");
+
+ var login6A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user1", "form7pass1", "uname", "pword");
+ var login6B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user2", "form7pass2", "uname", "pword");
+
+ var login7 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete4", null,
+ "form8user", "form8pass", "uname", "pword");
+
+ var login8A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAB", "form9pass", "uname", "pword");
+ var login8B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAAB", "form9pass", "uname", "pword");
+ var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAABzz", "form9pass", "uname", "pword");
+
+ var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
+ "testuser10", "testpass10", "uname", "pword");
+
+
+ // try/catch in case someone runs the tests manually, twice.
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ Services.logins.addLogin(login4);
+ Services.logins.addLogin(login5);
+ Services.logins.addLogin(login6A);
+ Services.logins.addLogin(login6B);
+ Services.logins.addLogin(login7);
+ Services.logins.addLogin(login8A);
+ Services.logins.addLogin(login8B);
+ // login8C is added later
+ Services.logins.addLogin(login10);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+
+ addMessageListener("addLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+ Services.logins.addLogin(login);
+ });
+ addMessageListener("removeLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+ Services.logins.removeLogin(login);
+ });
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- other forms test single logins, with autocomplete=off set -->
+ <form id="form2" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://autocomplete2" onsubmit="return false;" autocomplete="off">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form5" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- control -->
+ <form id="form6" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- This form will be manipulated to insert a different username field. -->
+ <form id="form7" action="http://autocomplete3" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for no autofill after onblur with blank username -->
+ <form id="form8" action="http://autocomplete4" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test autocomplete dropdown -->
+ <form id="form9" action="http://autocomplete5" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for onUsernameInput recipe testing -->
+ <form id="form11" action="http://autocomplete7" onsubmit="return false;">
+ <input type="text" name="1">
+ <input type="text" name="2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- tests <form>-less autocomplete -->
+ <div id="form12">
+ <input type="text" name="uname" id="uname">
+ <input type="password" name="pword" id="pword">
+ <button type="submit">Submit</button>
+ </div>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", false],
+ ["signon.autofillForms.http", true]]});
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form1_menuitems() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACForm("", "");
+});
+
+add_task(function* test_form1_first_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ doKey("down"); // first
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_second_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_third_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser3", "testpass3");
+});
+
+add_task(function* test_form1_fourth_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_first_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ yield spinEventLoop(); // let focus happen
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("down"); // deselects
+ doKey("down"); // first
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_wraparound_up_last_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("up"); // last (fourth)
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_down_up_up() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // select first entry
+ doKey("up"); // selects nothing!
+ doKey("up"); // select last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_up_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down");
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("up");
+ doKey("up");
+ doKey("up"); // first entry
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_fill_username_without_autofill_right() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // first
+ doKey("right");
+ yield spinEventLoop();
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_fill_username_without_autofill_left() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // first
+ doKey("left");
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_pageup_first() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry (page up)
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("page_up"); // first
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_pagedown_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ /* test 13 */
+ // Check last entry (page down)
+ doKey("down"); // first
+ doKey("page_down"); // last
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_untrusted_event() {
+ restoreForm();
+ yield spinEventLoop();
+
+ // Send a fake (untrusted) event.
+ checkACForm("", "");
+ uname.value = "zzzuser4";
+ sendFakeAutocompleteEvent(uname);
+ yield spinEventLoop();
+ checkACForm("zzzuser4", "");
+});
+
+add_task(function* test_form1_delete() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // XXX tried sending character "t" before/during dropdown to test
+ // filtering, but had no luck. Seemed like the character was getting lost.
+ // Setting uname.value didn't seem to work either. This works with a human
+ // driver, so I'm not sure what's up.
+
+ // Delete the first entry (of 4), "tempuser1"
+ doKey("down");
+ var numLogins;
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 5, "Correct number of logins before deleting one");
+
+ let countChangedPromise = notifyMenuChanged(3);
+ var deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 4, "Correct number of logins after deleting one");
+ yield countChangedPromise;
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_deletion() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check the new first entry (of 3)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_second() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Delete the second entry (of 3), "testuser3"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 3, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_first_after_deletion2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check the new first entry (of 2)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ /* test 54 */
+ // Delete the last entry (of 2), "zzzuser4"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 2, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_3_deletions() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check the only remaining entry
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_check_only_entry_remaining() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ /* test 56 */
+ // Delete the only remaining entry, "testuser2"
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 1, "Correct number of logins after deleting one");
+
+ // remove the login that's not shown in the list.
+ setupScript.sendSyncMessage("removeLogin", "login0");
+});
+
+/* Tests for single-user forms for ignoring autocomplete=off */
+add_task(function* test_form2() {
+ // Turn our attention to form2
+ uname = $_(2, "uname");
+ pword = $_(2, "pword");
+ checkACForm("singleuser5", "singlepass5");
+
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form3() {
+ uname = $_(3, "uname");
+ pword = $_(3, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form4() {
+ uname = $_(4, "uname");
+ pword = $_(4, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form5() {
+ uname = $_(5, "uname");
+ pword = $_(5, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6() {
+ // (this is a control, w/o autocomplete=off, to ensure the login
+ // that was being suppressed would have been filled in otherwise)
+ uname = $_(6, "uname");
+ pword = $_(6, "pword");
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6_changeUsername() {
+ // Test that the password field remains filled in after changing
+ // the username.
+ uname.focus();
+ doKey("right");
+ sendChar("X");
+ // Trigger the 'blur' event on uname
+ pword.focus();
+ yield spinEventLoop();
+ checkACForm("singleuser5X", "singlepass5");
+
+ setupScript.sendSyncMessage("removeLogin", "login5");
+});
+
+add_task(function* test_form7() {
+ uname = $_(7, "uname");
+ pword = $_(7, "pword");
+ checkACForm("", "");
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ var newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ pword.parentNode.insertBefore(newField, pword);
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+
+ // Delete login6B. It was created just to prevent filling in a login
+ // automatically, removing it makes it more likely that we'll catch a
+ // future regression with form filling here.
+ setupScript.sendSyncMessage("removeLogin", "login6B");
+});
+
+add_task(function* test_form7_2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ // The form changes, so we expect the old username field to get the
+ // selected autocomplete value, but neither the new username field nor
+ // the password field should have any values filled in.
+ yield spinEventLoop();
+ checkACForm("form7user1", "");
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+ restoreForm(); // clear field, so reloading test doesn't fail
+
+ setupScript.sendSyncMessage("removeLogin", "login6A");
+});
+
+add_task(function* test_form8() {
+ uname = $_(8, "uname");
+ pword = $_(8, "pword");
+ checkACForm("form8user", "form8pass");
+ restoreForm();
+});
+
+add_task(function* test_form8_blur() {
+ checkACForm("", "");
+ // Focus the previous form to trigger a blur.
+ $_(7, "uname").focus();
+});
+
+add_task(function* test_form8_2() {
+ checkACForm("", "");
+ restoreForm();
+});
+
+add_task(function* test_form8_3() {
+ checkACForm("", "");
+ setupScript.sendSyncMessage("removeLogin", "login7");
+});
+
+add_task(function* test_form9_filtering() {
+ // Turn our attention to form9 to test the dropdown - bug 497541
+ uname = $_(9, "uname");
+ pword = $_(9, "pword");
+ uname.focus();
+ let shownPromise = promiseACShown();
+ sendString("form9userAB");
+ yield shownPromise;
+
+ checkACForm("form9userAB", "");
+ uname.focus();
+ doKey("left");
+ shownPromise = promiseACShown();
+ sendChar("A");
+ let results = yield shownPromise;
+
+ checkACForm("form9userAAB", "");
+ checkArrayValues(results, ["form9userAAB"], "Check dropdown is updated after inserting 'A'");
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("form9userAAB", "form9pass");
+});
+
+add_task(function* test_form9_autocomplete_cache() {
+ // Note that this addLogin call will only be seen by the autocomplete
+ // attempt for the sendChar if we do not successfully cache the
+ // autocomplete results.
+ setupScript.sendSyncMessage("addLogin", "login8C");
+ uname.focus();
+ let promise0 = notifyMenuChanged(0);
+ sendChar("z");
+ yield promise0;
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup shouldn't open");
+
+ // check that empty results are cached - bug 496466
+ promise0 = notifyMenuChanged(0);
+ sendChar("z");
+ yield promise0;
+ popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup stays closed due to cached empty result");
+});
+
+add_task(function* test_form11_recipes() {
+ yield loadRecipes({
+ siteRecipes: [{
+ "hosts": ["mochi.test:8888"],
+ "usernameSelector": "input[name='1']",
+ "passwordSelector": "input[name='2']"
+ }],
+ });
+ uname = $_(11, "1");
+ pword = $_(11, "2");
+
+ // First test DOMAutocomplete
+ // Switch the password field to type=password so _fillForm marks the username
+ // field for autocomplete.
+ pword.type = "password";
+ yield promiseFormsProcessed();
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+
+ // Now test recipes with blur on the username field.
+ restoreForm();
+ checkACForm("", "");
+ uname.value = "testuser10";
+ checkACForm("testuser10", "");
+ doKey("tab");
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+ yield resetRecipes();
+});
+
+add_task(function* test_form12_formless() {
+ // Test form-less autocomplete
+ uname = $_(12, "uname");
+ pword = $_(12, "pword");
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Trigger autocomplete
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ let processedPromise = promiseFormsProcessed();
+ doKey("return"); // not "enter"!
+ yield processedPromise;
+ checkACForm("testuser", "testpass");
+});
+
+add_task(function* test_form12_open_on_trusted_focus() {
+ uname = $_(12, "uname");
+ pword = $_(12, "pword");
+ uname.value = "";
+ pword.value = "";
+
+ // Move focus to the password field so we can test the first click on the
+ // username field.
+ pword.focus();
+ checkACForm("", "");
+ const firePrivEventPromise = new Promise((resolve) => {
+ uname.addEventListener("click", (e) => {
+ ok(e.isTrusted, "Ensure event is trusted");
+ resolve();
+ });
+ });
+ const shownPromise = promiseACShown();
+ synthesizeMouseAtCenter(uname, {});
+ yield firePrivEventPromise;
+ yield shownPromise;
+ doKey("down");
+ const processedPromise = promiseFormsProcessed();
+ doKey("return"); // not "enter"!
+ yield processedPromise;
+ checkACForm("testuser", "testpass");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
new file mode 100644
index 0000000000..40e322afd7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for html5 input types (email, tel, url, etc.)</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: html5 input types (email, tel, url, etc.)
+<script>
+runChecksAfterCommonInit(() => startTest());
+
+runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login3 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", "http://bug600551-1", null,
+ "testuser@example.com", "testpass1", "", "");
+ login2.init("http://mochi.test:8888", "http://bug600551-2", null,
+ "555-555-5555", "testpass2", "", "");
+ login3.init("http://mochi.test:8888", "http://bug600551-3", null,
+ "http://mozilla.org", "testpass3", "", "");
+ login4.init("http://mochi.test:8888", "http://bug600551-4", null,
+ "123456789", "testpass4", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2);
+ pwmgr.addLogin(login3);
+ pwmgr.addLogin(login4);
+});
+</script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <form id="form1" action="http://bug600551-1">
+ <input type="email" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="http://bug600551-2">
+ <input type="tel" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://bug600551-3">
+ <input type="url" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://bug600551-4">
+ <input type="number" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- The following forms should not be filled with usernames -->
+ <form id="form5" action="formtest.js">
+ <input type="search" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form6" action="formtest.js">
+ <input type="datetime" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form7" action="formtest.js">
+ <input type="date" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form8" action="formtest.js">
+ <input type="month" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form9" action="formtest.js">
+ <input type="week" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form10" action="formtest.js">
+ <input type="time" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form11" action="formtest.js">
+ <input type="datetime-local" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form12" action="formtest.js">
+ <input type="range" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form13" action="formtest.js">
+ <input type="color" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 600551
+ (Password manager not working with input type=email)
+ */
+function startTest() {
+ checkForm(1, "testuser@example.com", "testpass1");
+ checkForm(2, "555-555-5555", "testpass2");
+ checkForm(3, "http://mozilla.org", "testpass3");
+ checkForm(4, "123456789", "testpass4");
+
+ is($_(5, "uname").value, "", "type=search should not be considered a username");
+
+ is($_(6, "uname").value, "", "type=datetime should not be considered a username");
+
+ is($_(7, "uname").value, "", "type=date should not be considered a username");
+
+ is($_(8, "uname").value, "", "type=month should not be considered a username");
+
+ is($_(9, "uname").value, "", "type=week should not be considered a username");
+
+ is($_(10, "uname").value, "", "type=time should not be considered a username");
+
+ is($_(11, "uname").value, "", "type=datetime-local should not be considered a username");
+
+ is($_(12, "uname").value, "50", "type=range should not be considered a username");
+
+ is($_(13, "uname").value, "#000000", "type=color should not be considered a username");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html
new file mode 100644
index 0000000000..e0a2883c82
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=355063
+-->
+<head>
+ <meta charset="utf-8"/>
+ <title>Test for Bug 355063</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 355063 **/
+
+ runChecksAfterCommonInit(function startTest() {
+ info("startTest");
+ // Password Manager's own listener should always have been added first, so
+ // the test's listener should be called after the pwmgr's listener fills in
+ // a login.
+ //
+ SpecialPowers.addChromeEventListener("DOMFormHasPassword", function eventFired() {
+ SpecialPowers.removeChromeEventListener("DOMFormHasPassword", eventFired);
+ var passField = $("p1");
+ passField.addEventListener("input", checkForm);
+ });
+ addForm();
+ });
+
+ function addForm() {
+ info("addForm");
+ var c = document.getElementById("content");
+ c.innerHTML = "<form id=form1>form1: <input id=u1><input type=password id=p1></form><br>";
+ }
+
+ function checkForm() {
+ info("checkForm");
+ var userField = document.getElementById("u1");
+ var passField = document.getElementById("p1");
+ is(userField.value, "testuser", "checking filled username");
+ is(passField.value, "testpass", "checking filled password");
+
+ SimpleTest.finish();
+ }
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=355063">Mozilla Bug 355063</a>
+<p id="display"></p>
+<div id="content">
+forms go here!
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
new file mode 100644
index 0000000000..40fec8c462
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
@@ -0,0 +1,213 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms and logins without a username</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms and logins without a username.
+<script>
+runChecksAfterCommonInit(() => startTest());
+runInParent(() => {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ var pwmgr = Cc["@mozilla.org/login-manager;1"]
+ .getService(Ci.nsILoginManager);
+
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
+
+ // pwlogin1 uses a unique formSubmitURL, to check forms where no other logins
+ // will apply. pwlogin2 uses the normal formSubmitURL, so that we can test
+ // forms with a mix of username and non-username logins that might apply.
+ //
+ // Note: pwlogin2 is deleted at the end of the test.
+
+ pwlogin1 = new nsLoginInfo();
+ pwlogin2 = new nsLoginInfo();
+
+ pwlogin1.init("http://mochi.test:8888", "http://mochi.test:1111", null,
+ "", "1234", "uname", "pword");
+
+ pwlogin2.init("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "", "1234", "uname", "pword");
+
+
+ pwmgr.addLogin(pwlogin1);
+ pwmgr.addLogin(pwlogin2);
+});
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+<!-- simple form: no username field, 1 password field -->
+<form id='form1' action='http://mochi.test:1111/formtest.js'> 1
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- simple form: no username field, 2 password fields -->
+<form id='form2' action='http://mochi.test:1111/formtest.js'> 2
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- simple form: no username field, 3 password fields -->
+<form id='form3' action='http://mochi.test:1111/formtest.js'> 3
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 4 password fields, should be ignored. -->
+<form id='form4' action='http://mochi.test:1111/formtest.js'> 4
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+ <input type='password' name='pname4' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+<!-- 1 username field -->
+<form id='form5' action='http://mochi.test:1111/formtest.js'> 5
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+<!-- 1 username field, with a value set -->
+<form id='form6' action='http://mochi.test:1111/formtest.js'> 6
+ <input type='text' name='uname' value='someuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+<!--
+(The following forms have 2 potentially-matching logins, on is
+password-only, the other is username+password)
+-->
+
+
+
+<!-- 1 username field, with value set. Fill in the matching U+P login -->
+<form id='form7' action='formtest.js'> 7
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, with value set. Don't fill in U+P login-->
+<form id='form8' action='formtest.js'> 8
+ <input type='text' name='uname' value='someuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+<!-- 1 username field, too small for U+P login -->
+<form id='form9' action='formtest.js'> 9
+ <input type='text' name='uname' value='' maxlength="4">
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for U+P login -->
+<form id='form10' action='formtest.js'> 10
+ <input type='text' name='uname' value='' maxlength="0">
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for U+P login -->
+<form id='form11' action='formtest.js'> 11
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="4">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for either login -->
+<form id='form12' action='formtest.js'> 12
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="1">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+<!-- 1 username field, too small for either login -->
+<form id='form13' action='formtest.js'> 13
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="0">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: password-only logins **/
+function startTest() {
+
+ checkForm(1, "1234");
+ checkForm(2, "1234", "");
+ checkForm(3, "1234", "", "");
+ checkUnmodifiedForm(4);
+
+ checkForm(5, "", "1234");
+ checkForm(6, "someuser", "");
+
+ checkForm(7, "testuser", "testpass");
+ checkForm(8, "someuser", "");
+
+ checkForm(9, "", "1234");
+ checkForm(10, "", "1234");
+ checkForm(11, "", "1234");
+
+ checkUnmodifiedForm(12);
+ checkUnmodifiedForm(13);
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
new file mode 100644
index 0000000000..ad4a41cdb2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test bug 627616 related to proxy authentication</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var Ci = SpecialPowers.Ci;
+
+ function makeXHR(expectedStatus, expectedText, extra) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "authenticate.sjs?" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm&" +
+ "user=user1name&" +
+ "pass=user1pass&" +
+ "realm=mochirealm&" +
+ extra || "");
+ xhr.onloadend = function() {
+ is(xhr.status, expectedStatus, "xhr.status");
+ is(xhr.statusText, expectedText, "xhr.statusText");
+ runNextTest();
+ };
+ return xhr;
+ }
+
+ function testNonAnonymousCredentials() {
+ var xhr = makeXHR(200, "OK");
+ xhr.send();
+ }
+
+ function testAnonymousCredentials() {
+ // Test that an anonymous request correctly performs proxy authentication
+ var xhr = makeXHR(401, "Authentication required");
+ SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
+ xhr.send();
+ }
+
+ function testAnonymousNoAuth() {
+ // Next, test that an anonymous request still does not include any non-proxy
+ // authentication headers.
+ var xhr = makeXHR(200, "Authorization header not found", "anonymous=1");
+ SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
+ xhr.send();
+ }
+
+ var gExpectedDialogs = 0;
+ var gCurrentTest;
+ function runNextTest() {
+ is(gExpectedDialogs, 0, "received expected number of auth dialogs");
+ mm.sendAsyncMessage("prepareForNextTest");
+ mm.addMessageListener("prepareForNextTestDone", function prepared(msg) {
+ mm.removeMessageListener("prepareForNextTestDone", prepared);
+ if (pendingTests.length > 0) {
+ ({expectedDialogs: gExpectedDialogs,
+ test: gCurrentTest} = pendingTests.shift());
+ gCurrentTest.call(this);
+ } else {
+ mm.sendAsyncMessage("cleanup");
+ mm.addMessageListener("cleanupDone", () => {
+ // mm.destroy() is called as a cleanup function by runInParent(), no
+ // need to do it here.
+ SimpleTest.finish();
+ });
+ }
+ });
+ }
+
+ var pendingTests = [{expectedDialogs: 2, test: testNonAnonymousCredentials},
+ {expectedDialogs: 1, test: testAnonymousCredentials},
+ {expectedDialogs: 0, test: testAnonymousNoAuth}];
+
+ let mm = runInParent(() => {
+ const { classes: parentCc, interfaces: parentCi, utils: parentCu } = Components;
+
+ parentCu.import("resource://gre/modules/Services.jsm");
+ parentCu.import("resource://gre/modules/NetUtil.jsm");
+ parentCu.import("resource://gre/modules/Timer.jsm");
+ parentCu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ let channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true
+ });
+
+ let pps = parentCc["@mozilla.org/network/protocol-proxy-service;1"].
+ getService(parentCi.nsIProtocolProxyService);
+ pps.asyncResolve(channel, 0, {
+ onProxyAvailable(req, uri, pi, status) {
+ let mozproxy = "moz-proxy://" + pi.host + ":" + pi.port;
+ let login = parentCc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(parentCi.nsILoginInfo);
+ login.init(mozproxy, null, "proxy_realm", "proxy_user", "proxy_pass",
+ "", "");
+ Services.logins.addLogin(login);
+
+ let login2 = parentCc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(parentCi.nsILoginInfo);
+ login2.init("http://mochi.test:8888", null, "mochirealm", "user1name",
+ "user1pass", "", "");
+ Services.logins.addLogin(login2);
+
+ sendAsyncMessage("setupDone");
+ },
+ QueryInterface: XPCOMUtils.generateQI([parentCi.nsIProtocolProxyCallback]),
+ });
+
+ addMessageListener("prepareForNextTest", message => {
+ parentCc["@mozilla.org/network/http-auth-manager;1"].
+ getService(parentCi.nsIHttpAuthManager).
+ clearAll();
+ sendAsyncMessage("prepareForNextTestDone");
+ });
+
+ let dialogObserverTopic = "common-dialog-loaded";
+
+ function dialogObserver(subj, topic, data) {
+ subj.Dialog.ui.prompt.document.documentElement.acceptDialog();
+ sendAsyncMessage("promptAccepted");
+ }
+
+ Services.obs.addObserver(dialogObserver, dialogObserverTopic, false);
+
+ addMessageListener("cleanup", message => {
+ Services.obs.removeObserver(dialogObserver, dialogObserverTopic);
+ sendAsyncMessage("cleanupDone");
+ });
+ });
+
+ mm.addMessageListener("promptAccepted", msg => {
+ gExpectedDialogs--;
+ });
+ mm.addMessageListener("setupDone", msg => {
+ runNextTest();
+ });
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
new file mode 100644
index 0000000000..4ad08bee26
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=776171
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 776171 related to HTTP auth</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="startTest()">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test checks we correctly ignore authentication entry
+ * for a subpath and use creds from the URL when provided when XHR
+ * is used with filled user name and password.
+ *
+ * 1. connect auth2/authenticate.sjs that expects user1:pass1 password
+ * 2. connect a dummy URL at the same path
+ * 3. connect authenticate.sjs that again expects user1:pass1 password
+ * in this case, however, we have an entry without an identity
+ * for this path (that is a parent for auth2 path in the first step)
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function doxhr(URL, user, pass, next) {
+ var xhr = new XMLHttpRequest();
+ if (user && pass)
+ xhr.open("POST", URL, true, user, pass);
+ else
+ xhr.open("POST", URL, true);
+ xhr.onload = function() {
+ is(xhr.status, 200, "Got status 200");
+ next();
+ };
+ xhr.onerror = function() {
+ ok(false, "request passed");
+ finishTest();
+ };
+ xhr.send();
+}
+
+function startTest() {
+ doxhr("auth2/authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", function() {
+ doxhr("auth2", null, null, function() {
+ doxhr("authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", SimpleTest.finish);
+ });
+ });
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
new file mode 100644
index 0000000000..316f59da76
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete due to multiple matching logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: autocomplete due to multiple matching logins
+
+<script>
+runChecksAfterCommonInit(false);
+
+SpecialPowers.loadChromeScript(function addLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "name", "pass", "uname", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "Name", "Pass", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "USER", "PASS", "uname", "pword");
+
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: autocomplete due to multiple matching logins **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username");
+ is(pword.value, expectedPassword, "Checking " + formID + " password");
+}
+
+add_task(function* test_empty_first_entry() {
+ /* test 1 */
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ // Trigger autocomplete popup
+ restoreForm();
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ let shownPromise = promiseACShown();
+ doKey("down");
+ let results = yield shownPromise;
+ popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkArrayValues(results, ["name", "Name", "USER"], "initial");
+
+ // Check first entry
+ let index0Promise = notifySelectedIndex(0);
+ doKey("down");
+ yield index0Promise;
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("name", "pass");
+});
+
+add_task(function* test_empty_second_entry() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("Name", "Pass");
+});
+
+add_task(function* test_empty_third_entry() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("USER", "PASS");
+});
+
+add_task(function* test_preserve_matching_username_case() {
+ restoreForm();
+ uname.value = "user";
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check that we don't clobber user-entered text when tabbing away
+ // (even with no autocomplete entry selected)
+ doKey("tab");
+ yield promiseFormsProcessed();
+ checkACForm("user", "PASS");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html
new file mode 100644
index 0000000000..430081b3ad
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for considering form action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 360493
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <!-- normal form with normal relative action. -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL -->
+ <form id="form2" action="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL, and change the path -->
+ <form id="form3" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL, and change the path and filename -->
+ <form id="form4" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/not_a_test.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- specify the action URL relative to the current document-->
+ <form id="form5" action="./formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- specify the action URL relative to the current server -->
+ <form id="form6" action="/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change the method from get to post -->
+ <form id="form7" action="formtest.js" method="POST">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Blank action URL specified -->
+ <form id="form8" action="">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- |action| attribute entirely missing -->
+ <form id="form9" >
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- action url as javascript -->
+ <form id="form10" action="javascript:alert('this form is not submitted so this alert should not be invoked');">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- TODO: action=IP.ADDRESS instead of HOSTNAME? -->
+ <!-- TODO: test with |base href="http://othersite//"| ? -->
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 360493 (Cross-Site Forms + Password
+ Manager = Security Failure) **/
+
+// This test is designed to make sure variations on the form's |action|
+// and |method| continue to work with the fix for 360493.
+
+function startTest() {
+ for (var i = 1; i <= 9; i++) {
+ // Check form i
+ is($_(i, "uname").value, "testuser", "Checking for filled username " + i);
+ is($_(i, "pword").value, "testpass", "Checking for filled password " + i);
+ }
+
+ // The login's formSubmitURL isn't "javascript:", so don't fill it in.
+ isnot($_(10, "uname"), "testuser", "Checking username w/ JS action URL");
+ isnot($_(10, "pword"), "testpass", "Checking password w/ JS action URL");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html
new file mode 100644
index 0000000000..0f0056de01
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for considering form action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 360493
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <!-- The tests in this page exercise things that shouldn't work. -->
+
+ <!-- Change port # of action URL from 8888 to 7777 -->
+ <form id="form1" action="http://localhost:7777/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- No port # in action URL -->
+ <form id="form2" action="http://localhost/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change protocol from http:// to ftp://, include the expected 8888 port # -->
+ <form id="form3" action="ftp://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change protocol from http:// to ftp://, no port # specified -->
+ <form id="form4" action="ftp://localhost/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. -->
+ <form id="form5" action="about:blank">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. (If the normal embedded action URL doesn't work, that should mean other URLs won't either) -->
+ <form id="form6" action="view-source:http://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. -->
+ <form id="form7" action="view-source:formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Action URL points to a different host (this is the archetypical exploit) -->
+ <form id="form8" action="http://www.cnn.com/">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Action URL points to a different host, user field prefilled -->
+ <form id="form9" action="http://www.cnn.com/">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try wrapping a evil form around a good form, to see if we can confuse the parser. -->
+ <form id="form10-A" action="http://www.cnn.com/">
+ <form id="form10-B" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit (inner)</button>
+ <button type="reset"> Reset (inner)</button>
+ </form>
+ <button type="submit" id="neutered_submit10">Submit (outer)</button>
+ <button type="reset">Reset (outer)</button>
+ </form>
+
+ <!-- Try wrapping a good form around an evil form, to see if we can confuse the parser. -->
+ <form id="form11-A" action="formtest.js">
+ <form id="form11-B" action="http://www.cnn.com/">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit (inner)</button>
+ <button type="reset"> Reset (inner)</button>
+ </form>
+ <button type="submit" id="neutered_submit11">Submit (outer)</button>
+ <button type="reset">Reset (outer)</button>
+ </form>
+
+<!-- TODO: probably should have some accounts which have no port # in the action url. JS too. And different host/proto. -->
+<!-- TODO: www.site.com vs. site.com? -->
+<!-- TODO: foo.site.com vs. bar.site.com? -->
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 360493 (Cross-Site Forms + Password Manager = Security Failure) **/
+
+function startTest() {
+ for (var i = 1; i <= 8; i++) {
+ // Check form i
+ is($_(i, "uname").value, "", "Checking for unfilled username " + i);
+ is($_(i, "pword").value, "", "Checking for unfilled password " + i);
+ }
+
+ is($_(9, "uname").value, "testuser", "Checking for unmodified username 9");
+ is($_(9, "pword").value, "", "Checking for unfilled password 9");
+
+ is($_("10-A", "uname").value, "", "Checking for unfilled username 10A");
+ is($_("10-A", "pword").value, "", "Checking for unfilled password 10A");
+
+ // The DOM indicates this form could be filled, as the evil inner form
+ // is discarded. And yet pwmgr seems not to fill it. Not sure why.
+ todo(false, "Mangled form combo not being filled when maybe it could be?");
+ is($_("11-A", "uname").value, "testuser", "Checking filled username 11A");
+ is($_("11-A", "pword").value, "testpass", "Checking filled password 11A");
+
+ // Verify this by making sure there are no extra forms in the document, and
+ // that the submit button for the neutered forms don't do anything.
+ // If the test finds extra forms the submit() causes the test to timeout, then
+ // there may be a security issue.
+ is(document.forms.length, 11, "Checking for unexpected forms");
+ $("neutered_submit10").click();
+ $("neutered_submit11").click();
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html
new file mode 100644
index 0000000000..d37e92c40c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with a JS submit action</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: form with JS submit action
+<script>
+runChecksAfterCommonInit(() => startTest());
+
+runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let jslogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ jslogin.init("http://mochi.test:8888", "javascript:", null,
+ "jsuser", "jspass123", "uname", "pword");
+ Services.logins.addLogin(jslogin);
+});
+
+/** Test for Login Manager: JS action URL **/
+
+function startTest() {
+ checkForm(1, "jsuser", "jspass123");
+
+ SimpleTest.finish();
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+<form id='form1' action='javascript:alert("never shows")'> 1
+ <input name="uname">
+ <input name="pword" type="password">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+</form>
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
new file mode 100644
index 0000000000..6263c818da
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofilling of fields outside of a form</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ // Tell the parent to setup test logins.
+ chromeScript.sendAsyncMessage("setupParent", { selfFilling: true });
+ });
+});
+
+let doneSetupPromise = new Promise(resolve => {
+ // When the setup is done, load a recipe for this test.
+ chromeScript.addMessageListener("doneSetup", function doneSetup() {
+ resolve();
+ });
+});
+
+add_task(function* setup() {
+ info("Waiting for loads and setup");
+ yield doneSetupPromise;
+
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+
+const DEFAULT_ORIGIN = "http://mochi.test:8888";
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password>`,
+
+ // Expected outputs
+ expectedInputValues: ["testpass"],
+ },
+ {
+ document: `<input>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass"],
+ },
+ {
+ document: `<input>
+ <input type=password>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass", ""],
+ },
+ {
+ document: `<input>
+ <input type=password>
+ <input type=password>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass", "", ""],
+ },
+ {
+ document: `<input>
+ <input type=password form="form1">
+ <input type=password>
+ <form id="form1">
+ <input>
+ <input type=password>
+ </form>`,
+ expectedFormCount: 2,
+ expectedInputValues: ["testuser", "testpass", "testpass", "", ""],
+ },
+ {
+ document: `<!-- formless password field selector recipe test -->
+ <input>
+ <input type=password>
+ <input>
+ <input type=password name="recipepword">`,
+ expectedInputValues: ["", "", "testuser", "testpass"],
+ },
+ {
+ document: `<!-- formless username and password field selector recipe test -->
+ <input name="recipeuname">
+ <input>
+ <input type=password>
+ <input type=password name="recipepword">`,
+ expectedInputValues: ["testuser", "", "", "testpass"],
+ },
+ {
+ document: `<!-- form and formless recipe field selector test -->
+ <input name="recipeuname">
+ <input>
+ <input type=password form="form1"> <!-- not filled since recipe affects both FormLikes -->
+ <input type=password>
+ <input type=password name="recipepword">
+ <form id="form1">
+ <input>
+ <input type=password>
+ </form>`,
+ expectedFormCount: 2,
+ expectedInputValues: ["testuser", "", "", "", "testpass", "", ""],
+ },
+];
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+ let frameDoc = loginFrame.contentWindow.document;
+
+ for (let tc of TESTCASES) {
+ info("Starting testcase: " + JSON.stringify(tc));
+
+ let numFormLikesExpected = tc.expectedFormCount || 1;
+
+ let processedFormPromise = promiseFormsProcessed(numFormLikesExpected);
+
+ frameDoc.documentElement.innerHTML = tc.document;
+ info("waiting for " + numFormLikesExpected + " processed form(s)");
+ yield processedFormPromise;
+
+ let testInputs = frameDoc.documentElement.querySelectorAll("input");
+ is(testInputs.length, tc.expectedInputValues.length, "Check number of inputs");
+ for (let i = 0; i < tc.expectedInputValues.length; i++) {
+ let expectedValue = tc.expectedInputValues[i];
+ is(testInputs[i].value, expectedValue,
+ "Check expected input value " + i + ": " + expectedValue);
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
new file mode 100644
index 0000000000..468da1e7f5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields outside of a form</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+add_task(function* setup() {
+ info("Waiting for page and frame loads");
+ yield loadPromise;
+
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://mochi.test:8888";
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="pass1">`,
+ inputIndexForFormLike: 0,
+
+ // Expected outputs similar to RemoteLogins:onFormSubmit
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">`,
+ inputIndexForFormLike: 0,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">`,
+ inputIndexForFormLike: 1,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">`,
+ inputIndexForFormLike: 2,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">
+ <input type=password value="pass2">`,
+ inputIndexForFormLike: 3,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="user2" form="form1">
+ <input type=password value="pass1">
+ <form id="form1">
+ <input value="user3">
+ <input type=password value="pass2">
+ </form>`,
+ inputIndexForFormLike: 2,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="username from recipe">
+ <input value="default field username">
+ <input type=password value="pass1">
+ <input name="recipepword" type=password value="pass2">`,
+ inputIndexForFormLike: 2,
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+];
+
+function getSubmitMessage() {
+ info("getSubmitMessage");
+ return new Promise((resolve, reject) => {
+ chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+ info("got formSubmissionProcessed");
+ chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+ resolve(...args);
+ });
+ });
+}
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+ let frameDoc = loginFrame.contentWindow.document;
+
+ for (let tc of TESTCASES) {
+ info("Starting testcase: " + JSON.stringify(tc));
+ frameDoc.documentElement.innerHTML = tc.document;
+ let inputForFormLike = frameDoc.querySelectorAll("input")[tc.inputIndexForFormLike];
+
+ let formLike = LoginFormFactory.createFromField(inputForFormLike);
+
+ info("Calling _onFormSubmit with FormLike");
+ let processedPromise = getSubmitMessage();
+ LoginManagerContent._onFormSubmit(formLike);
+
+ let submittedResult = yield processedPromise;
+
+ // Check data sent via RemoteLogins:onFormSubmit
+ is(submittedResult.hostname, tc.hostname, "Check hostname");
+ is(submittedResult.formSubmitURL, tc.formSubmitURL, "Check formSubmitURL");
+
+ if (tc.usernameFieldValue === null) {
+ is(submittedResult.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(submittedResult.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(submittedResult.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(submittedResult.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(submittedResult.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
new file mode 100644
index 0000000000..b07d0886c9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formlessCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ yield loadPromise;
+
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["test1.mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+};
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="pass1">`,
+
+ // Expected outputs similar to RemoteLogins:onFormSubmit
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="pass1">
+ <input type=password value="pass2">
+ <input type=password value="pass2">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input value="user1">
+ <input type=password value="user2" form="form1">
+ <input type=password value="pass1">
+ <form id="form1">
+ <input value="user3">
+ <input type=password value="pass2">
+ </form>`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="username from recipe">
+ <input value="default field username">
+ <input type=password value="pass1">
+ <input name="recipepword" type=password value="pass2">`,
+
+ hostname: DEFAULT_ORIGIN,
+ formSubmitURL: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+];
+
+function getSubmitMessage() {
+ info("getSubmitMessage");
+ return new Promise((resolve, reject) => {
+ chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+ info("got formSubmissionProcessed");
+ chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+ resolve(...args);
+ });
+ });
+}
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+
+ for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function frameLoaded() {
+ loginFrame.removeEventListener("load", frameLoaded);
+ resolve();
+ });
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ yield loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ frameDoc.documentElement.innerHTML = tc.document;
+ // Wait for the form to be processed before trying to submit.
+ yield promiseFormsProcessed();
+ let processedPromise = getSubmitMessage();
+ info("Running " + scriptName + " script to cause a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ let submittedResult = yield processedPromise;
+
+ // Check data sent via RemoteLogins:onFormSubmit
+ is(submittedResult.hostname, tc.hostname, "Check hostname");
+ is(submittedResult.formSubmitURL, tc.formSubmitURL, "Check formSubmitURL");
+
+ if (tc.usernameFieldValue === null) {
+ is(submittedResult.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(submittedResult.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(submittedResult.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(submittedResult.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(submittedResult.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
new file mode 100644
index 0000000000..4283f128c2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test no capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript;version=1.8">
+const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+
+SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+function submissionProcessed(...args) {
+ ok(false, "No formSubmissionProcessed should occur in this test");
+ info("got: " + JSON.stringify(args));
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formlessCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ yield loadPromise;
+
+ chromeScript.addMessageListener("formSubmissionProcessed", submissionProcessed);
+
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.removeMessageListener("formSubmissionProcessed", submissionProcessed);
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+ WINDOW_LOCATION_RELOAD: `window.location.reload();`,
+ HISTORY_BACK: `history.back();`,
+ HISTORY_GO_MINUS1: `history.go(-1);`,
+};
+const TESTCASES = [
+ // Begin test cases that shouldn't trigger capture.
+ {
+ // For now we don't trigger upon navigation if <form> is used.
+ document: `<form><input type=password value="pass1"></form>`,
+ },
+ {
+ // Empty password field
+ document: `<input type=password value="">`,
+ },
+ {
+ // Test with an input that would normally be captured but with SCRIPTS that
+ // shouldn't trigger capture.
+ document: `<input type=password value="pass2">`,
+ wouldCapture: true,
+ },
+];
+
+add_task(function* test() {
+ let loginFrame = document.getElementById("loginFrame");
+
+ for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) {
+ // Don't run scripts that should actually capture for this testcase.
+ continue;
+ }
+
+ info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function frameLoaded() {
+ loginFrame.removeEventListener("load", frameLoaded);
+ resolve();
+ });
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ yield loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ frameDoc.documentElement.innerHTML = tc.document;
+
+ // Wait for the form to be processed before trying to submit.
+ yield promiseFormsProcessed();
+
+ info("Running " + scriptName + " script to check for a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ // Wait for 5000ms to see if the promise above resolves.
+ yield new Promise(resolve => setTimeout(resolve, 5000));
+ ok(true, "Done waiting for captures");
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
new file mode 100644
index 0000000000..0e77956d87
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for input events in Login Manager</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onNewEvent(event)">
+Login Manager test: input events should fire.
+
+<script>
+runChecksAfterCommonInit();
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+/** Test for Login Manager: form fill, should get input events. **/
+
+var usernameInputFired = false;
+var passwordInputFired = false;
+var usernameChangeFired = false;
+var passwordChangeFired = false;
+var onloadFired = false;
+
+function onNewEvent(e) {
+ info("Got " + e.type + " event.");
+ if (e.type == "load") {
+ onloadFired = true;
+ } else if (e.type == "input") {
+ if (e.target.name == "uname") {
+ is(e.target.value, "testuser", "Should get 'testuser' as username");
+ ok(!usernameInputFired, "Should not have gotten an input event for the username field yet.");
+ usernameInputFired = true;
+ } else if (e.target.name == "pword") {
+ is(e.target.value, "testpass", "Should get 'testpass' as password");
+ ok(!passwordInputFired, "Should not have gotten an input event for the password field yet.");
+ passwordInputFired = true;
+ }
+ } else if (e.type == "change") {
+ if (e.target.name == "uname") {
+ is(e.target.value, "testuser", "Should get 'testuser' as username");
+ ok(usernameInputFired, "Should get input event before change event for username field.");
+ ok(!usernameChangeFired, "Should not have gotten a change event for the username field yet.");
+ usernameChangeFired = true;
+ } else if (e.target.name == "pword") {
+ is(e.target.value, "testpass", "Should get 'testpass' as password");
+ ok(passwordInputFired, "Should get input event before change event for password field.");
+ ok(!passwordChangeFired, "Should not have gotten a change event for the password field yet.");
+ passwordChangeFired = true;
+ }
+ }
+ if (onloadFired && usernameInputFired && passwordInputFired && usernameChangeFired && passwordChangeFired) {
+ ok(true, "All events fired as expected, we're done.");
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.registerCleanupFunction(function cleanup() {
+ clearTimeout(timeout);
+ $_(1, "uname").removeAttribute("oninput");
+ $_(1, "pword").removeAttribute("oninput");
+ $_(1, "uname").removeAttribute("onchange");
+ $_(1, "pword").removeAttribute("onchange");
+ document.body.removeAttribute("onload");
+});
+
+var timeout = setTimeout(function() {
+ ok(usernameInputFired, "Username input event should have fired by now.");
+ ok(passwordInputFired, "Password input event should have fired by now.");
+ ok(usernameChangeFired, "Username change event should have fired by now.");
+ ok(passwordChangeFired, "Password change event should have fired by now.");
+ ok(onloadFired, "Window load event should have fired by now.");
+ ok(false, "Not all events fired yet.");
+ SimpleTest.finish();
+}, 10000);
+
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname" oninput="onNewEvent(event)" onchange="onNewEvent(event)">
+ <input type="password" name="pword" oninput="onNewEvent(event)" onchange="onNewEvent(event)">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
new file mode 100644
index 0000000000..d058a87f98
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for input events in Login Manager when username/password are filled in already</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onNewEvent(event)">
+Login Manager test: input events should fire.
+
+<script>
+runChecksAfterCommonInit();
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+/** Test for Login Manager: form fill when form is already filled, should not get input events. **/
+
+var onloadFired = false;
+
+function onNewEvent(e) {
+ console.error("Got " + e.type + " event.");
+ if (e.type == "load") {
+ onloadFired = true;
+ $_(1, "uname").focus();
+ sendKey("Tab");
+ } else {
+ ok(false, "Got an input event for " + e.target.name + " field, which shouldn't happen.");
+ }
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname" oninput="onNewEvent(event)" value="testuser">
+ <input type="password" name="pword" oninput="onNewEvent(event)" onfocus="setTimeout(function() { SimpleTest.finish() }, 1000);" value="testpass">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
new file mode 100644
index 0000000000..c5d0a44fa8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
@@ -0,0 +1,861 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test insecure form field autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "user0pass", "", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword");
+
+ var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword");
+
+ var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword");
+
+ // login 5 only used in the single-user forms
+ var login5 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete2", null,
+ "singleuser5", "singlepass5", "uname", "pword");
+
+ var login6A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user1", "form7pass1", "uname", "pword");
+ var login6B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+ "form7user2", "form7pass2", "uname", "pword");
+
+ var login7 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete4", null,
+ "form8user", "form8pass", "uname", "pword");
+
+ var login8A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAB", "form9pass", "uname", "pword");
+ var login8B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAAB", "form9pass", "uname", "pword");
+ var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+ "form9userAABzz", "form9pass", "uname", "pword");
+
+ var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
+ "testuser10", "testpass10", "uname", "pword");
+
+
+ // try/catch in case someone runs the tests manually, twice.
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ Services.logins.addLogin(login4);
+ Services.logins.addLogin(login5);
+ Services.logins.addLogin(login6A);
+ Services.logins.addLogin(login6B);
+ Services.logins.addLogin(login7);
+ Services.logins.addLogin(login8A);
+ Services.logins.addLogin(login8B);
+ // login8C is added later
+ Services.logins.addLogin(login10);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+
+ addMessageListener("addLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+ Services.logins.addLogin(login);
+ });
+ addMessageListener("removeLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+ Services.logins.removeLogin(login);
+ });
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- other forms test single logins, with autocomplete=off set -->
+ <form id="form2" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://autocomplete2" onsubmit="return false;" autocomplete="off">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form5" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname" autocomplete="off">
+ <input type="password" name="pword" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- control -->
+ <form id="form6" action="http://autocomplete2" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- This form will be manipulated to insert a different username field. -->
+ <form id="form7" action="http://autocomplete3" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for no autofill after onblur with blank username -->
+ <form id="form8" action="http://autocomplete4" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test autocomplete dropdown -->
+ <form id="form9" action="http://autocomplete5" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- test for onUsernameInput recipe testing -->
+ <form id="form11" action="http://autocomplete7" onsubmit="return false;">
+ <input type="text" name="1">
+ <input type="text" name="2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- tests <form>-less autocomplete -->
+ <div id="form12">
+ <input type="text" name="uname" id="uname">
+ <input type="password" name="pword" id="pword">
+ <button type="submit">Submit</button>
+ </div>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]]});
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form1_warning_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised. Learn More",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select insecure warning
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACForm("", "");
+});
+
+add_task(function* test_form1_first_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_second_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_third_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser3", "testpass3");
+});
+
+add_task(function* test_form1_fourth_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_first_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ yield spinEventLoop(); // let focus happen
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("down"); // third
+ doKey("down"); // fourth
+ doKey("down"); // deselects
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_wraparound_up_last_entry() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("up"); // last (fourth)
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_down_up_up() {
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // select first entry
+ doKey("up"); // selects nothing!
+ doKey("up"); // select last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_wraparound_up_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down");
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("up");
+ doKey("up");
+ doKey("up"); // skip insecure warning
+ doKey("up"); // first entry
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_fill_username_without_autofill_right() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("right");
+ yield spinEventLoop();
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_fill_username_without_autofill_left() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Set first entry w/o triggering autocomplete
+ doKey("down"); // skip insecure warning
+ doKey("down"); // first
+ doKey("left");
+ checkACForm("tempuser1", ""); // empty password
+});
+
+add_task(function* test_form1_pageup_first() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // Check first entry (page up)
+ doKey("down"); // first
+ doKey("down"); // second
+ doKey("page_up"); // first
+ doKey("down"); // skip insecure warning
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_pagedown_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // test 13
+ // Check last entry (page down)
+ doKey("down"); // first
+ doKey("page_down"); // last
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_untrusted_event() {
+ restoreForm();
+ yield spinEventLoop();
+
+ // Send a fake (untrusted) event.
+ checkACForm("", "");
+ uname.value = "zzzuser4";
+ sendFakeAutocompleteEvent(uname);
+ yield spinEventLoop();
+ checkACForm("zzzuser4", "");
+});
+
+add_task(function* test_form1_delete() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ // XXX tried sending character "t" before/during dropdown to test
+ // filtering, but had no luck. Seemed like the character was getting lost.
+ // Setting uname.value didn't seem to work either. This works with a human
+ // driver, so I'm not sure what's up.
+
+ doKey("down"); // skip insecure warning
+ // Delete the first entry (of 4), "tempuser1"
+ doKey("down");
+ var numLogins;
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 5, "Correct number of logins before deleting one");
+
+ let countChangedPromise = notifyMenuChanged(4);
+ var deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ doKey("delete", shiftModifier);
+ yield deletionPromise;
+
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 4, "Correct number of logins after deleting one");
+ yield countChangedPromise;
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_deletion() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check the new first entry (of 3)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_second() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Delete the second entry (of 3), "testuser3"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 3, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_first_after_deletion2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check the new first entry (of 2)
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_delete_last() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // test 54
+ // Delete the last entry (of 2), "zzzuser4"
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 2, "Correct number of logins after deleting one");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_first_after_3_deletions() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check the only remaining entry
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_check_only_entry_remaining() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // test 56
+ // Delete the only remaining entry, "testuser2"
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkACForm("", "");
+ numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+ is(numLogins, 1, "Correct number of logins after deleting one");
+
+ // remove the login that's not shown in the list.
+ setupScript.sendSyncMessage("removeLogin", "login0");
+});
+
+// Tests for single-user forms for ignoring autocomplete=off
+add_task(function* test_form2() {
+ // Turn our attention to form2
+ uname = $_(2, "uname");
+ pword = $_(2, "pword");
+ checkACForm("singleuser5", "singlepass5");
+
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form3() {
+ uname = $_(3, "uname");
+ pword = $_(3, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form4() {
+ uname = $_(4, "uname");
+ pword = $_(4, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form5() {
+ uname = $_(5, "uname");
+ pword = $_(5, "pword");
+ checkACForm("singleuser5", "singlepass5");
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6() {
+ // (this is a control, w/o autocomplete=off, to ensure the login
+ // that was being suppressed would have been filled in otherwise)
+ uname = $_(6, "uname");
+ pword = $_(6, "pword");
+ checkACForm("singleuser5", "singlepass5");
+});
+
+add_task(function* test_form6_changeUsername() {
+ // Test that the password field remains filled in after changing
+ // the username.
+ uname.focus();
+ doKey("right");
+ sendChar("X");
+ // Trigger the 'blur' event on uname
+ pword.focus();
+ yield spinEventLoop();
+ checkACForm("singleuser5X", "singlepass5");
+
+ setupScript.sendSyncMessage("removeLogin", "login5");
+});
+
+add_task(function* test_form7() {
+ uname = $_(7, "uname");
+ pword = $_(7, "pword");
+ checkACForm("", "");
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ var newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ pword.parentNode.insertBefore(newField, pword);
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+
+ // Delete login6B. It was created just to prevent filling in a login
+ // automatically, removing it makes it more likely that we'll catch a
+ // future regression with form filling here.
+ setupScript.sendSyncMessage("removeLogin", "login6B");
+});
+
+add_task(function* test_form7_2() {
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Check first entry
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ // The form changes, so we expect the old username field to get the
+ // selected autocomplete value, but neither the new username field nor
+ // the password field should have any values filled in.
+ yield spinEventLoop();
+ checkACForm("form7user1", "");
+ is($_(7, "uname2").value, "", "Verifying empty uname2");
+ restoreForm(); // clear field, so reloading test doesn't fail
+
+ setupScript.sendSyncMessage("removeLogin", "login6A");
+});
+
+add_task(function* test_form8() {
+ uname = $_(8, "uname");
+ pword = $_(8, "pword");
+ checkACForm("form8user", "form8pass");
+ restoreForm();
+});
+
+add_task(function* test_form8_blur() {
+ checkACForm("", "");
+ // Focus the previous form to trigger a blur.
+ $_(7, "uname").focus();
+});
+
+add_task(function* test_form8_2() {
+ checkACForm("", "");
+ restoreForm();
+});
+
+add_task(function* test_form8_3() {
+ checkACForm("", "");
+ setupScript.sendSyncMessage("removeLogin", "login7");
+});
+
+add_task(function* test_form9_filtering() {
+ // Turn our attention to form9 to test the dropdown - bug 497541
+ uname = $_(9, "uname");
+ pword = $_(9, "pword");
+ uname.focus();
+ let shownPromise = promiseACShown();
+ sendString("form9userAB");
+ yield shownPromise;
+
+ checkACForm("form9userAB", "");
+ uname.focus();
+ doKey("left");
+ shownPromise = promiseACShown();
+ sendChar("A");
+ let results = yield shownPromise;
+
+ checkACForm("form9userAAB", "");
+ checkArrayValues(results, ["This connection is not secure. Logins entered here could be compromised. Learn More", "form9userAAB"],
+ "Check dropdown is updated after inserting 'A'");
+ doKey("down"); // skip insecure warning
+ doKey("down");
+ doKey("return");
+ yield promiseFormsProcessed();
+ checkACForm("form9userAAB", "form9pass");
+});
+
+add_task(function* test_form9_autocomplete_cache() {
+ // Note that this addLogin call will only be seen by the autocomplete
+ // attempt for the sendChar if we do not successfully cache the
+ // autocomplete results.
+ setupScript.sendSyncMessage("addLogin", "login8C");
+ uname.focus();
+ let promise0 = notifyMenuChanged(1);
+ let shownPromise = promiseACShown();
+ sendChar("z");
+ yield promise0;
+ yield shownPromise;
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup should open");
+
+ // check that empty results are cached - bug 496466
+ promise0 = notifyMenuChanged(1);
+ sendChar("z");
+ yield promise0;
+ popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup stays opened due to cached empty result");
+});
+
+add_task(function* test_form11_recipes() {
+ yield loadRecipes({
+ siteRecipes: [{
+ "hosts": ["mochi.test:8888"],
+ "usernameSelector": "input[name='1']",
+ "passwordSelector": "input[name='2']"
+ }],
+ });
+ uname = $_(11, "1");
+ pword = $_(11, "2");
+
+ // First test DOMAutocomplete
+ // Switch the password field to type=password so _fillForm marks the username
+ // field for autocomplete.
+ pword.type = "password";
+ yield promiseFormsProcessed();
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+
+ // Now test recipes with blur on the username field.
+ restoreForm();
+ checkACForm("", "");
+ uname.value = "testuser10";
+ checkACForm("testuser10", "");
+ doKey("tab");
+ yield promiseFormsProcessed();
+ checkACForm("testuser10", "testpass10");
+ yield resetRecipes();
+});
+
+add_task(function* test_form12_formless() {
+ // Test form-less autocomplete
+ uname = $_(12, "uname");
+ pword = $_(12, "pword");
+ restoreForm();
+ checkACForm("", "");
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ doKey("down"); // skip insecure warning
+ // Trigger autocomplete
+ doKey("down");
+ checkACForm("", ""); // value shouldn't update
+ let processedPromise = promiseFormsProcessed();
+ doKey("return"); // not "enter"!
+ yield processedPromise;
+ checkACForm("testuser", "testpass");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
new file mode 100644
index 0000000000..c3a894958a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login, contextual inscure password warning without saved logins</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: contextual inscure password warning without saved logins
+
+<script>
+let chromeScript = runChecksAfterCommonInit();
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: contextual inscure password warning without saved logins. **/
+
+// Set to pref before the document loads.
+SpecialPowers.setBoolPref(
+ "security.insecure_field_warning.contextual.enabled", true);
+
+SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref(
+ "security.insecure_field_warning.contextual.enabled");
+});
+
+let uname = $_(1, "uname");
+let pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+// Check for expected username/password in form.
+function checkACForm(expectedUsername, expectedPassword) {
+ let formID = uname.parentNode.id;
+ is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACForm("", "");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form1_warning_entry() {
+ yield SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.open, true, "Check popup is opened");
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ doKey("down"); // select insecure warning
+ checkACForm("", ""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACForm("", "");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html
new file mode 100644
index 0000000000..2b6da33ec2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for maxlength attributes</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 391514
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- normal form. -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- limited username -->
+ <form id="form2" action="formtest.js">
+ <input type="text" name="uname" maxlength="4">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited password -->
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="4">
+ </form>
+
+ <!-- limited username and password -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname" maxlength="4">
+ <input type="password" name="pword" maxlength="4">
+ </form>
+
+
+ <!-- limited username -->
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname" maxlength="0">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited password -->
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="0">
+ </form>
+
+ <!-- limited username and password -->
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname" maxlength="0">
+ <input type="password" name="pword" maxlength="0">
+ </form>
+
+
+ <!-- limited, but ok, username -->
+ <form id="form8" action="formtest.js">
+ <input type="text" name="uname" maxlength="999">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited, but ok, password -->
+ <form id="form9" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="999">
+ </form>
+
+ <!-- limited, but ok, username and password -->
+ <form id="form10" action="formtest.js">
+ <input type="text" name="uname" maxlength="999">
+ <input type="password" name="pword" maxlength="999">
+ </form>
+
+
+ <!-- limited, but ok, username -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form11" action="formtest.js">
+ <input type="text" name="uname" maxlength="8">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited, but ok, password -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form12" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="8">
+ </form>
+
+ <!-- limited, but ok, username and password -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form13" action="formtest.js">
+ <input type="text" name="uname" maxlength="8">
+ <input type="password" name="pword" maxlength="8">
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 391514 (Login Manager gets confused with
+ * password/PIN on usaa.com)
+ */
+
+function startTest() {
+ var i;
+
+ is($_(1, "uname").value, "testuser", "Checking for filled username 1");
+ is($_(1, "pword").value, "testpass", "Checking for filled password 1");
+
+ for (i = 2; i < 8; i++) {
+ is($_(i, "uname").value, "", "Checking for unfilled username " + i);
+ is($_(i, "pword").value, "", "Checking for unfilled password " + i);
+ }
+
+ for (i = 8; i < 14; i++) {
+ is($_(i, "uname").value, "testuser", "Checking for filled username " + i);
+ is($_(i, "pword").value, "testpass", "Checking for filled password " + i);
+ }
+
+ // Note that tests 11-13 are limited to exactly the expected value.
+ // Assert this lest someone change the login we're testing with.
+ is($_(11, "uname").value.length, 8, "asserting test assumption is valid.");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
new file mode 100644
index 0000000000..443c8a5e9e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
@@ -0,0 +1,291 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: multiple login autocomplete
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Create some logins just for this form, since we'll be deleting them.
+ var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+ assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "user0pass", "", "pword");
+
+ var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword");
+
+ var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword");
+
+ var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword");
+
+ var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword");
+
+
+ // try/catch in case someone runs the tests manually, twice.
+ try {
+ Services.logins.addLogin(login0);
+ Services.logins.addLogin(login1);
+ Services.logins.addLogin(login2);
+ Services.logins.addLogin(login3);
+ Services.logins.addLogin(login4);
+ } catch (e) {
+ assert.ok(false, "addLogin threw: " + e);
+ }
+
+ addMessageListener("addLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+ Services.logins.addLogin(login);
+ });
+ addMessageListener("removeLogin", loginVariableName => {
+ let login = eval(loginVariableName);
+ assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+ Services.logins.removeLogin(login);
+ });
+});
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" readonly="true">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" disabled="true">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+var uname = $_(1, "uname");
+var pword = $_(1, "pword");
+const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
+
+// Restore the form to the default state.
+function* reinitializeForm(index) {
+ // Using innerHTML is for creating the autocomplete popup again, so the
+ // preference value will be applied to the constructor of
+ // UserAutoCompleteResult.
+ let form = document.getElementById("form" + index);
+ let temp = form.innerHTML;
+ form.innerHTML = "";
+ form.innerHTML = temp;
+
+ yield new Promise(resolve => {
+ let observer = SpecialPowers.wrapCallback(() => {
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ resolve();
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+ });
+
+ yield SimpleTest.promiseFocus(window);
+
+ uname = $_(index, "uname");
+ pword = $_(index, "pword");
+ uname.value = "";
+ pword.value = "";
+ pword.focus();
+}
+
+function generateDateString(date) {
+ let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+ return dateAndTimeFormatter.format(date);
+}
+
+const DATE_NOW_STRING = generateDateString(new Date());
+
+// Check for expected username/password in form.
+function checkACFormPasswordField(expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_task(function* setup() {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(function* test_form1_initial_empty() {
+ yield SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACFormPasswordField("");
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(function* test_form2_password_readonly() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(2);
+
+ // Trigger autocomplete popup
+ doKey("down"); // open
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed for a readonly field.");
+});
+
+add_task(function* test_form3_password_disabled() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(3);
+
+ // Trigger autocomplete popup
+ doKey("down"); // open
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed for a disabled field.");
+});
+
+add_task(function* test_form1_enabledInsecureFieldWarning_enabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(1);
+ // Trigger autocomplete popup
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised. Learn More",
+ "No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select insecure warning
+ checkACFormPasswordField(""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACFormPasswordField("");
+});
+
+add_task(function* test_form1_disabledInsecureFieldWarning_enabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", false],
+ ["signon.autofillForms.http", true]
+ ]});
+ yield reinitializeForm(1);
+
+ // Trigger autocomplete popup
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select first item
+ checkACFormPasswordField(""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACFormPasswordField("user0pass");
+});
+
+add_task(function* test_form1_enabledInsecureFieldWarning_disabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", true],
+ ["signon.autofillForms.http", false]
+ ]});
+ yield reinitializeForm(1);
+
+ // Trigger autocomplete popup
+ let shownPromise = promiseACShown();
+ doKey("down"); // open
+ let results = yield shownPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised. Learn More",
+ "No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
+
+ doKey("down"); // select insecure warning
+ checkACFormPasswordField(""); // value shouldn't update just by selecting
+ doKey("return"); // not "enter"!
+ yield spinEventLoop(); // let focus happen
+ checkACFormPasswordField("");
+});
+
+add_task(function* test_form1_disabledInsecureFieldWarning_disabledInsecureAutoFillForm() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", false],
+ ["signon.autofillForms.http", false]
+ ]});
+ yield reinitializeForm(1);
+
+ // Trigger autocomplete popup
+ doKey("down"); // open
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed with no AutoFillForms.");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html
new file mode 100644
index 0000000000..e107cebe60
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that passwords only get filled in type=password</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 242956
+<script>
+runChecksAfterCommonInit(() => startTest());
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- pword is not a type=password input -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="text" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- uname is not a type=text input -->
+ <form id="form2" action="formtest.js">
+ <input type="password" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- two "pword" inputs, (text + password) -->
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="text" name="pword">
+ <input type="password" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- same thing, different order -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="text" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- uname is not a type=text input (try a checkbox just for variety) -->
+ <form id="form5" action="formtest.js">
+ <input type="checkbox" name="uname" value="">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- pword is not a type=password input (try a checkbox just for variety) -->
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="checkbox" name="pword" value="">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- pword is not a type=password input -->
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="text" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 242956 (Stored password is inserted into a
+ readable text input on a second page) **/
+
+// Make sure that pwmgr only puts passwords into type=password <input>s.
+// Might as well test the converse, too (username in password field).
+
+function startTest() {
+ is($_(1, "uname").value, "", "Checking for unfilled username 1");
+ is($_(1, "pword").value, "", "Checking for unfilled password 1");
+
+ is($_(2, "uname").value, "testpass", "Checking for password not username 2");
+ is($_(2, "pword").value, "", "Checking for unfilled password 2");
+
+ is($_(3, "uname").value, "", "Checking for unfilled username 3");
+ is($_(3, "pword").value, "testuser", "Checking for unfilled password 3");
+ is($_(3, "qword").value, "testpass", "Checking for unfilled qassword 3");
+
+ is($_(4, "uname").value, "testuser", "Checking for password not username 4");
+ is($_(4, "pword").value, "testpass", "Checking for unfilled password 4");
+ is($_(4, "qword").value, "", "Checking for unfilled qassword 4");
+
+ is($_(5, "uname").value, "", "Checking for unfilled username 5");
+ is($_(5, "pword").value, "testpass", "Checking for filled password 5");
+
+ is($_(6, "uname").value, "", "Checking for unfilled username 6");
+ is($_(6, "pword").value, "", "Checking for unfilled password 6");
+
+ is($_(7, "uname").value, "testuser", "Checking for unmodified username 7");
+ is($_(7, "pword").value, "", "Checking for unfilled password 7");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
new file mode 100644
index 0000000000..1050ab66b3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
@@ -0,0 +1,705 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test prompter.{prompt,promptPassword,promptUsernameAndPassword}</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var uname = { value: null };
+var pword = { value: null };
+var result = { value: null };
+var isOk;
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let prompterParent = runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompter1 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt);
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ let rv = prompter1[msg.methodName](...msg.args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+});
+
+let prompter1 = new PrompterProxy(prompterParent);
+
+const defaultTitle = "the title";
+const defaultMsg = "the message";
+
+function initLogins() {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ var login1, login2A, login2B, login2C, login2D, login2E;
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2D = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2E = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://example.com", null, "http://example.com",
+ "", "examplepass", "", "");
+ login2A.init("http://example2.com", null, "http://example2.com",
+ "user1name", "user1pass", "", "");
+ login2B.init("http://example2.com", null, "http://example2.com",
+ "user2name", "user2pass", "", "");
+ login2C.init("http://example2.com", null, "http://example2.com",
+ "user3.name@host", "user3pass", "", "");
+ login2D.init("http://example2.com", null, "http://example2.com",
+ "100@beef", "user3pass", "", "");
+ login2E.init("http://example2.com", null, "http://example2.com",
+ "100%beef", "user3pass", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+ pwmgr.addLogin(login2C);
+ pwmgr.addLogin(login2D);
+ pwmgr.addLogin(login2E);
+}
+
+add_task(function* setup() {
+ runInParent(initLogins);
+});
+
+add_task(function* test_prompt_accept() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "abc",
+ passValue : "",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "xyz",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(result.value, "xyz", "Checking prompt() returned value");
+});
+
+add_task(function* test_prompt_cancel() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "abc",
+ passValue : "",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
+ yield promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_promptPassword_defaultAccept() {
+ // Default password provided, existing logins are ignored.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "inputpw",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "secret",
+ };
+ pword.value = "inputpw";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_defaultCancel() {
+ // Default password provided, existing logins are ignored.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "inputpw",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ pword.value = "inputpw";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_promptPassword_emptyAccept() {
+ // No default password provided, realm does not match existing login.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "secret",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_saved() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "examplepass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_noMatchingPasswordForEmptyUN() {
+ // No default password provided, none of the logins from this host are
+ // password-only so the user is prompted.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "secret",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_matchingPWForUN() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://user1name@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user1pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_matchingPWForUN2() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://user2name@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_matchingPWForUN3() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://user3%2Ename%40host@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_extraAt() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://100@beef@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_usernameEncoding() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "http://100%25beef@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+
+ // XXX test saving a password with Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY
+});
+
+add_task(function* test_promptPassword_realm() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "fill2pass",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(function* test_promptPassword_realm2() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "fill2pass",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_accept() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "inuser",
+ passValue : "inpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "outuser",
+ passField : "outpass",
+ };
+ uname.value = "inuser";
+ pword.value = "inpass";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "outuser", "Checking returned username");
+ is(pword.value, "outpass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_cancel() {
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "inuser",
+ passValue : "inpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ uname.value = "inuser";
+ pword.value = "inpass";
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ yield promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_promptUsernameAndPassword_autofill() {
+ // test filling in existing password-only login
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "examplepass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "", "Checking returned username");
+ is(pword.value, "examplepass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_multipleExisting() {
+ // test filling in existing login (undetermined from multiple selection)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ ok(uname.value == "user1name" || uname.value == "user2name", "Checking returned username");
+ ok(pword.value == "user1pass" || uname.value == "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_multipleExisting1() {
+ // test filling in existing login (user1 from multiple selection)
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = "user1name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user1name", "Checking returned username");
+ is(pword.value, "user1pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_multipleExisting2() {
+ // test filling in existing login (user2 from multiple selection)
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user2name",
+ passValue : "user2pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_passwordChange() {
+ // test changing password
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user2name",
+ passValue : "user2pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "NEWuser2pass",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "NEWuser2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_changePasswordBack() {
+ // test changing password (back to original value)
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "user2name",
+ passValue : "NEWuser2pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ checkMsg : "Use Password Manager to remember this password.",
+ checked : true,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "user2pass",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_realm() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "fill2user",
+ passField : "fill2pass",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "fill2user", "Checking returned username");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(function* test_promptUsernameAndPassword_realm2() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "fill2user",
+ passField : "fill2pass",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.promptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ yield promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "fill2user", "Checking returned username");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
new file mode 100644
index 0000000000..0dc8fdf9cb
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
@@ -0,0 +1,362 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test HTTP auth prompts by loading authenticate.sjs</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var iframe = document.getElementById("iframe");
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+const AUTHENTICATE_PATH = new URL("authenticate.sjs", window.location.href).pathname;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ let login3A, login3B, login4;
+ login3A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login3B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let httpUpgradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let httpsDowngradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let dedupeHttpUpgradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let dedupeHttpsUpgradeLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+
+ login3A.init("http://mochi.test:8888", null, "mochitest",
+ "mochiuser1", "mochipass1", "", "");
+ login3B.init("http://mochi.test:8888", null, "mochitest2",
+ "mochiuser2", "mochipass2", "", "");
+ login4.init("http://mochi.test:8888", null, "mochitest3",
+ "mochiuser3", "mochipass3-old", "", "");
+ // Logins to test scheme upgrades (allowed) and downgrades (disallowed)
+ httpUpgradeLogin.init("http://example.com", null, "schemeUpgrade",
+ "httpUser", "httpPass", "", "");
+ httpsDowngradeLogin.init("https://example.com", null, "schemeDowngrade",
+ "httpsUser", "httpsPass", "", "");
+ // HTTP and HTTPS version of the same domain and realm but with different passwords.
+ dedupeHttpUpgradeLogin.init("http://example.org", null, "schemeUpgradeDedupe",
+ "dedupeUser", "httpPass", "", "");
+ dedupeHttpsUpgradeLogin.init("https://example.org", null, "schemeUpgradeDedupe",
+ "dedupeUser", "httpsPass", "", "");
+
+
+ pwmgr.addLogin(login3A);
+ pwmgr.addLogin(login3B);
+ pwmgr.addLogin(login4);
+ pwmgr.addLogin(httpUpgradeLogin);
+ pwmgr.addLogin(httpsDowngradeLogin);
+ pwmgr.addLogin(dedupeHttpUpgradeLogin);
+ pwmgr.addLogin(dedupeHttpsUpgradeLogin);
+});
+
+add_task(function* test_iframe() {
+ let state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "mochiuser1",
+ passValue : "mochipass1",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ var iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe.contentDocument);
+
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest2”",
+ title : "Authentication Required",
+ textValue : "mochiuser2",
+ passValue : "mochipass2",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ promptDone = handlePrompt(state, action);
+ // We've already authenticated to this host:port. For this next
+ // request, the existing auth should be sent, we'll get a 401 reply,
+ // and we should prompt for new auth.
+ iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest2";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
+ iframe.contentDocument);
+
+ // Now make a load that requests the realm from test 1000. It was
+ // already provided there, so auth will *not* be prompted for -- the
+ // networking layer already knows it!
+ iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe.contentDocument);
+
+ // Same realm we've already authenticated to, but with a different
+ // expected password (to trigger an auth prompt, and change-password
+ // popup notification).
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "mochiuser1",
+ passValue : "mochipass1",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "mochipass1-new",
+ };
+ promptDone = handlePrompt(state, action);
+ iframeLoaded = onloadPromiseFor("iframe");
+ let promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"},
+ iframe.contentDocument);
+ yield promptShownPromise;
+
+ // Same as last test, but for a realm we haven't already authenticated
+ // to (but have an existing saved login for, so that we'll trigger
+ // a change-password popup notification.
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest3”",
+ title : "Authentication Required",
+ textValue : "mochiuser3",
+ passValue : "mochipass3-old",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ passField : "mochipass3-new",
+ };
+ promptDone = handlePrompt(state, action);
+ iframeLoaded = onloadPromiseFor("iframe");
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
+ iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"},
+ iframe.contentDocument);
+ yield promptShownPromise;
+
+ // Housekeeping: Delete login4 to test the save prompt in the next test.
+ runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ var tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ tmpLogin.init("http://mochi.test:8888", null, "mochitest3",
+ "mochiuser3", "mochipass3-old", "", "");
+ Services.logins.removeLogin(tmpLogin);
+
+ // Clear cached auth from this subtest, and avoid leaking due to bug 459620.
+ var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].
+ getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+ });
+
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest3”",
+ title : "Authentication Required",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "mochiuser3",
+ passField : "mochipass3-old",
+ };
+ // Trigger a new prompt, so we can test adding a new login.
+ promptDone = handlePrompt(state, action);
+
+ iframeLoaded = onloadPromiseFor("iframe");
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-save");
+ iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"},
+ iframe.contentDocument);
+ yield promptShownPromise;
+});
+
+add_task(function* test_schemeUpgrade() {
+ let state = {
+ msg : "https://example.com is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "httpUser",
+ passValue : "httpPass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "https://example.com" + AUTHENTICATE_PATH +
+ "?user=httpUser&pass=httpPass&realm=schemeUpgrade";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "httpUser", pass: "httpPass"},
+ SpecialPowers.wrap(iframe).contentDocument);
+});
+
+add_task(function* test_schemeDowngrade() {
+ let state = {
+ msg : "http://example.com is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "", // empty because we shouldn't downgrade
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "cancel",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "http://example.com" + AUTHENTICATE_PATH +
+ "?user=unused&pass=unused&realm=schemeDowngrade";
+ yield promptDone;
+ yield iframeLoaded;
+});
+
+add_task(function* test_schemeUpgrade_dedupe() {
+ let state = {
+ msg : "https://example.org is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "dedupeUser",
+ passValue : "httpsPass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "https://example.org" + AUTHENTICATE_PATH +
+ "?user=dedupeUser&pass=httpsPass&realm=schemeUpgradeDedupe";
+ yield promptDone;
+ yield iframeLoaded;
+ checkEchoedAuthInfo({user: "dedupeUser", pass: "httpsPass"},
+ SpecialPowers.wrap(iframe).contentDocument);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
new file mode 100644
index 0000000000..92af172ca1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test HTTP auth prompts by loading authenticate.sjs with no window</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login.init("http://mochi.test:8888", null, "mochitest",
+ "mochiuser1", "mochipass1", "", "");
+ Services.logins.addLogin(login);
+});
+
+add_task(function* test_sandbox_xhr() {
+ let state = {
+ msg : "http://mochi.test:8888 is requesting your username and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "mochiuser1",
+ passValue : "mochipass1",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick : "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ let url = new URL("authenticate.sjs?user=mochiuser1&pass=mochipass1", window.location.href);
+ let sandboxConstructor = SpecialPowers.Cu.Sandbox;
+ let sandbox = new sandboxConstructor(this, {wantXrays: true});
+ function sandboxedRequest(sandboxedUrl) {
+ let req = SpecialPowers.Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(SpecialPowers.Ci.nsIXMLHttpRequest);
+ req.open("GET", sandboxedUrl, true);
+ req.send(null);
+ }
+
+ let loginModifiedPromise = promiseStorageChanged(["modifyLogin"]);
+ sandbox.sandboxedRequest = sandboxedRequest(url);
+ info("send the XHR request in the sandbox");
+ SpecialPowers.Cu.evalInSandbox("sandboxedRequest;", sandbox);
+
+ yield promptDone;
+ info("prompt shown, waiting for metadata updates");
+ // Ensure the timeLastUsed and timesUsed metadata are updated.
+ yield loginModifiedPromise;
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
new file mode 100644
index 0000000000..36f53a54a0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
@@ -0,0 +1,406 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test promptAuth prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var isOk;
+
+var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+var authinfo = {
+ username : "",
+ password : "",
+ domain : "",
+
+ flags : Ci.nsIAuthInformation.AUTH_HOST,
+ authenticationScheme : "basic",
+ realm : ""
+};
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let prompterParent = runInParent(() => {
+ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2);
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ let channels = {};
+ channels.channel1 = ioService.newChannel2("http://example.com",
+ null,
+ null,
+ null, // aLoadingNode
+ Services.
+ scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ channels.channel2 = ioService.newChannel2("http://example2.com",
+ null,
+ null,
+ null, // aLoadingNode
+ Services.
+ scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ let args = [...msg.args];
+ let channelName = args.shift();
+ // Replace the channel name string (arg. 0) with the channel by that name.
+ args.unshift(channels[channelName]);
+
+ let rv = prompter2[msg.methodName](...args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ let login1, login2A, login2B, login2C, login2D, login2E;
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2D = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2E = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://example.com", null, "http://example.com",
+ "", "examplepass", "", "");
+ login2A.init("http://example2.com", null, "http://example2.com",
+ "user1name", "user1pass", "", "");
+ login2B.init("http://example2.com", null, "http://example2.com",
+ "user2name", "user2pass", "", "");
+ login2C.init("http://example2.com", null, "http://example2.com",
+ "user3.name@host", "user3pass", "", "");
+ login2D.init("http://example2.com", null, "http://example2.com",
+ "100@beef", "user3pass", "", "");
+ login2E.init("http://example2.com", null, "http://example2.com",
+ "100%beef", "user3pass", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+ pwmgr.addLogin(login2C);
+ pwmgr.addLogin(login2D);
+ pwmgr.addLogin(login2E);
+});
+
+let prompter2 = new PrompterProxy(prompterParent);
+
+add_task(function* test_accept() {
+ state = {
+ msg : "http://example.com is requesting your username and password. The site says: “some realm”",
+ title : "Authentication Required",
+ textValue : "inuser",
+ passValue : "inpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "outuser",
+ passField : "outpass",
+ };
+ authinfo.username = "inuser";
+ authinfo.password = "inpass";
+ authinfo.realm = "some realm";
+
+ promptDone = handlePrompt(state, action);
+ // Since prompter2 is actually a proxy to send a message to a chrome script and
+ // we can't send a channel in a message, we instead send the channel name that
+ // already exists in the chromeScript.
+ isOk = prompter2.promptAuth("channel1", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "outuser", "Checking returned username");
+ is(authinfo.password, "outpass", "Checking returned password");
+});
+
+add_task(function* test_cancel() {
+ state = {
+ msg : "http://example.com is requesting your username and password. The site says: “some realm”",
+ title : "Authentication Required",
+ textValue : "outuser",
+ passValue : "outpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel1", level, authinfo);
+ yield promptDone;
+
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(function* test_pwonly() {
+ // test filling in password-only login
+ state = {
+ msg : "http://example.com is requesting your username and password. The site says: “http://example.com”",
+ title : "Authentication Required",
+ textValue : "",
+ passValue : "examplepass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel1", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "", "Checking returned username");
+ is(authinfo.password, "examplepass", "Checking returned password");
+});
+
+add_task(function* test_multipleExisting() {
+ // test filling in existing login (undetermined from multiple selection)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ ok(authinfo.username == "user1name" || authinfo.username == "user2name", "Checking returned username");
+ ok(authinfo.password == "user1pass" || authinfo.password == "user2pass", "Checking returned password");
+});
+
+add_task(function* test_multipleExisting2() {
+ // test filling in existing login (undetermined --> user1)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // enter one of the known logins, test 504+505 exercise the two possible states.
+ action = {
+ buttonClick : "ok",
+ textField : "user1name",
+ passField : "user1pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user1name", "Checking returned username");
+ is(authinfo.password, "user1pass", "Checking returned password");
+});
+
+add_task(function* test_multipleExisting3() {
+ // test filling in existing login (undetermined --> user2)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // enter one of the known logins, test 504+505 exercise the two possible states.
+ action = {
+ buttonClick : "ok",
+ textField : "user2name",
+ passField : "user2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "user2pass", "Checking returned password");
+});
+
+add_task(function* test_changingMultiple() {
+ // test changing a password (undetermined --> user2 w/ newpass)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // force to user2, and change the password
+ action = {
+ buttonClick : "ok",
+ textField : "user2name",
+ passField : "NEWuser2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "NEWuser2pass", "Checking returned password");
+});
+
+add_task(function* test_changingMultiple2() {
+ // test changing a password (undetermined --> user2 w/ origpass)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg : "http://example2.com is requesting your username and password. The site says: “http://example2.com”",
+ title : "Authentication Required",
+ textValue : "user1name",
+ passValue : "user1pass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ // force to user2, and change the password back
+ action = {
+ buttonClick : "ok",
+ textField : "user2name",
+ passField : "user2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth("channel2", level, authinfo);
+ yield promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "user2pass", "Checking returned password");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
new file mode 100644
index 0000000000..95dd4c7bc7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
@@ -0,0 +1,264 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test promptAuth proxy prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var pwmgr;
+var proxyLogin;
+var isOk;
+var mozproxy, proxiedHost = "http://mochi.test:8888";
+var proxyChannel;
+var systemPrincipal = SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal();
+var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+
+var proxyAuthinfo = {
+ username : "",
+ password : "",
+ domain : "",
+
+ flags : Ci.nsIAuthInformation.AUTH_PROXY,
+ authenticationScheme : "basic",
+ realm : ""
+};
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+isTabModal = false;
+
+const Cc_promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"];
+ok(Cc_promptFac != null, "Access Cc[@mozilla.org/passwordmanager/authpromptfactory;1]");
+
+const Ci_promptFac = Ci.nsIPromptFactory;
+ok(Ci_promptFac != null, "Access Ci.nsIPromptFactory");
+
+const promptFac = Cc_promptFac.getService(Ci_promptFac);
+ok(promptFac != null, "promptFac getService()");
+
+var prompter2 = promptFac.getPrompt(window, Ci.nsIAuthPrompt2);
+
+function initLogins(pi) {
+ pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ mozproxy = "moz-proxy://" + SpecialPowers.wrap(pi).host + ":" +
+ SpecialPowers.wrap(pi).port;
+
+ proxyLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ proxyLogin.init(mozproxy, null, "Proxy Realm",
+ "proxuser", "proxpass", "", "");
+
+ pwmgr.addLogin(proxyLogin);
+}
+
+var startupCompleteResolver;
+var startupComplete = new Promise(resolve => startupCompleteResolver = resolve);
+
+function proxyChannelListener() { }
+proxyChannelListener.prototype = {
+ onStartRequest: function(request, context) {
+ startupCompleteResolver();
+ },
+ onStopRequest: function(request, context, status) { }
+};
+
+var resolveCallback = SpecialPowers.wrapCallbackObject({
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ onProxyAvailable : function (req, uri, pi, status) {
+ initLogins(pi);
+
+ // I'm cheating a bit here... We should probably do some magic foo to get
+ // something implementing nsIProxiedProtocolHandler and then call
+ // NewProxiedChannel(), so we have something that's definately a proxied
+ // channel. But Mochitests use a proxy for a number of hosts, so just
+ // requesting a normal channel will give us a channel that's proxied.
+ // The proxyChannel needs to move to at least on-modify-request to
+ // have valid ProxyInfo, but we use OnStartRequest during startup()
+ // for simplicity.
+ proxyChannel = ioService.newChannel2(proxiedHost,
+ null,
+ null,
+ null, // aLoadingNode
+ systemPrincipal,
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ proxyChannel.asyncOpen2(SpecialPowers.wrapCallbackObject(new proxyChannelListener()));
+ }
+});
+
+function startup() {
+ // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
+ var ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"].
+ getService(SpecialPowers.Ci.nsIIOService);
+
+ var pps = SpecialPowers.Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+ var channel = ios.newChannel2("http://example.com",
+ null,
+ null,
+ null, // aLoadingNode
+ systemPrincipal,
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ pps.asyncResolve(channel, 0, resolveCallback);
+}
+
+startup();
+
+add_task(function* setup() {
+ info("Waiting for startup to complete...");
+ yield startupComplete;
+});
+
+add_task(function* test_noAutologin() {
+ // test proxy login (default = no autologin), make sure it prompts.
+ state = {
+ msg : "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”",
+ title : "Authentication Required",
+ textValue : "proxuser",
+ passValue : "proxpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ var time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
+ yield promptDone;
+ var time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(function* test_autologin() {
+ // test proxy login (with autologin)
+
+ // Enable the autologin pref.
+ prefs.setBoolPref("signon.autologin.proxy", true);
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
+ time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(function* test_autologin_incorrect() {
+ // test proxy login (with autologin), ensure it prompts after a failed auth.
+ state = {
+ msg : "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”",
+ title : "Authentication Required",
+ textValue : "proxuser",
+ passValue : "proxpass",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = (Ci.nsIAuthInformation.AUTH_PROXY | Ci.nsIAuthInformation.PREVIOUS_FAILED);
+
+ time1 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ promptDone = handlePrompt(state, action);
+ isOk = prompter2.promptAuth(proxyChannel, level, proxyAuthinfo);
+ yield promptDone;
+ time2 = pwmgr.findLogins({}, mozproxy, null, "Proxy Realm")[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(function* test_autologin_private() {
+ // test proxy login (with autologin), ensure it prompts in Private Browsing mode.
+ state = {
+ msg : "the message",
+ title : "the title",
+ textValue : "proxuser",
+ passValue : "proxpass",
+ };
+ action = {
+ buttonClick : "ok",
+ };
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ prefs.clearUserPref("signon.autologin.proxy");
+
+ // XXX check for and kill popup notification??
+ // XXX check for checkbox / checkstate on old prompts?
+ // XXX check NTLM domain stuff
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html
new file mode 100644
index 0000000000..943bffc522
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for recipes overriding login fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+let fillPromiseResolvers = [];
+
+function waitForFills(fillCount) {
+ let promises = [];
+ while (fillCount--) {
+ let promise = new Promise(resolve => fillPromiseResolvers.push(resolve));
+ promises.push(promise);
+ }
+
+ return Promise.all(promises);
+}
+
+add_task(function* setup() {
+ if (document.readyState !== "complete") {
+ yield new Promise((resolve) => {
+ document.onreadystatechange = () => {
+ if (document.readyState !== "complete") {
+ return;
+ }
+ document.onreadystatechange = null;
+ resolve();
+ };
+ });
+ }
+
+ document.getElementById("content")
+ .addEventListener("input", function handleInputEvent(evt) {
+ let resolve = fillPromiseResolvers.shift();
+ if (!resolve) {
+ ok(false, "Too many fills");
+ return;
+ }
+
+ resolve(evt.target);
+ });
+});
+
+add_task(function* loadUsernamePasswordSelectorRecipes() {
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='uname1']",
+ passwordSelector: "input[name='pword2']",
+ }],
+ });
+});
+
+add_task(function* testOverriddingFields() {
+ // Insert the form dynamically so autofill is triggered after setup above.
+ document.getElementById("content").innerHTML = `
+ <!-- form with recipe for the username and password -->
+ <form id="form1">
+ <input type="text" name="uname1" data-expected="true">
+ <input type="text" name="uname2" data-expected="false">
+ <input type="password" name="pword1" data-expected="false">
+ <input type="password" name="pword2" data-expected="true">
+ </form>`;
+
+ let elements = yield waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(function* testDefaultHeuristics() {
+ // Insert the form dynamically so autofill is triggered after setup above.
+ document.getElementById("content").innerHTML = `
+ <!-- Fallback to the default heuristics since the selectors don't match -->
+ <form id="form2">
+ <input type="text" name="uname3" data-expected="false">
+ <input type="text" name="uname4" data-expected="true">
+ <input type="password" name="pword3" data-expected="true">
+ <input type="password" name="pword4" data-expected="false">
+ </form>`;
+
+ let elements = yield waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(function* loadNotUsernameSelectorRecipes() {
+ yield resetRecipes();
+ yield loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ notUsernameSelector: "input[name='not_uname1']"
+ }],
+ });
+});
+
+add_task(function* testNotUsernameField() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notUsernameSelector should be skipped -->
+ <form id="form3">
+ <input type="text" name="uname5" data-expected="true">
+ <input type="text" name="not_uname1" data-expected="false">
+ <input type="password" name="pword5" data-expected="true">
+ </form>`;
+
+ let elements = yield waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(function* testNotUsernameFieldNoUsername() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notUsernameSelector should be skipped.
+ No username field should be found and filled in this case -->
+ <form id="form4">
+ <input type="text" name="not_uname1" data-expected="false">
+ <input type="password" name="pword6" data-expected="true">
+ </form>`;
+
+ let elements = yield waitForFills(1);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ // Forms are inserted dynamically
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
new file mode 100644
index 0000000000..c93c1e9c9b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
@@ -0,0 +1,263 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test interaction between autocomplete and focus on username fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+
+let readyPromise = registerRunTests();
+let chromeScript = runInParent(function chromeSetup() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ let pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+
+ let login1A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login1B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ let login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1A.init("http://mochi.test:8888", "http://username-focus-1", null,
+ "testuser1A", "testpass1A", "", "");
+
+ login2A.init("http://mochi.test:8888", "http://username-focus-2", null,
+ "testuser2A", "testpass2A", "", "");
+ login2B.init("http://mochi.test:8888", "http://username-focus-2", null,
+ "testuser2B", "testpass2B", "", "");
+
+ pwmgr.addLogin(login1A);
+ pwmgr.addLogin(login2A);
+ pwmgr.addLogin(login2B);
+});
+</script>
+
+<p id="display"></p>
+<div id="content">
+ <!-- first 3 forms have a matching user+pass login -->
+
+ <!-- user+pass form. -->
+ <form id="form-autofilled" action="http://username-focus-1">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit" name="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form-autofilled-prefilled-un" action="http://username-focus-1">
+ <input type="text" name="uname" value="testuser1A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form. -->
+ <form id="form-autofilled-focused-dynamic" action="http://username-focus-1">
+ <input type="text" name="uname">
+ <input type="not-yet-password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+ <!-- next 5 forms have matching user+pass (2x) logins -->
+
+ <!-- user+pass form. -->
+ <form id="form-multiple" action="http://username-focus-2">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form dynamic with existing focus -->
+ <form id="form-multiple-dynamic" action="http://username-focus-2">
+ <input type="text" name="uname">
+ <input type="not-yet-password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form-multiple-prefilled-un1" action="http://username-focus-2">
+ <input type="text" name="uname" value="testuser2A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, different username prefilled -->
+ <form id="form-multiple-prefilled-un2" action="http://username-focus-2">
+ <input type="text" name="uname" value="testuser2B">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled with existing focus -->
+ <form id="form-multiple-prefilled-focused-dynamic" action="http://username-focus-2">
+ <input type="text" name="uname" value="testuser2B">
+ <input type="not-yet-password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function removeFocus() {
+ $_("-autofilled", "submit").focus();
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({"set": [
+ ["security.insecure_field_warning.contextual.enabled", false],
+ ]});
+
+ ok(readyPromise, "check promise is available");
+ yield readyPromise;
+});
+
+add_task(function* test_autofilled() {
+ let usernameField = $_("-autofilled", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_autofilled_prefilled_un() {
+ let usernameField = $_("-autofilled-prefilled-un", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_autofilled_focused_dynamic() {
+ let usernameField = $_("-autofilled-focused-dynamic", "uname");
+ let passwordField = $_("-autofilled-focused-dynamic", "pword");
+ info("Username and password will be filled while username focused");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+ info("triggering autofill");
+ noPopupPromise = promiseNoUnexpectedPopupShown();
+ passwordField.type = "password";
+ yield noPopupPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed");
+
+ removeFocus();
+ passwordField.value = "test";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+// Begin testing forms that have multiple saved logins
+
+add_task(function* test_multiple() {
+ let usernameField = $_("-multiple", "uname");
+ info("Fields not filled due to multiple so autocomplete upon focus");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_dynamic() {
+ let usernameField = $_("-multiple-dynamic", "uname");
+ let passwordField = $_("-multiple-dynamic", "pword");
+ info("Fields not filled but username is focused upon marking so open");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ info("triggering _fillForm code");
+ let shownPromise = promiseACShown();
+ passwordField.type = "password";
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_prefilled_un1() {
+ let usernameField = $_("-multiple-prefilled-un1", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_prefilled_un2() {
+ let usernameField = $_("-multiple-prefilled-un2", "uname");
+ info("Username and password already filled so don't show autocomplete");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+
+ removeFocus();
+ usernameField.value = "testuser";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* test_multiple_prefilled_focused_dynamic() {
+ let usernameField = $_("-multiple-prefilled-focused-dynamic", "uname");
+ let passwordField = $_("-multiple-prefilled-focused-dynamic", "pword");
+ info("Username and password will be filled while username focused");
+ let noPopupPromise = promiseNoUnexpectedPopupShown();
+ usernameField.focus();
+ yield noPopupPromise;
+ info("triggering autofill");
+ noPopupPromise = promiseNoUnexpectedPopupShown();
+ passwordField.type = "password";
+ yield noPopupPromise;
+
+ let popupState = yield getPopupState();
+ is(popupState.open, false, "Check popup is closed");
+
+ removeFocus();
+ passwordField.value = "test";
+ info("Focus when we don't have an exact match");
+ shownPromise = promiseACShown();
+ usernameField.focus();
+ yield shownPromise;
+});
+
+add_task(function* cleanup() {
+ removeFocus();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html
new file mode 100644
index 0000000000..fa8357792b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=654348
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test XHR auth with user and pass arguments</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="startTest()">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test checks we correctly ignore authentication entry
+ * for a subpath and use creds from the URL when provided when XHR
+ * is used with filled user name and password.
+ *
+ * 1. connect authenticate.sjs that excepts user1:pass1 password
+ * 2. connect authenticate.sjs that this time expects differentuser2:pass2 password
+ * we must use the creds that are provided to the xhr witch are different and expected
+ */
+
+function doxhr(URL, user, pass, code, next) {
+ var xhr = new XMLHttpRequest();
+ if (user && pass)
+ xhr.open("POST", URL, true, user, pass);
+ else
+ xhr.open("POST", URL, true);
+ xhr.onload = function() {
+ is(xhr.status, code, "expected response code " + code);
+ next();
+ };
+ xhr.onerror = function() {
+ ok(false, "request passed");
+ finishTest();
+ };
+ xhr.send();
+}
+
+function startTest() {
+ doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "dummy", 403, function() {
+ doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "pass1", 200, finishTest);
+ });
+}
+
+function finishTest() {
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/prompt_common.js b/toolkit/components/passwordmgr/test/prompt_common.js
new file mode 100644
index 0000000000..267e697ae6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/prompt_common.js
@@ -0,0 +1,79 @@
+/**
+ * NOTE:
+ * This file is currently only being used for tests which haven't been
+ * fixed to work with e10s. Favor using the `prompt_common.js` file that
+ * is in `toolkit/components/prompts/test/` instead.
+ */
+
+var Ci = SpecialPowers.Ci;
+ok(Ci != null, "Access Ci");
+var Cc = SpecialPowers.Cc;
+ok(Cc != null, "Access Cc");
+
+var didDialog;
+
+var timer; // keep in outer scope so it's not GC'd before firing
+function startCallbackTimer() {
+ didDialog = false;
+
+ // Delay before the callback twiddles the prompt.
+ const dialogDelay = 10;
+
+ // Use a timer to invoke a callback to twiddle the authentication dialog
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+
+var observer = SpecialPowers.wrapCallbackObject({
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIObserver,
+ Ci.nsISupports, Ci.nsISupportsWeakReference];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ observe : function (subject, topic, data) {
+ var doc = getDialogDoc();
+ if (doc)
+ handleDialog(doc, testNum);
+ else
+ startCallbackTimer(); // try again in a bit
+ }
+});
+
+function getDialogDoc() {
+ // Find the <browser> which contains notifyWindow, by looking
+ // through all the open windows and all the <browsers> in each.
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ // var enumerator = wm.getEnumerator("navigator:browser");
+ var enumerator = wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ var containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ var childDocShell = containedDocShells.getNext();
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+ continue;
+ var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ // ok(true, "Got window: " + childDoc.location.href);
+ if (childDoc.location.href == "chrome://global/content/commonDialog.xul")
+ return childDoc;
+ }
+ }
+
+ return null;
+}
diff --git a/toolkit/components/passwordmgr/test/pwmgr_common.js b/toolkit/components/passwordmgr/test/pwmgr_common.js
new file mode 100644
index 0000000000..fa7c4fd85b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/pwmgr_common.js
@@ -0,0 +1,509 @@
+const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
+
+/**
+ * Returns the element with the specified |name| attribute.
+ */
+function $_(formNum, name) {
+ var form = document.getElementById("form" + formNum);
+ if (!form) {
+ logWarning("$_ couldn't find requested form " + formNum);
+ return null;
+ }
+
+ var element = form.children.namedItem(name);
+ if (!element) {
+ logWarning("$_ couldn't find requested element " + name);
+ return null;
+ }
+
+ // Note that namedItem is a bit stupid, and will prefer an
+ // |id| attribute over a |name| attribute when looking for
+ // the element. Login Mananger happens to use .namedItem
+ // anyway, but let's rigorously check it here anyway so
+ // that we don't end up with tests that mistakenly pass.
+
+ if (element.getAttribute("name") != name) {
+ logWarning("$_ got confused.");
+ return null;
+ }
+
+ return element;
+}
+
+/**
+ * Check a form for expected values. If an argument is null, a field's
+ * expected value will be the default value.
+ *
+ * <form id="form#">
+ * checkForm(#, "foo");
+ */
+function checkForm(formNum, val1, val2, val3) {
+ var e, form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ var numToCheck = arguments.length - 1;
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[0];
+ if (val1 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val1, "Test value of field " + e.name +
+ " in form " + formNum);
+
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[1];
+ if (val2 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val2, "Test value of field " + e.name +
+ " in form " + formNum);
+
+
+ if (!numToCheck--)
+ return;
+ e = form.elements[2];
+ if (val3 == null)
+ is(e.value, e.defaultValue, "Test default value of field " + e.name +
+ " in form " + formNum);
+ else
+ is(e.value, val3, "Test value of field " + e.name +
+ " in form " + formNum);
+}
+
+/**
+ * Check a form for unmodified values from when page was loaded.
+ *
+ * <form id="form#">
+ * checkUnmodifiedForm(#);
+ */
+function checkUnmodifiedForm(formNum) {
+ var form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ for (var i = 0; i < form.elements.length; i++) {
+ var ele = form.elements[i];
+
+ // No point in checking form submit/reset buttons.
+ if (ele.type == "submit" || ele.type == "reset")
+ continue;
+
+ is(ele.value, ele.defaultValue, "Test to default value of field " +
+ ele.name + " in form " + formNum);
+ }
+}
+
+/**
+ * Mochitest gives us a sendKey(), but it's targeted to a specific element.
+ * This basically sends an untargeted key event, to whatever's focused.
+ */
+function doKey(aKey, modifier) {
+ var keyName = "DOM_VK_" + aKey.toUpperCase();
+ var key = KeyEvent[keyName];
+
+ // undefined --> null
+ if (!modifier)
+ modifier = null;
+
+ // Window utils for sending fake sey events.
+ var wutils = SpecialPowers.wrap(window).
+ QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor).
+ getInterface(SpecialPowers.Ci.nsIDOMWindowUtils);
+
+ if (wutils.sendKeyEvent("keydown", key, 0, modifier)) {
+ wutils.sendKeyEvent("keypress", key, 0, modifier);
+ }
+ wutils.sendKeyEvent("keyup", key, 0, modifier);
+}
+
+/**
+ * Init with a common login
+ * If selfFilling is true or non-undefined, fires an event at the page so that
+ * the test can start checking filled-in values. Tests that check observer
+ * notifications might be confused by this.
+ */
+function commonInit(selfFilling) {
+ var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"].
+ getService(SpecialPowers.Ci.nsILoginManager);
+ ok(pwmgr != null, "Access LoginManager");
+
+ // Check that initial state has no logins
+ var logins = pwmgr.getAllLogins();
+ is(logins.length, 0, "Not expecting logins to be present");
+ var disabledHosts = pwmgr.getAllDisabledHosts();
+ if (disabledHosts.length) {
+ ok(false, "Warning: wasn't expecting disabled hosts to be present.");
+ for (var host of disabledHosts)
+ pwmgr.setLoginSavingEnabled(host, true);
+ }
+
+ // Add a login that's used in multiple tests
+ var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(SpecialPowers.Ci.nsILoginInfo);
+ login.init("http://mochi.test:8888", "http://mochi.test:8888", null,
+ "testuser", "testpass", "uname", "pword");
+ pwmgr.addLogin(login);
+
+ // Last sanity check
+ logins = pwmgr.getAllLogins();
+ is(logins.length, 1, "Checking for successful init login");
+ disabledHosts = pwmgr.getAllDisabledHosts();
+ is(disabledHosts.length, 0, "Checking for no disabled hosts");
+
+ if (selfFilling)
+ return;
+
+ if (this.sendAsyncMessage) {
+ sendAsyncMessage("registerRunTests");
+ } else {
+ registerRunTests();
+ }
+}
+
+function registerRunTests() {
+ return new Promise(resolve => {
+ // We provide a general mechanism for our tests to know when they can
+ // safely run: we add a final form that we know will be filled in, wait
+ // for the login manager to tell us that it's filled in and then continue
+ // with the rest of the tests.
+ window.addEventListener("DOMContentLoaded", (event) => {
+ var form = document.createElement('form');
+ form.id = 'observerforcer';
+ var username = document.createElement('input');
+ username.name = 'testuser';
+ form.appendChild(username);
+ var password = document.createElement('input');
+ password.name = 'testpass';
+ password.type = 'password';
+ form.appendChild(password);
+
+ var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
+ var formLikeRoot = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode);
+ if (formLikeRoot.id !== 'observerforcer')
+ return;
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ formLikeRoot.remove();
+ SimpleTest.executeSoon(() => {
+ var runTestEvent = new Event("runTests");
+ window.dispatchEvent(runTestEvent);
+ resolve();
+ });
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
+
+ document.body.appendChild(form);
+ });
+ });
+}
+
+const masterPassword = "omgsecret!";
+
+function enableMasterPassword() {
+ setMasterPassword(true);
+}
+
+function disableMasterPassword() {
+ setMasterPassword(false);
+}
+
+function setMasterPassword(enable) {
+ var oldPW, newPW;
+ if (enable) {
+ oldPW = "";
+ newPW = masterPassword;
+ } else {
+ oldPW = masterPassword;
+ newPW = "";
+ }
+ // Set master password. Note that this does not log you in, so the next
+ // invocation of pwmgr can trigger a MP prompt.
+
+ var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
+ var token = pk11db.findTokenByName("");
+ info("MP change from " + oldPW + " to " + newPW);
+ token.changePassword(oldPW, newPW);
+}
+
+function logoutMasterPassword() {
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+}
+
+function dumpLogins(pwmgr) {
+ var logins = pwmgr.getAllLogins();
+ ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
+ for (var i = 0; i < logins.length; i++)
+ dumpLogin("login #" + i + " --- ", logins[i]);
+}
+
+function dumpLogin(label, login) {
+ var loginText = "";
+ loginText += "host: ";
+ loginText += login.hostname;
+ loginText += " / formURL: ";
+ loginText += login.formSubmitURL;
+ loginText += " / realm: ";
+ loginText += login.httpRealm;
+ loginText += " / user: ";
+ loginText += login.username;
+ loginText += " / pass: ";
+ loginText += login.password;
+ loginText += " / ufield: ";
+ loginText += login.usernameField;
+ loginText += " / pfield: ";
+ loginText += login.passwordField;
+ ok(true, label + loginText);
+}
+
+function getRecipeParent() {
+ var { LoginManagerParent } = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
+ if (!LoginManagerParent.recipeParentPromise) {
+ return null;
+ }
+ return LoginManagerParent.recipeParentPromise.then((recipeParent) => {
+ return SpecialPowers.wrap(recipeParent);
+ });
+}
+
+/**
+ * Resolves when a specified number of forms have been processed.
+ */
+function promiseFormsProcessed(expectedCount = 1) {
+ var processedCount = 0;
+ return new Promise((resolve, reject) => {
+ function onProcessedForm(subject, topic, data) {
+ processedCount++;
+ if (processedCount == expectedCount) {
+ SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form");
+ resolve(SpecialPowers.Cu.waiveXrays(subject), data);
+ }
+ }
+ SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form", false);
+ });
+}
+
+function loadRecipes(recipes) {
+ info("Loading recipes");
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("loadedRecipes", function loaded() {
+ chromeScript.removeMessageListener("loadedRecipes", loaded);
+ resolve(recipes);
+ });
+ chromeScript.sendAsyncMessage("loadRecipes", recipes);
+ });
+}
+
+function resetRecipes() {
+ info("Resetting recipes");
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("recipesReset", function reset() {
+ chromeScript.removeMessageListener("recipesReset", reset);
+ resolve();
+ });
+ chromeScript.sendAsyncMessage("resetRecipes");
+ });
+}
+
+function promiseStorageChanged(expectedChangeTypes) {
+ return new Promise((resolve, reject) => {
+ function onStorageChanged({ topic, data }) {
+ let changeType = expectedChangeTypes.shift();
+ is(data, changeType, "Check expected passwordmgr-storage-changed type");
+ if (expectedChangeTypes.length === 0) {
+ chromeScript.removeMessageListener("storageChanged", onStorageChanged);
+ resolve();
+ }
+ }
+ chromeScript.addMessageListener("storageChanged", onStorageChanged);
+ });
+}
+
+function promisePromptShown(expectedTopic) {
+ return new Promise((resolve, reject) => {
+ function onPromptShown({ topic, data }) {
+ is(topic, expectedTopic, "Check expected prompt topic");
+ chromeScript.removeMessageListener("promptShown", onPromptShown);
+ resolve();
+ }
+ chromeScript.addMessageListener("promptShown", onPromptShown);
+ });
+}
+
+/**
+ * Run a function synchronously in the parent process and destroy it in the test cleanup function.
+ * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
+ * or the URL to a JS file.
+ * @return {Object} - the return value of loadChromeScript providing message-related methods.
+ * @see loadChromeScript in specialpowersAPI.js
+ */
+function runInParent(aFunctionOrURL) {
+ let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL);
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.destroy();
+ });
+ return chromeScript;
+}
+
+/**
+ * Run commonInit synchronously in the parent then run the test function after the runTests event.
+ *
+ * @param {Function} aFunction The test function to run
+ */
+function runChecksAfterCommonInit(aFunction = null) {
+ SimpleTest.waitForExplicitFinish();
+ let pwmgrCommonScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
+ if (aFunction) {
+ window.addEventListener("runTests", aFunction);
+ pwmgrCommonScript.addMessageListener("registerRunTests", () => registerRunTests());
+ }
+ pwmgrCommonScript.sendSyncMessage("setupParent");
+ return pwmgrCommonScript;
+}
+
+// Code to run when loaded as a chrome script in tests via loadChromeScript
+if (this.addMessageListener) {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ var SpecialPowers = { Cc, Ci, Cr, Cu, };
+ var ok, is;
+ // Ignore ok/is in commonInit since they aren't defined in a chrome script.
+ ok = is = () => {}; // eslint-disable-line no-native-reassign
+
+ Cu.import("resource://gre/modules/LoginHelper.jsm");
+ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+
+ function onStorageChanged(subject, topic, data) {
+ sendAsyncMessage("storageChanged", {
+ topic,
+ data,
+ });
+ }
+ Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed", false);
+
+ function onPrompt(subject, topic, data) {
+ sendAsyncMessage("promptShown", {
+ topic,
+ data,
+ });
+ }
+ Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change", false);
+ Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save", false);
+
+ addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
+ // Force LoginManagerParent to init for the tests since it's normally delayed
+ // by apps such as on Android.
+ LoginManagerParent.init();
+
+ commonInit(selfFilling);
+ sendAsyncMessage("doneSetup");
+ });
+
+ addMessageListener("loadRecipes", Task.async(function*(recipes) {
+ var recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.load(recipes);
+ sendAsyncMessage("loadedRecipes", recipes);
+ }));
+
+ addMessageListener("resetRecipes", Task.async(function*() {
+ let recipeParent = yield LoginManagerParent.recipeParentPromise;
+ yield recipeParent.reset();
+ sendAsyncMessage("recipesReset");
+ }));
+
+ addMessageListener("proxyLoginManager", msg => {
+ // Recreate nsILoginInfo objects from vanilla JS objects.
+ let recreatedArgs = msg.args.map((arg, index) => {
+ if (msg.loginInfoIndices.includes(index)) {
+ return LoginHelper.vanillaObjectToLogin(arg);
+ }
+
+ return arg;
+ });
+
+ let rv = Services.logins[msg.methodName](...recreatedArgs);
+ if (rv instanceof Ci.nsILoginInfo) {
+ rv = LoginHelper.loginToVanillaObject(rv);
+ }
+ return rv;
+ });
+
+ var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
+ globalMM.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
+ sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
+ });
+} else {
+ // Code to only run in the mochitest pages (not in the chrome script).
+ SpecialPowers.pushPrefEnv({"set": [["signon.autofillForms.http", true],
+ ["security.insecure_field_warning.contextual.enabled", false]]
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.popPrefEnv();
+ runInParent(function cleanupParent() {
+ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/LoginManagerParent.jsm");
+
+ // Remove all logins and disabled hosts
+ Services.logins.removeAllLogins();
+
+ let disabledHosts = Services.logins.getAllDisabledHosts();
+ disabledHosts.forEach(host => Services.logins.setLoginSavingEnabled(host, true));
+
+ let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
+ getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ if (LoginManagerParent._recipeManager) {
+ LoginManagerParent._recipeManager.reset();
+ }
+
+ // Cleanup PopupNotifications (if on a relevant platform)
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (chromeWin && chromeWin.PopupNotifications) {
+ let notes = chromeWin.PopupNotifications._currentNotifications;
+ if (notes.length > 0) {
+ dump("Removing " + notes.length + " popup notifications.\n");
+ }
+ for (let note of notes) {
+ note.remove();
+ }
+ }
+ });
+ });
+
+
+ let { LoginHelper } = SpecialPowers.Cu.import("resource://gre/modules/LoginHelper.jsm", {});
+ /**
+ * Proxy for Services.logins (nsILoginManager).
+ * Only supports arguments which support structured clone plus {nsILoginInfo}
+ * Assumes properties are methods.
+ */
+ this.LoginManager = new Proxy({}, {
+ get(target, prop, receiver) {
+ return (...args) => {
+ let loginInfoIndices = [];
+ let cloneableArgs = args.map((val, index) => {
+ if (SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)) {
+ loginInfoIndices.push(index);
+ return LoginHelper.loginToVanillaObject(val);
+ }
+
+ return val;
+ });
+
+ return chromeScript.sendSyncMessage("proxyLoginManager", {
+ args: cloneableArgs,
+ loginInfoIndices,
+ methodName: prop,
+ })[0][0];
+ };
+ },
+ });
+}
diff --git a/toolkit/components/passwordmgr/test/subtst_master_pass.html b/toolkit/components/passwordmgr/test/subtst_master_pass.html
new file mode 100644
index 0000000000..20211866ae
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_master_pass.html
@@ -0,0 +1,12 @@
+<h2>MP subtest</h2>
+This form triggers a MP and gets filled in.<br>
+<form>
+Username: <input type="text" id="userfield" name="u"><br>
+Password: <input type="password" id="passfield" name="p"><br>
+<script>
+ // Only notify when we fill in the password field.
+ document.getElementById("passfield").addEventListener("input", function() {
+ parent.postMessage("filled", "*");
+ });
+</script>
+</form>
diff --git a/toolkit/components/passwordmgr/test/subtst_prompt_async.html b/toolkit/components/passwordmgr/test/subtst_prompt_async.html
new file mode 100644
index 0000000000..f60f638145
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_prompt_async.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Multiple auth request</title>
+</head>
+<body>
+ <iframe id="iframe1" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=1&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+ <iframe id="iframe2" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=2&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+ <iframe id="iframe3" src="http://example.com/tests/toolkit/components/passwordmgr/test/authenticate.sjs?r=3&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/test_master_password.html b/toolkit/components/passwordmgr/test/test_master_password.html
new file mode 100644
index 0000000000..c8884811f7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_master_password.html
@@ -0,0 +1,308 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for master password</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: master password.
+<script>
+"use strict";
+
+commonInit();
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
+ .getService(SpecialPowers.Ci.nsILoginManager);
+var pwcrypt = SpecialPowers.Cc["@mozilla.org/login-manager/crypto/SDR;1"]
+ .getService(Ci.nsILoginManagerCrypto);
+
+var nsLoginInfo = new SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
+
+var exampleCom = "http://example.com/tests/toolkit/components/passwordmgr/test/";
+var exampleOrg = "http://example.org/tests/toolkit/components/passwordmgr/test/";
+
+var login1 = new nsLoginInfo();
+var login2 = new nsLoginInfo();
+
+login1.init("http://example.com", "http://example.com", null,
+ "user1", "pass1", "uname", "pword");
+login2.init("http://example.org", "http://example.org", null,
+ "user2", "pass2", "uname", "pword");
+
+pwmgr.addLogin(login1);
+pwmgr.addLogin(login2);
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+<iframe id="iframe1"></iframe>
+<iframe id="iframe2"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var testNum = 1;
+var iframe1 = document.getElementById("iframe1");
+var iframe2 = document.getElementById("iframe2");
+
+// A couple of tests have to wait until the password manager gets around to
+// filling in the password in the subtest (after we dismiss the master
+// password dialog). In order to accomplish this, the test waits for an event
+// and then posts a message back up to us telling us to continue.
+var continuation = null;
+addEventListener("message", () => {
+ if (continuation) {
+ var c = continuation;
+ continuation = null;
+ c();
+ }
+});
+
+/*
+ * handleDialog
+ *
+ * Invoked a short period of time after calling startCallbackTimer(), and
+ * allows testing the actual auth dialog while it's being displayed. Tests
+ * should call startCallbackTimer() each time the auth dialog is expected (the
+ * timer is a one-shot).
+ */
+function handleDialog(doc, testNumber) {
+ ok(true, "handleDialog running for test " + testNumber);
+
+ var clickOK = true;
+ var doNothing = false;
+ var passfield = doc.getElementById("password1Textbox");
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNumber) {
+ case 1:
+ is(passfield.getAttribute("value"), "", "Checking empty prompt");
+ passfield.setAttribute("value", masterPassword);
+ is(passfield.getAttribute("value"), masterPassword, "Checking filled prompt");
+ break;
+
+ case 2:
+ clickOK = false;
+ break;
+
+ case 3:
+ is(passfield.getAttribute("value"), "", "Checking empty prompt");
+ passfield.setAttribute("value", masterPassword);
+ break;
+
+ case 4:
+ doNothing = true;
+ break;
+
+ case 5:
+ is(passfield.getAttribute("value"), "", "Checking empty prompt");
+ passfield.setAttribute("value", masterPassword);
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNumber);
+ break;
+ }
+
+ didDialog = true;
+
+ if (!doNothing) {
+ SpecialPowers.addObserver(outerWindowObserver, "outer-window-destroyed", false);
+ if (clickOK)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ }
+
+ ok(true, "handleDialog done for test " + testNumber);
+
+ if (testNumber == 4)
+ checkTest4A();
+}
+
+var outerWindowObserver = {
+ observe: function(id) {
+ SpecialPowers.removeObserver(outerWindowObserver, "outer-window-destroyed");
+ var func;
+ if (testNum == 1)
+ func = startTest2;
+ else if (testNum == 2)
+ func = startTest3;
+
+ // For tests 3 and 4C, we use the 'continuation' mechanism, described
+ // above.
+ if (func)
+ setTimeout(func, 300);
+ }
+};
+
+
+function startTest1() {
+ ok(pwcrypt.isLoggedIn, "should be initially logged in (no MP)");
+ enableMasterPassword();
+ ok(!pwcrypt.isLoggedIn, "should be logged out after setting MP");
+
+ // --- Test 1 ---
+ // Trigger a MP prompt via the API
+ startCallbackTimer();
+ var logins = pwmgr.getAllLogins();
+ ok(didDialog, "handleDialog was invoked");
+ is(logins.length, 3, "expected number of logins");
+
+ ok(pwcrypt.isLoggedIn, "should be logged in after MP prompt");
+ logoutMasterPassword();
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+}
+
+function startTest2() {
+ // Try again but click cancel.
+ testNum++;
+ startCallbackTimer();
+ var failedAsExpected = false;
+ logins = null;
+ try {
+ logins = pwmgr.getAllLogins();
+ } catch (e) { failedAsExpected = true; }
+ ok(didDialog, "handleDialog was invoked");
+ ok(failedAsExpected, "getAllLogins should have thrown");
+ is(logins, null, "shouldn't have gotten logins");
+ ok(!pwcrypt.isLoggedIn, "should still be logged out");
+}
+
+function startTest3() {
+ // Load a single iframe to trigger a MP
+ testNum++;
+ iframe1.src = exampleCom + "subtst_master_pass.html";
+ continuation = checkTest3;
+ startCallbackTimer();
+}
+
+function checkTest3() {
+ ok(true, "checkTest3 starting");
+ ok(didDialog, "handleDialog was invoked");
+
+ // check contents of iframe1 fields
+ var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
+ is(u.value, "user1", "checking expected user to have been filled in");
+ is(p.value, "pass1", "checking expected pass to have been filled in");
+
+ ok(pwcrypt.isLoggedIn, "should be logged in");
+ logoutMasterPassword();
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+
+
+ // --- Test 4 ---
+ // first part of loading 2 MP-triggering iframes
+ testNum++;
+ iframe1.src = exampleOrg + "subtst_master_pass.html";
+ // start the callback, but we'll not enter the MP, just call checkTest4A
+ startCallbackTimer();
+}
+
+function checkTest4A() {
+ ok(true, "checkTest4A starting");
+ ok(didDialog, "handleDialog was invoked");
+
+ // check contents of iframe1 fields
+ var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
+ is(u.value, "", "checking expected empty user");
+ is(p.value, "", "checking expected empty pass");
+
+
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+
+ // XXX check that there's 1 MP window open
+
+ // Load another iframe with a login form
+ // This should detect that there's already a pending MP prompt, and not
+ // put up a second one. The load event will fire (note that when pwmgr is
+ // driven from DOMContentLoaded, if that blocks due to prompting for a MP,
+ // the load even will also be blocked until the prompt is dismissed).
+ iframe2.onload = checkTest4B_delay;
+ iframe2.src = exampleCom + "subtst_master_pass.html";
+}
+
+function checkTest4B_delay() {
+ // Testing a negative, wait a little to give the login manager a chance to
+ // (incorrectly) fill in the form. Note, we cannot use setTimeout()
+ // here because the modal window suspends all window timers. Instead we
+ // must use a chrome script to use nsITimer directly.
+ let chromeURL = SimpleTest.getTestFileURL("chrome_timeout.js");
+ let script = SpecialPowers.loadChromeScript(chromeURL);
+ script.addMessageListener('ready', _ => {
+ script.sendAsyncMessage('setTimeout', { delay: 500 });
+ });
+ script.addMessageListener('timeout', checkTest4B);
+}
+
+function checkTest4B() {
+ ok(true, "checkTest4B starting");
+ // iframe2 should load without having triggered a MP prompt (because one
+ // is already waiting)
+
+ // check contents of iframe2 fields
+ var u = SpecialPowers.wrap(iframe2).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe2).contentDocument.getElementById("passfield");
+ is(u.value, "", "checking expected empty user");
+ is(p.value, "", "checking expected empty pass");
+
+ // XXX check that there's 1 MP window open
+ ok(!pwcrypt.isLoggedIn, "should be logged out");
+
+ continuation = checkTest4C;
+
+ // Ok, now enter the MP. The MP prompt is already up, but we'll just reuse startCallBackTimer.
+ // --- Test 5 ---
+ testNum++;
+ startCallbackTimer();
+}
+
+function checkTest4C() {
+ ok(true, "checkTest4C starting");
+ ok(didDialog, "handleDialog was invoked");
+
+ // We shouldn't have to worry about iframe1's load event racing with
+ // filling of iframe2's data. We notify observers synchronously, so
+ // iframe2's observer will process iframe2 before iframe1 even finishes
+ // processing the form (which is blocking its load event).
+ ok(pwcrypt.isLoggedIn, "should be logged in");
+
+ // check contents of iframe1 fields
+ var u = SpecialPowers.wrap(iframe1).contentDocument.getElementById("userfield");
+ var p = SpecialPowers.wrap(iframe1).contentDocument.getElementById("passfield");
+ is(u.value, "user2", "checking expected user to have been filled in");
+ is(p.value, "pass2", "checking expected pass to have been filled in");
+
+ // check contents of iframe2 fields
+ u = SpecialPowers.wrap(iframe2).contentDocument.getElementById("userfield");
+ p = SpecialPowers.wrap(iframe2).contentDocument.getElementById("passfield");
+ is(u.value, "user1", "checking expected user to have been filled in");
+ is(p.value, "pass1", "checking expected pass to have been filled in");
+
+ SimpleTest.finish();
+}
+
+// XXX do a test5ABC with clicking cancel?
+
+SimpleTest.registerCleanupFunction(function finishTest() {
+ disableMasterPassword();
+
+ pwmgr.removeLogin(login1);
+ pwmgr.removeLogin(login2);
+});
+
+window.addEventListener("runTests", startTest1);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/passwordmgr/test/test_prompt_async.html b/toolkit/components/passwordmgr/test/test_prompt_async.html
new file mode 100644
index 0000000000..38b34679aa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_prompt_async.html
@@ -0,0 +1,540 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Async Auth Prompt</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("untriaged");
+
+ const { NetUtil } = SpecialPowers.Cu.import('resource://gre/modules/NetUtil.jsm');
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+ // Class monitoring number of open dialog windows
+ // It checks there is always open just a single dialog per application
+ function dialogMonitor() {
+ var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ observerService.addObserver(this, "domwindowopened", false);
+ observerService.addObserver(this, "domwindowclosed", false);
+ }
+
+ /*
+ * As documented in Bug 718543, checking equality of objects pulled
+ * from SpecialPowers-wrapped objects is unreliable. Because of that,
+ * `dialogMonitor` now tracks the number of open windows rather than
+ * specific window objects.
+ *
+ * NB: Because the constructor (above) adds |this| directly as an observer,
+ * we need to do SpecialPowers.wrapCallbackObject directly on the prototype.
+ */
+ dialogMonitor.prototype = SpecialPowers.wrapCallbackObject({
+ windowsOpen : 0,
+ windowsRegistered : 0,
+
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIObserver, Ci.nsISupports];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ observe: function(subject, topic, data) {
+ if (topic === "domwindowopened") {
+ this.windowsOpen++;
+ this.windowsRegistered++;
+ return;
+ }
+ if (topic === "domwindowclosed") {
+ this.windowsOpen--;
+ return;
+ }
+ },
+
+ shutdown: function() {
+ var observerService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ observerService.removeObserver(this, "domwindowopened");
+ observerService.removeObserver(this, "domwindowclosed");
+ },
+
+ reset: function() {
+ this.windowsOpen = 0;
+ this.windowsRegistered = 0;
+ }
+ });
+
+ var monitor = new dialogMonitor();
+
+ var pwmgr, logins = [];
+
+ function initLogins(pi) {
+ pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
+ .getService(Ci.nsILoginManager);
+
+ function addLogin(host, realm, user, pass) {
+ var login = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ login.init(host, null, realm, user, pass, "", "");
+ pwmgr.addLogin(login);
+ logins.push(login);
+ }
+
+ var mozproxy = "moz-proxy://" +
+ SpecialPowers.wrap(pi).host + ":" +
+ SpecialPowers.wrap(pi).port;
+
+ addLogin(mozproxy, "proxy_realm",
+ "proxy_user", "proxy_pass");
+ addLogin(mozproxy, "proxy_realm2",
+ "proxy_user2", "proxy_pass2");
+ addLogin(mozproxy, "proxy_realm3",
+ "proxy_user3", "proxy_pass3");
+ addLogin(mozproxy, "proxy_realm4",
+ "proxy_user4", "proxy_pass4");
+ addLogin(mozproxy, "proxy_realm5",
+ "proxy_user5", "proxy_pass5");
+ addLogin("http://example.com", "mochirealm",
+ "user1name", "user1pass");
+ addLogin("http://example.org", "mochirealm2",
+ "user2name", "user2pass");
+ addLogin("http://example.com", "mochirealm3",
+ "user3name", "user3pass");
+ addLogin("http://example.com", "mochirealm4",
+ "user4name", "user4pass");
+ addLogin("http://example.com", "mochirealm5",
+ "user5name", "user5pass");
+ addLogin("http://example.com", "mochirealm6",
+ "user6name", "user6pass");
+ }
+
+ function finishTest() {
+ ok(true, "finishTest removing testing logins...");
+ for (i in logins)
+ pwmgr.removeLogin(logins[i]);
+
+ var authMgr = SpecialPowers.Cc['@mozilla.org/network/http-auth-manager;1']
+ .getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ monitor.shutdown();
+ SimpleTest.finish();
+ }
+
+ var resolveCallback = SpecialPowers.wrapCallbackObject({
+ QueryInterface : function (iid) {
+ const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
+
+ if (!interfaces.some( function(v) { return iid.equals(v); } ))
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ onProxyAvailable : function (req, uri, pi, status) {
+ initLogins(pi);
+ doTest(testNum);
+ }
+ });
+
+ function startup() {
+ // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
+ var channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true
+ });
+
+ var pps = SpecialPowers.Cc["@mozilla.org/network/protocol-proxy-service;1"]
+ .getService();
+
+ pps.asyncResolve(channel, 0, resolveCallback);
+ }
+
+ // --------------- Test loop spin ----------------
+ var testNum = 1;
+ var iframe1;
+ var iframe2a;
+ var iframe2b;
+ window.onload = function () {
+ iframe1 = document.getElementById("iframe1");
+ iframe2a = document.getElementById("iframe2a");
+ iframe2b = document.getElementById("iframe2b");
+ iframe1.onload = onFrameLoad;
+ iframe2a.onload = onFrameLoad;
+ iframe2b.onload = onFrameLoad;
+
+ startup();
+ };
+
+ var expectedLoads;
+ var expectedDialogs;
+ function onFrameLoad()
+ {
+ if (--expectedLoads == 0) {
+ // All pages expected to load has loaded, continue with the next test
+ ok(true, "Expected frames loaded");
+
+ doCheck(testNum);
+ monitor.reset();
+
+ testNum++;
+ doTest(testNum);
+ }
+ }
+
+ function doTest(testNumber)
+ {
+ /*
+ * These contentDocument variables are located here,
+ * rather than in the global scope, because SpecialPowers threw
+ * errors (complaining that the objects were deleted)
+ * when these were in the global scope.
+ */
+ var iframe1Doc = SpecialPowers.wrap(iframe1).contentDocument;
+ var iframe2aDoc = SpecialPowers.wrap(iframe2a).contentDocument;
+ var iframe2bDoc = SpecialPowers.wrap(iframe2b).contentDocument;
+ var exampleCom = "http://example.com/tests/toolkit/components/passwordmgr/test/";
+ var exampleOrg = "http://example.org/tests/toolkit/components/passwordmgr/test/";
+
+ switch (testNumber)
+ {
+ case 1:
+ // Load through a single proxy with authentication required 3 different
+ // pages, first with one login, other two with their own different login.
+ // We expect to show just a single dialog for proxy authentication and
+ // then two dialogs to authenticate to login 1 and then login 2.
+ ok(true, "doTest testNum 1");
+ expectedLoads = 3;
+ expectedDialogs = 3;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "r=1&" +
+ "user=user1name&" +
+ "pass=user1pass&" +
+ "realm=mochirealm&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ iframe2a.src = exampleOrg + "authenticate.sjs?" +
+ "r=2&" +
+ "user=user2name&" +
+ "pass=user2pass&" +
+ "realm=mochirealm2&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ iframe2b.src = exampleOrg + "authenticate.sjs?" +
+ "r=3&" +
+ "user=user2name&" +
+ "pass=user2pass&" +
+ "realm=mochirealm2&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ break;
+
+ case 2:
+ // Load an iframe with 3 subpages all requiring the same login through
+ // anuthenticated proxy. We expect 2 dialogs, proxy authentication
+ // and web authentication.
+ ok(true, "doTest testNum 2");
+ expectedLoads = 3;
+ expectedDialogs = 2;
+ iframe1.src = exampleCom + "subtst_prompt_async.html";
+ iframe2a.src = "about:blank";
+ iframe2b.src = "about:blank";
+ break;
+
+ case 3:
+ // Load in the iframe page through unauthenticated proxy
+ // and discard the proxy authentication. We expect to see
+ // unauthenticated page content and just a single dialog.
+ ok(true, "doTest testNum 3");
+ expectedLoads = 1;
+ expectedDialogs = 1;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user4name&" +
+ "pass=user4pass&" +
+ "realm=mochirealm4&" +
+ "proxy_user=proxy_user3&" +
+ "proxy_pass=proxy_pass3&" +
+ "proxy_realm=proxy_realm3";
+ break;
+
+ case 4:
+ // Reload the frame from previous step and pass the proxy authentication
+ // but cancel the WWW authentication. We should get the proxy=ok and WWW=fail
+ // content as a result.
+ ok(true, "doTest testNum 4");
+ expectedLoads = 1;
+ expectedDialogs = 2;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user4name&" +
+ "pass=user4pass&" +
+ "realm=mochirealm4&" +
+ "proxy_user=proxy_user3&" +
+ "proxy_pass=proxy_pass3&" +
+ "proxy_realm=proxy_realm3";
+
+
+ break;
+
+ case 5:
+ // Same as the previous two steps but let the server generate
+ // huge content load to check http channel is capable to handle
+ // case when auth dialog is canceled or accepted before unauthenticated
+ // content data is load from the server. (This would be better to
+ // implement using delay of server response).
+ ok(true, "doTest testNum 5");
+ expectedLoads = 1;
+ expectedDialogs = 1;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user5name&" +
+ "pass=user5pass&" +
+ "realm=mochirealm5&" +
+ "proxy_user=proxy_user4&" +
+ "proxy_pass=proxy_pass4&" +
+ "proxy_realm=proxy_realm4&" +
+ "huge=1";
+ break;
+
+ case 6:
+ // Reload the frame from the previous step and let the proxy
+ // authentication pass but WWW fail. We expect two dialogs
+ // and an unathenticated page content load.
+ ok(true, "doTest testNum 6");
+ expectedLoads = 1;
+ expectedDialogs = 2;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user5name&" +
+ "pass=user5pass&" +
+ "realm=mochirealm5&" +
+ "proxy_user=proxy_user4&" +
+ "proxy_pass=proxy_pass4&" +
+ "proxy_realm=proxy_realm4&" +
+ "huge=1";
+ break;
+
+ case 7:
+ // Reload again and let pass all authentication dialogs.
+ // Check we get the authenticated content not broken by
+ // the unauthenticated content.
+ ok(true, "doTest testNum 7");
+ expectedLoads = 1;
+ expectedDialogs = 1;
+ iframe1Doc.location.reload();
+ break;
+
+ case 8:
+ // Check we proccess all challenges sent by server when
+ // user cancels prompts
+ ok(true, "doTest testNum 8");
+ expectedLoads = 1;
+ expectedDialogs = 5;
+ iframe1.src = exampleCom + "authenticate.sjs?" +
+ "user=user6name&" +
+ "pass=user6pass&" +
+ "realm=mochirealm6&" +
+ "proxy_user=proxy_user5&" +
+ "proxy_pass=proxy_pass5&" +
+ "proxy_realm=proxy_realm5&" +
+ "huge=1&" +
+ "multiple=3";
+ break;
+
+ case 9:
+ finishTest();
+ return;
+ }
+
+ startCallbackTimer();
+ }
+
+ function handleDialog(doc, testNumber)
+ {
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNumber)
+ {
+ case 1:
+ case 2:
+ dialog.acceptDialog();
+ break;
+
+ case 3:
+ dialog.cancelDialog();
+ setTimeout(onFrameLoad, 10); // there are no successful frames for test 3
+ break;
+
+ case 4:
+ if (expectedDialogs == 2)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ break;
+
+ case 5:
+ dialog.cancelDialog();
+ setTimeout(onFrameLoad, 10); // there are no successful frames for test 5
+ break;
+
+ case 6:
+ if (expectedDialogs == 2)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ break;
+
+ case 7:
+ dialog.acceptDialog();
+ break;
+
+ case 8:
+ if (expectedDialogs == 3 || expectedDialogs == 1)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+ break;
+
+ default:
+ ok(false, "Unhandled testNum " + testNumber + " in handleDialog");
+ }
+
+ if (--expectedDialogs > 0)
+ startCallbackTimer();
+ }
+
+ function doCheck(testNumber)
+ {
+ var iframe1Doc = SpecialPowers.wrap(iframe1).contentDocument;
+ var iframe2aDoc = SpecialPowers.wrap(iframe2a).contentDocument;
+ var iframe2bDoc = SpecialPowers.wrap(iframe2b).contentDocument;
+ var authok1;
+ var proxyok1;
+ var footnote;
+ switch (testNumber)
+ {
+ case 1:
+ ok(true, "doCheck testNum 1");
+ is(monitor.windowsRegistered, 3, "Registered 3 open dialogs");
+
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+
+ var authok2a = iframe2aDoc.getElementById("ok").textContent;
+ var proxyok2a = iframe2aDoc.getElementById("proxy").textContent;
+
+ var authok2b = iframe2bDoc.getElementById("ok").textContent;
+ var proxyok2b = iframe2bDoc.getElementById("proxy").textContent;
+
+ is(authok1, "PASS", "WWW Authorization OK, frame1");
+ is(authok2a, "PASS", "WWW Authorization OK, frame2a");
+ is(authok2b, "PASS", "WWW Authorization OK, frame2b");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(proxyok2a, "PASS", "Proxy Authorization OK, frame2a");
+ is(proxyok2b, "PASS", "Proxy Authorization OK, frame2b");
+ break;
+
+ case 2:
+ is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
+ ok(true, "doCheck testNum 2");
+
+ function checkIframe(frame) {
+ var doc = SpecialPowers.wrap(frame).contentDocument;
+
+ var authok = doc.getElementById("ok").textContent;
+ var proxyok = doc.getElementById("proxy").textContent;
+
+ is(authok, "PASS", "WWW Authorization OK, " + frame.id);
+ is(proxyok, "PASS", "Proxy Authorization OK, " + frame.id);
+ }
+
+ checkIframe(iframe1Doc.getElementById("iframe1"));
+ checkIframe(iframe1Doc.getElementById("iframe2"));
+ checkIframe(iframe1Doc.getElementById("iframe3"));
+ break;
+
+ case 3:
+ ok(true, "doCheck testNum 3");
+ is(monitor.windowsRegistered, 1, "Registered 1 open dialog");
+
+ // ensure that the page content is not displayed on failed proxy auth
+ is(iframe1Doc.getElementById("ok"), null, "frame did not load");
+ break;
+
+ case 4:
+ ok(true, "doCheck testNum 4");
+ is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+
+ is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ break;
+
+ case 5:
+ ok(true, "doCheck testNum 5");
+ is(monitor.windowsRegistered, 1, "Registered 1 open dialog");
+
+ // ensure that the page content is not displayed on failed proxy auth
+ is(iframe1Doc.getElementById("footnote"), null, "frame did not load");
+ break;
+
+ case 6:
+ ok(true, "doCheck testNum 6");
+ is(monitor.windowsRegistered, 2, "Registered 2 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+ footnote = iframe1Doc.getElementById("footnote").textContent;
+
+ is(authok1, "FAIL", "WWW Authorization FAILED, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ break;
+
+ case 7:
+ ok(true, "doCheck testNum 7");
+ is(monitor.windowsRegistered, 1, "Registered 1 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+ footnote = iframe1Doc.getElementById("footnote").textContent;
+
+ is(authok1, "PASS", "WWW Authorization OK, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ break;
+
+ case 8:
+ ok(true, "doCheck testNum 8");
+ is(monitor.windowsRegistered, 5, "Registered 5 open dialogs");
+ authok1 = iframe1Doc.getElementById("ok").textContent;
+ proxyok1 = iframe1Doc.getElementById("proxy").textContent;
+ footnote = iframe1Doc.getElementById("footnote").textContent;
+
+ is(authok1, "PASS", "WWW Authorization OK, frame1");
+ is(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ is(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ break;
+
+ default:
+ ok(false, "Unhandled testNum " + testNumber + " in doCheck");
+ }
+ }
+
+ </script>
+</head>
+<body>
+ <iframe id="iframe1"></iframe>
+ <iframe id="iframe2a"></iframe>
+ <iframe id="iframe2b"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/test_xhr.html b/toolkit/components/passwordmgr/test/test_xhr.html
new file mode 100644
index 0000000000..2963716852
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_xhr.html
@@ -0,0 +1,201 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for XHR prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: XHR prompt
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: XHR prompts. **/
+var pwmgr, login1, login2;
+
+function initLogins() {
+ pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+
+ login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", null, "xhr",
+ "xhruser1", "xhrpass1", "", "");
+ login2.init("http://mochi.test:8888", null, "xhr2",
+ "xhruser2", "xhrpass2", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2);
+}
+
+function finishTest() {
+ ok(true, "finishTest removing testing logins...");
+ pwmgr.removeLogin(login1);
+ pwmgr.removeLogin(login2);
+
+ SimpleTest.finish();
+}
+
+function handleDialog(doc, testNum) {
+ ok(true, "handleDialog running for test " + testNum);
+
+ var clickOK = true;
+ var userfield = doc.getElementById("loginTextbox");
+ var passfield = doc.getElementById("password1Textbox");
+ var username = userfield.getAttribute("value");
+ var password = passfield.getAttribute("value");
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNum) {
+ case 1:
+ is(username, "xhruser1", "Checking provided username");
+ is(password, "xhrpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xhruser2", "Checking provided username");
+ is(password, "xhrpass2", "Checking provided password");
+
+ // Check that the dialog is modal, chrome and dependent;
+ // We can't just check window.opener because that'll be
+ // a content window, which therefore isn't exposed (it'll lie and
+ // be null).
+ var win = doc.defaultView;
+ var Ci = SpecialPowers.Ci;
+ var treeOwner = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner;
+ treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
+ var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+ var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+ info("Flags: " + flags);
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
+ "Dialog should be opened as chrome");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
+ "Dialog should be opened as a dialog");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
+ "Dialog should be opened as dependent.");
+ ok(wbc.isWindowModal(), "Dialog should be modal");
+
+ // Check that the right tab is focused:
+ var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
+ var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+ ok(spec.startsWith("http://mochi.test:8888"),
+ "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
+
+
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ // Explicitly cancel the dialog and report a fail in this failure
+ // case, rather than letting the dialog get stuck due to an auth
+ // failure and having the test timeout.
+ if (!username && !password) {
+ ok(false, "No values prefilled");
+ clickOK = false;
+ }
+
+ if (clickOK)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+
+ ok(true, "handleDialog done");
+ didDialog = true;
+}
+
+var newWin;
+function xhrLoad(xmlDoc) {
+ ok(true, "xhrLoad running for test " + testNum);
+
+ // The server echos back the user/pass it received.
+ var username = xmlDoc.getElementById("user").textContent;
+ var password = xmlDoc.getElementById("pass").textContent;
+ var authok = xmlDoc.getElementById("ok").textContent;
+
+
+ switch (testNum) {
+ case 1:
+ is(username, "xhruser1", "Checking provided username");
+ is(password, "xhrpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xhruser2", "Checking provided username");
+ is(password, "xhrpass2", "Checking provided password");
+
+ newWin.close();
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ doTest();
+}
+
+function doTest() {
+ switch (++testNum) {
+ case 1:
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr");
+ break;
+
+ case 2:
+ // Test correct parenting, by opening another tab in the foreground,
+ // and making sure the prompt re-focuses the original tab when shown:
+ newWin = window.open();
+ newWin.focus();
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2");
+ break;
+
+ default:
+ finishTest();
+ }
+}
+
+function makeRequest(uri) {
+ var request = new XMLHttpRequest();
+ request.open("GET", uri, true);
+ request.onreadystatechange = function () {
+ if (request.readyState == 4)
+ xhrLoad(request.responseXML);
+ };
+ request.send(null);
+}
+
+
+initLogins();
+
+// clear plain HTTP auth sessions before the test, to allow
+// running them more than once.
+var authMgr = SpecialPowers.Cc['@mozilla.org/network/http-auth-manager;1']
+ .getService(SpecialPowers.Ci.nsIHttpAuthManager);
+authMgr.clearAll();
+
+// start the tests
+testNum = 0;
+doTest();
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/test_xml_load.html b/toolkit/components/passwordmgr/test/test_xml_load.html
new file mode 100644
index 0000000000..5672c7117f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/test_xml_load.html
@@ -0,0 +1,191 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test XML document prompts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: XML prompt
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: XML prompts. **/
+var pwmgr, login1, login2;
+
+function initLogins() {
+ pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
+ .getService(Ci.nsILoginManager);
+
+ login1 = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+ login2 = SpecialPowers.Cc["@mozilla.org/login-manager/loginInfo;1"]
+ .createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", null, "xml",
+ "xmluser1", "xmlpass1", "", "");
+ login2.init("http://mochi.test:8888", null, "xml2",
+ "xmluser2", "xmlpass2", "", "");
+
+ pwmgr.addLogin(login1);
+ pwmgr.addLogin(login2);
+}
+
+function handleDialog(doc, testNum) {
+ ok(true, "handleDialog running for test " + testNum);
+
+ var clickOK = true;
+ var userfield = doc.getElementById("loginTextbox");
+ var passfield = doc.getElementById("password1Textbox");
+ var username = userfield.getAttribute("value");
+ var password = passfield.getAttribute("value");
+ var dialog = doc.getElementById("commonDialog");
+
+ switch (testNum) {
+ case 1:
+ is(username, "xmluser1", "Checking provided username");
+ is(password, "xmlpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xmluser2", "Checking provided username");
+ is(password, "xmlpass2", "Checking provided password");
+
+ // Check that the dialog is modal, chrome and dependent;
+ // We can't just check window.opener because that'll be
+ // a content window, which therefore isn't exposed (it'll lie and
+ // be null).
+ var win = doc.defaultView;
+ var Ci = SpecialPowers.Ci;
+ var treeOwner = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner;
+ treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
+ var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+ var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+ info("Flags: " + flags);
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
+ "Dialog should be opened as chrome");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
+ "Dialog should be opened as a dialog");
+ ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
+ "Dialog should be opened as dependent.");
+ ok(wbc.isWindowModal(), "Dialog should be modal");
+
+ // Check that the right tab is focused:
+ var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
+ var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+ ok(spec.startsWith("http://mochi.test:8888"),
+ "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
+
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ // Explicitly cancel the dialog and report a fail in this failure
+ // case, rather than letting the dialog get stuck due to an auth
+ // failure and having the test timeout.
+ if (!username && !password) {
+ ok(false, "No values prefilled");
+ clickOK = false;
+ }
+
+ if (clickOK)
+ dialog.acceptDialog();
+ else
+ dialog.cancelDialog();
+
+ ok(true, "handleDialog done");
+ didDialog = true;
+}
+
+var newWin;
+function xmlLoad(responseDoc) {
+ ok(true, "xmlLoad running for test " + testNum);
+
+ // The server echos back the user/pass it received.
+ var username = responseDoc.getElementById("user").textContent;
+ var password = responseDoc.getElementById("pass").textContent;
+ var authok = responseDoc.getElementById("ok").textContent;
+
+ switch (testNum) {
+ case 1:
+ is(username, "xmluser1", "Checking provided username");
+ is(password, "xmlpass1", "Checking provided password");
+ break;
+
+ case 2:
+ is(username, "xmluser2", "Checking provided username");
+ is(password, "xmlpass2", "Checking provided password");
+
+ newWin.close();
+ break;
+
+ default:
+ ok(false, "Uhh, unhandled switch for testNum #" + testNum);
+ break;
+ }
+
+ doTest();
+}
+
+function doTest() {
+ switch (++testNum) {
+ case 1:
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xmluser1&pass=xmlpass1&realm=xml");
+ break;
+
+ case 2:
+ // Test correct parenting, by opening another tab in the foreground,
+ // and making sure the prompt re-focuses the original tab when shown:
+ newWin = window.open();
+ newWin.focus();
+ startCallbackTimer();
+ makeRequest("authenticate.sjs?user=xmluser2&pass=xmlpass2&realm=xml2");
+ break;
+
+ default:
+ SimpleTest.finish();
+ }
+}
+
+function makeRequest(uri) {
+ var xmlDoc = document.implementation.createDocument("", "test", null);
+
+ function documentLoaded(e) {
+ xmlLoad(xmlDoc);
+ }
+ xmlDoc.addEventListener("load", documentLoaded, false);
+ xmlDoc.load(uri);
+}
+
+
+initLogins();
+
+// clear plain HTTP auth sessions before the test, to allow
+// running them more than once.
+var authMgr = SpecialPowers.Cc['@mozilla.org/network/http-auth-manager;1']
+ .getService(SpecialPowers.Ci.nsIHttpAuthManager);
+authMgr.clearAll();
+
+// start the tests
+testNum = 0;
+doTest();
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/unit/.eslintrc.js b/toolkit/components/passwordmgr/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite b/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/corruptDB.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/key3.db b/toolkit/components/passwordmgr/test/unit/data/key3.db
new file mode 100644
index 0000000000..a83a0a577b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/key3.db
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite
new file mode 100644
index 0000000000..fe030b61fd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v1.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite
new file mode 100644
index 0000000000..729512a12b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v1v2.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite
new file mode 100644
index 0000000000..a6c72b31e8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v2.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite
new file mode 100644
index 0000000000..359df5d311
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v2v3.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite
new file mode 100644
index 0000000000..918f4142fe
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v3.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite
new file mode 100644
index 0000000000..e06c33aae3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v3v4.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite
new file mode 100644
index 0000000000..227c09c816
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v4.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite
new file mode 100644
index 0000000000..4534cf2553
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v4v5.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite
new file mode 100644
index 0000000000..eb4ee6d01e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v5v6.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite
new file mode 100644
index 0000000000..e09c4f7100
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v999-2.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite b/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite
new file mode 100644
index 0000000000..0328a1a02a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/data/signons-v999.sqlite
Binary files differ
diff --git a/toolkit/components/passwordmgr/test/unit/head.js b/toolkit/components/passwordmgr/test/unit/head.js
new file mode 100644
index 0000000000..baf958ab40
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/head.js
@@ -0,0 +1,135 @@
+/**
+ * Provides infrastructure for automated login components tests.
+ */
+
+"use strict";
+
+// Globals
+
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoginRecipes.jsm");
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+Cu.import("resource://testing-common/MockDocument.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+const LoginInfo =
+ Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo", "init");
+
+// Import LoginTestUtils.jsm as LoginTestUtils.
+XPCOMUtils.defineLazyModuleGetter(this, "LoginTestUtils",
+ "resource://testing-common/LoginTestUtils.jsm");
+LoginTestUtils.Assert = Assert;
+const TestData = LoginTestUtils.testData;
+const newPropertyBag = LoginHelper.newPropertyBag;
+
+/**
+ * All the tests are implemented with add_task, this starts them automatically.
+ */
+function run_test()
+{
+ do_get_profile();
+ run_next_test();
+}
+
+// Global helpers
+
+// Some of these functions are already implemented in other parts of the source
+// tree, see bug 946708 about sharing more code.
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system. Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function () {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+const RecipeHelpers = {
+ initNewParent() {
+ return (new LoginRecipesParent({ defaults: null })).initializationPromise;
+ },
+};
+
+// Initialization functions common to all tests
+
+add_task(function* test_common_initialize()
+{
+ // Before initializing the service for the first time, we should copy the key
+ // file required to decrypt the logins contained in the SQLite databases used
+ // by migration tests. This file is not required for the other tests.
+ yield OS.File.copy(do_get_file("data/key3.db").path,
+ OS.Path.join(OS.Constants.Path.profileDir, "key3.db"));
+
+ // Ensure that the service and the storage module are initialized.
+ yield Services.logins.initializationPromise;
+
+ // Ensure that every test file starts with an empty database.
+ LoginTestUtils.clearData();
+
+ // Clean up after every test.
+ do_register_cleanup(() => LoginTestUtils.clearData());
+});
+
+/**
+ * Compare two FormLike to see if they represent the same information. Elements
+ * are compared using their @id attribute.
+ */
+function formLikeEqual(a, b) {
+ Assert.strictEqual(Object.keys(a).length, Object.keys(b).length,
+ "Check the formLikes have the same number of properties");
+
+ for (let propName of Object.keys(a)) {
+ if (propName == "elements") {
+ Assert.strictEqual(a.elements.length, b.elements.length, "Check element count");
+ for (let i = 0; i < a.elements.length; i++) {
+ Assert.strictEqual(a.elements[i].id, b.elements[i].id, "Check element " + i + " id");
+ }
+ continue;
+ }
+ Assert.strictEqual(a[propName], b[propName], "Compare formLike " + propName + " property");
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js b/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js
new file mode 100644
index 0000000000..94d2e50c05
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_OSCrypto_win.js
@@ -0,0 +1,75 @@
+/**
+ * Tests the OSCrypto object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
+ "resource://gre/modules/OSCrypto.jsm");
+
+var crypto = new OSCrypto();
+
+// Tests
+
+add_task(function test_getIELoginHash()
+{
+ do_check_eq(crypto.getIELoginHash("https://bugzilla.mozilla.org/page.cgi"),
+ "4A66FE96607885790F8E67B56EEE52AB539BAFB47D");
+
+ do_check_eq(crypto.getIELoginHash("https://github.com/login"),
+ "0112F7DCE67B8579EA01367678AA44AB9868B5A143");
+
+ do_check_eq(crypto.getIELoginHash("https://login.live.com/login.srf"),
+ "FBF92E5D804C82717A57856533B779676D92903688");
+
+ do_check_eq(crypto.getIELoginHash("https://preview.c9.io/riadh/w1/pass.1.html"),
+ "6935CF27628830605927F86AB53831016FC8973D1A");
+
+
+ do_check_eq(crypto.getIELoginHash("https://reviewboard.mozilla.org/account/login/"),
+ "09141FD287E2E59A8B1D3BB5671537FD3D6B61337A");
+
+ do_check_eq(crypto.getIELoginHash("https://www.facebook.com/"),
+ "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796");
+
+});
+
+add_task(function test_decryptData_encryptData()
+{
+ function decryptEncryptTest(key) {
+ do_check_eq(crypto.decryptData(crypto.encryptData("", key), key),
+ "");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("secret", key), key),
+ "secret");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("https://www.mozilla.org", key),
+ key),
+ "https://www.mozilla.org");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("https://reviewboard.mozilla.org", key),
+ key),
+ "https://reviewboard.mozilla.org");
+
+ do_check_eq(crypto.decryptData(crypto.encryptData("https://bugzilla.mozilla.org/page.cgi",
+ key),
+ key),
+ "https://bugzilla.mozilla.org/page.cgi");
+ }
+
+ let keys = [null, "a", "keys", "abcdedf", "pass", "https://bugzilla.mozilla.org/page.cgi",
+ "https://login.live.com/login.srf"];
+ for (let key of keys) {
+ decryptEncryptTest(key);
+ }
+ let url = "https://twitter.com/";
+ let value = [1, 0, 0, 0, 208, 140, 157, 223, 1, 21, 209, 17, 140, 122, 0, 192, 79, 194, 151, 235, 1, 0, 0, 0, 254, 58, 230, 75, 132, 228, 181, 79, 184, 160, 37, 106, 201, 29, 42, 152, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 16, 102, 0, 0, 0, 1, 0, 0, 32, 0, 0, 0, 90, 136, 17, 124, 122, 57, 178, 24, 34, 86, 209, 198, 184, 107, 58, 58, 32, 98, 61, 239, 129, 101, 56, 239, 114, 159, 139, 165, 183, 40, 183, 85, 0, 0, 0, 0, 14, 128, 0, 0, 0, 2, 0, 0, 32, 0, 0, 0, 147, 170, 34, 21, 53, 227, 191, 6, 201, 84, 106, 31, 57, 227, 46, 127, 219, 199, 80, 142, 37, 104, 112, 223, 26, 165, 223, 55, 176, 89, 55, 37, 112, 0, 0, 0, 98, 70, 221, 109, 5, 152, 46, 11, 190, 213, 226, 58, 244, 20, 180, 217, 63, 155, 227, 132, 7, 151, 235, 6, 37, 232, 176, 182, 141, 191, 251, 50, 20, 123, 53, 11, 247, 233, 112, 121, 130, 27, 168, 68, 92, 144, 192, 7, 12, 239, 53, 217, 253, 155, 54, 109, 236, 216, 225, 245, 79, 234, 165, 225, 104, 36, 77, 13, 195, 237, 143, 165, 100, 107, 230, 70, 54, 19, 179, 35, 8, 101, 93, 202, 121, 210, 222, 28, 93, 122, 36, 84, 185, 249, 238, 3, 102, 149, 248, 94, 137, 16, 192, 22, 251, 220, 22, 223, 16, 58, 104, 187, 64, 0, 0, 0, 70, 72, 15, 119, 144, 66, 117, 203, 190, 82, 131, 46, 111, 130, 238, 191, 170, 63, 186, 117, 46, 88, 171, 3, 94, 146, 75, 86, 243, 159, 63, 195, 149, 25, 105, 141, 42, 217, 108, 18, 63, 62, 98, 182, 241, 195, 12, 216, 152, 230, 176, 253, 202, 129, 41, 185, 135, 111, 226, 92, 27, 78, 27, 198];
+
+ let arr1 = crypto.arrayToString(value);
+ let arr2 = crypto.stringToArray(crypto.decryptData(crypto.encryptData(arr1, url), url));
+ for (let i = 0; i < arr1.length; i++) {
+ do_check_eq(arr2[i], value[i]);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_context_menu.js b/toolkit/components/passwordmgr/test/unit/test_context_menu.js
new file mode 100644
index 0000000000..722c13e155
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_context_menu.js
@@ -0,0 +1,165 @@
+/*
+ * Test the password manager context menu.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoginManagerContextMenu.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
+ return Services.strings.
+ createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function* test_initialize() {
+ for (let login of loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests if the LoginManagerContextMenu returns the correct login items.
+ */
+add_task(function* test_contextMenuAddAndRemoveLogins() {
+ const DOCUMENT_CONTENT = "<form><input id='pw' type=password></form>";
+ const INPUT_QUERY = "input[type='password']";
+
+ let testHostnames = [
+ "http://www.example.com",
+ "http://www2.example.com",
+ "http://www3.example.com",
+ "http://empty.example.com",
+ ];
+
+ for (let hostname of testHostnames) {
+ do_print("test for hostname: " + hostname);
+ // Get expected logins for this test.
+ let logins = getExpectedLogins(hostname);
+
+ // Create the logins menuitems fragment.
+ let {fragment, document} = createLoginsFragment(hostname, DOCUMENT_CONTENT, INPUT_QUERY);
+
+ if (!logins.length) {
+ Assert.ok(fragment === null, "Null returned. No logins where found.");
+ continue;
+ }
+ let items = [...fragment.querySelectorAll("menuitem")];
+
+ // Check if the items are those expected to be listed.
+ Assert.ok(checkLoginItems(logins, items), "All expected logins found.");
+ document.body.appendChild(fragment);
+
+ // Try to clear the fragment.
+ LoginManagerContextMenu.clearLoginsFromMenu(document);
+ Assert.equal(fragment.querySelectorAll("menuitem").length, 0, "All items correctly cleared.");
+ }
+
+ Services.logins.removeAllLogins();
+});
+
+/**
+ * Create a fragment with a menuitem for each login.
+ */
+function createLoginsFragment(url, content, elementQuery) {
+ const CHROME_URL = "chrome://mock-chrome";
+
+ // Create a mock document.
+ let document = MockDocument.createTestDocument(CHROME_URL, content);
+ let inputElement = document.querySelector(elementQuery);
+ MockDocument.mockOwnerDocumentProperty(inputElement, document, url);
+
+ // We also need a simple mock Browser object for this test.
+ let browser = {
+ ownerDocument: document
+ };
+
+ let URI = Services.io.newURI(url, null, null);
+ return {
+ document,
+ fragment: LoginManagerContextMenu.addLoginsToMenu(inputElement, browser, URI),
+ };
+}
+
+/**
+ * Check if every login have it's corresponding menuitem.
+ * Duplicates and empty usernames have a date appended.
+ */
+function checkLoginItems(logins, items) {
+ function findDuplicates(unfilteredLoginList) {
+ var seen = new Set();
+ var duplicates = new Set();
+ for (let login of unfilteredLoginList) {
+ if (seen.has(login.username)) {
+ duplicates.add(login.username);
+ }
+ seen.add(login.username);
+ }
+ return duplicates;
+ }
+ let duplicates = findDuplicates(logins);
+
+ let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+ for (let login of logins) {
+ if (login.username && !duplicates.has(login.username)) {
+ // If login is not duplicate and we can't find an item for it, fail.
+ if (!items.find(item => item.label == login.username)) {
+ return false;
+ }
+ continue;
+ }
+
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+ let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+ // If login is duplicate, check if we have a login item with appended date.
+ if (login.username && !items.find(item => item.label == login.username + " (" + time + ")")) {
+ return false;
+ }
+ // If login is empty, check if we have a login item with appended date.
+ if (!login.username &&
+ !items.find(item => item.label == _stringBundle.GetStringFromName("noUsername") + " (" + time + ")")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Gets the list of expected logins for a hostname.
+ */
+function getExpectedLogins(hostname) {
+ return Services.logins.getAllLogins().filter(entry => entry["hostname"] === hostname);
+}
+
+function loginList() {
+ return [
+ new LoginInfo("http://www.example.com", "http://www.example.com", null,
+ "username1", "password",
+ "form_field_username", "form_field_password"),
+
+ new LoginInfo("http://www.example.com", "http://www.example.com", null,
+ "username2", "password",
+ "form_field_username", "form_field_password"),
+
+ new LoginInfo("http://www2.example.com", "http://www.example.com", null,
+ "username", "password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www2.example.com", "http://www2.example.com", null,
+ "username", "password2",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www2.example.com", "http://www2.example.com", null,
+ "username2", "password2",
+ "form_field_username", "form_field_password"),
+
+ new LoginInfo("http://www3.example.com", "http://www.example.com", null,
+ "", "password",
+ "form_field_username", "form_field_password"),
+ new LoginInfo("http://www3.example.com", "http://www3.example.com", null,
+ "", "password2",
+ "form_field_username", "form_field_password"),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js b/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js
new file mode 100644
index 0000000000..d688a6dbf0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_dedupeLogins.js
@@ -0,0 +1,284 @@
+/*
+ * Test LoginHelper.dedupeLogins
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+const DOMAIN1_HTTP_TO_HTTP_U1_P1 = TestData.formLogin({
+ timePasswordChanged: 3000,
+ timeLastUsed: 2000,
+});
+const DOMAIN1_HTTP_TO_HTTP_U1_P2 = TestData.formLogin({
+ password: "password two",
+});
+const DOMAIN1_HTTP_TO_HTTP_U2_P2 = TestData.formLogin({
+ password: "password two",
+ username: "username two",
+});
+const DOMAIN1_HTTPS_TO_HTTPS_U1_P1 = TestData.formLogin({
+ formSubmitURL: "http://www.example.com",
+ hostname: "https://www3.example.com",
+ timePasswordChanged: 4000,
+ timeLastUsed: 1000,
+});
+const DOMAIN1_HTTPS_TO_EMPTY_U1_P1 = TestData.formLogin({
+ formSubmitURL: "",
+ hostname: "https://www3.example.com",
+});
+const DOMAIN1_HTTPS_TO_EMPTYU_P1 = TestData.formLogin({
+ hostname: "https://www3.example.com",
+ username: "",
+});
+const DOMAIN1_HTTP_AUTH = TestData.authLogin({
+ hostname: "http://www3.example.com",
+});
+const DOMAIN1_HTTPS_AUTH = TestData.authLogin({
+ hostname: "https://www3.example.com",
+});
+
+
+add_task(function test_dedupeLogins() {
+ // [description, expectedOutput, dedupe arg. 0, dedupe arg 1, ...]
+ let testcases = [
+ [
+ "exact dupes",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ [], // force no resolveBy logic to test behavior of preferring the first..
+ ],
+ [
+ "default uniqueKeys is un + pw",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
+ undefined,
+ [],
+ ],
+ [
+ "same usernames, different passwords, dedupe username only",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P2],
+ ["username"],
+ [],
+ ],
+ [
+ "same un+pw, different scheme",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ [],
+ ],
+ [
+ "same un+pw, different scheme, reverse order",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ [],
+ ],
+ [
+ "same un+pw, different scheme, include hostname",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ ["hostname", "username", "password"],
+ [],
+ ],
+ [
+ "empty username is not deduped with non-empty",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_EMPTYU_P1],
+ undefined,
+ [],
+ ],
+ [
+ "empty username is deduped with same passwords",
+ [DOMAIN1_HTTPS_TO_EMPTYU_P1],
+ [DOMAIN1_HTTPS_TO_EMPTYU_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ ["password"],
+ [],
+ ],
+ [
+ "mix of form and HTTP auth",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTP_AUTH],
+ undefined,
+ [],
+ ],
+ ];
+
+ for (let tc of testcases) {
+ let description = tc.shift();
+ let expected = tc.shift();
+ let actual = LoginHelper.dedupeLogins(...tc);
+ Assert.strictEqual(actual.length, expected.length, `Check: ${description}`);
+ for (let [i, login] of expected.entries()) {
+ Assert.strictEqual(actual[i], login, `Check index ${i}`);
+ }
+ }
+});
+
+
+add_task(function* test_dedupeLogins_resolveBy() {
+ Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timeLastUsed > DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timeLastUsed,
+ "Sanity check timeLastUsed difference");
+ Assert.ok(DOMAIN1_HTTP_TO_HTTP_U1_P1.timePasswordChanged < DOMAIN1_HTTPS_TO_HTTPS_U1_P1.timePasswordChanged,
+ "Sanity check timePasswordChanged difference");
+
+ let testcases = [
+ [
+ "default resolveBy is timeLastUsed",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ ],
+ [
+ "default resolveBy is timeLastUsed, reversed input",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ ],
+ [
+ "resolveBy timeLastUsed + timePasswordChanged",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timeLastUsed", "timePasswordChanged"],
+ ],
+ [
+ "resolveBy timeLastUsed + timePasswordChanged, reversed input",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["timeLastUsed", "timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged, reversed",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged + timeLastUsed",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timePasswordChanged", "timeLastUsed"],
+ ],
+ [
+ "resolveBy timePasswordChanged + timeLastUsed, reversed",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["timePasswordChanged", "timeLastUsed"],
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTP",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTP, reversed input",
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTP_TO_HTTP_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTPS",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, prefer HTTPS, reversed input",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTP_TO_HTTP_U1_P1, DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
+ ],
+ [
+ "resolveBy scheme HTTP auth",
+ [DOMAIN1_HTTPS_AUTH],
+ [DOMAIN1_HTTP_AUTH, DOMAIN1_HTTPS_AUTH],
+ undefined,
+ ["scheme"],
+ DOMAIN1_HTTPS_AUTH.hostname,
+ ],
+ [
+ "resolveBy scheme HTTP auth, reversed input",
+ [DOMAIN1_HTTPS_AUTH],
+ [DOMAIN1_HTTPS_AUTH, DOMAIN1_HTTP_AUTH],
+ undefined,
+ ["scheme"],
+ DOMAIN1_HTTPS_AUTH.hostname,
+ ],
+ [
+ "resolveBy scheme, empty form submit URL",
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1],
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTPS_TO_EMPTY_U1_P1],
+ undefined,
+ ["scheme"],
+ DOMAIN1_HTTPS_TO_HTTPS_U1_P1.hostname,
+ ],
+ ];
+
+ for (let tc of testcases) {
+ let description = tc.shift();
+ let expected = tc.shift();
+ let actual = LoginHelper.dedupeLogins(...tc);
+ Assert.strictEqual(actual.length, expected.length, `Check: ${description}`);
+ for (let [i, login] of expected.entries()) {
+ Assert.strictEqual(actual[i], login, `Check index ${i}`);
+ }
+ }
+
+});
+
+add_task(function* test_dedupeLogins_preferredOriginMissing() {
+ let testcases = [
+ [
+ "resolveBy scheme + timePasswordChanged, missing preferredOrigin",
+ /preferredOrigin/,
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ ],
+ [
+ "resolveBy timePasswordChanged + scheme, missing preferredOrigin",
+ /preferredOrigin/,
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["timePasswordChanged", "scheme"],
+ ],
+ [
+ "resolveBy scheme + timePasswordChanged, empty preferredOrigin",
+ /preferredOrigin/,
+ [DOMAIN1_HTTPS_TO_HTTPS_U1_P1, DOMAIN1_HTTP_TO_HTTP_U1_P1],
+ undefined,
+ ["scheme", "timePasswordChanged"],
+ "",
+ ],
+ ];
+
+ for (let tc of testcases) {
+ let description = tc.shift();
+ let expectedException = tc.shift();
+ Assert.throws(() => {
+ LoginHelper.dedupeLogins(...tc);
+ }, expectedException, `Check: ${description}`);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js b/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js
new file mode 100644
index 0000000000..ff3b7e8686
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_disabled_hosts.js
@@ -0,0 +1,196 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests getLoginSavingEnabled, setLoginSavingEnabled, and getAllDisabledHosts.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Tests setLoginSavingEnabled and getAllDisabledHosts.
+ */
+add_task(function test_setLoginSavingEnabled_getAllDisabledHosts()
+{
+ // Add some disabled hosts, and verify that different schemes for the same
+ // domain are considered different hosts.
+ let hostname1 = "http://disabled1.example.com";
+ let hostname2 = "http://disabled2.example.com";
+ let hostname3 = "https://disabled2.example.com";
+ Services.logins.setLoginSavingEnabled(hostname1, false);
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+ Services.logins.setLoginSavingEnabled(hostname3, false);
+
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1, hostname2, hostname3]);
+
+ // Adding the same host twice should not result in an error.
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1, hostname2, hostname3]);
+
+ // Removing a disabled host should work.
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1, hostname3]);
+
+ // Removing the last disabled host should work.
+ Services.logins.setLoginSavingEnabled(hostname1, true);
+ Services.logins.setLoginSavingEnabled(hostname3, true);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ []);
+});
+
+/**
+ * Tests setLoginSavingEnabled and getLoginSavingEnabled.
+ */
+add_task(function test_setLoginSavingEnabled_getLoginSavingEnabled()
+{
+ let hostname1 = "http://disabled.example.com";
+ let hostname2 = "https://disabled.example.com";
+
+ // Hosts should not be disabled by default.
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Test setting initial values.
+ Services.logins.setLoginSavingEnabled(hostname1, false);
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Test changing values.
+ Services.logins.setLoginSavingEnabled(hostname1, true);
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Clean up.
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+});
+
+/**
+ * Tests setLoginSavingEnabled with invalid NUL characters in the hostname.
+ */
+add_task(function test_setLoginSavingEnabled_invalid_characters()
+{
+ let hostname = "http://null\0X.example.com";
+ Assert.throws(() => Services.logins.setLoginSavingEnabled(hostname, false),
+ /Invalid hostname/);
+
+ // Verify that no data was stored by the previous call.
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ []);
+});
+
+/**
+ * Tests different values of the "signon.rememberSignons" property.
+ */
+add_task(function test_rememberSignons()
+{
+ let hostname1 = "http://example.com";
+ let hostname2 = "http://localhost";
+
+ // The default value for the preference should be true.
+ do_check_true(Services.prefs.getBoolPref("signon.rememberSignons"));
+
+ // Hosts should not be disabled by default.
+ Services.logins.setLoginSavingEnabled(hostname1, false);
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Disable storage of saved passwords globally.
+ Services.prefs.setBoolPref("signon.rememberSignons", false);
+ do_register_cleanup(
+ () => Services.prefs.clearUserPref("signon.rememberSignons"));
+
+ // All hosts should now appear disabled.
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // The list of disabled hosts should be unaltered.
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname1]);
+
+ // Changing values with the preference set should work.
+ Services.logins.setLoginSavingEnabled(hostname1, true);
+ Services.logins.setLoginSavingEnabled(hostname2, false);
+
+ // All hosts should still appear disabled.
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // The list of disabled hosts should have been changed.
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ [hostname2]);
+
+ // Enable storage of saved passwords again.
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+
+ // Hosts should now appear enabled as requested.
+ do_check_true(Services.logins.getLoginSavingEnabled(hostname1));
+ do_check_false(Services.logins.getLoginSavingEnabled(hostname2));
+
+ // Clean up.
+ Services.logins.setLoginSavingEnabled(hostname2, true);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ []);
+});
+
+/**
+ * Tests storing disabled hosts with non-ASCII characters where IDN is supported.
+ */
+add_task(function* test_storage_setLoginSavingEnabled_nonascii_IDN_is_supported()
+{
+ let hostname = "http://大.net";
+ let encoding = "http://xn--pss.net";
+
+ // Test adding disabled host with nonascii URL (http://大.net).
+ Services.logins.setLoginSavingEnabled(hostname, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [hostname]);
+
+ LoginTestUtils.clearData();
+
+ // Test adding disabled host with IDN ("http://xn--pss.net").
+ Services.logins.setLoginSavingEnabled(encoding, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [hostname]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests storing disabled hosts with non-ASCII characters where IDN is not supported.
+ */
+add_task(function* test_storage_setLoginSavingEnabled_nonascii_IDN_not_supported()
+{
+ let hostname = "http://√.com";
+ let encoding = "http://xn--19g.com";
+
+ // Test adding disabled host with nonascii URL (http://√.com).
+ Services.logins.setLoginSavingEnabled(hostname, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [encoding]);
+
+ LoginTestUtils.clearData();
+
+ // Test adding disabled host with IDN ("http://xn--19g.com").
+ Services.logins.setLoginSavingEnabled(encoding, false);
+ yield* LoginTestUtils.reloadData();
+ Assert.equal(Services.logins.getLoginSavingEnabled(hostname), false);
+ Assert.equal(Services.logins.getLoginSavingEnabled(encoding), false);
+ LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(), [encoding]);
+
+ LoginTestUtils.clearData();
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_getFormFields.js b/toolkit/components/passwordmgr/test/unit/test_getFormFields.js
new file mode 100644
index 0000000000..46912ab8fe
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getFormFields.js
@@ -0,0 +1,147 @@
+/*
+ * Test for LoginManagerContent._getFormFields.
+ */
+
+"use strict";
+
+// Services.prefs.setBoolPref("signon.debug", true);
+
+Cu.importGlobalProperties(["URL"]);
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+const TESTCASES = [
+ {
+ description: "1 password field outside of a <form>",
+ document: `<input id="pw1" type=password>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 text field outside of a <form> without a password field",
+ document: `<input id="un1">`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 username & password field outside of a <form>",
+ document: `<input id="un1">
+ <input id="pw1" type=password>`,
+ returnedFieldIDs: ["un1", "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 username & password field in a <form>",
+ document: `<form>
+ <input id="un1">
+ <input id="pw1" type=password>
+ </form>`,
+ returnedFieldIDs: ["un1", "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 empty password fields outside of a <form>",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password>
+ <input id="pw3" type=password>
+ <input id="pw4" type=password>`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 password fields outside of a <form> (1 empty, 3 full) with skipEmpty",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password value="pass2">
+ <input id="pw3" type=password value="pass3">
+ <input id="pw4" type=password value="pass4">`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: true,
+ },
+ {
+ description: "Form with 1 password field",
+ document: `<form><input id="pw1" type=password></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "Form with 2 password fields",
+ document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field in a form, 1 outside (not processed)",
+ document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field in a form, 1 text field outside (not processed)",
+ document: `<form><input id="pw1" type=password></form><input>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 text field in a form, 1 password field outside (not processed)",
+ document: `<form><input></form><input id="pw1" type=password>`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form='form1'>
+ <form id="form1"></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDs: [null, null, null],
+ skipEmptyFields: true,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty with 1 empty",
+ document: `<input id="pw1" type=password value="pass1"><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDs: [null, "pw1", null],
+ skipEmptyFields: true,
+ },
+];
+
+for (let tc of TESTCASES) {
+ do_print("Sanity checking the testcase: " + tc.description);
+
+ (function() {
+ let testcase = tc;
+ add_task(function*() {
+ do_print("Starting testcase: " + testcase.description);
+ let document = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+
+ let input = document.querySelector("input");
+ MockDocument.mockOwnerDocumentProperty(input, document, "http://localhost:8080/test/");
+
+ let formLike = LoginFormFactory.createFromField(input);
+
+ let actual = LoginManagerContent._getFormFields(formLike,
+ testcase.skipEmptyFields,
+ new Set());
+
+ Assert.strictEqual(testcase.returnedFieldIDs.length, 3,
+ "_getFormFields returns 3 elements");
+
+ for (let i = 0; i < testcase.returnedFieldIDs.length; i++) {
+ let expectedID = testcase.returnedFieldIDs[i];
+ if (expectedID === null) {
+ Assert.strictEqual(actual[i], expectedID,
+ "Check returned field " + i + " is null");
+ } else {
+ Assert.strictEqual(actual[i].id, expectedID,
+ "Check returned field " + i + " ID");
+ }
+ }
+ });
+ })();
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js
new file mode 100644
index 0000000000..08fa422abd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js
@@ -0,0 +1,156 @@
+/*
+ * Test for LoginManagerContent._getPasswordFields using LoginFormFactory.
+ */
+
+"use strict";
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, LoginFormFactory } = LMCBackstagePass;
+const TESTCASES = [
+ {
+ description: "Empty document",
+ document: ``,
+ returnedFieldIDsByFormLike: [],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "Non-password input with no <form> present",
+ document: `<input>`,
+ // Only the IDs of password fields should be in this array
+ returnedFieldIDsByFormLike: [[]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field outside of a <form>",
+ document: `<input id="pw1" type=password>`,
+ returnedFieldIDsByFormLike: [["pw1"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 empty password fields outside of a <form>",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password>
+ <input id="pw3" type=password>
+ <input id="pw4" type=password>`,
+ returnedFieldIDsByFormLike: [[]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "4 password fields outside of a <form> (1 empty, 3 full) with skipEmpty",
+ document: `<input id="pw1" type=password>
+ <input id="pw2" type=password value="pass2">
+ <input id="pw3" type=password value="pass3">
+ <input id="pw4" type=password value="pass4">`,
+ returnedFieldIDsByFormLike: [["pw2", "pw3", "pw4"]],
+ skipEmptyFields: true,
+ },
+ {
+ description: "Form with 1 password field",
+ document: `<form><input id="pw1" type=password></form>`,
+ returnedFieldIDsByFormLike: [["pw1"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "Form with 2 password fields",
+ document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
+ returnedFieldIDsByFormLike: [["pw1", "pw2"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "1 password field in a form, 1 outside",
+ document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
+ returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form='form1'>
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
+ skipEmptyFields: undefined,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty",
+ document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [[], []],
+ skipEmptyFields: true,
+ },
+ {
+ description: "skipEmptyFields should also skip white-space only fields",
+ document: `<input id="pw-space" type=password value=" ">
+ <input id="pw-tab" type=password value=" ">
+ <input id="pw-newline" type=password form="form1" value="
+">
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [[], []],
+ skipEmptyFields: true,
+ },
+ {
+ description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty with 1 empty",
+ document: `<input id="pw1" type=password value=" pass1 "><input id="pw2" type=password form="form1">
+ <form id="form1"></form>`,
+ returnedFieldIDsByFormLike: [["pw1"], []],
+ skipEmptyFields: true,
+ },
+];
+
+for (let tc of TESTCASES) {
+ do_print("Sanity checking the testcase: " + tc.description);
+
+ (function() {
+ let testcase = tc;
+ add_task(function*() {
+ do_print("Starting testcase: " + testcase.description);
+ let document = MockDocument.createTestDocument("http://localhost:8080/test/",
+ testcase.document);
+
+ let mapRootElementToFormLike = new Map();
+ for (let input of document.querySelectorAll("input")) {
+ let formLike = LoginFormFactory.createFromField(input);
+ let existingFormLike = mapRootElementToFormLike.get(formLike.rootElement);
+ if (!existingFormLike) {
+ mapRootElementToFormLike.set(formLike.rootElement, formLike);
+ continue;
+ }
+
+ // If the formLike is already present, ensure that the properties are the same.
+ do_print("Checking if the new FormLike for the same root has the same properties");
+ formLikeEqual(formLike, existingFormLike);
+ }
+
+ Assert.strictEqual(mapRootElementToFormLike.size, testcase.returnedFieldIDsByFormLike.length,
+ "Check the correct number of different formLikes were returned");
+
+ let formLikeIndex = -1;
+ for (let formLikeFromInput of mapRootElementToFormLike.values()) {
+ formLikeIndex++;
+ let pwFields = LoginManagerContent._getPasswordFields(formLikeFromInput,
+ testcase.skipEmptyFields);
+
+ if (formLikeFromInput.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
+ let formLikeFromForm = LoginFormFactory.createFromForm(formLikeFromInput.rootElement);
+ do_print("Checking that the FormLike created for the <form> matches" +
+ " the one from a password field");
+ formLikeEqual(formLikeFromInput, formLikeFromForm);
+ }
+
+
+ if (testcase.returnedFieldIDsByFormLike[formLikeIndex].length === 0) {
+ Assert.strictEqual(pwFields, null,
+ "If no password fields were found null should be returned");
+ } else {
+ Assert.strictEqual(pwFields.length,
+ testcase.returnedFieldIDsByFormLike[formLikeIndex].length,
+ "Check the # of password fields for formLike #" + formLikeIndex);
+ }
+
+ for (let i = 0; i < testcase.returnedFieldIDsByFormLike[formLikeIndex].length; i++) {
+ let expectedID = testcase.returnedFieldIDsByFormLike[formLikeIndex][i];
+ Assert.strictEqual(pwFields[i].element.id, expectedID,
+ "Check password field " + i + " ID");
+ }
+ }
+ });
+ })();
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js b/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js
new file mode 100644
index 0000000000..f2773ec624
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordOrigin.js
@@ -0,0 +1,28 @@
+/*
+ * Test for LoginUtils._getPasswordOrigin
+ */
+
+"use strict";
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const TESTCASES = [
+ ["javascript:void(0);", null],
+ ["javascript:void(0);", "javascript:", true],
+ ["chrome://MyAccount", null],
+ ["data:text/html,example", null],
+ ["http://username:password@example.com:80/foo?bar=baz#fragment", "http://example.com", true],
+ ["http://127.0.0.1:80/foo", "http://127.0.0.1"],
+ ["http://[::1]:80/foo", "http://[::1]"],
+ ["http://example.com:8080/foo", "http://example.com:8080"],
+ ["http://127.0.0.1:8080/foo", "http://127.0.0.1:8080", true],
+ ["http://[::1]:8080/foo", "http://[::1]:8080"],
+ ["https://example.com:443/foo", "https://example.com"],
+ ["https://[::1]:443/foo", "https://[::1]"],
+ ["https://[::1]:8443/foo", "https://[::1]:8443"],
+ ["ftp://username:password@[::1]:2121/foo", "ftp://[::1]:2121"],
+];
+
+for (let [input, expected, allowJS] of TESTCASES) {
+ let actual = LMCBackstagePass.LoginUtils._getPasswordOrigin(input, allowJS);
+ Assert.strictEqual(actual, expected, "Checking: " + input);
+}
diff --git a/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js
new file mode 100644
index 0000000000..660910dffa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js
@@ -0,0 +1,40 @@
+/*
+ * Test LoginHelper.isOriginMatching
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+add_task(function test_isOriginMatching() {
+ let testcases = [
+ // Index 0 holds the expected return value followed by arguments to isOriginMatching.
+ [true, "http://example.com", "http://example.com"],
+ [true, "http://example.com:8080", "http://example.com:8080"],
+ [true, "https://example.com", "https://example.com"],
+ [true, "https://example.com:8443", "https://example.com:8443"],
+ [false, "http://example.com", "http://mozilla.org"],
+ [false, "http://example.com", "http://example.com:8080"],
+ [false, "https://example.com", "http://example.com"],
+ [false, "https://example.com", "https://mozilla.org"],
+ [false, "http://example.com", "http://sub.example.com"],
+ [false, "https://example.com", "https://sub.example.com"],
+ [false, "http://example.com", "https://example.com:8443"],
+ [false, "http://example.com:8080", "http://example.com:8081"],
+ [false, "http://example.com", ""],
+ [false, "", "http://example.com"],
+ [true, "http://example.com", "https://example.com", { schemeUpgrades: true }],
+ [true, "https://example.com", "https://example.com", { schemeUpgrades: true }],
+ [true, "http://example.com:8080", "http://example.com:8080", { schemeUpgrades: true }],
+ [true, "https://example.com:8443", "https://example.com:8443", { schemeUpgrades: true }],
+ [false, "https://example.com", "http://example.com", { schemeUpgrades: true }], // downgrade
+ [false, "http://example.com:8080", "https://example.com", { schemeUpgrades: true }], // port mismatch
+ [false, "http://example.com", "https://example.com:8443", { schemeUpgrades: true }], // port mismatch
+ [false, "http://sub.example.com", "http://example.com", { schemeUpgrades: true }],
+ ];
+ for (let tc of testcases) {
+ let expected = tc.shift();
+ Assert.strictEqual(LoginHelper.isOriginMatching(...tc), expected,
+ "Check " + JSON.stringify(tc));
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js b/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js
new file mode 100644
index 0000000000..4e16aa267f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_legacy_empty_formSubmitURL.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the legacy case of a login store containing entries that have an empty
+ * string in the formSubmitURL field.
+ *
+ * In normal conditions, for the purpose of login autocomplete, HTML forms are
+ * identified using both the prePath of the URI on which they are located, and
+ * the prePath of the URI where the data will be submitted. This is represented
+ * by the hostname and formSubmitURL properties of the stored nsILoginInfo.
+ *
+ * When a new login for use in forms is saved (after the user replies to the
+ * password prompt), it is always stored with both the hostname and the
+ * formSubmitURL (that will be equal to the hostname when the form has no
+ * "action" attribute).
+ *
+ * When the same form is displayed again, the password is autocompleted. If
+ * there is another form on the same site that submits to a different site, it
+ * is considered a different form, so the password is not autocompleted, but a
+ * new password can be stored for the other form.
+ *
+ * However, the login database might contain data for an nsILoginInfo that has a
+ * valid hostname, but an empty formSubmitURL. This means that the login
+ * applies to all forms on the site, regardless of where they submit data to.
+ *
+ * A site can have at most one such login, and in case it is present, then it is
+ * not possible to store separate logins for forms on the same site that submit
+ * data to different sites.
+ *
+ * The only way to have such condition is to be using logins that were initially
+ * saved by a very old version of the browser, or because of data manually added
+ * by an extension in an old version.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Adds a login with an empty formSubmitURL, then it verifies that no other
+ * form logins can be added for the same host.
+ */
+add_task(function test_addLogin_wildcard()
+{
+ let loginInfo = TestData.formLogin({ hostname: "http://any.example.com",
+ formSubmitURL: "" });
+ Services.logins.addLogin(loginInfo);
+
+ // Normal form logins cannot be added anymore.
+ loginInfo = TestData.formLogin({ hostname: "http://any.example.com" });
+ Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/);
+
+ // Authentication logins can still be added.
+ loginInfo = TestData.authLogin({ hostname: "http://any.example.com" });
+ Services.logins.addLogin(loginInfo);
+
+ // Form logins can be added for other hosts.
+ loginInfo = TestData.formLogin({ hostname: "http://other.example.com" });
+ Services.logins.addLogin(loginInfo);
+});
+
+/**
+ * Verifies that findLogins, searchLogins, and countLogins include all logins
+ * that have an empty formSubmitURL in the store, even when a formSubmitURL is
+ * specified.
+ */
+add_task(function test_search_all_wildcard()
+{
+ // Search a given formSubmitURL on any host.
+ let matchData = newPropertyBag({ formSubmitURL: "http://www.example.com" });
+ do_check_eq(Services.logins.searchLogins({}, matchData).length, 2);
+
+ do_check_eq(Services.logins.findLogins({}, "", "http://www.example.com",
+ null).length, 2);
+
+ do_check_eq(Services.logins.countLogins("", "http://www.example.com",
+ null), 2);
+
+ // Restrict the search to one host.
+ matchData.setProperty("hostname", "http://any.example.com");
+ do_check_eq(Services.logins.searchLogins({}, matchData).length, 1);
+
+ do_check_eq(Services.logins.findLogins({}, "http://any.example.com",
+ "http://www.example.com",
+ null).length, 1);
+
+ do_check_eq(Services.logins.countLogins("http://any.example.com",
+ "http://www.example.com",
+ null), 1);
+});
+
+/**
+ * Verifies that specifying an empty string for formSubmitURL in searchLogins
+ * includes only logins that have an empty formSubmitURL in the store.
+ */
+add_task(function test_searchLogins_wildcard()
+{
+ let logins = Services.logins.searchLogins({},
+ newPropertyBag({ formSubmitURL: "" }));
+
+ let loginInfo = TestData.formLogin({ hostname: "http://any.example.com",
+ formSubmitURL: "" });
+ LoginTestUtils.assertLoginListsEqual(logins, [loginInfo]);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js b/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js
new file mode 100644
index 0000000000..709bc98189
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_legacy_validation.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the legacy validation made when storing nsILoginInfo or disabled hosts.
+ *
+ * These rules exist because of limitations of the "signons.txt" storage file,
+ * that is not used anymore. They are still enforced by the Login Manager
+ * service, despite these values can now be safely stored in the back-end.
+ */
+
+"use strict";
+
+// Tests
+
+/**
+ * Tests legacy validation with addLogin.
+ */
+add_task(function test_addLogin_invalid_characters_legacy()
+{
+ // Test newlines and carriage returns in properties that contain URLs.
+ for (let testValue of ["http://newline\n.example.com",
+ "http://carriagereturn.example.com\r"]) {
+ let loginInfo = TestData.formLogin({ hostname: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+
+ loginInfo = TestData.formLogin({ formSubmitURL: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+
+ loginInfo = TestData.authLogin({ httpRealm: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+ }
+
+ // Test newlines and carriage returns in form field names.
+ for (let testValue of ["newline_field\n", "carriagereturn\r_field"]) {
+ let loginInfo = TestData.formLogin({ usernameField: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+
+ loginInfo = TestData.formLogin({ passwordField: testValue });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't contain newlines/);
+ }
+
+ // Test a single dot as the value of usernameField and formSubmitURL.
+ let loginInfo = TestData.formLogin({ usernameField: "." });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't be periods/);
+
+ loginInfo = TestData.formLogin({ formSubmitURL: "." });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /login values can't be periods/);
+
+ // Test the sequence " (" inside the value of the "hostname" property.
+ loginInfo = TestData.formLogin({ hostname: "http://parens (.example.com" });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /bad parens in hostname/);
+});
+
+/**
+ * Tests legacy validation with setLoginSavingEnabled.
+ */
+add_task(function test_setLoginSavingEnabled_invalid_characters_legacy()
+{
+ for (let hostname of ["http://newline\n.example.com",
+ "http://carriagereturn.example.com\r",
+ "."]) {
+ Assert.throws(() => Services.logins.setLoginSavingEnabled(hostname, false),
+ /Invalid hostname/);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_change.js b/toolkit/components/passwordmgr/test/unit/test_logins_change.js
new file mode 100644
index 0000000000..79c6d2f54a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_change.js
@@ -0,0 +1,384 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests methods that add, remove, and modify logins.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Verifies that the specified login is considered invalid by addLogin and by
+ * modifyLogin with both nsILoginInfo and nsIPropertyBag arguments.
+ *
+ * This test requires that the login store is empty.
+ *
+ * @param aLoginInfo
+ * nsILoginInfo corresponding to an invalid login.
+ * @param aExpectedError
+ * This argument is passed to the "Assert.throws" test to determine which
+ * error is expected from the modification functions.
+ */
+function checkLoginInvalid(aLoginInfo, aExpectedError)
+{
+ // Try to add the new login, and verify that no data is stored.
+ Assert.throws(() => Services.logins.addLogin(aLoginInfo), aExpectedError);
+ LoginTestUtils.checkLogins([]);
+
+ // Add a login for the modification tests.
+ let testLogin = TestData.formLogin({ hostname: "http://modify.example.com" });
+ Services.logins.addLogin(testLogin);
+
+ // Try to modify the existing login using nsILoginInfo and nsIPropertyBag.
+ Assert.throws(() => Services.logins.modifyLogin(testLogin, aLoginInfo),
+ aExpectedError);
+ Assert.throws(() => Services.logins.modifyLogin(testLogin, newPropertyBag({
+ hostname: aLoginInfo.hostname,
+ formSubmitURL: aLoginInfo.formSubmitURL,
+ httpRealm: aLoginInfo.httpRealm,
+ username: aLoginInfo.username,
+ password: aLoginInfo.password,
+ usernameField: aLoginInfo.usernameField,
+ passwordField: aLoginInfo.passwordField,
+ })), aExpectedError);
+
+ // Verify that no data was stored by the previous calls.
+ LoginTestUtils.checkLogins([testLogin]);
+ Services.logins.removeLogin(testLogin);
+}
+
+/**
+ * Verifies that two objects are not the same instance
+ * but have equal attributes.
+ *
+ * @param {Object} objectA
+ * An object to compare.
+ *
+ * @param {Object} objectB
+ * Another object to compare.
+ *
+ * @param {string[]} attributes
+ * Attributes to compare.
+ *
+ * @return true if all passed attributes are equal for both objects, false otherwise.
+ */
+function compareAttributes(objectA, objectB, attributes) {
+ // If it's the same object, we want to return false.
+ if (objectA == objectB) {
+ return false;
+ }
+ return attributes.every(attr => objectA[attr] == objectB[attr]);
+}
+
+// Tests
+
+/**
+ * Tests that adding logins to the database works.
+ */
+add_task(function test_addLogin_removeLogin()
+{
+ // Each login from the test data should be valid and added to the list.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.addLogin(loginInfo);
+ }
+ LoginTestUtils.checkLogins(TestData.loginList());
+
+ // Trying to add each login again should result in an error.
+ for (let loginInfo of TestData.loginList()) {
+ Assert.throws(() => Services.logins.addLogin(loginInfo), /already exists/);
+ }
+
+ // Removing each login should succeed.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.removeLogin(loginInfo);
+ }
+
+ LoginTestUtils.checkLogins([]);
+});
+
+/**
+ * Tests invalid combinations of httpRealm and formSubmitURL.
+ *
+ * For an nsILoginInfo to be valid for storage, one of the two properties should
+ * be strictly equal to null, and the other must not be null or an empty string.
+ *
+ * The legacy case of an empty string in formSubmitURL and a null value in
+ * httpRealm is also supported for storage at the moment.
+ */
+add_task(function test_invalid_httpRealm_formSubmitURL()
+{
+ // httpRealm === null, formSubmitURL === null
+ checkLoginInvalid(TestData.formLogin({ formSubmitURL: null }),
+ /without a httpRealm or formSubmitURL/);
+
+ // httpRealm === "", formSubmitURL === null
+ checkLoginInvalid(TestData.authLogin({ httpRealm: "" }),
+ /without a httpRealm or formSubmitURL/);
+
+ // httpRealm === null, formSubmitURL === ""
+ // This is not enforced for now.
+ // checkLoginInvalid(TestData.formLogin({ formSubmitURL: "" }),
+ // /without a httpRealm or formSubmitURL/);
+
+ // httpRealm === "", formSubmitURL === ""
+ checkLoginInvalid(TestData.formLogin({ formSubmitURL: "", httpRealm: "" }),
+ /both a httpRealm and formSubmitURL/);
+
+ // !!httpRealm, !!formSubmitURL
+ checkLoginInvalid(TestData.formLogin({ httpRealm: "The HTTP Realm" }),
+ /both a httpRealm and formSubmitURL/);
+
+ // httpRealm === "", !!formSubmitURL
+ checkLoginInvalid(TestData.formLogin({ httpRealm: "" }),
+ /both a httpRealm and formSubmitURL/);
+
+ // !!httpRealm, formSubmitURL === ""
+ checkLoginInvalid(TestData.authLogin({ formSubmitURL: "" }),
+ /both a httpRealm and formSubmitURL/);
+});
+
+/**
+ * Tests null or empty values in required login properties.
+ */
+add_task(function test_missing_properties()
+{
+ checkLoginInvalid(TestData.formLogin({ hostname: null }),
+ /null or empty hostname/);
+
+ checkLoginInvalid(TestData.formLogin({ hostname: "" }),
+ /null or empty hostname/);
+
+ checkLoginInvalid(TestData.formLogin({ username: null }),
+ /null username/);
+
+ checkLoginInvalid(TestData.formLogin({ password: null }),
+ /null or empty password/);
+
+ checkLoginInvalid(TestData.formLogin({ password: "" }),
+ /null or empty password/);
+});
+
+/**
+ * Tests invalid NUL characters in nsILoginInfo properties.
+ */
+add_task(function test_invalid_characters()
+{
+ let loginList = [
+ TestData.authLogin({ hostname: "http://null\0X.example.com" }),
+ TestData.authLogin({ httpRealm: "realm\0" }),
+ TestData.formLogin({ formSubmitURL: "http://null\0X.example.com" }),
+ TestData.formLogin({ usernameField: "field\0_null" }),
+ TestData.formLogin({ usernameField: ".\0" }), // Special single dot case
+ TestData.formLogin({ passwordField: "field\0_null" }),
+ TestData.formLogin({ username: "user\0name" }),
+ TestData.formLogin({ password: "pass\0word" }),
+ ];
+ for (let loginInfo of loginList) {
+ checkLoginInvalid(loginInfo, /login values can't contain nulls/);
+ }
+});
+
+/**
+ * Tests removing a login that does not exists.
+ */
+add_task(function test_removeLogin_nonexisting()
+{
+ Assert.throws(() => Services.logins.removeLogin(TestData.formLogin()),
+ /No matching logins/);
+});
+
+/**
+ * Tests removing all logins at once.
+ */
+add_task(function test_removeAllLogins()
+{
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.addLogin(loginInfo);
+ }
+ Services.logins.removeAllLogins();
+ LoginTestUtils.checkLogins([]);
+
+ // The function should also work when there are no logins to delete.
+ Services.logins.removeAllLogins();
+});
+
+/**
+ * Tests the modifyLogin function with an nsILoginInfo argument.
+ */
+add_task(function test_modifyLogin_nsILoginInfo()
+{
+ let loginInfo = TestData.formLogin();
+ let updatedLoginInfo = TestData.formLogin({
+ username: "new username",
+ password: "new password",
+ usernameField: "new_form_field_username",
+ passwordField: "new_form_field_password",
+ });
+ let differentLoginInfo = TestData.authLogin();
+
+ // Trying to modify a login that does not exist should throw.
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, updatedLoginInfo),
+ /No matching logins/);
+
+ // Add the first form login, then modify it to match the second.
+ Services.logins.addLogin(loginInfo);
+ Services.logins.modifyLogin(loginInfo, updatedLoginInfo);
+
+ // The data should now match the second login.
+ LoginTestUtils.checkLogins([updatedLoginInfo]);
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, updatedLoginInfo),
+ /No matching logins/);
+
+ // The login can be changed to have a different type and hostname.
+ Services.logins.modifyLogin(updatedLoginInfo, differentLoginInfo);
+ LoginTestUtils.checkLogins([differentLoginInfo]);
+
+ // It is now possible to add a login with the old type and hostname.
+ Services.logins.addLogin(loginInfo);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ // Modifying a login to match an existing one should not be possible.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, differentLoginInfo),
+ /already exists/);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests the modifyLogin function with an nsIPropertyBag argument.
+ */
+add_task(function test_modifyLogin_nsIProperyBag()
+{
+ let loginInfo = TestData.formLogin();
+ let updatedLoginInfo = TestData.formLogin({
+ username: "new username",
+ password: "new password",
+ usernameField: "",
+ passwordField: "new_form_field_password",
+ });
+ let differentLoginInfo = TestData.authLogin();
+ let differentLoginProperties = newPropertyBag({
+ hostname: differentLoginInfo.hostname,
+ formSubmitURL: differentLoginInfo.formSubmitURL,
+ httpRealm: differentLoginInfo.httpRealm,
+ username: differentLoginInfo.username,
+ password: differentLoginInfo.password,
+ usernameField: differentLoginInfo.usernameField,
+ passwordField: differentLoginInfo.passwordField,
+ });
+
+ // Trying to modify a login that does not exist should throw.
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag()),
+ /No matching logins/);
+
+ // Add the first form login, then modify it to match the second, changing
+ // only some of its properties and checking the behavior with an empty string.
+ Services.logins.addLogin(loginInfo);
+ Services.logins.modifyLogin(loginInfo, newPropertyBag({
+ username: "new username",
+ password: "new password",
+ usernameField: "",
+ passwordField: "new_form_field_password",
+ }));
+
+ // The data should now match the second login.
+ LoginTestUtils.checkLogins([updatedLoginInfo]);
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag()),
+ /No matching logins/);
+
+ // It is also possible to provide no properties to be modified.
+ Services.logins.modifyLogin(updatedLoginInfo, newPropertyBag());
+
+ // Specifying a null property for a required value should throw.
+ Assert.throws(() => Services.logins.modifyLogin(loginInfo, newPropertyBag({
+ usernameField: null,
+ })));
+
+ // The login can be changed to have a different type and hostname.
+ Services.logins.modifyLogin(updatedLoginInfo, differentLoginProperties);
+ LoginTestUtils.checkLogins([differentLoginInfo]);
+
+ // It is now possible to add a login with the old type and hostname.
+ Services.logins.addLogin(loginInfo);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ // Modifying a login to match an existing one should not be possible.
+ Assert.throws(
+ () => Services.logins.modifyLogin(loginInfo, differentLoginProperties),
+ /already exists/);
+ LoginTestUtils.checkLogins([loginInfo, differentLoginInfo]);
+
+ LoginTestUtils.clearData();
+});
+
+/**
+ * Tests the login deduplication function.
+ */
+add_task(function test_deduplicate_logins() {
+ // Different key attributes combinations and the amount of unique
+ // results expected for the TestData login list.
+ let keyCombinations = [
+ {
+ keyset: ["username", "password"],
+ results: 13,
+ },
+ {
+ keyset: ["hostname", "username"],
+ results: 17,
+ },
+ {
+ keyset: ["hostname", "username", "password"],
+ results: 18,
+ },
+ {
+ keyset: ["hostname", "username", "password", "formSubmitURL"],
+ results: 23,
+ },
+ ];
+
+ let logins = TestData.loginList();
+
+ for (let testCase of keyCombinations) {
+ // Deduplicate the logins using the current testcase keyset.
+ let deduped = LoginHelper.dedupeLogins(logins, testCase.keyset);
+ Assert.equal(deduped.length, testCase.results, "Correct amount of results.");
+
+ // Checks that every login after deduping is unique.
+ Assert.ok(deduped.every(loginA =>
+ deduped.every(loginB => !compareAttributes(loginA, loginB, testCase.keyset))
+ ), "Every login is unique.");
+ }
+});
+
+/**
+ * Ensure that the login deduplication function keeps the most recent login.
+ */
+add_task(function test_deduplicate_keeps_most_recent() {
+ // Logins to deduplicate.
+ let logins = [
+ TestData.formLogin({timeLastUsed: Date.UTC(2004, 11, 4, 0, 0, 0)}),
+ TestData.formLogin({formSubmitURL: "http://example.com", timeLastUsed: Date.UTC(2015, 11, 4, 0, 0, 0)}),
+ ];
+
+ // Deduplicate the logins.
+ let deduped = LoginHelper.dedupeLogins(logins);
+ Assert.equal(deduped.length, 1, "Deduplicated the logins array.");
+
+ // Verify that the remaining login have the most recent date.
+ let loginTimeLastUsed = deduped[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ Assert.equal(loginTimeLastUsed, Date.UTC(2015, 11, 4, 0, 0, 0), "Most recent login was kept.");
+
+ // Deduplicate the reverse logins array.
+ deduped = LoginHelper.dedupeLogins(logins.reverse());
+ Assert.equal(deduped.length, 1, "Deduplicated the reversed logins array.");
+
+ // Verify that the remaining login have the most recent date.
+ loginTimeLastUsed = deduped[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ Assert.equal(loginTimeLastUsed, Date.UTC(2015, 11, 4, 0, 0, 0), "Most recent login was kept.");
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js b/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js
new file mode 100644
index 0000000000..ffbedb4de3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_decrypt_failure.js
@@ -0,0 +1,77 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the case where there are logins that cannot be decrypted.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Resets the token used to decrypt logins. This is equivalent to resetting the
+ * master password when it is not known.
+ */
+function resetMasterPassword()
+{
+ let token = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB).getInternalKeyToken();
+ token.reset();
+ token.changePassword("", "");
+}
+
+// Tests
+
+/**
+ * Resets the master password after some logins were added to the database.
+ */
+add_task(function test_logins_decrypt_failure()
+{
+ let logins = TestData.loginList();
+ for (let loginInfo of logins) {
+ Services.logins.addLogin(loginInfo);
+ }
+
+ // This makes the existing logins non-decryptable.
+ resetMasterPassword();
+
+ // These functions don't see the non-decryptable entries anymore.
+ do_check_eq(Services.logins.getAllLogins().length, 0);
+ do_check_eq(Services.logins.findLogins({}, "", "", "").length, 0);
+ do_check_eq(Services.logins.searchLogins({}, newPropertyBag()).length, 0);
+ Assert.throws(() => Services.logins.modifyLogin(logins[0], newPropertyBag()),
+ /No matching logins/);
+ Assert.throws(() => Services.logins.removeLogin(logins[0]),
+ /No matching logins/);
+
+ // The function that counts logins sees the non-decryptable entries also.
+ do_check_eq(Services.logins.countLogins("", "", ""), logins.length);
+
+ // Equivalent logins can be added.
+ for (let loginInfo of logins) {
+ Services.logins.addLogin(loginInfo);
+ }
+ LoginTestUtils.checkLogins(logins);
+ do_check_eq(Services.logins.countLogins("", "", ""), logins.length * 2);
+
+ // Finding logins doesn't return the non-decryptable duplicates.
+ do_check_eq(Services.logins.findLogins({}, "http://www.example.com",
+ "", "").length, 1);
+ let matchData = newPropertyBag({ hostname: "http://www.example.com" });
+ do_check_eq(Services.logins.searchLogins({}, matchData).length, 1);
+
+ // Removing single logins does not remove non-decryptable logins.
+ for (let loginInfo of TestData.loginList()) {
+ Services.logins.removeLogin(loginInfo);
+ }
+ do_check_eq(Services.logins.getAllLogins().length, 0);
+ do_check_eq(Services.logins.countLogins("", "", ""), logins.length);
+
+ // Removing all logins removes the non-decryptable entries also.
+ Services.logins.removeAllLogins();
+ do_check_eq(Services.logins.getAllLogins().length, 0);
+ do_check_eq(Services.logins.countLogins("", "", ""), 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js b/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js
new file mode 100644
index 0000000000..38344aa7d9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_metainfo.js
@@ -0,0 +1,284 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the handling of nsILoginMetaInfo by methods that add, remove, modify,
+ * and find logins.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+var gLooksLikeUUIDRegex = /^\{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\}$/;
+
+/**
+ * Retrieves the only login among the current data that matches the hostname of
+ * the given nsILoginInfo. In case there is more than one login for the
+ * hostname, the test fails.
+ */
+function retrieveLoginMatching(aLoginInfo)
+{
+ let logins = Services.logins.findLogins({}, aLoginInfo.hostname, "", "");
+ do_check_eq(logins.length, 1);
+ return logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+}
+
+/**
+ * Checks that the nsILoginInfo and nsILoginMetaInfo properties of two different
+ * login instances are equal.
+ */
+function assertMetaInfoEqual(aActual, aExpected)
+{
+ do_check_neq(aActual, aExpected);
+
+ // Check the nsILoginInfo properties.
+ do_check_true(aActual.equals(aExpected));
+
+ // Check the nsILoginMetaInfo properties.
+ do_check_eq(aActual.guid, aExpected.guid);
+ do_check_eq(aActual.timeCreated, aExpected.timeCreated);
+ do_check_eq(aActual.timeLastUsed, aExpected.timeLastUsed);
+ do_check_eq(aActual.timePasswordChanged, aExpected.timePasswordChanged);
+ do_check_eq(aActual.timesUsed, aExpected.timesUsed);
+}
+
+/**
+ * nsILoginInfo instances with or without nsILoginMetaInfo properties.
+ */
+var gLoginInfo1;
+var gLoginInfo2;
+var gLoginInfo3;
+
+/**
+ * nsILoginInfo instances reloaded with all the nsILoginMetaInfo properties.
+ * These are often used to provide the reference values to test against.
+ */
+var gLoginMetaInfo1;
+var gLoginMetaInfo2;
+var gLoginMetaInfo3;
+
+// Tests
+
+/**
+ * Prepare the test objects that will be used by the following tests.
+ */
+add_task(function test_initialize()
+{
+ // Use a reference time from ten minutes ago to initialize one instance of
+ // nsILoginMetaInfo, to test that reference times are updated when needed.
+ let baseTimeMs = Date.now() - 600000;
+
+ gLoginInfo1 = TestData.formLogin();
+ gLoginInfo2 = TestData.formLogin({
+ hostname: "http://other.example.com",
+ guid: gUUIDGenerator.generateUUID().toString(),
+ timeCreated: baseTimeMs,
+ timeLastUsed: baseTimeMs + 2,
+ timePasswordChanged: baseTimeMs + 1,
+ timesUsed: 2,
+ });
+ gLoginInfo3 = TestData.authLogin();
+});
+
+/**
+ * Tests the behavior of addLogin with regard to metadata. The logins added
+ * here are also used by the following tests.
+ */
+add_task(function test_addLogin_metainfo()
+{
+ // Add a login without metadata to the database.
+ Services.logins.addLogin(gLoginInfo1);
+
+ // The object provided to addLogin should not have been modified.
+ do_check_eq(gLoginInfo1.guid, null);
+ do_check_eq(gLoginInfo1.timeCreated, 0);
+ do_check_eq(gLoginInfo1.timeLastUsed, 0);
+ do_check_eq(gLoginInfo1.timePasswordChanged, 0);
+ do_check_eq(gLoginInfo1.timesUsed, 0);
+
+ // A login with valid metadata should have been stored.
+ gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1);
+ do_check_true(gLooksLikeUUIDRegex.test(gLoginMetaInfo1.guid));
+ let creationTime = gLoginMetaInfo1.timeCreated;
+ LoginTestUtils.assertTimeIsAboutNow(creationTime);
+ do_check_eq(gLoginMetaInfo1.timeLastUsed, creationTime);
+ do_check_eq(gLoginMetaInfo1.timePasswordChanged, creationTime);
+ do_check_eq(gLoginMetaInfo1.timesUsed, 1);
+
+ // Add a login without metadata to the database.
+ let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo);
+ Services.logins.addLogin(gLoginInfo2);
+
+ // The object provided to addLogin should not have been modified.
+ assertMetaInfoEqual(gLoginInfo2, originalLogin);
+
+ // A login with the provided metadata should have been stored.
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ assertMetaInfoEqual(gLoginMetaInfo2, gLoginInfo2);
+
+ // Add an authentication login to the database before continuing.
+ Services.logins.addLogin(gLoginInfo3);
+ gLoginMetaInfo3 = retrieveLoginMatching(gLoginInfo3);
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+});
+
+/**
+ * Tests that adding a login with a duplicate GUID throws an exception.
+ */
+add_task(function test_addLogin_metainfo_duplicate()
+{
+ let loginInfo = TestData.formLogin({
+ hostname: "http://duplicate.example.com",
+ guid: gLoginMetaInfo2.guid,
+ });
+ Assert.throws(() => Services.logins.addLogin(loginInfo),
+ /specified GUID already exists/);
+
+ // Verify that no data was stored by the previous call.
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+});
+
+/**
+ * Tests that the existing metadata is not changed when modifyLogin is called
+ * with an nsILoginInfo argument.
+ */
+add_task(function test_modifyLogin_nsILoginInfo_metainfo_ignored()
+{
+ let newLoginInfo = gLoginInfo1.clone().QueryInterface(Ci.nsILoginMetaInfo);
+ newLoginInfo.guid = gUUIDGenerator.generateUUID().toString();
+ newLoginInfo.timeCreated = Date.now();
+ newLoginInfo.timeLastUsed = Date.now();
+ newLoginInfo.timePasswordChanged = Date.now();
+ newLoginInfo.timesUsed = 12;
+ Services.logins.modifyLogin(gLoginInfo1, newLoginInfo);
+
+ newLoginInfo = retrieveLoginMatching(gLoginInfo1);
+ assertMetaInfoEqual(newLoginInfo, gLoginMetaInfo1);
+});
+
+/**
+ * Tests the modifyLogin function with an nsIProperyBag argument.
+ */
+add_task(function test_modifyLogin_nsIProperyBag_metainfo()
+{
+ // Use a new reference time that is two minutes from now.
+ let newTimeMs = Date.now() + 120000;
+ let newUUIDValue = gUUIDGenerator.generateUUID().toString();
+
+ // Check that properties are changed as requested.
+ Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({
+ guid: newUUIDValue,
+ timeCreated: newTimeMs,
+ timeLastUsed: newTimeMs + 2,
+ timePasswordChanged: newTimeMs + 1,
+ timesUsed: 2,
+ }));
+
+ gLoginMetaInfo1 = retrieveLoginMatching(gLoginInfo1);
+ do_check_eq(gLoginMetaInfo1.guid, newUUIDValue);
+ do_check_eq(gLoginMetaInfo1.timeCreated, newTimeMs);
+ do_check_eq(gLoginMetaInfo1.timeLastUsed, newTimeMs + 2);
+ do_check_eq(gLoginMetaInfo1.timePasswordChanged, newTimeMs + 1);
+ do_check_eq(gLoginMetaInfo1.timesUsed, 2);
+
+ // Check that timePasswordChanged is updated when changing the password.
+ let originalLogin = gLoginInfo2.clone().QueryInterface(Ci.nsILoginMetaInfo);
+ Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
+ password: "new password",
+ }));
+ gLoginInfo2.password = "new password";
+
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password);
+ do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
+ do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
+ LoginTestUtils.assertTimeIsAboutNow(gLoginMetaInfo2.timePasswordChanged);
+
+ // Check that timePasswordChanged is not set to the current time when changing
+ // the password and specifying a new value for the property at the same time.
+ Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
+ password: "other password",
+ timePasswordChanged: newTimeMs,
+ }));
+ gLoginInfo2.password = "other password";
+
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ do_check_eq(gLoginMetaInfo2.password, gLoginInfo2.password);
+ do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
+ do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
+ do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs);
+
+ // Check the special timesUsedIncrement property.
+ Services.logins.modifyLogin(gLoginInfo2, newPropertyBag({
+ timesUsedIncrement: 2,
+ }));
+
+ gLoginMetaInfo2 = retrieveLoginMatching(gLoginInfo2);
+ do_check_eq(gLoginMetaInfo2.timeCreated, originalLogin.timeCreated);
+ do_check_eq(gLoginMetaInfo2.timeLastUsed, originalLogin.timeLastUsed);
+ do_check_eq(gLoginMetaInfo2.timePasswordChanged, newTimeMs);
+ do_check_eq(gLoginMetaInfo2.timesUsed, 4);
+});
+
+/**
+ * Tests that modifying a login to a duplicate GUID throws an exception.
+ */
+add_task(function test_modifyLogin_nsIProperyBag_metainfo_duplicate()
+{
+ Assert.throws(() => Services.logins.modifyLogin(gLoginInfo1, newPropertyBag({
+ guid: gLoginInfo2.guid,
+ })), /specified GUID already exists/);
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+});
+
+/**
+ * Tests searching logins using nsILoginMetaInfo properties.
+ */
+add_task(function test_searchLogins_metainfo()
+{
+ // Find by GUID.
+ let logins = Services.logins.searchLogins({}, newPropertyBag({
+ guid: gLoginMetaInfo1.guid,
+ }));
+ do_check_eq(logins.length, 1);
+ let foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ assertMetaInfoEqual(foundLogin, gLoginMetaInfo1);
+
+ // Find by timestamp.
+ logins = Services.logins.searchLogins({}, newPropertyBag({
+ timePasswordChanged: gLoginMetaInfo2.timePasswordChanged,
+ }));
+ do_check_eq(logins.length, 1);
+ foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ assertMetaInfoEqual(foundLogin, gLoginMetaInfo2);
+
+ // Find using two properties at the same time.
+ logins = Services.logins.searchLogins({}, newPropertyBag({
+ guid: gLoginMetaInfo3.guid,
+ timePasswordChanged: gLoginMetaInfo3.timePasswordChanged,
+ }));
+ do_check_eq(logins.length, 1);
+ foundLogin = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ assertMetaInfoEqual(foundLogin, gLoginMetaInfo3);
+});
+
+/**
+ * Tests that the default nsILoginManagerStorage module attached to the Login
+ * Manager service is able to save and reload nsILoginMetaInfo properties.
+ */
+add_task(function* test_storage_metainfo()
+{
+ yield* LoginTestUtils.reloadData();
+ LoginTestUtils.checkLogins([gLoginInfo1, gLoginInfo2, gLoginInfo3]);
+
+ assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo1), gLoginMetaInfo1);
+ assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo2), gLoginMetaInfo2);
+ assertMetaInfoEqual(retrieveLoginMatching(gLoginInfo3), gLoginMetaInfo3);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_logins_search.js b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
new file mode 100644
index 0000000000..188c75039d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
@@ -0,0 +1,221 @@
+/*
+ * Tests methods that find specific logins in the store (findLogins,
+ * searchLogins, and countLogins).
+ *
+ * The getAllLogins method is not tested explicitly here, because it is used by
+ * all tests to verify additions, removals and modifications to the login store.
+ */
+
+"use strict";
+
+// Globals
+
+/**
+ * Returns a list of new nsILoginInfo objects that are a subset of the test
+ * data, built to match the specified query.
+ *
+ * @param aQuery
+ * Each property and value of this object restricts the search to those
+ * entries from the test data that match the property exactly.
+ */
+function buildExpectedLogins(aQuery)
+{
+ return TestData.loginList().filter(
+ entry => Object.keys(aQuery).every(name => entry[name] === aQuery[name]));
+}
+
+/**
+ * Tests the searchLogins function.
+ *
+ * @param aQuery
+ * Each property and value of this object is translated to an entry in
+ * the nsIPropertyBag parameter of searchLogins.
+ * @param aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkSearchLogins(aQuery, aExpectedCount)
+{
+ do_print("Testing searchLogins for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ let outCount = {};
+ let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery));
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+}
+
+/**
+ * Tests findLogins, searchLogins, and countLogins with the same query.
+ *
+ * @param aQuery
+ * The "hostname", "formSubmitURL", and "httpRealm" properties of this
+ * object are passed as parameters to findLogins and countLogins. The
+ * same object is then passed to the checkSearchLogins function.
+ * @param aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkAllSearches(aQuery, aExpectedCount)
+{
+ do_print("Testing all search functions for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ // The findLogins and countLogins functions support wildcard matches by
+ // specifying empty strings as parameters, while searchLogins requires
+ // omitting the property entirely.
+ let hostname = ("hostname" in aQuery) ? aQuery.hostname : "";
+ let formSubmitURL = ("formSubmitURL" in aQuery) ? aQuery.formSubmitURL : "";
+ let httpRealm = ("httpRealm" in aQuery) ? aQuery.httpRealm : "";
+
+ // Test findLogins.
+ let outCount = {};
+ let logins = Services.logins.findLogins(outCount, hostname, formSubmitURL,
+ httpRealm);
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+
+ // Test countLogins.
+ let count = Services.logins.countLogins(hostname, formSubmitURL, httpRealm);
+ do_check_eq(count, expectedLogins.length);
+
+ // Test searchLogins.
+ checkSearchLogins(aQuery, aExpectedCount);
+}
+
+// Tests
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function test_initialize()
+{
+ for (let login of TestData.loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests findLogins, searchLogins, and countLogins with basic queries.
+ */
+add_task(function test_search_all_basic()
+{
+ // Find all logins, using no filters in the search functions.
+ checkAllSearches({}, 23);
+
+ // Find all form logins, then all authentication logins.
+ checkAllSearches({ httpRealm: null }, 14);
+ checkAllSearches({ formSubmitURL: null }, 9);
+
+ // Find all form logins on one host, then all authentication logins.
+ checkAllSearches({ hostname: "http://www4.example.com",
+ httpRealm: null }, 3);
+ checkAllSearches({ hostname: "http://www2.example.org",
+ formSubmitURL: null }, 2);
+
+ // Verify that scheme and subdomain are distinct in the hostname.
+ checkAllSearches({ hostname: "http://www.example.com" }, 1);
+ checkAllSearches({ hostname: "https://www.example.com" }, 1);
+ checkAllSearches({ hostname: "https://example.com" }, 1);
+ checkAllSearches({ hostname: "http://www3.example.com" }, 3);
+
+ // Verify that scheme and subdomain are distinct in formSubmitURL.
+ checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
+ checkAllSearches({ formSubmitURL: "https://www.example.com" }, 2);
+ checkAllSearches({ formSubmitURL: "http://example.com" }, 1);
+
+ // Find by formSubmitURL on a single host.
+ checkAllSearches({ hostname: "http://www3.example.com",
+ formSubmitURL: "http://www.example.com" }, 1);
+ checkAllSearches({ hostname: "http://www3.example.com",
+ formSubmitURL: "https://www.example.com" }, 1);
+ checkAllSearches({ hostname: "http://www3.example.com",
+ formSubmitURL: "http://example.com" }, 1);
+
+ // Find by httpRealm on all hosts.
+ checkAllSearches({ httpRealm: "The HTTP Realm" }, 3);
+ checkAllSearches({ httpRealm: "ftp://ftp.example.org" }, 1);
+ checkAllSearches({ httpRealm: "The HTTP Realm Other" }, 2);
+
+ // Find by httpRealm on a single host.
+ checkAllSearches({ hostname: "http://example.net",
+ httpRealm: "The HTTP Realm" }, 1);
+ checkAllSearches({ hostname: "http://example.net",
+ httpRealm: "The HTTP Realm Other" }, 1);
+ checkAllSearches({ hostname: "ftp://example.net",
+ httpRealm: "ftp://example.net" }, 1);
+});
+
+/**
+ * Tests searchLogins with advanced queries.
+ */
+add_task(function test_searchLogins()
+{
+ checkSearchLogins({ usernameField: "form_field_username" }, 12);
+ checkSearchLogins({ passwordField: "form_field_password" }, 13);
+
+ // Find all logins with an empty usernameField, including for authentication.
+ checkSearchLogins({ usernameField: "" }, 11);
+
+ // Find form logins with an empty usernameField.
+ checkSearchLogins({ httpRealm: null,
+ usernameField: "" }, 2);
+
+ // Find logins with an empty usernameField on one host.
+ checkSearchLogins({ hostname: "http://www6.example.com",
+ usernameField: "" }, 1);
+});
+
+/**
+ * Tests searchLogins with invalid arguments.
+ */
+add_task(function test_searchLogins_invalid()
+{
+ Assert.throws(() => Services.logins.searchLogins({},
+ newPropertyBag({ username: "value" })),
+ /Unexpected field/);
+});
+
+/**
+ * Tests that matches are case-sensitive, compare the full field value, and are
+ * strict when interpreting the prePath of URIs.
+ */
+add_task(function test_search_all_full_case_sensitive()
+{
+ checkAllSearches({ hostname: "http://www.example.com" }, 1);
+ checkAllSearches({ hostname: "http://www.example.com/" }, 0);
+ checkAllSearches({ hostname: "http://" }, 0);
+ checkAllSearches({ hostname: "example.com" }, 0);
+
+ checkAllSearches({ formSubmitURL: "http://www.example.com" }, 2);
+ checkAllSearches({ formSubmitURL: "http://www.example.com/" }, 0);
+ checkAllSearches({ formSubmitURL: "http://" }, 0);
+ checkAllSearches({ formSubmitURL: "example.com" }, 0);
+
+ checkAllSearches({ httpRealm: "The HTTP Realm" }, 3);
+ checkAllSearches({ httpRealm: "The http Realm" }, 0);
+ checkAllSearches({ httpRealm: "The HTTP" }, 0);
+ checkAllSearches({ httpRealm: "Realm" }, 0);
+});
+
+/**
+ * Tests findLogins, searchLogins, and countLogins with queries that should
+ * return no values.
+ */
+add_task(function test_search_all_empty()
+{
+ checkAllSearches({ hostname: "http://nonexistent.example.com" }, 0);
+ checkAllSearches({ formSubmitURL: "http://www.example.com",
+ httpRealm: "The HTTP Realm" }, 0);
+
+ checkSearchLogins({ hostname: "" }, 0);
+ checkSearchLogins({ id: "1000" }, 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js b/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js
new file mode 100644
index 0000000000..19175df59b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_maybeImportLogin.js
@@ -0,0 +1,169 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+const HOST1 = "https://www.example.com/";
+const HOST2 = "https://www.mozilla.org/";
+
+const USER1 = "myuser";
+const USER2 = "anotheruser";
+
+const PASS1 = "mypass";
+const PASS2 = "anotherpass";
+const PASS3 = "yetanotherpass";
+
+add_task(function test_new_logins() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST2,
+ formSubmitURL: HOST2,
+ });
+
+ Assert.ok(importedLogin, "Return value should indicate another imported login.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST2});
+ Assert.equal(matchingLogins.length, 1, `There should also be 1 login for ${HOST2}`);
+ Assert.equal(Services.logins.getAllLogins().length, 2, "There should be 2 logins in total");
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_duplicate_logins() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(!importedLogin, "Return value should indicate no new login was imported.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_different_passwords() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ timeCreated: new Date(Date.now() - 1000),
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ // This item will be newer, so its password should take precedence.
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS2,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ timeCreated: new Date(),
+ });
+ Assert.ok(!importedLogin, "Return value should not indicate imported login (as we updated an existing one).");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Assert.equal(matchingLogins[0].password, PASS2, "We should have updated the password for this login.");
+
+ // Now try to update with an older password:
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS3,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ timeCreated: new Date(Date.now() - 1000000),
+ });
+ Assert.ok(!importedLogin, "Return value should not indicate imported login (as we didn't update anything).");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Assert.equal(matchingLogins[0].password, PASS2, "We should NOT have updated the password for this login.");
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_different_usernames() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER2,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate another imported login.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 2, `There should now be 2 logins for ${HOST1}`);
+
+ Services.logins.removeAllLogins();
+});
+
+add_task(function test_different_targets() {
+ let importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ formSubmitURL: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate imported login.");
+ let matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should be 1 login for ${HOST1}`);
+
+ // Not passing either a formSubmitURL or a httpRealm should be treated as
+ // the same as the previous login
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ });
+ Assert.ok(!importedLogin, "Return value should NOT indicate imported login " +
+ "(because a missing formSubmitURL and httpRealm should be duped to the existing login).");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 1, `There should still be 1 login for ${HOST1}`);
+ Assert.equal(matchingLogins[0].formSubmitURL, HOST1, "The form submission URL should have been kept.");
+
+ importedLogin = LoginHelper.maybeImportLogin({
+ username: USER1,
+ password: PASS1,
+ hostname: HOST1,
+ httpRealm: HOST1,
+ });
+ Assert.ok(importedLogin, "Return value should indicate another imported login " +
+ "as an httpRealm login shouldn't be duped.");
+ matchingLogins = LoginHelper.searchLoginsWithObject({hostname: HOST1});
+ Assert.equal(matchingLogins.length, 2, `There should now be 2 logins for ${HOST1}`);
+
+ Services.logins.removeAllLogins();
+});
+
diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js
new file mode 100644
index 0000000000..b8793e1bd9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginImport.js
@@ -0,0 +1,243 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the LoginImport object.
+ */
+
+"use strict";
+
+// Globals
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
+ "resource://gre/modules/LoginImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gLoginManagerCrypto",
+ "@mozilla.org/login-manager/crypto/SDR;1",
+ "nsILoginManagerCrypto");
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+/**
+ * Creates empty login data tables in the given SQLite connection, resembling
+ * the most recent schema version (excluding indices).
+ */
+function promiseCreateDatabaseSchema(aConnection)
+{
+ return Task.spawn(function* () {
+ yield aConnection.setSchemaVersion(5);
+ yield aConnection.execute("CREATE TABLE moz_logins (" +
+ "id INTEGER PRIMARY KEY," +
+ "hostname TEXT NOT NULL," +
+ "httpRealm TEXT," +
+ "formSubmitURL TEXT," +
+ "usernameField TEXT NOT NULL," +
+ "passwordField TEXT NOT NULL," +
+ "encryptedUsername TEXT NOT NULL," +
+ "encryptedPassword TEXT NOT NULL," +
+ "guid TEXT," +
+ "encType INTEGER," +
+ "timeCreated INTEGER," +
+ "timeLastUsed INTEGER," +
+ "timePasswordChanged INTEGER," +
+ "timesUsed INTEGER)");
+ yield aConnection.execute("CREATE TABLE moz_disabledHosts (" +
+ "id INTEGER PRIMARY KEY," +
+ "hostname TEXT UNIQUE)");
+ yield aConnection.execute("CREATE TABLE moz_deleted_logins (" +
+ "id INTEGER PRIMARY KEY," +
+ "guid TEXT," +
+ "timeDeleted INTEGER)");
+ });
+}
+
+/**
+ * Inserts a new entry in the database resembling the given nsILoginInfo object.
+ */
+function promiseInsertLoginInfo(aConnection, aLoginInfo)
+{
+ aLoginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+
+ // We can't use the aLoginInfo object directly in the execute statement
+ // because the bind code in Sqlite.jsm doesn't allow objects with extra
+ // properties beyond those being binded. So we might as well use an array as
+ // it is simpler.
+ let values = [
+ aLoginInfo.hostname,
+ aLoginInfo.httpRealm,
+ aLoginInfo.formSubmitURL,
+ aLoginInfo.usernameField,
+ aLoginInfo.passwordField,
+ gLoginManagerCrypto.encrypt(aLoginInfo.username),
+ gLoginManagerCrypto.encrypt(aLoginInfo.password),
+ aLoginInfo.guid,
+ aLoginInfo.encType,
+ aLoginInfo.timeCreated,
+ aLoginInfo.timeLastUsed,
+ aLoginInfo.timePasswordChanged,
+ aLoginInfo.timesUsed,
+ ];
+
+ return aConnection.execute("INSERT INTO moz_logins (hostname, " +
+ "httpRealm, formSubmitURL, usernameField, " +
+ "passwordField, encryptedUsername, " +
+ "encryptedPassword, guid, encType, timeCreated, " +
+ "timeLastUsed, timePasswordChanged, timesUsed) " +
+ "VALUES (?" + ",?".repeat(12) + ")", values);
+}
+
+/**
+ * Inserts a new disabled host entry in the database.
+ */
+function promiseInsertDisabledHost(aConnection, aHostname)
+{
+ return aConnection.execute("INSERT INTO moz_disabledHosts (hostname) " +
+ "VALUES (?)", [aHostname]);
+}
+
+// Tests
+
+/**
+ * Imports login data from a SQLite file constructed using the test data.
+ */
+add_task(function* test_import()
+{
+ let store = new LoginStore(getTempFile("test-import.json").path);
+ let loginsSqlite = getTempFile("test-logins.sqlite").path;
+
+ // Prepare the logins to be imported, including the nsILoginMetaInfo data.
+ let loginList = TestData.loginList();
+ for (let loginInfo of loginList) {
+ loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
+ loginInfo.guid = gUUIDGenerator.generateUUID().toString();
+ loginInfo.timeCreated = Date.now();
+ loginInfo.timeLastUsed = Date.now();
+ loginInfo.timePasswordChanged = Date.now();
+ loginInfo.timesUsed = 1;
+ }
+
+ // Create and populate the SQLite database first.
+ let connection = yield Sqlite.openConnection({ path: loginsSqlite });
+ try {
+ yield promiseCreateDatabaseSchema(connection);
+ for (let loginInfo of loginList) {
+ yield promiseInsertLoginInfo(connection, loginInfo);
+ }
+ yield promiseInsertDisabledHost(connection, "http://www.example.com");
+ yield promiseInsertDisabledHost(connection, "https://www.example.org");
+ } finally {
+ yield connection.close();
+ }
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ yield new LoginImport(store, loginsSqlite).import();
+
+ // Verify that every login in the test data has a matching imported row.
+ do_check_eq(loginList.length, store.data.logins.length);
+ do_check_true(loginList.every(function (loginInfo) {
+ return store.data.logins.some(function (loginDataItem) {
+ let username = gLoginManagerCrypto.decrypt(loginDataItem.encryptedUsername);
+ let password = gLoginManagerCrypto.decrypt(loginDataItem.encryptedPassword);
+ return loginDataItem.hostname == loginInfo.hostname &&
+ loginDataItem.httpRealm == loginInfo.httpRealm &&
+ loginDataItem.formSubmitURL == loginInfo.formSubmitURL &&
+ loginDataItem.usernameField == loginInfo.usernameField &&
+ loginDataItem.passwordField == loginInfo.passwordField &&
+ username == loginInfo.username &&
+ password == loginInfo.password &&
+ loginDataItem.guid == loginInfo.guid &&
+ loginDataItem.encType == loginInfo.encType &&
+ loginDataItem.timeCreated == loginInfo.timeCreated &&
+ loginDataItem.timeLastUsed == loginInfo.timeLastUsed &&
+ loginDataItem.timePasswordChanged == loginInfo.timePasswordChanged &&
+ loginDataItem.timesUsed == loginInfo.timesUsed;
+ });
+ }));
+
+ // Verify that disabled hosts have been imported.
+ do_check_eq(store.data.disabledHosts.length, 2);
+ do_check_true(store.data.disabledHosts.indexOf("http://www.example.com") != -1);
+ do_check_true(store.data.disabledHosts.indexOf("https://www.example.org") != -1);
+});
+
+/**
+ * Tests imports of NULL values due to a downgraded database.
+ */
+add_task(function* test_import_downgraded()
+{
+ let store = new LoginStore(getTempFile("test-import-downgraded.json").path);
+ let loginsSqlite = getTempFile("test-logins-downgraded.sqlite").path;
+
+ // Create and populate the SQLite database first.
+ let connection = yield Sqlite.openConnection({ path: loginsSqlite });
+ try {
+ yield promiseCreateDatabaseSchema(connection);
+ yield connection.setSchemaVersion(3);
+ yield promiseInsertLoginInfo(connection, TestData.formLogin({
+ guid: gUUIDGenerator.generateUUID().toString(),
+ timeCreated: null,
+ timeLastUsed: null,
+ timePasswordChanged: null,
+ timesUsed: 0,
+ }));
+ } finally {
+ yield connection.close();
+ }
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ yield new LoginImport(store, loginsSqlite).import();
+
+ // Verify that the missing metadata was generated correctly.
+ let loginItem = store.data.logins[0];
+ let creationTime = loginItem.timeCreated;
+ LoginTestUtils.assertTimeIsAboutNow(creationTime);
+ do_check_eq(loginItem.timeLastUsed, creationTime);
+ do_check_eq(loginItem.timePasswordChanged, creationTime);
+ do_check_eq(loginItem.timesUsed, 1);
+});
+
+/**
+ * Verifies that importing from a SQLite file with database version 2 fails.
+ */
+add_task(function* test_import_v2()
+{
+ let store = new LoginStore(getTempFile("test-import-v2.json").path);
+ let loginsSqlite = do_get_file("data/signons-v2.sqlite").path;
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ try {
+ yield new LoginImport(store, loginsSqlite).import();
+ do_throw("The operation should have failed.");
+ } catch (ex) { }
+});
+
+/**
+ * Imports login data from a SQLite file, with database version 3.
+ */
+add_task(function* test_import_v3()
+{
+ let store = new LoginStore(getTempFile("test-import-v3.json").path);
+ let loginsSqlite = do_get_file("data/signons-v3.sqlite").path;
+
+ // The "load" method must be called before importing data.
+ yield store.load();
+ yield new LoginImport(store, loginsSqlite).import();
+
+ // We only execute basic integrity checks.
+ do_check_eq(store.data.logins[0].usernameField, "u1");
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js b/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js
new file mode 100644
index 0000000000..335eb601bf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_module_LoginStore.js
@@ -0,0 +1,206 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the LoginStore object.
+ */
+
+"use strict";
+
+// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
+ "resource://gre/modules/LoginStore.jsm");
+
+const TEST_STORE_FILE_NAME = "test-logins.json";
+
+// Tests
+
+/**
+ * Saves login data to a file, then reloads it.
+ */
+add_task(function* test_save_reload()
+{
+ let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ // The "load" method must be called before preparing the data to be saved.
+ yield storeForSave.load();
+
+ let rawLoginData = {
+ id: storeForSave.data.nextId++,
+ hostname: "http://www.example.com",
+ httpRealm: null,
+ formSubmitURL: "http://www.example.com/submit-url",
+ usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
+ passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
+ encryptedUsername: "(test)",
+ encryptedPassword: "(test)",
+ guid: "(test)",
+ encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
+ timeCreated: Date.now(),
+ timeLastUsed: Date.now(),
+ timePasswordChanged: Date.now(),
+ timesUsed: 1,
+ };
+ storeForSave.data.logins.push(rawLoginData);
+
+ storeForSave.data.disabledHosts.push("http://www.example.org");
+
+ yield storeForSave._save();
+
+ // Test the asynchronous initialization path.
+ let storeForLoad = new LoginStore(storeForSave.path);
+ yield storeForLoad.load();
+
+ do_check_eq(storeForLoad.data.logins.length, 1);
+ do_check_matches(storeForLoad.data.logins[0], rawLoginData);
+ do_check_eq(storeForLoad.data.disabledHosts.length, 1);
+ do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
+
+ // Test the synchronous initialization path.
+ storeForLoad = new LoginStore(storeForSave.path);
+ storeForLoad.ensureDataReady();
+
+ do_check_eq(storeForLoad.data.logins.length, 1);
+ do_check_matches(storeForLoad.data.logins[0], rawLoginData);
+ do_check_eq(storeForLoad.data.disabledHosts.length, 1);
+ do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
+});
+
+/**
+ * Checks that loading from a missing file results in empty arrays.
+ */
+add_task(function* test_load_empty()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ yield store.load();
+
+ do_check_false(yield OS.File.exists(store.path));
+
+ do_check_eq(store.data.logins.length, 0);
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
+
+/**
+ * Checks that saving empty data still overwrites any existing file.
+ */
+add_task(function* test_save_empty()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ yield store.load();
+
+ let createdFile = yield OS.File.open(store.path, { create: true });
+ yield createdFile.close();
+
+ yield store._save();
+
+ do_check_true(yield OS.File.exists(store.path));
+});
+
+/**
+ * Loads data from a string in a predefined format. The purpose of this test is
+ * to verify that the JSON format used in previous versions can be loaded.
+ */
+add_task(function* test_load_string_predefined()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ let string = "{\"logins\":[{" +
+ "\"id\":1," +
+ "\"hostname\":\"http://www.example.com\"," +
+ "\"httpRealm\":null," +
+ "\"formSubmitURL\":\"http://www.example.com/submit-url\"," +
+ "\"usernameField\":\"usernameField\"," +
+ "\"passwordField\":\"passwordField\"," +
+ "\"encryptedUsername\":\"(test)\"," +
+ "\"encryptedPassword\":\"(test)\"," +
+ "\"guid\":\"(test)\"," +
+ "\"encType\":1," +
+ "\"timeCreated\":1262304000000," +
+ "\"timeLastUsed\":1262390400000," +
+ "\"timePasswordChanged\":1262476800000," +
+ "\"timesUsed\":1}],\"disabledHosts\":[" +
+ "\"http://www.example.org\"]}";
+
+ yield OS.File.writeAtomic(store.path,
+ new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ do_check_eq(store.data.logins.length, 1);
+ do_check_matches(store.data.logins[0], {
+ id: 1,
+ hostname: "http://www.example.com",
+ httpRealm: null,
+ formSubmitURL: "http://www.example.com/submit-url",
+ usernameField: "usernameField",
+ passwordField: "passwordField",
+ encryptedUsername: "(test)",
+ encryptedPassword: "(test)",
+ guid: "(test)",
+ encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
+ timeCreated: 1262304000000,
+ timeLastUsed: 1262390400000,
+ timePasswordChanged: 1262476800000,
+ timesUsed: 1,
+ });
+
+ do_check_eq(store.data.disabledHosts.length, 1);
+ do_check_eq(store.data.disabledHosts[0], "http://www.example.org");
+});
+
+/**
+ * Loads login data from a malformed JSON string.
+ */
+add_task(function* test_load_string_malformed()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
+ "\"id\":1,";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ // A backup file should have been created.
+ do_check_true(yield OS.File.exists(store.path + ".corrupt"));
+ yield OS.File.remove(store.path + ".corrupt");
+
+ // The store should be ready to accept new data.
+ do_check_eq(store.data.logins.length, 0);
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
+
+/**
+ * Loads login data from a malformed JSON string, using the synchronous
+ * initialization path.
+ */
+add_task(function* test_load_string_malformed_sync()
+{
+ let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
+
+ let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
+ "\"id\":1,";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ store.ensureDataReady();
+
+ // A backup file should have been created.
+ do_check_true(yield OS.File.exists(store.path + ".corrupt"));
+ yield OS.File.remove(store.path + ".corrupt");
+
+ // The store should be ready to accept new data.
+ do_check_eq(store.data.logins.length, 0);
+ do_check_eq(store.data.disabledHosts.length, 0);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_notifications.js b/toolkit/components/passwordmgr/test/unit/test_notifications.js
new file mode 100644
index 0000000000..41caa2c1b3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_notifications.js
@@ -0,0 +1,172 @@
+/*
+ * Tests notifications dispatched when modifying stored logins.
+ */
+
+var expectedNotification;
+var expectedData;
+
+var TestObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ do_check_eq(topic, "passwordmgr-storage-changed");
+ do_check_eq(data, expectedNotification);
+
+ switch (data) {
+ case "addLogin":
+ do_check_true(subject instanceof Ci.nsILoginInfo);
+ do_check_true(subject instanceof Ci.nsILoginMetaInfo);
+ do_check_true(expectedData.equals(subject)); // nsILoginInfo.equals()
+ break;
+ case "modifyLogin":
+ do_check_true(subject instanceof Ci.nsIArray);
+ do_check_eq(subject.length, 2);
+ var oldLogin = subject.queryElementAt(0, Ci.nsILoginInfo);
+ var newLogin = subject.queryElementAt(1, Ci.nsILoginInfo);
+ do_check_true(expectedData[0].equals(oldLogin)); // nsILoginInfo.equals()
+ do_check_true(expectedData[1].equals(newLogin));
+ break;
+ case "removeLogin":
+ do_check_true(subject instanceof Ci.nsILoginInfo);
+ do_check_true(subject instanceof Ci.nsILoginMetaInfo);
+ do_check_true(expectedData.equals(subject)); // nsILoginInfo.equals()
+ break;
+ case "removeAllLogins":
+ do_check_eq(subject, null);
+ break;
+ case "hostSavingEnabled":
+ case "hostSavingDisabled":
+ do_check_true(subject instanceof Ci.nsISupportsString);
+ do_check_eq(subject.data, expectedData);
+ break;
+ default:
+ do_throw("Unhandled notification: " + data + " / " + topic);
+ }
+
+ expectedNotification = null; // ensure a duplicate is flagged as unexpected.
+ expectedData = null;
+ }
+};
+
+add_task(function test_notifications()
+{
+
+try {
+
+var testnum = 0;
+var testdesc = "Setup of nsLoginInfo test-users";
+
+var testuser1 = new LoginInfo("http://testhost1", "", null,
+ "dummydude", "itsasecret", "put_user_here", "put_pw_here");
+
+var testuser2 = new LoginInfo("http://testhost2", "", null,
+ "dummydude2", "itsasecret2", "put_user2_here", "put_pw2_here");
+
+Services.obs.addObserver(TestObserver, "passwordmgr-storage-changed", false);
+
+
+/* ========== 1 ========== */
+testnum = 1;
+testdesc = "Initial connection to storage module";
+
+/* ========== 2 ========== */
+testnum++;
+testdesc = "addLogin";
+
+expectedNotification = "addLogin";
+expectedData = testuser1;
+Services.logins.addLogin(testuser1);
+LoginTestUtils.checkLogins([testuser1]);
+do_check_eq(expectedNotification, null); // check that observer got a notification
+
+/* ========== 3 ========== */
+testnum++;
+testdesc = "modifyLogin";
+
+expectedNotification = "modifyLogin";
+expectedData = [testuser1, testuser2];
+Services.logins.modifyLogin(testuser1, testuser2);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([testuser2]);
+
+/* ========== 4 ========== */
+testnum++;
+testdesc = "removeLogin";
+
+expectedNotification = "removeLogin";
+expectedData = testuser2;
+Services.logins.removeLogin(testuser2);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 5 ========== */
+testnum++;
+testdesc = "removeAllLogins";
+
+expectedNotification = "removeAllLogins";
+expectedData = null;
+Services.logins.removeAllLogins();
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 6 ========== */
+testnum++;
+testdesc = "removeAllLogins (again)";
+
+expectedNotification = "removeAllLogins";
+expectedData = null;
+Services.logins.removeAllLogins();
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 7 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / false";
+
+expectedNotification = "hostSavingDisabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", false);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ ["http://site.com"]);
+
+/* ========== 8 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / false (again)";
+
+expectedNotification = "hostSavingDisabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", false);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.assertDisabledHostsEqual(Services.logins.getAllDisabledHosts(),
+ ["http://site.com"]);
+
+/* ========== 9 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / true";
+
+expectedNotification = "hostSavingEnabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", true);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+/* ========== 10 ========== */
+testnum++;
+testdesc = "setLoginSavingEnabled / true (again)";
+
+expectedNotification = "hostSavingEnabled";
+expectedData = "http://site.com";
+Services.logins.setLoginSavingEnabled("http://site.com", true);
+do_check_eq(expectedNotification, null);
+LoginTestUtils.checkLogins([]);
+
+Services.obs.removeObserver(TestObserver, "passwordmgr-storage-changed");
+
+LoginTestUtils.clearData();
+
+} catch (e) {
+ throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
+}
+
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_recipes_add.js b/toolkit/components/passwordmgr/test/unit/test_recipes_add.js
new file mode 100644
index 0000000000..ef5086c3b5
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_recipes_add.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests adding and retrieving LoginRecipes in the parent process.
+ */
+
+"use strict";
+
+add_task(function* test_init() {
+ let parent = new LoginRecipesParent({ defaults: null });
+ let initPromise1 = parent.initializationPromise;
+ let initPromise2 = parent.initializationPromise;
+ Assert.strictEqual(initPromise1, initPromise2, "Check that the same promise is returned");
+
+ let recipesParent = yield initPromise1;
+ Assert.ok(recipesParent instanceof LoginRecipesParent, "Check init return value");
+ Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes");
+});
+
+add_task(function* test_get_missing_host() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ let exampleRecipes = recipesParent.getRecipesForHost("example.invalid");
+ Assert.strictEqual(exampleRecipes.size, 0, "Check recipe count for example.invalid");
+
+});
+
+add_task(function* test_add_get_simple_host() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.strictEqual(recipesParent._recipesByHost.size, 0, "Initially 0 recipes");
+ recipesParent.add({
+ hosts: ["example.com"],
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
+});
+
+add_task(function* test_add_get_non_standard_port_host() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com:8080"],
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com:8080");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com:8080");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com:8080", "Check the one host");
+});
+
+add_task(function* test_add_multiple_hosts() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com", "foo.invalid"],
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 2,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 2, "Check that two hosts are present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the first host");
+ Assert.strictEqual(recipe.hosts[1], "foo.invalid", "Check the second host");
+
+ let fooRecipes = recipesParent.getRecipesForHost("foo.invalid");
+ Assert.strictEqual(fooRecipes.size, 1, "Check recipe count for foo.invalid");
+ let fooRecipe = [...fooRecipes][0];
+ Assert.strictEqual(fooRecipe, recipe, "Check that the recipe is shared");
+ Assert.strictEqual(typeof(fooRecipe), "object", "Check recipe type");
+ Assert.strictEqual(fooRecipe.hosts.length, 2, "Check that two hosts are present");
+ Assert.strictEqual(fooRecipe.hosts[0], "example.com", "Check the first host");
+ Assert.strictEqual(fooRecipe.hosts[1], "foo.invalid", "Check the second host");
+});
+
+add_task(function* test_add_pathRegex() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com"],
+ pathRegex: /^\/mypath\//,
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
+ Assert.strictEqual(recipe.pathRegex.toString(), "/^\\/mypath\\//", "Check the pathRegex");
+});
+
+add_task(function* test_add_selectors() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ recipesParent.add({
+ hosts: ["example.com"],
+ usernameSelector: "#my-username",
+ passwordSelector: "#my-form > input.password",
+ });
+ Assert.strictEqual(recipesParent._recipesByHost.size, 1,
+ "Check number of hosts after the addition");
+
+ let exampleRecipes = recipesParent.getRecipesForHost("example.com");
+ Assert.strictEqual(exampleRecipes.size, 1, "Check recipe count for example.com");
+ let recipe = [...exampleRecipes][0];
+ Assert.strictEqual(typeof(recipe), "object", "Check recipe type");
+ Assert.strictEqual(recipe.hosts.length, 1, "Check that one host is present");
+ Assert.strictEqual(recipe.hosts[0], "example.com", "Check the one host");
+ Assert.strictEqual(recipe.usernameSelector, "#my-username", "Check the usernameSelector");
+ Assert.strictEqual(recipe.passwordSelector, "#my-form > input.password", "Check the passwordSelector");
+});
+
+/* Begin checking errors with add */
+
+add_task(function* test_add_missing_prop() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({}), /required/, "Some properties are required");
+});
+
+add_task(function* test_add_unknown_prop() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ unknownProp: true,
+ }), /supported/, "Unknown properties should cause an error to help with typos");
+});
+
+add_task(function* test_add_invalid_hosts() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: 404,
+ }), /array/, "hosts should be an array");
+});
+
+add_task(function* test_add_empty_host_array() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: [],
+ }), /array/, "hosts should be a non-empty array");
+});
+
+add_task(function* test_add_pathRegex_non_regexp() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: ["example.com"],
+ pathRegex: "foo",
+ }), /regular expression/, "pathRegex should be a RegExp");
+});
+
+add_task(function* test_add_usernameSelector_non_string() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: ["example.com"],
+ usernameSelector: 404,
+ }), /string/, "usernameSelector should be a string");
+});
+
+add_task(function* test_add_passwordSelector_non_string() {
+ let recipesParent = yield RecipeHelpers.initNewParent();
+ Assert.throws(() => recipesParent.add({
+ hosts: ["example.com"],
+ passwordSelector: 404,
+ }), /string/, "passwordSelector should be a string");
+});
+
+/* End checking errors with add */
diff --git a/toolkit/components/passwordmgr/test/unit/test_recipes_content.js b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js
new file mode 100644
index 0000000000..3d37514520
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test filtering recipes in LoginRecipesContent.
+ */
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+add_task(function* test_getFieldOverrides() {
+ let recipes = new Set([
+ { // path doesn't match but otherwise good
+ hosts: ["example.com:8080"],
+ passwordSelector: "#password",
+ pathRegex: /^\/$/,
+ usernameSelector: ".username",
+ },
+ { // match with no field overrides
+ hosts: ["example.com:8080"],
+ },
+ { // best match (field selectors + path match)
+ description: "best match",
+ hosts: ["a.invalid", "example.com:8080", "other.invalid"],
+ passwordSelector: "#password",
+ pathRegex: /^\/first\/second\/$/,
+ usernameSelector: ".username",
+ },
+ ]);
+
+ let form = MockDocument.createTestDocument("http://localhost:8080/first/second/", "<form>").
+ forms[0];
+ let override = LoginRecipesContent.getFieldOverrides(recipes, form);
+ Assert.strictEqual(override.description, "best match",
+ "Check the best field override recipe was returned");
+ Assert.strictEqual(override.usernameSelector, ".username", "Check usernameSelector");
+ Assert.strictEqual(override.passwordSelector, "#password", "Check passwordSelector");
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js b/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js
new file mode 100644
index 0000000000..51a107170d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_removeLegacySignonFiles.js
@@ -0,0 +1,69 @@
+/**
+ * Tests the LoginHelper object.
+ */
+
+"use strict";
+
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+
+
+function* createSignonFile(singon) {
+ let {file, pref} = singon;
+
+ if (pref) {
+ Services.prefs.setCharPref(pref, file);
+ }
+
+ yield OS.File.writeAtomic(
+ OS.Path.join(OS.Constants.Path.profileDir, file), new Uint8Array(1));
+}
+
+function* isSignonClear(singon) {
+ const {file, pref} = singon;
+ const fileExists = yield OS.File.exists(
+ OS.Path.join(OS.Constants.Path.profileDir, file));
+
+ if (pref) {
+ try {
+ Services.prefs.getCharPref(pref);
+ return false;
+ } catch (e) {}
+ }
+
+ return !fileExists;
+}
+
+add_task(function* test_remove_lagecy_signonfile() {
+ // In the last test case, signons3.txt being deleted even when
+ // it doesn't exist.
+ const signonsSettings = [[
+ { file: "signons.txt" },
+ { file: "signons2.txt" },
+ { file: "signons3.txt" }
+ ], [
+ { file: "signons.txt", pref: "signon.SignonFileName" },
+ { file: "signons2.txt", pref: "signon.SignonFileName2" },
+ { file: "signons3.txt", pref: "signon.SignonFileName3" }
+ ], [
+ { file: "signons2.txt" },
+ { file: "singons.txt", pref: "signon.SignonFileName" },
+ { file: "customized2.txt", pref: "signon.SignonFileName2" },
+ { file: "customized3.txt", pref: "signon.SignonFileName3" }
+ ]];
+
+ for (let setting of signonsSettings) {
+ for (let singon of setting) {
+ yield createSignonFile(singon);
+ }
+
+ LoginHelper.removeLegacySignonFiles();
+
+ for (let singon of setting) {
+ equal(yield isSignonClear(singon), true);
+ }
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js
new file mode 100644
index 0000000000..3406becff2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js
@@ -0,0 +1,184 @@
+/*
+ * Test Services.logins.searchLogins with the `schemeUpgrades` property.
+ */
+
+const HTTP3_ORIGIN = "http://www3.example.com";
+const HTTPS_ORIGIN = "https://www.example.com";
+const HTTP_ORIGIN = "http://www.example.com";
+
+/**
+ * Returns a list of new nsILoginInfo objects that are a subset of the test
+ * data, built to match the specified query.
+ *
+ * @param {Object} aQuery
+ * Each property and value of this object restricts the search to those
+ * entries from the test data that match the property exactly.
+ */
+function buildExpectedLogins(aQuery) {
+ return TestData.loginList().filter(
+ entry => Object.keys(aQuery).every(name => {
+ if (name == "schemeUpgrades") {
+ return true;
+ }
+ if (["hostname", "formSubmitURL"].includes(name)) {
+ return LoginHelper.isOriginMatching(entry[name], aQuery[name], {
+ schemeUpgrades: aQuery.schemeUpgrades,
+ });
+ }
+ return entry[name] === aQuery[name];
+ }));
+}
+
+/**
+ * Tests the searchLogins function.
+ *
+ * @param {Object} aQuery
+ * Each property and value of this object is translated to an entry in
+ * the nsIPropertyBag parameter of searchLogins.
+ * @param {Number} aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkSearch(aQuery, aExpectedCount) {
+ do_print("Testing searchLogins for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ let outCount = {};
+ let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery));
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+}
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function test_initialize() {
+ for (let login of TestData.loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests searchLogins with the `schemeUpgrades` property
+ */
+add_task(function test_search_schemeUpgrades_hostname() {
+ // Hostname-only
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 2);
+});
+
+/**
+ * Same as above but replacing hostname with formSubmitURL.
+ */
+add_task(function test_search_schemeUpgrades_formSubmitURL() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 4);
+});
+
+
+add_task(function test_search_schemeUpgrades_hostname_formSubmitURL() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ passwordField: "form_field_password",
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ httpRealm: null,
+ passwordField: "form_field_password",
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+});
+
+/**
+ * HTTP submitting to HTTPS
+ */
+add_task(function test_http_to_https() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTP3_ORIGIN,
+ httpRealm: null,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTP3_ORIGIN,
+ httpRealm: null,
+ schemeUpgrades: true,
+ }, 2);
+});
+
+/**
+ * schemeUpgrades shouldn't cause downgrades
+ */
+add_task(function test_search_schemeUpgrades_downgrade() {
+ checkSearch({
+ formSubmitURL: HTTP_ORIGIN,
+ hostname: HTTP_ORIGIN,
+ }, 1);
+ do_print("The same number should be found with schemeUpgrades since we're searching for HTTP");
+ checkSearch({
+ formSubmitURL: HTTP_ORIGIN,
+ hostname: HTTP_ORIGIN,
+ schemeUpgrades: true,
+ }, 1);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_storage.js b/toolkit/components/passwordmgr/test/unit/test_storage.js
new file mode 100644
index 0000000000..d65516d9b0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_storage.js
@@ -0,0 +1,102 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the default nsILoginManagerStorage module attached to the Login
+ * Manager service is able to save and reload nsILoginInfo properties correctly,
+ * even when they include special characters.
+ */
+
+"use strict";
+
+// Globals
+
+function* reloadAndCheckLoginsGen(aExpectedLogins)
+{
+ yield LoginTestUtils.reloadData();
+ LoginTestUtils.checkLogins(aExpectedLogins);
+ LoginTestUtils.clearData();
+}
+
+// Tests
+
+/**
+ * Tests addLogin with valid non-ASCII characters.
+ */
+add_task(function* test_storage_addLogin_nonascii()
+{
+ let hostname = "http://" + String.fromCharCode(355) + ".example.com";
+
+ // Store the strings "user" and "pass" using similarly looking glyphs.
+ let loginInfo = TestData.formLogin({
+ hostname: hostname,
+ formSubmitURL: hostname,
+ username: String.fromCharCode(533, 537, 7570, 345),
+ password: String.fromCharCode(421, 259, 349, 537),
+ usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
+ passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
+ });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+
+ // Store the string "test" using similarly looking glyphs.
+ loginInfo = TestData.authLogin({
+ httpRealm: String.fromCharCode(355, 277, 349, 357),
+ });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+});
+
+/**
+ * Tests addLogin with newline characters in the username and password.
+ */
+add_task(function* test_storage_addLogin_newlines()
+{
+ let loginInfo = TestData.formLogin({
+ username: "user\r\nname",
+ password: "password\r\n",
+ });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+});
+
+/**
+ * Tests addLogin with a single dot in fields where it is allowed.
+ *
+ * These tests exist to verify the legacy "signons.txt" storage format.
+ */
+add_task(function* test_storage_addLogin_dot()
+{
+ let loginInfo = TestData.formLogin({ hostname: ".", passwordField: "." });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+
+ loginInfo = TestData.authLogin({ httpRealm: "." });
+ Services.logins.addLogin(loginInfo);
+ yield* reloadAndCheckLoginsGen([loginInfo]);
+});
+
+/**
+ * Tests addLogin with parentheses in hostnames.
+ *
+ * These tests exist to verify the legacy "signons.txt" storage format.
+ */
+add_task(function* test_storage_addLogin_parentheses()
+{
+ let loginList = [
+ TestData.authLogin({ httpRealm: "(realm" }),
+ TestData.authLogin({ httpRealm: "realm)" }),
+ TestData.authLogin({ httpRealm: "(realm)" }),
+ TestData.authLogin({ httpRealm: ")realm(" }),
+ TestData.authLogin({ hostname: "http://parens(.example.com" }),
+ TestData.authLogin({ hostname: "http://parens).example.com" }),
+ TestData.authLogin({ hostname: "http://parens(example).example.com" }),
+ TestData.authLogin({ hostname: "http://parens)example(.example.com" }),
+ ];
+ for (let loginInfo of loginList) {
+ Services.logins.addLogin(loginInfo);
+ }
+ yield* reloadAndCheckLoginsGen(loginList);
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js
new file mode 100644
index 0000000000..8eab6efe56
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_storage_mozStorage.js
@@ -0,0 +1,507 @@
+/*
+ * This test interfaces directly with the mozStorage password storage module,
+ * bypassing the normal password manager usage.
+ */
+
+
+const ENCTYPE_BASE64 = 0;
+const ENCTYPE_SDR = 1;
+const PERMISSION_SAVE_LOGINS = "login-saving";
+
+// Current schema version used by storage-mozStorage.js. This will need to be
+// kept in sync with the version there (or else the tests fail).
+const CURRENT_SCHEMA = 6;
+
+function* copyFile(aLeafName)
+{
+ yield OS.File.copy(OS.Path.join(do_get_file("data").path, aLeafName),
+ OS.Path.join(OS.Constants.Path.profileDir, aLeafName));
+}
+
+function openDB(aLeafName)
+{
+ var dbFile = new FileUtils.File(OS.Constants.Path.profileDir);
+ dbFile.append(aLeafName);
+
+ return Services.storage.openDatabase(dbFile);
+}
+
+function deleteFile(pathname, filename)
+{
+ var file = new FileUtils.File(pathname);
+ file.append(filename);
+
+ // Suppress failures, this happens in the mozstorage tests on Windows
+ // because the module may still be holding onto the DB. (We don't
+ // have a way to explicitly shutdown/GC the module).
+ try {
+ if (file.exists())
+ file.remove(false);
+ } catch (e) {}
+}
+
+function reloadStorage(aInputPathName, aInputFileName)
+{
+ var inputFile = null;
+ if (aInputFileName) {
+ inputFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ inputFile.initWithPath(aInputPathName);
+ inputFile.append(aInputFileName);
+ }
+
+ let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"]
+ .createInstance(Ci.nsILoginManagerStorage);
+ storage.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIVariant)
+ .initWithFile(inputFile);
+
+ return storage;
+}
+
+function checkStorageData(storage, ref_disabledHosts, ref_logins)
+{
+ LoginTestUtils.assertLoginListsEqual(storage.getAllLogins(), ref_logins);
+ LoginTestUtils.assertDisabledHostsEqual(getAllDisabledHostsFromPermissionManager(),
+ ref_disabledHosts);
+}
+
+function getAllDisabledHostsFromPermissionManager() {
+ let disabledHosts = [];
+ let enumerator = Services.perms.enumerator;
+
+ while (enumerator.hasMoreElements()) {
+ let perm = enumerator.getNext();
+ if (perm.type == PERMISSION_SAVE_LOGINS && perm.capability == Services.perms.DENY_ACTION) {
+ disabledHosts.push(perm.principal.URI.prePath);
+ }
+ }
+
+ return disabledHosts;
+}
+
+function setLoginSavingEnabled(origin, enabled) {
+ let uri = Services.io.newURI(origin, null, null);
+
+ if (enabled) {
+ Services.perms.remove(uri, PERMISSION_SAVE_LOGINS);
+ } else {
+ Services.perms.add(uri, PERMISSION_SAVE_LOGINS, Services.perms.DENY_ACTION);
+ }
+}
+
+add_task(function* test_execute()
+{
+
+const OUTDIR = OS.Constants.Path.profileDir;
+
+try {
+
+var isGUID = /^\{[0-9a-f\d]{8}-[0-9a-f\d]{4}-[0-9a-f\d]{4}-[0-9a-f\d]{4}-[0-9a-f\d]{12}\}$/;
+function getGUIDforID(conn, id) {
+ var stmt = conn.createStatement("SELECT guid from moz_logins WHERE id = " + id);
+ stmt.executeStep();
+ var guid = stmt.getString(0);
+ stmt.finalize();
+ return guid;
+}
+
+function getEncTypeForID(conn, id) {
+ var stmt = conn.createStatement("SELECT encType from moz_logins WHERE id = " + id);
+ stmt.executeStep();
+ var encType = stmt.row.encType;
+ stmt.finalize();
+ return encType;
+}
+
+function getAllDisabledHostsFromMozStorage(conn) {
+ let disabledHosts = [];
+ let stmt = conn.createStatement("SELECT hostname from moz_disabledHosts");
+
+ while (stmt.executeStep()) {
+ disabledHosts.push(stmt.row.hostname);
+ }
+
+ return disabledHosts;
+}
+
+var storage;
+var dbConnection;
+var testnum = 0;
+var testdesc = "Setup of nsLoginInfo test-users";
+var nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Components.interfaces.nsILoginInfo);
+do_check_true(nsLoginInfo != null);
+
+var testuser1 = new nsLoginInfo;
+testuser1.init("http://test.com", "http://test.com", null,
+ "testuser1", "testpass1", "u1", "p1");
+var testuser1B = new nsLoginInfo;
+testuser1B.init("http://test.com", "http://test.com", null,
+ "testuser1B", "testpass1B", "u1", "p1");
+var testuser2 = new nsLoginInfo;
+testuser2.init("http://test.org", "http://test.org", null,
+ "testuser2", "testpass2", "u2", "p2");
+var testuser3 = new nsLoginInfo;
+testuser3.init("http://test.gov", "http://test.gov", null,
+ "testuser3", "testpass3", "u3", "p3");
+var testuser4 = new nsLoginInfo;
+testuser4.init("http://test.gov", "http://test.gov", null,
+ "testuser1", "testpass2", "u4", "p4");
+var testuser5 = new nsLoginInfo;
+testuser5.init("http://test.gov", "http://test.gov", null,
+ "testuser2", "testpass1", "u5", "p5");
+
+
+/* ========== 1 ========== */
+testnum++;
+testdesc = "Test downgrade from v999 storage";
+
+yield* copyFile("signons-v999.sqlite");
+// Verify the schema version in the test file.
+dbConnection = openDB("signons-v999.sqlite");
+do_check_eq(999, dbConnection.schemaVersion);
+dbConnection.close();
+
+storage = reloadStorage(OUTDIR, "signons-v999.sqlite");
+setLoginSavingEnabled("https://disabled.net", false);
+checkStorageData(storage, ["https://disabled.net"], [testuser1]);
+
+// Check to make sure we downgraded the schema version.
+dbConnection = openDB("signons-v999.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v999.sqlite");
+
+/* ========== 2 ========== */
+testnum++;
+testdesc = "Test downgrade from incompat v999 storage";
+// This file has a testuser999/testpass999, but is missing an expected column
+
+var origFile = OS.Path.join(OUTDIR, "signons-v999-2.sqlite");
+var failFile = OS.Path.join(OUTDIR, "signons-v999-2.sqlite.corrupt");
+
+// Make sure we always start clean in a clean state.
+yield* copyFile("signons-v999-2.sqlite");
+yield OS.File.remove(failFile);
+
+Assert.throws(() => reloadStorage(OUTDIR, "signons-v999-2.sqlite"),
+ /Initialization failed/);
+
+// Check to ensure the DB file was renamed to .corrupt.
+do_check_false(yield OS.File.exists(origFile));
+do_check_true(yield OS.File.exists(failFile));
+
+yield OS.File.remove(failFile);
+
+/* ========== 3 ========== */
+testnum++;
+testdesc = "Test upgrade from v1->v2 storage";
+
+yield* copyFile("signons-v1.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v1.sqlite");
+do_check_eq(1, dbConnection.schemaVersion);
+dbConnection.close();
+
+storage = reloadStorage(OUTDIR, "signons-v1.sqlite");
+checkStorageData(storage, ["https://disabled.net"], [testuser1, testuser2]);
+
+// Check to see that we added a GUIDs to the logins.
+dbConnection = openDB("signons-v1.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+var guid = getGUIDforID(dbConnection, 1);
+do_check_true(isGUID.test(guid));
+guid = getGUIDforID(dbConnection, 2);
+do_check_true(isGUID.test(guid));
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v1.sqlite");
+
+/* ========== 4 ========== */
+testnum++;
+testdesc = "Test upgrade v2->v1 storage";
+// This is the case where a v2 DB has been accessed with v1 code, and now we
+// are upgrading it again. Any logins added by the v1 code must be properly
+// upgraded.
+
+yield* copyFile("signons-v1v2.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v1v2.sqlite");
+do_check_eq(1, dbConnection.schemaVersion);
+dbConnection.close();
+
+storage = reloadStorage(OUTDIR, "signons-v1v2.sqlite");
+checkStorageData(storage, ["https://disabled.net"], [testuser1, testuser2, testuser3]);
+
+// While we're here, try modifying a login, to ensure that doing so doesn't
+// change the existing GUID.
+storage.modifyLogin(testuser1, testuser1B);
+checkStorageData(storage, ["https://disabled.net"], [testuser1B, testuser2, testuser3]);
+
+// Check the GUIDs. Logins 1 and 2 should retain their original GUID, login 3
+// should have one created (because it didn't have one previously).
+dbConnection = openDB("signons-v1v2.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+guid = getGUIDforID(dbConnection, 1);
+do_check_eq("{655c7358-f1d6-6446-adab-53f98ac5d80f}", guid);
+guid = getGUIDforID(dbConnection, 2);
+do_check_eq("{13d9bfdc-572a-4d4e-9436-68e9803e84c1}", guid);
+guid = getGUIDforID(dbConnection, 3);
+do_check_true(isGUID.test(guid));
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v1v2.sqlite");
+
+/* ========== 5 ========== */
+testnum++;
+testdesc = "Test upgrade from v2->v3 storage";
+
+yield* copyFile("signons-v2.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v2.sqlite");
+do_check_eq(2, dbConnection.schemaVersion);
+
+storage = reloadStorage(OUTDIR, "signons-v2.sqlite");
+
+// Check to see that we added the correct encType to the logins.
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+var encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+dbConnection.close();
+
+// There are 4 logins, but 3 will be invalid because we can no longer decrypt
+// base64-encoded items. (testuser1/4/5)
+checkStorageData(storage, ["https://disabled.net"],
+ [testuser2]);
+
+deleteFile(OUTDIR, "signons-v2.sqlite");
+
+/* ========== 6 ========== */
+testnum++;
+testdesc = "Test upgrade v3->v2 storage";
+// This is the case where a v3 DB has been accessed with v2 code, and now we
+// are upgrading it again. Any logins added by the v2 code must be properly
+// upgraded.
+
+yield* copyFile("signons-v2v3.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v2v3.sqlite");
+do_check_eq(2, dbConnection.schemaVersion);
+encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, null];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+
+// Reload storage, check that the new login now has encType=1, others untouched
+storage = reloadStorage(OUTDIR, "signons-v2v3.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, ENCTYPE_SDR];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+
+// Sanity check that the data gets migrated
+// There are 5 logins, but 3 will be invalid because we can no longer decrypt
+// base64-encoded items. (testuser1/4/5). We no longer reencrypt with SDR.
+checkStorageData(storage, ["https://disabled.net"], [testuser2, testuser3]);
+encTypes = [ENCTYPE_BASE64, ENCTYPE_SDR, ENCTYPE_BASE64, ENCTYPE_BASE64, ENCTYPE_SDR];
+for (let i = 0; i < encTypes.length; i++)
+ do_check_eq(encTypes[i], getEncTypeForID(dbConnection, i + 1));
+dbConnection.close();
+
+deleteFile(OUTDIR, "signons-v2v3.sqlite");
+
+
+/* ========== 7 ========== */
+testnum++;
+testdesc = "Test upgrade from v3->v4 storage";
+
+yield* copyFile("signons-v3.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v3.sqlite");
+do_check_eq(3, dbConnection.schemaVersion);
+
+storage = reloadStorage(OUTDIR, "signons-v3.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+// Remove old entry from permission manager.
+setLoginSavingEnabled("https://disabled.net", true);
+
+// Check that timestamps and counts were initialized correctly
+checkStorageData(storage, [], [testuser1, testuser2]);
+
+var logins = storage.getAllLogins();
+for (var i = 0; i < 2; i++) {
+ do_check_true(logins[i] instanceof Ci.nsILoginMetaInfo);
+ do_check_eq(1, logins[i].timesUsed);
+ LoginTestUtils.assertTimeIsAboutNow(logins[i].timeCreated);
+ LoginTestUtils.assertTimeIsAboutNow(logins[i].timeLastUsed);
+ LoginTestUtils.assertTimeIsAboutNow(logins[i].timePasswordChanged);
+}
+
+/* ========== 8 ========== */
+testnum++;
+testdesc = "Test upgrade from v3->v4->v3 storage";
+
+yield* copyFile("signons-v3v4.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v3v4.sqlite");
+do_check_eq(3, dbConnection.schemaVersion);
+
+storage = reloadStorage(OUTDIR, "signons-v3v4.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+// testuser1 already has timestamps, testuser2 does not.
+checkStorageData(storage, [], [testuser1, testuser2]);
+
+logins = storage.getAllLogins();
+
+var t1, t2;
+if (logins[0].username == "testuser1") {
+ t1 = logins[0];
+ t2 = logins[1];
+} else {
+ t1 = logins[1];
+ t2 = logins[0];
+}
+
+do_check_true(t1 instanceof Ci.nsILoginMetaInfo);
+do_check_true(t2 instanceof Ci.nsILoginMetaInfo);
+
+do_check_eq(9, t1.timesUsed);
+do_check_eq(1262049951275, t1.timeCreated);
+do_check_eq(1262049951275, t1.timeLastUsed);
+do_check_eq(1262049951275, t1.timePasswordChanged);
+
+do_check_eq(1, t2.timesUsed);
+LoginTestUtils.assertTimeIsAboutNow(t2.timeCreated);
+LoginTestUtils.assertTimeIsAboutNow(t2.timeLastUsed);
+LoginTestUtils.assertTimeIsAboutNow(t2.timePasswordChanged);
+
+
+/* ========== 9 ========== */
+testnum++;
+testdesc = "Test upgrade from v4 storage";
+
+yield* copyFile("signons-v4.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v4.sqlite");
+do_check_eq(4, dbConnection.schemaVersion);
+do_check_false(dbConnection.tableExists("moz_deleted_logins"));
+
+storage = reloadStorage(OUTDIR, "signons-v4.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_deleted_logins"));
+
+
+/* ========== 10 ========== */
+testnum++;
+testdesc = "Test upgrade from v4->v5->v4 storage";
+
+yield copyFile("signons-v4v5.sqlite");
+// Sanity check the test file.
+dbConnection = openDB("signons-v4v5.sqlite");
+do_check_eq(4, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_deleted_logins"));
+
+storage = reloadStorage(OUTDIR, "signons-v4v5.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_deleted_logins"));
+
+/* ========== 11 ========== */
+testnum++;
+testdesc = "Test upgrade from v5->v6 storage";
+
+yield* copyFile("signons-v5v6.sqlite");
+
+// Sanity check the test file.
+dbConnection = openDB("signons-v5v6.sqlite");
+do_check_eq(5, dbConnection.schemaVersion);
+do_check_true(dbConnection.tableExists("moz_disabledHosts"));
+
+// Initial disabled hosts inside signons-v5v6.sqlite
+var disabledHosts = [
+ "http://disabled1.example.com",
+ "http://大.net",
+ "http://xn--19g.com"
+];
+
+LoginTestUtils.assertDisabledHostsEqual(disabledHosts, getAllDisabledHostsFromMozStorage(dbConnection));
+
+// Reload storage
+storage = reloadStorage(OUTDIR, "signons-v5v6.sqlite");
+do_check_eq(CURRENT_SCHEMA, dbConnection.schemaVersion);
+
+// moz_disabledHosts should now be empty after migration.
+LoginTestUtils.assertDisabledHostsEqual([], getAllDisabledHostsFromMozStorage(dbConnection));
+
+// Get all the other hosts currently saved in the permission manager.
+let hostsInPermissionManager = getAllDisabledHostsFromPermissionManager();
+
+// All disabledHosts should have migrated to the permission manager
+LoginTestUtils.assertDisabledHostsEqual(disabledHosts, hostsInPermissionManager);
+
+// Remove all disabled hosts from the permission manager before test ends
+for (let host of disabledHosts) {
+ setLoginSavingEnabled(host, true);
+}
+
+/* ========== 12 ========== */
+testnum++;
+testdesc = "Create nsILoginInfo instances for testing with";
+
+testuser1 = new nsLoginInfo;
+testuser1.init("http://dummyhost.mozilla.org", "", null,
+ "dummydude", "itsasecret", "put_user_here", "put_pw_here");
+
+
+/*
+ * ---------------------- DB Corruption ----------------------
+ * Try to initialize with a corrupt database file. This should create a backup
+ * file, then upon next use create a new database file.
+ */
+
+/* ========== 13 ========== */
+testnum++;
+testdesc = "Corrupt database and backup";
+
+const filename = "signons-c.sqlite";
+const filepath = OS.Path.join(OS.Constants.Path.profileDir, filename);
+
+yield OS.File.copy(do_get_file("data/corruptDB.sqlite").path, filepath);
+
+// will init mozStorage module with corrupt database, init should fail
+Assert.throws(
+ () => reloadStorage(OS.Constants.Path.profileDir, filename),
+ /Initialization failed/);
+
+// check that the backup file exists
+do_check_true(yield OS.File.exists(filepath + ".corrupt"));
+
+// check that the original corrupt file has been deleted
+do_check_false(yield OS.File.exists(filepath));
+
+// initialize the storage module again
+storage = reloadStorage(OS.Constants.Path.profileDir, filename);
+
+// use the storage module again, should work now
+storage.addLogin(testuser1);
+checkStorageData(storage, [], [testuser1]);
+
+// check the file exists
+var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+file.initWithPath(OS.Constants.Path.profileDir);
+file.append(filename);
+do_check_true(file.exists());
+
+deleteFile(OS.Constants.Path.profileDir, filename + ".corrupt");
+deleteFile(OS.Constants.Path.profileDir, filename);
+
+} catch (e) {
+ throw new Error("FAILED in test #" + testnum + " -- " + testdesc + ": " + e);
+}
+
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_telemetry.js b/toolkit/components/passwordmgr/test/unit/test_telemetry.js
new file mode 100644
index 0000000000..1d8f80226d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_telemetry.js
@@ -0,0 +1,187 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the statistics and other counters reported through telemetry.
+ */
+
+"use strict";
+
+// Globals
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+// To prevent intermittent failures when the test is executed at a time that is
+// very close to a day boundary, we make it deterministic by using a static
+// reference date for all the time-based statistics.
+const gReferenceTimeMs = new Date("2000-01-01T00:00:00").getTime();
+
+// Returns a milliseconds value to use with nsILoginMetaInfo properties, falling
+// approximately in the middle of the specified number of days before the
+// reference time, where zero days indicates a time within the past 24 hours.
+var daysBeforeMs = days => gReferenceTimeMs - (days + 0.5) * MS_PER_DAY;
+
+/**
+ * Contains metadata that will be attached to test logins in order to verify
+ * that the statistics collection is working properly. Most properties of the
+ * logins are initialized to the default test values already.
+ *
+ * If you update this data or any of the telemetry histograms it checks, you'll
+ * probably need to update the expected statistics in the test below.
+ */
+const StatisticsTestData = [
+ {
+ timeLastUsed: daysBeforeMs(0),
+ },
+ {
+ timeLastUsed: daysBeforeMs(1),
+ },
+ {
+ timeLastUsed: daysBeforeMs(7),
+ formSubmitURL: null,
+ httpRealm: "The HTTP Realm",
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(7),
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(30),
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(31),
+ },
+ {
+ timeLastUsed: daysBeforeMs(365),
+ },
+ {
+ username: "",
+ timeLastUsed: daysBeforeMs(366),
+ },
+ {
+ // If the login was saved in the future, it is ignored for statistiscs.
+ timeLastUsed: daysBeforeMs(-1),
+ },
+ {
+ timeLastUsed: daysBeforeMs(1000),
+ },
+];
+
+/**
+ * Triggers the collection of those statistics that are not accumulated each
+ * time an action is taken, but are a static snapshot of the current state.
+ */
+function triggerStatisticsCollection() {
+ Services.obs.notifyObservers(null, "gather-telemetry", "" + gReferenceTimeMs);
+}
+
+/**
+ * Tests the telemetry histogram with the given ID contains only the specified
+ * non-zero ranges, expressed in the format { range1: value1, range2: value2 }.
+ */
+function testHistogram(histogramId, expectedNonZeroRanges) {
+ let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+
+ // Compute the actual ranges in the format { range1: value1, range2: value2 }.
+ let actualNonZeroRanges = {};
+ for (let [index, range] of snapshot.ranges.entries()) {
+ let value = snapshot.counts[index];
+ if (value > 0) {
+ actualNonZeroRanges[range] = value;
+ }
+ }
+
+ // These are stringified to visualize the differences between the values.
+ do_print("Testing histogram: " + histogramId);
+ do_check_eq(JSON.stringify(actualNonZeroRanges),
+ JSON.stringify(expectedNonZeroRanges));
+}
+
+// Tests
+
+/**
+ * Enable local telemetry recording for the duration of the tests, and prepare
+ * the test data that will be used by the following tests.
+ */
+add_task(function test_initialize() {
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ do_register_cleanup(function () {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+
+ let uniqueNumber = 1;
+ for (let loginModifications of StatisticsTestData) {
+ loginModifications.hostname = `http://${uniqueNumber++}.example.com`;
+ Services.logins.addLogin(TestData.formLogin(loginModifications));
+ }
+});
+
+/**
+ * Tests the collection of statistics related to login metadata.
+ */
+add_task(function test_logins_statistics() {
+ // Repeat the operation twice to test that histograms are not accumulated.
+ for (let repeating of [false, true]) {
+ triggerStatisticsCollection();
+
+ // Should record 1 in the bucket corresponding to the number of passwords.
+ testHistogram("PWMGR_NUM_SAVED_PASSWORDS",
+ { 10: 1 });
+
+ // Should record 1 in the bucket corresponding to the number of passwords.
+ testHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS",
+ { 1: 1 });
+
+ // For each saved login, should record 1 in the bucket corresponding to the
+ // age in days since the login was last used.
+ testHistogram("PWMGR_LOGIN_LAST_USED_DAYS",
+ { 0: 1, 1: 1, 7: 2, 29: 2, 356: 2, 750: 1 });
+
+ // Should record the number of logins without a username in bucket 0, and
+ // the number of logins with a username in bucket 1.
+ testHistogram("PWMGR_USERNAME_PRESENT",
+ { 0: 4, 1: 6 });
+ }
+});
+
+/**
+ * Tests the collection of statistics related to hosts for which passowrd saving
+ * has been explicitly disabled.
+ */
+add_task(function test_disabledHosts_statistics() {
+ // Should record 1 in the bucket corresponding to the number of sites for
+ // which password saving is disabled.
+ Services.logins.setLoginSavingEnabled("http://www.example.com", false);
+ triggerStatisticsCollection();
+ testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 1: 1 });
+
+ Services.logins.setLoginSavingEnabled("http://www.example.com", true);
+ triggerStatisticsCollection();
+ testHistogram("PWMGR_BLOCKLIST_NUM_SITES", { 0: 1 });
+});
+
+/**
+ * Tests the collection of statistics related to general settings.
+ */
+add_task(function test_settings_statistics() {
+ let oldRememberSignons = Services.prefs.getBoolPref("signon.rememberSignons");
+ do_register_cleanup(function () {
+ Services.prefs.setBoolPref("signon.rememberSignons", oldRememberSignons);
+ });
+
+ // Repeat the operation twice per value to test that histograms are reset.
+ for (let remember of [false, true, false, true]) {
+ // This change should be observed immediately by the login service.
+ Services.prefs.setBoolPref("signon.rememberSignons", remember);
+
+ triggerStatisticsCollection();
+
+ // Should record 1 in either bucket 0 or bucket 1 based on the preference.
+ testHistogram("PWMGR_SAVING_ENABLED", remember ? { 1: 1 } : { 0: 1 });
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js
new file mode 100644
index 0000000000..e1d250a76e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js
@@ -0,0 +1,488 @@
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo, "init");
+
+const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled";
+const PREF_INSECURE_AUTOFILLFORMS_ENABLED = "signon.autofillForms.http";
+
+let matchingLogins = [];
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "", "emptypass1", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "tempuser1", "temppass1", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser2", "testpass2", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "testuser3", "testpass3", "uname", "pword"));
+
+matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+ "zzzuser4", "zzzpass4", "uname", "pword"));
+
+let meta = matchingLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+const LABEL_NO_USERNAME = "No username (" + time + ")";
+
+let expectedResults = [
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: true,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: true,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: "This connection is not secure. Logins entered here could be compromised. Learn More",
+ style: "insecureWarning"
+ }, {
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "tempuser1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testuser2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testuser3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzuser4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: false,
+ matchingLogins: matchingLogins,
+ items: []
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: true,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: [{
+ value: "emptypass1",
+ label: LABEL_NO_USERNAME,
+ style: "login",
+ }, {
+ value: "temppass1",
+ label: "tempuser1",
+ style: "login",
+ }, {
+ value: "testpass2",
+ label: "testuser2",
+ style: "login",
+ }, {
+ value: "testpass3",
+ label: "testuser3",
+ style: "login",
+ }, {
+ value: "zzzpass4",
+ label: "zzzuser4",
+ style: "login",
+ }]
+ },
+ {
+ insecureFieldWarningEnabled: false,
+ insecureAutoFillFormsEnabled: false,
+ isSecure: false,
+ isPasswordField: true,
+ matchingLogins: matchingLogins,
+ items: []
+ },
+];
+
+add_task(function* test_all_patterns() {
+ LoginHelper.createLogger("UserAutoCompleteResult");
+ expectedResults.forEach(pattern => {
+ Services.prefs.setBoolPref(PREF_INSECURE_FIELD_WARNING_ENABLED,
+ pattern.insecureFieldWarningEnabled);
+ Services.prefs.setBoolPref(PREF_INSECURE_AUTOFILLFORMS_ENABLED,
+ pattern.insecureAutoFillFormsEnabled);
+ let actual = new UserAutoCompleteResult("", pattern.matchingLogins,
+ {
+ isSecure: pattern.isSecure,
+ isPasswordField: pattern.isPasswordField
+ });
+ pattern.items.forEach((item, index) => {
+ equal(actual.getValueAt(index), item.value);
+ equal(actual.getLabelAt(index), item.label);
+ equal(actual.getStyleAt(index), item.style);
+ });
+
+ if (pattern.items.length != 0) {
+ Assert.throws(() => actual.getValueAt(pattern.items.length),
+ /Index out of range\./);
+
+ Assert.throws(() => actual.getLabelAt(pattern.items.length),
+ /Index out of range\./);
+
+ Assert.throws(() => actual.removeValueAt(pattern.items.length, true),
+ /Index out of range\./);
+ }
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/unit/xpcshell.ini b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..8f8c92a28a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+head = head.js
+tail =
+support-files = data/**
+
+# Test JSON file access and import from SQLite, not applicable to Android.
+[test_module_LoginImport.js]
+skip-if = os == "android"
+[test_module_LoginStore.js]
+skip-if = os == "android"
+[test_removeLegacySignonFiles.js]
+skip-if = os == "android"
+
+# Test SQLite database backup and migration, applicable to Android only.
+[test_storage_mozStorage.js]
+skip-if = true || os != "android" # Bug 1171687: Needs fixing on Android
+
+# The following tests apply to any storage back-end.
+[test_context_menu.js]
+skip-if = os == "android" # The context menu isn't used on Android.
+# LoginManagerContextMenu is only included for MOZ_BUILD_APP == 'browser'.
+run-if = buildapp == "browser"
+[test_dedupeLogins.js]
+[test_disabled_hosts.js]
+[test_getFormFields.js]
+[test_getPasswordFields.js]
+[test_getPasswordOrigin.js]
+[test_isOriginMatching.js]
+[test_legacy_empty_formSubmitURL.js]
+[test_legacy_validation.js]
+[test_logins_change.js]
+[test_logins_decrypt_failure.js]
+skip-if = os == "android" # Bug 1171687: Needs fixing on Android
+[test_user_autocomplete_result.js]
+skip-if = os == "android"
+[test_logins_metainfo.js]
+[test_logins_search.js]
+[test_maybeImportLogin.js]
+[test_notifications.js]
+[test_OSCrypto_win.js]
+skip-if = os != "win"
+[test_recipes_add.js]
+[test_recipes_content.js]
+[test_search_schemeUpgrades.js]
+[test_storage.js]
+[test_telemetry.js]
diff --git a/toolkit/components/perf/.eslintrc.js b/toolkit/components/perf/.eslintrc.js
new file mode 100644
index 0000000000..4e6d4bcf08
--- /dev/null
+++ b/toolkit/components/perf/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/perf/PerfMeasurement.cpp b/toolkit/components/perf/PerfMeasurement.cpp
new file mode 100644
index 0000000000..1b211b79c7
--- /dev/null
+++ b/toolkit/components/perf/PerfMeasurement.cpp
@@ -0,0 +1,120 @@
+/* -*- 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 "PerfMeasurement.h"
+#include "jsperf.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsMemory.h"
+#include "mozilla/Preferences.h"
+#include "mozJSComponentLoader.h"
+#include "nsZipArchive.h"
+#include "xpc_make_class.h"
+
+#define JSPERF_CONTRACTID \
+ "@mozilla.org/jsperf;1"
+
+#define JSPERF_CID \
+{ 0x421c38e6, 0xaee0, 0x4509, \
+ { 0xa0, 0x25, 0x13, 0x0f, 0x43, 0x78, 0x03, 0x5a } }
+
+namespace mozilla {
+namespace jsperf {
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(Module)
+
+NS_IMPL_ISUPPORTS(Module, nsIXPCScriptable)
+
+Module::Module()
+{
+}
+
+Module::~Module()
+{
+}
+
+#define XPC_MAP_CLASSNAME Module
+#define XPC_MAP_QUOTED_CLASSNAME "Module"
+#define XPC_MAP_WANT_CALL
+#define XPC_MAP_FLAGS nsIXPCScriptable::WANT_CALL
+#include "xpc_map_end.h"
+
+static bool
+SealObjectAndPrototype(JSContext* cx, JS::Handle<JSObject *> parent, const char* name)
+{
+ JS::Rooted<JS::Value> prop(cx);
+ if (!JS_GetProperty(cx, parent, name, &prop))
+ return false;
+
+ if (prop.isUndefined()) {
+ // Pretend we sealed the object.
+ return true;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, prop.toObjectOrNull());
+ if (!JS_GetProperty(cx, obj, "prototype", &prop))
+ return false;
+
+ JS::Rooted<JSObject*> prototype(cx, prop.toObjectOrNull());
+ return JS_FreezeObject(cx, obj) && JS_FreezeObject(cx, prototype);
+}
+
+static bool
+InitAndSealPerfMeasurementClass(JSContext* cx, JS::Handle<JSObject*> global)
+{
+ // Init the PerfMeasurement class
+ if (!JS::RegisterPerfMeasurement(cx, global))
+ return false;
+
+ // Seal up Object, Function, and Array and their prototypes. (This single
+ // object instance is shared amongst everyone who imports the jsperf module.)
+ if (!SealObjectAndPrototype(cx, global, "Object") ||
+ !SealObjectAndPrototype(cx, global, "Function") ||
+ !SealObjectAndPrototype(cx, global, "Array"))
+ return false;
+
+ // Finally, seal the global object, for good measure. (But not recursively;
+ // this breaks things.)
+ return JS_FreezeObject(cx, global);
+}
+
+NS_IMETHODIMP
+Module::Call(nsIXPConnectWrappedNative* wrapper,
+ JSContext* cx,
+ JSObject* obj,
+ const JS::CallArgs& args,
+ bool* _retval)
+{
+
+ mozJSComponentLoader* loader = mozJSComponentLoader::Get();
+ JS::Rooted<JSObject*> targetObj(cx);
+ nsresult rv = loader->FindTargetObject(cx, &targetObj);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_retval = InitAndSealPerfMeasurementClass(cx, targetObj);
+ return NS_OK;
+}
+
+} // namespace jsperf
+} // namespace mozilla
+
+NS_DEFINE_NAMED_CID(JSPERF_CID);
+
+static const mozilla::Module::CIDEntry kPerfCIDs[] = {
+ { &kJSPERF_CID, false, nullptr, mozilla::jsperf::ModuleConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kPerfContracts[] = {
+ { JSPERF_CONTRACTID, &kJSPERF_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kPerfModule = {
+ mozilla::Module::kVersion,
+ kPerfCIDs,
+ kPerfContracts
+};
+
+NSMODULE_DEFN(jsperf) = &kPerfModule;
diff --git a/toolkit/components/perf/PerfMeasurement.h b/toolkit/components/perf/PerfMeasurement.h
new file mode 100644
index 0000000000..b158d16852
--- /dev/null
+++ b/toolkit/components/perf/PerfMeasurement.h
@@ -0,0 +1,30 @@
+/* -*- 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 COMPONENTS_PERFMEASUREMENT_H
+#define COMPONENTS_PERFMEASUREMENT_H
+
+#include "nsIXPCScriptable.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+namespace jsperf {
+
+class Module final : public nsIXPCScriptable
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIXPCSCRIPTABLE
+
+ Module();
+
+private:
+ ~Module();
+};
+
+} // namespace jsperf
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/perf/PerfMeasurement.jsm b/toolkit/components/perf/PerfMeasurement.jsm
new file mode 100644
index 0000000000..29a221c6f4
--- /dev/null
+++ b/toolkit/components/perf/PerfMeasurement.jsm
@@ -0,0 +1,19 @@
+/* -*- 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/. */
+
+this.EXPORTED_SYMBOLS = [ "PerfMeasurement" ];
+
+/*
+ * This is the js module for jsperf. Import it like so:
+ * Components.utils.import("resource://gre/modules/PerfMeasurement.jsm");
+ *
+ * This will create a 'PerfMeasurement' class. Instances of this class can
+ * be used to benchmark browser operations.
+ *
+ * For documentation on the API, see js/src/perf/jsperf.h.
+ *
+ */
+
+Components.classes["@mozilla.org/jsperf;1"].createInstance()();
diff --git a/toolkit/components/perf/chrome.ini b/toolkit/components/perf/chrome.ini
new file mode 100644
index 0000000000..eaa3c2401d
--- /dev/null
+++ b/toolkit/components/perf/chrome.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_pm.xul]
diff --git a/toolkit/components/perf/moz.build b/toolkit/components/perf/moz.build
new file mode 100644
index 0000000000..d153244c51
--- /dev/null
+++ b/toolkit/components/perf/moz.build
@@ -0,0 +1,21 @@
+# -*- 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 += [
+ 'PerfMeasurement.cpp',
+]
+
+EXTRA_JS_MODULES += [
+ 'PerfMeasurement.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+LOCAL_INCLUDES += [
+ '/js/xpconnect/loader',
+]
+
+MOCHITEST_CHROME_MANIFESTS += ['chrome.ini']
diff --git a/toolkit/components/perf/test_pm.xul b/toolkit/components/perf/test_pm.xul
new file mode 100644
index 0000000000..7dbf27b924
--- /dev/null
+++ b/toolkit/components/perf/test_pm.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/. -->
+
+<window title="Performance measurement tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test()">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"><![CDATA[
+function test()
+{
+ SimpleTest.waitForExplicitFinish();
+
+ Components.utils.import("resource://gre/modules/PerfMeasurement.jsm");
+ let pm = new PerfMeasurement(PerfMeasurement.ALL);
+ if (pm.eventsMeasured == 0) {
+ todo(false, "stub, skipping test");
+ } else {
+ pm.start();
+ for (let i = 0; i < 10000; i++) ;
+ pm.stop();
+
+ events = ["cpu_cycles", "instructions", "cache_references", "cache_misses",
+ "branch_instructions", "branch_misses", "bus_cycles", "page_faults",
+ "major_page_faults", "context_switches", "cpu_migrations"];
+
+ for (var i = 0; i < events.length; i++) {
+ var e = events[i];
+ ((pm.eventsMeasured & PerfMeasurement[e.toUpperCase()]) ? isnot : todo_is)(pm[e], -1, e);
+ }
+ }
+ SimpleTest.finish();
+}
+]]></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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/perfmonitoring/AddonWatcher.jsm b/toolkit/components/perfmonitoring/AddonWatcher.jsm
new file mode 100644
index 0000000000..58decba857
--- /dev/null
+++ b/toolkit/components/perfmonitoring/AddonWatcher.jsm
@@ -0,0 +1,239 @@
+// -*- 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["AddonWatcher"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PerformanceWatcher",
+ "resource://gre/modules/PerformanceWatcher.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
+ "@mozilla.org/base/telemetry;1",
+ Ci.nsITelemetry);
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "IdleService",
+ "@mozilla.org/widget/idleservice;1",
+ Ci.nsIIdleService);
+
+/**
+ * Don't notify observers of slow add-ons if at least `SUSPICIOUSLY_MANY_ADDONS`
+ * show up at the same time. We assume that this indicates that the system itself
+ * is busy, and that add-ons are not responsible.
+ */
+let SUSPICIOUSLY_MANY_ADDONS = 5;
+
+this.AddonWatcher = {
+ /**
+ * Watch this topic to be informed when a slow add-on is detected and should
+ * be reported to the user.
+ *
+ * If you need finer-grained control, use PerformanceWatcher.jsm.
+ */
+ TOPIC_SLOW_ADDON_DETECTED: "addon-watcher-detected-slow-addon",
+
+ init: function() {
+ this._initializedTimeStamp = Cu.now();
+
+ try {
+ this._ignoreList = new Set(JSON.parse(Preferences.get("browser.addon-watch.ignore", null)));
+ } catch (ex) {
+ // probably some malformed JSON, ignore and carry on
+ this._ignoreList = new Set();
+ }
+
+ this._warmupPeriod = Preferences.get("browser.addon-watch.warmup-ms", 60 * 1000 /* 1 minute */);
+ this._idleThreshold = Preferences.get("browser.addon-watch.deactivate-after-idle-ms", 3000);
+ this.paused = false;
+ },
+ uninit: function() {
+ this.paused = true;
+ },
+ _initializedTimeStamp: 0,
+
+ set paused(paused) {
+ if (paused) {
+ if (this._listener) {
+ PerformanceWatcher.removePerformanceListener({addonId: "*"}, this._listener);
+ }
+ this._listener = null;
+ } else {
+ this._listener = this._onSlowAddons.bind(this);
+ PerformanceWatcher.addPerformanceListener({addonId: "*"}, this._listener);
+ }
+ },
+ get paused() {
+ return !this._listener;
+ },
+ _listener: null,
+
+ /**
+ * Provide the following object for each addon:
+ * {number} occurrences The total number of performance alerts recorded for this addon.
+ * {number} occurrencesSinceLastNotification The number of performances alerts recorded
+ * since we last notified the user.
+ * {number} latestNotificationTimeStamp The timestamp of the latest user notification
+ * that this add-on is slow.
+ */
+ _getAlerts: function(addonId) {
+ let alerts = this._alerts.get(addonId);
+ if (!alerts) {
+ alerts = {
+ occurrences: 0,
+ occurrencesSinceLastNotification: 0,
+ latestNotificationTimeStamp: 0,
+ };
+ this._alerts.set(addonId, alerts);
+ }
+ return alerts;
+ },
+ _alerts: new Map(),
+ _onSlowAddons: function(addons) {
+ try {
+ if (IdleService.idleTime >= this._idleThreshold) {
+ // The application is idle. Maybe the computer is sleeping, or maybe
+ // the user isn't in front of it. Regardless, the user doesn't care
+ // about things that slow down her browser while she's not using it.
+ return;
+ }
+
+ if (addons.length > SUSPICIOUSLY_MANY_ADDONS) {
+ // Heuristic: if we are notified of many slow addons at once, the issue
+ // is probably not with the add-ons themselves with the system. We may
+ // for instance be waking up from hibernation, or the system may be
+ // busy swapping.
+ return;
+ }
+
+ let now = Cu.now();
+ if (now - this._initializedTimeStamp < this._warmupPeriod) {
+ // Heuristic: do not report slowdowns during or just after startup.
+ return;
+ }
+
+ // Report immediately to Telemetry, regardless of whether we report to
+ // the user.
+ for (let {source: {addonId}, details} of addons) {
+ Telemetry.getKeyedHistogramById("PERF_MONITORING_SLOW_ADDON_JANK_US").
+ add(addonId, details.highestJank);
+ Telemetry.getKeyedHistogramById("PERF_MONITORING_SLOW_ADDON_CPOW_US").
+ add(addonId, details.highestCPOW);
+ }
+
+ // We expect that users don't care about real-time alerts unless their
+ // browser is going very, very slowly. Therefore, we use the following
+ // heuristic:
+ // - if jank is above freezeThreshold (e.g. 5 seconds), report immediately; otherwise
+ // - if jank is below jankThreshold (e.g. 128ms), disregard; otherwise
+ // - if the latest jank was more than prescriptionDelay (e.g. 5 minutes) ago, reset number of occurrences;
+ // - if we have had fewer than occurrencesBetweenAlerts janks (e.g. 3) since last alert, disregard; otherwise
+ // - if we have displayed an alert for this add-on less than delayBetweenAlerts ago (e.g. 6h), disregard; otherwise
+ // - also, don't report more than highestNumberOfAddonsToReport (e.g. 1) at once.
+ let freezeThreshold = Preferences.get("browser.addon-watch.freeze-threshold-micros", /* 5 seconds */ 5000000);
+ let jankThreshold = Preferences.get("browser.addon-watch.jank-threshold-micros", /* 256 ms == 8 frames*/ 256000);
+ let occurrencesBetweenAlerts = Preferences.get("browser.addon-watch.occurrences-between-alerts", 3);
+ let delayBetweenAlerts = Preferences.get("browser.addon-watch.delay-between-alerts-ms", 6 * 3600 * 1000 /* 6h */);
+ let delayBetweenFreezeAlerts = Preferences.get("browser.addon-watch.delay-between-freeze-alerts-ms", 2 * 60 * 1000 /* 2 min */);
+ let prescriptionDelay = Preferences.get("browser.addon-watch.prescription-delay", 5 * 60 * 1000 /* 5 minutes */);
+ let highestNumberOfAddonsToReport = Preferences.get("browser.addon-watch.max-simultaneous-reports", 1);
+
+ addons = addons.filter(x => x.details.highestJank >= jankThreshold).
+ sort((a, b) => a.details.highestJank - b.details.highestJank);
+
+ for (let {source: {addonId}, details} of addons) {
+ if (highestNumberOfAddonsToReport <= 0) {
+ return;
+ }
+ if (this._ignoreList.has(addonId)) {
+ // Add-on is ignored.
+ continue;
+ }
+
+ let alerts = this._getAlerts(addonId);
+ if (now - alerts.latestOccurrence >= prescriptionDelay) {
+ // While this add-on has already caused slownesss, this
+ // was a long time ago, let's forgive.
+ alerts.occurrencesSinceLastNotification = 0;
+ }
+
+ alerts.occurrencesSinceLastNotification++;
+ alerts.occurrences++;
+
+ if (details.highestJank < freezeThreshold) {
+ if (alerts.occurrencesSinceLastNotification <= occurrencesBetweenAlerts) {
+ // While the add-on has caused jank at least once, we are only
+ // interested in repeat offenders. Store the data for future use.
+ continue;
+ }
+ if (now - alerts.latestNotificationTimeStamp <= delayBetweenAlerts) {
+ // We have already displayed an alert for this add-on recently.
+ // Wait a little before displaying another one.
+ continue;
+ }
+ } else if (now - alerts.latestNotificationTimeStamp <= delayBetweenFreezeAlerts) {
+ // Even in case of freeze, we want to avoid needlessly spamming the user.
+ // We have already displayed an alert for this add-on recently.
+ // Wait a little before displaying another one.
+ continue;
+ }
+
+ // Ok, time to inform the user.
+ alerts.latestNotificationTimeStamp = now;
+ alerts.occurrencesSinceLastNotification = 0;
+ Services.obs.notifyObservers(null, this.TOPIC_SLOW_ADDON_DETECTED, addonId);
+
+ highestNumberOfAddonsToReport--;
+ }
+ } catch (ex) {
+ Cu.reportError("Error in AddonWatcher._onSlowAddons " + ex);
+ Cu.reportError(Task.Debugging.generateReadableStack(ex.stack));
+ }
+ },
+
+ ignoreAddonForSession: function(addonid) {
+ this._ignoreList.add(addonid);
+ },
+ ignoreAddonPermanently: function(addonid) {
+ this._ignoreList.add(addonid);
+ try {
+ let ignoreList = JSON.parse(Preferences.get("browser.addon-watch.ignore", "[]"))
+ if (!ignoreList.includes(addonid)) {
+ ignoreList.push(addonid);
+ Preferences.set("browser.addon-watch.ignore", JSON.stringify(ignoreList));
+ }
+ } catch (ex) {
+ Preferences.set("browser.addon-watch.ignore", JSON.stringify([addonid]));
+ }
+ },
+
+ /**
+ * The list of alerts for this session.
+ *
+ * @type {Map<String, Object>} A map associating addonId to
+ * objects with fields
+ * {number} occurrences The total number of performance alerts recorded for this addon.
+ * {number} occurrencesSinceLastNotification The number of performances alerts recorded
+ * since we last notified the user.
+ * {number} latestNotificationTimeStamp The timestamp of the latest user notification
+ * that this add-on is slow.
+ */
+ get alerts() {
+ let result = new Map();
+ for (let [k, v] of this._alerts) {
+ result.set(k, Cu.cloneInto(v, this));
+ }
+ return result;
+ },
+};
diff --git a/toolkit/components/perfmonitoring/PerformanceStats-content.js b/toolkit/components/perfmonitoring/PerformanceStats-content.js
new file mode 100644
index 0000000000..9a6a2d81dd
--- /dev/null
+++ b/toolkit/components/perfmonitoring/PerformanceStats-content.js
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A proxy implementing communication between the PerformanceStats.jsm modules
+ * of the parent and children processes.
+ *
+ * This script is loaded in all processes but is essentially a NOOP in the
+ * parent process.
+ */
+
+"use strict";
+
+var { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "PerformanceStats",
+ "resource://gre/modules/PerformanceStats.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * A global performance monitor used by this process.
+ *
+ * For the sake of simplicity, rather than attempting to map each PerformanceMonitor
+ * of the parent to a PerformanceMonitor in each child process, we maintain a single
+ * PerformanceMonitor in each child process. Probes activation/deactivation for this
+ * monitor is controlled by the activation/deactivation of probes in the parent.
+ *
+ * In the parent, this is always an empty monitor.
+ */
+var gMonitor = PerformanceStats.getMonitor([]);
+
+/**
+ * `true` if this is a content process, `false` otherwise.
+ */
+var isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+/**
+ * Handle message `performance-stats-service-acquire`: ensure that the global
+ * monitor has a given probe. This message must be sent by the parent process
+ * whenever a probe is activated application-wide.
+ *
+ * Note that we may miss acquire messages if they are sent before this process is
+ * launched. For this reason, `performance-stats-service-collect` automatically
+ * re-acquires probes if it realizes that they are missing.
+ *
+ * This operation is a NOOP on the parent process.
+ *
+ * @param {{payload: Array<string>}} msg.data The message received. `payload`
+ * must be an array of probe names.
+ */
+Services.cpmm.addMessageListener("performance-stats-service-acquire", function(msg) {
+ if (!isContent) {
+ return;
+ }
+ let name = msg.data.payload;
+ ensureAcquired(name);
+});
+
+/**
+ * Handle message `performance-stats-service-release`: release a given probe
+ * from the global monitor. This message must be sent by the parent process
+ * whenever a probe is deactivated application-wide.
+ *
+ * Note that we may miss release messages if they are sent before this process is
+ * launched. This is ok, as probes are inactive by default: if we miss the release
+ * message, we have already missed the acquire message, and the effect of both
+ * messages together is to reset to the default state.
+ *
+ * This operation is a NOOP on the parent process.
+ *
+ * @param {{payload: Array<string>}} msg.data The message received. `payload`
+ * must be an array of probe names.
+ */
+Services.cpmm.addMessageListener("performance-stats-service-release", function(msg) {
+ if (!isContent) {
+ return;
+ }
+
+ // Keep only the probes that do not appear in the payload
+ let probes = gMonitor.probeNames
+ .filter(x => msg.data.payload.indexOf(x) == -1);
+ gMonitor = PerformanceStats.getMonitor(probes);
+});
+
+/**
+ * Ensure that this process has all the probes it needs.
+ *
+ * @param {Array<string>} probeNames The name of all probes needed by the
+ * process.
+ */
+function ensureAcquired(probeNames) {
+ let alreadyAcquired = gMonitor.probeNames;
+
+ // Algorithm is O(n^2) because we expect that n ≤ 3.
+ let shouldAcquire = [];
+ for (let probeName of probeNames) {
+ if (alreadyAcquired.indexOf(probeName) == -1) {
+ shouldAcquire.push(probeName)
+ }
+ }
+
+ if (shouldAcquire.length == 0) {
+ return;
+ }
+ gMonitor = PerformanceStats.getMonitor([...alreadyAcquired, ...shouldAcquire]);
+}
+
+/**
+ * Handle message `performance-stats-service-collected`: collect the data
+ * obtained by the monitor. This message must be sent by the parent process
+ * whenever we grab a performance snapshot of the application.
+ *
+ * This operation provides `null` on the parent process.
+ *
+ * @param {{data: {payload: Array<string>}}} msg The message received. `payload`
+ * must be an array of probe names.
+ */
+Services.cpmm.addMessageListener("performance-stats-service-collect", Task.async(function*(msg) {
+ let {id, payload: {probeNames}} = msg.data;
+ if (!isContent) {
+ // This message was sent by the parent process to itself.
+ // As per protocol, respond `null`.
+ Services.cpmm.sendAsyncMessage("performance-stats-service-collect", {
+ id,
+ data: null
+ });
+ return;
+ }
+
+ // We may have missed acquire messages if the process was loaded too late.
+ // Catch up now.
+ ensureAcquired(probeNames);
+
+ // Collect and return data.
+ let data = yield gMonitor.promiseSnapshot({probeNames});
+ Services.cpmm.sendAsyncMessage("performance-stats-service-collect", {
+ id,
+ data
+ });
+}));
diff --git a/toolkit/components/perfmonitoring/PerformanceStats.jsm b/toolkit/components/perfmonitoring/PerformanceStats.jsm
new file mode 100644
index 0000000000..20f27a51b4
--- /dev/null
+++ b/toolkit/components/perfmonitoring/PerformanceStats.jsm
@@ -0,0 +1,1000 @@
+// -*- 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PerformanceStats"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+/**
+ * API for querying and examining performance data.
+ *
+ * This API exposes data from several probes implemented by the JavaScript VM.
+ * See `PerformanceStats.getMonitor()` for information on how to monitor data
+ * from one or more probes and `PerformanceData` for the information obtained
+ * from the probes.
+ *
+ * Data is collected by "Performance Group". Typically, a Performance Group
+ * is an add-on, or a frame, or the internals of the application.
+ *
+ * Generally, if you have the choice between PerformanceStats and PerformanceWatcher,
+ * you should favor PerformanceWatcher.
+ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/ObjectUtils.jsm", this);
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+ "resource://gre/modules/Timer.jsm");
+
+// The nsIPerformanceStatsService provides lower-level
+// access to SpiderMonkey and the probes.
+XPCOMUtils.defineLazyServiceGetter(this, "performanceStatsService",
+ "@mozilla.org/toolkit/performance-stats-service;1",
+ Ci.nsIPerformanceStatsService);
+
+// The finalizer lets us automatically release (and when possible deactivate)
+// probes when a monitor is garbage-collected.
+XPCOMUtils.defineLazyServiceGetter(this, "finalizer",
+ "@mozilla.org/toolkit/finalizationwitness;1",
+ Ci.nsIFinalizationWitnessService
+);
+
+// The topic used to notify that a PerformanceMonitor has been garbage-collected
+// and that we can release/close the probes it holds.
+const FINALIZATION_TOPIC = "performancemonitor-finalize";
+
+const PROPERTIES_META_IMMUTABLE = ["addonId", "isSystem", "isChildProcess", "groupId", "processId"];
+const PROPERTIES_META = [...PROPERTIES_META_IMMUTABLE, "windowId", "title", "name"];
+
+// How long we wait for children processes to respond.
+const MAX_WAIT_FOR_CHILD_PROCESS_MS = 5000;
+
+var isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+/**
+ * Access to a low-level performance probe.
+ *
+ * Each probe is dedicated to some form of performance monitoring.
+ * As each probe may have a performance impact, a probe is activated
+ * only when a client has requested a PerformanceMonitor for this probe,
+ * and deactivated once all clients are disposed of.
+ */
+function Probe(name, impl) {
+ this._name = name;
+ this._counter = 0;
+ this._impl = impl;
+}
+Probe.prototype = {
+ /**
+ * Acquire the probe on behalf of a client.
+ *
+ * If the probe was inactive, activate it. Note that activating a probe
+ * can incur a memory or performance cost.
+ */
+ acquire: function() {
+ if (this._counter == 0) {
+ this._impl.isActive = true;
+ Process.broadcast("acquire", [this._name]);
+ }
+ this._counter++;
+ },
+
+ /**
+ * Release the probe on behalf of a client.
+ *
+ * If this was the last client for this probe, deactivate it.
+ */
+ release: function() {
+ this._counter--;
+ if (this._counter == 0) {
+ try {
+ this._impl.isActive = false;
+ } catch (ex) {
+ if (ex && typeof ex == "object" && ex.result == Components.results.NS_ERROR_NOT_AVAILABLE) {
+ // The service has already been shutdown. Ignore further shutdown requests.
+ return;
+ }
+ throw ex;
+ }
+ Process.broadcast("release", [this._name]);
+ }
+ },
+
+ /**
+ * Obtain data from this probe, once it is available.
+ *
+ * @param {nsIPerformanceStats} xpcom A xpcom object obtained from
+ * SpiderMonkey. Only the fields updated by the low-level probe
+ * are in a specified state.
+ * @return {object} An object containing the data extracted from this
+ * probe. Actual format depends on the probe.
+ */
+ extract: function(xpcom) {
+ if (!this._impl.isActive) {
+ throw new Error(`Probe is inactive: ${this._name}`);
+ }
+ return this._impl.extract(xpcom);
+ },
+
+ /**
+ * @param {object} a An object returned by `this.extract()`.
+ * @param {object} b An object returned by `this.extract()`.
+ *
+ * @return {true} If `a` and `b` hold identical values.
+ */
+ isEqual: function(a, b) {
+ if (a == null && b == null) {
+ return true;
+ }
+ if (a != null && b != null) {
+ return this._impl.isEqual(a, b);
+ }
+ return false;
+ },
+
+ /**
+ * @param {object} a An object returned by `this.extract()`. May
+ * NOT be `null`.
+ * @param {object} b An object returned by `this.extract()`. May
+ * be `null`.
+ *
+ * @return {object} An object representing `a - b`. If `b` is
+ * `null`, this is `a`.
+ */
+ subtract: function(a, b) {
+ if (a == null) {
+ throw new TypeError();
+ }
+ if (b == null) {
+ return a;
+ }
+ return this._impl.subtract(a, b);
+ },
+
+ importChildCompartments: function(parent, children) {
+ if (!Array.isArray(children)) {
+ throw new TypeError();
+ }
+ if (!parent || !(parent instanceof PerformanceDataLeaf)) {
+ throw new TypeError();
+ }
+ return this._impl.importChildCompartments(parent, children);
+ },
+
+ /**
+ * The name of the probe.
+ */
+ get name() {
+ return this._name;
+ },
+
+ compose: function(stats) {
+ if (!Array.isArray(stats)) {
+ throw new TypeError();
+ }
+ return this._impl.compose(stats);
+ }
+};
+
+// Utility function. Return the position of the last non-0 item in an
+// array, or -1 if there isn't any such item.
+function lastNonZero(array) {
+ for (let i = array.length - 1; i >= 0; --i) {
+ if (array[i] != 0) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * The actual Probes implemented by SpiderMonkey.
+ */
+var Probes = {
+ /**
+ * A probe measuring jank.
+ *
+ * Data provided by this probe uses the following format:
+ *
+ * @field {number} totalCPUTime The total amount of time spent using the
+ * CPU for this performance group, in µs.
+ * @field {number} totalSystemTime The total amount of time spent in the
+ * kernel for this performance group, in µs.
+ * @field {Array<number>} durations An array containing at each position `i`
+ * the number of times execution of this component has lasted at least `2^i`
+ * milliseconds.
+ * @field {number} longestDuration The index of the highest non-0 value in
+ * `durations`.
+ */
+ jank: new Probe("jank", {
+ set isActive(x) {
+ performanceStatsService.isMonitoringJank = x;
+ },
+ get isActive() {
+ return performanceStatsService.isMonitoringJank;
+ },
+ extract: function(xpcom) {
+ let durations = xpcom.getDurations();
+ return {
+ totalUserTime: xpcom.totalUserTime,
+ totalSystemTime: xpcom.totalSystemTime,
+ totalCPUTime: xpcom.totalUserTime + xpcom.totalSystemTime,
+ durations: durations,
+ longestDuration: lastNonZero(durations)
+ }
+ },
+ isEqual: function(a, b) {
+ // invariant: `a` and `b` are both non-null
+ if (a.totalUserTime != b.totalUserTime) {
+ return false;
+ }
+ if (a.totalSystemTime != b.totalSystemTime) {
+ return false;
+ }
+ for (let i = 0; i < a.durations.length; ++i) {
+ if (a.durations[i] != b.durations[i]) {
+ return false;
+ }
+ }
+ return true;
+ },
+ subtract: function(a, b) {
+ // invariant: `a` and `b` are both non-null
+ let result = {
+ totalUserTime: a.totalUserTime - b.totalUserTime,
+ totalSystemTime: a.totalSystemTime - b.totalSystemTime,
+ totalCPUTime: a.totalCPUTime - b.totalCPUTime,
+ durations: [],
+ longestDuration: -1,
+ };
+ for (let i = 0; i < a.durations.length; ++i) {
+ result.durations[i] = a.durations[i] - b.durations[i];
+ }
+ result.longestDuration = lastNonZero(result.durations);
+ return result;
+ },
+ importChildCompartments: function() { /* nothing to do */ },
+ compose: function(stats) {
+ let result = {
+ totalUserTime: 0,
+ totalSystemTime: 0,
+ totalCPUTime: 0,
+ durations: [],
+ longestDuration: -1
+ };
+ for (let stat of stats) {
+ result.totalUserTime += stat.totalUserTime;
+ result.totalSystemTime += stat.totalSystemTime;
+ result.totalCPUTime += stat.totalCPUTime;
+ for (let i = 0; i < stat.durations.length; ++i) {
+ result.durations[i] += stat.durations[i];
+ }
+ result.longestDuration = Math.max(result.longestDuration, stat.longestDuration);
+ }
+ return result;
+ }
+ }),
+
+ /**
+ * A probe measuring CPOW activity.
+ *
+ * Data provided by this probe uses the following format:
+ *
+ * @field {number} totalCPOWTime The amount of wallclock time
+ * spent executing blocking cross-process calls, in µs.
+ */
+ cpow: new Probe("cpow", {
+ set isActive(x) {
+ performanceStatsService.isMonitoringCPOW = x;
+ },
+ get isActive() {
+ return performanceStatsService.isMonitoringCPOW;
+ },
+ extract: function(xpcom) {
+ return {
+ totalCPOWTime: xpcom.totalCPOWTime
+ };
+ },
+ isEqual: function(a, b) {
+ return a.totalCPOWTime == b.totalCPOWTime;
+ },
+ subtract: function(a, b) {
+ return {
+ totalCPOWTime: a.totalCPOWTime - b.totalCPOWTime
+ };
+ },
+ importChildCompartments: function() { /* nothing to do */ },
+ compose: function(stats) {
+ let totalCPOWTime = 0;
+ for (let stat of stats) {
+ totalCPOWTime += stat.totalCPOWTime;
+ }
+ return { totalCPOWTime };
+ },
+ }),
+
+ /**
+ * A probe measuring activations, i.e. the number
+ * of times code execution has entered a given
+ * PerformanceGroup.
+ *
+ * Note that this probe is always active.
+ *
+ * Data provided by this probe uses the following format:
+ * @type {number} ticks The number of times execution has entered
+ * this performance group.
+ */
+ ticks: new Probe("ticks", {
+ set isActive(x) { /* this probe cannot be deactivated */ },
+ get isActive() { return true; },
+ extract: function(xpcom) {
+ return {
+ ticks: xpcom.ticks
+ };
+ },
+ isEqual: function(a, b) {
+ return a.ticks == b.ticks;
+ },
+ subtract: function(a, b) {
+ return {
+ ticks: a.ticks - b.ticks
+ };
+ },
+ importChildCompartments: function() { /* nothing to do */ },
+ compose: function(stats) {
+ let ticks = 0;
+ for (let stat of stats) {
+ ticks += stat.ticks;
+ }
+ return { ticks };
+ },
+ }),
+
+ compartments: new Probe("compartments", {
+ set isActive(x) {
+ performanceStatsService.isMonitoringPerCompartment = x;
+ },
+ get isActive() {
+ return performanceStatsService.isMonitoringPerCompartment;
+ },
+ extract: function(xpcom) {
+ return null;
+ },
+ isEqual: function(a, b) {
+ return true;
+ },
+ subtract: function(a, b) {
+ return true;
+ },
+ importChildCompartments: function(parent, children) {
+ parent.children = children;
+ },
+ compose: function(stats) {
+ return null;
+ },
+ }),
+};
+
+/**
+ * A monitor for a set of probes.
+ *
+ * Keeping probes active when they are unused is often a bad
+ * idea for performance reasons. Upon destruction, or whenever
+ * a client calls `dispose`, this monitor releases the probes,
+ * which may let the system deactivate them.
+ */
+function PerformanceMonitor(probes) {
+ this._probes = probes;
+
+ // Activate low-level features as needed
+ for (let probe of probes) {
+ probe.acquire();
+ }
+
+ // A finalization witness. At some point after the garbage-collection of
+ // `this` object, a notification of `FINALIZATION_TOPIC` will be triggered
+ // with `id` as message.
+ this._id = PerformanceMonitor.makeId();
+ this._finalizer = finalizer.make(FINALIZATION_TOPIC, this._id)
+ PerformanceMonitor._monitors.set(this._id, probes);
+}
+PerformanceMonitor.prototype = {
+ /**
+ * The names of probes activated in this monitor.
+ */
+ get probeNames() {
+ return this._probes.map(probe => probe.name);
+ },
+
+ /**
+ * Return asynchronously a snapshot with the data
+ * for each probe monitored by this PerformanceMonitor.
+ *
+ * All numeric values are non-negative and can only increase. Depending on
+ * the probe and the underlying operating system, probes may not be available
+ * immediately and may miss some activity.
+ *
+ * Clients should NOT expect that the first call to `promiseSnapshot()`
+ * will return a `Snapshot` in which all values are 0. For most uses,
+ * the appropriate scenario is to perform a first call to `promiseSnapshot()`
+ * to obtain a baseline, and then watch evolution of the values by calling
+ * `promiseSnapshot()` and `subtract()`.
+ *
+ * On the other hand, numeric values are also monotonic across several instances
+ * of a PerformanceMonitor with the same probes.
+ * let a = PerformanceStats.getMonitor(someProbes);
+ * let snapshot1 = yield a.promiseSnapshot();
+ *
+ * // ...
+ * let b = PerformanceStats.getMonitor(someProbes); // Same list of probes
+ * let snapshot2 = yield b.promiseSnapshot();
+ *
+ * // all values of `snapshot2` are greater or equal to values of `snapshot1`.
+ *
+ * @param {object} options If provided, an object that may contain the following
+ * fields:
+ * {Array<string>} probeNames The subset of probes to use for this snapshot.
+ * These probes must be a subset of the probes active in the monitor.
+ *
+ * @return {Promise}
+ * @resolve {Snapshot}
+ */
+ _checkBeforeSnapshot: function(options) {
+ if (!this._finalizer) {
+ throw new Error("dispose() has already been called, this PerformanceMonitor is not usable anymore");
+ }
+ let probes;
+ if (options && options.probeNames || undefined) {
+ if (!Array.isArray(options.probeNames)) {
+ throw new TypeError();
+ }
+ // Make sure that we only request probes that we have
+ for (let probeName of options.probeNames) {
+ let probe = this._probes.find(probe => probe.name == probeName);
+ if (!probe) {
+ throw new TypeError(`I need probe ${probeName} but I only have ${this.probeNames}`);
+ }
+ if (!probes) {
+ probes = [];
+ }
+ probes.push(probe);
+ }
+ } else {
+ probes = this._probes;
+ }
+ return probes;
+ },
+ promiseContentSnapshot: function(options = null) {
+ this._checkBeforeSnapshot(options);
+ return (new ProcessSnapshot(performanceStatsService.getSnapshot()));
+ },
+ promiseSnapshot: function(options = null) {
+ let probes = this._checkBeforeSnapshot(options);
+ return Task.spawn(function*() {
+ let childProcesses = yield Process.broadcastAndCollect("collect", {probeNames: probes.map(p => p.name)});
+ let xpcom = performanceStatsService.getSnapshot();
+ return new ApplicationSnapshot({
+ xpcom,
+ childProcesses,
+ probes,
+ date: Cu.now()
+ });
+ });
+ },
+
+ /**
+ * Release the probes used by this monitor.
+ *
+ * Releasing probes as soon as they are unused is a good idea, as some probes
+ * cost CPU and/or memory.
+ */
+ dispose: function() {
+ if (!this._finalizer) {
+ return;
+ }
+ this._finalizer.forget();
+ PerformanceMonitor.dispose(this._id);
+
+ // As a safeguard against double-release, reset everything to `null`
+ this._probes = null;
+ this._id = null;
+ this._finalizer = null;
+ }
+};
+/**
+ * @type {Map<string, Array<string>>} A map from id (as produced by `makeId`)
+ * to list of probes. Used to deallocate a list of probes during finalization.
+ */
+PerformanceMonitor._monitors = new Map();
+
+/**
+ * Create a `PerformanceMonitor` for a list of probes, register it for
+ * finalization.
+ */
+PerformanceMonitor.make = function(probeNames) {
+ // Sanity checks
+ if (!Array.isArray(probeNames)) {
+ throw new TypeError("Expected an array, got " + probes);
+ }
+ let probes = [];
+ for (let probeName of probeNames) {
+ if (!(probeName in Probes)) {
+ throw new TypeError("Probe not implemented: " + probeName);
+ }
+ probes.push(Probes[probeName]);
+ }
+
+ return (new PerformanceMonitor(probes));
+};
+
+/**
+ * Implementation of `dispose`.
+ *
+ * The actual implementation of `dispose` is as a method of `PerformanceMonitor`,
+ * rather than `PerformanceMonitor.prototype`, to avoid needing a strong reference
+ * to instances of `PerformanceMonitor`, which would defeat the purpose of
+ * finalization.
+ */
+PerformanceMonitor.dispose = function(id) {
+ let probes = PerformanceMonitor._monitors.get(id);
+ if (!probes) {
+ throw new TypeError("`dispose()` has already been called on this monitor");
+ }
+
+ PerformanceMonitor._monitors.delete(id);
+ for (let probe of probes) {
+ probe.release();
+ }
+}
+
+// Generate a unique id for each PerformanceMonitor. Used during
+// finalization.
+PerformanceMonitor._counter = 0;
+PerformanceMonitor.makeId = function() {
+ return "PerformanceMonitor-" + (this._counter++);
+}
+
+// Once a `PerformanceMonitor` has been garbage-collected,
+// release the probes unless `dispose()` has already been called.
+Services.obs.addObserver(function(subject, topic, value) {
+ PerformanceMonitor.dispose(value);
+}, FINALIZATION_TOPIC, false);
+
+// Public API
+this.PerformanceStats = {
+ /**
+ * Create a monitor for observing a set of performance probes.
+ */
+ getMonitor: function(probes) {
+ return PerformanceMonitor.make(probes);
+ }
+};
+
+
+/**
+ * Information on a single performance group.
+ *
+ * This offers the following fields:
+ *
+ * @field {string} name The name of the performance group:
+ * - for the process itself, "<process>";
+ * - for platform code, "<platform>";
+ * - for an add-on, the identifier of the addon (e.g. "myaddon@foo.bar");
+ * - for a webpage, the url of the page.
+ *
+ * @field {string} addonId The identifier of the addon (e.g. "myaddon@foo.bar").
+ *
+ * @field {string|null} title The title of the webpage to which this code
+ * belongs. Note that this is the title of the entire webpage (i.e. the tab),
+ * even if the code is executed in an iframe. Also note that this title may
+ * change over time.
+ *
+ * @field {number} windowId The outer window ID of the top-level nsIDOMWindow
+ * to which this code belongs. May be 0 if the code doesn't belong to any
+ * nsIDOMWindow.
+ *
+ * @field {boolean} isSystem `true` if the component is a system component (i.e.
+ * an add-on or platform-code), `false` otherwise (i.e. a webpage).
+ *
+ * @field {object|undefined} activations See the documentation of probe "ticks".
+ * `undefined` if this probe is not active.
+ *
+ * @field {object|undefined} jank See the documentation of probe "jank".
+ * `undefined` if this probe is not active.
+ *
+ * @field {object|undefined} cpow See the documentation of probe "cpow".
+ * `undefined` if this probe is not active.
+ */
+function PerformanceDataLeaf({xpcom, json, probes}) {
+ if (xpcom && json) {
+ throw new TypeError("Cannot import both xpcom and json data");
+ }
+ let source = xpcom || json;
+ for (let k of PROPERTIES_META) {
+ this[k] = source[k];
+ }
+ if (xpcom) {
+ for (let probe of probes) {
+ this[probe.name] = probe.extract(xpcom);
+ }
+ this.isChildProcess = false;
+ } else {
+ for (let probe of probes) {
+ this[probe.name] = json[probe.name];
+ }
+ this.isChildProcess = true;
+ }
+ this.owner = null;
+}
+PerformanceDataLeaf.prototype = {
+ /**
+ * Compare two instances of `PerformanceData`
+ *
+ * @return `true` if `this` and `to` have equal values in all fields.
+ */
+ equals: function(to) {
+ if (!(to instanceof PerformanceDataLeaf)) {
+ throw new TypeError();
+ }
+ for (let probeName of Object.keys(Probes)) {
+ let probe = Probes[probeName];
+ if (!probe.isEqual(this[probeName], to[probeName])) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Compute the delta between two instances of `PerformanceData`.
+ *
+ * @param {PerformanceData|null} to. If `null`, assumed an instance of
+ * `PerformanceData` in which all numeric values are 0.
+ *
+ * @return {PerformanceDiff} The performance usage between `to` and `this`.
+ */
+ subtract: function(to = null) {
+ return (new PerformanceDiffLeaf(this, to));
+ }
+};
+
+function PerformanceData(timestamp) {
+ this._parent = null;
+ this._content = new Map();
+ this._all = [];
+ this._timestamp = timestamp;
+}
+PerformanceData.prototype = {
+ addChild: function(stat) {
+ if (!(stat instanceof PerformanceDataLeaf)) {
+ throw new TypeError(); // FIXME
+ }
+ if (!stat.isChildProcess) {
+ throw new TypeError(); // FIXME
+ }
+ this._content.set(stat.groupId, stat);
+ this._all.push(stat);
+ stat.owner = this;
+ },
+ setParent: function(stat) {
+ if (!(stat instanceof PerformanceDataLeaf)) {
+ throw new TypeError(); // FIXME
+ }
+ if (stat.isChildProcess) {
+ throw new TypeError(); // FIXME
+ }
+ this._parent = stat;
+ this._all.push(stat);
+ stat.owner = this;
+ },
+ equals: function(to) {
+ if (this._parent && !to._parent) {
+ return false;
+ }
+ if (!this._parent && to._parent) {
+ return false;
+ }
+ if (this._content.size != to._content.size) {
+ return false;
+ }
+ if (this._parent && !this._parent.equals(to._parent)) {
+ return false;
+ }
+ for (let [k, v] of this._content) {
+ let v2 = to._content.get(k);
+ if (!v2) {
+ return false;
+ }
+ if (!v.equals(v2)) {
+ return false;
+ }
+ }
+ return true;
+ },
+ subtract: function(to = null) {
+ return (new PerformanceDiff(this, to));
+ },
+ get addonId() {
+ return this._all[0].addonId;
+ },
+ get title() {
+ return this._all[0].title;
+ }
+};
+
+function PerformanceDiff(current, old = null) {
+ this.addonId = current.addonId;
+ this.title = current.title;
+ this.windowId = current.windowId;
+ this.deltaT = old ? current._timestamp - old._timestamp : Infinity;
+ this._all = [];
+
+ // Handle the parent, if any.
+ if (current._parent) {
+ this._parent = old?current._parent.subtract(old._parent):current._parent;
+ this._all.push(this._parent);
+ this._parent.owner = this;
+ } else {
+ this._parent = null;
+ }
+
+ // Handle the children, if any.
+ this._content = new Map();
+ for (let [k, stat] of current._content) {
+ let diff = stat.subtract(old ? old._content.get(k) : null);
+ this._content.set(k, diff);
+ this._all.push(diff);
+ diff.owner = this;
+ }
+
+ // Now consolidate data
+ for (let k of Object.keys(Probes)) {
+ if (!(k in this._all[0])) {
+ // The stats don't contain data from this probe.
+ continue;
+ }
+ let data = this._all.map(item => item[k]);
+ let probe = Probes[k];
+ this[k] = probe.compose(data);
+ }
+}
+PerformanceDiff.prototype = {
+ toString: function() {
+ return `[PerformanceDiff] ${this.key}`;
+ },
+ get windowIds() {
+ return this._all.map(item => item.windowId).filter(x => !!x);
+ },
+ get groupIds() {
+ return this._all.map(item => item.groupId);
+ },
+ get key() {
+ if (this.addonId) {
+ return this.addonId;
+ }
+ if (this._parent) {
+ return this._parent.windowId;
+ }
+ return this._all[0].groupId;
+ },
+ get names() {
+ return this._all.map(item => item.name);
+ },
+ get processes() {
+ return this._all.map(item => ({ isChildProcess: item.isChildProcess, processId: item.processId}));
+ }
+};
+
+/**
+ * The delta between two instances of `PerformanceDataLeaf`.
+ *
+ * Used to monitor resource usage between two timestamps.
+ */
+function PerformanceDiffLeaf(current, old = null) {
+ for (let k of PROPERTIES_META) {
+ this[k] = current[k];
+ }
+
+ for (let probeName of Object.keys(Probes)) {
+ let other = null;
+ if (old && probeName in old) {
+ other = old[probeName];
+ }
+
+ if (probeName in current) {
+ this[probeName] = Probes[probeName].subtract(current[probeName], other);
+ }
+ }
+}
+
+/**
+ * A snapshot of a single process.
+ */
+function ProcessSnapshot({xpcom, probes}) {
+ this.componentsData = [];
+
+ let subgroups = new Map();
+ let enumeration = xpcom.getComponentsData().enumerate();
+ while (enumeration.hasMoreElements()) {
+ let xpcom = enumeration.getNext().QueryInterface(Ci.nsIPerformanceStats);
+ let stat = (new PerformanceDataLeaf({xpcom, probes}));
+
+ if (!xpcom.parentId) {
+ this.componentsData.push(stat);
+ } else {
+ let siblings = subgroups.get(xpcom.parentId);
+ if (!siblings) {
+ subgroups.set(xpcom.parentId, (siblings = []));
+ }
+ siblings.push(stat);
+ }
+ }
+
+ for (let group of this.componentsData) {
+ for (let probe of probes) {
+ probe.importChildCompartments(group, subgroups.get(group.groupId) || []);
+ }
+ }
+
+ this.processData = (new PerformanceDataLeaf({xpcom: xpcom.getProcessData(), probes}));
+}
+
+/**
+ * A snapshot of the performance usage of the application.
+ *
+ * @param {nsIPerformanceSnapshot} xpcom The data acquired from this process.
+ * @param {Array<Object>} childProcesses The data acquired from children processes.
+ * @param {Array<Probe>} probes The active probes.
+ */
+function ApplicationSnapshot({xpcom, childProcesses, probes, date}) {
+ ProcessSnapshot.call(this, {xpcom, probes});
+
+ this.addons = new Map();
+ this.webpages = new Map();
+ this.date = date;
+
+ // Child processes
+ for (let {componentsData} of (childProcesses || [])) {
+ // We are only interested in `componentsData` for the time being.
+ for (let json of componentsData) {
+ let leaf = (new PerformanceDataLeaf({json, probes}));
+ this.componentsData.push(leaf);
+ }
+ }
+
+ for (let leaf of this.componentsData) {
+ let key, map;
+ if (leaf.addonId) {
+ key = leaf.addonId;
+ map = this.addons;
+ } else if (leaf.windowId) {
+ key = leaf.windowId;
+ map = this.webpages;
+ } else {
+ continue;
+ }
+
+ let combined = map.get(key);
+ if (!combined) {
+ combined = new PerformanceData(date);
+ map.set(key, combined);
+ }
+ if (leaf.isChildProcess) {
+ combined.addChild(leaf);
+ } else {
+ combined.setParent(leaf);
+ }
+ }
+}
+
+/**
+ * Communication with other processes
+ */
+var Process = {
+ // a counter used to match responses to requests
+ _idcounter: 0,
+ _loader: null,
+ /**
+ * If we are in a child process, return `null`.
+ * Otherwise, return the global parent process message manager
+ * and load the script to connect to children processes.
+ */
+ get loader() {
+ if (isContent) {
+ return null;
+ }
+ if (this._loader) {
+ return this._loader;
+ }
+ Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceStats-content.js",
+ true/* including future processes*/);
+ return this._loader = Services.ppmm;
+ },
+
+ /**
+ * Broadcast a message to all children processes.
+ *
+ * NOOP if we are in a child process.
+ */
+ broadcast: function(topic, payload) {
+ if (!this.loader) {
+ return;
+ }
+ this.loader.broadcastAsyncMessage("performance-stats-service-" + topic, {payload});
+ },
+
+ /**
+ * Brodcast a message to all children processes and wait for answer.
+ *
+ * NOOP if we are in a child process, or if we have no children processes,
+ * in which case we return `undefined`.
+ *
+ * @return {undefined} If we have no children processes, in particular
+ * if we are in a child process.
+ * @return {Promise<Array<Object>>} If we have children processes, an
+ * array of objects with a structure similar to PerformanceData. Note
+ * that the array may be empty if no child process responded.
+ */
+ broadcastAndCollect: Task.async(function*(topic, payload) {
+ if (!this.loader || this.loader.childCount == 1) {
+ return undefined;
+ }
+ const TOPIC = "performance-stats-service-" + topic;
+ let id = this._idcounter++;
+
+ // The number of responses we are expecting. Note that we may
+ // not receive all responses if a process is too long to respond.
+ let expecting = this.loader.childCount;
+
+ // The responses we have collected, in arbitrary order.
+ let collected = [];
+ let deferred = PromiseUtils.defer();
+
+ let observer = function({data, target}) {
+ if (data.id != id) {
+ // Collision between two collections,
+ // ignore the other one.
+ return;
+ }
+ if (data.data) {
+ collected.push(data.data)
+ }
+ if (--expecting > 0) {
+ // We are still waiting for at least one response.
+ return;
+ }
+ deferred.resolve();
+ };
+ this.loader.addMessageListener(TOPIC, observer);
+ this.loader.broadcastAsyncMessage(
+ TOPIC,
+ {id, payload}
+ );
+
+ // Processes can die/freeze/be busy loading a page..., so don't expect
+ // that they will always respond.
+ let timeout = setTimeout(() => {
+ if (expecting == 0) {
+ return;
+ }
+ deferred.resolve();
+ }, MAX_WAIT_FOR_CHILD_PROCESS_MS);
+
+ deferred.promise.then(() => {
+ clearTimeout(timeout);
+ });
+
+ yield deferred.promise;
+ this.loader.removeMessageListener(TOPIC, observer);
+
+ return collected;
+ })
+};
diff --git a/toolkit/components/perfmonitoring/PerformanceWatcher-content.js b/toolkit/components/perfmonitoring/PerformanceWatcher-content.js
new file mode 100644
index 0000000000..2956cf5d06
--- /dev/null
+++ b/toolkit/components/perfmonitoring/PerformanceWatcher-content.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * An API for being informed of slow add-ons and tabs
+ * (content process scripts).
+ */
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+/**
+ * `true` if this is a content process, `false` otherwise.
+ */
+let isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+if (isContent) {
+
+const { PerformanceWatcher } = Cu.import("resource://gre/modules/PerformanceWatcher.jsm", {});
+
+let toMsg = function(alerts) {
+ let result = [];
+ for (let {source, details} of alerts) {
+ // Convert xpcom values to serializable data.
+ let serializableSource = {};
+ for (let k of ["groupId", "name", "addonId", "windowId", "isSystem", "processId", "isContentProcess"]) {
+ serializableSource[k] = source[k];
+ }
+
+ let serializableDetails = {};
+ for (let k of ["reason", "highestJank", "highestCPOW"]) {
+ serializableDetails[k] = details[k];
+ }
+ result.push({source:serializableSource, details:serializableDetails});
+ }
+ return result;
+}
+
+PerformanceWatcher.addPerformanceListener({addonId: "*"}, alerts => {
+ Services.cpmm.sendAsyncMessage("performancewatcher-propagate-notifications",
+ {addons: toMsg(alerts)}
+ );
+});
+
+PerformanceWatcher.addPerformanceListener({windowId: 0}, alerts => {
+ Services.cpmm.sendAsyncMessage("performancewatcher-propagate-notifications",
+ {windows: toMsg(alerts)}
+ );
+});
+
+}
diff --git a/toolkit/components/perfmonitoring/PerformanceWatcher.jsm b/toolkit/components/perfmonitoring/PerformanceWatcher.jsm
new file mode 100644
index 0000000000..d0d0349749
--- /dev/null
+++ b/toolkit/components/perfmonitoring/PerformanceWatcher.jsm
@@ -0,0 +1,367 @@
+// -*- 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/. */
+
+"use strict";
+
+/**
+ * An API for being informed of slow add-ons and tabs.
+ *
+ * Generally, this API is both more CPU-efficient and more battery-efficient
+ * than PerformanceStats. As PerformanceStats, this API does not provide any
+ * information during the startup or shutdown of Firefox.
+ *
+ * = Examples =
+ *
+ * Example use: reporting whenever a specific add-on slows down Firefox.
+ * let listener = function(source, details) {
+ * // This listener is triggered whenever the addon causes Firefox to miss
+ * // frames. Argument `source` contains information about the source of the
+ * // slowdown (including the process in which it happens), while `details`
+ * // contains performance statistics.
+ * console.log(`Oops, add-on ${source.addonId} seems to be slowing down Firefox.`, details);
+ * };
+ * PerformanceWatcher.addPerformanceListener({addonId: "myaddon@myself.name"}, listener);
+ *
+ * Example use: reporting whenever any webpage slows down Firefox.
+ * let listener = function(alerts) {
+ * // This listener is triggered whenever any window causes Firefox to miss
+ * // frames. FieldArgument `source` contains information about the source of the
+ * // slowdown (including the process in which it happens), while `details`
+ * // contains performance statistics.
+ * for (let {source, details} of alerts) {
+ * console.log(`Oops, window ${source.windowId} seems to be slowing down Firefox.`, details);
+ * };
+ * // Special windowId 0 lets us to listen to all webpages.
+ * PerformanceWatcher.addPerformanceListener({windowId: 0}, listener);
+ *
+ *
+ * = How this works =
+ *
+ * This high-level API is based on the lower-level nsIPerformanceStatsService.
+ * At the end of each event (including micro-tasks), the nsIPerformanceStatsService
+ * updates its internal performance statistics and determines whether any
+ * add-on/window in the current process has exceeded the jank threshold.
+ *
+ * The PerformanceWatcher maintains low-level performance observers in each
+ * process and forwards alerts to the main process. Internal observers collate
+ * low-level main process alerts and children process alerts and notify clients
+ * of this API.
+ */
+
+this.EXPORTED_SYMBOLS = ["PerformanceWatcher"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let { PerformanceStats, performanceStatsService } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {});
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// `true` if the code is executed in content, `false` otherwise
+let isContent = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+if (!isContent) {
+ // Initialize communication with children.
+ //
+ // To keep the protocol simple, the children inform the parent whenever a slow
+ // add-on/tab is detected. We do not attempt to implement thresholds.
+ Services.ppmm.loadProcessScript("resource://gre/modules/PerformanceWatcher-content.js",
+ true/* including future processes*/);
+
+ Services.ppmm.addMessageListener("performancewatcher-propagate-notifications",
+ (...args) => ChildManager.notifyObservers(...args)
+ );
+}
+
+// Configure the performance stats service to inform us in case of jank.
+performanceStatsService.jankAlertThreshold = 64000 /* us */;
+
+
+/**
+ * Handle communications with child processes. Handle listening to
+ * either a single add-on id (including the special add-on id "*",
+ * which is notified for all add-ons) or a single window id (including
+ * the special window id 0, which is notified for all windows).
+ *
+ * Acquire through `ChildManager.getAddon` and `ChildManager.getWindow`.
+ */
+function ChildManager(map, key) {
+ this.key = key;
+ this._map = map;
+ this._listeners = new Set();
+}
+ChildManager.prototype = {
+ /**
+ * Add a listener, which will be notified whenever a child process
+ * reports a slow performance alert for this addon/window.
+ */
+ addListener: function(listener) {
+ this._listeners.add(listener);
+ },
+ /**
+ * Remove a listener.
+ */
+ removeListener: function(listener) {
+ let deleted = this._listeners.delete(listener);
+ if (!deleted) {
+ throw new Error("Unknown listener");
+ }
+ },
+
+ listeners: function() {
+ return this._listeners.values();
+ }
+};
+
+/**
+ * Dispatch child alerts to observers.
+ *
+ * Triggered by messages from content processes.
+ */
+ChildManager.notifyObservers = function({data: {addons, windows}}) {
+ if (addons && addons.length > 0) {
+ // Dispatch the entire list to universal listeners
+ this._notify(ChildManager.getAddon("*").listeners(), addons);
+
+ // Dispatch individual alerts to individual listeners
+ for (let {source, details} of addons) {
+ this._notify(ChildManager.getAddon(source.addonId).listeners(), source, details);
+ }
+ }
+ if (windows && windows.length > 0) {
+ // Dispatch the entire list to universal listeners
+ this._notify(ChildManager.getWindow(0).listeners(), windows);
+
+ // Dispatch individual alerts to individual listeners
+ for (let {source, details} of windows) {
+ this._notify(ChildManager.getWindow(source.windowId).listeners(), source, details);
+ }
+ }
+};
+
+ChildManager._notify = function(targets, ...args) {
+ for (let target of targets) {
+ target(...args);
+ }
+};
+
+ChildManager.getAddon = function(key) {
+ return this._get(this._addons, key);
+};
+ChildManager._addons = new Map();
+
+ChildManager.getWindow = function(key) {
+ return this._get(this._windows, key);
+};
+ChildManager._windows = new Map();
+
+ChildManager._get = function(map, key) {
+ let result = map.get(key);
+ if (!result) {
+ result = new ChildManager(map, key);
+ map.set(key, result);
+ }
+ return result;
+};
+let gListeners = new WeakMap();
+
+/**
+ * An object in charge of managing all the observables for a single
+ * target (window/addon/all windows/all addons).
+ *
+ * In a content process, a target is represented by a single observable.
+ * The situation is more sophisticated in a parent process, as a target
+ * has both an in-process observable and several observables across children
+ * processes.
+ *
+ * This class abstracts away the difference to simplify the work of
+ * (un)registering observers for targets.
+ *
+ * @param {object} target The target being observed, as an object
+ * with one of the following fields:
+ * - {string} addonId Either "*" for the universal add-on observer
+ * or the add-on id of an addon. Note that this class does not
+ * check whether the add-on effectively exists, and that observers
+ * may be registered for an add-on before the add-on is installed
+ * or started.
+ * - {xul:tab} tab A single tab. It must already be initialized.
+ * - {number} windowId Either 0 for the universal window observer
+ * or the outer window id of the window.
+ */
+function Observable(target) {
+ // A mapping from `listener` (function) to `Observer`.
+ this._observers = new Map();
+ if ("addonId" in target) {
+ this._key = `addonId: ${target.addonId}`;
+ this._process = performanceStatsService.getObservableAddon(target.addonId);
+ this._children = isContent ? null : ChildManager.getAddon(target.addonId);
+ this._isBuffered = target.addonId == "*";
+ } else if ("tab" in target || "windowId" in target) {
+ let windowId;
+ if ("tab" in target) {
+ windowId = target.tab.linkedBrowser.outerWindowID;
+ // By convention, outerWindowID may not be 0.
+ } else if ("windowId" in target) {
+ windowId = target.windowId;
+ }
+ if (windowId == undefined || windowId == null) {
+ throw new TypeError(`No outerWindowID. Perhaps the target is a tab that is not initialized yet.`);
+ }
+ this._key = `tab-windowId: ${windowId}`;
+ this._process = performanceStatsService.getObservableWindow(windowId);
+ this._children = isContent ? null : ChildManager.getWindow(windowId);
+ this._isBuffered = windowId == 0;
+ } else {
+ throw new TypeError("Unexpected target");
+ }
+}
+Observable.prototype = {
+ addJankObserver: function(listener) {
+ if (this._observers.has(listener)) {
+ throw new TypeError(`Listener already registered for target ${this._key}`);
+ }
+ if (this._children) {
+ this._children.addListener(listener);
+ }
+ let observer = this._isBuffered ? new BufferedObserver(listener)
+ : new Observer(listener);
+ // Store the observer to be able to call `this._process.removeJankObserver`.
+ this._observers.set(listener, observer);
+
+ this._process.addJankObserver(observer);
+ },
+ removeJankObserver: function(listener) {
+ let observer = this._observers.get(listener);
+ if (!observer) {
+ throw new TypeError(`No listener for target ${this._key}`);
+ }
+ this._observers.delete(listener);
+
+ if (this._children) {
+ this._children.removeListener(listener);
+ }
+
+ this._process.removeJankObserver(observer);
+ observer.dispose();
+ },
+};
+
+/**
+ * Get a cached observable for a given target.
+ */
+Observable.get = function(target) {
+ let key;
+ if ("addonId" in target) {
+ key = target.addonId;
+ } else if ("tab" in target) {
+ // We do not want to use a tab as a key, as this would prevent it from
+ // being garbage-collected.
+ key = target.tab.linkedBrowser.outerWindowID;
+ } else if ("windowId" in target) {
+ key = target.windowId;
+ }
+ if (key == null) {
+ throw new TypeError(`Could not extract a key from ${JSON.stringify(target)}. Could the target be an unitialized tab?`);
+ }
+ let observable = this._cache.get(key);
+ if (!observable) {
+ observable = new Observable(target);
+ this._cache.set(key, observable);
+ }
+ return observable;
+};
+Observable._cache = new Map();
+
+/**
+ * Wrap a listener callback as an unbuffered nsIPerformanceObserver.
+ *
+ * Each observation is propagated immediately to the listener.
+ */
+function Observer(listener) {
+ // Make sure that monitoring stays alive (in all processes) at least as
+ // long as the observer.
+ this._monitor = PerformanceStats.getMonitor(["jank", "cpow"]);
+ this._listener = listener;
+}
+Observer.prototype = {
+ observe: function(...args) {
+ this._listener(...args);
+ },
+ dispose: function() {
+ this._monitor.dispose();
+ this.observe = function poison() {
+ throw new Error("Internal error: I should have stopped receiving notifications");
+ }
+ },
+};
+
+/**
+ * Wrap a listener callback as an buffered nsIPerformanceObserver.
+ *
+ * Observations are buffered and dispatch in the next tick to the listener.
+ */
+function BufferedObserver(listener) {
+ Observer.call(this, listener);
+ this._buffer = [];
+ this._isDispatching = false;
+ this._pending = null;
+}
+BufferedObserver.prototype = Object.create(Observer.prototype);
+BufferedObserver.prototype.observe = function(source, details) {
+ this._buffer.push({source, details});
+ if (!this._isDispatching) {
+ this._isDispatching = true;
+ Services.tm.mainThread.dispatch(() => {
+ // Grab buffer, in case something in the listener could modify it.
+ let buffer = this._buffer;
+ this._buffer = [];
+
+ // As of this point, any further observations need to use the new buffer
+ // and a new dispatcher.
+ this._isDispatching = false;
+
+ this._listener(buffer);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+};
+
+this.PerformanceWatcher = {
+ /**
+ * Add a listener informed whenever we receive a slow performance alert
+ * in the application.
+ *
+ * @param {object} target An object with one of the following fields:
+ * - {string} addonId Either "*" to observe all add-ons or a full add-on ID.
+ * to observe a single add-on.
+ * - {number} windowId Either 0 to observe all windows or an outer window ID
+ * to observe a single tab.
+ * - {xul:browser} tab To observe a single tab.
+ * @param {function} listener A function that will be triggered whenever
+ * the target causes a slow performance notification. The notification may
+ * have originated in any process of the application.
+ *
+ * If the listener listens to a single add-on/webpage, it is triggered with
+ * the following arguments:
+ * source: {groupId, name, addonId, windowId, isSystem, processId}
+ * Information on the source of the notification.
+ * details: {reason, highestJank, highestCPOW} Information on the
+ * notification.
+ *
+ * If the listener listens to all add-ons/all webpages, it is triggered with
+ * an array of {source, details}, as described above.
+ */
+ addPerformanceListener: function(target, listener) {
+ if (typeof listener != "function") {
+ throw new TypeError();
+ }
+ let observable = Observable.get(target);
+ observable.addJankObserver(listener);
+ },
+ removePerformanceListener: function(target, listener) {
+ if (typeof listener != "function") {
+ throw new TypeError();
+ }
+ let observable = Observable.get(target);
+ observable.removeJankObserver(listener);
+ },
+};
diff --git a/toolkit/components/perfmonitoring/README.md b/toolkit/components/perfmonitoring/README.md
new file mode 100644
index 0000000000..abd109e45f
--- /dev/null
+++ b/toolkit/components/perfmonitoring/README.md
@@ -0,0 +1,120 @@
+This directory is part of the implementation of the Performance Monitoring API
+
+# What is the Performance Monitoring API?
+
+The Performance Monitoring API is a set of interfaces designed to let front-end code find out if the application or a specific process is currently janky, quantify this jank and its evolution, and investigate what is causing jank (system code? a webpage? an add-on? CPOW?). In other words, this is a form of minimal profiler, designed to be lightweight enough to be enabled at all times in production code.
+
+In Firefox Nightly, the Performance Monitoring API is used to:
+- inform users if their machine janks because of an add-on;
+- upload add-on performance to Telemetry for the benefit of AMO maintainers and add-on developers;
+- let users inspect the performance of their browser through about:performance.
+
+# How can I use the API?
+
+The API is designed mainly to be used from JavaScript client code, using PerformanceStats.jsm. If you really need to use it from C++ code, you should use the performance stats service defined in nsIPerformanceStats.idl. Note that PerformanceStats.jsm contains support for entire e10s-enabled applications, while nsIPerformanceStats.idl only supports one process at a time.
+
+
+# How does the Performance Monitoring API work?
+
+At the time of this writing, the implementation of this API monitors only performance information related to the execution of JavaScript code, and only in the main thread. This is performed by an instrumentation of js/, orchestrated by toolkit/.
+
+At low-level, the unit of code used for monitoring is the JS Compartment: one .jsm module, one XPCOM component, one sandbox, one script in an iframe, ... When executing code in a compartment, it is possible to inspect either the compartment or the script itself to find out who this compartment belongs to: a `<xul:browser>`, an add-on, etc.
+
+At higher-level, the unit of code used for monitoring is the Performance Group. One Performance Group represents one or more JS Compartments, grouped together because we are interested in their performance. The current implementation uses Performance Groups to represent individual JS Compartments, entire add-ons, entire webpages including iframes and entire threads. Other applications have been discussed to represent entire eTLD+1 domains (e.g. to monitor the cost of ads), etc.
+
+A choice was made to represent the CPU cost in *clock cycles* at low-level, as extracting a number of clock cycles has a very low latency (typically a few dozen cycles on recent architectures) and is much more precise than `getrusage`-style CPU clocks (which are often limited to a precision of 16ms). The drawback of this choice is that distinct CPUs/cores may, depending on the architecture, have entirely unrelated clock cycles count. We assume that the risk of false positives is reasonably low, and bound the uncertainty by renormalizing the result with the actual CPU clocks once per event.
+
+## SpiderMonkey-level
+
+The instrumentation of SpiderMonkey lives in `js/src/vm/Stopwatch.*`. As SpiderMonkey does not know about the Gecko event loop, or DOM events, or windows, so any such information must be provided by the embedding. To communicate with higher levels, SpiderMonkey exposes a virtual class `js::PerformanceGroup` designed to be subclassed and instantiated by the embedding based on its interests.
+
+An instance of `js::PerformanceGroup` may be acquired (to mark that it is currently being monitored) and released (once monitoring is complete or cancelled) by SpiderMonkey. Furthermore, a `js::PerformanceGroup` can be marked as active (to mark that the embedding is currently interested in its performance) or inactive (otherwise) by the embedding.
+
+Each `js::Performance` holds a total CPU cost measured in *clock cycles* and a total CPOW cost measured in *microseconds*. Both costs can only increase while measuring data, and can be reset to 0 by the embedding, once we have finished execution of the event loop.
+
+### Upon starting to execute code in a JS Compartment `cx`
+1. If global monitoring is deactivated, bailout;
+2. If XPConnect has informed us that we are entering a nested event loop, cancel any ongoing measure on the outer event loop and proceed with the current measure;
+3. If we do not know to which performance groups `cx` is associated, request the information from the embedding;
+4. For each performance group `group` to which `cx` belongs *and* that is not acquired *and* for which monitoring is active, acquire the group;
+5. If no group was acquired, bailout;
+6. Capture a timestamp for the CPU cost of `cx`, in *clock cycles*. This value is provided directly by the CPU;
+7. Capture a timestamp for the CPOW cost of `cx`, in *CPOW microseconds*. This value is provided by the CPOW-level embedding.
+
+### Upon stopping execution of the code in the JS compartment `cx`
+1. If global monitoring is deactivated, bailout;
+2. If the measure has been canceled, bailout;
+3. If no group was acquired, bailout;
+4. Capture a timestamp for the CPU cost of `cx`, use it to update the total CPU cost of each of the groups acquired;
+5. Capture a timestamp for the CPOW cost of `cx`, use it to update the total CPOW cost of each of the groups acquired;
+6. Mark acquired groups as executed recently;
+7. Release groups.
+
+### When informed by the embedding that the iteration of the event loop is complete
+1. Commit all the groups executed recently to the embedding;
+2. Release all groups;
+3. Reset all CPU/CPOW costs to 0.
+
+## Cross-Process Object Wrapper-level
+
+The instrumentation of CPOW lives in `js/ipc/src`. It maintains a CPOW clock that increases whenever the process is blocked by a CPOW call.
+
+## XPConnect-level
+
+The instrumentation of XPConnect lives in `js/xpconnect/src/XPCJSContext.cpp`.
+
+### When we enter a nested event loop
+
+1. Inform the SpiderMonkey-level instrumentation, to let it cancel any ongoing measure.
+
+### When we finish executing an iteration of the event loop, including microtasks:
+
+1. Inform the SpiderMonkey-level instrumentation, to let it commit its recent data.
+
+## nsIPerformanceStatsService-level
+
+This code lives in `toolkit/components/perfmonitoring/ns*`. Its role is to orchestrate the information provided by SpiderMonkey at the scale of a single thread of a single process. At the time of this writing, this instrumentation is only activated on the main thread, for all Gecko processes.
+
+The service defines a class `nsPerformanceGroup`, designed to be the sole concrete implementation of `js::PerformanceGroup`. `nsPerformanceGroup` extends `js::PerformanceGroup` with the global performance information gathered for the group since the start of the service. The information is:
+- total CPU time measured;
+- total CPOW time measured;
+- number of times CPU time exceeded 1ms;
+- number of times CPU time exceeded 2ms;
+- number of times CPU time exceeded 4ms;
+- ...
+- number of times CPU time exceeded 2^9ms.
+
+Also, `nsPerformanceGroup` extends `js::PerformanceGroup` with high-level identification:
+- id of the window that executed the code, if any;
+- id of the add-on that provided the code, if any.
+
+### When the SpiderMonkey-level instrumentation requests a list of PerformanceGroup for a compartment
+
+Return a list with the following groups:
+* all compartments are associated with the "top group", which represents the entire thread;
+* find out if the compartment is code from a window, if so add a group shared by all compartments for this specific window;
+* find out if the compartment is code from an add-on, if so add a group shared by all compartments for this add-on;
+* add a group representing this specific compartment.
+
+For performance reasons, groups representing a single compartment are inactive by default, while all other groups are active by default.
+
+Performance groups are refcounted and destroyed with the implementation of `delete` used by toolkit/.
+
+### When the SpiderMonkey-level instrumentation commits a list of PerformanceGroups
+
+For each group in the list:
+1. transfer recent CPU time and recent CPOW time to total CPU time, total CPOW time, number of times CPU time exceeded *n* ms;
+2. reset group.
+
+Future versions are expected to trigger low-performance alerts at this stage.
+
+### Snapshotting
+
+(to be documented)
+
+## PerformanceStats.jsm-level
+
+PerformanceStats provides a JS-friendly API on top of nsIPerformanceStatsService. The main differences are:
+- utilities for subtracting snapshots;
+- tracking clients that need specific measures;
+- synchronization between e10s processes.
diff --git a/toolkit/components/perfmonitoring/moz.build b/toolkit/components/perfmonitoring/moz.build
new file mode 100644
index 0000000000..1effe5f009
--- /dev/null
+++ b/toolkit/components/perfmonitoring/moz.build
@@ -0,0 +1,35 @@
+# -*- 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']
+
+XPIDL_MODULE = 'toolkit_perfmonitoring'
+
+EXTRA_JS_MODULES += [
+ 'AddonWatcher.jsm',
+ 'PerformanceStats-content.js',
+ 'PerformanceStats.jsm',
+ 'PerformanceWatcher-content.js',
+ 'PerformanceWatcher.jsm',
+]
+
+XPIDL_SOURCES += [
+ 'nsIPerformanceStats.idl',
+]
+
+UNIFIED_SOURCES += [
+ 'nsPerformanceStats.cpp'
+]
+
+EXPORTS += [
+ 'nsPerformanceStats.h'
+]
+
+LOCAL_INCLUDES += [
+ '/dom/base',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/perfmonitoring/nsIPerformanceStats.idl b/toolkit/components/perfmonitoring/nsIPerformanceStats.idl
new file mode 100644
index 0000000000..2effd5403b
--- /dev/null
+++ b/toolkit/components/perfmonitoring/nsIPerformanceStats.idl
@@ -0,0 +1,333 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+#include "nsIArray.idl"
+#include "nsIDOMWindow.idl"
+
+/**
+ * Mechanisms for querying the current process about performance
+ * information.
+ *
+ * JavaScript clients should rather use PerformanceStats.jsm.
+ */
+
+/**
+ * Identification details for a performance group.
+ *
+ * A performance group is a set of JavaScript compartments whose
+ * performance is observed as a single entity. Typical examples of
+ * performance groups: an add-on, a webpage without its frames, a
+ * webpage with all its frames, the entire JS runtime, ...
+ */
+[scriptable, builtinclass, uuid(994c56be-939a-4f20-8364-124f6422d86a)]
+interface nsIPerformanceGroupDetails: nsISupports {
+ /**
+ * An identifier unique to the component.
+ *
+ * This identifier is somewhat human-readable to aid with debugging,
+ * but clients should not rely upon the format.
+ */
+ readonly attribute AString groupId;
+
+ /**
+ * A somewhat human-readable name for the component.
+ */
+ readonly attribute AString name;
+
+ /**
+ * If the component is an add-on, the ID of the addon,
+ * otherwise an empty string.
+ */
+ readonly attribute AString addonId;
+
+ /**
+ * If the component is code executed in a window, the ID of the topmost
+ * outer window (i.e. the tab), otherwise 0.
+ */
+ readonly attribute uint64_t windowId;
+
+ /**
+ * `true` if this component is executed with system privileges
+ * (e.g. the platform itself or an add-on), `false` otherwise
+ * (e.g. webpages).
+ */
+ readonly attribute bool isSystem;
+
+ /**
+ * The process running this group.
+ */
+ readonly attribute unsigned long long processId;
+
+ /**
+ * `true` if the code is executed in a content process, `false` otherwise.
+ */
+ readonly attribute bool isContentProcess;
+};
+
+/**
+ * Snapshot of the performance of a component, e.g. an add-on, a web
+ * page, system built-ins, a module or the entire process itself.
+ *
+ * All values are monotonic and are updated only when
+ * `nsIPerformanceStatsService.isStopwatchActive` is `true`.
+ */
+[scriptable, builtinclass, uuid(8a635d4b-aa56-466b-9a7d-9f91ca9405ef)]
+interface nsIPerformanceStats: nsIPerformanceGroupDetails {
+ /**
+ * Total amount of time spent executing code in this group, in
+ * microseconds.
+ */
+ readonly attribute unsigned long long totalUserTime;
+ readonly attribute unsigned long long totalSystemTime;
+ readonly attribute unsigned long long totalCPOWTime;
+
+ /**
+ * Total number of times code execution entered this group,
+ * since process launch. This may be greater than the number
+ * of times we have entered the event loop.
+ */
+ readonly attribute unsigned long long ticks;
+
+ /**
+ * Jank indicator.
+ *
+ * durations[i] == number of times execution of this group
+ * lasted at lest 2^i ms.
+ */
+ void getDurations([optional] out unsigned long aCount,
+ [retval, array, size_is(aCount)]out unsigned long long aNumberOfOccurrences);
+};
+
+/**
+ * A snapshot of the performance data of the process.
+ */
+[scriptable, builtinclass, uuid(13cc235b-739e-4690-b0e3-d89cbe036a93)]
+interface nsIPerformanceSnapshot: nsISupports {
+ /**
+ * Data on all individual components.
+ */
+ nsIArray getComponentsData();
+
+ /**
+ * Information on the process itself.
+ *
+ * This contains the total amount of time spent executing JS code,
+ * the total amount of time spent waiting for system calls while
+ * executing JS code, the total amount of time performing blocking
+ * inter-process calls, etc.
+ */
+ nsIPerformanceStats getProcessData();
+};
+
+/**
+ * A performance alert.
+ */
+[scriptable, builtinclass, uuid(a85706ab-d703-4687-8865-78cd771eab93)]
+interface nsIPerformanceAlert: nsISupports {
+ /**
+ * A slowdown was detected.
+ *
+ * See REASON_JANK_* for details on whether this slowdown was user-noticeable.
+ */
+ const unsigned long REASON_SLOWDOWN = 1;
+
+ /**
+ * This alert was triggered during a jank in animation.
+ *
+ * In the current implementation, we consider that there is a jank
+ * in animation if delivery of the vsync message to the main thread
+ * has been delayed too much (see
+ * nsIPerformanceStatsService.animationJankLevelThreshold).
+ *
+ * Note that this is a heuristic which may provide false positives,
+ * so clients of this API are expected to perform post-processing to
+ * filter out such false positives.
+ */
+ const unsigned long REASON_JANK_IN_ANIMATION = 2;
+
+ /**
+ * This alert was triggered during a jank in user input.
+ *
+ * In the current implementation, we consider that there is a jank
+ * in animation if a user input was received either immediately
+ * before executing the offending code (see
+ * nsIPerformanceStatsService.userInputDelayThreshold) or while
+ * executing the offending code.
+ *
+ * Note that this is a heuristic which may provide false positives,
+ * so clients of this API are expected to perform post-processing to
+ * filter out such false positives.
+ */
+ const unsigned long REASON_JANK_IN_INPUT = 4;
+
+ /**
+ * The reason for the alert, as a bitwise or of the various REASON_*
+ * constants.
+ */
+ readonly attribute unsigned long reason;
+
+ /**
+ * Longest interval spent executing code in this group
+ * since the latest alert, in microseconds.
+ *
+ * Note that the underlying algorithm is probabilistic and may
+ * provide false positives, so clients of this API are expected to
+ * perform post-processing to filter out such false positives. In
+ * particular, a high system load will increase the noise level on
+ * this measure.
+ */
+ readonly attribute unsigned long long highestJank;
+
+ /**
+ * Longest interval spent executing CPOW in this group
+ * since the latest alert, in microseconds.
+ *
+ * This measure is reliable and involves no heuristics. However,
+ * note that the duration of CPOWs is increased by high system
+ * loads.
+ */
+ readonly attribute unsigned long long highestCPOW;
+};
+
+
+/**
+ * An observer for slow performance alerts.
+ */
+[scriptable, function, uuid(b746a929-3fec-420b-8ed8-c35d71995e05)]
+interface nsIPerformanceObserver: nsISupports {
+ /**
+ * @param target The performance group that caused the jank.
+ * @param alert The performance cost that triggered the alert.
+ */
+ void observe(in nsIPerformanceGroupDetails target, in nsIPerformanceAlert alert);
+};
+
+
+/**
+ * A part of the system that may be observed for slow performance.
+ */
+[scriptable, builtinclass, uuid(b85720d0-e328-4342-9e46-8ca1acf8c70e)]
+interface nsIPerformanceObservable: nsISupports {
+ /**
+ * If a single group is being observed, information on this group.
+ */
+ readonly attribute nsIPerformanceGroupDetails target;
+
+ /**
+ * Add an observer that will be informed in case of jank.
+ *
+ * Set `jankAlertThreshold` to determine how much jank is needed
+ * to trigger alerts.
+ *
+ * If the same observer is added more than once, it will be
+ * triggered as many times as it has been added.
+ */
+ void addJankObserver(in nsIPerformanceObserver observer);
+
+ /**
+ * Remove an observer previously added with `addJankObserver`.
+ *
+ * Noop if the observer hasn't been added.
+ */
+ void removeJankObserver(in nsIPerformanceObserver observer);
+};
+
+
+[scriptable, uuid(505bc42e-be38-4a53-baba-92cb33690cde)]
+interface nsIPerformanceStatsService : nsISupports {
+ /**
+ * `true` if we should monitor CPOW, `false` otherwise.
+ */
+ [implicit_jscontext] attribute bool isMonitoringCPOW;
+
+ /**
+ * `true` if we should monitor jank, `false` otherwise.
+ */
+ [implicit_jscontext] attribute bool isMonitoringJank;
+
+ /**
+ * `true` if all compartments need to be monitored individually,
+ * `false` if only performance groups (i.e. entire add-ons, entire
+ * webpages, etc.) need to be monitored.
+ */
+ [implicit_jscontext] attribute bool isMonitoringPerCompartment;
+
+ /**
+ * Capture a snapshot of the performance data.
+ */
+ [implicit_jscontext] nsIPerformanceSnapshot getSnapshot();
+
+ /**
+ * The threshold, in microseconds, above which a performance group is
+ * considered "slow" and should raise performance alerts.
+ */
+ attribute unsigned long long jankAlertThreshold;
+
+ /**
+ * If a user is seeing an animation and we spend too long executing
+ * JS code while blocking refresh, this will be visible to the user.
+ *
+ * We assume that any jank during an animation and lasting more than
+ * 2^animationJankLevelThreshold ms will be visible.
+ */
+ attribute short animationJankLevelThreshold;
+
+ /**
+ * If a user performs an input (e.g. clicking, pressing a key, but
+ * *NOT* moving the mouse), and we spend too long executing JS code
+ * before displaying feedback, this will be visible to the user even
+ * if there is no ongoing animation.
+ *
+ * We assume that any jank during `userInputDelayThreshold` us after
+ * the user input will be visible.
+ */
+ attribute unsigned long long userInputDelayThreshold;
+
+ /**
+ * A buffering delay, in milliseconds, used by the service to
+ * regroup performance alerts, before observers are actually
+ * noticed. Higher delays let the system avoid redundant
+ * notifications for the same group, and are generally better for
+ * performance.
+ */
+ attribute unsigned long jankAlertBufferingDelay;
+
+ /**
+ * Get a nsIPerformanceObservable representing an add-on. This
+ * observable may then be used to (un)register for watching
+ * performance alerts for this add-on.
+ *
+ * Note that this method has no way of finding out whether an add-on with this
+ * id is installed on the system. Also note that this covers only the current
+ * process.
+ *
+ * Use special add-on name "*" to get an observable that may be used
+ * to (un)register for watching performance alerts of all add-ons at
+ * once.
+ */
+ nsIPerformanceObservable getObservableAddon(in AString addonId);
+
+ /**
+ * Get a nsIPerformanceObservable representing a DOM window. This
+ * observable may then be used to (un)register for watching
+ * performance alerts for this window.
+ *
+ * Note that this covers only the current process.
+ *
+ * Use special window id 0 to get an observable that may be used to
+ * (un)register for watching performance alerts of all windows at
+ * once.
+ */
+ nsIPerformanceObservable getObservableWindow(in unsigned long long windowId);
+};
+
+
+%{C++
+#define NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID {0xfd7435d4, 0x9ec4, 0x4699, \
+ {0xad, 0xd4, 0x1b, 0xe8, 0x3d, 0xd6, 0x8e, 0xf3} }
+#define NS_TOOLKIT_PERFORMANCESTATSSERVICE_CONTRACTID "@mozilla.org/toolkit/performance-stats-service;1"
+%}
diff --git a/toolkit/components/perfmonitoring/nsPerformanceStats.cpp b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
new file mode 100644
index 0000000000..eb924de46e
--- /dev/null
+++ b/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
@@ -0,0 +1,1620 @@
+/* -*- 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 "nsPerformanceStats.h"
+
+#include "nsMemory.h"
+#include "nsLiteralString.h"
+#include "nsCRTGlue.h"
+#include "nsServiceManagerUtils.h"
+
+#include "nsCOMArray.h"
+#include "nsContentUtils.h"
+#include "nsIMutableArray.h"
+#include "nsReadableUtils.h"
+
+#include "jsapi.h"
+#include "nsJSUtils.h"
+#include "xpcpublic.h"
+#include "jspubtd.h"
+
+#include "nsIDOMWindow.h"
+#include "nsGlobalWindow.h"
+#include "nsRefreshDriver.h"
+
+#include "mozilla/Unused.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+
+#if defined(XP_WIN)
+#include <processthreadsapi.h>
+#include <windows.h>
+#else
+#include <unistd.h>
+#endif // defined(XP_WIN)
+
+#if defined(XP_MACOSX)
+#include <mach/mach_init.h>
+#include <mach/mach_interface.h>
+#include <mach/mach_port.h>
+#include <mach/mach_types.h>
+#include <mach/message.h>
+#include <mach/thread_info.h>
+#elif defined(XP_UNIX)
+#include <sys/time.h>
+#include <sys/resource.h>
+#endif // defined(XP_UNIX)
+/* ------------------------------------------------------
+ *
+ * Utility functions.
+ *
+ */
+
+namespace {
+
+/**
+ * Get the private window for the current compartment.
+ *
+ * @return null if the code is not executed in a window or in
+ * case of error, a nsPIDOMWindow otherwise.
+ */
+already_AddRefed<nsPIDOMWindowOuter>
+GetPrivateWindow(JSContext* cx) {
+ nsGlobalWindow* win = xpc::CurrentWindowOrNull(cx);
+ if (!win) {
+ return nullptr;
+ }
+
+ nsPIDOMWindowOuter* outer = win->AsInner()->GetOuterWindow();
+ if (!outer) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> top = outer->GetTop();
+ if (!top) {
+ return nullptr;
+ }
+
+ return top.forget();
+}
+
+bool
+URLForGlobal(JSContext* cx, JS::Handle<JSObject*> global, nsAString& url) {
+ nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(global);
+ if (!principal) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = principal->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv) || !uri) {
+ return false;
+ }
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ url.Assign(NS_ConvertUTF8toUTF16(spec));
+ return true;
+}
+
+/**
+ * Extract a somewhat human-readable name from the current context.
+ */
+void
+CompartmentName(JSContext* cx, JS::Handle<JSObject*> global, nsAString& name) {
+ // Attempt to use the URL as name.
+ if (URLForGlobal(cx, global, name)) {
+ return;
+ }
+
+ // Otherwise, fallback to XPConnect's less readable but more
+ // complete naming scheme.
+ nsAutoCString cname;
+ xpc::GetCurrentCompartmentName(cx, cname);
+ name.Assign(NS_ConvertUTF8toUTF16(cname));
+}
+
+/**
+ * Generate a unique-to-the-application identifier for a group.
+ */
+void
+GenerateUniqueGroupId(const JSContext* cx, uint64_t uid, uint64_t processId, nsAString& groupId) {
+ uint64_t contextId = reinterpret_cast<uintptr_t>(cx);
+
+ groupId.AssignLiteral("process: ");
+ groupId.AppendInt(processId);
+ groupId.AppendLiteral(", thread: ");
+ groupId.AppendInt(contextId);
+ groupId.AppendLiteral(", group: ");
+ groupId.AppendInt(uid);
+}
+
+static const char* TOPICS[] = {
+ "profile-before-change",
+ "quit-application",
+ "quit-application-granted",
+ "xpcom-will-shutdown"
+};
+
+} // namespace
+
+/* ------------------------------------------------------
+ *
+ * class nsPerformanceObservationTarget
+ *
+ */
+
+
+NS_IMPL_ISUPPORTS(nsPerformanceObservationTarget, nsIPerformanceObservable)
+
+
+
+NS_IMETHODIMP
+nsPerformanceObservationTarget::GetTarget(nsIPerformanceGroupDetails** _result) {
+ if (mDetails) {
+ NS_IF_ADDREF(*_result = mDetails);
+ }
+ return NS_OK;
+};
+
+void
+nsPerformanceObservationTarget::SetTarget(nsPerformanceGroupDetails* details) {
+ MOZ_ASSERT(!mDetails);
+ mDetails = details;
+};
+
+NS_IMETHODIMP
+nsPerformanceObservationTarget::AddJankObserver(nsIPerformanceObserver* observer) {
+ if (!mObservers.append(observer)) {
+ MOZ_CRASH();
+ }
+ return NS_OK;
+};
+
+NS_IMETHODIMP
+nsPerformanceObservationTarget::RemoveJankObserver(nsIPerformanceObserver* observer) {
+ for (auto iter = mObservers.begin(), end = mObservers.end(); iter < end; ++iter) {
+ if (*iter == observer) {
+ mObservers.erase(iter);
+ return NS_OK;
+ }
+ }
+ return NS_OK;
+};
+
+bool
+nsPerformanceObservationTarget::HasObservers() const {
+ return !mObservers.empty();
+}
+
+void
+nsPerformanceObservationTarget::NotifyJankObservers(nsIPerformanceGroupDetails* source, nsIPerformanceAlert* gravity) {
+ // Copy the vector to make sure that it won't change under our feet.
+ mozilla::Vector<nsCOMPtr<nsIPerformanceObserver>> observers;
+ if (!observers.appendAll(mObservers)) {
+ MOZ_CRASH();
+ }
+
+ // Now actually notify.
+ for (auto iter = observers.begin(), end = observers.end(); iter < end; ++iter) {
+ nsCOMPtr<nsIPerformanceObserver> observer = *iter;
+ mozilla::Unused << observer->Observe(source, gravity);
+ }
+}
+
+/* ------------------------------------------------------
+ *
+ * class nsGroupHolder
+ *
+ */
+
+nsPerformanceObservationTarget*
+nsGroupHolder::ObservationTarget() {
+ if (!mPendingObservationTarget) {
+ mPendingObservationTarget = new nsPerformanceObservationTarget();
+ }
+ return mPendingObservationTarget;
+}
+
+nsPerformanceGroup*
+nsGroupHolder::GetGroup() {
+ return mGroup;
+}
+
+void
+nsGroupHolder::SetGroup(nsPerformanceGroup* group) {
+ MOZ_ASSERT(!mGroup);
+ mGroup = group;
+ group->SetObservationTarget(ObservationTarget());
+ mPendingObservationTarget->SetTarget(group->Details());
+}
+
+/* ------------------------------------------------------
+ *
+ * struct PerformanceData
+ *
+ */
+
+PerformanceData::PerformanceData()
+ : mTotalUserTime(0)
+ , mTotalSystemTime(0)
+ , mTotalCPOWTime(0)
+ , mTicks(0)
+{
+ mozilla::PodArrayZero(mDurations);
+}
+
+/* ------------------------------------------------------
+ *
+ * class nsPerformanceGroupDetails
+ *
+ */
+
+NS_IMPL_ISUPPORTS(nsPerformanceGroupDetails, nsIPerformanceGroupDetails)
+
+const nsAString&
+nsPerformanceGroupDetails::Name() const {
+ return mName;
+}
+
+const nsAString&
+nsPerformanceGroupDetails::GroupId() const {
+ return mGroupId;
+}
+
+const nsAString&
+nsPerformanceGroupDetails::AddonId() const {
+ return mAddonId;
+}
+
+uint64_t
+nsPerformanceGroupDetails::WindowId() const {
+ return mWindowId;
+}
+
+uint64_t
+nsPerformanceGroupDetails::ProcessId() const {
+ return mProcessId;
+}
+
+bool
+nsPerformanceGroupDetails::IsSystem() const {
+ return mIsSystem;
+}
+
+bool
+nsPerformanceGroupDetails::IsAddon() const {
+ return mAddonId.Length() != 0;
+}
+
+bool
+nsPerformanceGroupDetails::IsWindow() const {
+ return mWindowId != 0;
+}
+
+bool
+nsPerformanceGroupDetails::IsContentProcess() const {
+ return XRE_GetProcessType() == GeckoProcessType_Content;
+}
+
+/* readonly attribute AString name; */
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetName(nsAString& aName) {
+ aName.Assign(Name());
+ return NS_OK;
+};
+
+/* readonly attribute AString groupId; */
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetGroupId(nsAString& aGroupId) {
+ aGroupId.Assign(GroupId());
+ return NS_OK;
+};
+
+/* readonly attribute AString addonId; */
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetAddonId(nsAString& aAddonId) {
+ aAddonId.Assign(AddonId());
+ return NS_OK;
+};
+
+/* readonly attribute uint64_t windowId; */
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetWindowId(uint64_t *aWindowId) {
+ *aWindowId = WindowId();
+ return NS_OK;
+}
+
+/* readonly attribute bool isSystem; */
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetIsSystem(bool *_retval) {
+ *_retval = IsSystem();
+ return NS_OK;
+}
+
+/*
+ readonly attribute unsigned long long processId;
+*/
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetProcessId(uint64_t* processId) {
+ *processId = ProcessId();
+ return NS_OK;
+}
+
+/* readonly attribute bool IsContentProcess; */
+NS_IMETHODIMP
+nsPerformanceGroupDetails::GetIsContentProcess(bool *_retval) {
+ *_retval = IsContentProcess();
+ return NS_OK;
+}
+
+
+/* ------------------------------------------------------
+ *
+ * class nsPerformanceStats
+ *
+ */
+
+class nsPerformanceStats final: public nsIPerformanceStats
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERFORMANCESTATS
+ NS_FORWARD_NSIPERFORMANCEGROUPDETAILS(mDetails->)
+
+ nsPerformanceStats(nsPerformanceGroupDetails* item,
+ const PerformanceData& aPerformanceData)
+ : mDetails(item)
+ , mPerformanceData(aPerformanceData)
+ {
+ }
+
+
+private:
+ RefPtr<nsPerformanceGroupDetails> mDetails;
+ PerformanceData mPerformanceData;
+
+ ~nsPerformanceStats() {}
+};
+
+NS_IMPL_ISUPPORTS(nsPerformanceStats, nsIPerformanceStats, nsIPerformanceGroupDetails)
+
+/* readonly attribute unsigned long long totalUserTime; */
+NS_IMETHODIMP
+nsPerformanceStats::GetTotalUserTime(uint64_t *aTotalUserTime) {
+ *aTotalUserTime = mPerformanceData.mTotalUserTime;
+ return NS_OK;
+};
+
+/* readonly attribute unsigned long long totalSystemTime; */
+NS_IMETHODIMP
+nsPerformanceStats::GetTotalSystemTime(uint64_t *aTotalSystemTime) {
+ *aTotalSystemTime = mPerformanceData.mTotalSystemTime;
+ return NS_OK;
+};
+
+/* readonly attribute unsigned long long totalCPOWTime; */
+NS_IMETHODIMP
+nsPerformanceStats::GetTotalCPOWTime(uint64_t *aCpowTime) {
+ *aCpowTime = mPerformanceData.mTotalCPOWTime;
+ return NS_OK;
+};
+
+/* readonly attribute unsigned long long ticks; */
+NS_IMETHODIMP
+nsPerformanceStats::GetTicks(uint64_t *aTicks) {
+ *aTicks = mPerformanceData.mTicks;
+ return NS_OK;
+};
+
+/* void getDurations (out unsigned long aCount, [array, size_is (aCount), retval] out unsigned long long aNumberOfOccurrences); */
+NS_IMETHODIMP
+nsPerformanceStats::GetDurations(uint32_t *aCount, uint64_t **aNumberOfOccurrences) {
+ const size_t length = mozilla::ArrayLength(mPerformanceData.mDurations);
+ if (aCount) {
+ *aCount = length;
+ }
+ *aNumberOfOccurrences = new uint64_t[length];
+ for (size_t i = 0; i < length; ++i) {
+ (*aNumberOfOccurrences)[i] = mPerformanceData.mDurations[i];
+ }
+ return NS_OK;
+};
+
+
+/* ------------------------------------------------------
+ *
+ * struct nsPerformanceSnapshot
+ *
+ */
+
+class nsPerformanceSnapshot final : public nsIPerformanceSnapshot
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERFORMANCESNAPSHOT
+
+ nsPerformanceSnapshot() {}
+
+ /**
+ * Append statistics to the list of components data.
+ */
+ void AppendComponentsStats(nsIPerformanceStats* stats);
+
+ /**
+ * Set the statistics attached to process data.
+ */
+ void SetProcessStats(nsIPerformanceStats* group);
+
+private:
+ ~nsPerformanceSnapshot() {}
+
+private:
+ /**
+ * The data for all components.
+ */
+ nsCOMArray<nsIPerformanceStats> mComponentsData;
+
+ /**
+ * The data for the process.
+ */
+ nsCOMPtr<nsIPerformanceStats> mProcessData;
+};
+
+NS_IMPL_ISUPPORTS(nsPerformanceSnapshot, nsIPerformanceSnapshot)
+
+
+/* nsIArray getComponentsData (); */
+NS_IMETHODIMP
+nsPerformanceSnapshot::GetComponentsData(nsIArray * *aComponents)
+{
+ const size_t length = mComponentsData.Length();
+ nsCOMPtr<nsIMutableArray> components = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ for (size_t i = 0; i < length; ++i) {
+ nsCOMPtr<nsIPerformanceStats> stats = mComponentsData[i];
+ mozilla::DebugOnly<nsresult> rv = components->AppendElement(stats, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ components.forget(aComponents);
+ return NS_OK;
+}
+
+/* nsIPerformanceStats getProcessData (); */
+NS_IMETHODIMP
+nsPerformanceSnapshot::GetProcessData(nsIPerformanceStats * *aProcess)
+{
+ NS_IF_ADDREF(*aProcess = mProcessData);
+ return NS_OK;
+}
+
+void
+nsPerformanceSnapshot::AppendComponentsStats(nsIPerformanceStats* stats)
+{
+ mComponentsData.AppendElement(stats);
+}
+
+void
+nsPerformanceSnapshot::SetProcessStats(nsIPerformanceStats* stats)
+{
+ mProcessData = stats;
+}
+
+
+
+/* ------------------------------------------------------
+ *
+ * class PerformanceAlert
+ *
+ */
+class PerformanceAlert final: public nsIPerformanceAlert {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERFORMANCEALERT
+
+ PerformanceAlert(const uint32_t reason, nsPerformanceGroup* source);
+private:
+ ~PerformanceAlert() {}
+
+ const uint32_t mReason;
+
+ // The highest values reached by this group since the latest alert,
+ // in microseconds.
+ const uint64_t mHighestJank;
+ const uint64_t mHighestCPOW;
+};
+
+NS_IMPL_ISUPPORTS(PerformanceAlert, nsIPerformanceAlert);
+
+PerformanceAlert::PerformanceAlert(const uint32_t reason, nsPerformanceGroup* source)
+ : mReason(reason)
+ , mHighestJank(source->HighestRecentJank())
+ , mHighestCPOW(source->HighestRecentCPOW())
+{ }
+
+NS_IMETHODIMP
+PerformanceAlert::GetHighestJank(uint64_t* result) {
+ *result = mHighestJank;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PerformanceAlert::GetHighestCPOW(uint64_t* result) {
+ *result = mHighestCPOW;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PerformanceAlert::GetReason(uint32_t* result) {
+ *result = mReason;
+ return NS_OK;
+}
+/* ------------------------------------------------------
+ *
+ * class PendingAlertsCollector
+ *
+ */
+
+/**
+ * A timer callback in charge of collecting the groups in
+ * `mPendingAlerts` and triggering dispatch of performance alerts.
+ */
+class PendingAlertsCollector final: public nsITimerCallback {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+
+ explicit PendingAlertsCollector(nsPerformanceStatsService* service)
+ : mService(service)
+ , mPending(false)
+ { }
+
+ nsresult Start(uint32_t timerDelayMS);
+ nsresult Dispose();
+
+private:
+ ~PendingAlertsCollector() {}
+
+ RefPtr<nsPerformanceStatsService> mService;
+ bool mPending;
+
+ nsCOMPtr<nsITimer> mTimer;
+
+ mozilla::Vector<uint64_t> mJankLevels;
+};
+
+NS_IMPL_ISUPPORTS(PendingAlertsCollector, nsITimerCallback);
+
+NS_IMETHODIMP
+PendingAlertsCollector::Notify(nsITimer*) {
+ mPending = false;
+ mService->NotifyJankObservers(mJankLevels);
+ return NS_OK;
+}
+
+nsresult
+PendingAlertsCollector::Start(uint32_t timerDelayMS) {
+ if (mPending) {
+ // Collector is already started.
+ return NS_OK;
+ }
+
+ if (!mTimer) {
+ mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ }
+
+ nsresult rv = mTimer->InitWithCallback(this, timerDelayMS, nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mPending = true;
+ {
+ mozilla::DebugOnly<bool> result = nsRefreshDriver::GetJankLevels(mJankLevels);
+ MOZ_ASSERT(result);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+PendingAlertsCollector::Dispose() {
+ if (mTimer) {
+ mozilla::Unused << mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ mService = nullptr;
+ return NS_OK;
+}
+
+
+
+/* ------------------------------------------------------
+ *
+ * class nsPerformanceStatsService
+ *
+ */
+
+NS_IMPL_ISUPPORTS(nsPerformanceStatsService, nsIPerformanceStatsService, nsIObserver)
+
+nsPerformanceStatsService::nsPerformanceStatsService()
+ : mIsAvailable(false)
+ , mDisposed(false)
+#if defined(XP_WIN)
+ , mProcessId(GetCurrentProcessId())
+#else
+ , mProcessId(getpid())
+#endif
+ , mContext(mozilla::dom::danger::GetJSContext())
+ , mUIdCounter(0)
+ , mTopGroup(nsPerformanceGroup::Make(mContext,
+ this,
+ NS_LITERAL_STRING("<process>"), // name
+ NS_LITERAL_STRING(""), // addonid
+ 0, // windowId
+ mProcessId,
+ true, // isSystem
+ nsPerformanceGroup::GroupScope::RUNTIME // scope
+ ))
+ , mIsHandlingUserInput(false)
+ , mProcessStayed(0)
+ , mProcessMoved(0)
+ , mProcessUpdateCounter(0)
+ , mIsMonitoringPerCompartment(false)
+ , mJankAlertThreshold(mozilla::MaxValue<uint64_t>::value) // By default, no alerts
+ , mJankAlertBufferingDelay(1000 /* ms */)
+ , mJankLevelVisibilityThreshold(/* 2 ^ */ 8 /* ms */)
+ , mMaxExpectedDurationOfInteractionUS(150 * 1000)
+{
+ mPendingAlertsCollector = new PendingAlertsCollector(this);
+
+ // Attach some artificial group information to the universal listeners, to aid with debugging.
+ nsString groupIdForAddons;
+ GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForAddons);
+ mUniversalTargets.mAddons->
+ SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING("<universal add-on listener>"),
+ groupIdForAddons,
+ NS_LITERAL_STRING("<universal add-on listener>"),
+ 0, // window id
+ mProcessId,
+ false));
+
+
+ nsString groupIdForWindows;
+ GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForWindows);
+ mUniversalTargets.mWindows->
+ SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING("<universal window listener>"),
+ groupIdForWindows,
+ NS_LITERAL_STRING("<universal window listener>"),
+ 0, // window id
+ mProcessId,
+ false));
+}
+
+nsPerformanceStatsService::~nsPerformanceStatsService()
+{ }
+
+/**
+ * Clean up the service.
+ *
+ * Called during shutdown. Idempotent.
+ */
+void
+nsPerformanceStatsService::Dispose()
+{
+ // Make sure that we do not accidentally destroy `this` while we are
+ // cleaning up back references.
+ RefPtr<nsPerformanceStatsService> kungFuDeathGrip(this);
+ mIsAvailable = false;
+
+ if (mDisposed) {
+ // Make sure that we don't double-dispose.
+ return;
+ }
+ mDisposed = true;
+
+ // Disconnect from nsIObserverService.
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ for (size_t i = 0; i < mozilla::ArrayLength(TOPICS); ++i) {
+ mozilla::Unused << obs->RemoveObserver(this, TOPICS[i]);
+ }
+ }
+
+ // Clear up and disconnect from JSAPI.
+ JSContext* cx = mContext;
+ js::DisposePerformanceMonitoring(cx);
+
+ mozilla::Unused << js::SetStopwatchIsMonitoringCPOW(cx, false);
+ mozilla::Unused << js::SetStopwatchIsMonitoringJank(cx, false);
+
+ mozilla::Unused << js::SetStopwatchStartCallback(cx, nullptr, nullptr);
+ mozilla::Unused << js::SetStopwatchCommitCallback(cx, nullptr, nullptr);
+ mozilla::Unused << js::SetGetPerformanceGroupsCallback(cx, nullptr, nullptr);
+
+ // Clear up and disconnect the alerts collector.
+ if (mPendingAlertsCollector) {
+ mPendingAlertsCollector->Dispose();
+ mPendingAlertsCollector = nullptr;
+ }
+ mPendingAlerts.clear();
+
+ // Disconnect universal observers. Per-group observers will be
+ // disconnected below as part of `group->Dispose()`.
+ mUniversalTargets.mAddons = nullptr;
+ mUniversalTargets.mWindows = nullptr;
+
+ // At this stage, the JS VM may still be holding references to
+ // instances of PerformanceGroup on the stack. To let the service be
+ // collected, we need to break the references from these groups to
+ // `this`.
+ mTopGroup->Dispose();
+ mTopGroup = nullptr;
+
+ // Copy references to the groups to a vector to ensure that we do
+ // not modify the hashtable while iterating it.
+ GroupVector groups;
+ for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) {
+ if (!groups.append(iter.Get()->GetKey())) {
+ MOZ_CRASH();
+ }
+ }
+ for (auto iter = groups.begin(), end = groups.end(); iter < end; ++iter) {
+ RefPtr<nsPerformanceGroup> group = *iter;
+ group->Dispose();
+ }
+
+ // Any remaining references to PerformanceGroup will be released as
+ // the VM unrolls the stack. If there are any nested event loops,
+ // this may take time.
+}
+
+nsresult
+nsPerformanceStatsService::Init()
+{
+ nsresult rv = InitInternal();
+ if (NS_FAILED(rv)) {
+ // Attempt to clean up.
+ Dispose();
+ }
+ return rv;
+}
+
+nsresult
+nsPerformanceStatsService::InitInternal()
+{
+ // Make sure that we release everything during shutdown.
+ // We are a bit defensive here, as we know that some strange behavior can break the
+ // regular shutdown order.
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ for (size_t i = 0; i < mozilla::ArrayLength(TOPICS); ++i) {
+ mozilla::Unused << obs->AddObserver(this, TOPICS[i], false);
+ }
+ }
+
+ // Connect to JSAPI.
+ JSContext* cx = mContext;
+ if (!js::SetStopwatchStartCallback(cx, StopwatchStartCallback, this)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (!js::SetStopwatchCommitCallback(cx, StopwatchCommitCallback, this)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (!js::SetGetPerformanceGroupsCallback(cx, GetPerformanceGroupsCallback, this)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mTopGroup->setIsActive(true);
+ mIsAvailable = true;
+
+ return NS_OK;
+}
+
+// Observe shutdown events.
+NS_IMETHODIMP
+nsPerformanceStatsService::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ MOZ_ASSERT(strcmp(aTopic, "profile-before-change") == 0
+ || strcmp(aTopic, "quit-application") == 0
+ || strcmp(aTopic, "quit-application-granted") == 0
+ || strcmp(aTopic, "xpcom-will-shutdown") == 0);
+
+ Dispose();
+ return NS_OK;
+}
+
+/*static*/ bool
+nsPerformanceStatsService::IsHandlingUserInput() {
+ if (mozilla::EventStateManager::LatestUserInputStart().IsNull()) {
+ return false;
+ }
+ bool result = mozilla::TimeStamp::Now() - mozilla::EventStateManager::LatestUserInputStart() <= mozilla::TimeDuration::FromMicroseconds(mMaxExpectedDurationOfInteractionUS);
+ return result;
+}
+
+/* [implicit_jscontext] attribute bool isMonitoringCPOW; */
+NS_IMETHODIMP
+nsPerformanceStatsService::GetIsMonitoringCPOW(JSContext* cx, bool *aIsStopwatchActive)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ *aIsStopwatchActive = js::GetStopwatchIsMonitoringCPOW(cx);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPerformanceStatsService::SetIsMonitoringCPOW(JSContext* cx, bool aIsStopwatchActive)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!js::SetStopwatchIsMonitoringCPOW(cx, aIsStopwatchActive)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+/* [implicit_jscontext] attribute bool isMonitoringJank; */
+NS_IMETHODIMP
+nsPerformanceStatsService::GetIsMonitoringJank(JSContext* cx, bool *aIsStopwatchActive)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ *aIsStopwatchActive = js::GetStopwatchIsMonitoringJank(cx);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPerformanceStatsService::SetIsMonitoringJank(JSContext* cx, bool aIsStopwatchActive)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!js::SetStopwatchIsMonitoringJank(cx, aIsStopwatchActive)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+/* [implicit_jscontext] attribute bool isMonitoringPerCompartment; */
+NS_IMETHODIMP
+nsPerformanceStatsService::GetIsMonitoringPerCompartment(JSContext*, bool *aIsMonitoringPerCompartment)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ *aIsMonitoringPerCompartment = mIsMonitoringPerCompartment;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsPerformanceStatsService::SetIsMonitoringPerCompartment(JSContext*, bool aIsMonitoringPerCompartment)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aIsMonitoringPerCompartment == mIsMonitoringPerCompartment) {
+ return NS_OK;
+ }
+
+ // Relatively slow update: walk the entire lost of performance groups,
+ // update the active flag of those that have changed.
+ //
+ // Alternative strategies could be envisioned to make the update
+ // much faster, at the expense of the speed of calling `isActive()`,
+ // (e.g. deferring `isActive()` to the nsPerformanceStatsService),
+ // but we expect that `isActive()` can be called thousands of times
+ // per second, while `SetIsMonitoringPerCompartment` is not called
+ // at all during most Firefox runs.
+
+ for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) {
+ RefPtr<nsPerformanceGroup> group = iter.Get()->GetKey();
+ if (group->Scope() == nsPerformanceGroup::GroupScope::COMPARTMENT) {
+ group->setIsActive(aIsMonitoringPerCompartment);
+ }
+ }
+ mIsMonitoringPerCompartment = aIsMonitoringPerCompartment;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::GetJankAlertThreshold(uint64_t* result) {
+ *result = mJankAlertThreshold;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::SetJankAlertThreshold(uint64_t value) {
+ mJankAlertThreshold = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::GetJankAlertBufferingDelay(uint32_t* result) {
+ *result = mJankAlertBufferingDelay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::SetJankAlertBufferingDelay(uint32_t value) {
+ mJankAlertBufferingDelay = value;
+ return NS_OK;
+}
+
+nsresult
+nsPerformanceStatsService::UpdateTelemetry()
+{
+ // Promote everything to floating-point explicitly before dividing.
+ const double processStayed = mProcessStayed;
+ const double processMoved = mProcessMoved;
+
+ if (processStayed <= 0 || processMoved <= 0 || processStayed + processMoved <= 0) {
+ // Overflow/underflow/nothing to report
+ return NS_OK;
+ }
+
+ const double proportion = (100 * processStayed) / (processStayed + processMoved);
+ if (proportion < 0 || proportion > 100) {
+ // Overflow/underflow
+ return NS_OK;
+ }
+
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERF_MONITORING_TEST_CPU_RESCHEDULING_PROPORTION_MOVED, (uint32_t)proportion);
+ return NS_OK;
+}
+
+
+/* static */ nsIPerformanceStats*
+nsPerformanceStatsService::GetStatsForGroup(const js::PerformanceGroup* group)
+{
+ return GetStatsForGroup(nsPerformanceGroup::Get(group));
+}
+
+/* static */ nsIPerformanceStats*
+nsPerformanceStatsService::GetStatsForGroup(const nsPerformanceGroup* group)
+{
+ return new nsPerformanceStats(group->Details(), group->data);
+}
+
+/* [implicit_jscontext] nsIPerformanceSnapshot getSnapshot (); */
+NS_IMETHODIMP
+nsPerformanceStatsService::GetSnapshot(JSContext* cx, nsIPerformanceSnapshot * *aSnapshot)
+{
+ if (!mIsAvailable) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<nsPerformanceSnapshot> snapshot = new nsPerformanceSnapshot();
+ snapshot->SetProcessStats(GetStatsForGroup(mTopGroup));
+
+ for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) {
+ auto* entry = iter.Get();
+ nsPerformanceGroup* group = entry->GetKey();
+ if (group->isActive()) {
+ snapshot->AppendComponentsStats(GetStatsForGroup(group));
+ }
+ }
+
+ js::GetPerfMonitoringTestCpuRescheduling(cx, &mProcessStayed, &mProcessMoved);
+
+ if (++mProcessUpdateCounter % 10 == 0) {
+ mozilla::Unused << UpdateTelemetry();
+ }
+
+ snapshot.forget(aSnapshot);
+
+ return NS_OK;
+}
+
+uint64_t
+nsPerformanceStatsService::GetNextId() {
+ return ++mUIdCounter;
+}
+
+/* static*/ bool
+nsPerformanceStatsService::GetPerformanceGroupsCallback(JSContext* cx,
+ js::PerformanceGroupVector& out,
+ void* closure)
+{
+ RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(closure);
+ return self->GetPerformanceGroups(cx, out);
+}
+
+bool
+nsPerformanceStatsService::GetPerformanceGroups(JSContext* cx,
+ js::PerformanceGroupVector& out)
+{
+ JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
+ if (!global) {
+ // While it is possible for a compartment to have no global
+ // (e.g. atoms), this compartment is not very interesting for us.
+ return true;
+ }
+
+ // All compartments belong to the top group.
+ if (!out.append(mTopGroup)) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+
+ nsAutoString name;
+ CompartmentName(cx, global, name);
+ bool isSystem = nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(global));
+
+ // Find out if the compartment is executed by an add-on. If so, its
+ // duration should count towards the total duration of the add-on.
+ JSAddonId* jsaddonId = AddonIdOfObject(global);
+ nsString addonId;
+ if (jsaddonId) {
+ AssignJSFlatString(addonId, (JSFlatString*)jsaddonId);
+ auto entry = mAddonIdToGroup.PutEntry(addonId);
+ if (!entry->GetGroup()) {
+ nsString addonName = name;
+ addonName.AppendLiteral(" (as addon ");
+ addonName.Append(addonId);
+ addonName.AppendLiteral(")");
+ entry->
+ SetGroup(nsPerformanceGroup::Make(mContext, this,
+ addonName, addonId, 0,
+ mProcessId, isSystem,
+ nsPerformanceGroup::GroupScope::ADDON)
+ );
+ }
+ if (!out.append(entry->GetGroup())) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ // Find out if the compartment is executed by a window. If so, its
+ // duration should count towards the total duration of the window.
+ uint64_t windowId = 0;
+ if (nsCOMPtr<nsPIDOMWindowOuter> ptop = GetPrivateWindow(cx)) {
+ windowId = ptop->WindowID();
+ auto entry = mWindowIdToGroup.PutEntry(windowId);
+ if (!entry->GetGroup()) {
+ nsString windowName = name;
+ windowName.AppendLiteral(" (as window ");
+ windowName.AppendInt(windowId);
+ windowName.AppendLiteral(")");
+ entry->
+ SetGroup(nsPerformanceGroup::Make(mContext, this,
+ windowName, EmptyString(), windowId,
+ mProcessId, isSystem,
+ nsPerformanceGroup::GroupScope::WINDOW)
+ );
+ }
+ if (!out.append(entry->GetGroup())) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+ }
+
+ // All compartments have their own group.
+ auto group =
+ nsPerformanceGroup::Make(mContext, this,
+ name, addonId, windowId,
+ mProcessId, isSystem,
+ nsPerformanceGroup::GroupScope::COMPARTMENT);
+ if (!out.append(group)) {
+ JS_ReportOutOfMemory(cx);
+ return false;
+ }
+
+ return true;
+}
+
+/*static*/ bool
+nsPerformanceStatsService::StopwatchStartCallback(uint64_t iteration, void* closure) {
+ RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(closure);
+ return self->StopwatchStart(iteration);
+}
+
+bool
+nsPerformanceStatsService::StopwatchStart(uint64_t iteration) {
+ mIteration = iteration;
+
+ mIsHandlingUserInput = IsHandlingUserInput();
+ mUserInputCount = mozilla::EventStateManager::UserInputCount();
+
+ nsresult rv = GetResources(&mUserTimeStart, &mSystemTimeStart);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ return true;
+}
+
+/*static*/ bool
+nsPerformanceStatsService::StopwatchCommitCallback(uint64_t iteration,
+ js::PerformanceGroupVector& recentGroups,
+ void* closure)
+{
+ RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(closure);
+ return self->StopwatchCommit(iteration, recentGroups);
+}
+
+bool
+nsPerformanceStatsService::StopwatchCommit(uint64_t iteration,
+ js::PerformanceGroupVector& recentGroups)
+{
+ MOZ_ASSERT(iteration == mIteration);
+ MOZ_ASSERT(!recentGroups.empty());
+
+ uint64_t userTimeStop, systemTimeStop;
+ nsresult rv = GetResources(&userTimeStop, &systemTimeStop);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // `GetResources` is not guaranteed to be monotonic, so round up
+ // any negative result to 0 milliseconds.
+ uint64_t userTimeDelta = 0;
+ if (userTimeStop > mUserTimeStart)
+ userTimeDelta = userTimeStop - mUserTimeStart;
+
+ uint64_t systemTimeDelta = 0;
+ if (systemTimeStop > mSystemTimeStart)
+ systemTimeDelta = systemTimeStop - mSystemTimeStart;
+
+ MOZ_ASSERT(mTopGroup->isUsedInThisIteration());
+ const uint64_t totalRecentCycles = mTopGroup->recentCycles(iteration);
+
+ const bool isHandlingUserInput = mIsHandlingUserInput || mozilla::EventStateManager::UserInputCount() > mUserInputCount;
+
+ // We should only reach this stage if `group` has had some activity.
+ MOZ_ASSERT(mTopGroup->recentTicks(iteration) > 0);
+ for (auto iter = recentGroups.begin(), end = recentGroups.end(); iter != end; ++iter) {
+ RefPtr<nsPerformanceGroup> group = nsPerformanceGroup::Get(*iter);
+ CommitGroup(iteration, userTimeDelta, systemTimeDelta, totalRecentCycles, isHandlingUserInput, group);
+ }
+
+ // Make sure that `group` was treated along with the other items of `recentGroups`.
+ MOZ_ASSERT(!mTopGroup->isUsedInThisIteration());
+ MOZ_ASSERT(mTopGroup->recentTicks(iteration) == 0);
+
+ if (!mPendingAlerts.empty()) {
+ mPendingAlertsCollector->Start(mJankAlertBufferingDelay);
+ }
+
+ return true;
+}
+
+void
+nsPerformanceStatsService::CommitGroup(uint64_t iteration,
+ uint64_t totalUserTimeDelta, uint64_t totalSystemTimeDelta,
+ uint64_t totalCyclesDelta,
+ bool isHandlingUserInput,
+ nsPerformanceGroup* group) {
+
+ MOZ_ASSERT(group->isUsedInThisIteration());
+
+ const uint64_t ticksDelta = group->recentTicks(iteration);
+ const uint64_t cpowTimeDelta = group->recentCPOW(iteration);
+ const uint64_t cyclesDelta = group->recentCycles(iteration);
+ group->resetRecentData();
+
+ // We have now performed all cleanup and may `return` at any time without fear of leaks.
+
+ if (group->iteration() != iteration) {
+ // Stale data, don't commit.
+ return;
+ }
+
+ // When we add a group as changed, we immediately set its
+ // `recentTicks` from 0 to 1. If we have `ticksDelta == 0` at
+ // this stage, we have already called `resetRecentData` but we
+ // haven't removed it from the list.
+ MOZ_ASSERT(ticksDelta != 0);
+ MOZ_ASSERT(cyclesDelta <= totalCyclesDelta);
+ if (cyclesDelta == 0 || totalCyclesDelta == 0) {
+ // Nothing useful, don't commit.
+ return;
+ }
+
+ double proportion = (double)cyclesDelta / (double)totalCyclesDelta;
+ MOZ_ASSERT(proportion <= 1);
+
+ const uint64_t userTimeDelta = proportion * totalUserTimeDelta;
+ const uint64_t systemTimeDelta = proportion * totalSystemTimeDelta;
+
+ group->data.mTotalUserTime += userTimeDelta;
+ group->data.mTotalSystemTime += systemTimeDelta;
+ group->data.mTotalCPOWTime += cpowTimeDelta;
+ group->data.mTicks += ticksDelta;
+
+ const uint64_t totalTimeDelta = userTimeDelta + systemTimeDelta + cpowTimeDelta;
+ uint64_t duration = 1000; // 1ms in µs
+ for (size_t i = 0;
+ i < mozilla::ArrayLength(group->data.mDurations) && duration < totalTimeDelta;
+ ++i, duration *= 2) {
+ group->data.mDurations[i]++;
+ }
+
+ group->RecordJank(totalTimeDelta);
+ group->RecordCPOW(cpowTimeDelta);
+ if (isHandlingUserInput) {
+ group->RecordUserInput();
+ }
+
+ if (totalTimeDelta >= mJankAlertThreshold) {
+ if (!group->HasPendingAlert()) {
+ if (mPendingAlerts.append(group)) {
+ group->SetHasPendingAlert(true);
+ }
+ return;
+ }
+ }
+
+ return;
+}
+
+nsresult
+nsPerformanceStatsService::GetResources(uint64_t* userTime,
+ uint64_t* systemTime) const {
+ MOZ_ASSERT(userTime);
+ MOZ_ASSERT(systemTime);
+
+#if defined(XP_MACOSX)
+ // On MacOS X, to get we per-thread data, we need to
+ // reach into the kernel.
+
+ mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
+ thread_basic_info_data_t info;
+ mach_port_t port = mach_thread_self();
+ kern_return_t err =
+ thread_info(/* [in] targeted thread*/ port,
+ /* [in] nature of information*/ THREAD_BASIC_INFO,
+ /* [out] thread information */ (thread_info_t)&info,
+ /* [inout] number of items */ &count);
+
+ // We do not need ability to communicate with the thread, so
+ // let's release the port.
+ mach_port_deallocate(mach_task_self(), port);
+
+ if (err != KERN_SUCCESS)
+ return NS_ERROR_FAILURE;
+
+ *userTime = info.user_time.microseconds + info.user_time.seconds * 1000000;
+ *systemTime = info.system_time.microseconds + info.system_time.seconds * 1000000;
+
+#elif defined(XP_UNIX)
+ struct rusage rusage;
+#if defined(RUSAGE_THREAD)
+ // Under Linux, we can obtain per-thread statistics
+ int err = getrusage(RUSAGE_THREAD, &rusage);
+#else
+ // Under other Unices, we need to do with more noisy
+ // per-process statistics.
+ int err = getrusage(RUSAGE_SELF, &rusage);
+#endif // defined(RUSAGE_THREAD)
+
+ if (err)
+ return NS_ERROR_FAILURE;
+
+ *userTime = rusage.ru_utime.tv_usec + rusage.ru_utime.tv_sec * 1000000;
+ *systemTime = rusage.ru_stime.tv_usec + rusage.ru_stime.tv_sec * 1000000;
+
+#elif defined(XP_WIN)
+ // Under Windows, we can obtain per-thread statistics. Experience
+ // seems to suggest that they are not very accurate under Windows
+ // XP, though.
+ FILETIME creationFileTime; // Ignored
+ FILETIME exitFileTime; // Ignored
+ FILETIME kernelFileTime;
+ FILETIME userFileTime;
+ BOOL success = GetThreadTimes(GetCurrentThread(),
+ &creationFileTime, &exitFileTime,
+ &kernelFileTime, &userFileTime);
+
+ if (!success)
+ return NS_ERROR_FAILURE;
+
+ ULARGE_INTEGER kernelTimeInt;
+ kernelTimeInt.LowPart = kernelFileTime.dwLowDateTime;
+ kernelTimeInt.HighPart = kernelFileTime.dwHighDateTime;
+ // Convert 100 ns to 1 us.
+ *systemTime = kernelTimeInt.QuadPart / 10;
+
+ ULARGE_INTEGER userTimeInt;
+ userTimeInt.LowPart = userFileTime.dwLowDateTime;
+ userTimeInt.HighPart = userFileTime.dwHighDateTime;
+ // Convert 100 ns to 1 us.
+ *userTime = userTimeInt.QuadPart / 10;
+
+#endif // defined(XP_MACOSX) || defined(XP_UNIX) || defined(XP_WIN)
+
+ return NS_OK;
+}
+
+void
+nsPerformanceStatsService::NotifyJankObservers(const mozilla::Vector<uint64_t>& aPreviousJankLevels) {
+ GroupVector alerts;
+ mPendingAlerts.swap(alerts);
+ if (!mPendingAlertsCollector) {
+ // We are shutting down.
+ return;
+ }
+
+ // Find out if we have noticed any user-noticeable delay in an
+ // animation recently (i.e. since the start of the execution of JS
+ // code that caused this collector to start). If so, we'll mark any
+ // alert as part of a user-noticeable jank. Note that this doesn't
+ // mean with any certainty that the alert is the only cause of jank,
+ // or even the main cause of jank.
+ mozilla::Vector<uint64_t> latestJankLevels;
+ {
+ mozilla::DebugOnly<bool> result = nsRefreshDriver::GetJankLevels(latestJankLevels);
+ MOZ_ASSERT(result);
+ }
+ MOZ_ASSERT(latestJankLevels.length() == aPreviousJankLevels.length());
+
+ bool isJankInAnimation = false;
+ for (size_t i = mJankLevelVisibilityThreshold; i < latestJankLevels.length(); ++i) {
+ if (latestJankLevels[i] > aPreviousJankLevels[i]) {
+ isJankInAnimation = true;
+ break;
+ }
+ }
+
+ MOZ_ASSERT(!alerts.empty());
+ const bool hasUniversalAddonObservers = mUniversalTargets.mAddons->HasObservers();
+ const bool hasUniversalWindowObservers = mUniversalTargets.mWindows->HasObservers();
+ for (auto iter = alerts.begin(); iter < alerts.end(); ++iter) {
+ MOZ_ASSERT(iter);
+ RefPtr<nsPerformanceGroup> group = *iter;
+ group->SetHasPendingAlert(false);
+
+ RefPtr<nsPerformanceGroupDetails> details = group->Details();
+ nsPerformanceObservationTarget* targets[3] = {
+ hasUniversalAddonObservers && details->IsAddon() ? mUniversalTargets.mAddons.get() : nullptr,
+ hasUniversalWindowObservers && details->IsWindow() ? mUniversalTargets.mWindows.get() : nullptr,
+ group->ObservationTarget()
+ };
+
+ bool isJankInInput = group->HasRecentUserInput();
+
+ RefPtr<PerformanceAlert> alert;
+ for (nsPerformanceObservationTarget* target : targets) {
+ if (!target) {
+ continue;
+ }
+ if (!alert) {
+ const uint32_t reason = nsIPerformanceAlert::REASON_SLOWDOWN
+ | (isJankInAnimation ? nsIPerformanceAlert::REASON_JANK_IN_ANIMATION : 0)
+ | (isJankInInput ? nsIPerformanceAlert::REASON_JANK_IN_INPUT : 0);
+ // Wait until we are sure we need to allocate before we allocate.
+ alert = new PerformanceAlert(reason, group);
+ }
+ target->NotifyJankObservers(details, alert);
+ }
+
+ group->ResetRecent();
+ }
+
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::GetObservableAddon(const nsAString& addonId,
+ nsIPerformanceObservable** result) {
+ if (addonId.Equals(NS_LITERAL_STRING("*"))) {
+ NS_IF_ADDREF(*result = mUniversalTargets.mAddons);
+ } else {
+ auto entry = mAddonIdToGroup.PutEntry(addonId);
+ NS_IF_ADDREF(*result = entry->ObservationTarget());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::GetObservableWindow(uint64_t windowId,
+ nsIPerformanceObservable** result) {
+ if (windowId == 0) {
+ NS_IF_ADDREF(*result = mUniversalTargets.mWindows);
+ } else {
+ auto entry = mWindowIdToGroup.PutEntry(windowId);
+ NS_IF_ADDREF(*result = entry->ObservationTarget());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::GetAnimationJankLevelThreshold(short* result) {
+ *result = mJankLevelVisibilityThreshold;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::SetAnimationJankLevelThreshold(short value) {
+ mJankLevelVisibilityThreshold = value;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::GetUserInputDelayThreshold(uint64_t* result) {
+ *result = mMaxExpectedDurationOfInteractionUS;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPerformanceStatsService::SetUserInputDelayThreshold(uint64_t value) {
+ mMaxExpectedDurationOfInteractionUS = value;
+ return NS_OK;
+}
+
+
+
+nsPerformanceStatsService::UniversalTargets::UniversalTargets()
+ : mAddons(new nsPerformanceObservationTarget())
+ , mWindows(new nsPerformanceObservationTarget())
+{ }
+
+/* ------------------------------------------------------
+ *
+ * Class nsPerformanceGroup
+ *
+ */
+
+/*static*/ nsPerformanceGroup*
+nsPerformanceGroup::Make(JSContext* cx,
+ nsPerformanceStatsService* service,
+ const nsAString& name,
+ const nsAString& addonId,
+ uint64_t windowId,
+ uint64_t processId,
+ bool isSystem,
+ GroupScope scope)
+{
+ nsString groupId;
+ ::GenerateUniqueGroupId(cx, service->GetNextId(), processId, groupId);
+ return new nsPerformanceGroup(service, name, groupId, addonId, windowId, processId, isSystem, scope);
+}
+
+nsPerformanceGroup::nsPerformanceGroup(nsPerformanceStatsService* service,
+ const nsAString& name,
+ const nsAString& groupId,
+ const nsAString& addonId,
+ uint64_t windowId,
+ uint64_t processId,
+ bool isSystem,
+ GroupScope scope)
+ : mDetails(new nsPerformanceGroupDetails(name, groupId, addonId, windowId, processId, isSystem))
+ , mService(service)
+ , mScope(scope)
+ , mHighestJank(0)
+ , mHighestCPOW(0)
+ , mHasRecentUserInput(false)
+ , mHasPendingAlert(false)
+{
+ mozilla::Unused << mService->mGroups.PutEntry(this);
+
+#if defined(DEBUG)
+ if (scope == GroupScope::ADDON) {
+ MOZ_ASSERT(mDetails->IsAddon());
+ MOZ_ASSERT(!mDetails->IsWindow());
+ } else if (scope == GroupScope::WINDOW) {
+ MOZ_ASSERT(mDetails->IsWindow());
+ MOZ_ASSERT(!mDetails->IsAddon());
+ } else if (scope == GroupScope::RUNTIME) {
+ MOZ_ASSERT(!mDetails->IsWindow());
+ MOZ_ASSERT(!mDetails->IsAddon());
+ }
+#endif // defined(DEBUG)
+ setIsActive(mScope != GroupScope::COMPARTMENT || mService->mIsMonitoringPerCompartment);
+}
+
+void
+nsPerformanceGroup::Dispose() {
+ if (!mService) {
+ // We have already called `Dispose()`.
+ return;
+ }
+ if (mObservationTarget) {
+ mObservationTarget = nullptr;
+ }
+
+ // Remove any reference to the service.
+ RefPtr<nsPerformanceStatsService> service;
+ service.swap(mService);
+
+ // Remove any dangling pointer to `this`.
+ service->mGroups.RemoveEntry(this);
+
+ if (mScope == GroupScope::ADDON) {
+ MOZ_ASSERT(mDetails->IsAddon());
+ service->mAddonIdToGroup.RemoveEntry(mDetails->AddonId());
+ } else if (mScope == GroupScope::WINDOW) {
+ MOZ_ASSERT(mDetails->IsWindow());
+ service->mWindowIdToGroup.RemoveEntry(mDetails->WindowId());
+ }
+}
+
+nsPerformanceGroup::~nsPerformanceGroup() {
+ Dispose();
+}
+
+nsPerformanceGroup::GroupScope
+nsPerformanceGroup::Scope() const {
+ return mScope;
+}
+
+nsPerformanceGroupDetails*
+nsPerformanceGroup::Details() const {
+ return mDetails;
+}
+
+void
+nsPerformanceGroup::SetObservationTarget(nsPerformanceObservationTarget* target) {
+ MOZ_ASSERT(!mObservationTarget);
+ mObservationTarget = target;
+}
+
+nsPerformanceObservationTarget*
+nsPerformanceGroup::ObservationTarget() const {
+ return mObservationTarget;
+}
+
+bool
+nsPerformanceGroup::HasPendingAlert() const {
+ return mHasPendingAlert;
+}
+
+void
+nsPerformanceGroup::SetHasPendingAlert(bool value) {
+ mHasPendingAlert = value;
+}
+
+
+void
+nsPerformanceGroup::RecordJank(uint64_t jank) {
+ if (jank > mHighestJank) {
+ mHighestJank = jank;
+ }
+}
+
+void
+nsPerformanceGroup::RecordCPOW(uint64_t cpow) {
+ if (cpow > mHighestCPOW) {
+ mHighestCPOW = cpow;
+ }
+}
+
+uint64_t
+nsPerformanceGroup::HighestRecentJank() {
+ return mHighestJank;
+}
+
+uint64_t
+nsPerformanceGroup::HighestRecentCPOW() {
+ return mHighestCPOW;
+}
+
+bool
+nsPerformanceGroup::HasRecentUserInput() {
+ return mHasRecentUserInput;
+}
+
+void
+nsPerformanceGroup::RecordUserInput() {
+ mHasRecentUserInput = true;
+}
+
+void
+nsPerformanceGroup::ResetRecent() {
+ mHighestJank = 0;
+ mHighestCPOW = 0;
+ mHasRecentUserInput = false;
+}
diff --git a/toolkit/components/perfmonitoring/nsPerformanceStats.h b/toolkit/components/perfmonitoring/nsPerformanceStats.h
new file mode 100644
index 0000000000..c82a3e92c7
--- /dev/null
+++ b/toolkit/components/perfmonitoring/nsPerformanceStats.h
@@ -0,0 +1,825 @@
+/* -*- 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/. */
+
+#ifndef nsPerformanceStats_h
+#define nsPerformanceStats_h
+
+#include "jsapi.h"
+
+#include "nsHashKeys.h"
+#include "nsTHashtable.h"
+
+#include "nsIObserver.h"
+#include "nsPIDOMWindow.h"
+
+#include "nsIPerformanceStats.h"
+
+class nsPerformanceGroup;
+class nsPerformanceGroupDetails;
+
+typedef mozilla::Vector<RefPtr<nsPerformanceGroup>> GroupVector;
+
+/**
+ * A data structure for registering observers interested in
+ * performance alerts.
+ *
+ * Each performance group owns a single instance of this class.
+ * Additionally, the service owns instances designed to observe the
+ * performance alerts in all add-ons (respectively webpages).
+ */
+class nsPerformanceObservationTarget final: public nsIPerformanceObservable {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERFORMANCEOBSERVABLE
+
+ /**
+ * `true` if this target has at least once performance observer
+ * registered, `false` otherwise.
+ */
+ bool HasObservers() const;
+
+ /**
+ * Notify all the observers that jank has happened.
+ */
+ void NotifyJankObservers(nsIPerformanceGroupDetails* source, nsIPerformanceAlert* gravity);
+
+ /**
+ * Set the details on the group being observed.
+ */
+ void SetTarget(nsPerformanceGroupDetails* details);
+
+private:
+ ~nsPerformanceObservationTarget() {}
+
+ // The observers for this target. We hold them as a vector, despite
+ // the linear removal cost, as we expect that the typical number of
+ // observers will be lower than 3, and that (un)registrations will
+ // be fairly infrequent.
+ mozilla::Vector<nsCOMPtr<nsIPerformanceObserver>> mObservers;
+
+ // Details on the group being observed. May be `nullptr`.
+ RefPtr<nsPerformanceGroupDetails> mDetails;
+};
+
+/**
+ * The base class for entries of maps from addon id/window id to
+ * performance group.
+ *
+ * Performance observers may be registered before their group is
+ * created (e.g., one may register an observer for an add-on before
+ * all its modules are loaded, or even before the add-on is loaded at
+ * all or for an observer for a webpage before all its iframes are
+ * loaded). This class serves to hold the observation target until the
+ * performance group may be created, and then to associate the
+ * observation target and the performance group.
+ */
+class nsGroupHolder {
+public:
+ nsGroupHolder()
+ : mGroup(nullptr)
+ , mPendingObservationTarget(nullptr)
+ { }
+
+ /**
+ * Get the observation target, creating it if necessary.
+ */
+ nsPerformanceObservationTarget* ObservationTarget();
+
+ /**
+ * Get the group, if it has been created.
+ *
+ * May return `null` if the group hasn't been created yet.
+ */
+ class nsPerformanceGroup* GetGroup();
+
+ /**
+ * Set the group.
+ *
+ * Once this method has been called, calling
+ * `this->ObservationTarget()` and `group->ObservationTarget()` is equivalent.
+ *
+ * Must only be called once.
+ */
+ void SetGroup(class nsPerformanceGroup*);
+private:
+ // The group. Initially `nullptr`, until we have called `SetGroup`.
+ class nsPerformanceGroup* mGroup;
+
+ // The observation target. Instantiated by the first call to
+ // `ObservationTarget()`.
+ RefPtr<nsPerformanceObservationTarget> mPendingObservationTarget;
+};
+
+/**
+ * An implementation of the nsIPerformanceStatsService.
+ *
+ * Note that this implementation is not thread-safe.
+ */
+class nsPerformanceStatsService final : public nsIPerformanceStatsService,
+ public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERFORMANCESTATSSERVICE
+ NS_DECL_NSIOBSERVER
+
+ nsPerformanceStatsService();
+ nsresult Init();
+
+private:
+ nsresult InitInternal();
+ void Dispose();
+ ~nsPerformanceStatsService();
+
+protected:
+ friend nsPerformanceGroup;
+
+ /**
+ * `false` until `Init()` and after `Dispose()`, `true` inbetween.
+ */
+ bool mIsAvailable;
+
+ /**
+ * `true` once we have called `Dispose()`.
+ */
+ bool mDisposed;
+
+ /**
+ * A unique identifier for the process.
+ *
+ * Process HANDLE under Windows, pid under Unix.
+ */
+ const uint64_t mProcessId;
+
+ /**
+ * The JS Context for the main thread.
+ */
+ JSContext* const mContext;
+
+ /**
+ * Generate unique identifiers.
+ */
+ uint64_t GetNextId();
+ uint64_t mUIdCounter;
+
+
+
+ /**
+ * Extract a snapshot of performance statistics from a performance group.
+ */
+ static nsIPerformanceStats* GetStatsForGroup(const js::PerformanceGroup* group);
+ static nsIPerformanceStats* GetStatsForGroup(const nsPerformanceGroup* group);
+
+
+
+ /**
+ * Get the performance groups associated to a given JS compartment.
+ *
+ * A compartment is typically associated to the following groups:
+ * - the top group, shared by the entire process;
+ * - the window group, if the code is executed in a window, shared
+ * by all compartments for that window (typically, all frames);
+ * - the add-on group, if the code is executed as an add-on, shared
+ * by all compartments for that add-on (typically, all modules);
+ * - the compartment's own group.
+ *
+ * Pre-condition: the VM must have entered the JS compartment.
+ *
+ * The caller is expected to cache the results of this method, as
+ * calling it more than once may not return the same instances of
+ * performance groups.
+ */
+ bool GetPerformanceGroups(JSContext* cx, js::PerformanceGroupVector&);
+ static bool GetPerformanceGroupsCallback(JSContext* cx, js::PerformanceGroupVector&, void* closure);
+
+
+
+ /**********************************************************
+ *
+ * Sets of all performance groups, indexed by several keys.
+ *
+ * These sets do not keep the performance groups alive. Rather, a
+ * performance group is inserted in the relevant sets upon
+ * construction and removed from the sets upon destruction or when
+ * we Dispose() of the service.
+ *
+ * A `nsPerformanceGroup` is typically kept alive (as a
+ * `js::PerformanceGroup`) by the JSCompartment to which it is
+ * associated. It may also temporarily be kept alive by the JS
+ * stack, in particular in case of nested event loops.
+ */
+
+ /**
+ * Set of performance groups associated to add-ons, indexed
+ * by add-on id. Each item is shared by all the compartments
+ * that belong to the add-on.
+ */
+ struct AddonIdToGroup: public nsStringHashKey,
+ public nsGroupHolder {
+ explicit AddonIdToGroup(const nsAString* key)
+ : nsStringHashKey(key)
+ { }
+ };
+ nsTHashtable<AddonIdToGroup> mAddonIdToGroup;
+
+ /**
+ * Set of performance groups associated to windows, indexed by outer
+ * window id. Each item is shared by all the compartments that
+ * belong to the window.
+ */
+ struct WindowIdToGroup: public nsUint64HashKey,
+ public nsGroupHolder {
+ explicit WindowIdToGroup(const uint64_t* key)
+ : nsUint64HashKey(key)
+ {}
+ };
+ nsTHashtable<WindowIdToGroup> mWindowIdToGroup;
+
+ /**
+ * Set of all performance groups.
+ */
+ struct Groups: public nsPtrHashKey<nsPerformanceGroup> {
+ explicit Groups(const nsPerformanceGroup* key)
+ : nsPtrHashKey<nsPerformanceGroup>(key)
+ {}
+ };
+ nsTHashtable<Groups> mGroups;
+
+ /**
+ * The performance group representing the runtime itself. All
+ * compartments are associated to this group.
+ */
+ RefPtr<nsPerformanceGroup> mTopGroup;
+
+ /**********************************************************
+ *
+ * Measuring and recording the CPU use of the system.
+ *
+ */
+
+ /**
+ * Get the OS-reported time spent in userland/systemland, in
+ * microseconds. On most platforms, this data is per-thread,
+ * but on some platforms we need to fall back to per-process.
+ *
+ * Data is not guaranteed to be monotonic.
+ */
+ nsresult GetResources(uint64_t* userTime, uint64_t* systemTime) const;
+
+ /**
+ * Amount of user/system CPU time used by the thread (or process,
+ * for platforms that don't support per-thread measure) since start.
+ * Updated by `StopwatchStart` at most once per event.
+ *
+ * Unit: microseconds.
+ */
+ uint64_t mUserTimeStart;
+ uint64_t mSystemTimeStart;
+
+ bool mIsHandlingUserInput;
+
+ /**
+ * The number of user inputs since the start of the process. Used to
+ * determine whether the current iteration has triggered a
+ * (JS-implemented) user input.
+ */
+ uint64_t mUserInputCount;
+
+ /**********************************************************
+ *
+ * Callbacks triggered by the JS VM when execution of JavaScript
+ * code starts/completes.
+ *
+ * As measures of user CPU time/system CPU time have low resolution
+ * (and are somewhat slow), we measure both only during the calls to
+ * `StopwatchStart`/`StopwatchCommit` and we make the assumption
+ * that each group's user/system CPU time is proportional to the
+ * number of clock cycles spent executing code in the group between
+ * `StopwatchStart`/`StopwatchCommit`.
+ *
+ * The results may be skewed by the thread being rescheduled to a
+ * different CPU during the measure, but we expect that on average,
+ * the skew will have limited effects, and will generally tend to
+ * make already-slow executions appear slower.
+ */
+
+ /**
+ * Execution of JavaScript code has started. This may happen several
+ * times in succession if the JavaScript code contains nested event
+ * loops, in which case only the innermost call will receive
+ * `StopwatchCommitCallback`.
+ *
+ * @param iteration The number of times we have started executing
+ * JavaScript code.
+ */
+ static bool StopwatchStartCallback(uint64_t iteration, void* closure);
+ bool StopwatchStart(uint64_t iteration);
+
+ /**
+ * Execution of JavaScript code has reached completion (including
+ * enqueued microtasks). In cse of tested event loops, any ongoing
+ * measurement on outer loops is silently cancelled without any call
+ * to this method.
+ *
+ * @param iteration The number of times we have started executing
+ * JavaScript code.
+ * @param recentGroups The groups that have seen activity during this
+ * event.
+ */
+ static bool StopwatchCommitCallback(uint64_t iteration,
+ js::PerformanceGroupVector& recentGroups,
+ void* closure);
+ bool StopwatchCommit(uint64_t iteration, js::PerformanceGroupVector& recentGroups);
+
+ /**
+ * The number of times we have started executing JavaScript code.
+ */
+ uint64_t mIteration;
+
+ /**
+ * Commit performance measures of a single group.
+ *
+ * Data is transfered from `group->recent*` to `group->data`.
+ *
+ *
+ * @param iteration The current iteration.
+ * @param userTime The total user CPU time for this thread (or
+ * process, if per-thread data is not available) between the
+ * calls to `StopwatchStart` and `StopwatchCommit`.
+ * @param systemTime The total system CPU time for this thread (or
+ * process, if per-thread data is not available) between the
+ * calls to `StopwatchStart` and `StopwatchCommit`.
+ * @param cycles The total number of cycles for this thread
+ * between the calls to `StopwatchStart` and `StopwatchCommit`.
+ * @param isJankVisible If `true`, expect that the user will notice
+ * any slowdown.
+ * @param group The group containing the data to commit.
+ */
+ void CommitGroup(uint64_t iteration,
+ uint64_t userTime, uint64_t systemTime, uint64_t cycles,
+ bool isJankVisible,
+ nsPerformanceGroup* group);
+
+
+
+
+ /**********************************************************
+ *
+ * To check whether our algorithm makes sense, we keep count of the
+ * number of times the process has been rescheduled to another CPU
+ * while we were monitoring the performance of a group and we upload
+ * this data through Telemetry.
+ */
+ nsresult UpdateTelemetry();
+
+ uint64_t mProcessStayed;
+ uint64_t mProcessMoved;
+ uint32_t mProcessUpdateCounter;
+
+ /**********************************************************
+ *
+ * Options controlling measurements.
+ */
+
+ /**
+ * Determine if we are measuring the performance of every individual
+ * compartment (in particular, every individual module, frame,
+ * sandbox). Note that this makes measurements noticeably slower.
+ */
+ bool mIsMonitoringPerCompartment;
+
+
+ /**********************************************************
+ *
+ * Determining whether jank is user-visible.
+ */
+
+ /**
+ * `true` if we believe that any slowdown can cause a noticeable
+ * delay in handling user-input.
+ *
+ * In the current implementation, we return `true` if the latest
+ * user input was less than MAX_DURATION_OF_INTERACTION_MS ago. This
+ * includes all inputs (mouse, keyboard, other devices), with the
+ * exception of mousemove.
+ */
+ bool IsHandlingUserInput();
+
+
+public:
+ /**********************************************************
+ *
+ * Letting observers register themselves to watch for performance
+ * alerts.
+ *
+ * To avoid saturating clients with alerts (or even creating loops
+ * of alerts), each alert is buffered. At the end of each iteration
+ * of the event loop, groups that have caused performance alerts
+ * are registered in a set of pending alerts, and the collection
+ * timer hasn't been started yet, it is started. Once the timer
+ * firers, we gather all the pending alerts, empty the set and
+ * dispatch to observers.
+ */
+
+ /**
+ * Clear the set of pending alerts and dispatch the pending alerts
+ * to observers.
+ */
+ void NotifyJankObservers(const mozilla::Vector<uint64_t>& previousJankLevels);
+
+private:
+ /**
+ * The set of groups for which we know that an alert should be
+ * raised. This set is cleared once `mPendingAlertsCollector`
+ * fires.
+ *
+ * Invariant: no group may appear twice in this vector.
+ */
+ GroupVector mPendingAlerts;
+
+ /**
+ * A timer callback in charge of collecting the groups in
+ * `mPendingAlerts` and triggering `NotifyJankObservers` to dispatch
+ * performance alerts.
+ */
+ RefPtr<class PendingAlertsCollector> mPendingAlertsCollector;
+
+
+ /**
+ * Observation targets that are not attached to a specific group.
+ */
+ struct UniversalTargets {
+ UniversalTargets();
+ /**
+ * A target for observers interested in watching all addons.
+ */
+ RefPtr<nsPerformanceObservationTarget> mAddons;
+
+ /**
+ * A target for observers interested in watching all windows.
+ */
+ RefPtr<nsPerformanceObservationTarget> mWindows;
+ };
+ UniversalTargets mUniversalTargets;
+
+ /**
+ * The threshold, in microseconds, above which a performance group is
+ * considered "slow" and should raise performance alerts.
+ */
+ uint64_t mJankAlertThreshold;
+
+ /**
+ * A buffering delay, in milliseconds, used by the service to
+ * regroup performance alerts, before observers are actually
+ * noticed. Higher delays let the system avoid redundant
+ * notifications for the same group, and are generally better for
+ * performance.
+ */
+ uint32_t mJankAlertBufferingDelay;
+
+ /**
+ * The threshold above which jank, as reported by the refresh drivers,
+ * is considered user-visible.
+ *
+ * A value of n means that any jank above 2^n ms will be considered
+ * user visible.
+ */
+ short mJankLevelVisibilityThreshold;
+
+ /**
+ * The number of microseconds during which we assume that a
+ * user-interaction can keep the code jank-critical. Any user
+ * interaction that lasts longer than this duration is expected to
+ * either have already caused jank or have caused a nested event
+ * loop.
+ *
+ * In either case, we consider that monitoring
+ * jank-during-interaction after this duration is useless.
+ */
+ uint64_t mMaxExpectedDurationOfInteractionUS;
+};
+
+
+
+/**
+ * Container for performance data.
+ *
+ * All values are monotonic.
+ *
+ * All values are updated after running to completion.
+ */
+struct PerformanceData {
+ /**
+ * Number of times we have spent at least 2^n consecutive
+ * milliseconds executing code in this group.
+ * durations[0] is increased whenever we spend at least 1 ms
+ * executing code in this group
+ * durations[1] whenever we spend 2ms+
+ * ...
+ * durations[i] whenever we spend 2^ims+
+ */
+ uint64_t mDurations[10];
+
+ /**
+ * Total amount of time spent executing code in this group, in
+ * microseconds.
+ */
+ uint64_t mTotalUserTime;
+ uint64_t mTotalSystemTime;
+ uint64_t mTotalCPOWTime;
+
+ /**
+ * Total number of times code execution entered this group, since
+ * process launch. This may be greater than the number of times we
+ * have entered the event loop.
+ */
+ uint64_t mTicks;
+
+ PerformanceData();
+ PerformanceData(const PerformanceData& from) = default;
+ PerformanceData& operator=(const PerformanceData& from) = default;
+};
+
+
+
+/**
+ * Identification information for an item that can hold performance
+ * data.
+ */
+class nsPerformanceGroupDetails final: public nsIPerformanceGroupDetails {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPERFORMANCEGROUPDETAILS
+
+ nsPerformanceGroupDetails(const nsAString& aName,
+ const nsAString& aGroupId,
+ const nsAString& aAddonId,
+ const uint64_t aWindowId,
+ const uint64_t aProcessId,
+ const bool aIsSystem)
+ : mName(aName)
+ , mGroupId(aGroupId)
+ , mAddonId(aAddonId)
+ , mWindowId(aWindowId)
+ , mProcessId(aProcessId)
+ , mIsSystem(aIsSystem)
+ { }
+public:
+ const nsAString& Name() const;
+ const nsAString& GroupId() const;
+ const nsAString& AddonId() const;
+ uint64_t WindowId() const;
+ uint64_t ProcessId() const;
+ bool IsAddon() const;
+ bool IsWindow() const;
+ bool IsSystem() const;
+ bool IsContentProcess() const;
+private:
+ ~nsPerformanceGroupDetails() {}
+
+ const nsString mName;
+ const nsString mGroupId;
+ const nsString mAddonId;
+ const uint64_t mWindowId;
+ const uint64_t mProcessId;
+ const bool mIsSystem;
+};
+
+/**
+ * The kind of compartments represented by this group.
+ */
+enum class PerformanceGroupScope {
+ /**
+ * This group represents the entire runtime (i.e. the thread).
+ */
+ RUNTIME,
+
+ /**
+ * This group represents all the compartments executed in a window.
+ */
+ WINDOW,
+
+ /**
+ * This group represents all the compartments provided by an addon.
+ */
+ ADDON,
+
+ /**
+ * This group represents a single compartment.
+ */
+ COMPARTMENT,
+};
+
+/**
+ * A concrete implementation of `js::PerformanceGroup`, also holding
+ * performance data. Instances may represent individual compartments,
+ * windows, addons or the entire runtime.
+ *
+ * This class is intended to be the sole implementation of
+ * `js::PerformanceGroup`.
+ */
+class nsPerformanceGroup final: public js::PerformanceGroup {
+public:
+
+ // Ideally, we would define the enum class in nsPerformanceGroup,
+ // but this seems to choke some versions of gcc.
+ typedef PerformanceGroupScope GroupScope;
+
+ /**
+ * Construct a performance group.
+ *
+ * @param cx The container context. Used to generate a unique identifier.
+ * @param service The performance service. Used during destruction to
+ * cleanup the hash tables.
+ * @param name A name for the group, designed mostly for debugging purposes,
+ * so it should be at least somewhat human-readable.
+ * @param addonId The identifier of the add-on. Should be "" when the
+ * group is not part of an add-on,
+ * @param windowId The identifier of the window. Should be 0 when the
+ * group is not part of a window.
+ * @param processId A unique identifier for the process.
+ * @param isSystem `true` if the code of the group is executed with
+ * system credentials, `false` otherwise.
+ * @param scope the scope of this group.
+ */
+ static nsPerformanceGroup*
+ Make(JSContext* cx,
+ nsPerformanceStatsService* service,
+ const nsAString& name,
+ const nsAString& addonId,
+ uint64_t windowId,
+ uint64_t processId,
+ bool isSystem,
+ GroupScope scope);
+
+ /**
+ * Utility: type-safer conversion from js::PerformanceGroup to nsPerformanceGroup.
+ */
+ static inline nsPerformanceGroup* Get(js::PerformanceGroup* self) {
+ return static_cast<nsPerformanceGroup*>(self);
+ }
+ static inline const nsPerformanceGroup* Get(const js::PerformanceGroup* self) {
+ return static_cast<const nsPerformanceGroup*>(self);
+ }
+
+ /**
+ * The performance data committed to this group.
+ */
+ PerformanceData data;
+
+ /**
+ * The scope of this group. Used to determine whether the group
+ * should be (de)activated.
+ */
+ GroupScope Scope() const;
+
+ /**
+ * Identification details for this group.
+ */
+ nsPerformanceGroupDetails* Details() const;
+
+ /**
+ * Cleanup any references.
+ */
+ void Dispose();
+
+ /**
+ * Set the observation target for this group.
+ *
+ * This method must be called exactly once, when the performance
+ * group is attached to its `nsGroupHolder`.
+ */
+ void SetObservationTarget(nsPerformanceObservationTarget*);
+
+
+ /**
+ * `true` if we have already noticed that a performance alert should
+ * be raised for this group but we have not dispatched it yet,
+ * `false` otherwise.
+ */
+ bool HasPendingAlert() const;
+ void SetHasPendingAlert(bool value);
+
+protected:
+ nsPerformanceGroup(nsPerformanceStatsService* service,
+ const nsAString& name,
+ const nsAString& groupId,
+ const nsAString& addonId,
+ uint64_t windowId,
+ uint64_t processId,
+ bool isSystem,
+ GroupScope scope);
+
+
+ /**
+ * Virtual implementation of `delete`, to make sure that objects are
+ * destoyed with an implementation of `delete` compatible with the
+ * implementation of `new` used to allocate them.
+ *
+ * Called by SpiderMonkey.
+ */
+ virtual void Delete() override {
+ delete this;
+ }
+ ~nsPerformanceGroup();
+
+private:
+ /**
+ * Identification details for this group.
+ */
+ RefPtr<nsPerformanceGroupDetails> mDetails;
+
+ /**
+ * The stats service. Used to perform cleanup during destruction.
+ */
+ RefPtr<nsPerformanceStatsService> mService;
+
+ /**
+ * The scope of this group. Used to determine whether the group
+ * should be (de)activated.
+ */
+ const GroupScope mScope;
+
+// Observing performance alerts.
+
+public:
+ /**
+ * The observation target, used to register observers.
+ */
+ nsPerformanceObservationTarget* ObservationTarget() const;
+
+ /**
+ * Record a jank duration.
+ *
+ * Update the highest recent jank if necessary.
+ */
+ void RecordJank(uint64_t jank);
+ uint64_t HighestRecentJank();
+
+ /**
+ * Record a CPOW duration.
+ *
+ * Update the highest recent CPOW if necessary.
+ */
+ void RecordCPOW(uint64_t cpow);
+ uint64_t HighestRecentCPOW();
+
+ /**
+ * Record that this group has recently been involved in handling
+ * user input. Note that heuristics are involved here, so the
+ * result is not 100% accurate.
+ */
+ void RecordUserInput();
+ bool HasRecentUserInput();
+
+ /**
+ * Reset recent values (recent highest CPOW and jank, involvement in
+ * user input).
+ */
+ void ResetRecent();
+private:
+ /**
+ * The target used by observers to register for watching slow
+ * performance alerts caused by this group.
+ *
+ * May be nullptr for groups that cannot be watched (the top group).
+ */
+ RefPtr<class nsPerformanceObservationTarget> mObservationTarget;
+
+ /**
+ * The highest jank encountered since jank observers for this group
+ * were last called, in microseconds.
+ */
+ uint64_t mHighestJank;
+
+ /**
+ * The highest CPOW encountered since jank observers for this group
+ * were last called, in microseconds.
+ */
+ uint64_t mHighestCPOW;
+
+ /**
+ * `true` if this group has been involved in handling user input,
+ * `false` otherwise.
+ *
+ * Note that we use heuristics to determine whether a group is
+ * involved in handling user input, so this value is not 100%
+ * accurate.
+ */
+ bool mHasRecentUserInput;
+
+ /**
+ * `true` if this group has caused a performance alert and this alert
+ * hasn't been dispatched yet.
+ *
+ * We use this as part of the buffering of performance alerts. If
+ * the group generates several alerts several times during the
+ * buffering delay, we only wish to add the group once to the list
+ * of alerts.
+ */
+ bool mHasPendingAlert;
+};
+
+#endif
diff --git a/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js b/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser.ini b/toolkit/components/perfmonitoring/tests/browser/browser.ini
new file mode 100644
index 0000000000..7f4ac8514c
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+head = head.js
+tags = addons
+support-files =
+ browser_Addons_sample.xpi
+ browser_compartments.html
+ browser_compartments_frame.html
+ browser_compartments_script.js
+
+[browser_AddonWatcher.js]
+[browser_compartments.js]
+skip-if = os == "linux" && !debug && e10s # Bug 1230018
+[browser_addonPerformanceAlerts.js]
+[browser_addonPerformanceAlerts_2.js]
+[browser_webpagePerformanceAlerts.js]
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js b/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js
new file mode 100644
index 0000000000..b4e80faa7a
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_AddonWatcher.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests for AddonWatcher.jsm
+
+"use strict";
+
+requestLongerTimeout(2);
+
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/AddonManager.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+const ADDON_URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi";
+const ADDON_ID = "addonwatcher-test@mozilla.com";
+
+add_task(function* init() {
+ info("Installing test add-on");
+ let installer = yield new Promise(resolve => AddonManager.getInstallForURL(ADDON_URL, resolve, "application/x-xpinstall"));
+ if (installer.error) {
+ throw installer.error;
+ }
+ let installed = new Promise((resolve, reject) => installer.addListener({
+ onInstallEnded: (_, addon) => resolve(addon),
+ onInstallFailed: reject,
+ onDownloadFailed: reject
+ }));
+
+ // We also need to wait for the add-on to report that it's ready
+ // to be used in the test.
+ let ready = TestUtils.topicObserved("test-addonwatcher-ready");
+ installer.install();
+
+ info("Waiting for installation to terminate");
+ let addon = yield installed;
+
+ yield ready;
+
+ registerCleanupFunction(() => {
+ info("Uninstalling test add-on");
+ addon.uninstall()
+ });
+
+ Preferences.set("browser.addon-watch.warmup-ms", 0);
+ Preferences.set("browser.addon-watch.freeze-threshold-micros", 0);
+ Preferences.set("browser.addon-watch.jank-threshold-micros", 0);
+ Preferences.set("browser.addon-watch.occurrences-between-alerts", 0);
+ Preferences.set("browser.addon-watch.delay-between-alerts-ms", 0);
+ Preferences.set("browser.addon-watch.delay-between-freeze-alerts-ms", 0);
+ Preferences.set("browser.addon-watch.max-simultaneous-reports", 10000);
+ Preferences.set("browser.addon-watch.deactivate-after-idle-ms", 100000000);
+ registerCleanupFunction(() => {
+ for (let k of [
+ "browser.addon-watch.warmup-ms",
+ "browser.addon-watch.freeze-threshold-micros",
+ "browser.addon-watch.jank-threshold-micros",
+ "browser.addon-watch.occurrences-between-alerts",
+ "browser.addon-watch.delay-between-alerts-ms",
+ "browser.addon-watch.delay-between-freeze-alerts-ms",
+ "browser.addon-watch.max-simultaneous-reports",
+ "browser.addon-watch.deactivate-after-idle-ms"
+ ]) {
+ Preferences.reset(k);
+ }
+ });
+
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ AddonWatcher.init();
+
+ registerCleanupFunction(function () {
+ AddonWatcher.paused = true;
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+});
+
+// Utility function to burn some resource, trigger a reaction of the add-on watcher
+// and check both its notification and telemetry.
+let burn_rubber = Task.async(function*({histogramName, topic, expectedMinSum}) {
+ let detected = false;
+ let observer = (_, topic, id) => {
+ Assert.equal(id, ADDON_ID, "The add-on watcher has detected the misbehaving addon");
+ detected = true;
+ };
+
+ try {
+ info("Preparing add-on watcher");
+
+ Services.obs.addObserver(observer, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false);
+
+ let histogram = Services.telemetry.getKeyedHistogramById(histogramName);
+ histogram.clear();
+ let snap1 = histogram.snapshot(ADDON_ID);
+ Assert.equal(snap1.sum, 0, `Histogram ${histogramName} is initially empty for the add-on`);
+
+ let histogramUpdated = false;
+ do {
+ info(`Burning some CPU with ${topic}. This should cause an add-on watcher notification`);
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ Services.obs.notifyObservers(null, topic, "");
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ let snap2 = histogram.snapshot(ADDON_ID);
+ histogramUpdated = snap2.sum > 0;
+ info(`For the moment, histogram ${histogramName} shows ${snap2.sum} => ${histogramUpdated}`);
+ info(`For the moment, we have ${detected?"":"NOT "}detected the slow add-on`);
+ } while (!histogramUpdated || !detected);
+
+ let snap3 = histogram.snapshot(ADDON_ID);
+ Assert.ok(snap3.sum >= expectedMinSum, `Histogram ${histogramName} recorded a gravity of ${snap3.sum}, expecting at least ${expectedMinSum}.`);
+ } finally {
+ Services.obs.removeObserver(observer, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED);
+ }
+});
+
+// Test that burning CPU will cause the add-on watcher to notice that
+// the add-on is misbehaving.
+add_task(function* test_burn_CPU() {
+ yield burn_rubber({
+ histogramName: "PERF_MONITORING_SLOW_ADDON_JANK_US",
+ topic: "test-addonwatcher-burn-some-cpu",
+ expectedMinSum: 7,
+ });
+});
+
+// Test that burning content CPU will cause the add-on watcher to notice that
+// the add-on is misbehaving.
+/*
+Blocked by bug 1227283.
+add_task(function* test_burn_content_CPU() {
+ yield burn_rubber({
+ histogramName: "PERF_MONITORING_SLOW_ADDON_JANK_US",
+ topic: "test-addonwatcher-burn-some-content-cpu",
+ expectedMinSum: 7,
+ });
+});
+*/
+
+// Test that burning CPOW will cause the add-on watcher to notice that
+// the add-on is misbehaving.
+add_task(function* test_burn_CPOW() {
+ if (!gMultiProcessBrowser) {
+ info("This is a single-process Firefox, we can't test for CPOW");
+ return;
+ }
+ yield burn_rubber({
+ histogramName: "PERF_MONITORING_SLOW_ADDON_CPOW_US",
+ topic: "test-addonwatcher-burn-some-cpow",
+ expectedMinSum: 400,
+ });
+});
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi
new file mode 100644
index 0000000000..ae5bcc5ff3
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample.xpi
Binary files differ
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js
new file mode 100644
index 0000000000..9a55758279
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/bootstrap.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Sample for browser_AddonWatcher.js
+
+"use strict";
+
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+const TOPIC_BURNCPU = "test-addonwatcher-burn-some-cpu";
+const TOPIC_BURNCPOW = "test-addonwatcher-burn-some-cpow";
+const TOPIC_BURNCONTENTCPU = "test-addonwatcher-burn-some-content-cpu";
+const TOPIC_READY = "test-addonwatcher-ready";
+
+const MESSAGE_BURNCPOW = "test-addonwatcher-cpow:init";
+const URL_FRAMESCRIPT = "chrome://addonwatcher-test/content/framescript.js";
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+const { setTimeout } = Cu.import("resource://gre/modules/Timer.jsm", {});
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageListenerManager);
+
+/**
+ * Spend some time using CPU.
+ */
+function burnCPU() {
+ let ignored = [];
+ let start = Date.now();
+ let i = 0;
+ while (Date.now() - start < 1000) {
+ ignored[i++ % 2] = i;
+ }
+}
+
+/**
+ * Spend some time in CPOW.
+ */
+function burnCPOW() {
+ gBurnCPOW();
+}
+let gBurnCPOW = null;
+
+function burnContentCPU() {
+ setTimeout(() => { try {
+ gBurnContentCPU()
+ } catch (ex) {
+ dump(`test-addon error: ${ex}\n`);
+ } }, 0);
+}
+let gBurnContentCPU = null;
+
+let gTab = null;
+let gTabBrowser = null;
+
+function startup() {
+ Services.obs.addObserver(burnCPU, TOPIC_BURNCPU, false);
+ Services.obs.addObserver(burnCPOW, TOPIC_BURNCPOW, false);
+ Services.obs.addObserver(burnContentCPU, TOPIC_BURNCONTENTCPU, false);
+
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ let win = windows.getNext();
+ gTabBrowser = win.gBrowser;
+ gTab = gTabBrowser.addTab("about:robots");
+ gBurnContentCPU = function() {
+ gTab.linkedBrowser.messageManager.sendAsyncMessage("test-addonwatcher-burn-some-content-cpu", {});
+ }
+
+ gTab.linkedBrowser.messageManager.loadFrameScript(URL_FRAMESCRIPT, false);
+ globalMM.loadFrameScript(URL_FRAMESCRIPT, false);
+
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ // This profile has e10s enabled, which means we'll want to
+ // test CPOW traffic.
+ globalMM.addMessageListener("test-addonwatcher-cpow:init", function waitForCPOW(msg) {
+ if (Components.utils.isCrossProcessWrapper(msg.objects.burnCPOW)) {
+ gBurnCPOW = msg.objects.burnCPOW;
+ globalMM.removeMessageListener("test-addonwatcher-cpow:init", waitForCPOW);
+ Services.obs.notifyObservers(null, TOPIC_READY, null);
+ } else {
+ Cu.reportError("test-addonwatcher-cpow:init didn't give us a CPOW! Expect timeouts.");
+ }
+ });
+ } else {
+ // e10s is not enabled, so a CPOW is not necessary - we can report ready
+ // right away.
+ Services.obs.notifyObservers(null, TOPIC_READY, null);
+ }
+}
+
+function shutdown() {
+ Services.obs.removeObserver(burnCPU, TOPIC_BURNCPU);
+ Services.obs.removeObserver(burnCPOW, TOPIC_BURNCPOW);
+ Services.obs.removeObserver(burnContentCPU, TOPIC_BURNCONTENTCPU);
+ gTabBrowser.removeTab(gTab);
+}
+
+function install() {
+ // Nothing to do
+}
+
+function uninstall() {
+ // Nothing to do
+}
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh
new file mode 100644
index 0000000000..28d52ea3a8
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/build.sh
@@ -0,0 +1,4 @@
+echo "Rebuilding browser_Addons_sample.xpi..."
+zip -r ../browser_Addons_sample.xpi .
+echo "
+Done! Don't forget to sign it: https://wiki.mozilla.org/EngineeringProductivity/HowTo/SignExtensions" \ No newline at end of file
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest
new file mode 100644
index 0000000000..9f53da861b
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/chrome.manifest
@@ -0,0 +1 @@
+content addonwatcher-test content/
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js
new file mode 100644
index 0000000000..e7ebc2a616
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/content/framescript.js
@@ -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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function burnCPOW(msg) {
+ dump(`Addon: content burnCPU start ${Math.sin(Math.random())}\n`);
+ let start = content.performance.now();
+ let ignored = [];
+ while (content.performance.now() - start < 5000) {
+ ignored[ignored.length % 2] = ignored.length;
+ }
+ dump(`Addon: content burnCPU done: ${content.performance.now() - start}\n`);
+}
+
+if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ sendAsyncMessage("test-addonwatcher-cpow:init", {}, {burnCPOW});
+}
+
+addMessageListener("test-addonwatcher-burn-some-content-cpu", burnCPOW);
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf
new file mode 100644
index 0000000000..cae10ace66
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_Addons_sample/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>addonwatcher-test@mozilla.com</em:id>
+ <em:version>1.1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:name>Sample for browser_AddonWatcher.js</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js
new file mode 100644
index 0000000000..af78f00745
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js
@@ -0,0 +1,91 @@
+"use strict";
+
+/**
+ * Tests for PerformanceWatcher watching slow addons.
+ */
+
+add_task(function* init() {
+ // Get rid of buffering.
+ let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].getService(
+ Ci.nsIPerformanceStatsService);
+ let oldDelay = service.jankAlertBufferingDelay;
+
+ service.jankAlertBufferingDelay = 0 /* ms */;
+ registerCleanupFunction(() => {
+ info("Cleanup");
+ service.jankAlertBufferingDelay = oldDelay;
+ });
+});
+
+add_task(function* test_install_addon_then_watch_it() {
+ for (let topic of ["burnCPU", "promiseBurnContentCPU", "promiseBurnCPOW"]) {
+ info(`Starting subtest ${topic}`);
+ info("Spawning fake add-on, making sure that the compartment is initialized");
+ let addon = new AddonBurner();
+ yield addon.promiseInitialized;
+ addon.burnCPU();
+
+ info(`Check that burning CPU triggers the real listener, but not the fake listener ${topic}`);
+ let realListener = new AddonListener(addon.addonId, (group, details) => {
+ if (group.addonId == addon.addonId) {
+ return details.highestJank;
+ }
+ throw new Error(`I shouldn't have been called with addon ${group.addonId}`);
+ });
+ let fakeListener = new AddonListener(addon.addonId + "-fake-" + Math.random(), group => true); // This listener should never be triggered.
+ let universalListener = new AddonListener("*", alerts => {
+ info(`AddonListener: received alerts ${JSON.stringify(alerts)}`);
+ let alert = alerts.find(({source}) => {
+ return source.addonId == addon.addonId;
+ });
+ if (alert) {
+ info(`AddonListener: I found an alert for ${addon.addonId}`);
+ return alert.details.highestJank;
+ }
+ info(`AddonListener: I didn't find any alert for ${addon.addonId}`);
+ return null;
+ });
+
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield addon.run(topic, 10, realListener);
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ Assert.ok(realListener.triggered, `1. The real listener was triggered ${topic}`);
+ Assert.ok(universalListener.triggered, `1. The universal listener was triggered ${topic}`);
+ Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered ${topic}`);
+ Assert.ok(realListener.result >= addon.jankThreshold, `1. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result/1000}ms) ${topic}`);
+
+ info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener ${topic}`);
+ Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {}));
+ Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener));
+
+ yield addon.run(topic, 10, realListener);
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 300));
+
+ Assert.ok(realListener.triggered, `2. The real listener was triggered ${topic}`);
+ Assert.ok(universalListener.triggered, `2. The universal listener was triggered ${topic}`);
+ Assert.ok(!fakeListener.triggered, `2. The fake listener was not triggered ${topic}`);
+ Assert.ok(realListener.result >= 200000, `2. jank is at least 300ms (${realListener.result/1000}ms) ${topic}`);
+
+ info(`Attempting to remove correctly, check if the listener is still triggered ${topic}`);
+ realListener.unregister();
+ yield addon.run(topic, 3, realListener);
+ Assert.ok(!realListener.triggered, `3. After being unregistered, the real listener was not triggered ${topic}`);
+ Assert.ok(universalListener.triggered, `3. The universal listener is still triggered ${topic}`);
+
+ info("Unregistering universal listener");
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ universalListener.unregister();
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield addon.run(topic, 3, realListener);
+ Assert.ok(!universalListener.triggered, `4. After being unregistered, the universal listener is not triggered ${topic}`);
+
+ fakeListener.unregister();
+ addon.dispose();
+ }
+});
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js
new file mode 100644
index 0000000000..d39c38b1f6
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js
@@ -0,0 +1,25 @@
+"use strict";
+
+/**
+ * Tests for PerformanceWatcher watching slow addons.
+ */
+
+add_task(function* test_watch_addon_then_install_it() {
+ for (let topic of ["burnCPU", "promiseBurnContentCPU", "promiseBurnCPOW"]) {
+ let addonId = "addon:test_watch_addons_before_installing" + Math.random();
+ let realListener = new AddonListener(addonId, (group, details) => {
+ if (group.addonId == addonId) {
+ return details.highestJank;
+ }
+ throw new Error(`I shouldn't have been called with addon ${group.addonId}`);
+ });
+
+ info("Now install the add-on, *after* having installed the listener");
+ let addon = new AddonBurner(addonId);
+
+ Assert.ok((yield addon.run(topic, 10, realListener)), `5. The real listener was triggered ${topic}`);
+ Assert.ok(realListener.result >= addon.jankThreshold, `5. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result}µs) ${topic}`);
+ realListener.unregister();
+ addon.dispose();
+ }
+});
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html
new file mode 100644
index 0000000000..d7ee6c4189
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>
+ Main frame for test browser_compartments.js
+ </title>
+</head>
+<body>
+Main frame.
+
+<iframe src="browser_compartments_frame.html?frame=1">
+ Subframe 1
+</iframe>
+
+<iframe src="browser_compartments_frame.html?frame=2">
+ Subframe 2.
+</iframe>
+
+</body>
+</html>
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
new file mode 100644
index 0000000000..f04fefb339
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that we see jank that takes place in a webpage,
+ * and that jank from several iframes are actually charged
+ * to the top window.
+ */
+Cu.import("resource://gre/modules/PerformanceStats.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://testing-common/ContentTask.jsm", this);
+
+
+const URL = "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random();
+const PARENT_TITLE = `Main frame for test browser_compartments.js ${Math.random()}`;
+const FRAME_TITLE = `Subframe for test browser_compartments.js ${Math.random()}`;
+
+const PARENT_PID = Services.appinfo.processID;
+
+// This function is injected as source as a frameScript
+function frameScript() {
+ try {
+ "use strict";
+
+ const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+ Cu.import("resource://gre/modules/PerformanceStats.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ // Make sure that the stopwatch is now active.
+ let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks", "compartments"]);
+
+ addMessageListener("compartments-test:getStatistics", () => {
+ try {
+ monitor.promiseSnapshot().then(snapshot => {
+ sendAsyncMessage("compartments-test:getStatistics", {snapshot, pid: Services.appinfo.processID});
+ });
+ } catch (ex) {
+ Cu.reportError("Error in content (getStatistics): " + ex);
+ Cu.reportError(ex.stack);
+ }
+ });
+
+ addMessageListener("compartments-test:setTitles", titles => {
+ try {
+ content.document.title = titles.data.parent;
+ for (let i = 0; i < content.frames.length; ++i) {
+ content.frames[i].postMessage({title: titles.data.frames}, "*");
+ }
+ console.log("content", "Done setting titles", content.document.title);
+ sendAsyncMessage("compartments-test:setTitles");
+ } catch (ex) {
+ Cu.reportError("Error in content (setTitles): " + ex);
+ Cu.reportError(ex.stack);
+ }
+ });
+ } catch (ex) {
+ Cu.reportError("Error in content (setup): " + ex);
+ Cu.reportError(ex.stack);
+ }
+}
+
+// A variant of `Assert` that doesn't spam the logs
+// in case of success.
+var SilentAssert = {
+ equal: function(a, b, msg) {
+ if (a == b) {
+ return;
+ }
+ Assert.equal(a, b, msg);
+ },
+ notEqual: function(a, b, msg) {
+ if (a != b) {
+ return;
+ }
+ Assert.notEqual(a, b, msg);
+ },
+ ok: function(a, msg) {
+ if (a) {
+ return;
+ }
+ Assert.ok(a, msg);
+ },
+ leq: function(a, b, msg) {
+ this.ok(a <= b, `${msg}: ${a} <= ${b}`);
+ }
+};
+
+var isShuttingDown = false;
+function monotinicity_tester(source, testName) {
+ // In the background, check invariants:
+ // - numeric data can only ever increase;
+ // - the name, addonId, isSystem of a component never changes;
+ // - the name, addonId, isSystem of the process data;
+ // - there is at most one component with a combination of `name` and `addonId`;
+ // - types, etc.
+ let previous = {
+ processData: null,
+ componentsMap: new Map(),
+ };
+
+ let sanityCheck = function(prev, next) {
+ if (prev == null) {
+ return;
+ }
+ for (let k of ["groupId", "addonId", "isSystem"]) {
+ SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed (${prev.name}).`);
+ }
+ for (let [probe, k] of [
+ ["jank", "totalUserTime"],
+ ["jank", "totalSystemTime"],
+ ["cpow", "totalCPOWTime"],
+ ["ticks", "ticks"]
+ ]) {
+ SilentAssert.equal(typeof next[probe][k], "number", `Sanity check (${testName}): ${k} is a number.`);
+ SilentAssert.leq(prev[probe][k], next[probe][k], `Sanity check (${testName}): ${k} is monotonic.`);
+ SilentAssert.leq(0, next[probe][k], `Sanity check (${testName}): ${k} is >= 0.`)
+ }
+ SilentAssert.equal(prev.jank.durations.length, next.jank.durations.length);
+ for (let i = 0; i < next.jank.durations.length; ++i) {
+ SilentAssert.ok(typeof next.jank.durations[i] == "number" && next.jank.durations[i] >= 0,
+ `Sanity check (${testName}): durations[${i}] is a non-negative number.`);
+ SilentAssert.leq(prev.jank.durations[i], next.jank.durations[i],
+ `Sanity check (${testName}): durations[${i}] is monotonic.`);
+ }
+ for (let i = 0; i < next.jank.durations.length - 1; ++i) {
+ SilentAssert.leq(next.jank.durations[i + 1], next.jank.durations[i],
+ `Sanity check (${testName}): durations[${i}] >= durations[${i + 1}].`)
+ }
+ };
+ let iteration = 0;
+ let frameCheck = Task.async(function*() {
+ if (isShuttingDown) {
+ window.clearInterval(interval);
+ return;
+ }
+ let name = `${testName}: ${iteration++}`;
+ let result = yield source();
+ if (!result) {
+ // This can happen at the end of the test when we attempt
+ // to communicate too late with the content process.
+ window.clearInterval(interval);
+ return;
+ }
+ let {pid, snapshot} = result;
+
+ // Sanity check on the process data.
+ sanityCheck(previous.processData, snapshot.processData);
+ SilentAssert.equal(snapshot.processData.isSystem, true);
+ SilentAssert.equal(snapshot.processData.name, "<process>");
+ SilentAssert.equal(snapshot.processData.addonId, "");
+ SilentAssert.equal(snapshot.processData.processId, pid);
+ previous.procesData = snapshot.processData;
+
+ // Sanity check on components data.
+ let map = new Map();
+ for (let item of snapshot.componentsData) {
+ for (let [probe, k] of [
+ ["jank", "totalUserTime"],
+ ["jank", "totalSystemTime"],
+ ["cpow", "totalCPOWTime"]
+ ]) {
+ // Note that we cannot expect components data to be always smaller
+ // than process data, as `getrusage` & co are not monotonic.
+ SilentAssert.leq(item[probe][k], 3 * snapshot.processData[probe][k],
+ `Sanity check (${name}): ${k} of component is not impossibly larger than that of process`);
+ }
+
+ let isCorrectPid = (item.processId == pid && !item.isChildProcess)
+ || (item.processId != pid && item.isChildProcess);
+ SilentAssert.ok(isCorrectPid, `Pid check (${name}): the item comes from the right process`);
+
+ let key = item.groupId;
+ if (map.has(key)) {
+ let old = map.get(key);
+ Assert.ok(false, `Component ${key} has already been seen. Latest: ${item.addonId||item.name}, previous: ${old.addonId||old.name}`);
+ }
+ map.set(key, item);
+ }
+ for (let item of snapshot.componentsData) {
+ if (!item.parentId) {
+ continue;
+ }
+ let parent = map.get(item.parentId);
+ SilentAssert.ok(parent, `The parent exists ${item.parentId}`);
+
+ for (let [probe, k] of [
+ ["jank", "totalUserTime"],
+ ["jank", "totalSystemTime"],
+ ["cpow", "totalCPOWTime"]
+ ]) {
+ // Note that we cannot expect components data to be always smaller
+ // than parent data, as `getrusage` & co are not monotonic.
+ SilentAssert.leq(item[probe][k], 2 * parent[probe][k],
+ `Sanity check (${testName}): ${k} of component is not impossibly larger than that of parent`);
+ }
+ }
+ for (let [key, item] of map) {
+ sanityCheck(previous.componentsMap.get(key), item);
+ previous.componentsMap.set(key, item);
+ }
+ });
+ let interval = window.setInterval(frameCheck, 300);
+ registerCleanupFunction(() => {
+ window.clearInterval(interval);
+ });
+}
+
+add_task(function* test() {
+ let monitor = PerformanceStats.getMonitor(["jank", "cpow", "ticks"]);
+
+ info("Extracting initial state");
+ let stats0 = yield monitor.promiseSnapshot();
+ Assert.notEqual(stats0.componentsData.length, 0, "There is more than one component");
+ Assert.ok(!stats0.componentsData.find(stat => stat.name.indexOf(URL) != -1),
+ "The url doesn't appear yet");
+
+ let newTab = gBrowser.addTab();
+ let browser = newTab.linkedBrowser;
+ // Setup monitoring in the tab
+ info("Setting up monitoring in the tab");
+ yield ContentTask.spawn(newTab.linkedBrowser, null, frameScript);
+
+ info("Opening URL");
+ newTab.linkedBrowser.loadURI(URL);
+
+ if (Services.sysinfo.getPropertyAsAString("name") == "Windows_NT") {
+ info("Deactivating sanity checks under Windows (bug 1151240)");
+ } else {
+ info("Setting up sanity checks");
+ monotinicity_tester(() => monitor.promiseSnapshot().then(snapshot => ({snapshot, pid: PARENT_PID})), "parent process");
+ monotinicity_tester(() => promiseContentResponseOrNull(browser, "compartments-test:getStatistics", null), "content process" );
+ }
+
+ let skipTotalUserTime = hasLowPrecision();
+
+
+ while (true) {
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ // We may have race conditions with DOM loading.
+ // Don't waste too much brainpower here, let's just ask
+ // repeatedly for the title to be changed, until this works.
+ info("Setting titles");
+ yield promiseContentResponse(browser, "compartments-test:setTitles", {
+ parent: PARENT_TITLE,
+ frames: FRAME_TITLE
+ });
+ info("Titles set");
+
+ let {snapshot: stats} = (yield promiseContentResponse(browser, "compartments-test:getStatistics", null));
+
+ // Attach titles to components.
+ let titles = [];
+ let map = new Map();
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ let window = windows.getNext();
+ let tabbrowser = window.gBrowser;
+ for (let browser of tabbrowser.browsers) {
+ let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
+ if (id != null) {
+ map.set(id, browser);
+ }
+ }
+ }
+ for (let stat of stats.componentsData) {
+ if (!stat.windowId) {
+ continue;
+ }
+ let browser = map.get(stat.windowId);
+ if (!browser) {
+ continue;
+ }
+ let title = browser.contentTitle;
+ if (title) {
+ stat.title = title;
+ titles.push(title);
+ }
+ }
+
+ // While the webpage consists in three compartments, we should see only
+ // one `PerformanceData` in `componentsData`. Its `name` is undefined
+ // (could be either the main frame or one of its subframes), but its
+ // `title` should be the title of the main frame.
+ info(`Searching for frame title '${FRAME_TITLE}' in ${JSON.stringify(titles)} (I hope not to find it)`);
+ Assert.ok(!titles.includes(FRAME_TITLE), "Searching by title, the frames don't show up in the list of components");
+
+ info(`Searching for window title '${PARENT_TITLE}' in ${JSON.stringify(titles)} (I hope to find it)`);
+ let parent = stats.componentsData.find(x => x.title == PARENT_TITLE);
+ if (!parent) {
+ info("Searching by title, we didn't find the main frame");
+ continue;
+ }
+ info("Found the main frame");
+
+ if (skipTotalUserTime) {
+ info("Not looking for total user time on this platform, we're done");
+ break;
+ } else if (parent.jank.totalUserTime > 1000) {
+ info("Enough CPU time detected, we're done");
+ break;
+ } else {
+ info(`Not enough CPU time detected: ${parent.jank.totalUserTime}`);
+ }
+ }
+ isShuttingDown = true;
+
+ // Cleanup
+ gBrowser.removeTab(newTab, {skipPermitUnload: true});
+});
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html
new file mode 100644
index 0000000000..69edfe871b
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>
+ Subframe for test browser_compartments.html (do not change this title)
+ </title>
+ <script src="browser_compartments_script.js"></script>
+</head>
+<body>
+Subframe loaded.
+</body>
+</html>
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js
new file mode 100644
index 0000000000..3d5f7114f6
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_compartments_script.js
@@ -0,0 +1,29 @@
+
+var carryOn = true;
+
+window.addEventListener("message", e => {
+ console.log("frame content", "message", e);
+ if ("title" in e.data) {
+ document.title = e.data.title;
+ }
+ if ("stop" in e.data) {
+ carryOn = false;
+ }
+});
+
+// Use some CPU.
+var interval = window.setInterval(() => {
+ if (!carryOn) {
+ window.clearInterval(interval);
+ return;
+ }
+
+ // Compute an arbitrary value, print it out to make sure that the JS
+ // engine doesn't discard all our computation.
+ var date = Date.now();
+ var array = [];
+ var i = 0;
+ while (Date.now() - date <= 100) {
+ array[i%2] = i++;
+ }
+}, 300);
diff --git a/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js
new file mode 100644
index 0000000000..eb908c8dbf
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js
@@ -0,0 +1,111 @@
+"use strict";
+
+/**
+ * Tests for PerformanceWatcher watching slow web pages.
+ */
+
+ /**
+ * Simulate a slow webpage.
+ */
+function WebpageBurner() {
+ CPUBurner.call(this, "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random(), 300000);
+}
+WebpageBurner.prototype = Object.create(CPUBurner.prototype);
+WebpageBurner.prototype.promiseBurnContentCPU = function() {
+ return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {});
+};
+
+function WebpageListener(windowId, accept) {
+ info(`Creating WebpageListener for ${windowId}`);
+ AlertListener.call(this, accept, {
+ register: () => PerformanceWatcher.addPerformanceListener({windowId}, this.listener),
+ unregister: () => PerformanceWatcher.removePerformanceListener({windowId}, this.listener)
+ });
+}
+WebpageListener.prototype = Object.create(AlertListener.prototype);
+
+add_task(function* init() {
+ // Get rid of buffering.
+ let service = Cc["@mozilla.org/toolkit/performance-stats-service;1"].getService(
+ Ci.nsIPerformanceStatsService);
+ let oldDelay = service.jankAlertBufferingDelay;
+
+ service.jankAlertBufferingDelay = 0 /* ms */;
+ registerCleanupFunction(() => {
+ info("Cleanup");
+ service.jankAlertBufferingDelay = oldDelay;
+ });
+});
+
+add_task(function* test_open_window_then_watch_it() {
+ let burner = new WebpageBurner();
+ yield burner.promiseInitialized;
+ yield burner.promiseBurnContentCPU();
+
+ info(`Check that burning CPU triggers the real listener, but not the fake listener`);
+ let realListener = new WebpageListener(burner.windowId, (group, details) => {
+ info(`test: realListener for ${burner.tab.linkedBrowser.outerWindowID}: ${group}, ${details}\n`);
+ Assert.equal(group.windowId, burner.windowId, "We should not receive data meant for another group");
+ return details;
+ }); // This listener should be triggered.
+
+ info(`Creating fake burner`);
+ let otherTab = gBrowser.addTab();
+ yield BrowserTestUtils.browserLoaded(otherTab.linkedBrowser);
+ info(`Check that burning CPU triggers the real listener, but not the fake listener`);
+ let fakeListener = new WebpageListener(otherTab.linkedBrowser.outerWindowID, group => group.windowId == burner.windowId); // This listener should never be triggered.
+ let universalListener = new WebpageListener(0, alerts =>
+ alerts.find(alert => alert.source.windowId == burner.windowId)
+ );
+
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ yield burner.run("promiseBurnContentCPU", 20, realListener);
+ Assert.ok(realListener.triggered, `1. The real listener was triggered`);
+ Assert.ok(universalListener.triggered, `1. The universal listener was triggered`);
+ Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered`);
+
+ if (realListener.result) {
+ Assert.ok(realListener.result.highestJank >= 300, `1. jank is at least 300ms (${realListener.result.highestJank}ms)`);
+ }
+
+ info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener`);
+ Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {}));
+ Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener));
+
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield burner.run("promiseBurnContentCPU", 20, realListener);
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ Assert.ok(realListener.triggered, `2. The real listener was triggered`);
+ Assert.ok(universalListener.triggered, `2. The universal listener was triggered`);
+ Assert.ok(!fakeListener.triggered, `2. The fake listener was not triggered`);
+ if (realListener.result) {
+ Assert.ok(realListener.result.highestJank >= 300, `2. jank is at least 300ms (${realListener.jank}ms)`);
+ }
+
+ info(`Attempting to remove correctly, check if the listener is still triggered`);
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ realListener.unregister();
+
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield burner.run("promiseBurnContentCPU", 3, realListener);
+ Assert.ok(!realListener.triggered, `3. After being unregistered, the real listener was not triggered`);
+ Assert.ok(universalListener.triggered, `3. The universal listener is still triggered`);
+
+ universalListener.unregister();
+
+ // Waiting a little – listeners are buffered.
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield burner.run("promiseBurnContentCPU", 3, realListener);
+ Assert.ok(!universalListener.triggered, `4. After being unregistered, the universal listener is not triggered`);
+
+ fakeListener.unregister();
+ burner.dispose();
+ gBrowser.removeTab(otherTab);
+});
diff --git a/toolkit/components/perfmonitoring/tests/browser/head.js b/toolkit/components/perfmonitoring/tests/browser/head.js
new file mode 100644
index 0000000000..92258fd1b9
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/head.js
@@ -0,0 +1,287 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/AddonManager.jsm", this);
+Cu.import("resource://gre/modules/AddonWatcher.jsm", this);
+Cu.import("resource://gre/modules/PerformanceWatcher.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+/**
+ * Base class for simulating slow addons/webpages.
+ */
+function CPUBurner(url, jankThreshold) {
+ info(`CPUBurner: Opening tab for ${url}\n`);
+ this.url = url;
+ this.tab = gBrowser.addTab(url);
+ this.jankThreshold = jankThreshold;
+ let browser = this.tab.linkedBrowser;
+ this._browser = browser;
+ ContentTask.spawn(this._browser, null, CPUBurner.frameScript);
+ this.promiseInitialized = BrowserTestUtils.browserLoaded(browser);
+}
+CPUBurner.prototype = {
+ get windowId() {
+ return this._browser.outerWindowID;
+ },
+ /**
+ * Burn CPU until it triggers a listener with the specified jank threshold.
+ */
+ run: Task.async(function*(burner, max, listener) {
+ listener.reset();
+ for (let i = 0; i < max; ++i) {
+ yield new Promise(resolve => setTimeout(resolve, 50));
+ try {
+ yield this[burner]();
+ } catch (ex) {
+ return false;
+ }
+ if (listener.triggered && listener.result >= this.jankThreshold) {
+ return true;
+ }
+ }
+ return false;
+ }),
+ dispose: function() {
+ info(`CPUBurner: Closing tab for ${this.url}\n`);
+ gBrowser.removeTab(this.tab);
+ }
+};
+// This function is injected in all frames
+CPUBurner.frameScript = function() {
+ try {
+ "use strict";
+
+ const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+ let sandboxes = new Map();
+ let getSandbox = function(addonId) {
+ let sandbox = sandboxes.get(addonId);
+ if (!sandbox) {
+ sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId });
+ sandboxes.set(addonId, sandbox);
+ }
+ return sandbox;
+ };
+
+ let burnCPU = function() {
+ var start = Date.now();
+ var ignored = [];
+ while (Date.now() - start < 500) {
+ ignored[ignored.length % 2] = ignored.length;
+ }
+ };
+ let burnCPUInSandbox = function(addonId) {
+ let sandbox = getSandbox(addonId);
+ Cu.evalInSandbox(burnCPU.toSource() + "()", sandbox);
+ };
+
+ {
+ let topic = "test-performance-watcher:burn-content-cpu";
+ addMessageListener(topic, function(msg) {
+ try {
+ if (msg.data && msg.data.addonId) {
+ burnCPUInSandbox(msg.data.addonId);
+ } else {
+ burnCPU();
+ }
+ sendAsyncMessage(topic, {});
+ } catch (ex) {
+ dump(`This is the content attempting to burn CPU: error ${ex}\n`);
+ dump(`${ex.stack}\n`);
+ }
+ });
+ }
+
+ // Bind the function to the global context or it might be GC'd during test
+ // causing failures (bug 1230027)
+ this.burnCPOWInSandbox = function(addonId) {
+ try {
+ burnCPUInSandbox(addonId);
+ } catch (ex) {
+ dump(`This is the addon attempting to burn CPOW: error ${ex}\n`);
+ dump(`${ex.stack}\n`);
+ }
+ }
+
+ sendAsyncMessage("test-performance-watcher:cpow-init", {}, {
+ burnCPOWInSandbox: this.burnCPOWInSandbox
+ });
+
+ } catch (ex) {
+ Cu.reportError("This is the addon: error " + ex);
+ Cu.reportError(ex.stack);
+ }
+};
+
+/**
+ * Base class for listening to slow group alerts
+ */
+function AlertListener(accept, {register, unregister}) {
+ this.listener = (...args) => {
+ if (this._unregistered) {
+ throw new Error("Listener was unregistered");
+ }
+ let result = accept(...args);
+ if (!result) {
+ return;
+ }
+ this.result = result;
+ this.triggered = true;
+ return;
+ };
+ this.triggered = false;
+ this.result = null;
+ this._unregistered = false;
+ this._unregister = unregister;
+ registerCleanupFunction(() => {
+ this.unregister();
+ });
+ register();
+}
+AlertListener.prototype = {
+ unregister: function() {
+ this.reset();
+ if (this._unregistered) {
+ info(`head.js: No need to unregister, we're already unregistered.\n`);
+ return;
+ }
+ info(`head.js: Unregistering listener.\n`);
+ this._unregistered = true;
+ this._unregister();
+ info(`head.js: Unregistration complete.\n`);
+ },
+ reset: function() {
+ this.triggered = false;
+ this.result = null;
+ },
+};
+
+/**
+ * Simulate a slow add-on.
+ */
+function AddonBurner(addonId = "fake add-on id: " + Math.random()) {
+ this.jankThreshold = 200000;
+ CPUBurner.call(this, `http://example.com/?uri=${addonId}`, this.jankThreshold);
+ this._addonId = addonId;
+ this._sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId: this._addonId });
+ this._CPOWBurner = null;
+
+ this._promiseCPOWBurner = new Promise(resolve => {
+ this._browser.messageManager.addMessageListener("test-performance-watcher:cpow-init", msg => {
+ // Note that we cannot resolve Promises with CPOWs now that they
+ // have been outlawed in bug 1233497, so we stash it in the
+ // AddonBurner instance instead.
+ this._CPOWBurner = msg.objects.burnCPOWInSandbox;
+ resolve();
+ });
+ });
+}
+AddonBurner.prototype = Object.create(CPUBurner.prototype);
+Object.defineProperty(AddonBurner.prototype, "addonId", {
+ get: function() {
+ return this._addonId;
+ }
+});
+
+/**
+ * Simulate slow code being executed by the add-on in the chrome.
+ */
+AddonBurner.prototype.burnCPU = function() {
+ Cu.evalInSandbox(AddonBurner.burnCPU.toSource() + "()", this._sandbox);
+};
+
+/**
+ * Simulate slow code being executed by the add-on in a CPOW.
+ */
+AddonBurner.prototype.promiseBurnCPOW = Task.async(function*() {
+ yield this._promiseCPOWBurner;
+ ok(this._CPOWBurner, "Got the CPOW burner");
+ let burner = this._CPOWBurner;
+ info("Parent: Preparing to burn CPOW");
+ try {
+ yield burner(this._addonId);
+ info("Parent: Done burning CPOW");
+ } catch (ex) {
+ info(`Parent: Error burning CPOW: ${ex}\n`);
+ info(ex.stack + "\n");
+ }
+});
+
+/**
+ * Simulate slow code being executed by the add-on in the content.
+ */
+AddonBurner.prototype.promiseBurnContentCPU = function() {
+ return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {addonId: this._addonId});
+};
+AddonBurner.burnCPU = function() {
+ var start = Date.now();
+ var ignored = [];
+ while (Date.now() - start < 500) {
+ ignored[ignored.length % 2] = ignored.length;
+ }
+};
+
+
+function AddonListener(addonId, accept) {
+ let target = {addonId};
+ AlertListener.call(this, accept, {
+ register: () => {
+ info(`AddonListener: registering ${JSON.stringify(target, null, "\t")}`);
+ PerformanceWatcher.addPerformanceListener({addonId}, this.listener);
+ },
+ unregister: () => {
+ info(`AddonListener: unregistering ${JSON.stringify(target, null, "\t")}`);
+ PerformanceWatcher.removePerformanceListener({addonId}, this.listener);
+ }
+ });
+}
+AddonListener.prototype = Object.create(AlertListener.prototype);
+
+function promiseContentResponse(browser, name, message) {
+ let mm = browser.messageManager;
+ let promise = new Promise(resolve => {
+ function removeListener() {
+ mm.removeMessageListener(name, listener);
+ }
+
+ function listener(msg) {
+ removeListener();
+ resolve(msg.data);
+ }
+
+ mm.addMessageListener(name, listener);
+ registerCleanupFunction(removeListener);
+ });
+ mm.sendAsyncMessage(name, message);
+ return promise;
+}
+function promiseContentResponseOrNull(browser, name, message) {
+ if (!browser.messageManager) {
+ return null;
+ }
+ return promiseContentResponse(browser, name, message);
+}
+
+/**
+ * `true` if we are running an OS in which the OS performance
+ * clock has a low precision and might unpredictably
+ * never be updated during the execution of the test.
+ */
+function hasLowPrecision() {
+ let [sysName, sysVersion] = [Services.sysinfo.getPropertyAsAString("name"), Services.sysinfo.getPropertyAsDouble("version")];
+ info(`Running ${sysName} version ${sysVersion}`);
+
+ if (sysName == "Windows_NT" && sysVersion < 6) {
+ info("Running old Windows, need to deactivate tests due to bad precision.");
+ return true;
+ }
+ if (sysName == "Linux" && sysVersion <= 2.6) {
+ info("Running old Linux, need to deactivate tests due to bad precision.");
+ return true;
+ }
+ info("This platform has good precision.")
+ return false;
+}
diff --git a/toolkit/components/perfmonitoring/tests/browser/install.rdf b/toolkit/components/perfmonitoring/tests/browser/install.rdf
new file mode 100644
index 0000000000..65add014f6
--- /dev/null
+++ b/toolkit/components/perfmonitoring/tests/browser/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>addonwatcher-test@mozilla.com</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:name>Sample for browser_AddonWatcher.js</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/places/BookmarkHTMLUtils.jsm b/toolkit/components/places/BookmarkHTMLUtils.jsm
new file mode 100644
index 0000000000..a009a5e7c8
--- /dev/null
+++ b/toolkit/components/places/BookmarkHTMLUtils.jsm
@@ -0,0 +1,1188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file works on the old-style "bookmarks.html" file. It includes
+ * functions to import and export existing bookmarks to this file format.
+ *
+ * Format
+ * ------
+ *
+ * Primary heading := h1
+ * Old version used this to set attributes on the bookmarks RDF root, such
+ * as the last modified date. We only use H1 to check for the attribute
+ * PLACES_ROOT, which tells us that this hierarchy root is the places root.
+ * For backwards compatibility, if we don't find this, we assume that the
+ * hierarchy is rooted at the bookmarks menu.
+ * Heading := any heading other than h1
+ * Old version used this to set attributes on the current container. We only
+ * care about the content of the heading container, which contains the title
+ * of the bookmark container.
+ * Bookmark := a
+ * HREF is the destination of the bookmark
+ * FEEDURL is the URI of the RSS feed if this is a livemark.
+ * LAST_CHARSET is stored as an annotation so that the next time we go to
+ * that page we remember the user's preference.
+ * WEB_PANEL is set to "true" if the bookmark should be loaded in the sidebar.
+ * ICON will be stored in the favicon service
+ * ICON_URI is new for places bookmarks.html, it refers to the original
+ * URI of the favicon so we don't have to make up favicon URLs.
+ * Text of the <a> container is the name of the bookmark
+ * Ignored: LAST_VISIT, ID (writing out non-RDF IDs can confuse Firefox 2)
+ * Bookmark comment := dd
+ * This affects the previosly added bookmark
+ * Separator := hr
+ * Insert a separator into the current container
+ * The folder hierarchy is defined by <dl>/<ul>/<menu> (the old importing code
+ * handles all these cases, when we write, use <dl>).
+ *
+ * Overall design
+ * --------------
+ *
+ * We need to emulate a recursive parser. A "Bookmark import frame" is created
+ * corresponding to each folder we encounter. These are arranged in a stack,
+ * and contain all the state we need to keep track of.
+ *
+ * A frame is created when we find a heading, which defines a new container.
+ * The frame also keeps track of the nesting of <DL>s, (in well-formed
+ * bookmarks files, these will have a 1-1 correspondence with frames, but we
+ * try to be a little more flexible here). When the nesting count decreases
+ * to 0, then we know a frame is complete and to pop back to the previous
+ * frame.
+ *
+ * Note that a lot of things happen when tags are CLOSED because we need to
+ * get the text from the content of the tag. For example, link and heading tags
+ * both require the content (= title) before actually creating it.
+ */
+
+this.EXPORTED_SYMBOLS = [ "BookmarkHTMLUtils" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+const Container_Normal = 0;
+const Container_Toolbar = 1;
+const Container_Menu = 2;
+const Container_Unfiled = 3;
+const Container_Places = 4;
+
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+const MICROSEC_PER_SEC = 1000000;
+
+const EXPORT_INDENT = " "; // four spaces
+
+// Counter used to build fake favicon urls.
+var serialNumber = 0;
+
+function base64EncodeString(aString) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(aString, aString.length);
+ let encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
+ .createInstance(Ci.nsIScriptableBase64Encoder);
+ return encoder.encodeToString(stream, aString.length);
+}
+
+/**
+ * Provides HTML escaping for use in HTML attributes and body of the bookmarks
+ * file, compatible with the old bookmarks system.
+ */
+function escapeHtmlEntities(aText) {
+ return (aText || "").replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;");
+}
+
+/**
+ * Provides URL escaping for use in HTML attributes of the bookmarks file,
+ * compatible with the old bookmarks system.
+ */
+function escapeUrl(aText) {
+ return (aText || "").replace(/"/g, "%22");
+}
+
+function notifyObservers(aTopic, aInitialImport) {
+ Services.obs.notifyObservers(null, aTopic, aInitialImport ? "html-initial"
+ : "html");
+}
+
+this.BookmarkHTMLUtils = Object.freeze({
+ /**
+ * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
+ *
+ * @param aSpec
+ * String containing the "file:" URI for the existing "bookmarks.html"
+ * file to be loaded.
+ * @param aInitialImport
+ * Whether this is the initial import executed on a new profile.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ */
+ importFromURL: function BHU_importFromURL(aSpec, aInitialImport) {
+ return Task.spawn(function* () {
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
+ try {
+ let importer = new BookmarkImporter(aInitialImport);
+ yield importer.importFromURL(aSpec);
+
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
+ } catch (ex) {
+ Cu.reportError("Failed to import bookmarks from " + aSpec + ": " + ex);
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
+ throw ex;
+ }
+ });
+ },
+
+ /**
+ * Loads the current bookmarks hierarchy from a "bookmarks.html" file.
+ *
+ * @param aFilePath
+ * OS.File path string of the "bookmarks.html" file to be loaded.
+ * @param aInitialImport
+ * Whether this is the initial import executed on a new profile.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ importFromFile: function BHU_importFromFile(aFilePath, aInitialImport) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
+ "is deprecated. Please use an OS.File path string instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+
+ return Task.spawn(function* () {
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN, aInitialImport);
+ try {
+ if (!(yield OS.File.exists(aFilePath))) {
+ throw new Error("Cannot import from nonexisting html file: " + aFilePath);
+ }
+ let importer = new BookmarkImporter(aInitialImport);
+ yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
+
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS, aInitialImport);
+ } catch (ex) {
+ Cu.reportError("Failed to import bookmarks from " + aFilePath + ": " + ex);
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED, aInitialImport);
+ throw ex;
+ }
+ });
+ },
+
+ /**
+ * Saves the current bookmarks hierarchy to a "bookmarks.html" file.
+ *
+ * @param aFilePath
+ * OS.File path string for the "bookmarks.html" file to be created.
+ *
+ * @return {Promise}
+ * @resolves To the exported bookmarks count when the file has been created.
+ * @rejects JavaScript exception.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ exportToFile: function BHU_exportToFile(aFilePath) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to BookmarksHTMLUtils.exportToFile " +
+ "is deprecated. Please use an OS.File path string instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+ return Task.spawn(function* () {
+ let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
+ let startTime = Date.now();
+
+ // Report the time taken to convert the tree to HTML.
+ let exporter = new BookmarkExporter(bookmarks);
+ yield exporter.exportToFile(aFilePath);
+
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_EXPORT_TOHTML_MS")
+ .add(Date.now() - startTime);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ }
+
+ return count;
+ });
+ },
+
+ get defaultPath() {
+ try {
+ return Services.prefs.getCharPref("browser.bookmarks.file");
+ } catch (ex) {}
+ return OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html")
+ }
+});
+
+function Frame(aFrameId) {
+ this.containerId = aFrameId;
+
+ /**
+ * How many <dl>s have been nested. Each frame/container should start
+ * with a heading, and is then followed by a <dl>, <ul>, or <menu>. When
+ * that list is complete, then it is the end of this container and we need
+ * to pop back up one level for new items. If we never get an open tag for
+ * one of these things, we should assume that the container is empty and
+ * that things we find should be siblings of it. Normally, these <dl>s won't
+ * be nested so this will be 0 or 1.
+ */
+ this.containerNesting = 0;
+
+ /**
+ * when we find a heading tag, it actually affects the title of the NEXT
+ * container in the list. This stores that heading tag and whether it was
+ * special. 'consumeHeading' resets this._
+ */
+ this.lastContainerType = Container_Normal;
+
+ /**
+ * this contains the text from the last begin tag until now. It is reset
+ * at every begin tag. We can check it when we see a </a>, or </h3>
+ * to see what the text content of that node should be.
+ */
+ this.previousText = "";
+
+ /**
+ * true when we hit a <dd>, which contains the description for the preceding
+ * <a> tag. We can't just check for </dd> like we can for </a> or </h3>
+ * because if there is a sub-folder, it is actually a child of the <dd>
+ * because the tag is never explicitly closed. If this is true and we see a
+ * new open tag, that means to commit the description to the previous
+ * bookmark.
+ *
+ * Additional weirdness happens when the previous <dt> tag contains a <h3>:
+ * this means there is a new folder with the given description, and whose
+ * children are contained in the following <dl> list.
+ *
+ * This is handled in openContainer(), which commits previous text if
+ * necessary.
+ */
+ this.inDescription = false;
+
+ /**
+ * contains the URL of the previous bookmark created. This is used so that
+ * when we encounter a <dd>, we know what bookmark to associate the text with.
+ * This is cleared whenever we hit a <h3>, so that we know NOT to save this
+ * with a bookmark, but to keep it until
+ */
+ this.previousLink = null; // nsIURI
+
+ /**
+ * contains the URL of the previous livemark, so that when the link ends,
+ * and the livemark title is known, we can create it.
+ */
+ this.previousFeed = null; // nsIURI
+
+ /**
+ * Contains the id of an imported, or newly created bookmark.
+ */
+ this.previousId = 0;
+
+ /**
+ * Contains the date-added and last-modified-date of an imported item.
+ * Used to override the values set by insertBookmark, createFolder, etc.
+ */
+ this.previousDateAdded = 0;
+ this.previousLastModifiedDate = 0;
+}
+
+function BookmarkImporter(aInitialImport) {
+ this._isImportDefaults = aInitialImport;
+ // The bookmark change source, used to determine the sync status and change
+ // counter.
+ this._source = aInitialImport ? PlacesUtils.bookmarks.SOURCE_IMPORT_REPLACE :
+ PlacesUtils.bookmarks.SOURCE_IMPORT;
+ this._frames = new Array();
+ this._frames.push(new Frame(PlacesUtils.bookmarksMenuFolderId));
+}
+
+BookmarkImporter.prototype = {
+
+ _safeTrim: function safeTrim(aStr) {
+ return aStr ? aStr.trim() : aStr;
+ },
+
+ get _curFrame() {
+ return this._frames[this._frames.length - 1];
+ },
+
+ get _previousFrame() {
+ return this._frames[this._frames.length - 2];
+ },
+
+ /**
+ * This is called when there is a new folder found. The folder takes the
+ * name from the previous frame's heading.
+ */
+ _newFrame: function newFrame() {
+ let containerId = -1;
+ let frame = this._curFrame;
+ let containerTitle = frame.previousText;
+ frame.previousText = "";
+ let containerType = frame.lastContainerType;
+
+ switch (containerType) {
+ case Container_Normal:
+ // append a new folder
+ containerId =
+ PlacesUtils.bookmarks.createFolder(frame.containerId,
+ containerTitle,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aGuid */ null, this._source);
+ break;
+ case Container_Places:
+ containerId = PlacesUtils.placesRootId;
+ break;
+ case Container_Menu:
+ containerId = PlacesUtils.bookmarksMenuFolderId;
+ break;
+ case Container_Unfiled:
+ containerId = PlacesUtils.unfiledBookmarksFolderId;
+ break;
+ case Container_Toolbar:
+ containerId = PlacesUtils.toolbarFolderId;
+ break;
+ default:
+ // NOT REACHED
+ throw new Error("Unreached");
+ }
+
+ if (frame.previousDateAdded > 0) {
+ try {
+ PlacesUtils.bookmarks.setItemDateAdded(containerId, frame.previousDateAdded, this._source);
+ } catch (e) {
+ }
+ frame.previousDateAdded = 0;
+ }
+ if (frame.previousLastModifiedDate > 0) {
+ try {
+ PlacesUtils.bookmarks.setItemLastModified(containerId, frame.previousLastModifiedDate, this._source);
+ } catch (e) {
+ }
+ // don't clear last-modified, in case there's a description
+ }
+
+ frame.previousId = containerId;
+
+ this._frames.push(new Frame(containerId));
+ },
+
+ /**
+ * Handles <hr> as a separator.
+ *
+ * @note Separators may have a title in old html files, though Places dropped
+ * support for them.
+ * We also don't import ADD_DATE or LAST_MODIFIED for separators because
+ * pre-Places bookmarks did not support them.
+ */
+ _handleSeparator: function handleSeparator(aElt) {
+ let frame = this._curFrame;
+ try {
+ frame.previousId =
+ PlacesUtils.bookmarks.insertSeparator(frame.containerId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aGuid */ null,
+ this._source);
+ } catch (e) {}
+ },
+
+ /**
+ * Handles <H1>. We check for the attribute PLACES_ROOT and reset the
+ * container id if it's found. Otherwise, the default bookmark menu
+ * root is assumed and imported things will go into the bookmarks menu.
+ */
+ _handleHead1Begin: function handleHead1Begin(aElt) {
+ if (this._frames.length > 1) {
+ return;
+ }
+ if (aElt.hasAttribute("places_root")) {
+ this._curFrame.containerId = PlacesUtils.placesRootId;
+ }
+ },
+
+ /**
+ * Called for h2,h3,h4,h5,h6. This just stores the correct information in
+ * the current frame; the actual new frame corresponding to the container
+ * associated with the heading will be created when the tag has been closed
+ * and we know the title (we don't know to create a new folder or to merge
+ * with an existing one until we have the title).
+ */
+ _handleHeadBegin: function handleHeadBegin(aElt) {
+ let frame = this._curFrame;
+
+ // after a heading, a previous bookmark is not applicable (for example, for
+ // the descriptions contained in a <dd>). Neither is any previous head type
+ frame.previousLink = null;
+ frame.lastContainerType = Container_Normal;
+
+ // It is syntactically possible for a heading to appear after another heading
+ // but before the <dl> that encloses that folder's contents. This should not
+ // happen in practice, as the file will contain "<dl></dl>" sequence for
+ // empty containers.
+ //
+ // Just to be on the safe side, if we encounter
+ // <h3>FOO</h3>
+ // <h3>BAR</h3>
+ // <dl>...content 1...</dl>
+ // <dl>...content 2...</dl>
+ // we'll pop the stack when we find the h3 for BAR, treating that as an
+ // implicit ending of the FOO container. The output will be FOO and BAR as
+ // siblings. If there's another <dl> following (as in "content 2"), those
+ // items will be treated as further siblings of FOO and BAR
+ // This special frame popping business, of course, only happens when our
+ // frame array has more than one element so we can avoid situations where
+ // we don't have a frame to parse into anymore.
+ if (frame.containerNesting == 0 && this._frames.length > 1) {
+ this._frames.pop();
+ }
+
+ // We have to check for some attributes to see if this is a "special"
+ // folder, which will have different creation rules when the end tag is
+ // processed.
+ if (aElt.hasAttribute("personal_toolbar_folder")) {
+ if (this._isImportDefaults) {
+ frame.lastContainerType = Container_Toolbar;
+ }
+ } else if (aElt.hasAttribute("bookmarks_menu")) {
+ if (this._isImportDefaults) {
+ frame.lastContainerType = Container_Menu;
+ }
+ } else if (aElt.hasAttribute("unfiled_bookmarks_folder")) {
+ if (this._isImportDefaults) {
+ frame.lastContainerType = Container_Unfiled;
+ }
+ } else if (aElt.hasAttribute("places_root")) {
+ if (this._isImportDefaults) {
+ frame.lastContainerType = Container_Places;
+ }
+ } else {
+ let addDate = aElt.getAttribute("add_date");
+ if (addDate) {
+ frame.previousDateAdded =
+ this._convertImportedDateToInternalDate(addDate);
+ }
+ let modDate = aElt.getAttribute("last_modified");
+ if (modDate) {
+ frame.previousLastModifiedDate =
+ this._convertImportedDateToInternalDate(modDate);
+ }
+ }
+ this._curFrame.previousText = "";
+ },
+
+ /*
+ * Handles "<a" tags by creating a new bookmark. The title of the bookmark
+ * will be the text content, which will be stuffed in previousText for us
+ * and which will be saved by handleLinkEnd
+ */
+ _handleLinkBegin: function handleLinkBegin(aElt) {
+ let frame = this._curFrame;
+
+ // Make sure that the feed URIs from previous frames are emptied.
+ frame.previousFeed = null;
+ // Make sure that the bookmark id from previous frames are emptied.
+ frame.previousId = 0;
+ // mPreviousText will hold link text, clear it.
+ frame.previousText = "";
+
+ // Get the attributes we care about.
+ let href = this._safeTrim(aElt.getAttribute("href"));
+ let feedUrl = this._safeTrim(aElt.getAttribute("feedurl"));
+ let icon = this._safeTrim(aElt.getAttribute("icon"));
+ let iconUri = this._safeTrim(aElt.getAttribute("icon_uri"));
+ let lastCharset = this._safeTrim(aElt.getAttribute("last_charset"));
+ let keyword = this._safeTrim(aElt.getAttribute("shortcuturl"));
+ let postData = this._safeTrim(aElt.getAttribute("post_data"));
+ let webPanel = this._safeTrim(aElt.getAttribute("web_panel"));
+ let dateAdded = this._safeTrim(aElt.getAttribute("add_date"));
+ let lastModified = this._safeTrim(aElt.getAttribute("last_modified"));
+ let tags = this._safeTrim(aElt.getAttribute("tags"));
+
+ // For feeds, get the feed URL. If it is invalid, mPreviousFeed will be
+ // NULL and we'll create it as a normal bookmark.
+ if (feedUrl) {
+ frame.previousFeed = NetUtil.newURI(feedUrl);
+ }
+
+ // Ignore <a> tags that have no href.
+ if (href) {
+ // Save the address if it's valid. Note that we ignore errors if this is a
+ // feed since href is optional for them.
+ try {
+ frame.previousLink = NetUtil.newURI(href);
+ } catch (e) {
+ if (!frame.previousFeed) {
+ frame.previousLink = null;
+ return;
+ }
+ }
+ } else {
+ frame.previousLink = null;
+ // The exception is for feeds, where the href is an optional component
+ // indicating the source web site.
+ if (!frame.previousFeed) {
+ return;
+ }
+ }
+
+ // Save bookmark's last modified date.
+ if (lastModified) {
+ frame.previousLastModifiedDate =
+ this._convertImportedDateToInternalDate(lastModified);
+ }
+
+ // If this is a live bookmark, we will handle it in HandleLinkEnd(), so we
+ // can skip bookmark creation.
+ if (frame.previousFeed) {
+ return;
+ }
+
+ // Create the bookmark. The title is unknown for now, we will set it later.
+ try {
+ frame.previousId =
+ PlacesUtils.bookmarks.insertBookmark(frame.containerId,
+ frame.previousLink,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aTitle */ "",
+ /* aGuid */ null,
+ this._source);
+ } catch (e) {
+ return;
+ }
+
+ // Set the date added value, if we have it.
+ if (dateAdded) {
+ try {
+ PlacesUtils.bookmarks.setItemDateAdded(frame.previousId,
+ this._convertImportedDateToInternalDate(dateAdded), this._source);
+ } catch (e) {
+ }
+ }
+
+ // Adds tags to the URI, if there are any.
+ if (tags) {
+ try {
+ let tagsArray = tags.split(",");
+ PlacesUtils.tagging.tagURI(frame.previousLink, tagsArray, this._source);
+ } catch (e) {
+ }
+ }
+
+ // Save the favicon.
+ if (icon || iconUri) {
+ let iconUriObject;
+ try {
+ iconUriObject = NetUtil.newURI(iconUri);
+ } catch (e) {
+ }
+ if (icon || iconUriObject) {
+ try {
+ this._setFaviconForURI(frame.previousLink, iconUriObject, icon);
+ } catch (e) {
+ }
+ }
+ }
+
+ // Save the keyword.
+ if (keyword) {
+ let kwPromise = PlacesUtils.keywords.insert({ keyword,
+ url: frame.previousLink.spec,
+ postData,
+ source: this._source });
+ this._importPromises.push(kwPromise);
+ }
+
+ // Set load-in-sidebar annotation for the bookmark.
+ if (webPanel && webPanel.toLowerCase() == "true") {
+ try {
+ PlacesUtils.annotations.setItemAnnotation(frame.previousId,
+ LOAD_IN_SIDEBAR_ANNO,
+ 1,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ this._source);
+ } catch (e) {
+ }
+ }
+
+ // Import last charset.
+ if (lastCharset) {
+ let chPromise = PlacesUtils.setCharsetForURI(frame.previousLink, lastCharset, this._source);
+ this._importPromises.push(chPromise);
+ }
+ },
+
+ _handleContainerBegin: function handleContainerBegin() {
+ this._curFrame.containerNesting++;
+ },
+
+ /**
+ * Our "indent" count has decreased, and when we hit 0 that means that this
+ * container is complete and we need to pop back to the outer frame. Never
+ * pop the toplevel frame
+ */
+ _handleContainerEnd: function handleContainerEnd() {
+ let frame = this._curFrame;
+ if (frame.containerNesting > 0)
+ frame.containerNesting --;
+ if (this._frames.length > 1 && frame.containerNesting == 0) {
+ // we also need to re-set the imported last-modified date here. Otherwise
+ // the addition of items will override the imported field.
+ let prevFrame = this._previousFrame;
+ if (prevFrame.previousLastModifiedDate > 0) {
+ PlacesUtils.bookmarks.setItemLastModified(frame.containerId,
+ prevFrame.previousLastModifiedDate,
+ this._source);
+ }
+ this._frames.pop();
+ }
+ },
+
+ /**
+ * Creates the new frame for this heading now that we know the name of the
+ * container (tokens since the heading open tag will have been placed in
+ * previousText).
+ */
+ _handleHeadEnd: function handleHeadEnd() {
+ this._newFrame();
+ },
+
+ /**
+ * Saves the title for the given bookmark.
+ */
+ _handleLinkEnd: function handleLinkEnd() {
+ let frame = this._curFrame;
+ frame.previousText = frame.previousText.trim();
+
+ try {
+ if (frame.previousFeed) {
+ // The is a live bookmark. We create it here since in HandleLinkBegin we
+ // don't know the title.
+ let lmPromise = PlacesUtils.livemarks.addLivemark({
+ "title": frame.previousText,
+ "parentId": frame.containerId,
+ "index": PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "feedURI": frame.previousFeed,
+ "siteURI": frame.previousLink,
+ "source": this._source,
+ });
+ this._importPromises.push(lmPromise);
+ } else if (frame.previousLink) {
+ // This is a common bookmark.
+ PlacesUtils.bookmarks.setItemTitle(frame.previousId,
+ frame.previousText,
+ this._source);
+ }
+ } catch (e) {
+ }
+
+
+ // Set last modified date as the last change.
+ if (frame.previousId > 0 && frame.previousLastModifiedDate > 0) {
+ try {
+ PlacesUtils.bookmarks.setItemLastModified(frame.previousId,
+ frame.previousLastModifiedDate,
+ this._source);
+ } catch (e) {
+ }
+ // Note: don't clear previousLastModifiedDate, because if this item has a
+ // description, we'll need to set it again.
+ }
+
+ frame.previousText = "";
+
+ },
+
+ _openContainer: function openContainer(aElt) {
+ if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
+ return;
+ }
+ switch (aElt.localName) {
+ case "h1":
+ this._handleHead1Begin(aElt);
+ break;
+ case "h2":
+ case "h3":
+ case "h4":
+ case "h5":
+ case "h6":
+ this._handleHeadBegin(aElt);
+ break;
+ case "a":
+ this._handleLinkBegin(aElt);
+ break;
+ case "dl":
+ case "ul":
+ case "menu":
+ this._handleContainerBegin();
+ break;
+ case "dd":
+ this._curFrame.inDescription = true;
+ break;
+ case "hr":
+ this._handleSeparator(aElt);
+ break;
+ }
+ },
+
+ _closeContainer: function closeContainer(aElt) {
+ let frame = this._curFrame;
+
+ // see the comment for the definition of inDescription. Basically, we commit
+ // any text in previousText to the description of the node/folder if there
+ // is any.
+ if (frame.inDescription) {
+ // NOTE ES5 trim trims more than the previous C++ trim.
+ frame.previousText = frame.previousText.trim(); // important
+ if (frame.previousText) {
+
+ let itemId = !frame.previousLink ? frame.containerId
+ : frame.previousId;
+
+ try {
+ if (!PlacesUtils.annotations.itemHasAnnotation(itemId, DESCRIPTION_ANNO)) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ DESCRIPTION_ANNO,
+ frame.previousText,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ this._source);
+ }
+ } catch (e) {
+ }
+ frame.previousText = "";
+
+ // Set last-modified a 2nd time for all items with descriptions
+ // we need to set last-modified as the *last* step in processing
+ // any item type in the bookmarks.html file, so that we do
+ // not overwrite the imported value. for items without descriptions,
+ // setting this value after setting the item title is that
+ // last point at which we can save this value before it gets reset.
+ // for items with descriptions, it must set after that point.
+ // however, at the point at which we set the title, there's no way
+ // to determine if there will be a description following,
+ // so we need to set the last-modified-date at both places.
+
+ let lastModified;
+ if (!frame.previousLink) {
+ lastModified = this._previousFrame.previousLastModifiedDate;
+ } else {
+ lastModified = frame.previousLastModifiedDate;
+ }
+
+ if (itemId > 0 && lastModified > 0) {
+ PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified,
+ this._source);
+ }
+ }
+ frame.inDescription = false;
+ }
+
+ if (aElt.namespaceURI != "http://www.w3.org/1999/xhtml") {
+ return;
+ }
+ switch (aElt.localName) {
+ case "dl":
+ case "ul":
+ case "menu":
+ this._handleContainerEnd();
+ break;
+ case "dt":
+ break;
+ case "h1":
+ // ignore
+ break;
+ case "h2":
+ case "h3":
+ case "h4":
+ case "h5":
+ case "h6":
+ this._handleHeadEnd();
+ break;
+ case "a":
+ this._handleLinkEnd();
+ break;
+ default:
+ break;
+ }
+ },
+
+ _appendText: function appendText(str) {
+ this._curFrame.previousText += str;
+ },
+
+ /**
+ * data is a string that is a data URI for the favicon. Our job is to
+ * decode it and store it in the favicon service.
+ *
+ * When aIconURI is non-null, we will use that as the URI of the favicon
+ * when storing in the favicon service.
+ *
+ * When aIconURI is null, we have to make up a URI for this favicon so that
+ * it can be stored in the service. The real one will be set the next time
+ * the user visits the page. Our made up one should get expired when the
+ * page no longer references it.
+ */
+ _setFaviconForURI: function setFaviconForURI(aPageURI, aIconURI, aData) {
+ // if the input favicon URI is a chrome: URI, then we just save it and don't
+ // worry about data
+ if (aIconURI) {
+ if (aIconURI.schemeIs("chrome")) {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, aIconURI, false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ return;
+ }
+ }
+
+ // some bookmarks have placeholder URIs that contain just "data:"
+ // ignore these
+ if (aData.length <= 5) {
+ return;
+ }
+
+ let faviconURI;
+ if (aIconURI) {
+ faviconURI = aIconURI;
+ } else {
+ // Make up a favicon URI for this page. Later, we'll make sure that this
+ // favicon URI is always associated with local favicon data, so that we
+ // don't load this URI from the network.
+ let faviconSpec = "http://www.mozilla.org/2005/made-up-favicon/"
+ + serialNumber
+ + "-"
+ + new Date().getTime();
+ faviconURI = NetUtil.newURI(faviconSpec);
+ serialNumber++;
+ }
+
+ // This could fail if the favicon is bigger than defined limit, in such a
+ // case neither the favicon URI nor the favicon data will be saved. If the
+ // bookmark is visited again later, the URI and data will be fetched.
+ PlacesUtils.favicons.replaceFaviconDataFromDataURL(faviconURI, aData, 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ PlacesUtils.favicons.setAndFetchFaviconForPage(aPageURI, faviconURI, false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ },
+
+ /**
+ * Converts a string date in seconds to an int date in microseconds
+ */
+ _convertImportedDateToInternalDate: function convertImportedDateToInternalDate(aDate) {
+ if (aDate && !isNaN(aDate)) {
+ return parseInt(aDate) * 1000000; // in bookmarks.html this value is in seconds, not microseconds
+ }
+ return Date.now();
+ },
+
+ runBatched: function runBatched(aDoc) {
+ if (!aDoc) {
+ return;
+ }
+
+ if (this._isImportDefaults) {
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarksMenuFolderId, this._source);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId, this._source);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId, this._source);
+ }
+
+ let current = aDoc;
+ let next;
+ for (;;) {
+ switch (current.nodeType) {
+ case Ci.nsIDOMNode.ELEMENT_NODE:
+ this._openContainer(current);
+ break;
+ case Ci.nsIDOMNode.TEXT_NODE:
+ this._appendText(current.data);
+ break;
+ }
+ if ((next = current.firstChild)) {
+ current = next;
+ continue;
+ }
+ for (;;) {
+ if (current.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+ this._closeContainer(current);
+ }
+ if (current == aDoc) {
+ return;
+ }
+ if ((next = current.nextSibling)) {
+ current = next;
+ break;
+ }
+ current = current.parentNode;
+ }
+ }
+ },
+
+ _walkTreeForImport: function walkTreeForImport(aDoc) {
+ PlacesUtils.bookmarks.runInBatchMode(this, aDoc);
+ },
+
+ importFromURL: Task.async(function* (href) {
+ this._importPromises = [];
+ yield new Promise((resolve, reject) => {
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.onload = () => {
+ try {
+ this._walkTreeForImport(xhr.responseXML);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ };
+ xhr.onabort = xhr.onerror = xhr.ontimeout = () => {
+ reject(new Error("xmlhttprequest failed"));
+ };
+ xhr.open("GET", href);
+ xhr.responseType = "document";
+ xhr.overrideMimeType("text/html");
+ xhr.send();
+ });
+ // TODO (bug 1095427) once converted to the new bookmarks API, methods will
+ // yield, so this hack should not be needed anymore.
+ try {
+ yield Promise.all(this._importPromises);
+ } finally {
+ delete this._importPromises;
+ }
+ }),
+};
+
+function BookmarkExporter(aBookmarksTree) {
+ // Create a map of the roots.
+ let rootsMap = new Map();
+ for (let child of aBookmarksTree.children) {
+ if (child.root)
+ rootsMap.set(child.root, child);
+ }
+
+ // For backwards compatibility reasons the bookmarks menu is the root, while
+ // the bookmarks toolbar and unfiled bookmarks will be child items.
+ this._root = rootsMap.get("bookmarksMenuFolder");
+
+ for (let key of [ "toolbarFolder", "unfiledBookmarksFolder" ]) {
+ let root = rootsMap.get(key);
+ if (root.children && root.children.length > 0) {
+ if (!this._root.children)
+ this._root.children = [];
+ this._root.children.push(root);
+ }
+ }
+}
+
+BookmarkExporter.prototype = {
+ exportToFile: function exportToFile(aFilePath) {
+ return Task.spawn(function* () {
+ // Create a file that can be accessed by the current user only.
+ let out = FileUtils.openAtomicFileOutputStream(new FileUtils.File(aFilePath));
+ try {
+ // We need a buffered output stream for performance. See bug 202477.
+ let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]
+ .createInstance(Ci.nsIBufferedOutputStream);
+ bufferedOut.init(out, 4096);
+ try {
+ // Write bookmarks in UTF-8.
+ this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Ci.nsIConverterOutputStream);
+ this._converterOut.init(bufferedOut, "utf-8", 0, 0);
+ try {
+ this._writeHeader();
+ yield this._writeContainer(this._root);
+ // Retain the target file on success only.
+ bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
+ } finally {
+ this._converterOut.close();
+ this._converterOut = null;
+ }
+ } finally {
+ bufferedOut.close();
+ }
+ } finally {
+ out.close();
+ }
+ }.bind(this));
+ },
+
+ _converterOut: null,
+
+ _write: function (aText) {
+ this._converterOut.writeString(aText || "");
+ },
+
+ _writeAttribute: function (aName, aValue) {
+ this._write(' ' + aName + '="' + aValue + '"');
+ },
+
+ _writeLine: function (aText) {
+ this._write(aText + "\n");
+ },
+
+ _writeHeader: function () {
+ this._writeLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
+ this._writeLine("<!-- This is an automatically generated file.");
+ this._writeLine(" It will be read and overwritten.");
+ this._writeLine(" DO NOT EDIT! -->");
+ this._writeLine('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; ' +
+ 'charset=UTF-8">');
+ this._writeLine("<TITLE>Bookmarks</TITLE>");
+ },
+
+ *_writeContainer(aItem, aIndent = "") {
+ if (aItem == this._root) {
+ this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
+ this._writeLine("");
+ }
+ else {
+ this._write(aIndent + "<DT><H3");
+ this._writeDateAttributes(aItem);
+
+ if (aItem.root === "toolbarFolder")
+ this._writeAttribute("PERSONAL_TOOLBAR_FOLDER", "true");
+ else if (aItem.root === "unfiledBookmarksFolder")
+ this._writeAttribute("UNFILED_BOOKMARKS_FOLDER", "true");
+ this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</H3>");
+ }
+
+ this._writeDescription(aItem, aIndent);
+
+ this._writeLine(aIndent + "<DL><p>");
+ if (aItem.children)
+ yield this._writeContainerContents(aItem, aIndent);
+ if (aItem == this._root)
+ this._writeLine(aIndent + "</DL>");
+ else
+ this._writeLine(aIndent + "</DL><p>");
+ },
+
+ *_writeContainerContents(aItem, aIndent) {
+ let localIndent = aIndent + EXPORT_INDENT;
+
+ for (let child of aItem.children) {
+ if (child.annos && child.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
+ this._writeLivemark(child, localIndent);
+ } else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
+ yield this._writeContainer(child, localIndent);
+ } else if (child.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
+ this._writeSeparator(child, localIndent);
+ } else {
+ yield this._writeItem(child, localIndent);
+ }
+ }
+ },
+
+ _writeSeparator: function (aItem, aIndent) {
+ this._write(aIndent + "<HR");
+ // We keep exporting separator titles, but don't support them anymore.
+ if (aItem.title)
+ this._writeAttribute("NAME", escapeHtmlEntities(aItem.title));
+ this._write(">");
+ },
+
+ _writeLivemark: function (aItem, aIndent) {
+ this._write(aIndent + "<DT><A");
+ let feedSpec = aItem.annos.find(anno => anno.name == PlacesUtils.LMANNO_FEEDURI).value;
+ this._writeAttribute("FEEDURL", escapeUrl(feedSpec));
+ let siteSpecAnno = aItem.annos.find(anno => anno.name == PlacesUtils.LMANNO_SITEURI);
+ if (siteSpecAnno)
+ this._writeAttribute("HREF", escapeUrl(siteSpecAnno.value));
+ this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
+ this._writeDescription(aItem, aIndent);
+ },
+
+ *_writeItem(aItem, aIndent) {
+ try {
+ NetUtil.newURI(aItem.uri);
+ } catch (ex) {
+ // If the item URI is invalid, skip the item instead of failing later.
+ return;
+ }
+
+ this._write(aIndent + "<DT><A");
+ this._writeAttribute("HREF", escapeUrl(aItem.uri));
+ this._writeDateAttributes(aItem);
+ yield this._writeFaviconAttribute(aItem);
+
+ if (aItem.keyword) {
+ this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(aItem.keyword));
+ if (aItem.postData)
+ this._writeAttribute("POST_DATA", escapeHtmlEntities(aItem.postData));
+ }
+
+ if (aItem.annos && aItem.annos.some(anno => anno.name == LOAD_IN_SIDEBAR_ANNO))
+ this._writeAttribute("WEB_PANEL", "true");
+ if (aItem.charset)
+ this._writeAttribute("LAST_CHARSET", escapeHtmlEntities(aItem.charset));
+ if (aItem.tags)
+ this._writeAttribute("TAGS", aItem.tags);
+ this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
+ this._writeDescription(aItem, aIndent);
+ },
+
+ _writeDateAttributes: function (aItem) {
+ if (aItem.dateAdded)
+ this._writeAttribute("ADD_DATE",
+ Math.floor(aItem.dateAdded / MICROSEC_PER_SEC));
+ if (aItem.lastModified)
+ this._writeAttribute("LAST_MODIFIED",
+ Math.floor(aItem.lastModified / MICROSEC_PER_SEC));
+ },
+
+ *_writeFaviconAttribute(aItem) {
+ if (!aItem.iconuri)
+ return;
+ let favicon;
+ try {
+ favicon = yield PlacesUtils.promiseFaviconData(aItem.uri);
+ } catch (ex) {
+ Components.utils.reportError("Unexpected Error trying to fetch icon data");
+ return;
+ }
+
+ this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
+
+ if (!favicon.uri.schemeIs("chrome") && favicon.dataLen > 0) {
+ let faviconContents = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, favicon.data));
+ this._writeAttribute("ICON", faviconContents);
+ }
+ },
+
+ _writeDescription: function (aItem, aIndent) {
+ let descriptionAnno = aItem.annos &&
+ aItem.annos.find(anno => anno.name == DESCRIPTION_ANNO);
+ if (descriptionAnno)
+ this._writeLine(aIndent + "<DD>" + escapeHtmlEntities(descriptionAnno.value));
+ }
+};
diff --git a/toolkit/components/places/BookmarkJSONUtils.jsm b/toolkit/components/places/BookmarkJSONUtils.jsm
new file mode 100644
index 0000000000..7f8d3fd8f5
--- /dev/null
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -0,0 +1,589 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
+XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
+
+/**
+ * Generates an hash for the given string.
+ *
+ * @note The generated hash is returned in base64 form. Mind the fact base64
+ * is case-sensitive if you are going to reuse this code.
+ */
+function generateHash(aString) {
+ let cryptoHash = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ cryptoHash.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stringStream.data = aString;
+ cryptoHash.updateFromStream(stringStream, -1);
+ // base64 allows the '/' char, but we can't use it for filenames.
+ return cryptoHash.finish(true).replace(/\//g, "-");
+}
+
+this.BookmarkJSONUtils = Object.freeze({
+ /**
+ * Import bookmarks from a url.
+ *
+ * @param aSpec
+ * url of the bookmark data.
+ * @param aReplace
+ * Boolean if true, replace existing bookmarks, else merge.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ */
+ importFromURL: function BJU_importFromURL(aSpec, aReplace) {
+ return Task.spawn(function* () {
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
+ try {
+ let importer = new BookmarkImporter(aReplace);
+ yield importer.importFromURL(aSpec);
+
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
+ } catch (ex) {
+ Cu.reportError("Failed to restore bookmarks from " + aSpec + ": " + ex);
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
+ }
+ });
+ },
+
+ /**
+ * Restores bookmarks and tags from a JSON file.
+ * @note any item annotated with "places/excludeFromBackup" won't be removed
+ * before executing the restore.
+ *
+ * @param aFilePath
+ * OS.File path string of bookmarks in JSON or JSONlz4 format to be restored.
+ * @param aReplace
+ * Boolean if true, replace existing bookmarks, else merge.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ importFromFile: function BJU_importFromFile(aFilePath, aReplace) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.importFromFile " +
+ "is deprecated. Please use an OS.File path string instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+
+ return Task.spawn(function* () {
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_BEGIN);
+ try {
+ if (!(yield OS.File.exists(aFilePath)))
+ throw new Error("Cannot restore from nonexisting json file");
+
+ let importer = new BookmarkImporter(aReplace);
+ if (aFilePath.endsWith("jsonlz4")) {
+ yield importer.importFromCompressedFile(aFilePath);
+ } else {
+ yield importer.importFromURL(OS.Path.toFileURI(aFilePath));
+ }
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_SUCCESS);
+ } catch (ex) {
+ Cu.reportError("Failed to restore bookmarks from " + aFilePath + ": " + ex);
+ notifyObservers(PlacesUtils.TOPIC_BOOKMARKS_RESTORE_FAILED);
+ throw ex;
+ }
+ });
+ },
+
+ /**
+ * Serializes bookmarks using JSON, and writes to the supplied file path.
+ *
+ * @param aFilePath
+ * OS.File path string for the bookmarks file to be created.
+ * @param [optional] aOptions
+ * Object containing options for the export:
+ * - failIfHashIs: if the generated file would have the same hash
+ * defined here, will reject with ex.becauseSameHash
+ * - compress: if true, writes file using lz4 compression
+ * @return {Promise}
+ * @resolves once the file has been created, to an object with the
+ * following properties:
+ * - count: number of exported bookmarks
+ * - hash: file hash for contents comparison
+ * @rejects JavaScript exception.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ exportToFile: function BJU_exportToFile(aFilePath, aOptions={}) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " +
+ "is deprecated. Please use an OS.File path string instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+ return Task.spawn(function* () {
+ let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
+ let startTime = Date.now();
+ let jsonString = JSON.stringify(bookmarks);
+ // Report the time taken to convert the tree to JSON.
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_BACKUPS_TOJSON_MS")
+ .add(Date.now() - startTime);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ }
+
+ let hash = generateHash(jsonString);
+
+ if (hash === aOptions.failIfHashIs) {
+ let e = new Error("Hash conflict");
+ e.becauseSameHash = true;
+ throw e;
+ }
+
+ // Do not write to the tmp folder, otherwise if it has a different
+ // filesystem writeAtomic will fail. Eventual dangling .tmp files should
+ // be cleaned up by the caller.
+ let writeOptions = { tmpPath: OS.Path.join(aFilePath + ".tmp") };
+ if (aOptions.compress)
+ writeOptions.compression = "lz4";
+
+ yield OS.File.writeAtomic(aFilePath, jsonString, writeOptions);
+ return { count: count, hash: hash };
+ });
+ }
+});
+
+function BookmarkImporter(aReplace) {
+ this._replace = aReplace;
+ // The bookmark change source, used to determine the sync status and change
+ // counter.
+ this._source = aReplace ? PlacesUtils.bookmarks.SOURCE_IMPORT_REPLACE :
+ PlacesUtils.bookmarks.SOURCE_IMPORT;
+}
+BookmarkImporter.prototype = {
+ /**
+ * Import bookmarks from a url.
+ *
+ * @param aSpec
+ * url of the bookmark data.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ */
+ importFromURL(spec) {
+ return new Promise((resolve, reject) => {
+ let streamObserver = {
+ onStreamComplete: (aLoader, aContext, aStatus, aLength, aResult) => {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ try {
+ let jsonString = converter.convertFromByteArray(aResult,
+ aResult.length);
+ resolve(this.importFromJSON(jsonString));
+ } catch (ex) {
+ Cu.reportError("Failed to import from URL: " + ex);
+ reject(ex);
+ }
+ }
+ };
+
+ let uri = NetUtil.newURI(spec);
+ let channel = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true
+ });
+ let streamLoader = Cc["@mozilla.org/network/stream-loader;1"]
+ .createInstance(Ci.nsIStreamLoader);
+ streamLoader.init(streamObserver);
+ channel.asyncOpen2(streamLoader);
+ });
+ },
+
+ /**
+ * Import bookmarks from a compressed file.
+ *
+ * @param aFilePath
+ * OS.File path string of the bookmark data.
+ *
+ * @return {Promise}
+ * @resolves When the new bookmarks have been created.
+ * @rejects JavaScript exception.
+ */
+ importFromCompressedFile: function* BI_importFromCompressedFile(aFilePath) {
+ let aResult = yield OS.File.read(aFilePath, { compression: "lz4" });
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let jsonString = converter.convertFromByteArray(aResult, aResult.length);
+ yield this.importFromJSON(jsonString);
+ },
+
+ /**
+ * Import bookmarks from a JSON string.
+ *
+ * @param aString
+ * JSON string of serialized bookmark data.
+ */
+ importFromJSON: Task.async(function* (aString) {
+ this._importPromises = [];
+ let deferred = PromiseUtils.defer();
+ let nodes =
+ PlacesUtils.unwrapNodes(aString, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+
+ if (nodes.length == 0 || !nodes[0].children ||
+ nodes[0].children.length == 0) {
+ deferred.resolve(); // Nothing to restore
+ } else {
+ // Ensure tag folder gets processed last
+ nodes[0].children.sort(function sortRoots(aNode, bNode) {
+ if (aNode.root && aNode.root == "tagsFolder")
+ return 1;
+ if (bNode.root && bNode.root == "tagsFolder")
+ return -1;
+ return 0;
+ });
+
+ let batch = {
+ nodes: nodes[0].children,
+ runBatched: function runBatched() {
+ if (this._replace) {
+ // Get roots excluded from the backup, we will not remove them
+ // before restoring.
+ let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
+ PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
+ // Delete existing children of the root node, excepting:
+ // 1. special folders: delete the child nodes
+ // 2. tags folder: untag via the tagging api
+ let root = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
+ false, false).root;
+ let childIds = [];
+ for (let i = 0; i < root.childCount; i++) {
+ let childId = root.getChild(i).itemId;
+ if (!excludeItems.includes(childId) &&
+ childId != PlacesUtils.tagsFolderId) {
+ childIds.push(childId);
+ }
+ }
+ root.containerOpen = false;
+
+ for (let i = 0; i < childIds.length; i++) {
+ let rootItemId = childIds[i];
+ if (PlacesUtils.isRootItem(rootItemId)) {
+ PlacesUtils.bookmarks.removeFolderChildren(rootItemId,
+ this._source);
+ } else {
+ PlacesUtils.bookmarks.removeItem(rootItemId, this._source);
+ }
+ }
+ }
+
+ let searchIds = [];
+ let folderIdMap = [];
+
+ for (let node of batch.nodes) {
+ if (!node.children || node.children.length == 0)
+ continue; // Nothing to restore for this root
+
+ if (node.root) {
+ let container = PlacesUtils.placesRootId; // Default to places root
+ switch (node.root) {
+ case "bookmarksMenuFolder":
+ container = PlacesUtils.bookmarksMenuFolderId;
+ break;
+ case "tagsFolder":
+ container = PlacesUtils.tagsFolderId;
+ break;
+ case "unfiledBookmarksFolder":
+ container = PlacesUtils.unfiledBookmarksFolderId;
+ break;
+ case "toolbarFolder":
+ container = PlacesUtils.toolbarFolderId;
+ break;
+ case "mobileFolder":
+ container = PlacesUtils.mobileFolderId;
+ break;
+ }
+
+ // Insert the data into the db
+ for (let child of node.children) {
+ let index = child.index;
+ let [folders, searches] =
+ this.importJSONNode(child, container, index, 0);
+ for (let i = 0; i < folders.length; i++) {
+ if (folders[i])
+ folderIdMap[i] = folders[i];
+ }
+ searchIds = searchIds.concat(searches);
+ }
+ } else {
+ let [folders, searches] = this.importJSONNode(
+ node, PlacesUtils.placesRootId, node.index, 0);
+ for (let i = 0; i < folders.length; i++) {
+ if (folders[i])
+ folderIdMap[i] = folders[i];
+ }
+ searchIds = searchIds.concat(searches);
+ }
+ }
+
+ // Fixup imported place: uris that contain folders
+ for (let id of searchIds) {
+ let oldURI = PlacesUtils.bookmarks.getBookmarkURI(id);
+ let uri = fixupQuery(oldURI, folderIdMap);
+ if (!uri.equals(oldURI)) {
+ PlacesUtils.bookmarks.changeBookmarkURI(id, uri, this._source);
+ }
+ }
+
+ deferred.resolve();
+ }.bind(this)
+ };
+
+ PlacesUtils.bookmarks.runInBatchMode(batch, null);
+ }
+ yield deferred.promise;
+ // TODO (bug 1095426) once converted to the new bookmarks API, methods will
+ // yield, so this hack should not be needed anymore.
+ try {
+ yield Promise.all(this._importPromises);
+ } finally {
+ delete this._importPromises;
+ }
+ }),
+
+ /**
+ * Takes a JSON-serialized node and inserts it into the db.
+ *
+ * @param aData
+ * The unwrapped data blob of dropped or pasted data.
+ * @param aContainer
+ * The container the data was dropped or pasted into
+ * @param aIndex
+ * The index within the container the item was dropped or pasted at
+ * @return an array containing of maps of old folder ids to new folder ids,
+ * and an array of saved search ids that need to be fixed up.
+ * eg: [[[oldFolder1, newFolder1]], [search1]]
+ */
+ importJSONNode: function BI_importJSONNode(aData, aContainer, aIndex,
+ aGrandParentId) {
+ let folderIdMap = [];
+ let searchIds = [];
+ let id = -1;
+ switch (aData.type) {
+ case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER:
+ if (aContainer == PlacesUtils.tagsFolderId) {
+ // Node is a tag
+ if (aData.children) {
+ for (let child of aData.children) {
+ try {
+ PlacesUtils.tagging.tagURI(
+ NetUtil.newURI(child.uri), [aData.title], this._source);
+ } catch (ex) {
+ // Invalid tag child, skip it
+ }
+ }
+ return [folderIdMap, searchIds];
+ }
+ } else if (aData.annos &&
+ aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
+ // Node is a livemark
+ let feedURI = null;
+ let siteURI = null;
+ aData.annos = aData.annos.filter(function(aAnno) {
+ switch (aAnno.name) {
+ case PlacesUtils.LMANNO_FEEDURI:
+ feedURI = NetUtil.newURI(aAnno.value);
+ return false;
+ case PlacesUtils.LMANNO_SITEURI:
+ siteURI = NetUtil.newURI(aAnno.value);
+ return false;
+ default:
+ return true;
+ }
+ });
+
+ if (feedURI) {
+ let lmPromise = PlacesUtils.livemarks.addLivemark({
+ title: aData.title,
+ feedURI: feedURI,
+ parentId: aContainer,
+ index: aIndex,
+ lastModified: aData.lastModified,
+ siteURI: siteURI,
+ guid: aData.guid,
+ source: this._source
+ }).then(aLivemark => {
+ let id = aLivemark.id;
+ if (aData.dateAdded)
+ PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
+ this._source);
+ if (aData.annos && aData.annos.length)
+ PlacesUtils.setAnnotationsForItem(id, aData.annos,
+ this._source);
+ });
+ this._importPromises.push(lmPromise);
+ }
+ } else {
+ let isMobileFolder = aData.annos &&
+ aData.annos.some(anno => anno.name == PlacesUtils.MOBILE_ROOT_ANNO);
+ if (isMobileFolder) {
+ // Mobile bookmark folders are special: we move their children to
+ // the mobile root instead of importing them. We also rewrite
+ // queries to use the special folder ID, and ignore generic
+ // properties like timestamps and annotations set on the folder.
+ id = PlacesUtils.mobileFolderId;
+ } else {
+ // For other folders, set `id` so that we can import timestamps
+ // and annotations at the end of this function.
+ id = PlacesUtils.bookmarks.createFolder(
+ aContainer, aData.title, aIndex, aData.guid, this._source);
+ }
+ folderIdMap[aData.id] = id;
+ // Process children
+ if (aData.children) {
+ for (let i = 0; i < aData.children.length; i++) {
+ let child = aData.children[i];
+ let [folders, searches] =
+ this.importJSONNode(child, id, i, aContainer);
+ for (let j = 0; j < folders.length; j++) {
+ if (folders[j])
+ folderIdMap[j] = folders[j];
+ }
+ searchIds = searchIds.concat(searches);
+ }
+ }
+ }
+ break;
+ case PlacesUtils.TYPE_X_MOZ_PLACE:
+ id = PlacesUtils.bookmarks.insertBookmark(
+ aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title, aData.guid, this._source);
+ if (aData.keyword) {
+ // POST data could be set in 2 ways:
+ // 1. new backups have a postData property
+ // 2. old backups have an item annotation
+ let postDataAnno = aData.annos &&
+ aData.annos.find(anno => anno.name == PlacesUtils.POST_DATA_ANNO);
+ let postData = aData.postData || (postDataAnno && postDataAnno.value);
+ let kwPromise = PlacesUtils.keywords.insert({ keyword: aData.keyword,
+ url: aData.uri,
+ postData,
+ source: this._source });
+ this._importPromises.push(kwPromise);
+ }
+ if (aData.tags) {
+ let tags = aData.tags.split(",").filter(aTag =>
+ aTag.length <= Ci.nsITaggingService.MAX_TAG_LENGTH);
+ if (tags.length) {
+ try {
+ PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags, this._source);
+ } catch (ex) {
+ // Invalid tag child, skip it.
+ Cu.reportError(`Unable to set tags "${tags.join(", ")}" for ${aData.uri}: ${ex}`);
+ }
+ }
+ }
+ if (aData.charset) {
+ PlacesUtils.annotations.setPageAnnotation(
+ NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
+ 0, Ci.nsIAnnotationService.EXPIRE_NEVER);
+ }
+ if (aData.uri.substr(0, 6) == "place:")
+ searchIds.push(id);
+ if (aData.icon) {
+ try {
+ // Create a fake faviconURI to use (FIXME: bug 523932)
+ let faviconURI = NetUtil.newURI("fake-favicon-uri:" + aData.uri);
+ PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+ faviconURI, aData.icon, 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ NetUtil.newURI(aData.uri), faviconURI, false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ } catch (ex) {
+ Components.utils.reportError("Failed to import favicon data:" + ex);
+ }
+ }
+ if (aData.iconUri) {
+ try {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ NetUtil.newURI(aData.uri), NetUtil.newURI(aData.iconUri), false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ } catch (ex) {
+ Components.utils.reportError("Failed to import favicon URI:" + ex);
+ }
+ }
+ break;
+ case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
+ id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex, aData.guid, this._source);
+ break;
+ default:
+ // Unknown node type
+ }
+
+ // Set generic properties, valid for all nodes except tags and the mobile
+ // root.
+ if (id != -1 && id != PlacesUtils.mobileFolderId &&
+ aContainer != PlacesUtils.tagsFolderId &&
+ aGrandParentId != PlacesUtils.tagsFolderId) {
+ if (aData.dateAdded)
+ PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
+ this._source);
+ if (aData.lastModified)
+ PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified,
+ this._source);
+ if (aData.annos && aData.annos.length)
+ PlacesUtils.setAnnotationsForItem(id, aData.annos, this._source);
+ }
+
+ return [folderIdMap, searchIds];
+ }
+}
+
+function notifyObservers(topic) {
+ Services.obs.notifyObservers(null, topic, "json");
+}
+
+/**
+ * Replaces imported folder ids with their local counterparts in a place: URI.
+ *
+ * @param aURI
+ * A place: URI with folder ids.
+ * @param aFolderIdMap
+ * An array mapping old folder id to new folder ids.
+ * @returns the fixed up URI if all matched. If some matched, it returns
+ * the URI with only the matching folders included. If none matched
+ * it returns the input URI unchanged.
+ */
+function fixupQuery(aQueryURI, aFolderIdMap) {
+ let convert = function(str, p1, offset, s) {
+ return "folder=" + aFolderIdMap[p1];
+ }
+ let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
+
+ return NetUtil.newURI(stringURI);
+}
diff --git a/toolkit/components/places/Bookmarks.jsm b/toolkit/components/places/Bookmarks.jsm
new file mode 100644
index 0000000000..835b4fc620
--- /dev/null
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -0,0 +1,1536 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * This module provides an asynchronous API for managing bookmarks.
+ *
+ * Bookmarks are organized in a tree structure, and include URLs, folders and
+ * separators. Multiple bookmarks for the same URL are allowed.
+ *
+ * Note that if you are handling bookmarks operations in the UI, you should
+ * not use this API directly, but rather use PlacesTransactions.jsm, so that
+ * any operation is undo/redo-able.
+ *
+ * Each bookmark-item is represented by an object having the following
+ * properties:
+ *
+ * - guid (string)
+ * The globally unique identifier of the item.
+ * - parentGuid (string)
+ * The globally unique identifier of the folder containing the item.
+ * This will be an empty string for the Places root folder.
+ * - index (number)
+ * The 0-based position of the item in the parent folder.
+ * - dateAdded (Date)
+ * The time at which the item was added.
+ * - lastModified (Date)
+ * The time at which the item was last modified.
+ * - type (number)
+ * The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
+ *
+ * The following properties are only valid for URLs or folders.
+ *
+ * - title (string)
+ * The item's title, if any. Empty titles and null titles are considered
+ * the same, and the property is unset on retrieval in such a case.
+ * Titles longer than DB_TITLE_LENGTH_MAX will be truncated.
+ *
+ * The following properties are only valid for URLs:
+ *
+ * - url (URL, href or nsIURI)
+ * The item's URL. Note that while input objects can contains either
+ * an URL object, an href string, or an nsIURI, output objects will always
+ * contain an URL object.
+ * An URL cannot be longer than DB_URL_LENGTH_MAX, methods will throw if a
+ * longer value is provided.
+ *
+ * Each successful operation notifies through the nsINavBookmarksObserver
+ * interface. To listen to such notifications you must register using
+ * nsINavBookmarksService addObserver and removeObserver methods.
+ * Note that bookmark addition or order changes won't notify onItemMoved for
+ * items that have their indexes changed.
+ * Similarly, lastModified changes not done explicitly (like changing another
+ * property) won't fire an onItemChanged notification for the lastModified
+ * property.
+ * @see nsINavBookmarkObserver
+ */
+
+this.EXPORTED_SYMBOLS = [ "Bookmarks" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesSyncUtils",
+ "resource://gre/modules/PlacesSyncUtils.jsm");
+
+const MATCH_ANYWHERE_UNMODIFIED = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
+const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+
+var Bookmarks = Object.freeze({
+ /**
+ * Item's type constants.
+ * These should stay consistent with nsINavBookmarksService.idl
+ */
+ TYPE_BOOKMARK: 1,
+ TYPE_FOLDER: 2,
+ TYPE_SEPARATOR: 3,
+
+ /**
+ * Default index used to append a bookmark-item at the end of a folder.
+ * This should stay consistent with nsINavBookmarksService.idl
+ */
+ DEFAULT_INDEX: -1,
+
+ /**
+ * Bookmark change source constants, passed as optional properties and
+ * forwarded to observers. See nsINavBookmarksService.idl for an explanation.
+ */
+ SOURCES: {
+ DEFAULT: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ SYNC: Ci.nsINavBookmarksService.SOURCE_SYNC,
+ IMPORT: Ci.nsINavBookmarksService.SOURCE_IMPORT,
+ IMPORT_REPLACE: Ci.nsINavBookmarksService.SOURCE_IMPORT_REPLACE,
+ },
+
+ /**
+ * Special GUIDs associated with bookmark roots.
+ * It's guaranteed that the roots will always have these guids.
+ */
+
+ rootGuid: "root________",
+ menuGuid: "menu________",
+ toolbarGuid: "toolbar_____",
+ unfiledGuid: "unfiled_____",
+ mobileGuid: "mobile______",
+
+ // With bug 424160, tags will stop being bookmarks, thus this root will
+ // be removed. Do not rely on this, rather use the tagging service API.
+ tagsGuid: "tags________",
+
+ /**
+ * Inserts a bookmark-item into the bookmarks tree.
+ *
+ * For creating a bookmark, the following set of properties is required:
+ * - type
+ * - parentGuid
+ * - url, only for bookmarked URLs
+ *
+ * If an index is not specified, it defaults to appending.
+ * It's also possible to pass a non-existent GUID to force creation of an
+ * item with the given GUID, but unless you have a very sound reason, such as
+ * an undo manager implementation or synchronization, don't do that.
+ *
+ * Note that any known properties that don't apply to the specific item type
+ * cause an exception.
+ *
+ * @param info
+ * object representing a bookmark-item.
+ *
+ * @return {Promise} resolved when the creation is complete.
+ * @resolves to an object representing the created bookmark.
+ * @rejects if it's not possible to create the requested bookmark.
+ * @throws if the arguments are invalid.
+ */
+ insert(info) {
+ // Ensure to use the same date for dateAdded and lastModified, even if
+ // dateAdded may be imposed by the caller.
+ let time = (info && info.dateAdded) || new Date();
+ let insertInfo = validateBookmarkObject(info,
+ { type: { defaultValue: this.TYPE_BOOKMARK }
+ , index: { defaultValue: this.DEFAULT_INDEX }
+ , url: { requiredIf: b => b.type == this.TYPE_BOOKMARK
+ , validIf: b => b.type == this.TYPE_BOOKMARK }
+ , parentGuid: { required: true }
+ , title: { validIf: b => [ this.TYPE_BOOKMARK
+ , this.TYPE_FOLDER ].includes(b.type) }
+ , dateAdded: { defaultValue: time
+ , validIf: b => !b.lastModified ||
+ b.dateAdded <= b.lastModified }
+ , lastModified: { defaultValue: time,
+ validIf: b => (!b.dateAdded && b.lastModified >= time) ||
+ (b.dateAdded && b.lastModified >= b.dateAdded) }
+ , source: { defaultValue: this.SOURCES.DEFAULT }
+ });
+
+ return Task.spawn(function* () {
+ // Ensure the parent exists.
+ let parent = yield fetchBookmark({ guid: insertInfo.parentGuid });
+ if (!parent)
+ throw new Error("parentGuid must be valid");
+
+ // Set index in the appending case.
+ if (insertInfo.index == this.DEFAULT_INDEX ||
+ insertInfo.index > parent._childCount) {
+ insertInfo.index = parent._childCount;
+ }
+
+ let item = yield insertBookmark(insertInfo, parent);
+
+ // Notify onItemAdded to listeners.
+ let observers = PlacesUtils.bookmarks.getObservers();
+ // We need the itemId to notify, though once the switch to guids is
+ // complete we may stop using it.
+ let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
+ let itemId = yield PlacesUtils.promiseItemId(item.guid);
+
+ // Pass tagging information for the observers to skip over these notifications when needed.
+ let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
+ let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
+ notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
+ item.type, uri, item.title || null,
+ PlacesUtils.toPRTime(item.dateAdded), item.guid,
+ item.parentGuid, item.source ],
+ { isTagging: isTagging || isTagsFolder });
+
+ // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
+ if (isTagging) {
+ for (let entry of (yield fetchBookmarksByURL(item))) {
+ notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+ PlacesUtils.toPRTime(entry.lastModified),
+ entry.type, entry._parentId,
+ entry.guid, entry.parentGuid,
+ "", item.source ]);
+ }
+ }
+
+ // Remove non-enumerable properties.
+ delete item.source;
+ return Object.assign({}, item);
+ }.bind(this));
+ },
+
+ /**
+ * Updates a bookmark-item.
+ *
+ * Only set the properties which should be changed (undefined properties
+ * won't be taken into account).
+ * Moreover, the item's type or dateAdded cannot be changed, since they are
+ * immutable after creation. Trying to change them will reject.
+ *
+ * Note that any known properties that don't apply to the specific item type
+ * cause an exception.
+ *
+ * @param info
+ * object representing a bookmark-item, as defined above.
+ *
+ * @return {Promise} resolved when the update is complete.
+ * @resolves to an object representing the updated bookmark.
+ * @rejects if it's not possible to update the given bookmark.
+ * @throws if the arguments are invalid.
+ */
+ update(info) {
+ // The info object is first validated here to ensure it's consistent, then
+ // it's compared to the existing item to remove any properties that don't
+ // need to be updated.
+ let updateInfo = validateBookmarkObject(info,
+ { guid: { required: true }
+ , index: { requiredIf: b => b.hasOwnProperty("parentGuid")
+ , validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX }
+ , source: { defaultValue: this.SOURCES.DEFAULT }
+ });
+
+ // There should be at last one more property in addition to guid and source.
+ if (Object.keys(updateInfo).length < 3)
+ throw new Error("Not enough properties to update");
+
+ return Task.spawn(function* () {
+ // Ensure the item exists.
+ let item = yield fetchBookmark(updateInfo);
+ if (!item)
+ throw new Error("No bookmarks found for the provided GUID");
+ if (updateInfo.hasOwnProperty("type") && updateInfo.type != item.type)
+ throw new Error("The bookmark type cannot be changed");
+ if (updateInfo.hasOwnProperty("dateAdded") &&
+ updateInfo.dateAdded.getTime() != item.dateAdded.getTime())
+ throw new Error("The bookmark dateAdded cannot be changed");
+
+ // Remove any property that will stay the same.
+ removeSameValueProperties(updateInfo, item);
+ // Check if anything should still be updated.
+ if (Object.keys(updateInfo).length < 3) {
+ // Remove non-enumerable properties.
+ return Object.assign({}, item);
+ }
+
+ updateInfo = validateBookmarkObject(updateInfo,
+ { url: { validIf: () => item.type == this.TYPE_BOOKMARK }
+ , title: { validIf: () => [ this.TYPE_BOOKMARK
+ , this.TYPE_FOLDER ].includes(item.type) }
+ , lastModified: { defaultValue: new Date()
+ , validIf: b => b.lastModified >= item.dateAdded }
+ });
+
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: update",
+ Task.async(function*(db) {
+ let parent;
+ if (updateInfo.hasOwnProperty("parentGuid")) {
+ if (item.type == this.TYPE_FOLDER) {
+ // Make sure we are not moving a folder into itself or one of its
+ // descendants.
+ let rows = yield db.executeCached(
+ `WITH RECURSIVE
+ descendants(did) AS (
+ VALUES(:id)
+ UNION ALL
+ SELECT id FROM moz_bookmarks
+ JOIN descendants ON parent = did
+ WHERE type = :type
+ )
+ SELECT guid FROM moz_bookmarks
+ WHERE id IN descendants
+ `, { id: item._id, type: this.TYPE_FOLDER });
+ if (rows.map(r => r.getResultByName("guid")).includes(updateInfo.parentGuid))
+ throw new Error("Cannot insert a folder into itself or one of its descendants");
+ }
+
+ parent = yield fetchBookmark({ guid: updateInfo.parentGuid });
+ if (!parent)
+ throw new Error("No bookmarks found for the provided parentGuid");
+ }
+
+ if (updateInfo.hasOwnProperty("index")) {
+ // If at this point we don't have a parent yet, we are moving into
+ // the same container. Thus we know it exists.
+ if (!parent)
+ parent = yield fetchBookmark({ guid: item.parentGuid });
+
+ if (updateInfo.index >= parent._childCount ||
+ updateInfo.index == this.DEFAULT_INDEX) {
+ updateInfo.index = parent._childCount;
+
+ // Fix the index when moving within the same container.
+ if (parent.guid == item.parentGuid)
+ updateInfo.index--;
+ }
+ }
+
+ let updatedItem = yield updateBookmark(updateInfo, item, parent);
+
+ if (item.type == this.TYPE_BOOKMARK &&
+ item.url.href != updatedItem.url.href) {
+ // ...though we don't wait for the calculation.
+ updateFrecency(db, [item.url]).then(null, Cu.reportError);
+ updateFrecency(db, [updatedItem.url]).then(null, Cu.reportError);
+ }
+
+ // Notify onItemChanged to listeners.
+ let observers = PlacesUtils.bookmarks.getObservers();
+ // For lastModified, we only care about the original input, since we
+ // should not notify implciit lastModified changes.
+ if (info.hasOwnProperty("lastModified") &&
+ updateInfo.hasOwnProperty("lastModified") &&
+ item.lastModified != updatedItem.lastModified) {
+ notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
+ false,
+ `${PlacesUtils.toPRTime(updatedItem.lastModified)}`,
+ PlacesUtils.toPRTime(updatedItem.lastModified),
+ updatedItem.type,
+ updatedItem._parentId,
+ updatedItem.guid,
+ updatedItem.parentGuid, "",
+ updatedItem.source ]);
+ }
+ if (updateInfo.hasOwnProperty("title")) {
+ notify(observers, "onItemChanged", [ updatedItem._id, "title",
+ false, updatedItem.title,
+ PlacesUtils.toPRTime(updatedItem.lastModified),
+ updatedItem.type,
+ updatedItem._parentId,
+ updatedItem.guid,
+ updatedItem.parentGuid, "",
+ updatedItem.source ]);
+ }
+ if (updateInfo.hasOwnProperty("url")) {
+ notify(observers, "onItemChanged", [ updatedItem._id, "uri",
+ false, updatedItem.url.href,
+ PlacesUtils.toPRTime(updatedItem.lastModified),
+ updatedItem.type,
+ updatedItem._parentId,
+ updatedItem.guid,
+ updatedItem.parentGuid,
+ item.url.href,
+ updatedItem.source ]);
+ }
+ // If the item was moved, notify onItemMoved.
+ if (item.parentGuid != updatedItem.parentGuid ||
+ item.index != updatedItem.index) {
+ notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
+ item.index, updatedItem._parentId,
+ updatedItem.index, updatedItem.type,
+ updatedItem.guid, item.parentGuid,
+ updatedItem.parentGuid,
+ updatedItem.source ]);
+ }
+
+ // Remove non-enumerable properties.
+ delete updatedItem.source;
+ return Object.assign({}, updatedItem);
+ }.bind(this)));
+ }.bind(this));
+ },
+
+ /**
+ * Removes a bookmark-item.
+ *
+ * @param guidOrInfo
+ * The globally unique identifier of the item to remove, or an
+ * object representing it, as defined above.
+ * @param {Object} [options={}]
+ * Additional options that can be passed to the function.
+ * Currently supports the following properties:
+ * - preventRemovalOfNonEmptyFolders: Causes an exception to be
+ * thrown when attempting to remove a folder that is not empty.
+ * - source: The change source, forwarded to all bookmark observers.
+ * Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
+ *
+ * @return {Promise} resolved when the removal is complete.
+ * @resolves to an object representing the removed bookmark.
+ * @rejects if the provided guid doesn't match any existing bookmark.
+ * @throws if the arguments are invalid.
+ */
+ remove(guidOrInfo, options={}) {
+ let info = guidOrInfo;
+ if (!info)
+ throw new Error("Input should be a valid object");
+ if (typeof(guidOrInfo) != "object")
+ info = { guid: guidOrInfo };
+
+ // Disallow removing the root folders.
+ if ([this.rootGuid, this.menuGuid, this.toolbarGuid, this.unfiledGuid,
+ this.tagsGuid, this.mobileGuid].includes(info.guid)) {
+ throw new Error("It's not possible to remove Places root folders.");
+ }
+
+ // Even if we ignore any other unneeded property, we still validate any
+ // known property to reduce likelihood of hidden bugs.
+ let removeInfo = validateBookmarkObject(info);
+
+ return Task.spawn(function* () {
+ let item = yield fetchBookmark(removeInfo);
+ if (!item)
+ throw new Error("No bookmarks found for the provided GUID.");
+
+ item = yield removeBookmark(item, options);
+
+ // Notify onItemRemoved to listeners.
+ let { source = Bookmarks.SOURCES.DEFAULT } = options;
+ let observers = PlacesUtils.bookmarks.getObservers();
+ let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
+ let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
+ notify(observers, "onItemRemoved", [ item._id, item._parentId, item.index,
+ item.type, uri, item.guid,
+ item.parentGuid,
+ source ],
+ { isTagging: isUntagging });
+
+ if (isUntagging) {
+ for (let entry of (yield fetchBookmarksByURL(item))) {
+ notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+ PlacesUtils.toPRTime(entry.lastModified),
+ entry.type, entry._parentId,
+ entry.guid, entry.parentGuid,
+ "", source ]);
+ }
+ }
+
+ // Remove non-enumerable properties.
+ return Object.assign({}, item);
+ });
+ },
+
+ /**
+ * Removes ALL bookmarks, resetting the bookmarks storage to an empty tree.
+ *
+ * Note that roots are preserved, only their children will be removed.
+ *
+ * @param {Object} [options={}]
+ * Additional options. Currently supports the following properties:
+ * - source: The change source, forwarded to all bookmark observers.
+ * Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
+ *
+ * @return {Promise} resolved when the removal is complete.
+ * @resolves once the removal is complete.
+ */
+ eraseEverything: function(options={}) {
+ const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid,
+ this.mobileGuid];
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
+ db => db.executeTransaction(function* () {
+ yield removeFoldersContents(db, folderGuids, options);
+ const time = PlacesUtils.toPRTime(new Date());
+ for (let folderGuid of folderGuids) {
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET lastModified = :time
+ WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
+ `, { folderGuid, time });
+ }
+ })
+ );
+ },
+
+ /**
+ * Returns a list of recently bookmarked items.
+ *
+ * @param {integer} numberOfItems
+ * The maximum number of bookmark items to return.
+ *
+ * @return {Promise} resolved when the listing is complete.
+ * @resolves to an array of recent bookmark-items.
+ * @rejects if an error happens while querying.
+ */
+ getRecent(numberOfItems) {
+ if (numberOfItems === undefined) {
+ throw new Error("numberOfItems argument is required");
+ }
+ if (!typeof numberOfItems === 'number' || (numberOfItems % 1) !== 0) {
+ throw new Error("numberOfItems argument must be an integer");
+ }
+ if (numberOfItems <= 0) {
+ throw new Error("numberOfItems argument must be greater than zero");
+ }
+
+ return Task.spawn(function* () {
+ return yield fetchRecentBookmarks(numberOfItems);
+ });
+ },
+
+ /**
+ * Fetches information about a bookmark-item.
+ *
+ * REMARK: any successful call to this method resolves to a single
+ * bookmark-item (or null), even when multiple bookmarks may exist
+ * (e.g. fetching by url). If you wish to retrieve all of the
+ * bookmarks for a given match, use the callback instead.
+ *
+ * Input can be either a guid or an object with one, and only one, of these
+ * filtering properties set:
+ * - guid
+ * retrieves the item with the specified guid.
+ * - parentGuid and index
+ * retrieves the item by its position.
+ * - url
+ * retrieves the most recent bookmark having the given URL.
+ * To retrieve ALL of the bookmarks for that URL, you must pass in an
+ * onResult callback, that will be invoked once for each found bookmark.
+ *
+ * @param guidOrInfo
+ * The globally unique identifier of the item to fetch, or an
+ * object representing it, as defined above.
+ * @param onResult [optional]
+ * Callback invoked for each found bookmark.
+ *
+ * @return {Promise} resolved when the fetch is complete.
+ * @resolves to an object representing the found item, as described above, or
+ * an array of such objects. if no item is found, the returned
+ * promise is resolved to null.
+ * @rejects if an error happens while fetching.
+ * @throws if the arguments are invalid.
+ *
+ * @note Any unknown property in the info object is ignored. Known properties
+ * may be overwritten.
+ */
+ fetch(guidOrInfo, onResult=null) {
+ if (onResult && typeof onResult != "function")
+ throw new Error("onResult callback must be a valid function");
+ let info = guidOrInfo;
+ if (!info)
+ throw new Error("Input should be a valid object");
+ if (typeof(info) != "object")
+ info = { guid: guidOrInfo };
+
+ // Only one condition at a time can be provided.
+ let conditionsCount = [
+ v => v.hasOwnProperty("guid"),
+ v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
+ v => v.hasOwnProperty("url")
+ ].reduce((old, fn) => old + fn(info)|0, 0);
+ if (conditionsCount != 1)
+ throw new Error(`Unexpected number of conditions provided: ${conditionsCount}`);
+
+ // Even if we ignore any other unneeded property, we still validate any
+ // known property to reduce likelihood of hidden bugs.
+ let fetchInfo = validateBookmarkObject(info,
+ { parentGuid: { requiredIf: b => b.hasOwnProperty("index") }
+ , index: { requiredIf: b => b.hasOwnProperty("parentGuid")
+ , validIf: b => typeof(b.index) == "number" &&
+ b.index >= 0 || b.index == this.DEFAULT_INDEX }
+ });
+
+ return Task.spawn(function* () {
+ let results;
+ if (fetchInfo.hasOwnProperty("guid"))
+ results = yield fetchBookmark(fetchInfo);
+ else if (fetchInfo.hasOwnProperty("parentGuid") && fetchInfo.hasOwnProperty("index"))
+ results = yield fetchBookmarkByPosition(fetchInfo);
+ else if (fetchInfo.hasOwnProperty("url"))
+ results = yield fetchBookmarksByURL(fetchInfo);
+
+ if (!results)
+ return null;
+
+ if (!Array.isArray(results))
+ results = [results];
+ // Remove non-enumerable properties.
+ results = results.map(r => Object.assign({}, r));
+
+ // Ideally this should handle an incremental behavior and thus be invoked
+ // while we fetch. Though, the likelihood of 2 or more bookmarks for the
+ // same match is very low, so it's not worth the added code complication.
+ if (onResult) {
+ for (let result of results) {
+ try {
+ onResult(result);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ return results[0];
+ });
+ },
+
+ /**
+ * Retrieves an object representation of a bookmark-item, along with all of
+ * its descendants, if any.
+ *
+ * Each node in the tree is an object that extends the item representation
+ * described above with some additional properties:
+ *
+ * - [deprecated] id (number)
+ * the item's id. Defined only if aOptions.includeItemIds is set.
+ * - annos (array)
+ * the item's annotations. This is not set if there are no annotations
+ * set for the item.
+ *
+ * The root object of the tree also has the following properties set:
+ * - itemsCount (number, not enumerable)
+ * the number of items, including the root item itself, which are
+ * represented in the resolved object.
+ *
+ * Bookmarked URLs may also have the following properties:
+ * - tags (string)
+ * csv string of the bookmark's tags, if any.
+ * - charset (string)
+ * the last known charset of the bookmark, if any.
+ * - iconurl (URL)
+ * the bookmark's favicon URL, if any.
+ *
+ * Folders may also have the following properties:
+ * - children (array)
+ * the folder's children information, each of them having the same set of
+ * properties as above.
+ *
+ * @param [optional] guid
+ * the topmost item to be queried. If it's not passed, the Places
+ * root folder is queried: that is, you get a representation of the
+ * entire bookmarks hierarchy.
+ * @param [optional] options
+ * Options for customizing the query behavior, in the form of an
+ * object with any of the following properties:
+ * - excludeItemsCallback: a function for excluding items, along with
+ * their descendants. Given an item object (that has everything set
+ * apart its potential children data), it should return true if the
+ * item should be excluded. Once an item is excluded, the function
+ * isn't called for any of its descendants. This isn't called for
+ * the root item.
+ * WARNING: since the function may be called for each item, using
+ * this option can slow down the process significantly if the
+ * callback does anything that's not relatively trivial. It is
+ * highly recommended to avoid any synchronous I/O or DB queries.
+ * - includeItemIds: opt-in to include the deprecated id property.
+ * Use it if you must. It'll be removed once the switch to guids is
+ * complete.
+ *
+ * @return {Promise} resolved when the fetch is complete.
+ * @resolves to an object that represents either a single item or a
+ * bookmarks tree. if guid points to a non-existent item, the
+ * returned promise is resolved to null.
+ * @rejects if an error happens while fetching.
+ * @throws if the arguments are invalid.
+ */
+ // TODO must implement these methods yet:
+ // PlacesUtils.promiseBookmarksTree()
+ fetchTree(guid = "", options = {}) {
+ throw new Error("Not yet implemented");
+ },
+
+ /**
+ * Reorders contents of a folder based on a provided array of GUIDs.
+ *
+ * @param parentGuid
+ * The globally unique identifier of the folder whose contents should
+ * be reordered.
+ * @param orderedChildrenGuids
+ * Ordered array of the children's GUIDs. If this list contains
+ * non-existing entries they will be ignored. If the list is
+ * incomplete, missing entries will be appended.
+ * @param {Object} [options={}]
+ * Additional options. Currently supports the following properties:
+ * - source: The change source, forwarded to all bookmark observers.
+ * Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
+ *
+ * @return {Promise} resolved when reordering is complete.
+ * @rejects if an error happens while reordering.
+ * @throws if the arguments are invalid.
+ */
+ reorder(parentGuid, orderedChildrenGuids, options={}) {
+ let info = { guid: parentGuid, source: this.SOURCES.DEFAULT };
+ info = validateBookmarkObject(info, { guid: { required: true } });
+
+ if (!Array.isArray(orderedChildrenGuids) || !orderedChildrenGuids.length)
+ throw new Error("Must provide a sorted array of children GUIDs.");
+ try {
+ orderedChildrenGuids.forEach(PlacesUtils.BOOKMARK_VALIDATORS.guid);
+ } catch (ex) {
+ throw new Error("Invalid GUID found in the sorted children array.");
+ }
+
+ return Task.spawn(function* () {
+ let parent = yield fetchBookmark(info);
+ if (!parent || parent.type != this.TYPE_FOLDER)
+ throw new Error("No folder found for the provided GUID.");
+
+ let sortedChildren = yield reorderChildren(parent, orderedChildrenGuids);
+
+ let { source = Ci.nsINavBookmarksService.SOURCE_DEFAULT } = options;
+ let observers = PlacesUtils.bookmarks.getObservers();
+ // Note that child.index is the old index.
+ for (let i = 0; i < sortedChildren.length; ++i) {
+ let child = sortedChildren[i];
+ notify(observers, "onItemMoved", [ child._id, child._parentId,
+ child.index, child._parentId,
+ i, child.type,
+ child.guid, child.parentGuid,
+ child.parentGuid,
+ source ]);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Searches a list of bookmark-items by a search term, url or title.
+ *
+ * IMPORTANT:
+ * This is intended as an interim API for the web-extensions implementation.
+ * It will be removed as soon as we have a new querying API.
+ *
+ * If you just want to search bookmarks by URL, use .fetch() instead.
+ *
+ * @param query
+ * Either a string to use as search term, or an object
+ * containing any of these keys: query, title or url with the
+ * corresponding string to match as value.
+ * The url property can be either a string or an nsIURI.
+ *
+ * @return {Promise} resolved when the search is complete.
+ * @resolves to an array of found bookmark-items.
+ * @rejects if an error happens while searching.
+ * @throws if the arguments are invalid.
+ *
+ * @note Any unknown property in the query object is ignored.
+ * Known properties may be overwritten.
+ */
+ search(query) {
+ if (!query) {
+ throw new Error("Query object is required");
+ }
+ if (typeof query === "string") {
+ query = { query: query };
+ }
+ if (typeof query !== "object") {
+ throw new Error("Query must be an object or a string");
+ }
+ if (query.query && typeof query.query !== "string") {
+ throw new Error("Query option must be a string");
+ }
+ if (query.title && typeof query.title !== "string") {
+ throw new Error("Title option must be a string");
+ }
+
+ if (query.url) {
+ if (typeof query.url === "string" || (query.url instanceof URL)) {
+ query.url = new URL(query.url).href;
+ } else if (query.url instanceof Ci.nsIURI) {
+ query.url = query.url.spec;
+ } else {
+ throw new Error("Url option must be a string or a URL object");
+ }
+ }
+
+ return Task.spawn(function* () {
+ let results = yield queryBookmarks(query);
+
+ return results;
+ });
+ },
+});
+
+// Globals.
+
+/**
+ * Sends a bookmarks notification through the given observers.
+ *
+ * @param observers
+ * array of nsINavBookmarkObserver objects.
+ * @param notification
+ * the notification name.
+ * @param args
+ * array of arguments to pass to the notification.
+ * @param information
+ * Information about the notification, so we can filter based
+ * based on the observer's preferences.
+ */
+function notify(observers, notification, args, information = {}) {
+ for (let observer of observers) {
+ if (information.isTagging && observer.skipTags) {
+ continue;
+ }
+
+ if (information.isDescendantRemoval && observer.skipDescendantsOnItemRemoval) {
+ continue;
+ }
+
+ try {
+ observer[notification](...args);
+ } catch (ex) {}
+ }
+}
+
+// Update implementation.
+
+function updateBookmark(info, item, newParent) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
+ Task.async(function*(db) {
+
+ let tuples = new Map();
+ if (info.hasOwnProperty("lastModified"))
+ tuples.set("lastModified", { value: PlacesUtils.toPRTime(info.lastModified) });
+ if (info.hasOwnProperty("title"))
+ tuples.set("title", { value: info.title });
+
+ yield db.executeTransaction(function* () {
+ if (info.hasOwnProperty("url")) {
+ // Ensure a page exists in moz_places for this URL.
+ yield maybeInsertPlace(db, info.url);
+ // Update tuples for the update query.
+ tuples.set("url", { value: info.url.href
+ , fragment: "fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)" });
+ }
+
+ if (newParent) {
+ // For simplicity, update the index regardless.
+ let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
+ tuples.set("position", { value: newIndex });
+
+ if (newParent.guid == item.parentGuid) {
+ // Moving inside the original container.
+ // When moving "up", add 1 to each index in the interval.
+ // Otherwise when moving down, we subtract 1.
+ let sign = newIndex < item.index ? +1 : -1;
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET position = position + :sign
+ WHERE parent = :newParentId
+ AND position BETWEEN :lowIndex AND :highIndex
+ `, { sign: sign, newParentId: newParent._id,
+ lowIndex: Math.min(item.index, newIndex),
+ highIndex: Math.max(item.index, newIndex) });
+ } else {
+ // Moving across different containers.
+ tuples.set("parent", { value: newParent._id} );
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET position = position + :sign
+ WHERE parent = :oldParentId
+ AND position >= :oldIndex
+ `, { sign: -1, oldParentId: item._parentId, oldIndex: item.index });
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET position = position + :sign
+ WHERE parent = :newParentId
+ AND position >= :newIndex
+ `, { sign: +1, newParentId: newParent._id, newIndex: newIndex });
+
+ yield setAncestorsLastModified(db, item.parentGuid, info.lastModified);
+ }
+ yield setAncestorsLastModified(db, newParent.guid, info.lastModified);
+ }
+
+ yield db.executeCached(
+ `UPDATE moz_bookmarks
+ SET ${Array.from(tuples.keys()).map(v => tuples.get(v).fragment || `${v} = :${v}`).join(", ")}
+ WHERE guid = :guid
+ `, Object.assign({ guid: info.guid },
+ [...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
+ });
+
+ // If the parent changed, update related non-enumerable properties.
+ let additionalParentInfo = {};
+ if (newParent) {
+ Object.defineProperty(additionalParentInfo, "_parentId",
+ { value: newParent._id, enumerable: false });
+ Object.defineProperty(additionalParentInfo, "_grandParentId",
+ { value: newParent._parentId, enumerable: false });
+ }
+
+ let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
+
+ // Don't return an empty title to the caller.
+ if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
+ delete updatedItem.title;
+
+ return updatedItem;
+ }));
+}
+
+// Insert implementation.
+
+function insertBookmark(item, parent) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmark",
+ Task.async(function*(db) {
+
+ // If a guid was not provided, generate one, so we won't need to fetch the
+ // bookmark just after having created it.
+ if (!item.hasOwnProperty("guid"))
+ item.guid = (yield db.executeCached("SELECT GENERATE_GUID() AS guid"))[0].getResultByName("guid");
+
+ yield db.executeTransaction(function* transaction() {
+ if (item.type == Bookmarks.TYPE_BOOKMARK) {
+ // Ensure a page exists in moz_places for this URL.
+ // The IGNORE conflict can trigger on `guid`.
+ yield maybeInsertPlace(db, item.url);
+ }
+
+ // Adjust indices.
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET position = position + 1
+ WHERE parent = :parent
+ AND position >= :index
+ `, { parent: parent._id, index: item.index });
+
+ // Insert the bookmark into the database.
+ yield db.executeCached(
+ `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
+ dateAdded, lastModified, guid)
+ VALUES ((SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url), :type, :parent,
+ :index, :title, :date_added, :last_modified, :guid)
+ `, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
+ type: item.type, parent: parent._id, index: item.index,
+ title: item.title, date_added: PlacesUtils.toPRTime(item.dateAdded),
+ last_modified: PlacesUtils.toPRTime(item.lastModified), guid: item.guid });
+
+ yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
+ });
+
+ // If not a tag recalculate frecency...
+ let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
+ if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
+ // ...though we don't wait for the calculation.
+ updateFrecency(db, [item.url]).then(null, Cu.reportError);
+ }
+
+ // Don't return an empty title to the caller.
+ if (item.hasOwnProperty("title") && item.title === null)
+ delete item.title;
+
+ return item;
+ }));
+}
+
+// Query implementation.
+
+function queryBookmarks(info) {
+ let queryParams = {tags_folder: PlacesUtils.tagsFolderId};
+ // we're searching for bookmarks, so exclude tags
+ let queryString = "WHERE p.parent <> :tags_folder";
+
+ if (info.title) {
+ queryString += " AND b.title = :title";
+ queryParams.title = info.title;
+ }
+
+ if (info.url) {
+ queryString += " AND h.url_hash = hash(:url) AND h.url = :url";
+ queryParams.url = info.url;
+ }
+
+ if (info.query) {
+ queryString += " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior) ";
+ queryParams.query = info.query;
+ queryParams.matchBehavior = MATCH_ANYWHERE_UNMODIFIED;
+ queryParams.searchBehavior = BEHAVIOR_BOOKMARK;
+ }
+
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks",
+ Task.async(function*(db) {
+
+ // _id, _childCount, _grandParentId and _parentId fields
+ // are required to be in the result by the converting function
+ // hence setting them to NULL
+ let rows = yield db.executeCached(
+ `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+ b.dateAdded, b.lastModified, b.type, b.title,
+ h.url AS url, b.parent, p.parent,
+ NULL AS _id,
+ NULL AS _childCount,
+ NULL AS _grandParentId,
+ NULL AS _parentId
+ FROM moz_bookmarks b
+ LEFT JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON h.id = b.fk
+ ${queryString}
+ `, queryParams);
+
+ return rowsToItemsArray(rows);
+ }));
+}
+
+
+// Fetch implementation.
+
+function fetchBookmark(info) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmark",
+ Task.async(function*(db) {
+
+ let rows = yield db.executeCached(
+ `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+ b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+ b.id AS _id, b.parent AS _parentId,
+ (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+ p.parent AS _grandParentId
+ FROM moz_bookmarks b
+ LEFT JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON h.id = b.fk
+ WHERE b.guid = :guid
+ `, { guid: info.guid });
+
+ return rows.length ? rowsToItemsArray(rows)[0] : null;
+ }));
+}
+
+function fetchBookmarkByPosition(info) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarkByPosition",
+ Task.async(function*(db) {
+ let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
+
+ let rows = yield db.executeCached(
+ `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+ b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+ b.id AS _id, b.parent AS _parentId,
+ (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+ p.parent AS _grandParentId
+ FROM moz_bookmarks b
+ LEFT JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON h.id = b.fk
+ WHERE p.guid = :parentGuid
+ AND b.position = IFNULL(:index, (SELECT count(*) - 1
+ FROM moz_bookmarks
+ WHERE parent = p.id))
+ `, { parentGuid: info.parentGuid, index });
+
+ return rows.length ? rowsToItemsArray(rows)[0] : null;
+ }));
+}
+
+function fetchBookmarksByURL(info) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByURL",
+ Task.async(function*(db) {
+
+ let rows = yield db.executeCached(
+ `/* do not warn (bug no): not worth to add an index */
+ SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+ b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+ b.id AS _id, b.parent AS _parentId,
+ (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+ p.parent AS _grandParentId
+ FROM moz_bookmarks b
+ LEFT JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON h.id = b.fk
+ WHERE h.url_hash = hash(:url) AND h.url = :url
+ AND _grandParentId <> :tags_folder
+ ORDER BY b.lastModified DESC
+ `, { url: info.url.href,
+ tags_folder: PlacesUtils.tagsFolderId });
+
+ return rows.length ? rowsToItemsArray(rows) : null;
+ }));
+}
+
+function fetchRecentBookmarks(numberOfItems) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchRecentBookmarks",
+ Task.async(function*(db) {
+
+ let rows = yield db.executeCached(
+ `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+ b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+ NULL AS _id, NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId
+ FROM moz_bookmarks b
+ LEFT JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON h.id = b.fk
+ WHERE p.parent <> :tags_folder
+ ORDER BY b.dateAdded DESC, b.ROWID DESC
+ LIMIT :numberOfItems
+ `, { tags_folder: PlacesUtils.tagsFolderId, numberOfItems });
+
+ return rows.length ? rowsToItemsArray(rows) : [];
+ }));
+}
+
+function fetchBookmarksByParent(info) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByParent",
+ Task.async(function*(db) {
+
+ let rows = yield db.executeCached(
+ `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
+ b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
+ b.id AS _id, b.parent AS _parentId,
+ (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
+ p.parent AS _grandParentId
+ FROM moz_bookmarks b
+ LEFT JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON h.id = b.fk
+ WHERE p.guid = :parentGuid
+ ORDER BY b.position ASC
+ `, { parentGuid: info.parentGuid });
+
+ return rowsToItemsArray(rows);
+ }));
+}
+
+// Remove implementation.
+
+function removeBookmark(item, options) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: removeBookmark",
+ Task.async(function*(db) {
+
+ let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
+
+ yield db.executeTransaction(function* transaction() {
+ // If it's a folder, remove its contents first.
+ if (item.type == Bookmarks.TYPE_FOLDER) {
+ if (options.preventRemovalOfNonEmptyFolders && item._childCount > 0) {
+ throw new Error("Cannot remove a non-empty folder.");
+ }
+ yield removeFoldersContents(db, [item.guid], options);
+ }
+
+ // Remove annotations first. If it's a tag, we can avoid paying that cost.
+ if (!isUntagging) {
+ // We don't go through the annotations service for this cause otherwise
+ // we'd get a pointless onItemChanged notification and it would also
+ // set lastModified to an unexpected value.
+ yield removeAnnotationsForItem(db, item._id);
+ }
+
+ // Remove the bookmark from the database.
+ yield db.executeCached(
+ `DELETE FROM moz_bookmarks WHERE guid = :guid`, { guid: item.guid });
+
+ // Fix indices in the parent.
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET position = position - 1 WHERE
+ parent = :parentId AND position > :index
+ `, { parentId: item._parentId, index: item.index });
+
+ yield setAncestorsLastModified(db, item.parentGuid, new Date());
+ });
+
+ // If not a tag recalculate frecency...
+ if (item.type == Bookmarks.TYPE_BOOKMARK && !isUntagging) {
+ // ...though we don't wait for the calculation.
+ updateFrecency(db, [item.url]).then(null, Cu.reportError);
+ }
+
+ return item;
+ }));
+}
+
+// Reorder implementation.
+
+function reorderChildren(parent, orderedChildrenGuids) {
+ return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
+ db => db.executeTransaction(function* () {
+ // Select all of the direct children for the given parent.
+ let children = yield fetchBookmarksByParent({ parentGuid: parent.guid });
+ if (!children.length)
+ return undefined;
+
+ // Build a map of GUIDs to indices for fast lookups in the comparator
+ // function.
+ let guidIndices = new Map();
+ for (let i = 0; i < orderedChildrenGuids.length; ++i) {
+ let guid = orderedChildrenGuids[i];
+ guidIndices.set(guid, i);
+ }
+
+ // Reorder the children array according to the specified order, provided
+ // GUIDs come first, others are appended in somehow random order.
+ children.sort((a, b) => {
+ // This works provided fetchBookmarksByParent returns sorted children.
+ if (!guidIndices.has(a.guid) && !guidIndices.has(b.guid)) {
+ return 0;
+ }
+ if (!guidIndices.has(a.guid)) {
+ return 1;
+ }
+ if (!guidIndices.has(b.guid)) {
+ return -1;
+ }
+ return guidIndices.get(a.guid) < guidIndices.get(b.guid) ? -1 : 1;
+ });
+
+ // Update the bookmarks position now. If any unknown guid have been
+ // inserted meanwhile, its position will be set to -position, and we'll
+ // handle it later.
+ // To do the update in a single step, we build a VALUES (guid, position)
+ // table. We then use count() in the sorting table to avoid skipping values
+ // when no more existing GUIDs have been provided.
+ let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
+ .join();
+ yield db.execute(
+ `WITH sorting(g, p) AS (
+ VALUES ${valuesTable}
+ )
+ UPDATE moz_bookmarks SET position = (
+ SELECT CASE count(*) WHEN 0 THEN -position
+ ELSE count(*) - 1
+ END
+ FROM sorting a
+ JOIN sorting b ON b.p <= a.p
+ WHERE a.g = guid
+ )
+ WHERE parent = :parentId
+ `, { parentId: parent._id});
+
+ // Update position of items that could have been inserted in the meanwhile.
+ // Since this can happen rarely and it's only done for schema coherence
+ // resonds, we won't notify about these changes.
+ yield db.executeCached(
+ `CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
+ AFTER UPDATE OF position ON moz_bookmarks
+ WHEN NEW.position = -1
+ BEGIN
+ UPDATE moz_bookmarks
+ SET position = (SELECT MAX(position) FROM moz_bookmarks
+ WHERE parent = NEW.parent) +
+ (SELECT count(*) FROM moz_bookmarks
+ WHERE parent = NEW.parent
+ AND position BETWEEN OLD.position AND -1)
+ WHERE guid = NEW.guid;
+ END
+ `);
+
+ yield db.executeCached(
+ `UPDATE moz_bookmarks SET position = -1 WHERE position < 0`);
+
+ yield db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
+
+ return children;
+ }.bind(this))
+ );
+}
+
+// Helpers.
+
+/**
+ * Merges objects into a new object, included non-enumerable properties.
+ *
+ * @param sources
+ * source objects to merge.
+ * @return a new object including all properties from the source objects.
+ */
+function mergeIntoNewObject(...sources) {
+ let dest = {};
+ for (let src of sources) {
+ for (let prop of Object.getOwnPropertyNames(src)) {
+ Object.defineProperty(dest, prop, Object.getOwnPropertyDescriptor(src, prop));
+ }
+ }
+ return dest;
+}
+
+/**
+ * Remove properties that have the same value across two bookmark objects.
+ *
+ * @param dest
+ * destination bookmark object.
+ * @param src
+ * source bookmark object.
+ * @return a cleaned up bookmark object.
+ * @note "guid" is never removed.
+ */
+function removeSameValueProperties(dest, src) {
+ for (let prop in dest) {
+ let remove = false;
+ switch (prop) {
+ case "lastModified":
+ case "dateAdded":
+ remove = src.hasOwnProperty(prop) && dest[prop].getTime() == src[prop].getTime();
+ break;
+ case "url":
+ remove = src.hasOwnProperty(prop) && dest[prop].href == src[prop].href;
+ break;
+ default:
+ remove = dest[prop] == src[prop];
+ }
+ if (remove && prop != "guid")
+ delete dest[prop];
+ }
+}
+
+/**
+ * Convert an array of mozIStorageRow objects to an array of bookmark objects.
+ *
+ * @param rows
+ * the array of mozIStorageRow objects.
+ * @return an array of bookmark objects.
+ */
+function rowsToItemsArray(rows) {
+ return rows.map(row => {
+ let item = {};
+ for (let prop of ["guid", "index", "type"]) {
+ item[prop] = row.getResultByName(prop);
+ }
+ for (let prop of ["dateAdded", "lastModified"]) {
+ item[prop] = PlacesUtils.toDate(row.getResultByName(prop));
+ }
+ for (let prop of ["title", "parentGuid", "url" ]) {
+ let val = row.getResultByName(prop);
+ if (val)
+ item[prop] = prop === "url" ? new URL(val) : val;
+ }
+ for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId"]) {
+ let val = row.getResultByName(prop);
+ if (val !== null) {
+ // These properties should not be returned to the API consumer, thus
+ // they are non-enumerable and removed through Object.assign just before
+ // the object is returned.
+ // Configurable is set to support mergeIntoNewObject overwrites.
+ Object.defineProperty(item, prop, { value: val, enumerable: false,
+ configurable: true });
+ }
+ }
+
+ return item;
+ });
+}
+
+function validateBookmarkObject(input, behavior) {
+ return PlacesUtils.validateItemProperties(
+ PlacesUtils.BOOKMARK_VALIDATORS, input, behavior);
+}
+
+/**
+ * Updates frecency for a list of URLs.
+ *
+ * @param db
+ * the Sqlite.jsm connection handle.
+ * @param urls
+ * the array of URLs to update.
+ */
+var updateFrecency = Task.async(function* (db, urls) {
+ // We just use the hashes, since updating a few additional urls won't hurt.
+ yield db.execute(
+ `UPDATE moz_places
+ SET frecency = NOTIFY_FRECENCY(
+ CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date
+ ) WHERE url_hash IN ( ${urls.map(url => `hash("${url.href}")`).join(", ")} )
+ `);
+
+ yield db.execute(
+ `UPDATE moz_places
+ SET hidden = 0
+ WHERE url_hash IN ( ${urls.map(url => `hash(${JSON.stringify(url.href)})`).join(", ")} )
+ AND frecency <> 0
+ `);
+});
+
+/**
+ * Removes any orphan annotation entries.
+ *
+ * @param db
+ * the Sqlite.jsm connection handle.
+ */
+var removeOrphanAnnotations = Task.async(function* (db) {
+ yield db.executeCached(
+ `DELETE FROM moz_items_annos
+ WHERE id IN (SELECT a.id from moz_items_annos a
+ LEFT JOIN moz_bookmarks b ON a.item_id = b.id
+ WHERE b.id ISNULL)
+ `);
+ yield db.executeCached(
+ `DELETE FROM moz_anno_attributes
+ WHERE id IN (SELECT n.id from moz_anno_attributes n
+ LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
+ LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
+ WHERE a1.id ISNULL AND a2.id ISNULL)
+ `);
+});
+
+/**
+ * Removes annotations for a given item.
+ *
+ * @param db
+ * the Sqlite.jsm connection handle.
+ * @param itemId
+ * internal id of the item for which to remove annotations.
+ */
+var removeAnnotationsForItem = Task.async(function* (db, itemId) {
+ yield db.executeCached(
+ `DELETE FROM moz_items_annos
+ WHERE item_id = :id
+ `, { id: itemId });
+ yield db.executeCached(
+ `DELETE FROM moz_anno_attributes
+ WHERE id IN (SELECT n.id from moz_anno_attributes n
+ LEFT JOIN moz_annos a1 ON a1.anno_attribute_id = n.id
+ LEFT JOIN moz_items_annos a2 ON a2.anno_attribute_id = n.id
+ WHERE a1.id ISNULL AND a2.id ISNULL)
+ `);
+});
+
+/**
+ * Updates lastModified for all the ancestors of a given folder GUID.
+ *
+ * @param db
+ * the Sqlite.jsm connection handle.
+ * @param folderGuid
+ * the GUID of the folder whose ancestors should be updated.
+ * @param time
+ * a Date object to use for the update.
+ *
+ * @note the folder itself is also updated.
+ */
+var setAncestorsLastModified = Task.async(function* (db, folderGuid, time) {
+ yield db.executeCached(
+ `WITH RECURSIVE
+ ancestors(aid) AS (
+ SELECT id FROM moz_bookmarks WHERE guid = :guid
+ UNION ALL
+ SELECT parent FROM moz_bookmarks
+ JOIN ancestors ON id = aid
+ WHERE type = :type
+ )
+ UPDATE moz_bookmarks SET lastModified = :time
+ WHERE id IN ancestors
+ `, { guid: folderGuid, type: Bookmarks.TYPE_FOLDER,
+ time: PlacesUtils.toPRTime(time) });
+});
+
+/**
+ * Remove all descendants of one or more bookmark folders.
+ *
+ * @param db
+ * the Sqlite.jsm connection handle.
+ * @param folderGuids
+ * array of folder guids.
+ */
+var removeFoldersContents =
+Task.async(function* (db, folderGuids, options) {
+ let itemsRemoved = [];
+ for (let folderGuid of folderGuids) {
+ let rows = yield db.executeCached(
+ `WITH RECURSIVE
+ descendants(did) AS (
+ SELECT b.id FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE p.guid = :folderGuid
+ UNION ALL
+ SELECT id FROM moz_bookmarks
+ JOIN descendants ON parent = did
+ )
+ SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
+ b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
+ b.lastModified, b.title, p.parent AS _grandParentId,
+ NULL AS _childCount
+ FROM descendants
+ JOIN moz_bookmarks b ON did = b.id
+ JOIN moz_bookmarks p ON p.id = b.parent
+ LEFT JOIN moz_places h ON b.fk = h.id`, { folderGuid });
+
+ itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
+
+ yield db.executeCached(
+ `WITH RECURSIVE
+ descendants(did) AS (
+ SELECT b.id FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ WHERE p.guid = :folderGuid
+ UNION ALL
+ SELECT id FROM moz_bookmarks
+ JOIN descendants ON parent = did
+ )
+ DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid });
+ }
+
+ // Cleanup orphans.
+ yield removeOrphanAnnotations(db);
+
+ // TODO (Bug 1087576): this may leave orphan tags behind.
+
+ let urls = itemsRemoved.filter(item => "url" in item).map(item => item.url);
+ updateFrecency(db, urls).then(null, Cu.reportError);
+
+ // Send onItemRemoved notifications to listeners.
+ // TODO (Bug 1087580): for the case of eraseEverything, this should send a
+ // single clear bookmarks notification rather than notifying for each
+ // bookmark.
+
+ // Notify listeners in reverse order to serve children before parents.
+ let { source = Ci.nsINavBookmarksService.SOURCE_DEFAULT } = options;
+ let observers = PlacesUtils.bookmarks.getObservers();
+ for (let item of itemsRemoved.reverse()) {
+ let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
+ notify(observers, "onItemRemoved", [ item._id, item._parentId,
+ item.index, item.type, uri,
+ item.guid, item.parentGuid,
+ source ],
+ // Notify observers that this item is being
+ // removed as a descendent.
+ { isDescendantRemoval: true });
+
+ let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
+ if (isUntagging) {
+ for (let entry of (yield fetchBookmarksByURL(item))) {
+ notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
+ PlacesUtils.toPRTime(entry.lastModified),
+ entry.type, entry._parentId,
+ entry.guid, entry.parentGuid,
+ "", source ]);
+ }
+ }
+ }
+});
+
+/**
+ * Tries to insert a new place if it doesn't exist yet.
+ * @param url
+ * A valid URL object.
+ * @return {Promise} resolved when the operation is complete.
+ */
+function maybeInsertPlace(db, url) {
+ // The IGNORE conflict can trigger on `guid`.
+ return db.executeCached(
+ `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid)
+ VALUES (:url, hash(:url), :rev_host, 0, :frecency,
+ IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
+ GENERATE_GUID()))
+ `, { url: url.href,
+ rev_host: PlacesUtils.getReversedHost(url),
+ frecency: url.protocol == "place:" ? 0 : -1 });
+}
diff --git a/toolkit/components/places/ClusterLib.js b/toolkit/components/places/ClusterLib.js
new file mode 100644
index 0000000000..ae5debff93
--- /dev/null
+++ b/toolkit/components/places/ClusterLib.js
@@ -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/. */
+
+/**
+ * Class that can run the hierarchical clustering algorithm with the given
+ * parameters.
+ *
+ * @param distance
+ * Function that should return the distance between two items.
+ * Defaults to clusterlib.euclidean_distance.
+ * @param merge
+ * Function that should take in two items and return a merged one.
+ * Defaults to clusterlib.average_linkage.
+ * @param threshold
+ * The maximum distance between two items for which their clusters
+ * can be merged.
+ */
+function HierarchicalClustering(distance, merge, threshold) {
+ this.distance = distance || clusterlib.euclidean_distance;
+ this.merge = merge || clusterlib.average_linkage;
+ this.threshold = threshold == undefined ? Infinity : threshold;
+}
+
+HierarchicalClustering.prototype = {
+ /**
+ * Run the hierarchical clustering algorithm on the given items to produce
+ * a final set of clusters. Uses the parameters set in the constructor.
+ *
+ * @param items
+ * An array of "things" to cluster - this is the domain-specific
+ * collection you're trying to cluster (colors, points, etc.)
+ * @param snapshotGap
+ * How many iterations of the clustering algorithm to wait between
+ * calling the snapshotCallback
+ * @param snapshotCallback
+ * If provided, will be called as clusters are merged to let you view
+ * the progress of the algorithm. Passed the current array of
+ * clusters, cached distances, and cached closest clusters.
+ *
+ * @return An array of merged clusters. The represented item can be
+ * found in the "item" property of the cluster.
+ */
+ cluster: function HC_cluster(items, snapshotGap, snapshotCallback) {
+ // array of all remaining clusters
+ let clusters = [];
+ // 2D matrix of distances between each pair of clusters, indexed by key
+ let distances = [];
+ // closest cluster key for each cluster, indexed by key
+ let neighbors = [];
+ // an array of all clusters, but indexed by key
+ let clustersByKey = [];
+
+ // set up clusters from the initial items array
+ for (let index = 0; index < items.length; index++) {
+ let cluster = {
+ // the item this cluster represents
+ item: items[index],
+ // a unique key for this cluster, stays constant unless merged itself
+ key: index,
+ // index of cluster in clusters array, can change during any merge
+ index: index,
+ // how many clusters have been merged into this one
+ size: 1
+ };
+ clusters[index] = cluster;
+ clustersByKey[index] = cluster;
+ distances[index] = [];
+ neighbors[index] = 0;
+ }
+
+ // initialize distance matrix and cached neighbors
+ for (let i = 0; i < clusters.length; i++) {
+ for (let j = 0; j <= i; j++) {
+ var dist = (i == j) ? Infinity :
+ this.distance(clusters[i].item, clusters[j].item);
+ distances[i][j] = dist;
+ distances[j][i] = dist;
+
+ if (dist < distances[i][neighbors[i]]) {
+ neighbors[i] = j;
+ }
+ }
+ }
+
+ // merge the next two closest clusters until none of them are close enough
+ let next = null, i = 0;
+ for (; next = this.closestClusters(clusters, distances, neighbors); i++) {
+ if (snapshotCallback && (i % snapshotGap) == 0) {
+ snapshotCallback(clusters);
+ }
+ this.mergeClusters(clusters, distances, neighbors, clustersByKey,
+ clustersByKey[next[0]], clustersByKey[next[1]]);
+ }
+ return clusters;
+ },
+
+ /**
+ * Once we decide to merge two clusters in the cluster method, actually
+ * merge them. Alters the given state of the algorithm.
+ *
+ * @param clusters
+ * The array of all remaining clusters
+ * @param distances
+ * Cached distances between pairs of clusters
+ * @param neighbors
+ * Cached closest clusters
+ * @param clustersByKey
+ * Array of all clusters, indexed by key
+ * @param cluster1
+ * First cluster to merge
+ * @param cluster2
+ * Second cluster to merge
+ */
+ mergeClusters: function HC_mergeClus(clusters, distances, neighbors,
+ clustersByKey, cluster1, cluster2) {
+ let merged = { item: this.merge(cluster1.item, cluster2.item),
+ left: cluster1,
+ right: cluster2,
+ key: cluster1.key,
+ size: cluster1.size + cluster2.size };
+
+ clusters[cluster1.index] = merged;
+ clusters.splice(cluster2.index, 1);
+ clustersByKey[cluster1.key] = merged;
+
+ // update distances with new merged cluster
+ for (let i = 0; i < clusters.length; i++) {
+ var ci = clusters[i];
+ var dist;
+ if (cluster1.key == ci.key) {
+ dist = Infinity;
+ } else if (this.merge == clusterlib.single_linkage) {
+ dist = distances[cluster1.key][ci.key];
+ if (distances[cluster1.key][ci.key] >
+ distances[cluster2.key][ci.key]) {
+ dist = distances[cluster2.key][ci.key];
+ }
+ } else if (this.merge == clusterlib.complete_linkage) {
+ dist = distances[cluster1.key][ci.key];
+ if (distances[cluster1.key][ci.key] <
+ distances[cluster2.key][ci.key]) {
+ dist = distances[cluster2.key][ci.key];
+ }
+ } else if (this.merge == clusterlib.average_linkage) {
+ dist = (distances[cluster1.key][ci.key] * cluster1.size
+ + distances[cluster2.key][ci.key] * cluster2.size)
+ / (cluster1.size + cluster2.size);
+ } else {
+ dist = this.distance(ci.item, cluster1.item);
+ }
+
+ distances[cluster1.key][ci.key] = distances[ci.key][cluster1.key]
+ = dist;
+ }
+
+ // update cached neighbors
+ for (let i = 0; i < clusters.length; i++) {
+ var key1 = clusters[i].key;
+ if (neighbors[key1] == cluster1.key ||
+ neighbors[key1] == cluster2.key) {
+ let minKey = key1;
+ for (let j = 0; j < clusters.length; j++) {
+ var key2 = clusters[j].key;
+ if (distances[key1][key2] < distances[key1][minKey]) {
+ minKey = key2;
+ }
+ }
+ neighbors[key1] = minKey;
+ }
+ clusters[i].index = i;
+ }
+ },
+
+ /**
+ * Given the current state of the algorithm, return the keys of the two
+ * clusters that are closest to each other so we know which ones to merge
+ * next.
+ *
+ * @param clusters
+ * The array of all remaining clusters
+ * @param distances
+ * Cached distances between pairs of clusters
+ * @param neighbors
+ * Cached closest clusters
+ *
+ * @return An array of two keys of clusters to merge, or null if there are
+ * no more clusters close enough to merge
+ */
+ closestClusters: function HC_closestClus(clusters, distances, neighbors) {
+ let minKey = 0, minDist = Infinity;
+ for (let i = 0; i < clusters.length; i++) {
+ var key = clusters[i].key;
+ if (distances[key][neighbors[key]] < minDist) {
+ minKey = key;
+ minDist = distances[key][neighbors[key]];
+ }
+ }
+ if (minDist < this.threshold) {
+ return [minKey, neighbors[minKey]];
+ }
+ return null;
+ }
+};
+
+var clusterlib = {
+ hcluster: function hcluster(items, distance, merge, threshold, snapshotGap,
+ snapshotCallback) {
+ return (new HierarchicalClustering(distance, merge, threshold))
+ .cluster(items, snapshotGap, snapshotCallback);
+ },
+
+ single_linkage: function single_linkage(cluster1, cluster2) {
+ return cluster1;
+ },
+
+ complete_linkage: function complete_linkage(cluster1, cluster2) {
+ return cluster1;
+ },
+
+ average_linkage: function average_linkage(cluster1, cluster2) {
+ return cluster1;
+ },
+
+ euclidean_distance: function euclidean_distance(v1, v2) {
+ let total = 0;
+ for (let i = 0; i < v1.length; i++) {
+ total += Math.pow(v2[i] - v1[i], 2);
+ }
+ return Math.sqrt(total);
+ },
+
+ manhattan_distance: function manhattan_distance(v1, v2) {
+ let total = 0;
+ for (let i = 0; i < v1.length; i++) {
+ total += Math.abs(v2[i] - v1[i]);
+ }
+ return total;
+ },
+
+ max_distance: function max_distance(v1, v2) {
+ let max = 0;
+ for (let i = 0; i < v1.length; i++) {
+ max = Math.max(max, Math.abs(v2[i] - v1[i]));
+ }
+ return max;
+ }
+};
diff --git a/toolkit/components/places/ColorAnalyzer.js b/toolkit/components/places/ColorAnalyzer.js
new file mode 100644
index 0000000000..861ce71075
--- /dev/null
+++ b/toolkit/components/places/ColorAnalyzer.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const MAXIMUM_PIXELS = Math.pow(144, 2);
+
+function ColorAnalyzer() {
+ // a queue of callbacks for each job we give to the worker
+ this.callbacks = [];
+
+ this.hiddenWindowDoc = Cc["@mozilla.org/appshell/appShellService;1"].
+ getService(Ci.nsIAppShellService).
+ hiddenDOMWindow.document;
+
+ this.worker = new ChromeWorker("resource://gre/modules/ColorAnalyzer_worker.js");
+ this.worker.onmessage = this.onWorkerMessage.bind(this);
+ this.worker.onerror = this.onWorkerError.bind(this);
+}
+
+ColorAnalyzer.prototype = {
+ findRepresentativeColor: function ColorAnalyzer_frc(imageURI, callback) {
+ function cleanup() {
+ image.removeEventListener("load", loadListener);
+ image.removeEventListener("error", errorListener);
+ }
+ let image = this.hiddenWindowDoc.createElementNS(XHTML_NS, "img");
+ let loadListener = this.onImageLoad.bind(this, image, callback, cleanup);
+ let errorListener = this.onImageError.bind(this, image, callback, cleanup);
+ image.addEventListener("load", loadListener);
+ image.addEventListener("error", errorListener);
+ image.src = imageURI.spec;
+ },
+
+ onImageLoad: function ColorAnalyzer_onImageLoad(image, callback, cleanup) {
+ if (image.naturalWidth * image.naturalHeight > MAXIMUM_PIXELS) {
+ // this will probably take too long to process - fail
+ callback.onComplete(false);
+ } else {
+ let canvas = this.hiddenWindowDoc.createElementNS(XHTML_NS, "canvas");
+ canvas.width = image.naturalWidth;
+ canvas.height = image.naturalHeight;
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ this.startJob(ctx.getImageData(0, 0, canvas.width, canvas.height),
+ callback);
+ }
+ cleanup();
+ },
+
+ onImageError: function ColorAnalyzer_onImageError(image, callback, cleanup) {
+ Cu.reportError("ColorAnalyzer: image at " + image.src + " didn't load");
+ callback.onComplete(false);
+ cleanup();
+ },
+
+ startJob: function ColorAnalyzer_startJob(imageData, callback) {
+ this.callbacks.push(callback);
+ this.worker.postMessage({ imageData: imageData, maxColors: 1 });
+ },
+
+ onWorkerMessage: function ColorAnalyzer_onWorkerMessage(event) {
+ // colors can be empty on failure
+ if (event.data.colors.length < 1) {
+ this.callbacks.shift().onComplete(false);
+ } else {
+ this.callbacks.shift().onComplete(true, event.data.colors[0]);
+ }
+ },
+
+ onWorkerError: function ColorAnalyzer_onWorkerError(error) {
+ // this shouldn't happen, but just in case
+ error.preventDefault();
+ Cu.reportError("ColorAnalyzer worker: " + error.message);
+ this.callbacks.shift().onComplete(false);
+ },
+
+ classID: Components.ID("{d056186c-28a0-494e-aacc-9e433772b143}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.mozIColorAnalyzer])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ColorAnalyzer]);
diff --git a/toolkit/components/places/ColorAnalyzer_worker.js b/toolkit/components/places/ColorAnalyzer_worker.js
new file mode 100644
index 0000000000..01fce06375
--- /dev/null
+++ b/toolkit/components/places/ColorAnalyzer_worker.js
@@ -0,0 +1,392 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+importScripts("ClusterLib.js", "ColorConversion.js");
+
+// Offsets in the ImageData pixel array to reach pixel colors
+const PIXEL_RED = 0;
+const PIXEL_GREEN = 1;
+const PIXEL_BLUE = 2;
+const PIXEL_ALPHA = 3;
+
+// Number of components in one ImageData pixel (RGBA)
+const NUM_COMPONENTS = 4;
+
+// Shift a color represented as a 24 bit integer by N bits to get a component
+const RED_SHIFT = 16;
+const GREEN_SHIFT = 8;
+
+// Only run the N most frequent unique colors through the clustering algorithm
+// Images with more than this many unique colors will have reduced accuracy.
+const MAX_COLORS_TO_MERGE = 500;
+
+// Each cluster of colors has a mean color in the Lab color space.
+// If the euclidean distance between the means of two clusters is greater
+// than or equal to this threshold, they won't be merged.
+const MERGE_THRESHOLD = 12;
+
+// The highest the distance handicap can be for large clusters
+const MAX_SIZE_HANDICAP = 5;
+// If the handicap is below this number, it is cut off to zero
+const SIZE_HANDICAP_CUTOFF = 2;
+
+// If potential background colors deviate from the mean background color by
+// this threshold or greater, finding a background color will fail
+const BACKGROUND_THRESHOLD = 10;
+
+// Alpha component of colors must be larger than this in order to make it into
+// the clustering algorithm or be considered a background color (0 - 255).
+const MIN_ALPHA = 25;
+
+// The euclidean distance in the Lab color space under which merged colors
+// are weighted lower for being similar to the background color
+const BACKGROUND_WEIGHT_THRESHOLD = 15;
+
+// The range in which color chroma differences will affect desirability.
+// Colors with chroma outside of the range take on the desirability of
+// their nearest extremes. Should be roughly 0 - 150.
+const CHROMA_WEIGHT_UPPER = 90;
+const CHROMA_WEIGHT_LOWER = 1;
+const CHROMA_WEIGHT_MIDDLE = (CHROMA_WEIGHT_UPPER + CHROMA_WEIGHT_LOWER) / 2;
+
+/**
+ * When we receive a message from the outside world, find the representative
+ * colors of the given image. The colors will be posted back to the caller
+ * through the "colors" property on the event data object as an array of
+ * integers. Colors of lower indices are more representative.
+ * This array can be empty if this worker can't find a color.
+ *
+ * @param event
+ * A MessageEvent whose data should have the following properties:
+ * imageData - A DOM ImageData instance to analyze
+ * maxColors - The maximum number of representative colors to find,
+ * defaults to 1 if not provided
+ */
+onmessage = function(event) {
+ let imageData = event.data.imageData;
+ let pixels = imageData.data;
+ let width = imageData.width;
+ let height = imageData.height;
+ let maxColors = event.data.maxColors;
+ if (typeof(maxColors) != "number") {
+ maxColors = 1;
+ }
+
+ let allColors = getColors(pixels, width, height);
+
+ // Only merge top colors by frequency for speed.
+ let mergedColors = mergeColors(allColors.slice(0, MAX_COLORS_TO_MERGE),
+ width * height, MERGE_THRESHOLD);
+
+ let backgroundColor = getBackgroundColor(pixels, width, height);
+
+ mergedColors = mergedColors.map(function(cluster) {
+ // metadata holds a bunch of information about the color represented by
+ // this cluster
+ let metadata = cluster.item;
+
+ // the basis of color desirability is how much of the image the color is
+ // responsible for, but we'll need to weigh this number differently
+ // depending on other factors
+ metadata.desirability = metadata.ratio;
+ let weight = 1;
+
+ // if the color is close to the background color, we don't want it
+ if (backgroundColor != null) {
+ let backgroundDistance = labEuclidean(metadata.mean, backgroundColor);
+ if (backgroundDistance < BACKGROUND_WEIGHT_THRESHOLD) {
+ weight = backgroundDistance / BACKGROUND_WEIGHT_THRESHOLD;
+ }
+ }
+
+ // prefer more interesting colors, but don't knock low chroma colors
+ // completely out of the running (lower bound), and we don't really care
+ // if a color is slightly more intense than another on the higher end
+ let chroma = labChroma(metadata.mean);
+ if (chroma < CHROMA_WEIGHT_LOWER) {
+ chroma = CHROMA_WEIGHT_LOWER;
+ } else if (chroma > CHROMA_WEIGHT_UPPER) {
+ chroma = CHROMA_WEIGHT_UPPER;
+ }
+ weight *= chroma / CHROMA_WEIGHT_MIDDLE;
+
+ metadata.desirability *= weight;
+ return metadata;
+ });
+
+ // only send back the most desirable colors
+ mergedColors.sort(function(a, b) {
+ return b.desirability != a.desirability ? b.desirability - a.desirability : b.color - a.color;
+ });
+ mergedColors = mergedColors.map(function(metadata) {
+ return metadata.color;
+ }).slice(0, maxColors);
+ postMessage({ colors: mergedColors });
+};
+
+/**
+ * Given the pixel data and dimensions of an image, return an array of objects
+ * associating each unique color and its frequency in the image, sorted
+ * descending by frequency. Sufficiently transparent colors are ignored.
+ *
+ * @param pixels
+ * Pixel data array for the image to get colors from (ImageData.data).
+ * @param width
+ * Width of the image, in # of pixels.
+ * @param height
+ * Height of the image, in # of pixels.
+ *
+ * @return An array of objects with color and freq properties, sorted
+ * descending by freq
+ */
+function getColors(pixels, width, height) {
+ let colorFrequency = {};
+ for (let x = 0; x < width; x++) {
+ for (let y = 0; y < height; y++) {
+ let offset = (x * NUM_COMPONENTS) + (y * NUM_COMPONENTS * width);
+
+ if (pixels[offset + PIXEL_ALPHA] < MIN_ALPHA) {
+ continue;
+ }
+
+ let color = pixels[offset + PIXEL_RED] << RED_SHIFT
+ | pixels[offset + PIXEL_GREEN] << GREEN_SHIFT
+ | pixels[offset + PIXEL_BLUE];
+
+ if (color in colorFrequency) {
+ colorFrequency[color]++;
+ } else {
+ colorFrequency[color] = 1;
+ }
+ }
+ }
+
+ let colors = [];
+ for (var color in colorFrequency) {
+ colors.push({ color: +color, freq: colorFrequency[+color] });
+ }
+ colors.sort(descendingFreqSort);
+ return colors;
+}
+
+/**
+ * Given an array of objects from getColors, the number of pixels in the
+ * image, and a merge threshold, run the clustering algorithm on the colors
+ * and return the set of merged clusters.
+ *
+ * @param colorFrequencies
+ * An array of objects from getColors to cluster
+ * @param numPixels
+ * The number of pixels in the image
+ * @param threshold
+ * The maximum distance between two clusters for which those clusters
+ * can be merged.
+ *
+ * @return An array of merged clusters
+ *
+ * @see clusterlib.hcluster
+ * @see getColors
+ */
+function mergeColors(colorFrequencies, numPixels, threshold) {
+ let items = colorFrequencies.map(function(colorFrequency) {
+ let color = colorFrequency.color;
+ let freq = colorFrequency.freq;
+ return {
+ mean: rgb2lab(color >> RED_SHIFT, color >> GREEN_SHIFT & 0xff,
+ color & 0xff),
+ // the canonical color of the cluster
+ // (one w/ highest freq or closest to mean)
+ color: color,
+ colors: [color],
+ highFreq: freq,
+ highRatio: freq / numPixels,
+ // the individual color w/ the highest frequency in this cluster
+ highColor: color,
+ // ratio of image taken up by colors in this cluster
+ ratio: freq / numPixels,
+ freq: freq,
+ };
+ });
+
+ let merged = clusterlib.hcluster(items, distance, merge, threshold);
+ return merged;
+}
+
+function descendingFreqSort(a, b) {
+ return b.freq != a.freq ? b.freq - a.freq : b.color - a.color;
+}
+
+/**
+ * Given two items for a pair of clusters (as created in mergeColors above),
+ * determine the distance between them so we know if we should merge or not.
+ * Uses the euclidean distance between their mean colors in the lab color
+ * space, weighted so larger items are harder to merge.
+ *
+ * @param item1
+ * The first item to compare
+ * @param item2
+ * The second item to compare
+ *
+ * @return The distance between the two items
+ */
+function distance(item1, item2) {
+ // don't cluster large blocks of color unless they're really similar
+ let minRatio = Math.min(item1.ratio, item2.ratio);
+ let dist = labEuclidean(item1.mean, item2.mean);
+ let handicap = Math.min(MAX_SIZE_HANDICAP, dist * minRatio);
+ if (handicap <= SIZE_HANDICAP_CUTOFF) {
+ handicap = 0;
+ }
+ return dist + handicap;
+}
+
+/**
+ * Find the euclidean distance between two colors in the Lab color space.
+ *
+ * @param color1
+ * The first color to compare
+ * @param color2
+ * The second color to compare
+ *
+ * @return The euclidean distance between the two colors
+ */
+function labEuclidean(color1, color2) {
+ return Math.sqrt(
+ Math.pow(color2.lightness - color1.lightness, 2)
+ + Math.pow(color2.a - color1.a, 2)
+ + Math.pow(color2.b - color1.b, 2));
+}
+
+/**
+ * Given items from two clusters we know are appropriate for merging,
+ * merge them together into a third item such that its metadata describes both
+ * input items. The "color" property is set to the color in the new item that
+ * is closest to its mean color.
+ *
+ * @param item1
+ * The first item to merge
+ * @param item2
+ * The second item to merge
+ *
+ * @return An item that represents the merging of the given items
+ */
+function merge(item1, item2) {
+ let lab1 = item1.mean;
+ let lab2 = item2.mean;
+
+ /* algorithm tweak point - weighting the mean of the cluster */
+ let num1 = item1.freq;
+ let num2 = item2.freq;
+
+ let total = num1 + num2;
+
+ let mean = {
+ lightness: (lab1.lightness * num1 + lab2.lightness * num2) / total,
+ a: (lab1.a * num1 + lab2.a * num2) / total,
+ b: (lab1.b * num1 + lab2.b * num2) / total
+ };
+
+ let colors = item1.colors.concat(item2.colors);
+
+ // get the canonical color of the new cluster
+ let color;
+ let avgFreq = colors.length / (item1.freq + item2.freq);
+ if ((item1.highFreq > item2.highFreq) && (item1.highFreq > avgFreq * 2)) {
+ color = item1.highColor;
+ } else if (item2.highFreq > avgFreq * 2) {
+ color = item2.highColor;
+ } else {
+ // if there's no stand-out color
+ let minDist = Infinity, closest = 0;
+ for (let i = 0; i < colors.length; i++) {
+ let color = colors[i];
+ let lab = rgb2lab(color >> RED_SHIFT, color >> GREEN_SHIFT & 0xff,
+ color & 0xff);
+ let dist = labEuclidean(lab, mean);
+ if (dist < minDist) {
+ minDist = dist;
+ closest = i;
+ }
+ }
+ color = colors[closest];
+ }
+
+ const higherItem = item1.highFreq > item2.highFreq ? item1 : item2;
+
+ return {
+ mean: mean,
+ color: color,
+ highFreq: higherItem.highFreq,
+ highColor: higherItem.highColor,
+ highRatio: higherItem.highRatio,
+ ratio: item1.ratio + item2.ratio,
+ freq: item1.freq + item2.freq,
+ colors: colors,
+ };
+}
+
+/**
+ * Find the background color of the given image.
+ *
+ * @param pixels
+ * The pixel data for the image (an array of component integers)
+ * @param width
+ * The width of the image
+ * @param height
+ * The height of the image
+ *
+ * @return The background color of the image as a Lab object, or null if we
+ * can't determine the background color
+ */
+function getBackgroundColor(pixels, width, height) {
+ // we'll assume that if the four corners are roughly the same color,
+ // then that's the background color
+ let coordinates = [[0, 0], [width - 1, 0], [width - 1, height - 1],
+ [0, height - 1]];
+
+ // find the corner colors in LAB
+ let cornerColors = [];
+ for (let i = 0; i < coordinates.length; i++) {
+ let offset = (coordinates[i][0] * NUM_COMPONENTS)
+ + (coordinates[i][1] * NUM_COMPONENTS * width);
+ if (pixels[offset + PIXEL_ALPHA] < MIN_ALPHA) {
+ // we can't make very accurate judgements below this opacity
+ continue;
+ }
+ cornerColors.push(rgb2lab(pixels[offset + PIXEL_RED],
+ pixels[offset + PIXEL_GREEN],
+ pixels[offset + PIXEL_BLUE]));
+ }
+
+ // we want at least two points at acceptable alpha levels
+ if (cornerColors.length <= 1) {
+ return null;
+ }
+
+ // find the average color among the corners
+ let averageColor = { lightness: 0, a: 0, b: 0 };
+ cornerColors.forEach(function(color) {
+ for (let i in color) {
+ averageColor[i] += color[i];
+ }
+ });
+ for (let i in averageColor) {
+ averageColor[i] /= cornerColors.length;
+ }
+
+ // if we have fewer points due to low alpha, they need to be closer together
+ let threshold = BACKGROUND_THRESHOLD
+ * (cornerColors.length / coordinates.length);
+
+ // if any of the corner colors deviate enough from the average, they aren't
+ // similar enough to be considered the background color
+ for (let cornerColor of cornerColors) {
+ if (labEuclidean(cornerColor, averageColor) > threshold) {
+ return null;
+ }
+ }
+ return averageColor;
+}
diff --git a/toolkit/components/places/ColorConversion.js b/toolkit/components/places/ColorConversion.js
new file mode 100644
index 0000000000..b8a2c860da
--- /dev/null
+++ b/toolkit/components/places/ColorConversion.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/. */
+
+/**
+ * Given a color in the Lab space, return its chroma (colorfulness,
+ * saturation).
+ *
+ * @param lab
+ * The lab color to get the chroma from
+ *
+ * @return A number greater than zero that measures chroma in the image
+ */
+function labChroma(lab) {
+ return Math.sqrt(Math.pow(lab.a, 2) + Math.pow(lab.b, 2));
+}
+
+/**
+ * Given the RGB components of a color as integers from 0-255, return the
+ * color in the XYZ color space.
+ *
+ * @return An object with x, y, z properties holding those components of the
+ * color in the XYZ color space.
+ */
+function rgb2xyz(r, g, b) {
+ r /= 255;
+ g /= 255;
+ b /= 255;
+
+ // assume sRGB
+ r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92);
+ g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92);
+ b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92);
+
+ return {
+ x: ((r * 0.4124) + (g * 0.3576) + (b * 0.1805)) * 100,
+ y: ((r * 0.2126) + (g * 0.7152) + (b * 0.0722)) * 100,
+ z: ((r * 0.0193) + (g * 0.1192) + (b * 0.9505)) * 100
+ };
+}
+
+/**
+ * Given the RGB components of a color as integers from 0-255, return the
+ * color in the Lab color space.
+ *
+ * @return An object with lightness, a, b properties holding those components
+ * of the color in the Lab color space.
+ */
+function rgb2lab(r, g, b) {
+ let xyz = rgb2xyz(r, g, b),
+ x = xyz.x / 95.047,
+ y = xyz.y / 100,
+ z = xyz.z / 108.883;
+
+ x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);
+ y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);
+ z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);
+
+ return {
+ lightness: (116 * y) - 16,
+ a: 500 * (x - y),
+ b: 200 * (y - z)
+ };
+}
diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp
new file mode 100644
index 0000000000..37502e2a14
--- /dev/null
+++ b/toolkit/components/places/Database.cpp
@@ -0,0 +1,2333 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/ScopeExit.h"
+
+#include "Database.h"
+
+#include "nsIAnnotationService.h"
+#include "nsINavBookmarksService.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIFile.h"
+#include "nsIWritablePropertyBag2.h"
+
+#include "nsNavHistory.h"
+#include "nsPlacesTables.h"
+#include "nsPlacesIndexes.h"
+#include "nsPlacesTriggers.h"
+#include "nsPlacesMacros.h"
+#include "nsVariant.h"
+#include "SQLFunctions.h"
+#include "Helpers.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "prenv.h"
+#include "prsystem.h"
+#include "nsPrintfCString.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Unused.h"
+#include "prtime.h"
+
+#include "nsXULAppAPI.h"
+
+// Time between corrupt database backups.
+#define RECENT_BACKUP_TIME_MICROSEC (int64_t)86400 * PR_USEC_PER_SEC // 24H
+
+// Filename of the database.
+#define DATABASE_FILENAME NS_LITERAL_STRING("places.sqlite")
+// Filename used to backup corrupt databases.
+#define DATABASE_CORRUPT_FILENAME NS_LITERAL_STRING("places.sqlite.corrupt")
+
+// Set when the database file was found corrupt by a previous maintenance.
+#define PREF_FORCE_DATABASE_REPLACEMENT "places.database.replaceOnStartup"
+
+// Set to specify the size of the places database growth increments in kibibytes
+#define PREF_GROWTH_INCREMENT_KIB "places.database.growthIncrementKiB"
+
+// Set to disable the default robust storage and use volatile, in-memory
+// storage without robust transaction flushing guarantees. This makes
+// SQLite use much less I/O at the cost of losing data when things crash.
+// The pref is only honored if an environment variable is set. The env
+// variable is intentionally named something scary to help prevent someone
+// from thinking it is a useful performance optimization they should enable.
+#define PREF_DISABLE_DURABILITY "places.database.disableDurability"
+#define ENV_ALLOW_CORRUPTION "ALLOW_PLACES_DATABASE_TO_LOSE_DATA_AND_BECOME_CORRUPT"
+
+// The maximum url length we can store in history.
+// We do not add to history URLs longer than this value.
+#define PREF_HISTORY_MAXURLLEN "places.history.maxUrlLength"
+// This number is mostly a guess based on various facts:
+// * IE didn't support urls longer than 2083 chars
+// * Sitemaps protocol used to support a maximum of 2048 chars
+// * Various SEO guides suggest to not go over 2000 chars
+// * Various apps/services are known to have issues over 2000 chars
+// * RFC 2616 - HTTP/1.1 suggests being cautious about depending
+// on URI lengths above 255 bytes
+#define PREF_HISTORY_MAXURLLEN_DEFAULT 2000
+
+// Maximum size for the WAL file. It should be small enough since in case of
+// crashes we could lose all the transactions in the file. But a too small
+// file could hurt performance.
+#define DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES 512
+
+#define BYTES_PER_KIBIBYTE 1024
+
+// How much time Sqlite can wait before returning a SQLITE_BUSY error.
+#define DATABASE_BUSY_TIMEOUT_MS 100
+
+// Old Sync GUID annotation.
+#define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid")
+
+// Places string bundle, contains internationalized bookmark root names.
+#define PLACES_BUNDLE "chrome://places/locale/places.properties"
+
+// Livemarks annotations.
+#define LMANNO_FEEDURI "livemark/feedURI"
+#define LMANNO_SITEURI "livemark/siteURI"
+
+#define MOBILE_ROOT_GUID "mobile______"
+#define MOBILE_ROOT_ANNO "mobile/bookmarksRoot"
+
+// We use a fixed title for the mobile root to avoid marking the database as
+// corrupt if we can't look up the localized title in the string bundle. Sync
+// sets the title to the localized version when it creates the left pane query.
+#define MOBILE_ROOT_TITLE "mobile"
+
+using namespace mozilla;
+
+namespace mozilla {
+namespace places {
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helpers
+
+/**
+ * Checks whether exists a database backup created not longer than
+ * RECENT_BACKUP_TIME_MICROSEC ago.
+ */
+bool
+hasRecentCorruptDB()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIFile> profDir;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profDir));
+ NS_ENSURE_TRUE(profDir, false);
+ nsCOMPtr<nsISimpleEnumerator> entries;
+ profDir->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_TRUE(entries, false);
+ bool hasMore;
+ while (NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> next;
+ entries->GetNext(getter_AddRefs(next));
+ NS_ENSURE_TRUE(next, false);
+ nsCOMPtr<nsIFile> currFile = do_QueryInterface(next);
+ NS_ENSURE_TRUE(currFile, false);
+
+ nsAutoString leafName;
+ if (NS_SUCCEEDED(currFile->GetLeafName(leafName)) &&
+ leafName.Length() >= DATABASE_CORRUPT_FILENAME.Length() &&
+ leafName.Find(".corrupt", DATABASE_FILENAME.Length()) != -1) {
+ PRTime lastMod = 0;
+ currFile->GetLastModifiedTime(&lastMod);
+ NS_ENSURE_TRUE(lastMod > 0, false);
+ return (PR_Now() - lastMod) > RECENT_BACKUP_TIME_MICROSEC;
+ }
+ }
+ return false;
+}
+
+/**
+ * Updates sqlite_stat1 table through ANALYZE.
+ * Since also nsPlacesExpiration.js executes ANALYZE, the analyzed tables
+ * must be the same in both components. So ensure they are in sync.
+ *
+ * @param aDBConn
+ * The database connection.
+ */
+nsresult
+updateSQLiteStatistics(mozIStorageConnection* aDBConn)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<mozIStorageAsyncStatement> analyzePlacesStmt;
+ aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "ANALYZE moz_places"
+ ), getter_AddRefs(analyzePlacesStmt));
+ NS_ENSURE_STATE(analyzePlacesStmt);
+ nsCOMPtr<mozIStorageAsyncStatement> analyzeBookmarksStmt;
+ aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "ANALYZE moz_bookmarks"
+ ), getter_AddRefs(analyzeBookmarksStmt));
+ NS_ENSURE_STATE(analyzeBookmarksStmt);
+ nsCOMPtr<mozIStorageAsyncStatement> analyzeVisitsStmt;
+ aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "ANALYZE moz_historyvisits"
+ ), getter_AddRefs(analyzeVisitsStmt));
+ NS_ENSURE_STATE(analyzeVisitsStmt);
+ nsCOMPtr<mozIStorageAsyncStatement> analyzeInputStmt;
+ aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "ANALYZE moz_inputhistory"
+ ), getter_AddRefs(analyzeInputStmt));
+ NS_ENSURE_STATE(analyzeInputStmt);
+
+ mozIStorageBaseStatement *stmts[] = {
+ analyzePlacesStmt,
+ analyzeBookmarksStmt,
+ analyzeVisitsStmt,
+ analyzeInputStmt
+ };
+
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ (void)aDBConn->ExecuteAsync(stmts, ArrayLength(stmts), nullptr,
+ getter_AddRefs(ps));
+ return NS_OK;
+}
+
+/**
+ * Sets the connection journal mode to one of the JOURNAL_* types.
+ *
+ * @param aDBConn
+ * The database connection.
+ * @param aJournalMode
+ * One of the JOURNAL_* types.
+ * @returns the current journal mode.
+ * @note this may return a different journal mode than the required one, since
+ * setting it may fail.
+ */
+enum JournalMode
+SetJournalMode(nsCOMPtr<mozIStorageConnection>& aDBConn,
+ enum JournalMode aJournalMode)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsAutoCString journalMode;
+ switch (aJournalMode) {
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Trying to set an unknown journal mode.");
+ // Fall through to the default DELETE journal.
+ case JOURNAL_DELETE:
+ journalMode.AssignLiteral("delete");
+ break;
+ case JOURNAL_TRUNCATE:
+ journalMode.AssignLiteral("truncate");
+ break;
+ case JOURNAL_MEMORY:
+ journalMode.AssignLiteral("memory");
+ break;
+ case JOURNAL_WAL:
+ journalMode.AssignLiteral("wal");
+ break;
+ }
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR
+ "PRAGMA journal_mode = ");
+ query.Append(journalMode);
+ aDBConn->CreateStatement(query, getter_AddRefs(statement));
+ NS_ENSURE_TRUE(statement, JOURNAL_DELETE);
+
+ bool hasResult = false;
+ if (NS_SUCCEEDED(statement->ExecuteStep(&hasResult)) && hasResult &&
+ NS_SUCCEEDED(statement->GetUTF8String(0, journalMode))) {
+ if (journalMode.EqualsLiteral("delete")) {
+ return JOURNAL_DELETE;
+ }
+ if (journalMode.EqualsLiteral("truncate")) {
+ return JOURNAL_TRUNCATE;
+ }
+ if (journalMode.EqualsLiteral("memory")) {
+ return JOURNAL_MEMORY;
+ }
+ if (journalMode.EqualsLiteral("wal")) {
+ return JOURNAL_WAL;
+ }
+ // This is an unknown journal.
+ MOZ_ASSERT(true);
+ }
+
+ return JOURNAL_DELETE;
+}
+
+nsresult
+CreateRoot(nsCOMPtr<mozIStorageConnection>& aDBConn,
+ const nsCString& aRootName, const nsCString& aGuid,
+ const nsXPIDLString& titleString)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // The position of the new item in its folder.
+ static int32_t itemPosition = 0;
+
+ // A single creation timestamp for all roots so that the root folder's
+ // last modification time isn't earlier than its childrens' creation time.
+ static PRTime timestamp = 0;
+ if (!timestamp)
+ timestamp = RoundedPRNow();
+
+ // Create a new bookmark folder for the root.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_bookmarks "
+ "(type, position, title, dateAdded, lastModified, guid, parent) "
+ "VALUES (:item_type, :item_position, :item_title,"
+ ":date_added, :last_modified, :guid,"
+ "IFNULL((SELECT id FROM moz_bookmarks WHERE parent = 0), 0))"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"),
+ nsINavBookmarksService::TYPE_FOLDER);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_position"), itemPosition);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
+ NS_ConvertUTF16toUTF8(titleString));
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), timestamp);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), timestamp);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGuid);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ // The 'places' root is a folder containing the other roots.
+ // The first bookmark in a folder has position 0.
+ if (!aRootName.EqualsLiteral("places"))
+ ++itemPosition;
+
+ return NS_OK;
+}
+
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+//// Database
+
+PLACES_FACTORY_SINGLETON_IMPLEMENTATION(Database, gDatabase)
+
+NS_IMPL_ISUPPORTS(Database
+, nsIObserver
+, nsISupportsWeakReference
+)
+
+Database::Database()
+ : mMainThreadStatements(mMainConn)
+ , mMainThreadAsyncStatements(mMainConn)
+ , mAsyncThreadStatements(mMainConn)
+ , mDBPageSize(0)
+ , mDatabaseStatus(nsINavHistoryService::DATABASE_STATUS_OK)
+ , mClosed(false)
+ , mClientsShutdown(new ClientsShutdownBlocker())
+ , mConnectionShutdown(new ConnectionShutdownBlocker(this))
+ , mMaxUrlLength(0)
+{
+ MOZ_ASSERT(!XRE_IsContentProcess(),
+ "Cannot instantiate Places in the content process");
+ // Attempting to create two instances of the service?
+ MOZ_ASSERT(!gDatabase);
+ gDatabase = this;
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+Database::GetProfileChangeTeardownPhase()
+{
+ nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
+ MOZ_ASSERT(asyncShutdownSvc);
+ if (NS_WARN_IF(!asyncShutdownSvc)) {
+ return nullptr;
+ }
+
+ // Consumers of Places should shutdown before us, at profile-change-teardown.
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
+ DebugOnly<nsresult> rv = asyncShutdownSvc->
+ GetProfileChangeTeardown(getter_AddRefs(shutdownPhase));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ return shutdownPhase.forget();
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+Database::GetProfileBeforeChangePhase()
+{
+ nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
+ MOZ_ASSERT(asyncShutdownSvc);
+ if (NS_WARN_IF(!asyncShutdownSvc)) {
+ return nullptr;
+ }
+
+ // Consumers of Places should shutdown before us, at profile-change-teardown.
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
+ DebugOnly<nsresult> rv = asyncShutdownSvc->
+ GetProfileBeforeChange(getter_AddRefs(shutdownPhase));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ return shutdownPhase.forget();
+}
+
+Database::~Database()
+{
+}
+
+bool
+Database::IsShutdownStarted() const
+{
+ if (!mConnectionShutdown) {
+ // We have already broken the cycle between `this` and `mConnectionShutdown`.
+ return true;
+ }
+ return mConnectionShutdown->IsStarted();
+}
+
+already_AddRefed<mozIStorageAsyncStatement>
+Database::GetAsyncStatement(const nsACString& aQuery) const
+{
+ if (IsShutdownStarted()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(NS_IsMainThread());
+ return mMainThreadAsyncStatements.GetCachedStatement(aQuery);
+}
+
+already_AddRefed<mozIStorageStatement>
+Database::GetStatement(const nsACString& aQuery) const
+{
+ if (IsShutdownStarted()) {
+ return nullptr;
+ }
+ if (NS_IsMainThread()) {
+ return mMainThreadStatements.GetCachedStatement(aQuery);
+ }
+ return mAsyncThreadStatements.GetCachedStatement(aQuery);
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+Database::GetClientsShutdown()
+{
+ MOZ_ASSERT(mClientsShutdown);
+ return mClientsShutdown->GetClient();
+}
+
+// static
+already_AddRefed<Database>
+Database::GetDatabase()
+{
+ if (PlacesShutdownBlocker::IsStarted()) {
+ return nullptr;
+ }
+ return GetSingleton();
+}
+
+nsresult
+Database::Init()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_STATE(storage);
+
+ // Init the database file and connect to it.
+ bool databaseCreated = false;
+ nsresult rv = InitDatabaseFile(storage, &databaseCreated);
+ if (NS_SUCCEEDED(rv) && databaseCreated) {
+ mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE;
+ }
+ else if (rv == NS_ERROR_FILE_CORRUPTED) {
+ // The database is corrupt, backup and replace it with a new one.
+ mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
+ rv = BackupAndReplaceDatabaseFile(storage);
+ // Fallback to catch-all handler, that notifies a database locked failure.
+ }
+
+ // If the database connection still cannot be opened, it may just be locked
+ // by third parties. Send out a notification and interrupt initialization.
+ if (NS_FAILED(rv)) {
+ RefPtr<PlacesEvent> lockedEvent = new PlacesEvent(TOPIC_DATABASE_LOCKED);
+ (void)NS_DispatchToMainThread(lockedEvent);
+ return rv;
+ }
+
+ // Initialize the database schema. In case of failure the existing schema is
+ // is corrupt or incoherent, thus the database should be replaced.
+ bool databaseMigrated = false;
+ rv = InitSchema(&databaseMigrated);
+ if (NS_FAILED(rv)) {
+ mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
+ rv = BackupAndReplaceDatabaseFile(storage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Try to initialize the schema again on the new database.
+ rv = InitSchema(&databaseMigrated);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (databaseMigrated) {
+ mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_UPGRADED;
+ }
+
+ if (mDatabaseStatus != nsINavHistoryService::DATABASE_STATUS_OK) {
+ rv = updateSQLiteStatistics(MainConn());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Initialize here all the items that are not part of the on-disk database,
+ // like views, temp triggers or temp tables. The database should not be
+ // considered corrupt if any of the following fails.
+
+ rv = InitTempEntities();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Notify we have finished database initialization.
+ // Enqueue the notification, so if we init another service that requires
+ // nsNavHistoryService we don't recursive try to get it.
+ RefPtr<PlacesEvent> completeEvent =
+ new PlacesEvent(TOPIC_PLACES_INIT_COMPLETE);
+ rv = NS_DispatchToMainThread(completeEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // At this point we know the Database object points to a valid connection
+ // and we need to setup async shutdown.
+ {
+ // First of all Places clients should block profile-change-teardown.
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileChangeTeardownPhase();
+ MOZ_ASSERT(shutdownPhase);
+ if (shutdownPhase) {
+ DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
+ static_cast<nsIAsyncShutdownBlocker*>(mClientsShutdown.get()),
+ NS_LITERAL_STRING(__FILE__),
+ __LINE__,
+ NS_LITERAL_STRING(""));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ {
+ // Then connection closing should block profile-before-change.
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileBeforeChangePhase();
+ MOZ_ASSERT(shutdownPhase);
+ if (shutdownPhase) {
+ DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
+ static_cast<nsIAsyncShutdownBlocker*>(mConnectionShutdown.get()),
+ NS_LITERAL_STRING(__FILE__),
+ __LINE__,
+ NS_LITERAL_STRING(""));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ // Finally observe profile shutdown notifications.
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ (void)os->AddObserver(this, TOPIC_PROFILE_CHANGE_TEARDOWN, true);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::InitDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
+ bool* aNewDatabaseCreated)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ *aNewDatabaseCreated = false;
+
+ nsCOMPtr<nsIFile> databaseFile;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(databaseFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = databaseFile->Append(DATABASE_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool databaseFileExists = false;
+ rv = databaseFile->Exists(&databaseFileExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (databaseFileExists &&
+ Preferences::GetBool(PREF_FORCE_DATABASE_REPLACEMENT, false)) {
+ // If this pref is set, Maintenance required a database replacement, due to
+ // integrity corruption.
+ // Be sure to clear the pref to avoid handling it more than once.
+ (void)Preferences::ClearUser(PREF_FORCE_DATABASE_REPLACEMENT);
+
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ // Open the database file. If it does not exist a new one will be created.
+ // Use an unshared connection, it will consume more memory but avoid shared
+ // cache contentions across threads.
+ rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aNewDatabaseCreated = !databaseFileExists;
+ return NS_OK;
+}
+
+nsresult
+Database::BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIFile> profDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> databaseFile;
+ rv = profDir->Clone(getter_AddRefs(databaseFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = databaseFile->Append(DATABASE_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we have
+ // already failed in the last 24 hours avoid to create another corrupt file,
+ // since doing so, in some situation, could cause us to create a new corrupt
+ // file at every try to access any Places service. That is bad because it
+ // would quickly fill the user's disk space without any notice.
+ if (!hasRecentCorruptDB()) {
+ nsCOMPtr<nsIFile> backup;
+ (void)aStorage->BackupDatabaseFile(databaseFile, DATABASE_CORRUPT_FILENAME,
+ profDir, getter_AddRefs(backup));
+ }
+
+ // If anything fails from this point on, we have a stale connection or
+ // database file, and there's not much more we can do.
+ // The only thing we can try to do is to replace the database on the next
+ // startup, and report the problem through telemetry.
+ {
+ enum eCorruptDBReplaceStage : int8_t {
+ stage_closing = 0,
+ stage_removing,
+ stage_reopening,
+ stage_replaced
+ };
+ eCorruptDBReplaceStage stage = stage_closing;
+ auto guard = MakeScopeExit([&]() {
+ if (stage != stage_replaced) {
+ // Reaching this point means the database is corrupt and we failed to
+ // replace it. For this session part of the application related to
+ // bookmarks and history will misbehave. The frontend may show a
+ // "locked" notification to the user though.
+ // Set up a pref to try replacing the database at the next startup.
+ Preferences::SetBool(PREF_FORCE_DATABASE_REPLACEMENT, true);
+ }
+ // Report the corruption through telemetry.
+ Telemetry::Accumulate(Telemetry::PLACES_DATABASE_CORRUPTION_HANDLING_STAGE,
+ static_cast<int8_t>(stage));
+ });
+
+ // Close database connection if open.
+ if (mMainConn) {
+ rv = mMainConn->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Remove the broken database.
+ stage = stage_removing;
+ rv = databaseFile->Remove(false);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
+ return rv;
+ }
+
+ // Create a new database file.
+ // Use an unshared connection, it will consume more memory but avoid shared
+ // cache contentions across threads.
+ stage = stage_reopening;
+ rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ stage = stage_replaced;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::InitSchema(bool* aDatabaseMigrated)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ *aDatabaseMigrated = false;
+
+ // WARNING: any statement executed before setting the journal mode must be
+ // finalized, since SQLite doesn't allow changing the journal mode if there
+ // is any outstanding statement.
+
+ {
+ // Get the page size. This may be different than the default if the
+ // database file already existed with a different page size.
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA page_size"
+ ), getter_AddRefs(statement));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool hasResult = false;
+ rv = statement->ExecuteStep(&hasResult);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FAILURE);
+ rv = statement->GetInt32(0, &mDBPageSize);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && mDBPageSize > 0, NS_ERROR_UNEXPECTED);
+ }
+
+ // Ensure that temp tables are held in memory, not on disk.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA temp_store = MEMORY"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (PR_GetEnv(ENV_ALLOW_CORRUPTION) && Preferences::GetBool(PREF_DISABLE_DURABILITY, false)) {
+ // Volatile storage was requested. Use the in-memory journal (no
+ // filesystem I/O) and don't sync the filesystem after writing.
+ SetJournalMode(mMainConn, JOURNAL_MEMORY);
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "PRAGMA synchronous = OFF"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ // Be sure to set journal mode after page_size. WAL would prevent the change
+ // otherwise.
+ if (JOURNAL_WAL == SetJournalMode(mMainConn, JOURNAL_WAL)) {
+ // Set the WAL journal size limit. We want it to be small, since in
+ // synchronous = NORMAL mode a crash could cause loss of all the
+ // transactions in the journal. For added safety we will also force
+ // checkpointing at strategic moments.
+ int32_t checkpointPages =
+ static_cast<int32_t>(DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES * 1024 / mDBPageSize);
+ nsAutoCString checkpointPragma("PRAGMA wal_autocheckpoint = ");
+ checkpointPragma.AppendInt(checkpointPages);
+ rv = mMainConn->ExecuteSimpleSQL(checkpointPragma);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ // Ignore errors, if we fail here the database could be considered corrupt
+ // and we won't be able to go on, even if it's just matter of a bogus file
+ // system. The default mode (DELETE) will be fine in such a case.
+ (void)SetJournalMode(mMainConn, JOURNAL_TRUNCATE);
+
+ // Set synchronous to FULL to ensure maximum data integrity, even in
+ // case of crashes or unclean shutdowns.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "PRAGMA synchronous = FULL"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // The journal is usually free to grow for performance reasons, but it never
+ // shrinks back. Since the space taken may be problematic, especially on
+ // mobile devices, limit its size.
+ // Since exceeding the limit will cause a truncate, allow a slightly
+ // larger limit than DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES to reduce the number
+ // of times it is needed.
+ nsAutoCString journalSizePragma("PRAGMA journal_size_limit = ");
+ journalSizePragma.AppendInt(DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES * 3);
+ (void)mMainConn->ExecuteSimpleSQL(journalSizePragma);
+
+ // Grow places in |growthIncrementKiB| increments to limit fragmentation on disk.
+ // By default, it's 10 MB.
+ int32_t growthIncrementKiB =
+ Preferences::GetInt(PREF_GROWTH_INCREMENT_KIB, 10 * BYTES_PER_KIBIBYTE);
+ if (growthIncrementKiB > 0) {
+ (void)mMainConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE, EmptyCString());
+ }
+
+ nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = ");
+ busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS);
+ (void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma);
+
+ // We use our functions during migration, so initialize them now.
+ rv = InitFunctions();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the database schema version.
+ int32_t currentSchemaVersion;
+ rv = mMainConn->GetSchemaVersion(&currentSchemaVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool databaseInitialized = currentSchemaVersion > 0;
+
+ if (databaseInitialized && currentSchemaVersion == DATABASE_SCHEMA_VERSION) {
+ // The database is up to date and ready to go.
+ return NS_OK;
+ }
+
+ // We are going to update the database, so everything from now on should be in
+ // a transaction for performances.
+ mozStorageTransaction transaction(mMainConn, false);
+
+ if (databaseInitialized) {
+ // Migration How-to:
+ //
+ // 1. increment PLACES_SCHEMA_VERSION.
+ // 2. implement a method that performs upgrade to your version from the
+ // previous one.
+ //
+ // NOTE: The downgrade process is pretty much complicated by the fact old
+ // versions cannot know what a new version is going to implement.
+ // The only thing we will do for downgrades is setting back the schema
+ // version, so that next upgrades will run again the migration step.
+
+ if (currentSchemaVersion > 36) {
+ // These versions are not downgradable.
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ if (currentSchemaVersion < DATABASE_SCHEMA_VERSION) {
+ *aDatabaseMigrated = true;
+
+ if (currentSchemaVersion < 11) {
+ // These are versions older than Firefox 4 that are not supported
+ // anymore. In this case it's safer to just replace the database.
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ // Firefox 4 uses schema version 11.
+
+ // Firefox 8 uses schema version 12.
+
+ if (currentSchemaVersion < 13) {
+ rv = MigrateV13Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (currentSchemaVersion < 15) {
+ rv = MigrateV15Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (currentSchemaVersion < 17) {
+ rv = MigrateV17Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 12 uses schema version 17.
+
+ if (currentSchemaVersion < 18) {
+ rv = MigrateV18Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (currentSchemaVersion < 19) {
+ rv = MigrateV19Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 13 uses schema version 19.
+
+ if (currentSchemaVersion < 20) {
+ rv = MigrateV20Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (currentSchemaVersion < 21) {
+ rv = MigrateV21Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 14 uses schema version 21.
+
+ if (currentSchemaVersion < 22) {
+ rv = MigrateV22Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 22 uses schema version 22.
+
+ if (currentSchemaVersion < 23) {
+ rv = MigrateV23Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 24 uses schema version 23.
+
+ if (currentSchemaVersion < 24) {
+ rv = MigrateV24Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 34 uses schema version 24.
+
+ if (currentSchemaVersion < 25) {
+ rv = MigrateV25Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 36 uses schema version 25.
+
+ if (currentSchemaVersion < 26) {
+ rv = MigrateV26Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 37 uses schema version 26.
+
+ if (currentSchemaVersion < 27) {
+ rv = MigrateV27Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (currentSchemaVersion < 28) {
+ rv = MigrateV28Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 39 uses schema version 28.
+
+ if (currentSchemaVersion < 30) {
+ rv = MigrateV30Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 41 uses schema version 30.
+
+ if (currentSchemaVersion < 31) {
+ rv = MigrateV31Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 48 uses schema version 31.
+
+ if (currentSchemaVersion < 32) {
+ rv = MigrateV32Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 49 uses schema version 32.
+
+ if (currentSchemaVersion < 33) {
+ rv = MigrateV33Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 50 uses schema version 33.
+
+ if (currentSchemaVersion < 34) {
+ rv = MigrateV34Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 51 uses schema version 34.
+
+ if (currentSchemaVersion < 35) {
+ rv = MigrateV35Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 52 uses schema version 35.
+
+ // Schema Upgrades must add migration code here.
+
+ rv = UpdateBookmarkRootTitles();
+ // We don't want a broken localization to cause us to think
+ // the database is corrupt and needs to be replaced.
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+ else {
+ // This is a new database, so we have to create all the tables and indices.
+
+ // moz_places.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_URL_HASH);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_FAVICON);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_REVHOST);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_VISITCOUNT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_FRECENCY);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_LASTVISITDATE);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_GUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_historyvisits.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HISTORYVISITS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_PLACEDATE);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_FROMVISIT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_VISITDATE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_inputhistory.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_INPUTHISTORY);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_hosts.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_bookmarks.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACETYPE);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PARENTPOSITION);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACELASTMODIFIED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_GUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_keywords.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_KEYWORDS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_favicons.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_FAVICONS);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_anno_attributes.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ANNO_ATTRIBUTES);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_annos.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ANNOS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ANNOS_PLACEATTRIBUTE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // moz_items_annos.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ITEMS_ANNOS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ITEMSANNOS_PLACEATTRIBUTE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Initialize the bookmark roots in the new DB.
+ rv = CreateBookmarkRoots();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set the schema version to the current one.
+ rv = mMainConn->SetSchemaVersion(DATABASE_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ForceWALCheckpoint();
+
+ // ANY FAILURE IN THIS METHOD WILL CAUSE US TO MARK THE DATABASE AS CORRUPT
+ // AND TRY TO REPLACE IT.
+ // DO NOT PUT HERE ANYTHING THAT IS NOT RELATED TO INITIALIZATION OR MODIFYING
+ // THE DISK DATABASE.
+
+ return NS_OK;
+}
+
+nsresult
+Database::CreateBookmarkRoots()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ services::GetStringBundleService();
+ NS_ENSURE_STATE(bundleService);
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = bundleService->CreateBundle(PLACES_BUNDLE, getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) return rv;
+
+ nsXPIDLString rootTitle;
+ // The first root's title is an empty string.
+ rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("places"),
+ NS_LITERAL_CSTRING("root________"), rootTitle);
+ if (NS_FAILED(rv)) return rv;
+
+ // Fetch the internationalized folder name from the string bundle.
+ rv = bundle->GetStringFromName(u"BookmarksMenuFolderTitle",
+ getter_Copies(rootTitle));
+ if (NS_FAILED(rv)) return rv;
+ rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("menu"),
+ NS_LITERAL_CSTRING("menu________"), rootTitle);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = bundle->GetStringFromName(u"BookmarksToolbarFolderTitle",
+ getter_Copies(rootTitle));
+ if (NS_FAILED(rv)) return rv;
+ rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("toolbar"),
+ NS_LITERAL_CSTRING("toolbar_____"), rootTitle);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = bundle->GetStringFromName(u"TagsFolderTitle",
+ getter_Copies(rootTitle));
+ if (NS_FAILED(rv)) return rv;
+ rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("tags"),
+ NS_LITERAL_CSTRING("tags________"), rootTitle);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = bundle->GetStringFromName(u"OtherBookmarksFolderTitle",
+ getter_Copies(rootTitle));
+ if (NS_FAILED(rv)) return rv;
+ rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("unfiled"),
+ NS_LITERAL_CSTRING("unfiled_____"), rootTitle);
+ if (NS_FAILED(rv)) return rv;
+
+ int64_t mobileRootId = CreateMobileRoot();
+ if (mobileRootId <= 0) return NS_ERROR_FAILURE;
+
+#if DEBUG
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT count(*), sum(position) FROM moz_bookmarks"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) return rv;
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ if (NS_FAILED(rv)) return rv;
+ MOZ_ASSERT(hasResult);
+ int32_t bookmarkCount = stmt->AsInt32(0);
+ int32_t positionSum = stmt->AsInt32(1);
+ MOZ_ASSERT(bookmarkCount == 6 && positionSum == 10);
+#endif
+
+ return NS_OK;
+}
+
+nsresult
+Database::InitFunctions()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = GetUnreversedHostFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = MatchAutoCompleteFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = CalculateFrecencyFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GenerateGUIDFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = FixupURLFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = FrecencyNotificationFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = StoreLastInsertedIdFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = HashFunction::create(mMainConn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::InitTempEntities()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mMainConn->ExecuteSimpleSQL(CREATE_HISTORYVISITS_AFTERINSERT_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_HISTORYVISITS_AFTERDELETE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the triggers that update the moz_hosts table as necessary.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERINSERT_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_UPDATEHOSTS_TEMP);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_UPDATEHOSTS_AFTERDELETE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERDELETE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_FRECENCY_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::UpdateBookmarkRootTitles()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ services::GetStringBundleService();
+ NS_ENSURE_STATE(bundleService);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = bundleService->CreateBundle(PLACES_BUNDLE, getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET title = :new_title WHERE guid = :guid"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ rv = stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ if (NS_FAILED(rv)) return rv;
+
+ const char *rootGuids[] = { "menu________"
+ , "toolbar_____"
+ , "tags________"
+ , "unfiled_____"
+ , "mobile______"
+ };
+ const char *titleStringIDs[] = { "BookmarksMenuFolderTitle"
+ , "BookmarksToolbarFolderTitle"
+ , "TagsFolderTitle"
+ , "OtherBookmarksFolderTitle"
+ , "MobileBookmarksFolderTitle"
+ };
+
+ for (uint32_t i = 0; i < ArrayLength(rootGuids); ++i) {
+ nsXPIDLString title;
+ rv = bundle->GetStringFromName(NS_ConvertASCIItoUTF16(titleStringIDs[i]).get(),
+ getter_Copies(title));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<mozIStorageBindingParams> params;
+ rv = paramsArray->NewBindingParams(getter_AddRefs(params));
+ if (NS_FAILED(rv)) return rv;
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ nsDependentCString(rootGuids[i]));
+ if (NS_FAILED(rv)) return rv;
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("new_title"),
+ NS_ConvertUTF16toUTF8(title));
+ if (NS_FAILED(rv)) return rv;
+ rv = paramsArray->AddParams(params);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ rv = stmt->BindParameters(paramsArray);
+ if (NS_FAILED(rv)) return rv;
+ nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
+ rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt));
+ if (NS_FAILED(rv)) return rv;
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV13Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Dynamic containers are no longer supported.
+ nsCOMPtr<mozIStorageAsyncStatement> deleteDynContainersStmt;
+ nsresult rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_bookmarks WHERE type = :item_type"),
+ getter_AddRefs(deleteDynContainersStmt));
+ rv = deleteDynContainersStmt->BindInt32ByName(
+ NS_LITERAL_CSTRING("item_type"),
+ nsINavBookmarksService::TYPE_DYNAMIC_CONTAINER
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = deleteDynContainersStmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV15Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Drop moz_bookmarks_beforedelete_v1_trigger, since it's more expensive than
+ // useful.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TRIGGER IF EXISTS moz_bookmarks_beforedelete_v1_trigger"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove any orphan keywords.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_keywords "
+ "WHERE NOT EXISTS ( "
+ "SELECT id "
+ "FROM moz_bookmarks "
+ "WHERE keyword_id = moz_keywords.id "
+ ")"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV17Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ bool tableExists = false;
+
+ nsresult rv = mMainConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &tableExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!tableExists) {
+ // For anyone who used in-development versions of this autocomplete,
+ // drop the old tables and its indexes.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP INDEX IF EXISTS moz_hostnames_frecencyindex"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE IF EXISTS moz_hostnames"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the moz_hosts table so we can get hostnames for URL autocomplete.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Fill the moz_hosts table with all the domains in moz_places.
+ nsCOMPtr<mozIStorageAsyncStatement> fillHostsStmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_hosts (host, frecency) "
+ "SELECT fixup_url(get_unreversed_host(h.rev_host)) AS host, "
+ "(SELECT MAX(frecency) FROM moz_places "
+ "WHERE rev_host = h.rev_host "
+ "OR rev_host = h.rev_host || 'www.' "
+ ") AS frecency "
+ "FROM moz_places h "
+ "WHERE LENGTH(h.rev_host) > 1 "
+ "GROUP BY h.rev_host"
+ ), getter_AddRefs(fillHostsStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = fillHostsStmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV18Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // moz_hosts should distinguish on typed entries.
+
+ // Check if the profile already has a typed column.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT typed FROM moz_hosts"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_hosts ADD COLUMN typed NOT NULL DEFAULT 0"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // With the addition of the typed column the covering index loses its
+ // advantages. On the other side querying on host and (optionally) typed
+ // largely restricts the number of results, making scans decently fast.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP INDEX IF EXISTS moz_hosts_frecencyhostindex"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update typed data.
+ nsCOMPtr<mozIStorageAsyncStatement> updateTypedStmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_hosts SET typed = 1 WHERE host IN ( "
+ "SELECT fixup_url(get_unreversed_host(rev_host)) "
+ "FROM moz_places WHERE typed = 1 "
+ ") "
+ ), getter_AddRefs(updateTypedStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = updateTypedStmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV19Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Livemarks children are no longer bookmarks.
+
+ // Remove all children of folders annotated as livemarks.
+ nsCOMPtr<mozIStorageStatement> deleteLivemarksChildrenStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_bookmarks WHERE parent IN("
+ "SELECT b.id FROM moz_bookmarks b "
+ "JOIN moz_items_annos a ON a.item_id = b.id "
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+ "WHERE b.type = :item_type AND n.name = :anno_name "
+ ")"
+ ), getter_AddRefs(deleteLivemarksChildrenStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksChildrenStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(LMANNO_FEEDURI)
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksChildrenStmt->BindInt32ByName(
+ NS_LITERAL_CSTRING("item_type"), nsINavBookmarksService::TYPE_FOLDER
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksChildrenStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clear obsolete livemark prefs.
+ (void)Preferences::ClearUser("browser.bookmarks.livemark_refresh_seconds");
+ (void)Preferences::ClearUser("browser.bookmarks.livemark_refresh_limit_count");
+ (void)Preferences::ClearUser("browser.bookmarks.livemark_refresh_delay_time");
+
+ // Remove the old status annotations.
+ nsCOMPtr<mozIStorageStatement> deleteLivemarksAnnosStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_items_annos WHERE anno_attribute_id IN("
+ "SELECT id FROM moz_anno_attributes "
+ "WHERE name IN (:anno_loading, :anno_loadfailed, :anno_expiration) "
+ ")"
+ ), getter_AddRefs(deleteLivemarksAnnosStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_loading"), NS_LITERAL_CSTRING("livemark/loading")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_loadfailed"), NS_LITERAL_CSTRING("livemark/loadfailed")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_expiration"), NS_LITERAL_CSTRING("livemark/expiration")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove orphan annotation names.
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_anno_attributes "
+ "WHERE name IN (:anno_loading, :anno_loadfailed, :anno_expiration) "
+ ), getter_AddRefs(deleteLivemarksAnnosStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_loading"), NS_LITERAL_CSTRING("livemark/loading")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_loadfailed"), NS_LITERAL_CSTRING("livemark/loadfailed")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_expiration"), NS_LITERAL_CSTRING("livemark/expiration")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteLivemarksAnnosStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV20Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Remove obsolete bookmark GUID annotations.
+ nsCOMPtr<mozIStorageStatement> deleteOldBookmarkGUIDAnnosStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_items_annos WHERE anno_attribute_id = ("
+ "SELECT id FROM moz_anno_attributes "
+ "WHERE name = :anno_guid"
+ ")"
+ ), getter_AddRefs(deleteOldBookmarkGUIDAnnosStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteOldBookmarkGUIDAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_guid"), NS_LITERAL_CSTRING("placesInternal/GUID")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteOldBookmarkGUIDAnnosStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove the orphan annotation name.
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_anno_attributes "
+ "WHERE name = :anno_guid"
+ ), getter_AddRefs(deleteOldBookmarkGUIDAnnosStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteOldBookmarkGUIDAnnosStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_guid"), NS_LITERAL_CSTRING("placesInternal/GUID")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteOldBookmarkGUIDAnnosStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV21Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Add a prefix column to moz_hosts.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT prefix FROM moz_hosts"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_hosts ADD COLUMN prefix"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV22Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Reset all session IDs to 0 since we don't support them anymore.
+ // We don't set them to NULL to avoid breaking downgrades.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_historyvisits SET session = 0"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+nsresult
+Database::MigrateV23Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Recalculate hosts prefixes.
+ nsCOMPtr<mozIStorageAsyncStatement> updatePrefixesStmt;
+ nsresult rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_hosts SET prefix = ( " HOSTS_PREFIX_PRIORITY_FRAGMENT ") "
+ ), getter_AddRefs(updatePrefixesStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = updatePrefixesStmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV24Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Add a foreign_count column to moz_places
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT foreign_count FROM moz_places"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_places ADD COLUMN foreign_count INTEGER DEFAULT 0 NOT NULL"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Adjust counts for all the rows
+ nsCOMPtr<mozIStorageStatement> updateStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_places SET foreign_count = "
+ "(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id) "
+ ), getter_AddRefs(updateStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper updateScoper(updateStmt);
+ rv = updateStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV25Up()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Change bookmark roots GUIDs to constant values.
+
+ // If moz_bookmarks_roots doesn't exist anymore, it's because we finally have
+ // been able to remove it. In such a case, we already assigned constant GUIDs
+ // to the roots and we can skip this migration.
+ {
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT root_name FROM moz_bookmarks_roots"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+ }
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET guid = :guid "
+ "WHERE id = (SELECT folder_id FROM moz_bookmarks_roots WHERE root_name = :name) "
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char *rootNames[] = { "places", "menu", "toolbar", "tags", "unfiled" };
+ const char *rootGuids[] = { "root________"
+ , "menu________"
+ , "toolbar_____"
+ , "tags________"
+ , "unfiled_____"
+ };
+
+ for (uint32_t i = 0; i < ArrayLength(rootNames); ++i) {
+ // Since this is using the synchronous API, we cannot use
+ // a BindingParamsArray.
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("name"),
+ nsDependentCString(rootNames[i]));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ nsDependentCString(rootGuids[i]));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV26Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Round down dateAdded and lastModified values to milliseconds precision.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET dateAdded = dateAdded - dateAdded % 1000, "
+ " lastModified = lastModified - lastModified % 1000"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV27Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Change keywords store, moving their relation from bookmarks to urls.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT place_id FROM moz_keywords"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+ // Even if these 2 columns have a unique constraint, we allow NULL values
+ // for backwards compatibility. NULL never breaks a unique constraint.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_keywords ADD COLUMN place_id INTEGER"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_keywords ADD COLUMN post_data TEXT"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Associate keywords with uris. A keyword could be associated to multiple
+ // bookmarks uris, or multiple keywords could be associated to the same uri.
+ // The new system only allows multiple uris per keyword, provided they have
+ // a different post_data value.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT OR REPLACE INTO moz_keywords (id, keyword, place_id, post_data) "
+ "SELECT k.id, k.keyword, h.id, MAX(a.content) "
+ "FROM moz_places h "
+ "JOIN moz_bookmarks b ON b.fk = h.id "
+ "JOIN moz_keywords k ON k.id = b.keyword_id "
+ "LEFT JOIN moz_items_annos a ON a.item_id = b.id "
+ "AND a.anno_attribute_id = (SELECT id FROM moz_anno_attributes "
+ "WHERE name = 'bookmarkProperties/POSTData') "
+ "WHERE k.place_id ISNULL "
+ "GROUP BY keyword"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Remove any keyword that points to a non-existing place id.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_keywords "
+ "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = moz_keywords.place_id)"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET keyword_id = NULL "
+ "WHERE NOT EXISTS (SELECT 1 FROM moz_keywords WHERE id = moz_bookmarks.keyword_id)"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Adjust foreign_count for all the rows.
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_places SET foreign_count = "
+ "(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id) + "
+ "(SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id) "
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV28Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // v27 migration was bogus and set some unrelated annotations as post_data for
+ // keywords having an annotated bookmark.
+ // The current v27 migration function is fixed, but we still need to handle
+ // users that hit the bogus version. Since we can't distinguish, we'll just
+ // set again all of the post data.
+ DebugOnly<nsresult> rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_keywords "
+ "SET post_data = ( "
+ "SELECT content FROM moz_items_annos a "
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+ "JOIN moz_bookmarks b on b.id = a.item_id "
+ "WHERE n.name = 'bookmarkProperties/POSTData' "
+ "AND b.keyword_id = moz_keywords.id "
+ "ORDER BY b.lastModified DESC "
+ "LIMIT 1 "
+ ") "
+ "WHERE EXISTS(SELECT 1 FROM moz_bookmarks WHERE keyword_id = moz_keywords.id) "
+ ));
+ // In case the update fails a constraint, we don't want to throw away the
+ // whole database for just a few keywords. In rare cases the user might have
+ // to recreate them. Though, at this point, there shouldn't be 2 keywords
+ // pointing to the same url and post data, cause the previous migration step
+ // removed them.
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV30Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP INDEX IF EXISTS moz_favicons_guid_uniqueindex"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV31Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE IF EXISTS moz_bookmarks_roots"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV32Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Remove some old and no more used Places preferences that may be confusing
+ // for the user.
+ mozilla::Unused << Preferences::ClearUser("places.history.expiration.transient_optimal_database_size");
+ mozilla::Unused << Preferences::ClearUser("places.last_vacuum");
+ mozilla::Unused << Preferences::ClearUser("browser.history_expire_sites");
+ mozilla::Unused << Preferences::ClearUser("browser.history_expire_days.mirror");
+ mozilla::Unused << Preferences::ClearUser("browser.history_expire_days_min");
+
+ // For performance reasons we want to remove too long urls from history.
+ // We cannot use the moz_places triggers here, cause they are defined only
+ // after the schema migration. Thus we need to collect the hosts that need to
+ // be updated first.
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TEMP TABLE moz_migrate_v32_temp ("
+ "host TEXT PRIMARY KEY "
+ ") WITHOUT ROWID "
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ {
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_migrate_v32_temp (host) "
+ "SELECT fixup_url(get_unreversed_host(rev_host)) "
+ "FROM moz_places WHERE LENGTH(url) > :maxlen AND foreign_count = 0"
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("maxlen"), MaxUrlLength());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Now remove the pages with a long url.
+ {
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_places WHERE LENGTH(url) > :maxlen AND foreign_count = 0"
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("maxlen"), MaxUrlLength());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Expire orphan visits and update moz_hosts.
+ // These may be a bit more expensive and are not critical for the DB
+ // functionality, so we execute them asynchronously.
+ nsCOMPtr<mozIStorageAsyncStatement> expireOrphansStmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_historyvisits "
+ "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = place_id)"
+ ), getter_AddRefs(expireOrphansStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStorageAsyncStatement> deleteHostsStmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_hosts "
+ "WHERE host IN (SELECT host FROM moz_migrate_v32_temp) "
+ "AND NOT EXISTS("
+ "SELECT 1 FROM moz_places "
+ "WHERE rev_host = get_unreversed_host(host || '.') || '.' "
+ "OR rev_host = get_unreversed_host(host || '.') || '.www.' "
+ "); "
+ ), getter_AddRefs(deleteHostsStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStorageAsyncStatement> updateHostsStmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_hosts "
+ "SET prefix = (" HOSTS_PREFIX_PRIORITY_FRAGMENT ") "
+ "WHERE host IN (SELECT host FROM moz_migrate_v32_temp) "
+ ), getter_AddRefs(updateHostsStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStorageAsyncStatement> dropTableStmt;
+ rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DROP TABLE IF EXISTS moz_migrate_v32_temp"
+ ), getter_AddRefs(dropTableStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozIStorageBaseStatement *stmts[] = {
+ expireOrphansStmt,
+ deleteHostsStmt,
+ updateHostsStmt,
+ dropTableStmt
+ };
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = mMainConn->ExecuteAsync(stmts, ArrayLength(stmts), nullptr,
+ getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV33Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP INDEX IF EXISTS moz_places_url_uniqueindex"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add an url_hash column to moz_places.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT url_hash FROM moz_places"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) {
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_places ADD COLUMN url_hash INTEGER DEFAULT 0 NOT NULL"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create an index on url_hash.
+ rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_URL_HASH);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV34Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_keywords WHERE id IN ( "
+ "SELECT id FROM moz_keywords k "
+ "WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) "
+ ")"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Database::MigrateV35Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ int64_t mobileRootId = CreateMobileRoot();
+ if (mobileRootId <= 0) {
+ // Either the schema is broken or there isn't any root. The latter can
+ // happen if a consumer, for example Thunderbird, never used bookmarks.
+ // If there are no roots, this migration should not run.
+ nsCOMPtr<mozIStorageStatement> checkRootsStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id FROM moz_bookmarks WHERE parent = 0"
+ ), getter_AddRefs(checkRootsStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(checkRootsStmt);
+ bool hasResult = false;
+ rv = checkRootsStmt->ExecuteStep(&hasResult);
+ if (NS_SUCCEEDED(rv) && !hasResult) {
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+
+ // At this point, we should have no more than two folders with the mobile
+ // bookmarks anno: the new root, and the old folder if one exists. If, for
+ // some reason, we have multiple folders with the anno, we append their
+ // children to the new root.
+ nsTArray<int64_t> folderIds;
+ nsresult rv = GetItemsWithAnno(NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO),
+ nsINavBookmarksService::TYPE_FOLDER,
+ folderIds);
+ if (NS_FAILED(rv)) return rv;
+
+ for (uint32_t i = 0; i < folderIds.Length(); ++i) {
+ if (folderIds[i] == mobileRootId) {
+ // Ignore the new mobile root. We'll remove this anno from the root in
+ // bug 1306445.
+ continue;
+ }
+
+ // Append the folder's children to the new root.
+ nsCOMPtr<mozIStorageStatement> moveStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks "
+ "SET parent = :root_id, "
+ "position = position + IFNULL("
+ "(SELECT MAX(position) + 1 FROM moz_bookmarks "
+ "WHERE parent = :root_id), 0)"
+ "WHERE parent = :folder_id"
+ ), getter_AddRefs(moveStmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper moveScoper(moveStmt);
+
+ rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"),
+ mobileRootId);
+ if (NS_FAILED(rv)) return rv;
+ rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("folder_id"),
+ folderIds[i]);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = moveStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ // Delete the old folder.
+ rv = DeleteBookmarkItem(folderIds[i]);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
+ nsTArray<int64_t>& aItemIds)
+{
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT b.id FROM moz_items_annos a "
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+ "JOIN moz_bookmarks b ON b.id = a.item_id "
+ "WHERE n.name = :anno_name AND "
+ "b.type = :item_type"
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aAnnoName);
+ if (NS_FAILED(rv)) return rv;
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_type"), aItemType);
+ if (NS_FAILED(rv)) return rv;
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ int64_t itemId;
+ rv = stmt->GetInt64(0, &itemId);
+ if (NS_FAILED(rv)) return rv;
+ aItemIds.AppendElement(itemId);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Database::DeleteBookmarkItem(int32_t aItemId)
+{
+ // Delete the old bookmark.
+ nsCOMPtr<mozIStorageStatement> deleteStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_bookmarks WHERE id = :item_id"
+ ), getter_AddRefs(deleteStmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper deleteScoper(deleteStmt);
+
+ rv = deleteStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+ aItemId);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = deleteStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ // Clean up orphan annotations.
+ nsCOMPtr<mozIStorageStatement> removeAnnosStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_items_annos WHERE item_id = :item_id"
+ ), getter_AddRefs(removeAnnosStmt));
+ if (NS_FAILED(rv)) return rv;
+ mozStorageStatementScoper removeAnnosScoper(removeAnnosStmt);
+
+ rv = removeAnnosStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+ aItemId);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = removeAnnosStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+
+ return NS_OK;
+}
+
+int64_t
+Database::CreateMobileRoot()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Create the mobile root, ignoring conflicts if one already exists (for
+ // example, if the user downgraded to an earlier release channel).
+ nsCOMPtr<mozIStorageStatement> createStmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_bookmarks "
+ "(type, title, dateAdded, lastModified, guid, position, parent) "
+ "SELECT :item_type, :item_title, :timestamp, :timestamp, :guid, "
+ "(SELECT COUNT(*) FROM moz_bookmarks p WHERE p.parent = b.id), b.id "
+ "FROM moz_bookmarks b WHERE b.parent = 0"
+ ), getter_AddRefs(createStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper createScoper(createStmt);
+
+ rv = createStmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"),
+ nsINavBookmarksService::TYPE_FOLDER);
+ if (NS_FAILED(rv)) return -1;
+ rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
+ NS_LITERAL_CSTRING(MOBILE_ROOT_TITLE));
+ if (NS_FAILED(rv)) return -1;
+ rv = createStmt->BindInt64ByName(NS_LITERAL_CSTRING("timestamp"),
+ RoundedPRNow());
+ if (NS_FAILED(rv)) return -1;
+ rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ NS_LITERAL_CSTRING(MOBILE_ROOT_GUID));
+ if (NS_FAILED(rv)) return -1;
+
+ rv = createStmt->Execute();
+ if (NS_FAILED(rv)) return -1;
+
+ // Find the mobile root ID. We can't use the last inserted ID because the
+ // root might already exist, and we ignore on conflict.
+ nsCOMPtr<mozIStorageStatement> findIdStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id FROM moz_bookmarks WHERE guid = :guid"
+ ), getter_AddRefs(findIdStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper findIdScoper(findIdStmt);
+
+ rv = findIdStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+ NS_LITERAL_CSTRING(MOBILE_ROOT_GUID));
+ if (NS_FAILED(rv)) return -1;
+
+ bool hasResult = false;
+ rv = findIdStmt->ExecuteStep(&hasResult);
+ if (NS_FAILED(rv) || !hasResult) return -1;
+
+ int64_t rootId;
+ rv = findIdStmt->GetInt64(0, &rootId);
+ if (NS_FAILED(rv)) return -1;
+
+ // Set the mobile bookmarks anno on the new root, so that Sync code on an
+ // older channel can still find it in case of a downgrade. This can be
+ // removed in bug 1306445.
+ nsCOMPtr<mozIStorageStatement> addAnnoNameStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)"
+ ), getter_AddRefs(addAnnoNameStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper addAnnoNameScoper(addAnnoNameStmt);
+
+ rv = addAnnoNameStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO));
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoNameStmt->Execute();
+ if (NS_FAILED(rv)) return -1;
+
+ nsCOMPtr<mozIStorageStatement> addAnnoStmt;
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT OR IGNORE INTO moz_items_annos "
+ "(id, item_id, anno_attribute_id, content, flags, "
+ "expiration, type, dateAdded, lastModified) "
+ "SELECT "
+ "(SELECT a.id FROM moz_items_annos a "
+ "WHERE a.anno_attribute_id = n.id AND "
+ "a.item_id = :root_id), "
+ ":root_id, n.id, 1, 0, :expiration, :type, :timestamp, :timestamp "
+ "FROM moz_anno_attributes n WHERE name = :anno_name"
+ ), getter_AddRefs(addAnnoStmt));
+ if (NS_FAILED(rv)) return -1;
+ mozStorageStatementScoper addAnnoScoper(addAnnoStmt);
+
+ rv = addAnnoStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"), rootId);
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO));
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("expiration"),
+ nsIAnnotationService::EXPIRE_NEVER);
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("type"),
+ nsIAnnotationService::TYPE_INT32);
+ if (NS_FAILED(rv)) return -1;
+ rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("timestamp"),
+ RoundedPRNow());
+ if (NS_FAILED(rv)) return -1;
+
+ rv = addAnnoStmt->Execute();
+ if (NS_FAILED(rv)) return -1;
+
+ return rootId;
+}
+
+void
+Database::Shutdown()
+{
+ // As the last step in the shutdown path, finalize the database handle.
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mClosed);
+
+ // Break cycles with the shutdown blockers.
+ mClientsShutdown = nullptr;
+ nsCOMPtr<mozIStorageCompletionCallback> connectionShutdown = mConnectionShutdown.forget();
+
+ if (!mMainConn) {
+ // The connection has never been initialized. Just mark it as closed.
+ mClosed = true;
+ (void)connectionShutdown->Complete(NS_OK, nullptr);
+ return;
+ }
+
+#ifdef DEBUG
+ { // Sanity check for missing guids.
+ bool haveNullGuids = false;
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 "
+ "FROM moz_places "
+ "WHERE guid IS NULL "
+ ), getter_AddRefs(stmt));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = stmt->ExecuteStep(&haveNullGuids);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(!haveNullGuids, "Found a page without a GUID!");
+
+ rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 "
+ "FROM moz_bookmarks "
+ "WHERE guid IS NULL "
+ ), getter_AddRefs(stmt));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = stmt->ExecuteStep(&haveNullGuids);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(!haveNullGuids, "Found a bookmark without a GUID!");
+ }
+
+ { // Sanity check for unrounded dateAdded and lastModified values (bug
+ // 1107308).
+ bool hasUnroundedDates = false;
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 "
+ "FROM moz_bookmarks "
+ "WHERE dateAdded % 1000 > 0 OR lastModified % 1000 > 0 LIMIT 1"
+ ), getter_AddRefs(stmt));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = stmt->ExecuteStep(&hasUnroundedDates);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(!hasUnroundedDates, "Found unrounded dates!");
+ }
+
+ { // Sanity check url_hash
+ bool hasNullHash = false;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 FROM moz_places WHERE url_hash = 0"
+ ), getter_AddRefs(stmt));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = stmt->ExecuteStep(&hasNullHash);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(!hasNullHash, "Found a place without a hash!");
+ }
+
+ { // Sanity check unique urls
+ bool hasDupeUrls = false;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 FROM moz_places GROUP BY url HAVING count(*) > 1 "
+ ), getter_AddRefs(stmt));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = stmt->ExecuteStep(&hasDupeUrls);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(!hasDupeUrls, "Found a duplicate url!");
+ }
+#endif
+
+ mMainThreadStatements.FinalizeStatements();
+ mMainThreadAsyncStatements.FinalizeStatements();
+
+ RefPtr< FinalizeStatementCacheProxy<mozIStorageStatement> > event =
+ new FinalizeStatementCacheProxy<mozIStorageStatement>(
+ mAsyncThreadStatements,
+ NS_ISUPPORTS_CAST(nsIObserver*, this)
+ );
+ DispatchToAsyncThread(event);
+
+ mClosed = true;
+
+ (void)mMainConn->AsyncClose(connectionShutdown);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+Database::Observe(nsISupports *aSubject,
+ const char *aTopic,
+ const char16_t *aData)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (strcmp(aTopic, TOPIC_PROFILE_CHANGE_TEARDOWN) == 0) {
+ // Tests simulating shutdown may cause multiple notifications.
+ if (IsShutdownStarted()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ NS_ENSURE_STATE(os);
+
+ // If shutdown happens in the same mainthread loop as init, observers could
+ // handle the places-init-complete notification after xpcom-shutdown, when
+ // the connection does not exist anymore. Removing those observers would
+ // be less expensive but may cause their RemoveObserver calls to throw.
+ // Thus notify the topic now, so they stop listening for it.
+ nsCOMPtr<nsISimpleEnumerator> e;
+ if (NS_SUCCEEDED(os->EnumerateObservers(TOPIC_PLACES_INIT_COMPLETE,
+ getter_AddRefs(e))) && e) {
+ bool hasMore = false;
+ while (NS_SUCCEEDED(e->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> supports;
+ if (NS_SUCCEEDED(e->GetNext(getter_AddRefs(supports)))) {
+ nsCOMPtr<nsIObserver> observer = do_QueryInterface(supports);
+ (void)observer->Observe(observer, TOPIC_PLACES_INIT_COMPLETE, nullptr);
+ }
+ }
+ }
+
+ // Notify all Places users that we are about to shutdown.
+ (void)os->NotifyObservers(nullptr, TOPIC_PLACES_SHUTDOWN, nullptr);
+ } else if (strcmp(aTopic, TOPIC_SIMULATE_PLACES_SHUTDOWN) == 0) {
+ // This notification is (and must be) only used by tests that are trying
+ // to simulate Places shutdown out of the normal shutdown path.
+
+ // Tests simulating shutdown may cause re-entrance.
+ if (IsShutdownStarted()) {
+ return NS_OK;
+ }
+
+ // We are simulating a shutdown, so invoke the shutdown blockers,
+ // wait for them, then proceed with connection shutdown.
+ // Since we are already going through shutdown, but it's not the real one,
+ // we won't need to block the real one anymore, so we can unblock it.
+ {
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileChangeTeardownPhase();
+ if (shutdownPhase) {
+ shutdownPhase->RemoveBlocker(mClientsShutdown.get());
+ }
+ (void)mClientsShutdown->BlockShutdown(nullptr);
+ }
+
+ // Spin the events loop until the clients are done.
+ // Note, this is just for tests, specifically test_clearHistory_shutdown.js
+ while (mClientsShutdown->State() != PlacesShutdownBlocker::States::RECEIVED_DONE) {
+ (void)NS_ProcessNextEvent();
+ }
+
+ {
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileBeforeChangePhase();
+ if (shutdownPhase) {
+ shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
+ }
+ (void)mConnectionShutdown->BlockShutdown(nullptr);
+ }
+ }
+ return NS_OK;
+}
+
+uint32_t
+Database::MaxUrlLength() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mMaxUrlLength) {
+ mMaxUrlLength = Preferences::GetInt(PREF_HISTORY_MAXURLLEN,
+ PREF_HISTORY_MAXURLLEN_DEFAULT);
+ if (mMaxUrlLength < 255 || mMaxUrlLength > INT32_MAX) {
+ mMaxUrlLength = PREF_HISTORY_MAXURLLEN_DEFAULT;
+ }
+ }
+ return mMaxUrlLength;
+}
+
+
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/Database.h b/toolkit/components/places/Database.h
new file mode 100644
index 0000000000..22488fddbe
--- /dev/null
+++ b/toolkit/components/places/Database.h
@@ -0,0 +1,331 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla_places_Database_h_
+#define mozilla_places_Database_h_
+
+#include "MainThreadUtils.h"
+#include "nsWeakReference.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIObserver.h"
+#include "nsIAsyncShutdown.h"
+#include "mozilla/storage.h"
+#include "mozilla/storage/StatementCache.h"
+#include "mozilla/Attributes.h"
+#include "nsIEventTarget.h"
+#include "Shutdown.h"
+
+// This is the schema version. Update it at any schema change and add a
+// corresponding migrateVxx method below.
+#define DATABASE_SCHEMA_VERSION 35
+
+// Fired after Places inited.
+#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
+// Fired when initialization fails due to a locked database.
+#define TOPIC_DATABASE_LOCKED "places-database-locked"
+// This topic is received when the profile is about to be lost. Places does
+// initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
+// Any shutdown work that requires the Places APIs should happen here.
+#define TOPIC_PROFILE_CHANGE_TEARDOWN "profile-change-teardown"
+// Fired when Places is shutting down. Any code should stop accessing Places
+// APIs after this notification. If you need to listen for Places shutdown
+// you should only use this notification, next ones are intended only for
+// internal Places use.
+#define TOPIC_PLACES_SHUTDOWN "places-shutdown"
+// For Internal use only. Fired when connection is about to be closed, only
+// cleanup tasks should run at this stage, nothing should be added to the
+// database, nor APIs should be called.
+#define TOPIC_PLACES_WILL_CLOSE_CONNECTION "places-will-close-connection"
+// Fired when the connection has gone, nothing will work from now on.
+#define TOPIC_PLACES_CONNECTION_CLOSED "places-connection-closed"
+
+// Simulate profile-before-change. This topic may only be used by
+// calling `observe` directly on the database. Used for testing only.
+#define TOPIC_SIMULATE_PLACES_SHUTDOWN "test-simulate-places-shutdown"
+
+class nsIRunnable;
+
+namespace mozilla {
+namespace places {
+
+enum JournalMode {
+ // Default SQLite journal mode.
+ JOURNAL_DELETE = 0
+ // Can reduce fsyncs on Linux when journal is deleted (See bug 460315).
+ // We fallback to this mode when WAL is unavailable.
+, JOURNAL_TRUNCATE
+ // Unsafe in case of crashes on database swap or low memory.
+, JOURNAL_MEMORY
+ // Can reduce number of fsyncs. We try to use this mode by default.
+, JOURNAL_WAL
+};
+
+class ClientsShutdownBlocker;
+class ConnectionShutdownBlocker;
+
+class Database final : public nsIObserver
+ , public nsSupportsWeakReference
+{
+ typedef mozilla::storage::StatementCache<mozIStorageStatement> StatementCache;
+ typedef mozilla::storage::StatementCache<mozIStorageAsyncStatement> AsyncStatementCache;
+
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ Database();
+
+ /**
+ * Initializes the database connection and the schema.
+ * In case of corruption the database is copied to a backup file and replaced.
+ */
+ nsresult Init();
+
+ /**
+ * The AsyncShutdown client used by clients of this API to be informed of shutdown.
+ */
+ already_AddRefed<nsIAsyncShutdownClient> GetClientsShutdown();
+
+ /**
+ * Getter to use when instantiating the class.
+ *
+ * @return Singleton instance of this class.
+ */
+ static already_AddRefed<Database> GetDatabase();
+
+ /**
+ * Returns last known database status.
+ *
+ * @return one of the nsINavHistoryService::DATABASE_STATUS_* constants.
+ */
+ uint16_t GetDatabaseStatus() const
+ {
+ return mDatabaseStatus;
+ }
+
+ /**
+ * Returns a pointer to the storage connection.
+ *
+ * @return The connection handle.
+ */
+ mozIStorageConnection* MainConn() const
+ {
+ return mMainConn;
+ }
+
+ /**
+ * Dispatches a runnable to the connection async thread, to be serialized
+ * with async statements.
+ *
+ * @param aEvent
+ * The runnable to be dispatched.
+ */
+ void DispatchToAsyncThread(nsIRunnable* aEvent) const
+ {
+ if (mClosed) {
+ return;
+ }
+ nsCOMPtr<nsIEventTarget> target = do_GetInterface(mMainConn);
+ if (target) {
+ (void)target->Dispatch(aEvent, NS_DISPATCH_NORMAL);
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Statements Getters.
+
+ /**
+ * Gets a cached synchronous statement.
+ *
+ * @param aQuery
+ * SQL query literal.
+ * @return The cached statement.
+ * @note Always null check the result.
+ * @note Always use a scoper to reset the statement.
+ */
+ template<int N>
+ already_AddRefed<mozIStorageStatement>
+ GetStatement(const char (&aQuery)[N]) const
+ {
+ nsDependentCString query(aQuery, N - 1);
+ return GetStatement(query);
+ }
+
+ /**
+ * Gets a cached synchronous statement.
+ *
+ * @param aQuery
+ * nsCString of SQL query.
+ * @return The cached statement.
+ * @note Always null check the result.
+ * @note Always use a scoper to reset the statement.
+ */
+ already_AddRefed<mozIStorageStatement> GetStatement(const nsACString& aQuery) const;
+
+ /**
+ * Gets a cached asynchronous statement.
+ *
+ * @param aQuery
+ * SQL query literal.
+ * @return The cached statement.
+ * @note Always null check the result.
+ * @note AsyncStatements are automatically reset on execution.
+ */
+ template<int N>
+ already_AddRefed<mozIStorageAsyncStatement>
+ GetAsyncStatement(const char (&aQuery)[N]) const
+ {
+ nsDependentCString query(aQuery, N - 1);
+ return GetAsyncStatement(query);
+ }
+
+ /**
+ * Gets a cached asynchronous statement.
+ *
+ * @param aQuery
+ * nsCString of SQL query.
+ * @return The cached statement.
+ * @note Always null check the result.
+ * @note AsyncStatements are automatically reset on execution.
+ */
+ already_AddRefed<mozIStorageAsyncStatement> GetAsyncStatement(const nsACString& aQuery) const;
+
+ uint32_t MaxUrlLength();
+
+protected:
+ /**
+ * Finalizes the cached statements and closes the database connection.
+ * A TOPIC_PLACES_CONNECTION_CLOSED notification is fired when done.
+ */
+ void Shutdown();
+
+ bool IsShutdownStarted() const;
+
+ /**
+ * Initializes the database file. If the database does not exist or is
+ * corrupt, a new one is created. In case of corruption it also creates a
+ * backup copy of the database.
+ *
+ * @param aStorage
+ * mozStorage service instance.
+ * @param aNewDatabaseCreated
+ * whether a new database file has been created.
+ */
+ nsresult InitDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
+ bool* aNewDatabaseCreated);
+
+ /**
+ * Creates a database backup and replaces the original file with a new
+ * one.
+ *
+ * @param aStorage
+ * mozStorage service instance.
+ */
+ nsresult BackupAndReplaceDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage);
+
+ /**
+ * Initializes the database. This performs any necessary migrations for the
+ * database. All migration is done inside a transaction that is rolled back
+ * if any error occurs.
+ * @param aDatabaseMigrated
+ * Whether a schema upgrade happened.
+ */
+ nsresult InitSchema(bool* aDatabaseMigrated);
+
+ /**
+ * Creates bookmark roots in a new DB.
+ */
+ nsresult CreateBookmarkRoots();
+
+ /**
+ * Initializes additionale SQLite functions, defined in SQLFunctions.h
+ */
+ nsresult InitFunctions();
+
+ /**
+ * Initializes temp entities, like triggers, tables, views...
+ */
+ nsresult InitTempEntities();
+
+ /**
+ * Helpers used by schema upgrades.
+ */
+ nsresult MigrateV13Up();
+ nsresult MigrateV15Up();
+ nsresult MigrateV17Up();
+ nsresult MigrateV18Up();
+ nsresult MigrateV19Up();
+ nsresult MigrateV20Up();
+ nsresult MigrateV21Up();
+ nsresult MigrateV22Up();
+ nsresult MigrateV23Up();
+ nsresult MigrateV24Up();
+ nsresult MigrateV25Up();
+ nsresult MigrateV26Up();
+ nsresult MigrateV27Up();
+ nsresult MigrateV28Up();
+ nsresult MigrateV30Up();
+ nsresult MigrateV31Up();
+ nsresult MigrateV32Up();
+ nsresult MigrateV33Up();
+ nsresult MigrateV34Up();
+ nsresult MigrateV35Up();
+
+ nsresult UpdateBookmarkRootTitles();
+
+ friend class ConnectionShutdownBlocker;
+
+ int64_t CreateMobileRoot();
+ nsresult GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
+ nsTArray<int64_t>& aItemIds);
+ nsresult DeleteBookmarkItem(int32_t aItemId);
+
+private:
+ ~Database();
+
+ /**
+ * Singleton getter, invoked by class instantiation.
+ */
+ static already_AddRefed<Database> GetSingleton();
+
+ static Database* gDatabase;
+
+ nsCOMPtr<mozIStorageConnection> mMainConn;
+
+ mutable StatementCache mMainThreadStatements;
+ mutable AsyncStatementCache mMainThreadAsyncStatements;
+ mutable StatementCache mAsyncThreadStatements;
+
+ int32_t mDBPageSize;
+ uint16_t mDatabaseStatus;
+ bool mClosed;
+
+ /**
+ * Phases for shutting down the Database.
+ * See Shutdown.h for further details about the shutdown procedure.
+ */
+ already_AddRefed<nsIAsyncShutdownClient> GetProfileChangeTeardownPhase();
+ already_AddRefed<nsIAsyncShutdownClient> GetProfileBeforeChangePhase();
+
+ /**
+ * Blockers in charge of waiting for the Places clients and then shutting
+ * down the mozStorage connection.
+ * See Shutdown.h for further details about the shutdown procedure.
+ *
+ * Cycles with these are broken in `Shutdown()`.
+ */
+ RefPtr<ClientsShutdownBlocker> mClientsShutdown;
+ RefPtr<ConnectionShutdownBlocker> mConnectionShutdown;
+
+ // Maximum length of a stored url.
+ // For performance reasons we don't store very long urls in history, since
+ // they are slower to search through and cause abnormal database growth,
+ // affecting the awesomebar fetch time.
+ uint32_t mMaxUrlLength;
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_Database_h_
diff --git a/toolkit/components/places/ExtensionSearchHandler.jsm b/toolkit/components/places/ExtensionSearchHandler.jsm
new file mode 100644
index 0000000000..3eb699ca18
--- /dev/null
+++ b/toolkit/components/places/ExtensionSearchHandler.jsm
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [ "ExtensionSearchHandler" ];
+
+// Used to keep track of all of the registered keywords, where each keyword is
+// mapped to a KeywordInfo instance.
+let gKeywordMap = new Map();
+
+// Used to keep track of the active input session.
+let gActiveInputSession = null;
+
+// Used to keep track of who has control over the active suggestion callback
+// so older callbacks can be ignored. The callback ID should increment whenever
+// the input changes or the input session ends.
+let gCurrentCallbackID = 0;
+
+// Handles keeping track of information associated to the registered keyword.
+class KeywordInfo {
+ constructor(extension, description) {
+ this._extension = extension;
+ this._description = description;
+ }
+
+ get description() {
+ return this._description;
+ }
+
+ set description(desc) {
+ this._description = desc;
+ }
+
+ get extension() {
+ return this._extension;
+ }
+}
+
+// Responsible for handling communication between the extension and the urlbar.
+class InputSession {
+ constructor(keyword, extension) {
+ this._keyword = keyword;
+ this._extension = extension;
+ this._suggestionsCallback = null;
+ this._searchFinishedCallback = null;
+ }
+
+ get keyword() {
+ return this._keyword;
+ }
+
+ addSuggestions(suggestions) {
+ this._suggestionsCallback(suggestions);
+ }
+
+ start(eventName) {
+ this._extension.emit(eventName);
+ }
+
+ update(eventName, text, suggestionsCallback, searchFinishedCallback) {
+ if (this._searchFinishedCallback) {
+ this._searchFinishedCallback();
+ }
+ this._searchFinishedCallback = searchFinishedCallback;
+ this._suggestionsCallback = suggestionsCallback;
+ this._extension.emit(eventName, text, ++gCurrentCallbackID);
+ }
+
+ cancel(eventName) {
+ this._searchFinishedCallback();
+ this._extension.emit(eventName);
+ }
+
+ end(eventName, text, disposition) {
+ this._searchFinishedCallback();
+ this._extension.emit(eventName, text, disposition);
+ }
+}
+
+var ExtensionSearchHandler = Object.freeze({
+ MSG_INPUT_STARTED: "webext-omnibox-input-started",
+ MSG_INPUT_CHANGED: "webext-omnibox-input-changed",
+ MSG_INPUT_ENTERED: "webext-omnibox-input-entered",
+ MSG_INPUT_CANCELLED: "webext-omnibox-input-cancelled",
+
+ /**
+ * Registers a keyword.
+ *
+ * @param {string} keyword The keyword to register.
+ * @param {Extension} extension The extension registering the keyword.
+ */
+ registerKeyword(keyword, extension) {
+ if (gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is already registered: "${keyword}"`);
+ }
+ gKeywordMap.set(keyword, new KeywordInfo(extension, extension.name));
+ },
+
+ /**
+ * Unregisters a keyword.
+ *
+ * @param {string} keyword The keyword to unregister.
+ */
+ unregisterKeyword(keyword) {
+ if (!gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is not registered: "${keyword}"`);
+ }
+ gActiveInputSession = null;
+ gKeywordMap.delete(keyword);
+ },
+
+ /**
+ * Checks if a keyword is registered.
+ *
+ * @param {string} keyword The word to check.
+ * @return {boolean} true if the word is a registered keyword.
+ */
+ isKeywordRegistered(keyword) {
+ return gKeywordMap.has(keyword);
+ },
+
+ /**
+ * @return {boolean} true if there is an active input session.
+ */
+ hasActiveInputSession() {
+ return gActiveInputSession != null;
+ },
+
+ /**
+ * @param {string} keyword The keyword to look up.
+ * @return {string} the description to use for the heuristic result.
+ */
+ getDescription(keyword) {
+ if (!gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is not registered: "${keyword}"`);
+ }
+ return gKeywordMap.get(keyword).description;
+ },
+
+ /**
+ * Sets the default suggestion for the registered keyword. The suggestion's
+ * description will be used for the comment in the heuristic result.
+ *
+ * @param {string} keyword The keyword.
+ * @param {string} description The description to use for the heuristic result.
+ */
+ setDefaultSuggestion(keyword, {description}) {
+ if (!gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is not registered: "${keyword}"`);
+ }
+ gKeywordMap.get(keyword).description = description;
+ },
+
+ /**
+ * Adds suggestions for the registered keyword. This function will throw if
+ * the keyword provided is not registered or active, or if the callback ID
+ * provided is no longer equal to the active callback ID.
+ *
+ * @param {string} keyword The keyword.
+ * @param {integer} id The ID of the suggestion callback.
+ * @param {Array<Object>} suggestions An array of suggestions to provide to the urlbar.
+ */
+ addSuggestions(keyword, id, suggestions) {
+ if (!gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is not registered: "${keyword}"`);
+ }
+
+ if (!gActiveInputSession || gActiveInputSession.keyword != keyword) {
+ throw new Error(`The keyword provided is not apart of an active input session: "${keyword}"`);
+ }
+
+ if (id != gCurrentCallbackID) {
+ throw new Error(`The callback is no longer active for the keyword provided: "${keyword}"`);
+ }
+
+ gActiveInputSession.addSuggestions(suggestions);
+ },
+
+ /**
+ * Called when the input in the urlbar begins with `<keyword><space>`.
+ *
+ * If the keyword is inactive, MSG_INPUT_STARTED is emitted and the
+ * keyword is marked as active. If the keyword is followed by any text,
+ * MSG_INPUT_CHANGED is fired with the current callback ID that can be
+ * used to provide suggestions to the urlbar while the callback ID is active.
+ * The callback is invalidated when either the input changes or the urlbar blurs.
+ *
+ * @param {string} keyword The keyword to handle.
+ * @param {string} text The search text in the urlbar.
+ * @param {Function} callback The callback used to provide search suggestions.
+ * @return {Promise} promise that resolves when the current search is complete.
+ */
+ handleSearch(keyword, text, callback) {
+ if (!gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is not registered: "${keyword}"`);
+ }
+
+ if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
+ throw new Error("A different input session is already ongoing");
+ }
+
+ if (!text || !text.startsWith(`${keyword} `)) {
+ throw new Error(`The text provided must start with: "${keyword} "`);
+ }
+
+ if (!callback) {
+ throw new Error("A callback must be provided");
+ }
+
+ // The search text in the urlbar currently starts with <keyword><space>, and
+ // we only want the text that follows.
+ text = text.substring(keyword.length + 1);
+
+ // We fire MSG_INPUT_STARTED once we have <keyword><space>, and only fire
+ // MSG_INPUT_CHANGED when we have text to process. This is different from Chrome's
+ // behavior, which always fires MSG_INPUT_STARTED right before MSG_INPUT_CHANGED
+ // first fires, but this is a bug in Chrome according to https://crbug.com/258911.
+ if (!gActiveInputSession) {
+ gActiveInputSession = new InputSession(keyword, gKeywordMap.get(keyword).extension);
+ gActiveInputSession.start(this.MSG_INPUT_STARTED);
+
+ // Resolve early if there is no text to process. There can be text to process when
+ // the input starts if the user copy/pastes the text into the urlbar.
+ if (!text.length) {
+ return Promise.resolve();
+ }
+ }
+
+ return new Promise(resolve => {
+ gActiveInputSession.update(this.MSG_INPUT_CHANGED, text, callback, resolve);
+ });
+ },
+
+ /**
+ * Called when the user clicks on a suggestion that was added by
+ * an extension. MSG_INPUT_ENTERED is emitted to the extension with
+ * the keyword, the current search string, and info about how the
+ * the search should be handled. This ends the active input session.
+ *
+ * @param {string} keyword The keyword associated to the suggestion.
+ * @param {string} text The search text in the urlbar.
+ * @param {string} where How the page should be opened. Accepted values are:
+ * "current": open the page in the same tab.
+ * "tab": open the page in a new foreground tab.
+ * "tabshifted": open the page in a new background tab.
+ */
+ handleInputEntered(keyword, text, where) {
+ if (!gKeywordMap.has(keyword)) {
+ throw new Error(`The keyword provided is not registered: "${keyword}"`);
+ }
+
+ if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
+ throw new Error("A different input session is already ongoing");
+ }
+
+ if (!text || !text.startsWith(`${keyword} `)) {
+ throw new Error(`The text provided must start with: "${keyword} "`);
+ }
+
+ let dispositionMap = {
+ current: "currentTab",
+ tab: "newForegroundTab",
+ tabshifted: "newBackgroundTab",
+ }
+ let disposition = dispositionMap[where];
+
+ if (!disposition) {
+ throw new Error(`Invalid "where" argument: ${where}`);
+ }
+
+ // The search text in the urlbar currently starts with <keyword><space>, and
+ // we only want to send the text that follows.
+ text = text.substring(keyword.length + 1);
+
+ gActiveInputSession.end(this.MSG_INPUT_ENTERED, text, disposition)
+ gActiveInputSession = null;
+ },
+
+ /**
+ * If the user has ended the keyword input session without accepting the input,
+ * MSG_INPUT_CANCELLED is emitted and the input session is ended.
+ */
+ handleInputCancelled() {
+ if (!gActiveInputSession) {
+ throw new Error("There is no active input session");
+ }
+ gActiveInputSession.cancel(this.MSG_INPUT_CANCELLED);
+ gActiveInputSession = null;
+ }
+});
diff --git a/toolkit/components/places/FaviconHelpers.cpp b/toolkit/components/places/FaviconHelpers.cpp
new file mode 100644
index 0000000000..69c2023380
--- /dev/null
+++ b/toolkit/components/places/FaviconHelpers.cpp
@@ -0,0 +1,934 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FaviconHelpers.h"
+
+#include "nsICacheEntry.h"
+#include "nsICachingChannel.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIPrincipal.h"
+
+#include "nsNavHistory.h"
+#include "nsFaviconService.h"
+#include "mozilla/storage.h"
+#include "mozilla/Telemetry.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsStreamUtils.h"
+#include "nsIPrivateBrowsingChannel.h"
+#include "nsISupportsPriority.h"
+#include "nsContentUtils.h"
+#include <algorithm>
+
+using namespace mozilla::places;
+using namespace mozilla::storage;
+
+namespace mozilla {
+namespace places {
+
+namespace {
+
+/**
+ * Fetches information on a page from the Places database.
+ *
+ * @param aDBConn
+ * Database connection to history tables.
+ * @param _page
+ * Page that should be fetched.
+ */
+nsresult
+FetchPageInfo(const RefPtr<Database>& aDB,
+ PageData& _page)
+{
+ MOZ_ASSERT(_page.spec.Length(), "Must have a non-empty spec!");
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // This query finds the bookmarked uri we want to set the icon for,
+ // walking up to two redirect levels.
+ nsCString query = nsPrintfCString(
+ "SELECT h.id, h.favicon_id, h.guid, ( "
+ "SELECT h.url FROM moz_bookmarks b WHERE b.fk = h.id "
+ "UNION ALL " // Union not directly bookmarked pages.
+ "SELECT url FROM moz_places WHERE id = ( "
+ "SELECT COALESCE(grandparent.place_id, parent.place_id) as r_place_id "
+ "FROM moz_historyvisits dest "
+ "LEFT JOIN moz_historyvisits parent ON parent.id = dest.from_visit "
+ "AND dest.visit_type IN (%d, %d) "
+ "LEFT JOIN moz_historyvisits grandparent ON parent.from_visit = grandparent.id "
+ "AND parent.visit_type IN (%d, %d) "
+ "WHERE dest.place_id = h.id "
+ "AND EXISTS(SELECT 1 FROM moz_bookmarks b WHERE b.fk = r_place_id) "
+ "LIMIT 1 "
+ ") "
+ ") FROM moz_places h WHERE h.url_hash = hash(:page_url) AND h.url = :page_url",
+ nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY,
+ nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY
+ );
+
+ nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
+ _page.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasResult) {
+ // The page does not exist.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ rv = stmt->GetInt64(0, &_page.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isNull;
+ rv = stmt->GetIsNull(1, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // favicon_id can be nullptr.
+ if (!isNull) {
+ rv = stmt->GetInt64(1, &_page.iconId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->GetUTF8String(2, _page.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetIsNull(3, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // The page could not be bookmarked.
+ if (!isNull) {
+ rv = stmt->GetUTF8String(3, _page.bookmarkedSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (!_page.canAddToHistory) {
+ // Either history is disabled or the scheme is not supported. In such a
+ // case we want to update the icon only if the page is bookmarked.
+
+ if (_page.bookmarkedSpec.IsEmpty()) {
+ // The page is not bookmarked. Since updating the icon with a disabled
+ // history would be a privacy leak, bail out as if the page did not exist.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ else {
+ // The page, or a redirect to it, is bookmarked. If the bookmarked spec
+ // is different from the requested one, use it.
+ if (!_page.bookmarkedSpec.Equals(_page.spec)) {
+ _page.spec = _page.bookmarkedSpec;
+ rv = FetchPageInfo(aDB, _page);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Stores information on a icon in the database.
+ *
+ * @param aDBConn
+ * Database connection to history tables.
+ * @param aIcon
+ * Icon that should be stored.
+ */
+nsresult
+SetIconInfo(const RefPtr<Database>& aDB,
+ const IconData& aIcon)
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(
+ "INSERT OR REPLACE INTO moz_favicons "
+ "(id, url, data, mime_type, expiration) "
+ "VALUES ((SELECT id FROM moz_favicons WHERE url = :icon_url), "
+ ":icon_url, :data, :mime_type, :expiration) "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("icon_url"), aIcon.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindBlobByName(NS_LITERAL_CSTRING("data"),
+ TO_INTBUFFER(aIcon.data), aIcon.data.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("mime_type"), aIcon.mimeType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("expiration"), aIcon.expiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * Fetches information on a icon from the Places database.
+ *
+ * @param aDBConn
+ * Database connection to history tables.
+ * @param _icon
+ * Icon that should be fetched.
+ */
+nsresult
+FetchIconInfo(const RefPtr<Database>& aDB,
+ IconData& _icon)
+{
+ MOZ_ASSERT(_icon.spec.Length(), "Must have a non-empty spec!");
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ if (_icon.status & ICON_STATUS_CACHED) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(
+ "SELECT id, expiration, data, mime_type "
+ "FROM moz_favicons WHERE url = :icon_url"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ DebugOnly<nsresult> rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("icon_url"),
+ _icon.spec);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ if (!hasResult) {
+ // The icon does not exist yet, bail out.
+ return NS_OK;
+ }
+
+ rv = stmt->GetInt64(0, &_icon.id);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Expiration can be nullptr.
+ bool isNull;
+ rv = stmt->GetIsNull(1, &isNull);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ if (!isNull) {
+ rv = stmt->GetInt64(1, reinterpret_cast<int64_t*>(&_icon.expiration));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ // Data can be nullptr.
+ rv = stmt->GetIsNull(2, &isNull);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ if (!isNull) {
+ uint8_t* data;
+ uint32_t dataLen = 0;
+ rv = stmt->GetBlob(2, &dataLen, &data);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ _icon.data.Adopt(TO_CHARBUFFER(data), dataLen);
+ // Read mime only if we have data.
+ rv = stmt->GetUTF8String(3, _icon.mimeType);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ return NS_OK;
+}
+
+nsresult
+FetchIconURL(const RefPtr<Database>& aDB,
+ const nsACString& aPageSpec,
+ nsACString& aIconSpec)
+{
+ MOZ_ASSERT(!aPageSpec.IsEmpty(), "Page spec must not be empty.");
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ aIconSpec.Truncate();
+
+ nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(
+ "SELECT f.url "
+ "FROM moz_places h "
+ "JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
+ aPageSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ rv = stmt->GetUTF8String(0, aIconSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Tries to compute the expiration time for a icon from the channel.
+ *
+ * @param aChannel
+ * The network channel used to fetch the icon.
+ * @return a valid expiration value for the fetched icon.
+ */
+PRTime
+GetExpirationTimeFromChannel(nsIChannel* aChannel)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Attempt to get an expiration time from the cache. If this fails, we'll
+ // make one up.
+ PRTime expiration = -1;
+ nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aChannel);
+ if (cachingChannel) {
+ nsCOMPtr<nsISupports> cacheToken;
+ nsresult rv = cachingChannel->GetCacheToken(getter_AddRefs(cacheToken));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsICacheEntry> cacheEntry = do_QueryInterface(cacheToken);
+ uint32_t seconds;
+ rv = cacheEntry->GetExpirationTime(&seconds);
+ if (NS_SUCCEEDED(rv)) {
+ // Set the expiration, but make sure we honor our cap.
+ expiration = PR_Now() + std::min((PRTime)seconds * PR_USEC_PER_SEC,
+ MAX_FAVICON_EXPIRATION);
+ }
+ }
+ }
+ // If we did not obtain a time from the cache, use the cap value.
+ return expiration < 0 ? PR_Now() + MAX_FAVICON_EXPIRATION
+ : expiration;
+}
+
+/**
+ * Checks the icon and evaluates if it needs to be optimized. In such a case it
+ * will try to reduce its size through OptimizeFaviconImage method of the
+ * favicons service.
+ *
+ * @param aIcon
+ * The icon to be evaluated.
+ * @param aFaviconSvc
+ * Pointer to the favicons service.
+ */
+nsresult
+OptimizeIconSize(IconData& aIcon,
+ nsFaviconService* aFaviconSvc)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Even if the page provides a large image for the favicon (eg, a highres
+ // image or a multiresolution .ico file), don't try to store more data than
+ // needed.
+ nsAutoCString newData, newMimeType;
+ if (aIcon.data.Length() > MAX_FAVICON_FILESIZE) {
+ nsresult rv = aFaviconSvc->OptimizeFaviconImage(TO_INTBUFFER(aIcon.data),
+ aIcon.data.Length(),
+ aIcon.mimeType,
+ newData,
+ newMimeType);
+ if (NS_SUCCEEDED(rv) && newData.Length() < aIcon.data.Length()) {
+ aIcon.data = newData;
+ aIcon.mimeType = newMimeType;
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncFetchAndSetIconForPage
+
+NS_IMPL_ISUPPORTS_INHERITED(
+ AsyncFetchAndSetIconForPage
+, Runnable
+, nsIStreamListener
+, nsIInterfaceRequestor
+, nsIChannelEventSink
+, mozIPlacesPendingOperation
+)
+
+AsyncFetchAndSetIconForPage::AsyncFetchAndSetIconForPage(
+ IconData& aIcon
+, PageData& aPage
+, bool aFaviconLoadPrivate
+, nsIFaviconDataCallback* aCallback
+, nsIPrincipal* aLoadingPrincipal
+) : mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(aCallback))
+ , mIcon(aIcon)
+ , mPage(aPage)
+ , mFaviconLoadPrivate(aFaviconLoadPrivate)
+ , mLoadingPrincipal(new nsMainThreadPtrHolder<nsIPrincipal>(aLoadingPrincipal))
+ , mCanceled(false)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::Run()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Try to fetch the icon from the database.
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ nsresult rv = FetchIconInfo(DB, mIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isInvalidIcon = mIcon.data.IsEmpty() ||
+ (mIcon.expiration && PR_Now() > mIcon.expiration);
+ bool fetchIconFromNetwork = mIcon.fetchMode == FETCH_ALWAYS ||
+ (mIcon.fetchMode == FETCH_IF_MISSING && isInvalidIcon);
+
+ if (!fetchIconFromNetwork) {
+ // There is already a valid icon or we don't want to fetch a new one,
+ // directly proceed with association.
+ RefPtr<AsyncAssociateIconToPage> event =
+ new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
+ DB->DispatchToAsyncThread(event);
+
+ return NS_OK;
+ }
+
+ // Fetch the icon from the network, the request starts from the main-thread.
+ // When done this will associate the icon to the page and notify.
+ nsCOMPtr<nsIRunnable> event =
+ NewRunnableMethod(this, &AsyncFetchAndSetIconForPage::FetchFromNetwork);
+ return NS_DispatchToMainThread(event);
+}
+
+nsresult
+AsyncFetchAndSetIconForPage::FetchFromNetwork() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mCanceled) {
+ return NS_OK;
+ }
+
+ // Ensure data is cleared, since it's going to be overwritten.
+ if (mIcon.data.Length() > 0) {
+ mIcon.data.Truncate(0);
+ mIcon.mimeType.Truncate(0);
+ }
+
+ nsCOMPtr<nsIURI> iconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel),
+ iconURI,
+ mLoadingPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
+ nsILoadInfo::SEC_ALLOW_CHROME |
+ nsILoadInfo::SEC_DISALLOW_SCRIPT,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIInterfaceRequestor> listenerRequestor =
+ do_QueryInterface(reinterpret_cast<nsISupports*>(this));
+ NS_ENSURE_STATE(listenerRequestor);
+ rv = channel->SetNotificationCallbacks(listenerRequestor);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(channel);
+ if (pbChannel) {
+ rv = pbChannel->SetPrivate(mFaviconLoadPrivate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(channel);
+ if (priorityChannel) {
+ priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST);
+ }
+
+ rv = channel->AsyncOpen2(this);
+ if (NS_SUCCEEDED(rv)) {
+ mRequest = channel;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::Cancel()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mCanceled) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ mCanceled = true;
+ if (mRequest) {
+ mRequest->Cancel(NS_BINDING_ABORTED);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::OnStartRequest(nsIRequest* aRequest,
+ nsISupports* aContext)
+{
+ // mRequest should already be set from ::FetchFromNetwork, but in the case of
+ // a redirect we might get a new request, and we should make sure we keep a
+ // reference to the most current request.
+ mRequest = aRequest;
+ if (mCanceled) {
+ mRequest->Cancel(NS_BINDING_ABORTED);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::OnDataAvailable(nsIRequest* aRequest,
+ nsISupports* aContext,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset,
+ uint32_t aCount)
+{
+ const size_t kMaxFaviconDownloadSize = 1 * 1024 * 1024;
+ if (mIcon.data.Length() + aCount > kMaxFaviconDownloadSize) {
+ mIcon.data.Truncate();
+ return NS_ERROR_FILE_TOO_BIG;
+ }
+
+ nsAutoCString buffer;
+ nsresult rv = NS_ConsumeStream(aInputStream, aCount, buffer);
+ if (rv != NS_BASE_STREAM_WOULD_BLOCK && NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!mIcon.data.Append(buffer, fallible)) {
+ mIcon.data.Truncate();
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::GetInterface(const nsIID& uuid,
+ void** aResult)
+{
+ return QueryInterface(uuid, aResult);
+}
+
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::AsyncOnChannelRedirect(
+ nsIChannel* oldChannel
+, nsIChannel* newChannel
+, uint32_t flags
+, nsIAsyncVerifyRedirectCallback *cb
+)
+{
+ // If we've been canceled, stop the redirect with NS_BINDING_ABORTED, and
+ // handle the cancel on the original channel.
+ (void)cb->OnRedirectVerifyCallback(mCanceled ? NS_BINDING_ABORTED : NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncFetchAndSetIconForPage::OnStopRequest(nsIRequest* aRequest,
+ nsISupports* aContext,
+ nsresult aStatusCode)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Don't need to track this anymore.
+ mRequest = nullptr;
+ if (mCanceled) {
+ return NS_OK;
+ }
+
+ nsFaviconService* favicons = nsFaviconService::GetFaviconService();
+ NS_ENSURE_STATE(favicons);
+
+ nsresult rv;
+
+ // If fetching the icon failed, add it to the failed cache.
+ if (NS_FAILED(aStatusCode) || mIcon.data.Length() == 0) {
+ nsCOMPtr<nsIURI> iconURI;
+ rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = favicons->AddFailedFavicon(iconURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ // aRequest should always QI to nsIChannel.
+ MOZ_ASSERT(channel);
+
+ nsAutoCString contentType;
+ channel->GetContentType(contentType);
+ // Bug 366324 - can't sniff SVG yet, so rely on server-specified type
+ if (contentType.EqualsLiteral("image/svg+xml")) {
+ mIcon.mimeType.AssignLiteral("image/svg+xml");
+ } else {
+ NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, aRequest,
+ TO_INTBUFFER(mIcon.data), mIcon.data.Length(),
+ mIcon.mimeType);
+ }
+
+ // If the icon does not have a valid MIME type, add it to the failed cache.
+ if (mIcon.mimeType.IsEmpty()) {
+ nsCOMPtr<nsIURI> iconURI;
+ rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = favicons->AddFailedFavicon(iconURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ mIcon.expiration = GetExpirationTimeFromChannel(channel);
+
+ // Telemetry probes to measure the favicon file sizes for each different file type.
+ // This allow us to measure common file sizes while also observing each type popularity.
+ if (mIcon.mimeType.EqualsLiteral("image/png")) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_PNG_SIZES, mIcon.data.Length());
+ }
+ else if (mIcon.mimeType.EqualsLiteral("image/x-icon") ||
+ mIcon.mimeType.EqualsLiteral("image/vnd.microsoft.icon")) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_ICO_SIZES, mIcon.data.Length());
+ }
+ else if (mIcon.mimeType.EqualsLiteral("image/jpeg") ||
+ mIcon.mimeType.EqualsLiteral("image/pjpeg")) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_JPEG_SIZES, mIcon.data.Length());
+ }
+ else if (mIcon.mimeType.EqualsLiteral("image/gif")) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_GIF_SIZES, mIcon.data.Length());
+ }
+ else if (mIcon.mimeType.EqualsLiteral("image/bmp") ||
+ mIcon.mimeType.EqualsLiteral("image/x-windows-bmp")) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_BMP_SIZES, mIcon.data.Length());
+ }
+ else if (mIcon.mimeType.EqualsLiteral("image/svg+xml")) {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_SVG_SIZES, mIcon.data.Length());
+ }
+ else {
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_OTHER_SIZES, mIcon.data.Length());
+ }
+
+ rv = OptimizeIconSize(mIcon, favicons);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If over the maximum size allowed, don't save data to the database to
+ // avoid bloating it.
+ if (mIcon.data.Length() > nsIFaviconService::MAX_FAVICON_BUFFER_SIZE) {
+ return NS_OK;
+ }
+
+ mIcon.status = ICON_STATUS_CHANGED;
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ RefPtr<AsyncAssociateIconToPage> event =
+ new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
+ DB->DispatchToAsyncThread(event);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncAssociateIconToPage
+
+AsyncAssociateIconToPage::AsyncAssociateIconToPage(
+ const IconData& aIcon
+, const PageData& aPage
+, const nsMainThreadPtrHandle<nsIFaviconDataCallback>& aCallback
+) : mCallback(aCallback)
+ , mIcon(aIcon)
+ , mPage(aPage)
+{
+}
+
+NS_IMETHODIMP
+AsyncAssociateIconToPage::Run()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ nsresult rv = FetchPageInfo(DB, mPage);
+ if (rv == NS_ERROR_NOT_AVAILABLE){
+ // We have never seen this page. If we can add the page to history,
+ // we will try to do it later, otherwise just bail out.
+ if (!mPage.canAddToHistory) {
+ return NS_OK;
+ }
+ }
+ else {
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mozStorageTransaction transaction(DB->MainConn(), false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ // If there is no entry for this icon, or the entry is obsolete, replace it.
+ if (mIcon.id == 0 || (mIcon.status & ICON_STATUS_CHANGED)) {
+ rv = SetIconInfo(DB, mIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the new icon id. Do this regardless mIcon.id, since other code
+ // could have added a entry before us. Indeed we interrupted the thread
+ // after the previous call to FetchIconInfo.
+ mIcon.status = (mIcon.status & ~(ICON_STATUS_CACHED)) | ICON_STATUS_SAVED;
+ rv = FetchIconInfo(DB, mIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If the page does not have an id, don't try to insert a new one, cause we
+ // don't know where the page comes from. Not doing so we may end adding
+ // a page that otherwise we'd explicitly ignore, like a POST or an error page.
+ if (mPage.id == 0) {
+ return NS_OK;
+ }
+
+ // Otherwise just associate the icon to the page, if needed.
+ if (mPage.iconId != mIcon.id) {
+ nsCOMPtr<mozIStorageStatement> stmt;
+ if (mPage.id) {
+ stmt = DB->GetStatement(
+ "UPDATE moz_places SET favicon_id = :icon_id WHERE id = :page_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPage.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ stmt = DB->GetStatement(
+ "UPDATE moz_places SET favicon_id = :icon_id "
+ "WHERE url_hash = hash(:page_url) AND url = :page_url"
+ );
+ NS_ENSURE_STATE(stmt);
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mPage.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("icon_id"), mIcon.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mIcon.status |= ICON_STATUS_ASSOCIATED;
+ }
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, dispatch an event to the main thread to notify observers.
+ nsCOMPtr<nsIRunnable> event = new NotifyIconObservers(mIcon, mPage, mCallback);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncGetFaviconURLForPage
+
+AsyncGetFaviconURLForPage::AsyncGetFaviconURLForPage(
+ const nsACString& aPageSpec
+, nsIFaviconDataCallback* aCallback
+) : mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(aCallback))
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mPageSpec.Assign(aPageSpec);
+}
+
+NS_IMETHODIMP
+AsyncGetFaviconURLForPage::Run()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ nsAutoCString iconSpec;
+ nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now notify our callback of the icon spec we retrieved, even if empty.
+ IconData iconData;
+ iconData.spec.Assign(iconSpec);
+
+ PageData pageData;
+ pageData.spec.Assign(mPageSpec);
+
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyIconObservers(iconData, pageData, mCallback);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncGetFaviconDataForPage
+
+AsyncGetFaviconDataForPage::AsyncGetFaviconDataForPage(
+ const nsACString& aPageSpec
+, nsIFaviconDataCallback* aCallback
+) : mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(aCallback))
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPageSpec.Assign(aPageSpec);
+}
+
+NS_IMETHODIMP
+AsyncGetFaviconDataForPage::Run()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ nsAutoCString iconSpec;
+ nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ IconData iconData;
+ iconData.spec.Assign(iconSpec);
+
+ PageData pageData;
+ pageData.spec.Assign(mPageSpec);
+
+ if (!iconSpec.IsEmpty()) {
+ rv = FetchIconInfo(DB, iconData);
+ if (NS_FAILED(rv)) {
+ iconData.spec.Truncate();
+ }
+ }
+
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyIconObservers(iconData, pageData, mCallback);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncReplaceFaviconData
+
+AsyncReplaceFaviconData::AsyncReplaceFaviconData(const IconData &aIcon)
+ : mIcon(aIcon)
+{
+}
+
+NS_IMETHODIMP
+AsyncReplaceFaviconData::Run()
+{
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ IconData dbIcon;
+ dbIcon.spec.Assign(mIcon.spec);
+ nsresult rv = FetchIconInfo(DB, dbIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!dbIcon.id) {
+ return NS_OK;
+ }
+
+ rv = SetIconInfo(DB, mIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We can invalidate the cache version since we now persist the icon.
+ nsCOMPtr<nsIRunnable> event =
+ NewRunnableMethod(this, &AsyncReplaceFaviconData::RemoveIconDataCacheEntry);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+AsyncReplaceFaviconData::RemoveIconDataCacheEntry()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIURI> iconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsFaviconService* favicons = nsFaviconService::GetFaviconService();
+ NS_ENSURE_STATE(favicons);
+ favicons->mUnassociatedIcons.RemoveEntry(iconURI);
+
+ return NS_OK;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// NotifyIconObservers
+
+NotifyIconObservers::NotifyIconObservers(
+ const IconData& aIcon
+, const PageData& aPage
+, const nsMainThreadPtrHandle<nsIFaviconDataCallback>& aCallback
+)
+: mCallback(aCallback)
+, mIcon(aIcon)
+, mPage(aPage)
+{
+}
+
+NS_IMETHODIMP
+NotifyIconObservers::Run()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIURI> iconURI;
+ if (!mIcon.spec.IsEmpty()) {
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(iconURI), mIcon.spec));
+ if (iconURI)
+ {
+ // Notify observers only if something changed.
+ if (mIcon.status & ICON_STATUS_SAVED ||
+ mIcon.status & ICON_STATUS_ASSOCIATED) {
+ SendGlobalNotifications(iconURI);
+ }
+ }
+ }
+
+ if (mCallback) {
+ (void)mCallback->OnComplete(iconURI, mIcon.data.Length(),
+ TO_INTBUFFER(mIcon.data), mIcon.mimeType);
+ }
+
+ return NS_OK;
+}
+
+void
+NotifyIconObservers::SendGlobalNotifications(nsIURI* aIconURI)
+{
+ nsCOMPtr<nsIURI> pageURI;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(pageURI), mPage.spec));
+ if (pageURI) {
+ nsFaviconService* favicons = nsFaviconService::GetFaviconService();
+ MOZ_ASSERT(favicons);
+ if (favicons) {
+ (void)favicons->SendFaviconNotifications(pageURI, aIconURI, mPage.guid);
+ }
+ }
+
+ // If the page is bookmarked and the bookmarked url is different from the
+ // updated one, start a new task to update its icon as well.
+ if (!mPage.bookmarkedSpec.IsEmpty() &&
+ !mPage.bookmarkedSpec.Equals(mPage.spec)) {
+ // Create a new page struct to avoid polluting it with old data.
+ PageData bookmarkedPage;
+ bookmarkedPage.spec = mPage.bookmarkedSpec;
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ if (!DB)
+ return;
+ // This will be silent, so be sure to not pass in the current callback.
+ nsMainThreadPtrHandle<nsIFaviconDataCallback> nullCallback;
+ RefPtr<AsyncAssociateIconToPage> event =
+ new AsyncAssociateIconToPage(mIcon, bookmarkedPage, nullCallback);
+ DB->DispatchToAsyncThread(event);
+ }
+}
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/FaviconHelpers.h b/toolkit/components/places/FaviconHelpers.h
new file mode 100644
index 0000000000..1c6d5b2bfa
--- /dev/null
+++ b/toolkit/components/places/FaviconHelpers.h
@@ -0,0 +1,273 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#pragma once
+
+#include "nsIFaviconService.h"
+#include "nsIChannelEventSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIStreamListener.h"
+#include "mozIPlacesPendingOperation.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+
+class nsIPrincipal;
+
+#include "Database.h"
+#include "mozilla/storage.h"
+
+#define ICON_STATUS_UNKNOWN 0
+#define ICON_STATUS_CHANGED 1 << 0
+#define ICON_STATUS_SAVED 1 << 1
+#define ICON_STATUS_ASSOCIATED 1 << 2
+#define ICON_STATUS_CACHED 1 << 3
+
+#define TO_CHARBUFFER(_buffer) \
+ reinterpret_cast<char*>(const_cast<uint8_t*>(_buffer))
+#define TO_INTBUFFER(_string) \
+ reinterpret_cast<uint8_t*>(const_cast<char*>(_string.get()))
+
+/**
+ * The maximum time we will keep a favicon around. We always ask the cache, if
+ * we can, but default to this value if we do not get a time back, or the time
+ * is more in the future than this.
+ * Currently set to one week from now.
+ */
+#define MAX_FAVICON_EXPIRATION ((PRTime)7 * 24 * 60 * 60 * PR_USEC_PER_SEC)
+
+namespace mozilla {
+namespace places {
+
+/**
+ * Indicates when a icon should be fetched from network.
+ */
+enum AsyncFaviconFetchMode {
+ FETCH_NEVER = 0
+, FETCH_IF_MISSING
+, FETCH_ALWAYS
+};
+
+/**
+ * Data cache for a icon entry.
+ */
+struct IconData
+{
+ IconData()
+ : id(0)
+ , expiration(0)
+ , fetchMode(FETCH_NEVER)
+ , status(ICON_STATUS_UNKNOWN)
+ {
+ }
+
+ int64_t id;
+ nsCString spec;
+ nsCString data;
+ nsCString mimeType;
+ PRTime expiration;
+ enum AsyncFaviconFetchMode fetchMode;
+ uint16_t status; // This is a bitset, see ICON_STATUS_* defines above.
+};
+
+/**
+ * Data cache for a page entry.
+ */
+struct PageData
+{
+ PageData()
+ : id(0)
+ , canAddToHistory(true)
+ , iconId(0)
+ {
+ guid.SetIsVoid(true);
+ }
+
+ int64_t id;
+ nsCString spec;
+ nsCString bookmarkedSpec;
+ nsString revHost;
+ bool canAddToHistory; // False for disabled history and unsupported schemas.
+ int64_t iconId;
+ nsCString guid;
+};
+
+/**
+ * Async fetches icon from database or network, associates it with the required
+ * page and finally notifies the change.
+ */
+class AsyncFetchAndSetIconForPage final : public Runnable
+ , public nsIStreamListener
+ , public nsIInterfaceRequestor
+ , public nsIChannelEventSink
+ , public mozIPlacesPendingOperation
+ {
+ public:
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_MOZIPLACESPENDINGOPERATION
+ NS_DECL_ISUPPORTS_INHERITED
+
+ /**
+ * Constructor.
+ *
+ * @param aIcon
+ * Icon to be fetched and associated.
+ * @param aPage
+ * Page to which associate the icon.
+ * @param aFaviconLoadPrivate
+ * Whether this favicon load is in private browsing.
+ * @param aCallback
+ * Function to be called when the fetch-and-associate process finishes.
+ * @param aLoadingPrincipal
+ * LoadingPrincipal of the icon to be fetched.
+ */
+ AsyncFetchAndSetIconForPage(IconData& aIcon,
+ PageData& aPage,
+ bool aFaviconLoadPrivate,
+ nsIFaviconDataCallback* aCallback,
+ nsIPrincipal* aLoadingPrincipal);
+
+private:
+ nsresult FetchFromNetwork();
+ virtual ~AsyncFetchAndSetIconForPage() {}
+
+ nsMainThreadPtrHandle<nsIFaviconDataCallback> mCallback;
+ IconData mIcon;
+ PageData mPage;
+ const bool mFaviconLoadPrivate;
+ nsMainThreadPtrHandle<nsIPrincipal> mLoadingPrincipal;
+ bool mCanceled;
+ nsCOMPtr<nsIRequest> mRequest;
+};
+
+/**
+ * Associates the icon to the required page, finally dispatches an event to the
+ * main thread to notify the change to observers.
+ */
+class AsyncAssociateIconToPage final : public Runnable
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ /**
+ * Constructor.
+ *
+ * @param aIcon
+ * Icon to be associated.
+ * @param aPage
+ * Page to which associate the icon.
+ * @param aCallback
+ * Function to be called when the associate process finishes.
+ */
+ AsyncAssociateIconToPage(const IconData& aIcon,
+ const PageData& aPage,
+ const nsMainThreadPtrHandle<nsIFaviconDataCallback>& aCallback);
+
+private:
+ nsMainThreadPtrHandle<nsIFaviconDataCallback> mCallback;
+ IconData mIcon;
+ PageData mPage;
+};
+
+/**
+ * Asynchronously tries to get the URL of a page's favicon, then notifies the
+ * given observer.
+ */
+class AsyncGetFaviconURLForPage final : public Runnable
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ /**
+ * Constructor.
+ *
+ * @param aPageSpec
+ * URL of the page whose favicon's URL we're fetching
+ * @param aCallback
+ * function to be called once finished
+ */
+ AsyncGetFaviconURLForPage(const nsACString& aPageSpec,
+ nsIFaviconDataCallback* aCallback);
+
+private:
+ nsMainThreadPtrHandle<nsIFaviconDataCallback> mCallback;
+ nsCString mPageSpec;
+};
+
+
+/**
+ * Asynchronously tries to get the URL and data of a page's favicon, then
+ * notifies the given observer.
+ */
+class AsyncGetFaviconDataForPage final : public Runnable
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ /**
+ * Constructor.
+ *
+ * @param aPageSpec
+ * URL of the page whose favicon URL and data we're fetching
+ * @param aCallback
+ * function to be called once finished
+ */
+ AsyncGetFaviconDataForPage(const nsACString& aPageSpec,
+ nsIFaviconDataCallback* aCallback);
+
+private:
+ nsMainThreadPtrHandle<nsIFaviconDataCallback> mCallback;
+ nsCString mPageSpec;
+};
+
+class AsyncReplaceFaviconData final : public Runnable
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ explicit AsyncReplaceFaviconData(const IconData& aIcon);
+
+private:
+ nsresult RemoveIconDataCacheEntry();
+
+ IconData mIcon;
+};
+
+/**
+ * Notifies the icon change to favicon observers.
+ */
+class NotifyIconObservers final : public Runnable
+{
+public:
+ NS_DECL_NSIRUNNABLE
+
+ /**
+ * Constructor.
+ *
+ * @param aIcon
+ * Icon information. Can be empty if no icon is associated to the page.
+ * @param aPage
+ * Page to which the icon information applies.
+ * @param aCallback
+ * Function to be notified in all cases.
+ */
+ NotifyIconObservers(const IconData& aIcon,
+ const PageData& aPage,
+ const nsMainThreadPtrHandle<nsIFaviconDataCallback>& aCallback);
+
+private:
+ nsMainThreadPtrHandle<nsIFaviconDataCallback> mCallback;
+ IconData mIcon;
+ PageData mPage;
+
+ void SendGlobalNotifications(nsIURI* aIconURI);
+};
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/Helpers.cpp b/toolkit/components/places/Helpers.cpp
new file mode 100644
index 0000000000..66c4e79a9b
--- /dev/null
+++ b/toolkit/components/places/Helpers.cpp
@@ -0,0 +1,395 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "Helpers.h"
+#include "mozIStorageError.h"
+#include "prio.h"
+#include "nsString.h"
+#include "nsNavHistory.h"
+#include "mozilla/Base64.h"
+#include "mozilla/Services.h"
+
+// The length of guids that are used by history and bookmarks.
+#define GUID_LENGTH 12
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncStatementCallback
+
+NS_IMPL_ISUPPORTS(
+ AsyncStatementCallback
+, mozIStorageStatementCallback
+)
+
+NS_IMETHODIMP
+WeakAsyncStatementCallback::HandleResult(mozIStorageResultSet *aResultSet)
+{
+ MOZ_ASSERT(false, "Was not expecting a resultset, but got it.");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WeakAsyncStatementCallback::HandleCompletion(uint16_t aReason)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WeakAsyncStatementCallback::HandleError(mozIStorageError *aError)
+{
+#ifdef DEBUG
+ int32_t result;
+ nsresult rv = aError->GetResult(&result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString message;
+ rv = aError->GetMessage(message);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString warnMsg;
+ warnMsg.AppendLiteral("An error occurred while executing an async statement: ");
+ warnMsg.AppendInt(result);
+ warnMsg.Append(' ');
+ warnMsg.Append(message);
+ NS_WARNING(warnMsg.get());
+#endif
+
+ return NS_OK;
+}
+
+#define URI_TO_URLCSTRING(uri, spec) \
+ nsAutoCString spec; \
+ if (NS_FAILED(aURI->GetSpec(spec))) { \
+ return NS_ERROR_UNEXPECTED; \
+ }
+
+// Bind URI to statement by index.
+nsresult // static
+URIBinder::Bind(mozIStorageStatement* aStatement,
+ int32_t aIndex,
+ nsIURI* aURI)
+{
+ NS_ASSERTION(aStatement, "Must have non-null statement");
+ NS_ASSERTION(aURI, "Must have non-null uri");
+
+ URI_TO_URLCSTRING(aURI, spec);
+ return URIBinder::Bind(aStatement, aIndex, spec);
+}
+
+// Statement URLCString to statement by index.
+nsresult // static
+URIBinder::Bind(mozIStorageStatement* aStatement,
+ int32_t index,
+ const nsACString& aURLString)
+{
+ NS_ASSERTION(aStatement, "Must have non-null statement");
+ return aStatement->BindUTF8StringByIndex(
+ index, StringHead(aURLString, URI_LENGTH_MAX)
+ );
+}
+
+// Bind URI to statement by name.
+nsresult // static
+URIBinder::Bind(mozIStorageStatement* aStatement,
+ const nsACString& aName,
+ nsIURI* aURI)
+{
+ NS_ASSERTION(aStatement, "Must have non-null statement");
+ NS_ASSERTION(aURI, "Must have non-null uri");
+
+ URI_TO_URLCSTRING(aURI, spec);
+ return URIBinder::Bind(aStatement, aName, spec);
+}
+
+// Bind URLCString to statement by name.
+nsresult // static
+URIBinder::Bind(mozIStorageStatement* aStatement,
+ const nsACString& aName,
+ const nsACString& aURLString)
+{
+ NS_ASSERTION(aStatement, "Must have non-null statement");
+ return aStatement->BindUTF8StringByName(
+ aName, StringHead(aURLString, URI_LENGTH_MAX)
+ );
+}
+
+// Bind URI to params by index.
+nsresult // static
+URIBinder::Bind(mozIStorageBindingParams* aParams,
+ int32_t aIndex,
+ nsIURI* aURI)
+{
+ NS_ASSERTION(aParams, "Must have non-null statement");
+ NS_ASSERTION(aURI, "Must have non-null uri");
+
+ URI_TO_URLCSTRING(aURI, spec);
+ return URIBinder::Bind(aParams, aIndex, spec);
+}
+
+// Bind URLCString to params by index.
+nsresult // static
+URIBinder::Bind(mozIStorageBindingParams* aParams,
+ int32_t index,
+ const nsACString& aURLString)
+{
+ NS_ASSERTION(aParams, "Must have non-null statement");
+ return aParams->BindUTF8StringByIndex(
+ index, StringHead(aURLString, URI_LENGTH_MAX)
+ );
+}
+
+// Bind URI to params by name.
+nsresult // static
+URIBinder::Bind(mozIStorageBindingParams* aParams,
+ const nsACString& aName,
+ nsIURI* aURI)
+{
+ NS_ASSERTION(aParams, "Must have non-null params array");
+ NS_ASSERTION(aURI, "Must have non-null uri");
+
+ URI_TO_URLCSTRING(aURI, spec);
+ return URIBinder::Bind(aParams, aName, spec);
+}
+
+// Bind URLCString to params by name.
+nsresult // static
+URIBinder::Bind(mozIStorageBindingParams* aParams,
+ const nsACString& aName,
+ const nsACString& aURLString)
+{
+ NS_ASSERTION(aParams, "Must have non-null params array");
+
+ nsresult rv = aParams->BindUTF8StringByName(
+ aName, StringHead(aURLString, URI_LENGTH_MAX)
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+#undef URI_TO_URLCSTRING
+
+nsresult
+GetReversedHostname(nsIURI* aURI, nsString& aRevHost)
+{
+ nsAutoCString forward8;
+ nsresult rv = aURI->GetHost(forward8);
+ // Not all URIs have a host.
+ if (NS_FAILED(rv))
+ return rv;
+
+ // can't do reversing in UTF8, better use 16-bit chars
+ GetReversedHostname(NS_ConvertUTF8toUTF16(forward8), aRevHost);
+ return NS_OK;
+}
+
+void
+GetReversedHostname(const nsString& aForward, nsString& aRevHost)
+{
+ ReverseString(aForward, aRevHost);
+ aRevHost.Append(char16_t('.'));
+}
+
+void
+ReverseString(const nsString& aInput, nsString& aReversed)
+{
+ aReversed.Truncate(0);
+ for (int32_t i = aInput.Length() - 1; i >= 0; i--) {
+ aReversed.Append(aInput[i]);
+ }
+}
+
+#ifdef XP_WIN
+} // namespace places
+} // namespace mozilla
+
+// Included here because windows.h conflicts with the use of mozIStorageError
+// above, but make sure that these are not included inside mozilla::places.
+#include <windows.h>
+#include <wincrypt.h>
+
+namespace mozilla {
+namespace places {
+#endif
+
+static
+nsresult
+GenerateRandomBytes(uint32_t aSize,
+ uint8_t* _buffer)
+{
+ // On Windows, we'll use its built-in cryptographic API.
+#if defined(XP_WIN)
+ HCRYPTPROV cryptoProvider;
+ BOOL rc = CryptAcquireContext(&cryptoProvider, 0, 0, PROV_RSA_FULL,
+ CRYPT_VERIFYCONTEXT | CRYPT_SILENT);
+ if (rc) {
+ rc = CryptGenRandom(cryptoProvider, aSize, _buffer);
+ (void)CryptReleaseContext(cryptoProvider, 0);
+ }
+ return rc ? NS_OK : NS_ERROR_FAILURE;
+
+ // On Unix, we'll just read in from /dev/urandom.
+#elif defined(XP_UNIX)
+ NS_ENSURE_ARG_MAX(aSize, INT32_MAX);
+ PRFileDesc* urandom = PR_Open("/dev/urandom", PR_RDONLY, 0);
+ nsresult rv = NS_ERROR_FAILURE;
+ if (urandom) {
+ int32_t bytesRead = PR_Read(urandom, _buffer, aSize);
+ if (bytesRead == static_cast<int32_t>(aSize)) {
+ rv = NS_OK;
+ }
+ (void)PR_Close(urandom);
+ }
+ return rv;
+#endif
+}
+
+nsresult
+GenerateGUID(nsCString& _guid)
+{
+ _guid.Truncate();
+
+ // Request raw random bytes and base64url encode them. For each set of three
+ // bytes, we get one character.
+ const uint32_t kRequiredBytesLength =
+ static_cast<uint32_t>(GUID_LENGTH / 4 * 3);
+
+ uint8_t buffer[kRequiredBytesLength];
+ nsresult rv = GenerateRandomBytes(kRequiredBytesLength, buffer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Base64URLEncode(kRequiredBytesLength, buffer,
+ Base64URLEncodePaddingPolicy::Omit, _guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(_guid.Length() == GUID_LENGTH, "GUID is not the right size!");
+ return NS_OK;
+}
+
+bool
+IsValidGUID(const nsACString& aGUID)
+{
+ nsCString::size_type len = aGUID.Length();
+ if (len != GUID_LENGTH) {
+ return false;
+ }
+
+ for (nsCString::size_type i = 0; i < len; i++ ) {
+ char c = aGUID[i];
+ if ((c >= 'a' && c <= 'z') || // a-z
+ (c >= 'A' && c <= 'Z') || // A-Z
+ (c >= '0' && c <= '9') || // 0-9
+ c == '-' || c == '_') { // - or _
+ continue;
+ }
+ return false;
+ }
+ return true;
+}
+
+void
+TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed)
+{
+ aTrimmed = aTitle;
+ if (aTitle.Length() > TITLE_LENGTH_MAX) {
+ aTrimmed = StringHead(aTitle, TITLE_LENGTH_MAX);
+ }
+}
+
+PRTime
+RoundToMilliseconds(PRTime aTime) {
+ return aTime - (aTime % PR_USEC_PER_MSEC);
+}
+
+PRTime
+RoundedPRNow() {
+ return RoundToMilliseconds(PR_Now());
+}
+
+void
+ForceWALCheckpoint()
+{
+ RefPtr<Database> DB = Database::GetDatabase();
+ if (DB) {
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = DB->GetAsyncStatement(
+ "pragma wal_checkpoint "
+ );
+ if (stmt) {
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ (void)stmt->ExecuteAsync(nullptr, getter_AddRefs(handle));
+ }
+ }
+}
+
+bool
+GetHiddenState(bool aIsRedirect,
+ uint32_t aTransitionType)
+{
+ return aTransitionType == nsINavHistoryService::TRANSITION_FRAMED_LINK ||
+ aTransitionType == nsINavHistoryService::TRANSITION_EMBED ||
+ aIsRedirect;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// PlacesEvent
+
+PlacesEvent::PlacesEvent(const char* aTopic)
+: mTopic(aTopic)
+{
+}
+
+NS_IMETHODIMP
+PlacesEvent::Run()
+{
+ Notify();
+ return NS_OK;
+}
+
+void
+PlacesEvent::Notify()
+{
+ NS_ASSERTION(NS_IsMainThread(), "Must only be used on the main thread!");
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ (void)obs->NotifyObservers(nullptr, mTopic, nullptr);
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(
+ PlacesEvent
+, Runnable
+)
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncStatementCallbackNotifier
+
+NS_IMETHODIMP
+AsyncStatementCallbackNotifier::HandleCompletion(uint16_t aReason)
+{
+ if (aReason != mozIStorageStatementCallback::REASON_FINISHED)
+ return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ (void)obs->NotifyObservers(nullptr, mTopic, nullptr);
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AsyncStatementCallbackNotifier
+
+NS_IMETHODIMP
+AsyncStatementTelemetryTimer::HandleCompletion(uint16_t aReason)
+{
+ if (aReason == mozIStorageStatementCallback::REASON_FINISHED) {
+ Telemetry::AccumulateTimeDelta(mHistogramId, mStart);
+ }
+ return NS_OK;
+}
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/Helpers.h b/toolkit/components/places/Helpers.h
new file mode 100644
index 0000000000..654e425393
--- /dev/null
+++ b/toolkit/components/places/Helpers.h
@@ -0,0 +1,296 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla_places_Helpers_h_
+#define mozilla_places_Helpers_h_
+
+/**
+ * This file contains helper classes used by various bits of Places code.
+ */
+
+#include "mozilla/storage.h"
+#include "nsIURI.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+#include "prtime.h"
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// Asynchronous Statement Callback Helper
+
+class WeakAsyncStatementCallback : public mozIStorageStatementCallback
+{
+public:
+ NS_DECL_MOZISTORAGESTATEMENTCALLBACK
+ WeakAsyncStatementCallback() {}
+
+protected:
+ virtual ~WeakAsyncStatementCallback() {}
+};
+
+class AsyncStatementCallback : public WeakAsyncStatementCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+ AsyncStatementCallback() {}
+
+protected:
+ virtual ~AsyncStatementCallback() {}
+};
+
+/**
+ * Macros to use in place of NS_DECL_MOZISTORAGESTATEMENTCALLBACK to declare the
+ * methods this class assumes silent or notreached.
+ */
+#define NS_DECL_ASYNCSTATEMENTCALLBACK \
+ NS_IMETHOD HandleResult(mozIStorageResultSet *) override; \
+ NS_IMETHOD HandleCompletion(uint16_t) override;
+
+/**
+ * Utils to bind a specified URI (or URL) to a statement or binding params, at
+ * the specified index or name.
+ * @note URIs are always bound as UTF8.
+ */
+class URIBinder // static
+{
+public:
+ // Bind URI to statement by index.
+ static nsresult Bind(mozIStorageStatement* statement,
+ int32_t index,
+ nsIURI* aURI);
+ // Statement URLCString to statement by index.
+ static nsresult Bind(mozIStorageStatement* statement,
+ int32_t index,
+ const nsACString& aURLString);
+ // Bind URI to statement by name.
+ static nsresult Bind(mozIStorageStatement* statement,
+ const nsACString& aName,
+ nsIURI* aURI);
+ // Bind URLCString to statement by name.
+ static nsresult Bind(mozIStorageStatement* statement,
+ const nsACString& aName,
+ const nsACString& aURLString);
+ // Bind URI to params by index.
+ static nsresult Bind(mozIStorageBindingParams* aParams,
+ int32_t index,
+ nsIURI* aURI);
+ // Bind URLCString to params by index.
+ static nsresult Bind(mozIStorageBindingParams* aParams,
+ int32_t index,
+ const nsACString& aURLString);
+ // Bind URI to params by name.
+ static nsresult Bind(mozIStorageBindingParams* aParams,
+ const nsACString& aName,
+ nsIURI* aURI);
+ // Bind URLCString to params by name.
+ static nsresult Bind(mozIStorageBindingParams* aParams,
+ const nsACString& aName,
+ const nsACString& aURLString);
+};
+
+/**
+ * This extracts the hostname from the URI and reverses it in the
+ * form that we use (always ending with a "."). So
+ * "http://microsoft.com/" becomes "moc.tfosorcim."
+ *
+ * The idea behind this is that we can create an index over the items in
+ * the reversed host name column, and then query for as much or as little
+ * of the host name as we feel like.
+ *
+ * For example, the query "host >= 'gro.allizom.' AND host < 'gro.allizom/'
+ * Matches all host names ending in '.mozilla.org', including
+ * 'developer.mozilla.org' and just 'mozilla.org' (since we define all
+ * reversed host names to end in a period, even 'mozilla.org' matches).
+ * The important thing is that this operation uses the index. Any substring
+ * calls in a select statement (even if it's for the beginning of a string)
+ * will bypass any indices and will be slow).
+ *
+ * @param aURI
+ * URI that contains spec to reverse
+ * @param aRevHost
+ * Out parameter
+ */
+nsresult GetReversedHostname(nsIURI* aURI, nsString& aRevHost);
+
+/**
+ * Similar method to GetReversedHostName but for strings
+ */
+void GetReversedHostname(const nsString& aForward, nsString& aRevHost);
+
+/**
+ * Reverses a string.
+ *
+ * @param aInput
+ * The string to be reversed
+ * @param aReversed
+ * Output parameter will contain the reversed string
+ */
+void ReverseString(const nsString& aInput, nsString& aReversed);
+
+/**
+ * Generates an 12 character guid to be used by bookmark and history entries.
+ *
+ * @note This guid uses the characters a-z, A-Z, 0-9, '-', and '_'.
+ */
+nsresult GenerateGUID(nsCString& _guid);
+
+/**
+ * Determines if the string is a valid guid or not.
+ *
+ * @param aGUID
+ * The guid to test.
+ * @return true if it is a valid guid, false otherwise.
+ */
+bool IsValidGUID(const nsACString& aGUID);
+
+/**
+ * Truncates the title if it's longer than TITLE_LENGTH_MAX.
+ *
+ * @param aTitle
+ * The title to truncate (if necessary)
+ * @param aTrimmed
+ * Output parameter to return the trimmed string
+ */
+void TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed);
+
+/**
+ * Round down a PRTime value to milliseconds precision (...000).
+ *
+ * @param aTime
+ * a PRTime value.
+ * @return aTime rounded down to milliseconds precision.
+ */
+PRTime RoundToMilliseconds(PRTime aTime);
+
+/**
+ * Round down PR_Now() to milliseconds precision.
+ *
+ * @return @see PR_Now, RoundToMilliseconds.
+ */
+PRTime RoundedPRNow();
+
+/**
+ * Used to finalize a statementCache on a specified thread.
+ */
+template<typename StatementType>
+class FinalizeStatementCacheProxy : public Runnable
+{
+public:
+ /**
+ * Constructor.
+ *
+ * @param aStatementCache
+ * The statementCache that should be finalized.
+ * @param aOwner
+ * The object that owns the statement cache. This runnable will hold
+ * a strong reference to it so aStatementCache will not disappear from
+ * under us.
+ */
+ FinalizeStatementCacheProxy(
+ mozilla::storage::StatementCache<StatementType>& aStatementCache,
+ nsISupports* aOwner
+ )
+ : mStatementCache(aStatementCache)
+ , mOwner(aOwner)
+ , mCallingThread(do_GetCurrentThread())
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ mStatementCache.FinalizeStatements();
+ // Release the owner back on the calling thread.
+ NS_ProxyRelease(mCallingThread, mOwner.forget());
+ return NS_OK;
+ }
+
+protected:
+ mozilla::storage::StatementCache<StatementType>& mStatementCache;
+ nsCOMPtr<nsISupports> mOwner;
+ nsCOMPtr<nsIThread> mCallingThread;
+};
+
+/**
+ * Forces a WAL checkpoint. This will cause all transactions stored in the
+ * journal file to be committed to the main database.
+ *
+ * @note The checkpoint will force a fsync/flush.
+ */
+void ForceWALCheckpoint();
+
+/**
+ * Determines if a visit should be marked as hidden given its transition type
+ * and whether or not it was a redirect.
+ *
+ * @param aIsRedirect
+ * True if this visit was a redirect, false otherwise.
+ * @param aTransitionType
+ * The transition type of the visit.
+ * @return true if this visit should be hidden.
+ */
+bool GetHiddenState(bool aIsRedirect,
+ uint32_t aTransitionType);
+
+/**
+ * Notifies a specified topic via the observer service.
+ */
+class PlacesEvent : public Runnable
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIRUNNABLE
+
+ explicit PlacesEvent(const char* aTopic);
+protected:
+ ~PlacesEvent() {}
+ void Notify();
+
+ const char* const mTopic;
+};
+
+/**
+ * Used to notify a topic to system observers on async execute completion.
+ */
+class AsyncStatementCallbackNotifier : public AsyncStatementCallback
+{
+public:
+ explicit AsyncStatementCallbackNotifier(const char* aTopic)
+ : mTopic(aTopic)
+ {
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason);
+
+private:
+ const char* mTopic;
+};
+
+/**
+ * Used to notify a topic to system observers on async execute completion.
+ */
+class AsyncStatementTelemetryTimer : public AsyncStatementCallback
+{
+public:
+ explicit AsyncStatementTelemetryTimer(Telemetry::ID aHistogramId,
+ TimeStamp aStart = TimeStamp::Now())
+ : mHistogramId(aHistogramId)
+ , mStart(aStart)
+ {
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason);
+
+private:
+ const Telemetry::ID mHistogramId;
+ const TimeStamp mStart;
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_Helpers_h_
diff --git a/toolkit/components/places/History.cpp b/toolkit/components/places/History.cpp
new file mode 100644
index 0000000000..61f78cb83e
--- /dev/null
+++ b/toolkit/components/places/History.cpp
@@ -0,0 +1,2977 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/MemoryReporting.h"
+
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "nsXULAppAPI.h"
+
+#include "History.h"
+#include "nsNavHistory.h"
+#include "nsNavBookmarks.h"
+#include "nsAnnotationService.h"
+#include "Helpers.h"
+#include "PlaceInfo.h"
+#include "VisitInfo.h"
+#include "nsPlacesMacros.h"
+
+#include "mozilla/storage.h"
+#include "mozilla/dom/Link.h"
+#include "nsDocShellCID.h"
+#include "mozilla/Services.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIXPConnect.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h" // for nsAutoScriptBlocker
+#include "nsJSUtils.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "nsPrintfCString.h"
+#include "nsTHashtable.h"
+#include "jsapi.h"
+
+// Initial size for the cache holding visited status observers.
+#define VISIT_OBSERVERS_INITIAL_CACHE_LENGTH 64
+
+// Initial length for the visits removal hash.
+#define VISITS_REMOVAL_INITIAL_HASH_LENGTH 64
+
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+using mozilla::Unused;
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// Global Defines
+
+#define URI_VISITED "visited"
+#define URI_NOT_VISITED "not visited"
+#define URI_VISITED_RESOLUTION_TOPIC "visited-status-resolution"
+// Observer event fired after a visit has been registered in the DB.
+#define URI_VISIT_SAVED "uri-visit-saved"
+
+#define DESTINATIONFILEURI_ANNO \
+ NS_LITERAL_CSTRING("downloads/destinationFileURI")
+#define DESTINATIONFILENAME_ANNO \
+ NS_LITERAL_CSTRING("downloads/destinationFileName")
+
+////////////////////////////////////////////////////////////////////////////////
+//// VisitData
+
+struct VisitData {
+ VisitData()
+ : placeId(0)
+ , visitId(0)
+ , hidden(true)
+ , shouldUpdateHidden(true)
+ , typed(false)
+ , transitionType(UINT32_MAX)
+ , visitTime(0)
+ , frecency(-1)
+ , lastVisitId(0)
+ , lastVisitTime(0)
+ , visitCount(0)
+ , referrerVisitId(0)
+ , titleChanged(false)
+ , shouldUpdateFrecency(true)
+ {
+ guid.SetIsVoid(true);
+ title.SetIsVoid(true);
+ }
+
+ explicit VisitData(nsIURI* aURI,
+ nsIURI* aReferrer = nullptr)
+ : placeId(0)
+ , visitId(0)
+ , hidden(true)
+ , shouldUpdateHidden(true)
+ , typed(false)
+ , transitionType(UINT32_MAX)
+ , visitTime(0)
+ , frecency(-1)
+ , lastVisitId(0)
+ , lastVisitTime(0)
+ , visitCount(0)
+ , referrerVisitId(0)
+ , titleChanged(false)
+ , shouldUpdateFrecency(true)
+ {
+ MOZ_ASSERT(aURI);
+ if (aURI) {
+ (void)aURI->GetSpec(spec);
+ (void)GetReversedHostname(aURI, revHost);
+ }
+ if (aReferrer) {
+ (void)aReferrer->GetSpec(referrerSpec);
+ }
+ guid.SetIsVoid(true);
+ title.SetIsVoid(true);
+ }
+
+ /**
+ * Sets the transition type of the visit, as well as if it was typed.
+ *
+ * @param aTransitionType
+ * The transition type constant to set. Must be one of the
+ * TRANSITION_ constants on nsINavHistoryService.
+ */
+ void SetTransitionType(uint32_t aTransitionType)
+ {
+ typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED;
+ transitionType = aTransitionType;
+ }
+
+ int64_t placeId;
+ nsCString guid;
+ int64_t visitId;
+ nsCString spec;
+ nsString revHost;
+ bool hidden;
+ bool shouldUpdateHidden;
+ bool typed;
+ uint32_t transitionType;
+ PRTime visitTime;
+ int32_t frecency;
+ int64_t lastVisitId;
+ PRTime lastVisitTime;
+ uint32_t visitCount;
+
+ /**
+ * Stores the title. If this is empty (IsEmpty() returns true), then the
+ * title should be removed from the Place. If the title is void (IsVoid()
+ * returns true), then no title has been set on this object, and titleChanged
+ * should remain false.
+ */
+ nsString title;
+
+ nsCString referrerSpec;
+ int64_t referrerVisitId;
+
+ // TODO bug 626836 hook up hidden and typed change tracking too!
+ bool titleChanged;
+
+ // Indicates whether frecency should be updated for this visit.
+ bool shouldUpdateFrecency;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// RemoveVisitsFilter
+
+/**
+ * Used to store visit filters for RemoveVisits.
+ */
+struct RemoveVisitsFilter {
+ RemoveVisitsFilter()
+ : transitionType(UINT32_MAX)
+ {
+ }
+
+ uint32_t transitionType;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// PlaceHashKey
+
+class PlaceHashKey : public nsCStringHashKey
+{
+public:
+ explicit PlaceHashKey(const nsACString& aSpec)
+ : nsCStringHashKey(&aSpec)
+ , mVisitCount(0)
+ , mBookmarked(false)
+#ifdef DEBUG
+ , mIsInitialized(false)
+#endif
+ {
+ }
+
+ explicit PlaceHashKey(const nsACString* aSpec)
+ : nsCStringHashKey(aSpec)
+ , mVisitCount(0)
+ , mBookmarked(false)
+#ifdef DEBUG
+ , mIsInitialized(false)
+#endif
+ {
+ }
+
+ PlaceHashKey(const PlaceHashKey& aOther)
+ : nsCStringHashKey(&aOther.GetKey())
+ {
+ MOZ_ASSERT(false, "Do not call me!");
+ }
+
+ void SetProperties(uint32_t aVisitCount, bool aBookmarked)
+ {
+ mVisitCount = aVisitCount;
+ mBookmarked = aBookmarked;
+#ifdef DEBUG
+ mIsInitialized = true;
+#endif
+ }
+
+ uint32_t VisitCount() const
+ {
+#ifdef DEBUG
+ MOZ_ASSERT(mIsInitialized, "PlaceHashKey::mVisitCount not set");
+#endif
+ return mVisitCount;
+ }
+
+ bool IsBookmarked() const
+ {
+#ifdef DEBUG
+ MOZ_ASSERT(mIsInitialized, "PlaceHashKey::mBookmarked not set");
+#endif
+ return mBookmarked;
+ }
+
+ // Array of VisitData objects.
+ nsTArray<VisitData> mVisits;
+private:
+ // Visit count for this place.
+ uint32_t mVisitCount;
+ // Whether this place is bookmarked.
+ bool mBookmarked;
+#ifdef DEBUG
+ // Whether previous attributes are set.
+ bool mIsInitialized;
+#endif
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Anonymous Helpers
+
+namespace {
+
+/**
+ * Convert the given js value to a js array.
+ *
+ * @param [in] aValue
+ * the JS value to convert.
+ * @param [in] aCtx
+ * The JSContext for aValue.
+ * @param [out] _array
+ * the JS array.
+ * @param [out] _arrayLength
+ * _array's length.
+ */
+nsresult
+GetJSArrayFromJSValue(JS::Handle<JS::Value> aValue,
+ JSContext* aCtx,
+ JS::MutableHandle<JSObject*> _array,
+ uint32_t* _arrayLength) {
+ if (aValue.isObjectOrNull()) {
+ JS::Rooted<JSObject*> val(aCtx, aValue.toObjectOrNull());
+ bool isArray;
+ if (!JS_IsArrayObject(aCtx, val, &isArray)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (isArray) {
+ _array.set(val);
+ (void)JS_GetArrayLength(aCtx, _array, _arrayLength);
+ NS_ENSURE_ARG(*_arrayLength > 0);
+ return NS_OK;
+ }
+ }
+
+ // Build a temporary array to store this one item so the code below can
+ // just loop.
+ *_arrayLength = 1;
+ _array.set(JS_NewArrayObject(aCtx, 0));
+ NS_ENSURE_TRUE(_array, NS_ERROR_OUT_OF_MEMORY);
+
+ bool rc = JS_DefineElement(aCtx, _array, 0, aValue, 0);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+ return NS_OK;
+}
+
+/**
+ * Attemps to convert a given js value to a nsIURI object.
+ * @param aCtx
+ * The JSContext for aValue.
+ * @param aValue
+ * The JS value to convert.
+ * @return the nsIURI object, or null if aValue is not a nsIURI object.
+ */
+already_AddRefed<nsIURI>
+GetJSValueAsURI(JSContext* aCtx,
+ const JS::Value& aValue) {
+ if (!aValue.isPrimitive()) {
+ nsCOMPtr<nsIXPConnect> xpc = mozilla::services::GetXPConnect();
+
+ nsCOMPtr<nsIXPConnectWrappedNative> wrappedObj;
+ nsresult rv = xpc->GetWrappedNativeOfJSObject(aCtx, aValue.toObjectOrNull(),
+ getter_AddRefs(wrappedObj));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ nsCOMPtr<nsIURI> uri = do_QueryWrappedNative(wrappedObj);
+ return uri.forget();
+ }
+ return nullptr;
+}
+
+/**
+ * Obtains an nsIURI from the "uri" property of a JSObject.
+ *
+ * @param aCtx
+ * The JSContext for aObject.
+ * @param aObject
+ * The JSObject to get the URI from.
+ * @param aProperty
+ * The name of the property to get the URI from.
+ * @return the URI if it exists.
+ */
+already_AddRefed<nsIURI>
+GetURIFromJSObject(JSContext* aCtx,
+ JS::Handle<JSObject *> aObject,
+ const char* aProperty)
+{
+ JS::Rooted<JS::Value> uriVal(aCtx);
+ bool rc = JS_GetProperty(aCtx, aObject, aProperty, &uriVal);
+ NS_ENSURE_TRUE(rc, nullptr);
+ return GetJSValueAsURI(aCtx, uriVal);
+}
+
+/**
+ * Attemps to convert a JS value to a string.
+ * @param aCtx
+ * The JSContext for aObject.
+ * @param aValue
+ * The JS value to convert.
+ * @param _string
+ * The string to populate with the value, or set it to void.
+ */
+void
+GetJSValueAsString(JSContext* aCtx,
+ const JS::Value& aValue,
+ nsString& _string) {
+ if (aValue.isUndefined() ||
+ !(aValue.isNull() || aValue.isString())) {
+ _string.SetIsVoid(true);
+ return;
+ }
+
+ // |null| in JS maps to the empty string.
+ if (aValue.isNull()) {
+ _string.Truncate();
+ return;
+ }
+
+ if (!AssignJSString(aCtx, _string, aValue.toString())) {
+ _string.SetIsVoid(true);
+ }
+}
+
+/**
+ * Obtains the specified property of a JSObject.
+ *
+ * @param aCtx
+ * The JSContext for aObject.
+ * @param aObject
+ * The JSObject to get the string from.
+ * @param aProperty
+ * The property to get the value from.
+ * @param _string
+ * The string to populate with the value, or set it to void.
+ */
+void
+GetStringFromJSObject(JSContext* aCtx,
+ JS::Handle<JSObject *> aObject,
+ const char* aProperty,
+ nsString& _string)
+{
+ JS::Rooted<JS::Value> val(aCtx);
+ bool rc = JS_GetProperty(aCtx, aObject, aProperty, &val);
+ if (!rc) {
+ _string.SetIsVoid(true);
+ return;
+ }
+ else {
+ GetJSValueAsString(aCtx, val, _string);
+ }
+}
+
+/**
+ * Obtains the specified property of a JSObject.
+ *
+ * @param aCtx
+ * The JSContext for aObject.
+ * @param aObject
+ * The JSObject to get the int from.
+ * @param aProperty
+ * The property to get the value from.
+ * @param _int
+ * The integer to populate with the value on success.
+ */
+template <typename IntType>
+nsresult
+GetIntFromJSObject(JSContext* aCtx,
+ JS::Handle<JSObject *> aObject,
+ const char* aProperty,
+ IntType* _int)
+{
+ JS::Rooted<JS::Value> value(aCtx);
+ bool rc = JS_GetProperty(aCtx, aObject, aProperty, &value);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+ if (value.isUndefined()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ NS_ENSURE_ARG(value.isPrimitive());
+ NS_ENSURE_ARG(value.isNumber());
+
+ double num;
+ rc = JS::ToNumber(aCtx, value, &num);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+ NS_ENSURE_ARG(IntType(num) == num);
+
+ *_int = IntType(num);
+ return NS_OK;
+}
+
+/**
+ * Obtains the specified property of a JSObject.
+ *
+ * @pre aArray must be an Array object.
+ *
+ * @param aCtx
+ * The JSContext for aArray.
+ * @param aArray
+ * The JSObject to get the object from.
+ * @param aIndex
+ * The index to get the object from.
+ * @param objOut
+ * Set to the JSObject pointer on success.
+ */
+nsresult
+GetJSObjectFromArray(JSContext* aCtx,
+ JS::Handle<JSObject*> aArray,
+ uint32_t aIndex,
+ JS::MutableHandle<JSObject*> objOut)
+{
+ JS::Rooted<JS::Value> value(aCtx);
+ bool rc = JS_GetElement(aCtx, aArray, aIndex, &value);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+ NS_ENSURE_ARG(!value.isPrimitive());
+ objOut.set(&value.toObject());
+ return NS_OK;
+}
+
+class VisitedQuery final : public AsyncStatementCallback,
+ public mozIStorageCompletionCallback
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ static nsresult Start(nsIURI* aURI,
+ mozIVisitedStatusCallback* aCallback=nullptr)
+ {
+ NS_PRECONDITION(aURI, "Null URI");
+
+ // If we are a content process, always remote the request to the
+ // parent process.
+ if (XRE_IsContentProcess()) {
+ URIParams uri;
+ SerializeURI(aURI, uri);
+
+ mozilla::dom::ContentChild* cpc =
+ mozilla::dom::ContentChild::GetSingleton();
+ NS_ASSERTION(cpc, "Content Protocol is NULL!");
+ (void)cpc->SendStartVisitedQuery(uri);
+ return NS_OK;
+ }
+
+ nsMainThreadPtrHandle<mozIVisitedStatusCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitedStatusCallback>(aCallback));
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(navHistory);
+ if (navHistory->hasEmbedVisit(aURI)) {
+ RefPtr<VisitedQuery> cb = new VisitedQuery(aURI, callback, true);
+ NS_ENSURE_TRUE(cb, NS_ERROR_OUT_OF_MEMORY);
+ // As per IHistory contract, we must notify asynchronously.
+ NS_DispatchToMainThread(NewRunnableMethod(cb, &VisitedQuery::NotifyVisitedStatus));
+
+ return NS_OK;
+ }
+
+ History* history = History::GetService();
+ NS_ENSURE_STATE(history);
+ RefPtr<VisitedQuery> cb = new VisitedQuery(aURI, callback);
+ NS_ENSURE_TRUE(cb, NS_ERROR_OUT_OF_MEMORY);
+ nsresult rv = history->GetIsVisitedStatement(cb);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ // Note: the return value matters here. We call into this method, it's not
+ // just xpcom boilerplate.
+ NS_IMETHOD Complete(nsresult aResult, nsISupports* aStatement) override
+ {
+ NS_ENSURE_SUCCESS(aResult, aResult);
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = do_QueryInterface(aStatement);
+ NS_ENSURE_STATE(stmt);
+ // Bind by index for performance.
+ nsresult rv = URIBinder::Bind(stmt, 0, mURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ return stmt->ExecuteAsync(this, getter_AddRefs(handle));
+ }
+
+ NS_IMETHOD HandleResult(mozIStorageResultSet* aResults) override
+ {
+ // If this method is called, we've gotten results, which means we have a
+ // visit.
+ mIsVisited = true;
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleError(mozIStorageError* aError) override
+ {
+ // mIsVisited is already set to false, and that's the assumption we will
+ // make if an error occurred.
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override
+ {
+ if (aReason != mozIStorageStatementCallback::REASON_FINISHED) {
+ return NS_OK;
+ }
+
+ nsresult rv = NotifyVisitedStatus();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ nsresult NotifyVisitedStatus()
+ {
+ // If an external handling callback is provided, just notify through it.
+ if (!!mCallback) {
+ mCallback->IsVisited(mURI, mIsVisited);
+ return NS_OK;
+ }
+
+ if (mIsVisited) {
+ History* history = History::GetService();
+ NS_ENSURE_STATE(history);
+ history->NotifyVisited(mURI);
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ nsAutoString status;
+ if (mIsVisited) {
+ status.AssignLiteral(URI_VISITED);
+ }
+ else {
+ status.AssignLiteral(URI_NOT_VISITED);
+ }
+ (void)observerService->NotifyObservers(mURI,
+ URI_VISITED_RESOLUTION_TOPIC,
+ status.get());
+ }
+
+ return NS_OK;
+ }
+
+private:
+ explicit VisitedQuery(nsIURI* aURI,
+ const nsMainThreadPtrHandle<mozIVisitedStatusCallback>& aCallback,
+ bool aIsVisited=false)
+ : mURI(aURI)
+ , mCallback(aCallback)
+ , mIsVisited(aIsVisited)
+ {
+ }
+
+ ~VisitedQuery()
+ {
+ }
+
+ nsCOMPtr<nsIURI> mURI;
+ nsMainThreadPtrHandle<mozIVisitedStatusCallback> mCallback;
+ bool mIsVisited;
+};
+
+NS_IMPL_ISUPPORTS_INHERITED(
+ VisitedQuery
+, AsyncStatementCallback
+, mozIStorageCompletionCallback
+)
+
+/**
+ * Notifies observers about a visit.
+ */
+class NotifyVisitObservers : public Runnable
+{
+public:
+ explicit NotifyVisitObservers(VisitData& aPlace)
+ : mPlace(aPlace)
+ , mHistory(History::GetService())
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ // We are in the main thread, no need to lock.
+ if (mHistory->IsShuttingDown()) {
+ // If we are shutting down, we cannot notify the observers.
+ return NS_OK;
+ }
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ if (!navHistory) {
+ NS_WARNING("Trying to notify about a visit but cannot get the history service!");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlace.spec));
+ if (!uri) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // Notify the visit. Note that TRANSITION_EMBED visits are never added
+ // to the database, thus cannot be queried and we don't notify them.
+ if (mPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) {
+ navHistory->NotifyOnVisit(uri, mPlace.visitId, mPlace.visitTime,
+ mPlace.referrerVisitId, mPlace.transitionType,
+ mPlace.guid, mPlace.hidden,
+ mPlace.visitCount + 1, // Add current visit.
+ static_cast<uint32_t>(mPlace.typed));
+ }
+
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ DebugOnly<nsresult> rv =
+ obsService->NotifyObservers(uri, URI_VISIT_SAVED, nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Could not notify observers");
+ }
+
+ History* history = History::GetService();
+ NS_ENSURE_STATE(history);
+ history->AppendToRecentlyVisitedURIs(uri);
+ history->NotifyVisited(uri);
+
+ return NS_OK;
+ }
+private:
+ VisitData mPlace;
+ RefPtr<History> mHistory;
+};
+
+/**
+ * Notifies observers about a pages title changing.
+ */
+class NotifyTitleObservers : public Runnable
+{
+public:
+ /**
+ * Notifies observers on the main thread.
+ *
+ * @param aSpec
+ * The spec of the URI to notify about.
+ * @param aTitle
+ * The new title to notify about.
+ */
+ NotifyTitleObservers(const nsCString& aSpec,
+ const nsString& aTitle,
+ const nsCString& aGUID)
+ : mSpec(aSpec)
+ , mTitle(aTitle)
+ , mGUID(aGUID)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mSpec));
+ if (!uri) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ navHistory->NotifyTitleChange(uri, mTitle, mGUID);
+
+ return NS_OK;
+ }
+private:
+ const nsCString mSpec;
+ const nsString mTitle;
+ const nsCString mGUID;
+};
+
+/**
+ * Helper class for methods which notify their callers through the
+ * mozIVisitInfoCallback interface.
+ */
+class NotifyPlaceInfoCallback : public Runnable
+{
+public:
+ NotifyPlaceInfoCallback(const nsMainThreadPtrHandle<mozIVisitInfoCallback>& aCallback,
+ const VisitData& aPlace,
+ bool aIsSingleVisit,
+ nsresult aResult)
+ : mCallback(aCallback)
+ , mPlace(aPlace)
+ , mResult(aResult)
+ , mIsSingleVisit(aIsSingleVisit)
+ {
+ MOZ_ASSERT(aCallback, "Must pass a non-null callback!");
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ bool hasValidURIs = true;
+ nsCOMPtr<nsIURI> referrerURI;
+ if (!mPlace.referrerSpec.IsEmpty()) {
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(referrerURI), mPlace.referrerSpec));
+ hasValidURIs = !!referrerURI;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), mPlace.spec));
+ hasValidURIs = hasValidURIs && !!uri;
+
+ nsCOMPtr<mozIPlaceInfo> place;
+ if (mIsSingleVisit) {
+ nsCOMPtr<mozIVisitInfo> visit =
+ new VisitInfo(mPlace.visitId, mPlace.visitTime, mPlace.transitionType,
+ referrerURI.forget());
+ PlaceInfo::VisitsArray visits;
+ (void)visits.AppendElement(visit);
+
+ // The frecency isn't exposed because it may not reflect the updated value
+ // in the case of InsertVisitedURIs.
+ place =
+ new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title,
+ -1, visits);
+ }
+ else {
+ // Same as above.
+ place =
+ new PlaceInfo(mPlace.placeId, mPlace.guid, uri.forget(), mPlace.title,
+ -1);
+ }
+
+ if (NS_SUCCEEDED(mResult) && hasValidURIs) {
+ (void)mCallback->HandleResult(place);
+ } else {
+ (void)mCallback->HandleError(mResult, place);
+ }
+
+ return NS_OK;
+ }
+
+private:
+ nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback;
+ VisitData mPlace;
+ const nsresult mResult;
+ bool mIsSingleVisit;
+};
+
+/**
+ * Notifies a callback object when the operation is complete.
+ */
+class NotifyCompletion : public Runnable
+{
+public:
+ explicit NotifyCompletion(const nsMainThreadPtrHandle<mozIVisitInfoCallback>& aCallback)
+ : mCallback(aCallback)
+ {
+ MOZ_ASSERT(aCallback, "Must pass a non-null callback!");
+ }
+
+ NS_IMETHOD Run() override
+ {
+ if (NS_IsMainThread()) {
+ (void)mCallback->HandleCompletion();
+ }
+ else {
+ (void)NS_DispatchToMainThread(this);
+ }
+ return NS_OK;
+ }
+
+private:
+ nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback;
+};
+
+/**
+ * Checks to see if we can add aURI to history, and dispatches an error to
+ * aCallback (if provided) if we cannot.
+ *
+ * @param aURI
+ * The URI to check.
+ * @param [optional] aGUID
+ * The guid of the URI to check. This is passed back to the callback.
+ * @param [optional] aCallback
+ * The callback to notify if the URI cannot be added to history.
+ * @return true if the URI can be added to history, false otherwise.
+ */
+bool
+CanAddURI(nsIURI* aURI,
+ const nsCString& aGUID = EmptyCString(),
+ mozIVisitInfoCallback* aCallback = nullptr)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(navHistory, false);
+
+ bool canAdd;
+ nsresult rv = navHistory->CanAddURI(aURI, &canAdd);
+ if (NS_SUCCEEDED(rv) && canAdd) {
+ return true;
+ };
+
+ // We cannot add the URI. Notify the callback, if we were given one.
+ if (aCallback) {
+ VisitData place(aURI);
+ place.guid = aGUID;
+ nsMainThreadPtrHandle<mozIVisitInfoCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyPlaceInfoCallback(callback, place, true, NS_ERROR_INVALID_ARG);
+ (void)NS_DispatchToMainThread(event);
+ }
+
+ return false;
+}
+
+/**
+ * Adds a visit to the database.
+ */
+class InsertVisitedURIs final: public Runnable
+{
+public:
+ /**
+ * Adds a visit to the database asynchronously.
+ *
+ * @param aConnection
+ * The database connection to use for these operations.
+ * @param aPlaces
+ * The locations to record visits.
+ * @param [optional] aCallback
+ * The callback to notify about the visit.
+ */
+ static nsresult Start(mozIStorageConnection* aConnection,
+ nsTArray<VisitData>& aPlaces,
+ mozIVisitInfoCallback* aCallback = nullptr)
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+ MOZ_ASSERT(aPlaces.Length() > 0, "Must pass a non-empty array!");
+
+ // Make sure nsNavHistory service is up before proceeding:
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ MOZ_ASSERT(navHistory, "Could not get nsNavHistory?!");
+ if (!navHistory) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsMainThreadPtrHandle<mozIVisitInfoCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
+ RefPtr<InsertVisitedURIs> event =
+ new InsertVisitedURIs(aConnection, aPlaces, callback);
+
+ // Get the target thread, and then start the work!
+ nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
+ NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
+ nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
+
+ // Prevent the main thread from shutting down while this is running.
+ MutexAutoLock lockedScope(mHistory->GetShutdownMutex());
+ if (mHistory->IsShuttingDown()) {
+ // If we were already shutting down, we cannot insert the URIs.
+ return NS_OK;
+ }
+
+ mozStorageTransaction transaction(mDBConn, false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ VisitData* lastFetchedPlace = nullptr;
+ for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
+ VisitData& place = mPlaces.ElementAt(i);
+
+ // Fetching from the database can overwrite this information, so save it
+ // apart.
+ bool typed = place.typed;
+ bool hidden = place.hidden;
+
+ // We can avoid a database lookup if it's the same place as the last
+ // visit we added.
+ bool known = lastFetchedPlace && lastFetchedPlace->spec.Equals(place.spec);
+ if (!known) {
+ nsresult rv = mHistory->FetchPageInfo(place, &known);
+ if (NS_FAILED(rv)) {
+ if (!!mCallback) {
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyPlaceInfoCallback(mCallback, place, true, rv);
+ return NS_DispatchToMainThread(event);
+ }
+ return NS_OK;
+ }
+ lastFetchedPlace = &mPlaces.ElementAt(i);
+ } else {
+ // Copy over the data from the already known place.
+ place.placeId = lastFetchedPlace->placeId;
+ place.guid = lastFetchedPlace->guid;
+ place.lastVisitId = lastFetchedPlace->visitId;
+ place.lastVisitTime = lastFetchedPlace->visitTime;
+ place.titleChanged = !lastFetchedPlace->title.Equals(place.title);
+ place.frecency = lastFetchedPlace->frecency;
+ // Add one visit for the previous loop.
+ place.visitCount = ++(*lastFetchedPlace).visitCount;
+ }
+
+ // If any transition is typed, ensure the page is marked as typed.
+ if (typed != lastFetchedPlace->typed) {
+ place.typed = true;
+ }
+
+ // If any transition is visible, ensure the page is marked as visible.
+ if (hidden != lastFetchedPlace->hidden) {
+ place.hidden = false;
+ }
+
+ // If this is a new page, or the existing page was already visible,
+ // there's no need to try to unhide it.
+ if (!known || !lastFetchedPlace->hidden) {
+ place.shouldUpdateHidden = false;
+ }
+
+ FetchReferrerInfo(place);
+
+ nsresult rv = DoDatabaseInserts(known, place);
+ if (!!mCallback) {
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyPlaceInfoCallback(mCallback, place, true, rv);
+ nsresult rv2 = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv2, rv2);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Notify about title change if needed.
+ if ((!known && !place.title.IsVoid()) || place.titleChanged) {
+ event = new NotifyTitleObservers(place.spec, place.title, place.guid);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsresult rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+private:
+ InsertVisitedURIs(mozIStorageConnection* aConnection,
+ nsTArray<VisitData>& aPlaces,
+ const nsMainThreadPtrHandle<mozIVisitInfoCallback>& aCallback)
+ : mDBConn(aConnection)
+ , mCallback(aCallback)
+ , mHistory(History::GetService())
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ mPlaces.SwapElements(aPlaces);
+
+#ifdef DEBUG
+ for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ASSERT(NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec)));
+ MOZ_ASSERT(CanAddURI(uri),
+ "Passed a VisitData with a URI we cannot add to history!");
+ }
+#endif
+ }
+
+ /**
+ * Inserts or updates the entry in moz_places for this visit, adds the visit,
+ * and updates the frecency of the place.
+ *
+ * @param aKnown
+ * True if we already have an entry for this place in moz_places, false
+ * otherwise.
+ * @param aPlace
+ * The place we are adding a visit for.
+ */
+ nsresult DoDatabaseInserts(bool aKnown,
+ VisitData& aPlace)
+ {
+ MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
+
+ // If the page was in moz_places, we need to update the entry.
+ nsresult rv;
+ if (aKnown) {
+ rv = mHistory->UpdatePlace(aPlace);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // Otherwise, the page was not in moz_places, so now we have to add it.
+ else {
+ rv = mHistory->InsertPlace(aPlace);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aPlace.placeId = nsNavHistory::sLastInsertedPlaceId;
+ }
+ MOZ_ASSERT(aPlace.placeId > 0);
+
+ rv = AddVisit(aPlace);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // TODO (bug 623969) we shouldn't update this after each visit, but
+ // rather only for each unique place to save disk I/O.
+
+ // Don't update frecency if the page should not appear in autocomplete.
+ if (aPlace.shouldUpdateFrecency) {
+ rv = UpdateFrecency(aPlace);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ }
+
+ /**
+ * Fetches information about a referrer for aPlace if it was a recent
+ * visit or not.
+ *
+ * @param aPlace
+ * The VisitData for the visit we will eventually add.
+ *
+ */
+ void FetchReferrerInfo(VisitData& aPlace)
+ {
+ if (aPlace.referrerSpec.IsEmpty()) {
+ return;
+ }
+
+ VisitData referrer;
+ referrer.spec = aPlace.referrerSpec;
+ // If the referrer is the same as the page, we don't need to fetch it.
+ if (aPlace.referrerSpec.Equals(aPlace.spec)) {
+ referrer = aPlace;
+ // The page last visit id is also the referrer visit id.
+ aPlace.referrerVisitId = aPlace.lastVisitId;
+ } else {
+ bool exists = false;
+ if (NS_SUCCEEDED(mHistory->FetchPageInfo(referrer, &exists)) && exists) {
+ // Copy the referrer last visit id.
+ aPlace.referrerVisitId = referrer.lastVisitId;
+ }
+ }
+
+ // Check if the page has effectively been visited recently, otherwise
+ // discard the referrer info.
+ if (!aPlace.referrerVisitId || !referrer.lastVisitTime ||
+ aPlace.visitTime - referrer.lastVisitTime > RECENT_EVENT_THRESHOLD) {
+ // We will not be using the referrer data.
+ aPlace.referrerSpec.Truncate();
+ aPlace.referrerVisitId = 0;
+ }
+ }
+
+ /**
+ * Adds a visit for _place and updates it with the right visit id.
+ *
+ * @param _place
+ * The VisitData for the place we need to know visit information about.
+ */
+ nsresult AddVisit(VisitData& _place)
+ {
+ MOZ_ASSERT(_place.placeId > 0);
+
+ nsresult rv;
+ nsCOMPtr<mozIStorageStatement> stmt;
+ stmt = mHistory->GetStatement(
+ "INSERT INTO moz_historyvisits "
+ "(from_visit, place_id, visit_date, visit_type, session) "
+ "VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0) "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("from_visit"),
+ _place.referrerVisitId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("visit_date"),
+ _place.visitTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t transitionType = _place.transitionType;
+ MOZ_ASSERT(transitionType >= nsINavHistoryService::TRANSITION_LINK &&
+ transitionType <= nsINavHistoryService::TRANSITION_RELOAD,
+ "Invalid transition type!");
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("visit_type"),
+ transitionType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ _place.visitId = nsNavHistory::sLastInsertedVisitId;
+ MOZ_ASSERT(_place.visitId > 0);
+
+ return NS_OK;
+ }
+
+ /**
+ * Updates the frecency, and possibly the hidden-ness of aPlace.
+ *
+ * @param aPlace
+ * The VisitData for the place we want to update.
+ */
+ nsresult UpdateFrecency(const VisitData& aPlace)
+ {
+ MOZ_ASSERT(aPlace.shouldUpdateFrecency);
+ MOZ_ASSERT(aPlace.placeId > 0);
+
+ nsresult rv;
+ { // First, set our frecency to the proper value.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ stmt = mHistory->GetStatement(
+ "UPDATE moz_places "
+ "SET frecency = NOTIFY_FRECENCY("
+ "CALCULATE_FRECENCY(:page_id), "
+ "url, guid, hidden, last_visit_date"
+ ") "
+ "WHERE id = :page_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (!aPlace.hidden && aPlace.shouldUpdateHidden) {
+ // Mark the page as not hidden if the frecency is now nonzero.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ stmt = mHistory->GetStatement(
+ "UPDATE moz_places "
+ "SET hidden = 0 "
+ "WHERE id = :page_id AND frecency <> 0"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ }
+
+ mozIStorageConnection* mDBConn;
+
+ nsTArray<VisitData> mPlaces;
+
+ nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback;
+
+ /**
+ * Strong reference to the History object because we do not want it to
+ * disappear out from under us.
+ */
+ RefPtr<History> mHistory;
+};
+
+class GetPlaceInfo final : public Runnable {
+public:
+ /**
+ * Get the place info for a given place (by GUID or URI) asynchronously.
+ */
+ static nsresult Start(mozIStorageConnection* aConnection,
+ VisitData& aPlace,
+ mozIVisitInfoCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ nsMainThreadPtrHandle<mozIVisitInfoCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
+ RefPtr<GetPlaceInfo> event = new GetPlaceInfo(aPlace, callback);
+
+ // Get the target thread, and then start the work!
+ nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
+ NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
+ nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
+
+ bool exists;
+ nsresult rv = mHistory->FetchPageInfo(mPlace, &exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists)
+ rv = NS_ERROR_NOT_AVAILABLE;
+
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyPlaceInfoCallback(mCallback, mPlace, false, rv);
+
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+private:
+ GetPlaceInfo(VisitData& aPlace,
+ const nsMainThreadPtrHandle<mozIVisitInfoCallback>& aCallback)
+ : mPlace(aPlace)
+ , mCallback(aCallback)
+ , mHistory(History::GetService())
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+ }
+
+ VisitData mPlace;
+ nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback;
+ RefPtr<History> mHistory;
+};
+
+/**
+ * Sets the page title for a page in moz_places (if necessary).
+ */
+class SetPageTitle : public Runnable
+{
+public:
+ /**
+ * Sets a pages title in the database asynchronously.
+ *
+ * @param aConnection
+ * The database connection to use for this operation.
+ * @param aURI
+ * The URI to set the page title on.
+ * @param aTitle
+ * The title to set for the page, if the page exists.
+ */
+ static nsresult Start(mozIStorageConnection* aConnection,
+ nsIURI* aURI,
+ const nsAString& aTitle)
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+ MOZ_ASSERT(aURI, "Must pass a non-null URI object!");
+
+ nsCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<SetPageTitle> event = new SetPageTitle(spec, aTitle);
+
+ // Get the target thread, and then start the work!
+ nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
+ NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
+ rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
+
+ // First, see if the page exists in the database (we'll need its id later).
+ bool exists;
+ nsresult rv = mHistory->FetchPageInfo(mPlace, &exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists || !mPlace.titleChanged) {
+ // We have no record of this page, or we have no title change, so there
+ // is no need to do any further work.
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mPlace.placeId > 0,
+ "We somehow have an invalid place id here!");
+
+ // Now we can update our database record.
+ nsCOMPtr<mozIStorageStatement> stmt =
+ mHistory->GetStatement(
+ "UPDATE moz_places "
+ "SET title = :page_title "
+ "WHERE id = :page_id "
+ );
+ NS_ENSURE_STATE(stmt);
+
+ {
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Empty strings should clear the title, just like
+ // nsNavHistory::SetPageTitle.
+ if (mPlace.title.IsEmpty()) {
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("page_title"));
+ }
+ else {
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("page_title"),
+ StringHead(mPlace.title, TITLE_LENGTH_MAX));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyTitleObservers(mPlace.spec, mPlace.title, mPlace.guid);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+private:
+ SetPageTitle(const nsCString& aSpec,
+ const nsAString& aTitle)
+ : mHistory(History::GetService())
+ {
+ mPlace.spec = aSpec;
+ mPlace.title = aTitle;
+ }
+
+ VisitData mPlace;
+
+ /**
+ * Strong reference to the History object because we do not want it to
+ * disappear out from under us.
+ */
+ RefPtr<History> mHistory;
+};
+
+/**
+ * Adds download-specific annotations to a download page.
+ */
+class SetDownloadAnnotations final : public mozIVisitInfoCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit SetDownloadAnnotations(nsIURI* aDestination)
+ : mDestination(aDestination)
+ , mHistory(History::GetService())
+ {
+ MOZ_ASSERT(mDestination);
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ NS_IMETHOD HandleError(nsresult aResultCode, mozIPlaceInfo *aPlaceInfo) override
+ {
+ // Just don't add the annotations in case the visit isn't added.
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleResult(mozIPlaceInfo *aPlaceInfo) override
+ {
+ // Exit silently if the download destination is not a local file.
+ nsCOMPtr<nsIFileURL> destinationFileURL = do_QueryInterface(mDestination);
+ if (!destinationFileURL) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> source;
+ nsresult rv = aPlaceInfo->GetUri(getter_AddRefs(source));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> destinationFile;
+ rv = destinationFileURL->GetFile(getter_AddRefs(destinationFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString destinationFileName;
+ rv = destinationFile->GetLeafName(destinationFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString destinationURISpec;
+ rv = destinationFileURL->GetSpec(destinationURISpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use annotations for storing the additional download metadata.
+ nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+ NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
+
+ rv = annosvc->SetPageAnnotationString(
+ source,
+ DESTINATIONFILEURI_ANNO,
+ NS_ConvertUTF8toUTF16(destinationURISpec),
+ 0,
+ nsIAnnotationService::EXPIRE_WITH_HISTORY
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = annosvc->SetPageAnnotationString(
+ source,
+ DESTINATIONFILENAME_ANNO,
+ destinationFileName,
+ 0,
+ nsIAnnotationService::EXPIRE_WITH_HISTORY
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString title;
+ rv = aPlaceInfo->GetTitle(title);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // In case we are downloading a file that does not correspond to a web
+ // page for which the title is present, we populate the otherwise empty
+ // history title with the name of the destination file, to allow it to be
+ // visible and searchable in history results.
+ if (title.IsEmpty()) {
+ rv = mHistory->SetURITitle(source, destinationFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleCompletion() override
+ {
+ return NS_OK;
+ }
+
+private:
+ ~SetDownloadAnnotations() {}
+
+ nsCOMPtr<nsIURI> mDestination;
+
+ /**
+ * Strong reference to the History object because we do not want it to
+ * disappear out from under us.
+ */
+ RefPtr<History> mHistory;
+};
+NS_IMPL_ISUPPORTS(
+ SetDownloadAnnotations,
+ mozIVisitInfoCallback
+)
+
+/**
+ * Notify removed visits to observers.
+ */
+class NotifyRemoveVisits : public Runnable
+{
+public:
+
+ explicit NotifyRemoveVisits(nsTHashtable<PlaceHashKey>& aPlaces)
+ : mPlaces(VISITS_REMOVAL_INITIAL_HASH_LENGTH)
+ , mHistory(History::GetService())
+ {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "This should not be called on the main thread");
+ for (auto iter = aPlaces.Iter(); !iter.Done(); iter.Next()) {
+ PlaceHashKey* entry = iter.Get();
+ PlaceHashKey* copy = mPlaces.PutEntry(entry->GetKey());
+ copy->SetProperties(entry->VisitCount(), entry->IsBookmarked());
+ entry->mVisits.SwapElements(copy->mVisits);
+ }
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ // We are in the main thread, no need to lock.
+ if (mHistory->IsShuttingDown()) {
+ // If we are shutting down, we cannot notify the observers.
+ return NS_OK;
+ }
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ if (!navHistory) {
+ NS_WARNING("Cannot notify without the history service!");
+ return NS_OK;
+ }
+
+ // Wrap all notifications in a batch, so the view can handle changes in a
+ // more performant way, by initiating a refresh after a limited number of
+ // single changes.
+ (void)navHistory->BeginUpdateBatch();
+ for (auto iter = mPlaces.Iter(); !iter.Done(); iter.Next()) {
+ PlaceHashKey* entry = iter.Get();
+ const nsTArray<VisitData>& visits = entry->mVisits;
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), visits[0].spec));
+ // Notify an expiration only if we have a valid uri, otherwise
+ // the observer couldn't gather any useful data from the notification.
+ // This should be false only if there's a bug in the code preceding us.
+ if (uri) {
+ bool removingPage = visits.Length() == entry->VisitCount() &&
+ !entry->IsBookmarked();
+
+ // FindRemovableVisits only sets the transition type on the VisitData
+ // objects it collects if the visits were filtered by transition type.
+ // RemoveVisitsFilter currently only supports filtering by transition
+ // type, so FindRemovableVisits will either find all visits, or all
+ // visits of a given type. Therefore, if transitionType is set on this
+ // visit, we pass the transition type to NotifyOnPageExpired which in
+ // turns passes it to OnDeleteVisits to indicate that all visits of a
+ // given type were removed.
+ uint32_t transition = visits[0].transitionType < UINT32_MAX
+ ? visits[0].transitionType
+ : 0;
+ navHistory->NotifyOnPageExpired(uri, visits[0].visitTime, removingPage,
+ visits[0].guid,
+ nsINavHistoryObserver::REASON_DELETED,
+ transition);
+ }
+ }
+ (void)navHistory->EndUpdateBatch();
+
+ return NS_OK;
+ }
+
+private:
+ nsTHashtable<PlaceHashKey> mPlaces;
+
+ /**
+ * Strong reference to the History object because we do not want it to
+ * disappear out from under us.
+ */
+ RefPtr<History> mHistory;
+};
+
+/**
+ * Remove visits from history.
+ */
+class RemoveVisits : public Runnable
+{
+public:
+ /**
+ * Asynchronously removes visits from history.
+ *
+ * @param aConnection
+ * The database connection to use for these operations.
+ * @param aFilter
+ * Filter to remove visits.
+ */
+ static nsresult Start(mozIStorageConnection* aConnection,
+ RemoveVisitsFilter& aFilter)
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ RefPtr<RemoveVisits> event = new RemoveVisits(aConnection, aFilter);
+
+ // Get the target thread, and then start the work!
+ nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
+ NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
+ nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "This should not be called on the main thread");
+
+ // Prevent the main thread from shutting down while this is running.
+ MutexAutoLock lockedScope(mHistory->GetShutdownMutex());
+ if (mHistory->IsShuttingDown()) {
+ // If we were already shutting down, we cannot remove the visits.
+ return NS_OK;
+ }
+
+ // Find all the visits relative to the current filters and whether their
+ // pages will be removed or not.
+ nsTHashtable<PlaceHashKey> places(VISITS_REMOVAL_INITIAL_HASH_LENGTH);
+ nsresult rv = FindRemovableVisits(places);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (places.Count() == 0)
+ return NS_OK;
+
+ mozStorageTransaction transaction(mDBConn, false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ rv = RemoveVisitsFromDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = RemovePagesFromDatabase(places);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIRunnable> event = new NotifyRemoveVisits(places);
+ rv = NS_DispatchToMainThread(event);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+private:
+ RemoveVisits(mozIStorageConnection* aConnection,
+ RemoveVisitsFilter& aFilter)
+ : mDBConn(aConnection)
+ , mHasTransitionType(false)
+ , mHistory(History::GetService())
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+ // Build query conditions.
+ nsTArray<nsCString> conditions;
+ // TODO: add support for binding params when adding further stuff here.
+ if (aFilter.transitionType < UINT32_MAX) {
+ conditions.AppendElement(nsPrintfCString("visit_type = %d", aFilter.transitionType));
+ mHasTransitionType = true;
+ }
+ if (conditions.Length() > 0) {
+ mWhereClause.AppendLiteral (" WHERE ");
+ for (uint32_t i = 0; i < conditions.Length(); ++i) {
+ if (i > 0)
+ mWhereClause.AppendLiteral(" AND ");
+ mWhereClause.Append(conditions[i]);
+ }
+ }
+ }
+
+ /**
+ * Find the list of entries that may be removed from `moz_places`.
+ *
+ * Calling this method makes sense only if we are not clearing the entire history.
+ */
+ nsresult
+ FindRemovableVisits(nsTHashtable<PlaceHashKey>& aPlaces)
+ {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "This should not be called on the main thread");
+
+ nsCString query("SELECT h.id, url, guid, visit_date, visit_type, "
+ "(SELECT count(*) FROM moz_historyvisits WHERE place_id = h.id) as full_visit_count, "
+ "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) as bookmarked "
+ "FROM moz_historyvisits "
+ "JOIN moz_places h ON place_id = h.id");
+ query.Append(mWhereClause);
+
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ bool hasResult;
+ nsresult rv;
+ while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) {
+ VisitData visit;
+ rv = stmt->GetInt64(0, &visit.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(1, visit.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(2, visit.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(3, &visit.visitTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mHasTransitionType) {
+ int32_t transition;
+ rv = stmt->GetInt32(4, &transition);
+ NS_ENSURE_SUCCESS(rv, rv);
+ visit.transitionType = static_cast<uint32_t>(transition);
+ }
+ int32_t visitCount, bookmarked;
+ rv = stmt->GetInt32(5, &visitCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(6, &bookmarked);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PlaceHashKey* entry = aPlaces.GetEntry(visit.spec);
+ if (!entry) {
+ entry = aPlaces.PutEntry(visit.spec);
+ }
+ entry->SetProperties(static_cast<uint32_t>(visitCount), static_cast<bool>(bookmarked));
+ entry->mVisits.AppendElement(visit);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ nsresult
+ RemoveVisitsFromDatabase()
+ {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "This should not be called on the main thread");
+
+ nsCString query("DELETE FROM moz_historyvisits");
+ query.Append(mWhereClause);
+
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ nsresult rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ nsresult
+ RemovePagesFromDatabase(nsTHashtable<PlaceHashKey>& aPlaces)
+ {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "This should not be called on the main thread");
+
+ nsCString placeIdsToRemove;
+ for (auto iter = aPlaces.Iter(); !iter.Done(); iter.Next()) {
+ PlaceHashKey* entry = iter.Get();
+ const nsTArray<VisitData>& visits = entry->mVisits;
+ // Only orphan ids should be listed.
+ if (visits.Length() == entry->VisitCount() && !entry->IsBookmarked()) {
+ if (!placeIdsToRemove.IsEmpty())
+ placeIdsToRemove.Append(',');
+ placeIdsToRemove.AppendInt(visits[0].placeId);
+ }
+ }
+
+#ifdef DEBUG
+ {
+ // Ensure that we are not removing any problematic entry.
+ nsCString query("SELECT id FROM moz_places h WHERE id IN (");
+ query.Append(placeIdsToRemove);
+ query.AppendLiteral(") AND ("
+ "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) OR "
+ "EXISTS(SELECT 1 FROM moz_historyvisits WHERE place_id = h.id) OR "
+ "SUBSTR(h.url, 1, 6) = 'place:' "
+ ")");
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ bool hasResult;
+ MOZ_ASSERT(NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && !hasResult,
+ "Trying to remove a non-oprhan place from the database");
+ }
+#endif
+
+ {
+ nsCString query("DELETE FROM moz_places "
+ "WHERE id IN (");
+ query.Append(placeIdsToRemove);
+ query.Append(')');
+
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ nsresult rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ {
+ // Hosts accumulated during the places delete are updated through a trigger
+ // (see nsPlacesTriggers.h).
+ nsAutoCString query("DELETE FROM moz_updatehosts_temp");
+ nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ nsresult rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ }
+
+ mozIStorageConnection* mDBConn;
+ bool mHasTransitionType;
+ nsCString mWhereClause;
+
+ /**
+ * Strong reference to the History object because we do not want it to
+ * disappear out from under us.
+ */
+ RefPtr<History> mHistory;
+};
+
+/**
+ * Stores an embed visit, and notifies observers.
+ *
+ * @param aPlace
+ * The VisitData of the visit to store as an embed visit.
+ * @param [optional] aCallback
+ * The mozIVisitInfoCallback to notify, if provided.
+ */
+void
+StoreAndNotifyEmbedVisit(VisitData& aPlace,
+ mozIVisitInfoCallback* aCallback = nullptr)
+{
+ MOZ_ASSERT(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED,
+ "Must only pass TRANSITION_EMBED visits to this!");
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!");
+
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aPlace.spec));
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ if (!navHistory || !uri) {
+ return;
+ }
+
+ navHistory->registerEmbedVisit(uri, aPlace.visitTime);
+
+ if (!!aCallback) {
+ nsMainThreadPtrHandle<mozIVisitInfoCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
+ nsCOMPtr<nsIRunnable> event =
+ new NotifyPlaceInfoCallback(callback, aPlace, true, NS_OK);
+ (void)NS_DispatchToMainThread(event);
+ }
+
+ nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace);
+ (void)NS_DispatchToMainThread(event);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+//// History
+
+History* History::gService = nullptr;
+
+History::History()
+ : mShuttingDown(false)
+ , mShutdownMutex("History::mShutdownMutex")
+ , mObservers(VISIT_OBSERVERS_INITIAL_CACHE_LENGTH)
+ , mRecentlyVisitedURIs(RECENTLY_VISITED_URIS_SIZE)
+{
+ NS_ASSERTION(!gService, "Ruh-roh! This service has already been created!");
+ gService = this;
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ NS_WARNING_ASSERTION(os, "Observer service was not found!");
+ if (os) {
+ (void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, false);
+ }
+}
+
+History::~History()
+{
+ UnregisterWeakMemoryReporter(this);
+
+ gService = nullptr;
+
+ NS_ASSERTION(mObservers.Count() == 0,
+ "Not all Links were removed before we disappear!");
+}
+
+void
+History::InitMemoryReporter()
+{
+ RegisterWeakMemoryReporter(this);
+}
+
+NS_IMETHODIMP
+History::NotifyVisited(nsIURI* aURI)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(aURI);
+
+ nsAutoScriptBlocker scriptBlocker;
+
+ if (XRE_IsParentProcess()) {
+ nsTArray<ContentParent*> cplist;
+ ContentParent::GetAll(cplist);
+
+ if (!cplist.IsEmpty()) {
+ URIParams uri;
+ SerializeURI(aURI, uri);
+ for (uint32_t i = 0; i < cplist.Length(); ++i) {
+ Unused << cplist[i]->SendNotifyVisited(uri);
+ }
+ }
+ }
+
+ // If we have no observers for this URI, we have nothing to notify about.
+ KeyClass* key = mObservers.GetEntry(aURI);
+ if (!key) {
+ return NS_OK;
+ }
+
+ // Update status of each Link node.
+ {
+ // RemoveEntry will destroy the array, this iterator should not survive it.
+ ObserverArray::ForwardIterator iter(key->array);
+ while (iter.HasMore()) {
+ Link* link = iter.GetNext();
+ link->SetLinkState(eLinkState_Visited);
+ // Verify that the observers hash doesn't mutate while looping through
+ // the links associated with this URI.
+ MOZ_ASSERT(key == mObservers.GetEntry(aURI),
+ "The URIs hash mutated!");
+ }
+ }
+
+ // All the registered nodes can now be removed for this URI.
+ mObservers.RemoveEntry(key);
+ return NS_OK;
+}
+
+class ConcurrentStatementsHolder final : public mozIStorageCompletionCallback {
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit ConcurrentStatementsHolder(mozIStorageConnection* aDBConn)
+ {
+ DebugOnly<nsresult> rv = aDBConn->AsyncClone(true, this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ NS_IMETHOD Complete(nsresult aStatus, nsISupports* aConnection) override {
+ if (NS_FAILED(aStatus))
+ return NS_OK;
+ mReadOnlyDBConn = do_QueryInterface(aConnection);
+
+ // Now we can create our cached statements.
+
+ if (!mIsVisitedStatement) {
+ (void)mReadOnlyDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT 1 FROM moz_places h "
+ "WHERE url_hash = hash(?1) AND url = ?1 AND last_visit_date NOTNULL "
+ ), getter_AddRefs(mIsVisitedStatement));
+ MOZ_ASSERT(mIsVisitedStatement);
+ nsresult result = mIsVisitedStatement ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ for (int32_t i = 0; i < mIsVisitedCallbacks.Count(); ++i) {
+ DebugOnly<nsresult> rv;
+ rv = mIsVisitedCallbacks[i]->Complete(result, mIsVisitedStatement);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ mIsVisitedCallbacks.Clear();
+ }
+
+ return NS_OK;
+ }
+
+ void GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback)
+ {
+ if (mIsVisitedStatement) {
+ DebugOnly<nsresult> rv;
+ rv = aCallback->Complete(NS_OK, mIsVisitedStatement);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ } else {
+ DebugOnly<bool> added = mIsVisitedCallbacks.AppendObject(aCallback);
+ MOZ_ASSERT(added);
+ }
+ }
+
+ void Shutdown() {
+ if (mReadOnlyDBConn) {
+ mIsVisitedCallbacks.Clear();
+ DebugOnly<nsresult> rv;
+ if (mIsVisitedStatement) {
+ rv = mIsVisitedStatement->Finalize();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ rv = mReadOnlyDBConn->AsyncClose(nullptr);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+private:
+ ~ConcurrentStatementsHolder()
+ {
+ }
+
+ nsCOMPtr<mozIStorageAsyncConnection> mReadOnlyDBConn;
+ nsCOMPtr<mozIStorageAsyncStatement> mIsVisitedStatement;
+ nsCOMArray<mozIStorageCompletionCallback> mIsVisitedCallbacks;
+};
+
+NS_IMPL_ISUPPORTS(
+ ConcurrentStatementsHolder
+, mozIStorageCompletionCallback
+)
+
+nsresult
+History::GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mShuttingDown)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ if (!mConcurrentStatementsHolder) {
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+ mConcurrentStatementsHolder = new ConcurrentStatementsHolder(dbConn);
+ }
+ mConcurrentStatementsHolder->GetIsVisitedStatement(aCallback);
+ return NS_OK;
+}
+
+nsresult
+History::InsertPlace(VisitData& aPlace)
+{
+ MOZ_ASSERT(aPlace.placeId == 0, "should not have a valid place id!");
+ MOZ_ASSERT(!aPlace.shouldUpdateHidden, "We should not need to update hidden");
+ MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
+
+ nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
+ "INSERT INTO moz_places "
+ "(url, url_hash, title, rev_host, hidden, typed, frecency, guid) "
+ "VALUES (:url, hash(:url), :title, :rev_host, :hidden, :typed, :frecency, :guid) "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("rev_host"),
+ aPlace.revHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), aPlace.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString title = aPlace.title;
+ // Empty strings should have no title, just like nsNavHistory::SetPageTitle.
+ if (title.IsEmpty()) {
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("title"));
+ }
+ else {
+ title.Assign(StringHead(aPlace.title, TITLE_LENGTH_MAX));
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"), title);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // When inserting a page for a first visit that should not appear in
+ // autocomplete, for example an error page, use a zero frecency.
+ int32_t frecency = aPlace.shouldUpdateFrecency ? aPlace.frecency : 0;
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("frecency"), frecency);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aPlace.guid.IsVoid()) {
+ rv = GenerateGUID(aPlace.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aPlace.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Post an onFrecencyChanged observer notification.
+ const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
+ NS_ENSURE_STATE(navHistory);
+ navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency,
+ aPlace.guid,
+ aPlace.hidden,
+ aPlace.visitTime);
+
+ return NS_OK;
+}
+
+nsresult
+History::UpdatePlace(const VisitData& aPlace)
+{
+ MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
+ MOZ_ASSERT(aPlace.placeId > 0, "must have a valid place id!");
+ MOZ_ASSERT(!aPlace.guid.IsVoid(), "must have a guid!");
+
+ nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
+ "UPDATE moz_places "
+ "SET title = :title, "
+ "hidden = :hidden, "
+ "typed = :typed, "
+ "guid = :guid "
+ "WHERE id = :page_id "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv;
+ // Empty strings should clear the title, just like nsNavHistory::SetPageTitle.
+ if (aPlace.title.IsEmpty()) {
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("title"));
+ }
+ else {
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"),
+ StringHead(aPlace.title, TITLE_LENGTH_MAX));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aPlace.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"),
+ aPlace.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+History::FetchPageInfo(VisitData& _place, bool* _exists)
+{
+ MOZ_ASSERT(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!");
+ MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
+
+ nsresult rv;
+
+ // URI takes precedence.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ bool selectByURI = !_place.spec.IsEmpty();
+ if (selectByURI) {
+ stmt = GetStatement(
+ "SELECT guid, id, title, hidden, typed, frecency, visit_count, last_visit_date, "
+ "(SELECT id FROM moz_historyvisits "
+ "WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id "
+ "FROM moz_places h "
+ "WHERE url_hash = hash(:page_url) AND url = :page_url "
+ );
+ NS_ENSURE_STATE(stmt);
+
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ stmt = GetStatement(
+ "SELECT url, id, title, hidden, typed, frecency, visit_count, last_visit_date, "
+ "(SELECT id FROM moz_historyvisits "
+ "WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id "
+ "FROM moz_places h "
+ "WHERE guid = :guid "
+ );
+ NS_ENSURE_STATE(stmt);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _place.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->ExecuteStep(_exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!*_exists) {
+ return NS_OK;
+ }
+
+ if (selectByURI) {
+ if (_place.guid.IsEmpty()) {
+ rv = stmt->GetUTF8String(0, _place.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ else {
+ nsAutoCString spec;
+ rv = stmt->GetUTF8String(0, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ _place.spec = spec;
+ }
+
+ rv = stmt->GetInt64(1, &_place.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString title;
+ rv = stmt->GetString(2, title);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If the title we were given was void, that means we did not bother to set
+ // it to anything. As a result, ignore the fact that we may have changed the
+ // title (because we don't want to, that would be empty), and set the title
+ // to what is currently stored in the datbase.
+ if (_place.title.IsVoid()) {
+ _place.title = title;
+ }
+ // Otherwise, just indicate if the title has changed.
+ else {
+ _place.titleChanged = !(_place.title.Equals(title) ||
+ (_place.title.IsEmpty() && title.IsVoid()));
+ }
+
+ int32_t hidden;
+ rv = stmt->GetInt32(3, &hidden);
+ NS_ENSURE_SUCCESS(rv, rv);
+ _place.hidden = !!hidden;
+
+ int32_t typed;
+ rv = stmt->GetInt32(4, &typed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ _place.typed = !!typed;
+
+ rv = stmt->GetInt32(5, &_place.frecency);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t visitCount;
+ rv = stmt->GetInt32(6, &visitCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ _place.visitCount = visitCount;
+ rv = stmt->GetInt64(7, &_place.lastVisitTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(8, &_place.lastVisitId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(HistoryMallocSizeOf)
+
+NS_IMETHODIMP
+History::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize)
+{
+ MOZ_COLLECT_REPORT(
+ "explicit/history-links-hashtable", KIND_HEAP, UNITS_BYTES,
+ SizeOfIncludingThis(HistoryMallocSizeOf),
+ "Memory used by the hashtable that records changes to the visited state "
+ "of links.");
+
+ return NS_OK;
+}
+
+size_t
+History::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOfThis)
+{
+ return aMallocSizeOfThis(this) +
+ mObservers.SizeOfExcludingThis(aMallocSizeOfThis);
+}
+
+/* static */
+History*
+History::GetService()
+{
+ if (gService) {
+ return gService;
+ }
+
+ nsCOMPtr<IHistory> service(do_GetService(NS_IHISTORY_CONTRACTID));
+ MOZ_ASSERT(service, "Cannot obtain IHistory service!");
+ NS_ASSERTION(gService, "Our constructor was not run?!");
+
+ return gService;
+}
+
+/* static */
+History*
+History::GetSingleton()
+{
+ if (!gService) {
+ gService = new History();
+ NS_ENSURE_TRUE(gService, nullptr);
+ gService->InitMemoryReporter();
+ }
+
+ NS_ADDREF(gService);
+ return gService;
+}
+
+mozIStorageConnection*
+History::GetDBConn()
+{
+ if (mShuttingDown)
+ return nullptr;
+ if (!mDB) {
+ mDB = Database::GetDatabase();
+ NS_ENSURE_TRUE(mDB, nullptr);
+ }
+ return mDB->MainConn();
+}
+
+void
+History::Shutdown()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Prevent other threads from scheduling uses of the DB while we mark
+ // ourselves as shutting down.
+ MutexAutoLock lockedScope(mShutdownMutex);
+ MOZ_ASSERT(!mShuttingDown && "Shutdown was called more than once!");
+
+ mShuttingDown = true;
+
+ if (mConcurrentStatementsHolder) {
+ mConcurrentStatementsHolder->Shutdown();
+ }
+}
+
+void
+History::AppendToRecentlyVisitedURIs(nsIURI* aURI) {
+ // Add a new entry, if necessary.
+ RecentURIKey* entry = mRecentlyVisitedURIs.GetEntry(aURI);
+ if (!entry) {
+ entry = mRecentlyVisitedURIs.PutEntry(aURI);
+ }
+ if (entry) {
+ entry->time = PR_Now();
+ }
+
+ // Remove entries older than RECENTLY_VISITED_URIS_MAX_AGE.
+ for (auto iter = mRecentlyVisitedURIs.Iter(); !iter.Done(); iter.Next()) {
+ RecentURIKey* entry = iter.Get();
+ if ((PR_Now() - entry->time) > RECENTLY_VISITED_URIS_MAX_AGE) {
+ iter.Remove();
+ }
+ }
+}
+
+inline bool
+History::IsRecentlyVisitedURI(nsIURI* aURI) {
+ RecentURIKey* entry = mRecentlyVisitedURIs.GetEntry(aURI);
+ // Check if the entry exists and is younger than RECENTLY_VISITED_URIS_MAX_AGE.
+ return entry && (PR_Now() - entry->time) < RECENTLY_VISITED_URIS_MAX_AGE;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// IHistory
+
+NS_IMETHODIMP
+History::VisitURI(nsIURI* aURI,
+ nsIURI* aLastVisitedURI,
+ uint32_t aFlags)
+{
+ NS_ENSURE_ARG(aURI);
+
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess()) {
+ URIParams uri;
+ SerializeURI(aURI, uri);
+
+ OptionalURIParams lastVisitedURI;
+ SerializeURI(aLastVisitedURI, lastVisitedURI);
+
+ mozilla::dom::ContentChild* cpc =
+ mozilla::dom::ContentChild::GetSingleton();
+ NS_ASSERTION(cpc, "Content Protocol is NULL!");
+ (void)cpc->SendVisitURI(uri, lastVisitedURI, aFlags);
+ return NS_OK;
+ }
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
+
+ // Silently return if URI is something we shouldn't add to DB.
+ bool canAdd;
+ nsresult rv = navHistory->CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ // Do not save a reloaded uri if we have visited the same URI recently.
+ bool reload = false;
+ if (aLastVisitedURI) {
+ rv = aURI->Equals(aLastVisitedURI, &reload);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (reload && IsRecentlyVisitedURI(aURI)) {
+ // Regardless we must update the stored visit time.
+ AppendToRecentlyVisitedURIs(aURI);
+ return NS_OK;
+ }
+ }
+
+ nsTArray<VisitData> placeArray(1);
+ NS_ENSURE_TRUE(placeArray.AppendElement(VisitData(aURI, aLastVisitedURI)),
+ NS_ERROR_OUT_OF_MEMORY);
+ VisitData& place = placeArray.ElementAt(0);
+ NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG);
+
+ place.visitTime = PR_Now();
+
+ // Assigns a type to the edge in the visit linked list. Each type will be
+ // considered differently when weighting the frecency of a location.
+ uint32_t recentFlags = navHistory->GetRecentFlags(aURI);
+ bool isFollowedLink = recentFlags & nsNavHistory::RECENT_ACTIVATED;
+
+ // Embed visits should never be added to the database, and the same is valid
+ // for redirects across frames.
+ // For the above reasoning non-toplevel transitions are handled at first.
+ // if the visit is toplevel or a non-toplevel followed link, then it can be
+ // handled as usual and stored on disk.
+
+ uint32_t transitionType = nsINavHistoryService::TRANSITION_LINK;
+
+ if (!(aFlags & IHistory::TOP_LEVEL) && !isFollowedLink) {
+ // A frame redirected to a new site without user interaction.
+ transitionType = nsINavHistoryService::TRANSITION_EMBED;
+ }
+ else if (aFlags & IHistory::REDIRECT_TEMPORARY) {
+ transitionType = nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY;
+ }
+ else if (aFlags & IHistory::REDIRECT_PERMANENT) {
+ transitionType = nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT;
+ }
+ else if (reload) {
+ transitionType = nsINavHistoryService::TRANSITION_RELOAD;
+ }
+ else if ((recentFlags & nsNavHistory::RECENT_TYPED) &&
+ !(aFlags & IHistory::UNRECOVERABLE_ERROR)) {
+ // Don't mark error pages as typed, even if they were actually typed by
+ // the user. This is useful to limit their score in autocomplete.
+ transitionType = nsINavHistoryService::TRANSITION_TYPED;
+ }
+ else if (recentFlags & nsNavHistory::RECENT_BOOKMARKED) {
+ transitionType = nsINavHistoryService::TRANSITION_BOOKMARK;
+ }
+ else if (!(aFlags & IHistory::TOP_LEVEL) && isFollowedLink) {
+ // User activated a link in a frame.
+ transitionType = nsINavHistoryService::TRANSITION_FRAMED_LINK;
+ }
+
+ place.SetTransitionType(transitionType);
+ place.hidden = GetHiddenState(aFlags & IHistory::REDIRECT_SOURCE,
+ transitionType);
+
+ // Error pages should never be autocompleted.
+ if (aFlags & IHistory::UNRECOVERABLE_ERROR) {
+ place.shouldUpdateFrecency = false;
+ }
+
+ // EMBED visits are session-persistent and should not go through the database.
+ // They exist only to keep track of isVisited status during the session.
+ if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) {
+ StoreAndNotifyEmbedVisit(place);
+ }
+ else {
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+
+ rv = InsertVisitedURIs::Start(dbConn, placeArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Finally, notify that we've been visited.
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+History::RegisterVisitedCallback(nsIURI* aURI,
+ Link* aLink)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ASSERTION(aURI, "Must pass a non-null URI!");
+ if (XRE_IsContentProcess()) {
+ NS_PRECONDITION(aLink, "Must pass a non-null Link!");
+ }
+
+ // Obtain our array of observers for this URI.
+#ifdef DEBUG
+ bool keyAlreadyExists = !!mObservers.GetEntry(aURI);
+#endif
+ KeyClass* key = mObservers.PutEntry(aURI);
+ NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY);
+ ObserverArray& observers = key->array;
+
+ if (observers.IsEmpty()) {
+ NS_ASSERTION(!keyAlreadyExists,
+ "An empty key was kept around in our hashtable!");
+
+ // We are the first Link node to ask about this URI, or there are no pending
+ // Links wanting to know about this URI. Therefore, we should query the
+ // database now.
+ nsresult rv = VisitedQuery::Start(aURI);
+
+ // In IPC builds, we are passed a nullptr Link from
+ // ContentParent::RecvStartVisitedQuery. Since we won't be adding a
+ // nullptr entry to our list of observers, and the code after this point
+ // assumes that aLink is non-nullptr, we will need to return now.
+ if (NS_FAILED(rv) || !aLink) {
+ // Remove our array from the hashtable so we don't keep it around.
+ // In some case calling RemoveEntry on the key obtained by PutEntry
+ // crashes for currently unknown reasons. Our suspect is that something
+ // between PutEntry and this call causes a nested loop that either removes
+ // the entry or reallocs the hash.
+ // TODO (Bug 1412647): we must figure the root cause for these issues and
+ // remove this stop-gap crash fix.
+ key = mObservers.GetEntry(aURI);
+ if (key) {
+ mObservers.RemoveEntry(key);
+ }
+ return rv;
+ }
+ }
+ // In IPC builds, we are passed a nullptr Link from
+ // ContentParent::RecvStartVisitedQuery. All of our code after this point
+ // assumes aLink is non-nullptr, so we have to return now.
+ else if (!aLink) {
+ NS_ASSERTION(XRE_IsParentProcess(),
+ "We should only ever get a null Link in the default process!");
+ return NS_OK;
+ }
+
+ // Sanity check that Links are not registered more than once for a given URI.
+ // This will not catch a case where it is registered for two different URIs.
+ NS_ASSERTION(!observers.Contains(aLink),
+ "Already tracking this Link object!");
+
+ // Start tracking our Link.
+ if (!observers.AppendElement(aLink)) {
+ // Curses - unregister and return failure.
+ (void)UnregisterVisitedCallback(aURI, aLink);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+History::UnregisterVisitedCallback(nsIURI* aURI,
+ Link* aLink)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // TODO: aURI is sometimes null - see bug 548685
+ NS_ASSERTION(aURI, "Must pass a non-null URI!");
+ NS_ASSERTION(aLink, "Must pass a non-null Link object!");
+
+ // Get the array, and remove the item from it.
+ KeyClass* key = mObservers.GetEntry(aURI);
+ if (!key) {
+ NS_ERROR("Trying to unregister for a URI that wasn't registered!");
+ return NS_ERROR_UNEXPECTED;
+ }
+ ObserverArray& observers = key->array;
+ if (!observers.RemoveElement(aLink)) {
+ NS_ERROR("Trying to unregister a node that wasn't registered!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // If the array is now empty, we should remove it from the hashtable.
+ if (observers.IsEmpty()) {
+ mObservers.RemoveEntry(aURI);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+History::SetURITitle(nsIURI* aURI, const nsAString& aTitle)
+{
+ NS_ENSURE_ARG(aURI);
+
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess()) {
+ URIParams uri;
+ SerializeURI(aURI, uri);
+
+ mozilla::dom::ContentChild * cpc =
+ mozilla::dom::ContentChild::GetSingleton();
+ NS_ASSERTION(cpc, "Content Protocol is NULL!");
+ (void)cpc->SendSetURITitle(uri, PromiseFlatString(aTitle));
+ return NS_OK;
+ }
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+
+ // At first, it seems like nav history should always be available here, no
+ // matter what.
+ //
+ // nsNavHistory fails to register as a service if there is no profile in
+ // place (for instance, if user is choosing a profile).
+ //
+ // Maybe the correct thing to do is to not register this service if no
+ // profile has been selected?
+ //
+ NS_ENSURE_TRUE(navHistory, NS_ERROR_FAILURE);
+
+ bool canAdd;
+ nsresult rv = navHistory->CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ // Embed visits don't have a database entry, thus don't set a title on them.
+ if (navHistory->hasEmbedVisit(aURI)) {
+ return NS_OK;
+ }
+
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+
+ rv = SetPageTitle::Start(dbConn, aURI, aTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIDownloadHistory
+
+NS_IMETHODIMP
+History::AddDownload(nsIURI* aSource, nsIURI* aReferrer,
+ PRTime aStartTime, nsIURI* aDestination)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(aSource);
+
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess()) {
+ NS_ERROR("Cannot add downloads to history from content process!");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
+
+ // Silently return if URI is something we shouldn't add to DB.
+ bool canAdd;
+ nsresult rv = navHistory->CanAddURI(aSource, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ nsTArray<VisitData> placeArray(1);
+ NS_ENSURE_TRUE(placeArray.AppendElement(VisitData(aSource, aReferrer)),
+ NS_ERROR_OUT_OF_MEMORY);
+ VisitData& place = placeArray.ElementAt(0);
+ NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG);
+
+ place.visitTime = aStartTime;
+ place.SetTransitionType(nsINavHistoryService::TRANSITION_DOWNLOAD);
+ place.hidden = false;
+
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+
+ nsMainThreadPtrHandle<mozIVisitInfoCallback> callback;
+ if (aDestination) {
+ callback = new nsMainThreadPtrHolder<mozIVisitInfoCallback>(new SetDownloadAnnotations(aDestination));
+ }
+
+ rv = InsertVisitedURIs::Start(dbConn, placeArray, callback);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally, notify that we've been visited.
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(aSource, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+History::RemoveAllDownloads()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess()) {
+ NS_ERROR("Cannot remove downloads to history from content process!");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Ensure navHistory is initialized.
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+
+ RemoveVisitsFilter filter;
+ filter.transitionType = nsINavHistoryService::TRANSITION_DOWNLOAD;
+
+ nsresult rv = RemoveVisits::Start(dbConn, filter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// mozIAsyncHistory
+
+NS_IMETHODIMP
+History::GetPlacesInfo(JS::Handle<JS::Value> aPlaceIdentifiers,
+ mozIVisitInfoCallback* aCallback,
+ JSContext* aCtx)
+{
+ // Make sure nsNavHistory service is up before proceeding:
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ MOZ_ASSERT(navHistory, "Could not get nsNavHistory?!");
+ if (!navHistory) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t placesIndentifiersLength;
+ JS::Rooted<JSObject*> placesIndentifiers(aCtx);
+ nsresult rv = GetJSArrayFromJSValue(aPlaceIdentifiers, aCtx,
+ &placesIndentifiers,
+ &placesIndentifiersLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<VisitData> placesInfo;
+ placesInfo.SetCapacity(placesIndentifiersLength);
+ for (uint32_t i = 0; i < placesIndentifiersLength; i++) {
+ JS::Rooted<JS::Value> placeIdentifier(aCtx);
+ bool rc = JS_GetElement(aCtx, placesIndentifiers, i, &placeIdentifier);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+
+ // GUID
+ nsAutoString fatGUID;
+ GetJSValueAsString(aCtx, placeIdentifier, fatGUID);
+ if (!fatGUID.IsVoid()) {
+ NS_ConvertUTF16toUTF8 guid(fatGUID);
+ if (!IsValidGUID(guid))
+ return NS_ERROR_INVALID_ARG;
+
+ VisitData& placeInfo = *placesInfo.AppendElement(VisitData());
+ placeInfo.guid = guid;
+ }
+ else {
+ nsCOMPtr<nsIURI> uri = GetJSValueAsURI(aCtx, placeIdentifier);
+ if (!uri)
+ return NS_ERROR_INVALID_ARG; // neither a guid, nor a uri.
+ placesInfo.AppendElement(VisitData(uri));
+ }
+ }
+
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+
+ for (nsTArray<VisitData>::size_type i = 0; i < placesInfo.Length(); i++) {
+ nsresult rv = GetPlaceInfo::Start(dbConn, placesInfo.ElementAt(i), aCallback);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Be sure to notify that all of our operations are complete. This
+ // is dispatched to the background thread first and redirected to the
+ // main thread from there to make sure that all database notifications
+ // and all embed or canAddURI notifications have finished.
+ if (aCallback) {
+ nsMainThreadPtrHandle<mozIVisitInfoCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
+ nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn);
+ NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIRunnable> event = new NotifyCompletion(callback);
+ return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+History::UpdatePlaces(JS::Handle<JS::Value> aPlaceInfos,
+ mozIVisitInfoCallback* aCallback,
+ JSContext* aCtx)
+{
+ NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
+ NS_ENSURE_TRUE(!aPlaceInfos.isPrimitive(), NS_ERROR_INVALID_ARG);
+
+ uint32_t infosLength;
+ JS::Rooted<JSObject*> infos(aCtx);
+ nsresult rv = GetJSArrayFromJSValue(aPlaceInfos, aCtx, &infos, &infosLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<VisitData> visitData;
+ for (uint32_t i = 0; i < infosLength; i++) {
+ JS::Rooted<JSObject*> info(aCtx);
+ nsresult rv = GetJSObjectFromArray(aCtx, infos, i, &info);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri = GetURIFromJSObject(aCtx, info, "uri");
+ nsCString guid;
+ {
+ nsString fatGUID;
+ GetStringFromJSObject(aCtx, info, "guid", fatGUID);
+ if (fatGUID.IsVoid()) {
+ guid.SetIsVoid(true);
+ }
+ else {
+ guid = NS_ConvertUTF16toUTF8(fatGUID);
+ }
+ }
+
+ // Make sure that any uri we are given can be added to history, and if not,
+ // skip it (CanAddURI will notify our callback for us).
+ if (uri && !CanAddURI(uri, guid, aCallback)) {
+ continue;
+ }
+
+ // We must have at least one of uri or guid.
+ NS_ENSURE_ARG(uri || !guid.IsVoid());
+
+ // If we were given a guid, make sure it is valid.
+ bool isValidGUID = IsValidGUID(guid);
+ NS_ENSURE_ARG(guid.IsVoid() || isValidGUID);
+
+ nsString title;
+ GetStringFromJSObject(aCtx, info, "title", title);
+
+ JS::Rooted<JSObject*> visits(aCtx, nullptr);
+ {
+ JS::Rooted<JS::Value> visitsVal(aCtx);
+ bool rc = JS_GetProperty(aCtx, info, "visits", &visitsVal);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+ if (!visitsVal.isPrimitive()) {
+ visits = visitsVal.toObjectOrNull();
+ bool isArray;
+ if (!JS_IsArrayObject(aCtx, visits, &isArray)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (!isArray) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ NS_ENSURE_ARG(visits);
+
+ uint32_t visitsLength = 0;
+ if (visits) {
+ (void)JS_GetArrayLength(aCtx, visits, &visitsLength);
+ }
+ NS_ENSURE_ARG(visitsLength > 0);
+
+ // Check each visit, and build our array of VisitData objects.
+ visitData.SetCapacity(visitData.Length() + visitsLength);
+ for (uint32_t j = 0; j < visitsLength; j++) {
+ JS::Rooted<JSObject*> visit(aCtx);
+ rv = GetJSObjectFromArray(aCtx, visits, j, &visit);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ VisitData& data = *visitData.AppendElement(VisitData(uri));
+ data.title = title;
+ data.guid = guid;
+
+ // We must have a date and a transaction type!
+ rv = GetIntFromJSObject(aCtx, visit, "visitDate", &data.visitTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t transitionType = 0;
+ rv = GetIntFromJSObject(aCtx, visit, "transitionType", &transitionType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_ARG_RANGE(transitionType,
+ nsINavHistoryService::TRANSITION_LINK,
+ nsINavHistoryService::TRANSITION_RELOAD);
+ data.SetTransitionType(transitionType);
+ data.hidden = GetHiddenState(false, transitionType);
+
+ // If the visit is an embed visit, we do not actually add it to the
+ // database.
+ if (transitionType == nsINavHistoryService::TRANSITION_EMBED) {
+ StoreAndNotifyEmbedVisit(data, aCallback);
+ visitData.RemoveElementAt(visitData.Length() - 1);
+ continue;
+ }
+
+ // The referrer is optional.
+ nsCOMPtr<nsIURI> referrer = GetURIFromJSObject(aCtx, visit,
+ "referrerURI");
+ if (referrer) {
+ (void)referrer->GetSpec(data.referrerSpec);
+ }
+ }
+ }
+
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_STATE(dbConn);
+
+ nsMainThreadPtrHandle<mozIVisitInfoCallback>
+ callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
+
+ // It is possible that all of the visits we were passed were dissallowed by
+ // CanAddURI, which isn't an error. If we have no visits to add, however,
+ // we should not call InsertVisitedURIs::Start.
+ if (visitData.Length()) {
+ nsresult rv = InsertVisitedURIs::Start(dbConn, visitData, callback);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Be sure to notify that all of our operations are complete. This
+ // is dispatched to the background thread first and redirected to the
+ // main thread from there to make sure that all database notifications
+ // and all embed or canAddURI notifications have finished.
+ if (aCallback) {
+ nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn);
+ NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIRunnable> event = new NotifyCompletion(callback);
+ return backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+History::IsURIVisited(nsIURI* aURI,
+ mozIVisitedStatusCallback* aCallback)
+{
+ NS_ENSURE_STATE(NS_IsMainThread());
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG(aCallback);
+
+ nsresult rv = VisitedQuery::Start(aURI, aCallback);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+History::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
+ Shutdown();
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ (void)os->RemoveObserver(this, TOPIC_PLACES_SHUTDOWN);
+ }
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsISupports
+
+NS_IMPL_ISUPPORTS(
+ History
+, IHistory
+, nsIDownloadHistory
+, mozIAsyncHistory
+, nsIObserver
+, nsIMemoryReporter
+)
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/History.h b/toolkit/components/places/History.h
new file mode 100644
index 0000000000..16ae2b5de2
--- /dev/null
+++ b/toolkit/components/places/History.h
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla_places_History_h_
+#define mozilla_places_History_h_
+
+#include "mozilla/IHistory.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+#include "mozIAsyncHistory.h"
+#include "nsIDownloadHistory.h"
+#include "Database.h"
+
+#include "mozilla/dom/Link.h"
+#include "nsTHashtable.h"
+#include "nsString.h"
+#include "nsURIHashKey.h"
+#include "nsTObserverArray.h"
+#include "nsDeque.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserver.h"
+#include "mozIStorageConnection.h"
+
+namespace mozilla {
+namespace places {
+
+struct VisitData;
+class ConcurrentStatementsHolder;
+
+#define NS_HISTORYSERVICE_CID \
+ {0x0937a705, 0x91a6, 0x417a, {0x82, 0x92, 0xb2, 0x2e, 0xb1, 0x0d, 0xa8, 0x6c}}
+
+// Initial size of mRecentlyVisitedURIs.
+#define RECENTLY_VISITED_URIS_SIZE 64
+// Microseconds after which a visit can be expired from mRecentlyVisitedURIs.
+// When an URI is reloaded we only take into account the first visit to it, and
+// ignore any subsequent visits, if they happen before this time has elapsed.
+// A commonly found case is to reload a page every 5 minutes, so we pick a time
+// larger than that.
+#define RECENTLY_VISITED_URIS_MAX_AGE 6 * 60 * PR_USEC_PER_SEC
+
+class History final : public IHistory
+ , public nsIDownloadHistory
+ , public mozIAsyncHistory
+ , public nsIObserver
+ , public nsIMemoryReporter
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_IHISTORY
+ NS_DECL_NSIDOWNLOADHISTORY
+ NS_DECL_MOZIASYNCHISTORY
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMEMORYREPORTER
+
+ History();
+
+ /**
+ * Obtains the statement to use to check if a URI is visited or not.
+ */
+ nsresult GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback);
+
+ /**
+ * Adds an entry in moz_places with the data in aVisitData.
+ *
+ * @param aVisitData
+ * The visit data to use to populate a new row in moz_places.
+ */
+ nsresult InsertPlace(VisitData& aVisitData);
+
+ /**
+ * Updates an entry in moz_places with the data in aVisitData.
+ *
+ * @param aVisitData
+ * The visit data to use to update the existing row in moz_places.
+ */
+ nsresult UpdatePlace(const VisitData& aVisitData);
+
+ /**
+ * Loads information about the page into _place from moz_places.
+ *
+ * @param _place
+ * The VisitData for the place we need to know information about.
+ * @param [out] _exists
+ * Whether or the page was recorded in moz_places, false otherwise.
+ */
+ nsresult FetchPageInfo(VisitData& _place, bool* _exists);
+
+ /**
+ * Get the number of bytes of memory this History object is using,
+ * including sizeof(*this))
+ */
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ /**
+ * Obtains a pointer to this service.
+ */
+ static History* GetService();
+
+ /**
+ * Obtains a pointer that has had AddRef called on it. Used by the service
+ * manager only.
+ */
+ static History* GetSingleton();
+
+ template<int N>
+ already_AddRefed<mozIStorageStatement>
+ GetStatement(const char (&aQuery)[N])
+ {
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_TRUE(dbConn, nullptr);
+ return mDB->GetStatement(aQuery);
+ }
+
+ already_AddRefed<mozIStorageStatement>
+ GetStatement(const nsACString& aQuery)
+ {
+ mozIStorageConnection* dbConn = GetDBConn();
+ NS_ENSURE_TRUE(dbConn, nullptr);
+ return mDB->GetStatement(aQuery);
+ }
+
+ bool IsShuttingDown() const {
+ return mShuttingDown;
+ }
+ Mutex& GetShutdownMutex() {
+ return mShutdownMutex;
+ }
+
+ /**
+ * Helper function to append a new URI to mRecentlyVisitedURIs. See
+ * mRecentlyVisitedURIs.
+ */
+ void AppendToRecentlyVisitedURIs(nsIURI* aURI);
+
+private:
+ virtual ~History();
+
+ void InitMemoryReporter();
+
+ /**
+ * Obtains a read-write database connection.
+ */
+ mozIStorageConnection* GetDBConn();
+
+ /**
+ * The database handle. This is initialized lazily by the first call to
+ * GetDBConn(), so never use it directly, or, if you really need, always
+ * invoke GetDBConn() before.
+ */
+ RefPtr<mozilla::places::Database> mDB;
+
+ RefPtr<ConcurrentStatementsHolder> mConcurrentStatementsHolder;
+
+ /**
+ * Remove any memory references to tasks and do not take on any more.
+ */
+ void Shutdown();
+
+ static History* gService;
+
+ // Ensures new tasks aren't started on destruction.
+ bool mShuttingDown;
+ // This mutex guards mShuttingDown. Code running in other threads that might
+ // schedule tasks that use the database should grab it and check the value of
+ // mShuttingDown. If we are already shutting down, the code must gracefully
+ // avoid using the db. If we are not, the lock will prevent shutdown from
+ // starting in an unexpected moment.
+ Mutex mShutdownMutex;
+
+ typedef nsTObserverArray<mozilla::dom::Link* > ObserverArray;
+
+ class KeyClass : public nsURIHashKey
+ {
+ public:
+ explicit KeyClass(const nsIURI* aURI)
+ : nsURIHashKey(aURI)
+ {
+ }
+ KeyClass(const KeyClass& aOther)
+ : nsURIHashKey(aOther)
+ {
+ NS_NOTREACHED("Do not call me!");
+ }
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+ {
+ return array.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ ObserverArray array;
+ };
+
+ nsTHashtable<KeyClass> mObservers;
+
+ /**
+ * mRecentlyVisitedURIs remembers URIs which have been recently added to
+ * history, to avoid saving these locations repeatedly in a short period.
+ */
+ class RecentURIKey : public nsURIHashKey
+ {
+ public:
+ explicit RecentURIKey(const nsIURI* aURI) : nsURIHashKey(aURI)
+ {
+ }
+ RecentURIKey(const RecentURIKey& aOther) : nsURIHashKey(aOther)
+ {
+ NS_NOTREACHED("Do not call me!");
+ }
+ MOZ_INIT_OUTSIDE_CTOR PRTime time;
+ };
+ nsTHashtable<RecentURIKey> mRecentlyVisitedURIs;
+ /**
+ * Whether aURI has been visited "recently".
+ * See RECENTLY_VISITED_URIS_MAX_AGE.
+ */
+ bool IsRecentlyVisitedURI(nsIURI* aURI);
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_History_h_
diff --git a/toolkit/components/places/History.jsm b/toolkit/components/places/History.jsm
new file mode 100644
index 0000000000..59c24fcc6f
--- /dev/null
+++ b/toolkit/components/places/History.jsm
@@ -0,0 +1,1049 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+/**
+ * Asynchronous API for managing history.
+ *
+ *
+ * The API makes use of `PageInfo` and `VisitInfo` objects, defined as follows.
+ *
+ * A `PageInfo` object is any object that contains A SUBSET of the
+ * following properties:
+ * - guid: (string)
+ * The globally unique id of the page.
+ * - url: (URL)
+ * or (nsIURI)
+ * or (string)
+ * The full URI of the page. Note that `PageInfo` values passed as
+ * argument may hold `nsIURI` or `string` values for property `url`,
+ * but `PageInfo` objects returned by this module always hold `URL`
+ * values.
+ * - title: (string)
+ * The title associated with the page, if any.
+ * - frecency: (number)
+ * The frecency of the page, if any.
+ * See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Frecency_algorithm
+ * Note that this property may not be used to change the actualy frecency
+ * score of a page, only to retrieve it. In other words, any `frecency` field
+ * passed as argument to a function of this API will be ignored.
+ * - visits: (Array<VisitInfo>)
+ * All the visits for this page, if any.
+ *
+ * See the documentation of individual methods to find out which properties
+ * are required for `PageInfo` arguments or returned for `PageInfo` results.
+ *
+ * A `VisitInfo` object is any object that contains A SUBSET of the following
+ * properties:
+ * - date: (Date)
+ * The time the visit occurred.
+ * - transition: (number)
+ * How the user reached the page. See constants `TRANSITIONS.*`
+ * for the possible transition types.
+ * - referrer: (URL)
+ * or (nsIURI)
+ * or (string)
+ * The referring URI of this visit. Note that `VisitInfo` passed
+ * as argument may hold `nsIURI` or `string` values for property `referrer`,
+ * but `VisitInfo` objects returned by this module always hold `URL`
+ * values.
+ * See the documentation of individual methods to find out which properties
+ * are required for `VisitInfo` arguments or returned for `VisitInfo` results.
+ *
+ *
+ *
+ * Each successful operation notifies through the nsINavHistoryObserver
+ * interface. To listen to such notifications you must register using
+ * nsINavHistoryService `addObserver` and `removeObserver` methods.
+ * @see nsINavHistoryObserver
+ */
+
+this.EXPORTED_SYMBOLS = [ "History" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+Cu.importGlobalProperties(["URL"]);
+
+/**
+ * Whenever we update or remove numerous pages, it is preferable
+ * to yield time to the main thread every so often to avoid janking.
+ * These constants determine the maximal number of notifications we
+ * may emit before we yield.
+ */
+const NOTIFICATION_CHUNK_SIZE = 300;
+const ONRESULT_CHUNK_SIZE = 300;
+
+// Timers resolution is not always good, it can have a 16ms precision on Win.
+const TIMERS_RESOLUTION_SKEW_MS = 16;
+
+/**
+ * Sends a bookmarks notification through the given observers.
+ *
+ * @param observers
+ * array of nsINavBookmarkObserver objects.
+ * @param notification
+ * the notification name.
+ * @param args
+ * array of arguments to pass to the notification.
+ */
+function notify(observers, notification, args = []) {
+ for (let observer of observers) {
+ try {
+ observer[notification](...args);
+ } catch (ex) {}
+ }
+}
+
+this.History = Object.freeze({
+ /**
+ * Fetch the available information for one page.
+ *
+ * @param guidOrURI: (URL or nsIURI)
+ * The full URI of the page.
+ * or (string)
+ * Either the full URI of the page or the GUID of the page.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ * @resolves (PageInfo | null) If the page could be found, the information
+ * on that page. Note that this `PageInfo` does NOT contain the visit
+ * data (i.e. `visits` is `undefined`).
+ *
+ * @throws (Error)
+ * If `guidOrURI` does not have the expected type or if it is a string
+ * that may be parsed neither as a valid URL nor as a valid GUID.
+ */
+ fetch: function (guidOrURI) {
+ throw new Error("Method not implemented");
+ },
+
+ /**
+ * Adds a number of visits for a single page.
+ *
+ * Any change may be observed through nsINavHistoryObserver
+ *
+ * @param pageInfo: (PageInfo)
+ * Information on a page. This `PageInfo` MUST contain
+ * - a property `url`, as specified by the definition of `PageInfo`.
+ * - a property `visits`, as specified by the definition of
+ * `PageInfo`, which MUST contain at least one visit.
+ * If a property `title` is provided, the title of the page
+ * is updated.
+ * If the `date` of a visit is not provided, it defaults
+ * to now.
+ * If the `transition` of a visit is not provided, it defaults to
+ * TRANSITION_LINK.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ * @resolves (PageInfo)
+ * A PageInfo object populated with data after the insert is complete.
+ * @rejects (Error)
+ * Rejects if the insert was unsuccessful.
+ *
+ * @throws (Error)
+ * If the `url` specified was for a protocol that should not be
+ * stored (e.g. "chrome:", "mailbox:", "about:", "imap:", "news:",
+ * "moz-anno:", "view-source:", "resource:", "data:", "wyciwyg:",
+ * "javascript:", "blob:").
+ * @throws (Error)
+ * If `pageInfo` has an unexpected type.
+ * @throws (Error)
+ * If `pageInfo` does not have a `url`.
+ * @throws (Error)
+ * If `pageInfo` does not have a `visits` property or if the
+ * value of `visits` is ill-typed or is an empty array.
+ * @throws (Error)
+ * If an element of `visits` has an invalid `date`.
+ * @throws (Error)
+ * If an element of `visits` has an invalid `transition`.
+ */
+ insert: function (pageInfo) {
+ if (typeof pageInfo != "object" || !pageInfo) {
+ throw new TypeError("pageInfo must be an object");
+ }
+
+ let info = validatePageInfo(pageInfo);
+
+ return PlacesUtils.withConnectionWrapper("History.jsm: insert",
+ db => insert(db, info));
+ },
+
+ /**
+ * Adds a number of visits for a number of pages.
+ *
+ * Any change may be observed through nsINavHistoryObserver
+ *
+ * @param pageInfos: (Array<PageInfo>)
+ * Information on a page. This `PageInfo` MUST contain
+ * - a property `url`, as specified by the definition of `PageInfo`.
+ * - a property `visits`, as specified by the definition of
+ * `PageInfo`, which MUST contain at least one visit.
+ * If a property `title` is provided, the title of the page
+ * is updated.
+ * If the `date` of a visit is not provided, it defaults
+ * to now.
+ * If the `transition` of a visit is not provided, it defaults to
+ * TRANSITION_LINK.
+ * @param onResult: (function(PageInfo))
+ * A callback invoked for each page inserted.
+ * @param onError: (function(PageInfo))
+ * A callback invoked for each page which generated an error
+ * when an insert was attempted.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ * @resolves (null)
+ * @rejects (Error)
+ * Rejects if all of the inserts were unsuccessful.
+ *
+ * @throws (Error)
+ * If the `url` specified was for a protocol that should not be
+ * stored (e.g. "chrome:", "mailbox:", "about:", "imap:", "news:",
+ * "moz-anno:", "view-source:", "resource:", "data:", "wyciwyg:",
+ * "javascript:", "blob:").
+ * @throws (Error)
+ * If `pageInfos` has an unexpected type.
+ * @throws (Error)
+ * If a `pageInfo` does not have a `url`.
+ * @throws (Error)
+ * If a `PageInfo` does not have a `visits` property or if the
+ * value of `visits` is ill-typed or is an empty array.
+ * @throws (Error)
+ * If an element of `visits` has an invalid `date`.
+ * @throws (Error)
+ * If an element of `visits` has an invalid `transition`.
+ */
+ insertMany: function (pageInfos, onResult, onError) {
+ let infos = [];
+
+ if (!Array.isArray(pageInfos)) {
+ throw new TypeError("pageInfos must be an array");
+ }
+ if (!pageInfos.length) {
+ throw new TypeError("pageInfos may not be an empty array");
+ }
+
+ if (onResult && typeof onResult != "function") {
+ throw new TypeError(`onResult: ${onResult} is not a valid function`);
+ }
+ if (onError && typeof onError != "function") {
+ throw new TypeError(`onError: ${onError} is not a valid function`);
+ }
+
+ for (let pageInfo of pageInfos) {
+ let info = validatePageInfo(pageInfo);
+ infos.push(info);
+ }
+
+ return PlacesUtils.withConnectionWrapper("History.jsm: insertMany",
+ db => insertMany(db, infos, onResult, onError));
+ },
+
+ /**
+ * Remove pages from the database.
+ *
+ * Any change may be observed through nsINavHistoryObserver
+ *
+ *
+ * @param page: (URL or nsIURI)
+ * The full URI of the page.
+ * or (string)
+ * Either the full URI of the page or the GUID of the page.
+ * or (Array<URL|nsIURI|string>)
+ * An array of the above, to batch requests.
+ * @param onResult: (function(PageInfo))
+ * A callback invoked for each page found.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ * @resolve (bool)
+ * `true` if at least one page was removed, `false` otherwise.
+ * @throws (TypeError)
+ * If `pages` has an unexpected type or if a string provided
+ * is neither a valid GUID nor a valid URI or if `pages`
+ * is an empty array.
+ */
+ remove: function (pages, onResult = null) {
+ // Normalize and type-check arguments
+ if (Array.isArray(pages)) {
+ if (pages.length == 0) {
+ throw new TypeError("Expected at least one page");
+ }
+ } else {
+ pages = [pages];
+ }
+
+ let guids = [];
+ let urls = [];
+ for (let page of pages) {
+ // Normalize to URL or GUID, or throw if `page` cannot
+ // be normalized.
+ let normalized = normalizeToURLOrGUID(page);
+ if (typeof normalized === "string") {
+ guids.push(normalized);
+ } else {
+ urls.push(normalized.href);
+ }
+ }
+ let normalizedPages = {guids: guids, urls: urls};
+
+ // At this stage, we know that either `guids` is not-empty
+ // or `urls` is not-empty.
+
+ if (onResult && typeof onResult != "function") {
+ throw new TypeError("Invalid function: " + onResult);
+ }
+
+ return PlacesUtils.withConnectionWrapper("History.jsm: remove",
+ db => remove(db, normalizedPages, onResult));
+ },
+
+ /**
+ * Remove visits matching specific characteristics.
+ *
+ * Any change may be observed through nsINavHistoryObserver.
+ *
+ * @param filter: (object)
+ * The `object` may contain some of the following
+ * properties:
+ * - beginDate: (Date) Remove visits that have
+ * been added since this date (inclusive).
+ * - endDate: (Date) Remove visits that have
+ * been added before this date (inclusive).
+ * - limit: (Number) Limit the number of visits
+ * we remove to this number
+ * - url: (URL) Only remove visits to this URL
+ * If both `beginDate` and `endDate` are specified,
+ * visits between `beginDate` (inclusive) and `end`
+ * (inclusive) are removed.
+ *
+ * @param onResult: (function(VisitInfo), [optional])
+ * A callback invoked for each visit found and removed.
+ * Note that the referrer property of `VisitInfo`
+ * is NOT populated.
+ *
+ * @return (Promise)
+ * @resolve (bool)
+ * `true` if at least one visit was removed, `false`
+ * otherwise.
+ * @throws (TypeError)
+ * If `filter` does not have the expected type, in
+ * particular if the `object` is empty.
+ */
+ removeVisitsByFilter: function(filter, onResult = null) {
+ if (!filter || typeof filter != "object") {
+ throw new TypeError("Expected a filter");
+ }
+
+ let hasBeginDate = "beginDate" in filter;
+ let hasEndDate = "endDate" in filter;
+ let hasURL = "url" in filter;
+ let hasLimit = "limit" in filter;
+ if (hasBeginDate) {
+ ensureDate(filter.beginDate);
+ }
+ if (hasEndDate) {
+ ensureDate(filter.endDate);
+ }
+ if (hasBeginDate && hasEndDate && filter.beginDate > filter.endDate) {
+ throw new TypeError("`beginDate` should be at least as old as `endDate`");
+ }
+ if (!hasBeginDate && !hasEndDate && !hasURL && !hasLimit) {
+ throw new TypeError("Expected a non-empty filter");
+ }
+
+ if (hasURL && !(filter.url instanceof URL) && typeof filter.url != "string" &&
+ !(filter.url instanceof Ci.nsIURI)) {
+ throw new TypeError("Expected a valid URL for `url`");
+ }
+
+ if (hasLimit &&
+ (typeof filter.limit != "number" ||
+ filter.limit <= 0 ||
+ !Number.isInteger(filter.limit))) {
+ throw new TypeError("Expected a non-zero positive integer as a limit");
+ }
+
+ if (onResult && typeof onResult != "function") {
+ throw new TypeError("Invalid function: " + onResult);
+ }
+
+ return PlacesUtils.withConnectionWrapper("History.jsm: removeVisitsByFilter",
+ db => removeVisitsByFilter(db, filter, onResult)
+ );
+ },
+
+ /**
+ * Determine if a page has been visited.
+ *
+ * @param pages: (URL or nsIURI)
+ * The full URI of the page.
+ * or (string)
+ * The full URI of the page or the GUID of the page.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ * @resolve (bool)
+ * `true` if the page has been visited, `false` otherwise.
+ * @throws (Error)
+ * If `pages` has an unexpected type or if a string provided
+ * is neither not a valid GUID nor a valid URI.
+ */
+ hasVisits: function(page, onResult) {
+ throw new Error("Method not implemented");
+ },
+
+ /**
+ * Clear all history.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ */
+ clear() {
+ return PlacesUtils.withConnectionWrapper("History.jsm: clear",
+ clear
+ );
+ },
+
+ /**
+ * Possible values for the `transition` property of `VisitInfo`
+ * objects.
+ */
+
+ TRANSITIONS: {
+ /**
+ * The user followed a link and got a new toplevel window.
+ */
+ LINK: Ci.nsINavHistoryService.TRANSITION_LINK,
+
+ /**
+ * The user typed the page's URL in the URL bar or selected it from
+ * URL bar autocomplete results, clicked on it from a history query
+ * (from the History sidebar, History menu, or history query in the
+ * personal toolbar or Places organizer.
+ */
+ TYPED: Ci.nsINavHistoryService.TRANSITION_TYPED,
+
+ /**
+ * The user followed a bookmark to get to the page.
+ */
+ BOOKMARK: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+
+ /**
+ * Some inner content is loaded. This is true of all images on a
+ * page, and the contents of the iframe. It is also true of any
+ * content in a frame if the user did not explicitly follow a link
+ * to get there.
+ */
+ EMBED: Ci.nsINavHistoryService.TRANSITION_EMBED,
+
+ /**
+ * Set when the transition was a permanent redirect.
+ */
+ REDIRECT_PERMANENT: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+
+ /**
+ * Set when the transition was a temporary redirect.
+ */
+ REDIRECT_TEMPORARY: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
+
+ /**
+ * Set when the transition is a download.
+ */
+ DOWNLOAD: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+
+ /**
+ * The user followed a link and got a visit in a frame.
+ */
+ FRAMED_LINK: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
+
+ /**
+ * The user reloaded a page.
+ */
+ RELOAD: Ci.nsINavHistoryService.TRANSITION_RELOAD,
+ },
+});
+
+/**
+ * Validate an input PageInfo object, returning a valid PageInfo object.
+ *
+ * @param pageInfo: (PageInfo)
+ * @return (PageInfo)
+ */
+function validatePageInfo(pageInfo) {
+ let info = {
+ visits: [],
+ };
+
+ if (!pageInfo.url) {
+ throw new TypeError("PageInfo object must have a url property");
+ }
+
+ info.url = normalizeToURLOrGUID(pageInfo.url);
+
+ if (typeof pageInfo.title === "string") {
+ info.title = pageInfo.title;
+ } else if (pageInfo.title != null && pageInfo.title != undefined) {
+ throw new TypeError(`title property of PageInfo object: ${pageInfo.title} must be a string if provided`);
+ }
+
+ if (!pageInfo.visits || !Array.isArray(pageInfo.visits) || !pageInfo.visits.length) {
+ throw new TypeError("PageInfo object must have an array of visits");
+ }
+ for (let inVisit of pageInfo.visits) {
+ let visit = {
+ date: new Date(),
+ transition: inVisit.transition || History.TRANSITIONS.LINK,
+ };
+
+ if (!isValidTransitionType(visit.transition)) {
+ throw new TypeError(`transition: ${visit.transition} is not a valid transition type`);
+ }
+
+ if (inVisit.date) {
+ ensureDate(inVisit.date);
+ if (inVisit.date > (Date.now() + TIMERS_RESOLUTION_SKEW_MS)) {
+ throw new TypeError(`date: ${inVisit.date} cannot be a future date`);
+ }
+ visit.date = inVisit.date;
+ }
+
+ if (inVisit.referrer) {
+ visit.referrer = normalizeToURLOrGUID(inVisit.referrer);
+ }
+ info.visits.push(visit);
+ }
+ return info;
+}
+
+/**
+ * Convert a PageInfo object into the format expected by updatePlaces.
+ *
+ * Note: this assumes that the PageInfo object has already been validated
+ * via validatePageInfo.
+ *
+ * @param pageInfo: (PageInfo)
+ * @return (info)
+ */
+function convertForUpdatePlaces(pageInfo) {
+ let info = {
+ uri: PlacesUtils.toURI(pageInfo.url),
+ title: pageInfo.title,
+ visits: [],
+ };
+
+ for (let inVisit of pageInfo.visits) {
+ let visit = {
+ visitDate: PlacesUtils.toPRTime(inVisit.date),
+ transitionType: inVisit.transition,
+ referrerURI: (inVisit.referrer) ? PlacesUtils.toURI(inVisit.referrer) : undefined,
+ };
+ info.visits.push(visit);
+ }
+ return info;
+}
+
+/**
+ * Is a value a valid transition type?
+ *
+ * @param transitionType: (String)
+ * @return (Boolean)
+ */
+function isValidTransitionType(transitionType) {
+ return Object.values(History.TRANSITIONS).includes(transitionType);
+}
+
+/**
+ * Normalize a key to either a string (if it is a valid GUID) or an
+ * instance of `URL` (if it is a `URL`, `nsIURI`, or a string
+ * representing a valid url).
+ *
+ * @throws (TypeError)
+ * If the key is neither a valid guid nor a valid url.
+ */
+function normalizeToURLOrGUID(key) {
+ if (typeof key === "string") {
+ // A string may be a URL or a guid
+ if (PlacesUtils.isValidGuid(key)) {
+ return key;
+ }
+ return new URL(key);
+ }
+ if (key instanceof URL) {
+ return key;
+ }
+ if (key instanceof Ci.nsIURI) {
+ return new URL(key.spec);
+ }
+ throw new TypeError("Invalid url or guid: " + key);
+}
+
+/**
+ * Throw if an object is not a Date object.
+ */
+function ensureDate(arg) {
+ if (!arg || typeof arg != "object" || arg.constructor.name != "Date") {
+ throw new TypeError("Expected a Date, got " + arg);
+ }
+}
+
+/**
+ * Convert a list of strings or numbers to its SQL
+ * representation as a string.
+ */
+function sqlList(list) {
+ return list.map(JSON.stringify).join();
+}
+
+/**
+ * Invalidate and recompute the frecency of a list of pages,
+ * informing frecency observers.
+ *
+ * @param db: (Sqlite connection)
+ * @param idList: (Array)
+ * The `moz_places` identifiers for the places to invalidate.
+ * @return (Promise)
+ */
+var invalidateFrecencies = Task.async(function*(db, idList) {
+ if (idList.length == 0) {
+ return;
+ }
+ let ids = sqlList(idList);
+ yield db.execute(
+ `UPDATE moz_places
+ SET frecency = NOTIFY_FRECENCY(
+ CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date
+ ) WHERE id in (${ ids })`
+ );
+ yield db.execute(
+ `UPDATE moz_places
+ SET hidden = 0
+ WHERE id in (${ ids })
+ AND frecency <> 0`
+ );
+});
+
+// Inner implementation of History.clear().
+var clear = Task.async(function* (db) {
+ // Remove all history.
+ yield db.execute("DELETE FROM moz_historyvisits");
+
+ // Clear the registered embed visits.
+ PlacesUtils.history.clearEmbedVisits();
+
+ // Expiration will take care of orphans.
+ let observers = PlacesUtils.history.getObservers();
+ notify(observers, "onClearHistory");
+
+ // Invalidate frecencies for the remaining places. This must happen
+ // after the notification to ensure it runs enqueued to expiration.
+ yield db.execute(
+ `UPDATE moz_places SET frecency =
+ (CASE
+ WHEN url_hash BETWEEN hash("place", "prefix_lo") AND
+ hash("place", "prefix_hi")
+ THEN 0
+ ELSE -1
+ END)
+ WHERE frecency > 0`);
+
+ // Notify frecency change observers.
+ notify(observers, "onManyFrecenciesChanged");
+});
+
+/**
+ * Clean up pages whose history has been modified, by either
+ * removing them entirely (if they are marked for removal,
+ * typically because all visits have been removed and there
+ * are no more foreign keys such as bookmarks) or updating
+ * their frecency (otherwise).
+ *
+ * @param db: (Sqlite connection)
+ * The database.
+ * @param pages: (Array of objects)
+ * Pages that have been touched and that need cleaning up.
+ * Each object should have the following properties:
+ * - id: (number) The `moz_places` identifier for the place.
+ * - hasVisits: (boolean) If `true`, there remains at least one
+ * visit to this page, so the page should be kept and its
+ * frecency updated.
+ * - hasForeign: (boolean) If `true`, the page has at least
+ * one foreign reference (i.e. a bookmark), so the page should
+ * be kept and its frecency updated.
+ * @return (Promise)
+ */
+var cleanupPages = Task.async(function*(db, pages) {
+ yield invalidateFrecencies(db, pages.filter(p => p.hasForeign || p.hasVisits).map(p => p.id));
+
+ let pageIdsToRemove = pages.filter(p => !p.hasForeign && !p.hasVisits).map(p => p.id);
+ if (pageIdsToRemove.length > 0) {
+ let idsList = sqlList(pageIdsToRemove);
+ // Note, we are already in a transaction, since callers create it.
+ // Check relations regardless, to avoid creating orphans in case of
+ // async race conditions.
+ yield db.execute(`DELETE FROM moz_places WHERE id IN ( ${ idsList } )
+ AND foreign_count = 0 AND last_visit_date ISNULL`);
+ // Hosts accumulated during the places delete are updated through a trigger
+ // (see nsPlacesTriggers.h).
+ yield db.executeCached(`DELETE FROM moz_updatehosts_temp`);
+
+ // Expire orphans.
+ yield db.executeCached(`
+ DELETE FROM moz_favicons WHERE NOT EXISTS
+ (SELECT 1 FROM moz_places WHERE favicon_id = moz_favicons.id)`);
+ yield db.execute(`DELETE FROM moz_annos
+ WHERE place_id IN ( ${ idsList } )`);
+ yield db.execute(`DELETE FROM moz_inputhistory
+ WHERE place_id IN ( ${ idsList } )`);
+ }
+});
+
+/**
+ * Notify observers that pages have been removed/updated.
+ *
+ * @param db: (Sqlite connection)
+ * The database.
+ * @param pages: (Array of objects)
+ * Pages that have been touched and that need cleaning up.
+ * Each object should have the following properties:
+ * - id: (number) The `moz_places` identifier for the place.
+ * - hasVisits: (boolean) If `true`, there remains at least one
+ * visit to this page, so the page should be kept and its
+ * frecency updated.
+ * - hasForeign: (boolean) If `true`, the page has at least
+ * one foreign reference (i.e. a bookmark), so the page should
+ * be kept and its frecency updated.
+ * @return (Promise)
+ */
+var notifyCleanup = Task.async(function*(db, pages) {
+ let notifiedCount = 0;
+ let observers = PlacesUtils.history.getObservers();
+
+ let reason = Ci.nsINavHistoryObserver.REASON_DELETED;
+
+ for (let page of pages) {
+ let uri = NetUtil.newURI(page.url.href);
+ let guid = page.guid;
+ if (page.hasVisits) {
+ // For the moment, we do not have the necessary observer API
+ // to notify when we remove a subset of visits, see bug 937560.
+ continue;
+ }
+ if (page.hasForeign) {
+ // We have removed all visits, but the page is still alive, e.g.
+ // because of a bookmark.
+ notify(observers, "onDeleteVisits",
+ [uri, /* last visit*/0, guid, reason, -1]);
+ } else {
+ // The page has been entirely removed.
+ notify(observers, "onDeleteURI",
+ [uri, guid, reason]);
+ }
+ if (++notifiedCount % NOTIFICATION_CHUNK_SIZE == 0) {
+ // Every few notifications, yield time back to the main
+ // thread to avoid jank.
+ yield Promise.resolve();
+ }
+ }
+});
+
+/**
+ * Notify an `onResult` callback of a set of operations
+ * that just took place.
+ *
+ * @param data: (Array)
+ * The data to send to the callback.
+ * @param onResult: (function [optional])
+ * If provided, call `onResult` with `data[0]`, `data[1]`, etc.
+ * Otherwise, do nothing.
+ */
+var notifyOnResult = Task.async(function*(data, onResult) {
+ if (!onResult) {
+ return;
+ }
+ let notifiedCount = 0;
+ for (let info of data) {
+ try {
+ onResult(info);
+ } catch (ex) {
+ // Errors should be reported but should not stop the operation.
+ Promise.reject(ex);
+ }
+ if (++notifiedCount % ONRESULT_CHUNK_SIZE == 0) {
+ // Every few notifications, yield time back to the main
+ // thread to avoid jank.
+ yield Promise.resolve();
+ }
+ }
+});
+
+// Inner implementation of History.removeVisitsByFilter.
+var removeVisitsByFilter = Task.async(function*(db, filter, onResult = null) {
+ // 1. Determine visits that took place during the interval. Note
+ // that the database uses microseconds, while JS uses milliseconds,
+ // so we need to *1000 one way and /1000 the other way.
+ let conditions = [];
+ let args = {};
+ if ("beginDate" in filter) {
+ conditions.push("v.visit_date >= :begin * 1000");
+ args.begin = Number(filter.beginDate);
+ }
+ if ("endDate" in filter) {
+ conditions.push("v.visit_date <= :end * 1000");
+ args.end = Number(filter.endDate);
+ }
+ if ("limit" in filter) {
+ args.limit = Number(filter.limit);
+ }
+
+ let optionalJoin = "";
+ if ("url" in filter) {
+ let url = filter.url;
+ if (url instanceof Ci.nsIURI) {
+ url = filter.url.spec;
+ } else {
+ url = new URL(url).href;
+ }
+ optionalJoin = `JOIN moz_places h ON h.id = v.place_id`;
+ conditions.push("h.url_hash = hash(:url)", "h.url = :url");
+ args.url = url;
+ }
+
+
+ let visitsToRemove = [];
+ let pagesToInspect = new Set();
+ let onResultData = onResult ? [] : null;
+
+ yield db.executeCached(
+ `SELECT v.id, place_id, visit_date / 1000 AS date, visit_type FROM moz_historyvisits v
+ ${optionalJoin}
+ WHERE ${ conditions.join(" AND ") }${ args.limit ? " LIMIT :limit" : "" }`,
+ args,
+ row => {
+ let id = row.getResultByName("id");
+ let place_id = row.getResultByName("place_id");
+ visitsToRemove.push(id);
+ pagesToInspect.add(place_id);
+
+ if (onResult) {
+ onResultData.push({
+ date: new Date(row.getResultByName("date")),
+ transition: row.getResultByName("visit_type")
+ });
+ }
+ }
+ );
+
+ try {
+ if (visitsToRemove.length == 0) {
+ // Nothing to do
+ return false;
+ }
+
+ let pages = [];
+ yield db.executeTransaction(function*() {
+ // 2. Remove all offending visits.
+ yield db.execute(`DELETE FROM moz_historyvisits
+ WHERE id IN (${ sqlList(visitsToRemove) } )`);
+
+ // 3. Find out which pages have been orphaned
+ yield db.execute(
+ `SELECT id, url, guid,
+ (foreign_count != 0) AS has_foreign,
+ (last_visit_date NOTNULL) as has_visits
+ FROM moz_places
+ WHERE id IN (${ sqlList([...pagesToInspect]) })`,
+ null,
+ row => {
+ let page = {
+ id: row.getResultByName("id"),
+ guid: row.getResultByName("guid"),
+ hasForeign: row.getResultByName("has_foreign"),
+ hasVisits: row.getResultByName("has_visits"),
+ url: new URL(row.getResultByName("url")),
+ };
+ pages.push(page);
+ });
+
+ // 4. Clean up and notify
+ yield cleanupPages(db, pages);
+ });
+
+ notifyCleanup(db, pages);
+ notifyOnResult(onResultData, onResult); // don't wait
+ } finally {
+ // Ensure we cleanup embed visits, even if we bailed out early.
+ PlacesUtils.history.clearEmbedVisits();
+ }
+
+ return visitsToRemove.length != 0;
+});
+
+
+// Inner implementation of History.remove.
+var remove = Task.async(function*(db, {guids, urls}, onResult = null) {
+ // 1. Find out what needs to be removed
+ let query =
+ `SELECT id, url, guid, foreign_count, title, frecency
+ FROM moz_places
+ WHERE guid IN (${ sqlList(guids) })
+ OR (url_hash IN (${ urls.map(u => "hash(" + JSON.stringify(u) + ")").join(",") })
+ AND url IN (${ sqlList(urls) }))
+ `;
+
+ let onResultData = onResult ? [] : null;
+ let pages = [];
+ let hasPagesToRemove = false;
+ yield db.execute(query, null, Task.async(function*(row) {
+ let hasForeign = row.getResultByName("foreign_count") != 0;
+ if (!hasForeign) {
+ hasPagesToRemove = true;
+ }
+ let id = row.getResultByName("id");
+ let guid = row.getResultByName("guid");
+ let url = row.getResultByName("url");
+ let page = {
+ id,
+ guid,
+ hasForeign,
+ hasVisits: false,
+ url: new URL(url),
+ };
+ pages.push(page);
+ if (onResult) {
+ onResultData.push({
+ guid: guid,
+ title: row.getResultByName("title"),
+ frecency: row.getResultByName("frecency"),
+ url: new URL(url)
+ });
+ }
+ }));
+
+ try {
+ if (pages.length == 0) {
+ // Nothing to do
+ return false;
+ }
+
+ yield db.executeTransaction(function*() {
+ // 2. Remove all visits to these pages.
+ yield db.execute(`DELETE FROM moz_historyvisits
+ WHERE place_id IN (${ sqlList(pages.map(p => p.id)) })
+ `);
+
+ // 3. Clean up and notify
+ yield cleanupPages(db, pages);
+ });
+
+ notifyCleanup(db, pages);
+ notifyOnResult(onResultData, onResult); // don't wait
+ } finally {
+ // Ensure we cleanup embed visits, even if we bailed out early.
+ PlacesUtils.history.clearEmbedVisits();
+ }
+
+ return hasPagesToRemove;
+});
+
+/**
+ * Merges an updateInfo object, as returned by asyncHistory.updatePlaces
+ * into a PageInfo object as defined in this file.
+ *
+ * @param updateInfo: (Object)
+ * An object that represents a page that is generated by
+ * asyncHistory.updatePlaces.
+ * @param pageInfo: (PageInfo)
+ * An PageInfo object into which to merge the data from updateInfo.
+ * Defaults to an empty object so that this method can be used
+ * to simply convert an updateInfo object into a PageInfo object.
+ *
+ * @return (PageInfo)
+ * A PageInfo object populated with data from updateInfo.
+ */
+function mergeUpdateInfoIntoPageInfo(updateInfo, pageInfo={}) {
+ pageInfo.guid = updateInfo.guid;
+ if (!pageInfo.url) {
+ pageInfo.url = new URL(updateInfo.uri.spec);
+ pageInfo.title = updateInfo.title;
+ pageInfo.visits = updateInfo.visits.map(visit => {
+ return {
+ date: PlacesUtils.toDate(visit.visitDate),
+ transition: visit.transitionType,
+ referrer: (visit.referrerURI) ? new URL(visit.referrerURI.spec) : null
+ }
+ });
+ }
+ return pageInfo;
+}
+
+// Inner implementation of History.insert.
+var insert = Task.async(function*(db, pageInfo) {
+ let info = convertForUpdatePlaces(pageInfo);
+
+ return new Promise((resolve, reject) => {
+ PlacesUtils.asyncHistory.updatePlaces(info, {
+ handleError: error => {
+ reject(error);
+ },
+ handleResult: result => {
+ pageInfo = mergeUpdateInfoIntoPageInfo(result, pageInfo);
+ },
+ handleCompletion: () => {
+ resolve(pageInfo);
+ }
+ });
+ });
+});
+
+// Inner implementation of History.insertMany.
+var insertMany = Task.async(function*(db, pageInfos, onResult, onError) {
+ let infos = [];
+ let onResultData = [];
+ let onErrorData = [];
+
+ for (let pageInfo of pageInfos) {
+ let info = convertForUpdatePlaces(pageInfo);
+ infos.push(info);
+ }
+
+ return new Promise((resolve, reject) => {
+ PlacesUtils.asyncHistory.updatePlaces(infos, {
+ handleError: (resultCode, result) => {
+ let pageInfo = mergeUpdateInfoIntoPageInfo(result);
+ onErrorData.push(pageInfo);
+ },
+ handleResult: result => {
+ let pageInfo = mergeUpdateInfoIntoPageInfo(result);
+ onResultData.push(pageInfo);
+ },
+ handleCompletion: () => {
+ notifyOnResult(onResultData, onResult);
+ notifyOnResult(onErrorData, onError);
+ if (onResultData.length) {
+ resolve();
+ } else {
+ reject({message: "No items were added to history."})
+ }
+ }
+ });
+ });
+});
diff --git a/toolkit/components/places/PageIconProtocolHandler.js b/toolkit/components/places/PageIconProtocolHandler.js
new file mode 100644
index 0000000000..05e43ccf3e
--- /dev/null
+++ b/toolkit/components/places/PageIconProtocolHandler.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+function makeDefaultFaviconChannel(uri, loadInfo) {
+ let channel = Services.io.newChannelFromURIWithLoadInfo(
+ PlacesUtils.favicons.defaultFavicon, loadInfo);
+ channel.originalURI = uri;
+ return channel;
+}
+
+function streamDefaultFavicon(uri, loadInfo, outputStream) {
+ try {
+ // Open up a new channel to get that data, and push it to our output stream.
+ // Create a listener to hand data to the pipe's output stream.
+ let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
+ .createInstance(Ci.nsISimpleStreamListener);
+ listener.init(outputStream, {
+ onStartRequest(request, context) {},
+ onStopRequest(request, context, statusCode) {
+ // We must close the outputStream regardless.
+ outputStream.close();
+ }
+ });
+ let defaultIconChannel = makeDefaultFaviconChannel(uri, loadInfo);
+ defaultIconChannel.asyncOpen2(listener);
+ } catch (ex) {
+ Cu.reportError(ex);
+ outputStream.close();
+ }
+}
+
+function PageIconProtocolHandler() {
+}
+
+PageIconProtocolHandler.prototype = {
+ get scheme() {
+ return "page-icon";
+ },
+
+ get defaultPort() {
+ return -1;
+ },
+
+ get protocolFlags() {
+ return Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_NOAUTH |
+ Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE;
+ },
+
+ newURI(spec, originCharset, baseURI) {
+ let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
+ uri.spec = spec;
+ return uri;
+ },
+
+ newChannel2(uri, loadInfo) {
+ try {
+ // Create a pipe that will give us an output stream that we can use once
+ // we got all the favicon data.
+ let pipe = Cc["@mozilla.org/pipe;1"]
+ .createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, Ci.nsIFaviconService.MAX_FAVICON_BUFFER_SIZE);
+
+ // Create our channel.
+ let channel = Cc['@mozilla.org/network/input-stream-channel;1']
+ .createInstance(Ci.nsIInputStreamChannel);
+ channel.QueryInterface(Ci.nsIChannel);
+ channel.setURI(uri);
+ channel.contentStream = pipe.inputStream;
+ channel.loadInfo = loadInfo;
+
+ let pageURI = NetUtil.newURI(uri.path);
+ PlacesUtils.favicons.getFaviconDataForPage(pageURI, (iconuri, len, data, mime) => {
+ if (len == 0) {
+ channel.contentType = "image/png";
+ streamDefaultFavicon(uri, loadInfo, pipe.outputStream);
+ return;
+ }
+
+ try {
+ channel.contentType = mime;
+ // Pass the icon data to the output stream.
+ let stream = Cc["@mozilla.org/binaryoutputstream;1"]
+ .createInstance(Ci.nsIBinaryOutputStream);
+ stream.setOutputStream(pipe.outputStream);
+ stream.writeByteArray(data, len);
+ stream.close();
+ pipe.outputStream.close();
+ } catch (ex) {
+ channel.contentType = "image/png";
+ streamDefaultFavicon(uri, loadInfo, pipe.outputStream);
+ }
+ });
+
+ return channel;
+ } catch (ex) {
+ return makeDefaultFaviconChannel(uri, loadInfo);
+ }
+ },
+
+ newChannel(uri) {
+ return this.newChannel2(uri, null);
+ },
+
+ allowPort(port, scheme) {
+ return false;
+ },
+
+ classID: Components.ID("{60a1f7c6-4ff9-4a42-84d3-5a185faa6f32}"),
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIProtocolHandler
+ ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PageIconProtocolHandler]);
diff --git a/toolkit/components/places/PlaceInfo.cpp b/toolkit/components/places/PlaceInfo.cpp
new file mode 100644
index 0000000000..760b0e7188
--- /dev/null
+++ b/toolkit/components/places/PlaceInfo.cpp
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "PlaceInfo.h"
+#include "VisitInfo.h"
+#include "nsIURI.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIXPConnect.h"
+#include "mozilla/Services.h"
+#include "jsapi.h"
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// PlaceInfo
+
+PlaceInfo::PlaceInfo(int64_t aId,
+ const nsCString& aGUID,
+ already_AddRefed<nsIURI> aURI,
+ const nsString& aTitle,
+ int64_t aFrecency)
+: mId(aId)
+, mGUID(aGUID)
+, mURI(aURI)
+, mTitle(aTitle)
+, mFrecency(aFrecency)
+, mVisitsAvailable(false)
+{
+ NS_PRECONDITION(mURI, "Must provide a non-null uri!");
+}
+
+PlaceInfo::PlaceInfo(int64_t aId,
+ const nsCString& aGUID,
+ already_AddRefed<nsIURI> aURI,
+ const nsString& aTitle,
+ int64_t aFrecency,
+ const VisitsArray& aVisits)
+: mId(aId)
+, mGUID(aGUID)
+, mURI(aURI)
+, mTitle(aTitle)
+, mFrecency(aFrecency)
+, mVisits(aVisits)
+, mVisitsAvailable(true)
+{
+ NS_PRECONDITION(mURI, "Must provide a non-null uri!");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// mozIPlaceInfo
+
+NS_IMETHODIMP
+PlaceInfo::GetPlaceId(int64_t* _placeId)
+{
+ *_placeId = mId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceInfo::GetGuid(nsACString& _guid)
+{
+ _guid = mGUID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceInfo::GetUri(nsIURI** _uri)
+{
+ NS_ADDREF(*_uri = mURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceInfo::GetTitle(nsAString& _title)
+{
+ _title = mTitle;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceInfo::GetFrecency(int64_t* _frecency)
+{
+ *_frecency = mFrecency;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PlaceInfo::GetVisits(JSContext* aContext,
+ JS::MutableHandle<JS::Value> _visits)
+{
+ // If the visits data was not provided, return null rather
+ // than an empty array to distinguish this case from the case
+ // of a place without any visit.
+ if (!mVisitsAvailable) {
+ _visits.setNull();
+ return NS_OK;
+ }
+
+ // TODO bug 625913 when we use this in situations that have more than one
+ // visit here, we will likely want to make this cache the value.
+ JS::Rooted<JSObject*> visits(aContext,
+ JS_NewArrayObject(aContext, 0));
+ NS_ENSURE_TRUE(visits, NS_ERROR_OUT_OF_MEMORY);
+
+ JS::Rooted<JSObject*> global(aContext, JS::CurrentGlobalOrNull(aContext));
+ NS_ENSURE_TRUE(global, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIXPConnect> xpc = mozilla::services::GetXPConnect();
+
+ for (VisitsArray::size_type idx = 0; idx < mVisits.Length(); idx++) {
+ JS::RootedObject jsobj(aContext);
+ nsresult rv = xpc->WrapNative(aContext, global, mVisits[idx],
+ NS_GET_IID(mozIVisitInfo),
+ jsobj.address());
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(jsobj);
+
+ bool rc = JS_DefineElement(aContext, visits, idx, jsobj, JSPROP_ENUMERATE);
+ NS_ENSURE_TRUE(rc, NS_ERROR_UNEXPECTED);
+ }
+
+ _visits.setObject(*visits);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsISupports
+
+NS_IMPL_ISUPPORTS(
+ PlaceInfo
+, mozIPlaceInfo
+)
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/PlaceInfo.h b/toolkit/components/places/PlaceInfo.h
new file mode 100644
index 0000000000..b1d3c0893b
--- /dev/null
+++ b/toolkit/components/places/PlaceInfo.h
@@ -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/. */
+
+#ifndef mozilla_places_PlaceInfo_h__
+#define mozilla_places_PlaceInfo_h__
+
+#include "mozIAsyncHistory.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsAutoPtr.h"
+#include "mozilla/Attributes.h"
+
+class nsIURI;
+class mozIVisitInfo;
+
+namespace mozilla {
+namespace places {
+
+
+class PlaceInfo final : public mozIPlaceInfo
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIPLACEINFO
+
+ typedef nsTArray< nsCOMPtr<mozIVisitInfo> > VisitsArray;
+
+ PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed<nsIURI> aURI,
+ const nsString& aTitle, int64_t aFrecency);
+ PlaceInfo(int64_t aId, const nsCString& aGUID, already_AddRefed<nsIURI> aURI,
+ const nsString& aTitle, int64_t aFrecency,
+ const VisitsArray& aVisits);
+
+private:
+ ~PlaceInfo() {}
+
+ const int64_t mId;
+ const nsCString mGUID;
+ nsCOMPtr<nsIURI> mURI;
+ const nsString mTitle;
+ const int64_t mFrecency;
+ const VisitsArray mVisits;
+ bool mVisitsAvailable;
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_PlaceInfo_h__
diff --git a/toolkit/components/places/PlacesBackups.jsm b/toolkit/components/places/PlacesBackups.jsm
new file mode 100644
index 0000000000..8315aeb3ac
--- /dev/null
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -0,0 +1,550 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab 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/. */
+
+this.EXPORTED_SYMBOLS = ["PlacesBackups"];
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
+ "resource://gre/modules/BookmarkJSONUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "localFileCtor",
+ () => Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath"));
+
+XPCOMUtils.defineLazyGetter(this, "filenamesRegex",
+ () => /^bookmarks-([0-9-]+)(?:_([0-9]+)){0,1}(?:_([a-z0-9=+-]{24})){0,1}\.(json(lz4)?)$/i
+);
+
+/**
+ * Appends meta-data information to a given filename.
+ */
+function appendMetaDataToFilename(aFilename, aMetaData) {
+ let matches = aFilename.match(filenamesRegex);
+ return "bookmarks-" + matches[1] +
+ "_" + aMetaData.count +
+ "_" + aMetaData.hash +
+ "." + matches[4];
+}
+
+/**
+ * Gets the hash from a backup filename.
+ *
+ * @return the extracted hash or null.
+ */
+function getHashFromFilename(aFilename) {
+ let matches = aFilename.match(filenamesRegex);
+ if (matches && matches[3])
+ return matches[3];
+ return null;
+}
+
+/**
+ * Given two filenames, checks if they contain the same date.
+ */
+function isFilenameWithSameDate(aSourceName, aTargetName) {
+ let sourceMatches = aSourceName.match(filenamesRegex);
+ let targetMatches = aTargetName.match(filenamesRegex);
+
+ return sourceMatches && targetMatches &&
+ sourceMatches[1] == targetMatches[1];
+}
+
+/**
+ * Given a filename, searches for another backup with the same date.
+ *
+ * @return OS.File path string or null.
+ */
+function getBackupFileForSameDate(aFilename) {
+ return Task.spawn(function* () {
+ let backupFiles = yield PlacesBackups.getBackupFiles();
+ for (let backupFile of backupFiles) {
+ if (isFilenameWithSameDate(OS.Path.basename(backupFile), aFilename))
+ return backupFile;
+ }
+ return null;
+ });
+}
+
+this.PlacesBackups = {
+ /**
+ * Matches the backup filename:
+ * 0: file name
+ * 1: date in form Y-m-d
+ * 2: bookmarks count
+ * 3: contents hash
+ * 4: file extension
+ */
+ get filenamesRegex() {
+ return filenamesRegex;
+ },
+
+ get folder() {
+ Deprecated.warning(
+ "PlacesBackups.folder is deprecated and will be removed in a future version",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
+ return this._folder;
+ },
+
+ /**
+ * This exists just to avoid spamming deprecate warnings from internal calls
+ * needed to support deprecated methods themselves.
+ */
+ get _folder() {
+ let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ bookmarksBackupDir.append(this.profileRelativeFolderPath);
+ if (!bookmarksBackupDir.exists()) {
+ bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
+ if (!bookmarksBackupDir.exists())
+ throw ("Unable to create bookmarks backup folder");
+ }
+ delete this._folder;
+ return this._folder = bookmarksBackupDir;
+ },
+
+ /**
+ * Gets backup folder asynchronously.
+ * @return {Promise}
+ * @resolve the folder (the folder string path).
+ */
+ getBackupFolder: function PB_getBackupFolder() {
+ return Task.spawn(function* () {
+ if (this._backupFolder) {
+ return this._backupFolder;
+ }
+ let profileDir = OS.Constants.Path.profileDir;
+ let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath);
+ yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true });
+ return this._backupFolder = backupsDirPath;
+ }.bind(this));
+ },
+
+ get profileRelativeFolderPath() {
+ return "bookmarkbackups";
+ },
+
+ /**
+ * Cache current backups in a sorted (by date DESC) array.
+ */
+ get entries() {
+ Deprecated.warning(
+ "PlacesBackups.entries is deprecated and will be removed in a future version",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
+ return this._entries;
+ },
+
+ /**
+ * This exists just to avoid spamming deprecate warnings from internal calls
+ * needed to support deprecated methods themselves.
+ */
+ get _entries() {
+ delete this._entries;
+ this._entries = [];
+ let files = this._folder.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.getNext().QueryInterface(Ci.nsIFile);
+ // A valid backup is any file that matches either the localized or
+ // not-localized filename (bug 445704).
+ if (!entry.isHidden() && filenamesRegex.test(entry.leafName)) {
+ // Remove bogus backups in future dates.
+ if (this.getDateForFile(entry) > new Date()) {
+ entry.remove(false);
+ continue;
+ }
+ this._entries.push(entry);
+ }
+ }
+ this._entries.sort((a, b) => {
+ let aDate = this.getDateForFile(a);
+ let bDate = this.getDateForFile(b);
+ return bDate - aDate;
+ });
+ return this._entries;
+ },
+
+ /**
+ * Cache current backups in a sorted (by date DESC) array.
+ * @return {Promise}
+ * @resolve a sorted array of string paths.
+ */
+ getBackupFiles: function PB_getBackupFiles() {
+ return Task.spawn(function* () {
+ if (this._backupFiles)
+ return this._backupFiles;
+
+ this._backupFiles = [];
+
+ let backupFolderPath = yield this.getBackupFolder();
+ let iterator = new OS.File.DirectoryIterator(backupFolderPath);
+ yield iterator.forEach(function(aEntry) {
+ // Since this is a lazy getter and OS.File I/O is serialized, we can
+ // safely remove .tmp files without risking to remove ongoing backups.
+ if (aEntry.name.endsWith(".tmp")) {
+ OS.File.remove(aEntry.path);
+ return undefined;
+ }
+
+ if (filenamesRegex.test(aEntry.name)) {
+ // Remove bogus backups in future dates.
+ let filePath = aEntry.path;
+ if (this.getDateForFile(filePath) > new Date()) {
+ return OS.File.remove(filePath);
+ }
+ this._backupFiles.push(filePath);
+ }
+
+ return undefined;
+ }.bind(this));
+ iterator.close();
+
+ this._backupFiles.sort((a, b) => {
+ let aDate = this.getDateForFile(a);
+ let bDate = this.getDateForFile(b);
+ return bDate - aDate;
+ });
+
+ return this._backupFiles;
+ }.bind(this));
+ },
+
+ /**
+ * Generates a ISO date string (YYYY-MM-DD) from a Date object.
+ *
+ * @param dateObj
+ * The date object to parse.
+ * @return an ISO date string.
+ */
+ toISODateString: function toISODateString(dateObj) {
+ if (!dateObj || dateObj.constructor.name != "Date" || !dateObj.getTime())
+ throw new Error("invalid date object");
+ let padDate = val => ("0" + val).substr(-2, 2);
+ return [
+ dateObj.getFullYear(),
+ padDate(dateObj.getMonth() + 1),
+ padDate(dateObj.getDate())
+ ].join("-");
+ },
+
+ /**
+ * Creates a filename for bookmarks backup files.
+ *
+ * @param [optional] aDateObj
+ * Date object used to build the filename.
+ * Will use current date if empty.
+ * @param [optional] bool - aCompress
+ * Determines if file extension is json or jsonlz4
+ Default is json
+ * @return A bookmarks backup filename.
+ */
+ getFilenameForDate: function PB_getFilenameForDate(aDateObj, aCompress) {
+ let dateObj = aDateObj || new Date();
+ // Use YYYY-MM-DD (ISO 8601) as it doesn't contain illegal characters
+ // and makes the alphabetical order of multiple backup files more useful.
+ return "bookmarks-" + PlacesBackups.toISODateString(dateObj) + ".json" +
+ (aCompress ? "lz4" : "");
+ },
+
+ /**
+ * Creates a Date object from a backup file. The date is the backup
+ * creation date.
+ *
+ * @param aBackupFile
+ * nsIFile or string path of the backup.
+ * @return A Date object for the backup's creation time.
+ */
+ getDateForFile: function PB_getDateForFile(aBackupFile) {
+ let filename = (aBackupFile instanceof Ci.nsIFile) ? aBackupFile.leafName
+ : OS.Path.basename(aBackupFile);
+ let matches = filename.match(filenamesRegex);
+ if (!matches)
+ throw ("Invalid backup file name: " + filename);
+ return new Date(matches[1].replace(/-/g, "/"));
+ },
+
+ /**
+ * Get the most recent backup file.
+ *
+ * @returns nsIFile backup file
+ */
+ getMostRecent: function PB_getMostRecent() {
+ Deprecated.warning(
+ "PlacesBackups.getMostRecent is deprecated and will be removed in a future version",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
+
+ for (let i = 0; i < this._entries.length; i++) {
+ let rx = /\.json(lz4)?$/;
+ if (this._entries[i].leafName.match(rx))
+ return this._entries[i];
+ }
+ return null;
+ },
+
+ /**
+ * Get the most recent backup file.
+ *
+ * @return {Promise}
+ * @result the path to the file.
+ */
+ getMostRecentBackup: function PB_getMostRecentBackup() {
+ return Task.spawn(function* () {
+ let entries = yield this.getBackupFiles();
+ for (let entry of entries) {
+ let rx = /\.json(lz4)?$/;
+ if (OS.Path.basename(entry).match(rx)) {
+ return entry;
+ }
+ }
+ return null;
+ }.bind(this));
+ },
+
+ /**
+ * Serializes bookmarks using JSON, and writes to the supplied file.
+ * Note: any item that should not be backed up must be annotated with
+ * "places/excludeFromBackup".
+ *
+ * @param aFilePath
+ * OS.File path for the "bookmarks.json" file to be created.
+ * @return {Promise}
+ * @resolves the number of serialized uri nodes.
+ * @deprecated passing an nsIFile is deprecated
+ */
+ saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) {
+ if (aFilePath instanceof Ci.nsIFile) {
+ Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " +
+ "is deprecated. Please use an OS.File path instead.",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ aFilePath = aFilePath.path;
+ }
+ return Task.spawn(function* () {
+ let { count: nodeCount, hash: hash } =
+ yield BookmarkJSONUtils.exportToFile(aFilePath);
+
+ let backupFolderPath = yield this.getBackupFolder();
+ if (OS.Path.dirname(aFilePath) == backupFolderPath) {
+ // We are creating a backup in the default backups folder,
+ // so just update the internal cache.
+ this._entries.unshift(new localFileCtor(aFilePath));
+ if (!this._backupFiles) {
+ yield this.getBackupFiles();
+ }
+ this._backupFiles.unshift(aFilePath);
+ } else {
+ // If we are saving to a folder different than our backups folder, then
+ // we also want to create a new compressed version in it.
+ // This way we ensure the latest valid backup is the same saved by the
+ // user. See bug 424389.
+ let mostRecentBackupFile = yield this.getMostRecentBackup();
+ if (!mostRecentBackupFile ||
+ hash != getHashFromFilename(OS.Path.basename(mostRecentBackupFile))) {
+ let name = this.getFilenameForDate(undefined, true);
+ let newFilename = appendMetaDataToFilename(name,
+ { count: nodeCount,
+ hash: hash });
+ let newFilePath = OS.Path.join(backupFolderPath, newFilename);
+ let backupFile = yield getBackupFileForSameDate(name);
+ if (backupFile) {
+ // There is already a backup for today, replace it.
+ yield OS.File.remove(backupFile, { ignoreAbsent: true });
+ if (!this._backupFiles)
+ yield this.getBackupFiles();
+ else
+ this._backupFiles.shift();
+ this._backupFiles.unshift(newFilePath);
+ } else {
+ // There is no backup for today, add the new one.
+ this._entries.unshift(new localFileCtor(newFilePath));
+ if (!this._backupFiles)
+ yield this.getBackupFiles();
+ this._backupFiles.unshift(newFilePath);
+ }
+ let jsonString = yield OS.File.read(aFilePath);
+ yield OS.File.writeAtomic(newFilePath, jsonString, { compression: "lz4" });
+ }
+ }
+
+ return nodeCount;
+ }.bind(this));
+ },
+
+ /**
+ * Creates a dated backup in <profile>/bookmarkbackups.
+ * Stores the bookmarks using a lz4 compressed JSON file.
+ * Note: any item that should not be backed up must be annotated with
+ * "places/excludeFromBackup".
+ *
+ * @param [optional] int aMaxBackups
+ * The maximum number of backups to keep. If set to 0
+ * all existing backups are removed and aForceBackup is
+ * ignored, so a new one won't be created.
+ * @param [optional] bool aForceBackup
+ * Forces creating a backup even if one was already
+ * created that day (overwrites).
+ * @return {Promise}
+ */
+ create: function PB_create(aMaxBackups, aForceBackup) {
+ let limitBackups = function* () {
+ let backupFiles = yield this.getBackupFiles();
+ if (typeof aMaxBackups == "number" && aMaxBackups > -1 &&
+ backupFiles.length >= aMaxBackups) {
+ let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
+ while (numberOfBackupsToDelete--) {
+ this._entries.pop();
+ let oldestBackup = this._backupFiles.pop();
+ yield OS.File.remove(oldestBackup);
+ }
+ }
+ }.bind(this);
+
+ return Task.spawn(function* () {
+ if (aMaxBackups === 0) {
+ // Backups are disabled, delete any existing one and bail out.
+ yield limitBackups(0);
+ return;
+ }
+
+ // Ensure to initialize _backupFiles
+ if (!this._backupFiles)
+ yield this.getBackupFiles();
+ let newBackupFilename = this.getFilenameForDate(undefined, true);
+ // If we already have a backup for today we should do nothing, unless we
+ // were required to enforce a new backup.
+ let backupFile = yield getBackupFileForSameDate(newBackupFilename);
+ if (backupFile && !aForceBackup)
+ return;
+
+ if (backupFile) {
+ // In case there is a backup for today we should recreate it.
+ this._backupFiles.shift();
+ this._entries.shift();
+ yield OS.File.remove(backupFile, { ignoreAbsent: true });
+ }
+
+ // Now check the hash of the most recent backup, and try to create a new
+ // backup, if that fails due to hash conflict, just rename the old backup.
+ let mostRecentBackupFile = yield this.getMostRecentBackup();
+ let mostRecentHash = mostRecentBackupFile &&
+ getHashFromFilename(OS.Path.basename(mostRecentBackupFile));
+
+ // Save bookmarks to a backup file.
+ let backupFolder = yield this.getBackupFolder();
+ let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
+ let newFilenameWithMetaData;
+ try {
+ let { count: nodeCount, hash: hash } =
+ yield BookmarkJSONUtils.exportToFile(newBackupFile,
+ { compress: true,
+ failIfHashIs: mostRecentHash });
+ newFilenameWithMetaData = appendMetaDataToFilename(newBackupFilename,
+ { count: nodeCount,
+ hash: hash });
+ } catch (ex) {
+ if (!ex.becauseSameHash) {
+ throw ex;
+ }
+ // The last backup already contained up-to-date information, just
+ // rename it as if it was today's backup.
+ this._backupFiles.shift();
+ this._entries.shift();
+ newBackupFile = mostRecentBackupFile;
+ // Ensure we retain the proper extension when renaming
+ // the most recent backup file.
+ if (/\.json$/.test(OS.Path.basename(mostRecentBackupFile)))
+ newBackupFilename = this.getFilenameForDate();
+ newFilenameWithMetaData = appendMetaDataToFilename(
+ newBackupFilename,
+ { count: this.getBookmarkCountForFile(mostRecentBackupFile),
+ hash: mostRecentHash });
+ }
+
+ // Append metadata to the backup filename.
+ let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
+ yield OS.File.move(newBackupFile, newBackupFileWithMetadata);
+ this._entries.unshift(new localFileCtor(newBackupFileWithMetadata));
+ this._backupFiles.unshift(newBackupFileWithMetadata);
+
+ // Limit the number of backups.
+ yield limitBackups(aMaxBackups);
+ }.bind(this));
+ },
+
+ /**
+ * Gets the bookmark count for backup file.
+ *
+ * @param aFilePath
+ * File path The backup file.
+ *
+ * @return the bookmark count or null.
+ */
+ getBookmarkCountForFile: function PB_getBookmarkCountForFile(aFilePath) {
+ let count = null;
+ let filename = OS.Path.basename(aFilePath);
+ let matches = filename.match(filenamesRegex);
+ if (matches && matches[2])
+ count = matches[2];
+ return count;
+ },
+
+ /**
+ * Gets a bookmarks tree representation usable to create backups in different
+ * file formats. The root or the tree is PlacesUtils.placesRootId.
+ * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their
+ * descendants are excluded.
+ *
+ * @return an object representing a tree with the places root as its root.
+ * Each bookmark is represented by an object having these properties:
+ * * id: the item id (make this not enumerable after bug 824502)
+ * * title: the title
+ * * guid: unique id
+ * * parent: item id of the parent folder, not enumerable
+ * * index: the position in the parent
+ * * dateAdded: microseconds from the epoch
+ * * lastModified: microseconds from the epoch
+ * * type: type of the originating node as defined in PlacesUtils
+ * The following properties exist only for a subset of bookmarks:
+ * * annos: array of annotations
+ * * uri: url
+ * * iconuri: favicon's url
+ * * keyword: associated keyword
+ * * charset: last known charset
+ * * tags: csv string of tags
+ * * root: string describing whether this represents a root
+ * * children: array of child items in a folder
+ */
+ getBookmarksTree: Task.async(function* () {
+ let startTime = Date.now();
+ let root = yield PlacesUtils.promiseBookmarksTree(PlacesUtils.bookmarks.rootGuid, {
+ excludeItemsCallback: aItem => {
+ return aItem.annos &&
+ aItem.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
+ },
+ includeItemIds: true
+ });
+
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
+ .add(Date.now() - startTime);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ }
+ return [root, root.itemsCount];
+ })
+}
+
diff --git a/toolkit/components/places/PlacesCategoriesStarter.js b/toolkit/components/places/PlacesCategoriesStarter.js
new file mode 100644
index 0000000000..560bd486a8
--- /dev/null
+++ b/toolkit/components/places/PlacesCategoriesStarter.js
@@ -0,0 +1,110 @@
+/* -*- 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/. */
+
+// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+// Fired by TelemetryController when async telemetry data should be collected.
+const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
+
+// Seconds between maintenance runs.
+const MAINTENANCE_INTERVAL_SECONDS = 7 * 86400;
+
+// Imports
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
+ "resource://gre/modules/PlacesDBUtils.jsm");
+
+/**
+ * This component can be used as a starter for modules that have to run when
+ * certain categories are invoked.
+ */
+function PlacesCategoriesStarter()
+{
+ Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY, false);
+ Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, false);
+
+ // nsINavBookmarkObserver implementation.
+ let notify = () => {
+ if (!this._notifiedBookmarksSvcReady) {
+ // TODO (bug 1145424): for whatever reason, even if we remove this
+ // component from the category (and thus from the category cache we use
+ // to notify), we keep being notified.
+ this._notifiedBookmarksSvcReady = true;
+ // For perf reasons unregister from the category, since no further
+ // notifications are needed.
+ Cc["@mozilla.org/categorymanager;1"]
+ .getService(Ci.nsICategoryManager)
+ .deleteCategoryEntry("bookmark-observers", "PlacesCategoriesStarter", false);
+ // Directly notify PlacesUtils, to ensure it catches the notification.
+ PlacesUtils.observe(null, "bookmarks-service-ready", null);
+ }
+ };
+
+ [ "onItemAdded", "onItemRemoved", "onItemChanged", "onBeginUpdateBatch",
+ "onEndUpdateBatch", "onItemVisited", "onItemMoved"
+ ].forEach(aMethod => this[aMethod] = notify);
+}
+
+PlacesCategoriesStarter.prototype = {
+ // nsIObserver
+
+ observe: function PCS_observe(aSubject, aTopic, aData)
+ {
+ switch (aTopic) {
+ case PlacesUtils.TOPIC_SHUTDOWN:
+ Services.obs.removeObserver(this, PlacesUtils.TOPIC_SHUTDOWN);
+ Services.obs.removeObserver(this, TOPIC_GATHER_TELEMETRY);
+ let globalObj =
+ Cu.getGlobalForObject(PlacesCategoriesStarter.prototype);
+ let descriptor =
+ Object.getOwnPropertyDescriptor(globalObj, "PlacesDBUtils");
+ if (descriptor.value !== undefined) {
+ PlacesDBUtils.shutdown();
+ }
+ break;
+ case TOPIC_GATHER_TELEMETRY:
+ PlacesDBUtils.telemetry();
+ break;
+ case "idle-daily":
+ // Once a week run places.sqlite maintenance tasks.
+ let lastMaintenance = 0;
+ try {
+ lastMaintenance =
+ Services.prefs.getIntPref("places.database.lastMaintenance");
+ } catch (ex) {}
+ let nowSeconds = parseInt(Date.now() / 1000);
+ if (lastMaintenance < nowSeconds - MAINTENANCE_INTERVAL_SECONDS) {
+ PlacesDBUtils.maintenanceOnIdle();
+ }
+ break;
+ default:
+ throw new Error("Trying to handle an unknown category.");
+ }
+ },
+
+ // nsISupports
+
+ classID: Components.ID("803938d5-e26d-4453-bf46-ad4b26e41114"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(PlacesCategoriesStarter),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver
+ , Ci.nsINavBookmarkObserver
+ ])
+};
+
+// Module Registration
+
+var components = [PlacesCategoriesStarter];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm
new file mode 100644
index 0000000000..4ac6ea2610
--- /dev/null
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -0,0 +1,1138 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+this.EXPORTED_SYMBOLS = [ "PlacesDBUtils" ];
+
+// Constants
+
+const FINISHED_MAINTENANCE_TOPIC = "places-maintenance-finished";
+
+const BYTES_PER_MEBIBYTE = 1048576;
+
+// Smart getters
+
+XPCOMUtils.defineLazyGetter(this, "DBConn", function() {
+ return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+});
+
+// PlacesDBUtils
+
+this.PlacesDBUtils = {
+ /**
+ * Executes a list of maintenance tasks.
+ * Once finished it will pass a array log to the callback attached to tasks.
+ * FINISHED_MAINTENANCE_TOPIC is notified through observer service on finish.
+ *
+ * @param aTasks
+ * Tasks object to execute.
+ */
+ _executeTasks: function PDBU__executeTasks(aTasks)
+ {
+ if (PlacesDBUtils._isShuttingDown) {
+ aTasks.log("- We are shutting down. Will not schedule the tasks.");
+ aTasks.clear();
+ }
+
+ let task = aTasks.pop();
+ if (task) {
+ task.call(PlacesDBUtils, aTasks);
+ }
+ else {
+ // All tasks have been completed.
+ // Telemetry the time it took for maintenance, if a start time exists.
+ if (aTasks._telemetryStart) {
+ Services.telemetry.getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS")
+ .add(Date.now() - aTasks._telemetryStart);
+ aTasks._telemetryStart = 0;
+ }
+
+ if (aTasks.callback) {
+ let scope = aTasks.scope || Cu.getGlobalForObject(aTasks.callback);
+ aTasks.callback.call(scope, aTasks.messages);
+ }
+
+ // Notify observers that maintenance finished.
+ Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_TOPIC, null);
+ }
+ },
+
+ _isShuttingDown : false,
+ shutdown: function PDBU_shutdown() {
+ PlacesDBUtils._isShuttingDown = true;
+ },
+
+ /**
+ * Executes integrity check and common maintenance tasks.
+ *
+ * @param [optional] aCallback
+ * Callback to be invoked when done. The callback will get a array
+ * of log messages.
+ * @param [optional] aScope
+ * Scope for the callback.
+ */
+ maintenanceOnIdle: function PDBU_maintenanceOnIdle(aCallback, aScope)
+ {
+ let tasks = new Tasks([
+ this.checkIntegrity
+ , this.checkCoherence
+ , this._refreshUI
+ ]);
+ tasks._telemetryStart = Date.now();
+ tasks.callback = function() {
+ Services.prefs.setIntPref("places.database.lastMaintenance",
+ parseInt(Date.now() / 1000));
+ if (aCallback)
+ aCallback();
+ }
+ tasks.scope = aScope;
+ this._executeTasks(tasks);
+ },
+
+ /**
+ * Executes integrity check, common and advanced maintenance tasks (like
+ * expiration and vacuum). Will also collect statistics on the database.
+ *
+ * @param [optional] aCallback
+ * Callback to be invoked when done. The callback will get a array
+ * of log messages.
+ * @param [optional] aScope
+ * Scope for the callback.
+ */
+ checkAndFixDatabase: function PDBU_checkAndFixDatabase(aCallback, aScope)
+ {
+ let tasks = new Tasks([
+ this.checkIntegrity
+ , this.checkCoherence
+ , this.expire
+ , this.vacuum
+ , this.stats
+ , this._refreshUI
+ ]);
+ tasks.callback = aCallback;
+ tasks.scope = aScope;
+ this._executeTasks(tasks);
+ },
+
+ /**
+ * Forces a full refresh of Places views.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ _refreshUI: function PDBU__refreshUI(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+
+ // Send batch update notifications to update the UI.
+ PlacesUtils.history.runInBatchMode({
+ runBatched: function (aUserData) {}
+ }, null);
+ PlacesDBUtils._executeTasks(tasks);
+ },
+
+ _handleError: function PDBU__handleError(aError)
+ {
+ Cu.reportError("Async statement execution returned with '" +
+ aError.result + "', '" + aError.message + "'");
+ },
+
+ /**
+ * Tries to execute a REINDEX on the database.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ reindex: function PDBU_reindex(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+ tasks.log("> Reindex");
+
+ let stmt = DBConn.createAsyncStatement("REINDEX");
+ stmt.executeAsync({
+ handleError: PlacesDBUtils._handleError,
+ handleResult: function () {},
+
+ handleCompletion: function (aReason)
+ {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ tasks.log("+ The database has been reindexed");
+ }
+ else {
+ tasks.log("- Unable to reindex database");
+ }
+
+ PlacesDBUtils._executeTasks(tasks);
+ }
+ });
+ stmt.finalize();
+ },
+
+ /**
+ * Checks integrity but does not try to fix the database through a reindex.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ _checkIntegritySkipReindex: function PDBU__checkIntegritySkipReindex(aTasks) {
+ return this.checkIntegrity(aTasks, true);
+ },
+
+ /**
+ * Checks integrity and tries to fix the database through a reindex.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ * @param [optional] aSkipdReindex
+ * Whether to try to reindex database or not.
+ */
+ checkIntegrity: function PDBU_checkIntegrity(aTasks, aSkipReindex)
+ {
+ let tasks = new Tasks(aTasks);
+ tasks.log("> Integrity check");
+
+ // Run a integrity check, but stop at the first error.
+ let stmt = DBConn.createAsyncStatement("PRAGMA integrity_check(1)");
+ stmt.executeAsync({
+ handleError: PlacesDBUtils._handleError,
+
+ _corrupt: false,
+ handleResult: function (aResultSet)
+ {
+ let row = aResultSet.getNextRow();
+ this._corrupt = row.getResultByIndex(0) != "ok";
+ },
+
+ handleCompletion: function (aReason)
+ {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ if (this._corrupt) {
+ tasks.log("- The database is corrupt");
+ if (aSkipReindex) {
+ tasks.log("- Unable to fix corruption, database will be replaced on next startup");
+ Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
+ tasks.clear();
+ }
+ else {
+ // Try to reindex, this often fixed simple indices corruption.
+ // We insert from the top of the queue, they will run inverse.
+ tasks.push(PlacesDBUtils._checkIntegritySkipReindex);
+ tasks.push(PlacesDBUtils.reindex);
+ }
+ }
+ else {
+ tasks.log("+ The database is sane");
+ }
+ }
+ else {
+ tasks.log("- Unable to check database status");
+ tasks.clear();
+ }
+
+ PlacesDBUtils._executeTasks(tasks);
+ }
+ });
+ stmt.finalize();
+ },
+
+ /**
+ * Checks data coherence and tries to fix most common errors.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ checkCoherence: function PDBU_checkCoherence(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+ tasks.log("> Coherence check");
+
+ let stmts = PlacesDBUtils._getBoundCoherenceStatements();
+ DBConn.executeAsync(stmts, stmts.length, {
+ handleError: PlacesDBUtils._handleError,
+ handleResult: function () {},
+
+ handleCompletion: function (aReason)
+ {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ tasks.log("+ The database is coherent");
+ }
+ else {
+ tasks.log("- Unable to check database coherence");
+ tasks.clear();
+ }
+
+ PlacesDBUtils._executeTasks(tasks);
+ }
+ });
+ stmts.forEach(aStmt => aStmt.finalize());
+ },
+
+ _getBoundCoherenceStatements: function PDBU__getBoundCoherenceStatements()
+ {
+ let cleanupStatements = [];
+
+ // MOZ_ANNO_ATTRIBUTES
+ // A.1 remove obsolete annotations from moz_annos.
+ // The 'weave0' idiom exploits character ordering (0 follows /) to
+ // efficiently select all annos with a 'weave/' prefix.
+ let deleteObsoleteAnnos = DBConn.createAsyncStatement(
+ `DELETE FROM moz_annos
+ WHERE type = 4
+ OR anno_attribute_id IN (
+ SELECT id FROM moz_anno_attributes
+ WHERE name BETWEEN 'weave/' AND 'weave0'
+ )`);
+ cleanupStatements.push(deleteObsoleteAnnos);
+
+ // A.2 remove obsolete annotations from moz_items_annos.
+ let deleteObsoleteItemsAnnos = DBConn.createAsyncStatement(
+ `DELETE FROM moz_items_annos
+ WHERE type = 4
+ OR anno_attribute_id IN (
+ SELECT id FROM moz_anno_attributes
+ WHERE name = 'sync/children'
+ OR name = 'placesInternal/GUID'
+ OR name BETWEEN 'weave/' AND 'weave0'
+ )`);
+ cleanupStatements.push(deleteObsoleteItemsAnnos);
+
+ // A.3 remove unused attributes.
+ let deleteUnusedAnnoAttributes = DBConn.createAsyncStatement(
+ `DELETE FROM moz_anno_attributes WHERE id IN (
+ SELECT id FROM moz_anno_attributes n
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_annos WHERE anno_attribute_id = n.id LIMIT 1)
+ AND NOT EXISTS
+ (SELECT id FROM moz_items_annos WHERE anno_attribute_id = n.id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteUnusedAnnoAttributes);
+
+ // MOZ_ANNOS
+ // B.1 remove annos with an invalid attribute
+ let deleteInvalidAttributeAnnos = DBConn.createAsyncStatement(
+ `DELETE FROM moz_annos WHERE id IN (
+ SELECT id FROM moz_annos a
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_anno_attributes
+ WHERE id = a.anno_attribute_id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteInvalidAttributeAnnos);
+
+ // B.2 remove orphan annos
+ let deleteOrphanAnnos = DBConn.createAsyncStatement(
+ `DELETE FROM moz_annos WHERE id IN (
+ SELECT id FROM moz_annos a
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = a.place_id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteOrphanAnnos);
+
+ // Bookmarks roots
+ // C.1 fix missing Places root
+ // Bug 477739 shows a case where the root could be wrongly removed
+ // due to an endianness issue. We try to fix broken roots here.
+ let selectPlacesRoot = DBConn.createStatement(
+ "SELECT id FROM moz_bookmarks WHERE id = :places_root");
+ selectPlacesRoot.params["places_root"] = PlacesUtils.placesRootId;
+ if (!selectPlacesRoot.executeStep()) {
+ // We are missing the root, try to recreate it.
+ let createPlacesRoot = DBConn.createAsyncStatement(
+ `INSERT INTO moz_bookmarks (id, type, fk, parent, position, title,
+ guid)
+ VALUES (:places_root, 2, NULL, 0, 0, :title, :guid)`);
+ createPlacesRoot.params["places_root"] = PlacesUtils.placesRootId;
+ createPlacesRoot.params["title"] = "";
+ createPlacesRoot.params["guid"] = PlacesUtils.bookmarks.rootGuid;
+ cleanupStatements.push(createPlacesRoot);
+
+ // Now ensure that other roots are children of Places root.
+ let fixPlacesRootChildren = DBConn.createAsyncStatement(
+ `UPDATE moz_bookmarks SET parent = :places_root WHERE guid IN
+ ( :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid )`);
+ fixPlacesRootChildren.params["places_root"] = PlacesUtils.placesRootId;
+ fixPlacesRootChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ fixPlacesRootChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ fixPlacesRootChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ fixPlacesRootChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(fixPlacesRootChildren);
+ }
+ selectPlacesRoot.finalize();
+
+ // C.2 fix roots titles
+ // some alpha version has wrong roots title, and this also fixes them if
+ // locale has changed.
+ let updateRootTitleSql = `UPDATE moz_bookmarks SET title = :title
+ WHERE id = :root_id AND title <> :title`;
+ // root
+ let fixPlacesRootTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+ fixPlacesRootTitle.params["root_id"] = PlacesUtils.placesRootId;
+ fixPlacesRootTitle.params["title"] = "";
+ cleanupStatements.push(fixPlacesRootTitle);
+ // bookmarks menu
+ let fixBookmarksMenuTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+ fixBookmarksMenuTitle.params["root_id"] = PlacesUtils.bookmarksMenuFolderId;
+ fixBookmarksMenuTitle.params["title"] =
+ PlacesUtils.getString("BookmarksMenuFolderTitle");
+ cleanupStatements.push(fixBookmarksMenuTitle);
+ // bookmarks toolbar
+ let fixBookmarksToolbarTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+ fixBookmarksToolbarTitle.params["root_id"] = PlacesUtils.toolbarFolderId;
+ fixBookmarksToolbarTitle.params["title"] =
+ PlacesUtils.getString("BookmarksToolbarFolderTitle");
+ cleanupStatements.push(fixBookmarksToolbarTitle);
+ // unsorted bookmarks
+ let fixUnsortedBookmarksTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+ fixUnsortedBookmarksTitle.params["root_id"] = PlacesUtils.unfiledBookmarksFolderId;
+ fixUnsortedBookmarksTitle.params["title"] =
+ PlacesUtils.getString("OtherBookmarksFolderTitle");
+ cleanupStatements.push(fixUnsortedBookmarksTitle);
+ // tags
+ let fixTagsRootTitle = DBConn.createAsyncStatement(updateRootTitleSql);
+ fixTagsRootTitle.params["root_id"] = PlacesUtils.tagsFolderId;
+ fixTagsRootTitle.params["title"] =
+ PlacesUtils.getString("TagsFolderTitle");
+ cleanupStatements.push(fixTagsRootTitle);
+
+ // MOZ_BOOKMARKS
+ // D.1 remove items without a valid place
+ // if fk IS NULL we fix them in D.7
+ let deleteNoPlaceItems = DBConn.createAsyncStatement(
+ `DELETE FROM moz_bookmarks WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT b.id FROM moz_bookmarks b
+ WHERE fk NOT NULL AND b.type = :bookmark_type
+ AND NOT EXISTS (SELECT url FROM moz_places WHERE id = b.fk LIMIT 1)
+ )`);
+ deleteNoPlaceItems.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+ deleteNoPlaceItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ deleteNoPlaceItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ deleteNoPlaceItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ deleteNoPlaceItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ deleteNoPlaceItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(deleteNoPlaceItems);
+
+ // D.2 remove items that are not uri bookmarks from tag containers
+ let deleteBogusTagChildren = DBConn.createAsyncStatement(
+ `DELETE FROM moz_bookmarks WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT b.id FROM moz_bookmarks b
+ WHERE b.parent IN
+ (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder)
+ AND b.type <> :bookmark_type
+ )`);
+ deleteBogusTagChildren.params["tags_folder"] = PlacesUtils.tagsFolderId;
+ deleteBogusTagChildren.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+ deleteBogusTagChildren.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ deleteBogusTagChildren.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ deleteBogusTagChildren.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ deleteBogusTagChildren.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ deleteBogusTagChildren.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(deleteBogusTagChildren);
+
+ // D.3 remove empty tags
+ let deleteEmptyTags = DBConn.createAsyncStatement(
+ `DELETE FROM moz_bookmarks WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT b.id FROM moz_bookmarks b
+ WHERE b.id IN
+ (SELECT id FROM moz_bookmarks WHERE parent = :tags_folder)
+ AND NOT EXISTS
+ (SELECT id from moz_bookmarks WHERE parent = b.id LIMIT 1)
+ )`);
+ deleteEmptyTags.params["tags_folder"] = PlacesUtils.tagsFolderId;
+ deleteEmptyTags.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ deleteEmptyTags.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ deleteEmptyTags.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ deleteEmptyTags.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ deleteEmptyTags.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(deleteEmptyTags);
+
+ // D.4 move orphan items to unsorted folder
+ let fixOrphanItems = DBConn.createAsyncStatement(
+ `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT b.id FROM moz_bookmarks b
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_bookmarks WHERE id = b.parent LIMIT 1)
+ )`);
+ fixOrphanItems.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId;
+ fixOrphanItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ fixOrphanItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ fixOrphanItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ fixOrphanItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ fixOrphanItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(fixOrphanItems);
+
+ // D.6 fix wrong item types
+ // Folders and separators should not have an fk.
+ // If they have a valid fk convert them to bookmarks. Later in D.9 we
+ // will move eventual children to unsorted bookmarks.
+ let fixBookmarksAsFolders = DBConn.createAsyncStatement(
+ `UPDATE moz_bookmarks SET type = :bookmark_type WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT id FROM moz_bookmarks b
+ WHERE type IN (:folder_type, :separator_type)
+ AND fk NOTNULL
+ )`);
+ fixBookmarksAsFolders.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+ fixBookmarksAsFolders.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER;
+ fixBookmarksAsFolders.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR;
+ fixBookmarksAsFolders.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ fixBookmarksAsFolders.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ fixBookmarksAsFolders.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ fixBookmarksAsFolders.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ fixBookmarksAsFolders.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(fixBookmarksAsFolders);
+
+ // D.7 fix wrong item types
+ // Bookmarks should have an fk, if they don't have any, convert them to
+ // folders.
+ let fixFoldersAsBookmarks = DBConn.createAsyncStatement(
+ `UPDATE moz_bookmarks SET type = :folder_type WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT id FROM moz_bookmarks b
+ WHERE type = :bookmark_type
+ AND fk IS NULL
+ )`);
+ fixFoldersAsBookmarks.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+ fixFoldersAsBookmarks.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER;
+ fixFoldersAsBookmarks.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ fixFoldersAsBookmarks.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ fixFoldersAsBookmarks.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ fixFoldersAsBookmarks.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ fixFoldersAsBookmarks.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(fixFoldersAsBookmarks);
+
+ // D.9 fix wrong parents
+ // Items cannot have separators or other bookmarks
+ // as parent, if they have bad parent move them to unsorted bookmarks.
+ let fixInvalidParents = DBConn.createAsyncStatement(
+ `UPDATE moz_bookmarks SET parent = :unsorted_folder WHERE guid NOT IN (
+ :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
+ ) AND id IN (
+ SELECT id FROM moz_bookmarks b
+ WHERE EXISTS
+ (SELECT id FROM moz_bookmarks WHERE id = b.parent
+ AND type IN (:bookmark_type, :separator_type)
+ LIMIT 1)
+ )`);
+ fixInvalidParents.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId;
+ fixInvalidParents.params["bookmark_type"] = PlacesUtils.bookmarks.TYPE_BOOKMARK;
+ fixInvalidParents.params["separator_type"] = PlacesUtils.bookmarks.TYPE_SEPARATOR;
+ fixInvalidParents.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
+ fixInvalidParents.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
+ fixInvalidParents.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
+ fixInvalidParents.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
+ fixInvalidParents.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
+ cleanupStatements.push(fixInvalidParents);
+
+ // D.10 recalculate positions
+ // This requires multiple related statements.
+ // We can detect a folder with bad position values comparing the sum of
+ // all distinct position values (+1 since position is 0-based) with the
+ // triangular numbers obtained by the number of children (n).
+ // SUM(DISTINCT position + 1) == (n * (n + 1) / 2).
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ `CREATE TEMP TABLE IF NOT EXISTS moz_bm_reindex_temp (
+ id INTEGER PRIMARY_KEY
+ , parent INTEGER
+ , position INTEGER
+ )`
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ `INSERT INTO moz_bm_reindex_temp
+ SELECT id, parent, 0
+ FROM moz_bookmarks b
+ WHERE parent IN (
+ SELECT parent
+ FROM moz_bookmarks
+ GROUP BY parent
+ HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0
+ )
+ ORDER BY parent ASC, position ASC, ROWID ASC`
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ `CREATE INDEX IF NOT EXISTS moz_bm_reindex_temp_index
+ ON moz_bm_reindex_temp(parent)`
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ `UPDATE moz_bm_reindex_temp SET position = (
+ ROWID - (SELECT MIN(t.ROWID) FROM moz_bm_reindex_temp t
+ WHERE t.parent = moz_bm_reindex_temp.parent)
+ )`
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_reindex_temp_trigger
+ BEFORE DELETE ON moz_bm_reindex_temp
+ FOR EACH ROW
+ BEGIN
+ UPDATE moz_bookmarks SET position = OLD.position WHERE id = OLD.id;
+ END`
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ "DELETE FROM moz_bm_reindex_temp "
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ "DROP INDEX moz_bm_reindex_temp_index "
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ "DROP TRIGGER moz_bm_reindex_temp_trigger "
+ ));
+ cleanupStatements.push(DBConn.createAsyncStatement(
+ "DROP TABLE moz_bm_reindex_temp "
+ ));
+
+ // D.12 Fix empty-named tags.
+ // Tags were allowed to have empty names due to a UI bug. Fix them
+ // replacing their title with "(notitle)".
+ let fixEmptyNamedTags = DBConn.createAsyncStatement(
+ `UPDATE moz_bookmarks SET title = :empty_title
+ WHERE length(title) = 0 AND type = :folder_type
+ AND parent = :tags_folder`
+ );
+ fixEmptyNamedTags.params["empty_title"] = "(notitle)";
+ fixEmptyNamedTags.params["folder_type"] = PlacesUtils.bookmarks.TYPE_FOLDER;
+ fixEmptyNamedTags.params["tags_folder"] = PlacesUtils.tagsFolderId;
+ cleanupStatements.push(fixEmptyNamedTags);
+
+ // MOZ_FAVICONS
+ // E.1 remove orphan icons
+ let deleteOrphanIcons = DBConn.createAsyncStatement(
+ `DELETE FROM moz_favicons WHERE id IN (
+ SELECT id FROM moz_favicons f
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE favicon_id = f.id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteOrphanIcons);
+
+ // MOZ_HISTORYVISITS
+ // F.1 remove orphan visits
+ let deleteOrphanVisits = DBConn.createAsyncStatement(
+ `DELETE FROM moz_historyvisits WHERE id IN (
+ SELECT id FROM moz_historyvisits v
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = v.place_id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteOrphanVisits);
+
+ // MOZ_INPUTHISTORY
+ // G.1 remove orphan input history
+ let deleteOrphanInputHistory = DBConn.createAsyncStatement(
+ `DELETE FROM moz_inputhistory WHERE place_id IN (
+ SELECT place_id FROM moz_inputhistory i
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = i.place_id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteOrphanInputHistory);
+
+ // MOZ_ITEMS_ANNOS
+ // H.1 remove item annos with an invalid attribute
+ let deleteInvalidAttributeItemsAnnos = DBConn.createAsyncStatement(
+ `DELETE FROM moz_items_annos WHERE id IN (
+ SELECT id FROM moz_items_annos t
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_anno_attributes
+ WHERE id = t.anno_attribute_id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteInvalidAttributeItemsAnnos);
+
+ // H.2 remove orphan item annos
+ let deleteOrphanItemsAnnos = DBConn.createAsyncStatement(
+ `DELETE FROM moz_items_annos WHERE id IN (
+ SELECT id FROM moz_items_annos t
+ WHERE NOT EXISTS
+ (SELECT id FROM moz_bookmarks WHERE id = t.item_id LIMIT 1)
+ )`);
+ cleanupStatements.push(deleteOrphanItemsAnnos);
+
+ // MOZ_KEYWORDS
+ // I.1 remove unused keywords
+ let deleteUnusedKeywords = DBConn.createAsyncStatement(
+ `DELETE FROM moz_keywords WHERE id IN (
+ SELECT id FROM moz_keywords k
+ WHERE NOT EXISTS
+ (SELECT 1 FROM moz_places h WHERE k.place_id = h.id)
+ )`);
+ cleanupStatements.push(deleteUnusedKeywords);
+
+ // MOZ_PLACES
+ // L.1 fix wrong favicon ids
+ let fixInvalidFaviconIds = DBConn.createAsyncStatement(
+ `UPDATE moz_places SET favicon_id = NULL WHERE id IN (
+ SELECT id FROM moz_places h
+ WHERE favicon_id NOT NULL
+ AND NOT EXISTS
+ (SELECT id FROM moz_favicons WHERE id = h.favicon_id LIMIT 1)
+ )`);
+ cleanupStatements.push(fixInvalidFaviconIds);
+
+ // L.2 recalculate visit_count and last_visit_date
+ let fixVisitStats = DBConn.createAsyncStatement(
+ `UPDATE moz_places
+ SET visit_count = (SELECT count(*) FROM moz_historyvisits
+ WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7,8,9)),
+ last_visit_date = (SELECT MAX(visit_date) FROM moz_historyvisits
+ WHERE place_id = moz_places.id)
+ WHERE id IN (
+ SELECT h.id FROM moz_places h
+ WHERE visit_count <> (SELECT count(*) FROM moz_historyvisits v
+ WHERE v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9))
+ OR last_visit_date <> (SELECT MAX(visit_date) FROM moz_historyvisits v
+ WHERE v.place_id = h.id)
+ )`);
+ cleanupStatements.push(fixVisitStats);
+
+ // L.3 recalculate hidden for redirects.
+ let fixRedirectsHidden = DBConn.createAsyncStatement(
+ `UPDATE moz_places
+ SET hidden = 1
+ WHERE id IN (
+ SELECT h.id FROM moz_places h
+ JOIN moz_historyvisits src ON src.place_id = h.id
+ JOIN moz_historyvisits dst ON dst.from_visit = src.id AND dst.visit_type IN (5,6)
+ LEFT JOIN moz_bookmarks on fk = h.id AND fk ISNULL
+ GROUP BY src.place_id HAVING count(*) = visit_count
+ )`);
+ cleanupStatements.push(fixRedirectsHidden);
+
+ // L.4 recalculate foreign_count.
+ let fixForeignCount = DBConn.createAsyncStatement(
+ `UPDATE moz_places SET foreign_count =
+ (SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id ) +
+ (SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id )`);
+ cleanupStatements.push(fixForeignCount);
+
+ // L.5 recalculate missing hashes.
+ let fixMissingHashes = DBConn.createAsyncStatement(
+ `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`);
+ cleanupStatements.push(fixMissingHashes);
+
+ // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT!
+
+ return cleanupStatements;
+ },
+
+ /**
+ * Tries to vacuum the database.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ vacuum: function PDBU_vacuum(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+ tasks.log("> Vacuum");
+
+ let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ DBFile.append("places.sqlite");
+ tasks.log("Initial database size is " +
+ parseInt(DBFile.fileSize / 1024) + " KiB");
+
+ let stmt = DBConn.createAsyncStatement("VACUUM");
+ stmt.executeAsync({
+ handleError: PlacesDBUtils._handleError,
+ handleResult: function () {},
+
+ handleCompletion: function (aReason)
+ {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ tasks.log("+ The database has been vacuumed");
+ let vacuumedDBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ vacuumedDBFile.append("places.sqlite");
+ tasks.log("Final database size is " +
+ parseInt(vacuumedDBFile.fileSize / 1024) + " KiB");
+ }
+ else {
+ tasks.log("- Unable to vacuum database");
+ tasks.clear();
+ }
+
+ PlacesDBUtils._executeTasks(tasks);
+ }
+ });
+ stmt.finalize();
+ },
+
+ /**
+ * Forces a full expiration on the database.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ expire: function PDBU_expire(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+ tasks.log("> Orphans expiration");
+
+ let expiration = Cc["@mozilla.org/places/expiration;1"].
+ getService(Ci.nsIObserver);
+
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ tasks.log("+ Database cleaned up");
+ PlacesDBUtils._executeTasks(tasks);
+ }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+
+ // Force an orphans expiration step.
+ expiration.observe(null, "places-debug-start-expiration", 0);
+ },
+
+ /**
+ * Collects statistical data on the database.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ stats: function PDBU_stats(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+ tasks.log("> Statistics");
+
+ let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ DBFile.append("places.sqlite");
+ tasks.log("Database size is " + parseInt(DBFile.fileSize / 1024) + " KiB");
+
+ [ "user_version"
+ , "page_size"
+ , "cache_size"
+ , "journal_mode"
+ , "synchronous"
+ ].forEach(function (aPragma) {
+ let stmt = DBConn.createStatement("PRAGMA " + aPragma);
+ stmt.executeStep();
+ tasks.log(aPragma + " is " + stmt.getString(0));
+ stmt.finalize();
+ });
+
+ // Get maximum number of unique URIs.
+ try {
+ let limitURIs = Services.prefs.getIntPref(
+ "places.history.expiration.transient_current_max_pages");
+ tasks.log("History can store a maximum of " + limitURIs + " unique pages");
+ } catch (ex) {}
+
+ let stmt = DBConn.createStatement(
+ "SELECT name FROM sqlite_master WHERE type = :type");
+ stmt.params.type = "table";
+ while (stmt.executeStep()) {
+ let tableName = stmt.getString(0);
+ let countStmt = DBConn.createStatement(
+ `SELECT count(*) FROM ${tableName}`);
+ countStmt.executeStep();
+ tasks.log("Table " + tableName + " has " + countStmt.getInt32(0) + " records");
+ countStmt.finalize();
+ }
+ stmt.reset();
+
+ stmt.params.type = "index";
+ while (stmt.executeStep()) {
+ tasks.log("Index " + stmt.getString(0));
+ }
+ stmt.reset();
+
+ stmt.params.type = "trigger";
+ while (stmt.executeStep()) {
+ tasks.log("Trigger " + stmt.getString(0));
+ }
+ stmt.finalize();
+
+ PlacesDBUtils._executeTasks(tasks);
+ },
+
+ /**
+ * Collects telemetry data and reports it to Telemetry.
+ *
+ * @param [optional] aTasks
+ * Tasks object to execute.
+ */
+ telemetry: function PDBU_telemetry(aTasks)
+ {
+ let tasks = new Tasks(aTasks);
+
+ // This will be populated with one integer property for each probe result,
+ // using the histogram name as key.
+ let probeValues = {};
+
+ // The following array contains an ordered list of entries that are
+ // processed to collect telemetry data. Each entry has these properties:
+ //
+ // histogram: Name of the telemetry histogram to update.
+ // query: This is optional. If present, contains a database command
+ // that will be executed asynchronously, and whose result will
+ // be added to the telemetry histogram.
+ // callback: This is optional. If present, contains a function that must
+ // return the value that will be added to the telemetry
+ // histogram. If a query is also present, its result is passed
+ // as the first argument of the function. If the function
+ // raises an exception, no data is added to the histogram.
+ //
+ // Since all queries are executed in order by the database backend, the
+ // callbacks can also use the result of previous queries stored in the
+ // probeValues object.
+ let probes = [
+ { histogram: "PLACES_PAGES_COUNT",
+ query: "SELECT count(*) FROM moz_places" },
+
+ { histogram: "PLACES_BOOKMARKS_COUNT",
+ query: `SELECT count(*) FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = b.parent
+ AND t.parent <> :tags_folder
+ WHERE b.type = :type_bookmark` },
+
+ { histogram: "PLACES_TAGS_COUNT",
+ query: `SELECT count(*) FROM moz_bookmarks
+ WHERE parent = :tags_folder` },
+
+ { histogram: "PLACES_KEYWORDS_COUNT",
+ query: "SELECT count(*) FROM moz_keywords" },
+
+ { histogram: "PLACES_SORTED_BOOKMARKS_PERC",
+ query: `SELECT IFNULL(ROUND((
+ SELECT count(*) FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = b.parent
+ AND t.parent <> :tags_folder AND t.parent > :places_root
+ WHERE b.type = :type_bookmark
+ ) * 100 / (
+ SELECT count(*) FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = b.parent
+ AND t.parent <> :tags_folder
+ WHERE b.type = :type_bookmark
+ )), 0)` },
+
+ { histogram: "PLACES_TAGGED_BOOKMARKS_PERC",
+ query: `SELECT IFNULL(ROUND((
+ SELECT count(*) FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = b.parent
+ AND t.parent = :tags_folder
+ ) * 100 / (
+ SELECT count(*) FROM moz_bookmarks b
+ JOIN moz_bookmarks t ON t.id = b.parent
+ AND t.parent <> :tags_folder
+ WHERE b.type = :type_bookmark
+ )), 0)` },
+
+ { histogram: "PLACES_DATABASE_FILESIZE_MB",
+ callback: function () {
+ let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ DBFile.append("places.sqlite");
+ return parseInt(DBFile.fileSize / BYTES_PER_MEBIBYTE);
+ }
+ },
+
+ { histogram: "PLACES_DATABASE_PAGESIZE_B",
+ query: "PRAGMA page_size /* PlacesDBUtils.jsm PAGESIZE_B */" },
+
+ { histogram: "PLACES_DATABASE_SIZE_PER_PAGE_B",
+ query: "PRAGMA page_count",
+ callback: function (aDbPageCount) {
+ // Note that the database file size would not be meaningful for this
+ // calculation, because the file grows in fixed-size chunks.
+ let dbPageSize = probeValues.PLACES_DATABASE_PAGESIZE_B;
+ let placesPageCount = probeValues.PLACES_PAGES_COUNT;
+ return Math.round((dbPageSize * aDbPageCount) / placesPageCount);
+ }
+ },
+
+ { histogram: "PLACES_ANNOS_BOOKMARKS_COUNT",
+ query: "SELECT count(*) FROM moz_items_annos" },
+
+ { histogram: "PLACES_ANNOS_PAGES_COUNT",
+ query: "SELECT count(*) FROM moz_annos" },
+
+ { histogram: "PLACES_MAINTENANCE_DAYSFROMLAST",
+ callback: function () {
+ try {
+ let lastMaintenance = Services.prefs.getIntPref("places.database.lastMaintenance");
+ let nowSeconds = parseInt(Date.now() / 1000);
+ return parseInt((nowSeconds - lastMaintenance) / 86400);
+ } catch (ex) {
+ return 60;
+ }
+ }
+ },
+ ];
+
+ let params = {
+ tags_folder: PlacesUtils.tagsFolderId,
+ type_folder: PlacesUtils.bookmarks.TYPE_FOLDER,
+ type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ places_root: PlacesUtils.placesRootId
+ };
+
+ for (let i = 0; i < probes.length; i++) {
+ let probe = probes[i];
+
+ let promiseDone = new Promise((resolve, reject) => {
+ if (!("query" in probe)) {
+ resolve([probe]);
+ return;
+ }
+
+ let stmt = DBConn.createAsyncStatement(probe.query);
+ for (let param in params) {
+ if (probe.query.indexOf(":" + param) > 0) {
+ stmt.params[param] = params[param];
+ }
+ }
+
+ try {
+ stmt.executeAsync({
+ handleError: reject,
+ handleResult: function (aResultSet) {
+ let row = aResultSet.getNextRow();
+ resolve([probe, row.getResultByIndex(0)]);
+ },
+ handleCompletion: function () {}
+ });
+ } finally {
+ stmt.finalize();
+ }
+ });
+
+ // Report the result of the probe through Telemetry.
+ // The resulting promise cannot reject.
+ promiseDone.then(
+ // On success
+ ([aProbe, aValue]) => {
+ let value = aValue;
+ try {
+ if ("callback" in aProbe) {
+ value = aProbe.callback(value);
+ }
+ probeValues[aProbe.histogram] = value;
+ Services.telemetry.getHistogramById(aProbe.histogram).add(value);
+ } catch (ex) {
+ Components.utils.reportError("Error adding value " + value +
+ " to histogram " + aProbe.histogram +
+ ": " + ex);
+ }
+ },
+ // On failure
+ this._handleError);
+ }
+
+ PlacesDBUtils._executeTasks(tasks);
+ },
+
+ /**
+ * Runs a list of tasks, notifying log messages to the callback.
+ *
+ * @param aTasks
+ * Array of tasks to be executed, in form of pointers to methods in
+ * this module.
+ * @param [optional] aCallback
+ * Callback to be invoked when done. It will receive an array of
+ * log messages.
+ */
+ runTasks: function PDBU_runTasks(aTasks, aCallback) {
+ let tasks = new Tasks(aTasks);
+ tasks.callback = aCallback;
+ PlacesDBUtils._executeTasks(tasks);
+ }
+};
+
+/**
+ * LIFO tasks stack.
+ *
+ * @param [optional] aTasks
+ * Array of tasks or another Tasks object to clone.
+ */
+function Tasks(aTasks)
+{
+ if (aTasks) {
+ if (Array.isArray(aTasks)) {
+ this._list = aTasks.slice(0, aTasks.length);
+ }
+ // This supports passing in a Tasks-like object, with a "list" property,
+ // for compatibility reasons.
+ else if (typeof(aTasks) == "object" &&
+ (Tasks instanceof Tasks || "list" in aTasks)) {
+ this._list = aTasks.list;
+ this._log = aTasks.messages;
+ this.callback = aTasks.callback;
+ this.scope = aTasks.scope;
+ this._telemetryStart = aTasks._telemetryStart;
+ }
+ }
+}
+
+Tasks.prototype = {
+ _list: [],
+ _log: [],
+ callback: null,
+ scope: null,
+ _telemetryStart: 0,
+
+ /**
+ * Adds a task to the top of the list.
+ *
+ * @param aNewElt
+ * Task to be added.
+ */
+ push: function T_push(aNewElt)
+ {
+ this._list.unshift(aNewElt);
+ },
+
+ /**
+ * Returns and consumes next task.
+ *
+ * @return next task or undefined if no task is left.
+ */
+ pop: function T_pop()
+ {
+ return this._list.shift();
+ },
+
+ /**
+ * Removes all tasks.
+ */
+ clear: function T_clear()
+ {
+ this._list.length = 0;
+ },
+
+ /**
+ * Returns array of tasks ordered from the next to be run to the latest.
+ */
+ get list()
+ {
+ return this._list.slice(0, this._list.length);
+ },
+
+ /**
+ * Adds a message to the log.
+ *
+ * @param aMsg
+ * String message to be added.
+ */
+ log: function T_log(aMsg)
+ {
+ this._log.push(aMsg);
+ },
+
+ /**
+ * Returns array of log messages ordered from oldest to newest.
+ */
+ get messages()
+ {
+ return this._log.slice(0, this._log.length);
+ },
+}
diff --git a/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm b/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm
new file mode 100644
index 0000000000..d23d5bc6e7
--- /dev/null
+++ b/toolkit/components/places/PlacesRemoteTabsAutocompleteProvider.jsm
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Provides functions to handle remote tabs (ie, tabs known by Sync) in
+ * the awesomebar.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PlacesRemoteTabsAutocompleteProvider"];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "weaveXPCService", function() {
+ return Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+});
+
+XPCOMUtils.defineLazyGetter(this, "Weave", () => {
+ try {
+ let {Weave} = Cu.import("resource://services-sync/main.js", {});
+ return Weave;
+ } catch (ex) {
+ // The app didn't build Sync.
+ }
+ return null;
+});
+
+// from MDN...
+function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+// Build the in-memory structure we use.
+function buildItems() {
+ let clients = new Map(); // keyed by client guid, value is client
+ let tabs = new Map(); // keyed by string URL, value is {clientId, tab}
+
+ // If Sync isn't initialized (either due to lag at startup or due to no user
+ // being signed in), don't reach in to Weave.Service as that may initialize
+ // Sync unnecessarily - we'll get an observer notification later when it
+ // becomes ready and has synced a list of tabs.
+ if (weaveXPCService.ready) {
+ let engine = Weave.Service.engineManager.get("tabs");
+
+ for (let [guid, client] of Object.entries(engine.getAllClients())) {
+ clients.set(guid, client);
+ for (let tab of client.tabs) {
+ let url = tab.urlHistory[0];
+ tabs.set(url, { clientId: guid, tab });
+ }
+ }
+ }
+ return { clients, tabs };
+}
+
+// Manage the cache of the items we use.
+// The cache itself.
+let _items = null;
+
+// Ensure the cache is good.
+function ensureItems() {
+ if (!_items) {
+ _items = buildItems();
+ }
+ return _items;
+}
+
+// A preference used to disable the showing of icons in remote tab records.
+const PREF_SHOW_REMOTE_ICONS = "services.sync.syncedTabs.showRemoteIcons";
+let showRemoteIcons;
+
+// An observer to invalidate _items and watch for changed prefs.
+function observe(subject, topic, data) {
+ switch (topic) {
+ case "weave:engine:sync:finish":
+ if (data == "tabs") {
+ // The tabs engine just finished syncing, so may have a different list
+ // of tabs then we previously cached.
+ _items = null;
+ }
+ break;
+
+ case "weave:service:start-over":
+ // Sync is being reset due to the user disconnecting - we must invalidate
+ // the cache so we don't supply tabs from a different user.
+ _items = null;
+ break;
+
+ case "nsPref:changed":
+ if (data == PREF_SHOW_REMOTE_ICONS) {
+ try {
+ showRemoteIcons = Services.prefs.getBoolPref(PREF_SHOW_REMOTE_ICONS);
+ } catch (_) {
+ showRemoteIcons = true; // no such pref - default is to show the icons.
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+Services.obs.addObserver(observe, "weave:engine:sync:finish", false);
+Services.obs.addObserver(observe, "weave:service:start-over", false);
+
+// Observe the pref for showing remote icons and prime our bool that reflects its value.
+Services.prefs.addObserver(PREF_SHOW_REMOTE_ICONS, observe, false);
+observe(null, "nsPref:changed", PREF_SHOW_REMOTE_ICONS);
+
+// This public object is a static singleton.
+this.PlacesRemoteTabsAutocompleteProvider = {
+ // a promise that resolves with an array of matching remote tabs.
+ getMatches(searchString) {
+ // If Sync isn't configured we bail early.
+ if (Weave === null ||
+ !Services.prefs.prefHasUserValue("services.sync.username")) {
+ return Promise.resolve([]);
+ }
+
+ let re = new RegExp(escapeRegExp(searchString), "i");
+ let matches = [];
+ let { tabs, clients } = ensureItems();
+ for (let [url, { clientId, tab }] of tabs) {
+ let title = tab.title;
+ if (url.match(re) || (title && title.match(re))) {
+ // lookup the client record.
+ let client = clients.get(clientId);
+ let icon = showRemoteIcons ? tab.icon : null;
+ // create the record we return for auto-complete.
+ let record = {
+ url, title, icon,
+ deviceClass: Weave.Service.clientsEngine.isMobile(clientId) ? "mobile" : "desktop",
+ deviceName: client.clientName,
+ };
+ matches.push(record);
+ }
+ }
+ return Promise.resolve(matches);
+ },
+}
diff --git a/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm b/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
new file mode 100644
index 0000000000..f4d8f39731
--- /dev/null
+++ b/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Provides functions to handle search engine URLs in the browser history.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "PlacesSearchAutocompleteProvider" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
+ "resource://gre/modules/SearchSuggestionController.jsm");
+
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+const SearchAutocompleteProviderInternal = {
+ /**
+ * Array of objects in the format returned by findMatchByToken.
+ */
+ priorityMatches: null,
+
+ /**
+ * Array of objects in the format returned by findMatchByAlias.
+ */
+ aliasMatches: null,
+
+ /**
+ * Object for the default search match.
+ **/
+ defaultMatch: null,
+
+ initialize: function () {
+ return new Promise((resolve, reject) => {
+ Services.search.init(status => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error("Unable to initialize search service."));
+ }
+
+ try {
+ // The initial loading of the search engines must succeed.
+ this._refresh();
+
+ Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
+
+ this.initialized = true;
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ },
+
+ initialized: false,
+
+ observe: function (subject, topic, data) {
+ switch (data) {
+ case "engine-added":
+ case "engine-changed":
+ case "engine-removed":
+ case "engine-current":
+ this._refresh();
+ }
+ },
+
+ _refresh: function () {
+ this.priorityMatches = [];
+ this.aliasMatches = [];
+ this.defaultMatch = null;
+
+ let currentEngine = Services.search.currentEngine;
+ // This can be null in XCPShell.
+ if (currentEngine) {
+ this.defaultMatch = {
+ engineName: currentEngine.name,
+ iconUrl: currentEngine.iconURI ? currentEngine.iconURI.spec : null,
+ }
+ }
+
+ // The search engines will always be processed in the order returned by the
+ // search service, which can be defined by the user.
+ Services.search.getVisibleEngines().forEach(e => this._addEngine(e));
+ },
+
+ _addEngine: function (engine) {
+ if (engine.alias) {
+ this.aliasMatches.push({
+ alias: engine.alias,
+ engineName: engine.name,
+ iconUrl: engine.iconURI ? engine.iconURI.spec : null,
+ });
+ }
+
+ let domain = engine.getResultDomain();
+ if (domain) {
+ this.priorityMatches.push({
+ token: domain,
+ // The searchForm property returns a simple URL for the search engine, but
+ // we may need an URL which includes an affiliate code (bug 990799).
+ url: engine.searchForm,
+ engineName: engine.name,
+ iconUrl: engine.iconURI ? engine.iconURI.spec : null,
+ });
+ }
+ },
+
+ getSuggestionController(searchToken, inPrivateContext, maxResults, userContextId) {
+ let engine = Services.search.currentEngine;
+ if (!engine) {
+ return null;
+ }
+ return new SearchSuggestionControllerWrapper(engine, searchToken,
+ inPrivateContext, maxResults,
+ userContextId);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+}
+
+function SearchSuggestionControllerWrapper(engine, searchToken,
+ inPrivateContext, maxResults,
+ userContextId) {
+ this._controller = new SearchSuggestionController();
+ this._controller.maxLocalResults = 0;
+ this._controller.maxRemoteResults = maxResults;
+ let promise = this._controller.fetch(searchToken, inPrivateContext, engine, userContextId);
+ this._suggestions = [];
+ this._success = false;
+ this._promise = promise.then(results => {
+ this._success = true;
+ this._suggestions = (results ? results.remote : null) || [];
+ }).catch(err => {
+ // fetch() rejects its promise if there's a pending request.
+ });
+}
+
+SearchSuggestionControllerWrapper.prototype = {
+
+ /**
+ * Resolved when all suggestions have been fetched.
+ */
+ get fetchCompletePromise() {
+ return this._promise;
+ },
+
+ /**
+ * Returns one suggestion, if any are available. The returned value is an
+ * array [match, suggestion]. If none are available, returns [null, null].
+ * Note that there are two reasons that suggestions might not be available:
+ * all suggestions may have been fetched and consumed, or the fetch may not
+ * have completed yet.
+ *
+ * @return An array [match, suggestion].
+ */
+ consume() {
+ return !this._suggestions.length ? [null, null] :
+ [SearchAutocompleteProviderInternal.defaultMatch,
+ this._suggestions.shift()];
+ },
+
+ /**
+ * Returns the number of fetched suggestions, or -1 if the fetching was
+ * incomplete or failed.
+ */
+ get resultsCount() {
+ return this._success ? this._suggestions.length : -1;
+ },
+
+ /**
+ * Stops the fetch.
+ */
+ stop() {
+ this._controller.stop();
+ },
+};
+
+var gInitializationPromise = null;
+
+this.PlacesSearchAutocompleteProvider = Object.freeze({
+ /**
+ * Starts initializing the component and returns a promise that is resolved or
+ * rejected when initialization finished. The same promise is returned if
+ * this function is called multiple times.
+ */
+ ensureInitialized: function () {
+ if (!gInitializationPromise) {
+ gInitializationPromise = SearchAutocompleteProviderInternal.initialize();
+ }
+ return gInitializationPromise;
+ },
+
+ /**
+ * Matches a given string to an item that should be included by URL search
+ * components, like autocomplete in the address bar.
+ *
+ * @param searchToken
+ * String containing the first part of the matching domain name.
+ *
+ * @return An object with the following properties, or undefined if the token
+ * does not match any relevant URL:
+ * {
+ * token: The full string used to match the search term to the URL.
+ * url: The URL to navigate to if the match is selected.
+ * engineName: The display name of the search engine.
+ * iconUrl: Icon associated to the match, or null if not available.
+ * }
+ */
+ findMatchByToken: Task.async(function* (searchToken) {
+ yield this.ensureInitialized();
+
+ // Match at the beginning for now. In the future, an "options" argument may
+ // allow the matching behavior to be tuned.
+ return SearchAutocompleteProviderInternal.priorityMatches
+ .find(m => m.token.startsWith(searchToken));
+ }),
+
+ /**
+ * Matches a given search string to an item that should be included by
+ * components wishing to search using search engine aliases, like
+ * autocomple.
+ *
+ * @param searchToken
+ * Search string to match exactly a search engine alias.
+ *
+ * @return An object with the following properties, or undefined if the token
+ * does not match any relevant URL:
+ * {
+ * alias: The matched search engine's alias.
+ * engineName: The display name of the search engine.
+ * iconUrl: Icon associated to the match, or null if not available.
+ * }
+ */
+ findMatchByAlias: Task.async(function* (searchToken) {
+ yield this.ensureInitialized();
+
+ return SearchAutocompleteProviderInternal.aliasMatches
+ .find(m => m.alias.toLocaleLowerCase() == searchToken.toLocaleLowerCase());
+ }),
+
+ getDefaultMatch: Task.async(function* () {
+ yield this.ensureInitialized();
+
+ return SearchAutocompleteProviderInternal.defaultMatch;
+ }),
+
+ /**
+ * Synchronously determines if the provided URL represents results from a
+ * search engine, and provides details about the match.
+ *
+ * @param url
+ * String containing the URL to parse.
+ *
+ * @return An object with the following properties, or null if the URL does
+ * not represent a search result:
+ * {
+ * engineName: The display name of the search engine.
+ * terms: The originally sought terms extracted from the URI.
+ * }
+ *
+ * @remarks The asynchronous ensureInitialized function must be called before
+ * this synchronous method can be used.
+ *
+ * @note This API function needs to be synchronous because it is called inside
+ * a row processing callback of Sqlite.jsm, in UnifiedComplete.js.
+ */
+ parseSubmissionURL: function (url) {
+ if (!SearchAutocompleteProviderInternal.initialized) {
+ throw new Error("The component has not been initialized.");
+ }
+
+ let parseUrlResult = Services.search.parseSubmissionURL(url);
+ return parseUrlResult.engine && {
+ engineName: parseUrlResult.engine.name,
+ terms: parseUrlResult.terms,
+ };
+ },
+
+ getSuggestionController(searchToken, inPrivateContext, maxResults, userContextId) {
+ if (!SearchAutocompleteProviderInternal.initialized) {
+ throw new Error("The component has not been initialized.");
+ }
+ return SearchAutocompleteProviderInternal.getSuggestionController(
+ searchToken, inPrivateContext, maxResults, userContextId);
+ },
+});
diff --git a/toolkit/components/places/PlacesSyncUtils.jsm b/toolkit/components/places/PlacesSyncUtils.jsm
new file mode 100644
index 0000000000..15dd412e88
--- /dev/null
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -0,0 +1,1155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["PlacesSyncUtils"];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL", "URLSearchParams"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * This module exports functions for Sync to use when applying remote
+ * records. The calls are similar to those in `Bookmarks.jsm` and
+ * `nsINavBookmarksService`, with special handling for smart bookmarks,
+ * tags, keywords, synced annotations, and missing parents.
+ */
+var PlacesSyncUtils = {};
+
+const { SOURCE_SYNC } = Ci.nsINavBookmarksService;
+
+// These are defined as lazy getters to defer initializing the bookmarks
+// service until it's needed.
+XPCOMUtils.defineLazyGetter(this, "ROOT_SYNC_ID_TO_GUID", () => ({
+ menu: PlacesUtils.bookmarks.menuGuid,
+ places: PlacesUtils.bookmarks.rootGuid,
+ tags: PlacesUtils.bookmarks.tagsGuid,
+ toolbar: PlacesUtils.bookmarks.toolbarGuid,
+ unfiled: PlacesUtils.bookmarks.unfiledGuid,
+ mobile: PlacesUtils.bookmarks.mobileGuid,
+}));
+
+XPCOMUtils.defineLazyGetter(this, "ROOT_GUID_TO_SYNC_ID", () => ({
+ [PlacesUtils.bookmarks.menuGuid]: "menu",
+ [PlacesUtils.bookmarks.rootGuid]: "places",
+ [PlacesUtils.bookmarks.tagsGuid]: "tags",
+ [PlacesUtils.bookmarks.toolbarGuid]: "toolbar",
+ [PlacesUtils.bookmarks.unfiledGuid]: "unfiled",
+ [PlacesUtils.bookmarks.mobileGuid]: "mobile",
+}));
+
+XPCOMUtils.defineLazyGetter(this, "ROOTS", () =>
+ Object.keys(ROOT_SYNC_ID_TO_GUID)
+);
+
+const BookmarkSyncUtils = PlacesSyncUtils.bookmarks = Object.freeze({
+ SMART_BOOKMARKS_ANNO: "Places/SmartBookmark",
+ DESCRIPTION_ANNO: "bookmarkProperties/description",
+ SIDEBAR_ANNO: "bookmarkProperties/loadInSidebar",
+ SYNC_PARENT_ANNO: "sync/parent",
+ SYNC_MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
+
+ KINDS: {
+ BOOKMARK: "bookmark",
+ // Microsummaries were removed from Places in bug 524091. For now, Sync
+ // treats them identically to bookmarks. Bug 745410 tracks removing them
+ // entirely.
+ MICROSUMMARY: "microsummary",
+ QUERY: "query",
+ FOLDER: "folder",
+ LIVEMARK: "livemark",
+ SEPARATOR: "separator",
+ },
+
+ get ROOTS() {
+ return ROOTS;
+ },
+
+ /**
+ * Converts a Places GUID to a Sync ID. Sync IDs are identical to Places
+ * GUIDs for all items except roots.
+ */
+ guidToSyncId(guid) {
+ return ROOT_GUID_TO_SYNC_ID[guid] || guid;
+ },
+
+ /**
+ * Converts a Sync record ID to a Places GUID.
+ */
+ syncIdToGuid(syncId) {
+ return ROOT_SYNC_ID_TO_GUID[syncId] || syncId;
+ },
+
+ /**
+ * Fetches the sync IDs for a folder's children, ordered by their position
+ * within the folder.
+ */
+ fetchChildSyncIds: Task.async(function* (parentSyncId) {
+ PlacesUtils.SYNC_BOOKMARK_VALIDATORS.syncId(parentSyncId);
+ let parentGuid = BookmarkSyncUtils.syncIdToGuid(parentSyncId);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ let children = yield fetchAllChildren(db, parentGuid);
+ return children.map(child =>
+ BookmarkSyncUtils.guidToSyncId(child.guid)
+ );
+ }),
+
+ /**
+ * Reorders a folder's children, based on their order in the array of sync
+ * IDs.
+ *
+ * Sync uses this method to reorder all synced children after applying all
+ * incoming records.
+ *
+ */
+ order: Task.async(function* (parentSyncId, childSyncIds) {
+ PlacesUtils.SYNC_BOOKMARK_VALIDATORS.syncId(parentSyncId);
+ if (!childSyncIds.length) {
+ return undefined;
+ }
+ let parentGuid = BookmarkSyncUtils.syncIdToGuid(parentSyncId);
+ if (parentGuid == PlacesUtils.bookmarks.rootGuid) {
+ // Reordering roots doesn't make sense, but Sync will do this on the
+ // first sync.
+ return undefined;
+ }
+ let orderedChildrenGuids = childSyncIds.map(BookmarkSyncUtils.syncIdToGuid);
+ return PlacesUtils.bookmarks.reorder(parentGuid, orderedChildrenGuids,
+ { source: SOURCE_SYNC });
+ }),
+
+ /**
+ * Removes an item from the database. Options are passed through to
+ * PlacesUtils.bookmarks.remove.
+ */
+ remove: Task.async(function* (syncId, options = {}) {
+ let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
+ if (guid in ROOT_GUID_TO_SYNC_ID) {
+ BookmarkSyncLog.warn(`remove: Refusing to remove root ${syncId}`);
+ return null;
+ }
+ return PlacesUtils.bookmarks.remove(guid, Object.assign({}, options, {
+ source: SOURCE_SYNC,
+ }));
+ }),
+
+ /**
+ * Returns true for sync IDs that are considered roots.
+ */
+ isRootSyncID(syncID) {
+ return ROOT_SYNC_ID_TO_GUID.hasOwnProperty(syncID);
+ },
+
+ /**
+ * Changes the GUID of an existing item. This method only allows Places GUIDs
+ * because root sync IDs cannot be changed.
+ *
+ * @return {Promise} resolved once the GUID has been changed.
+ * @resolves to the new GUID.
+ * @rejects if the old GUID does not exist.
+ */
+ changeGuid: Task.async(function* (oldGuid, newGuid) {
+ PlacesUtils.BOOKMARK_VALIDATORS.guid(oldGuid);
+ PlacesUtils.BOOKMARK_VALIDATORS.guid(newGuid);
+
+ let itemId = yield PlacesUtils.promiseItemId(oldGuid);
+ if (PlacesUtils.isRootItem(itemId)) {
+ throw new Error(`Cannot change GUID of Places root ${oldGuid}`);
+ }
+ return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: changeGuid",
+ Task.async(function* (db) {
+ yield db.executeCached(`UPDATE moz_bookmarks SET guid = :newGuid
+ WHERE id = :itemId`, { newGuid, itemId });
+ PlacesUtils.invalidateCachedGuidFor(itemId);
+ return newGuid;
+ })
+ );
+ }),
+
+ /**
+ * Updates a bookmark with synced properties. Only Sync should call this
+ * method; other callers should use `Bookmarks.update`.
+ *
+ * The following properties are supported:
+ * - kind: Optional.
+ * - guid: Required.
+ * - parentGuid: Optional; reparents the bookmark if specified.
+ * - title: Optional.
+ * - url: Optional.
+ * - tags: Optional; replaces all existing tags.
+ * - keyword: Optional.
+ * - description: Optional.
+ * - loadInSidebar: Optional.
+ * - query: Optional.
+ *
+ * @param info
+ * object representing a bookmark-item, as defined above.
+ *
+ * @return {Promise} resolved when the update is complete.
+ * @resolves to an object representing the updated bookmark.
+ * @rejects if it's not possible to update the given bookmark.
+ * @throws if the arguments are invalid.
+ */
+ update: Task.async(function* (info) {
+ let updateInfo = validateSyncBookmarkObject(info,
+ { syncId: { required: true }
+ });
+
+ return updateSyncBookmark(updateInfo);
+ }),
+
+ /**
+ * Inserts a synced bookmark into the tree. Only Sync should call this
+ * method; other callers should use `Bookmarks.insert`.
+ *
+ * The following properties are supported:
+ * - kind: Required.
+ * - guid: Required.
+ * - parentGuid: Required.
+ * - url: Required for bookmarks.
+ * - query: A smart bookmark query string, optional.
+ * - tags: An optional array of tag strings.
+ * - keyword: An optional keyword string.
+ * - description: An optional description string.
+ * - loadInSidebar: An optional boolean; defaults to false.
+ *
+ * Sync doesn't set the index, since it appends and reorders children
+ * after applying all incoming items.
+ *
+ * @param info
+ * object representing a synced bookmark.
+ *
+ * @return {Promise} resolved when the creation is complete.
+ * @resolves to an object representing the created bookmark.
+ * @rejects if it's not possible to create the requested bookmark.
+ * @throws if the arguments are invalid.
+ */
+ insert: Task.async(function* (info) {
+ let insertInfo = validateNewBookmark(info);
+ return insertSyncBookmark(insertInfo);
+ }),
+
+ /**
+ * Fetches a Sync bookmark object for an item in the tree. The object contains
+ * the following properties, depending on the item's kind:
+ *
+ * - kind (all): A string representing the item's kind.
+ * - syncId (all): The item's sync ID.
+ * - parentSyncId (all): The sync ID of the item's parent.
+ * - parentTitle (all): The title of the item's parent, used for de-duping.
+ * Omitted for the Places root and parents with empty titles.
+ * - title ("bookmark", "folder", "livemark", "query"): The item's title.
+ * Omitted if empty.
+ * - url ("bookmark", "query"): The item's URL.
+ * - tags ("bookmark", "query"): An array containing the item's tags.
+ * - keyword ("bookmark"): The bookmark's keyword, if one exists.
+ * - description ("bookmark", "folder", "livemark"): The item's description.
+ * Omitted if one isn't set.
+ * - loadInSidebar ("bookmark", "query"): Whether to load the bookmark in
+ * the sidebar. Always `false` for queries.
+ * - feed ("livemark"): A `URL` object pointing to the livemark's feed URL.
+ * - site ("livemark"): A `URL` object pointing to the livemark's site URL,
+ * or `null` if one isn't set.
+ * - childSyncIds ("folder"): An array containing the sync IDs of the item's
+ * children, used to determine child order.
+ * - folder ("query"): The tag folder name, if this is a tag query.
+ * - query ("query"): The smart bookmark query name, if this is a smart
+ * bookmark.
+ * - index ("separator"): The separator's position within its parent.
+ */
+ fetch: Task.async(function* (syncId) {
+ let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
+ let bookmarkItem = yield PlacesUtils.bookmarks.fetch(guid);
+ if (!bookmarkItem) {
+ return null;
+ }
+
+ // Convert the Places bookmark object to a Sync bookmark and add
+ // kind-specific properties. Titles are required for bookmarks,
+ // folders, and livemarks; optional for queries, and omitted for
+ // separators.
+ let kind = yield getKindForItem(bookmarkItem);
+ let item;
+ switch (kind) {
+ case BookmarkSyncUtils.KINDS.BOOKMARK:
+ case BookmarkSyncUtils.KINDS.MICROSUMMARY:
+ item = yield fetchBookmarkItem(bookmarkItem);
+ break;
+
+ case BookmarkSyncUtils.KINDS.QUERY:
+ item = yield fetchQueryItem(bookmarkItem);
+ break;
+
+ case BookmarkSyncUtils.KINDS.FOLDER:
+ item = yield fetchFolderItem(bookmarkItem);
+ break;
+
+ case BookmarkSyncUtils.KINDS.LIVEMARK:
+ item = yield fetchLivemarkItem(bookmarkItem);
+ break;
+
+ case BookmarkSyncUtils.KINDS.SEPARATOR:
+ item = yield placesBookmarkToSyncBookmark(bookmarkItem);
+ item.index = bookmarkItem.index;
+ break;
+
+ default:
+ throw new Error(`Unknown bookmark kind: ${kind}`);
+ }
+
+ // Sync uses the parent title for de-duping. All Sync bookmark objects
+ // except the Places root should have this property.
+ if (bookmarkItem.parentGuid) {
+ let parent = yield PlacesUtils.bookmarks.fetch(bookmarkItem.parentGuid);
+ item.parentTitle = parent.title || "";
+ }
+
+ return item;
+ }),
+
+ /**
+ * Get the sync record kind for the record with provided sync id.
+ *
+ * @param syncId
+ * Sync ID for the item in question
+ *
+ * @returns {Promise} A promise that resolves with the sync record kind (e.g.
+ * something under `PlacesSyncUtils.bookmarks.KIND`), or
+ * with `null` if no item with that guid exists.
+ * @throws if `guid` is invalid.
+ */
+ getKindForSyncId(syncId) {
+ PlacesUtils.SYNC_BOOKMARK_VALIDATORS.syncId(syncId);
+ let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
+ return PlacesUtils.bookmarks.fetch(guid)
+ .then(item => {
+ if (!item) {
+ return null;
+ }
+ return getKindForItem(item)
+ });
+ },
+});
+
+XPCOMUtils.defineLazyGetter(this, "BookmarkSyncLog", () => {
+ return Log.repository.getLogger("BookmarkSyncUtils");
+});
+
+function validateSyncBookmarkObject(input, behavior) {
+ return PlacesUtils.validateItemProperties(
+ PlacesUtils.SYNC_BOOKMARK_VALIDATORS, input, behavior);
+}
+
+// Similar to the private `fetchBookmarksByParent` implementation in
+// `Bookmarks.jsm`.
+var fetchAllChildren = Task.async(function* (db, parentGuid) {
+ let rows = yield db.executeCached(`
+ SELECT id, parent, position, type, guid
+ FROM moz_bookmarks
+ WHERE parent = (
+ SELECT id FROM moz_bookmarks WHERE guid = :parentGuid
+ )
+ ORDER BY position`,
+ { parentGuid }
+ );
+ return rows.map(row => ({
+ id: row.getResultByName("id"),
+ parentId: row.getResultByName("parent"),
+ index: row.getResultByName("position"),
+ type: row.getResultByName("type"),
+ guid: row.getResultByName("guid"),
+ }));
+});
+
+// A helper for whenever we want to know if a GUID doesn't exist in the places
+// database. Primarily used to detect orphans on incoming records.
+var GUIDMissing = Task.async(function* (guid) {
+ try {
+ yield PlacesUtils.promiseItemId(guid);
+ return false;
+ } catch (ex) {
+ if (ex.message == "no item found for the given GUID") {
+ return true;
+ }
+ throw ex;
+ }
+});
+
+// Tag queries use a `place:` URL that refers to the tag folder ID. When we
+// apply a synced tag query from a remote client, we need to update the URL to
+// point to the local tag folder.
+var updateTagQueryFolder = Task.async(function* (info) {
+ if (info.kind != BookmarkSyncUtils.KINDS.QUERY || !info.folder || !info.url ||
+ info.url.protocol != "place:") {
+ return info;
+ }
+
+ let params = new URLSearchParams(info.url.pathname);
+ let type = +params.get("type");
+
+ if (type != Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS) {
+ return info;
+ }
+
+ let id = yield getOrCreateTagFolder(info.folder);
+ BookmarkSyncLog.debug(`updateTagQueryFolder: Tag query folder: ${
+ info.folder} = ${id}`);
+
+ // Rewrite the query to reference the new ID.
+ params.set("folder", id);
+ info.url = new URL(info.url.protocol + params);
+
+ return info;
+});
+
+var annotateOrphan = Task.async(function* (item, requestedParentSyncId) {
+ let guid = BookmarkSyncUtils.syncIdToGuid(item.syncId);
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.SYNC_PARENT_ANNO, requestedParentSyncId, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+});
+
+var reparentOrphans = Task.async(function* (item) {
+ if (item.kind != BookmarkSyncUtils.KINDS.FOLDER) {
+ return;
+ }
+ let orphanGuids = yield fetchGuidsWithAnno(BookmarkSyncUtils.SYNC_PARENT_ANNO,
+ item.syncId);
+ let folderGuid = BookmarkSyncUtils.syncIdToGuid(item.syncId);
+ BookmarkSyncLog.debug(`reparentOrphans: Reparenting ${
+ JSON.stringify(orphanGuids)} to ${item.syncId}`);
+ for (let i = 0; i < orphanGuids.length; ++i) {
+ let isReparented = false;
+ try {
+ // Reparenting can fail if we have a corrupted or incomplete tree
+ // where an item's parent is one of its descendants.
+ BookmarkSyncLog.trace(`reparentOrphans: Attempting to move item ${
+ orphanGuids[i]} to new parent ${item.syncId}`);
+ yield PlacesUtils.bookmarks.update({
+ guid: orphanGuids[i],
+ parentGuid: folderGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ source: SOURCE_SYNC,
+ });
+ isReparented = true;
+ } catch (ex) {
+ BookmarkSyncLog.error(`reparentOrphans: Failed to reparent item ${
+ orphanGuids[i]} to ${item.syncId}`, ex);
+ }
+ if (isReparented) {
+ // Remove the annotation once we've reparented the item.
+ let orphanId = yield PlacesUtils.promiseItemId(orphanGuids[i]);
+ PlacesUtils.annotations.removeItemAnnotation(orphanId,
+ BookmarkSyncUtils.SYNC_PARENT_ANNO, SOURCE_SYNC);
+ }
+ }
+});
+
+// Inserts a synced bookmark into the database.
+var insertSyncBookmark = Task.async(function* (insertInfo) {
+ let requestedParentSyncId = insertInfo.parentSyncId;
+ let requestedParentGuid =
+ BookmarkSyncUtils.syncIdToGuid(insertInfo.parentSyncId);
+ let isOrphan = yield GUIDMissing(requestedParentGuid);
+
+ // Default to "unfiled" for new bookmarks if the parent doesn't exist.
+ if (!isOrphan) {
+ BookmarkSyncLog.debug(`insertSyncBookmark: Item ${
+ insertInfo.syncId} is not an orphan`);
+ } else {
+ BookmarkSyncLog.debug(`insertSyncBookmark: Item ${
+ insertInfo.syncId} is an orphan: parent ${
+ insertInfo.parentSyncId} doesn't exist; reparenting to unfiled`);
+ insertInfo.parentSyncId = "unfiled";
+ }
+
+ // If we're inserting a tag query, make sure the tag exists and fix the
+ // folder ID to refer to the local tag folder.
+ insertInfo = yield updateTagQueryFolder(insertInfo);
+
+ let newItem;
+ if (insertInfo.kind == BookmarkSyncUtils.KINDS.LIVEMARK) {
+ newItem = yield insertSyncLivemark(insertInfo);
+ } else {
+ let bookmarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
+ let bookmarkItem = yield PlacesUtils.bookmarks.insert(bookmarkInfo);
+ newItem = yield insertBookmarkMetadata(bookmarkItem, insertInfo);
+ }
+
+ if (!newItem) {
+ return null;
+ }
+
+ // If the item is an orphan, annotate it with its real parent sync ID.
+ if (isOrphan) {
+ yield annotateOrphan(newItem, requestedParentSyncId);
+ }
+
+ // Reparent all orphans that expect this folder as the parent.
+ yield reparentOrphans(newItem);
+
+ return newItem;
+});
+
+// Inserts a synced livemark.
+var insertSyncLivemark = Task.async(function* (insertInfo) {
+ if (!insertInfo.feed) {
+ BookmarkSyncLog.debug(`insertSyncLivemark: ${
+ insertInfo.syncId} missing feed URL`);
+ return null;
+ }
+ let livemarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
+ let parentIsLivemark = yield getAnno(livemarkInfo.parentGuid,
+ PlacesUtils.LMANNO_FEEDURI);
+ if (parentIsLivemark) {
+ // A livemark can't be a descendant of another livemark.
+ BookmarkSyncLog.debug(`insertSyncLivemark: Invalid parent ${
+ insertInfo.parentSyncId}; skipping livemark record ${
+ insertInfo.syncId}`);
+ return null;
+ }
+
+ let livemarkItem = yield PlacesUtils.livemarks.addLivemark(livemarkInfo);
+
+ return insertBookmarkMetadata(livemarkItem, insertInfo);
+});
+
+// Sets annotations, keywords, and tags on a new bookmark. Returns a Sync
+// bookmark object.
+var insertBookmarkMetadata = Task.async(function* (bookmarkItem, insertInfo) {
+ let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
+ let newItem = yield placesBookmarkToSyncBookmark(bookmarkItem);
+
+ if (insertInfo.query) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.SMART_BOOKMARKS_ANNO, insertInfo.query, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+ newItem.query = insertInfo.query;
+ }
+
+ try {
+ newItem.tags = yield tagItem(bookmarkItem, insertInfo.tags);
+ } catch (ex) {
+ BookmarkSyncLog.warn(`insertBookmarkMetadata: Error tagging item ${
+ insertInfo.syncId}`, ex);
+ }
+
+ if (insertInfo.keyword) {
+ yield PlacesUtils.keywords.insert({
+ keyword: insertInfo.keyword,
+ url: bookmarkItem.url.href,
+ source: SOURCE_SYNC,
+ });
+ newItem.keyword = insertInfo.keyword;
+ }
+
+ if (insertInfo.description) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.DESCRIPTION_ANNO, insertInfo.description, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+ newItem.description = insertInfo.description;
+ }
+
+ if (insertInfo.loadInSidebar) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.SIDEBAR_ANNO, insertInfo.loadInSidebar, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+ newItem.loadInSidebar = insertInfo.loadInSidebar;
+ }
+
+ return newItem;
+});
+
+// Determines the Sync record kind for an existing bookmark.
+var getKindForItem = Task.async(function* (item) {
+ switch (item.type) {
+ case PlacesUtils.bookmarks.TYPE_FOLDER: {
+ let isLivemark = yield getAnno(item.guid,
+ PlacesUtils.LMANNO_FEEDURI);
+ return isLivemark ? BookmarkSyncUtils.KINDS.LIVEMARK :
+ BookmarkSyncUtils.KINDS.FOLDER;
+ }
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ return item.url.protocol == "place:" ?
+ BookmarkSyncUtils.KINDS.QUERY :
+ BookmarkSyncUtils.KINDS.BOOKMARK;
+
+ case PlacesUtils.bookmarks.TYPE_SEPARATOR:
+ return BookmarkSyncUtils.KINDS.SEPARATOR;
+ }
+ return null;
+});
+
+// Returns the `nsINavBookmarksService` bookmark type constant for a Sync
+// record kind.
+function getTypeForKind(kind) {
+ switch (kind) {
+ case BookmarkSyncUtils.KINDS.BOOKMARK:
+ case BookmarkSyncUtils.KINDS.MICROSUMMARY:
+ case BookmarkSyncUtils.KINDS.QUERY:
+ return PlacesUtils.bookmarks.TYPE_BOOKMARK;
+
+ case BookmarkSyncUtils.KINDS.FOLDER:
+ case BookmarkSyncUtils.KINDS.LIVEMARK:
+ return PlacesUtils.bookmarks.TYPE_FOLDER;
+
+ case BookmarkSyncUtils.KINDS.SEPARATOR:
+ return PlacesUtils.bookmarks.TYPE_SEPARATOR;
+ }
+ throw new Error(`Unknown bookmark kind: ${kind}`);
+}
+
+// Determines if a livemark should be reinserted. Returns true if `updateInfo`
+// specifies different feed or site URLs; false otherwise.
+var shouldReinsertLivemark = Task.async(function* (updateInfo) {
+ let hasFeed = updateInfo.hasOwnProperty("feed");
+ let hasSite = updateInfo.hasOwnProperty("site");
+ if (!hasFeed && !hasSite) {
+ return false;
+ }
+ let guid = BookmarkSyncUtils.syncIdToGuid(updateInfo.syncId);
+ let livemark = yield PlacesUtils.livemarks.getLivemark({
+ guid,
+ });
+ if (hasFeed) {
+ let feedURI = PlacesUtils.toURI(updateInfo.feed);
+ if (!livemark.feedURI.equals(feedURI)) {
+ return true;
+ }
+ }
+ if (hasSite) {
+ if (!updateInfo.site) {
+ return !!livemark.siteURI;
+ }
+ let siteURI = PlacesUtils.toURI(updateInfo.site);
+ if (!livemark.siteURI || !siteURI.equals(livemark.siteURI)) {
+ return true;
+ }
+ }
+ return false;
+});
+
+var updateSyncBookmark = Task.async(function* (updateInfo) {
+ let guid = BookmarkSyncUtils.syncIdToGuid(updateInfo.syncId);
+ let oldBookmarkItem = yield PlacesUtils.bookmarks.fetch(guid);
+ if (!oldBookmarkItem) {
+ throw new Error(`Bookmark with sync ID ${
+ updateInfo.syncId} does not exist`);
+ }
+
+ let shouldReinsert = false;
+ let oldKind = yield getKindForItem(oldBookmarkItem);
+ if (updateInfo.hasOwnProperty("kind") && updateInfo.kind != oldKind) {
+ // If the item's aren't the same kind, we can't update the record;
+ // we must remove and reinsert.
+ shouldReinsert = true;
+ if (BookmarkSyncLog.level <= Log.Level.Warn) {
+ let oldSyncId = BookmarkSyncUtils.guidToSyncId(oldBookmarkItem.guid);
+ BookmarkSyncLog.warn(`updateSyncBookmark: Local ${
+ oldSyncId} kind = ${oldKind}; remote ${
+ updateInfo.syncId} kind = ${
+ updateInfo.kind}. Deleting and recreating`);
+ }
+ } else if (oldKind == BookmarkSyncUtils.KINDS.LIVEMARK) {
+ // Similarly, if we're changing a livemark's site or feed URL, we need to
+ // reinsert.
+ shouldReinsert = yield shouldReinsertLivemark(updateInfo);
+ if (BookmarkSyncLog.level <= Log.Level.Debug) {
+ let oldSyncId = BookmarkSyncUtils.guidToSyncId(oldBookmarkItem.guid);
+ BookmarkSyncLog.debug(`updateSyncBookmark: Local ${
+ oldSyncId} and remote ${
+ updateInfo.syncId} livemarks have different URLs`);
+ }
+ }
+
+ if (shouldReinsert) {
+ let newInfo = validateNewBookmark(updateInfo);
+ yield PlacesUtils.bookmarks.remove({
+ guid,
+ source: SOURCE_SYNC,
+ });
+ // A reinsertion likely indicates a confused client, since there aren't
+ // public APIs for changing livemark URLs or an item's kind (e.g., turning
+ // a folder into a separator while preserving its annos and position).
+ // This might be a good case to repair later; for now, we assume Sync has
+ // passed a complete record for the new item, and don't try to merge
+ // `oldBookmarkItem` with `updateInfo`.
+ return insertSyncBookmark(newInfo);
+ }
+
+ let isOrphan = false, requestedParentSyncId;
+ if (updateInfo.hasOwnProperty("parentSyncId")) {
+ requestedParentSyncId = updateInfo.parentSyncId;
+ let oldParentSyncId =
+ BookmarkSyncUtils.guidToSyncId(oldBookmarkItem.parentGuid);
+ if (requestedParentSyncId != oldParentSyncId) {
+ let oldId = yield PlacesUtils.promiseItemId(oldBookmarkItem.guid);
+ if (PlacesUtils.isRootItem(oldId)) {
+ throw new Error(`Cannot move Places root ${oldId}`);
+ }
+ let requestedParentGuid =
+ BookmarkSyncUtils.syncIdToGuid(requestedParentSyncId);
+ isOrphan = yield GUIDMissing(requestedParentGuid);
+ if (!isOrphan) {
+ BookmarkSyncLog.debug(`updateSyncBookmark: Item ${
+ updateInfo.syncId} is not an orphan`);
+ } else {
+ // Don't move the item if the new parent doesn't exist. Instead, mark
+ // the item as an orphan. We'll annotate it with its real parent after
+ // updating.
+ BookmarkSyncLog.trace(`updateSyncBookmark: Item ${
+ updateInfo.syncId} is an orphan: could not find parent ${
+ requestedParentSyncId}`);
+ delete updateInfo.parentSyncId;
+ }
+ } else {
+ // If the parent is the same, just omit it so that `update` doesn't do
+ // extra work.
+ delete updateInfo.parentSyncId;
+ }
+ }
+
+ updateInfo = yield updateTagQueryFolder(updateInfo);
+
+ let bookmarkInfo = syncBookmarkToPlacesBookmark(updateInfo);
+ let newBookmarkItem = shouldUpdateBookmark(bookmarkInfo) ?
+ yield PlacesUtils.bookmarks.update(bookmarkInfo) :
+ oldBookmarkItem;
+ let newItem = yield updateBookmarkMetadata(oldBookmarkItem, newBookmarkItem,
+ updateInfo);
+
+ // If the item is an orphan, annotate it with its real parent sync ID.
+ if (isOrphan) {
+ yield annotateOrphan(newItem, requestedParentSyncId);
+ }
+
+ // Reparent all orphans that expect this folder as the parent.
+ yield reparentOrphans(newItem);
+
+ return newItem;
+});
+
+// Updates tags, keywords, and annotations for an existing bookmark. Returns a
+// Sync bookmark object.
+var updateBookmarkMetadata = Task.async(function* (oldBookmarkItem,
+ newBookmarkItem,
+ updateInfo) {
+ let itemId = yield PlacesUtils.promiseItemId(newBookmarkItem.guid);
+ let newItem = yield placesBookmarkToSyncBookmark(newBookmarkItem);
+
+ try {
+ newItem.tags = yield tagItem(newBookmarkItem, updateInfo.tags);
+ } catch (ex) {
+ BookmarkSyncLog.warn(`updateBookmarkMetadata: Error tagging item ${
+ updateInfo.syncId}`, ex);
+ }
+
+ if (updateInfo.hasOwnProperty("keyword")) {
+ // Unconditionally remove the old keyword.
+ let entry = yield PlacesUtils.keywords.fetch({
+ url: oldBookmarkItem.url.href,
+ });
+ if (entry) {
+ yield PlacesUtils.keywords.remove({
+ keyword: entry.keyword,
+ source: SOURCE_SYNC,
+ });
+ }
+ if (updateInfo.keyword) {
+ yield PlacesUtils.keywords.insert({
+ keyword: updateInfo.keyword,
+ url: newItem.url.href,
+ source: SOURCE_SYNC,
+ });
+ }
+ newItem.keyword = updateInfo.keyword;
+ }
+
+ if (updateInfo.hasOwnProperty("description")) {
+ if (updateInfo.description) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.DESCRIPTION_ANNO, updateInfo.description, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+ } else {
+ PlacesUtils.annotations.removeItemAnnotation(itemId,
+ BookmarkSyncUtils.DESCRIPTION_ANNO, SOURCE_SYNC);
+ }
+ newItem.description = updateInfo.description;
+ }
+
+ if (updateInfo.hasOwnProperty("loadInSidebar")) {
+ if (updateInfo.loadInSidebar) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.SIDEBAR_ANNO, updateInfo.loadInSidebar, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+ } else {
+ PlacesUtils.annotations.removeItemAnnotation(itemId,
+ BookmarkSyncUtils.SIDEBAR_ANNO, SOURCE_SYNC);
+ }
+ newItem.loadInSidebar = updateInfo.loadInSidebar;
+ }
+
+ if (updateInfo.hasOwnProperty("query")) {
+ PlacesUtils.annotations.setItemAnnotation(itemId,
+ BookmarkSyncUtils.SMART_BOOKMARKS_ANNO, updateInfo.query, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER,
+ SOURCE_SYNC);
+ newItem.query = updateInfo.query;
+ }
+
+ return newItem;
+});
+
+function validateNewBookmark(info) {
+ let insertInfo = validateSyncBookmarkObject(info,
+ { kind: { required: true }
+ , syncId: { required: true }
+ , url: { requiredIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind)
+ , validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) }
+ , parentSyncId: { required: true }
+ , title: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY
+ , BookmarkSyncUtils.KINDS.FOLDER
+ , BookmarkSyncUtils.KINDS.LIVEMARK ].includes(b.kind) }
+ , query: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.QUERY }
+ , folder: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.QUERY }
+ , tags: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) }
+ , keyword: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) }
+ , description: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY
+ , BookmarkSyncUtils.KINDS.FOLDER
+ , BookmarkSyncUtils.KINDS.LIVEMARK ].includes(b.kind) }
+ , loadInSidebar: { validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
+ , BookmarkSyncUtils.KINDS.MICROSUMMARY
+ , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) }
+ , feed: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK }
+ , site: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK }
+ });
+
+ return insertInfo;
+}
+
+// Returns an array of GUIDs for items that have an `anno` with the given `val`.
+var fetchGuidsWithAnno = Task.async(function* (anno, val) {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(`
+ SELECT b.guid FROM moz_items_annos a
+ JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
+ JOIN moz_bookmarks b ON b.id = a.item_id
+ WHERE n.name = :anno AND
+ a.content = :val`,
+ { anno, val });
+ return rows.map(row => row.getResultByName("guid"));
+});
+
+// Returns the value of an item's annotation, or `null` if it's not set.
+var getAnno = Task.async(function* (guid, anno) {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(`
+ SELECT a.content FROM moz_items_annos a
+ JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
+ JOIN moz_bookmarks b ON b.id = a.item_id
+ WHERE b.guid = :guid AND
+ n.name = :anno`,
+ { guid, anno });
+ return rows.length ? rows[0].getResultByName("content") : null;
+});
+
+var tagItem = Task.async(function (item, tags) {
+ if (!item.url) {
+ return [];
+ }
+
+ // Remove leading and trailing whitespace, then filter out empty tags.
+ let newTags = tags.map(tag => tag.trim()).filter(Boolean);
+
+ // Removing the last tagged item will also remove the tag. To preserve
+ // tag IDs, we temporarily tag a dummy URI, ensuring the tags exist.
+ let dummyURI = PlacesUtils.toURI("about:weave#BStore_tagURI");
+ let bookmarkURI = PlacesUtils.toURI(item.url.href);
+ PlacesUtils.tagging.tagURI(dummyURI, newTags, SOURCE_SYNC);
+ PlacesUtils.tagging.untagURI(bookmarkURI, null, SOURCE_SYNC);
+ PlacesUtils.tagging.tagURI(bookmarkURI, newTags, SOURCE_SYNC);
+ PlacesUtils.tagging.untagURI(dummyURI, null, SOURCE_SYNC);
+
+ return newTags;
+});
+
+// `PlacesUtils.bookmarks.update` checks if we've supplied enough properties,
+// but doesn't know about additional livemark properties. We check this to avoid
+// having it throw in case we only pass properties like `{ guid, feedURI }`.
+function shouldUpdateBookmark(bookmarkInfo) {
+ return bookmarkInfo.hasOwnProperty("parentGuid") ||
+ bookmarkInfo.hasOwnProperty("title") ||
+ bookmarkInfo.hasOwnProperty("url");
+}
+
+var getTagFolder = Task.async(function* (tag) {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let results = yield db.executeCached(`SELECT id FROM moz_bookmarks
+ WHERE parent = :tagsFolder AND title = :tag LIMIT 1`,
+ { tagsFolder: PlacesUtils.bookmarks.tagsFolder, tag });
+ return results.length ? results[0].getResultByName("id") : null;
+});
+
+var getOrCreateTagFolder = Task.async(function* (tag) {
+ let id = yield getTagFolder(tag);
+ if (id) {
+ return id;
+ }
+ // Create the tag if it doesn't exist.
+ let item = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ title: tag,
+ source: SOURCE_SYNC,
+ });
+ return PlacesUtils.promiseItemId(item.guid);
+});
+
+// Converts a Places bookmark or livemark to a Sync bookmark. This function
+// maps Places GUIDs to sync IDs and filters out extra Places properties like
+// date added, last modified, and index.
+var placesBookmarkToSyncBookmark = Task.async(function* (bookmarkItem) {
+ let item = {};
+
+ for (let prop in bookmarkItem) {
+ switch (prop) {
+ // Sync IDs are identical to Places GUIDs for all items except roots.
+ case "guid":
+ item.syncId = BookmarkSyncUtils.guidToSyncId(bookmarkItem.guid);
+ break;
+
+ case "parentGuid":
+ item.parentSyncId =
+ BookmarkSyncUtils.guidToSyncId(bookmarkItem.parentGuid);
+ break;
+
+ // Sync uses kinds instead of types, which distinguish between folders,
+ // livemarks, bookmarks, and queries.
+ case "type":
+ item.kind = yield getKindForItem(bookmarkItem);
+ break;
+
+ case "title":
+ case "url":
+ item[prop] = bookmarkItem[prop];
+ break;
+
+ // Livemark objects contain additional properties. The feed URL is
+ // required; the site URL is optional.
+ case "feedURI":
+ item.feed = new URL(bookmarkItem.feedURI.spec);
+ break;
+
+ case "siteURI":
+ if (bookmarkItem.siteURI) {
+ item.site = new URL(bookmarkItem.siteURI.spec);
+ }
+ break;
+ }
+ }
+
+ return item;
+});
+
+// Converts a Sync bookmark object to a Places bookmark or livemark object.
+// This function maps sync IDs to Places GUIDs, and filters out extra Sync
+// properties like keywords, tags, and descriptions. Returns an object that can
+// be passed to `PlacesUtils.livemarks.addLivemark` or
+// `PlacesUtils.bookmarks.{insert, update}`.
+function syncBookmarkToPlacesBookmark(info) {
+ let bookmarkInfo = {
+ source: SOURCE_SYNC,
+ };
+
+ for (let prop in info) {
+ switch (prop) {
+ case "kind":
+ bookmarkInfo.type = getTypeForKind(info.kind);
+ break;
+
+ // Convert sync IDs to Places GUIDs for roots.
+ case "syncId":
+ bookmarkInfo.guid = BookmarkSyncUtils.syncIdToGuid(info.syncId);
+ break;
+
+ case "parentSyncId":
+ bookmarkInfo.parentGuid =
+ BookmarkSyncUtils.syncIdToGuid(info.parentSyncId);
+ // Instead of providing an index, Sync reorders children at the end of
+ // the sync using `BookmarkSyncUtils.order`. We explicitly specify the
+ // default index here to prevent `PlacesUtils.bookmarks.update` and
+ // `PlacesUtils.livemarks.addLivemark` from throwing.
+ bookmarkInfo.index = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ break;
+
+ case "title":
+ case "url":
+ bookmarkInfo[prop] = info[prop];
+ break;
+
+ // Livemark-specific properties.
+ case "feed":
+ bookmarkInfo.feedURI = PlacesUtils.toURI(info.feed);
+ break;
+
+ case "site":
+ if (info.site) {
+ bookmarkInfo.siteURI = PlacesUtils.toURI(info.site);
+ }
+ break;
+ }
+ }
+
+ return bookmarkInfo;
+}
+
+// Creates and returns a Sync bookmark object containing the bookmark's
+// tags, keyword, description, and whether it loads in the sidebar.
+var fetchBookmarkItem = Task.async(function* (bookmarkItem) {
+ let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
+
+ if (!item.title) {
+ item.title = "";
+ }
+
+ item.tags = PlacesUtils.tagging.getTagsForURI(
+ PlacesUtils.toURI(bookmarkItem.url), {});
+
+ let keywordEntry = yield PlacesUtils.keywords.fetch({
+ url: bookmarkItem.url,
+ });
+ if (keywordEntry) {
+ item.keyword = keywordEntry.keyword;
+ }
+
+ let description = yield getAnno(bookmarkItem.guid,
+ BookmarkSyncUtils.DESCRIPTION_ANNO);
+ if (description) {
+ item.description = description;
+ }
+
+ item.loadInSidebar = !!(yield getAnno(bookmarkItem.guid,
+ BookmarkSyncUtils.SIDEBAR_ANNO));
+
+ return item;
+});
+
+// Creates and returns a Sync bookmark object containing the folder's
+// description and children.
+var fetchFolderItem = Task.async(function* (bookmarkItem) {
+ let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
+
+ if (!item.title) {
+ item.title = "";
+ }
+
+ let description = yield getAnno(bookmarkItem.guid,
+ BookmarkSyncUtils.DESCRIPTION_ANNO);
+ if (description) {
+ item.description = description;
+ }
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ let children = yield fetchAllChildren(db, bookmarkItem.guid);
+ item.childSyncIds = children.map(child =>
+ BookmarkSyncUtils.guidToSyncId(child.guid)
+ );
+
+ return item;
+});
+
+// Creates and returns a Sync bookmark object containing the livemark's
+// description, children (none), feed URI, and site URI.
+var fetchLivemarkItem = Task.async(function* (bookmarkItem) {
+ let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
+
+ if (!item.title) {
+ item.title = "";
+ }
+
+ let description = yield getAnno(bookmarkItem.guid,
+ BookmarkSyncUtils.DESCRIPTION_ANNO);
+ if (description) {
+ item.description = description;
+ }
+
+ let feedAnno = yield getAnno(bookmarkItem.guid, PlacesUtils.LMANNO_FEEDURI);
+ item.feed = new URL(feedAnno);
+
+ let siteAnno = yield getAnno(bookmarkItem.guid, PlacesUtils.LMANNO_SITEURI);
+ if (siteAnno) {
+ item.site = new URL(siteAnno);
+ }
+
+ return item;
+});
+
+// Creates and returns a Sync bookmark object containing the query's tag
+// folder name and smart bookmark query ID.
+var fetchQueryItem = Task.async(function* (bookmarkItem) {
+ let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
+
+ let description = yield getAnno(bookmarkItem.guid,
+ BookmarkSyncUtils.DESCRIPTION_ANNO);
+ if (description) {
+ item.description = description;
+ }
+
+ let folder = null;
+ let params = new URLSearchParams(bookmarkItem.url.pathname);
+ let tagFolderId = +params.get("folder");
+ if (tagFolderId) {
+ try {
+ let tagFolderGuid = yield PlacesUtils.promiseItemGuid(tagFolderId);
+ let tagFolder = yield PlacesUtils.bookmarks.fetch(tagFolderGuid);
+ folder = tagFolder.title;
+ } catch (ex) {
+ BookmarkSyncLog.warn("fetchQueryItem: Query " + bookmarkItem.url.href +
+ " points to nonexistent folder " + tagFolderId, ex);
+ }
+ }
+ if (folder != null) {
+ item.folder = folder;
+ }
+
+ let query = yield getAnno(bookmarkItem.guid,
+ BookmarkSyncUtils.SMART_BOOKMARKS_ANNO);
+ if (query) {
+ item.query = query;
+ }
+
+ return item;
+});
diff --git a/toolkit/components/places/PlacesTransactions.jsm b/toolkit/components/places/PlacesTransactions.jsm
new file mode 100644
index 0000000000..c355d92b6d
--- /dev/null
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -0,0 +1,1645 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
+
+/**
+ * Overview
+ * --------
+ * This modules serves as the transactions manager for Places (hereinafter PTM).
+ * It implements all the elementary transactions for its UI commands: creating
+ * items, editing their various properties, and so forth.
+ *
+ * Note that since the effect of invoking a Places command is not limited to the
+ * window in which it was performed (e.g. a folder created in the Library may be
+ * the parent of a bookmark created in some browser window), PTM is a singleton.
+ * It's therefore unnecessary to initialize PTM in any way apart importing this
+ * module.
+ *
+ * PTM shares most of its semantics with common command pattern implementations.
+ * However, the asynchronous design of contemporary and future APIs, combined
+ * with the commitment to serialize all UI operations, does make things a little
+ * bit different. For example, when |undo| is called in order to undo the top
+ * undo entry, the caller cannot tell for sure what entry would it be, because
+ * the execution of some transactions is either in process, or enqueued to be.
+ *
+ * Also note that unlike the nsITransactionManager, for example, this API is by
+ * no means generic. That is, it cannot be used to execute anything but the
+ * elementary transactions implemented here (Please file a bug if you find
+ * anything uncovered). More-complex transactions (e.g. creating a folder and
+ * moving a bookmark into it) may be implemented as a batch (see below).
+ *
+ * A note about GUIDs and item-ids
+ * -------------------------------
+ * There's an ongoing effort (see bug 1071511) to deprecate item-ids in Places
+ * in favor of GUIDs. Both because new APIs (e.g. Bookmark.jsm) expose them to
+ * the minimum necessary, and because GUIDs play much better with implementing
+ * |redo|, this API doesn't support item-ids at all, and only accepts bookmark
+ * GUIDs, both for input (e.g. for setting the parent folder for a new bookmark)
+ * and for output (when the GUID for such a bookmark is propagated).
+ *
+ * When working in conjugation with older Places API which only expose item ids,
+ * use PlacesUtils.promiseItemGuid for converting those to GUIDs (note that
+ * for result nodes, the guid is available through their bookmarkGuid getter).
+ * Should you need to convert GUIDs to item-ids, use PlacesUtils.promiseItemId.
+ *
+ * Constructing transactions
+ * -------------------------
+ * At the bottom of this module you will find transactions for all Places UI
+ * commands. They are exposed as constructors set on the PlacesTransactions
+ * object (e.g. PlacesTransactions.NewFolder). The input for this constructors
+ * is taken in the form of a single argument, a plain object consisting of the
+ * properties for the transaction. Input properties may be either required or
+ * optional (for example, |keyword| is required for the EditKeyword transaction,
+ * but optional for the NewBookmark transaction).
+ *
+ * To make things simple, a given input property has the same basic meaning and
+ * valid values across all transactions which accept it in the input object.
+ * Here is a list of all supported input properties along with their expected
+ * values:
+ * - url: a URL object, an nsIURI object, or a href.
+ * - urls: an array of urls, as above.
+ * - feedUrl: an url (as above), holding the url for a live bookmark.
+ * - siteUrl an url (as above), holding the url for the site with which
+ * a live bookmark is associated.
+ * - tag - a string.
+ * - tags: an array of strings.
+ * - guid, parentGuid, newParentGuid: a valid Places GUID string.
+ * - guids: an array of valid Places GUID strings.
+ * - title: a string
+ * - index, newIndex: the position of an item in its containing folder,
+ * starting from 0.
+ * integer and PlacesUtils.bookmarks.DEFAULT_INDEX
+ * - annotation: see PlacesUtils.setAnnotationsForItem
+ * - annotations: an array of annotation objects as above.
+ * - excludingAnnotation: a string (annotation name).
+ * - excludingAnnotations: an array of string (annotation names).
+ *
+ * If a required property is missing in the input object (e.g. not specifying
+ * parentGuid for NewBookmark), or if the value for any of the input properties
+ * is invalid "on the surface" (e.g. a numeric value for GUID, or a string that
+ * isn't 12-characters long), the transaction constructor throws right way.
+ * More complex errors (e.g. passing a non-existent GUID for parentGuid) only
+ * reveal once the transaction is executed.
+ *
+ * Executing Transactions (the |transact| method of transactions)
+ * --------------------------------------------------------------
+ * Once a transaction is created, you must call its |transact| method for it to
+ * be executed and take effect. |transact| is an asynchronous method that takes
+ * no arguments, and returns a promise that resolves once the transaction is
+ * executed. Executing one of the transactions for creating items (NewBookmark,
+ * NewFolder, NewSeparator or NewLivemark) resolve to the new item's GUID.
+ * There's no resolution value for other transactions.
+ * If a transaction fails to execute, |transact| rejects and the transactions
+ * history is not affected.
+ *
+ * |transact| throws if it's called more than once (successfully or not) on the
+ * same transaction object.
+ *
+ * Batches
+ * -------
+ * Sometimes it is useful to "batch" or "merge" transactions. For example,
+ * something like "Bookmark All Tabs" may be implemented as one NewFolder
+ * transaction followed by numerous NewBookmark transactions - all to be undone
+ * or redone in a single undo or redo command. Use |PlacesTransactions.batch|
+ * in such cases. It can take either an array of transactions which will be
+ * executed in the given order and later be treated a a single entry in the
+ * transactions history, or a generator function that is passed to Task.spawn,
+ * that is to "contain" the batch: once the generator function is called a batch
+ * starts, and it lasts until the asynchronous generator iteration is complete
+ * All transactions executed by |transact| during this time are to be treated as
+ * a single entry in the transactions history.
+ *
+ * In both modes, |PlacesTransactions.batch| returns a promise that is to be
+ * resolved when the batch ends. In the array-input mode, there's no resolution
+ * value. In the generator mode, the resolution value is whatever the generator
+ * function returned (the semantics are the same as in Task.spawn, basically).
+ *
+ * The array-input mode of |PlacesTransactions.batch| is useful for implementing
+ * a batch of mostly-independent transaction (for example, |paste| into a folder
+ * can be implemented as a batch of multiple NewBookmark transactions).
+ * The generator mode is useful when the resolution value of executing one
+ * transaction is the input of one more subsequent transaction.
+ *
+ * In the array-input mode, if any transactions fails to execute, the batch
+ * continues (exceptions are logged). Only transactions that were executed
+ * successfully are added to the transactions history.
+ *
+ * WARNING: "nested" batches are not supported, if you call batch while another
+ * batch is still running, the new batch is enqueued with all other PTM work
+ * and thus not run until the running batch ends. The same goes for undo, redo
+ * and clearTransactionsHistory (note batches cannot be done partially, meaning
+ * undo and redo calls that during a batch are just enqueued).
+ *
+ * *****************************************************************************
+ * IT"S PARTICULARLY IMPORTANT NOT TO YIELD ANY PROMISE RETURNED BY ANY OF
+ * THESE METHODS (undo, redo, clearTransactionsHistory) FROM A BATCH FUNCTION.
+ * UNTIL WE FIND A WAY TO THROW IN THAT CASE (SEE BUG 1091446) DOING SO WILL
+ * COMPLETELY BREAK PTM UNTIL SHUTDOWN, NOT ALLOWING THE EXECUTION OF ANY
+ * TRANSACTION!
+ * *****************************************************************************
+ *
+ * Serialization
+ * -------------
+ * All |PlacesTransaction| operations are serialized. That is, even though the
+ * implementation is asynchronous, the order in which PlacesTransactions methods
+ * is called does guarantee the order in which they are to be invoked.
+ *
+ * The only exception to this rule is |transact| calls done during a batch (see
+ * above). |transact| calls are serialized with each other (and with undo, redo
+ * and clearTransactionsHistory), but they are, of course, not serialized with
+ * batches.
+ *
+ * The transactions-history structure
+ * ----------------------------------
+ * The transactions-history is a two-dimensional stack of transactions: the
+ * transactions are ordered in reverse to the order they were committed.
+ * It's two-dimensional because PTM allows batching transactions together for
+ * the purpose of undo or redo (see Batches above).
+ *
+ * The undoPosition property is set to the index of the top entry. If there is
+ * no entry at that index, there is nothing to undo.
+ * Entries prior to undoPosition, if any, are redo entries, the first one being
+ * the top redo entry.
+ *
+ * [ [2nd redo txn, 1st redo txn], <= 2nd redo entry
+ * [2nd redo txn, 1st redo txn], <= 1st redo entry
+ * [1st undo txn, 2nd undo txn], <= 1st undo entry
+ * [1st undo txn, 2nd undo txn] <= 2nd undo entry ]
+ * undoPostion: 2.
+ *
+ * Note that when a new entry is created, all redo entries are removed.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+ "resource://gre/modules/Console.jsm");
+
+Components.utils.importGlobalProperties(["URL"]);
+
+var TransactionsHistory = [];
+TransactionsHistory.__proto__ = {
+ __proto__: Array.prototype,
+
+ // The index of the first undo entry (if any) - See the documentation
+ // at the top of this file.
+ _undoPosition: 0,
+ get undoPosition() {
+ return this._undoPosition;
+ },
+
+ // Handy shortcuts
+ get topUndoEntry() {
+ return this.undoPosition < this.length ? this[this.undoPosition] : null;
+ },
+ get topRedoEntry() {
+ return this.undoPosition > 0 ? this[this.undoPosition - 1] : null;
+ },
+
+ // Outside of this module, the API of transactions is inaccessible, and so
+ // are any internal properties. To achieve that, transactions are proxified
+ // in their constructors. This maps the proxies to their respective raw
+ // objects.
+ proxifiedToRaw: new WeakMap(),
+
+ /**
+ * Proxify a transaction object for consumers.
+ * @param aRawTransaction
+ * the raw transaction object.
+ * @return the proxified transaction object.
+ * @see getRawTransaction for retrieving the raw transaction.
+ */
+ proxifyTransaction: function (aRawTransaction) {
+ let proxy = Object.freeze({
+ transact() {
+ return TransactionsManager.transact(this);
+ }
+ });
+ this.proxifiedToRaw.set(proxy, aRawTransaction);
+ return proxy;
+ },
+
+ /**
+ * Check if the given object is a the proxy object for some transaction.
+ * @param aValue
+ * any JS value.
+ * @return true if aValue is the proxy object for some transaction, false
+ * otherwise.
+ */
+ isProxifiedTransactionObject(aValue) {
+ return this.proxifiedToRaw.has(aValue);
+ },
+
+ /**
+ * Get the raw transaction for the given proxy.
+ * @param aProxy
+ * the proxy object
+ * @return the transaction proxified by aProxy; |undefined| is returned if
+ * aProxy is not a proxified transaction.
+ */
+ getRawTransaction(aProxy) {
+ return this.proxifiedToRaw.get(aProxy);
+ },
+
+ /**
+ * Add a transaction either as a new entry, if forced or if there are no undo
+ * entries, or to the top undo entry.
+ *
+ * @param aProxifiedTransaction
+ * the proxified transaction object to be added to the transaction
+ * history.
+ * @param [optional] aForceNewEntry
+ * Force a new entry for the transaction. Default: false.
+ * If false, an entry will we created only if there's no undo entry
+ * to extend.
+ */
+ add(aProxifiedTransaction, aForceNewEntry = false) {
+ if (!this.isProxifiedTransactionObject(aProxifiedTransaction))
+ throw new Error("aProxifiedTransaction is not a proxified transaction");
+
+ if (this.length == 0 || aForceNewEntry) {
+ this.clearRedoEntries();
+ this.unshift([aProxifiedTransaction]);
+ }
+ else {
+ this[this.undoPosition].unshift(aProxifiedTransaction);
+ }
+ },
+
+ /**
+ * Clear all undo entries.
+ */
+ clearUndoEntries() {
+ if (this.undoPosition < this.length)
+ this.splice(this.undoPosition);
+ },
+
+ /**
+ * Clear all redo entries.
+ */
+ clearRedoEntries() {
+ if (this.undoPosition > 0) {
+ this.splice(0, this.undoPosition);
+ this._undoPosition = 0;
+ }
+ },
+
+ /**
+ * Clear all entries.
+ */
+ clearAllEntries() {
+ if (this.length > 0) {
+ this.splice(0);
+ this._undoPosition = 0;
+ }
+ }
+};
+
+
+var PlacesTransactions = {
+ /**
+ * @see Batches in the module documentation.
+ */
+ batch(aToBatch) {
+ if (Array.isArray(aToBatch)) {
+ if (aToBatch.length == 0)
+ throw new Error("aToBatch must not be an empty array");
+
+ if (aToBatch.some(
+ o => !TransactionsHistory.isProxifiedTransactionObject(o))) {
+ throw new Error("aToBatch contains non-transaction element");
+ }
+ return TransactionsManager.batch(function* () {
+ for (let txn of aToBatch) {
+ try {
+ yield txn.transact();
+ }
+ catch (ex) {
+ console.error(ex);
+ }
+ }
+ });
+ }
+ if (typeof(aToBatch) == "function") {
+ return TransactionsManager.batch(aToBatch);
+ }
+
+ throw new Error("aToBatch must be either a function or a transactions array");
+ },
+
+ /**
+ * Asynchronously undo the transaction immediately after the current undo
+ * position in the transactions history in the reverse order, if any, and
+ * adjusts the undo position.
+ *
+ * @return {Promises). The promise always resolves.
+ * @note All undo manager operations are queued. This means that transactions
+ * history may change by the time your request is fulfilled.
+ */
+ undo() {
+ return TransactionsManager.undo();
+ },
+
+ /**
+ * Asynchronously redo the transaction immediately before the current undo
+ * position in the transactions history, if any, and adjusts the undo
+ * position.
+ *
+ * @return {Promises). The promise always resolves.
+ * @note All undo manager operations are queued. This means that transactions
+ * history may change by the time your request is fulfilled.
+ */
+ redo() {
+ return TransactionsManager.redo();
+ },
+
+ /**
+ * Asynchronously clear the undo, redo, or all entries from the transactions
+ * history.
+ *
+ * @param [optional] aUndoEntries
+ * Whether or not to clear undo entries. Default: true.
+ * @param [optional] aRedoEntries
+ * Whether or not to clear undo entries. Default: true.
+ *
+ * @return {Promises). The promise always resolves.
+ * @throws if both aUndoEntries and aRedoEntries are false.
+ * @note All undo manager operations are queued. This means that transactions
+ * history may change by the time your request is fulfilled.
+ */
+ clearTransactionsHistory(aUndoEntries = true, aRedoEntries = true) {
+ return TransactionsManager.clearTransactionsHistory(aUndoEntries, aRedoEntries);
+ },
+
+ /**
+ * The numbers of entries in the transactions history.
+ */
+ get length() {
+ return TransactionsHistory.length;
+ },
+
+ /**
+ * Get the transaction history entry at a given index. Each entry consists
+ * of one or more transaction objects.
+ *
+ * @param aIndex
+ * the index of the entry to retrieve.
+ * @return an array of transaction objects in their undo order (that is,
+ * reversely to the order they were executed).
+ * @throw if aIndex is invalid (< 0 or >= length).
+ * @note the returned array is a clone of the history entry and is not
+ * kept in sync with the original entry if it changes.
+ */
+ entry(aIndex) {
+ if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
+ throw new Error("Invalid index");
+
+ return TransactionsHistory[aIndex];
+ },
+
+ /**
+ * The index of the top undo entry in the transactions history.
+ * If there are no undo entries, it equals to |length|.
+ * Entries past this point
+ * Entries at and past this point are redo entries.
+ */
+ get undoPosition() {
+ return TransactionsHistory.undoPosition;
+ },
+
+ /**
+ * Shortcut for accessing the top undo entry in the transaction history.
+ */
+ get topUndoEntry() {
+ return TransactionsHistory.topUndoEntry;
+ },
+
+ /**
+ * Shortcut for accessing the top redo entry in the transaction history.
+ */
+ get topRedoEntry() {
+ return TransactionsHistory.topRedoEntry;
+ }
+};
+
+/**
+ * Helper for serializing the calls to TransactionsManager methods. It allows
+ * us to guarantee that the order in which TransactionsManager asynchronous
+ * methods are called also enforces the order in which they're executed, and
+ * that they are never executed in parallel.
+ *
+ * In other words: Enqueuer.enqueue(aFunc1); Enqueuer.enqueue(aFunc2) is roughly
+ * the same as Task.spawn(aFunc1).then(Task.spawn(aFunc2)).
+ */
+function Enqueuer() {
+ this._promise = Promise.resolve();
+}
+Enqueuer.prototype = {
+ /**
+ * Spawn a functions once all previous functions enqueued are done running,
+ * and all promises passed to alsoWaitFor are no longer pending.
+ *
+ * @param aFunc
+ * @see Task.spawn.
+ * @return a promise that resolves once aFunc is done running. The promise
+ * "mirrors" the promise returned by aFunc.
+ */
+ enqueue(aFunc) {
+ let promise = this._promise.then(Task.async(aFunc));
+
+ // Propagate exceptions to the caller, but dismiss them internally.
+ this._promise = promise.catch(console.error);
+ return promise;
+ },
+
+ /**
+ * Same as above, but for a promise returned by a function that already run.
+ * This is useful, for example, for serializing transact calls with undo calls,
+ * even though transact has its own Enqueuer.
+ *
+ * @param aPromise
+ * any promise.
+ */
+ alsoWaitFor(aPromise) {
+ // We don't care if aPromise resolves or rejects, but just that is not
+ // pending anymore.
+ let promise = aPromise.catch(console.error);
+ this._promise = Promise.all([this._promise, promise]);
+ },
+
+ /**
+ * The promise for this queue.
+ */
+ get promise() {
+ return this._promise;
+ }
+};
+
+var TransactionsManager = {
+ // See the documentation at the top of this file. |transact| calls are not
+ // serialized with |batch| calls.
+ _mainEnqueuer: new Enqueuer(),
+ _transactEnqueuer: new Enqueuer(),
+
+ // Is a batch in progress? set when we enter a batch function and unset when
+ // it's execution is done.
+ _batching: false,
+
+ // If a batch started, this indicates if we've already created an entry in the
+ // transactions history for the batch (i.e. if at least one transaction was
+ // executed successfully).
+ _createdBatchEntry: false,
+
+ // Transactions object should never be recycled (that is, |execute| should
+ // only be called once (or not at all) after they're constructed.
+ // This keeps track of all transactions which were executed.
+ _executedTransactions: new WeakSet(),
+
+ transact(aTxnProxy) {
+ let rawTxn = TransactionsHistory.getRawTransaction(aTxnProxy);
+ if (!rawTxn)
+ throw new Error("|transact| was called with an unexpected object");
+
+ if (this._executedTransactions.has(rawTxn))
+ throw new Error("Transactions objects may not be recycled.");
+
+ // Add it in advance so one doesn't accidentally do
+ // sameTxn.transact(); sameTxn.transact();
+ this._executedTransactions.add(rawTxn);
+
+ let promise = this._transactEnqueuer.enqueue(function* () {
+ // Don't try to catch exceptions. If execute fails, we better not add the
+ // transaction to the undo stack.
+ let retval = yield rawTxn.execute();
+
+ let forceNewEntry = !this._batching || !this._createdBatchEntry;
+ TransactionsHistory.add(aTxnProxy, forceNewEntry);
+ if (this._batching)
+ this._createdBatchEntry = true;
+
+ this._updateCommandsOnActiveWindow();
+ return retval;
+ }.bind(this));
+ this._mainEnqueuer.alsoWaitFor(promise);
+ return promise;
+ },
+
+ batch(aTask) {
+ return this._mainEnqueuer.enqueue(function* () {
+ this._batching = true;
+ this._createdBatchEntry = false;
+ let rv;
+ try {
+ // We should return here, but bug 958949 makes that impossible.
+ rv = (yield Task.spawn(aTask));
+ }
+ finally {
+ this._batching = false;
+ this._createdBatchEntry = false;
+ }
+ return rv;
+ }.bind(this));
+ },
+
+ /**
+ * Undo the top undo entry, if any, and update the undo position accordingly.
+ */
+ undo() {
+ let promise = this._mainEnqueuer.enqueue(function* () {
+ let entry = TransactionsHistory.topUndoEntry;
+ if (!entry)
+ return;
+
+ for (let txnProxy of entry) {
+ try {
+ yield TransactionsHistory.getRawTransaction(txnProxy).undo();
+ }
+ catch (ex) {
+ // If one transaction is broken, it's not safe to work with any other
+ // undo entry. Report the error and clear the undo history.
+ console.error(ex,
+ "Couldn't undo a transaction, clearing all undo entries.");
+ TransactionsHistory.clearUndoEntries();
+ return;
+ }
+ }
+ TransactionsHistory._undoPosition++;
+ this._updateCommandsOnActiveWindow();
+ }.bind(this));
+ this._transactEnqueuer.alsoWaitFor(promise);
+ return promise;
+ },
+
+ /**
+ * Redo the top redo entry, if any, and update the undo position accordingly.
+ */
+ redo() {
+ let promise = this._mainEnqueuer.enqueue(function* () {
+ let entry = TransactionsHistory.topRedoEntry;
+ if (!entry)
+ return;
+
+ for (let i = entry.length - 1; i >= 0; i--) {
+ let transaction = TransactionsHistory.getRawTransaction(entry[i]);
+ try {
+ if (transaction.redo)
+ yield transaction.redo();
+ else
+ yield transaction.execute();
+ }
+ catch (ex) {
+ // If one transaction is broken, it's not safe to work with any other
+ // redo entry. Report the error and clear the undo history.
+ console.error(ex,
+ "Couldn't redo a transaction, clearing all redo entries.");
+ TransactionsHistory.clearRedoEntries();
+ return;
+ }
+ }
+ TransactionsHistory._undoPosition--;
+ this._updateCommandsOnActiveWindow();
+ }.bind(this));
+
+ this._transactEnqueuer.alsoWaitFor(promise);
+ return promise;
+ },
+
+ clearTransactionsHistory(aUndoEntries, aRedoEntries) {
+ let promise = this._mainEnqueuer.enqueue(function* () {
+ if (aUndoEntries && aRedoEntries)
+ TransactionsHistory.clearAllEntries();
+ else if (aUndoEntries)
+ TransactionsHistory.clearUndoEntries();
+ else if (aRedoEntries)
+ TransactionsHistory.clearRedoEntries();
+ else
+ throw new Error("either aUndoEntries or aRedoEntries should be true");
+ }.bind(this));
+
+ this._transactEnqueuer.alsoWaitFor(promise);
+ return promise;
+ },
+
+ // Updates commands in the undo group of the active window commands.
+ // Inactive windows commands will be updated on focus.
+ _updateCommandsOnActiveWindow() {
+ // Updating "undo" will cause a group update including "redo".
+ try {
+ let win = Services.focus.activeWindow;
+ if (win)
+ win.updateCommands("undo");
+ }
+ catch (ex) { console.error(ex, "Couldn't update undo commands"); }
+ }
+};
+
+/**
+ * Internal helper for defining the standard transactions and their input.
+ * It takes the required and optional properties, and generates the public
+ * constructor (which takes the input in the form of a plain object) which,
+ * when called, creates the argument-less "public" |execute| method by binding
+ * the input properties to the function arguments (required properties first,
+ * then the optional properties).
+ *
+ * If this seems confusing, look at the consumers.
+ *
+ * This magic serves two purposes:
+ * (1) It completely hides the transactions' internals from the module
+ * consumers.
+ * (2) It keeps each transaction implementation to what is about, bypassing
+ * all this bureaucracy while still validating input appropriately.
+ */
+function DefineTransaction(aRequiredProps = [], aOptionalProps = []) {
+ for (let prop of [...aRequiredProps, ...aOptionalProps]) {
+ if (!DefineTransaction.inputProps.has(prop))
+ throw new Error("Property '" + prop + "' is not defined");
+ }
+
+ let ctor = function (aInput) {
+ // We want to support both syntaxes:
+ // let t = new PlacesTransactions.NewBookmark(),
+ // let t = PlacesTransactions.NewBookmark()
+ if (this == PlacesTransactions)
+ return new ctor(aInput);
+
+ if (aRequiredProps.length > 0 || aOptionalProps.length > 0) {
+ // Bind the input properties to the arguments of execute.
+ let input = DefineTransaction.verifyInput(aInput, aRequiredProps,
+ aOptionalProps);
+ let executeArgs = [this,
+ ...aRequiredProps.map(prop => input[prop]),
+ ...aOptionalProps.map(prop => input[prop])];
+ this.execute = Function.bind.apply(this.execute, executeArgs);
+ }
+ return TransactionsHistory.proxifyTransaction(this);
+ };
+ return ctor;
+}
+
+function simpleValidateFunc(aCheck) {
+ return v => {
+ if (!aCheck(v))
+ throw new Error("Invalid value");
+ return v;
+ };
+}
+
+DefineTransaction.strValidate = simpleValidateFunc(v => typeof(v) == "string");
+DefineTransaction.strOrNullValidate =
+ simpleValidateFunc(v => typeof(v) == "string" || v === null);
+DefineTransaction.indexValidate =
+ simpleValidateFunc(v => Number.isInteger(v) &&
+ v >= PlacesUtils.bookmarks.DEFAULT_INDEX);
+DefineTransaction.guidValidate =
+ simpleValidateFunc(v => /^[a-zA-Z0-9\-_]{12}$/.test(v));
+
+function isPrimitive(v) {
+ return v === null || (typeof(v) != "object" && typeof(v) != "function");
+}
+
+DefineTransaction.annotationObjectValidate = function (obj) {
+ let checkProperty = (aPropName, aRequired, aCheckFunc) => {
+ if (aPropName in obj)
+ return aCheckFunc(obj[aPropName]);
+
+ return !aRequired;
+ };
+
+ if (obj &&
+ checkProperty("name", true, v => typeof(v) == "string" && v.length > 0) &&
+ checkProperty("expires", false, Number.isInteger) &&
+ checkProperty("flags", false, Number.isInteger) &&
+ checkProperty("value", false, isPrimitive) ) {
+ // Nothing else should be set
+ let validKeys = ["name", "value", "flags", "expires"];
+ if (Object.keys(obj).every( (k) => validKeys.includes(k)))
+ return obj;
+ }
+ throw new Error("Invalid annotation object");
+};
+
+DefineTransaction.urlValidate = function(url) {
+ // When this module is updated to use Bookmarks.jsm, we should actually
+ // convert nsIURIs/spec to URL objects.
+ if (url instanceof Components.interfaces.nsIURI)
+ return url;
+ let spec = url instanceof URL ? url.href : url;
+ return NetUtil.newURI(spec);
+};
+
+DefineTransaction.inputProps = new Map();
+DefineTransaction.defineInputProps =
+function (aNames, aValidationFunction, aDefaultValue) {
+ for (let name of aNames) {
+ // Workaround bug 449811.
+ let propName = name;
+ this.inputProps.set(propName, {
+ validateValue: function (aValue) {
+ if (aValue === undefined)
+ return aDefaultValue;
+ try {
+ return aValidationFunction(aValue);
+ }
+ catch (ex) {
+ throw new Error(`Invalid value for input property ${propName}`);
+ }
+ },
+
+ validateInput: function (aInput, aRequired) {
+ if (aRequired && !(propName in aInput))
+ throw new Error(`Required input property is missing: ${propName}`);
+ return this.validateValue(aInput[propName]);
+ },
+
+ isArrayProperty: false
+ });
+ }
+};
+
+DefineTransaction.defineArrayInputProp =
+function (aName, aBasePropertyName) {
+ let baseProp = this.inputProps.get(aBasePropertyName);
+ if (!baseProp)
+ throw new Error(`Unknown input property: ${aBasePropertyName}`);
+
+ this.inputProps.set(aName, {
+ validateValue: function (aValue) {
+ if (aValue == undefined)
+ return [];
+
+ if (!Array.isArray(aValue))
+ throw new Error(`${aName} input property value must be an array`);
+
+ // This also takes care of abandoning the global scope of the input
+ // array (through Array.prototype).
+ return aValue.map(baseProp.validateValue);
+ },
+
+ // We allow setting either the array property itself (e.g. urls), or a
+ // single element of it (url, in that example), that is then transformed
+ // into a single-element array.
+ validateInput: function (aInput, aRequired) {
+ if (aName in aInput) {
+ // It's not allowed to set both though.
+ if (aBasePropertyName in aInput) {
+ throw new Error(`It is not allowed to set both ${aName} and
+ ${aBasePropertyName} as input properties`);
+ }
+ let array = this.validateValue(aInput[aName]);
+ if (aRequired && array.length == 0) {
+ throw new Error(`Empty array passed for required input property:
+ ${aName}`);
+ }
+ return array;
+ }
+ // If the property is required and it's not set as is, check if the base
+ // property is set.
+ if (aRequired && !(aBasePropertyName in aInput))
+ throw new Error(`Required input property is missing: ${aName}`);
+
+ if (aBasePropertyName in aInput)
+ return [baseProp.validateValue(aInput[aBasePropertyName])];
+
+ return [];
+ },
+
+ isArrayProperty: true
+ });
+};
+
+DefineTransaction.validatePropertyValue =
+function (aProp, aInput, aRequired) {
+ return this.inputProps.get(aProp).validateInput(aInput, aRequired);
+};
+
+DefineTransaction.getInputObjectForSingleValue =
+function (aInput, aRequiredProps, aOptionalProps) {
+ // The following input forms may be deduced from a single value:
+ // * a single required property with or without optional properties (the given
+ // value is set to the required property).
+ // * a single optional property with no required properties.
+ if (aRequiredProps.length > 1 ||
+ (aRequiredProps.length == 0 && aOptionalProps.length > 1)) {
+ throw new Error("Transaction input isn't an object");
+ }
+
+ let propName = aRequiredProps.length == 1 ?
+ aRequiredProps[0] : aOptionalProps[0];
+ let propValue =
+ this.inputProps.get(propName).isArrayProperty && !Array.isArray(aInput) ?
+ [aInput] : aInput;
+ return { [propName]: propValue };
+};
+
+DefineTransaction.verifyInput =
+function (aInput, aRequiredProps = [], aOptionalProps = []) {
+ if (aRequiredProps.length == 0 && aOptionalProps.length == 0)
+ return {};
+
+ // If there's just a single required/optional property, we allow passing it
+ // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGuid)
+ // rather than PlacesTransactions.RemoveItem({ guid: myGuid}).
+ // This shortcut isn't supported for "complex" properties - e.g. one cannot
+ // pass an annotation object this way (note there is no use case for this at
+ // the moment anyway).
+ let input = aInput;
+ let isSinglePropertyInput =
+ isPrimitive(aInput) ||
+ Array.isArray(aInput) ||
+ (aInput instanceof Components.interfaces.nsISupports);
+ if (isSinglePropertyInput) {
+ input = this.getInputObjectForSingleValue(aInput,
+ aRequiredProps,
+ aOptionalProps);
+ }
+
+ let fixedInput = { };
+ for (let prop of aRequiredProps) {
+ fixedInput[prop] = this.validatePropertyValue(prop, input, true);
+ }
+ for (let prop of aOptionalProps) {
+ fixedInput[prop] = this.validatePropertyValue(prop, input, false);
+ }
+
+ return fixedInput;
+};
+
+// Update the documentation at the top of this module if you add or
+// remove properties.
+DefineTransaction.defineInputProps(["url", "feedUrl", "siteUrl"],
+ DefineTransaction.urlValidate, null);
+DefineTransaction.defineInputProps(["guid", "parentGuid", "newParentGuid"],
+ DefineTransaction.guidValidate);
+DefineTransaction.defineInputProps(["title"],
+ DefineTransaction.strOrNullValidate, null);
+DefineTransaction.defineInputProps(["keyword", "oldKeyword", "postData", "tag",
+ "excludingAnnotation"],
+ DefineTransaction.strValidate, "");
+DefineTransaction.defineInputProps(["index", "newIndex"],
+ DefineTransaction.indexValidate,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+DefineTransaction.defineInputProps(["annotation"],
+ DefineTransaction.annotationObjectValidate);
+DefineTransaction.defineArrayInputProp("guids", "guid");
+DefineTransaction.defineArrayInputProp("urls", "url");
+DefineTransaction.defineArrayInputProp("tags", "tag");
+DefineTransaction.defineArrayInputProp("annotations", "annotation");
+DefineTransaction.defineArrayInputProp("excludingAnnotations",
+ "excludingAnnotation");
+
+/**
+ * Internal helper for implementing the execute method of NewBookmark, NewFolder
+ * and NewSeparator.
+ *
+ * @param aTransaction
+ * The transaction object
+ * @param aParentGuid
+ * The GUID of the parent folder
+ * @param aCreateItemFunction(aParentId, aGuidToRestore)
+ * The function to be called for creating the item on execute and redo.
+ * It should return the itemId for the new item
+ * - aGuidToRestore - the GUID to set for the item (used for redo).
+ * @param [optional] aOnUndo
+ * an additional function to call after undo
+ * @param [optional] aOnRedo
+ * an additional function to call after redo
+ */
+function* ExecuteCreateItem(aTransaction, aParentGuid, aCreateItemFunction,
+ aOnUndo = null, aOnRedo = null) {
+ let parentId = yield PlacesUtils.promiseItemId(aParentGuid),
+ itemId = yield aCreateItemFunction(parentId, ""),
+ guid = yield PlacesUtils.promiseItemGuid(itemId);
+
+ // On redo, we'll restore the date-added and last-modified properties.
+ let dateAdded = 0, lastModified = 0;
+ aTransaction.undo = function* () {
+ if (dateAdded == 0) {
+ dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId);
+ lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId);
+ }
+ PlacesUtils.bookmarks.removeItem(itemId);
+ if (aOnUndo) {
+ yield aOnUndo();
+ }
+ };
+ aTransaction.redo = function* () {
+ parentId = yield PlacesUtils.promiseItemId(aParentGuid);
+ itemId = yield aCreateItemFunction(parentId, guid);
+ if (aOnRedo)
+ yield aOnRedo();
+
+ // aOnRedo is called first to make sure it doesn't override
+ // lastModified.
+ PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded);
+ PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
+ PlacesUtils.bookmarks.setItemLastModified(parentId, dateAdded);
+ };
+ return guid;
+}
+
+/**
+ * Creates items (all types) from a bookmarks tree representation, as defined
+ * in PlacesUtils.promiseBookmarksTree.
+ *
+ * @param aBookmarksTree
+ * the bookmarks tree object. You may pass either a bookmarks tree
+ * returned by promiseBookmarksTree, or a manually defined one.
+ * @param [optional] aRestoring (default: false)
+ * Whether or not the items are restored. Only in restore mode, are
+ * the guid, dateAdded and lastModified properties honored.
+ * @param [optional] aExcludingAnnotations
+ * Array of annotations names to ignore in aBookmarksTree. This argument
+ * is ignored if aRestoring is set.
+ * @note the id, root and charset properties of items in aBookmarksTree are
+ * always ignored. The index property is ignored for all items but the
+ * root one.
+ * @return {Promise}
+ */
+function* createItemsFromBookmarksTree(aBookmarksTree, aRestoring = false,
+ aExcludingAnnotations = []) {
+ function extractLivemarkDetails(aAnnos) {
+ let feedURI = null, siteURI = null;
+ aAnnos = aAnnos.filter(
+ aAnno => {
+ switch (aAnno.name) {
+ case PlacesUtils.LMANNO_FEEDURI:
+ feedURI = NetUtil.newURI(aAnno.value);
+ return false;
+ case PlacesUtils.LMANNO_SITEURI:
+ siteURI = NetUtil.newURI(aAnno.value);
+ return false;
+ default:
+ return true;
+ }
+ } );
+ return [feedURI, siteURI];
+ }
+
+ function* createItem(aItem,
+ aParentGuid,
+ aIndex = PlacesUtils.bookmarks.DEFAULT_INDEX) {
+ let itemId;
+ let guid = aRestoring ? aItem.guid : undefined;
+ let parentId = yield PlacesUtils.promiseItemId(aParentGuid);
+ let annos = aItem.annos ? [...aItem.annos] : [];
+ switch (aItem.type) {
+ case PlacesUtils.TYPE_X_MOZ_PLACE: {
+ let uri = NetUtil.newURI(aItem.uri);
+ itemId = PlacesUtils.bookmarks.insertBookmark(
+ parentId, uri, aIndex, aItem.title, guid);
+ if ("keyword" in aItem) {
+ yield PlacesUtils.keywords.insert({
+ keyword: aItem.keyword,
+ url: uri.spec
+ });
+ }
+ if ("tags" in aItem) {
+ PlacesUtils.tagging.tagURI(uri, aItem.tags.split(","));
+ }
+ break;
+ }
+ case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: {
+ // Either a folder or a livemark
+ let [feedURI, siteURI] = extractLivemarkDetails(annos);
+ if (!feedURI) {
+ itemId = PlacesUtils.bookmarks.createFolder(
+ parentId, aItem.title, aIndex, guid);
+ if (guid === undefined)
+ guid = yield PlacesUtils.promiseItemGuid(itemId);
+ if ("children" in aItem) {
+ for (let child of aItem.children) {
+ yield createItem(child, guid);
+ }
+ }
+ }
+ else {
+ let livemark =
+ yield PlacesUtils.livemarks.addLivemark({ title: aItem.title
+ , feedURI: feedURI
+ , siteURI: siteURI
+ , parentId: parentId
+ , index: aIndex
+ , guid: guid});
+ itemId = livemark.id;
+ }
+ break;
+ }
+ case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR: {
+ itemId = PlacesUtils.bookmarks.insertSeparator(parentId, aIndex, guid);
+ break;
+ }
+ }
+ if (annos.length > 0) {
+ if (!aRestoring && aExcludingAnnotations.length > 0) {
+ annos = annos.filter(a => !aExcludingAnnotations.includes(a.name));
+
+ }
+
+ PlacesUtils.setAnnotationsForItem(itemId, annos);
+ }
+
+ if (aRestoring) {
+ if ("dateAdded" in aItem)
+ PlacesUtils.bookmarks.setItemDateAdded(itemId, aItem.dateAdded);
+ if ("lastModified" in aItem)
+ PlacesUtils.bookmarks.setItemLastModified(itemId, aItem.lastModified);
+ }
+ return itemId;
+ }
+ return yield createItem(aBookmarksTree,
+ aBookmarksTree.parentGuid,
+ aBookmarksTree.index);
+}
+
+/** ***************************************************************************
+ * The Standard Places Transactions.
+ *
+ * See the documentation at the top of this file. The valid values for input
+ * are also documented there.
+ *****************************************************************************/
+
+var PT = PlacesTransactions;
+
+/**
+ * Transaction for creating a bookmark.
+ *
+ * Required Input Properties: url, parentGuid.
+ * Optional Input Properties: index, title, keyword, annotations, tags.
+ *
+ * When this transaction is executed, it's resolved to the new bookmark's GUID.
+ */
+PT.NewBookmark = DefineTransaction(["parentGuid", "url"],
+ ["index", "title", "keyword", "postData",
+ "annotations", "tags"]);
+PT.NewBookmark.prototype = Object.seal({
+ execute: function (aParentGuid, aURI, aIndex, aTitle,
+ aKeyword, aPostData, aAnnos, aTags) {
+ return ExecuteCreateItem(this, aParentGuid,
+ function* (parentId, guidToRestore = "") {
+ let itemId = PlacesUtils.bookmarks.insertBookmark(
+ parentId, aURI, aIndex, aTitle, guidToRestore);
+
+ if (aKeyword) {
+ yield PlacesUtils.keywords.insert({
+ url: aURI.spec,
+ keyword: aKeyword,
+ postData: aPostData
+ });
+ }
+ if (aAnnos.length) {
+ PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
+ }
+ if (aTags.length > 0) {
+ let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
+ aTags = aTags.filter(t => !currentTags.includes(t));
+ PlacesUtils.tagging.tagURI(aURI, aTags);
+ }
+
+ return itemId;
+ },
+ function _additionalOnUndo() {
+ if (aTags.length > 0) {
+ PlacesUtils.tagging.untagURI(aURI, aTags);
+ }
+ });
+ }
+});
+
+/**
+ * Transaction for creating a folder.
+ *
+ * Required Input Properties: title, parentGuid.
+ * Optional Input Properties: index, annotations.
+ *
+ * When this transaction is executed, it's resolved to the new folder's GUID.
+ */
+PT.NewFolder = DefineTransaction(["parentGuid", "title"],
+ ["index", "annotations"]);
+PT.NewFolder.prototype = Object.seal({
+ execute: function (aParentGuid, aTitle, aIndex, aAnnos) {
+ return ExecuteCreateItem(this, aParentGuid,
+ function* (parentId, guidToRestore = "") {
+ let itemId = PlacesUtils.bookmarks.createFolder(
+ parentId, aTitle, aIndex, guidToRestore);
+ if (aAnnos.length > 0)
+ PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
+ return itemId;
+ });
+ }
+});
+
+/**
+ * Transaction for creating a separator.
+ *
+ * Required Input Properties: parentGuid.
+ * Optional Input Properties: index.
+ *
+ * When this transaction is executed, it's resolved to the new separator's
+ * GUID.
+ */
+PT.NewSeparator = DefineTransaction(["parentGuid"], ["index"]);
+PT.NewSeparator.prototype = Object.seal({
+ execute: function (aParentGuid, aIndex) {
+ return ExecuteCreateItem(this, aParentGuid,
+ function* (parentId, guidToRestore = "") {
+ let itemId = PlacesUtils.bookmarks.insertSeparator(
+ parentId, aIndex, guidToRestore);
+ return itemId;
+ });
+ }
+});
+
+/**
+ * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
+ * semantics).
+ *
+ * Required Input Properties: feedUrl, title, parentGuid.
+ * Optional Input Properties: siteUrl, index, annotations.
+ *
+ * When this transaction is executed, it's resolved to the new livemark's
+ * GUID.
+ */
+PT.NewLivemark = DefineTransaction(["feedUrl", "title", "parentGuid"],
+ ["siteUrl", "index", "annotations"]);
+PT.NewLivemark.prototype = Object.seal({
+ execute: function* (aFeedURI, aTitle, aParentGuid, aSiteURI, aIndex, aAnnos) {
+ let livemarkInfo = { title: aTitle
+ , feedURI: aFeedURI
+ , siteURI: aSiteURI
+ , index: aIndex };
+ let createItem = function* () {
+ livemarkInfo.parentId = yield PlacesUtils.promiseItemId(aParentGuid);
+ let livemark = yield PlacesUtils.livemarks.addLivemark(livemarkInfo);
+ if (aAnnos.length > 0)
+ PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos);
+
+ if ("dateAdded" in livemarkInfo) {
+ PlacesUtils.bookmarks.setItemDateAdded(livemark.id,
+ livemarkInfo.dateAdded);
+ PlacesUtils.bookmarks.setItemLastModified(livemark.id,
+ livemarkInfo.lastModified);
+ }
+ return livemark;
+ };
+
+ let livemark = yield createItem();
+ this.undo = function* () {
+ livemarkInfo.guid = livemark.guid;
+ if (!("dateAdded" in livemarkInfo)) {
+ livemarkInfo.dateAdded =
+ PlacesUtils.bookmarks.getItemDateAdded(livemark.id);
+ livemarkInfo.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(livemark.id);
+ }
+ yield PlacesUtils.livemarks.removeLivemark(livemark);
+ };
+ this.redo = function* () {
+ livemark = yield createItem();
+ };
+ return livemark.guid;
+ }
+});
+
+/**
+ * Transaction for moving an item.
+ *
+ * Required Input Properties: guid, newParentGuid.
+ * Optional Input Properties newIndex.
+ */
+PT.Move = DefineTransaction(["guid", "newParentGuid"], ["newIndex"]);
+PT.Move.prototype = Object.seal({
+ execute: function* (aGuid, aNewParentGuid, aNewIndex) {
+ let itemId = yield PlacesUtils.promiseItemId(aGuid),
+ oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId),
+ oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId),
+ newParentId = yield PlacesUtils.promiseItemId(aNewParentGuid);
+
+ PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex);
+
+ let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId);
+ this.undo = () => {
+ // Moving down in the same parent takes in count removal of the item
+ // so to revert positions we must move to oldIndex + 1
+ if (newParentId == oldParentId && oldIndex > undoIndex)
+ PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1);
+ else
+ PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex);
+ };
+ }
+});
+
+/**
+ * Transaction for setting the title for an item.
+ *
+ * Required Input Properties: guid, title.
+ */
+PT.EditTitle = DefineTransaction(["guid", "title"]);
+PT.EditTitle.prototype = Object.seal({
+ execute: function* (aGuid, aTitle) {
+ let itemId = yield PlacesUtils.promiseItemId(aGuid),
+ oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
+ PlacesUtils.bookmarks.setItemTitle(itemId, aTitle);
+ this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); };
+ }
+});
+
+/**
+ * Transaction for setting the URI for an item.
+ *
+ * Required Input Properties: guid, url.
+ */
+PT.EditUrl = DefineTransaction(["guid", "url"]);
+PT.EditUrl.prototype = Object.seal({
+ execute: function* (aGuid, aURI) {
+ let itemId = yield PlacesUtils.promiseItemId(aGuid),
+ oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
+ oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI),
+ newURIAdditionalTags = null;
+ PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI);
+
+ // Move tags from old URI to new URI.
+ if (oldURITags.length > 0) {
+ // Only untag the old URI if this is the only bookmark.
+ if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0)
+ PlacesUtils.tagging.untagURI(oldURI, oldURITags);
+
+ let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI);
+ newURIAdditionalTags = oldURITags.filter(t => !currentNewURITags.includes(t));
+ if (newURIAdditionalTags)
+ PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags);
+ }
+
+ this.undo = () => {
+ PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI);
+ // Move tags from new URI to old URI.
+ if (oldURITags.length > 0) {
+ // Only untag the new URI if this is the only bookmark.
+ if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
+ PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) {
+ PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags);
+ }
+
+ PlacesUtils.tagging.tagURI(oldURI, oldURITags);
+ }
+ };
+ }
+});
+
+/**
+ * Transaction for setting annotations for an item.
+ *
+ * Required Input Properties: guid, annotationObject
+ */
+PT.Annotate = DefineTransaction(["guids", "annotations"]);
+PT.Annotate.prototype = {
+ *execute(aGuids, aNewAnnos) {
+ let undoAnnosForItem = new Map(); // itemId => undoAnnos;
+ for (let guid of aGuids) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
+
+ let undoAnnos = [];
+ for (let newAnno of aNewAnnos) {
+ let currentAnno = currentAnnos.find(a => a.name == newAnno.name);
+ if (currentAnno) {
+ undoAnnos.push(currentAnno);
+ }
+ else {
+ // An unset value removes the annotation.
+ undoAnnos.push({ name: newAnno.name });
+ }
+ }
+ undoAnnosForItem.set(itemId, undoAnnos);
+
+ PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+ }
+
+ this.undo = function() {
+ for (let [itemId, undoAnnos] of undoAnnosForItem) {
+ PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
+ }
+ };
+ this.redo = function* () {
+ for (let guid of aGuids) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
+ }
+ };
+ }
+};
+
+/**
+ * Transaction for setting the keyword for a bookmark.
+ *
+ * Required Input Properties: guid, keyword.
+ */
+PT.EditKeyword = DefineTransaction(["guid", "keyword"],
+ ["postData", "oldKeyword"]);
+PT.EditKeyword.prototype = Object.seal({
+ execute: function* (aGuid, aKeyword, aPostData, aOldKeyword) {
+ let url;
+ let oldKeywordEntry;
+ if (aOldKeyword) {
+ oldKeywordEntry = yield PlacesUtils.keywords.fetch(aOldKeyword);
+ url = oldKeywordEntry.url;
+ yield PlacesUtils.keywords.remove(aOldKeyword);
+ }
+
+ if (aKeyword) {
+ if (!url) {
+ url = (yield PlacesUtils.bookmarks.fetch(aGuid)).url;
+ }
+ yield PlacesUtils.keywords.insert({
+ url: url,
+ keyword: aKeyword,
+ postData: aPostData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
+ });
+ }
+
+ this.undo = function* () {
+ if (aKeyword) {
+ yield PlacesUtils.keywords.remove(aKeyword);
+ }
+ if (oldKeywordEntry) {
+ yield PlacesUtils.keywords.insert(oldKeywordEntry);
+ }
+ };
+ }
+});
+
+/**
+ * Transaction for sorting a folder by name.
+ *
+ * Required Input Properties: guid.
+ */
+PT.SortByName = DefineTransaction(["guid"]);
+PT.SortByName.prototype = {
+ execute: function* (aGuid) {
+ let itemId = yield PlacesUtils.promiseItemId(aGuid),
+ oldOrder = [], // [itemId] = old index
+ contents = PlacesUtils.getFolderContents(itemId, false, false).root,
+ count = contents.childCount;
+
+ // Sort between separators.
+ let newOrder = [], // nodes, in the new order.
+ preSep = []; // Temporary array for sorting each group of nodes.
+ let sortingMethod = (a, b) => {
+ if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
+ return -1;
+ if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
+ return 1;
+ return a.title.localeCompare(b.title);
+ };
+
+ for (let i = 0; i < count; ++i) {
+ let node = contents.getChild(i);
+ oldOrder[node.itemId] = i;
+ if (PlacesUtils.nodeIsSeparator(node)) {
+ if (preSep.length > 0) {
+ preSep.sort(sortingMethod);
+ newOrder = newOrder.concat(preSep);
+ preSep.splice(0, preSep.length);
+ }
+ newOrder.push(node);
+ }
+ else
+ preSep.push(node);
+ }
+ contents.containerOpen = false;
+
+ if (preSep.length > 0) {
+ preSep.sort(sortingMethod);
+ newOrder = newOrder.concat(preSep);
+ }
+
+ // Set the nex indexes.
+ let callback = {
+ runBatched: function() {
+ for (let i = 0; i < newOrder.length; ++i) {
+ PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
+ }
+ }
+ };
+ PlacesUtils.bookmarks.runInBatchMode(callback, null);
+
+ this.undo = () => {
+ let callback = {
+ runBatched: function() {
+ for (let item in oldOrder) {
+ PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]);
+ }
+ }
+ };
+ PlacesUtils.bookmarks.runInBatchMode(callback, null);
+ };
+ }
+};
+
+/**
+ * Transaction for removing an item (any type).
+ *
+ * Required Input Properties: guids.
+ */
+PT.Remove = DefineTransaction(["guids"]);
+PT.Remove.prototype = {
+ *execute(aGuids) {
+ function promiseBookmarksTree(guid) {
+ try {
+ return PlacesUtils.promiseBookmarksTree(guid);
+ }
+ catch (ex) {
+ throw new Error("Failed to get info for the specified item (guid: " +
+ guid + "). Ex: " + ex);
+ }
+ }
+
+ let toRestore = [];
+ for (let guid of aGuids) {
+ toRestore.push(yield promiseBookmarksTree(guid));
+ }
+
+ let removeThem = Task.async(function* () {
+ for (let guid of aGuids) {
+ PlacesUtils.bookmarks.removeItem(yield PlacesUtils.promiseItemId(guid));
+ }
+ });
+ yield removeThem();
+
+ this.undo = Task.async(function* () {
+ for (let info of toRestore) {
+ yield createItemsFromBookmarksTree(info, true);
+ }
+ });
+ this.redo = removeThem;
+ }
+};
+
+/**
+ * Transactions for removing all bookmarks for one or more urls.
+ *
+ * Required Input Properties: urls.
+ */
+PT.RemoveBookmarksForUrls = DefineTransaction(["urls"]);
+PT.RemoveBookmarksForUrls.prototype = {
+ *execute(aUrls) {
+ let guids = [];
+ for (let url of aUrls) {
+ yield PlacesUtils.bookmarks.fetch({ url }, info => {
+ guids.push(info.guid);
+ });
+ }
+ let removeTxn = TransactionsHistory.getRawTransaction(PT.Remove(guids));
+ yield removeTxn.execute();
+ this.undo = removeTxn.undo.bind(removeTxn);
+ this.redo = removeTxn.redo.bind(removeTxn);
+ }
+};
+
+/**
+ * Transaction for tagging urls.
+ *
+ * Required Input Properties: urls, tags.
+ */
+PT.Tag = DefineTransaction(["urls", "tags"]);
+PT.Tag.prototype = {
+ execute: function* (aURIs, aTags) {
+ let onUndo = [], onRedo = [];
+ for (let uri of aURIs) {
+ // Workaround bug 449811.
+ let currentURI = uri;
+
+ let promiseIsBookmarked = function* () {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncGetBookmarkIds(
+ currentURI, ids => { deferred.resolve(ids.length > 0); });
+ return deferred.promise;
+ };
+
+ if (yield promiseIsBookmarked(currentURI)) {
+ // Tagging is only allowed for bookmarked URIs (but see 424160).
+ let createTxn = TransactionsHistory.getRawTransaction(
+ PT.NewBookmark({ url: currentURI
+ , tags: aTags
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid }));
+ yield createTxn.execute();
+ onUndo.unshift(createTxn.undo.bind(createTxn));
+ onRedo.push(createTxn.redo.bind(createTxn));
+ }
+ else {
+ let currentTags = PlacesUtils.tagging.getTagsForURI(currentURI);
+ let newTags = aTags.filter(t => !currentTags.includes(t));
+ PlacesUtils.tagging.tagURI(currentURI, newTags);
+ onUndo.unshift(() => {
+ PlacesUtils.tagging.untagURI(currentURI, newTags);
+ });
+ onRedo.push(() => {
+ PlacesUtils.tagging.tagURI(currentURI, newTags);
+ });
+ }
+ }
+ this.undo = function* () {
+ for (let f of onUndo) {
+ yield f();
+ }
+ };
+ this.redo = function* () {
+ for (let f of onRedo) {
+ yield f();
+ }
+ };
+ }
+};
+
+/**
+ * Transaction for removing tags from a URI.
+ *
+ * Required Input Properties: urls.
+ * Optional Input Properties: tags.
+ *
+ * If |tags| is not set, all tags set for |url| are removed.
+ */
+PT.Untag = DefineTransaction(["urls"], ["tags"]);
+PT.Untag.prototype = {
+ execute: function* (aURIs, aTags) {
+ let onUndo = [], onRedo = [];
+ for (let uri of aURIs) {
+ // Workaround bug 449811.
+ let currentURI = uri;
+ let tagsToRemove;
+ let tagsSet = PlacesUtils.tagging.getTagsForURI(currentURI);
+ if (aTags.length > 0)
+ tagsToRemove = aTags.filter(t => tagsSet.includes(t));
+ else
+ tagsToRemove = tagsSet;
+ PlacesUtils.tagging.untagURI(currentURI, tagsToRemove);
+ onUndo.unshift(() => {
+ PlacesUtils.tagging.tagURI(currentURI, tagsToRemove);
+ });
+ onRedo.push(() => {
+ PlacesUtils.tagging.untagURI(currentURI, tagsToRemove);
+ });
+ }
+ this.undo = function* () {
+ for (let f of onUndo) {
+ yield f();
+ }
+ };
+ this.redo = function* () {
+ for (let f of onRedo) {
+ yield f();
+ }
+ };
+ }
+};
+
+/**
+ * Transaction for copying an item.
+ *
+ * Required Input Properties: guid, newParentGuid
+ * Optional Input Properties: newIndex, excludingAnnotations.
+ */
+PT.Copy = DefineTransaction(["guid", "newParentGuid"],
+ ["newIndex", "excludingAnnotations"]);
+PT.Copy.prototype = {
+ execute: function* (aGuid, aNewParentGuid, aNewIndex, aExcludingAnnotations) {
+ let creationInfo = null;
+ try {
+ creationInfo = yield PlacesUtils.promiseBookmarksTree(aGuid);
+ }
+ catch (ex) {
+ throw new Error("Failed to get info for the specified item (guid: " +
+ aGuid + "). Ex: " + ex);
+ }
+ creationInfo.parentGuid = aNewParentGuid;
+ creationInfo.index = aNewIndex;
+
+ let newItemId =
+ yield createItemsFromBookmarksTree(creationInfo, false,
+ aExcludingAnnotations);
+ let newItemInfo = null;
+ this.undo = function* () {
+ if (!newItemInfo) {
+ let newItemGuid = yield PlacesUtils.promiseItemGuid(newItemId);
+ newItemInfo = yield PlacesUtils.promiseBookmarksTree(newItemGuid);
+ }
+ PlacesUtils.bookmarks.removeItem(newItemId);
+ };
+ this.redo = function* () {
+ newItemId = yield createItemsFromBookmarksTree(newItemInfo, true);
+ }
+
+ return yield PlacesUtils.promiseItemGuid(newItemId);
+ }
+};
diff --git a/toolkit/components/places/PlacesUtils.jsm b/toolkit/components/places/PlacesUtils.jsm
new file mode 100644
index 0000000000..4b7bcb82a4
--- /dev/null
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -0,0 +1,3863 @@
+/* -*- 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/. */
+
+this.EXPORTED_SYMBOLS = [
+ "PlacesUtils"
+, "PlacesAggregatedTransaction"
+, "PlacesCreateFolderTransaction"
+, "PlacesCreateBookmarkTransaction"
+, "PlacesCreateSeparatorTransaction"
+, "PlacesCreateLivemarkTransaction"
+, "PlacesMoveItemTransaction"
+, "PlacesRemoveItemTransaction"
+, "PlacesEditItemTitleTransaction"
+, "PlacesEditBookmarkURITransaction"
+, "PlacesSetItemAnnotationTransaction"
+, "PlacesSetPageAnnotationTransaction"
+, "PlacesEditBookmarkKeywordTransaction"
+, "PlacesEditBookmarkPostDataTransaction"
+, "PlacesEditItemDateAddedTransaction"
+, "PlacesEditItemLastModifiedTransaction"
+, "PlacesSortFolderByNameTransaction"
+, "PlacesTagURITransaction"
+, "PlacesUntagURITransaction"
+];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
+ "resource://gre/modules/Bookmarks.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "History",
+ "resource://gre/modules/History.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesSyncUtils",
+ "resource://gre/modules/PlacesSyncUtils.jsm");
+
+// The minimum amount of transactions before starting a batch. Usually we do
+// do incremental updates, a batch will cause views to completely
+// refresh instead.
+const MIN_TRANSACTIONS_FOR_BATCH = 5;
+
+// On Mac OSX, the transferable system converts "\r\n" to "\n\n", where
+// we really just want "\n". On other platforms, the transferable system
+// converts "\r\n" to "\n".
+const NEWLINE = AppConstants.platform == "macosx" ? "\n" : "\r\n";
+
+function QI_node(aNode, aIID) {
+ var result = null;
+ try {
+ result = aNode.QueryInterface(aIID);
+ }
+ catch (e) {
+ }
+ return result;
+}
+function asContainer(aNode) {
+ return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
+}
+function asQuery(aNode) {
+ return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
+}
+
+/**
+ * Sends a bookmarks notification through the given observers.
+ *
+ * @param observers
+ * array of nsINavBookmarkObserver objects.
+ * @param notification
+ * the notification name.
+ * @param args
+ * array of arguments to pass to the notification.
+ */
+function notify(observers, notification, args) {
+ for (let observer of observers) {
+ try {
+ observer[notification](...args);
+ } catch (ex) {}
+ }
+}
+
+/**
+ * Sends a keyword change notification.
+ *
+ * @param url
+ * the url to notify about.
+ * @param keyword
+ * The keyword to notify, or empty string if a keyword was removed.
+ */
+function* notifyKeywordChange(url, keyword, source) {
+ // Notify bookmarks about the removal.
+ let bookmarks = [];
+ yield PlacesUtils.bookmarks.fetch({ url }, b => bookmarks.push(b));
+ // We don't want to yield in the gIgnoreKeywordNotifications section.
+ for (let bookmark of bookmarks) {
+ bookmark.id = yield PlacesUtils.promiseItemId(bookmark.guid);
+ bookmark.parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid);
+ }
+ let observers = PlacesUtils.bookmarks.getObservers();
+ gIgnoreKeywordNotifications = true;
+ for (let bookmark of bookmarks) {
+ notify(observers, "onItemChanged", [ bookmark.id, "keyword", false,
+ keyword,
+ bookmark.lastModified * 1000,
+ bookmark.type,
+ bookmark.parentId,
+ bookmark.guid, bookmark.parentGuid,
+ "", source
+ ]);
+ }
+ gIgnoreKeywordNotifications = false;
+}
+
+/**
+ * Serializes the given node in JSON format.
+ *
+ * @param aNode
+ * An nsINavHistoryResultNode
+ * @param aIsLivemark
+ * Whether the node represents a livemark.
+ */
+function serializeNode(aNode, aIsLivemark) {
+ let data = {};
+
+ data.title = aNode.title;
+ data.id = aNode.itemId;
+ data.livemark = aIsLivemark;
+
+ let guid = aNode.bookmarkGuid;
+ if (guid) {
+ data.itemGuid = guid;
+ if (aNode.parent)
+ data.parent = aNode.parent.itemId;
+ let grandParent = aNode.parent && aNode.parent.parent;
+ if (grandParent)
+ data.grandParentId = grandParent.itemId;
+
+ data.dateAdded = aNode.dateAdded;
+ data.lastModified = aNode.lastModified;
+
+ let annos = PlacesUtils.getAnnotationsForItem(data.id);
+ if (annos.length > 0)
+ data.annos = annos;
+ }
+
+ if (PlacesUtils.nodeIsURI(aNode)) {
+ // Check for url validity.
+ NetUtil.newURI(aNode.uri);
+
+ // Tag root accepts only folder nodes, not URIs.
+ if (data.parent == PlacesUtils.tagsFolderId)
+ throw new Error("Unexpected node type");
+
+ data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ data.uri = aNode.uri;
+
+ if (aNode.tags)
+ data.tags = aNode.tags;
+ }
+ else if (PlacesUtils.nodeIsContainer(aNode)) {
+ // Tag containers accept only uri nodes.
+ if (data.grandParentId == PlacesUtils.tagsFolderId)
+ throw new Error("Unexpected node type");
+
+ let concreteId = PlacesUtils.getConcreteItemId(aNode);
+ if (concreteId != -1) {
+ // This is a bookmark or a tag container.
+ if (PlacesUtils.nodeIsQuery(aNode) || concreteId != aNode.itemId) {
+ // This is a folder shortcut.
+ data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ data.uri = aNode.uri;
+ data.concreteId = concreteId;
+ }
+ else {
+ // This is a bookmark folder.
+ data.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+ }
+ }
+ else {
+ // This is a grouped container query, dynamically generated.
+ data.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ data.uri = aNode.uri;
+ }
+ }
+ else if (PlacesUtils.nodeIsSeparator(aNode)) {
+ // Tag containers don't accept separators.
+ if (data.parent == PlacesUtils.tagsFolderId ||
+ data.grandParentId == PlacesUtils.tagsFolderId)
+ throw new Error("Unexpected node type");
+
+ data.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+ }
+
+ return JSON.stringify(data);
+}
+
+// Imposed to limit database size.
+const DB_URL_LENGTH_MAX = 65536;
+const DB_TITLE_LENGTH_MAX = 4096;
+
+/**
+ * List of bookmark object validators, one per each known property.
+ * Validators must throw if the property value is invalid and return a fixed up
+ * version of the value, if needed.
+ */
+const BOOKMARK_VALIDATORS = Object.freeze({
+ guid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
+ parentGuid: simpleValidateFunc(v => typeof(v) == "string" &&
+ /^[a-zA-Z0-9\-_]{12}$/.test(v)),
+ index: simpleValidateFunc(v => Number.isInteger(v) &&
+ v >= PlacesUtils.bookmarks.DEFAULT_INDEX),
+ dateAdded: simpleValidateFunc(v => v.constructor.name == "Date"),
+ lastModified: simpleValidateFunc(v => v.constructor.name == "Date"),
+ type: simpleValidateFunc(v => Number.isInteger(v) &&
+ [ PlacesUtils.bookmarks.TYPE_BOOKMARK
+ , PlacesUtils.bookmarks.TYPE_FOLDER
+ , PlacesUtils.bookmarks.TYPE_SEPARATOR ].includes(v)),
+ title: v => {
+ simpleValidateFunc(val => val === null || typeof(val) == "string").call(this, v);
+ if (!v)
+ return null;
+ return v.slice(0, DB_TITLE_LENGTH_MAX);
+ },
+ url: v => {
+ simpleValidateFunc(val => (typeof(val) == "string" && val.length <= DB_URL_LENGTH_MAX) ||
+ (val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
+ (val instanceof URL && val.href.length <= DB_URL_LENGTH_MAX)
+ ).call(this, v);
+ if (typeof(v) === "string")
+ return new URL(v);
+ if (v instanceof Ci.nsIURI)
+ return new URL(v.spec);
+ return v;
+ },
+ source: simpleValidateFunc(v => Number.isInteger(v) &&
+ Object.values(PlacesUtils.bookmarks.SOURCES).includes(v)),
+});
+
+// Sync bookmark records can contain additional properties.
+const SYNC_BOOKMARK_VALIDATORS = Object.freeze({
+ // Sync uses Places GUIDs for all records except roots.
+ syncId: simpleValidateFunc(v => typeof v == "string" && (
+ (PlacesSyncUtils.bookmarks.ROOTS.includes(v) ||
+ PlacesUtils.isValidGuid(v)))),
+ parentSyncId: v => SYNC_BOOKMARK_VALIDATORS.syncId(v),
+ // Sync uses kinds instead of types, which distinguish between livemarks,
+ // queries, and smart bookmarks.
+ kind: simpleValidateFunc(v => typeof v == "string" &&
+ Object.values(PlacesSyncUtils.bookmarks.KINDS).includes(v)),
+ query: simpleValidateFunc(v => v === null || (typeof v == "string" && v)),
+ folder: simpleValidateFunc(v => typeof v == "string" && v &&
+ v.length <= Ci.nsITaggingService.MAX_TAG_LENGTH),
+ tags: v => {
+ if (v === null) {
+ return [];
+ }
+ if (!Array.isArray(v)) {
+ throw new Error("Invalid tag array");
+ }
+ for (let tag of v) {
+ if (typeof tag != "string" || !tag ||
+ tag.length > Ci.nsITaggingService.MAX_TAG_LENGTH) {
+ throw new Error(`Invalid tag: ${tag}`);
+ }
+ }
+ return v;
+ },
+ keyword: simpleValidateFunc(v => v === null || typeof v == "string"),
+ description: simpleValidateFunc(v => v === null || typeof v == "string"),
+ loadInSidebar: simpleValidateFunc(v => v === true || v === false),
+ feed: v => v === null ? v : BOOKMARK_VALIDATORS.url(v),
+ site: v => v === null ? v : BOOKMARK_VALIDATORS.url(v),
+ title: BOOKMARK_VALIDATORS.title,
+ url: BOOKMARK_VALIDATORS.url,
+});
+
+this.PlacesUtils = {
+ // Place entries that are containers, e.g. bookmark folders or queries.
+ TYPE_X_MOZ_PLACE_CONTAINER: "text/x-moz-place-container",
+ // Place entries that are bookmark separators.
+ TYPE_X_MOZ_PLACE_SEPARATOR: "text/x-moz-place-separator",
+ // Place entries that are not containers or separators
+ TYPE_X_MOZ_PLACE: "text/x-moz-place",
+ // Place entries in shortcut url format (url\ntitle)
+ TYPE_X_MOZ_URL: "text/x-moz-url",
+ // Place entries formatted as HTML anchors
+ TYPE_HTML: "text/html",
+ // Place entries as raw URL text
+ TYPE_UNICODE: "text/unicode",
+ // Used to track the action that populated the clipboard.
+ TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
+
+ EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
+ LMANNO_FEEDURI: "livemark/feedURI",
+ LMANNO_SITEURI: "livemark/siteURI",
+ POST_DATA_ANNO: "bookmarkProperties/POSTData",
+ READ_ONLY_ANNO: "placesInternal/READ_ONLY",
+ CHARSET_ANNO: "URIProperties/characterSet",
+ MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
+
+ TOPIC_SHUTDOWN: "places-shutdown",
+ TOPIC_INIT_COMPLETE: "places-init-complete",
+ TOPIC_DATABASE_LOCKED: "places-database-locked",
+ TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
+ TOPIC_FEEDBACK_UPDATED: "places-autocomplete-feedback-updated",
+ TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
+ TOPIC_VACUUM_STARTING: "places-vacuum-starting",
+ TOPIC_BOOKMARKS_RESTORE_BEGIN: "bookmarks-restore-begin",
+ TOPIC_BOOKMARKS_RESTORE_SUCCESS: "bookmarks-restore-success",
+ TOPIC_BOOKMARKS_RESTORE_FAILED: "bookmarks-restore-failed",
+
+ asContainer: aNode => asContainer(aNode),
+ asQuery: aNode => asQuery(aNode),
+
+ endl: NEWLINE,
+
+ /**
+ * Makes a URI from a spec.
+ * @param aSpec
+ * The string spec of the URI
+ * @returns A URI object for the spec.
+ */
+ _uri: function PU__uri(aSpec) {
+ return NetUtil.newURI(aSpec);
+ },
+
+ /**
+ * Is a string a valid GUID?
+ *
+ * @param guid: (String)
+ * @return (Boolean)
+ */
+ isValidGuid(guid) {
+ return typeof guid == "string" && guid &&
+ (/^[a-zA-Z0-9\-_]{12}$/.test(guid));
+ },
+
+ /**
+ * Converts a string or n URL object to an nsIURI.
+ *
+ * @param url (URL) or (String)
+ * the URL to convert.
+ * @return nsIURI for the given URL.
+ */
+ toURI(url) {
+ url = (url instanceof URL) ? url.href : url;
+
+ return NetUtil.newURI(url);
+ },
+
+ /**
+ * Convert a Date object to a PRTime (microseconds).
+ *
+ * @param date
+ * the Date object to convert.
+ * @return microseconds from the epoch.
+ */
+ toPRTime(date) {
+ return date * 1000;
+ },
+
+ /**
+ * Convert a PRTime to a Date object.
+ *
+ * @param time
+ * microseconds from the epoch.
+ * @return a Date object.
+ */
+ toDate(time) {
+ return new Date(parseInt(time / 1000));
+ },
+
+ /**
+ * Wraps a string in a nsISupportsString wrapper.
+ * @param aString
+ * The string to wrap.
+ * @returns A nsISupportsString object containing a string.
+ */
+ toISupportsString: function PU_toISupportsString(aString) {
+ let s = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ s.data = aString;
+ return s;
+ },
+
+ getFormattedString: function PU_getFormattedString(key, params) {
+ return bundle.formatStringFromName(key, params, params.length);
+ },
+
+ getString: function PU_getString(key) {
+ return bundle.GetStringFromName(key);
+ },
+
+ /**
+ * Makes a moz-action URI for the given action and set of parameters.
+ *
+ * @param type
+ * The action type.
+ * @param params
+ * A JS object of action params.
+ * @returns A moz-action URI as a string.
+ */
+ mozActionURI(type, params) {
+ let encodedParams = {};
+ for (let key in params) {
+ // Strip null or undefined.
+ // Regardless, don't encode them or they would be converted to a string.
+ if (params[key] === null || params[key] === undefined) {
+ continue;
+ }
+ encodedParams[key] = encodeURIComponent(params[key]);
+ }
+ return "moz-action:" + type + "," + JSON.stringify(encodedParams);
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a Bookmark folder.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a Bookmark folder, false otherwise
+ */
+ nodeIsFolder: function PU_nodeIsFolder(aNode) {
+ return (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
+ aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT);
+ },
+
+ /**
+ * Determines whether or not a ResultNode represents a bookmarked URI.
+ * @param aNode
+ * A result node
+ * @returns true if the node represents a bookmarked URI, false otherwise
+ */
+ nodeIsBookmark: function PU_nodeIsBookmark(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI &&
+ aNode.itemId != -1;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a Bookmark separator.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a Bookmark separator, false otherwise
+ */
+ nodeIsSeparator: function PU_nodeIsSeparator(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a URL item.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a URL item, false otherwise
+ */
+ nodeIsURI: function PU_nodeIsURI(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a Query item.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a Query item, false otherwise
+ */
+ nodeIsQuery: function PU_nodeIsQuery(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+ },
+
+ /**
+ * Generator for a node's ancestors.
+ * @param aNode
+ * A result node
+ */
+ nodeAncestors: function* PU_nodeAncestors(aNode) {
+ let node = aNode.parent;
+ while (node) {
+ yield node;
+ node = node.parent;
+ }
+ },
+
+ /**
+ * Checks validity of an object, filling up default values for optional
+ * properties.
+ *
+ * @param validators (object)
+ * An object containing input validators. Keys should be field names;
+ * values should be validation functions.
+ * @param props (object)
+ * The object to validate.
+ * @param behavior (object) [optional]
+ * Object defining special behavior for some of the properties.
+ * The following behaviors may be optionally set:
+ * - requiredIf: if the provided condition is satisfied, then this
+ * property is required.
+ * - validIf: if the provided condition is not satisfied, then this
+ * property is invalid.
+ * - defaultValue: an undefined property should default to this value.
+ *
+ * @return a validated and normalized item.
+ * @throws if the object contains invalid data.
+ * @note any unknown properties are pass-through.
+ */
+ validateItemProperties(validators, props, behavior={}) {
+ if (!props)
+ throw new Error("Input should be a valid object");
+ // Make a shallow copy of `props` to avoid mutating the original object
+ // when filling in defaults.
+ let input = Object.assign({}, props);
+ let normalizedInput = {};
+ let required = new Set();
+ for (let prop in behavior) {
+ if (behavior[prop].hasOwnProperty("required") && behavior[prop].required) {
+ required.add(prop);
+ }
+ if (behavior[prop].hasOwnProperty("requiredIf") && behavior[prop].requiredIf(input)) {
+ required.add(prop);
+ }
+ if (behavior[prop].hasOwnProperty("validIf") && input[prop] !== undefined &&
+ !behavior[prop].validIf(input)) {
+ throw new Error(`Invalid value for property '${prop}': ${input[prop]}`);
+ }
+ if (behavior[prop].hasOwnProperty("defaultValue") && input[prop] === undefined) {
+ input[prop] = behavior[prop].defaultValue;
+ }
+ }
+
+ for (let prop in input) {
+ if (required.has(prop)) {
+ required.delete(prop);
+ } else if (input[prop] === undefined) {
+ // Skip undefined properties that are not required.
+ continue;
+ }
+ if (validators.hasOwnProperty(prop)) {
+ try {
+ normalizedInput[prop] = validators[prop](input[prop], input);
+ } catch (ex) {
+ throw new Error(`Invalid value for property '${prop}': ${input[prop]}`);
+ }
+ }
+ }
+ if (required.size > 0)
+ throw new Error(`The following properties were expected: ${[...required].join(", ")}`);
+ return normalizedInput;
+ },
+
+ BOOKMARK_VALIDATORS,
+ SYNC_BOOKMARK_VALIDATORS,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver
+ , Ci.nsITransactionListener
+ ]),
+
+ _shutdownFunctions: [],
+ registerShutdownFunction: function PU_registerShutdownFunction(aFunc)
+ {
+ // If this is the first registered function, add the shutdown observer.
+ if (this._shutdownFunctions.length == 0) {
+ Services.obs.addObserver(this, this.TOPIC_SHUTDOWN, false);
+ }
+ this._shutdownFunctions.push(aFunc);
+ },
+
+ // nsIObserver
+ observe: function PU_observe(aSubject, aTopic, aData)
+ {
+ switch (aTopic) {
+ case this.TOPIC_SHUTDOWN:
+ Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
+ while (this._shutdownFunctions.length > 0) {
+ this._shutdownFunctions.shift().apply(this);
+ }
+ if (this._bookmarksServiceObserversQueue.length > 0) {
+ // Since we are shutting down, there's no reason to add the observers.
+ this._bookmarksServiceObserversQueue.length = 0;
+ }
+ break;
+ case "bookmarks-service-ready":
+ this._bookmarksServiceReady = true;
+ while (this._bookmarksServiceObserversQueue.length > 0) {
+ let observerInfo = this._bookmarksServiceObserversQueue.shift();
+ this.bookmarks.addObserver(observerInfo.observer, observerInfo.weak);
+ }
+
+ // Initialize the keywords cache to start observing bookmarks
+ // notifications. This is needed as far as we support both the old and
+ // the new bookmarking APIs at the same time.
+ gKeywordsCachePromise.catch(Cu.reportError);
+ break;
+ }
+ },
+
+ onPageAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+
+
+ // nsITransactionListener
+
+ didDo: function PU_didDo(aManager, aTransaction, aDoResult)
+ {
+ updateCommandsOnActiveWindow();
+ },
+
+ didUndo: function PU_didUndo(aManager, aTransaction, aUndoResult)
+ {
+ updateCommandsOnActiveWindow();
+ },
+
+ didRedo: function PU_didRedo(aManager, aTransaction, aRedoResult)
+ {
+ updateCommandsOnActiveWindow();
+ },
+
+ didBeginBatch: function PU_didBeginBatch(aManager, aResult)
+ {
+ // A no-op transaction is pushed to the stack, in order to make safe and
+ // easy to implement "Undo" an unknown number of transactions (including 0),
+ // "above" beginBatch and endBatch. Otherwise,implementing Undo that way
+ // head to dataloss: for example, if no changes were done in the
+ // edit-item panel, the last transaction on the undo stack would be the
+ // initial createItem transaction, or even worse, the batched editing of
+ // some other item.
+ // DO NOT MOVE this to the window scope, that would leak (bug 490068)!
+ this.transactionManager.doTransaction({ doTransaction: function() {},
+ undoTransaction: function() {},
+ redoTransaction: function() {},
+ isTransient: false,
+ merge: function() { return false; }
+ });
+ },
+
+ willDo: function PU_willDo() {},
+ willUndo: function PU_willUndo() {},
+ willRedo: function PU_willRedo() {},
+ willBeginBatch: function PU_willBeginBatch() {},
+ willEndBatch: function PU_willEndBatch() {},
+ didEndBatch: function PU_didEndBatch() {},
+ willMerge: function PU_willMerge() {},
+ didMerge: function PU_didMerge() {},
+
+ /**
+ * Determines whether or not a ResultNode is a host container.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a host container, false otherwise
+ */
+ nodeIsHost: function PU_nodeIsHost(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
+ aNode.parent &&
+ asQuery(aNode.parent).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a day container.
+ * @param node
+ * A NavHistoryResultNode
+ * @returns true if the node is a day container, false otherwise
+ */
+ nodeIsDay: function PU_nodeIsDay(aNode) {
+ var resultType;
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
+ aNode.parent &&
+ ((resultType = asQuery(aNode.parent).queryOptions.resultType) ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
+ },
+
+ /**
+ * Determines whether or not a result-node is a tag container.
+ * @param aNode
+ * A result-node
+ * @returns true if the node is a tag container, false otherwise
+ */
+ nodeIsTagQuery: function PU_nodeIsTagQuery(aNode) {
+ return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
+ asQuery(aNode).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS;
+ },
+
+ /**
+ * Determines whether or not a ResultNode is a container.
+ * @param aNode
+ * A result node
+ * @returns true if the node is a container item, false otherwise
+ */
+ containerTypes: [Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY],
+ nodeIsContainer: function PU_nodeIsContainer(aNode) {
+ return this.containerTypes.includes(aNode.type);
+ },
+
+ /**
+ * Determines whether or not a ResultNode is an history related container.
+ * @param node
+ * A result node
+ * @returns true if the node is an history related container, false otherwise
+ */
+ nodeIsHistoryContainer: function PU_nodeIsHistoryContainer(aNode) {
+ var resultType;
+ return this.nodeIsQuery(aNode) &&
+ ((resultType = asQuery(aNode).queryOptions.resultType) ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
+ resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
+ this.nodeIsDay(aNode) ||
+ this.nodeIsHost(aNode));
+ },
+
+ /**
+ * Gets the concrete item-id for the given node. Generally, this is just
+ * node.itemId, but for folder-shortcuts that's node.folderItemId.
+ */
+ getConcreteItemId: function PU_getConcreteItemId(aNode) {
+ if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
+ return asQuery(aNode).folderItemId;
+ else if (PlacesUtils.nodeIsTagQuery(aNode)) {
+ // RESULTS_AS_TAG_CONTENTS queries are similar to folder shortcuts
+ // so we can still get the concrete itemId for them.
+ var queries = aNode.getQueries();
+ var folders = queries[0].getFolders();
+ return folders[0];
+ }
+ return aNode.itemId;
+ },
+
+ /**
+ * Gets the concrete item-guid for the given node. For everything but folder
+ * shortcuts, this is just node.bookmarkGuid. For folder shortcuts, this is
+ * node.targetFolderGuid (see nsINavHistoryService.idl for the semantics).
+ *
+ * @param aNode
+ * a result node.
+ * @return the concrete item-guid for aNode.
+ * @note unlike getConcreteItemId, this doesn't allow retrieving the guid of a
+ * ta container.
+ */
+ getConcreteItemGuid(aNode) {
+ if (aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT)
+ return asQuery(aNode).targetFolderGuid;
+ return aNode.bookmarkGuid;
+ },
+
+ /**
+ * Reverse a host based on the moz_places algorithm, that is reverse the host
+ * string and add a trailing period. For example "google.com" becomes
+ * "moc.elgoog.".
+ *
+ * @param url
+ * the URL to generate a rev host for.
+ * @return the reversed host string.
+ */
+ getReversedHost(url) {
+ return url.host.split("").reverse().join("") + ".";
+ },
+
+ /**
+ * String-wraps a result node according to the rules of the specified
+ * content type for copy or move operations.
+ *
+ * @param aNode
+ * The Result node to wrap (serialize)
+ * @param aType
+ * The content type to serialize as
+ * @param [optional] aFeedURI
+ * Used instead of the node's URI if provided.
+ * This is useful for wrapping a livemark as TYPE_X_MOZ_URL,
+ * TYPE_HTML or TYPE_UNICODE.
+ * @return A string serialization of the node
+ */
+ wrapNode(aNode, aType, aFeedURI) {
+ // when wrapping a node, we want all the items, even if the original
+ // query options are excluding them.
+ // This can happen when copying from the left hand pane of the bookmarks
+ // organizer.
+ // @return [node, shouldClose]
+ function gatherDataFromNode(node, gatherDataFunc) {
+ if (PlacesUtils.nodeIsFolder(node) &&
+ node.type != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT &&
+ asQuery(node).queryOptions.excludeItems) {
+ let folderRoot = PlacesUtils.getFolderContents(node.itemId, false, true).root;
+ try {
+ return gatherDataFunc(folderRoot);
+ } finally {
+ folderRoot.containerOpen = false;
+ }
+ }
+ // If we didn't create our own query, do not alter the node's state.
+ return gatherDataFunc(node);
+ }
+
+ function gatherDataHtml(node) {
+ let htmlEscape = s => s.replace(/&/g, "&amp;")
+ .replace(/>/g, "&gt;")
+ .replace(/</g, "&lt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&apos;");
+
+ // escape out potential HTML in the title
+ let escapedTitle = node.title ? htmlEscape(node.title) : "";
+
+ if (aFeedURI) {
+ return `<A HREF="${aFeedURI}">${escapedTitle}</A>${NEWLINE}`;
+ }
+
+ if (PlacesUtils.nodeIsContainer(node)) {
+ asContainer(node);
+ let wasOpen = node.containerOpen;
+ if (!wasOpen)
+ node.containerOpen = true;
+
+ let childString = "<DL><DT>" + escapedTitle + "</DT>" + NEWLINE;
+ let cc = node.childCount;
+ for (let i = 0; i < cc; ++i) {
+ childString += "<DD>"
+ + NEWLINE
+ + gatherDataHtml(node.getChild(i))
+ + "</DD>"
+ + NEWLINE;
+ }
+ node.containerOpen = wasOpen;
+ return childString + "</DL>" + NEWLINE;
+ }
+ if (PlacesUtils.nodeIsURI(node))
+ return `<A HREF="${node.uri}">${escapedTitle}</A>${NEWLINE}`;
+ if (PlacesUtils.nodeIsSeparator(node))
+ return "<HR>" + NEWLINE;
+ return "";
+ }
+
+ function gatherDataText(node) {
+ if (aFeedURI) {
+ return aFeedURI;
+ }
+
+ if (PlacesUtils.nodeIsContainer(node)) {
+ asContainer(node);
+ let wasOpen = node.containerOpen;
+ if (!wasOpen)
+ node.containerOpen = true;
+
+ let childString = node.title + NEWLINE;
+ let cc = node.childCount;
+ for (let i = 0; i < cc; ++i) {
+ let child = node.getChild(i);
+ let suffix = i < (cc - 1) ? NEWLINE : "";
+ childString += gatherDataText(child) + suffix;
+ }
+ node.containerOpen = wasOpen;
+ return childString;
+ }
+ if (PlacesUtils.nodeIsURI(node))
+ return node.uri;
+ if (PlacesUtils.nodeIsSeparator(node))
+ return "--------------------";
+ return "";
+ }
+
+ switch (aType) {
+ case this.TYPE_X_MOZ_PLACE:
+ case this.TYPE_X_MOZ_PLACE_SEPARATOR:
+ case this.TYPE_X_MOZ_PLACE_CONTAINER: {
+ // Serialize the node to JSON.
+ return serializeNode(aNode, aFeedURI);
+ }
+ case this.TYPE_X_MOZ_URL: {
+ if (aFeedURI || PlacesUtils.nodeIsURI(aNode))
+ return (aFeedURI || aNode.uri) + NEWLINE + aNode.title;
+ return "";
+ }
+ case this.TYPE_HTML: {
+ return gatherDataFromNode(aNode, gatherDataHtml);
+ }
+ }
+
+ // Otherwise, we wrap as TYPE_UNICODE.
+ return gatherDataFromNode(aNode, gatherDataText);
+ },
+
+ /**
+ * Unwraps data from the Clipboard or the current Drag Session.
+ * @param blob
+ * A blob (string) of data, in some format we potentially know how
+ * to parse.
+ * @param type
+ * The content type of the blob.
+ * @returns An array of objects representing each item contained by the source.
+ */
+ unwrapNodes: function PU_unwrapNodes(blob, type) {
+ // We split on "\n" because the transferable system converts "\r\n" to "\n"
+ var nodes = [];
+ switch (type) {
+ case this.TYPE_X_MOZ_PLACE:
+ case this.TYPE_X_MOZ_PLACE_SEPARATOR:
+ case this.TYPE_X_MOZ_PLACE_CONTAINER:
+ nodes = JSON.parse("[" + blob + "]");
+ break;
+ case this.TYPE_X_MOZ_URL: {
+ let parts = blob.split("\n");
+ // data in this type has 2 parts per entry, so if there are fewer
+ // than 2 parts left, the blob is malformed and we should stop
+ // but drag and drop of files from the shell has parts.length = 1
+ if (parts.length != 1 && parts.length % 2)
+ break;
+ for (let i = 0; i < parts.length; i=i+2) {
+ let uriString = parts[i];
+ let titleString = "";
+ if (parts.length > i+1)
+ titleString = parts[i+1];
+ else {
+ // for drag and drop of files, try to use the leafName as title
+ try {
+ titleString = this._uri(uriString).QueryInterface(Ci.nsIURL)
+ .fileName;
+ }
+ catch (e) {}
+ }
+ // note: this._uri() will throw if uriString is not a valid URI
+ if (this._uri(uriString)) {
+ nodes.push({ uri: uriString,
+ title: titleString ? titleString : uriString,
+ type: this.TYPE_X_MOZ_URL });
+ }
+ }
+ break;
+ }
+ case this.TYPE_UNICODE: {
+ let parts = blob.split("\n");
+ for (let i = 0; i < parts.length; i++) {
+ let uriString = parts[i];
+ // text/uri-list is converted to TYPE_UNICODE but it could contain
+ // comments line prepended by #, we should skip them
+ if (uriString.substr(0, 1) == '\x23')
+ continue;
+ // note: this._uri() will throw if uriString is not a valid URI
+ if (uriString != "" && this._uri(uriString))
+ nodes.push({ uri: uriString,
+ title: uriString,
+ type: this.TYPE_X_MOZ_URL });
+ }
+ break;
+ }
+ default:
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ return nodes;
+ },
+
+ /**
+ * Generates a nsINavHistoryResult for the contents of a folder.
+ * @param folderId
+ * The folder to open
+ * @param [optional] excludeItems
+ * True to hide all items (individual bookmarks). This is used on
+ * the left places pane so you just get a folder hierarchy.
+ * @param [optional] expandQueries
+ * True to make query items expand as new containers. For managing,
+ * you want this to be false, for menus and such, you want this to
+ * be true.
+ * @returns A nsINavHistoryResult containing the contents of the
+ * folder. The result.root is guaranteed to be open.
+ */
+ getFolderContents:
+ function PU_getFolderContents(aFolderId, aExcludeItems, aExpandQueries) {
+ var query = this.history.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ var options = this.history.getNewQueryOptions();
+ options.excludeItems = aExcludeItems;
+ options.expandQueries = aExpandQueries;
+
+ var result = this.history.executeQuery(query, options);
+ result.root.containerOpen = true;
+ return result;
+ },
+
+ /**
+ * Fetch all annotations for a URI, including all properties of each
+ * annotation which would be required to recreate it.
+ * @param aURI
+ * The URI for which annotations are to be retrieved.
+ * @return Array of objects, each containing the following properties:
+ * name, flags, expires, value
+ */
+ getAnnotationsForURI: function PU_getAnnotationsForURI(aURI) {
+ var annosvc = this.annotations;
+ var annos = [], val = null;
+ var annoNames = annosvc.getPageAnnotationNames(aURI);
+ for (var i = 0; i < annoNames.length; i++) {
+ var flags = {}, exp = {}, storageType = {};
+ annosvc.getPageAnnotationInfo(aURI, annoNames[i], flags, exp, storageType);
+ val = annosvc.getPageAnnotation(aURI, annoNames[i]);
+ annos.push({name: annoNames[i],
+ flags: flags.value,
+ expires: exp.value,
+ value: val});
+ }
+ return annos;
+ },
+
+ /**
+ * Fetch all annotations for an item, including all properties of each
+ * annotation which would be required to recreate it.
+ * @param aItemId
+ * The identifier of the itme for which annotations are to be
+ * retrieved.
+ * @return Array of objects, each containing the following properties:
+ * name, flags, expires, mimeType, type, value
+ */
+ getAnnotationsForItem: function PU_getAnnotationsForItem(aItemId) {
+ var annosvc = this.annotations;
+ var annos = [], val = null;
+ var annoNames = annosvc.getItemAnnotationNames(aItemId);
+ for (var i = 0; i < annoNames.length; i++) {
+ var flags = {}, exp = {}, storageType = {};
+ annosvc.getItemAnnotationInfo(aItemId, annoNames[i], flags, exp, storageType);
+ val = annosvc.getItemAnnotation(aItemId, annoNames[i]);
+ annos.push({name: annoNames[i],
+ flags: flags.value,
+ expires: exp.value,
+ value: val});
+ }
+ return annos;
+ },
+
+ /**
+ * Annotate a URI with a batch of annotations.
+ * @param aURI
+ * The URI for which annotations are to be set.
+ * @param aAnnotations
+ * Array of objects, each containing the following properties:
+ * name, flags, expires.
+ * If the value for an annotation is not set it will be removed.
+ */
+ setAnnotationsForURI: function PU_setAnnotationsForURI(aURI, aAnnos) {
+ var annosvc = this.annotations;
+ aAnnos.forEach(function(anno) {
+ if (anno.value === undefined || anno.value === null) {
+ annosvc.removePageAnnotation(aURI, anno.name);
+ }
+ else {
+ let flags = ("flags" in anno) ? anno.flags : 0;
+ let expires = ("expires" in anno) ?
+ anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
+ annosvc.setPageAnnotation(aURI, anno.name, anno.value, flags, expires);
+ }
+ });
+ },
+
+ /**
+ * Annotate an item with a batch of annotations.
+ * @param aItemId
+ * The identifier of the item for which annotations are to be set
+ * @param aAnnotations
+ * Array of objects, each containing the following properties:
+ * name, flags, expires.
+ * If the value for an annotation is not set it will be removed.
+ */
+ setAnnotationsForItem: function PU_setAnnotationsForItem(aItemId, aAnnos, aSource) {
+ var annosvc = this.annotations;
+
+ aAnnos.forEach(function(anno) {
+ if (anno.value === undefined || anno.value === null) {
+ annosvc.removeItemAnnotation(aItemId, anno.name, aSource);
+ }
+ else {
+ let flags = ("flags" in anno) ? anno.flags : 0;
+ let expires = ("expires" in anno) ?
+ anno.expires : Ci.nsIAnnotationService.EXPIRE_NEVER;
+ annosvc.setItemAnnotation(aItemId, anno.name, anno.value, flags,
+ expires, aSource);
+ }
+ });
+ },
+
+ // Identifier getters for special folders.
+ // You should use these everywhere PlacesUtils is available to avoid XPCOM
+ // traversal just to get roots' ids.
+ get placesRootId() {
+ delete this.placesRootId;
+ return this.placesRootId = this.bookmarks.placesRoot;
+ },
+
+ get bookmarksMenuFolderId() {
+ delete this.bookmarksMenuFolderId;
+ return this.bookmarksMenuFolderId = this.bookmarks.bookmarksMenuFolder;
+ },
+
+ get toolbarFolderId() {
+ delete this.toolbarFolderId;
+ return this.toolbarFolderId = this.bookmarks.toolbarFolder;
+ },
+
+ get tagsFolderId() {
+ delete this.tagsFolderId;
+ return this.tagsFolderId = this.bookmarks.tagsFolder;
+ },
+
+ get unfiledBookmarksFolderId() {
+ delete this.unfiledBookmarksFolderId;
+ return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
+ },
+
+ get mobileFolderId() {
+ delete this.mobileFolderId;
+ return this.mobileFolderId = this.bookmarks.mobileFolder;
+ },
+
+ /**
+ * Checks if aItemId is a root.
+ *
+ * @param aItemId
+ * item id to look for.
+ * @returns true if aItemId is a root, false otherwise.
+ */
+ isRootItem: function PU_isRootItem(aItemId) {
+ return aItemId == PlacesUtils.bookmarksMenuFolderId ||
+ aItemId == PlacesUtils.toolbarFolderId ||
+ aItemId == PlacesUtils.unfiledBookmarksFolderId ||
+ aItemId == PlacesUtils.tagsFolderId ||
+ aItemId == PlacesUtils.placesRootId ||
+ aItemId == PlacesUtils.mobileFolderId;
+ },
+
+ /**
+ * Set the POST data associated with a bookmark, if any.
+ * Used by POST keywords.
+ * @param aBookmarkId
+ *
+ * @deprecated Use PlacesUtils.keywords.insert() API instead.
+ */
+ setPostDataForBookmark(aBookmarkId, aPostData) {
+ if (!aPostData)
+ throw new Error("Must provide valid POST data");
+ // For now we don't have a unified API to create a keyword with postData,
+ // thus here we can just try to complete a keyword that should already exist
+ // without any post data.
+ let stmt = PlacesUtils.history.DBConnection.createStatement(
+ `UPDATE moz_keywords SET post_data = :post_data
+ WHERE id = (SELECT k.id FROM moz_keywords k
+ JOIN moz_bookmarks b ON b.fk = k.place_id
+ WHERE b.id = :item_id
+ AND post_data ISNULL
+ LIMIT 1)`);
+ stmt.params.item_id = aBookmarkId;
+ stmt.params.post_data = aPostData;
+ try {
+ stmt.execute();
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Update the cache.
+ return Task.spawn(function* () {
+ let guid = yield PlacesUtils.promiseItemGuid(aBookmarkId);
+ let bm = yield PlacesUtils.bookmarks.fetch(guid);
+
+ // Fetch keywords for this href.
+ let cache = yield gKeywordsCachePromise;
+ for (let [ , entry ] of cache) {
+ // Set the POST data on keywords not having it.
+ if (entry.url.href == bm.url.href && !entry.postData) {
+ entry.postData = aPostData;
+ }
+ }
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * Get the POST data associated with a bookmark, if any.
+ * @param aBookmarkId
+ * @returns string of POST data if set for aBookmarkId. null otherwise.
+ *
+ * @deprecated Use PlacesUtils.keywords.fetch() API instead.
+ */
+ getPostDataForBookmark(aBookmarkId) {
+ let stmt = PlacesUtils.history.DBConnection.createStatement(
+ `SELECT k.post_data
+ FROM moz_keywords k
+ JOIN moz_places h ON h.id = k.place_id
+ JOIN moz_bookmarks b ON b.fk = h.id
+ WHERE b.id = :item_id`);
+ stmt.params.item_id = aBookmarkId;
+ try {
+ if (!stmt.executeStep())
+ return null;
+ return stmt.row.post_data;
+ }
+ finally {
+ stmt.finalize();
+ }
+ },
+
+ /**
+ * Get the URI (and any associated POST data) for a given keyword.
+ * @param aKeyword string keyword
+ * @returns an array containing a string URL and a string of POST data
+ *
+ * @deprecated
+ */
+ getURLAndPostDataForKeyword(aKeyword) {
+ Deprecated.warning("getURLAndPostDataForKeyword() is deprecated, please " +
+ "use PlacesUtils.keywords.fetch() instead",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294");
+
+ let stmt = PlacesUtils.history.DBConnection.createStatement(
+ `SELECT h.url, k.post_data
+ FROM moz_keywords k
+ JOIN moz_places h ON h.id = k.place_id
+ WHERE k.keyword = :keyword`);
+ stmt.params.keyword = aKeyword.toLowerCase();
+ try {
+ if (!stmt.executeStep())
+ return [ null, null ];
+ return [ stmt.row.url, stmt.row.post_data ];
+ }
+ finally {
+ stmt.finalize();
+ }
+ },
+
+ /**
+ * Get all bookmarks for a URL, excluding items under tags.
+ */
+ getBookmarksForURI:
+ function PU_getBookmarksForURI(aURI) {
+ var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
+
+ // filter the ids list
+ return bmkIds.filter(function(aID) {
+ var parentId = this.bookmarks.getFolderIdForItem(aID);
+ var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
+ // item under a tag container
+ if (grandparentId == this.tagsFolderId)
+ return false;
+ return true;
+ }, this);
+ },
+
+ /**
+ * Get the most recently added/modified bookmark for a URL, excluding items
+ * under tags.
+ *
+ * @param aURI
+ * nsIURI of the page we will look for.
+ * @returns itemId of the found bookmark, or -1 if nothing is found.
+ */
+ getMostRecentBookmarkForURI:
+ function PU_getMostRecentBookmarkForURI(aURI) {
+ var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
+ for (var i = 0; i < bmkIds.length; i++) {
+ // Find the first folder which isn't a tag container
+ var itemId = bmkIds[i];
+ var parentId = this.bookmarks.getFolderIdForItem(itemId);
+ // Optimization: if this is a direct child of a root we don't need to
+ // check if its grandparent is a tag.
+ if (parentId == this.unfiledBookmarksFolderId ||
+ parentId == this.toolbarFolderId ||
+ parentId == this.bookmarksMenuFolderId)
+ return itemId;
+
+ var grandparentId = this.bookmarks.getFolderIdForItem(parentId);
+ if (grandparentId != this.tagsFolderId)
+ return itemId;
+ }
+ return -1;
+ },
+
+ /**
+ * Returns a nsNavHistoryContainerResultNode with forced excludeItems and
+ * expandQueries.
+ * @param aNode
+ * The node to convert
+ * @param [optional] excludeItems
+ * True to hide all items (individual bookmarks). This is used on
+ * the left places pane so you just get a folder hierarchy.
+ * @param [optional] expandQueries
+ * True to make query items expand as new containers. For managing,
+ * you want this to be false, for menus and such, you want this to
+ * be true.
+ * @returns A nsINavHistoryContainerResultNode containing the unfiltered
+ * contents of the container.
+ * @note The returned container node could be open or closed, we don't
+ * guarantee its status.
+ */
+ getContainerNodeWithOptions:
+ function PU_getContainerNodeWithOptions(aNode, aExcludeItems, aExpandQueries) {
+ if (!this.nodeIsContainer(aNode))
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ // excludeItems is inherited by child containers in an excludeItems view.
+ var excludeItems = asQuery(aNode).queryOptions.excludeItems ||
+ asQuery(aNode.parentResult.root).queryOptions.excludeItems;
+ // expandQueries is inherited by child containers in an expandQueries view.
+ var expandQueries = asQuery(aNode).queryOptions.expandQueries &&
+ asQuery(aNode.parentResult.root).queryOptions.expandQueries;
+
+ // If our options are exactly what we expect, directly return the node.
+ if (excludeItems == aExcludeItems && expandQueries == aExpandQueries)
+ return aNode;
+
+ // Otherwise, get contents manually.
+ var queries = {}, options = {};
+ this.history.queryStringToQueries(aNode.uri, queries, {}, options);
+ options.value.excludeItems = aExcludeItems;
+ options.value.expandQueries = aExpandQueries;
+ return this.history.executeQueries(queries.value,
+ queries.value.length,
+ options.value).root;
+ },
+
+ /**
+ * Returns true if a container has uri nodes in its first level.
+ * Has better performance than (getURLsForContainerNode(node).length > 0).
+ * @param aNode
+ * The container node to search through.
+ * @returns true if the node contains uri nodes, false otherwise.
+ */
+ hasChildURIs: function PU_hasChildURIs(aNode) {
+ if (!this.nodeIsContainer(aNode))
+ return false;
+
+ let root = this.getContainerNodeWithOptions(aNode, false, true);
+ let result = root.parentResult;
+ let didSuppressNotifications = false;
+ let wasOpen = root.containerOpen;
+ if (!wasOpen) {
+ didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ root.containerOpen = true;
+ }
+
+ let found = false;
+ for (let i = 0; i < root.childCount && !found; i++) {
+ let child = root.getChild(i);
+ if (this.nodeIsURI(child))
+ found = true;
+ }
+
+ if (!wasOpen) {
+ root.containerOpen = false;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ return found;
+ },
+
+ /**
+ * Returns an array containing all the uris in the first level of the
+ * passed in container.
+ * If you only need to know if the node contains uris, use hasChildURIs.
+ * @param aNode
+ * The container node to search through
+ * @returns array of uris in the first level of the container.
+ */
+ getURLsForContainerNode: function PU_getURLsForContainerNode(aNode) {
+ let urls = [];
+ if (!this.nodeIsContainer(aNode))
+ return urls;
+
+ let root = this.getContainerNodeWithOptions(aNode, false, true);
+ let result = root.parentResult;
+ let wasOpen = root.containerOpen;
+ let didSuppressNotifications = false;
+ if (!wasOpen) {
+ didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ root.containerOpen = true;
+ }
+
+ for (let i = 0; i < root.childCount; ++i) {
+ let child = root.getChild(i);
+ if (this.nodeIsURI(child))
+ urls.push({uri: child.uri, isBookmark: this.nodeIsBookmark(child)});
+ }
+
+ if (!wasOpen) {
+ root.containerOpen = false;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ return urls;
+ },
+
+ /**
+ * Gets a shared Sqlite.jsm readonly connection to the Places database,
+ * usable only for SELECT queries.
+ *
+ * This is intended to be used mostly internally, components outside of
+ * Places should, when possible, use API calls and file bugs to get proper
+ * APIs, where they are missing.
+ * Keep in mind the Places DB schema is by no means frozen or even stable.
+ * Your custom queries can - and will - break overtime.
+ *
+ * Example:
+ * let db = yield PlacesUtils.promiseDBConnection();
+ * let rows = yield db.executeCached(sql, params);
+ */
+ promiseDBConnection: () => gAsyncDBConnPromised,
+
+ /**
+ * Performs a read/write operation on the Places database through a Sqlite.jsm
+ * wrapped connection to the Places database.
+ *
+ * This is intended to be used only by Places itself, always use APIs if you
+ * need to modify the Places database. Use promiseDBConnection if you need to
+ * SELECT from the database and there's no covering API.
+ * Keep in mind the Places DB schema is by no means frozen or even stable.
+ * Your custom queries can - and will - break overtime.
+ *
+ * As all operations on the Places database are asynchronous, if shutdown
+ * is initiated while an operation is pending, this could cause dataloss.
+ * Using `withConnectionWrapper` ensures that shutdown waits until all
+ * operations are complete before proceeding.
+ *
+ * Example:
+ * yield withConnectionWrapper("Bookmarks: Remove a bookmark", Task.async(function*(db) {
+ * // Proceed with the db, asynchronously.
+ * // Shutdown will not interrupt operations that take place here.
+ * }));
+ *
+ * @param {string} name The name of the operation. Used for debugging, logging
+ * and crash reporting.
+ * @param {function(db)} task A function that takes as argument a Sqlite.jsm
+ * connection and returns a Promise. Shutdown is guaranteed to not interrupt
+ * execution of `task`.
+ */
+ withConnectionWrapper: (name, task) => {
+ if (!name) {
+ throw new TypeError("Expecting a user-readable name");
+ }
+ return Task.spawn(function*() {
+ let db = yield gAsyncDBWrapperPromised;
+ return db.executeBeforeShutdown(name, task);
+ });
+ },
+
+ /**
+ * Given a uri returns list of itemIds associated to it.
+ *
+ * @param aURI
+ * nsIURI or spec of the page.
+ * @param aCallback
+ * Function to be called when done.
+ * The function will receive an array of itemIds associated to aURI and
+ * aURI itself.
+ *
+ * @return A object with a .cancel() method allowing to cancel the request.
+ *
+ * @note Children of live bookmarks folders are excluded. The callback function is
+ * not invoked if the request is cancelled or hits an error.
+ */
+ asyncGetBookmarkIds: function PU_asyncGetBookmarkIds(aURI, aCallback)
+ {
+ let abort = false;
+ let itemIds = [];
+ Task.spawn(function* () {
+ let conn = yield this.promiseDBConnection();
+ const QUERY_STR = `SELECT b.id FROM moz_bookmarks b
+ JOIN moz_places h on h.id = b.fk
+ WHERE h.url_hash = hash(:url) AND h.url = :url`;
+ let spec = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ yield conn.executeCached(QUERY_STR, { url: spec }, aRow => {
+ if (abort)
+ throw StopIteration;
+ itemIds.push(aRow.getResultByIndex(0));
+ });
+ if (!abort)
+ aCallback(itemIds, aURI);
+ }.bind(this)).then(null, Cu.reportError);
+ return { cancel: () => { abort = true; } };
+ },
+
+ /**
+ * Lazily adds a bookmarks observer, waiting for the bookmarks service to be
+ * alive before registering the observer. This is especially useful in the
+ * startup path, to avoid initializing the service just to add an observer.
+ *
+ * @param aObserver
+ * Object implementing nsINavBookmarkObserver
+ * @param [optional]aWeakOwner
+ * Whether to use weak ownership.
+ *
+ * @note Correct functionality of lazy observers relies on the fact Places
+ * notifies categories before real observers, and uses
+ * PlacesCategoriesStarter component to kick-off the registration.
+ */
+ _bookmarksServiceReady: false,
+ _bookmarksServiceObserversQueue: [],
+ addLazyBookmarkObserver:
+ function PU_addLazyBookmarkObserver(aObserver, aWeakOwner) {
+ if (this._bookmarksServiceReady) {
+ this.bookmarks.addObserver(aObserver, aWeakOwner === true);
+ return;
+ }
+ this._bookmarksServiceObserversQueue.push({ observer: aObserver,
+ weak: aWeakOwner === true });
+ },
+
+ /**
+ * Removes a bookmarks observer added through addLazyBookmarkObserver.
+ *
+ * @param aObserver
+ * Object implementing nsINavBookmarkObserver
+ */
+ removeLazyBookmarkObserver:
+ function PU_removeLazyBookmarkObserver(aObserver) {
+ if (this._bookmarksServiceReady) {
+ this.bookmarks.removeObserver(aObserver);
+ return;
+ }
+ let index = -1;
+ for (let i = 0;
+ i < this._bookmarksServiceObserversQueue.length && index == -1; i++) {
+ if (this._bookmarksServiceObserversQueue[i].observer === aObserver)
+ index = i;
+ }
+ if (index != -1) {
+ this._bookmarksServiceObserversQueue.splice(index, 1);
+ }
+ },
+
+ /**
+ * Sets the character-set for a URI.
+ *
+ * @param aURI nsIURI
+ * @param aCharset character-set value.
+ * @return {Promise}
+ */
+ setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
+ let deferred = Promise.defer();
+
+ // Delaying to catch issues with asynchronous behavior while waiting
+ // to implement asynchronous annotations in bug 699844.
+ Services.tm.mainThread.dispatch(function() {
+ if (aCharset && aCharset.length > 0) {
+ PlacesUtils.annotations.setPageAnnotation(
+ aURI, PlacesUtils.CHARSET_ANNO, aCharset, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+ } else {
+ PlacesUtils.annotations.removePageAnnotation(
+ aURI, PlacesUtils.CHARSET_ANNO);
+ }
+ deferred.resolve();
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Gets the last saved character-set for a URI.
+ *
+ * @param aURI nsIURI
+ * @return {Promise}
+ * @resolve a character-set or null.
+ */
+ getCharsetForURI: function PU_getCharsetForURI(aURI) {
+ let deferred = Promise.defer();
+
+ Services.tm.mainThread.dispatch(function() {
+ let charset = null;
+
+ try {
+ charset = PlacesUtils.annotations.getPageAnnotation(aURI,
+ PlacesUtils.CHARSET_ANNO);
+ } catch (ex) { }
+
+ deferred.resolve(charset);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Promised wrapper for mozIAsyncHistory::getPlacesInfo for a single place.
+ *
+ * @param aPlaceIdentifier
+ * either an nsIURI or a GUID (@see getPlacesInfo)
+ * @resolves to the place info object handed to handleResult.
+ */
+ promisePlaceInfo: function PU_promisePlaceInfo(aPlaceIdentifier) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.getPlacesInfo(aPlaceIdentifier, {
+ _placeInfo: null,
+ handleResult: function handleResult(aPlaceInfo) {
+ this._placeInfo = aPlaceInfo;
+ },
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ deferred.reject(new Components.Exception("Error", aResultCode));
+ },
+ handleCompletion: function() {
+ deferred.resolve(this._placeInfo);
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Gets favicon data for a given page url.
+ *
+ * @param aPageUrl url of the page to look favicon for.
+ * @resolves to an object representing a favicon entry, having the following
+ * properties: { uri, dataLen, data, mimeType }
+ * @rejects JavaScript exception if the given url has no associated favicon.
+ */
+ promiseFaviconData: function (aPageUrl) {
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.getFaviconDataForPage(NetUtil.newURI(aPageUrl),
+ function (aURI, aDataLen, aData, aMimeType) {
+ if (aURI) {
+ deferred.resolve({ uri: aURI,
+ dataLen: aDataLen,
+ data: aData,
+ mimeType: aMimeType });
+ } else {
+ deferred.reject();
+ }
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Gets the favicon link url (moz-anno:) for a given page url.
+ *
+ * @param aPageURL url of the page to lookup the favicon for.
+ * @resolves to the nsIURL of the favicon link
+ * @rejects if the given url has no associated favicon.
+ */
+ promiseFaviconLinkUrl: function (aPageUrl) {
+ let deferred = Promise.defer();
+ if (!(aPageUrl instanceof Ci.nsIURI))
+ aPageUrl = NetUtil.newURI(aPageUrl);
+
+ PlacesUtils.favicons.getFaviconURLForPage(aPageUrl, uri => {
+ if (uri) {
+ uri = PlacesUtils.favicons.getFaviconLinkForIcon(uri);
+ deferred.resolve(uri);
+ } else {
+ deferred.reject("favicon not found for uri");
+ }
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Get the unique id for an item (a bookmark, a folder or a separator) given
+ * its item id.
+ *
+ * @param aItemId
+ * an item id
+ * @return {Promise}
+ * @resolves to the GUID.
+ * @rejects if aItemId is invalid.
+ */
+ promiseItemGuid(aItemId) {
+ return GuidHelper.getItemGuid(aItemId)
+ },
+
+ /**
+ * Get the item id for an item (a bookmark, a folder or a separator) given
+ * its unique id.
+ *
+ * @param aGuid
+ * an item GUID
+ * @return {Promise}
+ * @resolves to the GUID.
+ * @rejects if there's no item for the given GUID.
+ */
+ promiseItemId(aGuid) {
+ return GuidHelper.getItemId(aGuid)
+ },
+
+ /**
+ * Invalidate the GUID cache for the given itemId.
+ *
+ * @param aItemId
+ * an item id
+ */
+ invalidateCachedGuidFor(aItemId) {
+ GuidHelper.invalidateCacheForItemId(aItemId)
+ },
+
+ /**
+ * Asynchronously retrieve a JS-object representation of a places bookmarks
+ * item (a bookmark, a folder, or a separator) along with all of its
+ * descendants.
+ *
+ * @param [optional] aItemGuid
+ * the (topmost) item to be queried. If it's not passed, the places
+ * root is queried: that is, you get a representation of the entire
+ * bookmarks hierarchy.
+ * @param [optional] aOptions
+ * Options for customizing the query behavior, in the form of a JS
+ * object with any of the following properties:
+ * - excludeItemsCallback: a function for excluding items, along with
+ * their descendants. Given an item object (that has everything set
+ * apart its potential children data), it should return true if the
+ * item should be excluded. Once an item is excluded, the function
+ * isn't called for any of its descendants. This isn't called for
+ * the root item.
+ * WARNING: since the function may be called for each item, using
+ * this option can slow down the process significantly if the
+ * callback does anything that's not relatively trivial. It is
+ * highly recommended to avoid any synchronous I/O or DB queries.
+ * - includeItemIds: opt-in to include the deprecated id property.
+ * Use it if you must. It'll be removed once the switch to GUIDs is
+ * complete.
+ *
+ * @return {Promise}
+ * @resolves to a JS object that represents either a single item or a
+ * bookmarks tree. Each node in the tree has the following properties set:
+ * - guid (string): the item's GUID (same as aItemGuid for the top item).
+ * - [deprecated] id (number): the item's id. This is only if
+ * aOptions.includeItemIds is set.
+ * - type (string): the item's type. @see PlacesUtils.TYPE_X_*
+ * - title (string): the item's title. If it has no title, this property
+ * isn't set.
+ * - dateAdded (number, microseconds from the epoch): the date-added value of
+ * the item.
+ * - lastModified (number, microseconds from the epoch): the last-modified
+ * value of the item.
+ * - annos (see getAnnotationsForItem): the item's annotations. This is not
+ * set if there are no annotations set for the item).
+ * - index: the item's index under it's parent.
+ *
+ * The root object (i.e. the one for aItemGuid) also has the following
+ * properties set:
+ * - parentGuid (string): the GUID of the root's parent. This isn't set if
+ * the root item is the places root.
+ * - itemsCount (number, not enumerable): the number of items, including the
+ * root item itself, which are represented in the resolved object.
+ *
+ * Bookmark items also have the following properties:
+ * - uri (string): the item's url.
+ * - tags (string): csv string of the bookmark's tags.
+ * - charset (string): the last known charset of the bookmark.
+ * - keyword (string): the bookmark's keyword (unset if none).
+ * - postData (string): the bookmark's keyword postData (unset if none).
+ * - iconuri (string): the bookmark's favicon url.
+ * The last four properties are not set at all if they're irrelevant (e.g.
+ * |charset| is not set if no charset was previously set for the bookmark
+ * url).
+ *
+ * Folders may also have the following properties:
+ * - children (array): the folder's children information, each of them
+ * having the same set of properties as above.
+ *
+ * @rejects if the query failed for any reason.
+ * @note if aItemGuid points to a non-existent item, the returned promise is
+ * resolved to null.
+ */
+ promiseBookmarksTree: Task.async(function* (aItemGuid = "", aOptions = {}) {
+ let createItemInfoObject = function* (aRow, aIncludeParentGuid) {
+ let item = {};
+ let copyProps = (...props) => {
+ for (let prop of props) {
+ let val = aRow.getResultByName(prop);
+ if (val !== null)
+ item[prop] = val;
+ }
+ };
+ copyProps("guid", "title", "index", "dateAdded", "lastModified");
+ if (aIncludeParentGuid)
+ copyProps("parentGuid");
+
+ let itemId = aRow.getResultByName("id");
+ if (aOptions.includeItemIds)
+ item.id = itemId;
+
+ // Cache it for promiseItemId consumers regardless.
+ GuidHelper.updateCache(itemId, item.guid);
+
+ let type = aRow.getResultByName("type");
+ if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK)
+ copyProps("charset", "tags", "iconuri");
+
+ // Add annotations.
+ if (aRow.getResultByName("has_annos")) {
+ try {
+ item.annos = PlacesUtils.getAnnotationsForItem(itemId);
+ } catch (e) {
+ Cu.reportError("Unexpected error while reading annotations " + e);
+ }
+ }
+
+ switch (type) {
+ case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
+ item.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+ // If this throws due to an invalid url, the item will be skipped.
+ item.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
+ // Keywords are cached, so this should be decently fast.
+ let entry = yield PlacesUtils.keywords.fetch({ url: item.uri });
+ if (entry) {
+ item.keyword = entry.keyword;
+ item.postData = entry.postData;
+ }
+ break;
+ case Ci.nsINavBookmarksService.TYPE_FOLDER:
+ item.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+ // Mark root folders.
+ if (itemId == PlacesUtils.placesRootId)
+ item.root = "placesRoot";
+ else if (itemId == PlacesUtils.bookmarksMenuFolderId)
+ item.root = "bookmarksMenuFolder";
+ else if (itemId == PlacesUtils.unfiledBookmarksFolderId)
+ item.root = "unfiledBookmarksFolder";
+ else if (itemId == PlacesUtils.toolbarFolderId)
+ item.root = "toolbarFolder";
+ else if (itemId == PlacesUtils.mobileFolderId)
+ item.root = "mobileFolder";
+ break;
+ case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
+ item.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+ break;
+ default:
+ Cu.reportError("Unexpected bookmark type");
+ break;
+ }
+ return item;
+ }.bind(this);
+
+ const QUERY_STR =
+ `/* do not warn (bug no): cannot use an index */
+ WITH RECURSIVE
+ descendants(fk, level, type, id, guid, parent, parentGuid, position,
+ title, dateAdded, lastModified) AS (
+ SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
+ (SELECT guid FROM moz_bookmarks WHERE id = b1.parent),
+ b1.position, b1.title, b1.dateAdded, b1.lastModified
+ FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
+ UNION ALL
+ SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
+ descendants.guid, b2.position, b2.title, b2.dateAdded,
+ b2.lastModified
+ FROM moz_bookmarks b2
+ JOIN descendants ON b2.parent = descendants.id AND b2.id <> :tags_folder)
+ SELECT d.level, d.id, d.guid, d.parent, d.parentGuid, d.type,
+ d.position AS [index], d.title, d.dateAdded, d.lastModified,
+ h.url, f.url AS iconuri,
+ (SELECT GROUP_CONCAT(t.title, ',')
+ FROM moz_bookmarks b2
+ JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder
+ WHERE b2.fk = h.id
+ ) AS tags,
+ EXISTS (SELECT 1 FROM moz_items_annos
+ WHERE item_id = d.id LIMIT 1) AS has_annos,
+ (SELECT a.content FROM moz_annos a
+ JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
+ WHERE place_id = h.id AND n.name = :charset_anno
+ ) AS charset
+ FROM descendants d
+ LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent
+ LEFT JOIN moz_places h ON h.id = d.fk
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ ORDER BY d.level, d.parent, d.position`;
+
+
+ if (!aItemGuid)
+ aItemGuid = this.bookmarks.rootGuid;
+
+ let hasExcludeItemsCallback =
+ aOptions.hasOwnProperty("excludeItemsCallback");
+ let excludedParents = new Set();
+ let shouldExcludeItem = (aItem, aParentGuid) => {
+ let exclude = excludedParents.has(aParentGuid) ||
+ aOptions.excludeItemsCallback(aItem);
+ if (exclude) {
+ if (aItem.type == this.TYPE_X_MOZ_PLACE_CONTAINER)
+ excludedParents.add(aItem.guid);
+ }
+ return exclude;
+ };
+
+ let rootItem = null;
+ let parentsMap = new Map();
+ let conn = yield this.promiseDBConnection();
+ let rows = yield conn.executeCached(QUERY_STR,
+ { tags_folder: PlacesUtils.tagsFolderId,
+ charset_anno: PlacesUtils.CHARSET_ANNO,
+ item_guid: aItemGuid });
+ let yieldCounter = 0;
+ for (let row of rows) {
+ let item;
+ if (!rootItem) {
+ try {
+ // This is the first row.
+ rootItem = item = yield createItemInfoObject(row, true);
+ Object.defineProperty(rootItem, "itemsCount", { value: 1
+ , writable: true
+ , enumerable: false
+ , configurable: false });
+ } catch (ex) {
+ throw new Error("Failed to fetch the data for the root item " + ex);
+ }
+ } else {
+ try {
+ // Our query guarantees that we always visit parents ahead of their
+ // children.
+ item = yield createItemInfoObject(row, false);
+ let parentGuid = row.getResultByName("parentGuid");
+ if (hasExcludeItemsCallback && shouldExcludeItem(item, parentGuid))
+ continue;
+
+ let parentItem = parentsMap.get(parentGuid);
+ if ("children" in parentItem)
+ parentItem.children.push(item);
+ else
+ parentItem.children = [item];
+
+ rootItem.itemsCount++;
+ } catch (ex) {
+ // This is a bogus child, report and skip it.
+ Cu.reportError("Failed to fetch the data for an item " + ex);
+ continue;
+ }
+ }
+
+ if (item.type == this.TYPE_X_MOZ_PLACE_CONTAINER)
+ parentsMap.set(item.guid, item);
+
+ // With many bookmarks we end up stealing the CPU - even with yielding!
+ // So we let everyone else have a go every few items (bug 1186714).
+ if (++yieldCounter % 50 == 0) {
+ yield new Promise(resolve => {
+ Services.tm.currentThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ }
+ }
+
+ return rootItem;
+ })
+};
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "history", function() {
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService)
+ .QueryInterface(Ci.nsIBrowserHistory)
+ .QueryInterface(Ci.nsPIPlacesDatabase);
+ return Object.freeze(new Proxy(hs, {
+ get: function(target, name) {
+ let property, object;
+ if (name in target) {
+ property = target[name];
+ object = target;
+ } else {
+ property = History[name];
+ object = History;
+ }
+ if (typeof property == "function") {
+ return property.bind(object);
+ }
+ return property;
+ }
+ }));
+});
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "asyncHistory",
+ "@mozilla.org/browser/history;1",
+ "mozIAsyncHistory");
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() {
+ return PlacesUtils.history;
+});
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons",
+ "@mozilla.org/browser/favicon-service;1",
+ "mozIAsyncFavicons");
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "bookmarks", () => {
+ let bm = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
+ .getService(Ci.nsINavBookmarksService);
+ return Object.freeze(new Proxy(bm, {
+ get: (target, name) => target.hasOwnProperty(name) ? target[name]
+ : Bookmarks[name]
+ }));
+});
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
+ "@mozilla.org/browser/annotation-service;1",
+ "nsIAnnotationService");
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
+ "@mozilla.org/browser/tagging-service;1",
+ "nsITaggingService");
+
+XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "livemarks",
+ "@mozilla.org/browser/livemark-service;2",
+ "mozIAsyncLivemarks");
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "keywords", () => Keywords);
+
+XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
+ let tm = Cc["@mozilla.org/transactionmanager;1"].
+ createInstance(Ci.nsITransactionManager);
+ tm.AddListener(PlacesUtils);
+ this.registerShutdownFunction(function () {
+ // Clear all references to local transactions in the transaction manager,
+ // this prevents from leaking it.
+ this.transactionManager.RemoveListener(this);
+ this.transactionManager.clear();
+ });
+
+ // Bug 750269
+ // The transaction manager keeps strong references to transactions, and by
+ // that, also to the global for each transaction. A transaction, however,
+ // could be either the transaction itself (for which the global is this
+ // module) or some js-proxy in another global, usually a window. The later
+ // would leak because the transaction lifetime (in the manager's stacks)
+ // is independent of the global from which doTransaction was called.
+ // To avoid such a leak, we hide the native doTransaction from callers,
+ // and let each doTransaction call go through this module.
+ // Doing so ensures that, as long as the transaction is any of the
+ // PlacesXXXTransaction objects declared in this module, the object
+ // referenced by the transaction manager has the module itself as global.
+ return Object.create(tm, {
+ "doTransaction": {
+ value: function(aTransaction) {
+ tm.doTransaction(aTransaction);
+ }
+ }
+ });
+});
+
+XPCOMUtils.defineLazyGetter(this, "bundle", function() {
+ const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
+ return Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle(PLACES_STRING_BUNDLE_URI);
+});
+
+/**
+ * Setup internal databases for closing properly during shutdown.
+ *
+ * 1. Places initiates shutdown.
+ * 2. Before places can move to the step where it closes the low-level connection,
+ * we need to make sure that we have closed `conn`.
+ * 3. Before we can close `conn`, we need to make sure that all external clients
+ * have stopped using `conn`.
+ * 4. Before we can close Sqlite, we need to close `conn`.
+ */
+function setupDbForShutdown(conn, name) {
+ try {
+ let state = "0. Not started.";
+ let promiseClosed = new Promise((resolve, reject) => {
+ // The service initiates shutdown.
+ // Before it can safely close its connection, we need to make sure
+ // that we have closed the high-level connection.
+ try {
+ AsyncShutdown.placesClosingInternalConnection.addBlocker(`${name} closing as part of Places shutdown`,
+ Task.async(function*() {
+ state = "1. Service has initiated shutdown";
+
+ // At this stage, all external clients have finished using the
+ // database. We just need to close the high-level connection.
+ yield conn.close();
+ state = "2. Closed Sqlite.jsm connection.";
+
+ resolve();
+ }),
+ () => state
+ );
+ } catch (ex) {
+ // It's too late to block shutdown, just close the connection.
+ conn.close();
+ reject(ex);
+ }
+ });
+
+ // Make sure that Sqlite.jsm doesn't close until we are done
+ // with the high-level connection.
+ Sqlite.shutdown.addBlocker(`${name} must be closed before Sqlite.jsm`,
+ () => promiseClosed.catch(Cu.reportError),
+ () => state
+ );
+ } catch (ex) {
+ // It's too late to block shutdown, just close the connection.
+ conn.close();
+ throw ex;
+ }
+}
+
+XPCOMUtils.defineLazyGetter(this, "gAsyncDBConnPromised",
+ () => Sqlite.cloneStorageConnection({
+ connection: PlacesUtils.history.DBConnection,
+ readOnly: true
+ }).then(conn => {
+ setupDbForShutdown(conn, "PlacesUtils read-only connection");
+ return conn;
+ }).catch(Cu.reportError)
+);
+
+XPCOMUtils.defineLazyGetter(this, "gAsyncDBWrapperPromised",
+ () => Sqlite.wrapStorageConnection({
+ connection: PlacesUtils.history.DBConnection,
+ }).then(conn => {
+ setupDbForShutdown(conn, "PlacesUtils wrapped connection");
+ return conn;
+ }).catch(Cu.reportError)
+);
+
+/**
+ * Keywords management API.
+ * Sooner or later these keywords will merge with search keywords, this is an
+ * interim API that should then be replaced by a unified one.
+ * Keywords are associated with URLs and can have POST data.
+ * A single URL can have multiple keywords, provided they differ by POST data.
+ */
+var Keywords = {
+ /**
+ * Fetches a keyword entry based on keyword or URL.
+ *
+ * @param keywordOrEntry
+ * Either the keyword to fetch or an entry providing keyword
+ * or url property to find keywords for. If both properties are set,
+ * this returns their intersection.
+ * @param onResult [optional]
+ * Callback invoked for each found entry.
+ * @return {Promise}
+ * @resolves to an object in the form: { keyword, url, postData },
+ * or null if a keyword entry was not found.
+ */
+ fetch(keywordOrEntry, onResult=null) {
+ if (typeof(keywordOrEntry) == "string")
+ keywordOrEntry = { keyword: keywordOrEntry };
+
+ if (keywordOrEntry === null || typeof(keywordOrEntry) != "object" ||
+ (("keyword" in keywordOrEntry) && typeof(keywordOrEntry.keyword) != "string"))
+ throw new Error("Invalid keyword");
+
+ let hasKeyword = "keyword" in keywordOrEntry;
+ let hasUrl = "url" in keywordOrEntry;
+
+ if (!hasKeyword && !hasUrl)
+ throw new Error("At least keyword or url must be provided");
+ if (onResult && typeof onResult != "function")
+ throw new Error("onResult callback must be a valid function");
+
+ if (hasUrl)
+ keywordOrEntry.url = new URL(keywordOrEntry.url);
+ if (hasKeyword)
+ keywordOrEntry.keyword = keywordOrEntry.keyword.trim().toLowerCase();
+
+ let safeOnResult = entry => {
+ if (onResult) {
+ try {
+ onResult(entry);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ };
+
+ return gKeywordsCachePromise.then(cache => {
+ let entries = [];
+ if (hasKeyword) {
+ let entry = cache.get(keywordOrEntry.keyword);
+ if (entry)
+ entries.push(entry);
+ }
+ if (hasUrl) {
+ for (let entry of cache.values()) {
+ if (entry.url.href == keywordOrEntry.url.href)
+ entries.push(entry);
+ }
+ }
+
+ entries = entries.filter(e => {
+ return (!hasUrl || e.url.href == keywordOrEntry.url.href) &&
+ (!hasKeyword || e.keyword == keywordOrEntry.keyword);
+ });
+
+ entries.forEach(safeOnResult);
+ return entries.length ? entries[0] : null;
+ });
+ },
+
+ /**
+ * Adds a new keyword and postData for the given URL.
+ *
+ * @param keywordEntry
+ * An object describing the keyword to insert, in the form:
+ * {
+ * keyword: non-empty string,
+ * URL: URL or href to associate to the keyword,
+ * postData: optional POST data to associate to the keyword
+ * }
+ * @note Do not define a postData property if there isn't any POST data.
+ * @resolves when the addition is complete.
+ */
+ insert(keywordEntry) {
+ if (!keywordEntry || typeof keywordEntry != "object")
+ throw new Error("Input should be a valid object");
+
+ if (!("keyword" in keywordEntry) || !keywordEntry.keyword ||
+ typeof(keywordEntry.keyword) != "string")
+ throw new Error("Invalid keyword");
+ if (("postData" in keywordEntry) && keywordEntry.postData &&
+ typeof(keywordEntry.postData) != "string")
+ throw new Error("Invalid POST data");
+ if (!("url" in keywordEntry))
+ throw new Error("undefined is not a valid URL");
+ let { keyword, url,
+ source = Ci.nsINavBookmarksService.SOURCE_DEFAULT } = keywordEntry;
+ keyword = keyword.trim().toLowerCase();
+ let postData = keywordEntry.postData || null;
+ // This also checks href for validity
+ url = new URL(url);
+
+ return PlacesUtils.withConnectionWrapper("Keywords.insert", Task.async(function*(db) {
+ let cache = yield gKeywordsCachePromise;
+
+ // Trying to set the same keyword is a no-op.
+ let oldEntry = cache.get(keyword);
+ if (oldEntry && oldEntry.url.href == url.href &&
+ oldEntry.postData == keywordEntry.postData) {
+ return;
+ }
+
+ // A keyword can only be associated to a single page.
+ // If another page is using the new keyword, we must update the keyword
+ // entry.
+ // Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
+ // trigger.
+ if (oldEntry) {
+ yield db.executeCached(
+ `UPDATE moz_keywords
+ SET place_id = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
+ post_data = :post_data
+ WHERE keyword = :keyword
+ `, { url: url.href, keyword: keyword, post_data: postData });
+ yield notifyKeywordChange(oldEntry.url.href, "", source);
+ } else {
+ // An entry for the given page could be missing, in such a case we need to
+ // create it. The IGNORE conflict can trigger on `guid`.
+ yield db.executeCached(
+ `INSERT OR IGNORE INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid)
+ VALUES (:url, hash(:url), :rev_host, 0, :frecency,
+ IFNULL((SELECT guid FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
+ GENERATE_GUID()))
+ `, { url: url.href, rev_host: PlacesUtils.getReversedHost(url),
+ frecency: url.protocol == "place:" ? 0 : -1 });
+ yield db.executeCached(
+ `INSERT INTO moz_keywords (keyword, place_id, post_data)
+ VALUES (:keyword, (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url), :post_data)
+ `, { url: url.href, keyword: keyword, post_data: postData });
+ }
+
+ cache.set(keyword, { keyword, url, postData });
+
+ // In any case, notify about the new keyword.
+ yield notifyKeywordChange(url.href, keyword, source);
+ }.bind(this))
+ );
+ },
+
+ /**
+ * Removes a keyword.
+ *
+ * @param keyword
+ * The keyword to remove.
+ * @return {Promise}
+ * @resolves when the removal is complete.
+ */
+ remove(keywordOrEntry) {
+ if (typeof(keywordOrEntry) == "string")
+ keywordOrEntry = { keyword: keywordOrEntry };
+
+ if (keywordOrEntry === null || typeof(keywordOrEntry) != "object" ||
+ !keywordOrEntry.keyword || typeof keywordOrEntry.keyword != "string")
+ throw new Error("Invalid keyword");
+
+ let { keyword,
+ source = Ci.nsINavBookmarksService.SOURCE_DEFAULT } = keywordOrEntry;
+ keyword = keywordOrEntry.keyword.trim().toLowerCase();
+ return PlacesUtils.withConnectionWrapper("Keywords.remove", Task.async(function*(db) {
+ let cache = yield gKeywordsCachePromise;
+ if (!cache.has(keyword))
+ return;
+ let { url } = cache.get(keyword);
+ cache.delete(keyword);
+
+ yield db.execute(`DELETE FROM moz_keywords WHERE keyword = :keyword`,
+ { keyword });
+
+ // Notify bookmarks about the removal.
+ yield notifyKeywordChange(url.href, "", source);
+ }.bind(this))) ;
+ }
+};
+
+// Set by the keywords API to distinguish notifications fired by the old API.
+// Once the old API will be gone, we can remove this and stop observing.
+var gIgnoreKeywordNotifications = false;
+
+XPCOMUtils.defineLazyGetter(this, "gKeywordsCachePromise", () =>
+ PlacesUtils.withConnectionWrapper("PlacesUtils: gKeywordsCachePromise",
+ Task.async(function*(db) {
+ let cache = new Map();
+ let rows = yield db.execute(
+ `SELECT keyword, url, post_data
+ FROM moz_keywords k
+ JOIN moz_places h ON h.id = k.place_id
+ `);
+ for (let row of rows) {
+ let keyword = row.getResultByName("keyword");
+ let entry = { keyword,
+ url: new URL(row.getResultByName("url")),
+ postData: row.getResultByName("post_data") };
+ cache.set(keyword, entry);
+ }
+
+ // Helper to get a keyword from an href.
+ function keywordsForHref(href) {
+ let keywords = [];
+ for (let [ key, val ] of cache) {
+ if (val.url.href == href)
+ keywords.push(key);
+ }
+ return keywords;
+ }
+
+ // Start observing changes to bookmarks. For now we are going to keep that
+ // relation for backwards compatibility reasons, but mostly because we are
+ // lacking a UI to manage keywords directly.
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemAdded() {},
+ onItemVisited() {},
+ onItemMoved() {},
+
+ onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
+ if (itemType != PlacesUtils.bookmarks.TYPE_BOOKMARK)
+ return;
+
+ let keywords = keywordsForHref(uri.spec);
+ // This uri has no keywords associated, so there's nothing to do.
+ if (keywords.length == 0)
+ return;
+
+ Task.spawn(function* () {
+ // If the uri is not bookmarked anymore, we can remove this keyword.
+ let bookmark = yield PlacesUtils.bookmarks.fetch({ url: uri });
+ if (!bookmark) {
+ for (let keyword of keywords) {
+ yield PlacesUtils.keywords.remove(keyword);
+ }
+ }
+ }).catch(Cu.reportError);
+ },
+
+ onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid,
+ parentGuid, oldVal) {
+ if (gIgnoreKeywordNotifications) {
+ return;
+ }
+
+ if (prop == "keyword") {
+ this._onKeywordChanged(guid, val).catch(Cu.reportError);
+ } else if (prop == "uri") {
+ this._onUrlChanged(guid, val, oldVal).catch(Cu.reportError);
+ }
+ },
+
+ _onKeywordChanged: Task.async(function* (guid, keyword) {
+ let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
+ // Due to mixed sync/async operations, by this time the bookmark could
+ // have disappeared and we already handle removals in onItemRemoved.
+ if (!bookmark) {
+ return;
+ }
+
+ if (keyword.length == 0) {
+ // We are removing a keyword.
+ let keywords = keywordsForHref(bookmark.url.href)
+ for (let kw of keywords) {
+ cache.delete(kw);
+ }
+ } else {
+ // We are adding a new keyword.
+ cache.set(keyword, { keyword, url: bookmark.url });
+ }
+ }),
+
+ _onUrlChanged: Task.async(function* (guid, url, oldUrl) {
+ // Check if the old url is associated with keywords.
+ let entries = [];
+ yield PlacesUtils.keywords.fetch({ url: oldUrl }, e => entries.push(e));
+ if (entries.length == 0) {
+ return;
+ }
+
+ // Move the keywords to the new url.
+ for (let entry of entries) {
+ yield PlacesUtils.keywords.remove(entry.keyword);
+ entry.url = new URL(url);
+ yield PlacesUtils.keywords.insert(entry);
+ }
+ }),
+ };
+
+ PlacesUtils.bookmarks.addObserver(observer, false);
+ PlacesUtils.registerShutdownFunction(() => {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ });
+ return cache;
+ })
+));
+
+// Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
+// itemIds will be deprecated in favour of GUIDs, which play much better
+// with multiple undo/redo operations. Because these GUIDs are already stored,
+// and because we don't want to revise the transactions API once more when this
+// happens, transactions are set to work with GUIDs exclusively, in the sense
+// that they may never expose itemIds, nor do they accept them as input.
+// More importantly, transactions which add or remove items guarantee to
+// restore the GUIDs on undo/redo, so that the following transactions that may
+// done or undo can assume the items they're interested in are stil accessible
+// through the same GUID.
+// The current bookmarks API, however, doesn't expose the necessary means for
+// working with GUIDs. So, until it does, this helper object accesses the
+// Places database directly in order to switch between GUIDs and itemIds, and
+// "restore" GUIDs on items re-created items.
+var GuidHelper = {
+ // Cache for GUID<->itemId paris.
+ guidsForIds: new Map(),
+ idsForGuids: new Map(),
+
+ getItemId: Task.async(function* (aGuid) {
+ let cached = this.idsForGuids.get(aGuid);
+ if (cached !== undefined)
+ return cached;
+
+ let itemId = yield PlacesUtils.withConnectionWrapper("GuidHelper.getItemId",
+ Task.async(function* (db) {
+ let rows = yield db.executeCached(
+ "SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid LIMIT 1",
+ { guid: aGuid });
+ if (rows.length == 0)
+ throw new Error("no item found for the given GUID");
+
+ return rows[0].getResultByName("id");
+ }));
+
+ this.updateCache(itemId, aGuid);
+ return itemId;
+ }),
+
+ getItemGuid: Task.async(function* (aItemId) {
+ let cached = this.guidsForIds.get(aItemId);
+ if (cached !== undefined)
+ return cached;
+
+ let guid = yield PlacesUtils.withConnectionWrapper("GuidHelper.getItemGuid",
+ Task.async(function* (db) {
+
+ let rows = yield db.executeCached(
+ "SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id LIMIT 1",
+ { id: aItemId });
+ if (rows.length == 0)
+ throw new Error("no item found for the given itemId");
+
+ return rows[0].getResultByName("guid");
+ }));
+
+ this.updateCache(aItemId, guid);
+ return guid;
+ }),
+
+ /**
+ * Updates the cache.
+ *
+ * @note This is the only place where the cache should be populated,
+ * invalidation relies on both Maps being populated at the same time.
+ */
+ updateCache(aItemId, aGuid) {
+ if (typeof(aItemId) != "number" || aItemId <= 0)
+ throw new Error("Trying to update the GUIDs cache with an invalid itemId");
+ if (typeof(aGuid) != "string" || !/^[a-zA-Z0-9\-_]{12}$/.test(aGuid))
+ throw new Error("Trying to update the GUIDs cache with an invalid GUID");
+ this.ensureObservingRemovedItems();
+ this.guidsForIds.set(aItemId, aGuid);
+ this.idsForGuids.set(aGuid, aItemId);
+ },
+
+ invalidateCacheForItemId(aItemId) {
+ let guid = this.guidsForIds.get(aItemId);
+ this.guidsForIds.delete(aItemId);
+ this.idsForGuids.delete(guid);
+ },
+
+ ensureObservingRemovedItems: function () {
+ if (!("observer" in this)) {
+ /**
+ * This observers serves two purposes:
+ * (1) Invalidate cached id<->GUID paris on when items are removed.
+ * (2) Cache GUIDs given us free of charge by onItemAdded/onItemRemoved.
+ * So, for exmaple, when the NewBookmark needs the new GUID, we already
+ * have it cached.
+ */
+ this.observer = {
+ onItemAdded: (aItemId, aParentId, aIndex, aItemType, aURI, aTitle,
+ aDateAdded, aGuid, aParentGuid) => {
+ this.updateCache(aItemId, aGuid);
+ this.updateCache(aParentId, aParentGuid);
+ },
+ onItemRemoved:
+ (aItemId, aParentId, aIndex, aItemTyep, aURI, aGuid, aParentGuid) => {
+ this.guidsForIds.delete(aItemId);
+ this.idsForGuids.delete(aGuid);
+ this.updateCache(aParentId, aParentGuid);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
+
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onItemChanged: function() {},
+ onItemVisited: function() {},
+ onItemMoved: function() {},
+ };
+ PlacesUtils.bookmarks.addObserver(this.observer, false);
+ PlacesUtils.registerShutdownFunction(() => {
+ PlacesUtils.bookmarks.removeObserver(this.observer);
+ });
+ }
+ }
+};
+
+// Transactions handlers.
+
+/**
+ * Updates commands in the undo group of the active window commands.
+ * Inactive windows commands will be updated on focus.
+ */
+function updateCommandsOnActiveWindow()
+{
+ let win = Services.focus.activeWindow;
+ if (win && win instanceof Ci.nsIDOMWindow) {
+ // Updating "undo" will cause a group update including "redo".
+ win.updateCommands("undo");
+ }
+}
+
+
+/**
+ * Used to cache bookmark information in transactions.
+ *
+ * @note To avoid leaks any non-primitive property should be copied.
+ * @note Used internally, DO NOT EXPORT.
+ */
+function TransactionItemCache()
+{
+}
+
+TransactionItemCache.prototype = {
+ set id(v) {
+ this._id = (parseInt(v) > 0 ? v : null);
+ },
+ get id() {
+ return this._id || -1;
+ },
+ set parentId(v) {
+ this._parentId = (parseInt(v) > 0 ? v : null);
+ },
+ get parentId() {
+ return this._parentId || -1;
+ },
+ keyword: null,
+ title: null,
+ dateAdded: null,
+ lastModified: null,
+ postData: null,
+ itemType: null,
+ set uri(v) {
+ this._uri = (v instanceof Ci.nsIURI ? v.clone() : null);
+ },
+ get uri() {
+ return this._uri || null;
+ },
+ set feedURI(v) {
+ this._feedURI = (v instanceof Ci.nsIURI ? v.clone() : null);
+ },
+ get feedURI() {
+ return this._feedURI || null;
+ },
+ set siteURI(v) {
+ this._siteURI = (v instanceof Ci.nsIURI ? v.clone() : null);
+ },
+ get siteURI() {
+ return this._siteURI || null;
+ },
+ set index(v) {
+ this._index = (parseInt(v) >= 0 ? v : null);
+ },
+ // Index can be 0.
+ get index() {
+ return this._index != null ? this._index : PlacesUtils.bookmarks.DEFAULT_INDEX;
+ },
+ set annotations(v) {
+ this._annotations = Array.isArray(v) ? Cu.cloneInto(v, {}) : null;
+ },
+ get annotations() {
+ return this._annotations || null;
+ },
+ set tags(v) {
+ this._tags = (v && Array.isArray(v) ? Array.prototype.slice.call(v) : null);
+ },
+ get tags() {
+ return this._tags || null;
+ },
+};
+
+
+/**
+ * Base transaction implementation.
+ *
+ * @note used internally, DO NOT EXPORT.
+ */
+function BaseTransaction()
+{
+}
+
+BaseTransaction.prototype = {
+ name: null,
+ set childTransactions(v) {
+ this._childTransactions = (Array.isArray(v) ? Array.prototype.slice.call(v) : null);
+ },
+ get childTransactions() {
+ return this._childTransactions || null;
+ },
+ doTransaction: function BTXN_doTransaction() {},
+ redoTransaction: function BTXN_redoTransaction() {
+ return this.doTransaction();
+ },
+ undoTransaction: function BTXN_undoTransaction() {},
+ merge: function BTXN_merge() {
+ return false;
+ },
+ get isTransient() {
+ return false;
+ },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsITransaction
+ ]),
+};
+
+
+/**
+ * Transaction for performing several Places Transactions in a single batch.
+ *
+ * @param aName
+ * title of the aggregate transactions
+ * @param aTransactions
+ * an array of transactions to perform
+ *
+ * @return nsITransaction object
+ */
+this.PlacesAggregatedTransaction =
+ function PlacesAggregatedTransaction(aName, aTransactions)
+{
+ // Copy the transactions array to decouple it from its prototype, which
+ // otherwise keeps alive its associated global object.
+ this.childTransactions = aTransactions;
+ this.name = aName;
+ this.item = new TransactionItemCache();
+
+ // Check child transactions number. We will batch if we have more than
+ // MIN_TRANSACTIONS_FOR_BATCH total number of transactions.
+ let countTransactions = function(aTransactions, aTxnCount)
+ {
+ for (let i = 0;
+ i < aTransactions.length && aTxnCount < MIN_TRANSACTIONS_FOR_BATCH;
+ ++i, ++aTxnCount) {
+ let txn = aTransactions[i];
+ if (txn.childTransactions && txn.childTransactions.length > 0)
+ aTxnCount = countTransactions(txn.childTransactions, aTxnCount);
+ }
+ return aTxnCount;
+ }
+
+ let txnCount = countTransactions(this.childTransactions, 0);
+ this._useBatch = txnCount >= MIN_TRANSACTIONS_FOR_BATCH;
+}
+
+PlacesAggregatedTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function ATXN_doTransaction()
+ {
+ this._isUndo = false;
+ if (this._useBatch)
+ PlacesUtils.bookmarks.runInBatchMode(this, null);
+ else
+ this.runBatched(false);
+ },
+
+ undoTransaction: function ATXN_undoTransaction()
+ {
+ this._isUndo = true;
+ if (this._useBatch)
+ PlacesUtils.bookmarks.runInBatchMode(this, null);
+ else
+ this.runBatched(true);
+ },
+
+ runBatched: function ATXN_runBatched()
+ {
+ // Use a copy of the transactions array, so we won't reverse the original
+ // one on undoing.
+ let transactions = this.childTransactions.slice(0);
+ if (this._isUndo)
+ transactions.reverse();
+ for (let i = 0; i < transactions.length; ++i) {
+ let txn = transactions[i];
+ if (this.item.parentId != -1)
+ txn.item.parentId = this.item.parentId;
+ if (this._isUndo)
+ txn.undoTransaction();
+ else
+ txn.doTransaction();
+ }
+ }
+};
+
+
+/**
+ * Transaction for creating a new folder.
+ *
+ * @param aTitle
+ * the title for the new folder
+ * @param aParentId
+ * the id of the parent folder in which the new folder should be added
+ * @param [optional] aIndex
+ * the index of the item in aParentId
+ * @param [optional] aAnnotations
+ * array of annotations to set for the new folder
+ * @param [optional] aChildTransactions
+ * array of transactions for items to be created in the new folder
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateFolderTransaction =
+ function PlacesCreateFolderTransaction(aTitle, aParentId, aIndex, aAnnotations,
+ aChildTransactions)
+{
+ this.item = new TransactionItemCache();
+ this.item.title = aTitle;
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+ this.item.annotations = aAnnotations;
+ this.childTransactions = aChildTransactions;
+}
+
+PlacesCreateFolderTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CFTXN_doTransaction()
+ {
+ this.item.id = PlacesUtils.bookmarks.createFolder(this.item.parentId,
+ this.item.title,
+ this.item.index);
+ if (this.item.annotations && this.item.annotations.length > 0)
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ // Set the new parent id into child transactions.
+ for (let i = 0; i < this.childTransactions.length; ++i) {
+ this.childTransactions[i].item.parentId = this.item.id;
+ }
+
+ let txn = new PlacesAggregatedTransaction("Create folder childTxn",
+ this.childTransactions);
+ txn.doTransaction();
+ }
+ },
+
+ undoTransaction: function CFTXN_undoTransaction()
+ {
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ let txn = new PlacesAggregatedTransaction("Create folder childTxn",
+ this.childTransactions);
+ txn.undoTransaction();
+ }
+
+ // Remove item only after all child transactions have been reverted.
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }
+};
+
+
+/**
+ * Transaction for creating a new bookmark.
+ *
+ * @param aURI
+ * the nsIURI of the new bookmark
+ * @param aParentId
+ * the id of the folder in which the bookmark should be added.
+ * @param [optional] aIndex
+ * the index of the item in aParentId
+ * @param [optional] aTitle
+ * the title of the new bookmark
+ * @param [optional] aKeyword
+ * the keyword for the new bookmark
+ * @param [optional] aAnnotations
+ * array of annotations to set for the new bookmark
+ * @param [optional] aChildTransactions
+ * child transactions to commit after creating the bookmark. Prefer
+ * using any of the arguments above if possible. In general, a child
+ * transations should be used only if the change it does has to be
+ * reverted manually when removing the bookmark item.
+ * a child transaction must support setting its bookmark-item
+ * identifier via an "id" js setter.
+ * @param [optional] aPostData
+ * keyword's POST data, if available.
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateBookmarkTransaction =
+ function PlacesCreateBookmarkTransaction(aURI, aParentId, aIndex, aTitle,
+ aKeyword, aAnnotations,
+ aChildTransactions, aPostData)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+ this.item.title = aTitle;
+ this.item.keyword = aKeyword;
+ this.item.postData = aPostData;
+ this.item.annotations = aAnnotations;
+ this.childTransactions = aChildTransactions;
+}
+
+PlacesCreateBookmarkTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CITXN_doTransaction()
+ {
+ this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
+ this.item.uri,
+ this.item.index,
+ this.item.title);
+ if (this.item.keyword) {
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
+ this.item.keyword);
+ if (this.item.postData) {
+ PlacesUtils.setPostDataForBookmark(this.item.id,
+ this.item.postData);
+ }
+ }
+ if (this.item.annotations && this.item.annotations.length > 0)
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ // Set the new item id into child transactions.
+ for (let i = 0; i < this.childTransactions.length; ++i) {
+ this.childTransactions[i].item.id = this.item.id;
+ }
+ let txn = new PlacesAggregatedTransaction("Create item childTxn",
+ this.childTransactions);
+ txn.doTransaction();
+ }
+ },
+
+ undoTransaction: function CITXN_undoTransaction()
+ {
+ if (this.childTransactions && this.childTransactions.length > 0) {
+ // Undo transactions should always be done in reverse order.
+ let txn = new PlacesAggregatedTransaction("Create item childTxn",
+ this.childTransactions);
+ txn.undoTransaction();
+ }
+
+ // Remove item only after all child transactions have been reverted.
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }
+};
+
+
+/**
+ * Transaction for creating a new separator.
+ *
+ * @param aParentId
+ * the id of the folder in which the separator should be added
+ * @param [optional] aIndex
+ * the index of the item in aParentId
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateSeparatorTransaction =
+ function PlacesCreateSeparatorTransaction(aParentId, aIndex)
+{
+ this.item = new TransactionItemCache();
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+}
+
+PlacesCreateSeparatorTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CSTXN_doTransaction()
+ {
+ this.item.id =
+ PlacesUtils.bookmarks.insertSeparator(this.item.parentId, this.item.index);
+ },
+
+ undoTransaction: function CSTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }
+};
+
+
+/**
+ * Transaction for creating a new livemark item.
+ *
+ * @see mozIAsyncLivemarks for documentation regarding the arguments.
+ *
+ * @param aFeedURI
+ * nsIURI of the feed
+ * @param [optional] aSiteURI
+ * nsIURI of the page serving the feed
+ * @param aTitle
+ * title for the livemark
+ * @param aParentId
+ * the id of the folder in which the livemark should be added
+ * @param [optional] aIndex
+ * the index of the livemark in aParentId
+ * @param [optional] aAnnotations
+ * array of annotations to set for the new livemark.
+ *
+ * @return nsITransaction object
+ */
+this.PlacesCreateLivemarkTransaction =
+ function PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aTitle, aParentId,
+ aIndex, aAnnotations)
+{
+ this.item = new TransactionItemCache();
+ this.item.feedURI = aFeedURI;
+ this.item.siteURI = aSiteURI;
+ this.item.title = aTitle;
+ this.item.parentId = aParentId;
+ this.item.index = aIndex;
+ this.item.annotations = aAnnotations;
+}
+
+PlacesCreateLivemarkTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function CLTXN_doTransaction()
+ {
+ this._promise = PlacesUtils.livemarks.addLivemark(
+ { title: this.item.title
+ , feedURI: this.item.feedURI
+ , parentId: this.item.parentId
+ , index: this.item.index
+ , siteURI: this.item.siteURI
+ }).then(aLivemark => {
+ this.item.id = aLivemark.id;
+ if (this.item.annotations && this.item.annotations.length > 0) {
+ PlacesUtils.setAnnotationsForItem(this.item.id,
+ this.item.annotations);
+ }
+ }, Cu.reportError);
+ },
+
+ undoTransaction: function CLTXN_undoTransaction()
+ {
+ // The getLivemark callback may fail, but it is used just to serialize,
+ // so it doesn't matter.
+ this._promise = PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+ .then(null, null).then( () => {
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ });
+ }
+};
+
+
+/**
+ * Transaction for removing a livemark item.
+ *
+ * @param aLivemarkId
+ * the identifier of the folder for the livemark.
+ *
+ * @return nsITransaction object
+ * @note used internally by PlacesRemoveItemTransaction, DO NOT EXPORT.
+ */
+function PlacesRemoveLivemarkTransaction(aLivemarkId)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aLivemarkId;
+ this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
+ this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
+
+ let annos = PlacesUtils.getAnnotationsForItem(this.item.id);
+ // Exclude livemark service annotations, those will be recreated automatically
+ let annosToExclude = [PlacesUtils.LMANNO_FEEDURI,
+ PlacesUtils.LMANNO_SITEURI];
+ this.item.annotations = annos.filter(function(aValue, aIndex, aArray) {
+ return !annosToExclude.includes(aValue.name);
+ });
+ this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
+ this.item.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(this.item.id);
+}
+
+PlacesRemoveLivemarkTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function RLTXN_doTransaction()
+ {
+ PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+ .then(aLivemark => {
+ this.item.feedURI = aLivemark.feedURI;
+ this.item.siteURI = aLivemark.siteURI;
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ }, Cu.reportError);
+ },
+
+ undoTransaction: function RLTXN_undoTransaction()
+ {
+ // Undo work must be serialized, otherwise won't be able to know the
+ // feedURI and siteURI of the livemark.
+ // The getLivemark callback is expected to receive a failure status but it
+ // is used just to serialize, so doesn't matter.
+ PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+ .then(null, () => {
+ PlacesUtils.livemarks.addLivemark({ parentId: this.item.parentId
+ , title: this.item.title
+ , siteURI: this.item.siteURI
+ , feedURI: this.item.feedURI
+ , index: this.item.index
+ , lastModified: this.item.lastModified
+ }).then(
+ aLivemark => {
+ let itemId = aLivemark.id;
+ PlacesUtils.bookmarks.setItemDateAdded(itemId, this.item.dateAdded);
+ PlacesUtils.setAnnotationsForItem(itemId, this.item.annotations);
+ }, Cu.reportError);
+ });
+ }
+};
+
+
+/**
+ * Transaction for moving an Item.
+ *
+ * @param aItemId
+ * the id of the item to move
+ * @param aNewParentId
+ * id of the new parent to move to
+ * @param aNewIndex
+ * index of the new position to move to
+ *
+ * @return nsITransaction object
+ */
+this.PlacesMoveItemTransaction =
+ function PlacesMoveItemTransaction(aItemId, aNewParentId, aNewIndex)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
+ this.new = new TransactionItemCache();
+ this.new.parentId = aNewParentId;
+ this.new.index = aNewIndex;
+}
+
+PlacesMoveItemTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function MITXN_doTransaction()
+ {
+ this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
+ PlacesUtils.bookmarks.moveItem(this.item.id,
+ this.new.parentId, this.new.index);
+ this._undoIndex = PlacesUtils.bookmarks.getItemIndex(this.item.id);
+ },
+
+ undoTransaction: function MITXN_undoTransaction()
+ {
+ // moving down in the same parent takes in count removal of the item
+ // so to revert positions we must move to oldIndex + 1
+ if (this.new.parentId == this.item.parentId &&
+ this.item.index > this._undoIndex) {
+ PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
+ this.item.index + 1);
+ }
+ else {
+ PlacesUtils.bookmarks.moveItem(this.item.id, this.item.parentId,
+ this.item.index);
+ }
+ }
+};
+
+
+/**
+ * Transaction for removing an Item
+ *
+ * @param aItemId
+ * id of the item to remove
+ *
+ * @return nsITransaction object
+ */
+this.PlacesRemoveItemTransaction =
+ function PlacesRemoveItemTransaction(aItemId)
+{
+ if (PlacesUtils.isRootItem(aItemId))
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ // if the item lives within a tag container, use the tagging transactions
+ let parent = PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
+ let grandparent = PlacesUtils.bookmarks.getFolderIdForItem(parent);
+ if (grandparent == PlacesUtils.tagsFolderId) {
+ let uri = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
+ return new PlacesUntagURITransaction(uri, [parent]);
+ }
+
+ // if the item is a livemark container we will not save its children.
+ if (PlacesUtils.annotations.itemHasAnnotation(aItemId,
+ PlacesUtils.LMANNO_FEEDURI))
+ return new PlacesRemoveLivemarkTransaction(aItemId);
+
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.item.itemType = PlacesUtils.bookmarks.getItemType(this.item.id);
+ if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
+ this.childTransactions = this._getFolderContentsTransactions();
+ // Remove this folder itself.
+ let txn = PlacesUtils.bookmarks.getRemoveFolderTransaction(this.item.id);
+ this.childTransactions.push(txn);
+ }
+ else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
+ this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
+ this.item.keyword =
+ PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
+ if (this.item.keyword)
+ this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
+ }
+
+ if (this.item.itemType != Ci.nsINavBookmarksService.TYPE_SEPARATOR)
+ this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
+
+ this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
+ this.item.annotations = PlacesUtils.getAnnotationsForItem(this.item.id);
+ this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
+ this.item.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(this.item.id);
+}
+
+PlacesRemoveItemTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function RITXN_doTransaction()
+ {
+ this.item.index = PlacesUtils.bookmarks.getItemIndex(this.item.id);
+
+ if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
+ let txn = new PlacesAggregatedTransaction("Remove item childTxn",
+ this.childTransactions);
+ txn.doTransaction();
+ }
+ else {
+ // Before removing the bookmark, save its tags.
+ let tags = this.item.uri ?
+ PlacesUtils.tagging.getTagsForURI(this.item.uri) : null;
+
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+
+ // If this was the last bookmark (excluding tag-items) for this url,
+ // persist the tags.
+ if (tags && PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
+ this.item.tags = tags;
+ }
+ }
+ },
+
+ undoTransaction: function RITXN_undoTransaction()
+ {
+ if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
+ this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
+ this.item.uri,
+ this.item.index,
+ this.item.title);
+ if (this.item.tags && this.item.tags.length > 0)
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ if (this.item.keyword) {
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
+ this.item.keyword);
+ if (this.item.postData) {
+ PlacesUtils.bookmarks.setPostDataForBookmark(this.item.id);
+ }
+ }
+ }
+ else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
+ let txn = new PlacesAggregatedTransaction("Remove item childTxn",
+ this.childTransactions);
+ txn.undoTransaction();
+ }
+ else { // TYPE_SEPARATOR
+ this.item.id = PlacesUtils.bookmarks.insertSeparator(this.item.parentId,
+ this.item.index);
+ }
+
+ if (this.item.annotations && this.item.annotations.length > 0)
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+
+ PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
+ PlacesUtils.bookmarks.setItemLastModified(this.item.id,
+ this.item.lastModified);
+ },
+
+ /**
+ * Returns a flat, ordered list of transactions for a depth-first recreation
+ * of items within this folder.
+ */
+ _getFolderContentsTransactions:
+ function RITXN__getFolderContentsTransactions()
+ {
+ let transactions = [];
+ let contents =
+ PlacesUtils.getFolderContents(this.item.id, false, false).root;
+ for (let i = 0; i < contents.childCount; ++i) {
+ let txn = new PlacesRemoveItemTransaction(contents.getChild(i).itemId);
+ transactions.push(txn);
+ }
+ contents.containerOpen = false;
+ // Reverse transactions to preserve parent-child relationship.
+ return transactions.reverse();
+ }
+};
+
+
+/**
+ * Transaction for editting a bookmark's title.
+ *
+ * @param aItemId
+ * id of the item to edit
+ * @param aNewTitle
+ * new title for the item to edit
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditItemTitleTransaction =
+ function PlacesEditItemTitleTransaction(aItemId, aNewTitle)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.title = aNewTitle;
+}
+
+PlacesEditItemTitleTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EITTXN_doTransaction()
+ {
+ this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
+ PlacesUtils.bookmarks.setItemTitle(this.item.id, this.new.title);
+ },
+
+ undoTransaction: function EITTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setItemTitle(this.item.id, this.item.title);
+ }
+};
+
+
+/**
+ * Transaction for editing a bookmark's uri.
+ *
+ * @param aItemId
+ * id of the bookmark to edit
+ * @param aNewURI
+ * new uri for the bookmark
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditBookmarkURITransaction =
+ function PlacesEditBookmarkURITransaction(aItemId, aNewURI) {
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.uri = aNewURI;
+}
+
+PlacesEditBookmarkURITransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EBUTXN_doTransaction()
+ {
+ this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
+ PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.new.uri);
+ // move tags from old URI to new URI
+ this.item.tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
+ if (this.item.tags.length > 0) {
+ // only untag the old URI if this is the only bookmark
+ if (PlacesUtils.getBookmarksForURI(this.item.uri, {}).length == 0)
+ PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
+ PlacesUtils.tagging.tagURI(this.new.uri, this.item.tags);
+ }
+ },
+
+ undoTransaction: function EBUTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.changeBookmarkURI(this.item.id, this.item.uri);
+ // move tags from new URI to old URI
+ if (this.item.tags.length > 0) {
+ // only untag the new URI if this is the only bookmark
+ if (PlacesUtils.getBookmarksForURI(this.new.uri, {}).length == 0)
+ PlacesUtils.tagging.untagURI(this.new.uri, this.item.tags);
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ }
+ }
+};
+
+
+/**
+ * Transaction for setting/unsetting an item annotation
+ *
+ * @param aItemId
+ * id of the item where to set annotation
+ * @param aAnnotationObject
+ * Object representing an annotation, containing the following
+ * properties: name, flags, expires, value.
+ * If value is null the annotation will be removed
+ *
+ * @return nsITransaction object
+ */
+this.PlacesSetItemAnnotationTransaction =
+ function PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.annotations = [aAnnotationObject];
+}
+
+PlacesSetItemAnnotationTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function SIATXN_doTransaction()
+ {
+ let annoName = this.new.annotations[0].name;
+ if (PlacesUtils.annotations.itemHasAnnotation(this.item.id, annoName)) {
+ // fill the old anno if it is set
+ let flags = {}, expires = {}, type = {};
+ PlacesUtils.annotations.getItemAnnotationInfo(this.item.id, annoName, flags,
+ expires, type);
+ let value = PlacesUtils.annotations.getItemAnnotation(this.item.id,
+ annoName);
+ this.item.annotations = [{ name: annoName,
+ type: type.value,
+ flags: flags.value,
+ value: value,
+ expires: expires.value }];
+ }
+ else {
+ // create an empty old anno
+ this.item.annotations = [{ name: annoName,
+ flags: 0,
+ value: null,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+ }
+
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.new.annotations);
+ },
+
+ undoTransaction: function SIATXN_undoTransaction()
+ {
+ PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
+ }
+};
+
+
+/**
+ * Transaction for setting/unsetting a page annotation
+ *
+ * @param aURI
+ * URI of the page where to set annotation
+ * @param aAnnotationObject
+ * Object representing an annotation, containing the following
+ * properties: name, flags, expires, value.
+ * If value is null the annotation will be removed
+ *
+ * @return nsITransaction object
+ */
+this.PlacesSetPageAnnotationTransaction =
+ function PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ this.new = new TransactionItemCache();
+ this.new.annotations = [aAnnotationObject];
+}
+
+PlacesSetPageAnnotationTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function SPATXN_doTransaction()
+ {
+ let annoName = this.new.annotations[0].name;
+ if (PlacesUtils.annotations.pageHasAnnotation(this.item.uri, annoName)) {
+ // fill the old anno if it is set
+ let flags = {}, expires = {}, type = {};
+ PlacesUtils.annotations.getPageAnnotationInfo(this.item.uri, annoName, flags,
+ expires, type);
+ let value = PlacesUtils.annotations.getPageAnnotation(this.item.uri,
+ annoName);
+ this.item.annotations = [{ name: annoName,
+ flags: flags.value,
+ value: value,
+ expires: expires.value }];
+ }
+ else {
+ // create an empty old anno
+ this.item.annotations = [{ name: annoName,
+ type: Ci.nsIAnnotationService.TYPE_STRING,
+ flags: 0,
+ value: null,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+ }
+
+ PlacesUtils.setAnnotationsForURI(this.item.uri, this.new.annotations);
+ },
+
+ undoTransaction: function SPATXN_undoTransaction()
+ {
+ PlacesUtils.setAnnotationsForURI(this.item.uri, this.item.annotations);
+ }
+};
+
+
+/**
+ * Transaction for editing a bookmark's keyword.
+ *
+ * @param aItemId
+ * id of the bookmark to edit
+ * @param aNewKeyword
+ * new keyword for the bookmark
+ * @param aNewPostData [optional]
+ * new keyword's POST data, if available
+ * @param aOldKeyword [optional]
+ * old keyword of the bookmark
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditBookmarkKeywordTransaction =
+ function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword,
+ aNewPostData, aOldKeyword) {
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.item.keyword = aOldKeyword;
+ this.item.href = (PlacesUtils.bookmarks.getBookmarkURI(aItemId)).spec;
+ this.new = new TransactionItemCache();
+ this.new.keyword = aNewKeyword;
+ this.new.postData = aNewPostData
+}
+
+PlacesEditBookmarkKeywordTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EBKTXN_doTransaction()
+ {
+ let done = false;
+ Task.spawn(function* () {
+ if (this.item.keyword) {
+ let oldEntry = yield PlacesUtils.keywords.fetch(this.item.keyword);
+ this.item.postData = oldEntry.postData;
+ yield PlacesUtils.keywords.remove(this.item.keyword);
+ }
+
+ if (this.new.keyword) {
+ yield PlacesUtils.keywords.insert({
+ url: this.item.href,
+ keyword: this.new.keyword,
+ postData: this.new.postData || this.item.postData
+ });
+ }
+ }.bind(this)).catch(Cu.reportError)
+ .then(() => done = true);
+ // TODO: Until we can move to PlacesTransactions.jsm, we must spin the
+ // events loop :(
+ let thread = Services.tm.currentThread;
+ while (!done) {
+ thread.processNextEvent(true);
+ }
+ },
+
+ undoTransaction: function EBKTXN_undoTransaction()
+ {
+
+ let done = false;
+ Task.spawn(function* () {
+ if (this.new.keyword) {
+ yield PlacesUtils.keywords.remove(this.new.keyword);
+ }
+
+ if (this.item.keyword) {
+ yield PlacesUtils.keywords.insert({
+ url: this.item.href,
+ keyword: this.item.keyword,
+ postData: this.item.postData
+ });
+ }
+ }.bind(this)).catch(Cu.reportError)
+ .then(() => done = true);
+ // TODO: Until we can move to PlacesTransactions.jsm, we must spin the
+ // events loop :(
+ let thread = Services.tm.currentThread;
+ while (!done) {
+ thread.processNextEvent(true);
+ }
+ }
+};
+
+
+/**
+ * Transaction for editing the post data associated with a bookmark.
+ *
+ * @param aItemId
+ * id of the bookmark to edit
+ * @param aPostData
+ * post data
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditBookmarkPostDataTransaction =
+ function PlacesEditBookmarkPostDataTransaction(aItemId, aPostData)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.postData = aPostData;
+}
+
+PlacesEditBookmarkPostDataTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction() {
+ // Setting null postData is not supported by the current schema.
+ if (this.new.postData) {
+ this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
+ PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
+ }
+ },
+
+ undoTransaction() {
+ // Setting null postData is not supported by the current schema.
+ if (this.item.postData) {
+ PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
+ }
+ }
+};
+
+
+/**
+ * Transaction for editing an item's date added property.
+ *
+ * @param aItemId
+ * id of the item to edit
+ * @param aNewDateAdded
+ * new date added for the item
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditItemDateAddedTransaction =
+ function PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.dateAdded = aNewDateAdded;
+}
+
+PlacesEditItemDateAddedTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function EIDATXN_doTransaction()
+ {
+ // Child transactions have the id set as parentId.
+ if (this.item.id == -1 && this.item.parentId != -1)
+ this.item.id = this.item.parentId;
+ this.item.dateAdded =
+ PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
+ PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.new.dateAdded);
+ },
+
+ undoTransaction: function EIDATXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setItemDateAdded(this.item.id, this.item.dateAdded);
+ }
+};
+
+
+/**
+ * Transaction for editing an item's last modified time.
+ *
+ * @param aItemId
+ * id of the item to edit
+ * @param aNewLastModified
+ * new last modified date for the item
+ *
+ * @return nsITransaction object
+ */
+this.PlacesEditItemLastModifiedTransaction =
+ function PlacesEditItemLastModifiedTransaction(aItemId, aNewLastModified)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aItemId;
+ this.new = new TransactionItemCache();
+ this.new.lastModified = aNewLastModified;
+}
+
+PlacesEditItemLastModifiedTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction:
+ function EILMTXN_doTransaction()
+ {
+ // Child transactions have the id set as parentId.
+ if (this.item.id == -1 && this.item.parentId != -1)
+ this.item.id = this.item.parentId;
+ this.item.lastModified =
+ PlacesUtils.bookmarks.getItemLastModified(this.item.id);
+ PlacesUtils.bookmarks.setItemLastModified(this.item.id,
+ this.new.lastModified);
+ },
+
+ undoTransaction:
+ function EILMTXN_undoTransaction()
+ {
+ PlacesUtils.bookmarks.setItemLastModified(this.item.id,
+ this.item.lastModified);
+ }
+};
+
+
+/**
+ * Transaction for sorting a folder by name
+ *
+ * @param aFolderId
+ * id of the folder to sort
+ *
+ * @return nsITransaction object
+ */
+this.PlacesSortFolderByNameTransaction =
+ function PlacesSortFolderByNameTransaction(aFolderId)
+{
+ this.item = new TransactionItemCache();
+ this.item.id = aFolderId;
+}
+
+PlacesSortFolderByNameTransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function SFBNTXN_doTransaction()
+ {
+ this._oldOrder = [];
+
+ let contents =
+ PlacesUtils.getFolderContents(this.item.id, false, false).root;
+ let count = contents.childCount;
+
+ // sort between separators
+ let newOrder = [];
+ let preSep = []; // temporary array for sorting each group of items
+ let sortingMethod =
+ function (a, b) {
+ if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
+ return -1;
+ if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
+ return 1;
+ return a.title.localeCompare(b.title);
+ };
+
+ for (let i = 0; i < count; ++i) {
+ let item = contents.getChild(i);
+ this._oldOrder[item.itemId] = i;
+ if (PlacesUtils.nodeIsSeparator(item)) {
+ if (preSep.length > 0) {
+ preSep.sort(sortingMethod);
+ newOrder = newOrder.concat(preSep);
+ preSep.splice(0, preSep.length);
+ }
+ newOrder.push(item);
+ }
+ else
+ preSep.push(item);
+ }
+ contents.containerOpen = false;
+
+ if (preSep.length > 0) {
+ preSep.sort(sortingMethod);
+ newOrder = newOrder.concat(preSep);
+ }
+
+ // set the nex indexes
+ let callback = {
+ runBatched: function() {
+ for (let i = 0; i < newOrder.length; ++i) {
+ PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
+ }
+ }
+ };
+ PlacesUtils.bookmarks.runInBatchMode(callback, null);
+ },
+
+ undoTransaction: function SFBNTXN_undoTransaction()
+ {
+ let callback = {
+ _self: this,
+ runBatched: function() {
+ for (let item in this._self._oldOrder)
+ PlacesUtils.bookmarks.setItemIndex(item, this._self._oldOrder[item]);
+ }
+ };
+ PlacesUtils.bookmarks.runInBatchMode(callback, null);
+ }
+};
+
+
+/**
+ * Transaction for tagging a URL with the given set of tags. Current tags set
+ * for the URL persist. It's the caller's job to check whether or not aURI
+ * was already tagged by any of the tags in aTags, undoing this tags
+ * transaction removes them all from aURL!
+ *
+ * @param aURI
+ * the URL to tag.
+ * @param aTags
+ * Array of tags to set for the given URL.
+ */
+this.PlacesTagURITransaction =
+ function PlacesTagURITransaction(aURI, aTags)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ this.item.tags = aTags;
+}
+
+PlacesTagURITransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function TUTXN_doTransaction()
+ {
+ if (PlacesUtils.getMostRecentBookmarkForURI(this.item.uri) == -1) {
+ // There is no bookmark for this uri, but we only allow to tag bookmarks.
+ // Force an unfiled bookmark first.
+ this.item.id =
+ PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ this.item.uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ PlacesUtils.history.getPageTitle(this.item.uri));
+ }
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ },
+
+ undoTransaction: function TUTXN_undoTransaction()
+ {
+ if (this.item.id != -1) {
+ PlacesUtils.bookmarks.removeItem(this.item.id);
+ this.item.id = -1;
+ }
+ PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
+ }
+};
+
+
+/**
+ * Transaction for removing tags from a URL. It's the caller's job to check
+ * whether or not aURI isn't tagged by any of the tags in aTags, undoing this
+ * tags transaction adds them all to aURL!
+ *
+ * @param aURI
+ * the URL to un-tag.
+ * @param aTags
+ * Array of tags to unset. pass null to remove all tags from the given
+ * url.
+ */
+this.PlacesUntagURITransaction =
+ function PlacesUntagURITransaction(aURI, aTags)
+{
+ this.item = new TransactionItemCache();
+ this.item.uri = aURI;
+ if (aTags) {
+ // Within this transaction, we cannot rely on tags given by itemId
+ // since the tag containers may be gone after we call untagURI.
+ // Thus, we convert each tag given by its itemId to name.
+ let tags = [];
+ for (let i = 0; i < aTags.length; ++i) {
+ if (typeof(aTags[i]) == "number")
+ tags.push(PlacesUtils.bookmarks.getItemTitle(aTags[i]));
+ else
+ tags.push(aTags[i]);
+ }
+ this.item.tags = tags;
+ }
+}
+
+PlacesUntagURITransaction.prototype = {
+ __proto__: BaseTransaction.prototype,
+
+ doTransaction: function UTUTXN_doTransaction()
+ {
+ // Filter tags existing on the bookmark, otherwise on undo we may try to
+ // set nonexistent tags.
+ let tags = PlacesUtils.tagging.getTagsForURI(this.item.uri);
+ this.item.tags = this.item.tags.filter(function (aTag) {
+ return tags.includes(aTag);
+ });
+ PlacesUtils.tagging.untagURI(this.item.uri, this.item.tags);
+ },
+
+ undoTransaction: function UTUTXN_undoTransaction()
+ {
+ PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
+ }
+};
+
+/**
+ * Executes a boolean validate function, throwing if it returns false.
+ *
+ * @param boolValidateFn
+ * A boolean validate function.
+ * @return the input value.
+ * @throws if input doesn't pass the validate function.
+ */
+function simpleValidateFunc(boolValidateFn) {
+ return (v, input) => {
+ if (!boolValidateFn(v, input))
+ throw new Error("Invalid value");
+ return v;
+ };
+}
diff --git a/toolkit/components/places/SQLFunctions.cpp b/toolkit/components/places/SQLFunctions.cpp
new file mode 100644
index 0000000000..e3cc7d7f08
--- /dev/null
+++ b/toolkit/components/places/SQLFunctions.cpp
@@ -0,0 +1,941 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/storage.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "nsEscape.h"
+#include "mozIPlacesAutoComplete.h"
+#include "SQLFunctions.h"
+#include "nsMathUtils.h"
+#include "nsUTF8Utils.h"
+#include "nsINavHistoryService.h"
+#include "nsPrintfCString.h"
+#include "nsNavHistory.h"
+#include "mozilla/Likely.h"
+#include "nsVariant.h"
+#include "mozilla/HashFunctions.h"
+
+// Maximum number of chars to search through.
+// MatchAutoCompleteFunction won't look for matches over this threshold.
+#define MAX_CHARS_TO_SEARCH_THROUGH 255
+
+using namespace mozilla::storage;
+
+// Keep the GUID-related parts of this file in sync with toolkit/downloads/SQLFunctions.cpp!
+
+////////////////////////////////////////////////////////////////////////////////
+//// Anonymous Helpers
+
+namespace {
+
+ typedef nsACString::const_char_iterator const_char_iterator;
+
+ /**
+ * Get a pointer to the word boundary after aStart if aStart points to an
+ * ASCII letter (i.e. [a-zA-Z]). Otherwise, return aNext, which we assume
+ * points to the next character in the UTF-8 sequence.
+ *
+ * We define a word boundary as anything that's not [a-z] -- this lets us
+ * match CamelCase words.
+ *
+ * @param aStart the beginning of the UTF-8 sequence
+ * @param aNext the next character in the sequence
+ * @param aEnd the first byte which is not part of the sequence
+ *
+ * @return a pointer to the next word boundary after aStart
+ */
+ static
+ MOZ_ALWAYS_INLINE const_char_iterator
+ nextWordBoundary(const_char_iterator const aStart,
+ const_char_iterator const aNext,
+ const_char_iterator const aEnd) {
+
+ const_char_iterator cur = aStart;
+ if (('a' <= *cur && *cur <= 'z') ||
+ ('A' <= *cur && *cur <= 'Z')) {
+
+ // Since we'll halt as soon as we see a non-ASCII letter, we can do a
+ // simple byte-by-byte comparison here and avoid the overhead of a
+ // UTF8CharEnumerator.
+ do {
+ cur++;
+ } while (cur < aEnd && 'a' <= *cur && *cur <= 'z');
+ }
+ else {
+ cur = aNext;
+ }
+
+ return cur;
+ }
+
+ enum FindInStringBehavior {
+ eFindOnBoundary,
+ eFindAnywhere
+ };
+
+ /**
+ * findAnywhere and findOnBoundary do almost the same thing, so it's natural
+ * to implement them in terms of a single function. They're both
+ * performance-critical functions, however, and checking aBehavior makes them
+ * a bit slower. Our solution is to define findInString as MOZ_ALWAYS_INLINE
+ * and rely on the compiler to optimize out the aBehavior check.
+ *
+ * @param aToken
+ * The token we're searching for
+ * @param aSourceString
+ * The string in which we're searching
+ * @param aBehavior
+ * eFindOnBoundary if we should only consider matchines which occur on
+ * word boundaries, or eFindAnywhere if we should consider matches
+ * which appear anywhere.
+ *
+ * @return true if aToken was found in aSourceString, false otherwise.
+ */
+ static
+ MOZ_ALWAYS_INLINE bool
+ findInString(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString,
+ FindInStringBehavior aBehavior)
+ {
+ // CaseInsensitiveUTF8CharsEqual assumes that there's at least one byte in
+ // the both strings, so don't pass an empty token here.
+ NS_PRECONDITION(!aToken.IsEmpty(), "Don't search for an empty token!");
+
+ // We cannot match anything if there is nothing to search.
+ if (aSourceString.IsEmpty()) {
+ return false;
+ }
+
+ const_char_iterator tokenStart(aToken.BeginReading()),
+ tokenEnd(aToken.EndReading()),
+ sourceStart(aSourceString.BeginReading()),
+ sourceEnd(aSourceString.EndReading());
+
+ do {
+ // We are on a word boundary (if aBehavior == eFindOnBoundary). See if
+ // aToken matches sourceStart.
+
+ // Check whether the first character in the token matches the character
+ // at sourceStart. At the same time, get a pointer to the next character
+ // in both the token and the source.
+ const_char_iterator sourceNext, tokenCur;
+ bool error;
+ if (CaseInsensitiveUTF8CharsEqual(sourceStart, tokenStart,
+ sourceEnd, tokenEnd,
+ &sourceNext, &tokenCur, &error)) {
+
+ // We don't need to check |error| here -- if
+ // CaseInsensitiveUTF8CharCompare encounters an error, it'll also
+ // return false and we'll catch the error outside the if.
+
+ const_char_iterator sourceCur = sourceNext;
+ while (true) {
+ if (tokenCur >= tokenEnd) {
+ // We matched the whole token!
+ return true;
+ }
+
+ if (sourceCur >= sourceEnd) {
+ // We ran into the end of source while matching a token. This
+ // means we'll never find the token we're looking for.
+ return false;
+ }
+
+ if (!CaseInsensitiveUTF8CharsEqual(sourceCur, tokenCur,
+ sourceEnd, tokenEnd,
+ &sourceCur, &tokenCur, &error)) {
+ // sourceCur doesn't match tokenCur (or there's an error), so break
+ // out of this loop.
+ break;
+ }
+ }
+ }
+
+ // If something went wrong above, get out of here!
+ if (MOZ_UNLIKELY(error)) {
+ return false;
+ }
+
+ // We didn't match the token. If we're searching for matches on word
+ // boundaries, skip to the next word boundary. Otherwise, advance
+ // forward one character, using the sourceNext pointer we saved earlier.
+
+ if (aBehavior == eFindOnBoundary) {
+ sourceStart = nextWordBoundary(sourceStart, sourceNext, sourceEnd);
+ }
+ else {
+ sourceStart = sourceNext;
+ }
+
+ } while (sourceStart < sourceEnd);
+
+ return false;
+ }
+
+ static
+ MOZ_ALWAYS_INLINE nsDependentCString
+ getSharedString(mozIStorageValueArray* aValues, uint32_t aIndex) {
+ uint32_t len;
+ const char* str = aValues->AsSharedUTF8String(aIndex, &len);
+ if (!str) {
+ return nsDependentCString("", (uint32_t)0);
+ }
+ return nsDependentCString(str, len);
+ }
+
+} // End anonymous namespace
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// AutoComplete Matching Function
+
+ /* static */
+ nsresult
+ MatchAutoCompleteFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<MatchAutoCompleteFunction> function =
+ new MatchAutoCompleteFunction();
+
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("autocomplete_match"), kArgIndexLength, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ /* static */
+ nsDependentCSubstring
+ MatchAutoCompleteFunction::fixupURISpec(const nsACString &aURISpec,
+ int32_t aMatchBehavior,
+ nsACString &aSpecBuf)
+ {
+ nsDependentCSubstring fixedSpec;
+
+ // Try to unescape the string. If that succeeds and yields a different
+ // string which is also valid UTF-8, we'll use it.
+ // Otherwise, we will simply use our original string.
+ bool unescaped = NS_UnescapeURL(aURISpec.BeginReading(),
+ aURISpec.Length(), esc_SkipControl, aSpecBuf);
+ if (unescaped && IsUTF8(aSpecBuf)) {
+ fixedSpec.Rebind(aSpecBuf, 0);
+ } else {
+ fixedSpec.Rebind(aURISpec, 0);
+ }
+
+ if (aMatchBehavior == mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED)
+ return fixedSpec;
+
+ if (StringBeginsWith(fixedSpec, NS_LITERAL_CSTRING("http://"))) {
+ fixedSpec.Rebind(fixedSpec, 7);
+ } else if (StringBeginsWith(fixedSpec, NS_LITERAL_CSTRING("https://"))) {
+ fixedSpec.Rebind(fixedSpec, 8);
+ } else if (StringBeginsWith(fixedSpec, NS_LITERAL_CSTRING("ftp://"))) {
+ fixedSpec.Rebind(fixedSpec, 6);
+ }
+
+ if (StringBeginsWith(fixedSpec, NS_LITERAL_CSTRING("www."))) {
+ fixedSpec.Rebind(fixedSpec, 4);
+ }
+
+ return fixedSpec;
+ }
+
+ /* static */
+ bool
+ MatchAutoCompleteFunction::findAnywhere(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString)
+ {
+ // We can't use FindInReadable here; it works only for ASCII.
+
+ return findInString(aToken, aSourceString, eFindAnywhere);
+ }
+
+ /* static */
+ bool
+ MatchAutoCompleteFunction::findOnBoundary(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString)
+ {
+ return findInString(aToken, aSourceString, eFindOnBoundary);
+ }
+
+ /* static */
+ bool
+ MatchAutoCompleteFunction::findBeginning(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString)
+ {
+ NS_PRECONDITION(!aToken.IsEmpty(), "Don't search for an empty token!");
+
+ // We can't use StringBeginsWith here, unfortunately. Although it will
+ // happily take a case-insensitive UTF8 comparator, it eventually calls
+ // nsACString::Equals, which checks that the two strings contain the same
+ // number of bytes before calling the comparator. Two characters may be
+ // case-insensitively equal while taking up different numbers of bytes, so
+ // this is not what we want.
+
+ const_char_iterator tokenStart(aToken.BeginReading()),
+ tokenEnd(aToken.EndReading()),
+ sourceStart(aSourceString.BeginReading()),
+ sourceEnd(aSourceString.EndReading());
+
+ bool dummy;
+ while (sourceStart < sourceEnd &&
+ CaseInsensitiveUTF8CharsEqual(sourceStart, tokenStart,
+ sourceEnd, tokenEnd,
+ &sourceStart, &tokenStart, &dummy)) {
+
+ // We found the token!
+ if (tokenStart >= tokenEnd) {
+ return true;
+ }
+ }
+
+ // We don't need to check CaseInsensitiveUTF8CharsEqual's error condition
+ // (stored in |dummy|), since the function will return false if it
+ // encounters an error.
+
+ return false;
+ }
+
+ /* static */
+ bool
+ MatchAutoCompleteFunction::findBeginningCaseSensitive(
+ const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString)
+ {
+ NS_PRECONDITION(!aToken.IsEmpty(), "Don't search for an empty token!");
+
+ return StringBeginsWith(aSourceString, aToken);
+ }
+
+ /* static */
+ MatchAutoCompleteFunction::searchFunctionPtr
+ MatchAutoCompleteFunction::getSearchFunction(int32_t aBehavior)
+ {
+ switch (aBehavior) {
+ case mozIPlacesAutoComplete::MATCH_ANYWHERE:
+ case mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED:
+ return findAnywhere;
+ case mozIPlacesAutoComplete::MATCH_BEGINNING:
+ return findBeginning;
+ case mozIPlacesAutoComplete::MATCH_BEGINNING_CASE_SENSITIVE:
+ return findBeginningCaseSensitive;
+ case mozIPlacesAutoComplete::MATCH_BOUNDARY:
+ default:
+ return findOnBoundary;
+ };
+ }
+
+ NS_IMPL_ISUPPORTS(
+ MatchAutoCompleteFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ MatchAutoCompleteFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+ {
+ // Macro to make the code a bit cleaner and easier to read. Operates on
+ // searchBehavior.
+ int32_t searchBehavior = aArguments->AsInt32(kArgIndexSearchBehavior);
+ #define HAS_BEHAVIOR(aBitName) \
+ (searchBehavior & mozIPlacesAutoComplete::BEHAVIOR_##aBitName)
+
+ nsDependentCString searchString =
+ getSharedString(aArguments, kArgSearchString);
+ nsDependentCString url =
+ getSharedString(aArguments, kArgIndexURL);
+
+ int32_t matchBehavior = aArguments->AsInt32(kArgIndexMatchBehavior);
+
+ // We only want to filter javascript: URLs if we are not supposed to search
+ // for them, and the search does not start with "javascript:".
+ if (matchBehavior != mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED &&
+ StringBeginsWith(url, NS_LITERAL_CSTRING("javascript:")) &&
+ !HAS_BEHAVIOR(JAVASCRIPT) &&
+ !StringBeginsWith(searchString, NS_LITERAL_CSTRING("javascript:"))) {
+ NS_ADDREF(*_result = new IntegerVariant(0));
+ return NS_OK;
+ }
+
+ int32_t visitCount = aArguments->AsInt32(kArgIndexVisitCount);
+ bool typed = aArguments->AsInt32(kArgIndexTyped) ? true : false;
+ bool bookmark = aArguments->AsInt32(kArgIndexBookmark) ? true : false;
+ nsDependentCString tags = getSharedString(aArguments, kArgIndexTags);
+ int32_t openPageCount = aArguments->AsInt32(kArgIndexOpenPageCount);
+ bool matches = false;
+ if (HAS_BEHAVIOR(RESTRICT)) {
+ // Make sure we match all the filter requirements. If a given restriction
+ // is active, make sure the corresponding condition is not true.
+ matches = (!HAS_BEHAVIOR(HISTORY) || visitCount > 0) &&
+ (!HAS_BEHAVIOR(TYPED) || typed) &&
+ (!HAS_BEHAVIOR(BOOKMARK) || bookmark) &&
+ (!HAS_BEHAVIOR(TAG) || !tags.IsVoid()) &&
+ (!HAS_BEHAVIOR(OPENPAGE) || openPageCount > 0);
+ } else {
+ // Make sure that we match all the filter requirements and that the
+ // corresponding condition is true if at least a given restriction is active.
+ matches = (HAS_BEHAVIOR(HISTORY) && visitCount > 0) ||
+ (HAS_BEHAVIOR(TYPED) && typed) ||
+ (HAS_BEHAVIOR(BOOKMARK) && bookmark) ||
+ (HAS_BEHAVIOR(TAG) && !tags.IsVoid()) ||
+ (HAS_BEHAVIOR(OPENPAGE) && openPageCount > 0);
+ }
+
+ if (!matches) {
+ NS_ADDREF(*_result = new IntegerVariant(0));
+ return NS_OK;
+ }
+
+ // Obtain our search function.
+ searchFunctionPtr searchFunction = getSearchFunction(matchBehavior);
+
+ // Clean up our URI spec and prepare it for searching.
+ nsCString fixedUrlBuf;
+ nsDependentCSubstring fixedUrl =
+ fixupURISpec(url, matchBehavior, fixedUrlBuf);
+ // Limit the number of chars we search through.
+ const nsDependentCSubstring& trimmedUrl =
+ Substring(fixedUrl, 0, MAX_CHARS_TO_SEARCH_THROUGH);
+
+ nsDependentCString title = getSharedString(aArguments, kArgIndexTitle);
+ // Limit the number of chars we search through.
+ const nsDependentCSubstring& trimmedTitle =
+ Substring(title, 0, MAX_CHARS_TO_SEARCH_THROUGH);
+
+ // Determine if every token matches either the bookmark title, tags, page
+ // title, or page URL.
+ nsCWhitespaceTokenizer tokenizer(searchString);
+ while (matches && tokenizer.hasMoreTokens()) {
+ const nsDependentCSubstring &token = tokenizer.nextToken();
+
+ if (HAS_BEHAVIOR(TITLE) && HAS_BEHAVIOR(URL)) {
+ matches = (searchFunction(token, trimmedTitle) ||
+ searchFunction(token, tags)) &&
+ searchFunction(token, trimmedUrl);
+ }
+ else if (HAS_BEHAVIOR(TITLE)) {
+ matches = searchFunction(token, trimmedTitle) ||
+ searchFunction(token, tags);
+ }
+ else if (HAS_BEHAVIOR(URL)) {
+ matches = searchFunction(token, trimmedUrl);
+ }
+ else {
+ matches = searchFunction(token, trimmedTitle) ||
+ searchFunction(token, tags) ||
+ searchFunction(token, trimmedUrl);
+ }
+ }
+
+ NS_ADDREF(*_result = new IntegerVariant(matches ? 1 : 0));
+ return NS_OK;
+ #undef HAS_BEHAVIOR
+ }
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Frecency Calculation Function
+
+ /* static */
+ nsresult
+ CalculateFrecencyFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<CalculateFrecencyFunction> function =
+ new CalculateFrecencyFunction();
+
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("calculate_frecency"), 1, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMPL_ISUPPORTS(
+ CalculateFrecencyFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ CalculateFrecencyFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+ {
+ // Fetch arguments. Use default values if they were omitted.
+ uint32_t numEntries;
+ nsresult rv = aArguments->GetNumEntries(&numEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numEntries == 1, "unexpected number of arguments");
+
+ int64_t pageId = aArguments->AsInt64(0);
+ MOZ_ASSERT(pageId > 0, "Should always pass a valid page id");
+ if (pageId <= 0) {
+ NS_ADDREF(*_result = new IntegerVariant(0));
+ return NS_OK;
+ }
+
+ int32_t typed = 0;
+ int32_t visitCount = 0;
+ bool hasBookmark = false;
+ int32_t isQuery = 0;
+ float pointsForSampledVisits = 0.0;
+ int32_t numSampledVisits = 0;
+ int32_t bonus = 0;
+
+ // This is a const version of the history object for thread-safety.
+ const nsNavHistory* history = nsNavHistory::GetConstHistoryService();
+ NS_ENSURE_STATE(history);
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+
+
+ // Fetch the page stats from the database.
+ {
+ RefPtr<mozIStorageStatement> getPageInfo = DB->GetStatement(
+ "SELECT typed, visit_count, foreign_count, "
+ "(substr(url, 0, 7) = 'place:') "
+ "FROM moz_places "
+ "WHERE id = :page_id "
+ );
+ NS_ENSURE_STATE(getPageInfo);
+ mozStorageStatementScoper infoScoper(getPageInfo);
+
+ rv = getPageInfo->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult = false;
+ rv = getPageInfo->ExecuteStep(&hasResult);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_UNEXPECTED);
+
+ rv = getPageInfo->GetInt32(0, &typed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = getPageInfo->GetInt32(1, &visitCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t foreignCount = 0;
+ rv = getPageInfo->GetInt32(2, &foreignCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hasBookmark = foreignCount > 0;
+ rv = getPageInfo->GetInt32(3, &isQuery);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (visitCount > 0) {
+ // Get a sample of the last visits to the page, to calculate its weight.
+ // In case of a temporary or permanent redirect, calculate the frecency
+ // as if the original page was visited.
+ nsCOMPtr<mozIStorageStatement> getVisits = DB->GetStatement(
+ NS_LITERAL_CSTRING(
+ "/* do not warn (bug 659740 - SQLite may ignore index if few visits exist) */"
+ "SELECT "
+ "ROUND((strftime('%s','now','localtime','utc') - v.visit_date/1000000)/86400), "
+ "IFNULL(r.visit_type, v.visit_type), "
+ "v.visit_date "
+ "FROM moz_historyvisits v "
+ "LEFT JOIN moz_historyvisits r ON r.id = v.from_visit AND v.visit_type BETWEEN "
+ ) + nsPrintfCString("%d AND %d ", nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY) +
+ NS_LITERAL_CSTRING(
+ "WHERE v.place_id = :page_id "
+ "ORDER BY v.visit_date DESC "
+ )
+ );
+ NS_ENSURE_STATE(getVisits);
+ mozStorageStatementScoper visitsScoper(getVisits);
+ rv = getVisits->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Fetch only a limited number of recent visits.
+ bool hasResult = false;
+ for (int32_t maxVisits = history->GetNumVisitsForFrecency();
+ numSampledVisits < maxVisits &&
+ NS_SUCCEEDED(getVisits->ExecuteStep(&hasResult)) && hasResult;
+ numSampledVisits++) {
+ int32_t visitType;
+ rv = getVisits->GetInt32(1, &visitType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bonus = history->GetFrecencyTransitionBonus(visitType, true);
+
+ // Add the bookmark visit bonus.
+ if (hasBookmark) {
+ bonus += history->GetFrecencyTransitionBonus(nsINavHistoryService::TRANSITION_BOOKMARK, true);
+ }
+
+ // If bonus was zero, we can skip the work to determine the weight.
+ if (bonus) {
+ int32_t ageInDays = getVisits->AsInt32(0);
+ int32_t weight = history->GetFrecencyAgedWeight(ageInDays);
+ pointsForSampledVisits += (float)(weight * (bonus / 100.0));
+ }
+ }
+ }
+
+ // If we sampled some visits for this page, use the calculated weight.
+ if (numSampledVisits) {
+ // We were unable to calculate points, maybe cause all the visits in the
+ // sample had a zero bonus. Though, we know the page has some past valid
+ // visit, or visit_count would be zero. Thus we set the frecency to
+ // -1, so they are still shown in autocomplete.
+ if (!pointsForSampledVisits) {
+ NS_ADDREF(*_result = new IntegerVariant(-1));
+ }
+ else {
+ // Estimate frecency using the sampled visits.
+ // Use ceilf() so that we don't round down to 0, which
+ // would cause us to completely ignore the place during autocomplete.
+ NS_ADDREF(*_result = new IntegerVariant((int32_t) ceilf(visitCount * ceilf(pointsForSampledVisits) / numSampledVisits)));
+ }
+ return NS_OK;
+ }
+
+ // Otherwise this page has no visits, it may be bookmarked.
+ if (!hasBookmark || isQuery) {
+ NS_ADDREF(*_result = new IntegerVariant(0));
+ return NS_OK;
+ }
+
+ // For unvisited bookmarks, produce a non-zero frecency, so that they show
+ // up in URL bar autocomplete.
+ visitCount = 1;
+
+ // Make it so something bookmarked and typed will have a higher frecency
+ // than something just typed or just bookmarked.
+ bonus += history->GetFrecencyTransitionBonus(nsINavHistoryService::TRANSITION_BOOKMARK, false);
+ if (typed) {
+ bonus += history->GetFrecencyTransitionBonus(nsINavHistoryService::TRANSITION_TYPED, false);
+ }
+
+ // Assume "now" as our ageInDays, so use the first bucket.
+ pointsForSampledVisits = history->GetFrecencyBucketWeight(1) * (bonus / (float)100.0);
+
+ // use ceilf() so that we don't round down to 0, which
+ // would cause us to completely ignore the place during autocomplete
+ NS_ADDREF(*_result = new IntegerVariant((int32_t) ceilf(visitCount * ceilf(pointsForSampledVisits))));
+
+ return NS_OK;
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+//// GUID Creation Function
+
+ /* static */
+ nsresult
+ GenerateGUIDFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<GenerateGUIDFunction> function = new GenerateGUIDFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("generate_guid"), 0, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMPL_ISUPPORTS(
+ GenerateGUIDFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ GenerateGUIDFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+ {
+ nsAutoCString guid;
+ nsresult rv = GenerateGUID(guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*_result = new UTF8TextVariant(guid));
+ return NS_OK;
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+//// Get Unreversed Host Function
+
+ /* static */
+ nsresult
+ GetUnreversedHostFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<GetUnreversedHostFunction> function = new GetUnreversedHostFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("get_unreversed_host"), 1, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMPL_ISUPPORTS(
+ GetUnreversedHostFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ GetUnreversedHostFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+ {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ nsAutoString src;
+ aArguments->GetString(0, src);
+
+ RefPtr<nsVariant> result = new nsVariant();
+
+ if (src.Length()>1) {
+ src.Truncate(src.Length() - 1);
+ nsAutoString dest;
+ ReverseString(src, dest);
+ result->SetAsAString(dest);
+ }
+ else {
+ result->SetAsAString(EmptyString());
+ }
+ result.forget(_result);
+ return NS_OK;
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+//// Fixup URL Function
+
+ /* static */
+ nsresult
+ FixupURLFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<FixupURLFunction> function = new FixupURLFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("fixup_url"), 1, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMPL_ISUPPORTS(
+ FixupURLFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ FixupURLFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+ {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ nsAutoString src;
+ aArguments->GetString(0, src);
+
+ RefPtr<nsVariant> result = new nsVariant();
+
+ if (StringBeginsWith(src, NS_LITERAL_STRING("http://")))
+ src.Cut(0, 7);
+ else if (StringBeginsWith(src, NS_LITERAL_STRING("https://")))
+ src.Cut(0, 8);
+ else if (StringBeginsWith(src, NS_LITERAL_STRING("ftp://")))
+ src.Cut(0, 6);
+
+ // Remove common URL hostname prefixes
+ if (StringBeginsWith(src, NS_LITERAL_STRING("www."))) {
+ src.Cut(0, 4);
+ }
+
+ result->SetAsAString(src);
+ result.forget(_result);
+ return NS_OK;
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+//// Frecency Changed Notification Function
+
+ /* static */
+ nsresult
+ FrecencyNotificationFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<FrecencyNotificationFunction> function =
+ new FrecencyNotificationFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("notify_frecency"), 5, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMPL_ISUPPORTS(
+ FrecencyNotificationFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ FrecencyNotificationFunction::OnFunctionCall(mozIStorageValueArray *aArgs,
+ nsIVariant **_result)
+ {
+ uint32_t numArgs;
+ nsresult rv = aArgs->GetNumEntries(&numArgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numArgs == 5);
+
+ int32_t newFrecency = aArgs->AsInt32(0);
+
+ nsAutoCString spec;
+ rv = aArgs->GetUTF8String(1, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString guid;
+ rv = aArgs->GetUTF8String(2, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hidden = static_cast<bool>(aArgs->AsInt32(3));
+ PRTime lastVisitDate = static_cast<PRTime>(aArgs->AsInt64(4));
+
+ const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
+ NS_ENSURE_STATE(navHistory);
+ navHistory->DispatchFrecencyChangedNotification(spec, newFrecency, guid,
+ hidden, lastVisitDate);
+
+ RefPtr<nsVariant> result = new nsVariant();
+ rv = result->SetAsInt32(newFrecency);
+ NS_ENSURE_SUCCESS(rv, rv);
+ result.forget(_result);
+ return NS_OK;
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+//// Store Last Inserted Id Function
+
+ /* static */
+ nsresult
+ StoreLastInsertedIdFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<StoreLastInsertedIdFunction> function =
+ new StoreLastInsertedIdFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("store_last_inserted_id"), 2, function
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ NS_IMPL_ISUPPORTS(
+ StoreLastInsertedIdFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ StoreLastInsertedIdFunction::OnFunctionCall(mozIStorageValueArray *aArgs,
+ nsIVariant **_result)
+ {
+ uint32_t numArgs;
+ nsresult rv = aArgs->GetNumEntries(&numArgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numArgs == 2);
+
+ nsAutoCString table;
+ rv = aArgs->GetUTF8String(0, table);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t lastInsertedId = aArgs->AsInt64(1);
+
+ MOZ_ASSERT(table.EqualsLiteral("moz_places") ||
+ table.EqualsLiteral("moz_historyvisits") ||
+ table.EqualsLiteral("moz_bookmarks"));
+
+ if (table.EqualsLiteral("moz_bookmarks")) {
+ nsNavBookmarks::StoreLastInsertedId(table, lastInsertedId);
+ } else {
+ nsNavHistory::StoreLastInsertedId(table, lastInsertedId);
+ }
+
+ RefPtr<nsVariant> result = new nsVariant();
+ rv = result->SetAsInt64(lastInsertedId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ result.forget(_result);
+ return NS_OK;
+ }
+
+////////////////////////////////////////////////////////////////////////////////
+//// Hash Function
+
+ /* static */
+ nsresult
+ HashFunction::create(mozIStorageConnection *aDBConn)
+ {
+ RefPtr<HashFunction> function = new HashFunction();
+ return aDBConn->CreateFunction(
+ NS_LITERAL_CSTRING("hash"), -1, function
+ );
+ }
+
+ NS_IMPL_ISUPPORTS(
+ HashFunction,
+ mozIStorageFunction
+ )
+
+ NS_IMETHODIMP
+ HashFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+ nsIVariant **_result)
+ {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ // Fetch arguments. Use default values if they were omitted.
+ uint32_t numEntries;
+ nsresult rv = aArguments->GetNumEntries(&numEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(numEntries >= 1 && numEntries <= 2, NS_ERROR_FAILURE);
+
+ nsString str;
+ aArguments->GetString(0, str);
+ nsAutoCString mode;
+ if (numEntries > 1) {
+ aArguments->GetUTF8String(1, mode);
+ }
+
+ RefPtr<nsVariant> result = new nsVariant();
+ if (mode.IsEmpty()) {
+ // URI-like strings (having a prefix before a colon), are handled specially,
+ // as a 48 bit hash, where first 16 bits are the prefix hash, while the
+ // other 32 are the string hash.
+ // The 16 bits have been decided based on the fact hashing all of the IANA
+ // known schemes, plus "places", does not generate collisions.
+ nsAString::const_iterator start, tip, end;
+ str.BeginReading(tip);
+ start = tip;
+ str.EndReading(end);
+ if (FindInReadable(NS_LITERAL_STRING(":"), tip, end)) {
+ const nsDependentSubstring& prefix = Substring(start, tip);
+ uint64_t prefixHash = static_cast<uint64_t>(HashString(prefix) & 0x0000FFFF);
+ // The second half of the url is more likely to be unique, so we add it.
+ uint32_t srcHash = HashString(str);
+ uint64_t hash = (prefixHash << 32) + srcHash;
+ result->SetAsInt64(hash);
+ } else {
+ uint32_t hash = HashString(str);
+ result->SetAsInt64(hash);
+ }
+ } else if (mode.Equals(NS_LITERAL_CSTRING("prefix_lo"))) {
+ // Keep only 16 bits.
+ uint64_t hash = static_cast<uint64_t>(HashString(str) & 0x0000FFFF) << 32;
+ result->SetAsInt64(hash);
+ } else if (mode.Equals(NS_LITERAL_CSTRING("prefix_hi"))) {
+ // Keep only 16 bits.
+ uint64_t hash = static_cast<uint64_t>(HashString(str) & 0x0000FFFF) << 32;
+ // Make this a prefix upper bound by filling the lowest 32 bits.
+ hash += 0xFFFFFFFF;
+ result->SetAsInt64(hash);
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+
+ result.forget(_result);
+ return NS_OK;
+ }
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/SQLFunctions.h b/toolkit/components/places/SQLFunctions.h
new file mode 100644
index 0000000000..bba1593456
--- /dev/null
+++ b/toolkit/components/places/SQLFunctions.h
@@ -0,0 +1,394 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozilla_places_SQLFunctions_h_
+#define mozilla_places_SQLFunctions_h_
+
+/**
+ * This file contains functions that Places adds to the database handle that can
+ * be accessed by SQL queries.
+ *
+ * Keep the GUID-related parts of this file in sync with
+ * toolkit/downloads/SQLFunctions.[h|cpp]!
+ */
+
+#include "mozIStorageFunction.h"
+#include "mozilla/Attributes.h"
+
+class mozIStorageConnection;
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// AutoComplete Matching Function
+
+/**
+ * This function is used to determine if a given set of data should match an
+ * AutoComplete query.
+ *
+ * In SQL, you'd use it in the WHERE clause like so:
+ * WHERE AUTOCOMPLETE_MATCH(aSearchString, aURL, aTitle, aTags, aVisitCount,
+ * aTyped, aBookmark, aOpenPageCount, aMatchBehavior,
+ * aSearchBehavior)
+ *
+ * @param aSearchString
+ * The string to compare against.
+ * @param aURL
+ * The URL to test for an AutoComplete match.
+ * @param aTitle
+ * The title to test for an AutoComplete match.
+ * @param aTags
+ * The tags to test for an AutoComplete match.
+ * @param aVisitCount
+ * The number of visits aURL has.
+ * @param aTyped
+ * Indicates if aURL is a typed URL or not. Treated as a boolean.
+ * @param aBookmark
+ * Indicates if aURL is a bookmark or not. Treated as a boolean.
+ * @param aOpenPageCount
+ * The number of times aURL has been registered as being open. (See
+ * mozIPlacesAutoComplete::registerOpenPage.)
+ * @param aMatchBehavior
+ * The match behavior to use for this search.
+ * @param aSearchBehavior
+ * A bitfield dictating the search behavior.
+ */
+class MatchAutoCompleteFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+
+private:
+ ~MatchAutoCompleteFunction() {}
+
+ /**
+ * Argument Indexes
+ */
+ static const uint32_t kArgSearchString = 0;
+ static const uint32_t kArgIndexURL = 1;
+ static const uint32_t kArgIndexTitle = 2;
+ static const uint32_t kArgIndexTags = 3;
+ static const uint32_t kArgIndexVisitCount = 4;
+ static const uint32_t kArgIndexTyped = 5;
+ static const uint32_t kArgIndexBookmark = 6;
+ static const uint32_t kArgIndexOpenPageCount = 7;
+ static const uint32_t kArgIndexMatchBehavior = 8;
+ static const uint32_t kArgIndexSearchBehavior = 9;
+ static const uint32_t kArgIndexLength = 10;
+
+ /**
+ * Typedefs
+ */
+ typedef bool (*searchFunctionPtr)(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString);
+
+ typedef nsACString::const_char_iterator const_char_iterator;
+
+ /**
+ * Obtains the search function to match on.
+ *
+ * @param aBehavior
+ * The matching behavior to use defined by one of the
+ * mozIPlacesAutoComplete::MATCH_* values.
+ * @return a pointer to the function that will perform the proper search.
+ */
+ static searchFunctionPtr getSearchFunction(int32_t aBehavior);
+
+ /**
+ * Tests if aSourceString starts with aToken.
+ *
+ * @param aToken
+ * The string to search for.
+ * @param aSourceString
+ * The string to search.
+ * @return true if found, false otherwise.
+ */
+ static bool findBeginning(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString);
+
+ /**
+ * Tests if aSourceString starts with aToken in a case sensitive way.
+ *
+ * @param aToken
+ * The string to search for.
+ * @param aSourceString
+ * The string to search.
+ * @return true if found, false otherwise.
+ */
+ static bool findBeginningCaseSensitive(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString);
+
+ /**
+ * Searches aSourceString for aToken anywhere in the string in a case-
+ * insensitive way.
+ *
+ * @param aToken
+ * The string to search for.
+ * @param aSourceString
+ * The string to search.
+ * @return true if found, false otherwise.
+ */
+ static bool findAnywhere(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString);
+
+ /**
+ * Tests if aToken is found on a word boundary in aSourceString.
+ *
+ * @param aToken
+ * The string to search for.
+ * @param aSourceString
+ * The string to search.
+ * @return true if found, false otherwise.
+ */
+ static bool findOnBoundary(const nsDependentCSubstring &aToken,
+ const nsACString &aSourceString);
+
+
+ /**
+ * Fixes a URI's spec such that it is ready to be searched. This includes
+ * unescaping escaped characters and removing certain specs that we do not
+ * care to search for.
+ *
+ * @param aURISpec
+ * The spec of the URI to prepare for searching.
+ * @param aMatchBehavior
+ * The matching behavior to use defined by one of the
+ * mozIPlacesAutoComplete::MATCH_* values.
+ * @param aSpecBuf
+ * A string buffer that the returned slice can point into, if needed.
+ * @return the fixed up string.
+ */
+ static nsDependentCSubstring fixupURISpec(const nsACString &aURISpec,
+ int32_t aMatchBehavior,
+ nsACString &aSpecBuf);
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Frecency Calculation Function
+
+/**
+ * This function is used to calculate frecency for a page.
+ *
+ * In SQL, you'd use it in when setting frecency like:
+ * SET frecency = CALCULATE_FRECENCY(place_id).
+ * Optional parameters must be passed in if the page is not yet in the database,
+ * otherwise they will be fetched from it automatically.
+ *
+ * @param pageId
+ * The id of the page. Pass -1 if the page is being added right now.
+ * @param [optional] typed
+ * Whether the page has been typed in. Default is false.
+ * @param [optional] fullVisitCount
+ * Count of all the visits (All types). Default is 0.
+ * @param [optional] isBookmarked
+ * Whether the page is bookmarked. Default is false.
+ */
+class CalculateFrecencyFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~CalculateFrecencyFunction() {}
+};
+
+/**
+ * SQL function to generate a GUID for a place or bookmark item. This is just
+ * a wrapper around GenerateGUID in Helpers.h.
+ *
+ * @return a guid for the item.
+ */
+class GenerateGUIDFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~GenerateGUIDFunction() {}
+};
+
+/**
+ * SQL function to unreverse the rev_host of a page.
+ *
+ * @param rev_host
+ * The rev_host value of the page.
+ *
+ * @return the unreversed host of the page.
+ */
+class GetUnreversedHostFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~GetUnreversedHostFunction() {}
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Fixup URL Function
+
+/**
+ * Make a given URL more suitable for searches, by removing common prefixes
+ * such as "www."
+ *
+ * @param url
+ * A URL.
+ * @return
+ * The same URL, with redundant parts removed.
+ */
+class FixupURLFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~FixupURLFunction() {}
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Frecency Changed Notification Function
+
+/**
+ * For a given place, posts a runnable to the main thread that calls
+ * onFrecencyChanged on nsNavHistory's nsINavHistoryObservers. The passed-in
+ * newFrecency value is returned unchanged.
+ *
+ * @param newFrecency
+ * The place's new frecency.
+ * @param url
+ * The place's URL.
+ * @param guid
+ * The place's GUID.
+ * @param hidden
+ * The place's hidden boolean.
+ * @param lastVisitDate
+ * The place's last visit date.
+ * @return newFrecency
+ */
+class FrecencyNotificationFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~FrecencyNotificationFunction() {}
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Store Last Inserted Id Function
+
+/**
+ * Store the last inserted id for reference purpose.
+ *
+ * @param tableName
+ * The table name.
+ * @param id
+ * The last inserted id.
+ * @return null
+ */
+class StoreLastInsertedIdFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~StoreLastInsertedIdFunction() {}
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Hash Function
+
+/**
+ * Calculates hash for a given string using the mfbt AddToHash function.
+ *
+ * @param string
+ * A string.
+ * @return
+ * The hash for the string.
+ */
+class HashFunction final : public mozIStorageFunction
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ /**
+ * Registers the function with the specified database connection.
+ *
+ * @param aDBConn
+ * The database connection to register with.
+ */
+ static nsresult create(mozIStorageConnection *aDBConn);
+private:
+ ~HashFunction() {}
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_SQLFunctions_h_
diff --git a/toolkit/components/places/Shutdown.cpp b/toolkit/components/places/Shutdown.cpp
new file mode 100644
index 0000000000..43586542b9
--- /dev/null
+++ b/toolkit/components/places/Shutdown.cpp
@@ -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/. */
+
+#include "Shutdown.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+namespace places {
+
+uint16_t PlacesShutdownBlocker::sCounter = 0;
+Atomic<bool> PlacesShutdownBlocker::sIsStarted(false);
+
+PlacesShutdownBlocker::PlacesShutdownBlocker(const nsString& aName)
+ : mName(aName)
+ , mState(NOT_STARTED)
+ , mCounter(sCounter++)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // During tests, we can end up with the Database singleton being resurrected.
+ // Make sure that each instance of DatabaseShutdown has a unique name.
+ if (mCounter > 1) {
+ mName.AppendInt(mCounter);
+ }
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+PlacesShutdownBlocker::GetName(nsAString& aName)
+{
+ aName = mName;
+ return NS_OK;
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+PlacesShutdownBlocker::GetState(nsIPropertyBag** _state)
+{
+ NS_ENSURE_ARG_POINTER(_state);
+
+ nsCOMPtr<nsIWritablePropertyBag2> bag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+ NS_ENSURE_TRUE(bag, NS_ERROR_OUT_OF_MEMORY);
+ bag.forget(_state);
+
+ // Put `mState` in field `progress`
+ RefPtr<nsVariant> progress = new nsVariant();
+ nsresult rv = progress->SetAsUint8(mState);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ rv = static_cast<nsIWritablePropertyBag2*>(*_state)->SetPropertyAsInterface(
+ NS_LITERAL_STRING("progress"), progress);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+
+ // Put `mBarrier`'s state in field `barrier`, if possible
+ if (!mBarrier) {
+ return NS_OK;
+ }
+ nsCOMPtr<nsIPropertyBag> barrierState;
+ rv = mBarrier->GetState(getter_AddRefs(barrierState));
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ RefPtr<nsVariant> barrier = new nsVariant();
+ rv = barrier->SetAsInterface(NS_GET_IID(nsIPropertyBag), barrierState);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ rv = static_cast<nsIWritablePropertyBag2*>(*_state)->SetPropertyAsInterface(
+ NS_LITERAL_STRING("Barrier"), barrier);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+
+ return NS_OK;
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+PlacesShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
+{
+ MOZ_ASSERT(false, "should always be overridden");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(
+ PlacesShutdownBlocker,
+ nsIAsyncShutdownBlocker
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+ClientsShutdownBlocker::ClientsShutdownBlocker()
+ : PlacesShutdownBlocker(NS_LITERAL_STRING("Places Clients shutdown"))
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // Create a barrier that will be exposed to clients through GetClient(), so
+ // they can block Places shutdown.
+ nsCOMPtr<nsIAsyncShutdownService> asyncShutdown = services::GetAsyncShutdown();
+ MOZ_ASSERT(asyncShutdown);
+ if (asyncShutdown) {
+ nsCOMPtr<nsIAsyncShutdownBarrier> barrier;
+ MOZ_ALWAYS_SUCCEEDS(asyncShutdown->MakeBarrier(mName, getter_AddRefs(barrier)));
+ mBarrier = new nsMainThreadPtrHolder<nsIAsyncShutdownBarrier>(barrier);
+ }
+}
+
+already_AddRefed<nsIAsyncShutdownClient>
+ClientsShutdownBlocker::GetClient()
+{
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ if (mBarrier) {
+ MOZ_ALWAYS_SUCCEEDS(mBarrier->GetClient(getter_AddRefs(client)));
+ }
+ return client.forget();
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+ClientsShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mParentClient = new nsMainThreadPtrHolder<nsIAsyncShutdownClient>(aParentClient);
+ mState = RECEIVED_BLOCK_SHUTDOWN;
+
+ if (NS_WARN_IF(!mBarrier)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Wait until all the clients have removed their blockers.
+ MOZ_ALWAYS_SUCCEEDS(mBarrier->Wait(this));
+
+ mState = CALLED_WAIT_CLIENTS;
+ return NS_OK;
+}
+
+// nsIAsyncShutdownCompletionCallback
+NS_IMETHODIMP
+ClientsShutdownBlocker::Done()
+{
+ // At this point all the clients are done, we can stop blocking the shutdown
+ // phase.
+ mState = RECEIVED_DONE;
+
+ // mParentClient is nullptr in tests.
+ if (mParentClient) {
+ nsresult rv = mParentClient->RemoveBlocker(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ mParentClient = nullptr;
+ }
+ mBarrier = nullptr;
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(
+ ClientsShutdownBlocker,
+ PlacesShutdownBlocker,
+ nsIAsyncShutdownCompletionCallback
+)
+
+////////////////////////////////////////////////////////////////////////////////
+
+ConnectionShutdownBlocker::ConnectionShutdownBlocker(Database* aDatabase)
+ : PlacesShutdownBlocker(NS_LITERAL_STRING("Places Connection shutdown"))
+ , mDatabase(aDatabase)
+{
+ // Do nothing.
+}
+
+// nsIAsyncShutdownBlocker
+NS_IMETHODIMP
+ConnectionShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mParentClient = new nsMainThreadPtrHolder<nsIAsyncShutdownClient>(aParentClient);
+ mState = RECEIVED_BLOCK_SHUTDOWN;
+ // Annotate that Database shutdown started.
+ sIsStarted = true;
+
+ // Fire internal database closing notification.
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ MOZ_ASSERT(os);
+ if (os) {
+ Unused << os->NotifyObservers(nullptr, TOPIC_PLACES_WILL_CLOSE_CONNECTION, nullptr);
+ }
+ mState = NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION;
+
+ // At this stage, any use of this database is forbidden. Get rid of
+ // `gDatabase`. Note, however, that the database could be
+ // resurrected. This can happen in particular during tests.
+ MOZ_ASSERT(Database::gDatabase == nullptr || Database::gDatabase == mDatabase);
+ Database::gDatabase = nullptr;
+
+ // Database::Shutdown will invoke Complete once the connection is closed.
+ mDatabase->Shutdown();
+ mState = CALLED_STORAGESHUTDOWN;
+ return NS_OK;
+}
+
+// mozIStorageCompletionCallback
+NS_IMETHODIMP
+ConnectionShutdownBlocker::Complete(nsresult, nsISupports*)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mState = RECEIVED_STORAGESHUTDOWN_COMPLETE;
+
+ // The connection is closed, the Database has no more use, so we can break
+ // possible cycles.
+ mDatabase = nullptr;
+
+ // Notify the connection has gone.
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ MOZ_ASSERT(os);
+ if (os) {
+ MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(nullptr,
+ TOPIC_PLACES_CONNECTION_CLOSED,
+ nullptr));
+ }
+ mState = NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED;
+
+ // mParentClient is nullptr in tests
+ if (mParentClient) {
+ nsresult rv = mParentClient->RemoveBlocker(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) return rv;
+ mParentClient = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(
+ ConnectionShutdownBlocker,
+ PlacesShutdownBlocker,
+ mozIStorageCompletionCallback
+)
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/Shutdown.h b/toolkit/components/places/Shutdown.h
new file mode 100644
index 0000000000..69023c6089
--- /dev/null
+++ b/toolkit/components/places/Shutdown.h
@@ -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/. */
+
+#ifndef mozilla_places_Shutdown_h_
+#define mozilla_places_Shutdown_h_
+
+#include "nsIAsyncShutdown.h"
+#include "Database.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla {
+namespace places {
+
+class Database;
+
+/**
+ * This is most of the code responsible for Places shutdown.
+ *
+ * PHASE 1 (Legacy clients shutdown)
+ * The shutdown procedure begins when the Database singleton receives
+ * profile-change-teardown (note that tests will instead notify nsNavHistory,
+ * that forwards the notification to the Database instance).
+ * Database::Observe first of all checks if initialization was completed
+ * properly, to avoid race conditions, then it notifies "places-shutdown" to
+ * legacy clients. Legacy clients are supposed to start and complete any
+ * shutdown critical work in the same tick, since we won't wait for them.
+
+ * PHASE 2 (Modern clients shutdown)
+ * Modern clients should instead register as a blocker by passing a promise to
+ * nsPIPlacesDatabase::shutdownClient (for example see sanitize.js), so they
+ * block Places shutdown until the promise is resolved.
+ * When profile-change-teardown is observed by async shutdown, it calls
+ * ClientsShutdownBlocker::BlockShutdown. This class is registered as a teardown
+ * phase blocker in Database::Init (see Database::mClientsShutdown).
+ * ClientsShutdownBlocker::BlockShudown waits for all the clients registered
+ * through nsPIPlacesDatabase::shutdownClient. When all the clients are done,
+ * its `Done` method is invoked, and it stops blocking the shutdown phase, so
+ * that it can continue.
+ *
+ * PHASE 3 (Connection shutdown)
+ * ConnectionBlocker is registered as a profile-before-change blocker in
+ * Database::Init (see Database::mConnectionShutdown).
+ * When profile-before-change is observer by async shutdown, it calls
+ * ConnectionShutdownBlocker::BlockShutdown.
+ * This is the last chance for any Places internal work, like privacy cleanups,
+ * before the connection is closed. This a places-will-close-connection
+ * notification is sent to legacy clients that must complete any operation in
+ * the same tick, since we won't wait for them.
+ * Then the control is passed to Database::Shutdown, that executes some sanity
+ * checks, clears cached statements and proceeds with asyncClose.
+ * Once the connection is definitely closed, Database will call back
+ * ConnectionBlocker::Complete. At this point a final
+ * places-connection-closed notification is sent, for testing purposes.
+ */
+
+/**
+ * A base AsyncShutdown blocker in charge of shutting down Places.
+ */
+class PlacesShutdownBlocker : public nsIAsyncShutdownBlocker
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ explicit PlacesShutdownBlocker(const nsString& aName);
+
+ /**
+ * `true` if we have not started shutdown, i.e. if
+ * `BlockShutdown()` hasn't been called yet, false otherwise.
+ */
+ static bool IsStarted() {
+ return sIsStarted;
+ }
+
+ // The current state, used internally and for forensics/debugging purposes.
+ // Not all the states make sense for all the derived classes.
+ enum States {
+ NOT_STARTED,
+ // Execution of `BlockShutdown` in progress.
+ RECEIVED_BLOCK_SHUTDOWN,
+
+ // Values specific to ClientsShutdownBlocker
+ // a. Set while we are waiting for clients to do their job and unblock us.
+ CALLED_WAIT_CLIENTS,
+ // b. Set when all the clients are done.
+ RECEIVED_DONE,
+
+ // Values specific to ConnectionShutdownBlocker
+ // a. Set after we notified observers that Places is closing the connection.
+ NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION,
+ // b. Set after we pass control to Database::Shutdown, and wait for it to
+ // close the connection and call our `Complete` method when done.
+ CALLED_STORAGESHUTDOWN,
+ // c. Set when Database has closed the connection and passed control to
+ // us through `Complete`.
+ RECEIVED_STORAGESHUTDOWN_COMPLETE,
+ // d. We have notified observers that Places has closed the connection.
+ NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
+ };
+ States State() {
+ return mState;
+ }
+
+protected:
+ // The blocker name, also used as barrier name.
+ nsString mName;
+ // The current state, see States.
+ States mState;
+ // The barrier optionally used to wait for clients.
+ nsMainThreadPtrHandle<nsIAsyncShutdownBarrier> mBarrier;
+ // The parent object who registered this as a blocker.
+ nsMainThreadPtrHandle<nsIAsyncShutdownClient> mParentClient;
+
+ // As tests may resurrect a dead `Database`, we use a counter to
+ // give the instances of `PlacesShutdownBlocker` unique names.
+ uint16_t mCounter;
+ static uint16_t sCounter;
+
+ static Atomic<bool> sIsStarted;
+
+ virtual ~PlacesShutdownBlocker() {}
+};
+
+/**
+ * Blocker also used to wait for clients, through an owned barrier.
+ */
+class ClientsShutdownBlocker final : public PlacesShutdownBlocker
+ , public nsIAsyncShutdownCompletionCallback
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK
+
+ explicit ClientsShutdownBlocker();
+
+ NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aParentClient) override;
+
+ already_AddRefed<nsIAsyncShutdownClient> GetClient();
+
+private:
+ ~ClientsShutdownBlocker() {}
+};
+
+/**
+ * Blocker used to wait when closing the database connection.
+ */
+class ConnectionShutdownBlocker final : public PlacesShutdownBlocker
+ , public mozIStorageCompletionCallback
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
+
+ NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aParentClient) override;
+
+ explicit ConnectionShutdownBlocker(mozilla::places::Database* aDatabase);
+
+private:
+ ~ConnectionShutdownBlocker() {}
+
+ // The owning database.
+ // The cycle is broken in method Complete(), once the connection
+ // has been closed by mozStorage.
+ RefPtr<mozilla::places::Database> mDatabase;
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_Shutdown_h_
diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js
new file mode 100644
index 0000000000..ad3d35aab7
--- /dev/null
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -0,0 +1,2149 @@
+/* -*- 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/. */
+
+"use strict";
+
+// Constants
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+// 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;
+
+const PREF_BRANCH = "browser.urlbar.";
+
+// Prefs are defined as [pref name, default value].
+const PREF_ENABLED = [ "autocomplete.enabled", true ];
+const PREF_AUTOFILL = [ "autoFill", true ];
+const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ];
+const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", false ];
+const PREF_RESTYLESEARCHES = [ "restyleSearches", false ];
+const PREF_DELAY = [ "delay", 50 ];
+const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
+const PREF_FILTER_JS = [ "filter.javascript", true ];
+const PREF_MAXRESULTS = [ "maxRichResults", 25 ];
+const PREF_RESTRICT_HISTORY = [ "restrict.history", "^" ];
+const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark", "*" ];
+const PREF_RESTRICT_TYPED = [ "restrict.typed", "~" ];
+const PREF_RESTRICT_TAG = [ "restrict.tag", "+" ];
+const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage", "%" ];
+const PREF_RESTRICT_SEARCHES = [ "restrict.searces", "$" ];
+const PREF_MATCH_TITLE = [ "match.title", "#" ];
+const PREF_MATCH_URL = [ "match.url", "@" ];
+
+const PREF_SUGGEST_HISTORY = [ "suggest.history", true ];
+const PREF_SUGGEST_BOOKMARK = [ "suggest.bookmark", true ];
+const PREF_SUGGEST_OPENPAGE = [ "suggest.openpage", true ];
+const PREF_SUGGEST_HISTORY_ONLYTYPED = [ "suggest.history.onlyTyped", false ];
+const PREF_SUGGEST_SEARCHES = [ "suggest.searches", false ];
+
+const PREF_MAX_CHARS_FOR_SUGGEST = [ "maxCharsForSearchSuggestions", 20];
+
+// AutoComplete query type constants.
+// Describes the various types of queries that we can process rows for.
+const QUERYTYPE_FILTERED = 0;
+const QUERYTYPE_AUTOFILL_HOST = 1;
+const QUERYTYPE_AUTOFILL_URL = 2;
+
+// 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 TITLE_TAGS_SEPARATOR = " \u2013 ";
+
+// Telemetry probes.
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
+// The default frecency value used when inserting matches with unknown frecency.
+const FRECENCY_DEFAULT = 1000;
+
+// Remote matches are appended when local matches are below a given frecency
+// threshold (FRECENCY_DEFAULT) as soon as they arrive. However we'll
+// always try to have at least MINIMUM_LOCAL_MATCHES local matches.
+const MINIMUM_LOCAL_MATCHES = 6;
+
+// Extensions are allowed to add suggestions if they have registered a keyword
+// with the omnibox API. This is the maximum number of suggestions an extension
+// is allowed to add for a given search string.
+const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
+
+// A regex that matches "single word" hostnames for whitelisting purposes.
+// The hostname will already have been checked for general validity, so we
+// don't need to be exhaustive here, so allow dashes anywhere.
+const REGEXP_SINGLEWORD_HOST = new RegExp("^[a-z0-9-]+$", "i");
+
+// Regex used to match userContextId.
+const REGEXP_USER_CONTEXT_ID = /(?:^| )user-context-id:(\d+)/;
+
+// Regex used to match one or more whitespace.
+const REGEXP_SPACES = /\s+/;
+
+// Sqlite result row index constants.
+const QUERYINDEX_QUERYTYPE = 0;
+const QUERYINDEX_URL = 1;
+const QUERYINDEX_TITLE = 2;
+const QUERYINDEX_ICONURL = 3;
+const QUERYINDEX_BOOKMARKED = 4;
+const QUERYINDEX_BOOKMARKTITLE = 5;
+const QUERYINDEX_TAGS = 6;
+const QUERYINDEX_VISITCOUNT = 7;
+const QUERYINDEX_TYPED = 8;
+const QUERYINDEX_PLACEID = 9;
+const QUERYINDEX_SWITCHTAB = 10;
+const QUERYINDEX_FRECENCY = 11;
+
+// This SQL query fragment provides the following:
+// - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
+// - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
+// - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
+const SQL_BOOKMARK_TAGS_FRAGMENT =
+ `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`;
+
+// TODO bug 412736: in case of a frecency tie, we might break it with h.typed
+// and h.visit_count. That is slower though, so not doing it yet...
+// NB: as a slight performance optimization, we only evaluate the "btitle"
+// and "tags" queries for bookmarked entries.
+function defaultQuery(conditions = "") {
+ let query =
+ `SELECT :query_type, h.url, h.title, f.url, ${SQL_BOOKMARK_TAGS_FRAGMENT},
+ h.visit_count, h.typed, h.id, t.open_count, h.frecency
+ FROM moz_places h
+ LEFT JOIN moz_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t
+ ON t.url = h.url
+ AND t.userContextId = :userContextId
+ WHERE h.frecency <> 0
+ AND AUTOCOMPLETE_MATCH(:searchString, h.url,
+ CASE WHEN bookmarked THEN
+ IFNULL(btitle, h.title)
+ ELSE h.title END,
+ CASE WHEN bookmarked THEN
+ tags
+ ELSE '' END,
+ h.visit_count, h.typed,
+ bookmarked, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ${conditions}
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT :maxResults`;
+ return query;
+}
+
+const SQL_SWITCHTAB_QUERY =
+ `SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ 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 t.userContextId = :userContextId
+ AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
+ NULL, NULL, NULL, t.open_count,
+ :matchBehavior, :searchBehavior)
+ ORDER BY t.ROWID DESC
+ LIMIT :maxResults`;
+
+const SQL_ADAPTIVE_QUERY =
+ `/* do not warn (bug 487789) */
+ SELECT :query_type, h.url, h.title, f.url, ${SQL_BOOKMARK_TAGS_FRAGMENT},
+ h.visit_count, h.typed, h.id, t.open_count, h.frecency
+ 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_favicons f ON f.id = h.favicon_id
+ LEFT JOIN moz_openpages_temp t
+ ON t.url = h.url
+ AND t.userContextId = :userContextId
+ 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`;
+
+
+function hostQuery(conditions = "") {
+ let query =
+ `/* do not warn (bug NA): not worth to index on (typed, frecency) */
+ SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
+ ( SELECT f.url FROM moz_favicons f
+ JOIN moz_places h ON h.favicon_id = f.id
+ WHERE rev_host = get_unreversed_host(host || '.') || '.'
+ OR rev_host = get_unreversed_host(host || '.') || '.www.'
+ ) AS favicon_url,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency
+ FROM moz_hosts
+ WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
+ AND frecency <> 0
+ ${conditions}
+ ORDER BY frecency DESC
+ LIMIT 1`;
+ return query;
+}
+
+const SQL_HOST_QUERY = hostQuery();
+
+const SQL_TYPED_HOST_QUERY = hostQuery("AND typed = 1");
+
+function bookmarkedHostQuery(conditions = "") {
+ let query =
+ `/* do not warn (bug NA): not worth to index on (typed, frecency) */
+ SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
+ ( SELECT f.url FROM moz_favicons f
+ JOIN moz_places h ON h.favicon_id = f.id
+ WHERE rev_host = get_unreversed_host(host || '.') || '.'
+ OR rev_host = get_unreversed_host(host || '.') || '.www.'
+ ) AS favicon_url,
+ ( SELECT foreign_count > 0 FROM moz_places
+ WHERE rev_host = get_unreversed_host(host || '.') || '.'
+ OR rev_host = get_unreversed_host(host || '.') || '.www.'
+ ) AS bookmarked, NULL, NULL, NULL, NULL, NULL, NULL, frecency
+ FROM moz_hosts
+ WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
+ AND bookmarked
+ AND frecency <> 0
+ ${conditions}
+ ORDER BY frecency DESC
+ LIMIT 1`;
+ return query;
+}
+
+const SQL_BOOKMARKED_HOST_QUERY = bookmarkedHostQuery();
+
+const SQL_BOOKMARKED_TYPED_HOST_QUERY = bookmarkedHostQuery("AND typed = 1");
+
+function urlQuery(conditions = "") {
+ return `/* do not warn (bug no): cannot use an index to sort */
+ SELECT :query_type, h.url, NULL, f.url AS favicon_url,
+ foreign_count > 0 AS bookmarked,
+ NULL, NULL, NULL, NULL, NULL, NULL, h.frecency
+ FROM moz_places h
+ LEFT JOIN moz_favicons f ON h.favicon_id = f.id
+ WHERE (rev_host = :revHost OR rev_host = :revHost || "www.")
+ AND h.frecency <> 0
+ AND fixup_url(h.url) BETWEEN :searchString AND :searchString || X'FFFF'
+ ${conditions}
+ ORDER BY h.frecency DESC, h.id DESC
+ LIMIT 1`;
+}
+
+const SQL_URL_QUERY = urlQuery();
+
+const SQL_TYPED_URL_QUERY = urlQuery("AND h.typed = 1");
+
+// TODO (bug 1045924): use foreign_count once available.
+const SQL_BOOKMARKED_URL_QUERY = urlQuery("AND bookmarked");
+
+const SQL_BOOKMARKED_TYPED_URL_QUERY = urlQuery("AND bookmarked AND h.typed = 1");
+
+// Getters
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
+ "resource://gre/modules/ExtensionSearchHandler.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
+ "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
+ "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
+ "@mozilla.org/intl/texttosuburi;1",
+ "nsITextToSubURI");
+
+/**
+ * Storage object for switch-to-tab entries.
+ * This takes care of caching and registering open pages, that will be reused
+ * by switch-to-tab queries. It has an internal cache, so that the Sqlite
+ * store is lazy initialized only on first use.
+ * It has a simple API:
+ * initDatabase(conn): initializes the temporary Sqlite entities to store data
+ * add(uri): adds a given nsIURI to the store
+ * delete(uri): removes a given nsIURI from the store
+ * shutdown(): stops storing data to Sqlite
+ */
+XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
+ _conn: null,
+ // Temporary queue used while the database connection is not available.
+ _queue: new Map(),
+ initDatabase: Task.async(function* (conn) {
+ // To reduce IO use an in-memory table for switch-to-tab tracking.
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTables.h.
+ yield conn.execute(
+ `CREATE TEMP TABLE moz_openpages_temp (
+ url TEXT,
+ userContextId INTEGER,
+ open_count INTEGER,
+ PRIMARY KEY (url, userContextId)
+ )`);
+
+ // Note: this should be kept up-to-date with the definition in
+ // nsPlacesTriggers.h.
+ yield conn.execute(
+ `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
+ AND userContextId = NEW.userContextId;
+ END`);
+
+ this._conn = conn;
+
+ // Populate the table with the current cache contents...
+ for (let [userContextId, uris] of this._queue) {
+ for (let uri of uris) {
+ this.add(uri, userContextId);
+ }
+ }
+
+ // ...then clear it to avoid double additions.
+ this._queue.clear();
+ }),
+
+ add(uri, userContextId) {
+ if (!this._conn) {
+ if (!this._queue.has(userContextId)) {
+ this._queue.set(userContextId, new Set());
+ }
+ this._queue.get(userContextId).add(uri);
+ return;
+ }
+ this._conn.executeCached(
+ `INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
+ VALUES ( :url,
+ :userContextId,
+ IFNULL( ( SELECT open_count + 1
+ FROM moz_openpages_temp
+ WHERE url = :url
+ AND userContextId = :userContextId ),
+ 1
+ )
+ )`
+ , { url: uri.spec, userContextId });
+ },
+
+ delete(uri, userContextId) {
+ if (!this._conn) {
+ // This should not happen.
+ if (!this._queue.has(userContextId)) {
+ throw new Error("Unknown userContextId!");
+ }
+
+ this._queue.get(userContextId).delete(uri);
+ if (this._queue.get(userContextId).size == 0) {
+ this._queue.delete(userContextId);
+ }
+ return;
+ }
+ this._conn.executeCached(
+ `UPDATE moz_openpages_temp
+ SET open_count = open_count - 1
+ WHERE url = :url
+ AND userContextId = :userContextId`
+ , { url: uri.spec, userContextId });
+ },
+
+ shutdown: function () {
+ this._conn = null;
+ this._queue.clear();
+ }
+}));
+
+/**
+ * This helper keeps track of preferences and keeps their values up-to-date.
+ */
+XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
+ let prefs = new Preferences(PREF_BRANCH);
+ let types = ["History", "Bookmark", "Openpage", "Searches"];
+
+ function syncEnabledPref() {
+ loadSyncedPrefs();
+
+ let suggestPrefs = [
+ PREF_SUGGEST_HISTORY,
+ PREF_SUGGEST_BOOKMARK,
+ PREF_SUGGEST_OPENPAGE,
+ PREF_SUGGEST_SEARCHES,
+ ];
+
+ if (store.enabled) {
+ // If the autocomplete preference is active, set to default value all suggest
+ // preferences only if all of them are false.
+ if (types.every(type => store["suggest" + type] == false)) {
+ for (let type of suggestPrefs) {
+ prefs.set(...type);
+ }
+ }
+ } else {
+ // If the preference was deactivated, deactivate all suggest preferences.
+ for (let type of suggestPrefs) {
+ prefs.set(type[0], false);
+ }
+ }
+ }
+
+ function loadSyncedPrefs () {
+ store.enabled = prefs.get(...PREF_ENABLED);
+ store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
+ store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
+ store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
+ store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
+ store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
+ }
+
+ function loadPrefs(subject, topic, data) {
+ if (data) {
+ // Synchronize suggest.* prefs with autocomplete.enabled.
+ if (data == PREF_BRANCH + PREF_ENABLED[0]) {
+ syncEnabledPref();
+ } else if (data.startsWith(PREF_BRANCH + "suggest.")) {
+ loadSyncedPrefs();
+ prefs.set(PREF_ENABLED[0], types.some(type => store["suggest" + type]));
+ }
+ }
+
+ store.enabled = prefs.get(...PREF_ENABLED);
+ store.autofill = prefs.get(...PREF_AUTOFILL);
+ store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
+ store.autofillSearchEngines = prefs.get(...PREF_AUTOFILL_SEARCHENGINES);
+ store.restyleSearches = prefs.get(...PREF_RESTYLESEARCHES);
+ store.delay = prefs.get(...PREF_DELAY);
+ store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
+ store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
+ store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
+ store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
+ store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
+ store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
+ store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
+ store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
+ store.restrictSearchesToken = prefs.get(...PREF_RESTRICT_SEARCHES);
+ store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
+ store.matchURLToken = prefs.get(...PREF_MATCH_URL);
+ store.suggestHistory = prefs.get(...PREF_SUGGEST_HISTORY);
+ store.suggestBookmark = prefs.get(...PREF_SUGGEST_BOOKMARK);
+ store.suggestOpenpage = prefs.get(...PREF_SUGGEST_OPENPAGE);
+ store.suggestTyped = prefs.get(...PREF_SUGGEST_HISTORY_ONLYTYPED);
+ store.suggestSearches = prefs.get(...PREF_SUGGEST_SEARCHES);
+ store.maxCharsForSearchSuggestions = prefs.get(...PREF_MAX_CHARS_FOR_SUGGEST);
+ store.keywordEnabled = true;
+ try {
+ store.keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
+ } catch (ex) {}
+
+ // If history is not set, onlyTyped value should be ignored.
+ if (!store.suggestHistory) {
+ store.suggestTyped = false;
+ }
+ store.defaultBehavior = types.concat("Typed").reduce((memo, type) => {
+ let prefValue = store["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.
+ store.emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
+ if (store.suggestHistory) {
+ store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+ Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
+ } else if (store.suggestBookmark) {
+ store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
+ } else {
+ store.emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
+ }
+
+ // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+ if (store.matchBehavior != MATCH_ANYWHERE &&
+ store.matchBehavior != MATCH_BOUNDARY &&
+ store.matchBehavior != MATCH_BEGINNING) {
+ store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
+ }
+
+ store.tokenToBehaviorMap = new Map([
+ [ store.restrictHistoryToken, "history" ],
+ [ store.restrictBookmarkToken, "bookmark" ],
+ [ store.restrictTagToken, "tag" ],
+ [ store.restrictOpenPageToken, "openpage" ],
+ [ store.matchTitleToken, "title" ],
+ [ store.matchURLToken, "url" ],
+ [ store.restrictTypedToken, "typed" ],
+ [ store.restrictSearchesToken, "searches" ],
+ ]);
+ }
+
+ let store = {
+ _ignoreNotifications: false,
+ observe(subject, topic, data) {
+ // Avoid re-entrancy when flipping linked preferences.
+ if (this._ignoreNotifications)
+ return;
+ this._ignoreNotifications = true;
+ loadPrefs(subject, topic, data);
+ this._ignoreNotifications = false;
+ },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference ])
+ };
+
+ // Synchronize suggest.* prefs with autocomplete.enabled at initialization
+ syncEnabledPref();
+
+ loadPrefs();
+ prefs.observe("", store);
+ Services.prefs.addObserver("keyword.enabled", store, true);
+
+ return Object.seal(store);
+});
+
+// Helper functions
+
+/**
+ * Used to unescape encoded URI strings and drop information that we do not
+ * care about.
+ *
+ * @param spec
+ * The text to unescape and modify.
+ * @return the modified spec.
+ */
+function fixupSearchText(spec) {
+ return textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec));
+}
+
+/**
+ * Generates the tokens used in searching from a given string.
+ *
+ * @param searchString
+ * The string to generate tokens from.
+ * @return an array of tokens.
+ * @note 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.
+ */
+function getUnfilteredSearchTokens(searchString) {
+ return searchString.length ? searchString.split(REGEXP_SPACES) : [];
+}
+
+/**
+ * Strip prefixes from the URI that we don't care about for searching.
+ *
+ * @param spec
+ * The text to modify.
+ * @return the modified spec.
+ */
+function stripPrefix(spec)
+{
+ ["http://", "https://", "ftp://"].some(scheme => {
+ // Strip protocol if not directly followed by a space
+ if (spec.startsWith(scheme) && spec[scheme.length] != " ") {
+ spec = spec.slice(scheme.length);
+ return true;
+ }
+ return false;
+ });
+
+ // Strip www. if not directly followed by a space
+ if (spec.startsWith("www.") && spec[4] != " ") {
+ spec = spec.slice(4);
+ }
+ return spec;
+}
+
+/**
+ * Strip http and trailing separators from a spec.
+ *
+ * @param spec
+ * The text to modify.
+ * @return the modified spec.
+ */
+function stripHttpAndTrim(spec) {
+ if (spec.startsWith("http://")) {
+ spec = spec.slice(7);
+ }
+ if (spec.endsWith("?")) {
+ spec = spec.slice(0, -1);
+ }
+ if (spec.endsWith("/")) {
+ spec = spec.slice(0, -1);
+ }
+ return spec;
+}
+
+/**
+ * Returns the key to be used for a URL in a map for the purposes of removing
+ * duplicate entries - any 2 URLs that should be considered the same should
+ * return the same key. For some moz-action URLs this will unwrap the params
+ * and return a key based on the wrapped URL.
+ */
+function makeKeyForURL(actionUrl) {
+ // At this stage we only consider moz-action URLs.
+ if (!actionUrl.startsWith("moz-action:")) {
+ return stripHttpAndTrim(actionUrl);
+ }
+ let [, type, params] = actionUrl.match(/^moz-action:([^,]+),(.*)$/);
+ try {
+ params = JSON.parse(params);
+ } catch (ex) {
+ // This is unexpected in this context, so just return the input.
+ return stripHttpAndTrim(actionUrl);
+ }
+ // For now we only handle these 2 action types and treat them as the same.
+ switch (type) {
+ case "remotetab":
+ case "switchtab":
+ if (params.url) {
+ return "moz-action:tab:" + stripHttpAndTrim(params.url);
+ }
+ break;
+ // TODO (bug 1222435) - "switchtab" should be handled as an "autofill"
+ // entry.
+ default:
+ // do nothing.
+ // TODO (bug 1222436) - extend this method so it can be used instead of
+ // the |placeId| that's also used to remove duplicate entries.
+ }
+ return stripHttpAndTrim(actionUrl);
+}
+
+/**
+ * Returns whether the passed in string looks like a url.
+ */
+function looksLikeUrl(str, ignoreAlphanumericHosts = false) {
+ // Single word not including special chars.
+ return !REGEXP_SPACES.test(str) &&
+ (["/", "@", ":", "["].some(c => str.includes(c)) ||
+ (ignoreAlphanumericHosts ? /(.*\..*){3,}/.test(str) : str.includes(".")));
+}
+
+/**
+ * Manages a single instance of an autocomplete search.
+ *
+ * The first three parameters all originate from the similarly named parameters
+ * of nsIAutoCompleteSearch.startSearch().
+ *
+ * @param searchString
+ * The search string.
+ * @param searchParam
+ * A space-delimited string of search parameters. The following
+ * parameters are supported:
+ * * enable-actions: Include "actions", such as switch-to-tab and search
+ * engine aliases, in the results.
+ * * disable-private-actions: The search is taking place in a private
+ * window outside of permanent private-browsing mode. The search
+ * should exclude privacy-sensitive results as appropriate.
+ * * private-window: The search is taking place in a private window,
+ * possibly in permanent private-browsing mode. The search
+ * should exclude privacy-sensitive results as appropriate.
+ * * user-context-id: The userContextId of the selected tab.
+ * @param autocompleteListener
+ * An nsIAutoCompleteObserver.
+ * @param resultListener
+ * An nsIAutoCompleteSimpleResultListener.
+ * @param autocompleteSearch
+ * An nsIAutoCompleteSearch.
+ * @param prohibitSearchSuggestions
+ * Whether search suggestions are allowed for this search.
+ */
+function Search(searchString, searchParam, autocompleteListener,
+ resultListener, autocompleteSearch, prohibitSearchSuggestions) {
+ // We want to store the original string for case sensitive searches.
+ this._originalSearchString = searchString;
+ this._trimmedOriginalSearchString = searchString.trim();
+ this._searchString = fixupSearchText(this._trimmedOriginalSearchString.toLowerCase());
+
+ this._matchBehavior = Prefs.matchBehavior;
+ // Set the default behavior for this search.
+ this._behavior = this._searchString ? Prefs.defaultBehavior
+ : Prefs.emptySearchDefaultBehavior;
+
+ let params = new Set(searchParam.split(" "));
+ this._enableActions = params.has("enable-actions");
+ this._disablePrivateActions = params.has("disable-private-actions");
+ this._inPrivateWindow = params.has("private-window");
+ this._prohibitAutoFill = params.has("prohibit-autofill");
+
+ let userContextId = searchParam.match(REGEXP_USER_CONTEXT_ID);
+ this._userContextId = userContextId ?
+ parseInt(userContextId[1], 10) :
+ Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+
+ this._searchTokens =
+ this.filterTokens(getUnfilteredSearchTokens(this._searchString));
+ // The protocol and the host are lowercased by nsIURI, so it's fine to
+ // lowercase the typed prefix, to add it back to the results later.
+ this._strippedPrefix = this._trimmedOriginalSearchString.slice(
+ 0, this._trimmedOriginalSearchString.length - this._searchString.length
+ ).toLowerCase();
+ // The URIs in the database are fixed-up, so we can match on a lowercased
+ // host, but the path must be matched in a case sensitive way.
+ let pathIndex =
+ this._trimmedOriginalSearchString.indexOf("/", this._strippedPrefix.length);
+ this._autofillUrlSearchString = fixupSearchText(
+ this._trimmedOriginalSearchString.slice(0, pathIndex).toLowerCase() +
+ this._trimmedOriginalSearchString.slice(pathIndex)
+ );
+
+ this._prohibitSearchSuggestions = prohibitSearchSuggestions;
+
+ this._listener = autocompleteListener;
+ this._autocompleteSearch = autocompleteSearch;
+
+ // Create a new result to add eventual matches. Note we need a result
+ // regardless having matches.
+ let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
+ .createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.setSearchString(searchString);
+ result.setListener(resultListener);
+ // Will be set later, if needed.
+ result.setDefaultIndex(-1);
+ this._result = result;
+
+ // These are used to avoid adding duplicate entries to the results.
+ this._usedURLs = new Set();
+ this._usedPlaceIds = new Set();
+
+ // Resolved when all the remote matches have been fetched.
+ this._remoteMatchesPromises = [];
+
+ // The index to insert remote matches at.
+ this._remoteMatchesStartIndex = 0;
+ // The index to insert local matches at.
+
+ this._localMatchesStartIndex = 0;
+
+ // Counts the number of inserted local matches.
+ this._localMatchesCount = 0;
+ // Counts the number of inserted remote matches.
+ this._remoteMatchesCount = 0;
+ // Counts the number of inserted extension matches.
+ this._extensionMatchesCount = 0;
+}
+
+Search.prototype = {
+ /**
+ * Enables the desired AutoComplete behavior.
+ *
+ * @param type
+ * The behavior type to set.
+ */
+ setBehavior: function (type) {
+ type = type.toUpperCase();
+ this._behavior |=
+ Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type];
+
+ // Setting the "typed" behavior should also set the "history" behavior.
+ if (type == "TYPED") {
+ this.setBehavior("history");
+ }
+ },
+
+ /**
+ * 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 (type) {
+ let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
+
+ if (this._disablePrivateActions &&
+ behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE) {
+ return false;
+ }
+
+ return this._behavior & behavior;
+ },
+
+ /**
+ * Used to delay the most complex queries, to save IO while the user is
+ * typing.
+ */
+ _sleepDeferred: null,
+ _sleep: function (aTimeMs) {
+ // Reuse a single instance to try shaving off some usless work before
+ // the first query.
+ if (!this._sleepTimer)
+ this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._sleepDeferred = PromiseUtils.defer();
+ this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
+ aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
+ return this._sleepDeferred.promise;
+ },
+
+ /**
+ * Given an array of tokens, this function determines which query should be
+ * ran. It also removes any special search tokens.
+ *
+ * @param tokens
+ * An array of search tokens.
+ * @return the filtered list of tokens to search with.
+ */
+ filterTokens: function (tokens) {
+ let foundToken = false;
+ // Set the proper behavior while filtering tokens.
+ for (let i = tokens.length - 1; i >= 0; i--) {
+ let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]);
+ // Don't remove the token if it didn't match, or if it's an action but
+ // actions are not enabled.
+ if (behavior && (behavior != "openpage" || this._enableActions)) {
+ // Don't use the suggest preferences if it is a token search and
+ // set the restrict bit to 1 (to intersect the search results).
+ if (!foundToken) {
+ foundToken = true;
+ // Do not take into account previous behavior (e.g.: history, bookmark)
+ this._behavior = 0;
+ this.setBehavior("restrict");
+ }
+ this.setBehavior(behavior);
+ tokens.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 (!Prefs.filterJavaScript) {
+ this.setBehavior("javascript");
+ }
+
+ return tokens;
+ },
+
+ /**
+ * Stop this search.
+ * After invoking this method, we won't run any more searches or heuristics,
+ * and no new matches may be added to the current result.
+ */
+ stop() {
+ if (this._sleepTimer)
+ this._sleepTimer.cancel();
+ if (this._sleepDeferred) {
+ this._sleepDeferred.resolve();
+ this._sleepDeferred = null;
+ }
+ if (this._searchSuggestionController) {
+ this._searchSuggestionController.stop();
+ this._searchSuggestionController = null;
+ }
+ this.pending = false;
+ },
+
+ /**
+ * Whether this search is active.
+ */
+ pending: true,
+
+ /**
+ * Execute the search and populate results.
+ * @param conn
+ * The Sqlite connection.
+ */
+ execute: Task.async(function* (conn) {
+ // A search might be canceled before it starts.
+ if (!this.pending)
+ return;
+
+ TelemetryStopwatch.start(TELEMETRY_1ST_RESULT, this);
+ if (this._searchString)
+ TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS, this);
+
+ // Since we call the synchronous parseSubmissionURL function later, we must
+ // wait for the initialization of PlacesSearchAutocompleteProvider first.
+ yield PlacesSearchAutocompleteProvider.ensureInitialized();
+ if (!this.pending)
+ return;
+
+ // For any given search, we run many queries/heuristics:
+ // 1) by alias (as defined in SearchService)
+ // 2) inline completion from search engine resultDomains
+ // 3) inline completion for hosts (this._hostQuery) or urls (this._urlQuery)
+ // 4) directly typed in url (ie, can be navigated to as-is)
+ // 5) submission for the current search engine
+ // 6) Places keywords
+ // 7) adaptive learning (this._adaptiveQuery)
+ // 8) open pages not supported by history (this._switchToTabQuery)
+ // 9) query based on match behavior
+ //
+ // (6) only gets ran if we get any filtered tokens, since if there are no
+ // tokens, there is nothing to match. This is the *first* query we check if
+ // we want to run, but it gets queued to be run later.
+ //
+ // (1), (4), (5) only get run if actions are enabled. When actions are
+ // enabled, the first result is always a special result (resulting from one
+ // of the queries between (1) and (6) inclusive). As such, the UI is
+ // expected to auto-select the first result when actions are enabled. If the
+ // first result is an inline completion result, that will also be the
+ // default result and therefore be autofilled (this also happens if actions
+ // are not enabled).
+
+ // Get the final query, based on the tokens found in the search string.
+ let queries = [ this._adaptiveQuery ];
+
+ // "openpage" behavior is supported by the default query.
+ // _switchToTabQuery instead returns only pages not supported by history.
+ if (this.hasBehavior("openpage")) {
+ queries.push(this._switchToTabQuery);
+ }
+ queries.push(this._searchQuery);
+
+ // Add the first heuristic result, if any. Set _addingHeuristicFirstMatch
+ // to true so that when the result is added, "heuristic" can be included in
+ // its style.
+ this._addingHeuristicFirstMatch = true;
+ let hasHeuristic = yield this._matchFirstHeuristicResult(conn);
+ this._addingHeuristicFirstMatch = false;
+ if (!this.pending)
+ return;
+
+ // We sleep a little between adding the heuristicFirstMatch and matching
+ // any other searches so we aren't kicking off potentially expensive
+ // searches on every keystroke.
+ // Though, if there's no heuristic result, we start searching immediately,
+ // since autocomplete may be waiting for us.
+ if (hasHeuristic) {
+ yield this._sleep(Prefs.delay);
+ if (!this.pending)
+ return;
+ }
+
+ if (this._enableActions && this._searchTokens.length > 0) {
+ yield this._matchSearchSuggestions();
+ if (!this.pending)
+ return;
+ }
+
+ for (let [query, params] of queries) {
+ yield conn.executeCached(query, params, this._onResultRow.bind(this));
+ if (!this.pending)
+ return;
+ }
+
+ if (this._enableActions && this.hasBehavior("openpage")) {
+ yield this._matchRemoteTabs();
+ if (!this.pending)
+ 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._localMatchesCount < Prefs.maxRichResults) {
+ this._matchBehavior = MATCH_ANYWHERE;
+ for (let [query, params] of [ this._adaptiveQuery,
+ this._searchQuery ]) {
+ yield conn.executeCached(query, params, this._onResultRow.bind(this));
+ if (!this.pending)
+ return;
+ }
+ }
+
+ // Only add extension suggestions if the first token is a registered keyword
+ // and the search string has characters after the first token.
+ if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+ this._originalSearchString.length > this._searchTokens[0].length) {
+ yield this._matchExtensionSuggestions();
+ if (!this.pending)
+ return;
+ } else if (ExtensionSearchHandler.hasActiveInputSession()) {
+ ExtensionSearchHandler.handleInputCancelled();
+ }
+
+ // Ensure to fill any remaining space. Suggestions which come from extensions are
+ // inserted at the beginning, so any suggestions
+ yield Promise.all(this._remoteMatchesPromises);
+ }),
+
+ *_matchFirstHeuristicResult(conn) {
+ // We always try to make the first result a special "heuristic" result. The
+ // heuristics below determine what type of result it will be, if any.
+
+ let hasSearchTerms = this._searchTokens.length > 0;
+
+ if (hasSearchTerms) {
+ // It may be a keyword registered by an extension.
+ let matched = yield this._matchExtensionHeuristicResult();
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this._enableActions && hasSearchTerms) {
+ // It may be a search engine with an alias - which works like a keyword.
+ let matched = yield this._matchSearchEngineAlias();
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this.pending && hasSearchTerms) {
+ // It may be a Places keyword.
+ let matched = yield this._matchPlacesKeyword();
+ if (matched) {
+ return true;
+ }
+ }
+
+ let shouldAutofill = this._shouldAutofill;
+ if (this.pending && shouldAutofill) {
+ // It may also look like a URL we know from the database.
+ let matched = yield this._matchKnownUrl(conn);
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this.pending && shouldAutofill) {
+ // Or it may look like a URL we know about from search engines.
+ let matched = yield this._matchSearchEngineUrl();
+ if (matched) {
+ return true;
+ }
+ }
+
+ if (this.pending && hasSearchTerms && this._enableActions) {
+ // If we don't have a result that matches what we know about, then
+ // we use a fallback for things we don't know about.
+
+ // We may not have auto-filled, but this may still look like a URL.
+ // However, even if the input is a valid URL, we may not want to use
+ // it as such. This can happen if the host would require whitelisting,
+ // but isn't in the whitelist.
+ let matched = yield this._matchUnknownUrl();
+ if (matched) {
+ // Since we can't tell if this is a real URL and
+ // whether the user wants to visit or search for it,
+ // we always provide an alternative searchengine match.
+ try {
+ new URL(this._originalSearchString);
+ } catch (ex) {
+ if (Prefs.keywordEnabled && !looksLikeUrl(this._originalSearchString, true)) {
+ this._addingHeuristicFirstMatch = false;
+ yield this._matchCurrentSearchEngine();
+ this._addingHeuristicFirstMatch = true;
+ }
+ }
+ return true;
+ }
+ }
+
+ if (this.pending && this._enableActions && this._originalSearchString) {
+ // When all else fails, and the search string is non-empty, we search
+ // using the current search engine.
+ let matched = yield this._matchCurrentSearchEngine();
+ if (matched) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ *_matchSearchSuggestions() {
+ // Limit the string sent for search suggestions to a maximum length.
+ let searchString = this._searchTokens.join(" ")
+ .substr(0, Prefs.maxCharsForSearchSuggestions);
+ // Avoid fetching suggestions if they are not required, private browsing
+ // mode is enabled, or the search string may expose sensitive information.
+ if (!this.hasBehavior("searches") || this._inPrivateWindow ||
+ this._prohibitSearchSuggestionsFor(searchString)) {
+ return;
+ }
+
+ this._searchSuggestionController =
+ PlacesSearchAutocompleteProvider.getSuggestionController(
+ searchString,
+ this._inPrivateWindow,
+ Prefs.maxRichResults,
+ this._userContextId
+ );
+ let promise = this._searchSuggestionController.fetchCompletePromise
+ .then(() => {
+ // The search has been canceled already.
+ if (!this._searchSuggestionController)
+ return;
+ if (this._searchSuggestionController.resultsCount >= 0 &&
+ this._searchSuggestionController.resultsCount < 2) {
+ // The original string is used to properly compare with the next search.
+ this._lastLowResultsSearchSuggestion = this._originalSearchString;
+ }
+ while (this.pending && this._remoteMatchesCount < Prefs.maxRichResults) {
+ let [match, suggestion] = this._searchSuggestionController.consume();
+ if (!suggestion)
+ break;
+ if (!looksLikeUrl(suggestion)) {
+ // Don't include the restrict token, if present.
+ let searchString = this._searchTokens.join(" ");
+ this._addSearchEngineMatch(match, searchString, suggestion);
+ }
+ }
+ });
+
+ if (this.hasBehavior("restrict")) {
+ // We're done if we're restricting to search suggestions.
+ yield promise;
+ this.stop();
+ } else {
+ this._remoteMatchesPromises.push(promise);
+ }
+ },
+
+ _prohibitSearchSuggestionsFor(searchString) {
+ if (this._prohibitSearchSuggestions)
+ return true;
+
+ // Suggestions for a single letter are unlikely to be useful.
+ if (searchString.length < 2)
+ return true;
+
+ // The first token may be a whitelisted host.
+ if (this._searchTokens.length == 1 &&
+ REGEXP_SINGLEWORD_HOST.test(this._searchTokens[0]) &&
+ Services.uriFixup.isDomainWhitelisted(this._searchTokens[0], -1)) {
+ return true;
+ }
+
+ // Disallow fetching search suggestions for strings looking like URLs, to
+ // avoid disclosing information about networks or passwords.
+ return this._searchTokens.some(looksLikeUrl);
+ },
+
+ _matchKnownUrl: function* (conn) {
+ // Hosts have no "/" in them.
+ let lastSlashIndex = this._searchString.lastIndexOf("/");
+ // Search only URLs if there's a slash in the search string...
+ if (lastSlashIndex != -1) {
+ // ...but not if it's exactly at the end of the search string.
+ if (lastSlashIndex < this._searchString.length - 1) {
+ // We don't want to execute this query right away because it needs to
+ // search the entire DB without an index, but we need to know if we have
+ // a result as it will influence other heuristics. So we guess by
+ // assuming that if we get a result from a *host* query and it *looks*
+ // like a URL, then we'll probably have a result.
+ let gotResult = false;
+ let [ query, params ] = this._urlQuery;
+ yield conn.executeCached(query, params, row => {
+ gotResult = true;
+ this._onResultRow(row);
+ });
+ return gotResult;
+ }
+ return false;
+ }
+
+ let gotResult = false;
+ let [ query, params ] = this._hostQuery;
+ yield conn.executeCached(query, params, row => {
+ gotResult = true;
+ this._onResultRow(row);
+ });
+ return gotResult;
+ },
+
+ _matchExtensionHeuristicResult: function* () {
+ if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+ this._originalSearchString.length > this._searchTokens[0].length) {
+ let description = ExtensionSearchHandler.getDescription(this._searchTokens[0]);
+ this._addExtensionMatch(this._originalSearchString, description);
+ return true;
+ }
+ return false;
+ },
+
+ _matchPlacesKeyword: function* () {
+ // The first word could be a keyword, so that's what we'll search.
+ let keyword = this._searchTokens[0];
+ let entry = yield PlacesUtils.keywords.fetch(this._searchTokens[0]);
+ if (!entry)
+ return false;
+
+ let searchString = this._trimmedOriginalSearchString.substr(keyword.length + 1);
+
+ let url = null, postData = null;
+ try {
+ [url, postData] =
+ yield BrowserUtils.parseUrlAndPostData(entry.url.href,
+ entry.postData,
+ searchString);
+ } catch (ex) {
+ // It's not possible to bind a param to this keyword.
+ return false;
+ }
+
+ let style = (this._enableActions ? "action " : "") + "keyword";
+ let actionURL = PlacesUtils.mozActionURI("keyword", {
+ url,
+ input: this._originalSearchString,
+ postData,
+ });
+ let value = this._enableActions ? actionURL : url;
+ // The title will end up being "host: queryString"
+ let comment = entry.url.host;
+
+ this._addMatch({ value, comment, style, frecency: FRECENCY_DEFAULT });
+ return true;
+ },
+
+ _matchSearchEngineUrl: function* () {
+ if (!Prefs.autofillSearchEngines)
+ return false;
+
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(
+ this._searchString);
+ if (!match)
+ return false;
+
+ // The match doesn't contain a 'scheme://www.' prefix, but since we have
+ // stripped it from the search string, here we could still be matching
+ // 'https://www.g' to 'google.com'.
+ // There are a couple cases where we don't want to match though:
+ //
+ // * If the protocol differs we should not match. For example if the user
+ // searched https we should not return http.
+ try {
+ let prefixURI = NetUtil.newURI(this._strippedPrefix);
+ let finalURI = NetUtil.newURI(match.url);
+ if (prefixURI.scheme != finalURI.scheme)
+ return false;
+ } catch (e) {}
+
+ // * If the user typed "www." but the final url doesn't have it, we
+ // should not match as well, the two urls may point to different pages.
+ if (this._strippedPrefix.endsWith("www.") &&
+ !stripHttpAndTrim(match.url).startsWith("www."))
+ return false;
+
+ let value = this._strippedPrefix + match.token;
+
+ // In any case, we should never arrive here with a value that doesn't
+ // match the search string. If this happens there is some case we
+ // are not handling properly yet.
+ if (!value.startsWith(this._originalSearchString)) {
+ Components.utils.reportError(`Trying to inline complete in-the-middle
+ ${this._originalSearchString} to ${value}`);
+ return false;
+ }
+
+ this._result.setDefaultIndex(0);
+ this._addMatch({
+ value: value,
+ comment: match.engineName,
+ icon: match.iconUrl,
+ style: "priority-search",
+ finalCompleteValue: match.url,
+ frecency: FRECENCY_DEFAULT
+ });
+ return true;
+ },
+
+ _matchSearchEngineAlias: function* () {
+ if (this._searchTokens.length < 1)
+ return false;
+
+ let alias = this._searchTokens[0];
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias(alias);
+ if (!match)
+ return false;
+
+ match.engineAlias = alias;
+ let query = this._trimmedOriginalSearchString.substr(alias.length + 1);
+
+ this._addSearchEngineMatch(match, query);
+ return true;
+ },
+
+ _matchCurrentSearchEngine: function* () {
+ let match = yield PlacesSearchAutocompleteProvider.getDefaultMatch();
+ if (!match)
+ return false;
+
+ let query = this._originalSearchString;
+ this._addSearchEngineMatch(match, query);
+ return true;
+ },
+
+ _addExtensionMatch(content, comment) {
+ if (this._extensionMatchesCount >= MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
+ return;
+ }
+
+ this._addMatch({
+ value: PlacesUtils.mozActionURI("extension", {
+ content,
+ keyword: this._searchTokens[0]
+ }),
+ comment,
+ icon: "chrome://browser/content/extension.svg",
+ style: "action extension",
+ frecency: FRECENCY_DEFAULT,
+ extension: true,
+ });
+ },
+
+ _addSearchEngineMatch(match, query, suggestion) {
+ let actionURLParams = {
+ engineName: match.engineName,
+ input: suggestion || this._originalSearchString,
+ searchQuery: query,
+ };
+ if (suggestion)
+ actionURLParams.searchSuggestion = suggestion;
+ if (match.engineAlias) {
+ actionURLParams.alias = match.engineAlias;
+ }
+ let value = PlacesUtils.mozActionURI("searchengine", actionURLParams);
+
+ this._addMatch({
+ value: value,
+ comment: match.engineName,
+ icon: match.iconUrl,
+ style: "action searchengine",
+ frecency: FRECENCY_DEFAULT,
+ remote: !!suggestion
+ });
+ },
+
+ *_matchExtensionSuggestions() {
+ let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
+ suggestions => {
+ suggestions.forEach(suggestion => {
+ let content = `${this._searchTokens[0]} ${suggestion.content}`;
+ this._addExtensionMatch(content, suggestion.description);
+ });
+ }
+ );
+ this._remoteMatchesPromises.push(promise);
+ },
+
+ *_matchRemoteTabs() {
+ let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
+ for (let {url, title, icon, deviceName} of matches) {
+ // It's rare that Sync supplies the icon for the page (but if it does, it
+ // is a string URL)
+ if (!icon) {
+ try {
+ let favicon = yield PlacesUtils.promiseFaviconLinkUrl(url);
+ if (favicon) {
+ icon = favicon.spec;
+ }
+ } catch (ex) {} // no favicon for this URL.
+ } else {
+ icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(icon)).spec;
+ }
+
+ let match = {
+ // We include the deviceName in the action URL so we can render it in
+ // the URLBar.
+ value: PlacesUtils.mozActionURI("remotetab", { url, deviceName }),
+ comment: title || url,
+ style: "action remotetab",
+ // we want frecency > FRECENCY_DEFAULT so it doesn't get pushed out
+ // by "remote" matches.
+ frecency: FRECENCY_DEFAULT + 1,
+ icon,
+ }
+ this._addMatch(match);
+ }
+ },
+
+ // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
+ // scheme isn't specificed.
+ _matchUnknownUrl: function* () {
+ let flags = Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
+ Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ let fixupInfo = null;
+ try {
+ fixupInfo = Services.uriFixup.getFixupURIInfo(this._originalSearchString,
+ flags);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_MALFORMED_URI && !Prefs.keywordEnabled) {
+ let value = PlacesUtils.mozActionURI("visiturl", {
+ url: this._originalSearchString,
+ input: this._originalSearchString,
+ });
+ this._addMatch({
+ value,
+ comment: this._originalSearchString,
+ style: "action visiturl",
+ frecency: 0,
+ });
+
+ return true;
+ }
+ return false;
+ }
+
+ // If the URI cannot be fixed or the preferred URI would do a keyword search,
+ // that basically means this isn't useful to us. Note that
+ // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref
+ // is false or there are no engines, so in that case we will always return
+ // a "visit".
+ if (!fixupInfo.fixedURI || fixupInfo.keywordAsSent)
+ return false;
+
+ let uri = fixupInfo.fixedURI;
+ // Check the host, as "http:///" is a valid nsIURI, but not useful to us.
+ // But, some schemes are expected to have no host. So we check just against
+ // schemes we know should have a host. This allows new schemes to be
+ // implemented without us accidentally blocking access to them.
+ let hostExpected = new Set(["http", "https", "ftp", "chrome", "resource"]);
+ if (hostExpected.has(uri.scheme) && !uri.host)
+ return false;
+
+ // getFixupURIInfo() escaped the URI, so it may not be pretty. Embed the
+ // escaped URL in the action URI since that URL should be "canonical". But
+ // pass the pretty, unescaped URL as the match comment, since it's likely
+ // to be displayed to the user, and in any case the front-end should not
+ // rely on it being canonical.
+ let escapedURL = uri.spec;
+ let displayURL = textURIService.unEscapeURIForUI("UTF-8", uri.spec);
+
+ let value = PlacesUtils.mozActionURI("visiturl", {
+ url: escapedURL,
+ input: this._originalSearchString,
+ });
+
+ let match = {
+ value: value,
+ comment: displayURL,
+ style: "action visiturl",
+ frecency: 0,
+ };
+
+ try {
+ let favicon = yield PlacesUtils.promiseFaviconLinkUrl(uri);
+ if (favicon)
+ match.icon = favicon.spec;
+ } catch (e) {
+ // It's possible we don't have a favicon for this - and that's ok.
+ }
+
+ this._addMatch(match);
+ return true;
+ },
+
+ _onResultRow: function (row) {
+ if (this._localMatchesCount == 0) {
+ TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, this);
+ }
+ let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
+ let match;
+ switch (queryType) {
+ case QUERYTYPE_AUTOFILL_HOST:
+ this._result.setDefaultIndex(0);
+ match = this._processHostRow(row);
+ break;
+ case QUERYTYPE_AUTOFILL_URL:
+ this._result.setDefaultIndex(0);
+ match = this._processUrlRow(row);
+ break;
+ case QUERYTYPE_FILTERED:
+ match = this._processRow(row);
+ break;
+ }
+ this._addMatch(match);
+ // If the search has been canceled by the user or by _addMatch, or we
+ // fetched enough results, we can stop the underlying Sqlite query.
+ if (!this.pending || this._localMatchesCount == Prefs.maxRichResults)
+ throw StopIteration;
+ },
+
+ _maybeRestyleSearchMatch: function (match) {
+ // Return if the URL does not represent a search result.
+ let parseResult =
+ PlacesSearchAutocompleteProvider.parseSubmissionURL(match.value);
+ if (!parseResult) {
+ return;
+ }
+
+ // Do not apply the special style if the user is doing a search from the
+ // location bar but the entered terms match an irrelevant portion of the
+ // URL. For example, "https://www.google.com/search?q=terms&client=firefox"
+ // when searching for "Firefox".
+ let terms = parseResult.terms.toLowerCase();
+ if (this._searchTokens.length > 0 &&
+ this._searchTokens.every(token => !terms.includes(token))) {
+ return;
+ }
+
+ // Turn the match into a searchengine action with a favicon.
+ match.value = PlacesUtils.mozActionURI("searchengine", {
+ engineName: parseResult.engineName,
+ input: parseResult.terms,
+ searchQuery: parseResult.terms,
+ });
+ match.comment = parseResult.engineName;
+ match.icon = match.icon || match.iconUrl;
+ match.style = "action searchengine favicon";
+ },
+
+ _addMatch(match) {
+ // A search could be canceled between a query start and its completion,
+ // in such a case ensure we won't notify any result for it.
+ if (!this.pending)
+ return;
+
+ // Must check both id and url, cause keywords dynamically modify the url.
+ let urlMapKey = makeKeyForURL(match.value);
+ if ((match.placeId && this._usedPlaceIds.has(match.placeId)) ||
+ this._usedURLs.has(urlMapKey)) {
+ return;
+ }
+
+ // Add this to our internal tracker to ensure duplicates do not end up in
+ // the result.
+ // 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.
+ if (match.placeId)
+ this._usedPlaceIds.add(match.placeId);
+ this._usedURLs.add(urlMapKey);
+
+ match.style = match.style || "favicon";
+
+ // Restyle past searches, unless they are bookmarks or special results.
+ if (Prefs.restyleSearches && match.style == "favicon") {
+ this._maybeRestyleSearchMatch(match);
+ }
+
+ if (this._addingHeuristicFirstMatch) {
+ match.style += " heuristic";
+ }
+
+ match.icon = match.icon || "";
+ match.finalCompleteValue = match.finalCompleteValue || "";
+
+ this._result.insertMatchAt(this._getInsertIndexForMatch(match),
+ match.value,
+ match.comment,
+ match.icon,
+ match.style,
+ match.finalCompleteValue);
+
+ if (this._result.matchCount == 6)
+ TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, this);
+
+ this.notifyResults(true);
+ },
+
+ _getInsertIndexForMatch(match) {
+ let index = 0;
+ if (match.remote) {
+ // Append after local matches.
+ index = this._remoteMatchesStartIndex + this._remoteMatchesCount;
+ this._remoteMatchesCount++;
+ } else if (match.extension) {
+ index = this._localMatchesStartIndex;
+ this._localMatchesStartIndex++;
+ this._remoteMatchesStartIndex++;
+ this._extensionMatchesCount++;
+ } else {
+ // This is a local match.
+ if (match.frecency > FRECENCY_DEFAULT ||
+ this._localMatchesCount < MINIMUM_LOCAL_MATCHES) {
+ // Append before remote matches.
+ index = this._remoteMatchesStartIndex;
+ this._remoteMatchesStartIndex++
+ } else {
+ // Append after remote matches.
+ index = this._localMatchesCount + this._remoteMatchesCount;
+ }
+ this._localMatchesCount++;
+ }
+ return index;
+ },
+
+ _processHostRow: function (row) {
+ let match = {};
+ let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
+ let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
+ let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+ let faviconUrl = row.getResultByIndex(QUERYINDEX_ICONURL);
+
+ // If the untrimmed value doesn't preserve the user's input just
+ // ignore it and complete to the found host.
+ if (untrimmedHost &&
+ !untrimmedHost.toLowerCase().includes(this._trimmedOriginalSearchString.toLowerCase())) {
+ untrimmedHost = null;
+ }
+
+ match.value = this._strippedPrefix + trimmedHost;
+ // Remove the trailing slash.
+ match.comment = stripHttpAndTrim(trimmedHost);
+ match.finalCompleteValue = untrimmedHost;
+ if (faviconUrl) {
+ match.icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(faviconUrl)).spec;
+ }
+ // Although this has a frecency, this query is executed before any other
+ // queries that would result in frecency matches.
+ match.frecency = frecency;
+ match.style = "autofill";
+ return match;
+ },
+
+ _processUrlRow: function (row) {
+ let match = {};
+ let value = row.getResultByIndex(QUERYINDEX_URL);
+ let url = fixupSearchText(value);
+ let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+ let faviconUrl = row.getResultByIndex(QUERYINDEX_ICONURL);
+
+ let prefix = value.slice(0, value.length - stripPrefix(value).length);
+
+ // We must complete the URL up to the next separator (which is /, ? or #).
+ let separatorIndex = url.slice(this._searchString.length)
+ .search(/[\/\?\#]/);
+ if (separatorIndex != -1) {
+ separatorIndex += this._searchString.length;
+ if (url[separatorIndex] == "/") {
+ separatorIndex++; // Include the "/" separator
+ }
+ url = url.slice(0, separatorIndex);
+ }
+
+ // If the untrimmed value doesn't preserve the user's input just
+ // ignore it and complete to the found url.
+ let untrimmedURL = prefix + url;
+ if (untrimmedURL &&
+ !untrimmedURL.toLowerCase().includes(this._trimmedOriginalSearchString.toLowerCase())) {
+ untrimmedURL = null;
+ }
+
+ match.value = this._strippedPrefix + url;
+ match.comment = url;
+ match.finalCompleteValue = untrimmedURL;
+ if (faviconUrl) {
+ match.icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(faviconUrl)).spec;
+ }
+ // Although this has a frecency, this query is executed before any other
+ // queries that would result in frecency matches.
+ match.frecency = frecency;
+ match.style = "autofill";
+ return match;
+ },
+
+ _processRow: function (row) {
+ let match = {};
+ match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
+ let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
+ let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
+ let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
+ let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || "";
+ let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
+ let bookmarkTitle = bookmarked ?
+ row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null;
+ let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
+ let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+
+ // If actions are enabled and the page is open, add only the switch-to-tab
+ // result. Otherwise, add the normal result.
+ let url = escapedURL;
+ let action = null;
+ if (this._enableActions && openPageCount > 0 && this.hasBehavior("openpage")) {
+ url = PlacesUtils.mozActionURI("switchtab", {url: escapedURL});
+ action = "switchtab";
+ }
+
+ // Always prefer the bookmark title unless it is empty
+ let title = bookmarkTitle || historyTitle;
+
+ // We will always prefer to show tags if we have them.
+ let showTags = !!tags;
+
+ // 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;
+ match.style = "favicon";
+ }
+
+ // If we have tags and should show them, we need to add them to the title.
+ if (showTags) {
+ title += TITLE_TAGS_SEPARATOR + tags;
+ }
+
+ // 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 (!match.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) {
+ // If we're not suggesting bookmarks, then this shouldn't
+ // display as one.
+ match.style = this.hasBehavior("bookmark") ? "bookmark-tag" : "tag";
+ }
+ else if (bookmarked) {
+ match.style = "bookmark";
+ }
+ }
+
+ if (action)
+ match.style = "action " + action;
+
+ match.value = url;
+ match.comment = title;
+ if (iconurl) {
+ match.icon = PlacesUtils.favicons
+ .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec;
+ }
+ match.frecency = frecency;
+
+ return match;
+ },
+
+ /**
+ * @return a string consisting of the search query to be used based on the
+ * previously set urlbar suggestion preferences.
+ */
+ get _suggestionPrefQuery() {
+ if (!this.hasBehavior("restrict") && this.hasBehavior("history") &&
+ this.hasBehavior("bookmark")) {
+ return this.hasBehavior("typed") ? defaultQuery("AND h.typed = 1")
+ : 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 ? defaultQuery("AND " + conditions.join(" AND "))
+ : defaultQuery();
+ },
+
+ /**
+ * Obtains the search query to be used based on the previously set search
+ * preferences (accessed by this.hasBehavior).
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _searchQuery() {
+ let query = this._suggestionPrefQuery;
+
+ return [
+ query,
+ {
+ parent: PlacesUtils.tagsFolderId,
+ query_type: QUERYTYPE_FILTERED,
+ matchBehavior: this._matchBehavior,
+ searchBehavior: this._behavior,
+ // We only want to search the tokens that we are left with - not the
+ // original search string.
+ searchString: this._searchTokens.join(" "),
+ userContextId: this._userContextId,
+ // Limit the query to the the maximum number of desired results.
+ // This way we can avoid doing more work than needed.
+ maxResults: Prefs.maxRichResults
+ }
+ ];
+ },
+
+ /**
+ * Obtains the query to search for switch-to-tab entries.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _switchToTabQuery() {
+ return [
+ SQL_SWITCHTAB_QUERY,
+ {
+ query_type: QUERYTYPE_FILTERED,
+ matchBehavior: this._matchBehavior,
+ searchBehavior: this._behavior,
+ // We only want to search the tokens that we are left with - not the
+ // original search string.
+ searchString: this._searchTokens.join(" "),
+ userContextId: this._userContextId,
+ maxResults: Prefs.maxRichResults
+ }
+ ];
+ },
+
+ /**
+ * Obtains the query to search for adaptive results.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _adaptiveQuery() {
+ return [
+ SQL_ADAPTIVE_QUERY,
+ {
+ parent: PlacesUtils.tagsFolderId,
+ search_string: this._searchString,
+ query_type: QUERYTYPE_FILTERED,
+ matchBehavior: this._matchBehavior,
+ searchBehavior: this._behavior,
+ userContextId: this._userContextId,
+ }
+ ];
+ },
+
+ /**
+ * Whether we should try to autoFill.
+ */
+ get _shouldAutofill() {
+ // First of all, check for the autoFill pref.
+ if (!Prefs.autofill)
+ return false;
+
+ if (this._searchTokens.length != 1)
+ return false;
+
+ // autoFill can only cope with history or bookmarks entries.
+ if (!this.hasBehavior("history") &&
+ !this.hasBehavior("bookmark"))
+ return false;
+
+ // autoFill doesn't search titles or tags.
+ if (this.hasBehavior("title") || this.hasBehavior("tag"))
+ return false;
+
+ // Don't try to autofill if the search term includes any whitespace.
+ // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+ // tokenizer ends up trimming the search string and returning a value
+ // that doesn't match it, or is even shorter.
+ if (REGEXP_SPACES.test(this._originalSearchString))
+ return false;
+
+ if (this._searchString.length == 0)
+ return false;
+
+ if (this._prohibitAutoFill)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Obtains the query to search for autoFill host results.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _hostQuery() {
+ let typed = Prefs.autofillTyped || this.hasBehavior("typed");
+ let bookmarked = this.hasBehavior("bookmark") && !this.hasBehavior("history");
+
+ let query = [];
+ if (bookmarked) {
+ query.push(typed ? SQL_BOOKMARKED_TYPED_HOST_QUERY
+ : SQL_BOOKMARKED_HOST_QUERY);
+ } else {
+ query.push(typed ? SQL_TYPED_HOST_QUERY
+ : SQL_HOST_QUERY);
+ }
+
+ query.push({
+ query_type: QUERYTYPE_AUTOFILL_HOST,
+ searchString: this._searchString.toLowerCase()
+ });
+
+ return query;
+ },
+
+ /**
+ * Obtains the query to search for autoFill url results.
+ *
+ * @return an array consisting of the correctly optimized query to search the
+ * database with and an object containing the params to bound.
+ */
+ get _urlQuery() {
+ // We expect this to be a full URL, not just a host. We want to extract the
+ // host and use that as a guess for whether we'll get a result from a URL
+ // query.
+ let slashIndex = this._autofillUrlSearchString.indexOf("/");
+ let revHost = this._autofillUrlSearchString.substring(0, slashIndex).toLowerCase()
+ .split("").reverse().join("") + ".";
+
+ let typed = Prefs.autofillTyped || this.hasBehavior("typed");
+ let bookmarked = this.hasBehavior("bookmark") && !this.hasBehavior("history");
+
+ let query = [];
+ if (bookmarked) {
+ query.push(typed ? SQL_BOOKMARKED_TYPED_URL_QUERY
+ : SQL_BOOKMARKED_URL_QUERY);
+ } else {
+ query.push(typed ? SQL_TYPED_URL_QUERY
+ : SQL_URL_QUERY);
+ }
+
+ query.push({
+ query_type: QUERYTYPE_AUTOFILL_URL,
+ searchString: this._autofillUrlSearchString,
+ revHost
+ });
+
+ return query;
+ },
+
+ /**
+ * Notifies the listener about results.
+ *
+ * @param searchOngoing
+ * Indicates whether the search is ongoing.
+ */
+ notifyResults: function (searchOngoing) {
+ let result = this._result;
+ let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+ if (searchOngoing) {
+ resultCode += "_ONGOING";
+ }
+ result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+ this._listener.onSearchResult(this._autocompleteSearch, result);
+ },
+}
+
+// UnifiedComplete class
+// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
+
+function UnifiedComplete() {
+ // Make sure the preferences are initialized as soon as possible.
+ // If the value of browser.urlbar.autocomplete.enabled is set to false,
+ // then all the other suggest preferences for history, bookmarks and
+ // open pages should be set to false.
+ Prefs;
+}
+
+UnifiedComplete.prototype = {
+ // Database handling
+
+ /**
+ * Promise resolved when the database initialization has completed, or null
+ * if it has never been requested.
+ */
+ _promiseDatabase: null,
+
+ /**
+ * Gets a Sqlite database handle.
+ *
+ * @return {Promise}
+ * @resolves to the Sqlite database handle (according to Sqlite.jsm).
+ * @rejects javascript exception.
+ */
+ getDatabaseHandle: function () {
+ if (Prefs.enabled && !this._promiseDatabase) {
+ this._promiseDatabase = Task.spawn(function* () {
+ let conn = yield Sqlite.cloneStorageConnection({
+ connection: PlacesUtils.history.DBConnection,
+ readOnly: true
+ });
+
+ try {
+ Sqlite.shutdown.addBlocker("Places UnifiedComplete.js clone closing",
+ Task.async(function* () {
+ SwitchToTabStorage.shutdown();
+ yield conn.close();
+ }));
+ } catch (ex) {
+ // It's too late to block shutdown, just close the connection.
+ yield conn.close();
+ throw ex;
+ }
+
+ // Autocomplete often fallbacks to a table scan due to lack of text
+ // indices. A larger cache helps reducing IO and improving performance.
+ // The value used here is larger than the default Storage value defined
+ // as MAX_CACHE_SIZE_BYTES in storage/mozStorageConnection.cpp.
+ yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB
+
+ yield SwitchToTabStorage.initDatabase(conn);
+
+ return conn;
+ }.bind(this)).then(null, ex => { dump("Couldn't get database handle: " + ex + "\n");
+ Cu.reportError(ex); });
+ }
+ return this._promiseDatabase;
+ },
+
+ // mozIPlacesAutoComplete
+
+ registerOpenPage(uri, userContextId) {
+ SwitchToTabStorage.add(uri, userContextId);
+ },
+
+ unregisterOpenPage(uri, userContextId) {
+ SwitchToTabStorage.delete(uri, userContextId);
+ },
+
+ // nsIAutoCompleteSearch
+
+ startSearch: function (searchString, searchParam, previousResult, listener) {
+ // Stop the search in case the controller has not taken care of it.
+ if (this._currentSearch) {
+ this.stopSearch();
+ }
+
+ // Note: We don't use previousResult to make sure ordering of results are
+ // consistent. See bug 412730 for more details.
+
+ // If the previous search didn't fetch enough search suggestions, it's
+ // unlikely a longer text would do.
+ let prohibitSearchSuggestions =
+ this._lastLowResultsSearchSuggestion &&
+ searchString.length > this._lastLowResultsSearchSuggestion.length &&
+ searchString.startsWith(this._lastLowResultsSearchSuggestion);
+
+ this._currentSearch = new Search(searchString, searchParam, listener,
+ this, this, prohibitSearchSuggestions);
+
+ // If we are not enabled, we need to return now. Notice we need an empty
+ // result regardless, so we still create the Search object.
+ if (!Prefs.enabled) {
+ this.finishSearch(true);
+ return;
+ }
+
+ let search = this._currentSearch;
+ this.getDatabaseHandle().then(conn => search.execute(conn))
+ .then(null, ex => {
+ dump(`Query failed: ${ex}\n`);
+ Cu.reportError(ex);
+ })
+ .then(() => {
+ if (search == this._currentSearch) {
+ this.finishSearch(true);
+ }
+ });
+ },
+
+ stopSearch: function () {
+ if (this._currentSearch) {
+ this._currentSearch.stop();
+ }
+ // Don't notify since we are canceling this search. This also means we
+ // won't fire onSearchComplete for this search.
+ this.finishSearch();
+ },
+
+ /**
+ * Properly cleans up when searching is completed.
+ *
+ * @param notify [optional]
+ * Indicates if we should notify the AutoComplete listener about our
+ * results or not.
+ */
+ finishSearch: function (notify=false) {
+ TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT, this);
+ TelemetryStopwatch.cancel(TELEMETRY_6_FIRST_RESULTS, this);
+ // Clear state now to avoid race conditions, see below.
+ let search = this._currentSearch;
+ if (!search)
+ return;
+ this._lastLowResultsSearchSuggestion = search._lastLowResultsSearchSuggestion;
+ delete this._currentSearch;
+
+ if (!notify)
+ return;
+
+ // There is a possible race condition here.
+ // When a search completes it calls finishSearch that notifies results
+ // here. When the controller gets the last result it fires
+ // onSearchComplete.
+ // If onSearchComplete immediately starts a new search it will set a new
+ // _currentSearch, and on return the execution will continue here, after
+ // notifyResults.
+ // Thus, ensure that notifyResults is the last call in this method,
+ // otherwise you might be touching the wrong search.
+ search.notifyResults(false);
+ },
+
+ // nsIAutoCompleteSimpleResultListener
+
+ onValueRemoved: function (result, spec, removeFromDB) {
+ if (removeFromDB) {
+ PlacesUtils.history.removePage(NetUtil.newURI(spec));
+ }
+ },
+
+ // nsIAutoCompleteSearchDescriptor
+
+ get searchType() {
+ return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
+ },
+
+ get clearingAutoFillSearchesAgain() {
+ return true;
+ },
+
+ // nsISupports
+
+ classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteSimpleResultListener,
+ Ci.nsIAutoCompleteSearchDescriptor,
+ Ci.mozIPlacesAutoComplete,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);
diff --git a/toolkit/components/places/VisitInfo.cpp b/toolkit/components/places/VisitInfo.cpp
new file mode 100644
index 0000000000..cd3ec2f79f
--- /dev/null
+++ b/toolkit/components/places/VisitInfo.cpp
@@ -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/. */
+
+#include "VisitInfo.h"
+#include "nsIURI.h"
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// VisitInfo
+
+VisitInfo::VisitInfo(int64_t aVisitId,
+ PRTime aVisitDate,
+ uint32_t aTransitionType,
+ already_AddRefed<nsIURI> aReferrer)
+: mVisitId(aVisitId)
+, mVisitDate(aVisitDate)
+, mTransitionType(aTransitionType)
+, mReferrer(aReferrer)
+{
+}
+
+VisitInfo::~VisitInfo()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// mozIVisitInfo
+
+NS_IMETHODIMP
+VisitInfo::GetVisitId(int64_t* _visitId)
+{
+ *_visitId = mVisitId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VisitInfo::GetVisitDate(PRTime* _visitDate)
+{
+ *_visitDate = mVisitDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VisitInfo::GetTransitionType(uint32_t* _transitionType)
+{
+ *_transitionType = mTransitionType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VisitInfo::GetReferrerURI(nsIURI** _referrer)
+{
+ NS_IF_ADDREF(*_referrer = mReferrer);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsISupports
+
+NS_IMPL_ISUPPORTS(
+ VisitInfo
+, mozIVisitInfo
+)
+
+} // namespace places
+} // namespace mozilla
diff --git a/toolkit/components/places/VisitInfo.h b/toolkit/components/places/VisitInfo.h
new file mode 100644
index 0000000000..54b25c686a
--- /dev/null
+++ b/toolkit/components/places/VisitInfo.h
@@ -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/. */
+
+#ifndef mozilla_places_VisitInfo_h__
+#define mozilla_places_VisitInfo_h__
+
+#include "mozIAsyncHistory.h"
+#include "nsAutoPtr.h"
+#include "mozilla/Attributes.h"
+
+class nsIURI;
+
+namespace mozilla {
+namespace places {
+
+class VisitInfo final : public mozIVisitInfo
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIVISITINFO
+
+ VisitInfo(int64_t aVisitId, PRTime aVisitDate, uint32_t aTransitionType,
+ already_AddRefed<nsIURI> aReferrer);
+
+private:
+ ~VisitInfo();
+ const int64_t mVisitId;
+ const PRTime mVisitDate;
+ const uint32_t mTransitionType;
+ nsCOMPtr<nsIURI> mReferrer;
+};
+
+} // namespace places
+} // namespace mozilla
+
+#endif // mozilla_places_VisitInfo_h__
diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build
new file mode 100644
index 0000000000..adac79cba4
--- /dev/null
+++ b/toolkit/components/places/moz.build
@@ -0,0 +1,97 @@
+# -*- 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/.
+
+if CONFIG['MOZ_PLACES']:
+ TEST_DIRS += ['tests']
+
+XPIDL_SOURCES += [
+ 'nsINavHistoryService.idl',
+]
+
+XPIDL_MODULE = 'places'
+
+if CONFIG['MOZ_PLACES']:
+ XPIDL_SOURCES += [
+ 'mozIAsyncFavicons.idl',
+ 'mozIAsyncHistory.idl',
+ 'mozIAsyncLivemarks.idl',
+ 'mozIColorAnalyzer.idl',
+ 'mozIPlacesAutoComplete.idl',
+ 'mozIPlacesPendingOperation.idl',
+ 'nsIAnnotationService.idl',
+ 'nsIBrowserHistory.idl',
+ 'nsIFaviconService.idl',
+ 'nsINavBookmarksService.idl',
+ 'nsITaggingService.idl',
+ 'nsPIPlacesDatabase.idl',
+ ]
+
+ EXPORTS.mozilla.places = [
+ 'Database.h',
+ 'History.h',
+ ]
+
+ UNIFIED_SOURCES += [
+ 'Database.cpp',
+ 'FaviconHelpers.cpp',
+ 'Helpers.cpp',
+ 'History.cpp',
+ 'nsAnnoProtocolHandler.cpp',
+ 'nsAnnotationService.cpp',
+ 'nsFaviconService.cpp',
+ 'nsNavBookmarks.cpp',
+ 'nsNavHistory.cpp',
+ 'nsNavHistoryQuery.cpp',
+ 'nsNavHistoryResult.cpp',
+ 'nsPlacesModule.cpp',
+ 'PlaceInfo.cpp',
+ 'Shutdown.cpp',
+ 'SQLFunctions.cpp',
+ 'VisitInfo.cpp',
+ ]
+
+ LOCAL_INCLUDES += [
+ '../build',
+ ]
+
+ EXTRA_JS_MODULES += [
+ 'BookmarkHTMLUtils.jsm',
+ 'BookmarkJSONUtils.jsm',
+ 'Bookmarks.jsm',
+ 'ClusterLib.js',
+ 'ColorAnalyzer_worker.js',
+ 'ColorConversion.js',
+ 'ExtensionSearchHandler.jsm',
+ 'History.jsm',
+ 'PlacesBackups.jsm',
+ 'PlacesDBUtils.jsm',
+ 'PlacesRemoteTabsAutocompleteProvider.jsm',
+ 'PlacesSearchAutocompleteProvider.jsm',
+ 'PlacesSyncUtils.jsm',
+ 'PlacesTransactions.jsm',
+ 'PlacesUtils.jsm',
+ ]
+
+ EXTRA_COMPONENTS += [
+ 'ColorAnalyzer.js',
+ 'nsLivemarkService.js',
+ 'nsPlacesExpiration.js',
+ 'nsTaggingService.js',
+ 'PageIconProtocolHandler.js',
+ 'PlacesCategoriesStarter.js',
+ 'toolkitplaces.manifest',
+ 'UnifiedComplete.js',
+ ]
+
+ FINAL_LIBRARY = 'xul'
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Places')
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/toolkit/components/places/mozIAsyncFavicons.idl b/toolkit/components/places/mozIAsyncFavicons.idl
new file mode 100644
index 0000000000..f1be18278f
--- /dev/null
+++ b/toolkit/components/places/mozIAsyncFavicons.idl
@@ -0,0 +1,174 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+interface nsIFaviconDataCallback;
+interface nsIPrincipal;
+interface mozIPlacesPendingOperation;
+
+[scriptable, uuid(a9c81797-9133-4823-b55f-3646e67cfd41)]
+interface mozIAsyncFavicons : nsISupports
+{
+ /**
+ * Declares that a given page uses a favicon with the given URI and
+ * attempts to fetch and save the icon data by loading the favicon URI
+ * through an async network request.
+ *
+ * If the icon data already exists, we won't try to reload the icon unless
+ * aForceReload is true. Similarly, if the icon is in the failed favicon
+ * cache we won't do anything unless aForceReload is true, in which case
+ * we'll try to reload the favicon.
+ *
+ * This function will only save favicons for pages that are already stored in
+ * the database, like visited pages or bookmarks. For any other URIs, it
+ * will succeed but do nothing. This function will also ignore the error
+ * page favicon URI (see FAVICON_ERRORPAGE_URL below).
+ *
+ * Icons that fail to load will automatically be added to the failed favicon
+ * cache, and this function will not save favicons for non-bookmarked URIs
+ * when history is disabled.
+ *
+ * @note This function is identical to
+ * nsIFaviconService::setAndLoadFaviconForPage.
+ *
+ * @param aPageURI
+ * URI of the page whose favicon is being set.
+ * @param aFaviconURI
+ * URI of the favicon to associate with the page.
+ * @param aForceReload
+ * If aForceReload is false, we try to reload the favicon only if we
+ * don't have it or it has expired from the cache. Setting
+ * aForceReload to true causes us to reload the favicon even if we
+ * have a usable copy.
+ * @param aFaviconLoadType
+ * Set to FAVICON_LOAD_PRIVATE if the favicon is loaded from a private
+ * browsing window. Set to FAVICON_LOAD_NON_PRIVATE otherwise.
+ * @param aCallback
+ * Once we're done setting and/or fetching the favicon, we invoke this
+ * callback.
+ * @param aLoadingPrincipal
+ * Principal of the page whose favicon is being set. If this argument
+ * is omitted, the loadingPrincipal defaults to the nullPrincipal.
+ *
+ * @see nsIFaviconDataCallback in nsIFaviconService.idl.
+ */
+ mozIPlacesPendingOperation setAndFetchFaviconForPage(
+ in nsIURI aPageURI,
+ in nsIURI aFaviconURI,
+ in boolean aForceReload,
+ in unsigned long aFaviconLoadType,
+ [optional] in nsIFaviconDataCallback aCallback,
+ [optional] in nsIPrincipal aLoadingPrincipal);
+ /**
+ * Sets the data for a given favicon URI either by replacing existing data in
+ * the database or taking the place of otherwise fetched icon data when
+ * calling setAndFetchFaviconForPage later.
+ *
+ * Favicon data for favicon URIs that are not associated with a page URI via
+ * setAndFetchFaviconForPage will be stored in memory, but may be expired at
+ * any time, so you should make an effort to associate favicon URIs with page
+ * URIs as soon as possible.
+ *
+ * It's better to not use this function for chrome: icon URIs since you can
+ * reference the chrome image yourself. getFaviconLinkForIcon/Page will ignore
+ * any associated data if the favicon URI is "chrome:" and just return the
+ * same chrome URI.
+ *
+ * This function does NOT send out notifications that the data has changed.
+ * Pages using this favicons that are visible in history or bookmarks views
+ * will keep the old icon until they have been refreshed by other means.
+ *
+ * This function tries to optimize the favicon size, if it is bigger
+ * than a defined limit we will try to convert it to a 16x16 png image.
+ * If the conversion fails and favicon is still bigger than our max accepted
+ * size it won't be saved.
+ *
+ * @param aFaviconURI
+ * URI of the favicon whose data is being set.
+ * @param aData
+ * Binary contents of the favicon to save
+ * @param aDataLength
+ * Length of binary data
+ * @param aMimeType
+ * MIME type of the data to store. This is important so that we know
+ * what to report when the favicon is used. You should always set this
+ * param unless you are clearing an icon.
+ * @param aExpiration
+ * Time in microseconds since the epoch when this favicon expires.
+ * Until this time, we won't try to load it again.
+ * @throws NS_ERROR_FAILURE
+ * Thrown if the favicon is overbloated and won't be saved to the db.
+ */
+ void replaceFaviconData(in nsIURI aFaviconURI,
+ [const,array,size_is(aDataLen)] in octet aData,
+ in unsigned long aDataLen,
+ in AUTF8String aMimeType,
+ [optional] in PRTime aExpiration);
+
+ /**
+ * Same as replaceFaviconData but the data is provided by a string
+ * containing a data URL.
+ *
+ * @see replaceFaviconData
+ *
+ * @param aFaviconURI
+ * URI of the favicon whose data is being set.
+ * @param aDataURL
+ * string containing a data URL that represents the contents of
+ * the favicon to save
+ * @param aExpiration
+ * Time in microseconds since the epoch when this favicon expires.
+ * Until this time, we won't try to load it again.
+ * @param aLoadingPrincipal
+ * Principal of the page whose favicon is being set. If this argument
+ * is omitted, the loadingPrincipal defaults to the nullPrincipal.
+ * @throws NS_ERROR_FAILURE
+ * Thrown if the favicon is overbloated and won't be saved to the db.
+ */
+ void replaceFaviconDataFromDataURL(in nsIURI aFaviconURI,
+ in AString aDataURL,
+ [optional] in PRTime aExpiration,
+ [optional] in nsIPrincipal aLoadingPrincipal);
+
+ /**
+ * Retrieves the favicon URI associated to the given page, if any.
+ *
+ * @param aPageURI
+ * URI of the page whose favicon URI we're looking up.
+ * @param aCallback
+ * This callback is always invoked to notify the result of the lookup.
+ * The aURI parameter will be the favicon URI, or null when no favicon
+ * is associated with the page or an error occurred while fetching it.
+ *
+ * @note When the callback is invoked, aDataLen will be always 0, aData will
+ * be an empty array, and aMimeType will be an empty string, regardless
+ * of whether a favicon is associated with the page.
+ *
+ * @see nsIFaviconDataCallback in nsIFaviconService.idl.
+ */
+ void getFaviconURLForPage(in nsIURI aPageURI,
+ in nsIFaviconDataCallback aCallback);
+
+ /**
+ * Retrieves the favicon URI and data associated to the given page, if any.
+ *
+ * @param aPageURI
+ * URI of the page whose favicon URI and data we're looking up.
+ * @param aCallback
+ * This callback is always invoked to notify the result of the lookup. The aURI
+ * parameter will be the favicon URI, or null when no favicon is
+ * associated with the page or an error occurred while fetching it. If
+ * aURI is not null, the other parameters may contain the favicon data.
+ * However, if no favicon data is currently associated with the favicon
+ * URI, aDataLen will be 0, aData will be an empty array, and aMimeType
+ * will be an empty string.
+ *
+ * @see nsIFaviconDataCallback in nsIFaviconService.idl.
+ */
+ void getFaviconDataForPage(in nsIURI aPageURI,
+ in nsIFaviconDataCallback aCallback);
+};
diff --git a/toolkit/components/places/mozIAsyncHistory.idl b/toolkit/components/places/mozIAsyncHistory.idl
new file mode 100644
index 0000000000..35c8cc3a66
--- /dev/null
+++ b/toolkit/components/places/mozIAsyncHistory.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIVariant;
+
+[scriptable, uuid(41e4ccc9-f0c8-4cd7-9753-7a38514b8488)]
+interface mozIVisitInfo : nsISupports
+{
+ /**
+ * The machine-local (internal) id of the visit.
+ */
+ readonly attribute long long visitId;
+
+ /**
+ * The time the visit occurred.
+ */
+ readonly attribute PRTime visitDate;
+
+ /**
+ * The transition type used to get to this visit. One of the TRANSITION_TYPE
+ * constants on nsINavHistory.
+ *
+ * @see nsINavHistory.idl
+ */
+ readonly attribute unsigned long transitionType;
+
+ /**
+ * The referring URI of this visit. This may be null.
+ */
+ readonly attribute nsIURI referrerURI;
+};
+
+[scriptable, uuid(ad83e137-c92a-4b7b-b67e-0a318811f91e)]
+interface mozIPlaceInfo : nsISupports
+{
+ /**
+ * The machine-local (internal) id of the place.
+ */
+ readonly attribute long long placeId;
+
+ /**
+ * The globally unique id of the place.
+ */
+ readonly attribute ACString guid;
+
+ /**
+ * The URI of the place.
+ */
+ readonly attribute nsIURI uri;
+
+ /**
+ * The title associated with the place.
+ */
+ readonly attribute AString title;
+
+ /**
+ * The frecency of the place.
+ */
+ readonly attribute long long frecency;
+
+ /**
+ * An array of mozIVisitInfo objects for the place.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval visits;
+};
+
+/**
+ * Shared Callback interface for mozIAsyncHistory methods. The semantics
+ * for each method are detailed in mozIAsyncHistory.
+ */
+[scriptable, uuid(1f266877-2859-418b-a11b-ec3ae4f4f93d)]
+interface mozIVisitInfoCallback : nsISupports
+{
+ /**
+ * Called when the given place could not be processed.
+ *
+ * @param aResultCode
+ * nsresult indicating the failure reason.
+ * @param aPlaceInfo
+ * The information that was given to the caller for the place.
+ */
+ void handleError(in nsresult aResultCode,
+ in mozIPlaceInfo aPlaceInfo);
+
+ /**
+ * Called for each place processed successfully.
+ *
+ * @param aPlaceInfo
+ * The current info stored for the place.
+ */
+ void handleResult(in mozIPlaceInfo aPlaceInfo);
+
+ /**
+ * Called when all records were processed.
+ */
+ void handleCompletion();
+
+};
+
+[scriptable, function, uuid(994092bf-936f-449b-8dd6-0941e024360d)]
+interface mozIVisitedStatusCallback : nsISupports
+{
+ /**
+ * Notifies whether a certain URI has been visited.
+ *
+ * @param aURI
+ * URI being notified about.
+ * @param aVisitedStatus
+ * The visited status of aURI.
+ */
+ void isVisited(in nsIURI aURI,
+ in boolean aVisitedStatus);
+};
+
+[scriptable, uuid(1643EFD2-A329-4733-A39D-17069C8D3B2D)]
+interface mozIAsyncHistory : nsISupports
+{
+ /**
+ * Gets the available information for the given array of places, each
+ * identified by either nsIURI or places GUID (string).
+ *
+ * The retrieved places info objects DO NOT include the visits data (the
+ * |visits| attribute is set to null).
+ *
+ * If a given place does not exist in the database, aCallback.handleError is
+ * called for it with NS_ERROR_NOT_AVAILABLE result code.
+ *
+ * @param aPlaceIdentifiers
+ * The place[s] for which to retrieve information, identified by either
+ * a single place GUID, a single URI, or a JS array of URIs and/or GUIDs.
+ * @param aCallback
+ * A mozIVisitInfoCallback object which consists of callbacks to be
+ * notified for successful or failed retrievals.
+ * If there's no information available for a given place, aCallback
+ * is called with a stub place info object, containing just the provided
+ * data (GUID or URI).
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * - Passing in NULL for aPlaceIdentifiers or aCallback.
+ * - Not providing at least one valid GUID or URI.
+ */
+ [implicit_jscontext]
+ void getPlacesInfo(in jsval aPlaceIdentifiers,
+ in mozIVisitInfoCallback aCallback);
+
+ /**
+ * Adds a set of visits for one or more mozIPlaceInfo objects, and updates
+ * each mozIPlaceInfo's title or guid.
+ *
+ * aCallback.handleResult is called for each visit added.
+ *
+ * @param aPlaceInfo
+ * The mozIPlaceInfo object[s] containing the information to store or
+ * update. This can be a single object, or an array of objects.
+ * @param [optional] aCallback
+ * A mozIVisitInfoCallback object which consists of callbacks to be
+ * notified for successful and/or failed changes.
+ *
+ * @throws NS_ERROR_INVALID_ARG
+ * - Passing in NULL for aPlaceInfo.
+ * - Not providing at least one valid guid, or uri for all
+ * mozIPlaceInfo object[s].
+ * - Not providing an array or nothing for the visits property of
+ * mozIPlaceInfo.
+ * - Not providing a visitDate and transitionType for each
+ * mozIVisitInfo.
+ * - Providing an invalid transitionType for a mozIVisitInfo.
+ */
+ [implicit_jscontext]
+ void updatePlaces(in jsval aPlaceInfo,
+ [optional] in mozIVisitInfoCallback aCallback);
+
+ /**
+ * Checks if a given URI has been visited.
+ *
+ * @param aURI
+ * The URI to check for.
+ * @param aCallback
+ * A mozIVisitStatusCallback object which receives the visited status.
+ */
+ void isURIVisited(in nsIURI aURI,
+ in mozIVisitedStatusCallback aCallback);
+};
diff --git a/toolkit/components/places/mozIAsyncLivemarks.idl b/toolkit/components/places/mozIAsyncLivemarks.idl
new file mode 100644
index 0000000000..e84ecca8e2
--- /dev/null
+++ b/toolkit/components/places/mozIAsyncLivemarks.idl
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 mozILivemarkInfo;
+interface mozILivemark;
+
+interface nsINavHistoryResultObserver;
+
+[scriptable, uuid(672387b7-a75d-4e8f-9b49-5c1dcbfff46b)]
+interface mozIAsyncLivemarks : nsISupports
+{
+ /**
+ * Creates a new livemark
+ *
+ * @param aLivemarkInfo
+ * mozILivemarkInfo object containing at least title, parentId,
+ * index and feedURI of the livemark to create.
+ *
+ * @return {Promise}
+ * @throws NS_ERROR_INVALID_ARG if the supplied information is insufficient
+ * for the creation.
+ */
+ jsval addLivemark(in jsval aLivemarkInfo);
+
+ /**
+ * Removes an existing livemark.
+ *
+ * @param aLivemarkInfo
+ * mozILivemarkInfo object containing either an id or a guid of the
+ * livemark to remove.
+ *
+ * @return {Promise}
+ * @throws NS_ERROR_INVALID_ARG if the id/guid is invalid.
+ */
+ jsval removeLivemark(in jsval aLivemarkInfo);
+
+ /**
+ * Gets an existing livemark.
+ *
+ * @param aLivemarkInfo
+ * mozILivemarkInfo object containing either an id or a guid of the
+ * livemark to retrieve.
+ *
+ * @return {Promise}
+ * @throws NS_ERROR_INVALID_ARG if the id/guid is invalid or an invalid
+ * callback is provided.
+ */
+ jsval getLivemark(in jsval aLivemarkInfo);
+
+ /**
+ * Reloads all livemarks if they are expired or if forced to do so.
+ *
+ * @param [optional]aForceUpdate
+ * If set to true forces a reload even if contents are still valid.
+ *
+ * @note The update process is asynchronous, observers registered through
+ * registerForUpdates will be notified of updated contents.
+ */
+ void reloadLivemarks([optional]in boolean aForceUpdate);
+};
+
+[scriptable, uuid(3a3c5e8f-ec4a-4086-ae0a-d16420d30c9f)]
+interface mozILivemarkInfo : nsISupports
+{
+ /**
+ * Id of the bookmarks folder representing this livemark.
+ *
+ * @deprecated Use guid instead.
+ */
+ readonly attribute long long id;
+
+ /**
+ * The globally unique identifier of this livemark.
+ */
+ readonly attribute ACString guid;
+
+ /**
+ * Title of this livemark.
+ */
+ readonly attribute AString title;
+
+ /**
+ * Id of the bookmarks parent folder containing this livemark.
+ *
+ * @deprecated Use parentGuid instead.
+ */
+ readonly attribute long long parentId;
+
+ /**
+ * Guid of the bookmarks parent folder containing this livemark.
+ */
+ readonly attribute long long parentGuid;
+
+ /**
+ * The position of this livemark in the bookmarks parent folder.
+ */
+ readonly attribute long index;
+
+ /**
+ * Time this livemark was created.
+ */
+ readonly attribute PRTime dateAdded;
+
+ /**
+ * Time this livemark's details were last modified. Doesn't track changes to
+ * the livemark contents.
+ */
+ readonly attribute PRTime lastModified;
+
+ /**
+ * The URI of the syndication feed associated with this livemark.
+ */
+ readonly attribute nsIURI feedURI;
+
+ /**
+ * The URI of the website associated with this livemark.
+ */
+ readonly attribute nsIURI siteURI;
+};
+
+[scriptable, uuid(9f6fdfae-db9a-4bd8-bde1-148758cf1b18)]
+interface mozILivemark : mozILivemarkInfo
+{
+ // Indicates the livemark is inactive.
+ const unsigned short STATUS_READY = 0;
+ // Indicates the livemark is fetching new contents.
+ const unsigned short STATUS_LOADING = 1;
+ // Indicates the livemark failed to fetch new contents.
+ const unsigned short STATUS_FAILED = 2;
+
+ /**
+ * Status of this livemark. One of the STATUS_* constants above.
+ */
+ readonly attribute unsigned short status;
+
+ /**
+ * Reload livemark contents if they are expired or if forced to do so.
+ *
+ * @param [optional]aForceUpdate
+ * If set to true forces a reload even if contents are still valid.
+ *
+ * @note The update process is asynchronous, it's possible to register a
+ * result observer to be notified of updated contents through
+ * registerForUpdates.
+ */
+ void reload([optional]in boolean aForceUpdate);
+
+ /**
+ * Returns an array of nsINavHistoryResultNode objects, representing children
+ * of this livemark. The nodes will have aContainerNode as parent.
+ *
+ * @param aContainerNode
+ * Object implementing nsINavHistoryContainerResultNode, to be used as
+ * parent of the livemark nodes.
+ */
+ jsval getNodesForContainer(in jsval aContainerNode);
+
+ /**
+ * Registers a container node for updates on this livemark.
+ * When the livemark contents change, an invalidateContainer(aContainerNode)
+ * request is sent to aResultObserver.
+ *
+ * @param aContainerNode
+ * Object implementing nsINavHistoryContainerResultNode, representing
+ * this livemark.
+ * @param aResultObserver
+ * The nsINavHistoryResultObserver that should be notified of changes
+ * to the livemark contents.
+ */
+ void registerForUpdates(in jsval aContainerNode,
+ in nsINavHistoryResultObserver aResultObserver);
+
+ /**
+ * Unregisters a previously registered container node.
+ *
+ * @param aContainerNode
+ * Object implementing nsINavHistoryContainerResultNode, representing
+ * this livemark.
+ *
+ * @note it's suggested to always unregister containers that are no more used,
+ * to free up the associated resources. A good time to do so is when
+ * the container gets closed.
+ */
+ void unregisterForUpdates(in jsval aContainerNode);
+};
diff --git a/toolkit/components/places/mozIColorAnalyzer.idl b/toolkit/components/places/mozIColorAnalyzer.idl
new file mode 100644
index 0000000000..368958cbb5
--- /dev/null
+++ b/toolkit/components/places/mozIColorAnalyzer.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+[function, scriptable, uuid(e4089e21-71b6-40af-b546-33c21b90e874)]
+interface mozIRepresentativeColorCallback : nsISupports
+{
+ /**
+ * Will be called when color analysis finishes.
+ *
+ * @param success
+ * True if analysis was successful, false otherwise.
+ * Analysis can fail if the image is transparent, imageURI doesn't
+ * resolve to a valid image, or the image is too big.
+ *
+ * @param color
+ * The representative color as an integer in RGB form.
+ * e.g. 0xFF0102 == rgb(255,1,2)
+ * If success is false, color is not provided.
+ */
+ void onComplete(in boolean success, [optional] in unsigned long color);
+};
+
+[scriptable, uuid(d056186c-28a0-494e-aacc-9e433772b143)]
+interface mozIColorAnalyzer : nsISupports
+{
+ /**
+ * Given an image URI, find the most representative color for that image
+ * based on the frequency of each color. Preference is given to colors that
+ * are more interesting. Avoids the background color if it can be
+ * discerned. Ignores sufficiently transparent colors.
+ *
+ * This is intended to be used on favicon images. Larger images take longer
+ * to process, especially those with a larger number of unique colors. If
+ * imageURI points to an image that has more than 128^2 pixels, this method
+ * will fail before analyzing it for performance reasons.
+ *
+ * @param imageURI
+ * A URI pointing to the image - ideally a data: URI, but any scheme
+ * that will load when setting the src attribute of a DOM img element
+ * should work.
+ * @param callback
+ * Function to call when the representative color is found or an
+ * error occurs.
+ */
+ void findRepresentativeColor(in nsIURI imageURI,
+ in mozIRepresentativeColorCallback callback);
+};
diff --git a/toolkit/components/places/mozIPlacesAutoComplete.idl b/toolkit/components/places/mozIPlacesAutoComplete.idl
new file mode 100644
index 0000000000..7f3247fdc3
--- /dev/null
+++ b/toolkit/components/places/mozIPlacesAutoComplete.idl
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=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;
+
+/**
+ * This interface provides some constants used by the Places AutoComplete
+ * search provider as well as methods to track opened pages for AutoComplete
+ * purposes.
+ */
+[scriptable, uuid(61b6348a-09e1-4810-8057-f8cb3cec6ef8)]
+interface mozIPlacesAutoComplete : nsISupports
+{
+ //////////////////////////////////////////////////////////////////////////////
+ //// Matching Constants
+
+ /**
+ * Match anywhere in each searchable term.
+ */
+ const long MATCH_ANYWHERE = 0;
+
+ /**
+ * Match first on word boundaries, and if we do not get enough results, then
+ * match anywhere in each searchable term.
+ */
+ const long MATCH_BOUNDARY_ANYWHERE = 1;
+
+ /**
+ * Match on word boundaries in each searchable term.
+ */
+ const long MATCH_BOUNDARY = 2;
+
+ /**
+ * Match only the beginning of each search term.
+ */
+ const long MATCH_BEGINNING = 3;
+
+ /**
+ * Match anywhere in each searchable term without doing any transformation
+ * or stripping on the underlying data.
+ */
+ const long MATCH_ANYWHERE_UNMODIFIED = 4;
+
+ /**
+ * Match only the beginning of each search term using a case sensitive
+ * comparator.
+ */
+ const long MATCH_BEGINNING_CASE_SENSITIVE = 5;
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Search Behavior Constants
+
+ /**
+ * Search through history.
+ */
+ const long BEHAVIOR_HISTORY = 1 << 0;
+
+ /**
+ * Search though bookmarks.
+ */
+ const long BEHAVIOR_BOOKMARK = 1 << 1;
+
+ /**
+ * Search through tags.
+ */
+ const long BEHAVIOR_TAG = 1 << 2;
+
+ /**
+ * Search the title of pages.
+ */
+ const long BEHAVIOR_TITLE = 1 << 3;
+
+ /**
+ * Search the URL of pages.
+ */
+ const long BEHAVIOR_URL = 1 << 4;
+
+ /**
+ * Search for typed pages.
+ */
+ const long BEHAVIOR_TYPED = 1 << 5;
+
+ /**
+ * Search javascript: URLs.
+ */
+ const long BEHAVIOR_JAVASCRIPT = 1 << 6;
+
+ /**
+ * Search for pages that have been marked as being opened, such as a tab
+ * in a tabbrowser.
+ */
+ const long BEHAVIOR_OPENPAGE = 1 << 7;
+
+ /**
+ * Use intersection between history, typed, bookmark, tag and openpage
+ * instead of union, when the restrict bit is set.
+ */
+ const long BEHAVIOR_RESTRICT = 1 << 8;
+
+ /**
+ * Include search suggestions from the currently selected search provider.
+ */
+ const long BEHAVIOR_SEARCHES = 1 << 9;
+
+ /**
+ * Mark a page as being currently open.
+ *
+ * @note Pages will not be automatically unregistered when Private Browsing
+ * mode is entered or exited. Therefore, consumers MUST unregister or
+ * register themselves.
+ *
+ * @param aURI
+ * The URI to register as an open page.
+ * @param aUserContextId
+ * The Container Id of the tab.
+ */
+ void registerOpenPage(in nsIURI aURI, in uint32_t aUserContextId);
+
+ /**
+ * Mark a page as no longer being open (either by closing the window or tab,
+ * or by navigating away from that page).
+ *
+ * @note Pages will not be automatically unregistered when Private Browsing
+ * mode is entered or exited. Therefore, consumers MUST unregister or
+ * register themselves.
+ *
+ * @param aURI
+ * The URI to unregister as an open page.
+ * @param aUserContextId
+ * The Container Id of the tab.
+ */
+ void unregisterOpenPage(in nsIURI aURI, in uint32_t aUserContextId);
+};
diff --git a/toolkit/components/places/mozIPlacesPendingOperation.idl b/toolkit/components/places/mozIPlacesPendingOperation.idl
new file mode 100644
index 0000000000..678a908708
--- /dev/null
+++ b/toolkit/components/places/mozIPlacesPendingOperation.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(ebd31374-3808-40e4-9e73-303bf70467c3)]
+interface mozIPlacesPendingOperation : nsISupports {
+ /**
+ * Cancels a pending operation, if possible. This will only fail if you try
+ * to cancel more than once.
+ */
+ void cancel();
+};
diff --git a/toolkit/components/places/nsAnnoProtocolHandler.cpp b/toolkit/components/places/nsAnnoProtocolHandler.cpp
new file mode 100644
index 0000000000..b98942e337
--- /dev/null
+++ b/toolkit/components/places/nsAnnoProtocolHandler.cpp
@@ -0,0 +1,367 @@
+//* -*- 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/. */
+
+/**
+ * Implementation of moz-anno: URLs for accessing favicons. The urls are sent
+ * to the favicon service. If the favicon service doesn't have the
+ * data, a stream containing the default favicon will be returned.
+ *
+ * The reference to annotations ("moz-anno") is a leftover from previous
+ * iterations of this component. As of now the moz-anno protocol is independent
+ * of annotations.
+ */
+
+#include "nsAnnoProtocolHandler.h"
+#include "nsFaviconService.h"
+#include "nsIChannel.h"
+#include "nsIInputStreamChannel.h"
+#include "nsILoadGroup.h"
+#include "nsIStandardURL.h"
+#include "nsIStringStream.h"
+#include "nsISupportsUtils.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsIOutputStream.h"
+#include "nsContentUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStringStream.h"
+#include "mozilla/storage.h"
+#include "nsIPipe.h"
+#include "Helpers.h"
+
+using namespace mozilla;
+using namespace mozilla::places;
+
+////////////////////////////////////////////////////////////////////////////////
+//// Global Functions
+
+/**
+ * Creates a channel to obtain the default favicon.
+ */
+static
+nsresult
+GetDefaultIcon(nsILoadInfo *aLoadInfo, nsIChannel **aChannel)
+{
+ nsCOMPtr<nsIURI> defaultIconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(defaultIconURI),
+ NS_LITERAL_CSTRING(FAVICON_DEFAULT_URL));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_NewChannelInternal(aChannel, defaultIconURI, aLoadInfo);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// faviconAsyncLoader
+
+namespace {
+
+/**
+ * An instance of this class is passed to the favicon service as the callback
+ * for getting favicon data from the database. We'll get this data back in
+ * HandleResult, and on HandleCompletion, we'll close our output stream which
+ * will close the original channel for the favicon request.
+ *
+ * However, if an error occurs at any point, we do not set mReturnDefaultIcon to
+ * false, so we will open up another channel to get the default favicon, and
+ * pass that along to our output stream in HandleCompletion. If anything
+ * happens at that point, the world must be against us, so we return nothing.
+ */
+class faviconAsyncLoader : public AsyncStatementCallback
+ , public nsIRequestObserver
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ faviconAsyncLoader(nsIChannel *aChannel, nsIOutputStream *aOutputStream) :
+ mChannel(aChannel)
+ , mOutputStream(aOutputStream)
+ , mReturnDefaultIcon(true)
+ {
+ NS_ASSERTION(aChannel,
+ "Not providing a channel will result in crashes!");
+ NS_ASSERTION(aOutputStream,
+ "Not providing an output stream will result in crashes!");
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// mozIStorageStatementCallback
+
+ NS_IMETHOD HandleResult(mozIStorageResultSet *aResultSet) override
+ {
+ // We will only get one row back in total, so we do not need to loop.
+ nsCOMPtr<mozIStorageRow> row;
+ nsresult rv = aResultSet->GetNextRow(getter_AddRefs(row));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We do not allow favicons without a MIME type, so we'll return the default
+ // icon.
+ nsAutoCString mimeType;
+ (void)row->GetUTF8String(1, mimeType);
+ NS_ENSURE_FALSE(mimeType.IsEmpty(), NS_OK);
+
+ // Set our mimeType now that we know it.
+ rv = mChannel->SetContentType(mimeType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Obtain the binary blob that contains our favicon data.
+ uint8_t *favicon;
+ uint32_t size = 0;
+ rv = row->GetBlob(0, &size, &favicon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t totalWritten = 0;
+ do {
+ uint32_t bytesWritten;
+ rv = mOutputStream->Write(
+ &(reinterpret_cast<const char *>(favicon)[totalWritten]),
+ size - totalWritten,
+ &bytesWritten
+ );
+ if (NS_FAILED(rv) || !bytesWritten)
+ break;
+ totalWritten += bytesWritten;
+ } while (size != totalWritten);
+ NS_ASSERTION(NS_FAILED(rv) || size == totalWritten,
+ "Failed to write all of our data out to the stream!");
+
+ // Free our favicon array.
+ free(favicon);
+
+ // Handle an error to write if it occurred, but only after we've freed our
+ // favicon.
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // At this point, we should have written out all of our data to our stream.
+ // HandleCompletion will close the output stream, so we are done here.
+ mReturnDefaultIcon = false;
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override
+ {
+ if (!mReturnDefaultIcon)
+ return mOutputStream->Close();
+
+ // We need to return our default icon, so we'll open up a new channel to get
+ // that data, and push it to our output stream. If at any point we get an
+ // error, we can't do anything, so we'll just close our output stream.
+ nsCOMPtr<nsIStreamListener> listener;
+ nsresult rv = NS_NewSimpleStreamListener(getter_AddRefs(listener),
+ mOutputStream, this);
+ NS_ENSURE_SUCCESS(rv, mOutputStream->Close());
+
+ // we should pass the loadInfo of the original channel along
+ // to the new channel. Note that mChannel can not be null,
+ // constructor checks that.
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
+ nsCOMPtr<nsIChannel> newChannel;
+ rv = GetDefaultIcon(loadInfo, getter_AddRefs(newChannel));
+ NS_ENSURE_SUCCESS(rv, mOutputStream->Close());
+
+ rv = newChannel->AsyncOpen2(listener);
+ NS_ENSURE_SUCCESS(rv, mOutputStream->Close());
+
+ return NS_OK;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIRequestObserver
+
+ NS_IMETHOD OnStartRequest(nsIRequest *, nsISupports *) override
+ {
+ return NS_OK;
+ }
+
+ NS_IMETHOD OnStopRequest(nsIRequest *, nsISupports *, nsresult aStatusCode) override
+ {
+ // We always need to close our output stream, regardless of the status code.
+ (void)mOutputStream->Close();
+
+ // But, we'll warn about it not being successful if it wasn't.
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(aStatusCode),
+ "Got an error when trying to load our default favicon!");
+
+ return NS_OK;
+ }
+
+protected:
+ virtual ~faviconAsyncLoader() {}
+
+private:
+ nsCOMPtr<nsIChannel> mChannel;
+ nsCOMPtr<nsIOutputStream> mOutputStream;
+ bool mReturnDefaultIcon;
+};
+
+NS_IMPL_ISUPPORTS_INHERITED(
+ faviconAsyncLoader,
+ AsyncStatementCallback,
+ nsIRequestObserver
+)
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsAnnoProtocolHandler
+
+NS_IMPL_ISUPPORTS(nsAnnoProtocolHandler, nsIProtocolHandler)
+
+// nsAnnoProtocolHandler::GetScheme
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::GetScheme(nsACString& aScheme)
+{
+ aScheme.AssignLiteral("moz-anno");
+ return NS_OK;
+}
+
+
+// nsAnnoProtocolHandler::GetDefaultPort
+//
+// There is no default port for annotation URLs
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::GetDefaultPort(int32_t *aDefaultPort)
+{
+ *aDefaultPort = -1;
+ return NS_OK;
+}
+
+
+// nsAnnoProtocolHandler::GetProtocolFlags
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::GetProtocolFlags(uint32_t *aProtocolFlags)
+{
+ *aProtocolFlags = (URI_NORELATIVE | URI_NOAUTH | URI_DANGEROUS_TO_LOAD |
+ URI_IS_LOCAL_RESOURCE);
+ return NS_OK;
+}
+
+
+// nsAnnoProtocolHandler::NewURI
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::NewURI(const nsACString& aSpec,
+ const char *aOriginCharset,
+ nsIURI *aBaseURI, nsIURI **_retval)
+{
+ nsCOMPtr <nsIURI> uri = do_CreateInstance(NS_SIMPLEURI_CONTRACTID);
+ if (!uri)
+ return NS_ERROR_OUT_OF_MEMORY;
+ nsresult rv = uri->SetSpec(aSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_retval = nullptr;
+ uri.swap(*_retval);
+ return NS_OK;
+}
+
+
+// nsAnnoProtocolHandler::NewChannel
+//
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::NewChannel2(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo,
+ nsIChannel** _retval)
+{
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ // annotation info
+ nsCOMPtr<nsIURI> annoURI;
+ nsAutoCString annoName;
+ nsresult rv = ParseAnnoURI(aURI, getter_AddRefs(annoURI), annoName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only favicon annotation are supported.
+ if (!annoName.EqualsLiteral(FAVICON_ANNOTATION_NAME))
+ return NS_ERROR_INVALID_ARG;
+
+ return NewFaviconChannel(aURI, annoURI, aLoadInfo, _retval);
+}
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::NewChannel(nsIURI *aURI, nsIChannel **_retval)
+{
+ return NewChannel2(aURI, nullptr, _retval);
+}
+
+
+// nsAnnoProtocolHandler::AllowPort
+//
+// Don't override any bans on bad ports.
+
+NS_IMETHODIMP
+nsAnnoProtocolHandler::AllowPort(int32_t port, const char *scheme,
+ bool *_retval)
+{
+ *_retval = false;
+ return NS_OK;
+}
+
+
+// nsAnnoProtocolHandler::ParseAnnoURI
+//
+// Splits an annotation URL into its URI and name parts
+
+nsresult
+nsAnnoProtocolHandler::ParseAnnoURI(nsIURI* aURI,
+ nsIURI** aResultURI, nsCString& aName)
+{
+ nsresult rv;
+ nsAutoCString path;
+ rv = aURI->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t firstColon = path.FindChar(':');
+ if (firstColon <= 0)
+ return NS_ERROR_MALFORMED_URI;
+
+ rv = NS_NewURI(aResultURI, Substring(path, firstColon + 1));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aName = Substring(path, 0, firstColon);
+ return NS_OK;
+}
+
+nsresult
+nsAnnoProtocolHandler::NewFaviconChannel(nsIURI *aURI, nsIURI *aAnnotationURI,
+ nsILoadInfo* aLoadInfo, nsIChannel **_channel)
+{
+ // Create our pipe. This will give us our input stream and output stream
+ // that will be written to when we get data from the database.
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsresult rv = NS_NewPipe(getter_AddRefs(inputStream),
+ getter_AddRefs(outputStream),
+ 0, nsIFaviconService::MAX_FAVICON_BUFFER_SIZE,
+ true, true);
+ NS_ENSURE_SUCCESS(rv, GetDefaultIcon(aLoadInfo, _channel));
+
+ // Create our channel. We'll call SetContentType with the right type when
+ // we know what it actually is.
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel),
+ aURI,
+ inputStream,
+ EmptyCString(), // aContentType
+ EmptyCString(), // aContentCharset
+ aLoadInfo);
+ NS_ENSURE_SUCCESS(rv, GetDefaultIcon(aLoadInfo, _channel));
+
+ // Now we go ahead and get our data asynchronously for the favicon.
+ nsCOMPtr<mozIStorageStatementCallback> callback =
+ new faviconAsyncLoader(channel, outputStream);
+ NS_ENSURE_TRUE(callback, GetDefaultIcon(aLoadInfo, _channel));
+ nsFaviconService* faviconService = nsFaviconService::GetFaviconService();
+ NS_ENSURE_TRUE(faviconService, GetDefaultIcon(aLoadInfo, _channel));
+
+ rv = faviconService->GetFaviconDataAsync(aAnnotationURI, callback);
+ NS_ENSURE_SUCCESS(rv, GetDefaultIcon(aLoadInfo, _channel));
+
+ channel.forget(_channel);
+ return NS_OK;
+}
diff --git a/toolkit/components/places/nsAnnoProtocolHandler.h b/toolkit/components/places/nsAnnoProtocolHandler.h
new file mode 100644
index 0000000000..8e543c7c5f
--- /dev/null
+++ b/toolkit/components/places/nsAnnoProtocolHandler.h
@@ -0,0 +1,54 @@
+//* -*- 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/. */
+
+#ifndef nsAnnoProtocolHandler_h___
+#define nsAnnoProtocolHandler_h___
+
+#include "nsCOMPtr.h"
+#include "nsIProtocolHandler.h"
+#include "nsIURI.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "mozilla/Attributes.h"
+
+// {e8b8bdb7-c96c-4d82-9c6f-2b3c585ec7ea}
+#define NS_ANNOPROTOCOLHANDLER_CID \
+{ 0xe8b8bdb7, 0xc96c, 0x4d82, { 0x9c, 0x6f, 0x2b, 0x3c, 0x58, 0x5e, 0xc7, 0xea } }
+
+class nsAnnoProtocolHandler final : public nsIProtocolHandler, public nsSupportsWeakReference
+{
+public:
+ nsAnnoProtocolHandler() {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROTOCOLHANDLER
+
+private:
+ ~nsAnnoProtocolHandler() {}
+
+protected:
+ nsresult ParseAnnoURI(nsIURI* aURI, nsIURI** aResultURI, nsCString& aName);
+
+ /**
+ * Obtains a new channel to be used to get a favicon from the database. This
+ * method is asynchronous.
+ *
+ * @param aURI
+ * The URI the channel will be created for. This is the URI that is
+ * set as the original URI on the channel.
+ * @param aAnnotationURI
+ * The URI that holds the data needed to get the favicon from the
+ * database.
+ * @param aLoadInfo
+ * The loadinfo that requested the resource load.
+ * @returns (via _channel) the channel that will obtain the favicon data.
+ */
+ nsresult NewFaviconChannel(nsIURI *aURI,
+ nsIURI *aAnnotationURI,
+ nsILoadInfo *aLoadInfo,
+ nsIChannel **_channel);
+};
+
+#endif /* nsAnnoProtocolHandler_h___ */
diff --git a/toolkit/components/places/nsAnnotationService.cpp b/toolkit/components/places/nsAnnotationService.cpp
new file mode 100644
index 0000000000..9d62bd34a8
--- /dev/null
+++ b/toolkit/components/places/nsAnnotationService.cpp
@@ -0,0 +1,1990 @@
+/* -*- 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 "mozilla/ArrayUtils.h"
+
+#include "nsAnnotationService.h"
+#include "nsNavHistory.h"
+#include "nsPlacesTables.h"
+#include "nsPlacesIndexes.h"
+#include "nsPlacesMacros.h"
+#include "Helpers.h"
+
+#include "nsNetUtil.h"
+#include "nsIVariant.h"
+#include "nsString.h"
+#include "nsVariant.h"
+#include "mozilla/storage.h"
+
+#include "GeckoProfiler.h"
+
+#include "nsNetCID.h"
+
+using namespace mozilla;
+using namespace mozilla::places;
+
+#define ENSURE_ANNO_TYPE(_type, _statement) \
+ PR_BEGIN_MACRO \
+ int32_t type = _statement->AsInt32(kAnnoIndex_Type); \
+ NS_ENSURE_TRUE(type == nsIAnnotationService::_type, NS_ERROR_INVALID_ARG); \
+ PR_END_MACRO
+
+#define NOTIFY_ANNOS_OBSERVERS(_notification) \
+ PR_BEGIN_MACRO \
+ for (int32_t i = 0; i < mObservers.Count(); i++) \
+ mObservers[i]->_notification; \
+ PR_END_MACRO
+
+const int32_t nsAnnotationService::kAnnoIndex_ID = 0;
+const int32_t nsAnnotationService::kAnnoIndex_PageOrItem = 1;
+const int32_t nsAnnotationService::kAnnoIndex_NameID = 2;
+const int32_t nsAnnotationService::kAnnoIndex_Content = 3;
+const int32_t nsAnnotationService::kAnnoIndex_Flags = 4;
+const int32_t nsAnnotationService::kAnnoIndex_Expiration = 5;
+const int32_t nsAnnotationService::kAnnoIndex_Type = 6;
+const int32_t nsAnnotationService::kAnnoIndex_DateAdded = 7;
+const int32_t nsAnnotationService::kAnnoIndex_LastModified = 8;
+
+namespace mozilla {
+namespace places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// AnnotatedResult
+
+AnnotatedResult::AnnotatedResult(const nsCString& aGUID,
+ nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aAnnotationName,
+ nsIVariant* aAnnotationValue)
+: mGUID(aGUID)
+, mURI(aURI)
+, mItemId(aItemId)
+, mAnnotationName(aAnnotationName)
+, mAnnotationValue(aAnnotationValue)
+{
+}
+
+AnnotatedResult::~AnnotatedResult()
+{
+}
+
+NS_IMETHODIMP
+AnnotatedResult::GetGuid(nsACString& _guid)
+{
+ _guid = mGUID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AnnotatedResult::GetUri(nsIURI** _uri)
+{
+ NS_IF_ADDREF(*_uri = mURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AnnotatedResult::GetItemId(int64_t* _itemId)
+{
+ *_itemId = mItemId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AnnotatedResult::GetAnnotationName(nsACString& _annotationName)
+{
+ _annotationName = mAnnotationName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AnnotatedResult::GetAnnotationValue(nsIVariant** _annotationValue)
+{
+ NS_IF_ADDREF(*_annotationValue = mAnnotationValue);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(AnnotatedResult, mozIAnnotatedResult)
+
+} // namespace places
+} // namespace mozilla
+
+PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsAnnotationService, gAnnotationService)
+
+NS_IMPL_ISUPPORTS(nsAnnotationService
+, nsIAnnotationService
+, nsIObserver
+, nsISupportsWeakReference
+)
+
+
+nsAnnotationService::nsAnnotationService()
+ : mHasSessionAnnotations(false)
+{
+ NS_ASSERTION(!gAnnotationService,
+ "Attempting to create two instances of the service!");
+ gAnnotationService = this;
+}
+
+
+nsAnnotationService::~nsAnnotationService()
+{
+ NS_ASSERTION(gAnnotationService == this,
+ "Deleting a non-singleton instance of the service");
+ if (gAnnotationService == this)
+ gAnnotationService = nullptr;
+}
+
+
+nsresult
+nsAnnotationService::Init()
+{
+ mDB = Database::GetDatabase();
+ NS_ENSURE_STATE(mDB);
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ if (obsSvc) {
+ (void)obsSvc->AddObserver(this, TOPIC_PLACES_SHUTDOWN, true);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsAnnotationService::SetAnnotationStringInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ const nsAString& aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartSetAnnotation(aURI, aItemId, aName, aFlags, aExpiration,
+ nsIAnnotationService::TYPE_STRING,
+ statement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(statement);
+
+ rv = statement->BindStringByName(NS_LITERAL_CSTRING("content"), aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetPageAnnotation(nsIURI* aURI,
+ const nsACString& aName,
+ nsIVariant* aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG(aValue);
+
+ uint16_t dataType;
+ nsresult rv = aValue->GetDataType(&dataType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (dataType) {
+ case nsIDataType::VTYPE_INT8:
+ case nsIDataType::VTYPE_UINT8:
+ case nsIDataType::VTYPE_INT16:
+ case nsIDataType::VTYPE_UINT16:
+ case nsIDataType::VTYPE_INT32:
+ case nsIDataType::VTYPE_UINT32:
+ case nsIDataType::VTYPE_BOOL: {
+ int32_t valueInt;
+ rv = aValue->GetAsInt32(&valueInt);
+ if (NS_SUCCEEDED(rv)) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPageAnnotationInt32(aURI, aName, valueInt, aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ // Fall through int64_t case otherwise.
+ MOZ_FALLTHROUGH;
+ }
+ case nsIDataType::VTYPE_INT64:
+ case nsIDataType::VTYPE_UINT64: {
+ int64_t valueLong;
+ rv = aValue->GetAsInt64(&valueLong);
+ if (NS_SUCCEEDED(rv)) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPageAnnotationInt64(aURI, aName, valueLong, aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ // Fall through double case otherwise.
+ MOZ_FALLTHROUGH;
+ }
+ case nsIDataType::VTYPE_FLOAT:
+ case nsIDataType::VTYPE_DOUBLE: {
+ double valueDouble;
+ rv = aValue->GetAsDouble(&valueDouble);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPageAnnotationDouble(aURI, aName, valueDouble, aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ case nsIDataType::VTYPE_CHAR:
+ case nsIDataType::VTYPE_WCHAR:
+ case nsIDataType::VTYPE_DOMSTRING:
+ case nsIDataType::VTYPE_CHAR_STR:
+ case nsIDataType::VTYPE_WCHAR_STR:
+ case nsIDataType::VTYPE_STRING_SIZE_IS:
+ case nsIDataType::VTYPE_WSTRING_SIZE_IS:
+ case nsIDataType::VTYPE_UTF8STRING:
+ case nsIDataType::VTYPE_CSTRING:
+ case nsIDataType::VTYPE_ASTRING: {
+ nsAutoString stringValue;
+ rv = aValue->GetAsAString(stringValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPageAnnotationString(aURI, aName, stringValue, aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetItemAnnotation(int64_t aItemId,
+ const nsACString& aName,
+ nsIVariant* aValue,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aSource)
+{
+ PROFILER_LABEL("AnnotationService", "SetItemAnnotation",
+ js::ProfileEntry::Category::OTHER);
+
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG(aValue);
+
+ if (aExpiration == EXPIRE_WITH_HISTORY)
+ return NS_ERROR_INVALID_ARG;
+
+ uint16_t dataType;
+ nsresult rv = aValue->GetDataType(&dataType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (dataType) {
+ case nsIDataType::VTYPE_INT8:
+ case nsIDataType::VTYPE_UINT8:
+ case nsIDataType::VTYPE_INT16:
+ case nsIDataType::VTYPE_UINT16:
+ case nsIDataType::VTYPE_INT32:
+ case nsIDataType::VTYPE_UINT32:
+ case nsIDataType::VTYPE_BOOL: {
+ int32_t valueInt;
+ rv = aValue->GetAsInt32(&valueInt);
+ if (NS_SUCCEEDED(rv)) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetItemAnnotationInt32(aItemId, aName, valueInt, aFlags, aExpiration, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ // Fall through int64_t case otherwise.
+ MOZ_FALLTHROUGH;
+ }
+ case nsIDataType::VTYPE_INT64:
+ case nsIDataType::VTYPE_UINT64: {
+ int64_t valueLong;
+ rv = aValue->GetAsInt64(&valueLong);
+ if (NS_SUCCEEDED(rv)) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetItemAnnotationInt64(aItemId, aName, valueLong, aFlags, aExpiration, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ // Fall through double case otherwise.
+ MOZ_FALLTHROUGH;
+ }
+ case nsIDataType::VTYPE_FLOAT:
+ case nsIDataType::VTYPE_DOUBLE: {
+ double valueDouble;
+ rv = aValue->GetAsDouble(&valueDouble);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetItemAnnotationDouble(aItemId, aName, valueDouble, aFlags, aExpiration, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ case nsIDataType::VTYPE_CHAR:
+ case nsIDataType::VTYPE_WCHAR:
+ case nsIDataType::VTYPE_DOMSTRING:
+ case nsIDataType::VTYPE_CHAR_STR:
+ case nsIDataType::VTYPE_WCHAR_STR:
+ case nsIDataType::VTYPE_STRING_SIZE_IS:
+ case nsIDataType::VTYPE_WSTRING_SIZE_IS:
+ case nsIDataType::VTYPE_UTF8STRING:
+ case nsIDataType::VTYPE_CSTRING:
+ case nsIDataType::VTYPE_ASTRING: {
+ nsAutoString stringValue;
+ rv = aValue->GetAsAString(stringValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetItemAnnotationString(aItemId, aName, stringValue, aFlags, aExpiration, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetPageAnnotationString(nsIURI* aURI,
+ const nsACString& aName,
+ const nsAString& aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsresult rv = SetAnnotationStringInternal(aURI, 0, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationSet(aURI, aName));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetItemAnnotationString(int64_t aItemId,
+ const nsACString& aName,
+ const nsAString& aValue,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ if (aExpiration == EXPIRE_WITH_HISTORY)
+ return NS_ERROR_INVALID_ARG;
+
+ nsresult rv = SetAnnotationStringInternal(nullptr, aItemId, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+
+ return NS_OK;
+}
+
+
+nsresult
+nsAnnotationService::SetAnnotationInt32Internal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ int32_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartSetAnnotation(aURI, aItemId, aName, aFlags, aExpiration,
+ nsIAnnotationService::TYPE_INT32,
+ statement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(statement);
+
+ rv = statement->BindInt32ByName(NS_LITERAL_CSTRING("content"), aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetPageAnnotationInt32(nsIURI* aURI,
+ const nsACString& aName,
+ int32_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsresult rv = SetAnnotationInt32Internal(aURI, 0, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationSet(aURI, aName));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetItemAnnotationInt32(int64_t aItemId,
+ const nsACString& aName,
+ int32_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ if (aExpiration == EXPIRE_WITH_HISTORY)
+ return NS_ERROR_INVALID_ARG;
+
+ nsresult rv = SetAnnotationInt32Internal(nullptr, aItemId, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+
+ return NS_OK;
+}
+
+
+nsresult
+nsAnnotationService::SetAnnotationInt64Internal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ int64_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartSetAnnotation(aURI, aItemId, aName, aFlags, aExpiration,
+ nsIAnnotationService::TYPE_INT64,
+ statement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(statement);
+
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("content"), aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetPageAnnotationInt64(nsIURI* aURI,
+ const nsACString& aName,
+ int64_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsresult rv = SetAnnotationInt64Internal(aURI, 0, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationSet(aURI, aName));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetItemAnnotationInt64(int64_t aItemId,
+ const nsACString& aName,
+ int64_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ if (aExpiration == EXPIRE_WITH_HISTORY)
+ return NS_ERROR_INVALID_ARG;
+
+ nsresult rv = SetAnnotationInt64Internal(nullptr, aItemId, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+
+ return NS_OK;
+}
+
+
+nsresult
+nsAnnotationService::SetAnnotationDoubleInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ double aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartSetAnnotation(aURI, aItemId, aName, aFlags, aExpiration,
+ nsIAnnotationService::TYPE_DOUBLE,
+ statement);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozStorageStatementScoper scoper(statement);
+
+ rv = statement->BindDoubleByName(NS_LITERAL_CSTRING("content"), aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetPageAnnotationDouble(nsIURI* aURI,
+ const nsACString& aName,
+ double aValue,
+ int32_t aFlags,
+ uint16_t aExpiration)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsresult rv = SetAnnotationDoubleInternal(aURI, 0, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationSet(aURI, aName));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::SetItemAnnotationDouble(int64_t aItemId,
+ const nsACString& aName,
+ double aValue,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ if (aExpiration == EXPIRE_WITH_HISTORY)
+ return NS_ERROR_INVALID_ARG;
+
+ nsresult rv = SetAnnotationDoubleInternal(nullptr, aItemId, aName, aValue,
+ aFlags, aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aItemId, aName, aSource));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationString(nsIURI* aURI,
+ const nsACString& aName,
+ nsAString& _retval)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_STRING, statement);
+ rv = statement->GetString(kAnnoIndex_Content, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationString(int64_t aItemId,
+ const nsACString& aName,
+ nsAString& _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_STRING, statement);
+ rv = statement->GetString(kAnnoIndex_Content, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotation(nsIURI* aURI,
+ const nsACString& aName,
+ nsIVariant** _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+
+ nsCOMPtr<nsIWritableVariant> value = new nsVariant();
+ int32_t type = statement->AsInt32(kAnnoIndex_Type);
+ switch (type) {
+ case nsIAnnotationService::TYPE_INT32:
+ case nsIAnnotationService::TYPE_INT64:
+ case nsIAnnotationService::TYPE_DOUBLE: {
+ rv = value->SetAsDouble(statement->AsDouble(kAnnoIndex_Content));
+ break;
+ }
+ case nsIAnnotationService::TYPE_STRING: {
+ nsAutoString valueString;
+ rv = statement->GetString(kAnnoIndex_Content, valueString);
+ if (NS_SUCCEEDED(rv))
+ rv = value->SetAsAString(valueString);
+ break;
+ }
+ default: {
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ value.forget(_retval);
+ }
+
+ return rv;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotation(int64_t aItemId,
+ const nsACString& aName,
+ nsIVariant** _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+
+ nsCOMPtr<nsIWritableVariant> value = new nsVariant();
+ int32_t type = statement->AsInt32(kAnnoIndex_Type);
+ switch (type) {
+ case nsIAnnotationService::TYPE_INT32:
+ case nsIAnnotationService::TYPE_INT64:
+ case nsIAnnotationService::TYPE_DOUBLE: {
+ rv = value->SetAsDouble(statement->AsDouble(kAnnoIndex_Content));
+ break;
+ }
+ case nsIAnnotationService::TYPE_STRING: {
+ nsAutoString valueString;
+ rv = statement->GetString(kAnnoIndex_Content, valueString);
+ if (NS_SUCCEEDED(rv))
+ rv = value->SetAsAString(valueString);
+ break;
+ }
+ default: {
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ value.forget(_retval);
+ }
+
+ return rv;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationInt32(nsIURI* aURI,
+ const nsACString& aName,
+ int32_t* _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_INT32, statement);
+ *_retval = statement->AsInt32(kAnnoIndex_Content);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationInt32(int64_t aItemId,
+ const nsACString& aName,
+ int32_t* _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_INT32, statement);
+ *_retval = statement->AsInt32(kAnnoIndex_Content);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationInt64(nsIURI* aURI,
+ const nsACString& aName,
+ int64_t* _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_INT64, statement);
+ *_retval = statement->AsInt64(kAnnoIndex_Content);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationInt64(int64_t aItemId,
+ const nsACString& aName,
+ int64_t* _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_INT64, statement);
+ *_retval = statement->AsInt64(kAnnoIndex_Content);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationType(nsIURI* aURI,
+ const nsACString& aName,
+ uint16_t* _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ *_retval = statement->AsInt32(kAnnoIndex_Type);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationType(int64_t aItemId,
+ const nsACString& aName,
+ uint16_t* _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ *_retval = statement->AsInt32(kAnnoIndex_Type);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationDouble(nsIURI* aURI,
+ const nsACString& aName,
+ double* _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_DOUBLE, statement);
+ *_retval = statement->AsDouble(kAnnoIndex_Content);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationDouble(int64_t aItemId,
+ const nsACString& aName,
+ double* _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ ENSURE_ANNO_TYPE(TYPE_DOUBLE, statement);
+ *_retval = statement->AsDouble(kAnnoIndex_Content);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationInfo(nsIURI* aURI,
+ const nsACString& aName,
+ int32_t* _flags,
+ uint16_t* _expiration,
+ uint16_t* _storageType)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_flags);
+ NS_ENSURE_ARG_POINTER(_expiration);
+ NS_ENSURE_ARG_POINTER(_storageType);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(aURI, 0, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ *_flags = statement->AsInt32(kAnnoIndex_Flags);
+ *_expiration = (uint16_t)statement->AsInt32(kAnnoIndex_Expiration);
+ int32_t type = (uint16_t)statement->AsInt32(kAnnoIndex_Type);
+ if (type == 0) {
+ // For annotations created before explicit typing,
+ // we can't determine type, just return as string type.
+ *_storageType = nsIAnnotationService::TYPE_STRING;
+ }
+ else
+ *_storageType = type;
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationInfo(int64_t aItemId,
+ const nsACString& aName,
+ int32_t* _flags,
+ uint16_t* _expiration,
+ uint16_t* _storageType)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_flags);
+ NS_ENSURE_ARG_POINTER(_expiration);
+ NS_ENSURE_ARG_POINTER(_storageType);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ nsresult rv = StartGetAnnotation(nullptr, aItemId, aName, statement);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mozStorageStatementScoper scoper(statement);
+ *_flags = statement->AsInt32(kAnnoIndex_Flags);
+ *_expiration = (uint16_t)statement->AsInt32(kAnnoIndex_Expiration);
+ int32_t type = (uint16_t)statement->AsInt32(kAnnoIndex_Type);
+ if (type == 0) {
+ // For annotations created before explicit typing,
+ // we can't determine type, just return as string type.
+ *_storageType = nsIAnnotationService::TYPE_STRING;
+ }
+ else {
+ *_storageType = type;
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPagesWithAnnotation(const nsACString& aName,
+ uint32_t* _resultCount,
+ nsIURI*** _results)
+{
+ NS_ENSURE_TRUE(!aName.IsEmpty(), NS_ERROR_INVALID_ARG);
+ NS_ENSURE_ARG_POINTER(_resultCount);
+ NS_ENSURE_ARG_POINTER(_results);
+
+ *_resultCount = 0;
+ *_results = nullptr;
+ nsCOMArray<nsIURI> results;
+
+ nsresult rv = GetPagesWithAnnotationCOMArray(aName, &results);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert to raw array.
+ if (results.Count() == 0)
+ return NS_OK;
+
+ *_resultCount = results.Count();
+ results.Forget(_results);
+
+ return NS_OK;
+}
+
+
+nsresult
+nsAnnotationService::GetPagesWithAnnotationCOMArray(const nsACString& aName,
+ nsCOMArray<nsIURI>* _results)
+{
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT h.url "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_annos a ON n.id = a.anno_attribute_id "
+ "JOIN moz_places h ON h.id = a.place_id "
+ "WHERE n.name = :anno_name"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasMore)) &&
+ hasMore) {
+ nsAutoCString uristring;
+ rv = stmt->GetUTF8String(0, uristring);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // convert to a URI, in case of some invalid URI, just ignore this row
+ // so we can mostly continue.
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), uristring);
+ if (NS_FAILED(rv))
+ continue;
+
+ bool added = _results->AppendObject(uri);
+ NS_ENSURE_TRUE(added, NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemsWithAnnotation(const nsACString& aName,
+ uint32_t* _resultCount,
+ int64_t** _results)
+{
+ NS_ENSURE_TRUE(!aName.IsEmpty(), NS_ERROR_INVALID_ARG);
+ NS_ENSURE_ARG_POINTER(_resultCount);
+ NS_ENSURE_ARG_POINTER(_results);
+
+ *_resultCount = 0;
+ *_results = nullptr;
+ nsTArray<int64_t> results;
+
+ nsresult rv = GetItemsWithAnnotationTArray(aName, &results);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert to raw array.
+ if (results.Length() == 0)
+ return NS_OK;
+
+ *_results = static_cast<int64_t*>
+ (moz_xmalloc(results.Length() * sizeof(int64_t)));
+ NS_ENSURE_TRUE(*_results, NS_ERROR_OUT_OF_MEMORY);
+
+ *_resultCount = results.Length();
+ for (uint32_t i = 0; i < *_resultCount; i ++) {
+ (*_results)[i] = results[i];
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetAnnotationsWithName(const nsACString& aName,
+ uint32_t* _count,
+ mozIAnnotatedResult*** _annotations)
+{
+ NS_ENSURE_ARG(!aName.IsEmpty());
+ NS_ENSURE_ARG_POINTER(_annotations);
+
+ *_count = 0;
+ *_annotations = nullptr;
+ nsCOMArray<mozIAnnotatedResult> annotations;
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT h.guid, h.url, -1, a.type, a.content "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_annos a ON n.id = a.anno_attribute_id "
+ "JOIN moz_places h ON h.id = a.place_id "
+ "WHERE n.name = :anno_name "
+ "UNION ALL "
+ "SELECT b.guid, h.url, b.id, a.type, a.content "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_items_annos a ON n.id = a.anno_attribute_id "
+ "JOIN moz_bookmarks b ON b.id = a.item_id "
+ "LEFT JOIN moz_places h ON h.id = b.fk "
+ "WHERE n.name = :anno_name "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"),
+ aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasMore)) && hasMore) {
+ nsAutoCString guid;
+ rv = stmt->GetUTF8String(0, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ bool uriIsNull = false;
+ rv = stmt->GetIsNull(1, &uriIsNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!uriIsNull) {
+ nsAutoCString url;
+ rv = stmt->GetUTF8String(1, url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NS_NewURI(getter_AddRefs(uri), url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int64_t itemId = stmt->AsInt64(2);
+ int32_t type = stmt->AsInt32(3);
+
+ nsCOMPtr<nsIWritableVariant> variant = new nsVariant();
+ switch (type) {
+ case nsIAnnotationService::TYPE_INT32: {
+ rv = variant->SetAsInt32(stmt->AsInt32(4));
+ break;
+ }
+ case nsIAnnotationService::TYPE_INT64: {
+ rv = variant->SetAsInt64(stmt->AsInt64(4));
+ break;
+ }
+ case nsIAnnotationService::TYPE_DOUBLE: {
+ rv = variant->SetAsDouble(stmt->AsDouble(4));
+ break;
+ }
+ case nsIAnnotationService::TYPE_STRING: {
+ nsAutoString valueString;
+ rv = stmt->GetString(4, valueString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = variant->SetAsAString(valueString);
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unsupported annotation type");
+ // Move to the next result.
+ continue;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIAnnotatedResult> anno = new AnnotatedResult(guid, uri, itemId,
+ aName, variant);
+ NS_ENSURE_TRUE(annotations.AppendObject(anno), NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // Convert to raw array.
+ if (annotations.Count() == 0)
+ return NS_OK;
+
+ *_count = annotations.Count();
+ annotations.Forget(_annotations);
+
+ return NS_OK;
+}
+
+
+nsresult
+nsAnnotationService::GetItemsWithAnnotationTArray(const nsACString& aName,
+ nsTArray<int64_t>* _results)
+{
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT a.item_id "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_items_annos a ON n.id = a.anno_attribute_id "
+ "WHERE n.name = :anno_name"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) &&
+ hasMore) {
+ if (!_results->AppendElement(stmt->AsInt64(0)))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetPageAnnotationNames(nsIURI* aURI,
+ uint32_t* _count,
+ nsIVariant*** _result)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_count);
+ NS_ENSURE_ARG_POINTER(_result);
+
+ *_count = 0;
+ *_result = nullptr;
+
+ nsTArray<nsCString> names;
+ nsresult rv = GetAnnotationNamesTArray(aURI, 0, &names);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (names.Length() == 0)
+ return NS_OK;
+
+ *_result = static_cast<nsIVariant**>
+ (moz_xmalloc(sizeof(nsIVariant*) * names.Length()));
+ NS_ENSURE_TRUE(*_result, NS_ERROR_OUT_OF_MEMORY);
+
+ for (uint32_t i = 0; i < names.Length(); i ++) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ if (!var) {
+ // need to release all the variants we've already created
+ for (uint32_t j = 0; j < i; j ++)
+ NS_RELEASE((*_result)[j]);
+ free(*_result);
+ *_result = nullptr;
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ var->SetAsAUTF8String(names[i]);
+ NS_ADDREF((*_result)[i] = var);
+ }
+ *_count = names.Length();
+
+ return NS_OK;
+}
+
+
+nsresult
+nsAnnotationService::GetAnnotationNamesTArray(nsIURI* aURI,
+ int64_t aItemId,
+ nsTArray<nsCString>* _result)
+{
+ _result->Clear();
+
+ bool isItemAnnotation = (aItemId > 0);
+ nsCOMPtr<mozIStorageStatement> statement;
+ if (isItemAnnotation) {
+ statement = mDB->GetStatement(
+ "SELECT n.name "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_items_annos a ON a.anno_attribute_id = n.id "
+ "WHERE a.item_id = :item_id"
+ );
+ }
+ else {
+ statement = mDB->GetStatement(
+ "SELECT n.name "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_annos a ON a.anno_attribute_id = n.id "
+ "JOIN moz_places h ON h.id = a.place_id "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url"
+ );
+ }
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv;
+ if (isItemAnnotation)
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ else
+ rv = URIBinder::Bind(statement, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult = false;
+ while (NS_SUCCEEDED(statement->ExecuteStep(&hasResult)) &&
+ hasResult) {
+ nsAutoCString name;
+ rv = statement->GetUTF8String(0, name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!_result->AppendElement(name))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::GetItemAnnotationNames(int64_t aItemId,
+ uint32_t* _count,
+ nsIVariant*** _result)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_count);
+ NS_ENSURE_ARG_POINTER(_result);
+
+ *_count = 0;
+ *_result = nullptr;
+
+ nsTArray<nsCString> names;
+ nsresult rv = GetAnnotationNamesTArray(nullptr, aItemId, &names);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (names.Length() == 0)
+ return NS_OK;
+
+ *_result = static_cast<nsIVariant**>
+ (moz_xmalloc(sizeof(nsIVariant*) * names.Length()));
+ NS_ENSURE_TRUE(*_result, NS_ERROR_OUT_OF_MEMORY);
+
+ for (uint32_t i = 0; i < names.Length(); i ++) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ if (!var) {
+ // need to release all the variants we've already created
+ for (uint32_t j = 0; j < i; j ++)
+ NS_RELEASE((*_result)[j]);
+ free(*_result);
+ *_result = nullptr;
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ var->SetAsAUTF8String(names[i]);
+ NS_ADDREF((*_result)[i] = var);
+ }
+ *_count = names.Length();
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::PageHasAnnotation(nsIURI* aURI,
+ const nsACString& aName,
+ bool* _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv = HasAnnotationInternal(aURI, 0, aName, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::ItemHasAnnotation(int64_t aItemId,
+ const nsACString& aName,
+ bool* _retval)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv = HasAnnotationInternal(nullptr, aItemId, aName, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+/**
+ * @note We don't remove anything from the moz_anno_attributes table. If we
+ * delete the last item of a given name, that item really should go away.
+ * It will be cleaned up by expiration.
+ */
+nsresult
+nsAnnotationService::RemoveAnnotationInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName)
+{
+ bool isItemAnnotation = (aItemId > 0);
+ nsCOMPtr<mozIStorageStatement> statement;
+ if (isItemAnnotation) {
+ statement = mDB->GetStatement(
+ "DELETE FROM moz_items_annos "
+ "WHERE item_id = :item_id "
+ "AND anno_attribute_id = "
+ "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name)"
+ );
+ }
+ else {
+ statement = mDB->GetStatement(
+ "DELETE FROM moz_annos "
+ "WHERE place_id = "
+ "(SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) "
+ "AND anno_attribute_id = "
+ "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name)"
+ );
+ }
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv;
+ if (isItemAnnotation)
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ else
+ rv = URIBinder::Bind(statement, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::RemovePageAnnotation(nsIURI* aURI,
+ const nsACString& aName)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsresult rv = RemoveAnnotationInternal(aURI, 0, aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationRemoved(aURI, aName));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::RemoveItemAnnotation(int64_t aItemId,
+ const nsACString& aName,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ nsresult rv = RemoveAnnotationInternal(nullptr, aItemId, aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationRemoved(aItemId, aName, aSource));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::RemovePageAnnotations(nsIURI* aURI)
+{
+ NS_ENSURE_ARG(aURI);
+
+ // Should this be precompiled or a getter?
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "DELETE FROM moz_annos WHERE place_id = "
+ "(SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url)"
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv = URIBinder::Bind(statement, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update observers
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationRemoved(aURI, EmptyCString()));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::RemoveItemAnnotations(int64_t aItemId,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ // Should this be precompiled or a getter?
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "DELETE FROM moz_items_annos WHERE item_id = :item_id"
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationRemoved(aItemId, EmptyCString(),
+ aSource));
+
+ return NS_OK;
+}
+
+
+/**
+ * @note If we use annotations for some standard items like GeckoFlags, it
+ * might be a good idea to blacklist these standard annotations from this
+ * copy function.
+ */
+NS_IMETHODIMP
+nsAnnotationService::CopyPageAnnotations(nsIURI* aSourceURI,
+ nsIURI* aDestURI,
+ bool aOverwriteDest)
+{
+ NS_ENSURE_ARG(aSourceURI);
+ NS_ENSURE_ARG(aDestURI);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ nsCOMPtr<mozIStorageStatement> sourceStmt = mDB->GetStatement(
+ "SELECT h.id, n.id, n.name, a2.id "
+ "FROM moz_places h "
+ "JOIN moz_annos a ON a.place_id = h.id "
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+ "LEFT JOIN moz_annos a2 ON a2.place_id = "
+ "(SELECT id FROM moz_places WHERE url_hash = hash(:dest_url) AND url = :dest_url) "
+ "AND a2.anno_attribute_id = n.id "
+ "WHERE url = :source_url"
+ );
+ NS_ENSURE_STATE(sourceStmt);
+ mozStorageStatementScoper sourceScoper(sourceStmt);
+
+ nsresult rv = URIBinder::Bind(sourceStmt, NS_LITERAL_CSTRING("source_url"), aSourceURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = URIBinder::Bind(sourceStmt, NS_LITERAL_CSTRING("dest_url"), aDestURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> copyStmt = mDB->GetStatement(
+ "INSERT INTO moz_annos "
+ "(place_id, anno_attribute_id, content, flags, expiration, "
+ "type, dateAdded, lastModified) "
+ "SELECT (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url), "
+ "anno_attribute_id, content, flags, expiration, type, "
+ ":date, :date "
+ "FROM moz_annos "
+ "WHERE place_id = :page_id "
+ "AND anno_attribute_id = :name_id"
+ );
+ NS_ENSURE_STATE(copyStmt);
+ mozStorageStatementScoper copyScoper(copyStmt);
+
+ bool hasResult;
+ while (NS_SUCCEEDED(sourceStmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t sourcePlaceId = sourceStmt->AsInt64(0);
+ int64_t annoNameID = sourceStmt->AsInt64(1);
+ nsAutoCString annoName;
+ rv = sourceStmt->GetUTF8String(2, annoName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t annoExistsOnDest = sourceStmt->AsInt64(3);
+
+ if (annoExistsOnDest) {
+ if (!aOverwriteDest)
+ continue;
+ rv = RemovePageAnnotation(aDestURI, annoName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Copy the annotation.
+ mozStorageStatementScoper scoper(copyStmt);
+ rv = URIBinder::Bind(copyStmt, NS_LITERAL_CSTRING("page_url"), aDestURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), sourcePlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("name_id"), annoNameID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), PR_Now());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copyStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnPageAnnotationSet(aDestURI, annoName));
+ }
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::CopyItemAnnotations(int64_t aSourceItemId,
+ int64_t aDestItemId,
+ bool aOverwriteDest,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aSourceItemId, 1);
+ NS_ENSURE_ARG_MIN(aDestItemId, 1);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ nsCOMPtr<mozIStorageStatement> sourceStmt = mDB->GetStatement(
+ "SELECT n.id, n.name, a2.id "
+ "FROM moz_bookmarks b "
+ "JOIN moz_items_annos a ON a.item_id = b.id "
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+ "LEFT JOIN moz_items_annos a2 ON a2.item_id = :dest_item_id "
+ "AND a2.anno_attribute_id = n.id "
+ "WHERE b.id = :source_item_id"
+ );
+ NS_ENSURE_STATE(sourceStmt);
+ mozStorageStatementScoper sourceScoper(sourceStmt);
+
+ nsresult rv = sourceStmt->BindInt64ByName(NS_LITERAL_CSTRING("source_item_id"), aSourceItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = sourceStmt->BindInt64ByName(NS_LITERAL_CSTRING("dest_item_id"), aDestItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> copyStmt = mDB->GetStatement(
+ "INSERT OR REPLACE INTO moz_items_annos "
+ "(item_id, anno_attribute_id, content, flags, expiration, "
+ "type, dateAdded, lastModified) "
+ "SELECT :dest_item_id, anno_attribute_id, content, flags, expiration, "
+ "type, :date, :date "
+ "FROM moz_items_annos "
+ "WHERE item_id = :source_item_id "
+ "AND anno_attribute_id = :name_id"
+ );
+ NS_ENSURE_STATE(copyStmt);
+ mozStorageStatementScoper copyScoper(copyStmt);
+
+ bool hasResult;
+ while (NS_SUCCEEDED(sourceStmt->ExecuteStep(&hasResult)) && hasResult) {
+ int64_t annoNameID = sourceStmt->AsInt64(0);
+ nsAutoCString annoName;
+ rv = sourceStmt->GetUTF8String(1, annoName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t annoExistsOnDest = sourceStmt->AsInt64(2);
+
+ if (annoExistsOnDest) {
+ if (!aOverwriteDest)
+ continue;
+ rv = RemoveItemAnnotation(aDestItemId, annoName, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Copy the annotation.
+ mozStorageStatementScoper scoper(copyStmt);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("dest_item_id"), aDestItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("source_item_id"), aSourceItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("name_id"), annoNameID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), PR_Now());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copyStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_ANNOS_OBSERVERS(OnItemAnnotationSet(aDestItemId, annoName, aSource));
+ }
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::AddObserver(nsIAnnotationObserver* aObserver)
+{
+ NS_ENSURE_ARG(aObserver);
+
+ if (mObservers.IndexOfObject(aObserver) >= 0)
+ return NS_ERROR_INVALID_ARG; // Already registered.
+ if (!mObservers.AppendObject(aObserver))
+ return NS_ERROR_OUT_OF_MEMORY;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAnnotationService::RemoveObserver(nsIAnnotationObserver* aObserver)
+{
+ NS_ENSURE_ARG(aObserver);
+
+ if (!mObservers.RemoveObject(aObserver))
+ return NS_ERROR_INVALID_ARG;
+ return NS_OK;
+}
+
+nsresult
+nsAnnotationService::HasAnnotationInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ bool* _hasAnno)
+{
+ bool isItemAnnotation = (aItemId > 0);
+ nsCOMPtr<mozIStorageStatement> stmt;
+ if (isItemAnnotation) {
+ stmt = mDB->GetStatement(
+ "SELECT b.id, "
+ "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS nameid, "
+ "a.id, a.dateAdded "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_items_annos a ON a.item_id = b.id "
+ "AND a.anno_attribute_id = nameid "
+ "WHERE b.id = :item_id"
+ );
+ }
+ else {
+ stmt = mDB->GetStatement(
+ "SELECT h.id, "
+ "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS nameid, "
+ "a.id, a.dateAdded "
+ "FROM moz_places h "
+ "LEFT JOIN moz_annos a ON a.place_id = h.id "
+ "AND a.anno_attribute_id = nameid "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url"
+ );
+ }
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper checkAnnoScoper(stmt);
+
+ nsresult rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isItemAnnotation)
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ else
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasResult) {
+ // We are trying to get an annotation on an invalid bookmarks or
+ // history entry.
+ // Here we preserve the old behavior, returning that we don't have the
+ // annotation, ignoring the fact itemId is invalid.
+ // Otherwise we should return NS_ERROR_INVALID_ARG, but this will somehow
+ // break the API. In future we could want to be pickier.
+ *_hasAnno = false;
+ }
+ else {
+ int64_t annotationId = stmt->AsInt64(2);
+ *_hasAnno = (annotationId > 0);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * This loads the statement and steps it once so you can get data out of it.
+ *
+ * @note You have to reset the statement when you're done if this succeeds.
+ * @throws NS_ERROR_NOT_AVAILABLE if the annotation is not found.
+ */
+
+nsresult
+nsAnnotationService::StartGetAnnotation(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ nsCOMPtr<mozIStorageStatement>& aStatement)
+{
+ bool isItemAnnotation = (aItemId > 0);
+
+ if (isItemAnnotation) {
+ aStatement = mDB->GetStatement(
+ "SELECT a.id, a.item_id, :anno_name, a.content, a.flags, "
+ "a.expiration, a.type "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_items_annos a ON a.anno_attribute_id = n.id "
+ "WHERE a.item_id = :item_id "
+ "AND n.name = :anno_name"
+ );
+ }
+ else {
+ aStatement = mDB->GetStatement(
+ "SELECT a.id, a.place_id, :anno_name, a.content, a.flags, "
+ "a.expiration, a.type "
+ "FROM moz_anno_attributes n "
+ "JOIN moz_annos a ON n.id = a.anno_attribute_id "
+ "JOIN moz_places h ON h.id = a.place_id "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url "
+ "AND n.name = :anno_name"
+ );
+ }
+ NS_ENSURE_STATE(aStatement);
+ mozStorageStatementScoper getAnnoScoper(aStatement);
+
+ nsresult rv;
+ if (isItemAnnotation)
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ else
+ rv = URIBinder::Bind(aStatement, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aStatement->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult = false;
+ rv = aStatement->ExecuteStep(&hasResult);
+ if (NS_FAILED(rv) || !hasResult)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ // on success, DON'T reset the statement, the caller needs to read from it,
+ // and it is the caller's job to reset it.
+ getAnnoScoper.Abandon();
+
+ return NS_OK;
+}
+
+
+/**
+ * This does most of the setup work needed to set an annotation, except for
+ * binding the the actual value and executing the statement.
+ * It will either update an existing annotation or insert a new one.
+ *
+ * @note The aStatement RESULT IS NOT ADDREFED. This is just one of the class
+ * vars, which control its scope. DO NOT RELEASE.
+ * The caller must take care of resetting the statement if this succeeds.
+ */
+nsresult
+nsAnnotationService::StartSetAnnotation(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aType,
+ nsCOMPtr<mozIStorageStatement>& aStatement)
+{
+ bool isItemAnnotation = (aItemId > 0);
+
+ if (aExpiration == EXPIRE_SESSION) {
+ mHasSessionAnnotations = true;
+ }
+
+ // Ensure the annotation name exists.
+ nsCOMPtr<mozIStorageStatement> addNameStmt = mDB->GetStatement(
+ "INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)"
+ );
+ NS_ENSURE_STATE(addNameStmt);
+ mozStorageStatementScoper scoper(addNameStmt);
+
+ nsresult rv = addNameStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = addNameStmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We have to check 2 things:
+ // - if the annotation already exists we should update it.
+ // - we should not allow setting annotations on invalid URIs or itemIds.
+ // This query will tell us:
+ // - whether the item or page exists.
+ // - whether the annotation already exists.
+ // - the nameID associated with the annotation name.
+ // - the id and dateAdded of the old annotation, if it exists.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ if (isItemAnnotation) {
+ stmt = mDB->GetStatement(
+ "SELECT b.id, "
+ "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS nameid, "
+ "a.id, a.dateAdded "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_items_annos a ON a.item_id = b.id "
+ "AND a.anno_attribute_id = nameid "
+ "WHERE b.id = :item_id"
+ );
+ }
+ else {
+ stmt = mDB->GetStatement(
+ "SELECT h.id, "
+ "(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS nameid, "
+ "a.id, a.dateAdded "
+ "FROM moz_places h "
+ "LEFT JOIN moz_annos a ON a.place_id = h.id "
+ "AND a.anno_attribute_id = nameid "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url"
+ );
+ }
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper checkAnnoScoper(stmt);
+
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isItemAnnotation)
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ else
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasResult) {
+ // We are trying to create an annotation on an invalid bookmark
+ // or history entry.
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ int64_t fkId = stmt->AsInt64(0);
+ int64_t nameID = stmt->AsInt64(1);
+ int64_t oldAnnoId = stmt->AsInt64(2);
+ int64_t oldAnnoDate = stmt->AsInt64(3);
+
+ if (isItemAnnotation) {
+ aStatement = mDB->GetStatement(
+ "INSERT OR REPLACE INTO moz_items_annos "
+ "(id, item_id, anno_attribute_id, content, flags, "
+ "expiration, type, dateAdded, lastModified) "
+ "VALUES (:id, :fk, :name_id, :content, :flags, "
+ ":expiration, :type, :date_added, :last_modified)"
+ );
+ }
+ else {
+ aStatement = mDB->GetStatement(
+ "INSERT OR REPLACE INTO moz_annos "
+ "(id, place_id, anno_attribute_id, content, flags, "
+ "expiration, type, dateAdded, lastModified) "
+ "VALUES (:id, :fk, :name_id, :content, :flags, "
+ ":expiration, :type, :date_added, :last_modified)"
+ );
+ }
+ NS_ENSURE_STATE(aStatement);
+ mozStorageStatementScoper setAnnoScoper(aStatement);
+
+ // Don't replace existing annotations.
+ if (oldAnnoId > 0) {
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), oldAnnoId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), oldAnnoDate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ rv = aStatement->BindNullByName(NS_LITERAL_CSTRING("id"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), RoundedPRNow());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("fk"), fkId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("name_id"), nameID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("flags"), aFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("expiration"), aExpiration);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("type"), aType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), RoundedPRNow());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // On success, leave the statement open, the caller will set the value
+ // and execute the statement.
+ setAnnoScoper.Abandon();
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+nsAnnotationService::Observe(nsISupports *aSubject,
+ const char *aTopic,
+ const char16_t *aData)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
+ // Remove all session annotations, if any.
+ if (mHasSessionAnnotations) {
+ nsCOMPtr<mozIStorageAsyncStatement> pageAnnoStmt = mDB->GetAsyncStatement(
+ "DELETE FROM moz_annos WHERE expiration = :expire_session"
+ );
+ NS_ENSURE_STATE(pageAnnoStmt);
+ nsresult rv = pageAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("expire_session"),
+ EXPIRE_SESSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageAsyncStatement> itemAnnoStmt = mDB->GetAsyncStatement(
+ "DELETE FROM moz_items_annos WHERE expiration = :expire_session"
+ );
+ NS_ENSURE_STATE(itemAnnoStmt);
+ rv = itemAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("expire_session"),
+ EXPIRE_SESSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozIStorageBaseStatement *stmts[] = {
+ pageAnnoStmt.get()
+ , itemAnnoStmt.get()
+ };
+
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), nullptr,
+ getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/places/nsAnnotationService.h b/toolkit/components/places/nsAnnotationService.h
new file mode 100644
index 0000000000..f1b4921d81
--- /dev/null
+++ b/toolkit/components/places/nsAnnotationService.h
@@ -0,0 +1,161 @@
+//* -*- 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/. */
+
+#ifndef nsAnnotationService_h___
+#define nsAnnotationService_h___
+
+#include "nsIAnnotationService.h"
+#include "nsTArray.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWeakReference.h"
+#include "nsToolkitCompsCID.h"
+#include "Database.h"
+#include "nsString.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+namespace places {
+
+class AnnotatedResult final : public mozIAnnotatedResult
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIANNOTATEDRESULT
+
+ AnnotatedResult(const nsCString& aGUID, nsIURI* aURI, int64_t aItemd,
+ const nsACString& aAnnotationName,
+ nsIVariant* aAnnotationValue);
+
+private:
+ ~AnnotatedResult();
+
+ const nsCString mGUID;
+ nsCOMPtr<nsIURI> mURI;
+ const int64_t mItemId;
+ const nsCString mAnnotationName;
+ nsCOMPtr<nsIVariant> mAnnotationValue;
+};
+
+} // namespace places
+} // namespace mozilla
+
+class nsAnnotationService final : public nsIAnnotationService
+ , public nsIObserver
+ , public nsSupportsWeakReference
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIANNOTATIONSERVICE
+ NS_DECL_NSIOBSERVER
+
+ nsAnnotationService();
+
+ /**
+ * Obtains the service's object.
+ */
+ static already_AddRefed<nsAnnotationService> GetSingleton();
+
+ /**
+ * Initializes the service's object. This should only be called once.
+ */
+ nsresult Init();
+
+ /**
+ * Returns a cached pointer to the annotation service for consumers in the
+ * places directory.
+ */
+ static nsAnnotationService* GetAnnotationService()
+ {
+ if (!gAnnotationService) {
+ nsCOMPtr<nsIAnnotationService> serv =
+ do_GetService(NS_ANNOTATIONSERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(serv, nullptr);
+ NS_ASSERTION(gAnnotationService,
+ "Should have static instance pointer now");
+ }
+ return gAnnotationService;
+ }
+
+private:
+ ~nsAnnotationService();
+
+protected:
+ RefPtr<mozilla::places::Database> mDB;
+
+ nsCOMArray<nsIAnnotationObserver> mObservers;
+ bool mHasSessionAnnotations;
+
+ static nsAnnotationService* gAnnotationService;
+
+ static const int kAnnoIndex_ID;
+ static const int kAnnoIndex_PageOrItem;
+ static const int kAnnoIndex_NameID;
+ static const int kAnnoIndex_Content;
+ static const int kAnnoIndex_Flags;
+ static const int kAnnoIndex_Expiration;
+ static const int kAnnoIndex_Type;
+ static const int kAnnoIndex_DateAdded;
+ static const int kAnnoIndex_LastModified;
+
+ nsresult HasAnnotationInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ bool* _hasAnno);
+
+ nsresult StartGetAnnotation(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ nsCOMPtr<mozIStorageStatement>& aStatement);
+
+ nsresult StartSetAnnotation(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ int32_t aFlags,
+ uint16_t aExpiration,
+ uint16_t aType,
+ nsCOMPtr<mozIStorageStatement>& aStatement);
+
+ nsresult SetAnnotationStringInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ const nsAString& aValue,
+ int32_t aFlags,
+ uint16_t aExpiration);
+ nsresult SetAnnotationInt32Internal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ int32_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration);
+ nsresult SetAnnotationInt64Internal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ int64_t aValue,
+ int32_t aFlags,
+ uint16_t aExpiration);
+ nsresult SetAnnotationDoubleInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName,
+ double aValue,
+ int32_t aFlags,
+ uint16_t aExpiration);
+
+ nsresult RemoveAnnotationInternal(nsIURI* aURI,
+ int64_t aItemId,
+ const nsACString& aName);
+
+public:
+ nsresult GetPagesWithAnnotationCOMArray(const nsACString& aName,
+ nsCOMArray<nsIURI>* _results);
+ nsresult GetItemsWithAnnotationTArray(const nsACString& aName,
+ nsTArray<int64_t>* _result);
+ nsresult GetAnnotationNamesTArray(nsIURI* aURI,
+ int64_t aItemId,
+ nsTArray<nsCString>* _result);
+};
+
+#endif /* nsAnnotationService_h___ */
diff --git a/toolkit/components/places/nsFaviconService.cpp b/toolkit/components/places/nsFaviconService.cpp
new file mode 100644
index 0000000000..42526b2857
--- /dev/null
+++ b/toolkit/components/places/nsFaviconService.cpp
@@ -0,0 +1,716 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 the favicon service, which stores favicons for web pages with your
+ * history as you browse. It is also used to save the favicons for bookmarks.
+ *
+ * DANGER: The history query system makes assumptions about the favicon storage
+ * so that icons can be quickly generated for history/bookmark result sets. If
+ * you change the database layout at all, you will have to update both services.
+ */
+
+#include "nsFaviconService.h"
+
+#include "nsNavHistory.h"
+#include "nsPlacesMacros.h"
+#include "Helpers.h"
+
+#include "nsNetUtil.h"
+#include "nsReadableUtils.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "plbase64.h"
+#include "nsIClassInfoImpl.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/Preferences.h"
+#include "nsILoadInfo.h"
+#include "nsIContentPolicy.h"
+#include "nsContentUtils.h"
+#include "nsNullPrincipal.h"
+
+// For large favicons optimization.
+#include "imgITools.h"
+#include "imgIContainer.h"
+
+// The target dimension, in pixels, for favicons we optimize.
+#define OPTIMIZED_FAVICON_DIMENSION 32
+
+#define MAX_FAILED_FAVICONS 256
+#define FAVICON_CACHE_REDUCE_COUNT 64
+
+#define UNASSOCIATED_FAVICONS_LENGTH 32
+
+// When replaceFaviconData is called, we store the icons in an in-memory cache
+// instead of in storage. Icons in the cache are expired according to this
+// interval.
+#define UNASSOCIATED_ICON_EXPIRY_INTERVAL 60000
+
+// The MIME type of the default favicon and favicons created by
+// OptimizeFaviconImage.
+#define DEFAULT_MIME_TYPE "image/png"
+
+using namespace mozilla;
+using namespace mozilla::places;
+
+/**
+ * Used to notify a topic to system observers on async execute completion.
+ * Will throw on error.
+ */
+class ExpireFaviconsStatementCallbackNotifier : public AsyncStatementCallback
+{
+public:
+ ExpireFaviconsStatementCallbackNotifier();
+ NS_IMETHOD HandleCompletion(uint16_t aReason);
+};
+
+
+PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsFaviconService, gFaviconService)
+
+NS_IMPL_CLASSINFO(nsFaviconService, nullptr, 0, NS_FAVICONSERVICE_CID)
+NS_IMPL_ISUPPORTS_CI(
+ nsFaviconService
+, nsIFaviconService
+, mozIAsyncFavicons
+, nsITimerCallback
+)
+
+nsFaviconService::nsFaviconService()
+ : mFailedFaviconSerial(0)
+ , mFailedFavicons(MAX_FAILED_FAVICONS / 2)
+ , mUnassociatedIcons(UNASSOCIATED_FAVICONS_LENGTH)
+{
+ NS_ASSERTION(!gFaviconService,
+ "Attempting to create two instances of the service!");
+ gFaviconService = this;
+}
+
+
+nsFaviconService::~nsFaviconService()
+{
+ NS_ASSERTION(gFaviconService == this,
+ "Deleting a non-singleton instance of the service");
+ if (gFaviconService == this)
+ gFaviconService = nullptr;
+}
+
+
+nsresult
+nsFaviconService::Init()
+{
+ mDB = Database::GetDatabase();
+ NS_ENSURE_STATE(mDB);
+
+ mExpireUnassociatedIconsTimer = do_CreateInstance("@mozilla.org/timer;1");
+ NS_ENSURE_STATE(mExpireUnassociatedIconsTimer);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFaviconService::ExpireAllFavicons()
+{
+ nsCOMPtr<mozIStorageAsyncStatement> unlinkIconsStmt = mDB->GetAsyncStatement(
+ "UPDATE moz_places "
+ "SET favicon_id = NULL "
+ "WHERE favicon_id NOT NULL"
+ );
+ NS_ENSURE_STATE(unlinkIconsStmt);
+ nsCOMPtr<mozIStorageAsyncStatement> removeIconsStmt = mDB->GetAsyncStatement(
+ "DELETE FROM moz_favicons WHERE id NOT IN ("
+ "SELECT favicon_id FROM moz_places WHERE favicon_id NOT NULL "
+ ")"
+ );
+ NS_ENSURE_STATE(removeIconsStmt);
+
+ mozIStorageBaseStatement* stmts[] = {
+ unlinkIconsStmt.get()
+ , removeIconsStmt.get()
+ };
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ RefPtr<ExpireFaviconsStatementCallbackNotifier> callback =
+ new ExpireFaviconsStatementCallbackNotifier();
+ nsresult rv = mDB->MainConn()->ExecuteAsync(
+ stmts, ArrayLength(stmts), callback, getter_AddRefs(ps)
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsITimerCallback
+
+NS_IMETHODIMP
+nsFaviconService::Notify(nsITimer* timer)
+{
+ if (timer != mExpireUnassociatedIconsTimer.get()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ PRTime now = PR_Now();
+ for (auto iter = mUnassociatedIcons.Iter(); !iter.Done(); iter.Next()) {
+ UnassociatedIconHashKey* iconKey = iter.Get();
+ if (now - iconKey->created >= UNASSOCIATED_ICON_EXPIRY_INTERVAL) {
+ iter.Remove();
+ }
+ }
+
+ // Re-init the expiry timer if the cache isn't empty.
+ if (mUnassociatedIcons.Count() > 0) {
+ mExpireUnassociatedIconsTimer->InitWithCallback(
+ this, UNASSOCIATED_ICON_EXPIRY_INTERVAL, nsITimer::TYPE_ONE_SHOT);
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIFaviconService
+
+NS_IMETHODIMP
+nsFaviconService::GetDefaultFavicon(nsIURI** _retval)
+{
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ // not found, use default
+ if (!mDefaultIcon) {
+ nsresult rv = NS_NewURI(getter_AddRefs(mDefaultIcon),
+ NS_LITERAL_CSTRING(FAVICON_DEFAULT_URL));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return mDefaultIcon->Clone(_retval);
+}
+
+void
+nsFaviconService::SendFaviconNotifications(nsIURI* aPageURI,
+ nsIURI* aFaviconURI,
+ const nsACString& aGUID)
+{
+ nsAutoCString faviconSpec;
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ if (history && NS_SUCCEEDED(aFaviconURI->GetSpec(faviconSpec))) {
+ history->SendPageChangedNotification(aPageURI,
+ nsINavHistoryObserver::ATTRIBUTE_FAVICON,
+ NS_ConvertUTF8toUTF16(faviconSpec),
+ aGUID);
+ }
+}
+
+NS_IMETHODIMP
+nsFaviconService::SetAndFetchFaviconForPage(nsIURI* aPageURI,
+ nsIURI* aFaviconURI,
+ bool aForceReload,
+ uint32_t aFaviconLoadType,
+ nsIFaviconDataCallback* aCallback,
+ nsIPrincipal* aLoadingPrincipal,
+ mozIPlacesPendingOperation **_canceler)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(aPageURI);
+ NS_ENSURE_ARG(aFaviconURI);
+ NS_ENSURE_ARG_POINTER(_canceler);
+
+ // If a favicon is in the failed cache, only load it during a forced reload.
+ bool previouslyFailed;
+ nsresult rv = IsFailedFavicon(aFaviconURI, &previouslyFailed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (previouslyFailed) {
+ if (aForceReload)
+ RemoveFailedFavicon(aFaviconURI);
+ else
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadingPrincipal;
+ MOZ_ASSERT(loadingPrincipal, "please provide aLoadingPrincipal for this favicon");
+ if (!loadingPrincipal) {
+ // Let's default to the nullPrincipal if no loadingPrincipal is provided.
+ const char16_t* params[] = {
+ u"nsFaviconService::setAndFetchFaviconForPage()",
+ u"nsFaviconService::setAndFetchFaviconForPage(..., [optional aLoadingPrincipal])"
+ };
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Security by Default"),
+ nullptr, // aDocument
+ nsContentUtils::eNECKO_PROPERTIES,
+ "APIDeprecationWarning",
+ params, ArrayLength(params));
+ loadingPrincipal = nsNullPrincipal::Create();
+ }
+ NS_ENSURE_TRUE(loadingPrincipal, NS_ERROR_FAILURE);
+
+ // Check if the icon already exists and fetch it from the network, if needed.
+ // Finally associate the icon to the requested page if not yet associated.
+ bool loadPrivate = aFaviconLoadType == nsIFaviconService::FAVICON_LOAD_PRIVATE;
+
+ PageData page;
+ rv = aPageURI->GetSpec(page.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // URIs can arguably miss a host.
+ (void)GetReversedHostname(aPageURI, page.revHost);
+ bool canAddToHistory;
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
+ rv = navHistory->CanAddURI(aPageURI, &canAddToHistory);
+ NS_ENSURE_SUCCESS(rv, rv);
+ page.canAddToHistory = !!canAddToHistory && !loadPrivate;
+
+ IconData icon;
+ UnassociatedIconHashKey* iconKey = mUnassociatedIcons.GetEntry(aFaviconURI);
+ if (iconKey) {
+ icon = iconKey->iconData;
+ mUnassociatedIcons.RemoveEntry(iconKey);
+ } else {
+ icon.fetchMode = aForceReload ? FETCH_ALWAYS : FETCH_IF_MISSING;
+ rv = aFaviconURI->GetSpec(icon.spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If the page url points to an image, the icon's url will be the same.
+ // In future evaluate to store a resample of the image. For now avoid that
+ // for database size concerns.
+ // Don't store favicons for error pages too.
+ if (icon.spec.Equals(page.spec) ||
+ icon.spec.Equals(FAVICON_ERRORPAGE_URL)) {
+ return NS_OK;
+ }
+
+ RefPtr<AsyncFetchAndSetIconForPage> event =
+ new AsyncFetchAndSetIconForPage(icon, page, loadPrivate,
+ aCallback, aLoadingPrincipal);
+
+ // Get the target thread and start the work.
+ // DB will be updated and observers notified when data has finished loading.
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ DB->DispatchToAsyncThread(event);
+
+ // Return this event to the caller to allow aborting an eventual fetch.
+ event.forget(_canceler);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFaviconService::ReplaceFaviconData(nsIURI* aFaviconURI,
+ const uint8_t* aData,
+ uint32_t aDataLen,
+ const nsACString& aMimeType,
+ PRTime aExpiration)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(aFaviconURI);
+ NS_ENSURE_ARG(aData);
+ NS_ENSURE_TRUE(aDataLen > 0, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aMimeType.Length() > 0, NS_ERROR_INVALID_ARG);
+ if (aExpiration == 0) {
+ aExpiration = PR_Now() + MAX_FAVICON_EXPIRATION;
+ }
+
+ UnassociatedIconHashKey* iconKey = mUnassociatedIcons.PutEntry(aFaviconURI);
+ if (!iconKey) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ iconKey->created = PR_Now();
+
+ // If the cache contains unassociated icons, an expiry timer should already exist, otherwise
+ // there may be a timer left hanging around, so make sure we fire a new one.
+ int32_t unassociatedCount = mUnassociatedIcons.Count();
+ if (unassociatedCount == 1) {
+ mExpireUnassociatedIconsTimer->Cancel();
+ mExpireUnassociatedIconsTimer->InitWithCallback(
+ this, UNASSOCIATED_ICON_EXPIRY_INTERVAL, nsITimer::TYPE_ONE_SHOT);
+ }
+
+ IconData* iconData = &(iconKey->iconData);
+ iconData->expiration = aExpiration;
+ iconData->status = ICON_STATUS_CACHED;
+ iconData->fetchMode = FETCH_NEVER;
+ nsresult rv = aFaviconURI->GetSpec(iconData->spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If the page provided a large image for the favicon (eg, a highres image
+ // or a multiresolution .ico file), we don't want to store more data than
+ // needed.
+ if (aDataLen > MAX_FAVICON_FILESIZE) {
+ rv = OptimizeFaviconImage(aData, aDataLen, aMimeType, iconData->data, iconData->mimeType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (iconData->data.Length() > nsIFaviconService::MAX_FAVICON_BUFFER_SIZE) {
+ // We cannot optimize this favicon size and we are over the maximum size
+ // allowed, so we will not save data to the db to avoid bloating it.
+ mUnassociatedIcons.RemoveEntry(aFaviconURI);
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ iconData->mimeType.Assign(aMimeType);
+ iconData->data.Assign(TO_CHARBUFFER(aData), aDataLen);
+ }
+
+ // If the database contains an icon at the given url, we will update the
+ // database immediately so that the associated pages are kept in sync.
+ // Otherwise, do nothing and let the icon be picked up from the memory hash.
+ RefPtr<AsyncReplaceFaviconData> event = new AsyncReplaceFaviconData(*iconData);
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ DB->DispatchToAsyncThread(event);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFaviconService::ReplaceFaviconDataFromDataURL(nsIURI* aFaviconURI,
+ const nsAString& aDataURL,
+ PRTime aExpiration,
+ nsIPrincipal* aLoadingPrincipal)
+{
+ NS_ENSURE_ARG(aFaviconURI);
+ NS_ENSURE_TRUE(aDataURL.Length() > 0, NS_ERROR_INVALID_ARG);
+ if (aExpiration == 0) {
+ aExpiration = PR_Now() + MAX_FAVICON_EXPIRATION;
+ }
+
+ nsCOMPtr<nsIURI> dataURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(dataURI), aDataURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use the data: protocol handler to convert the data.
+ nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIProtocolHandler> protocolHandler;
+ rv = ioService->GetProtocolHandler("data", getter_AddRefs(protocolHandler));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadingPrincipal;
+ MOZ_ASSERT(loadingPrincipal, "please provide aLoadingPrincipal for this favicon");
+ if (!loadingPrincipal) {
+ // Let's default to the nullPrincipal if no loadingPrincipal is provided.
+ const char16_t* params[] = {
+ u"nsFaviconService::ReplaceFaviconDataFromDataURL()",
+ u"nsFaviconService::ReplaceFaviconDataFromDataURL(..., [optional aLoadingPrincipal])"
+ };
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Security by Default"),
+ nullptr, // aDocument
+ nsContentUtils::eNECKO_PROPERTIES,
+ "APIDeprecationWarning",
+ params, ArrayLength(params));
+
+ loadingPrincipal = nsNullPrincipal::Create();
+ }
+ NS_ENSURE_TRUE(loadingPrincipal, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsILoadInfo> loadInfo =
+ new mozilla::LoadInfo(loadingPrincipal,
+ nullptr, // aTriggeringPrincipal
+ nullptr, // aLoadingNode
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
+ nsILoadInfo::SEC_ALLOW_CHROME |
+ nsILoadInfo::SEC_DISALLOW_SCRIPT,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON);
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = protocolHandler->NewChannel2(dataURI, loadInfo, getter_AddRefs(channel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Blocking stream is OK for data URIs.
+ nsCOMPtr<nsIInputStream> stream;
+ rv = channel->Open2(getter_AddRefs(stream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint64_t available64;
+ rv = stream->Available(&available64);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (available64 == 0 || available64 > UINT32_MAX / sizeof(uint8_t))
+ return NS_ERROR_FILE_TOO_BIG;
+ uint32_t available = (uint32_t)available64;
+
+ // Read all the decoded data.
+ uint8_t* buffer = static_cast<uint8_t*>
+ (moz_xmalloc(sizeof(uint8_t) * available));
+ if (!buffer)
+ return NS_ERROR_OUT_OF_MEMORY;
+ uint32_t numRead;
+ rv = stream->Read(TO_CHARBUFFER(buffer), available, &numRead);
+ if (NS_FAILED(rv) || numRead != available) {
+ free(buffer);
+ return rv;
+ }
+
+ nsAutoCString mimeType;
+ rv = channel->GetContentType(mimeType);
+ if (NS_FAILED(rv)) {
+ free(buffer);
+ return rv;
+ }
+
+ // ReplaceFaviconData can now do the dirty work.
+ rv = ReplaceFaviconData(aFaviconURI, buffer, available, mimeType, aExpiration);
+ free(buffer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFaviconService::GetFaviconURLForPage(nsIURI *aPageURI,
+ nsIFaviconDataCallback* aCallback)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(aPageURI);
+ NS_ENSURE_ARG(aCallback);
+
+ nsAutoCString pageSpec;
+ nsresult rv = aPageURI->GetSpec(pageSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<AsyncGetFaviconURLForPage> event =
+ new AsyncGetFaviconURLForPage(pageSpec, aCallback);
+
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ DB->DispatchToAsyncThread(event);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFaviconService::GetFaviconDataForPage(nsIURI* aPageURI,
+ nsIFaviconDataCallback* aCallback)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG(aPageURI);
+ NS_ENSURE_ARG(aCallback);
+
+ nsAutoCString pageSpec;
+ nsresult rv = aPageURI->GetSpec(pageSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<AsyncGetFaviconDataForPage> event =
+ new AsyncGetFaviconDataForPage(pageSpec, aCallback);
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ DB->DispatchToAsyncThread(event);
+
+ return NS_OK;
+}
+
+nsresult
+nsFaviconService::GetFaviconLinkForIcon(nsIURI* aFaviconURI,
+ nsIURI** aOutputURI)
+{
+ NS_ENSURE_ARG(aFaviconURI);
+ NS_ENSURE_ARG_POINTER(aOutputURI);
+
+ nsAutoCString spec;
+ if (aFaviconURI) {
+ nsresult rv = aFaviconURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return GetFaviconLinkForIconString(spec, aOutputURI);
+}
+
+
+NS_IMETHODIMP
+nsFaviconService::AddFailedFavicon(nsIURI* aFaviconURI)
+{
+ NS_ENSURE_ARG(aFaviconURI);
+
+ nsAutoCString spec;
+ nsresult rv = aFaviconURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFailedFavicons.Put(spec, mFailedFaviconSerial);
+ mFailedFaviconSerial ++;
+
+ if (mFailedFavicons.Count() > MAX_FAILED_FAVICONS) {
+ // need to expire some entries, delete the FAVICON_CACHE_REDUCE_COUNT number
+ // of items that are the oldest
+ uint32_t threshold = mFailedFaviconSerial -
+ MAX_FAILED_FAVICONS + FAVICON_CACHE_REDUCE_COUNT;
+ for (auto iter = mFailedFavicons.Iter(); !iter.Done(); iter.Next()) {
+ if (iter.Data() < threshold) {
+ iter.Remove();
+ }
+ }
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsFaviconService::RemoveFailedFavicon(nsIURI* aFaviconURI)
+{
+ NS_ENSURE_ARG(aFaviconURI);
+
+ nsAutoCString spec;
+ nsresult rv = aFaviconURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we silently do nothing and succeed if the icon is not in the cache
+ mFailedFavicons.Remove(spec);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsFaviconService::IsFailedFavicon(nsIURI* aFaviconURI, bool* _retval)
+{
+ NS_ENSURE_ARG(aFaviconURI);
+ nsAutoCString spec;
+ nsresult rv = aFaviconURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t serial;
+ *_retval = mFailedFavicons.Get(spec, &serial);
+ return NS_OK;
+}
+
+
+// nsFaviconService::GetFaviconLinkForIconString
+//
+// This computes a favicon URL with string input and using the cached
+// default one to minimize parsing.
+
+nsresult
+nsFaviconService::GetFaviconLinkForIconString(const nsCString& aSpec,
+ nsIURI** aOutput)
+{
+ if (aSpec.IsEmpty()) {
+ // default icon for empty strings
+ if (! mDefaultIcon) {
+ nsresult rv = NS_NewURI(getter_AddRefs(mDefaultIcon),
+ NS_LITERAL_CSTRING(FAVICON_DEFAULT_URL));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return mDefaultIcon->Clone(aOutput);
+ }
+
+ if (StringBeginsWith(aSpec, NS_LITERAL_CSTRING("chrome:"))) {
+ // pass through for chrome URLs, since they can be referenced without
+ // this service
+ return NS_NewURI(aOutput, aSpec);
+ }
+
+ nsAutoCString annoUri;
+ annoUri.AssignLiteral("moz-anno:" FAVICON_ANNOTATION_NAME ":");
+ annoUri += aSpec;
+ return NS_NewURI(aOutput, annoUri);
+}
+
+
+// nsFaviconService::GetFaviconSpecForIconString
+//
+// This computes a favicon spec for when you don't want a URI object (as in
+// the tree view implementation), sparing all parsing and normalization.
+void
+nsFaviconService::GetFaviconSpecForIconString(const nsCString& aSpec,
+ nsACString& aOutput)
+{
+ if (aSpec.IsEmpty()) {
+ aOutput.AssignLiteral(FAVICON_DEFAULT_URL);
+ } else if (StringBeginsWith(aSpec, NS_LITERAL_CSTRING("chrome:"))) {
+ aOutput = aSpec;
+ } else {
+ aOutput.AssignLiteral("moz-anno:" FAVICON_ANNOTATION_NAME ":");
+ aOutput += aSpec;
+ }
+}
+
+
+// nsFaviconService::OptimizeFaviconImage
+//
+// Given a blob of data (a image file already read into a buffer), optimize
+// its size by recompressing it as a 16x16 PNG.
+nsresult
+nsFaviconService::OptimizeFaviconImage(const uint8_t* aData, uint32_t aDataLen,
+ const nsACString& aMimeType,
+ nsACString& aNewData,
+ nsACString& aNewMimeType)
+{
+ nsresult rv;
+
+ nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1");
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewByteInputStream(getter_AddRefs(stream),
+ reinterpret_cast<const char*>(aData), aDataLen,
+ NS_ASSIGNMENT_DEPEND);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // decode image
+ nsCOMPtr<imgIContainer> container;
+ rv = imgtool->DecodeImageData(stream, aMimeType, getter_AddRefs(container));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aNewMimeType.AssignLiteral(DEFAULT_MIME_TYPE);
+
+ // scale and recompress
+ nsCOMPtr<nsIInputStream> iconStream;
+ rv = imgtool->EncodeScaledImage(container, aNewMimeType,
+ OPTIMIZED_FAVICON_DIMENSION,
+ OPTIMIZED_FAVICON_DIMENSION,
+ EmptyString(),
+ getter_AddRefs(iconStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Read the stream into a new buffer.
+ rv = NS_ConsumeStream(iconStream, UINT32_MAX, aNewData);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+nsFaviconService::GetFaviconDataAsync(nsIURI* aFaviconURI,
+ mozIStorageStatementCallback *aCallback)
+{
+ NS_ASSERTION(aCallback, "Doesn't make sense to call this without a callback");
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
+ "SELECT f.data, f.mime_type FROM moz_favicons f WHERE url = :icon_url"
+ );
+ NS_ENSURE_STATE(stmt);
+
+ // Ignore the ref part of the URI before querying the database because
+ // we may have added a media fragment for rendering purposes.
+
+ nsAutoCString faviconURI;
+ aFaviconURI->GetSpecIgnoringRef(faviconURI);
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("icon_url"), faviconURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> pendingStatement;
+ return stmt->ExecuteAsync(aCallback, getter_AddRefs(pendingStatement));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// ExpireFaviconsStatementCallbackNotifier
+
+ExpireFaviconsStatementCallbackNotifier::ExpireFaviconsStatementCallbackNotifier()
+{
+}
+
+
+NS_IMETHODIMP
+ExpireFaviconsStatementCallbackNotifier::HandleCompletion(uint16_t aReason)
+{
+ // We should dispatch only if expiration has been successful.
+ if (aReason != mozIStorageStatementCallback::REASON_FINISHED)
+ return NS_OK;
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ (void)observerService->NotifyObservers(nullptr,
+ NS_PLACES_FAVICONS_EXPIRED_TOPIC_ID,
+ nullptr);
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/places/nsFaviconService.h b/toolkit/components/places/nsFaviconService.h
new file mode 100644
index 0000000000..b2fcdbeaa2
--- /dev/null
+++ b/toolkit/components/places/nsFaviconService.h
@@ -0,0 +1,147 @@
+/* -*- 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/. */
+
+#ifndef nsFaviconService_h_
+#define nsFaviconService_h_
+
+#include "nsIFaviconService.h"
+#include "mozIAsyncFavicons.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsDataHashtable.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTHashtable.h"
+#include "nsToolkitCompsCID.h"
+#include "nsURIHashKey.h"
+#include "nsITimer.h"
+#include "Database.h"
+#include "mozilla/storage.h"
+#include "mozilla/Attributes.h"
+
+#include "FaviconHelpers.h"
+
+// Favicons bigger than this (in bytes) will not be stored in the database. We
+// expect that most 32x32 PNG favicons will be no larger due to compression.
+#define MAX_FAVICON_FILESIZE 3072 /* 3 KiB */
+
+// forward class definitions
+class mozIStorageStatementCallback;
+
+class UnassociatedIconHashKey : public nsURIHashKey
+{
+public:
+ explicit UnassociatedIconHashKey(const nsIURI* aURI)
+ : nsURIHashKey(aURI)
+ {
+ }
+ UnassociatedIconHashKey(const UnassociatedIconHashKey& aOther)
+ : nsURIHashKey(aOther)
+ {
+ NS_NOTREACHED("Do not call me!");
+ }
+ mozilla::places::IconData iconData;
+ PRTime created;
+};
+
+class nsFaviconService final : public nsIFaviconService
+ , public mozIAsyncFavicons
+ , public nsITimerCallback
+{
+public:
+ nsFaviconService();
+
+ /**
+ * Obtains the service's object.
+ */
+ static already_AddRefed<nsFaviconService> GetSingleton();
+
+ /**
+ * Initializes the service's object. This should only be called once.
+ */
+ nsresult Init();
+
+ /**
+ * Returns a cached pointer to the favicon service for consumers in the
+ * places directory.
+ */
+ static nsFaviconService* GetFaviconService()
+ {
+ if (!gFaviconService) {
+ nsCOMPtr<nsIFaviconService> serv =
+ do_GetService(NS_FAVICONSERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(serv, nullptr);
+ NS_ASSERTION(gFaviconService, "Should have static instance pointer now");
+ }
+ return gFaviconService;
+ }
+
+ // addition to API for strings to prevent excessive parsing of URIs
+ nsresult GetFaviconLinkForIconString(const nsCString& aIcon, nsIURI** aOutput);
+ void GetFaviconSpecForIconString(const nsCString& aIcon, nsACString& aOutput);
+
+ nsresult OptimizeFaviconImage(const uint8_t* aData, uint32_t aDataLen,
+ const nsACString& aMimeType,
+ nsACString& aNewData, nsACString& aNewMimeType);
+
+ /**
+ * Obtains the favicon data asynchronously.
+ *
+ * @param aFaviconURI
+ * The URI representing the favicon we are looking for.
+ * @param aCallback
+ * The callback where results or errors will be dispatch to. In the
+ * returned result, the favicon binary data will be at index 0, and the
+ * mime type will be at index 1.
+ */
+ nsresult GetFaviconDataAsync(nsIURI* aFaviconURI,
+ mozIStorageStatementCallback* aCallback);
+
+ /**
+ * Call to send out favicon changed notifications. Should only be called
+ * when there is data loaded for the favicon.
+ * @param aPageURI
+ * The URI of the page to notify about.
+ * @param aFaviconURI
+ * The moz-anno:favicon URI of the icon.
+ * @param aGUID
+ * The unique ID associated with the page.
+ */
+ void SendFaviconNotifications(nsIURI* aPageURI, nsIURI* aFaviconURI,
+ const nsACString& aGUID);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFAVICONSERVICE
+ NS_DECL_MOZIASYNCFAVICONS
+ NS_DECL_NSITIMERCALLBACK
+
+private:
+ ~nsFaviconService();
+
+ RefPtr<mozilla::places::Database> mDB;
+
+ nsCOMPtr<nsITimer> mExpireUnassociatedIconsTimer;
+
+ static nsFaviconService* gFaviconService;
+
+ /**
+ * A cached URI for the default icon. We return this a lot, and don't want to
+ * re-parse and normalize our unchanging string many times. Important: do
+ * not return this directly; use Clone() since callers may change the object
+ * they get back. May be null, in which case it needs initialization.
+ */
+ nsCOMPtr<nsIURI> mDefaultIcon;
+
+ uint32_t mFailedFaviconSerial;
+ nsDataHashtable<nsCStringHashKey, uint32_t> mFailedFavicons;
+
+ // This class needs access to the icons cache.
+ friend class mozilla::places::AsyncReplaceFaviconData;
+ nsTHashtable<UnassociatedIconHashKey> mUnassociatedIcons;
+};
+
+#define FAVICON_ANNOTATION_NAME "favicon"
+
+#endif // nsFaviconService_h_
diff --git a/toolkit/components/places/nsIAnnotationService.idl b/toolkit/components/places/nsIAnnotationService.idl
new file mode 100644
index 0000000000..bdd417ece3
--- /dev/null
+++ b/toolkit/components/places/nsIAnnotationService.idl
@@ -0,0 +1,422 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+interface nsIVariant;
+interface mozIAnnotatedResult;
+
+[scriptable, uuid(63fe98e0-6889-4c2c-ac9f-703e4bc25027)]
+interface nsIAnnotationObserver : nsISupports
+{
+ /**
+ * Called when an annotation value is set. It could be a new annotation,
+ * or it could be a new value for an existing annotation.
+ */
+ void onPageAnnotationSet(in nsIURI aPage,
+ in AUTF8String aName);
+ void onItemAnnotationSet(in long long aItemId,
+ in AUTF8String aName,
+ in unsigned short aSource);
+
+ /**
+ * Called when an annotation is deleted. If aName is empty, then ALL
+ * annotations for the given URI have been deleted. This is not called when
+ * annotations are expired (normally happens when the app exits).
+ */
+ void onPageAnnotationRemoved(in nsIURI aURI,
+ in AUTF8String aName);
+ void onItemAnnotationRemoved(in long long aItemId,
+ in AUTF8String aName,
+ in unsigned short aSource);
+};
+
+[scriptable, uuid(D4CDAAB1-8EEC-47A8-B420-AD7CB333056A)]
+interface nsIAnnotationService : nsISupports
+{
+ /**
+ * Valid values for aExpiration, which sets the expiration policy for your
+ * annotation. The times for the days, weeks and months policies are
+ * measured since the last visit date of the page in question. These
+ * will not expire so long as the user keeps visiting the page from time
+ * to time.
+ */
+
+ // For temporary data that can be discarded when the user exits.
+ // Removed at application exit.
+ const unsigned short EXPIRE_SESSION = 0;
+
+ // NOTE: 1 is skipped due to its temporary use as EXPIRE_NEVER in bug #319455.
+
+ // For general page settings, things the user is interested in seeing
+ // if they come back to this page some time in the near future.
+ // Removed at 30 days.
+ const unsigned short EXPIRE_WEEKS = 2;
+
+ // Something that the user will be interested in seeing in their
+ // history like favicons. If they haven't visited a page in a couple
+ // of months, they probably aren't interested in many other annotations,
+ // the positions of things, or other stuff you create, so put that in
+ // the weeks policy.
+ // Removed at 180 days.
+ const unsigned short EXPIRE_MONTHS = 3;
+
+ // For annotations that only live as long as the URI is in the database.
+ // A page annotation will expire if the page has no visits
+ // and is not bookmarked.
+ // An item annotation will expire when the item is deleted.
+ const unsigned short EXPIRE_NEVER = 4;
+
+ // For annotations that only live as long as the URI has visits.
+ // Valid only for page annotations.
+ const unsigned short EXPIRE_WITH_HISTORY = 5;
+
+ // For short-lived temporary data that you still want to outlast a session.
+ // Removed at 7 days.
+ const unsigned short EXPIRE_DAYS = 6;
+
+ // type constants
+ const unsigned short TYPE_INT32 = 1;
+ const unsigned short TYPE_DOUBLE = 2;
+ const unsigned short TYPE_STRING = 3;
+ const unsigned short TYPE_INT64 = 5;
+
+ /**
+ * Sets an annotation, overwriting any previous annotation with the same
+ * URL/name. IT IS YOUR JOB TO NAMESPACE YOUR ANNOTATION NAMES.
+ * Use the form "namespace/value", so your name would be like
+ * "bills_extension/page_state" or "history/thumbnail".
+ *
+ * Do not use characters that are not valid in URLs such as spaces, ":",
+ * commas, or most other symbols. You should stick to ASCII letters and
+ * numbers plus "_", "-", and "/".
+ *
+ * aExpiration is one of EXPIRE_* above. aFlags should be 0 for now, some
+ * flags will be defined in the future.
+ *
+ * NOTE: ALL PAGE ANNOTATIONS WILL GET DELETED WHEN THE PAGE IS REMOVED FROM
+ * HISTORY IF THE PAGE IS NOT BOOKMARKED. This means that if you create an
+ * annotation on an unvisited URI, it will get deleted when the browser
+ * shuts down. Otherwise, URIs can exist in history as annotations but the
+ * user has no way of knowing it, potentially violating their privacy
+ * expectations about actions such as "Clear history".
+ * If there is an important annotation that the user or extension wants to
+ * keep, you should add a bookmark for the page and use an EXPIRE_NEVER
+ * annotation. This will ensure the annotation exists until the item is
+ * removed by the user.
+ * See EXPIRE_* constants above for further information.
+ *
+ * For item annotations, aSource should be a change source constant from
+ * nsINavBookmarksService::SOURCE_*, and defaults to SOURCE_DEFAULT if
+ * omitted.
+ *
+ * The annotation "favicon" is special. Favicons are stored in the favicon
+ * service, but are special cased in the protocol handler so they look like
+ * annotations. Do not set favicons using this service, it will not work.
+ *
+ * Only C++ consumers may use the type-specific methods.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
+ */
+ void setPageAnnotation(in nsIURI aURI,
+ in AUTF8String aName,
+ in nsIVariant aValue,
+ in long aFlags,
+ in unsigned short aExpiration);
+ void setItemAnnotation(in long long aItemId,
+ in AUTF8String aName,
+ in nsIVariant aValue,
+ in long aFlags,
+ in unsigned short aExpiration,
+ [optional] in unsigned short aSource);
+
+ /**
+ * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
+ */
+ [noscript] void setPageAnnotationString(in nsIURI aURI,
+ in AUTF8String aName,
+ in AString aValue,
+ in long aFlags,
+ in unsigned short aExpiration);
+ [noscript] void setItemAnnotationString(in long long aItemId,
+ in AUTF8String aName,
+ in AString aValue,
+ in long aFlags,
+ in unsigned short aExpiration,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Sets an annotation just like setAnnotationString, but takes an Int32 as
+ * input.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
+ */
+ [noscript] void setPageAnnotationInt32(in nsIURI aURI,
+ in AUTF8String aName,
+ in long aValue,
+ in long aFlags,
+ in unsigned short aExpiration);
+ [noscript] void setItemAnnotationInt32(in long long aItemId,
+ in AUTF8String aName,
+ in long aValue,
+ in long aFlags,
+ in unsigned short aExpiration,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Sets an annotation just like setAnnotationString, but takes an Int64 as
+ * input.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
+ */
+ [noscript] void setPageAnnotationInt64(in nsIURI aURI,
+ in AUTF8String aName,
+ in long long aValue,
+ in long aFlags,
+ in unsigned short aExpiration);
+ [noscript] void setItemAnnotationInt64(in long long aItemId,
+ in AUTF8String aName,
+ in long long aValue,
+ in long aFlags,
+ in unsigned short aExpiration,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Sets an annotation just like setAnnotationString, but takes a double as
+ * input.
+ *
+ * @throws NS_ERROR_ILLEGAL_VALUE if the page or the bookmark doesn't exist.
+ */
+ [noscript] void setPageAnnotationDouble(in nsIURI aURI,
+ in AUTF8String aName,
+ in double aValue,
+ in long aFlags,
+ in unsigned short aExpiration);
+ [noscript] void setItemAnnotationDouble(in long long aItemId,
+ in AUTF8String aName,
+ in double aValue,
+ in long aFlags,
+ in unsigned short aExpiration,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Retrieves the value of a given annotation. Throws an error if the
+ * annotation does not exist. C++ consumers may use the type-specific
+ * methods.
+ *
+ * The type-specific methods throw if the given annotation is set in
+ * a different type.
+ */
+ nsIVariant getPageAnnotation(in nsIURI aURI,
+ in AUTF8String aName);
+ nsIVariant getItemAnnotation(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * @see getPageAnnotation
+ */
+ [noscript] AString getPageAnnotationString(in nsIURI aURI,
+ in AUTF8String aName);
+ [noscript] AString getItemAnnotationString(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * @see getPageAnnotation
+ */
+ [noscript] long getPageAnnotationInt32(in nsIURI aURI,
+ in AUTF8String aName);
+ [noscript] long getItemAnnotationInt32(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * @see getPageAnnotation
+ */
+ [noscript] long long getPageAnnotationInt64(in nsIURI aURI,
+ in AUTF8String aName);
+ [noscript] long long getItemAnnotationInt64(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * @see getPageAnnotation
+ */
+ [noscript] double getPageAnnotationDouble(in nsIURI aURI,
+ in AUTF8String aName);
+ [noscript] double getItemAnnotationDouble(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * Retrieves info about an existing annotation.
+ *
+ * aType will be one of TYPE_* constansts above
+ *
+ * example JS:
+ * var flags = {}, exp = {}, type = {};
+ * annotator.getAnnotationInfo(myURI, "foo", flags, exp, type);
+ * // now you can use 'exp.value' and 'flags.value'
+ */
+ void getPageAnnotationInfo(in nsIURI aURI,
+ in AUTF8String aName,
+ out int32_t aFlags,
+ out unsigned short aExpiration,
+ out unsigned short aType);
+ void getItemAnnotationInfo(in long long aItemId,
+ in AUTF8String aName,
+ out long aFlags,
+ out unsigned short aExpiration,
+ out unsigned short aType);
+
+ /**
+ * Retrieves the type of an existing annotation
+ * Use getAnnotationInfo if you need this along with the mime-type etc.
+ *
+ * @param aURI
+ * the uri on which the annotation is set
+ * @param aName
+ * the annotation name
+ * @return one of the TYPE_* constants above
+ * @throws if the annotation is not set
+ */
+ uint16_t getPageAnnotationType(in nsIURI aURI,
+ in AUTF8String aName);
+ uint16_t getItemAnnotationType(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * Returns a list of all URIs having a given annotation.
+ */
+ void getPagesWithAnnotation(
+ in AUTF8String name,
+ [optional] out unsigned long resultCount,
+ [retval, array, size_is(resultCount)] out nsIURI results);
+ void getItemsWithAnnotation(
+ in AUTF8String name,
+ [optional] out unsigned long resultCount,
+ [retval, array, size_is(resultCount)] out long long results);
+
+ /**
+ * Returns a list of mozIAnnotation(s), having a given annotation name.
+ *
+ * @param name
+ * The annotation to search for.
+ * @return list of mozIAnnotation objects.
+ */
+ void getAnnotationsWithName(
+ in AUTF8String name,
+ [optional] out unsigned long count,
+ [retval, array, size_is(count)] out mozIAnnotatedResult results);
+
+ /**
+ * Get the names of all annotations for this URI.
+ *
+ * example JS:
+ * var annotations = annotator.getPageAnnotations(myURI, {});
+ */
+ void getPageAnnotationNames(
+ in nsIURI aURI,
+ [optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsIVariant result);
+ void getItemAnnotationNames(
+ in long long aItemId,
+ [optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsIVariant result);
+
+ /**
+ * Test for annotation existence.
+ */
+ boolean pageHasAnnotation(in nsIURI aURI,
+ in AUTF8String aName);
+ boolean itemHasAnnotation(in long long aItemId,
+ in AUTF8String aName);
+
+ /**
+ * Removes a specific annotation. Succeeds even if the annotation is
+ * not found.
+ */
+ void removePageAnnotation(in nsIURI aURI,
+ in AUTF8String aName);
+ void removeItemAnnotation(in long long aItemId,
+ in AUTF8String aName,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Removes all annotations for the given page/item.
+ * We may want some other similar functions to get annotations with given
+ * flags (once we have flags defined).
+ */
+ void removePageAnnotations(in nsIURI aURI);
+ void removeItemAnnotations(in long long aItemId,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Copies all annotations from the source to the destination URI/item. If
+ * the destination already has an annotation with the same name as one on
+ * the source, it will be overwritten if aOverwriteDest is set. Otherwise,
+ * the original annotation will be preferred.
+ *
+ * All the source annotations will stay as-is. If you don't want them
+ * any more, use removePageAnnotations on that URI.
+ */
+ void copyPageAnnotations(in nsIURI aSourceURI,
+ in nsIURI aDestURI,
+ in boolean aOverwriteDest);
+ void copyItemAnnotations(in long long aSourceItemId,
+ in long long aDestItemId,
+ in boolean aOverwriteDest,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Adds an annotation observer. The annotation service will keep an owning
+ * reference to the observer object.
+ */
+ void addObserver(in nsIAnnotationObserver aObserver);
+
+
+ /**
+ * Removes an annotaton observer previously registered by addObserver.
+ */
+ void removeObserver(in nsIAnnotationObserver aObserver);
+};
+
+/**
+ * Represents a place annotated with a given annotation. If a place has
+ * multiple annotations, it can be represented by multiple
+ * mozIAnnotatedResult(s).
+ */
+[scriptable, uuid(81fd0188-db6a-492e-80b6-f6414913b396)]
+interface mozIAnnotatedResult : nsISupports
+{
+ /**
+ * The globally unique identifier of the place with this annotation.
+ *
+ * @note if itemId is valid this is the guid of the bookmark, otherwise
+ * of the page.
+ */
+ readonly attribute AUTF8String guid;
+
+ /**
+ * The URI of the place with this annotation, if available, null otherwise.
+ */
+ readonly attribute nsIURI uri;
+
+ /**
+ * The bookmark id of the place with this annotation, if available,
+ * -1 otherwise.
+ *
+ * @note if itemId is -1, it doesn't mean the page is not bookmarked, just
+ * that this annotation is relative to the page, not to the bookmark.
+ */
+ readonly attribute long long itemId;
+
+ /**
+ * Name of the annotation.
+ */
+ readonly attribute AUTF8String annotationName;
+
+ /**
+ * Value of the annotation.
+ */
+ readonly attribute nsIVariant annotationValue;
+};
diff --git a/toolkit/components/places/nsIBrowserHistory.idl b/toolkit/components/places/nsIBrowserHistory.idl
new file mode 100644
index 0000000000..8f3265972c
--- /dev/null
+++ b/toolkit/components/places/nsIBrowserHistory.idl
@@ -0,0 +1,70 @@
+/* -*- 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/. */
+
+/*
+ * browser-specific interface to global history
+ */
+
+#include "nsISupports.idl"
+#include "nsIGlobalHistory2.idl"
+
+[scriptable, uuid(20d31479-38de-49f4-9300-566d6e834c66)]
+interface nsIBrowserHistory : nsISupports
+{
+ /**
+ * Removes a page from global history.
+ *
+ * @note It is preferrable to use this one rather then RemovePages when
+ * removing less than 10 pages, since it won't start a full batch
+ * operation.
+ */
+ void removePage(in nsIURI aURI);
+
+ /**
+ * Removes a list of pages from global history.
+ *
+ * @param aURIs
+ * Array of URIs to be removed.
+ * @param aLength
+ * Length of the array.
+ *
+ * @note the removal happens in a batch.
+ */
+ void removePages([array, size_is(aLength)] in nsIURI aURIs,
+ in unsigned long aLength);
+
+ /**
+ * Removes all global history information about pages for a given host.
+ *
+ * @param aHost
+ * Hostname to be removed.
+ * An empty host name means local files and anything else with no
+ * hostname. You can also pass in the localized "(local files)"
+ * title given to you from a history query to remove all
+ * history information from local files.
+ * @param aEntireDomain
+ * If true, will also delete pages from sub hosts (so if
+ * passed in "microsoft.com" will delete "www.microsoft.com",
+ * "msdn.microsoft.com", etc.).
+ *
+ * @note The removal happens in a batch.
+ */
+ void removePagesFromHost(in AUTF8String aHost,
+ in boolean aEntireDomain);
+
+ /**
+ * Removes all pages for a given timeframe.
+ * Limits are included: aBeginTime <= timeframe <= aEndTime
+ *
+ * @param aBeginTime
+ * Microseconds from epoch, representing the initial time.
+ * @param aEndTime
+ * Microseconds from epoch, representing the final time.
+ *
+ * @note The removal happens in a batch.
+ */
+ void removePagesByTimeframe(in PRTime aBeginTime,
+ in PRTime aEndTime);
+};
diff --git a/toolkit/components/places/nsIFaviconService.idl b/toolkit/components/places/nsIFaviconService.idl
new file mode 100644
index 0000000000..25339d64b5
--- /dev/null
+++ b/toolkit/components/places/nsIFaviconService.idl
@@ -0,0 +1,145 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+
+[scriptable, uuid(e81e0b0c-b9f1-4c2e-8f3c-b809933cf73c)]
+interface nsIFaviconService : nsISupports
+{
+ // The favicon is being loaded from a private browsing window
+ const unsigned long FAVICON_LOAD_PRIVATE = 1;
+ // The favicon is being loaded from a non-private browsing window
+ const unsigned long FAVICON_LOAD_NON_PRIVATE = 2;
+
+ /**
+ * The limit in bytes of the size of favicons in memory and passed via the
+ * favicon protocol.
+ */
+ const unsigned long MAX_FAVICON_BUFFER_SIZE = 10240;
+
+ /**
+ * For a given icon URI, this will return a URI that will result in the image.
+ * In most cases, this is an annotation URI. For chrome URIs, this will do
+ * nothing but returning the input URI.
+ *
+ * No validity checking is done. If you pass an icon URI that we've never
+ * seen, you'll get back a URI that references an invalid icon. The moz-anno
+ * protocol handler's special case for "favicon" annotations will resolve
+ * invalid icons to the default icon, although without caching.
+ * For invalid chrome URIs, you'll get a broken image.
+ *
+ * @param aFaviconURI
+ * The URI of an icon in the favicon service.
+ * @return A URI that will give you the icon image. This is NOT the URI of
+ * the icon as set on the page, but a URI that will give you the
+ * data out of the favicon service. For a normal page with a
+ * favicon we've stored, this will be an annotation URI which will
+ * then cause the corresponding favicon data to be loaded async from
+ * this service. For pages where we don't have a favicon, this will
+ * be a chrome URI of the default icon. For chrome URIs, the
+ * output will be the same as the input.
+ */
+ nsIURI getFaviconLinkForIcon(in nsIURI aFaviconURI);
+
+ /**
+ * Expire all known favicons from the database.
+ *
+ * @note This is an async method.
+ * On successful completion a "places-favicons-expired" notification is
+ * dispatched through observer's service.
+ */
+ void expireAllFavicons();
+
+ /**
+ * Adds a given favicon's URI to the failed favicon cache.
+ *
+ * The lifespan of the favicon cache is up to the caching system. This cache
+ * will also be written when setAndLoadFaviconForPage hits an error while
+ * fetching an icon.
+ *
+ * @param aFaviconURI
+ * The URI of an icon in the favicon service.
+ */
+ void addFailedFavicon(in nsIURI aFaviconURI);
+
+ /**
+ * Removes the given favicon from the failed favicon cache. If the icon is
+ * not in the cache, it will silently succeed.
+ *
+ * @param aFaviconURI
+ * The URI of an icon in the favicon service.
+ */
+ void removeFailedFavicon(in nsIURI aFaviconURI);
+
+ /**
+ * Checks to see if a favicon is in the failed favicon cache.
+ * A positive return value means the icon is in the failed cache and you
+ * probably shouldn't try to load it. A false return value means that it's
+ * worth trying to load it.
+ * This allows you to avoid trying to load "foo.com/favicon.ico" for every
+ * page on a site that doesn't have a favicon.
+ *
+ * @param aFaviconURI
+ * The URI of an icon in the favicon service.
+ */
+ boolean isFailedFavicon(in nsIURI aFaviconURI);
+
+ /**
+ * The default favicon URI
+ */
+ readonly attribute nsIURI defaultFavicon;
+};
+
+[scriptable, function, uuid(c85e5c82-b70f-4621-9528-beb2aa47fb44)]
+interface nsIFaviconDataCallback : nsISupports
+{
+ /**
+ * Called when the required favicon's information is available.
+ *
+ * It's up to the invoking method to state if the callback is always invoked,
+ * or called on success only. Check the method documentation to ensure that.
+ *
+ * The caller will receive the most information we can gather on the icon,
+ * but it's not guaranteed that all of them will be set. For some method
+ * we could not know the favicon's data (it could just be too expensive to
+ * get it, or the method does not require we actually have any data).
+ * It's up to the caller to check aDataLen > 0 before using any data-related
+ * information like mime-type or data itself.
+ *
+ * @param aFaviconURI
+ * Receives the "favicon URI" (not the "favicon link URI") associated
+ * to the requested page. This can be null if there is no associated
+ * favicon URI, or the callback is notifying a failure.
+ * @param aDataLen
+ * Size of the icon data in bytes. Notice that a value of 0 does not
+ * necessarily mean that we don't have an icon.
+ * @param aData
+ * Icon data, or an empty array if aDataLen is 0.
+ * @param aMimeType
+ * Mime type of the icon, or an empty string if aDataLen is 0.
+ *
+ * @note If you want to open a network channel to access the favicon, it's
+ * recommended that you call the getFaviconLinkForIcon method to convert
+ * the "favicon URI" into a "favicon link URI".
+ */
+ void onComplete(in nsIURI aFaviconURI,
+ in unsigned long aDataLen,
+ [const,array,size_is(aDataLen)] in octet aData,
+ in AUTF8String aMimeType);
+};
+
+%{C++
+
+/**
+ * Notification sent when all favicons are expired.
+ */
+#define NS_PLACES_FAVICONS_EXPIRED_TOPIC_ID "places-favicons-expired"
+
+#define FAVICON_DEFAULT_URL "chrome://mozapps/skin/places/defaultFavicon.png"
+#define FAVICON_ERRORPAGE_URL "chrome://global/skin/icons/warning-16.png"
+
+%}
diff --git a/toolkit/components/places/nsINavBookmarksService.idl b/toolkit/components/places/nsINavBookmarksService.idl
new file mode 100644
index 0000000000..e9e49a4f48
--- /dev/null
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -0,0 +1,697 @@
+/* -*- 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;
+interface nsIURI;
+interface nsITransaction;
+interface nsINavHistoryBatchCallback;
+
+/**
+ * Observer for bookmarks changes.
+ */
+[scriptable, uuid(c06b4e7d-15b1-4d4f-bdf7-147d2be9084a)]
+interface nsINavBookmarkObserver : nsISupports
+{
+ /*
+ * This observer should not be called for items that are tags.
+ */
+ readonly attribute boolean skipTags;
+
+ /*
+ * This observer should not be called for descendants when the parent is removed.
+ * For example when revmoing a folder containing bookmarks.
+ */
+ readonly attribute boolean skipDescendantsOnItemRemoval;
+
+ /**
+ * Notifies that a batch transaction has started.
+ * Other notifications will be sent during the batch, but the observer is
+ * guaranteed that onEndUpdateBatch() will be called at its completion.
+ * During a batch the observer should do its best to reduce the work done to
+ * handle notifications, since multiple changes are going to happen in a short
+ * timeframe.
+ */
+ void onBeginUpdateBatch();
+
+ /**
+ * Notifies that a batch transaction has ended.
+ */
+ void onEndUpdateBatch();
+
+ /**
+ * Notifies that an item (any type) was added. Called after the actual
+ * addition took place.
+ * When a new item is created, all the items following it in the same folder
+ * will have their index shifted down, but no additional notifications will
+ * be sent.
+ *
+ * @param aItemId
+ * The id of the item that was added.
+ * @param aParentId
+ * The id of the folder to which the item was added.
+ * @param aIndex
+ * The item's index in the folder.
+ * @param aItemType
+ * The type of the added item (see TYPE_* constants below).
+ * @param aURI
+ * The URI of the added item if it was TYPE_BOOKMARK, null otherwise.
+ * @param aTitle
+ * The title of the added item.
+ * @param aDateAdded
+ * The stored date added value, in microseconds from the epoch.
+ * @param aGuid
+ * The unique ID associated with the item.
+ * @param aParentGuid
+ * The unique ID associated with the item's parent.
+ * @param aSource
+ * A change source constant from nsINavBookmarksService::SOURCE_*,
+ * passed to the method that notifies the observer.
+ */
+ void onItemAdded(in long long aItemId,
+ in long long aParentId,
+ in long aIndex,
+ in unsigned short aItemType,
+ in nsIURI aURI,
+ in AUTF8String aTitle,
+ in PRTime aDateAdded,
+ in ACString aGuid,
+ in ACString aParentGuid,
+ in unsigned short aSource);
+
+ /**
+ * Notifies that an item was removed. Called after the actual remove took
+ * place.
+ * When an item is removed, all the items following it in the same folder
+ * will have their index shifted down, but no additional notifications will
+ * be sent.
+ *
+ * @param aItemId
+ * The id of the item that was removed.
+ * @param aParentId
+ * The id of the folder from which the item was removed.
+ * @param aIndex
+ * The bookmark's index in the folder.
+ * @param aItemType
+ * The type of the item to be removed (see TYPE_* constants below).
+ * @param aURI
+ * The URI of the added item if it was TYPE_BOOKMARK, null otherwise.
+ * @param aGuid
+ * The unique ID associated with the item.
+ * @param aParentGuid
+ * The unique ID associated with the item's parent.
+ * @param aSource
+ * A change source constant from nsINavBookmarksService::SOURCE_*,
+ * passed to the method that notifies the observer.
+ */
+ void onItemRemoved(in long long aItemId,
+ in long long aParentId,
+ in long aIndex,
+ in unsigned short aItemType,
+ in nsIURI aURI,
+ in ACString aGuid,
+ in ACString aParentGuid,
+ in unsigned short aSource);
+
+ /**
+ * Notifies that an item's information has changed. This will be called
+ * whenever any attributes like "title" are changed.
+ *
+ * @param aItemId
+ * The id of the item that was changed.
+ * @param aProperty
+ * The property which changed. Can be null for the removal of all of
+ * the annotations, in this case aIsAnnotationProperty is true.
+ * @param aIsAnnotationProperty
+ * Whether or not aProperty is the name of an annotation. If true
+ * aNewValue is always an empty string.
+ * @param aNewValue
+ * For certain properties, this is set to the new value of the
+ * property (see the list below).
+ * @param aLastModified
+ * The updated last-modified value.
+ * @param aItemType
+ * The type of the item to be removed (see TYPE_* constants below).
+ * @param aParentId
+ * The id of the folder containing the item.
+ * @param aGuid
+ * The unique ID associated with the item.
+ * @param aParentGuid
+ * The unique ID associated with the item's parent.
+ * @param aOldValue
+ * For certain properties, this is set to the new value of the
+ * property (see the list below).
+ * @param aSource
+ * A change source constant from nsINavBookmarksService::SOURCE_*,
+ * passed to the method that notifies the observer.
+ *
+ * @note List of values that may be associated with properties:
+ * aProperty | aNewValue
+ * =====================================================================
+ * cleartime | Empty string (all visits to this item were removed).
+ * title | The new title.
+ * favicon | The "moz-anno" URL of the new favicon.
+ * uri | new URL.
+ * tags | Empty string (tags for this item changed)
+ * dateAdded | PRTime (as string) when the item was first added.
+ * lastModified | PRTime (as string) when the item was last modified.
+ *
+ * aProperty | aOldValue
+ * =====================================================================
+ * cleartime | Empty string (currently unused).
+ * title | Empty string (currently unused).
+ * favicon | Empty string (currently unused).
+ * uri | old URL.
+ * tags | Empty string (currently unused).
+ * dateAdded | Empty string (currently unused).
+ * lastModified | Empty string (currently unused).
+ */
+ void onItemChanged(in long long aItemId,
+ in ACString aProperty,
+ in boolean aIsAnnotationProperty,
+ in AUTF8String aNewValue,
+ in PRTime aLastModified,
+ in unsigned short aItemType,
+ in long long aParentId,
+ in ACString aGuid,
+ in ACString aParentGuid,
+ in AUTF8String aOldValue,
+ in unsigned short aSource);
+
+ /**
+ * Notifies that the item was visited. Can be invoked only for TYPE_BOOKMARK
+ * items.
+ *
+ * @param aItemId
+ * The id of the bookmark that was visited.
+ * @param aVisitId
+ * The id of the visit.
+ * @param aTime
+ * The time of the visit.
+ * @param aTransitionType
+ * The transition for the visit. See nsINavHistoryService::TRANSITION_*
+ * constants for a list of possible values.
+ * @param aURI
+ * The nsIURI for this bookmark.
+ * @param aParentId
+ * The id of the folder containing the item.
+ * @param aGuid
+ * The unique ID associated with the item.
+ * @param aParentGuid
+ * The unique ID associated with the item's parent.
+ *
+ * @see onItemChanged with property = "cleartime" for when all visits to an
+ * item are removed.
+ *
+ * @note The reported time is the time of the visit that was added, which may
+ * be well in the past since the visit time can be specified. This
+ * means that the visit the observer is told about may not be the most
+ * recent visit for that page.
+ */
+ void onItemVisited(in long long aItemId,
+ in long long aVisitId,
+ in PRTime aTime,
+ in unsigned long aTransitionType,
+ in nsIURI aURI,
+ in long long aParentId,
+ in ACString aGuid,
+ in ACString aParentGuid);
+
+ /**
+ * Notifies that an item has been moved.
+ *
+ * @param aItemId
+ * The id of the item that was moved.
+ * @param aOldParentId
+ * The id of the old parent.
+ * @param aOldIndex
+ * The old index inside the old parent.
+ * @param aNewParentId
+ * The id of the new parent.
+ * @param aNewIndex
+ * The index inside the new parent.
+ * @param aItemType
+ * The type of the item to be removed (see TYPE_* constants below).
+ * @param aGuid
+ * The unique ID associated with the item.
+ * @param aOldParentGuid
+ * The unique ID associated with the old item's parent.
+ * @param aNewParentGuid
+ * The unique ID associated with the new item's parent.
+ * @param aSource
+ * A change source constant from nsINavBookmarksService::SOURCE_*,
+ * passed to the method that notifies the observer.
+ */
+ void onItemMoved(in long long aItemId,
+ in long long aOldParentId,
+ in long aOldIndex,
+ in long long aNewParentId,
+ in long aNewIndex,
+ in unsigned short aItemType,
+ in ACString aGuid,
+ in ACString aOldParentGuid,
+ in ACString aNewParentGuid,
+ in unsigned short aSource);
+};
+
+/**
+ * The BookmarksService interface provides methods for managing bookmarked
+ * history items. Bookmarks consist of a set of user-customizable
+ * folders. A URI in history can be contained in one or more such folders.
+ */
+
+[scriptable, uuid(24533891-afa6-4663-b72d-3143d03f1b04)]
+interface nsINavBookmarksService : nsISupports
+{
+ /**
+ * The item ID of the Places root.
+ */
+ readonly attribute long long placesRoot;
+
+ /**
+ * The item ID of the bookmarks menu folder.
+ */
+ readonly attribute long long bookmarksMenuFolder;
+
+ /**
+ * The item ID of the top-level folder that contain the tag "folders".
+ */
+ readonly attribute long long tagsFolder;
+
+ /**
+ * The item ID of the unfiled-bookmarks folder.
+ */
+ readonly attribute long long unfiledBookmarksFolder;
+
+ /**
+ * The item ID of the personal toolbar folder.
+ */
+ readonly attribute long long toolbarFolder;
+
+ /**
+ * The item ID of the mobile bookmarks folder.
+ */
+ readonly attribute long long mobileFolder;
+
+ /**
+ * This value should be used for APIs that allow passing in an index
+ * where an index is not known, or not required to be specified.
+ * e.g.: When appending an item to a folder.
+ */
+ const short DEFAULT_INDEX = -1;
+
+ const unsigned short TYPE_BOOKMARK = 1;
+ const unsigned short TYPE_FOLDER = 2;
+ const unsigned short TYPE_SEPARATOR = 3;
+ // Dynamic containers are deprecated and unsupported.
+ // This const exists just to avoid reusing the value.
+ const unsigned short TYPE_DYNAMIC_CONTAINER = 4;
+
+ // Change source constants. These are used to distinguish changes made by
+ // Sync and bookmarks import from other Places consumers, though they can
+ // be extended to support other callers. Sources are passed as optional
+ // parameters to methods used by Sync, and forwarded to observers.
+ const unsigned short SOURCE_DEFAULT = 0;
+ const unsigned short SOURCE_SYNC = 1;
+ const unsigned short SOURCE_IMPORT = 2;
+ const unsigned short SOURCE_IMPORT_REPLACE = 3;
+
+ /**
+ * Inserts a child bookmark into the given folder.
+ *
+ * @param aParentId
+ * The id of the parent folder
+ * @param aURI
+ * The URI to insert
+ * @param aIndex
+ * The index to insert at, or DEFAULT_INDEX to append
+ * @param aTitle
+ * The title for the new bookmark
+ * @param [optional] aGuid
+ * The GUID to be set for the new item. If not set, a new GUID is
+ * generated. Unless you've a very sound reason, such as an undo
+ * manager implementation, do not pass this argument.
+ * @param [optional] aSource
+ * The change source. This is forwarded to all bookmark observers,
+ * allowing them to distinguish between insertions from different
+ * callers. Defaults to SOURCE_DEFAULT if omitted.
+ * @return The ID of the newly-created bookmark.
+ *
+ * @note aTitle will be truncated to TITLE_LENGTH_MAX and
+ * aURI will be truncated to URI_LENGTH_MAX.
+ * @throws if aGuid is malformed.
+ */
+ long long insertBookmark(in long long aParentId, in nsIURI aURI,
+ in long aIndex, in AUTF8String aTitle,
+ [optional] in ACString aGuid,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Removes a child item. Used to delete a bookmark or separator.
+ * @param aItemId
+ * The child item to remove
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ */
+ void removeItem(in long long aItemId, [optional] in unsigned short aSource);
+
+ /**
+ * Creates a new child folder and inserts it under the given parent.
+ * @param aParentFolder
+ * The id of the parent folder
+ * @param aName
+ * The name of the new folder
+ * @param aIndex
+ * The index to insert at, or DEFAULT_INDEX to append
+ * @param [optional] aGuid
+ * The GUID to be set for the new item. If not set, a new GUID is
+ * generated. Unless you've a very sound reason, such as an undo
+ * manager implementation, do not pass this argument.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ * @return The ID of the newly-inserted folder.
+ * @throws if aGuid is malformed.
+ */
+ long long createFolder(in long long aParentFolder, in AUTF8String name,
+ in long index,
+ [optional] in ACString aGuid,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Gets an undo-able transaction for removing a folder from the bookmarks
+ * tree.
+ * @param aItemId
+ * The id of the folder to remove.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ * @return An object implementing nsITransaction that can be used to undo
+ * or redo the action.
+ *
+ * This method exists because complex delete->undo operations rely on
+ * recreated folders to have the same ID they had before they were deleted,
+ * so that any other items deleted in different transactions can be
+ * re-inserted correctly. This provides a safe encapsulation of this
+ * functionality without exposing the ability to recreate folders with
+ * specific IDs (potentially dangerous if abused by other code!) in the
+ * public API.
+ */
+ nsITransaction getRemoveFolderTransaction(in long long aItemId,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Convenience function for container services. Removes
+ * all children of the given folder.
+ * @param aItemId
+ * The id of the folder to remove children from.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ */
+ void removeFolderChildren(in long long aItemId,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Moves an item to a different container, preserving its contents.
+ * @param aItemId
+ * The id of the item to move
+ * @param aNewParentId
+ * The id of the new parent
+ * @param aIndex
+ * The index under aNewParent, or DEFAULT_INDEX to append
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ *
+ * NOTE: When moving down in the same container we take into account the
+ * removal of the original item. If you want to move from index X to
+ * index Y > X you must use moveItem(id, folder, Y + 1)
+ */
+ void moveItem(in long long aItemId,
+ in long long aNewParentId,
+ in long aIndex,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Inserts a bookmark separator into the given folder at the given index.
+ * The separator can be removed using removeChildAt().
+ * @param aParentId
+ * The id of the parent folder
+ * @param aIndex
+ * The separator's index under folder, or DEFAULT_INDEX to append
+ * @param [optional] aGuid
+ * The GUID to be set for the new item. If not set, a new GUID is
+ * generated. Unless you've a very sound reason, such as an undo
+ * manager implementation, do not pass this argument.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ * @return The ID of the new separator.
+ * @throws if aGuid is malformed.
+ */
+ long long insertSeparator(in long long aParentId, in long aIndex,
+ [optional] in ACString aGuid,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Get the itemId given the containing folder and the index.
+ * @param aParentId
+ * The id of the diret parent folder of the item
+ * @param aIndex
+ * The index of the item within the parent folder.
+ * Pass DEFAULT_INDEX for the last item.
+ * @return The ID of the found item, -1 if the item does not exists.
+ */
+ long long getIdForItemAt(in long long aParentId, in long aIndex);
+
+ /**
+ * Set the title for an item.
+ * @param aItemId
+ * The id of the item whose title should be updated.
+ * @param aTitle
+ * The new title for the bookmark.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ *
+ * @note aTitle will be truncated to TITLE_LENGTH_MAX.
+ */
+ void setItemTitle(in long long aItemId, in AUTF8String aTitle,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Get the title for an item.
+ *
+ * If no item title is available it will return a void string (null in JS).
+ *
+ * @param aItemId
+ * The id of the item whose title should be retrieved
+ * @return The title of the item.
+ */
+ AUTF8String getItemTitle(in long long aItemId);
+
+ /**
+ * Set the date added time for an item.
+ *
+ * @param aItemId
+ * the id of the item whose date added time should be updated.
+ * @param aDateAdded
+ * the new date added value in microseconds. Note that it is rounded
+ * down to milliseconds precision.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ */
+ void setItemDateAdded(in long long aItemId,
+ in PRTime aDateAdded,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Get the date added time for an item.
+ *
+ * @param aItemId
+ * the id of the item whose date added time should be retrieved.
+ *
+ * @return the date added value in microseconds.
+ */
+ PRTime getItemDateAdded(in long long aItemId);
+
+ /**
+ * Set the last modified time for an item.
+ *
+ * @param aItemId
+ * the id of the item whose last modified time should be updated.
+ * @param aLastModified
+ * the new last modified value in microseconds. Note that it is
+ * rounded down to milliseconds precision.
+ * @param [optional] aSource
+ * The change source, forwarded to all bookmark observers. Defaults
+ * to SOURCE_DEFAULT.
+ *
+ * @note This is the only method that will send an itemChanged notification
+ * for the property. lastModified will still be updated in
+ * any other method that changes an item property, but we will send
+ * the corresponding itemChanged notification instead.
+ */
+ void setItemLastModified(in long long aItemId,
+ in PRTime aLastModified,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Get the last modified time for an item.
+ *
+ * @param aItemId
+ * the id of the item whose last modified time should be retrieved.
+ *
+ * @return the date added value in microseconds.
+ *
+ * @note When an item is added lastModified is set to the same value as
+ * dateAdded.
+ */
+ PRTime getItemLastModified(in long long aItemId);
+
+ /**
+ * Get the URI for a bookmark item.
+ */
+ nsIURI getBookmarkURI(in long long aItemId);
+
+ /**
+ * Get the index for an item.
+ */
+ long getItemIndex(in long long aItemId);
+
+ /**
+ * Changes the index for a item. This method does not change the indices of
+ * any other items in the same folder, so ensure that the new index does not
+ * already exist, or change the index of other items accordingly, otherwise
+ * the indices will become corrupted.
+ *
+ * WARNING: This is API is intended for scenarios such as folder sorting,
+ * where the caller manages the indices of *all* items in the folder.
+ * You must always ensure each index is unique after a reordering.
+ *
+ * @param aItemId The id of the item to modify
+ * @param aNewIndex The new index
+ * @param aSource The optional change source, forwarded to all bookmark
+ * observers. Defaults to SOURCE_DEFAULT.
+ *
+ * @throws If aNewIndex is out of bounds.
+ */
+ void setItemIndex(in long long aItemId,
+ in long aNewIndex,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Get an item's type (bookmark, separator, folder).
+ * The type is one of the TYPE_* constants defined above.
+ */
+ unsigned short getItemType(in long long aItemId);
+
+ /**
+ * Returns true if the given URI is in any bookmark folder. If you want the
+ * results to be redirect-aware, use getBookmarkedURIFor()
+ */
+ boolean isBookmarked(in nsIURI aURI);
+
+ /**
+ * Used to see if the given URI is bookmarked, or any page that redirected to
+ * it is bookmarked. For example, if I bookmark "mozilla.org" by manually
+ * typing it in, and follow the bookmark, I will get redirected to
+ * "www.mozilla.org". Logically, this new page is also bookmarked. This
+ * function, if given "www.mozilla.org", will return the URI of the bookmark,
+ * in this case "mozilla.org".
+ *
+ * If there is no bookmarked page found, it will return NULL.
+ *
+ * @note The function will only return bookmarks in the first 2 levels of
+ * redirection (1 -> 2 -> aURI).
+ */
+ nsIURI getBookmarkedURIFor(in nsIURI aURI);
+
+ /**
+ * Change the bookmarked URI for a bookmark.
+ * This changes which "place" the bookmark points at,
+ * which means all annotations, etc are carried along.
+ */
+ void changeBookmarkURI(in long long aItemId,
+ in nsIURI aNewURI,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Get the parent folder's id for an item.
+ */
+ long long getFolderIdForItem(in long long aItemId);
+
+ /**
+ * Returns the list of bookmark ids that contain the given URI.
+ */
+ void getBookmarkIdsForURI(in nsIURI aURI, [optional] out unsigned long count,
+ [array, retval, size_is(count)] out long long bookmarks);
+
+ /**
+ * Associates the given keyword with the given bookmark.
+ *
+ * Use an empty keyword to clear the keyword associated with the URI.
+ * In both of these cases, succeeds but does nothing if the URL/keyword is not found.
+ *
+ * @deprecated Use PlacesUtils.keywords.insert() API instead.
+ */
+ void setKeywordForBookmark(in long long aItemId,
+ in AString aKeyword,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Retrieves the keyword for the given bookmark. Will be void string
+ * (null in JS) if no such keyword is found.
+ *
+ * @deprecated Use PlacesUtils.keywords.fetch() API instead.
+ */
+ AString getKeywordForBookmark(in long long aItemId);
+
+ /**
+ * Returns the URI associated with the given keyword. Empty if no such
+ * keyword is found.
+ *
+ * @deprecated Use PlacesUtils.keywords.fetch() API instead.
+ */
+ nsIURI getURIForKeyword(in AString keyword);
+
+ /**
+ * Adds a bookmark observer. If ownsWeak is false, the bookmark service will
+ * keep an owning reference to the observer. If ownsWeak is true, then
+ * aObserver must implement nsISupportsWeakReference, and the bookmark
+ * service will keep a weak reference to the observer.
+ */
+ void addObserver(in nsINavBookmarkObserver observer, in boolean ownsWeak);
+
+ /**
+ * Removes a bookmark observer.
+ */
+ void removeObserver(in nsINavBookmarkObserver observer);
+
+ /**
+ * Gets an array of registered nsINavBookmarkObserver objects.
+ */
+ void getObservers([optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsINavBookmarkObserver observers);
+
+ /**
+ * Runs the passed callback inside of a database transaction.
+ * Use this when a lot of things are about to change, for example
+ * adding or deleting a large number of bookmark items. Calls can
+ * be nested. Observers are notified when batches begin and end, via
+ * nsINavBookmarkObserver.onBeginUpdateBatch/onEndUpdateBatch.
+ *
+ * @param aCallback
+ * nsINavHistoryBatchCallback interface to call.
+ * @param aUserData
+ * Opaque parameter passed to nsINavBookmarksBatchCallback
+ */
+ void runInBatchMode(in nsINavHistoryBatchCallback aCallback,
+ in nsISupports aUserData);
+};
diff --git a/toolkit/components/places/nsINavHistoryService.idl b/toolkit/components/places/nsINavHistoryService.idl
new file mode 100644
index 0000000000..3fd8518709
--- /dev/null
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -0,0 +1,1451 @@
+/* -*- 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/. */
+
+/**
+ * Using Places services after quit-application is not reliable, so make
+ * sure to do any shutdown work on quit-application, or history
+ * synchronization could fail, losing latest changes.
+ */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+interface nsIURI;
+interface nsIVariant;
+interface nsIFile;
+
+interface nsINavHistoryContainerResultNode;
+interface nsINavHistoryQueryResultNode;
+interface nsINavHistoryQuery;
+interface nsINavHistoryQueryOptions;
+interface nsINavHistoryResult;
+interface nsINavHistoryBatchCallback;
+
+[scriptable, uuid(91d104bb-17ef-404b-9f9a-d9ed8de6824c)]
+interface nsINavHistoryResultNode : nsISupports
+{
+ /**
+ * Indentifies the parent result node in the result set. This is null for
+ * top level nodes.
+ */
+ readonly attribute nsINavHistoryContainerResultNode parent;
+
+ /**
+ * The history-result to which this node belongs.
+ */
+ readonly attribute nsINavHistoryResult parentResult;
+
+ /**
+ * URI of the resource in question. For visits and URLs, this is the URL of
+ * the page. For folders and queries, this is the place: URI of the
+ * corresponding folder or query. This may be empty for other types of
+ * objects like host containers.
+ */
+ readonly attribute AUTF8String uri;
+
+ /**
+ * Identifies the type of this node. This node can then be QI-ed to the
+ * corresponding specialized result node interface.
+ */
+ const unsigned long RESULT_TYPE_URI = 0; // nsINavHistoryResultNode
+
+ // Visit nodes are deprecated and unsupported.
+ // This line exists just to avoid reusing the value:
+ // const unsigned long RESULT_TYPE_VISIT = 1;
+
+ // Full visit nodes are deprecated and unsupported.
+ // This line exists just to avoid reusing the value:
+ // const unsigned long RESULT_TYPE_FULL_VISIT = 2;
+
+ // Dynamic containers are deprecated and unsupported.
+ // This const exists just to avoid reusing the value:
+ // const unsigned long RESULT_TYPE_DYNAMIC_CONTAINER = 4; // nsINavHistoryContainerResultNode
+
+ const unsigned long RESULT_TYPE_QUERY = 5; // nsINavHistoryQueryResultNode
+ const unsigned long RESULT_TYPE_FOLDER = 6; // nsINavHistoryQueryResultNode
+ const unsigned long RESULT_TYPE_SEPARATOR = 7; // nsINavHistoryResultNode
+ const unsigned long RESULT_TYPE_FOLDER_SHORTCUT = 9; // nsINavHistoryQueryResultNode
+ readonly attribute unsigned long type;
+
+ /**
+ * Title of the web page, or of the node's query (day, host, folder, etc)
+ */
+ readonly attribute AUTF8String title;
+
+ /**
+ * Total number of times the URI has ever been accessed. For hosts, this
+ * is the total of the children under it, NOT the total times the host has
+ * been accessed (this would require an additional query, so is not given
+ * by default when most of the time it is never needed).
+ */
+ readonly attribute unsigned long accessCount;
+
+ /**
+ * This is the time the user accessed the page.
+ *
+ * If this is a visit, it is the exact time that the page visit occurred.
+ *
+ * If this is a URI, it is the most recent time that the URI was visited.
+ * Even if you ask for all URIs for a given date range long ago, this might
+ * contain today's date if the URI was visited today.
+ *
+ * For hosts, or other node types with children, this is the most recent
+ * access time for any of the children.
+ *
+ * For days queries this is the respective endTime - a maximum possible
+ * visit time to fit in the day range.
+ */
+ readonly attribute PRTime time;
+
+ /**
+ * This URI can be used as an image source URI and will give you the favicon
+ * for the page. It is *not* the URI of the favicon, but rather something
+ * that will resolve to the actual image.
+ *
+ * In most cases, this is an annotation URI that will query the favicon
+ * service. If the entry has no favicon, this is the chrome URI of the
+ * default favicon. If the favicon originally lived in chrome, this will
+ * be the original chrome URI of the icon.
+ */
+ readonly attribute AUTF8String icon;
+
+ /**
+ * This is the number of levels between this node and the top of the
+ * hierarchy. The members of result.children have indentLevel = 0, their
+ * children have indentLevel = 1, etc. The indent level of the root node is
+ * set to -1.
+ */
+ readonly attribute long indentLevel;
+
+ /**
+ * When this item is in a bookmark folder (parent is of type folder), this is
+ * the index into that folder of this node. These indices start at 0 and
+ * increase in the order that they appear in the bookmark folder. For items
+ * that are not in a bookmark folder, this value is -1.
+ */
+ readonly attribute long bookmarkIndex;
+
+ /**
+ * If the node is an item (bookmark, folder or a separator) this value is the
+ * row ID of that bookmark in the database. For other nodes, this value is
+ * set to -1.
+ */
+ readonly attribute long long itemId;
+
+ /**
+ * If the node is an item (bookmark, folder or a separator) this value is the
+ * time that the item was created. For other nodes, this value is 0.
+ */
+ readonly attribute PRTime dateAdded;
+
+ /**
+ * If the node is an item (bookmark, folder or a separator) this value is the
+ * time that the item was last modified. For other nodes, this value is 0.
+ *
+ * @note When an item is added lastModified is set to the same value as
+ * dateAdded.
+ */
+ readonly attribute PRTime lastModified;
+
+ /**
+ * For uri nodes, this is a sorted list of the tags, delimited with commans,
+ * for the uri represented by this node. Otherwise this is an empty string.
+ */
+ readonly attribute AString tags;
+
+ /**
+ * The unique ID associated with the page. It my return an empty string
+ * if the result node is a non-URI node.
+ */
+ readonly attribute ACString pageGuid;
+
+ /**
+ * The unique ID associated with the bookmark. It returns an empty string
+ * if the result node is not associated with a bookmark, a folder or a
+ * separator.
+ */
+ readonly attribute ACString bookmarkGuid;
+
+ /**
+ * The unique ID associated with the history visit. For node types other than
+ * history visit nodes, this value is -1.
+ */
+ readonly attribute long long visitId;
+
+ /**
+ * The unique ID associated with visit node which was the referrer of this
+ * history visit. For node types other than history visit nodes, or visits
+ * without any known referrer, this value is -1.
+ */
+ readonly attribute long long fromVisitId;
+
+ /**
+ * The transition type associated with this visit. For node types other than
+ * history visit nodes, this value is 0.
+ */
+ readonly attribute unsigned long visitType;
+};
+
+
+/**
+ * Base class for container results. This includes all types of groupings.
+ * Bookmark folders and places queries will be QueryResultNodes which extends
+ * these items.
+ */
+[scriptable, uuid(3E9CC95F-0D93-45F1-894F-908EEB9866D7)]
+interface nsINavHistoryContainerResultNode : nsINavHistoryResultNode
+{
+
+ /**
+ * Set this to allow descent into the container. When closed, attempting
+ * to call getChildren or childCount will result in an error. You should
+ * set this to false when you are done reading.
+ *
+ * For HOST and DAY groupings, doing this is free since the children have
+ * been precomputed. For queries and bookmark folders, being open means they
+ * will keep themselves up-to-date by listening for updates and re-querying
+ * as needed.
+ */
+ attribute boolean containerOpen;
+
+ /**
+ * Indicates whether the container is closed, loading, or opened. Loading
+ * implies that the container has been opened asynchronously and has not yet
+ * fully opened.
+ */
+ readonly attribute unsigned short state;
+ const unsigned short STATE_CLOSED = 0;
+ const unsigned short STATE_LOADING = 1;
+ const unsigned short STATE_OPENED = 2;
+
+ /**
+ * This indicates whether this node "may" have children, and can be used
+ * when the container is open or closed. When the container is closed, it
+ * will give you an exact answer if the node can easily be populated (for
+ * example, a bookmark folder). If not (for example, a complex history query),
+ * it will return true. When the container is open, it will always be
+ * accurate. It is intended to be used to see if we should draw the "+" next
+ * to a tree item.
+ */
+ readonly attribute boolean hasChildren;
+
+ /**
+ * This gives you the children of the nodes. It is preferrable to use this
+ * interface over the array one, since it avoids creating an nsIArray object
+ * and the interface is already the correct type.
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE if containerOpen is false.
+ */
+ readonly attribute unsigned long childCount;
+ nsINavHistoryResultNode getChild(in unsigned long aIndex);
+
+ /**
+ * Get the index of a direct child in this container.
+ *
+ * @param aNode
+ * a result node.
+ *
+ * @return aNode's index in this container.
+ * @throws NS_ERROR_NOT_AVAILABLE if containerOpen is false.
+ * @throws NS_ERROR_INVALID_ARG if aNode isn't a direct child of this
+ * container.
+ */
+ unsigned long getChildIndex(in nsINavHistoryResultNode aNode);
+
+ /**
+ * Look for a node in the container by some of its details. Does not search
+ * closed containers.
+ *
+ * @param aURI
+ * the node's uri attribute value
+ * @param aTime
+ * the node's time attribute value.
+ * @param aItemId
+ * the node's itemId attribute value.
+ * @param aRecursive
+ * whether or not to search recursively.
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE if this container is closed.
+ * @return a result node that matches the given details if any, null
+ * otherwise.
+ */
+ nsINavHistoryResultNode findNodeByDetails(in AUTF8String aURIString,
+ in PRTime aTime,
+ in long long aItemId,
+ in boolean aRecursive);
+};
+
+
+/**
+ * Used for places queries and as a base for bookmark folders.
+ *
+ * Note that if you request places to *not* be expanded in the options that
+ * generated this node, this item will report it has no children and never try
+ * to populate itself.
+ */
+[scriptable, uuid(62817759-4FEE-44A3-B58C-3E2F5AFC9D0A)]
+interface nsINavHistoryQueryResultNode : nsINavHistoryContainerResultNode
+{
+ /**
+ * Get the queries which build this node's children.
+ * Only valid for RESULT_TYPE_QUERY nodes.
+ */
+ void getQueries([optional] out unsigned long queryCount,
+ [retval,array,size_is(queryCount)] out nsINavHistoryQuery queries);
+
+ /**
+ * Get the options which group this node's children.
+ * Only valid for RESULT_TYPE_QUERY nodes.
+ */
+ readonly attribute nsINavHistoryQueryOptions queryOptions;
+
+ /**
+ * For both simple folder queries and folder shortcut queries, this is set to
+ * the concrete itemId of the folder (i.e. for folder shortcuts it's the
+ * target folder id). Otherwise, this is set to -1.
+ */
+ readonly attribute long long folderItemId;
+
+ /**
+ * For both simple folder queries and folder shortcut queries, this is set to
+ * the concrete guid of the folder (i.e. for folder shortcuts it's the target
+ * folder guid). Otherwise, this is set to an empty string.
+ */
+ readonly attribute ACString targetFolderGuid;
+};
+
+
+/**
+ * Allows clients to observe what is happening to a result as it updates itself
+ * according to history and bookmark system events. Register this observer on a
+ * result using nsINavHistoryResult::addObserver.
+ */
+[scriptable, uuid(f62d8b6b-3c4e-4a9f-a897-db605d0b7a0f)]
+interface nsINavHistoryResultObserver : nsISupports
+{
+ /**
+ * Called when 'aItem' is inserted into 'aParent' at index 'aNewIndex'.
+ * The item previously at index (if any) and everything below it will have
+ * been shifted down by one. The item may be a container or a leaf.
+ */
+ void nodeInserted(in nsINavHistoryContainerResultNode aParent,
+ in nsINavHistoryResultNode aNode,
+ in unsigned long aNewIndex);
+
+ /**
+ * Called whan 'aItem' is removed from 'aParent' at 'aOldIndex'. The item
+ * may be a container or a leaf. This function will be called after the item
+ * has been removed from its parent list, but before anything else (including
+ * NULLing out the item's parent) has happened.
+ */
+ void nodeRemoved(in nsINavHistoryContainerResultNode aParent,
+ in nsINavHistoryResultNode aItem,
+ in unsigned long aOldIndex);
+
+ /**
+ * Called whan 'aItem' is moved from 'aOldParent' at 'aOldIndex' to
+ * aNewParent at aNewIndex. The item may be a container or a leaf.
+ *
+ * XXX: at the moment, this method is called only when an item is moved
+ * within the same container. When an item is moved between containers,
+ * a new node is created for the item, and the itemRemoved/itemAdded methods
+ * are used.
+ */
+ void nodeMoved(in nsINavHistoryResultNode aNode,
+ in nsINavHistoryContainerResultNode aOldParent,
+ in unsigned long aOldIndex,
+ in nsINavHistoryContainerResultNode aNewParent,
+ in unsigned long aNewIndex);
+
+ /**
+ * Called right after aNode's title has changed.
+ *
+ * @param aNode
+ * a result node
+ * @param aNewTitle
+ * the new title
+ */
+ void nodeTitleChanged(in nsINavHistoryResultNode aNode,
+ in AUTF8String aNewTitle);
+
+ /**
+ * Called right after aNode's uri property has changed.
+ *
+ * @param aNode
+ * a result node
+ * @param aNewURI
+ * the new uri
+ */
+ void nodeURIChanged(in nsINavHistoryResultNode aNode,
+ in AUTF8String aNewURI);
+
+ /**
+ * Called right after aNode's icon property has changed.
+ *
+ * @param aNode
+ * a result node
+ *
+ * @note: The new icon is accessible through aNode.icon.
+ */
+ void nodeIconChanged(in nsINavHistoryResultNode aNode);
+
+ /**
+ * Called right after aNode's time property or accessCount property, or both,
+ * have changed.
+ *
+ * @param aNode
+ * a uri result node
+ * @param aNewVisitDate
+ * the new visit date
+ * @param aNewAccessCount
+ * the new access-count
+ */
+ void nodeHistoryDetailsChanged(in nsINavHistoryResultNode aNode,
+ in PRTime aNewVisitDate,
+ in unsigned long aNewAccessCount);
+
+ /**
+ * Called when the tags set on the uri represented by aNode have changed.
+ *
+ * @param aNode
+ * a uri result node
+ *
+ * @note: The new tags list is accessible through aNode.tags.
+ */
+ void nodeTagsChanged(in nsINavHistoryResultNode aNode);
+
+ /**
+ * Called right after the aNode's keyword property has changed.
+ *
+ * @param aNode
+ * a uri result node
+ * @param aNewKeyword
+ * the new keyword
+ */
+ void nodeKeywordChanged(in nsINavHistoryResultNode aNode,
+ in AUTF8String aNewKeyword);
+
+ /**
+ * Called right after an annotation of aNode's has changed (set, altered, or
+ * unset).
+ *
+ * @param aNode
+ * a result node
+ * @param aAnnoName
+ * the name of the annotation that changed
+ */
+ void nodeAnnotationChanged(in nsINavHistoryResultNode aNode,
+ in AUTF8String aAnnoName);
+
+ /**
+ * Called right after aNode's dateAdded property has changed.
+ *
+ * @param aNode
+ * a result node
+ * @param aNewValue
+ * the new value of the dateAdded property
+ */
+ void nodeDateAddedChanged(in nsINavHistoryResultNode aNode,
+ in PRTime aNewValue);
+
+ /**
+ * Called right after aNode's dateModified property has changed.
+ *
+ * @param aNode
+ * a result node
+ * @param aNewValue
+ * the new value of the dateModified property
+ */
+ void nodeLastModifiedChanged(in nsINavHistoryResultNode aNode,
+ in PRTime aNewValue);
+
+ /**
+ * Called after a container changes state.
+ *
+ * @param aContainerNode
+ * The container that has changed state.
+ * @param aOldState
+ * The state that aContainerNode has transitioned out of.
+ * @param aNewState
+ * The state that aContainerNode has transitioned into.
+ */
+ void containerStateChanged(in nsINavHistoryContainerResultNode aContainerNode,
+ in unsigned long aOldState,
+ in unsigned long aNewState);
+
+ /**
+ * Called when something significant has happened within the container. The
+ * contents of the container should be re-built.
+ *
+ * @param aContainerNode
+ * the container node to invalidate
+ */
+ void invalidateContainer(in nsINavHistoryContainerResultNode aContainerNode);
+
+ /**
+ * This is called to indicate to the UI that the sort has changed to the
+ * given mode. For trees, for example, this would update the column headers
+ * to reflect the sorting. For many other types of views, this won't be
+ * applicable.
+ *
+ * @param sortingMode One of nsINavHistoryQueryOptions.SORT_BY_* that
+ * indicates the new sorting mode.
+ *
+ * This only is expected to update the sorting UI. invalidateAll() will also
+ * get called if the sorting changes to update everything.
+ */
+ void sortingChanged(in unsigned short sortingMode);
+
+ /**
+ * This is called to indicate that a batch operation is about to start or end.
+ * The observer could want to disable some events or updates during batches,
+ * since multiple operations are packed in a short time.
+ * For example treeviews could temporarily suppress select notifications.
+ *
+ * @param aToggleMode
+ * true if a batch is starting, false if it's ending.
+ */
+ void batching(in boolean aToggleMode);
+
+ /**
+ * Called by the result when this observer is added.
+ */
+ attribute nsINavHistoryResult result;
+};
+
+
+/**
+ * TODO: Bug 517719.
+ *
+ * A predefined view adaptor for interfacing results with an nsITree. This
+ * object will remove itself from its associated result when the tree has been
+ * detached. This prevents circular references. Users should be aware of this,
+ * if you want to re-use the same viewer, you will need to keep your own
+ * reference to it and re-initialize it when the tree changes. If you use this
+ * object, attach it to a result, never attach it to a tree, and forget about
+ * it, it will leak!
+ */
+[scriptable, uuid(f8b518c0-1faf-11df-8a39-0800200c9a66)]
+interface nsINavHistoryResultTreeViewer : nsINavHistoryResultObserver
+{
+ /**
+ * This allows you to get at the real node for a given row index. This is
+ * only valid when a tree is attached.
+ */
+ nsINavHistoryResultNode nodeForTreeIndex(in unsigned long aIndex);
+
+ /**
+ * Reverse of nodeForFlatIndex, returns the row index for a given result node.
+ * Returns INDEX_INVISIBLE if the item is not visible (for example, its
+ * parent is collapsed). This is only valid when a tree is attached. The
+ * the result will always be INDEX_INVISIBLE if not.
+ *
+ * Note: This sounds sort of obvious, but it got me: aNode must be a node
+ * retrieved from the same result that this viewer is for. If you
+ * execute another query and get a node from a _different_ result, this
+ * function will always return the index of that node in the tree that
+ * is attached to that result.
+ */
+ const unsigned long INDEX_INVISIBLE = 0xffffffff;
+ unsigned long treeIndexForNode(in nsINavHistoryResultNode aNode);
+};
+
+
+/**
+ * The result of a history/bookmark query.
+ */
+[scriptable, uuid(c2229ce3-2159-4001-859c-7013c52f7619)]
+interface nsINavHistoryResult : nsISupports
+{
+ /**
+ * Sorts all nodes recursively by the given parameter, one of
+ * nsINavHistoryQueryOptions.SORT_BY_* This will update the corresponding
+ * options for this result, so that re-using the current options/queries will
+ * always give you the current view.
+ */
+ attribute unsigned short sortingMode;
+
+ /**
+ * The annotation to use in SORT_BY_ANNOTATION_* sorting modes, set this
+ * before setting the sortingMode attribute.
+ */
+ attribute AUTF8String sortingAnnotation;
+
+ /**
+ * Whether or not notifications on result changes are suppressed.
+ * Initially set to false.
+ *
+ * Use this to avoid flickering and to improve performance when you
+ * do temporary changes to the result structure (e.g. when searching for a
+ * node recursively).
+ */
+ attribute boolean suppressNotifications;
+
+ /**
+ * Adds an observer for changes done in the result.
+ *
+ * @param aObserver
+ * a result observer.
+ * @param aOwnsWeak
+ * If false, the result will keep an owning reference to the observer,
+ * which must be removed using removeObserver.
+ * If true, the result will keep a weak reference to the observer, which
+ * must implement nsISupportsWeakReference.
+ *
+ * @see nsINavHistoryResultObserver
+ */
+ void addObserver(in nsINavHistoryResultObserver aObserver, in boolean aOwnsWeak);
+
+ /**
+ * Removes an observer that was added by addObserver.
+ *
+ * @param aObserver
+ * a result observer that was added by addObserver.
+ */
+ void removeObserver(in nsINavHistoryResultObserver aObserver);
+
+ /**
+ * This is the root of the results. Remember that you need to open all
+ * containers for their contents to be valid.
+ *
+ * When a result goes out of scope it will continue to observe changes till
+ * it is cycle collected. While the result waits to be collected it will stay
+ * in memory, and continue to update itself, potentially causing unwanted
+ * additional work. When you close the root node the result will stop
+ * observing changes, so it is good practice to close the root node when you
+ * are done with a result, since that will avoid unwanted performance hits.
+ */
+ readonly attribute nsINavHistoryContainerResultNode root;
+};
+
+
+/**
+ * Similar to nsIRDFObserver for history. Note that we don't pass the data
+ * source since that is always the global history.
+ *
+ * DANGER! If you are in the middle of a batch transaction, there may be a
+ * database transaction active. You can still access the DB, but be careful.
+ */
+[scriptable, uuid(0f0f45b0-13a1-44ae-a0ab-c6046ec6d4da)]
+interface nsINavHistoryObserver : nsISupports
+{
+ /**
+ * Notifies you that a bunch of things are about to change, don't do any
+ * heavy-duty processing until onEndUpdateBatch is called.
+ */
+ void onBeginUpdateBatch();
+
+ /**
+ * Notifies you that we are done doing a bunch of things and you should go
+ * ahead and update UI, etc.
+ */
+ void onEndUpdateBatch();
+
+ /**
+ * Called everytime a URI is visited.
+ *
+ * @note TRANSITION_EMBED visits (corresponding to images in a page, for
+ * example) are not displayed in history results. Most observers can
+ * ignore TRANSITION_EMBED visit notifications (which will comprise the
+ * majority of visit notifications) to save work.
+ *
+ * @param aVisitId
+ * Id of the visit that was just created.
+ * @param aTime
+ * Time of the visit.
+ * @param aSessionId
+ * No longer supported and always set to 0.
+ * @param aReferrerVisitId
+ * The id of the visit the user came from, defaults to 0 for no referrer.
+ * @param aTransitionType
+ * One of nsINavHistory.TRANSITION_*
+ * @param aGuid
+ * The unique id associated with the page.
+ * @param aHidden
+ * Whether the visited page is marked as hidden.
+ * @param aVisitCount
+ * Number of visits (included this one) for this URI.
+ * @param aTyped
+ * Whether the URI has been typed or not.
+ * TODO (Bug 1271801): This will become a count, rather than a boolean.
+ * For future compatibility, always compare it with "> 0".
+ */
+ void onVisit(in nsIURI aURI,
+ in long long aVisitId,
+ in PRTime aTime,
+ in long long aSessionId,
+ in long long aReferrerVisitId,
+ in unsigned long aTransitionType,
+ in ACString aGuid,
+ in boolean aHidden,
+ in unsigned long aVisitCount,
+ in unsigned long aTyped);
+
+ /**
+ * Called whenever either the "real" title or the custom title of the page
+ * changed. BOTH TITLES ARE ALWAYS INCLUDED in this notification, even though
+ * only one will change at a time. Often, consumers will want to display the
+ * user title if it is available, and fall back to the page title (the one
+ * specified in the <title> tag of the page).
+ *
+ * Note that there is a difference between an empty title and a NULL title.
+ * An empty string means that somebody specifically set the title to be
+ * nothing. NULL means nobody set it. From C++: use IsVoid() and SetIsVoid()
+ * to see whether an empty string is "null" or not (it will always be an
+ * empty string in either case).
+ *
+ * @param aURI
+ * The URI of the page.
+ * @param aPageTitle
+ * The new title of the page.
+ * @param aGUID
+ * The unique ID associated with the page.
+ */
+ void onTitleChanged(in nsIURI aURI,
+ in AString aPageTitle,
+ in ACString aGUID);
+
+ /**
+ * Called when an individual page's frecency has changed.
+ *
+ * This is not called for pages whose frecencies change as the result of some
+ * large operation where some large or unknown number of frecencies change at
+ * once. Use onManyFrecenciesChanged to detect such changes.
+ *
+ * @param aURI
+ * The page's URI.
+ * @param aNewFrecency
+ * The page's new frecency.
+ * @param aGUID
+ * The page's GUID.
+ * @param aHidden
+ * True if the page is marked as hidden.
+ * @param aVisitDate
+ * The page's last visit date.
+ */
+ void onFrecencyChanged(in nsIURI aURI,
+ in long aNewFrecency,
+ in ACString aGUID,
+ in boolean aHidden,
+ in PRTime aVisitDate);
+
+ /**
+ * Called when the frecencies of many pages have changed at once.
+ *
+ * onFrecencyChanged is not called for each of those pages.
+ */
+ void onManyFrecenciesChanged();
+
+ /**
+ * Removed by the user.
+ */
+ const unsigned short REASON_DELETED = 0;
+ /**
+ * Removed by automatic expiration.
+ */
+ const unsigned short REASON_EXPIRED = 1;
+
+ /**
+ * This page and all of its visits are being deleted. Note: the page may not
+ * necessarily have actually existed for this function to be called.
+ *
+ * Delete notifications are only 99.99% accurate. Batch delete operations
+ * must be done in two steps, so first come notifications, then a bulk
+ * delete. If there is some error in the middle (for example, out of memory)
+ * then you'll get a notification and it won't get deleted. There's no easy
+ * way around this.
+ *
+ * @param aURI
+ * The URI that was deleted.
+ * @param aGUID
+ * The unique ID associated with the page.
+ * @param aReason
+ * Indicates the reason for the removal. see REASON_* constants.
+ */
+ void onDeleteURI(in nsIURI aURI,
+ in ACString aGUID,
+ in unsigned short aReason);
+
+ /**
+ * Notification that all of history is being deleted.
+ */
+ void onClearHistory();
+
+ /**
+ * onPageChanged attribute indicating that favicon has been updated.
+ * aNewValue parameter will be set to the new favicon URI string.
+ */
+ const unsigned long ATTRIBUTE_FAVICON = 3;
+
+ /**
+ * An attribute of this page changed.
+ *
+ * @param aURI
+ * The URI of the page on which an attribute changed.
+ * @param aChangedAttribute
+ * The attribute whose value changed. See ATTRIBUTE_* constants.
+ * @param aNewValue
+ * The attribute's new value.
+ * @param aGUID
+ * The unique ID associated with the page.
+ */
+ void onPageChanged(in nsIURI aURI,
+ in unsigned long aChangedAttribute,
+ in AString aNewValue,
+ in ACString aGUID);
+
+ /**
+ * Called when some visits of an history entry are expired.
+ *
+ * @param aURI
+ * The page whose visits have been expired.
+ * @param aVisitTime
+ * The largest visit time in microseconds that has been expired. We
+ * guarantee that we don't have any visit older than this date.
+ * @param aGUID
+ * The unique ID associated with the page.
+ *
+ * @note: when all visits for a page are expired and also the full page entry
+ * is expired, you will only get an onDeleteURI notification. If a
+ * page entry is removed, then you can be sure that we don't have
+ * anymore visits for it.
+ * @param aReason
+ * Indicates the reason for the removal. see REASON_* constants.
+ * @param aTransitionType
+ * If it's a valid TRANSITION_* value, all visits of the specified type
+ * have been removed.
+ */
+ void onDeleteVisits(in nsIURI aURI,
+ in PRTime aVisitTime,
+ in ACString aGUID,
+ in unsigned short aReason,
+ in unsigned long aTransitionType);
+};
+
+
+/**
+ * This object encapsulates all the query parameters you're likely to need
+ * when building up history UI. All parameters are ANDed together.
+ *
+ * This is not intended to be a super-general query mechanism. This was designed
+ * so that most queries can be done in only one SQL query. This is important
+ * because, if the user has their profile on a networked drive, query latency
+ * can be non-negligible.
+ */
+
+[scriptable, uuid(dc87ae79-22f1-4dcf-975b-852b01d210cb)]
+interface nsINavHistoryQuery : nsISupports
+{
+ /**
+ * Time range for results (INCLUSIVE). The *TimeReference is one of the
+ * constants TIME_RELATIVE_* which indicates how to interpret the
+ * corresponding time value.
+ * TIME_RELATIVE_EPOCH (default):
+ * The time is relative to Jan 1 1970 GMT, (this is a normal PRTime)
+ * TIME_RELATIVE_TODAY:
+ * The time is relative to this morning at midnight. Normally used for
+ * queries relative to today. For example, a "past week" query would be
+ * today-6 days -> today+1 day
+ * TIME_RELATIVE_NOW:
+ * The time is relative to right now.
+ *
+ * Note: PRTime is in MICROseconds since 1 Jan 1970. Javascript date objects
+ * are expressed in MILLIseconds since 1 Jan 1970.
+ *
+ * As a special case, a 0 time relative to TIME_RELATIVE_EPOCH indicates that
+ * the time is not part of the query. This is the default, so an empty query
+ * will match any time. The has* functions return whether the corresponding
+ * time is considered.
+ *
+ * You can read absolute*Time to get the time value that the currently loaded
+ * reference points + offset resolve to.
+ */
+ const unsigned long TIME_RELATIVE_EPOCH = 0;
+ const unsigned long TIME_RELATIVE_TODAY = 1;
+ const unsigned long TIME_RELATIVE_NOW = 2;
+
+ attribute PRTime beginTime;
+ attribute unsigned long beginTimeReference;
+ readonly attribute boolean hasBeginTime;
+ readonly attribute PRTime absoluteBeginTime;
+
+ attribute PRTime endTime;
+ attribute unsigned long endTimeReference;
+ readonly attribute boolean hasEndTime;
+ readonly attribute PRTime absoluteEndTime;
+
+ /**
+ * Text search terms.
+ */
+ attribute AString searchTerms;
+ readonly attribute boolean hasSearchTerms;
+
+ /**
+ * Set lower or upper limits for how many times an item has been
+ * visited. The default is -1, and in that case all items are
+ * matched regardless of their visit count.
+ */
+ attribute long minVisits;
+ attribute long maxVisits;
+
+ /**
+ * When the set of transitions is nonempty, results are limited to pages which
+ * have at least one visit for each of the transition types.
+ * @note: For searching on more than one transition this can be very slow.
+ *
+ * Limit results to the specified list of transition types.
+ */
+ void setTransitions([const,array, size_is(count)] in unsigned long transitions,
+ in unsigned long count);
+
+ /**
+ * Get the transitions set for this query.
+ */
+ void getTransitions([optional] out unsigned long count,
+ [retval,array,size_is(count)] out unsigned long transitions);
+
+ /**
+ * Get the count of the set query transitions.
+ */
+ readonly attribute unsigned long transitionCount;
+
+ /**
+ * When set, returns only bookmarked items, when unset, returns anything. Setting this
+ * is equivalent to listing all bookmark folders in the 'folders' parameter.
+ */
+ attribute boolean onlyBookmarked;
+
+ /**
+ * This controls the meaning of 'domain', and whether it is an exact match
+ * 'domainIsHost' = true, or hierarchical (= false).
+ */
+ attribute boolean domainIsHost;
+
+ /**
+ * This is the host or domain name (controlled by domainIsHost). When
+ * domainIsHost, domain only does exact matching on host names. Otherwise,
+ * it will return anything whose host name ends in 'domain'.
+ *
+ * This one is a little different than most. Setting it to an empty string
+ * is a real query and will match any URI that has no host name (local files
+ * and such). Set this to NULL (in C++ use SetIsVoid) if you don't want
+ * domain matching.
+ */
+ attribute AUTF8String domain;
+ readonly attribute boolean hasDomain;
+
+ /**
+ * This is a URI to match, to, for example, find out every time you visited
+ * a given URI. This is an exact match.
+ */
+ attribute nsIURI uri;
+ readonly attribute boolean hasUri;
+
+ /**
+ * Test for existence or non-existence of a given annotation. We don't
+ * currently support >1 annotation name per query. If 'annotationIsNot' is
+ * true, we test for the non-existence of the specified annotation.
+ *
+ * Testing for not annotation will do the same thing as a normal query and
+ * remove everything that doesn't have that annotation. Asking for things
+ * that DO have a given annotation is a little different. It also includes
+ * things that have never been visited. This allows place queries to be
+ * returned as well as anything else that may have been tagged with an
+ * annotation. This will only work for RESULTS_AS_URI since there will be
+ * no visits for these items.
+ */
+ attribute boolean annotationIsNot;
+ attribute AUTF8String annotation;
+ readonly attribute boolean hasAnnotation;
+
+ /**
+ * Limit results to items that are tagged with all of the given tags. This
+ * attribute must be set to an array of strings. When called as a getter it
+ * will return an array of strings sorted ascending in lexicographical order.
+ * The array may be empty in either case. Duplicate tags may be specified
+ * when setting the attribute, but the getter returns only unique tags.
+ *
+ * To search for items that are tagged with any given tags rather than all,
+ * multiple queries may be passed to nsINavHistoryService.executeQueries().
+ */
+ attribute nsIVariant tags;
+
+ /**
+ * If 'tagsAreNot' is true, the results are instead limited to items that
+ * are not tagged with any of the given tags. This attribute is used in
+ * conjunction with the 'tags' attribute.
+ */
+ attribute boolean tagsAreNot;
+
+ /**
+ * Limit results to items that are in all of the given folders.
+ */
+ void getFolders([optional] out unsigned long count,
+ [retval,array,size_is(count)] out long long folders);
+ readonly attribute unsigned long folderCount;
+
+ /**
+ * For the special result type RESULTS_AS_TAG_CONTENTS we can define only
+ * one folder that must be a tag folder. This is not recursive so results
+ * will be returned from the first level of that folder.
+ */
+ void setFolders([const,array, size_is(folderCount)] in long long folders,
+ in unsigned long folderCount);
+
+ /**
+ * Creates a new query item with the same parameters of this one.
+ */
+ nsINavHistoryQuery clone();
+};
+
+/**
+ * This object represents the global options for executing a query.
+ */
+[scriptable, uuid(8198dfa7-8061-4766-95cb-fa86b3c00a47)]
+interface nsINavHistoryQueryOptions : nsISupports
+{
+ /**
+ * You can ask for the results to be pre-sorted. Since the DB has indices
+ * of many items, it can produce sorted results almost for free. These should
+ * be self-explanatory.
+ *
+ * Note: re-sorting is slower, as is sorting by title or when you have a
+ * host name.
+ *
+ * For bookmark items, SORT_BY_NONE means sort by the natural bookmark order.
+ */
+ const unsigned short SORT_BY_NONE = 0;
+ const unsigned short SORT_BY_TITLE_ASCENDING = 1;
+ const unsigned short SORT_BY_TITLE_DESCENDING = 2;
+ const unsigned short SORT_BY_DATE_ASCENDING = 3;
+ const unsigned short SORT_BY_DATE_DESCENDING = 4;
+ const unsigned short SORT_BY_URI_ASCENDING = 5;
+ const unsigned short SORT_BY_URI_DESCENDING = 6;
+ const unsigned short SORT_BY_VISITCOUNT_ASCENDING = 7;
+ const unsigned short SORT_BY_VISITCOUNT_DESCENDING = 8;
+ const unsigned short SORT_BY_KEYWORD_ASCENDING = 9;
+ const unsigned short SORT_BY_KEYWORD_DESCENDING = 10;
+ const unsigned short SORT_BY_DATEADDED_ASCENDING = 11;
+ const unsigned short SORT_BY_DATEADDED_DESCENDING = 12;
+ const unsigned short SORT_BY_LASTMODIFIED_ASCENDING = 13;
+ const unsigned short SORT_BY_LASTMODIFIED_DESCENDING = 14;
+ const unsigned short SORT_BY_TAGS_ASCENDING = 17;
+ const unsigned short SORT_BY_TAGS_DESCENDING = 18;
+ const unsigned short SORT_BY_ANNOTATION_ASCENDING = 19;
+ const unsigned short SORT_BY_ANNOTATION_DESCENDING = 20;
+ const unsigned short SORT_BY_FRECENCY_ASCENDING = 21;
+ const unsigned short SORT_BY_FRECENCY_DESCENDING = 22;
+
+ /**
+ * "URI" results, one for each URI visited in the range. Individual result
+ * nodes will be of type "URI".
+ */
+ const unsigned short RESULTS_AS_URI = 0;
+
+ /**
+ * "Visit" results, with one for each time a page was visited (this will
+ * often give you multiple results for one URI). Individual result nodes will
+ * have type "Visit"
+ *
+ * @note This result type is only supported by QUERY_TYPE_HISTORY.
+ */
+ const unsigned short RESULTS_AS_VISIT = 1;
+
+ /**
+ * This is identical to RESULT_TYPE_VISIT except that individual result nodes
+ * will have type "FullVisit". This is used for the attributes that are not
+ * commonly accessed to save space in the common case (the lists can be very
+ * long).
+ *
+ * @note Not yet implemented. See bug 409662.
+ * @note This result type is only supported by QUERY_TYPE_HISTORY.
+ */
+ const unsigned short RESULTS_AS_FULL_VISIT = 2;
+
+ /**
+ * This returns query nodes for each predefined date range where we
+ * had visits. The node contains information how to load its content:
+ * - visits for the given date range will be loaded.
+ *
+ * @note This result type is only supported by QUERY_TYPE_HISTORY.
+ */
+ const unsigned short RESULTS_AS_DATE_QUERY = 3;
+
+ /**
+ * This returns nsINavHistoryQueryResultNode nodes for each site where we
+ * have visits. The node contains information how to load its content:
+ * - last visit for each url in the given host will be loaded.
+ *
+ * @note This result type is only supported by QUERY_TYPE_HISTORY.
+ */
+ const unsigned short RESULTS_AS_SITE_QUERY = 4;
+
+ /**
+ * This returns nsINavHistoryQueryResultNode nodes for each day where we
+ * have visits. The node contains information how to load its content:
+ * - list of hosts visited in the given period will be loaded.
+ *
+ * @note This result type is only supported by QUERY_TYPE_HISTORY.
+ */
+ const unsigned short RESULTS_AS_DATE_SITE_QUERY = 5;
+
+ /**
+ * This returns nsINavHistoryQueryResultNode nodes for each tag.
+ * The node contains information how to load its content:
+ * - list of bookmarks with the given tag will be loaded.
+ *
+ * @note Setting this resultType will force queryType to QUERY_TYPE_BOOKMARKS.
+ */
+ const unsigned short RESULTS_AS_TAG_QUERY = 6;
+
+ /**
+ * This is a container with an URI result type that contains the last
+ * modified bookmarks for the given tag.
+ * Tag folder id must be defined in the query.
+ *
+ * @note Setting this resultType will force queryType to QUERY_TYPE_BOOKMARKS.
+ */
+ const unsigned short RESULTS_AS_TAG_CONTENTS = 7;
+
+ /**
+ * The sorting mode to be used for this query.
+ * mode is one of SORT_BY_*
+ */
+ attribute unsigned short sortingMode;
+
+ /**
+ * The annotation to use in SORT_BY_ANNOTATION_* sorting modes.
+ */
+ attribute AUTF8String sortingAnnotation;
+
+ /**
+ * Sets the result type. One of RESULT_TYPE_* which includes how URIs are
+ * represented.
+ */
+ attribute unsigned short resultType;
+
+ /**
+ * This option excludes all URIs and separators from a bookmarks query.
+ * This would be used if you just wanted a list of bookmark folders and
+ * queries (such as the left pane of the places page).
+ * Defaults to false.
+ */
+ attribute boolean excludeItems;
+
+ /**
+ * Set to true to exclude queries ("place:" URIs) from the query results.
+ * Simple folder queries (bookmark folder symlinks) will still be included.
+ * Defaults to false.
+ */
+ attribute boolean excludeQueries;
+
+ /**
+ * DO NOT USE THIS API. IT'LL BE REMOVED IN BUG 1072833.
+ *
+ * Set to true to exclude live bookmarks from the query results.
+ */
+ attribute boolean excludeReadOnlyFolders;
+
+ /**
+ * When set, allows items with "place:" URIs to appear as containers,
+ * with the container's contents filled in from the stored query.
+ * If not set, these will appear as normal items. Doesn't do anything if
+ * excludeQueries is set. Defaults to false.
+ *
+ * Note that this has no effect on folder links, which are place: URIs
+ * returned by nsINavBookmarkService.GetFolderURI. These are always expanded
+ * and will appear as bookmark folders.
+ */
+ attribute boolean expandQueries;
+
+ /**
+ * Some pages in history are marked "hidden" and thus don't appear by default
+ * in queries. These include automatic framed visits and redirects. Setting
+ * this attribute will return all pages, even hidden ones. Does nothing for
+ * bookmark queries. Defaults to false.
+ */
+ attribute boolean includeHidden;
+
+ /**
+ * This is the maximum number of results that you want. The query is exeucted,
+ * the results are sorted, and then the top 'maxResults' results are taken
+ * and returned. Set to 0 (the default) to get all results.
+ *
+ * THIS DOES NOT WORK IN CONJUNCTION WITH SORTING BY TITLE. This is because
+ * sorting by title requires us to sort after using locale-sensetive sorting
+ * (as opposed to letting the database do it for us).
+ *
+ * Instead, we get the result ordered by date, pick the maxResult most recent
+ * ones, and THEN sort by title.
+ */
+ attribute unsigned long maxResults;
+
+ const unsigned short QUERY_TYPE_HISTORY = 0;
+ const unsigned short QUERY_TYPE_BOOKMARKS = 1;
+ /* Unified queries are not yet implemented. See bug 378798 */
+ const unsigned short QUERY_TYPE_UNIFIED = 2;
+
+ /**
+ * The type of search to use when querying the DB; This attribute is only
+ * honored by query nodes. It is silently ignored for simple folder queries.
+ */
+ attribute unsigned short queryType;
+
+ /**
+ * When this is true, the root container node generated by these options and
+ * its descendant containers will be opened asynchronously if they support it.
+ * This is false by default.
+ *
+ * @note Currently only bookmark folder containers support being opened
+ * asynchronously.
+ */
+ attribute boolean asyncEnabled;
+
+ /**
+ * Creates a new options item with the same parameters of this one.
+ */
+ nsINavHistoryQueryOptions clone();
+};
+
+[scriptable, uuid(8a1f527e-c9d7-4a51-bf0c-d86f0379b701)]
+interface nsINavHistoryService : nsISupports
+{
+ /**
+ * System Notifications:
+ *
+ * places-init-complete - Sent once the History service is completely
+ * initialized successfully.
+ * places-database-locked - Sent if initialization of the History service
+ * failed due to the inability to open the places.sqlite
+ * for access reasons.
+ */
+
+ /**
+ * This transition type means the user followed a link and got a new toplevel
+ * window.
+ */
+ const unsigned long TRANSITION_LINK = 1;
+
+ /**
+ * This transition type means that the user typed the page's URL in the
+ * URL bar or selected it from URL bar autocomplete results, clicked on
+ * it from a history query (from the History sidebar, History menu,
+ * or history query in the personal toolbar or Places organizer.
+ */
+ const unsigned long TRANSITION_TYPED = 2;
+
+ /**
+ * This transition is set when the user followed a bookmark to get to the
+ * page.
+ */
+ const unsigned long TRANSITION_BOOKMARK = 3;
+
+ /**
+ * This transition type is set when some inner content is loaded. This is
+ * true of all images on a page, and the contents of the iframe. It is also
+ * true of any content in a frame if the user did not explicitly follow
+ * a link to get there.
+ */
+ const unsigned long TRANSITION_EMBED = 4;
+
+ /**
+ * Set when the transition was a permanent redirect.
+ */
+ const unsigned long TRANSITION_REDIRECT_PERMANENT = 5;
+
+ /**
+ * Set when the transition was a temporary redirect.
+ */
+ const unsigned long TRANSITION_REDIRECT_TEMPORARY = 6;
+
+ /**
+ * Set when the transition is a download.
+ */
+ const unsigned long TRANSITION_DOWNLOAD = 7;
+
+ /**
+ * This transition type means the user followed a link and got a visit in
+ * a frame.
+ */
+ const unsigned long TRANSITION_FRAMED_LINK = 8;
+
+ /**
+ * This transition type means the page has been reloaded.
+ */
+ const unsigned long TRANSITION_RELOAD = 9;
+
+ /**
+ * Set when database is coherent
+ */
+ const unsigned short DATABASE_STATUS_OK = 0;
+
+ /**
+ * Set when database did not exist and we created a new one
+ */
+ const unsigned short DATABASE_STATUS_CREATE = 1;
+
+ /**
+ * Set when database was corrupt and we replaced it
+ */
+ const unsigned short DATABASE_STATUS_CORRUPT = 2;
+
+ /**
+ * Set when database schema has been upgraded
+ */
+ const unsigned short DATABASE_STATUS_UPGRADED = 3;
+
+ /**
+ * Returns the current database status
+ */
+ readonly attribute unsigned short databaseStatus;
+
+ /**
+ * True if there is any history. This can be used in UI to determine whether
+ * the "clear history" button should be enabled or not. This is much better
+ * than using BrowserHistory.count since that can be very slow if there is
+ * a lot of history (it must enumerate each item). This is pretty fast.
+ */
+ readonly attribute boolean hasHistoryEntries;
+
+ /**
+ * Gets the original title of the page.
+ * @deprecated use mozIAsyncHistory.getPlacesInfo instead.
+ */
+ AString getPageTitle(in nsIURI aURI);
+
+ /**
+ * This is just like markPageAsTyped (in nsIBrowserHistory, also implemented
+ * by the history service), but for bookmarks. It declares that the given URI
+ * is being opened as a result of following a bookmark. If this URI is loaded
+ * soon after this message has been received, that transition will be marked
+ * as following a bookmark.
+ */
+ void markPageAsFollowedBookmark(in nsIURI aURI);
+
+ /**
+ * Designates the url as having been explicitly typed in by the user.
+ *
+ * @param aURI
+ * URI of the page to be marked.
+ */
+ void markPageAsTyped(in nsIURI aURI);
+
+ /**
+ * Designates the url as coming from a link explicitly followed by
+ * the user (for example by clicking on it).
+ *
+ * @param aURI
+ * URI of the page to be marked.
+ */
+ void markPageAsFollowedLink(in nsIURI aURI);
+
+ /**
+ * Returns true if this URI would be added to the history. You don't have to
+ * worry about calling this, adding a visit will always check before
+ * actually adding the page. This function is public because some components
+ * may want to check if this page would go in the history (i.e. for
+ * annotations).
+ */
+ boolean canAddURI(in nsIURI aURI);
+
+ /**
+ * This returns a new query object that you can pass to executeQuer[y/ies].
+ * It will be initialized to all empty (so using it will give you all history).
+ */
+ nsINavHistoryQuery getNewQuery();
+
+ /**
+ * This returns a new options object that you can pass to executeQuer[y/ies]
+ * after setting the desired options.
+ */
+ nsINavHistoryQueryOptions getNewQueryOptions();
+
+ /**
+ * Executes a single query.
+ */
+ nsINavHistoryResult executeQuery(in nsINavHistoryQuery aQuery,
+ in nsINavHistoryQueryOptions options);
+
+ /**
+ * Executes an array of queries. All of the query objects are ORed
+ * together. Within a query, all the terms are ANDed together as in
+ * executeQuery. See executeQuery()
+ */
+ nsINavHistoryResult executeQueries(
+ [array,size_is(aQueryCount)] in nsINavHistoryQuery aQueries,
+ in unsigned long aQueryCount,
+ in nsINavHistoryQueryOptions options);
+
+ /**
+ * Converts a query URI-like string to an array of actual query objects for
+ * use to executeQueries(). The output query array may be empty if there is
+ * no information. However, there will always be an options structure returned
+ * (if nothing is defined, it will just have the default values).
+ */
+ void queryStringToQueries(in AUTF8String aQueryString,
+ [array, size_is(aResultCount)] out nsINavHistoryQuery aQueries,
+ out unsigned long aResultCount,
+ out nsINavHistoryQueryOptions options);
+
+ /**
+ * Converts a query into an equivalent string that can be persisted. Inverse
+ * of queryStringToQueries()
+ */
+ AUTF8String queriesToQueryString(
+ [array, size_is(aQueryCount)] in nsINavHistoryQuery aQueries,
+ in unsigned long aQueryCount,
+ in nsINavHistoryQueryOptions options);
+
+ /**
+ * Adds a history observer. If ownsWeak is false, the history service will
+ * keep an owning reference to the observer. If ownsWeak is true, then
+ * aObserver must implement nsISupportsWeakReference, and the history service
+ * will keep a weak reference to the observer.
+ */
+ void addObserver(in nsINavHistoryObserver observer, in boolean ownsWeak);
+
+ /**
+ * Removes a history observer.
+ */
+ void removeObserver(in nsINavHistoryObserver observer);
+
+ /**
+ * Gets an array of registered nsINavHistoryObserver objects.
+ */
+ void getObservers([optional] out unsigned long count,
+ [retval, array, size_is(count)] out nsINavHistoryObserver observers);
+
+ /**
+ * Runs the passed callback in batch mode. Use this when a lot of things
+ * are about to change. Calls can be nested, observers will only be
+ * notified when all batches begin/end.
+ *
+ * @param aCallback
+ * nsINavHistoryBatchCallback interface to call.
+ * @param aUserData
+ * Opaque parameter passed to nsINavBookmarksBatchCallback
+ */
+ void runInBatchMode(in nsINavHistoryBatchCallback aCallback,
+ in nsISupports aClosure);
+
+ /**
+ * True if history is disabled. currently,
+ * history is disabled if the places.history.enabled pref is false.
+ */
+ readonly attribute boolean historyDisabled;
+
+ /**
+ * Clear all TRANSITION_EMBED visits.
+ */
+ void clearEmbedVisits();
+};
+
+/**
+ * @see runInBatchMode of nsINavHistoryService/nsINavBookmarksService
+ */
+[scriptable, function, uuid(5a5a9154-95ac-4e3d-90df-558816297407)]
+interface nsINavHistoryBatchCallback : nsISupports {
+ void runBatched(in nsISupports aUserData);
+};
diff --git a/toolkit/components/places/nsITaggingService.idl b/toolkit/components/places/nsITaggingService.idl
new file mode 100644
index 0000000000..f3731feb62
--- /dev/null
+++ b/toolkit/components/places/nsITaggingService.idl
@@ -0,0 +1,95 @@
+/* -*- 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 nsIURI;
+interface nsIVariant;
+
+[scriptable, uuid(9759bd0e-78e2-4421-9ed1-c676e1af3513)]
+interface nsITaggingService : nsISupports
+{
+
+ /**
+ * Defines the maximal length of a tag. Related to the bug 407821
+ * (https://bugzilla.mozilla.org/show_bug.cgi?id=407821)
+ */
+ const unsigned long MAX_TAG_LENGTH = 100;
+
+ /**
+ * Tags a URL with the given set of tags. Current tags set for the URL
+ * persist. Tags in aTags which are already set for the given URL are
+ * ignored.
+ *
+ * @param aURI
+ * the URL to tag.
+ * @param aTags
+ * Array of tags to set for the given URL. Each element within the
+ * array can be either a tag name (non-empty string) or a concrete
+ * itemId of a tag container.
+ * @param [optional] aSource
+ * A change source constant from nsINavBookmarksService::SOURCE_*.
+ * Defaults to SOURCE_DEFAULT if omitted.
+ */
+ void tagURI(in nsIURI aURI,
+ in nsIVariant aTags,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Removes tags from a URL. Tags from aTags which are not set for the
+ * given URL are ignored.
+ *
+ * @param aURI
+ * the URL to un-tag.
+ * @param aTags
+ * Array of tags to unset. Pass null to remove all tags from the given
+ * url. Each element within the array can be either a tag name
+ * (non-empty string) or a concrete itemId of a tag container.
+ * @param [optional] aSource
+ * A change source constant from nsINavBookmarksService::SOURCE_*.
+ * Defaults to SOURCE_DEFAULT if omitted.
+ */
+ void untagURI(in nsIURI aURI,
+ in nsIVariant aTags,
+ [optional] in unsigned short aSource);
+
+ /**
+ * Retrieves all URLs tagged with the given tag.
+ *
+ * @param aTag
+ * tag name
+ * @returns Array of uris tagged with aTag.
+ */
+ nsIVariant getURIsForTag(in AString aTag);
+
+ /**
+ * Retrieves all tags set for the given URL.
+ *
+ * @param aURI
+ * a URL.
+ * @returns array of tags (sorted by name).
+ */
+ void getTagsForURI(in nsIURI aURI,
+ [optional] out unsigned long length,
+ [retval, array, size_is(length)] out wstring aTags);
+
+ /**
+ * Retrieves all tags used to tag URIs in the data-base (sorted by name).
+ */
+ readonly attribute nsIVariant allTags;
+
+ /**
+ * Whether any tags exist.
+ *
+ * @note This is faster than allTags.length, since doesn't need to sort tags.
+ */
+ readonly attribute boolean hasTags;
+};
+
+%{C++
+
+#define TAGGING_SERVICE_CID "@mozilla.org/browser/tagging-service;1"
+
+%}
diff --git a/toolkit/components/places/nsLivemarkService.js b/toolkit/components/places/nsLivemarkService.js
new file mode 100644
index 0000000000..eeca7e1399
--- /dev/null
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -0,0 +1,891 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+// Modules and services.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
+ // Lazily add an history observer when it's actually needed.
+ PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
+ return PlacesUtils.asyncHistory;
+});
+
+// Constants
+
+// Delay between reloads of consecute livemarks.
+const RELOAD_DELAY_MS = 500;
+// Expire livemarks after this time.
+const EXPIRE_TIME_MS = 3600000; // 1 hour.
+// Expire livemarks after this time on error.
+const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes.
+
+// Livemarks cache.
+
+XPCOMUtils.defineLazyGetter(this, "CACHE_SQL", () => {
+ function getAnnoSQLFragment(aAnnoParam) {
+ return `SELECT a.content
+ FROM moz_items_annos a
+ JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
+ WHERE a.item_id = b.id
+ AND n.name = ${aAnnoParam}`;
+ }
+
+ return `SELECT b.id, b.title, b.parent As parentId, b.position AS 'index',
+ b.guid, b.dateAdded, b.lastModified, p.guid AS parentGuid,
+ ( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
+ ( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
+ FROM moz_bookmarks b
+ JOIN moz_bookmarks p ON b.parent = p.id
+ JOIN moz_items_annos a ON a.item_id = b.id
+ JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
+ WHERE b.type = :folder_type
+ AND n.name = :feedURI_anno`;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gLivemarksCachePromised",
+ Task.async(function* () {
+ let livemarksMap = new Map();
+ let conn = yield PlacesUtils.promiseDBConnection();
+ let rows = yield conn.executeCached(CACHE_SQL,
+ { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
+ feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
+ siteURI_anno: PlacesUtils.LMANNO_SITEURI });
+ for (let row of rows) {
+ let siteURI = row.getResultByName("siteURI");
+ let livemark = new Livemark({
+ id: row.getResultByName("id"),
+ guid: row.getResultByName("guid"),
+ title: row.getResultByName("title"),
+ parentId: row.getResultByName("parentId"),
+ parentGuid: row.getResultByName("parentGuid"),
+ index: row.getResultByName("index"),
+ dateAdded: row.getResultByName("dateAdded"),
+ lastModified: row.getResultByName("lastModified"),
+ feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
+ siteURI: siteURI ? NetUtil.newURI(siteURI) : null
+ });
+ livemarksMap.set(livemark.guid, livemark);
+ }
+ return livemarksMap;
+ })
+);
+
+/**
+ * Convert a Date object to a PRTime (microseconds).
+ *
+ * @param date
+ * the Date object to convert.
+ * @return microseconds from the epoch.
+ */
+function toPRTime(date) {
+ return date * 1000;
+}
+
+/**
+ * Convert a PRTime to a Date object.
+ *
+ * @param time
+ * microseconds from the epoch.
+ * @return a Date object or undefined if time was not defined.
+ */
+function toDate(time) {
+ return time ? new Date(parseInt(time / 1000)) : undefined;
+}
+
+// LivemarkService
+
+function LivemarkService() {
+ // Cleanup on shutdown.
+ Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
+
+ // Observe bookmarks but don't init the service just for that.
+ PlacesUtils.addLazyBookmarkObserver(this, true);
+}
+
+LivemarkService.prototype = {
+ // This is just an helper for code readability.
+ _promiseLivemarksMap: () => gLivemarksCachePromised,
+
+ _reloading: false,
+ _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
+ if (this._reloadTimer) {
+ this._reloadTimer.cancel();
+ }
+ else {
+ this._reloadTimer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ }
+
+ this._reloading = true;
+ this._reloadTimer.initWithCallback(() => {
+ // Find first livemark to be reloaded.
+ for (let [ guid, livemark ] of livemarksMap) {
+ if (!reloaded.has(guid)) {
+ reloaded.add(guid);
+ livemark.reload(forceUpdate);
+ this._startReloadTimer(livemarksMap, forceUpdate, reloaded);
+ return;
+ }
+ }
+ // All livemarks have been reloaded.
+ this._reloading = false;
+ }, RELOAD_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ // nsIObserver
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
+ if (this._reloadTimer) {
+ this._reloading = false;
+ this._reloadTimer.cancel();
+ delete this._reloadTimer;
+ }
+
+ // Stop any ongoing network fetch.
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.terminate();
+ }
+ });
+ }
+ },
+
+ // mozIAsyncLivemarks
+
+ addLivemark(aLivemarkInfo) {
+ if (!aLivemarkInfo) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+ let hasParentId = "parentId" in aLivemarkInfo;
+ let hasParentGuid = "parentGuid" in aLivemarkInfo;
+ let hasIndex = "index" in aLivemarkInfo;
+ // Must provide at least non-null parent guid/id, index and feedURI.
+ if ((!hasParentId && !hasParentGuid) ||
+ (hasParentId && aLivemarkInfo.parentId < 1) ||
+ (hasParentGuid &&!/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.parentGuid)) ||
+ (hasIndex && aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX) ||
+ !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) ||
+ (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) ||
+ (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return Task.spawn(function* () {
+ if (!aLivemarkInfo.parentGuid)
+ aLivemarkInfo.parentGuid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.parentId);
+
+ let livemarksMap = yield this._promiseLivemarksMap();
+
+ // Disallow adding a livemark inside another livemark.
+ if (livemarksMap.has(aLivemarkInfo.parentGuid)) {
+ throw new Components.Exception("Cannot create a livemark inside a livemark", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Create a new livemark.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: aLivemarkInfo.parentGuid,
+ title: aLivemarkInfo.title,
+ index: aLivemarkInfo.index,
+ guid: aLivemarkInfo.guid,
+ dateAdded: toDate(aLivemarkInfo.dateAdded) || toDate(aLivemarkInfo.lastModified),
+ source: aLivemarkInfo.source,
+ });
+
+ // Set feed and site URI annotations.
+ let id = yield PlacesUtils.promiseItemId(folder.guid);
+
+ // Create the internal Livemark object.
+ let livemark = new Livemark({ id
+ , title: folder.title
+ , parentGuid: folder.parentGuid
+ , parentId: yield PlacesUtils.promiseItemId(folder.parentGuid)
+ , index: folder.index
+ , feedURI: aLivemarkInfo.feedURI
+ , siteURI: aLivemarkInfo.siteURI
+ , guid: folder.guid
+ , dateAdded: toPRTime(folder.dateAdded)
+ , lastModified: toPRTime(folder.lastModified)
+ });
+
+ livemark.writeFeedURI(aLivemarkInfo.feedURI, aLivemarkInfo.source);
+ if (aLivemarkInfo.siteURI) {
+ livemark.writeSiteURI(aLivemarkInfo.siteURI, aLivemarkInfo.source);
+ }
+
+ if (aLivemarkInfo.lastModified) {
+ yield PlacesUtils.bookmarks.update({ guid: folder.guid,
+ lastModified: toDate(aLivemarkInfo.lastModified),
+ source: aLivemarkInfo.source });
+ livemark.lastModified = aLivemarkInfo.lastModified;
+ }
+
+ livemarksMap.set(folder.guid, livemark);
+
+ return livemark;
+ }.bind(this));
+ },
+
+ removeLivemark(aLivemarkInfo) {
+ if (!aLivemarkInfo) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+ // Accept either a guid or an id.
+ let hasGuid = "guid" in aLivemarkInfo;
+ let hasId = "id" in aLivemarkInfo;
+ if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
+ (hasId && aLivemarkInfo.id < 1) ||
+ (!hasId && !hasGuid)) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return Task.spawn(function* () {
+ if (!aLivemarkInfo.guid)
+ aLivemarkInfo.guid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
+
+ let livemarksMap = yield this._promiseLivemarksMap();
+ if (!livemarksMap.has(aLivemarkInfo.guid))
+ throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
+
+ yield PlacesUtils.bookmarks.remove(aLivemarkInfo.guid,
+ { source: aLivemarkInfo.source });
+ }.bind(this));
+ },
+
+ reloadLivemarks(aForceUpdate) {
+ // Check if there's a currently running reload, to save some useless work.
+ let notWorthRestarting =
+ this._forceUpdate || // We're already forceUpdating.
+ !aForceUpdate; // The caller didn't request a forced update.
+ if (this._reloading && notWorthRestarting) {
+ // Ignore this call.
+ return;
+ }
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ this._forceUpdate = !!aForceUpdate;
+ // Livemarks reloads happen on a timer for performance reasons.
+ this._startReloadTimer(livemarksMap, this._forceUpdate, new Set());
+ });
+ },
+
+ getLivemark(aLivemarkInfo) {
+ if (!aLivemarkInfo) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+ // Accept either a guid or an id.
+ let hasGuid = "guid" in aLivemarkInfo;
+ let hasId = "id" in aLivemarkInfo;
+ if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
+ (hasId && aLivemarkInfo.id < 1) ||
+ (!hasId && !hasGuid)) {
+ throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return Task.spawn(function*() {
+ if (!aLivemarkInfo.guid)
+ aLivemarkInfo.guid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.id);
+
+ let livemarksMap = yield this._promiseLivemarksMap();
+ if (!livemarksMap.has(aLivemarkInfo.guid))
+ throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);
+
+ return livemarksMap.get(aLivemarkInfo.guid);
+ }.bind(this));
+ },
+
+ // nsINavBookmarkObserver
+
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemVisited() {},
+ onItemAdded() {},
+
+ onItemChanged(id, property, isAnno, value, lastModified, itemType, parentId,
+ guid, parentGuid) {
+ if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
+ return;
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ if (livemarksMap.has(guid)) {
+ let livemark = livemarksMap.get(guid);
+ if (property == "title") {
+ livemark.title = value;
+ }
+ livemark.lastModified = lastModified;
+ }
+ });
+ },
+
+ onItemMoved(id, parentId, oldIndex, newParentId, newIndex, itemType, guid,
+ oldParentGuid, newParentGuid) {
+ if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
+ return;
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ if (livemarksMap.has(guid)) {
+ let livemark = livemarksMap.get(guid);
+ livemark.parentId = newParentId;
+ livemark.parentGuid = newParentGuid;
+ livemark.index = newIndex;
+ }
+ });
+ },
+
+ onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
+ if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
+ return;
+
+ this._promiseLivemarksMap().then(livemarksMap => {
+ if (livemarksMap.has(guid)) {
+ let livemark = livemarksMap.get(guid);
+ livemark.terminate();
+ livemarksMap.delete(guid);
+ }
+ });
+ },
+
+ // nsINavHistoryObserver
+
+ onPageChanged() {},
+ onTitleChanged() {},
+ onDeleteVisits() {},
+
+ onClearHistory() {
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.updateURIVisitedStatus(null, false);
+ }
+ });
+ },
+
+ onDeleteURI(aURI) {
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.updateURIVisitedStatus(aURI, false);
+ }
+ });
+ },
+
+ onVisit(aURI) {
+ this._promiseLivemarksMap().then(livemarksMap => {
+ for (let livemark of livemarksMap.values()) {
+ livemark.updateURIVisitedStatus(aURI, true);
+ }
+ });
+ },
+
+ // nsISupports
+
+ classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.mozIAsyncLivemarks
+ , Ci.nsINavBookmarkObserver
+ , Ci.nsINavHistoryObserver
+ , Ci.nsIObserver
+ , Ci.nsISupportsWeakReference
+ ])
+};
+
+// Livemark
+
+/**
+ * Object used internally to represent a livemark.
+ *
+ * @param aLivemarkInfo
+ * Object containing information on the livemark. If the livemark is
+ * not included in the object, a new livemark will be created.
+ *
+ * @note terminate() must be invoked before getting rid of this object.
+ */
+function Livemark(aLivemarkInfo)
+{
+ this.id = aLivemarkInfo.id;
+ this.guid = aLivemarkInfo.guid;
+ this.feedURI = aLivemarkInfo.feedURI;
+ this.siteURI = aLivemarkInfo.siteURI || null;
+ this.title = aLivemarkInfo.title;
+ this.parentId = aLivemarkInfo.parentId;
+ this.parentGuid = aLivemarkInfo.parentGuid;
+ this.index = aLivemarkInfo.index;
+ this.dateAdded = aLivemarkInfo.dateAdded;
+ this.lastModified = aLivemarkInfo.lastModified;
+
+ this._status = Ci.mozILivemark.STATUS_READY;
+
+ // Hash of resultObservers, hashed by container.
+ this._resultObservers = new Map();
+
+ // Sorted array of objects representing livemark children in the form
+ // { uri, title, visited }.
+ this._children = [];
+
+ // Keeps a separate array of nodes for each requesting container, hashed by
+ // the container itself.
+ this._nodes = new Map();
+
+ this.loadGroup = null;
+ this.expireTime = 0;
+}
+
+Livemark.prototype = {
+ get status() {
+ return this._status;
+ },
+ set status(val) {
+ if (this._status != val) {
+ this._status = val;
+ this._invalidateRegisteredContainers();
+ }
+ return this._status;
+ },
+
+ writeFeedURI(aFeedURI, aSource) {
+ PlacesUtils.annotations
+ .setItemAnnotation(this.id, PlacesUtils.LMANNO_FEEDURI,
+ aFeedURI.spec,
+ 0, PlacesUtils.annotations.EXPIRE_NEVER,
+ aSource);
+ this.feedURI = aFeedURI;
+ },
+
+ writeSiteURI(aSiteURI, aSource) {
+ if (!aSiteURI) {
+ PlacesUtils.annotations.removeItemAnnotation(this.id,
+ PlacesUtils.LMANNO_SITEURI,
+ aSource)
+ this.siteURI = null;
+ return;
+ }
+
+ // Security check the site URI against the feed URI principal.
+ let secMan = Services.scriptSecurityManager;
+ let feedPrincipal = secMan.createCodebasePrincipal(this.feedURI, {});
+ try {
+ secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ }
+ catch (ex) {
+ return;
+ }
+
+ PlacesUtils.annotations
+ .setItemAnnotation(this.id, PlacesUtils.LMANNO_SITEURI,
+ aSiteURI.spec,
+ 0, PlacesUtils.annotations.EXPIRE_NEVER,
+ aSource);
+ this.siteURI = aSiteURI;
+ },
+
+ /**
+ * Tries to updates the livemark if needed.
+ * The update process is asynchronous.
+ *
+ * @param [optional] aForceUpdate
+ * If true will try to update the livemark even if its contents have
+ * not yet expired.
+ */
+ updateChildren(aForceUpdate) {
+ // Check if the livemark is already updating.
+ if (this.status == Ci.mozILivemark.STATUS_LOADING)
+ return;
+
+ // Check the TTL/expiration on this, to check if there is no need to update
+ // this livemark.
+ if (!aForceUpdate && this.children.length && this.expireTime > Date.now())
+ return;
+
+ this.status = Ci.mozILivemark.STATUS_LOADING;
+
+ // Setting the status notifies observers that may remove the livemark.
+ if (this._terminated)
+ return;
+
+ try {
+ // Create a load group for the request. This will allow us to
+ // automatically keep track of redirects, so we can always
+ // cancel the channel.
+ let loadgroup = Cc["@mozilla.org/network/load-group;1"].
+ createInstance(Ci.nsILoadGroup);
+ // Creating a CodeBasePrincipal and using it as the loadingPrincipal
+ // is *not* desired and is only tolerated within this file.
+ // TODO: Find the right OriginAttributes and pass something other
+ // than {} to .createCodeBasePrincipal().
+ let channel = NetUtil.newChannel({
+ uri: this.feedURI,
+ loadingPrincipal: Services.scriptSecurityManager.createCodebasePrincipal(this.feedURI, {}),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_XMLHTTPREQUEST
+ }).QueryInterface(Ci.nsIHttpChannel);
+ channel.loadGroup = loadgroup;
+ channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
+ Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ channel.requestMethod = "GET";
+ channel.setRequestHeader("X-Moz", "livebookmarks", false);
+
+ // Stream the result to the feed parser with this listener
+ let listener = new LivemarkLoadListener(this);
+ channel.notificationCallbacks = listener;
+ channel.asyncOpen2(listener);
+
+ this.loadGroup = loadgroup;
+ }
+ catch (ex) {
+ this.status = Ci.mozILivemark.STATUS_FAILED;
+ }
+ },
+
+ reload(aForceUpdate) {
+ this.updateChildren(aForceUpdate);
+ },
+
+ get children() {
+ return this._children;
+ },
+ set children(val) {
+ this._children = val;
+
+ // Discard the previous cached nodes, new ones should be generated.
+ for (let container of this._resultObservers.keys()) {
+ this._nodes.delete(container);
+ }
+
+ // Update visited status for each entry.
+ for (let child of this._children) {
+ asyncHistory.isURIVisited(child.uri, (aURI, aIsVisited) => {
+ this.updateURIVisitedStatus(aURI, aIsVisited);
+ });
+ }
+
+ return this._children;
+ },
+
+ _isURIVisited(aURI) {
+ return this.children.some(child => child.uri.equals(aURI) && child.visited);
+ },
+
+ getNodesForContainer(aContainerNode) {
+ if (this._nodes.has(aContainerNode)) {
+ return this._nodes.get(aContainerNode);
+ }
+
+ let livemark = this;
+ let nodes = [];
+ let now = Date.now() * 1000;
+ for (let child of this.children) {
+ // Workaround for bug 449811.
+ let localChild = child;
+ let node = {
+ // The QueryInterface is needed cause aContainerNode is a jsval.
+ // This is required to avoid issues with scriptable wrappers that would
+ // not allow the view to correctly set expandos.
+ get parent() {
+ return aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ },
+ get parentResult() {
+ return this.parent.parentResult;
+ },
+ get uri() {
+ return localChild.uri.spec;
+ },
+ get type() {
+ return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+ },
+ get title() {
+ return localChild.title;
+ },
+ get accessCount() {
+ return Number(livemark._isURIVisited(NetUtil.newURI(this.uri)));
+ },
+ get time() {
+ return 0;
+ },
+ get icon() {
+ return "";
+ },
+ get indentLevel() {
+ return this.parent.indentLevel + 1;
+ },
+ get bookmarkIndex() {
+ return -1;
+ },
+ get itemId() {
+ return -1;
+ },
+ get dateAdded() {
+ return now;
+ },
+ get lastModified() {
+ return now;
+ },
+ get tags() {
+ return PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", ");
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode])
+ };
+ nodes.push(node);
+ }
+ this._nodes.set(aContainerNode, nodes);
+ return nodes;
+ },
+
+ registerForUpdates(aContainerNode, aResultObserver) {
+ this._resultObservers.set(aContainerNode, aResultObserver);
+ },
+
+ unregisterForUpdates(aContainerNode) {
+ this._resultObservers.delete(aContainerNode);
+ this._nodes.delete(aContainerNode);
+ },
+
+ _invalidateRegisteredContainers() {
+ for (let [ container, observer ] of this._resultObservers) {
+ observer.invalidateContainer(container);
+ }
+ },
+
+ /**
+ * Updates the visited status of nodes observing this livemark.
+ *
+ * @param aURI
+ * If provided will update nodes having the given uri,
+ * otherwise any node.
+ * @param aVisitedStatus
+ * Whether the nodes should be set as visited.
+ */
+ updateURIVisitedStatus(aURI, aVisitedStatus) {
+ for (let child of this.children) {
+ if (!aURI || child.uri.equals(aURI)) {
+ child.visited = aVisitedStatus;
+ }
+ }
+
+ for (let [ container, observer ] of this._resultObservers) {
+ if (this._nodes.has(container)) {
+ let nodes = this._nodes.get(container);
+ for (let node of nodes) {
+ // Workaround for bug 449811.
+ let localObserver = observer;
+ let localNode = node;
+ if (!aURI || node.uri == aURI.spec) {
+ Services.tm.mainThread.dispatch(() => {
+ localObserver.nodeHistoryDetailsChanged(localNode, 0, aVisitedStatus);
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Terminates the livemark entry, cancelling any ongoing load.
+ * Must be invoked before destroying the entry.
+ */
+ terminate() {
+ // Avoid handling any updateChildren request from now on.
+ this._terminated = true;
+ this.abort();
+ },
+
+ /**
+ * Aborts the livemark loading if needed.
+ */
+ abort() {
+ this.status = Ci.mozILivemark.STATUS_FAILED;
+ if (this.loadGroup) {
+ this.loadGroup.cancel(Cr.NS_BINDING_ABORTED);
+ this.loadGroup = null;
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.mozILivemark
+ ])
+}
+
+// LivemarkLoadListener
+
+/**
+ * Object used internally to handle loading a livemark's contents.
+ *
+ * @param aLivemark
+ * The Livemark that is loading.
+ */
+function LivemarkLoadListener(aLivemark) {
+ this._livemark = aLivemark;
+ this._processor = null;
+ this._isAborted = false;
+ this._ttl = EXPIRE_TIME_MS;
+}
+
+LivemarkLoadListener.prototype = {
+ abort(aException) {
+ if (!this._isAborted) {
+ this._isAborted = true;
+ this._livemark.abort();
+ this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
+ }
+ },
+
+ // nsIFeedResultListener
+ handleResult(aResult) {
+ if (this._isAborted) {
+ return;
+ }
+
+ try {
+ // We need this to make sure the item links are safe
+ let feedPrincipal =
+ Services.scriptSecurityManager
+ .createCodebasePrincipal(this._livemark.feedURI, {});
+
+ // Enforce well-formedness because the existing code does
+ if (!aResult || !aResult.doc || aResult.bozo) {
+ throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
+ let siteURI = this._livemark.siteURI;
+ if (feed.link && (!siteURI || !feed.link.equals(siteURI))) {
+ siteURI = feed.link;
+ this._livemark.writeSiteURI(siteURI);
+ }
+
+ // Insert feed items.
+ let livemarkChildren = [];
+ for (let i = 0; i < feed.items.length; ++i) {
+ let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
+ let uri = entry.link || siteURI;
+ if (!uri) {
+ continue;
+ }
+
+ try {
+ Services.scriptSecurityManager
+ .checkLoadURIWithPrincipal(feedPrincipal, uri,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ }
+ catch (ex) {
+ continue;
+ }
+
+ let title = entry.title ? entry.title.plainText() : "";
+ livemarkChildren.push({ uri: uri, title: title, visited: false });
+ }
+
+ this._livemark.children = livemarkChildren;
+ }
+ catch (ex) {
+ this.abort(ex);
+ }
+ finally {
+ this._processor.listener = null;
+ this._processor = null;
+ }
+ },
+
+ onDataAvailable(aRequest, aContext, aInputStream, aSourceOffset, aCount) {
+ if (this._processor) {
+ this._processor.onDataAvailable(aRequest, aContext, aInputStream,
+ aSourceOffset, aCount);
+ }
+ },
+
+ onStartRequest(aRequest, aContext) {
+ if (this._isAborted) {
+ throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ try {
+ // 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(aRequest, aContext);
+ }
+ catch (ex) {
+ Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec);
+ this.abort(ex);
+ }
+ },
+
+ onStopRequest(aRequest, aContext, aStatus) {
+ if (!Components.isSuccessCode(aStatus)) {
+ this.abort();
+ return;
+ }
+
+ // Set an expiration on the livemark, to reloading the data in future.
+ try {
+ if (this._processor) {
+ this._processor.onStopRequest(aRequest, aContext, aStatus);
+ }
+
+ // Calculate a new ttl
+ let channel = aRequest.QueryInterface(Ci.nsICachingChannel);
+ if (channel) {
+ let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry);
+ if (entryInfo) {
+ // nsICacheEntry returns value as seconds.
+ let expireTime = entryInfo.expirationTime * 1000;
+ let nowTime = Date.now();
+ // Note, expireTime can be 0, see bug 383538.
+ if (expireTime > nowTime) {
+ this._setResourceTTL(Math.max((expireTime - nowTime),
+ EXPIRE_TIME_MS));
+ return;
+ }
+ }
+ }
+ this._setResourceTTL(EXPIRE_TIME_MS);
+ }
+ catch (ex) {
+ this.abort(ex);
+ }
+ finally {
+ if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) {
+ this._livemark.status = Ci.mozILivemark.STATUS_READY;
+ }
+ this._livemark.locked = false;
+ this._livemark.loadGroup = null;
+ }
+ },
+
+ _setResourceTTL(aMilliseconds) {
+ this._livemark.expireTime = Date.now() + aMilliseconds;
+ },
+
+ // nsIInterfaceRequestor
+ getInterface(aIID) {
+ return this.QueryInterface(aIID);
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIFeedResultListener
+ , Ci.nsIStreamListener
+ , Ci.nsIRequestObserver
+ , Ci.nsIInterfaceRequestor
+ ])
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);
diff --git a/toolkit/components/places/nsMaybeWeakPtr.h b/toolkit/components/places/nsMaybeWeakPtr.h
new file mode 100644
index 0000000000..ce52e5090d
--- /dev/null
+++ b/toolkit/components/places/nsMaybeWeakPtr.h
@@ -0,0 +1,145 @@
+/* -*- 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/. */
+
+#ifndef nsMaybeWeakPtr_h_
+#define nsMaybeWeakPtr_h_
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsWeakReference.h"
+#include "nsTArray.h"
+#include "nsCycleCollectionNoteChild.h"
+
+// nsMaybeWeakPtr is a helper object to hold a strong-or-weak reference
+// to the template class. It's pretty minimal, but sufficient.
+
+template<class T>
+class nsMaybeWeakPtr
+{
+public:
+ MOZ_IMPLICIT nsMaybeWeakPtr(nsISupports* aRef) : mPtr(aRef) {}
+ MOZ_IMPLICIT nsMaybeWeakPtr(const nsCOMPtr<nsIWeakReference>& aRef) : mPtr(aRef) {}
+ MOZ_IMPLICIT nsMaybeWeakPtr(const nsCOMPtr<T>& aRef) : mPtr(aRef) {}
+
+ bool operator==(const nsMaybeWeakPtr<T> &other) const {
+ return mPtr == other.mPtr;
+ }
+
+ nsISupports* GetRawValue() const { return mPtr.get(); }
+
+ const nsCOMPtr<T> GetValue() const;
+
+private:
+ nsCOMPtr<nsISupports> mPtr;
+};
+
+// nsMaybeWeakPtrArray is an array of MaybeWeakPtr objects, that knows how to
+// grab a weak reference to a given object if requested. It only allows a
+// given object to appear in the array once.
+
+template<class T>
+class nsMaybeWeakPtrArray : public nsTArray<nsMaybeWeakPtr<T>>
+{
+ typedef nsTArray<nsMaybeWeakPtr<T>> MaybeWeakArray;
+
+public:
+ nsresult AppendWeakElement(T* aElement, bool aOwnsWeak)
+ {
+ nsCOMPtr<nsISupports> ref;
+ if (aOwnsWeak) {
+ ref = do_GetWeakReference(aElement);
+ } else {
+ ref = aElement;
+ }
+
+ if (MaybeWeakArray::Contains(ref.get())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (!MaybeWeakArray::AppendElement(ref)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+ }
+
+ nsresult RemoveWeakElement(T* aElement)
+ {
+ if (MaybeWeakArray::RemoveElement(aElement)) {
+ return NS_OK;
+ }
+
+ // Don't use do_GetWeakReference; it should only be called if we know
+ // the object supports weak references.
+ nsCOMPtr<nsISupportsWeakReference> supWeakRef = do_QueryInterface(aElement);
+ NS_ENSURE_TRUE(supWeakRef, NS_ERROR_INVALID_ARG);
+
+ nsCOMPtr<nsIWeakReference> weakRef;
+ nsresult rv = supWeakRef->GetWeakReference(getter_AddRefs(weakRef));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MaybeWeakArray::RemoveElement(weakRef)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_INVALID_ARG;
+ }
+};
+
+template<class T>
+const nsCOMPtr<T>
+nsMaybeWeakPtr<T>::GetValue() const
+{
+ if (!mPtr) {
+ return nullptr;
+ }
+
+ nsresult rv;
+ nsCOMPtr<T> ref = do_QueryInterface(mPtr, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ return ref;
+ }
+
+ nsCOMPtr<nsIWeakReference> weakRef = do_QueryInterface(mPtr);
+ if (weakRef) {
+ ref = do_QueryReferent(weakRef, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ return ref;
+ }
+ }
+
+ return nullptr;
+}
+
+template <typename T>
+inline void
+ImplCycleCollectionUnlink(nsMaybeWeakPtrArray<T>& aField)
+{
+ aField.Clear();
+}
+
+template <typename E>
+inline void
+ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+ nsMaybeWeakPtrArray<E>& aField,
+ const char* aName,
+ uint32_t aFlags = 0)
+{
+ aFlags |= CycleCollectionEdgeNameArrayFlag;
+ size_t length = aField.Length();
+ for (size_t i = 0; i < length; ++i) {
+ CycleCollectionNoteChild(aCallback, aField[i].GetRawValue(), aName, aFlags);
+ }
+}
+
+// Call a method on each element in the array, but only if the element is
+// non-null.
+
+#define ENUMERATE_WEAKARRAY(array, type, method) \
+ for (uint32_t array_idx = 0; array_idx < array.Length(); ++array_idx) { \
+ const nsCOMPtr<type> &e = array.ElementAt(array_idx).GetValue(); \
+ if (e) \
+ e->method; \
+ }
+
+#endif
diff --git a/toolkit/components/places/nsNavBookmarks.cpp b/toolkit/components/places/nsNavBookmarks.cpp
new file mode 100644
index 0000000000..74707be994
--- /dev/null
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -0,0 +1,2926 @@
+/* -*- 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 "nsNavBookmarks.h"
+
+#include "nsNavHistory.h"
+#include "nsAnnotationService.h"
+#include "nsPlacesMacros.h"
+#include "Helpers.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsNetUtil.h"
+#include "nsUnicharUtils.h"
+#include "nsPrintfCString.h"
+#include "prprf.h"
+#include "mozilla/storage.h"
+
+#include "GeckoProfiler.h"
+
+using namespace mozilla;
+
+// These columns sit to the right of the kGetInfoIndex_* columns.
+const int32_t nsNavBookmarks::kGetChildrenIndex_Guid = 18;
+const int32_t nsNavBookmarks::kGetChildrenIndex_Position = 19;
+const int32_t nsNavBookmarks::kGetChildrenIndex_Type = 20;
+const int32_t nsNavBookmarks::kGetChildrenIndex_PlaceID = 21;
+
+using namespace mozilla::places;
+
+PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)
+
+#define BOOKMARKS_ANNO_PREFIX "bookmarks/"
+#define BOOKMARKS_TOOLBAR_FOLDER_ANNO NS_LITERAL_CSTRING(BOOKMARKS_ANNO_PREFIX "toolbarFolder")
+#define FEED_URI_ANNO NS_LITERAL_CSTRING("livemark/feedURI")
+
+
+namespace {
+
+#define SKIP_TAGS(condition) ((condition) ? SkipTags : DontSkip)
+
+bool DontSkip(nsCOMPtr<nsINavBookmarkObserver> obs) { return false; }
+bool SkipTags(nsCOMPtr<nsINavBookmarkObserver> obs) {
+ bool skipTags = false;
+ (void) obs->GetSkipTags(&skipTags);
+ return skipTags;
+}
+bool SkipDescendants(nsCOMPtr<nsINavBookmarkObserver> obs) {
+ bool skipDescendantsOnItemRemoval = false;
+ (void) obs->GetSkipTags(&skipDescendantsOnItemRemoval);
+ return skipDescendantsOnItemRemoval;
+}
+
+template<typename Method, typename DataType>
+class AsyncGetBookmarksForURI : public AsyncStatementCallback
+{
+public:
+ AsyncGetBookmarksForURI(nsNavBookmarks* aBookmarksSvc,
+ Method aCallback,
+ const DataType& aData)
+ : mBookmarksSvc(aBookmarksSvc)
+ , mCallback(aCallback)
+ , mData(aData)
+ {
+ }
+
+ void Init()
+ {
+ RefPtr<Database> DB = Database::GetDatabase();
+ if (DB) {
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = DB->GetAsyncStatement(
+ "/* do not warn (bug 1175249) */ "
+ "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent "
+ "FROM moz_bookmarks b "
+ "JOIN moz_bookmarks t on t.id = b.parent "
+ "WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) "
+ "ORDER BY b.lastModified DESC, b.id DESC "
+ );
+ if (stmt) {
+ (void)URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
+ mData.bookmark.url);
+ nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
+ (void)stmt->ExecuteAsync(this, getter_AddRefs(pendingStmt));
+ }
+ }
+ }
+
+ NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet)
+ {
+ nsCOMPtr<mozIStorageRow> row;
+ while (NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row) {
+ // Skip tags, for the use-cases of this async getter they are useless.
+ int64_t grandParentId, tagsFolderId;
+ nsresult rv = row->GetInt64(5, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mBookmarksSvc->GetTagsFolder(&tagsFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (grandParentId == tagsFolderId) {
+ continue;
+ }
+
+ mData.bookmark.grandParentId = grandParentId;
+ rv = row->GetInt64(0, &mData.bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = row->GetUTF8String(1, mData.bookmark.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = row->GetInt64(2, &mData.bookmark.parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // lastModified (3) should not be set for the use-cases of this getter.
+ rv = row->GetUTF8String(4, mData.bookmark.parentGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mCallback) {
+ ((*mBookmarksSvc).*mCallback)(mData);
+ }
+ }
+ return NS_OK;
+ }
+
+private:
+ RefPtr<nsNavBookmarks> mBookmarksSvc;
+ Method mCallback;
+ DataType mData;
+};
+
+} // namespace
+
+
+nsNavBookmarks::nsNavBookmarks()
+ : mItemCount(0)
+ , mRoot(0)
+ , mMenuRoot(0)
+ , mTagsRoot(0)
+ , mUnfiledRoot(0)
+ , mToolbarRoot(0)
+ , mMobileRoot(0)
+ , mCanNotify(false)
+ , mCacheObservers("bookmark-observers")
+ , mBatching(false)
+{
+ NS_ASSERTION(!gBookmarksService,
+ "Attempting to create two instances of the service!");
+ gBookmarksService = this;
+}
+
+
+nsNavBookmarks::~nsNavBookmarks()
+{
+ NS_ASSERTION(gBookmarksService == this,
+ "Deleting a non-singleton instance of the service");
+ if (gBookmarksService == this)
+ gBookmarksService = nullptr;
+}
+
+
+NS_IMPL_ISUPPORTS(nsNavBookmarks
+, nsINavBookmarksService
+, nsINavHistoryObserver
+, nsIAnnotationObserver
+, nsIObserver
+, nsISupportsWeakReference
+)
+
+
+Atomic<int64_t> nsNavBookmarks::sLastInsertedItemId(0);
+
+
+void // static
+nsNavBookmarks::StoreLastInsertedId(const nsACString& aTable,
+ const int64_t aLastInsertedId) {
+ MOZ_ASSERT(aTable.EqualsLiteral("moz_bookmarks"));
+ sLastInsertedItemId = aLastInsertedId;
+}
+
+
+nsresult
+nsNavBookmarks::Init()
+{
+ mDB = Database::GetDatabase();
+ NS_ENSURE_STATE(mDB);
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ (void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, true);
+ (void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
+ }
+
+ nsresult rv = ReadRoots();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCanNotify = true;
+
+ // Observe annotations.
+ nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+ NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
+ annosvc->AddObserver(this);
+
+ // Allows us to notify on title changes. MUST BE LAST so it is impossible
+ // to fail after this call, or the history service will have a reference to
+ // us and we won't go away.
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(history);
+ history->AddObserver(this, true);
+
+ // DO NOT PUT STUFF HERE that can fail. See observer comment above.
+
+ return NS_OK;
+}
+
+nsresult
+nsNavBookmarks::ReadRoots()
+{
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT guid, id FROM moz_bookmarks WHERE guid IN ( "
+ "'root________', 'menu________', 'toolbar_____', "
+ "'tags________', 'unfiled_____', 'mobile______' )"
+ ), getter_AddRefs(stmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ nsAutoCString guid;
+ rv = stmt->GetUTF8String(0, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t id;
+ rv = stmt->GetInt64(1, &id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (guid.EqualsLiteral("root________")) {
+ mRoot = id;
+ }
+ else if (guid.EqualsLiteral("menu________")) {
+ mMenuRoot = id;
+ }
+ else if (guid.EqualsLiteral("toolbar_____")) {
+ mToolbarRoot = id;
+ }
+ else if (guid.EqualsLiteral("tags________")) {
+ mTagsRoot = id;
+ }
+ else if (guid.EqualsLiteral("unfiled_____")) {
+ mUnfiledRoot = id;
+ }
+ else if (guid.EqualsLiteral("mobile______")) {
+ mMobileRoot = id;
+ }
+ }
+
+ if (!mRoot || !mMenuRoot || !mToolbarRoot || !mTagsRoot || !mUnfiledRoot ||
+ !mMobileRoot)
+ return NS_ERROR_FAILURE;
+
+ return NS_OK;
+}
+
+// nsNavBookmarks::IsBookmarkedInDatabase
+//
+// This checks to see if the specified place_id is actually bookmarked.
+
+nsresult
+nsNavBookmarks::IsBookmarkedInDatabase(int64_t aPlaceId,
+ bool* aIsBookmarked)
+{
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT 1 FROM moz_bookmarks WHERE fk = :page_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->ExecuteStep(aIsBookmarked);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::AdjustIndices(int64_t aFolderId,
+ int32_t aStartIndex,
+ int32_t aEndIndex,
+ int32_t aDelta)
+{
+ NS_ASSERTION(aStartIndex >= 0 && aEndIndex <= INT32_MAX &&
+ aStartIndex <= aEndIndex, "Bad indices");
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET position = position + :delta "
+ "WHERE parent = :parent "
+ "AND position BETWEEN :from_index AND :to_index"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("from_index"), aStartIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("to_index"), aEndIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetPlacesRoot(int64_t* aRoot)
+{
+ *aRoot = mRoot;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetBookmarksMenuFolder(int64_t* aRoot)
+{
+ *aRoot = mMenuRoot;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetToolbarFolder(int64_t* aFolderId)
+{
+ *aFolderId = mToolbarRoot;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetTagsFolder(int64_t* aRoot)
+{
+ *aRoot = mTagsRoot;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetUnfiledBookmarksFolder(int64_t* aRoot)
+{
+ *aRoot = mUnfiledRoot;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetMobileFolder(int64_t* aRoot)
+{
+ *aRoot = mMobileRoot;
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::InsertBookmarkInDB(int64_t aPlaceId,
+ enum ItemType aItemType,
+ int64_t aParentId,
+ int32_t aIndex,
+ const nsACString& aTitle,
+ PRTime aDateAdded,
+ PRTime aLastModified,
+ const nsACString& aParentGuid,
+ int64_t aGrandParentId,
+ nsIURI* aURI,
+ uint16_t aSource,
+ int64_t* _itemId,
+ nsACString& _guid)
+{
+ // Check for a valid itemId.
+ MOZ_ASSERT(_itemId && (*_itemId == -1 || *_itemId > 0));
+ // Check for a valid placeId.
+ MOZ_ASSERT(aPlaceId && (aPlaceId == -1 || aPlaceId > 0));
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "INSERT INTO moz_bookmarks "
+ "(id, fk, type, parent, position, title, "
+ "dateAdded, lastModified, guid) "
+ "VALUES (:item_id, :page_id, :item_type, :parent, :item_index, "
+ ":item_title, :date_added, :last_modified, "
+ ":item_guid)"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv;
+ if (*_itemId != -1)
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), *_itemId);
+ else
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("item_id"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aPlaceId != -1)
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlaceId);
+ else
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("page_id"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), aItemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Support NULL titles.
+ if (aTitle.IsVoid())
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("item_title"));
+ else
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"), aTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), aDateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aLastModified) {
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"),
+ aLastModified);
+ }
+ else {
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), aDateAdded);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Could use IsEmpty because our callers check for GUID validity,
+ // but it doesn't hurt.
+ if (_guid.Length() == 12) {
+ MOZ_ASSERT(IsValidGUID(_guid));
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_guid"), _guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ nsAutoCString guid;
+ rv = GenerateGUID(guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_guid"), guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ _guid.Assign(guid);
+ }
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*_itemId == -1) {
+ *_itemId = sLastInsertedItemId;
+ }
+
+ if (aParentId > 0) {
+ // Update last modified date of the ancestors.
+ // TODO (bug 408991): Doing this for all ancestors would be slow without a
+ // nested tree, so for now update only the parent.
+ rv = SetItemDateInternal(LAST_MODIFIED, aParentId, aDateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Add a cache entry since we know everything about this bookmark.
+ BookmarkData bookmark;
+ bookmark.id = *_itemId;
+ bookmark.guid.Assign(_guid);
+ if (aTitle.IsVoid()) {
+ bookmark.title.SetIsVoid(true);
+ }
+ else {
+ bookmark.title.Assign(aTitle);
+ }
+ bookmark.position = aIndex;
+ bookmark.placeId = aPlaceId;
+ bookmark.parentId = aParentId;
+ bookmark.type = aItemType;
+ bookmark.dateAdded = aDateAdded;
+ if (aLastModified)
+ bookmark.lastModified = aLastModified;
+ else
+ bookmark.lastModified = aDateAdded;
+ if (aURI) {
+ rv = aURI->GetSpec(bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ bookmark.parentGuid = aParentGuid;
+ bookmark.grandParentId = aGrandParentId;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::InsertBookmark(int64_t aFolder,
+ nsIURI* aURI,
+ int32_t aIndex,
+ const nsACString& aTitle,
+ const nsACString& aGUID,
+ uint16_t aSource,
+ int64_t* aNewBookmarkId)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(aNewBookmarkId);
+ NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX);
+
+ if (!aGUID.IsEmpty() && !IsValidGUID(aGUID))
+ return NS_ERROR_INVALID_ARG;
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ int64_t placeId;
+ nsAutoCString placeGuid;
+ nsresult rv = history->GetOrCreateIdForPage(aURI, &placeId, placeGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the correct index for insertion. This also ensures the parent exists.
+ int32_t index, folderCount;
+ int64_t grandParentId;
+ nsAutoCString folderGuid;
+ rv = FetchFolderInfo(aFolder, &folderCount, folderGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
+ aIndex >= folderCount) {
+ index = folderCount;
+ }
+ else {
+ index = aIndex;
+ // Create space for the insertion.
+ rv = AdjustIndices(aFolder, index, INT32_MAX, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aNewBookmarkId = -1;
+ PRTime dateAdded = RoundedPRNow();
+ nsAutoCString guid(aGUID);
+ nsCString title;
+ TruncateTitle(aTitle, title);
+
+ rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded,
+ 0, folderGuid, grandParentId, aURI, aSource,
+ aNewBookmarkId, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If not a tag, recalculate frecency for this entry, since it changed.
+ if (grandParentId != mTagsRoot) {
+ rv = history->UpdateFrecency(placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ SKIP_TAGS(grandParentId == mTagsRoot),
+ OnItemAdded(*aNewBookmarkId, aFolder, index,
+ TYPE_BOOKMARK, aURI, title, dateAdded,
+ guid, folderGuid, aSource));
+
+ // If the bookmark has been added to a tag container, notify all
+ // bookmark-folder result nodes which contain a bookmark for the new
+ // bookmark's url.
+ if (grandParentId == mTagsRoot) {
+ // Notify a tags change to all bookmarks for this URI.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(aURI, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ // Check that bookmarks doesn't include the current tag itemId.
+ MOZ_ASSERT(bookmarks[i].id != *aNewBookmarkId);
+
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ DontSkip,
+ OnItemChanged(bookmarks[i].id,
+ NS_LITERAL_CSTRING("tags"),
+ false,
+ EmptyCString(),
+ bookmarks[i].lastModified,
+ TYPE_BOOKMARK,
+ bookmarks[i].parentId,
+ bookmarks[i].guid,
+ bookmarks[i].parentGuid,
+ EmptyCString(),
+ aSource));
+ }
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::RemoveItem(int64_t aItemId, uint16_t aSource)
+{
+ PROFILER_LABEL("nsNavBookmarks", "RemoveItem",
+ js::ProfileEntry::Category::OTHER);
+
+ NS_ENSURE_ARG(!IsRoot(aItemId));
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ // First, if not a tag, remove item annotations.
+ if (bookmark.parentId != mTagsRoot &&
+ bookmark.grandParentId != mTagsRoot) {
+ nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+ NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
+ rv = annosvc->RemoveItemAnnotations(bookmark.id, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (bookmark.type == TYPE_FOLDER) {
+ // Remove all of the folder's children.
+ rv = RemoveFolderChildren(bookmark.id, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "DELETE FROM moz_bookmarks WHERE id = :item_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Fix indices in the parent.
+ if (bookmark.position != DEFAULT_INDEX) {
+ rv = AdjustIndices(bookmark.parentId,
+ bookmark.position + 1, INT32_MAX, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ bookmark.lastModified = RoundedPRNow();
+ rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId,
+ bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ if (bookmark.type == TYPE_BOOKMARK) {
+ // If not a tag, recalculate frecency for this entry, since it changed.
+ if (bookmark.grandParentId != mTagsRoot) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->UpdateFrecency(bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // A broken url should not interrupt the removal process.
+ (void)NS_NewURI(getter_AddRefs(uri), bookmark.url);
+ // We cannot assert since some automated tests are checking this path.
+ NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveItem");
+ }
+
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ SKIP_TAGS(bookmark.parentId == mTagsRoot ||
+ bookmark.grandParentId == mTagsRoot),
+ OnItemRemoved(bookmark.id,
+ bookmark.parentId,
+ bookmark.position,
+ bookmark.type,
+ uri,
+ bookmark.guid,
+ bookmark.parentGuid,
+ aSource));
+
+ if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == mTagsRoot &&
+ uri) {
+ // If the removed bookmark was child of a tag container, notify a tags
+ // change to all bookmarks for this URI.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(uri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ DontSkip,
+ OnItemChanged(bookmarks[i].id,
+ NS_LITERAL_CSTRING("tags"),
+ false,
+ EmptyCString(),
+ bookmarks[i].lastModified,
+ TYPE_BOOKMARK,
+ bookmarks[i].parentId,
+ bookmarks[i].guid,
+ bookmarks[i].parentGuid,
+ EmptyCString(),
+ aSource));
+ }
+
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::CreateFolder(int64_t aParent, const nsACString& aName,
+ int32_t aIndex, const nsACString& aGUID,
+ uint16_t aSource, int64_t* aNewFolder)
+{
+ // NOTE: aParent can be null for root creation, so not checked
+ NS_ENSURE_ARG_POINTER(aNewFolder);
+
+ if (!aGUID.IsEmpty() && !IsValidGUID(aGUID))
+ return NS_ERROR_INVALID_ARG;
+
+ // CreateContainerWithID returns the index of the new folder, but that's not
+ // used here. To avoid any risk of corrupting data should this function
+ // be changed, we'll use a local variable to hold it. The true argument
+ // will cause notifications to be sent to bookmark observers.
+ int32_t localIndex = aIndex;
+ nsresult rv = CreateContainerWithID(-1, aParent, aName, true, &localIndex,
+ aGUID, aSource, aNewFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+bool nsNavBookmarks::IsLivemark(int64_t aFolderId)
+{
+ nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+ NS_ENSURE_TRUE(annosvc, false);
+ bool isLivemark;
+ nsresult rv = annosvc->ItemHasAnnotation(aFolderId,
+ FEED_URI_ANNO,
+ &isLivemark);
+ NS_ENSURE_SUCCESS(rv, false);
+ return isLivemark;
+}
+
+nsresult
+nsNavBookmarks::CreateContainerWithID(int64_t aItemId,
+ int64_t aParent,
+ const nsACString& aTitle,
+ bool aIsBookmarkFolder,
+ int32_t* aIndex,
+ const nsACString& aGUID,
+ uint16_t aSource,
+ int64_t* aNewFolder)
+{
+ NS_ENSURE_ARG_MIN(*aIndex, nsINavBookmarksService::DEFAULT_INDEX);
+
+ // Get the correct index for insertion. This also ensures the parent exists.
+ int32_t index, folderCount;
+ int64_t grandParentId;
+ nsAutoCString folderGuid;
+ nsresult rv = FetchFolderInfo(aParent, &folderCount, folderGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ if (*aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
+ *aIndex >= folderCount) {
+ index = folderCount;
+ } else {
+ index = *aIndex;
+ // Create space for the insertion.
+ rv = AdjustIndices(aParent, index, INT32_MAX, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aNewFolder = aItemId;
+ PRTime dateAdded = RoundedPRNow();
+ nsAutoCString guid(aGUID);
+ nsCString title;
+ TruncateTitle(aTitle, title);
+
+ rv = InsertBookmarkInDB(-1, FOLDER, aParent, index,
+ title, dateAdded, 0, folderGuid, grandParentId,
+ nullptr, aSource, aNewFolder, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ SKIP_TAGS(aParent == mTagsRoot),
+ OnItemAdded(*aNewFolder, aParent, index, FOLDER,
+ nullptr, title, dateAdded, guid,
+ folderGuid, aSource));
+
+ *aIndex = index;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::InsertSeparator(int64_t aParent,
+ int32_t aIndex,
+ const nsACString& aGUID,
+ uint16_t aSource,
+ int64_t* aNewItemId)
+{
+ NS_ENSURE_ARG_MIN(aParent, 1);
+ NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX);
+ NS_ENSURE_ARG_POINTER(aNewItemId);
+
+ if (!aGUID.IsEmpty() && !IsValidGUID(aGUID))
+ return NS_ERROR_INVALID_ARG;
+
+ // Get the correct index for insertion. This also ensures the parent exists.
+ int32_t index, folderCount;
+ int64_t grandParentId;
+ nsAutoCString folderGuid;
+ nsresult rv = FetchFolderInfo(aParent, &folderCount, folderGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
+ aIndex >= folderCount) {
+ index = folderCount;
+ }
+ else {
+ index = aIndex;
+ // Create space for the insertion.
+ rv = AdjustIndices(aParent, index, INT32_MAX, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aNewItemId = -1;
+ // Set a NULL title rather than an empty string.
+ nsAutoCString guid(aGUID);
+ PRTime dateAdded = RoundedPRNow();
+ rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, NullCString(), dateAdded,
+ 0, folderGuid, grandParentId, nullptr, aSource,
+ aNewItemId, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ DontSkip,
+ OnItemAdded(*aNewItemId, aParent, index, TYPE_SEPARATOR,
+ nullptr, NullCString(), dateAdded, guid,
+ folderGuid, aSource));
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::GetLastChildId(int64_t aFolderId, int64_t* aItemId)
+{
+ NS_ASSERTION(aFolderId > 0, "Invalid folder id");
+ *aItemId = -1;
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT id FROM moz_bookmarks WHERE parent = :parent "
+ "ORDER BY position DESC LIMIT 1"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool found;
+ rv = stmt->ExecuteStep(&found);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (found) {
+ rv = stmt->GetInt64(0, aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetIdForItemAt(int64_t aFolder,
+ int32_t aIndex,
+ int64_t* aItemId)
+{
+ NS_ENSURE_ARG_MIN(aFolder, 1);
+ NS_ENSURE_ARG_POINTER(aItemId);
+
+ *aItemId = -1;
+
+ nsresult rv;
+ if (aIndex == nsINavBookmarksService::DEFAULT_INDEX) {
+ // Get last item within aFolder.
+ rv = GetLastChildId(aFolder, aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ // Get the item in aFolder with position aIndex.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT id, fk, type FROM moz_bookmarks "
+ "WHERE parent = :parent AND position = :item_index"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool found;
+ rv = stmt->ExecuteStep(&found);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (found) {
+ rv = stmt->GetInt64(0, aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsNavBookmarks::RemoveFolderTransaction, nsITransaction)
+
+NS_IMETHODIMP
+nsNavBookmarks::GetRemoveFolderTransaction(int64_t aFolderId, uint16_t aSource,
+ nsITransaction** aResult)
+{
+ NS_ENSURE_ARG_MIN(aFolderId, 1);
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // Create and initialize a RemoveFolderTransaction object that can be used to
+ // recreate the folder safely later.
+
+ RemoveFolderTransaction* rft =
+ new RemoveFolderTransaction(aFolderId, aSource);
+ if (!rft)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ NS_ADDREF(*aResult = rft);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::GetDescendantFolders(int64_t aFolderId,
+ nsTArray<int64_t>& aDescendantFoldersArray) {
+ nsresult rv;
+ // New descendant folders will be added from this index on.
+ uint32_t startIndex = aDescendantFoldersArray.Length();
+ {
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT id "
+ "FROM moz_bookmarks "
+ "WHERE parent = :parent "
+ "AND type = :item_type "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), TYPE_FOLDER);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ int64_t itemId;
+ rv = stmt->GetInt64(0, &itemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aDescendantFoldersArray.AppendElement(itemId);
+ }
+ }
+
+ // Recursively call GetDescendantFolders for added folders.
+ // We start at startIndex since previous folders are checked
+ // by previous calls to this method.
+ uint32_t childCount = aDescendantFoldersArray.Length();
+ for (uint32_t i = startIndex; i < childCount; ++i) {
+ GetDescendantFolders(aDescendantFoldersArray[i], aDescendantFoldersArray);
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::GetDescendantChildren(int64_t aFolderId,
+ const nsACString& aFolderGuid,
+ int64_t aGrandParentId,
+ nsTArray<BookmarkData>& aFolderChildrenArray) {
+ // New children will be added from this index on.
+ uint32_t startIndex = aFolderChildrenArray.Length();
+ nsresult rv;
+ {
+ // Collect children informations.
+ // Select all children of a given folder, sorted by position.
+ // This is a LEFT JOIN because not all bookmarks types have a place.
+ // We construct a result where the first columns exactly match
+ // kGetInfoIndex_* order, and additionally contains columns for position,
+ // item_child, and folder_child from moz_bookmarks.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, "
+ "h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, "
+ "b.parent, null, h.frecency, h.hidden, h.guid, null, null, null, "
+ "b.guid, b.position, b.type, b.fk "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_places h ON b.fk = h.id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE b.parent = :parent "
+ "ORDER BY b.position ASC"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ BookmarkData child;
+ rv = stmt->GetInt64(nsNavHistory::kGetInfoIndex_ItemId, &child.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ child.parentId = aFolderId;
+ child.grandParentId = aGrandParentId;
+ child.parentGuid = aFolderGuid;
+ rv = stmt->GetInt32(kGetChildrenIndex_Type, &child.type);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(kGetChildrenIndex_PlaceID, &child.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(kGetChildrenIndex_Position, &child.position);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(kGetChildrenIndex_Guid, child.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (child.type == TYPE_BOOKMARK) {
+ rv = stmt->GetUTF8String(nsNavHistory::kGetInfoIndex_URL, child.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Append item to children's array.
+ aFolderChildrenArray.AppendElement(child);
+ }
+ }
+
+ // Recursively call GetDescendantChildren for added folders.
+ // We start at startIndex since previous folders are checked
+ // by previous calls to this method.
+ uint32_t childCount = aFolderChildrenArray.Length();
+ for (uint32_t i = startIndex; i < childCount; ++i) {
+ if (aFolderChildrenArray[i].type == TYPE_FOLDER) {
+ // nsTarray assumes that all children can be memmove()d, thus we can't
+ // just pass aFolderChildrenArray[i].guid to a method that will change
+ // the array itself. Otherwise, since it's passed by reference, after a
+ // memmove() it could point to garbage and cause intermittent crashes.
+ nsCString guid = aFolderChildrenArray[i].guid;
+ GetDescendantChildren(aFolderChildrenArray[i].id,
+ guid,
+ aFolderId,
+ aFolderChildrenArray);
+ }
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId, uint16_t aSource)
+{
+ PROFILER_LABEL("nsNavBookmarks", "RemoveFolderChilder",
+ js::ProfileEntry::Category::OTHER);
+
+ NS_ENSURE_ARG_MIN(aFolderId, 1);
+ NS_ENSURE_ARG(aFolderId != mRoot);
+
+ BookmarkData folder;
+ nsresult rv = FetchItemInfo(aFolderId, folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_ARG(folder.type == TYPE_FOLDER);
+
+ // Fill folder children array recursively.
+ nsTArray<BookmarkData> folderChildrenArray;
+ rv = GetDescendantChildren(folder.id, folder.guid, folder.parentId,
+ folderChildrenArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build a string of folders whose children will be removed.
+ nsCString foldersToRemove;
+ for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
+ BookmarkData& child = folderChildrenArray[i];
+
+ if (child.type == TYPE_FOLDER) {
+ foldersToRemove.Append(',');
+ foldersToRemove.AppendInt(child.id);
+ }
+ }
+
+ // Delete items from the database now.
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ nsCOMPtr<mozIStorageStatement> deleteStatement = mDB->GetStatement(
+ NS_LITERAL_CSTRING(
+ "DELETE FROM moz_bookmarks "
+ "WHERE parent IN (:parent") + foldersToRemove + NS_LITERAL_CSTRING(")")
+ );
+ NS_ENSURE_STATE(deleteStatement);
+ mozStorageStatementScoper deleteStatementScoper(deleteStatement);
+
+ rv = deleteStatement->BindInt64ByName(NS_LITERAL_CSTRING("parent"), folder.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = deleteStatement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clean up orphan items annotations.
+ rv = mDB->MainConn()->ExecuteSimpleSQL(
+ NS_LITERAL_CSTRING(
+ "DELETE FROM moz_items_annos "
+ "WHERE id IN ("
+ "SELECT a.id from moz_items_annos a "
+ "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
+ "WHERE b.id ISNULL)"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set the lastModified date.
+ rv = SetItemDateInternal(LAST_MODIFIED, folder.id, RoundedPRNow());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Call observers in reverse order to serve children before their parent.
+ for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
+ BookmarkData& child = folderChildrenArray[i];
+
+ nsCOMPtr<nsIURI> uri;
+ if (child.type == TYPE_BOOKMARK) {
+ // If not a tag, recalculate frecency for this entry, since it changed.
+ if (child.grandParentId != mTagsRoot) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->UpdateFrecency(child.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ // A broken url should not interrupt the removal process.
+ (void)NS_NewURI(getter_AddRefs(uri), child.url);
+ // We cannot assert since some automated tests are checking this path.
+ NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveFolderChildren");
+ }
+
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ ((child.grandParentId == mTagsRoot) ? SkipTags : SkipDescendants),
+ OnItemRemoved(child.id,
+ child.parentId,
+ child.position,
+ child.type,
+ uri,
+ child.guid,
+ child.parentGuid,
+ aSource));
+
+ if (child.type == TYPE_BOOKMARK && child.grandParentId == mTagsRoot &&
+ uri) {
+ // If the removed bookmark was a child of a tag container, notify all
+ // bookmark-folder result nodes which contain a bookmark for the removed
+ // bookmark's url.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(uri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ DontSkip,
+ OnItemChanged(bookmarks[i].id,
+ NS_LITERAL_CSTRING("tags"),
+ false,
+ EmptyCString(),
+ bookmarks[i].lastModified,
+ TYPE_BOOKMARK,
+ bookmarks[i].parentId,
+ bookmarks[i].guid,
+ bookmarks[i].parentGuid,
+ EmptyCString(),
+ aSource));
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::MoveItem(int64_t aItemId,
+ int64_t aNewParent,
+ int32_t aIndex,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG(!IsRoot(aItemId));
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_MIN(aNewParent, 1);
+ // -1 is append, but no other negative number is allowed.
+ NS_ENSURE_ARG_MIN(aIndex, -1);
+ // Disallow making an item its own parent.
+ NS_ENSURE_ARG(aItemId != aNewParent);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if parent and index are the same, nothing to do
+ if (bookmark.parentId == aNewParent && bookmark.position == aIndex)
+ return NS_OK;
+
+ // Make sure aNewParent is not aFolder or a subfolder of aFolder.
+ // TODO: make this performant, maybe with a nested tree (bug 408991).
+ if (bookmark.type == TYPE_FOLDER) {
+ int64_t ancestorId = aNewParent;
+
+ while (ancestorId) {
+ if (ancestorId == bookmark.id) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ rv = GetFolderIdForItem(ancestorId, &ancestorId);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ }
+
+ // calculate new index
+ int32_t newIndex, folderCount;
+ int64_t grandParentId;
+ nsAutoCString newParentGuid;
+ rv = FetchFolderInfo(aNewParent, &folderCount, newParentGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
+ aIndex >= folderCount) {
+ newIndex = folderCount;
+ // If the parent remains the same, then the folder is really being moved
+ // to count - 1 (since it's being removed from the old position)
+ if (bookmark.parentId == aNewParent) {
+ --newIndex;
+ }
+ } else {
+ newIndex = aIndex;
+
+ if (bookmark.parentId == aNewParent && newIndex > bookmark.position) {
+ // when an item is being moved lower in the same folder, the new index
+ // refers to the index before it was removed. Removal causes everything
+ // to shift up.
+ --newIndex;
+ }
+ }
+
+ // this is like the previous check, except this covers if
+ // the specified index was -1 (append), and the calculated
+ // new index is the same as the existing index
+ if (aNewParent == bookmark.parentId && newIndex == bookmark.position) {
+ // Nothing to do!
+ return NS_OK;
+ }
+
+ // adjust indices to account for the move
+ // do this before we update the parent/index fields
+ // or we'll re-adjust the index for the item we are moving
+ if (bookmark.parentId == aNewParent) {
+ // We can optimize the updates if moving within the same container.
+ // We only shift the items between the old and new positions, since the
+ // insertion will offset the deletion.
+ if (bookmark.position > newIndex) {
+ rv = AdjustIndices(bookmark.parentId, newIndex, bookmark.position - 1, 1);
+ }
+ else {
+ rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, newIndex, -1);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ // We're moving between containers, so this happens in two steps.
+ // First, fill the hole from the removal from the old parent.
+ rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, INT32_MAX, -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Now, make room in the new parent for the insertion.
+ rv = AdjustIndices(aNewParent, newIndex, INT32_MAX, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ {
+ // Update parent and position.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET parent = :parent, position = :item_index "
+ "WHERE id = :item_id "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aNewParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), newIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ PRTime now = RoundedPRNow();
+ rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, now);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetItemDateInternal(LAST_MODIFIED, aNewParent, now);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemMoved(bookmark.id,
+ bookmark.parentId,
+ bookmark.position,
+ aNewParent,
+ newIndex,
+ bookmark.type,
+ bookmark.guid,
+ bookmark.parentGuid,
+ newParentGuid,
+ aSource));
+ return NS_OK;
+}
+
+nsresult
+nsNavBookmarks::FetchItemInfo(int64_t aItemId,
+ BookmarkData& _bookmark)
+{
+ // LEFT JOIN since not all bookmarks have an associated place.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT b.id, h.url, b.title, b.position, b.fk, b.parent, b.type, "
+ "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_bookmarks t ON t.id = b.parent "
+ "LEFT JOIN moz_places h ON h.id = b.fk "
+ "WHERE b.id = :item_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasResult) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ _bookmark.id = aItemId;
+ rv = stmt->GetUTF8String(1, _bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isNull;
+ rv = stmt->GetIsNull(2, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isNull) {
+ _bookmark.title.SetIsVoid(true);
+ }
+ else {
+ rv = stmt->GetUTF8String(2, _bookmark.title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = stmt->GetInt32(3, &_bookmark.position);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(4, &_bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(5, &_bookmark.parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt32(6, &_bookmark.type);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(7, reinterpret_cast<int64_t*>(&_bookmark.dateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(8, reinterpret_cast<int64_t*>(&_bookmark.lastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(9, _bookmark.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Getting properties of the root would show no parent.
+ rv = stmt->GetIsNull(10, &isNull);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = stmt->GetUTF8String(10, _bookmark.parentGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(11, &_bookmark.grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ _bookmark.grandParentId = -1;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
+ int64_t aItemId,
+ PRTime aValue)
+{
+ aValue = RoundToMilliseconds(aValue);
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ if (aDateType == DATE_ADDED) {
+ // lastModified is set to the same value as dateAdded. We do this for
+ // performance reasons, since it will allow us to use an index to sort items
+ // by date.
+ stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET dateAdded = :date, lastModified = :date "
+ "WHERE id = :item_id"
+ );
+ }
+ else {
+ stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET lastModified = :date WHERE id = :item_id"
+ );
+ }
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date"), aValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // note, we are not notifying the observers
+ // that the item has changed.
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::SetItemDateAdded(int64_t aItemId, PRTime aDateAdded,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Round here so that we notify with the right value.
+ bookmark.dateAdded = RoundToMilliseconds(aDateAdded);
+
+ rv = SetItemDateInternal(DATE_ADDED, bookmark.id, bookmark.dateAdded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmark.id,
+ NS_LITERAL_CSTRING("dateAdded"),
+ false,
+ nsPrintfCString("%lld", bookmark.dateAdded),
+ bookmark.dateAdded,
+ bookmark.type,
+ bookmark.parentId,
+ bookmark.guid,
+ bookmark.parentGuid,
+ EmptyCString(),
+ aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetItemDateAdded(int64_t aItemId, PRTime* _dateAdded)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_dateAdded);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_dateAdded = bookmark.dateAdded;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::SetItemLastModified(int64_t aItemId, PRTime aLastModified,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Round here so that we notify with the right value.
+ bookmark.lastModified = RoundToMilliseconds(aLastModified);
+
+ rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmark.id,
+ NS_LITERAL_CSTRING("lastModified"),
+ false,
+ nsPrintfCString("%lld", bookmark.lastModified),
+ bookmark.lastModified,
+ bookmark.type,
+ bookmark.parentId,
+ bookmark.guid,
+ bookmark.parentGuid,
+ EmptyCString(),
+ aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetItemLastModified(int64_t aItemId, PRTime* _lastModified)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_lastModified);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_lastModified = bookmark.lastModified;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::SetItemTitle(int64_t aItemId, const nsACString& aTitle,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET title = :item_title, lastModified = :date "
+ "WHERE id = :item_id "
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsCString title;
+ TruncateTitle(aTitle, title);
+
+ // Support setting a null title, we support this in insertBookmark.
+ if (title.IsVoid()) {
+ rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title"));
+ }
+ else {
+ rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
+ title);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ bookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
+ bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmark.id,
+ NS_LITERAL_CSTRING("title"),
+ false,
+ title,
+ bookmark.lastModified,
+ bookmark.type,
+ bookmark.parentId,
+ bookmark.guid,
+ bookmark.parentGuid,
+ EmptyCString(),
+ aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetItemTitle(int64_t aItemId,
+ nsACString& _title)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ _title = bookmark.title;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetBookmarkURI(int64_t aItemId,
+ nsIURI** _URI)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_URI);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_NewURI(_URI, bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetItemType(int64_t aItemId, uint16_t* _type)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_type);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_type = static_cast<uint16_t>(bookmark.type);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::ResultNodeForContainer(int64_t aItemId,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aNode)
+{
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bookmark.type == TYPE_FOLDER) { // TYPE_FOLDER
+ *aNode = new nsNavHistoryFolderResultNode(bookmark.title,
+ aOptions,
+ bookmark.id);
+ }
+ else {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ (*aNode)->mDateAdded = bookmark.dateAdded;
+ (*aNode)->mLastModified = bookmark.lastModified;
+ (*aNode)->mBookmarkGuid = bookmark.guid;
+ (*aNode)->GetAsFolder()->mTargetFolderGuid = bookmark.guid;
+
+ NS_ADDREF(*aNode);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::QueryFolderChildren(
+ int64_t aFolderId,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aChildren)
+{
+ NS_ENSURE_ARG_POINTER(aOptions);
+ NS_ENSURE_ARG_POINTER(aChildren);
+
+ // Select all children of a given folder, sorted by position.
+ // This is a LEFT JOIN because not all bookmarks types have a place.
+ // We construct a result where the first columns exactly match those returned
+ // by mDBGetURLPageInfo, and additionally contains columns for position,
+ // item_child, and folder_child from moz_bookmarks.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, "
+ "h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, "
+ "b.parent, null, h.frecency, h.hidden, h.guid, null, null, null, "
+ "b.guid, b.position, b.type, b.fk "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_places h ON b.fk = h.id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE b.parent = :parent "
+ "ORDER BY b.position ASC"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(stmt, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t index = -1;
+ bool hasResult;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ rv = ProcessFolderNodeRow(row, aOptions, aChildren, index);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::ProcessFolderNodeRow(
+ mozIStorageValueArray* aRow,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aChildren,
+ int32_t& aCurrentIndex)
+{
+ NS_ENSURE_ARG_POINTER(aRow);
+ NS_ENSURE_ARG_POINTER(aOptions);
+ NS_ENSURE_ARG_POINTER(aChildren);
+
+ // The results will be in order of aCurrentIndex. Even if we don't add a node
+ // because it was excluded, we need to count its index, so do that before
+ // doing anything else.
+ aCurrentIndex++;
+
+ int32_t itemType;
+ nsresult rv = aRow->GetInt32(kGetChildrenIndex_Type, &itemType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t id;
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemId, &id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsNavHistoryResultNode> node;
+
+ if (itemType == TYPE_BOOKMARK) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->RowToResult(aRow, aOptions, getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t nodeType;
+ node->GetType(&nodeType);
+ if ((nodeType == nsINavHistoryResultNode::RESULT_TYPE_QUERY &&
+ aOptions->ExcludeQueries()) ||
+ (nodeType != nsINavHistoryResultNode::RESULT_TYPE_QUERY &&
+ nodeType != nsINavHistoryResultNode::RESULT_TYPE_FOLDER_SHORTCUT &&
+ aOptions->ExcludeItems())) {
+ return NS_OK;
+ }
+ }
+ else if (itemType == TYPE_FOLDER) {
+ // ExcludeReadOnlyFolders currently means "ExcludeLivemarks" (to be fixed in
+ // bug 1072833)
+ if (aOptions->ExcludeReadOnlyFolders()) {
+ if (IsLivemark(id))
+ return NS_OK;
+ }
+
+ nsAutoCString title;
+ rv = aRow->GetUTF8String(nsNavHistory::kGetInfoIndex_Title, title);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ node = new nsNavHistoryFolderResultNode(title, aOptions, id);
+
+ rv = aRow->GetUTF8String(kGetChildrenIndex_Guid, node->mBookmarkGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ node->GetAsFolder()->mTargetFolderGuid = node->mBookmarkGuid;
+
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemDateAdded,
+ reinterpret_cast<int64_t*>(&node->mDateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemLastModified,
+ reinterpret_cast<int64_t*>(&node->mLastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else {
+ // This is a separator.
+ if (aOptions->ExcludeItems()) {
+ return NS_OK;
+ }
+ node = new nsNavHistorySeparatorResultNode();
+
+ node->mItemId = id;
+ rv = aRow->GetUTF8String(kGetChildrenIndex_Guid, node->mBookmarkGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemDateAdded,
+ reinterpret_cast<int64_t*>(&node->mDateAdded));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aRow->GetInt64(nsNavHistory::kGetInfoIndex_ItemLastModified,
+ reinterpret_cast<int64_t*>(&node->mLastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Store the index of the node within this container. Note that this is not
+ // moz_bookmarks.position.
+ node->mBookmarkIndex = aCurrentIndex;
+
+ NS_ENSURE_TRUE(aChildren->AppendObject(node), NS_ERROR_OUT_OF_MEMORY);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::QueryFolderChildrenAsync(
+ nsNavHistoryFolderResultNode* aNode,
+ int64_t aFolderId,
+ mozIStoragePendingStatement** _pendingStmt)
+{
+ NS_ENSURE_ARG_POINTER(aNode);
+ NS_ENSURE_ARG_POINTER(_pendingStmt);
+
+ // Select all children of a given folder, sorted by position.
+ // This is a LEFT JOIN because not all bookmarks types have a place.
+ // We construct a result where the first columns exactly match those returned
+ // by mDBGetURLPageInfo, and additionally contains columns for position,
+ // item_child, and folder_child from moz_bookmarks.
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
+ "SELECT h.id, h.url, IFNULL(b.title, h.title), h.rev_host, h.visit_count, "
+ "h.last_visit_date, f.url, b.id, b.dateAdded, b.lastModified, "
+ "b.parent, null, h.frecency, h.hidden, h.guid, null, null, null, "
+ "b.guid, b.position, b.type, b.fk "
+ "FROM moz_bookmarks b "
+ "LEFT JOIN moz_places h ON b.fk = h.id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE b.parent = :parent "
+ "ORDER BY b.position ASC"
+ );
+ NS_ENSURE_STATE(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
+ rv = stmt->ExecuteAsync(aNode, getter_AddRefs(pendingStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_pendingStmt = pendingStmt);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::FetchFolderInfo(int64_t aFolderId,
+ int32_t* _folderCount,
+ nsACString& _guid,
+ int64_t* _parentId)
+{
+ *_folderCount = 0;
+ *_parentId = -1;
+
+ // This query has to always return results, so it can't be written as a join,
+ // though a left join of 2 subqueries would have the same cost.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT count(*), "
+ "(SELECT guid FROM moz_bookmarks WHERE id = :parent), "
+ "(SELECT parent FROM moz_bookmarks WHERE id = :parent) "
+ "FROM moz_bookmarks "
+ "WHERE parent = :parent"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(hasResult, NS_ERROR_UNEXPECTED);
+
+ // Ensure that the folder we are looking for exists.
+ // Can't rely only on parent, since the root has parent 0, that doesn't exist.
+ bool isNull;
+ rv = stmt->GetIsNull(2, &isNull);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && (!isNull || aFolderId == 0),
+ NS_ERROR_INVALID_ARG);
+
+ rv = stmt->GetInt32(0, _folderCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isNull) {
+ rv = stmt->GetUTF8String(1, _guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(2, _parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::IsBookmarked(nsIURI* aURI, bool* aBookmarked)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(aBookmarked);
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT 1 FROM moz_bookmarks b "
+ "JOIN moz_places h ON b.fk = h.id "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->ExecuteStep(aBookmarked);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetBookmarkedURIFor(nsIURI* aURI, nsIURI** _retval)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = nullptr;
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ int64_t placeId;
+ nsAutoCString placeGuid;
+ nsresult rv = history->GetIdForPage(aURI, &placeId, placeGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!placeId) {
+ // This URI is unknown, just return null.
+ return NS_OK;
+ }
+
+ // Check if a bookmark exists in the redirects chain for this URI.
+ // The query will also check if the page is directly bookmarked, and return
+ // the first found bookmark in case. The check is directly on moz_bookmarks
+ // without special filtering.
+ // The next query finds the bookmarked ancestors in a redirects chain.
+ // It won't go further than 3 levels of redirects (a->b->c->your_place_id).
+ // To make this path 100% correct (up to any level) we would need either:
+ // - A separate hash, build through recursive querying of the database.
+ // This solution was previously implemented, but it had a negative effect
+ // on startup since at each startup we have to recursively query the
+ // database to rebuild a hash that is always the same across sessions.
+ // It must be updated at each visit and bookmarks change too. The code to
+ // manage it is complex and prone to errors, sometimes causing incorrect
+ // data fetches (for example wrong favicon for a redirected bookmark).
+ // - A better way to track redirects for a visit.
+ // We would need a separate table to track redirects, in the table we would
+ // have visit_id, redirect_session. To get all sources for
+ // a visit then we could just join this table and get all visit_id that
+ // are in the same redirect_session as our visit. This has the drawback
+ // that we can't ensure data integrity in the downgrade -> upgrade path,
+ // since an old version would not update the table on new visits.
+ //
+ // For most cases these levels of redirects should be fine though, it's hard
+ // to hit a page that is 4 or 5 levels of redirects below a bookmarked page.
+ //
+ // As a bonus the query also checks first if place_id is already a bookmark,
+ // so you don't have to check that apart.
+
+ nsCString query = nsPrintfCString(
+ "SELECT url FROM moz_places WHERE id = ( "
+ "SELECT :page_id FROM moz_bookmarks WHERE fk = :page_id "
+ "UNION ALL "
+ "SELECT COALESCE(grandparent.place_id, parent.place_id) AS r_place_id "
+ "FROM moz_historyvisits dest "
+ "LEFT JOIN moz_historyvisits parent ON parent.id = dest.from_visit "
+ "AND dest.visit_type IN (%d, %d) "
+ "LEFT JOIN moz_historyvisits grandparent ON parent.from_visit = grandparent.id "
+ "AND parent.visit_type IN (%d, %d) "
+ "WHERE dest.place_id = :page_id "
+ "AND EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = r_place_id) "
+ "LIMIT 1 "
+ ")",
+ nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY,
+ nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY
+ );
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(query);
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool hasBookmarkedOrigin;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasBookmarkedOrigin)) &&
+ hasBookmarkedOrigin) {
+ nsAutoCString spec;
+ rv = stmt->GetUTF8String(0, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NS_NewURI(_retval, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If there is no bookmarked origin, we will just return null.
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::ChangeBookmarkURI(int64_t aBookmarkId, nsIURI* aNewURI,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aBookmarkId, 1);
+ NS_ENSURE_ARG(aNewURI);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_ARG(bookmark.type == TYPE_BOOKMARK);
+
+ mozStorageTransaction transaction(mDB->MainConn(), false);
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ int64_t newPlaceId;
+ nsAutoCString newPlaceGuid;
+ rv = history->GetOrCreateIdForPage(aNewURI, &newPlaceId, newPlaceGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!newPlaceId)
+ return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date "
+ "WHERE id = :item_id "
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), newPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bookmark.lastModified = RoundedPRNow();
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
+ bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = statement->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = transaction.Commit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = history->UpdateFrecency(newPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Upon changing the URI for a bookmark, update the frecency for the old
+ // place as well.
+ rv = history->UpdateFrecency(bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec;
+ rv = aNewURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmark.id,
+ NS_LITERAL_CSTRING("uri"),
+ false,
+ spec,
+ bookmark.lastModified,
+ bookmark.type,
+ bookmark.parentId,
+ bookmark.guid,
+ bookmark.parentGuid,
+ bookmark.url,
+ aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetFolderIdForItem(int64_t aItemId, int64_t* _parentId)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_parentId);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this should not happen, but see bug #400448 for details
+ NS_ENSURE_TRUE(bookmark.id != bookmark.parentId, NS_ERROR_UNEXPECTED);
+
+ *_parentId = bookmark.parentId;
+ return NS_OK;
+}
+
+
+nsresult
+nsNavBookmarks::GetBookmarkIdsForURITArray(nsIURI* aURI,
+ nsTArray<int64_t>& aResult,
+ bool aSkipTags)
+{
+ NS_ENSURE_ARG(aURI);
+
+ // Double ordering covers possible lastModified ties, that could happen when
+ // importing, syncing or due to extensions.
+ // Note: not using a JOIN is cheaper in this case.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "/* do not warn (bug 1175249) */ "
+ "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent "
+ "FROM moz_bookmarks b "
+ "JOIN moz_bookmarks t on t.id = b.parent "
+ "WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) "
+ "ORDER BY b.lastModified DESC, b.id DESC "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more;
+ while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) {
+ if (aSkipTags) {
+ // Skip tags, for the use-cases of this async getter they are useless.
+ int64_t grandParentId;
+ nsresult rv = stmt->GetInt64(5, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (grandParentId == mTagsRoot) {
+ continue;
+ }
+ }
+ int64_t bookmarkId;
+ rv = stmt->GetInt64(0, &bookmarkId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(aResult.AppendElement(bookmarkId), NS_ERROR_OUT_OF_MEMORY);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+nsNavBookmarks::GetBookmarksForURI(nsIURI* aURI,
+ nsTArray<BookmarkData>& aBookmarks)
+{
+ NS_ENSURE_ARG(aURI);
+
+ // Double ordering covers possible lastModified ties, that could happen when
+ // importing, syncing or due to extensions.
+ // Note: not using a JOIN is cheaper in this case.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "/* do not warn (bug 1175249) */ "
+ "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent "
+ "FROM moz_bookmarks b "
+ "JOIN moz_bookmarks t on t.id = b.parent "
+ "WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) "
+ "ORDER BY b.lastModified DESC, b.id DESC "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more;
+ nsAutoString tags;
+ while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) {
+ // Skip tags.
+ int64_t grandParentId;
+ nsresult rv = stmt->GetInt64(5, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (grandParentId == mTagsRoot) {
+ continue;
+ }
+
+ BookmarkData bookmark;
+ bookmark.grandParentId = grandParentId;
+ rv = stmt->GetInt64(0, &bookmark.id);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(1, bookmark.guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(2, &bookmark.parentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetInt64(3, reinterpret_cast<int64_t*>(&bookmark.lastModified));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(4, bookmark.parentGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(aBookmarks.AppendElement(bookmark), NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::GetBookmarkIdsForURI(nsIURI* aURI, uint32_t* aCount,
+ int64_t** aBookmarks)
+{
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(aCount);
+ NS_ENSURE_ARG_POINTER(aBookmarks);
+
+ *aCount = 0;
+ *aBookmarks = nullptr;
+ nsTArray<int64_t> bookmarks;
+
+ // Get the information from the DB as a TArray
+ // TODO (bug 653816): make this API skip tags by default.
+ nsresult rv = GetBookmarkIdsForURITArray(aURI, bookmarks, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copy the results into a new array for output
+ if (bookmarks.Length()) {
+ *aBookmarks =
+ static_cast<int64_t*>(moz_xmalloc(sizeof(int64_t) * bookmarks.Length()));
+ if (!*aBookmarks)
+ return NS_ERROR_OUT_OF_MEMORY;
+ for (uint32_t i = 0; i < bookmarks.Length(); i ++)
+ (*aBookmarks)[i] = bookmarks[i];
+ }
+
+ *aCount = bookmarks.Length();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetItemIndex(int64_t aItemId, int32_t* _index)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_POINTER(_index);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ // With respect to the API.
+ if (NS_FAILED(rv)) {
+ *_index = -1;
+ return NS_OK;
+ }
+
+ *_index = bookmark.position;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::SetItemIndex(int64_t aItemId,
+ int32_t aNewIndex,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aItemId, 1);
+ NS_ENSURE_ARG_MIN(aNewIndex, 0);
+
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure we are not going out of range.
+ int32_t folderCount;
+ int64_t grandParentId;
+ nsAutoCString folderGuid;
+ rv = FetchFolderInfo(bookmark.parentId, &folderCount, folderGuid, &grandParentId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(aNewIndex < folderCount, NS_ERROR_INVALID_ARG);
+ // Check the parent's guid is the expected one.
+ MOZ_ASSERT(bookmark.parentGuid == folderGuid);
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "UPDATE moz_bookmarks SET position = :item_index WHERE id = :item_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aNewIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemMoved(bookmark.id,
+ bookmark.parentId,
+ bookmark.position,
+ bookmark.parentId,
+ aNewIndex,
+ bookmark.type,
+ bookmark.guid,
+ bookmark.parentGuid,
+ bookmark.parentGuid,
+ aSource));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
+ const nsAString& aUserCasedKeyword,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG_MIN(aBookmarkId, 1);
+
+ // This also ensures the bookmark is valid.
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Shortcuts are always lowercased internally.
+ nsAutoString keyword(aUserCasedKeyword);
+ ToLowerCase(keyword);
+
+ // The same URI can be associated to more than one keyword, provided the post
+ // data differs. Check if there are already keywords associated to this uri.
+ nsTArray<nsString> oldKeywords;
+ {
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT keyword FROM moz_keywords WHERE place_id = :place_id"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ nsString oldKeyword;
+ rv = stmt->GetString(0, oldKeyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+ oldKeywords.AppendElement(oldKeyword);
+ }
+ }
+
+ // Trying to remove a non-existent keyword is a no-op.
+ if (keyword.IsEmpty() && oldKeywords.Length() == 0) {
+ return NS_OK;
+ }
+
+ if (keyword.IsEmpty()) {
+ // We are removing the existing keywords.
+ for (uint32_t i = 0; i < oldKeywords.Length(); ++i) {
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "DELETE FROM moz_keywords WHERE keyword = :old_keyword"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("old_keyword"),
+ oldKeywords[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(uri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmarks[i].id,
+ NS_LITERAL_CSTRING("keyword"),
+ false,
+ EmptyCString(),
+ bookmarks[i].lastModified,
+ TYPE_BOOKMARK,
+ bookmarks[i].parentId,
+ bookmarks[i].guid,
+ bookmarks[i].parentGuid,
+ EmptyCString(),
+ aSource));
+ }
+
+ return NS_OK;
+ }
+
+ // A keyword can only be associated to a single URI. Check if the requested
+ // keyword was already associated, in such a case we will need to notify about
+ // the change.
+ nsCOMPtr<nsIURI> oldUri;
+ {
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT url "
+ "FROM moz_keywords "
+ "JOIN moz_places h ON h.id = place_id "
+ "WHERE keyword = :keyword"
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ nsAutoCString spec;
+ rv = stmt->GetUTF8String(0, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NS_NewURI(getter_AddRefs(oldUri), spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // If another uri is using the new keyword, we must update the keyword entry.
+ // Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
+ // trigger.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ if (oldUri) {
+ // In both cases, notify about the change.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(oldUri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmarks[i].id,
+ NS_LITERAL_CSTRING("keyword"),
+ false,
+ EmptyCString(),
+ bookmarks[i].lastModified,
+ TYPE_BOOKMARK,
+ bookmarks[i].parentId,
+ bookmarks[i].guid,
+ bookmarks[i].parentGuid,
+ EmptyCString(),
+ aSource));
+ }
+
+ stmt = mDB->GetStatement(
+ "UPDATE moz_keywords SET place_id = :place_id WHERE keyword = :keyword"
+ );
+ NS_ENSURE_STATE(stmt);
+ }
+ else {
+ stmt = mDB->GetStatement(
+ "INSERT INTO moz_keywords (keyword, place_id) "
+ "VALUES (:keyword, :place_id)"
+ );
+ }
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // In both cases, notify about the change.
+ nsTArray<BookmarkData> bookmarks;
+ rv = GetBookmarksForURI(uri, bookmarks);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmarks[i].id,
+ NS_LITERAL_CSTRING("keyword"),
+ false,
+ NS_ConvertUTF16toUTF8(keyword),
+ bookmarks[i].lastModified,
+ TYPE_BOOKMARK,
+ bookmarks[i].parentId,
+ bookmarks[i].guid,
+ bookmarks[i].parentGuid,
+ EmptyCString(),
+ aSource));
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword)
+{
+ NS_ENSURE_ARG_MIN(aBookmarkId, 1);
+ aKeyword.Truncate(0);
+
+ // We can have multiple keywords for the same uri, here we'll just return the
+ // last created one.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT k.keyword "
+ "FROM moz_bookmarks b "
+ "JOIN moz_keywords k ON k.place_id = b.fk "
+ "WHERE b.id = :item_id "
+ "ORDER BY k.ROWID DESC "
+ "LIMIT 1"
+ ));
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+ aBookmarkId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ nsAutoString keyword;
+ rv = stmt->GetString(0, keyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aKeyword = keyword;
+ return NS_OK;
+ }
+
+ aKeyword.SetIsVoid(true);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword,
+ nsIURI** aURI)
+{
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_TRUE(!aUserCasedKeyword.IsEmpty(), NS_ERROR_INVALID_ARG);
+ *aURI = nullptr;
+
+ PLACES_WARN_DEPRECATED();
+
+ // Shortcuts are always lowercased internally.
+ nsAutoString keyword(aUserCasedKeyword);
+ ToLowerCase(keyword);
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT h.url "
+ "FROM moz_places h "
+ "JOIN moz_keywords k ON k.place_id = h.id "
+ "WHERE k.keyword = :keyword"
+ ));
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ nsAutoCString spec;
+ rv = stmt->GetUTF8String(0, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uri.forget(aURI);
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::RunInBatchMode(nsINavHistoryBatchCallback* aCallback,
+ nsISupports* aUserData) {
+ PROFILER_LABEL("nsNavBookmarks", "RunInBatchMode",
+ js::ProfileEntry::Category::OTHER);
+
+ NS_ENSURE_ARG(aCallback);
+
+ mBatching = true;
+
+ // Just forward the request to history. History service must exist for
+ // bookmarks to work and we are observing it, thus batch notifications will be
+ // forwarded to bookmarks observers.
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ nsresult rv = history->RunInBatchMode(aCallback, aUserData);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::AddObserver(nsINavBookmarkObserver* aObserver,
+ bool aOwnsWeak)
+{
+ NS_ENSURE_ARG(aObserver);
+
+ if (NS_WARN_IF(!mCanNotify))
+ return NS_ERROR_UNEXPECTED;
+
+ return mObservers.AppendWeakElement(aObserver, aOwnsWeak);
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::RemoveObserver(nsINavBookmarkObserver* aObserver)
+{
+ return mObservers.RemoveWeakElement(aObserver);
+}
+
+NS_IMETHODIMP
+nsNavBookmarks::GetObservers(uint32_t* _count,
+ nsINavBookmarkObserver*** _observers)
+{
+ NS_ENSURE_ARG_POINTER(_count);
+ NS_ENSURE_ARG_POINTER(_observers);
+
+ *_count = 0;
+ *_observers = nullptr;
+
+ if (!mCanNotify)
+ return NS_OK;
+
+ nsCOMArray<nsINavBookmarkObserver> observers;
+
+ // First add the category cache observers.
+ mCacheObservers.GetEntries(observers);
+
+ // Then add the other observers.
+ for (uint32_t i = 0; i < mObservers.Length(); ++i) {
+ const nsCOMPtr<nsINavBookmarkObserver> &observer = mObservers.ElementAt(i).GetValue();
+ // Skip nullified weak observers.
+ if (observer)
+ observers.AppendElement(observer);
+ }
+
+ if (observers.Count() == 0)
+ return NS_OK;
+
+ *_count = observers.Count();
+ observers.Forget(_observers);
+
+ return NS_OK;
+}
+
+void
+nsNavBookmarks::NotifyItemVisited(const ItemVisitData& aData)
+{
+ nsCOMPtr<nsIURI> uri;
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), aData.bookmark.url));
+ // Notify the visit only if we have a valid uri, otherwise the observer
+ // couldn't gather enough data from the notification.
+ // This should be false only if there's a bug in the code preceding us.
+ if (uri) {
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemVisited(aData.bookmark.id,
+ aData.visitId,
+ aData.time,
+ aData.transitionType,
+ uri,
+ aData.bookmark.parentId,
+ aData.bookmark.guid,
+ aData.bookmark.parentGuid));
+ }
+}
+
+void
+nsNavBookmarks::NotifyItemChanged(const ItemChangeData& aData)
+{
+ // A guid must always be defined.
+ MOZ_ASSERT(!aData.bookmark.guid.IsEmpty());
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(aData.bookmark.id,
+ aData.property,
+ aData.isAnnotation,
+ aData.newValue,
+ aData.bookmark.lastModified,
+ aData.bookmark.type,
+ aData.bookmark.parentId,
+ aData.bookmark.guid,
+ aData.bookmark.parentGuid,
+ aData.oldValue,
+ // We specify the default source here because
+ // this method is only called for history
+ // visits, and we don't track sources in
+ // history.
+ SOURCE_DEFAULT));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+nsNavBookmarks::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
+ // Stop Observing annotations.
+ nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+ if (annosvc) {
+ annosvc->RemoveObserver(this);
+ }
+ }
+ else if (strcmp(aTopic, TOPIC_PLACES_CONNECTION_CLOSED) == 0) {
+ // Don't even try to notify observers from this point on, the category
+ // cache would init services that could try to use our APIs.
+ mCanNotify = false;
+ mObservers.Clear();
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsINavHistoryObserver
+
+NS_IMETHODIMP
+nsNavBookmarks::OnBeginUpdateBatch()
+{
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver, OnBeginUpdateBatch());
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnEndUpdateBatch()
+{
+ if (mBatching) {
+ mBatching = false;
+ }
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver, OnEndUpdateBatch());
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
+ int64_t aSessionID, int64_t aReferringID,
+ uint32_t aTransitionType, const nsACString& aGUID,
+ bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
+{
+ NS_ENSURE_ARG(aURI);
+
+ // If the page is bookmarked, notify observers for each associated bookmark.
+ ItemVisitData visitData;
+ nsresult rv = aURI->GetSpec(visitData.bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ visitData.visitId = aVisitId;
+ visitData.time = aTime;
+ visitData.transitionType = aTransitionType;
+
+ RefPtr< AsyncGetBookmarksForURI<ItemVisitMethod, ItemVisitData> > notifier =
+ new AsyncGetBookmarksForURI<ItemVisitMethod, ItemVisitData>(this, &nsNavBookmarks::NotifyItemVisited, visitData);
+ notifier->Init();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnDeleteURI(nsIURI* aURI,
+ const nsACString& aGUID,
+ uint16_t aReason)
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnClearHistory()
+{
+ // TODO(bryner): we should notify on visited-time change for all URIs
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnTitleChanged(nsIURI* aURI,
+ const nsAString& aPageTitle,
+ const nsACString& aGUID)
+{
+ // NOOP. We don't consume page titles from moz_places anymore.
+ // Title-change notifications are sent from SetItemTitle.
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnFrecencyChanged(nsIURI* aURI,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate)
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnManyFrecenciesChanged()
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnPageChanged(nsIURI* aURI,
+ uint32_t aChangedAttribute,
+ const nsAString& aNewValue,
+ const nsACString& aGUID)
+{
+ NS_ENSURE_ARG(aURI);
+
+ nsresult rv;
+ if (aChangedAttribute == nsINavHistoryObserver::ATTRIBUTE_FAVICON) {
+ ItemChangeData changeData;
+ rv = aURI->GetSpec(changeData.bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ changeData.property = NS_LITERAL_CSTRING("favicon");
+ changeData.isAnnotation = false;
+ changeData.newValue = NS_ConvertUTF16toUTF8(aNewValue);
+ changeData.bookmark.lastModified = 0;
+ changeData.bookmark.type = TYPE_BOOKMARK;
+
+ // Favicons may be set to either pure URIs or to folder URIs
+ bool isPlaceURI;
+ rv = aURI->SchemeIs("place", &isPlaceURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isPlaceURI) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+
+ nsCOMArray<nsNavHistoryQuery> queries;
+ nsCOMPtr<nsNavHistoryQueryOptions> options;
+ rv = history->QueryStringToQueryArray(changeData.bookmark.url,
+ &queries, getter_AddRefs(options));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (queries.Count() == 1 && queries[0]->Folders().Length() == 1) {
+ // Fetch missing data.
+ rv = FetchItemInfo(queries[0]->Folders()[0], changeData.bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NotifyItemChanged(changeData);
+ }
+ }
+ else {
+ RefPtr< AsyncGetBookmarksForURI<ItemChangeMethod, ItemChangeData> > notifier =
+ new AsyncGetBookmarksForURI<ItemChangeMethod, ItemChangeData>(this, &nsNavBookmarks::NotifyItemChanged, changeData);
+ notifier->Init();
+ }
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnDeleteVisits(nsIURI* aURI, PRTime aVisitTime,
+ const nsACString& aGUID,
+ uint16_t aReason, uint32_t aTransitionType)
+{
+ NS_ENSURE_ARG(aURI);
+
+ // Notify "cleartime" only if all visits to the page have been removed.
+ if (!aVisitTime) {
+ // If the page is bookmarked, notify observers for each associated bookmark.
+ ItemChangeData changeData;
+ nsresult rv = aURI->GetSpec(changeData.bookmark.url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ changeData.property = NS_LITERAL_CSTRING("cleartime");
+ changeData.isAnnotation = false;
+ changeData.bookmark.lastModified = 0;
+ changeData.bookmark.type = TYPE_BOOKMARK;
+
+ RefPtr< AsyncGetBookmarksForURI<ItemChangeMethod, ItemChangeData> > notifier =
+ new AsyncGetBookmarksForURI<ItemChangeMethod, ItemChangeData>(this, &nsNavBookmarks::NotifyItemChanged, changeData);
+ notifier->Init();
+ }
+ return NS_OK;
+}
+
+
+// nsIAnnotationObserver
+
+NS_IMETHODIMP
+nsNavBookmarks::OnPageAnnotationSet(nsIURI* aPage, const nsACString& aName)
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnItemAnnotationSet(int64_t aItemId, const nsACString& aName,
+ uint16_t aSource)
+{
+ BookmarkData bookmark;
+ nsresult rv = FetchItemInfo(aItemId, bookmark);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bookmark.lastModified = RoundedPRNow();
+ rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavBookmarkObserver,
+ OnItemChanged(bookmark.id,
+ aName,
+ true,
+ EmptyCString(),
+ bookmark.lastModified,
+ bookmark.type,
+ bookmark.parentId,
+ bookmark.guid,
+ bookmark.parentGuid,
+ EmptyCString(),
+ aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnPageAnnotationRemoved(nsIURI* aPage, const nsACString& aName)
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavBookmarks::OnItemAnnotationRemoved(int64_t aItemId, const nsACString& aName,
+ uint16_t aSource)
+{
+ // As of now this is doing the same as OnItemAnnotationSet, so just forward
+ // the call.
+ nsresult rv = OnItemAnnotationSet(aItemId, aName, aSource);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
diff --git a/toolkit/components/places/nsNavBookmarks.h b/toolkit/components/places/nsNavBookmarks.h
new file mode 100644
index 0000000000..d5cc3b5b75
--- /dev/null
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -0,0 +1,445 @@
+/* -*- 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/. */
+
+#ifndef nsNavBookmarks_h_
+#define nsNavBookmarks_h_
+
+#include "nsINavBookmarksService.h"
+#include "nsIAnnotationService.h"
+#include "nsITransaction.h"
+#include "nsNavHistory.h"
+#include "nsToolkitCompsCID.h"
+#include "nsCategoryCache.h"
+#include "nsTHashtable.h"
+#include "nsWeakReference.h"
+#include "mozilla/Attributes.h"
+#include "prtime.h"
+
+class nsNavBookmarks;
+
+namespace mozilla {
+namespace places {
+
+ enum BookmarkStatementId {
+ DB_FIND_REDIRECTED_BOOKMARK = 0
+ , DB_GET_BOOKMARKS_FOR_URI
+ };
+
+ struct BookmarkData {
+ int64_t id;
+ nsCString url;
+ nsCString title;
+ int32_t position;
+ int64_t placeId;
+ int64_t parentId;
+ int64_t grandParentId;
+ int32_t type;
+ nsCString serviceCID;
+ PRTime dateAdded;
+ PRTime lastModified;
+ nsCString guid;
+ nsCString parentGuid;
+ };
+
+ struct ItemVisitData {
+ BookmarkData bookmark;
+ int64_t visitId;
+ uint32_t transitionType;
+ PRTime time;
+ };
+
+ struct ItemChangeData {
+ BookmarkData bookmark;
+ nsCString property;
+ bool isAnnotation;
+ nsCString newValue;
+ nsCString oldValue;
+ };
+
+ typedef void (nsNavBookmarks::*ItemVisitMethod)(const ItemVisitData&);
+ typedef void (nsNavBookmarks::*ItemChangeMethod)(const ItemChangeData&);
+
+ enum BookmarkDate {
+ DATE_ADDED = 0
+ , LAST_MODIFIED
+ };
+
+} // namespace places
+} // namespace mozilla
+
+class nsNavBookmarks final : public nsINavBookmarksService
+ , public nsINavHistoryObserver
+ , public nsIAnnotationObserver
+ , public nsIObserver
+ , public nsSupportsWeakReference
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINAVBOOKMARKSSERVICE
+ NS_DECL_NSINAVHISTORYOBSERVER
+ NS_DECL_NSIANNOTATIONOBSERVER
+ NS_DECL_NSIOBSERVER
+
+ nsNavBookmarks();
+
+ /**
+ * Obtains the service's object.
+ */
+ static already_AddRefed<nsNavBookmarks> GetSingleton();
+
+ /**
+ * Initializes the service's object. This should only be called once.
+ */
+ nsresult Init();
+
+ static nsNavBookmarks* GetBookmarksService() {
+ if (!gBookmarksService) {
+ nsCOMPtr<nsINavBookmarksService> serv =
+ do_GetService(NS_NAVBOOKMARKSSERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(serv, nullptr);
+ NS_ASSERTION(gBookmarksService,
+ "Should have static instance pointer now");
+ }
+ return gBookmarksService;
+ }
+
+ typedef mozilla::places::BookmarkData BookmarkData;
+ typedef mozilla::places::ItemVisitData ItemVisitData;
+ typedef mozilla::places::ItemChangeData ItemChangeData;
+ typedef mozilla::places::BookmarkStatementId BookmarkStatementId;
+
+ nsresult ResultNodeForContainer(int64_t aID,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aNode);
+
+ // Find all the children of a folder, using the given query and options.
+ // For each child, a ResultNode is created and added to |children|.
+ // The results are ordered by folder position.
+ nsresult QueryFolderChildren(int64_t aFolderId,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* children);
+
+ /**
+ * Turns aRow into a node and appends it to aChildren if it is appropriate to
+ * do so.
+ *
+ * @param aRow
+ * A Storage statement (in the case of synchronous execution) or row of
+ * a result set (in the case of asynchronous execution).
+ * @param aOptions
+ * The options of the parent folder node.
+ * @param aChildren
+ * The children of the parent folder node.
+ * @param aCurrentIndex
+ * The index of aRow within the results. When called on the first row,
+ * this should be set to -1.
+ */
+ nsresult ProcessFolderNodeRow(mozIStorageValueArray* aRow,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aChildren,
+ int32_t& aCurrentIndex);
+
+ /**
+ * The async version of QueryFolderChildren.
+ *
+ * @param aNode
+ * The folder node that will receive the children.
+ * @param _pendingStmt
+ * The Storage pending statement that will be used to control async
+ * execution.
+ */
+ nsresult QueryFolderChildrenAsync(nsNavHistoryFolderResultNode* aNode,
+ int64_t aFolderId,
+ mozIStoragePendingStatement** _pendingStmt);
+
+ /**
+ * @return index of the new folder in aIndex, whether it was passed in or
+ * generated by autoincrement.
+ *
+ * @note If aFolder is -1, uses the autoincrement id for folder index.
+ * @note aTitle will be truncated to TITLE_LENGTH_MAX
+ */
+ nsresult CreateContainerWithID(int64_t aId, int64_t aParent,
+ const nsACString& aTitle,
+ bool aIsBookmarkFolder,
+ int32_t* aIndex,
+ const nsACString& aGUID,
+ uint16_t aSource,
+ int64_t* aNewFolder);
+
+ /**
+ * Fetches information about the specified id from the database.
+ *
+ * @param aItemId
+ * Id of the item to fetch information for.
+ * @param aBookmark
+ * BookmarkData to store the information.
+ */
+ nsresult FetchItemInfo(int64_t aItemId,
+ BookmarkData& _bookmark);
+
+ /**
+ * Notifies that a bookmark has been visited.
+ *
+ * @param aItemId
+ * The visited item id.
+ * @param aData
+ * Details about the new visit.
+ */
+ void NotifyItemVisited(const ItemVisitData& aData);
+
+ /**
+ * Notifies that a bookmark has changed.
+ *
+ * @param aItemId
+ * The changed item id.
+ * @param aData
+ * Details about the change.
+ */
+ void NotifyItemChanged(const ItemChangeData& aData);
+
+ /**
+ * Recursively builds an array of descendant folders inside a given folder.
+ *
+ * @param aFolderId
+ * The folder to fetch descendants from.
+ * @param aDescendantFoldersArray
+ * Output array to put descendant folders id.
+ */
+ nsresult GetDescendantFolders(int64_t aFolderId,
+ nsTArray<int64_t>& aDescendantFoldersArray);
+
+ static const int32_t kGetChildrenIndex_Guid;
+ static const int32_t kGetChildrenIndex_Position;
+ static const int32_t kGetChildrenIndex_Type;
+ static const int32_t kGetChildrenIndex_PlaceID;
+
+ static mozilla::Atomic<int64_t> sLastInsertedItemId;
+ static void StoreLastInsertedId(const nsACString& aTable,
+ const int64_t aLastInsertedId);
+
+private:
+ static nsNavBookmarks* gBookmarksService;
+
+ ~nsNavBookmarks();
+
+ /**
+ * Checks whether or not aFolderId points to a live bookmark.
+ *
+ * @param aFolderId
+ * the item-id of the folder to check.
+ * @return true if aFolderId points to live bookmarks, false otherwise.
+ */
+ bool IsLivemark(int64_t aFolderId);
+
+ /**
+ * Locates the root items in the bookmarks folder hierarchy assigning folder
+ * ids to the root properties that are exposed through the service interface.
+ */
+ nsresult ReadRoots();
+
+ nsresult AdjustIndices(int64_t aFolder,
+ int32_t aStartIndex,
+ int32_t aEndIndex,
+ int32_t aDelta);
+
+ /**
+ * Fetches properties of a folder.
+ *
+ * @param aFolderId
+ * Folder to count children for.
+ * @param _folderCount
+ * Number of children in the folder.
+ * @param _guid
+ * Unique id of the folder.
+ * @param _parentId
+ * Id of the parent of the folder.
+ *
+ * @throws If folder does not exist.
+ */
+ nsresult FetchFolderInfo(int64_t aFolderId,
+ int32_t* _folderCount,
+ nsACString& _guid,
+ int64_t* _parentId);
+
+ nsresult GetLastChildId(int64_t aFolder, int64_t* aItemId);
+
+ /**
+ * This is an handle to the Places database.
+ */
+ RefPtr<mozilla::places::Database> mDB;
+
+ int32_t mItemCount;
+
+ nsMaybeWeakPtrArray<nsINavBookmarkObserver> mObservers;
+
+ int64_t mRoot;
+ int64_t mMenuRoot;
+ int64_t mTagsRoot;
+ int64_t mUnfiledRoot;
+ int64_t mToolbarRoot;
+ int64_t mMobileRoot;
+
+ inline bool IsRoot(int64_t aFolderId) {
+ return aFolderId == mRoot || aFolderId == mMenuRoot ||
+ aFolderId == mTagsRoot || aFolderId == mUnfiledRoot ||
+ aFolderId == mToolbarRoot || aFolderId == mMobileRoot;
+ }
+
+ nsresult IsBookmarkedInDatabase(int64_t aBookmarkID, bool* aIsBookmarked);
+
+ nsresult SetItemDateInternal(enum mozilla::places::BookmarkDate aDateType,
+ int64_t aItemId,
+ PRTime aValue);
+
+ // Recursive method to build an array of folder's children
+ nsresult GetDescendantChildren(int64_t aFolderId,
+ const nsACString& aFolderGuid,
+ int64_t aGrandParentId,
+ nsTArray<BookmarkData>& aFolderChildrenArray);
+
+ enum ItemType {
+ BOOKMARK = TYPE_BOOKMARK,
+ FOLDER = TYPE_FOLDER,
+ SEPARATOR = TYPE_SEPARATOR,
+ };
+
+ /**
+ * Helper to insert a bookmark in the database.
+ *
+ * @param aItemId
+ * The itemId to insert, pass -1 to generate a new one.
+ * @param aPlaceId
+ * The placeId to which this bookmark refers to, pass nullptr for
+ * items that don't refer to an URI (eg. folders, separators, ...).
+ * @param aItemType
+ * The type of the new bookmark, see TYPE_* constants.
+ * @param aParentId
+ * The itemId of the parent folder.
+ * @param aIndex
+ * The position inside the parent folder.
+ * @param aTitle
+ * The title for the new bookmark.
+ * Pass a void string to set a NULL title.
+ * @param aDateAdded
+ * The date for the insertion.
+ * @param [optional] aLastModified
+ * The last modified date for the insertion.
+ * It defaults to aDateAdded.
+ *
+ * @return The new item id that has been inserted.
+ *
+ * @note This will also update last modified date of the parent folder.
+ */
+ nsresult InsertBookmarkInDB(int64_t aPlaceId,
+ enum ItemType aItemType,
+ int64_t aParentId,
+ int32_t aIndex,
+ const nsACString& aTitle,
+ PRTime aDateAdded,
+ PRTime aLastModified,
+ const nsACString& aParentGuid,
+ int64_t aGrandParentId,
+ nsIURI* aURI,
+ uint16_t aSource,
+ int64_t* _itemId,
+ nsACString& _guid);
+
+ /**
+ * TArray version of getBookmarksIdForURI for ease of use in C++ code.
+ * Pass in a reference to a TArray; it will get filled with the
+ * resulting list of bookmark IDs.
+ *
+ * @param aURI
+ * URI to get bookmarks for.
+ * @param aResult
+ * Array of bookmark ids.
+ * @param aSkipTags
+ * If true ids of tags-as-bookmarks entries will be excluded.
+ */
+ nsresult GetBookmarkIdsForURITArray(nsIURI* aURI,
+ nsTArray<int64_t>& aResult,
+ bool aSkipTags);
+
+ nsresult GetBookmarksForURI(nsIURI* aURI,
+ nsTArray<BookmarkData>& _bookmarks);
+
+ int64_t RecursiveFindRedirectedBookmark(int64_t aPlaceId);
+
+ class RemoveFolderTransaction final : public nsITransaction {
+ public:
+ RemoveFolderTransaction(int64_t aID, uint16_t aSource)
+ : mID(aID)
+ , mSource(aSource)
+ {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD DoTransaction() override {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+ BookmarkData folder;
+ nsresult rv = bookmarks->FetchItemInfo(mID, folder);
+ // TODO (Bug 656935): store the BookmarkData struct instead.
+ mParent = folder.parentId;
+ mIndex = folder.position;
+
+ rv = bookmarks->GetItemTitle(mID, mTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return bookmarks->RemoveItem(mID, mSource);
+ }
+
+ NS_IMETHOD UndoTransaction() override {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+ int64_t newFolder;
+ return bookmarks->CreateContainerWithID(mID, mParent, mTitle, true,
+ &mIndex, EmptyCString(),
+ mSource, &newFolder);
+ }
+
+ NS_IMETHOD RedoTransaction() override {
+ return DoTransaction();
+ }
+
+ NS_IMETHOD GetIsTransient(bool* aResult) override {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aResult) override {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ private:
+ ~RemoveFolderTransaction() {}
+
+ int64_t mID;
+ uint16_t mSource;
+ MOZ_INIT_OUTSIDE_CTOR int64_t mParent;
+ nsCString mTitle;
+ MOZ_INIT_OUTSIDE_CTOR int32_t mIndex;
+ };
+
+ // Used to enable and disable the observer notifications.
+ bool mCanNotify;
+ nsCategoryCache<nsINavBookmarkObserver> mCacheObservers;
+
+ // Tracks whether we are in batch mode.
+ // Note: this is only tracking bookmarks batches, not history ones.
+ bool mBatching;
+
+ /**
+ * This function must be called every time a bookmark is removed.
+ *
+ * @param aURI
+ * Uri to test.
+ */
+ nsresult UpdateKeywordsForRemovedBookmark(const BookmarkData& aBookmark);
+};
+
+#endif // nsNavBookmarks_h_
diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp
new file mode 100644
index 0000000000..8cf3a2e324
--- /dev/null
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -0,0 +1,4523 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <stdio.h>
+
+#include "mozilla/DebugOnly.h"
+
+#include "nsNavHistory.h"
+
+#include "mozIPlacesAutoComplete.h"
+#include "nsNavBookmarks.h"
+#include "nsAnnotationService.h"
+#include "nsFaviconService.h"
+#include "nsPlacesMacros.h"
+#include "History.h"
+#include "Helpers.h"
+
+#include "nsTArray.h"
+#include "nsCollationCID.h"
+#include "nsILocaleService.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsPromiseFlatString.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "prsystem.h"
+#include "prtime.h"
+#include "nsEscape.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIClassInfoImpl.h"
+#include "nsIIDNService.h"
+#include "nsThreadUtils.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsMathUtils.h"
+#include "mozilla/storage.h"
+#include "mozilla/Preferences.h"
+#include <algorithm>
+
+#ifdef MOZ_XUL
+#include "nsIAutoCompleteInput.h"
+#include "nsIAutoCompletePopup.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::places;
+
+// The maximum number of things that we will store in the recent events list
+// before calling ExpireNonrecentEvents. This number should be big enough so it
+// is very difficult to get that many unconsumed events (for example, typed but
+// never visited) in the RECENT_EVENT_THRESHOLD. Otherwise, we'll start
+// checking each one for every page visit, which will be somewhat slower.
+#define RECENT_EVENT_QUEUE_MAX_LENGTH 128
+
+// preference ID strings
+#define PREF_HISTORY_ENABLED "places.history.enabled"
+
+#define PREF_FREC_NUM_VISITS "places.frecency.numVisits"
+#define PREF_FREC_NUM_VISITS_DEF 10
+#define PREF_FREC_FIRST_BUCKET_CUTOFF "places.frecency.firstBucketCutoff"
+#define PREF_FREC_FIRST_BUCKET_CUTOFF_DEF 4
+#define PREF_FREC_SECOND_BUCKET_CUTOFF "places.frecency.secondBucketCutoff"
+#define PREF_FREC_SECOND_BUCKET_CUTOFF_DEF 14
+#define PREF_FREC_THIRD_BUCKET_CUTOFF "places.frecency.thirdBucketCutoff"
+#define PREF_FREC_THIRD_BUCKET_CUTOFF_DEF 31
+#define PREF_FREC_FOURTH_BUCKET_CUTOFF "places.frecency.fourthBucketCutoff"
+#define PREF_FREC_FOURTH_BUCKET_CUTOFF_DEF 90
+#define PREF_FREC_FIRST_BUCKET_WEIGHT "places.frecency.firstBucketWeight"
+#define PREF_FREC_FIRST_BUCKET_WEIGHT_DEF 100
+#define PREF_FREC_SECOND_BUCKET_WEIGHT "places.frecency.secondBucketWeight"
+#define PREF_FREC_SECOND_BUCKET_WEIGHT_DEF 70
+#define PREF_FREC_THIRD_BUCKET_WEIGHT "places.frecency.thirdBucketWeight"
+#define PREF_FREC_THIRD_BUCKET_WEIGHT_DEF 50
+#define PREF_FREC_FOURTH_BUCKET_WEIGHT "places.frecency.fourthBucketWeight"
+#define PREF_FREC_FOURTH_BUCKET_WEIGHT_DEF 30
+#define PREF_FREC_DEFAULT_BUCKET_WEIGHT "places.frecency.defaultBucketWeight"
+#define PREF_FREC_DEFAULT_BUCKET_WEIGHT_DEF 10
+#define PREF_FREC_EMBED_VISIT_BONUS "places.frecency.embedVisitBonus"
+#define PREF_FREC_EMBED_VISIT_BONUS_DEF 0
+#define PREF_FREC_FRAMED_LINK_VISIT_BONUS "places.frecency.framedLinkVisitBonus"
+#define PREF_FREC_FRAMED_LINK_VISIT_BONUS_DEF 0
+#define PREF_FREC_LINK_VISIT_BONUS "places.frecency.linkVisitBonus"
+#define PREF_FREC_LINK_VISIT_BONUS_DEF 100
+#define PREF_FREC_TYPED_VISIT_BONUS "places.frecency.typedVisitBonus"
+#define PREF_FREC_TYPED_VISIT_BONUS_DEF 2000
+#define PREF_FREC_BOOKMARK_VISIT_BONUS "places.frecency.bookmarkVisitBonus"
+#define PREF_FREC_BOOKMARK_VISIT_BONUS_DEF 75
+#define PREF_FREC_DOWNLOAD_VISIT_BONUS "places.frecency.downloadVisitBonus"
+#define PREF_FREC_DOWNLOAD_VISIT_BONUS_DEF 0
+#define PREF_FREC_PERM_REDIRECT_VISIT_BONUS "places.frecency.permRedirectVisitBonus"
+#define PREF_FREC_PERM_REDIRECT_VISIT_BONUS_DEF 0
+#define PREF_FREC_TEMP_REDIRECT_VISIT_BONUS "places.frecency.tempRedirectVisitBonus"
+#define PREF_FREC_TEMP_REDIRECT_VISIT_BONUS_DEF 0
+#define PREF_FREC_DEFAULT_VISIT_BONUS "places.frecency.defaultVisitBonus"
+#define PREF_FREC_DEFAULT_VISIT_BONUS_DEF 0
+#define PREF_FREC_UNVISITED_BOOKMARK_BONUS "places.frecency.unvisitedBookmarkBonus"
+#define PREF_FREC_UNVISITED_BOOKMARK_BONUS_DEF 140
+#define PREF_FREC_UNVISITED_TYPED_BONUS "places.frecency.unvisitedTypedBonus"
+#define PREF_FREC_UNVISITED_TYPED_BONUS_DEF 200
+#define PREF_FREC_RELOAD_VISIT_BONUS "places.frecency.reloadVisitBonus"
+#define PREF_FREC_RELOAD_VISIT_BONUS_DEF 0
+
+// In order to avoid calling PR_now() too often we use a cached "now" value
+// for repeating stuff. These are milliseconds between "now" cache refreshes.
+#define RENEW_CACHED_NOW_TIMEOUT ((int32_t)3 * PR_MSEC_PER_SEC)
+
+// character-set annotation
+#define CHARSET_ANNO NS_LITERAL_CSTRING("URIProperties/characterSet")
+
+// These macros are used when splitting history by date.
+// These are the day containers and catch-all final container.
+#define HISTORY_ADDITIONAL_DATE_CONT_NUM 3
+// We use a guess of the number of months considering all of them 30 days
+// long, but we split only the last 6 months.
+#define HISTORY_DATE_CONT_NUM(_daysFromOldestVisit) \
+ (HISTORY_ADDITIONAL_DATE_CONT_NUM + \
+ std::min(6, (int32_t)ceilf((float)_daysFromOldestVisit/30)))
+// Max number of containers, used to initialize the params hash.
+#define HISTORY_DATE_CONT_LENGTH 8
+
+// Initial length of the embed visits cache.
+#define EMBED_VISITS_INITIAL_CACHE_LENGTH 64
+
+// Initial length of the recent events cache.
+#define RECENT_EVENTS_INITIAL_CACHE_LENGTH 64
+
+// Observed topics.
+#ifdef MOZ_XUL
+#define TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING "autocomplete-will-enter-text"
+#endif
+#define TOPIC_IDLE_DAILY "idle-daily"
+#define TOPIC_PREF_CHANGED "nsPref:changed"
+#define TOPIC_PROFILE_TEARDOWN "profile-change-teardown"
+#define TOPIC_PROFILE_CHANGE "profile-before-change"
+
+static const char* kObservedPrefs[] = {
+ PREF_HISTORY_ENABLED
+, PREF_FREC_NUM_VISITS
+, PREF_FREC_FIRST_BUCKET_CUTOFF
+, PREF_FREC_SECOND_BUCKET_CUTOFF
+, PREF_FREC_THIRD_BUCKET_CUTOFF
+, PREF_FREC_FOURTH_BUCKET_CUTOFF
+, PREF_FREC_FIRST_BUCKET_WEIGHT
+, PREF_FREC_SECOND_BUCKET_WEIGHT
+, PREF_FREC_THIRD_BUCKET_WEIGHT
+, PREF_FREC_FOURTH_BUCKET_WEIGHT
+, PREF_FREC_DEFAULT_BUCKET_WEIGHT
+, PREF_FREC_EMBED_VISIT_BONUS
+, PREF_FREC_FRAMED_LINK_VISIT_BONUS
+, PREF_FREC_LINK_VISIT_BONUS
+, PREF_FREC_TYPED_VISIT_BONUS
+, PREF_FREC_BOOKMARK_VISIT_BONUS
+, PREF_FREC_DOWNLOAD_VISIT_BONUS
+, PREF_FREC_PERM_REDIRECT_VISIT_BONUS
+, PREF_FREC_TEMP_REDIRECT_VISIT_BONUS
+, PREF_FREC_DEFAULT_VISIT_BONUS
+, PREF_FREC_UNVISITED_BOOKMARK_BONUS
+, PREF_FREC_UNVISITED_TYPED_BONUS
+, nullptr
+};
+
+NS_IMPL_ADDREF(nsNavHistory)
+NS_IMPL_RELEASE(nsNavHistory)
+
+NS_IMPL_CLASSINFO(nsNavHistory, nullptr, nsIClassInfo::SINGLETON,
+ NS_NAVHISTORYSERVICE_CID)
+NS_INTERFACE_MAP_BEGIN(nsNavHistory)
+ NS_INTERFACE_MAP_ENTRY(nsINavHistoryService)
+ NS_INTERFACE_MAP_ENTRY(nsIBrowserHistory)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsPIPlacesDatabase)
+ NS_INTERFACE_MAP_ENTRY(mozIStorageVacuumParticipant)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryService)
+ NS_IMPL_QUERY_CLASSINFO(nsNavHistory)
+NS_INTERFACE_MAP_END
+
+// We don't care about flattening everything
+NS_IMPL_CI_INTERFACE_GETTER(nsNavHistory,
+ nsINavHistoryService,
+ nsIBrowserHistory)
+
+namespace {
+
+static int64_t GetSimpleBookmarksQueryFolder(
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions);
+static void ParseSearchTermsFromQueries(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsTArray<nsTArray<nsString>*>* aTerms);
+
+void GetTagsSqlFragment(int64_t aTagsFolder,
+ const nsACString& aRelation,
+ bool aHasSearchTerms,
+ nsACString& _sqlFragment) {
+ if (!aHasSearchTerms)
+ _sqlFragment.AssignLiteral("null");
+ else {
+ // This subquery DOES NOT order tags for performance reasons.
+ _sqlFragment.Assign(NS_LITERAL_CSTRING(
+ "(SELECT GROUP_CONCAT(t_t.title, ',') "
+ "FROM moz_bookmarks b_t "
+ "JOIN moz_bookmarks t_t ON t_t.id = +b_t.parent "
+ "WHERE b_t.fk = ") + aRelation + NS_LITERAL_CSTRING(" "
+ "AND t_t.parent = ") +
+ nsPrintfCString("%lld", aTagsFolder) + NS_LITERAL_CSTRING(" "
+ ")"));
+ }
+
+ _sqlFragment.AppendLiteral(" AS tags ");
+}
+
+/**
+ * This class sets begin/end of batch updates to correspond to C++ scopes so
+ * we can be sure end always gets called.
+ */
+class UpdateBatchScoper
+{
+public:
+ explicit UpdateBatchScoper(nsNavHistory& aNavHistory) : mNavHistory(aNavHistory)
+ {
+ mNavHistory.BeginUpdateBatch();
+ }
+ ~UpdateBatchScoper()
+ {
+ mNavHistory.EndUpdateBatch();
+ }
+protected:
+ nsNavHistory& mNavHistory;
+};
+
+} // namespace
+
+
+// Queries rows indexes to bind or get values, if adding a new one, be sure to
+// update nsNavBookmarks statements and its kGetChildrenIndex_* constants
+const int32_t nsNavHistory::kGetInfoIndex_PageID = 0;
+const int32_t nsNavHistory::kGetInfoIndex_URL = 1;
+const int32_t nsNavHistory::kGetInfoIndex_Title = 2;
+const int32_t nsNavHistory::kGetInfoIndex_RevHost = 3;
+const int32_t nsNavHistory::kGetInfoIndex_VisitCount = 4;
+const int32_t nsNavHistory::kGetInfoIndex_VisitDate = 5;
+const int32_t nsNavHistory::kGetInfoIndex_FaviconURL = 6;
+const int32_t nsNavHistory::kGetInfoIndex_ItemId = 7;
+const int32_t nsNavHistory::kGetInfoIndex_ItemDateAdded = 8;
+const int32_t nsNavHistory::kGetInfoIndex_ItemLastModified = 9;
+const int32_t nsNavHistory::kGetInfoIndex_ItemParentId = 10;
+const int32_t nsNavHistory::kGetInfoIndex_ItemTags = 11;
+const int32_t nsNavHistory::kGetInfoIndex_Frecency = 12;
+const int32_t nsNavHistory::kGetInfoIndex_Hidden = 13;
+const int32_t nsNavHistory::kGetInfoIndex_Guid = 14;
+const int32_t nsNavHistory::kGetInfoIndex_VisitId = 15;
+const int32_t nsNavHistory::kGetInfoIndex_FromVisitId = 16;
+const int32_t nsNavHistory::kGetInfoIndex_VisitType = 17;
+// These columns are followed by corresponding constants in nsNavBookmarks.cpp,
+// which must be kept in sync:
+// nsNavBookmarks::kGetChildrenIndex_Guid = 18;
+// nsNavBookmarks::kGetChildrenIndex_Position = 19;
+// nsNavBookmarks::kGetChildrenIndex_Type = 20;
+// nsNavBookmarks::kGetChildrenIndex_PlaceID = 21;
+
+PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavHistory, gHistoryService)
+
+
+nsNavHistory::nsNavHistory()
+ : mBatchLevel(0)
+ , mBatchDBTransaction(nullptr)
+ , mCachedNow(0)
+ , mRecentTyped(RECENT_EVENTS_INITIAL_CACHE_LENGTH)
+ , mRecentLink(RECENT_EVENTS_INITIAL_CACHE_LENGTH)
+ , mRecentBookmark(RECENT_EVENTS_INITIAL_CACHE_LENGTH)
+ , mEmbedVisits(EMBED_VISITS_INITIAL_CACHE_LENGTH)
+ , mHistoryEnabled(true)
+ , mNumVisitsForFrecency(10)
+ , mTagsFolder(-1)
+ , mDaysOfHistory(-1)
+ , mLastCachedStartOfDay(INT64_MAX)
+ , mLastCachedEndOfDay(0)
+ , mCanNotify(true)
+ , mCacheObservers("history-observers")
+{
+ NS_ASSERTION(!gHistoryService,
+ "Attempting to create two instances of the service!");
+ gHistoryService = this;
+}
+
+
+nsNavHistory::~nsNavHistory()
+{
+ // remove the static reference to the service. Check to make sure its us
+ // in case somebody creates an extra instance of the service.
+ NS_ASSERTION(gHistoryService == this,
+ "Deleting a non-singleton instance of the service");
+ if (gHistoryService == this)
+ gHistoryService = nullptr;
+}
+
+
+nsresult
+nsNavHistory::Init()
+{
+ LoadPrefs();
+
+ mDB = Database::GetDatabase();
+ NS_ENSURE_STATE(mDB);
+
+ /*****************************************************************************
+ *** IMPORTANT NOTICE!
+ ***
+ *** Nothing after these add observer calls should return anything but NS_OK.
+ *** If a failure code is returned, this nsNavHistory object will be held onto
+ *** by the observer service and the preference service.
+ ****************************************************************************/
+
+ // Observe preferences changes.
+ Preferences::AddWeakObservers(this, kObservedPrefs);
+
+ nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
+ if (obsSvc) {
+ (void)obsSvc->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
+ (void)obsSvc->AddObserver(this, TOPIC_IDLE_DAILY, true);
+#ifdef MOZ_XUL
+ (void)obsSvc->AddObserver(this, TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING, true);
+#endif
+ }
+
+ // Don't add code that can fail here! Do it up above, before we add our
+ // observers.
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistory::GetDatabaseStatus(uint16_t *aDatabaseStatus)
+{
+ NS_ENSURE_ARG_POINTER(aDatabaseStatus);
+ *aDatabaseStatus = mDB->GetDatabaseStatus();
+ return NS_OK;
+}
+
+uint32_t
+nsNavHistory::GetRecentFlags(nsIURI *aURI)
+{
+ uint32_t result = 0;
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to get aURI's spec");
+
+ if (NS_SUCCEEDED(rv)) {
+ if (CheckIsRecentEvent(&mRecentTyped, spec))
+ result |= RECENT_TYPED;
+ if (CheckIsRecentEvent(&mRecentLink, spec))
+ result |= RECENT_ACTIVATED;
+ if (CheckIsRecentEvent(&mRecentBookmark, spec))
+ result |= RECENT_BOOKMARKED;
+ }
+
+ return result;
+}
+
+nsresult
+nsNavHistory::GetIdForPage(nsIURI* aURI,
+ int64_t* _pageId,
+ nsCString& _GUID)
+{
+ *_pageId = 0;
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT id, url, title, rev_host, visit_count, guid "
+ "FROM moz_places "
+ "WHERE url_hash = hash(:page_url) AND url = :page_url "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasEntry = false;
+ rv = stmt->ExecuteStep(&hasEntry);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (hasEntry) {
+ rv = stmt->GetInt64(0, _pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->GetUTF8String(5, _GUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsNavHistory::GetOrCreateIdForPage(nsIURI* aURI,
+ int64_t* _pageId,
+ nsCString& _GUID)
+{
+ nsresult rv = GetIdForPage(aURI, _pageId, _GUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*_pageId != 0) {
+ return NS_OK;
+ }
+
+ // Create a new hidden, untyped and unvisited entry.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "INSERT INTO moz_places (url, url_hash, rev_host, hidden, frecency, guid) "
+ "VALUES (:page_url, hash(:page_url), :rev_host, :hidden, :frecency, :guid) "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // host (reversed with trailing period)
+ nsAutoString revHost;
+ rv = GetReversedHostname(aURI, revHost);
+ // Not all URI types have hostnames, so this is optional.
+ if (NS_SUCCEEDED(rv)) {
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("rev_host"), revHost);
+ } else {
+ rv = stmt->BindNullByName(NS_LITERAL_CSTRING("rev_host"));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString spec;
+ rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("frecency"),
+ IsQueryURI(spec) ? 0 : -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString guid;
+ rv = GenerateGUID(_GUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _GUID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = stmt->Execute();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_pageId = sLastInsertedPlaceId;
+
+ return NS_OK;
+}
+
+
+void
+nsNavHistory::LoadPrefs()
+{
+ // History preferences.
+ mHistoryEnabled = Preferences::GetBool(PREF_HISTORY_ENABLED, true);
+
+ // Frecency preferences.
+#define FRECENCY_PREF(_prop, _pref) \
+ _prop = Preferences::GetInt(_pref, _pref##_DEF)
+
+ FRECENCY_PREF(mNumVisitsForFrecency, PREF_FREC_NUM_VISITS);
+ FRECENCY_PREF(mFirstBucketCutoffInDays, PREF_FREC_FIRST_BUCKET_CUTOFF);
+ FRECENCY_PREF(mSecondBucketCutoffInDays, PREF_FREC_SECOND_BUCKET_CUTOFF);
+ FRECENCY_PREF(mThirdBucketCutoffInDays, PREF_FREC_THIRD_BUCKET_CUTOFF);
+ FRECENCY_PREF(mFourthBucketCutoffInDays, PREF_FREC_FOURTH_BUCKET_CUTOFF);
+ FRECENCY_PREF(mEmbedVisitBonus, PREF_FREC_EMBED_VISIT_BONUS);
+ FRECENCY_PREF(mFramedLinkVisitBonus, PREF_FREC_FRAMED_LINK_VISIT_BONUS);
+ FRECENCY_PREF(mLinkVisitBonus, PREF_FREC_LINK_VISIT_BONUS);
+ FRECENCY_PREF(mTypedVisitBonus, PREF_FREC_TYPED_VISIT_BONUS);
+ FRECENCY_PREF(mBookmarkVisitBonus, PREF_FREC_BOOKMARK_VISIT_BONUS);
+ FRECENCY_PREF(mDownloadVisitBonus, PREF_FREC_DOWNLOAD_VISIT_BONUS);
+ FRECENCY_PREF(mPermRedirectVisitBonus, PREF_FREC_PERM_REDIRECT_VISIT_BONUS);
+ FRECENCY_PREF(mTempRedirectVisitBonus, PREF_FREC_TEMP_REDIRECT_VISIT_BONUS);
+ FRECENCY_PREF(mDefaultVisitBonus, PREF_FREC_DEFAULT_VISIT_BONUS);
+ FRECENCY_PREF(mUnvisitedBookmarkBonus, PREF_FREC_UNVISITED_BOOKMARK_BONUS);
+ FRECENCY_PREF(mUnvisitedTypedBonus, PREF_FREC_UNVISITED_TYPED_BONUS);
+ FRECENCY_PREF(mReloadVisitBonus, PREF_FREC_RELOAD_VISIT_BONUS);
+ FRECENCY_PREF(mFirstBucketWeight, PREF_FREC_FIRST_BUCKET_WEIGHT);
+ FRECENCY_PREF(mSecondBucketWeight, PREF_FREC_SECOND_BUCKET_WEIGHT);
+ FRECENCY_PREF(mThirdBucketWeight, PREF_FREC_THIRD_BUCKET_WEIGHT);
+ FRECENCY_PREF(mFourthBucketWeight, PREF_FREC_FOURTH_BUCKET_WEIGHT);
+ FRECENCY_PREF(mDefaultWeight, PREF_FREC_DEFAULT_BUCKET_WEIGHT);
+
+#undef FRECENCY_PREF
+}
+
+
+void
+nsNavHistory::NotifyOnVisit(nsIURI* aURI,
+ int64_t aVisitId,
+ PRTime aTime,
+ int64_t aReferrerVisitId,
+ int32_t aTransitionType,
+ const nsACString& aGuid,
+ bool aHidden,
+ uint32_t aVisitCount,
+ uint32_t aTyped)
+{
+ MOZ_ASSERT(!aGuid.IsEmpty());
+ // If there's no history, this visit will surely add a day. If the visit is
+ // added before or after the last cached day, the day count may have changed.
+ // Otherwise adding multiple visits in the same day should not invalidate
+ // the cache.
+ if (mDaysOfHistory == 0) {
+ mDaysOfHistory = 1;
+ } else if (aTime > mLastCachedEndOfDay || aTime < mLastCachedStartOfDay) {
+ mDaysOfHistory = -1;
+ }
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnVisit(aURI, aVisitId, aTime, 0, aReferrerVisitId,
+ aTransitionType, aGuid, aHidden, aVisitCount, aTyped));
+}
+
+void
+nsNavHistory::NotifyTitleChange(nsIURI* aURI,
+ const nsString& aTitle,
+ const nsACString& aGUID)
+{
+ MOZ_ASSERT(!aGUID.IsEmpty());
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver, OnTitleChanged(aURI, aTitle, aGUID));
+}
+
+void
+nsNavHistory::NotifyFrecencyChanged(nsIURI* aURI,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate)
+{
+ MOZ_ASSERT(!aGUID.IsEmpty());
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden,
+ aLastVisitDate));
+}
+
+void
+nsNavHistory::NotifyManyFrecenciesChanged()
+{
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnManyFrecenciesChanged());
+}
+
+namespace {
+
+class FrecencyNotification : public Runnable
+{
+public:
+ FrecencyNotification(const nsACString& aSpec,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate)
+ : mSpec(aSpec)
+ , mNewFrecency(aNewFrecency)
+ , mGUID(aGUID)
+ , mHidden(aHidden)
+ , mLastVisitDate(aLastVisitDate)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
+ nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+ if (navHistory) {
+ nsCOMPtr<nsIURI> uri;
+ (void)NS_NewURI(getter_AddRefs(uri), mSpec);
+ // We cannot assert since some automated tests are checking this path.
+ NS_WARNING_ASSERTION(uri, "Invalid URI in FrecencyNotification");
+ // Notify a frecency change only if we have a valid uri, otherwise
+ // the observer couldn't gather any useful data from the notification.
+ if (uri) {
+ navHistory->NotifyFrecencyChanged(uri, mNewFrecency, mGUID, mHidden,
+ mLastVisitDate);
+ }
+ }
+ return NS_OK;
+ }
+
+private:
+ nsCString mSpec;
+ int32_t mNewFrecency;
+ nsCString mGUID;
+ bool mHidden;
+ PRTime mLastVisitDate;
+};
+
+} // namespace
+
+void
+nsNavHistory::DispatchFrecencyChangedNotification(const nsACString& aSpec,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate) const
+{
+ nsCOMPtr<nsIRunnable> notif = new FrecencyNotification(aSpec, aNewFrecency,
+ aGUID, aHidden,
+ aLastVisitDate);
+ (void)NS_DispatchToMainThread(notif);
+}
+
+Atomic<int64_t> nsNavHistory::sLastInsertedPlaceId(0);
+Atomic<int64_t> nsNavHistory::sLastInsertedVisitId(0);
+
+void // static
+nsNavHistory::StoreLastInsertedId(const nsACString& aTable,
+ const int64_t aLastInsertedId) {
+ if (aTable.Equals(NS_LITERAL_CSTRING("moz_places"))) {
+ nsNavHistory::sLastInsertedPlaceId = aLastInsertedId;
+ } else if (aTable.Equals(NS_LITERAL_CSTRING("moz_historyvisits"))) {
+ nsNavHistory::sLastInsertedVisitId = aLastInsertedId;
+ } else {
+ MOZ_ASSERT(false, "Trying to store the insert id for an unknown table?");
+ }
+}
+
+int32_t
+nsNavHistory::GetDaysOfHistory() {
+ MOZ_ASSERT(NS_IsMainThread(), "This can only be called on the main thread");
+
+ if (mDaysOfHistory != -1)
+ return mDaysOfHistory;
+
+ // SQLite doesn't have a CEIL() function, so we must do that later.
+ // We should also take into account timers resolution, that may be as bad as
+ // 16ms on Windows, so in some cases the difference may be 0, if the
+ // check is done near the visit. Thus remember to check for NULL separately.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT CAST(( "
+ "strftime('%s','now','localtime','utc') - "
+ "(SELECT MIN(visit_date)/1000000 FROM moz_historyvisits) "
+ ") AS DOUBLE) "
+ "/86400, "
+ "strftime('%s','now','localtime','+1 day','start of day','utc') * 1000000"
+ );
+ NS_ENSURE_TRUE(stmt, 0);
+ mozStorageStatementScoper scoper(stmt);
+
+ bool hasResult;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
+ // If we get NULL, then there are no visits, otherwise there must always be
+ // at least 1 day of history.
+ bool hasNoVisits;
+ (void)stmt->GetIsNull(0, &hasNoVisits);
+ mDaysOfHistory = hasNoVisits ?
+ 0 : std::max(1, static_cast<int32_t>(ceil(stmt->AsDouble(0))));
+ mLastCachedStartOfDay =
+ NormalizeTime(nsINavHistoryQuery::TIME_RELATIVE_TODAY, 0);
+ mLastCachedEndOfDay = stmt->AsInt64(1) - 1; // Start of tomorrow - 1.
+ }
+
+ return mDaysOfHistory;
+}
+
+PRTime
+nsNavHistory::GetNow()
+{
+ if (!mCachedNow) {
+ mCachedNow = PR_Now();
+ if (!mExpireNowTimer)
+ mExpireNowTimer = do_CreateInstance("@mozilla.org/timer;1");
+ if (mExpireNowTimer)
+ mExpireNowTimer->InitWithFuncCallback(expireNowTimerCallback, this,
+ RENEW_CACHED_NOW_TIMEOUT,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+ return mCachedNow;
+}
+
+
+void nsNavHistory::expireNowTimerCallback(nsITimer* aTimer, void* aClosure)
+{
+ nsNavHistory *history = static_cast<nsNavHistory *>(aClosure);
+ if (history) {
+ history->mCachedNow = 0;
+ history->mExpireNowTimer = nullptr;
+ }
+}
+
+
+/**
+ * Code borrowed from mozilla/xpfe/components/history/src/nsGlobalHistory.cpp
+ * Pass in a pre-normalized now and a date, and we'll find the difference since
+ * midnight on each of the days.
+ */
+static PRTime
+NormalizeTimeRelativeToday(PRTime aTime)
+{
+ // round to midnight this morning
+ PRExplodedTime explodedTime;
+ PR_ExplodeTime(aTime, PR_LocalTimeParameters, &explodedTime);
+
+ // set to midnight (0:00)
+ explodedTime.tm_min =
+ explodedTime.tm_hour =
+ explodedTime.tm_sec =
+ explodedTime.tm_usec = 0;
+
+ return PR_ImplodeTime(&explodedTime);
+}
+
+// nsNavHistory::NormalizeTime
+//
+// Converts a nsINavHistoryQuery reference+offset time into a PRTime
+// relative to the epoch.
+//
+// It is important that this function NOT use the current time optimization.
+// It is called to update queries, and we really need to know what right
+// now is because those incoming values will also have current times that
+// we will have to compare against.
+
+PRTime // static
+nsNavHistory::NormalizeTime(uint32_t aRelative, PRTime aOffset)
+{
+ PRTime ref;
+ switch (aRelative)
+ {
+ case nsINavHistoryQuery::TIME_RELATIVE_EPOCH:
+ return aOffset;
+ case nsINavHistoryQuery::TIME_RELATIVE_TODAY:
+ ref = NormalizeTimeRelativeToday(PR_Now());
+ break;
+ case nsINavHistoryQuery::TIME_RELATIVE_NOW:
+ ref = PR_Now();
+ break;
+ default:
+ NS_NOTREACHED("Invalid relative time");
+ return 0;
+ }
+ return ref + aOffset;
+}
+
+// nsNavHistory::GetUpdateRequirements
+//
+// Returns conditions for query update.
+//
+// QUERYUPDATE_TIME:
+// This query is only limited by an inclusive time range on the first
+// query object. The caller can quickly evaluate the time itself if it
+// chooses. This is even simpler than "simple" below.
+// QUERYUPDATE_SIMPLE:
+// This query is evaluatable using EvaluateQueryForNode to do live
+// updating.
+// QUERYUPDATE_COMPLEX:
+// This query is not evaluatable using EvaluateQueryForNode. When something
+// happens that this query updates, you will need to re-run the query.
+// QUERYUPDATE_COMPLEX_WITH_BOOKMARKS:
+// A complex query that additionally has dependence on bookmarks. All
+// bookmark-dependent queries fall under this category.
+//
+// aHasSearchTerms will be set to true if the query has any dependence on
+// keywords. When there is no dependence on keywords, we can handle title
+// change operations as simple instead of complex.
+
+uint32_t
+nsNavHistory::GetUpdateRequirements(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions,
+ bool* aHasSearchTerms)
+{
+ NS_ASSERTION(aQueries.Count() > 0, "Must have at least one query");
+
+ // first check if there are search terms
+ *aHasSearchTerms = false;
+ int32_t i;
+ for (i = 0; i < aQueries.Count(); i ++) {
+ aQueries[i]->GetHasSearchTerms(aHasSearchTerms);
+ if (*aHasSearchTerms)
+ break;
+ }
+
+ bool nonTimeBasedItems = false;
+ bool domainBasedItems = false;
+
+ for (i = 0; i < aQueries.Count(); i ++) {
+ nsNavHistoryQuery* query = aQueries[i];
+
+ if (query->Folders().Length() > 0 ||
+ query->OnlyBookmarked() ||
+ query->Tags().Length() > 0) {
+ return QUERYUPDATE_COMPLEX_WITH_BOOKMARKS;
+ }
+
+ // Note: we don't currently have any complex non-bookmarked items, but these
+ // are expected to be added. Put detection of these items here.
+ if (!query->SearchTerms().IsEmpty() ||
+ !query->Domain().IsVoid() ||
+ query->Uri() != nullptr)
+ nonTimeBasedItems = true;
+
+ if (! query->Domain().IsVoid())
+ domainBasedItems = true;
+ }
+
+ if (aOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_TAG_QUERY)
+ return QUERYUPDATE_COMPLEX_WITH_BOOKMARKS;
+
+ // Whenever there is a maximum number of results,
+ // and we are not a bookmark query we must requery. This
+ // is because we can't generally know if any given addition/change causes
+ // the item to be in the top N items in the database.
+ if (aOptions->MaxResults() > 0)
+ return QUERYUPDATE_COMPLEX;
+
+ if (aQueries.Count() == 1 && domainBasedItems)
+ return QUERYUPDATE_HOST;
+ if (aQueries.Count() == 1 && !nonTimeBasedItems)
+ return QUERYUPDATE_TIME;
+
+ return QUERYUPDATE_SIMPLE;
+}
+
+
+// nsNavHistory::EvaluateQueryForNode
+//
+// This runs the node through the given queries to see if satisfies the
+// query conditions. Not every query parameters are handled by this code,
+// but we handle the most common ones so that performance is better.
+//
+// We assume that the time on the node is the time that we want to compare.
+// This is not necessarily true because URL nodes have the last access time,
+// which is not necessarily the same. However, since this is being called
+// to update the list, we assume that the last access time is the current
+// access time that we are being asked to compare so it works out.
+//
+// Returns true if node matches the query, false if not.
+
+bool
+nsNavHistory::EvaluateQueryForNode(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode* aNode)
+{
+ // lazily created from the node's string when we need to match URIs
+ nsCOMPtr<nsIURI> nodeUri;
+
+ // --- hidden ---
+ if (aNode->mHidden && !aOptions->IncludeHidden())
+ return false;
+
+ for (int32_t i = 0; i < aQueries.Count(); i ++) {
+ bool hasIt;
+ nsCOMPtr<nsNavHistoryQuery> query = aQueries[i];
+
+ // --- begin time ---
+ query->GetHasBeginTime(&hasIt);
+ if (hasIt) {
+ PRTime beginTime = NormalizeTime(query->BeginTimeReference(),
+ query->BeginTime());
+ if (aNode->mTime < beginTime)
+ continue; // before our time range
+ }
+
+ // --- end time ---
+ query->GetHasEndTime(&hasIt);
+ if (hasIt) {
+ PRTime endTime = NormalizeTime(query->EndTimeReference(),
+ query->EndTime());
+ if (aNode->mTime > endTime)
+ continue; // after our time range
+ }
+
+ // --- search terms ---
+ if (! query->SearchTerms().IsEmpty()) {
+ // we can use the existing filtering code, just give it our one object in
+ // an array.
+ nsCOMArray<nsNavHistoryResultNode> inputSet;
+ inputSet.AppendObject(aNode);
+ nsCOMArray<nsNavHistoryQuery> queries;
+ queries.AppendObject(query);
+ nsCOMArray<nsNavHistoryResultNode> filteredSet;
+ nsresult rv = FilterResultSet(nullptr, inputSet, &filteredSet, queries, aOptions);
+ if (NS_FAILED(rv))
+ continue;
+ if (! filteredSet.Count())
+ continue; // did not make it through the filter, doesn't match
+ }
+
+ // --- domain/host matching ---
+ query->GetHasDomain(&hasIt);
+ if (hasIt) {
+ if (! nodeUri) {
+ // lazy creation of nodeUri, which might be checked for multiple queries
+ if (NS_FAILED(NS_NewURI(getter_AddRefs(nodeUri), aNode->mURI)))
+ continue;
+ }
+ nsAutoCString asciiRequest;
+ if (NS_FAILED(AsciiHostNameFromHostString(query->Domain(), asciiRequest)))
+ continue;
+
+ if (query->DomainIsHost()) {
+ nsAutoCString host;
+ if (NS_FAILED(nodeUri->GetAsciiHost(host)))
+ continue;
+
+ if (! asciiRequest.Equals(host))
+ continue; // host names don't match
+ }
+ // check domain names
+ nsAutoCString domain;
+ DomainNameFromURI(nodeUri, domain);
+ if (! asciiRequest.Equals(domain))
+ continue; // domain names don't match
+ }
+
+ // --- URI matching ---
+ if (query->Uri()) {
+ if (! nodeUri) { // lazy creation of nodeUri
+ if (NS_FAILED(NS_NewURI(getter_AddRefs(nodeUri), aNode->mURI)))
+ continue;
+ }
+
+ bool equals;
+ nsresult rv = query->Uri()->Equals(nodeUri, &equals);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (! equals)
+ continue;
+ }
+
+ // Transitions matching.
+ const nsTArray<uint32_t>& transitions = query->Transitions();
+ if (aNode->mTransitionType > 0 &&
+ transitions.Length() &&
+ !transitions.Contains(aNode->mTransitionType)) {
+ continue; // transition doesn't match.
+ }
+
+ // If we ever make it to the bottom of this loop, that means it passed all
+ // tests for the given query. Since queries are ORed together, that means
+ // it passed everything and we are done.
+ return true;
+ }
+
+ // didn't match any query
+ return false;
+}
+
+
+// nsNavHistory::AsciiHostNameFromHostString
+//
+// We might have interesting encodings and different case in the host name.
+// This will convert that host name into an ASCII host name by sending it
+// through the URI canonicalization. The result can be used for comparison
+// with other ASCII host name strings.
+nsresult // static
+nsNavHistory::AsciiHostNameFromHostString(const nsACString& aHostName,
+ nsACString& aAscii)
+{
+ // To properly generate a uri we must provide a protocol.
+ nsAutoCString fakeURL("http://");
+ fakeURL.Append(aHostName);
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), fakeURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = uri->GetAsciiHost(aAscii);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+
+// nsNavHistory::DomainNameFromURI
+//
+// This does the www.mozilla.org -> mozilla.org and
+// foo.theregister.co.uk -> theregister.co.uk conversion
+void
+nsNavHistory::DomainNameFromURI(nsIURI *aURI,
+ nsACString& aDomainName)
+{
+ // lazily get the effective tld service
+ if (!mTLDService)
+ mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+
+ if (mTLDService) {
+ // get the base domain for a given hostname.
+ // e.g. for "images.bbc.co.uk", this would be "bbc.co.uk".
+ nsresult rv = mTLDService->GetBaseDomain(aURI, 0, aDomainName);
+ if (NS_SUCCEEDED(rv))
+ return;
+ }
+
+ // just return the original hostname
+ // (it's also possible the host is an IP address)
+ aURI->GetAsciiHost(aDomainName);
+}
+
+
+NS_IMETHODIMP
+nsNavHistory::GetHasHistoryEntries(bool* aHasEntries)
+{
+ NS_ENSURE_ARG_POINTER(aHasEntries);
+ *aHasEntries = GetDaysOfHistory() > 0;
+ return NS_OK;
+}
+
+
+namespace {
+
+class InvalidateAllFrecenciesCallback : public AsyncStatementCallback
+{
+public:
+ InvalidateAllFrecenciesCallback()
+ {
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason)
+ {
+ if (aReason == REASON_FINISHED) {
+ nsNavHistory *navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(navHistory);
+ navHistory->NotifyManyFrecenciesChanged();
+ }
+ return NS_OK;
+ }
+};
+
+} // namespace
+
+nsresult
+nsNavHistory::invalidateFrecencies(const nsCString& aPlaceIdsQueryString)
+{
+ // Exclude place: queries by setting their frecency to zero.
+ nsCString invalidFrecenciesSQLFragment(
+ "UPDATE moz_places SET frecency = "
+ );
+ if (!aPlaceIdsQueryString.IsEmpty())
+ invalidFrecenciesSQLFragment.AppendLiteral("NOTIFY_FRECENCY(");
+ invalidFrecenciesSQLFragment.AppendLiteral(
+ "(CASE "
+ "WHEN url_hash BETWEEN hash('place', 'prefix_lo') AND "
+ "hash('place', 'prefix_hi') "
+ "THEN 0 "
+ "ELSE -1 "
+ "END) "
+ );
+ if (!aPlaceIdsQueryString.IsEmpty()) {
+ invalidFrecenciesSQLFragment.AppendLiteral(
+ ", url, guid, hidden, last_visit_date) "
+ );
+ }
+ invalidFrecenciesSQLFragment.AppendLiteral(
+ "WHERE frecency > 0 "
+ );
+ if (!aPlaceIdsQueryString.IsEmpty()) {
+ invalidFrecenciesSQLFragment.AppendLiteral("AND id IN(");
+ invalidFrecenciesSQLFragment.Append(aPlaceIdsQueryString);
+ invalidFrecenciesSQLFragment.Append(')');
+ }
+ RefPtr<InvalidateAllFrecenciesCallback> cb =
+ aPlaceIdsQueryString.IsEmpty() ? new InvalidateAllFrecenciesCallback()
+ : nullptr;
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
+ invalidFrecenciesSQLFragment
+ );
+ NS_ENSURE_STATE(stmt);
+
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ nsresult rv = stmt->ExecuteAsync(cb, getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+// Call this method before visiting a URL in order to help determine the
+// transition type of the visit.
+//
+// @see MarkPageAsTyped
+
+NS_IMETHODIMP
+nsNavHistory::MarkPageAsFollowedBookmark(nsIURI* aURI)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+
+ // don't add when history is disabled
+ if (IsHistoryDisabled())
+ return NS_OK;
+
+ nsAutoCString uriString;
+ nsresult rv = aURI->GetSpec(uriString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if URL is already in the bookmark queue, then we need to remove the old one
+ int64_t unusedEventTime;
+ if (mRecentBookmark.Get(uriString, &unusedEventTime))
+ mRecentBookmark.Remove(uriString);
+
+ if (mRecentBookmark.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
+ ExpireNonrecentEvents(&mRecentBookmark);
+
+ mRecentBookmark.Put(uriString, GetNow());
+ return NS_OK;
+}
+
+
+// nsNavHistory::CanAddURI
+//
+// Filter out unwanted URIs such as "chrome:", "mailbox:", etc.
+//
+// The model is if we don't know differently then add which basically means
+// we are suppose to try all the things we know not to allow in and then if
+// we don't bail go on and allow it in.
+
+NS_IMETHODIMP
+nsNavHistory::CanAddURI(nsIURI* aURI, bool* canAdd)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(canAdd);
+
+ // Default to false.
+ *canAdd = false;
+
+ // If history is disabled, don't add any entry.
+ if (IsHistoryDisabled()) {
+ return NS_OK;
+ }
+
+ // If the url length is over a threshold, don't add it.
+ nsCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mDB || spec.Length() > mDB->MaxUrlLength()) {
+ return NS_OK;
+ }
+
+ nsAutoCString scheme;
+ rv = aURI->GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // first check the most common cases (HTTP, HTTPS) to allow in to avoid most
+ // of the work
+ if (scheme.EqualsLiteral("http")) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("https")) {
+ *canAdd = true;
+ return NS_OK;
+ }
+
+ // now check for all bad things
+ if (scheme.EqualsLiteral("about") ||
+ scheme.EqualsLiteral("blob") ||
+ scheme.EqualsLiteral("chrome") ||
+ scheme.EqualsLiteral("data") ||
+ scheme.EqualsLiteral("imap") ||
+ scheme.EqualsLiteral("javascript") ||
+ scheme.EqualsLiteral("mailbox") ||
+ scheme.EqualsLiteral("moz-anno") ||
+ scheme.EqualsLiteral("news") ||
+ scheme.EqualsLiteral("page-icon") ||
+ scheme.EqualsLiteral("resource") ||
+ scheme.EqualsLiteral("view-source") ||
+ scheme.EqualsLiteral("wyciwyg")) {
+ return NS_OK;
+ }
+ *canAdd = true;
+ return NS_OK;
+}
+
+// nsNavHistory::GetNewQuery
+
+NS_IMETHODIMP
+nsNavHistory::GetNewQuery(nsINavHistoryQuery **_retval)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ RefPtr<nsNavHistoryQuery> query = new nsNavHistoryQuery();
+ query.forget(_retval);
+ return NS_OK;
+}
+
+// nsNavHistory::GetNewQueryOptions
+
+NS_IMETHODIMP
+nsNavHistory::GetNewQueryOptions(nsINavHistoryQueryOptions **_retval)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ RefPtr<nsNavHistoryQueryOptions> queryOptions = new nsNavHistoryQueryOptions();
+ queryOptions.forget(_retval);
+ return NS_OK;
+}
+
+// nsNavHistory::ExecuteQuery
+//
+
+NS_IMETHODIMP
+nsNavHistory::ExecuteQuery(nsINavHistoryQuery *aQuery, nsINavHistoryQueryOptions *aOptions,
+ nsINavHistoryResult** _retval)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aQuery);
+ NS_ENSURE_ARG(aOptions);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ return ExecuteQueries(&aQuery, 1, aOptions, _retval);
+}
+
+
+// nsNavHistory::ExecuteQueries
+//
+// This function is actually very simple, we just create the proper root node (either
+// a bookmark folder or a complex query node) and assign it to the result. The node
+// will then populate itself accordingly.
+//
+// Quick overview of query operation: When you call this function, we will construct
+// the correct container node and set the options you give it. This node will then
+// fill itself. Folder nodes will call nsNavBookmarks::QueryFolderChildren, and
+// all other queries will call GetQueryResults. If these results contain other
+// queries, those will be populated when the container is opened.
+
+NS_IMETHODIMP
+nsNavHistory::ExecuteQueries(nsINavHistoryQuery** aQueries, uint32_t aQueryCount,
+ nsINavHistoryQueryOptions *aOptions,
+ nsINavHistoryResult** _retval)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aQueries);
+ NS_ENSURE_ARG(aOptions);
+ NS_ENSURE_ARG(aQueryCount);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv;
+ // concrete options
+ nsCOMPtr<nsNavHistoryQueryOptions> options = do_QueryInterface(aOptions);
+ NS_ENSURE_TRUE(options, NS_ERROR_INVALID_ARG);
+
+ // concrete queries array
+ nsCOMArray<nsNavHistoryQuery> queries;
+ for (uint32_t i = 0; i < aQueryCount; i ++) {
+ nsCOMPtr<nsNavHistoryQuery> query = do_QueryInterface(aQueries[i], &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ queries.AppendElement(query.forget());
+ }
+
+ // Create the root node.
+ RefPtr<nsNavHistoryContainerResultNode> rootNode;
+ int64_t folderId = GetSimpleBookmarksQueryFolder(queries, options);
+ if (folderId) {
+ // In the simple case where we're just querying children of a single
+ // bookmark folder, we can more efficiently generate results.
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+ RefPtr<nsNavHistoryResultNode> tempRootNode;
+ rv = bookmarks->ResultNodeForContainer(folderId, options,
+ getter_AddRefs(tempRootNode));
+ if (NS_SUCCEEDED(rv)) {
+ rootNode = tempRootNode->GetAsContainer();
+ }
+ else {
+ NS_WARNING("Generating a generic empty node for a broken query!");
+ // This is a perf hack to generate an empty query that skips filtering.
+ options->SetExcludeItems(true);
+ }
+ }
+
+ if (!rootNode) {
+ // Either this is not a folder shortcut, or is a broken one. In both cases
+ // just generate a query node.
+ rootNode = new nsNavHistoryQueryResultNode(EmptyCString(), EmptyCString(),
+ queries, options);
+ }
+
+ // Create the result that will hold nodes. Inject batching status into it.
+ RefPtr<nsNavHistoryResult> result;
+ rv = nsNavHistoryResult::NewHistoryResult(aQueries, aQueryCount, options,
+ rootNode, isBatching(),
+ getter_AddRefs(result));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ result.forget(_retval);
+ return NS_OK;
+}
+
+// determine from our nsNavHistoryQuery array and nsNavHistoryQueryOptions
+// if this is the place query from the history menu.
+// from browser-menubar.inc, our history menu query is:
+// place:sort=4&maxResults=10
+// note, any maxResult > 0 will still be considered a history menu query
+// or if this is the place query from the "Most Visited" item in the
+// "Smart Bookmarks" folder: place:sort=8&maxResults=10
+// note, any maxResult > 0 will still be considered a Most Visited menu query
+static
+bool IsOptimizableHistoryQuery(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions *aOptions,
+ uint16_t aSortMode)
+{
+ if (aQueries.Count() != 1)
+ return false;
+
+ nsNavHistoryQuery *aQuery = aQueries[0];
+
+ if (aOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY)
+ return false;
+
+ if (aOptions->ResultType() != nsINavHistoryQueryOptions::RESULTS_AS_URI)
+ return false;
+
+ if (aOptions->SortingMode() != aSortMode)
+ return false;
+
+ if (aOptions->MaxResults() <= 0)
+ return false;
+
+ if (aOptions->ExcludeItems())
+ return false;
+
+ if (aOptions->IncludeHidden())
+ return false;
+
+ if (aQuery->MinVisits() != -1 || aQuery->MaxVisits() != -1)
+ return false;
+
+ if (aQuery->BeginTime() || aQuery->BeginTimeReference())
+ return false;
+
+ if (aQuery->EndTime() || aQuery->EndTimeReference())
+ return false;
+
+ if (!aQuery->SearchTerms().IsEmpty())
+ return false;
+
+ if (aQuery->OnlyBookmarked())
+ return false;
+
+ if (aQuery->DomainIsHost() || !aQuery->Domain().IsEmpty())
+ return false;
+
+ if (aQuery->AnnotationIsNot() || !aQuery->Annotation().IsEmpty())
+ return false;
+
+ if (aQuery->Folders().Length() > 0)
+ return false;
+
+ if (aQuery->Tags().Length() > 0)
+ return false;
+
+ if (aQuery->Transitions().Length() > 0)
+ return false;
+
+ return true;
+}
+
+static
+bool NeedToFilterResultSet(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions *aOptions)
+{
+ uint16_t resultType = aOptions->ResultType();
+ return resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS;
+}
+
+// ** Helper class for ConstructQueryString **/
+
+class PlacesSQLQueryBuilder
+{
+public:
+ PlacesSQLQueryBuilder(const nsCString& aConditions,
+ nsNavHistoryQueryOptions* aOptions,
+ bool aUseLimit,
+ nsNavHistory::StringHash& aAddParams,
+ bool aHasSearchTerms);
+
+ nsresult GetQueryString(nsCString& aQueryString);
+
+private:
+ nsresult Select();
+
+ nsresult SelectAsURI();
+ nsresult SelectAsVisit();
+ nsresult SelectAsDay();
+ nsresult SelectAsSite();
+ nsresult SelectAsTag();
+
+ nsresult Where();
+ nsresult GroupBy();
+ nsresult OrderBy();
+ nsresult Limit();
+
+ void OrderByColumnIndexAsc(int32_t aIndex);
+ void OrderByColumnIndexDesc(int32_t aIndex);
+ // Use these if you want a case insensitive sorting.
+ void OrderByTextColumnIndexAsc(int32_t aIndex);
+ void OrderByTextColumnIndexDesc(int32_t aIndex);
+
+ const nsCString& mConditions;
+ bool mUseLimit;
+ bool mHasSearchTerms;
+
+ uint16_t mResultType;
+ uint16_t mQueryType;
+ bool mIncludeHidden;
+ uint16_t mSortingMode;
+ uint32_t mMaxResults;
+
+ nsCString mQueryString;
+ nsCString mGroupBy;
+ bool mHasDateColumns;
+ bool mSkipOrderBy;
+ nsNavHistory::StringHash& mAddParams;
+};
+
+PlacesSQLQueryBuilder::PlacesSQLQueryBuilder(
+ const nsCString& aConditions,
+ nsNavHistoryQueryOptions* aOptions,
+ bool aUseLimit,
+ nsNavHistory::StringHash& aAddParams,
+ bool aHasSearchTerms)
+: mConditions(aConditions)
+, mUseLimit(aUseLimit)
+, mHasSearchTerms(aHasSearchTerms)
+, mResultType(aOptions->ResultType())
+, mQueryType(aOptions->QueryType())
+, mIncludeHidden(aOptions->IncludeHidden())
+, mSortingMode(aOptions->SortingMode())
+, mMaxResults(aOptions->MaxResults())
+, mSkipOrderBy(false)
+, mAddParams(aAddParams)
+{
+ mHasDateColumns = (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS);
+}
+
+nsresult
+PlacesSQLQueryBuilder::GetQueryString(nsCString& aQueryString)
+{
+ nsresult rv = Select();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = Where();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GroupBy();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = OrderBy();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = Limit();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aQueryString = mQueryString;
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::Select()
+{
+ nsresult rv;
+
+ switch (mResultType)
+ {
+ case nsINavHistoryQueryOptions::RESULTS_AS_URI:
+ case nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS:
+ rv = SelectAsURI();
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+
+ case nsINavHistoryQueryOptions::RESULTS_AS_VISIT:
+ case nsINavHistoryQueryOptions::RESULTS_AS_FULL_VISIT:
+ rv = SelectAsVisit();
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+
+ case nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY:
+ case nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY:
+ rv = SelectAsDay();
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+
+ case nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY:
+ rv = SelectAsSite();
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+
+ case nsINavHistoryQueryOptions::RESULTS_AS_TAG_QUERY:
+ rv = SelectAsTag();
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+
+ default:
+ NS_NOTREACHED("Invalid result type");
+ }
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::SelectAsURI()
+{
+ nsNavHistory *history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ nsAutoCString tagsSqlFragment;
+
+ switch (mQueryType) {
+ case nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY:
+ GetTagsSqlFragment(history->GetTagsFolder(),
+ NS_LITERAL_CSTRING("h.id"),
+ mHasSearchTerms,
+ tagsSqlFragment);
+
+ mQueryString = NS_LITERAL_CSTRING(
+ "SELECT h.id, h.url, h.title AS page_title, h.rev_host, h.visit_count, "
+ "h.last_visit_date, f.url, null, null, null, null, ") +
+ tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "null, null, null "
+ "FROM moz_places h "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ // WHERE 1 is a no-op since additonal conditions will start with AND.
+ "WHERE 1 "
+ "{QUERY_OPTIONS_VISITS} {QUERY_OPTIONS_PLACES} "
+ "{ADDITIONAL_CONDITIONS} ");
+ break;
+
+ case nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS:
+ if (mResultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS) {
+ // Order-by clause is hardcoded because we need to discard duplicates
+ // in FilterResultSet. We will retain only the last modified item,
+ // so we are ordering by place id and last modified to do a faster
+ // filtering.
+ mSkipOrderBy = true;
+
+ GetTagsSqlFragment(history->GetTagsFolder(),
+ NS_LITERAL_CSTRING("b2.fk"),
+ mHasSearchTerms,
+ tagsSqlFragment);
+
+ mQueryString = NS_LITERAL_CSTRING(
+ "SELECT b2.fk, h.url, COALESCE(b2.title, h.title) AS page_title, "
+ "h.rev_host, h.visit_count, h.last_visit_date, f.url, b2.id, "
+ "b2.dateAdded, b2.lastModified, b2.parent, ") +
+ tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "null, null, null, b2.guid, b2.position, b2.type, b2.fk "
+ "FROM moz_bookmarks b2 "
+ "JOIN (SELECT b.fk "
+ "FROM moz_bookmarks b "
+ // ADDITIONAL_CONDITIONS will filter on parent.
+ "WHERE b.type = 1 {ADDITIONAL_CONDITIONS} "
+ ") AS seed ON b2.fk = seed.fk "
+ "JOIN moz_places h ON h.id = b2.fk "
+ "LEFT OUTER JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE NOT EXISTS ( "
+ "SELECT id FROM moz_bookmarks WHERE id = b2.parent AND parent = ") +
+ nsPrintfCString("%lld", history->GetTagsFolder()) +
+ NS_LITERAL_CSTRING(") "
+ "ORDER BY b2.fk DESC, b2.lastModified DESC");
+ }
+ else {
+ GetTagsSqlFragment(history->GetTagsFolder(),
+ NS_LITERAL_CSTRING("b.fk"),
+ mHasSearchTerms,
+ tagsSqlFragment);
+ mQueryString = NS_LITERAL_CSTRING(
+ "SELECT b.fk, h.url, COALESCE(b.title, h.title) AS page_title, "
+ "h.rev_host, h.visit_count, h.last_visit_date, f.url, b.id, "
+ "b.dateAdded, b.lastModified, b.parent, ") +
+ tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid,"
+ "null, null, null, b.guid, b.position, b.type, b.fk "
+ "FROM moz_bookmarks b "
+ "JOIN moz_places h ON b.fk = h.id "
+ "LEFT OUTER JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE NOT EXISTS "
+ "(SELECT id FROM moz_bookmarks "
+ "WHERE id = b.parent AND parent = ") +
+ nsPrintfCString("%lld", history->GetTagsFolder()) +
+ NS_LITERAL_CSTRING(") "
+ "{ADDITIONAL_CONDITIONS}");
+ }
+ break;
+
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::SelectAsVisit()
+{
+ nsNavHistory *history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ nsAutoCString tagsSqlFragment;
+ GetTagsSqlFragment(history->GetTagsFolder(),
+ NS_LITERAL_CSTRING("h.id"),
+ mHasSearchTerms,
+ tagsSqlFragment);
+ mQueryString = NS_LITERAL_CSTRING(
+ "SELECT h.id, h.url, h.title AS page_title, h.rev_host, h.visit_count, "
+ "v.visit_date, f.url, null, null, null, null, ") +
+ tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "v.id, v.from_visit, v.visit_type "
+ "FROM moz_places h "
+ "JOIN moz_historyvisits v ON h.id = v.place_id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ // WHERE 1 is a no-op since additonal conditions will start with AND.
+ "WHERE 1 "
+ "{QUERY_OPTIONS_VISITS} {QUERY_OPTIONS_PLACES} "
+ "{ADDITIONAL_CONDITIONS} ");
+
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::SelectAsDay()
+{
+ mSkipOrderBy = true;
+
+ // Sort child queries based on sorting mode if it's provided, otherwise
+ // fallback to default sort by title ascending.
+ uint16_t sortingMode = nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING;
+ if (mSortingMode != nsINavHistoryQueryOptions::SORT_BY_NONE &&
+ mResultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY)
+ sortingMode = mSortingMode;
+
+ uint16_t resultType =
+ mResultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY ?
+ (uint16_t)nsINavHistoryQueryOptions::RESULTS_AS_URI :
+ (uint16_t)nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY;
+
+ // beginTime will become the node's time property, we don't use endTime
+ // because it could overlap, and we use time to sort containers and find
+ // insert position in a result.
+ mQueryString = nsPrintfCString(
+ "SELECT null, "
+ "'place:type=%ld&sort=%ld&beginTime='||beginTime||'&endTime='||endTime, "
+ "dayTitle, null, null, beginTime, null, null, null, null, null, null, "
+ "null, null, null "
+ "FROM (", // TOUTER BEGIN
+ resultType,
+ sortingMode);
+
+ nsNavHistory *history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(history);
+
+ int32_t daysOfHistory = history->GetDaysOfHistory();
+ for (int32_t i = 0; i <= HISTORY_DATE_CONT_NUM(daysOfHistory); i++) {
+ nsAutoCString dateName;
+ // Timeframes are calculated as BeginTime <= container < EndTime.
+ // Notice times can't be relative to now, since to recognize a query we
+ // must ensure it won't change based on the time it is built.
+ // So, to select till now, we really select till start of tomorrow, that is
+ // a fixed timestamp.
+ // These are used as limits for the inside containers.
+ nsAutoCString sqlFragmentContainerBeginTime, sqlFragmentContainerEndTime;
+ // These are used to query if the container should be visible.
+ nsAutoCString sqlFragmentSearchBeginTime, sqlFragmentSearchEndTime;
+ switch(i) {
+ case 0:
+ // Today
+ history->GetStringFromName(
+ u"finduri-AgeInDays-is-0", dateName);
+ // From start of today
+ sqlFragmentContainerBeginTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','utc')*1000000)");
+ // To now (tomorrow)
+ sqlFragmentContainerEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','+1 day','utc')*1000000)");
+ // Search for the same timeframe.
+ sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
+ sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
+ break;
+ case 1:
+ // Yesterday
+ history->GetStringFromName(
+ u"finduri-AgeInDays-is-1", dateName);
+ // From start of yesterday
+ sqlFragmentContainerBeginTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','-1 day','utc')*1000000)");
+ // To start of today
+ sqlFragmentContainerEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','utc')*1000000)");
+ // Search for the same timeframe.
+ sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
+ sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
+ break;
+ case 2:
+ // Last 7 days
+ history->GetAgeInDaysString(7,
+ u"finduri-AgeInDays-last-is", dateName);
+ // From start of 7 days ago
+ sqlFragmentContainerBeginTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','-7 days','utc')*1000000)");
+ // To now (tomorrow)
+ sqlFragmentContainerEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','+1 day','utc')*1000000)");
+ // This is an overlapped container, but we show it only if there are
+ // visits older than yesterday.
+ sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
+ sqlFragmentSearchEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','-2 days','utc')*1000000)");
+ break;
+ case 3:
+ // This month
+ history->GetStringFromName(
+ u"finduri-AgeInMonths-is-0", dateName);
+ // From start of this month
+ sqlFragmentContainerBeginTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of month','utc')*1000000)");
+ // To now (tomorrow)
+ sqlFragmentContainerEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','+1 day','utc')*1000000)");
+ // This is an overlapped container, but we show it only if there are
+ // visits older than 7 days ago.
+ sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
+ sqlFragmentSearchEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of day','-7 days','utc')*1000000)");
+ break;
+ default:
+ if (i == HISTORY_ADDITIONAL_DATE_CONT_NUM + 6) {
+ // Older than 6 months
+ history->GetAgeInDaysString(6,
+ u"finduri-AgeInMonths-isgreater", dateName);
+ // From start of epoch
+ sqlFragmentContainerBeginTime = NS_LITERAL_CSTRING(
+ "(datetime(0, 'unixepoch')*1000000)");
+ // To start of 6 months ago ( 5 months + this month).
+ sqlFragmentContainerEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of month','-5 months','utc')*1000000)");
+ // Search for the same timeframe.
+ sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
+ sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
+ break;
+ }
+ int32_t MonthIndex = i - HISTORY_ADDITIONAL_DATE_CONT_NUM;
+ // Previous months' titles are month's name if inside this year,
+ // month's name and year for previous years.
+ PRExplodedTime tm;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &tm);
+ uint16_t currentYear = tm.tm_year;
+ // Set day before month, setting month without day could cause issues.
+ // For example setting month to February when today is 30, since
+ // February has not 30 days, will return March instead.
+ // Also, we use day 2 instead of day 1, so that the GMT month is always
+ // the same as the local month. (Bug 603002)
+ tm.tm_mday = 2;
+ tm.tm_month -= MonthIndex;
+ // Notice we use GMTParameters because we just want to get the first
+ // day of each month. Using LocalTimeParameters would instead force us
+ // to apply a DST correction that we don't really need here.
+ PR_NormalizeTime(&tm, PR_GMTParameters);
+ // If the container is for a past year, add the year to its title,
+ // otherwise just show the month name.
+ // Note that tm_month starts from 0, while we need a 1-based index.
+ if (tm.tm_year < currentYear) {
+ history->GetMonthYear(tm.tm_month + 1, tm.tm_year, dateName);
+ }
+ else {
+ history->GetMonthName(tm.tm_month + 1, dateName);
+ }
+
+ // From start of MonthIndex + 1 months ago
+ sqlFragmentContainerBeginTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of month','-");
+ sqlFragmentContainerBeginTime.AppendInt(MonthIndex);
+ sqlFragmentContainerBeginTime.Append(NS_LITERAL_CSTRING(
+ " months','utc')*1000000)"));
+ // To start of MonthIndex months ago
+ sqlFragmentContainerEndTime = NS_LITERAL_CSTRING(
+ "(strftime('%s','now','localtime','start of month','-");
+ sqlFragmentContainerEndTime.AppendInt(MonthIndex - 1);
+ sqlFragmentContainerEndTime.Append(NS_LITERAL_CSTRING(
+ " months','utc')*1000000)"));
+ // Search for the same timeframe.
+ sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
+ sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
+ break;
+ }
+
+ nsPrintfCString dateParam("dayTitle%d", i);
+ mAddParams.Put(dateParam, dateName);
+
+ nsPrintfCString dayRange(
+ "SELECT :%s AS dayTitle, "
+ "%s AS beginTime, "
+ "%s AS endTime "
+ "WHERE EXISTS ( "
+ "SELECT id FROM moz_historyvisits "
+ "WHERE visit_date >= %s "
+ "AND visit_date < %s "
+ "AND visit_type NOT IN (0,%d,%d) "
+ "{QUERY_OPTIONS_VISITS} "
+ "LIMIT 1 "
+ ") ",
+ dateParam.get(),
+ sqlFragmentContainerBeginTime.get(),
+ sqlFragmentContainerEndTime.get(),
+ sqlFragmentSearchBeginTime.get(),
+ sqlFragmentSearchEndTime.get(),
+ nsINavHistoryService::TRANSITION_EMBED,
+ nsINavHistoryService::TRANSITION_FRAMED_LINK
+ );
+
+ mQueryString.Append(dayRange);
+
+ if (i < HISTORY_DATE_CONT_NUM(daysOfHistory))
+ mQueryString.AppendLiteral(" UNION ALL ");
+ }
+
+ mQueryString.AppendLiteral(") "); // TOUTER END
+
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::SelectAsSite()
+{
+ nsAutoCString localFiles;
+
+ nsNavHistory *history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(history);
+
+ history->GetStringFromName(u"localhost", localFiles);
+ mAddParams.Put(NS_LITERAL_CSTRING("localhost"), localFiles);
+
+ // If there are additional conditions the query has to join on visits too.
+ nsAutoCString visitsJoin;
+ nsAutoCString additionalConditions;
+ nsAutoCString timeConstraints;
+ if (!mConditions.IsEmpty()) {
+ visitsJoin.AssignLiteral("JOIN moz_historyvisits v ON v.place_id = h.id ");
+ additionalConditions.AssignLiteral("{QUERY_OPTIONS_VISITS} "
+ "{QUERY_OPTIONS_PLACES} "
+ "{ADDITIONAL_CONDITIONS} ");
+ timeConstraints.AssignLiteral("||'&beginTime='||:begin_time||"
+ "'&endTime='||:end_time");
+ }
+
+ mQueryString = nsPrintfCString(
+ "SELECT null, 'place:type=%ld&sort=%ld&domain=&domainIsHost=true'%s, "
+ ":localhost, :localhost, null, null, null, null, null, null, null, "
+ "null, null, null "
+ "WHERE EXISTS ( "
+ "SELECT h.id FROM moz_places h "
+ "%s "
+ "WHERE h.hidden = 0 "
+ "AND h.visit_count > 0 "
+ "AND h.url_hash BETWEEN hash('file', 'prefix_lo') AND "
+ "hash('file', 'prefix_hi') "
+ "%s "
+ "LIMIT 1 "
+ ") "
+ "UNION ALL "
+ "SELECT null, "
+ "'place:type=%ld&sort=%ld&domain='||host||'&domainIsHost=true'%s, "
+ "host, host, null, null, null, null, null, null, null, "
+ "null, null, null "
+ "FROM ( "
+ "SELECT get_unreversed_host(h.rev_host) AS host "
+ "FROM moz_places h "
+ "%s "
+ "WHERE h.hidden = 0 "
+ "AND h.rev_host <> '.' "
+ "AND h.visit_count > 0 "
+ "%s "
+ "GROUP BY h.rev_host "
+ "ORDER BY host ASC "
+ ") ",
+ nsINavHistoryQueryOptions::RESULTS_AS_URI,
+ mSortingMode,
+ timeConstraints.get(),
+ visitsJoin.get(),
+ additionalConditions.get(),
+ nsINavHistoryQueryOptions::RESULTS_AS_URI,
+ mSortingMode,
+ timeConstraints.get(),
+ visitsJoin.get(),
+ additionalConditions.get()
+ );
+
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::SelectAsTag()
+{
+ nsNavHistory *history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(history);
+
+ // This allows sorting by date fields what is not possible with
+ // other history queries.
+ mHasDateColumns = true;
+
+ mQueryString = nsPrintfCString(
+ "SELECT null, 'place:folder=' || id || '&queryType=%d&type=%ld', "
+ "title, null, null, null, null, null, dateAdded, "
+ "lastModified, null, null, null, null, null, null "
+ "FROM moz_bookmarks "
+ "WHERE parent = %lld",
+ nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS,
+ nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS,
+ history->GetTagsFolder()
+ );
+
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::Where()
+{
+
+ // Set query options
+ nsAutoCString additionalVisitsConditions;
+ nsAutoCString additionalPlacesConditions;
+
+ if (!mIncludeHidden) {
+ additionalPlacesConditions += NS_LITERAL_CSTRING("AND hidden = 0 ");
+ }
+
+ if (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY) {
+ // last_visit_date is updated for any kind of visit, so it's a good
+ // indicator whether the page has visits.
+ additionalPlacesConditions += NS_LITERAL_CSTRING(
+ "AND last_visit_date NOTNULL "
+ );
+ }
+
+ if (mResultType == nsINavHistoryQueryOptions::RESULTS_AS_URI &&
+ !additionalVisitsConditions.IsEmpty()) {
+ // URI results don't join on visits.
+ nsAutoCString tmp = additionalVisitsConditions;
+ additionalVisitsConditions = "AND EXISTS (SELECT 1 FROM moz_historyvisits WHERE place_id = h.id ";
+ additionalVisitsConditions.Append(tmp);
+ additionalVisitsConditions.AppendLiteral("LIMIT 1)");
+ }
+
+ mQueryString.ReplaceSubstring("{QUERY_OPTIONS_VISITS}",
+ additionalVisitsConditions.get());
+ mQueryString.ReplaceSubstring("{QUERY_OPTIONS_PLACES}",
+ additionalPlacesConditions.get());
+
+ // If we used WHERE already, we inject the conditions
+ // in place of {ADDITIONAL_CONDITIONS}
+ if (mQueryString.Find("{ADDITIONAL_CONDITIONS}", 0) != kNotFound) {
+ nsAutoCString innerCondition;
+ // If we have condition AND it
+ if (!mConditions.IsEmpty()) {
+ innerCondition = " AND (";
+ innerCondition += mConditions;
+ innerCondition += ")";
+ }
+ mQueryString.ReplaceSubstring("{ADDITIONAL_CONDITIONS}",
+ innerCondition.get());
+
+ } else if (!mConditions.IsEmpty()) {
+
+ mQueryString += "WHERE ";
+ mQueryString += mConditions;
+
+ }
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::GroupBy()
+{
+ mQueryString += mGroupBy;
+ return NS_OK;
+}
+
+nsresult
+PlacesSQLQueryBuilder::OrderBy()
+{
+ if (mSkipOrderBy)
+ return NS_OK;
+
+ // Sort clause: we will sort later, but if it comes out of the DB sorted,
+ // our later sort will be basically free. The DB can sort these for free
+ // most of the time anyway, because it has indices over these items.
+ switch(mSortingMode)
+ {
+ case nsINavHistoryQueryOptions::SORT_BY_NONE:
+ // Ensure sorting does not change based on tables status.
+ if (mResultType == nsINavHistoryQueryOptions::RESULTS_AS_URI) {
+ if (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS)
+ mQueryString += NS_LITERAL_CSTRING(" ORDER BY b.id ASC ");
+ else if (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY)
+ mQueryString += NS_LITERAL_CSTRING(" ORDER BY h.id ASC ");
+ }
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING:
+ case nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING:
+ // If the user wants few results, we limit them by date, necessitating
+ // a sort by date here (see the IDL definition for maxResults).
+ // Otherwise we will do actual sorting by title, but since we could need
+ // to special sort for some locale we will repeat a second sorting at the
+ // end in nsNavHistoryResult, that should be faster since the list will be
+ // almost ordered.
+ if (mMaxResults > 0)
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_VisitDate);
+ else if (mSortingMode == nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING)
+ OrderByTextColumnIndexAsc(nsNavHistory::kGetInfoIndex_Title);
+ else
+ OrderByTextColumnIndexDesc(nsNavHistory::kGetInfoIndex_Title);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING:
+ OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_VisitDate);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING:
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_VisitDate);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_URI_ASCENDING:
+ OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_URL);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_URI_DESCENDING:
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_URL);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING:
+ OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_VisitCount);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING:
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_VisitCount);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_ASCENDING:
+ if (mHasDateColumns)
+ OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_ItemDateAdded);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_DESCENDING:
+ if (mHasDateColumns)
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_ItemDateAdded);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_ASCENDING:
+ if (mHasDateColumns)
+ OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_ItemLastModified);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_DESCENDING:
+ if (mHasDateColumns)
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_ItemLastModified);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_TAGS_ASCENDING:
+ case nsINavHistoryQueryOptions::SORT_BY_TAGS_DESCENDING:
+ case nsINavHistoryQueryOptions::SORT_BY_ANNOTATION_ASCENDING:
+ case nsINavHistoryQueryOptions::SORT_BY_ANNOTATION_DESCENDING:
+ break; // Sort later in nsNavHistoryQueryResultNode::FillChildren()
+ case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING:
+ OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_Frecency);
+ break;
+ case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING:
+ OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_Frecency);
+ break;
+ default:
+ NS_NOTREACHED("Invalid sorting mode");
+ }
+ return NS_OK;
+}
+
+void PlacesSQLQueryBuilder::OrderByColumnIndexAsc(int32_t aIndex)
+{
+ mQueryString += nsPrintfCString(" ORDER BY %d ASC", aIndex+1);
+}
+
+void PlacesSQLQueryBuilder::OrderByColumnIndexDesc(int32_t aIndex)
+{
+ mQueryString += nsPrintfCString(" ORDER BY %d DESC", aIndex+1);
+}
+
+void PlacesSQLQueryBuilder::OrderByTextColumnIndexAsc(int32_t aIndex)
+{
+ mQueryString += nsPrintfCString(" ORDER BY %d COLLATE NOCASE ASC",
+ aIndex+1);
+}
+
+void PlacesSQLQueryBuilder::OrderByTextColumnIndexDesc(int32_t aIndex)
+{
+ mQueryString += nsPrintfCString(" ORDER BY %d COLLATE NOCASE DESC",
+ aIndex+1);
+}
+
+nsresult
+PlacesSQLQueryBuilder::Limit()
+{
+ if (mUseLimit && mMaxResults > 0) {
+ mQueryString += NS_LITERAL_CSTRING(" LIMIT ");
+ mQueryString.AppendInt(mMaxResults);
+ mQueryString.Append(' ');
+ }
+ return NS_OK;
+}
+
+nsresult
+nsNavHistory::ConstructQueryString(
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCString& queryString,
+ bool& aParamsPresent,
+ nsNavHistory::StringHash& aAddParams)
+{
+ // For information about visit_type see nsINavHistoryService.idl.
+ // visitType == 0 is undefined (see bug #375777 for details).
+ // Some sites, especially Javascript-heavy ones, load things in frames to
+ // display them, resulting in a lot of these entries. This is the reason
+ // why such visits are filtered out.
+ nsresult rv;
+ aParamsPresent = false;
+
+ int32_t sortingMode = aOptions->SortingMode();
+ NS_ASSERTION(sortingMode >= nsINavHistoryQueryOptions::SORT_BY_NONE &&
+ sortingMode <= nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING,
+ "Invalid sortingMode found while building query!");
+
+ bool hasSearchTerms = false;
+ for (int32_t i = 0; i < aQueries.Count() && !hasSearchTerms; i++) {
+ aQueries[i]->GetHasSearchTerms(&hasSearchTerms);
+ }
+
+ nsAutoCString tagsSqlFragment;
+ GetTagsSqlFragment(GetTagsFolder(),
+ NS_LITERAL_CSTRING("h.id"),
+ hasSearchTerms,
+ tagsSqlFragment);
+
+ if (IsOptimizableHistoryQuery(aQueries, aOptions,
+ nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING) ||
+ IsOptimizableHistoryQuery(aQueries, aOptions,
+ nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING)) {
+ // Generate an optimized query for the history menu and most visited
+ // smart bookmark.
+ queryString = NS_LITERAL_CSTRING(
+ "SELECT h.id, h.url, h.title AS page_title, h.rev_host, h.visit_count, h.last_visit_date, "
+ "f.url, null, null, null, null, ") +
+ tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "null, null, null "
+ "FROM moz_places h "
+ "LEFT OUTER JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE h.hidden = 0 "
+ "AND EXISTS (SELECT id FROM moz_historyvisits WHERE place_id = h.id "
+ "AND visit_type NOT IN ") +
+ nsPrintfCString("(0,%d,%d) ",
+ nsINavHistoryService::TRANSITION_EMBED,
+ nsINavHistoryService::TRANSITION_FRAMED_LINK) +
+ NS_LITERAL_CSTRING("LIMIT 1) "
+ "{QUERY_OPTIONS} "
+ );
+
+ queryString.AppendLiteral("ORDER BY ");
+ if (sortingMode == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING)
+ queryString.AppendLiteral("last_visit_date DESC ");
+ else
+ queryString.AppendLiteral("visit_count DESC ");
+
+ queryString.AppendLiteral("LIMIT ");
+ queryString.AppendInt(aOptions->MaxResults());
+
+ nsAutoCString additionalQueryOptions;
+
+ queryString.ReplaceSubstring("{QUERY_OPTIONS}",
+ additionalQueryOptions.get());
+ return NS_OK;
+ }
+
+ nsAutoCString conditions;
+ for (int32_t i = 0; i < aQueries.Count(); i++) {
+ nsCString queryClause;
+ rv = QueryToSelectClause(aQueries[i], aOptions, i, &queryClause);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (! queryClause.IsEmpty()) {
+ aParamsPresent = true;
+ if (! conditions.IsEmpty()) // exists previous clause: multiple ones are ORed
+ conditions += NS_LITERAL_CSTRING(" OR ");
+ conditions += NS_LITERAL_CSTRING("(") + queryClause +
+ NS_LITERAL_CSTRING(")");
+ }
+ }
+
+ // Determine whether we can push maxResults constraints into the queries
+ // as LIMIT, or if we need to do result count clamping later
+ // using FilterResultSet()
+ bool useLimitClause = !NeedToFilterResultSet(aQueries, aOptions);
+
+ PlacesSQLQueryBuilder queryStringBuilder(conditions, aOptions,
+ useLimitClause, aAddParams,
+ hasSearchTerms);
+ rv = queryStringBuilder.GetQueryString(queryString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// nsNavHistory::GetQueryResults
+//
+// Call this to get the results from a complex query. This is used by
+// nsNavHistoryQueryResultNode to populate its children. For simple bookmark
+// queries, use nsNavBookmarks::QueryFolderChildren.
+//
+// THIS DOES NOT DO SORTING. You will need to sort the container yourself
+// when you get the results. This is because sorting depends on tree
+// statistics that will be built from the perspective of the tree. See
+// nsNavHistoryQueryResultNode::FillChildren
+//
+// FIXME: This only does keyword searching for the first query, and does
+// it ANDed with the all the rest of the queries.
+
+nsresult
+nsNavHistory::GetQueryResults(nsNavHistoryQueryResultNode *aResultNode,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions *aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aResults)
+{
+ NS_ENSURE_ARG_POINTER(aOptions);
+ NS_ASSERTION(aResults->Count() == 0, "Initial result array must be empty");
+ if (! aQueries.Count())
+ return NS_ERROR_INVALID_ARG;
+
+ nsCString queryString;
+ bool paramsPresent = false;
+ nsNavHistory::StringHash addParams(HISTORY_DATE_CONT_LENGTH);
+ nsresult rv = ConstructQueryString(aQueries, aOptions, queryString,
+ paramsPresent, addParams);
+ NS_ENSURE_SUCCESS(rv,rv);
+
+ // create statement
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(queryString);
+#ifdef DEBUG
+ if (!statement) {
+ nsAutoCString lastErrorString;
+ (void)mDB->MainConn()->GetLastErrorString(lastErrorString);
+ int32_t lastError = 0;
+ (void)mDB->MainConn()->GetLastError(&lastError);
+ printf("Places failed to create a statement from this query:\n%s\nStorage error (%d): %s\n",
+ queryString.get(), lastError, lastErrorString.get());
+ }
+#endif
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ if (paramsPresent) {
+ // bind parameters
+ int32_t i;
+ for (i = 0; i < aQueries.Count(); i++) {
+ rv = BindQueryClauseParameters(statement, i, aQueries[i], aOptions);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ for (auto iter = addParams.Iter(); !iter.Done(); iter.Next()) {
+ nsresult rv = statement->BindUTF8StringByName(iter.Key(), iter.Data());
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+
+ // Optimize the case where there is no need for any post-query filtering.
+ if (NeedToFilterResultSet(aQueries, aOptions)) {
+ // Generate the top-level results.
+ nsCOMArray<nsNavHistoryResultNode> toplevel;
+ rv = ResultsAsList(statement, aOptions, &toplevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ FilterResultSet(aResultNode, toplevel, aResults, aQueries, aOptions);
+ } else {
+ rv = ResultsAsList(statement, aOptions, aResults);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistory::AddObserver(nsINavHistoryObserver* aObserver, bool aOwnsWeak)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aObserver);
+
+ if (NS_WARN_IF(!mCanNotify))
+ return NS_ERROR_UNEXPECTED;
+
+ return mObservers.AppendWeakElement(aObserver, aOwnsWeak);
+}
+
+NS_IMETHODIMP
+nsNavHistory::RemoveObserver(nsINavHistoryObserver* aObserver)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aObserver);
+
+ return mObservers.RemoveWeakElement(aObserver);
+}
+
+NS_IMETHODIMP
+nsNavHistory::GetObservers(uint32_t* _count,
+ nsINavHistoryObserver*** _observers)
+{
+ NS_ENSURE_ARG_POINTER(_count);
+ NS_ENSURE_ARG_POINTER(_observers);
+
+ *_count = 0;
+ *_observers = nullptr;
+
+ // Clear any cached value, cause it's very likely the consumer has made
+ // changes to history and is now trying to notify them.
+ mDaysOfHistory = -1;
+
+ if (!mCanNotify)
+ return NS_OK;
+
+ nsCOMArray<nsINavHistoryObserver> observers;
+
+ // First add the category cache observers.
+ mCacheObservers.GetEntries(observers);
+
+ // Then add the other observers.
+ for (uint32_t i = 0; i < mObservers.Length(); ++i) {
+ const nsCOMPtr<nsINavHistoryObserver> &observer = mObservers.ElementAt(i).GetValue();
+ // Skip nullified weak observers.
+ if (observer)
+ observers.AppendElement(observer);
+ }
+
+ if (observers.Count() == 0)
+ return NS_OK;
+
+ *_count = observers.Count();
+ observers.Forget(_observers);
+
+ return NS_OK;
+}
+
+// See RunInBatchMode
+nsresult
+nsNavHistory::BeginUpdateBatch()
+{
+ if (mBatchLevel++ == 0) {
+ mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false,
+ mozIStorageConnection::TRANSACTION_DEFERRED,
+ true);
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver, OnBeginUpdateBatch());
+ }
+ return NS_OK;
+}
+
+// nsNavHistory::EndUpdateBatch
+nsresult
+nsNavHistory::EndUpdateBatch()
+{
+ if (--mBatchLevel == 0) {
+ if (mBatchDBTransaction) {
+ DebugOnly<nsresult> rv = mBatchDBTransaction->Commit();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Batch failed to commit transaction");
+ delete mBatchDBTransaction;
+ mBatchDBTransaction = nullptr;
+ }
+
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver, OnEndUpdateBatch());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistory::RunInBatchMode(nsINavHistoryBatchCallback* aCallback,
+ nsISupports* aUserData)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aCallback);
+
+ UpdateBatchScoper batch(*this);
+ return aCallback->RunBatched(aUserData);
+}
+
+NS_IMETHODIMP
+nsNavHistory::GetHistoryDisabled(bool *_retval)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = IsHistoryDisabled();
+ return NS_OK;
+}
+
+// Browser history *************************************************************
+
+
+// nsNavHistory::RemovePagesInternal
+//
+// Deletes a list of placeIds from history.
+// This is an internal method used by RemovePages, RemovePagesFromHost and
+// RemovePagesByTimeframe.
+// Takes a comma separated list of place ids.
+// This method does not do any observer notification.
+
+nsresult
+nsNavHistory::RemovePagesInternal(const nsCString& aPlaceIdsQueryString)
+{
+ // Return early if there is nothing to delete.
+ if (aPlaceIdsQueryString.IsEmpty())
+ return NS_OK;
+
+ mozStorageTransaction transaction(mDB->MainConn(), false,
+ mozIStorageConnection::TRANSACTION_DEFERRED,
+ true);
+
+ // Delete all visits for the specified place ids.
+ nsresult rv = mDB->MainConn()->ExecuteSimpleSQL(
+ NS_LITERAL_CSTRING(
+ "DELETE FROM moz_historyvisits WHERE place_id IN (") +
+ aPlaceIdsQueryString +
+ NS_LITERAL_CSTRING(")")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CleanupPlacesOnVisitsDelete(aPlaceIdsQueryString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Invalidate the cached value for whether there's history or not.
+ mDaysOfHistory = -1;
+
+ return transaction.Commit();
+}
+
+
+/**
+ * Performs cleanup on places that just had all their visits removed, including
+ * deletion of those places. This is an internal method used by
+ * RemovePagesInternal. This method does not execute in a transaction, so
+ * callers should make sure they begin one if needed.
+ *
+ * @param aPlaceIdsQueryString
+ * A comma-separated list of place IDs, each of which just had all its
+ * visits removed
+ */
+nsresult
+nsNavHistory::CleanupPlacesOnVisitsDelete(const nsCString& aPlaceIdsQueryString)
+{
+ // Return early if there is nothing to delete.
+ if (aPlaceIdsQueryString.IsEmpty())
+ return NS_OK;
+
+ // Collect about-to-be-deleted URIs to notify onDeleteURI.
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT h.id, h.url, h.guid, "
+ "(SUBSTR(h.url, 1, 6) <> 'place:' "
+ " AND NOT EXISTS (SELECT b.id FROM moz_bookmarks b "
+ "WHERE b.fk = h.id LIMIT 1)) as whole_entry "
+ "FROM moz_places h "
+ "WHERE h.id IN ( ") + aPlaceIdsQueryString + NS_LITERAL_CSTRING(")")
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsCString filteredPlaceIds;
+ nsCOMArray<nsIURI> URIs;
+ nsTArray<nsCString> GUIDs;
+ bool hasMore;
+ while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+ int64_t placeId;
+ nsresult rv = stmt->GetInt64(0, &placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString URLString;
+ rv = stmt->GetUTF8String(1, URLString);
+ nsCString guid;
+ rv = stmt->GetUTF8String(2, guid);
+ int32_t wholeEntry;
+ rv = stmt->GetInt32(3, &wholeEntry);
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), URLString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (wholeEntry) {
+ if (!filteredPlaceIds.IsEmpty()) {
+ filteredPlaceIds.Append(',');
+ }
+ filteredPlaceIds.AppendInt(placeId);
+ URIs.AppendElement(uri.forget());
+ GUIDs.AppendElement(guid);
+ }
+ else {
+ // Notify that we will delete all visits for this page, but not the page
+ // itself, since it's bookmarked or a place: query.
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnDeleteVisits(uri, 0, guid, nsINavHistoryObserver::REASON_DELETED, 0));
+ }
+ }
+
+ // if the entry is not bookmarked and is not a place: uri
+ // then we can remove it from moz_places.
+ // Note that we do NOT delete favicons. Any unreferenced favicons will be
+ // deleted next time the browser is shut down.
+ nsresult rv = mDB->MainConn()->ExecuteSimpleSQL(
+ NS_LITERAL_CSTRING(
+ "DELETE FROM moz_places WHERE id IN ( "
+ ) + filteredPlaceIds + NS_LITERAL_CSTRING(
+ ") "
+ )
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Hosts accumulated during the places delete are updated through a trigger
+ // (see nsPlacesTriggers.h).
+ rv = mDB->MainConn()->ExecuteSimpleSQL(
+ NS_LITERAL_CSTRING("DELETE FROM moz_updatehosts_temp")
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Invalidate frecencies of touched places, since they need recalculation.
+ rv = invalidateFrecencies(aPlaceIdsQueryString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Finally notify about the removed URIs.
+ for (int32_t i = 0; i < URIs.Count(); ++i) {
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnDeleteURI(URIs[i], GUIDs[i], nsINavHistoryObserver::REASON_DELETED));
+ }
+
+ return NS_OK;
+}
+
+
+// nsNavHistory::RemovePages
+//
+// Removes a bunch of uris from history.
+// Has better performance than RemovePage when deleting a lot of history.
+// We don't do duplicates removal, URIs array should be cleaned-up before.
+
+NS_IMETHODIMP
+nsNavHistory::RemovePages(nsIURI **aURIs, uint32_t aLength)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURIs);
+
+ nsresult rv;
+ // build a list of place ids to delete
+ nsCString deletePlaceIdsQueryString;
+ for (uint32_t i = 0; i < aLength; i++) {
+ int64_t placeId;
+ nsAutoCString guid;
+ if (!aURIs[i])
+ continue;
+ rv = GetIdForPage(aURIs[i], &placeId, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (placeId != 0) {
+ if (!deletePlaceIdsQueryString.IsEmpty())
+ deletePlaceIdsQueryString.Append(',');
+ deletePlaceIdsQueryString.AppendInt(placeId);
+ }
+ }
+
+ UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers
+
+ rv = RemovePagesInternal(deletePlaceIdsQueryString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clear the registered embed visits.
+ clearEmbedVisits();
+
+ return NS_OK;
+}
+
+
+// nsNavHistory::RemovePage
+//
+// Removes all visits and the main history entry for the given URI.
+// Silently fails if we have no knowledge of the page.
+
+NS_IMETHODIMP
+nsNavHistory::RemovePage(nsIURI *aURI)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+
+ // Build a list of place ids to delete.
+ int64_t placeId;
+ nsAutoCString guid;
+ nsresult rv = GetIdForPage(aURI, &placeId, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (placeId == 0) {
+ return NS_OK;
+ }
+ nsAutoCString deletePlaceIdQueryString;
+ deletePlaceIdQueryString.AppendInt(placeId);
+
+ rv = RemovePagesInternal(deletePlaceIdQueryString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clear the registered embed visits.
+ clearEmbedVisits();
+
+ return NS_OK;
+}
+
+
+// nsNavHistory::RemovePagesFromHost
+//
+// This function will delete all history information about pages from a
+// given host. If aEntireDomain is set, we will also delete pages from
+// sub hosts (so if we are passed in "microsoft.com" we delete
+// "www.microsoft.com", "msdn.microsoft.com", etc.). An empty host name
+// means local files and anything else with no host name. You can also pass
+// in the localized "(local files)" title given to you from a history query.
+//
+// Silently fails if we have no knowledge of the host.
+//
+// This sends onBeginUpdateBatch/onEndUpdateBatch to observers
+
+NS_IMETHODIMP
+nsNavHistory::RemovePagesFromHost(const nsACString& aHost, bool aEntireDomain)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ nsresult rv;
+ // Local files don't have any host name. We don't want to delete all files in
+ // history when we get passed an empty string, so force to exact match
+ if (aHost.IsEmpty())
+ aEntireDomain = false;
+
+ // translate "(local files)" to an empty host name
+ // be sure to use the TitleForDomain to get the localized name
+ nsCString localFiles;
+ TitleForDomain(EmptyCString(), localFiles);
+ nsAutoString host16;
+ if (!aHost.Equals(localFiles))
+ CopyUTF8toUTF16(aHost, host16);
+
+ // see BindQueryClauseParameters for how this host selection works
+ nsAutoString revHostDot;
+ GetReversedHostname(host16, revHostDot);
+ NS_ASSERTION(revHostDot[revHostDot.Length() - 1] == '.', "Invalid rev. host");
+ nsAutoString revHostSlash(revHostDot);
+ revHostSlash.Truncate(revHostSlash.Length() - 1);
+ revHostSlash.Append('/');
+
+ // build condition string based on host selection type
+ nsAutoCString conditionString;
+ if (aEntireDomain)
+ conditionString.AssignLiteral("rev_host >= ?1 AND rev_host < ?2 ");
+ else
+ conditionString.AssignLiteral("rev_host = ?1 ");
+
+ // create statement depending on delete type
+ nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
+ NS_LITERAL_CSTRING("SELECT id FROM moz_places WHERE ") + conditionString
+ );
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ rv = statement->BindStringByIndex(0, revHostDot);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aEntireDomain) {
+ rv = statement->BindStringByIndex(1, revHostSlash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString hostPlaceIds;
+ bool hasMore = false;
+ while (NS_SUCCEEDED(statement->ExecuteStep(&hasMore)) && hasMore) {
+ if (!hostPlaceIds.IsEmpty())
+ hostPlaceIds.Append(',');
+ int64_t placeId;
+ rv = statement->GetInt64(0, &placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hostPlaceIds.AppendInt(placeId);
+ }
+
+ // force a full refresh calling onEndUpdateBatch (will call Refresh())
+ UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers
+
+ rv = RemovePagesInternal(hostPlaceIds);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clear the registered embed visits.
+ clearEmbedVisits();
+
+ return NS_OK;
+}
+
+
+// nsNavHistory::RemovePagesByTimeframe
+//
+// This function will delete all history information about
+// pages for a given timeframe.
+// Limits are included: aBeginTime <= timeframe <= aEndTime
+//
+// This method sends onBeginUpdateBatch/onEndUpdateBatch to observers
+
+NS_IMETHODIMP
+nsNavHistory::RemovePagesByTimeframe(PRTime aBeginTime, PRTime aEndTime)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ nsresult rv;
+ // build a list of place ids to delete
+ nsCString deletePlaceIdsQueryString;
+
+ // we only need to know if a place has a visit into the given timeframe
+ // this query is faster than actually selecting in moz_historyvisits
+ nsCOMPtr<mozIStorageStatement> selectByTime = mDB->GetStatement(
+ "SELECT h.id FROM moz_places h WHERE "
+ "EXISTS "
+ "(SELECT id FROM moz_historyvisits v WHERE v.place_id = h.id "
+ "AND v.visit_date >= :from_date AND v.visit_date <= :to_date LIMIT 1)"
+ );
+ NS_ENSURE_STATE(selectByTime);
+ mozStorageStatementScoper selectByTimeScoper(selectByTime);
+
+ rv = selectByTime->BindInt64ByName(NS_LITERAL_CSTRING("from_date"), aBeginTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = selectByTime->BindInt64ByName(NS_LITERAL_CSTRING("to_date"), aEndTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(selectByTime->ExecuteStep(&hasMore)) && hasMore) {
+ int64_t placeId;
+ rv = selectByTime->GetInt64(0, &placeId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (placeId != 0) {
+ if (!deletePlaceIdsQueryString.IsEmpty())
+ deletePlaceIdsQueryString.Append(',');
+ deletePlaceIdsQueryString.AppendInt(placeId);
+ }
+ }
+
+ // force a full refresh calling onEndUpdateBatch (will call Refresh())
+ UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers
+
+ rv = RemovePagesInternal(deletePlaceIdsQueryString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clear the registered embed visits.
+ clearEmbedVisits();
+
+ return NS_OK;
+}
+
+
+// Call this method before visiting a URL in order to help determine the
+// transition type of the visit.
+//
+// @see MarkPageAsFollowedBookmark
+
+NS_IMETHODIMP
+nsNavHistory::MarkPageAsTyped(nsIURI *aURI)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+
+ // don't add when history is disabled
+ if (IsHistoryDisabled())
+ return NS_OK;
+
+ nsAutoCString uriString;
+ nsresult rv = aURI->GetSpec(uriString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if URL is already in the typed queue, then we need to remove the old one
+ int64_t unusedEventTime;
+ if (mRecentTyped.Get(uriString, &unusedEventTime))
+ mRecentTyped.Remove(uriString);
+
+ if (mRecentTyped.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
+ ExpireNonrecentEvents(&mRecentTyped);
+
+ mRecentTyped.Put(uriString, GetNow());
+ return NS_OK;
+}
+
+
+// Call this method before visiting a URL in order to help determine the
+// transition type of the visit.
+//
+// @see MarkPageAsTyped
+
+NS_IMETHODIMP
+nsNavHistory::MarkPageAsFollowedLink(nsIURI *aURI)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+
+ // don't add when history is disabled
+ if (IsHistoryDisabled())
+ return NS_OK;
+
+ nsAutoCString uriString;
+ nsresult rv = aURI->GetSpec(uriString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if URL is already in the links queue, then we need to remove the old one
+ int64_t unusedEventTime;
+ if (mRecentLink.Get(uriString, &unusedEventTime))
+ mRecentLink.Remove(uriString);
+
+ if (mRecentLink.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
+ ExpireNonrecentEvents(&mRecentLink);
+
+ mRecentLink.Put(uriString, GetNow());
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistory::GetPageTitle(nsIURI* aURI, nsAString& aTitle)
+{
+ PLACES_WARN_DEPRECATED();
+
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+
+ aTitle.Truncate(0);
+
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+ "SELECT id, url, title, rev_host, visit_count, guid "
+ "FROM moz_places "
+ "WHERE url_hash = hash(:page_url) AND url = :page_url "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResults = false;
+ rv = stmt->ExecuteStep(&hasResults);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!hasResults) {
+ aTitle.SetIsVoid(true);
+ return NS_OK; // Not found, return a void string.
+ }
+
+ rv = stmt->GetString(2, aTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// mozIStorageVacuumParticipant
+
+NS_IMETHODIMP
+nsNavHistory::GetDatabaseConnection(mozIStorageConnection** _DBConnection)
+{
+ return GetDBConnection(_DBConnection);
+}
+
+
+NS_IMETHODIMP
+nsNavHistory::GetExpectedDatabasePageSize(int32_t* _expectedPageSize)
+{
+ NS_ENSURE_STATE(mDB);
+ NS_ENSURE_STATE(mDB->MainConn());
+ return mDB->MainConn()->GetDefaultPageSize(_expectedPageSize);
+}
+
+
+NS_IMETHODIMP
+nsNavHistory::OnBeginVacuum(bool* _vacuumGranted)
+{
+ // TODO: Check if we have to deny the vacuum in some heavy-load case.
+ // We could maybe want to do that during batches?
+ *_vacuumGranted = true;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistory::OnEndVacuum(bool aSucceeded)
+{
+ NS_WARNING_ASSERTION(aSucceeded, "Places.sqlite vacuum failed.");
+ return NS_OK;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsPIPlacesDatabase
+
+NS_IMETHODIMP
+nsNavHistory::GetDBConnection(mozIStorageConnection **_DBConnection)
+{
+ NS_ENSURE_ARG_POINTER(_DBConnection);
+ RefPtr<mozIStorageConnection> connection = mDB->MainConn();
+ connection.forget(_DBConnection);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistory::GetShutdownClient(nsIAsyncShutdownClient **_shutdownClient)
+{
+ NS_ENSURE_ARG_POINTER(_shutdownClient);
+ RefPtr<nsIAsyncShutdownClient> client = mDB->GetClientsShutdown();
+ MOZ_ASSERT(client);
+ client.forget(_shutdownClient);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistory::AsyncExecuteLegacyQueries(nsINavHistoryQuery** aQueries,
+ uint32_t aQueryCount,
+ nsINavHistoryQueryOptions* aOptions,
+ mozIStorageStatementCallback* aCallback,
+ mozIStoragePendingStatement** _stmt)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aQueries);
+ NS_ENSURE_ARG(aOptions);
+ NS_ENSURE_ARG(aCallback);
+ NS_ENSURE_ARG_POINTER(_stmt);
+
+ nsCOMArray<nsNavHistoryQuery> queries;
+ for (uint32_t i = 0; i < aQueryCount; i ++) {
+ nsCOMPtr<nsNavHistoryQuery> query = do_QueryInterface(aQueries[i]);
+ NS_ENSURE_STATE(query);
+ queries.AppendElement(query.forget());
+ }
+ NS_ENSURE_ARG_MIN(queries.Count(), 1);
+
+ nsCOMPtr<nsNavHistoryQueryOptions> options = do_QueryInterface(aOptions);
+ NS_ENSURE_ARG(options);
+
+ nsCString queryString;
+ bool paramsPresent = false;
+ nsNavHistory::StringHash addParams(HISTORY_DATE_CONT_LENGTH);
+ nsresult rv = ConstructQueryString(queries, options, queryString,
+ paramsPresent, addParams);
+ NS_ENSURE_SUCCESS(rv,rv);
+
+ nsCOMPtr<mozIStorageAsyncStatement> statement =
+ mDB->GetAsyncStatement(queryString);
+ NS_ENSURE_STATE(statement);
+
+#ifdef DEBUG
+ if (NS_FAILED(rv)) {
+ nsAutoCString lastErrorString;
+ (void)mDB->MainConn()->GetLastErrorString(lastErrorString);
+ int32_t lastError = 0;
+ (void)mDB->MainConn()->GetLastError(&lastError);
+ printf("Places failed to create a statement from this query:\n%s\nStorage error (%d): %s\n",
+ queryString.get(), lastError, lastErrorString.get());
+ }
+#endif
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (paramsPresent) {
+ // bind parameters
+ int32_t i;
+ for (i = 0; i < queries.Count(); i++) {
+ rv = BindQueryClauseParameters(statement, i, queries[i], options);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ for (auto iter = addParams.Iter(); !iter.Done(); iter.Next()) {
+ nsresult rv = statement->BindUTF8StringByName(iter.Key(), iter.Data());
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+
+ rv = statement->ExecuteAsync(aCallback, _stmt);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+nsresult
+nsNavHistory::NotifyOnPageExpired(nsIURI *aURI, PRTime aVisitTime,
+ bool aWholeEntry, const nsACString& aGUID,
+ uint16_t aReason, uint32_t aTransitionType)
+{
+ // Invalidate the cached value for whether there's history or not.
+ mDaysOfHistory = -1;
+
+ MOZ_ASSERT(!aGUID.IsEmpty());
+ if (aWholeEntry) {
+ // Notify our observers that the page has been removed.
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver, OnDeleteURI(aURI, aGUID, aReason));
+ }
+ else {
+ // Notify our observers that some visits for the page have been removed.
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnDeleteVisits(aURI, aVisitTime, aGUID, aReason,
+ aTransitionType));
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+nsNavHistory::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ if (strcmp(aTopic, TOPIC_PROFILE_TEARDOWN) == 0 ||
+ strcmp(aTopic, TOPIC_PROFILE_CHANGE) == 0 ||
+ strcmp(aTopic, TOPIC_SIMULATE_PLACES_SHUTDOWN) == 0) {
+ // These notifications are used by tests to simulate a Places shutdown.
+ // They should just be forwarded to the Database handle.
+ mDB->Observe(aSubject, aTopic, aData);
+ }
+
+ else if (strcmp(aTopic, TOPIC_PLACES_CONNECTION_CLOSED) == 0) {
+ // Don't even try to notify observers from this point on, the category
+ // cache would init services that could try to use our APIs.
+ mCanNotify = false;
+ mObservers.Clear();
+ }
+
+#ifdef MOZ_XUL
+ else if (strcmp(aTopic, TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING) == 0) {
+ nsCOMPtr<nsIAutoCompleteInput> input = do_QueryInterface(aSubject);
+ if (!input)
+ return NS_OK;
+
+ // If the source is a private window, don't add any input history.
+ bool isPrivate;
+ nsresult rv = input->GetInPrivateContext(&isPrivate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isPrivate)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompletePopup> popup;
+ input->GetPopup(getter_AddRefs(popup));
+ if (!popup)
+ return NS_OK;
+
+ nsCOMPtr<nsIAutoCompleteController> controller;
+ input->GetController(getter_AddRefs(controller));
+ if (!controller)
+ return NS_OK;
+
+ // Don't bother if the popup is closed
+ bool open;
+ rv = popup->GetPopupOpen(&open);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!open)
+ return NS_OK;
+
+ // Ignore if nothing selected from the popup
+ int32_t selectedIndex;
+ rv = popup->GetSelectedIndex(&selectedIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (selectedIndex == -1)
+ return NS_OK;
+
+ rv = AutoCompleteFeedback(selectedIndex, controller);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+#endif
+ else if (strcmp(aTopic, TOPIC_PREF_CHANGED) == 0) {
+ LoadPrefs();
+ }
+
+ else if (strcmp(aTopic, TOPIC_IDLE_DAILY) == 0) {
+ (void)DecayFrecency();
+ }
+
+ return NS_OK;
+}
+
+
+namespace {
+
+class DecayFrecencyCallback : public AsyncStatementTelemetryTimer
+{
+public:
+ DecayFrecencyCallback()
+ : AsyncStatementTelemetryTimer(Telemetry::PLACES_IDLE_FRECENCY_DECAY_TIME_MS)
+ {
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason)
+ {
+ (void)AsyncStatementTelemetryTimer::HandleCompletion(aReason);
+ if (aReason == REASON_FINISHED) {
+ nsNavHistory *navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(navHistory);
+ navHistory->NotifyManyFrecenciesChanged();
+ }
+ return NS_OK;
+ }
+};
+
+} // namespace
+
+nsresult
+nsNavHistory::DecayFrecency()
+{
+ nsresult rv = FixInvalidFrecencies();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Globally decay places frecency rankings to estimate reduced frecency
+ // values of pages that haven't been visited for a while, i.e., they do
+ // not get an updated frecency. A scaling factor of .975 results in .5 the
+ // original value after 28 days.
+ // When changing the scaling factor, ensure that the barrier in
+ // moz_places_afterupdate_frecency_trigger still ignores these changes.
+ nsCOMPtr<mozIStorageAsyncStatement> decayFrecency = mDB->GetAsyncStatement(
+ "UPDATE moz_places SET frecency = ROUND(frecency * .975) "
+ "WHERE frecency > 0"
+ );
+ NS_ENSURE_STATE(decayFrecency);
+
+ // Decay potentially unused adaptive entries (e.g. those that are at 1)
+ // to allow better chances for new entries that will start at 1.
+ nsCOMPtr<mozIStorageAsyncStatement> decayAdaptive = mDB->GetAsyncStatement(
+ "UPDATE moz_inputhistory SET use_count = use_count * .975"
+ );
+ NS_ENSURE_STATE(decayAdaptive);
+
+ // Delete any adaptive entries that won't help in ordering anymore.
+ nsCOMPtr<mozIStorageAsyncStatement> deleteAdaptive = mDB->GetAsyncStatement(
+ "DELETE FROM moz_inputhistory WHERE use_count < .01"
+ );
+ NS_ENSURE_STATE(deleteAdaptive);
+
+ mozIStorageBaseStatement *stmts[] = {
+ decayFrecency.get(),
+ decayAdaptive.get(),
+ deleteAdaptive.get()
+ };
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ RefPtr<DecayFrecencyCallback> cb = new DecayFrecencyCallback();
+ rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), cb,
+ getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+// Query stuff *****************************************************************
+
+// Helper class for QueryToSelectClause
+//
+// This class helps to build part of the WHERE clause. It supports
+// multiple queries by appending the query index to the parameter name.
+// For the query with index 0 the parameter name is not altered what
+// allows using this parameter in other situations (see SelectAsSite).
+
+class ConditionBuilder
+{
+public:
+
+ explicit ConditionBuilder(int32_t aQueryIndex): mQueryIndex(aQueryIndex)
+ { }
+
+ ConditionBuilder& Condition(const char* aStr)
+ {
+ if (!mClause.IsEmpty())
+ mClause.AppendLiteral(" AND ");
+ Str(aStr);
+ return *this;
+ }
+
+ ConditionBuilder& Str(const char* aStr)
+ {
+ mClause.Append(' ');
+ mClause.Append(aStr);
+ mClause.Append(' ');
+ return *this;
+ }
+
+ ConditionBuilder& Param(const char* aParam)
+ {
+ mClause.Append(' ');
+ if (!mQueryIndex)
+ mClause.Append(aParam);
+ else
+ mClause += nsPrintfCString("%s%d", aParam, mQueryIndex);
+
+ mClause.Append(' ');
+ return *this;
+ }
+
+ void GetClauseString(nsCString& aResult)
+ {
+ aResult = mClause;
+ }
+
+private:
+
+ int32_t mQueryIndex;
+ nsCString mClause;
+};
+
+
+// nsNavHistory::QueryToSelectClause
+//
+// THE BEHAVIOR SHOULD BE IN SYNC WITH BindQueryClauseParameters
+//
+// I don't check return values from the query object getters because there's
+// no way for those to fail.
+
+nsresult
+nsNavHistory::QueryToSelectClause(nsNavHistoryQuery* aQuery, // const
+ nsNavHistoryQueryOptions* aOptions,
+ int32_t aQueryIndex,
+ nsCString* aClause)
+{
+ bool hasIt;
+ bool excludeQueries = aOptions->ExcludeQueries();
+
+ ConditionBuilder clause(aQueryIndex);
+
+ if ((NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt) ||
+ (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt)) {
+ clause.Condition("EXISTS (SELECT 1 FROM moz_historyvisits "
+ "WHERE place_id = h.id");
+ // begin time
+ if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt)
+ clause.Condition("visit_date >=").Param(":begin_time");
+ // end time
+ if (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt)
+ clause.Condition("visit_date <=").Param(":end_time");
+ clause.Str(" LIMIT 1)");
+ }
+
+ // search terms
+ bool hasSearchTerms;
+ int32_t searchBehavior = mozIPlacesAutoComplete::BEHAVIOR_HISTORY |
+ mozIPlacesAutoComplete::BEHAVIOR_BOOKMARK;
+ if (NS_SUCCEEDED(aQuery->GetHasSearchTerms(&hasSearchTerms)) && hasSearchTerms) {
+ // Re-use the autocomplete_match function. Setting the behavior to match
+ // history or typed history or bookmarks or open pages will match almost
+ // everything.
+ clause.Condition("AUTOCOMPLETE_MATCH(").Param(":search_string")
+ .Str(", h.url, page_title, tags, ")
+ .Str(nsPrintfCString("1, 1, 1, 1, %d, %d)",
+ mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED,
+ searchBehavior).get());
+ // Serching by terms implicitly exclude queries.
+ excludeQueries = true;
+ }
+
+ // min and max visit count
+ if (aQuery->MinVisits() >= 0)
+ clause.Condition("h.visit_count >=").Param(":min_visits");
+
+ if (aQuery->MaxVisits() >= 0)
+ clause.Condition("h.visit_count <=").Param(":max_visits");
+
+ // only bookmarked, has no affect on bookmarks-only queries
+ if (aOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS &&
+ aQuery->OnlyBookmarked())
+ clause.Condition("EXISTS (SELECT b.fk FROM moz_bookmarks b WHERE b.type = ")
+ .Str(nsPrintfCString("%d", nsNavBookmarks::TYPE_BOOKMARK).get())
+ .Str("AND b.fk = h.id)");
+
+ // domain
+ if (NS_SUCCEEDED(aQuery->GetHasDomain(&hasIt)) && hasIt) {
+ bool domainIsHost = false;
+ aQuery->GetDomainIsHost(&domainIsHost);
+ if (domainIsHost)
+ clause.Condition("h.rev_host =").Param(":domain_lower");
+ else
+ // see domain setting in BindQueryClauseParameters for why we do this
+ clause.Condition("h.rev_host >=").Param(":domain_lower")
+ .Condition("h.rev_host <").Param(":domain_upper");
+ }
+
+ // URI
+ if (NS_SUCCEEDED(aQuery->GetHasUri(&hasIt)) && hasIt) {
+ clause.Condition("h.url_hash = hash(").Param(":uri").Str(")")
+ .Condition("h.url =").Param(":uri");
+ }
+
+ // annotation
+ aQuery->GetHasAnnotation(&hasIt);
+ if (hasIt) {
+ clause.Condition("");
+ if (aQuery->AnnotationIsNot())
+ clause.Str("NOT");
+ clause.Str(
+ "EXISTS "
+ "(SELECT h.id "
+ "FROM moz_annos anno "
+ "JOIN moz_anno_attributes annoname "
+ "ON anno.anno_attribute_id = annoname.id "
+ "WHERE anno.place_id = h.id "
+ "AND annoname.name = ").Param(":anno").Str(")");
+ // annotation-based queries don't get the common conditions, so you get
+ // all URLs with that annotation
+ }
+
+ // tags
+ const nsTArray<nsString> &tags = aQuery->Tags();
+ if (tags.Length() > 0) {
+ clause.Condition("h.id");
+ if (aQuery->TagsAreNot())
+ clause.Str("NOT");
+ clause.Str(
+ "IN "
+ "(SELECT bms.fk "
+ "FROM moz_bookmarks bms "
+ "JOIN moz_bookmarks tags ON bms.parent = tags.id "
+ "WHERE tags.parent =").
+ Param(":tags_folder").
+ Str("AND tags.title IN (");
+ for (uint32_t i = 0; i < tags.Length(); ++i) {
+ nsPrintfCString param(":tag%d_", i);
+ clause.Param(param.get());
+ if (i < tags.Length() - 1)
+ clause.Str(",");
+ }
+ clause.Str(")");
+ if (!aQuery->TagsAreNot())
+ clause.Str("GROUP BY bms.fk HAVING count(*) >=").Param(":tag_count");
+ clause.Str(")");
+ }
+
+ // transitions
+ const nsTArray<uint32_t>& transitions = aQuery->Transitions();
+ for (uint32_t i = 0; i < transitions.Length(); ++i) {
+ nsPrintfCString param(":transition%d_", i);
+ clause.Condition("h.id IN (SELECT place_id FROM moz_historyvisits "
+ "WHERE visit_type = ")
+ .Param(param.get())
+ .Str(")");
+ }
+
+ // folders
+ const nsTArray<int64_t>& folders = aQuery->Folders();
+ if (folders.Length() > 0) {
+ aOptions->SetQueryType(nsNavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS);
+
+ nsTArray<int64_t> includeFolders;
+ includeFolders.AppendElements(folders);
+
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_STATE(bookmarks);
+
+ for (nsTArray<int64_t>::size_type i = 0; i < folders.Length(); ++i) {
+ nsTArray<int64_t> subFolders;
+ if (NS_FAILED(bookmarks->GetDescendantFolders(folders[i], subFolders)))
+ continue;
+ includeFolders.AppendElements(subFolders);
+ }
+
+ clause.Condition("b.parent IN(");
+ for (nsTArray<int64_t>::size_type i = 0; i < includeFolders.Length(); ++i) {
+ clause.Str(nsPrintfCString("%lld", includeFolders[i]).get());
+ if (i < includeFolders.Length() - 1) {
+ clause.Str(",");
+ }
+ }
+ clause.Str(")");
+ }
+
+ if (excludeQueries) {
+ // Serching by terms implicitly exclude queries.
+ clause.Condition("NOT h.url_hash BETWEEN hash('place', 'prefix_lo') AND "
+ "hash('place', 'prefix_hi')");
+ }
+
+ clause.GetClauseString(*aClause);
+ return NS_OK;
+}
+
+
+// nsNavHistory::BindQueryClauseParameters
+//
+// THE BEHAVIOR SHOULD BE IN SYNC WITH QueryToSelectClause
+
+nsresult
+nsNavHistory::BindQueryClauseParameters(mozIStorageBaseStatement* statement,
+ int32_t aQueryIndex,
+ nsNavHistoryQuery* aQuery, // const
+ nsNavHistoryQueryOptions* aOptions)
+{
+ nsresult rv;
+
+ bool hasIt;
+ // Append numbered index to param names, to replace them correctly in
+ // case of multiple queries. If we have just one query we don't change the
+ // param name though.
+ nsAutoCString qIndex;
+ if (aQueryIndex > 0)
+ qIndex.AppendInt(aQueryIndex);
+
+ // begin time
+ if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt) {
+ PRTime time = NormalizeTime(aQuery->BeginTimeReference(),
+ aQuery->BeginTime());
+ rv = statement->BindInt64ByName(
+ NS_LITERAL_CSTRING("begin_time") + qIndex, time);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // end time
+ if (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt) {
+ PRTime time = NormalizeTime(aQuery->EndTimeReference(),
+ aQuery->EndTime());
+ rv = statement->BindInt64ByName(
+ NS_LITERAL_CSTRING("end_time") + qIndex, time
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // search terms
+ if (NS_SUCCEEDED(aQuery->GetHasSearchTerms(&hasIt)) && hasIt) {
+ rv = statement->BindStringByName(
+ NS_LITERAL_CSTRING("search_string") + qIndex,
+ aQuery->SearchTerms()
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // min and max visit count
+ int32_t visits = aQuery->MinVisits();
+ if (visits >= 0) {
+ rv = statement->BindInt32ByName(
+ NS_LITERAL_CSTRING("min_visits") + qIndex, visits
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ visits = aQuery->MaxVisits();
+ if (visits >= 0) {
+ rv = statement->BindInt32ByName(
+ NS_LITERAL_CSTRING("max_visits") + qIndex, visits
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // domain (see GetReversedHostname for more info on reversed host names)
+ if (NS_SUCCEEDED(aQuery->GetHasDomain(&hasIt)) && hasIt) {
+ nsString revDomain;
+ GetReversedHostname(NS_ConvertUTF8toUTF16(aQuery->Domain()), revDomain);
+
+ if (aQuery->DomainIsHost()) {
+ rv = statement->BindStringByName(
+ NS_LITERAL_CSTRING("domain_lower") + qIndex, revDomain
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // for "mozilla.org" do query >= "gro.allizom." AND < "gro.allizom/"
+ // which will get everything starting with "gro.allizom." while using the
+ // index (using SUBSTRING() causes indexes to be discarded).
+ NS_ASSERTION(revDomain[revDomain.Length() - 1] == '.', "Invalid rev. host");
+ rv = statement->BindStringByName(
+ NS_LITERAL_CSTRING("domain_lower") + qIndex, revDomain
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ revDomain.Truncate(revDomain.Length() - 1);
+ revDomain.Append(char16_t('/'));
+ rv = statement->BindStringByName(
+ NS_LITERAL_CSTRING("domain_upper") + qIndex, revDomain
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // URI
+ if (aQuery->Uri()) {
+ rv = URIBinder::Bind(
+ statement, NS_LITERAL_CSTRING("uri") + qIndex, aQuery->Uri()
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // annotation
+ if (!aQuery->Annotation().IsEmpty()) {
+ rv = statement->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("anno") + qIndex, aQuery->Annotation()
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // tags
+ const nsTArray<nsString> &tags = aQuery->Tags();
+ if (tags.Length() > 0) {
+ for (uint32_t i = 0; i < tags.Length(); ++i) {
+ nsPrintfCString paramName("tag%d_", i);
+ NS_ConvertUTF16toUTF8 tag(tags[i]);
+ rv = statement->BindUTF8StringByName(paramName + qIndex, tag);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ int64_t tagsFolder = GetTagsFolder();
+ rv = statement->BindInt64ByName(
+ NS_LITERAL_CSTRING("tags_folder") + qIndex, tagsFolder
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!aQuery->TagsAreNot()) {
+ rv = statement->BindInt32ByName(
+ NS_LITERAL_CSTRING("tag_count") + qIndex, tags.Length()
+ );
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // transitions
+ const nsTArray<uint32_t>& transitions = aQuery->Transitions();
+ if (transitions.Length() > 0) {
+ for (uint32_t i = 0; i < transitions.Length(); ++i) {
+ nsPrintfCString paramName("transition%d_", i);
+ rv = statement->BindInt64ByName(paramName + qIndex, transitions[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+
+// nsNavHistory::ResultsAsList
+//
+
+nsresult
+nsNavHistory::ResultsAsList(mozIStorageStatement* statement,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aResults)
+{
+ nsresult rv;
+ nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(statement, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(statement->ExecuteStep(&hasMore)) && hasMore) {
+ RefPtr<nsNavHistoryResultNode> result;
+ rv = RowToResult(row, aOptions, getter_AddRefs(result));
+ NS_ENSURE_SUCCESS(rv, rv);
+ aResults->AppendElement(result.forget());
+ }
+ return NS_OK;
+}
+
+const int64_t UNDEFINED_URN_VALUE = -1;
+
+// Create a urn (like
+// urn:places-persist:place:group=0&group=1&sort=1&type=1,,%28local%20files%29)
+// to be used to persist the open state of this container
+nsresult
+CreatePlacesPersistURN(nsNavHistoryQueryResultNode *aResultNode,
+ int64_t aValue, const nsCString& aTitle, nsCString& aURN)
+{
+ nsAutoCString uri;
+ nsresult rv = aResultNode->GetUri(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aURN.AssignLiteral("urn:places-persist:");
+ aURN.Append(uri);
+
+ aURN.Append(',');
+ if (aValue != UNDEFINED_URN_VALUE)
+ aURN.AppendInt(aValue);
+
+ aURN.Append(',');
+ if (!aTitle.IsEmpty()) {
+ nsAutoCString escapedTitle;
+ bool success = NS_Escape(aTitle, escapedTitle, url_XAlphas);
+ NS_ENSURE_TRUE(success, NS_ERROR_OUT_OF_MEMORY);
+ aURN.Append(escapedTitle);
+ }
+
+ return NS_OK;
+}
+
+int64_t
+nsNavHistory::GetTagsFolder()
+{
+ // cache our tags folder
+ // note, we can't do this in nsNavHistory::Init(),
+ // as getting the bookmarks service would initialize it.
+ if (mTagsFolder == -1) {
+ nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, -1);
+
+ nsresult rv = bookmarks->GetTagsFolder(&mTagsFolder);
+ NS_ENSURE_SUCCESS(rv, -1);
+ }
+ return mTagsFolder;
+}
+
+// nsNavHistory::FilterResultSet
+//
+// This does some post-query-execution filtering:
+// - searching on title, url and tags
+// - limit count
+//
+// Note: changes to filtering in FilterResultSet()
+// may require changes to NeedToFilterResultSet()
+
+nsresult
+nsNavHistory::FilterResultSet(nsNavHistoryQueryResultNode* aQueryNode,
+ const nsCOMArray<nsNavHistoryResultNode>& aSet,
+ nsCOMArray<nsNavHistoryResultNode>* aFiltered,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions *aOptions)
+{
+ // get the bookmarks service
+ nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+
+ // parse the search terms
+ nsTArray<nsTArray<nsString>*> terms;
+ ParseSearchTermsFromQueries(aQueries, &terms);
+
+ uint16_t resultType = aOptions->ResultType();
+ for (int32_t nodeIndex = 0; nodeIndex < aSet.Count(); nodeIndex++) {
+ // exclude-queries is implicit when searching, we're only looking at
+ // plan URI nodes
+ if (!aSet[nodeIndex]->IsURI())
+ continue;
+
+ // RESULTS_AS_TAG_CONTENTS returns a set ordered by place_id and
+ // lastModified. So, to remove duplicates, we can retain the first result
+ // for each uri.
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS &&
+ nodeIndex > 0 && aSet[nodeIndex]->mURI == aSet[nodeIndex-1]->mURI)
+ continue;
+
+ if (aSet[nodeIndex]->mItemId != -1 && aQueryNode &&
+ aQueryNode->mItemId == aSet[nodeIndex]->mItemId) {
+ continue;
+ }
+
+ // Append the node only if it matches one of the queries.
+ bool appendNode = false;
+ for (int32_t queryIndex = 0;
+ queryIndex < aQueries.Count() && !appendNode; queryIndex++) {
+
+ if (terms[queryIndex]->Length()) {
+ // Filter based on search terms.
+ // Convert title and url for the current node to UTF16 strings.
+ NS_ConvertUTF8toUTF16 nodeTitle(aSet[nodeIndex]->mTitle);
+ // Unescape the URL for search terms matching.
+ nsAutoCString cNodeURL(aSet[nodeIndex]->mURI);
+ NS_ConvertUTF8toUTF16 nodeURL(NS_UnescapeURL(cNodeURL));
+
+ // Determine if every search term matches anywhere in the title, url or
+ // tag.
+ bool matchAll = true;
+ for (int32_t termIndex = terms[queryIndex]->Length() - 1;
+ termIndex >= 0 && matchAll;
+ termIndex--) {
+ nsString& term = terms[queryIndex]->ElementAt(termIndex);
+
+ // True if any of them match; false makes us quit the loop
+ matchAll = CaseInsensitiveFindInReadable(term, nodeTitle) ||
+ CaseInsensitiveFindInReadable(term, nodeURL) ||
+ CaseInsensitiveFindInReadable(term, aSet[nodeIndex]->mTags);
+ }
+
+ // Skip the node if we don't match all terms in the title, url or tag
+ if (!matchAll)
+ continue;
+ }
+
+ // We passed all filters, so we can append the node to filtered results.
+ appendNode = true;
+ }
+
+ if (appendNode)
+ aFiltered->AppendObject(aSet[nodeIndex]);
+
+ // Stop once we have reached max results.
+ if (aOptions->MaxResults() > 0 &&
+ (uint32_t)aFiltered->Count() >= aOptions->MaxResults())
+ break;
+ }
+
+ // De-allocate the temporary matrixes.
+ for (int32_t i = 0; i < aQueries.Count(); i++) {
+ delete terms[i];
+ }
+
+ return NS_OK;
+}
+
+void
+nsNavHistory::registerEmbedVisit(nsIURI* aURI,
+ int64_t aTime)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ VisitHashKey* visit = mEmbedVisits.PutEntry(aURI);
+ if (!visit) {
+ NS_WARNING("Unable to register a EMBED visit.");
+ return;
+ }
+ visit->visitTime = aTime;
+}
+
+bool
+nsNavHistory::hasEmbedVisit(nsIURI* aURI) {
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ return !!mEmbedVisits.GetEntry(aURI);
+}
+
+void
+nsNavHistory::clearEmbedVisits() {
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+
+ mEmbedVisits.Clear();
+}
+
+NS_IMETHODIMP
+nsNavHistory::ClearEmbedVisits() {
+ clearEmbedVisits();
+ return NS_OK;
+}
+
+// nsNavHistory::CheckIsRecentEvent
+//
+// Sees if this URL happened "recently."
+//
+// It is always removed from our recent list no matter what. It only counts
+// as "recent" if the event happened more recently than our event
+// threshold ago.
+
+bool
+nsNavHistory::CheckIsRecentEvent(RecentEventHash* hashTable,
+ const nsACString& url)
+{
+ PRTime eventTime;
+ if (hashTable->Get(url, reinterpret_cast<int64_t*>(&eventTime))) {
+ hashTable->Remove(url);
+ if (eventTime > GetNow() - RECENT_EVENT_THRESHOLD)
+ return true;
+ return false;
+ }
+ return false;
+}
+
+
+// nsNavHistory::ExpireNonrecentEvents
+//
+// This goes through our
+
+void
+nsNavHistory::ExpireNonrecentEvents(RecentEventHash* hashTable)
+{
+ int64_t threshold = GetNow() - RECENT_EVENT_THRESHOLD;
+ for (auto iter = hashTable->Iter(); !iter.Done(); iter.Next()) {
+ if (iter.Data() < threshold) {
+ iter.Remove();
+ }
+ }
+}
+
+
+// nsNavHistory::RowToResult
+//
+// Here, we just have a generic row. It could be a query, URL, visit,
+// or full visit.
+
+nsresult
+nsNavHistory::RowToResult(mozIStorageValueArray* aRow,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult)
+{
+ NS_ASSERTION(aRow && aOptions && aResult, "Null pointer in RowToResult");
+
+ // URL
+ nsAutoCString url;
+ nsresult rv = aRow->GetUTF8String(kGetInfoIndex_URL, url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // title
+ nsAutoCString title;
+ rv = aRow->GetUTF8String(kGetInfoIndex_Title, title);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t accessCount = aRow->AsInt32(kGetInfoIndex_VisitCount);
+ PRTime time = aRow->AsInt64(kGetInfoIndex_VisitDate);
+
+ // favicon
+ nsAutoCString favicon;
+ rv = aRow->GetUTF8String(kGetInfoIndex_FaviconURL, favicon);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // itemId
+ int64_t itemId = aRow->AsInt64(kGetInfoIndex_ItemId);
+ int64_t parentId = -1;
+ if (itemId == 0) {
+ // This is not a bookmark. For non-bookmarks we use a -1 itemId value.
+ // Notice ids in sqlite tables start from 1, so itemId cannot ever be 0.
+ itemId = -1;
+ }
+ else {
+ // This is a bookmark, so it has a parent.
+ int64_t itemParentId = aRow->AsInt64(kGetInfoIndex_ItemParentId);
+ if (itemParentId > 0) {
+ // The Places root has parent == 0, but that item id does not really
+ // exist. We want to set the parent only if it's a real one.
+ parentId = itemParentId;
+ }
+ }
+
+ if (IsQueryURI(url)) {
+ // Special case "place:" URIs: turn them into containers.
+ if (itemId != -1) {
+ // We should never expose the history title for query nodes if the
+ // bookmark-item's title is set to null (the history title may be the
+ // query string without the place: prefix). Thus we call getItemTitle
+ // explicitly. Doing this in the SQL query would be less performant since
+ // it should be done for all results rather than only for queries.
+ nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+
+ rv = bookmarks->GetItemTitle(itemId, title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsAutoCString guid;
+ if (itemId != -1) {
+ rv = aRow->GetUTF8String(nsNavBookmarks::kGetChildrenIndex_Guid, guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ RefPtr<nsNavHistoryResultNode> resultNode;
+ rv = QueryRowToResult(itemId, guid, url, title, accessCount, time, favicon,
+ getter_AddRefs(resultNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (itemId != -1 ||
+ aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_TAG_QUERY) {
+ // RESULTS_AS_TAG_QUERY has date columns
+ resultNode->mDateAdded = aRow->AsInt64(kGetInfoIndex_ItemDateAdded);
+ resultNode->mLastModified = aRow->AsInt64(kGetInfoIndex_ItemLastModified);
+ if (resultNode->IsFolder()) {
+ // If it's a simple folder node (i.e. a shortcut to another folder), apply
+ // our options for it. However, if the parent type was tag query, we do not
+ // apply them, because it would not yield any results.
+ resultNode->GetAsContainer()->mOptions = aOptions;
+ }
+ }
+
+ resultNode.forget(aResult);
+ return rv;
+ } else if (aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_URI ||
+ aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS) {
+ RefPtr<nsNavHistoryResultNode> resultNode =
+ new nsNavHistoryResultNode(url, title, accessCount, time, favicon);
+
+ if (itemId != -1) {
+ resultNode->mItemId = itemId;
+ resultNode->mFolderId = parentId;
+ resultNode->mDateAdded = aRow->AsInt64(kGetInfoIndex_ItemDateAdded);
+ resultNode->mLastModified = aRow->AsInt64(kGetInfoIndex_ItemLastModified);
+
+ rv = aRow->GetUTF8String(nsNavBookmarks::kGetChildrenIndex_Guid,
+ resultNode->mBookmarkGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ resultNode->mFrecency = aRow->AsInt32(kGetInfoIndex_Frecency);
+ resultNode->mHidden = !!aRow->AsInt32(kGetInfoIndex_Hidden);
+
+ nsAutoString tags;
+ rv = aRow->GetString(kGetInfoIndex_ItemTags, tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!tags.IsVoid()) {
+ resultNode->mTags.Assign(tags);
+ }
+
+ rv = aRow->GetUTF8String(kGetInfoIndex_Guid, resultNode->mPageGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ resultNode.forget(aResult);
+ return NS_OK;
+ }
+
+ if (aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_VISIT) {
+ RefPtr<nsNavHistoryResultNode> resultNode =
+ new nsNavHistoryResultNode(url, title, accessCount, time, favicon);
+
+ nsAutoString tags;
+ rv = aRow->GetString(kGetInfoIndex_ItemTags, tags);
+ if (!tags.IsVoid())
+ resultNode->mTags.Assign(tags);
+
+ rv = aRow->GetUTF8String(kGetInfoIndex_Guid, resultNode->mPageGuid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aRow->GetInt64(kGetInfoIndex_VisitId, &resultNode->mVisitId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t fromVisitId;
+ rv = aRow->GetInt64(kGetInfoIndex_FromVisitId, &fromVisitId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (fromVisitId > 0) {
+ resultNode->mFromVisitId = fromVisitId;
+ }
+
+ resultNode->mTransitionType = aRow->AsInt32(kGetInfoIndex_VisitType);
+
+ resultNode.forget(aResult);
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+
+// nsNavHistory::QueryRowToResult
+//
+// Called by RowToResult when the URI is a place: URI to generate the proper
+// folder or query node.
+
+nsresult
+nsNavHistory::QueryRowToResult(int64_t itemId,
+ const nsACString& aBookmarkGuid,
+ const nsACString& aURI,
+ const nsACString& aTitle,
+ uint32_t aAccessCount, PRTime aTime,
+ const nsACString& aFavicon,
+ nsNavHistoryResultNode** aNode)
+{
+ MOZ_ASSERT((itemId != -1 && !aBookmarkGuid.IsEmpty()) ||
+ (itemId == -1 && aBookmarkGuid.IsEmpty()));
+
+ nsCOMArray<nsNavHistoryQuery> queries;
+ nsCOMPtr<nsNavHistoryQueryOptions> options;
+ nsresult rv = QueryStringToQueryArray(aURI, &queries,
+ getter_AddRefs(options));
+
+ RefPtr<nsNavHistoryResultNode> resultNode;
+ // If this failed the query does not parse correctly, let the error pass and
+ // handle it later.
+ if (NS_SUCCEEDED(rv)) {
+ // Check if this is a folder shortcut, so we can take a faster path.
+ int64_t targetFolderId = GetSimpleBookmarksQueryFolder(queries, options);
+ if (targetFolderId) {
+ nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+
+ rv = bookmarks->ResultNodeForContainer(targetFolderId, options,
+ getter_AddRefs(resultNode));
+ // If this failed the shortcut is pointing to nowhere, let the error pass
+ // and handle it later.
+ if (NS_SUCCEEDED(rv)) {
+ // At this point the node is set up like a regular folder node. Here
+ // we make the necessary change to make it a folder shortcut.
+ resultNode->GetAsFolder()->mTargetFolderItemId = targetFolderId;
+ resultNode->mItemId = itemId;
+ nsAutoCString targetFolderGuid(resultNode->GetAsFolder()->mBookmarkGuid);
+ resultNode->mBookmarkGuid = aBookmarkGuid;
+ resultNode->GetAsFolder()->mTargetFolderGuid = targetFolderGuid;
+
+ // Use the query item title, unless it's void (in that case use the
+ // concrete folder title).
+ if (!aTitle.IsVoid()) {
+ resultNode->mTitle = aTitle;
+ }
+ }
+ }
+ else {
+ // This is a regular query.
+ resultNode = new nsNavHistoryQueryResultNode(aTitle, EmptyCString(),
+ aTime, queries, options);
+ resultNode->mItemId = itemId;
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Generating a generic empty node for a broken query!");
+ // This is a broken query, that either did not parse or points to not
+ // existing data. We don't want to return failure since that will kill the
+ // whole result. Instead make a generic empty query node.
+ resultNode = new nsNavHistoryQueryResultNode(aTitle, aFavicon, aURI);
+ resultNode->mItemId = itemId;
+ // This is a perf hack to generate an empty query that skips filtering.
+ resultNode->GetAsQuery()->Options()->SetExcludeItems(true);
+ }
+
+ resultNode.forget(aNode);
+ return NS_OK;
+}
+
+
+// nsNavHistory::VisitIdToResultNode
+//
+// Used by the query results to create new nodes on the fly when
+// notifications come in. This just creates a node for the given visit ID.
+
+nsresult
+nsNavHistory::VisitIdToResultNode(int64_t visitId,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult)
+{
+ nsAutoCString tagsFragment;
+ GetTagsSqlFragment(GetTagsFolder(), NS_LITERAL_CSTRING("h.id"),
+ true, tagsFragment);
+
+ nsCOMPtr<mozIStorageStatement> statement;
+ switch (aOptions->ResultType())
+ {
+ case nsNavHistoryQueryOptions::RESULTS_AS_VISIT:
+ case nsNavHistoryQueryOptions::RESULTS_AS_FULL_VISIT:
+ // visit query - want exact visit time
+ // Should match kGetInfoIndex_* (see GetQueryResults)
+ statement = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT h.id, h.url, h.title, h.rev_host, h.visit_count, "
+ "v.visit_date, f.url, null, null, null, null, "
+ ) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "v.id, v.from_visit, v.visit_type "
+ "FROM moz_places h "
+ "JOIN moz_historyvisits v ON h.id = v.place_id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE v.id = :visit_id ")
+ );
+ break;
+
+ case nsNavHistoryQueryOptions::RESULTS_AS_URI:
+ // URL results - want last visit time
+ // Should match kGetInfoIndex_* (see GetQueryResults)
+ statement = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT h.id, h.url, h.title, h.rev_host, h.visit_count, "
+ "h.last_visit_date, f.url, null, null, null, null, "
+ ) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "null, null, null "
+ "FROM moz_places h "
+ "JOIN moz_historyvisits v ON h.id = v.place_id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE v.id = :visit_id ")
+ );
+ break;
+
+ default:
+ // Query base types like RESULTS_AS_*_QUERY handle additions
+ // by registering their own observers when they are expanded.
+ return NS_OK;
+ }
+ NS_ENSURE_STATE(statement);
+ mozStorageStatementScoper scoper(statement);
+
+ nsresult rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("visit_id"),
+ visitId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ rv = statement->ExecuteStep(&hasMore);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (! hasMore) {
+ NS_NOTREACHED("Trying to get a result node for an invalid visit");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(statement, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RowToResult(row, aOptions, aResult);
+}
+
+nsresult
+nsNavHistory::BookmarkIdToResultNode(int64_t aBookmarkId, nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult)
+{
+ nsAutoCString tagsFragment;
+ GetTagsSqlFragment(GetTagsFolder(), NS_LITERAL_CSTRING("h.id"),
+ true, tagsFragment);
+ // Should match kGetInfoIndex_*
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT b.fk, h.url, COALESCE(b.title, h.title), "
+ "h.rev_host, h.visit_count, h.last_visit_date, f.url, b.id, "
+ "b.dateAdded, b.lastModified, b.parent, "
+ ) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "null, null, null, b.guid, b.position, b.type, b.fk "
+ "FROM moz_bookmarks b "
+ "JOIN moz_places h ON b.fk = h.id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE b.id = :item_id ")
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+ aBookmarkId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ rv = stmt->ExecuteStep(&hasMore);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasMore) {
+ NS_NOTREACHED("Trying to get a result node for an invalid bookmark identifier");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(stmt, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RowToResult(row, aOptions, aResult);
+}
+
+nsresult
+nsNavHistory::URIToResultNode(nsIURI* aURI,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult)
+{
+ nsAutoCString tagsFragment;
+ GetTagsSqlFragment(GetTagsFolder(), NS_LITERAL_CSTRING("h.id"),
+ true, tagsFragment);
+ // Should match kGetInfoIndex_*
+ nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+ "SELECT h.id, :page_url, COALESCE(b.title, h.title), "
+ "h.rev_host, h.visit_count, h.last_visit_date, f.url, "
+ "b.id, b.dateAdded, b.lastModified, b.parent, "
+ ) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
+ "null, null, null, b.guid, b.position, b.type, b.fk "
+ "FROM moz_places h "
+ "LEFT JOIN moz_bookmarks b ON b.fk = h.id "
+ "LEFT JOIN moz_favicons f ON h.favicon_id = f.id "
+ "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url ")
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ rv = stmt->ExecuteStep(&hasMore);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hasMore) {
+ NS_NOTREACHED("Trying to get a result node for an invalid url");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(stmt, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RowToResult(row, aOptions, aResult);
+}
+
+void
+nsNavHistory::SendPageChangedNotification(nsIURI* aURI,
+ uint32_t aChangedAttribute,
+ const nsAString& aNewValue,
+ const nsACString& aGUID)
+{
+ MOZ_ASSERT(!aGUID.IsEmpty());
+ NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+ nsINavHistoryObserver,
+ OnPageChanged(aURI, aChangedAttribute, aNewValue, aGUID));
+}
+
+// nsNavHistory::TitleForDomain
+//
+// This computes the title for a given domain. Normally, this is just the
+// domain name, but we specially handle empty cases to give you a nice
+// localized string.
+
+void
+nsNavHistory::TitleForDomain(const nsCString& domain, nsACString& aTitle)
+{
+ if (! domain.IsEmpty()) {
+ aTitle = domain;
+ return;
+ }
+
+ // use the localized one instead
+ GetStringFromName(u"localhost", aTitle);
+}
+
+void
+nsNavHistory::GetAgeInDaysString(int32_t aInt, const char16_t *aName,
+ nsACString& aResult)
+{
+ nsIStringBundle *bundle = GetBundle();
+ if (bundle) {
+ nsAutoString intString;
+ intString.AppendInt(aInt);
+ const char16_t* strings[1] = { intString.get() };
+ nsXPIDLString value;
+ nsresult rv = bundle->FormatStringFromName(aName, strings,
+ 1, getter_Copies(value));
+ if (NS_SUCCEEDED(rv)) {
+ CopyUTF16toUTF8(value, aResult);
+ return;
+ }
+ }
+ CopyUTF16toUTF8(nsDependentString(aName), aResult);
+}
+
+void
+nsNavHistory::GetStringFromName(const char16_t *aName, nsACString& aResult)
+{
+ nsIStringBundle *bundle = GetBundle();
+ if (bundle) {
+ nsXPIDLString value;
+ nsresult rv = bundle->GetStringFromName(aName, getter_Copies(value));
+ if (NS_SUCCEEDED(rv)) {
+ CopyUTF16toUTF8(value, aResult);
+ return;
+ }
+ }
+ CopyUTF16toUTF8(nsDependentString(aName), aResult);
+}
+
+void
+nsNavHistory::GetMonthName(int32_t aIndex, nsACString& aResult)
+{
+ nsIStringBundle *bundle = GetDateFormatBundle();
+ if (bundle) {
+ nsCString name = nsPrintfCString("month.%d.name", aIndex);
+ nsXPIDLString value;
+ nsresult rv = bundle->GetStringFromName(NS_ConvertUTF8toUTF16(name).get(),
+ getter_Copies(value));
+ if (NS_SUCCEEDED(rv)) {
+ CopyUTF16toUTF8(value, aResult);
+ return;
+ }
+ }
+ aResult = nsPrintfCString("[%d]", aIndex);
+}
+
+void
+nsNavHistory::GetMonthYear(int32_t aMonth, int32_t aYear, nsACString& aResult)
+{
+ nsIStringBundle *bundle = GetBundle();
+ if (bundle) {
+ nsAutoCString monthName;
+ GetMonthName(aMonth, monthName);
+ nsAutoString yearString;
+ yearString.AppendInt(aYear);
+ const char16_t* strings[2] = {
+ NS_ConvertUTF8toUTF16(monthName).get()
+ , yearString.get()
+ };
+ nsXPIDLString value;
+ if (NS_SUCCEEDED(bundle->FormatStringFromName(
+ u"finduri-MonthYear", strings, 2,
+ getter_Copies(value)
+ ))) {
+ CopyUTF16toUTF8(value, aResult);
+ return;
+ }
+ }
+ aResult.AppendLiteral("finduri-MonthYear");
+}
+
+
+namespace {
+
+// GetSimpleBookmarksQueryFolder
+//
+// Determines if this set of queries is a simple bookmarks query for a
+// folder with no other constraints. In these common cases, we can more
+// efficiently compute the results.
+//
+// A simple bookmarks query will result in a hierarchical tree of
+// bookmark items, folders and separators.
+//
+// Returns the folder ID if it is a simple folder query, 0 if not.
+static int64_t
+GetSimpleBookmarksQueryFolder(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions)
+{
+ if (aQueries.Count() != 1)
+ return 0;
+
+ nsNavHistoryQuery* query = aQueries[0];
+ if (query->Folders().Length() != 1)
+ return 0;
+
+ bool hasIt;
+ query->GetHasBeginTime(&hasIt);
+ if (hasIt)
+ return 0;
+ query->GetHasEndTime(&hasIt);
+ if (hasIt)
+ return 0;
+ query->GetHasDomain(&hasIt);
+ if (hasIt)
+ return 0;
+ query->GetHasUri(&hasIt);
+ if (hasIt)
+ return 0;
+ (void)query->GetHasSearchTerms(&hasIt);
+ if (hasIt)
+ return 0;
+ if (query->Tags().Length() > 0)
+ return 0;
+ if (aOptions->MaxResults() > 0)
+ return 0;
+
+ // RESULTS_AS_TAG_CONTENTS is quite similar to a folder shortcut, but it must
+ // not be treated like that, since it needs all query options.
+ if(aOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS)
+ return 0;
+
+ // Don't care about onlyBookmarked flag, since specifying a bookmark
+ // folder is inferring onlyBookmarked.
+
+ return query->Folders()[0];
+}
+
+
+// ParseSearchTermsFromQueries
+//
+// Construct a matrix of search terms from the given queries array.
+// All of the query objects are ORed together. Within a query, all the terms
+// are ANDed together. See nsINavHistoryService.idl.
+//
+// This just breaks the query up into words. We don't do anything fancy,
+// not even quoting. We do, however, strip quotes, because people might
+// try to input quotes expecting them to do something and get no results
+// back.
+
+inline bool isQueryWhitespace(char16_t ch)
+{
+ return ch == ' ';
+}
+
+void ParseSearchTermsFromQueries(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsTArray<nsTArray<nsString>*>* aTerms)
+{
+ int32_t lastBegin = -1;
+ for (int32_t i = 0; i < aQueries.Count(); i++) {
+ nsTArray<nsString> *queryTerms = new nsTArray<nsString>();
+ bool hasSearchTerms;
+ if (NS_SUCCEEDED(aQueries[i]->GetHasSearchTerms(&hasSearchTerms)) &&
+ hasSearchTerms) {
+ const nsString& searchTerms = aQueries[i]->SearchTerms();
+ for (uint32_t j = 0; j < searchTerms.Length(); j++) {
+ if (isQueryWhitespace(searchTerms[j]) ||
+ searchTerms[j] == '"') {
+ if (lastBegin >= 0) {
+ // found the end of a word
+ queryTerms->AppendElement(Substring(searchTerms, lastBegin,
+ j - lastBegin));
+ lastBegin = -1;
+ }
+ } else {
+ if (lastBegin < 0) {
+ // found the beginning of a word
+ lastBegin = j;
+ }
+ }
+ }
+ // last word
+ if (lastBegin >= 0)
+ queryTerms->AppendElement(Substring(searchTerms, lastBegin));
+ }
+ aTerms->AppendElement(queryTerms);
+ }
+}
+
+} // namespace
+
+
+nsresult
+nsNavHistory::UpdateFrecency(int64_t aPlaceId)
+{
+ nsCOMPtr<mozIStorageAsyncStatement> updateFrecencyStmt = mDB->GetAsyncStatement(
+ "UPDATE moz_places "
+ "SET frecency = NOTIFY_FRECENCY("
+ "CALCULATE_FRECENCY(:page_id), url, guid, hidden, last_visit_date"
+ ") "
+ "WHERE id = :page_id"
+ );
+ NS_ENSURE_STATE(updateFrecencyStmt);
+ nsresult rv = updateFrecencyStmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"),
+ aPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIStorageAsyncStatement> updateHiddenStmt = mDB->GetAsyncStatement(
+ "UPDATE moz_places "
+ "SET hidden = 0 "
+ "WHERE id = :page_id AND frecency <> 0"
+ );
+ NS_ENSURE_STATE(updateHiddenStmt);
+ rv = updateHiddenStmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"),
+ aPlaceId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozIStorageBaseStatement *stmts[] = {
+ updateFrecencyStmt.get()
+ , updateHiddenStmt.get()
+ };
+
+ RefPtr<AsyncStatementCallbackNotifier> cb =
+ new AsyncStatementCallbackNotifier(TOPIC_FRECENCY_UPDATED);
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), cb,
+ getter_AddRefs(ps));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+namespace {
+
+class FixInvalidFrecenciesCallback : public AsyncStatementCallbackNotifier
+{
+public:
+ FixInvalidFrecenciesCallback()
+ : AsyncStatementCallbackNotifier(TOPIC_FRECENCY_UPDATED)
+ {
+ }
+
+ NS_IMETHOD HandleCompletion(uint16_t aReason)
+ {
+ nsresult rv = AsyncStatementCallbackNotifier::HandleCompletion(aReason);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aReason == REASON_FINISHED) {
+ nsNavHistory *navHistory = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(navHistory);
+ navHistory->NotifyManyFrecenciesChanged();
+ }
+ return NS_OK;
+ }
+};
+
+} // namespace
+
+nsresult
+nsNavHistory::FixInvalidFrecencies()
+{
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
+ "UPDATE moz_places "
+ "SET frecency = CALCULATE_FRECENCY(id) "
+ "WHERE frecency < 0"
+ );
+ NS_ENSURE_STATE(stmt);
+
+ RefPtr<FixInvalidFrecenciesCallback> callback =
+ new FixInvalidFrecenciesCallback();
+ nsCOMPtr<mozIStoragePendingStatement> ps;
+ (void)stmt->ExecuteAsync(callback, getter_AddRefs(ps));
+
+ return NS_OK;
+}
+
+
+#ifdef MOZ_XUL
+
+nsresult
+nsNavHistory::AutoCompleteFeedback(int32_t aIndex,
+ nsIAutoCompleteController *aController)
+{
+ nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
+ "INSERT OR REPLACE INTO moz_inputhistory "
+ // use_count will asymptotically approach the max of 10.
+ "SELECT h.id, IFNULL(i.input, :input_text), IFNULL(i.use_count, 0) * .9 + 1 "
+ "FROM moz_places h "
+ "LEFT JOIN moz_inputhistory i ON i.place_id = h.id AND i.input = :input_text "
+ "WHERE url_hash = hash(:page_url) AND url = :page_url "
+ );
+ NS_ENSURE_STATE(stmt);
+
+ nsAutoString input;
+ nsresult rv = aController->GetSearchString(input);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindStringByName(NS_LITERAL_CSTRING("input_text"), input);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString url;
+ rv = aController->GetValueAt(aIndex, url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
+ NS_ConvertUTF16toUTF8(url));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We do the update asynchronously and we do not care about failures.
+ RefPtr<AsyncStatementCallbackNotifier> callback =
+ new AsyncStatementCallbackNotifier(TOPIC_AUTOCOMPLETE_FEEDBACK_UPDATED);
+ nsCOMPtr<mozIStoragePendingStatement> canceler;
+ rv = stmt->ExecuteAsync(callback, getter_AddRefs(canceler));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+#endif
+
+
+nsICollation *
+nsNavHistory::GetCollation()
+{
+ if (mCollation)
+ return mCollation;
+
+ // locale
+ nsCOMPtr<nsILocale> locale;
+ nsCOMPtr<nsILocaleService> ls(do_GetService(NS_LOCALESERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(ls, nullptr);
+ nsresult rv = ls->GetApplicationLocale(getter_AddRefs(locale));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ // collation
+ nsCOMPtr<nsICollationFactory> cfact =
+ do_CreateInstance(NS_COLLATIONFACTORY_CONTRACTID);
+ NS_ENSURE_TRUE(cfact, nullptr);
+ rv = cfact->CreateCollation(locale, getter_AddRefs(mCollation));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return mCollation;
+}
+
+nsIStringBundle *
+nsNavHistory::GetBundle()
+{
+ if (!mBundle) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ services::GetStringBundleService();
+ NS_ENSURE_TRUE(bundleService, nullptr);
+ nsresult rv = bundleService->CreateBundle(
+ "chrome://places/locale/places.properties",
+ getter_AddRefs(mBundle));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ return mBundle;
+}
+
+nsIStringBundle *
+nsNavHistory::GetDateFormatBundle()
+{
+ if (!mDateFormatBundle) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ services::GetStringBundleService();
+ NS_ENSURE_TRUE(bundleService, nullptr);
+ nsresult rv = bundleService->CreateBundle(
+ "chrome://global/locale/dateFormat.properties",
+ getter_AddRefs(mDateFormatBundle));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ return mDateFormatBundle;
+}
diff --git a/toolkit/components/places/nsNavHistory.h b/toolkit/components/places/nsNavHistory.h
new file mode 100644
index 0000000000..ed5272ce01
--- /dev/null
+++ b/toolkit/components/places/nsNavHistory.h
@@ -0,0 +1,659 @@
+/* -*- 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/. */
+
+#ifndef nsNavHistory_h_
+#define nsNavHistory_h_
+
+#include "nsINavHistoryService.h"
+#include "nsPIPlacesDatabase.h"
+#include "nsIBrowserHistory.h"
+#include "nsINavBookmarksService.h"
+#include "nsIFaviconService.h"
+
+#include "nsIObserverService.h"
+#include "nsICollation.h"
+#include "nsIStringBundle.h"
+#include "nsITimer.h"
+#include "nsMaybeWeakPtr.h"
+#include "nsCategoryCache.h"
+#include "nsNetCID.h"
+#include "nsToolkitCompsCID.h"
+#include "nsURIHashKey.h"
+#include "nsTHashtable.h"
+
+#include "nsNavHistoryResult.h"
+#include "nsNavHistoryQuery.h"
+#include "Database.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Atomics.h"
+
+#define QUERYUPDATE_TIME 0
+#define QUERYUPDATE_SIMPLE 1
+#define QUERYUPDATE_COMPLEX 2
+#define QUERYUPDATE_COMPLEX_WITH_BOOKMARKS 3
+#define QUERYUPDATE_HOST 4
+
+// Clamp title and URL to generously large, but not too large, length.
+// See bug 319004 for details.
+#define URI_LENGTH_MAX 65536
+#define TITLE_LENGTH_MAX 4096
+
+// Microsecond timeout for "recent" events such as typed and bookmark following.
+// If you typed it more than this time ago, it's not recent.
+#define RECENT_EVENT_THRESHOLD PRTime((int64_t)15 * 60 * PR_USEC_PER_SEC)
+
+#ifdef MOZ_XUL
+// Fired after autocomplete feedback has been updated.
+#define TOPIC_AUTOCOMPLETE_FEEDBACK_UPDATED "places-autocomplete-feedback-updated"
+#endif
+
+// Fired after frecency has been updated.
+#define TOPIC_FRECENCY_UPDATED "places-frecency-updated"
+
+class nsNavHistory;
+class QueryKeyValuePair;
+class nsIEffectiveTLDService;
+class nsIIDNService;
+class PlacesSQLQueryBuilder;
+class nsIAutoCompleteController;
+
+// nsNavHistory
+
+class nsNavHistory final : public nsSupportsWeakReference
+ , public nsINavHistoryService
+ , public nsIObserver
+ , public nsIBrowserHistory
+ , public nsPIPlacesDatabase
+ , public mozIStorageVacuumParticipant
+{
+ friend class PlacesSQLQueryBuilder;
+
+public:
+ nsNavHistory();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSINAVHISTORYSERVICE
+ NS_DECL_NSIBROWSERHISTORY
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSPIPLACESDATABASE
+ NS_DECL_MOZISTORAGEVACUUMPARTICIPANT
+
+ /**
+ * Obtains the nsNavHistory object.
+ */
+ static already_AddRefed<nsNavHistory> GetSingleton();
+
+ /**
+ * Initializes the nsNavHistory object. This should only be called once.
+ */
+ nsresult Init();
+
+ /**
+ * Used by other components in the places directory such as the annotation
+ * service to get a reference to this history object. Returns a pointer to
+ * the service if it exists. Otherwise creates one. Returns nullptr on error.
+ */
+ static nsNavHistory* GetHistoryService()
+ {
+ if (!gHistoryService) {
+ nsCOMPtr<nsINavHistoryService> serv =
+ do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(serv, nullptr);
+ NS_ASSERTION(gHistoryService, "Should have static instance pointer now");
+ }
+ return gHistoryService;
+ }
+
+ /**
+ * Used by other components in the places directory to get a reference to a
+ * const version of this history object.
+ *
+ * @return A pointer to a const version of the service if it exists,
+ * nullptr otherwise.
+ */
+ static const nsNavHistory* GetConstHistoryService()
+ {
+ const nsNavHistory* const history = gHistoryService;
+ return history;
+ }
+
+ /**
+ * Fetches the database id and the GUID associated to the given URI.
+ *
+ * @param aURI
+ * The page to look for.
+ * @param _pageId
+ * Will be set to the database id associated with the page.
+ * If the page doesn't exist, this will be zero.
+ * @param _GUID
+ * Will be set to the unique id associated with the page.
+ * If the page doesn't exist, this will be empty.
+ * @note This DOES NOT check for bad URLs other than that they're nonempty.
+ */
+ nsresult GetIdForPage(nsIURI* aURI,
+ int64_t* _pageId, nsCString& _GUID);
+
+ /**
+ * Fetches the database id and the GUID associated to the given URI, creating
+ * a new database entry if one doesn't exist yet.
+ *
+ * @param aURI
+ * The page to look for or create.
+ * @param _pageId
+ * Will be set to the database id associated with the page.
+ * @param _GUID
+ * Will be set to the unique id associated with the page.
+ * @note This DOES NOT check for bad URLs other than that they're nonempty.
+ * @note This DOES NOT update frecency of the page.
+ */
+ nsresult GetOrCreateIdForPage(nsIURI* aURI,
+ int64_t* _pageId, nsCString& _GUID);
+
+ /**
+ * Asynchronously recalculates frecency for a given page.
+ *
+ * @param aPlaceId
+ * Place id to recalculate the frecency for.
+ * @note If the new frecency is a non-zero value it will also unhide the page,
+ * otherwise will reuse the old hidden value.
+ */
+ nsresult UpdateFrecency(int64_t aPlaceId);
+
+ /**
+ * Recalculates frecency for all pages requesting that (frecency < 0). Those
+ * may be generated:
+ * * After a "clear private data"
+ * * After removing visits
+ * * After migrating from older versions
+ */
+ nsresult FixInvalidFrecencies();
+
+ /**
+ * Invalidate the frecencies of a list of places, so they will be recalculated
+ * at the first idle-daily notification.
+ *
+ * @param aPlacesIdsQueryString
+ * Query string containing list of places to be invalidated. If it's
+ * an empty string all places will be invalidated.
+ */
+ nsresult invalidateFrecencies(const nsCString& aPlaceIdsQueryString);
+
+ /**
+ * Calls onDeleteVisits and onDeleteURI notifications on registered listeners
+ * with the history service.
+ *
+ * @param aURI
+ * The nsIURI object representing the URI of the page being expired.
+ * @param aVisitTime
+ * The time, in microseconds, that the page being expired was visited.
+ * @param aWholeEntry
+ * Indicates if this is the last visit for this URI.
+ * @param aGUID
+ * The unique ID associated with the page.
+ * @param aReason
+ * Indicates the reason for the removal.
+ * See nsINavHistoryObserver::REASON_* constants.
+ * @param aTransitionType
+ * If it's a valid TRANSITION_* value, all visits of the specified type
+ * have been removed.
+ */
+ nsresult NotifyOnPageExpired(nsIURI *aURI, PRTime aVisitTime,
+ bool aWholeEntry, const nsACString& aGUID,
+ uint16_t aReason, uint32_t aTransitionType);
+
+ /**
+ * These functions return non-owning references to the locale-specific
+ * objects for places components.
+ */
+ nsIStringBundle* GetBundle();
+ nsIStringBundle* GetDateFormatBundle();
+ nsICollation* GetCollation();
+ void GetStringFromName(const char16_t* aName, nsACString& aResult);
+ void GetAgeInDaysString(int32_t aInt, const char16_t *aName,
+ nsACString& aResult);
+ void GetMonthName(int32_t aIndex, nsACString& aResult);
+ void GetMonthYear(int32_t aMonth, int32_t aYear, nsACString& aResult);
+
+ // Returns whether history is enabled or not.
+ bool IsHistoryDisabled() {
+ return !mHistoryEnabled;
+ }
+
+ // Constants for the columns returned by the above statement.
+ static const int32_t kGetInfoIndex_PageID;
+ static const int32_t kGetInfoIndex_URL;
+ static const int32_t kGetInfoIndex_Title;
+ static const int32_t kGetInfoIndex_RevHost;
+ static const int32_t kGetInfoIndex_VisitCount;
+ static const int32_t kGetInfoIndex_VisitDate;
+ static const int32_t kGetInfoIndex_FaviconURL;
+ static const int32_t kGetInfoIndex_ItemId;
+ static const int32_t kGetInfoIndex_ItemDateAdded;
+ static const int32_t kGetInfoIndex_ItemLastModified;
+ static const int32_t kGetInfoIndex_ItemParentId;
+ static const int32_t kGetInfoIndex_ItemTags;
+ static const int32_t kGetInfoIndex_Frecency;
+ static const int32_t kGetInfoIndex_Hidden;
+ static const int32_t kGetInfoIndex_Guid;
+ static const int32_t kGetInfoIndex_VisitId;
+ static const int32_t kGetInfoIndex_FromVisitId;
+ static const int32_t kGetInfoIndex_VisitType;
+
+ int64_t GetTagsFolder();
+
+ // this actually executes a query and gives you results, it is used by
+ // nsNavHistoryQueryResultNode
+ nsresult GetQueryResults(nsNavHistoryQueryResultNode *aResultNode,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions *aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aResults);
+
+ // Take a row of kGetInfoIndex_* columns and construct a ResultNode.
+ // The row must contain the full set of columns.
+ nsresult RowToResult(mozIStorageValueArray* aRow,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult);
+ nsresult QueryRowToResult(int64_t aItemId,
+ const nsACString& aBookmarkGuid,
+ const nsACString& aURI,
+ const nsACString& aTitle,
+ uint32_t aAccessCount, PRTime aTime,
+ const nsACString& aFavicon,
+ nsNavHistoryResultNode** aNode);
+
+ nsresult VisitIdToResultNode(int64_t visitId,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult);
+
+ nsresult BookmarkIdToResultNode(int64_t aBookmarkId,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult);
+ nsresult URIToResultNode(nsIURI* aURI,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode** aResult);
+
+ // used by other places components to send history notifications (for example,
+ // when the favicon has changed)
+ void SendPageChangedNotification(nsIURI* aURI, uint32_t aChangedAttribute,
+ const nsAString& aValue,
+ const nsACString& aGUID);
+
+ /**
+ * Returns current number of days stored in history.
+ */
+ int32_t GetDaysOfHistory();
+
+ // used by query result nodes to update: see comment on body of CanLiveUpdateQuery
+ static uint32_t GetUpdateRequirements(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions,
+ bool* aHasSearchTerms);
+ bool EvaluateQueryForNode(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryResultNode* aNode);
+
+ static nsresult AsciiHostNameFromHostString(const nsACString& aHostName,
+ nsACString& aAscii);
+ void DomainNameFromURI(nsIURI* aURI,
+ nsACString& aDomainName);
+ static PRTime NormalizeTime(uint32_t aRelative, PRTime aOffset);
+
+ // Don't use these directly, inside nsNavHistory use UpdateBatchScoper,
+ // else use nsINavHistoryService::RunInBatchMode
+ nsresult BeginUpdateBatch();
+ nsresult EndUpdateBatch();
+
+ // The level of batches' nesting, 0 when no batches are open.
+ int32_t mBatchLevel;
+ // Current active transaction for a batch.
+ mozStorageTransaction* mBatchDBTransaction;
+
+ // better alternative to QueryStringToQueries (in nsNavHistoryQuery.cpp)
+ nsresult QueryStringToQueryArray(const nsACString& aQueryString,
+ nsCOMArray<nsNavHistoryQuery>* aQueries,
+ nsNavHistoryQueryOptions** aOptions);
+
+ typedef nsDataHashtable<nsCStringHashKey, nsCString> StringHash;
+
+ /**
+ * Indicates if it is OK to notify history observers or not.
+ *
+ * @return true if it is OK to notify, false otherwise.
+ */
+ bool canNotify() { return mCanNotify; }
+
+ enum RecentEventFlags {
+ RECENT_TYPED = 1 << 0, // User typed in URL recently
+ RECENT_ACTIVATED = 1 << 1, // User tapped URL link recently
+ RECENT_BOOKMARKED = 1 << 2 // User bookmarked URL recently
+ };
+
+ /**
+ * Returns any recent activity done with a URL.
+ * @return Any recent events associated with this URI. Each bit is set
+ * according to RecentEventFlags enum values.
+ */
+ uint32_t GetRecentFlags(nsIURI *aURI);
+
+ /**
+ * Registers a TRANSITION_EMBED visit for the session.
+ *
+ * @param aURI
+ * URI of the page.
+ * @param aTime
+ * Visit time. Only the last registered visit time is retained.
+ */
+ void registerEmbedVisit(nsIURI* aURI, int64_t aTime);
+
+ /**
+ * Returns whether the specified url has a embed visit.
+ *
+ * @param aURI
+ * URI of the page.
+ * @return whether the page has a embed visit.
+ */
+ bool hasEmbedVisit(nsIURI* aURI);
+
+ /**
+ * Clears all registered embed visits.
+ */
+ void clearEmbedVisits();
+
+ int32_t GetFrecencyAgedWeight(int32_t aAgeInDays) const
+ {
+ if (aAgeInDays <= mFirstBucketCutoffInDays) {
+ return mFirstBucketWeight;
+ }
+ if (aAgeInDays <= mSecondBucketCutoffInDays) {
+ return mSecondBucketWeight;
+ }
+ if (aAgeInDays <= mThirdBucketCutoffInDays) {
+ return mThirdBucketWeight;
+ }
+ if (aAgeInDays <= mFourthBucketCutoffInDays) {
+ return mFourthBucketWeight;
+ }
+ return mDefaultWeight;
+ }
+
+ int32_t GetFrecencyBucketWeight(int32_t aBucketIndex) const
+ {
+ switch(aBucketIndex) {
+ case 1:
+ return mFirstBucketWeight;
+ case 2:
+ return mSecondBucketWeight;
+ case 3:
+ return mThirdBucketWeight;
+ case 4:
+ return mFourthBucketWeight;
+ default:
+ return mDefaultWeight;
+ }
+ }
+
+ int32_t GetFrecencyTransitionBonus(int32_t aTransitionType,
+ bool aVisited) const
+ {
+ switch (aTransitionType) {
+ case nsINavHistoryService::TRANSITION_EMBED:
+ return mEmbedVisitBonus;
+ case nsINavHistoryService::TRANSITION_FRAMED_LINK:
+ return mFramedLinkVisitBonus;
+ case nsINavHistoryService::TRANSITION_LINK:
+ return mLinkVisitBonus;
+ case nsINavHistoryService::TRANSITION_TYPED:
+ return aVisited ? mTypedVisitBonus : mUnvisitedTypedBonus;
+ case nsINavHistoryService::TRANSITION_BOOKMARK:
+ return aVisited ? mBookmarkVisitBonus : mUnvisitedBookmarkBonus;
+ case nsINavHistoryService::TRANSITION_DOWNLOAD:
+ return mDownloadVisitBonus;
+ case nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT:
+ return mPermRedirectVisitBonus;
+ case nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY:
+ return mTempRedirectVisitBonus;
+ case nsINavHistoryService::TRANSITION_RELOAD:
+ return mReloadVisitBonus;
+ default:
+ // 0 == undefined (see bug #375777 for details)
+ NS_WARNING_ASSERTION(!aTransitionType,
+ "new transition but no bonus for frecency");
+ return mDefaultVisitBonus;
+ }
+ }
+
+ int32_t GetNumVisitsForFrecency() const
+ {
+ return mNumVisitsForFrecency;
+ }
+
+ /**
+ * Fires onVisit event to nsINavHistoryService observers
+ */
+ void NotifyOnVisit(nsIURI* aURI,
+ int64_t aVisitId,
+ PRTime aTime,
+ int64_t aReferrerVisitId,
+ int32_t aTransitionType,
+ const nsACString& aGuid,
+ bool aHidden,
+ uint32_t aVisitCount,
+ uint32_t aTyped);
+
+ /**
+ * Fires onTitleChanged event to nsINavHistoryService observers
+ */
+ void NotifyTitleChange(nsIURI* aURI,
+ const nsString& title,
+ const nsACString& aGUID);
+
+ /**
+ * Fires onFrecencyChanged event to nsINavHistoryService observers
+ */
+ void NotifyFrecencyChanged(nsIURI* aURI,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate);
+
+ /**
+ * Fires onManyFrecenciesChanged event to nsINavHistoryService observers
+ */
+ void NotifyManyFrecenciesChanged();
+
+ /**
+ * Posts a runnable to the main thread that calls NotifyFrecencyChanged.
+ */
+ void DispatchFrecencyChangedNotification(const nsACString& aSpec,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate) const;
+
+ /**
+ * Store last insterted id for a table.
+ */
+ static mozilla::Atomic<int64_t> sLastInsertedPlaceId;
+ static mozilla::Atomic<int64_t> sLastInsertedVisitId;
+
+ static void StoreLastInsertedId(const nsACString& aTable,
+ const int64_t aLastInsertedId);
+
+ bool isBatching() {
+ return mBatchLevel > 0;
+ }
+
+private:
+ ~nsNavHistory();
+
+ // used by GetHistoryService
+ static nsNavHistory *gHistoryService;
+
+protected:
+
+ // Database handle.
+ RefPtr<mozilla::places::Database> mDB;
+
+ /**
+ * Decays frecency and inputhistory values. Runs on idle-daily.
+ */
+ nsresult DecayFrecency();
+
+ nsresult RemovePagesInternal(const nsCString& aPlaceIdsQueryString);
+ nsresult CleanupPlacesOnVisitsDelete(const nsCString& aPlaceIdsQueryString);
+
+ /**
+ * Loads all of the preferences that we use into member variables.
+ *
+ * @note If mPrefBranch is nullptr, this does nothing.
+ */
+ void LoadPrefs();
+
+ /**
+ * Calculates and returns value for mCachedNow.
+ * This is an hack to avoid calling PR_Now() too often, as is the case when
+ * we're asked the ageindays of many history entries in a row. A timer is
+ * set which will clear our valid flag after a short timeout.
+ */
+ PRTime GetNow();
+ PRTime mCachedNow;
+ nsCOMPtr<nsITimer> mExpireNowTimer;
+ /**
+ * Called when the cached now value is expired and needs renewal.
+ */
+ static void expireNowTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ nsresult ConstructQueryString(const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCString& queryString,
+ bool& aParamsPresent,
+ StringHash& aAddParams);
+
+ nsresult QueryToSelectClause(nsNavHistoryQuery* aQuery,
+ nsNavHistoryQueryOptions* aOptions,
+ int32_t aQueryIndex,
+ nsCString* aClause);
+ nsresult BindQueryClauseParameters(mozIStorageBaseStatement* statement,
+ int32_t aQueryIndex,
+ nsNavHistoryQuery* aQuery,
+ nsNavHistoryQueryOptions* aOptions);
+
+ nsresult ResultsAsList(mozIStorageStatement* statement,
+ nsNavHistoryQueryOptions* aOptions,
+ nsCOMArray<nsNavHistoryResultNode>* aResults);
+
+ void TitleForDomain(const nsCString& domain, nsACString& aTitle);
+
+ nsresult FilterResultSet(nsNavHistoryQueryResultNode *aParentNode,
+ const nsCOMArray<nsNavHistoryResultNode>& aSet,
+ nsCOMArray<nsNavHistoryResultNode>* aFiltered,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions);
+
+ // observers
+ nsMaybeWeakPtrArray<nsINavHistoryObserver> mObservers;
+
+ // effective tld service
+ nsCOMPtr<nsIEffectiveTLDService> mTLDService;
+ nsCOMPtr<nsIIDNService> mIDNService;
+
+ // localization
+ nsCOMPtr<nsIStringBundle> mBundle;
+ nsCOMPtr<nsIStringBundle> mDateFormatBundle;
+ nsCOMPtr<nsICollation> mCollation;
+
+ // recent events
+ typedef nsDataHashtable<nsCStringHashKey, int64_t> RecentEventHash;
+ RecentEventHash mRecentTyped;
+ RecentEventHash mRecentLink;
+ RecentEventHash mRecentBookmark;
+
+ // Embed visits tracking.
+ class VisitHashKey : public nsURIHashKey
+ {
+ public:
+ explicit VisitHashKey(const nsIURI* aURI)
+ : nsURIHashKey(aURI)
+ {
+ }
+ VisitHashKey(const VisitHashKey& aOther)
+ : nsURIHashKey(aOther)
+ {
+ NS_NOTREACHED("Do not call me!");
+ }
+ PRTime visitTime;
+ };
+
+ nsTHashtable<VisitHashKey> mEmbedVisits;
+
+ bool CheckIsRecentEvent(RecentEventHash* hashTable,
+ const nsACString& url);
+ void ExpireNonrecentEvents(RecentEventHash* hashTable);
+
+#ifdef MOZ_XUL
+ nsresult AutoCompleteFeedback(int32_t aIndex,
+ nsIAutoCompleteController *aController);
+#endif
+
+ // Whether history is enabled or not.
+ // Will mimic value of the places.history.enabled preference.
+ bool mHistoryEnabled;
+
+ // Frecency preferences.
+ int32_t mNumVisitsForFrecency;
+ int32_t mFirstBucketCutoffInDays;
+ int32_t mSecondBucketCutoffInDays;
+ int32_t mThirdBucketCutoffInDays;
+ int32_t mFourthBucketCutoffInDays;
+ int32_t mFirstBucketWeight;
+ int32_t mSecondBucketWeight;
+ int32_t mThirdBucketWeight;
+ int32_t mFourthBucketWeight;
+ int32_t mDefaultWeight;
+ int32_t mEmbedVisitBonus;
+ int32_t mFramedLinkVisitBonus;
+ int32_t mLinkVisitBonus;
+ int32_t mTypedVisitBonus;
+ int32_t mBookmarkVisitBonus;
+ int32_t mDownloadVisitBonus;
+ int32_t mPermRedirectVisitBonus;
+ int32_t mTempRedirectVisitBonus;
+ int32_t mDefaultVisitBonus;
+ int32_t mUnvisitedBookmarkBonus;
+ int32_t mUnvisitedTypedBonus;
+ int32_t mReloadVisitBonus;
+
+ // in nsNavHistoryQuery.cpp
+ nsresult TokensToQueries(const nsTArray<QueryKeyValuePair>& aTokens,
+ nsCOMArray<nsNavHistoryQuery>* aQueries,
+ nsNavHistoryQueryOptions* aOptions);
+
+ int64_t mTagsFolder;
+
+ int32_t mDaysOfHistory;
+ int64_t mLastCachedStartOfDay;
+ int64_t mLastCachedEndOfDay;
+
+ // Used to enable and disable the observer notifications
+ bool mCanNotify;
+ nsCategoryCache<nsINavHistoryObserver> mCacheObservers;
+};
+
+
+#define PLACES_URI_PREFIX "place:"
+
+/* Returns true if the given URI represents a history query. */
+inline bool IsQueryURI(const nsCString &uri)
+{
+ return StringBeginsWith(uri, NS_LITERAL_CSTRING(PLACES_URI_PREFIX));
+}
+
+/* Extracts the query string from a query URI. */
+inline const nsDependentCSubstring QueryURIToQuery(const nsCString &uri)
+{
+ NS_ASSERTION(IsQueryURI(uri), "should only be called for query URIs");
+ return Substring(uri, NS_LITERAL_CSTRING(PLACES_URI_PREFIX).Length());
+}
+
+#endif // nsNavHistory_h_
diff --git a/toolkit/components/places/nsNavHistoryQuery.cpp b/toolkit/components/places/nsNavHistoryQuery.cpp
new file mode 100644
index 0000000000..1a7b1c2390
--- /dev/null
+++ b/toolkit/components/places/nsNavHistoryQuery.cpp
@@ -0,0 +1,1694 @@
+//* -*- 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/. */
+
+/**
+ * This file contains the definitions of nsNavHistoryQuery,
+ * nsNavHistoryQueryOptions, and those functions in nsINavHistory that directly
+ * support queries (specifically QueryStringToQueries and QueriesToQueryString).
+ */
+
+#include "mozilla/DebugOnly.h"
+
+#include "nsNavHistory.h"
+#include "nsNavBookmarks.h"
+#include "nsEscape.h"
+#include "nsCOMArray.h"
+#include "nsNetUtil.h"
+#include "nsTArray.h"
+#include "prprf.h"
+#include "nsVariant.h"
+
+using namespace mozilla;
+
+class QueryKeyValuePair
+{
+public:
+
+ // QueryKeyValuePair
+ //
+ // 01234567890
+ // input : qwerty&key=value&qwerty
+ // ^ ^ ^
+ // aKeyBegin | aPastEnd (may point to null terminator)
+ // aEquals
+ //
+ // Special case: if aKeyBegin == aEquals, then there is only one string
+ // and no equal sign, so we treat the entire thing as a key with no value
+
+ QueryKeyValuePair(const nsCSubstring& aSource, int32_t aKeyBegin,
+ int32_t aEquals, int32_t aPastEnd)
+ {
+ if (aEquals == aKeyBegin)
+ aEquals = aPastEnd;
+ key = Substring(aSource, aKeyBegin, aEquals - aKeyBegin);
+ if (aPastEnd - aEquals > 0)
+ value = Substring(aSource, aEquals + 1, aPastEnd - aEquals - 1);
+ }
+ nsCString key;
+ nsCString value;
+};
+
+static nsresult TokenizeQueryString(const nsACString& aQuery,
+ nsTArray<QueryKeyValuePair>* aTokens);
+static nsresult ParseQueryBooleanString(const nsCString& aString,
+ bool* aValue);
+
+// query getters
+typedef NS_STDCALL_FUNCPROTO(nsresult, BoolQueryGetter, nsINavHistoryQuery,
+ GetOnlyBookmarked, (bool*));
+typedef NS_STDCALL_FUNCPROTO(nsresult, Uint32QueryGetter, nsINavHistoryQuery,
+ GetBeginTimeReference, (uint32_t*));
+typedef NS_STDCALL_FUNCPROTO(nsresult, Int64QueryGetter, nsINavHistoryQuery,
+ GetBeginTime, (int64_t*));
+static void AppendBoolKeyValueIfTrue(nsACString& aString,
+ const nsCString& aName,
+ nsINavHistoryQuery* aQuery,
+ BoolQueryGetter getter);
+static void AppendUint32KeyValueIfNonzero(nsACString& aString,
+ const nsCString& aName,
+ nsINavHistoryQuery* aQuery,
+ Uint32QueryGetter getter);
+static void AppendInt64KeyValueIfNonzero(nsACString& aString,
+ const nsCString& aName,
+ nsINavHistoryQuery* aQuery,
+ Int64QueryGetter getter);
+
+// query setters
+typedef NS_STDCALL_FUNCPROTO(nsresult, BoolQuerySetter, nsINavHistoryQuery,
+ SetOnlyBookmarked, (bool));
+typedef NS_STDCALL_FUNCPROTO(nsresult, Uint32QuerySetter, nsINavHistoryQuery,
+ SetBeginTimeReference, (uint32_t));
+typedef NS_STDCALL_FUNCPROTO(nsresult, Int64QuerySetter, nsINavHistoryQuery,
+ SetBeginTime, (int64_t));
+static void SetQueryKeyBool(const nsCString& aValue, nsINavHistoryQuery* aQuery,
+ BoolQuerySetter setter);
+static void SetQueryKeyUint32(const nsCString& aValue, nsINavHistoryQuery* aQuery,
+ Uint32QuerySetter setter);
+static void SetQueryKeyInt64(const nsCString& aValue, nsINavHistoryQuery* aQuery,
+ Int64QuerySetter setter);
+
+// options setters
+typedef NS_STDCALL_FUNCPROTO(nsresult, BoolOptionsSetter,
+ nsINavHistoryQueryOptions,
+ SetExpandQueries, (bool));
+typedef NS_STDCALL_FUNCPROTO(nsresult, Uint32OptionsSetter,
+ nsINavHistoryQueryOptions,
+ SetMaxResults, (uint32_t));
+typedef NS_STDCALL_FUNCPROTO(nsresult, Uint16OptionsSetter,
+ nsINavHistoryQueryOptions,
+ SetResultType, (uint16_t));
+static void SetOptionsKeyBool(const nsCString& aValue,
+ nsINavHistoryQueryOptions* aOptions,
+ BoolOptionsSetter setter);
+static void SetOptionsKeyUint16(const nsCString& aValue,
+ nsINavHistoryQueryOptions* aOptions,
+ Uint16OptionsSetter setter);
+static void SetOptionsKeyUint32(const nsCString& aValue,
+ nsINavHistoryQueryOptions* aOptions,
+ Uint32OptionsSetter setter);
+
+// Components of a query string.
+// Note that query strings are also generated in nsNavBookmarks::GetFolderURI
+// for performance reasons, so if you change these values, change that, too.
+#define QUERYKEY_BEGIN_TIME "beginTime"
+#define QUERYKEY_BEGIN_TIME_REFERENCE "beginTimeRef"
+#define QUERYKEY_END_TIME "endTime"
+#define QUERYKEY_END_TIME_REFERENCE "endTimeRef"
+#define QUERYKEY_SEARCH_TERMS "terms"
+#define QUERYKEY_MIN_VISITS "minVisits"
+#define QUERYKEY_MAX_VISITS "maxVisits"
+#define QUERYKEY_ONLY_BOOKMARKED "onlyBookmarked"
+#define QUERYKEY_DOMAIN_IS_HOST "domainIsHost"
+#define QUERYKEY_DOMAIN "domain"
+#define QUERYKEY_FOLDER "folder"
+#define QUERYKEY_NOTANNOTATION "!annotation"
+#define QUERYKEY_ANNOTATION "annotation"
+#define QUERYKEY_URI "uri"
+#define QUERYKEY_SEPARATOR "OR"
+#define QUERYKEY_GROUP "group"
+#define QUERYKEY_SORT "sort"
+#define QUERYKEY_SORTING_ANNOTATION "sortingAnnotation"
+#define QUERYKEY_RESULT_TYPE "type"
+#define QUERYKEY_EXCLUDE_ITEMS "excludeItems"
+#define QUERYKEY_EXCLUDE_QUERIES "excludeQueries"
+#define QUERYKEY_EXCLUDE_READ_ONLY_FOLDERS "excludeReadOnlyFolders"
+#define QUERYKEY_EXPAND_QUERIES "expandQueries"
+#define QUERYKEY_FORCE_ORIGINAL_TITLE "originalTitle"
+#define QUERYKEY_INCLUDE_HIDDEN "includeHidden"
+#define QUERYKEY_MAX_RESULTS "maxResults"
+#define QUERYKEY_QUERY_TYPE "queryType"
+#define QUERYKEY_TAG "tag"
+#define QUERYKEY_NOTTAGS "!tags"
+#define QUERYKEY_ASYNC_ENABLED "asyncEnabled"
+#define QUERYKEY_TRANSITION "transition"
+
+inline void AppendAmpersandIfNonempty(nsACString& aString)
+{
+ if (! aString.IsEmpty())
+ aString.Append('&');
+}
+inline void AppendInt16(nsACString& str, int16_t i)
+{
+ nsAutoCString tmp;
+ tmp.AppendInt(i);
+ str.Append(tmp);
+}
+inline void AppendInt32(nsACString& str, int32_t i)
+{
+ nsAutoCString tmp;
+ tmp.AppendInt(i);
+ str.Append(tmp);
+}
+inline void AppendInt64(nsACString& str, int64_t i)
+{
+ nsCString tmp;
+ tmp.AppendInt(i);
+ str.Append(tmp);
+}
+
+namespace PlacesFolderConversion {
+ #define PLACES_ROOT_FOLDER "PLACES_ROOT"
+ #define BOOKMARKS_MENU_FOLDER "BOOKMARKS_MENU"
+ #define TAGS_FOLDER "TAGS"
+ #define UNFILED_BOOKMARKS_FOLDER "UNFILED_BOOKMARKS"
+ #define TOOLBAR_FOLDER "TOOLBAR"
+ #define MOBILE_BOOKMARKS_FOLDER "MOBILE_BOOKMARKS"
+
+ /**
+ * Converts a folder name to a folder id.
+ *
+ * @param aName
+ * The name of the folder to convert to a folder id.
+ * @returns the folder id if aName is a recognizable name, -1 otherwise.
+ */
+ inline int64_t DecodeFolder(const nsCString &aName)
+ {
+ nsNavBookmarks *bs = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bs, false);
+ int64_t folderID = -1;
+
+ if (aName.EqualsLiteral(PLACES_ROOT_FOLDER))
+ (void)bs->GetPlacesRoot(&folderID);
+ else if (aName.EqualsLiteral(BOOKMARKS_MENU_FOLDER))
+ (void)bs->GetBookmarksMenuFolder(&folderID);
+ else if (aName.EqualsLiteral(TAGS_FOLDER))
+ (void)bs->GetTagsFolder(&folderID);
+ else if (aName.EqualsLiteral(UNFILED_BOOKMARKS_FOLDER))
+ (void)bs->GetUnfiledBookmarksFolder(&folderID);
+ else if (aName.EqualsLiteral(TOOLBAR_FOLDER))
+ (void)bs->GetToolbarFolder(&folderID);
+ else if (aName.EqualsLiteral(MOBILE_BOOKMARKS_FOLDER))
+ (void)bs->GetMobileFolder(&folderID);
+
+ return folderID;
+ }
+
+ /**
+ * Converts a folder id to a named constant, or a string representation of the
+ * folder id if there is no named constant for the folder, and appends it to
+ * aQuery.
+ *
+ * @param aQuery
+ * The string to append the folder string to. This is generally a
+ * query string, but could really be anything.
+ * @param aFolderID
+ * The folder ID to convert to the proper named constant.
+ */
+ inline nsresult AppendFolder(nsCString &aQuery, int64_t aFolderID)
+ {
+ nsNavBookmarks *bs = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_STATE(bs);
+ int64_t folderID;
+
+ if (NS_SUCCEEDED(bs->GetPlacesRoot(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(PLACES_ROOT_FOLDER);
+ }
+ else if (NS_SUCCEEDED(bs->GetBookmarksMenuFolder(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(BOOKMARKS_MENU_FOLDER);
+ }
+ else if (NS_SUCCEEDED(bs->GetTagsFolder(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(TAGS_FOLDER);
+ }
+ else if (NS_SUCCEEDED(bs->GetUnfiledBookmarksFolder(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(UNFILED_BOOKMARKS_FOLDER);
+ }
+ else if (NS_SUCCEEDED(bs->GetToolbarFolder(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(TOOLBAR_FOLDER);
+ }
+ else if (NS_SUCCEEDED(bs->GetMobileFolder(&folderID)) &&
+ aFolderID == folderID) {
+ aQuery.AppendLiteral(MOBILE_BOOKMARKS_FOLDER);
+ }
+ else {
+ // It wasn't one of our named constants, so just convert it to a string.
+ aQuery.AppendInt(aFolderID);
+ }
+
+ return NS_OK;
+ }
+} // namespace PlacesFolderConversion
+
+// nsNavHistory::QueryStringToQueries
+//
+// From C++ places code, you should use QueryStringToQueryArray, this is
+// the harder-to-use XPCOM version.
+
+NS_IMETHODIMP
+nsNavHistory::QueryStringToQueries(const nsACString& aQueryString,
+ nsINavHistoryQuery*** aQueries,
+ uint32_t* aResultCount,
+ nsINavHistoryQueryOptions** aOptions)
+{
+ NS_ENSURE_ARG_POINTER(aQueries);
+ NS_ENSURE_ARG_POINTER(aResultCount);
+ NS_ENSURE_ARG_POINTER(aOptions);
+
+ *aQueries = nullptr;
+ *aResultCount = 0;
+ nsCOMPtr<nsNavHistoryQueryOptions> options;
+ nsCOMArray<nsNavHistoryQuery> queries;
+ nsresult rv = QueryStringToQueryArray(aQueryString, &queries,
+ getter_AddRefs(options));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResultCount = queries.Count();
+ if (queries.Count() > 0) {
+ // convert COM array to raw
+ *aQueries = static_cast<nsINavHistoryQuery**>
+ (moz_xmalloc(sizeof(nsINavHistoryQuery*) * queries.Count()));
+ NS_ENSURE_TRUE(*aQueries, NS_ERROR_OUT_OF_MEMORY);
+ for (int32_t i = 0; i < queries.Count(); i ++) {
+ (*aQueries)[i] = queries[i];
+ NS_ADDREF((*aQueries)[i]);
+ }
+ }
+ options.forget(aOptions);
+ return NS_OK;
+}
+
+
+// nsNavHistory::QueryStringToQueryArray
+//
+// An internal version of QueryStringToQueries that fills a COM array for
+// ease-of-use.
+
+nsresult
+nsNavHistory::QueryStringToQueryArray(const nsACString& aQueryString,
+ nsCOMArray<nsNavHistoryQuery>* aQueries,
+ nsNavHistoryQueryOptions** aOptions)
+{
+ nsresult rv;
+ aQueries->Clear();
+ *aOptions = nullptr;
+
+ RefPtr<nsNavHistoryQueryOptions> options(new nsNavHistoryQueryOptions());
+ if (! options)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ nsTArray<QueryKeyValuePair> tokens;
+ rv = TokenizeQueryString(aQueryString, &tokens);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = TokensToQueries(tokens, aQueries, options);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Unable to parse the query string: ");
+ NS_WARNING(PromiseFlatCString(aQueryString).get());
+ return rv;
+ }
+
+ options.forget(aOptions);
+ return NS_OK;
+}
+
+
+// nsNavHistory::QueriesToQueryString
+
+NS_IMETHODIMP
+nsNavHistory::QueriesToQueryString(nsINavHistoryQuery **aQueries,
+ uint32_t aQueryCount,
+ nsINavHistoryQueryOptions* aOptions,
+ nsACString& aQueryString)
+{
+ NS_ENSURE_ARG(aQueries);
+ NS_ENSURE_ARG(aOptions);
+
+ nsCOMPtr<nsNavHistoryQueryOptions> options = do_QueryInterface(aOptions);
+ NS_ENSURE_TRUE(options, NS_ERROR_INVALID_ARG);
+
+ nsAutoCString queryString;
+ for (uint32_t queryIndex = 0; queryIndex < aQueryCount; queryIndex ++) {
+ nsCOMPtr<nsNavHistoryQuery> query = do_QueryInterface(aQueries[queryIndex]);
+ if (queryIndex > 0) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_SEPARATOR);
+ }
+
+ bool hasIt;
+
+ // begin time
+ query->GetHasBeginTime(&hasIt);
+ if (hasIt) {
+ AppendInt64KeyValueIfNonzero(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_BEGIN_TIME),
+ query, &nsINavHistoryQuery::GetBeginTime);
+ AppendUint32KeyValueIfNonzero(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_BEGIN_TIME_REFERENCE),
+ query, &nsINavHistoryQuery::GetBeginTimeReference);
+ }
+
+ // end time
+ query->GetHasEndTime(&hasIt);
+ if (hasIt) {
+ AppendInt64KeyValueIfNonzero(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_END_TIME),
+ query, &nsINavHistoryQuery::GetEndTime);
+ AppendUint32KeyValueIfNonzero(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_END_TIME_REFERENCE),
+ query, &nsINavHistoryQuery::GetEndTimeReference);
+ }
+
+ // search terms
+ query->GetHasSearchTerms(&hasIt);
+ if (hasIt) {
+ nsAutoString searchTerms;
+ query->GetSearchTerms(searchTerms);
+ nsCString escapedTerms;
+ if (! NS_Escape(NS_ConvertUTF16toUTF8(searchTerms), escapedTerms,
+ url_XAlphas))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_SEARCH_TERMS "=");
+ queryString += escapedTerms;
+ }
+
+ // min and max visits
+ int32_t minVisits;
+ if (NS_SUCCEEDED(query->GetMinVisits(&minVisits)) && minVisits >= 0) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString.AppendLiteral(QUERYKEY_MIN_VISITS "=");
+ AppendInt32(queryString, minVisits);
+ }
+
+ int32_t maxVisits;
+ if (NS_SUCCEEDED(query->GetMaxVisits(&maxVisits)) && maxVisits >= 0) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString.AppendLiteral(QUERYKEY_MAX_VISITS "=");
+ AppendInt32(queryString, maxVisits);
+ }
+
+ // only bookmarked
+ AppendBoolKeyValueIfTrue(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_ONLY_BOOKMARKED),
+ query, &nsINavHistoryQuery::GetOnlyBookmarked);
+
+ // domain (+ is host), only call if hasDomain, which means non-IsVoid
+ // this means we may get an empty string for the domain in the result,
+ // which is valid
+ query->GetHasDomain(&hasIt);
+ if (hasIt) {
+ AppendBoolKeyValueIfTrue(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_DOMAIN_IS_HOST),
+ query, &nsINavHistoryQuery::GetDomainIsHost);
+ nsAutoCString domain;
+ nsresult rv = query->GetDomain(domain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString escapedDomain;
+ bool success = NS_Escape(domain, escapedDomain, url_XAlphas);
+ NS_ENSURE_TRUE(success, NS_ERROR_OUT_OF_MEMORY);
+
+ AppendAmpersandIfNonempty(queryString);
+ queryString.AppendLiteral(QUERYKEY_DOMAIN "=");
+ queryString.Append(escapedDomain);
+ }
+
+ // uri
+ query->GetHasUri(&hasIt);
+ if (hasIt) {
+ nsCOMPtr<nsIURI> uri;
+ query->GetUri(getter_AddRefs(uri));
+ NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); // hasURI should tell is if invalid
+ nsAutoCString uriSpec;
+ nsresult rv = uri->GetSpec(uriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString escaped;
+ bool success = NS_Escape(uriSpec, escaped, url_XAlphas);
+ NS_ENSURE_TRUE(success, NS_ERROR_OUT_OF_MEMORY);
+
+ AppendAmpersandIfNonempty(queryString);
+ queryString.AppendLiteral(QUERYKEY_URI "=");
+ queryString.Append(escaped);
+ }
+
+ // annotation
+ query->GetHasAnnotation(&hasIt);
+ if (hasIt) {
+ AppendAmpersandIfNonempty(queryString);
+ bool annotationIsNot;
+ query->GetAnnotationIsNot(&annotationIsNot);
+ if (annotationIsNot)
+ queryString.AppendLiteral(QUERYKEY_NOTANNOTATION "=");
+ else
+ queryString.AppendLiteral(QUERYKEY_ANNOTATION "=");
+ nsAutoCString annot;
+ query->GetAnnotation(annot);
+ nsAutoCString escaped;
+ bool success = NS_Escape(annot, escaped, url_XAlphas);
+ NS_ENSURE_TRUE(success, NS_ERROR_OUT_OF_MEMORY);
+ queryString.Append(escaped);
+ }
+
+ // folders
+ int64_t *folders = nullptr;
+ uint32_t folderCount = 0;
+ query->GetFolders(&folderCount, &folders);
+ for (uint32_t i = 0; i < folderCount; ++i) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_FOLDER "=");
+ nsresult rv = PlacesFolderConversion::AppendFolder(queryString, folders[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ free(folders);
+
+ // tags
+ const nsTArray<nsString> &tags = query->Tags();
+ for (uint32_t i = 0; i < tags.Length(); ++i) {
+ nsAutoCString escapedTag;
+ if (!NS_Escape(NS_ConvertUTF16toUTF8(tags[i]), escapedTag, url_XAlphas))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_TAG "=");
+ queryString += escapedTag;
+ }
+ AppendBoolKeyValueIfTrue(queryString,
+ NS_LITERAL_CSTRING(QUERYKEY_NOTTAGS),
+ query,
+ &nsINavHistoryQuery::GetTagsAreNot);
+
+ // transitions
+ const nsTArray<uint32_t>& transitions = query->Transitions();
+ for (uint32_t i = 0; i < transitions.Length(); ++i) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_TRANSITION "=");
+ AppendInt64(queryString, transitions[i]);
+ }
+ }
+
+ // sorting
+ if (options->SortingMode() != nsINavHistoryQueryOptions::SORT_BY_NONE) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_SORT "=");
+ AppendInt16(queryString, options->SortingMode());
+ if (options->SortingMode() == nsINavHistoryQueryOptions::SORT_BY_ANNOTATION_DESCENDING ||
+ options->SortingMode() == nsINavHistoryQueryOptions::SORT_BY_ANNOTATION_ASCENDING) {
+ // sortingAnnotation
+ nsAutoCString sortingAnnotation;
+ if (NS_SUCCEEDED(options->GetSortingAnnotation(sortingAnnotation))) {
+ nsCString escaped;
+ if (!NS_Escape(sortingAnnotation, escaped, url_XAlphas))
+ return NS_ERROR_OUT_OF_MEMORY;
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_SORTING_ANNOTATION "=");
+ queryString.Append(escaped);
+ }
+ }
+ }
+
+ // result type
+ if (options->ResultType() != nsINavHistoryQueryOptions::RESULTS_AS_URI) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_RESULT_TYPE "=");
+ AppendInt16(queryString, options->ResultType());
+ }
+
+ // exclude items
+ if (options->ExcludeItems()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_EXCLUDE_ITEMS "=1");
+ }
+
+ // exclude queries
+ if (options->ExcludeQueries()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_EXCLUDE_QUERIES "=1");
+ }
+
+ // exclude read only folders
+ if (options->ExcludeReadOnlyFolders()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_EXCLUDE_READ_ONLY_FOLDERS "=1");
+ }
+
+ // expand queries
+ if (!options->ExpandQueries()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_EXPAND_QUERIES "=0");
+ }
+
+ // include hidden
+ if (options->IncludeHidden()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_INCLUDE_HIDDEN "=1");
+ }
+
+ // max results
+ if (options->MaxResults()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_MAX_RESULTS "=");
+ AppendInt32(queryString, options->MaxResults());
+ }
+
+ // queryType
+ if (options->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_QUERY_TYPE "=");
+ AppendInt16(queryString, options->QueryType());
+ }
+
+ // async enabled
+ if (options->AsyncEnabled()) {
+ AppendAmpersandIfNonempty(queryString);
+ queryString += NS_LITERAL_CSTRING(QUERYKEY_ASYNC_ENABLED "=1");
+ }
+
+ aQueryString.AssignLiteral("place:");
+ aQueryString.Append(queryString);
+ return NS_OK;
+}
+
+
+// TokenizeQueryString
+
+nsresult
+TokenizeQueryString(const nsACString& aQuery,
+ nsTArray<QueryKeyValuePair>* aTokens)
+{
+ // Strip off the "place:" prefix
+ const uint32_t prefixlen = 6; // = strlen("place:");
+ nsCString query;
+ if (aQuery.Length() >= prefixlen &&
+ Substring(aQuery, 0, prefixlen).EqualsLiteral("place:"))
+ query = Substring(aQuery, prefixlen);
+ else
+ query = aQuery;
+
+ int32_t keyFirstIndex = 0;
+ int32_t equalsIndex = 0;
+ for (uint32_t i = 0; i < query.Length(); i ++) {
+ if (query[i] == '&') {
+ // new clause, save last one
+ if (i - keyFirstIndex > 1) {
+ if (! aTokens->AppendElement(QueryKeyValuePair(query, keyFirstIndex,
+ equalsIndex, i)))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ keyFirstIndex = equalsIndex = i + 1;
+ } else if (query[i] == '=') {
+ equalsIndex = i;
+ }
+ }
+
+ // handle last pair, if any
+ if (query.Length() - keyFirstIndex > 1) {
+ if (! aTokens->AppendElement(QueryKeyValuePair(query, keyFirstIndex,
+ equalsIndex, query.Length())))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+// nsNavHistory::TokensToQueries
+
+nsresult
+nsNavHistory::TokensToQueries(const nsTArray<QueryKeyValuePair>& aTokens,
+ nsCOMArray<nsNavHistoryQuery>* aQueries,
+ nsNavHistoryQueryOptions* aOptions)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsNavHistoryQuery> query(new nsNavHistoryQuery());
+ if (! query)
+ return NS_ERROR_OUT_OF_MEMORY;
+ if (! aQueries->AppendObject(query))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ if (aTokens.Length() == 0)
+ return NS_OK; // nothing to do
+
+ nsTArray<int64_t> folders;
+ nsTArray<nsString> tags;
+ nsTArray<uint32_t> transitions;
+ for (uint32_t i = 0; i < aTokens.Length(); i ++) {
+ const QueryKeyValuePair& kvp = aTokens[i];
+
+ // begin time
+ if (kvp.key.EqualsLiteral(QUERYKEY_BEGIN_TIME)) {
+ SetQueryKeyInt64(kvp.value, query, &nsINavHistoryQuery::SetBeginTime);
+
+ // begin time reference
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_BEGIN_TIME_REFERENCE)) {
+ SetQueryKeyUint32(kvp.value, query, &nsINavHistoryQuery::SetBeginTimeReference);
+
+ // end time
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_END_TIME)) {
+ SetQueryKeyInt64(kvp.value, query, &nsINavHistoryQuery::SetEndTime);
+
+ // end time reference
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_END_TIME_REFERENCE)) {
+ SetQueryKeyUint32(kvp.value, query, &nsINavHistoryQuery::SetEndTimeReference);
+
+ // search terms
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_SEARCH_TERMS)) {
+ nsCString unescapedTerms = kvp.value;
+ NS_UnescapeURL(unescapedTerms); // modifies input
+ rv = query->SetSearchTerms(NS_ConvertUTF8toUTF16(unescapedTerms));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // min visits
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_MIN_VISITS)) {
+ int32_t visits = kvp.value.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv))
+ query->SetMinVisits(visits);
+ else
+ NS_WARNING("Bad number for minVisits in query");
+
+ // max visits
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_MAX_VISITS)) {
+ int32_t visits = kvp.value.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv))
+ query->SetMaxVisits(visits);
+ else
+ NS_WARNING("Bad number for maxVisits in query");
+
+ // onlyBookmarked flag
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_ONLY_BOOKMARKED)) {
+ SetQueryKeyBool(kvp.value, query, &nsINavHistoryQuery::SetOnlyBookmarked);
+
+ // domainIsHost flag
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_DOMAIN_IS_HOST)) {
+ SetQueryKeyBool(kvp.value, query, &nsINavHistoryQuery::SetDomainIsHost);
+
+ // domain string
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_DOMAIN)) {
+ nsAutoCString unescapedDomain(kvp.value);
+ NS_UnescapeURL(unescapedDomain); // modifies input
+ rv = query->SetDomain(unescapedDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // folders
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_FOLDER)) {
+ int64_t folder;
+ if (PR_sscanf(kvp.value.get(), "%lld", &folder) == 1) {
+ NS_ENSURE_TRUE(folders.AppendElement(folder), NS_ERROR_OUT_OF_MEMORY);
+ } else {
+ folder = PlacesFolderConversion::DecodeFolder(kvp.value);
+ if (folder != -1)
+ NS_ENSURE_TRUE(folders.AppendElement(folder), NS_ERROR_OUT_OF_MEMORY);
+ else
+ NS_WARNING("folders value in query is invalid, ignoring");
+ }
+
+ // uri
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_URI)) {
+ nsAutoCString unescapedUri(kvp.value);
+ NS_UnescapeURL(unescapedUri); // modifies input
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), unescapedUri);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Unable to parse URI");
+ }
+ rv = query->SetUri(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // not annotation
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_NOTANNOTATION)) {
+ nsAutoCString unescaped(kvp.value);
+ NS_UnescapeURL(unescaped); // modifies input
+ query->SetAnnotationIsNot(true);
+ query->SetAnnotation(unescaped);
+
+ // annotation
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_ANNOTATION)) {
+ nsAutoCString unescaped(kvp.value);
+ NS_UnescapeURL(unescaped); // modifies input
+ query->SetAnnotationIsNot(false);
+ query->SetAnnotation(unescaped);
+
+ // tag
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_TAG)) {
+ nsAutoCString unescaped(kvp.value);
+ NS_UnescapeURL(unescaped); // modifies input
+ NS_ConvertUTF8toUTF16 tag(unescaped);
+ if (!tags.Contains(tag)) {
+ NS_ENSURE_TRUE(tags.AppendElement(tag), NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // not tags
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_NOTTAGS)) {
+ SetQueryKeyBool(kvp.value, query, &nsINavHistoryQuery::SetTagsAreNot);
+
+ // transition
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_TRANSITION)) {
+ uint32_t transition = kvp.value.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) {
+ if (!transitions.Contains(transition))
+ NS_ENSURE_TRUE(transitions.AppendElement(transition),
+ NS_ERROR_OUT_OF_MEMORY);
+ }
+ else {
+ NS_WARNING("Invalid Int32 transition value.");
+ }
+
+ // new query component
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_SEPARATOR)) {
+
+ if (folders.Length() != 0) {
+ query->SetFolders(folders.Elements(), folders.Length());
+ folders.Clear();
+ }
+
+ if (tags.Length() > 0) {
+ rv = query->SetTags(tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ tags.Clear();
+ }
+
+ if (transitions.Length() > 0) {
+ rv = query->SetTransitions(transitions);
+ NS_ENSURE_SUCCESS(rv, rv);
+ transitions.Clear();
+ }
+
+ query = new nsNavHistoryQuery();
+ if (! query)
+ return NS_ERROR_OUT_OF_MEMORY;
+ if (! aQueries->AppendObject(query))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // sorting mode
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_SORT)) {
+ SetOptionsKeyUint16(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetSortingMode);
+ // sorting annotation
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_SORTING_ANNOTATION)) {
+ nsCString sortingAnnotation = kvp.value;
+ NS_UnescapeURL(sortingAnnotation);
+ rv = aOptions->SetSortingAnnotation(sortingAnnotation);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // result type
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_RESULT_TYPE)) {
+ SetOptionsKeyUint16(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetResultType);
+
+ // exclude items
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_EXCLUDE_ITEMS)) {
+ SetOptionsKeyBool(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetExcludeItems);
+
+ // exclude queries
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_EXCLUDE_QUERIES)) {
+ SetOptionsKeyBool(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetExcludeQueries);
+
+ // exclude read only folders
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_EXCLUDE_READ_ONLY_FOLDERS)) {
+ SetOptionsKeyBool(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetExcludeReadOnlyFolders);
+
+ // expand queries
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_EXPAND_QUERIES)) {
+ SetOptionsKeyBool(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetExpandQueries);
+ // include hidden
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_INCLUDE_HIDDEN)) {
+ SetOptionsKeyBool(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetIncludeHidden);
+ // max results
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_MAX_RESULTS)) {
+ SetOptionsKeyUint32(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetMaxResults);
+ // query type
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_QUERY_TYPE)) {
+ SetOptionsKeyUint16(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetQueryType);
+ // async enabled
+ } else if (kvp.key.EqualsLiteral(QUERYKEY_ASYNC_ENABLED)) {
+ SetOptionsKeyBool(kvp.value, aOptions,
+ &nsINavHistoryQueryOptions::SetAsyncEnabled);
+ // unknown key
+ } else {
+ NS_WARNING("TokensToQueries(), ignoring unknown key: ");
+ NS_WARNING(kvp.key.get());
+ }
+ }
+
+ if (folders.Length() != 0)
+ query->SetFolders(folders.Elements(), folders.Length());
+
+ if (tags.Length() > 0) {
+ rv = query->SetTags(tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (transitions.Length() > 0) {
+ rv = query->SetTransitions(transitions);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+// ParseQueryBooleanString
+//
+// Converts a 0/1 or true/false string into a bool
+
+nsresult
+ParseQueryBooleanString(const nsCString& aString, bool* aValue)
+{
+ if (aString.EqualsLiteral("1") || aString.EqualsLiteral("true")) {
+ *aValue = true;
+ return NS_OK;
+ } else if (aString.EqualsLiteral("0") || aString.EqualsLiteral("false")) {
+ *aValue = false;
+ return NS_OK;
+ }
+ return NS_ERROR_INVALID_ARG;
+}
+
+
+// nsINavHistoryQuery **********************************************************
+
+NS_IMPL_ISUPPORTS(nsNavHistoryQuery, nsNavHistoryQuery, nsINavHistoryQuery)
+
+// nsINavHistoryQuery::nsNavHistoryQuery
+//
+// This must initialize the object such that the default values will cause
+// all history to be returned if this query is used. Then the caller can
+// just set the things it's interested in.
+
+nsNavHistoryQuery::nsNavHistoryQuery()
+ : mMinVisits(-1), mMaxVisits(-1), mBeginTime(0),
+ mBeginTimeReference(TIME_RELATIVE_EPOCH),
+ mEndTime(0), mEndTimeReference(TIME_RELATIVE_EPOCH),
+ mOnlyBookmarked(false),
+ mDomainIsHost(false),
+ mAnnotationIsNot(false),
+ mTagsAreNot(false)
+{
+ // differentiate not set (IsVoid) from empty string (local files)
+ mDomain.SetIsVoid(true);
+}
+
+nsNavHistoryQuery::nsNavHistoryQuery(const nsNavHistoryQuery& aOther)
+ : mMinVisits(aOther.mMinVisits), mMaxVisits(aOther.mMaxVisits),
+ mBeginTime(aOther.mBeginTime),
+ mBeginTimeReference(aOther.mBeginTimeReference),
+ mEndTime(aOther.mEndTime), mEndTimeReference(aOther.mEndTimeReference),
+ mSearchTerms(aOther.mSearchTerms), mOnlyBookmarked(aOther.mOnlyBookmarked),
+ mDomainIsHost(aOther.mDomainIsHost), mDomain(aOther.mDomain),
+ mUri(aOther.mUri),
+ mAnnotationIsNot(aOther.mAnnotationIsNot),
+ mAnnotation(aOther.mAnnotation), mTags(aOther.mTags),
+ mTagsAreNot(aOther.mTagsAreNot), mTransitions(aOther.mTransitions)
+{}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetBeginTime(PRTime *aBeginTime)
+{
+ *aBeginTime = mBeginTime;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetBeginTime(PRTime aBeginTime)
+{
+ mBeginTime = aBeginTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetBeginTimeReference(uint32_t* _retval)
+{
+ *_retval = mBeginTimeReference;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetBeginTimeReference(uint32_t aReference)
+{
+ if (aReference > TIME_RELATIVE_NOW)
+ return NS_ERROR_INVALID_ARG;
+ mBeginTimeReference = aReference;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetHasBeginTime(bool* _retval)
+{
+ *_retval = ! (mBeginTimeReference == TIME_RELATIVE_EPOCH && mBeginTime == 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetAbsoluteBeginTime(PRTime* _retval)
+{
+ *_retval = nsNavHistory::NormalizeTime(mBeginTimeReference, mBeginTime);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetEndTime(PRTime *aEndTime)
+{
+ *aEndTime = mEndTime;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetEndTime(PRTime aEndTime)
+{
+ mEndTime = aEndTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetEndTimeReference(uint32_t* _retval)
+{
+ *_retval = mEndTimeReference;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetEndTimeReference(uint32_t aReference)
+{
+ if (aReference > TIME_RELATIVE_NOW)
+ return NS_ERROR_INVALID_ARG;
+ mEndTimeReference = aReference;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetHasEndTime(bool* _retval)
+{
+ *_retval = ! (mEndTimeReference == TIME_RELATIVE_EPOCH && mEndTime == 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetAbsoluteEndTime(PRTime* _retval)
+{
+ *_retval = nsNavHistory::NormalizeTime(mEndTimeReference, mEndTime);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetSearchTerms(nsAString& aSearchTerms)
+{
+ aSearchTerms = mSearchTerms;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetSearchTerms(const nsAString& aSearchTerms)
+{
+ mSearchTerms = aSearchTerms;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::GetHasSearchTerms(bool* _retval)
+{
+ *_retval = (! mSearchTerms.IsEmpty());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetMinVisits(int32_t* _retval)
+{
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mMinVisits;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetMinVisits(int32_t aVisits)
+{
+ mMinVisits = aVisits;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetMaxVisits(int32_t* _retval)
+{
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mMaxVisits;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetMaxVisits(int32_t aVisits)
+{
+ mMaxVisits = aVisits;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetOnlyBookmarked(bool *aOnlyBookmarked)
+{
+ *aOnlyBookmarked = mOnlyBookmarked;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetOnlyBookmarked(bool aOnlyBookmarked)
+{
+ mOnlyBookmarked = aOnlyBookmarked;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetDomainIsHost(bool *aDomainIsHost)
+{
+ *aDomainIsHost = mDomainIsHost;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetDomainIsHost(bool aDomainIsHost)
+{
+ mDomainIsHost = aDomainIsHost;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetDomain(nsACString& aDomain)
+{
+ aDomain = mDomain;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetDomain(const nsACString& aDomain)
+{
+ mDomain = aDomain;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::GetHasDomain(bool* _retval)
+{
+ // note that empty but not void is still a valid query (local files)
+ *_retval = (! mDomain.IsVoid());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetUri(nsIURI** aUri)
+{
+ NS_IF_ADDREF(*aUri = mUri);
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetUri(nsIURI* aUri)
+{
+ mUri = aUri;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::GetHasUri(bool* aHasUri)
+{
+ *aHasUri = (mUri != nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetAnnotationIsNot(bool* aIsNot)
+{
+ *aIsNot = mAnnotationIsNot;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetAnnotationIsNot(bool aIsNot)
+{
+ mAnnotationIsNot = aIsNot;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetAnnotation(nsACString& aAnnotation)
+{
+ aAnnotation = mAnnotation;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::SetAnnotation(const nsACString& aAnnotation)
+{
+ mAnnotation = aAnnotation;
+ return NS_OK;
+}
+NS_IMETHODIMP nsNavHistoryQuery::GetHasAnnotation(bool* aHasIt)
+{
+ *aHasIt = ! mAnnotation.IsEmpty();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetTags(nsIVariant **aTags)
+{
+ NS_ENSURE_ARG_POINTER(aTags);
+
+ RefPtr<nsVariant> out = new nsVariant();
+
+ uint32_t arrayLen = mTags.Length();
+
+ nsresult rv;
+ if (arrayLen == 0)
+ rv = out->SetAsEmptyArray();
+ else {
+ // Note: The resulting nsIVariant dupes both the array and its elements.
+ const char16_t **array = reinterpret_cast<const char16_t **>
+ (moz_xmalloc(arrayLen * sizeof(char16_t *)));
+ NS_ENSURE_TRUE(array, NS_ERROR_OUT_OF_MEMORY);
+
+ for (uint32_t i = 0; i < arrayLen; ++i) {
+ array[i] = mTags[i].get();
+ }
+
+ rv = out->SetAsArray(nsIDataType::VTYPE_WCHAR_STR,
+ nullptr,
+ arrayLen,
+ reinterpret_cast<void *>(array));
+ free(array);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ out.forget(aTags);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::SetTags(nsIVariant *aTags)
+{
+ NS_ENSURE_ARG(aTags);
+
+ uint16_t dataType;
+ aTags->GetDataType(&dataType);
+
+ // Caller passed in empty array. Easy -- clear our mTags array and return.
+ if (dataType == nsIDataType::VTYPE_EMPTY_ARRAY) {
+ mTags.Clear();
+ return NS_OK;
+ }
+
+ // Before we go any further, make sure caller passed in an array.
+ NS_ENSURE_TRUE(dataType == nsIDataType::VTYPE_ARRAY, NS_ERROR_ILLEGAL_VALUE);
+
+ uint16_t eltType;
+ nsIID eltIID;
+ uint32_t arrayLen;
+ void *array;
+
+ // Convert the nsIVariant to an array. We own the resulting buffer and its
+ // elements.
+ nsresult rv = aTags->GetAsArray(&eltType, &eltIID, &arrayLen, &array);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If element type is not wstring, thanks a lot. Your memory die now.
+ if (eltType != nsIDataType::VTYPE_WCHAR_STR) {
+ switch (eltType) {
+ case nsIDataType::VTYPE_ID:
+ case nsIDataType::VTYPE_CHAR_STR:
+ {
+ char **charArray = reinterpret_cast<char **>(array);
+ for (uint32_t i = 0; i < arrayLen; ++i) {
+ if (charArray[i])
+ free(charArray[i]);
+ }
+ }
+ break;
+ case nsIDataType::VTYPE_INTERFACE:
+ case nsIDataType::VTYPE_INTERFACE_IS:
+ {
+ nsISupports **supportsArray = reinterpret_cast<nsISupports **>(array);
+ for (uint32_t i = 0; i < arrayLen; ++i) {
+ NS_IF_RELEASE(supportsArray[i]);
+ }
+ }
+ break;
+ // The other types are primitives that do not need to be freed.
+ }
+ free(array);
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ char16_t **tags = reinterpret_cast<char16_t **>(array);
+ mTags.Clear();
+
+ // Finally, add each passed-in tag to our mTags array and then sort it.
+ for (uint32_t i = 0; i < arrayLen; ++i) {
+
+ // Don't allow nulls.
+ if (!tags[i]) {
+ free(tags);
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ nsDependentString tag(tags[i]);
+
+ // Don't store duplicate tags. This isn't just to save memory or to be
+ // fancy; the SQL that's built from the tags relies on no dupes.
+ if (!mTags.Contains(tag)) {
+ if (!mTags.AppendElement(tag)) {
+ free(tags[i]);
+ free(tags);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ free(tags[i]);
+ }
+ free(tags);
+
+ mTags.Sort();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetTagsAreNot(bool *aTagsAreNot)
+{
+ NS_ENSURE_ARG_POINTER(aTagsAreNot);
+ *aTagsAreNot = mTagsAreNot;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::SetTagsAreNot(bool aTagsAreNot)
+{
+ mTagsAreNot = aTagsAreNot;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetFolders(uint32_t *aCount,
+ int64_t **aFolders)
+{
+ uint32_t count = mFolders.Length();
+ int64_t *folders = nullptr;
+ if (count > 0) {
+ folders = static_cast<int64_t*>
+ (moz_xmalloc(count * sizeof(int64_t)));
+ NS_ENSURE_TRUE(folders, NS_ERROR_OUT_OF_MEMORY);
+
+ for (uint32_t i = 0; i < count; ++i) {
+ folders[i] = mFolders[i];
+ }
+ }
+ *aCount = count;
+ *aFolders = folders;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetFolderCount(uint32_t *aCount)
+{
+ *aCount = mFolders.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::SetFolders(const int64_t *aFolders,
+ uint32_t aFolderCount)
+{
+ if (!mFolders.ReplaceElementsAt(0, mFolders.Length(),
+ aFolders, aFolderCount)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetTransitions(uint32_t* aCount,
+ uint32_t** aTransitions)
+{
+ uint32_t count = mTransitions.Length();
+ uint32_t* transitions = nullptr;
+ if (count > 0) {
+ transitions = reinterpret_cast<uint32_t*>
+ (moz_xmalloc(count * sizeof(uint32_t)));
+ NS_ENSURE_TRUE(transitions, NS_ERROR_OUT_OF_MEMORY);
+ for (uint32_t i = 0; i < count; ++i) {
+ transitions[i] = mTransitions[i];
+ }
+ }
+ *aCount = count;
+ *aTransitions = transitions;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::GetTransitionCount(uint32_t* aCount)
+{
+ *aCount = mTransitions.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::SetTransitions(const uint32_t* aTransitions,
+ uint32_t aCount)
+{
+ if (!mTransitions.ReplaceElementsAt(0, mTransitions.Length(), aTransitions,
+ aCount))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNavHistoryQuery::Clone(nsINavHistoryQuery** _retval)
+{
+ *_retval = nullptr;
+
+ RefPtr<nsNavHistoryQuery> clone = new nsNavHistoryQuery(*this);
+ NS_ENSURE_TRUE(clone, NS_ERROR_OUT_OF_MEMORY);
+
+ clone.forget(_retval);
+ return NS_OK;
+}
+
+
+// nsNavHistoryQueryOptions
+NS_IMPL_ISUPPORTS(nsNavHistoryQueryOptions, nsNavHistoryQueryOptions, nsINavHistoryQueryOptions)
+
+// sortingMode
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetSortingMode(uint16_t* aMode)
+{
+ *aMode = mSort;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetSortingMode(uint16_t aMode)
+{
+ if (aMode > SORT_BY_FRECENCY_DESCENDING)
+ return NS_ERROR_INVALID_ARG;
+ mSort = aMode;
+ return NS_OK;
+}
+
+// sortingAnnotation
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetSortingAnnotation(nsACString& _result) {
+ _result.Assign(mSortingAnnotation);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetSortingAnnotation(const nsACString& aSortingAnnotation) {
+ mSortingAnnotation.Assign(aSortingAnnotation);
+ return NS_OK;
+}
+
+// resultType
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetResultType(uint16_t* aType)
+{
+ *aType = mResultType;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetResultType(uint16_t aType)
+{
+ if (aType > RESULTS_AS_TAG_CONTENTS)
+ return NS_ERROR_INVALID_ARG;
+ // Tag queries and containers are bookmarks related, so we set the QueryType
+ // accordingly.
+ if (aType == RESULTS_AS_TAG_QUERY || aType == RESULTS_AS_TAG_CONTENTS)
+ mQueryType = QUERY_TYPE_BOOKMARKS;
+ mResultType = aType;
+ return NS_OK;
+}
+
+// excludeItems
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetExcludeItems(bool* aExclude)
+{
+ *aExclude = mExcludeItems;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetExcludeItems(bool aExclude)
+{
+ mExcludeItems = aExclude;
+ return NS_OK;
+}
+
+// excludeQueries
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetExcludeQueries(bool* aExclude)
+{
+ *aExclude = mExcludeQueries;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetExcludeQueries(bool aExclude)
+{
+ mExcludeQueries = aExclude;
+ return NS_OK;
+}
+
+// excludeReadOnlyFolders
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetExcludeReadOnlyFolders(bool* aExclude)
+{
+ *aExclude = mExcludeReadOnlyFolders;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetExcludeReadOnlyFolders(bool aExclude)
+{
+ mExcludeReadOnlyFolders = aExclude;
+ return NS_OK;
+}
+
+// expandQueries
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetExpandQueries(bool* aExpand)
+{
+ *aExpand = mExpandQueries;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetExpandQueries(bool aExpand)
+{
+ mExpandQueries = aExpand;
+ return NS_OK;
+}
+
+// includeHidden
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetIncludeHidden(bool* aIncludeHidden)
+{
+ *aIncludeHidden = mIncludeHidden;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetIncludeHidden(bool aIncludeHidden)
+{
+ mIncludeHidden = aIncludeHidden;
+ return NS_OK;
+}
+
+// maxResults
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetMaxResults(uint32_t* aMaxResults)
+{
+ *aMaxResults = mMaxResults;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetMaxResults(uint32_t aMaxResults)
+{
+ mMaxResults = aMaxResults;
+ return NS_OK;
+}
+
+// queryType
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetQueryType(uint16_t* _retval)
+{
+ *_retval = mQueryType;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetQueryType(uint16_t aQueryType)
+{
+ // Tag query and containers are forced to QUERY_TYPE_BOOKMARKS when the
+ // resultType is set.
+ if (mResultType == RESULTS_AS_TAG_CONTENTS ||
+ mResultType == RESULTS_AS_TAG_QUERY)
+ return NS_OK;
+ mQueryType = aQueryType;
+ return NS_OK;
+}
+
+// asyncEnabled
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::GetAsyncEnabled(bool* _asyncEnabled)
+{
+ *_asyncEnabled = mAsyncEnabled;
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::SetAsyncEnabled(bool aAsyncEnabled)
+{
+ mAsyncEnabled = aAsyncEnabled;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryOptions::Clone(nsINavHistoryQueryOptions** aResult)
+{
+ nsNavHistoryQueryOptions *clone = nullptr;
+ nsresult rv = Clone(&clone);
+ *aResult = clone;
+ return rv;
+}
+
+nsresult
+nsNavHistoryQueryOptions::Clone(nsNavHistoryQueryOptions **aResult)
+{
+ *aResult = nullptr;
+ nsNavHistoryQueryOptions *result = new nsNavHistoryQueryOptions();
+
+ RefPtr<nsNavHistoryQueryOptions> resultHolder(result);
+ result->mSort = mSort;
+ result->mResultType = mResultType;
+ result->mExcludeItems = mExcludeItems;
+ result->mExcludeQueries = mExcludeQueries;
+ result->mExpandQueries = mExpandQueries;
+ result->mMaxResults = mMaxResults;
+ result->mQueryType = mQueryType;
+ result->mParentAnnotationToExclude = mParentAnnotationToExclude;
+ result->mAsyncEnabled = mAsyncEnabled;
+
+ resultHolder.forget(aResult);
+ return NS_OK;
+}
+
+
+// AppendBoolKeyValueIfTrue
+
+void // static
+AppendBoolKeyValueIfTrue(nsACString& aString, const nsCString& aName,
+ nsINavHistoryQuery* aQuery,
+ BoolQueryGetter getter)
+{
+ bool value;
+ DebugOnly<nsresult> rv = (aQuery->*getter)(&value);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failure getting boolean value");
+ if (value) {
+ AppendAmpersandIfNonempty(aString);
+ aString += aName;
+ aString.AppendLiteral("=1");
+ }
+}
+
+
+// AppendUint32KeyValueIfNonzero
+
+void // static
+AppendUint32KeyValueIfNonzero(nsACString& aString,
+ const nsCString& aName,
+ nsINavHistoryQuery* aQuery,
+ Uint32QueryGetter getter)
+{
+ uint32_t value;
+ DebugOnly<nsresult> rv = (aQuery->*getter)(&value);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failure getting value");
+ if (value) {
+ AppendAmpersandIfNonempty(aString);
+ aString += aName;
+
+ // AppendInt requires a concrete string
+ nsAutoCString appendMe("=");
+ appendMe.AppendInt(value);
+ aString.Append(appendMe);
+ }
+}
+
+
+// AppendInt64KeyValueIfNonzero
+
+void // static
+AppendInt64KeyValueIfNonzero(nsACString& aString,
+ const nsCString& aName,
+ nsINavHistoryQuery* aQuery,
+ Int64QueryGetter getter)
+{
+ PRTime value;
+ DebugOnly<nsresult> rv = (aQuery->*getter)(&value);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failure getting value");
+ if (value) {
+ AppendAmpersandIfNonempty(aString);
+ aString += aName;
+ nsAutoCString appendMe("=");
+ appendMe.AppendInt(static_cast<int64_t>(value));
+ aString.Append(appendMe);
+ }
+}
+
+
+// SetQuery/OptionsKeyBool
+
+void // static
+SetQueryKeyBool(const nsCString& aValue, nsINavHistoryQuery* aQuery,
+ BoolQuerySetter setter)
+{
+ bool value;
+ nsresult rv = ParseQueryBooleanString(aValue, &value);
+ if (NS_SUCCEEDED(rv)) {
+ rv = (aQuery->*setter)(value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error setting boolean key value");
+ }
+ } else {
+ NS_WARNING("Invalid boolean key value in query string.");
+ }
+}
+void // static
+SetOptionsKeyBool(const nsCString& aValue, nsINavHistoryQueryOptions* aOptions,
+ BoolOptionsSetter setter)
+{
+ bool value;
+ nsresult rv = ParseQueryBooleanString(aValue, &value);
+ if (NS_SUCCEEDED(rv)) {
+ rv = (aOptions->*setter)(value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error setting boolean key value");
+ }
+ } else {
+ NS_WARNING("Invalid boolean key value in query string.");
+ }
+}
+
+
+// SetQuery/OptionsKeyUint32
+
+void // static
+SetQueryKeyUint32(const nsCString& aValue, nsINavHistoryQuery* aQuery,
+ Uint32QuerySetter setter)
+{
+ nsresult rv;
+ uint32_t value = aValue.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = (aQuery->*setter)(value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error setting Int32 key value");
+ }
+ } else {
+ NS_WARNING("Invalid Int32 key value in query string.");
+ }
+}
+void // static
+SetOptionsKeyUint32(const nsCString& aValue, nsINavHistoryQueryOptions* aOptions,
+ Uint32OptionsSetter setter)
+{
+ nsresult rv;
+ uint32_t value = aValue.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = (aOptions->*setter)(value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error setting Int32 key value");
+ }
+ } else {
+ NS_WARNING("Invalid Int32 key value in query string.");
+ }
+}
+
+void // static
+SetOptionsKeyUint16(const nsCString& aValue, nsINavHistoryQueryOptions* aOptions,
+ Uint16OptionsSetter setter)
+{
+ nsresult rv;
+ uint16_t value = static_cast<uint16_t>(aValue.ToInteger(&rv));
+ if (NS_SUCCEEDED(rv)) {
+ rv = (aOptions->*setter)(value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error setting Int16 key value");
+ }
+ } else {
+ NS_WARNING("Invalid Int16 key value in query string.");
+ }
+}
+
+
+// SetQueryKeyInt64
+
+void SetQueryKeyInt64(const nsCString& aValue, nsINavHistoryQuery* aQuery,
+ Int64QuerySetter setter)
+{
+ nsresult rv;
+ int64_t value;
+ if (PR_sscanf(aValue.get(), "%lld", &value) == 1) {
+ rv = (aQuery->*setter)(value);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Error setting Int64 key value");
+ }
+ } else {
+ NS_WARNING("Invalid Int64 value in query string.");
+ }
+}
diff --git a/toolkit/components/places/nsNavHistoryQuery.h b/toolkit/components/places/nsNavHistoryQuery.h
new file mode 100644
index 0000000000..d1a8b759a1
--- /dev/null
+++ b/toolkit/components/places/nsNavHistoryQuery.h
@@ -0,0 +1,160 @@
+/* -*- 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/. */
+
+/**
+ * The definitions of nsNavHistoryQuery and nsNavHistoryQueryOptions. This
+ * header file should only be included from nsNavHistory.h, include that if
+ * you want these classes.
+ */
+
+#ifndef nsNavHistoryQuery_h_
+#define nsNavHistoryQuery_h_
+
+// nsNavHistoryQuery
+//
+// This class encapsulates the parameters for basic history queries for
+// building UI, trees, lists, etc.
+
+#include "mozilla/Attributes.h"
+
+#define NS_NAVHISTORYQUERY_IID \
+{ 0xb10185e0, 0x86eb, 0x4612, { 0x95, 0x7c, 0x09, 0x34, 0xf2, 0xb1, 0xce, 0xd7 } }
+
+class nsNavHistoryQuery final : public nsINavHistoryQuery
+{
+public:
+ nsNavHistoryQuery();
+ nsNavHistoryQuery(const nsNavHistoryQuery& aOther);
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_NAVHISTORYQUERY_IID)
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINAVHISTORYQUERY
+
+ int32_t MinVisits() { return mMinVisits; }
+ int32_t MaxVisits() { return mMaxVisits; }
+ PRTime BeginTime() { return mBeginTime; }
+ uint32_t BeginTimeReference() { return mBeginTimeReference; }
+ PRTime EndTime() { return mEndTime; }
+ uint32_t EndTimeReference() { return mEndTimeReference; }
+ const nsString& SearchTerms() { return mSearchTerms; }
+ bool OnlyBookmarked() { return mOnlyBookmarked; }
+ bool DomainIsHost() { return mDomainIsHost; }
+ const nsCString& Domain() { return mDomain; }
+ nsIURI* Uri() { return mUri; } // NOT AddRef-ed!
+ bool AnnotationIsNot() { return mAnnotationIsNot; }
+ const nsCString& Annotation() { return mAnnotation; }
+ const nsTArray<int64_t>& Folders() const { return mFolders; }
+ const nsTArray<nsString>& Tags() const { return mTags; }
+ nsresult SetTags(const nsTArray<nsString>& aTags)
+ {
+ if (!mTags.ReplaceElementsAt(0, mTags.Length(), aTags))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+ }
+ bool TagsAreNot() { return mTagsAreNot; }
+
+ const nsTArray<uint32_t>& Transitions() const { return mTransitions; }
+ nsresult SetTransitions(const nsTArray<uint32_t>& aTransitions)
+ {
+ if (!mTransitions.ReplaceElementsAt(0, mTransitions.Length(),
+ aTransitions))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+ }
+
+private:
+ ~nsNavHistoryQuery() {}
+
+protected:
+
+ int32_t mMinVisits;
+ int32_t mMaxVisits;
+ PRTime mBeginTime;
+ uint32_t mBeginTimeReference;
+ PRTime mEndTime;
+ uint32_t mEndTimeReference;
+ nsString mSearchTerms;
+ bool mOnlyBookmarked;
+ bool mDomainIsHost;
+ nsCString mDomain; // Default is IsVoid, empty string is valid query
+ nsCOMPtr<nsIURI> mUri;
+ bool mAnnotationIsNot;
+ nsCString mAnnotation;
+ nsTArray<int64_t> mFolders;
+ nsTArray<nsString> mTags;
+ bool mTagsAreNot;
+ nsTArray<uint32_t> mTransitions;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsNavHistoryQuery, NS_NAVHISTORYQUERY_IID)
+
+// nsNavHistoryQueryOptions
+
+#define NS_NAVHISTORYQUERYOPTIONS_IID \
+{0x95f8ba3b, 0xd681, 0x4d89, {0xab, 0xd1, 0xfd, 0xae, 0xf2, 0xa3, 0xde, 0x18}}
+
+class nsNavHistoryQueryOptions final : public nsINavHistoryQueryOptions
+{
+public:
+ nsNavHistoryQueryOptions()
+ : mSort(0)
+ , mResultType(0)
+ , mExcludeItems(false)
+ , mExcludeQueries(false)
+ , mExcludeReadOnlyFolders(false)
+ , mExpandQueries(true)
+ , mIncludeHidden(false)
+ , mMaxResults(0)
+ , mQueryType(nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY)
+ , mAsyncEnabled(false)
+ { }
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_NAVHISTORYQUERYOPTIONS_IID)
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINAVHISTORYQUERYOPTIONS
+
+ uint16_t SortingMode() const { return mSort; }
+ uint16_t ResultType() const { return mResultType; }
+ bool ExcludeItems() const { return mExcludeItems; }
+ bool ExcludeQueries() const { return mExcludeQueries; }
+ bool ExcludeReadOnlyFolders() const { return mExcludeReadOnlyFolders; }
+ bool ExpandQueries() const { return mExpandQueries; }
+ bool IncludeHidden() const { return mIncludeHidden; }
+ uint32_t MaxResults() const { return mMaxResults; }
+ uint16_t QueryType() const { return mQueryType; }
+ bool AsyncEnabled() const { return mAsyncEnabled; }
+
+ nsresult Clone(nsNavHistoryQueryOptions **aResult);
+
+private:
+ ~nsNavHistoryQueryOptions() {}
+ nsNavHistoryQueryOptions(const nsNavHistoryQueryOptions& other) {} // no copy
+
+ // IF YOU ADD MORE ITEMS:
+ // * Add a new getter for C++ above if it makes sense
+ // * Add to the serialization code (see nsNavHistory::QueriesToQueryString())
+ // * Add to the deserialization code (see nsNavHistory::QueryStringToQueries)
+ // * Add to the nsNavHistoryQueryOptions::Clone() function
+ // * Add to the nsNavHistory.cpp::GetSimpleBookmarksQueryFolder function if applicable
+ uint16_t mSort;
+ nsCString mSortingAnnotation;
+ nsCString mParentAnnotationToExclude;
+ uint16_t mResultType;
+ bool mExcludeItems;
+ bool mExcludeQueries;
+ bool mExcludeReadOnlyFolders;
+ bool mExpandQueries;
+ bool mIncludeHidden;
+ uint32_t mMaxResults;
+ uint16_t mQueryType;
+ bool mAsyncEnabled;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsNavHistoryQueryOptions, NS_NAVHISTORYQUERYOPTIONS_IID)
+
+#endif // nsNavHistoryQuery_h_
diff --git a/toolkit/components/places/nsNavHistoryResult.cpp b/toolkit/components/places/nsNavHistoryResult.cpp
new file mode 100644
index 0000000000..7cd8c66cc3
--- /dev/null
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -0,0 +1,4813 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <stdio.h>
+#include "nsNavHistory.h"
+#include "nsNavBookmarks.h"
+#include "nsFaviconService.h"
+#include "nsITaggingService.h"
+#include "nsAnnotationService.h"
+#include "Helpers.h"
+#include "mozilla/DebugOnly.h"
+#include "nsDebug.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "prtime.h"
+#include "prprf.h"
+#include "nsQueryObject.h"
+
+#include "nsCycleCollectionParticipant.h"
+
+// Thanks, Windows.h :(
+#undef CompareString
+
+#define TO_ICONTAINER(_node) \
+ static_cast<nsINavHistoryContainerResultNode*>(_node)
+
+#define TO_CONTAINER(_node) \
+ static_cast<nsNavHistoryContainerResultNode*>(_node)
+
+#define NOTIFY_RESULT_OBSERVERS_RET(_result, _method, _ret) \
+ PR_BEGIN_MACRO \
+ NS_ENSURE_TRUE(_result, _ret); \
+ if (!_result->mSuppressNotifications) { \
+ ENUMERATE_WEAKARRAY(_result->mObservers, nsINavHistoryResultObserver, \
+ _method) \
+ } \
+ PR_END_MACRO
+
+#define NOTIFY_RESULT_OBSERVERS(_result, _method) \
+ NOTIFY_RESULT_OBSERVERS_RET(_result, _method, NS_ERROR_UNEXPECTED)
+
+// What we want is: NS_INTERFACE_MAP_ENTRY(self) for static IID accessors,
+// but some of our classes (like nsNavHistoryResult) have an ambiguous base
+// class of nsISupports which prevents this from working (the default macro
+// converts it to nsISupports, then addrefs it, then returns it). Therefore, we
+// expand the macro here and change it so that it works. Yuck.
+#define NS_INTERFACE_MAP_STATIC_AMBIGUOUS(_class) \
+ if (aIID.Equals(NS_GET_IID(_class))) { \
+ NS_ADDREF(this); \
+ *aInstancePtr = this; \
+ return NS_OK; \
+ } else
+
+// Number of changes to handle separately in a batch. If more changes are
+// requested the node will switch to full refresh mode.
+#define MAX_BATCH_CHANGES_BEFORE_REFRESH 5
+
+// Emulate string comparison (used for sorting) for PRTime and int.
+inline int32_t ComparePRTime(PRTime a, PRTime b)
+{
+ if (a < b)
+ return -1;
+ else if (a > b)
+ return 1;
+ return 0;
+}
+inline int32_t CompareIntegers(uint32_t a, uint32_t b)
+{
+ return a - b;
+}
+
+using namespace mozilla;
+using namespace mozilla::places;
+
+NS_IMPL_CYCLE_COLLECTION(nsNavHistoryResultNode, mParent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsNavHistoryResultNode)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryResultNode)
+ NS_INTERFACE_MAP_ENTRY(nsINavHistoryResultNode)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsNavHistoryResultNode)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsNavHistoryResultNode)
+
+nsNavHistoryResultNode::nsNavHistoryResultNode(
+ const nsACString& aURI, const nsACString& aTitle, uint32_t aAccessCount,
+ PRTime aTime, const nsACString& aIconURI) :
+ mParent(nullptr),
+ mURI(aURI),
+ mTitle(aTitle),
+ mAreTagsSorted(false),
+ mAccessCount(aAccessCount),
+ mTime(aTime),
+ mFaviconURI(aIconURI),
+ mBookmarkIndex(-1),
+ mItemId(-1),
+ mFolderId(-1),
+ mVisitId(-1),
+ mFromVisitId(-1),
+ mDateAdded(0),
+ mLastModified(0),
+ mIndentLevel(-1),
+ mFrecency(0),
+ mHidden(false),
+ mTransitionType(0)
+{
+ mTags.SetIsVoid(true);
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetIcon(nsACString& aIcon)
+{
+ if (mFaviconURI.IsEmpty()) {
+ aIcon.Truncate();
+ return NS_OK;
+ }
+
+ nsFaviconService* faviconService = nsFaviconService::GetFaviconService();
+ NS_ENSURE_TRUE(faviconService, NS_ERROR_OUT_OF_MEMORY);
+ faviconService->GetFaviconSpecForIconString(mFaviconURI, aIcon);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetParent(nsINavHistoryContainerResultNode** aParent)
+{
+ NS_IF_ADDREF(*aParent = mParent);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetParentResult(nsINavHistoryResult** aResult)
+{
+ *aResult = nullptr;
+ if (IsContainer())
+ NS_IF_ADDREF(*aResult = GetAsContainer()->mResult);
+ else if (mParent)
+ NS_IF_ADDREF(*aResult = mParent->mResult);
+
+ NS_ENSURE_STATE(*aResult);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetTags(nsAString& aTags) {
+ // Only URI-nodes may be associated with tags
+ if (!IsURI()) {
+ aTags.Truncate();
+ return NS_OK;
+ }
+
+ // Initially, the tags string is set to a void string (see constructor). We
+ // then build it the first time this method called is called (and by that,
+ // implicitly unset the void flag). Result observers may re-set the void flag
+ // in order to force rebuilding of the tags string.
+ if (!mTags.IsVoid()) {
+ // If mTags is assigned by a history query it is unsorted for performance
+ // reasons, it must be sorted by name on first read access.
+ if (!mAreTagsSorted) {
+ nsTArray<nsCString> tags;
+ ParseString(NS_ConvertUTF16toUTF8(mTags), ',', tags);
+ tags.Sort();
+ mTags.SetIsVoid(true);
+ for (nsTArray<nsCString>::index_type i = 0; i < tags.Length(); ++i) {
+ AppendUTF8toUTF16(tags[i], mTags);
+ if (i < tags.Length() - 1 )
+ mTags.AppendLiteral(", ");
+ }
+ mAreTagsSorted = true;
+ }
+ aTags.Assign(mTags);
+ return NS_OK;
+ }
+
+ // Fetch the tags
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+ nsCOMPtr<mozIStorageStatement> stmt = DB->GetStatement(
+ "/* do not warn (bug 487594) */ "
+ "SELECT GROUP_CONCAT(tag_title, ', ') "
+ "FROM ( "
+ "SELECT t.title AS tag_title "
+ "FROM moz_bookmarks b "
+ "JOIN moz_bookmarks t ON t.id = +b.parent "
+ "WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) "
+ "AND t.parent = :tags_folder "
+ "ORDER BY t.title COLLATE NOCASE ASC "
+ ") "
+ );
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper scoper(stmt);
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_STATE(history);
+ nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("tags_folder"),
+ history->GetTagsFolder());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasTags = false;
+ if (NS_SUCCEEDED(stmt->ExecuteStep(&hasTags)) && hasTags) {
+ rv = stmt->GetString(0, mTags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aTags.Assign(mTags);
+ mAreTagsSorted = true;
+ }
+
+ // If this node is a child of a history query, we need to make sure changes
+ // to tags are properly live-updated.
+ if (mParent && mParent->IsQuery() &&
+ mParent->mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY) {
+ nsNavHistoryQueryResultNode* query = mParent->GetAsQuery();
+ nsNavHistoryResult* result = query->GetResult();
+ NS_ENSURE_STATE(result);
+ result->AddAllBookmarksObserver(query);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetPageGuid(nsACString& aPageGuid) {
+ aPageGuid = mPageGuid;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetBookmarkGuid(nsACString& aBookmarkGuid) {
+ aBookmarkGuid = mBookmarkGuid;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetVisitId(int64_t* aVisitId) {
+ *aVisitId = mVisitId;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetFromVisitId(int64_t* aFromVisitId) {
+ *aFromVisitId = mFromVisitId;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::GetVisitType(uint32_t* aVisitType) {
+ *aVisitType = mTransitionType;
+ return NS_OK;
+}
+
+
+void
+nsNavHistoryResultNode::OnRemoving()
+{
+ mParent = nullptr;
+}
+
+
+/**
+ * This will find the result for this node. We can ask the nearest container
+ * for this value (either ourselves or our parents should be a container,
+ * and all containers have result pointers).
+ *
+ * @note The result may be null, if the container is detached from the result
+ * who owns it.
+ */
+nsNavHistoryResult*
+nsNavHistoryResultNode::GetResult()
+{
+ nsNavHistoryResultNode* node = this;
+ do {
+ if (node->IsContainer()) {
+ nsNavHistoryContainerResultNode* container = TO_CONTAINER(node);
+ return container->mResult;
+ }
+ node = node->mParent;
+ } while (node);
+ MOZ_ASSERT(false, "No container node found in hierarchy!");
+ return nullptr;
+}
+
+
+/**
+ * Searches up the tree for the closest ancestor node that has an options
+ * structure. This will tell us the options that were used to generate this
+ * node.
+ *
+ * Be careful, this function walks up the tree, so it can not be used when
+ * result nodes are created because they have no parent. Only call this
+ * function after the tree has been built.
+ */
+nsNavHistoryQueryOptions*
+nsNavHistoryResultNode::GetGeneratingOptions()
+{
+ if (!mParent) {
+ // When we have no parent, it either means we haven't built the tree yet,
+ // in which case calling this function is a bug, or this node is the root
+ // of the tree. When we are the root of the tree, our own options are the
+ // generating options.
+ if (IsContainer())
+ return GetAsContainer()->mOptions;
+
+ NS_NOTREACHED("Can't find a generating node for this container, perhaps FillStats has not been called on this tree yet?");
+ return nullptr;
+ }
+
+ // Look up the tree. We want the options that were used to create this node,
+ // and since it has a parent, it's the options of an ancestor, not of the node
+ // itself. So start at the parent.
+ nsNavHistoryContainerResultNode* cur = mParent;
+ while (cur) {
+ if (cur->IsContainer())
+ return cur->GetAsContainer()->mOptions;
+ cur = cur->mParent;
+ }
+
+ // We should always find a container node as an ancestor.
+ NS_NOTREACHED("Can't find a generating node for this container, the tree seemes corrupted.");
+ return nullptr;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode,
+ mResult,
+ mChildren)
+
+NS_IMPL_ADDREF_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode)
+NS_IMPL_RELEASE_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(nsNavHistoryContainerResultNode)
+ NS_INTERFACE_MAP_STATIC_AMBIGUOUS(nsNavHistoryContainerResultNode)
+ NS_INTERFACE_MAP_ENTRY(nsINavHistoryContainerResultNode)
+NS_INTERFACE_MAP_END_INHERITING(nsNavHistoryResultNode)
+
+nsNavHistoryContainerResultNode::nsNavHistoryContainerResultNode(
+ const nsACString& aURI, const nsACString& aTitle,
+ const nsACString& aIconURI, uint32_t aContainerType,
+ nsNavHistoryQueryOptions* aOptions) :
+ nsNavHistoryResultNode(aURI, aTitle, 0, 0, aIconURI),
+ mResult(nullptr),
+ mContainerType(aContainerType),
+ mExpanded(false),
+ mOptions(aOptions),
+ mAsyncCanceledState(NOT_CANCELED)
+{
+}
+
+nsNavHistoryContainerResultNode::nsNavHistoryContainerResultNode(
+ const nsACString& aURI, const nsACString& aTitle,
+ PRTime aTime,
+ const nsACString& aIconURI, uint32_t aContainerType,
+ nsNavHistoryQueryOptions* aOptions) :
+ nsNavHistoryResultNode(aURI, aTitle, 0, aTime, aIconURI),
+ mResult(nullptr),
+ mContainerType(aContainerType),
+ mExpanded(false),
+ mOptions(aOptions),
+ mAsyncCanceledState(NOT_CANCELED)
+{
+}
+
+
+nsNavHistoryContainerResultNode::~nsNavHistoryContainerResultNode()
+{
+ // Explicitly clean up array of children of this container. We must ensure
+ // all references are gone and all of their destructors are called.
+ mChildren.Clear();
+}
+
+
+/**
+ * Containers should notify their children that they are being removed when the
+ * container is being removed.
+ */
+void
+nsNavHistoryContainerResultNode::OnRemoving()
+{
+ nsNavHistoryResultNode::OnRemoving();
+ for (int32_t i = 0; i < mChildren.Count(); ++i)
+ mChildren[i]->OnRemoving();
+ mChildren.Clear();
+ mResult = nullptr;
+}
+
+
+bool
+nsNavHistoryContainerResultNode::AreChildrenVisible()
+{
+ nsNavHistoryResult* result = GetResult();
+ if (!result) {
+ NS_NOTREACHED("Invalid result");
+ return false;
+ }
+
+ if (!mExpanded)
+ return false;
+
+ // Now check if any ancestor is closed.
+ nsNavHistoryContainerResultNode* ancestor = mParent;
+ while (ancestor) {
+ if (!ancestor->mExpanded)
+ return false;
+
+ ancestor = ancestor->mParent;
+ }
+
+ return true;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::GetContainerOpen(bool *aContainerOpen)
+{
+ *aContainerOpen = mExpanded;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::SetContainerOpen(bool aContainerOpen)
+{
+ if (aContainerOpen) {
+ if (!mExpanded) {
+ nsNavHistoryQueryOptions* options = GetGeneratingOptions();
+ if (options && options->AsyncEnabled())
+ OpenContainerAsync();
+ else
+ OpenContainer();
+ }
+ }
+ else {
+ if (mExpanded)
+ CloseContainer();
+ else if (mAsyncPendingStmt)
+ CancelAsyncOpen(false);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * Notifies the result's observers of a change in the container's state. The
+ * notification includes both the old and new states: The old is aOldState, and
+ * the new is the container's current state.
+ *
+ * @param aOldState
+ * The state being transitioned out of.
+ */
+nsresult
+nsNavHistoryContainerResultNode::NotifyOnStateChange(uint16_t aOldState)
+{
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+
+ nsresult rv;
+ uint16_t currState;
+ rv = GetState(&currState);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Notify via the new ContainerStateChanged observer method.
+ NOTIFY_RESULT_OBSERVERS(result,
+ ContainerStateChanged(this, aOldState, currState));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::GetState(uint16_t* _state)
+{
+ NS_ENSURE_ARG_POINTER(_state);
+
+ *_state = mExpanded ? (uint16_t)STATE_OPENED
+ : mAsyncPendingStmt ? (uint16_t)STATE_LOADING
+ : (uint16_t)STATE_CLOSED;
+
+ return NS_OK;
+}
+
+
+/**
+ * This handles the generic container case. Other container types should
+ * override this to do their own handling.
+ */
+nsresult
+nsNavHistoryContainerResultNode::OpenContainer()
+{
+ NS_ASSERTION(!mExpanded, "Container must not be expanded to open it");
+ mExpanded = true;
+
+ nsresult rv = NotifyOnStateChange(STATE_CLOSED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+/**
+ * Unset aSuppressNotifications to notify observers on this change. That is
+ * the normal operation. This is set to false for the recursive calls since the
+ * root container that is being closed will handle recomputation of the visible
+ * elements for its entire subtree.
+ */
+nsresult
+nsNavHistoryContainerResultNode::CloseContainer(bool aSuppressNotifications)
+{
+ NS_ASSERTION((mExpanded && !mAsyncPendingStmt) ||
+ (!mExpanded && mAsyncPendingStmt),
+ "Container must be expanded or loading to close it");
+
+ nsresult rv;
+ uint16_t oldState;
+ rv = GetState(&oldState);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mExpanded) {
+ // Recursively close all child containers.
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]->IsContainer() &&
+ mChildren[i]->GetAsContainer()->mExpanded)
+ mChildren[i]->GetAsContainer()->CloseContainer(true);
+ }
+
+ mExpanded = false;
+ }
+
+ // Be sure to set this to null before notifying observers. It signifies that
+ // the container is no longer loading (if it was in the first place).
+ mAsyncPendingStmt = nullptr;
+
+ if (!aSuppressNotifications) {
+ rv = NotifyOnStateChange(oldState);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // If this is the root container of a result, we can tell the result to stop
+ // observing changes, otherwise the result will stay in memory and updates
+ // itself till it is cycle collected.
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ if (result->mRootNode == this) {
+ result->StopObserving();
+ // When reopening this node its result will be out of sync.
+ // We must clear our children to ensure we will call FillChildren
+ // again in such a case.
+ if (this->IsQuery())
+ this->GetAsQuery()->ClearChildren(true);
+ else if (this->IsFolder())
+ this->GetAsFolder()->ClearChildren(true);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * The async version of OpenContainer.
+ */
+nsresult
+nsNavHistoryContainerResultNode::OpenContainerAsync()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+
+/**
+ * Cancels the pending asynchronous Storage execution triggered by
+ * FillChildrenAsync, if it exists. This method doesn't do much, because after
+ * cancelation Storage will call this node's HandleCompletion callback, where
+ * the real work is done.
+ *
+ * @param aRestart
+ * If true, async execution will be restarted by HandleCompletion.
+ */
+void
+nsNavHistoryContainerResultNode::CancelAsyncOpen(bool aRestart)
+{
+ NS_ASSERTION(mAsyncPendingStmt, "Async execution canceled but not pending");
+
+ mAsyncCanceledState = aRestart ? CANCELED_RESTART_NEEDED : CANCELED;
+
+ // Cancel will fail if the pending statement has already been canceled.
+ // That's OK since this method may be called multiple times, and multiple
+ // cancels don't harm anything.
+ (void)mAsyncPendingStmt->Cancel();
+}
+
+
+/**
+ * This builds up tree statistics from the bottom up. Call with a container
+ * and the indent level of that container. To init the full tree, call with
+ * the root container. The default indent level is -1, which is appropriate
+ * for the root level.
+ *
+ * CALL THIS AFTER FILLING ANY CONTAINER to update the parent and result node
+ * pointers, even if you don't care about visit counts and last visit dates.
+ */
+void
+nsNavHistoryContainerResultNode::FillStats()
+{
+ uint32_t accessCount = 0;
+ PRTime newTime = 0;
+
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ nsNavHistoryResultNode* node = mChildren[i];
+ node->mParent = this;
+ node->mIndentLevel = mIndentLevel + 1;
+ if (node->IsContainer()) {
+ nsNavHistoryContainerResultNode* container = node->GetAsContainer();
+ container->mResult = mResult;
+ container->FillStats();
+ }
+ accessCount += node->mAccessCount;
+ // this is how container nodes get sorted by date
+ // The container gets the most recent time of the child nodes.
+ if (node->mTime > newTime)
+ newTime = node->mTime;
+ }
+
+ if (mExpanded) {
+ mAccessCount = accessCount;
+ if (!IsQuery() || newTime > mTime)
+ mTime = newTime;
+ }
+}
+
+
+/**
+ * This is used when one container changes to do a minimal update of the tree
+ * structure. When something changes, you want to call FillStats if necessary
+ * and update this container completely. Then call this function which will
+ * walk up the tree and fill in the previous containers.
+ *
+ * Note that you have to tell us by how much our access count changed. Our
+ * access count should already be set to the new value; this is used tochange
+ * the parents without having to re-count all their children.
+ *
+ * This does NOT update the last visit date downward. Therefore, if you are
+ * deleting a node that has the most recent last visit date, the parents will
+ * not get their last visit dates downshifted accordingly. This is a rather
+ * unusual case: we don't often delete things, and we usually don't even show
+ * the last visit date for folders. Updating would be slower because we would
+ * have to recompute it from scratch.
+ */
+nsresult
+nsNavHistoryContainerResultNode::ReverseUpdateStats(int32_t aAccessCountChange)
+{
+ if (mParent) {
+ nsNavHistoryResult* result = GetResult();
+ bool shouldNotify = result && mParent->mParent &&
+ mParent->mParent->AreChildrenVisible();
+
+ mParent->mAccessCount += aAccessCountChange;
+ bool timeChanged = false;
+ if (mTime > mParent->mTime) {
+ timeChanged = true;
+ mParent->mTime = mTime;
+ }
+
+ if (shouldNotify) {
+ NOTIFY_RESULT_OBSERVERS(result,
+ NodeHistoryDetailsChanged(TO_ICONTAINER(mParent),
+ mParent->mTime,
+ mParent->mAccessCount));
+ }
+
+ // check sorting, the stats may have caused this node to move if the
+ // sorting depended on something we are changing.
+ uint16_t sortMode = mParent->GetSortType();
+ bool sortingByVisitCount =
+ sortMode == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING ||
+ sortMode == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING;
+ bool sortingByTime =
+ sortMode == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING ||
+ sortMode == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING;
+
+ if ((sortingByVisitCount && aAccessCountChange != 0) ||
+ (sortingByTime && timeChanged)) {
+ int32_t ourIndex = mParent->FindChild(this);
+ NS_ASSERTION(ourIndex >= 0, "Could not find self in parent");
+ if (ourIndex >= 0)
+ EnsureItemPosition(static_cast<uint32_t>(ourIndex));
+ }
+
+ nsresult rv = mParent->ReverseUpdateStats(aAccessCountChange);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * This walks up the tree until we find a query result node or the root to get
+ * the sorting type.
+ */
+uint16_t
+nsNavHistoryContainerResultNode::GetSortType()
+{
+ if (mParent)
+ return mParent->GetSortType();
+ if (mResult)
+ return mResult->mSortingMode;
+
+ // This is a detached container, just use natural order.
+ return nsINavHistoryQueryOptions::SORT_BY_NONE;
+}
+
+
+nsresult nsNavHistoryContainerResultNode::Refresh() {
+ NS_WARNING("Refresh() is supported by queries or folders, not generic containers.");
+ return NS_OK;
+}
+
+void
+nsNavHistoryContainerResultNode::GetSortingAnnotation(nsACString& aAnnotation)
+{
+ if (mParent)
+ mParent->GetSortingAnnotation(aAnnotation);
+ else if (mResult)
+ aAnnotation.Assign(mResult->mSortingAnnotation);
+}
+
+/**
+ * @return the sorting comparator function for the give sort type, or null if
+ * there is no comparator.
+ */
+nsNavHistoryContainerResultNode::SortComparator
+nsNavHistoryContainerResultNode::GetSortingComparator(uint16_t aSortType)
+{
+ switch (aSortType)
+ {
+ case nsINavHistoryQueryOptions::SORT_BY_NONE:
+ return &SortComparison_Bookmark;
+ case nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING:
+ return &SortComparison_TitleLess;
+ case nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING:
+ return &SortComparison_TitleGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING:
+ return &SortComparison_DateLess;
+ case nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING:
+ return &SortComparison_DateGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_URI_ASCENDING:
+ return &SortComparison_URILess;
+ case nsINavHistoryQueryOptions::SORT_BY_URI_DESCENDING:
+ return &SortComparison_URIGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING:
+ return &SortComparison_VisitCountLess;
+ case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING:
+ return &SortComparison_VisitCountGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_KEYWORD_ASCENDING:
+ return &SortComparison_KeywordLess;
+ case nsINavHistoryQueryOptions::SORT_BY_KEYWORD_DESCENDING:
+ return &SortComparison_KeywordGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_ANNOTATION_ASCENDING:
+ return &SortComparison_AnnotationLess;
+ case nsINavHistoryQueryOptions::SORT_BY_ANNOTATION_DESCENDING:
+ return &SortComparison_AnnotationGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_ASCENDING:
+ return &SortComparison_DateAddedLess;
+ case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_DESCENDING:
+ return &SortComparison_DateAddedGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_ASCENDING:
+ return &SortComparison_LastModifiedLess;
+ case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_DESCENDING:
+ return &SortComparison_LastModifiedGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_TAGS_ASCENDING:
+ return &SortComparison_TagsLess;
+ case nsINavHistoryQueryOptions::SORT_BY_TAGS_DESCENDING:
+ return &SortComparison_TagsGreater;
+ case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING:
+ return &SortComparison_FrecencyLess;
+ case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING:
+ return &SortComparison_FrecencyGreater;
+ default:
+ NS_NOTREACHED("Bad sorting type");
+ return nullptr;
+ }
+}
+
+
+/**
+ * This is used by Result::SetSortingMode and QueryResultNode::FillChildren to
+ * sort the child list.
+ *
+ * This does NOT update any visibility or tree information. The caller will
+ * have to completely rebuild the visible list after this.
+ */
+void
+nsNavHistoryContainerResultNode::RecursiveSort(
+ const char* aData, SortComparator aComparator)
+{
+ void* data = const_cast<void*>(static_cast<const void*>(aData));
+
+ mChildren.Sort(aComparator, data);
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]->IsContainer())
+ mChildren[i]->GetAsContainer()->RecursiveSort(aData, aComparator);
+ }
+}
+
+
+/**
+ * @return the index that the given item would fall on if it were to be
+ * inserted using the given sorting.
+ */
+uint32_t
+nsNavHistoryContainerResultNode::FindInsertionPoint(
+ nsNavHistoryResultNode* aNode, SortComparator aComparator,
+ const char* aData, bool* aItemExists)
+{
+ if (aItemExists)
+ (*aItemExists) = false;
+
+ if (mChildren.Count() == 0)
+ return 0;
+
+ void* data = const_cast<void*>(static_cast<const void*>(aData));
+
+ // The common case is the beginning or the end because this is used to insert
+ // new items that are added to history, which is usually sorted by date.
+ int32_t res;
+ res = aComparator(aNode, mChildren[0], data);
+ if (res <= 0) {
+ if (aItemExists && res == 0)
+ (*aItemExists) = true;
+ return 0;
+ }
+ res = aComparator(aNode, mChildren[mChildren.Count() - 1], data);
+ if (res >= 0) {
+ if (aItemExists && res == 0)
+ (*aItemExists) = true;
+ return mChildren.Count();
+ }
+
+ uint32_t beginRange = 0; // inclusive
+ uint32_t endRange = mChildren.Count(); // exclusive
+ while (1) {
+ if (beginRange == endRange)
+ return endRange;
+ uint32_t center = beginRange + (endRange - beginRange) / 2;
+ int32_t res = aComparator(aNode, mChildren[center], data);
+ if (res <= 0) {
+ endRange = center; // left side
+ if (aItemExists && res == 0)
+ (*aItemExists) = true;
+ }
+ else {
+ beginRange = center + 1; // right site
+ }
+ }
+}
+
+
+/**
+ * This checks the child node at the given index to see if its sorting is
+ * correct. This is called when nodes are updated and we need to see whether
+ * we need to move it.
+ *
+ * @returns true if not and it should be resorted.
+*/
+bool
+nsNavHistoryContainerResultNode::DoesChildNeedResorting(uint32_t aIndex,
+ SortComparator aComparator, const char* aData)
+{
+ NS_ASSERTION(aIndex < uint32_t(mChildren.Count()),
+ "Input index out of range");
+ if (mChildren.Count() == 1)
+ return false;
+
+ void* data = const_cast<void*>(static_cast<const void*>(aData));
+
+ if (aIndex > 0) {
+ // compare to previous item
+ if (aComparator(mChildren[aIndex - 1], mChildren[aIndex], data) > 0)
+ return true;
+ }
+ if (aIndex < uint32_t(mChildren.Count()) - 1) {
+ // compare to next item
+ if (aComparator(mChildren[aIndex], mChildren[aIndex + 1], data) > 0)
+ return true;
+ }
+ return false;
+}
+
+
+/* static */
+int32_t nsNavHistoryContainerResultNode::SortComparison_StringLess(
+ const nsAString& a, const nsAString& b) {
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, 0);
+ nsICollation* collation = history->GetCollation();
+ NS_ENSURE_TRUE(collation, 0);
+
+ int32_t res = 0;
+ collation->CompareString(nsICollation::kCollationCaseInSensitive, a, b, &res);
+ return res;
+}
+
+
+/**
+ * When there are bookmark indices, we should never have ties, so we don't
+ * need to worry about tiebreaking. When there are no bookmark indices,
+ * everything will be -1 and we don't worry about sorting.
+ */
+int32_t nsNavHistoryContainerResultNode::SortComparison_Bookmark(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return a->mBookmarkIndex - b->mBookmarkIndex;
+}
+
+/**
+ * These are a little more complicated because they do a localization
+ * conversion. If this is too slow, we can compute the sort keys once in
+ * advance, sort that array, and then reorder the real array accordingly.
+ * This would save some key generations.
+ *
+ * The collation object must be allocated before sorting on title!
+ */
+int32_t nsNavHistoryContainerResultNode::SortComparison_TitleLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ uint32_t aType;
+ a->GetType(&aType);
+
+ int32_t value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle),
+ NS_ConvertUTF8toUTF16(b->mTitle));
+ if (value == 0) {
+ // resolve by URI
+ if (a->IsURI()) {
+ value = a->mURI.Compare(b->mURI.get());
+ }
+ if (value == 0) {
+ // resolve by date
+ value = ComparePRTime(a->mTime, b->mTime);
+ if (value == 0)
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ }
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_TitleGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -SortComparison_TitleLess(a, b, closure);
+}
+
+/**
+ * Equal times will be very unusual, but it is important that there is some
+ * deterministic ordering of the results so they don't move around.
+ */
+int32_t nsNavHistoryContainerResultNode::SortComparison_DateLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value = ComparePRTime(a->mTime, b->mTime);
+ if (value == 0) {
+ value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle),
+ NS_ConvertUTF8toUTF16(b->mTitle));
+ if (value == 0)
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_DateGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -nsNavHistoryContainerResultNode::SortComparison_DateLess(a, b, closure);
+}
+
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_DateAddedLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value = ComparePRTime(a->mDateAdded, b->mDateAdded);
+ if (value == 0) {
+ value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle),
+ NS_ConvertUTF8toUTF16(b->mTitle));
+ if (value == 0)
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_DateAddedGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -nsNavHistoryContainerResultNode::SortComparison_DateAddedLess(a, b, closure);
+}
+
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_LastModifiedLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value = ComparePRTime(a->mLastModified, b->mLastModified);
+ if (value == 0) {
+ value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle),
+ NS_ConvertUTF8toUTF16(b->mTitle));
+ if (value == 0)
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_LastModifiedGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -nsNavHistoryContainerResultNode::SortComparison_LastModifiedLess(a, b, closure);
+}
+
+
+/**
+ * Certain types of parent nodes are treated specially because URIs are not
+ * valid (like days or hosts).
+ */
+int32_t nsNavHistoryContainerResultNode::SortComparison_URILess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value;
+ if (a->IsURI() && b->IsURI()) {
+ // normal URI or visit
+ value = a->mURI.Compare(b->mURI.get());
+ } else {
+ // for everything else, use title (= host name)
+ value = SortComparison_StringLess(NS_ConvertUTF8toUTF16(a->mTitle),
+ NS_ConvertUTF8toUTF16(b->mTitle));
+ }
+
+ if (value == 0) {
+ value = ComparePRTime(a->mTime, b->mTime);
+ if (value == 0)
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_URIGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -SortComparison_URILess(a, b, closure);
+}
+
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_KeywordLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value = 0;
+ if (a->mItemId != -1 || b->mItemId != -1) {
+ // compare the keywords
+ nsAutoString keywordA, keywordB;
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, 0);
+
+ nsresult rv;
+ if (a->mItemId != -1) {
+ rv = bookmarks->GetKeywordForBookmark(a->mItemId, keywordA);
+ NS_ENSURE_SUCCESS(rv, 0);
+ }
+ if (b->mItemId != -1) {
+ rv = bookmarks->GetKeywordForBookmark(b->mItemId, keywordB);
+ NS_ENSURE_SUCCESS(rv, 0);
+ }
+
+ value = SortComparison_StringLess(keywordA, keywordB);
+ }
+
+ // Fall back to title sorting.
+ if (value == 0)
+ value = SortComparison_TitleLess(a, b, closure);
+
+ return value;
+}
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_KeywordGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -SortComparison_KeywordLess(a, b, closure);
+}
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_AnnotationLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ nsAutoCString annoName(static_cast<char*>(closure));
+ NS_ENSURE_TRUE(!annoName.IsEmpty(), 0);
+
+ bool a_itemAnno = false;
+ bool b_itemAnno = false;
+
+ // Not used for item annos
+ nsCOMPtr<nsIURI> a_uri, b_uri;
+ if (a->mItemId != -1) {
+ a_itemAnno = true;
+ } else {
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(a->GetUri(spec))){
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(a_uri), spec));
+ }
+ NS_ENSURE_TRUE(a_uri, 0);
+ }
+
+ if (b->mItemId != -1) {
+ b_itemAnno = true;
+ } else {
+ nsAutoCString spec;
+ if (NS_SUCCEEDED(b->GetUri(spec))) {
+ MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(b_uri), spec));
+ }
+ NS_ENSURE_TRUE(b_uri, 0);
+ }
+
+ nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+ NS_ENSURE_TRUE(annosvc, 0);
+
+ bool a_hasAnno, b_hasAnno;
+ if (a_itemAnno) {
+ NS_ENSURE_SUCCESS(annosvc->ItemHasAnnotation(a->mItemId, annoName,
+ &a_hasAnno), 0);
+ } else {
+ NS_ENSURE_SUCCESS(annosvc->PageHasAnnotation(a_uri, annoName,
+ &a_hasAnno), 0);
+ }
+ if (b_itemAnno) {
+ NS_ENSURE_SUCCESS(annosvc->ItemHasAnnotation(b->mItemId, annoName,
+ &b_hasAnno), 0);
+ } else {
+ NS_ENSURE_SUCCESS(annosvc->PageHasAnnotation(b_uri, annoName,
+ &b_hasAnno), 0);
+ }
+
+ int32_t value = 0;
+ if (a_hasAnno || b_hasAnno) {
+ uint16_t annoType;
+ if (a_hasAnno) {
+ if (a_itemAnno) {
+ NS_ENSURE_SUCCESS(annosvc->GetItemAnnotationType(a->mItemId,
+ annoName,
+ &annoType), 0);
+ } else {
+ NS_ENSURE_SUCCESS(annosvc->GetPageAnnotationType(a_uri, annoName,
+ &annoType), 0);
+ }
+ }
+ if (b_hasAnno) {
+ uint16_t b_type;
+ if (b_itemAnno) {
+ NS_ENSURE_SUCCESS(annosvc->GetItemAnnotationType(b->mItemId,
+ annoName,
+ &b_type), 0);
+ } else {
+ NS_ENSURE_SUCCESS(annosvc->GetPageAnnotationType(b_uri, annoName,
+ &b_type), 0);
+ }
+ // We better make the API not support this state, really
+ // XXXmano: this is actually wrong for double<->int and int64_t<->int32_t
+ if (a_hasAnno && b_type != annoType)
+ return 0;
+ annoType = b_type;
+ }
+
+#define GET_ANNOTATIONS_VALUES(METHOD_ITEM, METHOD_PAGE, A_VAL, B_VAL) \
+ if (a_hasAnno) { \
+ if (a_itemAnno) { \
+ NS_ENSURE_SUCCESS(annosvc->METHOD_ITEM(a->mItemId, annoName, \
+ A_VAL), 0); \
+ } else { \
+ NS_ENSURE_SUCCESS(annosvc->METHOD_PAGE(a_uri, annoName, \
+ A_VAL), 0); \
+ } \
+ } \
+ if (b_hasAnno) { \
+ if (b_itemAnno) { \
+ NS_ENSURE_SUCCESS(annosvc->METHOD_ITEM(b->mItemId, annoName, \
+ B_VAL), 0); \
+ } else { \
+ NS_ENSURE_SUCCESS(annosvc->METHOD_PAGE(b_uri, annoName, \
+ B_VAL), 0); \
+ } \
+ }
+
+ if (annoType == nsIAnnotationService::TYPE_STRING) {
+ nsAutoString a_val, b_val;
+ GET_ANNOTATIONS_VALUES(GetItemAnnotationString,
+ GetPageAnnotationString, a_val, b_val);
+ value = SortComparison_StringLess(a_val, b_val);
+ }
+ else if (annoType == nsIAnnotationService::TYPE_INT32) {
+ int32_t a_val = 0, b_val = 0;
+ GET_ANNOTATIONS_VALUES(GetItemAnnotationInt32,
+ GetPageAnnotationInt32, &a_val, &b_val);
+ value = (a_val < b_val) ? -1 : (a_val > b_val) ? 1 : 0;
+ }
+ else if (annoType == nsIAnnotationService::TYPE_INT64) {
+ int64_t a_val = 0, b_val = 0;
+ GET_ANNOTATIONS_VALUES(GetItemAnnotationInt64,
+ GetPageAnnotationInt64, &a_val, &b_val);
+ value = (a_val < b_val) ? -1 : (a_val > b_val) ? 1 : 0;
+ }
+ else if (annoType == nsIAnnotationService::TYPE_DOUBLE) {
+ double a_val = 0, b_val = 0;
+ GET_ANNOTATIONS_VALUES(GetItemAnnotationDouble,
+ GetPageAnnotationDouble, &a_val, &b_val);
+ value = (a_val < b_val) ? -1 : (a_val > b_val) ? 1 : 0;
+ }
+ }
+
+ // Note we also fall back to the title-sorting route one of the items didn't
+ // have the annotation set or if both had it set but in a different storage
+ // type
+ if (value == 0)
+ return SortComparison_TitleLess(a, b, nullptr);
+
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_AnnotationGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -SortComparison_AnnotationLess(a, b, closure);
+}
+
+/**
+ * Fall back on dates for conflict resolution
+ */
+int32_t nsNavHistoryContainerResultNode::SortComparison_VisitCountLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value = CompareIntegers(a->mAccessCount, b->mAccessCount);
+ if (value == 0) {
+ value = ComparePRTime(a->mTime, b->mTime);
+ if (value == 0)
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ return value;
+}
+int32_t nsNavHistoryContainerResultNode::SortComparison_VisitCountGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -nsNavHistoryContainerResultNode::SortComparison_VisitCountLess(a, b, closure);
+}
+
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_TagsLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ int32_t value = 0;
+ nsAutoString aTags, bTags;
+
+ nsresult rv = a->GetTags(aTags);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ rv = b->GetTags(bTags);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ value = SortComparison_StringLess(aTags, bTags);
+
+ // fall back to title sorting
+ if (value == 0)
+ value = SortComparison_TitleLess(a, b, closure);
+
+ return value;
+}
+
+int32_t nsNavHistoryContainerResultNode::SortComparison_TagsGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure)
+{
+ return -SortComparison_TagsLess(a, b, closure);
+}
+
+/**
+ * Fall back on date and bookmarked status, for conflict resolution.
+ */
+int32_t
+nsNavHistoryContainerResultNode::SortComparison_FrecencyLess(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure
+)
+{
+ int32_t value = CompareIntegers(a->mFrecency, b->mFrecency);
+ if (value == 0) {
+ value = ComparePRTime(a->mTime, b->mTime);
+ if (value == 0) {
+ value = nsNavHistoryContainerResultNode::SortComparison_Bookmark(a, b, closure);
+ }
+ }
+ return value;
+}
+int32_t
+nsNavHistoryContainerResultNode::SortComparison_FrecencyGreater(
+ nsNavHistoryResultNode* a, nsNavHistoryResultNode* b, void* closure
+)
+{
+ return -nsNavHistoryContainerResultNode::SortComparison_FrecencyLess(a, b, closure);
+}
+
+/**
+ * Searches this folder for a node with the given URI. Returns null if not
+ * found.
+ *
+ * @note Does not addref the node!
+ */
+nsNavHistoryResultNode*
+nsNavHistoryContainerResultNode::FindChildURI(const nsACString& aSpec,
+ uint32_t* aNodeIndex)
+{
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]->IsURI()) {
+ if (aSpec.Equals(mChildren[i]->mURI)) {
+ *aNodeIndex = i;
+ return mChildren[i];
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * This does the work of adding a child to the container. The child can be
+ * either a container or or a single item that may even be collapsed with the
+ * adjacent ones.
+ */
+nsresult
+nsNavHistoryContainerResultNode::InsertChildAt(nsNavHistoryResultNode* aNode,
+ int32_t aIndex)
+{
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+
+ aNode->mParent = this;
+ aNode->mIndentLevel = mIndentLevel + 1;
+ if (aNode->IsContainer()) {
+ // need to update all the new item's children
+ nsNavHistoryContainerResultNode* container = aNode->GetAsContainer();
+ container->mResult = result;
+ container->FillStats();
+ }
+
+ if (!mChildren.InsertObjectAt(aNode, aIndex))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // Update our stats and notify the result's observers.
+ mAccessCount += aNode->mAccessCount;
+ if (mTime < aNode->mTime)
+ mTime = aNode->mTime;
+ if (!mParent || mParent->AreChildrenVisible()) {
+ NOTIFY_RESULT_OBSERVERS(result,
+ NodeHistoryDetailsChanged(TO_ICONTAINER(this),
+ mTime,
+ mAccessCount));
+ }
+
+ nsresult rv = ReverseUpdateStats(aNode->mAccessCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update tree if we are visible. Note that we could be here and not
+ // expanded, like when there is a bookmark folder being updated because its
+ // parent is visible.
+ if (AreChildrenVisible())
+ NOTIFY_RESULT_OBSERVERS(result, NodeInserted(this, aNode, aIndex));
+
+ return NS_OK;
+}
+
+
+/**
+ * This locates the proper place for insertion according to the current sort
+ * and calls InsertChildAt
+ */
+nsresult
+nsNavHistoryContainerResultNode::InsertSortedChild(
+ nsNavHistoryResultNode* aNode,
+ bool aIgnoreDuplicates)
+{
+
+ if (mChildren.Count() == 0)
+ return InsertChildAt(aNode, 0);
+
+ SortComparator comparator = GetSortingComparator(GetSortType());
+ if (comparator) {
+ // When inserting a new node, it must have proper statistics because we use
+ // them to find the correct insertion point. The insert function will then
+ // recompute these statistics and fill in the proper parents and hierarchy
+ // level. Doing this twice shouldn't be a large performance penalty because
+ // when we are inserting new containers, they typically contain only one
+ // item (because we've browsed a new page).
+ if (aNode->IsContainer()) {
+ // need to update all the new item's children
+ nsNavHistoryContainerResultNode* container = aNode->GetAsContainer();
+ container->mResult = mResult;
+ container->FillStats();
+ }
+
+ nsAutoCString sortingAnnotation;
+ GetSortingAnnotation(sortingAnnotation);
+ bool itemExists;
+ uint32_t position = FindInsertionPoint(aNode, comparator,
+ sortingAnnotation.get(),
+ &itemExists);
+ if (aIgnoreDuplicates && itemExists)
+ return NS_OK;
+
+ return InsertChildAt(aNode, position);
+ }
+ return InsertChildAt(aNode, mChildren.Count());
+}
+
+/**
+ * This checks if the item at aIndex is located correctly given the sorting
+ * move. If it's not, the item is moved, and the result's observers are
+ * notified.
+ *
+ * @return true if the item position has been changed, false otherwise.
+ */
+bool
+nsNavHistoryContainerResultNode::EnsureItemPosition(uint32_t aIndex) {
+ NS_ASSERTION(aIndex < (uint32_t)mChildren.Count(), "Invalid index");
+ if (aIndex >= (uint32_t)mChildren.Count())
+ return false;
+
+ SortComparator comparator = GetSortingComparator(GetSortType());
+ if (!comparator)
+ return false;
+
+ nsAutoCString sortAnno;
+ GetSortingAnnotation(sortAnno);
+ if (!DoesChildNeedResorting(aIndex, comparator, sortAnno.get()))
+ return false;
+
+ RefPtr<nsNavHistoryResultNode> node(mChildren[aIndex]);
+ mChildren.RemoveObjectAt(aIndex);
+
+ uint32_t newIndex = FindInsertionPoint(
+ node, comparator,sortAnno.get(), nullptr);
+ mChildren.InsertObjectAt(node.get(), newIndex);
+
+ if (AreChildrenVisible()) {
+ nsNavHistoryResult* result = GetResult();
+ NOTIFY_RESULT_OBSERVERS_RET(result,
+ NodeMoved(node, this, aIndex, this, newIndex),
+ false);
+ }
+
+ return true;
+}
+
+/**
+ * This does all the work of removing a child from this container, including
+ * updating the tree if necessary. Note that we do not need to be open for
+ * this to work.
+ */
+nsresult
+nsNavHistoryContainerResultNode::RemoveChildAt(int32_t aIndex)
+{
+ NS_ASSERTION(aIndex >= 0 && aIndex < mChildren.Count(), "Invalid index");
+
+ // Hold an owning reference to keep from expiring while we work with it.
+ RefPtr<nsNavHistoryResultNode> oldNode = mChildren[aIndex];
+
+ // Update stats.
+ // XXX This assertion does not reliably pass -- investigate!! (bug 1049797)
+ // MOZ_ASSERT(mAccessCount >= mChildren[aIndex]->mAccessCount,
+ // "Invalid access count while updating!");
+ uint32_t oldAccessCount = mAccessCount;
+ mAccessCount -= mChildren[aIndex]->mAccessCount;
+
+ // Remove it from our list and notify the result's observers.
+ mChildren.RemoveObjectAt(aIndex);
+ if (AreChildrenVisible()) {
+ nsNavHistoryResult* result = GetResult();
+ NOTIFY_RESULT_OBSERVERS(result,
+ NodeRemoved(this, oldNode, aIndex));
+ }
+
+ nsresult rv = ReverseUpdateStats(mAccessCount - oldAccessCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ oldNode->OnRemoving();
+ return NS_OK;
+}
+
+
+/**
+ * Searches for matches for the given URI. If aOnlyOne is set, it will
+ * terminate as soon as it finds a single match. This would be used when there
+ * are URI results so there will only ever be one copy of any URI.
+ *
+ * When aOnlyOne is false, it will check all elements. This is for visit
+ * style results that may have multiple copies of any given URI.
+ */
+void
+nsNavHistoryContainerResultNode::RecursiveFindURIs(bool aOnlyOne,
+ nsNavHistoryContainerResultNode* aContainer, const nsCString& aSpec,
+ nsCOMArray<nsNavHistoryResultNode>* aMatches)
+{
+ for (int32_t child = 0; child < aContainer->mChildren.Count(); ++child) {
+ uint32_t type;
+ aContainer->mChildren[child]->GetType(&type);
+ if (nsNavHistoryResultNode::IsTypeURI(type)) {
+ // compare URIs
+ nsNavHistoryResultNode* uriNode = aContainer->mChildren[child];
+ if (uriNode->mURI.Equals(aSpec)) {
+ // found
+ aMatches->AppendObject(uriNode);
+ if (aOnlyOne)
+ return;
+ }
+ }
+ }
+}
+
+
+/**
+ * If aUpdateSort is true, we will also update the sorting of this item.
+ * Normally you want this to be true, but it can be false if the thing you are
+ * changing can not affect sorting (like favicons).
+ *
+ * You should NOT change any child lists as part of the callback function.
+ */
+bool
+nsNavHistoryContainerResultNode::UpdateURIs(bool aRecursive, bool aOnlyOne,
+ bool aUpdateSort, const nsCString& aSpec,
+ nsresult (*aCallback)(nsNavHistoryResultNode*, const void*, const nsNavHistoryResult*),
+ const void* aClosure)
+{
+ const nsNavHistoryResult* result = GetResult();
+ if (!result) {
+ MOZ_ASSERT(false, "Should have a result");
+ return false;
+ }
+
+ // this needs to be owning since sometimes we remove and re-insert nodes
+ // in their parents and we don't want them to go away.
+ nsCOMArray<nsNavHistoryResultNode> matches;
+
+ if (aRecursive) {
+ RecursiveFindURIs(aOnlyOne, this, aSpec, &matches);
+ } else if (aOnlyOne) {
+ uint32_t nodeIndex;
+ nsNavHistoryResultNode* node = FindChildURI(aSpec, &nodeIndex);
+ if (node)
+ matches.AppendObject(node);
+ } else {
+ MOZ_ASSERT(false,
+ "UpdateURIs does not handle nonrecursive updates of multiple items.");
+ // this case easy to add if you need it, just find all the matching URIs
+ // at this level. However, this isn't currently used. History uses
+ // recursive, Bookmarks uses one level and knows that the match is unique.
+ return false;
+ }
+
+ if (matches.Count() == 0)
+ return false;
+
+ // PERFORMANCE: This updates each container for each child in it that
+ // changes. In some cases, many elements have changed inside the same
+ // container. It would be better to compose a list of containers, and
+ // update each one only once for all the items that have changed in it.
+ for (int32_t i = 0; i < matches.Count(); ++i)
+ {
+ nsNavHistoryResultNode* node = matches[i];
+ nsNavHistoryContainerResultNode* parent = node->mParent;
+ if (!parent) {
+ MOZ_ASSERT(false, "All URI nodes being updated must have parents");
+ continue;
+ }
+
+ uint32_t oldAccessCount = node->mAccessCount;
+ PRTime oldTime = node->mTime;
+ aCallback(node, aClosure, result);
+
+ if (oldAccessCount != node->mAccessCount || oldTime != node->mTime) {
+ parent->mAccessCount += node->mAccessCount - oldAccessCount;
+ if (node->mTime > parent->mTime)
+ parent->mTime = node->mTime;
+ if (parent->AreChildrenVisible()) {
+ NOTIFY_RESULT_OBSERVERS_RET(result,
+ NodeHistoryDetailsChanged(
+ TO_ICONTAINER(parent),
+ parent->mTime,
+ parent->mAccessCount),
+ true);
+ }
+ DebugOnly<nsresult> rv = parent->ReverseUpdateStats(node->mAccessCount - oldAccessCount);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "should be able to ReverseUpdateStats");
+ }
+
+ if (aUpdateSort) {
+ int32_t childIndex = parent->FindChild(node);
+ MOZ_ASSERT(childIndex >= 0, "Could not find child we just got a reference to");
+ if (childIndex >= 0)
+ parent->EnsureItemPosition(childIndex);
+ }
+ }
+
+ return true;
+}
+
+
+/**
+ * This is used to update the titles in the tree. This is called from both
+ * query and bookmark folder containers to update the tree. Bookmark folders
+ * should be sure to set recursive to false, since child folders will have
+ * their own callbacks registered.
+ */
+static nsresult setTitleCallback(nsNavHistoryResultNode* aNode,
+ const void* aClosure,
+ const nsNavHistoryResult* aResult)
+{
+ const nsACString* newTitle = static_cast<const nsACString*>(aClosure);
+ aNode->mTitle = *newTitle;
+
+ if (aResult && (!aNode->mParent || aNode->mParent->AreChildrenVisible()))
+ NOTIFY_RESULT_OBSERVERS(aResult, NodeTitleChanged(aNode, *newTitle));
+
+ return NS_OK;
+}
+nsresult
+nsNavHistoryContainerResultNode::ChangeTitles(nsIURI* aURI,
+ const nsACString& aNewTitle,
+ bool aRecursive,
+ bool aOnlyOne)
+{
+ // uri string
+ nsAutoCString uriString;
+ nsresult rv = aURI->GetSpec(uriString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The recursive function will update the result's tree nodes, but only if we
+ // give it a non-null pointer. So if there isn't a tree, just pass nullptr
+ // so it doesn't bother trying to call the result.
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+
+ uint16_t sortType = GetSortType();
+ bool updateSorting =
+ (sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING);
+
+ UpdateURIs(aRecursive, aOnlyOne, updateSorting, uriString,
+ setTitleCallback,
+ static_cast<const void*>(&aNewTitle));
+
+ return NS_OK;
+}
+
+
+/**
+ * Complex containers (folders and queries) will override this. Here, we
+ * handle the case of simple containers (like host groups) where the children
+ * are always stored.
+ */
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::GetHasChildren(bool *aHasChildren)
+{
+ *aHasChildren = (mChildren.Count() > 0);
+ return NS_OK;
+}
+
+
+/**
+ * @throws if this node is closed.
+ */
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::GetChildCount(uint32_t* aChildCount)
+{
+ if (!mExpanded)
+ return NS_ERROR_NOT_AVAILABLE;
+ *aChildCount = mChildren.Count();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::GetChild(uint32_t aIndex,
+ nsINavHistoryResultNode** _retval)
+{
+ if (!mExpanded)
+ return NS_ERROR_NOT_AVAILABLE;
+ if (aIndex >= uint32_t(mChildren.Count()))
+ return NS_ERROR_INVALID_ARG;
+ NS_ADDREF(*_retval = mChildren[aIndex]);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::GetChildIndex(nsINavHistoryResultNode* aNode,
+ uint32_t* _retval)
+{
+ if (!mExpanded)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ int32_t nodeIndex = FindChild(static_cast<nsNavHistoryResultNode*>(aNode));
+ if (nodeIndex == -1)
+ return NS_ERROR_INVALID_ARG;
+
+ *_retval = nodeIndex;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryContainerResultNode::FindNodeByDetails(const nsACString& aURIString,
+ PRTime aTime,
+ int64_t aItemId,
+ bool aRecursive,
+ nsINavHistoryResultNode** _retval) {
+ if (!mExpanded)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ *_retval = nullptr;
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]->mURI.Equals(aURIString) &&
+ mChildren[i]->mTime == aTime &&
+ mChildren[i]->mItemId == aItemId) {
+ *_retval = mChildren[i];
+ break;
+ }
+
+ if (aRecursive && mChildren[i]->IsContainer()) {
+ nsNavHistoryContainerResultNode* asContainer =
+ mChildren[i]->GetAsContainer();
+ if (asContainer->mExpanded) {
+ nsresult rv = asContainer->FindNodeByDetails(aURIString, aTime,
+ aItemId,
+ aRecursive,
+ _retval);
+
+ if (NS_SUCCEEDED(rv) && _retval)
+ break;
+ }
+ }
+ }
+ NS_IF_ADDREF(*_retval);
+ return NS_OK;
+}
+
+/**
+ * HOW QUERY UPDATING WORKS
+ *
+ * Queries are different than bookmark folders in that we can not always do
+ * dynamic updates (easily) and updates are more expensive. Therefore, we do
+ * NOT query if we are not open and want to see if we have any children (for
+ * drawing a twisty) and always assume we will.
+ *
+ * When the container is opened, we execute the query and register the
+ * listeners. Like bookmark folders, we stay registered even when closed, and
+ * clear ourselves as soon as a message comes in. This lets us respond quickly
+ * if the user closes and reopens the container.
+ *
+ * We try to handle the most common notifications for the most common query
+ * types dynamically, that is, figuring out what should happen in response to
+ * a message without doing a requery. For complex changes or complex queries,
+ * we give up and requery.
+ */
+NS_IMPL_ISUPPORTS_INHERITED(nsNavHistoryQueryResultNode,
+ nsNavHistoryContainerResultNode,
+ nsINavHistoryQueryResultNode)
+
+nsNavHistoryQueryResultNode::nsNavHistoryQueryResultNode(
+ const nsACString& aTitle, const nsACString& aIconURI,
+ const nsACString& aQueryURI) :
+ nsNavHistoryContainerResultNode(aQueryURI, aTitle, aIconURI,
+ nsNavHistoryResultNode::RESULT_TYPE_QUERY,
+ nullptr),
+ mLiveUpdate(QUERYUPDATE_COMPLEX_WITH_BOOKMARKS),
+ mHasSearchTerms(false),
+ mContentsValid(false),
+ mBatchChanges(0)
+{
+}
+
+nsNavHistoryQueryResultNode::nsNavHistoryQueryResultNode(
+ const nsACString& aTitle, const nsACString& aIconURI,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions) :
+ nsNavHistoryContainerResultNode(EmptyCString(), aTitle, aIconURI,
+ nsNavHistoryResultNode::RESULT_TYPE_QUERY,
+ aOptions),
+ mQueries(aQueries),
+ mContentsValid(false),
+ mBatchChanges(0),
+ mTransitions(mQueries[0]->Transitions())
+{
+ NS_ASSERTION(aQueries.Count() > 0, "Must have at least one query");
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ASSERTION(history, "History service missing");
+ if (history) {
+ mLiveUpdate = history->GetUpdateRequirements(mQueries, mOptions,
+ &mHasSearchTerms);
+ }
+
+ // Collect transitions shared by all queries.
+ for (int32_t i = 1; i < mQueries.Count(); ++i) {
+ const nsTArray<uint32_t>& queryTransitions = mQueries[i]->Transitions();
+ for (uint32_t j = 0; j < mTransitions.Length() ; ++j) {
+ uint32_t transition = mTransitions.SafeElementAt(j, 0);
+ if (transition && !queryTransitions.Contains(transition))
+ mTransitions.RemoveElement(transition);
+ }
+ }
+}
+
+nsNavHistoryQueryResultNode::nsNavHistoryQueryResultNode(
+ const nsACString& aTitle, const nsACString& aIconURI,
+ PRTime aTime,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions) :
+ nsNavHistoryContainerResultNode(EmptyCString(), aTitle, aTime, aIconURI,
+ nsNavHistoryResultNode::RESULT_TYPE_QUERY,
+ aOptions),
+ mQueries(aQueries),
+ mContentsValid(false),
+ mBatchChanges(0),
+ mTransitions(mQueries[0]->Transitions())
+{
+ NS_ASSERTION(aQueries.Count() > 0, "Must have at least one query");
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ASSERTION(history, "History service missing");
+ if (history) {
+ mLiveUpdate = history->GetUpdateRequirements(mQueries, mOptions,
+ &mHasSearchTerms);
+ }
+
+ // Collect transitions shared by all queries.
+ for (int32_t i = 1; i < mQueries.Count(); ++i) {
+ const nsTArray<uint32_t>& queryTransitions = mQueries[i]->Transitions();
+ for (uint32_t j = 0; j < mTransitions.Length() ; ++j) {
+ uint32_t transition = mTransitions.SafeElementAt(j, 0);
+ if (transition && !queryTransitions.Contains(transition))
+ mTransitions.RemoveElement(transition);
+ }
+ }
+}
+
+nsNavHistoryQueryResultNode::~nsNavHistoryQueryResultNode() {
+ // Remove this node from result's observers. We don't need to be notified
+ // anymore.
+ if (mResult && mResult->mAllBookmarksObservers.Contains(this))
+ mResult->RemoveAllBookmarksObserver(this);
+ if (mResult && mResult->mHistoryObservers.Contains(this))
+ mResult->RemoveHistoryObserver(this);
+}
+
+/**
+ * Whoever made us may want non-expanding queries. However, we always expand
+ * when we are the root node, or else asking for non-expanding queries would be
+ * useless. A query node is not expandable if excludeItems is set or if
+ * expandQueries is unset.
+ */
+bool
+nsNavHistoryQueryResultNode::CanExpand()
+{
+ if (IsContainersQuery())
+ return true;
+
+ // If ExcludeItems is set on the root or on the node itself, don't expand.
+ if ((mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+ Options()->ExcludeItems())
+ return false;
+
+ // Check the ancestor container.
+ nsNavHistoryQueryOptions* options = GetGeneratingOptions();
+ if (options) {
+ if (options->ExcludeItems())
+ return false;
+ if (options->ExpandQueries())
+ return true;
+ }
+
+ if (mResult && mResult->mRootNode == this)
+ return true;
+
+ return false;
+}
+
+
+/**
+ * Some query with a particular result type can contain other queries. They
+ * must be always expandable
+ */
+bool
+nsNavHistoryQueryResultNode::IsContainersQuery()
+{
+ uint16_t resultType = Options()->ResultType();
+ return resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY;
+}
+
+
+/**
+ * Here we do not want to call ContainerResultNode::OnRemoving since our own
+ * ClearChildren will do the same thing and more (unregister the observers).
+ * The base ResultNode::OnRemoving will clear some regular node stats, so it
+ * is OK.
+ */
+void
+nsNavHistoryQueryResultNode::OnRemoving()
+{
+ nsNavHistoryResultNode::OnRemoving();
+ ClearChildren(true);
+ mResult = nullptr;
+}
+
+
+/**
+ * Marks the container as open, rebuilding results if they are invalid. We
+ * may still have valid results if the container was previously open and
+ * nothing happened since closing it.
+ *
+ * We do not handle CloseContainer specially. The default one just marks the
+ * container as closed, but doesn't actually mark the results as invalid.
+ * The results will be invalidated by the next history or bookmark
+ * notification that comes in. This means if you open and close the item
+ * without anything happening in between, it will be fast (this actually
+ * happens when results are used as menus).
+ */
+nsresult
+nsNavHistoryQueryResultNode::OpenContainer()
+{
+ NS_ASSERTION(!mExpanded, "Container must be closed to open it");
+ mExpanded = true;
+
+ nsresult rv;
+
+ if (!CanExpand())
+ return NS_OK;
+ if (!mContentsValid) {
+ rv = FillChildren();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = NotifyOnStateChange(STATE_CLOSED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+/**
+ * When we have valid results we can always give an exact answer. When we
+ * don't we just assume we'll have results, since actually doing the query
+ * might be hard. This is used to draw twisties on the tree, so precise results
+ * don't matter.
+ */
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetHasChildren(bool* aHasChildren)
+{
+ *aHasChildren = false;
+
+ if (!CanExpand()) {
+ return NS_OK;
+ }
+
+ uint16_t resultType = mOptions->ResultType();
+
+ // Tags are always populated, otherwise they are removed.
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS) {
+ *aHasChildren = true;
+ return NS_OK;
+ }
+
+ // For tag containers query we must check if we have any tag
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_TAG_QUERY) {
+ nsCOMPtr<nsITaggingService> tagging =
+ do_GetService(NS_TAGGINGSERVICE_CONTRACTID);
+ if (tagging) {
+ bool hasTags;
+ *aHasChildren = NS_SUCCEEDED(tagging->GetHasTags(&hasTags)) && hasTags;
+ }
+ return NS_OK;
+ }
+
+ // For history containers query we must check if we have any history
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ return history->GetHasHistoryEntries(aHasChildren);
+ }
+
+ //XXX: For other containers queries we must:
+ // 1. If it's open, just check mChildren for containers
+ // 2. Else null the view (keep it in a var), open container, check mChildren
+ // for containers, close container, reset the view
+
+ if (mContentsValid) {
+ *aHasChildren = (mChildren.Count() > 0);
+ return NS_OK;
+ }
+ *aHasChildren = true;
+ return NS_OK;
+}
+
+
+/**
+ * This doesn't just return mURI because in the case of queries that may
+ * be lazily constructed from the query objects.
+ */
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetUri(nsACString& aURI)
+{
+ nsresult rv = VerifyQueriesSerialized();
+ NS_ENSURE_SUCCESS(rv, rv);
+ aURI = mURI;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetFolderItemId(int64_t* aItemId)
+{
+ *aItemId = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetTargetFolderGuid(nsACString& aGuid) {
+ aGuid = EmptyCString();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetQueries(uint32_t* queryCount,
+ nsINavHistoryQuery*** queries)
+{
+ nsresult rv = VerifyQueriesParsed();
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(mQueries.Count() > 0, "Must have >= 1 query");
+
+ *queries = static_cast<nsINavHistoryQuery**>
+ (moz_xmalloc(mQueries.Count() * sizeof(nsINavHistoryQuery*)));
+ NS_ENSURE_TRUE(*queries, NS_ERROR_OUT_OF_MEMORY);
+
+ for (int32_t i = 0; i < mQueries.Count(); ++i)
+ NS_ADDREF((*queries)[i] = mQueries[i]);
+ *queryCount = mQueries.Count();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetQueryOptions(
+ nsINavHistoryQueryOptions** aQueryOptions)
+{
+ *aQueryOptions = Options();
+ NS_ADDREF(*aQueryOptions);
+ return NS_OK;
+}
+
+/**
+ * Safe options getter, ensures queries are parsed first.
+ */
+nsNavHistoryQueryOptions*
+nsNavHistoryQueryResultNode::Options()
+{
+ nsresult rv = VerifyQueriesParsed();
+ if (NS_FAILED(rv))
+ return nullptr;
+ NS_ASSERTION(mOptions, "Options invalid, cannot generate from URI");
+ return mOptions;
+}
+
+
+nsresult
+nsNavHistoryQueryResultNode::VerifyQueriesParsed()
+{
+ if (mQueries.Count() > 0) {
+ NS_ASSERTION(mOptions, "If a result has queries, it also needs options");
+ return NS_OK;
+ }
+ NS_ASSERTION(!mURI.IsEmpty(),
+ "Query nodes must have either a URI or query/options");
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+
+ nsresult rv = history->QueryStringToQueryArray(mURI, &mQueries,
+ getter_AddRefs(mOptions));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mLiveUpdate = history->GetUpdateRequirements(mQueries, mOptions,
+ &mHasSearchTerms);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavHistoryQueryResultNode::VerifyQueriesSerialized()
+{
+ if (!mURI.IsEmpty()) {
+ return NS_OK;
+ }
+ NS_ASSERTION(mQueries.Count() > 0 && mOptions,
+ "Query nodes must have either a URI or query/options");
+
+ nsTArray<nsINavHistoryQuery*> flatQueries;
+ flatQueries.SetCapacity(mQueries.Count());
+ for (int32_t i = 0; i < mQueries.Count(); ++i)
+ flatQueries.AppendElement(static_cast<nsINavHistoryQuery*>
+ (mQueries.ObjectAt(i)));
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+
+ nsresult rv = history->QueriesToQueryString(flatQueries.Elements(),
+ flatQueries.Length(),
+ mOptions, mURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(!mURI.IsEmpty());
+ return NS_OK;
+}
+
+
+nsresult
+nsNavHistoryQueryResultNode::FillChildren()
+{
+ NS_ASSERTION(!mContentsValid,
+ "Don't call FillChildren when contents are valid");
+ NS_ASSERTION(mChildren.Count() == 0,
+ "We are trying to fill children when there already are some");
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+
+ // get the results from the history service
+ nsresult rv = VerifyQueriesParsed();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = history->GetQueryResults(this, mQueries, mOptions, &mChildren);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // it is important to call FillStats to fill in the parents on all
+ // nodes and the result node pointers on the containers
+ FillStats();
+
+ uint16_t sortType = GetSortType();
+
+ if (mResult && mResult->mNeedsToApplySortingMode) {
+ // We should repopulate container and then apply sortingMode. To avoid
+ // sorting 2 times we simply do that here.
+ mResult->SetSortingMode(mResult->mSortingMode);
+ }
+ else if (mOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY ||
+ sortType != nsINavHistoryQueryOptions::SORT_BY_NONE) {
+ // The default SORT_BY_NONE sorts by the bookmark index (position),
+ // which we do not have for history queries.
+ // Once we've computed all tree stats, we can sort, because containers will
+ // then have proper visit counts and dates.
+ SortComparator comparator = GetSortingComparator(GetSortType());
+ if (comparator) {
+ nsAutoCString sortingAnnotation;
+ GetSortingAnnotation(sortingAnnotation);
+ // Usually containers queries results comes already sorted from the
+ // database, but some locales could have special rules to sort by title.
+ // RecursiveSort won't apply these rules to containers in containers
+ // queries because when setting sortingMode on the result we want to sort
+ // contained items (bug 473157).
+ // Base container RecursiveSort will sort both our children and all
+ // descendants, and is used in this case because we have to do manual
+ // title sorting.
+ // Query RecursiveSort will instead only sort descendants if we are a
+ // constinaersQuery, e.g. a grouped query that will return other queries.
+ // For other type of queries it will act as the base one.
+ if (IsContainersQuery() &&
+ sortType == mOptions->SortingMode() &&
+ (sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING))
+ nsNavHistoryContainerResultNode::RecursiveSort(sortingAnnotation.get(), comparator);
+ else
+ RecursiveSort(sortingAnnotation.get(), comparator);
+ }
+ }
+
+ // if we are limiting our results remove items from the end of the
+ // mChildren array after sorting. This is done for root node only.
+ // note, if count < max results, we won't do anything.
+ if (!mParent && mOptions->MaxResults()) {
+ while ((uint32_t)mChildren.Count() > mOptions->MaxResults())
+ mChildren.RemoveObjectAt(mChildren.Count() - 1);
+ }
+
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+
+ if (mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY ||
+ mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_UNIFIED) {
+ // Date containers that contain site containers have no reason to observe
+ // history, if the inside site container is expanded it will update,
+ // otherwise we are going to refresh the parent query.
+ if (!mParent || mParent->mOptions->ResultType() != nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY) {
+ // register with the result for history updates
+ result->AddHistoryObserver(this);
+ }
+ }
+
+ if (mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS ||
+ mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_UNIFIED ||
+ mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS ||
+ mHasSearchTerms) {
+ // register with the result for bookmark updates
+ result->AddAllBookmarksObserver(this);
+ }
+
+ mContentsValid = true;
+ return NS_OK;
+}
+
+
+/**
+ * Call with unregister = false when we are going to update the children (for
+ * example, when the container is open). This will clear the list and notify
+ * all the children that they are going away.
+ *
+ * When the results are becoming invalid and we are not going to refresh them,
+ * set unregister = true, which will unregister the listener from the
+ * result if any. We use unregister = false when we are refreshing the list
+ * immediately so want to stay a notifier.
+ */
+void
+nsNavHistoryQueryResultNode::ClearChildren(bool aUnregister)
+{
+ for (int32_t i = 0; i < mChildren.Count(); ++i)
+ mChildren[i]->OnRemoving();
+ mChildren.Clear();
+
+ if (aUnregister && mContentsValid) {
+ nsNavHistoryResult* result = GetResult();
+ if (result) {
+ result->RemoveHistoryObserver(this);
+ result->RemoveAllBookmarksObserver(this);
+ }
+ }
+ mContentsValid = false;
+}
+
+
+/**
+ * This is called to update the result when something has changed that we
+ * can not incrementally update.
+ */
+nsresult
+nsNavHistoryQueryResultNode::Refresh()
+{
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ if (result->mBatchInProgress) {
+ result->requestRefresh(this);
+ return NS_OK;
+ }
+
+ // This is not a root node but it does not have a parent - this means that
+ // the node has already been cleared and it is now called, because it was
+ // left in a local copy of the observers array.
+ if (mIndentLevel > -1 && !mParent)
+ return NS_OK;
+
+ // Do not refresh if we are not expanded or if we are child of a query
+ // containing other queries. In this case calling Refresh for each child
+ // query could cause a major slowdown. We should not refresh nested
+ // queries, since we will already refresh the parent one.
+ if (!mExpanded ||
+ (mParent && mParent->IsQuery() &&
+ mParent->GetAsQuery()->IsContainersQuery())) {
+ // Don't update, just invalidate and unhook
+ ClearChildren(true);
+ return NS_OK; // no updates in tree state
+ }
+
+ if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS)
+ ClearChildren(true);
+ else
+ ClearChildren(false);
+
+ // Ignore errors from FillChildren, since we will still want to refresh
+ // the tree (there just might not be anything in it on error).
+ (void)FillChildren();
+
+ NOTIFY_RESULT_OBSERVERS(result, InvalidateContainer(TO_CONTAINER(this)));
+ return NS_OK;
+}
+
+
+/**
+ * Here, we override GetSortType to return the current sorting for this
+ * query. GetSortType is used when dynamically inserting query results so we
+ * can see which comparator we should use to find the proper insertion point
+ * (it shouldn't be called from folder containers which maintain their own
+ * sorting).
+ *
+ * Normally, the container just forwards it up the chain. This is what we want
+ * for host groups, for example. For queries, we often want to use the query's
+ * sorting mode.
+ *
+ * However, we only use this query node's sorting when it is not the root.
+ * When it is the root, we use the result's sorting mode. This is because
+ * there are two cases:
+ * - You are looking at a bookmark hierarchy that contains an embedded
+ * result. We should always use the query's sort ordering since the result
+ * node's headers have nothing to do with us (and are disabled).
+ * - You are looking at a query in the tree. In this case, we want the
+ * result sorting to override ours (it should be initialized to the same
+ * sorting mode).
+ */
+uint16_t
+nsNavHistoryQueryResultNode::GetSortType()
+{
+ if (mParent)
+ return mOptions->SortingMode();
+ if (mResult)
+ return mResult->mSortingMode;
+
+ // This is a detached container, just use natural order.
+ return nsINavHistoryQueryOptions::SORT_BY_NONE;
+}
+
+
+void
+nsNavHistoryQueryResultNode::GetSortingAnnotation(nsACString& aAnnotation) {
+ if (mParent) {
+ // use our sorting, we are not the root
+ mOptions->GetSortingAnnotation(aAnnotation);
+ }
+ else if (mResult) {
+ aAnnotation.Assign(mResult->mSortingAnnotation);
+ }
+}
+
+void
+nsNavHistoryQueryResultNode::RecursiveSort(
+ const char* aData, SortComparator aComparator)
+{
+ void* data = const_cast<void*>(static_cast<const void*>(aData));
+
+ if (!IsContainersQuery())
+ mChildren.Sort(aComparator, data);
+
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]->IsContainer())
+ mChildren[i]->GetAsContainer()->RecursiveSort(aData, aComparator);
+ }
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetSkipTags(bool *aSkipTags)
+{
+ *aSkipTags = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::GetSkipDescendantsOnItemRemoval(bool *aSkipDescendantsOnItemRemoval)
+{
+ *aSkipDescendantsOnItemRemoval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnBeginUpdateBatch()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnEndUpdateBatch()
+{
+ // If the query has no children it's possible it's not yet listening to
+ // bookmarks changes, in such a case it's safer to force a refresh to gather
+ // eventual new nodes matching query options.
+ if (mChildren.Count() == 0) {
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mBatchChanges = 0;
+ return NS_OK;
+}
+
+static nsresult setHistoryDetailsCallback(nsNavHistoryResultNode* aNode,
+ const void* aClosure,
+ const nsNavHistoryResult* aResult)
+{
+ const nsNavHistoryResultNode* updatedNode =
+ static_cast<const nsNavHistoryResultNode*>(aClosure);
+
+ aNode->mAccessCount = updatedNode->mAccessCount;
+ aNode->mTime = updatedNode->mTime;
+ aNode->mFrecency = updatedNode->mFrecency;
+ aNode->mHidden = updatedNode->mHidden;
+
+ return NS_OK;
+}
+
+/**
+ * Here we need to update all copies of the URI we have with the new visit
+ * count, and potentially add a new entry in our query. This is the most
+ * common update operation and it is important that it be as efficient as
+ * possible.
+ */
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnVisit(nsIURI* aURI, int64_t aVisitId,
+ PRTime aTime, int64_t aSessionId,
+ int64_t aReferringId,
+ uint32_t aTransitionType,
+ const nsACString& aGUID,
+ bool aHidden,
+ uint32_t* aAdded)
+{
+ if (aHidden && !mOptions->IncludeHidden())
+ return NS_OK;
+
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ if (result->mBatchInProgress &&
+ ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+
+ switch(mLiveUpdate) {
+ case QUERYUPDATE_HOST: {
+ // For these simple yet common cases we can check the host ourselves
+ // before doing the overhead of creating a new result node.
+ MOZ_ASSERT(mQueries.Count() == 1,
+ "Host updated queries can have only one object");
+ RefPtr<nsNavHistoryQuery> query = do_QueryObject(mQueries[0]);
+
+ bool hasDomain;
+ query->GetHasDomain(&hasDomain);
+ if (!hasDomain)
+ return NS_OK;
+
+ nsAutoCString host;
+ if (NS_FAILED(aURI->GetAsciiHost(host)))
+ return NS_OK;
+
+ if (!query->Domain().Equals(host))
+ return NS_OK;
+
+ // Fall through to check the time, if the time is not present it will
+ // still match.
+ MOZ_FALLTHROUGH;
+ }
+
+ case QUERYUPDATE_TIME: {
+ // For these simple yet common cases we can check the time ourselves
+ // before doing the overhead of creating a new result node.
+ MOZ_ASSERT(mQueries.Count() == 1,
+ "Time updated queries can have only one object");
+ RefPtr<nsNavHistoryQuery> query = do_QueryObject(mQueries[0]);
+
+ bool hasIt;
+ query->GetHasBeginTime(&hasIt);
+ if (hasIt) {
+ PRTime beginTime = history->NormalizeTime(query->BeginTimeReference(),
+ query->BeginTime());
+ if (aTime < beginTime)
+ return NS_OK; // before our time range
+ }
+ query->GetHasEndTime(&hasIt);
+ if (hasIt) {
+ PRTime endTime = history->NormalizeTime(query->EndTimeReference(),
+ query->EndTime());
+ if (aTime > endTime)
+ return NS_OK; // after our time range
+ }
+ // Now we know that our visit satisfies the time range, fall through to
+ // the QUERYUPDATE_SIMPLE case below.
+ MOZ_FALLTHROUGH;
+ }
+
+ case QUERYUPDATE_SIMPLE: {
+ // If all of the queries are filtered by some transitions, skip the
+ // update if aTransitionType doesn't match any of them.
+ if (mTransitions.Length() > 0 && !mTransitions.Contains(aTransitionType))
+ return NS_OK;
+
+ // The history service can tell us whether the new item should appear
+ // in the result. We first have to construct a node for it to check.
+ RefPtr<nsNavHistoryResultNode> addition;
+ nsresult rv = history->VisitIdToResultNode(aVisitId, mOptions,
+ getter_AddRefs(addition));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(addition);
+ addition->mTransitionType = aTransitionType;
+ if (!history->EvaluateQueryForNode(mQueries, mOptions, addition))
+ return NS_OK; // don't need to include in our query
+
+ if (mOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_VISIT) {
+ // If this is a visit type query, just insert the new visit. We never
+ // update visits, only add or remove them.
+ rv = InsertSortedChild(addition);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ uint16_t sortType = GetSortType();
+ bool updateSorting =
+ sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING;
+
+ if (!UpdateURIs(false, true, updateSorting, addition->mURI,
+ setHistoryDetailsCallback,
+ const_cast<void*>(static_cast<void*>(addition.get())))) {
+ // Couldn't find a node to update.
+ rv = InsertSortedChild(addition);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ if (aAdded)
+ ++(*aAdded);
+
+ break;
+ }
+
+ case QUERYUPDATE_COMPLEX:
+ case QUERYUPDATE_COMPLEX_WITH_BOOKMARKS:
+ // need to requery in complex cases
+ return Refresh();
+
+ default:
+ MOZ_ASSERT(false, "Invalid value for mLiveUpdate");
+ return Refresh();
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * Find every node that matches this URI and rename it. We try to do
+ * incremental updates here, even when we are closed, because changing titles
+ * is easier than requerying if we are invalid.
+ *
+ * This actually gets called a lot. Typically, we will get an AddURI message
+ * when the user visits the page, and then the title will be set asynchronously
+ * when the title element of the page is parsed.
+ */
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnTitleChanged(nsIURI* aURI,
+ const nsAString& aPageTitle,
+ const nsACString& aGUID)
+{
+ if (!mExpanded) {
+ // When we are not expanded, we don't update, just invalidate and unhook.
+ // It would still be pretty easy to traverse the results and update the
+ // titles, but when a title changes, its unlikely that it will be the only
+ // thing. Therefore, we just give up.
+ ClearChildren(true);
+ return NS_OK; // no updates in tree state
+ }
+
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ if (result->mBatchInProgress &&
+ ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ // compute what the new title should be
+ NS_ConvertUTF16toUTF8 newTitle(aPageTitle);
+
+ bool onlyOneEntry =
+ mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_URI ||
+ mOptions->ResultType() == nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS;
+
+ // See if our queries have any search term matching.
+ if (mHasSearchTerms) {
+ // Find all matching URI nodes.
+ nsCOMArray<nsNavHistoryResultNode> matches;
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ RecursiveFindURIs(onlyOneEntry, this, spec, &matches);
+ if (matches.Count() == 0) {
+ // This could be a new node matching the query, thus we could need
+ // to add it to the result.
+ RefPtr<nsNavHistoryResultNode> node;
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->URIToResultNode(aURI, mOptions, getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (history->EvaluateQueryForNode(mQueries, mOptions, node)) {
+ rv = InsertSortedChild(node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ for (int32_t i = 0; i < matches.Count(); ++i) {
+ // For each matched node we check if it passes the query filter, if not
+ // we remove the node from the result, otherwise we'll update the title
+ // later.
+ nsNavHistoryResultNode* node = matches[i];
+ // We must check the node with the new title.
+ node->mTitle = newTitle;
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ if (!history->EvaluateQueryForNode(mQueries, mOptions, node)) {
+ nsNavHistoryContainerResultNode* parent = node->mParent;
+ // URI nodes should always have parents
+ NS_ENSURE_TRUE(parent, NS_ERROR_UNEXPECTED);
+ int32_t childIndex = parent->FindChild(node);
+ NS_ASSERTION(childIndex >= 0, "Child not found in parent");
+ parent->RemoveChildAt(childIndex);
+ }
+ }
+ }
+
+ return ChangeTitles(aURI, newTitle, true, onlyOneEntry);
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnFrecencyChanged(nsIURI* aURI,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate)
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnManyFrecenciesChanged()
+{
+ return NS_OK;
+}
+
+
+/**
+ * Here, we can always live update by just deleting all occurrences of
+ * the given URI.
+ */
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnDeleteURI(nsIURI* aURI,
+ const nsACString& aGUID,
+ uint16_t aReason)
+{
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ if (result->mBatchInProgress &&
+ ++mBatchChanges > MAX_BATCH_CHANGES_BEFORE_REFRESH) {
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ if (IsContainersQuery()) {
+ // Incremental updates of query returning queries are pretty much
+ // complicated. In this case it's possible one of the child queries has
+ // no more children and it should be removed. Unfortunately there is no
+ // way to know that without executing the child query and counting results.
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+ }
+
+ bool onlyOneEntry = (mOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_URI ||
+ mOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS);
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMArray<nsNavHistoryResultNode> matches;
+ RecursiveFindURIs(onlyOneEntry, this, spec, &matches);
+ for (int32_t i = 0; i < matches.Count(); ++i) {
+ nsNavHistoryResultNode* node = matches[i];
+ nsNavHistoryContainerResultNode* parent = node->mParent;
+ // URI nodes should always have parents
+ NS_ENSURE_TRUE(parent, NS_ERROR_UNEXPECTED);
+
+ int32_t childIndex = parent->FindChild(node);
+ NS_ASSERTION(childIndex >= 0, "Child not found in parent");
+ parent->RemoveChildAt(childIndex);
+ if (parent->mChildren.Count() == 0 && parent->IsQuery() &&
+ parent->mIndentLevel > -1) {
+ // When query subcontainers (like hosts) get empty we should remove them
+ // as well. If the parent is not the root node, append it to our list
+ // and it will get evaluated later in the loop.
+ matches.AppendObject(parent);
+ }
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnClearHistory()
+{
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+
+static nsresult setFaviconCallback(nsNavHistoryResultNode* aNode,
+ const void* aClosure,
+ const nsNavHistoryResult* aResult)
+{
+ const nsCString* newFavicon = static_cast<const nsCString*>(aClosure);
+ aNode->mFaviconURI = *newFavicon;
+
+ if (aResult && (!aNode->mParent || aNode->mParent->AreChildrenVisible()))
+ NOTIFY_RESULT_OBSERVERS(aResult, NodeIconChanged(aNode));
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnPageChanged(nsIURI* aURI,
+ uint32_t aChangedAttribute,
+ const nsAString& aNewValue,
+ const nsACString& aGUID)
+{
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (aChangedAttribute) {
+ case nsINavHistoryObserver::ATTRIBUTE_FAVICON: {
+ NS_ConvertUTF16toUTF8 newFavicon(aNewValue);
+ bool onlyOneEntry = (mOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_URI ||
+ mOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS);
+ UpdateURIs(true, onlyOneEntry, false, spec, setFaviconCallback,
+ &newFavicon);
+ break;
+ }
+ default:
+ NS_WARNING("Unknown page changed notification");
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnDeleteVisits(nsIURI* aURI,
+ PRTime aVisitTime,
+ const nsACString& aGUID,
+ uint16_t aReason,
+ uint32_t aTransitionType)
+{
+ NS_PRECONDITION(mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY,
+ "Bookmarks queries should not get a OnDeleteVisits notification");
+ if (aVisitTime == 0) {
+ // All visits for this uri have been removed, but the uri won't be removed
+ // from the databse, most likely because it's a bookmark. For a history
+ // query this is equivalent to a onDeleteURI notification.
+ nsresult rv = OnDeleteURI(aURI, aGUID, aReason);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (aTransitionType > 0) {
+ // All visits for aTransitionType have been removed, if the query is
+ // filtering on such transition type, this is equivalent to an onDeleteURI
+ // notification.
+ if (mTransitions.Length() > 0 && mTransitions.Contains(aTransitionType)) {
+ nsresult rv = OnDeleteURI(aURI, aGUID, aReason);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsNavHistoryQueryResultNode::NotifyIfTagsChanged(nsIURI* aURI)
+{
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool onlyOneEntry = (mOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_URI ||
+ mOptions->ResultType() ==
+ nsINavHistoryQueryOptions::RESULTS_AS_TAG_CONTENTS
+ );
+
+ // Find matching URI nodes.
+ RefPtr<nsNavHistoryResultNode> node;
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+
+ nsCOMArray<nsNavHistoryResultNode> matches;
+ RecursiveFindURIs(onlyOneEntry, this, spec, &matches);
+
+ if (matches.Count() == 0 && mHasSearchTerms) {
+ // A new tag has been added, it's possible it matches our query.
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->URIToResultNode(aURI, mOptions, getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (history->EvaluateQueryForNode(mQueries, mOptions, node)) {
+ rv = InsertSortedChild(node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ for (int32_t i = 0; i < matches.Count(); ++i) {
+ nsNavHistoryResultNode* node = matches[i];
+ // Force a tags update before checking the node.
+ node->mTags.SetIsVoid(true);
+ nsAutoString tags;
+ rv = node->GetTags(tags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // It's possible now this node does not respect anymore the conditions.
+ // In such a case it should be removed.
+ if (mHasSearchTerms &&
+ !history->EvaluateQueryForNode(mQueries, mOptions, node)) {
+ nsNavHistoryContainerResultNode* parent = node->mParent;
+ // URI nodes should always have parents
+ NS_ENSURE_TRUE(parent, NS_ERROR_UNEXPECTED);
+ int32_t childIndex = parent->FindChild(node);
+ NS_ASSERTION(childIndex >= 0, "Child not found in parent");
+ parent->RemoveChildAt(childIndex);
+ }
+ else {
+ NOTIFY_RESULT_OBSERVERS(result, NodeTagsChanged(node));
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * These are the bookmark observer functions for query nodes. They listen
+ * for bookmark events and refresh the results if we have any dependence on
+ * the bookmark system.
+ */
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnItemAdded(int64_t aItemId,
+ int64_t aParentId,
+ int32_t aIndex,
+ uint16_t aItemType,
+ nsIURI* aURI,
+ const nsACString& aTitle,
+ PRTime aDateAdded,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ uint16_t aSource)
+{
+ if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
+ mLiveUpdate != QUERYUPDATE_SIMPLE && mLiveUpdate != QUERYUPDATE_TIME) {
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnItemRemoved(int64_t aItemId,
+ int64_t aParentId,
+ int32_t aIndex,
+ uint16_t aItemType,
+ nsIURI* aURI,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ uint16_t aSource)
+{
+ if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
+ mLiveUpdate != QUERYUPDATE_SIMPLE && mLiveUpdate != QUERYUPDATE_TIME) {
+ nsresult rv = Refresh();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnItemChanged(int64_t aItemId,
+ const nsACString& aProperty,
+ bool aIsAnnotationProperty,
+ const nsACString& aNewValue,
+ PRTime aLastModified,
+ uint16_t aItemType,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ const nsACString& aOldValue,
+ uint16_t aSource)
+{
+ // History observers should not get OnItemChanged
+ // but should get the corresponding history notifications instead.
+ // For bookmark queries, "all bookmark" observers should get OnItemChanged.
+ // For example, when a title of a bookmark changes, we want that to refresh.
+
+ if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS) {
+ switch (aItemType) {
+ case nsINavBookmarksService::TYPE_SEPARATOR:
+ // No separators in queries.
+ return NS_OK;
+ case nsINavBookmarksService::TYPE_FOLDER:
+ // Queries never result as "folders", but the tags-query results as
+ // special "tag" containers, which should follow their corresponding
+ // folders titles.
+ if (mOptions->ResultType() != nsINavHistoryQueryOptions::RESULTS_AS_TAG_QUERY)
+ return NS_OK;
+ MOZ_FALLTHROUGH;
+ default:
+ (void)Refresh();
+ }
+ }
+ else {
+ // Some node could observe both bookmarks and history. But a node observing
+ // only history should never get a bookmark notification.
+ NS_WARNING_ASSERTION(
+ mResult && (mResult->mIsAllBookmarksObserver ||
+ mResult->mIsBookmarkFolderObserver),
+ "history observers should not get OnItemChanged, but should get the "
+ "corresponding history notifications instead");
+
+ // Tags in history queries are a special case since tags are per uri and
+ // we filter tags based on searchterms.
+ if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK &&
+ aProperty.EqualsLiteral("tags")) {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = bookmarks->GetBookmarkURI(aItemId, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NotifyIfTagsChanged(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return nsNavHistoryResultNode::OnItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty,
+ aNewValue, aLastModified,
+ aItemType, aParentId, aGUID,
+ aParentGUID, aOldValue, aSource);
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnItemVisited(int64_t aItemId,
+ int64_t aVisitId,
+ PRTime aTime,
+ uint32_t aTransitionType,
+ nsIURI* aURI,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID)
+{
+ // for bookmark queries, "all bookmark" observer should get OnItemVisited
+ // but it is ignored.
+ if (mLiveUpdate != QUERYUPDATE_COMPLEX_WITH_BOOKMARKS)
+ NS_WARNING_ASSERTION(
+ mResult && (mResult->mIsAllBookmarksObserver ||
+ mResult->mIsBookmarkFolderObserver),
+ "history observers should not get OnItemVisited, but should get OnVisit "
+ "instead");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryQueryResultNode::OnItemMoved(int64_t aFolder,
+ int64_t aOldParent,
+ int32_t aOldIndex,
+ int64_t aNewParent,
+ int32_t aNewIndex,
+ uint16_t aItemType,
+ const nsACString& aGUID,
+ const nsACString& aOldParentGUID,
+ const nsACString& aNewParentGUID,
+ uint16_t aSource)
+{
+ // 1. The query cannot be affected by the item's position
+ // 2. For the time being, we cannot optimize this not to update
+ // queries which are not restricted to some folders, due to way
+ // sub-queries are updated (see Refresh)
+ if (mLiveUpdate == QUERYUPDATE_COMPLEX_WITH_BOOKMARKS &&
+ aItemType != nsINavBookmarksService::TYPE_SEPARATOR &&
+ aOldParent != aNewParent) {
+ return Refresh();
+ }
+ return NS_OK;
+}
+
+/**
+ * HOW DYNAMIC FOLDER UPDATING WORKS
+ *
+ * When you create a result, it will automatically keep itself in sync with
+ * stuff that happens in the system. For folder nodes, this means changes to
+ * bookmarks.
+ *
+ * A folder will fill its children "when necessary." This means it is being
+ * opened or whether we need to see if it is empty for twisty drawing. It will
+ * then register its ID with the main result object that owns it. This result
+ * object will listen for all bookmark notifications and pass those
+ * notifications to folder nodes that have registered for that specific folder
+ * ID.
+ *
+ * When a bookmark folder is closed, it will not clear its children. Instead,
+ * it will keep them and also stay registered as a listener. This means that
+ * you can more quickly re-open the same folder without doing any work. This
+ * happens a lot for menus, and bookmarks don't change very often.
+ *
+ * When a message comes in and the folder is open, we will do the correct
+ * operations to keep ourselves in sync with the bookmark service. If the
+ * folder is closed, we just clear our list to mark it as invalid and
+ * unregister as a listener. This means we do not have to keep maintaining
+ * an up-to-date list for the entire bookmark menu structure in every place
+ * it is used.
+ */
+NS_IMPL_ISUPPORTS_INHERITED(nsNavHistoryFolderResultNode,
+ nsNavHistoryContainerResultNode,
+ nsINavHistoryQueryResultNode,
+ mozIStorageStatementCallback)
+
+nsNavHistoryFolderResultNode::nsNavHistoryFolderResultNode(
+ const nsACString& aTitle, nsNavHistoryQueryOptions* aOptions,
+ int64_t aFolderId) :
+ nsNavHistoryContainerResultNode(EmptyCString(), aTitle, EmptyCString(),
+ nsNavHistoryResultNode::RESULT_TYPE_FOLDER,
+ aOptions),
+ mContentsValid(false),
+ mTargetFolderItemId(aFolderId),
+ mIsRegisteredFolderObserver(false)
+{
+ mItemId = aFolderId;
+}
+
+nsNavHistoryFolderResultNode::~nsNavHistoryFolderResultNode()
+{
+ if (mIsRegisteredFolderObserver && mResult)
+ mResult->RemoveBookmarkFolderObserver(this, mTargetFolderItemId);
+}
+
+
+/**
+ * Here we do not want to call ContainerResultNode::OnRemoving since our own
+ * ClearChildren will do the same thing and more (unregister the observers).
+ * The base ResultNode::OnRemoving will clear some regular node stats, so it is
+ * OK.
+ */
+void
+nsNavHistoryFolderResultNode::OnRemoving()
+{
+ nsNavHistoryResultNode::OnRemoving();
+ ClearChildren(true);
+ mResult = nullptr;
+}
+
+
+nsresult
+nsNavHistoryFolderResultNode::OpenContainer()
+{
+ NS_ASSERTION(!mExpanded, "Container must be expanded to close it");
+ nsresult rv;
+
+ if (!mContentsValid) {
+ rv = FillChildren();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ mExpanded = true;
+
+ rv = NotifyOnStateChange(STATE_CLOSED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+/**
+ * The async version of OpenContainer.
+ */
+nsresult
+nsNavHistoryFolderResultNode::OpenContainerAsync()
+{
+ NS_ASSERTION(!mExpanded, "Container already expanded when opening it");
+
+ // If the children are valid, open the container synchronously. This will be
+ // the case when the container has already been opened and any other time
+ // FillChildren or FillChildrenAsync has previously been called.
+ if (mContentsValid)
+ return OpenContainer();
+
+ nsresult rv = FillChildrenAsync();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NotifyOnStateChange(STATE_CLOSED);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+/**
+ * @see nsNavHistoryQueryResultNode::HasChildren. The semantics here are a
+ * little different. Querying the contents of a bookmark folder is relatively
+ * fast and it is common to have empty folders. Therefore, we always want to
+ * return the correct result so that twisties are drawn properly.
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetHasChildren(bool* aHasChildren)
+{
+ if (!mContentsValid) {
+ nsresult rv = FillChildren();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ *aHasChildren = (mChildren.Count() > 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetFolderItemId(int64_t* aItemId)
+{
+ *aItemId = mTargetFolderItemId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetTargetFolderGuid(nsACString& aGuid) {
+ aGuid = mTargetFolderGuid;
+ return NS_OK;
+}
+
+/**
+ * Lazily computes the URI for this specific folder query with the current
+ * options.
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetUri(nsACString& aURI)
+{
+ if (!mURI.IsEmpty()) {
+ aURI = mURI;
+ return NS_OK;
+ }
+
+ uint32_t queryCount;
+ nsINavHistoryQuery** queries;
+ nsresult rv = GetQueries(&queryCount, &queries);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+
+ rv = history->QueriesToQueryString(queries, queryCount, mOptions, aURI);
+ for (uint32_t queryIndex = 0; queryIndex < queryCount; ++queryIndex) {
+ NS_RELEASE(queries[queryIndex]);
+ }
+ free(queries);
+ return rv;
+}
+
+
+/**
+ * @return the queries that give you this bookmarks folder
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetQueries(uint32_t* queryCount,
+ nsINavHistoryQuery*** queries)
+{
+ // get the query object
+ nsCOMPtr<nsINavHistoryQuery> query;
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ nsresult rv = history->GetNewQuery(getter_AddRefs(query));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // query just has the folder ID set and nothing else
+ rv = query->SetFolders(&mTargetFolderItemId, 1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // make array of our 1 query
+ *queries = static_cast<nsINavHistoryQuery**>
+ (moz_xmalloc(sizeof(nsINavHistoryQuery*)));
+ if (!*queries)
+ return NS_ERROR_OUT_OF_MEMORY;
+ (*queries)[0] = query.forget().take();
+ *queryCount = 1;
+ return NS_OK;
+}
+
+
+/**
+ * Options for the query that gives you this bookmarks folder. This is just
+ * the options for the folder with the current folder ID set.
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetQueryOptions(
+ nsINavHistoryQueryOptions** aQueryOptions)
+{
+ NS_ASSERTION(mOptions, "Options invalid");
+
+ *aQueryOptions = mOptions;
+ NS_ADDREF(*aQueryOptions);
+ return NS_OK;
+}
+
+
+nsresult
+nsNavHistoryFolderResultNode::FillChildren()
+{
+ NS_ASSERTION(!mContentsValid,
+ "Don't call FillChildren when contents are valid");
+ NS_ASSERTION(mChildren.Count() == 0,
+ "We are trying to fill children when there already are some");
+
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+
+ // Actually get the folder children from the bookmark service.
+ nsresult rv = bookmarks->QueryFolderChildren(mTargetFolderItemId, mOptions, &mChildren);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // PERFORMANCE: it may be better to also fill any child folders at this point
+ // so that we can draw tree twisties without doing a separate query later.
+ // If we don't end up drawing twisties a lot, it doesn't matter. If we do
+ // this, we should wrap everything in a transaction here on the bookmark
+ // service's connection.
+
+ return OnChildrenFilled();
+}
+
+
+/**
+ * Performs some tasks after all the children of the container have been added.
+ * The container's contents are not valid until this method has been called.
+ */
+nsresult
+nsNavHistoryFolderResultNode::OnChildrenFilled()
+{
+ // It is important to call FillStats to fill in the parents on all
+ // nodes and the result node pointers on the containers.
+ FillStats();
+
+ if (mResult && mResult->mNeedsToApplySortingMode) {
+ // We should repopulate container and then apply sortingMode. To avoid
+ // sorting 2 times we simply do that here.
+ mResult->SetSortingMode(mResult->mSortingMode);
+ }
+ else {
+ // Once we've computed all tree stats, we can sort, because containers will
+ // then have proper visit counts and dates.
+ SortComparator comparator = GetSortingComparator(GetSortType());
+ if (comparator) {
+ nsAutoCString sortingAnnotation;
+ GetSortingAnnotation(sortingAnnotation);
+ RecursiveSort(sortingAnnotation.get(), comparator);
+ }
+ }
+
+ // If we are limiting our results remove items from the end of the
+ // mChildren array after sorting. This is done for root node only.
+ // Note, if count < max results, we won't do anything.
+ if (!mParent && mOptions->MaxResults()) {
+ while ((uint32_t)mChildren.Count() > mOptions->MaxResults())
+ mChildren.RemoveObjectAt(mChildren.Count() - 1);
+ }
+
+ // Register with the result for updates.
+ EnsureRegisteredAsFolderObserver();
+
+ mContentsValid = true;
+ return NS_OK;
+}
+
+
+/**
+ * Registers the node with its result as a folder observer if it is not already
+ * registered.
+ */
+void
+nsNavHistoryFolderResultNode::EnsureRegisteredAsFolderObserver()
+{
+ if (!mIsRegisteredFolderObserver && mResult) {
+ mResult->AddBookmarkFolderObserver(this, mTargetFolderItemId);
+ mIsRegisteredFolderObserver = true;
+ }
+}
+
+
+/**
+ * The async version of FillChildren. This begins asynchronous execution by
+ * calling nsNavBookmarks::QueryFolderChildrenAsync. During execution, this
+ * node's async Storage callbacks, HandleResult and HandleCompletion, will be
+ * called.
+ */
+nsresult
+nsNavHistoryFolderResultNode::FillChildrenAsync()
+{
+ NS_ASSERTION(!mContentsValid, "FillChildrenAsync when contents are valid");
+ NS_ASSERTION(mChildren.Count() == 0, "FillChildrenAsync when children exist");
+
+ // ProcessFolderNodeChild, called in HandleResult, increments this for every
+ // result row it processes. Initialize it here as we begin async execution.
+ mAsyncBookmarkIndex = -1;
+
+ nsNavBookmarks* bmSvc = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bmSvc, NS_ERROR_OUT_OF_MEMORY);
+ nsresult rv =
+ bmSvc->QueryFolderChildrenAsync(this, mTargetFolderItemId,
+ getter_AddRefs(mAsyncPendingStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Register with the result for updates. All updates during async execution
+ // will cause it to be restarted.
+ EnsureRegisteredAsFolderObserver();
+
+ return NS_OK;
+}
+
+
+/**
+ * A mozIStorageStatementCallback method. Called during the async execution
+ * begun by FillChildrenAsync.
+ *
+ * @param aResultSet
+ * The result set containing the data from the database.
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::HandleResult(mozIStorageResultSet* aResultSet)
+{
+ NS_ENSURE_ARG_POINTER(aResultSet);
+
+ nsNavBookmarks* bmSvc = nsNavBookmarks::GetBookmarksService();
+ if (!bmSvc) {
+ CancelAsyncOpen(false);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Consume all the currently available rows of the result set.
+ nsCOMPtr<mozIStorageRow> row;
+ while (NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row) {
+ nsresult rv = bmSvc->ProcessFolderNodeRow(row, mOptions, &mChildren,
+ mAsyncBookmarkIndex);
+ if (NS_FAILED(rv)) {
+ CancelAsyncOpen(false);
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+
+/**
+ * A mozIStorageStatementCallback method. Called during the async execution
+ * begun by FillChildrenAsync.
+ *
+ * @param aReason
+ * Indicates the final state of execution.
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::HandleCompletion(uint16_t aReason)
+{
+ if (aReason == mozIStorageStatementCallback::REASON_FINISHED &&
+ mAsyncCanceledState == NOT_CANCELED) {
+ // Async execution successfully completed. The container is ready to open.
+
+ nsresult rv = OnChildrenFilled();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mExpanded = true;
+ mAsyncPendingStmt = nullptr;
+
+ // Notify observers only after mExpanded and mAsyncPendingStmt are set.
+ rv = NotifyOnStateChange(STATE_LOADING);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ else if (mAsyncCanceledState == CANCELED_RESTART_NEEDED) {
+ // Async execution was canceled and needs to be restarted.
+ mAsyncCanceledState = NOT_CANCELED;
+ ClearChildren(false);
+ FillChildrenAsync();
+ }
+
+ else {
+ // Async execution failed or was canceled without restart. Remove all
+ // children and close the container, notifying observers.
+ mAsyncCanceledState = NOT_CANCELED;
+ ClearChildren(true);
+ CloseContainer();
+ }
+
+ return NS_OK;
+}
+
+
+void
+nsNavHistoryFolderResultNode::ClearChildren(bool unregister)
+{
+ for (int32_t i = 0; i < mChildren.Count(); ++i)
+ mChildren[i]->OnRemoving();
+ mChildren.Clear();
+
+ bool needsUnregister = unregister && (mContentsValid || mAsyncPendingStmt);
+ if (needsUnregister && mResult && mIsRegisteredFolderObserver) {
+ mResult->RemoveBookmarkFolderObserver(this, mTargetFolderItemId);
+ mIsRegisteredFolderObserver = false;
+ }
+ mContentsValid = false;
+}
+
+
+/**
+ * This is called to update the result when something has changed that we
+ * can not incrementally update.
+ */
+nsresult
+nsNavHistoryFolderResultNode::Refresh()
+{
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+ if (result->mBatchInProgress) {
+ result->requestRefresh(this);
+ return NS_OK;
+ }
+
+ ClearChildren(true);
+
+ if (!mExpanded) {
+ // When we are not expanded, we don't update, just invalidate and unhook.
+ return NS_OK;
+ }
+
+ // Ignore errors from FillChildren, since we will still want to refresh
+ // the tree (there just might not be anything in it on error). ClearChildren
+ // has unregistered us as an observer since FillChildren will try to
+ // re-register us.
+ (void)FillChildren();
+
+ NOTIFY_RESULT_OBSERVERS(result, InvalidateContainer(TO_CONTAINER(this)));
+ return NS_OK;
+}
+
+
+/**
+ * Implements the logic described above the constructor. This sees if we
+ * should do an incremental update and returns true if so. If not, it
+ * invalidates our children, unregisters us an observer, and returns false.
+ */
+bool
+nsNavHistoryFolderResultNode::StartIncrementalUpdate()
+{
+ // if any items are excluded, we can not do incremental updates since the
+ // indices from the bookmark service will not be valid
+
+ if (!mOptions->ExcludeItems() &&
+ !mOptions->ExcludeQueries() &&
+ !mOptions->ExcludeReadOnlyFolders()) {
+ // easy case: we are visible, always do incremental update
+ if (mExpanded || AreChildrenVisible())
+ return true;
+
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_TRUE(result, false);
+
+ // When any observers are attached also do incremental updates if our
+ // parent is visible, so that twisties are drawn correctly.
+ if (mParent)
+ return result->mObservers.Length() > 0;
+ }
+
+ // otherwise, we don't do incremental updates, invalidate and unregister
+ (void)Refresh();
+ return false;
+}
+
+
+/**
+ * This function adds aDelta to all bookmark indices between the two endpoints,
+ * inclusive. It is used when items are added or removed from the bookmark
+ * folder.
+ */
+void
+nsNavHistoryFolderResultNode::ReindexRange(int32_t aStartIndex,
+ int32_t aEndIndex,
+ int32_t aDelta)
+{
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ nsNavHistoryResultNode* node = mChildren[i];
+ if (node->mBookmarkIndex >= aStartIndex &&
+ node->mBookmarkIndex <= aEndIndex)
+ node->mBookmarkIndex += aDelta;
+ }
+}
+
+
+/**
+ * Searches this folder for a node with the given id/target-folder-id.
+ *
+ * @return the node if found, null otherwise.
+ * @note Does not addref the node!
+ */
+nsNavHistoryResultNode*
+nsNavHistoryFolderResultNode::FindChildById(int64_t aItemId,
+ uint32_t* aNodeIndex)
+{
+ for (int32_t i = 0; i < mChildren.Count(); ++i) {
+ if (mChildren[i]->mItemId == aItemId ||
+ (mChildren[i]->IsFolder() &&
+ mChildren[i]->GetAsFolder()->mTargetFolderItemId == aItemId)) {
+ *aNodeIndex = i;
+ return mChildren[i];
+ }
+ }
+ return nullptr;
+}
+
+
+// Used by nsNavHistoryFolderResultNode's nsINavBookmarkObserver methods below.
+// If the container is notified of a bookmark event while asynchronous execution
+// is pending, this restarts it and returns.
+#define RESTART_AND_RETURN_IF_ASYNC_PENDING() \
+ if (mAsyncPendingStmt) { \
+ CancelAsyncOpen(true); \
+ return NS_OK; \
+ }
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetSkipTags(bool *aSkipTags)
+{
+ *aSkipTags = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::GetSkipDescendantsOnItemRemoval(bool *aSkipDescendantsOnItemRemoval)
+{
+ *aSkipDescendantsOnItemRemoval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnBeginUpdateBatch()
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnEndUpdateBatch()
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnItemAdded(int64_t aItemId,
+ int64_t aParentFolder,
+ int32_t aIndex,
+ uint16_t aItemType,
+ nsIURI* aURI,
+ const nsACString& aTitle,
+ PRTime aDateAdded,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ uint16_t aSource)
+{
+ MOZ_ASSERT(aParentFolder == mTargetFolderItemId, "Got wrong bookmark update");
+
+ RESTART_AND_RETURN_IF_ASYNC_PENDING();
+
+ {
+ uint32_t index;
+ nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+ // Bug 1097528.
+ // It's possible our result registered due to a previous notification, for
+ // example the Library left pane could have refreshed and replaced the
+ // right pane as a consequence. In such a case our contents are already
+ // up-to-date. That's OK.
+ if (node)
+ return NS_OK;
+ }
+
+ bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+ (mParent && mParent->mOptions->ExcludeItems()) ||
+ mOptions->ExcludeItems();
+
+ // here, try to do something reasonable if the bookmark service gives us
+ // a bogus index.
+ if (aIndex < 0) {
+ NS_NOTREACHED("Invalid index for item adding: <0");
+ aIndex = 0;
+ }
+ else if (aIndex > mChildren.Count()) {
+ if (!excludeItems) {
+ // Something wrong happened while updating indexes.
+ NS_NOTREACHED("Invalid index for item adding: greater than count");
+ }
+ aIndex = mChildren.Count();
+ }
+
+ nsresult rv;
+
+ // Check for query URIs, which are bookmarks, but treated as containers
+ // in results and views.
+ bool isQuery = false;
+ if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) {
+ NS_ASSERTION(aURI, "Got a null URI when we are a bookmark?!");
+ nsAutoCString itemURISpec;
+ rv = aURI->GetSpec(itemURISpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ isQuery = IsQueryURI(itemURISpec);
+ }
+
+ if (aItemType != nsINavBookmarksService::TYPE_FOLDER &&
+ !isQuery && excludeItems) {
+ // don't update items when we aren't displaying them, but we still need
+ // to adjust bookmark indices to account for the insertion
+ ReindexRange(aIndex, INT32_MAX, 1);
+ return NS_OK;
+ }
+
+ if (!StartIncrementalUpdate())
+ return NS_OK; // folder was completely refreshed for us
+
+ // adjust indices to account for insertion
+ ReindexRange(aIndex, INT32_MAX, 1);
+
+ RefPtr<nsNavHistoryResultNode> node;
+ if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ rv = history->BookmarkIdToResultNode(aItemId, mOptions, getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else if (aItemType == nsINavBookmarksService::TYPE_FOLDER) {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+ rv = bookmarks->ResultNodeForContainer(aItemId, mOptions, getter_AddRefs(node));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ else if (aItemType == nsINavBookmarksService::TYPE_SEPARATOR) {
+ node = new nsNavHistorySeparatorResultNode();
+ NS_ENSURE_TRUE(node, NS_ERROR_OUT_OF_MEMORY);
+ node->mItemId = aItemId;
+ node->mBookmarkGuid = aGUID;
+ node->mDateAdded = aDateAdded;
+ node->mLastModified = aDateAdded;
+ }
+
+ node->mBookmarkIndex = aIndex;
+
+ if (aItemType == nsINavBookmarksService::TYPE_SEPARATOR ||
+ GetSortType() == nsINavHistoryQueryOptions::SORT_BY_NONE) {
+ // insert at natural bookmarks position
+ return InsertChildAt(node, aIndex);
+ }
+
+ // insert at sorted position
+ return InsertSortedChild(node);
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnItemRemoved(int64_t aItemId,
+ int64_t aParentFolder,
+ int32_t aIndex,
+ uint16_t aItemType,
+ nsIURI* aURI,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ uint16_t aSource)
+{
+ // Folder shortcuts should not be notified removal of the target folder.
+ MOZ_ASSERT_IF(mItemId != mTargetFolderItemId, aItemId != mTargetFolderItemId);
+ // Concrete folders should not be notified their own removal.
+ // Note aItemId may equal mItemId for recursive folder shortcuts.
+ MOZ_ASSERT_IF(mItemId == mTargetFolderItemId, aItemId != mItemId);
+
+ // In any case though, here we only care about the children removal.
+ if (mTargetFolderItemId == aItemId || mItemId == aItemId)
+ return NS_OK;
+
+ MOZ_ASSERT(aParentFolder == mTargetFolderItemId, "Got wrong bookmark update");
+
+ RESTART_AND_RETURN_IF_ASYNC_PENDING();
+
+ // don't trust the index from the bookmark service, find it ourselves. The
+ // sorting could be different, or the bookmark services indices and ours might
+ // be out of sync somehow.
+ uint32_t index;
+ nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+ // Bug 1097528.
+ // It's possible our result registered due to a previous notification, for
+ // example the Library left pane could have refreshed and replaced the
+ // right pane as a consequence. In such a case our contents are already
+ // up-to-date. That's OK.
+ if (!node) {
+ return NS_OK;
+ }
+
+ bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+ (mParent && mParent->mOptions->ExcludeItems()) ||
+ mOptions->ExcludeItems();
+ if ((node->IsURI() || node->IsSeparator()) && excludeItems) {
+ // don't update items when we aren't displaying them, but we do need to
+ // adjust everybody's bookmark indices to account for the removal
+ ReindexRange(aIndex, INT32_MAX, -1);
+ return NS_OK;
+ }
+
+ if (!StartIncrementalUpdate())
+ return NS_OK; // we are completely refreshed
+
+ // shift all following indices down
+ ReindexRange(aIndex + 1, INT32_MAX, -1);
+
+ return RemoveChildAt(index);
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResultNode::OnItemChanged(int64_t aItemId,
+ const nsACString& aProperty,
+ bool aIsAnnotationProperty,
+ const nsACString& aNewValue,
+ PRTime aLastModified,
+ uint16_t aItemType,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ const nsACString& aOldValue,
+ uint16_t aSource)
+{
+ if (aItemId != mItemId)
+ return NS_OK;
+
+ mLastModified = aLastModified;
+
+ nsNavHistoryResult* result = GetResult();
+ NS_ENSURE_STATE(result);
+
+ bool shouldNotify = !mParent || mParent->AreChildrenVisible();
+
+ if (aIsAnnotationProperty) {
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeAnnotationChanged(this, aProperty));
+ }
+ else if (aProperty.EqualsLiteral("title")) {
+ // XXX: what should we do if the new title is void?
+ mTitle = aNewValue;
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeTitleChanged(this, mTitle));
+ }
+ else if (aProperty.EqualsLiteral("uri")) {
+ // clear the tags string as well
+ mTags.SetIsVoid(true);
+ mURI = aNewValue;
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeURIChanged(this, mURI));
+ }
+ else if (aProperty.EqualsLiteral("favicon")) {
+ mFaviconURI = aNewValue;
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeIconChanged(this));
+ }
+ else if (aProperty.EqualsLiteral("cleartime")) {
+ mTime = 0;
+ if (shouldNotify) {
+ NOTIFY_RESULT_OBSERVERS(result,
+ NodeHistoryDetailsChanged(this, 0, mAccessCount));
+ }
+ }
+ else if (aProperty.EqualsLiteral("tags")) {
+ mTags.SetIsVoid(true);
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeTagsChanged(this));
+ }
+ else if (aProperty.EqualsLiteral("dateAdded")) {
+ // aNewValue has the date as a string, but we can use aLastModified,
+ // because it's set to the same value when dateAdded is changed.
+ mDateAdded = aLastModified;
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeDateAddedChanged(this, mDateAdded));
+ }
+ else if (aProperty.EqualsLiteral("lastModified")) {
+ if (shouldNotify) {
+ NOTIFY_RESULT_OBSERVERS(result,
+ NodeLastModifiedChanged(this, aLastModified));
+ }
+ }
+ else if (aProperty.EqualsLiteral("keyword")) {
+ if (shouldNotify)
+ NOTIFY_RESULT_OBSERVERS(result, NodeKeywordChanged(this, aNewValue));
+ }
+ else
+ NS_NOTREACHED("Unknown bookmark property changing.");
+
+ if (!mParent)
+ return NS_OK;
+
+ // DO NOT OPTIMIZE THIS TO CHECK aProperty
+ // The sorting methods fall back to each other so we need to re-sort the
+ // result even if it's not set to sort by the given property.
+ int32_t ourIndex = mParent->FindChild(this);
+ NS_ASSERTION(ourIndex >= 0, "Could not find self in parent");
+ if (ourIndex >= 0)
+ mParent->EnsureItemPosition(ourIndex);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnItemChanged(int64_t aItemId,
+ const nsACString& aProperty,
+ bool aIsAnnotationProperty,
+ const nsACString& aNewValue,
+ PRTime aLastModified,
+ uint16_t aItemType,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ const nsACString& aOldValue,
+ uint16_t aSource)
+{
+ RESTART_AND_RETURN_IF_ASYNC_PENDING();
+
+ return nsNavHistoryResultNode::OnItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty,
+ aNewValue, aLastModified,
+ aItemType, aParentId, aGUID,
+ aParentGUID, aOldValue, aSource);
+}
+
+/**
+ * Updates visit count and last visit time and refreshes.
+ */
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnItemVisited(int64_t aItemId,
+ int64_t aVisitId,
+ PRTime aTime,
+ uint32_t aTransitionType,
+ nsIURI* aURI,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID)
+{
+ bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+ (mParent && mParent->mOptions->ExcludeItems()) ||
+ mOptions->ExcludeItems();
+ if (excludeItems)
+ return NS_OK; // don't update items when we aren't displaying them
+
+ RESTART_AND_RETURN_IF_ASYNC_PENDING();
+
+ if (!StartIncrementalUpdate())
+ return NS_OK;
+
+ uint32_t nodeIndex;
+ nsNavHistoryResultNode* node = FindChildById(aItemId, &nodeIndex);
+ if (!node)
+ return NS_ERROR_FAILURE;
+
+ // Update node.
+ node->mTime = aTime;
+ ++node->mAccessCount;
+
+ // Update us.
+ int32_t oldAccessCount = mAccessCount;
+ ++mAccessCount;
+ if (aTime > mTime)
+ mTime = aTime;
+ nsresult rv = ReverseUpdateStats(mAccessCount - oldAccessCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update frecency for proper frecency ordering.
+ // TODO (bug 832617): we may avoid one query here, by providing the new
+ // frecency value in the notification.
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_OK);
+ RefPtr<nsNavHistoryResultNode> visitNode;
+ rv = history->VisitIdToResultNode(aVisitId, mOptions,
+ getter_AddRefs(visitNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_STATE(visitNode);
+ node->mFrecency = visitNode->mFrecency;
+
+ if (AreChildrenVisible()) {
+ // Sorting has not changed, just redraw the row if it's visible.
+ nsNavHistoryResult* result = GetResult();
+ NOTIFY_RESULT_OBSERVERS(result,
+ NodeHistoryDetailsChanged(node, mTime, mAccessCount));
+ }
+
+ // Update sorting if necessary.
+ uint32_t sortType = GetSortType();
+ if (sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING ||
+ sortType == nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING) {
+ int32_t childIndex = FindChild(node);
+ NS_ASSERTION(childIndex >= 0, "Could not find child we just got a reference to");
+ if (childIndex >= 0) {
+ EnsureItemPosition(childIndex);
+ }
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryFolderResultNode::OnItemMoved(int64_t aItemId,
+ int64_t aOldParent,
+ int32_t aOldIndex,
+ int64_t aNewParent,
+ int32_t aNewIndex,
+ uint16_t aItemType,
+ const nsACString& aGUID,
+ const nsACString& aOldParentGUID,
+ const nsACString& aNewParentGUID,
+ uint16_t aSource)
+{
+ NS_ASSERTION(aOldParent == mTargetFolderItemId || aNewParent == mTargetFolderItemId,
+ "Got a bookmark message that doesn't belong to us");
+
+ RESTART_AND_RETURN_IF_ASYNC_PENDING();
+
+ uint32_t index;
+ nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
+ // Bug 1097528.
+ // It's possible our result registered due to a previous notification, for
+ // example the Library left pane could have refreshed and replaced the
+ // right pane as a consequence. In such a case our contents are already
+ // up-to-date. That's OK.
+ if (node && aNewParent == mTargetFolderItemId && index == static_cast<uint32_t>(aNewIndex))
+ return NS_OK;
+ if (!node && aOldParent == mTargetFolderItemId)
+ return NS_OK;
+
+ bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
+ (mParent && mParent->mOptions->ExcludeItems()) ||
+ mOptions->ExcludeItems();
+ if (node && excludeItems && (node->IsURI() || node->IsSeparator())) {
+ // Don't update items when we aren't displaying them.
+ return NS_OK;
+ }
+
+ if (!StartIncrementalUpdate())
+ return NS_OK; // entire container was refreshed for us
+
+ if (aOldParent == aNewParent) {
+ // getting moved within the same folder, we don't want to do a remove and
+ // an add because that will lose your tree state.
+
+ // adjust bookmark indices
+ ReindexRange(aOldIndex + 1, INT32_MAX, -1);
+ ReindexRange(aNewIndex, INT32_MAX, 1);
+
+ MOZ_ASSERT(node, "Can't find folder that is moving!");
+ if (!node) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(index < uint32_t(mChildren.Count()), "Invalid index!");
+ node->mBookmarkIndex = aNewIndex;
+
+ // adjust position
+ EnsureItemPosition(index);
+ return NS_OK;
+ } else {
+ // moving between two different folders, just do a remove and an add
+ nsCOMPtr<nsIURI> itemURI;
+ nsAutoCString itemTitle;
+ if (aItemType == nsINavBookmarksService::TYPE_BOOKMARK) {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
+ nsresult rv = bookmarks->GetBookmarkURI(aItemId, getter_AddRefs(itemURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = bookmarks->GetItemTitle(aItemId, itemTitle);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ if (aOldParent == mTargetFolderItemId) {
+ OnItemRemoved(aItemId, aOldParent, aOldIndex, aItemType, itemURI,
+ aGUID, aOldParentGUID, aSource);
+ }
+ if (aNewParent == mTargetFolderItemId) {
+ OnItemAdded(aItemId, aNewParent, aNewIndex, aItemType, itemURI, itemTitle,
+ RoundedPRNow(), // This is a dummy dateAdded, not the real value.
+ aGUID, aNewParentGUID, aSource);
+ }
+ }
+ return NS_OK;
+}
+
+
+/**
+ * Separator nodes do not hold any data.
+ */
+nsNavHistorySeparatorResultNode::nsNavHistorySeparatorResultNode()
+ : nsNavHistoryResultNode(EmptyCString(), EmptyCString(),
+ 0, 0, EmptyCString())
+{
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsNavHistoryResult)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsNavHistoryResult)
+ tmp->StopObserving();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mObservers)
+ for (auto it = tmp->mBookmarkFolderObservers.Iter(); !it.Done(); it.Next()) {
+ delete it.Data();
+ it.Remove();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAllBookmarksObservers)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mHistoryObservers)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsNavHistoryResult)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObservers)
+ for (auto it = tmp->mBookmarkFolderObservers.Iter(); !it.Done(); it.Next()) {
+ nsNavHistoryResult::FolderObserverList*& list = it.Data();
+ for (uint32_t i = 0; i < list->Length(); ++i) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb,
+ "mBookmarkFolderObservers value[i]");
+ nsNavHistoryResultNode* node = list->ElementAt(i);
+ cb.NoteXPCOMChild(node);
+ }
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAllBookmarksObservers)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHistoryObservers)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsNavHistoryResult)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsNavHistoryResult)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsNavHistoryResult)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryResult)
+ NS_INTERFACE_MAP_STATIC_AMBIGUOUS(nsNavHistoryResult)
+ NS_INTERFACE_MAP_ENTRY(nsINavHistoryResult)
+ NS_INTERFACE_MAP_ENTRY(nsINavBookmarkObserver)
+ NS_INTERFACE_MAP_ENTRY(nsINavHistoryObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+nsNavHistoryResult::nsNavHistoryResult(nsNavHistoryContainerResultNode* aRoot)
+ : mRootNode(aRoot)
+ , mNeedsToApplySortingMode(false)
+ , mIsHistoryObserver(false)
+ , mIsBookmarkFolderObserver(false)
+ , mIsAllBookmarksObserver(false)
+ , mBookmarkFolderObservers(64)
+ , mBatchInProgress(false)
+ , mSuppressNotifications(false)
+{
+ mRootNode->mResult = this;
+}
+
+nsNavHistoryResult::~nsNavHistoryResult()
+{
+ // Delete all heap-allocated bookmark folder observer arrays.
+ for (auto it = mBookmarkFolderObservers.Iter(); !it.Done(); it.Next()) {
+ delete it.Data();
+ it.Remove();
+ }
+}
+
+void
+nsNavHistoryResult::StopObserving()
+{
+ if (mIsBookmarkFolderObserver || mIsAllBookmarksObserver) {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ if (bookmarks) {
+ bookmarks->RemoveObserver(this);
+ mIsBookmarkFolderObserver = false;
+ mIsAllBookmarksObserver = false;
+ }
+ }
+ if (mIsHistoryObserver) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ if (history) {
+ history->RemoveObserver(this);
+ mIsHistoryObserver = false;
+ }
+ }
+}
+
+/**
+ * @note you must call AddRef before this, since we may do things like
+ * register ourselves.
+ */
+nsresult
+nsNavHistoryResult::Init(nsINavHistoryQuery** aQueries,
+ uint32_t aQueryCount,
+ nsNavHistoryQueryOptions *aOptions)
+{
+ nsresult rv;
+ NS_ASSERTION(aOptions, "Must have valid options");
+ NS_ASSERTION(aQueries && aQueryCount > 0, "Must have >1 query in result");
+
+ // Fill saved source queries with copies of the original (the caller might
+ // change their original objects, and we always want to reflect the source
+ // parameters).
+ for (uint32_t i = 0; i < aQueryCount; ++i) {
+ nsCOMPtr<nsINavHistoryQuery> queryClone;
+ rv = aQueries[i]->Clone(getter_AddRefs(queryClone));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mQueries.AppendObject(queryClone))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ rv = aOptions->Clone(getter_AddRefs(mOptions));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSortingMode = aOptions->SortingMode();
+ rv = aOptions->GetSortingAnnotation(mSortingAnnotation);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(mRootNode->mIndentLevel == -1,
+ "Root node's indent level initialized wrong");
+ mRootNode->FillStats();
+
+ return NS_OK;
+}
+
+
+/**
+ * Constructs a new history result object.
+ */
+nsresult // static
+nsNavHistoryResult::NewHistoryResult(nsINavHistoryQuery** aQueries,
+ uint32_t aQueryCount,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryContainerResultNode* aRoot,
+ bool aBatchInProgress,
+ nsNavHistoryResult** result)
+{
+ *result = new nsNavHistoryResult(aRoot);
+ if (!*result)
+ return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(*result); // must happen before Init
+ // Correctly set mBatchInProgress for the result based on the root node value.
+ (*result)->mBatchInProgress = aBatchInProgress;
+ nsresult rv = (*result)->Init(aQueries, aQueryCount, aOptions);
+ if (NS_FAILED(rv)) {
+ NS_RELEASE(*result);
+ *result = nullptr;
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+
+void
+nsNavHistoryResult::AddHistoryObserver(nsNavHistoryQueryResultNode* aNode)
+{
+ if (!mIsHistoryObserver) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ASSERTION(history, "Can't create history service");
+ history->AddObserver(this, true);
+ mIsHistoryObserver = true;
+ }
+ // Don't add duplicate observers. In some case we don't unregister when
+ // children are cleared (see ClearChildren) and the next FillChildren call
+ // will try to add the observer again.
+ if (mHistoryObservers.IndexOf(aNode) == mHistoryObservers.NoIndex) {
+ mHistoryObservers.AppendElement(aNode);
+ }
+}
+
+
+void
+nsNavHistoryResult::AddAllBookmarksObserver(nsNavHistoryQueryResultNode* aNode)
+{
+ if (!mIsAllBookmarksObserver && !mIsBookmarkFolderObserver) {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ if (!bookmarks) {
+ NS_NOTREACHED("Can't create bookmark service");
+ return;
+ }
+ bookmarks->AddObserver(this, true);
+ mIsAllBookmarksObserver = true;
+ }
+ // Don't add duplicate observers. In some case we don't unregister when
+ // children are cleared (see ClearChildren) and the next FillChildren call
+ // will try to add the observer again.
+ if (mAllBookmarksObservers.IndexOf(aNode) == mAllBookmarksObservers.NoIndex) {
+ mAllBookmarksObservers.AppendElement(aNode);
+ }
+}
+
+
+void
+nsNavHistoryResult::AddBookmarkFolderObserver(nsNavHistoryFolderResultNode* aNode,
+ int64_t aFolder)
+{
+ if (!mIsBookmarkFolderObserver && !mIsAllBookmarksObserver) {
+ nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
+ if (!bookmarks) {
+ NS_NOTREACHED("Can't create bookmark service");
+ return;
+ }
+ bookmarks->AddObserver(this, true);
+ mIsBookmarkFolderObserver = true;
+ }
+ // Don't add duplicate observers. In some case we don't unregister when
+ // children are cleared (see ClearChildren) and the next FillChildren call
+ // will try to add the observer again.
+ FolderObserverList* list = BookmarkFolderObserversForId(aFolder, true);
+ if (list->IndexOf(aNode) == list->NoIndex) {
+ list->AppendElement(aNode);
+ }
+}
+
+
+void
+nsNavHistoryResult::RemoveHistoryObserver(nsNavHistoryQueryResultNode* aNode)
+{
+ mHistoryObservers.RemoveElement(aNode);
+}
+
+
+void
+nsNavHistoryResult::RemoveAllBookmarksObserver(nsNavHistoryQueryResultNode* aNode)
+{
+ mAllBookmarksObservers.RemoveElement(aNode);
+}
+
+
+void
+nsNavHistoryResult::RemoveBookmarkFolderObserver(nsNavHistoryFolderResultNode* aNode,
+ int64_t aFolder)
+{
+ FolderObserverList* list = BookmarkFolderObserversForId(aFolder, false);
+ if (!list)
+ return; // we don't even have an entry for that folder
+ list->RemoveElement(aNode);
+}
+
+
+nsNavHistoryResult::FolderObserverList*
+nsNavHistoryResult::BookmarkFolderObserversForId(int64_t aFolderId, bool aCreate)
+{
+ FolderObserverList* list;
+ if (mBookmarkFolderObservers.Get(aFolderId, &list))
+ return list;
+ if (!aCreate)
+ return nullptr;
+
+ // need to create a new list
+ list = new FolderObserverList;
+ mBookmarkFolderObservers.Put(aFolderId, list);
+ return list;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetSortingMode(uint16_t* aSortingMode)
+{
+ *aSortingMode = mSortingMode;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::SetSortingMode(uint16_t aSortingMode)
+{
+ NS_ENSURE_STATE(mRootNode);
+
+ if (aSortingMode > nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING)
+ return NS_ERROR_INVALID_ARG;
+
+ // Keep everything in sync.
+ NS_ASSERTION(mOptions, "Options should always be present for a root query");
+
+ mSortingMode = aSortingMode;
+
+ if (!mRootNode->mExpanded) {
+ // Need to do this later when node will be expanded.
+ mNeedsToApplySortingMode = true;
+ return NS_OK;
+ }
+
+ // Actually do sorting.
+ nsNavHistoryContainerResultNode::SortComparator comparator =
+ nsNavHistoryContainerResultNode::GetSortingComparator(aSortingMode);
+ if (comparator) {
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
+ mRootNode->RecursiveSort(mSortingAnnotation.get(), comparator);
+ }
+
+ NOTIFY_RESULT_OBSERVERS(this, SortingChanged(aSortingMode));
+ NOTIFY_RESULT_OBSERVERS(this, InvalidateContainer(mRootNode));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetSortingAnnotation(nsACString& _result) {
+ _result.Assign(mSortingAnnotation);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::SetSortingAnnotation(const nsACString& aSortingAnnotation) {
+ mSortingAnnotation.Assign(aSortingAnnotation);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::AddObserver(nsINavHistoryResultObserver* aObserver,
+ bool aOwnsWeak)
+{
+ NS_ENSURE_ARG(aObserver);
+ nsresult rv = mObservers.AppendWeakElement(aObserver, aOwnsWeak);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aObserver->SetResult(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we are batching, notify a fake batch start to the observers.
+ // Not doing so would then notify a not coupled batch end.
+ if (mBatchInProgress) {
+ NOTIFY_RESULT_OBSERVERS(this, Batching(true));
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::RemoveObserver(nsINavHistoryResultObserver* aObserver)
+{
+ NS_ENSURE_ARG(aObserver);
+ return mObservers.RemoveWeakElement(aObserver);
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetSuppressNotifications(bool* _retval)
+{
+ *_retval = mSuppressNotifications;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::SetSuppressNotifications(bool aSuppressNotifications)
+{
+ mSuppressNotifications = aSuppressNotifications;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetRoot(nsINavHistoryContainerResultNode** aRoot)
+{
+ if (!mRootNode) {
+ NS_NOTREACHED("Root is null");
+ *aRoot = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<nsNavHistoryContainerResultNode> node(mRootNode);
+ node.forget(aRoot);
+ return NS_OK;
+}
+
+
+void
+nsNavHistoryResult::requestRefresh(nsNavHistoryContainerResultNode* aContainer)
+{
+ // Don't add twice the same container.
+ if (mRefreshParticipants.IndexOf(aContainer) == mRefreshParticipants.NoIndex)
+ mRefreshParticipants.AppendElement(aContainer);
+}
+
+// nsINavBookmarkObserver implementation
+
+// Here, it is important that we create a COPY of the observer array. Some
+// observers will requery themselves, which may cause the observer array to
+// be modified or added to.
+#define ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(_folderId, _functionCall) \
+ PR_BEGIN_MACRO \
+ FolderObserverList* _fol = BookmarkFolderObserversForId(_folderId, false); \
+ if (_fol) { \
+ FolderObserverList _listCopy(*_fol); \
+ for (uint32_t _fol_i = 0; _fol_i < _listCopy.Length(); ++_fol_i) { \
+ if (_listCopy[_fol_i]) \
+ _listCopy[_fol_i]->_functionCall; \
+ } \
+ } \
+ PR_END_MACRO
+#define ENUMERATE_LIST_OBSERVERS(_listType, _functionCall, _observersList, _conditionCall) \
+ PR_BEGIN_MACRO \
+ _listType _listCopy(_observersList); \
+ for (uint32_t _obs_i = 0; _obs_i < _listCopy.Length(); ++_obs_i) { \
+ if (_listCopy[_obs_i] && _listCopy[_obs_i]->_conditionCall) \
+ _listCopy[_obs_i]->_functionCall; \
+ } \
+ PR_END_MACRO
+#define ENUMERATE_QUERY_OBSERVERS(_functionCall, _observersList, _conditionCall) \
+ ENUMERATE_LIST_OBSERVERS(QueryObserverList, _functionCall, _observersList, _conditionCall)
+#define ENUMERATE_ALL_BOOKMARKS_OBSERVERS(_functionCall) \
+ ENUMERATE_QUERY_OBSERVERS(_functionCall, mAllBookmarksObservers, IsQuery())
+#define ENUMERATE_HISTORY_OBSERVERS(_functionCall) \
+ ENUMERATE_QUERY_OBSERVERS(_functionCall, mHistoryObservers, IsQuery())
+
+#define NOTIFY_REFRESH_PARTICIPANTS() \
+ PR_BEGIN_MACRO \
+ ENUMERATE_LIST_OBSERVERS(ContainerObserverList, Refresh(), mRefreshParticipants, IsContainer()); \
+ mRefreshParticipants.Clear(); \
+ PR_END_MACRO
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetSkipTags(bool *aSkipTags)
+{
+ *aSkipTags = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryResult::GetSkipDescendantsOnItemRemoval(bool *aSkipDescendantsOnItemRemoval)
+{
+ *aSkipDescendantsOnItemRemoval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnBeginUpdateBatch()
+{
+ // Since we could be observing both history and bookmarks, it's possible both
+ // notify the batch. We can safely ignore nested calls.
+ if (!mBatchInProgress) {
+ mBatchInProgress = true;
+ ENUMERATE_HISTORY_OBSERVERS(OnBeginUpdateBatch());
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnBeginUpdateBatch());
+
+ NOTIFY_RESULT_OBSERVERS(this, Batching(true));
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnEndUpdateBatch()
+{
+ // Since we could be observing both history and bookmarks, it's possible both
+ // notify the batch. We can safely ignore nested calls.
+ // Notice it's possible we are notified OnEndUpdateBatch more times than
+ // onBeginUpdateBatch, since the result could be created in the middle of
+ // nested batches.
+ if (mBatchInProgress) {
+ ENUMERATE_HISTORY_OBSERVERS(OnEndUpdateBatch());
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnEndUpdateBatch());
+
+ // Setting mBatchInProgress before notifying the end of the batch to
+ // observers would make evantual calls to Refresh() directly handled rather
+ // than enqueued. Thus set it just before handling refreshes.
+ mBatchInProgress = false;
+ NOTIFY_REFRESH_PARTICIPANTS();
+ NOTIFY_RESULT_OBSERVERS(this, Batching(false));
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnItemAdded(int64_t aItemId,
+ int64_t aParentId,
+ int32_t aIndex,
+ uint16_t aItemType,
+ nsIURI* aURI,
+ const nsACString& aTitle,
+ PRTime aDateAdded,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG(aItemType != nsINavBookmarksService::TYPE_BOOKMARK ||
+ aURI);
+
+ ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(aParentId,
+ OnItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
+ aGUID, aParentGUID, aSource)
+ );
+ ENUMERATE_HISTORY_OBSERVERS(
+ OnItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
+ aGUID, aParentGUID, aSource)
+ );
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
+ OnItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
+ aGUID, aParentGUID, aSource)
+ );
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnItemRemoved(int64_t aItemId,
+ int64_t aParentId,
+ int32_t aIndex,
+ uint16_t aItemType,
+ nsIURI* aURI,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ uint16_t aSource)
+{
+ NS_ENSURE_ARG(aItemType != nsINavBookmarksService::TYPE_BOOKMARK ||
+ aURI);
+
+ ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(aParentId,
+ OnItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID,
+ aParentGUID, aSource));
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
+ OnItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID,
+ aParentGUID, aSource));
+ ENUMERATE_HISTORY_OBSERVERS(
+ OnItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID,
+ aParentGUID, aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnItemChanged(int64_t aItemId,
+ const nsACString &aProperty,
+ bool aIsAnnotationProperty,
+ const nsACString &aNewValue,
+ PRTime aLastModified,
+ uint16_t aItemType,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ const nsACString& aOldValue,
+ uint16_t aSource)
+{
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
+ OnItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue,
+ aLastModified, aItemType, aParentId, aGUID, aParentGUID,
+ aOldValue, aSource));
+
+ // Note: folder-nodes set their own bookmark observer only once they're
+ // opened, meaning we cannot optimize this code path for changes done to
+ // folder-nodes.
+
+ FolderObserverList* list = BookmarkFolderObserversForId(aParentId, false);
+ if (!list)
+ return NS_OK;
+
+ for (uint32_t i = 0; i < list->Length(); ++i) {
+ RefPtr<nsNavHistoryFolderResultNode> folder = list->ElementAt(i);
+ if (folder) {
+ uint32_t nodeIndex;
+ RefPtr<nsNavHistoryResultNode> node =
+ folder->FindChildById(aItemId, &nodeIndex);
+ // if ExcludeItems is true we don't update non visible items
+ bool excludeItems = (mRootNode->mOptions->ExcludeItems()) ||
+ folder->mOptions->ExcludeItems();
+ if (node &&
+ (!excludeItems || !(node->IsURI() || node->IsSeparator())) &&
+ folder->StartIncrementalUpdate()) {
+ node->OnItemChanged(aItemId, aProperty, aIsAnnotationProperty,
+ aNewValue, aLastModified, aItemType, aParentId,
+ aGUID, aParentGUID, aOldValue, aSource);
+ }
+ }
+ }
+
+ // Note: we do NOT call history observers in this case. This notification is
+ // the same as other history notification, except that here we know the item
+ // is a bookmark. History observers will handle the history notification
+ // instead.
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnItemVisited(int64_t aItemId,
+ int64_t aVisitId,
+ PRTime aVisitTime,
+ uint32_t aTransitionType,
+ nsIURI* aURI,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID)
+{
+ NS_ENSURE_ARG(aURI);
+
+ ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(aParentId,
+ OnItemVisited(aItemId, aVisitId, aVisitTime, aTransitionType, aURI,
+ aParentId, aGUID, aParentGUID));
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(
+ OnItemVisited(aItemId, aVisitId, aVisitTime, aTransitionType, aURI,
+ aParentId, aGUID, aParentGUID));
+ // Note: we do NOT call history observers in this case. This notification is
+ // the same as OnVisit, except that here we know the item is a bookmark.
+ // History observers will handle the history notification instead.
+ return NS_OK;
+}
+
+
+/**
+ * Need to notify both the source and the destination folders (if they are
+ * different).
+ */
+NS_IMETHODIMP
+nsNavHistoryResult::OnItemMoved(int64_t aItemId,
+ int64_t aOldParent,
+ int32_t aOldIndex,
+ int64_t aNewParent,
+ int32_t aNewIndex,
+ uint16_t aItemType,
+ const nsACString& aGUID,
+ const nsACString& aOldParentGUID,
+ const nsACString& aNewParentGUID,
+ uint16_t aSource)
+{
+ ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(aOldParent,
+ OnItemMoved(aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex,
+ aItemType, aGUID, aOldParentGUID, aNewParentGUID, aSource));
+ if (aNewParent != aOldParent) {
+ ENUMERATE_BOOKMARK_FOLDER_OBSERVERS(aNewParent,
+ OnItemMoved(aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex,
+ aItemType, aGUID, aOldParentGUID, aNewParentGUID, aSource));
+ }
+ ENUMERATE_ALL_BOOKMARKS_OBSERVERS(OnItemMoved(aItemId, aOldParent, aOldIndex,
+ aNewParent, aNewIndex,
+ aItemType, aGUID,
+ aOldParentGUID,
+ aNewParentGUID, aSource));
+ ENUMERATE_HISTORY_OBSERVERS(OnItemMoved(aItemId, aOldParent, aOldIndex,
+ aNewParent, aNewIndex, aItemType,
+ aGUID, aOldParentGUID,
+ aNewParentGUID, aSource));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
+ int64_t aSessionId, int64_t aReferringId,
+ uint32_t aTransitionType, const nsACString& aGUID,
+ bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
+{
+ NS_ENSURE_ARG(aURI);
+
+ // Embed visits are never shown in our views.
+ if (aTransitionType == nsINavHistoryService::TRANSITION_EMBED) {
+ return NS_OK;
+ }
+
+ uint32_t added = 0;
+
+ ENUMERATE_HISTORY_OBSERVERS(OnVisit(aURI, aVisitId, aTime, aSessionId,
+ aReferringId, aTransitionType, aGUID,
+ aHidden, &added));
+
+ if (!mRootNode->mExpanded)
+ return NS_OK;
+
+ // If this visit is accepted by an overlapped container, and not all
+ // overlapped containers are visible, we should still call Refresh if the
+ // visit falls into any of them.
+ bool todayIsMissing = false;
+ uint32_t resultType = mRootNode->mOptions->ResultType();
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY) {
+ uint32_t childCount;
+ nsresult rv = mRootNode->GetChildCount(&childCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (childCount) {
+ nsCOMPtr<nsINavHistoryResultNode> firstChild;
+ rv = mRootNode->GetChild(0, getter_AddRefs(firstChild));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString title;
+ rv = firstChild->GetTitle(title);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsNavHistory* history = nsNavHistory::GetHistoryService();
+ NS_ENSURE_TRUE(history, NS_OK);
+ nsAutoCString todayLabel;
+ history->GetStringFromName(
+ u"finduri-AgeInDays-is-0", todayLabel);
+ todayIsMissing = !todayLabel.Equals(title);
+ }
+ }
+
+ if (!added || todayIsMissing) {
+ // None of registered query observers has accepted our URI. This means,
+ // that a matching query either was not expanded or it does not exist.
+ uint32_t resultType = mRootNode->mOptions->ResultType();
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY ||
+ resultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY) {
+ // If the visit falls into the Today bucket and the bucket exists, it was
+ // just not expanded, thus there's no reason to update.
+ int64_t beginOfToday =
+ nsNavHistory::NormalizeTime(nsINavHistoryQuery::TIME_RELATIVE_TODAY, 0);
+ if (todayIsMissing || aTime < beginOfToday) {
+ (void)mRootNode->GetAsQuery()->Refresh();
+ }
+ return NS_OK;
+ }
+
+ if (resultType == nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY) {
+ (void)mRootNode->GetAsQuery()->Refresh();
+ return NS_OK;
+ }
+
+ // We are result of a folder node, then we should run through history
+ // observers that are containers queries and refresh them.
+ // We use a copy of the observers array since requerying could potentially
+ // cause changes to the array.
+ ENUMERATE_QUERY_OBSERVERS(Refresh(), mHistoryObservers, IsContainersQuery());
+ }
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnTitleChanged(nsIURI* aURI,
+ const nsAString& aPageTitle,
+ const nsACString& aGUID)
+{
+ NS_ENSURE_ARG(aURI);
+
+ ENUMERATE_HISTORY_OBSERVERS(OnTitleChanged(aURI, aPageTitle, aGUID));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnFrecencyChanged(nsIURI* aURI,
+ int32_t aNewFrecency,
+ const nsACString& aGUID,
+ bool aHidden,
+ PRTime aLastVisitDate)
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnManyFrecenciesChanged()
+{
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnDeleteURI(nsIURI *aURI,
+ const nsACString& aGUID,
+ uint16_t aReason)
+{
+ NS_ENSURE_ARG(aURI);
+
+ ENUMERATE_HISTORY_OBSERVERS(OnDeleteURI(aURI, aGUID, aReason));
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnClearHistory()
+{
+ ENUMERATE_HISTORY_OBSERVERS(OnClearHistory());
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistoryResult::OnPageChanged(nsIURI* aURI,
+ uint32_t aChangedAttribute,
+ const nsAString& aValue,
+ const nsACString& aGUID)
+{
+ NS_ENSURE_ARG(aURI);
+
+ ENUMERATE_HISTORY_OBSERVERS(OnPageChanged(aURI, aChangedAttribute, aValue, aGUID));
+ return NS_OK;
+}
+
+
+/**
+ * Don't do anything when visits expire.
+ */
+NS_IMETHODIMP
+nsNavHistoryResult::OnDeleteVisits(nsIURI* aURI,
+ PRTime aVisitTime,
+ const nsACString& aGUID,
+ uint16_t aReason,
+ uint32_t aTransitionType)
+{
+ NS_ENSURE_ARG(aURI);
+
+ ENUMERATE_HISTORY_OBSERVERS(OnDeleteVisits(aURI, aVisitTime, aGUID, aReason,
+ aTransitionType));
+ return NS_OK;
+}
diff --git a/toolkit/components/places/nsNavHistoryResult.h b/toolkit/components/places/nsNavHistoryResult.h
new file mode 100644
index 0000000000..fffe2bf136
--- /dev/null
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -0,0 +1,782 @@
+/* -*- 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/. */
+
+/**
+ * The definitions of objects that make up a history query result set. This file
+ * should only be included by nsNavHistory.h, include that if you want these
+ * classes.
+ */
+
+#ifndef nsNavHistoryResult_h_
+#define nsNavHistoryResult_h_
+
+#include "nsTArray.h"
+#include "nsInterfaceHashtable.h"
+#include "nsDataHashtable.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/storage.h"
+#include "Helpers.h"
+
+class nsNavHistory;
+class nsNavHistoryQuery;
+class nsNavHistoryQueryOptions;
+
+class nsNavHistoryContainerResultNode;
+class nsNavHistoryFolderResultNode;
+class nsNavHistoryQueryResultNode;
+
+/**
+ * hashkey wrapper using int64_t KeyType
+ *
+ * @see nsTHashtable::EntryType for specification
+ *
+ * This just truncates the 64-bit int to a 32-bit one for using a hash number.
+ * It is used for bookmark folder IDs, which should be way less than 2^32.
+ */
+class nsTrimInt64HashKey : public PLDHashEntryHdr
+{
+public:
+ typedef const int64_t& KeyType;
+ typedef const int64_t* KeyTypePointer;
+
+ explicit nsTrimInt64HashKey(KeyTypePointer aKey) : mValue(*aKey) { }
+ nsTrimInt64HashKey(const nsTrimInt64HashKey& toCopy) : mValue(toCopy.mValue) { }
+ ~nsTrimInt64HashKey() { }
+
+ KeyType GetKey() const { return mValue; }
+ bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey)
+ { return static_cast<uint32_t>((*aKey) & UINT32_MAX); }
+ enum { ALLOW_MEMMOVE = true };
+
+private:
+ const int64_t mValue;
+};
+
+
+// Declare methods for implementing nsINavBookmarkObserver
+// and nsINavHistoryObserver (some methods, such as BeginUpdateBatch overlap)
+#define NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE(...) \
+ NS_DECL_NSINAVBOOKMARKOBSERVER \
+ NS_IMETHOD OnTitleChanged(nsIURI* aURI, const nsAString& aPageTitle, \
+ const nsACString& aGUID) __VA_ARGS__; \
+ NS_IMETHOD OnFrecencyChanged(nsIURI* aURI, int32_t aNewFrecency, \
+ const nsACString& aGUID, bool aHidden, \
+ PRTime aLastVisitDate) __VA_ARGS__; \
+ NS_IMETHOD OnManyFrecenciesChanged() __VA_ARGS__; \
+ NS_IMETHOD OnDeleteURI(nsIURI *aURI, const nsACString& aGUID, \
+ uint16_t aReason) __VA_ARGS__; \
+ NS_IMETHOD OnClearHistory() __VA_ARGS__; \
+ NS_IMETHOD OnPageChanged(nsIURI *aURI, uint32_t aChangedAttribute, \
+ const nsAString &aNewValue, \
+ const nsACString &aGUID) __VA_ARGS__; \
+ NS_IMETHOD OnDeleteVisits(nsIURI* aURI, PRTime aVisitTime, \
+ const nsACString& aGUID, uint16_t aReason, \
+ uint32_t aTransitionType) __VA_ARGS__;
+
+// The internal version has an output aAdded parameter, it is incremented by
+// query nodes when the visited uri belongs to them. If no such query exists,
+// the history result creates a new query node dynamically.
+#define NS_DECL_BOOKMARK_HISTORY_OBSERVER_INTERNAL \
+ NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE() \
+ NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime, \
+ int64_t aSessionId, int64_t aReferringId, \
+ uint32_t aTransitionType, const nsACString& aGUID, \
+ bool aHidden, uint32_t* aAdded);
+
+// The external version is used by results.
+#define NS_DECL_BOOKMARK_HISTORY_OBSERVER_EXTERNAL(...) \
+ NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE(__VA_ARGS__) \
+ NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime, \
+ int64_t aSessionId, int64_t aReferringId, \
+ uint32_t aTransitionType, const nsACString& aGUID, \
+ bool aHidden, uint32_t aVisitCount, uint32_t aTyped) __VA_ARGS__;
+
+// nsNavHistoryResult
+//
+// nsNavHistory creates this object and fills in mChildren (by getting
+// it through GetTopLevel()). Then FilledAllResults() is called to finish
+// object initialization.
+
+#define NS_NAVHISTORYRESULT_IID \
+ { 0x455d1d40, 0x1b9b, 0x40e6, { 0xa6, 0x41, 0x8b, 0xb7, 0xe8, 0x82, 0x23, 0x87 } }
+
+class nsNavHistoryResult final : public nsSupportsWeakReference,
+ public nsINavHistoryResult,
+ public nsINavBookmarkObserver,
+ public nsINavHistoryObserver
+{
+public:
+ static nsresult NewHistoryResult(nsINavHistoryQuery** aQueries,
+ uint32_t aQueryCount,
+ nsNavHistoryQueryOptions* aOptions,
+ nsNavHistoryContainerResultNode* aRoot,
+ bool aBatchInProgress,
+ nsNavHistoryResult** result);
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_NAVHISTORYRESULT_IID)
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSINAVHISTORYRESULT
+ NS_DECL_BOOKMARK_HISTORY_OBSERVER_EXTERNAL(override)
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsNavHistoryResult, nsINavHistoryResult)
+
+ void AddHistoryObserver(nsNavHistoryQueryResultNode* aNode);
+ void AddBookmarkFolderObserver(nsNavHistoryFolderResultNode* aNode, int64_t aFolder);
+ void AddAllBookmarksObserver(nsNavHistoryQueryResultNode* aNode);
+ void RemoveHistoryObserver(nsNavHistoryQueryResultNode* aNode);
+ void RemoveBookmarkFolderObserver(nsNavHistoryFolderResultNode* aNode, int64_t aFolder);
+ void RemoveAllBookmarksObserver(nsNavHistoryQueryResultNode* aNode);
+ void StopObserving();
+
+public:
+ // two-stage init, use NewHistoryResult to construct
+ explicit nsNavHistoryResult(nsNavHistoryContainerResultNode* mRoot);
+ nsresult Init(nsINavHistoryQuery** aQueries,
+ uint32_t aQueryCount,
+ nsNavHistoryQueryOptions *aOptions);
+
+ RefPtr<nsNavHistoryContainerResultNode> mRootNode;
+
+ nsCOMArray<nsINavHistoryQuery> mQueries;
+ nsCOMPtr<nsNavHistoryQueryOptions> mOptions;
+
+ // One of nsNavHistoryQueryOptions.SORY_BY_* This is initialized to mOptions.sortingMode,
+ // but may be overridden if the user clicks on one of the columns.
+ uint16_t mSortingMode;
+ // If root node is closed and we try to apply a sortingMode, it would not
+ // work. So we will apply it when the node will be reopened and populated.
+ // This var states the fact we need to apply sortingMode in such a situation.
+ bool mNeedsToApplySortingMode;
+
+ // The sorting annotation to be used for in SORT_BY_ANNOTATION_* modes
+ nsCString mSortingAnnotation;
+
+ // node observers
+ bool mIsHistoryObserver;
+ bool mIsBookmarkFolderObserver;
+ bool mIsAllBookmarksObserver;
+
+ typedef nsTArray< RefPtr<nsNavHistoryQueryResultNode> > QueryObserverList;
+ QueryObserverList mHistoryObservers;
+ QueryObserverList mAllBookmarksObservers;
+
+ typedef nsTArray< RefPtr<nsNavHistoryFolderResultNode> > FolderObserverList;
+ nsDataHashtable<nsTrimInt64HashKey, FolderObserverList*> mBookmarkFolderObservers;
+ FolderObserverList* BookmarkFolderObserversForId(int64_t aFolderId, bool aCreate);
+
+ typedef nsTArray< RefPtr<nsNavHistoryContainerResultNode> > ContainerObserverList;
+
+ void RecursiveExpandCollapse(nsNavHistoryContainerResultNode* aContainer,
+ bool aExpand);
+
+ void InvalidateTree();
+
+ bool mBatchInProgress;
+
+ nsMaybeWeakPtrArray<nsINavHistoryResultObserver> mObservers;
+ bool mSuppressNotifications;
+
+ ContainerObserverList mRefreshParticipants;
+ void requestRefresh(nsNavHistoryContainerResultNode* aContainer);
+
+protected:
+ virtual ~nsNavHistoryResult();
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsNavHistoryResult, NS_NAVHISTORYRESULT_IID)
+
+// nsNavHistoryResultNode
+//
+// This is the base class for every node in a result set. The result itself
+// is a node (nsNavHistoryResult inherits from this), as well as every
+// leaf and branch on the tree.
+
+#define NS_NAVHISTORYRESULTNODE_IID \
+ {0x54b61d38, 0x57c1, 0x11da, {0x95, 0xb8, 0x00, 0x13, 0x21, 0xc9, 0xf6, 0x9e}}
+
+// These are all the simple getters, they can be used for the result node
+// implementation and all subclasses. More complex are GetIcon, GetParent
+// (which depends on the definition of container result node), and GetUri
+// (which is overridded for lazy construction for some containers).
+#define NS_IMPLEMENT_SIMPLE_RESULTNODE \
+ NS_IMETHOD GetTitle(nsACString& aTitle) override \
+ { aTitle = mTitle; return NS_OK; } \
+ NS_IMETHOD GetAccessCount(uint32_t* aAccessCount) override \
+ { *aAccessCount = mAccessCount; return NS_OK; } \
+ NS_IMETHOD GetTime(PRTime* aTime) override \
+ { *aTime = mTime; return NS_OK; } \
+ NS_IMETHOD GetIndentLevel(int32_t* aIndentLevel) override \
+ { *aIndentLevel = mIndentLevel; return NS_OK; } \
+ NS_IMETHOD GetBookmarkIndex(int32_t* aIndex) override \
+ { *aIndex = mBookmarkIndex; return NS_OK; } \
+ NS_IMETHOD GetDateAdded(PRTime* aDateAdded) override \
+ { *aDateAdded = mDateAdded; return NS_OK; } \
+ NS_IMETHOD GetLastModified(PRTime* aLastModified) override \
+ { *aLastModified = mLastModified; return NS_OK; } \
+ NS_IMETHOD GetItemId(int64_t* aId) override \
+ { *aId = mItemId; return NS_OK; }
+
+// This is used by the base classes instead of
+// NS_FORWARD_NSINAVHISTORYRESULTNODE(nsNavHistoryResultNode) because they
+// need to redefine GetType and GetUri rather than forwarding them. This
+// implements all the simple getters instead of forwarding because they are so
+// short and we can save a virtual function call.
+//
+// (GetUri is redefined only by QueryResultNode and FolderResultNode because
+// the queries might not necessarily be parsed. The rest just return the node's
+// buffer.)
+#define NS_FORWARD_COMMON_RESULTNODE_TO_BASE \
+ NS_IMPLEMENT_SIMPLE_RESULTNODE \
+ NS_IMETHOD GetIcon(nsACString& aIcon) override \
+ { return nsNavHistoryResultNode::GetIcon(aIcon); } \
+ NS_IMETHOD GetParent(nsINavHistoryContainerResultNode** aParent) override \
+ { return nsNavHistoryResultNode::GetParent(aParent); } \
+ NS_IMETHOD GetParentResult(nsINavHistoryResult** aResult) override \
+ { return nsNavHistoryResultNode::GetParentResult(aResult); } \
+ NS_IMETHOD GetTags(nsAString& aTags) override \
+ { return nsNavHistoryResultNode::GetTags(aTags); } \
+ NS_IMETHOD GetPageGuid(nsACString& aPageGuid) override \
+ { return nsNavHistoryResultNode::GetPageGuid(aPageGuid); } \
+ NS_IMETHOD GetBookmarkGuid(nsACString& aBookmarkGuid) override \
+ { return nsNavHistoryResultNode::GetBookmarkGuid(aBookmarkGuid); } \
+ NS_IMETHOD GetVisitId(int64_t* aVisitId) override \
+ { return nsNavHistoryResultNode::GetVisitId(aVisitId); } \
+ NS_IMETHOD GetFromVisitId(int64_t* aFromVisitId) override \
+ { return nsNavHistoryResultNode::GetFromVisitId(aFromVisitId); } \
+ NS_IMETHOD GetVisitType(uint32_t* aVisitType) override \
+ { return nsNavHistoryResultNode::GetVisitType(aVisitType); }
+
+class nsNavHistoryResultNode : public nsINavHistoryResultNode
+{
+public:
+ nsNavHistoryResultNode(const nsACString& aURI, const nsACString& aTitle,
+ uint32_t aAccessCount, PRTime aTime,
+ const nsACString& aIconURI);
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_NAVHISTORYRESULTNODE_IID)
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsNavHistoryResultNode)
+
+ NS_IMPLEMENT_SIMPLE_RESULTNODE
+ NS_IMETHOD GetIcon(nsACString& aIcon) override;
+ NS_IMETHOD GetParent(nsINavHistoryContainerResultNode** aParent) override;
+ NS_IMETHOD GetParentResult(nsINavHistoryResult** aResult) override;
+ NS_IMETHOD GetType(uint32_t* type) override
+ { *type = nsNavHistoryResultNode::RESULT_TYPE_URI; return NS_OK; }
+ NS_IMETHOD GetUri(nsACString& aURI) override
+ { aURI = mURI; return NS_OK; }
+ NS_IMETHOD GetTags(nsAString& aTags) override;
+ NS_IMETHOD GetPageGuid(nsACString& aPageGuid) override;
+ NS_IMETHOD GetBookmarkGuid(nsACString& aBookmarkGuid) override;
+ NS_IMETHOD GetVisitId(int64_t* aVisitId) override;
+ NS_IMETHOD GetFromVisitId(int64_t* aFromVisitId) override;
+ NS_IMETHOD GetVisitType(uint32_t* aVisitType) override;
+
+ virtual void OnRemoving();
+
+ // Called from result's onItemChanged, see also bookmark observer declaration in
+ // nsNavHistoryFolderResultNode
+ NS_IMETHOD OnItemChanged(int64_t aItemId,
+ const nsACString &aProperty,
+ bool aIsAnnotationProperty,
+ const nsACString &aValue,
+ PRTime aNewLastModified,
+ uint16_t aItemType,
+ int64_t aParentId,
+ const nsACString& aGUID,
+ const nsACString& aParentGUID,
+ const nsACString &aOldValue,
+ uint16_t aSource);
+
+protected:
+ virtual ~nsNavHistoryResultNode() {}
+
+public:
+
+ nsNavHistoryResult* GetResult();
+ nsNavHistoryQueryOptions* GetGeneratingOptions();
+
+ // These functions test the type. We don't use a virtual function since that
+ // would take a vtable slot for every one of (potentially very many) nodes.
+ // Note that GetType() already has a vtable slot because its on the iface.
+ bool IsTypeContainer(uint32_t type) {
+ return type == nsINavHistoryResultNode::RESULT_TYPE_QUERY ||
+ type == nsINavHistoryResultNode::RESULT_TYPE_FOLDER ||
+ type == nsINavHistoryResultNode::RESULT_TYPE_FOLDER_SHORTCUT;
+ }
+ bool IsContainer() {
+ uint32_t type;
+ GetType(&type);
+ return IsTypeContainer(type);
+ }
+ static bool IsTypeURI(uint32_t type) {
+ return type == nsINavHistoryResultNode::RESULT_TYPE_URI;
+ }
+ bool IsURI() {
+ uint32_t type;
+ GetType(&type);
+ return IsTypeURI(type);
+ }
+ static bool IsTypeFolder(uint32_t type) {
+ return type == nsINavHistoryResultNode::RESULT_TYPE_FOLDER ||
+ type == nsINavHistoryResultNode::RESULT_TYPE_FOLDER_SHORTCUT;
+ }
+ bool IsFolder() {
+ uint32_t type;
+ GetType(&type);
+ return IsTypeFolder(type);
+ }
+ static bool IsTypeQuery(uint32_t type) {
+ return type == nsINavHistoryResultNode::RESULT_TYPE_QUERY;
+ }
+ bool IsQuery() {
+ uint32_t type;
+ GetType(&type);
+ return IsTypeQuery(type);
+ }
+ bool IsSeparator() {
+ uint32_t type;
+ GetType(&type);
+ return type == nsINavHistoryResultNode::RESULT_TYPE_SEPARATOR;
+ }
+ nsNavHistoryContainerResultNode* GetAsContainer() {
+ NS_ASSERTION(IsContainer(), "Not a container");
+ return reinterpret_cast<nsNavHistoryContainerResultNode*>(this);
+ }
+ nsNavHistoryFolderResultNode* GetAsFolder() {
+ NS_ASSERTION(IsFolder(), "Not a folder");
+ return reinterpret_cast<nsNavHistoryFolderResultNode*>(this);
+ }
+ nsNavHistoryQueryResultNode* GetAsQuery() {
+ NS_ASSERTION(IsQuery(), "Not a query");
+ return reinterpret_cast<nsNavHistoryQueryResultNode*>(this);
+ }
+
+ RefPtr<nsNavHistoryContainerResultNode> mParent;
+ nsCString mURI; // not necessarily valid for containers, call GetUri
+ nsCString mTitle;
+ nsString mTags;
+ bool mAreTagsSorted;
+ uint32_t mAccessCount;
+ int64_t mTime;
+ nsCString mFaviconURI;
+ int32_t mBookmarkIndex;
+ int64_t mItemId;
+ int64_t mFolderId;
+ int64_t mVisitId;
+ int64_t mFromVisitId;
+ PRTime mDateAdded;
+ PRTime mLastModified;
+
+ // The indent level of this node. The root node will have a value of -1. The
+ // root's children will have a value of 0, and so on.
+ int32_t mIndentLevel;
+
+ // Frecency of the page. Valid only for URI nodes.
+ int32_t mFrecency;
+
+ // Hidden status of the page. Valid only for URI nodes.
+ bool mHidden;
+
+ // Transition type used when this node represents a single visit.
+ uint32_t mTransitionType;
+
+ // Unique Id of the page.
+ nsCString mPageGuid;
+
+ // Unique Id of the bookmark.
+ nsCString mBookmarkGuid;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsNavHistoryResultNode, NS_NAVHISTORYRESULTNODE_IID)
+
+
+// nsNavHistoryContainerResultNode
+//
+// This is the base class for all nodes that can have children. It is
+// overridden for nodes that are dynamically populated such as queries and
+// folders. It is used directly for simple containers such as host groups
+// in history views.
+
+// derived classes each provide their own implementation of has children and
+// forward the rest to us using this macro
+#define NS_FORWARD_CONTAINERNODE_EXCEPT_HASCHILDREN \
+ NS_IMETHOD GetState(uint16_t* _state) override \
+ { return nsNavHistoryContainerResultNode::GetState(_state); } \
+ NS_IMETHOD GetContainerOpen(bool *aContainerOpen) override \
+ { return nsNavHistoryContainerResultNode::GetContainerOpen(aContainerOpen); } \
+ NS_IMETHOD SetContainerOpen(bool aContainerOpen) override \
+ { return nsNavHistoryContainerResultNode::SetContainerOpen(aContainerOpen); } \
+ NS_IMETHOD GetChildCount(uint32_t *aChildCount) override \
+ { return nsNavHistoryContainerResultNode::GetChildCount(aChildCount); } \
+ NS_IMETHOD GetChild(uint32_t index, nsINavHistoryResultNode **_retval) override \
+ { return nsNavHistoryContainerResultNode::GetChild(index, _retval); } \
+ NS_IMETHOD GetChildIndex(nsINavHistoryResultNode* aNode, uint32_t* _retval) override \
+ { return nsNavHistoryContainerResultNode::GetChildIndex(aNode, _retval); } \
+ NS_IMETHOD FindNodeByDetails(const nsACString& aURIString, PRTime aTime, \
+ int64_t aItemId, bool aRecursive, \
+ nsINavHistoryResultNode** _retval) override \
+ { return nsNavHistoryContainerResultNode::FindNodeByDetails(aURIString, aTime, aItemId, \
+ aRecursive, _retval); }
+
+#define NS_NAVHISTORYCONTAINERRESULTNODE_IID \
+ { 0x6e3bf8d3, 0x22aa, 0x4065, { 0x86, 0xbc, 0x37, 0x46, 0xb5, 0xb3, 0x2c, 0xe8 } }
+
+class nsNavHistoryContainerResultNode : public nsNavHistoryResultNode,
+ public nsINavHistoryContainerResultNode
+{
+public:
+ nsNavHistoryContainerResultNode(
+ const nsACString& aURI, const nsACString& aTitle,
+ const nsACString& aIconURI, uint32_t aContainerType,
+ nsNavHistoryQueryOptions* aOptions);
+ nsNavHistoryContainerResultNode(
+ const nsACString& aURI, const nsACString& aTitle,
+ PRTime aTime,
+ const nsACString& aIconURI, uint32_t aContainerType,
+ nsNavHistoryQueryOptions* aOptions);
+
+ virtual nsresult Refresh();
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_NAVHISTORYCONTAINERRESULTNODE_IID)
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsNavHistoryContainerResultNode, nsNavHistoryResultNode)
+ NS_FORWARD_COMMON_RESULTNODE_TO_BASE
+ NS_IMETHOD GetType(uint32_t* type) override
+ { *type = mContainerType; return NS_OK; }
+ NS_IMETHOD GetUri(nsACString& aURI) override
+ { aURI = mURI; return NS_OK; }
+ NS_DECL_NSINAVHISTORYCONTAINERRESULTNODE
+
+public:
+
+ virtual void OnRemoving() override;
+
+ bool AreChildrenVisible();
+
+ // Overridded by descendents to populate.
+ virtual nsresult OpenContainer();
+ nsresult CloseContainer(bool aSuppressNotifications = false);
+
+ virtual nsresult OpenContainerAsync();
+
+ // This points to the result that owns this container. All containers have
+ // their result pointer set so we can quickly get to the result without having
+ // to walk the tree. Yet, this also saves us from storing a million pointers
+ // for every leaf node to the result.
+ RefPtr<nsNavHistoryResult> mResult;
+
+ // For example, RESULT_TYPE_QUERY. Query and Folder results override GetType
+ // so this is not used, but is still kept in sync.
+ uint32_t mContainerType;
+
+ // When there are children, this stores the open state in the tree
+ // this is set to the default in the constructor.
+ bool mExpanded;
+
+ // Filled in by the result type generator in nsNavHistory.
+ nsCOMArray<nsNavHistoryResultNode> mChildren;
+
+ nsCOMPtr<nsNavHistoryQueryOptions> mOptions;
+
+ void FillStats();
+ nsresult ReverseUpdateStats(int32_t aAccessCountChange);
+
+ // Sorting methods.
+ typedef nsCOMArray<nsNavHistoryResultNode>::nsCOMArrayComparatorFunc SortComparator;
+ virtual uint16_t GetSortType();
+ virtual void GetSortingAnnotation(nsACString& aSortingAnnotation);
+
+ static SortComparator GetSortingComparator(uint16_t aSortType);
+ virtual void RecursiveSort(const char* aData,
+ SortComparator aComparator);
+ uint32_t FindInsertionPoint(nsNavHistoryResultNode* aNode, SortComparator aComparator,
+ const char* aData, bool* aItemExists);
+ bool DoesChildNeedResorting(uint32_t aIndex, SortComparator aComparator,
+ const char* aData);
+
+ static int32_t SortComparison_StringLess(const nsAString& a, const nsAString& b);
+
+ static int32_t SortComparison_Bookmark(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_TitleLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_TitleGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_DateLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_DateGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_URILess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_URIGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_VisitCountLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_VisitCountGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_KeywordLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_KeywordGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_AnnotationLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_AnnotationGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_DateAddedLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_DateAddedGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_LastModifiedLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_LastModifiedGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_TagsLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_TagsGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_FrecencyLess(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+ static int32_t SortComparison_FrecencyGreater(nsNavHistoryResultNode* a,
+ nsNavHistoryResultNode* b,
+ void* closure);
+
+ // finding children: THESE DO NOT ADDREF
+ nsNavHistoryResultNode* FindChildURI(const nsACString& aSpec,
+ uint32_t* aNodeIndex);
+ // returns the index of the given node, -1 if not found
+ int32_t FindChild(nsNavHistoryResultNode* aNode)
+ { return mChildren.IndexOf(aNode); }
+
+ nsresult InsertChildAt(nsNavHistoryResultNode* aNode, int32_t aIndex);
+ nsresult InsertSortedChild(nsNavHistoryResultNode* aNode,
+ bool aIgnoreDuplicates = false);
+ bool EnsureItemPosition(uint32_t aIndex);
+
+ nsresult RemoveChildAt(int32_t aIndex);
+
+ void RecursiveFindURIs(bool aOnlyOne,
+ nsNavHistoryContainerResultNode* aContainer,
+ const nsCString& aSpec,
+ nsCOMArray<nsNavHistoryResultNode>* aMatches);
+ bool UpdateURIs(bool aRecursive, bool aOnlyOne, bool aUpdateSort,
+ const nsCString& aSpec,
+ nsresult (*aCallback)(nsNavHistoryResultNode*, const void*,
+ const nsNavHistoryResult*),
+ const void* aClosure);
+ nsresult ChangeTitles(nsIURI* aURI, const nsACString& aNewTitle,
+ bool aRecursive, bool aOnlyOne);
+
+protected:
+ virtual ~nsNavHistoryContainerResultNode();
+
+ enum AsyncCanceledState {
+ NOT_CANCELED, CANCELED, CANCELED_RESTART_NEEDED
+ };
+
+ void CancelAsyncOpen(bool aRestart);
+ nsresult NotifyOnStateChange(uint16_t aOldState);
+
+ nsCOMPtr<mozIStoragePendingStatement> mAsyncPendingStmt;
+ AsyncCanceledState mAsyncCanceledState;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsNavHistoryContainerResultNode,
+ NS_NAVHISTORYCONTAINERRESULTNODE_IID)
+
+// nsNavHistoryQueryResultNode
+//
+// Overridden container type for complex queries over history and/or
+// bookmarks. This keeps itself in sync by listening to history and
+// bookmark notifications.
+
+class nsNavHistoryQueryResultNode final : public nsNavHistoryContainerResultNode,
+ public nsINavHistoryQueryResultNode,
+ public nsINavBookmarkObserver
+{
+public:
+ nsNavHistoryQueryResultNode(const nsACString& aTitle,
+ const nsACString& aIconURI,
+ const nsACString& aQueryURI);
+ nsNavHistoryQueryResultNode(const nsACString& aTitle,
+ const nsACString& aIconURI,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions);
+ nsNavHistoryQueryResultNode(const nsACString& aTitle,
+ const nsACString& aIconURI,
+ PRTime aTime,
+ const nsCOMArray<nsNavHistoryQuery>& aQueries,
+ nsNavHistoryQueryOptions* aOptions);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_FORWARD_COMMON_RESULTNODE_TO_BASE
+ NS_IMETHOD GetType(uint32_t* type) override
+ { *type = nsNavHistoryResultNode::RESULT_TYPE_QUERY; return NS_OK; }
+ NS_IMETHOD GetUri(nsACString& aURI) override; // does special lazy creation
+ NS_FORWARD_CONTAINERNODE_EXCEPT_HASCHILDREN
+ NS_IMETHOD GetHasChildren(bool* aHasChildren) override;
+ NS_DECL_NSINAVHISTORYQUERYRESULTNODE
+
+ bool CanExpand();
+ bool IsContainersQuery();
+
+ virtual nsresult OpenContainer() override;
+
+ NS_DECL_BOOKMARK_HISTORY_OBSERVER_INTERNAL
+ virtual void OnRemoving() override;
+
+public:
+ // this constructs lazily mURI from mQueries and mOptions, call
+ // VerifyQueriesSerialized either this or mQueries/mOptions should be valid
+ nsresult VerifyQueriesSerialized();
+
+ // these may be constructed lazily from mURI, call VerifyQueriesParsed
+ // either this or mURI should be valid
+ nsCOMArray<nsNavHistoryQuery> mQueries;
+ uint32_t mLiveUpdate; // one of QUERYUPDATE_* in nsNavHistory.h
+ bool mHasSearchTerms;
+ nsresult VerifyQueriesParsed();
+
+ // safe options getter, ensures queries are parsed
+ nsNavHistoryQueryOptions* Options();
+
+ // this indicates whether the query contents are valid, they don't go away
+ // after the container is closed until a notification comes in
+ bool mContentsValid;
+
+ nsresult FillChildren();
+ void ClearChildren(bool unregister);
+ nsresult Refresh() override;
+
+ virtual uint16_t GetSortType() override;
+ virtual void GetSortingAnnotation(nsACString& aSortingAnnotation) override;
+ virtual void RecursiveSort(const char* aData,
+ SortComparator aComparator) override;
+
+ nsresult NotifyIfTagsChanged(nsIURI* aURI);
+
+ uint32_t mBatchChanges;
+
+ // Tracks transition type filters shared by all mQueries.
+ nsTArray<uint32_t> mTransitions;
+
+protected:
+ virtual ~nsNavHistoryQueryResultNode();
+};
+
+
+// nsNavHistoryFolderResultNode
+//
+// Overridden container type for bookmark folders. It will keep the contents
+// of the folder in sync with the bookmark service.
+
+class nsNavHistoryFolderResultNode final : public nsNavHistoryContainerResultNode,
+ public nsINavHistoryQueryResultNode,
+ public nsINavBookmarkObserver,
+ public mozilla::places::WeakAsyncStatementCallback
+{
+public:
+ nsNavHistoryFolderResultNode(const nsACString& aTitle,
+ nsNavHistoryQueryOptions* options,
+ int64_t aFolderId);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_FORWARD_COMMON_RESULTNODE_TO_BASE
+ NS_IMETHOD GetType(uint32_t* type) override {
+ if (mTargetFolderItemId != mItemId) {
+ *type = nsNavHistoryResultNode::RESULT_TYPE_FOLDER_SHORTCUT;
+ } else {
+ *type = nsNavHistoryResultNode::RESULT_TYPE_FOLDER;
+ }
+ return NS_OK;
+ }
+ NS_IMETHOD GetUri(nsACString& aURI) override;
+ NS_FORWARD_CONTAINERNODE_EXCEPT_HASCHILDREN
+ NS_IMETHOD GetHasChildren(bool* aHasChildren) override;
+ NS_DECL_NSINAVHISTORYQUERYRESULTNODE
+
+ virtual nsresult OpenContainer() override;
+
+ virtual nsresult OpenContainerAsync() override;
+ NS_DECL_ASYNCSTATEMENTCALLBACK
+
+ // This object implements a bookmark observer interface. This is called from the
+ // result's actual observer and it knows all observers are FolderResultNodes
+ NS_DECL_NSINAVBOOKMARKOBSERVER
+
+ virtual void OnRemoving() override;
+
+ // this indicates whether the folder contents are valid, they don't go away
+ // after the container is closed until a notification comes in
+ bool mContentsValid;
+
+ // If the node is generated from a place:folder=X query, this is the target
+ // folder id and GUID. For regular folder nodes, they are set to the same
+ // values as mItemId and mBookmarkGuid. For more complex queries, they are set
+ // to -1/an empty string.
+ int64_t mTargetFolderItemId;
+ nsCString mTargetFolderGuid;
+
+ nsresult FillChildren();
+ void ClearChildren(bool aUnregister);
+ nsresult Refresh() override;
+
+ bool StartIncrementalUpdate();
+ void ReindexRange(int32_t aStartIndex, int32_t aEndIndex, int32_t aDelta);
+
+ nsNavHistoryResultNode* FindChildById(int64_t aItemId,
+ uint32_t* aNodeIndex);
+
+protected:
+ virtual ~nsNavHistoryFolderResultNode();
+
+private:
+
+ nsresult OnChildrenFilled();
+ void EnsureRegisteredAsFolderObserver();
+ nsresult FillChildrenAsync();
+
+ bool mIsRegisteredFolderObserver;
+ int32_t mAsyncBookmarkIndex;
+};
+
+// nsNavHistorySeparatorResultNode
+//
+// Separator result nodes do not hold any data.
+class nsNavHistorySeparatorResultNode : public nsNavHistoryResultNode
+{
+public:
+ nsNavHistorySeparatorResultNode();
+
+ NS_IMETHOD GetType(uint32_t* type)
+ { *type = nsNavHistoryResultNode::RESULT_TYPE_SEPARATOR; return NS_OK; }
+};
+
+#endif // nsNavHistoryResult_h_
diff --git a/toolkit/components/places/nsPIPlacesDatabase.idl b/toolkit/components/places/nsPIPlacesDatabase.idl
new file mode 100644
index 0000000000..5511b1be6c
--- /dev/null
+++ b/toolkit/components/places/nsPIPlacesDatabase.idl
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=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 mozIStorageConnection;
+interface nsINavHistoryQuery;
+interface nsINavHistoryQueryOptions;
+interface mozIStorageStatementCallback;
+interface mozIStoragePendingStatement;
+interface nsIAsyncShutdownClient;
+
+/**
+ * This is a private interface used by Places components to get access to the
+ * database. If outside consumers wish to use this, they should only read from
+ * the database so they do not break any internal invariants.
+ */
+[scriptable, uuid(366ee63e-a413-477d-9ad6-8d6863e89401)]
+interface nsPIPlacesDatabase : nsISupports
+{
+ /**
+ * The database connection used by Places.
+ */
+ readonly attribute mozIStorageConnection DBConnection;
+
+ /**
+ * Asynchronously executes the statement created from queries.
+ *
+ * @see nsINavHistoryService::executeQueries
+ * @note THIS IS A TEMPORARY API. Don't rely on it, since it will be replaced
+ * in future versions by a real async querying API.
+ * @note Results obtained from this method differ from results obtained from
+ * executeQueries, because there is additional filtering and sorting
+ * done by the latter. Thus you should use executeQueries, unless you
+ * are absolutely sure that the returned results are fine for
+ * your use-case.
+ */
+ mozIStoragePendingStatement asyncExecuteLegacyQueries(
+ [array, size_is(aQueryCount)] in nsINavHistoryQuery aQueries,
+ in unsigned long aQueryCount,
+ in nsINavHistoryQueryOptions aOptions,
+ in mozIStorageStatementCallback aCallback);
+
+ /**
+ * Hook for clients who need to perform actions during/by the end of
+ * the shutdown of the database.
+ */
+ readonly attribute nsIAsyncShutdownClient shutdownClient;
+};
diff --git a/toolkit/components/places/nsPlacesExpiration.js b/toolkit/components/places/nsPlacesExpiration.js
new file mode 100644
index 0000000000..499934362d
--- /dev/null
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -0,0 +1,1105 @@
+/* -*- 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/. */
+
+/**
+ * This component handles history and orphans expiration through asynchronous
+ * Storage statements.
+ * Expiration runs:
+ * - At idle, but just once, we stop any other kind of expiration during idle
+ * to preserve batteries in portable devices.
+ * - At shutdown, only if the database is dirty, we should still avoid to
+ * expire too heavily on shutdown.
+ * - On ClearHistory we run a full expiration for privacy reasons.
+ * - On a repeating timer we expire in small chunks.
+ *
+ * Expiration algorithm will adapt itself based on:
+ * - Memory size of the device.
+ * - Status of the database (clean or dirty).
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+// Constants
+
+// Last expiration step should run before the final sync.
+const TOPIC_SHUTDOWN = "places-will-close-connection";
+const TOPIC_PREF_CHANGED = "nsPref:changed";
+const TOPIC_DEBUG_START_EXPIRATION = "places-debug-start-expiration";
+const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
+const TOPIC_IDLE_BEGIN = "idle";
+const TOPIC_IDLE_END = "active";
+const TOPIC_IDLE_DAILY = "idle-daily";
+const TOPIC_TESTING_MODE = "testing-mode";
+const TOPIC_TEST_INTERVAL_CHANGED = "test-interval-changed";
+
+// Branch for all expiration preferences.
+const PREF_BRANCH = "places.history.expiration.";
+
+// Max number of unique URIs to retain in history.
+// Notice this is a lazy limit. This means we will start to expire if we will
+// go over it, but we won't ensure that we will stop exactly when we reach it,
+// instead we will stop after the next expiration step that will bring us
+// below it.
+// If this preference does not exist or has a negative value, we will calculate
+// a limit based on current hardware.
+const PREF_MAX_URIS = "max_pages";
+const PREF_MAX_URIS_NOTSET = -1; // Use our internally calculated limit.
+
+// We save the current unique URIs limit to this pref, to make it available to
+// other components without having to duplicate the full logic.
+const PREF_READONLY_CALCULATED_MAX_URIS = "transient_current_max_pages";
+
+// Seconds between each expiration step.
+const PREF_INTERVAL_SECONDS = "interval_seconds";
+const PREF_INTERVAL_SECONDS_NOTSET = 3 * 60;
+
+// We calculate an optimal database size, based on hardware specs.
+// This percentage of memory size is used to protect against calculating a too
+// large database size on systems with small memory.
+const DATABASE_TO_MEMORY_PERC = 4;
+// This percentage of disk size is used to protect against calculating a too
+// large database size on disks with tiny quota or available space.
+const DATABASE_TO_DISK_PERC = 2;
+// Maximum size of the optimal database. High-end hardware has plenty of
+// memory and disk space, but performances don't grow linearly.
+const DATABASE_MAX_SIZE = 73400320; // 70MiB
+// If the physical memory size is bogus, fallback to this.
+const MEMSIZE_FALLBACK_BYTES = 268435456; // 256 MiB
+// If the disk available space is bogus, fallback to this.
+const DISKSIZE_FALLBACK_BYTES = 268435456; // 256 MiB
+
+// Max number of entries to expire at each expiration step.
+// This value is globally used for different kind of data we expire, can be
+// tweaked based on data type. See below in getBoundStatement.
+const EXPIRE_LIMIT_PER_STEP = 6;
+// When we run a large expiration step, the above limit is multiplied by this.
+const EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER = 10;
+
+// When history is clean or dirty enough we will adapt the expiration algorithm
+// to be more lazy or more aggressive.
+// This is done acting on the interval between expiration steps and the number
+// of expirable items.
+// 1. Clean history:
+// We expire at (default interval * EXPIRE_AGGRESSIVITY_MULTIPLIER) the
+// default number of entries.
+// 2. Dirty history:
+// We expire at the default interval, but a greater number of entries
+// (default number of entries * EXPIRE_AGGRESSIVITY_MULTIPLIER).
+const EXPIRE_AGGRESSIVITY_MULTIPLIER = 3;
+
+// This is the average size in bytes of an URI entry in the database.
+// Magic numbers are determined through analysis of the distribution of a ratio
+// between number of unique URIs and database size among our users.
+// Used as a fall back value when it's not possible to calculate the real value.
+const URIENTRY_AVG_SIZE = 600;
+
+// Seconds of idle time before starting a larger expiration step.
+// Notice during idle we stop the expiration timer since we don't want to hurt
+// stand-by or mobile devices batteries.
+const IDLE_TIMEOUT_SECONDS = 5 * 60;
+
+// If a clear history ran just before we shutdown, we will skip most of the
+// expiration at shutdown. This is maximum number of seconds from last
+// clearHistory to decide to skip expiration at shutdown.
+const SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS = 10;
+
+// If the pages delta from the last ANALYZE is over this threashold, the tables
+// should be analyzed again.
+const ANALYZE_PAGES_THRESHOLD = 100;
+
+// If the number of pages over history limit is greater than this threshold,
+// expiration will be more aggressive, to bring back history to a saner size.
+const OVERLIMIT_PAGES_THRESHOLD = 1000;
+
+const MSECS_PER_DAY = 86400000;
+const ANNOS_EXPIRE_POLICIES = [
+ { bind: "expire_days",
+ type: Ci.nsIAnnotationService.EXPIRE_DAYS,
+ time: 7 * 1000 * MSECS_PER_DAY },
+ { bind: "expire_weeks",
+ type: Ci.nsIAnnotationService.EXPIRE_WEEKS,
+ time: 30 * 1000 * MSECS_PER_DAY },
+ { bind: "expire_months",
+ type: Ci.nsIAnnotationService.EXPIRE_MONTHS,
+ time: 180 * 1000 * MSECS_PER_DAY },
+];
+
+// When we expire we can use these limits:
+// - SMALL for usual partial expirations, will expire a small chunk.
+// - LARGE for idle or shutdown expirations, will expire a large chunk.
+// - UNLIMITED for clearHistory, will expire everything.
+// - DEBUG will use a known limit, passed along with the debug notification.
+const LIMIT = {
+ SMALL: 0,
+ LARGE: 1,
+ UNLIMITED: 2,
+ DEBUG: 3,
+};
+
+// Represents the status of history database.
+const STATUS = {
+ CLEAN: 0,
+ DIRTY: 1,
+ UNKNOWN: 2,
+};
+
+// Represents actions on which a query will run.
+const ACTION = {
+ TIMED: 1 << 0, // happens every this._interval
+ TIMED_OVERLIMIT: 1 << 1, // like TIMED but only when history is over limits
+ TIMED_ANALYZE: 1 << 2, // happens when ANALYZE statistics should be updated
+ CLEAR_HISTORY: 1 << 3, // happens when history is cleared
+ SHUTDOWN_DIRTY: 1 << 4, // happens at shutdown for DIRTY state
+ IDLE_DIRTY: 1 << 5, // happens on idle for DIRTY state
+ IDLE_DAILY: 1 << 6, // happens once a day on idle
+ DEBUG: 1 << 7, // happens on TOPIC_DEBUG_START_EXPIRATION
+};
+
+// The queries we use to expire.
+const EXPIRATION_QUERIES = {
+
+ // Some visits can be expired more often than others, cause they are less
+ // useful to the user and can pollute awesomebar results:
+ // 1. urls over 255 chars
+ // 2. redirect sources and downloads
+ // Note: due to the REPLACE option, this should be executed before
+ // QUERY_FIND_VISITS_TO_EXPIRE, that has a more complete result.
+ QUERY_FIND_EXOTIC_VISITS_TO_EXPIRE: {
+ sql: `INSERT INTO expiration_notify (v_id, url, guid, visit_date, reason)
+ SELECT v.id, h.url, h.guid, v.visit_date, "exotic"
+ FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = v.place_id
+ WHERE visit_date < strftime('%s','now','localtime','start of day','-60 days','utc') * 1000000
+ AND ( LENGTH(h.url) > 255 OR v.visit_type = 7 )
+ ORDER BY v.visit_date ASC
+ LIMIT :limit_visits`,
+ actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Finds visits to be expired when history is over the unique pages limit,
+ // otherwise will return nothing.
+ // This explicitly excludes any visits added in the last 7 days, to protect
+ // users with thousands of bookmarks from constantly losing history.
+ QUERY_FIND_VISITS_TO_EXPIRE: {
+ sql: `INSERT INTO expiration_notify
+ (v_id, url, guid, visit_date, expected_results)
+ SELECT v.id, h.url, h.guid, v.visit_date, :limit_visits
+ FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = v.place_id
+ WHERE (SELECT COUNT(*) FROM moz_places) > :max_uris
+ AND visit_date < strftime('%s','now','localtime','start of day','-7 days','utc') * 1000000
+ ORDER BY v.visit_date ASC
+ LIMIT :limit_visits`,
+ actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Removes the previously found visits.
+ QUERY_EXPIRE_VISITS: {
+ sql: `DELETE FROM moz_historyvisits WHERE id IN (
+ SELECT v_id FROM expiration_notify WHERE v_id NOTNULL
+ )`,
+ actions: ACTION.TIMED_OVERLIMIT | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Finds orphan URIs in the database.
+ // Notice we won't notify single removed URIs on History.clear(), so we don't
+ // run this query in such a case, but just delete URIs.
+ // This could run in the middle of adding a visit or bookmark to a new page.
+ // In such a case since it is async, could end up expiring the orphan page
+ // before it actually gets the new visit or bookmark.
+ // Thus, since new pages get frecency -1, we filter on that.
+ QUERY_FIND_URIS_TO_EXPIRE: {
+ sql: `INSERT INTO expiration_notify (p_id, url, guid, visit_date)
+ SELECT h.id, h.url, h.guid, h.last_visit_date
+ FROM moz_places h
+ LEFT JOIN moz_historyvisits v ON h.id = v.place_id
+ WHERE h.last_visit_date IS NULL
+ AND h.foreign_count = 0
+ AND v.id IS NULL
+ AND frecency <> -1
+ LIMIT :limit_uris`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+ ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+
+ // Expire found URIs from the database.
+ QUERY_EXPIRE_URIS: {
+ sql: `DELETE FROM moz_places WHERE id IN (
+ SELECT p_id FROM expiration_notify WHERE p_id NOTNULL
+ ) AND foreign_count = 0 AND last_visit_date ISNULL`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+ ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+
+ // Expire orphan URIs from the database.
+ QUERY_SILENT_EXPIRE_ORPHAN_URIS: {
+ sql: `DELETE FROM moz_places WHERE id IN (
+ SELECT h.id
+ FROM moz_places h
+ LEFT JOIN moz_historyvisits v ON h.id = v.place_id
+ WHERE h.last_visit_date IS NULL
+ AND h.foreign_count = 0
+ AND v.id IS NULL
+ LIMIT :limit_uris
+ )`,
+ actions: ACTION.CLEAR_HISTORY
+ },
+
+ // Hosts accumulated during the places delete are updated through a trigger
+ // (see nsPlacesTriggers.h).
+ QUERY_UPDATE_HOSTS: {
+ sql: `DELETE FROM moz_updatehosts_temp`,
+ actions: ACTION.CLEAR_HISTORY | ACTION.TIMED | ACTION.TIMED_OVERLIMIT |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire orphan icons from the database.
+ QUERY_EXPIRE_FAVICONS: {
+ sql: `DELETE FROM moz_favicons WHERE id IN (
+ SELECT f.id FROM moz_favicons f
+ LEFT JOIN moz_places h ON f.id = h.favicon_id
+ WHERE h.favicon_id IS NULL
+ LIMIT :limit_favicons
+ )`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire orphan page annotations from the database.
+ QUERY_EXPIRE_ANNOS: {
+ sql: `DELETE FROM moz_annos WHERE id in (
+ SELECT a.id FROM moz_annos a
+ LEFT JOIN moz_places h ON a.place_id = h.id
+ WHERE h.id IS NULL
+ LIMIT :limit_annos
+ )`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire page annotations based on expiration policy.
+ QUERY_EXPIRE_ANNOS_WITH_POLICY: {
+ sql: `DELETE FROM moz_annos
+ WHERE (expiration = :expire_days
+ AND :expire_days_time > MAX(lastModified, dateAdded))
+ OR (expiration = :expire_weeks
+ AND :expire_weeks_time > MAX(lastModified, dateAdded))
+ OR (expiration = :expire_months
+ AND :expire_months_time > MAX(lastModified, dateAdded))`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire items annotations based on expiration policy.
+ QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY: {
+ sql: `DELETE FROM moz_items_annos
+ WHERE (expiration = :expire_days
+ AND :expire_days_time > MAX(lastModified, dateAdded))
+ OR (expiration = :expire_weeks
+ AND :expire_weeks_time > MAX(lastModified, dateAdded))
+ OR (expiration = :expire_months
+ AND :expire_months_time > MAX(lastModified, dateAdded))`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire page annotations based on expiration policy.
+ QUERY_EXPIRE_ANNOS_WITH_HISTORY: {
+ sql: `DELETE FROM moz_annos
+ WHERE expiration = :expire_with_history
+ AND NOT EXISTS (SELECT id FROM moz_historyvisits
+ WHERE place_id = moz_annos.place_id LIMIT 1)`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire item annos without a corresponding item id.
+ QUERY_EXPIRE_ITEMS_ANNOS: {
+ sql: `DELETE FROM moz_items_annos WHERE id IN (
+ SELECT a.id FROM moz_items_annos a
+ LEFT JOIN moz_bookmarks b ON a.item_id = b.id
+ WHERE b.id IS NULL
+ LIMIT :limit_annos
+ )`,
+ actions: ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+
+ // Expire all annotation names without a corresponding annotation.
+ QUERY_EXPIRE_ANNO_ATTRIBUTES: {
+ sql: `DELETE FROM moz_anno_attributes WHERE id IN (
+ SELECT n.id FROM moz_anno_attributes n
+ LEFT JOIN moz_annos a ON n.id = a.anno_attribute_id
+ LEFT JOIN moz_items_annos t ON n.id = t.anno_attribute_id
+ WHERE a.anno_attribute_id IS NULL
+ AND t.anno_attribute_id IS NULL
+ LIMIT :limit_annos
+ )`,
+ actions: ACTION.CLEAR_HISTORY | ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY |
+ ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+
+ // Expire orphan inputhistory.
+ QUERY_EXPIRE_INPUTHISTORY: {
+ sql: `DELETE FROM moz_inputhistory WHERE place_id IN (
+ SELECT i.place_id FROM moz_inputhistory i
+ LEFT JOIN moz_places h ON h.id = i.place_id
+ WHERE h.id IS NULL
+ LIMIT :limit_inputhistory
+ )`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.CLEAR_HISTORY |
+ ACTION.SHUTDOWN_DIRTY | ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY |
+ ACTION.DEBUG
+ },
+
+ // Expire all session annotations. Should only be called at shutdown.
+ QUERY_EXPIRE_ANNOS_SESSION: {
+ sql: "DELETE FROM moz_annos WHERE expiration = :expire_session",
+ actions: ACTION.CLEAR_HISTORY | ACTION.DEBUG
+ },
+
+ // Expire all session item annotations. Should only be called at shutdown.
+ QUERY_EXPIRE_ITEMS_ANNOS_SESSION: {
+ sql: "DELETE FROM moz_items_annos WHERE expiration = :expire_session",
+ actions: ACTION.CLEAR_HISTORY | ACTION.DEBUG
+ },
+
+ // Select entries for notifications.
+ // If p_id is set whole_entry = 1, then we have expired the full page.
+ // Either p_id or v_id are always set.
+ QUERY_SELECT_NOTIFICATIONS: {
+ sql: `SELECT url, guid, MAX(visit_date) AS visit_date,
+ MAX(IFNULL(MIN(p_id, 1), MIN(v_id, 0))) AS whole_entry,
+ MAX(expected_results) AS expected_results,
+ (SELECT MAX(visit_date) FROM expiration_notify
+ WHERE reason = "expired" AND url = n.url AND p_id ISNULL
+ ) AS most_recent_expired_visit
+ FROM expiration_notify n
+ GROUP BY url`,
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+ ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+
+ // Empty the notifications table.
+ QUERY_DELETE_NOTIFICATIONS: {
+ sql: "DELETE FROM expiration_notify",
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.SHUTDOWN_DIRTY |
+ ACTION.IDLE_DIRTY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+
+ // The following queries are used to adjust the sqlite_stat1 table to help the
+ // query planner create better queries. These should always be run LAST, and
+ // are therefore at the end of the object.
+ // Since also nsNavHistory.cpp executes ANALYZE, the analyzed tables
+ // must be the same in both components. So ensure they are in sync.
+
+ QUERY_ANALYZE_MOZ_PLACES: {
+ sql: "ANALYZE moz_places",
+ actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
+ ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+ QUERY_ANALYZE_MOZ_BOOKMARKS: {
+ sql: "ANALYZE moz_bookmarks",
+ actions: ACTION.TIMED_ANALYZE | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+ QUERY_ANALYZE_MOZ_HISTORYVISITS: {
+ sql: "ANALYZE moz_historyvisits",
+ actions: ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
+ ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+ QUERY_ANALYZE_MOZ_INPUTHISTORY: {
+ sql: "ANALYZE moz_inputhistory",
+ actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
+ ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
+ },
+};
+
+/**
+ * Sends a bookmarks notification through the given observers.
+ *
+ * @param observers
+ * array of nsINavBookmarkObserver objects.
+ * @param notification
+ * the notification name.
+ * @param args
+ * array of arguments to pass to the notification.
+ */
+function notify(observers, notification, args = []) {
+ for (let observer of observers) {
+ try {
+ observer[notification](...args);
+ } catch (ex) {}
+ }
+}
+
+// nsPlacesExpiration definition
+
+function nsPlacesExpiration()
+{
+ // Smart Getters
+
+ XPCOMUtils.defineLazyGetter(this, "_db", function () {
+ let db = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsPIPlacesDatabase).
+ DBConnection;
+
+ // Create the temporary notifications table.
+ let stmt = db.createAsyncStatement(
+ `CREATE TEMP TABLE expiration_notify (
+ id INTEGER PRIMARY KEY
+ , v_id INTEGER
+ , p_id INTEGER
+ , url TEXT NOT NULL
+ , guid TEXT NOT NULL
+ , visit_date INTEGER
+ , expected_results INTEGER NOT NULL DEFAULT 0
+ , reason TEXT NOT NULL DEFAULT "expired"
+ )`);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ return db;
+ });
+
+ XPCOMUtils.defineLazyServiceGetter(this, "_idle",
+ "@mozilla.org/widget/idleservice;1",
+ "nsIIdleService");
+
+ this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ getBranch(PREF_BRANCH);
+
+ this._loadPrefs().then(() => {
+ // Observe our preferences branch for changes.
+ this._prefBranch.addObserver("", this, true);
+
+ // Create our expiration timer.
+ this._newTimer();
+ }, Cu.reportError);
+
+ // Register topic observers.
+ Services.obs.addObserver(this, TOPIC_SHUTDOWN, true);
+ Services.obs.addObserver(this, TOPIC_DEBUG_START_EXPIRATION, true);
+ Services.obs.addObserver(this, TOPIC_IDLE_DAILY, true);
+}
+
+nsPlacesExpiration.prototype = {
+
+ // nsIObserver
+
+ observe: function PEX_observe(aSubject, aTopic, aData)
+ {
+ if (this._shuttingDown) {
+ return;
+ }
+
+ if (aTopic == TOPIC_SHUTDOWN) {
+ this._shuttingDown = true;
+ this.expireOnIdle = false;
+
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+
+ // If we didn't ran a clearHistory recently and database is dirty, we
+ // want to expire some entries, to speed up the expiration process.
+ let hasRecentClearHistory =
+ Date.now() - this._lastClearHistoryTime <
+ SHUTDOWN_WITH_RECENT_CLEARHISTORY_TIMEOUT_SECONDS * 1000;
+ if (!hasRecentClearHistory && this.status == STATUS.DIRTY) {
+ this._expireWithActionAndLimit(ACTION.SHUTDOWN_DIRTY, LIMIT.LARGE);
+ }
+
+ this._finalizeInternalStatements();
+ }
+ else if (aTopic == TOPIC_PREF_CHANGED) {
+ this._loadPrefs().then(() => {
+ if (aData == PREF_INTERVAL_SECONDS) {
+ // Renew the timer with the new interval value.
+ this._newTimer();
+ }
+ }, Cu.reportError);
+ }
+ else if (aTopic == TOPIC_DEBUG_START_EXPIRATION) {
+ // The passed-in limit is the maximum number of visits to expire when
+ // history is over capacity. Mind to correctly handle the NaN value.
+ let limit = parseInt(aData);
+ if (limit == -1) {
+ // Everything should be expired without any limit. If history is over
+ // capacity then all existing visits will be expired.
+ // Should only be used in tests, since may cause dataloss.
+ this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.UNLIMITED);
+ }
+ else if (limit > 0) {
+ // The number of expired visits is limited by this amount. It may be
+ // used for testing purposes, like checking that limited queries work.
+ this._debugLimit = limit;
+ this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG);
+ }
+ else {
+ // Any other value is intended as a 0 limit, that means no visits
+ // will be expired. Even if this doesn't touch visits, it will remove
+ // any orphan pages, icons, annotations and similar from the database,
+ // so it may be used for cleanup purposes.
+ this._debugLimit = -1;
+ this._expireWithActionAndLimit(ACTION.DEBUG, LIMIT.DEBUG);
+ }
+ }
+ else if (aTopic == TOPIC_IDLE_BEGIN) {
+ // Stop the expiration timer. We don't want to keep up expiring on idle
+ // to preserve batteries on mobile devices and avoid killing stand-by.
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ if (this.expireOnIdle)
+ this._expireWithActionAndLimit(ACTION.IDLE_DIRTY, LIMIT.LARGE);
+ }
+ else if (aTopic == TOPIC_IDLE_END) {
+ // Restart the expiration timer.
+ if (!this._timer)
+ this._newTimer();
+ }
+ else if (aTopic == TOPIC_IDLE_DAILY) {
+ this._expireWithActionAndLimit(ACTION.IDLE_DAILY, LIMIT.LARGE);
+ }
+ else if (aTopic == TOPIC_TESTING_MODE) {
+ this._testingMode = true;
+ }
+ },
+
+ // nsINavHistoryObserver
+
+ _inBatchMode: false,
+ onBeginUpdateBatch: function PEX_onBeginUpdateBatch()
+ {
+ this._inBatchMode = true;
+
+ // We do not want to expire while we are doing batch work.
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ },
+
+ onEndUpdateBatch: function PEX_onEndUpdateBatch()
+ {
+ this._inBatchMode = false;
+
+ // Restore timer.
+ if (!this._timer)
+ this._newTimer();
+ },
+
+ _lastClearHistoryTime: 0,
+ onClearHistory: function PEX_onClearHistory() {
+ this._lastClearHistoryTime = Date.now();
+ // Expire orphans. History status is clean after a clear history.
+ this.status = STATUS.CLEAN;
+ this._expireWithActionAndLimit(ACTION.CLEAR_HISTORY, LIMIT.UNLIMITED);
+ },
+
+ onVisit: function() {},
+ onTitleChanged: function() {},
+ onDeleteURI: function() {},
+ onPageChanged: function() {},
+ onDeleteVisits: function() {},
+
+ // nsITimerCallback
+
+ notify: function PEX_timerCallback()
+ {
+ // Check if we are over history capacity, if so visits must be expired.
+ this._getPagesStats((function onPagesCount(aPagesCount, aStatsCount) {
+ let overLimitPages = aPagesCount - this._urisLimit;
+ this._overLimit = overLimitPages > 0;
+
+ let action = this._overLimit ? ACTION.TIMED_OVERLIMIT : ACTION.TIMED;
+ // If the number of pages changed significantly from the last ANALYZE
+ // update SQLite statistics.
+ if (Math.abs(aPagesCount - aStatsCount) >= ANALYZE_PAGES_THRESHOLD) {
+ action = action | ACTION.TIMED_ANALYZE;
+ }
+
+ // Adapt expiration aggressivity to the number of pages over the limit.
+ let limit = overLimitPages > OVERLIMIT_PAGES_THRESHOLD ? LIMIT.LARGE
+ : LIMIT.SMALL;
+
+ this._expireWithActionAndLimit(action, limit);
+ }).bind(this));
+ },
+
+ // mozIStorageStatementCallback
+
+ handleResult: function PEX_handleResult(aResultSet)
+ {
+ // We don't want to notify after shutdown.
+ if (this._shuttingDown)
+ return;
+
+ let row;
+ while ((row = aResultSet.getNextRow())) {
+ // expected_results is set to the number of expected visits by
+ // QUERY_FIND_VISITS_TO_EXPIRE. We decrease that counter for each found
+ // visit and if it reaches zero we mark the database as dirty, since all
+ // the expected visits were expired, so it's likely the next run will
+ // find more.
+ let expectedResults = row.getResultByName("expected_results");
+ if (expectedResults > 0) {
+ if (!("_expectedResultsCount" in this)) {
+ this._expectedResultsCount = expectedResults;
+ }
+ if (this._expectedResultsCount > 0) {
+ this._expectedResultsCount--;
+ }
+ }
+
+ let uri = Services.io.newURI(row.getResultByName("url"), null, null);
+ let guid = row.getResultByName("guid");
+ let visitDate = row.getResultByName("visit_date");
+ let wholeEntry = row.getResultByName("whole_entry");
+ let mostRecentExpiredVisit = row.getResultByName("most_recent_expired_visit");
+ let reason = Ci.nsINavHistoryObserver.REASON_EXPIRED;
+ let observers = PlacesUtils.history.getObservers();
+
+ if (mostRecentExpiredVisit) {
+ let days = parseInt((Date.now() - (mostRecentExpiredVisit / 1000)) / MSECS_PER_DAY);
+ if (!this._mostRecentExpiredVisitDays) {
+ this._mostRecentExpiredVisitDays = days;
+ }
+ else if (days < this._mostRecentExpiredVisitDays) {
+ this._mostRecentExpiredVisitDays = days;
+ }
+ }
+
+ // Dispatch expiration notifications to history.
+ if (wholeEntry) {
+ notify(observers, "onDeleteURI", [uri, guid, reason]);
+ } else {
+ notify(observers, "onDeleteVisits", [uri, visitDate, guid, reason, 0]);
+ }
+ }
+ },
+
+ handleError: function PEX_handleError(aError)
+ {
+ Cu.reportError("Async statement execution returned with '" +
+ aError.result + "', '" + aError.message + "'");
+ },
+
+ // Number of expiration steps needed to reach a CLEAN status.
+ _telemetrySteps: 1,
+ handleCompletion: function PEX_handleCompletion(aReason)
+ {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+
+ if (this._mostRecentExpiredVisitDays) {
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_MOST_RECENT_EXPIRED_VISIT_DAYS")
+ .add(this._mostRecentExpiredVisitDays);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ } finally {
+ delete this._mostRecentExpiredVisitDays;
+ }
+ }
+
+ if ("_expectedResultsCount" in this) {
+ // Adapt the aggressivity of steps based on the status of history.
+ // A dirty history will return all the entries we are expecting bringing
+ // our countdown to zero, while a clean one will not.
+ let oldStatus = this.status;
+ this.status = this._expectedResultsCount == 0 ? STATUS.DIRTY
+ : STATUS.CLEAN;
+
+ // Collect or send telemetry data.
+ if (this.status == STATUS.DIRTY) {
+ this._telemetrySteps++;
+ }
+ else {
+ // Avoid reporting the common cases where the database is clean, or
+ // a single step is needed.
+ if (oldStatus == STATUS.DIRTY) {
+ try {
+ Services.telemetry
+ .getHistogramById("PLACES_EXPIRATION_STEPS_TO_CLEAN2")
+ .add(this._telemetrySteps);
+ } catch (ex) {
+ Components.utils.reportError("Unable to report telemetry.");
+ }
+ }
+ this._telemetrySteps = 1;
+ }
+
+ delete this._expectedResultsCount;
+ }
+
+ // Dispatch a notification that expiration has finished.
+ Services.obs.notifyObservers(null, TOPIC_EXPIRATION_FINISHED, null);
+ }
+ },
+
+ // nsPlacesExpiration
+
+ _urisLimit: PREF_MAX_URIS_NOTSET,
+ _interval: PREF_INTERVAL_SECONDS_NOTSET,
+ _shuttingDown: false,
+
+ _status: STATUS.UNKNOWN,
+ set status(aNewStatus) {
+ if (aNewStatus != this._status) {
+ // If status changes we should restart the timer.
+ this._status = aNewStatus;
+ this._newTimer();
+ // If needed add/remove the cleanup step on idle. We want to expire on
+ // idle only if history is dirty, to preserve mobile devices batteries.
+ this.expireOnIdle = aNewStatus == STATUS.DIRTY;
+ }
+ return aNewStatus;
+ },
+ get status() {
+ return this._status;
+ },
+
+ _isIdleObserver: false,
+ _expireOnIdle: false,
+ set expireOnIdle(aExpireOnIdle) {
+ // Observe idle regardless aExpireOnIdle, since we always want to stop
+ // timed expiration on idle, to preserve mobile battery life.
+ if (!this._isIdleObserver && !this._shuttingDown) {
+ this._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+ this._isIdleObserver = true;
+ }
+ else if (this._isIdleObserver && this._shuttingDown) {
+ this._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+ this._isIdleObserver = false;
+ }
+
+ // If running a debug expiration we need full control of what happens
+ // but idle cleanup could activate in the middle, since tinderboxes are
+ // permanently idle. That would cause unexpected oranges, so disable it.
+ if (this._debugLimit !== undefined)
+ this._expireOnIdle = false;
+ else
+ this._expireOnIdle = aExpireOnIdle;
+ return this._expireOnIdle;
+ },
+ get expireOnIdle() {
+ return this._expireOnIdle;
+ },
+
+ _loadPrefs: Task.async(function* () {
+ // Get the user's limit, if it was set.
+ try {
+ // We want to silently fail since getIntPref throws if it does not exist,
+ // and use a default to fallback to.
+ this._urisLimit = this._prefBranch.getIntPref(PREF_MAX_URIS);
+ } catch (ex) { /* User limit not set */ }
+
+ if (this._urisLimit < 0) {
+ // Some testing code expects a pref change to be synchronous, so
+ // temporarily set this to a large value, while we asynchronously update
+ // to the correct value.
+ this._urisLimit = 300000;
+
+ // The user didn't specify a custom limit, so we calculate the number of
+ // unique places that may fit an optimal database size on this hardware.
+ // Oldest pages over this threshold will be expired.
+ let memSizeBytes = MEMSIZE_FALLBACK_BYTES;
+ try {
+ // Limit the size on systems with small memory.
+ memSizeBytes = Services.sysinfo.getProperty("memsize");
+ } catch (ex) {}
+ if (memSizeBytes <= 0) {
+ memsize = MEMSIZE_FALLBACK_BYTES;
+ }
+
+ let diskAvailableBytes = DISKSIZE_FALLBACK_BYTES;
+ try {
+ // Protect against a full disk or tiny quota.
+ let dbFile = this._db.databaseFile;
+ dbFile.QueryInterface(Ci.nsILocalFile);
+ diskAvailableBytes = dbFile.diskSpaceAvailable;
+ } catch (ex) {}
+ if (diskAvailableBytes <= 0) {
+ diskAvailableBytes = DISKSIZE_FALLBACK_BYTES;
+ }
+
+ let optimalDatabaseSize = Math.min(
+ memSizeBytes * DATABASE_TO_MEMORY_PERC / 100,
+ diskAvailableBytes * DATABASE_TO_DISK_PERC / 100,
+ DATABASE_MAX_SIZE
+ );
+
+ // Calculate avg size of a URI in the database.
+ let db = yield PlacesUtils.promiseDBConnection();
+ let pageSize = (yield db.execute(`PRAGMA page_size`))[0].getResultByIndex(0);
+ let pageCount = (yield db.execute(`PRAGMA page_count`))[0].getResultByIndex(0);
+ let freelistCount = (yield db.execute(`PRAGMA freelist_count`))[0].getResultByIndex(0);
+ let dbSize = (pageCount - freelistCount) * pageSize;
+ let uriCount = (yield db.execute(`SELECT count(*) FROM moz_places`))[0].getResultByIndex(0);
+ let avgURISize = Math.ceil(dbSize / uriCount);
+ // For new profiles this value may be too large, due to the Sqlite header,
+ // or Infinity when there are no pages. Thus we must limit it.
+ if (avgURISize > (URIENTRY_AVG_SIZE * 3)) {
+ avgURISize = URIENTRY_AVG_SIZE;
+ }
+ this._urisLimit = Math.ceil(optimalDatabaseSize / avgURISize);
+ }
+
+ // Expose the calculated limit to other components.
+ this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS,
+ this._urisLimit);
+
+ // Get the expiration interval value.
+ try {
+ // We want to silently fail since getIntPref throws if it does not exist,
+ // and use a default to fallback to.
+ this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS);
+ } catch (ex) { /* User interval not set */ }
+ if (this._interval <= 0) {
+ this._interval = PREF_INTERVAL_SECONDS_NOTSET;
+ }
+ }),
+
+ /**
+ * Evaluates the real number of pages in the database and the value currently
+ * used by the SQLite query planner.
+ *
+ * @param aCallback
+ * invoked on success, function (aPagesCount, aStatsCount).
+ */
+ _getPagesStats: function PEX__getPagesStats(aCallback) {
+ if (!this._cachedStatements["LIMIT_COUNT"]) {
+ this._cachedStatements["LIMIT_COUNT"] = this._db.createAsyncStatement(
+ `SELECT (SELECT COUNT(*) FROM moz_places),
+ (SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1
+ WHERE idx = 'moz_places_url_uniqueindex')`
+ );
+ }
+ this._cachedStatements["LIMIT_COUNT"].executeAsync({
+ _pagesCount: 0,
+ _statsCount: 0,
+ handleResult: function(aResults) {
+ let row = aResults.getNextRow();
+ this._pagesCount = row.getResultByIndex(0);
+ this._statsCount = row.getResultByIndex(1);
+ },
+ handleCompletion: function (aReason) {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ aCallback(this._pagesCount, this._statsCount);
+ }
+ },
+ handleError: function(aError) {
+ Cu.reportError("Async statement execution returned with '" +
+ aError.result + "', '" + aError.message + "'");
+ }
+ });
+ },
+
+ /**
+ * Execute async statements to expire with the specified queries.
+ *
+ * @param aAction
+ * The ACTION we are expiring for. See the ACTION const for values.
+ * @param aLimit
+ * Whether to use small, large or no limits when expiring. See the
+ * LIMIT const for values.
+ */
+ _expireWithActionAndLimit:
+ function PEX__expireWithActionAndLimit(aAction, aLimit)
+ {
+ // Skip expiration during batch mode.
+ if (this._inBatchMode)
+ return;
+ // Don't try to further expire after shutdown.
+ if (this._shuttingDown && aAction != ACTION.SHUTDOWN_DIRTY) {
+ return;
+ }
+
+ let boundStatements = [];
+ for (let queryType in EXPIRATION_QUERIES) {
+ if (EXPIRATION_QUERIES[queryType].actions & aAction)
+ boundStatements.push(this._getBoundStatement(queryType, aLimit, aAction));
+ }
+
+ // Execute statements asynchronously in a transaction.
+ this._db.executeAsync(boundStatements, boundStatements.length, this);
+ },
+
+ /**
+ * Finalizes all of our mozIStorageStatements so we can properly close the
+ * database.
+ */
+ _finalizeInternalStatements: function PEX__finalizeInternalStatements()
+ {
+ for (let queryType in this._cachedStatements) {
+ let stmt = this._cachedStatements[queryType];
+ stmt.finalize();
+ }
+ },
+
+ /**
+ * Generate the statement used for expiration.
+ *
+ * @param aQueryType
+ * Type of the query to build statement for.
+ * @param aLimit
+ * Whether to use small, large or no limits when expiring. See the
+ * LIMIT const for values.
+ * @param aAction
+ * Current action causing the expiration. See the ACTION const.
+ */
+ _cachedStatements: {},
+ _getBoundStatement: function PEX__getBoundStatement(aQueryType, aLimit, aAction)
+ {
+ // Statements creation can be expensive, so we want to cache them.
+ let stmt = this._cachedStatements[aQueryType];
+ if (stmt === undefined) {
+ stmt = this._cachedStatements[aQueryType] =
+ this._db.createAsyncStatement(EXPIRATION_QUERIES[aQueryType].sql);
+ }
+
+ let baseLimit;
+ switch (aLimit) {
+ case LIMIT.UNLIMITED:
+ baseLimit = -1;
+ break;
+ case LIMIT.SMALL:
+ baseLimit = EXPIRE_LIMIT_PER_STEP;
+ break;
+ case LIMIT.LARGE:
+ baseLimit = EXPIRE_LIMIT_PER_STEP * EXPIRE_LIMIT_PER_LARGE_STEP_MULTIPLIER;
+ break;
+ case LIMIT.DEBUG:
+ baseLimit = this._debugLimit;
+ break;
+ }
+ if (this.status == STATUS.DIRTY && aAction != ACTION.DEBUG &&
+ baseLimit > 0) {
+ baseLimit *= EXPIRE_AGGRESSIVITY_MULTIPLIER;
+ }
+
+ // Bind the appropriate parameters.
+ let params = stmt.params;
+ switch (aQueryType) {
+ case "QUERY_FIND_EXOTIC_VISITS_TO_EXPIRE":
+ // Avoid expiring all visits in case of an unlimited debug expiration,
+ // just remove orphans instead.
+ params.limit_visits =
+ aLimit == LIMIT.DEBUG && baseLimit == -1 ? 0 : baseLimit;
+ break;
+ case "QUERY_FIND_VISITS_TO_EXPIRE":
+ params.max_uris = this._urisLimit;
+ // Avoid expiring all visits in case of an unlimited debug expiration,
+ // just remove orphans instead.
+ params.limit_visits =
+ aLimit == LIMIT.DEBUG && baseLimit == -1 ? 0 : baseLimit;
+ break;
+ case "QUERY_FIND_URIS_TO_EXPIRE":
+ params.limit_uris = baseLimit;
+ break;
+ case "QUERY_SILENT_EXPIRE_ORPHAN_URIS":
+ params.limit_uris = baseLimit;
+ break;
+ case "QUERY_EXPIRE_FAVICONS":
+ params.limit_favicons = baseLimit;
+ break;
+ case "QUERY_EXPIRE_ANNOS":
+ // Each page may have multiple annos.
+ params.limit_annos = baseLimit * EXPIRE_AGGRESSIVITY_MULTIPLIER;
+ break;
+ case "QUERY_EXPIRE_ANNOS_WITH_POLICY":
+ case "QUERY_EXPIRE_ITEMS_ANNOS_WITH_POLICY":
+ let microNow = Date.now() * 1000;
+ ANNOS_EXPIRE_POLICIES.forEach(function(policy) {
+ params[policy.bind] = policy.type;
+ params[policy.bind + "_time"] = microNow - policy.time;
+ });
+ break;
+ case "QUERY_EXPIRE_ANNOS_WITH_HISTORY":
+ params.expire_with_history = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY;
+ break;
+ case "QUERY_EXPIRE_ITEMS_ANNOS":
+ params.limit_annos = baseLimit;
+ break;
+ case "QUERY_EXPIRE_ANNO_ATTRIBUTES":
+ params.limit_annos = baseLimit;
+ break;
+ case "QUERY_EXPIRE_INPUTHISTORY":
+ params.limit_inputhistory = baseLimit;
+ break;
+ case "QUERY_EXPIRE_ANNOS_SESSION":
+ case "QUERY_EXPIRE_ITEMS_ANNOS_SESSION":
+ params.expire_session = Ci.nsIAnnotationService.EXPIRE_SESSION;
+ break;
+ }
+
+ return stmt;
+ },
+
+ /**
+ * Creates a new timer based on this._interval.
+ *
+ * @return a REPEATING_SLACK nsITimer that runs every this._interval.
+ */
+ _newTimer: function PEX__newTimer()
+ {
+ if (this._timer)
+ this._timer.cancel();
+ if (this._shuttingDown)
+ return undefined;
+ let interval = this.status != STATUS.DIRTY ?
+ this._interval * EXPIRE_AGGRESSIVITY_MULTIPLIER : this._interval;
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, interval * 1000,
+ Ci.nsITimer.TYPE_REPEATING_SLACK);
+ if (this._testingMode) {
+ Services.obs.notifyObservers(null, TOPIC_TEST_INTERVAL_CHANGED,
+ interval);
+ }
+ return this._timer = timer;
+ },
+
+ // nsISupports
+
+ classID: Components.ID("705a423f-2f69-42f3-b9fe-1517e0dee56f"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesExpiration),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver
+ , Ci.nsINavHistoryObserver
+ , Ci.nsITimerCallback
+ , Ci.mozIStorageStatementCallback
+ , Ci.nsISupportsWeakReference
+ ])
+};
+
+// Module Registration
+
+var components = [nsPlacesExpiration];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/components/places/nsPlacesIndexes.h b/toolkit/components/places/nsPlacesIndexes.h
new file mode 100644
index 0000000000..9cce5a0aa9
--- /dev/null
+++ b/toolkit/components/places/nsPlacesIndexes.h
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsPlacesIndexes_h__
+#define nsPlacesIndexes_h__
+
+#define CREATE_PLACES_IDX(__name, __table, __columns, __type) \
+ NS_LITERAL_CSTRING( \
+ "CREATE " __type " INDEX IF NOT EXISTS " __table "_" __name \
+ " ON " __table " (" __columns ")" \
+ )
+
+/**
+ * moz_places
+ */
+#define CREATE_IDX_MOZ_PLACES_URL_HASH \
+ CREATE_PLACES_IDX( \
+ "url_hashindex", "moz_places", "url_hash", "" \
+ )
+
+#define CREATE_IDX_MOZ_PLACES_FAVICON \
+ CREATE_PLACES_IDX( \
+ "faviconindex", "moz_places", "favicon_id", "" \
+ )
+
+#define CREATE_IDX_MOZ_PLACES_REVHOST \
+ CREATE_PLACES_IDX( \
+ "hostindex", "moz_places", "rev_host", "" \
+ )
+
+#define CREATE_IDX_MOZ_PLACES_VISITCOUNT \
+ CREATE_PLACES_IDX( \
+ "visitcount", "moz_places", "visit_count", "" \
+ )
+
+#define CREATE_IDX_MOZ_PLACES_FRECENCY \
+ CREATE_PLACES_IDX( \
+ "frecencyindex", "moz_places", "frecency", "" \
+ )
+
+#define CREATE_IDX_MOZ_PLACES_LASTVISITDATE \
+ CREATE_PLACES_IDX( \
+ "lastvisitdateindex", "moz_places", "last_visit_date", "" \
+ )
+
+#define CREATE_IDX_MOZ_PLACES_GUID \
+ CREATE_PLACES_IDX( \
+ "guid_uniqueindex", "moz_places", "guid", "UNIQUE" \
+ )
+
+/**
+ * moz_historyvisits
+ */
+
+#define CREATE_IDX_MOZ_HISTORYVISITS_PLACEDATE \
+ CREATE_PLACES_IDX( \
+ "placedateindex", "moz_historyvisits", "place_id, visit_date", "" \
+ )
+
+#define CREATE_IDX_MOZ_HISTORYVISITS_FROMVISIT \
+ CREATE_PLACES_IDX( \
+ "fromindex", "moz_historyvisits", "from_visit", "" \
+ )
+
+#define CREATE_IDX_MOZ_HISTORYVISITS_VISITDATE \
+ CREATE_PLACES_IDX( \
+ "dateindex", "moz_historyvisits", "visit_date", "" \
+ )
+
+/**
+ * moz_bookmarks
+ */
+
+#define CREATE_IDX_MOZ_BOOKMARKS_PLACETYPE \
+ CREATE_PLACES_IDX( \
+ "itemindex", "moz_bookmarks", "fk, type", "" \
+ )
+
+#define CREATE_IDX_MOZ_BOOKMARKS_PARENTPOSITION \
+ CREATE_PLACES_IDX( \
+ "parentindex", "moz_bookmarks", "parent, position", "" \
+ )
+
+#define CREATE_IDX_MOZ_BOOKMARKS_PLACELASTMODIFIED \
+ CREATE_PLACES_IDX( \
+ "itemlastmodifiedindex", "moz_bookmarks", "fk, lastModified", "" \
+ )
+
+#define CREATE_IDX_MOZ_BOOKMARKS_GUID \
+ CREATE_PLACES_IDX( \
+ "guid_uniqueindex", "moz_bookmarks", "guid", "UNIQUE" \
+ )
+
+/**
+ * moz_annos
+ */
+
+#define CREATE_IDX_MOZ_ANNOS_PLACEATTRIBUTE \
+ CREATE_PLACES_IDX( \
+ "placeattributeindex", "moz_annos", "place_id, anno_attribute_id", "UNIQUE" \
+ )
+
+/**
+ * moz_items_annos
+ */
+
+#define CREATE_IDX_MOZ_ITEMSANNOS_PLACEATTRIBUTE \
+ CREATE_PLACES_IDX( \
+ "itemattributeindex", "moz_items_annos", "item_id, anno_attribute_id", "UNIQUE" \
+ )
+
+/**
+ * moz_keywords
+ */
+
+#define CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA \
+ CREATE_PLACES_IDX( \
+ "placepostdata_uniqueindex", "moz_keywords", "place_id, post_data", "UNIQUE" \
+ )
+
+#endif // nsPlacesIndexes_h__
diff --git a/toolkit/components/places/nsPlacesMacros.h b/toolkit/components/places/nsPlacesMacros.h
new file mode 100644
index 0000000000..47ebe17ac9
--- /dev/null
+++ b/toolkit/components/places/nsPlacesMacros.h
@@ -0,0 +1,82 @@
+/* -*- 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 "nsIConsoleService.h"
+#include "nsIScriptError.h"
+
+#ifndef __FUNCTION__
+#define __FUNCTION__ __func__
+#endif
+
+// Call a method on each observer in a category cache, then call the same
+// method on the observer array.
+#define NOTIFY_OBSERVERS(canFire, cache, array, type, method) \
+ PR_BEGIN_MACRO \
+ if (canFire) { \
+ nsCOMArray<type> entries; \
+ cache.GetEntries(entries); \
+ for (int32_t idx = 0; idx < entries.Count(); ++idx) \
+ entries[idx]->method; \
+ ENUMERATE_WEAKARRAY(array, type, method) \
+ } \
+ PR_END_MACRO;
+
+#define NOTIFY_BOOKMARKS_OBSERVERS(canFire, cache, array, skipIf, method) \
+ PR_BEGIN_MACRO \
+ if (canFire) { \
+ nsCOMArray<nsINavBookmarkObserver> entries; \
+ cache.GetEntries(entries); \
+ for (int32_t idx = 0; idx < entries.Count(); ++idx) { \
+ if (skipIf(entries[idx])) \
+ continue; \
+ entries[idx]->method; \
+ } \
+ for (uint32_t idx = 0; idx < array.Length(); ++idx) { \
+ const nsCOMPtr<nsINavBookmarkObserver> &e = array.ElementAt(idx).GetValue(); \
+ if (e) { \
+ if (skipIf(e)) \
+ continue; \
+ e->method; \
+ } \
+ } \
+ } \
+ PR_END_MACRO;
+
+#define PLACES_FACTORY_SINGLETON_IMPLEMENTATION(_className, _sInstance) \
+ _className * _className::_sInstance = nullptr; \
+ \
+ already_AddRefed<_className> \
+ _className::GetSingleton() \
+ { \
+ if (_sInstance) { \
+ RefPtr<_className> ret = _sInstance; \
+ return ret.forget(); \
+ } \
+ _sInstance = new _className(); \
+ RefPtr<_className> ret = _sInstance; \
+ if (NS_FAILED(_sInstance->Init())) { \
+ /* Null out ret before _sInstance so the destructor doesn't assert */ \
+ ret = nullptr; \
+ _sInstance = nullptr; \
+ return nullptr; \
+ } \
+ return ret.forget(); \
+ }
+
+#define PLACES_WARN_DEPRECATED() \
+ PR_BEGIN_MACRO \
+ nsCString msg(__FUNCTION__); \
+ msg.AppendLiteral(" is deprecated and will be removed in the next version.");\
+ NS_WARNING(msg.get()); \
+ nsCOMPtr<nsIConsoleService> cs = do_GetService(NS_CONSOLESERVICE_CONTRACTID);\
+ if (cs) { \
+ nsCOMPtr<nsIScriptError> e = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); \
+ if (e && NS_SUCCEEDED(e->Init(NS_ConvertUTF8toUTF16(msg), EmptyString(), \
+ EmptyString(), 0, 0, \
+ nsIScriptError::errorFlag, "Places"))) { \
+ cs->LogMessage(e); \
+ } \
+ } \
+ PR_END_MACRO
diff --git a/toolkit/components/places/nsPlacesModule.cpp b/toolkit/components/places/nsPlacesModule.cpp
new file mode 100644
index 0000000000..679d460b48
--- /dev/null
+++ b/toolkit/components/places/nsPlacesModule.cpp
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsIClassInfoImpl.h"
+
+#include "nsAnnoProtocolHandler.h"
+#include "nsAnnotationService.h"
+#include "nsNavHistory.h"
+#include "nsNavBookmarks.h"
+#include "nsFaviconService.h"
+#include "History.h"
+#include "nsDocShellCID.h"
+
+using namespace mozilla::places;
+
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsNavHistory,
+ nsNavHistory::GetSingleton)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsAnnotationService,
+ nsAnnotationService::GetSingleton)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsNavBookmarks,
+ nsNavBookmarks::GetSingleton)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsFaviconService,
+ nsFaviconService::GetSingleton)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(History, History::GetSingleton)
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsAnnoProtocolHandler)
+NS_DEFINE_NAMED_CID(NS_NAVHISTORYSERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_ANNOTATIONSERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_ANNOPROTOCOLHANDLER_CID);
+NS_DEFINE_NAMED_CID(NS_NAVBOOKMARKSSERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_FAVICONSERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_HISTORYSERVICE_CID);
+
+const mozilla::Module::CIDEntry kPlacesCIDs[] = {
+ { &kNS_NAVHISTORYSERVICE_CID, false, nullptr, nsNavHistoryConstructor },
+ { &kNS_ANNOTATIONSERVICE_CID, false, nullptr, nsAnnotationServiceConstructor },
+ { &kNS_ANNOPROTOCOLHANDLER_CID, false, nullptr, nsAnnoProtocolHandlerConstructor },
+ { &kNS_NAVBOOKMARKSSERVICE_CID, false, nullptr, nsNavBookmarksConstructor },
+ { &kNS_FAVICONSERVICE_CID, false, nullptr, nsFaviconServiceConstructor },
+ { &kNS_HISTORYSERVICE_CID, false, nullptr, HistoryConstructor },
+ { nullptr }
+};
+
+const mozilla::Module::ContractIDEntry kPlacesContracts[] = {
+ { NS_NAVHISTORYSERVICE_CONTRACTID, &kNS_NAVHISTORYSERVICE_CID },
+ { NS_ANNOTATIONSERVICE_CONTRACTID, &kNS_ANNOTATIONSERVICE_CID },
+ { NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "moz-anno", &kNS_ANNOPROTOCOLHANDLER_CID },
+ { NS_NAVBOOKMARKSSERVICE_CONTRACTID, &kNS_NAVBOOKMARKSSERVICE_CID },
+ { NS_FAVICONSERVICE_CONTRACTID, &kNS_FAVICONSERVICE_CID },
+ { "@mozilla.org/embeddor.implemented/bookmark-charset-resolver;1", &kNS_NAVHISTORYSERVICE_CID },
+ { NS_IHISTORY_CONTRACTID, &kNS_HISTORYSERVICE_CID },
+ { NS_DOWNLOADHISTORY_CONTRACTID, &kNS_HISTORYSERVICE_CID },
+ { nullptr }
+};
+
+const mozilla::Module::CategoryEntry kPlacesCategories[] = {
+ { "vacuum-participant", "Places", NS_NAVHISTORYSERVICE_CONTRACTID },
+ { nullptr }
+};
+
+const mozilla::Module kPlacesModule = {
+ mozilla::Module::kVersion,
+ kPlacesCIDs,
+ kPlacesContracts,
+ kPlacesCategories
+};
+
+NSMODULE_DEFN(nsPlacesModule) = &kPlacesModule;
diff --git a/toolkit/components/places/nsPlacesTables.h b/toolkit/components/places/nsPlacesTables.h
new file mode 100644
index 0000000000..aca92735e9
--- /dev/null
+++ b/toolkit/components/places/nsPlacesTables.h
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 __nsPlacesTables_h__
+#define __nsPlacesTables_h__
+
+
+#define CREATE_MOZ_PLACES NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_places ( " \
+ " id INTEGER PRIMARY KEY" \
+ ", url LONGVARCHAR" \
+ ", title LONGVARCHAR" \
+ ", rev_host LONGVARCHAR" \
+ ", visit_count INTEGER DEFAULT 0" \
+ ", hidden INTEGER DEFAULT 0 NOT NULL" \
+ ", typed INTEGER DEFAULT 0 NOT NULL" \
+ ", favicon_id INTEGER" \
+ ", frecency INTEGER DEFAULT -1 NOT NULL" \
+ ", last_visit_date INTEGER " \
+ ", guid TEXT" \
+ ", foreign_count INTEGER DEFAULT 0 NOT NULL" \
+ ", url_hash INTEGER DEFAULT 0 NOT NULL " \
+ ")" \
+)
+
+#define CREATE_MOZ_HISTORYVISITS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_historyvisits (" \
+ " id INTEGER PRIMARY KEY" \
+ ", from_visit INTEGER" \
+ ", place_id INTEGER" \
+ ", visit_date INTEGER" \
+ ", visit_type INTEGER" \
+ ", session INTEGER" \
+ ")" \
+)
+
+
+#define CREATE_MOZ_INPUTHISTORY NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_inputhistory (" \
+ " place_id INTEGER NOT NULL" \
+ ", input LONGVARCHAR NOT NULL" \
+ ", use_count INTEGER" \
+ ", PRIMARY KEY (place_id, input)" \
+ ")" \
+)
+
+#define CREATE_MOZ_ANNOS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_annos (" \
+ " id INTEGER PRIMARY KEY" \
+ ", place_id INTEGER NOT NULL" \
+ ", anno_attribute_id INTEGER" \
+ ", mime_type VARCHAR(32) DEFAULT NULL" \
+ ", content LONGVARCHAR" \
+ ", flags INTEGER DEFAULT 0" \
+ ", expiration INTEGER DEFAULT 0" \
+ ", type INTEGER DEFAULT 0" \
+ ", dateAdded INTEGER DEFAULT 0" \
+ ", lastModified INTEGER DEFAULT 0" \
+ ")" \
+)
+
+#define CREATE_MOZ_ANNO_ATTRIBUTES NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_anno_attributes (" \
+ " id INTEGER PRIMARY KEY" \
+ ", name VARCHAR(32) UNIQUE NOT NULL" \
+ ")" \
+)
+
+#define CREATE_MOZ_ITEMS_ANNOS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_items_annos (" \
+ " id INTEGER PRIMARY KEY" \
+ ", item_id INTEGER NOT NULL" \
+ ", anno_attribute_id INTEGER" \
+ ", mime_type VARCHAR(32) DEFAULT NULL" \
+ ", content LONGVARCHAR" \
+ ", flags INTEGER DEFAULT 0" \
+ ", expiration INTEGER DEFAULT 0" \
+ ", type INTEGER DEFAULT 0" \
+ ", dateAdded INTEGER DEFAULT 0" \
+ ", lastModified INTEGER DEFAULT 0" \
+ ")" \
+)
+
+#define CREATE_MOZ_FAVICONS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_favicons (" \
+ " id INTEGER PRIMARY KEY" \
+ ", url LONGVARCHAR UNIQUE" \
+ ", data BLOB" \
+ ", mime_type VARCHAR(32)" \
+ ", expiration LONG" \
+ ")" \
+)
+
+#define CREATE_MOZ_BOOKMARKS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_bookmarks (" \
+ " id INTEGER PRIMARY KEY" \
+ ", type INTEGER" \
+ ", fk INTEGER DEFAULT NULL" /* place_id */ \
+ ", parent INTEGER" \
+ ", position INTEGER" \
+ ", title LONGVARCHAR" \
+ ", keyword_id INTEGER" \
+ ", folder_type TEXT" \
+ ", dateAdded INTEGER" \
+ ", lastModified INTEGER" \
+ ", guid TEXT" \
+ ")" \
+)
+
+#define CREATE_MOZ_KEYWORDS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_keywords (" \
+ " id INTEGER PRIMARY KEY AUTOINCREMENT" \
+ ", keyword TEXT UNIQUE" \
+ ", place_id INTEGER" \
+ ", post_data TEXT" \
+ ")" \
+)
+
+#define CREATE_MOZ_HOSTS NS_LITERAL_CSTRING( \
+ "CREATE TABLE moz_hosts (" \
+ " id INTEGER PRIMARY KEY" \
+ ", host TEXT NOT NULL UNIQUE" \
+ ", frecency INTEGER" \
+ ", typed INTEGER NOT NULL DEFAULT 0" \
+ ", prefix TEXT" \
+ ")" \
+)
+
+// Note: this should be kept up-to-date with the definition in
+// nsPlacesAutoComplete.js.
+#define CREATE_MOZ_OPENPAGES_TEMP NS_LITERAL_CSTRING( \
+ "CREATE TEMP TABLE moz_openpages_temp (" \
+ " url TEXT" \
+ ", userContextId INTEGER" \
+ ", open_count INTEGER" \
+ ", PRIMARY KEY (url, userContextId)" \
+ ")" \
+)
+
+// This table is used, along with moz_places_afterdelete_trigger, to update
+// hosts after places removals. During a DELETE FROM moz_places, hosts are
+// accumulated into this table, then a DELETE FROM moz_updatehosts_temp will
+// take care of updating the moz_hosts table for every modified host.
+// See CREATE_PLACES_AFTERDELETE_TRIGGER in nsPlacestriggers.h for details.
+#define CREATE_UPDATEHOSTS_TEMP NS_LITERAL_CSTRING( \
+ "CREATE TEMP TABLE moz_updatehosts_temp (" \
+ " host TEXT PRIMARY KEY " \
+ ") WITHOUT ROWID " \
+)
+
+#endif // __nsPlacesTables_h__
diff --git a/toolkit/components/places/nsPlacesTriggers.h b/toolkit/components/places/nsPlacesTriggers.h
new file mode 100644
index 0000000000..d5b45ff5ec
--- /dev/null
+++ b/toolkit/components/places/nsPlacesTriggers.h
@@ -0,0 +1,267 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsPlacesTables.h"
+
+#ifndef __nsPlacesTriggers_h__
+#define __nsPlacesTriggers_h__
+
+/**
+ * Exclude these visit types:
+ * 0 - invalid
+ * 4 - EMBED
+ * 7 - DOWNLOAD
+ * 8 - FRAMED_LINK
+ * 9 - RELOAD
+ **/
+#define EXCLUDED_VISIT_TYPES "0, 4, 7, 8, 9"
+
+/**
+ * This triggers update visit_count and last_visit_date based on historyvisits
+ * table changes.
+ */
+#define CREATE_HISTORYVISITS_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_historyvisits_afterinsert_v2_trigger " \
+ "AFTER INSERT ON moz_historyvisits FOR EACH ROW " \
+ "BEGIN " \
+ "SELECT store_last_inserted_id('moz_historyvisits', NEW.id); " \
+ "UPDATE moz_places SET " \
+ "visit_count = visit_count + (SELECT NEW.visit_type NOT IN (" EXCLUDED_VISIT_TYPES ")), "\
+ "last_visit_date = MAX(IFNULL(last_visit_date, 0), NEW.visit_date) " \
+ "WHERE id = NEW.place_id;" \
+ "END" \
+)
+
+#define CREATE_HISTORYVISITS_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_historyvisits_afterdelete_v2_trigger " \
+ "AFTER DELETE ON moz_historyvisits FOR EACH ROW " \
+ "BEGIN " \
+ "UPDATE moz_places SET " \
+ "visit_count = visit_count - (SELECT OLD.visit_type NOT IN (" EXCLUDED_VISIT_TYPES ")), "\
+ "last_visit_date = (SELECT visit_date FROM moz_historyvisits " \
+ "WHERE place_id = OLD.place_id " \
+ "ORDER BY visit_date DESC LIMIT 1) " \
+ "WHERE id = OLD.place_id;" \
+ "END" \
+)
+
+/**
+ * A predicate matching pages on rev_host, based on a given host value.
+ * 'host' may be either the moz_hosts.host column or an alias representing an
+ * equivalent value.
+ */
+#define HOST_TO_REVHOST_PREDICATE \
+ "rev_host = get_unreversed_host(host || '.') || '.' " \
+ "OR rev_host = get_unreversed_host(host || '.') || '.www.'"
+
+/**
+ * Select the best prefix for a host, based on existing pages registered for it.
+ * Prefixes have a priority, from the top to the bottom, so that secure pages
+ * have higher priority, and more generically "www." prefixed hosts come before
+ * unprefixed ones.
+ * Given a host, examine associated pages and:
+ * - if all of the typed pages start with https://www. return https://www.
+ * - if all of the typed pages start with https:// return https://
+ * - if all of the typed pages start with ftp: return ftp://
+ * - if all of the typed pages start with www. return www.
+ * - otherwise don't use any prefix
+ */
+#define HOSTS_PREFIX_PRIORITY_FRAGMENT \
+ "SELECT CASE " \
+ "WHEN 1 = ( " \
+ "SELECT min(substr(url,1,12) = 'https://www.') FROM moz_places h " \
+ "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+ ") THEN 'https://www.' " \
+ "WHEN 1 = ( " \
+ "SELECT min(substr(url,1,8) = 'https://') FROM moz_places h " \
+ "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+ ") THEN 'https://' " \
+ "WHEN 1 = ( " \
+ "SELECT min(substr(url,1,4) = 'ftp:') FROM moz_places h " \
+ "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+ ") THEN 'ftp://' " \
+ "WHEN 1 = ( " \
+ "SELECT min(substr(url,1,11) = 'http://www.') FROM moz_places h " \
+ "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+ ") THEN 'www.' " \
+ "END "
+
+/**
+ * These triggers update the hostnames table whenever moz_places changes.
+ */
+#define CREATE_PLACES_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_places_afterinsert_trigger " \
+ "AFTER INSERT ON moz_places FOR EACH ROW " \
+ "BEGIN " \
+ "SELECT store_last_inserted_id('moz_places', NEW.id); " \
+ "INSERT OR REPLACE INTO moz_hosts (id, host, frecency, typed, prefix) " \
+ "SELECT " \
+ "(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
+ "fixup_url(get_unreversed_host(NEW.rev_host)), " \
+ "MAX(IFNULL((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), -1), NEW.frecency), " \
+ "MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed), " \
+ "(" HOSTS_PREFIX_PRIORITY_FRAGMENT \
+ "FROM ( " \
+ "SELECT fixup_url(get_unreversed_host(NEW.rev_host)) AS host " \
+ ") AS match " \
+ ") " \
+ " WHERE LENGTH(NEW.rev_host) > 1; " \
+ "END" \
+)
+
+// This is a hack to workaround the lack of FOR EACH STATEMENT in Sqlite, until
+// bug 871908 can be fixed properly.
+// We store the modified hosts in a temp table, and after every DELETE FROM
+// moz_places, we issue a DELETE FROM moz_updatehosts_temp. The AFTER DELETE
+// trigger will then take care of updating the moz_hosts table.
+// Note this way we lose atomicity, crashing between the 2 queries may break the
+// hosts table coherency. So it's better to run those DELETE queries in a single
+// transaction.
+// Regardless, this is still better than hanging the browser for several minutes
+// on a fast machine.
+#define CREATE_PLACES_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_places_afterdelete_trigger " \
+ "AFTER DELETE ON moz_places FOR EACH ROW " \
+ "BEGIN " \
+ "INSERT OR IGNORE INTO moz_updatehosts_temp (host)" \
+ "VALUES (fixup_url(get_unreversed_host(OLD.rev_host)));" \
+ "END" \
+)
+
+#define CREATE_UPDATEHOSTS_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_updatehosts_afterdelete_trigger " \
+ "AFTER DELETE ON moz_updatehosts_temp FOR EACH ROW " \
+ "BEGIN " \
+ "DELETE FROM moz_hosts " \
+ "WHERE host = OLD.host " \
+ "AND NOT EXISTS(" \
+ "SELECT 1 FROM moz_places " \
+ "WHERE rev_host = get_unreversed_host(host || '.') || '.' " \
+ "OR rev_host = get_unreversed_host(host || '.') || '.www.' " \
+ "); " \
+ "UPDATE moz_hosts " \
+ "SET prefix = (" HOSTS_PREFIX_PRIORITY_FRAGMENT ") " \
+ "WHERE host = OLD.host; " \
+ "END" \
+)
+
+// For performance reasons the host frecency is updated only when the page
+// frecency changes by a meaningful percentage. This is because the frecency
+// decay algorithm requires to update all the frecencies at once, causing a
+// too high overhead, while leaving the ordering unchanged.
+#define CREATE_PLACES_AFTERUPDATE_FRECENCY_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_places_afterupdate_frecency_trigger " \
+ "AFTER UPDATE OF frecency ON moz_places FOR EACH ROW " \
+ "WHEN NEW.frecency >= 0 " \
+ "AND ABS(" \
+ "IFNULL((NEW.frecency - OLD.frecency) / CAST(NEW.frecency AS REAL), " \
+ "(NEW.frecency - OLD.frecency))" \
+ ") > .05 " \
+ "BEGIN " \
+ "UPDATE moz_hosts " \
+ "SET frecency = (SELECT MAX(frecency) FROM moz_places " \
+ "WHERE rev_host = get_unreversed_host(host || '.') || '.' " \
+ "OR rev_host = get_unreversed_host(host || '.') || '.www.') " \
+ "WHERE host = fixup_url(get_unreversed_host(NEW.rev_host)); " \
+ "END" \
+)
+
+#define CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_places_afterupdate_typed_trigger " \
+ "AFTER UPDATE OF typed ON moz_places FOR EACH ROW " \
+ "WHEN NEW.typed = 1 " \
+ "BEGIN " \
+ "UPDATE moz_hosts " \
+ "SET typed = 1 " \
+ "WHERE host = fixup_url(get_unreversed_host(NEW.rev_host)); " \
+ "END" \
+)
+
+/**
+ * This trigger removes a row from moz_openpages_temp when open_count reaches 0.
+ *
+ * @note this should be kept up-to-date with the definition in
+ * nsPlacesAutoComplete.js
+ */
+#define CREATE_REMOVEOPENPAGE_CLEANUP_TRIGGER NS_LITERAL_CSTRING( \
+ "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 " \
+ "AND userContextId = NEW.userContextId;" \
+ "END" \
+)
+
+#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterdelete_trigger " \
+ "AFTER DELETE ON moz_bookmarks FOR EACH ROW " \
+ "BEGIN " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count - 1 " \
+ "WHERE id = OLD.fk;" \
+ "END" \
+)
+
+#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterinsert_trigger " \
+ "AFTER INSERT ON moz_bookmarks FOR EACH ROW " \
+ "BEGIN " \
+ "SELECT store_last_inserted_id('moz_bookmarks', NEW.id); " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count + 1 " \
+ "WHERE id = NEW.fk;" \
+ "END" \
+)
+
+#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterupdate_trigger " \
+ "AFTER UPDATE OF fk ON moz_bookmarks FOR EACH ROW " \
+ "BEGIN " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count + 1 " \
+ "WHERE id = NEW.fk;" \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count - 1 " \
+ "WHERE id = OLD.fk;" \
+ "END" \
+)
+
+#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_keywords_foreign_count_afterdelete_trigger " \
+ "AFTER DELETE ON moz_keywords FOR EACH ROW " \
+ "BEGIN " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count - 1 " \
+ "WHERE id = OLD.place_id;" \
+ "END" \
+)
+
+#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_keyords_foreign_count_afterinsert_trigger " \
+ "AFTER INSERT ON moz_keywords FOR EACH ROW " \
+ "BEGIN " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count + 1 " \
+ "WHERE id = NEW.place_id;" \
+ "END" \
+)
+
+#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
+ "CREATE TEMP TRIGGER moz_keywords_foreign_count_afterupdate_trigger " \
+ "AFTER UPDATE OF place_id ON moz_keywords FOR EACH ROW " \
+ "BEGIN " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count + 1 " \
+ "WHERE id = NEW.place_id; " \
+ "UPDATE moz_places " \
+ "SET foreign_count = foreign_count - 1 " \
+ "WHERE id = OLD.place_id; " \
+ "END" \
+)
+
+#endif // __nsPlacesTriggers_h__
diff --git a/toolkit/components/places/nsTaggingService.js b/toolkit/components/places/nsTaggingService.js
new file mode 100644
index 0000000000..1fad67a829
--- /dev/null
+++ b/toolkit/components/places/nsTaggingService.js
@@ -0,0 +1,709 @@
+/* -*- 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+const TOPIC_SHUTDOWN = "places-shutdown";
+
+/**
+ * The Places Tagging Service
+ */
+function TaggingService() {
+ // Observe bookmarks changes.
+ PlacesUtils.bookmarks.addObserver(this, false);
+
+ // Cleanup on shutdown.
+ Services.obs.addObserver(this, TOPIC_SHUTDOWN, false);
+}
+
+TaggingService.prototype = {
+ /**
+ * Creates a tag container under the tags-root with the given name.
+ *
+ * @param aTagName
+ * the name for the new tag.
+ * @param aSource
+ * a change source constant from nsINavBookmarksService::SOURCE_*.
+ * @returns the id of the new tag container.
+ */
+ _createTag: function TS__createTag(aTagName, aSource) {
+ var newFolderId = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.tagsFolderId, aTagName, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aGuid */ null, aSource
+ );
+ // Add the folder to our local cache, so we can avoid doing this in the
+ // observer that would have to check itemType.
+ this._tagFolders[newFolderId] = aTagName;
+
+ return newFolderId;
+ },
+
+ /**
+ * Checks whether the given uri is tagged with the given tag.
+ *
+ * @param [in] aURI
+ * url to check for
+ * @param [in] aTagName
+ * the tag to check for
+ * @returns the item id if the URI is tagged with the given tag, -1
+ * otherwise.
+ */
+ _getItemIdForTaggedURI: function TS__getItemIdForTaggedURI(aURI, aTagName) {
+ var tagId = this._getItemIdForTag(aTagName);
+ if (tagId == -1)
+ return -1;
+ // Using bookmarks service API for this would be a pain.
+ // Until tags implementation becomes sane, go the query way.
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT id FROM moz_bookmarks
+ WHERE parent = :tag_id
+ AND fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url)`
+ );
+ stmt.params.tag_id = tagId;
+ stmt.params.page_url = aURI.spec;
+ try {
+ if (stmt.executeStep()) {
+ return stmt.row.id;
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+ return -1;
+ },
+
+ /**
+ * Returns the folder id for a tag, or -1 if not found.
+ * @param [in] aTag
+ * string tag to search for
+ * @returns integer id for the bookmark folder for the tag
+ */
+ _getItemIdForTag: function TS_getItemIdForTag(aTagName) {
+ for (var i in this._tagFolders) {
+ if (aTagName.toLowerCase() == this._tagFolders[i].toLowerCase())
+ return parseInt(i);
+ }
+ return -1;
+ },
+
+ /**
+ * Makes a proper array of tag objects like { id: number, name: string }.
+ *
+ * @param aTags
+ * Array of tags. Entries can be tag names or concrete item id.
+ * @param trim [optional]
+ * Whether to trim passed-in named tags. Defaults to false.
+ * @return Array of tag objects like { id: number, name: string }.
+ *
+ * @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
+ * a valid tag.
+ */
+ _convertInputMixedTagsArray(aTags, trim=false) {
+ // Handle sparse array with a .filter.
+ return aTags.filter(tag => tag !== undefined)
+ .map(idOrName => {
+ let tag = {};
+ if (typeof(idOrName) == "number" && this._tagFolders[idOrName]) {
+ // This is a tag folder id.
+ tag.id = idOrName;
+ // We can't know the name at this point, since a previous tag could
+ // want to change it.
+ tag.__defineGetter__("name", () => this._tagFolders[tag.id]);
+ }
+ else if (typeof(idOrName) == "string" && idOrName.length > 0 &&
+ idOrName.length <= Ci.nsITaggingService.MAX_TAG_LENGTH) {
+ // This is a tag name.
+ tag.name = trim ? idOrName.trim() : idOrName;
+ // We can't know the id at this point, since a previous tag could
+ // have created it.
+ tag.__defineGetter__("id", () => this._getItemIdForTag(tag.name));
+ }
+ else {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ return tag;
+ });
+ },
+
+ // nsITaggingService
+ tagURI: function TS_tagURI(aURI, aTags, aSource)
+ {
+ if (!aURI || !aTags || !Array.isArray(aTags)) {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+
+ // This also does some input validation.
+ let tags = this._convertInputMixedTagsArray(aTags, true);
+
+ let taggingFunction = () => {
+ for (let tag of tags) {
+ if (tag.id == -1) {
+ // Tag does not exist yet, create it.
+ this._createTag(tag.name, aSource);
+ }
+
+ if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
+ // The provided URI is not yet tagged, add a tag for it.
+ // Note that bookmarks under tag containers must have null titles.
+ PlacesUtils.bookmarks.insertBookmark(
+ tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ /* aTitle */ null, /* aGuid */ null, aSource
+ );
+ }
+
+ // Try to preserve user's tag name casing.
+ // Rename the tag container so the Places view matches the most-recent
+ // user-typed value.
+ if (PlacesUtils.bookmarks.getItemTitle(tag.id) != tag.name) {
+ // this._tagFolders is updated by the bookmarks observer.
+ PlacesUtils.bookmarks.setItemTitle(tag.id, tag.name, aSource);
+ }
+ }
+ };
+
+ // Use a batch only if creating more than 2 tags.
+ if (tags.length < 3) {
+ taggingFunction();
+ } else {
+ PlacesUtils.bookmarks.runInBatchMode(taggingFunction, null);
+ }
+ },
+
+ /**
+ * Removes the tag container from the tags root if the given tag is empty.
+ *
+ * @param aTagId
+ * the itemId of the tag element under the tags root
+ * @param aSource
+ * a change source constant from nsINavBookmarksService::SOURCE_*
+ */
+ _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId, aSource) {
+ let count = 0;
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT count(*) AS count FROM moz_bookmarks
+ WHERE parent = :tag_id`
+ );
+ stmt.params.tag_id = aTagId;
+ try {
+ if (stmt.executeStep()) {
+ count = stmt.row.count;
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ if (count == 0) {
+ PlacesUtils.bookmarks.removeItem(aTagId, aSource);
+ }
+ },
+
+ // nsITaggingService
+ untagURI: function TS_untagURI(aURI, aTags, aSource)
+ {
+ if (!aURI || (aTags && !Array.isArray(aTags))) {
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aTags) {
+ // Passing null should clear all tags for aURI, see the IDL.
+ // XXXmano: write a perf-sensitive version of this code path...
+ aTags = this.getTagsForURI(aURI);
+ }
+
+ // This also does some input validation.
+ let tags = this._convertInputMixedTagsArray(aTags);
+
+ let isAnyTagNotTrimmed = tags.some(tag => /^\s|\s$/.test(tag.name));
+ if (isAnyTagNotTrimmed) {
+ Deprecated.warning("At least one tag passed to untagURI was not trimmed",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
+ }
+
+ let untaggingFunction = () => {
+ for (let tag of tags) {
+ if (tag.id != -1) {
+ // A tag could exist.
+ let itemId = this._getItemIdForTaggedURI(aURI, tag.name);
+ if (itemId != -1) {
+ // There is a tagged item.
+ PlacesUtils.bookmarks.removeItem(itemId, aSource);
+ }
+ }
+ }
+ };
+
+ // Use a batch only if creating more than 2 tags.
+ if (tags.length < 3) {
+ untaggingFunction();
+ } else {
+ PlacesUtils.bookmarks.runInBatchMode(untaggingFunction, null);
+ }
+ },
+
+ // nsITaggingService
+ getURIsForTag: function TS_getURIsForTag(aTagName) {
+ if (!aTagName || aTagName.length == 0)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ if (/^\s|\s$/.test(aTagName)) {
+ Deprecated.warning("Tag passed to getURIsForTag was not trimmed",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
+ }
+
+ let uris = [];
+ let tagId = this._getItemIdForTag(aTagName);
+ if (tagId == -1)
+ return uris;
+
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT h.url FROM moz_places h
+ JOIN moz_bookmarks b ON b.fk = h.id
+ WHERE b.parent = :tag_id`
+ );
+ stmt.params.tag_id = tagId;
+ try {
+ while (stmt.executeStep()) {
+ try {
+ uris.push(Services.io.newURI(stmt.row.url, null, null));
+ } catch (ex) {}
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ return uris;
+ },
+
+ // nsITaggingService
+ getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
+ if (!aURI)
+ throw Cr.NS_ERROR_INVALID_ARG;
+
+ var tags = [];
+ var bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(aURI);
+ for (var i=0; i < bookmarkIds.length; i++) {
+ var folderId = PlacesUtils.bookmarks.getFolderIdForItem(bookmarkIds[i]);
+ if (this._tagFolders[folderId])
+ tags.push(this._tagFolders[folderId]);
+ }
+
+ // sort the tag list
+ tags.sort(function(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ });
+ if (aCount)
+ aCount.value = tags.length;
+ return tags;
+ },
+
+ __tagFolders: null,
+ get _tagFolders() {
+ if (!this.__tagFolders) {
+ this.__tagFolders = [];
+
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ "SELECT id, title FROM moz_bookmarks WHERE parent = :tags_root "
+ );
+ stmt.params.tags_root = PlacesUtils.tagsFolderId;
+ try {
+ while (stmt.executeStep()) {
+ this.__tagFolders[stmt.row.id] = stmt.row.title;
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+ }
+
+ return this.__tagFolders;
+ },
+
+ // nsITaggingService
+ get allTags() {
+ var allTags = [];
+ for (var i in this._tagFolders)
+ allTags.push(this._tagFolders[i]);
+ // sort the tag list
+ allTags.sort(function(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ });
+ return allTags;
+ },
+
+ // nsITaggingService
+ get hasTags() {
+ return this._tagFolders.length > 0;
+ },
+
+ // nsIObserver
+ observe: function TS_observe(aSubject, aTopic, aData) {
+ if (aTopic == TOPIC_SHUTDOWN) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ Services.obs.removeObserver(this, TOPIC_SHUTDOWN);
+ }
+ },
+
+ /**
+ * If the only bookmark items associated with aURI are contained in tag
+ * folders, returns the IDs of those items. This can be the case if
+ * the URI was bookmarked and tagged at some point, but the bookmark was
+ * removed, leaving only the bookmark items in tag folders. If the URI is
+ * either properly bookmarked or not tagged just returns and empty array.
+ *
+ * @param aURI
+ * A URI (string) that may or may not be bookmarked
+ * @returns an array of item ids
+ */
+ _getTaggedItemIdsIfUnbookmarkedURI:
+ function TS__getTaggedItemIdsIfUnbookmarkedURI(aURI) {
+ var itemIds = [];
+ var isBookmarked = false;
+
+ // Using bookmarks service API for this would be a pain.
+ // Until tags implementation becomes sane, go the query way.
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let stmt = db.createStatement(
+ `SELECT id, parent
+ FROM moz_bookmarks
+ WHERE fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url)`
+ );
+ stmt.params.page_url = aURI.spec;
+ try {
+ while (stmt.executeStep() && !isBookmarked) {
+ if (this._tagFolders[stmt.row.parent]) {
+ // This is a tag entry.
+ itemIds.push(stmt.row.id);
+ }
+ else {
+ // This is a real bookmark, so the bookmarked URI is not an orphan.
+ isBookmarked = true;
+ }
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ return isBookmarked ? [] : itemIds;
+ },
+
+ // nsINavBookmarkObserver
+ onItemAdded: function TS_onItemAdded(aItemId, aFolderId, aIndex, aItemType,
+ aURI, aTitle) {
+ // Nothing to do if this is not a tag.
+ if (aFolderId != PlacesUtils.tagsFolderId ||
+ aItemType != PlacesUtils.bookmarks.TYPE_FOLDER)
+ return;
+
+ this._tagFolders[aItemId] = aTitle;
+ },
+
+ onItemRemoved: function TS_onItemRemoved(aItemId, aFolderId, aIndex,
+ aItemType, aURI, aGuid, aParentGuid,
+ aSource) {
+ // Item is a tag folder.
+ if (aFolderId == PlacesUtils.tagsFolderId && this._tagFolders[aItemId]) {
+ delete this._tagFolders[aItemId];
+ }
+ // Item is a bookmark that was removed from a non-tag folder.
+ else if (aURI && !this._tagFolders[aFolderId]) {
+ // If the only bookmark items now associated with the bookmark's URI are
+ // contained in tag folders, the URI is no longer properly bookmarked, so
+ // untag it.
+ let itemIds = this._getTaggedItemIdsIfUnbookmarkedURI(aURI);
+ for (let i = 0; i < itemIds.length; i++) {
+ try {
+ PlacesUtils.bookmarks.removeItem(itemIds[i], aSource);
+ } catch (ex) {}
+ }
+ }
+ // Item is a tag entry. If this was the last entry for this tag, remove it.
+ else if (aURI && this._tagFolders[aFolderId]) {
+ this._removeTagIfEmpty(aFolderId, aSource);
+ }
+ },
+
+ onItemChanged: function TS_onItemChanged(aItemId, aProperty,
+ aIsAnnotationProperty, aNewValue,
+ aLastModified, aItemType) {
+ if (aProperty == "title" && this._tagFolders[aItemId])
+ this._tagFolders[aItemId] = aNewValue;
+ },
+
+ onItemMoved: function TS_onItemMoved(aItemId, aOldParent, aOldIndex,
+ aNewParent, aNewIndex, aItemType) {
+ if (this._tagFolders[aItemId] && PlacesUtils.tagsFolderId == aOldParent &&
+ PlacesUtils.tagsFolderId != aNewParent)
+ delete this._tagFolders[aItemId];
+ },
+
+ onItemVisited: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+
+ // nsISupports
+
+ classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(TaggingService),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsITaggingService
+ , Ci.nsINavBookmarkObserver
+ , Ci.nsIObserver
+ ])
+};
+
+
+function TagAutoCompleteResult(searchString, searchResult,
+ defaultIndex, errorDescription,
+ results, comments) {
+ this._searchString = searchString;
+ this._searchResult = searchResult;
+ this._defaultIndex = defaultIndex;
+ this._errorDescription = errorDescription;
+ this._results = results;
+ this._comments = comments;
+}
+
+TagAutoCompleteResult.prototype = {
+
+ /**
+ * The original search string
+ */
+ get searchString() {
+ return this._searchString;
+ },
+
+ /**
+ * The result code of this result object, either:
+ * RESULT_IGNORED (invalid searchString)
+ * RESULT_FAILURE (failure)
+ * RESULT_NOMATCH (no matches found)
+ * RESULT_SUCCESS (matches found)
+ */
+ get searchResult() {
+ return this._searchResult;
+ },
+
+ /**
+ * Index of the default item that should be entered if none is selected
+ */
+ get defaultIndex() {
+ return this._defaultIndex;
+ },
+
+ /**
+ * A string describing the cause of a search failure
+ */
+ get errorDescription() {
+ return this._errorDescription;
+ },
+
+ /**
+ * The number of matches
+ */
+ get matchCount() {
+ return this._results.length;
+ },
+
+ /**
+ * Get the value of the result at the given index
+ */
+ getValueAt: function PTACR_getValueAt(index) {
+ return this._results[index];
+ },
+
+ getLabelAt: function PTACR_getLabelAt(index) {
+ return this.getValueAt(index);
+ },
+
+ /**
+ * Get the comment of the result at the given index
+ */
+ getCommentAt: function PTACR_getCommentAt(index) {
+ return this._comments[index];
+ },
+
+ /**
+ * Get the style hint for the result at the given index
+ */
+ getStyleAt: function PTACR_getStyleAt(index) {
+ if (!this._comments[index])
+ return null; // not a category label, so no special styling
+
+ if (index == 0)
+ return "suggestfirst"; // category label on first line of results
+
+ return "suggesthint"; // category label on any other line of results
+ },
+
+ /**
+ * Get the image for the result at the given index
+ */
+ getImageAt: function PTACR_getImageAt(index) {
+ return null;
+ },
+
+ /**
+ * Get the image for the result at the given index
+ */
+ getFinalCompleteValueAt: function PTACR_getFinalCompleteValueAt(index) {
+ return this.getValueAt(index);
+ },
+
+ /**
+ * Remove the value at the given index from the autocomplete results.
+ * If removeFromDb is set to true, the value should be removed from
+ * persistent storage as well.
+ */
+ removeValueAt: function PTACR_removeValueAt(index, removeFromDb) {
+ this._results.splice(index, 1);
+ this._comments.splice(index, 1);
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteResult
+ ])
+};
+
+// Implements nsIAutoCompleteSearch
+function TagAutoCompleteSearch() {
+ XPCOMUtils.defineLazyServiceGetter(this, "tagging",
+ "@mozilla.org/browser/tagging-service;1",
+ "nsITaggingService");
+}
+
+TagAutoCompleteSearch.prototype = {
+ _stopped : false,
+
+ /*
+ * Search for a given string and notify a listener (either synchronously
+ * or asynchronously) of the result
+ *
+ * @param searchString - The string to search for
+ * @param searchParam - An extra parameter
+ * @param previousResult - A previous result to use for faster searching
+ * @param listener - A listener to notify when the search is complete
+ */
+ startSearch: function PTACS_startSearch(searchString, searchParam, result, listener) {
+ var searchResults = this.tagging.allTags;
+ var results = [];
+ var comments = [];
+ this._stopped = false;
+
+ // only search on characters for the last tag
+ var index = Math.max(searchString.lastIndexOf(","),
+ searchString.lastIndexOf(";"));
+ var before = '';
+ if (index != -1) {
+ before = searchString.slice(0, index+1);
+ searchString = searchString.slice(index+1);
+ // skip past whitespace
+ var m = searchString.match(/\s+/);
+ if (m) {
+ before += m[0];
+ searchString = searchString.slice(m[0].length);
+ }
+ }
+
+ if (!searchString.length) {
+ var newResult = new TagAutoCompleteResult(searchString,
+ Ci.nsIAutoCompleteResult.RESULT_NOMATCH, 0, "", results, comments);
+ listener.onSearchResult(self, newResult);
+ return;
+ }
+
+ var self = this;
+ // generator: if yields true, not done
+ function* doSearch() {
+ var i = 0;
+ while (i < searchResults.length) {
+ if (self._stopped)
+ yield false;
+ // for each match, prepend what the user has typed so far
+ if (searchResults[i].toLowerCase()
+ .indexOf(searchString.toLowerCase()) == 0 &&
+ !comments.includes(searchResults[i])) {
+ results.push(before + searchResults[i]);
+ comments.push(searchResults[i]);
+ }
+
+ ++i;
+
+ /* TODO: bug 481451
+ * For each yield we pass a new result to the autocomplete
+ * listener. The listener appends instead of replacing previous results,
+ * causing invalid matchCount values.
+ *
+ * As a workaround, all tags are searched through in a single batch,
+ * making this synchronous until the above issue is fixed.
+ */
+
+ /*
+ // 100 loops per yield
+ if ((i % 100) == 0) {
+ var newResult = new TagAutoCompleteResult(searchString,
+ Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING, 0, "", results, comments);
+ listener.onSearchResult(self, newResult);
+ yield true;
+ }
+ */
+ }
+
+ let searchResult = results.length > 0 ?
+ Ci.nsIAutoCompleteResult.RESULT_SUCCESS :
+ Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
+ var newResult = new TagAutoCompleteResult(searchString, searchResult, 0,
+ "", results, comments);
+ listener.onSearchResult(self, newResult);
+ yield false;
+ }
+
+ // chunk the search results via the generator
+ var gen = doSearch();
+ while (gen.next().value);
+ },
+
+ /**
+ * Stop an asynchronous search that is in progress
+ */
+ stopSearch: function PTACS_stopSearch() {
+ this._stopped = true;
+ },
+
+ // nsISupports
+
+ classID: Components.ID("{1dcc23b0-d4cb-11dc-9ad6-479d56d89593}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(TagAutoCompleteSearch),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteSearch
+ ])
+};
+
+var component = [TaggingService, TagAutoCompleteSearch];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/places/tests/.eslintrc.js b/toolkit/components/places/tests/.eslintrc.js
new file mode 100644
index 0000000000..d5283c9665
--- /dev/null
+++ b/toolkit/components/places/tests/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js",
+ "../../../../testing/mochitest/chrome.eslintrc.js",
+ "../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/PlacesTestUtils.jsm b/toolkit/components/places/tests/PlacesTestUtils.jsm
new file mode 100644
index 0000000000..36e425cae8
--- /dev/null
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -0,0 +1,163 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "PlacesTestUtils",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+this.PlacesTestUtils = Object.freeze({
+ /**
+ * Asynchronously adds visits to a page.
+ *
+ * @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,
+ * [optional] transition: one of the TRANSITION_* from nsINavHistoryService,
+ * [optional] title: title of the page,
+ * [optional] visitDate: visit date, either in microseconds from the epoch or as a date object
+ * [optional] referrer: nsIURI of the referrer for this visit
+ * }
+ *
+ * @return {Promise}
+ * @resolves When all visits have been added successfully.
+ * @rejects JavaScript exception.
+ */
+ addVisits: Task.async(function* (placeInfo) {
+ let places = [];
+ let infos = [];
+
+ if (placeInfo instanceof Ci.nsIURI ||
+ placeInfo instanceof URL ||
+ typeof placeInfo == "string") {
+ places.push({ uri: placeInfo });
+ }
+ else if (Array.isArray(placeInfo)) {
+ places = places.concat(placeInfo);
+ } else if (typeof placeInfo == "object" && placeInfo.uri) {
+ places.push(placeInfo)
+ } else {
+ throw new Error("Unsupported type passed to addVisits");
+ }
+
+ // Create a PageInfo for each entry.
+ for (let place of places) {
+ let info = {url: place.uri};
+ info.title = (typeof place.title === "string") ? place.title : "test visit for " + info.url.spec ;
+ if (typeof place.referrer == "string") {
+ place.referrer = NetUtil.newURI(place.referrer);
+ } else if (place.referrer && place.referrer instanceof URL) {
+ place.referrer = NetUtil.newURI(place.referrer.href);
+ }
+ let visitDate = place.visitDate;
+ if (visitDate) {
+ if (!(visitDate instanceof Date)) {
+ visitDate = PlacesUtils.toDate(visitDate);
+ }
+ } else {
+ visitDate = new Date();
+ }
+ info.visits = [{
+ transition: place.transition,
+ date: visitDate,
+ referrer: place.referrer
+ }];
+ infos.push(info);
+ }
+ return PlacesUtils.history.insertMany(infos);
+ }),
+
+ /**
+ * Clear all history.
+ *
+ * @return {Promise}
+ * @resolves When history was cleared successfully.
+ * @rejects JavaScript exception.
+ */
+ clearHistory() {
+ let expirationFinished = new Promise(resolve => {
+ Services.obs.addObserver(function observe(subj, topic, data) {
+ Services.obs.removeObserver(observe, topic);
+ resolve();
+ }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+ });
+
+ return Promise.all([expirationFinished, PlacesUtils.history.clear()]);
+ },
+
+ /**
+ * Waits for all pending async statements on the default connection.
+ *
+ * @return {Promise}
+ * @resolves When all pending async statements finished.
+ * @rejects Never.
+ *
+ * @note The result is achieved by asynchronously executing a query requiring
+ * a write lock. Since all statements on the same connection are
+ * serialized, the end of this write operation means that all writes are
+ * complete. Note that WAL makes so that writers don't block readers, but
+ * this is a problem only across different connections.
+ */
+ promiseAsyncUpdates() {
+ return PlacesUtils.withConnectionWrapper("promiseAsyncUpdates", Task.async(function* (db) {
+ try {
+ yield db.executeCached("BEGIN EXCLUSIVE");
+ yield db.executeCached("COMMIT");
+ } catch (ex) {
+ // If we fail to start a transaction, it's because there is already one.
+ // In such a case we should not try to commit the existing transaction.
+ }
+ }));
+ },
+
+ /**
+ * Asynchronously checks if an address is found in the database.
+ * @param aURI
+ * nsIURI or address to look for.
+ *
+ * @return {Promise}
+ * @resolves Returns true if the page is found.
+ * @rejects JavaScript exception.
+ */
+ isPageInDB: Task.async(function* (aURI) {
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(
+ "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
+ { url });
+ return rows.length > 0;
+ }),
+
+ /**
+ * Asynchronously checks how many visits exist for a specified page.
+ * @param aURI
+ * nsIURI or address to look for.
+ *
+ * @return {Promise}
+ * @resolves Returns the number of visits found.
+ * @rejects JavaScript exception.
+ */
+ visitsInDB: Task.async(function* (aURI) {
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(
+ `SELECT count(*) FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = v.place_id
+ WHERE url_hash = hash(:url) AND url = :url`,
+ { url });
+ return rows[0].getResultByIndex(0);
+ })
+});
diff --git a/toolkit/components/places/tests/bookmarks/.eslintrc.js b/toolkit/components/places/tests/bookmarks/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/bookmarks/head_bookmarks.js b/toolkit/components/places/tests/bookmarks/head_bookmarks.js
new file mode 100644
index 0000000000..842a66b313
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/head_bookmarks.js
@@ -0,0 +1,20 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.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.
diff --git a/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js
new file mode 100644
index 0000000000..b6982987b5
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js
@@ -0,0 +1,103 @@
+function run_test() {
+ run_next_test();
+}
+
+/* Bug 1016953 - When a previous bookmark backup exists with the same hash
+regardless of date, an automatic backup should attempt to either rename it to
+today's date if the backup was for an old date or leave it alone if it was for
+the same date. However if the file ext was json it will accidentally rename it
+to jsonlz4 while keeping the json contents
+*/
+
+add_task(function* test_same_date_same_hash() {
+ // If old file has been created on the same date and has the same hash
+ // the file should be left alone
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+ // Save to profile dir to obtain hash and nodeCount to append to filename
+ let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "bug10169583_bookmarks.json");
+ let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+
+ // Save JSON file in backup folder with hash appended
+ let dateObj = new Date();
+ let filename = "bookmarks-" + PlacesBackups.toISODateString(dateObj) + "_" +
+ count + "_" + hash + ".json";
+ let backupFile = OS.Path.join(backupFolder, filename);
+ yield OS.File.move(tempPath, backupFile);
+
+ // Force a compressed backup which fallbacks to rename
+ yield PlacesBackups.create();
+ let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+ // check to ensure not renamed to jsonlz4
+ Assert.equal(mostRecentBackupFile, backupFile);
+ // inspect contents and check if valid json
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let result = yield OS.File.read(mostRecentBackupFile);
+ let jsonString = converter.convertFromByteArray(result, result.length);
+ do_print("Check is valid JSON");
+ JSON.parse(jsonString);
+
+ // Cleanup
+ yield OS.File.remove(backupFile);
+ yield OS.File.remove(tempPath);
+ PlacesBackups._backupFiles = null; // To force re-cache of backupFiles
+});
+
+add_task(function* test_same_date_diff_hash() {
+ // If the old file has been created on the same date, but has a different hash
+ // the existing file should be overwritten with the newer compressed version
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+ let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "bug10169583_bookmarks.json");
+ let {count} = yield BookmarkJSONUtils.exportToFile(tempPath);
+ let dateObj = new Date();
+ let filename = "bookmarks-" + PlacesBackups.toISODateString(dateObj) + "_" +
+ count + "_" + "differentHash==" + ".json";
+ let backupFile = OS.Path.join(backupFolder, filename);
+ yield OS.File.move(tempPath, backupFile);
+ yield PlacesBackups.create(); // Force compressed backup
+ mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+
+ // Decode lz4 compressed file to json and check if json is valid
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let result = yield OS.File.read(mostRecentBackupFile, { compression: "lz4" });
+ let jsonString = converter.convertFromByteArray(result, result.length);
+ do_print("Check is valid JSON");
+ JSON.parse(jsonString);
+
+ // Cleanup
+ yield OS.File.remove(mostRecentBackupFile);
+ yield OS.File.remove(tempPath);
+ PlacesBackups._backupFiles = null; // To force re-cache of backupFiles
+});
+
+add_task(function* test_diff_date_same_hash() {
+ // If the old file has been created on an older day but has the same hash
+ // it should be renamed with today's date without altering the contents.
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+ let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "bug10169583_bookmarks.json");
+ let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+ let oldDate = new Date(2014, 1, 1);
+ let curDate = new Date();
+ let oldFilename = "bookmarks-" + PlacesBackups.toISODateString(oldDate) + "_" +
+ count + "_" + hash + ".json";
+ let newFilename = "bookmarks-" + PlacesBackups.toISODateString(curDate) + "_" +
+ count + "_" + hash + ".json";
+ let backupFile = OS.Path.join(backupFolder, oldFilename);
+ let newBackupFile = OS.Path.join(backupFolder, newFilename);
+ yield OS.File.move(tempPath, backupFile);
+
+ // Ensure file has been renamed correctly
+ yield PlacesBackups.create();
+ let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+ Assert.equal(mostRecentBackupFile, newBackupFile);
+
+ // Cleanup
+ yield OS.File.remove(mostRecentBackupFile);
+ yield OS.File.remove(tempPath);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js
new file mode 100644
index 0000000000..13755e576e
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js
@@ -0,0 +1,112 @@
+/* -*- 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 1017502 - Add a foreign_count column to moz_places
+This tests, tests the triggers that adjust the foreign_count when a bookmark is
+added or removed and also the maintenance task to fix wrong counts.
+*/
+
+const T_URI = NetUtil.newURI("https://www.mozilla.org/firefox/nightly/firstrun/");
+
+function* getForeignCountForURL(conn, url) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ url = url instanceof Ci.nsIURI ? url.spec : url;
+ let rows = yield conn.executeCached(
+ `SELECT foreign_count FROM moz_places WHERE url_hash = hash(:t_url)
+ AND url = :t_url`, { t_url: url });
+ return rows[0].getResultByName("foreign_count");
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* add_remove_change_bookmark_test() {
+ let conn = yield PlacesUtils.promiseDBConnection();
+
+ // Simulate a visit to the url
+ yield PlacesTestUtils.addVisits(T_URI);
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
+
+ // Add 1st bookmark which should increment foreign_count by 1
+ let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ T_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "First Run");
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1);
+
+ // Add 2nd bookmark
+ let id2 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ T_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "First Run");
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 2);
+
+ // Remove 2nd bookmark which should decrement foreign_count by 1
+ PlacesUtils.bookmarks.removeItem(id2);
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1);
+
+ // Change first bookmark's URI
+ const URI2 = NetUtil.newURI("http://www.mozilla.org");
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, URI2);
+ // Check foreign count for original URI
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
+ // Check foreign count for new URI
+ Assert.equal((yield getForeignCountForURL(conn, URI2)), 1);
+
+ // Cleanup - Remove changed bookmark
+ let id = PlacesUtils.bookmarks.getBookmarkIdsForURI(URI2);
+ PlacesUtils.bookmarks.removeItem(id);
+ Assert.equal((yield getForeignCountForURL(conn, URI2)), 0);
+
+});
+
+add_task(function* maintenance_foreign_count_test() {
+ let conn = yield PlacesUtils.promiseDBConnection();
+
+ // Simulate a visit to the url
+ yield PlacesTestUtils.addVisits(T_URI);
+
+ // Adjust the foreign_count for the added entry to an incorrect value
+ let deferred = Promise.defer();
+ let stmt = DBConn().createAsyncStatement(
+ `UPDATE moz_places SET foreign_count = 10 WHERE url_hash = hash(:t_url)
+ AND url = :t_url `);
+ stmt.params.t_url = T_URI.spec;
+ stmt.executeAsync({
+ handleCompletion: function() {
+ deferred.resolve();
+ }
+ });
+ stmt.finalize();
+ yield deferred.promise;
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 10);
+
+ // Run maintenance
+ Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+ let promiseMaintenanceFinished =
+ promiseTopicObserved("places-maintenance-finished");
+ PlacesDBUtils.maintenanceOnIdle();
+ yield promiseMaintenanceFinished;
+
+ // Check if the foreign_count has been adjusted to the correct value
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
+});
+
+add_task(function* add_remove_tags_test() {
+ let conn = yield PlacesUtils.promiseDBConnection();
+
+ yield PlacesTestUtils.addVisits(T_URI);
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
+
+ // Check foreign count incremented by 1 for a single tag
+ PlacesUtils.tagging.tagURI(T_URI, ["test tag"]);
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1);
+
+ // Check foreign count is incremented by 2 for two tags
+ PlacesUtils.tagging.tagURI(T_URI, ["one", "two"]);
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 3);
+
+ // Check foreign count is set to 0 when all tags are removed
+ PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]);
+ Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_1129529.js b/toolkit/components/places/tests/bookmarks/test_1129529.js
new file mode 100644
index 0000000000..da1ff708fa
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_1129529.js
@@ -0,0 +1,76 @@
+var now = Date.now() * 1000;
+
+// Test that importing bookmark data where a bookmark has a tag longer than 100
+// chars imports everything except the tags for that bookmark.
+add_task(function* () {
+ let aData = {
+ guid: "root________",
+ index: 0,
+ id: 1,
+ type: "text/x-moz-place-container",
+ dateAdded: now,
+ lastModified: now,
+ root: "placesRoot",
+ children: [{
+ guid: "unfiled_____",
+ index: 0,
+ id: 2,
+ type: "text/x-moz-place-container",
+ dateAdded: now,
+ lastModified: now,
+ root: "unfiledBookmarksFolder",
+ children: [
+ {
+ guid: "___guid1____",
+ index: 0,
+ id: 3,
+ charset: "UTF-8",
+ tags: "tag0",
+ type: "text/x-moz-place",
+ dateAdded: now,
+ lastModified: now,
+ uri: "http://test0.com/"
+ },
+ {
+ guid: "___guid2____",
+ index: 1,
+ id: 4,
+ charset: "UTF-8",
+ tags: "tag1," + "a" + "0123456789".repeat(10), // 101 chars
+ type: "text/x-moz-place",
+ dateAdded: now,
+ lastModified: now,
+ uri: "http://test1.com/"
+ },
+ {
+ guid: "___guid3____",
+ index: 2,
+ id: 5,
+ charset: "UTF-8",
+ tags: "tag2",
+ type: "text/x-moz-place",
+ dateAdded: now,
+ lastModified: now,
+ uri: "http://test2.com/"
+ }
+ ]
+ }]
+ };
+
+ let contentType = "application/json";
+ let uri = "data:" + contentType + "," + JSON.stringify(aData);
+ yield BookmarkJSONUtils.importFromURL(uri, false);
+
+ let [bookmarks] = yield PlacesBackups.getBookmarksTree();
+ let unsortedBookmarks = bookmarks.children[2].children;
+ Assert.equal(unsortedBookmarks.length, 3);
+
+ for (let i = 0; i < unsortedBookmarks.length; ++i) {
+ let bookmark = unsortedBookmarks[i];
+ Assert.equal(bookmark.charset, "UTF-8");
+ Assert.equal(bookmark.dateAdded, now);
+ Assert.equal(bookmark.lastModified, now);
+ Assert.equal(bookmark.uri, "http://test" + i + ".com/");
+ Assert.equal(bookmark.tags, "tag" + i);
+ }
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_384228.js b/toolkit/components/places/tests/bookmarks/test_384228.js
new file mode 100644
index 0000000000..9a52c97467
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_384228.js
@@ -0,0 +1,98 @@
+/* -*- 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/. */
+
+/**
+ * test querying for bookmarks in multiple folders.
+ */
+add_task(function* search_bookmark_in_folder() {
+ let testFolder1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bug 384228 test folder 1"
+ });
+ Assert.equal(testFolder1.index, 0);
+
+ let testFolder2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bug 384228 test folder 2"
+ });
+ Assert.equal(testFolder2.index, 1);
+
+ let testFolder3 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bug 384228 test folder 3"
+ });
+ Assert.equal(testFolder3.index, 2);
+
+ let b1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: testFolder1.guid,
+ url: "http://foo.tld/",
+ title: "title b1 (folder 1)"
+ });
+ Assert.equal(b1.index, 0);
+
+ let b2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: testFolder1.guid,
+ url: "http://foo.tld/",
+ title: "title b2 (folder 1)"
+ });
+ Assert.equal(b2.index, 1);
+
+ let b3 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: testFolder2.guid,
+ url: "http://foo.tld/",
+ title: "title b3 (folder 2)"
+ });
+ Assert.equal(b3.index, 0);
+
+ let b4 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: testFolder3.guid,
+ url: "http://foo.tld/",
+ title: "title b4 (folder 3)"
+ });
+ Assert.equal(b4.index, 0);
+
+ // also test recursive search
+ let testFolder1_1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: testFolder1.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bug 384228 test folder 1.1"
+ });
+ Assert.equal(testFolder1_1.index, 2);
+
+ let b5 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: testFolder1_1.guid,
+ url: "http://foo.tld/",
+ title: "title b5 (folder 1.1)"
+ });
+ Assert.equal(b5.index, 0);
+
+
+ // query folder 1, folder 2 and get 4 bookmarks
+ let folderIds = [];
+ folderIds.push(yield PlacesUtils.promiseItemId(testFolder1.guid));
+ folderIds.push(yield PlacesUtils.promiseItemId(testFolder2.guid));
+
+ let hs = PlacesUtils.history;
+ let options = hs.getNewQueryOptions();
+ let query = hs.getNewQuery();
+ query.searchTerms = "title";
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ query.setFolders(folderIds, folderIds.length);
+ let rootNode = hs.executeQuery(query, options).root;
+ rootNode.containerOpen = true;
+
+ // should not match item from folder 3
+ Assert.equal(rootNode.childCount, 4);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid);
+ Assert.equal(rootNode.getChild(3).bookmarkGuid, b5.guid);
+
+ rootNode.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_385829.js b/toolkit/components/places/tests/bookmarks/test_385829.js
new file mode 100644
index 0000000000..63beee5f30
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_385829.js
@@ -0,0 +1,182 @@
+/* -*- 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/. */
+
+add_task(function* search_bookmark_by_lastModified_dateDated() {
+ // test search on folder with various sorts and max results
+ // see bug #385829 for more details
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bug 385829 test"
+ });
+
+ let now = new Date();
+ // ensure some unique values for date added and last modified
+ // for date added: b1 < b2 < b3 < b4
+ // for last modified: b1 > b2 > b3 > b4
+ let b1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ url: "http://a1.com/",
+ title: "1 title",
+ dateAdded: new Date(now.getTime() + 1000)
+ });
+ let b2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ url: "http://a2.com/",
+ title: "2 title",
+ dateAdded: new Date(now.getTime() + 2000)
+ });
+ let b3 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ url: "http://a3.com/",
+ title: "3 title",
+ dateAdded: new Date(now.getTime() + 3000)
+ });
+ let b4 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ url: "http://a4.com/",
+ title: "4 title",
+ dateAdded: new Date(now.getTime() + 4000)
+ });
+
+ // make sure lastModified is larger than dateAdded
+ let modifiedTime = new Date(now.getTime() + 5000);
+ yield PlacesUtils.bookmarks.update({
+ guid: b1.guid,
+ lastModified: new Date(modifiedTime.getTime() + 4000)
+ });
+ yield PlacesUtils.bookmarks.update({
+ guid: b2.guid,
+ lastModified: new Date(modifiedTime.getTime() + 3000)
+ });
+ yield PlacesUtils.bookmarks.update({
+ guid: b3.guid,
+ lastModified: new Date(modifiedTime.getTime() + 2000)
+ });
+ yield PlacesUtils.bookmarks.update({
+ guid: b4.guid,
+ lastModified: new Date(modifiedTime.getTime() + 1000)
+ });
+
+ let hs = PlacesUtils.history;
+ let options = hs.getNewQueryOptions();
+ let query = hs.getNewQuery();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ options.maxResults = 3;
+ let folderIds = [];
+ folderIds.push(yield PlacesUtils.promiseItemId(folder.guid));
+ query.setFolders(folderIds, 1);
+
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+
+ // test SORT_BY_DATEADDED_ASCENDING (live update)
+ result.sortingMode = options.SORT_BY_DATEADDED_ASCENDING;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid);
+ Assert.ok(rootNode.getChild(0).dateAdded <
+ rootNode.getChild(1).dateAdded);
+ Assert.ok(rootNode.getChild(1).dateAdded <
+ rootNode.getChild(2).dateAdded);
+
+ // test SORT_BY_DATEADDED_DESCENDING (live update)
+ result.sortingMode = options.SORT_BY_DATEADDED_DESCENDING;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b3.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b1.guid);
+ Assert.ok(rootNode.getChild(0).dateAdded >
+ rootNode.getChild(1).dateAdded);
+ Assert.ok(rootNode.getChild(1).dateAdded >
+ rootNode.getChild(2).dateAdded);
+
+ // test SORT_BY_LASTMODIFIED_ASCENDING (live update)
+ result.sortingMode = options.SORT_BY_LASTMODIFIED_ASCENDING;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b3.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b1.guid);
+ Assert.ok(rootNode.getChild(0).lastModified <
+ rootNode.getChild(1).lastModified);
+ Assert.ok(rootNode.getChild(1).lastModified <
+ rootNode.getChild(2).lastModified);
+
+ // test SORT_BY_LASTMODIFIED_DESCENDING (live update)
+ result.sortingMode = options.SORT_BY_LASTMODIFIED_DESCENDING;
+
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid);
+ Assert.ok(rootNode.getChild(0).lastModified >
+ rootNode.getChild(1).lastModified);
+ Assert.ok(rootNode.getChild(1).lastModified >
+ rootNode.getChild(2).lastModified);
+ rootNode.containerOpen = false;
+
+ // test SORT_BY_DATEADDED_ASCENDING
+ options.sortingMode = options.SORT_BY_DATEADDED_ASCENDING;
+ result = hs.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid);
+ Assert.ok(rootNode.getChild(0).dateAdded <
+ rootNode.getChild(1).dateAdded);
+ Assert.ok(rootNode.getChild(1).dateAdded <
+ rootNode.getChild(2).dateAdded);
+ rootNode.containerOpen = false;
+
+ // test SORT_BY_DATEADDED_DESCENDING
+ options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING;
+ result = hs.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b4.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b3.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b2.guid);
+ Assert.ok(rootNode.getChild(0).dateAdded >
+ rootNode.getChild(1).dateAdded);
+ Assert.ok(rootNode.getChild(1).dateAdded >
+ rootNode.getChild(2).dateAdded);
+ rootNode.containerOpen = false;
+
+ // test SORT_BY_LASTMODIFIED_ASCENDING
+ options.sortingMode = options.SORT_BY_LASTMODIFIED_ASCENDING;
+ result = hs.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b4.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b3.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b2.guid);
+ Assert.ok(rootNode.getChild(0).lastModified <
+ rootNode.getChild(1).lastModified);
+ Assert.ok(rootNode.getChild(1).lastModified <
+ rootNode.getChild(2).lastModified);
+ rootNode.containerOpen = false;
+
+ // test SORT_BY_LASTMODIFIED_DESCENDING
+ options.sortingMode = options.SORT_BY_LASTMODIFIED_DESCENDING;
+ result = hs.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ Assert.equal(rootNode.childCount, 3);
+ Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid);
+ Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid);
+ Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid);
+ Assert.ok(rootNode.getChild(0).lastModified >
+ rootNode.getChild(1).lastModified);
+ Assert.ok(rootNode.getChild(1).lastModified >
+ rootNode.getChild(2).lastModified);
+ rootNode.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_388695.js b/toolkit/components/places/tests/bookmarks/test_388695.js
new file mode 100644
index 0000000000..4e313c52f0
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_388695.js
@@ -0,0 +1,52 @@
+/* -*- 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/. */
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+} catch (ex) {
+ do_throw("Could not get nav-bookmarks-service\n");
+}
+
+var gTestRoot;
+var gURI;
+var gItemId1;
+var gItemId2;
+
+// main
+function run_test() {
+ gURI = uri("http://foo.tld.com/");
+ gTestRoot = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX);
+
+ // test getBookmarkIdsForURI
+ // getBookmarkIdsForURI sorts by the most recently added/modified (descending)
+ //
+ // we cannot rely on dateAdded growing when doing so in a simple iteration,
+ // see PR_Now() documentation
+ do_test_pending();
+
+ gItemId1 = bmsvc.insertBookmark(gTestRoot, gURI, bmsvc.DEFAULT_INDEX, "");
+ do_timeout(100, phase2);
+}
+
+function phase2() {
+ gItemId2 = bmsvc.insertBookmark(gTestRoot, gURI, bmsvc.DEFAULT_INDEX, "");
+ var b = bmsvc.getBookmarkIdsForURI(gURI);
+ do_check_eq(b[0], gItemId2);
+ do_check_eq(b[1], gItemId1);
+ do_timeout(100, phase3);
+}
+
+function phase3() {
+ // trigger last modified change
+ bmsvc.setItemTitle(gItemId1, "");
+ var b = bmsvc.getBookmarkIdsForURI(gURI);
+ do_check_eq(b[0], gItemId1);
+ do_check_eq(b[1], gItemId2);
+ do_test_finished();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_393498.js b/toolkit/components/places/tests/bookmarks/test_393498.js
new file mode 100644
index 0000000000..601f77a0a0
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_393498.js
@@ -0,0 +1,102 @@
+/* -*- 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 observer = {
+ __proto__: NavBookmarkObserver.prototype,
+
+ onItemAdded: function (id, folder, index) {
+ this._itemAddedId = id;
+ this._itemAddedParent = folder;
+ this._itemAddedIndex = index;
+ },
+ onItemChanged: function (id, property, isAnnotationProperty, value) {
+ this._itemChangedId = id;
+ this._itemChangedProperty = property;
+ this._itemChanged_isAnnotationProperty = isAnnotationProperty;
+ this._itemChangedValue = value;
+ }
+};
+PlacesUtils.bookmarks.addObserver(observer, false);
+
+do_register_cleanup(function () {
+ PlacesUtils.bookmarks.removeObserver(observer);
+});
+
+function run_test() {
+ // We set times in the past to workaround a timing bug due to virtual
+ // machines and the skew between PR_Now() and Date.now(), see bug 427142 and
+ // bug 858377 for details.
+ const PAST_PRTIME = (Date.now() - 86400000) * 1000;
+
+ // Insert a new bookmark.
+ let testFolder = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.placesRootId, "test Folder",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let bookmarkId = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri("http://google.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "");
+
+ // Sanity check.
+ do_check_true(observer.itemChangedProperty === undefined);
+
+ // Set dateAdded in the past and verify the bookmarks cache.
+ PlacesUtils.bookmarks.setItemDateAdded(bookmarkId, PAST_PRTIME);
+ do_check_eq(observer._itemChangedProperty, "dateAdded");
+ do_check_eq(observer._itemChangedValue, PAST_PRTIME);
+ let dateAdded = PlacesUtils.bookmarks.getItemDateAdded(bookmarkId);
+ do_check_eq(dateAdded, PAST_PRTIME);
+
+ // After just inserting, modified should be the same as dateAdded.
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId), dateAdded);
+
+ // Set lastModified in the past and verify the bookmarks cache.
+ PlacesUtils.bookmarks.setItemLastModified(bookmarkId, PAST_PRTIME);
+ do_check_eq(observer._itemChangedProperty, "lastModified");
+ do_check_eq(observer._itemChangedValue, PAST_PRTIME);
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId),
+ PAST_PRTIME);
+
+ // Set bookmark title
+ PlacesUtils.bookmarks.setItemTitle(bookmarkId, "Google");
+
+ // Test notifications.
+ do_check_eq(observer._itemChangedId, bookmarkId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, "Google");
+
+ // Check lastModified has been updated.
+ is_time_ordered(PAST_PRTIME,
+ PlacesUtils.bookmarks.getItemLastModified(bookmarkId));
+
+ // Check that node properties are updated.
+ let root = PlacesUtils.getFolderContents(testFolder).root;
+ do_check_eq(root.childCount, 1);
+ let childNode = root.getChild(0);
+
+ // confirm current dates match node properties
+ do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(bookmarkId),
+ childNode.dateAdded);
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId),
+ childNode.lastModified);
+
+ // Test live update of lastModified when setting title.
+ PlacesUtils.bookmarks.setItemLastModified(bookmarkId, PAST_PRTIME);
+ PlacesUtils.bookmarks.setItemTitle(bookmarkId, "Google");
+
+ // Check lastModified has been updated.
+ is_time_ordered(PAST_PRTIME, childNode.lastModified);
+ // Test that node value matches db value.
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(bookmarkId),
+ childNode.lastModified);
+
+ // Test live update of the exposed date apis.
+ PlacesUtils.bookmarks.setItemDateAdded(bookmarkId, PAST_PRTIME);
+ do_check_eq(childNode.dateAdded, PAST_PRTIME);
+ PlacesUtils.bookmarks.setItemLastModified(bookmarkId, PAST_PRTIME);
+ do_check_eq(childNode.lastModified, PAST_PRTIME);
+
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_395101.js b/toolkit/components/places/tests/bookmarks/test_395101.js
new file mode 100644
index 0000000000..a507e73615
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_395101.js
@@ -0,0 +1,87 @@
+/* -*- 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/. */
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Ci.nsINavBookmarksService);
+} catch (ex) {
+ do_throw("Could not get nav-bookmarks-service\n");
+}
+
+// Get history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService);
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+// get bookmarks root id
+var root = bmsvc.bookmarksMenuFolder;
+
+// main
+function run_test() {
+ // test searching for tagged bookmarks
+
+ // test folder
+ var folder = bmsvc.createFolder(root, "bug 395101 test", bmsvc.DEFAULT_INDEX);
+
+ // create a bookmark
+ var testURI = uri("http://a1.com");
+ var b1 = bmsvc.insertBookmark(folder, testURI,
+ bmsvc.DEFAULT_INDEX, "1 title");
+
+ // tag the bookmarked URI
+ tagssvc.tagURI(testURI, ["elephant", "walrus", "giraffe", "turkey", "hiPPo", "BABOON", "alf"]);
+
+ // search for the bookmark, using a tag
+ var query = histsvc.getNewQuery();
+ query.searchTerms = "elephant";
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ query.setFolders([folder], 1);
+
+ var result = histsvc.executeQuery(query, options);
+ var rootNode = result.root;
+ rootNode.containerOpen = true;
+
+ do_check_eq(rootNode.childCount, 1);
+ do_check_eq(rootNode.getChild(0).itemId, b1);
+ rootNode.containerOpen = false;
+
+ // partial matches are okay
+ query.searchTerms = "wal";
+ result = histsvc.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ do_check_eq(rootNode.childCount, 1);
+ rootNode.containerOpen = false;
+
+ // case insensitive search term
+ query.searchTerms = "WALRUS";
+ result = histsvc.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ do_check_eq(rootNode.childCount, 1);
+ do_check_eq(rootNode.getChild(0).itemId, b1);
+ rootNode.containerOpen = false;
+
+ // case insensitive tag
+ query.searchTerms = "baboon";
+ result = histsvc.executeQuery(query, options);
+ rootNode = result.root;
+ rootNode.containerOpen = true;
+ do_check_eq(rootNode.childCount, 1);
+ do_check_eq(rootNode.getChild(0).itemId, b1);
+ rootNode.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_395593.js b/toolkit/components/places/tests/bookmarks/test_395593.js
new file mode 100644
index 0000000000..46d8f5b80e
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_395593.js
@@ -0,0 +1,69 @@
+/* -*- 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 bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+function check_queries_results(aQueries, aOptions, aExpectedItemIds) {
+ var result = hs.executeQueries(aQueries, aQueries.length, aOptions);
+ var root = result.root;
+ root.containerOpen = true;
+
+ // Dump found nodes.
+ for (let i = 0; i < root.childCount; i++) {
+ dump("nodes[" + i + "]: " + root.getChild(0).title + "\n");
+ }
+
+ do_check_eq(root.childCount, aExpectedItemIds.length);
+ for (let i = 0; i < root.childCount; i++) {
+ do_check_eq(root.getChild(i).itemId, aExpectedItemIds[i]);
+ }
+
+ root.containerOpen = false;
+}
+
+// main
+function run_test() {
+ var id1 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"),
+ bs.DEFAULT_INDEX, "123 0");
+ var id2 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"),
+ bs.DEFAULT_INDEX, "456");
+ var id3 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"),
+ bs.DEFAULT_INDEX, "123 456");
+ var id4 = bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://foo.tld"),
+ bs.DEFAULT_INDEX, "789 456");
+
+ /**
+ * All of the query objects are ORed together. Within a query, all the terms
+ * are ANDed together. See nsINavHistory.idl.
+ */
+ var queries = [];
+ queries.push(hs.getNewQuery());
+ queries.push(hs.getNewQuery());
+ var options = hs.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ // Test 1
+ dump("Test searching for 123 OR 789\n");
+ queries[0].searchTerms = "123";
+ queries[1].searchTerms = "789";
+ check_queries_results(queries, options, [id1, id3, id4]);
+
+ // Test 2
+ dump("Test searching for 123 OR 456\n");
+ queries[0].searchTerms = "123";
+ queries[1].searchTerms = "456";
+ check_queries_results(queries, options, [id1, id2, id3, id4]);
+
+ // Test 3
+ dump("Test searching for 00 OR 789\n");
+ queries[0].searchTerms = "00";
+ queries[1].searchTerms = "789";
+ check_queries_results(queries, options, [id4]);
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
new file mode 100644
index 0000000000..e317cc2e9e
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
@@ -0,0 +1,221 @@
+/* -*- 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 tests = [];
+
+/*
+
+Backup/restore tests example:
+
+var myTest = {
+ populate: function () { ... add bookmarks ... },
+ validate: function () { ... query for your bookmarks ... }
+}
+
+this.push(myTest);
+
+*/
+
+/*
+
+test summary:
+- create folders with content
+- create a query bookmark for those folders
+- backs up bookmarks
+- restores bookmarks
+- confirms that the query has the new ids for the same folders
+
+scenarios:
+- 1 folder (folder shortcut)
+- n folders (single query)
+- n folders (multiple queries)
+
+*/
+
+const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX;
+
+var test = {
+ _testRootId: null,
+ _testRootTitle: "test root",
+ _folderIds: [],
+ _bookmarkURIs: [],
+ _count: 3,
+
+ populate: function populate() {
+ // folder to hold this test
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.toolbarFolderId);
+ this._testRootId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
+ this._testRootTitle, DEFAULT_INDEX);
+
+ // create test folders each with a bookmark
+ for (var i = 0; i < this._count; i++) {
+ var folderId =
+ PlacesUtils.bookmarks.createFolder(this._testRootId, "folder" + i, DEFAULT_INDEX);
+ this._folderIds.push(folderId)
+
+ var bookmarkURI = uri("http://" + i);
+ PlacesUtils.bookmarks.insertBookmark(folderId, bookmarkURI,
+ DEFAULT_INDEX, "bookmark" + i);
+ this._bookmarkURIs.push(bookmarkURI);
+ }
+
+ // create a query URI with 1 folder (ie: folder shortcut)
+ this._queryURI1 = uri("place:folder=" + this._folderIds[0] + "&queryType=1");
+ this._queryTitle1 = "query1";
+ PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI1,
+ DEFAULT_INDEX, this._queryTitle1);
+
+ // create a query URI with _count folders
+ this._queryURI2 = uri("place:folder=" + this._folderIds.join("&folder=") + "&queryType=1");
+ this._queryTitle2 = "query2";
+ PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI2,
+ DEFAULT_INDEX, this._queryTitle2);
+
+ // create a query URI with _count queries (each with a folder)
+ // first get a query object for each folder
+ var queries = this._folderIds.map(function(aFolderId) {
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ return query;
+ });
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ this._queryURI3 =
+ uri(PlacesUtils.history.queriesToQueryString(queries, queries.length, options));
+ this._queryTitle3 = "query3";
+ PlacesUtils.bookmarks.insertBookmark(this._testRootId, this._queryURI3,
+ DEFAULT_INDEX, this._queryTitle3);
+ },
+
+ clean: function () {},
+
+ validate: function validate() {
+ // Throw a wrench in the works by inserting some new bookmarks,
+ // ensuring folder ids won't be the same, when restoring.
+ for (let i = 0; i < 10; i++) {
+ PlacesUtils.bookmarks.
+ insertBookmark(PlacesUtils.bookmarksMenuFolderId, uri("http://aaaa"+i), DEFAULT_INDEX, "");
+ }
+
+ var toolbar =
+ PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId,
+ false, true).root;
+ do_check_true(toolbar.childCount, 1);
+
+ var folderNode = toolbar.getChild(0);
+ do_check_eq(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ do_check_eq(folderNode.title, this._testRootTitle);
+ folderNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
+ folderNode.containerOpen = true;
+
+ // |_count| folders + the query node
+ do_check_eq(folderNode.childCount, this._count+3);
+
+ for (let i = 0; i < this._count; i++) {
+ var subFolder = folderNode.getChild(i);
+ do_check_eq(subFolder.title, "folder"+i);
+ subFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ subFolder.containerOpen = true;
+ do_check_eq(subFolder.childCount, 1);
+ var child = subFolder.getChild(0);
+ do_check_eq(child.title, "bookmark"+i);
+ do_check_true(uri(child.uri).equals(uri("http://" + i)))
+ }
+
+ // validate folder shortcut
+ this.validateQueryNode1(folderNode.getChild(this._count));
+
+ // validate folders query
+ this.validateQueryNode2(folderNode.getChild(this._count + 1));
+
+ // validate multiple queries query
+ this.validateQueryNode3(folderNode.getChild(this._count + 2));
+
+ // clean up
+ folderNode.containerOpen = false;
+ toolbar.containerOpen = false;
+ },
+
+ validateQueryNode1: function validateQueryNode1(aNode) {
+ do_check_eq(aNode.title, this._queryTitle1);
+ do_check_true(PlacesUtils.nodeIsFolder(aNode));
+
+ aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ aNode.containerOpen = true;
+ do_check_eq(aNode.childCount, 1);
+ var child = aNode.getChild(0);
+ do_check_true(uri(child.uri).equals(uri("http://0")))
+ do_check_eq(child.title, "bookmark0")
+ aNode.containerOpen = false;
+ },
+
+ validateQueryNode2: function validateQueryNode2(aNode) {
+ do_check_eq(aNode.title, this._queryTitle2);
+ do_check_true(PlacesUtils.nodeIsQuery(aNode));
+
+ aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ aNode.containerOpen = true;
+ do_check_eq(aNode.childCount, this._count);
+ for (var i = 0; i < aNode.childCount; i++) {
+ var child = aNode.getChild(i);
+ do_check_true(uri(child.uri).equals(uri("http://" + i)))
+ do_check_eq(child.title, "bookmark" + i)
+ }
+ aNode.containerOpen = false;
+ },
+
+ validateQueryNode3: function validateQueryNode3(aNode) {
+ do_check_eq(aNode.title, this._queryTitle3);
+ do_check_true(PlacesUtils.nodeIsQuery(aNode));
+
+ aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ aNode.containerOpen = true;
+ do_check_eq(aNode.childCount, this._count);
+ for (var i = 0; i < aNode.childCount; i++) {
+ var child = aNode.getChild(i);
+ do_check_true(uri(child.uri).equals(uri("http://" + i)))
+ do_check_eq(child.title, "bookmark" + i)
+ }
+ aNode.containerOpen = false;
+ }
+}
+tests.push(test);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ // make json file
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+
+ // populate db
+ tests.forEach(function(aTest) {
+ aTest.populate();
+ // sanity
+ aTest.validate();
+ });
+
+ // export json to file
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+
+ // clean
+ tests.forEach(function(aTest) {
+ aTest.clean();
+ });
+
+ // restore json file
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+
+ // validate
+ tests.forEach(function(aTest) {
+ aTest.validate();
+ });
+
+ // clean up
+ yield OS.File.remove(jsonFile);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
new file mode 100644
index 0000000000..8584968561
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
@@ -0,0 +1,141 @@
+/* -*- 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/. */
+
+const EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup";
+// Menu, Toolbar, Unsorted, Tags, Mobile
+const PLACES_ROOTS_COUNT = 5;
+var tests = [];
+
+/*
+
+Backup/restore tests example:
+
+var myTest = {
+ populate: function () { ... add bookmarks ... },
+ validate: function () { ... query for your bookmarks ... }
+}
+
+this.push(myTest);
+
+*/
+
+var test = {
+ populate: function populate() {
+ // check initial size
+ var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
+ false, false).root;
+ do_check_eq(rootNode.childCount, PLACES_ROOTS_COUNT );
+ rootNode.containerOpen = false;
+
+ var idx = PlacesUtils.bookmarks.DEFAULT_INDEX;
+
+ // create a root to be restore
+ this._restoreRootTitle = "restore root";
+ var restoreRootId = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.placesRootId,
+ this._restoreRootTitle, idx);
+ // add a test bookmark
+ this._restoreRootURI = uri("http://restore.uri");
+ PlacesUtils.bookmarks.insertBookmark(restoreRootId, this._restoreRootURI,
+ idx, "restore uri");
+ // add a test bookmark to be exclude
+ this._restoreRootExcludeURI = uri("http://exclude.uri");
+ var exItemId = PlacesUtils.bookmarks
+ .insertBookmark(restoreRootId,
+ this._restoreRootExcludeURI,
+ idx, "exclude uri");
+ // Annotate the bookmark for exclusion.
+ PlacesUtils.annotations.setItemAnnotation(exItemId,
+ EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // create a root to be exclude
+ this._excludeRootTitle = "exclude root";
+ this._excludeRootId = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.placesRootId,
+ this._excludeRootTitle, idx);
+ // Annotate the root for exclusion.
+ PlacesUtils.annotations.setItemAnnotation(this._excludeRootId,
+ EXCLUDE_FROM_BACKUP_ANNO, 1, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ // add a test bookmark exclude by exclusion of its parent
+ PlacesUtils.bookmarks.insertBookmark(this._excludeRootId,
+ this._restoreRootExcludeURI,
+ idx, "exclude uri");
+ },
+
+ validate: function validate(aEmptyBookmarks) {
+ var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
+ false, false).root;
+
+ if (!aEmptyBookmarks) {
+ // since restore does not remove backup exclude items both
+ // roots should still exist.
+ do_check_eq(rootNode.childCount, PLACES_ROOTS_COUNT + 2);
+ // open exclude root and check it still contains one item
+ var restoreRootIndex = PLACES_ROOTS_COUNT;
+ var excludeRootIndex = PLACES_ROOTS_COUNT+1;
+ var excludeRootNode = rootNode.getChild(excludeRootIndex);
+ do_check_eq(this._excludeRootTitle, excludeRootNode.title);
+ excludeRootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
+ excludeRootNode.containerOpen = true;
+ do_check_eq(excludeRootNode.childCount, 1);
+ var excludeRootChildNode = excludeRootNode.getChild(0);
+ do_check_eq(excludeRootChildNode.uri, this._restoreRootExcludeURI.spec);
+ excludeRootNode.containerOpen = false;
+ }
+ else {
+ // exclude root should not exist anymore
+ do_check_eq(rootNode.childCount, PLACES_ROOTS_COUNT + 1);
+ restoreRootIndex = PLACES_ROOTS_COUNT;
+ }
+
+ var restoreRootNode = rootNode.getChild(restoreRootIndex);
+ do_check_eq(this._restoreRootTitle, restoreRootNode.title);
+ restoreRootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
+ restoreRootNode.containerOpen = true;
+ do_check_eq(restoreRootNode.childCount, 1);
+ var restoreRootChildNode = restoreRootNode.getChild(0);
+ do_check_eq(restoreRootChildNode.uri, this._restoreRootURI.spec);
+ restoreRootNode.containerOpen = false;
+
+ rootNode.containerOpen = false;
+ }
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function*() {
+ // make json file
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+
+ // populate db
+ test.populate();
+
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+
+ // restore json file
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+
+ // validate without removing all bookmarks
+ // restore do not remove backup exclude entries
+ test.validate(false);
+
+ // cleanup
+ yield PlacesUtils.bookmarks.eraseEverything();
+ // manually remove the excluded root
+ PlacesUtils.bookmarks.removeItem(test._excludeRootId);
+ // restore json file
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+
+ // validate after a complete bookmarks cleanup
+ test.validate(true);
+
+ // clean up
+ yield OS.File.remove(jsonFile);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
new file mode 100644
index 0000000000..1def75d2da
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -0,0 +1,158 @@
+/* -*- 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 tests = [];
+
+/*
+
+Backup/restore tests example:
+
+var myTest = {
+ populate: function () { ... add bookmarks ... },
+ validate: function () { ... query for your bookmarks ... }
+}
+
+this.push(myTest);
+
+*/
+
+tests.push({
+ excludeItemsFromRestore: [],
+ populate: function populate() {
+ // check initial size
+ var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
+ false, false).root;
+ do_check_eq(rootNode.childCount, 5);
+
+ // create a test root
+ this._folderTitle = "test folder";
+ this._folderId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
+ this._folderTitle,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ do_check_eq(rootNode.childCount, 6);
+
+ // add a tag
+ this._testURI = PlacesUtils._uri("http://test");
+ this._tags = ["a", "b"];
+ PlacesUtils.tagging.tagURI(this._testURI, this._tags);
+
+ // add a child to each root, including our test root
+ this._roots = [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.toolbarFolderId,
+ PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.mobileFolderId,
+ this._folderId];
+ this._roots.forEach(function(aRootId) {
+ // clean slate
+ PlacesUtils.bookmarks.removeFolderChildren(aRootId);
+ // add a test bookmark
+ PlacesUtils.bookmarks.insertBookmark(aRootId, this._testURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "test");
+ }, this);
+
+ // add a folder to exclude from replacing during restore
+ // this will still be present post-restore
+ var excludedFolderId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
+ "excluded",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ do_check_eq(rootNode.childCount, 7);
+ this.excludeItemsFromRestore.push(excludedFolderId);
+
+ // add a test bookmark to it
+ PlacesUtils.bookmarks.insertBookmark(excludedFolderId, this._testURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "test");
+ },
+
+ inbetween: function inbetween() {
+ // add some items that should be removed by the restore
+
+ // add a folder
+ this._litterTitle = "otter";
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
+ this._litterTitle, 0);
+
+ // add some tags
+ PlacesUtils.tagging.tagURI(this._testURI, ["c", "d"]);
+ },
+
+ validate: function validate() {
+ // validate tags restored
+ var tags = PlacesUtils.tagging.getTagsForURI(this._testURI);
+ // also validates that litter tags are gone
+ do_check_eq(this._tags.toString(), tags.toString());
+
+ var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
+ false, false).root;
+
+ // validate litter is gone
+ do_check_neq(rootNode.getChild(0).title, this._litterTitle);
+
+ // test root count is the same
+ do_check_eq(rootNode.childCount, 7);
+
+ var foundTestFolder = 0;
+ for (var i = 0; i < rootNode.childCount; i++) {
+ var node = rootNode.getChild(i);
+
+ do_print("validating " + node.title);
+ if (node.itemId != PlacesUtils.tagsFolderId) {
+ if (node.title == this._folderTitle) {
+ // check the test folder's properties
+ do_check_eq(node.type, node.RESULT_TYPE_FOLDER);
+ do_check_eq(node.title, this._folderTitle);
+ foundTestFolder++;
+ }
+
+ // test contents
+ node.QueryInterface(Ci.nsINavHistoryContainerResultNode).containerOpen = true;
+ do_check_eq(node.childCount, 1);
+ var child = node.getChild(0);
+ do_check_true(PlacesUtils._uri(child.uri).equals(this._testURI));
+
+ // clean up
+ node.containerOpen = false;
+ }
+ }
+ do_check_eq(foundTestFolder, 1);
+ rootNode.containerOpen = false;
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ // make json file
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+
+ // populate db
+ tests.forEach(function(aTest) {
+ aTest.populate();
+ // sanity
+ aTest.validate();
+
+ if (aTest.excludedItemsFromRestore)
+ excludedItemsFromRestore = excludedItems.concat(aTest.excludedItemsFromRestore);
+ });
+
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+
+ tests.forEach(function(aTest) {
+ aTest.inbetween();
+ });
+
+ // restore json file
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+
+ // validate
+ tests.forEach(function(aTest) {
+ aTest.validate();
+ });
+
+ // clean up
+ yield OS.File.remove(jsonFile);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
new file mode 100644
index 0000000000..7da1146cfc
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
@@ -0,0 +1,91 @@
+/* -*- 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 tests = [];
+
+/*
+
+Backup/restore tests example:
+
+var myTest = {
+ populate: function () { ... add bookmarks ... },
+ validate: function () { ... query for your bookmarks ... }
+}
+
+this.push(myTest);
+
+*/
+
+var quotesTest = {
+ _folderTitle: '"quoted folder"',
+ _folderId: null,
+
+ populate: function () {
+ this._folderId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
+ this._folderTitle,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ },
+
+ clean: function () {
+ PlacesUtils.bookmarks.removeItem(this._folderId);
+ },
+
+ validate: function () {
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ var result = PlacesUtils.history.executeQuery(query, PlacesUtils.history.getNewQueryOptions());
+
+ var toolbar = result.root;
+ toolbar.containerOpen = true;
+
+ // test for our quoted folder
+ do_check_true(toolbar.childCount, 1);
+ var folderNode = toolbar.getChild(0);
+ do_check_eq(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ do_check_eq(folderNode.title, this._folderTitle);
+
+ // clean up
+ toolbar.containerOpen = false;
+ }
+}
+tests.push(quotesTest);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ // make json file
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+
+ // populate db
+ tests.forEach(function(aTest) {
+ aTest.populate();
+ // sanity
+ aTest.validate();
+ });
+
+ // export json to file
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+
+ // clean
+ tests.forEach(function(aTest) {
+ aTest.clean();
+ });
+
+ // restore json file
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+
+ // validate
+ tests.forEach(function(aTest) {
+ aTest.validate();
+ });
+
+ // clean up
+ yield OS.File.remove(jsonFile);
+
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_448584.js b/toolkit/components/places/tests/bookmarks/test_448584.js
new file mode 100644
index 0000000000..6e58bd83a9
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_448584.js
@@ -0,0 +1,113 @@
+/* -*- 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 tests = [];
+
+// Get database connection
+try {
+ var mDBConn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+}
+catch (ex) {
+ do_throw("Could not get database connection\n");
+}
+
+/*
+ This test is:
+ - don't try to add invalid uri nodes to a JSON backup
+*/
+
+var invalidURITest = {
+ _itemTitle: "invalid uri",
+ _itemUrl: "http://test.mozilla.org/",
+ _itemId: null,
+
+ populate: function () {
+ // add a valid bookmark
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri(this._itemUrl),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ this._itemTitle);
+ // this bookmark will go corrupt
+ this._itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri(this._itemUrl),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ this._itemTitle);
+ },
+
+ clean: function () {
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ },
+
+ validate: function (aExpectValidItemsCount) {
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ var options = PlacesUtils.history.getNewQueryOptions();
+ var result = PlacesUtils.history.executeQuery(query, options);
+
+ var toolbar = result.root;
+ toolbar.containerOpen = true;
+
+ // test for our bookmark
+ do_check_eq(toolbar.childCount, aExpectValidItemsCount);
+ for (var i = 0; i < toolbar.childCount; i++) {
+ var folderNode = toolbar.getChild(0);
+ do_check_eq(folderNode.type, folderNode.RESULT_TYPE_URI);
+ do_check_eq(folderNode.title, this._itemTitle);
+ }
+
+ // clean up
+ toolbar.containerOpen = false;
+ }
+}
+tests.push(invalidURITest);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function*() {
+ // make json file
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+
+ // populate db
+ tests.forEach(function(aTest) {
+ aTest.populate();
+ // sanity
+ aTest.validate(2);
+ // Something in the code went wrong and we finish up losing the place, so
+ // the bookmark uri becomes null.
+ var sql = "UPDATE moz_bookmarks SET fk = 1337 WHERE id = ?1";
+ var stmt = mDBConn.createStatement(sql);
+ stmt.bindByIndex(0, aTest._itemId);
+ try {
+ stmt.execute();
+ } finally {
+ stmt.finalize();
+ }
+ });
+
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+
+ // clean
+ tests.forEach(function(aTest) {
+ aTest.clean();
+ });
+
+ // restore json file
+ try {
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+ } catch (ex) { do_throw("couldn't import the exported file: " + ex); }
+
+ // validate
+ tests.forEach(function(aTest) {
+ aTest.validate(1);
+ });
+
+ // clean up
+ yield OS.File.remove(jsonFile);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_458683.js b/toolkit/components/places/tests/bookmarks/test_458683.js
new file mode 100644
index 0000000000..c3722aab50
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_458683.js
@@ -0,0 +1,131 @@
+/* -*- 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 tests = [];
+
+// Get database connection
+try {
+ var mDBConn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+}
+catch (ex) {
+ do_throw("Could not get database connection\n");
+}
+
+/*
+ This test is:
+ - don't block while doing backup and restore if tag containers contain
+ bogus items (separators, folders)
+*/
+
+var invalidTagChildTest = {
+ _itemTitle: "invalid uri",
+ _itemUrl: "http://test.mozilla.org/",
+ _itemId: -1,
+ _tag: "testTag",
+ _tagItemId: -1,
+
+ populate: function () {
+ // add a valid bookmark
+ this._itemId = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.toolbarFolderId,
+ PlacesUtils._uri(this._itemUrl),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ this._itemTitle);
+
+ // create a tag
+ PlacesUtils.tagging.tagURI(PlacesUtils._uri(this._itemUrl), [this._tag]);
+ // get tag folder id
+ var options = PlacesUtils.history.getNewQueryOptions();
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.tagsFolder], 1);
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var tagRoot = result.root;
+ tagRoot.containerOpen = true;
+ do_check_eq(tagRoot.childCount, 1);
+ var tagNode = tagRoot.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ this._tagItemId = tagNode.itemId;
+ tagRoot.containerOpen = false;
+
+ // add a separator and a folder inside tag folder
+ PlacesUtils.bookmarks.insertSeparator(this._tagItemId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.createFolder(this._tagItemId,
+ "test folder",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ // add a separator and a folder inside tag root
+ PlacesUtils.bookmarks.insertSeparator(PlacesUtils.bookmarks.tagsFolder,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.bookmarks.tagsFolder,
+ "test tags root folder",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ },
+
+ clean: function () {
+ PlacesUtils.tagging.untagURI(PlacesUtils._uri(this._itemUrl), [this._tag]);
+ PlacesUtils.bookmarks.removeItem(this._itemId);
+ },
+
+ validate: function () {
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ var options = PlacesUtils.history.getNewQueryOptions();
+ var result = PlacesUtils.history.executeQuery(query, options);
+
+ var toolbar = result.root;
+ toolbar.containerOpen = true;
+
+ // test for our bookmark
+ do_check_eq(toolbar.childCount, 1);
+ for (var i = 0; i < toolbar.childCount; i++) {
+ var folderNode = toolbar.getChild(0);
+ do_check_eq(folderNode.type, folderNode.RESULT_TYPE_URI);
+ do_check_eq(folderNode.title, this._itemTitle);
+ }
+ toolbar.containerOpen = false;
+
+ // test for our tag
+ var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(this._itemUrl));
+ do_check_eq(tags.length, 1);
+ do_check_eq(tags[0], this._tag);
+ }
+}
+tests.push(invalidTagChildTest);
+
+function run_test() {
+ run_next_test()
+}
+
+add_task(function* () {
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+
+ // populate db
+ tests.forEach(function(aTest) {
+ aTest.populate();
+ // sanity
+ aTest.validate();
+ });
+
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+
+ // clean
+ tests.forEach(function(aTest) {
+ aTest.clean();
+ });
+
+ // restore json file
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+
+ // validate
+ tests.forEach(function(aTest) {
+ aTest.validate();
+ });
+
+ // clean up
+ yield OS.File.remove(jsonFile);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
new file mode 100644
index 0000000000..3ce0e6ad78
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
@@ -0,0 +1,124 @@
+/* -*- 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/. */
+
+// Since PlacesBackups.getbackupFiles() is a lazy getter, these tests must
+// run in the given order, to avoid making it out-of-sync.
+
+add_task(function* check_max_backups_is_respected() {
+ // Get bookmarkBackups directory
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+
+ // Create 2 json dummy backups in the past.
+ let oldJsonPath = OS.Path.join(backupFolder, "bookmarks-2008-01-01.json");
+ let oldJsonFile = yield OS.File.open(oldJsonPath, { truncate: true });
+ oldJsonFile.close();
+ do_check_true(yield OS.File.exists(oldJsonPath));
+
+ let jsonPath = OS.Path.join(backupFolder, "bookmarks-2008-01-31.json");
+ let jsonFile = yield OS.File.open(jsonPath, { truncate: true });
+ jsonFile.close();
+ do_check_true(yield OS.File.exists(jsonPath));
+
+ // Export bookmarks to JSON.
+ // Allow 2 backups, the older one should be removed.
+ yield PlacesBackups.create(2);
+
+ let count = 0;
+ let lastBackupPath = null;
+ let iterator = new OS.File.DirectoryIterator(backupFolder);
+ try {
+ yield iterator.forEach(aEntry => {
+ count++;
+ if (PlacesBackups.filenamesRegex.test(aEntry.name))
+ lastBackupPath = aEntry.path;
+ });
+ } finally {
+ iterator.close();
+ }
+
+ do_check_eq(count, 2);
+ do_check_neq(lastBackupPath, null);
+ do_check_false(yield OS.File.exists(oldJsonPath));
+ do_check_true(yield OS.File.exists(jsonPath));
+});
+
+add_task(function* check_max_backups_greater_than_backups() {
+ // Get bookmarkBackups directory
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+
+ // Export bookmarks to JSON.
+ // Allow 3 backups, none should be removed.
+ yield PlacesBackups.create(3);
+
+ let count = 0;
+ let lastBackupPath = null;
+ let iterator = new OS.File.DirectoryIterator(backupFolder);
+ try {
+ yield iterator.forEach(aEntry => {
+ count++;
+ if (PlacesBackups.filenamesRegex.test(aEntry.name))
+ lastBackupPath = aEntry.path;
+ });
+ } finally {
+ iterator.close();
+ }
+ do_check_eq(count, 2);
+ do_check_neq(lastBackupPath, null);
+});
+
+add_task(function* check_max_backups_null() {
+ // Get bookmarkBackups directory
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+
+ // Export bookmarks to JSON.
+ // Allow infinite backups, none should be removed, a new one is not created
+ // since one for today already exists.
+ yield PlacesBackups.create(null);
+
+ let count = 0;
+ let lastBackupPath = null;
+ let iterator = new OS.File.DirectoryIterator(backupFolder);
+ try {
+ yield iterator.forEach(aEntry => {
+ count++;
+ if (PlacesBackups.filenamesRegex.test(aEntry.name))
+ lastBackupPath = aEntry.path;
+ });
+ } finally {
+ iterator.close();
+ }
+ do_check_eq(count, 2);
+ do_check_neq(lastBackupPath, null);
+});
+
+add_task(function* check_max_backups_undefined() {
+ // Get bookmarkBackups directory
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+
+ // Export bookmarks to JSON.
+ // Allow infinite backups, none should be removed, a new one is not created
+ // since one for today already exists.
+ yield PlacesBackups.create();
+
+ let count = 0;
+ let lastBackupPath = null;
+ let iterator = new OS.File.DirectoryIterator(backupFolder);
+ try {
+ yield iterator.forEach(aEntry => {
+ count++;
+ if (PlacesBackups.filenamesRegex.test(aEntry.name))
+ lastBackupPath = aEntry.path;
+ });
+ } finally {
+ iterator.close();
+ }
+ do_check_eq(count, 2);
+ do_check_neq(lastBackupPath, null);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
new file mode 100644
index 0000000000..116352666c
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
@@ -0,0 +1,56 @@
+/* -*- 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/. */
+
+function run_test() {
+ do_test_pending();
+
+ Task.spawn(function*() {
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+ let bookmarksBackupDir = new FileUtils.File(backupFolder);
+ // Remove all files from backups folder.
+ let files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.getNext().QueryInterface(Ci.nsIFile);
+ entry.remove(false);
+ }
+
+ // Create a json dummy backup in the future.
+ let dateObj = new Date();
+ dateObj.setYear(dateObj.getFullYear() + 1);
+ let name = PlacesBackups.getFilenameForDate(dateObj);
+ do_check_eq(name, "bookmarks-" + PlacesBackups.toISODateString(dateObj) + ".json");
+ files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.getNext().QueryInterface(Ci.nsIFile);
+ if (PlacesBackups.filenamesRegex.test(entry.leafName))
+ entry.remove(false);
+ }
+
+ let futureBackupFile = bookmarksBackupDir.clone();
+ futureBackupFile.append(name);
+ futureBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0o600);
+ do_check_true(futureBackupFile.exists());
+
+ do_check_eq((yield PlacesBackups.getBackupFiles()).length, 0);
+
+ yield PlacesBackups.create();
+ // Check that a backup for today has been created.
+ do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1);
+ let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+ do_check_neq(mostRecentBackupFile, null);
+ do_check_true(PlacesBackups.filenamesRegex.test(OS.Path.basename(mostRecentBackupFile)));
+
+ // Check that future backup has been removed.
+ do_check_false(futureBackupFile.exists());
+
+ // Cleanup.
+ mostRecentBackupFile = new FileUtils.File(mostRecentBackupFile);
+ mostRecentBackupFile.remove(false);
+ do_check_false(mostRecentBackupFile.exists());
+
+ do_test_finished()
+ });
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_675416.js b/toolkit/components/places/tests/bookmarks/test_675416.js
new file mode 100644
index 0000000000..08b1c36205
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_675416.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ /**
+ * Requests information to the service, so that bookmark's data is cached.
+ * @param aItemId
+ * Id of the bookmark to be cached.
+ */
+ function forceBookmarkCaching(aItemId) {
+ PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
+ }
+
+ let observer = {
+ onBeginUpdateBatch: () => forceBookmarkCaching(itemId1),
+ onEndUpdateBatch: () => forceBookmarkCaching(itemId1),
+ onItemAdded: forceBookmarkCaching,
+ onItemChanged: forceBookmarkCaching,
+ onItemMoved: forceBookmarkCaching,
+ onItemRemoved: function(id) {
+ try {
+ forceBookmarkCaching(id);
+ do_throw("trying to fetch a removed bookmark should throw");
+ } catch (ex) {}
+ },
+ onItemVisited: forceBookmarkCaching,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+ };
+ PlacesUtils.bookmarks.addObserver(observer, false);
+
+ let folderId1 = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.bookmarksMenuFolderId,
+ "Bookmarks",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let itemId1 = PlacesUtils.bookmarks
+ .insertBookmark(folderId1,
+ NetUtil.newURI("http:/www.wired.com/wiredscience"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Wired Science");
+
+ PlacesUtils.bookmarks.removeItem(folderId1);
+
+ let folderId2 = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.bookmarksMenuFolderId,
+ "Science",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let folderId3 = PlacesUtils.bookmarks
+ .createFolder(folderId2,
+ "Blogs",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ // Check title is correctly reported.
+ do_check_eq(PlacesUtils.bookmarks.getItemTitle(folderId3), "Blogs");
+ do_check_eq(PlacesUtils.bookmarks.getItemTitle(folderId2), "Science");
+
+ PlacesUtils.bookmarks.removeObserver(observer, false);
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_711914.js b/toolkit/components/places/tests/bookmarks/test_711914.js
new file mode 100644
index 0000000000..3712c8a77f
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_711914.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ /**
+ * Requests information to the service, so that bookmark's data is cached.
+ * @param aItemId
+ * Id of the bookmark to be cached.
+ */
+ function forceBookmarkCaching(aItemId) {
+ let parent = PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
+ PlacesUtils.bookmarks.getFolderIdForItem(parent);
+ }
+
+ let observer = {
+ onBeginUpdateBatch: () => forceBookmarkCaching(itemId1),
+ onEndUpdateBatch: () => forceBookmarkCaching(itemId1),
+ onItemAdded: forceBookmarkCaching,
+ onItemChanged: forceBookmarkCaching,
+ onItemMoved: forceBookmarkCaching,
+ onItemRemoved: function (id) {
+ try {
+ forceBookmarkCaching(id);
+ do_throw("trying to fetch a removed bookmark should throw");
+ } catch (ex) {}
+ },
+ onItemVisited: forceBookmarkCaching,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+ };
+ PlacesUtils.bookmarks.addObserver(observer, false);
+
+ let folder1 = PlacesUtils.bookmarks
+ .createFolder(PlacesUtils.bookmarksMenuFolderId,
+ "Folder1",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let folder2 = PlacesUtils.bookmarks
+ .createFolder(folder1,
+ "Folder2",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.insertBookmark(folder2,
+ NetUtil.newURI("http://mozilla.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Mozilla");
+
+ PlacesUtils.bookmarks.removeFolderChildren(folder1);
+
+ // Check title is correctly reported.
+ do_check_eq(PlacesUtils.bookmarks.getItemTitle(folder1), "Folder1");
+ try {
+ PlacesUtils.bookmarks.getItemTitle(folder2);
+ do_throw("trying to fetch a removed bookmark should throw");
+ } catch (ex) {}
+
+ PlacesUtils.bookmarks.removeObserver(observer, false);
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js
new file mode 100644
index 0000000000..c88323478f
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.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/. */
+
+/**
+ * Checks that automatically created bookmark backups are discarded if they are
+ * duplicate of an existing ones.
+ */
+function run_test() {
+ run_next_test();
+}
+
+add_task(function*() {
+ // Create a backup for yesterday in the backups folder.
+ let backupFolder = yield PlacesBackups.getBackupFolder();
+ let dateObj = new Date();
+ dateObj.setDate(dateObj.getDate() - 1);
+ let oldBackupName = PlacesBackups.getFilenameForDate(dateObj);
+ let oldBackup = OS.Path.join(backupFolder, oldBackupName);
+ let {count: count, hash: hash} = yield BookmarkJSONUtils.exportToFile(oldBackup);
+ do_check_true(count > 0);
+ do_check_eq(hash.length, 24);
+ oldBackupName = oldBackupName.replace(/\.json/, "_" + count + "_" + hash + ".json");
+ yield OS.File.move(oldBackup, OS.Path.join(backupFolder, oldBackupName));
+
+ // Create a backup.
+ // This should just rename the existing backup, so in the end there should be
+ // only one backup with today's date.
+ yield PlacesBackups.create();
+
+ // Get the hash of the generated backup
+ let backupFiles = yield PlacesBackups.getBackupFiles();
+ do_check_eq(backupFiles.length, 1);
+
+ let matches = OS.Path.basename(backupFiles[0]).match(PlacesBackups.filenamesRegex);
+ do_check_eq(matches[1], PlacesBackups.toISODateString(new Date()));
+ do_check_eq(matches[2], count);
+ do_check_eq(matches[3], hash);
+
+ // Add a bookmark and create another backup.
+ let bookmarkId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
+ uri("http://foo.com"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "foo");
+ // We must enforce a backup since one for today already exists. The forced
+ // backup will replace the existing one.
+ yield PlacesBackups.create(undefined, true);
+ do_check_eq(backupFiles.length, 1);
+ recentBackup = yield PlacesBackups.getMostRecentBackup();
+ do_check_neq(recentBackup, OS.Path.join(backupFolder, oldBackupName));
+ matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex);
+ do_check_eq(matches[1], PlacesBackups.toISODateString(new Date()));
+ do_check_eq(matches[2], count + 1);
+ do_check_neq(matches[3], hash);
+
+ // Clean up
+ PlacesUtils.bookmarks.removeItem(bookmarkId);
+ yield PlacesBackups.create(0);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js
new file mode 100644
index 0000000000..2c84990b32
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* compress_bookmark_backups_test() {
+ // Check for jsonlz4 extension
+ let todayFilename = PlacesBackups.getFilenameForDate(new Date(2014, 4, 15), true);
+ do_check_eq(todayFilename, "bookmarks-2014-05-15.jsonlz4");
+
+ yield PlacesBackups.create();
+
+ // Check that a backup for today has been created and the regex works fine for lz4.
+ do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1);
+ let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+ do_check_neq(mostRecentBackupFile, null);
+ do_check_true(PlacesBackups.filenamesRegex.test(OS.Path.basename(mostRecentBackupFile)));
+
+ // The most recent backup file has to be removed since saveBookmarksToJSONFile
+ // will otherwise over-write the current backup, since it will be made on the
+ // same date
+ yield OS.File.remove(mostRecentBackupFile);
+ do_check_false((yield OS.File.exists(mostRecentBackupFile)));
+
+ // Check that, if the user created a custom backup out of the default
+ // backups folder, it gets copied (compressed) into it.
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.json");
+ yield PlacesBackups.saveBookmarksToJSONFile(jsonFile);
+ do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1);
+
+ // Check if import works from lz4 compressed json
+ let uri = NetUtil.newURI("http://www.mozilla.org/en-US/");
+ let bm = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+
+ // Force create a compressed backup, Remove the bookmark, the restore the backup
+ yield PlacesBackups.create(undefined, true);
+ let recentBackup = yield PlacesBackups.getMostRecentBackup();
+ PlacesUtils.bookmarks.removeItem(bm);
+ yield BookmarkJSONUtils.importFromFile(recentBackup, true);
+ let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let node = root.getChild(0);
+ do_check_eq(node.uri, uri.spec);
+
+ root.containerOpen = false;
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+
+ // Cleanup.
+ yield OS.File.remove(jsonFile);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js
new file mode 100644
index 0000000000..4ea07fb393
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.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/. */
+
+/**
+ * To confirm that metadata i.e. bookmark count is set and retrieved for
+ * automatic backups.
+ */
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_saveBookmarksToJSONFile_and_create()
+{
+ // Add a bookmark
+ let uri = NetUtil.newURI("http://getfirefox.com/");
+ let bookmarkId =
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
+
+ // Test saveBookmarksToJSONFile()
+ let backupFile = FileUtils.getFile("TmpD", ["bookmarks.json"]);
+ backupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, parseInt("0600", 8));
+
+ let nodeCount = yield PlacesBackups.saveBookmarksToJSONFile(backupFile, true);
+ do_check_true(nodeCount > 0);
+ do_check_true(backupFile.exists());
+ do_check_eq(backupFile.leafName, "bookmarks.json");
+
+ // Ensure the backup would be copied to our backups folder when the original
+ // backup is saved somewhere else.
+ let recentBackup = yield PlacesBackups.getMostRecentBackup();
+ let matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex);
+ do_check_eq(matches[2], nodeCount);
+ do_check_eq(matches[3].length, 24);
+
+ // Clear all backups in our backups folder.
+ yield PlacesBackups.create(0);
+ do_check_eq((yield PlacesBackups.getBackupFiles()).length, 0);
+
+ // Test create() which saves bookmarks with metadata on the filename.
+ yield PlacesBackups.create();
+ do_check_eq((yield PlacesBackups.getBackupFiles()).length, 1);
+
+ mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+ do_check_neq(mostRecentBackupFile, null);
+ matches = OS.Path.basename(recentBackup).match(PlacesBackups.filenamesRegex);
+ do_check_eq(matches[2], nodeCount);
+ do_check_eq(matches[3].length, 24);
+
+ // Cleanup
+ backupFile.remove(false);
+ yield PlacesBackups.create(0);
+ PlacesUtils.bookmarks.removeItem(bookmarkId);
+});
+
diff --git a/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js
new file mode 100644
index 0000000000..f5e9f81877
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.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/. */
+
+/**
+ * Checks that backups properly include all of the bookmarks if the hierarchy
+ * in the database is unordered so that a hierarchy is defined before its
+ * ancestor in the bookmarks table.
+ */
+function run_test() {
+ run_next_test();
+}
+
+add_task(function*() {
+ let bm = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI("http://mozilla.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+ let f2 = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, "f2",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.moveItem(bm, f2, PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let f1 = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, "f1",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.moveItem(f2, f1, PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ // Create a backup.
+ yield PlacesBackups.create();
+
+ // Remove the bookmarks, then restore the backup.
+ PlacesUtils.bookmarks.removeItem(f1);
+ yield BookmarkJSONUtils.importFromFile((yield PlacesBackups.getMostRecentBackup()), true);
+
+ do_print("Checking first level");
+ let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let level1 = root.getChild(0);
+ do_check_eq(level1.title, "f1");
+ do_print("Checking second level");
+ PlacesUtils.asContainer(level1).containerOpen = true
+ let level2 = level1.getChild(0);
+ do_check_eq(level2.title, "f2");
+ do_print("Checking bookmark");
+ PlacesUtils.asContainer(level2).containerOpen = true
+ let bookmark = level2.getChild(0);
+ do_check_eq(bookmark.title, "bookmark");
+ level2.containerOpen = false;
+ level1.containerOpen = false;
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js
new file mode 100644
index 0000000000..b900887b56
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js
@@ -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/. */
+
+/**
+ * Checks that we don't encodeURI twice when creating bookmarks.html.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let uri = NetUtil.newURI("http://bt.ktxp.com/search.php?keyword=%E5%A6%84%E6%83%B3%E5%AD%A6%E7%94%9F%E4%BC%9A");
+ let bm = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+
+ let file = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.997030.html");
+ if ((yield OS.File.exists(file))) {
+ yield OS.File.remove(file);
+ }
+ yield BookmarkHTMLUtils.exportToFile(file);
+
+ // Remove the bookmarks, then restore the backup.
+ PlacesUtils.bookmarks.removeItem(bm);
+ yield BookmarkHTMLUtils.importFromFile(file, true);
+
+ do_print("Checking first level");
+ let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let node = root.getChild(0);
+ do_check_eq(node.uri, uri.spec);
+
+ root.containerOpen = false;
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_async_observers.js b/toolkit/components/places/tests/bookmarks/test_async_observers.js
new file mode 100644
index 0000000000..86d48ac240
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_async_observers.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test checks that bookmarks service is correctly forwarding async
+ * events like visit or favicon additions. */
+
+const NOW = Date.now() * 1000;
+
+var observer = {
+ bookmarks: [],
+ observedBookmarks: 0,
+ observedVisitId: 0,
+ deferred: null,
+
+ /**
+ * Returns a promise that is resolved when the observer determines that the
+ * test can continue. This is required rather than calling run_next_test
+ * directly in the observer because there are cases where we must wait for
+ * other asynchronous events to be completed in addition to this.
+ */
+ setupCompletionPromise: function ()
+ {
+ this.observedBookmarks = 0;
+ this.deferred = Promise.defer();
+ return this.deferred.promise;
+ },
+
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemAdded: function () {},
+ onItemRemoved: function () {},
+ onItemMoved: function () {},
+ onItemChanged: function(aItemId, aProperty, aIsAnnotation, aNewValue,
+ aLastModified, aItemType)
+ {
+ do_print("Check that we got the correct change information.");
+ do_check_neq(this.bookmarks.indexOf(aItemId), -1);
+ if (aProperty == "favicon") {
+ do_check_false(aIsAnnotation);
+ do_check_eq(aNewValue, SMALLPNG_DATA_URI.spec);
+ do_check_eq(aLastModified, 0);
+ do_check_eq(aItemType, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ }
+ else if (aProperty == "cleartime") {
+ do_check_false(aIsAnnotation);
+ do_check_eq(aNewValue, "");
+ do_check_eq(aLastModified, 0);
+ do_check_eq(aItemType, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ }
+ else {
+ do_throw("Unexpected property change " + aProperty);
+ }
+
+ if (++this.observedBookmarks == this.bookmarks.length) {
+ this.deferred.resolve();
+ }
+ },
+ onItemVisited: function(aItemId, aVisitId, aTime)
+ {
+ do_print("Check that we got the correct visit information.");
+ do_check_neq(this.bookmarks.indexOf(aItemId), -1);
+ this.observedVisitId = aVisitId;
+ do_check_eq(aTime, NOW);
+ if (++this.observedBookmarks == this.bookmarks.length) {
+ this.deferred.resolve();
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ])
+};
+PlacesUtils.bookmarks.addObserver(observer, false);
+
+add_task(function* test_add_visit()
+{
+ let observerPromise = observer.setupCompletionPromise();
+
+ // Add a visit to the bookmark and wait for the observer.
+ let visitId;
+ let deferUpdatePlaces = Promise.defer();
+ PlacesUtils.asyncHistory.updatePlaces({
+ uri: NetUtil.newURI("http://book.ma.rk/"),
+ visits: [{ transitionType: TRANSITION_TYPED, visitDate: NOW }]
+ }, {
+ handleError: function TAV_handleError() {
+ deferUpdatePlaces.reject(new Error("Unexpected error in adding visit."));
+ },
+ handleResult: function (aPlaceInfo) {
+ visitId = aPlaceInfo.visits[0].visitId;
+ },
+ handleCompletion: function TAV_handleCompletion() {
+ deferUpdatePlaces.resolve();
+ }
+ });
+
+ // Wait for both the observer and the asynchronous update, in any order.
+ yield deferUpdatePlaces.promise;
+ yield observerPromise;
+
+ // Check that both asynchronous results are consistent.
+ do_check_eq(observer.observedVisitId, visitId);
+});
+
+add_task(function* test_add_icon()
+{
+ let observerPromise = observer.setupCompletionPromise();
+ PlacesUtils.favicons.setAndFetchFaviconForPage(NetUtil.newURI("http://book.ma.rk/"),
+ SMALLPNG_DATA_URI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ yield observerPromise;
+});
+
+add_task(function* test_remove_page()
+{
+ let observerPromise = observer.setupCompletionPromise();
+ PlacesUtils.history.removePage(NetUtil.newURI("http://book.ma.rk/"));
+ yield observerPromise;
+});
+
+add_task(function cleanup()
+{
+ PlacesUtils.bookmarks.removeObserver(observer, false);
+});
+
+add_task(function* shutdown()
+{
+ // Check that async observers don't try to create async statements after
+ // shutdown. That would cause assertions, since the async thread is gone
+ // already. Note that in such a case the notifications are not fired, so we
+ // cannot test for them.
+ // Put an history notification that triggers AsyncGetBookmarksForURI between
+ // asyncClose() and the actual connection closing. Enqueuing a main-thread
+ // event just after places-will-close-connection should ensure it runs before
+ // places-connection-closed.
+ // Notice this code is not using helpers cause it depends on a very specific
+ // order, a change in the helpers code could make this test useless.
+ let deferred = Promise.defer();
+
+ Services.obs.addObserver(function onNotification() {
+ Services.obs.removeObserver(onNotification, "places-will-close-connection");
+ do_check_true(true, "Observed fake places shutdown");
+
+ Services.tm.mainThread.dispatch(() => {
+ // WARNING: this is very bad, never use out of testing code.
+ PlacesUtils.bookmarks.QueryInterface(Ci.nsINavHistoryObserver)
+ .onPageChanged(NetUtil.newURI("http://book.ma.rk/"),
+ Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON,
+ "test", "test");
+ deferred.resolve(promiseTopicObserved("places-connection-closed"));
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ }, "places-will-close-connection", false);
+ shutdownPlaces();
+
+ yield deferred.promise;
+});
+
+function run_test()
+{
+ // Add multiple bookmarks to the same uri.
+ observer.bookmarks.push(
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI("http://book.ma.rk/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark")
+ );
+ observer.bookmarks.push(
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+ NetUtil.newURI("http://book.ma.rk/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark")
+ );
+
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bmindex.js b/toolkit/components/places/tests/bookmarks/test_bmindex.js
new file mode 100644
index 0000000000..c764e43100
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bmindex.js
@@ -0,0 +1,124 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 NUM_BOOKMARKS = 20;
+const NUM_SEPARATORS = 5;
+const NUM_FOLDERS = 10;
+const NUM_ITEMS = NUM_BOOKMARKS + NUM_SEPARATORS + NUM_FOLDERS;
+const MIN_RAND = -5;
+const MAX_RAND = 40;
+
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+function check_contiguous_indexes(aBookmarks) {
+ var indexes = [];
+ aBookmarks.forEach(function(aBookmarkId) {
+ let bmIndex = bs.getItemIndex(aBookmarkId);
+ dump("Index: " + bmIndex + "\n");
+ dump("Checking duplicates\n");
+ do_check_eq(indexes.indexOf(bmIndex), -1);
+ dump("Checking out of range, found " + aBookmarks.length + " items\n");
+ do_check_true(bmIndex >= 0 && bmIndex < aBookmarks.length);
+ indexes.push(bmIndex);
+ });
+ dump("Checking all valid indexes have been used\n");
+ do_check_eq(indexes.length, aBookmarks.length);
+}
+
+// main
+function run_test() {
+ var bookmarks = [];
+ // Insert bookmarks with random indexes.
+ for (let i = 0; bookmarks.length < NUM_BOOKMARKS; i++) {
+ let randIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND)));
+ try {
+ let id = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ uri("http://" + i + ".mozilla.org/"),
+ randIndex, "Test bookmark " + i);
+ if (randIndex < -1)
+ do_throw("Creating a bookmark at an invalid index should throw");
+ bookmarks.push(id);
+ }
+ catch (ex) {
+ if (randIndex >= -1)
+ do_throw("Creating a bookmark at a valid index should not throw");
+ }
+ }
+ check_contiguous_indexes(bookmarks);
+
+ // Insert separators with random indexes.
+ for (let i = 0; bookmarks.length < NUM_BOOKMARKS + NUM_SEPARATORS; i++) {
+ let randIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND)));
+ try {
+ let id = bs.insertSeparator(bs.unfiledBookmarksFolder, randIndex);
+ if (randIndex < -1)
+ do_throw("Creating a separator at an invalid index should throw");
+ bookmarks.push(id);
+ }
+ catch (ex) {
+ if (randIndex >= -1)
+ do_throw("Creating a separator at a valid index should not throw");
+ }
+ }
+ check_contiguous_indexes(bookmarks);
+
+ // Insert folders with random indexes.
+ for (let i = 0; bookmarks.length < NUM_ITEMS; i++) {
+ let randIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND)));
+ try {
+ let id = bs.createFolder(bs.unfiledBookmarksFolder,
+ "Test folder " + i, randIndex);
+ if (randIndex < -1)
+ do_throw("Creating a folder at an invalid index should throw");
+ bookmarks.push(id);
+ }
+ catch (ex) {
+ if (randIndex >= -1)
+ do_throw("Creating a folder at a valid index should not throw");
+ }
+ }
+ check_contiguous_indexes(bookmarks);
+
+ // Execute some random bookmark delete.
+ for (let i = 0; i < Math.ceil(NUM_ITEMS / 4); i++) {
+ let id = bookmarks.splice(Math.floor(Math.random() * bookmarks.length), 1);
+ dump("Removing item with id " + id + "\n");
+ bs.removeItem(id);
+ }
+ check_contiguous_indexes(bookmarks);
+
+ // Execute some random bookmark move. This will also try to move it to
+ // invalid index values.
+ for (let i = 0; i < Math.ceil(NUM_ITEMS / 4); i++) {
+ let randIndex = Math.floor(Math.random() * bookmarks.length);
+ let id = bookmarks[randIndex];
+ let newIndex = Math.round(MIN_RAND + (Math.random() * (MAX_RAND - MIN_RAND)));
+ dump("Moving item with id " + id + " to index " + newIndex + "\n");
+ try {
+ bs.moveItem(id, bs.unfiledBookmarksFolder, newIndex);
+ if (newIndex < -1)
+ do_throw("Moving an item to a negative index should throw\n");
+ }
+ catch (ex) {
+ if (newIndex >= -1)
+ do_throw("Moving an item to a valid index should not throw\n");
+ }
+
+ }
+ check_contiguous_indexes(bookmarks);
+
+ // Ensure setItemIndex throws if we pass it a negative index.
+ try {
+ bs.setItemIndex(bookmarks[0], -1);
+ do_throw("setItemIndex should throw for a negative index");
+ } catch (ex) {}
+ // Ensure setItemIndex throws if we pass it a bad itemId.
+ try {
+ bs.setItemIndex(0, 5);
+ do_throw("setItemIndex should throw for a bad itemId");
+ } catch (ex) {}
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks.js b/toolkit/components/places/tests/bookmarks/test_bookmarks.js
new file mode 100644
index 0000000000..b67482223c
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js
@@ -0,0 +1,718 @@
+/* -*- 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 bs = PlacesUtils.bookmarks;
+var hs = PlacesUtils.history;
+var anno = PlacesUtils.annotations;
+
+
+var bookmarksObserver = {
+ onBeginUpdateBatch: function() {
+ this._beginUpdateBatch = true;
+ },
+ onEndUpdateBatch: function() {
+ this._endUpdateBatch = true;
+ },
+ onItemAdded: function(id, folder, index, itemType, uri, title, dateAdded,
+ guid) {
+ this._itemAddedId = id;
+ this._itemAddedParent = folder;
+ this._itemAddedIndex = index;
+ this._itemAddedURI = uri;
+ this._itemAddedTitle = title;
+
+ // Ensure that we've created a guid for this item.
+ let stmt = DBConn().createStatement(
+ `SELECT guid
+ FROM moz_bookmarks
+ WHERE id = :item_id`
+ );
+ stmt.params.item_id = id;
+ do_check_true(stmt.executeStep());
+ do_check_false(stmt.getIsNull(0));
+ do_check_valid_places_guid(stmt.row.guid);
+ do_check_eq(stmt.row.guid, guid);
+ stmt.finalize();
+ },
+ onItemRemoved: function(id, folder, index, itemType) {
+ this._itemRemovedId = id;
+ this._itemRemovedFolder = folder;
+ this._itemRemovedIndex = index;
+ },
+ onItemChanged: function(id, property, isAnnotationProperty, value,
+ lastModified, itemType, parentId, guid, parentGuid,
+ oldValue) {
+ this._itemChangedId = id;
+ this._itemChangedProperty = property;
+ this._itemChanged_isAnnotationProperty = isAnnotationProperty;
+ this._itemChangedValue = value;
+ this._itemChangedOldValue = oldValue;
+ },
+ onItemVisited: function(id, visitID, time) {
+ this._itemVisitedId = id;
+ this._itemVisitedVistId = visitID;
+ this._itemVisitedTime = time;
+ },
+ onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex,
+ itemType) {
+ this._itemMovedId = id
+ this._itemMovedOldParent = oldParent;
+ this._itemMovedOldIndex = oldIndex;
+ this._itemMovedNewParent = newParent;
+ this._itemMovedNewIndex = newIndex;
+ },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ])
+};
+
+
+// Get bookmarks menu folder id.
+var root = bs.bookmarksMenuFolder;
+// Index at which items should begin.
+var bmStartIndex = 0;
+
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_bookmarks() {
+ bs.addObserver(bookmarksObserver, false);
+
+ // test special folders
+ do_check_true(bs.placesRoot > 0);
+ do_check_true(bs.bookmarksMenuFolder > 0);
+ do_check_true(bs.tagsFolder > 0);
+ do_check_true(bs.toolbarFolder > 0);
+ do_check_true(bs.unfiledBookmarksFolder > 0);
+
+ // test getFolderIdForItem() with bogus item id will throw
+ try {
+ bs.getFolderIdForItem(0);
+ do_throw("getFolderIdForItem accepted bad input");
+ } catch (ex) {}
+
+ // test getFolderIdForItem() with bogus item id will throw
+ try {
+ bs.getFolderIdForItem(-1);
+ do_throw("getFolderIdForItem accepted bad input");
+ } catch (ex) {}
+
+ // test root parentage
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+
+ // create a folder to hold all the tests
+ // this makes the tests more tolerant of changes to default_places.html
+ let testRoot = bs.createFolder(root, "places bookmarks xpcshell tests",
+ bs.DEFAULT_INDEX);
+ do_check_eq(bookmarksObserver._itemAddedId, testRoot);
+ do_check_eq(bookmarksObserver._itemAddedParent, root);
+ do_check_eq(bookmarksObserver._itemAddedIndex, bmStartIndex);
+ do_check_eq(bookmarksObserver._itemAddedURI, null);
+ let testStartIndex = 0;
+
+ // test getItemIndex for folders
+ do_check_eq(bs.getItemIndex(testRoot), bmStartIndex);
+
+ // test getItemType for folders
+ do_check_eq(bs.getItemType(testRoot), bs.TYPE_FOLDER);
+
+ // insert a bookmark.
+ // the time before we insert, in microseconds
+ let beforeInsert = Date.now() * 1000;
+ do_check_true(beforeInsert > 0);
+
+ let newId = bs.insertBookmark(testRoot, uri("http://google.com/"),
+ bs.DEFAULT_INDEX, "");
+ do_check_eq(bookmarksObserver._itemAddedId, newId);
+ do_check_eq(bookmarksObserver._itemAddedParent, testRoot);
+ do_check_eq(bookmarksObserver._itemAddedIndex, testStartIndex);
+ do_check_true(bookmarksObserver._itemAddedURI.equals(uri("http://google.com/")));
+ do_check_eq(bs.getBookmarkURI(newId).spec, "http://google.com/");
+
+ let dateAdded = bs.getItemDateAdded(newId);
+ // dateAdded can equal beforeInsert
+ do_check_true(is_time_ordered(beforeInsert, dateAdded));
+
+ // after just inserting, modified should not be set
+ let lastModified = bs.getItemLastModified(newId);
+ do_check_eq(lastModified, dateAdded);
+
+ // The time before we set the title, in microseconds.
+ let beforeSetTitle = Date.now() * 1000;
+ do_check_true(beforeSetTitle >= beforeInsert);
+
+ // Workaround possible VM timers issues moving lastModified and dateAdded
+ // to the past.
+ lastModified -= 1000;
+ bs.setItemLastModified(newId, lastModified);
+ dateAdded -= 1000;
+ bs.setItemDateAdded(newId, dateAdded);
+
+ // set bookmark title
+ bs.setItemTitle(newId, "Google");
+ do_check_eq(bookmarksObserver._itemChangedId, newId);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+ do_check_eq(bookmarksObserver._itemChangedValue, "Google");
+
+ // check that dateAdded hasn't changed
+ let dateAdded2 = bs.getItemDateAdded(newId);
+ do_check_eq(dateAdded2, dateAdded);
+
+ // check lastModified after we set the title
+ let lastModified2 = bs.getItemLastModified(newId);
+ do_print("test setItemTitle");
+ do_print("dateAdded = " + dateAdded);
+ do_print("beforeSetTitle = " + beforeSetTitle);
+ do_print("lastModified = " + lastModified);
+ do_print("lastModified2 = " + lastModified2);
+ do_check_true(is_time_ordered(lastModified, lastModified2));
+ do_check_true(is_time_ordered(dateAdded, lastModified2));
+
+ // get item title
+ let title = bs.getItemTitle(newId);
+ do_check_eq(title, "Google");
+
+ // test getItemType for bookmarks
+ do_check_eq(bs.getItemType(newId), bs.TYPE_BOOKMARK);
+
+ // get item title bad input
+ try {
+ bs.getItemTitle(-3);
+ do_throw("getItemTitle accepted bad input");
+ } catch (ex) {}
+
+ // get the folder that the bookmark is in
+ let folderId = bs.getFolderIdForItem(newId);
+ do_check_eq(folderId, testRoot);
+
+ // test getItemIndex for bookmarks
+ do_check_eq(bs.getItemIndex(newId), testStartIndex);
+
+ // create a folder at a specific index
+ let workFolder = bs.createFolder(testRoot, "Work", 0);
+ do_check_eq(bookmarksObserver._itemAddedId, workFolder);
+ do_check_eq(bookmarksObserver._itemAddedParent, testRoot);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 0);
+ do_check_eq(bookmarksObserver._itemAddedURI, null);
+
+ do_check_eq(bs.getItemTitle(workFolder), "Work");
+ bs.setItemTitle(workFolder, "Work #");
+ do_check_eq(bs.getItemTitle(workFolder), "Work #");
+
+ // add item into subfolder, specifying index
+ let newId2 = bs.insertBookmark(workFolder,
+ uri("http://developer.mozilla.org/"),
+ 0, "");
+ do_check_eq(bookmarksObserver._itemAddedId, newId2);
+ do_check_eq(bookmarksObserver._itemAddedParent, workFolder);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 0);
+
+ // change item
+ bs.setItemTitle(newId2, "DevMo");
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+
+ // insert item into subfolder
+ let newId3 = bs.insertBookmark(workFolder,
+ uri("http://msdn.microsoft.com/"),
+ bs.DEFAULT_INDEX, "");
+ do_check_eq(bookmarksObserver._itemAddedId, newId3);
+ do_check_eq(bookmarksObserver._itemAddedParent, workFolder);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 1);
+
+ // change item
+ bs.setItemTitle(newId3, "MSDN");
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+
+ // remove item
+ bs.removeItem(newId2);
+ do_check_eq(bookmarksObserver._itemRemovedId, newId2);
+ do_check_eq(bookmarksObserver._itemRemovedFolder, workFolder);
+ do_check_eq(bookmarksObserver._itemRemovedIndex, 0);
+
+ // insert item into subfolder
+ let newId4 = bs.insertBookmark(workFolder,
+ uri("http://developer.mozilla.org/"),
+ bs.DEFAULT_INDEX, "");
+ do_check_eq(bookmarksObserver._itemAddedId, newId4);
+ do_check_eq(bookmarksObserver._itemAddedParent, workFolder);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 1);
+
+ // create folder
+ let homeFolder = bs.createFolder(testRoot, "Home", bs.DEFAULT_INDEX);
+ do_check_eq(bookmarksObserver._itemAddedId, homeFolder);
+ do_check_eq(bookmarksObserver._itemAddedParent, testRoot);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 2);
+
+ // insert item
+ let newId5 = bs.insertBookmark(homeFolder, uri("http://espn.com/"),
+ bs.DEFAULT_INDEX, "");
+ do_check_eq(bookmarksObserver._itemAddedId, newId5);
+ do_check_eq(bookmarksObserver._itemAddedParent, homeFolder);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 0);
+
+ // change item
+ bs.setItemTitle(newId5, "ESPN");
+ do_check_eq(bookmarksObserver._itemChangedId, newId5);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+
+ // insert query item
+ let uri6 = uri("place:domain=google.com&type="+
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY);
+ let newId6 = bs.insertBookmark(testRoot, uri6, bs.DEFAULT_INDEX, "");
+ do_check_eq(bookmarksObserver._itemAddedParent, testRoot);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 3);
+
+ // change item
+ bs.setItemTitle(newId6, "Google Sites");
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+
+ // test getIdForItemAt
+ do_check_eq(bs.getIdForItemAt(testRoot, 0), workFolder);
+ // wrong parent, should return -1
+ do_check_eq(bs.getIdForItemAt(1337, 0), -1);
+ // wrong index, should return -1
+ do_check_eq(bs.getIdForItemAt(testRoot, 1337), -1);
+ // wrong parent and index, should return -1
+ do_check_eq(bs.getIdForItemAt(1337, 1337), -1);
+
+ // move folder, appending, to different folder
+ let oldParentCC = getChildCount(testRoot);
+ bs.moveItem(workFolder, homeFolder, bs.DEFAULT_INDEX);
+ do_check_eq(bookmarksObserver._itemMovedId, workFolder);
+ do_check_eq(bookmarksObserver._itemMovedOldParent, testRoot);
+ do_check_eq(bookmarksObserver._itemMovedOldIndex, 0);
+ do_check_eq(bookmarksObserver._itemMovedNewParent, homeFolder);
+ do_check_eq(bookmarksObserver._itemMovedNewIndex, 1);
+
+ // test that the new index is properly stored
+ do_check_eq(bs.getItemIndex(workFolder), 1);
+ do_check_eq(bs.getFolderIdForItem(workFolder), homeFolder);
+
+ // try to get index of the item from within the old parent folder
+ // check that it has been really removed from there
+ do_check_neq(bs.getIdForItemAt(testRoot, 0), workFolder);
+ // check the last item from within the old parent folder
+ do_check_neq(bs.getIdForItemAt(testRoot, -1), workFolder);
+ // check the index of the item within the new parent folder
+ do_check_eq(bs.getIdForItemAt(homeFolder, 1), workFolder);
+ // try to get index of the last item within the new parent folder
+ do_check_eq(bs.getIdForItemAt(homeFolder, -1), workFolder);
+ // XXX expose FolderCount, and check that the old parent has one less child?
+ do_check_eq(getChildCount(testRoot), oldParentCC-1);
+
+ // move item, appending, to different folder
+ bs.moveItem(newId5, testRoot, bs.DEFAULT_INDEX);
+ do_check_eq(bookmarksObserver._itemMovedId, newId5);
+ do_check_eq(bookmarksObserver._itemMovedOldParent, homeFolder);
+ do_check_eq(bookmarksObserver._itemMovedOldIndex, 0);
+ do_check_eq(bookmarksObserver._itemMovedNewParent, testRoot);
+ do_check_eq(bookmarksObserver._itemMovedNewIndex, 3);
+
+ // test get folder's index
+ let tmpFolder = bs.createFolder(testRoot, "tmp", 2);
+ do_check_eq(bs.getItemIndex(tmpFolder), 2);
+
+ // test setKeywordForBookmark
+ let kwTestItemId = bs.insertBookmark(testRoot, uri("http://keywordtest.com"),
+ bs.DEFAULT_INDEX, "");
+ bs.setKeywordForBookmark(kwTestItemId, "bar");
+
+ // test getKeywordForBookmark
+ let k = bs.getKeywordForBookmark(kwTestItemId);
+ do_check_eq("bar", k);
+
+ // test getURIForKeyword
+ let u = bs.getURIForKeyword("bar");
+ do_check_eq("http://keywordtest.com/", u.spec);
+
+ // test removeFolderChildren
+ // 1) add/remove each child type (bookmark, separator, folder)
+ tmpFolder = bs.createFolder(testRoot, "removeFolderChildren",
+ bs.DEFAULT_INDEX);
+ bs.insertBookmark(tmpFolder, uri("http://foo9.com/"), bs.DEFAULT_INDEX, "");
+ bs.createFolder(tmpFolder, "subfolder", bs.DEFAULT_INDEX);
+ bs.insertSeparator(tmpFolder, bs.DEFAULT_INDEX);
+ // 2) confirm that folder has 3 children
+ let options = hs.getNewQueryOptions();
+ let query = hs.getNewQuery();
+ query.setFolders([tmpFolder], 1);
+ try {
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ do_check_eq(rootNode.childCount, 3);
+ rootNode.containerOpen = false;
+ } catch (ex) {
+ do_throw("test removeFolderChildren() - querying for children failed: " + ex);
+ }
+ // 3) remove all children
+ bs.removeFolderChildren(tmpFolder);
+ // 4) confirm that folder has 0 children
+ try {
+ result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ do_check_eq(rootNode.childCount, 0);
+ rootNode.containerOpen = false;
+ } catch (ex) {
+ do_throw("removeFolderChildren(): " + ex);
+ }
+
+ // XXX - test folderReadOnly
+
+ // test bookmark id in query output
+ try {
+ options = hs.getNewQueryOptions();
+ query = hs.getNewQuery();
+ query.setFolders([testRoot], 1);
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_print("bookmark itemId test: CC = " + cc);
+ do_check_true(cc > 0);
+ for (let i=0; i < cc; ++i) {
+ let node = rootNode.getChild(i);
+ if (node.type == node.RESULT_TYPE_FOLDER ||
+ node.type == node.RESULT_TYPE_URI ||
+ node.type == node.RESULT_TYPE_SEPARATOR ||
+ node.type == node.RESULT_TYPE_QUERY) {
+ do_check_true(node.itemId > 0);
+ }
+ else {
+ do_check_eq(node.itemId, -1);
+ }
+ }
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("bookmarks query: " + ex);
+ }
+
+ // test that multiple bookmarks with same URI show up right in bookmark
+ // folder queries, todo: also to do for complex folder queries
+ try {
+ // test uri
+ let mURI = uri("http://multiple.uris.in.query");
+
+ let testFolder = bs.createFolder(testRoot, "test Folder", bs.DEFAULT_INDEX);
+ // add 2 bookmarks
+ bs.insertBookmark(testFolder, mURI, bs.DEFAULT_INDEX, "title 1");
+ bs.insertBookmark(testFolder, mURI, bs.DEFAULT_INDEX, "title 2");
+
+ // query
+ options = hs.getNewQueryOptions();
+ query = hs.getNewQuery();
+ query.setFolders([testFolder], 1);
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_check_eq(cc, 2);
+ do_check_eq(rootNode.getChild(0).title, "title 1");
+ do_check_eq(rootNode.getChild(1).title, "title 2");
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("bookmarks query: " + ex);
+ }
+
+ // test change bookmark uri
+ let newId10 = bs.insertBookmark(testRoot, uri("http://foo10.com/"),
+ bs.DEFAULT_INDEX, "");
+ dateAdded = bs.getItemDateAdded(newId10);
+ // after just inserting, modified should not be set
+ lastModified = bs.getItemLastModified(newId10);
+ do_check_eq(lastModified, dateAdded);
+
+ // Workaround possible VM timers issues moving lastModified and dateAdded
+ // to the past.
+ lastModified -= 1000;
+ bs.setItemLastModified(newId10, lastModified);
+ dateAdded -= 1000;
+ bs.setItemDateAdded(newId10, dateAdded);
+
+ bs.changeBookmarkURI(newId10, uri("http://foo11.com/"));
+
+ // check that lastModified is set after we change the bookmark uri
+ lastModified2 = bs.getItemLastModified(newId10);
+ do_print("test changeBookmarkURI");
+ do_print("dateAdded = " + dateAdded);
+ do_print("lastModified = " + lastModified);
+ do_print("lastModified2 = " + lastModified2);
+ do_check_true(is_time_ordered(lastModified, lastModified2));
+ do_check_true(is_time_ordered(dateAdded, lastModified2));
+
+ do_check_eq(bookmarksObserver._itemChangedId, newId10);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "uri");
+ do_check_eq(bookmarksObserver._itemChangedValue, "http://foo11.com/");
+ do_check_eq(bookmarksObserver._itemChangedOldValue, "http://foo10.com/");
+
+ // test getBookmarkURI
+ let newId11 = bs.insertBookmark(testRoot, uri("http://foo11.com/"),
+ bs.DEFAULT_INDEX, "");
+ let bmURI = bs.getBookmarkURI(newId11);
+ do_check_eq("http://foo11.com/", bmURI.spec);
+
+ // test getBookmarkURI with non-bookmark items
+ try {
+ bs.getBookmarkURI(testRoot);
+ do_throw("getBookmarkURI() should throw for non-bookmark items!");
+ } catch (ex) {}
+
+ // test getItemIndex
+ let newId12 = bs.insertBookmark(testRoot, uri("http://foo11.com/"), 1, "");
+ let bmIndex = bs.getItemIndex(newId12);
+ do_check_eq(1, bmIndex);
+
+ // insert a bookmark with title ZZZXXXYYY and then search for it.
+ // this test confirms that we can find bookmarks that we haven't visited
+ // (which are "hidden") and that we can find by title.
+ // see bug #369887 for more details
+ let newId13 = bs.insertBookmark(testRoot, uri("http://foobarcheese.com/"),
+ bs.DEFAULT_INDEX, "");
+ do_check_eq(bookmarksObserver._itemAddedId, newId13);
+ do_check_eq(bookmarksObserver._itemAddedParent, testRoot);
+ do_check_eq(bookmarksObserver._itemAddedIndex, 11);
+
+ // set bookmark title
+ bs.setItemTitle(newId13, "ZZZXXXYYY");
+ do_check_eq(bookmarksObserver._itemChangedId, newId13);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+ do_check_eq(bookmarksObserver._itemChangedValue, "ZZZXXXYYY");
+
+ // check if setting an item annotation triggers onItemChanged
+ bookmarksObserver._itemChangedId = -1;
+ anno.setItemAnnotation(newId3, "test-annotation", "foo", 0, 0);
+ do_check_eq(bookmarksObserver._itemChangedId, newId3);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "test-annotation");
+ do_check_true(bookmarksObserver._itemChanged_isAnnotationProperty);
+ do_check_eq(bookmarksObserver._itemChangedValue, "");
+
+ // test search on bookmark title ZZZXXXYYY
+ try {
+ options = hs.getNewQueryOptions();
+ options.excludeQueries = 1;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ query = hs.getNewQuery();
+ query.searchTerms = "ZZZXXXYYY";
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_check_eq(cc, 1);
+ let node = rootNode.getChild(0);
+ do_check_eq(node.title, "ZZZXXXYYY");
+ do_check_true(node.itemId > 0);
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("bookmarks query: " + ex);
+ }
+
+ // test dateAdded and lastModified properties
+ // for a search query
+ try {
+ options = hs.getNewQueryOptions();
+ options.excludeQueries = 1;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ query = hs.getNewQuery();
+ query.searchTerms = "ZZZXXXYYY";
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_check_eq(cc, 1);
+ let node = rootNode.getChild(0);
+
+ do_check_eq(typeof node.dateAdded, "number");
+ do_check_true(node.dateAdded > 0);
+
+ do_check_eq(typeof node.lastModified, "number");
+ do_check_true(node.lastModified > 0);
+
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("bookmarks query: " + ex);
+ }
+
+ // test dateAdded and lastModified properties
+ // for a folder query
+ try {
+ options = hs.getNewQueryOptions();
+ query = hs.getNewQuery();
+ query.setFolders([testRoot], 1);
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_check_true(cc > 0);
+ for (let i = 0; i < cc; i++) {
+ let node = rootNode.getChild(i);
+
+ if (node.type == node.RESULT_TYPE_URI) {
+ do_check_eq(typeof node.dateAdded, "number");
+ do_check_true(node.dateAdded > 0);
+
+ do_check_eq(typeof node.lastModified, "number");
+ do_check_true(node.lastModified > 0);
+ break;
+ }
+ }
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("bookmarks query: " + ex);
+ }
+
+ // check setItemLastModified() and setItemDateAdded()
+ let newId14 = bs.insertBookmark(testRoot, uri("http://bar.tld/"),
+ bs.DEFAULT_INDEX, "");
+ dateAdded = bs.getItemDateAdded(newId14);
+ lastModified = bs.getItemLastModified(newId14);
+ do_check_eq(lastModified, dateAdded);
+ bs.setItemLastModified(newId14, 1234000000000000);
+ let fakeLastModified = bs.getItemLastModified(newId14);
+ do_check_eq(fakeLastModified, 1234000000000000);
+ bs.setItemDateAdded(newId14, 4321000000000000);
+ let fakeDateAdded = bs.getItemDateAdded(newId14);
+ do_check_eq(fakeDateAdded, 4321000000000000);
+
+ // ensure that removing an item removes its annotations
+ do_check_true(anno.itemHasAnnotation(newId3, "test-annotation"));
+ bs.removeItem(newId3);
+ do_check_false(anno.itemHasAnnotation(newId3, "test-annotation"));
+
+ // bug 378820
+ let uri1 = uri("http://foo.tld/a");
+ bs.insertBookmark(testRoot, uri1, bs.DEFAULT_INDEX, "");
+ yield PlacesTestUtils.addVisits(uri1);
+
+ // bug 646993 - test bookmark titles longer than the maximum allowed length
+ let title15 = Array(TITLE_LENGTH_MAX + 5).join("X");
+ let title15expected = title15.substring(0, TITLE_LENGTH_MAX);
+ let newId15 = bs.insertBookmark(testRoot, uri("http://evil.com/"),
+ bs.DEFAULT_INDEX, title15);
+
+ do_check_eq(bs.getItemTitle(newId15).length,
+ title15expected.length);
+ do_check_eq(bookmarksObserver._itemAddedTitle, title15expected);
+ // test title length after updates
+ bs.setItemTitle(newId15, title15 + " updated");
+ do_check_eq(bs.getItemTitle(newId15).length,
+ title15expected.length);
+ do_check_eq(bookmarksObserver._itemChangedId, newId15);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+ do_check_eq(bookmarksObserver._itemChangedValue, title15expected);
+
+ testSimpleFolderResult();
+});
+
+function testSimpleFolderResult() {
+ // the time before we create a folder, in microseconds
+ // Workaround possible VM timers issues subtracting 1us.
+ let beforeCreate = Date.now() * 1000 - 1;
+ do_check_true(beforeCreate > 0);
+
+ // create a folder
+ let parent = bs.createFolder(root, "test", bs.DEFAULT_INDEX);
+
+ let dateCreated = bs.getItemDateAdded(parent);
+ do_print("check that the folder was created with a valid dateAdded");
+ do_print("beforeCreate = " + beforeCreate);
+ do_print("dateCreated = " + dateCreated);
+ do_check_true(is_time_ordered(beforeCreate, dateCreated));
+
+ // the time before we insert, in microseconds
+ // Workaround possible VM timers issues subtracting 1ms.
+ let beforeInsert = Date.now() * 1000 - 1;
+ do_check_true(beforeInsert > 0);
+
+ // insert a separator
+ let sep = bs.insertSeparator(parent, bs.DEFAULT_INDEX);
+
+ let dateAdded = bs.getItemDateAdded(sep);
+ do_print("check that the separator was created with a valid dateAdded");
+ do_print("beforeInsert = " + beforeInsert);
+ do_print("dateAdded = " + dateAdded);
+ do_check_true(is_time_ordered(beforeInsert, dateAdded));
+
+ // re-set item title separately so can test nodes' last modified
+ let item = bs.insertBookmark(parent, uri("about:blank"),
+ bs.DEFAULT_INDEX, "");
+ bs.setItemTitle(item, "test bookmark");
+
+ // see above
+ let folder = bs.createFolder(parent, "test folder", bs.DEFAULT_INDEX);
+ bs.setItemTitle(folder, "test folder");
+
+ let longName = Array(TITLE_LENGTH_MAX + 5).join("A");
+ let folderLongName = bs.createFolder(parent, longName, bs.DEFAULT_INDEX);
+ do_check_eq(bookmarksObserver._itemAddedTitle, longName.substring(0, TITLE_LENGTH_MAX));
+
+ let options = hs.getNewQueryOptions();
+ let query = hs.getNewQuery();
+ query.setFolders([parent], 1);
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ do_check_eq(rootNode.childCount, 4);
+
+ let node = rootNode.getChild(0);
+ do_check_true(node.dateAdded > 0);
+ do_check_eq(node.lastModified, node.dateAdded);
+ do_check_eq(node.itemId, sep);
+ do_check_eq(node.title, "");
+ node = rootNode.getChild(1);
+ do_check_eq(node.itemId, item);
+ do_check_true(node.dateAdded > 0);
+ do_check_true(node.lastModified > 0);
+ do_check_eq(node.title, "test bookmark");
+ node = rootNode.getChild(2);
+ do_check_eq(node.itemId, folder);
+ do_check_eq(node.title, "test folder");
+ do_check_true(node.dateAdded > 0);
+ do_check_true(node.lastModified > 0);
+ node = rootNode.getChild(3);
+ do_check_eq(node.itemId, folderLongName);
+ do_check_eq(node.title, longName.substring(0, TITLE_LENGTH_MAX));
+ do_check_true(node.dateAdded > 0);
+ do_check_true(node.lastModified > 0);
+
+ // update with another long title
+ bs.setItemTitle(folderLongName, longName + " updated");
+ do_check_eq(bookmarksObserver._itemChangedId, folderLongName);
+ do_check_eq(bookmarksObserver._itemChangedProperty, "title");
+ do_check_eq(bookmarksObserver._itemChangedValue, longName.substring(0, TITLE_LENGTH_MAX));
+
+ node = rootNode.getChild(3);
+ do_check_eq(node.title, longName.substring(0, TITLE_LENGTH_MAX));
+
+ rootNode.containerOpen = false;
+}
+
+function getChildCount(aFolderId) {
+ let cc = -1;
+ try {
+ let options = hs.getNewQueryOptions();
+ let query = hs.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ let result = hs.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ cc = rootNode.childCount;
+ rootNode.containerOpen = false;
+ } catch (ex) {
+ do_throw("getChildCount failed: " + ex);
+ }
+ return cc;
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
new file mode 100644
index 0000000000..e8414359b5
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* test_eraseEverything() {
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://example.com/") });
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/") });
+ let frecencyForExample = frecencyForUrl("http://example.com/");
+ let frecencyForMozilla = frecencyForUrl("http://example.com/");
+ Assert.ok(frecencyForExample > 0);
+ Assert.ok(frecencyForMozilla > 0);
+ let unfiledFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(unfiledFolder);
+ let unfiledBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/" });
+ checkBookmarkObject(unfiledBookmark);
+ let unfiledBookmarkInFolder =
+ yield PlacesUtils.bookmarks.insert({ parentGuid: unfiledFolder.guid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://mozilla.org/" });
+ checkBookmarkObject(unfiledBookmarkInFolder);
+ PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(unfiledBookmarkInFolder.guid)),
+ "testanno1", "testvalue1", 0, 0);
+
+ let menuFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(menuFolder);
+ let menuBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/" });
+ checkBookmarkObject(menuBookmark);
+ let menuBookmarkInFolder =
+ yield PlacesUtils.bookmarks.insert({ parentGuid: menuFolder.guid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://mozilla.org/" });
+ checkBookmarkObject(menuBookmarkInFolder);
+ PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(menuBookmarkInFolder.guid)),
+ "testanno1", "testvalue1", 0, 0);
+
+ let toolbarFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(toolbarFolder);
+ let toolbarBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/" });
+ checkBookmarkObject(toolbarBookmark);
+ let toolbarBookmarkInFolder =
+ yield PlacesUtils.bookmarks.insert({ parentGuid: toolbarFolder.guid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://mozilla.org/" });
+ checkBookmarkObject(toolbarBookmarkInFolder);
+ PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(toolbarBookmarkInFolder.guid)),
+ "testanno1", "testvalue1", 0, 0);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ Assert.ok(frecencyForUrl("http://example.com/") > frecencyForExample);
+ Assert.ok(frecencyForUrl("http://example.com/") > frecencyForMozilla);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ Assert.equal(frecencyForUrl("http://example.com/"), frecencyForExample);
+ Assert.equal(frecencyForUrl("http://example.com/"), frecencyForMozilla);
+
+ // Check there are no orphan annotations.
+ let conn = yield PlacesUtils.promiseDBConnection();
+ let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`);
+ // Bug 1306445 will eventually remove the mobile root anno.
+ Assert.equal(annoAttrs.length, 1);
+ Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO);
+ let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`);
+ Assert.equal(annos.length, 1);
+ Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId);
+ Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id"));
+});
+
+add_task(function* test_eraseEverything_roots() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Ensure the roots have not been removed.
+ Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid));
+ Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid));
+ Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.menuGuid));
+ Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.tagsGuid));
+ Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.rootGuid));
+});
+
+add_task(function* test_eraseEverything_reparented() {
+ // Create a folder with 1 bookmark in it...
+ let folder1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder1.guid,
+ url: "http://example.com/"
+ });
+ // ...and a second folder.
+ let folder2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER
+ });
+
+ // Reparent the bookmark to the 2nd folder.
+ bookmark1.parentGuid = folder2.guid;
+ yield PlacesUtils.bookmarks.update(bookmark1);
+
+ // Erase everything.
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // All the above items should no longer be in the GUIDHelper cache.
+ for (let guid of [folder1.guid, bookmark1.guid, folder2.guid]) {
+ yield Assert.rejects(PlacesUtils.promiseItemId(guid),
+ /no item found for the given GUID/);
+ }
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
new file mode 100644
index 0000000000..9527f02e61
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gAccumulator = {
+ get callback() {
+ this.results = [];
+ return result => this.results.push(result);
+ }
+};
+
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.fetch(),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch(null),
+ /Input should be a valid object/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012",
+ parentGuid: "012345678901" }),
+ /The following properties were expected: index/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012",
+ index: 0 }),
+ /The following properties were expected: parentGuid/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({}),
+ /Unexpected number of conditions provided: 0/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012",
+ parentGuid: "012345678901",
+ index: 0 }),
+ /Unexpected number of conditions provided: 2/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012",
+ url: "http://example.com"}),
+ /Unexpected number of conditions provided: 2/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch("test"),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch(123),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "test" }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: null }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: 123 }),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "test",
+ index: 0 }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: null,
+ index: 0 }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: 123,
+ index: 0 }),
+ /Invalid value for property 'parentGuid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012",
+ index: "0" }),
+ /Invalid value for property 'index'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012",
+ index: null }),
+ /Invalid value for property 'index'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012",
+ index: -10 }),
+ /Invalid value for property 'index'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: "http://te st/" }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: null }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: -10 }),
+ /Invalid value for property 'url'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.fetch("123456789012", "test"),
+ /onResult callback must be a valid function/);
+ Assert.throws(() => PlacesUtils.bookmarks.fetch("123456789012", {}),
+ /onResult callback must be a valid function/);
+});
+
+add_task(function* fetch_nonexistent_guid() {
+ let bm = yield PlacesUtils.bookmarks.fetch({ guid: "123456789012" },
+ gAccumulator.callback);
+ Assert.equal(bm, null);
+ Assert.equal(gAccumulator.results.length, 0);
+});
+
+add_task(function* fetch_bookmark() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid,
+ gAccumulator.callback);
+ checkBookmarkObject(bm2);
+ Assert.equal(gAccumulator.results.length, 1);
+ checkBookmarkObject(gAccumulator.results[0]);
+ Assert.deepEqual(gAccumulator.results[0], bm1);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm2.url.href, "http://example.com/");
+ Assert.equal(bm2.title, "a bookmark");
+
+ yield PlacesUtils.bookmarks.remove(bm1.guid);
+});
+
+add_task(function* fetch_bookmar_empty_title() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.index, 0);
+ Assert.ok(!("title" in bm2));
+
+ yield PlacesUtils.bookmarks.remove(bm1.guid);
+});
+
+add_task(function* fetch_folder() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(bm2.title, "a folder");
+ Assert.ok(!("url" in bm2));
+
+ yield PlacesUtils.bookmarks.remove(bm1.guid);
+});
+
+add_task(function* fetch_folder_empty_title() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.index, 0);
+ Assert.ok(!("title" in bm2));
+
+ yield PlacesUtils.bookmarks.remove(bm1.guid);
+});
+
+add_task(function* fetch_separator() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.ok(!("url" in bm2));
+ Assert.ok(!("title" in bm2));
+
+ yield PlacesUtils.bookmarks.remove(bm1.guid);
+});
+
+add_task(function* fetch_byposition_nonexisting_parentGuid() {
+ let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012",
+ index: 0 },
+ gAccumulator.callback);
+ Assert.equal(bm, null);
+ Assert.equal(gAccumulator.results.length, 0);
+});
+
+add_task(function* fetch_byposition_nonexisting_index() {
+ let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 100 },
+ gAccumulator.callback);
+ Assert.equal(bm, null);
+ Assert.equal(gAccumulator.results.length, 0);
+});
+
+add_task(function* fetch_byposition() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch({ parentGuid: bm1.parentGuid,
+ index: bm1.index },
+ gAccumulator.callback);
+ checkBookmarkObject(bm2);
+ Assert.equal(gAccumulator.results.length, 1);
+ checkBookmarkObject(gAccumulator.results[0]);
+ Assert.deepEqual(gAccumulator.results[0], bm1);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm2.url.href, "http://example.com/");
+ Assert.equal(bm2.title, "a bookmark");
+});
+
+add_task(function* fetch_byposition_default_index() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/last",
+ title: "last child" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch({ parentGuid: bm1.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX },
+ gAccumulator.callback);
+ checkBookmarkObject(bm2);
+ Assert.equal(gAccumulator.results.length, 1);
+ checkBookmarkObject(gAccumulator.results[0]);
+ Assert.deepEqual(gAccumulator.results[0], bm1);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 1);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm2.url.href, "http://example.com/last");
+ Assert.equal(bm2.title, "last child");
+
+ yield PlacesUtils.bookmarks.remove(bm1.guid);
+});
+
+add_task(function* fetch_byurl_nonexisting() {
+ let bm = yield PlacesUtils.bookmarks.fetch({ url: "http://nonexisting.com/" },
+ gAccumulator.callback);
+ Assert.equal(bm, null);
+ Assert.equal(gAccumulator.results.length, 0);
+});
+
+add_task(function* fetch_byurl() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://byurl.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm1);
+
+ // Also ensure that fecth-by-url excludes the tags folder.
+ PlacesUtils.tagging.tagURI(uri(bm1.url.href), ["Test Tag"]);
+
+ let bm2 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url },
+ gAccumulator.callback);
+ checkBookmarkObject(bm2);
+ Assert.equal(gAccumulator.results.length, 1);
+ checkBookmarkObject(gAccumulator.results[0]);
+ Assert.deepEqual(gAccumulator.results[0], bm1);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm2.url.href, "http://byurl.com/");
+ Assert.equal(bm2.title, "a bookmark");
+
+ let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://byurl.com/",
+ title: "a bookmark" });
+ let bm4 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url },
+ gAccumulator.callback);
+ checkBookmarkObject(bm4);
+ Assert.deepEqual(bm3, bm4);
+ Assert.equal(gAccumulator.results.length, 2);
+ gAccumulator.results.forEach(checkBookmarkObject);
+ Assert.deepEqual(gAccumulator.results[0], bm4);
+
+ // After an update the returned bookmark should change.
+ yield PlacesUtils.bookmarks.update({ guid: bm1.guid, title: "new title" });
+ let bm5 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url },
+ gAccumulator.callback);
+ checkBookmarkObject(bm5);
+ // Cannot use deepEqual cause lastModified changed.
+ Assert.equal(bm1.guid, bm5.guid);
+ Assert.ok(bm5.lastModified > bm1.lastModified);
+ Assert.equal(gAccumulator.results.length, 2);
+ gAccumulator.results.forEach(checkBookmarkObject);
+ Assert.deepEqual(gAccumulator.results[0], bm5);
+
+ // cleanup
+ PlacesUtils.tagging.untagURI(uri(bm1.url.href), ["Test Tag"]);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js
new file mode 100644
index 0000000000..35166bd95d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js
@@ -0,0 +1,44 @@
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.getRecent(),
+ /numberOfItems argument is required/);
+ Assert.throws(() => PlacesUtils.bookmarks.getRecent("abc"),
+ /numberOfItems argument must be an integer/);
+ Assert.throws(() => PlacesUtils.bookmarks.getRecent(1.2),
+ /numberOfItems argument must be an integer/);
+ Assert.throws(() => PlacesUtils.bookmarks.getRecent(0),
+ /numberOfItems argument must be greater than zero/);
+ Assert.throws(() => PlacesUtils.bookmarks.getRecent(-1),
+ /numberOfItems argument must be greater than zero/);
+});
+
+add_task(function* getRecent_returns_recent_bookmarks() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/path",
+ title: "another bookmark" });
+ let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.net/",
+ title: "another bookmark" });
+ let bm4 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.net/path",
+ title: "yet another bookmark" });
+ checkBookmarkObject(bm1);
+ checkBookmarkObject(bm2);
+ checkBookmarkObject(bm3);
+ checkBookmarkObject(bm4);
+
+ let results = yield PlacesUtils.bookmarks.getRecent(3);
+ Assert.equal(results.length, 3);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm4, results[0]);
+ checkBookmarkObject(results[1]);
+ Assert.deepEqual(bm3, results[1]);
+ checkBookmarkObject(results[2]);
+ Assert.deepEqual(bm2, results[2]);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
new file mode 100644
index 0000000000..0f772a92fc
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.insert(),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert(null),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({}),
+ /The following properties were expected/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ guid: "test" }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ guid: null }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ guid: 123 }),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ parentGuid: "test" }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ parentGuid: null }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ parentGuid: 123 }),
+ /Invalid value for property 'parentGuid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ index: "1" }),
+ /Invalid value for property 'index'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ index: -10 }),
+ /Invalid value for property 'index'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: -10 }),
+ /Invalid value for property 'dateAdded'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: "today" }),
+ /Invalid value for property 'dateAdded'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: Date.now() }),
+ /Invalid value for property 'dateAdded'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: -10 }),
+ /Invalid value for property 'lastModified'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: "today" }),
+ /Invalid value for property 'lastModified'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: Date.now() }),
+ /Invalid value for property 'lastModified'/);
+ let time = new Date();
+ let future = new Date(time + 86400000);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ dateAdded: future,
+ lastModified: time }),
+ /Invalid value for property 'dateAdded'/);
+ let past = new Date(time - 86400000);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ lastModified: past }),
+ /Invalid value for property 'lastModified'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: -1 }),
+ /Invalid value for property 'type'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: 100 }),
+ /Invalid value for property 'type'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: "bookmark" }),
+ /Invalid value for property 'type'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: -1 }),
+ /Invalid value for property 'title'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: 10 }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://te st" }),
+ /Invalid value for property 'url'/);
+ let longurl = "http://www.example.com/";
+ for (let i = 0; i < 65536; i++) {
+ longurl += "a";
+ }
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: longurl }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: NetUtil.newURI(longurl) }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "te st" }),
+ /Invalid value for property 'url'/);
+});
+
+add_task(function* invalid_properties_for_bookmark_type() {
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ url: "http://www.moz.com/" }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ url: "http://www.moz.com/" }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ title: "test" }),
+ /Invalid value for property 'title'/);
+});
+
+add_task(function* long_title_trim() {
+ let longtitle = "a";
+ for (let i = 0; i < 4096; i++) {
+ longtitle += "a";
+ }
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: longtitle });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm.index, 0);
+ Assert.equal(bm.dateAdded, bm.lastModified);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(bm.title.length, 4096, "title should have been trimmed");
+ Assert.ok(!("url" in bm), "url should not be set");
+});
+
+add_task(function* create_separator() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm.index, 1);
+ Assert.equal(bm.dateAdded, bm.lastModified);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.ok(!("title" in bm), "title should not be set");
+});
+
+add_task(function* create_separator_w_title_fail() {
+ try {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ title: "a separator" });
+ Assert.ok(false, "Trying to set title for a separator should reject");
+ } catch (ex) {}
+});
+
+add_task(function* create_separator_invalid_parent_fail() {
+ try {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: "123456789012",
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ title: "a separator" });
+ Assert.ok(false, "Trying to create an item in a non existing parent reject");
+ } catch (ex) {}
+});
+
+add_task(function* create_separator_given_guid() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ guid: "123456789012" });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.guid, "123456789012");
+ Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm.index, 2);
+ Assert.equal(bm.dateAdded, bm.lastModified);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.ok(!("title" in bm), "title should not be set");
+});
+
+add_task(function* create_item_given_guid_no_type_fail() {
+ try {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: "123456789012" });
+ Assert.ok(false, "Trying to create an item with a given guid but no type should reject");
+ } catch (ex) {}
+});
+
+add_task(function* create_separator_big_index() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 9999 });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm.index, 3);
+ Assert.equal(bm.dateAdded, bm.lastModified);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.ok(!("title" in bm), "title should not be set");
+});
+
+add_task(function* create_separator_given_dateAdded() {
+ let time = new Date();
+ let past = new Date(time - 86400000);
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ dateAdded: past });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.dateAdded, past);
+ Assert.equal(bm.lastModified, past);
+});
+
+add_task(function* create_folder() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm.dateAdded, bm.lastModified);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.ok(!("title" in bm), "title should not be set");
+
+ // And then create a nested folder.
+ let parentGuid = bm.guid;
+ bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, parentGuid);
+ Assert.equal(bm.index, 0);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.strictEqual(bm.title, "a folder");
+});
+
+add_task(function* create_bookmark() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ let parentGuid = bm.guid;
+
+ bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, parentGuid);
+ Assert.equal(bm.index, 0);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm.url.href, "http://example.com/");
+ Assert.equal(bm.title, "a bookmark");
+
+ // Check parent lastModified.
+ let parent = yield PlacesUtils.bookmarks.fetch({ guid: bm.parentGuid });
+ Assert.deepEqual(parent.lastModified, bm.dateAdded);
+
+ bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: new URL("http://example.com/") });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, parentGuid);
+ Assert.equal(bm.index, 1);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm.url.href, "http://example.com/");
+ Assert.ok(!("title" in bm), "title should not be set");
+});
+
+add_task(function* create_bookmark_frecency() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ Assert.ok(frecencyForUrl(bm.url) > 0, "Check frecency has been updated")
+});
+
+add_task(function* create_bookmark_without_type() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm.url.href, "http://example.com/");
+ Assert.equal(bm.title, "a bookmark");
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
new file mode 100644
index 0000000000..02787425d9
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -0,0 +1,527 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* insert_separator_notification() {
+ let observer = expectNotifications();
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid});
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+ observer.check([ { name: "onItemAdded",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ null, null, bm.dateAdded,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* insert_folder_notification() {
+ let observer = expectNotifications();
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "a folder" });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+ observer.check([ { name: "onItemAdded",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ null, bm.title, bm.dateAdded,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* insert_folder_notitle_notification() {
+ let observer = expectNotifications();
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+ observer.check([ { name: "onItemAdded",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ null, null, bm.dateAdded,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* insert_bookmark_notification() {
+ let observer = expectNotifications();
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://example.com/"),
+ title: "a bookmark" });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+ observer.check([ { name: "onItemAdded",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ bm.url, bm.title, bm.dateAdded,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* insert_bookmark_notitle_notification() {
+ let observer = expectNotifications();
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://example.com/") });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+ observer.check([ { name: "onItemAdded",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ bm.url, null, bm.dateAdded,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* insert_bookmark_tag_notification() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://tag.example.com/") });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ title: "tag" });
+ let observer = expectNotifications();
+ let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: tagFolder.guid,
+ url: new URL("http://tag.example.com/") });
+ let tagId = yield PlacesUtils.promiseItemId(tag.guid);
+ let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid);
+
+ observer.check([ { name: "onItemAdded",
+ arguments: [ tagId, tagParentId, tag.index, tag.type,
+ tag.url, null, tag.dateAdded,
+ tag.guid, tag.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ itemId, "tags", false, "",
+ bm.lastModified, bm.type, parentId,
+ bm.guid, bm.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* update_bookmark_lastModified() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://lastmod.example.com/") });
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ lastModified: new Date() });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ observer.check([ { name: "onItemChanged",
+ arguments: [ itemId, "lastModified", false,
+ `${bm.lastModified * 1000}`, bm.lastModified,
+ bm.type, parentId, bm.guid, bm.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* update_bookmark_title() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://title.example.com/") });
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ title: "new title" });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ observer.check([ { name: "onItemChanged",
+ arguments: [ itemId, "title", false, bm.title,
+ bm.lastModified, bm.type, parentId, bm.guid,
+ bm.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* update_bookmark_uri() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://url.example.com/") });
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ url: "http://mozilla.org/" });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ observer.check([ { name: "onItemChanged",
+ arguments: [ itemId, "uri", false, bm.url.href,
+ bm.lastModified, bm.type, parentId, bm.guid,
+ bm.parentGuid, "http://url.example.com/",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* update_move_same_folder() {
+ // Ensure there are at least two items in place (others test do so for us,
+ // but we don't have to depend on that).
+ yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://move.example.com/") });
+ let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let bmParentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+ let bmOldIndex = bm.index;
+
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0 });
+ Assert.equal(bm.index, 0);
+ observer.check([ { name: "onItemMoved",
+ arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index,
+ bm.type, bm.guid, bm.parentGuid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+
+ // Test that we get the right index for DEFAULT_INDEX input.
+ bmOldIndex = 0;
+ observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX });
+ Assert.ok(bm.index > 0);
+ observer.check([ { name: "onItemMoved",
+ arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index,
+ bm.type, bm.guid, bm.parentGuid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* update_move_different_folder() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://move.example.com/") });
+ let folder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let bmOldParentId = PlacesUtils.unfiledBookmarksFolderId;
+ let bmOldIndex = bm.index;
+
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ parentGuid: folder.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX });
+ Assert.equal(bm.index, 0);
+ let bmNewParentId = yield PlacesUtils.promiseItemId(folder.guid);
+ observer.check([ { name: "onItemMoved",
+ arguments: [ bmItemId, bmOldParentId, bmOldIndex, bmNewParentId,
+ bm.index, bm.type, bm.guid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* remove_bookmark() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://remove.example.com/") });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.remove(bm.guid);
+ // TODO (Bug 653910): onItemAnnotationRemoved notified even if there were no
+ // annotations.
+ observer.check([ { name: "onItemRemoved",
+ arguments: [ itemId, parentId, bm.index, bm.type, bm.url,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* remove_folder() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ let observer = expectNotifications();
+ bm = yield PlacesUtils.bookmarks.remove(bm.guid);
+ observer.check([ { name: "onItemRemoved",
+ arguments: [ itemId, parentId, bm.index, bm.type, null,
+ bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* remove_bookmark_tag_notification() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: new URL("http://untag.example.com/") });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ title: "tag" });
+ let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: tagFolder.guid,
+ url: new URL("http://untag.example.com/") });
+ let tagId = yield PlacesUtils.promiseItemId(tag.guid);
+ let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid);
+
+ let observer = expectNotifications();
+ yield PlacesUtils.bookmarks.remove(tag.guid);
+
+ observer.check([ { name: "onItemRemoved",
+ arguments: [ tagId, tagParentId, tag.index, tag.type,
+ tag.url, tag.guid, tag.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ itemId, "tags", false, "",
+ bm.lastModified, bm.type, parentId,
+ bm.guid, bm.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* remove_folder_notification() {
+ let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
+ let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
+
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: folder1.guid,
+ url: new URL("http://example.com/") });
+ let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
+
+ let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: folder1.guid });
+ let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);
+
+ let bm2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: folder2.guid,
+ url: new URL("http://example.com/") });
+ let bm2ItemId = yield PlacesUtils.promiseItemId(bm2.guid);
+
+ let observer = expectNotifications();
+ yield PlacesUtils.bookmarks.remove(folder1.guid);
+
+ observer.check([ { name: "onItemRemoved",
+ arguments: [ bm2ItemId, folder2Id, bm2.index, bm2.type,
+ bm2.url, bm2.guid, bm2.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ folder2Id, folder1Id, folder2.index,
+ folder2.type, null, folder2.guid,
+ folder2.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ bmItemId, folder1Id, bm.index, bm.type,
+ bm.url, bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ folder1Id, folder1ParentId, folder1.index,
+ folder1.type, null, folder1.guid,
+ folder1.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+});
+
+add_task(function* eraseEverything_notification() {
+ // Let's start from a clean situation.
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
+ let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
+
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: folder1.guid,
+ url: new URL("http://example.com/") });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);
+ let folder2ParentId = yield PlacesUtils.promiseItemId(folder2.parentGuid);
+
+ let toolbarBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: new URL("http://example.com/") });
+ let toolbarBmId = yield PlacesUtils.promiseItemId(toolbarBm.guid);
+ let toolbarBmParentId = yield PlacesUtils.promiseItemId(toolbarBm.parentGuid);
+
+ let menuBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: new URL("http://example.com/") });
+ let menuBmId = yield PlacesUtils.promiseItemId(menuBm.guid);
+ let menuBmParentId = yield PlacesUtils.promiseItemId(menuBm.parentGuid);
+
+ let observer = expectNotifications();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Bookmarks should always be notified before their parents.
+ observer.check([ { name: "onItemRemoved",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ bm.url, bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ folder2Id, folder2ParentId, folder2.index,
+ folder2.type, null, folder2.guid,
+ folder2.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ folder1Id, folder1ParentId, folder1.index,
+ folder1.type, null, folder1.guid,
+ folder1.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ menuBmId, menuBmParentId,
+ menuBm.index, menuBm.type,
+ menuBm.url, menuBm.guid,
+ menuBm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ toolbarBmId, toolbarBmParentId,
+ toolbarBm.index, toolbarBm.type,
+ toolbarBm.url, toolbarBm.guid,
+ toolbarBm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ ]);
+});
+
+add_task(function* eraseEverything_reparented_notification() {
+ // Let's start from a clean situation.
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
+ let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
+
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: folder1.guid,
+ url: new URL("http://example.com/") });
+ let itemId = yield PlacesUtils.promiseItemId(bm.guid);
+
+ let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);
+ let folder2ParentId = yield PlacesUtils.promiseItemId(folder2.parentGuid);
+
+ bm.parentGuid = folder2.guid;
+ bm = yield PlacesUtils.bookmarks.update(bm);
+ let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
+
+ let observer = expectNotifications();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Bookmarks should always be notified before their parents.
+ observer.check([ { name: "onItemRemoved",
+ arguments: [ itemId, parentId, bm.index, bm.type,
+ bm.url, bm.guid, bm.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ folder2Id, folder2ParentId, folder2.index,
+ folder2.type, null, folder2.guid,
+ folder2.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemRemoved",
+ arguments: [ folder1Id, folder1ParentId, folder1.index,
+ folder1.type, null, folder1.guid,
+ folder1.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ ]);
+});
+
+add_task(function* reorder_notification() {
+ let bookmarks = [
+ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example1.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example2.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example3.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ ];
+ let sorted = [];
+ for (let bm of bookmarks) {
+ sorted.push(yield PlacesUtils.bookmarks.insert(bm));
+ }
+
+ // Randomly reorder the array.
+ sorted.sort(() => 0.5 - Math.random());
+
+ let observer = expectNotifications();
+ yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid,
+ sorted.map(bm => bm.guid));
+
+ let expectedNotifications = [];
+ for (let i = 0; i < sorted.length; ++i) {
+ let child = sorted[i];
+ let childId = yield PlacesUtils.promiseItemId(child.guid);
+ expectedNotifications.push({ name: "onItemMoved",
+ arguments: [ childId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ child.index,
+ PlacesUtils.unfiledBookmarksFolderId,
+ i,
+ child.type,
+ child.guid,
+ child.parentGuid,
+ child.parentGuid,
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT
+ ] });
+ }
+ observer.check(expectedNotifications);
+});
+
+function expectNotifications() {
+ let notifications = [];
+ let observer = new Proxy(NavBookmarkObserver, {
+ get(target, name) {
+ if (name == "check") {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ return expectedNotifications =>
+ Assert.deepEqual(notifications, expectedNotifications);
+ }
+
+ if (name.startsWith("onItem")) {
+ return (...origArgs) => {
+ let args = Array.from(origArgs, arg => {
+ if (arg && arg instanceof Ci.nsIURI)
+ return new URL(arg.spec);
+ if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
+ return new Date(parseInt(arg/1000));
+ return arg;
+ });
+ notifications.push({ name: name, arguments: args });
+ }
+ }
+
+ if (name in target)
+ return target[name];
+ return undefined;
+ }
+ });
+ PlacesUtils.bookmarks.addObserver(observer, false);
+ return observer;
+}
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
new file mode 100644
index 0000000000..19085a2823
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.remove(),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove(null),
+ /Input should be a valid object/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.remove("test"),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove(123),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ guid: "test" }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ guid: null }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ guid: 123 }),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ parentGuid: "test" }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ parentGuid: null }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ parentGuid: 123 }),
+ /Invalid value for property 'parentGuid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ url: "http://te st/" }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ url: null }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.remove({ url: -10 }),
+ /Invalid value for property 'url'/);
+});
+
+add_task(function* remove_nonexistent_guid() {
+ try {
+ yield PlacesUtils.bookmarks.remove({ guid: "123456789012"});
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/No bookmarks found for the provided GUID/.test(ex));
+ }
+});
+
+add_task(function* remove_roots_fail() {
+ let guids = [PlacesUtils.bookmarks.rootGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.tagsGuid,
+ PlacesUtils.bookmarks.mobileGuid];
+ for (let guid of guids) {
+ Assert.throws(() => PlacesUtils.bookmarks.remove(guid),
+ /It's not possible to remove Places root folders/);
+ }
+});
+
+add_task(function* remove_normal_folder_under_root_succeeds() {
+ let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(folder);
+ let removed_folder = yield PlacesUtils.bookmarks.remove(folder);
+ Assert.deepEqual(folder, removed_folder);
+ Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder.guid)), null);
+});
+
+add_task(function* remove_bookmark() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ Assert.equal(bm2.url.href, "http://example.com/");
+ Assert.equal(bm2.title, "a bookmark");
+});
+
+
+add_task(function* remove_bookmark_orphans() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(bm1);
+ PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(bm1.guid)),
+ "testanno", "testvalue", 0, 0);
+
+ let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ // Check there are no orphan annotations.
+ let conn = yield PlacesUtils.promiseDBConnection();
+ let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`);
+ // Bug 1306445 will eventually remove the mobile root anno.
+ Assert.equal(annoAttrs.length, 1);
+ Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO);
+ let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`);
+ Assert.equal(annos.length, 1);
+ Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId);
+ Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id"));
+});
+
+add_task(function* remove_bookmark_empty_title() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.index, 0);
+ Assert.ok(!("title" in bm2));
+});
+
+add_task(function* remove_folder() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+ Assert.equal(bm2.title, "a folder");
+ Assert.ok(!("url" in bm2));
+});
+
+add_task(function* test_nested_contents_removed() {
+ let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ let folder2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ let sep = yield PlacesUtils.bookmarks.insert({ parentGuid: folder2.guid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+ yield PlacesUtils.bookmarks.remove(folder1);
+ Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder1.guid)), null);
+ Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder2.guid)), null);
+ Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(sep.guid)), null);
+});
+
+add_task(function* remove_folder_empty_title() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "" });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.index, 0);
+ Assert.ok(!("title" in bm2));
+});
+
+add_task(function* remove_separator() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+ checkBookmarkObject(bm1);
+
+ let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
+ Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ Assert.ok(!("url" in bm2));
+ Assert.ok(!("title" in bm2));
+});
+
+add_task(function* test_nested_content_fails_when_not_allowed() {
+ let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder" });
+ yield Assert.rejects(PlacesUtils.bookmarks.remove(folder1, {preventRemovalOfNonEmptyFolders: true}),
+ /Cannot remove a non-empty folder./);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js
new file mode 100644
index 0000000000..4f66172802
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.reorder(),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder(null),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("test"),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder(123),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.reorder({ guid: "test" }),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012"),
+ /Must provide a sorted array of children GUIDs./);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", {}),
+ /Must provide a sorted array of children GUIDs./);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", null),
+ /Must provide a sorted array of children GUIDs./);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", []),
+ /Must provide a sorted array of children GUIDs./);
+
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ null ]),
+ /Invalid GUID found in the sorted children array/);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ "" ]),
+ /Invalid GUID found in the sorted children array/);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ {} ]),
+ /Invalid GUID found in the sorted children array/);
+ Assert.throws(() => PlacesUtils.bookmarks.reorder("123456789012", [ "012345678901", null ]),
+ /Invalid GUID found in the sorted children array/);
+});
+
+add_task(function* reorder_nonexistent_guid() {
+ yield Assert.rejects(PlacesUtils.bookmarks.reorder("123456789012", [ "012345678901" ]),
+ /No folder found for the provided GUID/,
+ "Should throw for nonexisting guid");
+});
+
+add_task(function* reorder() {
+ let bookmarks = [
+ { url: "http://example1.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { url: "http://example2.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ },
+ { url: "http://example3.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ }
+ ];
+
+ let sorted = [];
+ for (let bm of bookmarks) {
+ sorted.push(yield PlacesUtils.bookmarks.insert(bm));
+ }
+
+ // Check the initial append sorting.
+ Assert.ok(sorted.every((bm, i) => bm.index == i),
+ "Initial bookmarks sorting is correct");
+
+ // Apply random sorting and run multiple tests.
+ for (let t = 0; t < 4; t++) {
+ sorted.sort(() => 0.5 - Math.random());
+ let sortedGuids = sorted.map(child => child.guid);
+ dump("Expected order: " + sortedGuids.join() + "\n");
+ // Add a nonexisting guid to the array, to ensure nothing will break.
+ sortedGuids.push("123456789012");
+ yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid,
+ sortedGuids);
+ for (let i = 0; i < sorted.length; ++i) {
+ let item = yield PlacesUtils.bookmarks.fetch(sorted[i].guid);
+ Assert.equal(item.index, i);
+ }
+ }
+
+ do_print("Test partial sorting");
+ // Try a partial sorting by passing only 2 entries.
+ // The unspecified entries should retain the original order.
+ sorted = [ sorted[1], sorted[0] ].concat(sorted.slice(2));
+ let sortedGuids = [ sorted[0].guid, sorted[1].guid ];
+ dump("Expected order: " + sorted.map(b => b.guid).join() + "\n");
+ yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid,
+ sortedGuids);
+ for (let i = 0; i < sorted.length; ++i) {
+ let item = yield PlacesUtils.bookmarks.fetch(sorted[i].guid);
+ Assert.equal(item.index, i);
+ }
+
+ // Use triangular numbers to detect skipped position.
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(
+ `SELECT parent
+ FROM moz_bookmarks
+ GROUP BY parent
+ HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0`);
+ Assert.equal(rows.length, 0, "All the bookmarks should have consistent positions");
+});
+
+add_task(function* move_and_reorder() {
+ // Start clean.
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ let bm1 = yield PlacesUtils.bookmarks.insert({
+ url: "http://example1.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ let f1 = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ let bm2 = yield PlacesUtils.bookmarks.insert({
+ url: "http://example2.com/",
+ parentGuid: f1.guid
+ });
+ let f2 = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ let bm3 = yield PlacesUtils.bookmarks.insert({
+ url: "http://example3.com/",
+ parentGuid: f2.guid
+ });
+ let bm4 = yield PlacesUtils.bookmarks.insert({
+ url: "http://example4.com/",
+ parentGuid: f2.guid
+ });
+ let bm5 = yield PlacesUtils.bookmarks.insert({
+ url: "http://example5.com/",
+ parentGuid: f2.guid
+ });
+
+ // Invert f2 children.
+ // This is critical to reproduce the bug, cause it inverts the position
+ // compared to the natural insertion order.
+ yield PlacesUtils.bookmarks.reorder(f2.guid, [bm5.guid, bm4.guid, bm3.guid]);
+
+ bm1.parentGuid = f1.guid;
+ bm1.index = 0;
+ yield PlacesUtils.bookmarks.update(bm1);
+
+ bm1 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+ Assert.equal(bm1.index, 0);
+ bm2 = yield PlacesUtils.bookmarks.fetch(bm2.guid);
+ Assert.equal(bm2.index, 1);
+ bm3 = yield PlacesUtils.bookmarks.fetch(bm3.guid);
+ Assert.equal(bm3.index, 2);
+ bm4 = yield PlacesUtils.bookmarks.fetch(bm4.guid);
+ Assert.equal(bm4.index, 1);
+ bm5 = yield PlacesUtils.bookmarks.fetch(bm5.guid);
+ Assert.equal(bm5.index, 0);
+
+ // No-op reorder on f1 children.
+ // Nothing should change. Though, due to bug 1293365 this was causing children
+ // of other folders to get messed up.
+ yield PlacesUtils.bookmarks.reorder(f1.guid, [bm1.guid, bm2.guid]);
+
+ bm1 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
+ Assert.equal(bm1.index, 0);
+ bm2 = yield PlacesUtils.bookmarks.fetch(bm2.guid);
+ Assert.equal(bm2.index, 1);
+ bm3 = yield PlacesUtils.bookmarks.fetch(bm3.guid);
+ Assert.equal(bm3.index, 2);
+ bm4 = yield PlacesUtils.bookmarks.fetch(bm4.guid);
+ Assert.equal(bm4.index, 1);
+ bm5 = yield PlacesUtils.bookmarks.fetch(bm5.guid);
+ Assert.equal(bm5.index, 0);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js
new file mode 100644
index 0000000000..02f7c5460a
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js
@@ -0,0 +1,223 @@
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.search(),
+ /Query object is required/);
+ Assert.throws(() => PlacesUtils.bookmarks.search(null),
+ /Query object is required/);
+ Assert.throws(() => PlacesUtils.bookmarks.search({title: 50}),
+ /Title option must be a string/);
+ Assert.throws(() => PlacesUtils.bookmarks.search({url: {url: "wombat"}}),
+ /Url option must be a string or a URL object/);
+ Assert.throws(() => PlacesUtils.bookmarks.search(50),
+ /Query must be an object or a string/);
+ Assert.throws(() => PlacesUtils.bookmarks.search(true),
+ /Query must be an object or a string/);
+});
+
+add_task(function* search_bookmark() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/",
+ title: "another bookmark" });
+ let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://menu.org/",
+ title: "an on-menu bookmark" });
+ let bm4 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://toolbar.org/",
+ title: "an on-toolbar bookmark" });
+ checkBookmarkObject(bm1);
+ checkBookmarkObject(bm2);
+ checkBookmarkObject(bm3);
+ checkBookmarkObject(bm4);
+
+ // finds a result by query
+ let results = yield PlacesUtils.bookmarks.search("example.com");
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm1, results[0]);
+
+ // finds multiple results
+ results = yield PlacesUtils.bookmarks.search("example");
+ Assert.equal(results.length, 2);
+ checkBookmarkObject(results[0]);
+ checkBookmarkObject(results[1]);
+
+ // finds menu bookmarks
+ results = yield PlacesUtils.bookmarks.search("an on-menu bookmark");
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm3, results[0]);
+
+ // finds toolbar bookmarks
+ results = yield PlacesUtils.bookmarks.search("an on-toolbar bookmark");
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm4, results[0]);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* search_bookmark_by_query_object() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/",
+ title: "another bookmark" });
+ checkBookmarkObject(bm1);
+ checkBookmarkObject(bm2);
+
+ let results = yield PlacesUtils.bookmarks.search({query: "example.com"});
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+
+ Assert.deepEqual(bm1, results[0]);
+
+ results = yield PlacesUtils.bookmarks.search({query: "example"});
+ Assert.equal(results.length, 2);
+ checkBookmarkObject(results[0]);
+ checkBookmarkObject(results[1]);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* search_bookmark_by_url() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/path",
+ title: "another bookmark" });
+ let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/path",
+ title: "third bookmark" });
+ checkBookmarkObject(bm1);
+ checkBookmarkObject(bm2);
+ checkBookmarkObject(bm3);
+
+ // finds the correct result by url
+ let results = yield PlacesUtils.bookmarks.search({url: "http://example.com/"});
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm1, results[0]);
+
+ // normalizes the url
+ results = yield PlacesUtils.bookmarks.search({url: "http:/example.com"});
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm1, results[0]);
+
+ // returns multiple matches
+ results = yield PlacesUtils.bookmarks.search({url: "http://example.org/path"});
+ Assert.equal(results.length, 2);
+
+ // requires exact match
+ results = yield PlacesUtils.bookmarks.search({url: "http://example.org/"});
+ Assert.equal(results.length, 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* search_bookmark_by_title() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/path",
+ title: "another bookmark" });
+ let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.net/",
+ title: "another bookmark" });
+ checkBookmarkObject(bm1);
+ checkBookmarkObject(bm2);
+ checkBookmarkObject(bm3);
+
+ // finds the correct result by title
+ let results = yield PlacesUtils.bookmarks.search({title: "a bookmark"});
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm1, results[0]);
+
+ // returns multiple matches
+ results = yield PlacesUtils.bookmarks.search({title: "another bookmark"});
+ Assert.equal(results.length, 2);
+
+ // requires exact match
+ results = yield PlacesUtils.bookmarks.search({title: "bookmark"});
+ Assert.equal(results.length, 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* search_bookmark_combinations() {
+ let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.org/path",
+ title: "another bookmark" });
+ let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.net/",
+ title: "third bookmark" });
+ checkBookmarkObject(bm1);
+ checkBookmarkObject(bm2);
+ checkBookmarkObject(bm3);
+
+ // finds the correct result if title and url match
+ let results = yield PlacesUtils.bookmarks.search({url: "http://example.com/", title: "a bookmark"});
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm1, results[0]);
+
+ // does not match if query is not matching but url and title match
+ results = yield PlacesUtils.bookmarks.search({url: "http://example.com/", title: "a bookmark", query: "nonexistent"});
+ Assert.equal(results.length, 0);
+
+ // does not match if one parameter is not matching
+ results = yield PlacesUtils.bookmarks.search({url: "http://what.ever", title: "a bookmark"});
+ Assert.equal(results.length, 0);
+
+ // query only matches if other fields match as well
+ results = yield PlacesUtils.bookmarks.search({query: "bookmark", url: "http://example.net/"});
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm3, results[0]);
+
+ // non-matching query will also return no results
+ results = yield PlacesUtils.bookmarks.search({query: "nonexistent", url: "http://example.net/"});
+ Assert.equal(results.length, 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* search_folder() {
+ let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a test folder" });
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: folder.guid,
+ url: "http://example.com/",
+ title: "a bookmark" });
+ checkBookmarkObject(folder);
+ checkBookmarkObject(bm);
+
+ // also finds folders
+ let results = yield PlacesUtils.bookmarks.search("a test folder");
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.equal(folder.title, results[0].title);
+ Assert.equal(folder.type, results[0].type);
+ Assert.equal(folder.parentGuid, results[0].parentGuid);
+
+ // finds elements in folders
+ results = yield PlacesUtils.bookmarks.search("example.com");
+ Assert.equal(results.length, 1);
+ checkBookmarkObject(results[0]);
+ Assert.deepEqual(bm, results[0]);
+ Assert.equal(folder.guid, results[0].parentGuid);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
new file mode 100644
index 0000000000..d077fd6f3d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
@@ -0,0 +1,414 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* invalid_input_throws() {
+ Assert.throws(() => PlacesUtils.bookmarks.update(),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.bookmarks.update(null),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({}),
+ /The following properties were expected/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "test" }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ guid: null }),
+ /Invalid value for property 'guid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ guid: 123 }),
+ /Invalid value for property 'guid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ parentGuid: "test" }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ parentGuid: null }),
+ /Invalid value for property 'parentGuid'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ parentGuid: 123 }),
+ /Invalid value for property 'parentGuid'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ index: "1" }),
+ /Invalid value for property 'index'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ index: -10 }),
+ /Invalid value for property 'index'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ dateAdded: -10 }),
+ /Invalid value for property 'dateAdded'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ dateAdded: "today" }),
+ /Invalid value for property 'dateAdded'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ dateAdded: Date.now() }),
+ /Invalid value for property 'dateAdded'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ lastModified: -10 }),
+ /Invalid value for property 'lastModified'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ lastModified: "today" }),
+ /Invalid value for property 'lastModified'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ lastModified: Date.now() }),
+ /Invalid value for property 'lastModified'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ type: -1 }),
+ /Invalid value for property 'type'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ type: 100 }),
+ /Invalid value for property 'type'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ type: "bookmark" }),
+ /Invalid value for property 'type'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ url: 10 }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ url: "http://te st" }),
+ /Invalid value for property 'url'/);
+ let longurl = "http://www.example.com/";
+ for (let i = 0; i < 65536; i++) {
+ longurl += "a";
+ }
+ Assert.throws(() => PlacesUtils.bookmarks.update({ url: longurl }),
+ /Invalid value for property 'url'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ url: NetUtil.newURI(longurl) }),
+ /Invalid value for property 'url'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ url: "te st" }),
+ /Invalid value for property 'url'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ title: -1 }),
+ /Invalid value for property 'title'/);
+ Assert.throws(() => PlacesUtils.bookmarks.update({ title: {} }),
+ /Invalid value for property 'title'/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012" }),
+ /Not enough properties to update/);
+
+ Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012",
+ parentGuid: "012345678901" }),
+ /The following properties were expected: index/);
+});
+
+add_task(function* nonexisting_bookmark_throws() {
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: "123456789012",
+ title: "test" });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/No bookmarks found for the provided GUID/.test(ex));
+ }
+});
+
+add_task(function* invalid_properties_for_existing_bookmark() {
+ let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/" });
+
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/The bookmark type cannot be changed/.test(ex));
+ }
+
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ dateAdded: new Date() });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/The bookmark dateAdded cannot be changed/.test(ex));
+ }
+
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ dateAdded: new Date() });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/The bookmark dateAdded cannot be changed/.test(ex));
+ }
+
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ parentGuid: "123456789012",
+ index: 1 });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/No bookmarks found for the provided parentGuid/.test(ex));
+ }
+
+ let past = new Date(Date.now() - 86400000);
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ lastModified: past });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/Invalid value for property 'lastModified'/.test(ex));
+ }
+
+ let folder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: folder.guid,
+ url: "http://example.com/" });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/Invalid value for property 'url'/.test(ex));
+ }
+
+ let separator = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: separator.guid,
+ url: "http://example.com/" });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/Invalid value for property 'url'/.test(ex));
+ }
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: separator.guid,
+ title: "test" });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/Invalid value for property 'title'/.test(ex));
+ }
+});
+
+add_task(function* long_title_trim() {
+ let longtitle = "a";
+ for (let i = 0; i < 4096; i++) {
+ longtitle += "a";
+ }
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "title" });
+ checkBookmarkObject(bm);
+
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ title: longtitle });
+ let newTitle = bm.title;
+ Assert.equal(newTitle.length, 4096, "title should have been trimmed");
+
+ bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
+ Assert.equal(bm.title, newTitle);
+});
+
+add_task(function* update_lastModified() {
+ let yesterday = new Date(Date.now() - 86400000);
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "title",
+ dateAdded: yesterday });
+ checkBookmarkObject(bm);
+ Assert.deepEqual(bm.lastModified, yesterday);
+
+ let time = new Date();
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ lastModified: time });
+ checkBookmarkObject(bm);
+ Assert.deepEqual(bm.lastModified, time);
+
+ bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
+ Assert.deepEqual(bm.lastModified, time);
+
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ lastModified: yesterday });
+ Assert.deepEqual(bm.lastModified, yesterday);
+
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ title: "title2" });
+ Assert.ok(bm.lastModified >= time);
+
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ title: "" });
+ Assert.ok(!("title" in bm));
+
+ bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
+ Assert.ok(!("title" in bm));
+});
+
+add_task(function* update_url() {
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "title" });
+ checkBookmarkObject(bm);
+ let lastModified = bm.lastModified;
+ let frecency = frecencyForUrl(bm.url);
+ Assert.ok(frecency > 0, "Check frecency has been updated");
+
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ url: "http://mozilla.org/" });
+ checkBookmarkObject(bm);
+ Assert.ok(bm.lastModified >= lastModified);
+ Assert.equal(bm.url.href, "http://mozilla.org/");
+
+ bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
+ Assert.equal(bm.url.href, "http://mozilla.org/");
+ Assert.ok(bm.lastModified >= lastModified);
+
+ Assert.equal(frecencyForUrl("http://example.com/"), frecency, "Check frecency for example.com");
+ Assert.equal(frecencyForUrl("http://mozilla.org/"), frecency, "Check frecency for mozilla.org");
+});
+
+add_task(function* update_index() {
+ let parent = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER }) ;
+ let f1 = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ Assert.equal(f1.index, 0);
+ let f2 = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ Assert.equal(f2.index, 1);
+ let f3 = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ Assert.equal(f3.index, 2);
+ let lastModified = f1.lastModified;
+
+ f1 = yield PlacesUtils.bookmarks.update({ guid: f1.guid,
+ parentGuid: f1.parentGuid,
+ index: 1});
+ checkBookmarkObject(f1);
+ Assert.equal(f1.index, 1);
+ Assert.ok(f1.lastModified >= lastModified);
+
+ parent = yield PlacesUtils.bookmarks.fetch(f1.parentGuid);
+ Assert.deepEqual(parent.lastModified, f1.lastModified);
+
+ f2 = yield PlacesUtils.bookmarks.fetch(f2.guid);
+ Assert.equal(f2.index, 0);
+
+ f3 = yield PlacesUtils.bookmarks.fetch(f3.guid);
+ Assert.equal(f3.index, 2);
+
+ f3 = yield PlacesUtils.bookmarks.update({ guid: f3.guid,
+ index: 0 });
+ f1 = yield PlacesUtils.bookmarks.fetch(f1.guid);
+ Assert.equal(f1.index, 2);
+
+ f2 = yield PlacesUtils.bookmarks.fetch(f2.guid);
+ Assert.equal(f2.index, 1);
+});
+
+add_task(function* update_move_folder_into_descendant_throws() {
+ let parent = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER }) ;
+ let descendant = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: parent.guid,
+ parentGuid: parent.guid,
+ index: 0 });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/Cannot insert a folder into itself or one of its descendants/.test(ex));
+ }
+
+ try {
+ yield PlacesUtils.bookmarks.update({ guid: parent.guid,
+ parentGuid: descendant.guid,
+ index: 0 });
+ Assert.ok(false, "Should have thrown");
+ } catch (ex) {
+ Assert.ok(/Cannot insert a folder into itself or one of its descendants/.test(ex));
+ }
+});
+
+add_task(function* update_move() {
+ let parent = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER }) ;
+ let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
+ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK }) ;
+ let descendant = yield PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ Assert.equal(descendant.index, 1);
+ let lastModified = bm.lastModified;
+
+ // This is moving to a nonexisting index by purpose, it will be appended.
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ parentGuid: descendant.guid,
+ index: 1 });
+ checkBookmarkObject(bm);
+ Assert.equal(bm.parentGuid, descendant.guid);
+ Assert.equal(bm.index, 0);
+ Assert.ok(bm.lastModified >= lastModified);
+
+ parent = yield PlacesUtils.bookmarks.fetch(parent.guid);
+ descendant = yield PlacesUtils.bookmarks.fetch(descendant.guid);
+ Assert.deepEqual(parent.lastModified, bm.lastModified);
+ Assert.deepEqual(descendant.lastModified, bm.lastModified);
+ Assert.equal(descendant.index, 0);
+
+ bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
+ Assert.equal(bm.parentGuid, descendant.guid);
+ Assert.equal(bm.index, 0);
+
+ bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
+ parentGuid: parent.guid,
+ index: 0 });
+ Assert.equal(bm.parentGuid, parent.guid);
+ Assert.equal(bm.index, 0);
+
+ descendant = yield PlacesUtils.bookmarks.fetch(descendant.guid);
+ Assert.equal(descendant.index, 1);
+});
+
+add_task(function* update_move_append() {
+ let folder_a =
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(folder_a);
+ let folder_b =
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER });
+ checkBookmarkObject(folder_b);
+
+ /* folder_a: [sep_1, sep_2, sep_3], folder_b: [] */
+ let sep_1 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+ checkBookmarkObject(sep_1);
+ let sep_2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+ checkBookmarkObject(sep_2);
+ let sep_3 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+ checkBookmarkObject(sep_3);
+
+ function ensurePosition(info, parentGuid, index) {
+ checkBookmarkObject(info);
+ Assert.equal(info.parentGuid, parentGuid);
+ Assert.equal(info.index, index);
+ }
+
+ // folder_a: [sep_2, sep_3, sep_1], folder_b: []
+ sep_1.index = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ // Note sep_1 includes parentGuid even though we're not moving the item to
+ // another folder
+ sep_1 = yield PlacesUtils.bookmarks.update(sep_1);
+ ensurePosition(sep_1, folder_a.guid, 2);
+ sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid);
+ ensurePosition(sep_2, folder_a.guid, 0);
+ sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid);
+ ensurePosition(sep_3, folder_a.guid, 1);
+ sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid);
+ ensurePosition(sep_1, folder_a.guid, 2);
+
+ // folder_a: [sep_2, sep_1], folder_b: [sep_3]
+ sep_3.index = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ sep_3.parentGuid = folder_b.guid;
+ sep_3 = yield PlacesUtils.bookmarks.update(sep_3);
+ ensurePosition(sep_3, folder_b.guid, 0);
+ sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid);
+ ensurePosition(sep_2, folder_a.guid, 0);
+ sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid);
+ ensurePosition(sep_1, folder_a.guid, 1);
+ sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid);
+ ensurePosition(sep_3, folder_b.guid, 0);
+
+ // folder_a: [sep_1], folder_b: [sep_3, sep_2]
+ sep_2.index = Number.MAX_SAFE_INTEGER;
+ sep_2.parentGuid = folder_b.guid;
+ sep_2 = yield PlacesUtils.bookmarks.update(sep_2);
+ ensurePosition(sep_2, folder_b.guid, 1);
+ sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid);
+ ensurePosition(sep_1, folder_a.guid, 0);
+ sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid);
+ ensurePosition(sep_3, folder_b.guid, 0);
+ sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid);
+ ensurePosition(sep_2, folder_b.guid, 1);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js b/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js
new file mode 100644
index 0000000000..f5cf34641a
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js
@@ -0,0 +1,18 @@
+
+// Bug 1192692 - promiseBookmarksTree caches items without adding observers to
+// invalidate the cache.
+add_task(function* boookmarks_tree_cache() {
+ // Note that for this test to be effective, it needs to use the "old" sync
+ // bookmarks methods - using, eg, PlacesUtils.bookmarks.insert() doesn't
+ // demonstrate the problem as it indirectly arranges for the observers to
+ // be added.
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://example.com"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ yield PlacesUtils.promiseBookmarksTree();
+
+ PlacesUtils.bookmarks.removeItem(id);
+
+ yield Assert.rejects(PlacesUtils.promiseItemGuid(id));
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js b/toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js
new file mode 100644
index 0000000000..55ffecf2f6
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_changeBookmarkURI.js
@@ -0,0 +1,68 @@
+/* -*- 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/. */
+
+// Get bookmark service
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+/**
+ * Ensures that the Places APIs recognize that aBookmarkedUri is bookmarked
+ * via aBookmarkId and that aUnbookmarkedUri is not bookmarked at all.
+ *
+ * @param aBookmarkId
+ * an item ID whose corresponding URI is aBookmarkedUri
+ * @param aBookmarkedUri
+ * a bookmarked URI that has a corresponding item ID aBookmarkId
+ * @param aUnbookmarkedUri
+ * a URI that is not currently bookmarked at all
+ */
+function checkUris(aBookmarkId, aBookmarkedUri, aUnbookmarkedUri)
+{
+ // Ensure that aBookmarkedUri equals some URI that is bookmarked
+ var uri = bmsvc.getBookmarkedURIFor(aBookmarkedUri);
+ do_check_neq(uri, null);
+ do_check_true(uri.equals(aBookmarkedUri));
+
+ // Ensure that aBookmarkedUri is considered bookmarked
+ do_check_true(bmsvc.isBookmarked(aBookmarkedUri));
+
+ // Ensure that the URI corresponding to aBookmarkId equals aBookmarkedUri
+ do_check_true(bmsvc.getBookmarkURI(aBookmarkId).equals(aBookmarkedUri));
+
+ // Ensure that aUnbookmarkedUri does not equal any URI that is bookmarked
+ uri = bmsvc.getBookmarkedURIFor(aUnbookmarkedUri);
+ do_check_eq(uri, null);
+
+ // Ensure that aUnbookmarkedUri is not considered bookmarked
+ do_check_false(bmsvc.isBookmarked(aUnbookmarkedUri));
+}
+
+// main
+function run_test() {
+ // Create a folder
+ var folderId = bmsvc.createFolder(bmsvc.toolbarFolder,
+ "test",
+ bmsvc.DEFAULT_INDEX);
+
+ // Create 2 URIs
+ var uri1 = uri("http://www.dogs.com");
+ var uri2 = uri("http://www.cats.com");
+
+ // Bookmark the first one
+ var bookmarkId = bmsvc.insertBookmark(folderId,
+ uri1,
+ bmsvc.DEFAULT_INDEX,
+ "Dogs");
+
+ // uri1 is bookmarked via bookmarkId, uri2 is not
+ checkUris(bookmarkId, uri1, uri2);
+
+ // Change the URI of the bookmark to uri2
+ bmsvc.changeBookmarkURI(bookmarkId, uri2);
+
+ // uri2 is now bookmarked via bookmarkId, uri1 is not
+ checkUris(bookmarkId, uri2, uri1);
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js b/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js
new file mode 100644
index 0000000000..c43e8e2837
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js
@@ -0,0 +1,84 @@
+/* -*- 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/. */
+
+ /**
+ * Test bookmarksService.getBookmarkedURIFor(aURI);
+ */
+
+var hs = PlacesUtils.history;
+var bs = PlacesUtils.bookmarks;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_getBookmarkedURIFor() {
+ let now = Date.now() * 1000;
+ const sourceURI = uri("http://test.mozilla.org/");
+ // Add a visit and a bookmark.
+ yield PlacesTestUtils.addVisits({ uri: sourceURI, visitDate: now });
+ do_check_eq(bs.getBookmarkedURIFor(sourceURI), null);
+
+ let sourceItemId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ sourceURI,
+ bs.DEFAULT_INDEX,
+ "bookmark");
+ do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
+
+ // Add a redirected visit.
+ const permaURI = uri("http://perma.mozilla.org/");
+ yield PlacesTestUtils.addVisits({
+ uri: permaURI,
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ visitDate: now++,
+ referrer: sourceURI
+ });
+ do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
+ do_check_true(bs.getBookmarkedURIFor(permaURI).equals(sourceURI));
+ // Add a bookmark to the destination.
+ let permaItemId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ permaURI,
+ bs.DEFAULT_INDEX,
+ "bookmark");
+ do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
+ do_check_true(bs.getBookmarkedURIFor(permaURI).equals(permaURI));
+ // Now remove the bookmark on the destination.
+ bs.removeItem(permaItemId);
+ // We should see the source as bookmark.
+ do_check_true(bs.getBookmarkedURIFor(permaURI).equals(sourceURI));
+
+ // Add another redirected visit.
+ const tempURI = uri("http://perma.mozilla.org/");
+ yield PlacesTestUtils.addVisits({
+ uri: tempURI,
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ visitDate: now++,
+ referrer: permaURI
+ });
+
+ do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
+ do_check_true(bs.getBookmarkedURIFor(tempURI).equals(sourceURI));
+ // Add a bookmark to the destination.
+ let tempItemId = bs.insertBookmark(bs.unfiledBookmarksFolder,
+ tempURI,
+ bs.DEFAULT_INDEX,
+ "bookmark");
+ do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
+ do_check_true(bs.getBookmarkedURIFor(tempURI).equals(tempURI));
+
+ // Now remove the bookmark on the destination.
+ bs.removeItem(tempItemId);
+ // We should see the source as bookmark.
+ do_check_true(bs.getBookmarkedURIFor(tempURI).equals(sourceURI));
+ // Remove the source bookmark as well.
+ bs.removeItem(sourceItemId);
+ do_check_eq(bs.getBookmarkedURIFor(tempURI), null);
+
+ // Try to pass in a never seen URI, should return null and a new entry should
+ // not be added to the database.
+ do_check_eq(bs.getBookmarkedURIFor(uri("http://does.not.exist/")), null);
+ do_check_false(page_in_database("http://does.not.exist/"));
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_keywords.js b/toolkit/components/places/tests/bookmarks/test_keywords.js
new file mode 100644
index 0000000000..149d6d0b08
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_keywords.js
@@ -0,0 +1,310 @@
+const URI1 = NetUtil.newURI("http://test1.mozilla.org/");
+const URI2 = NetUtil.newURI("http://test2.mozilla.org/");
+const URI3 = NetUtil.newURI("http://test3.mozilla.org/");
+
+function check_keyword(aURI, aKeyword) {
+ if (aKeyword)
+ aKeyword = aKeyword.toLowerCase();
+
+ for (let bm of PlacesUtils.getBookmarksForURI(aURI)) {
+ let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bm);
+ if (keyword && !aKeyword) {
+ throw (`${aURI.spec} should not have a keyword`);
+ } else if (aKeyword && keyword == aKeyword) {
+ Assert.equal(keyword, aKeyword);
+ }
+ }
+
+ if (aKeyword) {
+ let uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword);
+ Assert.equal(uri.spec, aURI.spec);
+ // Check case insensitivity.
+ uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword.toUpperCase());
+ Assert.equal(uri.spec, aURI.spec);
+ }
+}
+
+function* check_orphans() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(
+ `SELECT id FROM moz_keywords k
+ WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id)
+ `);
+ Assert.equal(rows.length, 0);
+}
+
+function expectNotifications() {
+ let notifications = [];
+ let observer = new Proxy(NavBookmarkObserver, {
+ get(target, name) {
+ if (name == "check") {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ return expectedNotifications =>
+ Assert.deepEqual(notifications, expectedNotifications);
+ }
+
+ if (name.startsWith("onItemChanged")) {
+ return function(id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal) {
+ if (prop != "keyword")
+ return;
+ let args = Array.from(arguments, arg => {
+ if (arg && arg instanceof Ci.nsIURI)
+ return new URL(arg.spec);
+ if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
+ return new Date(parseInt(arg/1000));
+ return arg;
+ });
+ notifications.push({ name: name, arguments: args });
+ }
+ }
+
+ return target[name];
+ }
+ });
+ PlacesUtils.bookmarks.addObserver(observer, false);
+ return observer;
+}
+
+add_task(function test_invalid_input() {
+ Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(null),
+ /NS_ERROR_ILLEGAL_VALUE/);
+ Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(""),
+ /NS_ERROR_ILLEGAL_VALUE/);
+ Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(null),
+ /NS_ERROR_ILLEGAL_VALUE/);
+ Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(0),
+ /NS_ERROR_ILLEGAL_VALUE/);
+ Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(null, "k"),
+ /NS_ERROR_ILLEGAL_VALUE/);
+ Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(0, "k"),
+ /NS_ERROR_ILLEGAL_VALUE/);
+});
+
+add_task(function* test_addBookmarkAndKeyword() {
+ check_keyword(URI1, null);
+ let fc = yield foreign_count(URI1);
+ let observer = expectNotifications();
+
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test");
+
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+ let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI1 });
+ observer.check([ { name: "onItemChanged",
+ arguments: [ itemId, "keyword", false, "keyword",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ check_keyword(URI1, "keyword");
+ Assert.equal((yield foreign_count(URI1)), fc + 2); // + 1 bookmark + 1 keyword
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield check_orphans();
+});
+
+add_task(function* test_addBookmarkToURIHavingKeyword() {
+ // The uri has already a keyword.
+ check_keyword(URI1, "keyword");
+ let fc = yield foreign_count(URI1);
+
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test");
+ check_keyword(URI1, "keyword");
+ Assert.equal((yield foreign_count(URI1)), fc + 1); // + 1 bookmark
+
+ PlacesUtils.bookmarks.removeItem(itemId);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ check_orphans();
+});
+
+add_task(function* test_sameKeywordDifferentURI() {
+ let fc1 = yield foreign_count(URI1);
+ let fc2 = yield foreign_count(URI2);
+ let observer = expectNotifications();
+
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI2,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test2");
+ check_keyword(URI1, "keyword");
+ check_keyword(URI2, null);
+
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "kEyWoRd");
+
+ let bookmark1 = yield PlacesUtils.bookmarks.fetch({ url: URI1 });
+ let bookmark2 = yield PlacesUtils.bookmarks.fetch({ url: URI2 });
+ observer.check([ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ itemId, "keyword", false, "keyword",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // The keyword should have been "moved" to the new URI.
+ check_keyword(URI1, null);
+ Assert.equal((yield foreign_count(URI1)), fc1 - 1); // - 1 keyword
+ check_keyword(URI2, "keyword");
+ Assert.equal((yield foreign_count(URI2)), fc2 + 2); // + 1 bookmark + 1 keyword
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ check_orphans();
+});
+
+add_task(function* test_sameURIDifferentKeyword() {
+ let fc = yield foreign_count(URI2);
+ let observer = expectNotifications();
+
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI2,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test2");
+ check_keyword(URI2, "keyword");
+
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword2");
+
+ let bookmarks = [];
+ yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
+ observer.check([ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)),
+ "keyword", false, "keyword2",
+ bookmarks[0].lastModified, bookmarks[0].type,
+ (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
+ bookmarks[0].guid, bookmarks[0].parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)),
+ "keyword", false, "keyword2",
+ bookmarks[1].lastModified, bookmarks[1].type,
+ (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
+ bookmarks[1].guid, bookmarks[1].parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ check_keyword(URI2, "keyword2");
+ Assert.equal((yield foreign_count(URI2)), fc + 2); // + 1 bookmark + 1 keyword
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ check_orphans();
+});
+
+add_task(function* test_removeBookmarkWithKeyword() {
+ let fc = yield foreign_count(URI2);
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI2,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test");
+
+ // The keyword should not be removed, since there are other bookmarks yet.
+ PlacesUtils.bookmarks.removeItem(itemId);
+
+ check_keyword(URI2, "keyword2");
+ Assert.equal((yield foreign_count(URI2)), fc); // + 1 bookmark - 1 bookmark
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ check_orphans();
+});
+
+add_task(function* test_unsetKeyword() {
+ let fc = yield foreign_count(URI2);
+ let observer = expectNotifications();
+
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI2,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test");
+
+ // The keyword should be removed from any bookmark.
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, null);
+
+ let bookmarks = [];
+ yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
+ do_print(bookmarks.length);
+ observer.check([ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)),
+ "keyword", false, "",
+ bookmarks[0].lastModified, bookmarks[0].type,
+ (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
+ bookmarks[0].guid, bookmarks[0].parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)),
+ "keyword", false, "",
+ bookmarks[1].lastModified, bookmarks[1].type,
+ (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
+ bookmarks[1].guid, bookmarks[1].parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[2].guid)),
+ "keyword", false, "",
+ bookmarks[2].lastModified, bookmarks[2].type,
+ (yield PlacesUtils.promiseItemId(bookmarks[2].parentGuid)),
+ bookmarks[2].guid, bookmarks[2].parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+
+ check_keyword(URI1, null);
+ check_keyword(URI2, null);
+ Assert.equal((yield foreign_count(URI2)), fc - 1); // + 1 bookmark - 2 keyword
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ check_orphans();
+});
+
+add_task(function* test_addRemoveBookmark() {
+ let observer = expectNotifications();
+
+ let itemId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ URI3,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test3");
+
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+ let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI3 });
+ let parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid);
+ PlacesUtils.bookmarks.removeItem(itemId);
+
+ observer.check([ { name: "onItemChanged",
+ arguments: [ itemId,
+ "keyword", false, "keyword",
+ bookmark.lastModified, bookmark.type,
+ parentId,
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
+ ]);
+
+ check_keyword(URI3, null);
+ // Don't check the foreign count since the process is async.
+ // The new test_keywords.js in unit is checking this though.
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ check_orphans();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
new file mode 100644
index 0000000000..06f45b18e0
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -0,0 +1,640 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that each nsINavBookmarksObserver method gets the correct input.
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+
+const GUID_RE = /^[a-zA-Z0-9\-_]{12}$/;
+
+var gBookmarksObserver = {
+ expected: [],
+ setup(expected) {
+ this.expected = expected;
+ this.deferred = PromiseUtils.defer();
+ return this.deferred.promise;
+ },
+ validate: function (aMethodName, aArguments) {
+ do_check_eq(this.expected[0].name, aMethodName);
+
+ let args = this.expected.shift().args;
+ do_check_eq(aArguments.length, args.length);
+ for (let i = 0; i < aArguments.length; i++) {
+ do_check_true(args[i].check(aArguments[i]), aMethodName + "(args[" + i + "]: " + args[i].name + ")");
+ }
+
+ if (this.expected.length === 0) {
+ this.deferred.resolve();
+ }
+ },
+
+ // nsINavBookmarkObserver
+ onBeginUpdateBatch() {
+ return this.validate("onBeginUpdateBatch", arguments);
+ },
+ onEndUpdateBatch() {
+ return this.validate("onEndUpdateBatch", arguments);
+ },
+ onItemAdded() {
+ return this.validate("onItemAdded", arguments);
+ },
+ onItemRemoved() {
+ return this.validate("onItemRemoved", arguments);
+ },
+ onItemChanged() {
+ return this.validate("onItemChanged", arguments);
+ },
+ onItemVisited() {
+ return this.validate("onItemVisited", arguments);
+ },
+ onItemMoved() {
+ return this.validate("onItemMoved", arguments);
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
+};
+
+var gBookmarkSkipObserver = {
+ skipTags: true,
+ skipDescendantsOnItemRemoval: true,
+
+ expected: null,
+ setup(expected) {
+ this.expected = expected;
+ this.deferred = PromiseUtils.defer();
+ return this.deferred.promise;
+ },
+ validate: function (aMethodName) {
+ do_check_eq(this.expected.shift(), aMethodName);
+ if (this.expected.length === 0) {
+ this.deferred.resolve();
+ }
+ },
+
+ // nsINavBookmarkObserver
+ onBeginUpdateBatch() {
+ return this.validate("onBeginUpdateBatch", arguments);
+ },
+ onEndUpdateBatch() {
+ return this.validate("onEndUpdateBatch", arguments);
+ },
+ onItemAdded() {
+ return this.validate("onItemAdded", arguments);
+ },
+ onItemRemoved() {
+ return this.validate("onItemRemoved", arguments);
+ },
+ onItemChanged() {
+ return this.validate("onItemChanged", arguments);
+ },
+ onItemVisited() {
+ return this.validate("onItemVisited", arguments);
+ },
+ onItemMoved() {
+ return this.validate("onItemMoved", arguments);
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
+};
+
+
+add_task(function setup() {
+ PlacesUtils.bookmarks.addObserver(gBookmarksObserver, false);
+ PlacesUtils.bookmarks.addObserver(gBookmarkSkipObserver, false);
+});
+
+add_task(function* batch() {
+ let promise = Promise.all([
+ gBookmarksObserver.setup([
+ { name: "onBeginUpdateBatch",
+ args: [] },
+ { name: "onEndUpdateBatch",
+ args: [] },
+ ]),
+ gBookmarkSkipObserver.setup([
+ "onBeginUpdateBatch", "onEndUpdateBatch"
+ ])]);
+ PlacesUtils.bookmarks.runInBatchMode({
+ runBatched: function () {
+ // Nothing.
+ }
+ }, null);
+ yield promise;
+});
+
+add_task(function* onItemAdded_bookmark() {
+ const TITLE = "Bookmark 1";
+ let uri = NetUtil.newURI("http://1.mozilla.org/");
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemAdded"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "title", check: v => v === TITLE },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TITLE);
+ yield promise;
+});
+
+add_task(function* onItemAdded_separator() {
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemAdded"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "index", check: v => v === 1 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
+ { name: "uri", check: v => v === null },
+ { name: "title", check: v => v === null },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ yield promise;
+});
+
+add_task(function* onItemAdded_folder() {
+ const TITLE = "Folder 1";
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemAdded"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "index", check: v => v === 2 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "title", check: v => v === TITLE },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ yield promise;
+});
+
+add_task(function* onItemChanged_title_bookmark() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ const TITLE = "New title";
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemChanged"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "title" },
+ { name: "isAnno", check: v => v === false },
+ { name: "newValue", check: v => v === TITLE },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.setItemTitle(id, TITLE);
+ yield promise;
+});
+
+add_task(function* onItemChanged_tags_bookmark() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
+ const TAG = "tag";
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemChanged", "onItemChanged"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemAdded", // This is the tag folder.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "title", check: v => v === TAG },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemAdded", // This is the tag.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "title", check: v => v === null },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemChanged",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "tags" },
+ { name: "isAnno", check: v => v === false },
+ { name: "newValue", check: v => v === "" },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved", // This is the tag.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved", // This is the tag folder.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.tagsFolderId },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemChanged",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "tags" },
+ { name: "isAnno", check: v => v === false },
+ { name: "newValue", check: v => v === "" },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.tagging.tagURI(uri, [TAG]);
+ PlacesUtils.tagging.untagURI(uri, [TAG]);
+ yield promise;
+});
+
+add_task(function* onItemMoved_bookmark() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemMoved", "onItemMoved"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemMoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "oldParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "oldIndex", check: v => v === 0 },
+ { name: "newParentId", check: v => v === PlacesUtils.toolbarFolderId },
+ { name: "newIndex", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "newParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemMoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "oldParentId", check: v => v === PlacesUtils.toolbarFolderId },
+ { name: "oldIndex", check: v => v === 0 },
+ { name: "newParentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "newIndex", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "newParentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.moveItem(id, PlacesUtils.toolbarFolderId, 0);
+ PlacesUtils.bookmarks.moveItem(id, PlacesUtils.unfiledBookmarksFolderId, 0);
+ yield promise;
+});
+
+add_task(function* onItemMoved_bookmark() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemVisited"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemVisited",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "visitId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "time", check: v => typeof(v) == "number" && v > 0 },
+ { name: "transitionType", check: v => v === PlacesUtils.history.TRANSITION_TYPED },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ ] },
+ ])]);
+ PlacesTestUtils.addVisits({ uri: uri, transition: TRANSITION_TYPED });
+ yield promise;
+});
+
+add_task(function* onItemRemoved_bookmark() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemChanged", "onItemRemoved"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "" },
+ { name: "isAnno", check: v => v === true },
+ { name: "newValue", check: v => v === "" },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.removeItem(id);
+ yield promise;
+});
+
+add_task(function* onItemRemoved_separator() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemChanged", "onItemRemoved"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "" },
+ { name: "isAnno", check: v => v === true },
+ { name: "newValue", check: v => v === "" },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
+ { name: "uri", check: v => v === null },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.removeItem(id);
+ yield promise;
+});
+
+add_task(function* onItemRemoved_folder() {
+ let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemChanged", "onItemRemoved"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "" },
+ { name: "isAnno", check: v => v === true },
+ { name: "newValue", check: v => v === "" },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ PlacesUtils.bookmarks.removeItem(id);
+ yield promise;
+});
+
+add_task(function* onItemRemoved_folder_recursive() {
+ const TITLE = "Folder 3";
+ const BMTITLE = "Bookmark 1";
+ let uri = NetUtil.newURI("http://1.mozilla.org/");
+ let promise = Promise.all([
+ gBookmarkSkipObserver.setup([
+ "onItemAdded", "onItemAdded", "onItemAdded", "onItemAdded",
+ "onItemChanged", "onItemRemoved"
+ ]),
+ gBookmarksObserver.setup([
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "title", check: v => v === TITLE },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "title", check: v => v === BMTITLE },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) },
+ { name: "index", check: v => v === 1 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "title", check: v => v === TITLE },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemAdded",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => v === PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0), 1) },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "title", check: v => v === BMTITLE },
+ { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "property", check: v => v === "" },
+ { name: "isAnno", check: v => v === true },
+ { name: "newValue", check: v => v === "" },
+ { name: "lastModified", check: v => typeof(v) == "number" && v > 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "oldValue", check: v => typeof(v) == "string" },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 1 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
+ { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ { name: "onItemRemoved",
+ args: [
+ { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
+ { name: "index", check: v => v === 0 },
+ { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER },
+ { name: "uri", check: v => v === null },
+ { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
+ { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
+ ] },
+ ])]);
+ let folder = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.insertBookmark(folder,
+ uri, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ BMTITLE);
+ let folder2 = PlacesUtils.bookmarks.createFolder(folder, TITLE,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.insertBookmark(folder2,
+ uri, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ BMTITLE);
+
+ PlacesUtils.bookmarks.removeItem(folder);
+ yield promise;
+});
+
+add_task(function cleanup()
+{
+ PlacesUtils.bookmarks.removeObserver(gBookmarksObserver);
+ PlacesUtils.bookmarks.removeObserver(gBookmarkSkipObserver);
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_protectRoots.js b/toolkit/components/places/tests/bookmarks/test_protectRoots.js
new file mode 100644
index 0000000000..0a59f16535
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_protectRoots.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test()
+{
+ const ROOTS = [
+ PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.toolbarFolderId,
+ PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.tagsFolderId,
+ PlacesUtils.placesRootId,
+ PlacesUtils.mobileFolderId,
+ ];
+
+ for (let root of ROOTS) {
+ do_check_true(PlacesUtils.isRootItem(root));
+
+ try {
+ PlacesUtils.bookmarks.removeItem(root);
+ do_throw("Trying to remove a root should throw");
+ } catch (ex) {}
+
+ try {
+ PlacesUtils.bookmarks.moveItem(root, PlacesUtils.placesRootId, 0);
+ do_throw("Trying to move a root should throw");
+ } catch (ex) {}
+
+ try {
+ PlacesUtils.bookmarks.removeFolderChildren(root);
+ if (root == PlacesUtils.placesRootId)
+ do_throw("Trying to remove children of the main root should throw");
+ } catch (ex) {
+ if (root != PlacesUtils.placesRootId)
+ do_throw("Trying to remove children of other roots should not throw");
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
new file mode 100644
index 0000000000..537974b38a
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
@@ -0,0 +1,70 @@
+/**
+ * This test ensures that reinserting a folder within a transaction gives it
+ * a different GUID, and passes the GUID to the observers.
+ */
+
+add_task(function* test_removeFolderTransaction_reinsert() {
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "Test folder",
+ });
+ let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+ let fx = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ title: "Get Firefox!",
+ url: "http://getfirefox.com",
+ });
+ let fxId = yield PlacesUtils.promiseItemId(fx.guid);
+ let tb = yield PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ title: "Get Thunderbird!",
+ url: "http://getthunderbird.com",
+ });
+ let tbId = yield PlacesUtils.promiseItemId(tb.guid);
+
+ let notifications = [];
+ function checkNotifications(expected, message) {
+ deepEqual(notifications, expected, message);
+ notifications.length = 0;
+ }
+
+ let observer = {
+ onItemAdded(itemId, parentId, index, type, uri, title, dateAdded, guid,
+ parentGuid) {
+ notifications.push(["onItemAdded", itemId, parentId, guid, parentGuid]);
+ },
+ onItemRemoved(itemId, parentId, index, type, uri, guid, parentGuid) {
+ notifications.push(["onItemRemoved", itemId, parentId, guid, parentGuid]);
+ },
+ };
+ PlacesUtils.bookmarks.addObserver(observer, false);
+ PlacesUtils.registerShutdownFunction(function() {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ });
+
+ let transaction = PlacesUtils.bookmarks.getRemoveFolderTransaction(folderId);
+ deepEqual(notifications, [], "We haven't executed the transaction yet");
+
+ transaction.doTransaction();
+ checkNotifications([
+ ["onItemRemoved", tbId, folderId, tb.guid, folder.guid],
+ ["onItemRemoved", fxId, folderId, fx.guid, folder.guid],
+ ["onItemRemoved", folderId, PlacesUtils.bookmarksMenuFolderId, folder.guid,
+ PlacesUtils.bookmarks.menuGuid],
+ ], "Executing transaction should remove folder and its descendants");
+
+ transaction.undoTransaction();
+ // At this point, the restored folder has the same ID, but a different GUID.
+ let newFolderGuid = yield PlacesUtils.promiseItemGuid(folderId);
+ checkNotifications([
+ ["onItemAdded", folderId, PlacesUtils.bookmarksMenuFolderId, newFolderGuid,
+ PlacesUtils.bookmarks.menuGuid],
+ ], "Undo should reinsert folder with same ID and different GUID");
+
+ transaction.redoTransaction();
+ checkNotifications([
+ ["onItemRemoved", folderId, PlacesUtils.bookmarksMenuFolderId,
+ newFolderGuid, PlacesUtils.bookmarks.menuGuid],
+ ], "Redo should forward new GUID to observer");
+});
diff --git a/toolkit/components/places/tests/bookmarks/test_removeItem.js b/toolkit/components/places/tests/bookmarks/test_removeItem.js
new file mode 100644
index 0000000000..ec846b28e0
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_removeItem.js
@@ -0,0 +1,30 @@
+/* -*- 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 tests = [];
+
+
+const DEFAULT_INDEX = PlacesUtils.bookmarks.DEFAULT_INDEX;
+
+function run_test() {
+ // folder to hold this test
+ var folderId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId,
+ "", DEFAULT_INDEX);
+
+ // add a bookmark to the new folder
+ var bookmarkURI = uri("http://iasdjkf");
+ do_check_false(PlacesUtils.bookmarks.isBookmarked(bookmarkURI));
+ var bookmarkId = PlacesUtils.bookmarks.insertBookmark(folderId, bookmarkURI,
+ DEFAULT_INDEX, "");
+ do_check_eq(PlacesUtils.bookmarks.getItemTitle(bookmarkId), "");
+
+ // remove the folder using removeItem
+ PlacesUtils.bookmarks.removeItem(folderId);
+ do_check_eq(PlacesUtils.bookmarks.getBookmarkIdsForURI(bookmarkURI).length, 0);
+ do_check_false(PlacesUtils.bookmarks.isBookmarked(bookmarkURI));
+ do_check_eq(PlacesUtils.bookmarks.getItemIndex(bookmarkId), -1);
+}
diff --git a/toolkit/components/places/tests/bookmarks/test_savedsearches.js b/toolkit/components/places/tests/bookmarks/test_savedsearches.js
new file mode 100644
index 0000000000..eee2c4489d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_savedsearches.js
@@ -0,0 +1,209 @@
+/* -*- 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/. */
+
+// get bookmarks root id
+var root = PlacesUtils.bookmarksMenuFolderId;
+
+// a search term that matches a default bookmark
+const searchTerm = "about";
+
+var testRoot;
+
+// main
+function run_test() {
+ // create a folder to hold all the tests
+ // this makes the tests more tolerant of changes to the default bookmarks set
+ // also, name it using the search term, for testing that containers that match don't show up in query results
+ testRoot = PlacesUtils.bookmarks.createFolder(
+ root, searchTerm, PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ run_next_test();
+}
+
+add_test(function test_savedsearches_bookmarks() {
+ // add a bookmark that matches the search term
+ var bookmarkId = PlacesUtils.bookmarks.insertBookmark(
+ root, uri("http://foo.com"), PlacesUtils.bookmarks.DEFAULT_INDEX,
+ searchTerm);
+
+ // create a saved-search that matches a default bookmark
+ var searchId = PlacesUtils.bookmarks.insertBookmark(
+ testRoot, uri("place:terms=" + searchTerm + "&excludeQueries=1&expandQueries=1&queryType=1"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm);
+
+ // query for the test root, expandQueries=0
+ // the query should show up as a regular bookmark
+ try {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.expandQueries = 0;
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([testRoot], 1);
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_check_eq(cc, 1);
+ for (let i = 0; i < cc; i++) {
+ let node = rootNode.getChild(i);
+ // test that queries have valid itemId
+ do_check_true(node.itemId > 0);
+ // test that the container is closed
+ node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(node.containerOpen, false);
+ }
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("expandQueries=0 query error: " + ex);
+ }
+
+ // bookmark saved search
+ // query for the test root, expandQueries=1
+ // the query should show up as a query container, with 1 child
+ try {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.expandQueries = 1;
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([testRoot], 1);
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let cc = rootNode.childCount;
+ do_check_eq(cc, 1);
+ for (let i = 0; i < cc; i++) {
+ let node = rootNode.getChild(i);
+ // test that query node type is container when expandQueries=1
+ do_check_eq(node.type, node.RESULT_TYPE_QUERY);
+ // test that queries (as containers) have valid itemId
+ do_check_true(node.itemId > 0);
+ node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ node.containerOpen = true;
+
+ // test that queries have children when excludeItems=1
+ // test that query nodes don't show containers (shouldn't have our folder that matches)
+ // test that queries don't show themselves in query results (shouldn't have our saved search)
+ do_check_eq(node.childCount, 1);
+
+ // test that bookmark shows in query results
+ var item = node.getChild(0);
+ do_check_eq(item.itemId, bookmarkId);
+
+ // XXX - FAILING - test live-update of query results - add a bookmark that matches the query
+ // var tmpBmId = PlacesUtils.bookmarks.insertBookmark(
+ // root, uri("http://" + searchTerm + ".com"),
+ // PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm + "blah");
+ // do_check_eq(query.childCount, 2);
+
+ // XXX - test live-update of query results - delete a bookmark that matches the query
+ // PlacesUtils.bookmarks.removeItem(tmpBMId);
+ // do_check_eq(query.childCount, 1);
+
+ // test live-update of query results - add a folder that matches the query
+ PlacesUtils.bookmarks.createFolder(
+ root, searchTerm + "zaa", PlacesUtils.bookmarks.DEFAULT_INDEX);
+ do_check_eq(node.childCount, 1);
+ // test live-update of query results - add a query that matches the query
+ PlacesUtils.bookmarks.insertBookmark(
+ root, uri("place:terms=foo&excludeQueries=1&expandQueries=1&queryType=1"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm + "blah");
+ do_check_eq(node.childCount, 1);
+ }
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("expandQueries=1 bookmarks query: " + ex);
+ }
+
+ // delete the bookmark search
+ PlacesUtils.bookmarks.removeItem(searchId);
+
+ run_next_test();
+});
+
+add_task(function* test_savedsearches_history() {
+ // add a visit that matches the search term
+ var testURI = uri("http://" + searchTerm + ".com");
+ yield PlacesTestUtils.addVisits({ uri: testURI, title: searchTerm });
+
+ // create a saved-search that matches the visit we added
+ var searchId = PlacesUtils.bookmarks.insertBookmark(testRoot,
+ uri("place:terms=" + searchTerm + "&excludeQueries=1&expandQueries=1&queryType=0"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm);
+
+ // query for the test root, expandQueries=1
+ // the query should show up as a query container, with 1 child
+ try {
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.expandQueries = 1;
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([testRoot], 1);
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var rootNode = result.root;
+ rootNode.containerOpen = true;
+ var cc = rootNode.childCount;
+ do_check_eq(cc, 1);
+ for (var i = 0; i < cc; i++) {
+ var node = rootNode.getChild(i);
+ // test that query node type is container when expandQueries=1
+ do_check_eq(node.type, node.RESULT_TYPE_QUERY);
+ // test that queries (as containers) have valid itemId
+ do_check_eq(node.itemId, searchId);
+ node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ node.containerOpen = true;
+
+ // test that queries have children when excludeItems=1
+ // test that query nodes don't show containers (shouldn't have our folder that matches)
+ // test that queries don't show themselves in query results (shouldn't have our saved search)
+ do_check_eq(node.childCount, 1);
+
+ // test that history visit shows in query results
+ var item = node.getChild(0);
+ do_check_eq(item.type, item.RESULT_TYPE_URI);
+ do_check_eq(item.itemId, -1); // history visit
+ do_check_eq(item.uri, testURI.spec); // history visit
+
+ // test live-update of query results - add a history visit that matches the query
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://foo.com"),
+ title: searchTerm + "blah"
+ });
+ do_check_eq(node.childCount, 2);
+
+ // test live-update of query results - delete a history visit that matches the query
+ PlacesUtils.history.removePage(uri("http://foo.com"));
+ do_check_eq(node.childCount, 1);
+ node.containerOpen = false;
+ }
+
+ // test live-update of moved queries
+ var tmpFolderId = PlacesUtils.bookmarks.createFolder(
+ testRoot, "foo", PlacesUtils.bookmarks.DEFAULT_INDEX);
+ PlacesUtils.bookmarks.moveItem(
+ searchId, tmpFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX);
+ var tmpFolderNode = rootNode.getChild(0);
+ do_check_eq(tmpFolderNode.itemId, tmpFolderId);
+ tmpFolderNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ tmpFolderNode.containerOpen = true;
+ do_check_eq(tmpFolderNode.childCount, 1);
+
+ // test live-update of renamed queries
+ PlacesUtils.bookmarks.setItemTitle(searchId, "foo");
+ do_check_eq(tmpFolderNode.title, "foo");
+
+ // test live-update of deleted queries
+ PlacesUtils.bookmarks.removeItem(searchId);
+ try {
+ tmpFolderNode = root.getChild(1);
+ do_throw("query was not removed");
+ } catch (ex) {}
+
+ tmpFolderNode.containerOpen = false;
+ rootNode.containerOpen = false;
+ }
+ catch (ex) {
+ do_throw("expandQueries=1 bookmarks query: " + ex);
+ }
+});
diff --git a/toolkit/components/places/tests/bookmarks/xpcshell.ini b/toolkit/components/places/tests/bookmarks/xpcshell.ini
new file mode 100644
index 0000000000..c290fd6934
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini
@@ -0,0 +1,50 @@
+[DEFAULT]
+head = head_bookmarks.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_1016953-renaming-uncompressed.js]
+[test_1017502-bookmarks_foreign_count.js]
+[test_384228.js]
+[test_385829.js]
+[test_388695.js]
+[test_393498.js]
+[test_395101.js]
+[test_395593.js]
+[test_405938_restore_queries.js]
+[test_417228-exclude-from-backup.js]
+[test_417228-other-roots.js]
+[test_424958-json-quoted-folders.js]
+[test_448584.js]
+[test_458683.js]
+[test_466303-json-remove-backups.js]
+[test_477583_json-backup-in-future.js]
+[test_675416.js]
+[test_711914.js]
+[test_818584-discard-duplicate-backups.js]
+[test_818587_compress-bookmarks-backups.js]
+[test_818593-store-backup-metadata.js]
+[test_992901-backup-unsorted-hierarchy.js]
+[test_997030-bookmarks-html-encode.js]
+[test_1129529.js]
+[test_async_observers.js]
+[test_bmindex.js]
+[test_bookmarkstree_cache.js]
+[test_bookmarks.js]
+[test_bookmarks_eraseEverything.js]
+[test_bookmarks_fetch.js]
+[test_bookmarks_getRecent.js]
+[test_bookmarks_insert.js]
+[test_bookmarks_notifications.js]
+[test_bookmarks_remove.js]
+[test_bookmarks_reorder.js]
+[test_bookmarks_search.js]
+[test_bookmarks_update.js]
+[test_changeBookmarkURI.js]
+[test_getBookmarkedURIFor.js]
+[test_keywords.js]
+[test_nsINavBookmarkObserver.js]
+[test_protectRoots.js]
+[test_removeFolderTransaction_reinsert.js]
+[test_removeItem.js]
+[test_savedsearches.js]
diff --git a/toolkit/components/places/tests/browser/.eslintrc.js b/toolkit/components/places/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..7a41a9cde0
--- /dev/null
+++ b/toolkit/components/places/tests/browser/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js",
+ "../../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/browser/399606-history.go-0.html b/toolkit/components/places/tests/browser/399606-history.go-0.html
new file mode 100644
index 0000000000..039708ed71
--- /dev/null
+++ b/toolkit/components/places/tests/browser/399606-history.go-0.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>history.go(0)</title>
+<script>
+setTimeout('history.go(0)', 1000);
+</script>
+</head>
+<body>
+Testing history.go(0)
+</body>
+</html>
diff --git a/toolkit/components/places/tests/browser/399606-httprefresh.html b/toolkit/components/places/tests/browser/399606-httprefresh.html
new file mode 100644
index 0000000000..e43455ee05
--- /dev/null
+++ b/toolkit/components/places/tests/browser/399606-httprefresh.html
@@ -0,0 +1,8 @@
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+
+<meta http-equiv="refresh" content="1">
+<title>httprefresh</title>
+</head><body>
+Testing httprefresh
+</body></html>
diff --git a/toolkit/components/places/tests/browser/399606-location.reload.html b/toolkit/components/places/tests/browser/399606-location.reload.html
new file mode 100644
index 0000000000..0f46538cdd
--- /dev/null
+++ b/toolkit/components/places/tests/browser/399606-location.reload.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>location.reload()</title>
+<script>
+setTimeout('location.reload();', 100);
+</script>
+</head>
+<body>
+Testing location.reload();
+</body>
+</html>
diff --git a/toolkit/components/places/tests/browser/399606-location.replace.html b/toolkit/components/places/tests/browser/399606-location.replace.html
new file mode 100644
index 0000000000..36705402cc
--- /dev/null
+++ b/toolkit/components/places/tests/browser/399606-location.replace.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>location.replace</title>
+<script>
+setTimeout('location.replace(window.location.href)', 1000);
+</script>
+</head>
+<body>
+Testing location.replace
+</body>
+</html>
diff --git a/toolkit/components/places/tests/browser/399606-window.location.href.html b/toolkit/components/places/tests/browser/399606-window.location.href.html
new file mode 100644
index 0000000000..61a2c8ba01
--- /dev/null
+++ b/toolkit/components/places/tests/browser/399606-window.location.href.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>window.location.href</title>
+<script>
+setTimeout('window.location.href = window.location.href', 1000);
+</script>
+</head>
+<body>
+Testing window.location.href
+</body>
+</html>
diff --git a/toolkit/components/places/tests/browser/399606-window.location.html b/toolkit/components/places/tests/browser/399606-window.location.html
new file mode 100644
index 0000000000..e77f730716
--- /dev/null
+++ b/toolkit/components/places/tests/browser/399606-window.location.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>window.location</title>
+<script>
+setTimeout('window.location = window.location', 1000);
+</script>
+</head>
+<body>
+Testing window.location
+</body>
+</html>
diff --git a/toolkit/components/places/tests/browser/461710_iframe.html b/toolkit/components/places/tests/browser/461710_iframe.html
new file mode 100644
index 0000000000..7480fe58f8
--- /dev/null
+++ b/toolkit/components/places/tests/browser/461710_iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ </head>
+ <body>
+ <iframe id="iframe"></iframe>
+ </body>
+</html>
diff --git a/toolkit/components/places/tests/browser/461710_link_page-2.html b/toolkit/components/places/tests/browser/461710_link_page-2.html
new file mode 100644
index 0000000000..1fc3e0959d
--- /dev/null
+++ b/toolkit/components/places/tests/browser/461710_link_page-2.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Link page 2</title>
+ <style type="text/css">
+ a:link { color: #0000ff; }
+ a:visited { color: #ff0000; }
+ </style>
+ </head>
+ <body>
+ <p><a href="461710_visited_page.html" id="link">Link to the second visited page</a></p>
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/places/tests/browser/461710_link_page-3.html b/toolkit/components/places/tests/browser/461710_link_page-3.html
new file mode 100644
index 0000000000..5966618032
--- /dev/null
+++ b/toolkit/components/places/tests/browser/461710_link_page-3.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Link page 3</title>
+ <style type="text/css">
+ a:link { color: #0000ff; }
+ a:visited { color: #ff0000; }
+ </style>
+ </head>
+ <body>
+ <p><a href="461710_visited_page.html" id="link">Link to the third visited page</a></p>
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/places/tests/browser/461710_link_page.html b/toolkit/components/places/tests/browser/461710_link_page.html
new file mode 100644
index 0000000000..6bea506284
--- /dev/null
+++ b/toolkit/components/places/tests/browser/461710_link_page.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Link page</title>
+ <style type="text/css">
+ a:link { color: #0000ff; }
+ a:visited { color: #ff0000; }
+ </style>
+ </head>
+ <body>
+ <p><a href="461710_visited_page.html" id="link">Link to the visited page</a></p>
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/places/tests/browser/461710_visited_page.html b/toolkit/components/places/tests/browser/461710_visited_page.html
new file mode 100644
index 0000000000..90e65116b6
--- /dev/null
+++ b/toolkit/components/places/tests/browser/461710_visited_page.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Visited page</title>
+ </head>
+ <body>
+ <p>This page is marked as visited</p>
+ </body>
+</html> \ No newline at end of file
diff --git a/toolkit/components/places/tests/browser/begin.html b/toolkit/components/places/tests/browser/begin.html
new file mode 100644
index 0000000000..da4c16dd25
--- /dev/null
+++ b/toolkit/components/places/tests/browser/begin.html
@@ -0,0 +1,10 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+ <body>
+ <a id="clickme" href="redirect_twice.sjs">Redirect twice</a>
+ </body>
+</html>
diff --git a/toolkit/components/places/tests/browser/browser.ini b/toolkit/components/places/tests/browser/browser.ini
new file mode 100644
index 0000000000..e6abe987f8
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+support-files =
+ colorAnalyzer/category-discover.png
+ colorAnalyzer/dictionaryGeneric-16.png
+ colorAnalyzer/extensionGeneric-16.png
+ colorAnalyzer/localeGeneric.png
+ head.js
+
+[browser_bug248970.js]
+[browser_bug399606.js]
+[browser_bug461710.js]
+[browser_bug646422.js]
+[browser_bug680727.js]
+[browser_colorAnalyzer.js]
+[browser_double_redirect.js]
+[browser_favicon_privatebrowsing_perwindowpb.js]
+[browser_favicon_setAndFetchFaviconForPage.js]
+[browser_favicon_setAndFetchFaviconForPage_failures.js]
+[browser_history_post.js]
+[browser_notfound.js]
+[browser_redirect.js]
+[browser_settitle.js]
+[browser_visited_notfound.js]
+[browser_visituri.js]
+[browser_visituri_nohistory.js]
+[browser_visituri_privatebrowsing_perwindowpb.js] \ No newline at end of file
diff --git a/toolkit/components/places/tests/browser/browser_bug248970.js b/toolkit/components/places/tests/browser/browser_bug248970.js
new file mode 100644
index 0000000000..5850a30383
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_bug248970.js
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test performs checks on the history testing area as outlined
+// https://wiki.mozilla.org/Firefox3.1/PrivateBrowsing/TestPlan#History
+// http://developer.mozilla.org/en/Using_the_Places_history_service
+
+var visitedURIs = [
+ "http://www.test-link.com/",
+ "http://www.test-typed.com/",
+ "http://www.test-bookmark.com/",
+ "http://www.test-redirect-permanent.com/",
+ "http://www.test-redirect-temporary.com/",
+ "http://www.test-embed.com/",
+ "http://www.test-framed.com/",
+ "http://www.test-download.com/"
+].map(NetUtil.newURI.bind(NetUtil));
+
+add_task(function* () {
+ let windowsToClose = [];
+ let placeItemsCount = 0;
+
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(win) {
+ win.close();
+ });
+ });
+
+ yield PlacesTestUtils.clearHistory();
+
+ // Ensure we wait for the default bookmarks import.
+ yield new Promise(resolve => {
+ waitForCondition(() => {
+ placeItemsCount = getPlacesItemsCount();
+ return placeItemsCount > 0
+ }, resolve, "Should have default bookmarks")
+ });
+
+ // Create a handful of history items with various visit types
+ yield PlacesTestUtils.addVisits([
+ { uri: visitedURIs[0], transition: TRANSITION_LINK },
+ { uri: visitedURIs[1], transition: TRANSITION_TYPED },
+ { uri: visitedURIs[2], transition: TRANSITION_BOOKMARK },
+ { uri: visitedURIs[3], transition: TRANSITION_REDIRECT_PERMANENT },
+ { uri: visitedURIs[4], transition: TRANSITION_REDIRECT_TEMPORARY },
+ { uri: visitedURIs[5], transition: TRANSITION_EMBED },
+ { uri: visitedURIs[6], transition: TRANSITION_FRAMED_LINK },
+ { uri: visitedURIs[7], transition: TRANSITION_DOWNLOAD }
+ ]);
+
+ placeItemsCount += 7;
+ // We added 7 new items to history.
+ is(getPlacesItemsCount(), placeItemsCount,
+ "Check the total items count");
+
+ function* testOnWindow(aIsPrivate, aCount) {
+ let win = yield new Promise(resolve => {
+ whenNewWindowLoaded({ private: aIsPrivate }, resolve);
+ });
+ windowsToClose.push(win);
+
+ // History items should be retrievable by query
+ yield checkHistoryItems();
+
+ // Updates the place items count
+ let count = getPlacesItemsCount();
+
+ // Create Bookmark
+ let title = "title " + windowsToClose.length;
+ let keyword = "keyword " + windowsToClose.length;
+ let url = "http://test-a-" + windowsToClose.length + ".com/";
+
+ yield PlacesUtils.bookmarks.insert({ url, title,
+ parentGuid: PlacesUtils.bookmarks.menuGuid });
+ yield PlacesUtils.keywords.insert({ url, keyword });
+ count++;
+
+ ok((yield PlacesUtils.bookmarks.fetch({ url })),
+ "Bookmark should be bookmarked, data should be retrievable");
+ is(getPlacesItemsCount(), count,
+ "Check the new bookmark items count");
+ is(isBookmarkAltered(), false, "Check if bookmark has been visited");
+ }
+
+ // Test on windows.
+ yield testOnWindow(false);
+ yield testOnWindow(true);
+ yield testOnWindow(false);
+});
+
+/**
+ * Function performs a really simple query on our places entries,
+ * and makes sure that the number of entries equal num_places_entries.
+ */
+function getPlacesItemsCount() {
+ // Get bookmarks count
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.includeHidden = true;
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(), options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ root.containerOpen = false;
+
+ // Get history item count
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(), options).root;
+ root.containerOpen = true;
+ cc += root.childCount;
+ root.containerOpen = false;
+
+ return cc;
+}
+
+function* checkHistoryItems() {
+ for (let i = 0; i < visitedURIs.length; i++) {
+ let visitedUri = visitedURIs[i];
+ ok((yield promiseIsURIVisited(visitedUri)), "");
+ if (/embed/.test(visitedUri.spec)) {
+ is((yield PlacesTestUtils.isPageInDB(visitedUri)), false, "Check if URI is in database");
+ } else {
+ ok((yield PlacesTestUtils.isPageInDB(visitedUri)), "Check if URI is in database");
+ }
+ }
+}
+
+/**
+ * Function attempts to check if Bookmark-A has been visited
+ * during private browsing mode, function should return false
+ *
+ * @returns false if the accessCount has not changed
+ * true if the accessCount has changed
+ */
+function isBookmarkAltered() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.maxResults = 1; // should only expect a new bookmark
+
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.bookmarksMenuFolder], 1);
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ is(root.childCount, options.maxResults, "Check new bookmarks results");
+ let node = root.getChild(0);
+ root.containerOpen = false;
+
+ return (node.accessCount != 0);
+}
diff --git a/toolkit/components/places/tests/browser/browser_bug399606.js b/toolkit/components/places/tests/browser/browser_bug399606.js
new file mode 100644
index 0000000000..b5eee0f92b
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_bug399606.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/. */
+
+gBrowser.selectedTab = gBrowser.addTab();
+
+function test() {
+ waitForExplicitFinish();
+
+ var URIs = [
+ "http://example.com/tests/toolkit/components/places/tests/browser/399606-window.location.href.html",
+ "http://example.com/tests/toolkit/components/places/tests/browser/399606-history.go-0.html",
+ "http://example.com/tests/toolkit/components/places/tests/browser/399606-location.replace.html",
+ "http://example.com/tests/toolkit/components/places/tests/browser/399606-location.reload.html",
+ "http://example.com/tests/toolkit/components/places/tests/browser/399606-httprefresh.html",
+ "http://example.com/tests/toolkit/components/places/tests/browser/399606-window.location.html",
+ ];
+ var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ // Create and add history observer.
+ var historyObserver = {
+ visitCount: Array(),
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType) {
+ info("Received onVisit: " + aURI.spec);
+ if (aURI.spec in this.visitCount)
+ this.visitCount[aURI.spec]++;
+ else
+ this.visitCount[aURI.spec] = 1;
+ },
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
+ };
+ hs.addObserver(historyObserver, false);
+
+ function confirm_results() {
+ gBrowser.removeCurrentTab();
+ hs.removeObserver(historyObserver, false);
+ for (let aURI in historyObserver.visitCount) {
+ is(historyObserver.visitCount[aURI], 1,
+ "onVisit has been received right number of times for " + aURI);
+ }
+ PlacesTestUtils.clearHistory().then(finish);
+ }
+
+ var loadCount = 0;
+ function handleLoad(aEvent) {
+ loadCount++;
+ info("new load count is " + loadCount);
+
+ if (loadCount == 3) {
+ gBrowser.removeEventListener("DOMContentLoaded", handleLoad, true);
+ gBrowser.loadURI("about:blank");
+ executeSoon(check_next_uri);
+ }
+ }
+
+ function check_next_uri() {
+ if (URIs.length) {
+ let uri = URIs.shift();
+ loadCount = 0;
+ gBrowser.addEventListener("DOMContentLoaded", handleLoad, true);
+ gBrowser.loadURI(uri);
+ }
+ else {
+ confirm_results();
+ }
+ }
+ executeSoon(check_next_uri);
+}
diff --git a/toolkit/components/places/tests/browser/browser_bug461710.js b/toolkit/components/places/tests/browser/browser_bug461710.js
new file mode 100644
index 0000000000..12af87a064
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_bug461710.js
@@ -0,0 +1,82 @@
+const kRed = "rgb(255, 0, 0)";
+const kBlue = "rgb(0, 0, 255)";
+
+const prefix = "http://example.com/tests/toolkit/components/places/tests/browser/461710_";
+
+add_task(function* () {
+ let contentPage = prefix + "iframe.html";
+ let normalWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+ let browser = normalWindow.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(browser, contentPage);
+ yield BrowserTestUtils.browserLoaded(browser, contentPage);
+
+ let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+
+ browser = privateWindow.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(browser, contentPage);
+ yield BrowserTestUtils.browserLoaded(browser, contentPage);
+
+ let tests = [{
+ win: normalWindow,
+ topic: "uri-visit-saved",
+ subtest: "visited_page.html"
+ }, {
+ win: normalWindow,
+ topic: "visited-status-resolution",
+ subtest: "link_page.html",
+ color: kRed,
+ message: "Visited link coloring should work outside of private mode"
+ }, {
+ win: privateWindow,
+ topic: "visited-status-resolution",
+ subtest: "link_page-2.html",
+ color: kBlue,
+ message: "Visited link coloring should not work inside of private mode"
+ }, {
+ win: normalWindow,
+ topic: "visited-status-resolution",
+ subtest: "link_page-3.html",
+ color: kRed,
+ message: "Visited link coloring should work outside of private mode"
+ }];
+
+ let visited_page_url = prefix + tests[0].subtest;
+ for (let test of tests) {
+ let promise = new Promise(resolve => {
+ let uri = NetUtil.newURI(visited_page_url);
+ Services.obs.addObserver(function observe(aSubject) {
+ if (uri.equals(aSubject.QueryInterface(Ci.nsIURI))) {
+ Services.obs.removeObserver(observe, test.topic);
+ resolve();
+ }
+ }, test.topic, false);
+ });
+ ContentTask.spawn(test.win.gBrowser.selectedBrowser, prefix + test.subtest, function* (aSrc) {
+ content.document.getElementById("iframe").src = aSrc;
+ });
+ yield promise;
+
+ if (test.color) {
+ // In e10s waiting for visited-status-resolution is not enough to ensure links
+ // have been updated, because it only tells us that messages to update links
+ // have been dispatched. We must still wait for the actual links to update.
+ yield BrowserTestUtils.waitForCondition(function* () {
+ let color = yield ContentTask.spawn(test.win.gBrowser.selectedBrowser, null, function* () {
+ let iframe = content.document.getElementById("iframe");
+ let elem = iframe.contentDocument.getElementById("link");
+ return content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .getVisitedDependentComputedStyle(elem, "", "color");
+ });
+ return (color == test.color);
+ }, test.message);
+ // The harness will consider the test as failed overall if there were no
+ // passes or failures, so record it as a pass.
+ ok(true, test.message);
+ }
+ }
+
+ yield BrowserTestUtils.closeWindow(normalWindow);
+ yield BrowserTestUtils.closeWindow(privateWindow);
+});
diff --git a/toolkit/components/places/tests/browser/browser_bug646422.js b/toolkit/components/places/tests/browser/browser_bug646422.js
new file mode 100644
index 0000000000..1a81de4e15
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_bug646422.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 646224. Make sure that after changing the URI via
+ * history.pushState, the history service has a title stored for the new URI.
+ **/
+
+add_task(function* () {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, 'http://example.com');
+
+ let newTitlePromise = new Promise(resolve => {
+ let observer = {
+ onTitleChanged: function(uri, title) {
+ // If the uri of the page whose title is changing ends with 'new_page',
+ // then it's the result of our pushState.
+ if (/new_page$/.test(uri.spec)) {
+ resolve(title);
+ PlacesUtils.history.removeObserver(observer);
+ }
+ },
+
+ onBeginUpdateBatch: function() { },
+ onEndUpdateBatch: function() { },
+ onVisit: function() { },
+ onDeleteURI: function() { },
+ onClearHistory: function() { },
+ onPageChanged: function() { },
+ onDeleteVisits: function() { },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
+ };
+
+ PlacesUtils.history.addObserver(observer, false);
+ });
+
+ yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+ let title = content.document.title;
+ content.history.pushState('', '', 'new_page');
+ Assert.ok(title, "Content window should initially have a title.");
+ });
+
+ let newtitle = yield newTitlePromise;
+
+ yield ContentTask.spawn(tab.linkedBrowser, { newtitle }, function* (args) {
+ Assert.equal(args.newtitle, content.document.title, "Title after pushstate.");
+ });
+
+ yield PlacesTestUtils.clearHistory();
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/places/tests/browser/browser_bug680727.js b/toolkit/components/places/tests/browser/browser_bug680727.js
new file mode 100644
index 0000000000..560cbfe6cc
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_bug680727.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Ensure that clicking the button in the Offline mode neterror page updates
+ global history. See bug 680727. */
+/* TEST_PATH=toolkit/components/places/tests/browser/browser_bug680727.js make -C $(OBJDIR) mochitest-browser-chrome */
+
+
+const kUniqueURI = Services.io.newURI("http://mochi.test:8888/#bug_680727",
+ null, null);
+var gAsyncHistory =
+ Cc["@mozilla.org/browser/history;1"].getService(Ci.mozIAsyncHistory);
+
+var proxyPrefValue;
+var ourTab;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Tests always connect to localhost, and per bug 87717, localhost is now
+ // reachable in offline mode. To avoid this, disable any proxy.
+ proxyPrefValue = Services.prefs.getIntPref("network.proxy.type");
+ Services.prefs.setIntPref("network.proxy.type", 0);
+
+ // Clear network cache.
+ Components.classes["@mozilla.org/netwerk/cache-storage-service;1"]
+ .getService(Components.interfaces.nsICacheStorageService)
+ .clear();
+
+ // Go offline, expecting the error page.
+ Services.io.offline = true;
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser).then(tab => {
+ ourTab = tab;
+ BrowserTestUtils.waitForContentEvent(ourTab.linkedBrowser, "DOMContentLoaded")
+ .then(errorListener);
+ BrowserTestUtils.loadURI(ourTab.linkedBrowser, kUniqueURI.spec);
+ });
+}
+
+// ------------------------------------------------------------------------------
+// listen to loading the neterror page. (offline mode)
+function errorListener() {
+ ok(Services.io.offline, "Services.io.offline is true.");
+
+ // This is an error page.
+ ContentTask.spawn(ourTab.linkedBrowser, kUniqueURI.spec, function(uri) {
+ Assert.equal(content.document.documentURI.substring(0, 27),
+ "about:neterror?e=netOffline", "Document URI is the error page.");
+
+ // But location bar should show the original request.
+ Assert.equal(content.location.href, uri, "Docshell URI is the original URI.");
+ }).then(() => {
+ // Global history does not record URI of a failed request.
+ return PlacesTestUtils.promiseAsyncUpdates().then(() => {
+ gAsyncHistory.isURIVisited(kUniqueURI, errorAsyncListener);
+ });
+ });
+}
+
+function errorAsyncListener(aURI, aIsVisited) {
+ ok(kUniqueURI.equals(aURI) && !aIsVisited,
+ "The neterror page is not listed in global history.");
+
+ Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
+
+ // Now press the "Try Again" button, with offline mode off.
+ Services.io.offline = false;
+
+ BrowserTestUtils.waitForContentEvent(ourTab.linkedBrowser, "DOMContentLoaded")
+ .then(reloadListener);
+
+ ContentTask.spawn(ourTab.linkedBrowser, null, function() {
+ Assert.ok(content.document.getElementById("errorTryAgain"),
+ "The error page has got a #errorTryAgain element");
+ content.document.getElementById("errorTryAgain").click();
+ });
+}
+
+// ------------------------------------------------------------------------------
+// listen to reload of neterror.
+function reloadListener() {
+ // This listener catches "DOMContentLoaded" on being called
+ // nsIWPL::onLocationChange(...). That is right *AFTER*
+ // IHistory::VisitURI(...) is called.
+ ok(!Services.io.offline, "Services.io.offline is false.");
+
+ ContentTask.spawn(ourTab.linkedBrowser, kUniqueURI.spec, function(uri) {
+ // This is not an error page.
+ Assert.equal(content.document.documentURI, uri,
+ "Document URI is not the offline-error page, but the original URI.");
+ }).then(() => {
+ // Check if global history remembers the successfully-requested URI.
+ PlacesTestUtils.promiseAsyncUpdates().then(() => {
+ gAsyncHistory.isURIVisited(kUniqueURI, reloadAsyncListener);
+ });
+ });
+}
+
+function reloadAsyncListener(aURI, aIsVisited) {
+ ok(kUniqueURI.equals(aURI) && aIsVisited, "We have visited the URI.");
+ PlacesTestUtils.clearHistory().then(finish);
+}
+
+registerCleanupFunction(function* () {
+ Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
+ Services.io.offline = false;
+ yield BrowserTestUtils.removeTab(ourTab);
+});
diff --git a/toolkit/components/places/tests/browser/browser_colorAnalyzer.js b/toolkit/components/places/tests/browser/browser_colorAnalyzer.js
new file mode 100644
index 0000000000..7b7fe6ec54
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_colorAnalyzer.js
@@ -0,0 +1,259 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const CA = Cc["@mozilla.org/places/colorAnalyzer;1"].
+ getService(Ci.mozIColorAnalyzer);
+
+const hiddenWindowDoc = Cc["@mozilla.org/appshell/appShellService;1"].
+ getService(Ci.nsIAppShellService).
+ hiddenDOMWindow.document;
+
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * Passes the given uri to findRepresentativeColor.
+ * If expected is null, you expect it to fail.
+ * If expected is a function, it will call that function.
+ * If expected is a color, you expect that color to be returned.
+ * Message is used in the calls to is().
+ */
+function frcTest(uri, expected, message) {
+ return new Promise(resolve => {
+ CA.findRepresentativeColor(Services.io.newURI(uri, "", null),
+ function(success, color) {
+ if (expected == null) {
+ ok(!success, message);
+ } else if (typeof expected == "function") {
+ expected(color, message);
+ } else {
+ ok(success, "success: " + message);
+ is(color, expected, message);
+ }
+ resolve();
+ });
+ });
+}
+
+/**
+ * Handy function for getting an image into findRepresentativeColor and testing it.
+ * Makes a canvas with the given dimensions, calls paintCanvasFunc with the 2d
+ * context of the canvas, sticks the generated canvas into findRepresentativeColor.
+ * See frcTest.
+ */
+function canvasTest(width, height, paintCanvasFunc, expected, message) {
+ let canvas = hiddenWindowDoc.createElementNS(XHTML_NS, "canvas");
+ canvas.width = width;
+ canvas.height = height;
+ paintCanvasFunc(canvas.getContext("2d"));
+ let uri = canvas.toDataURL();
+ return frcTest(uri, expected, message);
+}
+
+// simple test - draw a red box in the center, make sure we get red back
+add_task(function* test_redSquare() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "red";
+ ctx.fillRect(2, 2, 12, 12);
+ }, 0xFF0000, "redSquare analysis returns red");
+});
+
+
+// draw a blue square in one corner, red in the other, such that blue overlaps
+// red by one pixel, making it the dominant color
+add_task(function* test_blueOverlappingRed() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, 8, 8);
+ ctx.fillStyle = "blue";
+ ctx.fillRect(7, 7, 8, 8);
+ }, 0x0000FF, "blueOverlappingRed analysis returns blue");
+});
+
+// draw a red gradient next to a solid blue rectangle to ensure that a large
+// block of similar colors beats out a smaller block of one color
+add_task(function* test_redGradientBlueSolid() {
+ yield canvasTest(16, 16, function(ctx) {
+ let gradient = ctx.createLinearGradient(0, 0, 1, 15);
+ gradient.addColorStop(0, "#FF0000");
+ gradient.addColorStop(1, "#FF0808");
+
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, 16, 16);
+ ctx.fillStyle = "blue";
+ ctx.fillRect(9, 0, 7, 16);
+ }, function(actual, message) {
+ ok(actual >= 0xFF0000 && actual <= 0xFF0808, message);
+ }, "redGradientBlueSolid analysis returns redish");
+});
+
+// try a transparent image, should fail
+add_task(function* test_transparent() {
+ yield canvasTest(16, 16, function(ctx) {
+ // do nothing!
+ }, null, "transparent analysis fails");
+});
+
+add_task(function* test_invalidURI() {
+ yield frcTest("data:blah,Imnotavaliddatauri", null, "invalid URI analysis fails");
+});
+
+add_task(function* test_malformedPNGURI() {
+ yield frcTest("data:image/png;base64,iVBORblahblahblah", null,
+ "malformed PNG URI analysis fails");
+});
+
+add_task(function* test_unresolvableURI() {
+ yield frcTest("http://www.example.com/blah/idontexist.png", null,
+ "unresolvable URI analysis fails");
+});
+
+// draw a small blue box on a red background to make sure the algorithm avoids
+// using the background color
+add_task(function* test_blueOnRedBackground() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, 16, 16);
+ ctx.fillStyle = "blue";
+ ctx.fillRect(4, 4, 8, 8);
+ }, 0x0000FF, "blueOnRedBackground analysis returns blue");
+});
+
+// draw a slightly different color in the corners to make sure the corner colors
+// don't have to be exactly equal to be considered the background color
+add_task(function* test_variableBackground() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "white";
+ ctx.fillRect(0, 0, 16, 16);
+ ctx.fillStyle = "#FEFEFE";
+ ctx.fillRect(15, 0, 1, 1);
+ ctx.fillStyle = "#FDFDFD";
+ ctx.fillRect(15, 15, 1, 1);
+ ctx.fillStyle = "#FCFCFC";
+ ctx.fillRect(0, 15, 1, 1);
+ ctx.fillStyle = "black";
+ ctx.fillRect(4, 4, 8, 8);
+ }, 0x000000, "variableBackground analysis returns black");
+});
+
+// like the above test, but make the colors different enough that they aren't
+// considered the background color
+add_task(function* test_tooVariableBackground() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "white";
+ ctx.fillRect(0, 0, 16, 16);
+ ctx.fillStyle = "#EEDDCC";
+ ctx.fillRect(15, 0, 1, 1);
+ ctx.fillStyle = "#DDDDDD";
+ ctx.fillRect(15, 15, 1, 1);
+ ctx.fillStyle = "#CCCCCC";
+ ctx.fillRect(0, 15, 1, 1);
+ ctx.fillStyle = "black";
+ ctx.fillRect(4, 4, 8, 8);
+ }, function(actual, message) {
+ isnot(actual, 0x000000, message);
+ }, "tooVariableBackground analysis doesn't return black");
+});
+
+// draw a small black/white box over transparent background to make sure the
+// algorithm doesn't think rgb(0,0,0) == rgba(0,0,0,0)
+add_task(function* test_transparentBackgroundConflation() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "black";
+ ctx.fillRect(2, 2, 12, 12);
+ ctx.fillStyle = "white";
+ ctx.fillRect(5, 5, 6, 6);
+ }, 0x000000, "transparentBackgroundConflation analysis returns black");
+});
+
+
+// make sure we fall back to the background color if we have no other choice
+// (instead of failing as if there were no colors)
+add_task(function* test_backgroundFallback() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "black";
+ ctx.fillRect(0, 0, 16, 16);
+ }, 0x000000, "backgroundFallback analysis returns black");
+});
+
+// draw red rectangle next to a pink one to make sure the algorithm picks the
+// more interesting color
+add_task(function* test_interestingColorPreference() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "#FFDDDD";
+ ctx.fillRect(0, 0, 16, 16);
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, 3, 16);
+ }, 0xFF0000, "interestingColorPreference analysis returns red");
+});
+
+// draw high saturation but dark red next to slightly less saturated color but
+// much lighter, to make sure the algorithm doesn't pick colors that are
+// nearly black just because of high saturation (in HSL terms)
+add_task(function* test_saturationDependence() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "hsl(0, 100%, 5%)";
+ ctx.fillRect(0, 0, 16, 16);
+ ctx.fillStyle = "hsl(0, 90%, 35%)";
+ ctx.fillRect(0, 0, 8, 16);
+ }, 0xA90808, "saturationDependence analysis returns lighter red");
+});
+
+// make sure the preference for interesting colors won't stupidly pick 1 pixel
+// of red over 169 black pixels
+add_task(function* test_interestingColorPreferenceLenient() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "black";
+ ctx.fillRect(1, 1, 13, 13);
+ ctx.fillStyle = "red";
+ ctx.fillRect(3, 3, 1, 1);
+ }, 0x000000, "interestingColorPreferenceLenient analysis returns black");
+});
+
+// ...but 6 pixels of red is more reasonable
+add_task(function* test_interestingColorPreferenceNotTooLenient() {
+ yield canvasTest(16, 16, function(ctx) {
+ ctx.fillStyle = "black";
+ ctx.fillRect(1, 1, 13, 13);
+ ctx.fillStyle = "red";
+ ctx.fillRect(3, 3, 3, 2);
+ }, 0xFF0000, "interestingColorPreferenceNotTooLenient analysis returns red");
+});
+
+var maxPixels = 144; // see ColorAnalyzer MAXIMUM_PIXELS const
+
+// make sure that images larger than maxPixels*maxPixels fail
+add_task(function* test_imageTooLarge() {
+ yield canvasTest(1+maxPixels, 1+maxPixels, function(ctx) {
+ ctx.fillStyle = "red";
+ ctx.fillRect(0, 0, 1+maxPixels, 1+maxPixels);
+ }, null, "imageTooLarge analysis fails");
+});
+
+// the rest of the tests are for coverage of "real" favicons
+// exact color isn't terribly important, just make sure it's reasonable
+const filePrefix = getRootDirectory(gTestPath) + "colorAnalyzer/";
+
+add_task(function* test_categoryDiscover() {
+ yield frcTest(filePrefix + "category-discover.png", 0xB28D3A,
+ "category-discover analysis returns red");
+});
+
+add_task(function* test_localeGeneric() {
+ yield frcTest(filePrefix + "localeGeneric.png", 0x3EC23E,
+ "localeGeneric analysis returns green");
+});
+
+add_task(function* test_dictionaryGeneric() {
+ yield frcTest(filePrefix + "dictionaryGeneric-16.png", 0x854C30,
+ "dictionaryGeneric-16 analysis returns brown");
+});
+
+add_task(function* test_extensionGeneric() {
+ yield frcTest(filePrefix + "extensionGeneric-16.png", 0x53BA3F,
+ "extensionGeneric-16 analysis returns green");
+});
diff --git a/toolkit/components/places/tests/browser/browser_double_redirect.js b/toolkit/components/places/tests/browser/browser_double_redirect.js
new file mode 100644
index 0000000000..1e5dc9c166
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_double_redirect.js
@@ -0,0 +1,63 @@
+// Test for bug 411966.
+// When a page redirects multiple times, from_visit should point to the
+// previous visit in the chain, not to the first visit in the chain.
+
+add_task(function* () {
+ yield PlacesTestUtils.clearHistory();
+
+ const BASE_URL = "http://example.com/tests/toolkit/components/places/tests/browser/";
+ const TEST_URI = NetUtil.newURI(BASE_URL + "begin.html");
+ const FIRST_REDIRECTING_URI = NetUtil.newURI(BASE_URL + "redirect_twice.sjs");
+ const FINAL_URI = NetUtil.newURI(BASE_URL + "final.html");
+
+ let promiseVisits = new Promise(resolve => {
+ PlacesUtils.history.addObserver({
+ __proto__: NavHistoryObserver.prototype,
+ _notified: [],
+ onVisit: function (uri, id, time, sessionId, referrerId, transition) {
+ info("Received onVisit: " + uri.spec);
+ this._notified.push(uri);
+
+ if (!uri.equals(FINAL_URI)) {
+ return;
+ }
+
+ is(this._notified.length, 4);
+ PlacesUtils.history.removeObserver(this);
+
+ Task.spawn(function* () {
+ // Get all pages visited from the original typed one
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(
+ `SELECT url FROM moz_historyvisits
+ JOIN moz_places h ON h.id = place_id
+ WHERE from_visit IN
+ (SELECT v.id FROM moz_historyvisits v
+ JOIN moz_places p ON p.id = v.place_id
+ WHERE p.url_hash = hash(:url) AND p.url = :url)
+ `, { url: TEST_URI.spec });
+
+ is(rows.length, 1, "Found right number of visits");
+ let visitedUrl = rows[0].getResultByName("url");
+ // Check that redirect from_visit is not from the original typed one
+ is(visitedUrl, FIRST_REDIRECTING_URI.spec, "Check referrer for " + visitedUrl);
+
+ resolve();
+ });
+ }
+ }, false);
+ });
+
+ PlacesUtils.history.markPageAsTyped(TEST_URI);
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URI.spec,
+ }, function* (browser) {
+ // Load begin page, click link on page to record visits.
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#clickme", {}, browser);
+
+ yield promiseVisits;
+ });
+
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.js b/toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.js
new file mode 100644
index 0000000000..51d82adc68
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_favicon_privatebrowsing_perwindowpb.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() {
+ waitForExplicitFinish();
+
+ const pageURI =
+ "http://example.org/tests/toolkit/components/places/tests/browser/favicon.html";
+ let windowsToClose = [];
+
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(aWin) {
+ aWin.close();
+ });
+ });
+
+ function testOnWindow(aIsPrivate, aCallback) {
+ whenNewWindowLoaded({private: aIsPrivate}, function(aWin) {
+ windowsToClose.push(aWin);
+ executeSoon(() => aCallback(aWin));
+ });
+ }
+
+ function waitForTabLoad(aWin, aCallback) {
+ aWin.gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
+ aWin.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
+ aCallback();
+ }, true);
+ aWin.gBrowser.selectedBrowser.loadURI(pageURI);
+ }
+
+ testOnWindow(true, function(win) {
+ waitForTabLoad(win, function() {
+ PlacesUtils.favicons.getFaviconURLForPage(NetUtil.newURI(pageURI),
+ function(uri, dataLen, data, mimeType) {
+ is(uri, null, "No result should be found");
+ finish();
+ }
+ );
+ });
+ });
+}
diff --git a/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js
new file mode 100644
index 0000000000..60df8ebd7c
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage.js
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file tests the normal operation of setAndFetchFaviconForPage.
+function test() {
+ // Initialization
+ waitForExplicitFinish();
+ let windowsToClose = [];
+ let favIconLocation =
+ "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png";
+ let favIconURI = NetUtil.newURI(favIconLocation);
+ let favIconMimeType= "image/png";
+ let pageURI;
+ let favIconData;
+
+ function testOnWindow(aOptions, aCallback) {
+ whenNewWindowLoaded(aOptions, function(aWin) {
+ windowsToClose.push(aWin);
+ executeSoon(() => aCallback(aWin));
+ });
+ }
+
+ // This function is called after calling finish() on the test.
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(aWin) {
+ aWin.close();
+ });
+ });
+
+ function getIconFile(aCallback) {
+ NetUtil.asyncFetch({
+ uri: favIconLocation,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
+ }, function(inputStream, status) {
+ if (!Components.isSuccessCode(status)) {
+ ok(false, "Could not get the icon file");
+ // Handle error.
+ return;
+ }
+
+ // Check the returned size versus the expected size.
+ let size = inputStream.available();
+ favIconData = NetUtil.readInputStreamToString(inputStream, size);
+ is(size, favIconData.length, "Check correct icon size");
+ // Check that the favicon loaded correctly before starting the actual tests.
+ is(favIconData.length, 344, "Check correct icon length (344)");
+
+ if (aCallback) {
+ aCallback();
+ } else {
+ finish();
+ }
+ });
+ }
+
+ function testNormal(aWindow, aCallback) {
+ pageURI = NetUtil.newURI("http://example.com/normal");
+ waitForFaviconChanged(pageURI, favIconURI, aWindow,
+ function testNormalCallback() {
+ checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow,
+ aCallback);
+ }
+ );
+
+ addVisits({uri: pageURI, transition: TRANSITION_TYPED}, aWindow,
+ function () {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ }
+ );
+ }
+
+ function testAboutURIBookmarked(aWindow, aCallback) {
+ pageURI = NetUtil.newURI("about:testAboutURI_bookmarked");
+ waitForFaviconChanged(pageURI, favIconURI, aWindow,
+ function testAboutURIBookmarkedCallback() {
+ checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow,
+ aCallback);
+ }
+ );
+
+ aWindow.PlacesUtils.bookmarks.insertBookmark(
+ aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI,
+ aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec);
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ }
+
+ function testPrivateBrowsingBookmarked(aWindow, aCallback) {
+ pageURI = NetUtil.newURI("http://example.com/privateBrowsing_bookmarked");
+ waitForFaviconChanged(pageURI, favIconURI, aWindow,
+ function testPrivateBrowsingBookmarkedCallback() {
+ checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow,
+ aCallback);
+ }
+ );
+
+ aWindow.PlacesUtils.bookmarks.insertBookmark(
+ aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI,
+ aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec);
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ }
+
+ function testDisabledHistoryBookmarked(aWindow, aCallback) {
+ pageURI = NetUtil.newURI("http://example.com/disabledHistory_bookmarked");
+ waitForFaviconChanged(pageURI, favIconURI, aWindow,
+ function testDisabledHistoryBookmarkedCallback() {
+ checkFaviconDataForPage(pageURI, favIconMimeType, favIconData, aWindow,
+ aCallback);
+ }
+ );
+
+ // Disable history while changing the favicon.
+ aWindow.Services.prefs.setBoolPref("places.history.enabled", false);
+
+ aWindow.PlacesUtils.bookmarks.insertBookmark(
+ aWindow.PlacesUtils.unfiledBookmarksFolderId, pageURI,
+ aWindow.PlacesUtils.bookmarks.DEFAULT_INDEX, pageURI.spec);
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, favIconURI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ // The setAndFetchFaviconForPage function calls CanAddURI synchronously, thus
+ // we can set the preference back to true immediately. We don't clear the
+ // preference because not all products enable Places by default.
+ aWindow.Services.prefs.setBoolPref("places.history.enabled", true);
+ }
+
+ getIconFile(function () {
+ testOnWindow({}, function(aWin) {
+ testNormal(aWin, function () {
+ testOnWindow({}, function(aWin2) {
+ testAboutURIBookmarked(aWin2, function () {
+ testOnWindow({private: true}, function(aWin3) {
+ testPrivateBrowsingBookmarked(aWin3, function () {
+ testOnWindow({}, function(aWin4) {
+ testDisabledHistoryBookmarked(aWin4, finish);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js
new file mode 100644
index 0000000000..bd73af4413
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_favicon_setAndFetchFaviconForPage_failures.js
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file tests setAndFetchFaviconForPage when it is called with invalid
+ * arguments, and when no favicon is stored for the given arguments.
+ */
+function test() {
+ // Initialization
+ waitForExplicitFinish();
+ let windowsToClose = [];
+ let favIcon16Location =
+ "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal16.png";
+ let favIcon32Location =
+ "http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png";
+ let favIcon16URI = NetUtil.newURI(favIcon16Location);
+ let favIcon32URI = NetUtil.newURI(favIcon32Location);
+ let lastPageURI = NetUtil.newURI("http://example.com/verification");
+ // This error icon must stay in sync with FAVICON_ERRORPAGE_URL in
+ // nsIFaviconService.idl, aboutCertError.xhtml and netError.xhtml.
+ let favIconErrorPageURI =
+ NetUtil.newURI("chrome://global/skin/icons/warning-16.png");
+ let favIconsResultCount = 0;
+
+ function testOnWindow(aOptions, aCallback) {
+ whenNewWindowLoaded(aOptions, function(aWin) {
+ windowsToClose.push(aWin);
+ executeSoon(() => aCallback(aWin));
+ });
+ }
+
+ // This function is called after calling finish() on the test.
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(aWin) {
+ aWin.close();
+ });
+ });
+
+ function checkFavIconsDBCount(aCallback) {
+ let stmt = DBConn().createAsyncStatement("SELECT url FROM moz_favicons");
+ stmt.executeAsync({
+ handleResult: function final_handleResult(aResultSet) {
+ while (aResultSet.getNextRow()) {
+ favIconsResultCount++;
+ }
+ },
+ handleError: function final_handleError(aError) {
+ throw ("Unexpected error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function final_handleCompletion(aReason) {
+ // begin testing
+ info("Previous records in moz_favicons: " + favIconsResultCount);
+ if (aCallback) {
+ aCallback();
+ }
+ }
+ });
+ stmt.finalize();
+ }
+
+ function testNullPageURI(aWindow, aCallback) {
+ try {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(null, favIcon16URI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ throw ("Exception expected because aPageURI is null.");
+ } catch (ex) {
+ // We expected an exception.
+ ok(true, "Exception expected because aPageURI is null");
+ }
+
+ if (aCallback) {
+ aCallback();
+ }
+ }
+
+ function testNullFavIconURI(aWindow, aCallback) {
+ try {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ NetUtil.newURI("http://example.com/null_faviconURI"), null,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+ throw ("Exception expected because aFaviconURI is null.");
+ } catch (ex) {
+ // We expected an exception.
+ ok(true, "Exception expected because aFaviconURI is null.");
+ }
+
+ if (aCallback) {
+ aCallback();
+ }
+ }
+
+ function testAboutURI(aWindow, aCallback) {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ NetUtil.newURI("about:testAboutURI"), favIcon16URI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+
+ if (aCallback) {
+ aCallback();
+ }
+ }
+
+ function testPrivateBrowsingNonBookmarkedURI(aWindow, aCallback) {
+ let pageURI = NetUtil.newURI("http://example.com/privateBrowsing");
+ addVisits({ uri: pageURI, transitionType: TRANSITION_TYPED }, aWindow,
+ function () {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI,
+ favIcon16URI, true,
+ aWindow.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ }
+
+ function testDisabledHistory(aWindow, aCallback) {
+ let pageURI = NetUtil.newURI("http://example.com/disabledHistory");
+ addVisits({ uri: pageURI, transition: TRANSITION_TYPED }, aWindow,
+ function () {
+ aWindow.Services.prefs.setBoolPref("places.history.enabled", false);
+
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI,
+ favIcon16URI, true,
+ aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ // The setAndFetchFaviconForPage function calls CanAddURI synchronously, thus
+ // we can set the preference back to true immediately . We don't clear the
+ // preference because not all products enable Places by default.
+ aWindow.Services.prefs.setBoolPref("places.history.enabled", true);
+
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ }
+
+ function testErrorIcon(aWindow, aCallback) {
+ let pageURI = NetUtil.newURI("http://example.com/errorIcon");
+ addVisits({ uri: pageURI, transition: TRANSITION_TYPED }, aWindow,
+ function () {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI,
+ favIconErrorPageURI, true,
+ aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ if (aCallback) {
+ aCallback();
+ }
+ });
+ }
+
+ function testNonExistingPage(aWindow, aCallback) {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ NetUtil.newURI("http://example.com/nonexistingPage"), favIcon16URI,
+ true, aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ if (aCallback) {
+ aCallback();
+ }
+ }
+
+ function testFinalVerification(aWindow, aCallback) {
+ // Only the last test should raise the onPageChanged notification,
+ // executing the waitForFaviconChanged callback.
+ waitForFaviconChanged(lastPageURI, favIcon32URI, aWindow,
+ function final_callback() {
+ // Check that only one record corresponding to the last favicon is present.
+ let resultCount = 0;
+ let stmt = DBConn().createAsyncStatement("SELECT url FROM moz_favicons");
+ stmt.executeAsync({
+ handleResult: function final_handleResult(aResultSet) {
+
+ // If the moz_favicons DB had been previously loaded (before our
+ // test began), we should focus only in the URI we are testing and
+ // skip the URIs not related to our test.
+ if (favIconsResultCount > 0) {
+ for (let row; (row = aResultSet.getNextRow()); ) {
+ if (favIcon32URI.spec === row.getResultByIndex(0)) {
+ is(favIcon32URI.spec, row.getResultByIndex(0),
+ "Check equal favicons");
+ resultCount++;
+ }
+ }
+ } else {
+ for (let row; (row = aResultSet.getNextRow()); ) {
+ is(favIcon32URI.spec, row.getResultByIndex(0),
+ "Check equal favicons");
+ resultCount++;
+ }
+ }
+ },
+ handleError: function final_handleError(aError) {
+ throw ("Unexpected error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function final_handleCompletion(aReason) {
+ is(Ci.mozIStorageStatementCallback.REASON_FINISHED, aReason,
+ "Check reasons are equal");
+ is(1, resultCount, "Check result count");
+ if (aCallback) {
+ aCallback();
+ }
+ }
+ });
+ stmt.finalize();
+ });
+
+ // This is the only test that should cause the waitForFaviconChanged
+ // callback to be invoked. In turn, the callback will invoke
+ // finish() causing the tests to finish.
+ addVisits({ uri: lastPageURI, transition: TRANSITION_TYPED }, aWindow,
+ function () {
+ aWindow.PlacesUtils.favicons.setAndFetchFaviconForPage(lastPageURI,
+ favIcon32URI, true,
+ aWindow.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ });
+ }
+
+ checkFavIconsDBCount(function () {
+ testOnWindow({}, function(aWin) {
+ testNullPageURI(aWin, function () {
+ testOnWindow({}, function(aWin2) {
+ testNullFavIconURI(aWin2, function() {
+ testOnWindow({}, function(aWin3) {
+ testAboutURI(aWin3, function() {
+ testOnWindow({private: true}, function(aWin4) {
+ testPrivateBrowsingNonBookmarkedURI(aWin4, function () {
+ testOnWindow({}, function(aWin5) {
+ testDisabledHistory(aWin5, function () {
+ testOnWindow({}, function(aWin6) {
+ testErrorIcon(aWin6, function() {
+ testOnWindow({}, function(aWin7) {
+ testNonExistingPage(aWin7, function() {
+ testOnWindow({}, function(aWin8) {
+ testFinalVerification(aWin8, function() {
+ finish();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/toolkit/components/places/tests/browser/browser_history_post.js b/toolkit/components/places/tests/browser/browser_history_post.js
new file mode 100644
index 0000000000..c85e720f8e
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_history_post.js
@@ -0,0 +1,23 @@
+const PAGE_URI = "http://example.com/tests/toolkit/components/places/tests/browser/history_post.html";
+const SJS_URI = NetUtil.newURI("http://example.com/tests/toolkit/components/places/tests/browser/history_post.sjs");
+
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab({gBrowser, url: PAGE_URI}, Task.async(function* (aBrowser) {
+ yield ContentTask.spawn(aBrowser, null, function* () {
+ let doc = content.document;
+ let submit = doc.getElementById("submit");
+ let iframe = doc.getElementById("post_iframe");
+ let p = new Promise((resolve, reject) => {
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ resolve();
+ });
+ });
+ submit.click();
+ yield p;
+ });
+ let visited = yield promiseIsURIVisited(SJS_URI);
+ ok(!visited, "The POST page should not be added to history");
+ ok(!(yield PlacesTestUtils.isPageInDB(SJS_URI.spec)), "The page should not be in the database");
+ }));
+});
diff --git a/toolkit/components/places/tests/browser/browser_notfound.js b/toolkit/components/places/tests/browser/browser_notfound.js
new file mode 100644
index 0000000000..20467eef4b
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_notfound.js
@@ -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/. */
+
+add_task(function* () {
+ const TEST_URL = "http://mochi.test:8888/notFoundPage.html";
+
+ // Used to verify errors are not marked as typed.
+ PlacesUtils.history.markPageAsTyped(NetUtil.newURI(TEST_URL));
+
+ // Create and add history observer.
+ let visitedPromise = new Promise(resolve => {
+ let historyObserver = {
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType) {
+ PlacesUtils.history.removeObserver(historyObserver);
+ info("Received onVisit: " + aURI.spec);
+ fieldForUrl(aURI, "frecency", function (aFrecency) {
+ is(aFrecency, 0, "Frecency should be 0");
+ fieldForUrl(aURI, "hidden", function (aHidden) {
+ is(aHidden, 0, "Page should not be hidden");
+ fieldForUrl(aURI, "typed", function (aTyped) {
+ is(aTyped, 0, "page should not be marked as typed");
+ resolve();
+ });
+ });
+ });
+ },
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+ });
+
+ let newTabPromise = BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ yield Promise.all([visitedPromise, newTabPromise]);
+
+ yield PlacesTestUtils.clearHistory();
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/places/tests/browser/browser_redirect.js b/toolkit/components/places/tests/browser/browser_redirect.js
new file mode 100644
index 0000000000..d8a19731a9
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_redirect.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* () {
+ const REDIRECT_URI = NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/browser/redirect.sjs");
+ const TARGET_URI = NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/browser/redirect-target.html");
+
+ // Create and add history observer.
+ let visitedPromise = new Promise(resolve => {
+ let historyObserver = {
+ _redirectNotified: false,
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType) {
+ info("Received onVisit: " + aURI.spec);
+
+ if (aURI.equals(REDIRECT_URI)) {
+ this._redirectNotified = true;
+ // Wait for the target page notification.
+ return;
+ }
+
+ PlacesUtils.history.removeObserver(historyObserver);
+
+ ok(this._redirectNotified, "The redirect should have been notified");
+
+ fieldForUrl(REDIRECT_URI, "frecency", function (aFrecency) {
+ ok(aFrecency != 0, "Frecency or the redirecting page should not be 0");
+
+ fieldForUrl(REDIRECT_URI, "hidden", function (aHidden) {
+ is(aHidden, 1, "The redirecting page should be hidden");
+
+ fieldForUrl(TARGET_URI, "frecency", function (aFrecency2) {
+ ok(aFrecency2 != 0, "Frecency of the target page should not be 0");
+
+ fieldForUrl(TARGET_URI, "hidden", function (aHidden2) {
+ is(aHidden2, 0, "The target page should not be hidden");
+ resolve();
+ });
+ });
+ });
+ });
+ },
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+ });
+
+ let newTabPromise = BrowserTestUtils.openNewForegroundTab(gBrowser, REDIRECT_URI.spec);
+ yield Promise.all([visitedPromise, newTabPromise]);
+
+ yield PlacesTestUtils.clearHistory();
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/places/tests/browser/browser_settitle.js b/toolkit/components/places/tests/browser/browser_settitle.js
new file mode 100644
index 0000000000..68c8deda76
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_settitle.js
@@ -0,0 +1,76 @@
+var conn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+
+/**
+ * Gets a single column value from either the places or historyvisits table.
+ */
+function getColumn(table, column, url)
+{
+ var stmt = conn.createStatement(
+ `SELECT ${column} FROM ${table} WHERE url_hash = hash(:val) AND url = :val`);
+ try {
+ stmt.params.val = url;
+ stmt.executeStep();
+ return stmt.row[column];
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+add_task(function* ()
+{
+ // Make sure titles are correctly saved for a URI with the proper
+ // notifications.
+
+ // Create and add history observer.
+ let titleChangedPromise = new Promise(resolve => {
+ var historyObserver = {
+ data: [],
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType) {
+ },
+ onTitleChanged: function(aURI, aPageTitle, aGUID) {
+ this.data.push({ uri: aURI, title: aPageTitle, guid: aGUID });
+
+ // We only expect one title change.
+ //
+ // Although we are loading two different pages, the first page does not
+ // have a title. Since the title starts out as empty and then is set
+ // to empty, there is no title change notification.
+
+ PlacesUtils.history.removeObserver(this);
+ resolve(this.data);
+ },
+ onDeleteURI: function() {},
+ onClearHistory: function() {},
+ onPageChanged: function() {},
+ onDeleteVisits: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+ });
+
+ const url1 = "http://example.com/tests/toolkit/components/places/tests/browser/title1.html";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, url1);
+
+ const url2 = "http://example.com/tests/toolkit/components/places/tests/browser/title2.html";
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url2);
+ yield loadPromise;
+
+ let data = yield titleChangedPromise;
+ is(data[0].uri.spec, "http://example.com/tests/toolkit/components/places/tests/browser/title2.html");
+ is(data[0].title, "Some title");
+ is(data[0].guid, getColumn("moz_places", "guid", data[0].uri.spec));
+
+ data.forEach(function(item) {
+ var title = getColumn("moz_places", "title", data[0].uri.spec);
+ is(title, item.title);
+ });
+
+ gBrowser.removeCurrentTab();
+ yield PlacesTestUtils.clearHistory();
+});
+
diff --git a/toolkit/components/places/tests/browser/browser_visited_notfound.js b/toolkit/components/places/tests/browser/browser_visited_notfound.js
new file mode 100644
index 0000000000..b2b4f25b8e
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_visited_notfound.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_URI = NetUtil.newURI("http://mochi.test:8888/notFoundPage.html");
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ registerCleanupFunction(function() {
+ gBrowser.removeCurrentTab();
+ });
+
+ // First add a visit to the page, this will ensure that later we skip
+ // updating the frecency for a newly not-found page.
+ addVisits({ uri: TEST_URI }, window, () => {
+ info("Added visit");
+ fieldForUrl(TEST_URI, "frecency", aFrecency => {
+ ok(aFrecency > 0, "Frecency should be > 0");
+ continueTest(aFrecency);
+ });
+ });
+}
+
+function continueTest(aOldFrecency) {
+ // Used to verify errors are not marked as typed.
+ PlacesUtils.history.markPageAsTyped(TEST_URI);
+ gBrowser.selectedBrowser.loadURI(TEST_URI.spec);
+
+ // Create and add history observer.
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType) {
+ PlacesUtils.history.removeObserver(historyObserver);
+ info("Received onVisit: " + aURI.spec);
+ fieldForUrl(aURI, "frecency", function (aFrecency) {
+ is(aFrecency, aOldFrecency, "Frecency should be unchanged");
+ fieldForUrl(aURI, "hidden", function (aHidden) {
+ is(aHidden, 0, "Page should not be hidden");
+ fieldForUrl(aURI, "typed", function (aTyped) {
+ is(aTyped, 0, "page should not be marked as typed");
+ PlacesTestUtils.clearHistory().then(finish);
+ });
+ });
+ });
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
diff --git a/toolkit/components/places/tests/browser/browser_visituri.js b/toolkit/components/places/tests/browser/browser_visituri.js
new file mode 100644
index 0000000000..8ba2b72729
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_visituri.js
@@ -0,0 +1,84 @@
+/**
+ * One-time observer callback.
+ */
+function promiseObserve(name, checkFn) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject) {
+ if (checkFn(subject)) {
+ Services.obs.removeObserver(observer, name);
+ resolve();
+ }
+ }, name, false);
+ });
+}
+
+var conn = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+
+/**
+ * Gets a single column value from either the places or historyvisits table.
+ */
+function getColumn(table, column, fromColumnName, fromColumnValue) {
+ let sql = `SELECT ${column}
+ FROM ${table}
+ WHERE ${fromColumnName} = :val
+ ${fromColumnName == "url" ? "AND url_hash = hash(:val)" : ""}
+ LIMIT 1`;
+ let stmt = conn.createStatement(sql);
+ try {
+ stmt.params.val = fromColumnValue;
+ ok(stmt.executeStep(), "Expect to get a row");
+ return stmt.row[column];
+ }
+ finally {
+ stmt.reset();
+ }
+}
+
+add_task(function* () {
+ // Make sure places visit chains are saved correctly with a redirect
+ // transitions.
+
+ // Part 1: observe history events that fire when a visit occurs.
+ // Make sure visits appear in order, and that the visit chain is correct.
+ var expectedUrls = [
+ "http://example.com/tests/toolkit/components/places/tests/browser/begin.html",
+ "http://example.com/tests/toolkit/components/places/tests/browser/redirect_twice.sjs",
+ "http://example.com/tests/toolkit/components/places/tests/browser/redirect_once.sjs",
+ "http://example.com/tests/toolkit/components/places/tests/browser/final.html"
+ ];
+ var currentIndex = 0;
+
+ function checkObserver(subject) {
+ var uri = subject.QueryInterface(Ci.nsIURI);
+ var expected = expectedUrls[currentIndex];
+ is(uri.spec, expected, "Saved URL visit " + uri.spec);
+
+ var placeId = getColumn("moz_places", "id", "url", uri.spec);
+ var fromVisitId = getColumn("moz_historyvisits", "from_visit", "place_id", placeId);
+
+ if (currentIndex == 0) {
+ is(fromVisitId, 0, "First visit has no from visit");
+ }
+ else {
+ var lastVisitId = getColumn("moz_historyvisits", "place_id", "id", fromVisitId);
+ var fromVisitUrl = getColumn("moz_places", "url", "id", lastVisitId);
+ is(fromVisitUrl, expectedUrls[currentIndex - 1],
+ "From visit was " + expectedUrls[currentIndex - 1]);
+ }
+
+ currentIndex++;
+ return (currentIndex >= expectedUrls.length);
+ }
+ let visitUriPromise = promiseObserve("uri-visit-saved", checkObserver);
+
+ const testUrl = "http://example.com/tests/toolkit/components/places/tests/browser/begin.html";
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, testUrl);
+
+ // Load begin page, click link on page to record visits.
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#clickme", { }, gBrowser.selectedBrowser);
+ yield visitUriPromise;
+
+ yield PlacesTestUtils.clearHistory();
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/places/tests/browser/browser_visituri_nohistory.js b/toolkit/components/places/tests/browser/browser_visituri_nohistory.js
new file mode 100644
index 0000000000..a3a8e76261
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_visituri_nohistory.js
@@ -0,0 +1,42 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const INITIAL_URL = "http://example.com/tests/toolkit/components/places/tests/browser/begin.html";
+const FINAL_URL = "http://example.com/tests/toolkit/components/places/tests/browser/final.html";
+
+/**
+ * One-time observer callback.
+ */
+function promiseObserve(name)
+{
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject) {
+ Services.obs.removeObserver(observer, name);
+ resolve(subject);
+ }, name, false);
+ });
+}
+
+add_task(function* ()
+{
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv({"set": [["places.history.enabled", false]]}, resolve));
+
+ let visitUriPromise = promiseObserve("uri-visit-saved");
+
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, INITIAL_URL);
+
+ yield new Promise(resolve => SpecialPowers.popPrefEnv(resolve));
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.loadURI(FINAL_URL);
+ yield browserLoadedPromise;
+
+ let subject = yield visitUriPromise;
+ let uri = subject.QueryInterface(Ci.nsIURI);
+ is(uri.spec, FINAL_URL, "received expected visit");
+
+ yield PlacesTestUtils.clearHistory();
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.js b/toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.js
new file mode 100644
index 0000000000..abde69a7d9
--- /dev/null
+++ b/toolkit/components/places/tests/browser/browser_visituri_privatebrowsing_perwindowpb.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/. */
+
+function test() {
+ // initialization
+ waitForExplicitFinish();
+ let windowsToClose = [];
+ let initialURL =
+ "http://example.com/tests/toolkit/components/places/tests/browser/begin.html";
+ let finalURL =
+ "http://example.com/tests/toolkit/components/places/tests/browser/final.html";
+ let observer = null;
+ let enumerator = null;
+ let currentObserver = null;
+ let uri = null;
+
+ function doTest(aIsPrivateMode, aWindow, aTestURI, aCallback) {
+ observer = {
+ observe: function(aSubject, aTopic, aData) {
+ // The uri-visit-saved topic should only work when on normal mode.
+ if (aTopic == "uri-visit-saved") {
+ // Remove the observers set on per window private mode and normal
+ // mode.
+ enumerator = aWindow.Services.obs.enumerateObservers("uri-visit-saved");
+ while (enumerator.hasMoreElements()) {
+ currentObserver = enumerator.getNext();
+ aWindow.Services.obs.removeObserver(currentObserver, "uri-visit-saved");
+ }
+
+ // The expected visit should be the finalURL because private mode
+ // should not register a visit with the initialURL.
+ uri = aSubject.QueryInterface(Ci.nsIURI);
+ is(uri.spec, finalURL, "Check received expected visit");
+ }
+ }
+ };
+
+ aWindow.Services.obs.addObserver(observer, "uri-visit-saved", false);
+
+ BrowserTestUtils.browserLoaded(aWindow.gBrowser.selectedBrowser).then(aCallback);
+ aWindow.gBrowser.selectedBrowser.loadURI(aTestURI);
+ }
+
+ function testOnWindow(aOptions, aCallback) {
+ whenNewWindowLoaded(aOptions, function(aWin) {
+ windowsToClose.push(aWin);
+ // execute should only be called when need, like when you are opening
+ // web pages on the test. If calling executeSoon() is not necesary, then
+ // call whenNewWindowLoaded() instead of testOnWindow() on your test.
+ executeSoon(() => aCallback(aWin));
+ });
+ }
+
+ // This function is called after calling finish() on the test.
+ registerCleanupFunction(function() {
+ windowsToClose.forEach(function(aWin) {
+ aWin.close();
+ });
+ });
+
+ // test first when on private mode
+ testOnWindow({private: true}, function(aWin) {
+ doTest(true, aWin, initialURL, function() {
+ // then test when not on private mode
+ testOnWindow({}, function(aWin2) {
+ doTest(false, aWin2, finalURL, function () {
+ PlacesTestUtils.clearHistory().then(finish);
+ });
+ });
+ });
+ });
+}
diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/category-discover.png b/toolkit/components/places/tests/browser/colorAnalyzer/category-discover.png
new file mode 100644
index 0000000000..a6f5b49b37
--- /dev/null
+++ b/toolkit/components/places/tests/browser/colorAnalyzer/category-discover.png
Binary files differ
diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.png b/toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.png
new file mode 100644
index 0000000000..4ad1a1a825
--- /dev/null
+++ b/toolkit/components/places/tests/browser/colorAnalyzer/dictionaryGeneric-16.png
Binary files differ
diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.png b/toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.png
new file mode 100644
index 0000000000..fc6c8a2583
--- /dev/null
+++ b/toolkit/components/places/tests/browser/colorAnalyzer/extensionGeneric-16.png
Binary files differ
diff --git a/toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.png b/toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.png
new file mode 100644
index 0000000000..4d9ac5ad89
--- /dev/null
+++ b/toolkit/components/places/tests/browser/colorAnalyzer/localeGeneric.png
Binary files differ
diff --git a/toolkit/components/places/tests/browser/favicon-normal16.png b/toolkit/components/places/tests/browser/favicon-normal16.png
new file mode 100644
index 0000000000..62b69a3d03
--- /dev/null
+++ b/toolkit/components/places/tests/browser/favicon-normal16.png
Binary files differ
diff --git a/toolkit/components/places/tests/browser/favicon-normal32.png b/toolkit/components/places/tests/browser/favicon-normal32.png
new file mode 100644
index 0000000000..5535363c94
--- /dev/null
+++ b/toolkit/components/places/tests/browser/favicon-normal32.png
Binary files differ
diff --git a/toolkit/components/places/tests/browser/favicon.html b/toolkit/components/places/tests/browser/favicon.html
new file mode 100644
index 0000000000..a0f5ea9594
--- /dev/null
+++ b/toolkit/components/places/tests/browser/favicon.html
@@ -0,0 +1,13 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+ <head>
+ <link rel="shortcut icon" href="http://example.org/tests/toolkit/components/places/tests/browser/favicon-normal32.png">
+ </head>
+ <body>
+ OK we're done!
+ </body>
+</html>
diff --git a/toolkit/components/places/tests/browser/final.html b/toolkit/components/places/tests/browser/final.html
new file mode 100644
index 0000000000..ccd5819181
--- /dev/null
+++ b/toolkit/components/places/tests/browser/final.html
@@ -0,0 +1,10 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+ <body>
+ OK we're done!
+ </body>
+</html>
diff --git a/toolkit/components/places/tests/browser/head.js b/toolkit/components/places/tests/browser/head.js
new file mode 100644
index 0000000000..897585a81f
--- /dev/null
+++ b/toolkit/components/places/tests/browser/head.js
@@ -0,0 +1,319 @@
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserTestUtils",
+ "resource://testing-common/BrowserTestUtils.jsm");
+
+const TRANSITION_LINK = PlacesUtils.history.TRANSITION_LINK;
+const TRANSITION_TYPED = PlacesUtils.history.TRANSITION_TYPED;
+const TRANSITION_BOOKMARK = PlacesUtils.history.TRANSITION_BOOKMARK;
+const TRANSITION_REDIRECT_PERMANENT = PlacesUtils.history.TRANSITION_REDIRECT_PERMANENT;
+const TRANSITION_REDIRECT_TEMPORARY = PlacesUtils.history.TRANSITION_REDIRECT_TEMPORARY;
+const TRANSITION_EMBED = PlacesUtils.history.TRANSITION_EMBED;
+const TRANSITION_FRAMED_LINK = PlacesUtils.history.TRANSITION_FRAMED_LINK;
+const TRANSITION_DOWNLOAD = PlacesUtils.history.TRANSITION_DOWNLOAD;
+
+/**
+ * Returns a moz_places field value for a url.
+ *
+ * @param aURI
+ * The URI or spec to get field for.
+ * param aCallback
+ * Callback function that will get the property value.
+ */
+function fieldForUrl(aURI, aFieldName, aCallback)
+{
+ let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection.createAsyncStatement(
+ `SELECT ${aFieldName} FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url`
+ );
+ stmt.params.page_url = url;
+ stmt.executeAsync({
+ _value: -1,
+ handleResult: function(aResultSet) {
+ let row = aResultSet.getNextRow();
+ if (!row)
+ ok(false, "The page should exist in the database");
+ this._value = row.getResultByName(aFieldName);
+ },
+ handleError: function() {},
+ handleCompletion: function(aReason) {
+ if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
+ ok(false, "The statement should properly succeed");
+ aCallback(this._value);
+ }
+ });
+ stmt.finalize();
+}
+
+/**
+ * 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,
+ ])
+};
+
+/**
+ * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and
+ * verifies that it matches the expected page URI and associated favicon URI.
+ *
+ * This function also double-checks the GUID parameter of the notification.
+ *
+ * @param aExpectedPageURI
+ * nsIURI object of the page whose favicon should change.
+ * @param aExpectedFaviconURI
+ * nsIURI object of the newly associated favicon.
+ * @param aCallback
+ * This function is called after the check finished.
+ */
+function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, aWindow,
+ aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) {
+ if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
+ return;
+ }
+ aWindow.PlacesUtils.history.removeObserver(this);
+
+ ok(aURI.equals(aExpectedPageURI),
+ "Check URIs are equal for the page which favicon changed");
+ is(aValue, aExpectedFaviconURI.spec,
+ "Check changed favicon URI is the expected");
+ checkGuidForURI(aURI, aGUID);
+
+ if (aCallback) {
+ aCallback();
+ }
+ }
+ };
+ aWindow.PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+/**
+ * Asynchronously adds visits to a page, invoking a callback function when done.
+ *
+ * @param aPlaceInfo
+ * Either an nsIURI, in such a case a single LINK visit will be added.
+ * Or 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.
+ * @param [optional] aStack
+ * The stack frame used to report errors.
+ */
+function addVisits(aPlaceInfo, aWindow, aCallback, aStack) {
+ 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 place of places) {
+ if (!place.title) {
+ place.title = "test visit for " + place.uri.spec;
+ }
+ place.visits = [{
+ transitionType: place.transition === undefined ? TRANSITION_LINK
+ : place.transition,
+ visitDate: place.visitDate || (now++) * 1000,
+ referrerURI: place.referrer
+ }];
+ }
+
+ aWindow.PlacesUtils.asyncHistory.updatePlaces(
+ places,
+ {
+ handleError: function AAV_handleError() {
+ throw ("Unexpected error in adding visit.");
+ },
+ handleResult: function () {},
+ handleCompletion: function UP_handleCompletion() {
+ if (aCallback)
+ aCallback();
+ }
+ }
+ );
+}
+
+/**
+ * Checks that the favicon for the given page matches the provided data.
+ *
+ * @param aPageURI
+ * nsIURI object for the page to check.
+ * @param aExpectedMimeType
+ * Expected MIME type of the icon, for example "image/png".
+ * @param aExpectedData
+ * Expected icon data, expressed as an array of byte values.
+ * @param aCallback
+ * This function is called after the check finished.
+ */
+function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData,
+ aWindow, aCallback) {
+ aWindow.PlacesUtils.favicons.getFaviconDataForPage(aPageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ is(aExpectedMimeType, aMimeType, "Check expected MimeType");
+ is(aExpectedData.length, aData.length,
+ "Check favicon data for the given page matches the provided data");
+ checkGuidForURI(aPageURI);
+ aCallback();
+ });
+}
+
+/**
+ * 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 checkGuidForURI(aURI, aGUID) {
+ let guid = doGetGuidForURI(aURI);
+ if (aGUID) {
+ doCheckValidPlacesGuid(aGUID);
+ is(guid, aGUID, "Check equal guid for URIs");
+ }
+}
+
+/**
+ * Retrieves the guid for a given uri.
+ *
+ * @param aURI
+ * The uri to check.
+ * @return the associated the guid.
+ */
+function doGetGuidForURI(aURI) {
+ let stmt = DBConn().createStatement(
+ `SELECT guid
+ FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = aURI.spec;
+ ok(stmt.executeStep(), "Check get guid for uri from moz_places");
+ let guid = stmt.row.guid;
+ stmt.finalize();
+ doCheckValidPlacesGuid(guid);
+ return guid;
+}
+
+/**
+ * Tests if a given guid is valid for use in Places or not.
+ *
+ * @param aGuid
+ * The guid to test.
+ */
+function doCheckValidPlacesGuid(aGuid) {
+ ok(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), "Check guid for valid places");
+}
+
+/**
+ * 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.
+ */
+function DBConn(aForceNewConnection) {
+ let gDBConn;
+ 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.
+ Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(DBCloseCallback, aTopic);
+ dbConn.asyncClose();
+ }, "profile-before-change", false);
+ }
+
+ return gDBConn.connectionReady ? gDBConn : null;
+}
+
+function whenNewWindowLoaded(aOptions, aCallback) {
+ BrowserTestUtils.waitForNewWindow().then(aCallback);
+ OpenBrowserWindow(aOptions);
+}
+
+/**
+ * Asynchronously check a url is visited.
+ *
+ * @param aURI The URI.
+ * @param aExpectedValue The expected value.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI, aExpectedValue) {
+ return new Promise(resolve => {
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+ resolve(aIsVisited);
+ });
+ });
+}
+
+function waitForCondition(condition, nextTest, errorMsg) {
+ let tries = 0;
+ let interval = setInterval(function() {
+ if (tries >= 30) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ let conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 200);
+ function moveOn() {
+ clearInterval(interval);
+ nextTest();
+ }
+}
diff --git a/toolkit/components/places/tests/browser/history_post.html b/toolkit/components/places/tests/browser/history_post.html
new file mode 100644
index 0000000000..a579a9b8ae
--- /dev/null
+++ b/toolkit/components/places/tests/browser/history_post.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test post pages are not added to history</title>
+ </head>
+ <body>
+ <iframe name="post_iframe" id="post_iframe"></iframe>
+ <form method="post" action="http://example.com/tests/toolkit/components/places/tests/browser/history_post.sjs" target="post_iframe">
+ <input type="submit" id="submit"/>
+ </form>
+ </body>
+</html>
diff --git a/toolkit/components/places/tests/browser/history_post.sjs b/toolkit/components/places/tests/browser/history_post.sjs
new file mode 100644
index 0000000000..3c86aad7bc
--- /dev/null
+++ b/toolkit/components/places/tests/browser/history_post.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response)
+{
+ response.setStatusLine("1.0", 200, "OK");
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ response.write("Ciao");
+}
diff --git a/toolkit/components/places/tests/browser/redirect-target.html b/toolkit/components/places/tests/browser/redirect-target.html
new file mode 100644
index 0000000000..3700263385
--- /dev/null
+++ b/toolkit/components/places/tests/browser/redirect-target.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><body><p>Ciao!</p></body></html>
diff --git a/toolkit/components/places/tests/browser/redirect.sjs b/toolkit/components/places/tests/browser/redirect.sjs
new file mode 100644
index 0000000000..f55e78eb16
--- /dev/null
+++ b/toolkit/components/places/tests/browser/redirect.sjs
@@ -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/. */
+
+function handleRequest(request, response)
+{
+ let page = "<!DOCTYPE html><html><body><p>Redirecting...</p></body></html>";
+
+ response.setStatusLine(request.httpVersion, "301", "Moved Permanently");
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", page.length + "", false);
+ response.setHeader("Location", "redirect-target.html", false);
+ response.write(page);
+}
diff --git a/toolkit/components/places/tests/browser/redirect_once.sjs b/toolkit/components/places/tests/browser/redirect_once.sjs
new file mode 100644
index 0000000000..8b2a8aa55e
--- /dev/null
+++ b/toolkit/components/places/tests/browser/redirect_once.sjs
@@ -0,0 +1,9 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+ response.setStatusLine("1.1", 301, "Found");
+ response.setHeader("Location", "final.html", false);
+}
diff --git a/toolkit/components/places/tests/browser/redirect_twice.sjs b/toolkit/components/places/tests/browser/redirect_twice.sjs
new file mode 100644
index 0000000000..099d20022e
--- /dev/null
+++ b/toolkit/components/places/tests/browser/redirect_twice.sjs
@@ -0,0 +1,9 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", "redirect_once.sjs", false);
+}
diff --git a/toolkit/components/places/tests/browser/title1.html b/toolkit/components/places/tests/browser/title1.html
new file mode 100644
index 0000000000..3c98d693ec
--- /dev/null
+++ b/toolkit/components/places/tests/browser/title1.html
@@ -0,0 +1,12 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+ <head>
+ </head>
+ <body>
+ title1.html
+ </body>
+</html>
diff --git a/toolkit/components/places/tests/browser/title2.html b/toolkit/components/places/tests/browser/title2.html
new file mode 100644
index 0000000000..28a6b69b59
--- /dev/null
+++ b/toolkit/components/places/tests/browser/title2.html
@@ -0,0 +1,14 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+ <head>
+ <title>Some title</title>
+ </head>
+ <body>
+ title2.html
+ </body>
+</html>
+
diff --git a/toolkit/components/places/tests/chrome/.eslintrc.js b/toolkit/components/places/tests/chrome/.eslintrc.js
new file mode 100644
index 0000000000..bf379df8df
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js",
+ "../../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/chrome/bad_links.atom b/toolkit/components/places/tests/chrome/bad_links.atom
new file mode 100644
index 0000000000..4469272524
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/bad_links.atom
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+
+ <title>First good item</title>
+ <link href="http://example.org/first"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+ <entry>
+
+ <title>data: link</title>
+ <link href="data:text/plain,Hi"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b</id>
+ <updated>2003-12-13T18:30:03Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+ <entry>
+
+ <title>javascript: link</title>
+ <link href="javascript:alert('Hi')"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6c</id>
+ <updated>2003-12-13T18:30:04Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+ <entry>
+
+ <title>file: link</title>
+ <link href="file:///var/"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6d</id>
+ <updated>2003-12-13T18:30:05Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+ <entry>
+
+ <title>chrome: link</title>
+ <link href="chrome://browser/content/browser.js"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6e</id>
+ <updated>2003-12-13T18:30:06Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+ <entry>
+
+ <title>Last good item</title>
+ <link href="http://example.org/last"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b</id>
+ <updated>2003-12-13T18:30:07Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+
+</feed>
diff --git a/toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul b/toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul
new file mode 100644
index 0000000000..d7bbfda673
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/browser_disableglobalhistory.xul
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window title="Test disableglobalhistory attribute on remote browsers"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="run_test();">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+
+ <browser id="inprocess_disabled" src="about:blank" type="content" disableglobalhistory="true" />
+ <browser id="inprocess_enabled" src="about:blank" type="content" />
+
+ <browser id="remote_disabled" src="about:blank" type="content" disableglobalhistory="true" />
+ <browser id="remote_enabled" src="about:blank" type="content" />
+
+ <script type="text/javascript;version=1.7">
+ const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
+
+ Cu.import("resource://testing-common/ContentTask.jsm");
+ ContentTask.setTestScope(window.opener.wrappedJSObject);
+
+ function expectUseGlobalHistory(id, expected) {
+ let browser = document.getElementById(id);
+ return ContentTask.spawn(browser, {id, expected}, function*({id, expected}) {
+ Assert.equal(docShell.useGlobalHistory, expected,
+ "Got the right useGlobalHistory state in the docShell of " + id);
+ });
+ }
+
+ function run_test() {
+ spawn_task(function*() {
+ yield expectUseGlobalHistory("inprocess_disabled", false);
+ yield expectUseGlobalHistory("inprocess_enabled", true);
+
+ yield expectUseGlobalHistory("remote_disabled", false);
+ yield expectUseGlobalHistory("remote_enabled", true);
+ window.opener.done();
+ });
+ };
+
+ </script>
+</window> \ No newline at end of file
diff --git a/toolkit/components/places/tests/chrome/chrome.ini b/toolkit/components/places/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..5ac753e730
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/chrome.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+
+[test_303567.xul]
+[test_341972a.xul]
+[test_341972b.xul]
+[test_342484.xul]
+[test_371798.xul]
+[test_381357.xul]
+[test_favicon_annotations.xul]
+[test_reloadLivemarks.xul]
+[test_browser_disableglobalhistory.xul]
+support-files = browser_disableglobalhistory.xul \ No newline at end of file
diff --git a/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss b/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss
new file mode 100644
index 0000000000..612b0a5c2e
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<rss version="2.0">
+ <channel>
+ <title>feed title</title>
+ <ttl>180</ttl>
+ <item>
+ <title>linked feed item</title>
+ <link>http://feed-item-link.com</link>
+ </item>
+ <item>
+ <title>link-less feed item</title>
+ </item>
+ <item>
+ <title>linked feed item</title>
+ <link>http://feed-item-link.com</link>
+ </item>
+ </channel>
+</rss>
diff --git a/toolkit/components/places/tests/chrome/link-less-items.rss b/toolkit/components/places/tests/chrome/link-less-items.rss
new file mode 100644
index 0000000000..a30d4a3531
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/link-less-items.rss
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<rss version="2.0">
+ <channel>
+ <title>feed title</title>
+ <link>http://feed-link.com</link>
+ <ttl>180</ttl>
+ <item>
+ <title>linked feed item</title>
+ <link>http://feed-item-link.com</link>
+ </item>
+ <item>
+ <title>link-less feed item</title>
+ </item>
+ <item>
+ <title>linked feed item</title>
+ <link>http://feed-item-link.com</link>
+ </item>
+ </channel>
+</rss>
diff --git a/toolkit/components/places/tests/chrome/rss_as_html.rss b/toolkit/components/places/tests/chrome/rss_as_html.rss
new file mode 100644
index 0000000000..e823050353
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/rss_as_html.rss
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+<channel>
+<title>sadfasdfasdfasfasdf</title>
+<link>http://www.example.com</link>
+<description>asdfasdfasdf.example.com</description>
+<language>de</language>
+<copyright>asdfasdfasdfasdf</copyright>
+<lastBuildDate>Tue, 11 Mar 2008 18:52:52 +0100</lastBuildDate>
+<docs>http://blogs.law.harvard.edu/tech/rss</docs>
+<ttl>10</ttl>
+<item>
+<title>The First Title</title>
+<link>http://www.example.com/index.html</link>
+<pubDate>Tue, 11 Mar 2008 18:24:43 +0100</pubDate>
+<content:encoded>
+<![CDATA[
+<p>
+askdlfjas;dfkjas;fkdj
+</p>
+]]>
+</content:encoded>
+<description>aklsjdhfasdjfahasdfhj</description>
+<guid>http://foo.example.com/asdfasdf</guid>
+</item>
+</channel>
+</rss>
diff --git a/toolkit/components/places/tests/chrome/rss_as_html.rss^headers^ b/toolkit/components/places/tests/chrome/rss_as_html.rss^headers^
new file mode 100644
index 0000000000..04fbaa08fe
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/rss_as_html.rss^headers^
@@ -0,0 +1,2 @@
+HTTP 200 OK
+Content-Type: text/html
diff --git a/toolkit/components/places/tests/chrome/sample_feed.atom b/toolkit/components/places/tests/chrome/sample_feed.atom
new file mode 100644
index 0000000000..add75efb4d
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/sample_feed.atom
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <link href="http://example.org/"/>
+ <updated>2003-12-13T18:30:02Z</updated>
+
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+ <entry>
+
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
diff --git a/toolkit/components/places/tests/chrome/test_303567.xul b/toolkit/components/places/tests/chrome/test_303567.xul
new file mode 100644
index 0000000000..37ae77cbbd
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_303567.xul
@@ -0,0 +1,122 @@
+<?xml version="1.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="Add Bad Livemarks"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+// Test that for feeds with items that have no link:
+// * the link-less items are present in the database.
+// * the feed's site URI is substituted for each item's link.
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+const LIVEMARKS = [
+ { feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items.rss"),
+ siteURI: NetUtil.newURI("http://mochi.test:8888/"),
+ urls: [
+ "http://feed-item-link.com/",
+ "http://feed-link.com/",
+ "http://feed-item-link.com/",
+ ],
+ message: "Ensure link-less livemark item picked up site uri.",
+ },
+ { feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss"),
+ siteURI: null,
+ urls: [
+ "http://feed-item-link.com/",
+ "http://feed-item-link.com/",
+ ],
+ message: "Ensure livemark item links did not inherit site uri."
+ },
+];
+
+function runTest()
+{
+ let loadCount = 0;
+
+ function testLivemark(aLivemarkData) {
+ PlacesUtils.livemarks.addLivemark(
+ { title: "foo"
+ , parentGuid: PlacesUtils.bookmarks.toolbarGuid
+ , feedURI: aLivemarkData.feedURI
+ , siteURI: aLivemarkData.siteURI
+ })
+ .then(function (aLivemark) {
+ is (aLivemark.feedURI.spec, aLivemarkData.feedURI.spec,
+ "Get correct feedURI");
+ if (aLivemarkData.siteURI) {
+ is (aLivemark.siteURI.spec, aLivemarkData.siteURI.spec,
+ "Get correct siteURI");
+ }
+ else {
+ is (aLivemark.siteURI, null, "Get correct siteURI");
+ }
+
+ waitForLivemarkLoad(aLivemark, function (aLivemark) {
+ let nodes = aLivemark.getNodesForContainer({});
+ is(nodes.length, aLivemarkData.urls.length,
+ "Ensure all the livemark items were created.");
+ aLivemarkData.urls.forEach(function (aUrl, aIndex) {
+ let node = nodes[aIndex];
+ is(node.uri, aUrl, aLivemarkData.message);
+ });
+
+ PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => {
+ if (++loadCount == LIVEMARKS.length)
+ SimpleTest.finish();
+ });
+ });
+ }, function () {
+ is(true, false, "Should not fail adding a livemark");
+ }
+ );
+ }
+
+ LIVEMARKS.forEach(testLivemark);
+}
+
+function waitForLivemarkLoad(aLivemark, aCallback) {
+ // Don't need a real node here.
+ let node = {};
+ let resultObserver = {
+ nodeInserted: function() {},
+ nodeRemoved: function() {},
+ nodeAnnotationChanged: function() {},
+ nodeTitleChanged: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeMoved: function() {},
+ ontainerStateChanged: function () {},
+ sortingChanged: function() {},
+ batching: function() {},
+ invalidateContainer: function(node) {
+ isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED,
+ "Loading livemark should success");
+ if (aLivemark.status == Ci.mozILivemark.STATUS_READY) {
+ aLivemark.unregisterForUpdates(node, resultObserver);
+ aCallback(aLivemark);
+ }
+ }
+ };
+ aLivemark.registerForUpdates(node, resultObserver);
+ aLivemark.reload();
+}
+
+]]>
+</script>
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_341972a.xul b/toolkit/components/places/tests/chrome/test_341972a.xul
new file mode 100644
index 0000000000..7c78136a9f
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_341972a.xul
@@ -0,0 +1,87 @@
+<?xml version="1.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="Update Livemark SiteURI"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+/*
+ Test updating livemark siteURI to the value from the feed
+ */
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+function runTest() {
+ const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/sample_feed.atom";
+ const INITIALSITESPEC = "http://mochi.test:8888/";
+ const FEEDSITESPEC = "http://example.org/";
+
+ PlacesUtils.livemarks.addLivemark(
+ { title: "foo"
+ , parentGuid: PlacesUtils.bookmarks.toolbarGuid
+ , feedURI: NetUtil.newURI(FEEDSPEC)
+ , siteURI: NetUtil.newURI(INITIALSITESPEC)
+ })
+ .then(function (aLivemark) {
+ is(aLivemark.siteURI.spec, INITIALSITESPEC,
+ "Has correct initial livemark site URI");
+
+ waitForLivemarkLoad(aLivemark, function (aLivemark) {
+ is(aLivemark.siteURI.spec, FEEDSITESPEC,
+ "livemark site URI set to value in feed");
+
+ PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ }, function () {
+ is(true, false, "Should not fail adding a livemark");
+ }
+ );
+}
+
+function waitForLivemarkLoad(aLivemark, aCallback) {
+ // Don't need a real node here.
+ let node = {};
+ let resultObserver = {
+ nodeInserted: function() {},
+ nodeRemoved: function() {},
+ nodeAnnotationChanged: function() {},
+ nodeTitleChanged: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeMoved: function() {},
+ ontainerStateChanged: function () {},
+ sortingChanged: function() {},
+ batching: function() {},
+ invalidateContainer: function(node) {
+ isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED,
+ "Loading livemark should success");
+ if (aLivemark.status == Ci.mozILivemark.STATUS_READY) {
+ aLivemark.unregisterForUpdates(node, resultObserver);
+ aCallback(aLivemark);
+ }
+ }
+ };
+ aLivemark.registerForUpdates(node, resultObserver);
+ aLivemark.reload();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_341972b.xul b/toolkit/components/places/tests/chrome/test_341972b.xul
new file mode 100644
index 0000000000..86cdc75f34
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_341972b.xul
@@ -0,0 +1,84 @@
+<?xml version="1.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="Update Livemark SiteURI, null to start"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+/*
+ Test updating livemark siteURI to the value from the feed, when it's null
+ */
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+function runTest() {
+ const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/sample_feed.atom";
+ const FEEDSITESPEC = "http://example.org/";
+
+ PlacesUtils.livemarks.addLivemark(
+ { title: "foo"
+ , parentGuid: PlacesUtils.bookmarks.toolbarGuid
+ , feedURI: NetUtil.newURI(FEEDSPEC)
+ })
+ .then(function (aLivemark) {
+ is(aLivemark.siteURI, null, "Has null livemark site URI");
+
+ waitForLivemarkLoad(aLivemark, function (aLivemark) {
+ is(aLivemark.siteURI.spec, FEEDSITESPEC,
+ "livemark site URI set to value in feed");
+
+ PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ }, function () {
+ is(true, false, "Should not fail adding a livemark");
+ }
+ );
+}
+
+function waitForLivemarkLoad(aLivemark, aCallback) {
+ // Don't need a real node here.
+ let node = {};
+ let resultObserver = {
+ nodeInserted: function() {},
+ nodeRemoved: function() {},
+ nodeAnnotationChanged: function() {},
+ nodeTitleChanged: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeMoved: function() {},
+ ontainerStateChanged: function () {},
+ sortingChanged: function() {},
+ batching: function() {},
+ invalidateContainer: function(node) {
+ isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED,
+ "Loading livemark should success");
+ if (aLivemark.status == Ci.mozILivemark.STATUS_READY) {
+ aLivemark.unregisterForUpdates(node, resultObserver);
+ aCallback(aLivemark);
+ }
+ }
+ };
+ aLivemark.registerForUpdates(node, resultObserver);
+ aLivemark.reload();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_342484.xul b/toolkit/components/places/tests/chrome/test_342484.xul
new file mode 100644
index 0000000000..353313abb4
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_342484.xul
@@ -0,0 +1,88 @@
+<?xml version="1.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="Add Bad Livemarks"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+/*
+ Test loading feeds with items that aren't allowed
+ */
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+function runTest() {
+ const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/bad_links.atom";
+ const GOOD_URLS = ["http://example.org/first", "http://example.org/last"];
+
+ PlacesUtils.livemarks.addLivemark(
+ { title: "foo"
+ , parentGuid: PlacesUtils.bookmarks.toolbarGuid
+ , feedURI: NetUtil.newURI(FEEDSPEC)
+ , siteURI: NetUtil.newURI("http:/mochi.test/")
+ })
+ .then(function (aLivemark) {
+ waitForLivemarkLoad(aLivemark, function (aLivemark) {
+ let nodes = aLivemark.getNodesForContainer({});
+
+ is(nodes.length, 2, "Created the two good livemark items");
+ for (let i = 0; i < nodes.length; ++i) {
+ let node = nodes[i];
+ ok(GOOD_URLS.includes(node.uri), "livemark item created with bad uri " + node.uri);
+ }
+
+ PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ }, function () {
+ is(true, false, "Should not fail adding a livemark");
+ }
+ );
+}
+
+function waitForLivemarkLoad(aLivemark, aCallback) {
+ // Don't need a real node here.
+ let node = {};
+ let resultObserver = {
+ nodeInserted: function() {},
+ nodeRemoved: function() {},
+ nodeAnnotationChanged: function() {},
+ nodeTitleChanged: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeMoved: function() {},
+ ontainerStateChanged: function () {},
+ sortingChanged: function() {},
+ batching: function() {},
+ invalidateContainer: function(node) {
+ isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED,
+ "Loading livemark should success");
+ if (aLivemark.status == Ci.mozILivemark.STATUS_READY) {
+ aLivemark.unregisterForUpdates(node, resultObserver);
+ aCallback(aLivemark);
+ }
+ }
+ };
+ aLivemark.registerForUpdates(node, resultObserver);
+ aLivemark.reload();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_371798.xul b/toolkit/components/places/tests/chrome/test_371798.xul
new file mode 100644
index 0000000000..241db75c3c
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_371798.xul
@@ -0,0 +1,101 @@
+<?xml version="1.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="Bug 371798"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+// Test the asynchronous live-updating of bookmarks query results
+SimpleTest.waitForExplicitFinish();
+
+var {utils: Cu, interfaces: Ci} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+const TEST_URI = NetUtil.newURI("http://foo.com");
+
+function promiseOnItemChanged() {
+ return new Promise(resolve => {
+ PlacesUtils.bookmarks.addObserver({
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemAdded() {},
+ onItemRemoved() {},
+ onItemVisited() {},
+ onItemMoved() {},
+
+ onItemChanged() {
+ PlacesUtils.bookmarks.removeObserver(this);
+ resolve();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+ }, false);
+ });
+}
+
+Task.spawn(function* () {
+ // add 2 bookmarks to the toolbar, same URI, different titles (set later)
+ let bm1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URI
+ });
+
+ let bm2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URI
+ });
+
+ // query for bookmarks
+ let rootNode = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+
+ // set up observer
+ let promiseObserved = promiseOnItemChanged();
+
+ // modify the bookmark's title
+ yield PlacesUtils.bookmarks.update({
+ guid: bm2.guid, title: "foo"
+ });
+
+ // wait for notification
+ yield promiseObserved;
+
+ // Continue after our observer gets notified of onItemChanged
+ // which is triggered by updating the item's title.
+ // After receiving the notification, our original query should also
+ // have been live-updated, so we can iterate through its children,
+ // to check that only the modified bookmark has changed.
+
+ // result node should be updated
+ let cc = rootNode.childCount;
+ for (let i = 0; i < cc; ++i) {
+ let node = rootNode.getChild(i);
+ // test that bm1 does not have new title
+ if (node.bookmarkGuid == bm1.guid)
+ ok(node.title != "foo",
+ "Changing a bookmark's title did not affect the title of other bookmarks with the same URI");
+ }
+ rootNode.containerOpen = false;
+
+ // clean up
+ yield PlacesUtils.bookmarks.remove(bm1);
+ yield PlacesUtils.bookmarks.remove(bm2);
+}).catch(err => {
+ ok(false, `uncaught error: ${err}`);
+}).then(() => {
+ SimpleTest.finish();
+});
+]]>
+</script>
+
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_381357.xul b/toolkit/components/places/tests/chrome/test_381357.xul
new file mode 100644
index 0000000000..6bd6cb0245
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_381357.xul
@@ -0,0 +1,85 @@
+<?xml version="1.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="Add Livemarks from RSS feed served as text/html"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+/*
+ Test loading feeds with text/html
+ */
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+function runTest() {
+ const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/rss_as_html.rss";
+
+ PlacesUtils.livemarks.addLivemark(
+ { title: "foo"
+ , parentGuid: PlacesUtils.bookmarks.toolbarGuid
+ , feedURI: NetUtil.newURI(FEEDSPEC)
+ , siteURI: NetUtil.newURI("http:/mochi.test/")
+ })
+ .then(function (aLivemark) {
+ waitForLivemarkLoad(aLivemark, function (aLivemark) {
+ let nodes = aLivemark.getNodesForContainer({});
+
+ is(nodes[0].title, "The First Title",
+ "livemark site URI set to value in feed");
+
+ PlacesUtils.livemarks.removeLivemark(aLivemark).then(() => {
+ SimpleTest.finish();
+ });
+ });
+ }, function () {
+ is(true, false, "Should not fail adding a livemark");
+ SimpleTest.finish();
+ }
+ );
+}
+
+function waitForLivemarkLoad(aLivemark, aCallback) {
+ // Don't need a real node here.
+ let node = {};
+ let resultObserver = {
+ nodeInserted: function() {},
+ nodeRemoved: function() {},
+ nodeAnnotationChanged: function() {},
+ nodeTitleChanged: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeMoved: function() {},
+ ontainerStateChanged: function () {},
+ sortingChanged: function() {},
+ batching: function() {},
+ invalidateContainer: function(node) {
+ isnot(aLivemark.status, Ci.mozILivemark.STATUS_FAILED,
+ "Loading livemark should success");
+ if (aLivemark.status == Ci.mozILivemark.STATUS_READY) {
+ aLivemark.unregisterForUpdates(node, resultObserver);
+ aCallback(aLivemark);
+ }
+ }
+ };
+ aLivemark.registerForUpdates(node, resultObserver);
+ aLivemark.reload();
+}
+
+]]>
+</script>
+
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul b/toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul
new file mode 100644
index 0000000000..3a84f30303
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_browser_disableglobalhistory.xul
@@ -0,0 +1,26 @@
+<?xml version="1.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="Test disableglobalhistory attribute on remote browsers"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <script type="text/javascript;version=1.7">
+ SimpleTest.waitForExplicitFinish();
+
+ let w = window.open('browser_disableglobalhistory.xul', '_blank', 'chrome,resizable=yes,width=400,height=600');
+
+ function done() {
+ w.close();
+ SimpleTest.finish();
+ }
+ </script>
+
+</window> \ No newline at end of file
diff --git a/toolkit/components/places/tests/chrome/test_favicon_annotations.xul b/toolkit/components/places/tests/chrome/test_favicon_annotations.xul
new file mode 100644
index 0000000000..b7647cbc68
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_favicon_annotations.xul
@@ -0,0 +1,168 @@
+<?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 file tests the moz-anno protocol, which was added in Bug 316077 and how
+ * it loads favicons.
+-->
+
+<window title="Favicon Annotation Protocol Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+let fs = Cc["@mozilla.org/browser/favicon-service;1"].
+ getService(Ci.nsIFaviconService);
+
+// Test descriptions that will be printed in the case of failure.
+let testDescriptions = [
+ "moz-anno URI with no data in the database loads default icon",
+ "URI added to the database is properly loaded",
+];
+
+// URIs to load (will be compared with expectedURIs of the same index).
+let testURIs = [
+ "http://mozilla.org/2009/made-up-favicon/places-rocks/",
+ "http://mozilla.org/should-be-barney/",
+];
+
+// URIs to load for expected results.
+let expectedURIs = [
+ fs.defaultFavicon.spec,
+ "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%10%00%00%00%10%08%06%00%00%00%1F%F3%FFa%00%00%00%04gAMA%00%00%AF%C87%05%8A%E9%00%00%00%19tEXtSoftware%00Adobe%20ImageReadyq%C9e%3C%00%00%01%D6IDATx%DAb%FC%FF%FF%3F%03%25%00%20%80%98%909%EF%DF%BFg%EF%EC%EC%FC%AD%AC%AC%FC%DF%95%91%F1%BF%89%89%C9%7F%20%FF%D7%EA%D5%AB%B7%DF%BBwO%16%9B%01%00%01%C4%00r%01%08%9F9s%C6%CD%D8%D8%F8%BF%0B%03%C3%FF3%40%BC%0A%88%EF%02q%1A%10%BB%40%F1%AAU%ABv%C1%D4%C30%40%00%81%89%993g%3E%06%1A%F6%3F%14%AA%11D%97%03%F1%7Fc%08%0D%E2%2B))%FD%17%04%89%A1%19%00%10%40%0C%D00%F8%0F3%00%C8%F8%BF%1B%E4%0Ac%88a%E5%60%17%19%FF%0F%0D%0D%05%1B%02v%D9%DD%BB%0A0%03%00%02%08%AC%B9%A3%A3%E3%17%03%D4v%90%01%EF%18%106%C3%0Cz%07%C5%BB%A1%DE%82y%07%20%80%A0%A6%08B%FCn%0C1%60%26%D4%20d%C3VA%C3%06%26%BE%0A%EA-%80%00%82%B9%E0%F7L4%0D%EF%90%F8%C6%60%2F%0A%82%BD%01%13%07%0700%D0%01%02%88%11%E4%02P%B41%DC%BB%C7%D0%014%0D%E8l%06W%20%06%BA%88%A1%1C%1AS%15%40%7C%16%CA6.%2Fgx%BFg%0F%83%CB%D9%B3%0C%7B%80%7C%80%00%02%BB%00%E8%9F%ED%20%1B%3A%A0%A6%9F%81%DA%DC%01%C5%B0%80%ED%80%FA%BF%BC%BC%FC%3F%83%12%90%9D%96%F6%1F%20%80%18%DE%BD%7B%C7%0E%8E%05AD%20%FEGr%A6%A0%A0%E0%7F%25P%80%02%9D%0F%D28%13%18%23%C6%C0%B0%02E%3D%C8%F5%00%01%04%8F%05P%A8%BA%40my%87%E4%12c%A8%8D%20%8B%D0%D3%00%08%03%04%10%9C%01R%E4%82d%3B%C8%A0%99%C6%90%90%C6%A5%19%84%01%02%08%9E%17%80%C9x%F7%7B%A0%DBVC%F9%A0%C0%5C%7D%16%2C%CE%00%F4%C6O%5C%99%09%20%800L%04y%A5%03%1A%95%A0%80%05%05%14.%DBA%18%20%80%18)%CD%CE%00%01%06%00%0C'%94%C7%C0k%C9%2C%00%00%00%00IEND%AEB%60%82",
+];
+
+
+/**
+ * The event listener placed on our test windows used to determine when it is
+ * safe to compare the two windows.
+ */
+let _results = [];
+function loadEventHandler()
+{
+ _results.push(snapshotWindow(window));
+
+ loadNextTest();
+}
+
+/**
+ * This runs the comparison.
+ */
+function compareResults(aIndex, aImage1, aImage2)
+{
+ let [correct, data1, data2] = compareSnapshots(aImage1, aImage2, true);
+ SimpleTest.ok(correct,
+ "Test '" + testDescriptions[aIndex] + "' matches expectations. " +
+ "Data from window 1 is '" + data1 + "'. " +
+ "Data from window 2 is '" + data2 + "'");
+}
+
+/**
+ * Loads the next set of URIs to compare against.
+ */
+let _counter = -1;
+function loadNextTest()
+{
+ _counter++;
+ // If we have no more tests, finish.
+ if (_counter / 2 == testDescriptions.length) {
+ for (let i = 0; i < _results.length; i = i + 2)
+ compareResults(i / 2, _results[i], _results[i + 1]);
+
+ SimpleTest.finish();
+ return;
+ }
+
+ let nextURI = function() {
+ let index = Math.floor(_counter / 2);
+ if ((_counter % 2) == 0)
+ return "moz-anno:favicon:" + testURIs[index];
+ return expectedURIs[index];
+ }
+
+ let img = document.getElementById("favicon");
+ img.setAttribute("src", nextURI());
+}
+
+function test()
+{
+ SimpleTest.waitForExplicitFinish();
+ let db = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsPIPlacesDatabase).
+ DBConnection;
+
+ // Empty any old favicons
+ db.executeSimpleSQL("DELETE FROM moz_favicons");
+
+ let ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ let uri = function(aSpec) {
+ return ios.newURI(aSpec, null, null);
+ };
+
+ let pageURI = uri("http://example.com/favicon_annotations");
+ let history = Cc["@mozilla.org/browser/history;1"]
+ .getService(Ci.mozIAsyncHistory);
+ history.updatePlaces(
+ {
+ uri: pageURI,
+ visits: [{ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ visitDate: Date.now() * 1000
+ }],
+ },
+ {
+ handleError: function UP_handleError() {
+ ok(false, "Unexpected error in adding visit.");
+ },
+ handleResult: function () {},
+ handleCompletion: function UP_handleCompletion() {
+ // Set the favicon data. Note that the "moz-anno:" protocol requires
+ // the favicon to be stored in the database, but the
+ // replaceFaviconDataFromDataURL function will not save the favicon
+ // unless it is associated with a page. Thus, we must associate the
+ // icon with a page explicitly in order for it to be visible through
+ // the protocol.
+ var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
+ .createInstance(Ci.nsIPrincipal);
+
+ fs.replaceFaviconDataFromDataURL(uri(testURIs[1]), expectedURIs[1],
+ (Date.now() + 60 * 60 * 24 * 1000) * 1000,
+ systemPrincipal);
+
+ fs.setAndFetchFaviconForPage(pageURI, uri(testURIs[1]), true,
+ fs.FAVICON_LOAD_NON_PRIVATE,
+ null, systemPrincipal);
+
+ // And start our test process.
+ loadNextTest();
+ }
+ }
+ );
+
+
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <img id="favicon" onload="loadEventHandler();"/>
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+</window>
diff --git a/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul b/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul
new file mode 100644
index 0000000000..43772d09f4
--- /dev/null
+++ b/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul
@@ -0,0 +1,155 @@
+<?xml version="1.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="Reload Livemarks"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest()" onunload="cleanup()">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+<script type="application/javascript">
+<![CDATA[
+// Test that for concurrent reload of livemarks.
+
+SimpleTest.waitForExplicitFinish();
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+
+let gLivemarks = [
+ { id: -1,
+ title: "foo",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items.rss")
+ },
+ { id: -1,
+ title: "bar",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ feedURI: NetUtil.newURI("http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/link-less-items-no-site-uri.rss")
+ },
+];
+
+function runTest()
+{
+ addLivemarks(function () {
+ reloadLivemarks(false, function () {
+ reloadLivemarks(true, function () {
+ removeLivemarks(SimpleTest.finish);
+ });
+ });
+ // Ensure this normal reload doesn't overwrite the forced one.
+ PlacesUtils.livemarks.reloadLivemarks();
+ });
+}
+
+function addLivemarks(aCallback) {
+ info("Adding livemarks");
+ let count = gLivemarks.length;
+ gLivemarks.forEach(function(aLivemarkData) {
+ PlacesUtils.livemarks.addLivemark(aLivemarkData)
+ .then(function (aLivemark) {
+ ok(aLivemark.feedURI.equals(aLivemarkData.feedURI), "Livemark added");
+ aLivemarkData.id = aLivemark.id;
+ if (--count == 0) {
+ aCallback();
+ }
+ },
+ function () {
+ is(true, false, "Should not fail adding a livemark.");
+ aCallback();
+ });
+ });
+}
+
+function reloadLivemarks(aForceUpdate, aCallback) {
+ info("Reloading livemarks with forceUpdate: " + aForceUpdate);
+ let count = gLivemarks.length;
+ gLivemarks.forEach(function(aLivemarkData) {
+ PlacesUtils.livemarks.getLivemark(aLivemarkData)
+ .then(aLivemark => {
+ ok(aLivemark.feedURI.equals(aLivemarkData.feedURI), "Livemark found");
+ aLivemarkData._observer = new resultObserver(aLivemark, function() {
+ if (++count == gLivemarks.length) {
+ aCallback();
+ }
+ });
+ if (--count == 0) {
+ PlacesUtils.livemarks.reloadLivemarks(aForceUpdate);
+ }
+ },
+ function() {
+ is(true, false, "Should not fail getting a livemark.");
+ aCallback();
+ }
+ );
+ });
+}
+
+function removeLivemarks(aCallback) {
+ info("Removing livemarks");
+ let count = gLivemarks.length;
+ gLivemarks.forEach(function(aLivemarkData) {
+ PlacesUtils.livemarks.removeLivemark(aLivemarkData).then(
+ function (aLivemark) {
+ if (--count == 0) {
+ aCallback();
+ }
+ },
+ function() {
+ is(true, false, "Should not fail adding a livemark.");
+ aCallback();
+ }
+ );
+ });
+}
+
+function resultObserver(aLivemark, aCallback) {
+ this._node = {};
+ this._livemark = aLivemark;
+ this._callback = aCallback;
+ this._livemark.registerForUpdates(this._node, this);
+}
+resultObserver.prototype = {
+ nodeInserted: function() {},
+ nodeRemoved: function() {},
+ nodeAnnotationChanged: function() {},
+ nodeTitleChanged: function() {},
+ nodeHistoryDetailsChanged: function() {},
+ nodeMoved: function() {},
+ ontainerStateChanged: function () {},
+ sortingChanged: function() {},
+ batching: function() {},
+ invalidateContainer: function(aContainer) {
+ // Wait for load finish.
+ if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING)
+ return;
+
+ this._terminate();
+ this._callback();
+ },
+ _terminate: function () {
+ if (!this._terminated) {
+ this._livemark.unregisterForUpdates(this._node);
+ this._terminated = true;
+ }
+ }
+};
+
+function cleanup() {
+ gLivemarks.forEach(function(aLivemarkData) {
+ if (aLivemarkData._observer)
+ aLivemarkData._observer._terminate();
+ });
+}
+]]>
+</script>
+</window>
diff --git a/toolkit/components/places/tests/cpp/mock_Link.h b/toolkit/components/places/tests/cpp/mock_Link.h
new file mode 100644
index 0000000000..92ef25d6a1
--- /dev/null
+++ b/toolkit/components/places/tests/cpp/mock_Link.h
@@ -0,0 +1,229 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 a mock Link object which can be used in tests.
+ */
+
+#ifndef mock_Link_h__
+#define mock_Link_h__
+
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/dom/Link.h"
+#include "mozilla/dom/URLSearchParams.h"
+
+class mock_Link : public mozilla::dom::Link
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit mock_Link(void (*aHandlerFunction)(nsLinkState),
+ bool aRunNextTest = true)
+ : mozilla::dom::Link(nullptr)
+ , mHandler(aHandlerFunction)
+ , mRunNextTest(aRunNextTest)
+ {
+ // Create a cyclic ownership, so that the link will be released only
+ // after its status has been updated. This will ensure that, when it should
+ // run the next test, it will happen at the end of the test function, if
+ // the link status has already been set before. Indeed the link status is
+ // updated on a separate connection, thus may happen at any time.
+ mDeathGrip = this;
+ }
+
+ virtual void SetLinkState(nsLinkState aState) override
+ {
+ // Notify our callback function.
+ mHandler(aState);
+
+ // Break the cycle so the object can be destroyed.
+ mDeathGrip = nullptr;
+ }
+
+ virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const override
+ {
+ return 0; // the value shouldn't matter
+ }
+
+protected:
+ ~mock_Link() {
+ // Run the next test if we are supposed to.
+ if (mRunNextTest) {
+ run_next_test();
+ }
+ }
+
+private:
+ void (*mHandler)(nsLinkState);
+ bool mRunNextTest;
+ RefPtr<Link> mDeathGrip;
+};
+
+NS_IMPL_ISUPPORTS(
+ mock_Link,
+ mozilla::dom::Link
+)
+
+////////////////////////////////////////////////////////////////////////////////
+//// Needed Link Methods
+
+namespace mozilla {
+namespace dom {
+
+Link::Link(Element* aElement)
+: mElement(aElement)
+, mLinkState(eLinkState_NotLink)
+, mRegistered(false)
+{
+}
+
+Link::~Link()
+{
+}
+
+bool
+Link::ElementHasHref() const
+{
+ NS_NOTREACHED("Unexpected call to Link::ElementHasHref");
+ return false; // suppress compiler warning
+}
+
+void
+Link::SetLinkState(nsLinkState aState)
+{
+ NS_NOTREACHED("Unexpected call to Link::SetLinkState");
+}
+
+void
+Link::ResetLinkState(bool aNotify, bool aHasHref)
+{
+ NS_NOTREACHED("Unexpected call to Link::ResetLinkState");
+}
+
+nsIURI*
+Link::GetURI() const
+{
+ NS_NOTREACHED("Unexpected call to Link::GetURI");
+ return nullptr; // suppress compiler warning
+}
+
+size_t
+Link::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ NS_NOTREACHED("Unexpected call to Link::SizeOfExcludingThis");
+ return 0;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(URLSearchParams)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(URLSearchParams)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(URLSearchParams)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(URLSearchParams)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(URLSearchParams)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(URLSearchParams)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(URLSearchParams)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+
+URLSearchParams::URLSearchParams(nsISupports* aParent,
+ URLSearchParamsObserver* aObserver)
+{
+}
+
+URLSearchParams::~URLSearchParams()
+{
+}
+
+JSObject*
+URLSearchParams::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return nullptr;
+}
+
+void
+URLSearchParams::ParseInput(const nsACString& aInput)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::ParseInput");
+}
+
+void
+URLSearchParams::Serialize(nsAString& aValue) const
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::Serialize");
+}
+
+void
+URLSearchParams::Get(const nsAString& aName, nsString& aRetval)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::Get");
+}
+
+void
+URLSearchParams::GetAll(const nsAString& aName, nsTArray<nsString >& aRetval)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::GetAll");
+}
+
+void
+URLSearchParams::Set(const nsAString& aName, const nsAString& aValue)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::Set");
+}
+
+void
+URLSearchParams::Append(const nsAString& aName, const nsAString& aValue)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::Append");
+}
+
+void
+URLSearchParams::AppendInternal(const nsAString& aName, const nsAString& aValue)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::AppendInternal");
+}
+
+bool
+URLSearchParams::Has(const nsAString& aName)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::Has");
+ return false;
+}
+
+void
+URLSearchParams::Delete(const nsAString& aName)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::Delete");
+}
+
+void
+URLSearchParams::DeleteAll()
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::DeleteAll");
+}
+
+void
+URLSearchParams::NotifyObserver()
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::NotifyObserver");
+}
+
+NS_IMETHODIMP
+URLSearchParams::GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength,
+ nsACString& aContentType, nsACString& aCharset)
+{
+ NS_NOTREACHED("Unexpected call to URLSearchParams::GetSendInfo");
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mock_Link_h__
diff --git a/toolkit/components/places/tests/cpp/moz.build b/toolkit/components/places/tests/cpp/moz.build
new file mode 100644
index 0000000000..f6bd91bd73
--- /dev/null
+++ b/toolkit/components/places/tests/cpp/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/.
+
+GeckoCppUnitTests([
+ 'test_IHistory',
+])
+
+if CONFIG['JS_SHARED_LIBRARY']:
+ USE_LIBS += [
+ 'js',
+ ]
diff --git a/toolkit/components/places/tests/cpp/places_test_harness.h b/toolkit/components/places/tests/cpp/places_test_harness.h
new file mode 100644
index 0000000000..557a25f901
--- /dev/null
+++ b/toolkit/components/places/tests/cpp/places_test_harness.h
@@ -0,0 +1,413 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "TestHarness.h"
+#include "nsMemory.h"
+#include "nsThreadUtils.h"
+#include "nsDocShellCID.h"
+
+#include "nsToolkitCompsCID.h"
+#include "nsINavHistoryService.h"
+#include "nsIObserverService.h"
+#include "nsIURI.h"
+#include "mozilla/IHistory.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageAsyncStatement.h"
+#include "mozIStorageStatementCallback.h"
+#include "mozIStoragePendingStatement.h"
+#include "nsPIPlacesDatabase.h"
+#include "nsIObserver.h"
+#include "prinrval.h"
+#include "prtime.h"
+#include "mozilla/Attributes.h"
+
+#define WAITFORTOPIC_TIMEOUT_SECONDS 5
+
+
+static size_t gTotalTests = 0;
+static size_t gPassedTests = 0;
+
+#define do_check_true(aCondition) \
+ PR_BEGIN_MACRO \
+ gTotalTests++; \
+ if (aCondition) { \
+ gPassedTests++; \
+ } else { \
+ fail("%s | Expected true, got false at line %d", __FILE__, __LINE__); \
+ } \
+ PR_END_MACRO
+
+#define do_check_false(aCondition) \
+ PR_BEGIN_MACRO \
+ gTotalTests++; \
+ if (!aCondition) { \
+ gPassedTests++; \
+ } else { \
+ fail("%s | Expected false, got true at line %d", __FILE__, __LINE__); \
+ } \
+ PR_END_MACRO
+
+#define do_check_success(aResult) \
+ do_check_true(NS_SUCCEEDED(aResult))
+
+#ifdef LINUX
+// XXX Linux opt builds on tinderbox are orange due to linking with stdlib.
+// This is sad and annoying, but it's a workaround that works.
+#define do_check_eq(aExpected, aActual) \
+ do_check_true(aExpected == aActual)
+#else
+#include <sstream>
+
+#define do_check_eq(aActual, aExpected) \
+ PR_BEGIN_MACRO \
+ gTotalTests++; \
+ if (aExpected == aActual) { \
+ gPassedTests++; \
+ } else { \
+ std::ostringstream temp; \
+ temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \
+ temp << aActual <<"' at line " << __LINE__; \
+ fail(temp.str().c_str()); \
+ } \
+ PR_END_MACRO
+#endif
+
+struct Test
+{
+ void (*func)(void);
+ const char* const name;
+};
+#define TEST(aName) \
+ {aName, #aName}
+
+/**
+ * Runs the next text.
+ */
+void run_next_test();
+
+/**
+ * To be used around asynchronous work.
+ */
+void do_test_pending();
+void do_test_finished();
+
+/**
+ * Spins current thread until a topic is received.
+ */
+class WaitForTopicSpinner final : public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit WaitForTopicSpinner(const char* const aTopic)
+ : mTopicReceived(false)
+ , mStartTime(PR_IntervalNow())
+ {
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ do_check_true(observerService);
+ (void)observerService->AddObserver(this, aTopic, false);
+ }
+
+ void Spin() {
+ while (!mTopicReceived) {
+ if ((PR_IntervalNow() - mStartTime) > (WAITFORTOPIC_TIMEOUT_SECONDS * PR_USEC_PER_SEC)) {
+ // Timed out waiting for the topic.
+ do_check_true(false);
+ break;
+ }
+ (void)NS_ProcessNextEvent();
+ }
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) override
+ {
+ mTopicReceived = true;
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ do_check_true(observerService);
+ (void)observerService->RemoveObserver(this, aTopic);
+ return NS_OK;
+ }
+
+private:
+ ~WaitForTopicSpinner() {}
+
+ bool mTopicReceived;
+ PRIntervalTime mStartTime;
+};
+NS_IMPL_ISUPPORTS(
+ WaitForTopicSpinner,
+ nsIObserver
+)
+
+/**
+ * Spins current thread until an async statement is executed.
+ */
+class AsyncStatementSpinner final : public mozIStorageStatementCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGESTATEMENTCALLBACK
+
+ AsyncStatementSpinner();
+ void SpinUntilCompleted();
+ uint16_t completionReason;
+
+protected:
+ ~AsyncStatementSpinner() {}
+
+ volatile bool mCompleted;
+};
+
+NS_IMPL_ISUPPORTS(AsyncStatementSpinner,
+ mozIStorageStatementCallback)
+
+AsyncStatementSpinner::AsyncStatementSpinner()
+: completionReason(0)
+, mCompleted(false)
+{
+}
+
+NS_IMETHODIMP
+AsyncStatementSpinner::HandleResult(mozIStorageResultSet *aResultSet)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncStatementSpinner::HandleError(mozIStorageError *aError)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncStatementSpinner::HandleCompletion(uint16_t aReason)
+{
+ completionReason = aReason;
+ mCompleted = true;
+ return NS_OK;
+}
+
+void AsyncStatementSpinner::SpinUntilCompleted()
+{
+ nsCOMPtr<nsIThread> thread(::do_GetCurrentThread());
+ nsresult rv = NS_OK;
+ bool processed = true;
+ while (!mCompleted && NS_SUCCEEDED(rv)) {
+ rv = thread->ProcessNextEvent(true, &processed);
+ }
+}
+
+struct PlaceRecord
+{
+ int64_t id;
+ int32_t hidden;
+ int32_t typed;
+ int32_t visitCount;
+ nsCString guid;
+};
+
+struct VisitRecord
+{
+ int64_t id;
+ int64_t lastVisitId;
+ int32_t transitionType;
+};
+
+already_AddRefed<mozilla::IHistory>
+do_get_IHistory()
+{
+ nsCOMPtr<mozilla::IHistory> history = do_GetService(NS_IHISTORY_CONTRACTID);
+ do_check_true(history);
+ return history.forget();
+}
+
+already_AddRefed<nsINavHistoryService>
+do_get_NavHistory()
+{
+ nsCOMPtr<nsINavHistoryService> serv =
+ do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);
+ do_check_true(serv);
+ return serv.forget();
+}
+
+already_AddRefed<mozIStorageConnection>
+do_get_db()
+{
+ nsCOMPtr<nsINavHistoryService> history = do_get_NavHistory();
+ nsCOMPtr<nsPIPlacesDatabase> database = do_QueryInterface(history);
+ do_check_true(database);
+
+ nsCOMPtr<mozIStorageConnection> dbConn;
+ nsresult rv = database->GetDBConnection(getter_AddRefs(dbConn));
+ do_check_success(rv);
+ return dbConn.forget();
+}
+
+/**
+ * Get the place record from the database.
+ *
+ * @param aURI The unique URI of the place we are looking up
+ * @param result Out parameter where the result is stored
+ */
+void
+do_get_place(nsIURI* aURI, PlaceRecord& result)
+{
+ nsCOMPtr<mozIStorageConnection> dbConn = do_get_db();
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ nsCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ do_check_success(rv);
+
+ rv = dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id, hidden, typed, visit_count, guid FROM moz_places "
+ "WHERE url_hash = hash(?1) AND url = ?1"
+ ), getter_AddRefs(stmt));
+ do_check_success(rv);
+
+ rv = stmt->BindUTF8StringByIndex(0, spec);
+ do_check_success(rv);
+
+ bool hasResults;
+ rv = stmt->ExecuteStep(&hasResults);
+ do_check_success(rv);
+ if (!hasResults) {
+ result.id = 0;
+ return;
+ }
+
+ rv = stmt->GetInt64(0, &result.id);
+ do_check_success(rv);
+ rv = stmt->GetInt32(1, &result.hidden);
+ do_check_success(rv);
+ rv = stmt->GetInt32(2, &result.typed);
+ do_check_success(rv);
+ rv = stmt->GetInt32(3, &result.visitCount);
+ do_check_success(rv);
+ rv = stmt->GetUTF8String(4, result.guid);
+ do_check_success(rv);
+}
+
+/**
+ * Gets the most recent visit to a place.
+ *
+ * @param placeID ID from the moz_places table
+ * @param result Out parameter where visit is stored
+ */
+void
+do_get_lastVisit(int64_t placeId, VisitRecord& result)
+{
+ nsCOMPtr<mozIStorageConnection> dbConn = do_get_db();
+ nsCOMPtr<mozIStorageStatement> stmt;
+
+ nsresult rv = dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id, from_visit, visit_type FROM moz_historyvisits "
+ "WHERE place_id=?1 "
+ "LIMIT 1"
+ ), getter_AddRefs(stmt));
+ do_check_success(rv);
+
+ rv = stmt->BindInt64ByIndex(0, placeId);
+ do_check_success(rv);
+
+ bool hasResults;
+ rv = stmt->ExecuteStep(&hasResults);
+ do_check_success(rv);
+
+ if (!hasResults) {
+ result.id = 0;
+ return;
+ }
+
+ rv = stmt->GetInt64(0, &result.id);
+ do_check_success(rv);
+ rv = stmt->GetInt64(1, &result.lastVisitId);
+ do_check_success(rv);
+ rv = stmt->GetInt32(2, &result.transitionType);
+ do_check_success(rv);
+}
+
+void
+do_wait_async_updates() {
+ nsCOMPtr<mozIStorageConnection> db = do_get_db();
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING("BEGIN EXCLUSIVE"),
+ getter_AddRefs(stmt));
+ nsCOMPtr<mozIStoragePendingStatement> pending;
+ (void)stmt->ExecuteAsync(nullptr, getter_AddRefs(pending));
+
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING("COMMIT"),
+ getter_AddRefs(stmt));
+ RefPtr<AsyncStatementSpinner> spinner = new AsyncStatementSpinner();
+ (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pending));
+
+ spinner->SpinUntilCompleted();
+}
+
+/**
+ * Adds a URI to the database.
+ *
+ * @param aURI
+ * The URI to add to the database.
+ */
+void
+addURI(nsIURI* aURI)
+{
+ nsCOMPtr<mozilla::IHistory> history = do_GetService(NS_IHISTORY_CONTRACTID);
+ do_check_true(history);
+ nsresult rv = history->VisitURI(aURI, nullptr, mozilla::IHistory::TOP_LEVEL);
+ do_check_success(rv);
+
+ do_wait_async_updates();
+}
+
+static const char TOPIC_PROFILE_CHANGE[] = "profile-before-change";
+static const char TOPIC_PLACES_CONNECTION_CLOSED[] = "places-connection-closed";
+
+class WaitForConnectionClosed final : public nsIObserver
+{
+ RefPtr<WaitForTopicSpinner> mSpinner;
+
+ ~WaitForConnectionClosed() {}
+
+public:
+ NS_DECL_ISUPPORTS
+
+ WaitForConnectionClosed()
+ {
+ nsCOMPtr<nsIObserverService> os =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ MOZ_ASSERT(os);
+ if (os) {
+ MOZ_ALWAYS_SUCCEEDS(os->AddObserver(this, TOPIC_PROFILE_CHANGE, false));
+ }
+ mSpinner = new WaitForTopicSpinner(TOPIC_PLACES_CONNECTION_CLOSED);
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) override
+ {
+ nsCOMPtr<nsIObserverService> os =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ MOZ_ASSERT(os);
+ if (os) {
+ MOZ_ALWAYS_SUCCEEDS(os->RemoveObserver(this, aTopic));
+ }
+
+ mSpinner->Spin();
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(WaitForConnectionClosed, nsIObserver)
diff --git a/toolkit/components/places/tests/cpp/places_test_harness_tail.h b/toolkit/components/places/tests/cpp/places_test_harness_tail.h
new file mode 100644
index 0000000000..4bbd45ccbb
--- /dev/null
+++ b/toolkit/components/places/tests/cpp/places_test_harness_tail.h
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsWidgetsCID.h"
+#include "nsIComponentRegistrar.h"
+#ifdef MOZ_CRASHREPORTER
+#include "nsICrashReporter.h"
+#endif
+
+#ifndef TEST_NAME
+#error "Must #define TEST_NAME before including places_test_harness_tail.h"
+#endif
+
+#ifndef TEST_FILE
+#error "Must #define TEST_FILE before include places_test_harness_tail.h"
+#endif
+
+int gTestsIndex = 0;
+
+#define TEST_INFO_STR "TEST-INFO | (%s) | "
+
+class RunNextTest : public mozilla::Runnable
+{
+public:
+ NS_IMETHOD Run() override
+ {
+ NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?");
+ if (gTestsIndex < int(mozilla::ArrayLength(gTests))) {
+ do_test_pending();
+ Test &test = gTests[gTestsIndex++];
+ (void)fprintf(stderr, TEST_INFO_STR "Running %s.\n", TEST_FILE,
+ test.name);
+ test.func();
+ }
+
+ do_test_finished();
+ return NS_OK;
+ }
+};
+
+void
+run_next_test()
+{
+ nsCOMPtr<nsIRunnable> event = new RunNextTest();
+ do_check_success(NS_DispatchToCurrentThread(event));
+}
+
+int gPendingTests = 0;
+
+void
+do_test_pending()
+{
+ NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?");
+ gPendingTests++;
+}
+
+void
+do_test_finished()
+{
+ NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?");
+ NS_ASSERTION(gPendingTests > 0, "Invalid pending test count!");
+ gPendingTests--;
+}
+
+void
+disable_idle_service()
+{
+ (void)fprintf(stderr, TEST_INFO_STR "Disabling Idle Service.\n", TEST_FILE);
+ static NS_DEFINE_IID(kIdleCID, NS_IDLE_SERVICE_CID);
+ nsresult rv;
+ nsCOMPtr<nsIFactory> idleFactory = do_GetClassObject(kIdleCID, &rv);
+ do_check_success(rv);
+ nsCOMPtr<nsIComponentRegistrar> registrar;
+ rv = NS_GetComponentRegistrar(getter_AddRefs(registrar));
+ do_check_success(rv);
+ rv = registrar->UnregisterFactory(kIdleCID, idleFactory);
+ do_check_success(rv);
+}
+
+int
+main(int aArgc,
+ char** aArgv)
+{
+ ScopedXPCOM xpcom(TEST_NAME);
+ if (xpcom.failed())
+ return -1;
+ // Initialize a profile folder to ensure a clean shutdown.
+ nsCOMPtr<nsIFile> profile = xpcom.GetProfileDirectory();
+ if (!profile) {
+ fail("Couldn't get the profile directory.");
+ return -1;
+ }
+
+#ifdef MOZ_CRASHREPORTER
+ char* enabled = PR_GetEnv("MOZ_CRASHREPORTER");
+ if (enabled && !strcmp(enabled, "1")) {
+ // bug 787458: move this to an even-more-common location to use in all
+ // C++ unittests
+ nsCOMPtr<nsICrashReporter> crashreporter =
+ do_GetService("@mozilla.org/toolkit/crash-reporter;1");
+ if (crashreporter) {
+ fprintf(stderr, "Setting up crash reporting\n");
+
+ nsCOMPtr<nsIProperties> dirsvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ if (!dirsvc)
+ NS_RUNTIMEABORT("Couldn't get directory service");
+ nsCOMPtr<nsIFile> cwd;
+ nsresult rv = dirsvc->Get(NS_OS_CURRENT_WORKING_DIR,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(cwd));
+ if (NS_FAILED(rv))
+ NS_RUNTIMEABORT("Couldn't get CWD");
+ crashreporter->SetEnabled(true);
+ crashreporter->SetMinidumpPath(cwd);
+ }
+ }
+#endif
+
+ RefPtr<WaitForConnectionClosed> spinClose = new WaitForConnectionClosed();
+
+ // Tinderboxes are constantly on idle. Since idle tasks can interact with
+ // tests, causing random failures, disable the idle service.
+ disable_idle_service();
+
+ do_test_pending();
+ run_next_test();
+
+ // Spin the event loop until we've run out of tests to run.
+ while (gPendingTests) {
+ (void)NS_ProcessNextEvent();
+ }
+
+ // And let any other events finish before we quit.
+ (void)NS_ProcessPendingEvents(nullptr);
+
+ // Check that we have passed all of our tests, and output accordingly.
+ if (gPassedTests == gTotalTests) {
+ passed(TEST_FILE);
+ }
+
+ (void)fprintf(stderr, TEST_INFO_STR "%u of %u tests passed\n",
+ TEST_FILE, unsigned(gPassedTests), unsigned(gTotalTests));
+
+ return gPassedTests == gTotalTests ? 0 : -1;
+}
diff --git a/toolkit/components/places/tests/cpp/test_IHistory.cpp b/toolkit/components/places/tests/cpp/test_IHistory.cpp
new file mode 100644
index 0000000000..90998ce8c1
--- /dev/null
+++ b/toolkit/components/places/tests/cpp/test_IHistory.cpp
@@ -0,0 +1,639 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "places_test_harness.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "mozilla/Attributes.h"
+#include "nsNetUtil.h"
+
+#include "mock_Link.h"
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/**
+ * This file tests the IHistory interface.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helper Methods
+
+void
+expect_visit(nsLinkState aState)
+{
+ do_check_true(aState == eLinkState_Visited);
+}
+
+void
+expect_no_visit(nsLinkState aState)
+{
+ do_check_true(aState == eLinkState_Unvisited);
+}
+
+already_AddRefed<nsIURI>
+new_test_uri()
+{
+ // Create a unique spec.
+ static int32_t specNumber = 0;
+ nsAutoCString spec = NS_LITERAL_CSTRING("http://mozilla.org/");
+ spec.AppendInt(specNumber++);
+
+ // Create the URI for the spec.
+ nsCOMPtr<nsIURI> testURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(testURI), spec);
+ do_check_success(rv);
+ return testURI.forget();
+}
+
+class VisitURIObserver final : public nsIObserver
+{
+ ~VisitURIObserver() {}
+
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit VisitURIObserver(int aExpectedVisits = 1) :
+ mVisits(0),
+ mExpectedVisits(aExpectedVisits)
+ {
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ do_check_true(observerService);
+ (void)observerService->AddObserver(this,
+ "uri-visit-saved",
+ false);
+ }
+
+ void WaitForNotification()
+ {
+ while (mVisits < mExpectedVisits) {
+ (void)NS_ProcessNextEvent();
+ }
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) override
+ {
+ mVisits++;
+
+ if (mVisits == mExpectedVisits) {
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ (void)observerService->RemoveObserver(this, "uri-visit-saved");
+ }
+
+ return NS_OK;
+ }
+private:
+ int mVisits;
+ int mExpectedVisits;
+};
+NS_IMPL_ISUPPORTS(
+ VisitURIObserver,
+ nsIObserver
+)
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Functions
+
+void
+test_set_places_enabled()
+{
+ // Ensure places is enabled for everyone.
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ do_check_success(rv);
+
+ rv = prefBranch->SetBoolPref("places.history.enabled", true);
+ do_check_success(rv);
+
+ // Run the next test.
+ run_next_test();
+}
+
+
+void
+test_wait_checkpoint()
+{
+ // This "fake" test is here to wait for the initial WAL checkpoint we force
+ // after creating the database schema, since that may happen at any time,
+ // and cause concurrent readers to access an older checkpoint.
+ nsCOMPtr<mozIStorageConnection> db = do_get_db();
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING("SELECT 1"),
+ getter_AddRefs(stmt));
+ RefPtr<AsyncStatementSpinner> spinner = new AsyncStatementSpinner();
+ nsCOMPtr<mozIStoragePendingStatement> pending;
+ (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pending));
+ spinner->SpinUntilCompleted();
+
+ // Run the next test.
+ run_next_test();
+}
+
+// These variables are shared between part 1 and part 2 of the test. Part 2
+// sets the nsCOMPtr's to nullptr, freeing the reference.
+namespace test_unvisited_does_not_notify {
+ nsCOMPtr<nsIURI> testURI;
+ RefPtr<Link> testLink;
+} // namespace test_unvisited_does_not_notify
+void
+test_unvisited_does_not_notify_part1()
+{
+ using namespace test_unvisited_does_not_notify;
+
+ // This test is done in two parts. The first part registers for a URI that
+ // should not be visited. We then run another test that will also do a
+ // lookup and will be notified. Since requests are answered in the order they
+ // are requested (at least as long as the same URI isn't asked for later), we
+ // will know that the Link was not notified.
+
+ // First, we need a test URI.
+ testURI = new_test_uri();
+
+ // Create our test Link.
+ testLink = new mock_Link(expect_no_visit);
+
+ // Now, register our Link to be notified.
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->RegisterVisitedCallback(testURI, testLink);
+ do_check_success(rv);
+
+ // Run the next test.
+ run_next_test();
+}
+
+void
+test_visited_notifies()
+{
+ // First, we add our test URI to history.
+ nsCOMPtr<nsIURI> testURI = new_test_uri();
+ addURI(testURI);
+
+ // Create our test Link. The callback function will release the reference we
+ // have on the Link.
+ RefPtr<Link> link = new mock_Link(expect_visit);
+
+ // Now, register our Link to be notified.
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->RegisterVisitedCallback(testURI, link);
+ do_check_success(rv);
+
+ // Note: test will continue upon notification.
+}
+
+void
+test_unvisited_does_not_notify_part2()
+{
+ using namespace test_unvisited_does_not_notify;
+
+ // We would have had a failure at this point had the content node been told it
+ // was visited. Therefore, it is safe to unregister our content node.
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->UnregisterVisitedCallback(testURI, testLink);
+ do_check_success(rv);
+
+ // Clear the stored variables now.
+ testURI = nullptr;
+ testLink = nullptr;
+
+ // Run the next test.
+ run_next_test();
+}
+
+void
+test_same_uri_notifies_both()
+{
+ // First, we add our test URI to history.
+ nsCOMPtr<nsIURI> testURI = new_test_uri();
+ addURI(testURI);
+
+ // Create our two test Links. The callback function will release the
+ // reference we have on the Links. Only the second Link should run the next
+ // test!
+ RefPtr<Link> link1 = new mock_Link(expect_visit, false);
+ RefPtr<Link> link2 = new mock_Link(expect_visit);
+
+ // Now, register our Link to be notified.
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->RegisterVisitedCallback(testURI, link1);
+ do_check_success(rv);
+ rv = history->RegisterVisitedCallback(testURI, link2);
+ do_check_success(rv);
+
+ // Note: test will continue upon notification.
+}
+
+void
+test_unregistered_visited_does_not_notify()
+{
+ // This test must have a test that has a successful notification after it.
+ // The Link would have been notified by now if we were buggy and notified
+ // unregistered Links (due to request serialization).
+
+ nsCOMPtr<nsIURI> testURI = new_test_uri();
+ RefPtr<Link> link = new mock_Link(expect_no_visit);
+
+ // Now, register our Link to be notified.
+ nsCOMPtr<IHistory> history(do_get_IHistory());
+ nsresult rv = history->RegisterVisitedCallback(testURI, link);
+ do_check_success(rv);
+
+ // Unregister the Link.
+ rv = history->UnregisterVisitedCallback(testURI, link);
+ do_check_success(rv);
+
+ // And finally add a visit for the URI.
+ addURI(testURI);
+
+ // If history tries to notify us, we'll either crash because the Link will
+ // have been deleted (we are the only thing holding a reference to it), or our
+ // expect_no_visit call back will produce a failure. Either way, the test
+ // will be reported as a failure.
+
+ // Run the next test.
+ run_next_test();
+}
+
+void
+test_new_visit_notifies_waiting_Link()
+{
+ // Create our test Link. The callback function will release the reference we
+ // have on the link.
+ RefPtr<Link> link = new mock_Link(expect_visit);
+
+ // Now, register our content node to be notified.
+ nsCOMPtr<nsIURI> testURI = new_test_uri();
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->RegisterVisitedCallback(testURI, link);
+ do_check_success(rv);
+
+ // Add ourselves to history.
+ addURI(testURI);
+
+ // Note: test will continue upon notification.
+}
+
+void
+test_RegisterVisitedCallback_returns_before_notifying()
+{
+ // Add a URI so that it's already in history.
+ nsCOMPtr<nsIURI> testURI = new_test_uri();
+ addURI(testURI);
+
+ // Create our test Link.
+ RefPtr<Link> link = new mock_Link(expect_no_visit);
+
+ // Now, register our content node to be notified. It should not be notified.
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->RegisterVisitedCallback(testURI, link);
+ do_check_success(rv);
+
+ // Remove ourselves as an observer. We would have failed if we had been
+ // notified.
+ rv = history->UnregisterVisitedCallback(testURI, link);
+ do_check_success(rv);
+
+ run_next_test();
+}
+
+namespace test_observer_topic_dispatched_helpers {
+ #define URI_VISITED "visited"
+ #define URI_NOT_VISITED "not visited"
+ #define URI_VISITED_RESOLUTION_TOPIC "visited-status-resolution"
+ class statusObserver final : public nsIObserver
+ {
+ ~statusObserver() {}
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ statusObserver(nsIURI* aURI,
+ const bool aExpectVisit,
+ bool& _notified)
+ : mURI(aURI)
+ , mExpectVisit(aExpectVisit)
+ , mNotified(_notified)
+ {
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ do_check_true(observerService);
+ (void)observerService->AddObserver(this,
+ URI_VISITED_RESOLUTION_TOPIC,
+ false);
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) override
+ {
+ // Make sure we got notified of the right topic.
+ do_check_false(strcmp(aTopic, URI_VISITED_RESOLUTION_TOPIC));
+
+ // If this isn't for our URI, do not do anything.
+ nsCOMPtr<nsIURI> notifiedURI = do_QueryInterface(aSubject);
+ do_check_true(notifiedURI);
+
+ bool isOurURI;
+ nsresult rv = notifiedURI->Equals(mURI, &isOurURI);
+ do_check_success(rv);
+ if (!isOurURI) {
+ return NS_OK;
+ }
+
+ // Check that we have either the visited or not visited string.
+ bool visited = !!NS_LITERAL_STRING(URI_VISITED).Equals(aData);
+ bool notVisited = !!NS_LITERAL_STRING(URI_NOT_VISITED).Equals(aData);
+ do_check_true(visited || notVisited);
+
+ // Check to make sure we got the state we expected.
+ do_check_eq(visited, mExpectVisit);
+
+ // Indicate that we've been notified.
+ mNotified = true;
+
+ // Remove ourselves as an observer.
+ nsCOMPtr<nsIObserverService> observerService =
+ do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ (void)observerService->RemoveObserver(this,
+ URI_VISITED_RESOLUTION_TOPIC);
+ return NS_OK;
+ }
+ private:
+ nsCOMPtr<nsIURI> mURI;
+ const bool mExpectVisit;
+ bool& mNotified;
+ };
+ NS_IMPL_ISUPPORTS(
+ statusObserver,
+ nsIObserver
+ )
+} // namespace test_observer_topic_dispatched_helpers
+void
+test_observer_topic_dispatched()
+{
+ using namespace test_observer_topic_dispatched_helpers;
+
+ // Create two URIs, making sure only one is in history.
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+ nsCOMPtr<nsIURI> notVisitedURI = new_test_uri();
+ bool urisEqual;
+ nsresult rv = visitedURI->Equals(notVisitedURI, &urisEqual);
+ do_check_success(rv);
+ do_check_false(urisEqual);
+ addURI(visitedURI);
+
+ // Need two Link objects as well - one for each URI.
+ RefPtr<Link> visitedLink = new mock_Link(expect_visit, false);
+ RefPtr<Link> visitedLinkCopy = visitedLink;
+ RefPtr<Link> notVisitedLink = new mock_Link(expect_no_visit);
+
+ // Add the right observers for the URIs to check results.
+ bool visitedNotified = false;
+ nsCOMPtr<nsIObserver> visitedObs =
+ new statusObserver(visitedURI, true, visitedNotified);
+ bool notVisitedNotified = false;
+ nsCOMPtr<nsIObserver> unvisitedObs =
+ new statusObserver(notVisitedURI, false, notVisitedNotified);
+
+ // Register our Links to be notified.
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ rv = history->RegisterVisitedCallback(visitedURI, visitedLink);
+ do_check_success(rv);
+ rv = history->RegisterVisitedCallback(notVisitedURI, notVisitedLink);
+ do_check_success(rv);
+
+ // Spin the event loop as long as we have not been properly notified.
+ while (!visitedNotified || !notVisitedNotified) {
+ (void)NS_ProcessNextEvent();
+ }
+
+ // Unregister our observer that would not have been released.
+ rv = history->UnregisterVisitedCallback(notVisitedURI, notVisitedLink);
+ do_check_success(rv);
+
+ run_next_test();
+}
+
+void
+test_visituri_inserts()
+{
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsCOMPtr<nsIURI> lastURI = new_test_uri();
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+
+ history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL);
+
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ PlaceRecord place;
+ do_get_place(visitedURI, place);
+
+ do_check_true(place.id > 0);
+ do_check_false(place.hidden);
+ do_check_false(place.typed);
+ do_check_eq(place.visitCount, 1);
+
+ run_next_test();
+}
+
+void
+test_visituri_updates()
+{
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsCOMPtr<nsIURI> lastURI = new_test_uri();
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+ RefPtr<VisitURIObserver> finisher;
+
+ history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL);
+ finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL);
+ finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ PlaceRecord place;
+ do_get_place(visitedURI, place);
+
+ do_check_eq(place.visitCount, 2);
+
+ run_next_test();
+}
+
+void
+test_visituri_preserves_shown_and_typed()
+{
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsCOMPtr<nsIURI> lastURI = new_test_uri();
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+
+ history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL);
+ // this simulates the uri visit happening in a frame. Normally frame
+ // transitions would be hidden unless it was previously loaded top-level
+ history->VisitURI(visitedURI, lastURI, 0);
+
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver(2);
+ finisher->WaitForNotification();
+
+ PlaceRecord place;
+ do_get_place(visitedURI, place);
+ do_check_false(place.hidden);
+
+ run_next_test();
+}
+
+void
+test_visituri_creates_visit()
+{
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsCOMPtr<nsIURI> lastURI = new_test_uri();
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+
+ history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL);
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ PlaceRecord place;
+ VisitRecord visit;
+ do_get_place(visitedURI, place);
+ do_get_lastVisit(place.id, visit);
+
+ do_check_true(visit.id > 0);
+ do_check_eq(visit.lastVisitId, 0);
+ do_check_eq(visit.transitionType, nsINavHistoryService::TRANSITION_LINK);
+
+ run_next_test();
+}
+
+void
+test_visituri_transition_typed()
+{
+ nsCOMPtr<nsINavHistoryService> navHistory = do_get_NavHistory();
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsCOMPtr<nsIURI> lastURI = new_test_uri();
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+
+ navHistory->MarkPageAsTyped(visitedURI);
+ history->VisitURI(visitedURI, lastURI, mozilla::IHistory::TOP_LEVEL);
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ PlaceRecord place;
+ VisitRecord visit;
+ do_get_place(visitedURI, place);
+ do_get_lastVisit(place.id, visit);
+
+ do_check_true(visit.transitionType == nsINavHistoryService::TRANSITION_TYPED);
+
+ run_next_test();
+}
+
+void
+test_visituri_transition_embed()
+{
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsCOMPtr<nsIURI> lastURI = new_test_uri();
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+
+ history->VisitURI(visitedURI, lastURI, 0);
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ PlaceRecord place;
+ VisitRecord visit;
+ do_get_place(visitedURI, place);
+ do_get_lastVisit(place.id, visit);
+
+ do_check_eq(place.id, 0);
+ do_check_eq(visit.id, 0);
+
+ run_next_test();
+}
+
+void
+test_new_visit_adds_place_guid()
+{
+ // First, add a visit and wait. This will also add a place.
+ nsCOMPtr<nsIURI> visitedURI = new_test_uri();
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->VisitURI(visitedURI, nullptr,
+ mozilla::IHistory::TOP_LEVEL);
+ do_check_success(rv);
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ // Check that we have a guid for our visit.
+ PlaceRecord place;
+ do_get_place(visitedURI, place);
+ do_check_eq(place.visitCount, 1);
+ do_check_eq(place.guid.Length(), 12);
+
+ run_next_test();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// IPC-only Tests
+
+void
+test_two_null_links_same_uri()
+{
+ // Tests that we do not crash when we have had two nullptr Links passed to
+ // RegisterVisitedCallback and then the visit occurs (bug 607469). This only
+ // happens in IPC builds.
+ nsCOMPtr<nsIURI> testURI = new_test_uri();
+
+ nsCOMPtr<IHistory> history = do_get_IHistory();
+ nsresult rv = history->RegisterVisitedCallback(testURI, nullptr);
+ do_check_success(rv);
+ rv = history->RegisterVisitedCallback(testURI, nullptr);
+ do_check_success(rv);
+
+ rv = history->VisitURI(testURI, nullptr, mozilla::IHistory::TOP_LEVEL);
+ do_check_success(rv);
+
+ RefPtr<VisitURIObserver> finisher = new VisitURIObserver();
+ finisher->WaitForNotification();
+
+ run_next_test();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Harness
+
+/**
+ * Note: for tests marked "Order Important!", please see the test for details.
+ */
+Test gTests[] = {
+ TEST(test_set_places_enabled), // Must come first!
+ TEST(test_wait_checkpoint), // Must come second!
+ TEST(test_unvisited_does_not_notify_part1), // Order Important!
+ TEST(test_visited_notifies),
+ TEST(test_unvisited_does_not_notify_part2), // Order Important!
+ TEST(test_same_uri_notifies_both),
+ TEST(test_unregistered_visited_does_not_notify), // Order Important!
+ TEST(test_new_visit_notifies_waiting_Link),
+ TEST(test_RegisterVisitedCallback_returns_before_notifying),
+ TEST(test_observer_topic_dispatched),
+ TEST(test_visituri_inserts),
+ TEST(test_visituri_updates),
+ TEST(test_visituri_preserves_shown_and_typed),
+ TEST(test_visituri_creates_visit),
+ TEST(test_visituri_transition_typed),
+ TEST(test_visituri_transition_embed),
+ TEST(test_new_visit_adds_place_guid),
+
+ // The rest of these tests are tests that are only run in IPC builds.
+ TEST(test_two_null_links_same_uri),
+};
+
+const char* file = __FILE__;
+#define TEST_NAME "IHistory"
+#define TEST_FILE file
+#include "places_test_harness_tail.h"
diff --git a/toolkit/components/places/tests/expiration/.eslintrc.js b/toolkit/components/places/tests/expiration/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/expiration/head_expiration.js b/toolkit/components/places/tests/expiration/head_expiration.js
new file mode 100644
index 0000000000..2be4af307c
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/head_expiration.js
@@ -0,0 +1,124 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.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.
+
+
+// Simulates an expiration at shutdown.
+function shutdownExpiration()
+{
+ let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver);
+ expire.observe(null, "places-will-close-connection", null);
+}
+
+
+/**
+ * Causes expiration component to start, otherwise it would wait for the first
+ * history notification.
+ */
+function force_expiration_start() {
+ Cc["@mozilla.org/places/expiration;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "testing-mode", null);
+}
+
+
+/**
+ * Forces an expiration run.
+ *
+ * @param [optional] aLimit
+ * Limit for the expiration. Pass -1 for unlimited.
+ * Any other non-positive value will just expire orphans.
+ *
+ * @return {Promise}
+ * @resolves When expiration finishes.
+ * @rejects Never.
+ */
+function promiseForceExpirationStep(aLimit) {
+ let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver);
+ expire.observe(null, "places-debug-start-expiration", aLimit);
+ return promise;
+}
+
+
+/**
+ * Expiration preferences helpers.
+ */
+
+function setInterval(aNewInterval) {
+ Services.prefs.setIntPref("places.history.expiration.interval_seconds", aNewInterval);
+}
+function getInterval() {
+ return Services.prefs.getIntPref("places.history.expiration.interval_seconds");
+}
+function clearInterval() {
+ try {
+ Services.prefs.clearUserPref("places.history.expiration.interval_seconds");
+ }
+ catch (ex) {}
+}
+
+
+function setMaxPages(aNewMaxPages) {
+ Services.prefs.setIntPref("places.history.expiration.max_pages", aNewMaxPages);
+}
+function getMaxPages() {
+ return Services.prefs.getIntPref("places.history.expiration.max_pages");
+}
+function clearMaxPages() {
+ try {
+ Services.prefs.clearUserPref("places.history.expiration.max_pages");
+ }
+ catch (ex) {}
+}
+
+
+function setHistoryEnabled(aHistoryEnabled) {
+ Services.prefs.setBoolPref("places.history.enabled", aHistoryEnabled);
+}
+function getHistoryEnabled() {
+ return Services.prefs.getBoolPref("places.history.enabled");
+}
+function clearHistoryEnabled() {
+ try {
+ Services.prefs.clearUserPref("places.history.enabled");
+ }
+ catch (ex) {}
+}
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * param [optional] daysAgo
+ * Expiration ignores any visit added in the last 7 days, so by default
+ * this will be set to 7.
+ * @note to be safe against DST issues we go back one day more.
+ */
+function getExpirablePRTime(daysAgo = 7) {
+ let dateObj = new Date();
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000);
+ return dateObj.getTime() * 1000;
+}
diff --git a/toolkit/components/places/tests/expiration/test_analyze_runs.js b/toolkit/components/places/tests/expiration/test_analyze_runs.js
new file mode 100644
index 0000000000..1a84e1b38c
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_analyze_runs.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Constants
+
+const TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING = "autocomplete-will-enter-text";
+
+// Helpers
+
+/**
+ * Ensures that we have no data in the tables created by ANALYZE.
+ */
+function clearAnalyzeData() {
+ let db = DBConn();
+ if (!db.tableExists("sqlite_stat1")) {
+ return;
+ }
+ db.executeSimpleSQL("DELETE FROM sqlite_stat1");
+}
+
+/**
+ * Checks that we ran ANALYZE on the specified table.
+ *
+ * @param aTableName
+ * The table to check if ANALYZE was ran.
+ * @param aRan
+ * True if it was expected to run, false otherwise
+ */
+function do_check_analyze_ran(aTableName, aRan) {
+ let db = DBConn();
+ do_check_true(db.tableExists("sqlite_stat1"));
+ let stmt = db.createStatement("SELECT idx FROM sqlite_stat1 WHERE tbl = :table");
+ stmt.params.table = aTableName;
+ try {
+ if (aRan) {
+ do_check_true(stmt.executeStep());
+ do_check_neq(stmt.row.idx, null);
+ }
+ else {
+ do_check_false(stmt.executeStep());
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+// Tests
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* init_tests() {
+ const TEST_URI = NetUtil.newURI("http://mozilla.org/");
+ const TEST_TITLE = "This is a test";
+
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: TEST_TITLE,
+ url: TEST_URI
+ });
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ let thing = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ Ci.nsIAutoCompleteController]),
+ get popup() { return thing; },
+ get controller() { return thing; },
+ popupOpen: true,
+ selectedIndex: 0,
+ getValueAt: function() { return TEST_URI.spec; },
+ searchString: TEST_TITLE,
+ };
+ Services.obs.notifyObservers(thing, TOPIC_AUTOCOMPLETE_FEEDBACK_INCOMING,
+ null);
+});
+
+add_task(function* test_timed() {
+ clearAnalyzeData();
+
+ // Set a low interval and wait for the timed expiration to start.
+ let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ setInterval(3);
+ yield promise;
+ setInterval(3600);
+
+ do_check_analyze_ran("moz_places", false);
+ do_check_analyze_ran("moz_bookmarks", false);
+ do_check_analyze_ran("moz_historyvisits", false);
+ do_check_analyze_ran("moz_inputhistory", true);
+});
+
+add_task(function* test_debug() {
+ clearAnalyzeData();
+
+ yield promiseForceExpirationStep(1);
+
+ do_check_analyze_ran("moz_places", true);
+ do_check_analyze_ran("moz_bookmarks", true);
+ do_check_analyze_ran("moz_historyvisits", true);
+ do_check_analyze_ran("moz_inputhistory", true);
+});
+
+add_task(function* test_clear_history() {
+ clearAnalyzeData();
+
+ let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ let listener = Cc["@mozilla.org/places/expiration;1"]
+ .getService(Ci.nsINavHistoryObserver);
+ listener.onClearHistory();
+ yield promise;
+
+ do_check_analyze_ran("moz_places", true);
+ do_check_analyze_ran("moz_bookmarks", false);
+ do_check_analyze_ran("moz_historyvisits", true);
+ do_check_analyze_ran("moz_inputhistory", true);
+});
diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_history.js b/toolkit/components/places/tests/expiration/test_annos_expire_history.js
new file mode 100644
index 0000000000..f9568a769c
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_history.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * EXPIRE_WITH_HISTORY annotations should be expired when a page has no more
+ * visits, even if the page still exists in the database.
+ * This expiration policy is only valid for page annotations.
+ */
+
+var as = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_annos_expire_history() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Expire all expirable pages.
+ setMaxPages(0);
+
+ // Add some visited page and a couple expire with history annotations for each.
+ let now = getExpirablePRTime();
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ as.setPageAnnotation(pageURI, "page_expire1", "test", 0, as.EXPIRE_WITH_HISTORY);
+ as.setPageAnnotation(pageURI, "page_expire2", "test", 0, as.EXPIRE_WITH_HISTORY);
+ }
+
+ let pages = as.getPagesWithAnnotation("page_expire1");
+ do_check_eq(pages.length, 5);
+ pages = as.getPagesWithAnnotation("page_expire2");
+ do_check_eq(pages.length, 5);
+
+ // Add some bookmarked page and a couple session annotations for each.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
+ // We also add a visit before bookmarking.
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: pageURI,
+ title: null
+ });
+ // Notice we use page annotations here, items annotations can't use this
+ // kind of expiration policy.
+ as.setPageAnnotation(pageURI, "item_persist1", "test", 0, as.EXPIRE_WITH_HISTORY);
+ as.setPageAnnotation(pageURI, "item_persist2", "test", 0, as.EXPIRE_WITH_HISTORY);
+ }
+
+ let items = as.getPagesWithAnnotation("item_persist1");
+ do_check_eq(items.length, 5);
+ items = as.getPagesWithAnnotation("item_persist2");
+ do_check_eq(items.length, 5);
+
+ // Add other visited page and a couple expire with history annotations for each.
+ // We won't expire these visits, so the annotations should survive.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://persist_page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ as.setPageAnnotation(pageURI, "page_persist1", "test", 0, as.EXPIRE_WITH_HISTORY);
+ as.setPageAnnotation(pageURI, "page_persist2", "test", 0, as.EXPIRE_WITH_HISTORY);
+ }
+
+ pages = as.getPagesWithAnnotation("page_persist1");
+ do_check_eq(pages.length, 5);
+ pages = as.getPagesWithAnnotation("page_persist2");
+ do_check_eq(pages.length, 5);
+
+ // Expire all visits for the first 5 pages and the bookmarks.
+ yield promiseForceExpirationStep(10);
+
+ pages = as.getPagesWithAnnotation("page_expire1");
+ do_check_eq(pages.length, 0);
+ pages = as.getPagesWithAnnotation("page_expire2");
+ do_check_eq(pages.length, 0);
+ items = as.getItemsWithAnnotation("item_persist1");
+ do_check_eq(items.length, 0);
+ items = as.getItemsWithAnnotation("item_persist2");
+ do_check_eq(items.length, 0);
+ pages = as.getPagesWithAnnotation("page_persist1");
+ do_check_eq(pages.length, 5);
+ pages = as.getPagesWithAnnotation("page_persist2");
+ do_check_eq(pages.length, 5);
+});
diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_never.js b/toolkit/components/places/tests/expiration/test_annos_expire_never.js
new file mode 100644
index 0000000000..f146f25b5e
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_never.js
@@ -0,0 +1,95 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * EXPIRE_NEVER annotations should be expired when a page is removed from the
+ * database.
+ * If the annotation is a page annotation this will happen when the page is
+ * expired, namely when the page has no visits and is not bookmarked.
+ * Otherwise if it's an item annotation the annotation will be expired when
+ * the item is removed, thus expiration won't handle this case at all.
+ */
+
+var as = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_annos_expire_never() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Expire all expirable pages.
+ setMaxPages(0);
+
+ // Add some visited page and a couple expire never annotations for each.
+ let now = getExpirablePRTime();
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ as.setPageAnnotation(pageURI, "page_expire1", "test", 0, as.EXPIRE_NEVER);
+ as.setPageAnnotation(pageURI, "page_expire2", "test", 0, as.EXPIRE_NEVER);
+ }
+
+ let pages = as.getPagesWithAnnotation("page_expire1");
+ do_check_eq(pages.length, 5);
+ pages = as.getPagesWithAnnotation("page_expire2");
+ do_check_eq(pages.length, 5);
+
+ // Add some bookmarked page and a couple expire never annotations for each.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
+ // We also add a visit before bookmarking.
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: pageURI,
+ title: null
+ });
+ let id = yield PlacesUtils.promiseItemId(bm.guid);
+ as.setItemAnnotation(id, "item_persist1", "test", 0, as.EXPIRE_NEVER);
+ as.setItemAnnotation(id, "item_persist2", "test", 0, as.EXPIRE_NEVER);
+ }
+
+ let items = as.getItemsWithAnnotation("item_persist1");
+ do_check_eq(items.length, 5);
+ items = as.getItemsWithAnnotation("item_persist2");
+ do_check_eq(items.length, 5);
+
+ // Add other visited page and a couple expire never annotations for each.
+ // We won't expire these visits, so the annotations should survive.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://persist_page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ as.setPageAnnotation(pageURI, "page_persist1", "test", 0, as.EXPIRE_NEVER);
+ as.setPageAnnotation(pageURI, "page_persist2", "test", 0, as.EXPIRE_NEVER);
+ }
+
+ pages = as.getPagesWithAnnotation("page_persist1");
+ do_check_eq(pages.length, 5);
+ pages = as.getPagesWithAnnotation("page_persist2");
+ do_check_eq(pages.length, 5);
+
+ // Expire all visits for the first 5 pages and the bookmarks.
+ yield promiseForceExpirationStep(10);
+
+ pages = as.getPagesWithAnnotation("page_expire1");
+ do_check_eq(pages.length, 0);
+ pages = as.getPagesWithAnnotation("page_expire2");
+ do_check_eq(pages.length, 0);
+ items = as.getItemsWithAnnotation("item_persist1");
+ do_check_eq(items.length, 5);
+ items = as.getItemsWithAnnotation("item_persist2");
+ do_check_eq(items.length, 5);
+ pages = as.getPagesWithAnnotation("page_persist1");
+ do_check_eq(pages.length, 5);
+ pages = as.getPagesWithAnnotation("page_persist2");
+ do_check_eq(pages.length, 5);
+});
diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_policy.js b/toolkit/components/places/tests/expiration/test_annos_expire_policy.js
new file mode 100644
index 0000000000..2fe50e13e5
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_policy.js
@@ -0,0 +1,189 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * Annotations can be set with a timed expiration policy.
+ * Supported policies are:
+ * - EXPIRE_DAYS: annotation would be expired after 7 days
+ * - EXPIRE_WEEKS: annotation would be expired after 30 days
+ * - EXPIRE_MONTHS: annotation would be expired after 180 days
+ */
+
+var as = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+
+/**
+ * Creates an aged annotation.
+ *
+ * @param aIdentifier Either a page url or an item id.
+ * @param aIdentifier Name of the annotation.
+ * @param aValue Value for the annotation.
+ * @param aExpirePolicy Expiration policy of the annotation.
+ * @param aAgeInDays Age in days of the annotation.
+ * @param [optional] aLastModifiedAgeInDays Age in days of the annotation, for lastModified.
+ */
+var now = Date.now();
+function add_old_anno(aIdentifier, aName, aValue, aExpirePolicy,
+ aAgeInDays, aLastModifiedAgeInDays) {
+ let expireDate = (now - (aAgeInDays * 86400 * 1000)) * 1000;
+ let lastModifiedDate = 0;
+ if (aLastModifiedAgeInDays)
+ lastModifiedDate = (now - (aLastModifiedAgeInDays * 86400 * 1000)) * 1000;
+
+ let sql;
+ if (typeof(aIdentifier) == "number") {
+ // Item annotation.
+ as.setItemAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy);
+ // Update dateAdded for the last added annotation.
+ sql = "UPDATE moz_items_annos SET dateAdded = :expire_date, lastModified = :last_modified " +
+ "WHERE id = (SELECT id FROM moz_items_annos " +
+ "WHERE item_id = :id " +
+ "ORDER BY dateAdded DESC LIMIT 1)";
+ }
+ else if (aIdentifier instanceof Ci.nsIURI) {
+ // Page annotation.
+ as.setPageAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy);
+ // Update dateAdded for the last added annotation.
+ sql = "UPDATE moz_annos SET dateAdded = :expire_date, lastModified = :last_modified " +
+ "WHERE id = (SELECT a.id FROM moz_annos a " +
+ "LEFT JOIN moz_places h on h.id = a.place_id " +
+ "WHERE h.url_hash = hash(:id) AND h.url = :id " +
+ "ORDER BY a.dateAdded DESC LIMIT 1)";
+ }
+ else
+ do_throw("Wrong identifier type");
+
+ let stmt = DBConn().createStatement(sql);
+ stmt.params.id = (typeof(aIdentifier) == "number") ? aIdentifier
+ : aIdentifier.spec;
+ stmt.params.expire_date = expireDate;
+ stmt.params.last_modified = lastModifiedDate;
+ try {
+ stmt.executeStep();
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_annos_expire_policy() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Expire all expirable pages.
+ setMaxPages(0);
+
+ let now_specific_to_test = getExpirablePRTime();
+ // Add some bookmarked page and timed annotations for each.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now_specific_to_test++ });
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: pageURI,
+ title: null
+ });
+ let id = yield PlacesUtils.promiseItemId(bm.guid);
+ // Add a 6 days old anno.
+ add_old_anno(id, "persist_days", "test", as.EXPIRE_DAYS, 6);
+ // Add a 8 days old anno, modified 5 days ago.
+ add_old_anno(id, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6);
+ // Add a 8 days old anno.
+ add_old_anno(id, "expire_days", "test", as.EXPIRE_DAYS, 8);
+
+ // Add a 29 days old anno.
+ add_old_anno(id, "persist_weeks", "test", as.EXPIRE_WEEKS, 29);
+ // Add a 31 days old anno, modified 29 days ago.
+ add_old_anno(id, "persist_lm_weeks", "test", as.EXPIRE_WEEKS, 31, 29);
+ // Add a 31 days old anno.
+ add_old_anno(id, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
+
+ // Add a 179 days old anno.
+ add_old_anno(id, "persist_months", "test", as.EXPIRE_MONTHS, 179);
+ // Add a 181 days old anno, modified 179 days ago.
+ add_old_anno(id, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179);
+ // Add a 181 days old anno.
+ add_old_anno(id, "expire_months", "test", as.EXPIRE_MONTHS, 181);
+
+ // Add a 6 days old anno.
+ add_old_anno(pageURI, "persist_days", "test", as.EXPIRE_DAYS, 6);
+ // Add a 8 days old anno, modified 5 days ago.
+ add_old_anno(pageURI, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6);
+ // Add a 8 days old anno.
+ add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8);
+
+ // Add a 29 days old anno.
+ add_old_anno(pageURI, "persist_weeks", "test", as.EXPIRE_WEEKS, 29);
+ // Add a 31 days old anno, modified 29 days ago.
+ add_old_anno(pageURI, "persist_lm_weeks", "test", as.EXPIRE_WEEKS, 31, 29);
+ // Add a 31 days old anno.
+ add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
+
+ // Add a 179 days old anno.
+ add_old_anno(pageURI, "persist_months", "test", as.EXPIRE_MONTHS, 179);
+ // Add a 181 days old anno, modified 179 days ago.
+ add_old_anno(pageURI, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179);
+ // Add a 181 days old anno.
+ add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
+ }
+
+ // Add some visited page and timed annotations for each.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now_specific_to_test++ });
+ // Add a 6 days old anno.
+ add_old_anno(pageURI, "persist_days", "test", as.EXPIRE_DAYS, 6);
+ // Add a 8 days old anno, modified 5 days ago.
+ add_old_anno(pageURI, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6);
+ // Add a 8 days old anno.
+ add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8);
+
+ // Add a 29 days old anno.
+ add_old_anno(pageURI, "persist_weeks", "test", as.EXPIRE_WEEKS, 29);
+ // Add a 31 days old anno, modified 29 days ago.
+ add_old_anno(pageURI, "persist_lm_weeks", "test", as.EXPIRE_WEEKS, 31, 29);
+ // Add a 31 days old anno.
+ add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
+
+ // Add a 179 days old anno.
+ add_old_anno(pageURI, "persist_months", "test", as.EXPIRE_MONTHS, 179);
+ // Add a 181 days old anno, modified 179 days ago.
+ add_old_anno(pageURI, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179);
+ // Add a 181 days old anno.
+ add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
+ }
+
+ // Expire all visits for the bookmarks.
+ yield promiseForceExpirationStep(5);
+
+ ["expire_days", "expire_weeks", "expire_months"].forEach(function(aAnno) {
+ let pages = as.getPagesWithAnnotation(aAnno);
+ do_check_eq(pages.length, 0);
+ });
+
+ ["expire_days", "expire_weeks", "expire_months"].forEach(function(aAnno) {
+ let items = as.getItemsWithAnnotation(aAnno);
+ do_check_eq(items.length, 0);
+ });
+
+ ["persist_days", "persist_lm_days", "persist_weeks", "persist_lm_weeks",
+ "persist_months", "persist_lm_months"].forEach(function(aAnno) {
+ let pages = as.getPagesWithAnnotation(aAnno);
+ do_check_eq(pages.length, 10);
+ });
+
+ ["persist_days", "persist_lm_days", "persist_weeks", "persist_lm_weeks",
+ "persist_months", "persist_lm_months"].forEach(function(aAnno) {
+ let items = as.getItemsWithAnnotation(aAnno);
+ do_check_eq(items.length, 5);
+ });
+});
diff --git a/toolkit/components/places/tests/expiration/test_annos_expire_session.js b/toolkit/components/places/tests/expiration/test_annos_expire_session.js
new file mode 100644
index 0000000000..68c995f80e
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_session.js
@@ -0,0 +1,83 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * Session annotations should be expired when browsing session ends.
+ */
+
+var as = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_annos_expire_session() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Add some visited page and a couple session annotations for each.
+ let now = Date.now() * 1000;
+ for (let i = 0; i < 10; i++) {
+ let pageURI = uri("http://session_page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
+ as.setPageAnnotation(pageURI, "test1", "test", 0, as.EXPIRE_SESSION);
+ as.setPageAnnotation(pageURI, "test2", "test", 0, as.EXPIRE_SESSION);
+ }
+
+ // Add some bookmarked page and a couple session annotations for each.
+ for (let i = 0; i < 10; i++) {
+ let pageURI = uri("http://session_item_anno." + i + ".mozilla.org/");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: pageURI,
+ title: null
+ });
+ let id = yield PlacesUtils.promiseItemId(bm.guid);
+ as.setItemAnnotation(id, "test1", "test", 0, as.EXPIRE_SESSION);
+ as.setItemAnnotation(id, "test2", "test", 0, as.EXPIRE_SESSION);
+ }
+
+
+ let pages = as.getPagesWithAnnotation("test1");
+ do_check_eq(pages.length, 10);
+ pages = as.getPagesWithAnnotation("test2");
+ do_check_eq(pages.length, 10);
+ let items = as.getItemsWithAnnotation("test1");
+ do_check_eq(items.length, 10);
+ items = as.getItemsWithAnnotation("test2");
+ do_check_eq(items.length, 10);
+
+ let deferred = Promise.defer();
+ waitForConnectionClosed(function() {
+ let stmt = DBConn(true).createAsyncStatement(
+ `SELECT id FROM moz_annos
+ UNION ALL
+ SELECT id FROM moz_items_annos
+ WHERE expiration = :expiration`
+ );
+ stmt.params.expiration = as.EXPIRE_SESSION;
+ stmt.executeAsync({
+ handleResult: function(aResultSet) {
+ dump_table("moz_annos");
+ dump_table("moz_items_annos");
+ do_throw("Should not find any leftover session annotations");
+ },
+ handleError: function(aError) {
+ do_throw("Error code " + aError.result + " with message '" +
+ aError.message + "' returned.");
+ },
+ handleCompletion: function(aReason) {
+ do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
+ deferred.resolve();
+ }
+ });
+ stmt.finalize();
+ });
+ yield deferred.promise;
+});
diff --git a/toolkit/components/places/tests/expiration/test_clearHistory.js b/toolkit/components/places/tests/expiration/test_clearHistory.js
new file mode 100644
index 0000000000..d3879d7ad9
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_clearHistory.js
@@ -0,0 +1,157 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * History.clear() should expire everything but bookmarked pages and valid
+ * annos.
+ */
+
+var hs = PlacesUtils.history;
+var as = PlacesUtils.annotations;
+
+/**
+ * Creates an aged annotation.
+ *
+ * @param aIdentifier Either a page url or an item id.
+ * @param aIdentifier Name of the annotation.
+ * @param aValue Value for the annotation.
+ * @param aExpirePolicy Expiration policy of the annotation.
+ * @param aAgeInDays Age in days of the annotation.
+ * @param [optional] aLastModifiedAgeInDays Age in days of the annotation, for lastModified.
+ */
+var now = Date.now();
+function add_old_anno(aIdentifier, aName, aValue, aExpirePolicy,
+ aAgeInDays, aLastModifiedAgeInDays) {
+ let expireDate = (now - (aAgeInDays * 86400 * 1000)) * 1000;
+ let lastModifiedDate = 0;
+ if (aLastModifiedAgeInDays)
+ lastModifiedDate = (now - (aLastModifiedAgeInDays * 86400 * 1000)) * 1000;
+
+ let sql;
+ if (typeof(aIdentifier) == "number") {
+ // Item annotation.
+ as.setItemAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy);
+ // Update dateAdded for the last added annotation.
+ sql = "UPDATE moz_items_annos SET dateAdded = :expire_date, lastModified = :last_modified " +
+ "WHERE id = ( " +
+ "SELECT a.id FROM moz_items_annos a " +
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " +
+ "WHERE a.item_id = :id " +
+ "AND n.name = :anno_name " +
+ "ORDER BY a.dateAdded DESC LIMIT 1 " +
+ ")";
+ }
+ else if (aIdentifier instanceof Ci.nsIURI) {
+ // Page annotation.
+ as.setPageAnnotation(aIdentifier, aName, aValue, 0, aExpirePolicy);
+ // Update dateAdded for the last added annotation.
+ sql = "UPDATE moz_annos SET dateAdded = :expire_date, lastModified = :last_modified " +
+ "WHERE id = ( " +
+ "SELECT a.id FROM moz_annos a " +
+ "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " +
+ "JOIN moz_places h on h.id = a.place_id " +
+ "WHERE h.url_hash = hash(:id) AND h.url = :id " +
+ "AND n.name = :anno_name " +
+ "ORDER BY a.dateAdded DESC LIMIT 1 " +
+ ")";
+ }
+ else
+ do_throw("Wrong identifier type");
+
+ let stmt = DBConn().createStatement(sql);
+ stmt.params.id = (typeof(aIdentifier) == "number") ? aIdentifier
+ : aIdentifier.spec;
+ stmt.params.expire_date = expireDate;
+ stmt.params.last_modified = lastModifiedDate;
+ stmt.params.anno_name = aName;
+ try {
+ stmt.executeStep();
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_historyClear() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Expire all expirable pages.
+ setMaxPages(0);
+
+ // Add some bookmarked page with visit and annotations.
+ for (let i = 0; i < 5; i++) {
+ let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
+ // This visit will be expired.
+ yield PlacesTestUtils.addVisits({ uri: pageURI });
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: pageURI,
+ title: null
+ });
+ let id = yield PlacesUtils.promiseItemId(bm.guid);
+ // Will persist because it's an EXPIRE_NEVER item anno.
+ as.setItemAnnotation(id, "persist", "test", 0, as.EXPIRE_NEVER);
+ // Will persist because the page is bookmarked.
+ as.setPageAnnotation(pageURI, "persist", "test", 0, as.EXPIRE_NEVER);
+ // All EXPIRE_SESSION annotations are expected to expire on clear history.
+ as.setItemAnnotation(id, "expire_session", "test", 0, as.EXPIRE_SESSION);
+ as.setPageAnnotation(pageURI, "expire_session", "test", 0, as.EXPIRE_SESSION);
+ // Annotations with timed policy will expire regardless bookmarked status.
+ add_old_anno(id, "expire_days", "test", as.EXPIRE_DAYS, 8);
+ add_old_anno(id, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
+ add_old_anno(id, "expire_months", "test", as.EXPIRE_MONTHS, 181);
+ add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8);
+ add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
+ add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
+ }
+
+ // Add some visited page and annotations for each.
+ for (let i = 0; i < 5; i++) {
+ // All page annotations related to these expired pages are expected to
+ // expire as well.
+ let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
+ yield PlacesTestUtils.addVisits({ uri: pageURI });
+ as.setPageAnnotation(pageURI, "expire", "test", 0, as.EXPIRE_NEVER);
+ as.setPageAnnotation(pageURI, "expire_session", "test", 0, as.EXPIRE_SESSION);
+ add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8);
+ add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
+ add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
+ }
+
+ // Expire all visits for the bookmarks
+ yield PlacesUtils.history.clear();
+
+ ["expire_days", "expire_weeks", "expire_months", "expire_session",
+ "expire"].forEach(function(aAnno) {
+ let pages = as.getPagesWithAnnotation(aAnno);
+ do_check_eq(pages.length, 0);
+ });
+
+ ["expire_days", "expire_weeks", "expire_months", "expire_session",
+ "expire"].forEach(function(aAnno) {
+ let items = as.getItemsWithAnnotation(aAnno);
+ do_check_eq(items.length, 0);
+ });
+
+ let pages = as.getPagesWithAnnotation("persist");
+ do_check_eq(pages.length, 5);
+
+ let items = as.getItemsWithAnnotation("persist");
+ do_check_eq(items.length, 5);
+
+ for (let itemId of items) {
+ // Check item exists.
+ let guid = yield PlacesUtils.promiseItemGuid(itemId);
+ do_check_true((yield PlacesUtils.bookmarks.fetch({guid})), "item exists");
+ }
+});
diff --git a/toolkit/components/places/tests/expiration/test_debug_expiration.js b/toolkit/components/places/tests/expiration/test_debug_expiration.js
new file mode 100644
index 0000000000..456c03363e
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_debug_expiration.js
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * What this is aimed to test:
+ *
+ * Expiration can be manually triggered through a debug topic, but that should
+ * only expire orphan entries, unless -1 is passed as limit.
+ */
+
+var gNow = getExpirablePRTime(60);
+
+add_task(function* test_expire_orphans()
+{
+ // Add visits to 2 pages and force a orphan expiration. Visits should survive.
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://page1.mozilla.org/"),
+ visitDate: gNow++
+ });
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://page2.mozilla.org/"),
+ visitDate: gNow++
+ });
+ // Create a orphan place.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://page3.mozilla.org/",
+ title: ""
+ });
+ yield PlacesUtils.bookmarks.remove(bm);
+
+ // Expire now.
+ yield promiseForceExpirationStep(0);
+
+ // Check that visits survived.
+ do_check_eq(visits_in_database("http://page1.mozilla.org/"), 1);
+ do_check_eq(visits_in_database("http://page2.mozilla.org/"), 1);
+ do_check_false(page_in_database("http://page3.mozilla.org/"));
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_orphans_optionalarg()
+{
+ // Add visits to 2 pages and force a orphan expiration. Visits should survive.
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://page1.mozilla.org/"),
+ visitDate: gNow++
+ });
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://page2.mozilla.org/"),
+ visitDate: gNow++
+ });
+ // Create a orphan place.
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://page3.mozilla.org/",
+ title: ""
+ });
+ yield PlacesUtils.bookmarks.remove(bm);
+
+ // Expire now.
+ yield promiseForceExpirationStep();
+
+ // Check that visits survived.
+ do_check_eq(visits_in_database("http://page1.mozilla.org/"), 1);
+ do_check_eq(visits_in_database("http://page2.mozilla.org/"), 1);
+ do_check_false(page_in_database("http://page3.mozilla.org/"));
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_limited()
+{
+ yield PlacesTestUtils.addVisits([
+ { // Should be expired cause it's the oldest visit
+ uri: "http://old.mozilla.org/",
+ visitDate: gNow++
+ },
+ { // Should not be expired cause we limit 1
+ uri: "http://new.mozilla.org/",
+ visitDate: gNow++
+ },
+ ]);
+
+ // Expire now.
+ yield promiseForceExpirationStep(1);
+
+ // Check that newer visit survived.
+ do_check_eq(visits_in_database("http://new.mozilla.org/"), 1);
+ // Other visits should have been expired.
+ do_check_false(page_in_database("http://old.mozilla.org/"));
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_limited_longurl()
+{
+ let longurl = "http://long.mozilla.org/" + "a".repeat(232);
+ yield PlacesTestUtils.addVisits([
+ { // Should be expired cause it's the oldest visit
+ uri: "http://old.mozilla.org/",
+ visitDate: gNow++
+ },
+ { // Should be expired cause it's a long url older than 60 days.
+ uri: longurl,
+ visitDate: gNow++
+ },
+ { // Should not be expired cause younger than 60 days.
+ uri: longurl,
+ visitDate: getExpirablePRTime(58)
+ }
+ ]);
+
+ yield promiseForceExpirationStep(1);
+
+ // Check that some visits survived.
+ do_check_eq(visits_in_database(longurl), 1);
+ // Other visits should have been expired.
+ do_check_false(page_in_database("http://old.mozilla.org/"));
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_limited_exoticurl()
+{
+ yield PlacesTestUtils.addVisits([
+ { // Should be expired cause it's the oldest visit
+ uri: "http://old.mozilla.org/",
+ visitDate: gNow++
+ },
+ { // Should be expired cause it's a long url older than 60 days.
+ uri: "http://download.mozilla.org",
+ visitDate: gNow++,
+ transition: 7
+ },
+ { // Should not be expired cause younger than 60 days.
+ uri: "http://nonexpirable-download.mozilla.org",
+ visitDate: getExpirablePRTime(58),
+ transition: 7
+ }
+ ]);
+
+ yield promiseForceExpirationStep(1);
+
+ // Check that some visits survived.
+ do_check_eq(visits_in_database("http://nonexpirable-download.mozilla.org/"), 1);
+ // The visits are gone, the url is not yet, cause we limited the expiration
+ // to one entry, and we already removed http://old.mozilla.org/.
+ // The page normally would be expired by the next expiration run.
+ do_check_eq(visits_in_database("http://download.mozilla.org/"), 0);
+ // Other visits should have been expired.
+ do_check_false(page_in_database("http://old.mozilla.org/"));
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_expire_unlimited()
+{
+ let longurl = "http://long.mozilla.org/" + "a".repeat(232);
+ yield PlacesTestUtils.addVisits([
+ {
+ uri: "http://old.mozilla.org/",
+ visitDate: gNow++
+ },
+ {
+ uri: "http://new.mozilla.org/",
+ visitDate: gNow++
+ },
+ // Add expirable visits.
+ {
+ uri: "http://download.mozilla.org/",
+ visitDate: gNow++,
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD
+ },
+ {
+ uri: longurl,
+ visitDate: gNow++
+ },
+
+ // Add non-expirable visits
+ {
+ uri: "http://nonexpirable.mozilla.org/",
+ visitDate: getExpirablePRTime(5)
+ },
+ {
+ uri: "http://nonexpirable-download.mozilla.org/",
+ visitDate: getExpirablePRTime(5),
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD
+ },
+ {
+ uri: longurl,
+ visitDate: getExpirablePRTime(5)
+ }
+ ]);
+
+ yield promiseForceExpirationStep(-1);
+
+ // Check that some visits survived.
+ do_check_eq(visits_in_database("http://nonexpirable.mozilla.org/"), 1);
+ do_check_eq(visits_in_database("http://nonexpirable-download.mozilla.org/"), 1);
+ do_check_eq(visits_in_database(longurl), 1);
+ // Other visits should have been expired.
+ do_check_false(page_in_database("http://old.mozilla.org/"));
+ do_check_false(page_in_database("http://download.mozilla.org/"));
+ do_check_false(page_in_database("http://new.mozilla.org/"));
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+});
+
+function run_test()
+{
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+ // Set maxPages to a low value, so it's easy to go over it.
+ setMaxPages(1);
+
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/expiration/test_idle_daily.js b/toolkit/components/places/tests/expiration/test_idle_daily.js
new file mode 100644
index 0000000000..05e5a8125a
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_idle_daily.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that expiration runs on idle-daily.
+
+function run_test() {
+ do_test_pending();
+
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ Services.obs.addObserver(function observeExpiration(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observeExpiration,
+ PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ do_test_finished();
+ }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+
+ let expire = Cc["@mozilla.org/places/expiration;1"].
+ getService(Ci.nsIObserver);
+ expire.observe(null, "idle-daily", null);
+}
diff --git a/toolkit/components/places/tests/expiration/test_notifications.js b/toolkit/components/places/tests/expiration/test_notifications.js
new file mode 100644
index 0000000000..06e585c6cd
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_notifications.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * Ensure that History (through category cache) notifies us just once.
+ */
+
+var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+
+var gObserver = {
+ notifications: 0,
+ observe: function(aSubject, aTopic, aData) {
+ this.notifications++;
+ }
+};
+os.addObserver(gObserver, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+
+function run_test() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ PlacesTestUtils.clearHistory();
+
+ do_timeout(2000, check_result);
+ do_test_pending();
+}
+
+function check_result() {
+ os.removeObserver(gObserver, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ do_check_eq(gObserver.notifications, 1);
+ do_test_finished();
+}
diff --git a/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js b/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
new file mode 100644
index 0000000000..f70cd2b586
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * Expiring a full page should fire an onDeleteURI notification.
+ */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+var tests = [
+
+ { desc: "Add 1 bookmarked page.",
+ addPages: 1,
+ addBookmarks: 1,
+ expectedNotifications: 0, // No expirable pages.
+ },
+
+ { desc: "Add 2 pages, 1 bookmarked.",
+ addPages: 2,
+ addBookmarks: 1,
+ expectedNotifications: 1, // Only one expirable page.
+ },
+
+ { desc: "Add 10 pages, none bookmarked.",
+ addPages: 10,
+ addBookmarks: 0,
+ expectedNotifications: 10, // Will expire everything.
+ },
+
+ { desc: "Add 10 pages, all bookmarked.",
+ addPages: 10,
+ addBookmarks: 10,
+ expectedNotifications: 0, // No expirable pages.
+ },
+
+];
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_notifications_onDeleteURI() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Expire anything that is expirable.
+ setMaxPages(0);
+
+ for (let testIndex = 1; testIndex <= tests.length; testIndex++) {
+ let currentTest = tests[testIndex -1];
+ print("\nTEST " + testIndex + ": " + currentTest.desc);
+ currentTest.receivedNotifications = 0;
+
+ // Setup visits.
+ let now = getExpirablePRTime();
+ for (let i = 0; i < currentTest.addPages; i++) {
+ let page = "http://" + testIndex + "." + i + ".mozilla.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ });
+ }
+
+ // Setup bookmarks.
+ currentTest.bookmarks = [];
+ for (let i = 0; i < currentTest.addBookmarks; i++) {
+ let page = "http://" + testIndex + "." + i + ".mozilla.org/";
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: null,
+ url: page
+ });
+ currentTest.bookmarks.push(page);
+ }
+
+ // Observe history.
+ historyObserver = {
+ onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PEX_onEndUpdateBatch() {},
+ onClearHistory: function() {},
+ onVisit: function() {},
+ onTitleChanged: function() {},
+ onDeleteURI: function(aURI, aGUID, aReason) {
+ currentTest.receivedNotifications++;
+ // Check this uri was not bookmarked.
+ do_check_eq(currentTest.bookmarks.indexOf(aURI.spec), -1);
+ do_check_valid_places_guid(aGUID);
+ do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED);
+ },
+ onPageChanged: function() {},
+ onDeleteVisits: function(aURI, aTime) { },
+ };
+ hs.addObserver(historyObserver, false);
+
+ // Expire now.
+ yield promiseForceExpirationStep(-1);
+
+ hs.removeObserver(historyObserver, false);
+
+ do_check_eq(currentTest.receivedNotifications,
+ currentTest.expectedNotifications);
+
+ // Clean up.
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+ }
+
+ clearMaxPages();
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js b/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
new file mode 100644
index 0000000000..e6b99ff8ba
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
@@ -0,0 +1,142 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * Expiring only visits for a page, but not the full page, should fire an
+ * onDeleteVisits notification.
+ */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+var tests = [
+
+ { desc: "Add 1 bookmarked page.",
+ addPages: 1,
+ visitsPerPage: 1,
+ addBookmarks: 1,
+ limitExpiration: -1,
+ expectedNotifications: 1, // Will expire visits for 1 page.
+ },
+
+ { desc: "Add 2 pages, 1 bookmarked.",
+ addPages: 2,
+ visitsPerPage: 1,
+ addBookmarks: 1,
+ limitExpiration: -1,
+ expectedNotifications: 1, // Will expire visits for 1 page.
+ },
+
+ { desc: "Add 10 pages, none bookmarked.",
+ addPages: 10,
+ visitsPerPage: 1,
+ addBookmarks: 0,
+ limitExpiration: -1,
+ expectedNotifications: 0, // Will expire only full pages.
+ },
+
+ { desc: "Add 10 pages, all bookmarked.",
+ addPages: 10,
+ visitsPerPage: 1,
+ addBookmarks: 10,
+ limitExpiration: -1,
+ expectedNotifications: 10, // Will expire visist for all pages.
+ },
+
+ { desc: "Add 10 pages with lot of visits, none bookmarked.",
+ addPages: 10,
+ visitsPerPage: 10,
+ addBookmarks: 0,
+ limitExpiration: 10,
+ expectedNotifications: 10, // Will expire 1 visist for each page, but won't
+ }, // expire pages since they still have visits.
+
+];
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_notifications_onDeleteVisits() {
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ // Expire anything that is expirable.
+ setMaxPages(0);
+
+ for (let testIndex = 1; testIndex <= tests.length; testIndex++) {
+ let currentTest = tests[testIndex -1];
+ print("\nTEST " + testIndex + ": " + currentTest.desc);
+ currentTest.receivedNotifications = 0;
+
+ // Setup visits.
+ let timeInMicroseconds = getExpirablePRTime(8);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ for (let j = 0; j < currentTest.visitsPerPage; j++) {
+ for (let i = 0; i < currentTest.addPages; i++) {
+ let page = "http://" + testIndex + "." + i + ".mozilla.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: newTimeInMicroseconds() });
+ }
+ }
+
+ // Setup bookmarks.
+ currentTest.bookmarks = [];
+ for (let i = 0; i < currentTest.addBookmarks; i++) {
+ let page = "http://" + testIndex + "." + i + ".mozilla.org/";
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: null,
+ url: page
+ });
+ currentTest.bookmarks.push(page);
+ }
+
+ // Observe history.
+ historyObserver = {
+ onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PEX_onEndUpdateBatch() {},
+ onClearHistory: function() {},
+ onVisit: function() {},
+ onTitleChanged: function() {},
+ onDeleteURI: function(aURI, aGUID, aReason) {
+ // Check this uri was not bookmarked.
+ do_check_eq(currentTest.bookmarks.indexOf(aURI.spec), -1);
+ do_check_valid_places_guid(aGUID);
+ do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED);
+ },
+ onPageChanged: function() {},
+ onDeleteVisits: function(aURI, aTime, aGUID, aReason) {
+ currentTest.receivedNotifications++;
+ do_check_guid_for_uri(aURI, aGUID);
+ do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_EXPIRED);
+ },
+ };
+ hs.addObserver(historyObserver, false);
+
+ // Expire now.
+ yield promiseForceExpirationStep(currentTest.limitExpiration);
+
+ hs.removeObserver(historyObserver, false);
+
+ do_check_eq(currentTest.receivedNotifications,
+ currentTest.expectedNotifications);
+
+ // Clean up.
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+ }
+
+ clearMaxPages();
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/expiration/test_outdated_analyze.js b/toolkit/components/places/tests/expiration/test_outdated_analyze.js
new file mode 100644
index 0000000000..9cf61f06b2
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_outdated_analyze.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that expiration executes ANALYZE when statistics are outdated.
+
+const TEST_URL = "http://www.mozilla.org/";
+
+XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
+ "@mozilla.org/browser/history;1",
+ "mozIAsyncHistory");
+
+/**
+ * Object that represents a mozIVisitInfo object.
+ *
+ * @param [optional] aTransitionType
+ * The transition type of the visit. Defaults to TRANSITION_LINK if not
+ * provided.
+ * @param [optional] aVisitTime
+ * The time of the visit. Defaults to now if not provided.
+ */
+function VisitInfo(aTransitionType, aVisitTime) {
+ this.transitionType =
+ aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
+ this.visitDate = aVisitTime || Date.now() * 1000;
+}
+
+function run_test() {
+ do_test_pending();
+
+ // Init expiration before "importing".
+ force_expiration_start();
+
+ // Add a bunch of pages (at laast IMPORT_PAGES_THRESHOLD pages).
+ let places = [];
+ for (let i = 0; i < 100; i++) {
+ places.push({
+ uri: NetUtil.newURI(TEST_URL + i),
+ title: "Title" + i,
+ visits: [new VisitInfo]
+ });
+ }
+ gHistory.updatePlaces(places);
+
+ // Set interval to a small value to expire on it.
+ setInterval(1); // 1s
+
+ Services.obs.addObserver(function observeExpiration(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observeExpiration,
+ PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+
+ // Check that statistica are up-to-date.
+ let stmt = DBConn().createAsyncStatement(
+ "SELECT (SELECT COUNT(*) FROM moz_places) - "
+ + "(SELECT SUBSTR(stat,1,LENGTH(stat)-2) FROM sqlite_stat1 "
+ + "WHERE idx = 'moz_places_url_hashindex')"
+ );
+ stmt.executeAsync({
+ handleResult: function(aResultSet) {
+ let row = aResultSet.getNextRow();
+ this._difference = row.getResultByIndex(0);
+ },
+ handleError: function(aError) {
+ do_throw("Unexpected error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function(aReason) {
+ do_check_true(this._difference === 0);
+ do_test_finished();
+ }
+ });
+ stmt.finalize();
+ }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
+}
diff --git a/toolkit/components/places/tests/expiration/test_pref_interval.js b/toolkit/components/places/tests/expiration/test_pref_interval.js
new file mode 100644
index 0000000000..44c749d7a0
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_pref_interval.js
@@ -0,0 +1,61 @@
+/**
+ * What this is aimed to test:
+ *
+ * Expiration relies on an interval, that is user-preffable setting
+ * "places.history.expiration.interval_seconds".
+ * On pref change it will stop current interval timer and fire a new one,
+ * that will obey the new value.
+ * If the pref is set to a number <= 0 we will use the default value.
+ */
+
+// Default timer value for expiration in seconds. Must have same value as
+// PREF_INTERVAL_SECONDS_NOTSET in nsPlacesExpiration.
+const DEFAULT_TIMER_DELAY_SECONDS = 3 * 60;
+
+// Sync this with the const value in the component.
+const EXPIRE_AGGRESSIVITY_MULTIPLIER = 3;
+
+var tests = [
+
+ // This test should be the first, so the interval won't be influenced by
+ // status of history.
+ { desc: "Set interval to 1s.",
+ interval: 1,
+ expectedTimerDelay: 1
+ },
+
+ { desc: "Set interval to a negative value.",
+ interval: -1,
+ expectedTimerDelay: DEFAULT_TIMER_DELAY_SECONDS
+ },
+
+ { desc: "Set interval to 0.",
+ interval: 0,
+ expectedTimerDelay: DEFAULT_TIMER_DELAY_SECONDS
+ },
+
+ { desc: "Set interval to a large value.",
+ interval: 100,
+ expectedTimerDelay: 100
+ },
+
+];
+
+add_task(function* test() {
+ // The pref should not exist by default.
+ Assert.throws(() => getInterval());
+
+ // Force the component, so it will start observing preferences.
+ force_expiration_start();
+
+ for (let currentTest of tests) {
+ print(currentTest.desc);
+ let promise = promiseTopicObserved("test-interval-changed");
+ setInterval(currentTest.interval);
+ let [, data] = yield promise;
+ Assert.equal(data, currentTest.expectedTimerDelay * EXPIRE_AGGRESSIVITY_MULTIPLIER);
+ }
+
+ clearInterval();
+});
+
diff --git a/toolkit/components/places/tests/expiration/test_pref_maxpages.js b/toolkit/components/places/tests/expiration/test_pref_maxpages.js
new file mode 100644
index 0000000000..6a237afbb9
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/test_pref_maxpages.js
@@ -0,0 +1,124 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * What this is aimed to test:
+ *
+ * Expiration will obey to hardware spec, but user can set a custom maximum
+ * number of pages to retain, to restrict history, through
+ * "places.history.expiration.max_pages".
+ * This limit is used at next expiration run.
+ * If the pref is set to a number < 0 we will use the default value.
+ */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+var tests = [
+
+ { desc: "Set max_pages to a negative value, with 1 page.",
+ maxPages: -1,
+ addPages: 1,
+ expectedNotifications: 0, // Will ignore and won't expire anything.
+ },
+
+ { desc: "Set max_pages to 0.",
+ maxPages: 0,
+ addPages: 1,
+ expectedNotifications: 1,
+ },
+
+ { desc: "Set max_pages to 0, with 2 pages.",
+ maxPages: 0,
+ addPages: 2,
+ expectedNotifications: 2, // Will expire everything.
+ },
+
+ // Notice if we are over limit we do a full step of expiration. So we ensure
+ // that we will expire if we are over the limit, but we don't ensure that we
+ // will expire exactly up to the limit. Thus in this case we expire
+ // everything.
+ { desc: "Set max_pages to 1 with 2 pages.",
+ maxPages: 1,
+ addPages: 2,
+ expectedNotifications: 2, // Will expire everything (in this case).
+ },
+
+ { desc: "Set max_pages to 10, with 9 pages.",
+ maxPages: 10,
+ addPages: 9,
+ expectedNotifications: 0, // We are at the limit, won't expire anything.
+ },
+
+ { desc: "Set max_pages to 10 with 10 pages.",
+ maxPages: 10,
+ addPages: 10,
+ expectedNotifications: 0, // We are below the limit, won't expire anything.
+ },
+];
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_pref_maxpages() {
+ // The pref should not exist by default.
+ try {
+ getMaxPages();
+ do_throw("interval pref should not exist by default");
+ }
+ catch (ex) {}
+
+ // Set interval to a large value so we don't expire on it.
+ setInterval(3600); // 1h
+
+ for (let testIndex = 1; testIndex <= tests.length; testIndex++) {
+ let currentTest = tests[testIndex -1];
+ print("\nTEST " + testIndex + ": " + currentTest.desc);
+ currentTest.receivedNotifications = 0;
+
+ // Setup visits.
+ let now = getExpirablePRTime();
+ for (let i = 0; i < currentTest.addPages; i++) {
+ let page = "http://" + testIndex + "." + i + ".mozilla.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ });
+ }
+
+ // Observe history.
+ let historyObserver = {
+ onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {},
+ onEndUpdateBatch: function PEX_onEndUpdateBatch() {},
+ onClearHistory: function() {},
+ onVisit: function() {},
+ onTitleChanged: function() {},
+ onDeleteURI: function(aURI) {
+ print("onDeleteURI " + aURI.spec);
+ currentTest.receivedNotifications++;
+ },
+ onPageChanged: function() {},
+ onDeleteVisits: function(aURI, aTime) {
+ print("onDeleteVisits " + aURI.spec + " " + aTime);
+ },
+ };
+ hs.addObserver(historyObserver, false);
+
+ setMaxPages(currentTest.maxPages);
+
+ // Expire now.
+ yield promiseForceExpirationStep(-1);
+
+ hs.removeObserver(historyObserver, false);
+
+ do_check_eq(currentTest.receivedNotifications,
+ currentTest.expectedNotifications);
+
+ // Clean up.
+ yield PlacesTestUtils.clearHistory();
+ }
+
+ clearMaxPages();
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/expiration/xpcshell.ini b/toolkit/components/places/tests/expiration/xpcshell.ini
new file mode 100644
index 0000000000..cda7ac052c
--- /dev/null
+++ b/toolkit/components/places/tests/expiration/xpcshell.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+head = head_expiration.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_analyze_runs.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_annos_expire_history.js]
+[test_annos_expire_never.js]
+[test_annos_expire_policy.js]
+[test_annos_expire_session.js]
+[test_clearHistory.js]
+[test_debug_expiration.js]
+[test_idle_daily.js]
+[test_notifications.js]
+[test_notifications_onDeleteURI.js]
+[test_notifications_onDeleteVisits.js]
+[test_outdated_analyze.js]
+[test_pref_interval.js]
+[test_pref_maxpages.js]
+skip-if = os == "linux" # bug 1284083
diff --git a/toolkit/components/places/tests/favicons/.eslintrc.js b/toolkit/components/places/tests/favicons/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.png
new file mode 100644
index 0000000000..7230087710
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/expected-favicon-big32.jpg.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.png
new file mode 100644
index 0000000000..9932c18fb8
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/expected-favicon-big4.jpg.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big48.ico.png b/toolkit/components/places/tests/favicons/expected-favicon-big48.ico.png
new file mode 100644
index 0000000000..9f16bef433
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/expected-favicon-big48.ico.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/expected-favicon-big64.png.png b/toolkit/components/places/tests/favicons/expected-favicon-big64.png.png
new file mode 100644
index 0000000000..ed158d1611
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/expected-favicon-big64.png.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.png
new file mode 100644
index 0000000000..585c9e897e
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/expected-favicon-scale160x3.jpg.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.png b/toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.png
new file mode 100644
index 0000000000..e07dabc793
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/expected-favicon-scale3x160.jpg.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-big16.ico b/toolkit/components/places/tests/favicons/favicon-big16.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-big16.ico
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-big32.jpg b/toolkit/components/places/tests/favicons/favicon-big32.jpg
new file mode 100644
index 0000000000..b2131bf0c1
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-big32.jpg
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-big4.jpg b/toolkit/components/places/tests/favicons/favicon-big4.jpg
new file mode 100644
index 0000000000..b84fcd35a6
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-big4.jpg
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-big48.ico b/toolkit/components/places/tests/favicons/favicon-big48.ico
new file mode 100644
index 0000000000..f22522411d
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-big48.ico
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-big64.png b/toolkit/components/places/tests/favicons/favicon-big64.png
new file mode 100644
index 0000000000..2756cf0cb3
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-big64.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-normal16.png b/toolkit/components/places/tests/favicons/favicon-normal16.png
new file mode 100644
index 0000000000..62b69a3d03
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-normal16.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-normal32.png b/toolkit/components/places/tests/favicons/favicon-normal32.png
new file mode 100644
index 0000000000..5535363c94
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-normal32.png
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-scale160x3.jpg b/toolkit/components/places/tests/favicons/favicon-scale160x3.jpg
new file mode 100644
index 0000000000..422ee7ea0b
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-scale160x3.jpg
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/favicon-scale3x160.jpg b/toolkit/components/places/tests/favicons/favicon-scale3x160.jpg
new file mode 100644
index 0000000000..e8514966a0
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/favicon-scale3x160.jpg
Binary files differ
diff --git a/toolkit/components/places/tests/favicons/head_favicons.js b/toolkit/components/places/tests/favicons/head_favicons.js
new file mode 100644
index 0000000000..cc81791e87
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/head_favicons.js
@@ -0,0 +1,105 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.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.
+
+
+// This error icon must stay in sync with FAVICON_ERRORPAGE_URL in
+// nsIFaviconService.idl, aboutCertError.xhtml and netError.xhtml.
+const FAVICON_ERRORPAGE_URI =
+ NetUtil.newURI("chrome://global/skin/icons/warning-16.png");
+
+/**
+ * Waits for the first OnPageChanged notification for ATTRIBUTE_FAVICON, and
+ * verifies that it matches the expected page URI and associated favicon URI.
+ *
+ * This function also double-checks the GUID parameter of the notification.
+ *
+ * @param aExpectedPageURI
+ * nsIURI object of the page whose favicon should change.
+ * @param aExpectedFaviconURI
+ * nsIURI object of the newly associated favicon.
+ * @param aCallback
+ * This function is called after the check finished.
+ */
+function waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI,
+ aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onPageChanged: function WFFC_onPageChanged(aURI, aWhat, aValue, aGUID) {
+ if (aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) {
+ return;
+ }
+ PlacesUtils.history.removeObserver(this);
+
+ do_check_true(aURI.equals(aExpectedPageURI));
+ do_check_eq(aValue, aExpectedFaviconURI.spec);
+ do_check_guid_for_uri(aURI, aGUID);
+ aCallback();
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+/**
+ * Checks that the favicon for the given page matches the provided data.
+ *
+ * @param aPageURI
+ * nsIURI object for the page to check.
+ * @param aExpectedMimeType
+ * Expected MIME type of the icon, for example "image/png".
+ * @param aExpectedData
+ * Expected icon data, expressed as an array of byte values.
+ * @param aCallback
+ * This function is called after the check finished.
+ */
+function checkFaviconDataForPage(aPageURI, aExpectedMimeType, aExpectedData,
+ aCallback) {
+ PlacesUtils.favicons.getFaviconDataForPage(aPageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ do_check_eq(aExpectedMimeType, aMimeType);
+ do_check_true(compareArrays(aExpectedData, aData));
+ do_check_guid_for_uri(aPageURI);
+ aCallback();
+ });
+}
+
+/**
+ * Checks that the given page has no associated favicon.
+ *
+ * @param aPageURI
+ * nsIURI object for the page to check.
+ * @param aCallback
+ * This function is called after the check finished.
+ */
+function checkFaviconMissingForPage(aPageURI, aCallback) {
+ PlacesUtils.favicons.getFaviconURLForPage(aPageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ do_check_true(aURI === null);
+ aCallback();
+ });
+}
+
+function promiseFaviconMissingForPage(aPageURI) {
+ return new Promise(resolve => checkFaviconMissingForPage(aPageURI, resolve));
+}
+
+function promiseFaviconChanged(aExpectedPageURI, aExpectedFaviconURI) {
+ return new Promise(resolve => waitForFaviconChanged(aExpectedPageURI, aExpectedFaviconURI, resolve));
+}
diff --git a/toolkit/components/places/tests/favicons/test_expireAllFavicons.js b/toolkit/components/places/tests/favicons/test_expireAllFavicons.js
new file mode 100644
index 0000000000..c5d8edfdda
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_expireAllFavicons.js
@@ -0,0 +1,39 @@
+/**
+ * This file tests that favicons are correctly expired by expireAllFavicons.
+ */
+
+"use strict";
+
+const TEST_PAGE_URI = NetUtil.newURI("http://example.com/");
+const BOOKMARKED_PAGE_URI = NetUtil.newURI("http://example.com/bookmarked");
+
+add_task(function* test_expireAllFavicons() {
+ // Add a visited page.
+ yield PlacesTestUtils.addVisits({ uri: TEST_PAGE_URI, transition: TRANSITION_TYPED });
+
+ // Set a favicon for our test page.
+ yield promiseSetIconForPage(TEST_PAGE_URI, SMALLPNG_DATA_URI);
+
+ // Add a page with a bookmark.
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: BOOKMARKED_PAGE_URI,
+ title: "Test bookmark"
+ });
+
+ // Set a favicon for our bookmark.
+ yield promiseSetIconForPage(BOOKMARKED_PAGE_URI, SMALLPNG_DATA_URI);
+
+ // Start expiration only after data has been saved in the database.
+ let promise = promiseTopicObserved(PlacesUtils.TOPIC_FAVICONS_EXPIRED);
+ PlacesUtils.favicons.expireAllFavicons();
+ yield promise;
+
+ // Check that the favicons for the pages we added were removed.
+ yield promiseFaviconMissingForPage(TEST_PAGE_URI);
+ yield promiseFaviconMissingForPage(BOOKMARKED_PAGE_URI);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/favicons/test_favicons_conversions.js b/toolkit/components/places/tests/favicons/test_favicons_conversions.js
new file mode 100644
index 0000000000..fa0d332ecf
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_favicons_conversions.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the image conversions done by the favicon service.
+ */
+
+// Globals
+
+// The pixel values we get on Windows are sometimes +/- 1 value compared to
+// other platforms, so we need to skip some image content tests.
+var isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
+
+/**
+ * Checks the conversion of the given test image file.
+ *
+ * @param aFileName
+ * File that contains the favicon image, located in the test folder.
+ * @param aFileMimeType
+ * MIME type of the image contained in the file.
+ * @param aFileLength
+ * Expected length of the file.
+ * @param aExpectConversion
+ * If false, the icon should be stored as is. If true, the expected data
+ * is loaded from a file named "expected-" + aFileName + ".png".
+ * @param aVaryOnWindows
+ * Indicates that the content of the converted image can be different on
+ * Windows and should not be checked on that platform.
+ * @param aCallback
+ * This function is called after the check finished.
+ */
+function checkFaviconDataConversion(aFileName, aFileMimeType, aFileLength,
+ aExpectConversion, aVaryOnWindows,
+ aCallback) {
+ let pageURI = NetUtil.newURI("http://places.test/page/" + aFileName);
+ PlacesTestUtils.addVisits({ uri: pageURI, transition: TRANSITION_TYPED }).then(
+ function () {
+ let faviconURI = NetUtil.newURI("http://places.test/icon/" + aFileName);
+ let fileData = readFileOfLength(aFileName, aFileLength);
+
+ PlacesUtils.favicons.replaceFaviconData(faviconURI, fileData, fileData.length,
+ aFileMimeType);
+ PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, faviconURI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function CFDC_verify(aURI, aDataLen, aData, aMimeType) {
+ if (!aExpectConversion) {
+ do_check_true(compareArrays(aData, fileData));
+ do_check_eq(aMimeType, aFileMimeType);
+ } else {
+ if (!aVaryOnWindows || !isWindows) {
+ let expectedFile = do_get_file("expected-" + aFileName + ".png");
+ do_check_true(compareArrays(aData, readFileData(expectedFile)));
+ }
+ do_check_eq(aMimeType, "image/png");
+ }
+
+ aCallback();
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+}
+
+// Tests
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_storing_a_normal_16x16_icon() {
+ // 16x16 png, 286 bytes.
+ // optimized: no
+ checkFaviconDataConversion("favicon-normal16.png", "image/png", 286,
+ false, false, run_next_test);
+});
+
+add_test(function test_storing_a_normal_32x32_icon() {
+ // 32x32 png, 344 bytes.
+ // optimized: no
+ checkFaviconDataConversion("favicon-normal32.png", "image/png", 344,
+ false, false, run_next_test);
+});
+
+add_test(function test_storing_a_big_16x16_icon() {
+ // in: 16x16 ico, 1406 bytes.
+ // optimized: no
+ checkFaviconDataConversion("favicon-big16.ico", "image/x-icon", 1406,
+ false, false, run_next_test);
+});
+
+add_test(function test_storing_an_oversize_4x4_icon() {
+ // in: 4x4 jpg, 4751 bytes.
+ // optimized: yes
+ checkFaviconDataConversion("favicon-big4.jpg", "image/jpeg", 4751,
+ true, false, run_next_test);
+});
+
+add_test(function test_storing_an_oversize_32x32_icon() {
+ // in: 32x32 jpg, 3494 bytes.
+ // optimized: yes
+ checkFaviconDataConversion("favicon-big32.jpg", "image/jpeg", 3494,
+ true, true, run_next_test);
+});
+
+add_test(function test_storing_an_oversize_48x48_icon() {
+ // in: 48x48 ico, 56646 bytes.
+ // (howstuffworks.com icon, contains 13 icons with sizes from 16x16 to
+ // 48x48 in varying depths)
+ // optimized: yes
+ checkFaviconDataConversion("favicon-big48.ico", "image/x-icon", 56646,
+ true, false, run_next_test);
+});
+
+add_test(function test_storing_an_oversize_64x64_icon() {
+ // in: 64x64 png, 10698 bytes.
+ // optimized: yes
+ checkFaviconDataConversion("favicon-big64.png", "image/png", 10698,
+ true, false, run_next_test);
+});
+
+add_test(function test_scaling_an_oversize_160x3_icon() {
+ // in: 160x3 jpg, 5095 bytes.
+ // optimized: yes
+ checkFaviconDataConversion("favicon-scale160x3.jpg", "image/jpeg", 5095,
+ true, false, run_next_test);
+});
+
+add_test(function test_scaling_an_oversize_3x160_icon() {
+ // in: 3x160 jpg, 5059 bytes.
+ // optimized: yes
+ checkFaviconDataConversion("favicon-scale3x160.jpg", "image/jpeg", 5059,
+ true, false, run_next_test);
+});
diff --git a/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js
new file mode 100644
index 0000000000..73eea7436a
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests getFaviconDataForPage.
+ */
+
+// Globals
+
+const FAVICON_URI = NetUtil.newURI(do_get_file("favicon-normal32.png"));
+const FAVICON_DATA = readFileData(do_get_file("favicon-normal32.png"));
+const FAVICON_MIMETYPE = "image/png";
+
+// Tests
+
+function run_test()
+{
+ // Check that the favicon loaded correctly before starting the actual tests.
+ do_check_eq(FAVICON_DATA.length, 344);
+ run_next_test();
+}
+
+add_test(function test_normal()
+{
+ let pageURI = NetUtil.newURI("http://example.com/normal");
+
+ PlacesTestUtils.addVisits(pageURI).then(function () {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ pageURI, FAVICON_URI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function () {
+ PlacesUtils.favicons.getFaviconDataForPage(pageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ do_check_true(aURI.equals(FAVICON_URI));
+ do_check_eq(FAVICON_DATA.length, aDataLen);
+ do_check_true(compareArrays(FAVICON_DATA, aData));
+ do_check_eq(FAVICON_MIMETYPE, aMimeType);
+ run_next_test();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+});
+
+add_test(function test_missing()
+{
+ let pageURI = NetUtil.newURI("http://example.com/missing");
+
+ PlacesUtils.favicons.getFaviconDataForPage(pageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ // Check also the expected data types.
+ do_check_true(aURI === null);
+ do_check_true(aDataLen === 0);
+ do_check_true(aData.length === 0);
+ do_check_true(aMimeType === "");
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js
new file mode 100644
index 0000000000..fb2e23ff99
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests getFaviconURLForPage.
+ */
+
+// Tests
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_test(function test_normal()
+{
+ let pageURI = NetUtil.newURI("http://example.com/normal");
+
+ PlacesTestUtils.addVisits(pageURI).then(function () {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ pageURI, SMALLPNG_DATA_URI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function () {
+ PlacesUtils.favicons.getFaviconURLForPage(pageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ do_check_true(aURI.equals(SMALLPNG_DATA_URI));
+
+ // Check also the expected data types.
+ do_check_true(aDataLen === 0);
+ do_check_true(aData.length === 0);
+ do_check_true(aMimeType === "");
+ run_next_test();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+});
+
+add_test(function test_missing()
+{
+ let pageURI = NetUtil.newURI("http://example.com/missing");
+
+ PlacesUtils.favicons.getFaviconURLForPage(pageURI,
+ function (aURI, aDataLen, aData, aMimeType) {
+ // Check also the expected data types.
+ do_check_true(aURI === null);
+ do_check_true(aDataLen === 0);
+ do_check_true(aData.length === 0);
+ do_check_true(aMimeType === "");
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js b/toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js
new file mode 100644
index 0000000000..d055d8d61e
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_moz-anno_favicon_mime_type.js
@@ -0,0 +1,90 @@
+/* -*- 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/. */
+
+/**
+ * This test ensures that the mime type is set for moz-anno channels of favicons
+ * properly. Added with work in bug 481227.
+ */
+
+// Constants
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const testFaviconData = "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%10%00%00%00%10%08%06%00%00%00%1F%F3%FFa%00%00%00%04gAMA%00%00%AF%C87%05%8A%E9%00%00%00%19tEXtSoftware%00Adobe%20ImageReadyq%C9e%3C%00%00%01%D6IDATx%DAb%FC%FF%FF%3F%03%25%00%20%80%98%909%EF%DF%BFg%EF%EC%EC%FC%AD%AC%AC%FC%DF%95%91%F1%BF%89%89%C9%7F%20%FF%D7%EA%D5%AB%B7%DF%BBwO%16%9B%01%00%01%C4%00r%01%08%9F9s%C6%CD%D8%D8%F8%BF%0B%03%C3%FF3%40%BC%0A%88%EF%02q%1A%10%BB%40%F1%AAU%ABv%C1%D4%C30%40%00%81%89%993g%3E%06%1A%F6%3F%14%AA%11D%97%03%F1%7Fc%08%0D%E2%2B))%FD%17%04%89%A1%19%00%10%40%0C%D00%F8%0F3%00%C8%F8%BF%1B%E4%0Ac%88a%E5%60%17%19%FF%0F%0D%0D%05%1B%02v%D9%DD%BB%0A0%03%00%02%08%AC%B9%A3%A3%E3%17%03%D4v%90%01%EF%18%106%C3%0Cz%07%C5%BB%A1%DE%82y%07%20%80%A0%A6%08B%FCn%0C1%60%26%D4%20d%C3VA%C3%06%26%BE%0A%EA-%80%00%82%B9%E0%F7L4%0D%EF%90%F8%C6%60%2F%0A%82%BD%01%13%07%0700%D0%01%02%88%11%E4%02P%B41%DC%BB%C7%D0%014%0D%E8l%06W%20%06%BA%88%A1%1C%1AS%15%40%7C%16%CA6.%2Fgx%BFg%0F%83%CB%D9%B3%0C%7B%80%7C%80%00%02%BB%00%E8%9F%ED%20%1B%3A%A0%A6%9F%81%DA%DC%01%C5%B0%80%ED%80%FA%BF%BC%BC%FC%3F%83%12%90%9D%96%F6%1F%20%80%18%DE%BD%7B%C7%0E%8E%05AD%20%FEGr%A6%A0%A0%E0%7F%25P%80%02%9D%0F%D28%13%18%23%C6%C0%B0%02E%3D%C8%F5%00%01%04%8F%05P%A8%BA%40my%87%E4%12c%A8%8D%20%8B%D0%D3%00%08%03%04%10%9C%01R%E4%82d%3B%C8%A0%99%C6%90%90%C6%A5%19%84%01%02%08%9E%17%80%C9x%F7%7B%A0%DBVC%F9%A0%C0%5C%7D%16%2C%CE%00%F4%C6O%5C%99%09%20%800L%04y%A5%03%1A%95%A0%80%05%05%14.%DBA%18%20%80%18)%CD%CE%00%01%06%00%0C'%94%C7%C0k%C9%2C%00%00%00%00IEND%AEB%60%82";
+const moz_anno_favicon_prefix = "moz-anno:favicon:";
+
+// streamListener
+
+function streamListener(aExpectedContentType)
+{
+ this._expectedContentType = aExpectedContentType;
+}
+streamListener.prototype =
+{
+ onStartRequest: function(aRequest, aContext)
+ {
+ // We have other tests that make sure the data is what we expect. We just
+ // need to check the content type here.
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ dump("*** Checking " + channel.URI.spec + "\n");
+ do_check_eq(channel.contentType, this._expectedContentType);
+
+ // If we somehow throw before doing the above check, the test will pass, so
+ // we do this for extra sanity.
+ this._checked = true;
+ },
+ onStopRequest: function()
+ {
+ do_check_true(this._checked);
+ do_test_finished();
+ },
+ onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount)
+ {
+ aRequest.cancel(Cr.NS_ERROR_ABORT);
+ }
+};
+
+// Test Runner
+
+function run_test()
+{
+ let fs = Cc["@mozilla.org/browser/favicon-service;1"].
+ getService(Ci.nsIFaviconService);
+
+ // Test that the default icon has the content type of image/png.
+ let channel = NetUtil.newChannel({
+ uri: fs.defaultFavicon,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
+ });
+ channel.asyncOpen2(new streamListener("image/png"));
+ do_test_pending();
+
+ // Test URI that we don't know anything about. Will end up being the default
+ // icon, so expect image/png.
+ channel = NetUtil.newChannel({
+ uri: moz_anno_favicon_prefix + "http://mozilla.org",
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
+ });
+ channel.asyncOpen2(new streamListener("image/png"));
+ do_test_pending();
+
+ // Test that the content type of a favicon we add ends up being image/png.
+ let testURI = uri("http://mozilla.org/");
+ // Add the data before opening
+ fs.replaceFaviconDataFromDataURL(testURI, testFaviconData,
+ (Date.now() + 60 * 60 * 24 * 1000) * 1000,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ // Open the channel
+ channel = NetUtil.newChannel({
+ uri: moz_anno_favicon_prefix + testURI.spec,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
+ });
+ channel.asyncOpen2(new streamListener("image/png"));
+ do_test_pending();
+}
diff --git a/toolkit/components/places/tests/favicons/test_page-icon_protocol.js b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js
new file mode 100644
index 0000000000..5533d5135f
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js
@@ -0,0 +1,66 @@
+const ICON_DATA = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+const TEST_URI = NetUtil.newURI("http://mozilla.org/");
+const ICON_URI = NetUtil.newURI("http://mozilla.org/favicon.ico");
+
+function fetchIconForSpec(spec) {
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI("page-icon:" + TEST_URI.spec),
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON
+ }, (input, status, request) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error("unable to load icon"));
+ return;
+ }
+
+ try {
+ let data = NetUtil.readInputStreamToString(input, input.available());
+ let contentType = request.QueryInterface(Ci.nsIChannel).contentType;
+ input.close();
+ resolve({ data, contentType });
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+}
+
+var gDefaultFavicon;
+var gFavicon;
+
+add_task(function* setup() {
+ yield PlacesTestUtils.addVisits({ uri: TEST_URI });
+
+ PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+ ICON_URI, ICON_DATA, (Date.now() + 8640000) * 1000,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ yield new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ TEST_URI, ICON_URI, false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ gDefaultFavicon = yield fetchIconForSpec(PlacesUtils.favicons.defaultFavicon);
+ gFavicon = yield fetchIconForSpec(ICON_DATA);
+});
+
+add_task(function* known_url() {
+ let {data, contentType} = yield fetchIconForSpec(TEST_URI.spec);
+ Assert.equal(contentType, gFavicon.contentType);
+ Assert.ok(data == gFavicon.data, "Got the favicon data");
+});
+
+add_task(function* unknown_url() {
+ let {data, contentType} = yield fetchIconForSpec("http://www.moz.org/");
+ Assert.equal(contentType, gDefaultFavicon.contentType);
+ Assert.ok(data == gDefaultFavicon.data, "Got the default favicon data");
+});
+
+add_task(function* invalid_url() {
+ let {data, contentType} = yield fetchIconForSpec("test");
+ Assert.equal(contentType, gDefaultFavicon.contentType);
+ Assert.ok(data == gDefaultFavicon.data, "Got the default favicon data");
+});
diff --git a/toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js b/toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js
new file mode 100644
index 0000000000..df61c22cd0
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_query_result_favicon_changed_on_child.js
@@ -0,0 +1,74 @@
+/**
+ * Test for bug 451499 <https://bugzilla.mozilla.org/show_bug.cgi?id=451499>:
+ * Wrong folder icon appears on smart bookmarks.
+ */
+
+"use strict";
+
+const PAGE_URI = NetUtil.newURI("http://example.com/test_query_result");
+
+add_task(function* test_query_result_favicon_changed_on_child() {
+ // Bookmark our test page, so it will appear in the query resultset.
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "test_bookmark",
+ url: PAGE_URI
+ });
+
+ // Get the last 10 bookmarks added to the menu or the toolbar.
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.toolbarFolderId], 2);
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.maxResults = 10;
+ options.excludeQueries = 1;
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let resultObserver = {
+ __proto__: NavHistoryResultObserver.prototype,
+ containerStateChanged(aContainerNode, aOldState, aNewState) {
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ // We set a favicon on PAGE_URI while the container is open. The
+ // favicon for the page must have data associated with it in order for
+ // the icon changed notifications to be sent, so we use a valid image
+ // data URI.
+ PlacesUtils.favicons.setAndFetchFaviconForPage(PAGE_URI,
+ SMALLPNG_DATA_URI,
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ }
+ },
+ nodeIconChanged(aNode) {
+ do_throw("The icon should be set only for the page," +
+ " not for the containing query.");
+ }
+ };
+ result.addObserver(resultObserver, false);
+
+ // Open the container and wait for containerStateChanged. We should start
+ // observing before setting |containerOpen| as that's caused by the
+ // setAndFetchFaviconForPage() call caused by the containerStateChanged
+ // observer above.
+ let promise = promiseFaviconChanged(PAGE_URI, SMALLPNG_DATA_URI);
+ result.root.containerOpen = true;
+ yield promise;
+
+ // We must wait for the asynchronous database thread to finish the
+ // operation, and then for the main thread to process any pending
+ // notifications that came from the asynchronous thread, before we can be
+ // sure that nodeIconChanged was not invoked in the meantime.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ result.removeObserver(resultObserver);
+
+ // Free the resources immediately.
+ result.root.containerOpen = false;
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js
new file mode 100644
index 0000000000..ac53e70e98
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js
@@ -0,0 +1,264 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests for mozIAsyncFavicons::replaceFaviconData()
+ */
+
+var iconsvc = PlacesUtils.favicons;
+var histsvc = PlacesUtils.history;
+var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+var originalFavicon = {
+ file: do_get_file("favicon-normal16.png"),
+ uri: uri(do_get_file("favicon-normal16.png")),
+ data: readFileData(do_get_file("favicon-normal16.png")),
+ mimetype: "image/png"
+};
+
+var uniqueFaviconId = 0;
+function createFavicon(fileName) {
+ let tempdir = Services.dirsvc.get("TmpD", Ci.nsILocalFile);
+
+ // remove any existing file at the path we're about to copy to
+ let outfile = tempdir.clone();
+ outfile.append(fileName);
+ try { outfile.remove(false); } catch (e) {}
+
+ originalFavicon.file.copyToFollowingLinks(tempdir, fileName);
+
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ stream.init(outfile, 0x02 | 0x08 | 0x10, 0o600, 0);
+
+ // append some data that sniffers/encoders will ignore that will distinguish
+ // the different favicons we'll create
+ uniqueFaviconId++;
+ let uniqueStr = "uid:" + uniqueFaviconId;
+ stream.write(uniqueStr, uniqueStr.length);
+ stream.close();
+
+ do_check_eq(outfile.leafName.substr(0, fileName.length), fileName);
+
+ return {
+ file: outfile,
+ uri: uri(outfile),
+ data: readFileData(outfile),
+ mimetype: "image/png"
+ };
+}
+
+function checkCallbackSucceeded(callbackMimetype, callbackData, sourceMimetype, sourceData) {
+ do_check_eq(callbackMimetype, sourceMimetype);
+ do_check_true(compareArrays(callbackData, sourceData));
+}
+
+function run_test() {
+ // check that the favicon loaded correctly
+ do_check_eq(originalFavicon.data.length, 286);
+ run_next_test();
+}
+
+add_task(function* test_replaceFaviconData_validHistoryURI() {
+ do_print("test replaceFaviconData for valid history uri");
+
+ let pageURI = uri("http://test1.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let favicon = createFavicon("favicon1.png");
+
+ iconsvc.replaceFaviconData(favicon.uri, favicon.data, favicon.data.length,
+ favicon.mimetype);
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconData_validHistoryURI_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data);
+ checkFaviconDataForPage(
+ pageURI, favicon.mimetype, favicon.data,
+ function test_replaceFaviconData_validHistoryURI_callback() {
+ favicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, systemPrincipal);
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconData_overrideDefaultFavicon() {
+ do_print("test replaceFaviconData to override a later setAndFetchFaviconForPage");
+
+ let pageURI = uri("http://test2.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon2.png");
+ let secondFavicon = createFavicon("favicon3.png");
+
+ iconsvc.replaceFaviconData(
+ firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
+ secondFavicon.mimetype);
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconData_overrideDefaultFavicon_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconData_overrideDefaultFavicon_callback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, systemPrincipal);
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconData_replaceExisting() {
+ do_print("test replaceFaviconData to override a previous setAndFetchFaviconForPage");
+
+ let pageURI = uri("http://test3.bar");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon4.png");
+ let secondFavicon = createFavicon("favicon5.png");
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconData_replaceExisting_firstSet_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, firstFavicon.mimetype, firstFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, firstFavicon.mimetype, firstFavicon.data,
+ function test_replaceFaviconData_overrideDefaultFavicon_firstCallback() {
+ iconsvc.replaceFaviconData(
+ firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
+ secondFavicon.mimetype);
+ PlacesTestUtils.promiseAsyncUpdates().then(() => {
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconData_overrideDefaultFavicon_secondCallback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ }, systemPrincipal);
+ });
+ });
+ }, systemPrincipal);
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconData_unrelatedReplace() {
+ do_print("test replaceFaviconData to not make unrelated changes");
+
+ let pageURI = uri("http://test4.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let favicon = createFavicon("favicon6.png");
+ let unrelatedFavicon = createFavicon("favicon7.png");
+
+ iconsvc.replaceFaviconData(
+ unrelatedFavicon.uri, unrelatedFavicon.data, unrelatedFavicon.data.length,
+ unrelatedFavicon.mimetype);
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, favicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconData_unrelatedReplace_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data);
+ checkFaviconDataForPage(
+ pageURI, favicon.mimetype, favicon.data,
+ function test_replaceFaviconData_unrelatedReplace_callback() {
+ favicon.file.remove(false);
+ unrelatedFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, systemPrincipal);
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconData_badInputs() {
+ do_print("test replaceFaviconData to throw on bad inputs");
+
+ let favicon = createFavicon("favicon8.png");
+
+ let ex = null;
+ try {
+ iconsvc.replaceFaviconData(
+ favicon.uri, favicon.data, favicon.data.length, "");
+ } catch (e) {
+ ex = e;
+ } finally {
+ do_check_true(!!ex);
+ }
+
+ ex = null;
+ try {
+ iconsvc.replaceFaviconData(
+ null, favicon.data, favicon.data.length, favicon.mimeType);
+ } catch (e) {
+ ex = e;
+ } finally {
+ do_check_true(!!ex);
+ }
+
+ ex = null;
+ try {
+ iconsvc.replaceFaviconData(
+ favicon.uri, null, 0, favicon.mimeType);
+ } catch (e) {
+ ex = e;
+ } finally {
+ do_check_true(!!ex);
+ }
+
+ favicon.file.remove(false);
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconData_twiceReplace() {
+ do_print("test replaceFaviconData on multiple replacements");
+
+ let pageURI = uri("http://test5.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon9.png");
+ let secondFavicon = createFavicon("favicon10.png");
+
+ iconsvc.replaceFaviconData(
+ firstFavicon.uri, firstFavicon.data, firstFavicon.data.length,
+ firstFavicon.mimetype);
+ iconsvc.replaceFaviconData(
+ firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
+ secondFavicon.mimetype);
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconData_twiceReplace_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconData_twiceReplace_callback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ }, systemPrincipal);
+ }, systemPrincipal);
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js
new file mode 100644
index 0000000000..69a5ba8523
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js
@@ -0,0 +1,352 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests for mozIAsyncFavicons::replaceFaviconData()
+ */
+
+var iconsvc = PlacesUtils.favicons;
+var histsvc = PlacesUtils.history;
+
+var originalFavicon = {
+ file: do_get_file("favicon-normal16.png"),
+ uri: uri(do_get_file("favicon-normal16.png")),
+ data: readFileData(do_get_file("favicon-normal16.png")),
+ mimetype: "image/png"
+};
+
+var uniqueFaviconId = 0;
+function createFavicon(fileName) {
+ let tempdir = Services.dirsvc.get("TmpD", Ci.nsILocalFile);
+
+ // remove any existing file at the path we're about to copy to
+ let outfile = tempdir.clone();
+ outfile.append(fileName);
+ try { outfile.remove(false); } catch (e) {}
+
+ originalFavicon.file.copyToFollowingLinks(tempdir, fileName);
+
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ stream.init(outfile, 0x02 | 0x08 | 0x10, 0o600, 0);
+
+ // append some data that sniffers/encoders will ignore that will distinguish
+ // the different favicons we'll create
+ uniqueFaviconId++;
+ let uniqueStr = "uid:" + uniqueFaviconId;
+ stream.write(uniqueStr, uniqueStr.length);
+ stream.close();
+
+ do_check_eq(outfile.leafName.substr(0, fileName.length), fileName);
+
+ return {
+ file: outfile,
+ uri: uri(outfile),
+ data: readFileData(outfile),
+ mimetype: "image/png"
+ };
+}
+
+function createDataURLForFavicon(favicon) {
+ return "data:" + favicon.mimetype + ";base64," + toBase64(favicon.data);
+}
+
+function checkCallbackSucceeded(callbackMimetype, callbackData, sourceMimetype, sourceData) {
+ do_check_eq(callbackMimetype, sourceMimetype);
+ do_check_true(compareArrays(callbackData, sourceData));
+}
+
+function run_test() {
+ // check that the favicon loaded correctly
+ do_check_eq(originalFavicon.data.length, 286);
+ run_next_test();
+}
+
+add_task(function* test_replaceFaviconDataFromDataURL_validHistoryURI() {
+ do_print("test replaceFaviconDataFromDataURL for valid history uri");
+
+ let pageURI = uri("http://test1.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let favicon = createFavicon("favicon1.png");
+ iconsvc.replaceFaviconDataFromDataURL(favicon.uri, createDataURLForFavicon(favicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_validHistoryURI_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data);
+ checkFaviconDataForPage(
+ pageURI, favicon.mimetype, favicon.data,
+ function test_replaceFaviconDataFromDataURL_validHistoryURI_callback() {
+ favicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() {
+ do_print("test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage");
+
+ let pageURI = uri("http://test2.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon2.png");
+ let secondFavicon = createFavicon("favicon3.png");
+
+ iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_callback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_replaceExisting() {
+ do_print("test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage");
+
+ let pageURI = uri("http://test3.bar");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon4.png");
+ let secondFavicon = createFavicon("favicon5.png");
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_replaceExisting_firstSet_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, firstFavicon.mimetype, firstFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, firstFavicon.mimetype, firstFavicon.data,
+ function test_replaceFaviconDataFromDataURL_replaceExisting_firstCallback() {
+ iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconDataFromDataURL_replaceExisting_secondCallback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_unrelatedReplace() {
+ do_print("test replaceFaviconDataFromDataURL to not make unrelated changes");
+
+ let pageURI = uri("http://test4.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let favicon = createFavicon("favicon6.png");
+ let unrelatedFavicon = createFavicon("favicon7.png");
+
+ iconsvc.replaceFaviconDataFromDataURL(unrelatedFavicon.uri, createDataURLForFavicon(unrelatedFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, favicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_unrelatedReplace_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, favicon.mimetype, favicon.data);
+ checkFaviconDataForPage(
+ pageURI, favicon.mimetype, favicon.data,
+ function test_replaceFaviconDataFromDataURL_unrelatedReplace_callback() {
+ favicon.file.remove(false);
+ unrelatedFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_badInputs() {
+ do_print("test replaceFaviconDataFromDataURL to throw on bad inputs");
+
+ let favicon = createFavicon("favicon8.png");
+
+ let ex = null;
+ try {
+ iconsvc.replaceFaviconDataFromDataURL(favicon.uri, "", 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ } catch (e) {
+ ex = e;
+ } finally {
+ do_check_true(!!ex);
+ }
+
+ ex = null;
+ try {
+ iconsvc.replaceFaviconDataFromDataURL(null, createDataURLForFavicon(favicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ } catch (e) {
+ ex = e;
+ } finally {
+ do_check_true(!!ex);
+ }
+
+ favicon.file.remove(false);
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_twiceReplace() {
+ do_print("test replaceFaviconDataFromDataURL on multiple replacements");
+
+ let pageURI = uri("http://test5.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon9.png");
+ let secondFavicon = createFavicon("favicon10.png");
+
+ iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_twiceReplace_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconDataFromDataURL_twiceReplace_callback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_afterRegularAssign() {
+ do_print("test replaceFaviconDataFromDataURL after replaceFaviconData");
+
+ let pageURI = uri("http://test6.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon11.png");
+ let secondFavicon = createFavicon("favicon12.png");
+
+ iconsvc.replaceFaviconData(
+ firstFavicon.uri, firstFavicon.data, firstFavicon.data.length,
+ firstFavicon.mimetype);
+ iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_afterRegularAssign_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconDataFromDataURL_afterRegularAssign_callback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_replaceFaviconDataFromDataURL_beforeRegularAssign() {
+ do_print("test replaceFaviconDataFromDataURL before replaceFaviconData");
+
+ let pageURI = uri("http://test7.bar/");
+ yield PlacesTestUtils.addVisits(pageURI);
+
+ let firstFavicon = createFavicon("favicon13.png");
+ let secondFavicon = createFavicon("favicon14.png");
+
+ iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon), 0,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ iconsvc.replaceFaviconData(
+ firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
+ secondFavicon.mimetype);
+
+ let deferSetAndFetchFavicon = Promise.defer();
+ iconsvc.setAndFetchFaviconForPage(
+ pageURI, firstFavicon.uri, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ function test_replaceFaviconDataFromDataURL_beforeRegularAssign_check(aURI, aDataLen, aData, aMimeType) {
+ checkCallbackSucceeded(aMimeType, aData, secondFavicon.mimetype, secondFavicon.data);
+ checkFaviconDataForPage(
+ pageURI, secondFavicon.mimetype, secondFavicon.data,
+ function test_replaceFaviconDataFromDataURL_beforeRegularAssign_callback() {
+ firstFavicon.file.remove(false);
+ secondFavicon.file.remove(false);
+ deferSetAndFetchFavicon.resolve();
+ });
+ }, Services.scriptSecurityManager.getSystemPrincipal());
+ yield deferSetAndFetchFavicon.promise;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+/* toBase64 copied from image/test/unit/test_encoder_png.js */
+
+/* Convert data (an array of integers) to a Base64 string. */
+const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +
+ '0123456789+/';
+const base64Pad = '=';
+function toBase64(data) {
+ let result = '';
+ let length = data.length;
+ let i;
+ // Convert every three bytes to 4 ascii characters.
+ for (i = 0; i < (length - 2); i += 3) {
+ result += toBase64Table[data[i] >> 2];
+ result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)];
+ result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)];
+ result += toBase64Table[data[i+2] & 0x3f];
+ }
+
+ // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
+ if (length%3) {
+ i = length - (length%3);
+ result += toBase64Table[data[i] >> 2];
+ if ((length%3) == 2) {
+ result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)];
+ result += toBase64Table[(data[i+1] & 0x0f) << 2];
+ result += base64Pad;
+ } else {
+ result += toBase64Table[(data[i] & 0x03) << 4];
+ result += base64Pad + base64Pad;
+ }
+ }
+
+ return result;
+}
diff --git a/toolkit/components/places/tests/favicons/xpcshell.ini b/toolkit/components/places/tests/favicons/xpcshell.ini
new file mode 100644
index 0000000000..851f193c79
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/xpcshell.ini
@@ -0,0 +1,32 @@
+[DEFAULT]
+head = head_favicons.js
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ expected-favicon-big32.jpg.png
+ expected-favicon-big4.jpg.png
+ expected-favicon-big48.ico.png
+ expected-favicon-big64.png.png
+ expected-favicon-scale160x3.jpg.png
+ expected-favicon-scale3x160.jpg.png
+ favicon-big16.ico
+ favicon-big32.jpg
+ favicon-big4.jpg
+ favicon-big48.ico
+ favicon-big64.png
+ favicon-normal16.png
+ favicon-normal32.png
+ favicon-scale160x3.jpg
+ favicon-scale3x160.jpg
+
+[test_expireAllFavicons.js]
+[test_favicons_conversions.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_getFaviconDataForPage.js]
+[test_getFaviconURLForPage.js]
+[test_moz-anno_favicon_mime_type.js]
+[test_page-icon_protocol.js]
+[test_query_result_favicon_changed_on_child.js]
+[test_replaceFaviconData.js]
+[test_replaceFaviconDataFromDataURL.js]
diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js
new file mode 100644
index 0000000000..ddb6dcbd7e
--- /dev/null
+++ b/toolkit/components/places/tests/head_common.js
@@ -0,0 +1,869 @@
+/* -*- 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 = 35;
+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"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
+ "resource://gre/modules/BookmarkJSONUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
+ "resource://gre/modules/BookmarkHTMLUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
+ "resource://gre/modules/PlacesTransactions.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+// This imports various other objects in addition to PlacesUtils.
+Cu.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(true);
+
+// 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));
+ do_check_eq(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(aObsSubject, aObsTopic, aObsData) {
+ Services.obs.removeObserver(observe, aObsTopic);
+ resolve([aObsSubject, aObsData]);
+ }, aTopic, false);
+ });
+}
+
+/**
+ * Simulates a Places shutdown.
+ */
+var shutdownPlaces = function() {
+ do_print("shutdownPlaces: starting");
+ let promise = new Promise(resolve => {
+ Services.obs.addObserver(resolve, "places-connection-closed", false);
+ });
+ let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
+ hs.observe(null, "profile-change-teardown", null);
+ do_print("shutdownPlaces: sent profile-change-teardown");
+ hs.observe(null, "test-simulate-places-shutdown", null);
+ do_print("shutdownPlaces: sent test-simulate-places-shutdown");
+ return promise.then(() => {
+ do_print("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);
+ do_check_true(bookmarksHTMLFile.exists());
+ bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML);
+ let profileBookmarksHTMLFile = gProfD.clone();
+ profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
+ do_check_true(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);
+ do_check_false(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);
+ do_check_true(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));
+ do_check_true(bookmarksBackupDir.exists());
+ }
+ let profileBookmarksJSONFile = bookmarksBackupDir.clone();
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ if (profileBookmarksJSONFile.exists()) {
+ profileBookmarksJSONFile.remove();
+ }
+ let bookmarksJSONFile = gTestDir.clone();
+ bookmarksJSONFile.append(aFilename);
+ do_check_true(bookmarksJSONFile.exists());
+ bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
+ profileBookmarksJSONFile = bookmarksBackupDir.clone();
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ do_check_true(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);
+ do_check_false(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;
+ while (files.hasMoreElements()) {
+ let entry = files.getNext().QueryInterface(Ci.nsIFile);
+ if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
+ profileBookmarksJSONFile = entry;
+ break;
+ }
+ }
+ } else {
+ profileBookmarksJSONFile = gProfD.clone();
+ profileBookmarksJSONFile.append("bookmarkbackups");
+ profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
+ }
+ do_check_true(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;
+ }
+ do_check_true(/^[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;
+ do_check_true(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);
+ do_check_eq(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;
+ do_check_true(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);
+ do_check_eq(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(unused, 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.
+ */
+function* foreign_count(url) {
+ if (url instanceof Ci.nsIURI)
+ url = url.spec;
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield 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/toolkit/components/places/tests/history/.eslintrc.js b/toolkit/components/places/tests/history/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/history/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/history/head_history.js b/toolkit/components/places/tests/history/head_history.js
new file mode 100644
index 0000000000..870802dc1d
--- /dev/null
+++ b/toolkit/components/places/tests/history/head_history.js
@@ -0,0 +1,19 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.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);
+}
diff --git a/toolkit/components/places/tests/history/test_insert.js b/toolkit/components/places/tests/history/test_insert.js
new file mode 100644
index 0000000000..e2884af8c9
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_insert.js
@@ -0,0 +1,257 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Tests for `History.insert` and `History.insertMany`, as implemented in History.jsm
+
+"use strict";
+
+add_task(function* test_insert_error_cases() {
+ const TEST_URL = "http://mozilla.com";
+
+ Assert.throws(
+ () => PlacesUtils.history.insert(),
+ /TypeError: pageInfo must be an object/,
+ "passing a null into History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert(1),
+ /TypeError: pageInfo must be an object/,
+ "passing a non object into History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({}),
+ /TypeError: PageInfo object must have a url property/,
+ "passing an object without a url to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: 123}),
+ /TypeError: Invalid url or guid: 123/,
+ "passing an object with an invalid url to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: TEST_URL}),
+ /TypeError: PageInfo object must have an array of visits/,
+ "passing an object without a visits property to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: TEST_URL, visits: 1}),
+ /TypeError: PageInfo object must have an array of visits/,
+ "passing an object with a non-array visits property to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({url: TEST_URL, visits: []}),
+ /TypeError: PageInfo object must have an array of visits/,
+ "passing an object with an empty array as the visits property to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {
+ transition: TRANSITION_LINK,
+ date: "a"
+ }
+ ]}),
+ /TypeError: Expected a Date, got a/,
+ "passing a visit object with an invalid date to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {
+ transition: TRANSITION_LINK
+ },
+ {
+ transition: TRANSITION_LINK,
+ date: "a"
+ }
+ ]}),
+ /TypeError: Expected a Date, got a/,
+ "passing a second visit object with an invalid date to History.insert should throw a TypeError"
+ );
+ let futureDate = new Date();
+ futureDate.setDate(futureDate.getDate() + 1000);
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {
+ transition: TRANSITION_LINK,
+ date: futureDate,
+ }
+ ]}),
+ `TypeError: date: ${futureDate} is not a valid date`,
+ "passing a visit object with a future date to History.insert should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insert({
+ url: TEST_URL,
+ visits: [
+ {transition: "a"}
+ ]}),
+ /TypeError: transition: a is not a valid transition type/,
+ "passing a visit object with an invalid transition to History.insert should throw a TypeError"
+ );
+});
+
+add_task(function* test_history_insert() {
+ const TEST_URL = "http://mozilla.com/";
+
+ let inserter = Task.async(function*(name, filter, referrer, date, transition) {
+ do_print(name);
+ do_print(`filter: ${filter}, referrer: ${referrer}, date: ${date}, transition: ${transition}`);
+
+ let uri = NetUtil.newURI(TEST_URL + Math.random());
+ let title = "Visit " + Math.random();
+
+ let pageInfo = {
+ title,
+ visits: [
+ {transition: transition, referrer: referrer, date: date, }
+ ]
+ };
+
+ pageInfo.url = yield filter(uri);
+
+ let result = yield PlacesUtils.history.insert(pageInfo);
+
+ Assert.ok(PlacesUtils.isValidGuid(result.guid), "guid for pageInfo object is valid");
+ Assert.equal(uri.spec, result.url.href, "url is correct for pageInfo object");
+ Assert.equal(title, result.title, "title is correct for pageInfo object");
+ Assert.equal(TRANSITION_LINK, result.visits[0].transition, "transition is correct for pageInfo object");
+ if (referrer) {
+ Assert.equal(referrer, result.visits[0].referrer.href, "url of referrer for visit is correct");
+ } else {
+ Assert.equal(null, result.visits[0].referrer, "url of referrer for visit is correct");
+ }
+ if (date) {
+ Assert.equal(Number(date),
+ Number(result.visits[0].date),
+ "date of visit is correct");
+ }
+
+ Assert.ok(yield PlacesTestUtils.isPageInDB(uri), "Page was added");
+ Assert.ok(yield PlacesTestUtils.visitsInDB(uri), "Visit was added");
+ });
+
+ try {
+ for (let referrer of [TEST_URL, null]) {
+ for (let date of [new Date(), null]) {
+ for (let transition of [TRANSITION_LINK, null]) {
+ yield inserter("Testing History.insert() with an nsIURI", x => x, referrer, date, transition);
+ yield inserter("Testing History.insert() with a string url", x => x.spec, referrer, date, transition);
+ yield inserter("Testing History.insert() with a URL object", x => new URL(x.spec), referrer, date, transition);
+ }
+ }
+ }
+ } finally {
+ yield PlacesTestUtils.clearHistory();
+ }
+});
+
+add_task(function* test_insert_multiple_error_cases() {
+ let validPageInfo = {
+ url: "http://mozilla.com",
+ visits: [
+ {transition: TRANSITION_LINK}
+ ]
+ };
+
+ Assert.throws(
+ () => PlacesUtils.history.insertMany(),
+ /TypeError: pageInfos must be an array/,
+ "passing a null into History.insertMany should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insertMany([]),
+ /TypeError: pageInfos may not be an empty array/,
+ "passing an empty array into History.insertMany should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.insertMany([validPageInfo, {}]),
+ /TypeError: PageInfo object must have a url property/,
+ "passing a second invalid PageInfo object to History.insertMany should throw a TypeError"
+ );
+});
+
+add_task(function* test_history_insertMany() {
+ const BAD_URLS = ["about:config", "chrome://browser/content/browser.xul"];
+ const GOOD_URLS = [1, 2, 3].map(x => { return `http://mozilla.com/${x}`; });
+
+ let makePageInfos = Task.async(function*(urls, filter = x => x) {
+ let pageInfos = [];
+ for (let url of urls) {
+ let uri = NetUtil.newURI(url);
+
+ let pageInfo = {
+ title: `Visit to ${url}`,
+ visits: [
+ {transition: TRANSITION_LINK}
+ ]
+ };
+
+ pageInfo.url = yield filter(uri);
+ pageInfos.push(pageInfo);
+ }
+ return pageInfos;
+ });
+
+ let inserter = Task.async(function*(name, filter, useCallbacks) {
+ do_print(name);
+ do_print(`filter: ${filter}`);
+ do_print(`useCallbacks: ${useCallbacks}`);
+ yield PlacesTestUtils.clearHistory();
+
+ let result;
+ let allUrls = GOOD_URLS.concat(BAD_URLS);
+ let pageInfos = yield makePageInfos(allUrls, filter);
+
+ if (useCallbacks) {
+ let onResultUrls = [];
+ let onErrorUrls = [];
+ result = yield PlacesUtils.history.insertMany(pageInfos, pageInfo => {
+ let url = pageInfo.url.href;
+ Assert.ok(GOOD_URLS.includes(url), "onResult callback called for correct url");
+ onResultUrls.push(url);
+ Assert.equal(`Visit to ${url}`, pageInfo.title, "onResult callback provides the correct title");
+ Assert.ok(PlacesUtils.isValidGuid(pageInfo.guid), "onResult callback provides a valid guid");
+ }, pageInfo => {
+ let url = pageInfo.url.href;
+ Assert.ok(BAD_URLS.includes(url), "onError callback called for correct uri");
+ onErrorUrls.push(url);
+ Assert.equal(undefined, pageInfo.title, "onError callback provides the correct title");
+ Assert.equal(undefined, pageInfo.guid, "onError callback provides the expected guid");
+ });
+ Assert.equal(GOOD_URLS.sort().toString(), onResultUrls.sort().toString(), "onResult callback was called for each good url");
+ Assert.equal(BAD_URLS.sort().toString(), onErrorUrls.sort().toString(), "onError callback was called for each bad url");
+ } else {
+ result = yield PlacesUtils.history.insertMany(pageInfos);
+ }
+
+ Assert.equal(undefined, result, "insertMany returned undefined");
+
+ for (let url of allUrls) {
+ let expected = GOOD_URLS.includes(url);
+ Assert.equal(expected, yield PlacesTestUtils.isPageInDB(url), `isPageInDB for ${url} is ${expected}`);
+ Assert.equal(expected, yield PlacesTestUtils.visitsInDB(url), `visitsInDB for ${url} is ${expected}`);
+ }
+ });
+
+ try {
+ for (let useCallbacks of [false, true]) {
+ yield inserter("Testing History.insertMany() with an nsIURI", x => x, useCallbacks);
+ yield inserter("Testing History.insertMany() with a string url", x => x.spec, useCallbacks);
+ yield inserter("Testing History.insertMany() with a URL object", x => new URL(x.spec), useCallbacks);
+ }
+ // Test rejection when no items added
+ let pageInfos = yield makePageInfos(BAD_URLS);
+ PlacesUtils.history.insertMany(pageInfos).then(() => {
+ Assert.ok(false, "History.insertMany rejected promise with all bad URLs");
+ }, error => {
+ Assert.equal("No items were added to history.", error.message, "History.insertMany rejected promise with all bad URLs");
+ });
+ } finally {
+ yield PlacesTestUtils.clearHistory();
+ }
+});
diff --git a/toolkit/components/places/tests/history/test_remove.js b/toolkit/components/places/tests/history/test_remove.js
new file mode 100644
index 0000000000..7423f64649
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_remove.js
@@ -0,0 +1,360 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Tests for `History.remove`, as implemented in History.jsm
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+
+// Test removing a single page
+add_task(function* test_remove_single() {
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+
+ let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+ yield PlacesTestUtils.addVisits(WITNESS_URI);
+ Assert.ok(page_in_database(WITNESS_URI));
+
+ let remover = Task.async(function*(name, filter, options) {
+ do_print(name);
+ do_print(JSON.stringify(options));
+ do_print("Setting up visit");
+
+ let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+ let title = "Visit " + Math.random();
+ yield PlacesTestUtils.addVisits({uri: uri, title: title});
+ Assert.ok(visits_in_database(uri), "History entry created");
+
+ let removeArg = yield filter(uri);
+
+ if (options.addBookmark) {
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test bookmark");
+ }
+
+ let shouldRemove = !options.addBookmark;
+ let observer;
+ let promiseObserved = new Promise((resolve, reject) => {
+ observer = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(aUri) {
+ reject(new Error("Unexpected call to onVisit " + aUri.spec));
+ },
+ onTitleChanged: function(aUri) {
+ reject(new Error("Unexpected call to onTitleChanged " + aUri.spec));
+ },
+ onClearHistory: function() {
+ reject("Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(aUri) {
+ reject(new Error("Unexpected call to onPageChanged " + aUri.spec));
+ },
+ onFrecencyChanged: function(aURI) {
+ try {
+ Assert.ok(!shouldRemove, "Observing onFrecencyChanged");
+ Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri");
+ } finally {
+ resolve();
+ }
+ },
+ onManyFrecenciesChanged: function() {
+ try {
+ Assert.ok(!shouldRemove, "Observing onManyFrecenciesChanged");
+ } finally {
+ resolve();
+ }
+ },
+ onDeleteURI: function(aURI) {
+ try {
+ Assert.ok(shouldRemove, "Observing onDeleteURI");
+ Assert.equal(aURI.spec, uri.spec, "Observing effect on the right uri");
+ } finally {
+ resolve();
+ }
+ },
+ onDeleteVisits: function(aURI) {
+ Assert.equal(aURI.spec, uri.spec, "Observing onDeleteVisits on the right uri");
+ }
+ };
+ });
+ PlacesUtils.history.addObserver(observer, false);
+
+ do_print("Performing removal");
+ let removed = false;
+ if (options.useCallback) {
+ let onRowCalled = false;
+ let guid = do_get_guid_for_uri(uri);
+ removed = yield PlacesUtils.history.remove(removeArg, page => {
+ Assert.equal(onRowCalled, false, "Callback has not been called yet");
+ onRowCalled = true;
+ Assert.equal(page.url.href, uri.spec, "Callback provides the correct url");
+ Assert.equal(page.guid, guid, "Callback provides the correct guid");
+ Assert.equal(page.title, title, "Callback provides the correct title");
+ });
+ Assert.ok(onRowCalled, "Callback has been called");
+ } else {
+ removed = yield PlacesUtils.history.remove(removeArg);
+ }
+
+ yield promiseObserved;
+ PlacesUtils.history.removeObserver(observer);
+
+ Assert.equal(visits_in_database(uri), 0, "History entry has disappeared");
+ Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits");
+ Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here");
+ if (shouldRemove) {
+ Assert.ok(removed, "Something was removed");
+ Assert.equal(page_in_database(uri), 0, "Page has disappeared");
+ } else {
+ Assert.ok(!removed, "The page was not removed, as there was a bookmark");
+ Assert.notEqual(page_in_database(uri), 0, "The page is still present");
+ }
+ });
+
+ try {
+ for (let useCallback of [false, true]) {
+ for (let addBookmark of [false, true]) {
+ let options = { useCallback: useCallback, addBookmark: addBookmark };
+ yield remover("Testing History.remove() with a single URI", x => x, options);
+ yield remover("Testing History.remove() with a single string url", x => x.spec, options);
+ yield remover("Testing History.remove() with a single string guid", x => do_get_guid_for_uri(x), options);
+ yield remover("Testing History.remove() with a single URI in an array", x => [x], options);
+ yield remover("Testing History.remove() with a single string url in an array", x => [x.spec], options);
+ yield remover("Testing History.remove() with a single string guid in an array", x => [do_get_guid_for_uri(x)], options);
+ }
+ }
+ } finally {
+ yield PlacesTestUtils.clearHistory();
+ }
+ return;
+});
+
+// Test removing a list of pages
+add_task(function* test_remove_many() {
+ const SIZE = 10;
+
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ do_print("Adding a witness page");
+ let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
+ yield PlacesTestUtils.addVisits(WITNESS_URI);
+ Assert.ok(page_in_database(WITNESS_URI), "Witness page added");
+
+ do_print("Generating samples");
+ let pages = [];
+ for (let i = 0; i < SIZE; ++i) {
+ let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove?sample=" + i + "&salt=" + Math.random());
+ let title = "Visit " + i + ", " + Math.random();
+ let hasBookmark = i % 3 == 0;
+ let page = {
+ uri: uri,
+ title: title,
+ hasBookmark: hasBookmark,
+ // `true` once `onResult` has been called for this page
+ onResultCalled: false,
+ // `true` once `onDeleteVisits` has been called for this page
+ onDeleteVisitsCalled: false,
+ // `true` once `onFrecencyChangedCalled` has been called for this page
+ onFrecencyChangedCalled: false,
+ // `true` once `onDeleteURI` has been called for this page
+ onDeleteURICalled: false,
+ };
+ do_print("Pushing: " + uri.spec);
+ pages.push(page);
+
+ yield PlacesTestUtils.addVisits(page);
+ page.guid = do_get_guid_for_uri(uri);
+ if (hasBookmark) {
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test bookmark " + i);
+ }
+ Assert.ok(page_in_database(uri), "Page added");
+ }
+
+ do_print("Mixing key types and introducing dangling keys");
+ let keys = [];
+ for (let i = 0; i < SIZE; ++i) {
+ if (i % 4 == 0) {
+ keys.push(pages[i].uri);
+ keys.push(NetUtil.newURI("http://example.org/dangling/nsIURI/" + i));
+ } else if (i % 4 == 1) {
+ keys.push(new URL(pages[i].uri.spec));
+ keys.push(new URL("http://example.org/dangling/URL/" + i));
+ } else if (i % 4 == 2) {
+ keys.push(pages[i].uri.spec);
+ keys.push("http://example.org/dangling/stringuri/" + i);
+ } else {
+ keys.push(pages[i].guid);
+ keys.push(("guid_" + i + "_01234567890").substr(0, 12));
+ }
+ }
+
+ let observer = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(aURI) {
+ Assert.ok(false, "Unexpected call to onVisit " + aURI.spec);
+ },
+ onTitleChanged: function(aURI) {
+ Assert.ok(false, "Unexpected call to onTitleChanged " + aURI.spec);
+ },
+ onClearHistory: function() {
+ Assert.ok(false, "Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(aURI) {
+ Assert.ok(false, "Unexpected call to onPageChanged " + aURI.spec);
+ },
+ onFrecencyChanged: function(aURI) {
+ let origin = pages.find(x => x.uri.spec == aURI.spec);
+ Assert.ok(origin);
+ Assert.ok(origin.hasBookmark, "Observing onFrecencyChanged on a page with a bookmark");
+ origin.onFrecencyChangedCalled = true;
+ // We do not make sure that `origin.onFrecencyChangedCalled` is `false`, as
+ },
+ onManyFrecenciesChanged: function() {
+ Assert.ok(false, "Observing onManyFrecenciesChanges, this is most likely correct but not covered by this test");
+ },
+ onDeleteURI: function(aURI) {
+ let origin = pages.find(x => x.uri.spec == aURI.spec);
+ Assert.ok(origin);
+ Assert.ok(!origin.hasBookmark, "Observing onDeleteURI on a page without a bookmark");
+ Assert.ok(!origin.onDeleteURICalled, "Observing onDeleteURI for the first time");
+ origin.onDeleteURICalled = true;
+ },
+ onDeleteVisits: function(aURI) {
+ let origin = pages.find(x => x.uri.spec == aURI.spec);
+ Assert.ok(origin);
+ Assert.ok(!origin.onDeleteVisitsCalled, "Observing onDeleteVisits for the first time");
+ origin.onDeleteVisitsCalled = true;
+ }
+ };
+ PlacesUtils.history.addObserver(observer, false);
+
+ do_print("Removing the pages and checking the callbacks");
+ let removed = yield PlacesUtils.history.remove(keys, page => {
+ let origin = pages.find(candidate => candidate.uri.spec == page.url.href);
+
+ Assert.ok(origin, "onResult has a valid page");
+ Assert.ok(!origin.onResultCalled, "onResult has not seen this page yet");
+ origin.onResultCalled = true;
+ Assert.equal(page.guid, origin.guid, "onResult has the right guid");
+ Assert.equal(page.title, origin.title, "onResult has the right title");
+ });
+ Assert.ok(removed, "Something was removed");
+
+ PlacesUtils.history.removeObserver(observer);
+
+ do_print("Checking out results");
+ // By now the observers should have been called.
+ for (let i = 0; i < pages.length; ++i) {
+ let page = pages[i];
+ do_print("Page: " + i);
+ Assert.ok(page.onResultCalled, "We have reached the page from the callback");
+ Assert.ok(visits_in_database(page.uri) == 0, "History entry has disappeared");
+ Assert.equal(page_in_database(page.uri) != 0, page.hasBookmark, "Page is present only if it also has bookmarks");
+ Assert.equal(page.onFrecencyChangedCalled, page.onDeleteVisitsCalled, "onDeleteVisits was called iff onFrecencyChanged was called");
+ Assert.ok(page.onFrecencyChangedCalled ^ page.onDeleteURICalled, "Either onFrecencyChanged or onDeleteURI was called");
+ }
+
+ Assert.notEqual(visits_in_database(WITNESS_URI), 0, "Witness URI still has visits");
+ Assert.notEqual(page_in_database(WITNESS_URI), 0, "Witness URI is still here");
+});
+
+add_task(function* cleanup() {
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+// Test the various error cases
+add_task(function* test_error_cases() {
+ Assert.throws(
+ () => PlacesUtils.history.remove(),
+ /TypeError: Invalid url/,
+ "History.remove with no argument should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(null),
+ /TypeError: Invalid url/,
+ "History.remove with `null` should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(undefined),
+ /TypeError: Invalid url/,
+ "History.remove with `undefined` should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove("not a guid, obviously"),
+ /TypeError: .* is not a valid URL/,
+ "History.remove with an ill-formed guid/url argument should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove({"not the kind of object we know how to handle": true}),
+ /TypeError: Invalid url/,
+ "History.remove with an unexpected object should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove([]),
+ /TypeError: Expected at least one page/,
+ "History.remove with an empty array should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove([null]),
+ /TypeError: Invalid url or guid/,
+ "History.remove with an array containing null should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(["http://example.org", "not a guid, obviously"]),
+ /TypeError: .* is not a valid URL/,
+ "History.remove with an array containing an ill-formed guid/url argument should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(["0123456789ab"/* valid guid*/, null]),
+ /TypeError: Invalid url or guid: null/,
+ "History.remove with an array containing a guid and a second argument that is null should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove(["http://example.org", {"not the kind of object we know how to handle": true}]),
+ /TypeError: Invalid url/,
+ "History.remove with an array containing an unexpected objecgt should throw a TypeError"
+ );
+ Assert.throws(
+ () => PlacesUtils.history.remove("http://example.org", "not a function, obviously"),
+ /TypeError: Invalid function/,
+ "History.remove with a second argument that is not a function argument should throw a TypeError"
+ );
+ try {
+ PlacesUtils.history.remove("http://example.org/I/have/clearly/not/been/added", null);
+ Assert.ok(true, "History.remove should ignore `null` as a second argument");
+ } catch (ex) {
+ Assert.ok(false, "History.remove should ignore `null` as a second argument");
+ }
+});
+
+add_task(function* test_orphans() {
+ let uri = NetUtil.newURI("http://moz.org/");
+ yield PlacesTestUtils.addVisits({ uri });
+
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ uri, SMALLPNG_DATA_URI, true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+ PlacesUtils.annotations.setPageAnnotation(uri, "test", "restval", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ yield PlacesUtils.history.remove(uri);
+ Assert.ok(!(yield PlacesTestUtils.isPageInDB(uri)), "Page should have been removed");
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT (SELECT count(*) FROM moz_annos) +
+ (SELECT count(*) FROM moz_favicons) AS count`);
+ Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans");
+});
diff --git a/toolkit/components/places/tests/history/test_removeVisits.js b/toolkit/components/places/tests/history/test_removeVisits.js
new file mode 100644
index 0000000000..8df0c81a96
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_removeVisits.js
@@ -0,0 +1,316 @@
+const JS_NOW = Date.now();
+const DB_NOW = JS_NOW * 1000;
+const TEST_URI = uri("http://example.com/");
+const PLACE_URI = uri("place:queryType=0&sort=8&maxResults=10");
+
+function* cleanup() {
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+ // This is needed to remove place: entries.
+ DBConn().executeSimpleSQL("DELETE FROM moz_places");
+}
+
+add_task(function* remove_visits_outside_unbookmarked_uri() {
+ do_print("*** TEST: Remove some visits outside valid timeframe from an unbookmarked URI");
+
+ do_print("Add 10 visits for the URI from way in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - 100000 - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Remove visits using timerange outside the URI's visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that all visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 10);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - 100000 - (i * 1000));
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_visits_outside_bookmarked_uri() {
+ do_print("*** TEST: Remove some visits outside valid timeframe from a bookmarked URI");
+
+ do_print("Add 10 visits for the URI from way in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - 100000 - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ do_print("Bookmark the URI.");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Remove visits using timerange outside the URI's visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that all visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 10);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - 100000 - (i * 1000));
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_visits_unbookmarked_uri() {
+ do_print("*** TEST: Remove some visits from an unbookmarked URI");
+
+ do_print("Add 10 visits for the URI from now to 9 usecs in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Remove the 5 most recent visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 4),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that only the older 5 visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 5);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - (i * 1000) - 5000);
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_visits_bookmarked_uri() {
+ do_print("*** TEST: Remove some visits from a bookmarked URI");
+
+ do_print("Add 10 visits for the URI from now to 9 usecs in the past.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ do_print("Bookmark the URI.");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Remove the 5 most recent visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 4),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that only the older 5 visits still exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 5);
+ for (let i = 0; i < root.childCount; i++) {
+ let visitTime = root.getChild(i).time;
+ do_check_eq(visitTime, DB_NOW - (i * 1000) - 5000);
+ }
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return true.");
+ do_check_true(yield promiseIsURIVisited(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates()
+
+ do_print("Frecency should be positive.")
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ yield cleanup();
+});
+
+add_task(function* remove_all_visits_unbookmarked_uri() {
+ do_print("*** TEST: Remove all visits from an unbookmarked URI");
+
+ do_print("Add some visits for the URI.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Remove all visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should no longer exist in moz_places.");
+ do_check_false(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that no visits exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return false.");
+ do_check_false(yield promiseIsURIVisited(TEST_URI));
+
+ yield cleanup();
+});
+
+add_task(function* remove_all_visits_bookmarked_uri() {
+ do_print("*** TEST: Remove all visits from a bookmarked URI");
+
+ do_print("Add some visits for the URI.");
+ let visits = [];
+ for (let i = 0; i < 10; i++) {
+ visits.push({ uri: TEST_URI, visitDate: DB_NOW - (i * 1000) });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ do_print("Bookmark the URI.");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ let initialFrecency = frecencyForUrl(TEST_URI);
+
+ do_print("Remove all visits.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+
+ do_print("Run a history query and check that no visits exist.");
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let root = PlacesUtils.history.executeQuery(query, opts).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ do_print("asyncHistory.isURIVisited should return false.");
+ do_check_false(yield promiseIsURIVisited(TEST_URI));
+
+ do_print("nsINavBookmarksService.isBookmarked should return true.");
+ do_check_true(PlacesUtils.bookmarks.isBookmarked(TEST_URI));
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Frecency should be smaller.")
+ do_check_true(frecencyForUrl(TEST_URI) < initialFrecency);
+
+ yield cleanup();
+});
+
+add_task(function* remove_all_visits_bookmarked_uri() {
+ do_print("*** TEST: Remove some visits from a zero frecency URI retains zero frecency");
+
+ do_print("Add some visits for the URI.");
+ yield PlacesTestUtils.addVisits([
+ { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: (DB_NOW - 86400000000000) },
+ { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: DB_NOW }
+ ]);
+
+ do_print("Remove newer visit.");
+ let filter = {
+ beginDate: new Date(JS_NOW - 10),
+ endDate: new Date(JS_NOW)
+ };
+ yield PlacesUtils.history.removeVisitsByFilter(filter);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI should still exist in moz_places.");
+ do_check_true(page_in_database(TEST_URI.spec));
+ do_print("Frecency should be zero.")
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/history/test_removeVisitsByFilter.js b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js
new file mode 100644
index 0000000000..699420e432
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_removeVisitsByFilter.js
@@ -0,0 +1,345 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+// Tests for `History.removeVisitsByFilter`, as implemented in History.jsm
+
+"use strict";
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+
+add_task(function* test_removeVisitsByFilter() {
+ let referenceDate = new Date(1999, 9, 9, 9, 9);
+
+ // Populate a database with 20 entries, remove a subset of entries,
+ // ensure consistency.
+ let remover = Task.async(function*(options) {
+ do_print("Remover with options " + JSON.stringify(options));
+ let SAMPLE_SIZE = options.sampleSize;
+
+ yield PlacesTestUtils.clearHistory();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // Populate the database.
+ // Create `SAMPLE_SIZE` visits, from the oldest to the newest.
+
+ let bookmarkIndices = new Set(options.bookmarks);
+ let visits = [];
+ let frecencyChangePromises = new Map();
+ let uriDeletePromises = new Map();
+ let getURL = options.url ?
+ i => "http://mozilla.com/test_browserhistory/test_removeVisitsByFilter/removeme/byurl/" + Math.floor(i / (SAMPLE_SIZE / 5)) + "/" :
+ i => "http://mozilla.com/test_browserhistory/test_removeVisitsByFilter/removeme/" + i + "/" + Math.random();
+ for (let i = 0; i < SAMPLE_SIZE; ++i) {
+ let spec = getURL(i);
+ let uri = NetUtil.newURI(spec);
+ let jsDate = new Date(Number(referenceDate) + 3600 * 1000 * i);
+ let dbDate = jsDate * 1000;
+ let hasBookmark = bookmarkIndices.has(i);
+ let hasOwnBookmark = hasBookmark;
+ if (!hasOwnBookmark && options.url) {
+ // Also mark as bookmarked if one of the earlier bookmarked items has the same URL.
+ hasBookmark =
+ options.bookmarks.filter(n => n < i).some(n => visits[n].uri.spec == spec && visits[n].test.hasBookmark);
+ }
+ do_print("Generating " + uri.spec + ", " + dbDate);
+ let visit = {
+ uri,
+ title: "visit " + i,
+ visitDate: dbDate,
+ test: {
+ // `visitDate`, as a Date
+ jsDate: jsDate,
+ // `true` if we expect that the visit will be removed
+ toRemove: false,
+ // `true` if `onRow` informed of the removal of this visit
+ announcedByOnRow: false,
+ // `true` if there is a bookmark for this URI, i.e. of the page
+ // should not be entirely removed.
+ hasBookmark: hasBookmark,
+ onFrecencyChanged: null,
+ onDeleteURI: null,
+ },
+ };
+ visits.push(visit);
+ if (hasOwnBookmark) {
+ do_print("Adding a bookmark to visit " + i);
+ yield PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test bookmark"
+ });
+ do_print("Bookmark added");
+ }
+ }
+
+ do_print("Adding visits");
+ yield PlacesTestUtils.addVisits(visits);
+
+ do_print("Preparing filters");
+ let filter = {
+ };
+ let beginIndex = 0;
+ let endIndex = visits.length - 1;
+ if ("begin" in options) {
+ let ms = Number(visits[options.begin].test.jsDate) - 1000;
+ filter.beginDate = new Date(ms);
+ beginIndex = options.begin;
+ }
+ if ("end" in options) {
+ let ms = Number(visits[options.end].test.jsDate) + 1000;
+ filter.endDate = new Date(ms);
+ endIndex = options.end;
+ }
+ if ("limit" in options) {
+ endIndex = beginIndex + options.limit - 1; // -1 because the start index is inclusive.
+ filter.limit = options.limit;
+ }
+ let removedItems = visits.slice(beginIndex);
+ endIndex -= beginIndex;
+ if (options.url) {
+ let rawURL = "";
+ switch (options.url) {
+ case 1:
+ filter.url = new URL(removedItems[0].uri.spec);
+ rawURL = filter.url.href;
+ break;
+ case 2:
+ filter.url = removedItems[0].uri;
+ rawURL = filter.url.spec;
+ break;
+ case 3:
+ filter.url = removedItems[0].uri.spec;
+ rawURL = filter.url;
+ break;
+ }
+ endIndex = Math.min(endIndex, removedItems.findIndex((v, index) => v.uri.spec != rawURL) - 1);
+ }
+ removedItems.splice(endIndex + 1);
+ let remainingItems = visits.filter(v => !removedItems.includes(v));
+ for (let i = 0; i < removedItems.length; i++) {
+ let test = removedItems[i].test;
+ do_print("Marking visit " + (beginIndex + i) + " as expecting removal");
+ test.toRemove = true;
+ if (test.hasBookmark ||
+ (options.url && remainingItems.some(v => v.uri.spec == removedItems[i].uri.spec))) {
+ frecencyChangePromises.set(removedItems[i].uri.spec, PromiseUtils.defer());
+ } else if (!options.url || i == 0) {
+ uriDeletePromises.set(removedItems[i].uri.spec, PromiseUtils.defer());
+ }
+ }
+
+ let observer = {
+ deferred: PromiseUtils.defer(),
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(uri) {
+ this.deferred.reject(new Error("Unexpected call to onVisit " + uri.spec));
+ },
+ onTitleChanged: function(uri) {
+ this.deferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
+ },
+ onClearHistory: function() {
+ this.deferred.reject("Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(uri) {
+ this.deferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
+ },
+ onFrecencyChanged: function(aURI) {
+ do_print("onFrecencyChanged " + aURI.spec);
+ let deferred = frecencyChangePromises.get(aURI.spec);
+ Assert.ok(!!deferred, "Observing onFrecencyChanged");
+ deferred.resolve();
+ },
+ onManyFrecenciesChanged: function() {
+ do_print("Many frecencies changed");
+ for (let [, deferred] of frecencyChangePromises) {
+ deferred.resolve();
+ }
+ },
+ onDeleteURI: function(aURI) {
+ do_print("onDeleteURI " + aURI.spec);
+ let deferred = uriDeletePromises.get(aURI.spec);
+ Assert.ok(!!deferred, "Observing onDeleteURI");
+ deferred.resolve();
+ },
+ onDeleteVisits: function(aURI) {
+ // Not sure we can test anything.
+ }
+ };
+ PlacesUtils.history.addObserver(observer, false);
+
+ let cbarg;
+ if (options.useCallback) {
+ do_print("Setting up callback");
+ cbarg = [info => {
+ for (let visit of visits) {
+ do_print("Comparing " + info.date + " and " + visit.test.jsDate);
+ if (Math.abs(visit.test.jsDate - info.date) < 100) { // Assume rounding errors
+ Assert.ok(!visit.test.announcedByOnRow,
+ "This is the first time we announce the removal of this visit");
+ Assert.ok(visit.test.toRemove,
+ "This is a visit we intended to remove");
+ visit.test.announcedByOnRow = true;
+ return;
+ }
+ }
+ Assert.ok(false, "Could not find the visit we attempt to remove");
+ }];
+ } else {
+ do_print("No callback");
+ cbarg = [];
+ }
+ let result = yield PlacesUtils.history.removeVisitsByFilter(filter, ...cbarg);
+
+ Assert.ok(result, "Removal succeeded");
+
+ // Make sure that we have eliminated exactly the entries we expected
+ // to eliminate.
+ for (let i = 0; i < visits.length; ++i) {
+ let visit = visits[i];
+ do_print("Controlling the results on visit " + i);
+ let remainingVisitsForURI = remainingItems.filter(v => visit.uri.spec == v.uri.spec).length;
+ Assert.equal(
+ visits_in_database(visit.uri),
+ remainingVisitsForURI,
+ "Visit is still present iff expected");
+ if (options.useCallback) {
+ Assert.equal(
+ visit.test.toRemove,
+ visit.test.announcedByOnRow,
+ "Visit removal has been announced by onResult iff expected");
+ }
+ if (visit.test.hasBookmark || remainingVisitsForURI) {
+ Assert.notEqual(page_in_database(visit.uri), 0, "The page should still appear in the db");
+ } else {
+ Assert.equal(page_in_database(visit.uri), 0, "The page should have been removed from the db");
+ }
+ }
+
+ // Make sure that the observer has been called wherever applicable.
+ do_print("Checking URI delete promises.");
+ yield Promise.all(Array.from(uriDeletePromises.values()));
+ do_print("Checking frecency change promises.");
+ yield Promise.all(Array.from(frecencyChangePromises.values()));
+ PlacesUtils.history.removeObserver(observer);
+ });
+
+ let size = 20;
+ for (let range of [
+ {begin: 0},
+ {end: 19},
+ {begin: 0, end: 10},
+ {begin: 3, end: 4},
+ {begin: 5, end: 8, limit: 2},
+ {begin: 10, end: 18, limit: 5},
+ ]) {
+ for (let bookmarks of [[], [5, 6]]) {
+ let options = {
+ sampleSize: size,
+ bookmarks: bookmarks,
+ };
+ if ("begin" in range) {
+ options.begin = range.begin;
+ }
+ if ("end" in range) {
+ options.end = range.end;
+ }
+ if ("limit" in range) {
+ options.limit = range.limit;
+ }
+ yield remover(options);
+ options.url = 1;
+ yield remover(options);
+ options.url = 2;
+ yield remover(options);
+ options.url = 3;
+ yield remover(options);
+ }
+ }
+ yield PlacesTestUtils.clearHistory();
+});
+
+// Test the various error cases
+add_task(function* test_error_cases() {
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter(),
+ /TypeError: Expected a filter/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter("obviously, not a filter"),
+ /TypeError: Expected a filter/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({}),
+ /TypeError: Expected a non-empty filter/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: "now"}),
+ /TypeError: Expected a Date/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: Date.now()}),
+ /TypeError: Expected a Date/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date()}, "obviously, not a callback"),
+ /TypeError: Invalid function/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
+ /TypeError: `beginDate` should be at least as old/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: {}}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: -1}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: 0.1}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({limit: Infinity}),
+ /Expected a non-zero positive integer as a limit/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({url: {}}),
+ /Expected a valid URL for `url`/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({url: 0}),
+ /Expected a valid URL for `url`/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
+ /TypeError: `beginDate` should be at least as old/
+ );
+ Assert.throws(
+ () => PlacesUtils.history.removeVisitsByFilter({beginDate: new Date(1000), endDate: new Date(0)}),
+ /TypeError: `beginDate` should be at least as old/
+ );
+});
+
+add_task(function* test_orphans() {
+ let uri = NetUtil.newURI("http://moz.org/");
+ yield PlacesTestUtils.addVisits({ uri });
+
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ uri, SMALLPNG_DATA_URI, true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null, Services.scriptSecurityManager.getSystemPrincipal());
+ PlacesUtils.annotations.setPageAnnotation(uri, "test", "restval", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ yield PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(1999, 9, 9, 9, 9),
+ endDate: new Date() });
+ Assert.ok(!(yield PlacesTestUtils.isPageInDB(uri)), "Page should have been removed");
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT (SELECT count(*) FROM moz_annos) +
+ (SELECT count(*) FROM moz_favicons) AS count`);
+ Assert.equal(rows[0].getResultByName("count"), 0, "Should not find orphans");
+});
diff --git a/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js
new file mode 100644
index 0000000000..832df9d9a3
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js
@@ -0,0 +1,52 @@
+// Test that repeated additions of the same URI through updatePlaces, properly
+// update from_visit and notify titleChanged.
+
+add_task(function* test() {
+ let uri = "http://test.com/";
+
+ let promiseTitleChangedNotifications = new Promise(resolve => {
+ let historyObserver = {
+ _count: 0,
+ __proto__: NavHistoryObserver.prototype,
+ onTitleChanged(aURI, aTitle, aGUID) {
+ Assert.equal(aURI.spec, uri, "Should notify the proper url");
+ if (++this._count == 2) {
+ PlacesUtils.history.removeObserver(historyObserver);
+ resolve();
+ }
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+ });
+
+ // This repeats the url on purpose, don't merge it into a single place entry.
+ yield PlacesTestUtils.addVisits([
+ { uri, title: "test" },
+ { uri, referrer: uri, title: "test2" },
+ ]);
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.uri = NetUtil.newURI(uri);
+ options.resultType = options.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(root.childCount, 2);
+
+ let child = root.getChild(0);
+ Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+ Assert.equal(child.visitId, 1, "Visit ID should be 1");
+ Assert.equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+ Assert.equal(child.title, "test2", "Should have the correct title");
+
+ child = root.getChild(1);
+ Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+ Assert.equal(child.visitId, 2, "Visit ID should be 2");
+ Assert.equal(child.fromVisitId, 1, "First visit should be the referring visit");
+ Assert.equal(child.title, "test2", "Should have the correct title");
+
+ root.containerOpen = false;
+
+ yield promiseTitleChangedNotifications;
+});
diff --git a/toolkit/components/places/tests/history/xpcshell.ini b/toolkit/components/places/tests/history/xpcshell.ini
new file mode 100644
index 0000000000..ee182e0905
--- /dev/null
+++ b/toolkit/components/places/tests/history/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head = head_history.js
+tail =
+
+[test_insert.js]
+[test_remove.js]
+[test_removeVisits.js]
+[test_removeVisitsByFilter.js]
+[test_updatePlaces_sameUri_titleChanged.js]
diff --git a/toolkit/components/places/tests/migration/.eslintrc.js b/toolkit/components/places/tests/migration/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/migration/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/migration/head_migration.js b/toolkit/components/places/tests/migration/head_migration.js
new file mode 100644
index 0000000000..1ebecd4c0f
--- /dev/null
+++ b/toolkit/components/places/tests/migration/head_migration.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict"
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.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.
+
+const DB_FILENAME = "places.sqlite";
+
+/**
+ * Sets the database to use for the given test. This should be the very first
+ * thing in the test, otherwise this database will not be used!
+ *
+ * @param aFileName
+ * The filename of the database to use. This database must exist in
+ * toolkit/components/places/tests/migration!
+ * @return {Promise}
+ */
+var setupPlacesDatabase = Task.async(function* (aFileName) {
+ let currentDir = yield OS.File.getCurrentDirectory();
+
+ let src = OS.Path.join(currentDir, aFileName);
+ Assert.ok((yield OS.File.exists(src)), "Database file found");
+
+ // Ensure that our database doesn't already exist.
+ let dest = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ Assert.ok(!(yield OS.File.exists(dest)), "Database file should not exist yet");
+
+ yield OS.File.copy(src, dest);
+});
+
+// This works provided all tests in this folder use add_task.
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/migration/places_v10.sqlite b/toolkit/components/places/tests/migration/places_v10.sqlite
new file mode 100644
index 0000000000..80a8ecd6ad
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v10.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v11.sqlite b/toolkit/components/places/tests/migration/places_v11.sqlite
new file mode 100644
index 0000000000..bef27d5f59
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v11.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v17.sqlite b/toolkit/components/places/tests/migration/places_v17.sqlite
new file mode 100644
index 0000000000..5183cde83d
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v17.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v19.sqlite b/toolkit/components/places/tests/migration/places_v19.sqlite
new file mode 100644
index 0000000000..11e2e6247b
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v19.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v21.sqlite b/toolkit/components/places/tests/migration/places_v21.sqlite
new file mode 100644
index 0000000000..f729308261
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v21.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v22.sqlite b/toolkit/components/places/tests/migration/places_v22.sqlite
new file mode 100644
index 0000000000..30bf840b0f
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v22.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v23.sqlite b/toolkit/components/places/tests/migration/places_v23.sqlite
new file mode 100644
index 0000000000..b519b97d27
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v23.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v24.sqlite b/toolkit/components/places/tests/migration/places_v24.sqlite
new file mode 100644
index 0000000000..b35f958a66
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v24.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v25.sqlite b/toolkit/components/places/tests/migration/places_v25.sqlite
new file mode 100644
index 0000000000..2afd1da1fd
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v25.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v26.sqlite b/toolkit/components/places/tests/migration/places_v26.sqlite
new file mode 100644
index 0000000000..b4b2381797
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v26.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v27.sqlite b/toolkit/components/places/tests/migration/places_v27.sqlite
new file mode 100644
index 0000000000..57dfb75627
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v27.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v28.sqlite b/toolkit/components/places/tests/migration/places_v28.sqlite
new file mode 100644
index 0000000000..9a27db3240
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v28.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v29.sqlite b/toolkit/components/places/tests/migration/places_v29.sqlite
new file mode 100644
index 0000000000..f6de0fe8a4
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v29.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v30.sqlite b/toolkit/components/places/tests/migration/places_v30.sqlite
new file mode 100644
index 0000000000..9cbabe005b
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v30.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v31.sqlite b/toolkit/components/places/tests/migration/places_v31.sqlite
new file mode 100644
index 0000000000..9d33b9effc
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v31.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v32.sqlite b/toolkit/components/places/tests/migration/places_v32.sqlite
new file mode 100644
index 0000000000..239f6c5fe3
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v32.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v33.sqlite b/toolkit/components/places/tests/migration/places_v33.sqlite
new file mode 100644
index 0000000000..6071dc6a68
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v33.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v34.sqlite b/toolkit/components/places/tests/migration/places_v34.sqlite
new file mode 100644
index 0000000000..474628996f
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v34.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v35.sqlite b/toolkit/components/places/tests/migration/places_v35.sqlite
new file mode 100644
index 0000000000..5e157d7780
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v35.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/places_v6.sqlite b/toolkit/components/places/tests/migration/places_v6.sqlite
new file mode 100644
index 0000000000..2852a4cf97
--- /dev/null
+++ b/toolkit/components/places/tests/migration/places_v6.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/migration/test_current_from_downgraded.js b/toolkit/components/places/tests/migration/test_current_from_downgraded.js
new file mode 100644
index 0000000000..6d36cab144
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_downgraded.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase(`places_v${CURRENT_SCHEMA_VERSION}.sqlite`);
+ // Downgrade the schema version to the first supported one.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path: path });
+ yield db.setSchemaVersion(FIRST_UPGRADABLE_SCHEMA_VERSION);
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v11.js b/toolkit/components/places/tests/migration/test_current_from_v11.js
new file mode 100644
index 0000000000..43b8fb1f64
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v11.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v11.sqlite");
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_moz_hosts() {
+ let db = yield PlacesUtils.promiseDBConnection();
+
+ // This will throw if the column does not exist.
+ yield db.execute("SELECT host, frecency, typed, prefix FROM moz_hosts");
+
+ // moz_hosts is populated asynchronously, so we need to wait.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // check the number of entries in moz_hosts equals the number of
+ // unique rev_host in moz_places
+ let rows = yield db.execute(
+ `SELECT (SELECT COUNT(host) FROM moz_hosts),
+ (SELECT COUNT(DISTINCT rev_host)
+ FROM moz_places
+ WHERE LENGTH(rev_host) > 1)
+ `);
+
+ Assert.equal(rows.length, 1);
+ let mozHostsCount = rows[0].getResultByIndex(0);
+ let mozPlacesCount = rows[0].getResultByIndex(1);
+
+ Assert.ok(mozPlacesCount > 0, "There is some url in the database");
+ Assert.equal(mozPlacesCount, mozHostsCount, "moz_hosts has the expected number of entries");
+});
+
+add_task(function* test_journal() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute("PRAGMA journal_mode");
+ Assert.equal(rows.length, 1);
+ // WAL journal mode should be set on this database.
+ Assert.equal(rows[0].getResultByIndex(0), "wal");
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v19.js b/toolkit/components/places/tests/migration/test_current_from_v19.js
new file mode 100644
index 0000000000..b8d837e681
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v19.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ANNO_LEGACYGUID = "placesInternal/GUID";
+
+var getTotalGuidAnnotationsCount = Task.async(function* (db) {
+ let rows = yield db.execute(
+ `SELECT count(*)
+ FROM moz_items_annos a
+ JOIN moz_anno_attributes b ON a.anno_attribute_id = b.id
+ WHERE b.name = :attr_name
+ `, { attr_name: ANNO_LEGACYGUID });
+ return rows[0].getResultByIndex(0);
+});
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v19.sqlite");
+});
+
+add_task(function* initial_state() {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path: path });
+
+ Assert.equal((yield getTotalGuidAnnotationsCount(db)), 1,
+ "There should be 1 obsolete guid annotation");
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_bookmark_guid_annotation_removed()
+{
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield getTotalGuidAnnotationsCount(db)), 0,
+ "There should be no more obsolete GUID annotations.");
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v24.js b/toolkit/components/places/tests/migration/test_current_from_v24.js
new file mode 100644
index 0000000000..0561b49229
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v24.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v24.sqlite");
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_bookmark_guid_annotation_removed()
+{
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ let m = new Map([
+ [PlacesUtils.placesRootId, PlacesUtils.bookmarks.rootGuid],
+ [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.menuGuid],
+ [PlacesUtils.toolbarFolderId, PlacesUtils.bookmarks.toolbarGuid],
+ [PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.bookmarks.unfiledGuid],
+ [PlacesUtils.tagsFolderId, PlacesUtils.bookmarks.tagsGuid],
+ [PlacesUtils.mobileFolderId, PlacesUtils.bookmarks.mobileGuid],
+ ]);
+
+ let rows = yield db.execute(`SELECT id, guid FROM moz_bookmarks`);
+ for (let row of rows) {
+ let id = row.getResultByName("id");
+ let guid = row.getResultByName("guid");
+ Assert.equal(m.get(id), guid, "The root folder has the correct GUID");
+ }
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v25.js b/toolkit/components/places/tests/migration/test_current_from_v25.js
new file mode 100644
index 0000000000..b066975fc5
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v25.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v25.sqlite");
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_dates_rounded() {
+ let root = yield PlacesUtils.promiseBookmarksTree();
+ function ensureDates(node) {
+ // When/if promiseBookmarksTree returns these as Date objects, switch this
+ // test to use getItemDateAdded and getItemLastModified. And when these
+ // methods are removed, this test can be eliminated altogether.
+ Assert.strictEqual(typeof(node.dateAdded), "number");
+ Assert.strictEqual(typeof(node.lastModified), "number");
+ Assert.strictEqual(node.dateAdded % 1000, 0);
+ Assert.strictEqual(node.lastModified % 1000, 0);
+ if ("children" in node)
+ node.children.forEach(ensureDates);
+ }
+ ensureDates(root);
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v26.js b/toolkit/components/places/tests/migration/test_current_from_v26.js
new file mode 100644
index 0000000000..7ff4bc3521
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v26.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v26.sqlite");
+ // Setup database contents to be migrated.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path });
+ // Add pages.
+ yield db.execute(`INSERT INTO moz_places (url, guid)
+ VALUES ("http://test1.com/", "test1_______")
+ , ("http://test2.com/", "test2_______")
+ , ("http://test3.com/", "test3_______")
+ `);
+ // Add keywords.
+ yield db.execute(`INSERT INTO moz_keywords (keyword)
+ VALUES ("kw1")
+ , ("kw2")
+ , ("kw3")
+ , ("kw4")
+ , ("kw5")
+ `);
+ // Add bookmarks.
+ let now = Date.now() * 1000;
+ let index = 0;
+ yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid)
+ VALUES (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark1___")
+ /* same uri, different keyword */
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark2___")
+ /* different uri, same keyword as 1 */
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark3___")
+ /* same uri, same keyword as 1 */
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark4___")
+ /* same uri, same keyword as 2 */
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark5___")
+ /* different uri, same keyword as 1 */
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw3'), "bookmark6___")
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test3_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw4'), "bookmark7___")
+ /* same uri and post_data as bookmark7, different keyword */
+ , (1, (SELECT id FROM moz_places WHERE guid = 'test3_______'), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = 'kw5'), "bookmark8___")
+ `);
+ // Add postData.
+ yield db.execute(`INSERT INTO moz_anno_attributes (name)
+ VALUES ("bookmarkProperties/POSTData")
+ , ("someOtherAnno")`);
+ yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content)
+ VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1")
+ , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")
+ , ((SELECT id FROM moz_anno_attributes where name = "someOtherAnno"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "zzzzzzzzzz")
+ , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark7___"), "postData3")
+ , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark8___"), "postData3")
+ `);
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_keywords() {
+ // When 2 urls have the same keyword, if one has postData it will be
+ // preferred.
+ let entry1 = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry1.url.href, "http://test2.com/");
+ Assert.equal(entry1.postData, "postData1");
+ let entry2 = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry2.url.href, "http://test2.com/");
+ Assert.equal(entry2.postData, "postData2");
+ let entry3 = yield PlacesUtils.keywords.fetch("kw3");
+ Assert.equal(entry3.url.href, "http://test1.com/");
+ Assert.equal(entry3.postData, null);
+ let entry4 = yield PlacesUtils.keywords.fetch("kw4");
+ Assert.equal(entry4, null);
+ let entry5 = yield PlacesUtils.keywords.fetch("kw5");
+ Assert.equal(entry5.url.href, "http://test3.com/");
+ Assert.equal(entry5.postData, "postData3");
+
+ Assert.equal((yield foreign_count("http://test1.com/")), 5); // 4 bookmark2 + 1 keywords
+ Assert.equal((yield foreign_count("http://test2.com/")), 4); // 2 bookmark2 + 2 keywords
+ Assert.equal((yield foreign_count("http://test3.com/")), 3); // 2 bookmark2 + 1 keywords
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v27.js b/toolkit/components/places/tests/migration/test_current_from_v27.js
new file mode 100644
index 0000000000..1675901eb7
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v27.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v27.sqlite");
+ // Setup database contents to be migrated.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path });
+ // Add pages.
+ yield db.execute(`INSERT INTO moz_places (url, guid)
+ VALUES ("http://test1.com/", "test1_______")
+ , ("http://test2.com/", "test2_______")
+ `);
+ // Add keywords.
+ yield db.execute(`INSERT INTO moz_keywords (keyword, place_id, post_data)
+ VALUES ("kw1", (SELECT id FROM moz_places WHERE guid = "test2_______"), "broken data")
+ , ("kw2", (SELECT id FROM moz_places WHERE guid = "test2_______"), NULL)
+ , ("kw3", (SELECT id FROM moz_places WHERE guid = "test1_______"), "zzzzzzzzzz")
+ `);
+ // Add bookmarks.
+ let now = Date.now() * 1000;
+ let index = 0;
+ yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid)
+ VALUES (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark1___")
+ /* same uri, different keyword */
+ , (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = "kw2"), "bookmark2___")
+ /* different uri, same keyword as 1 */
+ , (1, (SELECT id FROM moz_places WHERE guid = "test2_______"), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark3___")
+ /* same uri, same keyword as 1 */
+ , (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = "kw1"), "bookmark4___")
+ /* same uri, same keyword as 2 */
+ , (1, (SELECT id FROM moz_places WHERE guid = "test2_______"), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = "kw2"), "bookmark5___")
+ /* different uri, same keyword as 1 */
+ , (1, (SELECT id FROM moz_places WHERE guid = "test1_______"), 3, ${index++}, ${now}, ${now},
+ (SELECT id FROM moz_keywords WHERE keyword = "kw3"), "bookmark6___")
+ `);
+ // Add postData.
+ yield db.execute(`INSERT INTO moz_anno_attributes (name)
+ VALUES ("bookmarkProperties/POSTData")
+ , ("someOtherAnno")`);
+ yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content)
+ VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1")
+ , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")
+ , ((SELECT id FROM moz_anno_attributes where name = "someOtherAnno"),
+ (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "zzzzzzzzzz")
+ `);
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_keywords() {
+ // When 2 urls have the same keyword, if one has postData it will be
+ // preferred.
+ let entry1 = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry1.url.href, "http://test2.com/");
+ Assert.equal(entry1.postData, "postData1");
+ let entry2 = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry2.url.href, "http://test2.com/");
+ Assert.equal(entry2.postData, "postData2");
+ let entry3 = yield PlacesUtils.keywords.fetch("kw3");
+ Assert.equal(entry3.url.href, "http://test1.com/");
+ Assert.equal(entry3.postData, null);
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v31.js b/toolkit/components/places/tests/migration/test_current_from_v31.js
new file mode 100644
index 0000000000..6b9131daa3
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v31.js
@@ -0,0 +1,46 @@
+// Add pages.
+let shorturl = "http://example.com/" + "a".repeat(1981);
+let longurl = "http://example.com/" + "a".repeat(1982);
+let bmurl = "http://example.com/" + "a".repeat(1983);
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v31.sqlite");
+ // Setup database contents to be migrated.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path });
+
+ yield db.execute(`INSERT INTO moz_places (url, guid, foreign_count)
+ VALUES (:shorturl, "test1_______", 0)
+ , (:longurl, "test2_______", 0)
+ , (:bmurl, "test3_______", 1)
+ `, { shorturl, longurl, bmurl });
+ // Add visits.
+ yield db.execute(`INSERT INTO moz_historyvisits (place_id)
+ VALUES ((SELECT id FROM moz_places WHERE url = :shorturl))
+ , ((SELECT id FROM moz_places WHERE url = :longurl))
+ `, { shorturl, longurl });
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_longurls() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT 1 FROM moz_places where url = :longurl`,
+ { longurl });
+ Assert.equal(rows.length, 0, "Long url should have been removed");
+ rows = yield db.execute(`SELECT 1 FROM moz_places where url = :shorturl`,
+ { shorturl });
+ Assert.equal(rows.length, 1, "Short url should have been retained");
+ rows = yield db.execute(`SELECT 1 FROM moz_places where url = :bmurl`,
+ { bmurl });
+ Assert.equal(rows.length, 1, "Bookmarked url should have been retained");
+ rows = yield db.execute(`SELECT count(*) FROM moz_historyvisits`);
+ Assert.equal(rows.length, 1, "Orphan visists should have been removed");
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v34.js b/toolkit/components/places/tests/migration/test_current_from_v34.js
new file mode 100644
index 0000000000..115bcec679
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v34.js
@@ -0,0 +1,141 @@
+Cu.importGlobalProperties(["URL", "crypto"]);
+
+const { TYPE_BOOKMARK, TYPE_FOLDER } = Ci.nsINavBookmarksService;
+const { EXPIRE_NEVER, TYPE_INT32 } = Ci.nsIAnnotationService;
+
+function makeGuid() {
+ return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
+ pad: false,
+ });
+}
+
+// These queries are more or less copied directly from Bookmarks.jsm, but
+// operate on the old, pre-migration DB. We can't use any of the Places SQL
+// functions yet, because those are only registered for the main connection.
+function* insertItem(db, info) {
+ let [parentInfo] = yield db.execute(`
+ SELECT b.id, (SELECT count(*) FROM moz_bookmarks
+ WHERE parent = b.id) AS childCount
+ FROM moz_bookmarks b
+ WHERE b.guid = :parentGuid`,
+ { parentGuid: info.parentGuid });
+
+ let guid = makeGuid();
+ yield db.execute(`
+ INSERT INTO moz_bookmarks (fk, type, parent, position, guid)
+ VALUES ((SELECT id FROM moz_places WHERE url = :url),
+ :type, :parent, :position, :guid)`,
+ { url: info.url || "nonexistent", type: info.type, guid,
+ // Just append items.
+ position: parentInfo.getResultByName("childCount"),
+ parent: parentInfo.getResultByName("id") });
+
+ let id = (yield db.execute(`
+ SELECT id FROM moz_bookmarks WHERE guid = :guid LIMIT 1`,
+ { guid }))[0].getResultByName("id");
+
+ return { id, guid };
+}
+
+function insertBookmark(db, info) {
+ return db.executeTransaction(function* () {
+ if (info.type == TYPE_BOOKMARK) {
+ // We don't have access to the hash function here, so we omit the
+ // `url_hash` column. These will be fixed up automatically during
+ // migration.
+ let url = new URL(info.url);
+ let placeGuid = makeGuid();
+ yield db.execute(`
+ INSERT INTO moz_places (url, rev_host, hidden, frecency, guid)
+ VALUES (:url, :rev_host, 0, -1, :guid)`,
+ { url: url.href, guid: placeGuid,
+ rev_host: PlacesUtils.getReversedHost(url) });
+ }
+ return yield* insertItem(db, info);
+ });
+}
+
+function* insertAnno(db, itemId, name, value) {
+ yield db.execute(`INSERT OR IGNORE INTO moz_anno_attributes (name)
+ VALUES (:name)`, { name });
+ yield db.execute(`
+ INSERT INTO moz_items_annos
+ (item_id, anno_attribute_id, content, flags,
+ expiration, type, dateAdded, lastModified)
+ VALUES (:itemId,
+ (SELECT id FROM moz_anno_attributes
+ WHERE name = :name),
+ 1, 0, :expiration, :type, 0, 0)
+ `, { itemId, name, expiration: EXPIRE_NEVER, type: TYPE_INT32 });
+}
+
+function insertMobileFolder(db) {
+ return db.executeTransaction(function* () {
+ let item = yield* insertItem(db, {
+ type: TYPE_FOLDER,
+ parentGuid: "root________",
+ });
+ yield* insertAnno(db, item.id, "mobile/bookmarksRoot", 1);
+ return item;
+ });
+}
+
+var mobileId, mobileGuid, fxGuid;
+var dupeMobileId, dupeMobileGuid, tbGuid;
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v34.sqlite");
+ // Setup database contents to be migrated.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path });
+
+ do_print("Create mobile folder with bookmarks");
+ ({ id: mobileId, guid: mobileGuid } = yield insertMobileFolder(db));
+ ({ guid: fxGuid } = yield insertBookmark(db, {
+ type: TYPE_BOOKMARK,
+ url: "http://getfirefox.com",
+ parentGuid: mobileGuid,
+ }));
+
+ // We should only have one mobile folder, but, in case an old version of Sync
+ // did the wrong thing and created multiple mobile folders, we should merge
+ // their contents into the new mobile root.
+ do_print("Create second mobile folder with different bookmarks");
+ ({ id: dupeMobileId, guid: dupeMobileGuid } = yield insertMobileFolder(db));
+ ({ guid: tbGuid } = yield insertBookmark(db, {
+ type: TYPE_BOOKMARK,
+ url: "http://getthunderbird.com",
+ parentGuid: dupeMobileGuid,
+ }));
+
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ // Accessing the database for the first time triggers migration.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_mobile_root() {
+ let fxBmk = yield PlacesUtils.bookmarks.fetch(fxGuid);
+ equal(fxBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid,
+ "Firefox bookmark should be moved to new mobile root");
+ equal(fxBmk.index, 0, "Firefox bookmark should be first child of new root");
+
+ let tbBmk = yield PlacesUtils.bookmarks.fetch(tbGuid);
+ equal(tbBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid,
+ "Thunderbird bookmark should be moved to new mobile root");
+ equal(tbBmk.index, 1,
+ "Thunderbird bookmark should be second child of new root");
+
+ let mobileRootId = PlacesUtils.promiseItemId(
+ PlacesUtils.bookmarks.mobileGuid);
+ let annoItemIds = PlacesUtils.annotations.getItemsWithAnnotation(
+ PlacesUtils.MOBILE_ROOT_ANNO, {});
+ deepEqual(annoItemIds, [mobileRootId],
+ "Only mobile root should have mobile anno");
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js b/toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js
new file mode 100644
index 0000000000..871fe89932
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v34_no_roots.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v34.sqlite");
+ // Setup database contents to be migrated.
+ let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+ let db = yield Sqlite.openConnection({ path });
+ // Remove all the roots.
+ yield db.execute("DELETE FROM moz_bookmarks");
+ yield db.close();
+});
+
+add_task(function* database_is_valid() {
+ // Accessing the database for the first time triggers migration.
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
diff --git a/toolkit/components/places/tests/migration/test_current_from_v6.js b/toolkit/components/places/tests/migration/test_current_from_v6.js
new file mode 100644
index 0000000000..a3f9dc2291
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v6.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests migration from a preliminary schema version 6 that
+ * lacks frecency column and moz_inputhistory table.
+ */
+
+add_task(function* setup() {
+ yield setupPlacesDatabase("places_v6.sqlite");
+});
+
+add_task(function* corrupt_database_not_exists() {
+ let corruptPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "places.sqlite.corrupt");
+ Assert.ok(!(yield OS.File.exists(corruptPath)), "Corrupt file should not exist");
+});
+
+add_task(function* database_is_valid() {
+ Assert.equal(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* check_columns() {
+ // Check the database has been replaced, these would throw otherwise.
+ let db = yield PlacesUtils.promiseDBConnection();
+ yield db.execute("SELECT frecency from moz_places");
+ yield db.execute("SELECT 1 from moz_inputhistory");
+});
+
+add_task(function* corrupt_database_exists() {
+ let corruptPath = OS.Path.join(OS.Constants.Path.profileDir,
+ "places.sqlite.corrupt");
+ Assert.ok((yield OS.File.exists(corruptPath)), "Corrupt file should exist");
+});
diff --git a/toolkit/components/places/tests/migration/xpcshell.ini b/toolkit/components/places/tests/migration/xpcshell.ini
new file mode 100644
index 0000000000..aae0f75ee2
--- /dev/null
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -0,0 +1,36 @@
+[DEFAULT]
+head = head_migration.js
+tail =
+
+support-files =
+ places_v6.sqlite
+ places_v10.sqlite
+ places_v11.sqlite
+ places_v17.sqlite
+ places_v19.sqlite
+ places_v21.sqlite
+ places_v22.sqlite
+ places_v23.sqlite
+ places_v24.sqlite
+ places_v25.sqlite
+ places_v26.sqlite
+ places_v27.sqlite
+ places_v28.sqlite
+ places_v30.sqlite
+ places_v31.sqlite
+ places_v32.sqlite
+ places_v33.sqlite
+ places_v34.sqlite
+ places_v35.sqlite
+
+[test_current_from_downgraded.js]
+[test_current_from_v6.js]
+[test_current_from_v11.js]
+[test_current_from_v19.js]
+[test_current_from_v24.js]
+[test_current_from_v25.js]
+[test_current_from_v26.js]
+[test_current_from_v27.js]
+[test_current_from_v31.js]
+[test_current_from_v34.js]
+[test_current_from_v34_no_roots.js]
diff --git a/toolkit/components/places/tests/moz.build b/toolkit/components/places/tests/moz.build
new file mode 100644
index 0000000000..a40c0e93a2
--- /dev/null
+++ b/toolkit/components/places/tests/moz.build
@@ -0,0 +1,67 @@
+# -*- 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/.
+
+TEST_DIRS += ['cpp']
+
+TESTING_JS_MODULES += [
+ 'PlacesTestUtils.jsm',
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'bookmarks/xpcshell.ini',
+ 'expiration/xpcshell.ini',
+ 'favicons/xpcshell.ini',
+ 'history/xpcshell.ini',
+ 'migration/xpcshell.ini',
+ 'queries/xpcshell.ini',
+ 'unifiedcomplete/xpcshell.ini',
+ 'unit/xpcshell.ini',
+]
+
+BROWSER_CHROME_MANIFESTS += ['browser/browser.ini']
+MOCHITEST_CHROME_MANIFESTS += [
+ 'chrome/chrome.ini',
+]
+
+TEST_HARNESS_FILES.xpcshell.toolkit.components.places.tests += [
+ 'head_common.js',
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.toolkit.components.places.tests.browser += [
+ 'browser/399606-history.go-0.html',
+ 'browser/399606-httprefresh.html',
+ 'browser/399606-location.reload.html',
+ 'browser/399606-location.replace.html',
+ 'browser/399606-window.location.href.html',
+ 'browser/399606-window.location.html',
+ 'browser/461710_iframe.html',
+ 'browser/461710_link_page-2.html',
+ 'browser/461710_link_page-3.html',
+ 'browser/461710_link_page.html',
+ 'browser/461710_visited_page.html',
+ 'browser/begin.html',
+ 'browser/favicon-normal16.png',
+ 'browser/favicon-normal32.png',
+ 'browser/favicon.html',
+ 'browser/final.html',
+ 'browser/history_post.html',
+ 'browser/history_post.sjs',
+ 'browser/redirect-target.html',
+ 'browser/redirect.sjs',
+ 'browser/redirect_once.sjs',
+ 'browser/redirect_twice.sjs',
+ 'browser/title1.html',
+ 'browser/title2.html',
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.toolkit.components.places.tests.chrome += [
+ 'chrome/bad_links.atom',
+ 'chrome/link-less-items-no-site-uri.rss',
+ 'chrome/link-less-items.rss',
+ 'chrome/rss_as_html.rss',
+ 'chrome/rss_as_html.rss^headers^',
+ 'chrome/sample_feed.atom',
+]
diff --git a/toolkit/components/places/tests/queries/.eslintrc.js b/toolkit/components/places/tests/queries/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/queries/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/queries/head_queries.js b/toolkit/components/places/tests/queries/head_queries.js
new file mode 100644
index 0000000000..d37b3365fc
--- /dev/null
+++ b/toolkit/components/places/tests/queries/head_queries.js
@@ -0,0 +1,370 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.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.
+
+
+// Some Useful Date constants - PRTime uses microseconds, so convert
+const DAY_MICROSEC = 86400000000;
+const today = PlacesUtils.toPRTime(Date.now());
+const yesterday = today - DAY_MICROSEC;
+const lastweek = today - (DAY_MICROSEC * 7);
+const daybefore = today - (DAY_MICROSEC * 2);
+const old = today - (DAY_MICROSEC * 3);
+const futureday = today + (DAY_MICROSEC * 3);
+const olderthansixmonths = today - (DAY_MICROSEC * 31 * 7);
+
+
+/**
+ * Generalized function to pull in an array of objects of data and push it into
+ * the database. It does NOT do any checking to see that the input is
+ * appropriate. This function is an asynchronous task, it can be called using
+ * "Task.spawn" or using the "yield" function inside another task.
+ */
+function* task_populateDB(aArray)
+{
+ // Iterate over aArray and execute all instructions.
+ for (let arrayItem of aArray) {
+ try {
+ // make the data object into a query data object in order to create proper
+ // default values for anything left unspecified
+ var qdata = new queryData(arrayItem);
+ if (qdata.isVisit) {
+ // Then we should add a visit for this node
+ yield PlacesTestUtils.addVisits({
+ uri: uri(qdata.uri),
+ transition: qdata.transType,
+ visitDate: qdata.lastVisit,
+ referrer: qdata.referrer ? uri(qdata.referrer) : null,
+ title: qdata.title
+ });
+ if (qdata.visitCount && !qdata.isDetails) {
+ // Set a fake visit_count, this is not a real count but can be used
+ // to test sorting by visit_count.
+ let stmt = DBConn().createAsyncStatement(
+ "UPDATE moz_places SET visit_count = :vc WHERE url_hash = hash(:url) AND url = :url");
+ stmt.params.vc = qdata.visitCount;
+ stmt.params.url = qdata.uri;
+ try {
+ stmt.executeAsync();
+ }
+ catch (ex) {
+ print("Error while setting visit_count.");
+ }
+ finally {
+ stmt.finalize();
+ }
+ }
+ }
+
+ if (qdata.isRedirect) {
+ // This must be async to properly enqueue after the updateFrecency call
+ // done by the visit addition.
+ let stmt = DBConn().createAsyncStatement(
+ "UPDATE moz_places SET hidden = 1 WHERE url_hash = hash(:url) AND url = :url");
+ stmt.params.url = qdata.uri;
+ try {
+ stmt.executeAsync();
+ }
+ catch (ex) {
+ print("Error while setting hidden.");
+ }
+ finally {
+ stmt.finalize();
+ }
+ }
+
+ if (qdata.isDetails) {
+ // Then we add extraneous page details for testing
+ yield PlacesTestUtils.addVisits({
+ uri: uri(qdata.uri),
+ visitDate: qdata.lastVisit,
+ title: qdata.title
+ });
+ }
+
+ if (qdata.markPageAsTyped) {
+ PlacesUtils.history.markPageAsTyped(uri(qdata.uri));
+ }
+
+ if (qdata.isPageAnnotation) {
+ if (qdata.removeAnnotation)
+ PlacesUtils.annotations.removePageAnnotation(uri(qdata.uri),
+ qdata.annoName);
+ else {
+ PlacesUtils.annotations.setPageAnnotation(uri(qdata.uri),
+ qdata.annoName,
+ qdata.annoVal,
+ qdata.annoFlags,
+ qdata.annoExpiration);
+ }
+ }
+
+ if (qdata.isItemAnnotation) {
+ if (qdata.removeAnnotation)
+ PlacesUtils.annotations.removeItemAnnotation(qdata.itemId,
+ qdata.annoName);
+ else {
+ PlacesUtils.annotations.setItemAnnotation(qdata.itemId,
+ qdata.annoName,
+ qdata.annoVal,
+ qdata.annoFlags,
+ qdata.annoExpiration);
+ }
+ }
+
+ if (qdata.isFolder) {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: qdata.parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: qdata.title,
+ index: qdata.index
+ });
+ }
+
+ if (qdata.isLivemark) {
+ yield PlacesUtils.livemarks.addLivemark({ title: qdata.title
+ , parentId: (yield PlacesUtils.promiseItemId(qdata.parentGuid))
+ , index: qdata.index
+ , feedURI: uri(qdata.feedURI)
+ , siteURI: uri(qdata.uri)
+ });
+ }
+
+ if (qdata.isBookmark) {
+ let data = {
+ parentGuid: qdata.parentGuid,
+ index: qdata.index,
+ title: qdata.title,
+ url: qdata.uri
+ };
+
+ if (qdata.dateAdded) {
+ data.dateAdded = new Date(qdata.dateAdded / 1000);
+ }
+
+ if (qdata.lastModified) {
+ data.lastModified = new Date(qdata.lastModified / 1000);
+ }
+
+ yield PlacesUtils.bookmarks.insert(data);
+
+ if (qdata.keyword) {
+ yield PlacesUtils.keywords.insert({ url: qdata.uri,
+ keyword: qdata.keyword });
+ }
+ }
+
+ if (qdata.isTag) {
+ PlacesUtils.tagging.tagURI(uri(qdata.uri), qdata.tagArray);
+ }
+
+ if (qdata.isSeparator) {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: qdata.parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: qdata.index
+ });
+ }
+ } catch (ex) {
+ // use the arrayItem object here in case instantiation of qdata failed
+ do_print("Problem with this URI: " + arrayItem.uri);
+ do_throw("Error creating database: " + ex + "\n");
+ }
+ }
+}
+
+
+/**
+ * The Query Data Object - this object encapsulates data for our queries and is
+ * used to parameterize our calls to the Places APIs to put data into the
+ * database. It also has some interesting meta functions to determine which APIs
+ * should be called, and to determine if this object should show up in the
+ * resulting query.
+ * Its parameter is an object specifying which attributes you want to set.
+ * For ex:
+ * var myobj = new queryData({isVisit: true, uri:"http://mozilla.com", title="foo"});
+ * Note that it doesn't do any input checking on that object.
+ */
+function queryData(obj) {
+ this.isVisit = obj.isVisit ? obj.isVisit : false;
+ this.isBookmark = obj.isBookmark ? obj.isBookmark: false;
+ this.uri = obj.uri ? obj.uri : "";
+ this.lastVisit = obj.lastVisit ? obj.lastVisit : today;
+ this.referrer = obj.referrer ? obj.referrer : null;
+ this.transType = obj.transType ? obj.transType : Ci.nsINavHistoryService.TRANSITION_TYPED;
+ this.isRedirect = obj.isRedirect ? obj.isRedirect : false;
+ this.isDetails = obj.isDetails ? obj.isDetails : false;
+ this.title = obj.title ? obj.title : "";
+ this.markPageAsTyped = obj.markPageAsTyped ? obj.markPageAsTyped : false;
+ this.isPageAnnotation = obj.isPageAnnotation ? obj.isPageAnnotation : false;
+ this.removeAnnotation= obj.removeAnnotation ? true : false;
+ this.annoName = obj.annoName ? obj.annoName : "";
+ this.annoVal = obj.annoVal ? obj.annoVal : "";
+ this.annoFlags = obj.annoFlags ? obj.annoFlags : 0;
+ this.annoExpiration = obj.annoExpiration ? obj.annoExpiration : 0;
+ this.isItemAnnotation = obj.isItemAnnotation ? obj.isItemAnnotation : false;
+ this.itemId = obj.itemId ? obj.itemId : 0;
+ this.annoMimeType = obj.annoMimeType ? obj.annoMimeType : "";
+ this.isTag = obj.isTag ? obj.isTag : false;
+ this.tagArray = obj.tagArray ? obj.tagArray : null;
+ this.isLivemark = obj.isLivemark ? obj.isLivemark : false;
+ this.parentGuid = obj.parentGuid || PlacesUtils.bookmarks.rootGuid;
+ this.feedURI = obj.feedURI ? obj.feedURI : "";
+ this.index = obj.index ? obj.index : PlacesUtils.bookmarks.DEFAULT_INDEX;
+ this.isFolder = obj.isFolder ? obj.isFolder : false;
+ this.contractId = obj.contractId ? obj.contractId : "";
+ this.lastModified = obj.lastModified ? obj.lastModified : null;
+ this.dateAdded = obj.dateAdded ? obj.dateAdded : null;
+ this.keyword = obj.keyword ? obj.keyword : "";
+ this.visitCount = obj.visitCount ? obj.visitCount : 0;
+ this.isSeparator = obj.hasOwnProperty("isSeparator") && obj.isSeparator;
+
+ // And now, the attribute for whether or not this object should appear in the
+ // resulting query
+ this.isInQuery = obj.isInQuery ? obj.isInQuery : false;
+}
+
+// All attributes are set in the constructor above
+queryData.prototype = { }
+
+
+/**
+ * Helper function to compare an array of query objects with a result set.
+ * It assumes the array of query objects contains the SAME SORT as the result
+ * set. It checks the the uri, title, time, and bookmarkIndex properties of
+ * the results, where appropriate.
+ */
+function compareArrayToResult(aArray, aRoot) {
+ do_print("Comparing Array to Results");
+
+ var wasOpen = aRoot.containerOpen;
+ if (!wasOpen)
+ aRoot.containerOpen = true;
+
+ // check expected number of results against actual
+ var expectedResultCount = aArray.filter(function(aEl) { return aEl.isInQuery; }).length;
+ if (expectedResultCount != aRoot.childCount) {
+ // Debugging code for failures.
+ dump_table("moz_places");
+ dump_table("moz_historyvisits");
+ do_print("Found children:");
+ for (let i = 0; i < aRoot.childCount; i++) {
+ do_print(aRoot.getChild(i).uri);
+ }
+ do_print("Expected:");
+ for (let i = 0; i < aArray.length; i++) {
+ if (aArray[i].isInQuery)
+ do_print(aArray[i].uri);
+ }
+ }
+ do_check_eq(expectedResultCount, aRoot.childCount);
+
+ var inQueryIndex = 0;
+ for (var i = 0; i < aArray.length; i++) {
+ if (aArray[i].isInQuery) {
+ var child = aRoot.getChild(inQueryIndex);
+ // do_print("testing testData[" + i + "] vs result[" + inQueryIndex + "]");
+ if (!aArray[i].isFolder && !aArray[i].isSeparator) {
+ do_print("testing testData[" + aArray[i].uri + "] vs result[" + child.uri + "]");
+ if (aArray[i].uri != child.uri) {
+ dump_table("moz_places");
+ do_throw("Expected " + aArray[i].uri + " found " + child.uri);
+ }
+ }
+ if (!aArray[i].isSeparator && aArray[i].title != child.title)
+ do_throw("Expected " + aArray[i].title + " found " + child.title);
+ if (aArray[i].hasOwnProperty("lastVisit") &&
+ aArray[i].lastVisit != child.time)
+ do_throw("Expected " + aArray[i].lastVisit + " found " + child.time);
+ if (aArray[i].hasOwnProperty("index") &&
+ aArray[i].index != PlacesUtils.bookmarks.DEFAULT_INDEX &&
+ aArray[i].index != child.bookmarkIndex)
+ do_throw("Expected " + aArray[i].index + " found " + child.bookmarkIndex);
+
+ inQueryIndex++;
+ }
+ }
+
+ if (!wasOpen)
+ aRoot.containerOpen = false;
+ do_print("Comparing Array to Results passes");
+}
+
+
+/**
+ * Helper function to check to see if one object either is or is not in the
+ * result set. It can accept either a queryData object or an array of queryData
+ * objects. If it gets an array, it only compares the first object in the array
+ * to see if it is in the result set.
+ * Returns: True if item is in query set, and false if item is not in query set
+ * If input is an array, returns True if FIRST object in array is in
+ * query set. To compare entire array, use the function above.
+ */
+function isInResult(aQueryData, aRoot) {
+ var rv = false;
+ var uri;
+ var wasOpen = aRoot.containerOpen;
+ if (!wasOpen)
+ aRoot.containerOpen = true;
+
+ // If we have an array, pluck out the first item. If an object, pluc out the
+ // URI, we just compare URI's here.
+ if ("uri" in aQueryData) {
+ uri = aQueryData.uri;
+ } else {
+ uri = aQueryData[0].uri;
+ }
+
+ for (var i=0; i < aRoot.childCount; i++) {
+ if (uri == aRoot.getChild(i).uri) {
+ rv = true;
+ break;
+ }
+ }
+ if (!wasOpen)
+ aRoot.containerOpen = false;
+ return rv;
+}
+
+
+/**
+ * A nice helper function for debugging things. It prints the contents of a
+ * result set.
+ */
+function displayResultSet(aRoot) {
+
+ var wasOpen = aRoot.containerOpen;
+ if (!wasOpen)
+ aRoot.containerOpen = true;
+
+ if (!aRoot.hasChildren) {
+ // Something wrong? Empty result set?
+ do_print("Result Set Empty");
+ return;
+ }
+
+ for (var i=0; i < aRoot.childCount; ++i) {
+ do_print("Result Set URI: " + aRoot.getChild(i).uri + " Title: " +
+ aRoot.getChild(i).title + " Visit Time: " + aRoot.getChild(i).time);
+ }
+ if (!wasOpen)
+ aRoot.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/queries/readme.txt b/toolkit/components/places/tests/queries/readme.txt
new file mode 100644
index 0000000000..19414f96ed
--- /dev/null
+++ b/toolkit/components/places/tests/queries/readme.txt
@@ -0,0 +1,16 @@
+These are tests specific to the Places Query API.
+
+We are tracking the coverage of these tests here:
+http://wiki.mozilla.org/QA/TDAI/Projects/Places_Tests
+
+When creating one of these tests, you need to update those tables so that we
+know how well our test coverage is of this area. Furthermore, when adding tests
+ensure to cover live update (changing the query set) by performing the following
+operations on the query set you get after running the query:
+* Adding a new item to the query set
+* Updating an existing item so that it matches the query set
+* Change an existing item so that it does not match the query set
+* Do multiple of the above inside an Update Batch transaction.
+* Try these transactions in different orders.
+
+Use the stub test to help you create a test with the proper structure.
diff --git a/toolkit/components/places/tests/queries/test_415716.js b/toolkit/components/places/tests/queries/test_415716.js
new file mode 100644
index 0000000000..754a73e7cc
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_415716.js
@@ -0,0 +1,108 @@
+/* -*- 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/. */
+
+function modHistoryTypes(val) {
+ switch (val % 8) {
+ case 0:
+ case 1:
+ return TRANSITION_LINK;
+ case 2:
+ return TRANSITION_TYPED;
+ case 3:
+ return TRANSITION_BOOKMARK;
+ case 4:
+ return TRANSITION_EMBED;
+ case 5:
+ return TRANSITION_REDIRECT_PERMANENT;
+ case 6:
+ return TRANSITION_REDIRECT_TEMPORARY;
+ case 7:
+ return TRANSITION_DOWNLOAD;
+ case 8:
+ return TRANSITION_FRAMED_LINK;
+ }
+ return TRANSITION_TYPED;
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+/**
+ * Builds a test database by hand using various times, annotations and
+ * visit numbers for this test
+ */
+add_task(function* test_buildTestDatabase()
+{
+ // This is the set of visits that we will match - our min visit is 2 so that's
+ // why we add more visits to the same URIs.
+ let testURI = uri("http://www.foo.com");
+ let places = [];
+
+ for (let i = 0; i < 12; ++i) {
+ places.push({
+ uri: testURI,
+ transition: modHistoryTypes(i),
+ visitDate: today
+ });
+ }
+
+ testURI = uri("http://foo.com/youdontseeme.html");
+ let testAnnoName = "moz-test-places/testing123";
+ let testAnnoVal = "test";
+ for (let i = 0; i < 12; ++i) {
+ places.push({
+ uri: testURI,
+ transition: modHistoryTypes(i),
+ visitDate: today
+ });
+ }
+
+ yield PlacesTestUtils.addVisits(places);
+
+ PlacesUtils.annotations.setPageAnnotation(testURI, testAnnoName,
+ testAnnoVal, 0, 0);
+});
+
+/**
+ * This test will test Queries that use relative Time Range, minVists, maxVisits,
+ * annotation.
+ * The Query:
+ * Annotation == "moz-test-places/testing123" &&
+ * TimeRange == "now() - 2d" &&
+ * minVisits == 2 &&
+ * maxVisits == 10
+ */
+add_task(function test_execute()
+{
+ let query = PlacesUtils.history.getNewQuery();
+ query.annotation = "moz-test-places/testing123";
+ query.beginTime = daybefore * 1000;
+ query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_NOW;
+ query.endTime = today * 1000;
+ query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_NOW;
+ query.minVisits = 2;
+ query.maxVisits = 10;
+
+ // Options
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ // Results
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ dump("----> cc is: " + cc + "\n");
+ for (let i = 0; i < root.childCount; ++i) {
+ let resultNode = root.getChild(i);
+ let accesstime = Date(resultNode.time / 1000);
+ dump("----> result: " + resultNode.uri + " Date: " + accesstime.toLocaleString() + "\n");
+ }
+ do_check_eq(cc, 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_abstime-annotation-domain.js b/toolkit/components/places/tests/queries/test_abstime-annotation-domain.js
new file mode 100644
index 0000000000..199fc0865e
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_abstime-annotation-domain.js
@@ -0,0 +1,210 @@
+/* -*- 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/. */
+
+const DAY_MSEC = 86400000;
+const MIN_MSEC = 60000;
+const HOUR_MSEC = 3600000;
+// Jan 6 2008 at 8am is our begin edge of the query
+var beginTimeDate = new Date(2008, 0, 6, 8, 0, 0, 0);
+// Jan 15 2008 at 9:30pm is our ending edge of the query
+var endTimeDate = new Date(2008, 0, 15, 21, 30, 0, 0);
+
+// These as millisecond values
+var beginTime = beginTimeDate.getTime();
+var endTime = endTimeDate.getTime();
+
+// Some range dates inside our query - mult by 1000 to convert to PRTIME
+var jan7_800 = (beginTime + DAY_MSEC) * 1000;
+var jan6_815 = (beginTime + (MIN_MSEC * 15)) * 1000;
+var jan11_800 = (beginTime + (DAY_MSEC * 5)) * 1000;
+var jan14_2130 = (endTime - DAY_MSEC) * 1000;
+var jan15_2045 = (endTime - (MIN_MSEC * 45)) * 1000;
+var jan12_1730 = (endTime - (DAY_MSEC * 3) - (HOUR_MSEC*4)) * 1000;
+
+// Dates outside our query - mult by 1000 to convert to PRTIME
+var jan6_700 = (beginTime - HOUR_MSEC) * 1000;
+var jan5_800 = (beginTime - DAY_MSEC) * 1000;
+var dec27_800 = (beginTime - (DAY_MSEC * 10)) * 1000;
+var jan15_2145 = (endTime + (MIN_MSEC * 15)) * 1000;
+var jan16_2130 = (endTime + (DAY_MSEC)) * 1000;
+var jan25_2130 = (endTime + (DAY_MSEC * 10)) * 1000;
+
+// So that we can easily use these too, convert them to PRTIME
+beginTime *= 1000;
+endTime *= 1000;
+
+/**
+ * Array of objects to build our test database
+ */
+var goodAnnoName = "moz-test-places/testing123";
+var val = "test";
+var badAnnoName = "text/foo";
+
+// The test data for our database, note that the ordering of the results that
+// will be returned by the query (the isInQuery: true objects) is IMPORTANT.
+// see compareArrayToResult in head_queries.js for more info.
+var testData = [
+ // Test ftp protocol - vary the title length
+ {isInQuery: true, isVisit: true, isDetails: true,
+ uri: "ftp://foo.com/ftp", lastVisit: jan12_1730,
+ title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"},
+
+ // Test flat domain with annotation
+ {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true,
+ uri: "http://foo.com/", annoName: goodAnnoName, annoVal: val,
+ lastVisit: jan14_2130, title: "moz"},
+
+ // Test subdomain included with isRedirect=true, different transtype
+ {isInQuery: true, isVisit: true, isDetails: true, title: "moz",
+ isRedirect: true, uri: "http://mail.foo.com/redirect", lastVisit: jan11_800,
+ transType: PlacesUtils.history.TRANSITION_LINK},
+
+ // Test subdomain inclued at the leading time edge
+ {isInQuery: true, isVisit: true, isDetails: true,
+ uri: "http://mail.foo.com/yiihah", title: "moz", lastVisit: jan6_815},
+
+ // Test www. style URI is included, with an annotation
+ {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true,
+ uri: "http://www.foo.com/yiihah", annoName: goodAnnoName, annoVal: val,
+ lastVisit: jan7_800, title: "moz"},
+
+ // Test https protocol
+ {isInQuery: true, isVisit: true, isDetails: true, title: "moz",
+ uri: "https://foo.com/", lastVisit: jan15_2045},
+
+ // Test begin edge of time
+ {isInQuery: true, isVisit: true, isDetails: true, title: "moz mozilla",
+ uri: "https://foo.com/begin.html", lastVisit: beginTime},
+
+ // Test end edge of time
+ {isInQuery: true, isVisit: true, isDetails: true, title: "moz mozilla",
+ uri: "https://foo.com/end.html", lastVisit: endTime},
+
+ // Test an image link, with annotations
+ {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true,
+ title: "mozzie the dino", uri: "https://foo.com/mozzie.png",
+ annoName: goodAnnoName, annoVal: val, lastVisit: jan14_2130},
+
+ // Begin the invalid queries: Test too early
+ {isInQuery: false, isVisit:true, isDetails: true, title: "moz",
+ uri: "http://foo.com/tooearly.php", lastVisit: jan6_700},
+
+ // Test Bad Annotation
+ {isInQuery: false, isVisit:true, isDetails: true, isPageAnnotation: true,
+ title: "moz", uri: "http://foo.com/badanno.htm", lastVisit: jan12_1730,
+ annoName: badAnnoName, annoVal: val},
+
+ // Test bad URI
+ {isInQuery: false, isVisit:true, isDetails: true, title: "moz",
+ uri: "http://somefoo.com/justwrong.htm", lastVisit: jan11_800},
+
+ // Test afterward, one to update
+ {isInQuery: false, isVisit:true, isDetails: true, title: "changeme",
+ uri: "http://foo.com/changeme1.htm", lastVisit: jan12_1730},
+
+ // Test invalid title
+ {isInQuery: false, isVisit:true, isDetails: true, title: "changeme2",
+ uri: "http://foo.com/changeme2.htm", lastVisit: jan7_800},
+
+ // Test changing the lastVisit
+ {isInQuery: false, isVisit:true, isDetails: true, title: "moz",
+ uri: "http://foo.com/changeme3.htm", lastVisit: dec27_800}];
+
+/**
+ * This test will test a Query using several terms and do a bit of negative
+ * testing for items that should be ignored while querying over history.
+ * The Query:WHERE absoluteTime(matches) AND searchTerms AND URI
+ * AND annotationIsNot(match) GROUP BY Domain, Day SORT BY uri,ascending
+ * excludeITems(should be ignored)
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_abstime_annotation_domain()
+{
+ // Initialize database
+ yield task_populateDB(testData);
+
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.beginTime = beginTime;
+ query.endTime = endTime;
+ query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH;
+ query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH;
+ query.searchTerms = "moz";
+ query.domain = "foo.com";
+ query.domainIsHost = false;
+ query.annotation = "text/foo";
+ query.annotationIsNot = true;
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_URI_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ // The next two options should be ignored
+ // can't use this one, breaks test - bug 419779
+ // options.excludeItems = true;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ // Ensure the result set is correct
+ compareArrayToResult(testData, root);
+
+ // Make some changes to the result set
+ // Let's add something first
+ var addItem = [{isInQuery: true, isVisit: true, isDetails: true, title: "moz",
+ uri: "http://www.foo.com/i-am-added.html", lastVisit: jan11_800}];
+ yield task_populateDB(addItem);
+ do_print("Adding item foo.com/i-am-added.html");
+ do_check_eq(isInResult(addItem, root), true);
+
+ // Let's update something by title
+ var change1 = [{isDetails: true, uri: "http://foo.com/changeme1",
+ lastVisit: jan12_1730, title: "moz moz mozzie"}];
+ yield task_populateDB(change1);
+ do_print("LiveUpdate by changing title");
+ do_check_eq(isInResult(change1, root), true);
+
+ // Let's update something by annotation
+ // Updating a page by removing an annotation does not cause it to join this
+ // query set. I tend to think that it should cause that page to join this
+ // query set, because this visit fits all theother specified criteria once the
+ // annotation is removed. Uncommenting this will fail the test.
+ // Bug 424050
+ /* var change2 = [{isPageAnnotation: true, uri: "http://foo.com/badannotaion.html",
+ annoName: "text/mozilla", annoVal: "test"}];
+ yield task_populateDB(change2);
+ do_print("LiveUpdate by removing annotation");
+ do_check_eq(isInResult(change2, root), true);*/
+
+ // Let's update by adding a visit in the time range for an existing URI
+ var change3 = [{isDetails: true, uri: "http://foo.com/changeme3.htm",
+ title: "moz", lastVisit: jan15_2045}];
+ yield task_populateDB(change3);
+ do_print("LiveUpdate by adding visit within timerange");
+ do_check_eq(isInResult(change3, root), true);
+
+ // And delete something from the result set - using annotation
+ // Once again, bug 424050 prevents this from passing
+ /* var change4 = [{isPageAnnotation: true, uri: "ftp://foo.com/ftp",
+ annoVal: "test", annoName: badAnnoName}];
+ yield task_populateDB(change4);
+ do_print("LiveUpdate by deleting item from set by adding annotation");
+ do_check_eq(isInResult(change4, root), false);*/
+
+ // Delete something by changing the title
+ var change5 = [{isDetails: true, uri: "http://foo.com/end.html", title: "deleted"}];
+ yield task_populateDB(change5);
+ do_print("LiveUpdate by deleting item by changing title");
+ do_check_eq(isInResult(change5, root), false);
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js b/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js
new file mode 100644
index 0000000000..145d2cb596
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js
@@ -0,0 +1,162 @@
+/* -*- 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/. */
+
+const DAY_MSEC = 86400000;
+const MIN_MSEC = 60000;
+const HOUR_MSEC = 3600000;
+// Jan 6 2008 at 8am is our begin edge of the query
+var beginTimeDate = new Date(2008, 0, 6, 8, 0, 0, 0);
+// Jan 15 2008 at 9:30pm is our ending edge of the query
+var endTimeDate = new Date(2008, 0, 15, 21, 30, 0, 0);
+
+// These as millisecond values
+var beginTime = beginTimeDate.getTime();
+var endTime = endTimeDate.getTime();
+
+// Some range dates inside our query - mult by 1000 to convert to PRTIME
+var jan7_800 = (beginTime + DAY_MSEC) * 1000;
+var jan6_815 = (beginTime + (MIN_MSEC * 15)) * 1000;
+var jan11_800 = (beginTime + (DAY_MSEC * 5)) * 1000;
+var jan14_2130 = (endTime - DAY_MSEC) * 1000;
+var jan15_2045 = (endTime - (MIN_MSEC * 45)) * 1000;
+var jan12_1730 = (endTime - (DAY_MSEC * 3) - (HOUR_MSEC*4)) * 1000;
+
+// Dates outside our query - mult by 1000 to convert to PRTIME
+var jan6_700 = (beginTime - HOUR_MSEC) * 1000;
+var jan5_800 = (beginTime - DAY_MSEC) * 1000;
+var dec27_800 = (beginTime - (DAY_MSEC * 10)) * 1000;
+var jan15_2145 = (endTime + (MIN_MSEC * 15)) * 1000;
+var jan16_2130 = (endTime + (DAY_MSEC)) * 1000;
+var jan25_2130 = (endTime + (DAY_MSEC * 10)) * 1000;
+
+// So that we can easily use these too, convert them to PRTIME
+beginTime *= 1000;
+endTime *= 1000;
+
+/**
+ * Array of objects to build our test database
+ */
+var goodAnnoName = "moz-test-places/testing123";
+var val = "test";
+var badAnnoName = "text/foo";
+
+// The test data for our database, note that the ordering of the results that
+// will be returned by the query (the isInQuery: true objects) is IMPORTANT.
+// see compareArrayToResult in head_queries.js for more info.
+var testData = [
+
+ // Test flat domain with annotation
+ {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true,
+ uri: "http://foo.com/", annoName: goodAnnoName, annoVal: val,
+ lastVisit: jan14_2130, title: "moz"},
+
+ // Begin the invalid queries:
+ // Test www. style URI is not included, with an annotation
+ {isInQuery: false, isVisit: true, isDetails: true, isPageAnnotation: true,
+ uri: "http://www.foo.com/yiihah", annoName: goodAnnoName, annoVal: val,
+ lastVisit: jan7_800, title: "moz"},
+
+ // Test subdomain not inclued at the leading time edge
+ {isInQuery: false, isVisit: true, isDetails: true,
+ uri: "http://mail.foo.com/yiihah", title: "moz", lastVisit: jan6_815},
+
+ // Test https protocol
+ {isInQuery: false, isVisit: true, isDetails: true, title: "moz",
+ uri: "https://foo.com/", lastVisit: jan15_2045},
+
+ // Test ftp protocol
+ {isInQuery: false, isVisit: true, isDetails: true,
+ uri: "ftp://foo.com/ftp", lastVisit: jan12_1730,
+ title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"},
+
+ // Test too early
+ {isInQuery: false, isVisit:true, isDetails: true, title: "moz",
+ uri: "http://foo.com/tooearly.php", lastVisit: jan6_700},
+
+ // Test Bad Annotation
+ {isInQuery: false, isVisit:true, isDetails: true, isPageAnnotation: true,
+ title: "moz", uri: "http://foo.com/badanno.htm", lastVisit: jan12_1730,
+ annoName: badAnnoName, annoVal: val},
+
+ // Test afterward, one to update
+ {isInQuery: false, isVisit:true, isDetails: true, title: "changeme",
+ uri: "http://foo.com/changeme1.htm", lastVisit: jan12_1730},
+
+ // Test invalid title
+ {isInQuery: false, isVisit:true, isDetails: true, title: "changeme2",
+ uri: "http://foo.com/changeme2.htm", lastVisit: jan7_800},
+
+ // Test changing the lastVisit
+ {isInQuery: false, isVisit:true, isDetails: true, title: "moz",
+ uri: "http://foo.com/changeme3.htm", lastVisit: dec27_800}];
+
+/**
+ * This test will test a Query using several terms and do a bit of negative
+ * testing for items that should be ignored while querying over history.
+ * The Query:WHERE absoluteTime(matches) AND searchTerms AND URI
+ * AND annotationIsNot(match) GROUP BY Domain, Day SORT BY uri,ascending
+ * excludeITems(should be ignored)
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_abstime_annotation_uri()
+{
+ // Initialize database
+ yield task_populateDB(testData);
+
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.beginTime = beginTime;
+ query.endTime = endTime;
+ query.beginTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH;
+ query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH;
+ query.searchTerms = "moz";
+ query.uri = uri("http://foo.com");
+ query.annotation = "text/foo";
+ query.annotationIsNot = true;
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_URI_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ // The next two options should be ignored
+ // can't use this one, breaks test - bug 419779
+ // options.excludeItems = true;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ // Ensure the result set is correct
+ compareArrayToResult(testData, root);
+
+ // live update.
+ do_print("change title");
+ var change1 = [{isDetails: true, uri:"http://foo.com/",
+ title: "mo"}, ];
+ yield task_populateDB(change1);
+ do_check_false(isInResult({uri: "http://foo.com/"}, root));
+
+ var change2 = [{isDetails: true, uri:"http://foo.com/",
+ title: "moz", lastvisit: endTime}, ];
+ yield task_populateDB(change2);
+ dump_table("moz_places");
+ do_check_false(isInResult({uri: "http://foo.com/"}, root));
+
+ // Let's delete something from the result set - using annotation
+ var change3 = [{isPageAnnotation: true,
+ uri: "http://foo.com/",
+ annoName: badAnnoName, annoVal: "test"}];
+ yield task_populateDB(change3);
+ do_print("LiveUpdate by removing annotation");
+ do_check_false(isInResult({uri: "http://foo.com/"}, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_async.js b/toolkit/components/places/tests/queries/test_async.js
new file mode 100644
index 0000000000..0ec99f8fc2
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_async.js
@@ -0,0 +1,371 @@
+/* -*- 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 tests = [
+ {
+ desc: "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " +
+ "close container with a single child",
+
+ loading: function (node, newState, oldState) {
+ this.checkStateChanged("loading", 1);
+ this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
+ },
+
+ opened: function (node, newState, oldState) {
+ this.checkStateChanged("opened", 1);
+ this.checkState("loading", 1);
+ this.checkArgs("opened", node, oldState, node.STATE_LOADING);
+
+ print("Checking node children");
+ compareArrayToResult(this.data, node);
+
+ print("Closing container");
+ node.containerOpen = false;
+ },
+
+ closed: function (node, newState, oldState) {
+ this.checkStateChanged("closed", 1);
+ this.checkState("opened", 1);
+ this.checkArgs("closed", node, oldState, node.STATE_OPENED);
+ this.success();
+ }
+ },
+
+ {
+ desc: "nsNavHistoryFolderResultNode: After async open and no changes, " +
+ "second open should be synchronous",
+
+ loading: function (node, newState, oldState) {
+ this.checkStateChanged("loading", 1);
+ this.checkState("closed", 0);
+ this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
+ },
+
+ opened: function (node, newState, oldState) {
+ let cnt = this.checkStateChanged("opened", 1, 2);
+ let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED;
+ this.checkArgs("opened", node, oldState, expectOldState);
+
+ print("Checking node children");
+ compareArrayToResult(this.data, node);
+
+ print("Closing container");
+ node.containerOpen = false;
+ },
+
+ closed: function (node, newState, oldState) {
+ let cnt = this.checkStateChanged("closed", 1, 2);
+ this.checkArgs("closed", node, oldState, node.STATE_OPENED);
+
+ switch (cnt) {
+ case 1:
+ node.containerOpen = true;
+ break;
+ case 2:
+ this.success();
+ break;
+ }
+ }
+ },
+
+ {
+ desc: "nsNavHistoryFolderResultNode: After closing container in " +
+ "loading(), opened() should not be called",
+
+ loading: function (node, newState, oldState) {
+ this.checkStateChanged("loading", 1);
+ this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
+ print("Closing container");
+ node.containerOpen = false;
+ },
+
+ opened: function (node, newState, oldState) {
+ do_throw("opened should not be called");
+ },
+
+ closed: function (node, newState, oldState) {
+ this.checkStateChanged("closed", 1);
+ this.checkState("loading", 1);
+ this.checkArgs("closed", node, oldState, node.STATE_LOADING);
+ this.success();
+ }
+ }
+];
+
+
+/**
+ * Instances of this class become the prototypes of the test objects above.
+ * Each test can therefore use the methods of this class, or they can override
+ * them if they want. To run a test, call setup() and then run().
+ */
+function Test() {
+ // This maps a state name to the number of times it's been observed.
+ this.stateCounts = {};
+ // Promise object resolved when the next test can be run.
+ this.deferNextTest = Promise.defer();
+}
+
+Test.prototype = {
+ /**
+ * Call this when an observer observes a container state change to sanity
+ * check the arguments.
+ *
+ * @param aNewState
+ * The name of the new state. Used only for printing out helpful info.
+ * @param aNode
+ * The node argument passed to containerStateChanged.
+ * @param aOldState
+ * The old state argument passed to containerStateChanged.
+ * @param aExpectOldState
+ * The expected old state.
+ */
+ checkArgs: function (aNewState, aNode, aOldState, aExpectOldState) {
+ print("Node passed on " + aNewState + " should be result.root");
+ do_check_eq(this.result.root, aNode);
+ print("Old state passed on " + aNewState + " should be " + aExpectOldState);
+
+ // aOldState comes from xpconnect and will therefore be defined. It may be
+ // zero, though, so use strict equality just to make sure aExpectOldState is
+ // also defined.
+ do_check_true(aOldState === aExpectOldState);
+ },
+
+ /**
+ * Call this when an observer observes a container state change. It registers
+ * the state change and ensures that it has been observed the given number
+ * of times. See checkState for parameter explanations.
+ *
+ * @return The number of times aState has been observed, including the new
+ * observation.
+ */
+ checkStateChanged: function (aState, aExpectedMin, aExpectedMax) {
+ print(aState + " state change observed");
+ if (!this.stateCounts.hasOwnProperty(aState))
+ this.stateCounts[aState] = 0;
+ this.stateCounts[aState]++;
+ return this.checkState(aState, aExpectedMin, aExpectedMax);
+ },
+
+ /**
+ * Ensures that the state has been observed the given number of times.
+ *
+ * @param aState
+ * The name of the state.
+ * @param aExpectedMin
+ * The state must have been observed at least this number of times.
+ * @param aExpectedMax
+ * The state must have been observed at most this number of times.
+ * This parameter is optional. If undefined, it's set to
+ * aExpectedMin.
+ * @return The number of times aState has been observed, including the new
+ * observation.
+ */
+ checkState: function (aState, aExpectedMin, aExpectedMax) {
+ let cnt = this.stateCounts[aState] || 0;
+ if (aExpectedMax === undefined)
+ aExpectedMax = aExpectedMin;
+ if (aExpectedMin === aExpectedMax) {
+ print(aState + " should be observed only " + aExpectedMin +
+ " times (actual = " + cnt + ")");
+ }
+ else {
+ print(aState + " should be observed at least " + aExpectedMin +
+ " times and at most " + aExpectedMax + " times (actual = " +
+ cnt + ")");
+ }
+ do_check_true(cnt >= aExpectedMin && cnt <= aExpectedMax);
+ return cnt;
+ },
+
+ /**
+ * Asynchronously opens the root of the test's result.
+ */
+ openContainer: function () {
+ // Set up the result observer. It delegates to this object's callbacks and
+ // wraps them in a try-catch so that errors don't get eaten.
+ let self = this;
+ this.observer = {
+ containerStateChanged: function (container, oldState, newState) {
+ print("New state passed to containerStateChanged() should equal the " +
+ "container's current state");
+ do_check_eq(newState, container.state);
+
+ try {
+ switch (newState) {
+ case Ci.nsINavHistoryContainerResultNode.STATE_LOADING:
+ self.loading(container, newState, oldState);
+ break;
+ case Ci.nsINavHistoryContainerResultNode.STATE_OPENED:
+ self.opened(container, newState, oldState);
+ break;
+ case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED:
+ self.closed(container, newState, oldState);
+ break;
+ default:
+ do_throw("Unexpected new state! " + newState);
+ }
+ }
+ catch (err) {
+ do_throw(err);
+ }
+ },
+ };
+ this.result.addObserver(this.observer, false);
+
+ print("Opening container");
+ this.result.root.containerOpen = true;
+ },
+
+ /**
+ * Starts the test and returns a promise resolved when the test completes.
+ */
+ run: function () {
+ this.openContainer();
+ return this.deferNextTest.promise;
+ },
+
+ /**
+ * This must be called before run(). It adds a bookmark and sets up the
+ * test's result. Override if need be.
+ */
+ setup: function*() {
+ // Populate the database with different types of bookmark items.
+ this.data = DataHelper.makeDataArray([
+ { type: "bookmark" },
+ { type: "separator" },
+ { type: "folder" },
+ { type: "bookmark", uri: "place:terms=foo" }
+ ]);
+ yield task_populateDB(this.data);
+
+ // Make a query.
+ this.query = PlacesUtils.history.getNewQuery();
+ this.query.setFolders([DataHelper.defaults.bookmark.parent], 1);
+ this.opts = PlacesUtils.history.getNewQueryOptions();
+ this.opts.asyncEnabled = true;
+ this.result = PlacesUtils.history.executeQuery(this.query, this.opts);
+ },
+
+ /**
+ * Call this when the test has succeeded. It cleans up resources and starts
+ * the next test.
+ */
+ success: function () {
+ this.result.removeObserver(this.observer);
+
+ // Resolve the promise object that indicates that the next test can be run.
+ this.deferNextTest.resolve();
+ }
+};
+
+/**
+ * This makes it a little bit easier to use the functions of head_queries.js.
+ */
+var DataHelper = {
+ defaults: {
+ bookmark: {
+ parent: PlacesUtils.bookmarks.unfiledBookmarksFolder,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ uri: "http://example.com/",
+ title: "test bookmark"
+ },
+
+ folder: {
+ parent: PlacesUtils.bookmarks.unfiledBookmarksFolder,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test folder"
+ },
+
+ separator: {
+ parent: PlacesUtils.bookmarks.unfiledBookmarksFolder,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ }
+ },
+
+ /**
+ * Converts an array of simple bookmark item descriptions to the more verbose
+ * format required by task_populateDB() in head_queries.js.
+ *
+ * @param aData
+ * An array of objects, each of which describes a bookmark item.
+ * @return An array of objects suitable for passing to populateDB().
+ */
+ makeDataArray: function DH_makeDataArray(aData) {
+ let self = this;
+ return aData.map(function (dat) {
+ let type = dat.type;
+ dat = self._makeDataWithDefaults(dat, self.defaults[type]);
+ switch (type) {
+ case "bookmark":
+ return {
+ isBookmark: true,
+ uri: dat.uri,
+ parentGuid: dat.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: dat.title,
+ isInQuery: true
+ };
+ case "separator":
+ return {
+ isSeparator: true,
+ parentGuid: dat.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ isInQuery: true
+ };
+ case "folder":
+ return {
+ isFolder: true,
+ parentGuid: dat.parentGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: dat.title,
+ isInQuery: true
+ };
+ default:
+ do_throw("Unknown data type when populating DB: " + type);
+ return undefined;
+ }
+ });
+ },
+
+ /**
+ * Returns a copy of aData, except that any properties that are undefined but
+ * defined in aDefaults are set to the corresponding values in aDefaults.
+ *
+ * @param aData
+ * An object describing a bookmark item.
+ * @param aDefaults
+ * An object describing the default bookmark item.
+ * @return A copy of aData with defaults values set.
+ */
+ _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) {
+ let dat = {};
+ for (let [prop, val] of Object.entries(aDefaults)) {
+ dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val;
+ }
+ return dat;
+ }
+};
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_async()
+{
+ for (let test of tests) {
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ test.__proto__ = new Test();
+ yield test.setup();
+
+ print("------ Running test: " + test.desc);
+ yield test.run();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ print("All tests done, exiting");
+});
diff --git a/toolkit/components/places/tests/queries/test_containersQueries_sorting.js b/toolkit/components/places/tests/queries/test_containersQueries_sorting.js
new file mode 100644
index 0000000000..ab9f2bf901
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_containersQueries_sorting.js
@@ -0,0 +1,411 @@
+/* -*- 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/. */
+
+/**
+ * Testing behavior of bug 473157
+ * "Want to sort history in container view without sorting the containers"
+ * and regression bug 488783
+ * Tags list no longer sorted (alphabetized).
+ * This test is for global testing sorting containers queries.
+ */
+
+// Globals and Constants
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bh = hs.QueryInterface(Ci.nsIBrowserHistory);
+var tagging = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+
+var resultTypes = [
+ {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY, name: "RESULTS_AS_DATE_QUERY"},
+ {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY, name: "RESULTS_AS_SITE_QUERY"},
+ {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY, name: "RESULTS_AS_DATE_SITE_QUERY"},
+ {value: Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY, name: "RESULTS_AS_TAG_QUERY"},
+];
+
+var sortingModes = [
+ {value: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING, name: "SORT_BY_TITLE_ASCENDING"},
+ {value: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING, name: "SORT_BY_TITLE_DESCENDING"},
+ {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING, name: "SORT_BY_DATE_ASCENDING"},
+ {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING, name: "SORT_BY_DATE_DESCENDING"},
+ {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING, name: "SORT_BY_DATEADDED_ASCENDING"},
+ {value: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING, name: "SORT_BY_DATEADDED_DESCENDING"},
+];
+
+// These pages will be added from newest to oldest and from less visited to most
+// visited.
+var pages = [
+ "http://www.mozilla.org/c/",
+ "http://www.mozilla.org/a/",
+ "http://www.mozilla.org/b/",
+ "http://www.mozilla.com/c/",
+ "http://www.mozilla.com/a/",
+ "http://www.mozilla.com/b/",
+];
+
+var tags = [
+ "mozilla",
+ "Development",
+ "test",
+];
+
+// Test Runner
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback)
+{
+ if (aSequences.length === 0)
+ return 0;
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ var seqEltPtrs = aSequences.map(i => 0);
+
+ var numProds = 0;
+ var done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ var prod = [];
+ for (var i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ var seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0)
+ done = true;
+ }
+ else break;
+ }
+ }
+ return numProds;
+}
+
+/**
+ * Test a query based on passed-in options.
+ *
+ * @param aSequence
+ * array of options we will use to query.
+ */
+function test_query_callback(aSequence) {
+ do_check_eq(aSequence.length, 2);
+ var resultType = aSequence[0];
+ var sortingMode = aSequence[1];
+ print("\n\n*** Testing default sorting for resultType (" + resultType.name + ") and sortingMode (" + sortingMode.name + ")");
+
+ // Skip invalid combinations sorting queries by none.
+ if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY &&
+ (sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING ||
+ sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)) {
+ // This is a bookmark query, we can't sort by visit date.
+ sortingMode.value = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+ if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY ||
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) {
+ // This is an history query, we can't sort by date added.
+ if (sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING ||
+ sortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING)
+ sortingMode.value = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+
+ // Create a new query with required options.
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = resultType.value;
+ options.sortingMode = sortingMode.value;
+
+ // Compare resultset with expectedData.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) {
+ // Date containers are always sorted by date descending.
+ check_children_sorting(root,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
+ }
+ else
+ check_children_sorting(root, sortingMode.value);
+
+ // Now Check sorting of the first child container.
+ var container = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ container.containerOpen = true;
+
+ if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) {
+ // Has more than one level of containers, first we check the sorting of
+ // the first level (site containers), those won't inherit sorting...
+ check_children_sorting(container,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
+ // ...then we check sorting of the contained urls, we can't inherit sorting
+ // since the above level does not inherit it, so they will be sorted by
+ // title ascending.
+ let innerContainer = container.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ innerContainer.containerOpen = true;
+ check_children_sorting(innerContainer,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
+ innerContainer.containerOpen = false;
+ }
+ else if (resultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
+ // Sorting mode for tag contents is hardcoded for now, to allow for faster
+ // duplicates filtering.
+ check_children_sorting(container,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE);
+ }
+ else
+ check_children_sorting(container, sortingMode.value);
+
+ container.containerOpen = false;
+ root.containerOpen = false;
+
+ test_result_sortingMode_change(result, resultType, sortingMode);
+}
+
+/**
+ * Sets sortingMode on aResult and checks for correct sorting of children.
+ * Containers should not change their sorting, while contained uri nodes should.
+ *
+ * @param aResult
+ * nsINavHistoryResult generated by our query.
+ * @param aResultType
+ * required result type.
+ * @param aOriginalSortingMode
+ * the sorting mode from query's options.
+ */
+function test_result_sortingMode_change(aResult, aResultType, aOriginalSortingMode) {
+ var root = aResult.root;
+ // Now we set sortingMode on the result and check that containers are not
+ // sorted while children are.
+ sortingModes.forEach(function sortingModeChecker(aForcedSortingMode) {
+ print("\n* Test setting sortingMode (" + aForcedSortingMode.name + ") " +
+ "on result with resultType (" + aResultType.name + ") " +
+ "currently sorted as (" + aOriginalSortingMode.name + ")");
+
+ aResult.sortingMode = aForcedSortingMode.value;
+ root.containerOpen = true;
+
+ if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) {
+ // Date containers are always sorted by date descending.
+ check_children_sorting(root,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING);
+ }
+ else if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY &&
+ (aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING ||
+ aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)) {
+ // Site containers don't have a good time property to sort by.
+ check_children_sorting(root,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE);
+ }
+ else
+ check_children_sorting(root, aOriginalSortingMode.value);
+
+ // Now Check sorting of the first child container.
+ var container = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ container.containerOpen = true;
+
+ if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY) {
+ // Has more than one level of containers, first we check the sorting of
+ // the first level (site containers), those won't be sorted...
+ check_children_sorting(container,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING);
+ // ...then we check sorting of the second level of containers, result
+ // will sort them through recursiveSort.
+ let innerContainer = container.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ innerContainer.containerOpen = true;
+ check_children_sorting(innerContainer, aForcedSortingMode.value);
+ innerContainer.containerOpen = false;
+ }
+ else {
+ if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY ||
+ aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY ||
+ aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY) {
+ // Date containers are always sorted by date descending.
+ check_children_sorting(root, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE);
+ }
+ else if (aResultType.value == Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY &&
+ (aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING ||
+ aOriginalSortingMode.value == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING)) {
+ // Site containers don't have a good time property to sort by.
+ check_children_sorting(root, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE);
+ }
+ else
+ check_children_sorting(root, aOriginalSortingMode.value);
+
+ // Children should always be sorted.
+ check_children_sorting(container, aForcedSortingMode.value);
+ }
+
+ container.containerOpen = false;
+ root.containerOpen = false;
+ });
+}
+
+/**
+ * Test if children of aRootNode are correctly sorted.
+ * @param aRootNode
+ * already opened root node from our query's result.
+ * @param aExpectedSortingMode
+ * The sortingMode we expect results to be.
+ */
+function check_children_sorting(aRootNode, aExpectedSortingMode) {
+ var results = [];
+ print("Found children:");
+ for (let i = 0; i < aRootNode.childCount; i++) {
+ results[i] = aRootNode.getChild(i);
+ print(i + " " + results[i].title);
+ }
+
+ // Helper for case insensitive string comparison.
+ function caseInsensitiveStringComparator(a, b) {
+ var aLC = a.toLowerCase();
+ var bLC = b.toLowerCase();
+ if (aLC < bLC)
+ return -1;
+ if (aLC > bLC)
+ return 1;
+ return 0;
+ }
+
+ // Get a comparator based on expected sortingMode.
+ var comparator;
+ switch (aExpectedSortingMode) {
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_NONE:
+ comparator = function (a, b) {
+ return 0;
+ }
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
+ comparator = function (a, b) {
+ return caseInsensitiveStringComparator(a.title, b.title);
+ }
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
+ comparator = function (a, b) {
+ return -caseInsensitiveStringComparator(a.title, b.title);
+ }
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
+ comparator = function (a, b) {
+ return a.time - b.time;
+ }
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
+ comparator = function (a, b) {
+ return b.time - a.time;
+ }
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
+ comparator = function (a, b) {
+ return a.dateAdded - b.dateAdded;
+ }
+ break;
+ case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
+ comparator = function (a, b) {
+ return b.dateAdded - a.dateAdded;
+ }
+ break;
+ default:
+ do_throw("Unknown sorting type: " + aExpectedSortingMode);
+ }
+
+ // Make an independent copy of the results array and sort it.
+ var sortedResults = results.slice();
+ sortedResults.sort(comparator);
+ // Actually compare returned children with our sorted array.
+ for (let i = 0; i < sortedResults.length; i++) {
+ if (sortedResults[i].title != results[i].title)
+ print(i + " index wrong, expected " + sortedResults[i].title +
+ " found " + results[i].title);
+ do_check_eq(sortedResults[i].title, results[i].title);
+ }
+}
+
+// Main
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_containersQueries_sorting()
+{
+ // Add visits, bookmarks and tags to our database.
+ var timeInMilliseconds = Date.now();
+ var visitCount = 0;
+ var dayOffset = 0;
+ var visits = [];
+ pages.forEach(aPageUrl => visits.push(
+ { isVisit: true,
+ isBookmark: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ uri: aPageUrl,
+ title: aPageUrl,
+ // subtract 5 hours per iteration, to expose more than one day container.
+ lastVisit: (timeInMilliseconds - (18000 * 1000 * dayOffset++)) * 1000,
+ visitCount: visitCount++,
+ isTag: true,
+ tagArray: tags,
+ isInQuery: true }));
+ yield task_populateDB(visits);
+
+ cartProd([resultTypes, sortingModes], test_query_callback);
+});
diff --git a/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
new file mode 100644
index 0000000000..fbbacf6c9e
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_history_queries_tags_liveUpdate.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that tags changes are correctly live-updated in a history
+// query.
+
+let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+}
+
+var gTestData = [
+ {
+ isVisit: true,
+ uri: "http://example.com/1/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "example1",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/2/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "example2",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/3/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "example3",
+ },
+];
+
+function newQueryWithOptions()
+{
+ return [ PlacesUtils.history.getNewQuery(),
+ PlacesUtils.history.getNewQueryOptions() ];
+}
+
+function testQueryContents(aQuery, aOptions, aCallback)
+{
+ let root = PlacesUtils.history.executeQuery(aQuery, aOptions).root;
+ root.containerOpen = true;
+ aCallback(root);
+ root.containerOpen = false;
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_initialize()
+{
+ yield task_populateDB(gTestData);
+});
+
+add_task(function pages_query()
+{
+ let [query, options] = newQueryWithOptions();
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ do_check_eq(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, null);
+ }
+ });
+});
+
+add_task(function visits_query()
+{
+ let [query, options] = newQueryWithOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ do_check_eq(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, null);
+ }
+ });
+});
+
+add_task(function bookmarks_query()
+{
+ let [query, options] = newQueryWithOptions();
+ query.setFolders([PlacesUtils.unfiledBookmarksFolderId], 1);
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ do_check_eq(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, null);
+ }
+ });
+});
+
+add_task(function pages_searchterm_query()
+{
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "example";
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ do_check_eq(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, null);
+ }
+ });
+});
+
+add_task(function visits_searchterm_query()
+{
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "example";
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ do_check_eq(node.tags, null);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, "test-tag");
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ do_check_eq(node.tags, null);
+ }
+ });
+});
+
+add_task(function pages_searchterm_is_tag_query()
+{
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "test-tag";
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([], root);
+ gTestData.forEach(function (data) {
+ let uri = NetUtil.newURI(data.uri);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ data.title);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ compareArrayToResult([data], root);
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ compareArrayToResult([], root);
+ });
+ });
+});
+
+add_task(function visits_searchterm_is_tag_query()
+{
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "test-tag";
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ testQueryContents(query, options, function (root) {
+ compareArrayToResult([], root);
+ gTestData.forEach(function (data) {
+ let uri = NetUtil.newURI(data.uri);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ data.title);
+ PlacesUtils.tagging.tagURI(uri, ["test-tag"]);
+ compareArrayToResult([data], root);
+ PlacesUtils.tagging.untagURI(uri, ["test-tag"]);
+ compareArrayToResult([], root);
+ });
+ });
+});
diff --git a/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
new file mode 100644
index 0000000000..eec87fe0ea
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_history_queries_titles_liveUpdate.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that tags changes are correctly live-updated in a history
+// query.
+
+let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+}
+
+var gTestData = [
+ {
+ isVisit: true,
+ uri: "http://example.com/1/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ title: "title1",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/2/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ title: "title2",
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/3/",
+ lastVisit: newTimeInMicroseconds(),
+ isInQuery: true,
+ title: "title3",
+ },
+];
+
+function searchNodeHavingUrl(aRoot, aUrl) {
+ for (let i = 0; i < aRoot.childCount; i++) {
+ if (aRoot.getChild(i).uri == aUrl) {
+ return aRoot.getChild(i);
+ }
+ }
+ return undefined;
+}
+
+function newQueryWithOptions()
+{
+ return [ PlacesUtils.history.getNewQuery(),
+ PlacesUtils.history.getNewQueryOptions() ];
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* pages_query()
+{
+ yield task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ do_check_eq(node.title, gTestData[i].title);
+ let uri = NetUtil.newURI(node.uri);
+ yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"});
+ do_check_eq(node.title, "changedTitle");
+ yield PlacesTestUtils.addVisits({uri: uri, title: gTestData[i].title});
+ do_check_eq(node.title, gTestData[i].title);
+ }
+
+ root.containerOpen = false;
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* visits_query()
+{
+ yield task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+
+ for (let testData of gTestData) {
+ let uri = NetUtil.newURI(testData.uri);
+ let node = searchNodeHavingUrl(root, testData.uri);
+ do_check_eq(node.title, testData.title);
+ yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"});
+ node = searchNodeHavingUrl(root, testData.uri);
+ do_check_eq(node.title, "changedTitle");
+ yield PlacesTestUtils.addVisits({uri: uri, title: testData.title});
+ node = searchNodeHavingUrl(root, testData.uri);
+ do_check_eq(node.title, testData.title);
+ }
+
+ root.containerOpen = false;
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* pages_searchterm_query()
+{
+ yield task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "example";
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ let uri = NetUtil.newURI(node.uri);
+ do_check_eq(node.title, gTestData[i].title);
+ yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"});
+ do_check_eq(node.title, "changedTitle");
+ yield PlacesTestUtils.addVisits({uri: uri, title: gTestData[i].title});
+ do_check_eq(node.title, gTestData[i].title);
+ }
+
+ root.containerOpen = false;
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* visits_searchterm_query()
+{
+ yield task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "example";
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ compareArrayToResult([gTestData[0], gTestData[1], gTestData[2]], root);
+ for (let testData of gTestData) {
+ let uri = NetUtil.newURI(testData.uri);
+ let node = searchNodeHavingUrl(root, testData.uri);
+ do_check_eq(node.title, testData.title);
+ yield PlacesTestUtils.addVisits({uri: uri, title: "changedTitle"});
+ node = searchNodeHavingUrl(root, testData.uri);
+ do_check_eq(node.title, "changedTitle");
+ yield PlacesTestUtils.addVisits({uri: uri, title: testData.title});
+ node = searchNodeHavingUrl(root, testData.uri);
+ do_check_eq(node.title, testData.title);
+ }
+
+ root.containerOpen = false;
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* pages_searchterm_is_title_query()
+{
+ yield task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "match";
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult([], root);
+ for (let data of gTestData) {
+ let uri = NetUtil.newURI(data.uri);
+ let origTitle = data.title;
+ data.title = "match";
+ yield PlacesTestUtils.addVisits({ uri: uri, title: data.title,
+ visitDate: data.lastVisit });
+ compareArrayToResult([data], root);
+ data.title = origTitle;
+ yield PlacesTestUtils.addVisits({ uri: uri, title: data.title,
+ visitDate: data.lastVisit });
+ compareArrayToResult([], root);
+ }
+
+ root.containerOpen = false;
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* visits_searchterm_is_title_query()
+{
+ yield task_populateDB(gTestData);
+
+ let [query, options] = newQueryWithOptions();
+ query.searchTerms = "match";
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult([], root);
+ for (let data of gTestData) {
+ let uri = NetUtil.newURI(data.uri);
+ let origTitle = data.title;
+ data.title = "match";
+ yield PlacesTestUtils.addVisits({ uri: uri, title: data.title,
+ visitDate: data.lastVisit });
+ compareArrayToResult([data], root);
+ data.title = origTitle;
+ yield PlacesTestUtils.addVisits({ uri: uri, title: data.title,
+ visitDate: data.lastVisit });
+ compareArrayToResult([], root);
+ }
+
+ root.containerOpen = false;
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/queries/test_onlyBookmarked.js b/toolkit/components/places/tests/queries/test_onlyBookmarked.js
new file mode 100644
index 0000000000..45704c1098
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_onlyBookmarked.js
@@ -0,0 +1,128 @@
+/* -*- 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/. */
+
+/**
+ * The next thing we do is create a test database for us. Each test runs with
+ * its own database (tail_queries.js will clear it after the run). Take a look
+ * at the queryData object in head_queries.js, and you'll see how this object
+ * works. You can call it anything you like, but I usually use "testData".
+ * I'll include a couple of example entries in the database.
+ *
+ * Note that to use the compareArrayToResult API, you need to put all the
+ * results that are in the query set at the top of the testData list, and those
+ * results MUST be in the same sort order as the items in the resulting query.
+ */
+
+var testData = [
+ // Add a bookmark that should be in the results
+ { isBookmark: true,
+ uri: "http://bookmarked.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ isInQuery: true },
+
+ // Add a bookmark that should not be in the results
+ { isBookmark: true,
+ uri: "http://bookmarked-elsewhere.com/",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ isInQuery: false },
+
+ // Add an un-bookmarked visit
+ { isVisit: true,
+ uri: "http://notbookmarked.com/",
+ isInQuery: false }
+];
+
+
+/**
+ * run_test is where the magic happens. This is automatically run by the test
+ * harness. It is where you do the work of creating the query, running it, and
+ * playing with the result set.
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_onlyBookmarked()
+{
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(testData);
+
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.toolbarFolderId], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_HISTORY;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ // You can use this to compare the data in the array with the result set,
+ // if the array's isInQuery: true items are sorted the same way as the result
+ // set.
+ do_print("begin first test");
+ compareArrayToResult(testData, root);
+ do_print("end first test");
+
+ // Test live-update
+ var liveUpdateTestData = [
+ // Add a bookmark that should show up
+ { isBookmark: true,
+ uri: "http://bookmarked2.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ isInQuery: true },
+
+ // Add a bookmark that should not show up
+ { isBookmark: true,
+ uri: "http://bookmarked-elsewhere2.com/",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ isInQuery: false }
+ ];
+
+ yield task_populateDB(liveUpdateTestData); // add to the db
+
+ // add to the test data
+ testData.push(liveUpdateTestData[0]);
+ testData.push(liveUpdateTestData[1]);
+
+ // re-query and test
+ do_print("begin live-update test");
+ compareArrayToResult(testData, root);
+ do_print("end live-update test");
+/*
+ // we are actually not updating during a batch.
+ // see bug 432706 for details.
+
+ // Here's a batch update
+ var updateBatch = {
+ runBatched: function (aUserData) {
+ liveUpdateTestData[0].uri = "http://bookmarked3.com";
+ liveUpdateTestData[1].uri = "http://bookmarked-elsewhere3.com";
+ populateDB(liveUpdateTestData);
+ testData.push(liveUpdateTestData[0]);
+ testData.push(liveUpdateTestData[1]);
+ }
+ };
+
+ PlacesUtils.history.runInBatchMode(updateBatch, null);
+
+ // re-query and test
+ do_print("begin batched test");
+ compareArrayToResult(testData, root);
+ do_print("end batched test");
+*/
+ // Close the container when finished
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_queryMultipleFolder.js b/toolkit/components/places/tests/queries/test_queryMultipleFolder.js
new file mode 100644
index 0000000000..694728a437
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_queryMultipleFolder.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_queryMultipleFolders() {
+ // adding bookmarks in the folders
+ let folderIds = [];
+ let bookmarkIds = [];
+ for (let i = 0; i < 3; ++i) {
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: `Folder${i}`
+ });
+ folderIds.push(yield PlacesUtils.promiseItemId(folder.guid));
+
+ for (let j = 0; j < 7; ++j) {
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: (yield PlacesUtils.promiseItemGuid(folderIds[i])),
+ url: `http://Bookmark${i}_${j}.com`,
+ title: ""
+ });
+ bookmarkIds.push(yield PlacesUtils.promiseItemId(bm.guid));
+ }
+ }
+
+ // using queryStringToQueries
+ let query = {};
+ let options = {};
+ let maxResults = 20;
+ let queryString = "place:" + folderIds.map((id) => {
+ return "folder=" + id;
+ }).join('&') + "&sort=5&maxResults=" + maxResults;
+ PlacesUtils.history.queryStringToQueries(queryString, query, {}, options);
+ let rootNode = PlacesUtils.history.executeQuery(query.value[0], options.value).root;
+ rootNode.containerOpen = true;
+ let resultLength = rootNode.childCount;
+ Assert.equal(resultLength, maxResults);
+ for (let i = 0; i < resultLength; ++i) {
+ let node = rootNode.getChild(i);
+ Assert.equal(bookmarkIds[i], node.itemId, node.uri);
+ }
+ rootNode.containerOpen = false;
+
+ // using getNewQuery and getNewQueryOptions
+ query = PlacesUtils.history.getNewQuery();
+ options = PlacesUtils.history.getNewQueryOptions();
+ query.setFolders(folderIds, folderIds.length);
+ options.sortingMode = options.SORT_BY_URI_ASCENDING;
+ options.maxResults = maxResults;
+ rootNode = PlacesUtils.history.executeQuery(query, options).root;
+ rootNode.containerOpen = true;
+ resultLength = rootNode.childCount;
+ Assert.equal(resultLength, maxResults);
+ for (let i = 0; i < resultLength; ++i) {
+ let node = rootNode.getChild(i);
+ Assert.equal(bookmarkIds[i], node.itemId, node.uri);
+ }
+ rootNode.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_querySerialization.js b/toolkit/components/places/tests/queries/test_querySerialization.js
new file mode 100644
index 0000000000..24cf8aa9b6
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_querySerialization.js
@@ -0,0 +1,797 @@
+/* -*- 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 Places query serialization. Associated bug is
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=370197
+ *
+ * The simple idea behind this test is to try out different combinations of
+ * query switches and ensure that queries are the same before serialization
+ * as they are after de-serialization.
+ *
+ * In the code below, "switch" refers to a query option -- "option" in a broad
+ * sense, not nsINavHistoryQueryOptions specifically (which is why we refer to
+ * them as switches, not options). Both nsINavHistoryQuery and
+ * nsINavHistoryQueryOptions allow you to specify switches that affect query
+ * strings. nsINavHistoryQuery instances have attributes hasBeginTime,
+ * hasEndTime, hasSearchTerms, and so on. nsINavHistoryQueryOptions instances
+ * have attributes sortingMode, resultType, excludeItems, etc.
+ *
+ * Ideally we would like to test all 2^N subsets of switches, where N is the
+ * total number of switches; switches might interact in erroneous or other ways
+ * we do not expect. However, since N is large (21 at this time), that's
+ * impractical for a single test in a suite.
+ *
+ * Instead we choose all possible subsets of a certain, smaller size. In fact
+ * we begin by choosing CHOOSE_HOW_MANY_SWITCHES_LO and ramp up to
+ * CHOOSE_HOW_MANY_SWITCHES_HI.
+ *
+ * There are two more wrinkles. First, for some switches we'd like to be able to
+ * test multiple values. For example, it seems like a good idea to test both an
+ * empty string and a non-empty string for switch nsINavHistoryQuery.searchTerms.
+ * When switches have more than one value for a test run, we use the Cartesian
+ * product of their values to generate all possible combinations of values.
+ *
+ * Second, we need to also test serialization of multiple nsINavHistoryQuery
+ * objects at once. To do this, we remember the previous NUM_MULTIPLE_QUERIES
+ * queries we tested individually and then serialize them together. We do this
+ * each time we test an individual query. Thus the set of queries we test
+ * together loses one query and gains another each time.
+ *
+ * To summarize, here's how this test works:
+ *
+ * - For n = CHOOSE_HOW_MANY_SWITCHES_LO to CHOOSE_HOW_MANY_SWITCHES_HI:
+ * - From the total set of switches choose all possible subsets of size n.
+ * For each of those subsets s:
+ * - Collect the test runs of each switch in subset s and take their
+ * Cartesian product. For each sequence in the product:
+ * - Create nsINavHistoryQuery and nsINavHistoryQueryOptions objects
+ * with the chosen switches and test run values.
+ * - Serialize the query.
+ * - De-serialize and ensure that the de-serialized query objects equal
+ * the originals.
+ * - For each of the previous NUM_MULTIPLE_QUERIES
+ * nsINavHistoryQueryOptions objects o we created:
+ * - Serialize the previous NUM_MULTIPLE_QUERIES nsINavHistoryQuery
+ * objects together with o.
+ * - De-serialize and ensure that the de-serialized query objects
+ * equal the originals.
+ */
+
+const CHOOSE_HOW_MANY_SWITCHES_LO = 1;
+const CHOOSE_HOW_MANY_SWITCHES_HI = 2;
+
+const NUM_MULTIPLE_QUERIES = 2;
+
+// The switches are represented by objects below, in arrays querySwitches and
+// queryOptionSwitches. Use them to set up test runs.
+//
+// Some switches have special properties (where noted), but all switches must
+// have the following properties:
+//
+// matches: A function that takes two nsINavHistoryQuery objects (in the case
+// of nsINavHistoryQuery switches) or two nsINavHistoryQueryOptions
+// objects (for nsINavHistoryQueryOptions switches) and returns true
+// if the values of the switch in the two objects are equal. This is
+// the foundation of how we determine if two queries are equal.
+// runs: An array of functions. Each function takes an nsINavHistoryQuery
+// object and an nsINavHistoryQueryOptions object. The functions
+// should set the attributes of one of the two objects as appropriate
+// to their switches. This is how switch values are set for each test
+// run.
+//
+// The following properties are optional:
+//
+// desc: An informational string to print out during runs when the switch
+// is chosen. Hopefully helpful if the test fails.
+
+// nsINavHistoryQuery switches
+const querySwitches = [
+ // hasBeginTime
+ {
+ // flag and subswitches are used by the flagSwitchMatches function. Several
+ // of the nsINavHistoryQuery switches (like this one) are really guard flags
+ // that indicate if other "subswitches" are enabled.
+ flag: "hasBeginTime",
+ subswitches: ["beginTime", "beginTimeReference", "absoluteBeginTime"],
+ desc: "nsINavHistoryQuery.hasBeginTime",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.beginTime = Date.now() * 1000;
+ aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.beginTime = Date.now() * 1000;
+ aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
+ }
+ ]
+ },
+ // hasEndTime
+ {
+ flag: "hasEndTime",
+ subswitches: ["endTime", "endTimeReference", "absoluteEndTime"],
+ desc: "nsINavHistoryQuery.hasEndTime",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.endTime = Date.now() * 1000;
+ aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.endTime = Date.now() * 1000;
+ aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
+ }
+ ]
+ },
+ // hasSearchTerms
+ {
+ flag: "hasSearchTerms",
+ subswitches: ["searchTerms"],
+ desc: "nsINavHistoryQuery.hasSearchTerms",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.searchTerms = "shrimp and white wine";
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.searchTerms = "";
+ }
+ ]
+ },
+ // hasDomain
+ {
+ flag: "hasDomain",
+ subswitches: ["domain", "domainIsHost"],
+ desc: "nsINavHistoryQuery.hasDomain",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "mozilla.com";
+ aQuery.domainIsHost = false;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "www.mozilla.com";
+ aQuery.domainIsHost = true;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "";
+ }
+ ]
+ },
+ // hasUri
+ {
+ flag: "hasUri",
+ subswitches: ["uri"],
+ desc: "nsINavHistoryQuery.hasUri",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.uri = uri("http://mozilla.com");
+ },
+ ]
+ },
+ // hasAnnotation
+ {
+ flag: "hasAnnotation",
+ subswitches: ["annotation", "annotationIsNot"],
+ desc: "nsINavHistoryQuery.hasAnnotation",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.annotation = "bookmarks/toolbarFolder";
+ aQuery.annotationIsNot = false;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.annotation = "bookmarks/toolbarFolder";
+ aQuery.annotationIsNot = true;
+ }
+ ]
+ },
+ // minVisits
+ {
+ // property is used by function simplePropertyMatches.
+ property: "minVisits",
+ desc: "nsINavHistoryQuery.minVisits",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.minVisits = 0x7fffffff; // 2^31 - 1
+ }
+ ]
+ },
+ // maxVisits
+ {
+ property: "maxVisits",
+ desc: "nsINavHistoryQuery.maxVisits",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.maxVisits = 0x7fffffff; // 2^31 - 1
+ }
+ ]
+ },
+ // onlyBookmarked
+ {
+ property: "onlyBookmarked",
+ desc: "nsINavHistoryQuery.onlyBookmarked",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.onlyBookmarked = true;
+ }
+ ]
+ },
+ // getFolders
+ {
+ desc: "nsINavHistoryQuery.getFolders",
+ matches: function (aQuery1, aQuery2) {
+ var q1Folders = aQuery1.getFolders();
+ var q2Folders = aQuery2.getFolders();
+ if (q1Folders.length !== q2Folders.length)
+ return false;
+ for (let i = 0; i < q1Folders.length; i++) {
+ if (q2Folders.indexOf(q1Folders[i]) < 0)
+ return false;
+ }
+ for (let i = 0; i < q2Folders.length; i++) {
+ if (q1Folders.indexOf(q2Folders[i]) < 0)
+ return false;
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.setFolders([], 0);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setFolders([PlacesUtils.placesRootId], 1);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setFolders([PlacesUtils.placesRootId, PlacesUtils.tagsFolderId], 2);
+ }
+ ]
+ },
+ // tags
+ {
+ desc: "nsINavHistoryQuery.getTags",
+ matches: function (aQuery1, aQuery2) {
+ if (aQuery1.tagsAreNot !== aQuery2.tagsAreNot)
+ return false;
+ var q1Tags = aQuery1.tags;
+ var q2Tags = aQuery2.tags;
+ if (q1Tags.length !== q2Tags.length)
+ return false;
+ for (let i = 0; i < q1Tags.length; i++) {
+ if (q2Tags.indexOf(q1Tags[i]) < 0)
+ return false;
+ }
+ for (let i = 0; i < q2Tags.length; i++) {
+ if (q1Tags.indexOf(q2Tags[i]) < 0)
+ return false;
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [""];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ];
+ aQuery.tagsAreNot = true;
+ }
+ ]
+ },
+ // transitions
+ {
+ desc: "tests nsINavHistoryQuery.getTransitions",
+ matches: function (aQuery1, aQuery2) {
+ var q1Trans = aQuery1.getTransitions();
+ var q2Trans = aQuery2.getTransitions();
+ if (q1Trans.length !== q2Trans.length)
+ return false;
+ for (let i = 0; i < q1Trans.length; i++) {
+ if (q2Trans.indexOf(q1Trans[i]) < 0)
+ return false;
+ }
+ for (let i = 0; i < q2Trans.length; i++) {
+ if (q1Trans.indexOf(q2Trans[i]) < 0)
+ return false;
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([], 0);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD],
+ 1);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_TYPED,
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK], 2);
+ }
+ ]
+ },
+];
+
+// nsINavHistoryQueryOptions switches
+const queryOptionSwitches = [
+ // sortingMode
+ {
+ desc: "nsINavHistoryQueryOptions.sortingMode",
+ matches: function (aOptions1, aOptions2) {
+ if (aOptions1.sortingMode === aOptions2.sortingMode) {
+ switch (aOptions1.sortingMode) {
+ case aOptions1.SORT_BY_ANNOTATION_ASCENDING:
+ case aOptions1.SORT_BY_ANNOTATION_DESCENDING:
+ return aOptions1.sortingAnnotation === aOptions2.sortingAnnotation;
+ }
+ return true;
+ }
+ return false;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.sortingMode = aQueryOptions.SORT_BY_ANNOTATION_ASCENDING;
+ aQueryOptions.sortingAnnotation = "bookmarks/toolbarFolder";
+ }
+ ]
+ },
+ // resultType
+ {
+ // property is used by function simplePropertyMatches.
+ property: "resultType",
+ desc: "nsINavHistoryQueryOptions.resultType",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.resultType = aQueryOptions.RESULTS_AS_FULL_VISIT;
+ }
+ ]
+ },
+ // excludeItems
+ {
+ property: "excludeItems",
+ desc: "nsINavHistoryQueryOptions.excludeItems",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.excludeItems = true;
+ }
+ ]
+ },
+ // excludeQueries
+ {
+ property: "excludeQueries",
+ desc: "nsINavHistoryQueryOptions.excludeQueries",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.excludeQueries = true;
+ }
+ ]
+ },
+ // expandQueries
+ {
+ property: "expandQueries",
+ desc: "nsINavHistoryQueryOptions.expandQueries",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.expandQueries = true;
+ }
+ ]
+ },
+ // includeHidden
+ {
+ property: "includeHidden",
+ desc: "nsINavHistoryQueryOptions.includeHidden",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.includeHidden = true;
+ }
+ ]
+ },
+ // maxResults
+ {
+ property: "maxResults",
+ desc: "nsINavHistoryQueryOptions.maxResults",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.maxResults = 0xffffffff; // 2^32 - 1
+ }
+ ]
+ },
+ // queryType
+ {
+ property: "queryType",
+ desc: "nsINavHistoryQueryOptions.queryType",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_HISTORY;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_UNIFIED;
+ }
+ ]
+ },
+];
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback)
+{
+ if (aSequences.length === 0)
+ return 0;
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ var seqEltPtrs = aSequences.map(i => 0);
+
+ var numProds = 0;
+ var done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ let prod = [];
+ for (let i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ let seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0)
+ done = true;
+ }
+ else break;
+ }
+ }
+ return numProds;
+}
+
+/**
+ * Enumerates all the subsets in aSet of size aHowMany. There are
+ * C(aSet.length, aHowMany) such subsets. aCallback will be passed each subset
+ * as it is generated. Note that aSet and the subsets enumerated are -- even
+ * though they're arrays -- not sequences; the ordering of their elements is not
+ * important. Example:
+ *
+ * choose([1, 2, 3, 4], 2, callback);
+ * // callback is called C(4, 2) = 6 times with the following sets (arrays):
+ * // [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]
+ *
+ * @param aSet
+ * an array from which to choose elements, aSet.length > 0
+ * @param aHowMany
+ * the number of elements to choose, > 0 and <= aSet.length
+ * @return the total number of sets chosen
+ */
+function choose(aSet, aHowMany, aCallback)
+{
+ // ptrs = indices of the elements in aSet we're currently choosing
+ var ptrs = [];
+ for (let i = 0; i < aHowMany; i++) {
+ ptrs.push(i);
+ }
+
+ var numFound = 0;
+ var done = false;
+ while (!done) {
+ numFound++;
+ aCallback(ptrs.map(p => aSet[p]));
+
+ // The next subset to be chosen differs from the current one by just a
+ // single element. Determine which element that is. Advance the "rightmost"
+ // pointer to the "right" by one. If we move past the end of set, move the
+ // next non-adjacent rightmost pointer to the right by one, and reset all
+ // succeeding pointers so that they're adjacent to it. When all pointers
+ // are clustered all the way to the right, we're done.
+
+ // Advance the rightmost pointer.
+ ptrs[ptrs.length - 1]++;
+
+ // The rightmost pointer has gone past the end of set.
+ if (ptrs[ptrs.length - 1] >= aSet.length) {
+ // Find the next rightmost pointer that is not adjacent to the current one.
+ let si = aSet.length - 2; // aSet index
+ let pi = ptrs.length - 2; // ptrs index
+ while (pi >= 0 && ptrs[pi] === si) {
+ pi--;
+ si--;
+ }
+
+ // All pointers are adjacent and clustered all the way to the right.
+ if (pi < 0)
+ done = true;
+ else {
+ // pi = index of rightmost pointer with a gap between it and its
+ // succeeding pointer. Move it right and reset all succeeding pointers
+ // so that they're adjacent to it.
+ ptrs[pi]++;
+ for (let i = 0; i < ptrs.length - pi - 1; i++) {
+ ptrs[i + pi + 1] = ptrs[pi] + i + 1;
+ }
+ }
+ }
+ }
+ return numFound;
+}
+
+/**
+ * Convenience function for nsINavHistoryQuery switches that act as flags. This
+ * is attached to switch objects. See querySwitches array above.
+ *
+ * @param aQuery1
+ * an nsINavHistoryQuery object
+ * @param aQuery2
+ * another nsINavHistoryQuery object
+ * @return true if this switch is the same in both aQuery1 and aQuery2
+ */
+function flagSwitchMatches(aQuery1, aQuery2)
+{
+ if (aQuery1[this.flag] && aQuery2[this.flag]) {
+ for (let p in this.subswitches) {
+ if (p in aQuery1 && p in aQuery2) {
+ if (aQuery1[p] instanceof Ci.nsIURI) {
+ if (!aQuery1[p].equals(aQuery2[p]))
+ return false;
+ }
+ else if (aQuery1[p] !== aQuery2[p])
+ return false;
+ }
+ }
+ }
+ else if (aQuery1[this.flag] || aQuery2[this.flag])
+ return false;
+
+ return true;
+}
+
+/**
+ * Tests if aObj1 and aObj2 are equal. This function is general and may be used
+ * for either nsINavHistoryQuery or nsINavHistoryQueryOptions objects. aSwitches
+ * determines which set of switches is used for comparison. Pass in either
+ * querySwitches or queryOptionSwitches.
+ *
+ * @param aSwitches
+ * determines which set of switches applies to aObj1 and aObj2, either
+ * querySwitches or queryOptionSwitches
+ * @param aObj1
+ * an nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @param aObj2
+ * another nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @return true if aObj1 and aObj2 are equal
+ */
+function queryObjsEqual(aSwitches, aObj1, aObj2)
+{
+ for (let i = 0; i < aSwitches.length; i++) {
+ if (!aSwitches[i].matches(aObj1, aObj2))
+ return false;
+ }
+ return true;
+}
+
+/**
+ * This drives the test runs. See the comment at the top of this file.
+ *
+ * @param aHowManyLo
+ * the size of the switch subsets to start with
+ * @param aHowManyHi
+ * the size of the switch subsets to end with (inclusive)
+ */
+function runQuerySequences(aHowManyLo, aHowManyHi)
+{
+ var allSwitches = querySwitches.concat(queryOptionSwitches);
+ var prevQueries = [];
+ var prevOpts = [];
+
+ // Choose aHowManyLo switches up to aHowManyHi switches.
+ for (let howMany = aHowManyLo; howMany <= aHowManyHi; howMany++) {
+ let numIters = 0;
+ print("CHOOSING " + howMany + " SWITCHES");
+
+ // Choose all subsets of size howMany from allSwitches.
+ choose(allSwitches, howMany, function (chosenSwitches) {
+ print(numIters);
+ numIters++;
+
+ // Collect the runs.
+ // runs = [ [runs from switch 1], ..., [runs from switch howMany] ]
+ var runs = chosenSwitches.map(function (s) {
+ if (s.desc)
+ print(" " + s.desc);
+ return s.runs;
+ });
+
+ // cartProd(runs) => [
+ // [switch 1 run 1, switch 2 run 1, ..., switch howMany run 1 ],
+ // ...,
+ // [switch 1 run 1, switch 2 run 1, ..., switch howMany run N ],
+ // ..., ...,
+ // [switch 1 run N, switch 2 run N, ..., switch howMany run 1 ],
+ // ...,
+ // [switch 1 run N, switch 2 run N, ..., switch howMany run N ],
+ // ]
+ cartProd(runs, function (runSet) {
+ // Create a new query, apply the switches in runSet, and test it.
+ var query = PlacesUtils.history.getNewQuery();
+ var opts = PlacesUtils.history.getNewQueryOptions();
+ for (let i = 0; i < runSet.length; i++) {
+ runSet[i](query, opts);
+ }
+ serializeDeserialize([query], opts);
+
+ // Test the previous NUM_MULTIPLE_QUERIES queries together.
+ prevQueries.push(query);
+ prevOpts.push(opts);
+ if (prevQueries.length >= NUM_MULTIPLE_QUERIES) {
+ // We can serialize multiple nsINavHistoryQuery objects together but
+ // only one nsINavHistoryQueryOptions object with them. So, test each
+ // of the previous NUM_MULTIPLE_QUERIES nsINavHistoryQueryOptions.
+ for (let i = 0; i < prevOpts.length; i++) {
+ serializeDeserialize(prevQueries, prevOpts[i]);
+ }
+ prevQueries.shift();
+ prevOpts.shift();
+ }
+ });
+ });
+ }
+ print("\n");
+}
+
+/**
+ * Serializes the nsINavHistoryQuery objects in aQueryArr and the
+ * nsINavHistoryQueryOptions object aQueryOptions, de-serializes the
+ * serialization, and ensures (using do_check_* functions) that the
+ * de-serialized objects equal the originals.
+ *
+ * @param aQueryArr
+ * an array containing nsINavHistoryQuery objects
+ * @param aQueryOptions
+ * an nsINavHistoryQueryOptions object
+ */
+function serializeDeserialize(aQueryArr, aQueryOptions)
+{
+ var queryStr = PlacesUtils.history.queriesToQueryString(aQueryArr,
+ aQueryArr.length,
+ aQueryOptions);
+ print(" " + queryStr);
+ var queryArr2 = {};
+ var opts2 = {};
+ PlacesUtils.history.queryStringToQueries(queryStr, queryArr2, {}, opts2);
+ queryArr2 = queryArr2.value;
+ opts2 = opts2.value;
+
+ // The two sets of queries cannot be the same if their lengths differ.
+ do_check_eq(aQueryArr.length, queryArr2.length);
+
+ // Although the query serialization code as it is written now practically
+ // ensures that queries appear in the query string in the same order they
+ // appear in both the array to be serialized and the array resulting from
+ // de-serialization, the interface does not guarantee any ordering. So, for
+ // each query in aQueryArr, find its equivalent in queryArr2 and delete it
+ // from queryArr2. If queryArr2 is empty after looping through aQueryArr,
+ // the two sets of queries are equal.
+ for (let i = 0; i < aQueryArr.length; i++) {
+ let j = 0;
+ for (; j < queryArr2.length; j++) {
+ if (queryObjsEqual(querySwitches, aQueryArr[i], queryArr2[j]))
+ break;
+ }
+ if (j < queryArr2.length)
+ queryArr2.splice(j, 1);
+ }
+ do_check_eq(queryArr2.length, 0);
+
+ // Finally check the query options objects.
+ do_check_true(queryObjsEqual(queryOptionSwitches, aQueryOptions, opts2));
+}
+
+/**
+ * Convenience function for switches that have simple values. This is attached
+ * to switch objects. See querySwitches and queryOptionSwitches arrays above.
+ *
+ * @param aObj1
+ * an nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @param aObj2
+ * another nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @return true if this switch is the same in both aObj1 and aObj2
+ */
+function simplePropertyMatches(aObj1, aObj2)
+{
+ return aObj1[this.property] === aObj2[this.property];
+}
+
+function run_test()
+{
+ runQuerySequences(CHOOSE_HOW_MANY_SWITCHES_LO, CHOOSE_HOW_MANY_SWITCHES_HI);
+}
diff --git a/toolkit/components/places/tests/queries/test_redirects.js b/toolkit/components/places/tests/queries/test_redirects.js
new file mode 100644
index 0000000000..1be5a626fe
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_redirects.js
@@ -0,0 +1,311 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Array of visits we will add to the database, will be populated later
+// in the test.
+var visits = [];
+
+/**
+ * Takes a sequence of query options, and compare query results obtained through
+ * them with a custom filtered array of visits, based on the values we are
+ * expecting from the query.
+ *
+ * @param aSequence
+ * an array that contains query options in the form:
+ * [includeHidden, maxResults, sortingMode]
+ */
+function check_results_callback(aSequence) {
+ // Sanity check: we should receive 3 parameters.
+ do_check_eq(aSequence.length, 3);
+ let includeHidden = aSequence[0];
+ let maxResults = aSequence[1];
+ let sortingMode = aSequence[2];
+ print("\nTESTING: includeHidden(" + includeHidden + ")," +
+ " maxResults(" + maxResults + ")," +
+ " sortingMode(" + sortingMode + ").");
+
+ function isHidden(aVisit) {
+ return aVisit.transType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
+ aVisit.isRedirect;
+ }
+
+ // Build expectedData array.
+ let expectedData = visits.filter(function (aVisit, aIndex, aArray) {
+ // Embed visits never appear in results.
+ if (aVisit.transType == Ci.nsINavHistoryService.TRANSITION_EMBED)
+ return false;
+
+ if (!includeHidden && isHidden(aVisit)) {
+ // If the page has any non-hidden visit, then it's visible.
+ if (visits.filter(function (refVisit) {
+ return refVisit.uri == aVisit.uri && !isHidden(refVisit);
+ }).length == 0)
+ return false;
+ }
+
+ return true;
+ });
+
+ // Remove duplicates, since queries are RESULTS_AS_URI (unique pages).
+ let seen = [];
+ expectedData = expectedData.filter(function (aData) {
+ if (seen.includes(aData.uri)) {
+ return false;
+ }
+ seen.push(aData.uri);
+ return true;
+ });
+
+ // Sort expectedData.
+ function getFirstIndexFor(aEntry) {
+ for (let i = 0; i < visits.length; i++) {
+ if (visits[i].uri == aEntry.uri)
+ return i;
+ }
+ return undefined;
+ }
+ function comparator(a, b) {
+ if (sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING) {
+ return b.lastVisit - a.lastVisit;
+ }
+ if (sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING) {
+ return b.visitCount - a.visitCount;
+ }
+ return getFirstIndexFor(a) - getFirstIndexFor(b);
+ }
+ expectedData.sort(comparator);
+
+ // Crop results to maxResults if it's defined.
+ if (maxResults) {
+ expectedData = expectedData.slice(0, maxResults);
+ }
+
+ // Create a new query with required options.
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.includeHidden = includeHidden;
+ options.sortingMode = sortingMode;
+ if (maxResults)
+ options.maxResults = maxResults;
+
+ // Compare resultset with expectedData.
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(expectedData, root);
+ root.containerOpen = false;
+}
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback)
+{
+ if (aSequences.length === 0)
+ return 0;
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ let seqEltPtrs = aSequences.map(i => 0);
+
+ let numProds = 0;
+ let done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ let prod = [];
+ for (let i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ let seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0)
+ done = true;
+ }
+ else break;
+ }
+ }
+ return numProds;
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+/**
+ * Populate the visits array and add visits to the database.
+ * We will generate visit-chains like:
+ * visit -> redirect_temp -> redirect_perm
+ */
+add_task(function* test_add_visits_to_database()
+{
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ // We don't really bother on this, but we need a time to add visits.
+ let timeInMicroseconds = Date.now() * 1000;
+ let visitCount = 1;
+
+ // Array of all possible transition types we could be redirected from.
+ let t = [
+ Ci.nsINavHistoryService.TRANSITION_LINK,
+ Ci.nsINavHistoryService.TRANSITION_TYPED,
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ // Embed visits are not added to the database and we don't want redirects
+ // to them, thus just avoid addition.
+ // Ci.nsINavHistoryService.TRANSITION_EMBED,
+ Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
+ // Would make hard sorting by visit date because last_visit_date is actually
+ // calculated excluding download transitions, but the query includes
+ // downloads.
+ // TODO: Bug 488966 could fix this behavior.
+ //Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ ];
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds - 1000;
+ return timeInMicroseconds;
+ }
+
+ // we add a visit for each of the above transition types.
+ t.forEach(transition => visits.push(
+ { isVisit: true,
+ transType: transition,
+ uri: "http://" + transition + ".example.com/",
+ title: transition + "-example",
+ isRedirect: true,
+ lastVisit: newTimeInMicroseconds(),
+ visitCount: (transition == Ci.nsINavHistoryService.TRANSITION_EMBED ||
+ transition == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK) ? 0 : visitCount++,
+ isInQuery: true }));
+
+ // Add a REDIRECT_TEMPORARY layer of visits for each of the above visits.
+ t.forEach(transition => visits.push(
+ { isVisit: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
+ uri: "http://" + transition + ".redirect.temp.example.com/",
+ title: transition + "-redirect-temp-example",
+ lastVisit: newTimeInMicroseconds(),
+ isRedirect: true,
+ referrer: "http://" + transition + ".example.com/",
+ visitCount: visitCount++,
+ isInQuery: true }));
+
+ // Add a REDIRECT_PERMANENT layer of visits for each of the above redirects.
+ t.forEach(transition => visits.push(
+ { isVisit: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+ uri: "http://" + transition + ".redirect.perm.example.com/",
+ title: transition + "-redirect-perm-example",
+ lastVisit: newTimeInMicroseconds(),
+ isRedirect: true,
+ referrer: "http://" + transition + ".redirect.temp.example.com/",
+ visitCount: visitCount++,
+ isInQuery: true }));
+
+ // Add a REDIRECT_PERMANENT layer of visits that loop to the first visit.
+ // These entries should not change visitCount or lastVisit, otherwise
+ // guessing an order would be a nightmare.
+ function getLastValue(aURI, aProperty) {
+ for (let i = 0; i < visits.length; i++) {
+ if (visits[i].uri == aURI) {
+ return visits[i][aProperty];
+ }
+ }
+ do_throw("Unknown uri.");
+ return null;
+ }
+ t.forEach(transition => visits.push(
+ { isVisit: true,
+ transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+ uri: "http://" + transition + ".example.com/",
+ title: getLastValue("http://" + transition + ".example.com/", "title"),
+ lastVisit: getLastValue("http://" + transition + ".example.com/", "lastVisit"),
+ isRedirect: true,
+ referrer: "http://" + transition + ".redirect.perm.example.com/",
+ visitCount: getLastValue("http://" + transition + ".example.com/", "visitCount"),
+ isInQuery: true }));
+
+ // Add an unvisited bookmark in the database, it should never appear.
+ visits.push({ isBookmark: true,
+ uri: "http://unvisited.bookmark.com/",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "Unvisited Bookmark",
+ isInQuery: false });
+
+ // Put visits in the database.
+ yield task_populateDB(visits);
+});
+
+add_task(function* test_redirects()
+{
+ // Frecency and hidden are updated asynchronously, wait for them.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // This array will be used by cartProd to generate a matrix of all possible
+ // combinations.
+ let includeHidden_options = [true, false];
+ let maxResults_options = [5, 10, 20, null];
+ // These sortingMode are choosen to toggle using special queries for history
+ // menu and most visited smart bookmark.
+ let sorting_options = [Ci.nsINavHistoryQueryOptions.SORT_BY_NONE,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING];
+ // Will execute check_results_callback() for each generated combination.
+ cartProd([includeHidden_options, maxResults_options, sorting_options],
+ check_results_callback);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js b/toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js
new file mode 100644
index 0000000000..f1cbfd4d82
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_results-as-tag-contents-query.js
@@ -0,0 +1,127 @@
+/* -*- 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 testData = [
+ { isInQuery: true,
+ isDetails: true,
+ title: "bmoz",
+ uri: "http://foo.com/",
+ isBookmark: true,
+ isTag: true,
+ tagArray: ["bugzilla"] },
+
+ { isInQuery: true,
+ isDetails: true,
+ title: "C Moz",
+ uri: "http://foo.com/changeme1.html",
+ isBookmark: true,
+ isTag: true,
+ tagArray: ["moz", "bugzilla"] },
+
+ { isInQuery: false,
+ isDetails: true,
+ title: "amo",
+ uri: "http://foo2.com/",
+ isBookmark: true,
+ isTag: true,
+ tagArray: ["moz"] },
+
+ { isInQuery: false,
+ isDetails: true,
+ title: "amo",
+ uri: "http://foo.com/changeme2.html",
+ isBookmark: true },
+];
+
+function getIdForTag(aTagName) {
+ var id = -1;
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.tagsFolderId], 1);
+ var options = PlacesUtils.history.getNewQueryOptions();
+ var root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ do_check_eq(root.childCount, 2);
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ if (node.title == aTagName) {
+ id = node.itemId;
+ break;
+ }
+ }
+ root.containerOpen = false;
+ return id;
+}
+
+ /**
+ * This test will test Queries that use relative search terms and URI options
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_results_as_tag_contents_query()
+{
+ yield task_populateDB(testData);
+
+ // Get tag id.
+ let tagId = getIdForTag("bugzilla");
+ do_check_true(tagId > 0);
+
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_TAG_CONTENTS;
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([tagId], 1);
+
+ var root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ displayResultSet(root);
+ // Cannot use compare array to results, since results ordering is hardcoded
+ // and depending on lastModified (that could have VM timers issues).
+ testData.forEach(function(aEntry) {
+ if (aEntry.isInResult)
+ do_check_true(isInResult({uri: "http://foo.com/added.html"}, root));
+ });
+
+ // If that passes, check liveupdate
+ // Add to the query set
+ var change1 = { isVisit: true,
+ isDetails: true,
+ uri: "http://foo.com/added.html",
+ title: "mozadded",
+ isBookmark: true,
+ isTag: true,
+ tagArray: ["moz", "bugzilla"] };
+ do_print("Adding item to query");
+ yield task_populateDB([change1]);
+ do_print("These results should have been LIVE UPDATED with the new addition");
+ displayResultSet(root);
+ do_check_true(isInResult(change1, root));
+
+ // Add one by adding a tag, remove one by removing search term.
+ do_print("Updating items");
+ var change2 = [{ isDetails: true,
+ uri: "http://foo3.com/",
+ title: "foo"},
+ { isDetails: true,
+ uri: "http://foo.com/changeme2.html",
+ title: "zydeco",
+ isBookmark:true,
+ isTag: true,
+ tagArray: ["bugzilla", "moz"] }];
+ yield task_populateDB(change2);
+ do_check_false(isInResult({uri: "http://fooz.com/"}, root));
+ do_check_true(isInResult({uri: "http://foo.com/changeme2.html"}, root));
+
+ // Test removing a tag updates us.
+ do_print("Deleting item");
+ PlacesUtils.tagging.untagURI(uri("http://foo.com/changeme2.html"), ["bugzilla"]);
+ do_check_false(isInResult({uri: "http://foo.com/changeme2.html"}, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_results-as-visit.js b/toolkit/components/places/tests/queries/test_results-as-visit.js
new file mode 100644
index 0000000000..d0f270bd20
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_results-as-visit.js
@@ -0,0 +1,119 @@
+/* -*- 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 testData = [];
+var timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+}
+
+function createTestData() {
+ function generateVisits(aPage) {
+ for (var i = 0; i < aPage.visitCount; i++) {
+ testData.push({ isInQuery: aPage.inQuery,
+ isVisit: true,
+ title: aPage.title,
+ uri: aPage.uri,
+ lastVisit: newTimeInMicroseconds(),
+ isTag: aPage.tags && aPage.tags.length > 0,
+ tagArray: aPage.tags });
+ }
+ }
+
+ var pages = [
+ { uri: "http://foo.com/", title: "amo", tags: ["moz"], visitCount: 3, inQuery: true },
+ { uri: "http://moilla.com/", title: "bMoz", tags: ["bugzilla"], visitCount: 5, inQuery: true },
+ { uri: "http://foo.mail.com/changeme1.html", title: "c Moz", visitCount: 7, inQuery: true },
+ { uri: "http://foo.mail.com/changeme2.html", tags: ["moz"], title: "", visitCount: 1, inQuery: false },
+ { uri: "http://foo.mail.com/changeme3.html", title: "zydeco", visitCount: 5, inQuery: false },
+ ];
+ pages.forEach(generateVisits);
+}
+
+/**
+ * This test will test Queries that use relative search terms and URI options
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_results_as_visit()
+{
+ createTestData();
+ yield task_populateDB(testData);
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ query.minVisits = 2;
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_ASCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_print("Number of items in result set: " + root.childCount);
+ for (let i=0; i < root.childCount; ++i) {
+ do_print("result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title);
+ }
+
+ // Check our inital result set
+ compareArrayToResult(testData, root);
+
+ // If that passes, check liveupdate
+ // Add to the query set
+ do_print("Adding item to query")
+ var tmp = [];
+ for (let i=0; i < 2; i++) {
+ tmp.push({ isVisit: true,
+ uri: "http://foo.com/added.html",
+ title: "ab moz" });
+ }
+ yield task_populateDB(tmp);
+ for (let i=0; i < 2; i++)
+ do_check_eq(root.getChild(i).title, "ab moz");
+
+ // Update an existing URI
+ do_print("Updating Item");
+ var change2 = [{ isVisit: true,
+ title: "moz",
+ uri: "http://foo.mail.com/changeme2.html" }];
+ yield task_populateDB(change2);
+ do_check_true(isInResult(change2, root));
+
+ // Update some visits - add one and take one out of query set, and simply
+ // change one so that it still applies to the query.
+ do_print("Updating More Items");
+ var change3 = [{ isVisit: true,
+ lastVisit: newTimeInMicroseconds(),
+ uri: "http://foo.mail.com/changeme1.html",
+ title: "foo"},
+ { isVisit: true,
+ lastVisit: newTimeInMicroseconds(),
+ uri: "http://foo.mail.com/changeme3.html",
+ title: "moz",
+ isTag: true,
+ tagArray: ["foo", "moz"] }];
+ yield task_populateDB(change3);
+ do_check_false(isInResult({uri: "http://foo.mail.com/changeme1.html"}, root));
+ do_check_true(isInResult({uri: "http://foo.mail.com/changeme3.html"}, root));
+
+ // And now, delete one
+ do_print("Delete item outside of batch");
+ var change4 = [{ isVisit: true,
+ lastVisit: newTimeInMicroseconds(),
+ uri: "http://moilla.com/",
+ title: "mo,z" }];
+ yield task_populateDB(change4);
+ do_check_false(isInResult(change4, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js b/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js
new file mode 100644
index 0000000000..038367c0ba
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchTerms_includeHidden.js
@@ -0,0 +1,84 @@
+/* -*- 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 the interaction of includeHidden and searchTerms search options.
+
+var timeInMicroseconds = Date.now() * 1000;
+
+const VISITS = [
+ { isVisit: true,
+ transType: TRANSITION_TYPED,
+ uri: "http://redirect.example.com/",
+ title: "example",
+ isRedirect: true,
+ lastVisit: timeInMicroseconds--
+ },
+ { isVisit: true,
+ transType: TRANSITION_TYPED,
+ uri: "http://target.example.com/",
+ title: "example",
+ lastVisit: timeInMicroseconds--
+ }
+];
+
+const HIDDEN_VISITS = [
+ { isVisit: true,
+ transType: TRANSITION_FRAMED_LINK,
+ uri: "http://hidden.example.com/",
+ title: "red",
+ lastVisit: timeInMicroseconds--
+ },
+];
+
+const TEST_DATA = [
+ { searchTerms: "example",
+ includeHidden: true,
+ expectedResults: 2
+ },
+ { searchTerms: "example",
+ includeHidden: false,
+ expectedResults: 1
+ },
+ { searchTerms: "red",
+ includeHidden: true,
+ expectedResults: 1
+ }
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_initalize()
+{
+ yield task_populateDB(VISITS);
+});
+
+add_task(function* test_searchTerms_includeHidden()
+{
+ for (let data of TEST_DATA) {
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = data.searchTerms;
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.includeHidden = data.includeHidden;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ let cc = root.childCount;
+ // Live update with hidden visits.
+ yield task_populateDB(HIDDEN_VISITS);
+ let cc_update = root.childCount;
+
+ root.containerOpen = false;
+
+ do_check_eq(cc, data.expectedResults);
+ do_check_eq(cc_update, data.expectedResults + (data.includeHidden ? 1 : 0));
+
+ PlacesUtils.bhistory.removePage(uri("http://hidden.example.com/"));
+ }
+});
diff --git a/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js b/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js
new file mode 100644
index 0000000000..7bd91f057b
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchterms-bookmarklets.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that bookmarklets are returned by searches with searchTerms.
+
+var testData = [
+ { isInQuery: true
+ , isBookmark: true
+ , title: "bookmark 1"
+ , uri: "http://mozilla.org/script/"
+ },
+
+ { isInQuery: true
+ , isBookmark: true
+ , title: "bookmark 2"
+ , uri: "javascript:alert('moz');"
+ }
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_initalize()
+{
+ yield task_populateDB(testData);
+});
+
+add_test(function test_search_by_title()
+{
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "bookmark";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(testData, root);
+ root.containerOpen = false;
+
+ run_next_test();
+});
+
+add_test(function test_search_by_schemeToken()
+{
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "script";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(testData, root);
+ root.containerOpen = false;
+
+ run_next_test();
+});
+
+add_test(function test_search_by_uriAndTitle()
+{
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(testData, root);
+ root.containerOpen = false;
+
+ run_next_test();
+});
diff --git a/toolkit/components/places/tests/queries/test_searchterms-domain.js b/toolkit/components/places/tests/queries/test_searchterms-domain.js
new file mode 100644
index 0000000000..4f42e70005
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchterms-domain.js
@@ -0,0 +1,125 @@
+/* -*- 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/. */
+
+ // The test data for our database, note that the ordering of the results that
+ // will be returned by the query (the isInQuery: true objects) is IMPORTANT.
+ // see compareArrayToResult in head_queries.js for more info.
+ var testData = [
+ // Test ftp protocol - vary the title length, embed search term
+ {isInQuery: true, isVisit: true, isDetails: true,
+ uri: "ftp://foo.com/ftp", lastVisit: lastweek,
+ title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"},
+
+ // Test flat domain with annotation, search term in sentence
+ {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true,
+ uri: "http://foo.com/", annoName: "moz/test", annoVal: "val",
+ lastVisit: lastweek, title: "you know, moz is cool"},
+
+ // Test subdomain included with isRedirect=true, different transtype
+ {isInQuery: true, isVisit: true, isDetails: true, title: "amozzie",
+ isRedirect: true, uri: "http://mail.foo.com/redirect", lastVisit: old,
+ referrer: "http://myreferrer.com", transType: PlacesUtils.history.TRANSITION_LINK},
+
+ // Test subdomain inclued, search term at end
+ {isInQuery: true, isVisit: true, isDetails: true,
+ uri: "http://mail.foo.com/yiihah", title: "blahmoz", lastVisit: daybefore},
+
+ // Test www. style URI is included, with a tag
+ {isInQuery: true, isVisit: true, isDetails: true, isTag: true,
+ uri: "http://www.foo.com/yiihah", tagArray: ["moz"],
+ lastVisit: yesterday, title: "foo"},
+
+ // Test https protocol
+ {isInQuery: true, isVisit: true, isDetails: true, title: "moz",
+ uri: "https://foo.com/", lastVisit: today},
+
+ // Begin the invalid queries: wrong search term
+ {isInQuery: false, isVisit:true, isDetails: true, title: "m o z",
+ uri: "http://foo.com/tooearly.php", lastVisit: today},
+
+ // Test bad URI
+ {isInQuery: false, isVisit:true, isDetails: true, title: "moz",
+ uri: "http://sffoo.com/justwrong.htm", lastVisit: yesterday},
+
+ // Test what we do with escaping in titles
+ {isInQuery: false, isVisit:true, isDetails: true, title: "m%0o%0z",
+ uri: "http://foo.com/changeme1.htm", lastVisit: yesterday},
+
+ // Test another invalid title - for updating later
+ {isInQuery: false, isVisit:true, isDetails: true, title: "m,oz",
+ uri: "http://foo.com/changeme2.htm", lastVisit: yesterday}];
+
+/**
+ * This test will test Queries that use relative search terms and domain options
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_searchterms_domain()
+{
+ yield task_populateDB(testData);
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ query.domain = "foo.com";
+ query.domainIsHost = false;
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_print("Number of items in result set: " + root.childCount);
+ for (var i=0; i < root.childCount; ++i) {
+ do_print("result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title);
+ }
+
+ // Check our inital result set
+ compareArrayToResult(testData, root);
+
+ // If that passes, check liveupdate
+ // Add to the query set
+ do_print("Adding item to query");
+ var change1 = [{isVisit: true, isDetails: true, uri: "http://foo.com/added.htm",
+ title: "moz", transType: PlacesUtils.history.TRANSITION_LINK}];
+ yield task_populateDB(change1);
+ do_check_true(isInResult(change1, root));
+
+ // Update an existing URI
+ do_print("Updating Item");
+ var change2 = [{isDetails: true, uri: "http://foo.com/changeme1.htm",
+ title: "moz" }];
+ yield task_populateDB(change2);
+ do_check_true(isInResult(change2, root));
+
+ // Add one and take one out of query set, and simply change one so that it
+ // still applies to the query.
+ do_print("Updating More Items");
+ var change3 = [{isDetails: true, uri:"http://foo.com/changeme2.htm",
+ title: "moz"},
+ {isDetails: true, uri: "http://mail.foo.com/yiihah",
+ title: "moz now updated"},
+ {isDetails: true, uri: "ftp://foo.com/ftp", title: "gone"}];
+ yield task_populateDB(change3);
+ do_check_true(isInResult({uri: "http://foo.com/changeme2.htm"}, root));
+ do_check_true(isInResult({uri: "http://mail.foo.com/yiihah"}, root));
+ do_check_false(isInResult({uri: "ftp://foo.com/ftp"}, root));
+
+ // And now, delete one
+ do_print("Deleting items");
+ var change4 = [{isDetails: true, uri: "https://foo.com/",
+ title: "mo,z"}];
+ yield task_populateDB(change4);
+ do_check_false(isInResult(change4, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_searchterms-uri.js b/toolkit/components/places/tests/queries/test_searchterms-uri.js
new file mode 100644
index 0000000000..af4efe196e
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_searchterms-uri.js
@@ -0,0 +1,87 @@
+/* -*- 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/. */
+
+ // The test data for our database, note that the ordering of the results that
+ // will be returned by the query (the isInQuery: true objects) is IMPORTANT.
+ // see compareArrayToResult in head_queries.js for more info.
+ var testData = [
+ // Test flat domain with annotation, search term in sentence
+ {isInQuery: true, isVisit: true, isDetails: true, isPageAnnotation: true,
+ uri: "http://foo.com/", annoName: "moz/test", annoVal: "val",
+ lastVisit: lastweek, title: "you know, moz is cool"},
+
+ // Test https protocol
+ {isInQuery: false, isVisit: true, isDetails: true, title: "moz",
+ uri: "https://foo.com/", lastVisit: today},
+
+ // Begin the invalid queries: wrong search term
+ {isInQuery: false, isVisit:true, isDetails: true, title: "m o z",
+ uri: "http://foo.com/wrongsearch.php", lastVisit: today},
+
+ // Test subdomain inclued, search term at end
+ {isInQuery: false, isVisit: true, isDetails: true,
+ uri: "http://mail.foo.com/yiihah", title: "blahmoz", lastVisit: daybefore},
+
+ // Test ftp protocol - vary the title length, embed search term
+ {isInQuery: false, isVisit: true, isDetails: true,
+ uri: "ftp://foo.com/ftp", lastVisit: lastweek,
+ title: "hugelongconfmozlagurationofwordswithasearchtermsinit whoo-hoo"},
+
+ // Test what we do with escaping in titles
+ {isInQuery: false, isVisit:true, isDetails: true, title: "m%0o%0z",
+ uri: "http://foo.com/changeme1.htm", lastVisit: yesterday},
+
+ // Test another invalid title - for updating later
+ {isInQuery: false, isVisit:true, isDetails: true, title: "m,oz",
+ uri: "http://foo.com/changeme2.htm", lastVisit: yesterday}];
+
+/**
+ * This test will test Queries that use relative search terms and URI options
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_searchterms_uri()
+{
+ yield task_populateDB(testData);
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = "moz";
+ query.uri = uri("http://foo.com");
+
+ // Options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_ASCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+
+ // Results
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_print("Number of items in result set: " + root.childCount);
+ for (var i=0; i < root.childCount; ++i) {
+ do_print("result: " + root.getChild(i).uri + " Title: " + root.getChild(i).title);
+ }
+
+ // Check our inital result set
+ compareArrayToResult(testData, root);
+
+ // live update.
+ do_print("change title");
+ var change1 = [{isDetails: true, uri:"http://foo.com/",
+ title: "mo"}, ];
+ yield task_populateDB(change1);
+
+ do_check_false(isInResult({uri: "http://foo.com/"}, root));
+ var change2 = [{isDetails: true, uri:"http://foo.com/",
+ title: "moz"}, ];
+ yield task_populateDB(change2);
+ do_check_true(isInResult({uri: "http://foo.com/"}, root));
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js b/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js
new file mode 100644
index 0000000000..7ca50e6de3
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_sort-date-site-grouping.js
@@ -0,0 +1,225 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ * ***** END LICENSE BLOCK ***** */
+
+// This test ensures that the date and site type of |place:| query maintains
+// its quantifications correctly. Namely, it ensures that the date part of the
+// query is not lost when the domain queries are made.
+
+// We specifically craft these entries so that if a by Date and Site sorting is
+// applied, we find one domain in the today range, and two domains in the older
+// than six months range.
+// The correspondence between item in |testData| and date range is stored in
+// leveledTestData.
+var testData = [
+ {
+ isVisit: true,
+ uri: "file:///directory/1",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/1",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/2",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true
+ },
+ {
+ isVisit: true,
+ uri: "file:///directory/2",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/3",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/4",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true
+ },
+ {
+ isVisit: true,
+ uri: "http://example.net/1",
+ lastVisit: olderthansixmonths + 1000,
+ title: "test visit",
+ isInQuery: true
+ }
+];
+var domainsInRange = [2, 3];
+var leveledTestData = [// Today
+ [[0], // Today, local files
+ [1, 2]], // Today, example.com
+ // Older than six months
+ [[3], // Older than six months, local files
+ [4, 5], // Older than six months, example.com
+ [6] // Older than six months, example.net
+ ]];
+
+// This test data is meant for live updating. The |levels| property indicates
+// date range index and then domain index.
+var testDataAddedLater = [
+ {
+ isVisit: true,
+ uri: "http://example.com/5",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ levels: [1, 1]
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/6",
+ lastVisit: olderthansixmonths,
+ title: "test visit",
+ isInQuery: true,
+ levels: [1, 1]
+ },
+ {
+ isVisit: true,
+ uri: "http://example.com/7",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ levels: [0, 1]
+ },
+ {
+ isVisit: true,
+ uri: "file:///directory/3",
+ lastVisit: today,
+ title: "test visit",
+ isInQuery: true,
+ levels: [0, 0]
+ }
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_sort_date_site_grouping()
+{
+ yield task_populateDB(testData);
+
+ // On Linux, the (local files) folder is shown after sites unlike Mac/Windows.
+ // Thus, we avoid running this test on Linux but this should be re-enabled
+ // after bug 624024 is resolved.
+ let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Components.classes);
+ if (isLinux)
+ return;
+
+ // In this test, there are three levels of results:
+ // 1st: Date queries. e.g., today, last week, or older than 6 months.
+ // 2nd: Domain queries restricted to a date. e.g. mozilla.com today.
+ // 3rd: Actual visits. e.g. mozilla.com/index.html today.
+ //
+ // We store all the third level result roots so that we can easily close all
+ // containers and test live updating into specific results.
+ let roots = [];
+
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ // This corresponds to the number of date ranges.
+ do_check_eq(root.childCount, leveledTestData.length);
+
+ // We pass off to |checkFirstLevel| to check the first level of results.
+ for (let index = 0; index < leveledTestData.length; index++) {
+ let node = root.getChild(index);
+ checkFirstLevel(index, node, roots);
+ }
+
+ // Test live updating.
+ for (let visit of testDataAddedLater) {
+ yield task_populateDB([visit]);
+ let oldLength = testData.length;
+ let i = visit.levels[0];
+ let j = visit.levels[1];
+ testData.push(visit);
+ leveledTestData[i][j].push(oldLength);
+ compareArrayToResult(leveledTestData[i][j].
+ map(x => testData[x]), roots[i][j]);
+ }
+
+ for (let i = 0; i < roots.length; i++) {
+ for (let j = 0; j < roots[i].length; j++)
+ roots[i][j].containerOpen = false;
+ }
+
+ root.containerOpen = false;
+});
+
+function checkFirstLevel(index, node, roots) {
+ PlacesUtils.asContainer(node).containerOpen = true;
+
+ do_check_true(PlacesUtils.nodeIsDay(node));
+ PlacesUtils.asQuery(node);
+ let queries = node.getQueries();
+ let options = node.queryOptions;
+
+ do_check_eq(queries.length, 1);
+ let query = queries[0];
+
+ do_check_true(query.hasBeginTime && query.hasEndTime);
+
+ // Here we check the second level of results.
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ roots.push([]);
+ root.containerOpen = true;
+
+ do_check_eq(root.childCount, leveledTestData[index].length);
+ for (var secondIndex = 0; secondIndex < root.childCount; secondIndex++) {
+ let child = PlacesUtils.asQuery(root.getChild(secondIndex));
+ checkSecondLevel(index, secondIndex, child, roots);
+ }
+ root.containerOpen = false;
+ node.containerOpen = false;
+}
+
+function checkSecondLevel(index, secondIndex, child, roots) {
+ let queries = child.getQueries();
+ let options = child.queryOptions;
+
+ do_check_eq(queries.length, 1);
+ let query = queries[0];
+
+ do_check_true(query.hasDomain);
+ do_check_true(query.hasBeginTime && query.hasEndTime);
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ // We should now have that roots[index][secondIndex] is set to the second
+ // level's results root.
+ roots[index].push(root);
+
+ // We pass off to compareArrayToResult to check the third level of
+ // results.
+ root.containerOpen = true;
+ compareArrayToResult(leveledTestData[index][secondIndex].
+ map(x => testData[x]), root);
+ // We close |root|'s container later so that we can test live
+ // updates into it.
+}
diff --git a/toolkit/components/places/tests/queries/test_sorting.js b/toolkit/components/places/tests/queries/test_sorting.js
new file mode 100644
index 0000000000..4d8e1146d3
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_sorting.js
@@ -0,0 +1,1265 @@
+/* -*- 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 tests = [];
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_NONE,
+
+ *setup() {
+ do_print("Sorting test 1: SORT BY NONE");
+
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://example.com/b",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y",
+ keyword: "b",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "z",
+ keyword: "a",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "x",
+ keyword: "c",
+ isInQuery: true },
+ ];
+
+ this._sortedData = this._unsortedData;
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ // no reverse sorting for SORT BY NONE
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 2: SORT BY TITLE");
+
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "z",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "x",
+ isInQuery: true },
+
+ // if titles are equal, should fall back to URI
+ { isBookmark: true,
+ uri: "http://example.com/b2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y",
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[2],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 3: SORT BY DATE");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ uri: "http://example.com/c1",
+ lastVisit: timeInMicroseconds - 2000,
+ title: "x1",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ uri: "http://example.com/a",
+ lastVisit: timeInMicroseconds - 1000,
+ title: "z",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ uri: "http://example.com/b",
+ lastVisit: timeInMicroseconds - 3000,
+ title: "y",
+ isInQuery: true },
+
+ // if dates are equal, should fall back to title
+ { isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ uri: "http://example.com/c2",
+ lastVisit: timeInMicroseconds - 2000,
+ title: "x2",
+ isInQuery: true },
+
+ // if dates and title are equal, should fall back to bookmark index
+ { isVisit: true,
+ isDetails: true,
+ isBookmark: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ uri: "http://example.com/c2",
+ lastVisit: timeInMicroseconds - 2000,
+ title: "x2",
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[2],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 4: SORT BY URI");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isBookmark: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds,
+ uri: "http://example.com/b",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ title: "y",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ title: "x",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ title: "z",
+ isInQuery: true },
+
+ // if URIs are equal, should fall back to date
+ { isBookmark: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds + 1000,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ title: "x",
+ isInQuery: true },
+
+ // if no URI (e.g., node is a folder), should fall back to title
+ { isFolder: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ title: "a",
+ isInQuery: true },
+
+ // if URIs and dates are equal, should fall back to bookmark index
+ { isBookmark: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds + 1000,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 5,
+ title: "x",
+ isInQuery: true },
+
+ // if no URI and titles are equal, should fall back to bookmark index
+ { isFolder: true,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 6,
+ title: "a",
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[4],
+ this._unsortedData[6],
+ this._unsortedData[2],
+ this._unsortedData[0],
+ this._unsortedData[1],
+ this._unsortedData[3],
+ this._unsortedData[5],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 5: SORT BY VISITCOUNT");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ lastVisit: timeInMicroseconds,
+ title: "z",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ lastVisit: timeInMicroseconds,
+ title: "x",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/b1",
+ lastVisit: timeInMicroseconds,
+ title: "y1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ isInQuery: true },
+
+ // if visitCounts are equal, should fall back to date
+ { isBookmark: true,
+ uri: "http://example.com/b2",
+ lastVisit: timeInMicroseconds + 1000,
+ title: "y2a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ isInQuery: true },
+
+ // if visitCounts and dates are equal, should fall back to bookmark index
+ { isBookmark: true,
+ uri: "http://example.com/b2",
+ lastVisit: timeInMicroseconds + 1000,
+ title: "y2b",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[0],
+ this._unsortedData[2],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ // add visits to increase visit count
+ yield PlacesTestUtils.addVisits([
+ { uri: uri("http://example.com/a"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
+ { uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
+ { uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
+ { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 },
+ { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 },
+ { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
+ { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
+ { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
+ ]);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 6: SORT BY KEYWORD");
+
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "z",
+ keyword: "a",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "x",
+ keyword: "c",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y9",
+ keyword: "b",
+ isInQuery: true },
+
+ // without a keyword, should fall back to title
+ { isBookmark: true,
+ uri: "http://example.com/null2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "null8",
+ keyword: null,
+ isInQuery: true },
+
+ // without a keyword, should fall back to title
+ { isBookmark: true,
+ uri: "http://example.com/null1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "null9",
+ keyword: null,
+ isInQuery: true },
+
+ // if keywords are equal, should fall back to title
+ { isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "y8",
+ keyword: "b",
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[0],
+ this._unsortedData[5],
+ this._unsortedData[2],
+ this._unsortedData[1],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 7: SORT BY DATEADDED");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ title: "y1",
+ dateAdded: timeInMicroseconds - 1000,
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ title: "z",
+ dateAdded: timeInMicroseconds - 2000,
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ title: "x",
+ dateAdded: timeInMicroseconds,
+ isInQuery: true },
+
+ // if dateAddeds are equal, should fall back to title
+ { isBookmark: true,
+ uri: "http://example.com/b2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ title: "y2",
+ dateAdded: timeInMicroseconds - 1000,
+ isInQuery: true },
+
+ // if dateAddeds and titles are equal, should fall back to bookmark index
+ { isBookmark: true,
+ uri: "http://example.com/b3",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ title: "y3",
+ dateAdded: timeInMicroseconds - 1000,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 8: SORT BY LASTMODIFIED");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ var timeAddedInMicroseconds = timeInMicroseconds - 10000;
+
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://example.com/b1",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ title: "y1",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 1000,
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/a",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ title: "z",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 2000,
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://example.com/c",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 2,
+ title: "x",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds,
+ isInQuery: true },
+
+ // if lastModifieds are equal, should fall back to title
+ { isBookmark: true,
+ uri: "http://example.com/b2",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 3,
+ title: "y2",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 1000,
+ isInQuery: true },
+
+ // if lastModifieds and titles are equal, should fall back to bookmark
+ // index
+ { isBookmark: true,
+ uri: "http://example.com/b3",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 4,
+ title: "y3",
+ dateAdded: timeAddedInMicroseconds,
+ lastModified: timeInMicroseconds - 1000,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[4],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 9: SORT BY TAGS");
+
+ this._unsortedData = [
+ { isBookmark: true,
+ uri: "http://url2.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title x",
+ isTag: true,
+ tagArray: ["x", "y", "z"],
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://url1a.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title y1",
+ isTag: true,
+ tagArray: ["a", "b"],
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://url3a.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title w1",
+ isInQuery: true },
+
+ { isBookmark: true,
+ uri: "http://url0.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title z",
+ isTag: true,
+ tagArray: ["a", "y", "z"],
+ isInQuery: true },
+
+ // if tags are equal, should fall back to title
+ { isBookmark: true,
+ uri: "http://url1b.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title y2",
+ isTag: true,
+ tagArray: ["b", "a"],
+ isInQuery: true },
+
+ // if tags are equal, should fall back to title
+ { isBookmark: true,
+ uri: "http://url3b.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ title: "title w2",
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[2],
+ this._unsortedData[5],
+ this._unsortedData[1],
+ this._unsortedData[4],
+ this._unsortedData[3],
+ this._unsortedData[0],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarks.toolbarFolder], 1);
+ query.onlyBookmarked = true;
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+// SORT_BY_ANNOTATION_* (int32)
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 10: SORT BY ANNOTATION (int32)");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isVisit: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds,
+ uri: "http://example.com/b1",
+ title: "y1",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 2,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds,
+ uri: "http://example.com/a",
+ title: "z",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 1,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds,
+ uri: "http://example.com/c",
+ title: "x",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 3,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ // if annotations are equal, should fall back to title
+ { isVisit: true,
+ isDetails: true,
+ lastVisit: timeInMicroseconds,
+ uri: "http://example.com/b2",
+ title: "y2",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 2,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[3],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingAnnotation = "sorting";
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+// SORT_BY_ANNOTATION_* (int64)
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 11: SORT BY ANNOTATION (int64)");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: timeInMicroseconds,
+ title: "I",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 0xffffffff1,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://is.com/",
+ lastVisit: timeInMicroseconds,
+ title: "love",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 0xffffffff0,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: timeInMicroseconds,
+ title: "moz",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 0xffffffff2,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingAnnotation = "sorting";
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+// SORT_BY_ANNOTATION_* (string)
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 12: SORT BY ANNOTATION (string)");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: timeInMicroseconds,
+ title: "I",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: "a",
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://is.com/",
+ lastVisit: timeInMicroseconds,
+ title: "love",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: "",
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: timeInMicroseconds,
+ title: "moz",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: "z",
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingAnnotation = "sorting";
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+// SORT_BY_ANNOTATION_* (double)
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 13: SORT BY ANNOTATION (double)");
+
+ var timeInMicroseconds = Date.now() * 1000;
+ this._unsortedData = [
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: timeInMicroseconds,
+ title: "I",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 1.2,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://is.com/",
+ lastVisit: timeInMicroseconds,
+ title: "love",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 1.1,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: timeInMicroseconds,
+ title: "moz",
+ isPageAnnotation: true,
+ annoName: "sorting",
+ annoVal: 1.3,
+ annoFlags: 0,
+ annoExpiration: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[1],
+ this._unsortedData[0],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ // Query
+ var query = PlacesUtils.history.getNewQuery();
+
+ // query options
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingAnnotation = "sorting";
+ options.sortingMode = this._sortingMode;
+
+ // Results - this gets the result set and opens it for reading and modification.
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+// SORT_BY_FRECENCY_*
+
+tests.push({
+ _sortingMode: Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_ASCENDING,
+
+ *setup() {
+ do_print("Sorting test 13: SORT BY FRECENCY ");
+
+ let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ this._unsortedData = [
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "I",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "I",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://moz.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "I",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://is.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "love",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "moz",
+ isInQuery: true },
+
+ { isVisit: true,
+ isDetails: true,
+ uri: "http://best.com/",
+ lastVisit: newTimeInMicroseconds(),
+ title: "moz",
+ isInQuery: true },
+ ];
+
+ this._sortedData = [
+ this._unsortedData[3],
+ this._unsortedData[5],
+ this._unsortedData[2],
+ ];
+
+ // This function in head_queries.js creates our database with the above data
+ yield task_populateDB(this._unsortedData);
+ },
+
+ check: function() {
+ var query = PlacesUtils.history.getNewQuery();
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = this._sortingMode;
+
+ var root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ compareArrayToResult(this._sortedData, root);
+ root.containerOpen = false;
+ },
+
+ check_reverse: function() {
+ this._sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
+ this._sortedData.reverse();
+ this.check();
+ }
+});
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_sorting()
+{
+ for (let test of tests) {
+ yield test.setup();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ test.check();
+ // sorting reversed, usually SORT_BY have ASC and DESC
+ test.check_reverse();
+ // Execute cleanup tasks
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+ }
+});
diff --git a/toolkit/components/places/tests/queries/test_tags.js b/toolkit/components/places/tests/queries/test_tags.js
new file mode 100644
index 0000000000..afda3f03f7
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_tags.js
@@ -0,0 +1,743 @@
+/* -*- 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 bookmark and history queries with tags. See bug 399799.
+ */
+
+"use strict";
+
+add_task(function* tags_getter_setter() {
+ do_print("Tags getter/setter should work correctly");
+ do_print("Without setting tags, tags getter should return empty array");
+ var [query] = makeQuery();
+ do_check_eq(query.tags.length, 0);
+
+ do_print("Setting tags to an empty array, tags getter should return "+
+ "empty array");
+ [query] = makeQuery([]);
+ do_check_eq(query.tags.length, 0);
+
+ do_print("Setting a few tags, tags getter should return correct array");
+ var tags = ["bar", "baz", "foo"];
+ [query] = makeQuery(tags);
+ setsAreEqual(query.tags, tags, true);
+
+ do_print("Setting some dupe tags, tags getter return unique tags");
+ [query] = makeQuery(["foo", "foo", "bar", "foo", "baz", "bar"]);
+ setsAreEqual(query.tags, ["bar", "baz", "foo"], true);
+});
+
+add_task(function* invalid_setter_calls() {
+ do_print("Invalid calls to tags setter should fail");
+ try {
+ var query = PlacesUtils.history.getNewQuery();
+ query.tags = null;
+ do_throw("Passing null to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ query = PlacesUtils.history.getNewQuery();
+ query.tags = "this should not work";
+ do_throw("Passing a string to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery([null]);
+ do_throw("Passing one-element array with null to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery([undefined]);
+ do_throw("Passing one-element array with undefined to SetTags " +
+ "should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery(["foo", null, "bar"]);
+ do_throw("Passing mixture of tags and null to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery(["foo", undefined, "bar"]);
+ do_throw("Passing mixture of tags and undefined to SetTags " +
+ "should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery([1, 2, 3]);
+ do_throw("Passing numbers to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery(["foo", 1, 2, 3]);
+ do_throw("Passing mixture of tags and numbers to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ var str = PlacesUtils.toISupportsString("foo");
+ query = PlacesUtils.history.getNewQuery();
+ query.tags = str;
+ do_throw("Passing nsISupportsString to SetTags should fail");
+ }
+ catch (exc) {}
+
+ try {
+ makeQuery([str]);
+ do_throw("Passing array of nsISupportsStrings to SetTags should fail");
+ }
+ catch (exc) {}
+});
+
+add_task(function* not_setting_tags() {
+ do_print("Not setting tags at all should not affect query URI");
+ checkQueryURI();
+});
+
+add_task(function* empty_array_tags() {
+ do_print("Setting tags with an empty array should not affect query URI");
+ checkQueryURI([]);
+});
+
+add_task(function* set_tags() {
+ do_print("Setting some tags should result in correct query URI");
+ checkQueryURI([
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ]);
+});
+
+add_task(function* no_tags_tagsAreNot() {
+ do_print("Not setting tags at all but setting tagsAreNot should " +
+ "affect query URI");
+ checkQueryURI(null, true);
+});
+
+add_task(function* empty_array_tags_tagsAreNot() {
+ do_print("Setting tags with an empty array and setting tagsAreNot " +
+ "should affect query URI");
+ checkQueryURI([], true);
+});
+
+add_task(function* () {
+ do_print("Setting some tags and setting tagsAreNot should result in " +
+ "correct query URI");
+ checkQueryURI([
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ], true);
+});
+
+add_task(function* tag_to_uri() {
+ do_print("Querying history on tag associated with a URI should return " +
+ "that URI");
+ yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["bar"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(function* tags_to_uri() {
+ do_print("Querying history on many tags associated with a URI should " +
+ "return that URI");
+ yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "bar"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["bar", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "bar", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(function* repeated_tag() {
+ do_print("Specifying the same tag multiple times in a history query " +
+ "should not matter");
+ yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "foo"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]);
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(function* many_tags_no_uri() {
+ do_print("Querying history on many tags associated with a URI and " +
+ "tags not associated with that URI should not return that URI");
+ yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["foo", "bar", "bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ });
+});
+
+add_task(function* nonexistent_tags() {
+ do_print("Querying history on nonexistent tags should return no results");
+ yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["bogus"]);
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["bogus", "gnarly"]);
+ executeAndCheckQueryResults(query, opts, []);
+ });
+});
+
+add_task(function* tag_to_bookmark() {
+ do_print("Querying bookmarks on tag associated with a URI should " +
+ "return that URI");
+ yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["bar"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["baz"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(function* many_tags_to_bookmark() {
+ do_print("Querying bookmarks on many tags associated with a URI " +
+ "should return that URI");
+ yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "bar"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "baz"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["bar", "baz"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "bar", "baz"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(function* repeated_tag_to_bookmarks() {
+ do_print("Specifying the same tag multiple times in a bookmark query " +
+ "should not matter");
+ yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "foo"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, [aURI.spec]);
+ });
+});
+
+add_task(function* many_tags_no_bookmark() {
+ do_print("Querying bookmarks on many tags associated with a URI and " +
+ "tags not associated with that URI should not return that URI");
+ yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["foo", "bogus"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["foo", "bar", "bogus"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, []);
+ });
+});
+
+add_task(function* nonexistent_tags_bookmark() {
+ do_print("Querying bookmarks on nonexistent tag should return no results");
+ yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) {
+ var [query, opts] = makeQuery(["bogus"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, []);
+ [query, opts] = makeQuery(["bogus", "gnarly"]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ executeAndCheckQueryResults(query, opts, []);
+ });
+});
+
+add_task(function* tagsAreNot_history() {
+ do_print("Querying history using tagsAreNot should work correctly");
+ var urisAndTags = {
+ "http://example.com/1": ["foo", "bar"],
+ "http://example.com/2": ["baz", "qux"],
+ "http://example.com/3": null
+ };
+
+ do_print("Add visits and tag the URIs");
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ yield PlacesTestUtils.addVisits(nsiuri);
+ if (tags)
+ PlacesUtils.tagging.tagURI(nsiuri, tags);
+ }
+
+ do_print(' Querying for "foo" should match only /2 and /3');
+ var [query, opts] = makeQuery(["foo"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/2", "http://example.com/3"]);
+
+ do_print(' Querying for "foo" and "bar" should match only /2 and /3');
+ [query, opts] = makeQuery(["foo", "bar"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/2", "http://example.com/3"]);
+
+ do_print(' Querying for "foo" and "bogus" should match only /2 and /3');
+ [query, opts] = makeQuery(["foo", "bogus"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/2", "http://example.com/3"]);
+
+ do_print(' Querying for "foo" and "baz" should match only /3');
+ [query, opts] = makeQuery(["foo", "baz"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/3"]);
+
+ do_print(' Querying for "bogus" should match all');
+ [query, opts] = makeQuery(["bogus"], true);
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/1",
+ "http://example.com/2",
+ "http://example.com/3"]);
+
+ // Clean up.
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ if (tags)
+ PlacesUtils.tagging.untagURI(nsiuri, tags);
+ }
+ yield task_cleanDatabase();
+});
+
+add_task(function* tagsAreNot_bookmarks() {
+ do_print("Querying bookmarks using tagsAreNot should work correctly");
+ var urisAndTags = {
+ "http://example.com/1": ["foo", "bar"],
+ "http://example.com/2": ["baz", "qux"],
+ "http://example.com/3": null
+ };
+
+ do_print("Add bookmarks and tag the URIs");
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ yield addBookmark(nsiuri);
+ if (tags)
+ PlacesUtils.tagging.tagURI(nsiuri, tags);
+ }
+
+ do_print(' Querying for "foo" should match only /2 and /3');
+ var [query, opts] = makeQuery(["foo"], true);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/2", "http://example.com/3"]);
+
+ do_print(' Querying for "foo" and "bar" should match only /2 and /3');
+ [query, opts] = makeQuery(["foo", "bar"], true);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/2", "http://example.com/3"]);
+
+ do_print(' Querying for "foo" and "bogus" should match only /2 and /3');
+ [query, opts] = makeQuery(["foo", "bogus"], true);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/2", "http://example.com/3"]);
+
+ do_print(' Querying for "foo" and "baz" should match only /3');
+ [query, opts] = makeQuery(["foo", "baz"], true);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/3"]);
+
+ do_print(' Querying for "bogus" should match all');
+ [query, opts] = makeQuery(["bogus"], true);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root,
+ ["http://example.com/1",
+ "http://example.com/2",
+ "http://example.com/3"]);
+
+ // Clean up.
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ if (tags)
+ PlacesUtils.tagging.untagURI(nsiuri, tags);
+ }
+ yield task_cleanDatabase();
+});
+
+add_task(function* duplicate_tags() {
+ do_print("Duplicate existing tags (i.e., multiple tag folders with " +
+ "same name) should not throw off query results");
+ var tagName = "foo";
+
+ do_print("Add bookmark and tag it normally");
+ yield addBookmark(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, [tagName]);
+
+ do_print("Manually create tag folder with same name as tag and insert " +
+ "bookmark");
+ let dupTag = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: tagName
+ });
+
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: dupTag.guid,
+ title: "title",
+ url: TEST_URI
+ });
+
+ do_print("Querying for tag should match URI");
+ var [query, opts] = makeQuery([tagName]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]);
+
+ PlacesUtils.tagging.untagURI(TEST_URI, [tagName]);
+ yield task_cleanDatabase();
+});
+
+add_task(function* folder_named_as_tag() {
+ do_print("Regular folders with the same name as tag should not throw " +
+ "off query results");
+ var tagName = "foo";
+
+ do_print("Add bookmark and tag it");
+ yield addBookmark(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, [tagName]);
+
+ do_print("Create folder with same name as tag");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: tagName
+ });
+
+ do_print("Querying for tag should match URI");
+ var [query, opts] = makeQuery([tagName]);
+ opts.queryType = opts.QUERY_TYPE_BOOKMARKS;
+ queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]);
+
+ PlacesUtils.tagging.untagURI(TEST_URI, [tagName]);
+ yield task_cleanDatabase();
+});
+
+add_task(function* ORed_queries() {
+ do_print("Multiple queries ORed together should work");
+ var urisAndTags = {
+ "http://example.com/1": [],
+ "http://example.com/2": []
+ };
+
+ // Search with lots of tags to make sure tag parameter substitution in SQL
+ // can handle it with more than one query.
+ for (let i = 0; i < 11; i++) {
+ urisAndTags["http://example.com/1"].push("/1 tag " + i);
+ urisAndTags["http://example.com/2"].push("/2 tag " + i);
+ }
+
+ do_print("Add visits and tag the URIs");
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ yield PlacesTestUtils.addVisits(nsiuri);
+ if (tags)
+ PlacesUtils.tagging.tagURI(nsiuri, tags);
+ }
+
+ do_print("Query for /1 OR query for /2 should match both /1 and /2");
+ var [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]);
+ var [query2] = makeQuery(urisAndTags["http://example.com/2"]);
+ var root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root;
+ queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]);
+
+ do_print("Query for /1 OR query on bogus tag should match only /1");
+ [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]);
+ [query2] = makeQuery(["bogus"]);
+ root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root;
+ queryResultsAre(root, ["http://example.com/1"]);
+
+ do_print("Query for /1 OR query for /1 should match only /1");
+ [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]);
+ [query2] = makeQuery(urisAndTags["http://example.com/1"]);
+ root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root;
+ queryResultsAre(root, ["http://example.com/1"]);
+
+ do_print("Query for /1 with tagsAreNot OR query for /2 with tagsAreNot " +
+ "should match both /1 and /2");
+ [query1, opts] = makeQuery(urisAndTags["http://example.com/1"], true);
+ [query2] = makeQuery(urisAndTags["http://example.com/2"], true);
+ root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root;
+ queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]);
+
+ do_print("Query for /1 OR query for /2 with tagsAreNot should match " +
+ "only /1");
+ [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]);
+ [query2] = makeQuery(urisAndTags["http://example.com/2"], true);
+ root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root;
+ queryResultsAre(root, ["http://example.com/1"]);
+
+ do_print("Query for /1 OR query for /1 with tagsAreNot should match " +
+ "both URIs");
+ [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]);
+ [query2] = makeQuery(urisAndTags["http://example.com/1"], true);
+ root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root;
+ queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]);
+
+ // Clean up.
+ for (let [pURI, tags] of Object.entries(urisAndTags)) {
+ let nsiuri = uri(pURI);
+ if (tags)
+ PlacesUtils.tagging.untagURI(nsiuri, tags);
+ }
+ yield task_cleanDatabase();
+});
+
+// The tag keys in query URIs, i.e., "place:tag=foo&!tags=1"
+// --- -----
+const QUERY_KEY_TAG = "tag";
+const QUERY_KEY_NOT_TAGS = "!tags";
+
+const TEST_URI = uri("http://example.com/");
+
+/**
+ * Adds a bookmark.
+ *
+ * @param aURI
+ * URI of the page (an nsIURI)
+ */
+function addBookmark(aURI) {
+ return PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: aURI.spec,
+ url: aURI
+ });
+}
+
+/**
+ * Asynchronous task that removes all pages from history and bookmarks.
+ */
+function* task_cleanDatabase(aCallback) {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+}
+
+/**
+ * Sets up a query with the specified tags, converts it to a URI, and makes sure
+ * the URI is what we expect it to be.
+ *
+ * @param aTags
+ * The query's tags will be set to those in this array
+ * @param aTagsAreNot
+ * The query's tagsAreNot property will be set to this
+ */
+function checkQueryURI(aTags, aTagsAreNot) {
+ var pairs = (aTags || []).sort().map(t => QUERY_KEY_TAG + "=" + encodeTag(t));
+ if (aTagsAreNot)
+ pairs.push(QUERY_KEY_NOT_TAGS + "=1");
+ var expURI = "place:" + pairs.join("&");
+ var [query, opts] = makeQuery(aTags, aTagsAreNot);
+ var actualURI = queryURI(query, opts);
+ do_print("Query URI should be what we expect for the given tags");
+ do_check_eq(actualURI, expURI);
+}
+
+/**
+ * Asynchronous task that executes a callback task in a "scoped" database state.
+ * A bookmark is added and tagged before the callback is called, and afterward
+ * the database is cleared.
+ *
+ * @param aTags
+ * A bookmark will be added and tagged with this array of tags
+ * @param aCallback
+ * A task function that will be called after the bookmark has been tagged
+ */
+function* task_doWithBookmark(aTags, aCallback) {
+ yield addBookmark(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, aTags);
+ yield aCallback(TEST_URI);
+ PlacesUtils.tagging.untagURI(TEST_URI, aTags);
+ yield task_cleanDatabase();
+}
+
+/**
+ * Asynchronous task that executes a callback function in a "scoped" database
+ * state. A history visit is added and tagged before the callback is called,
+ * and afterward the database is cleared.
+ *
+ * @param aTags
+ * A history visit will be added and tagged with this array of tags
+ * @param aCallback
+ * A function that will be called after the visit has been tagged
+ */
+function* task_doWithVisit(aTags, aCallback) {
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, aTags);
+ yield aCallback(TEST_URI);
+ PlacesUtils.tagging.untagURI(TEST_URI, aTags);
+ yield task_cleanDatabase();
+}
+
+/**
+ * queriesToQueryString() encodes every character in the query URI that doesn't
+ * match /[a-zA-Z]/. There's no simple JavaScript function that does the same,
+ * but encodeURIComponent() comes close, only missing some punctuation. This
+ * function takes care of all of that.
+ *
+ * @param aTag
+ * A tag name to encode
+ * @return A UTF-8 escaped string suitable for inclusion in a query URI
+ */
+function encodeTag(aTag) {
+ return encodeURIComponent(aTag).
+ replace(/[-_.!~*'()]/g, // '
+ s => "%" + s.charCodeAt(0).toString(16));
+}
+
+/**
+ * Executes the given query and compares the results to the given URIs.
+ * See queryResultsAre().
+ *
+ * @param aQuery
+ * An nsINavHistoryQuery
+ * @param aQueryOpts
+ * An nsINavHistoryQueryOptions
+ * @param aExpectedURIs
+ * Array of URIs (as strings) that aResultRoot should contain
+ */
+function executeAndCheckQueryResults(aQuery, aQueryOpts, aExpectedURIs) {
+ var root = PlacesUtils.history.executeQuery(aQuery, aQueryOpts).root;
+ root.containerOpen = true;
+ queryResultsAre(root, aExpectedURIs);
+ root.containerOpen = false;
+}
+
+/**
+ * Returns new query and query options objects. The query's tags will be
+ * set to aTags. aTags may be null, in which case setTags() is not called at
+ * all on the query.
+ *
+ * @param aTags
+ * The query's tags will be set to those in this array
+ * @param aTagsAreNot
+ * The query's tagsAreNot property will be set to this
+ * @return [query, queryOptions]
+ */
+function makeQuery(aTags, aTagsAreNot) {
+ aTagsAreNot = !!aTagsAreNot;
+ do_print("Making a query " +
+ (aTags ?
+ "with tags " + aTags.toSource() :
+ "without calling setTags() at all") +
+ " and with tagsAreNot=" +
+ aTagsAreNot);
+ var query = PlacesUtils.history.getNewQuery();
+ query.tagsAreNot = aTagsAreNot;
+ if (aTags) {
+ query.tags = aTags;
+ var uniqueTags = [];
+ aTags.forEach(function (t) {
+ if (typeof(t) === "string" && uniqueTags.indexOf(t) < 0)
+ uniqueTags.push(t);
+ });
+ uniqueTags.sort();
+ }
+
+ do_print("Made query should be correct for tags and tagsAreNot");
+ if (uniqueTags)
+ setsAreEqual(query.tags, uniqueTags, true);
+ var expCount = uniqueTags ? uniqueTags.length : 0;
+ do_check_eq(query.tags.length, expCount);
+ do_check_eq(query.tagsAreNot, aTagsAreNot);
+
+ return [query, PlacesUtils.history.getNewQueryOptions()];
+}
+
+/**
+ * Ensures that the URIs of aResultRoot are the same as those in aExpectedURIs.
+ *
+ * @param aResultRoot
+ * The nsINavHistoryContainerResultNode root of an nsINavHistoryResult
+ * @param aExpectedURIs
+ * Array of URIs (as strings) that aResultRoot should contain
+ */
+function queryResultsAre(aResultRoot, aExpectedURIs) {
+ var rootWasOpen = aResultRoot.containerOpen;
+ if (!rootWasOpen)
+ aResultRoot.containerOpen = true;
+ var actualURIs = [];
+ for (let i = 0; i < aResultRoot.childCount; i++) {
+ actualURIs.push(aResultRoot.getChild(i).uri);
+ }
+ setsAreEqual(actualURIs, aExpectedURIs);
+ if (!rootWasOpen)
+ aResultRoot.containerOpen = false;
+}
+
+/**
+ * Converts the given query into its query URI.
+ *
+ * @param aQuery
+ * An nsINavHistoryQuery
+ * @param aQueryOpts
+ * An nsINavHistoryQueryOptions
+ * @return The query's URI
+ */
+function queryURI(aQuery, aQueryOpts) {
+ return PlacesUtils.history.queriesToQueryString([aQuery], 1, aQueryOpts);
+}
+
+/**
+ * Ensures that the arrays contain the same elements and, optionally, in the
+ * same order.
+ */
+function setsAreEqual(aArr1, aArr2, aIsOrdered) {
+ do_check_eq(aArr1.length, aArr2.length);
+ if (aIsOrdered) {
+ for (let i = 0; i < aArr1.length; i++) {
+ do_check_eq(aArr1[i], aArr2[i]);
+ }
+ }
+ else {
+ aArr1.forEach(u => do_check_true(aArr2.indexOf(u) >= 0));
+ aArr2.forEach(u => do_check_true(aArr1.indexOf(u) >= 0));
+ }
+}
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/queries/test_transitions.js b/toolkit/components/places/tests/queries/test_transitions.js
new file mode 100644
index 0000000000..bbd4c9e01b
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_transitions.js
@@ -0,0 +1,178 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ * ***** END LICENSE BLOCK ***** */
+var beginTime = Date.now();
+var testData = [
+ {
+ isVisit: true,
+ title: "page 0",
+ uri: "http://mozilla.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED
+ },
+ {
+ isVisit: true,
+ title: "page 1",
+ uri: "http://google.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ },
+ {
+ isVisit: true,
+ title: "page 2",
+ uri: "http://microsoft.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ },
+ {
+ isVisit: true,
+ title: "page 3",
+ uri: "http://en.wikipedia.org/",
+ transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK
+ },
+ {
+ isVisit: true,
+ title: "page 4",
+ uri: "http://fr.wikipedia.org/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ },
+ {
+ isVisit: true,
+ title: "page 5",
+ uri: "http://apple.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED
+ },
+ {
+ isVisit: true,
+ title: "page 6",
+ uri: "http://campus-bike-store.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ },
+ {
+ isVisit: true,
+ title: "page 7",
+ uri: "http://uwaterloo.ca/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED
+ },
+ {
+ isVisit: true,
+ title: "page 8",
+ uri: "http://pugcleaner.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK
+ },
+ {
+ isVisit: true,
+ title: "page 9",
+ uri: "http://de.wikipedia.org/",
+ transType: Ci.nsINavHistoryService.TRANSITION_TYPED
+ },
+ {
+ isVisit: true,
+ title: "arewefastyet",
+ uri: "http://arewefastyet.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ },
+ {
+ isVisit: true,
+ title: "arewefastyet",
+ uri: "http://arewefastyet.com/",
+ transType: Ci.nsINavHistoryService.TRANSITION_BOOKMARK
+ }];
+// sets of indices of testData array by transition type
+var testDataTyped = [0, 5, 7, 9];
+var testDataDownload = [1, 2, 4, 6, 10];
+var testDataBookmark = [3, 8, 11];
+
+/**
+ * run_test is where the magic happens. This is automatically run by the test
+ * harness. It is where you do the work of creating the query, running it, and
+ * playing with the result set.
+ */
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_transitions()
+{
+ let timeNow = Date.now();
+ for (let item of testData) {
+ yield PlacesTestUtils.addVisits({
+ uri: uri(item.uri),
+ transition: item.transType,
+ visitDate: timeNow++ * 1000,
+ title: item.title
+ });
+ }
+
+ // dump_table("moz_places");
+ // dump_table("moz_historyvisits");
+
+ var numSortFunc = function (a, b) { return (a - b); };
+ var arrs = testDataTyped.concat(testDataDownload).concat(testDataBookmark)
+ .sort(numSortFunc);
+
+ // Four tests which compare the result of a query to an expected set.
+ var data = arrs.filter(function (index) {
+ return (testData[index].uri.match(/arewefastyet\.com/) &&
+ testData[index].transType ==
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD);
+ });
+
+ compareQueryToTestData("place:domain=arewefastyet.com&transition=" +
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ data.slice());
+
+ compareQueryToTestData("place:transition=" +
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ testDataDownload.slice());
+
+ compareQueryToTestData("place:transition=" +
+ Ci.nsINavHistoryService.TRANSITION_TYPED,
+ testDataTyped.slice());
+
+ compareQueryToTestData("place:transition=" +
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD +
+ "&transition=" +
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ data);
+
+ // Tests the live update property of transitions.
+ var query = {};
+ var options = {};
+ PlacesUtils.history.
+ queryStringToQueries("place:transition=" +
+ Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ query, {}, options);
+ query = (query.value)[0];
+ options = PlacesUtils.history.getNewQueryOptions();
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(testDataDownload.length, root.childCount);
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://getfirefox.com"),
+ transition: TRANSITION_DOWNLOAD
+ });
+ do_check_eq(testDataDownload.length + 1, root.childCount);
+ root.containerOpen = false;
+});
+
+/*
+ * Takes a query and a set of indices. The indices correspond to elements
+ * of testData that are the result of the query.
+ */
+function compareQueryToTestData(queryStr, data) {
+ var query = {};
+ var options = {};
+ PlacesUtils.history.queryStringToQueries(queryStr, query, {}, options);
+ query = query.value[0];
+ options = options.value;
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ for (var i = 0; i < data.length; i++) {
+ data[i] = testData[data[i]];
+ data[i].isInQuery = true;
+ }
+ compareArrayToResult(data, root);
+}
diff --git a/toolkit/components/places/tests/queries/xpcshell.ini b/toolkit/components/places/tests/queries/xpcshell.ini
new file mode 100644
index 0000000000..7ff864679a
--- /dev/null
+++ b/toolkit/components/places/tests/queries/xpcshell.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+head = head_queries.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_415716.js]
+[test_abstime-annotation-domain.js]
+[test_abstime-annotation-uri.js]
+[test_async.js]
+[test_containersQueries_sorting.js]
+[test_history_queries_tags_liveUpdate.js]
+[test_history_queries_titles_liveUpdate.js]
+[test_onlyBookmarked.js]
+[test_queryMultipleFolder.js]
+[test_querySerialization.js]
+[test_redirects.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_results-as-tag-contents-query.js]
+[test_results-as-visit.js]
+[test_searchterms-domain.js]
+[test_searchterms-uri.js]
+[test_searchterms-bookmarklets.js]
+[test_sort-date-site-grouping.js]
+[test_sorting.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_tags.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_transitions.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_searchTerms_includeHidden.js]
diff --git a/toolkit/components/places/tests/unifiedcomplete/.eslintrc.js b/toolkit/components/places/tests/unifiedcomplete/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml b/toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml
new file mode 100644
index 0000000000..f4baad28ab
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/data/engine-rel-searchform.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform.xml</ShortName>
+<Url type="text/html" method="GET" template="http://example.com/?search" rel="searchform"/>
+</SearchPlugin>
diff --git a/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml b/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml
new file mode 100644
index 0000000000..a322a7c86e
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-suggestions.xml</ShortName>
+<Url type="application/x-suggestions+json"
+ method="GET"
+ template="http://localhost:9000/suggest?{searchTerms}"/>
+<Url type="text/html"
+ method="GET"
+ template="http://localhost:9000/search"
+ rel="searchform"/>
+</SearchPlugin>
diff --git a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
new file mode 100644
index 0000000000..11e917e187
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -0,0 +1,505 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+const FRECENCY_DEFAULT = 10000;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+// 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.
+
+const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 ";
+
+function run_test() {
+ run_next_test();
+}
+
+function* cleanup() {
+ Services.prefs.clearUserPref("browser.urlbar.autocomplete.enabled");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.typed");
+ Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
+ let suggestPrefs = [
+ "history",
+ "bookmark",
+ "history.onlyTyped",
+ "openpage",
+ "searches",
+ ];
+ for (let type of suggestPrefs) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+}
+do_register_cleanup(cleanup);
+
+/**
+ * @param aSearches
+ * Array of AutoCompleteSearch names.
+ */
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ popup: {
+ selectedIndex: -1,
+ invalidate: function () {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup])
+ },
+ popupOpen: false,
+
+ disableAutoComplete: false,
+ completeDefaultIndex: true,
+ completeSelectedIndex: true,
+ forceComplete: false,
+
+ minResultsForPopup: 0,
+ maxRows: 0,
+
+ showCommentColumn: false,
+ showImageColumn: false,
+
+ timeout: 10,
+ searchParam: "",
+
+ get searchCount() {
+ return this.searches.length;
+ },
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ textValue: "",
+ // Text selection range
+ _selStart: 0,
+ _selEnd: 0,
+ get selectionStart() {
+ return this._selStart;
+ },
+ get selectionEnd() {
+ return this._selEnd;
+ },
+ selectTextRange: function(aStart, aEnd) {
+ this._selStart = aStart;
+ this._selEnd = aEnd;
+ },
+
+ onSearchBegin: function () {},
+ onSearchComplete: function () {},
+
+ onTextEntered: () => false,
+ onTextReverted: () => false,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
+}
+
+// A helper for check_autocomplete to check a specific match against data from
+// the controller.
+function _check_autocomplete_matches(match, result) {
+ let { uri, title, tags, style } = match;
+ if (tags)
+ title += " \u2013 " + tags.sort().join(", ");
+ if (style)
+ style = style.sort();
+ else
+ style = ["favicon"];
+
+ do_print(`Checking against expected "${uri.spec}", "${title}"`);
+ // Got a match on both uri and title?
+ if (stripPrefix(uri.spec) != stripPrefix(result.value) || title != result.comment) {
+ return false;
+ }
+
+ let actualStyle = result.style.split(/\s+/).sort();
+ if (style)
+ Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style");
+ if (uri.spec.startsWith("moz-action:")) {
+ Assert.ok(actualStyle.includes("action"), "moz-action results should always have 'action' in their style");
+ }
+
+ if (match.icon)
+ Assert.equal(result.image, match.icon, "Match should have expected image");
+
+ return true;
+}
+
+function* check_autocomplete(test) {
+ // 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.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Make an AutoCompleteInput that uses our searches and confirms results.
+ let input = new AutoCompleteInput(["unifiedcomplete"]);
+ input.textValue = test.search;
+
+ if (test.searchParam)
+ input.searchParam = test.searchParam;
+
+ // Caret must be at the end for autoFill to happen.
+ let strLen = test.search.length;
+ input.selectTextRange(strLen, strLen);
+ Assert.equal(input.selectionStart, strLen, "Selection starts at end");
+ Assert.equal(input.selectionEnd, strLen, "Selection ends at the end");
+
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"]
+ .getService(Ci.nsIAutoCompleteController);
+ controller.input = input;
+
+ let numSearchesStarted = 0;
+ input.onSearchBegin = () => {
+ do_print("onSearchBegin received");
+ numSearchesStarted++;
+ };
+ let searchCompletePromise = new Promise(resolve => {
+ input.onSearchComplete = () => {
+ do_print("onSearchComplete received");
+ resolve();
+ }
+ });
+ let expectedSearches = 1;
+ if (test.incompleteSearch) {
+ controller.startSearch(test.incompleteSearch);
+ expectedSearches++;
+ }
+
+ do_print("Searching for: '" + test.search + "'");
+ controller.startSearch(test.search);
+ yield searchCompletePromise;
+
+ Assert.equal(numSearchesStarted, expectedSearches, "All searches started");
+
+ // Check to see the expected uris and titles match up. If 'enable-actions'
+ // is specified, we check that the first specified match is the first
+ // controller value (as this is the "special" always selected item), but the
+ // rest can match in any order.
+ // If 'enable-actions' is not specified, they can match in any order.
+ if (test.matches) {
+ // Do not modify the test original matches.
+ let matches = test.matches.slice();
+
+ if (matches.length) {
+ let firstIndexToCheck = 0;
+ if (test.searchParam && test.searchParam.includes("enable-actions")) {
+ firstIndexToCheck = 1;
+ do_print("Checking first match is first autocomplete entry")
+ let result = {
+ value: controller.getValueAt(0),
+ comment: controller.getCommentAt(0),
+ style: controller.getStyleAt(0),
+ image: controller.getImageAt(0),
+ }
+ do_print(`First match is "${result.value}", "${result.comment}"`);
+ Assert.ok(_check_autocomplete_matches(matches[0], result), "first item is correct");
+ do_print("Checking rest of the matches");
+ }
+
+ for (let i = firstIndexToCheck; i < controller.matchCount; i++) {
+ let result = {
+ value: controller.getValueAt(i),
+ comment: controller.getCommentAt(i),
+ style: controller.getStyleAt(i),
+ image: controller.getImageAt(i),
+ }
+ do_print(`Looking for "${result.value}", "${result.comment}" in expected results...`);
+ let lowerBound = test.checkSorting ? i : firstIndexToCheck;
+ let upperBound = test.checkSorting ? i + 1 : matches.length;
+ let found = false;
+ for (let j = lowerBound; j < upperBound; ++j) {
+ // Skip processed expected results
+ if (matches[j] == undefined)
+ continue;
+ if (_check_autocomplete_matches(matches[j], result)) {
+ do_print("Got a match at index " + j + "!");
+ // Make it undefined so we don't process it again
+ matches[j] = undefined;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`); // ' (Emacs syntax highlighting fix)
+ }
+ }
+
+ Assert.equal(controller.matchCount, matches.length,
+ "Got as many results as expected");
+
+ // If we expect results, make sure we got matches.
+ do_check_eq(controller.searchStatus, matches.length ?
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+ }
+
+ if (test.autofilled) {
+ // Check the autoFilled result.
+ Assert.equal(input.textValue, test.autofilled,
+ "Autofilled value is correct");
+
+ // Now force completion and check correct casing of the result.
+ // This ensures the controller is able to do its magic case-preserving
+ // stuff and correct replacement of the user's casing with result's one.
+ controller.handleEnter(false);
+ Assert.equal(input.textValue, test.completed,
+ "Completed value is correct");
+ }
+}
+
+var addBookmark = Task.async(function* (aBookmarkObj) {
+ Assert.ok(!!aBookmarkObj.uri, "Bookmark object contains an uri");
+ let parentId = aBookmarkObj.parentId ? aBookmarkObj.parentId
+ : PlacesUtils.unfiledBookmarksFolderId;
+
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: (yield PlacesUtils.promiseItemGuid(parentId)),
+ title: aBookmarkObj.title || "A bookmark",
+ url: aBookmarkObj.uri
+ });
+ yield PlacesUtils.promiseItemId(bm.guid);
+
+ if (aBookmarkObj.keyword) {
+ yield PlacesUtils.keywords.insert({ keyword: aBookmarkObj.keyword,
+ url: aBookmarkObj.uri.spec,
+ postData: aBookmarkObj.postData
+ });
+ }
+
+ if (aBookmarkObj.tags) {
+ PlacesUtils.tagging.tagURI(aBookmarkObj.uri, aBookmarkObj.tags);
+ }
+});
+
+function addOpenPages(aUri, aCount=1, aUserContextId=0) {
+ let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+ .getService(Ci.mozIPlacesAutoComplete);
+ for (let i = 0; i < aCount; i++) {
+ ac.registerOpenPage(aUri, aUserContextId);
+ }
+}
+
+function removeOpenPages(aUri, aCount=1, aUserContextId=0) {
+ let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+ .getService(Ci.mozIPlacesAutoComplete);
+ for (let i = 0; i < aCount; i++) {
+ ac.unregisterOpenPage(aUri, aUserContextId);
+ }
+}
+
+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.";
+
+ do_print("changing restrict for " + aType + " to '" + aChar + "'");
+ Services.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.";
+
+ Services.prefs.clearUserPref(branch + aType);
+}
+
+/**
+ * Strip prefixes from the URI that we don't care about for searching.
+ *
+ * @param spec
+ * The text to modify.
+ * @return the modified spec.
+ */
+function stripPrefix(spec)
+{
+ ["http://", "https://", "ftp://"].some(scheme => {
+ if (spec.startsWith(scheme)) {
+ spec = spec.slice(scheme.length);
+ return true;
+ }
+ return false;
+ });
+
+ if (spec.startsWith("www.")) {
+ spec = spec.slice(4);
+ }
+ return spec;
+}
+
+function makeActionURI(action, params) {
+ let encodedParams = {};
+ for (let key in params) {
+ encodedParams[key] = encodeURIComponent(params[key]);
+ }
+ let url = "moz-action:" + action + "," + JSON.stringify(encodedParams);
+ return NetUtil.newURI(url);
+}
+
+// Creates a full "match" entry for a search result, suitable for passing as
+// an entry to check_autocomplete.
+function makeSearchMatch(input, extra = {}) {
+ // Note that counter-intuitively, the order the object properties are defined
+ // in the object passed to makeActionURI is important for check_autocomplete
+ // to match them :(
+ let params = {
+ engineName: extra.engineName || "MozSearch",
+ input,
+ searchQuery: "searchQuery" in extra ? extra.searchQuery : input,
+ };
+ if ("alias" in extra) {
+ // May be undefined, which is expected, but in that case make sure it's not
+ // included in the params of the moz-action URL.
+ params.alias = extra.alias;
+ }
+ let style = [ "action", "searchengine" ];
+ if (Array.isArray(extra.style)) {
+ style.push(...extra.style);
+ }
+ if (extra.heuristic) {
+ style.push("heuristic");
+ }
+ return {
+ uri: makeActionURI("searchengine", params),
+ title: params.engineName,
+ style,
+ }
+}
+
+// Creates a full "match" entry for a search result, suitable for passing as
+// an entry to check_autocomplete.
+function makeVisitMatch(input, url, extra = {}) {
+ // Note that counter-intuitively, the order the object properties are defined
+ // in the object passed to makeActionURI is important for check_autocomplete
+ // to match them :(
+ let params = {
+ url,
+ input,
+ }
+ let style = [ "action", "visiturl" ];
+ if (extra.heuristic) {
+ style.push("heuristic");
+ }
+ return {
+ uri: makeActionURI("visiturl", params),
+ title: extra.title || url,
+ style,
+ }
+}
+
+function makeSwitchToTabMatch(url, extra = {}) {
+ return {
+ uri: makeActionURI("switchtab", {url}),
+ title: extra.title || url,
+ style: [ "action", "switchtab" ],
+ }
+}
+
+function makeExtensionMatch(extra = {}) {
+ let style = [ "action", "extension" ];
+ if (extra.heuristic) {
+ style.push("heuristic");
+ }
+
+ return {
+ uri: makeActionURI("extension", {
+ content: extra.content,
+ keyword: extra.keyword,
+ }),
+ title: extra.description,
+ style,
+ };
+}
+
+function setFaviconForHref(href, iconHref) {
+ return new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ NetUtil.newURI(href),
+ NetUtil.newURI(iconHref),
+ true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+}
+
+function makeTestServer(port=-1) {
+ let httpServer = new HttpServer();
+ httpServer.start(port);
+ do_register_cleanup(() => httpServer.stop(() => {}));
+ return httpServer;
+}
+
+function* addTestEngine(basename, httpServer=undefined) {
+ httpServer = httpServer || makeTestServer();
+ httpServer.registerDirectory("/", do_get_cwd());
+ let dataUrl =
+ "http://localhost:" + httpServer.identity.primaryPort + "/data/";
+
+ do_print("Adding engine: " + basename);
+ return yield new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ do_print("Observed " + data + " for " + engine.name);
+ if (data != "engine-added" || engine.name != basename) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+ resolve(engine);
+ }, "browser-search-engine-modified", false);
+
+ do_print("Adding engine from URL: " + dataUrl + basename);
+ Services.search.addEngine(dataUrl + basename, null, null, false);
+ });
+}
+
+// Ensure we have a default search engine and the keyword.enabled preference
+// set.
+add_task(function* ensure_search_engine() {
+ // keyword.enabled is necessary for the tests to see keyword searches.
+ Services.prefs.setBoolPref("keyword.enabled", true);
+
+ // Initialize the search service, but first set this geo IP pref to a dummy
+ // string. When the search service is initialized, it contacts the URI named
+ // in this pref, which breaks the test since outside connections aren't
+ // allowed.
+ let geoPref = "browser.search.geoip.url";
+ Services.prefs.setCharPref(geoPref, "");
+ do_register_cleanup(() => Services.prefs.clearUserPref(geoPref));
+ yield new Promise(resolve => {
+ Services.search.init(resolve);
+ });
+
+ // Remove any existing engines before adding ours.
+ for (let engine of Services.search.getEngines()) {
+ Services.search.removeEngine(engine);
+ }
+ Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+ "http://s.example.com/search");
+ let engine = Services.search.getEngineByName("MozSearch");
+ Services.search.currentEngine = engine;
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_416211.js b/toolkit/components/places/tests/unifiedcomplete/test_416211.js
new file mode 100644
index 0000000000..e02906ddce
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_416211.js
@@ -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/. */
+
+/*
+ * Test bug 416211 to make sure results that match the tag show the bookmark
+ * title instead of the page title.
+ */
+
+add_task(function* test_tag_match_has_bookmark_title() {
+ do_print("Make sure the tag match gives the bookmark title");
+ let uri = NetUtil.newURI("http://theuri/");
+ yield PlacesTestUtils.addVisits({ uri: uri, title: "Page title" });
+ yield addBookmark({ uri: uri,
+ title: "Bookmark title",
+ tags: [ "superTag" ]});
+ yield check_autocomplete({
+ search: "superTag",
+ matches: [ { uri: uri, title: "Bookmark title", tags: [ "superTag" ], style: [ "bookmark-tag" ] } ]
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_416214.js b/toolkit/components/places/tests/unifiedcomplete/test_416214.js
new file mode 100644
index 0000000000..a30b3fe742
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_416214.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/. */
+
+/*
+ * 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
+ */
+
+add_task(function* test_tag_match_url() {
+ do_print("Make sure tag matches return the right url as well as '+' remain escaped");
+ let uri1 = NetUtil.newURI("http://escaped/ユニコード");
+ let uri2 = NetUtil.newURI("http://asciiescaped/blocking-firefox3%2B");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" }
+ ]);
+ yield addBookmark({ uri: uri1,
+ title: "title",
+ tags: [ "superTag" ],
+ style: [ "bookmark-tag" ] });
+ yield addBookmark({ uri: uri2,
+ title: "title",
+ tags: [ "superTag" ],
+ style: [ "bookmark-tag" ] });
+ yield check_autocomplete({
+ search: "superTag",
+ matches: [ { uri: uri1, title: "title", tags: [ "superTag" ], style: [ "bookmark-tag" ] },
+ { uri: uri2, title: "title", tags: [ "superTag" ], style: [ "bookmark-tag" ] } ]
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_417798.js b/toolkit/components/places/tests/unifiedcomplete/test_417798.js
new file mode 100644
index 0000000000..bed14b2cea
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_417798.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ */
+
+add_task(function* test_javascript_match() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+
+ let uri1 = NetUtil.newURI("http://abc/def");
+ let uri2 = NetUtil.newURI("javascript:5");
+ yield PlacesTestUtils.addVisits([ { uri: uri1, title: "Title with javascript:" } ]);
+ yield addBookmark({ uri: uri2,
+ title: "Title with javascript:" });
+
+ do_print("Match non-javascript: with plain search");
+ yield check_autocomplete({
+ search: "a",
+ matches: [ { uri: uri1, title: "Title with javascript:" } ]
+ });
+
+ do_print("Match non-javascript: with almost javascript:");
+ yield check_autocomplete({
+ search: "javascript",
+ matches: [ { uri: uri1, title: "Title with javascript:" } ]
+ });
+
+ do_print("Match javascript:");
+ yield check_autocomplete({
+ search: "javascript:",
+ matches: [ { uri: uri1, title: "Title with javascript:" },
+ { uri: uri2, title: "Title with javascript:", style: [ "bookmark" ]} ]
+ });
+
+ do_print("Match nothing with non-first javascript:");
+ yield check_autocomplete({
+ search: "5 javascript:",
+ matches: [ ]
+ });
+
+ do_print("Match javascript: with multi-word search");
+ yield check_autocomplete({
+ search: "javascript: 5",
+ matches: [ { uri: uri2, title: "Title with javascript:", style: [ "bookmark" ]} ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_418257.js b/toolkit/components/places/tests/unifiedcomplete/test_418257.js
new file mode 100644
index 0000000000..323c2a7af3
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_418257.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ */
+
+add_task(function* test_javascript_match() {
+ let uri1 = NetUtil.newURI("http://page1");
+ let uri2 = NetUtil.newURI("http://page2");
+ let uri3 = NetUtil.newURI("http://page3");
+ let uri4 = NetUtil.newURI("http://page4");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "tagged" },
+ { uri: uri2, title: "tagged" },
+ { uri: uri3, title: "tagged" },
+ { uri: uri4, title: "tagged" }
+ ]);
+ yield addBookmark({ uri: uri1,
+ title: "tagged",
+ tags: [ "tag1" ] });
+ yield addBookmark({ uri: uri2,
+ title: "tagged",
+ tags: [ "tag1", "tag2" ] });
+ yield addBookmark({ uri: uri3,
+ title: "tagged",
+ tags: [ "tag1", "tag3" ] });
+ yield addBookmark({ uri: uri4,
+ title: "tagged",
+ tags: [ "tag1", "tag2", "tag3" ] });
+
+ do_print("Make sure tags come back in the title when matching tags");
+ yield check_autocomplete({
+ search: "page1 tag",
+ matches: [ { uri: uri1, title: "tagged", tags: [ "tag1" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Check tags in title for page2");
+ yield check_autocomplete({
+ search: "page2 tag",
+ matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Make sure tags appear even when not matching the tag");
+ yield check_autocomplete({
+ search: "page3",
+ matches: [ { uri: uri3, title: "tagged", tags: [ "tag1", "tag3" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Multiple tags come in commas for page4");
+ yield check_autocomplete({
+ search: "page4",
+ matches: [ { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Extra test just to make sure we match the title");
+ yield check_autocomplete({
+ search: "tag2",
+ matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] },
+ { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_422277.js b/toolkit/components/places/tests/unifiedcomplete/test_422277.js
new file mode 100644
index 0000000000..df6f7601a4
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_422277.js
@@ -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/. */
+
+/**
+ * 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".
+ */
+
+add_task(function* test_javascript_match() {
+ do_print("Bad escaped uri stays escaped");
+ let uri1 = NetUtil.newURI("http://site/%EAid");
+ yield PlacesTestUtils.addVisits([ { uri: uri1, title: "title" } ]);
+ yield check_autocomplete({
+ search: "site",
+ matches: [ { uri: uri1, title: "title" } ]
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js
new file mode 100644
index 0000000000..cd2dfdb17a
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.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/. */
+
+// Functional tests for inline autocomplete
+
+const PREF_AUTOCOMPLETE_ENABLED = "browser.urlbar.autocomplete.enabled";
+
+add_task(function* test_disabling_autocomplete() {
+ do_print("Check disabling autocomplete disables autofill");
+ Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, false);
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://visit.mozilla.org"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "vis",
+ autofilled: "vis",
+ completed: "vis"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_urls_order() {
+ do_print("Add urls, check for correct order");
+ let places = [{ uri: NetUtil.newURI("http://visit1.mozilla.org") },
+ { uri: NetUtil.newURI("http://visit2.mozilla.org"),
+ transition: TRANSITION_TYPED }];
+ yield PlacesTestUtils.addVisits(places);
+ yield check_autocomplete({
+ search: "vis",
+ autofilled: "visit2.mozilla.org/",
+ completed: "visit2.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_ignore_prefix() {
+ do_print("Add urls, make sure www and http are ignored");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit1.mozilla.org"));
+ yield check_autocomplete({
+ search: "visit1",
+ autofilled: "visit1.mozilla.org/",
+ completed: "visit1.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_after_host() {
+ do_print("Autocompleting after an existing host completes to the url");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit3.mozilla.org"));
+ yield check_autocomplete({
+ search: "visit3.mozilla.org/",
+ autofilled: "visit3.mozilla.org/",
+ completed: "visit3.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_respect_www() {
+ do_print("Searching for www.me should yield www.me.mozilla.org/");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://www.me.mozilla.org"));
+ yield check_autocomplete({
+ search: "www.me",
+ autofilled: "www.me.mozilla.org/",
+ completed: "www.me.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_bookmark_first() {
+ do_print("With a bookmark and history, the query result should be the bookmark");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield addBookmark({ uri: NetUtil.newURI("http://bookmark1.mozilla.org/") });
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo"));
+ yield check_autocomplete({
+ search: "bookmark",
+ autofilled: "bookmark1.mozilla.org/",
+ completed: "bookmark1.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_full_path() {
+ do_print("Check to make sure we get the proper results with full paths");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
+ { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
+ yield PlacesTestUtils.addVisits(places);
+ yield check_autocomplete({
+ search: "smokey",
+ autofilled: "smokey.mozilla.org/",
+ completed: "smokey.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_complete_to_slash() {
+ do_print("Check to make sure we autocomplete to the following '/'");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
+ { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
+ yield PlacesTestUtils.addVisits(places);
+ yield check_autocomplete({
+ search: "smokey.mozilla.org/fo",
+ autofilled: "smokey.mozilla.org/foo/",
+ completed: "http://smokey.mozilla.org/foo/",
+ });
+ yield cleanup();
+});
+
+add_task(function* test_complete_to_slash_with_www() {
+ do_print("Check to make sure we autocomplete to the following '/'");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ let places = [{ uri: NetUtil.newURI("http://www.smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
+ { uri: NetUtil.newURI("http://www.smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
+ yield PlacesTestUtils.addVisits(places);
+ yield check_autocomplete({
+ search: "smokey.mozilla.org/fo",
+ autofilled: "smokey.mozilla.org/foo/",
+ completed: "http://www.smokey.mozilla.org/foo/",
+ });
+ yield cleanup();
+});
+
+add_task(function* test_complete_querystring() {
+ do_print("Check to make sure we autocomplete after ?");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious"));
+ yield check_autocomplete({
+ search: "smokey.mozilla.org/foo?",
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious",
+ });
+ yield cleanup();
+});
+
+add_task(function* test_complete_fragment() {
+ do_print("Check to make sure we autocomplete after #");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar"));
+ yield check_autocomplete({
+ search: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ autofilled: "smokey.mozilla.org/foo?bacon=delicious#bar",
+ completed: "http://smokey.mozilla.org/foo?bacon=delicious#bar",
+ });
+ yield cleanup();
+});
+
+add_task(function* test_autocomplete_enabled_pref() {
+ Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, false);
+ let types = ["history", "bookmark", "openpage"];
+ for (type of types) {
+ do_check_eq(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false,
+ "suggest." + type + "pref should be false");
+ }
+ Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, true);
+ for (type of types) {
+ do_check_eq(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true,
+ "suggest." + type + "pref should be true");
+ }
+
+ // Clear prefs.
+ Services.prefs.clearUserPref(PREF_AUTOCOMPLETE_ENABLED);
+ for (type of types) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js
new file mode 100644
index 0000000000..ecc96266b4
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_on_value_removed_479089.js
@@ -0,0 +1,39 @@
+/* -*- 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
+ */
+
+add_task(function* test_autocomplete_on_value_removed() {
+ let listener = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"].
+ getService(Components.interfaces.nsIAutoCompleteSimpleResultListener);
+
+ let testUri = NetUtil.newURI("http://foo.mozilla.com/");
+ yield PlacesTestUtils.addVisits({
+ uri: testUri,
+ referrer: uri("http://mozilla.com/")
+ });
+
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ // look for this uri only
+ query.uri = testUri;
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ // call the untested code path
+ listener.onValueRemoved(null, testUri.spec, true);
+ // make sure it is GONE from the DB
+ Assert.equal(root.childCount, 0);
+ // close the container
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js b/toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js
new file mode 100644
index 0000000000..482fcf4853
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_autofill_default_behavior.js
@@ -0,0 +1,310 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 autoFill for different default behaviors.
+ */
+
+add_task(function* test_default_behavior_host() {
+ let uri1 = NetUtil.newURI("http://typed/");
+ let uri2 = NetUtil.newURI("http://visited/");
+ let uri3 = NetUtil.newURI("http://bookmarked/");
+ let uri4 = NetUtil.newURI("http://tpbk/");
+ let uri5 = NetUtil.newURI("http://tagged/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "typed", transition: TRANSITION_TYPED },
+ { uri: uri2, title: "visited" },
+ { uri: uri4, title: "tpbk", transition: TRANSITION_TYPED },
+ ]);
+ yield addBookmark( { uri: uri3, title: "bookmarked" } );
+ yield addBookmark( { uri: uri4, title: "tpbk" } );
+ yield addBookmark( { uri: uri5, title: "title", tags: ["foo"] } );
+
+ yield setFaviconForHref(uri1.spec, "chrome://global/skin/icons/information-16.png");
+ yield setFaviconForHref(uri3.spec, "chrome://global/skin/icons/error-16.png");
+
+ // RESTRICT TO HISTORY.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+
+ do_print("Restrict history, common visit, should not autoFill");
+ yield check_autocomplete({
+ search: "vi",
+ matches: [ { uri: uri2, title: "visited" } ],
+ autofilled: "vi",
+ completed: "vi"
+ });
+
+ do_print("Restrict history, typed visit, should autoFill");
+ yield check_autocomplete({
+ search: "ty",
+ matches: [ { uri: uri1, title: "typed", style: [ "autofill", "heuristic" ],
+ icon: "chrome://global/skin/icons/information-16.png" } ],
+ autofilled: "typed/",
+ completed: "typed/"
+ });
+
+ // Don't autoFill this one cause it's not typed.
+ do_print("Restrict history, bookmark, should not autoFill");
+ yield check_autocomplete({
+ search: "bo",
+ matches: [ ],
+ autofilled: "bo",
+ completed: "bo"
+ });
+
+ // Note we don't show this one cause it's not typed.
+ do_print("Restrict history, typed bookmark, should autoFill");
+ yield check_autocomplete({
+ search: "tp",
+ matches: [ { uri: uri4, title: "tpbk", style: [ "autofill", "heuristic" ] } ],
+ autofilled: "tpbk/",
+ completed: "tpbk/"
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+
+ // We are not restricting on typed, so we autoFill the bookmark even if we
+ // are restricted to history. We accept that cause not doing that
+ // would be a perf hit and the privacy implications are very weak.
+ do_print("Restrict history, bookmark, autoFill.typed = false, should autoFill");
+ yield check_autocomplete({
+ search: "bo",
+ matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill", "heuristic" ],
+ icon: "chrome://global/skin/icons/error-16.png" } ],
+ autofilled: "bookmarked/",
+ completed: "bookmarked/"
+ });
+
+ do_print("Restrict history, common visit, autoFill.typed = false, should autoFill");
+ yield check_autocomplete({
+ search: "vi",
+ matches: [ { uri: uri2, title: "visited", style: [ "autofill", "heuristic" ] } ],
+ autofilled: "visited/",
+ completed: "visited/"
+ });
+
+ // RESTRICT TO TYPED.
+ // This should basically ignore autoFill.typed and acts as if it would be set.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
+
+ // Typed behavior basically acts like history, but filters on typed.
+ do_print("Restrict typed, common visit, autoFill.typed = false, should not autoFill");
+ yield check_autocomplete({
+ search: "vi",
+ matches: [ ],
+ autofilled: "vi",
+ completed: "vi"
+ });
+
+ do_print("Restrict typed, typed visit, autofill.typed = false, should autoFill");
+ yield check_autocomplete({
+ search: "ty",
+ matches: [ { uri: uri1, title: "typed", style: [ "autofill", "heuristic" ],
+ icon: "chrome://global/skin/icons/information-16.png"} ],
+ autofilled: "typed/",
+ completed: "typed/"
+ });
+
+ do_print("Restrict typed, bookmark, autofill.typed = false, should not autoFill");
+ yield check_autocomplete({
+ search: "bo",
+ matches: [ ],
+ autofilled: "bo",
+ completed: "bo"
+ });
+
+ do_print("Restrict typed, typed bookmark, autofill.typed = false, should autoFill");
+ yield check_autocomplete({
+ search: "tp",
+ matches: [ { uri: uri4, title: "tpbk", style: [ "autofill", "heuristic" ] } ],
+ autofilled: "tpbk/",
+ completed: "tpbk/"
+ });
+
+ // RESTRICT BOOKMARKS.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
+
+ do_print("Restrict bookmarks, common visit, should not autoFill");
+ yield check_autocomplete({
+ search: "vi",
+ matches: [ ],
+ autofilled: "vi",
+ completed: "vi"
+ });
+
+ do_print("Restrict bookmarks, typed visit, should not autoFill");
+ yield check_autocomplete({
+ search: "ty",
+ matches: [ ],
+ autofilled: "ty",
+ completed: "ty"
+ });
+
+ // Don't autoFill this one cause it's not typed.
+ do_print("Restrict bookmarks, bookmark, should not autoFill");
+ yield check_autocomplete({
+ search: "bo",
+ matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ],
+ icon: "chrome://global/skin/icons/error-16.png"} ],
+ autofilled: "bo",
+ completed: "bo"
+ });
+
+ // Note we don't show this one cause it's not typed.
+ do_print("Restrict bookmarks, typed bookmark, should autoFill");
+ yield check_autocomplete({
+ search: "tp",
+ matches: [ { uri: uri4, title: "tpbk", style: [ "autofill", "heuristic" ] } ],
+ autofilled: "tpbk/",
+ completed: "tpbk/"
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+
+ do_print("Restrict bookmarks, bookmark, autofill.typed = false, should autoFill");
+ yield check_autocomplete({
+ search: "bo",
+ matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill", "heuristic" ],
+ icon: "chrome://global/skin/icons/error-16.png" } ],
+ autofilled: "bookmarked/",
+ completed: "bookmarked/"
+ });
+
+ // Don't autofill because it's a title.
+ do_print("Restrict bookmarks, title, autofill.typed = false, should not autoFill");
+ yield check_autocomplete({
+ search: "# ta",
+ matches: [ ],
+ autofilled: "# ta",
+ completed: "# ta"
+ });
+
+ // Don't autofill because it's a tag.
+ do_print("Restrict bookmarks, tag, autofill.typed = false, should not autoFill");
+ yield check_autocomplete({
+ search: "+ ta",
+ matches: [ { uri: uri5, title: "title", tags: [ "foo" ], style: [ "tag" ] } ],
+ autofilled: "+ ta",
+ completed: "+ ta"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_default_behavior_url() {
+ let uri1 = NetUtil.newURI("http://typed/ty/");
+ let uri2 = NetUtil.newURI("http://visited/vi/");
+ let uri3 = NetUtil.newURI("http://bookmarked/bo/");
+ let uri4 = NetUtil.newURI("http://tpbk/tp/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "typed", transition: TRANSITION_TYPED },
+ { uri: uri2, title: "visited" },
+ { uri: uri4, title: "tpbk", transition: TRANSITION_TYPED },
+ ]);
+ yield addBookmark( { uri: uri3, title: "bookmarked" } );
+ yield addBookmark( { uri: uri4, title: "tpbk" } );
+
+ yield setFaviconForHref(uri1.spec, "chrome://global/skin/icons/information-16.png");
+ yield setFaviconForHref(uri3.spec, "chrome://global/skin/icons/error-16.png");
+
+ // RESTRICT TO HISTORY.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+
+ do_print("URL: Restrict history, common visit, should not autoFill");
+ yield check_autocomplete({
+ search: "visited/v",
+ matches: [ { uri: uri2, title: "visited" } ],
+ autofilled: "visited/v",
+ completed: "visited/v"
+ });
+
+ do_print("URL: Restrict history, typed visit, should autoFill");
+ yield check_autocomplete({
+ search: "typed/t",
+ matches: [ { uri: uri1, title: "typed/ty/", style: [ "autofill", "heuristic" ],
+ icon: "chrome://global/skin/icons/information-16.png"} ],
+ autofilled: "typed/ty/",
+ completed: "http://typed/ty/"
+ });
+
+ // Don't autoFill this one cause it's not typed.
+ do_print("URL: Restrict history, bookmark, should not autoFill");
+ yield check_autocomplete({
+ search: "bookmarked/b",
+ matches: [ ],
+ autofilled: "bookmarked/b",
+ completed: "bookmarked/b"
+ });
+
+ // Note we don't show this one cause it's not typed.
+ do_print("URL: Restrict history, typed bookmark, should autoFill");
+ yield check_autocomplete({
+ search: "tpbk/t",
+ matches: [ { uri: uri4, title: "tpbk/tp/", style: [ "autofill", "heuristic" ] } ],
+ autofilled: "tpbk/tp/",
+ completed: "http://tpbk/tp/"
+ });
+
+ // RESTRICT BOOKMARKS.
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+
+ do_print("URL: Restrict bookmarks, common visit, should not autoFill");
+ yield check_autocomplete({
+ search: "visited/v",
+ matches: [ ],
+ autofilled: "visited/v",
+ completed: "visited/v"
+ });
+
+ do_print("URL: Restrict bookmarks, typed visit, should not autoFill");
+ yield check_autocomplete({
+ search: "typed/t",
+ matches: [ ],
+ autofilled: "typed/t",
+ completed: "typed/t"
+ });
+
+ // Don't autoFill this one cause it's not typed.
+ do_print("URL: Restrict bookmarks, bookmark, should not autoFill");
+ yield check_autocomplete({
+ search: "bookmarked/b",
+ matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ],
+ icon: "chrome://global/skin/icons/error-16.png" } ],
+ autofilled: "bookmarked/b",
+ completed: "bookmarked/b"
+ });
+
+ // Note we don't show this one cause it's not typed.
+ do_print("URL: Restrict bookmarks, typed bookmark, should autoFill");
+ yield check_autocomplete({
+ search: "tpbk/t",
+ matches: [ { uri: uri4, title: "tpbk/tp/", style: [ "autofill", "heuristic" ] } ],
+ autofilled: "tpbk/tp/",
+ completed: "http://tpbk/tp/"
+ });
+
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+
+ do_print("URL: Restrict bookmarks, bookmark, autofill.typed = false, should autoFill");
+ yield check_autocomplete({
+ search: "bookmarked/b",
+ matches: [ { uri: uri3, title: "bookmarked/bo/", style: [ "autofill", "heuristic" ],
+ icon: "chrome://global/skin/icons/error-16.png" } ],
+ autofilled: "bookmarked/bo/",
+ completed: "http://bookmarked/bo/"
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js b/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js
new file mode 100644
index 0000000000..54fc343cac
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* test_prefix_space_noautofill() {
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://moz.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+
+ do_print("Should not try to autoFill if search string contains a space");
+ yield check_autocomplete({
+ search: " mo",
+ autofilled: " mo",
+ completed: " mo"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_trailing_space_noautofill() {
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://moz.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+
+ do_print("Should not try to autoFill if search string contains a space");
+ yield check_autocomplete({
+ search: "mo ",
+ autofilled: "mo ",
+ completed: "mo "
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("CakeSearch", "", "", "",
+ "GET", "http://cake.search/");
+ let engine = Services.search.getEngineByName("CakeSearch");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ do_print("Should autoFill search engine if search string does not contains a space");
+ yield check_autocomplete({
+ search: "ca",
+ autofilled: "cake.search",
+ completed: "http://cake.search"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_prefix_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("CupcakeSearch", "", "", "",
+ "GET", "http://cupcake.search/");
+ let engine = Services.search.getEngineByName("CupcakeSearch");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ do_print("Should not try to autoFill search engine if search string contains a space");
+ yield check_autocomplete({
+ search: " cu",
+ autofilled: " cu",
+ completed: " cu"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_trailing_space_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("BaconSearch", "", "", "",
+ "GET", "http://bacon.search/");
+ let engine = Services.search.getEngineByName("BaconSearch");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ do_print("Should not try to autoFill search engine if search string contains a space");
+ yield check_autocomplete({
+ search: "ba ",
+ autofilled: "ba ",
+ completed: "ba "
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_www_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("HamSearch", "", "", "",
+ "GET", "http://ham.search/");
+ let engine = Services.search.getEngineByName("HamSearch");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ do_print("Should not autoFill search engine if search string contains www. but engine doesn't");
+ yield check_autocomplete({
+ search: "www.ham",
+ autofilled: "www.ham",
+ completed: "www.ham"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_different_scheme_noautofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("PieSearch", "", "", "",
+ "GET", "https://pie.search/");
+ let engine = Services.search.getEngineByName("PieSearch");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ do_print("Should not autoFill search engine if search string has a different scheme.");
+ yield check_autocomplete({
+ search: "http://pie",
+ autofilled: "http://pie",
+ completed: "http://pie"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_matching_prefix_autofill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("BeanSearch", "", "", "",
+ "GET", "http://www.bean.search/");
+ let engine = Services.search.getEngineByName("BeanSearch");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+
+ do_print("Should autoFill search engine if search string has matching prefix.");
+ yield check_autocomplete({
+ search: "http://www.be",
+ autofilled: "http://www.bean.search",
+ completed: "http://www.bean.search"
+ })
+
+ do_print("Should autoFill search engine if search string has www prefix.");
+ yield check_autocomplete({
+ search: "www.be",
+ autofilled: "www.bean.search",
+ completed: "http://www.bean.search"
+ });
+
+ do_print("Should autoFill search engine if search string has matching scheme.");
+ yield check_autocomplete({
+ search: "http://be",
+ autofilled: "http://bean.search",
+ completed: "http://www.bean.search"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_prefix_autofill() {
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://moz.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+
+ do_print("Should not try to autoFill in-the-middle if a search is canceled immediately");
+ yield check_autocomplete({
+ incompleteSearch: "moz",
+ search: "mozi",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js b/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js
new file mode 100644
index 0000000000..1fcfe1c751
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* test_protocol_trimming() {
+ for (let prot of ["http", "https", "ftp"]) {
+ let visit = {
+ // Include the protocol in the query string to ensure we get matches (see bug 1059395)
+ uri: NetUtil.newURI(prot + "://www.mozilla.org/test/?q=" + prot + encodeURIComponent("://") + "www.foo"),
+ title: "Test title",
+ transition: TRANSITION_TYPED
+ };
+ yield PlacesTestUtils.addVisits(visit);
+ let matches = [{uri: visit.uri, title: visit.title}];
+
+ let inputs = [
+ prot + "://",
+ prot + ":// ",
+ prot + ":// mo",
+ prot + "://mo te",
+ prot + "://www.",
+ prot + "://www. ",
+ prot + "://www. mo",
+ prot + "://www.mo te",
+ "www.",
+ "www. ",
+ "www. mo",
+ "www.mo te"
+ ];
+ for (let input of inputs) {
+ do_print("Searching for: " + input);
+ yield check_autocomplete({
+ search: input,
+ matches: matches
+ });
+ }
+
+ yield cleanup();
+ }
+});
+
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_casing.js b/toolkit/components/places/tests/unifiedcomplete/test_casing.js
new file mode 100644
index 0000000000..585b51be10
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_casing.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* test_casing_1() {
+ do_print("Searching for cased entry 1");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "MOZ",
+ autofilled: "MOZilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_casing_2() {
+ do_print("Searching for cased entry 2");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/T",
+ autofilled: "mozilla.org/T",
+ completed: "mozilla.org/T"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_casing_3() {
+ do_print("Searching for cased entry 3");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/T",
+ autofilled: "mozilla.org/Test/",
+ completed: "http://mozilla.org/Test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_casing_4() {
+ do_print("Searching for cased entry 4");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mOzilla.org/t",
+ autofilled: "mOzilla.org/t",
+ completed: "mOzilla.org/t"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_casing_5() {
+ do_print("Searching for cased entry 5");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mOzilla.org/T",
+ autofilled: "mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_casing() {
+ do_print("Searching for untrimmed cased entry");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "http://mOz",
+ autofilled: "http://mOzilla.org/",
+ completed: "http://mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_www_casing() {
+ do_print("Searching for untrimmed cased entry with www");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://www.mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "http://www.mOz",
+ autofilled: "http://www.mOzilla.org/",
+ completed: "http://www.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_path_casing() {
+ do_print("Searching for untrimmed cased entry with path");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "http://mOzilla.org/t",
+ autofilled: "http://mOzilla.org/t",
+ completed: "http://mOzilla.org/t"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_path_casing_2() {
+ do_print("Searching for untrimmed cased entry with path 2");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "http://mOzilla.org/T",
+ autofilled: "http://mOzilla.org/Test/",
+ completed: "http://mozilla.org/Test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_path_www_casing() {
+ do_print("Searching for untrimmed cased entry with www and path");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://www.mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "http://www.mOzilla.org/t",
+ autofilled: "http://www.mOzilla.org/t",
+ completed: "http://www.mOzilla.org/t"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_path_www_casing_2() {
+ do_print("Searching for untrimmed cased entry with www and path 2");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://www.mozilla.org/Test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "http://www.mOzilla.org/T",
+ autofilled: "http://www.mOzilla.org/Test/",
+ completed: "http://www.mozilla.org/Test/"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js b/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js
new file mode 100644
index 0000000000..014d749988
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Inline should never return matches shorter than the search string, since
+// that largely confuses completeDefaultIndex
+
+add_task(function* test_not_autofill_ws_1() {
+ do_print("Do not autofill whitespaced entry 1");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org ",
+ autofilled: "mozilla.org ",
+ completed: "mozilla.org "
+ });
+ yield cleanup();
+});
+
+add_task(function* test_not_autofill_ws_2() {
+ do_print("Do not autofill whitespaced entry 2");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/ ",
+ autofilled: "mozilla.org/ ",
+ completed: "mozilla.org/ "
+ });
+ yield cleanup();
+});
+
+add_task(function* test_not_autofill_ws_3() {
+ do_print("Do not autofill whitespaced entry 3");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/link ",
+ autofilled: "mozilla.org/link ",
+ completed: "mozilla.org/link "
+ });
+ yield cleanup();
+});
+
+add_task(function* test_not_autofill_ws_4() {
+ do_print("Do not autofill whitespaced entry 4");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/link/ ",
+ autofilled: "mozilla.org/link/ ",
+ completed: "mozilla.org/link/ "
+ });
+ yield cleanup();
+});
+
+
+add_task(function* test_not_autofill_ws_5() {
+ do_print("Do not autofill whitespaced entry 5");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "moz illa ",
+ autofilled: "moz illa ",
+ completed: "moz illa "
+ });
+ yield cleanup();
+});
+
+add_task(function* test_not_autofill_ws_6() {
+ do_print("Do not autofill whitespaced entry 6");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: " mozilla",
+ autofilled: " mozilla",
+ completed: " mozilla"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js b/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js
new file mode 100644
index 0000000000..72661d0750
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js
@@ -0,0 +1,71 @@
+/* -*- 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.
+ */
+
+add_task(function* test_download_embed_bookmarks() {
+ let uri1 = NetUtil.newURI("http://download/bookmarked");
+ let uri2 = NetUtil.newURI("http://embed/bookmarked");
+ let uri3 = NetUtil.newURI("http://framed/bookmarked");
+ let uri4 = NetUtil.newURI("http://download");
+ let uri5 = NetUtil.newURI("http://embed");
+ let uri6 = NetUtil.newURI("http://framed");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "download-bookmark", transition: TRANSITION_DOWNLOAD },
+ { uri: uri2, title: "embed-bookmark", transition: TRANSITION_EMBED },
+ { uri: uri3, title: "framed-bookmark", transition: TRANSITION_FRAMED_LINK},
+ { uri: uri4, title: "download2", transition: TRANSITION_DOWNLOAD },
+ { uri: uri5, title: "embed2", transition: TRANSITION_EMBED },
+ { uri: uri6, title: "framed2", transition: TRANSITION_FRAMED_LINK }
+ ]);
+ yield addBookmark({ uri: uri1,
+ title: "download-bookmark" });
+ yield addBookmark({ uri: uri2,
+ title: "embed-bookmark" });
+ yield addBookmark({ uri: uri3,
+ title: "framed-bookmark" });
+
+ do_print("Searching for bookmarked download uri matches");
+ yield check_autocomplete({
+ search: "download-bookmark",
+ matches: [ { uri: uri1, title: "download-bookmark", style: [ "bookmark" ] } ]
+ });
+
+ do_print("Searching for bookmarked embed uri matches");
+ yield check_autocomplete({
+ search: "embed-bookmark",
+ matches: [ { uri: uri2, title: "embed-bookmark", style: [ "bookmark" ] } ]
+ });
+
+ do_print("Searching for bookmarked framed uri matches");
+ yield check_autocomplete({
+ search: "framed-bookmark",
+ matches: [ { uri: uri3, title: "framed-bookmark", style: [ "bookmark" ] } ]
+ });
+
+ do_print("Searching for download uri does not match");
+ yield check_autocomplete({
+ search: "download2",
+ matches: [ ]
+ });
+
+ do_print("Searching for embed uri does not match");
+ yield check_autocomplete({
+ search: "embed2",
+ matches: [ ]
+ });
+
+ do_print("Searching for framed uri does not match");
+ yield check_autocomplete({
+ search: "framed2",
+ matches: [ ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js b/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js
new file mode 100644
index 0000000000..a39c152364
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure inline autocomplete doesn't return zero frecency pages.
+
+add_task(function* test_dupe_urls() {
+ do_print("Searching for urls with dupes should only show one");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/"),
+ transition: TRANSITION_TYPED
+ }, {
+ uri: NetUtil.newURI("http://mozilla.org/?")
+ });
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/",
+ matches: [ { uri: NetUtil.newURI("http://mozilla.org/"),
+ title: "mozilla.org",
+ style: [ "autofill", "heuristic" ] } ]
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js
new file mode 100644
index 0000000000..ef1159705d
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ */
+
+add_task(function* test_javascript_match() {
+ let uri1 = NetUtil.newURI("http://t.foo/0");
+ let uri2 = NetUtil.newURI("http://t.foo/1");
+ let uri3 = NetUtil.newURI("http://t.foo/2");
+ let uri4 = NetUtil.newURI("http://t.foo/3");
+ let uri5 = NetUtil.newURI("http://t.foo/4");
+ let uri6 = NetUtil.newURI("http://t.foo/5");
+ let uri7 = NetUtil.newURI("http://t.foo/6");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri3, title: "title", transition: TRANSITION_TYPED},
+ { uri: uri4, title: "title", transition: TRANSITION_TYPED },
+ { uri: uri6, title: "title", transition: TRANSITION_TYPED },
+ { uri: uri7, title: "title" }
+ ]);
+
+ yield addBookmark({ uri: uri2,
+ title: "title" });
+ yield addBookmark({ uri: uri4,
+ title: "title" });
+ yield addBookmark({ uri: uri5,
+ title: "title" });
+ yield addBookmark({ uri: uri6,
+ title: "title" });
+
+ addOpenPages(uri7, 1);
+
+ // Now remove page 6 from history, so it is an unvisited bookmark.
+ PlacesUtils.history.removePage(uri6);
+
+ do_print("Match everything");
+ yield check_autocomplete({
+ search: "foo",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("foo", { heuristic: true }),
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title", style: ["bookmark"] },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "title", style: ["bookmark"] },
+ { uri: uri5, title: "title", style: ["bookmark"] },
+ { uri: uri6, title: "title", style: ["bookmark"] },
+ makeSwitchToTabMatch("http://t.foo/6", { title: "title" }),
+ ]
+ });
+
+ // Note the next few tests do *not* get a search result as enable-actions
+ // isn't specified.
+ do_print("Match only typed history");
+ yield check_autocomplete({
+ search: "foo ^ ~",
+ matches: [ { uri: uri3, title: "title" },
+ { uri: uri4, title: "title" } ]
+ });
+
+ do_print("Drop-down empty search matches only typed history");
+ yield check_autocomplete({
+ search: "",
+ matches: [ { uri: uri3, title: "title" },
+ { uri: uri4, title: "title" } ]
+ });
+
+ do_print("Drop-down empty search matches only bookmarks");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ yield check_autocomplete({
+ search: "",
+ matches: [ { uri: uri2, title: "title", style: ["bookmark"] },
+ { uri: uri4, title: "title", style: ["bookmark"] },
+ { uri: uri5, title: "title", style: ["bookmark"] },
+ { uri: uri6, title: "title", style: ["bookmark"] } ]
+ });
+
+ do_print("Drop-down empty search matches only open tabs");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ yield check_autocomplete({
+ search: "",
+ searchParam: "enable-actions",
+ matches: [
+ makeSwitchToTabMatch("http://t.foo/6", { title: "title" }),
+ ]
+ });
+
+ Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+ Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark");
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
new file mode 100644
index 0000000000..dee8df8ecf
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
@@ -0,0 +1,68 @@
+add_task(function* test_enabled() {
+ // Test for bug 471903 to make sure searching in autocomplete can be turned on
+ // and off. Also test bug 463535 for pref changing search.
+ let uri = NetUtil.newURI("http://url/0");
+ yield PlacesTestUtils.addVisits([ { uri: uri, title: "title" } ]);
+
+ do_print("plain search");
+ yield check_autocomplete({
+ search: "url",
+ matches: [ { uri: uri, title: "title" } ]
+ });
+
+ do_print("search disabled");
+ Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
+ yield check_autocomplete({
+ search: "url",
+ matches: [ ]
+ });
+
+ do_print("resume normal search");
+ Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true);
+ yield check_autocomplete({
+ search: "url",
+ matches: [ { uri: uri, title: "title" } ]
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_sync_enabled() {
+ // Initialize unified complete.
+ Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+ .getService(Ci.mozIPlacesAutoComplete);
+
+ let types = [ "history", "bookmark", "openpage", "searches" ];
+
+ // 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")) {
+ if (type == "searches") {
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false);
+ } else {
+ Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true);
+ }
+ }
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js b/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js
new file mode 100644
index 0000000000..ff6e5f929e
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js
@@ -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/. */
+
+/**
+ * Test bug 422698 to make sure searches with urls from the location bar
+ * correctly match itself when it contains escaped characters.
+ */
+
+add_task(function* test_escape() {
+ let uri1 = NetUtil.newURI("http://unescapeduri/");
+ let uri2 = NetUtil.newURI("http://escapeduri/%40/");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" }
+ ]);
+
+ do_print("Unescaped location matches itself");
+ yield check_autocomplete({
+ search: "http://unescapeduri/",
+ matches: [ { uri: uri1, title: "title" } ]
+ });
+
+ do_print("Escaped location matches itself");
+ yield check_autocomplete({
+ search: "http://escapeduri/%40/",
+ matches: [ { uri: uri2, title: "title" } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
new file mode 100644
index 0000000000..76af20558d
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
@@ -0,0 +1,384 @@
+/* -*- 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/. */
+
+Cu.import("resource://gre/modules/ExtensionSearchHandler.jsm");
+
+let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(Ci.nsIAutoCompleteController);
+
+add_task(function* test_correct_errors_are_thrown() {
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+ let unregisteredKeyword = "baz";
+
+ // Register a keyword.
+ ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
+
+ // Try registering the keyword again.
+ Assert.throws(() => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }));
+
+ // Register a different keyword.
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
+
+ // Try calling handleSearch for an unregistered keyword.
+ Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `, () => {}));
+
+ // Try calling handleSearch without a callback.
+ Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `));
+
+ // Try getting the description for a keyword which isn't registered.
+ Assert.throws(() => ExtensionSearchHandler.getDescription(unregisteredKeyword));
+
+ // Try getting the extension name for a keyword which isn't registered.
+ Assert.throws(() => ExtensionSearchHandler.getExtensionName(unregisteredKeyword));
+
+ // Try setting the default suggestion for a keyword which isn't registered.
+ Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, "suggestion"));
+
+ // Try calling handleInputCancelled when there is no active input session.
+ Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
+
+ // Try calling handleInputEntered when there is no active input session.
+ Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
+
+ // Start a session by calling handleSearch with the registered keyword.
+ ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {});
+
+ // Try providing suggestions for an unregistered keyword.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []));
+
+ // Try providing suggestions for an inactive keyword.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []));
+
+ // Try calling handleSearch for an inactive keyword.
+ Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} `, () => {}));
+
+ // Try calling addSuggestions with an old callback ID.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, []));
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Add suggestions again with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+ // Try calling addSuggestions with a future callback ID.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
+
+ // End the input session by calling handleInputCancelled.
+ ExtensionSearchHandler.handleInputCancelled();
+
+ // Try calling handleInputCancelled after the session has ended.
+ Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
+
+ // Try calling handleSearch that doesn't have a space after the keyword.
+ Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword}`, () => {}));
+
+ // Try calling handleSearch with text starting with the wrong keyword.
+ Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${keyword} test`, () => {}));
+
+ // Start a new session by calling handleSearch with a different keyword
+ ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {});
+
+ // Try adding suggestions again with the same callback ID now that the input session has ended.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 1, []));
+
+ // Add suggestions with a valid callback ID.
+ ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
+
+ // Try adding suggestions with a valid callback ID but a different keyword.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
+
+ // Try adding suggestions with a valid callback ID but an unregistered keyword.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []));
+
+ // Set the default suggestion.
+ ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"});
+
+ // Try ending the session using handleInputEntered with a different keyword.
+ Assert.throws(() => ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} test`, "tab"));
+
+ // Try calling handleInputEntered with invalid text.
+ Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"));
+
+ // Try calling handleInputEntered with an invalid disposition.
+ Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "invalid"));
+
+ // End the session by calling handleInputEntered.
+ ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab");
+
+ // Try calling handleInputEntered after the session has ended.
+ Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
+
+ // Unregister the keyword.
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+
+ // Try setting the default suggestion for the unregistered keyword.
+ Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(keyword, {description: "test"}));
+
+ // Try handling a search with the unregistered keyword.
+ Assert.throws(() => ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {}));
+
+ // Try unregistering the keyword again.
+ Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(keyword));
+
+ // Unregister the other keyword.
+ ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
+
+ // Try unregistering the word which was never registered.
+ Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword));
+
+ // Try setting the default suggestion for a word that was never registered.
+ Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {description: "test"}));
+
+ yield cleanup();
+});
+
+add_task(function* test_correct_events_are_emitted() {
+ let events = [];
+ function checkEvents(expectedEvents) {
+ Assert.equal(events.length, expectedEvents.length, "The correct number of events fired");
+ expectedEvents.forEach((e, i) => Assert.equal(e, events[i], `Expected "${e}" event to fire`));
+ events = [];
+ }
+
+ let mockExtension = { emit: message => events.push(message) };
+
+ let keyword = "foo";
+ let anotherKeyword = "bar";
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+ ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
+
+ ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
+
+ ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
+
+ ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} f`, "tab");
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED
+ ]);
+
+ ExtensionSearchHandler.handleInputCancelled();
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
+
+ ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} baz`, () => {});
+ checkEvents([
+ ExtensionSearchHandler.MSG_INPUT_STARTED,
+ ExtensionSearchHandler.MSG_INPUT_CHANGED
+ ]);
+
+ ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} baz`, "tab");
+ checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+});
+
+add_task(function* test_removes_suggestion_if_its_content_is_typed_in() {
+ let keyword = "test";
+ let extensionName = "Foo Bar";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ {content: "foo", description: "first suggestion"},
+ {content: "bar", description: "second suggestion"},
+ {content: "baz", description: "third suggestion"},
+ ]);
+ controller.stopSearch();
+ }
+ }
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ yield check_autocomplete({
+ search: `${keyword} unmatched`,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} unmatched`}),
+ makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
+ ]
+ });
+
+ yield check_autocomplete({
+ search: `${keyword} foo`,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} foo`}),
+ makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
+ ]
+ });
+
+ yield check_autocomplete({
+ search: `${keyword} bar`,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} bar`}),
+ makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
+ ]
+ });
+
+ yield check_autocomplete({
+ search: `${keyword} baz`,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} baz`}),
+ makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"})
+ ]
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ yield cleanup();
+});
+
+add_task(function* test_extension_results_should_come_first() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let uri = NetUtil.newURI(`http://a.com/b`);
+ yield PlacesTestUtils.addVisits([
+ { uri, title: `${keyword} -` },
+ ]);
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ {content: "foo", description: "first suggestion"},
+ {content: "bar", description: "second suggestion"},
+ {content: "baz", description: "third suggestion"},
+ ]);
+ }
+ controller.stopSearch();
+ }
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
+
+ yield check_autocomplete({
+ search: `${keyword} -`,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} -`}),
+ makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}),
+ { uri, title: `${keyword} -` }
+ ]
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ yield cleanup();
+});
+
+add_task(function* test_setting_the_default_suggestion() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, []);
+ }
+ controller.stopSearch();
+ }
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "hello world"
+ });
+
+ let searchString = `${keyword} search query`;
+ yield check_autocomplete({
+ search: searchString,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: "hello world", content: searchString}),
+ ]
+ });
+
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+ description: "foo bar"
+ });
+
+ yield check_autocomplete({
+ search: searchString,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: "foo bar", content: searchString}),
+ ]
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ yield cleanup();
+});
+
+add_task(function* test_maximum_number_of_suggestions_is_enforced() {
+ let keyword = "test";
+ let extensionName = "Omnibox Example";
+
+ let mockExtension = {
+ name: extensionName,
+ emit(message, text, id) {
+ if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+ ExtensionSearchHandler.addSuggestions(keyword, id, [
+ {content: "a", description: "first suggestion"},
+ {content: "b", description: "second suggestion"},
+ {content: "c", description: "third suggestion"},
+ {content: "d", description: "fourth suggestion"},
+ {content: "e", description: "fifth suggestion"},
+ {content: "f", description: "sixth suggestion"},
+ {content: "g", description: "seventh suggestion"},
+ {content: "h", description: "eigth suggestion"},
+ {content: "i", description: "ninth suggestion"},
+ {content: "j", description: "tenth suggestion"},
+ ]);
+ controller.stopSearch();
+ }
+ }
+ };
+
+ ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+ // Start an input session before testing MSG_INPUT_CHANGED.
+ ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
+
+ yield check_autocomplete({
+ search: `${keyword} #`,
+ searchParam: "enable-actions",
+ matches: [
+ makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} #`}),
+ makeExtensionMatch({keyword, content: `${keyword} a`, description: "first suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} b`, description: "second suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} c`, description: "third suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} d`, description: "fourth suggestion"}),
+ makeExtensionMatch({keyword, content: `${keyword} e`, description: "fifth suggestion"}),
+ ]
+ });
+
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js b/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js
new file mode 100644
index 0000000000..92e7f601a1
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js
@@ -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/. */
+
+/**
+ * Test bug 424509 to make sure searching for "h" doesn't match "http" of urls.
+ */
+
+add_task(function* test_escape() {
+ let uri1 = NetUtil.newURI("http://site/");
+ let uri2 = NetUtil.newURI("http://happytimes/");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" }
+ ]);
+
+ do_print("Searching for h matches site and not http://");
+ yield check_autocomplete({
+ search: "h",
+ matches: [ { uri: uri2, title: "title" } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
new file mode 100644
index 0000000000..12b7fea777
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/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.
+ */
+
+add_task(function* test_keyword_searc() {
+ let uri1 = NetUtil.newURI("http://abc/?search=%s");
+ let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "Generic page title" },
+ { uri: uri2, title: "Generic page title" }
+ ]);
+ yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
+
+ do_print("Plain keyword query");
+ yield check_autocomplete({
+ search: "key term",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Plain keyword UC");
+ yield check_autocomplete({
+ search: "key TERM",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=TERM"), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Multi-word keyword query");
+ yield check_autocomplete({
+ search: "key multi word",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=multi%20word"), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Keyword query with +");
+ yield check_autocomplete({
+ search: "key blocking+",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Unescaped term in query");
+ yield check_autocomplete({
+ search: "key ユニコード",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Keyword that happens to match a page");
+ yield check_autocomplete({
+ search: "key ThisPageIsInHistory",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Keyword without query (without space)");
+ yield check_autocomplete({
+ search: "key",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Keyword without query (with space)");
+ yield check_autocomplete({
+ search: "key ",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
new file mode 100644
index 0000000000..61d98f72d6
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.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/. */
+
+/**
+ * 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.
+ */
+
+add_task(function* test_keyword_search() {
+ let uri1 = NetUtil.newURI("http://abc/?search=%s");
+ let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory");
+ let uri3 = NetUtil.newURI("http://abc/?search=%s&raw=%S");
+ let uri4 = NetUtil.newURI("http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1");
+ yield PlacesTestUtils.addVisits([{ uri: uri1 },
+ { uri: uri2 },
+ { uri: uri3 }]);
+ yield addBookmark({ uri: uri1, title: "Keyword", keyword: "key"});
+ yield addBookmark({ uri: uri1, title: "Post", keyword: "post", postData: "post_search=%s"});
+ yield addBookmark({ uri: uri3, title: "Encoded", keyword: "encoded"});
+ yield addBookmark({ uri: uri4, title: "Charset", keyword: "charset"});
+ yield addBookmark({ uri: uri2, title: "Noparam", keyword: "noparam"});
+ yield addBookmark({ uri: uri2, title: "Noparam-Post", keyword: "post_noparam", postData: "noparam=1"});
+
+ do_print("Plain keyword query");
+ yield check_autocomplete({
+ search: "key term",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Plain keyword UC");
+ yield check_autocomplete({
+ search: "key TERM",
+ matches: [ { uri: NetUtil.newURI("http://abc/?search=TERM"),
+ title: "abc", style: ["keyword", "heuristic"] } ]
+ });
+
+ do_print("Multi-word keyword query");
+ yield check_autocomplete({
+ search: "key multi word",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi%20word", input: "key multi word"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Keyword query with +");
+ yield check_autocomplete({
+ search: "key blocking+",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Unescaped term in query");
+ // ... but note that UnifiedComplete calls encodeURIComponent() on the query
+ // string when it builds the URL, so the expected result will have the
+ // ユニコード substring encoded in the URL.
+ yield check_autocomplete({
+ search: "key ユニコード",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=" + encodeURIComponent("ユニコード"), input: "key ユニコード"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Keyword that happens to match a page");
+ yield check_autocomplete({
+ search: "key ThisPageIsInHistory",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Keyword without query (without space)");
+ yield check_autocomplete({
+ search: "key",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Keyword without query (with space)");
+ yield check_autocomplete({
+ search: "key ",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("POST Keyword");
+ yield check_autocomplete({
+ search: "post foo",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=foo", input: "post foo", postData: "post_search=foo"}),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Bug 420328: no-param keyword with a param");
+ yield check_autocomplete({
+ search: "noparam foo",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("noparam foo", { heuristic: true }) ]
+ });
+ yield check_autocomplete({
+ search: "post_noparam foo",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("post_noparam foo", { heuristic: true }) ]
+ });
+
+ do_print("escaping with default UTF-8 charset");
+ yield check_autocomplete({
+ search: "encoded foé",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=fo%C3%A9&raw=foé", input: "encoded foé" }),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("escaping with forced ISO-8859-1 charset");
+ yield check_autocomplete({
+ search: "charset foé",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=fo%E9&raw=foé", input: "charset foé" }),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Bug 359809: escaping +, / and @ with default UTF-8 charset");
+ yield check_autocomplete({
+ search: "encoded +/@",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=%2B%2F%40&raw=+/@", input: "encoded +/@" }),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ do_print("Bug 359809: escaping +, / and @ with forced ISO-8859-1 charset");
+ yield check_autocomplete({
+ search: "charset +/@",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=%2B%2F%40&raw=+/@", input: "charset +/@" }),
+ title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keywords.js b/toolkit/components/places/tests/unifiedcomplete/test_keywords.js
new file mode 100644
index 0000000000..93e8d7a6f1
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keywords.js
@@ -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/. */
+
+add_task(function* test_non_keyword() {
+ do_print("Searching for non-keyworded entry should autoFill it");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/") });
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_keyword() {
+ do_print("Searching for keyworded entry should not autoFill it");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" });
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "moz",
+ completed: "moz",
+ });
+ yield cleanup();
+});
+
+add_task(function* test_more_than_keyword() {
+ do_print("Searching for more than keyworded entry should autoFill it");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" });
+ yield check_autocomplete({
+ search: "mozi",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_less_than_keyword() {
+ do_print("Searching for less than keyworded entry should autoFill it");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/",
+ });
+ yield cleanup();
+});
+
+add_task(function* test_keyword_casing() {
+ do_print("Searching for keyworded entry is case-insensitive");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" });
+ yield check_autocomplete({
+ search: "MoZ",
+ autofilled: "MoZ",
+ completed: "MoZ"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js b/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js
new file mode 100644
index 0000000000..57a1efaeb5
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ */
+
+add_task(function* test_match_beginning() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+
+ let uri1 = NetUtil.newURI("http://x.com/y");
+ let uri2 = NetUtil.newURI("https://y.com/x");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "a b" },
+ { uri: uri2, title: "b a" }
+ ]);
+
+ do_print("Match at the beginning of titles");
+ Services.prefs.setIntPref("browser.urlbar.matchBehavior", 3);
+ yield check_autocomplete({
+ search: "a",
+ matches: [ { uri: uri1, title: "a b" } ]
+ });
+
+ do_print("Match at the beginning of titles");
+ yield check_autocomplete({
+ search: "b",
+ matches: [ { uri: uri2, title: "b a" } ]
+ });
+
+ do_print("Match at the beginning of urls");
+ yield check_autocomplete({
+ search: "x",
+ matches: [ { uri: uri1, title: "a b" } ]
+ });
+
+ do_print("Match at the beginning of urls");
+ yield check_autocomplete({
+ search: "y",
+ matches: [ { uri: uri2, title: "b a" } ]
+ });
+
+ do_print("Sanity check that matching anywhere finds more");
+ Services.prefs.setIntPref("browser.urlbar.matchBehavior", 1);
+ yield check_autocomplete({
+ search: "a",
+ matches: [ { uri: uri1, title: "a b" },
+ { uri: uri2, title: "b a" } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js b/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js
new file mode 100644
index 0000000000..c6c9e952ee
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.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/. */
+
+/**
+ * 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.
+ */
+
+add_task(function* test_match_beginning() {
+ let uri1 = NetUtil.newURI("http://a.b.c/d-e_f/h/t/p");
+ let uri2 = NetUtil.newURI("http://d.e.f/g-h_i/h/t/p");
+ let uri3 = NetUtil.newURI("http://g.h.i/j-k_l/h/t/p");
+ let uri4 = NetUtil.newURI("http://j.k.l/m-n_o/h/t/p");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "f(o)o b<a>r" },
+ { uri: uri2, title: "b(a)r b<a>z" },
+ { uri: uri3, title: "f(o)o b<a>r" },
+ { uri: uri4, title: "f(o)o b<a>r" }
+ ]);
+ yield addBookmark({ uri: uri3, title: "f(o)o b<a>r" });
+ yield addBookmark({ uri: uri4, title: "b(a)r b<a>z" });
+
+ do_print("Match 2 terms all in url");
+ yield check_autocomplete({
+ search: "c d",
+ matches: [ { uri: uri1, title: "f(o)o b<a>r" } ]
+ });
+
+ do_print("Match 1 term in url and 1 term in title");
+ yield check_autocomplete({
+ search: "b e",
+ matches: [ { uri: uri1, title: "f(o)o b<a>r" },
+ { uri: uri2, title: "b(a)r b<a>z" } ]
+ });
+
+ do_print("Match 3 terms all in title; display bookmark title if matched");
+ yield check_autocomplete({
+ search: "b a z",
+ matches: [ { uri: uri2, title: "b(a)r b<a>z" },
+ { uri: uri4, title: "b(a)r b<a>z", style: [ "bookmark" ] } ]
+ });
+
+ do_print("Match 2 terms in url and 1 in title; make sure bookmark title is used for search");
+ yield check_autocomplete({
+ search: "k f t",
+ matches: [ { uri: uri3, title: "f(o)o b<a>r", style: [ "bookmark" ] } ]
+ });
+
+ do_print("Match 3 terms in url and 1 in title");
+ yield check_autocomplete({
+ search: "d i g z",
+ matches: [ { uri: uri2, title: "b(a)r b<a>z" } ]
+ });
+
+ do_print("Match nothing");
+ yield check_autocomplete({
+ search: "m o z i",
+ matches: [ ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_query_url.js b/toolkit/components/places/tests/unifiedcomplete/test_query_url.js
new file mode 100644
index 0000000000..915ba770ef
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_query_url.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/. */
+
+add_task(function* test_no_slash() {
+ do_print("Searching for host match without slash should match host");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://file.org/test/"),
+ transition: TRANSITION_TYPED
+ }, {
+ uri: NetUtil.newURI("file:///c:/test.html"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "file",
+ autofilled: "file.org/",
+ completed: "file.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_w_slash() {
+ do_print("Searching match with slash at the end should do nothing");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://file.org/test/"),
+ transition: TRANSITION_TYPED
+ }, {
+ uri: NetUtil.newURI("file:///c:/test.html"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "file.org/",
+ autofilled: "file.org/",
+ completed: "file.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_middle() {
+ do_print("Searching match with slash in the middle should match url");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://file.org/test/"),
+ transition: TRANSITION_TYPED
+ }, {
+ uri: NetUtil.newURI("file:///c:/test.html"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "file.org/t",
+ autofilled: "file.org/test/",
+ completed: "http://file.org/test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_nonhost() {
+ do_print("Searching for non-host match without slash should not match url");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("file:///c:/test.html"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "file",
+ autofilled: "file",
+ completed: "file"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js
new file mode 100644
index 0000000000..56998d4d69
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_remote_tab_matches.js
@@ -0,0 +1,203 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+*/
+"use strict";
+
+Cu.import("resource://services-sync/main.js");
+
+Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com");
+
+// A mock "Tabs" engine which autocomplete will use instead of the real
+// engine. We pass a constructor that Sync creates.
+function MockTabsEngine() {
+ this.clients = null; // We'll set this dynamically
+}
+
+MockTabsEngine.prototype = {
+ name: "tabs",
+
+ getAllClients() {
+ return this.clients;
+ },
+}
+
+// A clients engine that doesn't need to be a constructor.
+let MockClientsEngine = {
+ isMobile(guid) {
+ Assert.ok(guid.endsWith("desktop") || guid.endsWith("mobile"));
+ return guid.endsWith("mobile");
+ },
+}
+
+// Tell Sync about the mocks.
+Weave.Service.engineManager.register(MockTabsEngine);
+Weave.Service.clientsEngine = MockClientsEngine;
+
+// Tell the Sync XPCOM service it is initialized.
+let weaveXPCService = Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+weaveXPCService.ready = true;
+
+// Configure the singleton engine for a test.
+function configureEngine(clients) {
+ // Configure the instance Sync created.
+ let engine = Weave.Service.engineManager.get("tabs");
+ engine.clients = clients;
+ // Send an observer that pretends the engine just finished a sync.
+ Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
+}
+
+// Make a match object suitable for passing to check_autocomplete.
+function makeRemoteTabMatch(url, deviceName, extra = {}) {
+ return {
+ uri: makeActionURI("remotetab", {url, deviceName}),
+ title: extra.title || url,
+ style: [ "action", "remotetab" ],
+ icon: extra.icon,
+ }
+}
+
+// The tests.
+add_task(function* test_nomatch() {
+ // Nothing matches.
+ configureEngine({
+ guid_desktop: {
+ clientName: "My Desktop",
+ tabs: [{
+ urlHistory: ["http://foo.com/"],
+ }],
+ }
+ });
+
+ // No remote tabs match here, so we only expect search results.
+ yield check_autocomplete({
+ search: "ex",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("ex", { heuristic: true }) ],
+ });
+});
+
+add_task(function* test_minimal() {
+ // The minimal client and tabs info we can get away with.
+ configureEngine({
+ guid_desktop: {
+ clientName: "My Desktop",
+ tabs: [{
+ urlHistory: ["http://example.com/"],
+ }],
+ }
+ });
+
+ yield check_autocomplete({
+ search: "ex",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("ex", { heuristic: true }),
+ makeRemoteTabMatch("http://example.com/", "My Desktop") ],
+ });
+});
+
+add_task(function* test_maximal() {
+ // Every field that could possibly exist on a remote record.
+ configureEngine({
+ guid_mobile: {
+ clientName: "My Phone",
+ tabs: [{
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ }],
+ }
+ });
+
+ yield check_autocomplete({
+ search: "ex",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("ex", { heuristic: true }),
+ makeRemoteTabMatch("http://example.com/", "My Phone",
+ { title: "An Example",
+ icon: "moz-anno:favicon:http://favicon/"
+ }),
+ ],
+ });
+});
+
+add_task(function* test_noShowIcons() {
+ Services.prefs.setBoolPref("services.sync.syncedTabs.showRemoteIcons", false);
+ configureEngine({
+ guid_mobile: {
+ clientName: "My Phone",
+ tabs: [{
+ urlHistory: ["http://example.com/"],
+ title: "An Example",
+ icon: "http://favicon",
+ }],
+ }
+ });
+
+ yield check_autocomplete({
+ search: "ex",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("ex", { heuristic: true }),
+ makeRemoteTabMatch("http://example.com/", "My Phone",
+ { title: "An Example",
+ // expecting the default favicon due to that pref.
+ icon: "",
+ }),
+ ],
+ });
+ Services.prefs.clearUserPref("services.sync.syncedTabs.showRemoteIcons");
+});
+
+add_task(function* test_matches_title() {
+ // URL doesn't match search expression, should still match the title.
+ configureEngine({
+ guid_mobile: {
+ clientName: "My Phone",
+ tabs: [{
+ urlHistory: ["http://foo.com/"],
+ title: "An Example",
+ }],
+ }
+ });
+
+ yield check_autocomplete({
+ search: "ex",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("ex", { heuristic: true }),
+ makeRemoteTabMatch("http://foo.com/", "My Phone",
+ { title: "An Example" }),
+ ],
+ });
+});
+
+add_task(function* test_localtab_matches_override() {
+ // We have an open tab to the same page on a remote device, only "switch to
+ // tab" should appear as duplicate detection removed the remote one.
+
+ // First setup Sync to have the page as a remote tab.
+ configureEngine({
+ guid_mobile: {
+ clientName: "My Phone",
+ tabs: [{
+ urlHistory: ["http://foo.com/"],
+ title: "An Example",
+ }],
+ }
+ });
+
+ // Setup Places to think the tab is open locally.
+ let uri = NetUtil.newURI("http://foo.com/");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri, title: "An Example" },
+ ]);
+ addOpenPages(uri, 1);
+
+ yield check_autocomplete({
+ search: "ex",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("ex", { heuristic: true }),
+ makeSwitchToTabMatch("http://foo.com/", { title: "An Example" }),
+ ],
+ });
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
new file mode 100644
index 0000000000..f35242e210
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+add_task(function*() {
+ // Note that head_autocomplete.js has already added a MozSearch engine.
+ // Here we add another engine with a search alias.
+ Services.search.addEngineWithDetails("AliasedGETMozSearch", "", "get", "",
+ "GET", "http://s.example.com/search");
+ Services.search.addEngineWithDetails("AliasedPOSTMozSearch", "", "post", "",
+ "POST", "http://s.example.com/search");
+
+ for (let alias of ["get", "post"]) {
+ yield check_autocomplete({
+ search: alias,
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(alias, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ searchQuery: "", alias, heuristic: true }) ]
+ });
+
+ yield check_autocomplete({
+ search: `${alias} `,
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(`${alias} `, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ searchQuery: "", alias, heuristic: true }) ]
+ });
+
+ yield check_autocomplete({
+ search: `${alias} mozilla`,
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(`${alias} mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ searchQuery: "mozilla", alias, heuristic: true }) ]
+ });
+
+ yield check_autocomplete({
+ search: `${alias} MoZiLlA`,
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(`${alias} MoZiLlA`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ searchQuery: "MoZiLlA", alias, heuristic: true }) ]
+ });
+
+ yield check_autocomplete({
+ search: `${alias} mozzarella mozilla`,
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(`${alias} mozzarella mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+ searchQuery: "mozzarella mozilla", alias, heuristic: true }) ]
+ });
+ }
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js
new file mode 100644
index 0000000000..b41d9884bd
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_current.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+add_task(function*() {
+ // Note that head_autocomplete.js has already added a MozSearch engine.
+ // Here we add another engine with a search alias.
+ Services.search.addEngineWithDetails("AliasedMozSearch", "", "doit", "",
+ "GET", "http://s.example.com/search");
+
+ do_print("search engine");
+ yield check_autocomplete({
+ search: "mozilla",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("mozilla", { heuristic: true }) ]
+ });
+
+ do_print("search engine, uri-like input");
+ yield check_autocomplete({
+ search: "http:///",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("http:///", { heuristic: true }) ]
+ });
+
+ do_print("search engine, multiple words");
+ yield check_autocomplete({
+ search: "mozzarella cheese",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("mozzarella cheese", { heuristic: true }) ]
+ });
+
+ do_print("search engine, after current engine has changed");
+ Services.search.addEngineWithDetails("MozSearch2", "", "", "", "GET",
+ "http://s.example.com/search2");
+ engine = Services.search.getEngineByName("MozSearch2");
+ notEqual(Services.search.currentEngine, engine, "New engine shouldn't be the current engine yet");
+ Services.search.currentEngine = engine;
+ yield check_autocomplete({
+ search: "mozilla",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("mozilla", { engineName: "MozSearch2", heuristic: true }) ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js
new file mode 100644
index 0000000000..61b9826f7c
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_host.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* test_searchEngine_autoFill() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
+ Services.search.addEngineWithDetails("MySearchEngine", "", "", "",
+ "GET", "http://my.search.com/");
+ let engine = Services.search.getEngineByName("MySearchEngine");
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ // Add an uri that matches the search string with high frecency.
+ let uri = NetUtil.newURI("http://www.example.com/my/");
+ let visits = [];
+ for (let i = 0; i < 100; ++i) {
+ visits.push({ uri, title: "Terms - SearchEngine Search" });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+ yield addBookmark({ uri: uri, title: "Example bookmark" });
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ ok(frecencyForUrl(uri) > 10000, "Added URI should have expected high frecency");
+
+ do_print("Check search domain is autoFilled even if there's an higher frecency match");
+ yield check_autocomplete({
+ search: "my",
+ autofilled: "my.search.com",
+ completed: "http://my.search.com"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_searchEngine_noautoFill() {
+ let engineName = "engine-rel-searchform.xml";
+ let engine = yield addTestEngine(engineName);
+ equal(engine.searchForm, "http://example.com/?search");
+
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://example.com/my/"));
+
+ do_print("Check search domain is not autoFilled if it matches a visited domain");
+ yield check_autocomplete({
+ search: "example",
+ autofilled: "example.com/",
+ completed: "example.com/"
+ });
+
+ yield cleanup();
+});
+
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.js b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.js
new file mode 100644
index 0000000000..2a5f2d78e1
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_restyle.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/. */
+
+add_task(function* test_searchEngine() {
+ Services.search.addEngineWithDetails("SearchEngine", "", "", "",
+ "GET", "http://s.example.com/search");
+ let engine = Services.search.getEngineByName("SearchEngine");
+ engine.addParam("q", "{searchTerms}", null);
+ do_register_cleanup(() => Services.search.removeEngine(engine));
+
+ let uri1 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=1");
+ let uri2 = NetUtil.newURI("http://s.example.com/search?q=Terms&client=2");
+ yield PlacesTestUtils.addVisits({ uri: uri1, title: "Terms - SearchEngine Search" });
+ yield addBookmark({ uri: uri2, title: "Terms - SearchEngine Search" });
+
+ do_print("Past search terms should be styled, unless bookmarked");
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
+ yield check_autocomplete({
+ search: "term",
+ matches: [
+ makeSearchMatch("Terms", {
+ engineName: "SearchEngine",
+ style: ["favicon"]
+ }),
+ {
+ uri: uri2,
+ title: "Terms - SearchEngine Search",
+ style: ["bookmark"]
+ }
+ ]
+ });
+
+ do_print("Past search terms should not be styled if restyling is disabled");
+ Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false);
+ yield check_autocomplete({
+ search: "term",
+ matches: [ { uri: uri1, title: "Terms - SearchEngine Search" },
+ { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js b/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
new file mode 100644
index 0000000000..63b428cd4c
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
@@ -0,0 +1,651 @@
+Cu.import("resource://gre/modules/FormHistory.jsm");
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const SERVER_PORT = 9000;
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
+const SUGGEST_RESTRICT_TOKEN = "$";
+
+var suggestionsFn;
+var previousSuggestionsFn;
+
+function setSuggestionsFn(fn) {
+ previousSuggestionsFn = suggestionsFn;
+ suggestionsFn = fn;
+}
+
+function* cleanUpSuggestions() {
+ yield cleanup();
+ if (previousSuggestionsFn) {
+ suggestionsFn = previousSuggestionsFn;
+ previousSuggestionsFn = null;
+ }
+}
+
+add_task(function* setUp() {
+ // Set up a server that provides some suggestions by appending strings onto
+ // the search query.
+ let server = makeTestServer(SERVER_PORT);
+ server.registerPathHandler("/suggest", (req, resp) => {
+ // URL query params are x-www-form-urlencoded, which converts spaces into
+ // plus signs, so un-convert any plus signs back to spaces.
+ let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
+ let suggestions = suggestionsFn(searchStr);
+ let data = [searchStr, suggestions];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+ });
+ setSuggestionsFn(searchStr => {
+ let suffixes = ["foo", "bar"];
+ return suffixes.map(s => searchStr + " " + s);
+ });
+
+ // Install the test engine.
+ let oldCurrentEngine = Services.search.currentEngine;
+ do_register_cleanup(() => Services.search.currentEngine = oldCurrentEngine);
+ let engine = yield addTestEngine(ENGINE_NAME, server);
+ Services.search.currentEngine = engine;
+});
+
+add_task(function* disabled_urlbarSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, false);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ yield cleanUpSuggestions();
+});
+
+add_task(function* disabled_allSuggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ yield cleanUpSuggestions();
+});
+
+add_task(function* disabled_privateWindow() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "private-window enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+ yield cleanUpSuggestions();
+});
+
+add_task(function* singleWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ { uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello foo",
+ searchQuery: "hello",
+ searchSuggestion: "hello foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }, {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello bar",
+ searchQuery: "hello",
+ searchSuggestion: "hello bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* multiWordQuery() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ yield check_autocomplete({
+ search: "hello world",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello world", { engineName: ENGINE_NAME, heuristic: true }),
+ { uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello world foo",
+ searchQuery: "hello world",
+ searchSuggestion: "hello world foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }, {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello world bar",
+ searchQuery: "hello world",
+ searchSuggestion: "hello world bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* suffixMatch() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ let prefixes = ["baz", "quux"];
+ return prefixes.map(p => p + " " + searchStr);
+ });
+
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ { uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "baz hello",
+ searchQuery: "hello",
+ searchSuggestion: "baz hello",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }, {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "quux hello",
+ searchQuery: "hello",
+ searchSuggestion: "quux hello",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* queryIsNotASubstring() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ return ["aaa", "bbb"];
+ });
+
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ { uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "aaa",
+ searchQuery: "hello",
+ searchSuggestion: "aaa",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }, {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "bbb",
+ searchQuery: "hello",
+ searchSuggestion: "bbb",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* restrictToken() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+ Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ yield PlacesTestUtils.addVisits([
+ {
+ uri: NetUtil.newURI("http://example.com/hello-visit"),
+ title: "hello visit",
+ },
+ {
+ uri: NetUtil.newURI("http://example.com/hello-bookmark"),
+ title: "hello bookmark",
+ },
+ ]);
+
+ yield addBookmark({
+ uri: NetUtil.newURI("http://example.com/hello-bookmark"),
+ title: "hello bookmark",
+ });
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ yield check_autocomplete({
+ search: "hello",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("hello", { engineName: ENGINE_NAME, heuristic: true }),
+ {
+ uri: NetUtil.newURI("http://example.com/hello-visit"),
+ title: "hello visit",
+ },
+ {
+ uri: NetUtil.newURI("http://example.com/hello-bookmark"),
+ title: "hello bookmark",
+ style: ["bookmark"],
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello foo",
+ searchQuery: "hello",
+ searchSuggestion: "hello foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello bar",
+ searchQuery: "hello",
+ searchSuggestion: "hello bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ ],
+ });
+
+ // Now do a restricted search to make sure only suggestions appear.
+ yield check_autocomplete({
+ search: SUGGEST_RESTRICT_TOKEN + " hello",
+ searchParam: "enable-actions",
+ matches: [
+ // TODO (bug 1177895) This is wrong.
+ makeSearchMatch(SUGGEST_RESTRICT_TOKEN + " hello", { engineName: ENGINE_NAME, heuristic: true }),
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello foo",
+ searchQuery: "hello",
+ searchSuggestion: "hello foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "hello bar",
+ searchQuery: "hello",
+ searchSuggestion: "hello bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ }
+ ],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* mixup_frecency() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ // Add a visit and a bookmark. Actually, make the bookmark visited too so
+ // that it's guaranteed, with its higher frecency, to appear above the search
+ // suggestions.
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://example.com/lo0"),
+ title: "low frecency 0" },
+ { uri: NetUtil.newURI("http://example.com/lo1"),
+ title: "low frecency 1" },
+ { uri: NetUtil.newURI("http://example.com/lo2"),
+ title: "low frecency 2" },
+ { uri: NetUtil.newURI("http://example.com/lo3"),
+ title: "low frecency 3" },
+ { uri: NetUtil.newURI("http://example.com/lo4"),
+ title: "low frecency 4" },
+ ]);
+
+ for (let i = 0; i < 4; i++) {
+ let href = `http://example.com/lo${i}`;
+ let frecency = frecencyForUrl(href);
+ Assert.ok(frecency < FRECENCY_DEFAULT,
+ `frecency for ${href}: ${frecency}, should be lower than ${FRECENCY_DEFAULT}`);
+ }
+
+ for (let i = 0; i < 5; i++) {
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://example.com/hi0"),
+ title: "high frecency 0",
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://example.com/hi1"),
+ title: "high frecency 1",
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://example.com/hi2"),
+ title: "high frecency 2",
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://example.com/hi3"),
+ title: "high frecency 3",
+ transition: TRANSITION_TYPED },
+ ]);
+ }
+
+ for (let i = 0; i < 4; i++) {
+ let href = `http://example.com/hi${i}`;
+ yield addBookmark({ uri: href, title: `high frecency ${i}` });
+ let frecency = frecencyForUrl(href);
+ Assert.ok(frecency > FRECENCY_DEFAULT,
+ `frecency for ${href}: ${frecency}, should be higher than ${FRECENCY_DEFAULT}`);
+ }
+
+ // Do an unrestricted search to make sure everything appears in it, including
+ // the visit and bookmark.
+ yield check_autocomplete({
+ checkSorting: true,
+ search: "frecency",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("frecency", { engineName: ENGINE_NAME, heuristic: true }),
+ { uri: NetUtil.newURI("http://example.com/hi3"),
+ title: "high frecency 3",
+ style: [ "bookmark" ] },
+ { uri: NetUtil.newURI("http://example.com/hi2"),
+ title: "high frecency 2",
+ style: [ "bookmark" ] },
+ { uri: NetUtil.newURI("http://example.com/hi1"),
+ title: "high frecency 1",
+ style: [ "bookmark" ] },
+ { uri: NetUtil.newURI("http://example.com/hi0"),
+ title: "high frecency 0",
+ style: [ "bookmark" ] },
+ { uri: NetUtil.newURI("http://example.com/lo4"),
+ title: "low frecency 4" },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "frecency foo",
+ searchQuery: "frecency",
+ searchSuggestion: "frecency foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "frecency bar",
+ searchQuery: "frecency",
+ searchSuggestion: "frecency bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ { uri: NetUtil.newURI("http://example.com/lo3"),
+ title: "low frecency 3" },
+ { uri: NetUtil.newURI("http://example.com/lo2"),
+ title: "low frecency 2" },
+ { uri: NetUtil.newURI("http://example.com/lo1"),
+ title: "low frecency 1" },
+ { uri: NetUtil.newURI("http://example.com/lo0"),
+ title: "low frecency 0" },
+ ],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* prohibit_suggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ yield check_autocomplete({
+ search: "localhost",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("localhost", { engineName: ENGINE_NAME, heuristic: true }),
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "localhost foo",
+ searchQuery: "localhost",
+ searchSuggestion: "localhost foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "localhost bar",
+ searchQuery: "localhost",
+ searchSuggestion: "localhost bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ ],
+ });
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.localhost", true);
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost");
+ });
+ yield check_autocomplete({
+ search: "localhost",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("localhost", "http://localhost/", { heuristic: true }),
+ makeSearchMatch("localhost", { engineName: ENGINE_NAME, heuristic: false })
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ yield check_autocomplete({
+ search: "localhost other",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("localhost other", { engineName: ENGINE_NAME, heuristic: true }),
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "localhost other foo",
+ searchQuery: "localhost other",
+ searchSuggestion: "localhost other foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "localhost other bar",
+ searchQuery: "localhost other",
+ searchSuggestion: "localhost other bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ ],
+ });
+
+ // Clear the whitelist for localhost, and try preferring DNS for any single
+ // word instead:
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost");
+ Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true);
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+ });
+
+ yield check_autocomplete({
+ search: "localhost",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("localhost", "http://localhost/", { heuristic: true }),
+ makeSearchMatch("localhost", { engineName: ENGINE_NAME, heuristic: false })
+ ],
+ });
+
+ yield check_autocomplete({
+ search: "somethingelse",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("somethingelse", "http://somethingelse/", { heuristic: true }),
+ makeSearchMatch("somethingelse", { engineName: ENGINE_NAME, heuristic: false })
+ ],
+ });
+
+ // When using multiple words, we should still get suggestions:
+ yield check_autocomplete({
+ search: "localhost other",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("localhost other", { engineName: ENGINE_NAME, heuristic: true }),
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "localhost other foo",
+ searchQuery: "localhost other",
+ searchSuggestion: "localhost other foo",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "localhost other bar",
+ searchQuery: "localhost other",
+ searchSuggestion: "localhost other bar",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ ],
+ });
+
+ Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
+
+ yield check_autocomplete({
+ search: "1.2.3.4",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("1.2.3.4", "http://1.2.3.4/", { heuristic: true }),
+ ],
+ });
+ yield check_autocomplete({
+ search: "[2001::1]:30",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("[2001::1]:30", "http://[2001::1]:30/", { heuristic: true }),
+ ],
+ });
+ yield check_autocomplete({
+ search: "user:pass@test",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("user:pass@test", "http://user:pass@test/", { heuristic: true }),
+ ],
+ });
+ yield check_autocomplete({
+ search: "test/test",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("test/test", "http://test/test", { heuristic: true }),
+ ],
+ });
+ yield check_autocomplete({
+ search: "data:text/plain,Content",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("data:text/plain,Content", "data:text/plain,Content", { heuristic: true }),
+ ],
+ });
+
+ yield check_autocomplete({
+ search: "a",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("a", { engineName: ENGINE_NAME, heuristic: true }),
+ ],
+ });
+
+ yield cleanUpSuggestions();
+});
+
+add_task(function* avoid_url_suggestions() {
+ Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+ setSuggestionsFn(searchStr => {
+ let suffixes = [".com", "/test", ":1]", "@test", ". com"];
+ return suffixes.map(s => searchStr + s);
+ });
+
+ yield check_autocomplete({
+ search: "test",
+ searchParam: "enable-actions",
+ matches: [
+ makeSearchMatch("test", { engineName: ENGINE_NAME, heuristic: true }),
+ {
+ uri: makeActionURI(("searchengine"), {
+ engineName: ENGINE_NAME,
+ input: "test. com",
+ searchQuery: "test",
+ searchSuggestion: "test. com",
+ }),
+ title: ENGINE_NAME,
+ style: ["action", "searchengine"],
+ icon: "",
+ },
+ ],
+ });
+
+ yield cleanUpSuggestions();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_special_search.js b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js
new file mode 100644
index 0000000000..21df7046cc
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js
@@ -0,0 +1,447 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ */
+
+function setSuggestPrefsToFalse() {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+}
+
+add_task(function* test_special_searches() {
+ let uri1 = NetUtil.newURI("http://url/");
+ let uri2 = NetUtil.newURI("http://url/2");
+ let uri3 = NetUtil.newURI("http://foo.bar/");
+ let uri4 = NetUtil.newURI("http://foo.bar/2");
+ let uri5 = NetUtil.newURI("http://url/star");
+ let uri6 = NetUtil.newURI("http://url/star/2");
+ let uri7 = NetUtil.newURI("http://foo.bar/star");
+ let uri8 = NetUtil.newURI("http://foo.bar/star/2");
+ let uri9 = NetUtil.newURI("http://url/tag");
+ let uri10 = NetUtil.newURI("http://url/tag/2");
+ let uri11 = NetUtil.newURI("http://foo.bar/tag");
+ let uri12 = NetUtil.newURI("http://foo.bar/tag/2");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title", transition: TRANSITION_TYPED },
+ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar", transition: TRANSITION_TYPED },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", transition: TRANSITION_TYPED }
+ ]);
+ yield addBookmark( { uri: uri5, title: "title" } );
+ yield addBookmark( { uri: uri6, title: "foo.bar" } );
+ yield addBookmark( { uri: uri7, title: "title" } );
+ yield addBookmark( { uri: uri8, title: "foo.bar" } );
+ yield addBookmark( { uri: uri9, title: "title", tags: [ "foo.bar" ] } );
+ yield addBookmark( { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ] } );
+ yield addBookmark( { uri: uri11, title: "title", tags: [ "foo.bar" ] } );
+ yield addBookmark( { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ] } );
+
+ // Test restricting searches
+ do_print("History restrict");
+ yield check_autocomplete({
+ search: "^",
+ matches: [ { uri: uri1, title: "title" },
+ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("Star restrict");
+ yield check_autocomplete({
+ search: "*",
+ matches: [ { uri: uri5, title: "title", style: [ "bookmark" ] },
+ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Tag restrict");
+ yield check_autocomplete({
+ search: "+",
+ matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ // Test specials as any word position
+ do_print("Special as first word");
+ yield check_autocomplete({
+ search: "^ foo bar",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("Special as middle word");
+ yield check_autocomplete({
+ search: "foo ^ bar",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("Special as last word");
+ yield check_autocomplete({
+ search: "foo bar ^",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ // Test restricting and matching searches with a term
+ do_print("foo ^ -> history");
+ yield check_autocomplete({
+ search: "foo ^",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo | -> history (change pref)");
+ changeRestrict("history", "|");
+ yield check_autocomplete({
+ search: "foo |",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo * -> is star");
+ resetRestrict("history");
+ yield check_autocomplete({
+ search: "foo *",
+ matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo | -> is star (change pref)");
+ changeRestrict("bookmark", "|");
+ yield check_autocomplete({
+ search: "foo |",
+ matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo # -> in title");
+ resetRestrict("bookmark");
+ yield check_autocomplete({
+ search: "foo #",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo | -> in title (change pref)");
+ changeRestrict("title", "|");
+ yield check_autocomplete({
+ search: "foo |",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo @ -> in url");
+ resetRestrict("title");
+ yield check_autocomplete({
+ search: "foo @",
+ matches: [ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo | -> in url (change pref)");
+ changeRestrict("url", "|");
+ yield check_autocomplete({
+ search: "foo |",
+ matches: [ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo + -> is tag");
+ resetRestrict("url");
+ yield check_autocomplete({
+ search: "foo +",
+ matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo | -> is tag (change pref)");
+ changeRestrict("tag", "|");
+ yield check_autocomplete({
+ search: "foo |",
+ matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo ~ -> is typed");
+ resetRestrict("tag");
+ yield check_autocomplete({
+ search: "foo ~",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo | -> is typed (change pref)");
+ changeRestrict("typed", "|");
+ yield check_autocomplete({
+ search: "foo |",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ // Test various pairs of special searches
+ do_print("foo ^ * -> history, is star");
+ resetRestrict("typed");
+ yield check_autocomplete({
+ search: "foo ^ *",
+ matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo ^ # -> history, in title");
+ yield check_autocomplete({
+ search: "foo ^ #",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo ^ @ -> history, in url");
+ yield check_autocomplete({
+ search: "foo ^ @",
+ matches: [ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo ^ + -> history, is tag");
+ yield check_autocomplete({
+ search: "foo ^ +",
+ matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo ^ ~ -> history, is typed");
+ yield check_autocomplete({
+ search: "foo ^ ~",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo * # -> is star, in title");
+ yield check_autocomplete({
+ search: "foo * #",
+ matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo * @ -> is star, in url");
+ yield check_autocomplete({
+ search: "foo * @",
+ matches: [ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo * + -> same as +");
+ yield check_autocomplete({
+ search: "foo * +",
+ matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo * ~ -> is star, is typed");
+ yield check_autocomplete({
+ search: "foo * ~",
+ matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo # @ -> in title, in url");
+ yield check_autocomplete({
+ search: "foo # @",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo # + -> in title, is tag");
+ yield check_autocomplete({
+ search: "foo # +",
+ matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo # ~ -> in title, is typed");
+ yield check_autocomplete({
+ search: "foo # ~",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo @ + -> in url, is tag");
+ yield check_autocomplete({
+ search: "foo @ +",
+ matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo @ ~ -> in url, is typed");
+ yield check_autocomplete({
+ search: "foo @ ~",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ do_print("foo + ~ -> is tag, is typed");
+ yield check_autocomplete({
+ search: "foo + ~",
+ matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ]
+ });
+
+ // Disable autoFill for the next tests, see test_autoFill_default_behavior.js
+ // for specific tests.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+
+ // Test default usage by setting certain browser.urlbar.suggest.* prefs
+ do_print("foo -> default history");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ yield check_autocomplete({
+ search: "foo",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: ["foo.bar"], style: [ "tag" ] } ]
+ });
+
+ do_print("foo -> default history, is star");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ yield check_autocomplete({
+ search: "foo",
+ matches: [ { uri: uri2, title: "foo.bar" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "foo.bar" },
+ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar"], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo -> default history, is star, is typed");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ yield check_autocomplete({
+ search: "foo",
+ matches: [ { uri: uri4, title: "foo.bar" },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo -> is star");
+ setSuggestPrefsToFalse();
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ yield check_autocomplete({
+ search: "foo",
+ matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("foo -> is star, is typed");
+ setSuggestPrefsToFalse();
+ // only typed should be ignored
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ yield check_autocomplete({
+ search: "foo",
+ matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri7, title: "title", style: [ "bookmark" ] },
+ { uri: uri8, title: "foo.bar", style: [ "bookmark" ] },
+ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] },
+ { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js b/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js
new file mode 100644
index 0000000000..89ccc3206e
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.
+ */
+
+add_task(function* test_swap_protocol() {
+ let uri1 = NetUtil.newURI("http://www.site/");
+ let uri2 = NetUtil.newURI("http://site/");
+ let uri3 = NetUtil.newURI("ftp://ftp.site/");
+ let uri4 = NetUtil.newURI("ftp://site/");
+ let uri5 = NetUtil.newURI("https://www.site/");
+ let uri6 = NetUtil.newURI("https://site/");
+ let uri7 = NetUtil.newURI("http://woohoo/");
+ let uri8 = NetUtil.newURI("http://wwwwwwacko/");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "title" },
+ { uri: uri5, title: "title" },
+ { uri: uri6, title: "title" },
+ { uri: uri7, title: "title" },
+ { uri: uri8, title: "title" }
+ ]);
+
+ let allMatches = [
+ { uri: uri1, title: "title" },
+ { uri: uri2, title: "title" },
+ { uri: uri3, title: "title" },
+ { uri: uri4, title: "title" },
+ { uri: uri5, title: "title" },
+ { uri: uri6, title: "title" }
+ ];
+
+ // Disable autoFill to avoid handling the first result.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", "false");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+
+ do_print("http://www.site matches all site");
+ yield check_autocomplete({
+ search: "http://www.site",
+ matches: allMatches
+ });
+
+ do_print("http://site matches all site");
+ yield check_autocomplete({
+ search: "http://site",
+ matches: allMatches
+ });
+
+ do_print("ftp://ftp.site matches itself");
+ yield check_autocomplete({
+ search: "ftp://ftp.site",
+ matches: [ { uri: uri3, title: "title" } ]
+ });
+
+ do_print("ftp://site matches all site");
+ yield check_autocomplete({
+ search: "ftp://site",
+ matches: allMatches
+ });
+
+ do_print("https://www.site matches all site");
+ yield check_autocomplete({
+ search: "https://www.site",
+ matches: allMatches
+ });
+
+ do_print("https://site matches all site");
+ yield check_autocomplete({
+ search: "https://site",
+ matches: allMatches
+ });
+
+ do_print("www.site matches all site");
+ yield check_autocomplete({
+ search: "www.site",
+ matches: allMatches
+ });
+
+ do_print("w matches none of www.");
+ yield check_autocomplete({
+ search: "w",
+ matches: [ { uri: uri7, title: "title" },
+ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("http://w matches none of www.");
+ yield check_autocomplete({
+ search: "http://w",
+ matches: [ { uri: uri7, title: "title" },
+ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("http://w matches none of www.");
+ yield check_autocomplete({
+ search: "http://www.w",
+ matches: [ { uri: uri7, title: "title" },
+ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("ww matches none of www.");
+ yield check_autocomplete({
+ search: "ww",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("ww matches none of www.");
+ yield check_autocomplete({
+ search: "ww",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("http://ww matches none of www.");
+ yield check_autocomplete({
+ search: "http://ww",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("http://www.ww matches none of www.");
+ yield check_autocomplete({
+ search: "http://www.ww",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("www matches none of www.");
+ yield check_autocomplete({
+ search: "www",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("http://www matches none of www.");
+ yield check_autocomplete({
+ search: "http://www",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ do_print("http://www.www matches none of www.");
+ yield check_autocomplete({
+ search: "http://www.www",
+ matches: [ { uri: uri8, title: "title" } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js b/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js
new file mode 100644
index 0000000000..740b8d8edd
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_tab_matches.js
@@ -0,0 +1,164 @@
+/* -*- 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 = "%";
+
+add_task(function* test_tab_matches() {
+ let uri1 = NetUtil.newURI("http://abc.com/");
+ let uri2 = NetUtil.newURI("http://xyz.net/");
+ let uri3 = NetUtil.newURI("about:mozilla");
+ let uri4 = NetUtil.newURI("data:text/html,test");
+ let uri5 = NetUtil.newURI("http://foobar.org");
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "ABC rocks" },
+ { uri: uri2, title: "xyz.net - we're better than ABC" },
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ" }
+ ]);
+ addOpenPages(uri1, 1);
+ // Pages that cannot be registered in history.
+ addOpenPages(uri3, 1);
+ addOpenPages(uri4, 1);
+
+ do_print("two results, normal result is a tab match");
+ yield check_autocomplete({
+ search: "abc.com",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("abc.com", "http://abc.com/", { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }),
+ makeSearchMatch("abc.com", { heuristic: false }) ]
+ });
+
+ do_print("three results, one tab match");
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }),
+ { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] },
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("three results, both normal results are tab matches");
+ addOpenPages(uri2, 1);
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }),
+ makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC" }),
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("a container tab is not visible in 'switch to tab'");
+ addOpenPages(uri5, 1, /* userContextId: */ 3);
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }),
+ makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC" }),
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("a container tab should not see 'switch to tab' for other container tabs");
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions user-context-id:3",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ makeSwitchToTabMatch("http://foobar.org/", { title: "foobar.org - much better than ABC, definitely better than XYZ" }),
+ { uri: uri1, title: "ABC rocks", style: [ "favicon" ] },
+ { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] } ]
+ });
+
+ do_print("a different container tab should not see any 'switch to tab'");
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions user-context-id:2",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ { uri: uri1, title: "ABC rocks", style: [ "favicon" ] },
+ { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] },
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("three results, both normal results are tab matches, one has multiple tabs");
+ addOpenPages(uri2, 5);
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }),
+ makeSwitchToTabMatch("http://xyz.net/", { title: "xyz.net - we're better than ABC" }),
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("three results, no tab matches (disable-private-actions)");
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions disable-private-actions",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ { uri: uri1, title: "ABC rocks", style: [ "favicon" ] },
+ { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] },
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("two results (actions disabled)");
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "",
+ matches: [ { uri: uri1, title: "ABC rocks", style: [ "favicon" ] },
+ { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] },
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("three results, no tab matches");
+ removeOpenPages(uri1, 1);
+ removeOpenPages(uri2, 6);
+ yield check_autocomplete({
+ search: "abc",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("abc", { heuristic: true }),
+ { uri: uri1, title: "ABC rocks", style: [ "favicon" ] },
+ { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] },
+ { uri: uri5, title: "foobar.org - much better than ABC, definitely better than XYZ", style: [ "favicon" ] } ]
+ });
+
+ do_print("tab match search with restriction character");
+ addOpenPages(uri1, 1);
+ yield check_autocomplete({
+ search: gTabRestrictChar + " abc",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(gTabRestrictChar + " abc", { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }) ]
+ });
+
+ do_print("tab match with not-addable pages");
+ yield check_autocomplete({
+ search: "mozilla",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("mozilla", { heuristic: true }),
+ makeSwitchToTabMatch("about:mozilla") ]
+ });
+
+ do_print("tab match with not-addable pages and restriction character");
+ yield check_autocomplete({
+ search: gTabRestrictChar + " mozilla",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(gTabRestrictChar + " mozilla", { heuristic: true }),
+ makeSwitchToTabMatch("about:mozilla") ]
+ });
+
+ do_print("tab match with not-addable pages and only restriction character");
+ yield check_autocomplete({
+ search: gTabRestrictChar,
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch(gTabRestrictChar, { heuristic: true }),
+ makeSwitchToTabMatch("http://abc.com/", { title: "ABC rocks" }),
+ makeSwitchToTabMatch("about:mozilla"),
+ makeSwitchToTabMatch("data:text/html,test") ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_trimming.js b/toolkit/components/places/tests/unifiedcomplete/test_trimming.js
new file mode 100644
index 0000000000..e55b009ff9
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_trimming.js
@@ -0,0 +1,313 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(function* test_untrimmed_secure_www() {
+ do_print("Searching for untrimmed https://www entry");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("https://www.mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "https://www.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_secure_www_path() {
+ do_print("Searching for untrimmed https://www entry with path");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("https://www.mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/t",
+ autofilled: "mozilla.org/test/",
+ completed: "https://www.mozilla.org/test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_secure() {
+ do_print("Searching for untrimmed https:// entry");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("https://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "https://mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_secure_path() {
+ do_print("Searching for untrimmed https:// entry with path");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("https://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/t",
+ autofilled: "mozilla.org/test/",
+ completed: "https://mozilla.org/test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_www() {
+ do_print("Searching for untrimmed http://www entry");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "www.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_www_path() {
+ do_print("Searching for untrimmed http://www entry with path");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/t",
+ autofilled: "mozilla.org/test/",
+ completed: "http://www.mozilla.org/test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_ftp() {
+ do_print("Searching for untrimmed ftp:// entry");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "ftp://mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untrimmed_ftp_path() {
+ do_print("Searching for untrimmed ftp:// entry with path");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/t",
+ autofilled: "mozilla.org/test/",
+ completed: "ftp://mozilla.org/test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_priority_1() {
+ do_print("Ensuring correct priority 1");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("https://www.mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_periority_2() {
+ do_print( "Ensuring correct priority 2");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_periority_3() {
+ do_print("Ensuring correct priority 3");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_periority_4() {
+ do_print("Ensuring correct priority 4");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_priority_5() {
+ do_print("Ensuring correct priority 5");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("ftp://www.mozilla.org/test/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "ftp://mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_priority_6() {
+ do_print("Ensuring correct priority 6");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://www.mozilla.org/test1/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://www.mozilla.org/test2/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.org/",
+ completed: "www.mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_longer_domain() {
+ do_print("Ensuring longer domain can't match");
+ // The .co should be preferred, but should not get the https from the .com.
+ // The .co domain must be added later to activate the trigger bug.
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("https://mozilla.com/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://mozilla.co/"), transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://mozilla.co/"), transition: TRANSITION_TYPED }
+ ]);
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "mozilla.co/",
+ completed: "mozilla.co/"
+ });
+
+ yield cleanup();
+});
+
+add_task(function* test_escaped_chars() {
+ do_print("Searching for URL with characters that are normally escaped");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("https://www.mozilla.org/啊-test"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "https://www.mozilla.org/啊-test",
+ autofilled: "https://www.mozilla.org/啊-test",
+ completed: "https://www.mozilla.org/啊-test"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_unsecure_secure() {
+ do_print("Don't return unsecure URL when searching for secure ones");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://test.moz.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "https://test.moz.org/t",
+ autofilled: "https://test.moz.org/test/",
+ completed: "https://test.moz.org/test/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_unsecure_secure_domain() {
+ do_print("Don't return unsecure domain when searching for secure ones");
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://test.moz.org/test/"),
+ transition: TRANSITION_TYPED
+ });
+ yield check_autocomplete({
+ search: "https://test.moz",
+ autofilled: "https://test.moz.org/",
+ completed: "https://test.moz.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untyped_www() {
+ do_print("Untyped is not accounted for www");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.moz.org/test/") });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "moz.org/",
+ completed: "moz.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untyped_ftp() {
+ do_print("Untyped is not accounted for ftp");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("ftp://moz.org/test/") });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "moz.org/",
+ completed: "moz.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untyped_secure() {
+ do_print("Untyped is not accounted for https");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://moz.org/test/") });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "moz.org/",
+ completed: "moz.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untyped_secure_www() {
+ do_print("Untyped is not accounted for https://www");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("https://www.moz.org/test/") });
+ yield check_autocomplete({
+ search: "mo",
+ autofilled: "moz.org/",
+ completed: "moz.org/"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_typed.js b/toolkit/components/places/tests/unifiedcomplete/test_typed.js
new file mode 100644
index 0000000000..72f76159c1
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_typed.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// First do searches with typed behavior forced to false, so later tests will
+// ensure autocomplete is able to dinamically switch behavior.
+
+const FAVICON_HREF = NetUtil.newURI(do_get_file("../favicons/favicon-normal16.png")).spec;
+
+add_task(function* test_domain() {
+ do_print("Searching for domain should autoFill it");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+ yield setFaviconForHref("http://mozilla.org/link/", FAVICON_HREF);
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/",
+ icon: "moz-anno:favicon:" + FAVICON_HREF
+ });
+ yield cleanup();
+});
+
+add_task(function* test_url() {
+ do_print("Searching for url should autoFill it");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+ yield setFaviconForHref("http://mozilla.org/link/", FAVICON_HREF);
+ yield check_autocomplete({
+ search: "mozilla.org/li",
+ autofilled: "mozilla.org/link/",
+ completed: "http://mozilla.org/link/",
+ icon: "moz-anno:favicon:" + FAVICON_HREF
+ });
+ yield cleanup();
+});
+
+// Now do searches with typed behavior forced to true.
+
+add_task(function* test_untyped_domain() {
+ do_print("Searching for non-typed domain should not autoFill it");
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "moz",
+ completed: "moz"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_typed_domain() {
+ do_print("Searching for typed domain should autoFill it");
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/typed/"),
+ transition: TRANSITION_TYPED });
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "mozilla.org/",
+ completed: "mozilla.org/"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_untyped_url() {
+ do_print("Searching for non-typed url should not autoFill it");
+ yield PlacesTestUtils.addVisits(NetUtil.newURI("http://mozilla.org/link/"));
+ yield check_autocomplete({
+ search: "mozilla.org/li",
+ autofilled: "mozilla.org/li",
+ completed: "mozilla.org/li"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_typed_url() {
+ do_print("Searching for typed url should autoFill it");
+ yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
+ transition: TRANSITION_TYPED });
+ yield check_autocomplete({
+ search: "mozilla.org/li",
+ autofilled: "mozilla.org/link/",
+ completed: "http://mozilla.org/link/"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js b/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js
new file mode 100644
index 0000000000..eaccb23e5d
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_visit_url.js
@@ -0,0 +1,186 @@
+add_task(function*() {
+ do_print("visit url, no protocol");
+ yield check_autocomplete({
+ search: "mozilla.org",
+ searchParam: "enable-actions",
+ matches: [
+ { uri: makeActionURI("visiturl", {url: "http://mozilla.org/", input: "mozilla.org"}), title: "http://mozilla.org/", style: [ "action", "visiturl", "heuristic" ] },
+ { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "mozilla.org", searchQuery: "mozilla.org"}), title: "MozSearch", style: ["action", "searchengine"] }
+ ]
+ });
+
+ do_print("visit url, no protocol but with 2 dots");
+ yield check_autocomplete({
+ search: "www.mozilla.org",
+ searchParam: "enable-actions",
+ matches: [
+ { uri: makeActionURI("visiturl", {url: "http://www.mozilla.org/", input: "www.mozilla.org"}), title: "http://www.mozilla.org/", style: [ "action", "visiturl", "heuristic" ] },
+ { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "www.mozilla.org", searchQuery: "www.mozilla.org"}), title: "MozSearch", style: ["action", "searchengine"] }
+ ]
+ });
+
+ do_print("visit url, no protocol but with 3 dots");
+ yield check_autocomplete({
+ search: "www.mozilla.org.tw",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("visiturl", {url: "http://www.mozilla.org.tw/", input: "www.mozilla.org.tw"}), title: "http://www.mozilla.org.tw/", style: [ "action", "visiturl", "heuristic" ] } ]
+ });
+
+ do_print("visit url, with protocol but with 2 dots");
+ yield check_autocomplete({
+ search: "https://www.mozilla.org",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("visiturl", {url: "https://www.mozilla.org/", input: "https://www.mozilla.org"}), title: "https://www.mozilla.org/", style: [ "action", "visiturl", "heuristic" ] } ]
+ });
+
+ do_print("visit url, with protocol but with 3 dots");
+ yield check_autocomplete({
+ search: "https://www.mozilla.org.tw",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("visiturl", {url: "https://www.mozilla.org.tw/", input: "https://www.mozilla.org.tw"}), title: "https://www.mozilla.org.tw/", style: [ "action", "visiturl", "heuristic" ] } ]
+ });
+
+ do_print("visit url, with protocol");
+ yield check_autocomplete({
+ search: "https://mozilla.org",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("visiturl", {url: "https://mozilla.org/", input: "https://mozilla.org"}), title: "https://mozilla.org/", style: [ "action", "visiturl", "heuristic" ] } ]
+ });
+
+ do_print("visit url, about: protocol (no host)");
+ yield check_autocomplete({
+ search: "about:config",
+ searchParam: "enable-actions",
+ matches: [ { uri: makeActionURI("visiturl", {url: "about:config", input: "about:config"}), title: "about:config", style: [ "action", "visiturl", "heuristic" ] } ]
+ });
+
+ // This is distinct because of how we predict being able to url autofill via
+ // host lookups.
+ do_print("visit url, host matching visited host but not visited url");
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://mozilla.org/wine/"), title: "Mozilla Wine", transition: TRANSITION_TYPED },
+ ]);
+ yield check_autocomplete({
+ search: "mozilla.org/rum",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("mozilla.org/rum", "http://mozilla.org/rum", { heuristic: true }) ]
+ });
+
+ // And hosts with no dot in them are special, due to requiring whitelisting.
+ do_print("non-whitelisted host");
+ yield check_autocomplete({
+ search: "firefox",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("firefox", { heuristic: true }) ]
+ });
+
+ do_print("url with non-whitelisted host");
+ yield check_autocomplete({
+ search: "firefox/get",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("firefox/get", "http://firefox/get", { heuristic: true }) ]
+ });
+
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true);
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox");
+ });
+
+ do_print("whitelisted host");
+ yield check_autocomplete({
+ search: "firefox",
+ searchParam: "enable-actions",
+ matches: [
+ makeVisitMatch("firefox", "http://firefox/", { heuristic: true }),
+ makeSearchMatch("firefox", { heuristic: false })
+ ]
+ });
+
+ do_print("url with whitelisted host");
+ yield check_autocomplete({
+ search: "firefox/get",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("firefox/get", "http://firefox/get", { heuristic: true }) ]
+ });
+
+ do_print("visit url, host matching visited host but not visited url, whitelisted host");
+ Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true);
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla");
+ });
+ yield check_autocomplete({
+ search: "mozilla/rum",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("mozilla/rum", "http://mozilla/rum", { heuristic: true }) ]
+ });
+
+ // ipv4 and ipv6 literal addresses should offer to visit.
+ do_print("visit url, ipv4 literal");
+ yield check_autocomplete({
+ search: "127.0.0.1",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("127.0.0.1", "http://127.0.0.1/", { heuristic: true }) ]
+ });
+
+ do_print("visit url, ipv6 literal");
+ yield check_autocomplete({
+ search: "[2001:db8::1]",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("[2001:db8::1]", "http://[2001:db8::1]/", { heuristic: true }) ]
+ });
+
+ // Setting keyword.enabled to false should always try to visit.
+ let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled");
+ Services.prefs.setBoolPref("keyword.enabled", false);
+ do_register_cleanup(() => {
+ Services.prefs.clearUserPref("keyword.enabled");
+ });
+ do_print("visit url, keyword.enabled = false");
+ yield check_autocomplete({
+ search: "bacon",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("bacon", "http://bacon/", { heuristic: true }) ]
+ });
+ do_print("visit two word query, keyword.enabled = false");
+ yield check_autocomplete({
+ search: "bacon lovers",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("bacon lovers", "bacon lovers", { heuristic: true }) ]
+ });
+ Services.prefs.setBoolPref("keyword.enabled", keywordEnabled);
+
+ do_print("visit url, scheme+host");
+ yield check_autocomplete({
+ search: "http://example",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("http://example", "http://example/", { heuristic: true }) ]
+ });
+
+ do_print("visit url, scheme+host");
+ yield check_autocomplete({
+ search: "ftp://example",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("ftp://example", "ftp://example/", { heuristic: true }) ]
+ });
+
+ do_print("visit url, host+port");
+ yield check_autocomplete({
+ search: "example:8080",
+ searchParam: "enable-actions",
+ matches: [ makeVisitMatch("example:8080", "http://example:8080/", { heuristic: true }) ]
+ });
+
+ do_print("numerical operations that look like urls should search");
+ yield check_autocomplete({
+ search: "123/12",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("123/12", { heuristic: true }) ]
+ });
+
+ do_print("numerical operations that look like urls should search");
+ yield check_autocomplete({
+ search: "123.12/12.1",
+ searchParam: "enable-actions",
+ matches: [ makeSearchMatch("123.12/12.1", { heuristic: true }) ]
+ });
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js
new file mode 100644
index 0000000000..f79573ae6c
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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
+
+add_task(function* test_escape() {
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
+
+ let uri1 = NetUtil.newURI("http://matchme/");
+ let uri2 = NetUtil.newURI("http://dontmatchme/");
+ let uri3 = NetUtil.newURI("http://title/1");
+ let uri4 = NetUtil.newURI("http://title/2");
+ let uri5 = NetUtil.newURI("http://tag/1");
+ let uri6 = NetUtil.newURI("http://tag/2");
+ let uri7 = NetUtil.newURI("http://crazytitle/");
+ let uri8 = NetUtil.newURI("http://katakana/");
+ let uri9 = NetUtil.newURI("http://ideograph/");
+ let uri10 = NetUtil.newURI("http://camel/pleaseMatchMe/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri1, title: "title1" },
+ { uri: uri2, title: "title1" },
+ { uri: uri3, title: "matchme2" },
+ { uri: uri4, title: "dontmatchme3" },
+ { uri: uri5, title: "title1" },
+ { uri: uri6, title: "title1" },
+ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" },
+ { uri: uri8, title: katakana.join("") },
+ { uri: uri9, title: ideograph.join("") },
+ { uri: uri10, title: "title1" }
+ ]);
+ yield addBookmark( { uri: uri5, title: "title1", tags: [ "matchme2" ] } );
+ yield addBookmark( { uri: uri6, title: "title1", tags: [ "dontmatchme3" ] } );
+
+ // match only on word boundaries
+ Services.prefs.setIntPref("browser.urlbar.matchBehavior", 2);
+
+ do_print("Match 'match' at the beginning or after / or on a CamelCase");
+ yield check_autocomplete({
+ search: "match",
+ matches: [ { uri: uri1, title: "title1" },
+ { uri: uri3, title: "matchme2" },
+ { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "title1" } ]
+ });
+
+ do_print("Match 'dont' at the beginning or after /");
+ yield check_autocomplete({
+ search: "dont",
+ matches: [ { uri: uri2, title: "title1" },
+ { uri: uri4, title: "dontmatchme3" },
+ { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Match 'match' at the beginning or after / or on a CamelCase");
+ yield check_autocomplete({
+ search: "2",
+ matches: [ { uri: uri3, title: "matchme2" },
+ { uri: uri4, title: "dontmatchme3" },
+ { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+ { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ]
+ });
+
+ do_print("Match 't' at the beginning or after /");
+ yield check_autocomplete({
+ search: "t",
+ matches: [ { uri: uri1, title: "title1" },
+ { uri: uri2, title: "title1" },
+ { uri: uri3, title: "matchme2" },
+ { uri: uri4, title: "dontmatchme3" },
+ { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+ { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "title1" } ]
+ });
+
+ do_print("Match 'word' after many consecutive word boundaries");
+ yield check_autocomplete({
+ search: "word",
+ matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ]
+ });
+
+ do_print("Match a word boundary '/' for everything");
+ yield check_autocomplete({
+ search: "/",
+ matches: [ { uri: uri1, title: "title1" },
+ { uri: uri2, title: "title1" },
+ { uri: uri3, title: "matchme2" },
+ { uri: uri4, title: "dontmatchme3" },
+ { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+ { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] },
+ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" },
+ { uri: uri8, title: katakana.join("") },
+ { uri: uri9, title: ideograph.join("") },
+ { uri: uri10, title: "title1" } ]
+ });
+
+ do_print("Match word boundaries '()_+' that are among word boundaries");
+ yield check_autocomplete({
+ search: "()_+",
+ matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ]
+ });
+
+ do_print("Katakana characters form a string, so match the beginning");
+ yield check_autocomplete({
+ search: katakana[0],
+ matches: [ { uri: uri8, title: katakana.join("") } ]
+ });
+
+/*
+ do_print("Middle of a katakana word shouldn't be matched");
+ yield check_autocomplete({
+ search: katakana[1],
+ matches: [ ]
+ });
+*/
+ do_print("Ideographs are treated as words so 'nin' is one word");
+ yield check_autocomplete({
+ search: ideograph[0],
+ matches: [ { uri: uri9, title: ideograph.join("") } ]
+ });
+
+ do_print("Ideographs are treated as words so 'ten' is another word");
+ yield check_autocomplete({
+ search: ideograph[1],
+ matches: [ { uri: uri9, title: ideograph.join("") } ]
+ });
+
+ do_print("Ideographs are treated as words so 'do' is yet another word");
+ yield check_autocomplete({
+ search: ideograph[2],
+ matches: [ { uri: uri9, title: ideograph.join("") } ]
+ });
+
+ do_print("Extra negative assert that we don't match in the middle");
+ yield check_autocomplete({
+ search: "ch",
+ matches: [ ]
+ });
+
+ do_print("Don't match one character after a camel-case word boundary (bug 429498)");
+ yield check_autocomplete({
+ search: "atch",
+ matches: [ ]
+ });
+
+ // match against word boundaries and anywhere
+ Services.prefs.setIntPref("browser.urlbar.matchBehavior", 1);
+
+ yield check_autocomplete({
+ search: "tch",
+ matches: [ { uri: uri1, title: "title1" },
+ { uri: uri2, title: "title1" },
+ { uri: uri3, title: "matchme2" },
+ { uri: uri4, title: "dontmatchme3" },
+ { uri: uri5, title: "title1", tags: [ "matchme2" ], style: [ "bookmark-tag" ] },
+ { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] },
+ { uri: uri10, title: "title1" } ]
+ });
+
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js b/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js
new file mode 100644
index 0000000000..adf6388869
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Ensure inline autocomplete doesn't return zero frecency pages.
+
+add_task(function* test_zzero_frec_domain() {
+ do_print("Searching for zero frecency domain should not autoFill it");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/framed_link/"),
+ transition: TRANSITION_FRAMED_LINK
+ });
+ yield check_autocomplete({
+ search: "moz",
+ autofilled: "moz",
+ completed: "moz"
+ });
+ yield cleanup();
+});
+
+add_task(function* test_zzero_frec_url() {
+ do_print("Searching for zero frecency url should not autoFill it");
+ Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://mozilla.org/framed_link/"),
+ transition: TRANSITION_FRAMED_LINK
+ });
+ yield check_autocomplete({
+ search: "mozilla.org/f",
+ autofilled: "mozilla.org/f",
+ completed: "mozilla.org/f"
+ });
+ yield cleanup();
+});
diff --git a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
new file mode 100644
index 0000000000..60ef8c48af
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+head = head_autocomplete.js
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ data/engine-rel-searchform.xml
+ data/engine-suggestions.xml
+ !/toolkit/components/places/tests/favicons/favicon-normal16.png
+
+[test_416211.js]
+[test_416214.js]
+[test_417798.js]
+[test_418257.js]
+[test_422277.js]
+[test_autocomplete_functional.js]
+[test_autocomplete_on_value_removed_479089.js]
+[test_autofill_default_behavior.js]
+[test_avoid_middle_complete.js]
+[test_avoid_stripping_to_empty_tokens.js]
+[test_casing.js]
+[test_do_not_trim.js]
+[test_download_embed_bookmarks.js]
+[test_dupe_urls.js]
+[test_empty_search.js]
+[test_enabled.js]
+[test_escape_self.js]
+[test_extension_matches.js]
+[test_ignore_protocol.js]
+[test_keyword_search.js]
+[test_keyword_search_actions.js]
+[test_keywords.js]
+[test_match_beginning.js]
+[test_multi_word_search.js]
+[test_query_url.js]
+[test_remote_tab_matches.js]
+skip-if = !sync
+[test_search_engine_alias.js]
+[test_search_engine_current.js]
+[test_search_engine_host.js]
+[test_search_engine_restyle.js]
+[test_search_suggestions.js]
+[test_special_search.js]
+[test_swap_protocol.js]
+[test_tab_matches.js]
+[test_trimming.js]
+[test_typed.js]
+[test_visit_url.js]
+[test_word_boundary_search.js]
+[test_zero_frecency.js]
diff --git a/toolkit/components/places/tests/unit/.eslintrc.js b/toolkit/components/places/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/places/tests/unit/bookmarks.corrupt.html b/toolkit/components/places/tests/unit/bookmarks.corrupt.html
new file mode 100644
index 0000000000..3cf43367fb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.corrupt.html
@@ -0,0 +1,36 @@
+<!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 LAST_MODIFIED="1177541029">Bookmarks</H1>
+
+<DL><p>
+ <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3>
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
+ <DT><A HREF="b0rked" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
+ </DL><p>
+ <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3>
+<DD>folder test comment
+ <DL><p>
+ <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&amp;text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A>
+<DD>item description
+ </DL>
+ <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
+ <DL><p>
+ <DT><A HREF="http://example.tld">Example.tld</A>
+ </DL><p>
+ <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A>
+ <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A>
+ <DT><A HREF="http://bogus-icon.mozilla.com/" ICON="b0rked" ID="rdf:#$GvPhC3">Getting Started</A>
+<DD>Livemark test comment
+ </DL><p>
+</DL><p>
diff --git a/toolkit/components/places/tests/unit/bookmarks.json b/toolkit/components/places/tests/unit/bookmarks.json
new file mode 100644
index 0000000000..afe62abae6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.json
@@ -0,0 +1 @@
+{"guid":"root________","title":"","id":1,"dateAdded":1361551978957783,"lastModified":1361551978957783,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","id":2,"parent":1,"dateAdded":1361551978957783,"lastModified":1361551979382837,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"OCyeUO5uu9FF","title":"Mozilla Firefox","id":6,"parent":2,"dateAdded":1361551979350273,"lastModified":1361551979376699,"type":"text/x-moz-place-container","children":[{"guid":"OCyeUO5uu9FG","title":"Help and Tutorials","id":7,"parent":6,"dateAdded":1361551979356436,"lastModified":1361551979362718,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/help/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FH","index":1,"title":"Customize Firefox","id":8,"parent":6,"dateAdded":1361551979365662,"lastModified":1361551979368077,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/customize/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FI","index":2,"title":"Get Involved","id":9,"parent":6,"dateAdded":1361551979371071,"lastModified":1361551979373745,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/community/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FJ","index":3,"title":"About Us","id":10,"parent":6,"dateAdded":1361551979376699,"lastModified":1361551979379060,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/about/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="}]},{"guid":"OCyeUO5uu9FK","index":1,"title":"","id":11,"parent":2,"dateAdded":1361551979380988,"lastModified":1361551979380988,"type":"text/x-moz-place-separator"},{"guid":"OCyeUO5uu9FL","index":2,"title":"test","id":12,"parent":2,"dateAdded":1177541020000000,"lastModified":1177541050000000,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"folder test comment"}],"type":"text/x-moz-place-container","children":[{"guid":"OCyeUO5uu9GX","title":"test post keyword","id":13,"parent":12,"dateAdded":1177375336000000,"lastModified":1177375423000000,"annos":[{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"item description"},{"name":"bookmarkProperties/POSTData","flags":0,"expires":4,"mimeType":null,"type":3,"value":"hidden1%3Dbar&text1%3D%25s"},{"name":"bookmarkProperties/loadInSidebar","flags":0,"expires":4,"mimeType":null,"type":1,"value":1}],"type":"text/x-moz-place","uri":"http://test/post","keyword":"test","charset":"ISO-8859-1"}]}]},{"index":1,"title":"Bookmarks Toolbar","id":3,"parent":1,"dateAdded":1361551978957783,"lastModified":1177541050000000,"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":[{"guid":"OCyeUO5uu9FB","title":"Getting Started","id":15,"parent":3,"dateAdded":1361551979409695,"lastModified":1361551979412080,"type":"text/x-moz-place","uri":"http://en-us.www.mozilla.com/en-US/firefox/central/", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="},{"guid":"OCyeUO5uu9FR","index":1,"title":"Latest Headlines","id":16,"parent":3,"dateAdded":1361551979451584,"lastModified":1361551979457086,"livemark":1,"annos":[{"name":"placesInternal/READ_ONLY","flags":0,"expires":4,"mimeType":null,"type":1,"value":1},{"name":"livemark/feedURI","flags":0,"expires":4,"mimeType":null,"type":3,"value":"http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"},{"name":"livemark/siteURI","flags":0,"expires":4,"mimeType":null,"type":3,"value":"http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/"}],"type":"text/x-moz-place-container","children":[]}]},{"index":2,"title":"Tags","id":4,"parent":1,"dateAdded":1361551978957783,"lastModified":1361551978957783,"type":"text/x-moz-place-container","root":"tagsFolder","children":[]},{"index":3,"title":"Unsorted Bookmarks","id":5,"parent":1,"dateAdded":1361551978957783,"lastModified":1177541050000000,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"OCyeUO5uu9FW","title":"Example.tld","id":14,"parent":5,"dateAdded":1361551979401846,"lastModified":1361551979402952,"type":"text/x-moz-place","uri":"http://example.tld/"}]}]}
diff --git a/toolkit/components/places/tests/unit/bookmarks.preplaces.html b/toolkit/components/places/tests/unit/bookmarks.preplaces.html
new file mode 100644
index 0000000000..2e5a1baf02
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.preplaces.html
@@ -0,0 +1,35 @@
+<!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 LAST_MODIFIED="1177541029">Bookmarks</H1>
+
+<DL><p>
+ <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3>
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
+ </DL><p>
+ <HR>
+ <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3>
+<DD>folder test comment
+ <DL><p>
+ <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&amp;text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A>
+<DD>item description
+ </DL>
+ <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
+ <DL><p>
+ <DT><A HREF="http://example.tld">Example.tld</A>
+ </DL><p>
+ <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A>
+ <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A>
+<DD>Livemark test comment
+ </DL><p>
+</DL><p>
diff --git a/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html
new file mode 100644
index 0000000000..9fe662f320
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+ <HTML>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+ <Title>Bookmarks</Title>
+ <H1>Bookmarks</H1>
+ <DT><H3>Subtitle</H3>
+ <DL><p>
+ <DT><A HREF="http://www.mozilla.org/">Mozilla</A>
+ </DL><p>
+</HTML>
diff --git a/toolkit/components/places/tests/unit/bug476292.sqlite b/toolkit/components/places/tests/unit/bug476292.sqlite
new file mode 100644
index 0000000000..43130cb51e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bug476292.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/corruptDB.sqlite b/toolkit/components/places/tests/unit/corruptDB.sqlite
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/toolkit/components/places/tests/unit/corruptDB.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/default.sqlite b/toolkit/components/places/tests/unit/default.sqlite
new file mode 100644
index 0000000000..8fbd3bc9ac
--- /dev/null
+++ b/toolkit/components/places/tests/unit/default.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/head_bookmarks.js b/toolkit/components/places/tests/unit/head_bookmarks.js
new file mode 100644
index 0000000000..842a66b313
--- /dev/null
+++ b/toolkit/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,20 @@
+/* -*- 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 Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.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.
diff --git a/toolkit/components/places/tests/unit/livemark.xml b/toolkit/components/places/tests/unit/livemark.xml
new file mode 100644
index 0000000000..db2ea9023c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/livemark.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>Livemark Feed</title>
+ <link href="https://example.com/"/>
+ <updated>2016-08-09T19:51:45.147Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:e7947414-6ee0-4009-ae75-8b0ad3c6894b</id>
+ <entry>
+ <title>Some awesome article</title>
+ <link href="https://example.com/some-article"/>
+ <id>urn:uuid:d72ce019-0a56-4a0b-ac03-f66117d78141</id>
+ <updated>2016-08-09T19:57:22.178Z</updated>
+ <summary>My great article summary.</summary>
+ </entry>
+</feed>
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
new file mode 100644
index 0000000000..38762b3f18
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
@@ -0,0 +1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"},{"guid":"XF4yRP6bTuil","title":"Mobile bookmarks query","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":5,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":9,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"KIa9iKZab2Z5","title":"Add-ons","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":10,"type":"text/x-moz-place","uri":"https://addons.mozilla.org"}]}]} \ No newline at end of file
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
new file mode 100644
index 0000000000..7319a3a52a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
@@ -0,0 +1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":5,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":6,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":7,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":8,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"}]} \ No newline at end of file
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
new file mode 100644
index 0000000000..afe13c975c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
@@ -0,0 +1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":3,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"},{"guid":"F_LBgd1fS_uQ","title":"Mobile bookmarks query for first folder","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"},{"guid":"oIpmQXMWsXvY","title":"Mobile bookmarks query for second folder","index":2,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":12,"type":"text/x-moz-place","uri":"place:folder=102"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":5,"type":"text/x-moz-place","uri":"https://mozilla.org/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":6,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":7,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"o4YjJpgsufU-","title":"Mobile Bookmarks","index":7,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":102,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","children":[{"guid":"sSZ86WT9WbN3","title":"DXR","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":9,"type":"text/x-moz-place","uri":"https://dxr.mozilla.org"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":10,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":11,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]} \ No newline at end of file
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
new file mode 100644
index 0000000000..27f5825ecb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
@@ -0,0 +1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]}]} \ No newline at end of file
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
new file mode 100644
index 0000000000..85721f2fa0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
@@ -0,0 +1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731955000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731961000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":7,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":8,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]} \ No newline at end of file
diff --git a/toolkit/components/places/tests/unit/nsDummyObserver.js b/toolkit/components/places/tests/unit/nsDummyObserver.js
new file mode 100644
index 0000000000..9049d04b3f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/nsDummyObserver.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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+// Dummy boomark/history observer
+function DummyObserver() {
+ Services.obs.notifyObservers(null, "dummy-observer-created", null);
+}
+
+DummyObserver.prototype = {
+ // history observer
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, aTransitionType) {
+ Services.obs.notifyObservers(null, "dummy-observer-visited", null);
+ },
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+
+ // bookmark observer
+ // onBeginUpdateBatch: function() {},
+ // onEndUpdateBatch: function() {},
+ onItemAdded: function(aItemId, aParentId, aIndex, aItemType, aURI) {
+ Services.obs.notifyObservers(null, "dummy-observer-item-added", null);
+ },
+ onItemChanged: function () {},
+ onItemRemoved: function() {},
+ onItemVisited: function() {},
+ onItemMoved: function() {},
+
+ classID: Components.ID("62e221d3-68c3-4e1a-8943-a27beb5005fe"),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ Ci.nsINavHistoryObserver,
+ ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DummyObserver]);
diff --git a/toolkit/components/places/tests/unit/nsDummyObserver.manifest b/toolkit/components/places/tests/unit/nsDummyObserver.manifest
new file mode 100644
index 0000000000..ed4d87fffa
--- /dev/null
+++ b/toolkit/components/places/tests/unit/nsDummyObserver.manifest
@@ -0,0 +1,4 @@
+component 62e221d3-68c3-4e1a-8943-a27beb5005fe nsDummyObserver.js
+contract @mozilla.org/places/test/dummy-observer;1 62e221d3-68c3-4e1a-8943-a27beb5005fe
+category bookmark-observers nsDummyObserver @mozilla.org/places/test/dummy-observer;1
+category history-observers nsDummyObserver @mozilla.org/places/test/dummy-observer;1
diff --git a/toolkit/components/places/tests/unit/places.sparse.sqlite b/toolkit/components/places/tests/unit/places.sparse.sqlite
new file mode 100644
index 0000000000..915089021c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/places.sparse.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/test_000_frecency.js b/toolkit/components/places/tests/unit/test_000_frecency.js
new file mode 100644
index 0000000000..0a7347a026
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_000_frecency.js
@@ -0,0 +1,273 @@
+/* -*- 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/. */
+
+/*
+
+Autocomplete Frecency Tests
+
+- add a visit for each score permutation
+- search
+- test number of matches
+- test each item's location in results
+
+*/
+
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+} catch (ex) {
+ do_throw("Could not get services\n");
+}
+
+var bucketPrefs = [
+ [ "firstBucketCutoff", "firstBucketWeight"],
+ [ "secondBucketCutoff", "secondBucketWeight"],
+ [ "thirdBucketCutoff", "thirdBucketWeight"],
+ [ "fourthBucketCutoff", "fourthBucketWeight"],
+ [ null, "defaultBucketWeight"]
+];
+
+var bonusPrefs = {
+ embedVisitBonus: Ci.nsINavHistoryService.TRANSITION_EMBED,
+ framedLinkVisitBonus: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
+ linkVisitBonus: Ci.nsINavHistoryService.TRANSITION_LINK,
+ typedVisitBonus: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ bookmarkVisitBonus: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ downloadVisitBonus: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ permRedirectVisitBonus: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+ tempRedirectVisitBonus: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
+ reloadVisitBonus: Ci.nsINavHistoryService.TRANSITION_RELOAD,
+};
+
+// create test data
+var searchTerm = "frecency";
+var results = [];
+var matchCount = 0;
+var now = Date.now();
+var prefPrefix = "places.frecency.";
+
+function* task_initializeBucket(bucket) {
+ let [cutoffName, weightName] = bucket;
+ // get pref values
+ var weight = 0, cutoff = 0;
+ try {
+ weight = prefs.getIntPref(prefPrefix + weightName);
+ } catch (ex) {}
+ try {
+ cutoff = prefs.getIntPref(prefPrefix + cutoffName);
+ } catch (ex) {}
+
+ if (cutoff < 1)
+ return;
+
+ // generate a date within the cutoff period
+ var dateInPeriod = (now - ((cutoff - 1) * 86400 * 1000)) * 1000;
+
+ for (let [bonusName, visitType] of Object.entries(bonusPrefs)) {
+ var frecency = -1;
+ var calculatedURI = null;
+ var matchTitle = "";
+ var bonusValue = prefs.getIntPref(prefPrefix + bonusName);
+ // unvisited (only for first cutoff date bucket)
+ if (bonusName == "unvisitedBookmarkBonus" || bonusName == "unvisitedTypedBonus") {
+ if (cutoffName == "firstBucketCutoff") {
+ let points = Math.ceil(bonusValue / parseFloat(100.0) * weight);
+ var visitCount = 1; // bonusName == "unvisitedBookmarkBonus" ? 1 : 0;
+ frecency = Math.ceil(visitCount * points);
+ calculatedURI = uri("http://" + searchTerm + ".com/" +
+ bonusName + ":" + bonusValue + "/cutoff:" + cutoff +
+ "/weight:" + weight + "/frecency:" + frecency);
+ if (bonusName == "unvisitedBookmarkBonus") {
+ matchTitle = searchTerm + "UnvisitedBookmark";
+ bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, calculatedURI, bmsvc.DEFAULT_INDEX, matchTitle);
+ }
+ else {
+ matchTitle = searchTerm + "UnvisitedTyped";
+ yield PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: now
+ });
+ histsvc.markPageAsTyped(calculatedURI);
+ }
+ }
+ }
+ else {
+ // visited
+ // visited bookmarks get the visited bookmark bonus twice
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK)
+ bonusValue = bonusValue * 2;
+
+ let points = Math.ceil(1 * ((bonusValue / parseFloat(100.000000)).toFixed(6) * weight) / 1);
+ if (!points) {
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_EMBED ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_RELOAD ||
+ bonusName == "defaultVisitBonus")
+ frecency = 0;
+ else
+ frecency = -1;
+ }
+ else
+ frecency = points;
+ calculatedURI = uri("http://" + searchTerm + ".com/" +
+ bonusName + ":" + bonusValue + "/cutoff:" + cutoff +
+ "/weight:" + weight + "/frecency:" + frecency);
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) {
+ matchTitle = searchTerm + "Bookmarked";
+ bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, calculatedURI, bmsvc.DEFAULT_INDEX, matchTitle);
+ }
+ else
+ matchTitle = calculatedURI.spec.substr(calculatedURI.spec.lastIndexOf("/")+1);
+ yield PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ transition: visitType,
+ visitDate: dateInPeriod
+ });
+ }
+
+ if (calculatedURI && frecency) {
+ results.push([calculatedURI, frecency, matchTitle]);
+ yield PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: dateInPeriod
+ });
+ }
+ }
+}
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+add_task(function* test_frecency()
+{
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+ for (let bucket of bucketPrefs) {
+ yield task_initializeBucket(bucket);
+ }
+
+ // sort results by frecency
+ results.sort((a, b) => b[1] - a[1]);
+ // Make sure there's enough results returned
+ prefs.setIntPref("browser.urlbar.maxRichResults", results.length);
+
+ // DEBUG
+ // results.every(function(el) { dump("result: " + el[1] + ": " + el[0].spec + " (" + el[2] + ")\n"); return true; })
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ // always search in history + bookmarks, no matter what the default is
+ prefs.setIntPref("browser.urlbar.search.sources", 3);
+ prefs.setIntPref("browser.urlbar.default.behavior", 0);
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ let deferred = Promise.defer();
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+
+ // test that all records with non-zero frecency were matched
+ do_check_eq(controller.matchCount, results.length);
+
+ // test that matches are sorted by frecency
+ for (var i = 0; i < controller.matchCount; i++) {
+ let searchURL = controller.getValueAt(i);
+ let expectURL = results[i][0].spec;
+ if (searchURL == expectURL) {
+ do_check_eq(controller.getValueAt(i), results[i][0].spec);
+ do_check_eq(controller.getCommentAt(i), results[i][2]);
+ } else {
+ // If the results didn't match exactly, perhaps it's still the right
+ // frecency just in the wrong "order" (order of same frecency is
+ // undefined), so check if frecency matches. This is okay because we
+ // can still ensure the correct number of expected frecencies.
+ let getFrecency = aURL => aURL.match(/frecency:(-?\d+)$/)[1];
+ print("### checking for same frecency between '" + searchURL +
+ "' and '" + expectURL + "'");
+ do_check_eq(getFrecency(searchURL), getFrecency(expectURL));
+ }
+ }
+ deferred.resolve();
+ };
+
+ controller.startSearch(searchTerm);
+
+ yield deferred.promise;
+});
diff --git a/toolkit/components/places/tests/unit/test_1085291.js b/toolkit/components/places/tests/unit/test_1085291.js
new file mode 100644
index 0000000000..3159ff8bc7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1085291.js
@@ -0,0 +1,42 @@
+add_task(function* () {
+ // test that nodes inserted by incremental update for bookmarks of all types
+ // have the extra bookmark properties (bookmarkGuid, dateAdded, lastModified).
+
+ // getFolderContents opens the root node.
+ let root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+
+ function* insertAndTest(bmInfo) {
+ bmInfo = yield PlacesUtils.bookmarks.insert(bmInfo);
+ let node = root.getChild(root.childCount - 1);
+ Assert.equal(node.bookmarkGuid, bmInfo.guid);
+ Assert.equal(node.dateAdded, bmInfo.dateAdded * 1000);
+ Assert.equal(node.lastModified, bmInfo.lastModified * 1000);
+ }
+
+ // Normal bookmark.
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ , title: "Test Bookmark"
+ , url: "http://test.url.tld" });
+
+ // place: query
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ , title: "Test Query"
+ , url: "place:folder=BOOKMARKS_MENU" });
+
+ // folder
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_FOLDER
+ , title: "Test Folder" });
+
+ // separator
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+
+ root.containerOpen = false;
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_1105208.js b/toolkit/components/places/tests/unit/test_1105208.js
new file mode 100644
index 0000000000..39a27c95f0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1105208.js
@@ -0,0 +1,24 @@
+// Test that result node for folder shortcuts get the target folder title if
+// the shortcut itself has no title set.
+add_task(function* () {
+ let shortcutInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:folder=TOOLBAR"
+ });
+
+ let unfiledRoot =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.equal(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+
+ let toolbarInfo =
+ yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid);
+ Assert.equal(shortcutNode.title, toolbarInfo.title);
+
+ unfiledRoot.containerOpen = false;
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_1105866.js b/toolkit/components/places/tests/unit/test_1105866.js
new file mode 100644
index 0000000000..eb376bbe20
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1105866.js
@@ -0,0 +1,63 @@
+add_task(function* test_folder_shortcuts() {
+ let shortcutInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:folder=TOOLBAR"
+ });
+
+ let unfiledRoot =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(shortcutNode.itemId,
+ yield PlacesUtils.promiseItemId(shortcutInfo.guid));
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).folderItemId,
+ PlacesUtils.toolbarFolderId);
+ Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).targetFolderGuid,
+ PlacesUtils.bookmarks.toolbarGuid);
+
+ // test that a node added incrementally also behaves just as well.
+ shortcutInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:folder=BOOKMARKS_MENU"
+ });
+ shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(shortcutNode.itemId,
+ yield PlacesUtils.promiseItemId(shortcutInfo.guid));
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).folderItemId,
+ PlacesUtils.bookmarksMenuFolderId);
+ Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).targetFolderGuid,
+ PlacesUtils.bookmarks.menuGuid);
+
+ unfiledRoot.containerOpen = false;
+});
+
+add_task(function* test_plain_folder() {
+ let folderInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+
+ let unfiledRoot =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let lastChild = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(lastChild.bookmarkGuid, folderInfo.guid);
+ Assert.strictEqual(PlacesUtils.asQuery(lastChild).targetFolderGuid,
+ folderInfo.guid);
+});
+
+add_task(function* test_non_item_query() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(), options).root;
+ Assert.strictEqual(root.itemId, -1);
+ Assert.strictEqual(PlacesUtils.asQuery(root).folderItemId, -1);
+ Assert.strictEqual(root.bookmarkGuid, "");
+ Assert.strictEqual(PlacesUtils.asQuery(root).targetFolderGuid, "");
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_317472.js b/toolkit/components/places/tests/unit/test_317472.js
new file mode 100644
index 0000000000..a086519161
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_317472.js
@@ -0,0 +1,65 @@
+/* -*- 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/. */
+
+const charset = "UTF-8";
+const CHARSET_ANNO = "URIProperties/characterSet";
+
+const TEST_URI = uri("http://foo.com");
+const TEST_BOOKMARKED_URI = uri("http://bar.com");
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // add pages to history
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ yield PlacesTestUtils.addVisits(TEST_BOOKMARKED_URI);
+
+ // create bookmarks on TEST_BOOKMARKED_URI
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId,
+ TEST_BOOKMARKED_URI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_BOOKMARKED_URI.spec);
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.toolbarFolderId,
+ TEST_BOOKMARKED_URI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_BOOKMARKED_URI.spec);
+
+ // set charset on not-bookmarked page
+ yield PlacesUtils.setCharsetForURI(TEST_URI, charset);
+ // set charset on bookmarked page
+ yield PlacesUtils.setCharsetForURI(TEST_BOOKMARKED_URI, charset);
+
+ // check that we have created a page annotation
+ do_check_eq(PlacesUtils.annotations.getPageAnnotation(TEST_URI, CHARSET_ANNO), charset);
+
+ // get charset from not-bookmarked page
+ do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_URI)), charset);
+
+ // get charset from bookmarked page
+ do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset);
+
+ yield PlacesTestUtils.clearHistory();
+
+ // ensure that charset has gone for not-bookmarked page
+ do_check_neq((yield PlacesUtils.getCharsetForURI(TEST_URI)), charset);
+
+ // check that page annotation has been removed
+ try {
+ PlacesUtils.annotations.getPageAnnotation(TEST_URI, CHARSET_ANNO);
+ do_throw("Charset page annotation has not been removed correctly");
+ } catch (e) {}
+
+ // ensure that charset still exists for bookmarked page
+ do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset);
+
+ // remove charset from bookmark and check that has gone
+ yield PlacesUtils.setCharsetForURI(TEST_BOOKMARKED_URI, "");
+ do_check_neq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset);
+});
diff --git a/toolkit/components/places/tests/unit/test_331487.js b/toolkit/components/places/tests/unit/test_331487.js
new file mode 100644
index 0000000000..55d41aebf4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_331487.js
@@ -0,0 +1,95 @@
+/* -*- 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/. */
+
+// Get history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService);
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+// main
+function run_test() {
+ // add a folder
+ var folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX);
+
+ // add a bookmark to the folder
+ var b1 = bmsvc.insertBookmark(folder, uri("http://a1.com/"),
+ bmsvc.DEFAULT_INDEX, "1 title");
+ // add a subfolder
+ var sf1 = bmsvc.createFolder(folder, "subfolder 1", bmsvc.DEFAULT_INDEX);
+
+ // add a bookmark to the subfolder
+ var b2 = bmsvc.insertBookmark(sf1, uri("http://a2.com/"),
+ bmsvc.DEFAULT_INDEX, "2 title");
+
+ // add a subfolder to the subfolder
+ var sf2 = bmsvc.createFolder(sf1, "subfolder 2", bmsvc.DEFAULT_INDEX);
+
+ // add a bookmark to the subfolder of the subfolder
+ var b3 = bmsvc.insertBookmark(sf2, uri("http://a3.com/"),
+ bmsvc.DEFAULT_INDEX, "3 title");
+
+ // bookmark query that should result in the "hierarchical" result
+ // because there is one query, one folder,
+ // no begin time, no end time, no domain, no uri, no search term
+ // and no max results. See GetSimpleBookmarksQueryFolder()
+ // for more details.
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ var query = histsvc.getNewQuery();
+ query.setFolders([folder], 1);
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).itemId, b1);
+ do_check_eq(root.getChild(1).itemId, sf1);
+
+ // check the contents of the subfolder
+ var sf1Node = root.getChild(1);
+ sf1Node = sf1Node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ sf1Node.containerOpen = true;
+ do_check_eq(sf1Node.childCount, 2);
+ do_check_eq(sf1Node.getChild(0).itemId, b2);
+ do_check_eq(sf1Node.getChild(1).itemId, sf2);
+
+ // check the contents of the subfolder's subfolder
+ var sf2Node = sf1Node.getChild(1);
+ sf2Node = sf2Node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ sf2Node.containerOpen = true;
+ do_check_eq(sf2Node.childCount, 1);
+ do_check_eq(sf2Node.getChild(0).itemId, b3);
+
+ sf2Node.containerOpen = false;
+ sf1Node.containerOpen = false;
+ root.containerOpen = false;
+
+ // bookmark query that should result in a flat list
+ // because we specified max results
+ options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.maxResults = 10;
+ query = histsvc.getNewQuery();
+ query.setFolders([folder], 1);
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 3);
+ do_check_eq(root.getChild(0).itemId, b1);
+ do_check_eq(root.getChild(1).itemId, b2);
+ do_check_eq(root.getChild(2).itemId, b3);
+ root.containerOpen = false;
+
+ // XXX TODO
+ // test that if we have: more than one query,
+ // multiple folders, a begin time, an end time, a domain, a uri
+ // or a search term, that we get the (correct) flat list results
+ // (like we do when specified maxResults)
+}
diff --git a/toolkit/components/places/tests/unit/test_384370.js b/toolkit/components/places/tests/unit/test_384370.js
new file mode 100644
index 0000000000..ec6f43683e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -0,0 +1,173 @@
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+var tagData = [
+ { uri: uri("http://slint.us"), tags: ["indie", "kentucky", "music"] },
+ { uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), tags: ["dinosaur", "dj", "rad word"] }
+];
+
+var bookmarkData = [
+ { uri: uri("http://slint.us"), title: "indie, kentucky, music" },
+ { uri: uri("http://en.wikipedia.org/wiki/Diplodocus"), title: "dinosaur, dj, rad word" }
+];
+
+function run_test() {
+ run_next_test();
+}
+
+/*
+ HTML+FEATURES SUMMARY:
+ - import legacy bookmarks
+ - export as json, import, test (tests integrity of html > json)
+ - export as html, import, test (tests integrity of json > html)
+
+ BACKUP/RESTORE SUMMARY:
+ - create a bookmark in each root
+ - tag multiple URIs with multiple tags
+ - export as json, import, test
+*/
+add_task(function* () {
+ // Remove eventual bookmarks.exported.json.
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.json");
+ if ((yield OS.File.exists(jsonFile)))
+ yield OS.File.remove(jsonFile);
+
+ // Test importing a pre-Places canonical bookmarks file.
+ // Note: we do not empty the db before this import to catch bugs like 380999
+ let htmlFile = OS.Path.join(do_get_cwd().path, "bookmarks.preplaces.html");
+ yield BookmarkHTMLUtils.importFromFile(htmlFile, true);
+
+ // Populate the database.
+ for (let { uri, tags } of tagData) {
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+ for (let { uri, title } of bookmarkData) {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title });
+ }
+ for (let { uri, title } of bookmarkData) {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: uri,
+ title });
+ }
+
+ yield validate();
+
+ // Test exporting a Places canonical json file.
+ // 1. export to bookmarks.exported.json
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+ do_print("exported json");
+
+ // 2. empty bookmarks db
+ // 3. import bookmarks.exported.json
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+ do_print("imported json");
+
+ // 4. run the test-suite
+ yield validate();
+ do_print("validated import");
+});
+
+function* validate() {
+ yield testMenuBookmarks();
+ yield testToolbarBookmarks();
+ testUnfiledBookmarks();
+ testTags();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+}
+
+// Tests a bookmarks datastore that has a set of bookmarks, etc
+// that flex each supported field and feature.
+function* testMenuBookmarks() {
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ Assert.equal(root.childCount, 3);
+
+ let separatorNode = root.getChild(1);
+ Assert.equal(separatorNode.type, separatorNode.RESULT_TYPE_SEPARATOR);
+
+ let folderNode = root.getChild(2);
+ Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ Assert.equal(folderNode.title, "test");
+ let folder = yield PlacesUtils.bookmarks.fetch(folderNode.bookmarkGuid);
+ Assert.equal(folder.dateAdded.getTime(), 1177541020000);
+
+ Assert.equal(PlacesUtils.asQuery(folderNode).hasChildren, true);
+
+ Assert.equal("folder test comment",
+ PlacesUtils.annotations.getItemAnnotation(folderNode.itemId,
+ DESCRIPTION_ANNO));
+
+ // open test folder, and test the children
+ folderNode.containerOpen = true;
+ Assert.equal(folderNode.childCount, 1);
+
+ let bookmarkNode = folderNode.getChild(0);
+ Assert.equal("http://test/post", bookmarkNode.uri);
+ Assert.equal("test post keyword", bookmarkNode.title);
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(bookmarkNode.itemId,
+ LOAD_IN_SIDEBAR_ANNO));
+ Assert.equal(bookmarkNode.dateAdded, 1177375336000000);
+
+ let entry = yield PlacesUtils.keywords.fetch({ url: bookmarkNode.uri });
+ Assert.equal("test", entry.keyword);
+ Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData);
+
+ Assert.equal("ISO-8859-1",
+ (yield PlacesUtils.getCharsetForURI(NetUtil.newURI(bookmarkNode.uri))));
+ Assert.equal("item description",
+ PlacesUtils.annotations.getItemAnnotation(bookmarkNode.itemId,
+ DESCRIPTION_ANNO));
+
+ folderNode.containerOpen = false;
+ root.containerOpen = false;
+}
+
+function* testToolbarBookmarks() {
+ let root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+
+ // child count (add 2 for pre-existing items)
+ Assert.equal(root.childCount, bookmarkData.length + 2);
+
+ let livemarkNode = root.getChild(1);
+ Assert.equal("Latest Headlines", livemarkNode.title);
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemarkNode.itemId });
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ livemark.siteURI.spec);
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+ livemark.feedURI.spec);
+
+ // test added bookmark data
+ let bookmarkNode = root.getChild(2);
+ Assert.equal(bookmarkNode.uri, bookmarkData[0].uri.spec);
+ Assert.equal(bookmarkNode.title, bookmarkData[0].title);
+ bookmarkNode = root.getChild(3);
+ Assert.equal(bookmarkNode.uri, bookmarkData[1].uri.spec);
+ Assert.equal(bookmarkNode.title, bookmarkData[1].title);
+
+ root.containerOpen = false;
+}
+
+function testUnfiledBookmarks() {
+ let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ // child count (add 1 for pre-existing item)
+ Assert.equal(root.childCount, bookmarkData.length + 1);
+ for (let i = 1; i < root.childCount; ++i) {
+ let child = root.getChild(i);
+ Assert.equal(child.uri, bookmarkData[i - 1].uri.spec);
+ Assert.equal(child.title, bookmarkData[i - 1].title);
+ if (child.tags)
+ Assert.equal(child.tags, bookmarkData[i - 1].title);
+ }
+ root.containerOpen = false;
+}
+
+function testTags() {
+ for (let { uri, tags } of tagData) {
+ do_print("Test tags for " + uri.spec + ": " + tags + "\n");
+ let foundTags = PlacesUtils.tagging.getTagsForURI(uri);
+ Assert.equal(foundTags.length, tags.length);
+ Assert.ok(tags.every(tag => foundTags.includes(tag)));
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_385397.js b/toolkit/components/places/tests/unit/test_385397.js
new file mode 100644
index 0000000000..4b60d47683
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_385397.js
@@ -0,0 +1,142 @@
+/* -*- 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/. */
+
+const TOTAL_SITES = 20;
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ let now = (Date.now() - 10000) * 1000;
+
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test-" + i + ".com/";
+ let testURI = uri(site);
+ let testImageURI = uri(site + "blank.gif");
+ let when = now + (i * TOTAL_SITES * 1000);
+ yield PlacesTestUtils.addVisits([
+ { uri: testURI, visitDate: when, transition: TRANSITION_TYPED },
+ { uri: testImageURI, visitDate: when + 1000, transition: TRANSITION_EMBED },
+ { uri: testImageURI, visitDate: when + 2000, transition: TRANSITION_FRAMED_LINK },
+ { uri: testURI, visitDate: when + 3000, transition: TRANSITION_LINK },
+ ]);
+ }
+
+ // verify our visits AS_VISIT, ordered by date descending
+ // including hidden
+ // we should get 80 visits:
+ // http://www.test-19.com/
+ // http://www.test-19.com/blank.gif
+ // http://www.test-19.com/
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-0.com/
+ // http://www.test-0.com/blank.gif
+ // http://www.test-0.com/
+ // http://www.test-0.com/
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ options.includeHidden = true;
+ let root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ // Embed visits are not added to the database, thus they won't appear.
+ do_check_eq(cc, 3 * TOTAL_SITES);
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let index = i * 3;
+ let node = root.getChild(index);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ do_check_eq(node.uri, site + "blank.gif");
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // verify our visits AS_VISIT, ordered by date descending
+ // we should get 40 visits:
+ // http://www.test-19.com/
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-0.com/
+ // http://www.test-0.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ // 2 * TOTAL_SITES because we count the TYPED and LINK, but not EMBED or FRAMED
+ do_check_eq(cc, 2 * TOTAL_SITES);
+ for (let i=0; i < TOTAL_SITES; i++) {
+ let index = i * 2;
+ let node = root.getChild(index);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // test our optimized query for the places menu
+ // place:type=0&sort=4&maxResults=10
+ // verify our visits AS_URI, ordered by date descending
+ // we should get 10 visits:
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.maxResults = 10;
+ options.resultType = options.RESULTS_AS_URI;
+ root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ do_check_eq(cc, options.maxResults);
+ for (let i=0; i < cc; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // test without a maxResults, which executes a different query
+ // but the first 10 results should be the same.
+ // verify our visits AS_URI, ordered by date descending
+ // we should get 20 visits, but the first 10 should be
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ do_check_eq(cc, TOTAL_SITES);
+ for (let i=0; i < 10; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_399264_query_to_string.js b/toolkit/components/places/tests/unit/test_399264_query_to_string.js
new file mode 100644
index 0000000000..6e6cc279c3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399264_query_to_string.js
@@ -0,0 +1,51 @@
+/* -*- 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/. */
+
+/**
+ * Obtains the id of the folder obtained from the query.
+ *
+ * @param aFolderID
+ * The id of the folder we want to generate a query for.
+ * @returns the string representation of the query for the given folder.
+ */
+function query_string(aFolderID)
+{
+ var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ var query = hs.getNewQuery();
+ query.setFolders([aFolderID], 1);
+ var options = hs.getNewQueryOptions();
+ return hs.queriesToQueryString([query], 1, options);
+}
+
+function run_test()
+{
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+ const QUERIES = [
+ "folder=PLACES_ROOT"
+ , "folder=BOOKMARKS_MENU"
+ , "folder=TAGS"
+ , "folder=UNFILED_BOOKMARKS"
+ , "folder=TOOLBAR"
+ ];
+ const FOLDER_IDS = [
+ bs.placesRoot
+ , bs.bookmarksMenuFolder
+ , bs.tagsFolder
+ , bs.unfiledBookmarksFolder
+ , bs.toolbarFolder
+ ];
+
+
+ for (var i = 0; i < QUERIES.length; i++) {
+ var result = query_string(FOLDER_IDS[i]);
+ dump("Looking for '" + QUERIES[i] + "' in '" + result + "'\n");
+ do_check_neq(-1, result.indexOf(QUERIES[i]));
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_399264_string_to_query.js b/toolkit/components/places/tests/unit/test_399264_string_to_query.js
new file mode 100644
index 0000000000..bd29316d9a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399264_string_to_query.js
@@ -0,0 +1,75 @@
+/* -*- 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/. */
+
+/**
+ * Obtains the id of the folder obtained from the query.
+ *
+ * @param aQuery
+ * The query to obtain the folder id from.
+ * @returns the folder id of the folder of the root node of the query.
+ */
+function folder_id(aQuery)
+{
+ var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ dump("Checking query '" + aQuery + "'\n");
+ var options = { };
+ var queries = { };
+ var size = { };
+ hs.queryStringToQueries(aQuery, queries, size, options);
+ var result = hs.executeQueries(queries.value, size.value, options.value);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_true(root.hasChildren);
+ var folderID = root.getChild(0).parent.itemId;
+ root.containerOpen = false;
+ return folderID;
+}
+
+function run_test()
+{
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+ const QUERIES = [
+ "place:folder=PLACES_ROOT"
+ , "place:folder=BOOKMARKS_MENU"
+ , "place:folder=TAGS"
+ , "place:folder=UNFILED_BOOKMARKS"
+ , "place:folder=TOOLBAR"
+ ];
+ const FOLDER_IDS = [
+ bs.placesRoot
+ , bs.bookmarksMenuFolder
+ , bs.tagsFolder
+ , bs.unfiledBookmarksFolder
+ , bs.toolbarFolder
+ ];
+
+ // add something in the bookmarks menu folder so a query to it returns results
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://example.com/bmf/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "bmf");
+
+ // add something to the tags folder
+ var ts = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+ ts.tagURI(uri("http://www.example.com/"), ["tag"]);
+
+ // add something to the unfiled bookmarks folder
+ bs.insertBookmark(bs.unfiledBookmarksFolder, uri("http://example.com/ubf/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "ubf");
+
+ // add something to the toolbar folder
+ bs.insertBookmark(bs.toolbarFolder, uri("http://example.com/tf/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "tf");
+
+ for (var i = 0; i < QUERIES.length; i++) {
+ var result = folder_id(QUERIES[i]);
+ dump("expected " + FOLDER_IDS[i] + ", got " + result + "\n");
+ do_check_eq(FOLDER_IDS[i], result);
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_399266.js b/toolkit/components/places/tests/unit/test_399266.js
new file mode 100644
index 0000000000..296d69414a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399266.js
@@ -0,0 +1,78 @@
+/* -*- 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/. */
+
+const TOTAL_SITES = 20;
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ let places = [];
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ for (let j = 0; j <= i; j++) {
+ places.push({ uri: uri("http://www.test-" + i + ".com/"),
+ transition: TRANSITION_TYPED });
+ // because these are embedded visits, they should not show up on our
+ // query results. If they do, we have a problem.
+ places.push({ uri: uri("http://www.hidden.com/hidden.gif"),
+ transition: TRANSITION_EMBED });
+ places.push({ uri: uri("http://www.alsohidden.com/hidden.gif"),
+ transition: TRANSITION_FRAMED_LINK });
+ }
+ }
+ yield PlacesTestUtils.addVisits(places);
+
+ // test our optimized query for the "Most Visited" item
+ // in the "Smart Bookmarks" folder
+ // place:queryType=0&sort=8&maxResults=10
+ // verify our visits AS_URI, ordered by visit count descending
+ // we should get 10 visits:
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ options.maxResults = 10;
+ options.resultType = options.RESULTS_AS_URI;
+ let root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ do_check_eq(cc, options.maxResults);
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // test without a maxResults, which executes a different query
+ // but the first 10 results should be the same.
+ // verify our visits AS_URI, ordered by visit count descending
+ // we should get 20 visits, but the first 10 should be
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ do_check_eq(cc, TOTAL_SITES);
+ for (let i = 0; i < 10; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_402799.js b/toolkit/components/places/tests/unit/test_402799.js
new file mode 100644
index 0000000000..263e20aa5b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_402799.js
@@ -0,0 +1,62 @@
+/* -*- 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/. */
+
+// Get history services
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);
+} catch (ex) {
+ do_throw("Could not get history services\n");
+}
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+}
+catch (ex) {
+ do_throw("Could not get the nav-bookmarks-service\n");
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+
+// main
+function run_test() {
+ var uri1 = uri("http://foo.bar/");
+
+ // create 2 bookmarks on the same uri
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri1,
+ bmsvc.DEFAULT_INDEX, "title 1");
+ bmsvc.insertBookmark(bmsvc.toolbarFolder, uri1,
+ bmsvc.DEFAULT_INDEX, "title 2");
+ // add some tags
+ tagssvc.tagURI(uri1, ["foo", "bar", "foobar", "foo bar"]);
+
+ // check that a generic bookmark query returns only real bookmarks
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ var query = histsvc.getNewQuery();
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+
+ root.containerOpen = true;
+ var cc = root.childCount;
+ do_check_eq(cc, 2);
+ var node1 = root.getChild(0);
+ do_check_eq(bmsvc.getFolderIdForItem(node1.itemId), bmsvc.bookmarksMenuFolder);
+ var node2 = root.getChild(1);
+ do_check_eq(bmsvc.getFolderIdForItem(node2.itemId), bmsvc.toolbarFolder);
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_405497.js b/toolkit/components/places/tests/unit/test_405497.js
new file mode 100644
index 0000000000..951302b849
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_405497.js
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+/**
+ * The callback object for runInBatchMode.
+ *
+ * @param aService
+ * Takes a reference to the history service or the bookmark service.
+ * This determines which service should be called when calling the second
+ * runInBatchMode the second time.
+ */
+function callback(aService)
+{
+ this.callCount = 0;
+ this.service = aService;
+}
+callback.prototype = {
+ // nsINavHistoryBatchCallback
+
+ runBatched: function(aUserData)
+ {
+ this.callCount++;
+
+ if (this.callCount == 1) {
+ // We want to call run in batched once more.
+ this.service.runInBatchMode(this, null);
+ return;
+ }
+
+ do_check_eq(this.callCount, 2);
+ do_test_finished();
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryBatchCallback])
+};
+
+function run_test() {
+ // checking the history service
+ do_test_pending();
+ hs.runInBatchMode(new callback(hs), null);
+
+ // checking the bookmark service
+ do_test_pending();
+ bs.runInBatchMode(new callback(bs), null);
+}
diff --git a/toolkit/components/places/tests/unit/test_408221.js b/toolkit/components/places/tests/unit/test_408221.js
new file mode 100644
index 0000000000..2b41ce1a24
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_408221.js
@@ -0,0 +1,165 @@
+/* -*- 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 current_test = 0;
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+function ensure_tag_results(uris, searchTerm)
+{
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, uris.length);
+ let vals = [];
+ for (let i=0; i<controller.matchCount; i++) {
+ // Keep the URL for later because order of tag results is undefined
+ vals.push(controller.getValueAt(i));
+ do_check_eq(controller.getStyleAt(i), "bookmark-tag");
+ }
+ // Sort the results then check if we have the right items
+ vals.sort().forEach((val, i) => do_check_eq(val, uris[i].spec))
+
+ if (current_test < (tests.length - 1)) {
+ current_test++;
+ tests[current_test]();
+ }
+
+ do_test_finished();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+var uri1 = uri("http://site.tld/1");
+var uri2 = uri("http://site.tld/2");
+var uri3 = uri("http://site.tld/3");
+var uri4 = uri("http://site.tld/4");
+var uri5 = uri("http://site.tld/5");
+var uri6 = uri("http://site.tld/6");
+
+var tests = [function() { ensure_tag_results([uri1, uri2, uri3], "foo"); },
+ function() { ensure_tag_results([uri1, uri2, uri3], "Foo"); },
+ function() { ensure_tag_results([uri1, uri2, uri3], "foO"); },
+ function() { ensure_tag_results([uri4, uri5, uri6], "bar mud"); },
+ function() { ensure_tag_results([uri4, uri5, uri6], "BAR MUD"); },
+ function() { ensure_tag_results([uri4, uri5, uri6], "Bar Mud"); }];
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param aURI
+ * The nsIURI to tag.
+ * @param aTags
+ * The tags to add.
+ */
+function tagURI(aURI, aTags) {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ tagssvc.tagURI(aURI, aTags);
+}
+
+/**
+ * Test bug #408221
+ */
+function run_test() {
+ // always search in history + bookmarks, no matter what the default is
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("browser.urlbar.search.sources", 3);
+ prefs.setIntPref("browser.urlbar.default.behavior", 0);
+
+ tagURI(uri1, ["Foo"]);
+ tagURI(uri2, ["FOO"]);
+ tagURI(uri3, ["foO"]);
+ tagURI(uri4, ["BAR"]);
+ tagURI(uri4, ["MUD"]);
+ tagURI(uri5, ["bar"]);
+ tagURI(uri5, ["mud"]);
+ tagURI(uri6, ["baR"]);
+ tagURI(uri6, ["muD"]);
+
+ tests[0]();
+}
diff --git a/toolkit/components/places/tests/unit/test_412132.js b/toolkit/components/places/tests/unit/test_412132.js
new file mode 100644
index 0000000000..827391f189
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_412132.js
@@ -0,0 +1,136 @@
+/* -*- 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/. */
+
+/*
+ * TEST DESCRIPTION:
+ *
+ * Tests patch to Bug 412132:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=412132
+ */
+
+add_task(function* changeuri_unvisited_bookmark()
+{
+ do_print("After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should be zero if original URI is unvisited and " +
+ "no longer bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2"));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Unvisited URI no longer bookmarked => frecency should = 0");
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* changeuri_visited_bookmark()
+{
+ do_print("After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should not be zero if original URI is visited.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2"));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("*Visited* URI no longer bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* changeuri_bookmark_still_bookmarked()
+{
+ do_print("After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should not be zero if original URI is still " +
+ "bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark 1 title");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark 2 title");
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, uri("http://example.com/2"));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI still bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* changeuri_nonexistent_bookmark()
+{
+ do_print("Changing the URI of a nonexistent bookmark should fail.");
+ function tryChange(itemId)
+ {
+ try {
+ PlacesUtils.bookmarks.changeBookmarkURI(itemId + 1, uri("http://example.com/2"));
+ do_throw("Nonexistent bookmark should throw.");
+ }
+ catch (ex) {}
+ }
+
+ // First try a straight-up bogus item ID, one greater than the current max
+ // ID.
+ let stmt = DBConn().createStatement("SELECT MAX(id) FROM moz_bookmarks");
+ stmt.executeStep();
+ let maxId = stmt.getInt32(0);
+ stmt.finalize();
+ tryChange(maxId + 1);
+
+ // Now add a bookmark, delete it, and check.
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://example.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ PlacesUtils.bookmarks.removeItem(id);
+ tryChange(id);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_413784.js b/toolkit/components/places/tests/unit/test_413784.js
new file mode 100644
index 0000000000..6df4dfbbbb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_413784.js
@@ -0,0 +1,118 @@
+/* -*- 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/. */
+
+/*
+
+Test autocomplete for non-English URLs
+
+- add a visit for a page with a non-English URL
+- search
+- test number of matches (should be exactly one)
+
+*/
+
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+// create test data
+var searchTerm = "ユニコード";
+var decoded = "http://www.foobar.com/" + searchTerm + "/";
+var url = uri(decoded);
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+function run_test()
+{
+ do_test_pending();
+ PlacesTestUtils.addVisits(url).then(continue_test);
+}
+
+function continue_test()
+{
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+
+ // test that we found the entry we added
+ do_check_eq(controller.matchCount, 1);
+
+ // Make sure the url is the same according to spec, so it can be deleted
+ do_check_eq(controller.getValueAt(0), url.spec);
+
+ do_test_finished();
+ };
+
+ controller.startSearch(searchTerm);
+}
diff --git a/toolkit/components/places/tests/unit/test_415460.js b/toolkit/components/places/tests/unit/test_415460.js
new file mode 100644
index 0000000000..f2e049f099
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_415460.js
@@ -0,0 +1,43 @@
+/* -*- 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 hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+/**
+ * Checks to see that a search has exactly one result in the database.
+ *
+ * @param aTerms
+ * The terms to search for.
+ * @returns true if the search returns one result, false otherwise.
+ */
+function search_has_result(aTerms)
+{
+ var options = hs.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI;
+ var query = hs.getNewQuery();
+ query.searchTerms = aTerms;
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ root.containerOpen = false;
+ return (cc == 1);
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ const SEARCH_TERM = "ユニコード";
+ const TEST_URL = "http://example.com/" + SEARCH_TERM + "/";
+ yield PlacesTestUtils.addVisits(uri(TEST_URL));
+ do_check_true(search_has_result(SEARCH_TERM));
+});
diff --git a/toolkit/components/places/tests/unit/test_415757.js b/toolkit/components/places/tests/unit/test_415757.js
new file mode 100644
index 0000000000..afd396183a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_415757.js
@@ -0,0 +1,102 @@
+/* -*- 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/. */
+
+/**
+ * Checks to see that a URI is in the database.
+ *
+ * @param aURI
+ * The URI to check.
+ * @returns true if the URI is in the DB, false otherwise.
+ */
+function uri_in_db(aURI) {
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI
+ var query = PlacesUtils.history.getNewQuery();
+ query.uri = aURI;
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ root.containerOpen = false;
+ return (cc == 1);
+}
+
+const TOTAL_SITES = 20;
+
+// main
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // add pages to global history
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test-" + i + ".com/";
+ let testURI = uri(site);
+ let when = Date.now() * 1000 + (i * TOTAL_SITES);
+ yield PlacesTestUtils.addVisits({ uri: testURI, visitDate: when });
+ }
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test.com/" + i + "/";
+ let testURI = uri(site);
+ let when = Date.now() * 1000 + (i * TOTAL_SITES);
+ yield PlacesTestUtils.addVisits({ uri: testURI, visitDate: when });
+ }
+
+ // set a page annotation on one of the urls that will be removed
+ var testAnnoDeletedURI = uri("http://www.test.com/1/");
+ var testAnnoDeletedName = "foo";
+ var testAnnoDeletedValue = "bar";
+ PlacesUtils.annotations.setPageAnnotation(testAnnoDeletedURI,
+ testAnnoDeletedName,
+ testAnnoDeletedValue, 0,
+ PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+
+ // set a page annotation on one of the urls that will NOT be removed
+ var testAnnoRetainedURI = uri("http://www.test-1.com/");
+ var testAnnoRetainedName = "foo";
+ var testAnnoRetainedValue = "bar";
+ PlacesUtils.annotations.setPageAnnotation(testAnnoRetainedURI,
+ testAnnoRetainedName,
+ testAnnoRetainedValue, 0,
+ PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+
+ // remove pages from www.test.com
+ PlacesUtils.history.removePagesFromHost("www.test.com", false);
+
+ // check that all pages in www.test.com have been removed
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test.com/" + i + "/";
+ let testURI = uri(site);
+ do_check_false(uri_in_db(testURI));
+ }
+
+ // check that all pages in www.test-X.com have NOT been removed
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test-" + i + ".com/";
+ let testURI = uri(site);
+ do_check_true(uri_in_db(testURI));
+ }
+
+ // check that annotation on the removed item does not exists
+ try {
+ PlacesUtils.annotations.getPageAnnotation(testAnnoDeletedURI, testAnnoName);
+ do_throw("fetching page-annotation that doesn't exist, should've thrown");
+ } catch (ex) {}
+
+ // check that annotation on the NOT removed item still exists
+ try {
+ var annoVal = PlacesUtils.annotations.getPageAnnotation(testAnnoRetainedURI,
+ testAnnoRetainedName);
+ } catch (ex) {
+ do_throw("The annotation has been removed erroneously");
+ }
+ do_check_eq(annoVal, testAnnoRetainedValue);
+
+});
diff --git a/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js b/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js
new file mode 100644
index 0000000000..2eed029214
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js
@@ -0,0 +1,143 @@
+/* -*- 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/. */
+
+// Get services.
+try {
+ var histSvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmSvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var annoSvc = Cc["@mozilla.org/browser/annotation-service;1"]
+ .getService(Ci.nsIAnnotationService);
+} catch (ex) {
+ do_throw("Could not get services\n");
+}
+
+var validAnnoName = "validAnno";
+var validItemName = "validItem";
+var deletedAnnoName = "deletedAnno";
+var deletedItemName = "deletedItem";
+var bookmarkedURI = uri("http://www.mozilla.org/");
+// set lastModified to the past to prevent VM timing bugs
+var pastDate = Date.now() * 1000 - 1;
+var deletedBookmarkIds = [];
+
+// bookmarks observer
+var observer = {
+ // cached ordered array of notified items
+ _onItemRemovedItemIds: [],
+ onItemRemoved: function(aItemId, aParentId, aIndex) {
+ // We should first get notifications for children, then for their parent
+ do_check_eq(this._onItemRemovedItemIds.indexOf(aParentId), -1);
+ // Ensure we are not wrongly removing 1 level up
+ do_check_neq(aParentId, bmSvc.toolbarFolder);
+ // Removed item must be one of those we have manually deleted
+ do_check_neq(deletedBookmarkIds.indexOf(aItemId), -1);
+ this._onItemRemovedItemIds.push(aItemId);
+ },
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsINavBookmarkObserver) ||
+ aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+bmSvc.addObserver(observer, false);
+
+function add_bookmarks() {
+ // This is the folder we will cleanup
+ var validFolderId = bmSvc.createFolder(bmSvc.toolbarFolder,
+ validItemName,
+ bmSvc.DEFAULT_INDEX);
+ annoSvc.setItemAnnotation(validFolderId, validAnnoName,
+ "annotation", 0,
+ annoSvc.EXPIRE_NEVER);
+ bmSvc.setItemLastModified(validFolderId, pastDate);
+
+ // This bookmark should not be deleted
+ var validItemId = bmSvc.insertBookmark(bmSvc.toolbarFolder,
+ bookmarkedURI,
+ bmSvc.DEFAULT_INDEX,
+ validItemName);
+ annoSvc.setItemAnnotation(validItemId, validAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+
+ // The following contents should be deleted
+ var deletedItemId = bmSvc.insertBookmark(validFolderId,
+ bookmarkedURI,
+ bmSvc.DEFAULT_INDEX,
+ deletedItemName);
+ annoSvc.setItemAnnotation(deletedItemId, deletedAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+ deletedBookmarkIds.push(deletedItemId);
+
+ var internalFolderId = bmSvc.createFolder(validFolderId,
+ deletedItemName,
+ bmSvc.DEFAULT_INDEX);
+ annoSvc.setItemAnnotation(internalFolderId, deletedAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+ deletedBookmarkIds.push(internalFolderId);
+
+ deletedItemId = bmSvc.insertBookmark(internalFolderId,
+ bookmarkedURI,
+ bmSvc.DEFAULT_INDEX,
+ deletedItemName);
+ annoSvc.setItemAnnotation(deletedItemId, deletedAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+ deletedBookmarkIds.push(deletedItemId);
+
+ return validFolderId;
+}
+
+function check_bookmarks(aFolderId) {
+ // check that we still have valid bookmarks
+ var bookmarks = bmSvc.getBookmarkIdsForURI(bookmarkedURI);
+ for (var i = 0; i < bookmarks.length; i++) {
+ do_check_eq(bmSvc.getItemTitle(bookmarks[i]), validItemName);
+ do_check_true(annoSvc.itemHasAnnotation(bookmarks[i], validAnnoName));
+ }
+
+ // check that folder exists and has still its annotation
+ do_check_eq(bmSvc.getItemTitle(aFolderId), validItemName);
+ do_check_true(annoSvc.itemHasAnnotation(aFolderId, validAnnoName));
+
+ // check that folder is empty
+ var options = histSvc.getNewQueryOptions();
+ var query = histSvc.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ var result = histSvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ // test that lastModified got updated
+ do_check_true(pastDate < bmSvc.getItemLastModified(aFolderId));
+
+ // test that all children have been deleted, we use annos for that
+ var deletedItems = annoSvc.getItemsWithAnnotation(deletedAnnoName);
+ do_check_eq(deletedItems.length, 0);
+
+ // test that observer has been called for (and only for) deleted items
+ do_check_eq(observer._onItemRemovedItemIds.length, deletedBookmarkIds.length);
+
+ // Sanity check: all roots should be intact
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.placesRoot), 0);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.bookmarksMenuFolder), bmSvc.placesRoot);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.tagsFolder), bmSvc.placesRoot);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.unfiledBookmarksFolder), bmSvc.placesRoot);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.toolbarFolder), bmSvc.placesRoot);
+}
+
+// main
+function run_test() {
+ var folderId = add_bookmarks();
+ bmSvc.removeFolderChildren(folderId);
+ check_bookmarks(folderId);
+}
diff --git a/toolkit/components/places/tests/unit/test_419731.js b/toolkit/components/places/tests/unit/test_419731.js
new file mode 100644
index 0000000000..b1a434e12a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_419731.js
@@ -0,0 +1,96 @@
+/* -*- 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/. */
+
+function run_test() {
+ let uri1 = NetUtil.newURI("http://foo.bar/");
+
+ // create 2 bookmarks
+ let bookmark1id = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "title 1");
+ let bookmark2id = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.toolbarFolderId,
+ uri1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "title 2");
+ // add a new tag
+ PlacesUtils.tagging.tagURI(uri1, ["foo"]);
+
+ // get tag folder id
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.tagsFolderId], 1);
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let tagRoot = result.root;
+ tagRoot.containerOpen = true;
+ let tagNode = tagRoot.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ let tagItemId = tagNode.itemId;
+ tagRoot.containerOpen = false;
+
+ // change bookmark 1 title
+ PlacesUtils.bookmarks.setItemTitle(bookmark1id, "new title 1");
+
+ // Workaround timers resolution and time skews.
+ let bookmark2LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark2id);
+ PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1000);
+
+ // Query the tag.
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = options.RESULTS_AS_TAG_QUERY;
+
+ query = PlacesUtils.history.getNewQuery();
+ result = PlacesUtils.history.executeQuery(query, options);
+ let root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+
+ let theTag = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ // Bug 524219: Check that renaming the tag shows up in the result.
+ do_check_eq(theTag.title, "foo")
+ PlacesUtils.bookmarks.setItemTitle(tagItemId, "bar");
+
+ // Check that the item has been replaced
+ do_check_neq(theTag, root.getChild(0));
+ theTag = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(theTag.title, "bar");
+
+ // Check that tag container contains new title
+ theTag.containerOpen = true;
+ do_check_eq(theTag.childCount, 1);
+ let node = theTag.getChild(0);
+ do_check_eq(node.title, "new title 1");
+ theTag.containerOpen = false;
+ root.containerOpen = false;
+
+ // Change bookmark 2 title.
+ PlacesUtils.bookmarks.setItemTitle(bookmark2id, "new title 2");
+
+ // Workaround timers resolution and time skews.
+ let bookmark1LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark1id);
+ PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1000);
+
+ // Check that tag container contains new title
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = options.RESULTS_AS_TAG_CONTENTS;
+
+ query = PlacesUtils.history.getNewQuery();
+ query.setFolders([tagItemId], 1);
+ result = PlacesUtils.history.executeQuery(query, options);
+ root = result.root;
+
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ node = root.getChild(0);
+ do_check_eq(node.title, "new title 2");
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_419792_node_tags_property.js b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js
new file mode 100644
index 0000000000..4c726d6672
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js
@@ -0,0 +1,49 @@
+/* -*- 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/. */
+
+// get services
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+
+function run_test() {
+ // get toolbar node
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.toolbarFolder], 1);
+ var result = histsvc.executeQuery(query, options);
+ var toolbarNode = result.root;
+ toolbarNode.containerOpen = true;
+
+ // add a bookmark
+ var bookmarkURI = uri("http://foo.com");
+ var bookmarkId = bmsvc.insertBookmark(bmsvc.toolbarFolder, bookmarkURI,
+ bmsvc.DEFAULT_INDEX, "");
+
+ // get the node for the new bookmark
+ var node = toolbarNode.getChild(toolbarNode.childCount-1);
+ do_check_eq(node.itemId, bookmarkId);
+
+ // confirm there's no tags via the .tags property
+ do_check_eq(node.tags, null);
+
+ // add a tag
+ tagssvc.tagURI(bookmarkURI, ["foo"]);
+ do_check_eq(node.tags, "foo");
+
+ // add another tag, to test delimiter and sorting
+ tagssvc.tagURI(bookmarkURI, ["bar"]);
+ do_check_eq(node.tags, "bar, foo");
+
+ // remove the tags, confirming the property is cleared
+ tagssvc.untagURI(bookmarkURI, null);
+ do_check_eq(node.tags, null);
+
+ toolbarNode.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_425563.js b/toolkit/components/places/tests/unit/test_425563.js
new file mode 100644
index 0000000000..bee3a4a546
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_425563.js
@@ -0,0 +1,74 @@
+/* -*- 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/. */
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ let count_visited_URIs = ["http://www.test-link.com/",
+ "http://www.test-typed.com/",
+ "http://www.test-bookmark.com/",
+ "http://www.test-redirect-permanent.com/",
+ "http://www.test-redirect-temporary.com/"];
+
+ let notcount_visited_URIs = ["http://www.test-embed.com/",
+ "http://www.test-download.com/",
+ "http://www.test-framed.com/",
+ "http://www.test-reload.com/"];
+
+ // add visits, one for each transition type
+ yield PlacesTestUtils.addVisits([
+ { uri: uri("http://www.test-link.com/"),
+ transition: TRANSITION_LINK },
+ { uri: uri("http://www.test-typed.com/"),
+ transition: TRANSITION_TYPED },
+ { uri: uri("http://www.test-bookmark.com/"),
+ transition: TRANSITION_BOOKMARK },
+ { uri: uri("http://www.test-embed.com/"),
+ transition: TRANSITION_EMBED },
+ { uri: uri("http://www.test-framed.com/"),
+ transition: TRANSITION_FRAMED_LINK },
+ { uri: uri("http://www.test-redirect-permanent.com/"),
+ transition: TRANSITION_REDIRECT_PERMANENT },
+ { uri: uri("http://www.test-redirect-temporary.com/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY },
+ { uri: uri("http://www.test-download.com/"),
+ transition: TRANSITION_DOWNLOAD },
+ { uri: uri("http://www.test-reload.com/"),
+ transition: TRANSITION_RELOAD },
+ ]);
+
+ // check that all links are marked as visited
+ for (let visited_uri of count_visited_URIs) {
+ do_check_true(yield promiseIsURIVisited(uri(visited_uri)));
+ }
+ for (let visited_uri of notcount_visited_URIs) {
+ do_check_true(yield promiseIsURIVisited(uri(visited_uri)));
+ }
+
+ // check that visit_count does not take in count embed and downloads
+ // maxVisits query are directly binded to visit_count
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ options.includeHidden = true;
+ let query = PlacesUtils.history.getNewQuery();
+ query.minVisits = 1;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+
+ root.containerOpen = true;
+ let cc = root.childCount;
+ do_check_eq(cc, count_visited_URIs.length);
+
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ do_check_neq(count_visited_URIs.indexOf(node.uri), -1);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js
new file mode 100644
index 0000000000..e0b6be64cd
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+/*
+
+- add a folder
+- add a folder-shortcut to the new folder
+- query for the shortcut
+- remove the folder-shortcut
+- confirm the shortcut is removed from the query results
+
+*/
+
+function run_test() {
+ const IDX = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ var folderId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, "", IDX);
+
+ var queryId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+ uri("place:folder=" + folderId), IDX, "");
+
+ var root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId, false, true).root;
+
+ var oldCount = root.childCount;
+
+ PlacesUtils.bookmarks.removeItem(queryId);
+
+ do_check_eq(root.childCount, oldCount-1);
+
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_433317_query_title_update.js b/toolkit/components/places/tests/unit/test_433317_query_title_update.js
new file mode 100644
index 0000000000..52558e844b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_433317_query_title_update.js
@@ -0,0 +1,38 @@
+/* -*- 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/. */
+
+function run_test() {
+ try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ } catch (ex) {
+ do_throw("Unable to initialize Places services");
+ }
+
+ // create a query bookmark
+ var queryId = bmsvc.insertBookmark(bmsvc.toolbarFolder, uri("place:"),
+ 0 /* first item */, "test query");
+
+ // query for that query
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.toolbarFolder], 1);
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var queryNode = root.getChild(0);
+ do_check_eq(queryNode.title, "test query");
+
+ // change the title
+ bmsvc.setItemTitle(queryId, "foo");
+
+ // confirm the node was updated
+ do_check_eq(queryNode.title, "foo");
+
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js
new file mode 100644
index 0000000000..92dac0b17d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js
@@ -0,0 +1,56 @@
+/* -*- 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/. */
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ } catch (ex) {
+ do_throw("Unable to initialize Places services");
+ }
+
+ // add a visit
+ var testURI = uri("http://test");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ // query for the visit
+ var options = histsvc.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI;
+ var query = histsvc.getNewQuery();
+ query.uri = testURI;
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+
+ // check hasChildren while the container is closed
+ do_check_eq(root.hasChildren, true);
+
+ // now check via the saved search path
+ var queryURI = histsvc.queriesToQueryString([query], 1, options);
+ bmsvc.insertBookmark(bmsvc.toolbarFolder, uri(queryURI),
+ 0 /* first item */, "test query");
+
+ // query for that query
+ options = histsvc.getNewQueryOptions();
+ query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.toolbarFolder], 1);
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ var queryNode = root.getChild(0);
+ do_check_eq(queryNode.title, "test query");
+ queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(queryNode.hasChildren, true);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_452777.js b/toolkit/components/places/tests/unit/test_452777.js
new file mode 100644
index 0000000000..97b2852f6d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_452777.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=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/. */
+
+/**
+ * This test ensures that when removing a folder within a transaction, undoing
+ * the transaction restores it with the same id (as received by the observers).
+ */
+
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+function run_test()
+{
+ const TITLE = "test folder";
+
+ // Create two test folders; remove the first one. This ensures that undoing
+ // the removal will not get the same id by chance (the insert id's can be
+ // reused in SQLite).
+ let id = bs.createFolder(bs.placesRoot, TITLE, -1);
+ bs.createFolder(bs.placesRoot, "test folder 2", -1);
+ let transaction = bs.getRemoveFolderTransaction(id);
+ transaction.doTransaction();
+
+ // Now check to make sure it gets added with the right id
+ bs.addObserver({
+ onItemAdded: function(aItemId, aFolder, aIndex, aItemType, aURI, aTitle)
+ {
+ do_check_eq(aItemId, id);
+ do_check_eq(aTitle, TITLE);
+ }
+ }, false);
+ transaction.undoTransaction();
+}
diff --git a/toolkit/components/places/tests/unit/test_454977.js b/toolkit/components/places/tests/unit/test_454977.js
new file mode 100644
index 0000000000..606e830488
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_454977.js
@@ -0,0 +1,124 @@
+/* -*- 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/. */
+
+// Cache actual visit_count value, filled by add_visit, used by check_results
+var visit_count = 0;
+
+// Returns the Place ID corresponding to an added visit.
+function* task_add_visit(aURI, aVisitType)
+{
+ // Add the visit asynchronously, and save its visit ID.
+ let deferUpdatePlaces = new Promise((resolve, reject) =>
+ {
+ PlacesUtils.asyncHistory.updatePlaces({
+ uri: aURI,
+ visits: [{ transitionType: aVisitType, visitDate: Date.now() * 1000 }]
+ }, {
+ handleError: function TAV_handleError() {
+ reject(new Error("Unexpected error in adding visit."));
+ },
+ handleResult: function (aPlaceInfo) {
+ this.visitId = aPlaceInfo.visits[0].visitId;
+ },
+ handleCompletion: function TAV_handleCompletion() {
+ resolve(this.visitId);
+ }
+ });
+ });
+
+ let visitId = yield deferUpdatePlaces;
+
+ // Increase visit_count if applicable
+ if (aVisitType != 0 &&
+ aVisitType != TRANSITION_EMBED &&
+ aVisitType != TRANSITION_FRAMED_LINK &&
+ aVisitType != TRANSITION_DOWNLOAD &&
+ aVisitType != TRANSITION_RELOAD) {
+ visit_count ++;
+ }
+
+ // Get the place id
+ if (visitId > 0) {
+ let sql = "SELECT place_id FROM moz_historyvisits WHERE id = ?1";
+ let stmt = DBConn().createStatement(sql);
+ stmt.bindByIndex(0, visitId);
+ do_check_true(stmt.executeStep());
+ let placeId = stmt.getInt64(0);
+ stmt.finalize();
+ do_check_true(placeId > 0);
+ return placeId;
+ }
+ return 0;
+}
+
+/**
+ * Checks for results consistency, using visit_count as constraint
+ * @param aExpectedCount
+ * Number of history results we are expecting (excluded hidden ones)
+ * @param aExpectedCountWithHidden
+ * Number of history results we are expecting (included hidden ones)
+ */
+function check_results(aExpectedCount, aExpectedCountWithHidden)
+{
+ let query = PlacesUtils.history.getNewQuery();
+ // used to check visit_count
+ query.minVisits = visit_count;
+ query.maxVisits = visit_count;
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ // Children without hidden ones
+ do_check_eq(root.childCount, aExpectedCount);
+ root.containerOpen = false;
+
+ // Execute again with includeHidden = true
+ // This will ensure visit_count is correct
+ options.includeHidden = true;
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ // Children with hidden ones
+ do_check_eq(root.childCount, aExpectedCountWithHidden);
+ root.containerOpen = false;
+}
+
+// main
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ const TEST_URI = uri("http://test.mozilla.org/");
+
+ // Add a visit that force hidden
+ yield task_add_visit(TEST_URI, TRANSITION_EMBED);
+ check_results(0, 0);
+
+ let placeId = yield task_add_visit(TEST_URI, TRANSITION_FRAMED_LINK);
+ check_results(0, 1);
+
+ // Add a visit that force unhide and check the place id.
+ // - We expect that the place gets hidden = 0 while retaining the same
+ // place id and a correct visit_count.
+ do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_TYPED)), placeId);
+ check_results(1, 1);
+
+ // Add a visit that should not increase visit_count
+ do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_RELOAD)), placeId);
+ check_results(1, 1);
+
+ // Add a visit that should not increase visit_count
+ do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_DOWNLOAD)), placeId);
+ check_results(1, 1);
+
+ // Add a visit, check that hidden is not overwritten
+ // - We expect that the place has still hidden = 0, while retaining
+ // correct visit_count.
+ yield task_add_visit(TEST_URI, TRANSITION_EMBED);
+ check_results(1, 1);
+});
diff --git a/toolkit/components/places/tests/unit/test_463863.js b/toolkit/components/places/tests/unit/test_463863.js
new file mode 100644
index 0000000000..2f7cece4ac
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_463863.js
@@ -0,0 +1,60 @@
+/* -*- 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/. */
+
+/*
+ * TEST DESCRIPTION:
+ *
+ * This test checks that in a basic history query all transition types visits
+ * appear but TRANSITION_EMBED and TRANSITION_FRAMED_LINK ones.
+ */
+
+var transitions = [
+ TRANSITION_LINK
+, TRANSITION_TYPED
+, TRANSITION_BOOKMARK
+, TRANSITION_EMBED
+, TRANSITION_FRAMED_LINK
+, TRANSITION_REDIRECT_PERMANENT
+, TRANSITION_REDIRECT_TEMPORARY
+, TRANSITION_DOWNLOAD
+];
+
+function runQuery(aResultType) {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = aResultType;
+ let root = PlacesUtils.history.executeQuery(PlacesUtils.history.getNewQuery(),
+ options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ do_check_eq(cc, transitions.length - 2);
+
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ // Check that all transition types but EMBED and FRAMED appear in results
+ do_check_neq(node.uri.substr(6, 1), TRANSITION_EMBED);
+ do_check_neq(node.uri.substr(6, 1), TRANSITION_FRAMED_LINK);
+ }
+ root.containerOpen = false;
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // add visits, one for each transition type
+ for (let transition of transitions) {
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://" + transition + ".mozilla.org/"),
+ transition: transition
+ });
+ }
+
+ runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT);
+ runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_URI);
+});
diff --git a/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js
new file mode 100644
index 0000000000..873174ffd0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js
@@ -0,0 +1,21 @@
+/* -*- 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 hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+function run_test() {
+ var query = hs.getNewQuery();
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULT_TYPE_QUERY;
+ var result = hs.executeQuery(query, options);
+ result.root.containerOpen = true;
+ var rootNode = result.root;
+ rootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
+ var queries = rootNode.getQueries();
+ do_check_eq(queries[0].uri, null); // Should be null, instead of crashing the browser
+ rootNode.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
new file mode 100644
index 0000000000..05f3f83e7f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
@@ -0,0 +1,129 @@
+/* -*- 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/. */
+
+/*
+ * TEST DESCRIPTION:
+ *
+ * This test checks that setting a sort on a RESULTS_AS_DATE_QUERY query,
+ * children of inside containers are sorted accordingly.
+ */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+// Will be inserted in this order, so last one will be the newest visit.
+var pages = [
+ "http://a.mozilla.org/1/",
+ "http://a.mozilla.org/2/",
+ "http://a.mozilla.org/3/",
+ "http://a.mozilla.org/4/",
+ "http://b.mozilla.org/5/",
+ "http://b.mozilla.org/6/",
+ "http://b.mozilla.org/7/",
+ "http://b.mozilla.org/8/",
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_initialize()
+{
+ var noon = new Date();
+ noon.setHours(12);
+
+ // Add visits.
+ for (let pageIndex = 0; pageIndex < pages.length; ++pageIndex) {
+ let page = pages[pageIndex];
+ yield PlacesTestUtils.addVisits({
+ uri: uri(page),
+ visitDate: noon - (pages.length - pageIndex) * 1000
+ });
+ }
+});
+
+/**
+ * Tests that sorting date query by none will sort by title asc.
+ */
+add_task(function() {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_QUERY;
+ // This should sort by title asc.
+ options.sortingMode = options.SORT_BY_NONE;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var dayContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayContainer.containerOpen = true;
+
+ var cc = dayContainer.childCount;
+ do_check_eq(cc, pages.length);
+ for (var i = 0; i < cc; i++) {
+ var node = dayContainer.getChild(i);
+ do_check_eq(pages[i], node.uri);
+ }
+
+ dayContainer.containerOpen = false;
+ root.containerOpen = false;
+});
+
+/**
+ * Tests that sorting date query by date will sort accordingly.
+ */
+add_task(function() {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_QUERY;
+ // This should sort by title asc.
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var dayContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayContainer.containerOpen = true;
+
+ var cc = dayContainer.childCount;
+ do_check_eq(cc, pages.length);
+ for (var i = 0; i < cc; i++) {
+ var node = dayContainer.getChild(i);
+ do_check_eq(pages[pages.length - i - 1], node.uri);
+ }
+
+ dayContainer.containerOpen = false;
+ root.containerOpen = false;
+});
+
+/**
+ * Tests that sorting date site query by date will still sort by title asc.
+ */
+add_task(function() {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ // This should sort by title asc.
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var dayContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayContainer.containerOpen = true;
+ var siteContainer = dayContainer.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(siteContainer.title, "a.mozilla.org");
+ siteContainer.containerOpen = true;
+
+ var cc = siteContainer.childCount;
+ do_check_eq(cc, pages.length / 2);
+ for (var i = 0; i < cc / 2; i++) {
+ var node = siteContainer.getChild(i);
+ do_check_eq(pages[i], node.uri);
+ }
+
+ siteContainer.containerOpen = false;
+ dayContainer.containerOpen = false;
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_536081.js b/toolkit/components/places/tests/unit/test_536081.js
new file mode 100644
index 0000000000..b61b918666
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_536081.js
@@ -0,0 +1,56 @@
+/* -*- 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 hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bh = hs.QueryInterface(Ci.nsIBrowserHistory);
+var db = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+
+const URLS = [
+ { u: "http://www.google.com/search?q=testing%3Bthis&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:unofficial&client=firefox-a",
+ s: "goog" },
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ for (let url of URLS) {
+ yield task_test_url(url);
+ }
+});
+
+function* task_test_url(aURL) {
+ print("Testing url: " + aURL.u);
+ yield PlacesTestUtils.addVisits(uri(aURL.u));
+ let query = hs.getNewQuery();
+ query.searchTerms = aURL.s;
+ let options = hs.getNewQueryOptions();
+ let root = hs.executeQuery(query, options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ do_check_eq(cc, 1);
+ print("Checking url is in the query.");
+ let node = root.getChild(0);
+ print("Found " + node.uri);
+ root.containerOpen = false;
+ bh.removePage(uri(node.uri));
+}
+
+function check_empty_table(table_name) {
+ print("Checking url has been removed.");
+ let stmt = db.createStatement("SELECT count(*) FROM " + table_name);
+ try {
+ stmt.executeStep();
+ do_check_eq(stmt.getInt32(0), 0);
+ }
+ finally {
+ stmt.finalize();
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js
new file mode 100644
index 0000000000..1280ce3e79
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import("resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
+
+function run_test() {
+ // Tell the search service we are running in the US. This also has the
+ // desired side-effect of preventing our geoip lookup.
+ Services.prefs.setBoolPref("browser.search.isUS", true);
+ Services.prefs.setCharPref("browser.search.countryCode", "US");
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+ run_next_test();
+}
+
+add_task(function* search_engine_match() {
+ let engine = yield promiseDefaultSearchEngine();
+ let token = engine.getResultDomain();
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1));
+ do_check_eq(match.url, engine.searchForm);
+ do_check_eq(match.engineName, engine.name);
+ do_check_eq(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null);
+});
+
+add_task(function* no_match() {
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("test"));
+});
+
+add_task(function* hide_search_engine_nomatch() {
+ let engine = yield promiseDefaultSearchEngine();
+ let token = engine.getResultDomain();
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ Services.search.removeEngine(engine);
+ yield promiseTopic;
+ do_check_true(engine.hidden);
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1)));
+});
+
+add_task(function* add_search_engine_match() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
+ Services.search.addEngineWithDetails("bacon", "", "pork", "Search Bacon",
+ "GET", "http://www.bacon.moz/?search={searchTerms}");
+ yield promiseTopic;
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon");
+ do_check_eq(match.url, "http://www.bacon.moz");
+ do_check_eq(match.engineName, "bacon");
+ do_check_eq(match.iconUrl, null);
+});
+
+add_task(function* test_aliased_search_engine_match() {
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByAlias("sober"));
+ // Lower case
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias("pork");
+ do_check_eq(match.engineName, "bacon");
+ do_check_eq(match.alias, "pork");
+ do_check_eq(match.iconUrl, null);
+ // Upper case
+ let match1 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("PORK");
+ do_check_eq(match1.engineName, "bacon");
+ do_check_eq(match1.alias, "pork");
+ do_check_eq(match1.iconUrl, null);
+ // Cap case
+ let match2 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("Pork");
+ do_check_eq(match2.engineName, "bacon");
+ do_check_eq(match2.alias, "pork");
+ do_check_eq(match2.iconUrl, null);
+});
+
+add_task(function* test_aliased_search_engine_match_upper_case_alias() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("patch"));
+ Services.search.addEngineWithDetails("patch", "", "PR", "Search Patch",
+ "GET", "http://www.patch.moz/?search={searchTerms}");
+ yield promiseTopic;
+ // lower case
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias("pr");
+ do_check_eq(match.engineName, "patch");
+ do_check_eq(match.alias, "PR");
+ do_check_eq(match.iconUrl, null);
+ // Upper case
+ let match1 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("PR");
+ do_check_eq(match1.engineName, "patch");
+ do_check_eq(match1.alias, "PR");
+ do_check_eq(match1.iconUrl, null);
+ // Cap case
+ let match2 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("Pr");
+ do_check_eq(match2.engineName, "patch");
+ do_check_eq(match2.alias, "PR");
+ do_check_eq(match2.iconUrl, null);
+});
+
+add_task(function* remove_search_engine_nomatch() {
+ let engine = Services.search.getEngineByName("bacon");
+ let promiseTopic = promiseSearchTopic("engine-removed");
+ Services.search.removeEngine(engine);
+ yield promiseTopic;
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
+});
+
+add_task(function* test_parseSubmissionURL_basic() {
+ // Most of the logic of parseSubmissionURL is tested in the search service
+ // itself, thus we only do a sanity check of the wrapper here.
+ let engine = yield promiseDefaultSearchEngine();
+ let submissionURL = engine.getSubmission("terms").uri.spec;
+
+ let result = PlacesSearchAutocompleteProvider.parseSubmissionURL(submissionURL);
+ do_check_eq(result.engineName, engine.name);
+ do_check_eq(result.terms, "terms");
+
+ result = PlacesSearchAutocompleteProvider.parseSubmissionURL("http://example.org/");
+ do_check_eq(result, null);
+});
+
+function promiseDefaultSearchEngine() {
+ let deferred = Promise.defer();
+ Services.search.init( () => {
+ deferred.resolve(Services.search.defaultEngine);
+ });
+ return deferred.promise;
+}
+
+function promiseSearchTopic(expectedVerb) {
+ let deferred = Promise.defer();
+ Services.obs.addObserver( function observe(subject, topic, verb) {
+ do_print("browser-search-engine-modified: " + verb);
+ if (verb == expectedVerb) {
+ Services.obs.removeObserver(observe, "browser-search-engine-modified");
+ deferred.resolve();
+ }
+ }, "browser-search-engine-modified", false);
+ return deferred.promise;
+}
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js b/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js
new file mode 100644
index 0000000000..182f75eac8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js
@@ -0,0 +1,77 @@
+/**
+ * This file tests PlacesUtils.asyncGetBookmarkIds method.
+ */
+
+const TEST_URL = "http://www.example.com/";
+
+var promiseAsyncGetBookmarkIds = Task.async(function* (url) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ return new Promise(resolve => {
+ PlacesUtils.asyncGetBookmarkIds(url, (itemIds, uri) => {
+ Assert.equal(uri, url);
+ resolve({ itemIds, url });
+ });
+ });
+});
+
+add_task(function* test_no_bookmark() {
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.equal(itemIds.length, 0);
+ Assert.equal(url, TEST_URL);
+});
+
+add_task(function* test_one_bookmark() {
+ let bookmark = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "test"
+ });
+ let itemId = yield PlacesUtils.promiseItemId(bookmark.guid);
+ {
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(NetUtil.newURI(TEST_URL));
+ Assert.equal(itemIds.length, 1);
+ Assert.equal(itemIds[0], itemId);
+ Assert.equal(url.spec, TEST_URL);
+ }
+ {
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.equal(itemIds.length, 1);
+ Assert.equal(itemIds[0], itemId);
+ Assert.equal(url, TEST_URL);
+ }
+ yield PlacesUtils.bookmarks.remove(bookmark);
+});
+
+add_task(function* test_multiple_bookmarks() {
+ let ids = [];
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "test"
+ });
+ ids.push((yield PlacesUtils.promiseItemId(bookmark1.guid)));
+ let bookmark2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "test"
+ });
+ ids.push((yield PlacesUtils.promiseItemId(bookmark2.guid)));
+
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.deepEqual(ids, itemIds);
+ Assert.equal(url, TEST_URL);
+
+ yield PlacesUtils.bookmarks.remove(bookmark1);
+ yield PlacesUtils.bookmarks.remove(bookmark2);
+});
+
+add_task(function* test_cancel() {
+ let pending = PlacesUtils.asyncGetBookmarkIds(TEST_URL, () => {
+ Assert.ok(false, "A canceled pending statement should not be invoked");
+ });
+ pending.cancel();
+
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.equal(itemIds.length, 0);
+ Assert.equal(url, TEST_URL);
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js
new file mode 100644
index 0000000000..b7906ec5c0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js
@@ -0,0 +1,25 @@
+add_task(function* () {
+ do_print("Add a bookmark.");
+ let bm = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let id = yield PlacesUtils.promiseItemId(bm.guid);
+ Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid);
+
+ // Ensure invalidating a non-existent itemId doesn't throw.
+ PlacesUtils.invalidateCachedGuidFor(null);
+ PlacesUtils.invalidateCachedGuidFor(9999);
+
+ do_print("Change the GUID.");
+ yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) {
+ yield db.execute("UPDATE moz_bookmarks SET guid = :guid WHERE id = :id",
+ { guid: "123456789012", id});
+ }));
+ // The cache should still point to the wrong id.
+ Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid);
+
+ do_print("Invalidate the cache.");
+ PlacesUtils.invalidateCachedGuidFor(id);
+ Assert.equal((yield PlacesUtils.promiseItemGuid(id)), "123456789012");
+ Assert.equal((yield PlacesUtils.promiseItemId("123456789012")), id);
+ yield Assert.rejects(PlacesUtils.promiseItemId(bm.guid), /no item found for the given GUID/);
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js b/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js
new file mode 100644
index 0000000000..f0e9c5517a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ const TEST_URI = NetUtil.newURI("http://moz.org/")
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ]),
+
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemAdded: function (aItemId, aParentId, aIndex, aItemType, aURI) {
+ do_check_true(aURI.equals(TEST_URI));
+ PlacesUtils.removeLazyBookmarkObserver(this);
+ do_test_finished();
+ },
+ onItemRemoved: function () {},
+ onItemChanged: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ };
+
+ // Check registration and removal with uninitialized bookmarks service.
+ PlacesUtils.addLazyBookmarkObserver(observer);
+ PlacesUtils.removeLazyBookmarkObserver(observer);
+
+ // Add a proper lazy observer we will test.
+ PlacesUtils.addLazyBookmarkObserver(observer);
+
+ // Check that we don't leak when adding and removing an observer while the
+ // bookmarks service is instantiated but no change happened (bug 721319).
+ PlacesUtils.bookmarks;
+ PlacesUtils.addLazyBookmarkObserver(observer);
+ PlacesUtils.removeLazyBookmarkObserver(observer);
+ try {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ do_throw("Trying to remove a nonexisting observer should throw!");
+ } catch (ex) {}
+
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark title");
+}
diff --git a/toolkit/components/places/tests/unit/test_adaptive.js b/toolkit/components/places/tests/unit/test_adaptive.js
new file mode 100644
index 0000000000..78ffaedb51
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_adaptive.js
@@ -0,0 +1,406 @@
+/* -*- 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/. */
+
+/**
+ * Test for bug 395739 to make sure the feedback to the search results in those
+ * entries getting better ranks. Additionally, exact matches should be ranked
+ * higher. Because the interactions among adaptive rank and visit counts is not
+ * well defined, this test holds one of the two values constant when modifying
+ * the other.
+ *
+ * This also tests bug 395735 for the instrumentation feedback mechanism.
+ *
+ * Bug 411293 is tested to make sure the drop down strongly prefers previously
+ * typed pages that have been selected and are moved to the top with adaptive
+ * learning.
+ */
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ get minResultsForPopup() {
+ return 0;
+ },
+ get timeout() {
+ return 10;
+ },
+ get searchParam() {
+ return "";
+ },
+ get textValue() {
+ return "";
+ },
+ get disableAutoComplete() {
+ return false;
+ },
+ get completeDefaultIndex() {
+ return false;
+ },
+
+ get searchCount() {
+ return this.searches.length;
+ },
+ getSearchAt: function (aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function () {},
+ onSearchComplete: function() {},
+
+ get popupOpen() {
+ return false;
+ },
+ popup: {
+ set selectedIndex(aIndex) {},
+ invalidate: function () {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup])
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
+}
+
+/**
+ * Checks that autocomplete results are ordered correctly.
+ */
+function ensure_results(expected, searchTerm)
+{
+ 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(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ input.onSearchComplete = function() {
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, expected.length);
+ for (let i = 0; i < controller.matchCount; i++) {
+ print("Testing for '" + expected[i].uri.spec + "' got '" + controller.getValueAt(i) + "'");
+ do_check_eq(controller.getValueAt(i), expected[i].uri.spec);
+ do_check_eq(controller.getStyleAt(i), expected[i].style);
+ }
+
+ deferEnsureResults.resolve();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+/**
+ * Asynchronous task that bumps up the rank for an uri.
+ */
+function* task_setCountRank(aURI, aCount, aRank, aSearch, aBookmark)
+{
+ // Bump up the visit count for the uri.
+ let visits = [];
+ for (let i = 0; i < aCount; i++) {
+ visits.push({ uri: aURI, visitDate: d1, transition: TRANSITION_TYPED });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ // Make a nsIAutoCompleteController and friends for instrumentation feedback.
+ let thing = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ Ci.nsIAutoCompleteController]),
+ get popup() {
+ return thing;
+ },
+ get controller() {
+ return thing;
+ },
+ popupOpen: true,
+ selectedIndex: 0,
+ getValueAt: function() {
+ return aURI.spec;
+ },
+ searchString: aSearch
+ };
+
+ // Bump up the instrumentation feedback.
+ for (let i = 0; i < aRank; i++) {
+ Services.obs.notifyObservers(thing, "autocomplete-will-enter-text", null);
+ }
+
+ // If this is supposed to be a bookmark, add it.
+ if (aBookmark) {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test_book");
+
+ // And add the tag if we need to.
+ if (aBookmark == "tag") {
+ PlacesUtils.tagging.tagURI(aURI, ["test_tag"]);
+ }
+ }
+}
+
+/**
+ * Decay the adaptive entries by sending the daily idle topic.
+ */
+function doAdaptiveDecay()
+{
+ PlacesUtils.history.runInBatchMode({
+ runBatched: function() {
+ for (let i = 0; i < 10; i++) {
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver)
+ .observe(null, "idle-daily", null);
+ }
+ }
+ }, this);
+}
+
+var uri1 = uri("http://site.tld/1");
+var uri2 = uri("http://site.tld/2");
+
+// d1 is some date for the page visit
+var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+// c1 is larger (should show up higher) than c2
+var c1 = 10;
+var c2 = 1;
+// s1 is a partial match of s2
+var s0 = "";
+var s1 = "si";
+var s2 = "site";
+
+var observer = {
+ results: null,
+ search: null,
+ runCount: -1,
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (--this.runCount > 0)
+ return;
+ ensure_results(this.results, this.search);
+ }
+};
+Services.obs.addObserver(observer, PlacesUtils.TOPIC_FEEDBACK_UPDATED, false);
+
+/**
+ * Make the result object for a given URI that will be passed to ensure_results.
+ */
+function makeResult(aURI, aStyle = "favicon") {
+ return {
+ uri: aURI,
+ style: aStyle,
+ };
+}
+
+var tests = [
+ // Test things without a search term.
+ function*() {
+ print("Test 0 same count, diff rank, same term; no search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+ function*() {
+ print("Test 1 same count, diff rank, same term; no search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c2, s2);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+ function*() {
+ print("Test 2 diff count, same rank, same term; no search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c2, c1, s2);
+ },
+ function*() {
+ print("Test 3 diff count, same rank, same term; no search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c2, c1, s2);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+
+ // Test things with a search term (exact match one, partial other).
+ function*() {
+ print("Test 4 same count, same rank, diff term; one exact/one partial search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s1);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+ function*() {
+ print("Test 5 same count, same rank, diff term; one exact/one partial search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c1, c1, s1);
+ },
+
+ // Test things with a search term (exact match both).
+ function*() {
+ print("Test 6 same count, diff rank, same term; both exact search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s1);
+ yield task_setCountRank(uri2, c1, c2, s1);
+ },
+ function*() {
+ print("Test 7 same count, diff rank, same term; both exact search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c2, s1);
+ yield task_setCountRank(uri2, c1, c1, s1);
+ },
+
+ // Test things with a search term (partial match both).
+ function*() {
+ print("Test 8 same count, diff rank, same term; both partial search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+ function*() {
+ print("Test 9 same count, diff rank, same term; both partial search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c2, s2);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+ function*() {
+ print("Test 10 same count, same rank, same term, decay first; exact match");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s1);
+ doAdaptiveDecay();
+ yield task_setCountRank(uri2, c1, c1, s1);
+ },
+ function*() {
+ print("Test 11 same count, same rank, same term, decay second; exact match");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri2, c1, c1, s1);
+ doAdaptiveDecay();
+ yield task_setCountRank(uri1, c1, c1, s1);
+ },
+ // Test that bookmarks are hidden if the preferences are set right.
+ function*() {
+ print("Test 12 same count, diff rank, same term; no search; history only");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2, "bookmark");
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+ // Test that tags are shown if the preferences are set right.
+ function*() {
+ print("Test 13 same count, diff rank, same term; no search; history only with tag");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ observer.results = [
+ makeResult(uri1, "tag"),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2, "tag");
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+];
+
+/**
+ * This deferred object contains a promise that is resolved when the
+ * ensure_results function has finished its execution.
+ */
+var deferEnsureResults;
+
+/**
+ * Test adaptive autocomplete.
+ */
+add_task(function* test_adaptive()
+{
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+ for (let test of tests) {
+ // Cleanup.
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.tagsFolderId);
+ observer.runCount = -1;
+
+ let types = ["history", "bookmark", "openpage"];
+ for (let type of types) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+
+ yield PlacesTestUtils.clearHistory();
+
+ deferEnsureResults = Promise.defer();
+ yield test();
+ yield deferEnsureResults.promise;
+ }
+
+ Services.obs.removeObserver(observer, PlacesUtils.TOPIC_FEEDBACK_UPDATED);
+});
diff --git a/toolkit/components/places/tests/unit/test_adaptive_bug527311.js b/toolkit/components/places/tests/unit/test_adaptive_bug527311.js
new file mode 100644
index 0000000000..024553bba5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_adaptive_bug527311.js
@@ -0,0 +1,141 @@
+/* -*- 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/. */
+
+const TEST_URL = "http://adapt.mozilla.org/";
+const SEARCH_STRING = "adapt";
+const SUGGEST_TYPES = ["history", "bookmark", "openpage"];
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+var ps = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+const PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC =
+ "places-autocomplete-feedback-updated";
+
+function cleanup() {
+ for (let type of SUGGEST_TYPES) {
+ ps.clearUserPref("browser.urlbar.suggest." + type);
+ }
+}
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+ searches: null,
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function ACI_getSearchAt(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchComplete: function ACI_onSearchComplete() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function() {},
+ invalidate: function() {},
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ onSearchBegin: function() {},
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+function check_results() {
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ let input = new AutoCompleteInput(["unifiedcomplete"]);
+ controller.input = input;
+
+ input.onSearchComplete = function() {
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+ do_check_eq(controller.matchCount, 0);
+
+ PlacesUtils.bookmarks.eraseEverything().then(() => {
+ cleanup();
+ do_test_finished();
+ });
+ };
+
+ controller.startSearch(SEARCH_STRING);
+}
+
+
+function addAdaptiveFeedback(aUrl, aSearch, aCallback) {
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ os.removeObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC);
+ do_timeout(0, aCallback);
+ }
+ };
+ os.addObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC, false);
+
+ let thing = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ Ci.nsIAutoCompleteController]),
+ get popup() { return thing; },
+ get controller() { return thing; },
+ popupOpen: true,
+ selectedIndex: 0,
+ getValueAt: () => aUrl,
+ searchString: aSearch
+ };
+
+ os.notifyObservers(thing, "autocomplete-will-enter-text", null);
+}
+
+
+function run_test() {
+ do_test_pending();
+
+ // Add a bookmark to our url.
+ bs.insertBookmark(bs.unfiledBookmarksFolder, uri(TEST_URL),
+ bs.DEFAULT_INDEX, "test_book");
+ // We want to search only history.
+ for (let type of SUGGEST_TYPES) {
+ type == "history" ? ps.setBoolPref("browser.urlbar.suggest." + type, true)
+ : ps.setBoolPref("browser.urlbar.suggest." + type, false);
+ }
+
+ // Add an adaptive entry.
+ addAdaptiveFeedback(TEST_URL, SEARCH_STRING, check_results);
+}
diff --git a/toolkit/components/places/tests/unit/test_analyze.js b/toolkit/components/places/tests/unit/test_analyze.js
new file mode 100644
index 0000000000..456270101e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_analyze.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests sqlite_sta1 table exists, it should be created by analyze.
+// Since the bookmark roots are created when the DB is created (bug 704855),
+// the table will contain data.
+
+function run_test() {
+ do_test_pending();
+
+ let stmt = DBConn().createAsyncStatement(
+ "SELECT ROWID FROM sqlite_stat1"
+ );
+ stmt.executeAsync({
+ _gotResult: false,
+ handleResult: function(aResultSet) {
+ this._gotResult = true;
+ },
+ handleError: function(aError) {
+ do_throw("Unexpected error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function(aReason) {
+ do_check_true(this._gotResult);
+ do_test_finished();
+ }
+ });
+ stmt.finalize();
+}
diff --git a/toolkit/components/places/tests/unit/test_annotations.js b/toolkit/components/places/tests/unit/test_annotations.js
new file mode 100644
index 0000000000..a37d7e6c9f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_annotations.js
@@ -0,0 +1,363 @@
+/* -*- 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/. */
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Ci.nsINavBookmarksService);
+} catch (ex) {
+ do_throw("Could not get nav-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");
+}
+
+var annoObserver = {
+ PAGE_lastSet_URI: "",
+ PAGE_lastSet_AnnoName: "",
+
+ onPageAnnotationSet: function(aURI, aName) {
+ this.PAGE_lastSet_URI = aURI.spec;
+ this.PAGE_lastSet_AnnoName = aName;
+ },
+
+ ITEM_lastSet_Id: -1,
+ ITEM_lastSet_AnnoName: "",
+ onItemAnnotationSet: function(aItemId, aName) {
+ this.ITEM_lastSet_Id = aItemId;
+ this.ITEM_lastSet_AnnoName = aName;
+ },
+
+ PAGE_lastRemoved_URI: "",
+ PAGE_lastRemoved_AnnoName: "",
+ onPageAnnotationRemoved: function(aURI, aName) {
+ this.PAGE_lastRemoved_URI = aURI.spec;
+ this.PAGE_lastRemoved_AnnoName = aName;
+ },
+
+ ITEM_lastRemoved_Id: -1,
+ ITEM_lastRemoved_AnnoName: "",
+ onItemAnnotationRemoved: function(aItemId, aName) {
+ this.ITEM_lastRemoved_Id = aItemId;
+ this.ITEM_lastRemoved_AnnoName = aName;
+ }
+};
+
+// main
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ var testURI = uri("http://mozilla.com/");
+ var testItemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, "");
+ var testAnnoName = "moz-test-places/annotations";
+ var testAnnoVal = "test";
+
+ annosvc.addObserver(annoObserver);
+ // create new string annotation
+ try {
+ annosvc.setPageAnnotation(testURI, testAnnoName, testAnnoVal, 0, 0);
+ } catch (ex) {
+ do_throw("unable to add page-annotation");
+ }
+ do_check_eq(annoObserver.PAGE_lastSet_URI, testURI.spec);
+ do_check_eq(annoObserver.PAGE_lastSet_AnnoName, testAnnoName);
+
+ // get string annotation
+ do_check_true(annosvc.pageHasAnnotation(testURI, testAnnoName));
+ var storedAnnoVal = annosvc.getPageAnnotation(testURI, testAnnoName);
+ do_check_true(testAnnoVal === storedAnnoVal);
+ // string item-annotation
+ try {
+ var lastModified = bmsvc.getItemLastModified(testItemId);
+ // Verify that lastModified equals dateAdded before we set the annotation.
+ do_check_eq(lastModified, bmsvc.getItemDateAdded(testItemId));
+ // Workaround possible VM timers issues moving last modified to the past.
+ bmsvc.setItemLastModified(testItemId, --lastModified);
+ annosvc.setItemAnnotation(testItemId, testAnnoName, testAnnoVal, 0, 0);
+ var lastModified2 = bmsvc.getItemLastModified(testItemId);
+ // verify that setting the annotation updates the last modified time
+ do_check_true(lastModified2 > lastModified);
+ } catch (ex) {
+ do_throw("unable to add item annotation");
+ }
+ do_check_eq(annoObserver.ITEM_lastSet_Id, testItemId);
+ do_check_eq(annoObserver.ITEM_lastSet_AnnoName, testAnnoName);
+
+ try {
+ var annoVal = annosvc.getItemAnnotation(testItemId, testAnnoName);
+ // verify the anno value
+ do_check_true(testAnnoVal === annoVal);
+ } catch (ex) {
+ do_throw("unable to get item annotation");
+ }
+
+ // test getPagesWithAnnotation
+ var uri2 = uri("http://www.tests.tld");
+ yield PlacesTestUtils.addVisits(uri2);
+ annosvc.setPageAnnotation(uri2, testAnnoName, testAnnoVal, 0, 0);
+ var pages = annosvc.getPagesWithAnnotation(testAnnoName);
+ do_check_eq(pages.length, 2);
+ // Don't rely on the order
+ do_check_false(pages[0].equals(pages[1]));
+ do_check_true(pages[0].equals(testURI) || pages[1].equals(testURI));
+ do_check_true(pages[0].equals(uri2) || pages[1].equals(uri2));
+
+ // test getItemsWithAnnotation
+ var testItemId2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri2, -1, "");
+ annosvc.setItemAnnotation(testItemId2, testAnnoName, testAnnoVal, 0, 0);
+ var items = annosvc.getItemsWithAnnotation(testAnnoName);
+ do_check_eq(items.length, 2);
+ // Don't rely on the order
+ do_check_true(items[0] != items[1]);
+ do_check_true(items[0] == testItemId || items[1] == testItemId);
+ do_check_true(items[0] == testItemId2 || items[1] == testItemId2);
+
+ // get annotation that doesn't exist
+ try {
+ annosvc.getPageAnnotation(testURI, "blah");
+ do_throw("fetching page-annotation that doesn't exist, should've thrown");
+ } catch (ex) {}
+ try {
+ annosvc.getItemAnnotation(testURI, "blah");
+ do_throw("fetching item-annotation that doesn't exist, should've thrown");
+ } catch (ex) {}
+
+ // get annotation info
+ var flags = {}, exp = {}, storageType = {};
+ annosvc.getPageAnnotationInfo(testURI, testAnnoName, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_STRING);
+ annosvc.getItemAnnotationInfo(testItemId, testAnnoName, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_STRING);
+
+ // get annotation names for a uri
+ var annoNames = annosvc.getPageAnnotationNames(testURI);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "moz-test-places/annotations");
+
+ // get annotation names for an item
+ annoNames = annosvc.getItemAnnotationNames(testItemId);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "moz-test-places/annotations");
+
+ // copy annotations to another uri
+ var newURI = uri("http://mozilla.org");
+ yield PlacesTestUtils.addVisits(newURI);
+ annosvc.setPageAnnotation(testURI, "oldAnno", "new", 0, 0);
+ annosvc.setPageAnnotation(newURI, "oldAnno", "old", 0, 0);
+ annoNames = annosvc.getPageAnnotationNames(newURI);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "oldAnno");
+ var oldAnnoNames = annosvc.getPageAnnotationNames(testURI);
+ do_check_eq(oldAnnoNames.length, 2);
+ var copiedAnno = oldAnnoNames[0];
+ annosvc.copyPageAnnotations(testURI, newURI, false);
+ var newAnnoNames = annosvc.getPageAnnotationNames(newURI);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.pageHasAnnotation(newURI, "oldAnno"));
+ do_check_true(annosvc.pageHasAnnotation(newURI, copiedAnno));
+ do_check_eq(annosvc.getPageAnnotation(newURI, "oldAnno"), "old");
+ annosvc.setPageAnnotation(newURI, "oldAnno", "new", 0, 0);
+ annosvc.copyPageAnnotations(testURI, newURI, true);
+ newAnnoNames = annosvc.getPageAnnotationNames(newURI);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.pageHasAnnotation(newURI, "oldAnno"));
+ do_check_true(annosvc.pageHasAnnotation(newURI, copiedAnno));
+ do_check_eq(annosvc.getPageAnnotation(newURI, "oldAnno"), "new");
+
+
+ // copy annotations to another item
+ newURI = uri("http://mozilla.org");
+ var newItemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, newURI, -1, "");
+ var itemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, "");
+ annosvc.setItemAnnotation(itemId, "oldAnno", "new", 0, 0);
+ annosvc.setItemAnnotation(itemId, "testAnno", "test", 0, 0);
+ annosvc.setItemAnnotation(newItemId, "oldAnno", "old", 0, 0);
+ annoNames = annosvc.getItemAnnotationNames(newItemId);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "oldAnno");
+ oldAnnoNames = annosvc.getItemAnnotationNames(itemId);
+ do_check_eq(oldAnnoNames.length, 2);
+ copiedAnno = oldAnnoNames[0];
+ annosvc.copyItemAnnotations(itemId, newItemId, false);
+ newAnnoNames = annosvc.getItemAnnotationNames(newItemId);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.itemHasAnnotation(newItemId, "oldAnno"));
+ do_check_true(annosvc.itemHasAnnotation(newItemId, copiedAnno));
+ do_check_eq(annosvc.getItemAnnotation(newItemId, "oldAnno"), "old");
+ annosvc.setItemAnnotation(newItemId, "oldAnno", "new", 0, 0);
+ annosvc.copyItemAnnotations(itemId, newItemId, true);
+ newAnnoNames = annosvc.getItemAnnotationNames(newItemId);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.itemHasAnnotation(newItemId, "oldAnno"));
+ do_check_true(annosvc.itemHasAnnotation(newItemId, copiedAnno));
+ do_check_eq(annosvc.getItemAnnotation(newItemId, "oldAnno"), "new");
+
+ // test int32 anno type
+ var int32Key = testAnnoName + "/types/Int32";
+ var int32Val = 23;
+ annosvc.setPageAnnotation(testURI, int32Key, int32Val, 0, 0);
+ do_check_true(annosvc.pageHasAnnotation(testURI, int32Key));
+ flags = {}, exp = {}, storageType = {};
+ annosvc.getPageAnnotationInfo(testURI, int32Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_INT32);
+ var storedVal = annosvc.getPageAnnotation(testURI, int32Key);
+ do_check_true(int32Val === storedVal);
+ annosvc.setItemAnnotation(testItemId, int32Key, int32Val, 0, 0);
+ do_check_true(annosvc.itemHasAnnotation(testItemId, int32Key));
+ annosvc.getItemAnnotationInfo(testItemId, int32Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getItemAnnotation(testItemId, int32Key);
+ do_check_true(int32Val === storedVal);
+
+ // test int64 anno type
+ var int64Key = testAnnoName + "/types/Int64";
+ var int64Val = 4294967296;
+ annosvc.setPageAnnotation(testURI, int64Key, int64Val, 0, 0);
+ annosvc.getPageAnnotationInfo(testURI, int64Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getPageAnnotation(testURI, int64Key);
+ do_check_true(int64Val === storedVal);
+ annosvc.setItemAnnotation(testItemId, int64Key, int64Val, 0, 0);
+ do_check_true(annosvc.itemHasAnnotation(testItemId, int64Key));
+ annosvc.getItemAnnotationInfo(testItemId, int64Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getItemAnnotation(testItemId, int64Key);
+ do_check_true(int64Val === storedVal);
+
+ // test double anno type
+ var doubleKey = testAnnoName + "/types/Double";
+ var doubleVal = 0.000002342;
+ annosvc.setPageAnnotation(testURI, doubleKey, doubleVal, 0, 0);
+ annosvc.getPageAnnotationInfo(testURI, doubleKey, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getPageAnnotation(testURI, doubleKey);
+ do_check_true(doubleVal === storedVal);
+ annosvc.setItemAnnotation(testItemId, doubleKey, doubleVal, 0, 0);
+ do_check_true(annosvc.itemHasAnnotation(testItemId, doubleKey));
+ annosvc.getItemAnnotationInfo(testItemId, doubleKey, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_DOUBLE);
+ storedVal = annosvc.getItemAnnotation(testItemId, doubleKey);
+ do_check_true(doubleVal === storedVal);
+
+ // test annotation removal
+ annosvc.removePageAnnotation(testURI, int32Key);
+
+ annosvc.setItemAnnotation(testItemId, testAnnoName, testAnnoVal, 0, 0);
+ // verify that removing an annotation updates the last modified date
+ var lastModified3 = bmsvc.getItemLastModified(testItemId);
+ // Workaround possible VM timers issues moving last modified to the past.
+ bmsvc.setItemLastModified(testItemId, --lastModified3);
+ annosvc.removeItemAnnotation(testItemId, int32Key);
+ var lastModified4 = bmsvc.getItemLastModified(testItemId);
+ do_print("verify that removing an annotation updates the last modified date");
+ do_print("lastModified3 = " + lastModified3);
+ do_print("lastModified4 = " + lastModified4);
+ do_check_true(lastModified4 > lastModified3);
+
+ do_check_eq(annoObserver.PAGE_lastRemoved_URI, testURI.spec);
+ do_check_eq(annoObserver.PAGE_lastRemoved_AnnoName, int32Key);
+ do_check_eq(annoObserver.ITEM_lastRemoved_Id, testItemId);
+ do_check_eq(annoObserver.ITEM_lastRemoved_AnnoName, int32Key);
+
+ // test that getItems/PagesWithAnnotation returns an empty array after
+ // removing all items/pages which had the annotation set, see bug 380317.
+ do_check_eq(annosvc.getItemsWithAnnotation(int32Key).length, 0);
+ do_check_eq(annosvc.getPagesWithAnnotation(int32Key).length, 0);
+
+ // Setting item annotations on invalid item ids should throw
+ var invalidIds = [-1, 0, 37643];
+ for (var id of invalidIds) {
+ try {
+ annosvc.setItemAnnotation(id, "foo", "bar", 0, 0);
+ do_throw("setItemAnnotation* should throw for invalid item id: " + id)
+ }
+ catch (ex) { }
+ }
+
+ // setting an annotation with EXPIRE_HISTORY for an item should throw
+ itemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, "");
+ try {
+ annosvc.setItemAnnotation(itemId, "foo", "bar", 0, annosvc.EXPIRE_WITH_HISTORY);
+ do_throw("setting an item annotation with EXPIRE_HISTORY should throw");
+ }
+ catch (ex) {
+ }
+
+ annosvc.removeObserver(annoObserver);
+});
+
+add_test(function test_getAnnotationsHavingName() {
+ let uri = NetUtil.newURI("http://cat.mozilla.org");
+ let id = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "cat");
+ let fid = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.unfiledBookmarksFolderId, "pillow",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ const ANNOS = {
+ "int": 7,
+ "double": 7.7,
+ "string": "seven"
+ };
+ for (let name in ANNOS) {
+ PlacesUtils.annotations.setPageAnnotation(
+ uri, name, ANNOS[name], 0,
+ PlacesUtils.annotations.EXPIRE_SESSION);
+ PlacesUtils.annotations.setItemAnnotation(
+ id, name, ANNOS[name], 0,
+ PlacesUtils.annotations.EXPIRE_SESSION);
+ PlacesUtils.annotations.setItemAnnotation(
+ fid, name, ANNOS[name], 0,
+ PlacesUtils.annotations.EXPIRE_SESSION);
+ }
+
+ for (let name in ANNOS) {
+ let results = PlacesUtils.annotations.getAnnotationsWithName(name);
+ do_check_eq(results.length, 3);
+
+ for (let result of results) {
+ do_check_eq(result.annotationName, name);
+ do_check_eq(result.annotationValue, ANNOS[name]);
+ if (result.uri)
+ do_check_true(result.uri.equals(uri));
+ else
+ do_check_true(result.itemId > 0);
+
+ if (result.itemId != -1) {
+ if (result.uri)
+ do_check_eq(result.itemId, id);
+ else
+ do_check_eq(result.itemId, fid);
+ do_check_guid_for_bookmark(result.itemId, result.guid);
+ }
+ else {
+ do_check_guid_for_uri(result.uri, result.guid);
+ }
+ }
+ }
+
+ run_next_test();
+});
diff --git a/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js
new file mode 100644
index 0000000000..7296fe0610
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This is a test for asyncExecuteLegacyQueries API.
+
+var tests = [
+
+function test_history_query() {
+ let uri = NetUtil.newURI("http://test.visit.mozilla.com/");
+ let title = "Test visit";
+ PlacesTestUtils.addVisits({ uri: uri, title: title }).then(function () {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ let query = PlacesUtils.history.getNewQuery();
+
+ PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .asyncExecuteLegacyQueries([query], 1, options, {
+ handleResult: function (aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ try {
+ do_check_eq(row.getResultByIndex(1), uri.spec);
+ do_check_eq(row.getResultByIndex(2), title);
+ } catch (e) {
+ do_throw("Error while fetching page data.");
+ }
+ }
+ },
+ handleError: function (aError) {
+ do_throw("Async execution error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function (aReason) {
+ run_next_test();
+ },
+ });
+ });
+},
+
+function test_bookmarks_query() {
+ let uri = NetUtil.newURI("http://test.bookmark.mozilla.com/");
+ let title = "Test bookmark";
+ bookmark(uri, title);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_LASMODIFIED_DESCENDING;
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let query = PlacesUtils.history.getNewQuery();
+
+ PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .asyncExecuteLegacyQueries([query], 1, options, {
+ handleResult: function (aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ try {
+ do_check_eq(row.getResultByIndex(1), uri.spec);
+ do_check_eq(row.getResultByIndex(2), title);
+ } catch (e) {
+ do_throw("Error while fetching page data.");
+ }
+ }
+ },
+ handleError: function (aError) {
+ do_throw("Async execution error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function (aReason) {
+ run_next_test();
+ },
+ });
+},
+
+];
+
+function bookmark(aURI, aTitle)
+{
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ aTitle);
+}
+
+function run_test()
+{
+ do_test_pending();
+ run_next_test();
+}
+
+function run_next_test() {
+ if (tests.length == 0) {
+ do_test_finished();
+ return;
+ }
+
+ Promise.all([
+ PlacesTestUtils.clearHistory(),
+ PlacesUtils.bookmarks.eraseEverything()
+ ]).then(tests.shift());
+}
diff --git a/toolkit/components/places/tests/unit/test_async_history_api.js b/toolkit/components/places/tests/unit/test_async_history_api.js
new file mode 100644
index 0000000000..a012fcda2d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_history_api.js
@@ -0,0 +1,1118 @@
+/**
+ * This file tests the async history API exposed by mozIAsyncHistory.
+ */
+
+// Globals
+
+const TEST_DOMAIN = "http://mozilla.org/";
+const URI_VISIT_SAVED = "uri-visit-saved";
+const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000;
+
+// Helpers
+/**
+ * Object that represents a mozIVisitInfo object.
+ *
+ * @param [optional] aTransitionType
+ * The transition type of the visit. Defaults to TRANSITION_LINK if not
+ * provided.
+ * @param [optional] aVisitTime
+ * The time of the visit. Defaults to now if not provided.
+ */
+function VisitInfo(aTransitionType,
+ aVisitTime) {
+ this.transitionType =
+ aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
+ this.visitDate = aVisitTime || Date.now() * 1000;
+}
+
+function promiseUpdatePlaces(aPlaces) {
+ return new Promise((resolve, reject) => {
+ PlacesUtils.asyncHistory.updatePlaces(aPlaces, {
+ _errors: [],
+ _results: [],
+ handleError(aResultCode, aPlace) {
+ this._errors.push({ resultCode: aResultCode, info: aPlace});
+ },
+ handleResult(aPlace) {
+ this._results.push(aPlace);
+ },
+ handleCompletion() {
+ resolve({ errors: this._errors, results: this._results });
+ }
+ });
+ });
+}
+
+/**
+ * Listens for a title change notification, and calls aCallback when it gets it.
+ *
+ * @param aURI
+ * The URI of the page we expect a notification for.
+ * @param aExpectedTitle
+ * The expected title of the URI we expect a notification for.
+ * @param aCallback
+ * The method to call when we have gotten the proper notification about
+ * the title changing.
+ */
+function TitleChangedObserver(aURI,
+ aExpectedTitle,
+ aCallback) {
+ this.uri = aURI;
+ this.expectedTitle = aExpectedTitle;
+ this.callback = aCallback;
+}
+TitleChangedObserver.prototype = {
+ __proto__: NavHistoryObserver.prototype,
+ onTitleChanged(aURI, aTitle, aGUID) {
+ do_print("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")");
+ if (!this.uri.equals(aURI)) {
+ return;
+ }
+ do_check_eq(aTitle, this.expectedTitle);
+ do_check_guid_for_uri(aURI, aGUID);
+ this.callback();
+ },
+};
+
+/**
+ * Listens for a visit notification, and calls aCallback when it gets it.
+ *
+ * @param aURI
+ * The URI of the page we expect a notification for.
+ * @param aCallback
+ * The method to call when we have gotten the proper notification about
+ * being visited.
+ */
+function VisitObserver(aURI,
+ aGUID,
+ aCallback)
+{
+ this.uri = aURI;
+ this.guid = aGUID;
+ this.callback = aCallback;
+}
+VisitObserver.prototype = {
+ __proto__: NavHistoryObserver.prototype,
+ onVisit: function(aURI,
+ aVisitId,
+ aTime,
+ aSessionId,
+ aReferringId,
+ aTransitionType,
+ aGUID)
+ {
+ do_print("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime +
+ ", " + aSessionId + ", " + aReferringId + ", " +
+ aTransitionType + ", " + aGUID + ")");
+ if (!this.uri.equals(aURI) || this.guid != aGUID) {
+ return;
+ }
+ this.callback(aTime, aTransitionType);
+ },
+};
+
+/**
+ * Tests that a title was set properly in the database.
+ *
+ * @param aURI
+ * The uri to check.
+ * @param aTitle
+ * The expected title in the database.
+ */
+function do_check_title_for_uri(aURI,
+ aTitle)
+{
+ let stack = Components.stack.caller;
+ let stmt = DBConn().createStatement(
+ `SELECT title
+ FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = aURI.spec;
+ do_check_true(stmt.executeStep(), stack);
+ do_check_eq(stmt.row.title, aTitle, stack);
+ stmt.finalize();
+}
+
+// Test Functions
+
+add_task(function* test_interface_exists() {
+ let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports);
+ do_check_true(history instanceof Ci.mozIAsyncHistory);
+});
+
+add_task(function* test_invalid_uri_throws() {
+ // First, test passing in nothing.
+ let place = {
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Now, test other bogus things.
+ const TEST_VALUES = [
+ null,
+ undefined,
+ {},
+ [],
+ TEST_DOMAIN + "test_invalid_id_throws",
+ ];
+ for (let i = 0; i < TEST_VALUES.length; i++) {
+ place.uri = TEST_VALUES[i];
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+});
+
+add_task(function* test_invalid_places_throws() {
+ // First, test passing in nothing.
+ try {
+ PlacesUtils.asyncHistory.updatePlaces();
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
+ }
+
+ // Now, test other bogus things.
+ const TEST_VALUES = [
+ null,
+ undefined,
+ {},
+ [],
+ "",
+ ];
+ for (let i = 0; i < TEST_VALUES.length; i++) {
+ let value = TEST_VALUES[i];
+ try {
+ yield promiseUpdatePlaces(value);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+});
+
+add_task(function* test_invalid_guid_throws() {
+ // First check invalid length guid.
+ let place = {
+ guid: "BAD_GUID",
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Now check invalid character guid.
+ place.guid = "__BADGUID+__";
+ do_check_eq(place.guid.length, 12);
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_no_visits_throws() {
+ const TEST_URI =
+ NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws");
+ const TEST_GUID = "_RANDOMGUID_";
+
+ let log_test_conditions = function(aPlace) {
+ let str = "Testing place with " +
+ (aPlace.uri ? "uri" : "no uri") + ", " +
+ (aPlace.guid ? "guid" : "no guid") + ", " +
+ (aPlace.visits ? "visits array" : "no visits array");
+ do_print(str);
+ };
+
+ // Loop through every possible case. Note that we don't actually care about
+ // the case where we have no uri, place id, or guid (covered by another test),
+ // but it is easier to just make sure it too throws than to exclude it.
+ let place = { };
+ for (let uri = 1; uri >= 0; uri--) {
+ place.uri = uri ? TEST_URI : undefined;
+
+ for (let guid = 1; guid >= 0; guid--) {
+ place.guid = guid ? TEST_GUID : undefined;
+
+ for (let visits = 1; visits >= 0; visits--) {
+ place.visits = visits ? [] : undefined;
+
+ log_test_conditions(place);
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+ }
+ }
+});
+
+add_task(function* test_add_visit_no_date_throws() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ delete place.visits[0].visitDate;
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_add_visit_no_transitionType_throws() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_transitionType_throws"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ delete place.visits[0].transitionType;
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_add_visit_invalid_transitionType_throws() {
+ // First, test something that has a transition type lower than the first one.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_add_visit_invalid_transitionType_throws"),
+ visits: [
+ new VisitInfo(TRANSITION_LINK - 1),
+ ],
+ };
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Now, test something that has a transition type greater than the last one.
+ place.visits[0] = new VisitInfo(TRANSITION_RELOAD + 1);
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_non_addable_uri_errors() {
+ // Array of protocols that nsINavHistoryService::canAddURI returns false for.
+ const URLS = [
+ "about:config",
+ "imap://cyrus.andrew.cmu.edu/archive.imap",
+ "news://new.mozilla.org/mozilla.dev.apps.firefox",
+ "mailbox:Inbox",
+ "moz-anno:favicon:http://mozilla.org/made-up-favicon",
+ "view-source:http://mozilla.org",
+ "chrome://browser/content/browser.xul",
+ "resource://gre-resources/hiddenWindow.html",
+ "data:,Hello%2C%20World!",
+ "wyciwyg:/0/http://mozilla.org",
+ "javascript:alert('hello wolrd!');",
+ "blob:foo",
+ ];
+ let places = [];
+ URLS.forEach(function(url) {
+ try {
+ let place = {
+ uri: NetUtil.newURI(url),
+ title: "test for " + url,
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ places.push(place);
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ // NetUtil.newURI() can throw if e.g. our app knows about imap://
+ // but the account is not set up and so the URL is invalid for us.
+ // Note this in the log but ignore as it's not the subject of this test.
+ do_print("Could not construct URI for '" + url + "'; ignoring");
+ }
+ });
+
+ let placesResult = yield promiseUpdatePlaces(places);
+ if (placesResult.results.length > 0) {
+ do_throw("Unexpected success.");
+ }
+ for (let place of placesResult.errors) {
+ do_print("Checking '" + place.info.uri.spec + "'");
+ do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG);
+ do_check_false(yield promiseIsURIVisited(place.info.uri));
+ }
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_duplicate_guid_errors() {
+ // This test ensures that trying to add a visit, with a guid already found in
+ // another visit, fails.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(placeInfo.uri));
+
+ let badPlace = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"),
+ visits: [
+ new VisitInfo(),
+ ],
+ guid: placeInfo.guid,
+ };
+
+ do_check_false(yield promiseIsURIVisited(badPlace.uri));
+ placesResult = yield promiseUpdatePlaces(badPlace);
+ if (placesResult.results.length > 0) {
+ do_throw("Unexpected success.");
+ }
+ let badPlaceInfo = placesResult.errors[0];
+ do_check_eq(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT);
+ do_check_false(yield promiseIsURIVisited(badPlaceInfo.info.uri));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_invalid_referrerURI_ignored() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_invalid_referrerURI_ignored"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI");
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(placeInfo.uri));
+
+ // Check to make sure we do not visit the invalid referrer.
+ do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI));
+
+ // Check to make sure from_visit is zero in database.
+ let stmt = DBConn().createStatement(
+ `SELECT from_visit
+ FROM moz_historyvisits
+ WHERE id = :visit_id`
+ );
+ stmt.params.visit_id = placeInfo.visits[0].visitId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.from_visit, 0);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_nonnsIURI_referrerURI_ignored() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_nonnsIURI_referrerURI_ignored"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI";
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(placeInfo.uri));
+
+ // Check to make sure from_visit is zero in database.
+ let stmt = DBConn().createStatement(
+ `SELECT from_visit
+ FROM moz_historyvisits
+ WHERE id = :visit_id`
+ );
+ stmt.params.visit_id = placeInfo.visits[0].visitId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.from_visit, 0);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_old_referrer_ignored() {
+ // This tests that a referrer for a visit which is not recent (specifically,
+ // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by
+ // updatePlaces.
+ let oldTime = (Date.now() * 1000) - (RECENT_EVENT_THRESHOLD + 1);
+ let referrerPlace = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"),
+ visits: [
+ new VisitInfo(TRANSITION_LINK, oldTime),
+ ],
+ };
+
+ // First we must add our referrer to the history so that it is not ignored
+ // as being invalid.
+ do_check_false(yield promiseIsURIVisited(referrerPlace.uri));
+ let placesResult = yield promiseUpdatePlaces(referrerPlace);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+
+ // Now that the referrer is added, we can add a page with a valid
+ // referrer to determine if the recency of the referrer is taken into
+ // account.
+ do_check_true(yield promiseIsURIVisited(referrerPlace.uri));
+
+ let visitInfo = new VisitInfo();
+ visitInfo.referrerURI = referrerPlace.uri;
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"),
+ visits: [
+ visitInfo,
+ ],
+ };
+
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(place.uri));
+
+ // Though the visit will not contain the referrer, we must examine the
+ // database to be sure.
+ do_check_eq(placeInfo.visits[0].referrerURI, null);
+ let stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_historyvisits
+ JOIN moz_places h ON h.id = place_id
+ WHERE url_hash = hash(:page_url) AND url = :page_url
+ AND from_visit = 0`
+ );
+ stmt.params.page_url = place.uri.spec;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, 1);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_place_id_ignored() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(place.uri));
+
+ let placeId = placeInfo.placeId;
+ do_check_neq(placeId, 0);
+
+ let badPlace = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"),
+ visits: [
+ new VisitInfo(),
+ ],
+ placeId: placeId,
+ };
+
+ do_check_false(yield promiseIsURIVisited(badPlace.uri));
+ placesResult = yield promiseUpdatePlaces(badPlace);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ placeInfo = placesResult.results[0];
+
+ do_check_neq(placeInfo.placeId, placeId);
+ do_check_true(yield promiseIsURIVisited(badPlace.uri));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_handleCompletion_called_when_complete() {
+ // We test a normal visit, and embeded visit, and a uri that would fail
+ // the canAddURI test to make sure that the notification happens after *all*
+ // of them have had a callback.
+ let places = [
+ { uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_handleCompletion_called_when_complete"),
+ visits: [
+ new VisitInfo(),
+ new VisitInfo(TRANSITION_EMBED),
+ ],
+ },
+ { uri: NetUtil.newURI("data:,Hello%2C%20World!"),
+ visits: [
+ new VisitInfo(),
+ ],
+ },
+ ];
+ do_check_false(yield promiseIsURIVisited(places[0].uri));
+ do_check_false(yield promiseIsURIVisited(places[1].uri));
+
+ const EXPECTED_COUNT_SUCCESS = 2;
+ const EXPECTED_COUNT_FAILURE = 1;
+
+ let {results, errors} = yield promiseUpdatePlaces(places);
+
+ do_check_eq(results.length, EXPECTED_COUNT_SUCCESS);
+ do_check_eq(errors.length, EXPECTED_COUNT_FAILURE);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_add_visit() {
+ const VISIT_TIME = Date.now() * 1000;
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"),
+ title: "test_add_visit title",
+ visits: [],
+ };
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ let transitionType = PlacesUtils.history.TRANSITIONS[t];
+ place.visits.push(new VisitInfo(transitionType, VISIT_TIME));
+ }
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let callbackCount = 0;
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ for (let placeInfo of placesResult.results) {
+ do_check_true(yield promiseIsURIVisited(place.uri));
+
+ // Check mozIPlaceInfo properties.
+ do_check_true(place.uri.equals(placeInfo.uri));
+ do_check_eq(placeInfo.frecency, -1); // We don't pass frecency here!
+ do_check_eq(placeInfo.title, place.title);
+
+ // Check mozIVisitInfo properties.
+ let visits = placeInfo.visits;
+ do_check_eq(visits.length, 1);
+ let visit = visits[0];
+ do_check_eq(visit.visitDate, VISIT_TIME);
+ do_check_true(Object.values(PlacesUtils.history.TRANSITIONS).includes(visit.transitionType));
+ do_check_true(visit.referrerURI === null);
+
+ // For TRANSITION_EMBED visits, many properties will always be zero or
+ // undefined.
+ if (visit.transitionType == TRANSITION_EMBED) {
+ // Check mozIPlaceInfo properties.
+ do_check_eq(placeInfo.placeId, 0, '//');
+ do_check_eq(placeInfo.guid, null);
+
+ // Check mozIVisitInfo properties.
+ do_check_eq(visit.visitId, 0);
+ }
+ // But they should be valid for non-embed visits.
+ else {
+ // Check mozIPlaceInfo properties.
+ do_check_true(placeInfo.placeId > 0);
+ do_check_valid_places_guid(placeInfo.guid);
+
+ // Check mozIVisitInfo properties.
+ do_check_true(visit.visitId > 0);
+ }
+
+ // If we have had all of our callbacks, continue running tests.
+ if (++callbackCount == place.visits.length) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ }
+ }
+});
+
+add_task(function* test_properties_saved() {
+ // Check each transition type to make sure it is saved properly.
+ let places = [];
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ let transitionType = PlacesUtils.history.TRANSITIONS[t];
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" +
+ transitionType),
+ title: "test_properties_saved test",
+ visits: [
+ new VisitInfo(transitionType),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ places.push(place);
+ }
+
+ let callbackCount = 0;
+ let placesResult = yield promiseUpdatePlaces(places);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ for (let placeInfo of placesResult.results) {
+ let uri = placeInfo.uri;
+ do_check_true(yield promiseIsURIVisited(uri));
+ let visit = placeInfo.visits[0];
+ print("TEST-INFO | test_properties_saved | updatePlaces callback for " +
+ "transition type " + visit.transitionType);
+
+ // Note that TRANSITION_EMBED should not be in the database.
+ const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1;
+
+ // mozIVisitInfo::date
+ let stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_places h
+ JOIN moz_historyvisits v
+ ON h.id = v.place_id
+ WHERE h.url_hash = hash(:page_url) AND h.url = :page_url
+ AND v.visit_date = :visit_date`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.visit_date = visit.visitDate;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, EXPECTED_COUNT);
+ stmt.finalize();
+
+ // mozIVisitInfo::transitionType
+ stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_places h
+ JOIN moz_historyvisits v
+ ON h.id = v.place_id
+ WHERE h.url_hash = hash(:page_url) AND h.url = :page_url
+ AND v.visit_type = :transition_type`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.transition_type = visit.transitionType;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, EXPECTED_COUNT);
+ stmt.finalize();
+
+ // mozIPlaceInfo::title
+ stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_places h
+ WHERE h.url_hash = hash(:page_url) AND h.url = :page_url
+ AND h.title = :title`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.title = placeInfo.title;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, EXPECTED_COUNT);
+ stmt.finalize();
+
+ // If we have had all of our callbacks, continue running tests.
+ if (++callbackCount == places.length) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ }
+ }
+});
+
+add_task(function* test_guid_saved() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"),
+ guid: "__TESTGUID__",
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_valid_places_guid(place.guid);
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ let uri = placeInfo.uri;
+ do_check_true(yield promiseIsURIVisited(uri));
+ do_check_eq(placeInfo.guid, place.guid);
+ do_check_guid_for_uri(uri, place.guid);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_referrer_saved() {
+ let places = [
+ { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"),
+ visits: [
+ new VisitInfo(),
+ ],
+ },
+ { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"),
+ visits: [
+ new VisitInfo(),
+ ],
+ },
+ ];
+ places[1].visits[0].referrerURI = places[0].uri;
+ do_check_false(yield promiseIsURIVisited(places[0].uri));
+ do_check_false(yield promiseIsURIVisited(places[1].uri));
+
+ let resultCount = 0;
+ let placesResult = yield promiseUpdatePlaces(places);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ for (let placeInfo of placesResult.results) {
+ let uri = placeInfo.uri;
+ do_check_true(yield promiseIsURIVisited(uri));
+ let visit = placeInfo.visits[0];
+
+ // We need to insert all of our visits before we can test conditions.
+ if (++resultCount == places.length) {
+ do_check_true(places[0].uri.equals(visit.referrerURI));
+
+ let stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_historyvisits
+ JOIN moz_places h ON h.id = place_id
+ WHERE url_hash = hash(:page_url) AND url = :page_url
+ AND from_visit = (
+ SELECT v.id
+ FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = place_id
+ WHERE url_hash = hash(:referrer) AND url = :referrer
+ )`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.referrer = visit.referrerURI.spec;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, 1);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ }
+ }
+});
+
+add_task(function* test_guid_change_saved() {
+ // First, add a visit for it.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ // Then, change the guid with visits.
+ place.guid = "_GUIDCHANGE_";
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_guid_for_uri(place.uri, place.guid);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_title_change_saved() {
+ // First, add a visit for it.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"),
+ title: "original title",
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+
+ // Now, make sure the empty string clears the title.
+ place.title = "";
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, null);
+
+ // Then, change the title with visits.
+ place.title = "title change";
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, place.title);
+
+ // Lastly, check that the title is cleared if we set it to null.
+ place.title = null;
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, place.title);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_no_title_does_not_clear_title() {
+ const TITLE = "test title";
+ // First, add a visit for it.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"),
+ title: TITLE,
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ // Now, make sure that not specifying a title does not clear it.
+ delete place.title;
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, TITLE);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_title_change_notifies() {
+ // There are three cases to test. The first case is to make sure we do not
+ // get notified if we do not specify a title.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let silentObserver =
+ new TitleChangedObserver(place.uri, "DO NOT WANT", function() {
+ do_throw("unexpected callback!");
+ });
+
+ PlacesUtils.history.addObserver(silentObserver, false);
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+
+ // The second case to test is that we get the notification when we add
+ // it for the first time. The first case will fail before our callback if it
+ // is busted, so we can do this now.
+ place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title");
+ place.title = "title 1";
+ function promiseTitleChangedObserver(aPlace) {
+ return new Promise((resolve, reject) => {
+ let callbackCount = 0;
+ let observer = new TitleChangedObserver(aPlace.uri, aPlace.title, function() {
+ switch (++callbackCount) {
+ case 1:
+ // The third case to test is to make sure we get a notification when
+ // we change an existing place.
+ observer.expectedTitle = place.title = "title 2";
+ place.visits = [new VisitInfo()];
+ PlacesUtils.asyncHistory.updatePlaces(place);
+ break;
+ case 2:
+ PlacesUtils.history.removeObserver(silentObserver);
+ PlacesUtils.history.removeObserver(observer);
+ resolve();
+ break;
+ }
+ });
+
+ PlacesUtils.history.addObserver(observer, false);
+ PlacesUtils.asyncHistory.updatePlaces(aPlace);
+ });
+ }
+
+ yield promiseTitleChangedObserver(place);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_visit_notifies() {
+ // There are two observers we need to see for each visit. One is an
+ // nsINavHistoryObserver and the other is the uri-visit-saved observer topic.
+ let place = {
+ guid: "abcdefghijkl",
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ function promiseVisitObserver(aPlace) {
+ return new Promise((resolve, reject) => {
+ let callbackCount = 0;
+ let finisher = function() {
+ if (++callbackCount == 2) {
+ resolve();
+ }
+ }
+ let visitObserver = new VisitObserver(place.uri, place.guid,
+ function(aVisitDate,
+ aTransitionType) {
+ let visit = place.visits[0];
+ do_check_eq(visit.visitDate, aVisitDate);
+ do_check_eq(visit.transitionType, aTransitionType);
+
+ PlacesUtils.history.removeObserver(visitObserver);
+ finisher();
+ });
+ PlacesUtils.history.addObserver(visitObserver, false);
+ let observer = function(aSubject, aTopic, aData) {
+ do_print("observe(" + aSubject + ", " + aTopic + ", " + aData + ")");
+ do_check_true(aSubject instanceof Ci.nsIURI);
+ do_check_true(aSubject.equals(place.uri));
+
+ Services.obs.removeObserver(observer, URI_VISIT_SAVED);
+ finisher();
+ };
+ Services.obs.addObserver(observer, URI_VISIT_SAVED, false);
+ PlacesUtils.asyncHistory.updatePlaces(place);
+ });
+ }
+
+ yield promiseVisitObserver(place);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+// test with empty mozIVisitInfoCallback object
+add_task(function* test_callbacks_not_supplied() {
+ const URLS = [
+ "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI
+ "http://mozilla.org/" // valid URI
+ ];
+ let places = [];
+ URLS.forEach(function(url) {
+ try {
+ let place = {
+ uri: NetUtil.newURI(url),
+ title: "test for " + url,
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ places.push(place);
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ // NetUtil.newURI() can throw if e.g. our app knows about imap://
+ // but the account is not set up and so the URL is invalid for us.
+ // Note this in the log but ignore as it's not the subject of this test.
+ do_print("Could not construct URI for '" + url + "'; ignoring");
+ }
+ });
+
+ PlacesUtils.asyncHistory.updatePlaces(places, {});
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+// Test that we don't wrongly overwrite typed and hidden when adding new visits.
+add_task(function* test_typed_hidden_not_overwritten() {
+ yield PlacesTestUtils.clearHistory();
+ let places = [
+ { uri: NetUtil.newURI("http://mozilla.org/"),
+ title: "test",
+ visits: [
+ new VisitInfo(TRANSITION_TYPED),
+ new VisitInfo(TRANSITION_LINK)
+ ]
+ },
+ { uri: NetUtil.newURI("http://mozilla.org/"),
+ title: "test",
+ visits: [
+ new VisitInfo(TRANSITION_FRAMED_LINK)
+ ]
+ },
+ ];
+ yield promiseUpdatePlaces(places);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(
+ "SELECT hidden, typed FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
+ { url: "http://mozilla.org/" });
+ Assert.equal(rows[0].getResultByName("typed"), 1,
+ "The page should be marked as typed");
+ Assert.equal(rows[0].getResultByName("hidden"), 0,
+ "The page should be marked as not hidden");
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_async_in_batchmode.js b/toolkit/components/places/tests/unit/test_async_in_batchmode.js
new file mode 100644
index 0000000000..b39b26519d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_in_batchmode.js
@@ -0,0 +1,55 @@
+// This is testing the frankenstein situation Sync forces Places into.
+// Sync does runInBatchMode() and before the callback returns the Places async
+// APIs are used (either by Sync itself, or by any other code in the system)
+// As seen in bug 1197856 and bug 1190131.
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+// This function "waits" for a promise to resolve by spinning a nested event
+// loop.
+function waitForPromise(promise) {
+ let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
+
+ let finalResult, finalException;
+
+ promise.then(result => {
+ finalResult = result;
+ }, err => {
+ finalException = err;
+ });
+
+ // Keep waiting until our callback is triggered (unless the app is quitting).
+ while (!finalResult && !finalException) {
+ thread.processNextEvent(true);
+ }
+ if (finalException) {
+ throw finalException;
+ }
+ return finalResult;
+}
+
+add_test(function() {
+ let testCompleted = false;
+ PlacesUtils.bookmarks.runInBatchMode({
+ runBatched() {
+ // create a bookmark.
+ let info = { parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/" };
+ let insertPromise = PlacesUtils.bookmarks.insert(info);
+ let bookmark = waitForPromise(insertPromise);
+ // Check we got a bookmark (bookmark creation failed completely in
+ // bug 1190131)
+ equal(bookmark.url, info.url);
+ // Check the promiseItemGuid and promiseItemId helpers - failure in these
+ // was the underlying reason for the failure.
+ let id = waitForPromise(PlacesUtils.promiseItemId(bookmark.guid));
+ let guid = waitForPromise(PlacesUtils.promiseItemGuid(id));
+ equal(guid, bookmark.guid, "id and guid round-tripped correctly");
+ testCompleted = true;
+ }
+ }, null);
+ // make sure we tested what we think we tested.
+ ok(testCompleted);
+ run_next_test();
+});
diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js
new file mode 100644
index 0000000000..edc9abf870
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -0,0 +1,1739 @@
+/* -*- 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/. */
+
+const bmsvc = PlacesUtils.bookmarks;
+const tagssvc = PlacesUtils.tagging;
+const annosvc = PlacesUtils.annotations;
+const PT = PlacesTransactions;
+const rootGuid = PlacesUtils.bookmarks.rootGuid;
+
+Components.utils.importGlobalProperties(["URL"]);
+
+// Create and add bookmarks observer.
+var observer = {
+ __proto__: NavBookmarkObserver.prototype,
+
+ tagRelatedGuids: new Set(),
+
+ reset: function () {
+ this.itemsAdded = new Map();
+ this.itemsRemoved = new Map();
+ this.itemsChanged = new Map();
+ this.itemsMoved = new Map();
+ this.beginUpdateBatch = false;
+ this.endUpdateBatch = false;
+ },
+
+ onBeginUpdateBatch: function () {
+ this.beginUpdateBatch = true;
+ },
+
+ onEndUpdateBatch: function () {
+ this.endUpdateBatch = true;
+ },
+
+ onItemAdded:
+ function (aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
+ aGuid, aParentGuid) {
+ // Ignore tag items.
+ if (aParentId == PlacesUtils.tagsFolderId ||
+ (aParentId != PlacesUtils.placesRootId &&
+ bmsvc.getFolderIdForItem(aParentId) == PlacesUtils.tagsFolderId)) {
+ this.tagRelatedGuids.add(aGuid);
+ return;
+ }
+
+ this.itemsAdded.set(aGuid, { itemId: aItemId
+ , parentGuid: aParentGuid
+ , index: aIndex
+ , itemType: aItemType
+ , title: aTitle
+ , url: aURI });
+ },
+
+ onItemRemoved:
+ function (aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) {
+ if (this.tagRelatedGuids.has(aGuid))
+ return;
+
+ this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid
+ , index: aIndex
+ , itemType: aItemType });
+ },
+
+ onItemChanged:
+ function (aItemId, aProperty, aIsAnnoProperty, aNewValue, aLastModified,
+ aItemType, aParentId, aGuid, aParentGuid) {
+ if (this.tagRelatedGuids.has(aGuid))
+ return;
+
+ let changesForGuid = this.itemsChanged.get(aGuid);
+ if (changesForGuid === undefined) {
+ changesForGuid = new Map();
+ this.itemsChanged.set(aGuid, changesForGuid);
+ }
+
+ let newValue = aNewValue;
+ if (aIsAnnoProperty) {
+ if (annosvc.itemHasAnnotation(aItemId, aProperty))
+ newValue = annosvc.getItemAnnotation(aItemId, aProperty);
+ else
+ newValue = null;
+ }
+ let change = { isAnnoProperty: aIsAnnoProperty
+ , newValue: newValue
+ , lastModified: aLastModified
+ , itemType: aItemType };
+ changesForGuid.set(aProperty, change);
+ },
+
+ onItemVisited: () => {},
+
+ onItemMoved:
+ function (aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex, aItemType,
+ aGuid, aOldParentGuid, aNewParentGuid) {
+ this.itemsMoved.set(aGuid, { oldParentGuid: aOldParentGuid
+ , oldIndex: aOldIndex
+ , newParentGuid: aNewParentGuid
+ , newIndex: aNewIndex
+ , itemType: aItemType });
+ }
+};
+observer.reset();
+
+// index at which items should begin
+var bmStartIndex = 0;
+
+function run_test() {
+ bmsvc.addObserver(observer, false);
+ do_register_cleanup(function () {
+ bmsvc.removeObserver(observer);
+ });
+
+ run_next_test();
+}
+
+function sanityCheckTransactionHistory() {
+ do_check_true(PT.undoPosition <= PT.length);
+
+ let check_entry_throws = f => {
+ try {
+ f();
+ do_throw("PT.entry should throw for invalid input");
+ } catch (ex) {}
+ };
+ check_entry_throws( () => PT.entry(-1) );
+ check_entry_throws( () => PT.entry({}) );
+ check_entry_throws( () => PT.entry(PT.length) );
+
+ if (PT.undoPosition < PT.length)
+ do_check_eq(PT.topUndoEntry, PT.entry(PT.undoPosition));
+ else
+ do_check_null(PT.topUndoEntry);
+ if (PT.undoPosition > 0)
+ do_check_eq(PT.topRedoEntry, PT.entry(PT.undoPosition - 1));
+ else
+ do_check_null(PT.topRedoEntry);
+}
+
+function getTransactionsHistoryState() {
+ let history = [];
+ for (let i = 0; i < PT.length; i++) {
+ history.push(PT.entry(i));
+ }
+ return [history, PT.undoPosition];
+}
+
+function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) {
+ // ensureUndoState is called in various places during this test, so it's
+ // a good places to sanity-check the transaction-history APIs in all
+ // cases.
+ sanityCheckTransactionHistory();
+
+ let [actualEntries, actualUndoPosition] = getTransactionsHistoryState();
+ do_check_eq(actualEntries.length, aExpectedEntries.length);
+ do_check_eq(actualUndoPosition, aExpectedUndoPosition);
+
+ function checkEqualEntries(aExpectedEntry, aActualEntry) {
+ do_check_eq(aExpectedEntry.length, aActualEntry.length);
+ aExpectedEntry.forEach( (t, i) => do_check_eq(t, aActualEntry[i]) );
+ }
+ aExpectedEntries.forEach( (e, i) => checkEqualEntries(e, actualEntries[i]) );
+}
+
+function ensureItemsAdded(...items) {
+ Assert.equal(observer.itemsAdded.size, items.length);
+ for (let item of items) {
+ Assert.ok(observer.itemsAdded.has(item.guid));
+ let info = observer.itemsAdded.get(item.guid);
+ Assert.equal(info.parentGuid, item.parentGuid);
+ for (let propName of ["title", "index", "itemType"]) {
+ if (propName in item)
+ Assert.equal(info[propName], item[propName]);
+ }
+ if ("url" in item)
+ Assert.ok(info.url.equals(item.url));
+ }
+}
+
+function ensureItemsRemoved(...items) {
+ Assert.equal(observer.itemsRemoved.size, items.length);
+ for (let item of items) {
+ // We accept both guids and full info object here.
+ if (typeof(item) == "string") {
+ Assert.ok(observer.itemsRemoved.has(item));
+ }
+ else {
+ Assert.ok(observer.itemsRemoved.has(item.guid));
+ let info = observer.itemsRemoved.get(item.guid);
+ Assert.equal(info.parentGuid, item.parentGuid);
+ if ("index" in item)
+ Assert.equal(info.index, item.index);
+ }
+ }
+}
+
+function ensureItemsChanged(...items) {
+ for (let item of items) {
+ do_check_true(observer.itemsChanged.has(item.guid));
+ let changes = observer.itemsChanged.get(item.guid);
+ do_check_true(changes.has(item.property));
+ let info = changes.get(item.property);
+ do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty));
+ do_check_eq(info.newValue, item.newValue);
+ if ("url" in item)
+ do_check_true(item.url.equals(info.url));
+ }
+}
+
+function ensureAnnotationsSet(aGuid, aAnnos) {
+ do_check_true(observer.itemsChanged.has(aGuid));
+ let changes = observer.itemsChanged.get(aGuid);
+ for (let anno of aAnnos) {
+ do_check_true(changes.has(anno.name));
+ let changeInfo = changes.get(anno.name);
+ do_check_true(changeInfo.isAnnoProperty);
+ do_check_eq(changeInfo.newValue, anno.value);
+ }
+}
+
+function ensureItemsMoved(...items) {
+ do_check_true(observer.itemsMoved.size, items.length);
+ for (let item of items) {
+ do_check_true(observer.itemsMoved.has(item.guid));
+ let info = observer.itemsMoved.get(item.guid);
+ do_check_eq(info.oldParentGuid, item.oldParentGuid);
+ do_check_eq(info.oldIndex, item.oldIndex);
+ do_check_eq(info.newParentGuid, item.newParentGuid);
+ do_check_eq(info.newIndex, item.newIndex);
+ }
+}
+
+function ensureTimestampsUpdated(aGuid, aCheckDateAdded = false) {
+ do_check_true(observer.itemsChanged.has(aGuid));
+ let changes = observer.itemsChanged.get(aGuid);
+ if (aCheckDateAdded)
+ do_check_true(changes.has("dateAdded"))
+ do_check_true(changes.has("lastModified"));
+}
+
+function ensureTagsForURI(aURI, aTags) {
+ let tagsSet = tagssvc.getTagsForURI(aURI);
+ do_check_eq(tagsSet.length, aTags.length);
+ do_check_true(aTags.every( t => tagsSet.includes(t)));
+}
+
+function createTestFolderInfo(aTitle = "Test Folder") {
+ return { parentGuid: rootGuid, title: "Test Folder" };
+}
+
+function isLivemarkTree(aTree) {
+ return !!aTree.annos &&
+ aTree.annos.some( a => a.name == PlacesUtils.LMANNO_FEEDURI );
+}
+
+function* ensureLivemarkCreatedByAddLivemark(aLivemarkGuid) {
+ // This throws otherwise.
+ yield PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid });
+}
+
+// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the
+// same.
+// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set.
+function* ensureEqualBookmarksTrees(aOriginal,
+ aNew,
+ aIsRestoredItem = true,
+ aCheckParentAndPosition = false) {
+ // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both
+ // ours and the one at deepEqual). This is fine for us because ids are not
+ // restored by Redo.
+ if (aIsRestoredItem) {
+ Assert.deepEqual(aOriginal, aNew);
+ if (isLivemarkTree(aNew))
+ yield ensureLivemarkCreatedByAddLivemark(aNew.guid);
+ return;
+ }
+
+ for (let property of Object.keys(aOriginal)) {
+ if (property == "children") {
+ Assert.equal(aOriginal.children.length, aNew.children.length);
+ for (let i = 0; i < aOriginal.children.length; i++) {
+ yield ensureEqualBookmarksTrees(aOriginal.children[i],
+ aNew.children[i],
+ false,
+ true);
+ }
+ }
+ else if (property == "guid") {
+ // guid shouldn't be copied if the item was not restored.
+ Assert.notEqual(aOriginal.guid, aNew.guid);
+ }
+ else if (property == "dateAdded") {
+ // dateAdded shouldn't be copied if the item was not restored.
+ Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded));
+ }
+ else if (property == "lastModified") {
+ // same same, except for the never-changed case
+ if (!aOriginal.lastModified)
+ Assert.ok(!aNew.lastModified);
+ else
+ Assert.ok(is_time_ordered(aOriginal.lastModified, aNew.lastModified));
+ }
+ else if (aCheckParentAndPosition ||
+ (property != "parentGuid" && property != "index")) {
+ Assert.deepEqual(aOriginal[property], aNew[property]);
+ }
+ }
+
+ if (isLivemarkTree(aNew))
+ yield ensureLivemarkCreatedByAddLivemark(aNew.guid);
+}
+
+function* ensureBookmarksTreeRestoredCorrectly(...aOriginalBookmarksTrees) {
+ for (let originalTree of aOriginalBookmarksTrees) {
+ let restoredTree =
+ yield PlacesUtils.promiseBookmarksTree(originalTree.guid);
+ yield ensureEqualBookmarksTrees(originalTree, restoredTree);
+ }
+}
+
+function* ensureNonExistent(...aGuids) {
+ for (let guid of aGuids) {
+ Assert.strictEqual((yield PlacesUtils.promiseBookmarksTree(guid)), null);
+ }
+}
+
+add_task(function* test_recycled_transactions() {
+ function* ensureTransactThrowsFor(aTransaction) {
+ let [txns, undoPosition] = getTransactionsHistoryState();
+ try {
+ yield aTransaction.transact();
+ do_throw("Shouldn't be able to use the same transaction twice");
+ }
+ catch (ex) { }
+ ensureUndoState(txns, undoPosition);
+ }
+
+ let txn_a = PT.NewFolder(createTestFolderInfo());
+ yield txn_a.transact();
+ ensureUndoState([[txn_a]], 0);
+ yield ensureTransactThrowsFor(txn_a);
+
+ yield PT.undo();
+ ensureUndoState([[txn_a]], 1);
+ ensureTransactThrowsFor(txn_a);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+ ensureTransactThrowsFor(txn_a);
+
+ let txn_b = PT.NewFolder(createTestFolderInfo());
+ yield PT.batch(function* () {
+ try {
+ yield txn_a.transact();
+ do_throw("Shouldn't be able to use the same transaction twice");
+ }
+ catch (ex) { }
+ ensureUndoState();
+ yield txn_b.transact();
+ });
+ ensureUndoState([[txn_b]], 0);
+
+ yield PT.undo();
+ ensureUndoState([[txn_b]], 1);
+ ensureTransactThrowsFor(txn_a);
+ ensureTransactThrowsFor(txn_b);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+ observer.reset();
+});
+
+add_task(function* test_new_folder_with_annotation() {
+ const ANNO = { name: "TestAnno", value: "TestValue" };
+ let folder_info = createTestFolderInfo();
+ folder_info.index = bmStartIndex;
+ folder_info.annotations = [ANNO];
+ ensureUndoState();
+ let txn = PT.NewFolder(folder_info);
+ folder_info.guid = yield txn.transact();
+ let ensureDo = function* (aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ yield ensureItemsAdded(folder_info);
+ ensureAnnotationsSet(folder_info.guid, [ANNO]);
+ if (aRedo)
+ ensureTimestampsUpdated(folder_info.guid, true);
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({ guid: folder_info.guid
+ , parentGuid: folder_info.parentGuid
+ , index: bmStartIndex });
+ observer.reset();
+ };
+
+ yield ensureDo();
+ yield PT.undo();
+ yield ensureUndo();
+ yield PT.redo();
+ yield ensureDo(true);
+ yield PT.undo();
+ ensureUndo();
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_new_bookmark() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test_create_item.com")
+ , index: bmStartIndex
+ , title: "Test creating an item" };
+
+ ensureUndoState();
+ let txn = PT.NewBookmark(bm_info);
+ bm_info.guid = yield txn.transact();
+
+ let ensureDo = function* (aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ yield ensureItemsAdded(bm_info);
+ if (aRedo)
+ ensureTimestampsUpdated(bm_info.guid, true);
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({ guid: bm_info.guid
+ , parentGuid: bm_info.parentGuid
+ , index: bmStartIndex });
+ observer.reset();
+ };
+
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo(true);
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_merge_create_folder_and_item() {
+ let folder_info = createTestFolderInfo();
+ let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com")
+ , title: "Test Bookmark"
+ , index: bmStartIndex };
+
+ let [folderTxnResult, bkmTxnResult] = yield PT.batch(function* () {
+ let folderTxn = PT.NewFolder(folder_info);
+ folder_info.guid = bm_info.parentGuid = yield folderTxn.transact();
+ let bkmTxn = PT.NewBookmark(bm_info);
+ bm_info.guid = yield bkmTxn.transact();
+ return [folderTxn, bkmTxn];
+ });
+
+ let ensureDo = function* () {
+ ensureUndoState([[bkmTxnResult, folderTxnResult]], 0);
+ yield ensureItemsAdded(folder_info, bm_info);
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[bkmTxnResult, folderTxnResult]], 1);
+ ensureItemsRemoved(folder_info, bm_info);
+ observer.reset();
+ };
+
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo();
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_move_items_to_folder() {
+ let folder_a_info = createTestFolderInfo("Folder A");
+ let bkm_a_info = { url: new URL("http://test_move_items.com")
+ , title: "Bookmark A" };
+ let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com")
+ , title: "Bookmark B" };
+
+ // Test moving items within the same folder.
+ let [folder_a_txn_result, bkm_a_txn_result, bkm_b_txn_result] = yield PT.batch(function* () {
+ let folder_a_txn = PT.NewFolder(folder_a_info);
+
+ folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid =
+ yield folder_a_txn.transact();
+ let bkm_a_txn = PT.NewBookmark(bkm_a_info);
+ bkm_a_info.guid = yield bkm_a_txn.transact();
+ let bkm_b_txn = PT.NewBookmark(bkm_b_info);
+ bkm_b_info.guid = yield bkm_b_txn.transact();
+ return [folder_a_txn, bkm_a_txn, bkm_b_txn];
+ });
+
+ ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0);
+
+ let moveTxn = PT.Move({ guid: bkm_a_info.guid
+ , newParentGuid: folder_a_info.guid });
+ yield moveTxn.transact();
+
+ let ensureDo = () => {
+ ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_a_info.guid
+ , newParentGuid: folder_a_info.guid
+ , oldIndex: 0
+ , newIndex: 1 });
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 1);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_a_info.guid
+ , newParentGuid: folder_a_info.guid
+ , oldIndex: 1
+ , newIndex: 0 });
+ observer.reset();
+ };
+
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo();
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ yield PT.clearTransactionsHistory(false, true);
+ ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0);
+
+ // Test moving items between folders.
+ let folder_b_info = createTestFolderInfo("Folder B");
+ let folder_b_txn = PT.NewFolder(folder_b_info);
+ folder_b_info.guid = yield folder_b_txn.transact();
+ ensureUndoState([ [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0);
+
+ moveTxn = PT.Move({ guid: bkm_a_info.guid
+ , newParentGuid: folder_b_info.guid
+ , newIndex: bmsvc.DEFAULT_INDEX });
+ yield moveTxn.transact();
+
+ ensureDo = () => {
+ ensureUndoState([ [moveTxn]
+ , [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_a_info.guid
+ , newParentGuid: folder_b_info.guid
+ , oldIndex: 0
+ , newIndex: 0 });
+ observer.reset();
+ };
+ ensureUndo = () => {
+ ensureUndoState([ [moveTxn]
+ , [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 1);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_b_info.guid
+ , newParentGuid: folder_a_info.guid
+ , oldIndex: 0
+ , newIndex: 0 });
+ observer.reset();
+ };
+
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo();
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ // Clean up
+ yield PT.undo(); // folder_b_txn
+ yield PT.undo(); // folder_a_txn + the bookmarks;
+ do_check_eq(observer.itemsRemoved.size, 4);
+ ensureUndoState([ [moveTxn]
+ , [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 3);
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_remove_folder() {
+ let folder_level_1_info = createTestFolderInfo("Folder Level 1");
+ let folder_level_2_info = { title: "Folder Level 2" };
+ let [folder_level_1_txn_result,
+ folder_level_2_txn_result] = yield PT.batch(function* () {
+ let folder_level_1_txn = PT.NewFolder(folder_level_1_info);
+ folder_level_1_info.guid = yield folder_level_1_txn.transact();
+ folder_level_2_info.parentGuid = folder_level_1_info.guid;
+ let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
+ folder_level_2_info.guid = yield folder_level_2_txn.transact();
+ return [folder_level_1_txn, folder_level_2_txn];
+ });
+
+ ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]);
+ yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ observer.reset();
+
+ let remove_folder_2_txn = PT.Remove(folder_level_2_info);
+ yield remove_folder_2_txn.transact();
+
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ]);
+ yield ensureItemsRemoved(folder_level_2_info);
+
+ // Undo Remove "Folder Level 2"
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_2_info);
+ ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ yield PT.redo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ]);
+ yield ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo it again
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_2_info);
+ ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ observer.reset();
+
+ // Undo the creation of both folders
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2);
+ yield ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
+ observer.reset();
+
+ // Redo the creation of both folders
+ yield PT.redo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ ensureTimestampsUpdated(folder_level_1_info.guid, true);
+ ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ yield PT.redo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ]);
+ yield ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo everything one last time
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_2_info);
+ observer.reset();
+
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2);
+ yield ensureItemsRemoved(folder_level_2_info, folder_level_2_info);
+ observer.reset();
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
+ const testURI = NetUtil.newURI("http://add.remove.tag");
+ const TAG_1 = "TestTag1";
+ const TAG_2 = "TestTag2";
+ const KEYWORD = "test_keyword";
+ const POST_DATA = "post_data";
+ const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
+
+ let folder_info = createTestFolderInfo();
+ folder_info.guid = yield PT.NewFolder(folder_info).transact();
+ let ensureTags = ensureTagsForURI.bind(null, testURI);
+
+ // Check that the NewBookmark transaction preserves tags.
+ observer.reset();
+ let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
+ b1_info.guid = yield PT.NewBookmark(b1_info).transact();
+ ensureTags([TAG_1]);
+ yield PT.undo();
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.redo();
+ ensureTimestampsUpdated(b1_info.guid, true);
+ ensureTags([TAG_1]);
+
+ // Check if the Remove transaction removes and restores tags of children
+ // correctly.
+ yield PT.Remove(folder_info.guid).transact();
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.undo();
+ ensureTimestampsUpdated(b1_info.guid, true);
+ ensureTags([TAG_1]);
+
+ yield PT.redo();
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.undo();
+ ensureTimestampsUpdated(b1_info.guid, true);
+ ensureTags([TAG_1]);
+
+ // * Check that no-op tagging (the uri is already tagged with TAG_1) is
+ // also a no-op on undo.
+ // * Test the "keyword" property of the NewBookmark transaction.
+ observer.reset();
+ let b2_info = { parentGuid: folder_info.guid
+ , url: testURI, tags: [TAG_1, TAG_2]
+ , keyword: KEYWORD
+ , postData: POST_DATA
+ , annotations: [ANNO] };
+ b2_info.guid = yield PT.NewBookmark(b2_info).transact();
+ let b2_post_creation_changes = [
+ { guid: b2_info.guid
+ , isAnnoProperty: true
+ , property: ANNO.name
+ , newValue: ANNO.value },
+ { guid: b2_info.guid
+ , property: "keyword"
+ , newValue: KEYWORD } ];
+ ensureItemsChanged(...b2_post_creation_changes);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ yield PT.undo();
+ yield ensureItemsRemoved(b2_info);
+ ensureTags([TAG_1]);
+
+ // Check if Remove correctly restores keywords, tags and annotations.
+ // Since both bookmarks share the same uri, they also share the keyword that
+ // is not removed along with one of the bookmarks.
+ observer.reset();
+ yield PT.redo();
+ ensureItemsChanged({ guid: b2_info.guid
+ , isAnnoProperty: true
+ , property: ANNO.name
+ , newValue: ANNO.value });
+ ensureTags([TAG_1, TAG_2]);
+
+ // Test Remove for multiple items.
+ observer.reset();
+ yield PT.Remove(b1_info.guid).transact();
+ yield PT.Remove(b2_info.guid).transact();
+ yield PT.Remove(folder_info.guid).transact();
+ yield ensureItemsRemoved(b1_info, b2_info, folder_info);
+ ensureTags([]);
+ // There is no keyword removal notification cause all bookmarks are removed
+ // before the keyword itself, so there's no one to notify.
+ let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null, "keyword has been removed");
+
+ observer.reset();
+ yield PT.undo();
+ yield ensureItemsAdded(folder_info);
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.undo();
+ ensureItemsChanged(...b2_post_creation_changes);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ yield PT.undo();
+ yield ensureItemsAdded(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ // The redo calls below cleanup everything we did.
+ observer.reset();
+ yield PT.redo();
+ yield ensureItemsRemoved(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ yield PT.redo();
+ yield ensureItemsRemoved(b2_info);
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.redo();
+ yield ensureItemsRemoved(folder_info);
+ ensureTags([]);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_creating_and_removing_a_separator() {
+ let folder_info = createTestFolderInfo();
+ let separator_info = {};
+ let undoEntries = [];
+
+ observer.reset();
+ let create_txns = yield PT.batch(function* () {
+ let folder_txn = PT.NewFolder(folder_info);
+ folder_info.guid = separator_info.parentGuid = yield folder_txn.transact();
+ let separator_txn = PT.NewSeparator(separator_info);
+ separator_info.guid = yield separator_txn.transact();
+ return [separator_txn, folder_txn];
+ });
+ undoEntries.unshift(create_txns);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ yield PT.redo();
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ let remove_sep_txn = PT.Remove(separator_info);
+ yield remove_sep_txn.transact();
+ undoEntries.unshift([remove_sep_txn]);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsRemoved(separator_info);
+
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(separator_info);
+
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 2);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ yield PT.redo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(folder_info, separator_info);
+
+ // Clear redo entries and check that |redo| does nothing
+ observer.reset();
+ yield PT.clearTransactionsHistory(false, true);
+ undoEntries.shift();
+ ensureUndoState(undoEntries, 0);
+ yield PT.redo();
+ ensureItemsAdded();
+ ensureItemsRemoved();
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_add_and_remove_livemark() {
+ let createLivemarkTxn = PT.NewLivemark(
+ { feedUrl: NetUtil.newURI("http://test.remove.livemark")
+ , parentGuid: rootGuid
+ , title: "Test Remove Livemark" });
+ let guid = yield createLivemarkTxn.transact();
+ let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid);
+ Assert.ok(originalInfo);
+ yield ensureLivemarkCreatedByAddLivemark(guid);
+
+ let removeTxn = PT.Remove(guid);
+ yield removeTxn.transact();
+ yield ensureNonExistent(guid);
+ function* undo() {
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
+ yield PT.undo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
+ yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ yield PT.undo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
+ yield ensureNonExistent(guid);
+ }
+ function* redo() {
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
+ yield PT.redo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
+ yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ yield PT.redo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
+ yield ensureNonExistent(guid);
+ }
+
+ yield undo();
+ yield redo();
+ yield undo();
+ yield redo();
+
+ // Cleanup
+ yield undo();
+ observer.reset();
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_edit_title() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test_create_item.com")
+ , title: "Original Title" };
+
+ function ensureTitleChange(aCurrentTitle) {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "title"
+ , newValue: aCurrentTitle});
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact();
+ ensureTitleChange("New Title");
+
+ observer.reset();
+ yield PT.undo();
+ ensureTitleChange("Original Title");
+
+ observer.reset();
+ yield PT.redo();
+ ensureTitleChange("New Title");
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureTitleChange("Original Title");
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_edit_url() {
+ let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
+ let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
+ let bm_info = { parentGuid: rootGuid, url: oldURI, tags: ["TestTag"] };
+ function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "uri"
+ , newValue: aPostChangeURI.spec });
+ ensureTagsForURI(aPostChangeURI, bm_info.tags);
+ ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+ ensureTagsForURI(oldURI, bm_info.tags);
+
+ // When there's a single bookmark for the same url, tags should be moved.
+ observer.reset();
+ yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ observer.reset();
+ yield PT.redo();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ // When there're multiple bookmarks for the same url, tags should be copied.
+ let bm2_info = Object.create(bm_info);
+ bm2_info.guid = yield PT.NewBookmark(bm2_info).transact();
+ let bm3_info = Object.create(bm_info);
+ bm3_info.url = newURI;
+ bm3_info.guid = yield PT.NewBookmark(bm3_info).transact();
+
+ observer.reset();
+ yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+
+ observer.reset();
+ yield PT.redo();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+ yield PT.undo();
+ yield PT.undo();
+ yield PT.undo();
+ ensureItemsRemoved(bm3_info, bm2_info, bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_edit_keyword() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test.edit.keyword") };
+ const KEYWORD = "test_keyword";
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "") {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "keyword"
+ , newValue: aCurrentKeyword });
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD, postData: "postData" }).transact();
+ ensureKeywordChange(KEYWORD);
+ let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData");
+
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange();
+ entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.redo();
+ ensureKeywordChange(KEYWORD);
+ entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData");
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange();
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_edit_specific_keyword() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test.edit.keyword") };
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "keyword"
+ , newValue: aCurrentKeyword
+ });
+ }
+
+ yield PlacesUtils.keywords.insert({ keyword: "kw1", url: bm_info.url.spec, postData: "postData1" });
+ yield PlacesUtils.keywords.insert({ keyword: "kw2", url: bm_info.url.spec, postData: "postData2" });
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditKeyword({ guid: bm_info.guid, keyword: "keyword", oldKeyword: "kw2" }).transact();
+ ensureKeywordChange("keyword", "kw2");
+ let entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange("kw2", "keyword");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.redo();
+ ensureKeywordChange("keyword", "kw2");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange("kw2");
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_tag_uri() {
+ // This also tests passing uri specs.
+ let bm_info_a = { url: "http://bookmarked.uri"
+ , parentGuid: rootGuid };
+ let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
+ , parentGuid: rootGuid };
+ let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri");
+
+ function* promiseIsBookmarked(aURI) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncGetBookmarkIds(aURI, ids => {
+ deferred.resolve(ids.length > 0);
+ });
+ return deferred.promise;
+ }
+
+ yield PT.batch(function* () {
+ bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
+ bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
+ });
+
+ function* doTest(aInfo) {
+ let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+
+ let ensureURI = url => typeof(url) == "string" ? NetUtil.newURI(url) : url;
+ urls = urls.map(ensureURI);
+
+ let tagWillAlsoBookmark = new Set();
+ for (let url of urls) {
+ if (!(yield promiseIsBookmarked(url))) {
+ tagWillAlsoBookmark.add(url);
+ }
+ }
+
+ function* ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, tags);
+ Assert.ok(yield promiseIsBookmarked(url));
+ }
+ }
+ function* ensureTagsUnset() {
+ for (let url of urls) {
+ ensureTagsForURI(url, []);
+ if (tagWillAlsoBookmark.has(url))
+ Assert.ok(!(yield promiseIsBookmarked(url)));
+ else
+ Assert.ok(yield promiseIsBookmarked(url));
+ }
+ }
+
+ yield PT.Tag(aInfo).transact();
+ yield ensureTagsSet();
+ yield PT.undo();
+ yield ensureTagsUnset();
+ yield PT.redo();
+ yield ensureTagsSet();
+ yield PT.undo();
+ yield ensureTagsUnset();
+ }
+
+ yield doTest({ url: bm_info_a.url, tags: ["MyTag"] });
+ yield doTest({ urls: [bm_info_a.url], tag: "MyTag" });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] });
+ yield doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" });
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_untag_uri() {
+ let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri")
+ , parentGuid: rootGuid
+ , tags: ["A", "B"] };
+ let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
+ , parentGuid: rootGuid
+ , tag: "B" };
+
+ yield PT.batch(function* () {
+ bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
+ ensureTagsForURI(bm_info_a.url, bm_info_a.tags);
+ bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
+ ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]);
+ });
+
+ function* doTest(aInfo) {
+ let urls, tagsRemoved;
+ if (aInfo instanceof Ci.nsIURI) {
+ urls = [aInfo];
+ tagsRemoved = [];
+ }
+ else if (Array.isArray(aInfo)) {
+ urls = aInfo;
+ tagsRemoved = [];
+ }
+ else {
+ urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+ }
+
+ let preRemovalTags = new Map();
+ for (let url of urls) {
+ preRemovalTags.set(url, tagssvc.getTagsForURI(url));
+ }
+
+ function ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, preRemovalTags.get(url));
+ }
+ }
+ function ensureTagsUnset() {
+ for (let url of urls) {
+ let expectedTags = tagsRemoved.length == 0 ?
+ [] : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag));
+ ensureTagsForURI(url, expectedTags);
+ }
+ }
+
+ yield PT.Untag(aInfo).transact();
+ yield ensureTagsUnset();
+ yield PT.undo();
+ yield ensureTagsSet();
+ yield PT.redo();
+ yield ensureTagsUnset();
+ yield PT.undo();
+ yield ensureTagsSet();
+ }
+
+ yield doTest(bm_info_a);
+ yield doTest(bm_info_b);
+ yield doTest(bm_info_a.url);
+ yield doTest(bm_info_b.url);
+ yield doTest([bm_info_a.url, bm_info_b.url]);
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] });
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_annotate() {
+ let bm_info = { url: NetUtil.newURI("http://test.item.annotation")
+ , parentGuid: rootGuid };
+ let anno_info = { name: "TestAnno", value: "TestValue" };
+ function ensureAnnoState(aSet) {
+ ensureAnnotationsSet(bm_info.guid,
+ [{ name: anno_info.name
+ , value: aSet ? anno_info.value : null }]);
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.Annotate({ guid: bm_info.guid, annotation: anno_info }).transact();
+ ensureAnnoState(true);
+
+ observer.reset();
+ yield PT.undo();
+ ensureAnnoState(false);
+
+ observer.reset();
+ yield PT.redo();
+ ensureAnnoState(true);
+
+ // Test removing the annotation by not passing the |value| property.
+ observer.reset();
+ yield PT.Annotate({ guid: bm_info.guid,
+ annotation: { name: anno_info.name }}).transact();
+ ensureAnnoState(false);
+
+ observer.reset();
+ yield PT.undo();
+ ensureAnnoState(true);
+
+ observer.reset();
+ yield PT.redo();
+ ensureAnnoState(false);
+
+ // Cleanup
+ yield PT.undo();
+ observer.reset();
+});
+
+add_task(function* test_annotate_multiple() {
+ let guid = yield PT.NewFolder(createTestFolderInfo()).transact();
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+
+ function AnnoObj(aName, aValue) {
+ this.name = aName;
+ this.value = aValue;
+ this.flags = 0;
+ this.expires = Ci.nsIAnnotationService.EXPIRE_NEVER;
+ }
+
+ function annos(a = null, b = null) {
+ return [new AnnoObj("A", a), new AnnoObj("B", b)];
+ }
+
+ function verifyAnnoValues(a = null, b = null) {
+ let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
+ let expectedAnnos = [];
+ if (a !== null)
+ expectedAnnos.push(new AnnoObj("A", a));
+ if (b !== null)
+ expectedAnnos.push(new AnnoObj("B", b));
+
+ Assert.deepEqual(currentAnnos, expectedAnnos);
+ }
+
+ yield PT.Annotate({ guid: guid, annotations: annos(1, 2) }).transact();
+ verifyAnnoValues(1, 2);
+ yield PT.undo();
+ verifyAnnoValues();
+ yield PT.redo();
+ verifyAnnoValues(1, 2);
+
+ yield PT.Annotate({ guid: guid
+ , annotation: { name: "A" } }).transact();
+ verifyAnnoValues(null, 2);
+
+ yield PT.Annotate({ guid: guid
+ , annotation: { name: "B", value: 0 } }).transact();
+ verifyAnnoValues(null, 0);
+ yield PT.undo();
+ verifyAnnoValues(null, 2);
+ yield PT.redo();
+ verifyAnnoValues(null, 0);
+ yield PT.undo();
+ verifyAnnoValues(null, 2);
+ yield PT.undo();
+ verifyAnnoValues(1, 2);
+ yield PT.undo();
+ verifyAnnoValues();
+
+ // Cleanup
+ yield PT.undo();
+ observer.reset();
+});
+
+add_task(function* test_sort_folder_by_name() {
+ let folder_info = createTestFolderInfo();
+
+ let url = NetUtil.newURI("http://sort.by.name/");
+ let preSep = ["3", "2", "1"].map(i => ({ title: i, url }));
+ let sep = {};
+ let postSep = ["c", "b", "a"].map(l => ({ title: l, url }));
+ let originalOrder = [...preSep, sep, ...postSep];
+ let sortedOrder = [...preSep.slice(0).reverse(),
+ sep,
+ ...postSep.slice(0).reverse()];
+ yield PT.batch(function* () {
+ folder_info.guid = yield PT.NewFolder(folder_info).transact();
+ for (let info of originalOrder) {
+ info.parentGuid = folder_info.guid;
+ info.guid = yield info == sep ?
+ PT.NewSeparator(info).transact() :
+ PT.NewBookmark(info).transact();
+ }
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder_info.guid);
+ let folderContainer = PlacesUtils.getFolderContents(folderId).root;
+ function ensureOrder(aOrder) {
+ for (let i = 0; i < folderContainer.childCount; i++) {
+ do_check_eq(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid);
+ }
+ }
+
+ ensureOrder(originalOrder);
+ yield PT.SortByName(folder_info.guid).transact();
+ ensureOrder(sortedOrder);
+ yield PT.undo();
+ ensureOrder(originalOrder);
+ yield PT.redo();
+ ensureOrder(sortedOrder);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureOrder(originalOrder);
+ yield PT.undo();
+ ensureItemsRemoved(...originalOrder, folder_info);
+});
+
+add_task(function* test_livemark_txns() {
+ let livemark_info =
+ { feedUrl: NetUtil.newURI("http://test.feed.uri")
+ , parentGuid: rootGuid
+ , title: "Test Livemark" };
+ function ensureLivemarkAdded() {
+ ensureItemsAdded({ guid: livemark_info.guid
+ , title: livemark_info.title
+ , parentGuid: livemark_info.parentGuid
+ , itemType: bmsvc.TYPE_FOLDER });
+ let annos = [{ name: PlacesUtils.LMANNO_FEEDURI
+ , value: livemark_info.feedUrl.spec }];
+ if ("siteUrl" in livemark_info) {
+ annos.push({ name: PlacesUtils.LMANNO_SITEURI
+ , value: livemark_info.siteUrl.spec });
+ }
+ ensureAnnotationsSet(livemark_info.guid, annos);
+ }
+ function ensureLivemarkRemoved() {
+ ensureItemsRemoved({ guid: livemark_info.guid
+ , parentGuid: livemark_info.parentGuid });
+ }
+
+ function* _testDoUndoRedoUndo() {
+ observer.reset();
+ livemark_info.guid = yield PT.NewLivemark(livemark_info).transact();
+ ensureLivemarkAdded();
+
+ observer.reset();
+ yield PT.undo();
+ ensureLivemarkRemoved();
+
+ observer.reset();
+ yield PT.redo();
+ ensureLivemarkAdded();
+
+ yield PT.undo();
+ ensureLivemarkRemoved();
+ }
+
+ yield* _testDoUndoRedoUndo()
+ livemark_info.siteUrl = NetUtil.newURI("http://feed.site.uri");
+ yield* _testDoUndoRedoUndo();
+
+ // Cleanup
+ observer.reset();
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_copy() {
+ function* duplicate_and_test(aOriginalGuid) {
+ let txn = PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid });
+ yield duplicateGuid = yield txn.transact();
+ let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid);
+ let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid);
+ yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
+
+ function* redo() {
+ yield PT.redo();
+ yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ yield PT.redo();
+ yield ensureBookmarksTreeRestoredCorrectly(duplicateInfo);
+ }
+ function* undo() {
+ yield PT.undo();
+ // also undo the original item addition.
+ yield PT.undo();
+ yield ensureNonExistent(aOriginalGuid, duplicateGuid);
+ }
+
+ yield undo();
+ yield redo();
+ yield undo();
+ yield redo();
+
+ // Cleanup. This also remove the original item.
+ yield PT.undo();
+ observer.reset();
+ yield PT.clearTransactionsHistory();
+ }
+
+ // Test duplicating leafs (bookmark, separator, empty folder)
+ PT.NewBookmark({ url: new URL("http://test.item.duplicate")
+ , parentGuid: rootGuid
+ , annos: [{ name: "Anno", value: "AnnoValue"}] });
+ let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 });
+ let livemarkTxn = PT.NewLivemark(
+ { feedUrl: new URL("http://test.feed.uri")
+ , parentGuid: rootGuid
+ , title: "Test Livemark", index: 1 });
+ let emptyFolderTxn = PT.NewFolder(createTestFolderInfo());
+ for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) {
+ let guid = yield txn.transact();
+ yield duplicate_and_test(guid);
+ }
+
+ // Test duplicating a folder having some contents.
+ let filledFolderGuid = yield PT.batch(function *() {
+ let folderGuid = yield PT.NewFolder(createTestFolderInfo()).transact();
+ let nestedFolderGuid =
+ yield PT.NewFolder({ parentGuid: folderGuid
+ , title: "Nested Folder" }).transact();
+ // Insert a bookmark under the nested folder.
+ yield PT.NewBookmark({ url: new URL("http://nested.nested.bookmark")
+ , parentGuid: nestedFolderGuid }).transact();
+ // Insert a separator below the nested folder
+ yield PT.NewSeparator({ parentGuid: folderGuid }).transact();
+ // And another bookmark.
+ yield PT.NewBookmark({ url: new URL("http://nested.bookmark")
+ , parentGuid: folderGuid }).transact();
+ return folderGuid;
+ });
+
+ yield duplicate_and_test(filledFolderGuid);
+
+ // Cleanup
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_array_input_for_batch() {
+ let folderTxn = PT.NewFolder(createTestFolderInfo());
+ let folderGuid = yield folderTxn.transact();
+
+ let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ yield PT.batch([sep1_txn, sep2_txn]);
+ ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0);
+
+ let ensureChildCount = function* (count) {
+ let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid);
+ if (count == 0)
+ Assert.ok(!("children" in tree));
+ else
+ Assert.equal(tree.children.length, count);
+ };
+
+ yield ensureChildCount(2);
+ yield PT.undo();
+ yield ensureChildCount(0);
+ yield PT.redo()
+ yield ensureChildCount(2);
+ yield PT.undo();
+ yield ensureChildCount(0);
+
+ yield PT.undo();
+ Assert.equal((yield PlacesUtils.promiseBookmarksTree(folderGuid)), null);
+
+ // Cleanup
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_copy_excluding_annotations() {
+ let folderInfo = createTestFolderInfo();
+ let anno = n => { return { name: n, value: 1 } };
+ folderInfo.annotations = [anno("a"), anno("b"), anno("c")];
+ let folderGuid = yield PT.NewFolder(folderInfo).transact();
+
+ let ensureAnnosSet = function* (guid, ...expectedAnnoNames) {
+ let tree = yield PlacesUtils.promiseBookmarksTree(guid);
+ let annoNames = "annos" in tree ?
+ tree.annos.map(a => a.name).sort() : [];
+ Assert.deepEqual(annoNames, expectedAnnoNames);
+ };
+
+ yield ensureAnnosSet(folderGuid, "a", "b", "c");
+
+ let excluding_a_dupeGuid =
+ yield PT.Copy({ guid: folderGuid
+ , newParentGuid: rootGuid
+ , excludingAnnotation: "a" }).transact();
+ yield ensureAnnosSet(excluding_a_dupeGuid, "b", "c");
+
+ let excluding_ac_dupeGuid =
+ yield PT.Copy({ guid: folderGuid
+ , newParentGuid: rootGuid
+ , excludingAnnotations: ["a", "c"] }).transact();
+ yield ensureAnnosSet(excluding_ac_dupeGuid, "b");
+
+ // Cleanup
+ yield PT.undo();
+ yield PT.undo();
+ yield PT.undo();
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_invalid_uri_spec_throws() {
+ Assert.throws(() =>
+ PT.NewBookmark({ parentGuid: rootGuid
+ , url: "invalid uri spec"
+ , title: "test bookmark" }));
+ Assert.throws(() =>
+ PT.Tag({ tag: "TheTag"
+ , urls: ["invalid uri spec"] }));
+ Assert.throws(() =>
+ PT.Tag({ tag: "TheTag"
+ , urls: ["about:blank", "invalid uri spec"] }));
+});
+
+add_task(function* test_annotate_multiple_items() {
+ let parentGuid = rootGuid;
+ let guids = [
+ yield PT.NewBookmark({ url: "about:blank", parentGuid }).transact(),
+ yield PT.NewFolder({ title: "Test Folder", parentGuid }).transact()];
+
+ let annotation = { name: "TestAnno", value: "TestValue" };
+ yield PT.Annotate({ guids, annotation }).transact();
+
+ function *ensureAnnoSet() {
+ for (let guid of guids) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ Assert.equal(annosvc.getItemAnnotation(itemId, annotation.name),
+ annotation.value);
+ }
+ }
+ function *ensureAnnoUnset() {
+ for (let guid of guids) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ Assert.ok(!annosvc.itemHasAnnotation(itemId, annotation.name));
+ }
+ }
+
+ yield ensureAnnoSet();
+ yield PT.undo();
+ yield ensureAnnoUnset();
+ yield PT.redo();
+ yield ensureAnnoSet();
+ yield PT.undo();
+ yield ensureAnnoUnset();
+
+ // Cleanup
+ yield PT.undo();
+ yield PT.undo();
+ yield ensureNonExistent(...guids);
+ yield PT.clearTransactionsHistory();
+ observer.reset();
+});
+
+add_task(function* test_remove_multiple() {
+ let guids = [];
+ yield PT.batch(function* () {
+ let folderGuid = yield PT.NewFolder({ title: "Test Folder"
+ , parentGuid: rootGuid }).transact();
+ let nestedFolderGuid =
+ yield PT.NewFolder({ title: "Nested Test Folder"
+ , parentGuid: folderGuid }).transact();
+ yield PT.NewSeparator(nestedFolderGuid).transact();
+
+ guids.push(folderGuid);
+
+ let bmGuid =
+ yield PT.NewBookmark({ url: new URL("http://test.bookmark.removed")
+ , parentGuid: rootGuid }).transact();
+ guids.push(bmGuid);
+ });
+
+ let originalInfos = [];
+ for (let guid of guids) {
+ originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid));
+ }
+
+ yield PT.Remove(guids).transact();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Undo the New* transactions batch.
+ yield PT.undo();
+ yield ensureNonExistent(...guids);
+
+ // Redo it.
+ yield PT.redo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Redo remove.
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+
+ // Cleanup
+ yield PT.clearTransactionsHistory();
+ observer.reset();
+});
+
+add_task(function* test_remove_bookmarks_for_urls() {
+ let urls = [new URL("http://test.url.1"), new URL("http://test.url.2")];
+ let guids = [];
+ yield PT.batch(function* () {
+ for (let url of urls) {
+ for (let title of ["test title a", "test title b"]) {
+ let txn = PT.NewBookmark({ url, title, parentGuid: rootGuid });
+ guids.push(yield txn.transact());
+ }
+ }
+ });
+
+ let originalInfos = [];
+ for (let guid of guids) {
+ originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid));
+ }
+
+ yield PT.RemoveBookmarksForUrls(urls).transact();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Cleanup.
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+ yield PT.clearTransactionsHistory();
+ observer.reset();
+});
diff --git a/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js b/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js
new file mode 100644
index 0000000000..7d5df565f3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js
@@ -0,0 +1,39 @@
+/* -*- 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/. */
+
+/**
+ * Added with bug 508102 to make sure that calling stopSearch on our
+ * AutoComplete implementation does not throw.
+ */
+
+// Globals and Constants
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"].
+ getService(Ci.nsIAutoCompleteSearch);
+
+// Test Functions
+
+function test_stopSearch()
+{
+ try {
+ ac.stopSearch();
+ }
+ catch (e) {
+ do_throw("we should not have caught anything!");
+ }
+}
+
+// Test Runner
+
+var tests = [
+ test_stopSearch,
+];
+function run_test()
+{
+ tests.forEach(test => test());
+}
diff --git a/toolkit/components/places/tests/unit/test_bookmark_catobs.js b/toolkit/components/places/tests/unit/test_bookmark_catobs.js
new file mode 100644
index 0000000000..e2b589090f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmark_catobs.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ run_next_test()
+}
+
+add_task(function* test_observers() {
+ do_load_manifest("nsDummyObserver.manifest");
+
+ let dummyCreated = false;
+ let dummyReceivedOnItemAdded = false;
+
+ Services.obs.addObserver(function created() {
+ Services.obs.removeObserver(created, "dummy-observer-created");
+ dummyCreated = true;
+ }, "dummy-observer-created", false);
+ Services.obs.addObserver(function added() {
+ Services.obs.removeObserver(added, "dummy-observer-item-added");
+ dummyReceivedOnItemAdded = true;
+ }, "dummy-observer-item-added", false);
+
+ let initialObservers = PlacesUtils.bookmarks.getObservers();
+
+ // Add a common observer, it should be invoked after the category observer.
+ let notificationsPromised = new Promise((resolve, reject) => {
+ PlacesUtils.bookmarks.addObserver( {
+ __proto__: NavBookmarkObserver.prototype,
+ onItemAdded() {
+ let observers = PlacesUtils.bookmarks.getObservers();
+ Assert.equal(observers.length, initialObservers.length + 1);
+
+ // Check the common observer is the last one.
+ for (let i = 0; i < initialObservers.length; ++i) {
+ Assert.equal(initialObservers[i], observers[i]);
+ }
+
+ PlacesUtils.bookmarks.removeObserver(this);
+ observers = PlacesUtils.bookmarks.getObservers();
+ Assert.equal(observers.length, initialObservers.length);
+
+ // Check the category observer has been invoked before this one.
+ Assert.ok(dummyCreated);
+ Assert.ok(dummyReceivedOnItemAdded);
+ resolve();
+ }
+ }, false);
+ });
+
+ // Add a bookmark
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://typed.mozilla.org"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+
+ yield notificationsPromised;
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html.js b/toolkit/components/places/tests/unit/test_bookmarks_html.js
new file mode 100644
index 0000000000..b10dc61851
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js
@@ -0,0 +1,385 @@
+/* -*- 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/. */
+
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+// An object representing the contents of bookmarks.preplaces.html.
+var test_bookmarks = {
+ menu: [
+ { title: "Mozilla Firefox",
+ children: [
+ { title: "Help and Tutorials",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/help/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { title: "Customize Firefox",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { title: "Get Involved",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/community/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { title: "About Us",
+ url: "http://en-us.www.mozilla.com/en-US/about/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ }
+ ]
+ },
+ {
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR
+ },
+ { title: "test",
+ description: "folder test comment",
+ dateAdded: 1177541020000000,
+ lastModified: 1177541050000000,
+ children: [
+ { title: "test post keyword",
+ description: "item description",
+ dateAdded: 1177375336000000,
+ lastModified: 1177375423000000,
+ keyword: "test",
+ sidebar: true,
+ postData: "hidden1%3Dbar&text1%3D%25s",
+ charset: "ISO-8859-1",
+ url: "http://test/post"
+ }
+ ]
+ }
+ ],
+ toolbar: [
+ { title: "Getting Started",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/central/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { title: "Latest Headlines",
+ url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
+ }
+ ],
+ unfiled: [
+ { title: "Example.tld",
+ url: "http://example.tld/"
+ }
+ ]
+};
+
+// Pre-Places bookmarks.html file pointer.
+var gBookmarksFileOld;
+// Places bookmarks.html file pointer.
+var gBookmarksFileNew;
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* setup() {
+ // Avoid creating smart bookmarks during the test.
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+
+ // File pointer to legacy bookmarks file.
+ gBookmarksFileOld = do_get_file("bookmarks.preplaces.html");
+
+ // File pointer to a new Places-exported bookmarks file.
+ gBookmarksFileNew = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ gBookmarksFileNew.append("bookmarks.exported.html");
+ if (gBookmarksFileNew.exists()) {
+ gBookmarksFileNew.remove(false);
+ }
+
+ // This test must be the first one, since it setups the new bookmarks.html.
+ // Test importing a pre-Places canonical bookmarks file.
+ // 1. import bookmarks.preplaces.html
+ // 2. run the test-suite
+ // Note: we do not empty the db before this import to catch bugs like 380999
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_new()
+{
+ // Test importing a Places bookmarks.html file.
+ // 1. import bookmarks.exported.html
+ // 2. run the test-suite
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ yield testImportedBookmarks();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_emptytitle_export()
+{
+ // Test exporting and importing with an empty-titled bookmark.
+ // 1. import bookmarks
+ // 2. create an empty-titled bookmark.
+ // 3. export to bookmarks.exported.html
+ // 4. empty bookmarks db
+ // 5. import bookmarks.exported.html
+ // 6. run the test-suite
+ // 7. remove the empty-titled bookmark
+ // 8. export to bookmarks.exported.html
+ // 9. empty bookmarks db and continue
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ const NOTITLE_URL = "http://notitle.mozilla.org/";
+ let bookmark = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: NOTITLE_URL
+ });
+ test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+
+ // Cleanup.
+ test_bookmarks.unfiled.pop();
+ // HTML imports don't restore GUIDs yet.
+ let reimportedBookmark = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+ Assert.equal(reimportedBookmark.url.href, bookmark.url.href);
+ yield PlacesUtils.bookmarks.remove(reimportedBookmark);
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_chromefavicon()
+{
+ // Test exporting and importing with a bookmark pointing to a chrome favicon.
+ // 1. import bookmarks
+ // 2. create a bookmark pointing to a chrome favicon.
+ // 3. export to bookmarks.exported.html
+ // 4. empty bookmarks db
+ // 5. import bookmarks.exported.html
+ // 6. run the test-suite
+ // 7. remove the bookmark pointing to a chrome favicon.
+ // 8. export to bookmarks.exported.html
+ // 9. empty bookmarks db and continue
+
+ const PAGE_URI = NetUtil.newURI("http://example.com/chromefavicon_page");
+ const CHROME_FAVICON_URI = NetUtil.newURI("chrome://global/skin/icons/information-16.png");
+ const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png");
+
+ do_print("Importing from html");
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Insert bookmark");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: PAGE_URI,
+ title: "Test"
+ });
+
+ do_print("Set favicon");
+ yield new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI, CHROME_FAVICON_URI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ let data = yield new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(
+ PAGE_URI, (uri, dataLen, faviconData, mimeType) => resolve(faviconData));
+ });
+
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+
+ test_bookmarks.unfiled.push(
+ { title: "Test", url: PAGE_URI.spec, icon: base64Icon });
+
+ do_print("Export to html");
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Set favicon");
+ // Change the favicon to check it's really imported again later.
+ yield new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI, CHROME_FAVICON_URI_2, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ do_print("import from html");
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Test imported bookmarks");
+ yield testImportedBookmarks();
+
+ // Cleanup.
+ test_bookmarks.unfiled.pop();
+ // HTML imports don't restore GUIDs yet.
+ let reimportedBookmark = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+ yield PlacesUtils.bookmarks.remove(reimportedBookmark);
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_ontop()
+{
+ // Test importing the exported bookmarks.html file *on top of* the existing
+ // bookmarks.
+ // 1. empty bookmarks db
+ // 2. import the exported bookmarks file
+ // 3. export to file
+ // 3. import the exported bookmarks file
+ // 4. run the test-suite
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+function* testImportedBookmarks()
+{
+ for (let group in test_bookmarks) {
+ do_print("[testImportedBookmarks()] Checking group '" + group + "'");
+
+ let root;
+ switch (group) {
+ case "menu":
+ root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ break;
+ case "toolbar":
+ root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+ break;
+ case "unfiled":
+ root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ break;
+ }
+
+ let items = test_bookmarks[group];
+ do_check_eq(root.childCount, items.length);
+
+ for (let key in items) {
+ yield checkItem(items[key], root.getChild(key));
+ }
+
+ root.containerOpen = false;
+ }
+}
+
+function* checkItem(aExpected, aNode)
+{
+ let id = aNode.itemId;
+
+ return Task.spawn(function* () {
+ for (prop in aExpected) {
+ switch (prop) {
+ case "type":
+ do_check_eq(aNode.type, aExpected.type);
+ break;
+ case "title":
+ do_check_eq(aNode.title, aExpected.title);
+ break;
+ case "description":
+ do_check_eq(PlacesUtils.annotations
+ .getItemAnnotation(id, DESCRIPTION_ANNO),
+ aExpected.description);
+ break;
+ case "dateAdded":
+ do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(id),
+ aExpected.dateAdded);
+ break;
+ case "lastModified":
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(id),
+ aExpected.lastModified);
+ break;
+ case "url":
+ if (!("feedUrl" in aExpected))
+ do_check_eq(aNode.uri, aExpected.url)
+ break;
+ case "icon":
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.getFaviconDataForPage(
+ NetUtil.newURI(aExpected.url),
+ function (aURI, aDataLen, aData, aMimeType) {
+ deferred.resolve(aData);
+ });
+ let data = yield deferred.promise;
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+ do_check_true(base64Icon == aExpected.icon);
+ break;
+ case "keyword": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.keyword, aExpected.keyword);
+ break;
+ }
+ case "sidebar":
+ do_check_eq(PlacesUtils.annotations
+ .itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ aExpected.sidebar);
+ break;
+ case "postData": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.postData, aExpected.postData);
+ break;
+ }
+ case "charset":
+ let testURI = NetUtil.newURI(aNode.uri);
+ do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
+ break;
+ case "feedUrl":
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
+ do_check_eq(livemark.siteURI.spec, aExpected.url);
+ do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
+ break;
+ case "children":
+ let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(folder.hasChildren, aExpected.children.length > 0);
+ folder.containerOpen = true;
+ do_check_eq(folder.childCount, aExpected.children.length);
+
+ for (let index = 0; index < aExpected.children.length; index++) {
+ yield checkItem(aExpected.children[index], folder.getChild(index));
+ }
+
+ folder.containerOpen = false;
+ break;
+ default:
+ throw new Error("Unknown property");
+ }
+ }
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
new file mode 100644
index 0000000000..845b2227ba
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
@@ -0,0 +1,143 @@
+/*
+ * This test ensures that importing/exporting to HTML does not stop
+ * if a malformed uri is found.
+ */
+
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+
+const TEST_FAVICON_PAGE_URL = "http://en-US.www.mozilla.com/en-US/firefox/central/";
+const TEST_FAVICON_DATA_SIZE = 580;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_corrupt_file() {
+ // avoid creating the places smart folder during tests
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+
+ // Import bookmarks from the corrupt file.
+ let corruptHtml = OS.Path.join(do_get_cwd().path, "bookmarks.corrupt.html");
+ yield BookmarkHTMLUtils.importFromFile(corruptHtml, true);
+
+ // Check that bookmarks that are not corrupt have been imported.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield database_check();
+});
+
+add_task(function* test_corrupt_database() {
+ // Create corruption in the database, then export.
+ let corruptBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://test.mozilla.org",
+ title: "We love belugas" });
+ yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) {
+ yield db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid",
+ { guid: corruptBookmark.guid });
+ }));
+
+ let bookmarksFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.html");
+ if ((yield OS.File.exists(bookmarksFile)))
+ yield OS.File.remove(bookmarksFile);
+ yield BookmarkHTMLUtils.exportToFile(bookmarksFile);
+
+ // Import again and check for correctness.
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkHTMLUtils.importFromFile(bookmarksFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield database_check();
+});
+
+/*
+ * Check for imported bookmarks correctness
+ *
+ * @return {Promise}
+ * @resolves When the checks are finished.
+ * @rejects Never.
+ */
+var database_check = Task.async(function* () {
+ // BOOKMARKS MENU
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ Assert.equal(root.childCount, 2);
+
+ let folderNode = root.getChild(1);
+ Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ Assert.equal(folderNode.title, "test");
+ Assert.equal(PlacesUtils.bookmarks.getItemDateAdded(folderNode.itemId), 1177541020000000);
+ Assert.equal(PlacesUtils.bookmarks.getItemLastModified(folderNode.itemId), 1177541050000000);
+ Assert.equal("folder test comment",
+ PlacesUtils.annotations.getItemAnnotation(folderNode.itemId,
+ DESCRIPTION_ANNO));
+ // open test folder, and test the children
+ PlacesUtils.asQuery(folderNode);
+ Assert.equal(folderNode.hasChildren, true);
+ folderNode.containerOpen = true;
+ Assert.equal(folderNode.childCount, 1);
+
+ let bookmarkNode = folderNode.getChild(0);
+ Assert.equal("http://test/post", bookmarkNode.uri);
+ Assert.equal("test post keyword", bookmarkNode.title);
+
+ let entry = yield PlacesUtils.keywords.fetch({ url: bookmarkNode.uri });
+ Assert.equal("test", entry.keyword);
+ Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData);
+
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(bookmarkNode.itemId,
+ LOAD_IN_SIDEBAR_ANNO));
+ Assert.equal(bookmarkNode.dateAdded, 1177375336000000);
+ Assert.equal(bookmarkNode.lastModified, 1177375423000000);
+
+ Assert.equal((yield PlacesUtils.getCharsetForURI(NetUtil.newURI(bookmarkNode.uri))),
+ "ISO-8859-1");
+
+ Assert.equal("item description",
+ PlacesUtils.annotations.getItemAnnotation(bookmarkNode.itemId,
+ DESCRIPTION_ANNO));
+
+ // clean up
+ folderNode.containerOpen = false;
+ root.containerOpen = false;
+
+ // BOOKMARKS TOOLBAR
+ root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+ Assert.equal(root.childCount, 3);
+
+ // For now some promises are resolved later, so we can't guarantee an order.
+ let foundLivemark = false;
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ if (node.title == "Latest Headlines") {
+ foundLivemark = true;
+ Assert.equal("Latest Headlines", node.title);
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ guid: node.bookmarkGuid });
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ livemark.siteURI.spec);
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+ livemark.feedURI.spec);
+ }
+ }
+ Assert.ok(foundLivemark);
+
+ // cleanup
+ root.containerOpen = false;
+
+ // UNFILED BOOKMARKS
+ root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ Assert.equal(root.childCount, 1);
+ root.containerOpen = false;
+
+ // favicons
+ yield new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(uri(TEST_FAVICON_PAGE_URL),
+ (aURI, aDataLen, aData, aMimeType) => {
+ // aURI should never be null when aDataLen > 0.
+ Assert.notEqual(aURI, null);
+ // Favicon data is stored in the bookmarks file as a "data:" URI. For
+ // simplicity, instead of converting the data we receive to a "data:" URI
+ // and comparing it, we just check the data size.
+ Assert.equal(TEST_FAVICON_DATA_SIZE, aDataLen);
+ resolve();
+ });
+ });
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
new file mode 100644
index 0000000000..e4ba433a34
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
@@ -0,0 +1,57 @@
+var bookmarkData = [
+ { uri: uri("http://www.toastytech.com"),
+ title: "Nathan's Toasty Technology Page",
+ tags: ["technology", "personal", "retro"] },
+ { uri: uri("http://www.reddit.com"),
+ title: "reddit: the front page of the internet",
+ tags: ["social media", "news", "humour"] },
+ { uri: uri("http://www.4chan.org"),
+ title: "4chan",
+ tags: ["discussion", "imageboard", "anime"] }
+];
+
+/*
+ TEST SUMMARY
+ - Add bookmarks with tags
+ - Export tagged bookmarks as HTML file
+ - Delete bookmarks
+ - Import bookmarks from HTML file
+ - Check that all bookmarks are successfully imported with tags
+*/
+
+add_task(function* test_import_tags() {
+ // Removes bookmarks.html if the file already exists.
+ let HTMLFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html");
+ if ((yield OS.File.exists(HTMLFile)))
+ yield OS.File.remove(HTMLFile);
+
+ // Adds bookmarks and tags to the database.
+ let bookmarkList = new Set();
+ for (let { uri, title, tags } of bookmarkData) {
+ bookmarkList.add(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title }));
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+
+ // Exports the bookmarks as a HTML file.
+ yield BookmarkHTMLUtils.exportToFile(HTMLFile);
+
+ // Deletes bookmarks and tags from the database.
+ for (let bookmark of bookmarkList) {
+ yield PlacesUtils.bookmarks.remove(bookmark.guid);
+ }
+
+ // Re-imports the bookmarks from the HTML file.
+ yield BookmarkHTMLUtils.importFromFile(HTMLFile, true);
+
+ // Tests to ensure that the tags are still present for each bookmark URI.
+ for (let { uri, tags } of bookmarkData) {
+ do_print("Test tags for " + uri.spec + ": " + tags + "\n");
+ let foundTags = PlacesUtils.tagging.getTagsForURI(uri);
+ Assert.equal(foundTags.length, tags.length);
+ Assert.ok(tags.every(tag => foundTags.includes(tag)));
+ }
+});
+
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
new file mode 100644
index 0000000000..02b430ff23
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+
+// Test for bug #801450
+
+// Get Services
+Cu.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_bookmarks_html_singleframe()
+{
+ let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks_html_singleframe.html");
+ yield BookmarkHTMLUtils.importFromFile(bookmarksFile, true);
+
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ do_check_eq(root.childCount, 1);
+ let folder = root.getChild(0);
+ PlacesUtils.asContainer(folder).containerOpen = true;
+ do_check_eq(folder.title, "Subtitle");
+ do_check_eq(folder.childCount, 1);
+ let bookmark = folder.getChild(0);
+ do_check_eq(bookmark.uri, "http://www.mozilla.org/");
+ do_check_eq(bookmark.title, "Mozilla");
+ folder.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_json.js b/toolkit/components/places/tests/unit/test_bookmarks_json.js
new file mode 100644
index 0000000000..a6801540a7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+// An object representing the contents of bookmarks.json.
+var test_bookmarks = {
+ menu: [
+ { guid: "OCyeUO5uu9FF",
+ title: "Mozilla Firefox",
+ children: [
+ { guid:"OCyeUO5uu9FG",
+ title: "Help and Tutorials",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/help/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { guid:"OCyeUO5uu9FH",
+ title: "Customize Firefox",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { guid:"OCyeUO5uu9FI",
+ title: "Get Involved",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/community/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { guid:"OCyeUO5uu9FJ",
+ title: "About Us",
+ url: "http://en-us.www.mozilla.com/en-US/about/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ }
+ ]
+ },
+ {
+ guid: "OCyeUO5uu9FK",
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR
+ },
+ {
+ guid:"OCyeUO5uu9FL",
+ title: "test",
+ description: "folder test comment",
+ dateAdded: 1177541020000000,
+ // lastModified: 1177541050000000,
+ children: [
+ { guid:"OCyeUO5uu9GX",
+ title: "test post keyword",
+ description: "item description",
+ dateAdded: 1177375336000000,
+ // lastModified: 1177375423000000,
+ keyword: "test",
+ sidebar: true,
+ postData: "hidden1%3Dbar&text1%3D%25s",
+ charset: "ISO-8859-1"
+ }
+ ]
+ }
+ ],
+ toolbar: [
+ { guid: "OCyeUO5uu9FB",
+ title: "Getting Started",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/central/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ { guid:"OCyeUO5uu9FR",
+ title: "Latest Headlines",
+ url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
+ }
+ ],
+ unfiled: [
+ { guid: "OCyeUO5uu9FW",
+ title: "Example.tld",
+ url: "http://example.tld/"
+ }
+ ]
+};
+
+// Exported bookmarks file pointer.
+var bookmarksExportedFile;
+
+add_task(function* test_import_bookmarks() {
+ let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks.json");
+
+ yield BookmarkJSONUtils.importFromFile(bookmarksFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+});
+
+add_task(function* test_export_bookmarks() {
+ bookmarksExportedFile = OS.Path.join(OS.Constants.Path.profileDir,
+ "bookmarks.exported.json");
+ yield BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_import_exported_bookmarks() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+});
+
+add_task(function* test_import_ontop() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+});
+
+add_task(function* test_clean() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+function* testImportedBookmarks() {
+ for (let group in test_bookmarks) {
+ do_print("[testImportedBookmarks()] Checking group '" + group + "'");
+
+ let root;
+ switch (group) {
+ case "menu":
+ root =
+ PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ break;
+ case "toolbar":
+ root =
+ PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+ break;
+ case "unfiled":
+ root =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ break;
+ }
+
+ let items = test_bookmarks[group];
+ do_check_eq(root.childCount, items.length);
+
+ for (let key in items) {
+ yield checkItem(items[key], root.getChild(key));
+ }
+
+ root.containerOpen = false;
+ }
+}
+
+function* checkItem(aExpected, aNode) {
+ let id = aNode.itemId;
+
+ return Task.spawn(function* () {
+ for (prop in aExpected) {
+ switch (prop) {
+ case "type":
+ do_check_eq(aNode.type, aExpected.type);
+ break;
+ case "title":
+ do_check_eq(aNode.title, aExpected.title);
+ break;
+ case "description":
+ do_check_eq(PlacesUtils.annotations.getItemAnnotation(
+ id, DESCRIPTION_ANNO), aExpected.description);
+ break;
+ case "dateAdded":
+ do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(id),
+ aExpected.dateAdded);
+ break;
+ case "lastModified":
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(id),
+ aExpected.lastModified);
+ break;
+ case "url":
+ if (!("feedUrl" in aExpected))
+ do_check_eq(aNode.uri, aExpected.url);
+ break;
+ case "icon":
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.getFaviconDataForPage(
+ NetUtil.newURI(aExpected.url),
+ function (aURI, aDataLen, aData, aMimeType) {
+ deferred.resolve(aData);
+ });
+ let data = yield deferred.promise;
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+ do_check_true(base64Icon == aExpected.icon);
+ break;
+ case "keyword": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.keyword, aExpected.keyword);
+ break;
+ }
+ case "guid":
+ let guid = yield PlacesUtils.promiseItemGuid(id);
+ do_check_eq(guid, aExpected.guid);
+ break;
+ case "sidebar":
+ do_check_eq(PlacesUtils.annotations.itemHasAnnotation(
+ id, LOAD_IN_SIDEBAR_ANNO), aExpected.sidebar);
+ break;
+ case "postData": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.postData, aExpected.postData);
+ break;
+ }
+ case "charset":
+ let testURI = NetUtil.newURI(aNode.uri);
+ do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
+ break;
+ case "feedUrl":
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
+ do_check_eq(livemark.siteURI.spec, aExpected.url);
+ do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
+ break;
+ case "children":
+ let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(folder.hasChildren, aExpected.children.length > 0);
+ folder.containerOpen = true;
+ do_check_eq(folder.childCount, aExpected.children.length);
+
+ for (let index = 0; index < aExpected.children.length; index++) {
+ yield checkItem(aExpected.children[index], folder.getChild(index));
+ }
+
+ folder.containerOpen = false;
+ break;
+ default:
+ throw new Error("Unknown property");
+ }
+ }
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
new file mode 100644
index 0000000000..2f8022c6b6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
@@ -0,0 +1,325 @@
+/* -*- 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/. */
+
+Cu.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+
+/**
+ * Tests the bookmarks-restore-* nsIObserver notifications after restoring
+ * bookmarks from JSON and HTML. See bug 470314.
+ */
+
+// The topics and data passed to nsIObserver.observe() on bookmarks restore
+const NSIOBSERVER_TOPIC_BEGIN = "bookmarks-restore-begin";
+const NSIOBSERVER_TOPIC_SUCCESS = "bookmarks-restore-success";
+const NSIOBSERVER_TOPIC_FAILED = "bookmarks-restore-failed";
+const NSIOBSERVER_DATA_JSON = "json";
+const NSIOBSERVER_DATA_HTML = "html";
+const NSIOBSERVER_DATA_HTML_INIT = "html-initial";
+
+// Bookmarks are added for these URIs
+var uris = [
+ "http://example.com/1",
+ "http://example.com/2",
+ "http://example.com/3",
+ "http://example.com/4",
+ "http://example.com/5",
+];
+
+/**
+ * Adds some bookmarks for the URIs in |uris|.
+ */
+function* addBookmarks() {
+ for (let url of uris) {
+ yield PlacesUtils.bookmarks.insert({
+ url: url, parentGuid: PlacesUtils.bookmarks.menuGuid
+ })
+ }
+ checkBookmarksExist();
+}
+
+/**
+ * Checks that all of the bookmarks created for |uris| exist. It works by
+ * creating one query per URI and then ORing all the queries. The number of
+ * results returned should be uris.length.
+ */
+function checkBookmarksExist() {
+ let hs = PlacesUtils.history;
+ let queries = uris.map(function (u) {
+ let q = hs.getNewQuery();
+ q.uri = uri(u);
+ return q;
+ });
+ let options = hs.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = hs.executeQueries(queries, uris.length, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, uris.length);
+ root.containerOpen = false;
+}
+
+/**
+ * Creates an file in the profile directory.
+ *
+ * @param aBasename
+ * e.g., "foo.txt" in the path /some/long/path/foo.txt
+ * @return {Promise}
+ * @resolves to an OS.File path
+ */
+function promiseFile(aBasename) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, aBasename);
+ do_print("opening " + path);
+ return OS.File.open(path, { truncate: true })
+ .then(aFile => {
+ aFile.close();
+ return path;
+ });
+}
+
+/**
+ * Register observers via promiseTopicObserved helper.
+ *
+ * @param {boolean} expectSuccess pass true when expect a success notification
+ * @return {Promise[]}
+ */
+function registerObservers(expectSuccess) {
+ let promiseBegin = promiseTopicObserved(NSIOBSERVER_TOPIC_BEGIN);
+ let promiseResult;
+ if (expectSuccess) {
+ promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_SUCCESS);
+ } else {
+ promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_FAILED);
+ }
+
+ return [promiseBegin, promiseResult];
+}
+
+/**
+ * Check notification results.
+ *
+ * @param {Promise[]} expectPromises array contain promiseBegin and promiseResult
+ * @param {object} expectedData contain data and folderId
+ */
+function* checkObservers(expectPromises, expectedData) {
+ let [promiseBegin, promiseResult] = expectPromises;
+
+ let beginData = (yield promiseBegin)[1];
+ Assert.equal(beginData, expectedData.data,
+ "Data for current test should be what is expected");
+
+ let [resultSubject, resultData] = yield promiseResult;
+ Assert.equal(resultData, expectedData.data,
+ "Data for current test should be what is expected");
+
+ // Make sure folder ID is what is expected. For importing HTML into a
+ // folder, this will be an integer, otherwise null.
+ if (resultSubject) {
+ Assert.equal(aSubject.QueryInterface(Ci.nsISupportsPRInt64).data,
+ expectedData.folderId);
+ } else {
+ Assert.equal(expectedData.folderId, null);
+ }
+}
+
+/**
+ * Run after every test cases.
+ */
+function* teardown(file, begin, success, fail) {
+ // On restore failed, file may not exist, so wrap in try-catch.
+ try {
+ yield OS.File.remove(file, {ignoreAbsent: true});
+ } catch (e) {}
+
+ // clean up bookmarks
+ yield PlacesUtils.bookmarks.eraseEverything();
+}
+
+add_task(function* test_json_restore_normal() {
+ // data: the data passed to nsIObserver.observe() corresponding to the test
+ // folderId: for HTML restore into a folder, the folder ID to restore into;
+ // otherwise, set it to null
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("JSON restore: normal restore should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.json");
+ yield addBookmarks();
+
+ yield BookmarkJSONUtils.exportToFile(file);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ try {
+ yield BookmarkJSONUtils.importFromFile(file, true);
+ } catch (e) {
+ do_throw(" Restore should not have failed" + e);
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_json_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("JSON restore: empty file should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.json");
+ try {
+ yield BookmarkJSONUtils.importFromFile(file, true);
+ } catch (e) {
+ do_throw(" Restore should not have failed" + e);
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_json_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null
+ }
+ let expectPromises = registerObservers(false);
+
+ do_print("JSON restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ file.append("this file doesn't exist because nobody created it 1");
+ try {
+ yield BookmarkJSONUtils.importFromFile(file, true);
+ do_throw(" Restore should have failed");
+ } catch (e) {}
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_restore_normal() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML restore: normal restore should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.html");
+ yield addBookmarks();
+ yield BookmarkHTMLUtils.exportToFile(file);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ try {
+ BookmarkHTMLUtils.importFromFile(file, false)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML restore: empty file should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.init.html");
+ try {
+ BookmarkHTMLUtils.importFromFile(file, false)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null
+ }
+ let expectPromises = registerObservers(false);
+
+ do_print("HTML restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ file.append("this file doesn't exist because nobody created it 2");
+ try {
+ yield BookmarkHTMLUtils.importFromFile(file, false);
+ do_throw("Should fail!");
+ } catch (e) {}
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_init_restore_normal() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML initial restore: normal restore should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.init.html");
+ yield addBookmarks();
+ yield BookmarkHTMLUtils.exportToFile(file);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ try {
+ BookmarkHTMLUtils.importFromFile(file, true)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_init_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML initial restore: empty file should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.init.html");
+ try {
+ BookmarkHTMLUtils.importFromFile(file, true)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_init_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null
+ }
+ let expectPromises = registerObservers(false);
+
+ do_print("HTML initial restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ file.append("this file doesn't exist because nobody created it 3");
+ try {
+ yield BookmarkHTMLUtils.importFromFile(file, true);
+ do_throw("Should fail!");
+ } catch (e) {}
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
new file mode 100644
index 0000000000..959dfe85f9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
@@ -0,0 +1,44 @@
+/* -*- 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/. */
+
+/**
+ * Both SetItemtitle and insertBookmark should allow for null titles.
+ */
+
+const bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+const TEST_URL = "http://www.mozilla.org";
+
+function run_test() {
+ // Insert a bookmark with an empty title.
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri(TEST_URL),
+ bs.DEFAULT_INDEX,
+ "");
+ // Check returned title is an empty string.
+ do_check_eq(bs.getItemTitle(itemId), "");
+ // Set title to null.
+ bs.setItemTitle(itemId, null);
+ // Check returned title is null.
+ do_check_eq(bs.getItemTitle(itemId), null);
+ // Cleanup.
+ bs.removeItem(itemId);
+
+ // Insert a bookmark with a null title.
+ itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri(TEST_URL),
+ bs.DEFAULT_INDEX,
+ null);
+ // Check returned title is null.
+ do_check_eq(bs.getItemTitle(itemId), null);
+ // Set title to an empty string.
+ bs.setItemTitle(itemId, "");
+ // Check returned title is an empty string.
+ do_check_eq(bs.getItemTitle(itemId), "");
+ // Cleanup.
+ bs.removeItem(itemId);
+}
diff --git a/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js
new file mode 100644
index 0000000000..b67e141e66
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("http://1.moz.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 1"
+ );
+ let id1 = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("place:folder=1234"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Shortcut 1"
+ );
+ let id2 = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("place:folder=-1"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Shortcut 2"
+ );
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("http://2.moz.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 2"
+ );
+
+ // Add also a simple visit.
+ yield PlacesTestUtils.addVisits(uri(("http://3.moz.org/")));
+
+ // Query containing a broken folder shortcuts among results.
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.unfiledBookmarksFolderId], 1);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ do_check_eq(root.childCount, 4);
+
+ let shortcut = root.getChild(1);
+ do_check_eq(shortcut.uri, "place:folder=1234");
+ PlacesUtils.asContainer(shortcut);
+ shortcut.containerOpen = true;
+ do_check_eq(shortcut.childCount, 0);
+ shortcut.containerOpen = false;
+ // Remove the broken shortcut while the containing result is open.
+ PlacesUtils.bookmarks.removeItem(id1);
+ do_check_eq(root.childCount, 3);
+
+ shortcut = root.getChild(1);
+ do_check_eq(shortcut.uri, "place:folder=-1");
+ PlacesUtils.asContainer(shortcut);
+ shortcut.containerOpen = true;
+ do_check_eq(shortcut.childCount, 0);
+ shortcut.containerOpen = false;
+ // Remove the broken shortcut while the containing result is open.
+ PlacesUtils.bookmarks.removeItem(id2);
+ do_check_eq(root.childCount, 2);
+
+ root.containerOpen = false;
+
+ // Broken folder shortcut as root node.
+ query = PlacesUtils.history.getNewQuery();
+ query.setFolders([1234], 1);
+ options = PlacesUtils.history.getNewQueryOptions();
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ // Broken folder shortcut as root node with folder=-1.
+ query = PlacesUtils.history.getNewQuery();
+ query.setFolders([-1], 1);
+ options = PlacesUtils.history.getNewQueryOptions();
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_browserhistory.js b/toolkit/components/places/tests/unit/test_browserhistory.js
new file mode 100644
index 0000000000..5f88c26e3a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_browserhistory.js
@@ -0,0 +1,129 @@
+/* -*- 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/. */
+
+const TEST_URI = NetUtil.newURI("http://mozilla.com/");
+const TEST_SUBDOMAIN_URI = NetUtil.newURI("http://foobar.mozilla.com/");
+
+add_task(function* test_addPage() {
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePage() {
+ PlacesUtils.bhistory.removePage(TEST_URI);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePages() {
+ let pages = [];
+ for (let i = 0; i < 8; i++) {
+ pages.push(NetUtil.newURI(TEST_URI.spec + i));
+ }
+
+ yield PlacesTestUtils.addVisits(pages.map(uri => ({ uri: uri })));
+ // Bookmarked item should not be removed from moz_places.
+ const ANNO_INDEX = 1;
+ const ANNO_NAME = "testAnno";
+ const ANNO_VALUE = "foo";
+ const BOOKMARK_INDEX = 2;
+ PlacesUtils.annotations.setPageAnnotation(pages[ANNO_INDEX],
+ ANNO_NAME, ANNO_VALUE, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ pages[BOOKMARK_INDEX],
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test bookmark");
+ PlacesUtils.annotations.setPageAnnotation(pages[BOOKMARK_INDEX],
+ ANNO_NAME, ANNO_VALUE, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+
+ PlacesUtils.bhistory.removePages(pages, pages.length);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+
+ // Check that the bookmark and its annotation still exist.
+ do_check_true(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) > 0);
+ do_check_eq(PlacesUtils.annotations.getPageAnnotation(pages[BOOKMARK_INDEX], ANNO_NAME),
+ ANNO_VALUE);
+
+ // Check the annotation on the non-bookmarked page does not exist anymore.
+ try {
+ PlacesUtils.annotations.getPageAnnotation(pages[ANNO_INDEX], ANNO_NAME);
+ do_throw("did not expire expire_never anno on a not bookmarked item");
+ } catch (ex) {}
+
+ // Cleanup.
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_removePagesByTimeframe() {
+ let visits = [];
+ let startDate = (Date.now() - 10000) * 1000;
+ for (let i = 0; i < 10; i++) {
+ visits.push({
+ uri: NetUtil.newURI(TEST_URI.spec + i),
+ visitDate: startDate + i * 1000
+ });
+ }
+
+ yield PlacesTestUtils.addVisits(visits);
+
+ // Delete all pages except the first and the last.
+ PlacesUtils.bhistory.removePagesByTimeframe(startDate + 1000, startDate + 8000);
+
+ // Check that we have removed the correct pages.
+ for (let i = 0; i < 10; i++) {
+ do_check_eq(page_in_database(NetUtil.newURI(TEST_URI.spec + i)) == 0,
+ i > 0 && i < 9);
+ }
+
+ // Clear remaining items and check that all pages have been removed.
+ PlacesUtils.bhistory.removePagesByTimeframe(startDate, startDate + 9000);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePagesFromHost() {
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ PlacesUtils.bhistory.removePagesFromHost("mozilla.com", true);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePagesFromHost_keepSubdomains() {
+ yield PlacesTestUtils.addVisits([{ uri: TEST_URI }, { uri: TEST_SUBDOMAIN_URI }]);
+ PlacesUtils.bhistory.removePagesFromHost("mozilla.com", false);
+ do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_history_clear() {
+ yield PlacesTestUtils.clearHistory();
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_getObservers() {
+ // Ensure that getObservers() invalidates the hasHistoryEntries cache.
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
+ // This is just for testing purposes, never do it.
+ return new Promise((resolve, reject) => {
+ DBConn().executeSimpleSQLAsync("DELETE FROM moz_historyvisits", {
+ handleError: function(error) {
+ reject(error);
+ },
+ handleResult: function(result) {
+ },
+ handleCompletion: function(result) {
+ // Just invoking getObservers should be enough to invalidate the cache.
+ PlacesUtils.history.getObservers();
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+ resolve();
+ }
+ });
+ });
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js
new file mode 100644
index 0000000000..a7ad1257ad
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js
@@ -0,0 +1,35 @@
+// Test that asking for a livemark in a annotationChanged notification works.
+add_task(function* () {
+ let annoPromise = new Promise(resolve => {
+ let annoObserver = {
+ onItemAnnotationSet(id, name) {
+ if (name == PlacesUtils.LMANNO_FEEDURI) {
+ PlacesUtils.annotations.removeObserver(this);
+ resolve();
+ }
+ },
+ onItemAnnotationRemoved() {},
+ onPageAnnotationSet() {},
+ onPageAnnotationRemoved() {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAnnotationObserver
+ ]),
+ };
+ PlacesUtils.annotations.addObserver(annoObserver, false);
+ });
+
+
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "livemark title"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ , siteURI: uri("http://example.com/")
+ , feedURI: uri("http://example.com/rdf")
+ });
+
+ yield annoPromise;
+
+ livemark = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ Assert.ok(livemark);
+ yield PlacesUtils.livemarks.removeLivemark({ guid: livemark.guid });
+});
diff --git a/toolkit/components/places/tests/unit/test_childlessTags.js b/toolkit/components/places/tests/unit/test_childlessTags.js
new file mode 100644
index 0000000000..4c3e38fa45
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_childlessTags.js
@@ -0,0 +1,117 @@
+/* -*- 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/. */
+
+/**
+ * Ensures that removal of a bookmark untags the bookmark if it's no longer
+ * contained in any regular, non-tag folders. See bug 444849.
+ */
+
+// Add your tests here. Each is an object with a summary string |desc| and a
+// method run() that's called to run the test.
+var tests = [
+ {
+ desc: "Removing a tagged bookmark should cause the tag to be removed.",
+ run: function () {
+ print(" Make a bookmark.");
+ var bmId = bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder,
+ BOOKMARK_URI,
+ bmsvc.DEFAULT_INDEX,
+ "test bookmark");
+ do_check_true(bmId > 0);
+
+ print(" Tag it up.");
+ var tags = ["foo", "bar"];
+ tagssvc.tagURI(BOOKMARK_URI, tags);
+ ensureTagsExist(tags);
+
+ print(" Remove the bookmark. The tags should no longer exist.");
+ bmsvc.removeItem(bmId);
+ ensureTagsExist([]);
+ }
+ },
+
+ {
+ desc: "Removing a folder containing a tagged bookmark should cause the " +
+ "tag to be removed.",
+ run: function () {
+ print(" Make a folder.");
+ var folderId = bmsvc.createFolder(bmsvc.unfiledBookmarksFolder,
+ "test folder",
+ bmsvc.DEFAULT_INDEX);
+ do_check_true(folderId > 0);
+
+ print(" Stick a bookmark in the folder.");
+ var bmId = bmsvc.insertBookmark(folderId,
+ BOOKMARK_URI,
+ bmsvc.DEFAULT_INDEX,
+ "test bookmark");
+ do_check_true(bmId > 0);
+
+ print(" Tag the bookmark.");
+ var tags = ["foo", "bar"];
+ tagssvc.tagURI(BOOKMARK_URI, tags);
+ ensureTagsExist(tags);
+
+ print(" Remove the folder. The tags should no longer exist.");
+ bmsvc.removeItem(folderId);
+ ensureTagsExist([]);
+ }
+ }
+];
+
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+
+const BOOKMARK_URI = uri("http://example.com/");
+
+/**
+ * Runs a tag query and ensures that the tags returned are those and only those
+ * in aTags. aTags may be empty, in which case this function ensures that no
+ * tags exist.
+ *
+ * @param aTags
+ * An array of tags (strings)
+ */
+function ensureTagsExist(aTags) {
+ var query = histsvc.getNewQuery();
+ var opts = histsvc.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_TAG_QUERY;
+ var resultRoot = histsvc.executeQuery(query, opts).root;
+
+ // Dupe aTags.
+ var tags = aTags.slice(0);
+
+ resultRoot.containerOpen = true;
+
+ // Ensure that the number of tags returned from the query is the same as the
+ // number in |tags|.
+ do_check_eq(resultRoot.childCount, tags.length);
+
+ // For each tag result from the query, ensure that it's contained in |tags|.
+ // Remove the tag from |tags| so that we ensure the sets are equal.
+ for (let i = 0; i < resultRoot.childCount; i++) {
+ var tag = resultRoot.getChild(i).title;
+ var indexOfTag = tags.indexOf(tag);
+ do_check_true(indexOfTag >= 0);
+ tags.splice(indexOfTag, 1);
+ }
+
+ resultRoot.containerOpen = false;
+}
+
+function run_test()
+{
+ tests.forEach(function (test) {
+ print("Running test: " + test.desc);
+ test.run();
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_corrupt_telemetry.js b/toolkit/components/places/tests/unit/test_corrupt_telemetry.js
new file mode 100644
index 0000000000..cd9e9ec0c4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_corrupt_telemetry.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a request to forcibly
+// replace the current database.
+
+add_task(function* () {
+ let profileDBPath = yield OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
+ yield OS.File.remove(profileDBPath, {ignoreAbsent: true});
+ // Ensure that our database doesn't already exist.
+ Assert.ok(!(yield OS.File.exists(profileDBPath)), "places.sqlite shouldn't exist");
+ let dir = yield OS.File.getCurrentDirectory();
+ let src = OS.Path.join(dir, "corruptDB.sqlite");
+ yield OS.File.copy(src, profileDBPath);
+ Assert.ok(yield OS.File.exists(profileDBPath), "places.sqlite should exist");
+
+ let count = Services.telemetry
+ .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE")
+ .snapshot()
+ .counts[3];
+ Assert.equal(count, 0, "There should be no telemetry");
+
+ do_check_eq(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ count = Services.telemetry
+ .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE")
+ .snapshot()
+ .counts[3];
+ Assert.equal(count, 1, "Telemetry should have been added");
+});
diff --git a/toolkit/components/places/tests/unit/test_crash_476292.js b/toolkit/components/places/tests/unit/test_crash_476292.js
new file mode 100644
index 0000000000..8f08620224
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_crash_476292.js
@@ -0,0 +1,28 @@
+/* -*- 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/. */
+
+/**
+ * This tests a crash during startup found in bug 476292 that was caused by
+ * getting the bookmarks service during nsNavHistory::Init when the bookmarks
+ * service was created before the history service was.
+ */
+
+function run_test()
+{
+ // First, we need to move our old database file into our test profile
+ // directory. This will trigger DATABASE_STATUS_UPGRADED (CREATE is not
+ // sufficient since there will be no entries to update frecencies for, which
+ // causes us to get the bookmarks service in the first place).
+ let dbFile = do_get_file("bug476292.sqlite");
+ let profD = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile);
+ dbFile.copyTo(profD, "places.sqlite");
+
+ // Now get the bookmarks service. This will crash when the bug exists.
+ Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+}
diff --git a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
new file mode 100644
index 0000000000..e83d0fdaeb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a request to forcibly
+// replace the current database.
+
+function run_test() {
+ // Ensure that our database doesn't already exist.
+ let dbFile = gProfD.clone();
+ dbFile.append("places.sqlite");
+ do_check_false(dbFile.exists());
+
+ dbFile = gProfD.clone();
+ dbFile.append("places.sqlite.corrupt");
+ do_check_false(dbFile.exists());
+
+ let file = do_get_file("default.sqlite");
+ file.copyToFollowingLinks(gProfD, "places.sqlite");
+ file = gProfD.clone();
+ file.append("places.sqlite");
+
+ // Create some unique stuff to check later.
+ let db = Services.storage.openUnsharedDatabase(file);
+ db.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+ db.close();
+
+ Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
+ do_check_eq(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ dbFile = gProfD.clone();
+ dbFile.append("places.sqlite");
+ do_check_true(dbFile.exists());
+
+ // Check the new database is really a new one.
+ db = Services.storage.openUnsharedDatabase(file);
+ try {
+ db.executeSimpleSQL("DELETE * FROM test");
+ do_throw("The new database should not have our unique content");
+ } catch (ex) {}
+ db.close();
+
+ dbFile = gProfD.clone();
+ dbFile.append("places.sqlite.corrupt");
+ do_check_true(dbFile.exists());
+}
diff --git a/toolkit/components/places/tests/unit/test_download_history.js b/toolkit/components/places/tests/unit/test_download_history.js
new file mode 100644
index 0000000000..643360b20a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_download_history.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the nsIDownloadHistory Places implementation.
+ */
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
+ "@mozilla.org/browser/download-history;1",
+ "nsIDownloadHistory");
+
+const DOWNLOAD_URI = NetUtil.newURI("http://www.example.com/");
+const REFERRER_URI = NetUtil.newURI("http://www.example.org/");
+const PRIVATE_URI = NetUtil.newURI("http://www.example.net/");
+
+/**
+ * Waits for the first visit notification to be received.
+ *
+ * @param aCallback
+ * Callback function to be called with the same arguments of onVisit.
+ */
+function waitForOnVisit(aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onVisit: function HO_onVisit() {
+ PlacesUtils.history.removeObserver(this);
+ aCallback.apply(null, arguments);
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+/**
+ * Waits for the first onDeleteURI notification to be received.
+ *
+ * @param aCallback
+ * Callback function to be called with the same arguments of onDeleteURI.
+ */
+function waitForOnDeleteURI(aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onDeleteURI: function HO_onDeleteURI() {
+ PlacesUtils.history.removeObserver(this);
+ aCallback.apply(null, arguments);
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+/**
+ * Waits for the first onDeleteVisits notification to be received.
+ *
+ * @param aCallback
+ * Callback function to be called with the same arguments of onDeleteVisits.
+ */
+function waitForOnDeleteVisits(aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onDeleteVisits: function HO_onDeleteVisits() {
+ PlacesUtils.history.removeObserver(this);
+ aCallback.apply(null, arguments);
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_test(function test_dh_is_from_places()
+{
+ // Test that this nsIDownloadHistory is the one places implements.
+ do_check_true(gDownloadHistory instanceof Ci.mozIAsyncHistory);
+
+ run_next_test();
+});
+
+add_test(function test_dh_addRemoveDownload()
+{
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+
+ // Verify that the URI is already available in results at this time.
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ waitForOnDeleteURI(function DHRAD_onDeleteURI(aDeletedURI) {
+ do_check_true(aDeletedURI.equals(DOWNLOAD_URI));
+
+ // Verify that the URI is already available in results at this time.
+ do_check_false(!!page_in_database(DOWNLOAD_URI));
+
+ run_next_test();
+ });
+ gDownloadHistory.removeAllDownloads();
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+});
+
+add_test(function test_dh_addMultiRemoveDownload()
+{
+ PlacesTestUtils.addVisits({
+ uri: DOWNLOAD_URI,
+ transition: TRANSITION_TYPED
+ }).then(function () {
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aDeletedURI) {
+ do_check_true(aDeletedURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+ gDownloadHistory.removeAllDownloads();
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+ });
+});
+
+add_test(function test_dh_addBookmarkRemoveDownload()
+{
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ DOWNLOAD_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A bookmark");
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aDeletedURI) {
+ do_check_true(aDeletedURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+ gDownloadHistory.removeAllDownloads();
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+});
+
+add_test(function test_dh_addDownload_referrer()
+{
+ waitForOnVisit(function DHAD_prepareReferrer(aURI, aVisitID) {
+ do_check_true(aURI.equals(REFERRER_URI));
+ let referrerVisitId = aVisitID;
+
+ waitForOnVisit(function DHAD_onVisit(aVisitedURI, unused, unused2, unused3,
+ aReferringID) {
+ do_check_true(aVisitedURI.equals(DOWNLOAD_URI));
+ do_check_eq(aReferringID, referrerVisitId);
+
+ // Verify that the URI is already available in results at this time.
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
+ });
+
+ // Note that we don't pass the optional callback argument here because we must
+ // ensure that we receive the onVisit notification before we call addDownload.
+ PlacesUtils.asyncHistory.updatePlaces({
+ uri: REFERRER_URI,
+ visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ visitDate: Date.now() * 1000
+ }]
+ });
+});
+
+add_test(function test_dh_addDownload_disabledHistory()
+{
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ // We should only receive the notification for the non-private URI. This
+ // test is based on the assumption that visit notifications are received in
+ // the same order of the addDownload calls, which is currently true because
+ // database access is serialized on the same worker thread.
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+ do_check_false(!!page_in_database(PRIVATE_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+
+ Services.prefs.setBoolPref("places.history.enabled", false);
+ gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000);
+
+ // The addDownload functions calls CanAddURI synchronously, thus we can set
+ // the preference back to true immediately (not all apps enable places by
+ // default).
+ Services.prefs.setBoolPref("places.history.enabled", true);
+ gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
+});
+
+/**
+ * Tests that nsIDownloadHistory::AddDownload saves the additional download
+ * details if the optional destination URL is specified.
+ */
+add_test(function test_dh_details()
+{
+ const REMOTE_URI = NetUtil.newURI("http://localhost/");
+ const SOURCE_URI = NetUtil.newURI("http://example.com/test_dh_details");
+ const DEST_FILE_NAME = "dest.txt";
+
+ // We must build a real, valid file URI for the destination.
+ let destFileUri = NetUtil.newURI(FileUtils.getFile("TmpD", [DEST_FILE_NAME]));
+
+ let titleSet = false;
+ let destinationFileUriSet = false;
+ let destinationFileNameSet = false;
+
+ function checkFinished()
+ {
+ if (titleSet && destinationFileUriSet && destinationFileNameSet) {
+ PlacesUtils.annotations.removeObserver(annoObserver);
+ PlacesUtils.history.removeObserver(historyObserver);
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ }
+ }
+
+ let annoObserver = {
+ onPageAnnotationSet: function AO_onPageAnnotationSet(aPage, aName)
+ {
+ if (aPage.equals(SOURCE_URI)) {
+ let value = PlacesUtils.annotations.getPageAnnotation(aPage, aName);
+ switch (aName)
+ {
+ case "downloads/destinationFileURI":
+ destinationFileUriSet = true;
+ do_check_eq(value, destFileUri.spec);
+ break;
+ case "downloads/destinationFileName":
+ destinationFileNameSet = true;
+ do_check_eq(value, DEST_FILE_NAME);
+ break;
+ }
+ checkFinished();
+ }
+ },
+ onItemAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+ onItemAnnotationRemoved: function() {}
+ }
+
+ let historyObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function() {},
+ onTitleChanged: function HO_onTitleChanged(aURI, aPageTitle)
+ {
+ if (aURI.equals(SOURCE_URI)) {
+ titleSet = true;
+ do_check_eq(aPageTitle, DEST_FILE_NAME);
+ checkFinished();
+ }
+ },
+ onDeleteURI: function() {},
+ onClearHistory: function() {},
+ onPageChanged: function() {},
+ onDeleteVisits: function() {}
+ };
+
+ PlacesUtils.annotations.addObserver(annoObserver, false);
+ PlacesUtils.history.addObserver(historyObserver, false);
+
+ // Both null values and remote URIs should not cause errors.
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000);
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, null);
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, REMOTE_URI);
+
+ // Valid local file URIs should cause the download details to be saved.
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000,
+ destFileUri);
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency.js b/toolkit/components/places/tests/unit/test_frecency.js
new file mode 100644
index 0000000000..a04befe009
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency.js
@@ -0,0 +1,294 @@
+/* -*- 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/. */
+
+/**
+ * Test for bug 406358 to make sure frecency works for empty input/search, but
+ * this also tests for non-empty inputs as well. Because the interactions among
+ * *DIFFERENT* visit counts and visit dates is not well defined, this test
+ * holds one of the two values constant when modifying the other.
+ *
+ * Also test bug 419068 to make sure tagged pages don't necessarily have to be
+ * first in the results.
+ *
+ * Also test bug 426166 to make sure that the results of autocomplete searches
+ * are stable. Note that failures of this test will be intermittent by nature
+ * since we are testing to make sure that the unstable sort algorithm used
+ * by SQLite is not changing the order of the results on us.
+ */
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+function ensure_results(uris, searchTerm)
+{
+ PlacesTestUtils.promiseAsyncUpdates()
+ .then(() => ensure_results_internal(uris, searchTerm));
+}
+
+function ensure_results_internal(uris, searchTerm)
+{
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, uris.length);
+ for (var i=0; i<controller.matchCount; i++) {
+ do_check_eq(controller.getValueAt(i), uris[i].spec);
+ }
+
+ deferEnsureResults.resolve();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+// Get history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+ var bmksvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+function* task_setCountDate(aURI, aCount, aDate)
+{
+ // We need visits so that frecency can be computed over multiple visits
+ let visits = [];
+ for (let i = 0; i < aCount; i++) {
+ visits.push({ uri: aURI, visitDate: aDate, transition: TRANSITION_TYPED });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+}
+
+function setBookmark(aURI)
+{
+ bmksvc.insertBookmark(bmksvc.bookmarksMenuFolder, aURI, -1, "bleh");
+}
+
+function tagURI(aURI, aTags) {
+ bmksvc.insertBookmark(bmksvc.unfiledBookmarksFolder, aURI,
+ bmksvc.DEFAULT_INDEX, "bleh");
+ tagssvc.tagURI(aURI, aTags);
+}
+
+var uri1 = uri("http://site.tld/1");
+var uri2 = uri("http://site.tld/2");
+var uri3 = uri("http://aaaaaaaaaa/1");
+var uri4 = uri("http://aaaaaaaaaa/2");
+
+// d1 is younger (should show up higher) than d2 (PRTime is in usecs not msec)
+// Make sure the dates fall into different frecency buckets
+var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+var d2 = new Date(Date.now() - 1000 * 60 * 60 * 24 * 10) * 1000;
+// c1 is larger (should show up higher) than c2
+var c1 = 10;
+var c2 = 1;
+
+var tests = [
+// test things without a search term
+function*() {
+ print("TEST-INFO | Test 0: same count, different date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c1, d2);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "");
+},
+function*() {
+ print("TEST-INFO | Test 1: same count, different date");
+ yield task_setCountDate(uri1, c1, d2);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "");
+},
+function*() {
+ print("TEST-INFO | Test 2: different count, same date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c2, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "");
+},
+function*() {
+ print("TEST-INFO | Test 3: different count, same date");
+ yield task_setCountDate(uri1, c2, d1);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "");
+},
+
+// test things with a search term
+function*() {
+ print("TEST-INFO | Test 4: same count, different date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c1, d2);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "site");
+},
+function*() {
+ print("TEST-INFO | Test 5: same count, different date");
+ yield task_setCountDate(uri1, c1, d2);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "site");
+},
+function*() {
+ print("TEST-INFO | Test 6: different count, same date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c2, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "site");
+},
+function*() {
+ print("TEST-INFO | Test 7: different count, same date");
+ yield task_setCountDate(uri1, c2, d1);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "site");
+},
+// There are multiple tests for 8, hence the multiple functions
+// Bug 426166 section
+function*() {
+ print("TEST-INFO | Test 8.1a: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "a");
+},
+function*() {
+ print("TEST-INFO | Test 8.1b: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aa");
+},
+function*() {
+ print("TEST-INFO | Test 8.2: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aaa");
+},
+function*() {
+ print("TEST-INFO | Test 8.3: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aaaa");
+},
+function*() {
+ print("TEST-INFO | Test 8.4: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aaa");
+},
+function*() {
+ print("TEST-INFO | Test 8.5: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aa");
+},
+function*() {
+ print("TEST-INFO | Test 8.6: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "a");
+}
+];
+
+/**
+ * This deferred object contains a promise that is resolved when the
+ * ensure_results_internal function has finished its execution.
+ */
+var deferEnsureResults;
+
+add_task(function* test_frecency()
+{
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+ // always search in history + bookmarks, no matter what the default is
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ for (let test of tests) {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+
+ deferEnsureResults = Promise.defer();
+ yield test();
+ yield deferEnsureResults.promise;
+ }
+ for (let type of ["history", "bookmark", "openpage"]) {
+ prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_observers.js b/toolkit/components/places/tests/unit/test_frecency_observers.js
new file mode 100644
index 0000000000..7fadd4ae92
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_observers.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ run_next_test();
+}
+
+// Each of these tests a path that triggers a frecency update. Together they
+// hit all sites that update a frecency.
+
+// InsertVisitedURIs::UpdateFrecency and History::InsertPlace
+add_task(function* test_InsertVisitedURIs_UpdateFrecency_and_History_InsertPlace() {
+ // InsertPlace is at the end of a path that UpdateFrecency is also on, so kill
+ // two birds with one stone and expect two notifications. Trigger the path by
+ // adding a download.
+ let uri = NetUtil.newURI("http://example.com/a");
+ Cc["@mozilla.org/browser/download-history;1"].
+ getService(Ci.nsIDownloadHistory).
+ addDownload(uri);
+ yield Promise.all([onFrecencyChanged(uri), onFrecencyChanged(uri)]);
+});
+
+// nsNavHistory::UpdateFrecency
+add_task(function* test_nsNavHistory_UpdateFrecency() {
+ let bm = PlacesUtils.bookmarks;
+ let uri = NetUtil.newURI("http://example.com/b");
+ bm.insertBookmark(bm.unfiledBookmarksFolder, uri,
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "test");
+ yield onFrecencyChanged(uri);
+});
+
+// nsNavHistory::invalidateFrecencies for particular pages
+add_task(function* test_nsNavHistory_invalidateFrecencies_somePages() {
+ let uri = NetUtil.newURI("http://test-nsNavHistory-invalidateFrecencies-somePages.com/");
+ // Bookmarking the URI is enough to add it to moz_places, and importantly, it
+ // means that removePagesFromHost doesn't remove it from moz_places, so its
+ // frecency is able to be changed.
+ let bm = PlacesUtils.bookmarks;
+ bm.insertBookmark(bm.unfiledBookmarksFolder, uri,
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "test");
+ PlacesUtils.history.removePagesFromHost(uri.host, false);
+ yield onFrecencyChanged(uri);
+});
+
+// nsNavHistory::invalidateFrecencies for all pages
+add_task(function* test_nsNavHistory_invalidateFrecencies_allPages() {
+ yield Promise.all([onManyFrecenciesChanged(), PlacesTestUtils.clearHistory()]);
+});
+
+// nsNavHistory::DecayFrecency and nsNavHistory::FixInvalidFrecencies
+add_task(function* test_nsNavHistory_DecayFrecency_and_nsNavHistory_FixInvalidFrecencies() {
+ // FixInvalidFrecencies is at the end of a path that DecayFrecency is also on,
+ // so expect two notifications. Trigger the path by making nsNavHistory
+ // observe the idle-daily notification.
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver).
+ observe(null, "idle-daily", "");
+ yield Promise.all([onManyFrecenciesChanged(), onManyFrecenciesChanged()]);
+});
+
+function onFrecencyChanged(expectedURI) {
+ let deferred = Promise.defer();
+ let obs = new NavHistoryObserver();
+ obs.onFrecencyChanged =
+ (uri, newFrecency, guid, hidden, visitDate) => {
+ PlacesUtils.history.removeObserver(obs);
+ do_check_true(!!uri);
+ do_check_true(uri.equals(expectedURI));
+ deferred.resolve();
+ };
+ PlacesUtils.history.addObserver(obs, false);
+ return deferred.promise;
+}
+
+function onManyFrecenciesChanged() {
+ let deferred = Promise.defer();
+ let obs = new NavHistoryObserver();
+ obs.onManyFrecenciesChanged = () => {
+ PlacesUtils.history.removeObserver(obs);
+ do_check_true(true);
+ deferred.resolve();
+ };
+ PlacesUtils.history.addObserver(obs, false);
+ return deferred.promise;
+}
diff --git a/toolkit/components/places/tests/unit/test_frecency_zero_updated.js b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js
new file mode 100644
index 0000000000..e60030ca56
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests a zero frecency is correctly updated when inserting new valid visits.
+
+function run_test()
+{
+ run_next_test()
+}
+
+add_task(function* ()
+{
+ const TEST_URI = NetUtil.newURI("http://example.com/");
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ // Removing the bookmark should leave an orphan page with zero frecency.
+ // Note this would usually be expired later by expiration.
+ PlacesUtils.bookmarks.removeItem(id);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ // Now add a valid visit to the page, frecency should increase.
+ yield PlacesTestUtils.addVisits({ uri: TEST_URI });
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+});
diff --git a/toolkit/components/places/tests/unit/test_getChildIndex.js b/toolkit/components/places/tests/unit/test_getChildIndex.js
new file mode 100644
index 0000000000..4cf164d453
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_getChildIndex.js
@@ -0,0 +1,69 @@
+/* -*- 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 nsNavHistoryContainerResultNode::GetChildIndex(aNode) functionality.
+ */
+
+function run_test() {
+ // Add a bookmark to the menu.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri("http://test.mozilla.org/bookmark/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX,
+ "Test bookmark");
+
+ // Add a bookmark to unfiled folder.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://test.mozilla.org/unfiled/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX,
+ "Unfiled bookmark");
+
+ // Get the unfiled bookmark node.
+ let unfiledNode = getNodeAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ if (!unfiledNode)
+ do_throw("Unable to find bookmark in hierarchy!");
+ do_check_eq(unfiledNode.title, "Unfiled bookmark");
+
+ let hs = PlacesUtils.history;
+ let query = hs.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarksMenuFolderId], 1);
+ let options = hs.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = hs.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ // Check functionality for proper nodes.
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ print("Now testing: " + node.title);
+ do_check_eq(root.getChildIndex(node), i);
+ }
+
+ // Now search for an invalid node and expect an exception.
+ try {
+ root.getChildIndex(unfiledNode);
+ do_throw("Searching for an invalid node should have thrown.");
+ } catch (ex) {
+ print("We correctly got an exception.");
+ }
+
+ root.containerOpen = false;
+}
+
+function getNodeAt(aFolderId, aIndex) {
+ let hs = PlacesUtils.history;
+ let query = hs.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ let options = hs.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = hs.executeQuery(query, options).root;
+ root.containerOpen = true;
+ if (root.childCount < aIndex)
+ do_throw("Not enough children to find bookmark!");
+ let node = root.getChild(aIndex);
+ root.containerOpen = false;
+ return node;
+}
diff --git a/toolkit/components/places/tests/unit/test_getPlacesInfo.js b/toolkit/components/places/tests/unit/test_getPlacesInfo.js
new file mode 100644
index 0000000000..3dfecb934a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_getPlacesInfo.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function promiseGetPlacesInfo(aPlacesIdentifiers) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.getPlacesInfo(aPlacesIdentifiers, {
+ _results: [],
+ _errors: [],
+
+ handleResult: function handleResult(aPlaceInfo) {
+ this._results.push(aPlaceInfo);
+ },
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ this._errors.push({ resultCode: aResultCode, info: aPlaceInfo });
+ },
+ handleCompletion: function handleCompletion() {
+ deferred.resolve({ errors: this._errors, results: this._results });
+ }
+ });
+
+ return deferred.promise;
+}
+
+function ensurePlacesInfoObjectsAreEqual(a, b) {
+ do_check_true(a.uri.equals(b.uri));
+ do_check_eq(a.title, b.title);
+ do_check_eq(a.guid, b.guid);
+ do_check_eq(a.placeId, b.placeId);
+}
+
+function* test_getPlacesInfoExistentPlace() {
+ let testURI = NetUtil.newURI("http://www.example.tld");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ let getPlacesInfoResult = yield promiseGetPlacesInfo([testURI]);
+ do_check_eq(getPlacesInfoResult.results.length, 1);
+ do_check_eq(getPlacesInfoResult.errors.length, 0);
+
+ let placeInfo = getPlacesInfoResult.results[0];
+ do_check_true(placeInfo instanceof Ci.mozIPlaceInfo);
+
+ do_check_true(placeInfo.uri.equals(testURI));
+ do_check_eq(placeInfo.title, "test visit for " + testURI.spec);
+ do_check_true(placeInfo.guid.length > 0);
+ do_check_eq(placeInfo.visits, null);
+}
+add_task(test_getPlacesInfoExistentPlace);
+
+function* test_getPlacesInfoNonExistentPlace() {
+ let testURI = NetUtil.newURI("http://www.example_non_existent.tld");
+ let getPlacesInfoResult = yield promiseGetPlacesInfo(testURI);
+ do_check_eq(getPlacesInfoResult.results.length, 0);
+ do_check_eq(getPlacesInfoResult.errors.length, 1);
+}
+add_task(test_getPlacesInfoNonExistentPlace);
+
+function* test_promisedHelper() {
+ let uri = NetUtil.newURI("http://www.helper_existent_example.tld");
+ yield PlacesTestUtils.addVisits(uri);
+ let placeInfo = yield PlacesUtils.promisePlaceInfo(uri);
+ do_check_true(placeInfo instanceof Ci.mozIPlaceInfo);
+
+ uri = NetUtil.newURI("http://www.helper_non_existent_example.tld");
+ try {
+ yield PlacesUtils.promisePlaceInfo(uri);
+ do_throw("PlacesUtils.promisePlaceInfo should have rejected the promise");
+ }
+ catch (ex) { }
+}
+add_task(test_promisedHelper);
+
+function* test_infoByGUID() {
+ let testURI = NetUtil.newURI("http://www.guid_example.tld");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ let placeInfoByURI = yield PlacesUtils.promisePlaceInfo(testURI);
+ let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo(placeInfoByURI.guid);
+ ensurePlacesInfoObjectsAreEqual(placeInfoByURI, placeInfoByGUID);
+}
+add_task(test_infoByGUID);
+
+function* test_invalid_guid() {
+ try {
+ yield PlacesUtils.promisePlaceInfo("###");
+ do_throw("getPlacesInfo should fail for invalid guids")
+ }
+ catch (ex) { }
+}
+add_task(test_invalid_guid);
+
+function* test_mixed_selection() {
+ let placeInfo1, placeInfo2;
+ let uri = NetUtil.newURI("http://www.mixed_selection_test_1.tld");
+ yield PlacesTestUtils.addVisits(uri);
+ placeInfo1 = yield PlacesUtils.promisePlaceInfo(uri);
+
+ uri = NetUtil.newURI("http://www.mixed_selection_test_2.tld");
+ yield PlacesTestUtils.addVisits(uri);
+ placeInfo2 = yield PlacesUtils.promisePlaceInfo(uri);
+
+ let getPlacesInfoResult = yield promiseGetPlacesInfo([placeInfo1.uri, placeInfo2.guid]);
+ do_check_eq(getPlacesInfoResult.results.length, 2);
+ do_check_eq(getPlacesInfoResult.errors.length, 0);
+
+ do_check_eq(getPlacesInfoResult.results[0].uri.spec, placeInfo1.uri.spec);
+ do_check_eq(getPlacesInfoResult.results[1].guid, placeInfo2.guid);
+}
+add_task(test_mixed_selection);
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_history.js b/toolkit/components/places/tests/unit/test_history.js
new file mode 100644
index 0000000000..8d194cde1b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history.js
@@ -0,0 +1,184 @@
+/* -*- 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/. */
+
+// Get history services
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+/**
+ * Checks to see that a URI is in the database.
+ *
+ * @param aURI
+ * The URI to check.
+ * @returns true if the URI is in the DB, false otherwise.
+ */
+function uri_in_db(aURI) {
+ var options = histsvc.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI
+ var query = histsvc.getNewQuery();
+ query.uri = aURI;
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ root.containerOpen = false;
+ return (cc == 1);
+}
+
+// main
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // we have a new profile, so we should have imported bookmarks
+ do_check_eq(histsvc.databaseStatus, histsvc.DATABASE_STATUS_CREATE);
+
+ // add a visit
+ var testURI = uri("http://mozilla.com");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ // now query for the visit, setting sorting and limit such that
+ // we should retrieve only the visit we just added
+ var options = histsvc.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.maxResults = 1;
+ // TODO: using full visit crashes in xpcshell test
+ // options.resultType = options.RESULTS_AS_FULL_VISIT;
+ options.resultType = options.RESULTS_AS_VISIT;
+ var query = histsvc.getNewQuery();
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ for (var i=0; i < cc; ++i) {
+ var node = root.getChild(i);
+ // test node properties in RESULTS_AS_VISIT
+ do_check_eq(node.uri, testURI.spec);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ // TODO: change query type to RESULTS_AS_FULL_VISIT and test this
+ // do_check_eq(node.transitionType, histsvc.TRANSITION_TYPED);
+ }
+ root.containerOpen = false;
+
+ // add another visit for the same URI, and a third visit for a different URI
+ var testURI2 = uri("http://google.com/");
+ yield PlacesTestUtils.addVisits(testURI);
+ yield PlacesTestUtils.addVisits(testURI2);
+
+ options.maxResults = 5;
+ options.resultType = options.RESULTS_AS_URI;
+
+ // test minVisits
+ query.minVisits = 0;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.minVisits = 1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.minVisits = 2;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ query.minVisits = 3;
+ result.root.containerOpen = false;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 0);
+ result.root.containerOpen = false;
+
+ // test maxVisits
+ query.minVisits = -1;
+ query.maxVisits = -1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.maxVisits = 0;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 0);
+ result.root.containerOpen = false;
+ query.maxVisits = 1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ result.root.containerOpen = false;
+ query.maxVisits = 2;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.maxVisits = 3;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+
+ // test annotation-based queries
+ var annos = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+ annos.setPageAnnotation(uri("http://mozilla.com/"), "testAnno", 0, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+ query.annotation = "testAnno";
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ do_check_eq(result.root.getChild(0).uri, "http://mozilla.com/");
+ result.root.containerOpen = false;
+
+ // test annotationIsNot
+ query.annotationIsNot = true;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ do_check_eq(result.root.getChild(0).uri, "http://google.com/");
+ result.root.containerOpen = false;
+
+ // By default history is enabled.
+ do_check_true(!histsvc.historyDisabled);
+
+ // test getPageTitle
+ yield PlacesTestUtils.addVisits({ uri: uri("http://example.com"), title: "title" });
+ let placeInfo = yield PlacesUtils.promisePlaceInfo(uri("http://example.com"));
+ do_check_eq(placeInfo.title, "title");
+
+ // query for the visit
+ do_check_true(uri_in_db(testURI));
+
+ // test for schema changes in bug 373239
+ // get direct db connection
+ var db = histsvc.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+ var q = "SELECT id FROM moz_bookmarks";
+ var statement;
+ try {
+ statement = db.createStatement(q);
+ } catch (ex) {
+ do_throw("bookmarks table does not have id field, schema is too old!");
+ }
+ finally {
+ statement.finalize();
+ }
+
+ // bug 394741 - regressed history text searches
+ yield PlacesTestUtils.addVisits(uri("http://mozilla.com"));
+ options = histsvc.getNewQueryOptions();
+ // options.resultType = options.RESULTS_AS_VISIT;
+ query = histsvc.getNewQuery();
+ query.searchTerms = "moz";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_true(root.childCount > 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js
new file mode 100644
index 0000000000..a5e0e1cb1d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js
@@ -0,0 +1,185 @@
+/* -*- 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 current_test = 0;
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+function ensure_tag_results(uris, searchTerm)
+{
+ print("Searching for '" + searchTerm + "'");
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ uris.length ?
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+ do_check_eq(controller.matchCount, uris.length);
+ let vals = [];
+ for (let i=0; i<controller.matchCount; i++) {
+ // Keep the URL for later because order of tag results is undefined
+ vals.push(controller.getValueAt(i));
+ do_check_eq(controller.getStyleAt(i), "bookmark-tag");
+ }
+ // Sort the results then check if we have the right items
+ vals.sort().forEach((val, i) => do_check_eq(val, uris[i].spec))
+
+ if (current_test < (tests.length - 1)) {
+ current_test++;
+ tests[current_test]();
+ }
+
+ do_test_finished();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+var uri1 = uri("http://site.tld/1/aaa");
+var uri2 = uri("http://site.tld/2/bbb");
+var uri3 = uri("http://site.tld/3/aaa");
+var uri4 = uri("http://site.tld/4/bbb");
+var uri5 = uri("http://site.tld/5/aaa");
+var uri6 = uri("http://site.tld/6/bbb");
+
+var tests = [
+ () => ensure_tag_results([uri1, uri4, uri6], "foo"),
+ () => ensure_tag_results([uri1], "foo aaa"),
+ () => ensure_tag_results([uri4, uri6], "foo bbb"),
+ () => ensure_tag_results([uri2, uri4, uri5, uri6], "bar"),
+ () => ensure_tag_results([uri5], "bar aaa"),
+ () => ensure_tag_results([uri2, uri4, uri6], "bar bbb"),
+ () => ensure_tag_results([uri3, uri5, uri6], "cheese"),
+ () => ensure_tag_results([uri3, uri5], "chees aaa"),
+ () => ensure_tag_results([uri6], "chees bbb"),
+ () => ensure_tag_results([uri4, uri6], "fo bar"),
+ () => ensure_tag_results([], "fo bar aaa"),
+ () => ensure_tag_results([uri4, uri6], "fo bar bbb"),
+ () => ensure_tag_results([uri4, uri6], "ba foo"),
+ () => ensure_tag_results([], "ba foo aaa"),
+ () => ensure_tag_results([uri4, uri6], "ba foo bbb"),
+ () => ensure_tag_results([uri5, uri6], "ba chee"),
+ () => ensure_tag_results([uri5], "ba chee aaa"),
+ () => ensure_tag_results([uri6], "ba chee bbb"),
+ () => ensure_tag_results([uri5, uri6], "cheese bar"),
+ () => ensure_tag_results([uri5], "cheese bar aaa"),
+ () => ensure_tag_results([uri6], "chees bar bbb"),
+ () => ensure_tag_results([uri6], "cheese bar foo"),
+ () => ensure_tag_results([], "foo bar cheese aaa"),
+ () => ensure_tag_results([uri6], "foo bar cheese bbb"),
+];
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param aURI
+ * The nsIURI to tag.
+ * @param aTags
+ * The tags to add.
+ */
+function tagURI(aURI, aTags) {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ tagssvc.tagURI(aURI, aTags);
+}
+
+/**
+ * Test history autocomplete
+ */
+function run_test() {
+ // always search in history + bookmarks, no matter what the default is
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("browser.urlbar.search.sources", 3);
+ prefs.setIntPref("browser.urlbar.default.behavior", 0);
+
+ tagURI(uri1, ["foo"]);
+ tagURI(uri2, ["bar"]);
+ tagURI(uri3, ["cheese"]);
+ tagURI(uri4, ["foo bar"]);
+ tagURI(uri5, ["bar cheese"]);
+ tagURI(uri6, ["foo bar cheese"]);
+
+ tests[0]();
+}
diff --git a/toolkit/components/places/tests/unit/test_history_catobs.js b/toolkit/components/places/tests/unit/test_history_catobs.js
new file mode 100644
index 0000000000..e0a81d67bc
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_catobs.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ do_load_manifest("nsDummyObserver.manifest");
+
+ let dummyCreated = false;
+ let dummyReceivedOnVisit = false;
+
+ Services.obs.addObserver(function created() {
+ Services.obs.removeObserver(created, "dummy-observer-created");
+ dummyCreated = true;
+ }, "dummy-observer-created", false);
+ Services.obs.addObserver(function visited() {
+ Services.obs.removeObserver(visited, "dummy-observer-visited");
+ dummyReceivedOnVisit = true;
+ }, "dummy-observer-visited", false);
+
+ let initialObservers = PlacesUtils.history.getObservers();
+
+ // Add a common observer, it should be invoked after the category observer.
+ let notificationsPromised = new Promise((resolve, reject) => {
+ PlacesUtils.history.addObserver({
+ __proto__: NavHistoryObserver.prototype,
+ onVisit() {
+ let observers = PlacesUtils.history.getObservers();
+ Assert.equal(observers.length, initialObservers.length + 1);
+
+ // Check the common observer is the last one.
+ for (let i = 0; i < initialObservers.length; ++i) {
+ Assert.equal(initialObservers[i], observers[i]);
+ }
+
+ PlacesUtils.history.removeObserver(this);
+ observers = PlacesUtils.history.getObservers();
+ Assert.equal(observers.length, initialObservers.length);
+
+ // Check the category observer has been invoked before this one.
+ Assert.ok(dummyCreated);
+ Assert.ok(dummyReceivedOnVisit);
+ resolve();
+ }
+ }, false);
+ });
+
+ // Add a visit.
+ yield PlacesTestUtils.addVisits(uri("http://typed.mozilla.org"));
+
+ yield notificationsPromised;
+});
diff --git a/toolkit/components/places/tests/unit/test_history_clear.js b/toolkit/components/places/tests/unit/test_history_clear.js
new file mode 100644
index 0000000000..56d34994f7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_clear.js
@@ -0,0 +1,169 @@
+/* -*- 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 mDBConn = DBConn();
+
+function promiseOnClearHistoryObserved() {
+ let deferred = Promise.defer();
+
+ let historyObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function() {},
+ onTitleChanged: function() {},
+ onDeleteURI: function(aURI) {},
+ onPageChanged: function() {},
+ onDeleteVisits: function() {},
+
+ onClearHistory: function() {
+ PlacesUtils.history.removeObserver(this, false);
+ deferred.resolve();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavHistoryObserver,
+ ])
+ }
+ PlacesUtils.history.addObserver(historyObserver, false);
+ return deferred.promise;
+}
+
+// This global variable is a promise object, initialized in run_test and waited
+// upon in the first asynchronous test. It is resolved when the
+// "places-init-complete" notification is received. We cannot initialize it in
+// the asynchronous test, because then it's too late to register the observer.
+var promiseInit;
+
+function run_test() {
+ // places-init-complete is notified after run_test, and it will
+ // run a first frecency fix through async statements.
+ // To avoid random failures we have to run after all of this.
+ promiseInit = promiseTopicObserved(PlacesUtils.TOPIC_INIT_COMPLETE);
+
+ run_next_test();
+}
+
+add_task(function* test_history_clear()
+{
+ yield promiseInit;
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri("http://typed.mozilla.org/"),
+ transition: TRANSITION_TYPED },
+ { uri: uri("http://link.mozilla.org/"),
+ transition: TRANSITION_LINK },
+ { uri: uri("http://download.mozilla.org/"),
+ transition: TRANSITION_DOWNLOAD },
+ { uri: uri("http://redir_temp.mozilla.org/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ referrer: "http://link.mozilla.org/"},
+ { uri: uri("http://redir_perm.mozilla.org/"),
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ referrer: "http://link.mozilla.org/"},
+ ]);
+
+ // add a place: bookmark
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("place:folder=4"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "shortcut");
+
+ // Add an expire never annotation
+ // Actually expire never annotations are removed as soon as a page is removed
+ // from the database, so this should act as a normal visit.
+ PlacesUtils.annotations.setPageAnnotation(uri("http://download.mozilla.org/"),
+ "never", "never", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Add a bookmark
+ // Bookmarked page should have history cleared and frecency = -1
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://typed.mozilla.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri("http://typed.mozilla.org/"),
+ transition: TRANSITION_BOOKMARK },
+ { uri: uri("http://frecency.mozilla.org/"),
+ transition: TRANSITION_LINK },
+ ]);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Clear history and wait for the onClearHistory notification.
+ let promiseWaitClearHistory = promiseOnClearHistoryObserved();
+ PlacesUtils.history.clear();
+ yield promiseWaitClearHistory;
+
+ // check browserHistory returns no entries
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+
+ yield promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Check that frecency for not cleared items (bookmarks) has been converted
+ // to -1.
+ stmt = mDBConn.createStatement(
+ "SELECT h.id FROM moz_places h WHERE h.frecency > 0 ");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h WHERE h.frecency < 0
+ AND EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`);
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that all visit_counts have been brought to 0
+ stmt = mDBConn.createStatement(
+ "SELECT id FROM moz_places WHERE visit_count <> 0 LIMIT 1");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that history tables are empty
+ stmt = mDBConn.createStatement(
+ "SELECT * FROM (SELECT id FROM moz_historyvisits LIMIT 1)");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that all moz_places entries except bookmarks and place: have been removed
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h WHERE
+ url_hash NOT BETWEEN hash('place', 'prefix_lo') AND hash('place', 'prefix_hi')
+ AND NOT EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`);
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have favicons for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT f.id FROM moz_favicons f WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE favicon_id = f.id) LIMIT 1`);
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have annotations for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT a.id FROM moz_annos a WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = a.place_id) LIMIT 1`);
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have inputhistory for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT i.place_id FROM moz_inputhistory i WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = i.place_id) LIMIT 1`);
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that place:uris have frecency 0
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h
+ WHERE url_hash BETWEEN hash('place', 'prefix_lo')
+ AND hash('place', 'prefix_hi')
+ AND h.frecency <> 0 LIMIT 1`);
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+});
diff --git a/toolkit/components/places/tests/unit/test_history_notifications.js b/toolkit/components/places/tests/unit/test_history_notifications.js
new file mode 100644
index 0000000000..4e1e635a0d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_notifications.js
@@ -0,0 +1,38 @@
+const NS_PLACES_INIT_COMPLETE_TOPIC = "places-init-complete";
+const NS_PLACES_DATABASE_LOCKED_TOPIC = "places-database-locked";
+
+add_task(function* () {
+ // Create a dummy places.sqlite and open an unshared connection on it
+ let db = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ db.append("places.sqlite");
+ let dbConn = Services.storage.openUnsharedDatabase(db);
+ Assert.ok(db.exists(), "The database should have been created");
+
+ // We need an exclusive lock on the db
+ dbConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+ // Exclusive locking is lazy applied, we need to make a write to activate it
+ dbConn.executeSimpleSQL("PRAGMA USER_VERSION = 1");
+
+ // Try to create history service while the db is locked
+ let promiseLocked = promiseTopicObserved(NS_PLACES_DATABASE_LOCKED_TOPIC);
+ Assert.throws(() => Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService),
+ /NS_ERROR_XPC_GS_RETURNED_FAILURE/);
+ yield promiseLocked;
+
+ // Close our connection and try to cleanup the file (could fail on Windows)
+ dbConn.close();
+ if (db.exists()) {
+ try {
+ db.remove(false);
+ } catch (e) {
+ do_print("Unable to remove dummy places.sqlite");
+ }
+ }
+
+ // Create history service correctly
+ let promiseComplete = promiseTopicObserved(NS_PLACES_INIT_COMPLETE_TOPIC);
+ Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService);
+ yield promiseComplete;
+});
diff --git a/toolkit/components/places/tests/unit/test_history_observer.js b/toolkit/components/places/tests/unit/test_history_observer.js
new file mode 100644
index 0000000000..c101cfb61b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_observer.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * 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])
+};
+
+/**
+ * Registers a one-time history observer for and calls the callback
+ * when the specified nsINavHistoryObserver method is called.
+ * Returns a promise that is resolved when the callback returns.
+ */
+function onNotify(callback) {
+ return new Promise(resolve => {
+ let obs = new NavHistoryObserver();
+ obs[callback.name] = function () {
+ PlacesUtils.history.removeObserver(this);
+ callback.apply(this, arguments);
+ resolve();
+ };
+ PlacesUtils.history.addObserver(obs, false);
+ });
+}
+
+/**
+ * Asynchronous task that adds a visit to the history database.
+ */
+function* task_add_visit(uri, timestamp, transition) {
+ uri = uri || NetUtil.newURI("http://firefox.com/");
+ timestamp = timestamp || Date.now() * 1000;
+ yield PlacesTestUtils.addVisits({
+ uri: uri,
+ transition: transition || TRANSITION_TYPED,
+ visitDate: timestamp
+ });
+ return [uri, timestamp];
+}
+
+add_task(function* test_onVisit() {
+ let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
+ aSessionID, aReferringID,
+ aTransitionType, aGUID,
+ aHidden, aVisitCount, aTyped) {
+ Assert.ok(aURI.equals(testuri));
+ Assert.ok(aVisitID > 0);
+ Assert.equal(aTime, testtime);
+ Assert.equal(aSessionID, 0);
+ Assert.equal(aReferringID, 0);
+ Assert.equal(aTransitionType, TRANSITION_TYPED);
+ do_check_guid_for_uri(aURI, aGUID);
+ Assert.ok(!aHidden);
+ Assert.equal(aVisitCount, 1);
+ Assert.equal(aTyped, 1);
+ });
+ let testuri = NetUtil.newURI("http://firefox.com/");
+ let testtime = Date.now() * 1000;
+ yield task_add_visit(testuri, testtime);
+ yield promiseNotify;
+});
+
+add_task(function* test_onVisit() {
+ let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
+ aSessionID, aReferringID,
+ aTransitionType, aGUID,
+ aHidden, aVisitCount, aTyped) {
+ Assert.ok(aURI.equals(testuri));
+ Assert.ok(aVisitID > 0);
+ Assert.equal(aTime, testtime);
+ Assert.equal(aSessionID, 0);
+ Assert.equal(aReferringID, 0);
+ Assert.equal(aTransitionType, TRANSITION_FRAMED_LINK);
+ do_check_guid_for_uri(aURI, aGUID);
+ Assert.ok(aHidden);
+ Assert.equal(aVisitCount, 1);
+ Assert.equal(aTyped, 0);
+ });
+ let testuri = NetUtil.newURI("http://hidden.firefox.com/");
+ let testtime = Date.now() * 1000;
+ yield task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK);
+ yield promiseNotify;
+});
+
+add_task(function* test_multiple_onVisit() {
+ let testuri = NetUtil.newURI("http://self.firefox.com/");
+ let promiseNotifications = new Promise(resolve => {
+ let observer = {
+ _c: 0,
+ __proto__: NavHistoryObserver.prototype,
+ onVisit(uri, id, time, unused, referrerId, transition, guid,
+ hidden, visitCount, typed) {
+ Assert.ok(testuri.equals(uri));
+ Assert.ok(id > 0);
+ Assert.ok(time > 0);
+ Assert.ok(!hidden);
+ do_check_guid_for_uri(uri, guid);
+ switch (++this._c) {
+ case 1:
+ Assert.equal(referrerId, 0);
+ Assert.equal(transition, TRANSITION_LINK);
+ Assert.equal(visitCount, 1);
+ Assert.equal(typed, 0);
+ break;
+ case 2:
+ Assert.ok(referrerId > 0);
+ Assert.equal(transition, TRANSITION_LINK);
+ Assert.equal(visitCount, 2);
+ Assert.equal(typed, 0);
+ break;
+ case 3:
+ Assert.equal(referrerId, 0);
+ Assert.equal(transition, TRANSITION_TYPED);
+ Assert.equal(visitCount, 3);
+ Assert.equal(typed, 1);
+
+ PlacesUtils.history.removeObserver(observer, false);
+ resolve();
+ break;
+ }
+ }
+ };
+ PlacesUtils.history.addObserver(observer, false);
+ });
+ yield PlacesTestUtils.addVisits([
+ { uri: testuri, transition: TRANSITION_LINK },
+ { uri: testuri, referrer: testuri, transition: TRANSITION_LINK },
+ { uri: testuri, transition: TRANSITION_TYPED },
+ ]);
+ yield promiseNotifications;
+});
+
+add_task(function* test_onDeleteURI() {
+ let promiseNotify = onNotify(function onDeleteURI(aURI, aGUID, aReason) {
+ Assert.ok(aURI.equals(testuri));
+ // Can't use do_check_guid_for_uri() here because the visit is already gone.
+ Assert.equal(aGUID, testguid);
+ Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
+ });
+ let [testuri] = yield task_add_visit();
+ let testguid = do_get_guid_for_uri(testuri);
+ PlacesUtils.bhistory.removePage(testuri);
+ yield promiseNotify;
+});
+
+add_task(function* test_onDeleteVisits() {
+ let promiseNotify = onNotify(function onDeleteVisits(aURI, aVisitTime, aGUID,
+ aReason) {
+ Assert.ok(aURI.equals(testuri));
+ // Can't use do_check_guid_for_uri() here because the visit is already gone.
+ Assert.equal(aGUID, testguid);
+ Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
+ Assert.equal(aVisitTime, 0); // All visits have been removed.
+ });
+ let msecs24hrsAgo = Date.now() - (86400 * 1000);
+ let [testuri] = yield task_add_visit(undefined, msecs24hrsAgo * 1000);
+ // Add a bookmark so the page is not removed.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ testuri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test");
+ let testguid = do_get_guid_for_uri(testuri);
+ PlacesUtils.bhistory.removePage(testuri);
+ yield promiseNotify;
+});
+
+add_task(function* test_onTitleChanged() {
+ let promiseNotify = onNotify(function onTitleChanged(aURI, aTitle, aGUID) {
+ Assert.ok(aURI.equals(testuri));
+ Assert.equal(aTitle, title);
+ do_check_guid_for_uri(aURI, aGUID);
+ });
+
+ let [testuri] = yield task_add_visit();
+ let title = "test-title";
+ yield PlacesTestUtils.addVisits({
+ uri: testuri,
+ title: title
+ });
+ yield promiseNotify;
+});
+
+add_task(function* test_onPageChanged() {
+ let promiseNotify = onNotify(function onPageChanged(aURI, aChangedAttribute,
+ aNewValue, aGUID) {
+ Assert.equal(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON);
+ Assert.ok(aURI.equals(testuri));
+ Assert.equal(aNewValue, SMALLPNG_DATA_URI.spec);
+ do_check_guid_for_uri(aURI, aGUID);
+ });
+
+ let [testuri] = yield task_add_visit();
+
+ // The new favicon for the page must have data associated with it in order to
+ // receive the onPageChanged notification. To keep this test self-contained,
+ // we use an URI representing the smallest possible PNG file.
+ PlacesUtils.favicons.setAndFetchFaviconForPage(testuri, SMALLPNG_DATA_URI,
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ yield promiseNotify;
+});
diff --git a/toolkit/components/places/tests/unit/test_history_sidebar.js b/toolkit/components/places/tests/unit/test_history_sidebar.js
new file mode 100644
index 0000000000..1c03547d7f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_sidebar.js
@@ -0,0 +1,447 @@
+/* -*- 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/. */
+
+// Get history service
+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);
+var ps = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+/**
+ * Adds a test URI visit to the database.
+ *
+ * @param aURI
+ * The URI to add a visit for.
+ * @param aTime
+ * Reference "now" time.
+ * @param aDayOffset
+ * number of days to add, pass a negative value to subtract them.
+ */
+function* task_add_normalized_visit(aURI, aTime, aDayOffset) {
+ var dateObj = new Date(aTime);
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ // Days where DST changes should be taken in count.
+ var previousDateObj = new Date(dateObj.getTime() + aDayOffset * 86400000);
+ var DSTCorrection = (dateObj.getTimezoneOffset() -
+ previousDateObj.getTimezoneOffset()) * 60 * 1000;
+ // Substract aDayOffset
+ var PRTimeWithOffset = (previousDateObj.getTime() - DSTCorrection) * 1000;
+ var timeInMs = new Date(PRTimeWithOffset/1000);
+ print("Adding visit to " + aURI.spec + " at " + timeInMs);
+ yield PlacesTestUtils.addVisits({
+ uri: aURI,
+ visitDate: PRTimeWithOffset
+ });
+}
+
+function days_for_x_months_ago(aNowObj, aMonths) {
+ var oldTime = new Date();
+ // Set day before month, otherwise we could try to calculate 30 February, or
+ // other nonexistent days.
+ oldTime.setDate(1);
+ oldTime.setMonth(aNowObj.getMonth() - aMonths);
+ oldTime.setHours(0);
+ oldTime.setMinutes(0);
+ oldTime.setSeconds(0);
+ // Stay larger for eventual timezone issues, add 2 days.
+ return parseInt((aNowObj - oldTime) / (1000*60*60*24)) + 2;
+}
+
+var nowObj = new Date();
+// This test relies on en-US locale
+// Offset is number of days
+/* eslint-disable comma-spacing */
+var containers = [
+ { label: "Today" , offset: 0 , visible: true },
+ { label: "Yesterday" , offset: -1 , visible: true },
+ { label: "Last 7 days" , offset: -3 , visible: true },
+ { label: "This month" , offset: -8 , visible: nowObj.getDate() > 8 },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 0) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 1) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 2) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 3) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 4) , visible: true },
+ { label: "Older than 6 months" , offset: -days_for_x_months_ago(nowObj, 5) , visible: true },
+];
+/* eslint-enable comma-spacing */
+
+var visibleContainers = containers.filter(
+ function(aContainer) { return aContainer.visible });
+
+/**
+ * Asynchronous task that fills history and checks containers' labels.
+ */
+function* task_fill_history() {
+ print("\n\n*** TEST Fill History\n");
+ // We can't use "now" because our hardcoded offsets would be invalid for some
+ // date. So we hardcode a date.
+ for (let i = 0; i < containers.length; i++) {
+ let container = containers[i];
+ var testURI = uri("http://mirror"+i+".mozilla.com/b");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ testURI = uri("http://mirror"+i+".mozilla.com/a");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ testURI = uri("http://mirror"+i+".google.com/b");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ testURI = uri("http://mirror"+i+".google.com/a");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ // Bug 485703 - Hide date containers not containing additional entries
+ // compared to previous ones.
+ // Check after every new container is added.
+ check_visit(container.offset);
+ }
+
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ var query = hs.getNewQuery();
+
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ var cc = root.childCount;
+ print("Found containers:");
+ var previousLabels = [];
+ for (let i = 0; i < cc; i++) {
+ let container = visibleContainers[i];
+ var node = root.getChild(i);
+ print(node.title);
+ if (container.label)
+ do_check_eq(node.title, container.label);
+ // Check labels are not repeated.
+ do_check_eq(previousLabels.indexOf(node.title), -1);
+ previousLabels.push(node.title);
+ }
+ do_check_eq(cc, visibleContainers.length);
+ root.containerOpen = false;
+}
+
+/**
+ * Bug 485703 - Hide date containers not containing additional entries compared
+ * to previous ones.
+ */
+function check_visit(aOffset) {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+
+ var unexpected = [];
+ switch (aOffset) {
+ case 0:
+ unexpected = ["Yesterday", "Last 7 days", "This month"];
+ break;
+ case -1:
+ unexpected = ["Last 7 days", "This month"];
+ break;
+ case -3:
+ unexpected = ["This month"];
+ break;
+ default:
+ // Other containers are tested later.
+ }
+
+ print("Found containers:");
+ for (var i = 0; i < cc; i++) {
+ var node = root.getChild(i);
+ print(node.title);
+ do_check_eq(unexpected.indexOf(node.title), -1);
+ }
+
+ root.containerOpen = false;
+}
+
+/**
+ * Queries history grouped by date and site, checking containers' labels and
+ * children.
+ */
+function test_RESULTS_AS_DATE_SITE_QUERY() {
+ print("\n\n*** TEST RESULTS_AS_DATE_SITE_QUERY\n");
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ // Check one of the days
+ var dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 2);
+
+ // Items should be sorted by host
+ var site1 = dayNode.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site1.title, "mirror0.google.com");
+
+ var site2 = dayNode.getChild(1)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site2.title, "mirror0.mozilla.com");
+
+ site1.containerOpen = true;
+ do_check_eq(site1.childCount, 2);
+
+ // Inside of host sites are sorted by title
+ var site1visit = site1.getChild(0);
+ do_check_eq(site1visit.uri, "http://mirror0.google.com/a");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = options.SORT_BY_TITLE_DESCENDING;
+
+ // Check one of the days
+ dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 2);
+
+ // Hosts are still sorted by title
+ site1 = dayNode.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site1.title, "mirror0.google.com");
+
+ site2 = dayNode.getChild(1)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site2.title, "mirror0.mozilla.com");
+
+ site1.containerOpen = true;
+ do_check_eq(site1.childCount, 2);
+
+ // But URLs are now sorted by title descending
+ site1visit = site1.getChild(0);
+ do_check_eq(site1visit.uri, "http://mirror0.google.com/b");
+
+ site1.containerOpen = false;
+ dayNode.containerOpen = false;
+ root.containerOpen = false;
+}
+
+/**
+ * Queries history grouped by date, checking containers' labels and children.
+ */
+function test_RESULTS_AS_DATE_QUERY() {
+ print("\n\n*** TEST RESULTS_AS_DATE_QUERY\n");
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_QUERY;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ var cc = root.childCount;
+ do_check_eq(cc, visibleContainers.length);
+ print("Found containers:");
+ for (var i = 0; i < cc; i++) {
+ var container = visibleContainers[i];
+ var node = root.getChild(i);
+ print(node.title);
+ if (container.label)
+ do_check_eq(node.title, container.label);
+ }
+
+ // Check one of the days
+ var dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 4);
+
+ // Items should be sorted by title
+ var visit1 = dayNode.getChild(0);
+ do_check_eq(visit1.uri, "http://mirror0.google.com/a");
+
+ var visit2 = dayNode.getChild(3);
+ do_check_eq(visit2.uri, "http://mirror0.mozilla.com/b");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = options.SORT_BY_TITLE_DESCENDING;
+
+ // Check one of the days
+ dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 4);
+
+ // But URLs are now sorted by title descending
+ visit1 = dayNode.getChild(0);
+ do_check_eq(visit1.uri, "http://mirror0.mozilla.com/b");
+
+ visit2 = dayNode.getChild(3);
+ do_check_eq(visit2.uri, "http://mirror0.google.com/a");
+
+ dayNode.containerOpen = false;
+ root.containerOpen = false;
+}
+
+/**
+ * Queries history grouped by site, checking containers' labels and children.
+ */
+function test_RESULTS_AS_SITE_QUERY() {
+ print("\n\n*** TEST RESULTS_AS_SITE_QUERY\n");
+ // add a bookmark with a domain not in the set of visits in the db
+ var itemId = bs.insertBookmark(bs.toolbarFolder, uri("http://foobar"),
+ bs.DEFAULT_INDEX, "");
+
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_SITE_QUERY;
+ options.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, containers.length * 2);
+
+/* Expected results:
+ "mirror0.google.com",
+ "mirror0.mozilla.com",
+ "mirror1.google.com",
+ "mirror1.mozilla.com",
+ "mirror2.google.com",
+ "mirror2.mozilla.com",
+ "mirror3.google.com", <== We check for this site (index 6)
+ "mirror3.mozilla.com",
+ "mirror4.google.com",
+ "mirror4.mozilla.com",
+ "mirror5.google.com",
+ "mirror5.mozilla.com",
+ ...
+*/
+
+ // Items should be sorted by host
+ var siteNode = root.getChild(6)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(siteNode.title, "mirror3.google.com");
+
+ siteNode.containerOpen = true;
+ do_check_eq(siteNode.childCount, 2);
+
+ // Inside of host sites are sorted by title
+ var visitNode = siteNode.getChild(0);
+ do_check_eq(visitNode.uri, "http://mirror3.google.com/a");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = options.SORT_BY_TITLE_DESCENDING;
+ siteNode = root.getChild(6)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(siteNode.title, "mirror3.google.com");
+
+ siteNode.containerOpen = true;
+ do_check_eq(siteNode.childCount, 2);
+
+ // But URLs are now sorted by title descending
+ var visit = siteNode.getChild(0);
+ do_check_eq(visit.uri, "http://mirror3.google.com/b");
+
+ siteNode.containerOpen = false;
+ root.containerOpen = false;
+
+ // Cleanup.
+ bs.removeItem(itemId);
+}
+
+/**
+ * Checks that queries grouped by date do liveupdate correctly.
+ */
+function* task_test_date_liveupdate(aResultType) {
+ var midnight = nowObj;
+ midnight.setHours(0);
+ midnight.setMinutes(0);
+ midnight.setSeconds(0);
+ midnight.setMilliseconds(0);
+
+ // TEST 1. Test that the query correctly updates when it is root.
+ var options = hs.getNewQueryOptions();
+ options.resultType = aResultType;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_eq(root.childCount, visibleContainers.length);
+ // Remove "Today".
+ hs.removePagesByTimeframe(midnight.getTime() * 1000, Date.now() * 1000);
+ do_check_eq(root.childCount, visibleContainers.length - 1);
+
+ // Open "Last 7 days" container, this way we will have a container accepting
+ // the new visit, but we should still add back "Today" container.
+ var last7Days = root.getChild(1)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ last7Days.containerOpen = true;
+
+ // Add a visit for "Today". This should add back the missing "Today"
+ // container.
+ yield task_add_normalized_visit(uri("http://www.mozilla.org/"), nowObj.getTime(), 0);
+ do_check_eq(root.childCount, visibleContainers.length);
+
+ last7Days.containerOpen = false;
+ root.containerOpen = false;
+
+ // TEST 2. Test that the query correctly updates even if it is not root.
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri("place:type=" + aResultType),
+ bs.DEFAULT_INDEX, "");
+
+ // Query toolbar and open our query container, then check again liveupdate.
+ options = hs.getNewQueryOptions();
+ query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ result = hs.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ var dateContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dateContainer.containerOpen = true;
+
+ do_check_eq(dateContainer.childCount, visibleContainers.length);
+ // Remove "Today".
+ hs.removePagesByTimeframe(midnight.getTime() * 1000, Date.now() * 1000);
+ do_check_eq(dateContainer.childCount, visibleContainers.length - 1);
+ // Add a visit for "Today".
+ yield task_add_normalized_visit(uri("http://www.mozilla.org/"), nowObj.getTime(), 0);
+ do_check_eq(dateContainer.childCount, visibleContainers.length);
+
+ dateContainer.containerOpen = false;
+ root.containerOpen = false;
+
+ // Cleanup.
+ bs.removeItem(itemId);
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_history_sidebar()
+{
+ // If we're dangerously close to a date change, just bail out.
+ if (nowObj.getHours() == 23 && nowObj.getMinutes() >= 50) {
+ return;
+ }
+
+ yield task_fill_history();
+ test_RESULTS_AS_DATE_SITE_QUERY();
+ test_RESULTS_AS_DATE_QUERY();
+ test_RESULTS_AS_SITE_QUERY();
+
+ yield task_test_date_liveupdate(Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
+ yield task_test_date_liveupdate(Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY);
+
+ // The remaining views are
+ // RESULTS_AS_URI + SORT_BY_VISITCOUNT_DESCENDING
+ // -> test_399266.js
+ // RESULTS_AS_URI + SORT_BY_DATE_DESCENDING
+ // -> test_385397.js
+});
diff --git a/toolkit/components/places/tests/unit/test_hosts_triggers.js b/toolkit/components/places/tests/unit/test_hosts_triggers.js
new file mode 100644
index 0000000000..9c3359e76d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_hosts_triggers.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the validity of various triggers that add remove hosts from moz_hosts
+ */
+
+XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
+ "@mozilla.org/browser/history;1",
+ "mozIAsyncHistory");
+
+// add some visits and remove them, add a bookmark,
+// change its uri, then remove it, and
+// for each change check that moz_hosts has correctly been updated.
+
+function isHostInMozPlaces(aURI)
+{
+ let stmt = DBConn().createStatement(
+ `SELECT url
+ FROM moz_places
+ WHERE url_hash = hash(:host) AND url = :host`
+ );
+ let result = false;
+ stmt.params.host = aURI.spec;
+ while (stmt.executeStep()) {
+ if (stmt.row.url == aURI.spec) {
+ result = true;
+ break;
+ }
+ }
+ stmt.finalize();
+ return result;
+}
+
+function isHostInMozHosts(aURI, aTyped, aPrefix)
+{
+ let stmt = DBConn().createStatement(
+ `SELECT host, typed, prefix
+ FROM moz_hosts
+ WHERE host = fixup_url(:host)
+ AND frecency NOTNULL`
+ );
+ let result = false;
+ stmt.params.host = aURI.host;
+ if (stmt.executeStep()) {
+ result = aTyped == stmt.row.typed && aPrefix == stmt.row.prefix;
+ }
+ stmt.finalize();
+ return result;
+}
+
+var urls = [{uri: NetUtil.newURI("http://visit1.mozilla.org"),
+ expected: "visit1.mozilla.org",
+ typed: 0,
+ prefix: null
+ },
+ {uri: NetUtil.newURI("http://visit2.mozilla.org"),
+ expected: "visit2.mozilla.org",
+ typed: 0,
+ prefix: null
+ },
+ {uri: NetUtil.newURI("http://www.foo.mozilla.org"),
+ expected: "foo.mozilla.org",
+ typed: 1,
+ prefix: "www."
+ },
+ ];
+
+const NEW_URL = "http://different.mozilla.org/";
+
+add_task(function* test_moz_hosts_update()
+{
+ let places = [];
+ urls.forEach(function(url) {
+ let place = { uri: url.uri,
+ title: "test for " + url.url,
+ transition: url.typed ? TRANSITION_TYPED : undefined };
+ places.push(place);
+ });
+
+ yield PlacesTestUtils.addVisits(places);
+
+ do_check_true(isHostInMozHosts(urls[0].uri, urls[0].typed, urls[0].prefix));
+ do_check_true(isHostInMozHosts(urls[1].uri, urls[1].typed, urls[1].prefix));
+ do_check_true(isHostInMozHosts(urls[2].uri, urls[2].typed, urls[2].prefix));
+});
+
+add_task(function* test_remove_places()
+{
+ for (let idx in urls) {
+ PlacesUtils.history.removePage(urls[idx].uri);
+ }
+
+ yield PlacesTestUtils.clearHistory();
+
+ for (let idx in urls) {
+ do_check_false(isHostInMozHosts(urls[idx].uri, urls[idx].typed, urls[idx].prefix));
+ }
+});
+
+add_task(function* test_bookmark_changes()
+{
+ let testUri = NetUtil.newURI("http://test.mozilla.org");
+
+ let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ testUri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+
+ do_check_true(isHostInMozPlaces(testUri));
+
+ // Change the hostname
+ PlacesUtils.bookmarks.changeBookmarkURI(itemId, NetUtil.newURI(NEW_URL));
+
+ yield PlacesTestUtils.clearHistory();
+
+ let newUri = NetUtil.newURI(NEW_URL);
+ do_check_true(isHostInMozPlaces(newUri));
+ do_check_true(isHostInMozHosts(newUri, false, null));
+ do_check_false(isHostInMozHosts(NetUtil.newURI("http://test.mozilla.org"), false, null));
+});
+
+add_task(function* test_bookmark_removal()
+{
+ let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let newUri = NetUtil.newURI(NEW_URL);
+ PlacesUtils.bookmarks.removeItem(itemId);
+ yield PlacesTestUtils.clearHistory();
+
+ do_check_false(isHostInMozHosts(newUri, false, null));
+});
+
+add_task(function* test_moz_hosts_typed_update()
+{
+ const TEST_URI = NetUtil.newURI("http://typed.mozilla.com");
+ let places = [{ uri: TEST_URI
+ , title: "test for " + TEST_URI.spec
+ },
+ { uri: TEST_URI
+ , title: "test for " + TEST_URI.spec
+ , transition: TRANSITION_TYPED
+ }];
+
+ yield PlacesTestUtils.addVisits(places);
+
+ do_check_true(isHostInMozHosts(TEST_URI, true, null));
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_moz_hosts_www_remove()
+{
+ function* test_removal(aURIToRemove, aURIToKeep, aCallback) {
+ let places = [{ uri: aURIToRemove
+ , title: "test for " + aURIToRemove.spec
+ , transition: TRANSITION_TYPED
+ },
+ { uri: aURIToKeep
+ , title: "test for " + aURIToKeep.spec
+ , transition: TRANSITION_TYPED
+ }];
+
+ yield PlacesTestUtils.addVisits(places);
+ print("removing " + aURIToRemove.spec + " keeping " + aURIToKeep);
+ dump_table("moz_hosts");
+ dump_table("moz_places");
+ PlacesUtils.history.removePage(aURIToRemove);
+ let prefix = /www/.test(aURIToKeep.spec) ? "www." : null;
+ dump_table("moz_hosts");
+ dump_table("moz_places");
+ do_check_true(isHostInMozHosts(aURIToKeep, true, prefix));
+ }
+
+ const TEST_URI = NetUtil.newURI("http://rem.mozilla.com");
+ const TEST_WWW_URI = NetUtil.newURI("http://www.rem.mozilla.com");
+ yield test_removal(TEST_URI, TEST_WWW_URI);
+ yield test_removal(TEST_WWW_URI, TEST_URI);
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_moz_hosts_ftp_matchall()
+{
+ const TEST_URI_1 = NetUtil.newURI("ftp://www.mozilla.com/");
+ const TEST_URI_2 = NetUtil.newURI("ftp://mozilla.com/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: TEST_URI_1, transition: TRANSITION_TYPED },
+ { uri: TEST_URI_2, transition: TRANSITION_TYPED }
+ ]);
+
+ do_check_true(isHostInMozHosts(TEST_URI_1, true, "ftp://"));
+});
+
+add_task(function* test_moz_hosts_ftp_not_matchall()
+{
+ const TEST_URI_1 = NetUtil.newURI("http://mozilla.com/");
+ const TEST_URI_2 = NetUtil.newURI("ftp://mozilla.com/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: TEST_URI_1, transition: TRANSITION_TYPED },
+ { uri: TEST_URI_2, transition: TRANSITION_TYPED }
+ ]);
+
+ do_check_true(isHostInMozHosts(TEST_URI_1, true, null));
+});
+
+add_task(function* test_moz_hosts_update_2()
+{
+ // Check that updating trigger takes into account prefixes for different
+ // rev_hosts.
+ const TEST_URI_1 = NetUtil.newURI("https://www.google.it/");
+ const TEST_URI_2 = NetUtil.newURI("https://google.it/");
+ let places = [{ uri: TEST_URI_1
+ , transition: TRANSITION_TYPED
+ },
+ { uri: TEST_URI_2
+ }];
+ yield PlacesTestUtils.addVisits(places);
+
+ do_check_true(isHostInMozHosts(TEST_URI_1, true, "https://www."));
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
new file mode 100644
index 0000000000..771a6ac173
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -0,0 +1,292 @@
+function* importFromFixture(fixture, replace) {
+ let cwd = yield OS.File.getCurrentDirectory();
+ let path = OS.Path.join(cwd, fixture);
+
+ do_print(`Importing from ${path}`);
+ yield BookmarkJSONUtils.importFromFile(path, replace);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+}
+
+function* treeEquals(guid, expected, message) {
+ let root = yield PlacesUtils.promiseBookmarksTree(guid);
+ let bookmarks = (function nodeToEntry(node) {
+ let entry = { guid: node.guid, index: node.index }
+ if (node.children) {
+ entry.children = node.children.map(nodeToEntry);
+ }
+ if (node.annos) {
+ entry.annos = node.annos;
+ }
+ return entry;
+ }(root));
+
+ do_print(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`);
+ do_print(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`);
+
+ deepEqual(bookmarks, expected, message);
+}
+
+add_task(function* test_restore_mobile_bookmarks_root() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ true);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ }],
+ }, "Should restore mobile bookmarks from root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_root() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_root_merge.json",
+ /* replace */ false);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "Utodo9b0oVws", index: 0 },
+ { guid: "X6lUyOspVYwi", index: 1 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "a17yW6-nTxEJ", index: 0 },
+ { guid: "xV10h9Wi3FBM", index: 1 },
+ { guid: "_o8e1_zxTJFg", index: 2 },
+ { guid: "QCtSqkVYUbXB", index: 3 },
+ ],
+ }],
+ }, "Should merge bookmarks root contents");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_mobile_bookmarks_folder() {
+ yield* importFromFixture("mobile_bookmarks_folder_import.json",
+ /* replace */ true);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "XF4yRP6bTuil", index: 1 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "buy7711R3ZgE", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ }],
+ }, "Should restore mobile bookmark folder contents into mobile root");
+
+ // We rewrite queries to point to the root ID instead of the name
+ // ("MOBILE_BOOKMARKS") so that we don't break them if the user downgrades
+ // to an earlier release channel. This can be removed along with the anno in
+ // bug 1306445.
+ let queryById = yield PlacesUtils.bookmarks.fetch("XF4yRP6bTuil");
+ equal(queryById.url.href, "place:folder=" + PlacesUtils.mobileFolderId,
+ "Should rewrite mobile query to point to root ID");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_folder() {
+ yield* importFromFixture("mobile_bookmarks_folder_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_folder_merge.json",
+ /* replace */ false);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "Utodo9b0oVws", index: 0 },
+ { guid: "X6lUyOspVYwi", index: 1 },
+ { guid: "XF4yRP6bTuil", index: 2 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "buy7711R3ZgE", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "a17yW6-nTxEJ", index: 0 },
+ { guid: "xV10h9Wi3FBM", index: 1 },
+ { guid: "_o8e1_zxTJFg", index: 2 },
+ { guid: "QCtSqkVYUbXB", index: 3 },
+ ],
+ }],
+ }, "Should merge bookmarks folder contents into mobile root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_multiple_bookmarks_folders() {
+ yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+ /* replace */ true);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "buy7711R3ZgE", index: 0 },
+ { guid: "F_LBgd1fS_uQ", index: 1 },
+ { guid: "oIpmQXMWsXvY", index: 2 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "Utodo9b0oVws", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "sSZ86WT9WbN3", index: 0 },
+ { guid: "a17yW6-nTxEJ", index: 1 },
+ ],
+ }],
+ }, "Should restore multiple bookmarks folder contents into root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_multiple_bookmarks_folders() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+ /* replace */ false);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "buy7711R3ZgE", index: 0 },
+ { guid: "F_LBgd1fS_uQ", index: 1 },
+ { guid: "oIpmQXMWsXvY", index: 2 },
+ { guid: "X6lUyOspVYwi", index: 3 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "Utodo9b0oVws", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "sSZ86WT9WbN3", index: 0 },
+ { guid: "a17yW6-nTxEJ", index: 1 },
+ { guid: "_o8e1_zxTJFg", index: 2 },
+ { guid: "QCtSqkVYUbXB", index: 3 },
+ ],
+ }],
+ }, "Should merge multiple mobile folders into root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_isPageInDB.js b/toolkit/components/places/tests/unit/test_isPageInDB.js
new file mode 100644
index 0000000000..249853fa9a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isPageInDB.js
@@ -0,0 +1,10 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+add_task(function* test_execute() {
+ var good_uri = uri("http://mozilla.com");
+ var bad_uri = uri("http://google.com");
+ yield PlacesTestUtils.addVisits({uri: good_uri});
+ do_check_true(yield PlacesTestUtils.isPageInDB(good_uri));
+ do_check_false(yield PlacesTestUtils.isPageInDB(bad_uri));
+});
diff --git a/toolkit/components/places/tests/unit/test_isURIVisited.js b/toolkit/components/places/tests/unit/test_isURIVisited.js
new file mode 100644
index 0000000000..93c010e83e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isURIVisited.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests functionality of the isURIVisited API.
+
+const SCHEMES = {
+ "http://": true,
+ "https://": true,
+ "ftp://": true,
+ "file:///": true,
+ "about:": false,
+// nsIIOService.newURI() can throw if e.g. the app knows about imap://
+// but the account is not set up and so the URL is invalid for it.
+// "imap://": false,
+ "news://": false,
+ "mailbox:": false,
+ "moz-anno:favicon:http://": false,
+ "view-source:http://": false,
+ "chrome://browser/content/browser.xul?": false,
+ "resource://": false,
+ "data:,": false,
+ "wyciwyg:/0/http://": false,
+ "javascript:": false,
+};
+
+var gRunner;
+function run_test()
+{
+ do_test_pending();
+ gRunner = step();
+ gRunner.next();
+}
+
+function* step()
+{
+ let history = Cc["@mozilla.org/browser/history;1"]
+ .getService(Ci.mozIAsyncHistory);
+
+ for (let scheme in SCHEMES) {
+ do_print("Testing scheme " + scheme);
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ do_print("With transition " + t);
+ let transition = PlacesUtils.history.TRANSITIONS[t];
+
+ let uri = NetUtil.newURI(scheme + "mozilla.org/");
+
+ history.isURIVisited(uri, function(aURI, aIsVisited) {
+ do_check_true(uri.equals(aURI));
+ do_check_false(aIsVisited);
+
+ let callback = {
+ handleError: function () {},
+ handleResult: function () {},
+ handleCompletion: function () {
+ do_print("Added visit to " + uri.spec);
+
+ history.isURIVisited(uri, function (aURI2, aIsVisited2) {
+ do_check_true(uri.equals(aURI2));
+ let checker = SCHEMES[scheme] ? do_check_true : do_check_false;
+ checker(aIsVisited2);
+
+ PlacesTestUtils.clearHistory().then(function () {
+ history.isURIVisited(uri, function(aURI3, aIsVisited3) {
+ do_check_true(uri.equals(aURI3));
+ do_check_false(aIsVisited3);
+ gRunner.next();
+ });
+ });
+ });
+ },
+ };
+
+ history.updatePlaces({ uri: uri
+ , visits: [ { transitionType: transition
+ , visitDate: Date.now() * 1000
+ } ]
+ }, callback);
+ });
+ yield undefined;
+ }
+ }
+
+ do_test_finished();
+}
diff --git a/toolkit/components/places/tests/unit/test_isvisited.js b/toolkit/components/places/tests/unit/test_isvisited.js
new file mode 100644
index 0000000000..d7bcc2851a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isvisited.js
@@ -0,0 +1,75 @@
+/* -*- 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/. */
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ var referrer = uri("about:blank");
+
+ // add a http:// uri
+ var uri1 = uri("http://mozilla.com");
+ yield PlacesTestUtils.addVisits({uri: uri1, referrer: referrer});
+ do_check_guid_for_uri(uri1);
+ do_check_true(yield promiseIsURIVisited(uri1));
+
+ // add a https:// uri
+ var uri2 = uri("https://etrade.com");
+ yield PlacesTestUtils.addVisits({uri: uri2, referrer: referrer});
+ do_check_guid_for_uri(uri2);
+ do_check_true(yield promiseIsURIVisited(uri2));
+
+ // add a ftp:// uri
+ var uri3 = uri("ftp://ftp.mozilla.org");
+ yield PlacesTestUtils.addVisits({uri: uri3, referrer: referrer});
+ do_check_guid_for_uri(uri3);
+ do_check_true(yield promiseIsURIVisited(uri3));
+
+ // check if a nonexistent uri is visited
+ var uri4 = uri("http://foobarcheese.com");
+ do_check_false(yield promiseIsURIVisited(uri4));
+
+ // check that certain schemes never show up as visited
+ // even if we attempt to add them to history
+ // see CanAddURI() in nsNavHistory.cpp
+ const URLS = [
+ "about:config",
+ "imap://cyrus.andrew.cmu.edu/archive.imap",
+ "news://new.mozilla.org/mozilla.dev.apps.firefox",
+ "mailbox:Inbox",
+ "moz-anno:favicon:http://mozilla.org/made-up-favicon",
+ "view-source:http://mozilla.org",
+ "chrome://browser/content/browser.xul",
+ "resource://gre-resources/hiddenWindow.html",
+ "data:,Hello%2C%20World!",
+ "wyciwyg:/0/http://mozilla.org",
+ "javascript:alert('hello wolrd!');",
+ "http://localhost/" + "a".repeat(1984),
+ ];
+ for (let currentURL of URLS) {
+ try {
+ var cantAddUri = uri(currentURL);
+ }
+ catch (e) {
+ // nsIIOService.newURI() can throw if e.g. our app knows about imap://
+ // but the account is not set up and so the URL is invalid for us.
+ // Note this in the log but ignore as it's not the subject of this test.
+ do_print("Could not construct URI for '" + currentURL + "'; ignoring");
+ }
+ if (cantAddUri) {
+ PlacesTestUtils.addVisits({uri: cantAddUri, referrer: referrer}).then(() => {
+ do_throw("Should not have added history for invalid URI.");
+ }, error => {
+ do_check_true(error.message.includes("No items were added to history"));
+ });
+ do_check_false(yield promiseIsURIVisited(cantAddUri));
+ }
+ }
+});
+
diff --git a/toolkit/components/places/tests/unit/test_keywords.js b/toolkit/components/places/tests/unit/test_keywords.js
new file mode 100644
index 0000000000..57b734c5d8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -0,0 +1,548 @@
+"use strict"
+
+function* check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) {
+ // Check case-insensitivity.
+ aKeyword = aKeyword.toUpperCase();
+
+ let entry = yield PlacesUtils.keywords.fetch(aKeyword);
+
+ Assert.deepEqual(entry, yield PlacesUtils.keywords.fetch({ keyword: aKeyword }));
+
+ if (aExpectExists) {
+ Assert.ok(!!entry, "A keyword should exist");
+ Assert.equal(entry.url.href, aHref);
+ Assert.equal(entry.postData, aPostData);
+ Assert.deepEqual(entry, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }));
+ let entries = [];
+ yield PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e));
+ Assert.ok(entries.some(e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase()));
+ } else {
+ Assert.ok(!entry || entry.url.href != aHref,
+ "The given keyword entry should not exist");
+ Assert.equal(null, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }));
+ }
+}
+
+/**
+ * Polls the keywords cache waiting for the given keyword entry.
+ */
+function* promiseKeyword(keyword, expectedHref) {
+ let href = null;
+ do {
+ yield new Promise(resolve => do_timeout(100, resolve));
+ let entry = yield PlacesUtils.keywords.fetch(keyword);
+ if (entry)
+ href = entry.url.href;
+ } while (href != expectedHref);
+}
+
+function* check_no_orphans() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(
+ `SELECT id FROM moz_keywords k
+ WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id)
+ `);
+ Assert.equal(rows.length, 0);
+}
+
+function expectBookmarkNotifications() {
+ let notifications = [];
+ let observer = new Proxy(NavBookmarkObserver, {
+ get(target, name) {
+ if (name == "check") {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ return expectedNotifications =>
+ Assert.deepEqual(notifications, expectedNotifications);
+ }
+
+ if (name.startsWith("onItemChanged")) {
+ return function(itemId, property) {
+ if (property != "keyword")
+ return;
+ let args = Array.from(arguments, arg => {
+ if (arg && arg instanceof Ci.nsIURI)
+ return new URL(arg.spec);
+ if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
+ return new Date(parseInt(arg/1000));
+ return arg;
+ });
+ notifications.push({ name: name, arguments: args });
+ }
+ }
+
+ if (name in target)
+ return target[name];
+ return undefined;
+ }
+ });
+ PlacesUtils.bookmarks.addObserver(observer, false);
+ return observer;
+}
+
+add_task(function* test_invalid_input() {
+ Assert.throws(() => PlacesUtils.keywords.fetch(null),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch(5),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch(undefined),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: null }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: {} }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: 5 }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({}),
+ /At least keyword or url must be provided/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: "test" }, "test"),
+ /onResult callback must be a valid function/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: "test" }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: {} }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: null }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: "" }),
+ /is not a valid URL/);
+
+ Assert.throws(() => PlacesUtils.keywords.insert(null),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.keywords.insert("test"),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.keywords.insert(undefined),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: null }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: 5 }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "" }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }),
+ /Invalid POST data/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }),
+ /Invalid POST data/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test" }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "" }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: null }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }),
+ /is not a valid URL/);
+
+ Assert.throws(() => PlacesUtils.keywords.remove(null),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.remove(""),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.remove(5),
+ /Invalid keyword/);
+});
+
+add_task(function* test_addKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+ let observer = expectBookmarkNotifications();
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ observer.check([]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword
+
+ // Check using URL.
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: new URL("http://example.com/") });
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield PlacesUtils.keywords.remove("keyword");
+ yield check_keyword(false, "http://example.com/", "keyword");
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_addBookmarkAndKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+
+ let observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "keyword",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // -1 keyword
+
+ // Add again the keyword, then remove the bookmark.
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.bookmarks.remove(bookmark.guid);
+ // the notification is synchronous but the removal process is async.
+ // Unfortunately there's nothing explicit we can wait for.
+ while ((yield foreign_count("http://example.com/")));
+ // We don't get any itemChanged notification since the bookmark has been
+ // removed already.
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_addKeywordToURIHavingKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ observer.check([]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" });
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 keyword
+ let entries = [];
+ let entry = yield PlacesUtils.keywords.fetch({ url: "http://example.com/" }, e => entries.push(e));
+ Assert.equal(entries.length, 2);
+ Assert.deepEqual(entries[0], entry);
+
+ // Now remove the keywords.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ yield PlacesUtils.keywords.remove("keyword2");
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+ yield check_keyword(false, "http://example.com/", "keyword2");
+ Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_addBookmarkToURIHavingKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+ let observer = expectBookmarkNotifications();
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ observer.check([]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword
+
+ observer = expectBookmarkNotifications();
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark
+ observer.check([]);
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.bookmarks.remove(bookmark.guid);
+ // the notification is synchronous but the removal process is async.
+ // Unfortunately there's nothing explicit we can wait for.
+ while ((yield foreign_count("http://example.com/")));
+ // We don't get any itemChanged notification since the bookmark has been
+ // removed already.
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_sameKeywordDifferentURL() {
+ let fc1 = yield foreign_count("http://example1.com/");
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let fc2 = yield foreign_count("http://example2.com/");
+ let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example2.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example1.com/" });
+
+ yield check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword
+ yield check_keyword(false, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // +1 bookmark
+
+ // Assign the same keyword to another url.
+ let observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/" });
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "keyword",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); // -1 keyword
+ yield check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ yield check_keyword(false, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1);
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // -1 keyword
+
+ yield PlacesUtils.bookmarks.remove(bookmark1);
+ yield PlacesUtils.bookmarks.remove(bookmark2);
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark
+ while ((yield foreign_count("http://example2.com/"))); // -1 keyword
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_sameURIDifferentKeyword() {
+ let fc = yield foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({keyword: "keyword", url: "http://example.com/" });
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "keyword",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" });
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +1 keyword
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "keyword2",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ // Add a third keyword.
+ yield PlacesUtils.keywords.insert({ keyword: "keyword3", url: "http://example.com/" });
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ yield check_keyword(true, "http://example.com/", "keyword3");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 4); // +1 keyword
+
+ // Remove one of the keywords.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ yield check_keyword(false, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ yield check_keyword(true, "http://example.com/", "keyword3");
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // -1 keyword
+
+ // Now remove the bookmark.
+ yield PlacesUtils.bookmarks.remove(bookmark);
+ while ((yield foreign_count("http://example.com/")));
+ yield check_keyword(false, "http://example.com/", "keyword");
+ yield check_keyword(false, "http://example.com/", "keyword2");
+ yield check_keyword(false, "http://example.com/", "keyword3");
+
+ check_no_orphans();
+});
+
+add_task(function* test_deleteKeywordMultipleBookmarks() {
+ let fc = yield foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +2 bookmark +1 keyword
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "keyword",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "keyword",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ yield check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // -1 keyword
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ // Now remove the bookmarks.
+ yield PlacesUtils.bookmarks.remove(bookmark1);
+ yield PlacesUtils.bookmarks.remove(bookmark2);
+ Assert.equal((yield foreign_count("http://example.com/")), fc); // -2 bookmarks
+
+ check_no_orphans();
+});
+
+add_task(function* test_multipleKeywordsSamePostData() {
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1" });
+ yield check_keyword(true, "http://example.com/", "keyword", "postData1");
+ // Add another keyword with same postData, should fail.
+ yield Assert.rejects(PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1" }),
+ /constraint failed/);
+ yield check_keyword(false, "http://example.com/", "keyword2", "postData1");
+
+ yield PlacesUtils.keywords.remove("keyword");
+
+ check_no_orphans();
+});
+
+add_task(function* test_oldPostDataAPI() {
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ let itemId = yield PlacesUtils.promiseItemId(bookmark.guid);
+ yield PlacesUtils.setPostDataForBookmark(itemId, "postData");
+ yield check_keyword(true, "http://example.com/", "keyword", "postData");
+ Assert.equal(PlacesUtils.getPostDataForBookmark(itemId), "postData");
+
+ yield PlacesUtils.keywords.remove("keyword");
+ yield PlacesUtils.bookmarks.remove(bookmark);
+
+ check_no_orphans();
+});
+
+add_task(function* test_oldKeywordsAPI() {
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let itemId = yield PlacesUtils.promiseItemId(bookmark.guid);
+
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+ yield promiseKeyword("keyword", "http://example.com/");
+
+ // Remove the keyword.
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "");
+ yield promiseKeyword("keyword", null);
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" });
+ Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword");
+ Assert.equal(PlacesUtils.bookmarks.getURIForKeyword("keyword").spec, "http://example.com/");
+ yield PlacesUtils.bookmarks.remove(bookmark);
+
+ check_no_orphans();
+});
+
+add_task(function* test_bookmarkURLChange() {
+ let fc1 = yield foreign_count("http://example1.com/");
+ let fc2 = yield foreign_count("http://example2.com/");
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword",
+ url: "http://example1.com/" });
+
+ yield check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword
+
+ yield PlacesUtils.bookmarks.update({ guid: bookmark.guid,
+ url: "http://example2.com/"});
+ yield promiseKeyword("keyword", "http://example2.com/");
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ yield check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark -1 keyword
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 bookmark +1 keyword
+});
diff --git a/toolkit/components/places/tests/unit/test_lastModified.js b/toolkit/components/places/tests/unit/test_lastModified.js
new file mode 100644
index 0000000000..c754949327
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_lastModified.js
@@ -0,0 +1,34 @@
+/* -*- 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/. */
+
+ /**
+ * Test that inserting a new bookmark will set lastModified to the same
+ * values as dateAdded.
+ */
+// main
+function run_test() {
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var itemId = bs.insertBookmark(bs.bookmarksMenuFolder,
+ uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "itemTitle");
+ var dateAdded = bs.getItemDateAdded(itemId);
+ do_check_eq(dateAdded, bs.getItemLastModified(itemId));
+
+ // Change lastModified, then change dateAdded. LastModified should be set
+ // to the new dateAdded.
+ // This could randomly fail on virtual machines due to timing issues, so
+ // we manually increase the time value. See bug 500640 for details.
+ bs.setItemLastModified(itemId, dateAdded + 1000);
+ do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1000);
+ do_check_true(bs.getItemDateAdded(itemId) < bs.getItemLastModified(itemId));
+ bs.setItemDateAdded(itemId, dateAdded + 2000);
+ do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2000);
+ do_check_eq(bs.getItemDateAdded(itemId), bs.getItemLastModified(itemId));
+
+ bs.removeItem(itemId);
+}
diff --git a/toolkit/components/places/tests/unit/test_markpageas.js b/toolkit/components/places/tests/unit/test_markpageas.js
new file mode 100644
index 0000000000..ba4f740c66
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_markpageas.js
@@ -0,0 +1,61 @@
+/* -*- 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 gVisits = [{url: "http://www.mozilla.com/",
+ transition: TRANSITION_TYPED},
+ {url: "http://www.google.com/",
+ transition: TRANSITION_BOOKMARK},
+ {url: "http://www.espn.com/",
+ transition: TRANSITION_LINK}];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ let observer;
+ let completionPromise = new Promise(resolveCompletionPromise => {
+ observer = {
+ __proto__: NavHistoryObserver.prototype,
+ _visitCount: 0,
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType, aAdded)
+ {
+ do_check_eq(aURI.spec, gVisits[this._visitCount].url);
+ do_check_eq(aTransitionType, gVisits[this._visitCount].transition);
+ this._visitCount++;
+
+ if (this._visitCount == gVisits.length) {
+ resolveCompletionPromise();
+ }
+ },
+ };
+ });
+
+ PlacesUtils.history.addObserver(observer, false);
+
+ for (var visit of gVisits) {
+ if (visit.transition == TRANSITION_TYPED)
+ PlacesUtils.history.markPageAsTyped(uri(visit.url));
+ else if (visit.transition == TRANSITION_BOOKMARK)
+ PlacesUtils.history.markPageAsFollowedBookmark(uri(visit.url))
+ else {
+ // because it is a top level visit with no referrer,
+ // it will result in TRANSITION_LINK
+ }
+ yield PlacesTestUtils.addVisits({
+ uri: uri(visit.url),
+ transition: visit.transition
+ });
+ }
+
+ yield completionPromise;
+
+ PlacesUtils.history.removeObserver(observer);
+});
+
diff --git a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
new file mode 100644
index 0000000000..5136591ba4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
@@ -0,0 +1,514 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests functionality of the mozIAsyncLivemarks interface.
+
+const FEED_URI = NetUtil.newURI("http://feed.rss/");
+const SITE_URI = NetUtil.newURI("http://site.org/");
+
+// This test must be the first one, since it's testing the cache.
+add_task(function* test_livemark_cache() {
+ // Add a livemark through other APIs.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "test",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ let id = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations
+ .setItemAnnotation(id, PlacesUtils.LMANNO_FEEDURI,
+ "http://example.com/feed",
+ 0, PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations
+ .setItemAnnotation(id, PlacesUtils.LMANNO_SITEURI,
+ "http://example.com/site",
+ 0, PlacesUtils.annotations.EXPIRE_NEVER);
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ guid: folder.guid });
+ Assert.equal(folder.guid, livemark.guid);
+ Assert.equal(folder.dateAdded * 1000, livemark.dateAdded);
+ Assert.equal(folder.parentGuid, livemark.parentGuid);
+ Assert.equal(folder.index, livemark.index);
+ Assert.equal(folder.title, livemark.title);
+ Assert.equal(id, livemark.id);
+ Assert.equal(PlacesUtils.unfiledBookmarksFolderId, livemark.parentId);
+ Assert.equal("http://example.com/feed", livemark.feedURI.spec);
+ Assert.equal("http://example.com/site", livemark.siteURI.spec);
+
+ yield PlacesUtils.livemarks.removeLivemark(livemark);
+});
+
+add_task(function* test_addLivemark_noArguments_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark();
+ do_throw("Invoking addLivemark with no arguments should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
+ }
+});
+
+add_task(function* test_addLivemark_emptyObject_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({});
+ do_throw("Invoking addLivemark with empty object should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badParentId_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({ parentId: "test" });
+ do_throw("Invoking addLivemark with a bad parent id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_invalidParentId_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({ parentId: -2 });
+ do_throw("Invoking addLivemark with an invalid parent id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_noIndex_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({
+ parentId: PlacesUtils.unfiledBookmarksFolderId });
+ do_throw("Invoking addLivemark with no index should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badIndex_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentId: PlacesUtils.unfiledBookmarksFolderId
+ , index: "test" });
+ do_throw("Invoking addLivemark with a bad index should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_invalidIndex_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentId: PlacesUtils.unfiledBookmarksFolderId
+ , index: -2
+ });
+ do_throw("Invoking addLivemark with an invalid index should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_noFeedURI_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ do_throw("Invoking addLivemark with no feedURI should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badFeedURI_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: "test" });
+ do_throw("Invoking addLivemark with a bad feedURI should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badSiteURI_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , siteURI: "test" });
+ do_throw("Invoking addLivemark with a bad siteURI should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badGuid_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfileGuid
+ , feedURI: FEED_URI
+ , guid: "123456" });
+ do_throw("Invoking addLivemark with a bad guid should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_parentId_succeeds() {
+ let onItemAddedCalled = false;
+ PlacesUtils.bookmarks.addObserver({
+ __proto__: NavBookmarkObserver.prototype,
+ onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI, aTitle)
+ {
+ onItemAddedCalled = true;
+ PlacesUtils.bookmarks.removeObserver(this);
+ do_check_eq(aParentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(aIndex, 0);
+ do_check_eq(aItemType, Ci.nsINavBookmarksService.TYPE_FOLDER);
+ do_check_eq(aTitle, "test");
+ }
+ }, false);
+
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentId: PlacesUtils.unfiledBookmarksFolderId
+ , feedURI: FEED_URI });
+ do_check_true(onItemAddedCalled);
+});
+
+
+add_task(function* test_addLivemark_noSiteURI_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+ do_check_true(livemark.id > 0);
+ do_check_valid_places_guid(livemark.guid);
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_true(livemark.lastModified > 0);
+ do_check_true(is_time_ordered(livemark.dateAdded, livemark.lastModified));
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch(livemark.guid);
+ do_check_eq(livemark.index, bookmark.index);
+ do_check_eq(livemark.dateAdded, bookmark.dateAdded * 1000);
+});
+
+add_task(function* test_addLivemark_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , siteURI: SITE_URI
+ });
+
+ do_check_true(livemark.id > 0);
+ do_check_valid_places_guid(livemark.guid);
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_true(livemark.siteURI.equals(SITE_URI));
+ do_check_true(PlacesUtils.annotations
+ .itemHasAnnotation(livemark.id,
+ PlacesUtils.LMANNO_FEEDURI));
+ do_check_true(PlacesUtils.annotations
+ .itemHasAnnotation(livemark.id,
+ PlacesUtils.LMANNO_SITEURI));
+});
+
+add_task(function* test_addLivemark_bogusid_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { id: 100 // Should be ignored.
+ , title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , siteURI: SITE_URI
+ });
+ do_check_true(livemark.id > 0);
+ do_check_neq(livemark.id, 100);
+});
+
+add_task(function* test_addLivemark_bogusParentId_fails() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentId: 187
+ , feedURI: FEED_URI
+ });
+ do_throw("Adding a livemark with a bogus parent should fail");
+ } catch (ex) {}
+});
+
+add_task(function* test_addLivemark_bogusParentGuid_fails() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: "123456789012"
+ , feedURI: FEED_URI
+ });
+ do_throw("Adding a livemark with a bogus parent should fail");
+ } catch (ex) {}
+})
+
+add_task(function* test_addLivemark_intoLivemark_fails() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: livemark.guid
+ , feedURI: FEED_URI
+ });
+ do_throw("Adding a livemark into a livemark should fail");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_forceGuid_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , guid: "1234567890AB"
+ });
+ do_check_eq(livemark.guid, "1234567890AB");
+ do_check_guid_for_bookmark(livemark.id, "1234567890AB");
+});
+
+add_task(function* test_addLivemark_dateAdded_succeeds() {
+ let dateAdded = new Date("2013-03-01T01:10:00") * 1000;
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , dateAdded
+ });
+ do_check_eq(livemark.dateAdded, dateAdded);
+});
+
+add_task(function* test_addLivemark_lastModified_succeeds() {
+ let now = Date.now() * 1000;
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , lastModified: now
+ });
+ do_check_eq(livemark.dateAdded, now);
+ do_check_eq(livemark.lastModified, now);
+});
+
+add_task(function* test_removeLivemark_emptyObject_throws() {
+ try {
+ yield PlacesUtils.livemarks.removeLivemark({});
+ do_throw("Invoking removeLivemark with empty object should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_removeLivemark_noValidId_throws() {
+ try {
+ yield PlacesUtils.livemarks.removeLivemark({ id: -10, guid: "test"});
+ do_throw("Invoking removeLivemark with no valid id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_removeLivemark_nonExistent_fails() {
+ try {
+ yield PlacesUtils.livemarks.removeLivemark({ id: 1337 });
+ do_throw("Removing a non-existent livemark should fail");
+ }
+ catch (ex) {
+ }
+});
+
+add_task(function* test_removeLivemark_guid_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , guid: "234567890ABC"
+ });
+
+ do_check_eq(livemark.guid, "234567890ABC");
+
+ yield PlacesUtils.livemarks.removeLivemark({
+ id: 789, guid: "234567890ABC"
+ });
+
+ do_check_eq((yield PlacesUtils.bookmarks.fetch("234567890ABC")), null);
+});
+
+add_task(function* test_removeLivemark_id_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ yield PlacesUtils.livemarks.removeLivemark({ id: livemark.id });
+
+ do_check_eq((yield PlacesUtils.bookmarks.fetch("234567890ABC")), null);
+});
+
+add_task(function* test_getLivemark_emptyObject_throws() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({});
+ do_throw("Invoking getLivemark with empty object should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_getLivemark_noValidId_throws() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ id: -10, guid: "test"});
+ do_throw("Invoking getLivemark with no valid id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_getLivemark_nonExistentId_fails() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ id: 1234 });
+ do_throw("getLivemark for a non existent id should fail");
+ } catch (ex) {}
+});
+
+add_task(function* test_getLivemark_nonExistentGUID_fails() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" });
+ do_throw("getLivemark for a non-existent guid should fail");
+ } catch (ex) {}
+});
+
+add_task(function* test_getLivemark_guid_succeeds() {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , guid: "34567890ABCD" });
+
+ // invalid id to check the guid wins.
+ let livemark =
+ yield PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" });
+
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_eq(livemark.guid, "34567890ABCD");
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch("34567890ABCD");
+ do_check_eq(livemark.index, bookmark.index);
+});
+
+add_task(function* test_getLivemark_id_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemark.id });
+
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_guid_for_bookmark(livemark.id, livemark.guid);
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch(livemark.guid);
+ do_check_eq(livemark.index, bookmark.index);
+});
+
+add_task(function* test_getLivemark_removeItem_contention() {
+ // do not yield.
+ PlacesUtils.livemarks.addLivemark({ title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ }).catch(() => { /* swallow errors*/ });
+ yield PlacesUtils.bookmarks.eraseEverything();
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ livemark = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_guid_for_bookmark(livemark.id, livemark.guid);
+});
+
+add_task(function* test_title_change() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ yield PlacesUtils.bookmarks.update({ guid: livemark.guid,
+ title: "test2" });
+ // Poll for the title change.
+ while (true) {
+ let lm = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ if (lm.title == "test2")
+ break;
+ yield new Promise(resolve => do_timeout(resolve, 100));
+ }
+});
+
+add_task(function* test_livemark_move() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI } );
+
+ yield PlacesUtils.bookmarks.update({ guid: livemark.guid,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX });
+ // Poll for the parent change.
+ while (true) {
+ let lm = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ if (lm.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
+ break;
+ yield new Promise(resolve => do_timeout(resolve, 100));
+ }
+});
+
+add_task(function* test_livemark_removed() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI } );
+
+ yield PlacesUtils.bookmarks.remove(livemark.guid);
+ // Poll for the livemark removal.
+ while (true) {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ } catch (ex) {
+ break;
+ }
+ yield new Promise(resolve => do_timeout(resolve, 100));
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_multi_queries.js b/toolkit/components/places/tests/unit/test_multi_queries.js
new file mode 100644
index 0000000000..d485355a5c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_multi_queries.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/. */
+
+/**
+ * Adds a test URI visit to history.
+ *
+ * @param aURI
+ * The URI to add a visit for.
+ * @param aReferrer
+ * The referring URI for the given URI. This can be null.
+ */
+function* add_visit(aURI, aDayOffset, aTransition) {
+ yield PlacesTestUtils.addVisits({
+ uri: aURI,
+ transition: aTransition,
+ visitDate: (Date.now() + aDayOffset*86400000) * 1000
+ });
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ yield add_visit(uri("http://mirror1.mozilla.com/a"), -1, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror2.mozilla.com/b"), -2, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror3.mozilla.com/c"), -4, TRANSITION_FRAMED_LINK);
+ yield add_visit(uri("http://mirror1.google.com/b"), -1, TRANSITION_EMBED);
+ yield add_visit(uri("http://mirror2.google.com/a"), -2, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror1.apache.org/b"), -3, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror2.apache.org/a"), -4, TRANSITION_FRAMED_LINK);
+
+ let queries = [
+ PlacesUtils.history.getNewQuery(),
+ PlacesUtils.history.getNewQuery()
+ ];
+ queries[0].domain = "mozilla.com";
+ queries[1].domain = "google.com";
+
+ let root = PlacesUtils.history.executeQueries(
+ queries, queries.length, PlacesUtils.history.getNewQueryOptions()
+ ).root;
+ root.containerOpen = true;
+ let childCount = root.childCount;
+ root.containerOpen = false;
+
+ do_check_eq(childCount, 3);
+});
diff --git a/toolkit/components/places/tests/unit/test_multi_word_tags.js b/toolkit/components/places/tests/unit/test_multi_word_tags.js
new file mode 100644
index 0000000000..6a0e5f1304
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_multi_word_tags.js
@@ -0,0 +1,150 @@
+/* -*- 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/. */
+
+// Get history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+}
+catch (ex) {
+ do_throw("Could not get the nav-bookmarks-service\n");
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+// main
+function run_test() {
+ var uri1 = uri("http://site.tld/1");
+ var uri2 = uri("http://site.tld/2");
+ var uri3 = uri("http://site.tld/3");
+ var uri4 = uri("http://site.tld/4");
+ var uri5 = uri("http://site.tld/5");
+ var uri6 = uri("http://site.tld/6");
+
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri1, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri2, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri3, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri4, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri5, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri6, bmsvc.DEFAULT_INDEX, null);
+
+ tagssvc.tagURI(uri1, ["foo"]);
+ tagssvc.tagURI(uri2, ["bar"]);
+ tagssvc.tagURI(uri3, ["cheese"]);
+ tagssvc.tagURI(uri4, ["foo bar"]);
+ tagssvc.tagURI(uri5, ["bar cheese"]);
+ tagssvc.tagURI(uri6, ["foo bar cheese"]);
+
+ // exclude livemark items, search for "item", should get one result
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ var query = histsvc.getNewQuery();
+ query.searchTerms = "foo";
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 3);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/1");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/4");
+ do_check_eq(root.getChild(2).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 4);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/2");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/4");
+ do_check_eq(root.getChild(2).uri, "http://site.tld/5");
+ do_check_eq(root.getChild(3).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 3);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/3");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/5");
+ do_check_eq(root.getChild(2).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "foo bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/4");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "bar foo";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/4");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "bar cheese";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/5");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/5");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "foo bar cheese";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese foo bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese bar foo";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
new file mode 100644
index 0000000000..037ab7d085
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
@@ -0,0 +1,256 @@
+/* -*- 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/. */
+
+// Get history service
+var histsvc = PlacesUtils.history;
+var bhist = PlacesUtils.bhistory;
+var bmsvc = PlacesUtils.bookmarks;
+
+var resultObserver = {
+ insertedNode: null,
+ nodeInserted: function(parent, node, newIndex) {
+ this.insertedNode = node;
+ },
+ removedNode: null,
+ nodeRemoved: function(parent, node, oldIndex) {
+ this.removedNode = node;
+ },
+
+ nodeAnnotationChanged: function() {},
+
+ newTitle: "",
+ nodeChangedByTitle: null,
+ nodeTitleChanged: function(node, newTitle) {
+ this.nodeChangedByTitle = node;
+ this.newTitle = newTitle;
+ },
+
+ newAccessCount: 0,
+ newTime: 0,
+ nodeChangedByHistoryDetails: null,
+ nodeHistoryDetailsChanged: function(node,
+ updatedVisitDate,
+ updatedVisitCount) {
+ this.nodeChangedByHistoryDetails = node
+ this.newTime = updatedVisitDate;
+ this.newAccessCount = updatedVisitCount;
+ },
+
+ movedNode: null,
+ nodeMoved: function(node, oldParent, oldIndex, newParent, newIndex) {
+ this.movedNode = node;
+ },
+ openedContainer: null,
+ closedContainer: null,
+ containerStateChanged: function (aNode, aOldState, aNewState) {
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ this.openedContainer = aNode;
+ }
+ else if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
+ this.closedContainer = aNode;
+ }
+ },
+ invalidatedContainer: null,
+ invalidateContainer: function(node) {
+ this.invalidatedContainer = node;
+ },
+ sortingMode: null,
+ sortingChanged: function(sortingMode) {
+ this.sortingMode = sortingMode;
+ },
+ inBatchMode: false,
+ batching: function(aToggleMode) {
+ do_check_neq(this.inBatchMode, aToggleMode);
+ this.inBatchMode = aToggleMode;
+ },
+ result: null,
+ reset: function() {
+ this.insertedNode = null;
+ this.removedNode = null;
+ this.nodeChangedByTitle = null;
+ this.nodeChangedByHistoryDetails = null;
+ this.replacedNode = null;
+ this.movedNode = null;
+ this.openedContainer = null;
+ this.closedContainer = null;
+ this.invalidatedContainer = null;
+ this.sortingMode = null;
+ }
+};
+
+var testURI = uri("http://mozilla.com");
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function check_history_query() {
+ var options = histsvc.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ var query = histsvc.getNewQuery();
+ var result = histsvc.executeQuery(query, options);
+ result.addObserver(resultObserver, false);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_neq(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.nodeInserted
+ // add a visit
+ PlacesTestUtils.addVisits(testURI).then(function() {
+ do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
+
+ // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+ // adding a visit causes nodeHistoryDetailsChanged for the folder
+ do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+ // nsINavHistoryResultObserver.itemTitleChanged for a leaf node
+ PlacesTestUtils.addVisits({ uri: testURI, title: "baz" }).then(function () {
+ do_check_eq(resultObserver.nodeChangedByTitle.title, "baz");
+
+ // nsINavHistoryResultObserver.nodeRemoved
+ var removedURI = uri("http://google.com");
+ PlacesTestUtils.addVisits(removedURI).then(function() {
+ bhist.removePage(removedURI);
+ do_check_eq(removedURI.spec, resultObserver.removedNode.uri);
+
+ // nsINavHistoryResultObserver.invalidateContainer
+ bhist.removePagesFromHost("mozilla.com", false);
+ do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+
+ // nsINavHistoryResultObserver.sortingChanged
+ resultObserver.invalidatedContainer = null;
+ result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
+ do_check_eq(resultObserver.invalidatedContainer, result.root);
+
+ // nsINavHistoryResultObserver.invalidateContainer
+ PlacesTestUtils.clearHistoryEnabled().then(() => {
+ do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+
+ // nsINavHistoryResultObserver.batching
+ do_check_false(resultObserver.inBatchMode);
+ histsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+ bmsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+
+ root.containerOpen = false;
+ do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ PlacesTestUtils.promiseAsyncUpdates().then(run_next_test);
+ });
+ });
+ });
+ });
+});
+
+add_test(function check_bookmarks_query() {
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.bookmarksMenuFolder], 1);
+ var result = histsvc.executeQuery(query, options);
+ result.addObserver(resultObserver, false);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_neq(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.nodeInserted
+ // add a bookmark
+ var testBookmark = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, bmsvc.DEFAULT_INDEX, "foo");
+ do_check_eq("foo", resultObserver.insertedNode.title);
+ do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
+
+ // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+ // adding a visit causes nodeHistoryDetailsChanged for the folder
+ do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+ // nsINavHistoryResultObserver.nodeTitleChanged for a leaf node
+ bmsvc.setItemTitle(testBookmark, "baz");
+ do_check_eq(resultObserver.nodeChangedByTitle.title, "baz");
+ do_check_eq(resultObserver.newTitle, "baz");
+
+ var testBookmark2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri("http://google.com"), bmsvc.DEFAULT_INDEX, "foo");
+ bmsvc.moveItem(testBookmark2, bmsvc.bookmarksMenuFolder, 0);
+ do_check_eq(resultObserver.movedNode.itemId, testBookmark2);
+
+ // nsINavHistoryResultObserver.nodeRemoved
+ bmsvc.removeItem(testBookmark2);
+ do_check_eq(testBookmark2, resultObserver.removedNode.itemId);
+
+ // XXX nsINavHistoryResultObserver.invalidateContainer
+
+ // nsINavHistoryResultObserver.sortingChanged
+ resultObserver.invalidatedContainer = null;
+ result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
+ do_check_eq(resultObserver.invalidatedContainer, result.root);
+
+ // nsINavHistoryResultObserver.batching
+ do_check_false(resultObserver.inBatchMode);
+ histsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+ bmsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+
+ root.containerOpen = false;
+ do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ PlacesTestUtils.promiseAsyncUpdates().then(run_next_test);
+});
+
+add_test(function check_mixed_query() {
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.onlyBookmarked = true;
+ var result = histsvc.executeQuery(query, options);
+ result.addObserver(resultObserver, false);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_neq(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.batching
+ do_check_false(resultObserver.inBatchMode);
+ histsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+ bmsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+
+ root.containerOpen = false;
+ do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ PlacesTestUtils.promiseAsyncUpdates().then(run_next_test);
+});
diff --git a/toolkit/components/places/tests/unit/test_null_interfaces.js b/toolkit/components/places/tests/unit/test_null_interfaces.js
new file mode 100644
index 0000000000..524837ca3d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_null_interfaces.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.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 489872 to make sure passing nulls to nsNavHistory doesn't crash.
+ */
+
+// Make an array of services to test, each specifying a class id, interface
+// and an array of function names that don't throw when passed nulls
+var testServices = [
+ ["browser/nav-history-service;1",
+ ["nsINavHistoryService"],
+ ["queryStringToQueries", "removePagesByTimeframe", "removePagesFromHost", "getObservers"]
+ ],
+ ["browser/nav-bookmarks-service;1",
+ ["nsINavBookmarksService", "nsINavHistoryObserver", "nsIAnnotationObserver"],
+ ["createFolder", "getObservers", "onFrecencyChanged", "onTitleChanged",
+ "onPageAnnotationSet", "onPageAnnotationRemoved", "onDeleteURI"]
+ ],
+ ["browser/livemark-service;2", ["mozIAsyncLivemarks"], ["reloadLivemarks"]],
+ ["browser/annotation-service;1", ["nsIAnnotationService"], []],
+ ["browser/favicon-service;1", ["nsIFaviconService"], []],
+ ["browser/tagging-service;1", ["nsITaggingService"], []],
+];
+do_print(testServices.join("\n"));
+
+function run_test()
+{
+ for (let [cid, ifaces, nothrow] of testServices) {
+ do_print(`Running test with ${cid} ${ifaces.join(", ")} ${nothrow}`);
+ let s = Cc["@mozilla.org/" + cid].getService(Ci.nsISupports);
+ for (let iface of ifaces) {
+ s.QueryInterface(Ci[iface]);
+ }
+
+ let okName = function(name) {
+ do_print(`Checking if function is okay to test: ${name}`);
+ let func = s[name];
+
+ let mesg = "";
+ if (typeof func != "function")
+ mesg = "Not a function!";
+ else if (func.length == 0)
+ mesg = "No args needed!";
+ else if (name == "QueryInterface")
+ mesg = "Ignore QI!";
+
+ if (mesg) {
+ do_print(`${mesg} Skipping: ${name}`);
+ return false;
+ }
+
+ return true;
+ }
+
+ do_print(`Generating an array of functions to test service: ${s}`);
+ for (let n of Object.keys(s).filter(i => okName(i)).sort()) {
+ do_print(`\nTesting ${ifaces.join(", ")} function with null args: ${n}`);
+
+ let func = s[n];
+ let num = func.length;
+ do_print(`Generating array of nulls for #args: ${num}`);
+ let args = Array(num).fill(null);
+
+ let tryAgain = true;
+ while (tryAgain == true) {
+ try {
+ do_print(`Calling with args: ${JSON.stringify(args)}`);
+ func.apply(s, args);
+
+ do_print(`The function did not throw! Is it one of the nothrow? ${nothrow}`);
+ Assert.notEqual(nothrow.indexOf(n), -1);
+
+ do_print("Must have been an expected nothrow, so no need to try again");
+ tryAgain = false;
+ }
+ catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ do_print(`Caught an expected exception: ${ex.name}`);
+ do_print("Moving on to the next test..");
+ tryAgain = false;
+ } else if (ex.result == Cr.NS_ERROR_XPC_NEED_OUT_OBJECT) {
+ let pos = Number(ex.message.match(/object arg (\d+)/)[1]);
+ do_print(`Function call expects an out object at ${pos}`);
+ args[pos] = {};
+ } else if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
+ do_print(`Method not implemented exception: ${ex.name}`);
+ do_print("Moving on to the next test..");
+ tryAgain = false;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_onItemChanged_tags.js b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
new file mode 100644
index 0000000000..7a0eb354d0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that changing a tag for a bookmark with multiple tags
+// notifies OnItemChanged("tags") only once, and not once per tag.
+
+function run_test() {
+ do_test_pending();
+
+ let tags = ["a", "b", "c"];
+ let uri = NetUtil.newURI("http://1.moz.org/");
+
+ let id = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 1"
+ );
+ PlacesUtils.tagging.tagURI(uri, tags);
+
+ let bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ ]),
+
+ _changedCount: 0,
+ onItemChanged: function (aItemId, aProperty, aIsAnnotationProperty, aValue,
+ aLastModified, aItemType) {
+ if (aProperty == "tags") {
+ do_check_eq(aItemId, id);
+ this._changedCount++;
+ }
+ },
+
+ onItemRemoved: function (aItemId, aParentId, aIndex, aItemType) {
+ if (aItemId == id) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ do_check_eq(this._changedCount, 2);
+ do_test_finished();
+ }
+ },
+
+ onItemAdded: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ };
+ PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+
+ PlacesUtils.tagging.tagURI(uri, ["d"]);
+ PlacesUtils.tagging.tagURI(uri, ["e"]);
+ PlacesUtils.bookmarks.removeItem(id);
+}
diff --git a/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js
new file mode 100644
index 0000000000..f6131b2118
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js
@@ -0,0 +1,179 @@
+/* -*- 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/. */
+
+const bmsvc = PlacesUtils.bookmarks;
+const histsvc = PlacesUtils.history;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_addBookmarksAndCheckGuids() {
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX);
+ bmsvc.insertBookmark(folder, uri("http://test1.com/"),
+ bmsvc.DEFAULT_INDEX, "1 title");
+ bmsvc.insertBookmark(folder, uri("http://test2.com/"),
+ bmsvc.DEFAULT_INDEX, "2 title");
+ bmsvc.insertBookmark(folder, uri("http://test3.com/"),
+ bmsvc.DEFAULT_INDEX, "3 title");
+ bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX);
+ bmsvc.createFolder(folder, "test folder 2", bmsvc.DEFAULT_INDEX);
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ do_check_eq(root.childCount, 5);
+
+ // check bookmark guids
+ let bookmarkGuidZero = root.getChild(0).bookmarkGuid;
+ do_check_eq(bookmarkGuidZero.length, 12);
+ // bookmarks have bookmark guids
+ do_check_eq(root.getChild(1).bookmarkGuid.length, 12);
+ do_check_eq(root.getChild(2).bookmarkGuid.length, 12);
+ // separator has bookmark guid
+ do_check_eq(root.getChild(3).bookmarkGuid.length, 12);
+ // folder has bookmark guid
+ do_check_eq(root.getChild(4).bookmarkGuid.length, 12);
+ // all bookmark guids are different.
+ do_check_neq(bookmarkGuidZero, root.getChild(1).bookmarkGuid);
+ do_check_neq(root.getChild(1).bookmarkGuid, root.getChild(2).bookmarkGuid);
+ do_check_neq(root.getChild(2).bookmarkGuid, root.getChild(3).bookmarkGuid);
+ do_check_neq(root.getChild(3).bookmarkGuid, root.getChild(4).bookmarkGuid);
+
+ // check page guids
+ let pageGuidZero = root.getChild(0).pageGuid;
+ do_check_eq(pageGuidZero.length, 12);
+ // bookmarks have page guids
+ do_check_eq(root.getChild(1).pageGuid.length, 12);
+ do_check_eq(root.getChild(2).pageGuid.length, 12);
+ // folder and separator don't have page guids
+ do_check_eq(root.getChild(3).pageGuid, "");
+ do_check_eq(root.getChild(4).pageGuid, "");
+
+ do_check_neq(pageGuidZero, root.getChild(1).pageGuid);
+ do_check_neq(root.getChild(1).pageGuid, root.getChild(2).pageGuid);
+
+ root.containerOpen = false;
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_updateBookmarksAndCheckGuids() {
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX);
+ let b1 = bmsvc.insertBookmark(folder, uri("http://test1.com/"),
+ bmsvc.DEFAULT_INDEX, "1 title");
+ let f1 = bmsvc.createFolder(folder, "test folder 2", bmsvc.DEFAULT_INDEX);
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ do_check_eq(root.childCount, 2);
+
+ // ensure the bookmark and page guids remain the same after modifing other property.
+ let bookmarkGuidZero = root.getChild(0).bookmarkGuid;
+ let pageGuidZero = root.getChild(0).pageGuid;
+ bmsvc.setItemTitle(b1, "1 title mod");
+ do_check_eq(root.getChild(0).title, "1 title mod");
+ do_check_eq(root.getChild(0).bookmarkGuid, bookmarkGuidZero);
+ do_check_eq(root.getChild(0).pageGuid, pageGuidZero);
+
+ let bookmarkGuidOne = root.getChild(1).bookmarkGuid;
+ let pageGuidOne = root.getChild(1).pageGuid;
+ bmsvc.setItemTitle(f1, "test foolder 234");
+ do_check_eq(root.getChild(1).title, "test foolder 234");
+ do_check_eq(root.getChild(1).bookmarkGuid, bookmarkGuidOne);
+ do_check_eq(root.getChild(1).pageGuid, pageGuidOne);
+
+ root.containerOpen = false;
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_addVisitAndCheckGuid() {
+ // add a visit and test page guid and non-existing bookmark guids.
+ let sourceURI = uri("http://test4.com/");
+ yield PlacesTestUtils.addVisits({ uri: sourceURI });
+ do_check_eq(bmsvc.getBookmarkedURIFor(sourceURI), null);
+
+ let options = histsvc.getNewQueryOptions();
+ let query = histsvc.getNewQuery();
+ query.uri = sourceURI;
+ let root = histsvc.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+
+ do_check_valid_places_guid(root.getChild(0).pageGuid);
+ do_check_eq(root.getChild(0).bookmarkGuid, "");
+ root.containerOpen = false;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_addItemsWithInvalidGUIDsFails() {
+ const INVALID_GUID = "XYZ";
+ try {
+ bmsvc.createFolder(bmsvc.placesRoot, "XYZ folder",
+ bmsvc.DEFAULT_INDEX, INVALID_GUID);
+ do_throw("Adding a folder with an invalid guid should fail");
+ }
+ catch (ex) { }
+
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX);
+ try {
+ bmsvc.insertBookmark(folder, uri("http://test.tld"), bmsvc.DEFAULT_INDEX,
+ "title", INVALID_GUID);
+ do_throw("Adding a bookmark with an invalid guid should fail");
+ }
+ catch (ex) { }
+
+ try {
+ bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX, INVALID_GUID);
+ do_throw("Adding a separator with an invalid guid should fail");
+ }
+ catch (ex) { }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_addItemsWithGUIDs() {
+ const FOLDER_GUID = "FOLDER--GUID";
+ const BOOKMARK_GUID = "BM------GUID";
+ const SEPARATOR_GUID = "SEP-----GUID";
+
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX, FOLDER_GUID);
+ bmsvc.insertBookmark(folder, uri("http://test1.com/"), bmsvc.DEFAULT_INDEX,
+ "1 title", BOOKMARK_GUID);
+ bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX, SEPARATOR_GUID);
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.bookmarkGuid, FOLDER_GUID);
+ do_check_eq(root.getChild(0).bookmarkGuid, BOOKMARK_GUID);
+ do_check_eq(root.getChild(1).bookmarkGuid, SEPARATOR_GUID);
+
+ root.containerOpen = false;
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_emptyGUIDIgnored() {
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX, "");
+ do_check_valid_places_guid(PlacesUtils.getFolderContents(folder)
+ .root.bookmarkGuid);
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_usingSameGUIDFails() {
+ const GUID = "XYZXYZXYZXYZ";
+ bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX, GUID);
+ try {
+ bmsvc.createFolder(bmsvc.placesRoot, "test folder 2",
+ bmsvc.DEFAULT_INDEX, GUID);
+ do_throw("Using the same guid twice should fail");
+ }
+ catch (ex) { }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_placeURIs.js b/toolkit/components/places/tests/unit/test_placeURIs.js
new file mode 100644
index 0000000000..0f585ca514
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_placeURIs.js
@@ -0,0 +1,42 @@
+/* -*- 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/. */
+
+
+// Get history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService);
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+// main
+function run_test() {
+ // XXX Full testing coverage for QueriesToQueryString and
+ // QueryStringToQueries
+
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ const NHQO = Ci.nsINavHistoryQueryOptions;
+ // Bug 376798
+ var query = histsvc.getNewQuery();
+ query.setFolders([bs.placesRoot], 1);
+ do_check_eq(histsvc.queriesToQueryString([query], 1, histsvc.getNewQueryOptions()),
+ "place:folder=PLACES_ROOT");
+
+ // Bug 378828
+ var options = histsvc.getNewQueryOptions();
+ options.sortingAnnotation = "test anno";
+ options.sortingMode = NHQO.SORT_BY_ANNOTATION_DESCENDING;
+ var placeURI =
+ "place:folder=PLACES_ROOT&sort=" + NHQO.SORT_BY_ANNOTATION_DESCENDING +
+ "&sortingAnnotation=test%20anno";
+ do_check_eq(histsvc.queriesToQueryString([query], 1, options),
+ placeURI);
+ options = {};
+ histsvc.queryStringToQueries(placeURI, { }, {}, options);
+ do_check_eq(options.value.sortingAnnotation, "test anno");
+ do_check_eq(options.value.sortingMode, NHQO.SORT_BY_ANNOTATION_DESCENDING);
+}
diff --git a/toolkit/components/places/tests/unit/test_placesTxn.js b/toolkit/components/places/tests/unit/test_placesTxn.js
new file mode 100644
index 0000000000..3cc9809bb3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -0,0 +1,937 @@
+/* -*- 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 bmsvc = PlacesUtils.bookmarks;
+var tagssvc = PlacesUtils.tagging;
+var annosvc = PlacesUtils.annotations;
+var txnManager = PlacesUtils.transactionManager;
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+function* promiseKeyword(keyword, href, postData) {
+ while (true) {
+ let entry = yield PlacesUtils.keywords.fetch(keyword);
+ if (href == null && !entry)
+ break;
+ if (entry && entry.url.href == href && entry.postData == postData) {
+ break;
+ }
+
+ yield new Promise(resolve => do_timeout(100, resolve));
+ }
+}
+
+// create and add bookmarks observer
+var observer = {
+
+ onBeginUpdateBatch: function() {
+ this._beginUpdateBatch = true;
+ },
+ _beginUpdateBatch: false,
+
+ onEndUpdateBatch: function() {
+ this._endUpdateBatch = true;
+ },
+ _endUpdateBatch: false,
+
+ onItemAdded: function(id, folder, index, itemType, uri) {
+ this._itemAddedId = id;
+ this._itemAddedParent = folder;
+ this._itemAddedIndex = index;
+ this._itemAddedType = itemType;
+ },
+ _itemAddedId: null,
+ _itemAddedParent: null,
+ _itemAddedIndex: null,
+ _itemAddedType: null,
+
+ onItemRemoved: function(id, folder, index, itemType) {
+ this._itemRemovedId = id;
+ this._itemRemovedFolder = folder;
+ this._itemRemovedIndex = index;
+ },
+ _itemRemovedId: null,
+ _itemRemovedFolder: null,
+ _itemRemovedIndex: null,
+
+ onItemChanged: function(id, property, isAnnotationProperty, newValue,
+ lastModified, itemType) {
+ // The transaction manager is being rewritten in bug 891303, so just
+ // skip checking this for now.
+ if (property == "tags")
+ return;
+ this._itemChangedId = id;
+ this._itemChangedProperty = property;
+ this._itemChanged_isAnnotationProperty = isAnnotationProperty;
+ this._itemChangedValue = newValue;
+ },
+ _itemChangedId: null,
+ _itemChangedProperty: null,
+ _itemChanged_isAnnotationProperty: null,
+ _itemChangedValue: null,
+
+ onItemVisited: function(id, visitID, time) {
+ this._itemVisitedId = id;
+ this._itemVisitedVistId = visitID;
+ this._itemVisitedTime = time;
+ },
+ _itemVisitedId: null,
+ _itemVisitedVistId: null,
+ _itemVisitedTime: null,
+
+ onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex,
+ itemType) {
+ this._itemMovedId = id;
+ this._itemMovedOldParent = oldParent;
+ this._itemMovedOldIndex = oldIndex;
+ this._itemMovedNewParent = newParent;
+ this._itemMovedNewIndex = newIndex;
+ },
+ _itemMovedId: null,
+ _itemMovedOldParent: null,
+ _itemMovedOldIndex: null,
+ _itemMovedNewParent: null,
+ _itemMovedNewIndex: null,
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsINavBookmarkObserver) ||
+ iid.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+// index at which items should begin
+var bmStartIndex = 0;
+
+// get bookmarks root id
+var root = PlacesUtils.bookmarksMenuFolderId;
+
+add_task(function* init() {
+ bmsvc.addObserver(observer, false);
+ do_register_cleanup(function () {
+ bmsvc.removeObserver(observer);
+ });
+});
+
+add_task(function* test_create_folder_with_description() {
+ const TEST_FOLDERNAME = "Test creating a folder with a description";
+ const TEST_DESCRIPTION = "this is my test description";
+
+ let annos = [{ name: DESCRIPTION_ANNO,
+ type: annosvc.TYPE_STRING,
+ flags: 0,
+ value: TEST_DESCRIPTION,
+ expires: annosvc.EXPIRE_NEVER }];
+ let txn = new PlacesCreateFolderTransaction(TEST_FOLDERNAME, root, bmStartIndex, annos);
+ txnManager.doTransaction(txn);
+
+ // This checks that calling undoTransaction on an "empty batch" doesn't
+ // undo the previous transaction (getItemTitle will fail)
+ txnManager.beginBatch(null);
+ txnManager.endBatch(false);
+ txnManager.undoTransaction();
+
+ let folderId = observer._itemAddedId;
+ do_check_eq(bmsvc.getItemTitle(folderId), TEST_FOLDERNAME);
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, root);
+ do_check_eq(observer._itemAddedId, folderId);
+ do_check_eq(TEST_DESCRIPTION, annosvc.getItemAnnotation(folderId, DESCRIPTION_ANNO));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, root);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, root);
+ do_check_eq(observer._itemAddedId, folderId);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, root);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+});
+
+add_task(function* test_create_item() {
+ let testURI = NetUtil.newURI("http://test_create_item.com");
+
+ let txn = new PlacesCreateBookmarkTransaction(testURI, root, bmStartIndex,
+ "Test creating an item");
+
+ txnManager.doTransaction(txn);
+ let id = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedId, id);
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_true(bmsvc.isBookmarked(testURI));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, id);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+ do_check_false(bmsvc.isBookmarked(testURI));
+
+ txn.redoTransaction();
+ do_check_true(bmsvc.isBookmarked(testURI));
+ let newId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, root);
+ do_check_eq(observer._itemAddedId, newId);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newId);
+ do_check_eq(observer._itemRemovedFolder, root);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+});
+
+add_task(function* test_create_item_to_folder() {
+ const TEST_FOLDERNAME = "Test creating item to a folder";
+ let testURI = NetUtil.newURI("http://test_create_item_to_folder.com");
+ let folderId = bmsvc.createFolder(root, TEST_FOLDERNAME, bmsvc.DEFAULT_INDEX);
+
+ let txn = new PlacesCreateBookmarkTransaction(testURI, folderId, bmStartIndex,
+ "Test creating item");
+ txnManager.doTransaction(txn);
+ let bkmId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedId, bkmId);
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_true(bmsvc.isBookmarked(testURI));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, bkmId);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+
+ txn.redoTransaction();
+ let newBkmId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, folderId);
+ do_check_eq(observer._itemAddedId, newBkmId);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newBkmId);
+ do_check_eq(observer._itemRemovedFolder, folderId);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+});
+
+add_task(function* test_move_items_to_folder() {
+ let testFolderId = bmsvc.createFolder(root, "Test move items", bmsvc.DEFAULT_INDEX);
+ let testURI = NetUtil.newURI("http://test_move_items.com");
+ let testBkmId = bmsvc.insertBookmark(testFolderId, testURI, bmsvc.DEFAULT_INDEX, "1: Test move items");
+ bmsvc.insertBookmark(testFolderId, testURI, bmsvc.DEFAULT_INDEX, "2: Test move items");
+
+ // Moving items between the same folder
+ let sameTxn = new PlacesMoveItemTransaction(testBkmId, testFolderId, bmsvc.DEFAULT_INDEX);
+
+ sameTxn.doTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 1);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 1);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ sameTxn.redoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 1);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 1);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ // Moving items between different folders
+ let folderId = bmsvc.createFolder(testFolderId,
+ "Test move items between different folders",
+ bmsvc.DEFAULT_INDEX);
+ let diffTxn = new PlacesMoveItemTransaction(testBkmId, folderId, bmsvc.DEFAULT_INDEX);
+
+ diffTxn.doTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, folderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, folderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ diffTxn.redoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, folderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, folderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+});
+
+add_task(function* test_remove_folder() {
+ let testFolder = bmsvc.createFolder(root, "Test Removing a Folder", bmsvc.DEFAULT_INDEX);
+ let folderId = bmsvc.createFolder(testFolder, "Removed Folder", bmsvc.DEFAULT_INDEX);
+
+ let txn = new PlacesRemoveItemTransaction(folderId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, folderId);
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, folderId);
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+});
+
+add_task(function* test_remove_item_with_tag() {
+ // Notice in this case the tag persists since other bookmarks have same uri.
+ let testFolder = bmsvc.createFolder(root, "Test removing an item with a tag",
+ bmsvc.DEFAULT_INDEX);
+
+ const TAG_NAME = "tag-test_remove_item_with_tag";
+ let testURI = NetUtil.newURI("http://test_remove_item_with_tag.com");
+ let testBkmId = bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item1");
+
+ // create bookmark for not removing tag.
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item2");
+
+ // set tag
+ tagssvc.tagURI(testURI, [TAG_NAME]);
+
+ let txn = new PlacesRemoveItemTransaction(testBkmId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, testBkmId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI), TAG_NAME);
+
+ txn.undoTransaction();
+ let newbkmk2Id = observer._itemAddedId;
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, newbkmk2Id);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME);
+});
+
+add_task(function* test_remove_item_with_keyword() {
+ // Notice in this case the tag persists since other bookmarks have same uri.
+ let testFolder = bmsvc.createFolder(root, "Test removing an item with a keyword",
+ bmsvc.DEFAULT_INDEX);
+
+ const KEYWORD = "test: test removing an item with a keyword";
+ let testURI = NetUtil.newURI("http://test_remove_item_with_keyword.com");
+ let testBkmId = bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item1");
+
+ // set keyword
+ yield PlacesUtils.keywords.insert({ url: testURI.spec, keyword: KEYWORD});
+
+ let txn = new PlacesRemoveItemTransaction(testBkmId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, testBkmId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ yield promiseKeyword(KEYWORD, null);
+
+ txn.undoTransaction();
+ let newbkmk2Id = observer._itemAddedId;
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+ yield promiseKeyword(KEYWORD, testURI.spec);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, newbkmk2Id);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ yield promiseKeyword(KEYWORD, null);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+});
+
+add_task(function* test_creating_separator() {
+ let testFolder = bmsvc.createFolder(root, "Test creating a separator", bmsvc.DEFAULT_INDEX);
+
+ let txn = new PlacesCreateSeparatorTransaction(testFolder, 0);
+ txn.doTransaction();
+
+ let sepId = observer._itemAddedId;
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(observer._itemAddedParent, testFolder);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, sepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.redoTransaction();
+ let newSepId = observer._itemAddedId;
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(observer._itemAddedParent, testFolder);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newSepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+});
+
+add_task(function* test_removing_separator() {
+ let testFolder = bmsvc.createFolder(root, "Test removing a separator", bmsvc.DEFAULT_INDEX);
+
+ let sepId = bmsvc.insertSeparator(testFolder, 0);
+ let txn = new PlacesRemoveItemTransaction(sepId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, sepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, sepId); // New separator created
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, sepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, sepId); // New separator created
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+});
+
+add_task(function* test_editing_item_title() {
+ const TITLE = "Test editing item title";
+ const MOD_TITLE = "Mod: Test editing item title";
+ let testURI = NetUtil.newURI("http://www.test_editing_item_title.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, TITLE);
+
+ let txn = new PlacesEditItemTitleTransaction(testBkmId, MOD_TITLE);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, MOD_TITLE);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, TITLE);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, MOD_TITLE);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, TITLE);
+});
+
+add_task(function* test_editing_item_uri() {
+ const OLD_TEST_URI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
+ const NEW_TEST_URI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
+ let testBkmId = bmsvc.insertBookmark(root, OLD_TEST_URI, bmsvc.DEFAULT_INDEX,
+ "Test editing item title");
+ tagssvc.tagURI(OLD_TEST_URI, ["tag"]);
+
+ let txn = new PlacesEditBookmarkURITransaction(testBkmId, NEW_TEST_URI);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, NEW_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify([]));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, OLD_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify([]));
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, NEW_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify([]));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, OLD_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify([]));
+});
+
+add_task(function* test_edit_description_transaction() {
+ let testURI = NetUtil.newURI("http://test_edit_description_transaction.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit description transaction");
+
+ let anno = {
+ name: DESCRIPTION_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_STRING,
+ flags: 0,
+ value: "Test edit Description",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ };
+ let txn = new PlacesSetItemAnnotationTransaction(testBkmId, anno);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, DESCRIPTION_ANNO);
+});
+
+add_task(function* test_edit_keyword() {
+ const KEYWORD = "keyword-test_edit_keyword";
+
+ let testURI = NetUtil.newURI("http://test_edit_keyword.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
+
+ let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData");
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, KEYWORD);
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData");
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, "");
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), null);
+});
+
+add_task(function* test_edit_specific_keyword() {
+ const KEYWORD = "keyword-test_edit_keyword2";
+
+ let testURI = NetUtil.newURI("http://test_edit_keyword2.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
+ // Add multiple keyword to this uri.
+ yield PlacesUtils.keywords.insert({ keyword: "kw1", url: testURI.spec, postData: "postData1" });
+ yield PlacesUtils.keywords.insert({keyword: "kw2", url: testURI.spec, postData: "postData2" });
+
+ // Try to change only kw2.
+ let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData2", "kw2");
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, KEYWORD);
+ let entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, testURI.spec);
+ Assert.equal(entry.postData, "postData1");
+ yield promiseKeyword(KEYWORD, testURI.spec, "postData2");
+ yield promiseKeyword("kw2", null);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, "kw2");
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData1");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, testURI.spec);
+ Assert.equal(entry.postData, "postData1");
+ yield promiseKeyword("kw2", testURI.spec, "postData2");
+ yield promiseKeyword("keyword", null);
+});
+
+add_task(function* test_LoadInSidebar_transaction() {
+ let testURI = NetUtil.newURI("http://test_LoadInSidebar_transaction.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test LoadInSidebar transaction");
+
+ const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+ let anno = { name: LOAD_IN_SIDEBAR_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: true,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let txn = new PlacesSetItemAnnotationTransaction(testBkmId, anno);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, LOAD_IN_SIDEBAR_ANNO);
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, LOAD_IN_SIDEBAR_ANNO);
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+});
+
+add_task(function* test_generic_item_annotation() {
+ let testURI = NetUtil.newURI("http://test_generic_item_annotation.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test generic item annotation");
+
+ let itemAnnoObj = { name: "testAnno/testInt",
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: 123,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let txn = new PlacesSetItemAnnotationTransaction(testBkmId, itemAnnoObj);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+});
+
+add_task(function* test_editing_item_date_added() {
+ let testURI = NetUtil.newURI("http://test_editing_item_date_added.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
+ "Test editing item date added");
+
+ let oldAdded = bmsvc.getItemDateAdded(testBkmId);
+ let newAdded = Date.now() * 1000 + 1000;
+ let txn = new PlacesEditItemDateAddedTransaction(testBkmId, newAdded);
+
+ txn.doTransaction();
+ do_check_eq(newAdded, bmsvc.getItemDateAdded(testBkmId));
+
+ txn.undoTransaction();
+ do_check_eq(oldAdded, bmsvc.getItemDateAdded(testBkmId));
+});
+
+add_task(function* test_edit_item_last_modified() {
+ let testURI = NetUtil.newURI("http://test_edit_item_last_modified.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
+ "Test editing item last modified");
+
+ let oldModified = bmsvc.getItemLastModified(testBkmId);
+ let newModified = Date.now() * 1000 + 1000;
+ let txn = new PlacesEditItemLastModifiedTransaction(testBkmId, newModified);
+
+ txn.doTransaction();
+ do_check_eq(newModified, bmsvc.getItemLastModified(testBkmId));
+
+ txn.undoTransaction();
+ do_check_eq(oldModified, bmsvc.getItemLastModified(testBkmId));
+});
+
+add_task(function* test_generic_page_annotation() {
+ const TEST_ANNO = "testAnno/testInt";
+ let testURI = NetUtil.newURI("http://www.mozilla.org/");
+ PlacesTestUtils.addVisits(testURI).then(function () {
+ let pageAnnoObj = { name: TEST_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: 123,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let txn = new PlacesSetPageAnnotationTransaction(testURI, pageAnnoObj);
+
+ txn.doTransaction();
+ do_check_true(annosvc.pageHasAnnotation(testURI, TEST_ANNO));
+
+ txn.undoTransaction();
+ do_check_false(annosvc.pageHasAnnotation(testURI, TEST_ANNO));
+
+ txn.redoTransaction();
+ do_check_true(annosvc.pageHasAnnotation(testURI, TEST_ANNO));
+ });
+});
+
+add_task(function* test_sort_folder_by_name() {
+ let testFolder = bmsvc.createFolder(root, "Test PlacesSortFolderByNameTransaction",
+ bmsvc.DEFAULT_INDEX);
+ let testURI = NetUtil.newURI("http://test_sort_folder_by_name.com");
+
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark3");
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark2");
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark1");
+
+ let bkmIds = bmsvc.getBookmarkIdsForURI(testURI);
+ bkmIds.sort();
+
+ let b1 = bkmIds[0];
+ let b2 = bkmIds[1];
+ let b3 = bkmIds[2];
+
+ do_check_eq(0, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(2, bmsvc.getItemIndex(b3));
+
+ let txn = new PlacesSortFolderByNameTransaction(testFolder);
+
+ txn.doTransaction();
+ do_check_eq(2, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(0, bmsvc.getItemIndex(b3));
+
+ txn.undoTransaction();
+ do_check_eq(0, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(2, bmsvc.getItemIndex(b3));
+
+ txn.redoTransaction();
+ do_check_eq(2, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(0, bmsvc.getItemIndex(b3));
+
+ txn.undoTransaction();
+ do_check_eq(0, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(2, bmsvc.getItemIndex(b3));
+});
+
+add_task(function* test_tagURI_untagURI() {
+ const TAG_1 = "tag-test_tagURI_untagURI-bar";
+ const TAG_2 = "tag-test_tagURI_untagURI-foo";
+ let tagURI = NetUtil.newURI("http://test_tagURI_untagURI.com");
+
+ // Test tagURI
+ let tagTxn = new PlacesTagURITransaction(tagURI, [TAG_1, TAG_2]);
+
+ tagTxn.doTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2]));
+
+ tagTxn.undoTransaction();
+ do_check_eq(tagssvc.getTagsForURI(tagURI).length, 0);
+
+ tagTxn.redoTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2]));
+
+ // Test untagURI
+ let untagTxn = new PlacesUntagURITransaction(tagURI, [TAG_1]);
+
+ untagTxn.doTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_2]));
+
+ untagTxn.undoTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2]));
+
+ untagTxn.redoTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_2]));
+});
+
+add_task(function* test_aggregate_removeItem_Txn() {
+ let testFolder = bmsvc.createFolder(root, "Test aggregate removeItem transaction", bmsvc.DEFAULT_INDEX);
+
+ const TEST_URL = "http://test_aggregate_removeitem_txn.com/";
+ const FOLDERNAME = "Folder";
+ let testURI = NetUtil.newURI(TEST_URL);
+
+ let bkmk1Id = bmsvc.insertBookmark(testFolder, testURI, 0, "Mozilla");
+ let bkmk2Id = bmsvc.insertSeparator(testFolder, 1);
+ let bkmk3Id = bmsvc.createFolder(testFolder, FOLDERNAME, 2);
+
+ let bkmk3_1Id = bmsvc.insertBookmark(bkmk3Id, testURI, 0, "Mozilla");
+ let bkmk3_2Id = bmsvc.insertSeparator(bkmk3Id, 1);
+ let bkmk3_3Id = bmsvc.createFolder(bkmk3Id, FOLDERNAME, 2);
+
+ let childTxn1 = new PlacesRemoveItemTransaction(bkmk1Id);
+ let childTxn2 = new PlacesRemoveItemTransaction(bkmk2Id);
+ let childTxn3 = new PlacesRemoveItemTransaction(bkmk3Id);
+ let transactions = [childTxn1, childTxn2, childTxn3];
+ let txn = new PlacesAggregatedTransaction("RemoveItems", transactions);
+
+ txn.doTransaction();
+ do_check_eq(bmsvc.getItemIndex(bkmk1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3_1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3_2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3_3Id), -1);
+ // Check last removed item id.
+ do_check_eq(observer._itemRemovedId, bkmk3Id);
+
+ txn.undoTransaction();
+ let newBkmk1Id = bmsvc.getIdForItemAt(testFolder, 0);
+ let newBkmk2Id = bmsvc.getIdForItemAt(testFolder, 1);
+ let newBkmk3Id = bmsvc.getIdForItemAt(testFolder, 2);
+ let newBkmk3_1Id = bmsvc.getIdForItemAt(newBkmk3Id, 0);
+ let newBkmk3_2Id = bmsvc.getIdForItemAt(newBkmk3Id, 1);
+ let newBkmk3_3Id = bmsvc.getIdForItemAt(newBkmk3Id, 2);
+ do_check_eq(bmsvc.getItemType(newBkmk1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getItemType(newBkmk2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getItemType(newBkmk3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3Id), FOLDERNAME);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_1Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk3_1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_2Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_3Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3_3Id), FOLDERNAME);
+ // Check last added back item id.
+ // Notice items are restored in reverse order.
+ do_check_eq(observer._itemAddedId, newBkmk1Id);
+
+ txn.redoTransaction();
+ do_check_eq(bmsvc.getItemIndex(newBkmk1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3_1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3_2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3_3Id), -1);
+ // Check last removed item id.
+ do_check_eq(observer._itemRemovedId, newBkmk3Id);
+
+ txn.undoTransaction();
+ newBkmk1Id = bmsvc.getIdForItemAt(testFolder, 0);
+ newBkmk2Id = bmsvc.getIdForItemAt(testFolder, 1);
+ newBkmk3Id = bmsvc.getIdForItemAt(testFolder, 2);
+ newBkmk3_1Id = bmsvc.getIdForItemAt(newBkmk3Id, 0);
+ newBkmk3_2Id = bmsvc.getIdForItemAt(newBkmk3Id, 1);
+ newBkmk3_3Id = bmsvc.getIdForItemAt(newBkmk3Id, 2);
+ do_check_eq(bmsvc.getItemType(newBkmk1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getItemType(newBkmk2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getItemType(newBkmk3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3Id), FOLDERNAME);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_1Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk3_1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_2Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_3Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3_3Id), FOLDERNAME);
+ // Check last added back item id.
+ // Notice items are restored in reverse order.
+ do_check_eq(observer._itemAddedId, newBkmk1Id);
+});
+
+add_task(function* test_create_item_with_childTxn() {
+ let testFolder = bmsvc.createFolder(root, "Test creating an item with childTxns", bmsvc.DEFAULT_INDEX);
+
+ const BOOKMARK_TITLE = "parent item";
+ let testURI = NetUtil.newURI("http://test_create_item_with_childTxn.com");
+ let childTxns = [];
+ let newDateAdded = Date.now() * 1000 - 20000;
+ let editDateAdddedTxn = new PlacesEditItemDateAddedTransaction(null, newDateAdded);
+ childTxns.push(editDateAdddedTxn);
+
+ let itemChildAnnoObj = { name: "testAnno/testInt",
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: 123,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let annoTxn = new PlacesSetItemAnnotationTransaction(null, itemChildAnnoObj);
+ childTxns.push(annoTxn);
+
+ let itemWChildTxn = new PlacesCreateBookmarkTransaction(testURI, testFolder, bmStartIndex,
+ BOOKMARK_TITLE, null, null,
+ childTxns);
+ try {
+ txnManager.doTransaction(itemWChildTxn);
+ let itemId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedId, itemId);
+ do_check_eq(newDateAdded, bmsvc.getItemDateAdded(itemId));
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+ do_check_true(annosvc.itemHasAnnotation(itemId, itemChildAnnoObj.name))
+ do_check_eq(annosvc.getItemAnnotation(itemId, itemChildAnnoObj.name), itemChildAnnoObj.value);
+
+ itemWChildTxn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, itemId);
+
+ itemWChildTxn.redoTransaction();
+ do_check_true(bmsvc.isBookmarked(testURI));
+ let newId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(newDateAdded, bmsvc.getItemDateAdded(newId));
+ do_check_eq(observer._itemAddedId, newId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+ do_check_true(annosvc.itemHasAnnotation(newId, itemChildAnnoObj.name))
+ do_check_eq(annosvc.getItemAnnotation(newId, itemChildAnnoObj.name), itemChildAnnoObj.value);
+
+ itemWChildTxn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newId);
+ }
+ catch (ex) {
+ do_throw("Setting a child transaction in a createItem transaction did throw: " + ex);
+ }
+});
+
+add_task(function* test_create_folder_with_child_itemTxn() {
+ let childURI = NetUtil.newURI("http://test_create_folder_with_child_itemTxn.com");
+ let childItemTxn = new PlacesCreateBookmarkTransaction(childURI, root,
+ bmStartIndex, "childItem");
+ let txn = new PlacesCreateFolderTransaction("Test creating a folder with child itemTxns",
+ root, bmStartIndex, null, [childItemTxn]);
+ try {
+ txnManager.doTransaction(txn);
+ let childItemId = bmsvc.getBookmarkIdsForURI(childURI)[0];
+ do_check_eq(observer._itemAddedId, childItemId);
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_true(bmsvc.isBookmarked(childURI));
+
+ txn.undoTransaction();
+ do_check_false(bmsvc.isBookmarked(childURI));
+
+ txn.redoTransaction();
+ let newchildItemId = bmsvc.getBookmarkIdsForURI(childURI)[0];
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(observer._itemAddedId, newchildItemId);
+ do_check_true(bmsvc.isBookmarked(childURI));
+
+ txn.undoTransaction();
+ do_check_false(bmsvc.isBookmarked(childURI));
+ }
+ catch (ex) {
+ do_throw("Setting a child item transaction in a createFolder transaction did throw: " + ex);
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
new file mode 100644
index 0000000000..a533c8295a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
@@ -0,0 +1,1356 @@
+/* -*- 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/. */
+
+ /**
+ * Test preventive maintenance
+ * For every maintenance query create an uncoherent db and check that we take
+ * correct fix steps, without polluting valid data.
+ */
+
+// Include PlacesDBUtils module
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+const FINISHED_MAINTENANCE_NOTIFICATION_TOPIC = "places-maintenance-finished";
+
+// Get services and database connection
+var hs = PlacesUtils.history;
+var bs = PlacesUtils.bookmarks;
+var ts = PlacesUtils.tagging;
+var as = PlacesUtils.annotations;
+var fs = PlacesUtils.favicons;
+
+var mDBConn = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+
+// ------------------------------------------------------------------------------
+// Helpers
+
+var defaultBookmarksMaxId = 0;
+function cleanDatabase() {
+ mDBConn.executeSimpleSQL("DELETE FROM moz_places");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_historyvisits");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_anno_attributes");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_annos");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_items_annos");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_inputhistory");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_keywords");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_favicons");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE id > " + defaultBookmarksMaxId);
+}
+
+function addPlace(aUrl, aFavicon) {
+ let stmt = mDBConn.createStatement(
+ "INSERT INTO moz_places (url, url_hash, favicon_id) VALUES (:url, hash(:url), :favicon)");
+ stmt.params["url"] = aUrl || "http://www.mozilla.org";
+ stmt.params["favicon"] = aFavicon || null;
+ stmt.execute();
+ stmt.finalize();
+ return mDBConn.lastInsertRowID;
+}
+
+function addBookmark(aPlaceId, aType, aParent, aKeywordId, aFolderType, aTitle) {
+ let stmt = mDBConn.createStatement(
+ `INSERT INTO moz_bookmarks (fk, type, parent, keyword_id, folder_type,
+ title, guid)
+ VALUES (:place_id, :type, :parent, :keyword_id, :folder_type, :title,
+ GENERATE_GUID())`);
+ stmt.params["place_id"] = aPlaceId || null;
+ stmt.params["type"] = aType || bs.TYPE_BOOKMARK;
+ stmt.params["parent"] = aParent || bs.unfiledBookmarksFolder;
+ stmt.params["keyword_id"] = aKeywordId || null;
+ stmt.params["folder_type"] = aFolderType || null;
+ stmt.params["title"] = typeof(aTitle) == "string" ? aTitle : null;
+ stmt.execute();
+ stmt.finalize();
+ return mDBConn.lastInsertRowID;
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+
+var tests = [];
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "A.1",
+ desc: "Remove obsolete annotations from moz_annos",
+
+ _obsoleteWeaveAttribute: "weave/test",
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid.
+ this._placeId = addPlace();
+ // Add an obsolete attribute.
+ let stmt = mDBConn.createStatement(
+ "INSERT INTO moz_anno_attributes (name) VALUES (:anno)"
+ );
+ stmt.params['anno'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES (:place_id,
+ (SELECT id FROM moz_anno_attributes WHERE name = :anno)
+ )`
+ );
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that the obsolete annotation has been removed.
+ let stmt = mDBConn.createStatement(
+ "SELECT id FROM moz_anno_attributes WHERE name = :anno"
+ );
+ stmt.params['anno'] = this._obsoleteWeaveAttribute;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+tests.push({
+ name: "A.2",
+ desc: "Remove obsolete annotations from moz_items_annos",
+
+ _obsoleteSyncAttribute: "sync/children",
+ _obsoleteGuidAttribute: "placesInternal/GUID",
+ _obsoleteWeaveAttribute: "weave/test",
+ _placeId: null,
+ _bookmarkId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid.
+ this._placeId = addPlace();
+ // Add a bookmark.
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add an obsolete attribute.
+ let stmt = mDBConn.createStatement(
+ `INSERT INTO moz_anno_attributes (name)
+ VALUES (:anno1), (:anno2), (:anno3)`
+ );
+ stmt.params['anno1'] = this._obsoleteSyncAttribute;
+ stmt.params['anno2'] = this._obsoleteGuidAttribute;
+ stmt.params['anno3'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement(
+ `INSERT INTO moz_items_annos (item_id, anno_attribute_id)
+ SELECT :item_id, id
+ FROM moz_anno_attributes
+ WHERE name IN (:anno1, :anno2, :anno3)`
+ );
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.params['anno1'] = this._obsoleteSyncAttribute;
+ stmt.params['anno2'] = this._obsoleteGuidAttribute;
+ stmt.params['anno3'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that the obsolete annotations have been removed.
+ let stmt = mDBConn.createStatement(
+ `SELECT id FROM moz_anno_attributes
+ WHERE name IN (:anno1, :anno2, :anno3)`
+ );
+ stmt.params['anno1'] = this._obsoleteSyncAttribute;
+ stmt.params['anno2'] = this._obsoleteGuidAttribute;
+ stmt.params['anno3'] = this._obsoleteWeaveAttribute;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+tests.push({
+ name: "A.3",
+ desc: "Remove unused attributes",
+
+ _usedPageAttribute: "usedPage",
+ _usedItemAttribute: "usedItem",
+ _unusedAttribute: "unused",
+ _placeId: null,
+ _bookmarkId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // add a bookmark
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add a used attribute and an unused one.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.reset();
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.reset();
+ stmt.params['anno'] = this._unusedAttribute;
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attributes are still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that unused attribute has been removed
+ stmt.params['anno'] = this._unusedAttribute;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "B.1",
+ desc: "Remove annotations with an invalid attribute",
+
+ _usedPageAttribute: "usedPage",
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a used attribute.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ // Add an annotation with a nonexistent attribute
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, 1337)");
+ stmt.params['place_id'] = this._placeId;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that annotation with bogus attribute has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = 1337");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "B.2",
+ desc: "Remove orphan page annotations",
+
+ _usedPageAttribute: "usedPage",
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a used attribute.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.reset();
+ // Add an annotation to a nonexistent page
+ stmt.params['place_id'] = 1337;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that an annotation to a nonexistent page has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE place_id = 1337");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+tests.push({
+ name: "C.1",
+ desc: "fix missing Places root",
+
+ setup: function() {
+ // Sanity check: ensure that roots are intact.
+ do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+
+ // Remove the root.
+ mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE parent = 0");
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE parent = 0");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Ensure the roots have been correctly restored.
+ do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+ }
+});
+
+// ------------------------------------------------------------------------------
+tests.push({
+ name: "C.2",
+ desc: "Fix roots titles",
+
+ setup: function() {
+ // Sanity check: ensure that roots titles are correct. We can use our check.
+ this.check();
+ // Change some roots' titles.
+ bs.setItemTitle(bs.placesRoot, "bad title");
+ do_check_eq(bs.getItemTitle(bs.placesRoot), "bad title");
+ bs.setItemTitle(bs.unfiledBookmarksFolder, "bad title");
+ do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder), "bad title");
+ },
+
+ check: function() {
+ // Ensure all roots titles are correct.
+ do_check_eq(bs.getItemTitle(bs.placesRoot), "");
+ do_check_eq(bs.getItemTitle(bs.bookmarksMenuFolder),
+ PlacesUtils.getString("BookmarksMenuFolderTitle"));
+ do_check_eq(bs.getItemTitle(bs.tagsFolder),
+ PlacesUtils.getString("TagsFolderTitle"));
+ do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder),
+ PlacesUtils.getString("OtherBookmarksFolderTitle"));
+ do_check_eq(bs.getItemTitle(bs.toolbarFolder),
+ PlacesUtils.getString("BookmarksToolbarFolderTitle"));
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.1",
+ desc: "Remove items without a valid place",
+
+ _validItemId: null,
+ _invalidItemId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this.placeId = addPlace();
+ // Insert a valid bookmark
+ this._validItemId = addBookmark(this.placeId);
+ // Insert a bookmark with an invalid place
+ this._invalidItemId = addBookmark(1337);
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id");
+ stmt.params["item_id"] = this._validItemId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that invalid bookmark has been removed
+ stmt.params["item_id"] = this._invalidItemId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.2",
+ desc: "Remove items that are not uri bookmarks from tag containers",
+
+ _tagId: null,
+ _bookmarkId: null,
+ _separatorId: null,
+ _folderId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Create a tag
+ this._tagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder);
+ // Insert a bookmark in the tag
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._tagId);
+ // Insert a separator in the tag
+ this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR, this._tagId);
+ // Insert a folder in the tag
+ this._folderId = addBookmark(null, bs.TYPE_FOLDER, this._tagId);
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE type = :type AND parent = :parent");
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ stmt.params["parent"] = this._tagId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that separator is no more there
+ stmt.params["type"] = bs.TYPE_SEPARATOR;
+ stmt.params["parent"] = this._tagId;
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ // Check that folder is no more there
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ stmt.params["parent"] = this._tagId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.3",
+ desc: "Remove empty tags",
+
+ _tagId: null,
+ _bookmarkId: null,
+ _emptyTagId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Create a tag
+ this._tagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder);
+ // Insert a bookmark in the tag
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._tagId);
+ // Create another tag (empty)
+ this._emptyTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder);
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :id AND type = :type AND parent = :parent");
+ stmt.params["id"] = this._bookmarkId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ stmt.params["parent"] = this._tagId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["id"] = this._tagId;
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ stmt.params["parent"] = bs.tagsFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["id"] = this._emptyTagId;
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ stmt.params["parent"] = bs.tagsFolder;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.4",
+ desc: "Move orphan items to unsorted folder",
+
+ _orphanBookmarkId: null,
+ _orphanSeparatorId: null,
+ _orphanFolderId: null,
+ _bookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert an orphan bookmark
+ this._orphanBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, 8888);
+ // Insert an orphan separator
+ this._orphanSeparatorId = addBookmark(null, bs.TYPE_SEPARATOR, 8888);
+ // Insert a orphan folder
+ this._orphanFolderId = addBookmark(null, bs.TYPE_FOLDER, 8888);
+ // Create a child of the last created folder
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._orphanFolderId);
+ },
+
+ check: function() {
+ // Check that bookmarks are now children of a real folder (unsorted)
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent");
+ stmt.params["item_id"] = this._orphanBookmarkId;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._orphanSeparatorId;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._orphanFolderId;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._bookmarkId;
+ stmt.params["parent"] = this._orphanFolderId;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.6",
+ desc: "Fix wrong item types | bookmarks",
+
+ _separatorId: null,
+ _folderId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a separator with a fk
+ this._separatorId = addBookmark(this._placeId, bs.TYPE_SEPARATOR);
+ // Add a folder with a fk
+ this._folderId = addBookmark(this._placeId, bs.TYPE_FOLDER);
+ },
+
+ check: function() {
+ // Check that items with an fk have been converted to bookmarks
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type");
+ stmt.params["item_id"] = this._separatorId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._folderId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.7",
+ desc: "Fix wrong item types | bookmarks",
+
+ _validBookmarkId: null,
+ _invalidBookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a bookmark with a valid place id
+ this._validBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK);
+ // Add a bookmark with a null place id
+ this._invalidBookmarkId = addBookmark(null, bs.TYPE_BOOKMARK);
+ },
+
+ check: function() {
+ // Check valid bookmark
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type");
+ stmt.params["item_id"] = this._validBookmarkId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check invalid bookmark has been converted to a folder
+ stmt.params["item_id"] = this._invalidBookmarkId;
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.9",
+ desc: "Fix wrong parents",
+
+ _bookmarkId: null,
+ _separatorId: null,
+ _bookmarkId1: null,
+ _bookmarkId2: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert a bookmark
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK);
+ // Insert a separator
+ this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR);
+ // Create 3 children of these items
+ this._bookmarkId1 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._bookmarkId);
+ this._bookmarkId2 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._separatorId);
+ },
+
+ check: function() {
+ // Check that bookmarks are now children of a real folder (unsorted)
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent");
+ stmt.params["item_id"] = this._bookmarkId1;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._bookmarkId2;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.10",
+ desc: "Recalculate positions",
+
+ _unfiledBookmarks: [],
+ _toolbarBookmarks: [],
+
+ setup: function() {
+ const NUM_BOOKMARKS = 20;
+ bs.runInBatchMode({
+ runBatched: function (aUserData) {
+ // Add bookmarks to two folders to better perturbate the table.
+ for (let i = 0; i < NUM_BOOKMARKS; i++) {
+ bs.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI("http://example.com/"),
+ bs.DEFAULT_INDEX, "testbookmark");
+ }
+ for (let i = 0; i < NUM_BOOKMARKS; i++) {
+ bs.insertBookmark(PlacesUtils.toolbarFolderId,
+ NetUtil.newURI("http://example.com/"),
+ bs.DEFAULT_INDEX, "testbookmark");
+ }
+ }
+ }, null);
+
+ function randomize_positions(aParent, aResultArray) {
+ let stmt = mDBConn.createStatement(
+ `UPDATE moz_bookmarks SET position = :rand
+ WHERE id IN (
+ SELECT id FROM moz_bookmarks WHERE parent = :parent
+ ORDER BY RANDOM() LIMIT 1
+ )`
+ );
+ for (let i = 0; i < (NUM_BOOKMARKS / 2); i++) {
+ stmt.params["parent"] = aParent;
+ stmt.params["rand"] = Math.round(Math.random() * (NUM_BOOKMARKS - 1));
+ stmt.execute();
+ stmt.reset();
+ }
+ stmt.finalize();
+
+ // Build the expected ordered list of bookmarks.
+ stmt = mDBConn.createStatement(
+ `SELECT id, position
+ FROM moz_bookmarks WHERE parent = :parent
+ ORDER BY position ASC, ROWID ASC`
+ );
+ stmt.params["parent"] = aParent;
+ while (stmt.executeStep()) {
+ aResultArray.push(stmt.row.id);
+ print(stmt.row.id + "\t" + stmt.row.position + "\t" +
+ (aResultArray.length - 1));
+ }
+ stmt.finalize();
+ }
+
+ // Set random positions for the added bookmarks.
+ randomize_positions(PlacesUtils.unfiledBookmarksFolderId,
+ this._unfiledBookmarks);
+ randomize_positions(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
+ },
+
+ check: function() {
+ function check_order(aParent, aResultArray) {
+ // Build the expected ordered list of bookmarks.
+ let stmt = mDBConn.createStatement(
+ `SELECT id, position FROM moz_bookmarks WHERE parent = :parent
+ ORDER BY position ASC`
+ );
+ stmt.params["parent"] = aParent;
+ let pass = true;
+ while (stmt.executeStep()) {
+ print(stmt.row.id + "\t" + stmt.row.position);
+ if (aResultArray.indexOf(stmt.row.id) != stmt.row.position) {
+ pass = false;
+ }
+ }
+ stmt.finalize();
+ if (!pass) {
+ dump_table("moz_bookmarks");
+ do_throw("Unexpected unfiled bookmarks order.");
+ }
+ }
+
+ check_order(PlacesUtils.unfiledBookmarksFolderId, this._unfiledBookmarks);
+ check_order(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.12",
+ desc: "Fix empty-named tags",
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ let placeId = addPlace();
+ // Create a empty-named tag.
+ this._untitledTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder, null, null, "");
+ // Insert a bookmark in the tag, otherwise it will be removed.
+ addBookmark(placeId, bs.TYPE_BOOKMARK, this._untitledTagId);
+ // Create a empty-named folder.
+ this._untitledFolderId = addBookmark(null, bs.TYPE_FOLDER, bs.toolbarFolder, null, null, "");
+ // Create a titled tag.
+ this._titledTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder, null, null, "titledTag");
+ // Insert a bookmark in the tag, otherwise it will be removed.
+ addBookmark(placeId, bs.TYPE_BOOKMARK, this._titledTagId);
+ // Create a titled folder.
+ this._titledFolderId = addBookmark(null, bs.TYPE_FOLDER, bs.toolbarFolder, null, null, "titledFolder");
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement(
+ "SELECT title FROM moz_bookmarks WHERE id = :id"
+ );
+ stmt.params["id"] = this._untitledTagId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "(notitle)");
+ stmt.reset();
+ stmt.params["id"] = this._untitledFolderId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "");
+ stmt.reset();
+ stmt.params["id"] = this._titledTagId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "titledTag");
+ stmt.reset();
+ stmt.params["id"] = this._titledFolderId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "titledFolder");
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "E.1",
+ desc: "Remove orphan icons",
+
+ _placeId: null,
+
+ setup: function() {
+ // Insert favicon entries
+ let stmt = mDBConn.createStatement("INSERT INTO moz_favicons (id, url) VALUES(:favicon_id, :url)");
+ stmt.params["favicon_id"] = 1;
+ stmt.params["url"] = "http://www1.mozilla.org/favicon.ico";
+ stmt.execute();
+ stmt.reset();
+ stmt.params["favicon_id"] = 2;
+ stmt.params["url"] = "http://www2.mozilla.org/favicon.ico";
+ stmt.execute();
+ stmt.finalize();
+ // Insert a place using the existing favicon entry
+ this._placeId = addPlace("http://www.mozilla.org", 1);
+ },
+
+ check: function() {
+ // Check that used icon is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_favicons WHERE id = :favicon_id");
+ stmt.params["favicon_id"] = 1;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that unused icon has been removed
+ stmt.params["favicon_id"] = 2;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "F.1",
+ desc: "Remove orphan visits",
+
+ _placeId: null,
+ _invalidPlaceId: 1337,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a valid visit and an invalid one
+ stmt = mDBConn.createStatement("INSERT INTO moz_historyvisits(place_id) VALUES (:place_id)");
+ stmt.params["place_id"] = this._placeId;
+ stmt.execute();
+ stmt.reset();
+ stmt.params["place_id"] = this._invalidPlaceId;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that valid visit is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_historyvisits WHERE place_id = :place_id");
+ stmt.params["place_id"] = this._placeId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that invalid visit has been removed
+ stmt.params["place_id"] = this._invalidPlaceId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "G.1",
+ desc: "Remove orphan input history",
+
+ _placeId: null,
+ _invalidPlaceId: 1337,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add input history entries
+ let stmt = mDBConn.createStatement("INSERT INTO moz_inputhistory (place_id, input) VALUES (:place_id, :input)");
+ stmt.params["place_id"] = this._placeId;
+ stmt.params["input"] = "moz";
+ stmt.execute();
+ stmt.reset();
+ stmt.params["place_id"] = this._invalidPlaceId;
+ stmt.params["input"] = "moz";
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that inputhistory on valid place is still there
+ let stmt = mDBConn.createStatement("SELECT place_id FROM moz_inputhistory WHERE place_id = :place_id");
+ stmt.params["place_id"] = this._placeId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that inputhistory on invalid place has gone
+ stmt.params["place_id"] = this._invalidPlaceId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "H.1",
+ desc: "Remove item annos with an invalid attribute",
+
+ _usedItemAttribute: "usedItem",
+ _bookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert a bookmark
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add a used attribute.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ // Add an annotation with a nonexistent attribute
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, 1337)");
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that annotation with bogus attribute has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = 1337");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "H.2",
+ desc: "Remove orphan item annotations",
+
+ _usedItemAttribute: "usedItem",
+ _bookmarkId: null,
+ _invalidBookmarkId: 8888,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert a bookmark
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add a used attribute.
+ stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES (:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params["item_id"] = this._bookmarkId;
+ stmt.params["anno"] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.reset();
+ // Add an annotation to a nonexistent item
+ stmt.params["item_id"] = this._invalidBookmarkId;
+ stmt.params["anno"] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that an annotation to a nonexistent page has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE item_id = 8888");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "I.1",
+ desc: "Remove unused keywords",
+
+ _bookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Insert 2 keywords
+ let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword, place_id) VALUES(:id, :keyword, :place_id)");
+ stmt.params["id"] = 1;
+ stmt.params["keyword"] = "unused";
+ stmt.params["place_id"] = 100;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that "used" keyword is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_keywords WHERE keyword = :keyword");
+ // Check that "unused" keyword has gone
+ stmt.params["keyword"] = "unused";
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.1",
+ desc: "Fix wrong favicon ids",
+
+ _validIconPlaceId: null,
+ _invalidIconPlaceId: null,
+
+ setup: function() {
+ // Insert a favicon entry
+ let stmt = mDBConn.createStatement("INSERT INTO moz_favicons (id, url) VALUES(1, :url)");
+ stmt.params["url"] = "http://www.mozilla.org/favicon.ico";
+ stmt.execute();
+ stmt.finalize();
+ // Insert a place using the existing favicon entry
+ this._validIconPlaceId = addPlace("http://www1.mozilla.org", 1);
+
+ // Insert a place using a nonexistent favicon entry
+ this._invalidIconPlaceId = addPlace("http://www2.mozilla.org", 1337);
+ },
+
+ check: function() {
+ // Check that bogus favicon is not there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_places WHERE favicon_id = :favicon_id");
+ stmt.params["favicon_id"] = 1337;
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ // Check that valid favicon is still there
+ stmt.params["favicon_id"] = 1;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that place entries are there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_places WHERE id = :place_id");
+ stmt.params["place_id"] = this._validIconPlaceId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["place_id"] = this._invalidIconPlaceId;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.2",
+ desc: "Recalculate visit_count and last_visit_date",
+
+ setup: function* () {
+ function setVisitCount(aURL, aValue) {
+ let stmt = mDBConn.createStatement(
+ `UPDATE moz_places SET visit_count = :count WHERE url_hash = hash(:url)
+ AND url = :url`
+ );
+ stmt.params.count = aValue;
+ stmt.params.url = aURL;
+ stmt.execute();
+ stmt.finalize();
+ }
+ function setLastVisitDate(aURL, aValue) {
+ let stmt = mDBConn.createStatement(
+ `UPDATE moz_places SET last_visit_date = :date WHERE url_hash = hash(:url)
+ AND url = :url`
+ );
+ stmt.params.date = aValue;
+ stmt.params.url = aURL;
+ stmt.execute();
+ stmt.finalize();
+ }
+
+ let now = Date.now() * 1000;
+ // Add a page with 1 visit.
+ let url = "http://1.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ // Add a page with 1 visit and set wrong visit_count.
+ url = "http://2.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ setVisitCount(url, 10);
+ // Add a page with 1 visit and set wrong last_visit_date.
+ url = "http://3.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ setLastVisitDate(url, now++);
+ // Add a page with 1 visit and set wrong stats.
+ url = "http://4.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ setVisitCount(url, 10);
+ setLastVisitDate(url, now++);
+
+ // Add a page without visits.
+ url = "http://5.moz.org/";
+ addPlace(url);
+ // Add a page without visits and set wrong visit_count.
+ url = "http://6.moz.org/";
+ addPlace(url);
+ setVisitCount(url, 10);
+ // Add a page without visits and set wrong last_visit_date.
+ url = "http://7.moz.org/";
+ addPlace(url);
+ setLastVisitDate(url, now++);
+ // Add a page without visits and set wrong stats.
+ url = "http://8.moz.org/";
+ addPlace(url);
+ setVisitCount(url, 10);
+ setLastVisitDate(url, now++);
+ },
+
+ check: function() {
+ let stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h
+ JOIN moz_historyvisits v ON v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9)
+ GROUP BY h.id HAVING h.visit_count <> count(*)
+ UNION ALL
+ SELECT h.id FROM moz_places h
+ JOIN moz_historyvisits v ON v.place_id = h.id
+ GROUP BY h.id HAVING h.last_visit_date <> MAX(v.visit_date)`
+ );
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.3",
+ desc: "recalculate hidden for redirects.",
+
+ *setup() {
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://l3.moz.org/"),
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://l3.moz.org/redirecting/"),
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://l3.moz.org/redirecting2/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ referrer: NetUtil.newURI("http://l3.moz.org/redirecting/") },
+ { uri: NetUtil.newURI("http://l3.moz.org/target/"),
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ referrer: NetUtil.newURI("http://l3.moz.org/redirecting2/") },
+ ]);
+ },
+
+ check: function () {
+ return new Promise(resolve => {
+ let stmt = mDBConn.createAsyncStatement(
+ "SELECT h.url FROM moz_places h WHERE h.hidden = 1"
+ );
+ stmt.executeAsync({
+ _count: 0,
+ handleResult: function(aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ let url = row.getResultByIndex(0);
+ do_check_true(/redirecting/.test(url));
+ this._count++;
+ }
+ },
+ handleError: function(aError) {
+ },
+ handleCompletion: function(aReason) {
+ dump_table("moz_places");
+ dump_table("moz_historyvisits");
+ do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
+ do_check_eq(this._count, 2);
+ resolve();
+ }
+ });
+ stmt.finalize();
+ });
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.4",
+ desc: "recalculate foreign_count.",
+
+ *setup() {
+ this._pageGuid = (yield PlacesUtils.history.insert({ url: "http://l4.moz.org/",
+ visits: [{ date: new Date() }] })).guid;
+ yield PlacesUtils.bookmarks.insert({ url: "http://l4.moz.org/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid});
+ yield PlacesUtils.keywords.insert({ url: "http://l4.moz.org/", keyword: "kw" });
+ Assert.equal((yield this._getForeignCount()), 2);
+ },
+
+ *_getForeignCount() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT foreign_count FROM moz_places
+ WHERE guid = :guid`, { guid: this._pageGuid });
+ return rows[0].getResultByName("foreign_count");
+ },
+
+ *check() {
+ Assert.equal((yield this._getForeignCount()), 2);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.5",
+ desc: "recalculate hashes when missing.",
+
+ *setup() {
+ this._pageGuid = (yield PlacesUtils.history.insert({ url: "http://l5.moz.org/",
+ visits: [{ date: new Date() }] })).guid;
+ Assert.ok((yield this._getHash()) > 0);
+ yield PlacesUtils.withConnectionWrapper("change url hash", Task.async(function* (db) {
+ yield db.execute(`UPDATE moz_places SET url_hash = 0`);
+ }));
+ Assert.equal((yield this._getHash()), 0);
+ },
+
+ *_getHash() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT url_hash FROM moz_places
+ WHERE guid = :guid`, { guid: this._pageGuid });
+ return rows[0].getResultByName("url_hash");
+ },
+
+ *check() {
+ Assert.ok((yield this._getHash()) > 0);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "Z",
+ desc: "Sanity: Preventive maintenance does not touch valid items",
+
+ _uri1: uri("http://www1.mozilla.org"),
+ _uri2: uri("http://www2.mozilla.org"),
+ _folderId: null,
+ _bookmarkId: null,
+ _separatorId: null,
+
+ setup: function* () {
+ // use valid api calls to create a bunch of items
+ yield PlacesTestUtils.addVisits([
+ { uri: this._uri1 },
+ { uri: this._uri2 },
+ ]);
+
+ this._folderId = bs.createFolder(bs.toolbarFolder, "testfolder",
+ bs.DEFAULT_INDEX);
+ do_check_true(this._folderId > 0);
+ this._bookmarkId = bs.insertBookmark(this._folderId, this._uri1,
+ bs.DEFAULT_INDEX, "testbookmark");
+ do_check_true(this._bookmarkId > 0);
+ this._separatorId = bs.insertSeparator(bs.unfiledBookmarksFolder,
+ bs.DEFAULT_INDEX);
+ do_check_true(this._separatorId > 0);
+ ts.tagURI(this._uri1, ["testtag"]);
+ fs.setAndFetchFaviconForPage(this._uri2, SMALLPNG_DATA_URI, false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ yield PlacesUtils.keywords.insert({ url: this._uri1.spec, keyword: "testkeyword" });
+ as.setPageAnnotation(this._uri2, "anno", "anno", 0, as.EXPIRE_NEVER);
+ as.setItemAnnotation(this._bookmarkId, "anno", "anno", 0, as.EXPIRE_NEVER);
+ },
+
+ check: Task.async(function* () {
+ // Check that all items are correct
+ let isVisited = yield promiseIsURIVisited(this._uri1);
+ do_check_true(isVisited);
+ isVisited = yield promiseIsURIVisited(this._uri2);
+ do_check_true(isVisited);
+
+ do_check_eq(bs.getBookmarkURI(this._bookmarkId).spec, this._uri1.spec);
+ do_check_eq(bs.getItemIndex(this._folderId), 0);
+ do_check_eq(bs.getItemType(this._folderId), bs.TYPE_FOLDER);
+ do_check_eq(bs.getItemType(this._separatorId), bs.TYPE_SEPARATOR);
+
+ do_check_eq(ts.getTagsForURI(this._uri1).length, 1);
+ do_check_eq((yield PlacesUtils.keywords.fetch({ url: this._uri1.spec })).keyword, "testkeyword");
+ do_check_eq(as.getPageAnnotation(this._uri2, "anno"), "anno");
+ do_check_eq(as.getItemAnnotation(this._bookmarkId, "anno"), "anno");
+
+ yield new Promise(resolve => {
+ fs.getFaviconURLForPage(this._uri2, aFaviconURI => {
+ do_check_true(aFaviconURI.equals(SMALLPNG_DATA_URI));
+ resolve();
+ });
+ });
+ })
+});
+
+// ------------------------------------------------------------------------------
+
+add_task(function* test_preventive_maintenance()
+{
+ // Get current bookmarks max ID for cleanup
+ let stmt = mDBConn.createStatement("SELECT MAX(id) FROM moz_bookmarks");
+ stmt.executeStep();
+ defaultBookmarksMaxId = stmt.getInt32(0);
+ stmt.finalize();
+ do_check_true(defaultBookmarksMaxId > 0);
+
+ for (let test of tests) {
+ dump("\nExecuting test: " + test.name + "\n" + "*** " + test.desc + "\n");
+ yield test.setup();
+
+ let promiseMaintenanceFinished =
+ promiseTopicObserved(FINISHED_MAINTENANCE_NOTIFICATION_TOPIC);
+ Services.prefs.clearUserPref("places.database.lastMaintenance");
+ let callbackInvoked = false;
+ PlacesDBUtils.maintenanceOnIdle(() => callbackInvoked = true);
+ yield promiseMaintenanceFinished;
+ do_check_true(callbackInvoked);
+
+ // Check the lastMaintenance time has been saved.
+ do_check_neq(Services.prefs.getIntPref("places.database.lastMaintenance"), null);
+
+ yield test.check();
+
+ cleanDatabase();
+ }
+
+ // Sanity check: all roots should be intact
+ do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+});
diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
new file mode 100644
index 0000000000..a8acb4be05
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
@@ -0,0 +1,50 @@
+/* -*- 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/. */
+
+ /**
+ * Test preventive maintenance checkAndFixDatabase.
+ */
+
+// Include PlacesDBUtils module.
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+function run_test() {
+ do_test_pending();
+ PlacesDBUtils.checkAndFixDatabase(function(aLog) {
+ let sections = [];
+ let positives = [];
+ let negatives = [];
+ let infos = [];
+
+ aLog.forEach(function (aMsg) {
+ print (aMsg);
+ switch (aMsg.substr(0, 1)) {
+ case "+":
+ positives.push(aMsg);
+ break;
+ case "-":
+ negatives.push(aMsg);
+ break;
+ case ">":
+ sections.push(aMsg);
+ break;
+ default:
+ infos.push(aMsg);
+ }
+ });
+
+ print("Check that we have run all sections.");
+ do_check_eq(sections.length, 5);
+ print("Check that we have no negatives.");
+ do_check_false(!!negatives.length);
+ print("Check that we have positives.");
+ do_check_true(!!positives.length);
+ print("Check that we have info.");
+ do_check_true(!!infos.length);
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
new file mode 100644
index 0000000000..ebe308f03c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
@@ -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/. */
+
+ /**
+ * Test preventive maintenance runTasks.
+ */
+
+// Include PlacesDBUtils module.
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+function run_test() {
+ do_test_pending();
+ PlacesDBUtils.runTasks([PlacesDBUtils.reindex], function(aLog) {
+ let sections = [];
+ let positives = [];
+ let negatives = [];
+ let infos = [];
+
+ aLog.forEach(function (aMsg) {
+ print (aMsg);
+ switch (aMsg.substr(0, 1)) {
+ case "+":
+ positives.push(aMsg);
+ break;
+ case "-":
+ negatives.push(aMsg);
+ break;
+ case ">":
+ sections.push(aMsg);
+ break;
+ default:
+ infos.push(aMsg);
+ }
+ });
+
+ print("Check that we have run all sections.");
+ do_check_eq(sections.length, 1);
+ print("Check that we have no negatives.");
+ do_check_false(!!negatives.length);
+ print("Check that we have positives.");
+ do_check_true(!!positives.length);
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
new file mode 100644
index 0000000000..0719a0cd43
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
@@ -0,0 +1,256 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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* check_has_child(aParentGuid, aChildGuid) {
+ let parentTree = yield PlacesUtils.promiseBookmarksTree(aParentGuid);
+ do_check_true("children" in parentTree);
+ do_check_true(parentTree.children.find( e => e.guid == aChildGuid ) != null);
+}
+
+function* compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) {
+ // itemId==-1 indicates a non-bookmark node, which is unexpected.
+ do_check_neq(aNode.itemId, -1);
+
+ function check_unset(...aProps) {
+ aProps.forEach( p => { do_check_false(p in aItem); } );
+ }
+ function strict_eq_check(v1, v2) {
+ dump("v1: " + v1 + " v2: " + v2 + "\n");
+ do_check_eq(typeof v1, typeof v2);
+ do_check_eq(v1, v2);
+ }
+ function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) {
+ if (aOptional && aNode[aNodeProp] === null)
+ check_unset(aItemProp);
+ else
+ strict_eq_check(aItem[aItemProp], aNode[aNodeProp]);
+ }
+ function compare_prop_to_value(aItemProp, aValue, aOptional = true) {
+ if (aOptional && aValue === null)
+ check_unset(aItemProp);
+ else
+ strict_eq_check(aItem[aItemProp], aValue);
+ }
+
+ // Bug 1013053 - bookmarkIndex is unavailable for the query's root
+ if (aNode.bookmarkIndex == -1) {
+ compare_prop_to_value("index",
+ PlacesUtils.bookmarks.getItemIndex(aNode.itemId),
+ false);
+ }
+ else {
+ compare_prop("index", "bookmarkIndex");
+ }
+
+ compare_prop("dateAdded");
+ compare_prop("lastModified");
+
+ if (aIsRootItem && aNode.itemId != PlacesUtils.placesRootId) {
+ do_check_true("parentGuid" in aItem);
+ yield check_has_child(aItem.parentGuid, aItem.guid)
+ }
+ else {
+ check_unset("parentGuid");
+ }
+
+ let expectedAnnos = PlacesUtils.getAnnotationsForItem(aItem.id);
+ if (expectedAnnos.length > 0) {
+ let annosToString = annos => {
+ return annos.map(a => a.name + ":" + a.value).sort().join(",");
+ };
+ do_check_true(Array.isArray(aItem.annos))
+ do_check_eq(annosToString(aItem.annos), annosToString(expectedAnnos));
+ }
+ else {
+ check_unset("annos");
+ }
+ const BOOKMARK_ONLY_PROPS = ["uri", "iconuri", "tags", "charset", "keyword"];
+ const FOLDER_ONLY_PROPS = ["children", "root"];
+
+ let nodesCount = 1;
+
+ switch (aNode.type) {
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
+ do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+ compare_prop("title", "title", true);
+ check_unset(...BOOKMARK_ONLY_PROPS);
+
+ let expectedChildrenNodes = [];
+
+ PlacesUtils.asContainer(aNode);
+ if (!aNode.containerOpen)
+ aNode.containerOpen = true;
+
+ for (let i = 0; i < aNode.childCount; i++) {
+ let childNode = aNode.getChild(i);
+ if (childNode.itemId == PlacesUtils.tagsFolderId ||
+ aExcludedGuids.includes(childNode.bookmarkGuid)) {
+ continue;
+ }
+ expectedChildrenNodes.push(childNode);
+ }
+
+ if (expectedChildrenNodes.length > 0) {
+ do_check_true(Array.isArray(aItem.children));
+ do_check_eq(aItem.children.length, expectedChildrenNodes.length);
+ for (let i = 0; i < aItem.children.length; i++) {
+ nodesCount +=
+ yield compareToNode(aItem.children[i], expectedChildrenNodes[i],
+ false, aExcludedGuids);
+ }
+ }
+ else {
+ check_unset("children");
+ }
+
+ switch (aItem.id) {
+ case PlacesUtils.placesRootId:
+ compare_prop_to_value("root", "placesRoot");
+ break;
+ case PlacesUtils.bookmarksMenuFolderId:
+ compare_prop_to_value("root", "bookmarksMenuFolder");
+ break;
+ case PlacesUtils.toolbarFolderId:
+ compare_prop_to_value("root", "toolbarFolder");
+ break;
+ case PlacesUtils.unfiledBookmarksFolderId:
+ compare_prop_to_value("root", "unfiledBookmarksFolder");
+ break;
+ case PlacesUtils.mobileFolderId:
+ compare_prop_to_value("root", "mobileFolder");
+ break;
+ default:
+ check_unset("root");
+ }
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
+ do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
+ check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
+ break;
+ default:
+ do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE);
+ compare_prop("uri");
+ // node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b"
+ if (aNode.tags === null)
+ check_unset("tags");
+ else
+ compare_prop_to_value("tags", aNode.tags.replace(/, /g, ","), false);
+
+ if (aNode.icon) {
+ let nodeIconData = aNode.icon.replace("moz-anno:favicon:", "");
+ compare_prop_to_value("iconuri", nodeIconData);
+ }
+ else {
+ check_unset(aItem.iconuri);
+ }
+
+ check_unset(...FOLDER_ONLY_PROPS);
+
+ let itemURI = uri(aNode.uri);
+ compare_prop_to_value("charset",
+ yield PlacesUtils.getCharsetForURI(itemURI));
+
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ compare_prop_to_value("keyword", entry ? entry.keyword : null);
+
+ if ("title" in aItem)
+ compare_prop("title");
+ else
+ do_check_null(aNode.title);
+ }
+
+ if (aIsRootItem)
+ do_check_eq(aItem.itemsCount, nodesCount);
+
+ return nodesCount;
+}
+
+var itemsCount = 0;
+function* new_bookmark(aInfo) {
+ ++itemsCount;
+ if (!("url" in aInfo))
+ aInfo.url = uri("http://test.item." + itemsCount);
+
+ if (!("title" in aInfo))
+ aInfo.title = "Test Item (bookmark) " + itemsCount;
+
+ yield PlacesTransactions.NewBookmark(aInfo).transact();
+}
+
+function* new_folder(aInfo) {
+ if (!("title" in aInfo))
+ aInfo.title = "Test Item (folder) " + itemsCount;
+ return yield PlacesTransactions.NewFolder(aInfo).transact();
+}
+
+// Walks a result nodes tree and test promiseBookmarksTree for each node.
+// DO NOT COPY THIS LOGIC: It is done here to accomplish a more comprehensive
+// test of the API (the entire hierarchy data is available in the very test).
+function* test_promiseBookmarksTreeForEachNode(aNode, aOptions, aExcludedGuids) {
+ do_check_true(aNode.bookmarkGuid && aNode.bookmarkGuid.length > 0);
+ let item = yield PlacesUtils.promiseBookmarksTree(aNode.bookmarkGuid, aOptions);
+ yield* compareToNode(item, aNode, true, aExcludedGuids);
+
+ for (let i = 0; i < aNode.childCount; i++) {
+ let child = aNode.getChild(i);
+ if (child.itemId != PlacesUtils.tagsFolderId)
+ yield test_promiseBookmarksTreeForEachNode(child,
+ { includeItemIds: true },
+ aExcludedGuids);
+ }
+ return item;
+}
+
+function* test_promiseBookmarksTreeAgainstResult(aItemGuid = "",
+ aOptions = { includeItemIds: true },
+ aExcludedGuids) {
+ let itemId = aItemGuid ?
+ yield PlacesUtils.promiseItemId(aItemGuid) : PlacesUtils.placesRootId;
+ let node = PlacesUtils.getFolderContents(itemId).root;
+ return yield test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGuids);
+}
+
+add_task(function* () {
+ // Add some bookmarks to cover various use cases.
+ yield new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid });
+ yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ annotations: [{ name: "TestAnnoA", value: "TestVal" },
+ { name: "TestAnnoB", value: 0 }]});
+ let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid };
+ yield PlacesTransactions.NewSeparator(sepInfo).transact();
+ let folderGuid = yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid });
+ yield new_bookmark({ title: null,
+ parentGuid: folderGuid,
+ keyword: "test_keyword",
+ tags: ["TestTagA", "TestTagB"],
+ annotations: [{ name: "TestAnnoA", value: "TestVal2"}]});
+ let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
+ yield new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon });
+ yield PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8");
+ yield promiseSetIconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
+ // Test the default places root without specifying it.
+ yield test_promiseBookmarksTreeAgainstResult();
+
+ // Do specify it
+ yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid);
+
+ // Exclude the bookmarks menu.
+ // The calllback should be four times - once for the toolbar, once for
+ // the bookmark we inserted under, and once for the menu (and not
+ // at all for any of its descendants) and once for the unsorted bookmarks
+ // folder. However, promiseBookmarksTree is called multiple times, so
+ // rather than counting the calls, we count the number of unique items
+ // passed in.
+ let guidsPassedToExcludeCallback = new Set();
+ let placesRootWithoutTheMenu =
+ yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid, {
+ excludeItemsCallback: aItem => {
+ guidsPassedToExcludeCallback.add(aItem.guid);
+ return aItem.root == "bookmarksMenuFolder";
+ },
+ includeItemIds: true
+ }, [PlacesUtils.bookmarks.menuGuid]);
+ do_check_eq(guidsPassedToExcludeCallback.size, 5);
+ do_check_eq(placesRootWithoutTheMenu.children.length, 3);
+});
diff --git a/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js
new file mode 100644
index 0000000000..01fb3eef93
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js
@@ -0,0 +1,49 @@
+/* -*- 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/. */
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_resolveNullBookmarkTitles() {
+ let uri1 = uri("http://foo.tld/");
+ let uri2 = uri("https://bar.tld/");
+
+ PlacesTestUtils.addVisits([
+ { uri: uri1, title: "foo title" },
+ { uri: uri2, title: "bar title" }
+ ]).then(function () {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ null);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri2,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ null);
+
+ PlacesUtils.tagging.tagURI(uri1, ["tag 1"]);
+ PlacesUtils.tagging.tagURI(uri2, ["tag 2"]);
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = options.RESULTS_AS_TAG_CONTENTS;
+
+ let query = PlacesUtils.history.getNewQuery();
+ // if we don't set a tag folder, RESULTS_AS_TAG_CONTENTS will return all
+ // tagged URIs
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ // actually RESULTS_AS_TAG_CONTENTS return results ordered by place_id DESC
+ // so they are reversed
+ do_check_eq(root.getChild(0).title, "bar title");
+ do_check_eq(root.getChild(1).title, "foo title");
+ root.containerOpen = false;
+
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/places/tests/unit/test_result_sort.js b/toolkit/components/places/tests/unit/test_result_sort.js
new file mode 100644
index 0000000000..35405ac506
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_result_sort.js
@@ -0,0 +1,139 @@
+/* -*- 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/. */
+
+const NHQO = Ci.nsINavHistoryQueryOptions;
+
+/**
+ * Waits for onItemVisited notifications to be received.
+ */
+function promiseOnItemVisited() {
+ let defer = Promise.defer();
+ let bookmarksObserver = {
+ __proto__: NavBookmarkObserver.prototype,
+ onItemVisited: function BO_onItemVisited() {
+ PlacesUtils.bookmarks.removeObserver(this);
+ // Enqueue to be sure that all onItemVisited notifications ran.
+ do_execute_soon(defer.resolve);
+ }
+ };
+ PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+ return defer.promise;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test() {
+ let testFolder = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.bookmarks.placesRoot,
+ "Result-sort functionality tests root",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ let uri1 = NetUtil.newURI("http://foo.tld/a");
+ let uri2 = NetUtil.newURI("http://foo.tld/b");
+
+ let id1 = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri1, PlacesUtils.bookmarks.DEFAULT_INDEX, "b");
+ let id2 = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri2, PlacesUtils.bookmarks.DEFAULT_INDEX, "a");
+ // url of id1, title of id2
+ let id3 = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri1, PlacesUtils.bookmarks.DEFAULT_INDEX, "a");
+
+ // query with natural order
+ let result = PlacesUtils.getFolderContents(testFolder);
+ let root = result.root;
+
+ do_check_eq(root.childCount, 3);
+
+ function checkOrder(a, b, c) {
+ do_check_eq(root.getChild(0).itemId, a);
+ do_check_eq(root.getChild(1).itemId, b);
+ do_check_eq(root.getChild(2).itemId, c);
+ }
+
+ // natural order
+ do_print("Natural order");
+ checkOrder(id1, id2, id3);
+
+ // title: id3 should precede id2 since we fall-back to URI-based sorting
+ do_print("Sort by title asc");
+ result.sortingMode = NHQO.SORT_BY_TITLE_ASCENDING;
+ checkOrder(id3, id2, id1);
+
+ // In reverse
+ do_print("Sort by title desc");
+ result.sortingMode = NHQO.SORT_BY_TITLE_DESCENDING;
+ checkOrder(id1, id2, id3);
+
+ // uri sort: id1 should precede id3 since we fall-back to natural order
+ do_print("Sort by uri asc");
+ result.sortingMode = NHQO.SORT_BY_URI_ASCENDING;
+ checkOrder(id1, id3, id2);
+
+ // test live update
+ do_print("Change bookmark uri liveupdate");
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, uri2);
+ checkOrder(id3, id1, id2);
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, uri1);
+ checkOrder(id1, id3, id2);
+
+ // keyword sort
+ do_print("Sort by keyword asc");
+ result.sortingMode = NHQO.SORT_BY_KEYWORD_ASCENDING;
+ checkOrder(id3, id2, id1); // no keywords set - falling back to title sort
+ yield PlacesUtils.keywords.insert({ url: uri1.spec, keyword: "a" });
+ yield PlacesUtils.keywords.insert({ url: uri2.spec, keyword: "z" });
+ checkOrder(id3, id1, id2);
+
+ // XXXtodo: test history sortings (visit count, visit date)
+ // XXXtodo: test different item types once folderId and bookmarkId are merged.
+ // XXXtodo: test sortingAnnotation functionality with non-bookmark nodes
+
+ do_print("Sort by annotation desc");
+ PlacesUtils.annotations.setItemAnnotation(id1, "testAnno", "a", 0, 0);
+ PlacesUtils.annotations.setItemAnnotation(id3, "testAnno", "b", 0, 0);
+ result.sortingAnnotation = "testAnno";
+ result.sortingMode = NHQO.SORT_BY_ANNOTATION_DESCENDING;
+
+ // id1 precedes id2 per title-descending fallback
+ checkOrder(id3, id1, id2);
+
+ // XXXtodo: test dateAdded sort
+ // XXXtodo: test lastModified sort
+
+ // test live update
+ do_print("Annotation liveupdate");
+ PlacesUtils.annotations.setItemAnnotation(id1, "testAnno", "c", 0, 0);
+ checkOrder(id1, id3, id2);
+
+ // Add a visit, then check frecency ordering.
+
+ // When the bookmarks service gets onVisit, it asynchronously fetches all
+ // items for that visit, and then notifies onItemVisited. Thus we must
+ // explicitly wait for that.
+ let waitForVisited = promiseOnItemVisited();
+ yield PlacesTestUtils.addVisits({ uri: uri2, transition: TRANSITION_TYPED });
+ yield waitForVisited;
+
+ do_print("Sort by frecency desc");
+ result.sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING;
+ for (let i = 0; i < root.childCount; ++i) {
+ print(root.getChild(i).uri + " " + root.getChild(i).title);
+ }
+ // For id1 and id3, since they have same frecency and no visits, fallback
+ // to sort by the newest bookmark.
+ checkOrder(id2, id3, id1);
+ do_print("Sort by frecency asc");
+ result.sortingMode = NHQO.SORT_BY_FRECENCY_ASCENDING;
+ for (let i = 0; i < root.childCount; ++i) {
+ print(root.getChild(i).uri + " " + root.getChild(i).title);
+ }
+ checkOrder(id1, id3, id2);
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js
new file mode 100644
index 0000000000..8e71ffd0dc
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+const {bookmarks, history} = PlacesUtils
+
+add_task(function* test_addVisitCheckFields() {
+ let uri = NetUtil.newURI("http://test4.com/");
+ yield PlacesTestUtils.addVisits([
+ { uri },
+ { uri, referrer: uri },
+ { uri, transition: history.TRANSITION_TYPED },
+ ]);
+
+
+ let options = history.getNewQueryOptions();
+ let query = history.getNewQuery();
+
+ query.uri = uri;
+
+
+ // Check RESULTS_AS_VISIT node.
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ let root = history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ equal(root.childCount, 3);
+
+ let child = root.getChild(0);
+ equal(child.visitType, history.TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+ equal(child.visitId, 1, "Visit ID should be 1");
+ equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+
+ child = root.getChild(1);
+ equal(child.visitType, history.TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+ equal(child.visitId, 2, "Visit ID should be 2");
+ equal(child.fromVisitId, 1, "First visit should be the referring visit");
+
+ child = root.getChild(2);
+ equal(child.visitType, history.TRANSITION_TYPED, "Visit type should be TRANSITION_TYPED");
+ equal(child.visitId, 3, "Visit ID should be 3");
+ equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+
+ root.containerOpen = false;
+
+
+ // Check RESULTS_AS_URI node.
+ options.resultType = options.RESULTS_AS_URI;
+
+ root = history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ equal(root.childCount, 1);
+
+ child = root.getChild(0);
+ equal(child.visitType, 0, "Visit type should be 0");
+ equal(child.visitId, -1, "Visit ID should be -1");
+ equal(child.fromVisitId, -1, "Referrer visit id should be -1");
+
+ root.containerOpen = false;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_bookmarkFields() {
+ let folder = bookmarks.createFolder(bookmarks.placesRoot, "test folder", bookmarks.DEFAULT_INDEX);
+ bookmarks.insertBookmark(folder, uri("http://test4.com/"),
+ bookmarks.DEFAULT_INDEX, "test4 title");
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ equal(root.childCount, 1);
+
+ equal(root.visitType, 0, "Visit type should be 0");
+ equal(root.visitId, -1, "Visit ID should be -1");
+ equal(root.fromVisitId, -1, "Referrer visit id should be -1");
+
+ let child = root.getChild(0);
+ equal(child.visitType, 0, "Visit type should be 0");
+ equal(child.visitId, -1, "Visit ID should be -1");
+ equal(child.fromVisitId, -1, "Referrer visit id should be -1");
+
+ root.containerOpen = false;
+
+ yield bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_sql_guid_functions.js b/toolkit/components/places/tests/unit/test_sql_guid_functions.js
new file mode 100644
index 0000000000..41e6bab9e7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_sql_guid_functions.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests that the guid function generates a guid of the proper length,
+ * with no invalid characters.
+ */
+
+/**
+ * Checks all our invariants about our guids for a given result.
+ *
+ * @param aGuid
+ * The guid to check.
+ */
+function check_invariants(aGuid)
+{
+ do_print("Checking guid '" + aGuid + "'");
+
+ do_check_valid_places_guid(aGuid);
+}
+
+// Test Functions
+
+function test_guid_invariants()
+{
+ const kExpectedChars = 64;
+ const kAllowedChars =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
+ do_check_eq(kAllowedChars.length, kExpectedChars);
+ const kGuidLength = 12;
+
+ let checkedChars = [];
+ for (let i = 0; i < kGuidLength; i++) {
+ checkedChars[i] = {};
+ for (let j = 0; j < kAllowedChars; j++) {
+ checkedChars[i][kAllowedChars[j]] = false;
+ }
+ }
+
+ // We run this until we've seen every character that we expect to see in every
+ // position.
+ let seenChars = 0;
+ let stmt = DBConn().createStatement("SELECT GENERATE_GUID()");
+ while (seenChars != (kExpectedChars * kGuidLength)) {
+ do_check_true(stmt.executeStep());
+ let guid = stmt.getString(0);
+ check_invariants(guid);
+
+ for (let i = 0; i < guid.length; i++) {
+ let character = guid[i];
+ if (!checkedChars[i][character]) {
+ checkedChars[i][character] = true;
+ seenChars++;
+ }
+ }
+ stmt.reset();
+ }
+ stmt.finalize();
+
+ // One last reality check - make sure all of our characters were seen.
+ for (let i = 0; i < kGuidLength; i++) {
+ for (let j = 0; j < kAllowedChars; j++) {
+ do_check_true(checkedChars[i][kAllowedChars[j]]);
+ }
+ }
+
+ run_next_test();
+}
+
+function test_guid_on_background()
+{
+ // We should not assert if we execute this asynchronously.
+ let stmt = DBConn().createAsyncStatement("SELECT GENERATE_GUID()");
+ let checked = false;
+ stmt.executeAsync({
+ handleResult: function(aResult) {
+ try {
+ let row = aResult.getNextRow();
+ check_invariants(row.getResultByIndex(0));
+ do_check_eq(aResult.getNextRow(), null);
+ checked = true;
+ }
+ catch (e) {
+ do_throw(e);
+ }
+ },
+ handleCompletion: function(aReason) {
+ do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
+ do_check_true(checked);
+ run_next_test();
+ }
+ });
+ stmt.finalize();
+}
+
+// Test Runner
+
+[
+ test_guid_invariants,
+ test_guid_on_background,
+].forEach(add_test);
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_svg_favicon.js b/toolkit/components/places/tests/unit/test_svg_favicon.js
new file mode 100644
index 0000000000..cec40ddef4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_svg_favicon.js
@@ -0,0 +1,31 @@
+const PAGEURI = NetUtil.newURI("http://deliciousbacon.com/");
+
+add_task(function* () {
+ // First, add a history entry or else Places can't save a favicon.
+ yield PlacesTestUtils.addVisits({
+ uri: PAGEURI,
+ transition: TRANSITION_LINK,
+ visitDate: Date.now() * 1000
+ });
+
+ yield new Promise(resolve => {
+ function onSetComplete(aURI, aDataLen, aData, aMimeType) {
+ equal(aURI.spec, SMALLSVG_DATA_URI.spec, "setFavicon aURI check");
+ equal(aDataLen, 263, "setFavicon aDataLen check");
+ equal(aMimeType, "image/svg+xml", "setFavicon aMimeType check");
+ resolve();
+ }
+
+ PlacesUtils.favicons.setAndFetchFaviconForPage(PAGEURI, SMALLSVG_DATA_URI,
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ onSetComplete,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ let data = yield PlacesUtils.promiseFaviconData(PAGEURI.spec);
+ equal(data.uri.spec, SMALLSVG_DATA_URI.spec, "getFavicon aURI check");
+ equal(data.dataLen, 263, "getFavicon aDataLen check");
+ equal(data.mimeType, "image/svg+xml", "getFavicon aMimeType check");
+});
+
diff --git a/toolkit/components/places/tests/unit/test_sync_utils.js b/toolkit/components/places/tests/unit/test_sync_utils.js
new file mode 100644
index 0000000000..f8c7e6b58f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_sync_utils.js
@@ -0,0 +1,1150 @@
+Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.importGlobalProperties(["crypto", "URLSearchParams"]);
+
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const SYNC_PARENT_ANNO = "sync/parent";
+
+function makeGuid() {
+ return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
+ pad: false,
+ });
+}
+
+function makeLivemarkServer() {
+ let server = new HttpServer();
+ server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml"));
+ server.start(-1);
+ return {
+ server,
+ get site() {
+ let { identity } = server;
+ let host = identity.primaryHost.includes(":") ?
+ `[${identity.primaryHost}]` : identity.primaryHost;
+ return `${identity.primaryScheme}://${host}:${identity.primaryPort}`;
+ },
+ stopServer() {
+ return new Promise(resolve => server.stop(resolve));
+ },
+ };
+}
+
+function shuffle(array) {
+ let results = [];
+ for (let i = 0; i < array.length; ++i) {
+ let randomIndex = Math.floor(Math.random() * (i + 1));
+ results[i] = results[randomIndex];
+ results[randomIndex] = array[i];
+ }
+ return results;
+}
+
+function compareAscending(a, b) {
+ if (a > b) {
+ return 1;
+ }
+ if (a < b) {
+ return -1;
+ }
+ return 0;
+}
+
+function assertTagForURLs(tag, urls, message) {
+ let taggedURLs = PlacesUtils.tagging.getURIsForTag(tag).map(uri => uri.spec);
+ deepEqual(taggedURLs.sort(compareAscending), urls.sort(compareAscending), message);
+}
+
+function assertURLHasTags(url, tags, message) {
+ let actualTags = PlacesUtils.tagging.getTagsForURI(uri(url));
+ deepEqual(actualTags.sort(compareAscending), tags, message);
+}
+
+var populateTree = Task.async(function* populate(parentGuid, ...items) {
+ let guids = {};
+
+ for (let index = 0; index < items.length; index++) {
+ let item = items[index];
+ let guid = makeGuid();
+
+ switch (item.kind) {
+ case "bookmark":
+ case "query":
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: item.url,
+ title: item.title,
+ parentGuid, guid, index,
+ });
+ break;
+
+ case "separator":
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid, guid,
+ });
+ break;
+
+ case "folder":
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: item.title,
+ parentGuid, guid,
+ });
+ if (item.children) {
+ Object.assign(guids, yield* populate(guid, ...item.children));
+ }
+ break;
+
+ default:
+ throw new Error(`Unsupported item type: ${item.type}`);
+ }
+
+ if (item.exclude) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ PlacesUtils.annotations.setItemAnnotation(
+ itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, "Don't back this up", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ }
+
+ guids[item.title] = guid;
+ }
+
+ return guids;
+});
+
+var syncIdToId = Task.async(function* syncIdToId(syncId) {
+ let guid = yield PlacesSyncUtils.bookmarks.syncIdToGuid(syncId);
+ return PlacesUtils.promiseItemId(guid);
+});
+
+add_task(function* test_order() {
+ do_print("Insert some bookmarks");
+ let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, {
+ kind: "bookmark",
+ title: "childBmk",
+ url: "http://getfirefox.com",
+ }, {
+ kind: "bookmark",
+ title: "siblingBmk",
+ url: "http://getthunderbird.com",
+ }, {
+ kind: "folder",
+ title: "siblingFolder",
+ }, {
+ kind: "separator",
+ title: "siblingSep",
+ });
+
+ do_print("Reorder inserted bookmarks");
+ {
+ let order = [guids.siblingFolder, guids.siblingSep, guids.childBmk,
+ guids.siblingBmk];
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, order);
+ let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(PlacesUtils.bookmarks.menuGuid);
+ deepEqual(childSyncIds, order, "New bookmarks should be reordered according to array");
+ }
+
+ do_print("Reorder with unspecified children");
+ {
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.siblingSep, guids.siblingBmk,
+ ]);
+ let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.menuGuid);
+ deepEqual(childSyncIds, [guids.siblingSep, guids.siblingBmk,
+ guids.siblingFolder, guids.childBmk],
+ "Unordered children should be moved to end");
+ }
+
+ do_print("Reorder with nonexistent children");
+ {
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.childBmk, makeGuid(), guids.siblingBmk, guids.siblingSep,
+ makeGuid(), guids.siblingFolder, makeGuid()]);
+ let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.menuGuid);
+ deepEqual(childSyncIds, [guids.childBmk, guids.siblingBmk, guids.siblingSep,
+ guids.siblingFolder], "Nonexistent children should be ignored");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_changeGuid_invalid() {
+ yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid()),
+ "Should require a new GUID");
+ yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid(), "!@#$"),
+ "Should reject invalid GUIDs");
+ yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid(), makeGuid()),
+ "Should reject nonexistent item GUIDs");
+ yield rejects(
+ PlacesSyncUtils.bookmarks.changeGuid(PlacesUtils.bookmarks.menuGuid,
+ makeGuid()),
+ "Should reject roots");
+});
+
+add_task(function* test_changeGuid() {
+ let item = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "https://mozilla.org",
+ });
+ let id = yield PlacesUtils.promiseItemId(item.guid);
+
+ let newGuid = makeGuid();
+ let result = yield PlacesSyncUtils.bookmarks.changeGuid(item.guid, newGuid);
+ equal(result, newGuid, "Should return new GUID");
+
+ equal(yield PlacesUtils.promiseItemId(newGuid), id, "Should map ID to new GUID");
+ yield rejects(PlacesUtils.promiseItemId(item.guid), "Should not map ID to old GUID");
+ equal(yield PlacesUtils.promiseItemGuid(id), newGuid, "Should map new GUID to ID");
+});
+
+add_task(function* test_order_roots() {
+ let oldOrder = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.rootGuid);
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.rootGuid,
+ shuffle(oldOrder));
+ let newOrder = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.rootGuid);
+ deepEqual(oldOrder, newOrder, "Should ignore attempts to reorder roots");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_tags() {
+ do_print("Insert item without tags");
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://mozilla.org",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ });
+
+ do_print("Add tags");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: ["foo", "bar"],
+ });
+ deepEqual(updatedItem.tags, ["foo", "bar"], "Should return new tags");
+ assertURLHasTags("https://mozilla.org", ["bar", "foo"],
+ "Should set new tags for URL");
+ }
+
+ do_print("Add new tag, remove existing tag");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: ["foo", "baz"],
+ });
+ deepEqual(updatedItem.tags, ["foo", "baz"], "Should return updated tags");
+ assertURLHasTags("https://mozilla.org", ["baz", "foo"],
+ "Should update tags for URL");
+ assertTagForURLs("bar", [], "Should remove existing tag");
+ }
+
+ do_print("Tags with whitespace");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: [" leading", "trailing ", " baz ", " "],
+ });
+ deepEqual(updatedItem.tags, ["leading", "trailing", "baz"],
+ "Should return filtered tags");
+ assertURLHasTags("https://mozilla.org", ["baz", "leading", "trailing"],
+ "Should trim whitespace and filter blank tags");
+ }
+
+ do_print("Remove all tags");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: null,
+ });
+ deepEqual(updatedItem.tags, [], "Should return empty tag array");
+ assertURLHasTags("https://mozilla.org", [],
+ "Should remove all existing tags");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_keyword() {
+ do_print("Insert item without keyword");
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: "menu",
+ url: "https://mozilla.org",
+ syncId: makeGuid(),
+ });
+
+ do_print("Add item keyword");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: "moz",
+ });
+ equal(updatedItem.keyword, "moz", "Should return new keyword");
+ let entryByKeyword = yield PlacesUtils.keywords.fetch("moz");
+ equal(entryByKeyword.url.href, "https://mozilla.org/",
+ "Should set new keyword for URL");
+ let entryByURL = yield PlacesUtils.keywords.fetch({
+ url: "https://mozilla.org",
+ });
+ equal(entryByURL.keyword, "moz", "Looking up URL should return new keyword");
+ }
+
+ do_print("Change item keyword");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: "m",
+ });
+ equal(updatedItem.keyword, "m", "Should return updated keyword");
+ let newEntry = yield PlacesUtils.keywords.fetch("m");
+ equal(newEntry.url.href, "https://mozilla.org/", "Should update keyword for URL");
+ let oldEntry = yield PlacesUtils.keywords.fetch("moz");
+ ok(!oldEntry, "Should remove old keyword");
+ }
+
+ do_print("Remove existing keyword");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: null,
+ });
+ ok(!updatedItem.keyword,
+ "Should not include removed keyword in properties");
+ let entry = yield PlacesUtils.keywords.fetch({
+ url: "https://mozilla.org",
+ });
+ ok(!entry, "Should remove new keyword from URL");
+ }
+
+ do_print("Remove keyword for item without keyword");
+ {
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: null,
+ });
+ let entry = yield PlacesUtils.keywords.fetch({
+ url: "https://mozilla.org",
+ });
+ ok(!entry,
+ "Removing keyword for URL without existing keyword should succeed");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_annos() {
+ let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, {
+ kind: "folder",
+ title: "folder",
+ }, {
+ kind: "bookmark",
+ title: "bmk",
+ url: "https://example.com",
+ });
+
+ do_print("Add folder description");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.folder,
+ description: "Folder description",
+ });
+ equal(updatedItem.description, "Folder description",
+ "Should return new description");
+ let id = yield syncIdToId(updatedItem.syncId);
+ equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO),
+ "Folder description", "Should set description anno");
+ }
+
+ do_print("Clear folder description");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.folder,
+ description: null,
+ });
+ ok(!updatedItem.description, "Should not return cleared description");
+ let id = yield syncIdToId(updatedItem.syncId);
+ ok(!PlacesUtils.annotations.itemHasAnnotation(id, DESCRIPTION_ANNO),
+ "Should remove description anno");
+ }
+
+ do_print("Add bookmark sidebar anno");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.bmk,
+ loadInSidebar: true,
+ });
+ ok(updatedItem.loadInSidebar, "Should return sidebar anno");
+ let id = yield syncIdToId(updatedItem.syncId);
+ ok(PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should set sidebar anno for existing bookmark");
+ }
+
+ do_print("Clear bookmark sidebar anno");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.bmk,
+ loadInSidebar: false,
+ });
+ ok(!updatedItem.loadInSidebar, "Should not return cleared sidebar anno");
+ let id = yield syncIdToId(updatedItem.syncId);
+ ok(!PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should clear sidebar anno for existing bookmark");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_move_root() {
+ do_print("Move root to same parent");
+ {
+ // This should be a no-op.
+ let sameRoot = yield PlacesSyncUtils.bookmarks.update({
+ syncId: "menu",
+ parentSyncId: "places",
+ });
+ equal(sameRoot.syncId, "menu",
+ "Menu root GUID should not change");
+ equal(sameRoot.parentSyncId, "places",
+ "Parent Places root GUID should not change");
+ }
+
+ do_print("Try reparenting root");
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: "menu",
+ parentSyncId: "toolbar",
+ }));
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert() {
+ do_print("Insert bookmark");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ url: "https://example.org",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Bookmark should have correct type");
+ }
+
+ do_print("Insert query");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ url: "place:terms=term&folder=TOOLBAR&queryType=1",
+ folder: "Saved search",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Queries should be stored as bookmarks");
+ }
+
+ do_print("Insert folder");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ title: "New folder",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Folder should have correct type");
+ }
+
+ do_print("Insert separator");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "separator",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ "Separator should have correct type");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_livemark() {
+ let { site, stopServer } = makeLivemarkServer();
+
+ try {
+ do_print("Insert livemark with feed URL");
+ {
+ let livemark = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "livemark",
+ syncId: makeGuid(),
+ feed: site + "/feed/1",
+ parentSyncId: "menu",
+ });
+ let bmk = yield PlacesUtils.bookmarks.fetch({
+ guid: yield PlacesSyncUtils.bookmarks.syncIdToGuid(livemark.syncId),
+ })
+ equal(bmk.type, PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Livemarks should be stored as folders");
+ }
+
+ let livemarkSyncId;
+ do_print("Insert livemark with site and feed URLs");
+ {
+ let livemark = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "livemark",
+ syncId: makeGuid(),
+ site,
+ feed: site + "/feed/1",
+ parentSyncId: "menu",
+ });
+ livemarkSyncId = livemark.syncId;
+ }
+
+ do_print("Try inserting livemark into livemark");
+ {
+ let livemark = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "livemark",
+ syncId: makeGuid(),
+ site,
+ feed: site + "/feed/1",
+ parentSyncId: livemarkSyncId,
+ });
+ ok(!livemark, "Should not insert livemark as child of livemark");
+ }
+ } finally {
+ yield stopServer();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_livemark() {
+ let { site, stopServer } = makeLivemarkServer();
+ let feedURI = uri(site + "/feed/1");
+
+ try {
+ // We shouldn't reinsert the livemark if the URLs are the same.
+ do_print("Update livemark with same URLs");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ feed: feedURI,
+ });
+ // `nsLivemarkService` returns references to `Livemark` instances, so we
+ // can compare them with `==` to make sure they haven't been replaced.
+ equal(yield PlacesUtils.livemarks.getLivemark({
+ guid: livemark.guid,
+ }), livemark, "Livemark with same feed URL should not be replaced");
+
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site,
+ });
+ equal(yield PlacesUtils.livemarks.getLivemark({
+ guid: livemark.guid,
+ }), livemark, "Livemark with same site URL should not be replaced");
+
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ feed: feedURI,
+ site,
+ });
+ equal(yield PlacesUtils.livemarks.getLivemark({
+ guid: livemark.guid,
+ }), livemark, "Livemark with same feed and site URLs should not be replaced");
+ }
+
+ do_print("Change livemark feed URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ // Since we're reinserting, we need to pass all properties required
+ // for a new livemark. `update` won't merge the old and new ones.
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ feed: site + "/feed/2",
+ }), "Reinserting livemark with changed feed URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed: site + "/feed/2",
+ });
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark with changed feed URL");
+ equal(newLivemark.feed.href, site + "/feed/2",
+ "Reinserted livemark should have changed feed URI");
+ }
+
+ do_print("Add livemark site URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ });
+ ok(livemark.feedURI.equals(feedURI), "Livemark feed URI should match");
+ ok(!livemark.siteURI, "Livemark should not have site URI");
+
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site,
+ }), "Reinserting livemark with new site URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed: feedURI,
+ site,
+ });
+ notEqual(newLivemark, livemark,
+ "Livemark with new site URL should replace old livemark");
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark with new site URL");
+ equal(newLivemark.site.href, site + "/",
+ "Reinserted livemark should have new site URI");
+ equal(newLivemark.feed.href, feedURI.spec,
+ "Reinserted livemark with new site URL should have same feed URI");
+ }
+
+ do_print("Remove livemark site URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site: null,
+ }), "Reinserting livemark witout site URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed: feedURI,
+ site: null,
+ });
+ notEqual(newLivemark, livemark,
+ "Livemark without site URL should replace old livemark");
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark without site URL");
+ ok(!newLivemark.site, "Reinserted livemark should not have site URI");
+ }
+
+ do_print("Change livemark site URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site: site + "/new",
+ }), "Reinserting livemark with changed site URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed:feedURI,
+ site: site + "/new",
+ });
+ notEqual(newLivemark, livemark,
+ "Livemark with changed site URL should replace old livemark");
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark with changed site URL");
+ equal(newLivemark.site.href, site + "/new",
+ "Reinserted livemark should have changed site URI");
+ }
+
+ // Livemarks are stored as folders, but have different kinds. We should
+ // remove the folder and insert a livemark with the same GUID instead of
+ // trying to update the folder in-place.
+ do_print("Replace folder with livemark");
+ {
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "Plain folder",
+ });
+ let livemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: folder.guid,
+ feed: feedURI,
+ });
+ equal(livemark.guid, folder.syncId,
+ "Livemark should have same GUID as replaced folder");
+ }
+ } finally {
+ yield stopServer();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_tags() {
+ yield Promise.all([{
+ kind: "bookmark",
+ url: "https://example.com",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ tags: ["foo", "bar"],
+ }, {
+ kind: "bookmark",
+ url: "https://example.org",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ tags: ["foo", "baz"],
+ }, {
+ kind: "query",
+ url: "place:queryType=1&sort=12&maxResults=10",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ folder: "bar",
+ tags: ["baz", "qux"],
+ title: "bar",
+ }].map(info => PlacesSyncUtils.bookmarks.insert(info)));
+
+ assertTagForURLs("foo", ["https://example.com/", "https://example.org/"],
+ "2 URLs with new tag");
+ assertTagForURLs("bar", ["https://example.com/"], "1 URL with existing tag");
+ assertTagForURLs("baz", ["https://example.org/",
+ "place:queryType=1&sort=12&maxResults=10"],
+ "Should support tagging URLs and tag queries");
+ assertTagForURLs("qux", ["place:queryType=1&sort=12&maxResults=10"],
+ "Should support tagging tag queries");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_tags_whitespace() {
+ do_print("Untrimmed and blank tags");
+ let taggedBlanks = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.org",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ tags: [" untrimmed ", " ", "taggy"],
+ });
+ deepEqual(taggedBlanks.tags, ["untrimmed", "taggy"],
+ "Should not return empty tags");
+ assertURLHasTags("https://example.org/", ["taggy", "untrimmed"],
+ "Should set trimmed tags and ignore dupes");
+
+ do_print("Dupe tags");
+ let taggedDupes = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.net",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ tags: [" taggy", "taggy ", " taggy ", "taggy"],
+ });
+ deepEqual(taggedDupes.tags, ["taggy", "taggy", "taggy", "taggy"],
+ "Should return trimmed and dupe tags");
+ assertURLHasTags("https://example.net/", ["taggy"],
+ "Should ignore dupes when setting tags");
+
+ assertTagForURLs("taggy", ["https://example.net/", "https://example.org/"],
+ "Should exclude falsy tags");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_keyword() {
+ do_print("Insert item with new keyword");
+ {
+ yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: "menu",
+ url: "https://example.com",
+ keyword: "moz",
+ syncId: makeGuid(),
+ });
+ let entry = yield PlacesUtils.keywords.fetch("moz");
+ equal(entry.url.href, "https://example.com/",
+ "Should add keyword for item");
+ }
+
+ do_print("Insert item with existing keyword");
+ {
+ yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: "menu",
+ url: "https://mozilla.org",
+ keyword: "moz",
+ syncId: makeGuid(),
+ });
+ let entry = yield PlacesUtils.keywords.fetch("moz");
+ equal(entry.url.href, "https://mozilla.org/",
+ "Should reassign keyword to new item");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_annos() {
+ do_print("Bookmark with description");
+ let descBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.com",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ description: "Bookmark description",
+ });
+ {
+ equal(descBmk.description, "Bookmark description",
+ "Should return new bookmark description");
+ let id = yield syncIdToId(descBmk.syncId);
+ equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO),
+ "Bookmark description", "Should set new bookmark description");
+ }
+
+ do_print("Folder with description");
+ let descFolder = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ description: "Folder description",
+ });
+ {
+ equal(descFolder.description, "Folder description",
+ "Should return new folder description");
+ let id = yield syncIdToId(descFolder.syncId);
+ equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO),
+ "Folder description", "Should set new folder description");
+ }
+
+ do_print("Bookmark with sidebar anno");
+ let sidebarBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.com",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ loadInSidebar: true,
+ });
+ {
+ ok(sidebarBmk.loadInSidebar, "Should return sidebar anno for new bookmark");
+ let id = yield syncIdToId(sidebarBmk.syncId);
+ ok(PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should set sidebar anno for new bookmark");
+ }
+
+ do_print("Bookmark without sidebar anno");
+ let noSidebarBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.org",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ loadInSidebar: false,
+ });
+ {
+ ok(!noSidebarBmk.loadInSidebar,
+ "Should not return sidebar anno for new bookmark");
+ let id = yield syncIdToId(noSidebarBmk.syncId);
+ ok(!PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should not set sidebar anno for new bookmark");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_tag_query() {
+ let tagFolder = -1;
+
+ do_print("Insert tag query for new tag");
+ {
+ deepEqual(PlacesUtils.tagging.allTags, [], "New tag should not exist yet");
+ let query = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ url: "place:type=7&folder=90",
+ folder: "taggy",
+ title: "Tagged stuff",
+ });
+ notEqual(query.url.href, "place:type=7&folder=90",
+ "Tag query URL for new tag should differ");
+
+ [, tagFolder] = /\bfolder=(\d+)\b/.exec(query.url.pathname);
+ ok(tagFolder > 0, "New tag query URL should contain valid folder");
+ deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "New tag should exist");
+ }
+
+ do_print("Insert tag query for existing tag");
+ {
+ let url = "place:type=7&folder=90&maxResults=15";
+ let query = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ url,
+ folder: "taggy",
+ title: "Sorted and tagged",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ });
+ notEqual(query.url.href, url, "Tag query URL for existing tag should differ");
+ let params = new URLSearchParams(query.url.pathname);
+ equal(params.get("type"), "7", "Should preserve query type");
+ equal(params.get("maxResults"), "15", "Should preserve additional params");
+ equal(params.get("folder"), tagFolder, "Should update tag folder");
+ deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "Should not duplicate existing tags");
+ }
+
+ do_print("Use the public tagging API to ensure we added the tag correctly");
+ {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "https://mozilla.org",
+ title: "Mozilla",
+ });
+ PlacesUtils.tagging.tagURI(uri("https://mozilla.org"), ["taggy"]);
+ assertURLHasTags("https://mozilla.org/", ["taggy"],
+ "Should set tags using the tagging API");
+ }
+
+ do_print("Removing the tag should clean up the tag folder");
+ {
+ PlacesUtils.tagging.untagURI(uri("https://mozilla.org"), null);
+ deepEqual(PlacesUtils.tagging.allTags, [],
+ "Should remove tag folder once last item is untagged");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_orphans() {
+ let grandParentGuid = makeGuid();
+ let parentGuid = makeGuid();
+ let childGuid = makeGuid();
+ let childId;
+
+ do_print("Insert an orphaned child");
+ {
+ let child = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: parentGuid,
+ syncId: childGuid,
+ url: "https://mozilla.org",
+ });
+ equal(child.syncId, childGuid,
+ "Should insert orphan with requested GUID");
+ equal(child.parentSyncId, "unfiled",
+ "Should reparent orphan to unfiled");
+
+ childId = yield PlacesUtils.promiseItemId(childGuid);
+ equal(PlacesUtils.annotations.getItemAnnotation(childId, SYNC_PARENT_ANNO),
+ parentGuid, "Should set anno to missing parent GUID");
+ }
+
+ do_print("Insert the grandparent");
+ {
+ yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ parentSyncId: "menu",
+ syncId: grandParentGuid,
+ });
+ equal(PlacesUtils.annotations.getItemAnnotation(childId, SYNC_PARENT_ANNO),
+ parentGuid, "Child should still have orphan anno");
+ }
+
+ // Note that only `PlacesSyncUtils` reparents orphans, though Sync adds an
+ // observer that removes the orphan anno if the orphan is manually moved.
+ do_print("Insert the missing parent");
+ {
+ let parent = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ parentSyncId: grandParentGuid,
+ syncId: parentGuid,
+ });
+ equal(parent.syncId, parentGuid, "Should insert parent with requested GUID");
+ equal(parent.parentSyncId, grandParentGuid,
+ "Parent should be child of grandparent");
+ ok(!PlacesUtils.annotations.itemHasAnnotation(childId, SYNC_PARENT_ANNO),
+ "Orphan anno should be removed after reparenting");
+
+ let child = yield PlacesUtils.bookmarks.fetch({ guid: childGuid });
+ equal(child.parentGuid, parentGuid,
+ "Should reparent child after inserting missing parent");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_fetch() {
+ let folder = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ kind: "folder",
+ description: "Folder description",
+ });
+ let bmk = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ kind: "bookmark",
+ url: "https://example.com",
+ description: "Bookmark description",
+ loadInSidebar: true,
+ tags: ["taggy"],
+ });
+ let folderBmk = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: folder.syncId,
+ kind: "bookmark",
+ url: "https://example.org",
+ keyword: "kw",
+ });
+ let folderSep = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: folder.syncId,
+ kind: "separator",
+ });
+ let tagQuery = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ url: "place:type=7&folder=90",
+ folder: "taggy",
+ title: "Tagged stuff",
+ });
+ let [, tagFolderId] = /\bfolder=(\d+)\b/.exec(tagQuery.url.pathname);
+ let smartBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ url: "place:folder=TOOLBAR",
+ query: "BookmarksToolbar",
+ title: "Bookmarks toolbar query",
+ });
+
+ do_print("Fetch empty folder with description");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(folder.syncId);
+ deepEqual(item, {
+ syncId: folder.syncId,
+ kind: "folder",
+ parentSyncId: "menu",
+ description: "Folder description",
+ childSyncIds: [folderBmk.syncId, folderSep.syncId],
+ parentTitle: "Bookmarks Menu",
+ title: "",
+ }, "Should include description, children, title, and parent title in folder");
+ }
+
+ do_print("Fetch bookmark with description, sidebar anno, and tags");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(bmk.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "tags", "description", "loadInSidebar", "parentTitle", "title"].sort(),
+ "Should include bookmark-specific properties");
+ equal(item.syncId, bmk.syncId, "Sync ID should match");
+ equal(item.url.href, "https://example.com/", "Should return URL");
+ equal(item.parentSyncId, "menu", "Should return parent sync ID");
+ deepEqual(item.tags, ["taggy"], "Should return tags");
+ equal(item.description, "Bookmark description", "Should return bookmark description");
+ strictEqual(item.loadInSidebar, true, "Should return sidebar anno");
+ equal(item.parentTitle, "Bookmarks Menu", "Should return parent title");
+ strictEqual(item.title, "", "Should return empty title");
+ }
+
+ do_print("Fetch bookmark with keyword; without parent title or annos");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(folderBmk.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "keyword", "tags", "loadInSidebar", "parentTitle", "title"].sort(),
+ "Should omit blank bookmark-specific properties");
+ strictEqual(item.loadInSidebar, false, "Should not load bookmark in sidebar");
+ deepEqual(item.tags, [], "Tags should be empty");
+ equal(item.keyword, "kw", "Should return keyword");
+ strictEqual(item.parentTitle, "", "Should include parent title even if empty");
+ strictEqual(item.title, "", "Should include bookmark title even if empty");
+ }
+
+ do_print("Fetch separator");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(folderSep.syncId);
+ strictEqual(item.index, 1, "Should return separator position");
+ }
+
+ do_print("Fetch tag query");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(tagQuery.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "title", "folder", "parentTitle"].sort(),
+ "Should include query-specific properties");
+ equal(item.url.href, `place:type=7&folder=${tagFolderId}`, "Should not rewrite outgoing tag queries");
+ equal(item.folder, "taggy", "Should return tag name for tag queries");
+ }
+
+ do_print("Fetch smart bookmark");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(smartBmk.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "title", "query", "parentTitle"].sort(),
+ "Should include smart bookmark-specific properties");
+ equal(item.query, "BookmarksToolbar", "Should return query name for smart bookmarks");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_fetch_livemark() {
+ let { site, stopServer } = makeLivemarkServer();
+
+ try {
+ do_print("Create livemark");
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI: uri(site + "/feed/1"),
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+ PlacesUtils.annotations.setItemAnnotation(livemark.id, DESCRIPTION_ANNO,
+ "Livemark description", 0, PlacesUtils.annotations.EXPIRE_NEVER);
+
+ do_print("Fetch livemark");
+ let item = yield PlacesSyncUtils.bookmarks.fetch(livemark.guid);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "description", "feed", "site", "parentTitle", "title"].sort(),
+ "Should include livemark-specific properties");
+ equal(item.description, "Livemark description", "Should return description");
+ equal(item.feed.href, site + "/feed/1", "Should return feed URL");
+ equal(item.site.href, site + "/", "Should return site URL");
+ strictEqual(item.title, "", "Should include livemark title even if empty");
+ } finally {
+ yield stopServer();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js
new file mode 100644
index 0000000000..92930e329e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js
@@ -0,0 +1,137 @@
+/* -*- 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 current_test = 0;
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+function ensure_tag_results(results, searchTerm)
+{
+ var controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["places-tag-autocomplete"]);
+
+ controller.input = input;
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function input_onSearchBegin() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function input_onSearchComplete() {
+ do_check_eq(numSearchesStarted, 1);
+ if (results.length)
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ else
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+
+ do_check_eq(controller.matchCount, results.length);
+ for (var i=0; i<controller.matchCount; i++) {
+ do_check_eq(controller.getValueAt(i), results[i]);
+ }
+
+ if (current_test < (tests.length - 1)) {
+ current_test++;
+ tests[current_test]();
+ }
+ else {
+ // finish once all tests have run
+ do_test_finished();
+ }
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+var uri1 = uri("http://site.tld/1");
+
+var tests = [
+ function test1() { ensure_tag_results(["bar", "Baz", "boo"], "b"); },
+ function test2() { ensure_tag_results(["bar", "Baz"], "ba"); },
+ function test3() { ensure_tag_results(["bar", "Baz"], "Ba"); },
+ function test4() { ensure_tag_results(["bar"], "bar"); },
+ function test5() { ensure_tag_results(["Baz"], "Baz"); },
+ function test6() { ensure_tag_results([], "barb"); },
+ function test7() { ensure_tag_results([], "foo"); },
+ function test8() { ensure_tag_results(["first tag, bar", "first tag, Baz"], "first tag, ba"); },
+ function test9() { ensure_tag_results(["first tag; bar", "first tag; Baz"], "first tag; ba"); }
+];
+
+/**
+ * Test tag autocomplete
+ */
+function run_test() {
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ tagssvc.tagURI(uri1, ["bar", "Baz", "boo", "*nix"]);
+
+ tests[0]();
+}
diff --git a/toolkit/components/places/tests/unit/test_tagging.js b/toolkit/components/places/tests/unit/test_tagging.js
new file mode 100644
index 0000000000..ccb2870502
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tagging.js
@@ -0,0 +1,189 @@
+/* -*- 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/. */
+
+// Notice we use createInstance because later we will have to terminate the
+// service and restart it.
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ createInstance().QueryInterface(Ci.nsITaggingService);
+
+function run_test() {
+ var options = PlacesUtils.history.getNewQueryOptions();
+ var query = PlacesUtils.history.getNewQuery();
+
+ query.setFolders([PlacesUtils.tagsFolderId], 1);
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var tagRoot = result.root;
+ tagRoot.containerOpen = true;
+
+ do_check_eq(tagRoot.childCount, 0);
+
+ var uri1 = uri("http://foo.tld/");
+ var uri2 = uri("https://bar.tld/");
+
+ // this also tests that the multiple folders are not created for the same tag
+ tagssvc.tagURI(uri1, ["tag 1"]);
+ tagssvc.tagURI(uri2, ["tag 1"]);
+ do_check_eq(tagRoot.childCount, 1);
+
+ var tag1node = tagRoot.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ var tag1itemId = tag1node.itemId;
+
+ do_check_eq(tag1node.title, "tag 1");
+ tag1node.containerOpen = true;
+ do_check_eq(tag1node.childCount, 2);
+
+ // Tagging the same url twice (or even thrice!) with the same tag should be a
+ // no-op
+ tagssvc.tagURI(uri1, ["tag 1"]);
+ do_check_eq(tag1node.childCount, 2);
+ tagssvc.tagURI(uri1, [tag1itemId]);
+ do_check_eq(tag1node.childCount, 2);
+ do_check_eq(tagRoot.childCount, 1);
+
+ // also tests bug 407575
+ tagssvc.tagURI(uri1, [tag1itemId, "tag 1", "tag 2", "Tag 1", "Tag 2"]);
+ do_check_eq(tagRoot.childCount, 2);
+ do_check_eq(tag1node.childCount, 2);
+
+ // test getTagsForURI
+ var uri1tags = tagssvc.getTagsForURI(uri1);
+ do_check_eq(uri1tags.length, 2);
+ do_check_eq(uri1tags[0], "Tag 1");
+ do_check_eq(uri1tags[1], "Tag 2");
+ var uri2tags = tagssvc.getTagsForURI(uri2);
+ do_check_eq(uri2tags.length, 1);
+ do_check_eq(uri2tags[0], "Tag 1");
+
+ // test getURIsForTag
+ var tag1uris = tagssvc.getURIsForTag("tag 1");
+ do_check_eq(tag1uris.length, 2);
+ do_check_true(tag1uris[0].equals(uri1));
+ do_check_true(tag1uris[1].equals(uri2));
+
+ // test allTags attribute
+ var allTags = tagssvc.allTags;
+ do_check_eq(allTags.length, 2);
+ do_check_eq(allTags[0], "Tag 1");
+ do_check_eq(allTags[1], "Tag 2");
+
+ // test untagging
+ tagssvc.untagURI(uri1, ["tag 1"]);
+ do_check_eq(tag1node.childCount, 1);
+
+ // removing the last uri from a tag should remove the tag-container
+ tagssvc.untagURI(uri2, ["tag 1"]);
+ do_check_eq(tagRoot.childCount, 1);
+
+ // cleanup
+ tag1node.containerOpen = false;
+
+ // get array of tag folder ids => title
+ // for testing tagging with mixed folder ids and tags
+ var child = tagRoot.getChild(0);
+ var tagId = child.itemId;
+ var tagTitle = child.title;
+
+ // test mixed id/name tagging
+ // as well as non-id numeric tags
+ var uri3 = uri("http://testuri/3");
+ tagssvc.tagURI(uri3, [tagId, "tag 3", "456"]);
+ var tags = tagssvc.getTagsForURI(uri3);
+ do_check_true(tags.includes(tagTitle));
+ do_check_true(tags.includes("tag 3"));
+ do_check_true(tags.includes("456"));
+
+ // test mixed id/name tagging
+ tagssvc.untagURI(uri3, [tagId, "tag 3", "456"]);
+ tags = tagssvc.getTagsForURI(uri3);
+ do_check_eq(tags.length, 0);
+
+ // Terminate tagging service, fire up a new instance and check that existing
+ // tags are there. This will ensure that any internal caching system is
+ // correctly filled at startup and we are not losing previously existing tags.
+ var uri4 = uri("http://testuri/4");
+ tagssvc.tagURI(uri4, [tagId, "tag 3", "456"]);
+ tagssvc = null;
+ tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+ var uri4Tags = tagssvc.getTagsForURI(uri4);
+ do_check_eq(uri4Tags.length, 3);
+ do_check_true(uri4Tags.includes(tagTitle));
+ do_check_true(uri4Tags.includes("tag 3"));
+ do_check_true(uri4Tags.includes("456"));
+
+ // Test sparse arrays.
+ let curChildCount = tagRoot.childCount;
+
+ try {
+ tagssvc.tagURI(uri1, [, "tagSparse"]);
+ do_check_eq(tagRoot.childCount, curChildCount + 1);
+ } catch (ex) {
+ do_throw("Passing a sparse array should not throw");
+ }
+ try {
+ tagssvc.untagURI(uri1, [, "tagSparse"]);
+ do_check_eq(tagRoot.childCount, curChildCount);
+ } catch (ex) {
+ do_throw("Passing a sparse array should not throw");
+ }
+
+ // Test that the API throws for bad arguments.
+ try {
+ tagssvc.tagURI(uri1, ["", "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.untagURI(uri1, ["", "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.tagURI(uri1, [0, "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.tagURI(uri1, [0, "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+
+ // Tag name length should be limited to nsITaggingService.MAX_TAG_LENGTH (bug407821)
+ try {
+
+ // generate a long tag name. i.e. looooo...oong_tag
+ var n = Ci.nsITaggingService.MAX_TAG_LENGTH;
+ var someOos = new Array(n).join('o');
+ var longTagName = "l" + someOos + "ng_tag";
+
+ tagssvc.tagURI(uri1, ["short_tag", longTagName]);
+ do_throw("Passing a bad tags array should throw");
+
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+
+ // cleanup
+ tagRoot.containerOpen = false;
+
+ // Tagging service should trim tags (Bug967196)
+ let exampleURI = uri("http://www.example.com/");
+ PlacesUtils.tagging.tagURI(exampleURI, [ " test " ]);
+
+ let exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
+ do_check_eq(exampleTags.length, 1);
+ do_check_eq(exampleTags[0], "test");
+
+ PlacesUtils.tagging.untagURI(exampleURI, [ "test" ]);
+ exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
+ do_check_eq(exampleTags.length, 0);
+}
diff --git a/toolkit/components/places/tests/unit/test_telemetry.js b/toolkit/components/places/tests/unit/test_telemetry.js
new file mode 100644
index 0000000000..99f36d78c8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests common Places telemetry probes by faking the telemetry service.
+
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+var histograms = {
+ PLACES_PAGES_COUNT: val => do_check_eq(val, 1),
+ PLACES_BOOKMARKS_COUNT: val => do_check_eq(val, 1),
+ PLACES_TAGS_COUNT: val => do_check_eq(val, 1),
+ PLACES_KEYWORDS_COUNT: val => do_check_eq(val, 1),
+ PLACES_SORTED_BOOKMARKS_PERC: val => do_check_eq(val, 100),
+ PLACES_TAGGED_BOOKMARKS_PERC: val => do_check_eq(val, 100),
+ PLACES_DATABASE_FILESIZE_MB: val => do_check_true(val > 0),
+ PLACES_DATABASE_PAGESIZE_B: val => do_check_eq(val, 32768),
+ PLACES_DATABASE_SIZE_PER_PAGE_B: val => do_check_true(val > 0),
+ PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => do_check_true(val > 1),
+ // PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS: val => do_check_true(val > 1),
+ PLACES_IDLE_FRECENCY_DECAY_TIME_MS: val => do_check_true(val >= 0),
+ PLACES_IDLE_MAINTENANCE_TIME_MS: val => do_check_true(val > 0),
+ // One from the `setItemAnnotation` call; the other from the mobile root.
+ // This can be removed along with the anno in bug 1306445.
+ PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 2),
+ PLACES_ANNOS_PAGES_COUNT: val => do_check_eq(val, 1),
+ PLACES_MAINTENANCE_DAYSFROMLAST: val => do_check_true(val >= 0),
+}
+
+/**
+ * Forces an expiration run.
+ *
+ * @param [optional] aLimit
+ * Limit for the expiration. Pass -1 for unlimited.
+ * Any other non-positive value will just expire orphans.
+ *
+ * @return {Promise}
+ * @resolves When expiration finishes.
+ * @rejects Never.
+ */
+function promiseForceExpirationStep(aLimit) {
+ let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver);
+ expire.observe(null, "places-debug-start-expiration", aLimit);
+ return promise;
+}
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * param [optional] daysAgo
+ * Expiration ignores any visit added in the last 7 days, so by default
+ * this will be set to 7.
+ * @note to be safe against DST issues we go back one day more.
+ */
+function getExpirablePRTime(daysAgo = 7) {
+ let dateObj = new Date();
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000);
+ return dateObj.getTime() * 1000;
+}
+
+add_task(function* test_execute()
+{
+ // Put some trash in the database.
+ let uri = NetUtil.newURI("http://moz.org/");
+
+ let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "moz test",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let itemId = PlacesUtils.bookmarks.insertBookmark(folderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "moz test");
+ PlacesUtils.tagging.tagURI(uri, ["tag"]);
+ yield PlacesUtils.keywords.insert({ url: uri.spec, keyword: "keyword"});
+
+ // Set a large annotation.
+ let content = "";
+ while (content.length < 1024) {
+ content += "0";
+ }
+ PlacesUtils.annotations.setItemAnnotation(itemId, "test-anno", content, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setPageAnnotation(uri, "test-anno", content, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Request to gather telemetry data.
+ Cc["@mozilla.org/places/categoriesStarter;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "gather-telemetry", null);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Test expiration probes.
+ let timeInMicroseconds = getExpirablePRTime(8);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ for (let i = 0; i < 3; i++) {
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://" + i + ".moz.org/"),
+ visitDate: newTimeInMicroseconds()
+ });
+ }
+ Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
+ yield promiseForceExpirationStep(2);
+ yield promiseForceExpirationStep(2);
+
+ // Test autocomplete probes.
+ /*
+ // This is useful for manual testing by changing the minimum time for
+ // autocomplete telemetry to 0, but there is no way to artificially delay
+ // autocomplete by more than 50ms in a realiable way.
+ Services.prefs.setIntPref("browser.urlbar.search.sources", 3);
+ Services.prefs.setIntPref("browser.urlbar.default.behavior", 0);
+ function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ }
+ AutoCompleteInput.prototype = {
+ timeout: 10,
+ textValue: "",
+ 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,
+ ])
+ };
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ controller.input = new AutoCompleteInput(["unifiedcomplete"]);
+ controller.startSearch("moz");
+ */
+
+ // Test idle probes.
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver)
+ .observe(null, "idle-daily", null);
+ PlacesDBUtils.maintenanceOnIdle();
+
+ yield promiseTopicObserved("places-maintenance-finished");
+
+ for (let histogramId in histograms) {
+ do_print("checking histogram " + histogramId);
+ let validate = histograms[histogramId];
+ let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+ validate(snapshot.sum);
+ do_check_true(snapshot.counts.reduce((a, b) => a + b) > 0);
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js
new file mode 100644
index 0000000000..662ea0841f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js
@@ -0,0 +1,151 @@
+/* -*- 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/. */
+
+/**
+ * Bug 455315
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=412132
+ *
+ * Ensures that the frecency of a bookmark's URI is what it should be after the
+ * bookmark is deleted.
+ */
+
+add_task(function* removed_bookmark() {
+ do_print("After removing bookmark, frecency of bookmark's URI should be " +
+ "zero if URI is unvisited and no longer bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.remove(bm);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Unvisited URI no longer bookmarked => frecency should = 0");
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* removed_but_visited_bookmark() {
+ do_print("After removing bookmark, frecency of bookmark's URI should " +
+ "not be zero if URI is visited.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ yield PlacesUtils.bookmarks.remove(bm);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("*Visited* URI no longer bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* remove_bookmark_still_bookmarked() {
+ do_print("After removing bookmark, frecency of bookmark's URI should " +
+ "not be zero if URI is still bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let bm1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 1 title",
+ url: TEST_URI
+ });
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 2 title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.remove(bm1);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("URI still bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* cleared_parent_of_visited_bookmark() {
+ do_print("After removing all children from bookmark's parent, frecency " +
+ "of bookmark's URI should not be zero if URI is visited.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("*Visited* URI no longer bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* cleared_parent_of_bookmark_still_bookmarked() {
+ do_print("After removing all children from bookmark's parent, frecency " +
+ "of bookmark's URI should not be zero if URI is still " +
+ "bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "bookmark 1 title",
+ url: TEST_URI
+ });
+
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bookmark 2 folder"
+ });
+ yield PlacesUtils.bookmarks.insert({
+ title: "bookmark 2 title",
+ parentGuid: folder.guid,
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.remove(folder);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ // URI still bookmarked => frecency should != 0.
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_backups_create.js b/toolkit/components/places/tests/unit/test_utils_backups_create.js
new file mode 100644
index 0000000000..a30589c44f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js
@@ -0,0 +1,90 @@
+/* -*- 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/. */
+
+ /**
+ * Check for correct functionality of bookmarks backups
+ */
+
+const NUMBER_OF_BACKUPS = 10;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ // Generate random dates.
+ let dateObj = new Date();
+ let dates = [];
+ while (dates.length < NUMBER_OF_BACKUPS) {
+ // Use last year to ensure today's backup is the newest.
+ let randomDate = new Date(dateObj.getFullYear() - 1,
+ Math.floor(12 * Math.random()),
+ Math.floor(28 * Math.random()));
+ if (!dates.includes(randomDate.getTime()))
+ dates.push(randomDate.getTime());
+ }
+ // Sort dates from oldest to newest.
+ dates.sort();
+
+ // Get and cleanup the backups folder.
+ let backupFolderPath = yield PlacesBackups.getBackupFolder();
+ let bookmarksBackupDir = new FileUtils.File(backupFolderPath);
+
+ // Fake backups are created backwards to ensure we won't consider file
+ // creation time.
+ // Create fake backups for the newest dates.
+ for (let i = dates.length - 1; i >= 0; i--) {
+ let backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i]));
+ let backupFile = bookmarksBackupDir.clone();
+ backupFile.append(backupFilename);
+ backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
+ do_print("Creating fake backup " + backupFile.leafName);
+ if (!backupFile.exists())
+ do_throw("Unable to create fake backup " + backupFile.leafName);
+ }
+
+ yield PlacesBackups.create(NUMBER_OF_BACKUPS);
+ // Add today's backup.
+ dates.push(dateObj.getTime());
+
+ // Check backups. We have 11 dates but we the max number is 10 so the
+ // oldest backup should have been removed.
+ for (let i = 0; i < dates.length; i++) {
+ let backupFilename;
+ let shouldExist;
+ let backupFile;
+ if (i > 0) {
+ let files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.getNext().QueryInterface(Ci.nsIFile);
+ if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
+ backupFilename = entry.leafName;
+ backupFile = entry;
+ break;
+ }
+ }
+ shouldExist = true;
+ }
+ else {
+ backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i]));
+ backupFile = bookmarksBackupDir.clone();
+ backupFile.append(backupFilename);
+ shouldExist = false;
+ }
+ if (backupFile.exists() != shouldExist)
+ do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename);
+ }
+
+ // Cleanup backups folder.
+ // XXX: Can't use bookmarksBackupDir.remove(true) because file lock happens
+ // on WIN XP.
+ let files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.getNext().QueryInterface(Ci.nsIFile);
+ entry.remove(false);
+ }
+ do_check_false(bookmarksBackupDir.directoryEntries.hasMoreElements());
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js
new file mode 100644
index 0000000000..ecebce94a6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js
@@ -0,0 +1,180 @@
+/* -*- 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/. */
+
+ /**
+ * Check for correct functionality of PlacesUtils.getURLsForContainerNode and
+ * PlacesUtils.hasChildURIs (those helpers share almost all of their code)
+ */
+
+var PU = PlacesUtils;
+var hs = PU.history;
+var bs = PU.bookmarks;
+
+var tests = [
+
+function() {
+ dump("\n\n*** TEST: folder\n");
+ // This is the folder we will check for children.
+ var folderId = bs.createFolder(bs.toolbarFolder, "folder", bs.DEFAULT_INDEX);
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(folderId, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(folderId, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(folderId, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: folder in an excludeItems root\n");
+ // This is the folder we will check for children.
+ var folderId = bs.createFolder(bs.toolbarFolder, "folder", bs.DEFAULT_INDEX);
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(folderId, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(folderId, uri("place:sort=1"), bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+ options.excludeItems = true;
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(folderId, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: query\n");
+ // This is the query we will check for children.
+ bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+
+ dump("Check query without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check query with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: excludeItems Query\n");
+ // This is the query we will check for children.
+ bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=8"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+ options.excludeItems = true;
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: !expandQueries Query\n");
+ // This is the query we will check for children.
+ bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=8"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+ options.expandQueries = false;
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+}
+
+];
+
+/**
+ * Executes a query and checks number of uri nodes in the first container in
+ * query's results. To correctly test a container ensure that the query will
+ * return only your container in the first level.
+ *
+ * @param aQuery
+ * nsINavHistoryQuery object defining the query
+ * @param aOptions
+ * nsINavHistoryQueryOptions object defining the query's options
+ * @param aExpectedURINodes
+ * number of expected uri nodes
+ */
+function check_uri_nodes(aQuery, aOptions, aExpectedURINodes) {
+ var result = hs.executeQuery(aQuery, aOptions);
+ var root = result.root;
+ root.containerOpen = true;
+ var node = root.getChild(0);
+ do_check_eq(PU.hasChildURIs(node), aExpectedURINodes > 0);
+ do_check_eq(PU.getURLsForContainerNode(node).length, aExpectedURINodes);
+ root.containerOpen = false;
+}
+
+add_task(function* () {
+ for (let test of tests) {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ test();
+ }
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js b/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js
new file mode 100644
index 0000000000..62947620d5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js
@@ -0,0 +1,79 @@
+/* -*- 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/. */
+
+ /**
+ * Check for correct functionality of PlacesUtils.setAnnotationsForItem/URI
+ */
+
+var hs = PlacesUtils.history;
+var bs = PlacesUtils.bookmarks;
+var as = PlacesUtils.annotations;
+
+const TEST_URL = "http://test.mozilla.org/";
+
+function run_test() {
+ var testURI = uri(TEST_URL);
+ // add a bookmark
+ var itemId = bs.insertBookmark(bs.unfiledBookmarksFolder, testURI,
+ bs.DEFAULT_INDEX, "test");
+
+ // create annotations array
+ var testAnnos = [{ name: "testAnno/test0",
+ flags: 0,
+ value: "test0",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER },
+ { name: "testAnno/test1",
+ flags: 0,
+ value: "test1",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER },
+ { name: "testAnno/test2",
+ flags: 0,
+ value: "test2",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER },
+ { name: "testAnno/test3",
+ flags: 0,
+ value: 0,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+
+ // Add item annotations
+ PlacesUtils.setAnnotationsForItem(itemId, testAnnos);
+ // Check for correct addition
+ testAnnos.forEach(function(anno) {
+ do_check_true(as.itemHasAnnotation(itemId, anno.name));
+ do_check_eq(as.getItemAnnotation(itemId, anno.name), anno.value);
+ });
+
+ // Add page annotations
+ PlacesUtils.setAnnotationsForURI(testURI, testAnnos);
+ // Check for correct addition
+ testAnnos.forEach(function(anno) {
+ do_check_true(as.pageHasAnnotation(testURI, anno.name));
+ do_check_eq(as.getPageAnnotation(testURI, anno.name), anno.value);
+ });
+
+ // To unset annotations we unset their values or set them to
+ // null/undefined
+ testAnnos[0].value = null;
+ testAnnos[1].value = undefined;
+ delete testAnnos[2].value;
+ delete testAnnos[3].value;
+
+ // Unset all item annotations
+ PlacesUtils.setAnnotationsForItem(itemId, testAnnos);
+ // Check for correct removal
+ testAnnos.forEach(function(anno) {
+ do_check_false(as.itemHasAnnotation(itemId, anno.name));
+ // sanity: page annotations should not be removed here
+ do_check_true(as.pageHasAnnotation(testURI, anno.name));
+ });
+
+ // Unset all page annotations
+ PlacesUtils.setAnnotationsForURI(testURI, testAnnos);
+ // Check for correct removal
+ testAnnos.forEach(function(anno) {
+ do_check_false(as.pageHasAnnotation(testURI, anno.name));
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_visitsInDB.js b/toolkit/components/places/tests/unit/test_visitsInDB.js
new file mode 100644
index 0000000000..3cab39ed98
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_visitsInDB.js
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+add_task(function* test_execute() {
+ const TEST_URI = uri("http://mozilla.com");
+
+ do_check_eq(0, yield PlacesTestUtils.visitsInDB(TEST_URI));
+ yield PlacesTestUtils.addVisits({uri: TEST_URI});
+ do_check_eq(1, yield PlacesTestUtils.visitsInDB(TEST_URI));
+ yield PlacesTestUtils.addVisits({uri: TEST_URI});
+ do_check_eq(2, yield PlacesTestUtils.visitsInDB(TEST_URI));
+});
diff --git a/toolkit/components/places/tests/unit/xpcshell.ini b/toolkit/components/places/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..60bba47582
--- /dev/null
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -0,0 +1,163 @@
+[DEFAULT]
+head = head_bookmarks.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ bookmarks.corrupt.html
+ bookmarks.json
+ bookmarks.preplaces.html
+ bookmarks_html_singleframe.html
+ bug476292.sqlite
+ corruptDB.sqlite
+ default.sqlite
+ livemark.xml
+ mobile_bookmarks_folder_import.json
+ mobile_bookmarks_folder_merge.json
+ mobile_bookmarks_multiple_folders.json
+ mobile_bookmarks_root_import.json
+ mobile_bookmarks_root_merge.json
+ nsDummyObserver.js
+ nsDummyObserver.manifest
+ places.sparse.sqlite
+
+[test_000_frecency.js]
+[test_317472.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_331487.js]
+[test_384370.js]
+[test_385397.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_399264_query_to_string.js]
+[test_399264_string_to_query.js]
+[test_399266.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+# Bug 821781: test fails intermittently on Linux
+skip-if = os == "linux"
+[test_402799.js]
+[test_405497.js]
+[test_408221.js]
+[test_412132.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_413784.js]
+[test_415460.js]
+[test_415757.js]
+[test_418643_removeFolderChildren.js]
+[test_419731.js]
+[test_419792_node_tags_property.js]
+[test_425563.js]
+[test_429505_remove_shortcuts.js]
+[test_433317_query_title_update.js]
+[test_433525_hasChildren_crash.js]
+[test_452777.js]
+[test_454977.js]
+[test_463863.js]
+[test_485442_crash_bug_nsNavHistoryQuery_GetUri.js]
+[test_486978_sort_by_date_queries.js]
+[test_536081.js]
+[test_1085291.js]
+[test_1105208.js]
+[test_1105866.js]
+[test_adaptive.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_adaptive_bug527311.js]
+[test_analyze.js]
+[test_annotations.js]
+[test_asyncExecuteLegacyQueries.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_async_history_api.js]
+[test_async_in_batchmode.js]
+[test_async_transactions.js]
+skip-if = (os == "win" && os_version == "5.1") # Bug 1158887
+[test_autocomplete_stopSearch_no_throw.js]
+[test_bookmark_catobs.js]
+[test_bookmarks_json.js]
+[test_bookmarks_html.js]
+[test_bookmarks_html_corrupt.js]
+[test_bookmarks_html_import_tags.js]
+[test_bookmarks_html_singleframe.js]
+[test_bookmarks_restore_notification.js]
+[test_bookmarks_setNullTitle.js]
+[test_broken_folderShortcut_result.js]
+[test_browserhistory.js]
+[test_bug636917_isLivemark.js]
+[test_childlessTags.js]
+[test_corrupt_telemetry.js]
+[test_crash_476292.js]
+[test_database_replaceOnStartup.js]
+[test_download_history.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_frecency.js]
+[test_frecency_zero_updated.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_getChildIndex.js]
+[test_getPlacesInfo.js]
+[test_history.js]
+[test_history_autocomplete_tags.js]
+[test_history_catobs.js]
+[test_history_clear.js]
+[test_history_notifications.js]
+[test_history_observer.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_history_sidebar.js]
+[test_hosts_triggers.js]
+[test_import_mobile_bookmarks.js]
+[test_isPageInDB.js]
+[test_isURIVisited.js]
+[test_isvisited.js]
+[test_keywords.js]
+[test_lastModified.js]
+[test_markpageas.js]
+[test_mozIAsyncLivemarks.js]
+[test_multi_queries.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_multi_word_tags.js]
+[test_nsINavHistoryViewer.js]
+# Bug 902248: intermittent timeouts on all platforms
+skip-if = true
+[test_null_interfaces.js]
+[test_onItemChanged_tags.js]
+[test_pageGuid_bookmarkGuid.js]
+[test_frecency_observers.js]
+[test_placeURIs.js]
+[test_PlacesSearchAutocompleteProvider.js]
+[test_PlacesUtils_asyncGetBookmarkIds.js]
+[test_PlacesUtils_invalidateCachedGuidFor.js]
+[test_PlacesUtils_lazyobservers.js]
+[test_placesTxn.js]
+[test_preventive_maintenance.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_preventive_maintenance_checkAndFixDatabase.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_preventive_maintenance_runTasks.js]
+[test_promiseBookmarksTree.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_resolveNullBookmarkTitles.js]
+[test_result_sort.js]
+[test_resultsAsVisit_details.js]
+[test_sql_guid_functions.js]
+[test_svg_favicon.js]
+[test_sync_utils.js]
+[test_tag_autocomplete_search.js]
+[test_tagging.js]
+[test_telemetry.js]
+[test_update_frecency_after_delete.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_utils_backups_create.js]
+[test_utils_getURLsForContainerNode.js]
+[test_utils_setAnnotationsFor.js]
+[test_visitsInDB.js]
diff --git a/toolkit/components/places/toolkitplaces.manifest b/toolkit/components/places/toolkitplaces.manifest
new file mode 100644
index 0000000000..cd9665200e
--- /dev/null
+++ b/toolkit/components/places/toolkitplaces.manifest
@@ -0,0 +1,32 @@
+# nsLivemarkService.js
+component {dca61eb5-c7cd-4df1-b0fb-d0722baba251} nsLivemarkService.js
+contract @mozilla.org/browser/livemark-service;2 {dca61eb5-c7cd-4df1-b0fb-d0722baba251}
+
+# nsTaggingService.js
+component {bbc23860-2553-479d-8b78-94d9038334f7} nsTaggingService.js
+contract @mozilla.org/browser/tagging-service;1 {bbc23860-2553-479d-8b78-94d9038334f7}
+component {1dcc23b0-d4cb-11dc-9ad6-479d56d89593} nsTaggingService.js
+contract @mozilla.org/autocomplete/search;1?name=places-tag-autocomplete {1dcc23b0-d4cb-11dc-9ad6-479d56d89593}
+
+# nsPlacesExpiration.js
+component {705a423f-2f69-42f3-b9fe-1517e0dee56f} nsPlacesExpiration.js
+contract @mozilla.org/places/expiration;1 {705a423f-2f69-42f3-b9fe-1517e0dee56f}
+category history-observers nsPlacesExpiration @mozilla.org/places/expiration;1
+
+# PlacesCategoriesStarter.js
+component {803938d5-e26d-4453-bf46-ad4b26e41114} PlacesCategoriesStarter.js
+contract @mozilla.org/places/categoriesStarter;1 {803938d5-e26d-4453-bf46-ad4b26e41114}
+category idle-daily PlacesCategoriesStarter @mozilla.org/places/categoriesStarter;1
+category bookmark-observers PlacesCategoriesStarter @mozilla.org/places/categoriesStarter;1
+
+# ColorAnalyzer.js
+component {d056186c-28a0-494e-aacc-9e433772b143} ColorAnalyzer.js
+contract @mozilla.org/places/colorAnalyzer;1 {d056186c-28a0-494e-aacc-9e433772b143}
+
+# UnifiedComplete.js
+component {f964a319-397a-4d21-8be6-5cdd1ee3e3ae} UnifiedComplete.js
+contract @mozilla.org/autocomplete/search;1?name=unifiedcomplete {f964a319-397a-4d21-8be6-5cdd1ee3e3ae}
+
+# PageIconProtocolHandler.js
+component {60a1f7c6-4ff9-4a42-84d3-5a185faa6f32} PageIconProtocolHandler.js
+contract @mozilla.org/network/protocol;1?name=page-icon {60a1f7c6-4ff9-4a42-84d3-5a185faa6f32}
diff --git a/toolkit/components/printing/content/printPageSetup.js b/toolkit/components/printing/content/printPageSetup.js
new file mode 100644
index 0000000000..31eb1bdd34
--- /dev/null
+++ b/toolkit/components/printing/content/printPageSetup.js
@@ -0,0 +1,478 @@
+// -*- 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 gDialog;
+var paramBlock;
+var gPrefs = null;
+var gPrintService = null;
+var gPrintSettings = null;
+var gStringBundle = null;
+var gDoingMetric = false;
+
+var gPrintSettingsInterface = Components.interfaces.nsIPrintSettings;
+var gDoDebug = false;
+
+// ---------------------------------------------------
+function initDialog()
+{
+ gDialog = {};
+
+ gDialog.orientation = document.getElementById("orientation");
+ gDialog.portrait = document.getElementById("portrait");
+ gDialog.landscape = document.getElementById("landscape");
+
+ gDialog.printBG = document.getElementById("printBG");
+
+ gDialog.shrinkToFit = document.getElementById("shrinkToFit");
+
+ gDialog.marginGroup = document.getElementById("marginGroup");
+
+ gDialog.marginPage = document.getElementById("marginPage");
+ gDialog.marginTop = document.getElementById("marginTop");
+ gDialog.marginBottom = document.getElementById("marginBottom");
+ gDialog.marginLeft = document.getElementById("marginLeft");
+ gDialog.marginRight = document.getElementById("marginRight");
+
+ gDialog.topInput = document.getElementById("topInput");
+ gDialog.bottomInput = document.getElementById("bottomInput");
+ gDialog.leftInput = document.getElementById("leftInput");
+ gDialog.rightInput = document.getElementById("rightInput");
+
+ gDialog.hLeftOption = document.getElementById("hLeftOption");
+ gDialog.hCenterOption = document.getElementById("hCenterOption");
+ gDialog.hRightOption = document.getElementById("hRightOption");
+
+ gDialog.fLeftOption = document.getElementById("fLeftOption");
+ gDialog.fCenterOption = document.getElementById("fCenterOption");
+ gDialog.fRightOption = document.getElementById("fRightOption");
+
+ gDialog.scalingLabel = document.getElementById("scalingInput");
+ gDialog.scalingInput = document.getElementById("scalingInput");
+
+ gDialog.enabled = false;
+
+ gDialog.strings = new Array;
+ gDialog.strings["marginUnits.inches"] = document.getElementById("marginUnits.inches").childNodes[0].nodeValue;
+ gDialog.strings["marginUnits.metric"] = document.getElementById("marginUnits.metric").childNodes[0].nodeValue;
+ gDialog.strings["customPrompt.title"] = document.getElementById("customPrompt.title").childNodes[0].nodeValue;
+ gDialog.strings["customPrompt.prompt"] = document.getElementById("customPrompt.prompt").childNodes[0].nodeValue;
+
+}
+
+// ---------------------------------------------------
+function isListOfPrinterFeaturesAvailable()
+{
+ var has_printerfeatures = false;
+
+ try {
+ has_printerfeatures = gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".has_special_printerfeatures");
+ } catch (ex) {
+ }
+
+ return has_printerfeatures;
+}
+
+// ---------------------------------------------------
+function checkDouble(element)
+{
+ element.value = element.value.replace(/[^.0-9]/g, "");
+}
+
+// Theoretical paper width/height.
+var gPageWidth = 8.5;
+var gPageHeight = 11.0;
+
+// ---------------------------------------------------
+function setOrientation()
+{
+ var selection = gDialog.orientation.selectedItem;
+
+ var style = "background-color:white;";
+ if ((selection == gDialog.portrait && gPageWidth > gPageHeight) ||
+ (selection == gDialog.landscape && gPageWidth < gPageHeight)) {
+ // Swap width/height.
+ var temp = gPageHeight;
+ gPageHeight = gPageWidth;
+ gPageWidth = temp;
+ }
+ var div = gDoingMetric ? 100 : 10;
+ style += "width:" + gPageWidth/div + unitString() + ";height:" + gPageHeight/div + unitString() + ";";
+ gDialog.marginPage.setAttribute( "style", style );
+}
+
+// ---------------------------------------------------
+function unitString()
+{
+ return (gPrintSettings.paperSizeUnit == gPrintSettingsInterface.kPaperSizeInches) ? "in" : "mm";
+}
+
+// ---------------------------------------------------
+function checkMargin( value, max, other )
+{
+ // Don't draw this margin bigger than permitted.
+ return Math.min(value, max - other.value);
+}
+
+// ---------------------------------------------------
+function changeMargin( node )
+{
+ // Correct invalid input.
+ checkDouble(node);
+
+ // Reset the margin height/width for this node.
+ var val = node.value;
+ var nodeToStyle;
+ var attr="width";
+ if ( node == gDialog.topInput ) {
+ nodeToStyle = gDialog.marginTop;
+ val = checkMargin( val, gPageHeight, gDialog.bottomInput );
+ attr = "height";
+ } else if ( node == gDialog.bottomInput ) {
+ nodeToStyle = gDialog.marginBottom;
+ val = checkMargin( val, gPageHeight, gDialog.topInput );
+ attr = "height";
+ } else if ( node == gDialog.leftInput ) {
+ nodeToStyle = gDialog.marginLeft;
+ val = checkMargin( val, gPageWidth, gDialog.rightInput );
+ } else {
+ nodeToStyle = gDialog.marginRight;
+ val = checkMargin( val, gPageWidth, gDialog.leftInput );
+ }
+ var style = attr + ":" + (val/10) + unitString() + ";";
+ nodeToStyle.setAttribute( "style", style );
+}
+
+// ---------------------------------------------------
+function changeMargins()
+{
+ changeMargin( gDialog.topInput );
+ changeMargin( gDialog.bottomInput );
+ changeMargin( gDialog.leftInput );
+ changeMargin( gDialog.rightInput );
+}
+
+// ---------------------------------------------------
+function customize( node )
+{
+ // If selection is now "Custom..." then prompt user for custom setting.
+ if ( node.value == 6 ) {
+ var prompter = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService( Components.interfaces.nsIPromptService );
+ var title = gDialog.strings["customPrompt.title"];
+ var promptText = gDialog.strings["customPrompt.prompt"];
+ var result = { value: node.custom };
+ var ok = prompter.prompt(window, title, promptText, result, null, { value: false } );
+ if ( ok ) {
+ node.custom = result.value;
+ }
+ }
+}
+
+// ---------------------------------------------------
+function setHeaderFooter( node, value )
+{
+ node.value= hfValueToId(value);
+ if (node.value == 6) {
+ // Remember current Custom... value.
+ node.custom = value;
+ } else {
+ // Start with empty Custom... value.
+ node.custom = "";
+ }
+}
+
+var gHFValues = new Array;
+gHFValues["&T"] = 1;
+gHFValues["&U"] = 2;
+gHFValues["&D"] = 3;
+gHFValues["&P"] = 4;
+gHFValues["&PT"] = 5;
+
+function hfValueToId(val)
+{
+ if ( val in gHFValues ) {
+ return gHFValues[val];
+ }
+ if ( val.length ) {
+ return 6; // Custom...
+ }
+ return 0; // --blank--
+}
+
+function hfIdToValue(node)
+{
+ var result = "";
+ switch ( parseInt( node.value ) ) {
+ case 0:
+ break;
+ case 1:
+ result = "&T";
+ break;
+ case 2:
+ result = "&U";
+ break;
+ case 3:
+ result = "&D";
+ break;
+ case 4:
+ result = "&P";
+ break;
+ case 5:
+ result = "&PT";
+ break;
+ case 6:
+ result = node.custom;
+ break;
+ }
+ return result;
+}
+
+function setPrinterDefaultsForSelectedPrinter()
+{
+ if (gPrintSettings.printerName == "") {
+ gPrintSettings.printerName = gPrintService.defaultPrinterName;
+ }
+
+ // First get any defaults from the printer
+ gPrintService.initPrintSettingsFromPrinter(gPrintSettings.printerName, gPrintSettings);
+
+ // now augment them with any values from last time
+ gPrintService.initPrintSettingsFromPrefs(gPrintSettings, true, gPrintSettingsInterface.kInitSaveAll);
+
+ if (gDoDebug) {
+ dump("pagesetup/setPrinterDefaultsForSelectedPrinter: printerName='"+gPrintSettings.printerName+"', orientation='"+gPrintSettings.orientation+"'\n");
+ }
+}
+
+// ---------------------------------------------------
+function loadDialog()
+{
+ var print_orientation = 0;
+ var print_margin_top = 0.5;
+ var print_margin_left = 0.5;
+ var print_margin_bottom = 0.5;
+ var print_margin_right = 0.5;
+
+ try {
+ gPrefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+
+ gPrintService = Components.classes["@mozilla.org/gfx/printsettings-service;1"];
+ if (gPrintService) {
+ gPrintService = gPrintService.getService();
+ if (gPrintService) {
+ gPrintService = gPrintService.QueryInterface(Components.interfaces.nsIPrintSettingsService);
+ }
+ }
+ } catch (ex) {
+ dump("loadDialog: ex="+ex+"\n");
+ }
+
+ setPrinterDefaultsForSelectedPrinter();
+
+ gDialog.printBG.checked = gPrintSettings.printBGColors || gPrintSettings.printBGImages;
+
+ gDialog.shrinkToFit.checked = gPrintSettings.shrinkToFit;
+
+ gDialog.scalingLabel.disabled = gDialog.scalingInput.disabled = gDialog.shrinkToFit.checked;
+
+ var marginGroupLabel = gDialog.marginGroup.label;
+ if (gPrintSettings.paperSizeUnit == gPrintSettingsInterface.kPaperSizeInches) {
+ marginGroupLabel = marginGroupLabel.replace(/#1/, gDialog.strings["marginUnits.inches"]);
+ gDoingMetric = false;
+ } else {
+ marginGroupLabel = marginGroupLabel.replace(/#1/, gDialog.strings["marginUnits.metric"]);
+ // Also, set global page dimensions for A4 paper, in millimeters (assumes portrait at this point).
+ gPageWidth = 2100;
+ gPageHeight = 2970;
+ gDoingMetric = true;
+ }
+ gDialog.marginGroup.label = marginGroupLabel;
+
+ print_orientation = gPrintSettings.orientation;
+ print_margin_top = convertMarginInchesToUnits(gPrintSettings.marginTop, gDoingMetric);
+ print_margin_left = convertMarginInchesToUnits(gPrintSettings.marginLeft, gDoingMetric);
+ print_margin_right = convertMarginInchesToUnits(gPrintSettings.marginRight, gDoingMetric);
+ print_margin_bottom = convertMarginInchesToUnits(gPrintSettings.marginBottom, gDoingMetric);
+
+ if (gDoDebug) {
+ dump("print_orientation "+print_orientation+"\n");
+
+ dump("print_margin_top "+print_margin_top+"\n");
+ dump("print_margin_left "+print_margin_left+"\n");
+ dump("print_margin_right "+print_margin_right+"\n");
+ dump("print_margin_bottom "+print_margin_bottom+"\n");
+ }
+
+ if (print_orientation == gPrintSettingsInterface.kPortraitOrientation) {
+ gDialog.orientation.selectedItem = gDialog.portrait;
+ } else if (print_orientation == gPrintSettingsInterface.kLandscapeOrientation) {
+ gDialog.orientation.selectedItem = gDialog.landscape;
+ }
+
+ // Set orientation the first time on a timeout so the dialog sizes to the
+ // maximum height specified in the .xul file. Otherwise, if the user switches
+ // from landscape to portrait, the content grows and the buttons are clipped.
+ setTimeout( setOrientation, 0 );
+
+ gDialog.topInput.value = print_margin_top.toFixed(1);
+ gDialog.bottomInput.value = print_margin_bottom.toFixed(1);
+ gDialog.leftInput.value = print_margin_left.toFixed(1);
+ gDialog.rightInput.value = print_margin_right.toFixed(1);
+ changeMargins();
+
+ setHeaderFooter( gDialog.hLeftOption, gPrintSettings.headerStrLeft );
+ setHeaderFooter( gDialog.hCenterOption, gPrintSettings.headerStrCenter );
+ setHeaderFooter( gDialog.hRightOption, gPrintSettings.headerStrRight );
+
+ setHeaderFooter( gDialog.fLeftOption, gPrintSettings.footerStrLeft );
+ setHeaderFooter( gDialog.fCenterOption, gPrintSettings.footerStrCenter );
+ setHeaderFooter( gDialog.fRightOption, gPrintSettings.footerStrRight );
+
+ gDialog.scalingInput.value = (gPrintSettings.scaling * 100).toFixed(0);
+
+ // Enable/disable widgets based in the information whether the selected
+ // printer supports the matching feature or not
+ if (isListOfPrinterFeaturesAvailable()) {
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".can_change_orientation"))
+ gDialog.orientation.removeAttribute("disabled");
+ else
+ gDialog.orientation.setAttribute("disabled", "true");
+ }
+
+ // Give initial focus to the orientation radio group.
+ // Done on a timeout due to to bug 103197.
+ setTimeout( function() { gDialog.orientation.focus(); }, 0 );
+}
+
+// ---------------------------------------------------
+function onLoad()
+{
+ // Init gDialog.
+ initDialog();
+
+ if (window.arguments[0] != null) {
+ gPrintSettings = window.arguments[0].QueryInterface(Components.interfaces.nsIPrintSettings);
+ paramBlock = window.arguments[1].QueryInterface(Components.interfaces.nsIDialogParamBlock);
+ } else if (gDoDebug) {
+ alert("window.arguments[0] == null!");
+ }
+
+ // default return value is "cancel"
+ paramBlock.SetInt(0, 0);
+
+ if (gPrintSettings) {
+ loadDialog();
+ } else if (gDoDebug) {
+ alert("Could initialize gDialog, PrintSettings is null!");
+ }
+}
+
+function convertUnitsMarginToInches(aVal, aIsMetric)
+{
+ if (aIsMetric) {
+ return aVal / 25.4;
+ }
+ return aVal;
+}
+
+function convertMarginInchesToUnits(aVal, aIsMetric)
+{
+ if (aIsMetric) {
+ return aVal * 25.4;
+ }
+ return aVal;
+}
+
+// ---------------------------------------------------
+function onAccept()
+{
+
+ if (gPrintSettings) {
+ if ( gDialog.orientation.selectedItem == gDialog.portrait ) {
+ gPrintSettings.orientation = gPrintSettingsInterface.kPortraitOrientation;
+ } else {
+ gPrintSettings.orientation = gPrintSettingsInterface.kLandscapeOrientation;
+ }
+
+ // save these out so they can be picked up by the device spec
+ gPrintSettings.marginTop = convertUnitsMarginToInches(gDialog.topInput.value, gDoingMetric);
+ gPrintSettings.marginLeft = convertUnitsMarginToInches(gDialog.leftInput.value, gDoingMetric);
+ gPrintSettings.marginBottom = convertUnitsMarginToInches(gDialog.bottomInput.value, gDoingMetric);
+ gPrintSettings.marginRight = convertUnitsMarginToInches(gDialog.rightInput.value, gDoingMetric);
+
+ gPrintSettings.headerStrLeft = hfIdToValue(gDialog.hLeftOption);
+ gPrintSettings.headerStrCenter = hfIdToValue(gDialog.hCenterOption);
+ gPrintSettings.headerStrRight = hfIdToValue(gDialog.hRightOption);
+
+ gPrintSettings.footerStrLeft = hfIdToValue(gDialog.fLeftOption);
+ gPrintSettings.footerStrCenter = hfIdToValue(gDialog.fCenterOption);
+ gPrintSettings.footerStrRight = hfIdToValue(gDialog.fRightOption);
+
+ gPrintSettings.printBGColors = gDialog.printBG.checked;
+ gPrintSettings.printBGImages = gDialog.printBG.checked;
+
+ gPrintSettings.shrinkToFit = gDialog.shrinkToFit.checked;
+
+ var scaling = document.getElementById("scalingInput").value;
+ if (scaling < 10.0) {
+ scaling = 10.0;
+ }
+ if (scaling > 500.0) {
+ scaling = 500.0;
+ }
+ scaling /= 100.0;
+ gPrintSettings.scaling = scaling;
+
+ if (gDoDebug) {
+ dump("******* Page Setup Accepting ******\n");
+ dump("print_margin_top "+gDialog.topInput.value+"\n");
+ dump("print_margin_left "+gDialog.leftInput.value+"\n");
+ dump("print_margin_right "+gDialog.bottomInput.value+"\n");
+ dump("print_margin_bottom "+gDialog.rightInput.value+"\n");
+ }
+ }
+
+ // set return value to "ok"
+ if (paramBlock) {
+ paramBlock.SetInt(0, 1);
+ } else {
+ dump("*** FATAL ERROR: No paramBlock\n");
+ }
+
+ var flags = gPrintSettingsInterface.kInitSaveMargins |
+ gPrintSettingsInterface.kInitSaveHeaderLeft |
+ gPrintSettingsInterface.kInitSaveHeaderCenter |
+ gPrintSettingsInterface.kInitSaveHeaderRight |
+ gPrintSettingsInterface.kInitSaveFooterLeft |
+ gPrintSettingsInterface.kInitSaveFooterCenter |
+ gPrintSettingsInterface.kInitSaveFooterRight |
+ gPrintSettingsInterface.kInitSaveBGColors |
+ gPrintSettingsInterface.kInitSaveBGImages |
+ gPrintSettingsInterface.kInitSaveInColor |
+ gPrintSettingsInterface.kInitSaveReversed |
+ gPrintSettingsInterface.kInitSaveOrientation |
+ gPrintSettingsInterface.kInitSaveOddEvenPages |
+ gPrintSettingsInterface.kInitSaveShrinkToFit |
+ gPrintSettingsInterface.kInitSaveScaling;
+
+ gPrintService.savePrintSettingsToPrefs(gPrintSettings, true, flags);
+
+ return true;
+}
+
+// ---------------------------------------------------
+function onCancel()
+{
+ // set return value to "cancel"
+ if (paramBlock) {
+ paramBlock.SetInt(0, 0);
+ } else {
+ dump("*** FATAL ERROR: No paramBlock\n");
+ }
+
+ return true;
+}
+
diff --git a/toolkit/components/printing/content/printPageSetup.xul b/toolkit/components/printing/content/printPageSetup.xul
new file mode 100644
index 0000000000..a0c3afe174
--- /dev/null
+++ b/toolkit/components/printing/content/printPageSetup.xul
@@ -0,0 +1,234 @@
+<?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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/printPageSetup.css" type="text/css"?>
+<!DOCTYPE dialog SYSTEM "chrome://global/locale/printPageSetup.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="printPageSetupDialog"
+ onload="onLoad();"
+ ondialogaccept="return onAccept();"
+ oncancel="return onCancel();"
+ title="&printSetup.title;"
+ persist="screenX screenY"
+ screenX="24" screenY="24">
+
+ <script type="application/javascript" src="chrome://global/content/printPageSetup.js"/>
+
+ <!-- Localizable strings manipulated at run-time. -->
+ <data id="marginUnits.inches">&marginUnits.inches;</data>
+ <data id="marginUnits.metric">&marginUnits.metric;</data>
+ <data id="customPrompt.title">&customPrompt.title;</data>
+ <data id="customPrompt.prompt">&customPrompt.prompt;</data>
+
+ <tabbox flex="1">
+ <tabs>
+ <tab label="&basic.tab;"/>
+ <tab label="&advanced.tab;"/>
+ </tabs>
+ <tabpanels flex="1">
+ <vbox>
+ <groupbox>
+ <caption label="&formatGroup.label;"/>
+ <vbox>
+ <hbox align="center">
+ <label control="orientation" value="&orientation.label;"/>
+ <radiogroup id="orientation" oncommand="setOrientation()">
+ <hbox align="center">
+ <radio id="portrait"
+ class="portrait-page"
+ label="&portrait.label;"
+ accesskey="&portrait.accesskey;"/>
+ <radio id="landscape"
+ class="landscape-page"
+ label="&landscape.label;"
+ accesskey="&landscape.accesskey;"/>
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <separator/>
+ <hbox align="center">
+ <label control="scalingInput"
+ value="&scale.label;"
+ accesskey="&scale.accesskey;"/>
+ <textbox id="scalingInput" size="4" oninput="checkDouble(this)"/>
+ <label value="&scalePercent;"/>
+ <separator/>
+ <checkbox id="shrinkToFit"
+ label="&shrinkToFit.label;"
+ accesskey="&shrinkToFit.accesskey;"
+ oncommand="gDialog.scalingInput.disabled=gDialog.scalingLabel.disabled=this.checked"/>
+ </hbox>
+ </vbox>
+ </groupbox>
+ <groupbox>
+ <caption label="&optionsGroup.label;"/>
+ <checkbox id="printBG"
+ label="&printBG.label;"
+ accesskey="&printBG.accesskey;"/>
+ </groupbox>
+ </vbox>
+ <vbox>
+ <groupbox>
+ <caption id="marginGroup" label="&marginGroup.label;"/>
+ <vbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <label control="topInput"
+ value="&marginTop.label;"
+ accesskey="&marginTop.accesskey;"/>
+ <textbox id="topInput" size="5" oninput="changeMargin(this)"/>
+ <!-- This invisible label (with same content as the visible one!) is used
+ to ensure that the <textbox> is centered above the page. The same
+ technique is deployed for the bottom/left/right input fields, below. -->
+ <label value="&marginTop.label;" style="visibility: hidden;"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox dir="ltr">
+ <spacer flex="1"/>
+ <vbox>
+ <spacer flex="1"/>
+ <label control="leftInput"
+ value="&marginLeft.label;"
+ accesskey="&marginLeft.accesskey;"/>
+ <textbox id="leftInput" size="5" oninput="changeMargin(this)"/>
+ <label value="&marginLeft.label;" style="visibility: hidden;"/>
+ <spacer flex="1"/>
+ </vbox>
+ <!-- The "margin page" draws a simulated printout page with dashed lines
+ for the margins. The height/width style attributes of the marginTop,
+ marginBottom, marginLeft, and marginRight elements are set by
+ the JS code dynamically based on the user input. -->
+ <vbox id="marginPage" style="height:29.7mm;">
+ <box id="marginTop" style="height:0.05in;"/>
+ <hbox flex="1" dir="ltr">
+ <box id="marginLeft" style="width:0.025in;"/>
+ <box style="border: 1px; border-style: dashed; border-color: gray;" flex="1"/>
+ <box id="marginRight" style="width:0.025in;"/>
+ </hbox>
+ <box id="marginBottom" style="height:0.05in;"/>
+ </vbox>
+ <vbox>
+ <spacer flex="1"/>
+ <label control="rightInput"
+ value="&marginRight.label;"
+ accesskey="&marginRight.accesskey;"/>
+ <textbox id="rightInput" size="5" oninput="changeMargin(this)"/>
+ <label value="&marginRight.label;" style="visibility: hidden;"/>
+ <spacer flex="1"/>
+ </vbox>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <label control="bottomInput"
+ value="&marginBottom.label;"
+ accesskey="&marginBottom.accesskey;"/>
+ <textbox id="bottomInput" size="5" oninput="changeMargin(this)"/>
+ <label value="&marginBottom.label;" style="visibility: hidden;"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+ </groupbox>
+ <groupbox>
+ <caption id="headersAndFooters" label="&headerFooter.label;"/>
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ <column/>
+ </columns>
+ <rows>
+ <row dir="ltr">
+ <menulist id="hLeftOption" oncommand="customize(this)" tooltiptext="&headerLeft.tip;">
+ <menupopup>
+ <menuitem value="0" label="&hfBlank;"/>
+ <menuitem value="1" label="&hfTitle;"/>
+ <menuitem value="2" label="&hfURL;"/>
+ <menuitem value="3" label="&hfDateAndTime;"/>
+ <menuitem value="4" label="&hfPage;"/>
+ <menuitem value="5" label="&hfPageAndTotal;"/>
+ <menuitem value="6" label="&hfCustom;"/>
+ </menupopup>
+ </menulist>
+ <menulist id="hCenterOption" oncommand="customize(this)" tooltiptext="&headerCenter.tip;">
+ <menupopup>
+ <menuitem value="0" label="&hfBlank;"/>
+ <menuitem value="1" label="&hfTitle;"/>
+ <menuitem value="2" label="&hfURL;"/>
+ <menuitem value="3" label="&hfDateAndTime;"/>
+ <menuitem value="4" label="&hfPage;"/>
+ <menuitem value="5" label="&hfPageAndTotal;"/>
+ <menuitem value="6" label="&hfCustom;"/>
+ </menupopup>
+ </menulist>
+ <menulist id="hRightOption" oncommand="customize(this)" tooltiptext="&headerRight.tip;">
+ <menupopup>
+ <menuitem value="0" label="&hfBlank;"/>
+ <menuitem value="1" label="&hfTitle;"/>
+ <menuitem value="2" label="&hfURL;"/>
+ <menuitem value="3" label="&hfDateAndTime;"/>
+ <menuitem value="4" label="&hfPage;"/>
+ <menuitem value="5" label="&hfPageAndTotal;"/>
+ <menuitem value="6" label="&hfCustom;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row dir="ltr">
+ <vbox align="center">
+ <label value="&hfLeft.label;"/>
+ </vbox>
+ <vbox align="center">
+ <label value="&hfCenter.label;"/>
+ </vbox>
+ <vbox align="center">
+ <label value="&hfRight.label;"/>
+ </vbox>
+ </row>
+ <row dir="ltr">
+ <menulist id="fLeftOption" oncommand="customize(this)" tooltiptext="&footerLeft.tip;">
+ <menupopup>
+ <menuitem value="0" label="&hfBlank;"/>
+ <menuitem value="1" label="&hfTitle;"/>
+ <menuitem value="2" label="&hfURL;"/>
+ <menuitem value="3" label="&hfDateAndTime;"/>
+ <menuitem value="4" label="&hfPage;"/>
+ <menuitem value="5" label="&hfPageAndTotal;"/>
+ <menuitem value="6" label="&hfCustom;"/>
+ </menupopup>
+ </menulist>
+ <menulist id="fCenterOption" oncommand="customize(this)" tooltiptext="&footerCenter.tip;">
+ <menupopup>
+ <menuitem value="0" label="&hfBlank;"/>
+ <menuitem value="1" label="&hfTitle;"/>
+ <menuitem value="2" label="&hfURL;"/>
+ <menuitem value="3" label="&hfDateAndTime;"/>
+ <menuitem value="4" label="&hfPage;"/>
+ <menuitem value="5" label="&hfPageAndTotal;"/>
+ <menuitem value="6" label="&hfCustom;"/>
+ </menupopup>
+ </menulist>
+ <menulist id="fRightOption" oncommand="customize(this)" tooltiptext="&footerRight.tip;">
+ <menupopup>
+ <menuitem value="0" label="&hfBlank;"/>
+ <menuitem value="1" label="&hfTitle;"/>
+ <menuitem value="2" label="&hfURL;"/>
+ <menuitem value="3" label="&hfDateAndTime;"/>
+ <menuitem value="4" label="&hfPage;"/>
+ <menuitem value="5" label="&hfPageAndTotal;"/>
+ <menuitem value="6" label="&hfCustom;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+</dialog>
+
diff --git a/toolkit/components/printing/content/printPreviewBindings.xml b/toolkit/components/printing/content/printPreviewBindings.xml
new file mode 100644
index 0000000000..182ecc1993
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewBindings.xml
@@ -0,0 +1,415 @@
+<?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 file depends on printUtils.js -->
+
+<!DOCTYPE bindings [
+<!ENTITY % printPreviewDTD SYSTEM "chrome://global/locale/printPreview.dtd" >
+%printPreviewDTD;
+]>
+
+<bindings id="printPreviewBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <binding id="printpreviewtoolbar"
+ extends="chrome://global/content/bindings/toolbar.xml#toolbar">
+ <resources>
+ <stylesheet src="chrome://global/skin/printPreview.css"/>
+ </resources>
+
+ <content>
+ <xul:button label="&print.label;" accesskey="&print.accesskey;"
+ oncommand="this.parentNode.print();" icon="print"/>
+
+ <xul:button label="&pageSetup.label;" accesskey="&pageSetup.accesskey;"
+ oncommand="this.parentNode.doPageSetup();"/>
+
+ <xul:vbox align="center" pack="center">
+ <xul:label value="&page.label;" accesskey="&page.accesskey;" control="pageNumber"/>
+ </xul:vbox>
+ <xul:toolbarbutton anonid="navigateHome" class="navigate-button tabbable"
+ oncommand="parentNode.navigate(0, 0, 'home');" tooltiptext="&homearrow.tooltip;"/>
+ <xul:toolbarbutton anonid="navigatePrevious" class="navigate-button tabbable"
+ oncommand="parentNode.navigate(-1, 0, 0);" tooltiptext="&previousarrow.tooltip;"/>
+ <xul:hbox align="center" pack="center">
+ <xul:textbox id="pageNumber" size="3" value="1" min="1" type="number"
+ hidespinbuttons="true" onchange="navigate(0, this.valueNumber, 0);"/>
+ <xul:label value="&of.label;"/>
+ <xul:label value="1"/>
+ </xul:hbox>
+ <xul:toolbarbutton anonid="navigateNext" class="navigate-button tabbable"
+ oncommand="parentNode.navigate(1, 0, 0);" tooltiptext="&nextarrow.tooltip;"/>
+ <xul:toolbarbutton anonid="navigateEnd" class="navigate-button tabbable"
+ oncommand="parentNode.navigate(0, 0, 'end');" tooltiptext="&endarrow.tooltip;"/>
+
+ <xul:toolbarseparator class="toolbarseparator-primary"/>
+ <xul:vbox align="center" pack="center">
+ <xul:label value="&scale.label;" accesskey="&scale.accesskey;" control="scale"/>
+ </xul:vbox>
+
+ <xul:hbox align="center" pack="center">
+ <xul:menulist id="scale" crop="none"
+ oncommand="parentNode.parentNode.scale(this.selectedItem.value);">
+ <xul:menupopup>
+ <xul:menuitem value="0.3" label="&p30.label;"/>
+ <xul:menuitem value="0.4" label="&p40.label;"/>
+ <xul:menuitem value="0.5" label="&p50.label;"/>
+ <xul:menuitem value="0.6" label="&p60.label;"/>
+ <xul:menuitem value="0.7" label="&p70.label;"/>
+ <xul:menuitem value="0.8" label="&p80.label;"/>
+ <xul:menuitem value="0.9" label="&p90.label;"/>
+ <xul:menuitem value="1" label="&p100.label;"/>
+ <xul:menuitem value="1.25" label="&p125.label;"/>
+ <xul:menuitem value="1.5" label="&p150.label;"/>
+ <xul:menuitem value="1.75" label="&p175.label;"/>
+ <xul:menuitem value="2" label="&p200.label;"/>
+ <xul:menuseparator/>
+ <xul:menuitem flex="1" value="ShrinkToFit"
+ label="&ShrinkToFit.label;"/>
+ <xul:menuitem value="Custom" label="&Custom.label;"/>
+ </xul:menupopup>
+ </xul:menulist>
+ </xul:hbox>
+
+ <xul:toolbarseparator class="toolbarseparator-primary"/>
+ <xul:hbox align="center" pack="center">
+ <xul:toolbarbutton label="&portrait.label;" checked="true"
+ accesskey="&portrait.accesskey;"
+ type="radio" group="orient" class="toolbar-portrait-page tabbable"
+ oncommand="parentNode.parentNode.orient('portrait');"/>
+ <xul:toolbarbutton label="&landscape.label;"
+ accesskey="&landscape.accesskey;"
+ type="radio" group="orient" class="toolbar-landscape-page tabbable"
+ oncommand="parentNode.parentNode.orient('landscape');"/>
+ </xul:hbox>
+
+ <xul:toolbarseparator class="toolbarseparator-primary"/>
+ <xul:checkbox label="&simplifyPage.label;" checked="false" disabled="true"
+ accesskey="&simplifyPage.accesskey;"
+ tooltiptext-disabled="&simplifyPage.disabled.tooltip;"
+ tooltiptext-enabled="&simplifyPage.enabled.tooltip;"
+ oncommand="this.parentNode.simplify();"/>
+
+ <xul:toolbarseparator class="toolbarseparator-primary"/>
+ <xul:button label="&close.label;" accesskey="&close.accesskey;"
+ oncommand="PrintUtils.exitPrintPreview();" icon="close"/>
+ <xul:data value="&customPrompt.title;"/>
+ </content>
+
+ <implementation implements="nsIMessageListener">
+ <field name="mPrintButton">
+ document.getAnonymousNodes(this)[0]
+ </field>
+ <field name="mPageTextBox">
+ document.getAnonymousNodes(this)[5].childNodes[0]
+ </field>
+ <field name="mTotalPages">
+ document.getAnonymousNodes(this)[5].childNodes[2]
+ </field>
+ <field name="mScaleLabel">
+ document.getAnonymousNodes(this)[9].firstChild
+ </field>
+ <field name="mScaleCombobox">
+ document.getAnonymousNodes(this)[10].firstChild
+ </field>
+ <field name="mOrientButtonsBox">
+ document.getAnonymousNodes(this)[12]
+ </field>
+ <field name="mPortaitButton">
+ this.mOrientButtonsBox.childNodes[0]
+ </field>
+ <field name="mLandscapeButton">
+ this.mOrientButtonsBox.childNodes[1]
+ </field>
+ <field name="mSimplifyPageCheckbox">
+ document.getAnonymousNodes(this)[14]
+ </field>
+ <field name="mSimplifyPageToolbarSeparator">
+ document.getAnonymousNodes(this)[15]
+ </field>
+ <field name="mCustomTitle">
+ document.getAnonymousNodes(this)[17].firstChild
+ </field>
+ <field name="mPrintPreviewObs">
+ </field>
+ <field name="mWebProgress">
+ </field>
+ <field name="mPPBrowser">
+ null
+ </field>
+ <field name="mMessageManager">
+ null
+ </field>
+
+ <method name="initialize">
+ <parameter name="aPPBrowser"/>
+ <body>
+ <![CDATA[
+ let {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
+ if (!Services.prefs.getBoolPref("print.use_simplify_page")) {
+ this.mSimplifyPageCheckbox.hidden = true;
+ this.mSimplifyPageToolbarSeparator.hidden = true;
+ }
+ this.mPPBrowser = aPPBrowser;
+ this.mMessageManager = aPPBrowser.messageManager;
+ this.mMessageManager.addMessageListener("Printing:Preview:UpdatePageCount", this);
+ this.updateToolbar();
+
+ let $ = id => document.getAnonymousElementByAttribute(this, "anonid", id);
+ let ltr = document.documentElement.matches(":root:-moz-locale-dir(ltr)");
+ // Windows 7 doesn't support ⏮ and ⏭ by default, and fallback doesn't
+ // always work (bug 1343330).
+ let {AppConstants} = Components.utils.import("resource://gre/modules/AppConstants.jsm", {});
+ let useCompatCharacters = AppConstants.isPlatformAndVersionAtMost("win", "6.1");
+ let leftEnd = useCompatCharacters ? "⏪" : "⏮";
+ let rightEnd = useCompatCharacters ? "⏩" : "⏭";
+ $("navigateHome").label = ltr ? leftEnd : rightEnd;
+ $("navigatePrevious").label = ltr ? "◂" : "▸";
+ $("navigateNext").label = ltr ? "▸" : "◂";
+ $("navigateEnd").label = ltr ? rightEnd : leftEnd;
+ ]]>
+ </body>
+ </method>
+
+ <method name="doPageSetup">
+ <body>
+ <![CDATA[
+ var didOK = PrintUtils.showPageSetup();
+ if (didOK) {
+ // the changes that effect the UI
+ this.updateToolbar();
+
+ // Now do PrintPreview
+ PrintUtils.printPreview();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="navigate">
+ <parameter name="aDirection"/>
+ <parameter name="aPageNum"/>
+ <parameter name="aHomeOrEnd"/>
+ <body>
+ <![CDATA[
+ const nsIWebBrowserPrint = Components.interfaces.nsIWebBrowserPrint;
+ let navType, pageNum;
+
+ // we use only one of aHomeOrEnd, aDirection, or aPageNum
+ if (aHomeOrEnd) {
+ // We're going to either the very first page ("home"), or the
+ // very last page ("end").
+ if (aHomeOrEnd == "home") {
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_HOME;
+ this.mPageTextBox.value = 1;
+ } else {
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_END;
+ this.mPageTextBox.value = this.mPageTextBox.max;
+ }
+ pageNum = 0;
+ } else if (aDirection) {
+ // aDirection is either +1 or -1, and allows us to increment
+ // or decrement our currently viewed page.
+ this.mPageTextBox.valueNumber += aDirection;
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM;
+ pageNum = this.mPageTextBox.value; // TODO: back to valueNumber?
+ } else {
+ // We're going to a specific page (aPageNum)
+ navType = nsIWebBrowserPrint.PRINTPREVIEW_GOTO_PAGENUM;
+ pageNum = aPageNum;
+ }
+
+ this.mMessageManager.sendAsyncMessage("Printing:Preview:Navigate", {
+ navType: navType,
+ pageNum: pageNum,
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="print">
+ <body>
+ <![CDATA[
+ PrintUtils.printWindow(this.mPPBrowser.outerWindowID, this.mPPBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <method name="promptForScaleValue">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var value = Math.round(aValue);
+ var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
+ var promptStr = this.mScaleLabel.value;
+ var renameTitle = this.mCustomTitle;
+ var result = {value:value};
+ var confirmed = promptService.prompt(window, renameTitle, promptStr, result, null, {value:value});
+ if (!confirmed || (!result.value) || (result.value == "") || result.value == value) {
+ return -1;
+ }
+ return result.value;
+ ]]>
+ </body>
+ </method>
+
+ <method name="setScaleCombobox">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var scaleValues = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.25, 1.5, 1.75, 2];
+
+ aValue = new Number(aValue);
+
+ for (var i = 0; i < scaleValues.length; i++) {
+ if (aValue == scaleValues[i]) {
+ this.mScaleCombobox.selectedIndex = i;
+ return;
+ }
+ }
+ this.mScaleCombobox.value = "Custom";
+ ]]>
+ </body>
+ </method>
+
+ <method name="scale">
+ <parameter name="aValue"/>
+ <body>
+ <![CDATA[
+ var settings = PrintUtils.getPrintSettings();
+ if (aValue == "ShrinkToFit") {
+ if (!settings.shrinkToFit) {
+ settings.shrinkToFit = true;
+ this.savePrintSettings(settings, settings.kInitSaveShrinkToFit | settings.kInitSaveScaling);
+ PrintUtils.printPreview();
+ }
+ return;
+ }
+
+ if (aValue == "Custom") {
+ aValue = this.promptForScaleValue(settings.scaling * 100.0);
+ if (aValue >= 10) {
+ aValue /= 100.0;
+ } else {
+ if (this.mScaleCombobox.hasAttribute('lastValidInx')) {
+ this.mScaleCombobox.selectedIndex = this.mScaleCombobox.getAttribute('lastValidInx');
+ }
+ return;
+ }
+ }
+
+ this.setScaleCombobox(aValue);
+ this.mScaleCombobox.setAttribute('lastValidInx', this.mScaleCombobox.selectedIndex);
+
+ if (settings.scaling != aValue || settings.shrinkToFit)
+ {
+ settings.shrinkToFit = false;
+ settings.scaling = aValue;
+ this.savePrintSettings(settings, settings.kInitSaveShrinkToFit | settings.kInitSaveScaling);
+ PrintUtils.printPreview();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="orient">
+ <parameter name="aOrientation"/>
+ <body>
+ <![CDATA[
+ const kIPrintSettings = Components.interfaces.nsIPrintSettings;
+ var orientValue = (aOrientation == "portrait") ? kIPrintSettings.kPortraitOrientation :
+ kIPrintSettings.kLandscapeOrientation;
+ var settings = PrintUtils.getPrintSettings();
+ if (settings.orientation != orientValue)
+ {
+ settings.orientation = orientValue;
+ this.savePrintSettings(settings, settings.kInitSaveOrientation);
+ PrintUtils.printPreview();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="simplify">
+ <body>
+ <![CDATA[
+ PrintUtils.setSimplifiedMode(this.mSimplifyPageCheckbox.checked);
+ PrintUtils.printPreview();
+ ]]>
+ </body>
+ </method>
+
+ <method name="enableSimplifyPage">
+ <body>
+ <![CDATA[
+ this.mSimplifyPageCheckbox.disabled = false;
+ this.mSimplifyPageCheckbox.setAttribute("tooltiptext",
+ this.mSimplifyPageCheckbox.getAttribute("tooltiptext-enabled"));
+ ]]>
+ </body>
+ </method>
+
+ <method name="disableSimplifyPage">
+ <body>
+ <![CDATA[
+ this.mSimplifyPageCheckbox.disabled = true;
+ this.mSimplifyPageCheckbox.setAttribute("tooltiptext",
+ this.mSimplifyPageCheckbox.getAttribute("tooltiptext-disabled"));
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateToolbar">
+ <body>
+ <![CDATA[
+ var settings = PrintUtils.getPrintSettings();
+
+ var isPortrait = settings.orientation == Components.interfaces.nsIPrintSettings.kPortraitOrientation;
+
+ this.mPortaitButton.checked = isPortrait;
+ this.mLandscapeButton.checked = !isPortrait;
+
+ if (settings.shrinkToFit) {
+ this.mScaleCombobox.value = "ShrinkToFit";
+ } else {
+ this.setScaleCombobox(settings.scaling);
+ }
+
+ this.mPageTextBox.value = 1;
+
+ this.mMessageManager.sendAsyncMessage("Printing:Preview:UpdatePageCount");
+ ]]>
+ </body>
+ </method>
+
+ <method name="savePrintSettings">
+ <parameter name="settings"/>
+ <parameter name="flags"/>
+ <body><![CDATA[
+ var PSSVC = Components.classes["@mozilla.org/gfx/printsettings-service;1"]
+ .getService(Components.interfaces.nsIPrintSettingsService);
+ PSSVC.savePrintSettingsToPrefs(settings, true, flags);
+ ]]></body>
+ </method>
+
+ <!-- nsIMessageListener -->
+ <method name="receiveMessage">
+ <parameter name="message"/>
+ <body>
+ <![CDATA[
+ if (message.name == "Printing:Preview:UpdatePageCount") {
+ let numPages = message.data.numPages;
+ this.mTotalPages.value = numPages;
+ this.mPageTextBox.max = numPages;
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/components/printing/content/printPreviewProgress.js b/toolkit/components/printing/content/printPreviewProgress.js
new file mode 100644
index 0000000000..5c769e50a4
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewProgress.js
@@ -0,0 +1,154 @@
+// -*- 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/. */
+
+// dialog is just an array we'll use to store various properties from the dialog document...
+var dialog;
+
+// the printProgress is a nsIPrintProgress object
+var printProgress = null;
+
+// random global variables...
+var targetFile;
+
+var docTitle = "";
+var docURL = "";
+var progressParams = null;
+
+function ellipseString(aStr, doFront)
+{
+ if (aStr.length > 3 && (aStr.substr(0, 3) == "..." || aStr.substr(aStr.length-4, 3) == "..."))
+ return aStr;
+
+ var fixedLen = 64;
+ if (aStr.length <= fixedLen)
+ return aStr;
+
+ if (doFront)
+ return "..." + aStr.substr(aStr.length-fixedLen, fixedLen);
+
+ return aStr.substr(0, fixedLen) + "...";
+}
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+
+ onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ window.close();
+ },
+
+ onProgressChange: function (aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress)
+ {
+ if (!progressParams)
+ return;
+ var docTitleStr = ellipseString(progressParams.docTitle, false);
+ if (docTitleStr != docTitle) {
+ docTitle = docTitleStr;
+ dialog.title.value = docTitle;
+ }
+ var docURLStr = ellipseString(progressParams.docURL, true);
+ if (docURLStr != docURL && dialog.title != null) {
+ docURL = docURLStr;
+ if (docTitle == "")
+ dialog.title.value = docURLStr;
+ }
+ },
+
+ onLocationChange: function (aWebProgress, aRequest, aLocation, aFlags) {},
+ onSecurityChange: function (aWebProgress, aRequest, state) {},
+
+ onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage)
+ {
+ if (aMessage)
+ dialog.title.setAttribute("value", aMessage);
+ },
+
+ QueryInterface: function (iid)
+ {
+ if (iid.equals(Components.interfaces.nsIWebProgressListener) || iid.equals(Components.interfaces.nsISupportsWeakReference))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ }
+}
+
+function onLoad() {
+ // Set global variables.
+ printProgress = window.arguments[0];
+ if (window.arguments[1]) {
+ progressParams = window.arguments[1].QueryInterface(Components.interfaces.nsIPrintProgressParams)
+ if (progressParams) {
+ docTitle = ellipseString(progressParams.docTitle, false);
+ docURL = ellipseString(progressParams.docURL, true);
+ }
+ }
+
+ if (!printProgress) {
+ dump( "Invalid argument to printPreviewProgress.xul\n" );
+ window.close()
+ return;
+ }
+
+ dialog = {};
+ dialog.strings = new Array;
+ dialog.title = document.getElementById("dialog.title");
+ dialog.titleLabel = document.getElementById("dialog.titleLabel");
+
+ dialog.title.value = docTitle;
+
+ // set our web progress listener on the helper app launcher
+ printProgress.registerListener(progressListener);
+
+ // We need to delay the set title else dom will overwrite it
+ window.setTimeout(doneIniting, 100);
+}
+
+function onUnload()
+{
+ if (!printProgress)
+ return;
+ try {
+ printProgress.unregisterListener(progressListener);
+ printProgress = null;
+ }
+ catch (e) {}
+}
+
+function getString (stringId) {
+ // Check if we've fetched this string already.
+ if (!(stringId in dialog.strings)) {
+ // Try to get it.
+ var elem = document.getElementById( "dialog.strings."+stringId);
+ try {
+ if (elem && elem.childNodes && elem.childNodes[0] &&
+ elem.childNodes[0].nodeValue)
+ dialog.strings[stringId] = elem.childNodes[0].nodeValue;
+ // If unable to fetch string, use an empty string.
+ else
+ dialog.strings[stringId] = "";
+ } catch (e) { dialog.strings[stringId] = ""; }
+ }
+ return dialog.strings[stringId];
+}
+
+// If the user presses cancel, tell the app launcher and close the dialog...
+function onCancel ()
+{
+ // Cancel app launcher.
+ try {
+ printProgress.processCanceledByUser = true;
+ }
+ catch (e) { return true; }
+
+ // don't Close up dialog by returning false, the backend will close the dialog when everything will be aborted.
+ return false;
+}
+
+function doneIniting()
+{
+ // called by function timeout in onLoad
+ printProgress.doneIniting();
+}
diff --git a/toolkit/components/printing/content/printPreviewProgress.xul b/toolkit/components/printing/content/printPreviewProgress.xul
new file mode 100644
index 0000000000..fa2b9b61d2
--- /dev/null
+++ b/toolkit/components/printing/content/printPreviewProgress.xul
@@ -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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://global/locale/printPreviewProgress.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&printWindow.title;"
+ style="width: 36em;"
+ buttons="cancel"
+ oncancel="onCancel()"
+ onload="onLoad()"
+ onunload="onUnload()">
+
+ <script type="application/javascript" src="chrome://global/content/printPreviewProgress.js"/>
+
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row>
+ <hbox pack="end">
+ <label id="dialog.titleLabel" value="&title;"/>
+ </hbox>
+ <label id="dialog.title"/>
+ </row>
+ <row class="thin-separator">
+ <hbox pack="end">
+ <label id="dialog.progressSpaces" value="&progress;"/>
+ </hbox>
+ <label id="dialog.progressLabel" value="&preparing;"/>
+ </row>
+ </rows>
+ </grid>
+</dialog>
diff --git a/toolkit/components/printing/content/printProgress.js b/toolkit/components/printing/content/printProgress.js
new file mode 100644
index 0000000000..6cadfe45e6
--- /dev/null
+++ b/toolkit/components/printing/content/printProgress.js
@@ -0,0 +1,282 @@
+// -*- tab-width: 2; 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/. */
+
+// dialog is just an array we'll use to store various properties from the dialog document...
+var dialog;
+
+// the printProgress is a nsIPrintProgress object
+var printProgress = null;
+
+// random global variables...
+var targetFile;
+
+var docTitle = "";
+var docURL = "";
+var progressParams = null;
+var switchUI = true;
+
+function ellipseString(aStr, doFront)
+{
+ if (aStr.length > 3 && (aStr.substr(0, 3) == "..." || aStr.substr(aStr.length-4, 3) == "...")) {
+ return aStr;
+ }
+
+ var fixedLen = 64;
+ if (aStr.length > fixedLen) {
+ if (doFront) {
+ var endStr = aStr.substr(aStr.length-fixedLen, fixedLen);
+ return "..." + endStr;
+ }
+ var frontStr = aStr.substr(0, fixedLen);
+ return frontStr + "...";
+ }
+ return aStr;
+}
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_START)
+ {
+ // Put progress meter in undetermined mode.
+ // dialog.progress.setAttribute( "value", 0 );
+ dialog.progress.setAttribute( "mode", "undetermined" );
+ }
+
+ if (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ {
+ // we are done printing
+ // Indicate completion in title area.
+ var msg = getString( "printComplete" );
+ dialog.title.setAttribute("value", msg);
+
+ // Put progress meter at 100%.
+ dialog.progress.setAttribute( "value", 100 );
+ dialog.progress.setAttribute( "mode", "normal" );
+ var percentPrint = getString( "progressText" );
+ percentPrint = replaceInsert( percentPrint, 1, 100 );
+ dialog.progressText.setAttribute("value", percentPrint);
+
+ var fm = Components.classes["@mozilla.org/focus-manager;1"]
+ .getService(Components.interfaces.nsIFocusManager);
+ if (fm && fm.activeWindow == window) {
+ // This progress dialog is the currently active window. In
+ // this case we need to make sure that some other window
+ // gets focus before we close this dialog to work around the
+ // buggy Windows XP Fax dialog, which ends up parenting
+ // itself to the currently focused window and is unable to
+ // survive that window going away. What happens without this
+ // opener.focus() call on Windows XP is that the fax dialog
+ // is opened only to go away when this dialog actually
+ // closes (which can happen asynchronously, so the fax
+ // dialog just flashes once and then goes away), so w/o this
+ // fix, it's impossible to fax on Windows XP w/o manually
+ // switching focus to another window (or holding on to the
+ // progress dialog with the mouse long enough).
+ opener.focus();
+ }
+
+ window.close();
+ }
+ },
+
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress)
+ {
+ if (switchUI)
+ {
+ dialog.tempLabel.setAttribute("hidden", "true");
+ dialog.progress.setAttribute("hidden", "false");
+
+ var progressLabel = getString("progress");
+ if (progressLabel == "") {
+ progressLabel = "Progress:"; // better than nothing
+ }
+ switchUI = false;
+ }
+
+ if (progressParams)
+ {
+ var docTitleStr = ellipseString(progressParams.docTitle, false);
+ if (docTitleStr != docTitle) {
+ docTitle = docTitleStr;
+ dialog.title.value = docTitle;
+ }
+ var docURLStr = progressParams.docURL;
+ if (docURLStr != docURL && dialog.title != null) {
+ docURL = docURLStr;
+ if (docTitle == "") {
+ dialog.title.value = ellipseString(docURLStr, true);
+ }
+ }
+ }
+
+ // Calculate percentage.
+ var percent;
+ if ( aMaxTotalProgress > 0 )
+ {
+ percent = Math.round( (aCurTotalProgress*100)/aMaxTotalProgress );
+ if ( percent > 100 )
+ percent = 100;
+
+ dialog.progress.removeAttribute( "mode");
+
+ // Advance progress meter.
+ dialog.progress.setAttribute( "value", percent );
+
+ // Update percentage label on progress meter.
+ var percentPrint = getString( "progressText" );
+ percentPrint = replaceInsert( percentPrint, 1, percent );
+ dialog.progressText.setAttribute("value", percentPrint);
+ }
+ else
+ {
+ // Progress meter should be barber-pole in this case.
+ dialog.progress.setAttribute( "mode", "undetermined" );
+ // Update percentage label on progress meter.
+ dialog.progressText.setAttribute("value", "");
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ // we can ignore this notification
+ },
+
+ onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ if (aMessage != "")
+ dialog.title.setAttribute("value", aMessage);
+ },
+
+ onSecurityChange: function(aWebProgress, aRequest, state)
+ {
+ // we can ignore this notification
+ },
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Components.interfaces.nsIWebProgressListener) || iid.equals(Components.interfaces.nsISupportsWeakReference))
+ return this;
+
+ throw Components.results.NS_NOINTERFACE;
+ }
+};
+
+function getString( stringId ) {
+ // Check if we've fetched this string already.
+ if (!(stringId in dialog.strings)) {
+ // Try to get it.
+ var elem = document.getElementById( "dialog.strings."+stringId );
+ try {
+ if ( elem
+ &&
+ elem.childNodes
+ &&
+ elem.childNodes[0]
+ &&
+ elem.childNodes[0].nodeValue ) {
+ dialog.strings[stringId] = elem.childNodes[0].nodeValue;
+ } else {
+ // If unable to fetch string, use an empty string.
+ dialog.strings[stringId] = "";
+ }
+ } catch (e) { dialog.strings[stringId] = ""; }
+ }
+ return dialog.strings[stringId];
+}
+
+function loadDialog()
+{
+}
+
+function replaceInsert( text, index, value ) {
+ var result = text;
+ var regExp = new RegExp( "#"+index );
+ result = result.replace( regExp, value );
+ return result;
+}
+
+function onLoad() {
+
+ // Set global variables.
+ printProgress = window.arguments[0];
+ if (window.arguments[1])
+ {
+ progressParams = window.arguments[1].QueryInterface(Components.interfaces.nsIPrintProgressParams)
+ if (progressParams)
+ {
+ docTitle = ellipseString(progressParams.docTitle, false);
+ docURL = ellipseString(progressParams.docURL, true);
+ }
+ }
+
+ if ( !printProgress ) {
+ dump( "Invalid argument to printProgress.xul\n" );
+ window.close()
+ return;
+ }
+
+ dialog = {};
+ dialog.strings = new Array;
+ dialog.title = document.getElementById("dialog.title");
+ dialog.titleLabel = document.getElementById("dialog.titleLabel");
+ dialog.progress = document.getElementById("dialog.progress");
+ dialog.progressText = document.getElementById("dialog.progressText");
+ dialog.progressLabel = document.getElementById("dialog.progressLabel");
+ dialog.tempLabel = document.getElementById("dialog.tempLabel");
+
+ dialog.progress.setAttribute("hidden", "true");
+
+ var progressLabel = getString("preparing");
+ if (progressLabel == "") {
+ progressLabel = "Preparing..."; // better than nothing
+ }
+ dialog.tempLabel.value = progressLabel;
+
+ dialog.title.value = docTitle;
+
+ // Fill dialog.
+ loadDialog();
+
+ // set our web progress listener on the helper app launcher
+ printProgress.registerListener(progressListener);
+ // We need to delay the set title else dom will overwrite it
+ window.setTimeout(doneIniting, 500);
+}
+
+function onUnload()
+{
+ if (printProgress)
+ {
+ try
+ {
+ printProgress.unregisterListener(progressListener);
+ printProgress = null;
+ }
+
+ catch ( exception ) {}
+ }
+}
+
+// If the user presses cancel, tell the app launcher and close the dialog...
+function onCancel ()
+{
+ // Cancel app launcher.
+ try
+ {
+ printProgress.processCanceledByUser = true;
+ }
+ catch ( exception ) { return true; }
+
+ // don't Close up dialog by returning false, the backend will close the dialog when everything will be aborted.
+ return false;
+}
+
+function doneIniting()
+{
+ printProgress.doneIniting();
+}
diff --git a/toolkit/components/printing/content/printProgress.xul b/toolkit/components/printing/content/printProgress.xul
new file mode 100644
index 0000000000..2d724e54fb
--- /dev/null
+++ b/toolkit/components/printing/content/printProgress.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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://global/locale/printProgress.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="cancel"
+ title="&printWindow.title;"
+ style="width: 36em;"
+ ondialogcancel="onCancel()"
+ onload="onLoad()"
+ onunload="onUnload()">
+
+ <script type="application/javascript" src="chrome://global/content/printProgress.js"/>
+
+ <!-- This is non-visible content that simply adds translatable string
+ into the document so that it is accessible to JS code.
+
+ XXX-TODO:
+ convert to use string bundles.
+ -->
+
+ <data id="dialog.strings.dialogCloseLabel">&dialogClose.label;</data>
+ <data id="dialog.strings.printComplete">&printComplete;</data>
+ <data id="dialog.strings.progressText">&percentPrint;</data>
+ <data id="dialog.strings.progressLabel">&progress;</data>
+ <data id="dialog.strings.preparing">&preparing;</data>
+
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row>
+ <hbox pack="end">
+ <label id="dialog.titleLabel" value="&title;"/>
+ </hbox>
+ <label id="dialog.title"/>
+ </row>
+ <row class="thin-separator">
+ <hbox pack="end">
+ <label id="dialog.progressLabel" control="dialog.progress" value="&progress;"/>
+ </hbox>
+ <label id="dialog.tempLabel" value="&preparing;"/>
+ <progressmeter id="dialog.progress" mode="normal" value="0"/>
+ <hbox pack="end" style="min-width: 2.5em;">
+ <label id="dialog.progressText"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+</dialog>
diff --git a/toolkit/components/printing/content/printUtils.js b/toolkit/components/printing/content/printUtils.js
new file mode 100644
index 0000000000..4169541880
--- /dev/null
+++ b/toolkit/components/printing/content/printUtils.js
@@ -0,0 +1,710 @@
+// -*- tab-width: 2; 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/. */
+
+/**
+ * PrintUtils is a utility for front-end code to trigger common print
+ * operations (printing, show print preview, show page settings).
+ *
+ * Unfortunately, likely due to inconsistencies in how different operating
+ * systems do printing natively, our XPCOM-level printing interfaces
+ * are a bit confusing and the method by which we do something basic
+ * like printing a page is quite circuitous.
+ *
+ * To compound that, we need to support remote browsers, and that means
+ * kicking off the print jobs in the content process. This means we send
+ * messages back and forth to that process. browser-content.js contains
+ * the object that listens and responds to the messages that PrintUtils
+ * sends.
+ *
+ * This also means that <xul:browser>'s that hope to use PrintUtils must have
+ * their type attribute set to either "content", "content-targetable", or
+ * "content-primary".
+ *
+ * PrintUtils sends messages at different points in its implementation, but
+ * their documentation is consolidated here for ease-of-access.
+ *
+ *
+ * Messages sent:
+ *
+ * Printing:Print
+ * Kick off a print job for a nsIDOMWindow, passing the outer window ID as
+ * windowID.
+ *
+ * Printing:Preview:Enter
+ * This message is sent to put content into print preview mode. We pass
+ * the content window of the browser we're showing the preview of, and
+ * the target of the message is the browser that we'll be showing the
+ * preview in.
+ *
+ * Printing:Preview:Exit
+ * This message is sent to take content out of print preview mode.
+ *
+ *
+ * Messages Received
+ *
+ * Printing:Preview:Entered
+ * This message is sent by the content process once it has completed
+ * putting the content into print preview mode. We must wait for that to
+ * to complete before switching the chrome UI to print preview mode,
+ * otherwise we have layout issues.
+ *
+ * Printing:Preview:StateChange, Printing:Preview:ProgressChange
+ * Due to a timing issue resulting in a main-process crash, we have to
+ * manually open the progress dialog for print preview. The progress
+ * dialog is opened here in PrintUtils, and then we listen for update
+ * messages from the child. Bug 1088061 has been filed to investigate
+ * other solutions.
+ *
+ */
+
+var gPrintSettingsAreGlobal = false;
+var gSavePrintSettings = false;
+var gFocusedElement = null;
+
+var PrintUtils = {
+ init() {
+ window.messageManager.addMessageListener("Printing:Error", this);
+ },
+
+ get bundle() {
+ let stringService = Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService);
+ delete this.bundle;
+ return this.bundle = stringService.createBundle("chrome://global/locale/printing.properties");
+ },
+
+ /**
+ * Shows the page setup dialog, and saves any settings changed in
+ * that dialog if print.save_print_settings is set to true.
+ *
+ * @return true on success, false on failure
+ */
+ showPageSetup: function () {
+ try {
+ var printSettings = this.getPrintSettings();
+ var PRINTPROMPTSVC = Components.classes["@mozilla.org/embedcomp/printingprompt-service;1"]
+ .getService(Components.interfaces.nsIPrintingPromptService);
+ PRINTPROMPTSVC.showPageSetup(window, printSettings, null);
+ if (gSavePrintSettings) {
+ // Page Setup data is a "native" setting on the Mac
+ var PSSVC = Components.classes["@mozilla.org/gfx/printsettings-service;1"]
+ .getService(Components.interfaces.nsIPrintSettingsService);
+ PSSVC.savePrintSettingsToPrefs(printSettings, true, printSettings.kInitSaveNativeData);
+ }
+ } catch (e) {
+ dump("showPageSetup "+e+"\n");
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Starts the process of printing the contents of a window.
+ *
+ * @param aWindowID
+ * The outer window ID of the nsIDOMWindow to print.
+ * @param aBrowser
+ * The <xul:browser> that the nsIDOMWindow for aWindowID belongs to.
+ */
+ printWindow: function (aWindowID, aBrowser)
+ {
+ let mm = aBrowser.messageManager;
+ mm.sendAsyncMessage("Printing:Print", {
+ windowID: aWindowID,
+ simplifiedMode: this._shouldSimplify,
+ });
+ },
+
+ /**
+ * Deprecated.
+ *
+ * Starts the process of printing the contents of window.content.
+ *
+ */
+ print: function ()
+ {
+ if (gBrowser) {
+ return this.printWindow(gBrowser.selectedBrowser.outerWindowID,
+ gBrowser.selectedBrowser);
+ }
+
+ if (this.usingRemoteTabs) {
+ throw new Error("PrintUtils.print cannot be run in windows running with " +
+ "remote tabs. Use PrintUtils.printWindow instead.");
+ }
+
+ let domWindow = window.content;
+ let ifReq = domWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
+ let browser = ifReq.getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShell)
+ .chromeEventHandler;
+ if (!browser) {
+ throw new Error("PrintUtils.print could not resolve content window " +
+ "to a browser.");
+ }
+
+ let windowID = ifReq.getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .outerWindowID;
+
+ let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ let msg = "PrintUtils.print is now deprecated. Please use PrintUtils.printWindow.";
+ let url = "https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Printing";
+ Deprecated.warning(msg, url);
+
+ this.printWindow(windowID, browser);
+ return undefined;
+ },
+
+ /**
+ * Initializes print preview.
+ *
+ * @param aListenerObj
+ * An object that defines the following functions:
+ *
+ * getPrintPreviewBrowser:
+ * Returns the <xul:browser> to display the print preview in. This
+ * <xul:browser> must have its type attribute set to "content",
+ * "content-targetable", or "content-primary".
+ *
+ * getSourceBrowser:
+ * Returns the <xul:browser> that contains the document being
+ * printed. This <xul:browser> must have its type attribute set to
+ * "content", "content-targetable", or "content-primary".
+ *
+ * getNavToolbox:
+ * Returns the primary toolbox for this window.
+ *
+ * onEnter:
+ * Called upon entering print preview.
+ *
+ * onExit:
+ * Called upon exiting print preview.
+ *
+ * These methods must be defined. printPreview can be called
+ * with aListenerObj as null iff this window is already displaying
+ * print preview (in which case, the previous aListenerObj passed
+ * to it will be used).
+ */
+ printPreview: function (aListenerObj)
+ {
+ // if we're already in PP mode, don't set the listener; chances
+ // are it is null because someone is calling printPreview() to
+ // get us to refresh the display.
+ if (!this.inPrintPreview) {
+ this._listener = aListenerObj;
+ this._sourceBrowser = aListenerObj.getSourceBrowser();
+ this._originalTitle = this._sourceBrowser.contentTitle;
+ this._originalURL = this._sourceBrowser.currentURI.spec;
+
+ // Here we log telemetry data for when the user enters print preview.
+ this.logTelemetry("PRINT_PREVIEW_OPENED_COUNT");
+ } else {
+ // collapse the browser here -- it will be shown in
+ // enterPrintPreview; this forces a reflow which fixes display
+ // issues in bug 267422.
+ // We use the print preview browser as the source browser to avoid
+ // re-initializing print preview with a document that might now have changed.
+ this._sourceBrowser = this._listener.getPrintPreviewBrowser();
+ this._sourceBrowser.collapsed = true;
+
+ // If the user transits too quickly within preview and we have a pending
+ // progress dialog, we will close it before opening a new one.
+ this.ensureProgressDialogClosed();
+ }
+
+ this._webProgressPP = {};
+ let ppParams = {};
+ let notifyOnOpen = {};
+ let printSettings = this.getPrintSettings();
+ // Here we get the PrintingPromptService so we can display the PP Progress from script
+ // For the browser implemented via XUL with the PP toolbar we cannot let it be
+ // automatically opened from the print engine because the XUL scrollbars in the PP window
+ // will layout before the content window and a crash will occur.
+ // Doing it all from script, means it lays out before hand and we can let printing do its own thing
+ let PPROMPTSVC = Components.classes["@mozilla.org/embedcomp/printingprompt-service;1"]
+ .getService(Components.interfaces.nsIPrintingPromptService);
+ // just in case we are already printing,
+ // an error code could be returned if the Progress Dialog is already displayed
+ try {
+ PPROMPTSVC.showProgress(window, null, printSettings, this._obsPP, false,
+ this._webProgressPP, ppParams, notifyOnOpen);
+ if (ppParams.value) {
+ ppParams.value.docTitle = this._originalTitle;
+ ppParams.value.docURL = this._originalURL;
+ }
+
+ // this tells us whether we should continue on with PP or
+ // wait for the callback via the observer
+ if (!notifyOnOpen.value.valueOf() || this._webProgressPP.value == null) {
+ this.enterPrintPreview();
+ }
+ } catch (e) {
+ this.enterPrintPreview();
+ }
+ },
+
+ /**
+ * Returns the nsIWebBrowserPrint associated with some content window.
+ * This method is being kept here for compatibility reasons, but should not
+ * be called by code hoping to support e10s / remote browsers.
+ *
+ * @param aWindow
+ * The window from which to get the nsIWebBrowserPrint from.
+ * @return nsIWebBrowserPrint
+ */
+ getWebBrowserPrint: function (aWindow)
+ {
+ let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ let text = "getWebBrowserPrint is now deprecated, and fully unsupported for " +
+ "multi-process browsers. Please use a frame script to get " +
+ "access to nsIWebBrowserPrint from content.";
+ let url = "https://developer.mozilla.org/en-US/docs/Printing_from_a_XUL_App";
+ Deprecated.warning(text, url);
+
+ if (this.usingRemoteTabs) {
+ return {};
+ }
+
+ var contentWindow = aWindow || window.content;
+ return contentWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebBrowserPrint);
+ },
+
+ /**
+ * Returns the nsIWebBrowserPrint from the print preview browser's docShell.
+ * This method is being kept here for compatibility reasons, but should not
+ * be called by code hoping to support e10s / remote browsers.
+ *
+ * @return nsIWebBrowserPrint
+ */
+ getPrintPreview: function() {
+ let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ let text = "getPrintPreview is now deprecated, and fully unsupported for " +
+ "multi-process browsers. Please use a frame script to get " +
+ "access to nsIWebBrowserPrint from content.";
+ let url = "https://developer.mozilla.org/en-US/docs/Printing_from_a_XUL_App";
+ Deprecated.warning(text, url);
+
+ if (this.usingRemoteTabs) {
+ return {};
+ }
+
+ return this._listener.getPrintPreviewBrowser().docShell.printPreview;
+ },
+
+ get inPrintPreview() {
+ return document.getElementById("print-preview-toolbar") != null;
+ },
+
+ // "private" methods and members. Don't use them.
+
+ _listener: null,
+ _closeHandlerPP: null,
+ _webProgressPP: null,
+ _sourceBrowser: null,
+ _originalTitle: "",
+ _originalURL: "",
+ _shouldSimplify: false,
+
+ get usingRemoteTabs() {
+ // We memoize this, since it's highly unlikely to change over the lifetime
+ // of the window.
+ let usingRemoteTabs =
+ window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsILoadContext)
+ .useRemoteTabs;
+ delete this.usingRemoteTabs;
+ return this.usingRemoteTabs = usingRemoteTabs;
+ },
+
+ displayPrintingError(nsresult, isPrinting) {
+ // The nsresults from a printing error are mapped to strings that have
+ // similar names to the errors themselves. For example, for error
+ // NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE, the name of the string
+ // for the error message is: PERR_GFX_PRINTER_NO_PRINTER_AVAILABLE. What's
+ // more, if we're in the process of doing a print preview, it's possible
+ // that there are strings specific for print preview for these errors -
+ // if so, the names of those strings have _PP as a suffix. It's possible
+ // that no print preview specific strings exist, in which case it is fine
+ // to fall back to the original string name.
+ const MSG_CODES = [
+ "GFX_PRINTER_NO_PRINTER_AVAILABLE",
+ "GFX_PRINTER_NAME_NOT_FOUND",
+ "GFX_PRINTER_COULD_NOT_OPEN_FILE",
+ "GFX_PRINTER_STARTDOC",
+ "GFX_PRINTER_ENDDOC",
+ "GFX_PRINTER_STARTPAGE",
+ "GFX_PRINTER_DOC_IS_BUSY",
+ "ABORT",
+ "NOT_AVAILABLE",
+ "NOT_IMPLEMENTED",
+ "OUT_OF_MEMORY",
+ "UNEXPECTED",
+ ];
+
+ // PERR_FAILURE is the catch-all error message if we've gotten one that
+ // we don't recognize.
+ msgName = "PERR_FAILURE";
+
+ for (let code of MSG_CODES) {
+ let nsErrorResult = "NS_ERROR_" + code;
+ if (Components.results[nsErrorResult] == nsresult) {
+ msgName = "PERR_" + code;
+ break;
+ }
+ }
+
+ let msg, title;
+
+ if (!isPrinting) {
+ // Try first with _PP suffix.
+ let ppMsgName = msgName + "_PP";
+ try {
+ msg = this.bundle.GetStringFromName(ppMsgName);
+ } catch (e) {
+ // We allow localizers to not have the print preview error string,
+ // and just fall back to the printing error string.
+ }
+ }
+
+ if (!msg) {
+ msg = this.bundle.GetStringFromName(msgName);
+ }
+
+ title = this.bundle.GetStringFromName(isPrinting ? "print_error_dialog_title"
+ : "printpreview_error_dialog_title");
+
+ let promptSvc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ promptSvc.alert(window, title, msg);
+ },
+
+ receiveMessage(aMessage) {
+ if (aMessage.name == "Printing:Error") {
+ this.displayPrintingError(aMessage.data.nsresult,
+ aMessage.data.isPrinting);
+ return undefined;
+ }
+
+ // If we got here, then the message we've received must involve
+ // updating the print progress UI.
+ if (!this._webProgressPP.value) {
+ // We somehow didn't get a nsIWebProgressListener to be updated...
+ // I guess there's nothing to do.
+ return undefined;
+ }
+
+ let listener = this._webProgressPP.value;
+ let mm = aMessage.target.messageManager;
+ let data = aMessage.data;
+
+ switch (aMessage.name) {
+ case "Printing:Preview:ProgressChange": {
+ return listener.onProgressChange(null, null,
+ data.curSelfProgress,
+ data.maxSelfProgress,
+ data.curTotalProgress,
+ data.maxTotalProgress);
+ }
+
+ case "Printing:Preview:StateChange": {
+ if (data.stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) {
+ // Strangely, the printing engine sends 2 STATE_STOP messages when
+ // print preview is finishing. One has the STATE_IS_DOCUMENT flag,
+ // the other has the STATE_IS_NETWORK flag. However, the webProgressPP
+ // listener stops listening once the first STATE_STOP is sent.
+ // Any subsequent messages result in NS_ERROR_FAILURE errors getting
+ // thrown. This should all get torn out once bug 1088061 is fixed.
+ mm.removeMessageListener("Printing:Preview:StateChange", this);
+ mm.removeMessageListener("Printing:Preview:ProgressChange", this);
+ }
+
+ return listener.onStateChange(null, null,
+ data.stateFlags,
+ data.status);
+ }
+ }
+ return undefined;
+ },
+
+ setPrinterDefaultsForSelectedPrinter: function (aPSSVC, aPrintSettings)
+ {
+ if (!aPrintSettings.printerName)
+ aPrintSettings.printerName = aPSSVC.defaultPrinterName;
+
+ // First get any defaults from the printer
+ aPSSVC.initPrintSettingsFromPrinter(aPrintSettings.printerName, aPrintSettings);
+ // now augment them with any values from last time
+ aPSSVC.initPrintSettingsFromPrefs(aPrintSettings, true, aPrintSettings.kInitSaveAll);
+ },
+
+ getPrintSettings: function ()
+ {
+ var pref = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ if (pref) {
+ gPrintSettingsAreGlobal = pref.getBoolPref("print.use_global_printsettings", false);
+ gSavePrintSettings = pref.getBoolPref("print.save_print_settings", false);
+ }
+
+ var printSettings;
+ try {
+ var PSSVC = Components.classes["@mozilla.org/gfx/printsettings-service;1"]
+ .getService(Components.interfaces.nsIPrintSettingsService);
+ if (gPrintSettingsAreGlobal) {
+ printSettings = PSSVC.globalPrintSettings;
+ this.setPrinterDefaultsForSelectedPrinter(PSSVC, printSettings);
+ } else {
+ printSettings = PSSVC.newPrintSettings;
+ }
+ } catch (e) {
+ dump("getPrintSettings: "+e+"\n");
+ }
+ return printSettings;
+ },
+
+ // This observer is called once the progress dialog has been "opened"
+ _obsPP:
+ {
+ observe: function(aSubject, aTopic, aData)
+ {
+ // delay the print preview to show the content of the progress dialog
+ setTimeout(function () { PrintUtils.enterPrintPreview(); }, 0);
+ },
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Components.interfaces.nsIObserver) ||
+ iid.equals(Components.interfaces.nsISupportsWeakReference) ||
+ iid.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ }
+ },
+
+ setSimplifiedMode: function (shouldSimplify)
+ {
+ this._shouldSimplify = shouldSimplify;
+ },
+
+ enterPrintPreview: function ()
+ {
+ // Send a message to the print preview browser to initialize
+ // print preview. If we happen to have gotten a print preview
+ // progress listener from nsIPrintingPromptService.showProgress
+ // in printPreview, we add listeners to feed that progress
+ // listener.
+ let ppBrowser = this._listener.getPrintPreviewBrowser();
+ let mm = ppBrowser.messageManager;
+
+ let sendEnterPreviewMessage = function (browser, simplified) {
+ mm.sendAsyncMessage("Printing:Preview:Enter", {
+ windowID: browser.outerWindowID,
+ simplifiedMode: simplified,
+ });
+ };
+
+ // If we happen to have gotten simplify page checked, we will lazily
+ // instantiate a new tab that parses the original page using ReaderMode
+ // primitives. When it's ready, and in order to enter on preview, we send
+ // over a message to print preview browser passing up the simplified tab as
+ // reference. If not, we pass the original tab instead as content source.
+ if (this._shouldSimplify) {
+ let simplifiedBrowser = this._listener.getSimplifiedSourceBrowser();
+ if (simplifiedBrowser) {
+ sendEnterPreviewMessage(simplifiedBrowser, true);
+ } else {
+ simplifiedBrowser = this._listener.createSimplifiedBrowser();
+
+ // After instantiating the simplified tab, we attach a listener as
+ // callback. Once we discover reader mode has been loaded, we fire
+ // up a message to enter on print preview.
+ let spMM = simplifiedBrowser.messageManager;
+ spMM.addMessageListener("Printing:Preview:ReaderModeReady", function onReaderReady() {
+ spMM.removeMessageListener("Printing:Preview:ReaderModeReady", onReaderReady);
+ sendEnterPreviewMessage(simplifiedBrowser, true);
+ });
+
+ // Here, we send down a message to simplified browser in order to parse
+ // the original page. After we have parsed it, content will tell parent
+ // that the document is ready for print previewing.
+ spMM.sendAsyncMessage("Printing:Preview:ParseDocument", {
+ URL: this._originalURL,
+ windowID: this._sourceBrowser.outerWindowID,
+ });
+
+ // Here we log telemetry data for when the user enters simplify mode.
+ this.logTelemetry("PRINT_PREVIEW_SIMPLIFY_PAGE_OPENED_COUNT");
+ }
+ } else {
+ sendEnterPreviewMessage(this._sourceBrowser, false);
+ }
+
+ if (this._webProgressPP.value) {
+ mm.addMessageListener("Printing:Preview:StateChange", this);
+ mm.addMessageListener("Printing:Preview:ProgressChange", this);
+ }
+
+ let onEntered = (message) => {
+ mm.removeMessageListener("Printing:Preview:Entered", onEntered);
+
+ if (message.data.failed) {
+ // Something went wrong while putting the document into print preview
+ // mode. Bail out.
+ this._listener.onEnter();
+ this._listener.onExit();
+ return;
+ }
+
+ // Stash the focused element so that we can return to it after exiting
+ // print preview.
+ gFocusedElement = document.commandDispatcher.focusedElement;
+
+ let printPreviewTB = document.getElementById("print-preview-toolbar");
+ if (printPreviewTB) {
+ printPreviewTB.updateToolbar();
+ ppBrowser.collapsed = false;
+ ppBrowser.focus();
+ return;
+ }
+
+ // Set the original window as an active window so any mozPrintCallbacks can
+ // run without delayed setTimeouts.
+ if (this._listener.activateBrowser) {
+ this._listener.activateBrowser(this._sourceBrowser);
+ } else {
+ this._sourceBrowser.docShellIsActive = true;
+ }
+
+ // show the toolbar after we go into print preview mode so
+ // that we can initialize the toolbar with total num pages
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ printPreviewTB = document.createElementNS(XUL_NS, "toolbar");
+ printPreviewTB.setAttribute("printpreview", true);
+ printPreviewTB.setAttribute("fullscreentoolbar", true);
+ printPreviewTB.id = "print-preview-toolbar";
+
+ let navToolbox = this._listener.getNavToolbox();
+ navToolbox.parentNode.insertBefore(printPreviewTB, navToolbox);
+ printPreviewTB.initialize(ppBrowser);
+
+ // Enable simplify page checkbox when the page is an article
+ if (this._sourceBrowser.isArticle) {
+ printPreviewTB.enableSimplifyPage();
+ } else {
+ this.logTelemetry("PRINT_PREVIEW_SIMPLIFY_PAGE_UNAVAILABLE_COUNT");
+ printPreviewTB.disableSimplifyPage();
+ }
+
+ // copy the window close handler
+ if (document.documentElement.hasAttribute("onclose"))
+ this._closeHandlerPP = document.documentElement.getAttribute("onclose");
+ else
+ this._closeHandlerPP = null;
+ document.documentElement.setAttribute("onclose", "PrintUtils.exitPrintPreview(); return false;");
+
+ // disable chrome shortcuts...
+ window.addEventListener("keydown", this.onKeyDownPP, true);
+ window.addEventListener("keypress", this.onKeyPressPP, true);
+
+ ppBrowser.collapsed = false;
+ ppBrowser.focus();
+ // on Enter PP Call back
+ this._listener.onEnter();
+ };
+
+ mm.addMessageListener("Printing:Preview:Entered", onEntered);
+ },
+
+ exitPrintPreview: function ()
+ {
+ let ppBrowser = this._listener.getPrintPreviewBrowser();
+ let browserMM = ppBrowser.messageManager;
+ browserMM.sendAsyncMessage("Printing:Preview:Exit");
+ window.removeEventListener("keydown", this.onKeyDownPP, true);
+ window.removeEventListener("keypress", this.onKeyPressPP, true);
+
+ // restore the old close handler
+ document.documentElement.setAttribute("onclose", this._closeHandlerPP);
+ this._closeHandlerPP = null;
+
+ // remove the print preview toolbar
+ let printPreviewTB = document.getElementById("print-preview-toolbar");
+ this._listener.getNavToolbox().parentNode.removeChild(printPreviewTB);
+
+ let fm = Components.classes["@mozilla.org/focus-manager;1"]
+ .getService(Components.interfaces.nsIFocusManager);
+ if (gFocusedElement)
+ fm.setFocus(gFocusedElement, fm.FLAG_NOSCROLL);
+ else
+ this._sourceBrowser.focus();
+ gFocusedElement = null;
+
+ this.setSimplifiedMode(false);
+
+ this.ensureProgressDialogClosed();
+
+ this._listener.onExit();
+ },
+
+ logTelemetry: function (ID)
+ {
+ let histogram = Services.telemetry.getHistogramById(ID);
+ histogram.add(true);
+ },
+
+ onKeyDownPP: function (aEvent)
+ {
+ // Esc exits the PP
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ PrintUtils.exitPrintPreview();
+ }
+ },
+
+ onKeyPressPP: function (aEvent)
+ {
+ var closeKey;
+ try {
+ closeKey = document.getElementById("key_close")
+ .getAttribute("key");
+ closeKey = aEvent["DOM_VK_"+closeKey];
+ } catch (e) {}
+ var isModif = aEvent.ctrlKey || aEvent.metaKey;
+ // Ctrl-W exits the PP
+ if (isModif &&
+ (aEvent.charCode == closeKey || aEvent.charCode == closeKey + 32)) {
+ PrintUtils.exitPrintPreview();
+ }
+ else if (isModif) {
+ var printPreviewTB = document.getElementById("print-preview-toolbar");
+ var printKey = document.getElementById("printKb").getAttribute("key").toUpperCase();
+ var pressedKey = String.fromCharCode(aEvent.charCode).toUpperCase();
+ if (printKey == pressedKey) {
+ printPreviewTB.print();
+ }
+ }
+ // cancel shortkeys
+ if (isModif) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ },
+
+ /**
+ * If there's a printing or print preview progress dialog displayed, force
+ * it to close now.
+ */
+ ensureProgressDialogClosed() {
+ if (this._webProgressPP && this._webProgressPP.value) {
+ this._webProgressPP.value.onStateChange(null, null,
+ Components.interfaces.nsIWebProgressListener.STATE_STOP, 0);
+ }
+ },
+}
+
+PrintUtils.init();
diff --git a/toolkit/components/printing/content/printdialog.js b/toolkit/components/printing/content/printdialog.js
new file mode 100644
index 0000000000..e5a38ddced
--- /dev/null
+++ b/toolkit/components/printing/content/printdialog.js
@@ -0,0 +1,425 @@
+// -*- 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 dialog;
+var printService = null;
+var gOriginalNumCopies = 1;
+
+var paramBlock;
+var gPrefs = null;
+var gPrintSettings = null;
+var gWebBrowserPrint = null;
+var gPrintSetInterface = Components.interfaces.nsIPrintSettings;
+var doDebug = false;
+
+// ---------------------------------------------------
+function initDialog()
+{
+ dialog = {};
+
+ dialog.propertiesButton = document.getElementById("properties");
+ dialog.descText = document.getElementById("descText");
+
+ dialog.printrangeGroup = document.getElementById("printrangeGroup");
+ dialog.allpagesRadio = document.getElementById("allpagesRadio");
+ dialog.rangeRadio = document.getElementById("rangeRadio");
+ dialog.selectionRadio = document.getElementById("selectionRadio");
+ dialog.frompageInput = document.getElementById("frompageInput");
+ dialog.frompageLabel = document.getElementById("frompageLabel");
+ dialog.topageInput = document.getElementById("topageInput");
+ dialog.topageLabel = document.getElementById("topageLabel");
+
+ dialog.numCopiesInput = document.getElementById("numCopiesInput");
+
+ dialog.printframeGroup = document.getElementById("printframeGroup");
+ dialog.aslaidoutRadio = document.getElementById("aslaidoutRadio");
+ dialog.selectedframeRadio = document.getElementById("selectedframeRadio");
+ dialog.eachframesepRadio = document.getElementById("eachframesepRadio");
+ dialog.printframeGroupLabel = document.getElementById("printframeGroupLabel");
+
+ dialog.fileCheck = document.getElementById("fileCheck");
+ dialog.printerLabel = document.getElementById("printerLabel");
+ dialog.printerList = document.getElementById("printerList");
+
+ dialog.printButton = document.documentElement.getButton("accept");
+
+ // <data> elements
+ dialog.printName = document.getElementById("printButton");
+ dialog.fpDialog = document.getElementById("fpDialog");
+
+ dialog.enabled = false;
+}
+
+// ---------------------------------------------------
+function checkInteger(element)
+{
+ var value = element.value;
+ if (value && value.length > 0) {
+ value = value.replace(/[^0-9]/g, "");
+ if (!value) value = "";
+ element.value = value;
+ }
+ if (!value || value < 1 || value > 999)
+ dialog.printButton.setAttribute("disabled", "true");
+ else
+ dialog.printButton.removeAttribute("disabled");
+}
+
+// ---------------------------------------------------
+function stripTrailingWhitespace(element)
+{
+ var value = element.value;
+ value = value.replace(/\s+$/, "");
+ element.value = value;
+}
+
+// ---------------------------------------------------
+function getPrinterDescription(printerName)
+{
+ var s = "";
+
+ try {
+ /* This may not work with non-ASCII test (see bug 235763 comment #16) */
+ s = gPrefs.getCharPref("print.printer_" + printerName + ".printer_description")
+ } catch (e) {
+ }
+
+ return s;
+}
+
+// ---------------------------------------------------
+function listElement(aListElement)
+ {
+ this.listElement = aListElement;
+ }
+
+listElement.prototype =
+ {
+ clearList:
+ function ()
+ {
+ // remove the menupopup node child of the menulist.
+ var popup = this.listElement.firstChild;
+ if (popup) {
+ this.listElement.removeChild(popup);
+ }
+ },
+
+ appendPrinterNames:
+ function (aDataObject)
+ {
+ if ((null == aDataObject) || !aDataObject.hasMore()) {
+ // disable dialog
+ this.listElement.setAttribute("value", "");
+ this.listElement.setAttribute("label",
+ document.getElementById("printingBundle")
+ .getString("noprinter"));
+
+ this.listElement.setAttribute("disabled", "true");
+ dialog.printerLabel.setAttribute("disabled", "true");
+ dialog.propertiesButton.setAttribute("disabled", "true");
+ dialog.fileCheck.setAttribute("disabled", "true");
+ dialog.printButton.setAttribute("disabled", "true");
+ }
+ else {
+ // build popup menu from printer names
+ var list = document.getElementById("printerList");
+ do {
+ printerNameStr = aDataObject.getNext();
+ list.appendItem(printerNameStr, printerNameStr, getPrinterDescription(printerNameStr));
+ } while (aDataObject.hasMore());
+ this.listElement.removeAttribute("disabled");
+ }
+ }
+ };
+
+// ---------------------------------------------------
+function getPrinters()
+{
+ var selectElement = new listElement(dialog.printerList);
+ selectElement.clearList();
+
+ var printerEnumerator;
+ try {
+ printerEnumerator =
+ Components.classes["@mozilla.org/gfx/printerenumerator;1"]
+ .getService(Components.interfaces.nsIPrinterEnumerator)
+ .printerNameList;
+ } catch (e) { printerEnumerator = null; }
+
+ selectElement.appendPrinterNames(printerEnumerator);
+ selectElement.listElement.value = printService.defaultPrinterName;
+
+ // make sure we load the prefs for the initially selected printer
+ setPrinterDefaultsForSelectedPrinter();
+}
+
+
+// ---------------------------------------------------
+// update gPrintSettings with the defaults for the selected printer
+function setPrinterDefaultsForSelectedPrinter()
+{
+ gPrintSettings.printerName = dialog.printerList.value;
+
+ dialog.descText.value = getPrinterDescription(gPrintSettings.printerName);
+
+ // First get any defaults from the printer
+ printService.initPrintSettingsFromPrinter(gPrintSettings.printerName, gPrintSettings);
+
+ // now augment them with any values from last time
+ printService.initPrintSettingsFromPrefs(gPrintSettings, true, gPrintSetInterface.kInitSaveAll);
+
+ if (doDebug) {
+ dump("setPrinterDefaultsForSelectedPrinter: printerName='"+gPrintSettings.printerName+"', paperName='"+gPrintSettings.paperName+"'\n");
+ }
+}
+
+// ---------------------------------------------------
+function displayPropertiesDialog()
+{
+ gPrintSettings.numCopies = dialog.numCopiesInput.value;
+ try {
+ var printingPromptService = Components.classes["@mozilla.org/embedcomp/printingprompt-service;1"]
+ .getService(Components.interfaces.nsIPrintingPromptService);
+ if (printingPromptService) {
+ printingPromptService.showPrinterProperties(null, dialog.printerList.value, gPrintSettings);
+ dialog.numCopiesInput.value = gPrintSettings.numCopies;
+ }
+ } catch (e) {
+ dump("problems getting printingPromptService\n");
+ }
+}
+
+// ---------------------------------------------------
+function doPrintRange(inx)
+{
+ if (inx == 1) {
+ dialog.frompageInput.removeAttribute("disabled");
+ dialog.frompageLabel.removeAttribute("disabled");
+ dialog.topageInput.removeAttribute("disabled");
+ dialog.topageLabel.removeAttribute("disabled");
+ } else {
+ dialog.frompageInput.setAttribute("disabled", "true");
+ dialog.frompageLabel.setAttribute("disabled", "true");
+ dialog.topageInput.setAttribute("disabled", "true");
+ dialog.topageLabel.setAttribute("disabled", "true");
+ }
+}
+
+// ---------------------------------------------------
+function loadDialog()
+{
+ var print_copies = 1;
+ var print_selection_radio_enabled = false;
+ var print_frametype = gPrintSetInterface.kSelectedFrame;
+ var print_howToEnableUI = gPrintSetInterface.kFrameEnableNone;
+ var print_tofile = "";
+
+ try {
+ gPrefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+
+ printService = Components.classes["@mozilla.org/gfx/printsettings-service;1"];
+ if (printService) {
+ printService = printService.getService();
+ if (printService) {
+ printService = printService.QueryInterface(Components.interfaces.nsIPrintSettingsService);
+ }
+ }
+ } catch (e) {}
+
+ // Note: getPrinters sets up the PrintToFile control
+ getPrinters();
+
+ if (gPrintSettings) {
+ print_tofile = gPrintSettings.printToFile;
+ gOriginalNumCopies = gPrintSettings.numCopies;
+
+ print_copies = gPrintSettings.numCopies;
+ print_frametype = gPrintSettings.printFrameType;
+ print_howToEnableUI = gPrintSettings.howToEnableFrameUI;
+ print_selection_radio_enabled = gPrintSettings.GetPrintOptions(gPrintSetInterface.kEnableSelectionRB);
+ }
+
+ if (doDebug) {
+ dump("loadDialog*********************************************\n");
+ dump("print_tofile "+print_tofile+"\n");
+ dump("print_frame "+print_frametype+"\n");
+ dump("print_howToEnableUI "+print_howToEnableUI+"\n");
+ dump("selection_radio_enabled "+print_selection_radio_enabled+"\n");
+ }
+
+ dialog.printrangeGroup.selectedItem = dialog.allpagesRadio;
+ if (print_selection_radio_enabled) {
+ dialog.selectionRadio.removeAttribute("disabled");
+ } else {
+ dialog.selectionRadio.setAttribute("disabled", "true");
+ }
+ doPrintRange(dialog.rangeRadio.selected);
+ dialog.frompageInput.value = 1;
+ dialog.topageInput.value = 1;
+ dialog.numCopiesInput.value = print_copies;
+
+ if (doDebug) {
+ dump("print_howToEnableUI: "+print_howToEnableUI+"\n");
+ }
+
+ // print frame
+ if (print_howToEnableUI == gPrintSetInterface.kFrameEnableAll) {
+ dialog.aslaidoutRadio.removeAttribute("disabled");
+
+ dialog.selectedframeRadio.removeAttribute("disabled");
+ dialog.eachframesepRadio.removeAttribute("disabled");
+ dialog.printframeGroupLabel.removeAttribute("disabled");
+
+ // initialize radio group
+ dialog.printframeGroup.selectedItem = dialog.selectedframeRadio;
+
+ } else if (print_howToEnableUI == gPrintSetInterface.kFrameEnableAsIsAndEach) {
+ dialog.aslaidoutRadio.removeAttribute("disabled"); // enable
+
+ dialog.selectedframeRadio.setAttribute("disabled", "true"); // disable
+ dialog.eachframesepRadio.removeAttribute("disabled"); // enable
+ dialog.printframeGroupLabel.removeAttribute("disabled"); // enable
+
+ // initialize
+ dialog.printframeGroup.selectedItem = dialog.eachframesepRadio;
+
+ } else {
+ dialog.aslaidoutRadio.setAttribute("disabled", "true");
+ dialog.selectedframeRadio.setAttribute("disabled", "true");
+ dialog.eachframesepRadio.setAttribute("disabled", "true");
+ dialog.printframeGroupLabel.setAttribute("disabled", "true");
+ }
+
+ dialog.printButton.label = dialog.printName.getAttribute("label");
+}
+
+// ---------------------------------------------------
+function onLoad()
+{
+ // Init dialog.
+ initDialog();
+
+ // param[0]: nsIPrintSettings object
+ // param[1]: container for return value (1 = print, 0 = cancel)
+
+ gPrintSettings = window.arguments[0].QueryInterface(gPrintSetInterface);
+ gWebBrowserPrint = window.arguments[1].QueryInterface(Components.interfaces.nsIWebBrowserPrint);
+ paramBlock = window.arguments[2].QueryInterface(Components.interfaces.nsIDialogParamBlock);
+
+ // default return value is "cancel"
+ paramBlock.SetInt(0, 0);
+
+ loadDialog();
+}
+
+// ---------------------------------------------------
+function onAccept()
+{
+ if (gPrintSettings != null) {
+ var print_howToEnableUI = gPrintSetInterface.kFrameEnableNone;
+
+ // save these out so they can be picked up by the device spec
+ gPrintSettings.printerName = dialog.printerList.value;
+ print_howToEnableUI = gPrintSettings.howToEnableFrameUI;
+ gPrintSettings.printToFile = dialog.fileCheck.checked;
+
+ if (gPrintSettings.printToFile && !chooseFile())
+ return false;
+
+ if (dialog.allpagesRadio.selected) {
+ gPrintSettings.printRange = gPrintSetInterface.kRangeAllPages;
+ } else if (dialog.rangeRadio.selected) {
+ gPrintSettings.printRange = gPrintSetInterface.kRangeSpecifiedPageRange;
+ } else if (dialog.selectionRadio.selected) {
+ gPrintSettings.printRange = gPrintSetInterface.kRangeSelection;
+ }
+ gPrintSettings.startPageRange = dialog.frompageInput.value;
+ gPrintSettings.endPageRange = dialog.topageInput.value;
+ gPrintSettings.numCopies = dialog.numCopiesInput.value;
+
+ var frametype = gPrintSetInterface.kNoFrames;
+ if (print_howToEnableUI != gPrintSetInterface.kFrameEnableNone) {
+ if (dialog.aslaidoutRadio.selected) {
+ frametype = gPrintSetInterface.kFramesAsIs;
+ } else if (dialog.selectedframeRadio.selected) {
+ frametype = gPrintSetInterface.kSelectedFrame;
+ } else if (dialog.eachframesepRadio.selected) {
+ frametype = gPrintSetInterface.kEachFrameSep;
+ } else {
+ frametype = gPrintSetInterface.kSelectedFrame;
+ }
+ }
+ gPrintSettings.printFrameType = frametype;
+ if (doDebug) {
+ dump("onAccept*********************************************\n");
+ dump("frametype "+frametype+"\n");
+ dump("numCopies "+gPrintSettings.numCopies+"\n");
+ dump("printRange "+gPrintSettings.printRange+"\n");
+ dump("printerName "+gPrintSettings.printerName+"\n");
+ dump("startPageRange "+gPrintSettings.startPageRange+"\n");
+ dump("endPageRange "+gPrintSettings.endPageRange+"\n");
+ dump("printToFile "+gPrintSettings.printToFile+"\n");
+ }
+ }
+
+ var saveToPrefs = false;
+
+ saveToPrefs = gPrefs.getBoolPref("print.save_print_settings");
+
+ if (saveToPrefs && printService != null) {
+ var flags = gPrintSetInterface.kInitSavePaperSize |
+ gPrintSetInterface.kInitSaveEdges |
+ gPrintSetInterface.kInitSaveInColor |
+ gPrintSetInterface.kInitSaveShrinkToFit |
+ gPrintSetInterface.kInitSaveScaling;
+ printService.savePrintSettingsToPrefs(gPrintSettings, true, flags);
+ }
+
+ // set return value to "print"
+ if (paramBlock) {
+ paramBlock.SetInt(0, 1);
+ } else {
+ dump("*** FATAL ERROR: No paramBlock\n");
+ }
+
+ return true;
+}
+
+// ---------------------------------------------------
+function onCancel()
+{
+ // set return value to "cancel"
+ if (paramBlock) {
+ paramBlock.SetInt(0, 0);
+ } else {
+ dump("*** FATAL ERROR: No paramBlock\n");
+ }
+
+ return true;
+}
+
+// ---------------------------------------------------
+const nsIFilePicker = Components.interfaces.nsIFilePicker;
+function chooseFile()
+{
+ try {
+ var fp = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(window, dialog.fpDialog.getAttribute("label"), nsIFilePicker.modeSave);
+ fp.appendFilters(nsIFilePicker.filterAll);
+ if (fp.show() != Components.interfaces.nsIFilePicker.returnCancel &&
+ fp.file && fp.file.path) {
+ gPrintSettings.toFileName = fp.file.path;
+ return true;
+ }
+ } catch (ex) {
+ dump(ex);
+ }
+
+ return false;
+}
+
diff --git a/toolkit/components/printing/content/printdialog.xul b/toolkit/components/printing/content/printdialog.xul
new file mode 100644
index 0000000000..d6452945b1
--- /dev/null
+++ b/toolkit/components/printing/content/printdialog.xul
@@ -0,0 +1,126 @@
+<?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://global/locale/printdialog.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoad();"
+ ondialogaccept="return onAccept();"
+ oncancel="return onCancel();"
+ buttoniconaccept="print"
+ title="&printDialog.title;"
+ persist="screenX screenY"
+ screenX="24" screenY="24">
+
+ <script type="application/javascript" src="chrome://global/content/printdialog.js"/>
+
+ <stringbundle id="printingBundle" src="chrome://global/locale/printing.properties"/>
+
+ <groupbox>
+ <caption label="&printer.label;"/>
+
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label id="printerLabel"
+ value="&printerInput.label;"
+ accesskey="&printerInput.accesskey;"
+ control="printerList"/>
+ </hbox>
+ <menulist id="printerList" flex="1" type="description" oncommand="setPrinterDefaultsForSelectedPrinter();"/>
+ <button id="properties"
+ label="&propertiesButton.label;"
+ accesskey="&propertiesButton.accesskey;"
+ icon="properties"
+ oncommand="displayPropertiesDialog();"/>
+ </row>
+ <row align="center">
+ <hbox align="center" pack="end">
+ <label id="descTextLabel" control="descText" value="&descText.label;"/>
+ </hbox>
+ <label id="descText"/>
+ <checkbox id="fileCheck"
+ checked="false"
+ label="&fileCheck.label;"
+ accesskey="&fileCheck.accesskey;"
+ pack="end"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <hbox>
+ <groupbox flex="1">
+ <caption label="&printrangeGroup.label;"/>
+
+ <radiogroup id="printrangeGroup">
+ <radio id="allpagesRadio"
+ label="&allpagesRadio.label;"
+ accesskey="&allpagesRadio.accesskey;"
+ oncommand="doPrintRange(0)"/>
+ <hbox align="center">
+ <radio id="rangeRadio"
+ label="&rangeRadio.label;"
+ accesskey="&rangeRadio.accesskey;"
+ oncommand="doPrintRange(1)"/>
+ <label id="frompageLabel"
+ control="frompageInput"
+ value="&frompageInput.label;"
+ accesskey="&frompageInput.accesskey;"/>
+ <textbox id="frompageInput" style="width:5em;" onkeyup="checkInteger(this)"/>
+ <label id="topageLabel"
+ control="topageInput"
+ value="&topageInput.label;"
+ accesskey="&topageInput.accesskey;"/>
+ <textbox id="topageInput" style="width:5em;" onkeyup="checkInteger(this)"/>
+ </hbox>
+ <radio id="selectionRadio"
+ label="&selectionRadio.label;"
+ accesskey="&selectionRadio.accesskey;"
+ oncommand="doPrintRange(2)"/>
+ </radiogroup>
+ </groupbox>
+
+ <groupbox flex="1">
+ <caption label="&copies.label;"/>
+ <hbox align="center">
+ <label control="numCopiesInput"
+ value="&numCopies.label;"
+ accesskey="&numCopies.accesskey;"/>
+ <textbox id="numCopiesInput" style="width:5em;" onkeyup="checkInteger(this)"/>
+ </hbox>
+ </groupbox>
+ </hbox>
+
+ <groupbox flex="1">
+ <caption label="&printframeGroup.label;" id="printframeGroupLabel"/>
+ <radiogroup id="printframeGroup">
+ <radio id="aslaidoutRadio"
+ label="&aslaidoutRadio.label;"
+ accesskey="&aslaidoutRadio.accesskey;"/>
+ <radio id="selectedframeRadio"
+ label="&selectedframeRadio.label;"
+ accesskey="&selectedframeRadio.accesskey;"/>
+ <radio id="eachframesepRadio"
+ label="&eachframesepRadio.label;"
+ accesskey="&eachframesepRadio.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <!-- used to store titles and labels -->
+ <data style="display:none;" id="printButton" label="&printButton.label;"/>
+ <data style="display:none;" id="fpDialog" label="&fpDialog.title;"/>
+
+</dialog>
+
diff --git a/toolkit/components/printing/content/printjoboptions.js b/toolkit/components/printing/content/printjoboptions.js
new file mode 100644
index 0000000000..26f3ffd854
--- /dev/null
+++ b/toolkit/components/printing/content/printjoboptions.js
@@ -0,0 +1,401 @@
+// -*- 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 dialog;
+var gPrintBundle;
+var gPrintSettings = null;
+var gPrintSettingsInterface = Components.interfaces.nsIPrintSettings;
+var gPaperArray;
+var gPrefs;
+
+var gPrintSetInterface = Components.interfaces.nsIPrintSettings;
+var doDebug = true;
+
+// ---------------------------------------------------
+function checkDouble(element, maxVal)
+{
+ var value = element.value;
+ if (value && value.length > 0) {
+ value = value.replace(/[^\.|^0-9]/g, "");
+ if (!value) {
+ element.value = "";
+ } else if (value > maxVal) {
+ element.value = maxVal;
+ } else {
+ element.value = value;
+ }
+ }
+}
+
+// ---------------------------------------------------
+function isListOfPrinterFeaturesAvailable()
+{
+ var has_printerfeatures = false;
+
+ try {
+ has_printerfeatures = gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".has_special_printerfeatures");
+ } catch (ex) {
+ }
+
+ return has_printerfeatures;
+}
+
+// ---------------------------------------------------
+function getDoubleStr(val, dec)
+{
+ var str = val.toString();
+ var inx = str.indexOf(".");
+ return str.substring(0, inx+dec+1);
+}
+
+// ---------------------------------------------------
+function initDialog()
+{
+ gPrintBundle = document.getElementById("printBundle");
+
+ dialog = {};
+
+ dialog.paperList = document.getElementById("paperList");
+ dialog.paperGroup = document.getElementById("paperGroup");
+
+ dialog.jobTitleLabel = document.getElementById("jobTitleLabel");
+ dialog.jobTitleGroup = document.getElementById("jobTitleGroup");
+ dialog.jobTitleInput = document.getElementById("jobTitleInput");
+
+ dialog.colorGroup = document.getElementById("colorGroup");
+ dialog.colorRadioGroup = document.getElementById("colorRadioGroup");
+ dialog.colorRadio = document.getElementById("colorRadio");
+ dialog.grayRadio = document.getElementById("grayRadio");
+
+ dialog.topInput = document.getElementById("topInput");
+ dialog.bottomInput = document.getElementById("bottomInput");
+ dialog.leftInput = document.getElementById("leftInput");
+ dialog.rightInput = document.getElementById("rightInput");
+}
+
+// ---------------------------------------------------
+function round10(val)
+{
+ return Math.round(val * 10) / 10;
+}
+
+
+// ---------------------------------------------------
+function paperListElement(aPaperListElement)
+ {
+ this.paperListElement = aPaperListElement;
+ }
+
+paperListElement.prototype =
+ {
+ clearPaperList:
+ function ()
+ {
+ // remove the menupopup node child of the menulist.
+ this.paperListElement.removeChild(this.paperListElement.firstChild);
+ },
+
+ appendPaperNames:
+ function (aDataObject)
+ {
+ var popupNode = document.createElement("menupopup");
+ for (var i=0;i<aDataObject.length;i++) {
+ var paperObj = aDataObject[i];
+ var itemNode = document.createElement("menuitem");
+ var label;
+ try {
+ label = gPrintBundle.getString(paperObj.name);
+ }
+ catch (e) {
+ /* No name in string bundle ? Then build one manually (this
+ * usually happens when gPaperArray was build by createPaperArrayFromPrinterFeatures() ...) */
+ if (paperObj.inches) {
+ label = paperObj.name + " (" + round10(paperObj.width) + "x" + round10(paperObj.height) + " inch)";
+ }
+ else {
+ label = paperObj.name + " (" + paperObj.width + "x" + paperObj.height + " mm)";
+ }
+ }
+ itemNode.setAttribute("label", label);
+ itemNode.setAttribute("value", i);
+ popupNode.appendChild(itemNode);
+ }
+ this.paperListElement.appendChild(popupNode);
+ }
+ };
+
+// ---------------------------------------------------
+function createPaperArrayFromDefaults()
+{
+ var paperNames = ["letterSize", "legalSize", "exectiveSize", "a5Size", "a4Size", "a3Size", "a2Size", "a1Size", "a0Size"];
+ // var paperNames = ["&letterRadio.label;", "&legalRadio.label;", "&exectiveRadio.label;", "&a4Radio.label;", "&a3Radio.label;"];
+ var paperWidths = [ 8.5, 8.5, 7.25, 148.0, 210.0, 287.0, 420.0, 594.0, 841.0];
+ var paperHeights = [11.0, 14.0, 10.50, 210.0, 297.0, 420.0, 594.0, 841.0, 1189.0];
+ var paperInches = [true, true, true, false, false, false, false, false, false];
+
+ gPaperArray = new Array();
+
+ for (var i=0;i<paperNames.length;i++) {
+ var obj = {};
+ obj.name = paperNames[i];
+ obj.width = paperWidths[i];
+ obj.height = paperHeights[i];
+ obj.inches = paperInches[i];
+
+ /* Calculate the width/height in millimeters */
+ if (paperInches[i]) {
+ obj.width_mm = paperWidths[i] * 25.4;
+ obj.height_mm = paperHeights[i] * 25.4;
+ }
+ else {
+ obj.width_mm = paperWidths[i];
+ obj.height_mm = paperHeights[i];
+ }
+ gPaperArray[i] = obj;
+ }
+}
+
+// ---------------------------------------------------
+function createPaperArrayFromPrinterFeatures()
+{
+ var printername = gPrintSettings.printerName;
+ if (doDebug) {
+ dump("createPaperArrayFromPrinterFeatures for " + printername + ".\n");
+ }
+
+ gPaperArray = new Array();
+
+ var numPapers = gPrefs.getIntPref("print.tmp.printerfeatures." + printername + ".paper.count");
+
+ if (doDebug) {
+ dump("processing " + numPapers + " entries...\n");
+ }
+
+ for (var i=0;i<numPapers;i++) {
+ var obj = {};
+ obj.name = gPrefs.getCharPref("print.tmp.printerfeatures." + printername + ".paper." + i + ".name");
+ obj.width_mm = gPrefs.getIntPref("print.tmp.printerfeatures." + printername + ".paper." + i + ".width_mm");
+ obj.height_mm = gPrefs.getIntPref("print.tmp.printerfeatures." + printername + ".paper." + i + ".height_mm");
+ obj.inches = gPrefs.getBoolPref("print.tmp.printerfeatures." + printername + ".paper." + i + ".is_inch");
+
+ /* Calculate the width/height in paper's native units (either inches or millimeters) */
+ if (obj.inches) {
+ obj.width = obj.width_mm / 25.4;
+ obj.height = obj.height_mm / 25.4;
+ }
+ else {
+ obj.width = obj.width_mm;
+ obj.height = obj.height_mm;
+ }
+
+ gPaperArray[i] = obj;
+
+ if (doDebug) {
+ dump("paper index=" + i + ", name=" + obj.name + ", width=" + obj.width + ", height=" + obj.height + ".\n");
+ }
+ }
+}
+
+// ---------------------------------------------------
+function createPaperArray()
+{
+ if (isListOfPrinterFeaturesAvailable()) {
+ createPaperArrayFromPrinterFeatures();
+ }
+ else {
+ createPaperArrayFromDefaults();
+ }
+}
+
+// ---------------------------------------------------
+function createPaperSizeList(selectedInx)
+{
+ var selectElement = new paperListElement(dialog.paperList);
+ selectElement.clearPaperList();
+
+ selectElement.appendPaperNames(gPaperArray);
+
+ if (selectedInx > -1) {
+ selectElement.paperListElement.selectedIndex = selectedInx;
+ }
+
+ // dialog.paperList = selectElement;
+}
+
+// ---------------------------------------------------
+function loadDialog()
+{
+ var print_paper_unit = 0;
+ var print_paper_width = 0.0;
+ var print_paper_height = 0.0;
+ var print_paper_name = "";
+ var print_color = true;
+ var print_jobtitle = "";
+
+ gPrefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+
+ if (gPrintSettings) {
+ print_paper_unit = gPrintSettings.paperSizeUnit;
+ print_paper_width = gPrintSettings.paperWidth;
+ print_paper_height = gPrintSettings.paperHeight;
+ print_paper_name = gPrintSettings.paperName;
+ print_color = gPrintSettings.printInColor;
+ print_jobtitle = gPrintSettings.title;
+ }
+
+ if (doDebug) {
+ dump("loadDialog******************************\n");
+ dump("paperSizeType "+print_paper_unit+"\n");
+ dump("paperWidth "+print_paper_width+"\n");
+ dump("paperHeight "+print_paper_height+"\n");
+ dump("paperName "+print_paper_name+"\n");
+ dump("print_color "+print_color+"\n");
+ dump("print_jobtitle "+print_jobtitle+"\n");
+ }
+
+ createPaperArray();
+
+ var paperSelectedInx = 0;
+ for (var i=0;i<gPaperArray.length;i++) {
+ if (print_paper_name == gPaperArray[i].name) {
+ paperSelectedInx = i;
+ break;
+ }
+ }
+
+ if (doDebug) {
+ if (i == gPaperArray.length)
+ dump("loadDialog: No paper found.\n");
+ else
+ dump("loadDialog: found paper '"+gPaperArray[paperSelectedInx].name+"'.\n");
+ }
+
+ createPaperSizeList(paperSelectedInx);
+
+ /* Enable/disable and/or hide/unhide widgets based in the information
+ * whether the selected printer and/or print module supports the matching
+ * feature or not */
+ if (isListOfPrinterFeaturesAvailable()) {
+ // job title
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".can_change_jobtitle"))
+ dialog.jobTitleInput.removeAttribute("disabled");
+ else
+ dialog.jobTitleInput.setAttribute("disabled", "true");
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".supports_jobtitle_change"))
+ dialog.jobTitleGroup.removeAttribute("hidden");
+ else
+ dialog.jobTitleGroup.setAttribute("hidden", "true");
+
+ // paper size
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".can_change_paper_size"))
+ dialog.paperList.removeAttribute("disabled");
+ else
+ dialog.paperList.setAttribute("disabled", "true");
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".supports_paper_size_change"))
+ dialog.paperGroup.removeAttribute("hidden");
+ else
+ dialog.paperGroup.setAttribute("hidden", "true");
+
+ // color/grayscale radio
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".can_change_printincolor"))
+ dialog.colorRadioGroup.removeAttribute("disabled");
+ else
+ dialog.colorRadioGroup.setAttribute("disabled", "true");
+ if (gPrefs.getBoolPref("print.tmp.printerfeatures." + gPrintSettings.printerName + ".supports_printincolor_change"))
+ dialog.colorGroup.removeAttribute("hidden");
+ else
+ dialog.colorGroup.setAttribute("hidden", "true");
+ }
+
+ if (print_color) {
+ dialog.colorRadioGroup.selectedItem = dialog.colorRadio;
+ } else {
+ dialog.colorRadioGroup.selectedItem = dialog.grayRadio;
+ }
+
+ dialog.jobTitleInput.value = print_jobtitle;
+
+ dialog.topInput.value = gPrintSettings.edgeTop.toFixed(2);
+ dialog.bottomInput.value = gPrintSettings.edgeBottom.toFixed(2);
+ dialog.leftInput.value = gPrintSettings.edgeLeft.toFixed(2);
+ dialog.rightInput.value = gPrintSettings.edgeRight.toFixed(2);
+}
+
+// ---------------------------------------------------
+function onLoad()
+{
+ // Init dialog.
+ initDialog();
+
+ gPrintSettings = window.arguments[0].QueryInterface(gPrintSetInterface);
+ paramBlock = window.arguments[1].QueryInterface(Components.interfaces.nsIDialogParamBlock);
+
+ if (doDebug) {
+ if (gPrintSettings == null) alert("PrintSettings is null!");
+ if (paramBlock == null) alert("nsIDialogParam is null!");
+ }
+
+ // default return value is "cancel"
+ paramBlock.SetInt(0, 0);
+
+ loadDialog();
+}
+
+// ---------------------------------------------------
+function onAccept()
+{
+ var print_paper_unit = gPrintSettingsInterface.kPaperSizeInches;
+ var print_paper_width = 0.0;
+ var print_paper_height = 0.0;
+ var print_paper_name = "";
+
+ if (gPrintSettings != null) {
+ var paperSelectedInx = dialog.paperList.selectedIndex;
+ if (gPaperArray[paperSelectedInx].inches) {
+ print_paper_unit = gPrintSettingsInterface.kPaperSizeInches;
+ } else {
+ print_paper_unit = gPrintSettingsInterface.kPaperSizeMillimeters;
+ }
+ print_paper_width = gPaperArray[paperSelectedInx].width;
+ print_paper_height = gPaperArray[paperSelectedInx].height;
+ print_paper_name = gPaperArray[paperSelectedInx].name;
+
+ gPrintSettings.paperSizeUnit = print_paper_unit;
+ gPrintSettings.paperWidth = print_paper_width;
+ gPrintSettings.paperHeight = print_paper_height;
+ gPrintSettings.paperName = print_paper_name;
+
+ // save these out so they can be picked up by the device spec
+ gPrintSettings.printInColor = dialog.colorRadio.selected;
+ gPrintSettings.title = dialog.jobTitleInput.value;
+
+ gPrintSettings.edgeTop = dialog.topInput.value;
+ gPrintSettings.edgeBottom = dialog.bottomInput.value;
+ gPrintSettings.edgeLeft = dialog.leftInput.value;
+ gPrintSettings.edgeRight = dialog.rightInput.value;
+
+ if (doDebug) {
+ dump("onAccept******************************\n");
+ dump("paperSizeUnit "+print_paper_unit+"\n");
+ dump("paperWidth "+print_paper_width+"\n");
+ dump("paperHeight "+print_paper_height+"\n");
+ dump("paperName '"+print_paper_name+"'\n");
+
+ dump("printInColor "+gPrintSettings.printInColor+"\n");
+ }
+ } else {
+ dump("************ onAccept gPrintSettings: "+gPrintSettings+"\n");
+ }
+
+ if (paramBlock) {
+ // set return value to "ok"
+ paramBlock.SetInt(0, 1);
+ } else {
+ dump("*** FATAL ERROR: paramBlock missing\n");
+ }
+
+ return true;
+}
diff --git a/toolkit/components/printing/content/printjoboptions.xul b/toolkit/components/printing/content/printjoboptions.xul
new file mode 100644
index 0000000000..5726dfe3fd
--- /dev/null
+++ b/toolkit/components/printing/content/printjoboptions.xul
@@ -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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<!DOCTYPE dialog SYSTEM "chrome://global/locale/printjoboptions.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoad();"
+ ondialogaccept="return onAccept();"
+ title="&printJobOptions.title;"
+ persist="screenX screenY"
+ screenX="24" screenY="24">
+
+ <script type="application/javascript" src="chrome://global/content/printjoboptions.js"/>
+
+ <stringbundle id="printBundle" src="chrome://global/locale/printPageSetup.properties"/>
+
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row id="jobTitleGroup">
+ <hbox align="center" pack="end">
+ <label id="jobTitleLabel"
+ value="&jobTitleInput.label;"
+ accesskey="&jobTitleInput.accesskey;"
+ control="jobTitleInput"/>
+ </hbox>
+ <textbox id="jobTitleInput" flex="1"/>
+ </row>
+
+ <row id="paperGroup">
+ <hbox align="center" pack="end">
+ <label id="paperLabel"
+ value="&paperInput.label;"
+ accesskey="&paperInput.accesskey;"
+ control="paperList"/>
+ </hbox>
+ <menulist id="paperList" flex="1">
+ <menupopup/>
+ </menulist>
+ </row>
+
+ <row id="colorGroup">
+ <hbox align="center" pack="end">
+ <label control="colorRadioGroup" value="&colorGroup.label;"/>
+ </hbox>
+ <radiogroup id="colorRadioGroup" orient="horizontal">
+ <radio id="grayRadio"
+ label="&grayRadio.label;"
+ accesskey="&grayRadio.accesskey;"/>
+ <radio id="colorRadio"
+ label="&colorRadio.label;"
+ accesskey="&colorRadio.accesskey;"/>
+ </radiogroup>
+ </row>
+ </rows>
+ </grid>
+
+ <grid>
+ <columns>
+ <column/>
+ </columns>
+ <rows>
+ <row>
+ <groupbox flex="1">
+ <caption label="&edgeMarginInput.label;"/>
+ <hbox>
+ <hbox align="center">
+ <label id="topLabel"
+ value="&topInput.label;"
+ accesskey="&topInput.accesskey;"
+ control="topInput"/>
+ <textbox id="topInput" style="width:5em;" onkeyup="checkDouble(this, 0.5)"/>
+ </hbox>
+ <hbox align="center">
+ <label id="bottomLabel"
+ value="&bottomInput.label;"
+ accesskey="&bottomInput.accesskey;"
+ control="bottomInput"/>
+ <textbox id="bottomInput" style="width:5em;" onkeyup="checkDouble(this, 0.5)"/>
+ </hbox>
+ <hbox align="center">
+ <label id="leftLabel"
+ value="&leftInput.label;"
+ accesskey="&leftInput.accesskey;"
+ control="leftInput"/>
+ <textbox id="leftInput" style="width:5em;" onkeyup="checkDouble(this, 0.5)"/>
+ </hbox>
+ <hbox align="center">
+ <label id="rightLabel"
+ value="&rightInput.label;"
+ accesskey="&rightInput.accesskey;"
+ control="rightInput"/>
+ <textbox id="rightInput" style="width:5em;" onkeyup="checkDouble(this, 0.5)"/>
+ </hbox>
+ </hbox>
+ </groupbox>
+ </row>
+
+ </rows>
+ </grid>
+
+</dialog>
diff --git a/toolkit/components/printing/content/simplifyMode.css b/toolkit/components/printing/content/simplifyMode.css
new file mode 100644
index 0000000000..2a8706c750
--- /dev/null
+++ b/toolkit/components/printing/content/simplifyMode.css
@@ -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/. */
+
+/* This file defines specific rules for print preview when using simplify mode.
+ * These rules already exist on aboutReaderControls.css, however, we decoupled it
+ * from the original file so we don't need to load a bunch of extra queries that
+ * will not take effect when using the simplify page checkbox. This file defines
+ * styling for title and author on the header element. */
+
+.header > h1 {
+ font-size: 1.6em;
+ line-height: 1.25em;
+ margin: 30px 0;
+}
+
+.header > .credits {
+ font-size: 0.9em;
+ line-height: 1.48em;
+ margin: 0 0 30px 0;
+ font-style: italic;
+} \ No newline at end of file
diff --git a/toolkit/components/printing/jar.mn b/toolkit/components/printing/jar.mn
new file mode 100644
index 0000000000..40f9acf2b0
--- /dev/null
+++ b/toolkit/components/printing/jar.mn
@@ -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/.
+
+toolkit.jar:
+ content/global/printdialog.js (content/printdialog.js)
+ content/global/printdialog.xul (content/printdialog.xul)
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+ content/global/printjoboptions.js (content/printjoboptions.js)
+ content/global/printjoboptions.xul (content/printjoboptions.xul)
+#endif
+#endif
+ content/global/printPageSetup.js (content/printPageSetup.js)
+ content/global/printPageSetup.xul (content/printPageSetup.xul)
+ content/global/printPreviewBindings.xml (content/printPreviewBindings.xml)
+ content/global/printPreviewProgress.js (content/printPreviewProgress.js)
+ content/global/printPreviewProgress.xul (content/printPreviewProgress.xul)
+ content/global/printProgress.js (content/printProgress.js)
+ content/global/printProgress.xul (content/printProgress.xul)
+ content/global/printUtils.js (content/printUtils.js)
+ content/global/simplifyMode.css (content/simplifyMode.css)
diff --git a/toolkit/components/printing/moz.build b/toolkit/components/printing/moz.build
new file mode 100644
index 0000000000..dc8204b8ca
--- /dev/null
+++ b/toolkit/components/printing/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']
+
+BROWSER_CHROME_MANIFESTS += [
+ 'tests/browser.ini'
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Printing')
diff --git a/toolkit/components/printing/tests/browser.ini b/toolkit/components/printing/tests/browser.ini
new file mode 100644
index 0000000000..88d6bb454c
--- /dev/null
+++ b/toolkit/components/printing/tests/browser.ini
@@ -0,0 +1,2 @@
+[browser_page_change_print_original.js]
+skip-if = os == "mac"
diff --git a/toolkit/components/printing/tests/browser_page_change_print_original.js b/toolkit/components/printing/tests/browser_page_change_print_original.js
new file mode 100644
index 0000000000..5990a486b7
--- /dev/null
+++ b/toolkit/components/printing/tests/browser_page_change_print_original.js
@@ -0,0 +1,57 @@
+/**
+ * Verify that if the page contents change after print preview is initialized,
+ * and we re-initialize print preview (e.g. by changing page orientation),
+ * we still show (and will therefore print) the original contents.
+ */
+add_task(function* pp_after_orientation_change() {
+ const DATA_URI = `data:text/html,<script>window.onafterprint = function() { setTimeout("window.location = 'data:text/plain,REPLACED PAGE!'", 0); }</script><pre>INITIAL PAGE</pre>`;
+ // Can only do something if we have a print preview UI:
+ if (AppConstants.platform != "win" && AppConstants.platform != "linux") {
+ ok(true, "Can't test if there's no print preview.");
+ return;
+ }
+
+ // Ensure we get a browserStopped for this browser
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, DATA_URI, false, true);
+ let browserToPrint = tab.linkedBrowser;
+ let ppBrowser = PrintPreviewListener.getPrintPreviewBrowser();
+
+ // Get a promise now that resolves when the original tab's location changes.
+ let originalTabNavigated = BrowserTestUtils.browserStopped(browserToPrint);
+
+ // Enter print preview:
+ let printPreviewEntered = BrowserTestUtils.waitForMessage(ppBrowser.messageManager, "Printing:Preview:Entered");
+ document.getElementById("cmd_printPreview").doCommand();
+ yield printPreviewEntered;
+
+ // Assert that we are showing the original page
+ yield ContentTask.spawn(ppBrowser, null, function* () {
+ is(content.document.body.textContent, "INITIAL PAGE", "Should have initial page print previewed.");
+ });
+
+ yield originalTabNavigated;
+
+ // Change orientation and wait for print preview to re-enter:
+ let orient = PrintUtils.getPrintSettings().orientation;
+ let orientToSwitchTo = orient != Ci.nsIPrintSettings.kPortraitOrientation ?
+ "portrait" : "landscape";
+ let printPreviewToolbar = document.querySelector("toolbar[printpreview=true]");
+
+ printPreviewEntered = BrowserTestUtils.waitForMessage(ppBrowser.messageManager, "Printing:Preview:Entered");
+ printPreviewToolbar.orient(orientToSwitchTo);
+ yield printPreviewEntered;
+
+ // Check that we're still showing the original page.
+ yield ContentTask.spawn(ppBrowser, null, function* () {
+ is(content.document.body.textContent, "INITIAL PAGE", "Should still have initial page print previewed.");
+ });
+
+ // Check that the other tab is definitely showing the new page:
+ yield ContentTask.spawn(browserToPrint, null, function* () {
+ is(content.document.body.textContent, "REPLACED PAGE!", "Original page should have changed.");
+ });
+
+ PrintUtils.exitPrintPreview();
+
+ yield BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/privatebrowsing/PrivateBrowsing.manifest b/toolkit/components/privatebrowsing/PrivateBrowsing.manifest
new file mode 100644
index 0000000000..36b39bb85e
--- /dev/null
+++ b/toolkit/components/privatebrowsing/PrivateBrowsing.manifest
@@ -0,0 +1,2 @@
+component {a319b616-c45d-4037-8d86-01c592b5a9af} PrivateBrowsingTrackingProtectionWhitelist.js
+contract @mozilla.org/pbm-tp-whitelist;1 {a319b616-c45d-4037-8d86-01c592b5a9af}
diff --git a/toolkit/components/privatebrowsing/PrivateBrowsingTrackingProtectionWhitelist.js b/toolkit/components/privatebrowsing/PrivateBrowsingTrackingProtectionWhitelist.js
new file mode 100644
index 0000000000..5c1c27874d
--- /dev/null
+++ b/toolkit/components/privatebrowsing/PrivateBrowsingTrackingProtectionWhitelist.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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function PrivateBrowsingTrackingProtectionWhitelist() {
+ // The list of URIs explicitly excluded from tracking protection.
+ this._allowlist = [];
+
+ Services.obs.addObserver(this, "last-pb-context-exited", true);
+}
+
+PrivateBrowsingTrackingProtectionWhitelist.prototype = {
+ classID: Components.ID("{a319b616-c45d-4037-8d86-01c592b5a9af}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivateBrowsingTrackingProtectionWhitelist,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(PrivateBrowsingTrackingProtectionWhitelist),
+
+ /**
+ * Add the provided URI to the list of allowed tracking sites.
+ *
+ * @param uri nsIURI
+ * The URI to add to the list.
+ */
+ addToAllowList(uri) {
+ if (this._allowlist.indexOf(uri.spec) === -1) {
+ this._allowlist.push(uri.spec);
+ }
+ },
+
+ /**
+ * Remove the provided URI from the list of allowed tracking sites.
+ *
+ * @param uri nsIURI
+ * The URI to add to the list.
+ */
+ removeFromAllowList(uri) {
+ let index = this._allowlist.indexOf(uri.spec);
+ if (index !== -1) {
+ this._allowlist.splice(index, 1);
+ }
+ },
+
+ /**
+ * Check if the provided URI exists in the list of allowed tracking sites.
+ *
+ * @param uri nsIURI
+ * The URI to add to the list.
+ */
+ existsInAllowList(uri) {
+ return this._allowlist.indexOf(uri.spec) !== -1;
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic == "last-pb-context-exited") {
+ this._allowlist = [];
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PrivateBrowsingTrackingProtectionWhitelist]);
diff --git a/toolkit/components/privatebrowsing/moz.build b/toolkit/components/privatebrowsing/moz.build
new file mode 100644
index 0000000000..160539d08c
--- /dev/null
+++ b/toolkit/components/privatebrowsing/moz.build
@@ -0,0 +1,16 @@
+# -*- 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 += [
+ 'nsIPrivateBrowsingTrackingProtectionWhitelist.idl',
+]
+
+XPIDL_MODULE = 'privatebrowsing'
+
+EXTRA_COMPONENTS += [
+ 'PrivateBrowsing.manifest',
+ 'PrivateBrowsingTrackingProtectionWhitelist.js',
+]
diff --git a/toolkit/components/privatebrowsing/nsIPrivateBrowsingTrackingProtectionWhitelist.idl b/toolkit/components/privatebrowsing/nsIPrivateBrowsingTrackingProtectionWhitelist.idl
new file mode 100644
index 0000000000..d572b4e7e1
--- /dev/null
+++ b/toolkit/components/privatebrowsing/nsIPrivateBrowsingTrackingProtectionWhitelist.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * The Private Browsing Tracking Protection service checks a URI against an
+ * in-memory list of tracking sites.
+ */
+[scriptable, uuid(c77ddfac-6cd6-43a9-84e8-91682a1a7b18)]
+interface nsIPrivateBrowsingTrackingProtectionWhitelist : nsISupports
+{
+ /**
+ * Add a URI to the list of allowed tracking sites in Private Browsing mode
+ * (essentially a tracking whitelist). This operation will cause the URI to
+ * be registered if it does not currently exist. If it already exists, then
+ * the operation is essentially a no-op.
+ *
+ * @param uri the uri to add to the list
+ */
+ void addToAllowList(in nsIURI uri);
+
+ /**
+ * Remove a URI from the list of allowed tracking sites in Private Browsing
+ * mode (the tracking whitelist). If the URI is not already in the list,
+ * then the operation is essentially a no-op.
+ *
+ * @param uri the uri to remove from the list
+ */
+ void removeFromAllowList(in nsIURI uri);
+
+ /**
+ * Check if a URI exists in the list of allowed tracking sites in Private
+ * Browsing mode (the tracking whitelist).
+ *
+ * @param uri the uri to look for in the list
+ */
+ bool existsInAllowList(in nsIURI uri);
+};
+
+%{ C++
+#define NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID "@mozilla.org/pbm-tp-whitelist;1"
+%}
diff --git a/toolkit/components/processsingleton/ContentProcessSingleton.js b/toolkit/components/processsingleton/ContentProcessSingleton.js
new file mode 100644
index 0000000000..72f5803e12
--- /dev/null
+++ b/toolkit/components/processsingleton/ContentProcessSingleton.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+/*
+ * The message manager has an upper limit on message sizes that it can
+ * reliably forward to the parent so we limit the size of console log event
+ * messages that we forward here. The web console is local and receives the
+ * full console message, but addons subscribed to console event messages
+ * in the parent receive the truncated version. Due to fragmentation,
+ * messages as small as 1MB have resulted in IPC allocation failures on
+ * 32-bit platforms. To limit IPC allocation sizes, console.log messages
+ * with arguments with total size > MSG_MGR_CONSOLE_MAX_SIZE (bytes) have
+ * their arguments completely truncated. MSG_MGR_CONSOLE_VAR_SIZE is an
+ * approximation of how much space (in bytes) a JS non-string variable will
+ * require in the manager's implementation. For strings, we use 2 bytes per
+ * char. The console message URI and function name are limited to
+ * MSG_MGR_CONSOLE_INFO_MAX characters. We don't attempt to calculate
+ * the exact amount of space the message manager implementation will require
+ * for a given message so this is imperfect.
+ */
+const MSG_MGR_CONSOLE_MAX_SIZE = 1024 * 1024; // 1MB
+const MSG_MGR_CONSOLE_VAR_SIZE = 8;
+const MSG_MGR_CONSOLE_INFO_MAX = 1024;
+
+function ContentProcessSingleton() {}
+ContentProcessSingleton.prototype = {
+ classID: Components.ID("{ca2a8470-45c7-11e4-916c-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "app-startup": {
+ Services.obs.addObserver(this, "console-api-log-event", false);
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ cpmm.addMessageListener("DevTools:InitDebuggerServer", this);
+ break;
+ }
+ case "console-api-log-event": {
+ let consoleMsg = subject.wrappedJSObject;
+
+ let msgData = {
+ level: consoleMsg.level,
+ filename: consoleMsg.filename.substring(0, MSG_MGR_CONSOLE_INFO_MAX),
+ lineNumber: consoleMsg.lineNumber,
+ functionName: consoleMsg.functionName.substring(0,
+ MSG_MGR_CONSOLE_INFO_MAX),
+ timeStamp: consoleMsg.timeStamp,
+ arguments: [],
+ };
+
+ // We can't send objects over the message manager, so we sanitize
+ // them out, replacing those arguments with "<unavailable>".
+ let unavailString = "<unavailable>";
+ let unavailStringLength = unavailString.length * 2; // 2-bytes per char
+
+ // When the sum of argument sizes reaches MSG_MGR_CONSOLE_MAX_SIZE,
+ // replace all arguments with "<truncated>".
+ let totalArgLength = 0;
+
+ // Walk through the arguments, checking the type and size.
+ for (let arg of consoleMsg.arguments) {
+ if ((typeof arg == "object" || typeof arg == "function") &&
+ arg !== null) {
+ arg = unavailString;
+ totalArgLength += unavailStringLength;
+ } else if (typeof arg == "string") {
+ totalArgLength += arg.length * 2; // 2-bytes per char
+ } else {
+ totalArgLength += MSG_MGR_CONSOLE_VAR_SIZE;
+ }
+
+ if (totalArgLength <= MSG_MGR_CONSOLE_MAX_SIZE) {
+ msgData.arguments.push(arg);
+ } else {
+ // arguments take up too much space
+ msgData.arguments = ["<truncated>"];
+ break;
+ }
+ }
+
+ cpmm.sendAsyncMessage("Console:Log", msgData);
+ break;
+ }
+
+ case "xpcom-shutdown":
+ Services.obs.removeObserver(this, "console-api-log-event");
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ cpmm.removeMessageListener("DevTools:InitDebuggerServer", this);
+ break;
+ }
+ },
+
+ receiveMessage: function (message) {
+ // load devtools component on-demand
+ // Only reply if we are in a real content process
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ let {init} = Cu.import("resource://devtools/server/content-server.jsm", {});
+ init(message);
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentProcessSingleton]);
diff --git a/toolkit/components/processsingleton/MainProcessSingleton.js b/toolkit/components/processsingleton/MainProcessSingleton.js
new file mode 100644
index 0000000000..82beff5083
--- /dev/null
+++ b/toolkit/components/processsingleton/MainProcessSingleton.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const { utils: Cu, interfaces: Ci, classes: Cc, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+function MainProcessSingleton() {}
+MainProcessSingleton.prototype = {
+ classID: Components.ID("{0636a680-45cb-11e4-916c-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ logConsoleMessage: function(message) {
+ let logMsg = message.data;
+ logMsg.wrappedJSObject = logMsg;
+ Services.obs.notifyObservers(logMsg, "console-api-log-event", null);
+ },
+
+ // Called when a webpage calls window.external.AddSearchProvider
+ addSearchEngine: function({ target: browser, data: { pageURL, engineURL } }) {
+ pageURL = NetUtil.newURI(pageURL);
+ engineURL = NetUtil.newURI(engineURL, null, pageURL);
+
+ let iconURL;
+ let tabbrowser = browser.getTabBrowser();
+ if (browser.mIconURL && (!tabbrowser || tabbrowser.shouldLoadFavIcon(pageURL)))
+ iconURL = NetUtil.newURI(browser.mIconURL);
+
+ try {
+ // Make sure the URLs are HTTP, HTTPS, or FTP.
+ let isWeb = ["https", "http", "ftp"];
+
+ if (isWeb.indexOf(engineURL.scheme) < 0)
+ throw "Unsupported search engine URL: " + engineURL;
+
+ if (iconURL && isWeb.indexOf(iconURL.scheme) < 0)
+ throw "Unsupported search icon URL: " + iconURL;
+ }
+ catch (ex) {
+ Cu.reportError("Invalid argument passed to window.external.AddSearchProvider: " + 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(browser.ownerDocument.defaultView).alert(title, msg);
+ return;
+ }
+
+ Services.search.init(function(status) {
+ if (status != Cr.NS_OK)
+ return;
+
+ Services.search.addEngine(engineURL.spec, null, iconURL ? iconURL.spec : null, true);
+ })
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "app-startup": {
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+
+ // Load this script early so that console.* is initialized
+ // before other frame scripts.
+ Services.mm.loadFrameScript("chrome://global/content/browser-content.js", true);
+ Services.ppmm.loadProcessScript("chrome://global/content/process-content.js", true);
+ Services.ppmm.addMessageListener("Console:Log", this.logConsoleMessage);
+ Services.mm.addMessageListener("Search:AddEngine", this.addSearchEngine);
+ break;
+ }
+
+ case "xpcom-shutdown":
+ Services.ppmm.removeMessageListener("Console:Log", this.logConsoleMessage);
+ Services.mm.removeMessageListener("Search:AddEngine", this.addSearchEngine);
+ break;
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MainProcessSingleton]);
diff --git a/toolkit/components/processsingleton/ProcessSingleton.manifest b/toolkit/components/processsingleton/ProcessSingleton.manifest
new file mode 100644
index 0000000000..7a882ed7b6
--- /dev/null
+++ b/toolkit/components/processsingleton/ProcessSingleton.manifest
@@ -0,0 +1,7 @@
+component {0636a680-45cb-11e4-916c-0800200c9a66} MainProcessSingleton.js process=main
+contract @mozilla.org/main-process-singleton;1 {0636a680-45cb-11e4-916c-0800200c9a66} process=main
+category app-startup MainProcessSingleton service,@mozilla.org/main-process-singleton;1 process=main
+
+component {ca2a8470-45c7-11e4-916c-0800200c9a66} ContentProcessSingleton.js process=content
+contract @mozilla.org/content-process-singleton;1 {ca2a8470-45c7-11e4-916c-0800200c9a66} process=content
+category app-startup ContentProcessSingleton service,@mozilla.org/content-process-singleton;1 process=content
diff --git a/toolkit/components/processsingleton/moz.build b/toolkit/components/processsingleton/moz.build
new file mode 100644
index 0000000000..75610e36eb
--- /dev/null
+++ b/toolkit/components/processsingleton/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/.
+
+EXTRA_COMPONENTS += [
+ 'ContentProcessSingleton.js',
+ 'MainProcessSingleton.js',
+ 'ProcessSingleton.manifest',
+]
diff --git a/toolkit/components/promiseworker/PromiseWorker.jsm b/toolkit/components/promiseworker/PromiseWorker.jsm
new file mode 100644
index 0000000000..0c6e054a28
--- /dev/null
+++ b/toolkit/components/promiseworker/PromiseWorker.jsm
@@ -0,0 +1,390 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A wrapper around ChromeWorker with extended capabilities designed
+ * to simplify main thread-to-worker thread asynchronous function calls.
+ *
+ * This wrapper:
+ * - groups requests and responses as a method `post` that returns a `Promise`;
+ * - ensures that exceptions thrown on the worker thread are correctly deserialized;
+ * - provides some utilities for benchmarking various operations.
+ *
+ * Generally, you should use PromiseWorker.jsm along with its worker-side
+ * counterpart PromiseWorker.js.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["BasePromiseWorker"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * An implementation of queues (FIFO).
+ *
+ * The current implementation uses one array, runs in O(n ^ 2), and is optimized
+ * for the case in which queues are generally short.
+ */
+function Queue() {
+ this._array = [];
+}
+Queue.prototype = {
+ pop: function pop() {
+ return this._array.shift();
+ },
+ push: function push(x) {
+ return this._array.push(x);
+ },
+ isEmpty: function isEmpty() {
+ return this._array.length == 0;
+ }
+};
+
+/**
+ * Constructors for decoding standard exceptions received from the
+ * worker.
+ */
+const EXCEPTION_CONSTRUCTORS = {
+ EvalError: function(error) {
+ let result = new EvalError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ InternalError: function(error) {
+ let result = new InternalError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ RangeError: function(error) {
+ let result = new RangeError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ ReferenceError: function(error) {
+ let result = new ReferenceError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ SyntaxError: function(error) {
+ let result = new SyntaxError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ TypeError: function(error) {
+ let result = new TypeError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ URIError: function(error) {
+ let result = new URIError(error.message, error.fileName, error.lineNumber);
+ result.stack = error.stack;
+ return result;
+ },
+ StopIteration: function() {
+ return StopIteration;
+ }
+};
+
+/**
+ * An object responsible for dispatching messages to a chrome worker
+ * and routing the responses.
+ *
+ * Instances of this constructor who need logging may provide a method
+ * `log: function(...args) { ... }` in charge of printing out (or
+ * discarding) logs.
+ *
+ * Instances of this constructor may add exception handlers to
+ * `this.ExceptionHandlers`, if they need to handle custom exceptions.
+ *
+ * @param {string} url The url containing the source code for this worker,
+ * as in constructor ChromeWorker.
+ *
+ * @constructor
+ */
+this.BasePromiseWorker = function(url) {
+ if (typeof url != "string") {
+ throw new TypeError("Expecting a string");
+ }
+ this._url = url;
+
+ /**
+ * A set of methods, with the following
+ *
+ * ConstructorName: function({message, fileName, lineNumber}) {
+ * // Construct a new instance of ConstructorName based on
+ * // `message`, `fileName`, `lineNumber`
+ * }
+ *
+ * By default, this covers EvalError, InternalError, RangeError,
+ * ReferenceError, SyntaxError, TypeError, URIError, StopIteration.
+ */
+ this.ExceptionHandlers = Object.create(EXCEPTION_CONSTRUCTORS);
+
+ /**
+ * The queue of deferred, waiting for the completion of their
+ * respective job by the worker.
+ *
+ * Each item in the list may contain an additional field |closure|,
+ * used to store strong references to value that must not be
+ * garbage-collected before the reply has been received (e.g.
+ * arrays).
+ *
+ * @type {Queue<{deferred:deferred, closure:*=}>}
+ */
+ this._queue = new Queue();
+
+ /**
+ * The number of the current message.
+ *
+ * Used for debugging purposes.
+ */
+ this._id = 0;
+
+ /**
+ * The instant at which the worker was launched.
+ */
+ this.launchTimeStamp = null;
+
+ /**
+ * Timestamps provided by the worker for statistics purposes.
+ */
+ this.workerTimeStamps = null;
+};
+this.BasePromiseWorker.prototype = {
+ log: function() {
+ // By Default, ignore all logs.
+ },
+
+ /**
+ * Instantiate the worker lazily.
+ */
+ get _worker() {
+ delete this._worker;
+ let worker = new ChromeWorker(this._url);
+ Object.defineProperty(this, "_worker", {value:
+ worker
+ });
+
+ // We assume that we call to _worker for the purpose of calling
+ // postMessage().
+ this.launchTimeStamp = Date.now();
+
+ /**
+ * Receive errors that have been serialized by the built-in mechanism
+ * of DOM/Chrome Workers.
+ *
+ * PromiseWorker.js knows how to serialize a number of errors
+ * without losing information. These are treated by
+ * |worker.onmessage|. However, for other errors, we rely on
+ * DOM's mechanism for serializing errors, which transmits these
+ * errors through |worker.onerror|.
+ *
+ * @param {Error} error Some JS error.
+ */
+ worker.onerror = error => {
+ this.log("Received uncaught error from worker", error.message, error.filename, error.lineno);
+ error.preventDefault();
+ let {deferred} = this._queue.pop();
+ deferred.reject(error);
+ };
+
+ /**
+ * Receive messages from the worker, propagate them to the listeners.
+ *
+ * Messages must have one of the following shapes:
+ * - {ok: some_value} in case of success
+ * - {fail: some_error} in case of error, where
+ * some_error is an instance of |PromiseWorker.WorkerError|
+ *
+ * Messages may also contain a field |id| to help
+ * with debugging.
+ *
+ * Messages may also optionally contain a field |durationMs|, holding
+ * the duration of the function call in milliseconds.
+ *
+ * @param {*} msg The message received from the worker.
+ */
+ worker.onmessage = msg => {
+ this.log("Received message from worker", msg.data);
+ let handler = this._queue.pop();
+ let deferred = handler.deferred;
+ let data = msg.data;
+ if (data.id != handler.id) {
+ throw new Error("Internal error: expecting msg " + handler.id + ", " +
+ " got " + data.id + ": " + JSON.stringify(msg.data));
+ }
+ if ("timeStamps" in data) {
+ this.workerTimeStamps = data.timeStamps;
+ }
+ if ("ok" in data) {
+ // Pass the data to the listeners.
+ deferred.resolve(data);
+ } else if ("fail" in data) {
+ // We have received an error that was serialized by the
+ // worker.
+ deferred.reject(new WorkerError(data.fail));
+ }
+ };
+ return worker;
+ },
+
+ /**
+ * Post a message to a worker.
+ *
+ * @param {string} fun The name of the function to call.
+ * @param {Array} args The arguments to pass to `fun`. If any
+ * of the arguments is a Promise, it is resolved before posting the
+ * message. If any of the arguments needs to be transfered instead
+ * of copied, this may be specified by making the argument an instance
+ * of `BasePromiseWorker.Meta` or by using the `transfers` argument.
+ * By convention, the last argument may be an object `options`
+ * with some of the following fields:
+ * - {number|null} outExecutionDuration A parameter to be filled with the
+ * duration of the off main thread execution for this call.
+ * @param {*=} closure An object holding references that should not be
+ * garbage-collected before the message treatment is complete.
+ * @param {Array=} transfers An array of objects that should be transfered
+ * to the worker instead of being copied. If any of the objects is a Promise,
+ * it is resolved before posting the message.
+ *
+ * @return {promise}
+ */
+ post: function(fun, args, closure, transfers) {
+ return Task.spawn(function* postMessage() {
+ // Normalize in case any of the arguments is a promise
+ if (args) {
+ args = yield Promise.resolve(Promise.all(args));
+ }
+ if (transfers) {
+ transfers = yield Promise.resolve(Promise.all(transfers));
+ } else {
+ transfers = [];
+ }
+
+ if (args) {
+ // Extract `Meta` data
+ args = args.map(arg => {
+ if (arg instanceof BasePromiseWorker.Meta) {
+ if (arg.meta && "transfers" in arg.meta) {
+ transfers.push(...arg.meta.transfers);
+ }
+ return arg.data;
+ }
+ return arg;
+ });
+ }
+
+ let id = ++this._id;
+ let message = {fun: fun, args: args, id: id};
+ this.log("Posting message", message);
+ try {
+ this._worker.postMessage(message, ...[transfers]);
+ } catch (ex) {
+ if (typeof ex == "number") {
+ this.log("Could not post message", message, "due to xpcom error", ex);
+ // handle raw xpcom errors (see eg bug 961317)
+ throw new Components.Exception("Error in postMessage", ex);
+ }
+
+ this.log("Could not post message", message, "due to error", ex);
+ throw ex;
+ }
+
+ let deferred = Promise.defer();
+ this._queue.push({deferred:deferred, closure: closure, id: id});
+ this.log("Message posted");
+
+ let reply;
+ try {
+ this.log("Expecting reply");
+ reply = yield deferred.promise;
+ } catch (error) {
+ this.log("Got error", error);
+ reply = error;
+
+ if (error instanceof WorkerError) {
+ // We know how to deserialize most well-known errors
+ throw this.ExceptionHandlers[error.data.exn](error.data);
+ }
+
+ if (error instanceof ErrorEvent) {
+ // Other errors get propagated as instances of ErrorEvent
+ this.log("Error serialized by DOM", error.message, error.filename, error.lineno);
+ throw new Error(error.message, error.filename, error.lineno);
+ }
+
+ // We don't know about this kind of error
+ throw error;
+ }
+
+ // By convention, the last argument may be an object `options`.
+ let options = null;
+ if (args) {
+ options = args[args.length - 1];
+ }
+
+ // Check for duration and return result.
+ if (!options ||
+ typeof options !== "object" ||
+ !("outExecutionDuration" in options)) {
+ return reply.ok;
+ }
+ // If reply.durationMs is not present, just return the result,
+ // without updating durations (there was an error in the method
+ // dispatch).
+ if (!("durationMs" in reply)) {
+ return reply.ok;
+ }
+ // Bug 874425 demonstrates that two successive calls to Date.now()
+ // can actually produce an interval with negative duration.
+ // We assume that this is due to an operation that is so short
+ // that Date.now() is not monotonic, so we round this up to 0.
+ let durationMs = Math.max(0, reply.durationMs);
+ // Accumulate (or initialize) outExecutionDuration
+ if (typeof options.outExecutionDuration == "number") {
+ options.outExecutionDuration += durationMs;
+ } else {
+ options.outExecutionDuration = durationMs;
+ }
+ return reply.ok;
+
+ }.bind(this));
+ }
+};
+
+/**
+ * An error that has been serialized by the worker.
+ *
+ * @constructor
+ */
+function WorkerError(data) {
+ this.data = data;
+}
+
+/**
+ * A constructor used to send data to the worker thread while
+ * with special treatment (e.g. transmitting data instead of
+ * copying it).
+ *
+ * @param {object=} data The data to send to the caller thread.
+ * @param {object=} meta Additional instructions, as an object
+ * that may contain the following fields:
+ * - {Array} transfers An array of objects that should be transferred
+ * instead of being copied.
+ *
+ * @constructor
+ */
+this.BasePromiseWorker.Meta = function(data, meta) {
+ this.data = data;
+ this.meta = meta;
+};
diff --git a/toolkit/components/promiseworker/moz.build b/toolkit/components/promiseworker/moz.build
new file mode 100644
index 0000000000..44a90e6792
--- /dev/null
+++ b/toolkit/components/promiseworker/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/.
+
+DIRS += [
+ 'worker'
+]
+
+EXTRA_JS_MODULES += [
+ 'PromiseWorker.jsm',
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Async Tooling')
diff --git a/toolkit/components/promiseworker/tests/xpcshell/.eslintrc.js b/toolkit/components/promiseworker/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/promiseworker/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/promiseworker/tests/xpcshell/data/chrome.manifest b/toolkit/components/promiseworker/tests/xpcshell/data/chrome.manifest
new file mode 100644
index 0000000000..9e5dd29b22
--- /dev/null
+++ b/toolkit/components/promiseworker/tests/xpcshell/data/chrome.manifest
@@ -0,0 +1 @@
+content promiseworker ./
diff --git a/toolkit/components/promiseworker/tests/xpcshell/data/worker.js b/toolkit/components/promiseworker/tests/xpcshell/data/worker.js
new file mode 100644
index 0000000000..b4750788b5
--- /dev/null
+++ b/toolkit/components/promiseworker/tests/xpcshell/data/worker.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Trivial worker definition
+
+importScripts("resource://gre/modules/workers/require.js");
+var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+var worker = new PromiseWorker.AbstractWorker();
+worker.dispatch = function(method, args = []) {
+ return Agent[method](...args);
+},
+worker.postMessage = function(...args) {
+ self.postMessage(...args);
+};
+worker.close = function() {
+ self.close();
+};
+worker.log = function(...args) {
+ dump("Worker: " + args.join(" ") + "\n");
+};
+self.addEventListener("message", msg => worker.handleMessage(msg));
+
+var Agent = {
+ bounce: function(...args) {
+ return args;
+ },
+
+ throwError: function(msg, ...args) {
+ throw new Error(msg);
+ },
+};
diff --git a/toolkit/components/promiseworker/tests/xpcshell/test_Promise.js b/toolkit/components/promiseworker/tests/xpcshell/test_Promise.js
new file mode 100644
index 0000000000..70f49e92e0
--- /dev/null
+++ b/toolkit/components/promiseworker/tests/xpcshell/test_Promise.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+// Worker must be loaded from a chrome:// uri, not a file://
+// uri, so we first need to load it.
+
+var WORKER_SOURCE_URI = "chrome://promiseworker/content/worker.js";
+do_load_manifest("data/chrome.manifest");
+var worker = new BasePromiseWorker(WORKER_SOURCE_URI);
+worker.log = function(...args) {
+ do_print("Controller: " + args.join(" "));
+};
+
+// Test that simple messages work
+add_task(function* test_simple_args() {
+ let message = ["test_simple_args", Math.random()];
+ let result = yield worker.post("bounce", message);
+ Assert.equal(JSON.stringify(result), JSON.stringify(message));
+});
+
+// Test that it works when we don't provide a message
+add_task(function* test_no_args() {
+ let result = yield worker.post("bounce");
+ Assert.equal(JSON.stringify(result), JSON.stringify([]));
+});
+
+// Test that messages with promise work
+add_task(function* test_promise_args() {
+ let message = ["test_promise_args", Promise.resolve(Math.random())];
+ let stringified = JSON.stringify((yield Promise.resolve(Promise.all(message))));
+ let result = yield worker.post("bounce", message);
+ Assert.equal(JSON.stringify(result), stringified);
+});
+
+// Test that messages with delayed promise work
+add_task(function* test_delayed_promise_args() {
+ let promise = new Promise(resolve => setTimeout(() => resolve(Math.random()), 10));
+ let message = ["test_delayed_promise_args", promise];
+ let stringified = JSON.stringify((yield Promise.resolve(Promise.all(message))));
+ let result = yield worker.post("bounce", message);
+ Assert.equal(JSON.stringify(result), stringified);
+});
+
+// Test that messages with rejected promise cause appropriate errors
+add_task(function* test_rejected_promise_args() {
+ let error = new Error();
+ let message = ["test_promise_args", Promise.reject(error)];
+ try {
+ yield worker.post("bounce", message);
+ do_throw("I shound have thrown an error by now");
+ } catch (ex) {
+ if (ex != error)
+ throw ex;
+ do_print("I threw the right error");
+ }
+});
+
+// Test that we can transfer to the worker using argument `transfer`
+add_task(function* test_transfer_args() {
+ let array = new Uint8Array(4);
+ for (let i = 0; i < 4; ++i) {
+ array[i] = i;
+ }
+ Assert.equal(array.buffer.byteLength, 4, "The buffer is not detached yet");
+
+ let result = (yield worker.post("bounce", [array.buffer], [], [array.buffer]))[0];
+
+ // Check that the buffer has been sent
+ Assert.equal(array.buffer.byteLength, 0, "The buffer has been detached");
+
+ // Check that the result is correct
+ Assert.equal(result.byteLength, 4, "The result has the right size");
+ let array2 = new Uint8Array(result);
+ for (let i = 0; i < 4; ++i) {
+ Assert.equal(array2[i], i);
+ }
+});
+
+// Test that we can transfer to the worker using an instance of `Meta`
+add_task(function* test_transfer_with_meta() {
+ let array = new Uint8Array(4);
+ for (let i = 0; i < 4; ++i) {
+ array[i] = i;
+ }
+ Assert.equal(array.buffer.byteLength, 4, "The buffer is not detached yet");
+
+ let message = new BasePromiseWorker.Meta(array, {transfers: [array.buffer]});
+ let result = (yield worker.post("bounce", [message]))[0];
+
+ // Check that the buffer has been sent
+ Assert.equal(array.buffer.byteLength, 0, "The buffer has been detached");
+
+ // Check that the result is correct
+ Assert.equal(Object.prototype.toString.call(result), "[object Uint8Array]",
+ "The result appears to be a Typed Array");
+ Assert.equal(result.byteLength, 4, "The result has the right size");
+
+ for (let i = 0; i < 4; ++i) {
+ Assert.equal(result[i], i);
+ }
+});
+
+add_task(function* test_throw_error() {
+ try {
+ yield worker.post("throwError", ["error message"]);
+ Assert.ok(false, "should have thrown");
+ } catch (ex) {
+ Assert.equal(ex.message, "Error: error message");
+ }
+});
diff --git a/toolkit/components/promiseworker/tests/xpcshell/xpcshell.ini b/toolkit/components/promiseworker/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..1efcd8c9e0
--- /dev/null
+++ b/toolkit/components/promiseworker/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head=
+tail=
+skip-if = toolkit == 'android'
+support-files=
+ data/worker.js
+ data/chrome.manifest
+
+[test_Promise.js]
diff --git a/toolkit/components/promiseworker/worker/PromiseWorker.js b/toolkit/components/promiseworker/worker/PromiseWorker.js
new file mode 100644
index 0000000000..ba4408c1a3
--- /dev/null
+++ b/toolkit/components/promiseworker/worker/PromiseWorker.js
@@ -0,0 +1,206 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A wrapper around `self` with extended capabilities designed
+ * to simplify main thread-to-worker thread asynchronous function calls.
+ *
+ * This wrapper:
+ * - groups requests and responses as a method `post` that returns a `Promise`;
+ * - ensures that exceptions thrown on the worker thread are correctly serialized;
+ * - provides some utilities for benchmarking various operations.
+ *
+ * Generally, you should use PromiseWorker.js along with its main thread-side
+ * counterpart PromiseWorker.jsm.
+ */
+
+"use strict";
+
+if (typeof Components != "undefined") {
+ throw new Error("This module is meant to be used from the worker thread");
+}
+if (typeof require == "undefined" || typeof module == "undefined") {
+ throw new Error("this module is meant to be imported using the implementation of require() at resource://gre/modules/workers/require.js");
+}
+
+importScripts("resource://gre/modules/workers/require.js");
+
+/**
+ * Built-in JavaScript exceptions that may be serialized without
+ * loss of information.
+ */
+const EXCEPTION_NAMES = {
+ EvalError: "EvalError",
+ InternalError: "InternalError",
+ RangeError: "RangeError",
+ ReferenceError: "ReferenceError",
+ SyntaxError: "SyntaxError",
+ TypeError: "TypeError",
+ URIError: "URIError",
+};
+
+/**
+ * A constructor used to return data to the caller thread while
+ * also executing some specific treatment (e.g. shutting down
+ * the current thread, transmitting data instead of copying it).
+ *
+ * @param {object=} data The data to return to the caller thread.
+ * @param {object=} meta Additional instructions, as an object
+ * that may contain the following fields:
+ * - {bool} shutdown If |true|, shut down the current thread after
+ * having sent the result.
+ * - {Array} transfers An array of objects that should be transferred
+ * instead of being copied.
+ *
+ * @constructor
+ */
+function Meta(data, meta) {
+ this.data = data;
+ this.meta = meta;
+}
+exports.Meta = Meta;
+
+/**
+ * Base class for a worker.
+ *
+ * Derived classes are expected to provide the following methods:
+ * {
+ * dispatch: function(method, args) {
+ * // Dispatch a call to method `method` with args `args`
+ * },
+ * log: function(...msg) {
+ * // Log (or discard) messages (optional)
+ * },
+ * postMessage: function(message, ...transfers) {
+ * // Post a message to the main thread
+ * },
+ * close: function() {
+ * // Close the worker
+ * }
+ * }
+ *
+ * By default, the AbstractWorker is not connected to a message port,
+ * hence will not receive anything.
+ *
+ * To connect it, use `onmessage`, as follows:
+ * self.addEventListener("message", msg => myWorkerInstance.handleMessage(msg));
+ */
+function AbstractWorker(agent) {
+ this._agent = agent;
+}
+AbstractWorker.prototype = {
+ // Default logger: discard all messages
+ log: function() {
+ },
+
+ /**
+ * Handle a message.
+ */
+ handleMessage: function(msg) {
+ let data = msg.data;
+ this.log("Received message", data);
+ let id = data.id;
+
+ let start;
+ let options;
+ if (data.args) {
+ options = data.args[data.args.length - 1];
+ }
+ // If |outExecutionDuration| option was supplied, start measuring the
+ // duration of the operation.
+ if (options && typeof options === "object" && "outExecutionDuration" in options) {
+ start = Date.now();
+ }
+
+ let result;
+ let exn;
+ let durationMs;
+ let method = data.fun;
+ try {
+ this.log("Calling method", method);
+ result = this.dispatch(method, data.args);
+ this.log("Method", method, "succeeded");
+ } catch (ex) {
+ exn = ex;
+ this.log("Error while calling agent method", method, exn, exn.moduleStack || exn.stack || "");
+ }
+
+ if (start) {
+ // Record duration
+ durationMs = Date.now() - start;
+ this.log("Method took", durationMs, "ms");
+ }
+
+ // Now, post a reply, possibly as an uncaught error.
+ // We post this message from outside the |try ... catch| block
+ // to avoid capturing errors that take place during |postMessage| and
+ // built-in serialization.
+ if (!exn) {
+ this.log("Sending positive reply", result, "id is", id);
+ if (result instanceof Meta) {
+ if ("transfers" in result.meta) {
+ // Take advantage of zero-copy transfers
+ this.postMessage({ok: result.data, id: id, durationMs: durationMs},
+ result.meta.transfers);
+ } else {
+ this.postMessage({ok: result.data, id:id, durationMs: durationMs});
+ }
+ if (result.meta.shutdown || false) {
+ // Time to close the worker
+ this.close();
+ }
+ } else {
+ this.postMessage({ok: result, id:id, durationMs: durationMs});
+ }
+ } else if (exn.constructor.name in EXCEPTION_NAMES) {
+ // Rather than letting the DOM mechanism [de]serialize built-in
+ // JS errors, which loses lots of information (in particular,
+ // the constructor name, the moduleName and the moduleStack),
+ // we [de]serialize them manually with a little more care.
+ this.log("Sending back exception", exn.constructor.name, "id is", id);
+ let error = {
+ exn: exn.constructor.name,
+ message: exn.message,
+ fileName: exn.moduleName || exn.fileName,
+ lineNumber: exn.lineNumber,
+ stack: exn.moduleStack
+ };
+ this.postMessage({fail: error, id: id, durationMs: durationMs});
+ } else if (exn == StopIteration) {
+ // StopIteration is a well-known singleton, and requires a
+ // slightly different treatment.
+ this.log("Sending back StopIteration, id is", id);
+ let error = {
+ exn: "StopIteration"
+ };
+ this.postMessage({fail: error, id: id, durationMs: durationMs});
+ } else if ("toMsg" in exn) {
+ // Extension mechanism for exception [de]serialization. We
+ // assume that any exception with a method `toMsg()` knows how
+ // to serialize itself. The other side is expected to have
+ // registered a deserializer using the `ExceptionHandlers`
+ // object.
+ this.log("Sending back an error that knows how to serialize itself", exn, "id is", id);
+ let msg = exn.toMsg();
+ this.postMessage({fail: msg, id:id, durationMs: durationMs});
+ } else {
+ // If we encounter an exception for which we have no
+ // serialization mechanism in place, we have no choice but to
+ // let the DOM handle said [de]serialization. We can just
+ // attempt to mitigate the data loss by injecting `moduleName` and
+ // `moduleStack`.
+ this.log("Sending back regular error", exn, exn.moduleStack || exn.stack, "id is", id);
+
+ try {
+ // Attempt to introduce human-readable filename and stack
+ exn.filename = exn.moduleName;
+ exn.stack = exn.moduleStack;
+ } catch (_) {
+ // Nothing we can do
+ }
+ throw exn;
+ }
+ }
+};
+exports.AbstractWorker = AbstractWorker;
diff --git a/toolkit/components/promiseworker/worker/moz.build b/toolkit/components/promiseworker/worker/moz.build
new file mode 100644
index 0000000000..305d498382
--- /dev/null
+++ b/toolkit/components/promiseworker/worker/moz.build
@@ -0,0 +1,9 @@
+# -*- 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_JS_MODULES.workers = [
+ 'PromiseWorker.js',
+]
diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css
new file mode 100644
index 0000000000..89f88db7aa
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.css
@@ -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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
+
+#infoContainer {
+ max-width: 45em;
+}
+
+#info\.body {
+ -moz-user-focus: normal;
+ -moz-user-select: text;
+ cursor: text !important;
+ white-space: pre-wrap;
+ unicode-bidi: plaintext;
+}
+
+#loginLabel, #password1Label {
+ text-align: right;
+}
+
diff --git a/toolkit/components/prompts/content/commonDialog.js b/toolkit/components/prompts/content/commonDialog.js
new file mode 100644
index 0000000000..ef46866542
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.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/. */
+
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/CommonDialog.jsm");
+
+var propBag, args, Dialog;
+
+function commonDialogOnLoad() {
+ propBag = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag);
+ // Convert to a JS object
+ args = {};
+ let propEnum = propBag.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ args[prop.name] = prop.value;
+ }
+
+ let dialog = document.documentElement;
+
+ let ui = {
+ prompt : window,
+ loginContainer : document.getElementById("loginContainer"),
+ loginTextbox : document.getElementById("loginTextbox"),
+ loginLabel : document.getElementById("loginLabel"),
+ password1Container : document.getElementById("password1Container"),
+ password1Textbox : document.getElementById("password1Textbox"),
+ password1Label : document.getElementById("password1Label"),
+ infoBody : document.getElementById("info.body"),
+ infoTitle : document.getElementById("info.title"),
+ infoIcon : document.getElementById("info.icon"),
+ checkbox : document.getElementById("checkbox"),
+ checkboxContainer : document.getElementById("checkboxContainer"),
+ button3 : dialog.getButton("extra2"),
+ button2 : dialog.getButton("extra1"),
+ button1 : dialog.getButton("cancel"),
+ button0 : dialog.getButton("accept"),
+ focusTarget : window,
+ };
+
+ // limit the dialog to the screen width
+ document.getElementById("filler").maxWidth = screen.availWidth;
+
+ Dialog = new CommonDialog(args, ui);
+ Dialog.onLoad(dialog);
+ // resize the window to the content
+ window.sizeToContent();
+ window.getAttention();
+}
+
+function commonDialogOnUnload() {
+ // Convert args back into property bag
+ for (let propName in args)
+ propBag.setProperty(propName, args[propName]);
+}
diff --git a/toolkit/components/prompts/content/commonDialog.xul b/toolkit/components/prompts/content/commonDialog.xul
new file mode 100644
index 0000000000..990b26586b
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.xul
@@ -0,0 +1,97 @@
+<?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://global/content/commonDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/commonDialog.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://global/locale/commonDialog.dtd">
+
+<dialog id="commonDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ aria-describedby="info.body"
+ onunload="commonDialogOnUnload();"
+ ondialogaccept="Dialog.onButton0(); return true;"
+ ondialogcancel="Dialog.onButton1(); return true;"
+ ondialogextra1="Dialog.onButton2(); window.close();"
+ ondialogextra2="Dialog.onButton3(); window.close();"
+ buttonpack="center">
+
+ <script type="application/javascript" src="chrome://global/content/commonDialog.js"/>
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript">
+ document.addEventListener("DOMContentLoaded", function() {
+ commonDialogOnLoad();
+ });
+ </script>
+
+ <commandset id="selectEditMenuItems">
+ <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')" disabled="true"/>
+ <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/>
+ </commandset>
+
+ <popupset id="contentAreaContextSet">
+ <menupopup id="contentAreaContextMenu"
+ onpopupshowing="goUpdateCommand('cmd_copy')">
+ <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"/>
+ </menupopup>
+ </popupset>
+
+ <hbox id="filler" style="min-width: 0%;">
+ <spacer style="width: 29em;"/>
+ </hbox>
+
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <row>
+ <hbox id="iconContainer" align="start">
+ <image id="info.icon" class="spaced"/>
+ </hbox>
+ <vbox id="infoContainer"
+#ifndef XP_MACOSX
+ pack="center"
+#endif
+ >
+ <!-- Only shown on OS X, since it has no dialog title -->
+ <description id="info.title"
+#ifndef XP_MACOSX
+ hidden="true"
+#else
+ style="margin-bottom: 1em"
+#endif
+ />
+ <description id="info.body" context="contentAreaContextMenu" noinitialfocus="true"/>
+ </vbox>
+ </row>
+ <row id="loginContainer" hidden="true" align="center">
+ <label id="loginLabel" value="&editfield0.label;" control="loginTextbox"/>
+ <textbox id="loginTextbox"/>
+ </row>
+ <row id ="password1Container" hidden="true" align="center">
+ <label id="password1Label" value="&editfield1.label;" control="password1Textbox"/>
+ <textbox type="password" id="password1Textbox"/>
+ </row>
+ <row id="checkboxContainer" hidden="true">
+ <spacer/>
+ <checkbox id="checkbox" oncommand="Dialog.onCheckbox()"/>
+ </row>
+ </rows>
+ </grid>
+
+</dialog>
diff --git a/toolkit/components/prompts/content/selectDialog.js b/toolkit/components/prompts/content/selectDialog.js
new file mode 100644
index 0000000000..7628dc8d96
--- /dev/null
+++ b/toolkit/components/prompts/content/selectDialog.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces;
+var Cr = Components.results;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+var gArgs, listBox;
+
+function dialogOnLoad() {
+ gArgs = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag);
+
+ let promptType = gArgs.getProperty("promptType");
+ if (promptType != "select") {
+ Cu.reportError("selectDialog opened for unknown type: " + promptType);
+ window.close();
+ }
+
+ // Default to canceled.
+ gArgs.setProperty("ok", false);
+
+ document.title = gArgs.getProperty("title");
+
+ let text = gArgs.getProperty("text");
+ document.getElementById("info.txt").setAttribute("value", text);
+
+ let items = gArgs.getProperty("list");
+ listBox = document.getElementById("list");
+
+ for (let i = 0; i < items.length; i++) {
+ let str = items[i];
+ if (str == "")
+ str = "<>";
+ listBox.appendItem(str);
+ listBox.getItemAtIndex(i).addEventListener("dblclick", dialogDoubleClick, false);
+ }
+ listBox.selectedIndex = 0;
+ listBox.focus();
+
+ // resize the window to the content
+ window.sizeToContent();
+
+ // Move to the right location
+ moveToAlertPosition();
+ centerWindowOnScreen();
+
+ // play sound
+ try {
+ Cc["@mozilla.org/sound;1"].
+ createInstance(Ci.nsISound).
+ playEventSound(Ci.nsISound.EVENT_SELECT_DIALOG_OPEN);
+ } catch (e) { }
+}
+
+function dialogOK() {
+ gArgs.setProperty("selected", listBox.selectedIndex);
+ gArgs.setProperty("ok", true);
+ return true;
+}
+
+function dialogDoubleClick() {
+ dialogOK();
+ window.close();
+}
diff --git a/toolkit/components/prompts/content/selectDialog.xul b/toolkit/components/prompts/content/selectDialog.xul
new file mode 100644
index 0000000000..9b72bcfb09
--- /dev/null
+++ b/toolkit/components/prompts/content/selectDialog.xul
@@ -0,0 +1,22 @@
+<?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/global.css" type="text/css"?>
+<!DOCTYPE dialog SYSTEM "chrome://global/locale/commonDialog.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="dialogOnLoad()"
+ ondialogaccept="return dialogOK();">
+
+ <script type="application/javascript" src="chrome://global/content/selectDialog.js" />
+ <keyset id="dialogKeys"/>
+ <vbox style="width: 24em;margin: 5px;">
+ <label id="info.txt"/>
+ <vbox>
+ <listbox id="list" rows="4" flex="1"/>
+ </vbox>
+ </vbox>
+</dialog>
diff --git a/toolkit/components/prompts/content/tabprompts.css b/toolkit/components/prompts/content/tabprompts.css
new file mode 100644
index 0000000000..c4b0f7593c
--- /dev/null
+++ b/toolkit/components/prompts/content/tabprompts.css
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Tab Modal Prompt boxes */
+tabmodalprompt {
+ width: 100%;
+ height: 100%;
+ -moz-box-pack: center;
+ -moz-box-orient: vertical;
+}
+
+.mainContainer {
+ min-width: 20em;
+ min-height: 12em;
+ -moz-user-focus: normal;
+}
+
+.info\.title {
+ margin-bottom: 1em !important;
+ font-weight: bold;
+}
+
+.info\.body {
+ margin: 0 !important;
+ -moz-user-focus: normal;
+ -moz-user-select: text;
+ cursor: text !important;
+ white-space: pre-wrap;
+ unicode-bidi: plaintext;
+}
+
+label[value=""] {
+ visibility: collapse;
+}
diff --git a/toolkit/components/prompts/content/tabprompts.xml b/toolkit/components/prompts/content/tabprompts.xml
new file mode 100644
index 0000000000..07c6c8efbc
--- /dev/null
+++ b/toolkit/components/prompts/content/tabprompts.xml
@@ -0,0 +1,352 @@
+<?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 % commonDialogDTD SYSTEM "chrome://global/locale/commonDialog.dtd">
+<!ENTITY % dialogOverlayDTD SYSTEM "chrome://global/locale/dialogOverlay.dtd">
+%commonDialogDTD;
+%dialogOverlayDTD;
+]>
+
+<bindings id="tabPrompts"
+ 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="tabmodalprompt">
+
+ <resources>
+ <stylesheet src="chrome://global/content/tabprompts.css"/>
+ <stylesheet src="chrome://global/skin/tabprompts.css"/>
+ </resources>
+
+ <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ role="dialog"
+ aria-describedby="info.body">
+
+ <!-- This is based on the guts of commonDialog.xul -->
+ <spacer flex="1"/>
+ <hbox pack="center">
+ <vbox anonid="mainContainer" class="mainContainer">
+ <grid class="topContainer" flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+
+ <rows>
+ <vbox anonid="infoContainer" align="center" pack="center" flex="1">
+ <description anonid="info.title" class="info.title" hidden="true" />
+ <description anonid="info.body" class="info.body"/>
+ </vbox>
+
+ <row anonid="loginContainer" hidden="true" align="center">
+ <label anonid="loginLabel" value="&editfield0.label;" control="loginTextbox"/>
+ <textbox anonid="loginTextbox"/>
+ </row>
+
+ <row anonid="password1Container" hidden="true" align="center">
+ <label anonid="password1Label" value="&editfield1.label;" control="password1Textbox"/>
+ <textbox anonid="password1Textbox" type="password"/>
+ </row>
+
+ <row anonid="checkboxContainer" hidden="true">
+ <spacer/>
+ <checkbox anonid="checkbox"/>
+ </row>
+
+ <xbl:children includes="row"/>
+ </rows>
+ </grid>
+ <xbl:children/>
+ <hbox class="buttonContainer">
+#ifdef XP_UNIX
+ <button anonid="button3" hidden="true"/>
+ <button anonid="button2" hidden="true"/>
+ <spacer anonid="buttonSpacer" flex="1"/>
+ <button anonid="button1" label="&cancelButton.label;"/>
+ <button anonid="button0" label="&okButton.label;"/>
+#else
+ <button anonid="button3" hidden="true"/>
+ <spacer anonid="buttonSpacer" flex="1"/>
+ <button anonid="button0" label="&okButton.label;"/>
+ <button anonid="button2" hidden="true"/>
+ <button anonid="button1" label="&cancelButton.label;"/>
+#endif
+ </hbox>
+ </vbox>
+ </hbox>
+ <spacer flex="2"/>
+ </xbl:content>
+
+ <implementation implements="nsIDOMEventListener">
+ <constructor>
+ <![CDATA[
+ let self = this;
+ function getElement(anonid) {
+ return document.getAnonymousElementByAttribute(self, "anonid", anonid);
+ }
+
+ this.ui = {
+ prompt : this,
+ loginContainer : getElement("loginContainer"),
+ loginTextbox : getElement("loginTextbox"),
+ loginLabel : getElement("loginLabel"),
+ password1Container : getElement("password1Container"),
+ password1Textbox : getElement("password1Textbox"),
+ password1Label : getElement("password1Label"),
+ infoBody : getElement("info.body"),
+ infoTitle : getElement("info.title"),
+ infoIcon : null,
+ checkbox : getElement("checkbox"),
+ checkboxContainer : getElement("checkboxContainer"),
+ button3 : getElement("button3"),
+ button2 : getElement("button2"),
+ button1 : getElement("button1"),
+ button0 : getElement("button0"),
+ // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported
+ };
+
+ this.ui.button0.addEventListener("command", this.onButtonClick.bind(this, 0), false);
+ this.ui.button1.addEventListener("command", this.onButtonClick.bind(this, 1), false);
+ this.ui.button2.addEventListener("command", this.onButtonClick.bind(this, 2), false);
+ this.ui.button3.addEventListener("command", this.onButtonClick.bind(this, 3), false);
+ // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called!
+ this.ui.checkbox.addEventListener("command", function() { self.Dialog.onCheckbox(); }, false);
+ this.isLive = false;
+ ]]>
+ </constructor>
+ <destructor>
+ <![CDATA[
+ if (this.isLive) {
+ this.abortPrompt();
+ }
+ ]]>
+ </destructor>
+
+ <field name="ui"/>
+ <field name="args"/>
+ <field name="linkedTab"/>
+ <field name="onCloseCallback"/>
+ <field name="Dialog"/>
+ <field name="isLive"/>
+ <field name="availWidth"/>
+ <field name="availHeight"/>
+ <field name="minWidth"/>
+ <field name="minHeight"/>
+
+ <method name="init">
+ <parameter name="args"/>
+ <parameter name="linkedTab"/>
+ <parameter name="onCloseCallback"/>
+ <body>
+ <![CDATA[
+ this.args = args;
+ this.linkedTab = linkedTab;
+ this.onCloseCallback = onCloseCallback;
+
+ if (args.enableDelay)
+ throw "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts";
+
+ // We need to remove the prompt when the tab or browser window is closed or
+ // the page navigates, else we never unwind the event loop and that's sad times.
+ // Remember to cleanup in shutdownPrompt()!
+ this.isLive = true;
+ window.addEventListener("resize", this, false);
+ window.addEventListener("unload", this, false);
+ linkedTab.addEventListener("TabClose", this, false);
+ // Note:
+ // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt,
+ // when the domWindow, for which the prompt was created, generates
+ // a "pagehide" event.
+
+ let tmp = {};
+ Components.utils.import("resource://gre/modules/CommonDialog.jsm", tmp);
+ this.Dialog = new tmp.CommonDialog(args, this.ui);
+ this.Dialog.onLoad(null);
+
+ // Display the tabprompt title that shows the prompt origin when
+ // the prompt origin is not the same as that of the top window.
+ if (!args.showAlertOrigin)
+ this.ui.infoTitle.removeAttribute("hidden");
+
+ // TODO: should unhide buttonSpacer on Windows when there are 4 buttons.
+ // Better yet, just drop support for 4-button dialogs. (bug 609510)
+
+ this.onResize();
+ ]]>
+ </body>
+ </method>
+
+ <method name="shutdownPrompt">
+ <body>
+ <![CDATA[
+ // remove our event listeners
+ try {
+ window.removeEventListener("resize", this, false);
+ window.removeEventListener("unload", this, false);
+ this.linkedTab.removeEventListener("TabClose", this, false);
+ } catch (e) { }
+ this.isLive = false;
+ // invoke callback
+ this.onCloseCallback();
+ ]]>
+ </body>
+ </method>
+
+ <method name="abortPrompt">
+ <body>
+ <![CDATA[
+ // Called from other code when the page changes.
+ this.Dialog.abortPrompt();
+ this.shutdownPrompt();
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ switch (aEvent.type) {
+ case "resize":
+ this.onResize();
+ break;
+ case "unload":
+ case "TabClose":
+ this.abortPrompt();
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="onResize">
+ <body>
+ <![CDATA[
+ let availWidth = this.clientWidth;
+ let availHeight = this.clientHeight;
+ if (availWidth == this.availWidth && availHeight == this.availHeight)
+ return;
+ this.availWidth = availWidth;
+ this.availHeight = availHeight;
+
+ let self = this;
+ function getElement(anonid) {
+ return document.getAnonymousElementByAttribute(self, "anonid", anonid);
+ }
+ let main = getElement("mainContainer");
+ let info = getElement("infoContainer");
+ let body = this.ui.infoBody;
+
+ // cap prompt dimensions at 60% width and 60% height of content area
+ if (!this.minWidth)
+ this.minWidth = parseInt(window.getComputedStyle(main).minWidth);
+ if (!this.minHeight)
+ this.minHeight = parseInt(window.getComputedStyle(main).minHeight);
+ let maxWidth = Math.max(Math.floor(availWidth * 0.6), this.minWidth) +
+ info.clientWidth - main.clientWidth;
+ let maxHeight = Math.max(Math.floor(availHeight * 0.6), this.minHeight) +
+ info.clientHeight - main.clientHeight;
+ body.style.maxWidth = maxWidth + "px";
+ info.style.overflow = info.style.width = info.style.height = "";
+
+ // when prompt text is too long, use scrollbars
+ if (info.clientWidth > maxWidth) {
+ info.style.overflow = "auto";
+ info.style.width = maxWidth + "px";
+ }
+ if (info.clientHeight > maxHeight) {
+ info.style.overflow = "auto";
+ info.style.height = maxHeight + "px";
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="onButtonClick">
+ <parameter name="buttonNum"/>
+ <body>
+ <![CDATA[
+ // We want to do all the work her asynchronously off a Gecko
+ // runnable, because of situations like the one described in
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we
+ // get here off processing of an OS event and will also process
+ // one more Gecko runnable before we break out of the event loop
+ // spin whoever posted the prompt is doing. If we do all our
+ // work sync, we will exit modal state _before_ processing that
+ // runnable, and if exiting moral state posts a runnable we will
+ // incorrectly process that runnable before leaving our event
+ // loop spin.
+ Services.tm.mainThread.dispatch(() => {
+ this.Dialog["onButton" + buttonNum]();
+ this.shutdownPrompt();
+ },
+ Ci.nsIThread.DISPATCH_NORMAL);
+ ]]>
+ </body>
+ </method>
+
+ <method name="onKeyAction">
+ <parameter name="action"/>
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ if (event.defaultPrevented)
+ return;
+
+ event.stopPropagation();
+ if (action == "default") {
+ let bnum = this.args.defaultButtonNum || 0;
+ this.onButtonClick(bnum);
+ } else { // action == "cancel"
+ this.onButtonClick(1); // Cancel button
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <!-- Based on dialog.xml handlers -->
+ <handler event="keypress" keycode="VK_RETURN"
+ group="system" action="this.onKeyAction('default', event);"/>
+ <handler event="keypress" keycode="VK_ESCAPE"
+ group="system" action="this.onKeyAction('cancel', event);"/>
+#ifdef XP_MACOSX
+ <handler event="keypress" key="." modifiers="meta"
+ group="system" action="this.onKeyAction('cancel', event);"/>
+#endif
+ <handler event="focus" phase="capturing">
+ let bnum = this.args.defaultButtonNum || 0;
+ let defaultButton = this.ui["button" + bnum];
+
+ let { AppConstants } =
+ Components.utils.import("resource://gre/modules/AppConstants.jsm", {});
+ if (AppConstants.platform == "macosx") {
+ // On OS X, the default button always stays marked as such (until
+ // the entire prompt blurs).
+ defaultButton.setAttribute("default", true);
+ } else {
+ // On other platforms, the default button is only marked as such
+ // when no other button has focus. XUL buttons on not-OSX will
+ // react to pressing enter as a command, so you can't trigger the
+ // default without tabbing to it or something that isn't a button.
+ let focusedDefault = (event.originalTarget == defaultButton);
+ let someButtonFocused = event.originalTarget instanceof Ci.nsIDOMXULButtonElement;
+ defaultButton.setAttribute("default", focusedDefault || !someButtonFocused);
+ }
+ </handler>
+ <handler event="blur">
+ // If focus shifted to somewhere else in the browser, don't make
+ // the default button look active.
+ let bnum = this.args.defaultButtonNum || 0;
+ let button = this.ui["button" + bnum];
+ button.setAttribute("default", false);
+ </handler>
+ </handlers>
+
+ </binding>
+</bindings>
diff --git a/toolkit/components/prompts/jar.mn b/toolkit/components/prompts/jar.mn
new file mode 100644
index 0000000000..60ecbdcbc4
--- /dev/null
+++ b/toolkit/components/prompts/jar.mn
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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/commonDialog.js (content/commonDialog.js)
+* content/global/commonDialog.xul (content/commonDialog.xul)
+ content/global/commonDialog.css (content/commonDialog.css)
+ content/global/selectDialog.js (content/selectDialog.js)
+ content/global/selectDialog.xul (content/selectDialog.xul)
+ content/global/tabprompts.css (content/tabprompts.css)
+* content/global/tabprompts.xml (content/tabprompts.xml)
diff --git a/toolkit/components/prompts/moz.build b/toolkit/components/prompts/moz.build
new file mode 100644
index 0000000000..1dc21cca62
--- /dev/null
+++ b/toolkit/components/prompts/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/.
+
+DIRS += ['src']
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/prompts/src/CommonDialog.jsm b/toolkit/components/prompts/src/CommonDialog.jsm
new file mode 100644
index 0000000000..c4200feb31
--- /dev/null
+++ b/toolkit/components/prompts/src/CommonDialog.jsm
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = ["CommonDialog"];
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper",
+ "resource://gre/modules/SharedPromptUtils.jsm");
+
+
+this.CommonDialog = function CommonDialog(args, ui) {
+ this.args = args;
+ this.ui = ui;
+}
+
+CommonDialog.prototype = {
+ args : null,
+ ui : null,
+
+ hasInputField : true,
+ numButtons : undefined,
+ iconClass : undefined,
+ soundID : undefined,
+ focusTimer : null,
+
+ onLoad : function(xulDialog) {
+ switch (this.args.promptType) {
+ case "alert":
+ case "alertCheck":
+ this.hasInputField = false;
+ this.numButtons = 1;
+ this.iconClass = ["alert-icon"];
+ this.soundID = Ci.nsISound.EVENT_ALERT_DIALOG_OPEN;
+ break;
+ case "confirmCheck":
+ case "confirm":
+ this.hasInputField = false;
+ this.numButtons = 2;
+ this.iconClass = ["question-icon"];
+ this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
+ break;
+ case "confirmEx":
+ var numButtons = 0;
+ if (this.args.button0Label)
+ numButtons++;
+ if (this.args.button1Label)
+ numButtons++;
+ if (this.args.button2Label)
+ numButtons++;
+ if (this.args.button3Label)
+ numButtons++;
+ if (numButtons == 0)
+ throw "A dialog with no buttons? Can not haz.";
+ this.numButtons = numButtons;
+ this.hasInputField = false;
+ this.iconClass = ["question-icon"];
+ this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
+ break;
+ case "prompt":
+ this.numButtons = 2;
+ this.iconClass = ["question-icon"];
+ this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+ this.initTextbox("login", this.args.value);
+ // Clear the label, since this isn't really a username prompt.
+ this.ui.loginLabel.setAttribute("value", "");
+ break;
+ case "promptUserAndPass":
+ this.numButtons = 2;
+ this.iconClass = ["authentication-icon", "question-icon"];
+ this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+ this.initTextbox("login", this.args.user);
+ this.initTextbox("password1", this.args.pass);
+ break;
+ case "promptPassword":
+ this.numButtons = 2;
+ this.iconClass = ["authentication-icon", "question-icon"];
+ this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+ this.initTextbox("password1", this.args.pass);
+ // Clear the label, since the message presumably indicates its purpose.
+ this.ui.password1Label.setAttribute("value", "");
+ break;
+ default:
+ Cu.reportError("commonDialog opened for unknown type: " + this.args.promptType);
+ throw "unknown dialog type";
+ }
+
+ // set the document title
+ let title = this.args.title;
+ // OS X doesn't have a title on modal dialogs, this is hidden on other platforms.
+ let infoTitle = this.ui.infoTitle;
+ infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title));
+ if (xulDialog)
+ xulDialog.ownerDocument.title = title;
+
+ // Set button labels and visibility
+ //
+ // This assumes that button0 defaults to a visible "ok" button, and
+ // button1 defaults to a visible "cancel" button. The other 2 buttons
+ // have no default labels (and are hidden).
+ switch (this.numButtons) {
+ case 4:
+ this.setLabelForNode(this.ui.button3, this.args.button3Label);
+ this.ui.button3.hidden = false;
+ // fall through
+ case 3:
+ this.setLabelForNode(this.ui.button2, this.args.button2Label);
+ this.ui.button2.hidden = false;
+ // fall through
+ case 2:
+ // Defaults to a visible "cancel" button
+ if (this.args.button1Label)
+ this.setLabelForNode(this.ui.button1, this.args.button1Label);
+ break;
+
+ case 1:
+ this.ui.button1.hidden = true;
+ break;
+ }
+ // Defaults to a visible "ok" button
+ if (this.args.button0Label)
+ this.setLabelForNode(this.ui.button0, this.args.button0Label);
+
+ // display the main text
+ let croppedMessage = "";
+ if (this.args.text) {
+ // Bug 317334 - crop string length as a workaround.
+ croppedMessage = this.args.text.substr(0, 10000);
+ }
+ let infoBody = this.ui.infoBody;
+ infoBody.appendChild(infoBody.ownerDocument.createTextNode(croppedMessage));
+
+ let label = this.args.checkLabel;
+ if (label) {
+ // Only show the checkbox if label has a value.
+ this.ui.checkboxContainer.hidden = false;
+ this.setLabelForNode(this.ui.checkbox, label);
+ this.ui.checkbox.checked = this.args.checked;
+ }
+
+ // set the icon
+ let icon = this.ui.infoIcon;
+ if (icon)
+ this.iconClass.forEach((el, idx, arr) => icon.classList.add(el));
+
+ // set default result to cancelled
+ this.args.ok = false;
+ this.args.buttonNumClicked = 1;
+
+
+ // Set the default button
+ let b = (this.args.defaultButtonNum || 0);
+ let button = this.ui["button" + b];
+
+ if (xulDialog)
+ xulDialog.defaultButton = ['accept', 'cancel', 'extra1', 'extra2'][b];
+ else
+ button.setAttribute("default", "true");
+
+ // Set default focus / selection.
+ this.setDefaultFocus(true);
+
+ if (this.args.enableDelay) {
+ this.delayHelper = new EnableDelayHelper({
+ disableDialog: () => this.setButtonsEnabledState(false),
+ enableDialog: () => this.setButtonsEnabledState(true),
+ focusTarget: this.ui.focusTarget
+ });
+ }
+
+ // Play a sound (unless we're tab-modal -- don't want those to feel like OS prompts).
+ try {
+ if (xulDialog && this.soundID) {
+ Cc["@mozilla.org/sound;1"].
+ createInstance(Ci.nsISound).
+ playEventSound(this.soundID);
+ }
+ } catch (e) {
+ Cu.reportError("Couldn't play common dialog event sound: " + e);
+ }
+
+ let topic = "common-dialog-loaded";
+ if (!xulDialog)
+ topic = "tabmodal-dialog-loaded";
+ Services.obs.notifyObservers(this.ui.prompt, topic, null);
+ },
+
+ setLabelForNode: function(aNode, aLabel) {
+ // This is for labels which may contain embedded access keys.
+ // If we end in (&X) where X represents the access key, optionally preceded
+ // by spaces and/or followed by the ':' character, store the access key and
+ // remove the access key placeholder + leading spaces from the label.
+ // Otherwise a character preceded by one but not two &s is the access key.
+ // Store it and remove the &.
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+ var accessKey = null;
+ if (/ *\(\&([^&])\)(:?)$/.test(aLabel)) {
+ aLabel = RegExp.leftContext + RegExp.$2;
+ accessKey = RegExp.$1;
+ } else if (/^([^&]*)\&(([^&]).*$)/.test(aLabel)) {
+ aLabel = RegExp.$1 + RegExp.$2;
+ accessKey = RegExp.$3;
+ }
+
+ // && is the magic sequence to embed an & in your label.
+ aLabel = aLabel.replace(/\&\&/g, "&");
+ aNode.label = aLabel;
+
+ // XXXjag bug 325251
+ // Need to set this after aNode.setAttribute("value", aLabel);
+ if (accessKey)
+ aNode.accessKey = accessKey;
+ },
+
+
+ initTextbox : function (aName, aValue) {
+ this.ui[aName + "Container"].hidden = false;
+ this.ui[aName + "Textbox"].setAttribute("value",
+ aValue !== null ? aValue : "");
+ },
+
+ setButtonsEnabledState : function(enabled) {
+ this.ui.button0.disabled = !enabled;
+ // button1 (cancel) remains enabled.
+ this.ui.button2.disabled = !enabled;
+ this.ui.button3.disabled = !enabled;
+ },
+
+ setDefaultFocus : function(isInitialLoad) {
+ let b = (this.args.defaultButtonNum || 0);
+ let button = this.ui["button" + b];
+
+ if (!this.hasInputField) {
+ let isOSX = ("nsILocalFileMac" in Components.interfaces);
+ if (isOSX)
+ this.ui.infoBody.focus();
+ else
+ button.focus();
+ } else if (this.args.promptType == "promptPassword") {
+ // When the prompt is initialized, focus and select the textbox
+ // contents. Afterwards, only focus the textbox.
+ if (isInitialLoad)
+ this.ui.password1Textbox.select();
+ else
+ this.ui.password1Textbox.focus();
+ } else if (isInitialLoad) {
+ this.ui.loginTextbox.select();
+ } else {
+ this.ui.loginTextbox.focus();
+ }
+ },
+
+ onCheckbox : function() {
+ this.args.checked = this.ui.checkbox.checked;
+ },
+
+ onButton0 : function() {
+ this.args.promptActive = false;
+ this.args.ok = true;
+ this.args.buttonNumClicked = 0;
+
+ let username = this.ui.loginTextbox.value;
+ let password = this.ui.password1Textbox.value;
+
+ // Return textfield values
+ switch (this.args.promptType) {
+ case "prompt":
+ this.args.value = username;
+ break;
+ case "promptUserAndPass":
+ this.args.user = username;
+ this.args.pass = password;
+ break;
+ case "promptPassword":
+ this.args.pass = password;
+ break;
+ }
+ },
+
+ onButton1 : function() {
+ this.args.promptActive = false;
+ this.args.buttonNumClicked = 1;
+ },
+
+ onButton2 : function() {
+ this.args.promptActive = false;
+ this.args.buttonNumClicked = 2;
+ },
+
+ onButton3 : function() {
+ this.args.promptActive = false;
+ this.args.buttonNumClicked = 3;
+ },
+
+ abortPrompt : function() {
+ this.args.promptActive = false;
+ this.args.promptAborted = true;
+ },
+
+};
diff --git a/toolkit/components/prompts/src/SharedPromptUtils.jsm b/toolkit/components/prompts/src/SharedPromptUtils.jsm
new file mode 100644
index 0000000000..b27096ac28
--- /dev/null
+++ b/toolkit/components/prompts/src/SharedPromptUtils.jsm
@@ -0,0 +1,157 @@
+this.EXPORTED_SYMBOLS = [ "PromptUtils", "EnableDelayHelper" ];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.PromptUtils = {
+ // Fire a dialog open/close event. Used by tabbrowser to focus the
+ // tab which is triggering a prompt.
+ // For remote dialogs, we pass in a different DOM window and a separate
+ // target. If the caller doesn't pass in the target, then we'll simply use
+ // the passed-in DOM window.
+ // The detail may contain information about the principal on which the
+ // prompt is triggered, as well as whether or not this is a tabprompt
+ // (ie tabmodal alert/prompt/confirm and friends)
+ fireDialogEvent : function (domWin, eventName, maybeTarget, detail) {
+ let target = maybeTarget || domWin;
+ let eventOptions = {cancelable: true, bubbles: true};
+ if (detail) {
+ eventOptions.detail = detail;
+ }
+ let event = new domWin.CustomEvent(eventName, eventOptions);
+ let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.dispatchEventToChromeOnly(target, event);
+ },
+
+ objectToPropBag : function (obj) {
+ let bag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag2);
+ bag.QueryInterface(Ci.nsIWritablePropertyBag);
+
+ for (let propName in obj)
+ bag.setProperty(propName, obj[propName]);
+
+ return bag;
+ },
+
+ propBagToObject : function (propBag, obj) {
+ // Here we iterate over the object's original properties, not the bag
+ // (ie, the prompt can't return more/different properties than were
+ // passed in). This just helps ensure that the caller provides default
+ // values, lest the prompt forget to set them.
+ for (let propName in obj)
+ obj[propName] = propBag.getProperty(propName);
+ },
+};
+
+/**
+ * This helper handles the enabling/disabling of dialogs that might
+ * be subject to fast-clicking attacks. It handles the initial delayed
+ * enabling of the dialog, as well as disabling it on blur and reapplying
+ * the delay when the dialog regains focus.
+ *
+ * @param enableDialog A custom function to be called when the dialog
+ * is to be enabled.
+ * @param diableDialog A custom function to be called when the dialog
+ * is to be disabled.
+ * @param focusTarget The window used to watch focus/blur events.
+ */
+this.EnableDelayHelper = function({enableDialog, disableDialog, focusTarget}) {
+ this.enableDialog = makeSafe(enableDialog);
+ this.disableDialog = makeSafe(disableDialog);
+ this.focusTarget = focusTarget;
+
+ this.disableDialog();
+
+ this.focusTarget.addEventListener("blur", this, false);
+ this.focusTarget.addEventListener("focus", this, false);
+ this.focusTarget.document.addEventListener("unload", this, false);
+
+ this.startOnFocusDelay();
+};
+
+this.EnableDelayHelper.prototype = {
+ get delayTime() {
+ return Services.prefs.getIntPref("security.dialog_enable_delay");
+ },
+
+ handleEvent : function(event) {
+ if (event.target != this.focusTarget &&
+ event.target != this.focusTarget.document)
+ return;
+
+ switch (event.type) {
+ case "blur":
+ this.onBlur();
+ break;
+
+ case "focus":
+ this.onFocus();
+ break;
+
+ case "unload":
+ this.onUnload();
+ break;
+ }
+ },
+
+ onBlur : function () {
+ this.disableDialog();
+ // If we blur while waiting to enable the buttons, just cancel the
+ // timer to ensure the delay doesn't fire while not focused.
+ if (this._focusTimer) {
+ this._focusTimer.cancel();
+ this._focusTimer = null;
+ }
+ },
+
+ onFocus : function () {
+ this.startOnFocusDelay();
+ },
+
+ onUnload: function() {
+ this.focusTarget.removeEventListener("blur", this, false);
+ this.focusTarget.removeEventListener("focus", this, false);
+ this.focusTarget.document.removeEventListener("unload", this, false);
+
+ if (this._focusTimer) {
+ this._focusTimer.cancel();
+ this._focusTimer = null;
+ }
+
+ this.focusTarget = this.enableDialog = this.disableDialog = null;
+ },
+
+ startOnFocusDelay : function() {
+ if (this._focusTimer)
+ return;
+
+ this._focusTimer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ this._focusTimer.initWithCallback(
+ () => { this.onFocusTimeout(); },
+ this.delayTime,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ onFocusTimeout : function() {
+ this._focusTimer = null;
+ this.enableDialog();
+ },
+};
+
+function makeSafe(fn) {
+ return function () {
+ // The dialog could be gone by now (if the user closed it),
+ // which makes it likely that the given fn might throw.
+ try {
+ fn();
+ } catch (e) { }
+ };
+}
diff --git a/toolkit/components/prompts/src/moz.build b/toolkit/components/prompts/src/moz.build
new file mode 100644
index 0000000000..b13e47b426
--- /dev/null
+++ b/toolkit/components/prompts/src/moz.build
@@ -0,0 +1,16 @@
+# -*- 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 += [
+ 'nsPrompter.js',
+ 'nsPrompter.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'CommonDialog.jsm',
+ 'SharedPromptUtils.jsm',
+]
+
diff --git a/toolkit/components/prompts/src/nsPrompter.js b/toolkit/components/prompts/src/nsPrompter.js
new file mode 100644
index 0000000000..26efe28cc7
--- /dev/null
+++ b/toolkit/components/prompts/src/nsPrompter.js
@@ -0,0 +1,958 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
+
+function Prompter() {
+ // Note that EmbedPrompter clones this implementation.
+}
+
+Prompter.prototype = {
+ classID : Components.ID("{1c978d25-b37f-43a8-a2d6-0c7a239ead87}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]),
+
+
+ /* ---------- private members ---------- */
+
+ pickPrompter : function (domWin) {
+ return new ModalPrompter(domWin);
+ },
+
+
+ /* ---------- nsIPromptFactory ---------- */
+
+
+ getPrompt : function (domWin, iid) {
+ // This is still kind of dumb; the C++ code delegated to login manager
+ // here, which in turn calls back into us via nsIPromptService2.
+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPrompt)) {
+ try {
+ let pwmgr = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+ return pwmgr.getPrompt(domWin, iid);
+ } catch (e) {
+ Cu.reportError("nsPrompter: Delegation to password manager failed: " + e);
+ }
+ }
+
+ let p = new ModalPrompter(domWin);
+ p.QueryInterface(iid);
+ return p;
+ },
+
+
+ /* ---------- nsIPromptService ---------- */
+
+
+ alert : function (domWin, title, text) {
+ let p = this.pickPrompter(domWin);
+ p.alert(title, text);
+ },
+
+ alertCheck : function (domWin, title, text, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ p.alertCheck(title, text, checkLabel, checkValue);
+ },
+
+ confirm : function (domWin, title, text) {
+ let p = this.pickPrompter(domWin);
+ return p.confirm(title, text);
+ },
+
+ confirmCheck : function (domWin, title, text, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.confirmCheck(title, text, checkLabel, checkValue);
+ },
+
+ confirmEx : function (domWin, title, text, flags, button0, button1, button2, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.confirmEx(title, text, flags, button0, button1, button2, checkLabel, checkValue);
+ },
+
+ prompt : function (domWin, title, text, value, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.nsIPrompt_prompt(title, text, value, checkLabel, checkValue);
+ },
+
+ promptUsernameAndPassword : function (domWin, title, text, user, pass, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.nsIPrompt_promptUsernameAndPassword(title, text, user, pass, checkLabel, checkValue);
+ },
+
+ promptPassword : function (domWin, title, text, pass, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.nsIPrompt_promptPassword(title, text, pass, checkLabel, checkValue);
+ },
+
+ select : function (domWin, title, text, count, list, selected) {
+ let p = this.pickPrompter(domWin);
+ return p.select(title, text, count, list, selected);
+ },
+
+
+ /* ---------- nsIPromptService2 ---------- */
+
+
+ promptAuth : function (domWin, channel, level, authInfo, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.promptAuth(channel, level, authInfo, checkLabel, checkValue);
+ },
+
+ asyncPromptAuth : function (domWin, channel, callback, context, level, authInfo, checkLabel, checkValue) {
+ let p = this.pickPrompter(domWin);
+ return p.asyncPromptAuth(channel, callback, context, level, authInfo, checkLabel, checkValue);
+ },
+
+};
+
+
+// Common utils not specific to a particular prompter style.
+var PromptUtilsTemp = {
+ __proto__ : PromptUtils,
+
+ getLocalizedString : function (key, formatArgs) {
+ if (formatArgs)
+ return this.strBundle.formatStringFromName(key, formatArgs, formatArgs.length);
+ return this.strBundle.GetStringFromName(key);
+ },
+
+ confirmExHelper : function (flags, button0, button1, button2) {
+ const BUTTON_DEFAULT_MASK = 0x03000000;
+ let defaultButtonNum = (flags & BUTTON_DEFAULT_MASK) >> 24;
+ let isDelayEnabled = (flags & Ci.nsIPrompt.BUTTON_DELAY_ENABLE);
+
+ // Flags can be used to select a specific pre-defined button label or
+ // a caller-supplied string (button0/button1/button2). If no flags are
+ // set for a button, then the button won't be shown.
+ let argText = [button0, button1, button2];
+ let buttonLabels = [null, null, null];
+ for (let i = 0; i < 3; i++) {
+ let buttonLabel;
+ switch (flags & 0xff) {
+ case Ci.nsIPrompt.BUTTON_TITLE_OK:
+ buttonLabel = PromptUtils.getLocalizedString("OK");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
+ buttonLabel = PromptUtils.getLocalizedString("Cancel");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_YES:
+ buttonLabel = PromptUtils.getLocalizedString("Yes");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_NO:
+ buttonLabel = PromptUtils.getLocalizedString("No");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
+ buttonLabel = PromptUtils.getLocalizedString("Save");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
+ buttonLabel = PromptUtils.getLocalizedString("DontSave");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
+ buttonLabel = PromptUtils.getLocalizedString("Revert");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
+ buttonLabel = argText[i];
+ break;
+ }
+ if (buttonLabel)
+ buttonLabels[i] = buttonLabel;
+ flags >>= 8;
+ }
+
+ return [buttonLabels[0], buttonLabels[1], buttonLabels[2], defaultButtonNum, isDelayEnabled];
+ },
+
+ getAuthInfo : function (authInfo) {
+ let username, password;
+
+ let flags = authInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && authInfo.domain)
+ username = authInfo.domain + "\\" + authInfo.username;
+ else
+ username = authInfo.username;
+
+ password = authInfo.password;
+
+ return [username, password];
+ },
+
+ setAuthInfo : function (authInfo, username, password) {
+ let flags = authInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ let idx = username.indexOf("\\");
+ if (idx == -1) {
+ authInfo.username = username;
+ } else {
+ authInfo.domain = username.substring(0, idx);
+ authInfo.username = username.substring(idx+1);
+ }
+ } else {
+ authInfo.username = username;
+ }
+ authInfo.password = password;
+ },
+
+ /**
+ * Strip out things like userPass and path for display.
+ */
+ getFormattedHostname : function (uri) {
+ return uri.scheme + "://" + uri.hostPort;
+ },
+
+ // Copied from login manager
+ getAuthTarget : function (aChannel, aAuthInfo) {
+ let hostname, realm;
+
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ if (!(aChannel instanceof Ci.nsIProxiedChannel))
+ throw "proxy auth needs nsIProxiedChannel";
+
+ let info = aChannel.proxyInfo;
+ if (!info)
+ throw "proxy auth needs nsIProxyInfo";
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+ hostname = "moz-proxy://" +
+ idnService.convertUTF8toACE(info.host) +
+ ":" + info.port;
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ }
+
+ hostname = this.getFormattedHostname(aChannel.URI);
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ },
+
+
+ makeAuthMessage : function (channel, authInfo) {
+ let isProxy = (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY);
+ let isPassOnly = (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD);
+ let isCrossOrig = (authInfo.flags &
+ Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE);
+
+ let username = authInfo.username;
+ let [displayHost, realm] = this.getAuthTarget(channel, authInfo);
+
+ // Suppress "the site says: $realm" when we synthesized a missing realm.
+ if (!authInfo.realm && !isProxy)
+ realm = "";
+
+ // Trim obnoxiously long realms.
+ if (realm.length > 150) {
+ realm = realm.substring(0, 150);
+ // Append "..." (or localized equivalent).
+ realm += this.ellipsis;
+ }
+
+ let text;
+ if (isProxy) {
+ text = PromptUtils.getLocalizedString("EnterLoginForProxy3", [realm, displayHost]);
+ } else if (isPassOnly) {
+ text = PromptUtils.getLocalizedString("EnterPasswordFor", [username, displayHost]);
+ } else if (isCrossOrig) {
+ text = PromptUtils.getLocalizedString("EnterUserPasswordForCrossOrigin2", [displayHost]);
+ } else if (!realm) {
+ text = PromptUtils.getLocalizedString("EnterUserPasswordFor2", [displayHost]);
+ } else {
+ text = PromptUtils.getLocalizedString("EnterLoginForRealm3", [realm, displayHost]);
+ }
+
+ return text;
+ },
+
+ getTabModalPrompt : function (domWin) {
+ var promptBox = null;
+
+ try {
+ // Get the topmost window, in case we're in a frame.
+ var promptWin = domWin.top;
+
+ // Get the chrome window for the content window we're using.
+ // (Unwrap because we need a non-IDL property below.)
+ var chromeWin = promptWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument
+ .defaultView.wrappedJSObject;
+
+ if (chromeWin.getTabModalPromptBox)
+ promptBox = chromeWin.getTabModalPromptBox(promptWin);
+ } catch (e) {
+ // If any errors happen, just assume no tabmodal prompter.
+ }
+
+ return promptBox;
+ },
+};
+
+PromptUtils = PromptUtilsTemp;
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "strBundle", function () {
+ let bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ let bundle = bunService.createBundle("chrome://global/locale/commonDialogs.properties");
+ if (!bundle)
+ throw "String bundle for Prompter not present!";
+ return bundle;
+});
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "ellipsis", function () {
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+ return ellipsis;
+});
+
+
+
+function openModalWindow(domWin, uri, args) {
+ // There's an implied contract that says modal prompts should still work
+ // when no "parent" window is passed for the dialog (eg, the "Master
+ // Password" dialog does this). These prompts must be shown even if there
+ // are *no* visible windows at all.
+ // There's also a requirement for prompts to be blocked if a window is
+ // passed and that window is hidden (eg, auth prompts are supressed if the
+ // passed window is the hidden window).
+ // See bug 875157 comment 30 for more...
+ if (domWin) {
+ // a domWin was passed, so we can apply the check for it being hidden.
+ let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ if (winUtils && !winUtils.isParentWindowMainWidgetVisible) {
+ throw Components.Exception("Cannot call openModalWindow on a hidden window",
+ Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ } else {
+ // We try and find a window to use as the parent, but don't consider
+ // if that is visible before showing the prompt.
+ domWin = Services.ww.activeWindow;
+ // domWin may still be null here if there are _no_ windows open.
+ }
+ // Note that we don't need to fire DOMWillOpenModalDialog and
+ // DOMModalDialogClosed events here, wwatcher's OpenWindowInternal
+ // will do that. Similarly for enterModalState / leaveModalState.
+
+ Services.ww.openWindow(domWin, uri, "_blank", "centerscreen,chrome,modal,titlebar", args);
+}
+
+function openTabPrompt(domWin, tabPrompt, args) {
+ let docShell = domWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+ let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload;
+ let eventDetail = Cu.cloneInto({tabPrompt: true, inPermitUnload}, domWin);
+ PromptUtils.fireDialogEvent(domWin, "DOMWillOpenModalDialog", null, eventDetail);
+
+ let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.enterModalState();
+
+ let frameMM = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ frameMM.QueryInterface(Ci.nsIDOMEventTarget);
+
+ // We provide a callback so the prompt can close itself. We don't want to
+ // wait for this event loop to return... Otherwise the presence of other
+ // prompts on the call stack would in this dialog appearing unresponsive
+ // until the other prompts had been closed.
+ let callbackInvoked = false;
+ let newPrompt;
+ function onPromptClose(forceCleanup) {
+ if (!newPrompt && !forceCleanup)
+ return;
+ callbackInvoked = true;
+ if (newPrompt)
+ tabPrompt.removePrompt(newPrompt);
+
+ frameMM.removeEventListener("pagehide", pagehide, true);
+
+ winUtils.leaveModalState();
+
+ PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed");
+ }
+
+ frameMM.addEventListener("pagehide", pagehide, true);
+ function pagehide(e) {
+ // Check whether the event relates to our window or its ancestors
+ let window = domWin;
+ let eventWindow = e.target.defaultView;
+ while (window != eventWindow && window.parent != window) {
+ window = window.parent;
+ }
+ if (window != eventWindow) {
+ return;
+ }
+ frameMM.removeEventListener("pagehide", pagehide, true);
+
+ if (newPrompt) {
+ newPrompt.abortPrompt();
+ }
+ }
+
+ try {
+ let topPrincipal = domWin.top.document.nodePrincipal;
+ let promptPrincipal = domWin.document.nodePrincipal;
+ args.showAlertOrigin = topPrincipal.equals(promptPrincipal);
+ args.promptActive = true;
+
+ newPrompt = tabPrompt.appendPrompt(args, onPromptClose);
+
+ // TODO since we don't actually open a window, need to check if
+ // there's other stuff in nsWindowWatcher::OpenWindowInternal
+ // that we might need to do here as well.
+
+ let thread = Services.tm.currentThread;
+ while (args.promptActive)
+ thread.processNextEvent(true);
+ delete args.promptActive;
+
+ if (args.promptAborted)
+ throw Components.Exception("prompt aborted by user", Cr.NS_ERROR_NOT_AVAILABLE);
+ } finally {
+ // If the prompt unexpectedly failed to invoke the callback, do so here.
+ if (!callbackInvoked)
+ onPromptClose(true);
+ }
+}
+
+function openRemotePrompt(domWin, args, tabPrompt) {
+ let docShell = domWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+ let messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsITabChild)
+ .messageManager;
+
+ let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload;
+ let eventDetail = Cu.cloneInto({tabPrompt, inPermitUnload}, domWin);
+ PromptUtils.fireDialogEvent(domWin, "DOMWillOpenModalDialog", null, eventDetail);
+
+ let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.enterModalState();
+ let closed = false;
+
+ let frameMM = docShell.getInterface(Ci.nsIContentFrameMessageManager);
+ frameMM.QueryInterface(Ci.nsIDOMEventTarget);
+
+ // It should be hard or impossible to cause a window to create multiple
+ // prompts, but just in case, give our prompt an ID.
+ let id = "id" + Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+
+ messageManager.addMessageListener("Prompt:Close", function listener(message) {
+ if (message.data._remoteId !== id) {
+ return;
+ }
+
+ messageManager.removeMessageListener("Prompt:Close", listener);
+ frameMM.removeEventListener("pagehide", pagehide, true);
+
+ winUtils.leaveModalState();
+ PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed");
+
+ // Copy the response from the closed prompt into our args, it will be
+ // read by our caller.
+ if (message.data) {
+ for (let key in message.data) {
+ args[key] = message.data[key];
+ }
+ }
+
+ // Exit our nested event loop when we unwind.
+ closed = true;
+ });
+
+ frameMM.addEventListener("pagehide", pagehide, true);
+ function pagehide(e) {
+ // Check whether the event relates to our window or its ancestors
+ let window = domWin;
+ let eventWindow = e.target.defaultView;
+ while (window != eventWindow && window.parent != window) {
+ window = window.parent;
+ }
+ if (window != eventWindow) {
+ return;
+ }
+ frameMM.removeEventListener("pagehide", pagehide, true);
+ messageManager.sendAsyncMessage("Prompt:ForceClose", { _remoteId: id });
+ }
+
+ let topPrincipal = domWin.top.document.nodePrincipal;
+ let promptPrincipal = domWin.document.nodePrincipal;
+ args.promptPrincipal = promptPrincipal;
+ args.showAlertOrigin = topPrincipal.equals(promptPrincipal);
+ args.inPermitUnload = inPermitUnload;
+
+ args._remoteId = id;
+
+ messageManager.sendAsyncMessage("Prompt:Open", args, {});
+
+ let thread = Services.tm.currentThread;
+ while (!closed) {
+ thread.processNextEvent(true);
+ }
+}
+
+function ModalPrompter(domWin) {
+ this.domWin = domWin;
+}
+ModalPrompter.prototype = {
+ domWin : null,
+ /*
+ * Default to not using a tab-modal prompt, unless the caller opts in by
+ * QIing to nsIWritablePropertyBag and setting the value of this property
+ * to true.
+ */
+ allowTabModal : false,
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt,
+ Ci.nsIAuthPrompt2,
+ Ci.nsIWritablePropertyBag2]),
+
+
+ /* ---------- internal methods ---------- */
+
+
+ openPrompt : function (args) {
+ // Check pref, if false/missing do not ever allow tab-modal prompts.
+ const prefName = "prompts.tab_modal.enabled";
+ let prefValue = false;
+ if (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL)
+ prefValue = Services.prefs.getBoolPref(prefName);
+
+ let allowTabModal = this.allowTabModal && prefValue;
+
+ if (allowTabModal && this.domWin) {
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ openRemotePrompt(this.domWin, args, true);
+ return;
+ }
+
+ let tabPrompt = PromptUtils.getTabModalPrompt(this.domWin);
+ if (tabPrompt) {
+ openTabPrompt(this.domWin, tabPrompt, args);
+ return;
+ }
+ }
+
+ // If we can't do a tab modal prompt, fallback to using a window-modal dialog.
+ const COMMON_DIALOG = "chrome://global/content/commonDialog.xul";
+ const SELECT_DIALOG = "chrome://global/content/selectDialog.xul";
+
+ let uri = (args.promptType == "select") ? SELECT_DIALOG : COMMON_DIALOG;
+
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+ args.uri = uri;
+ openRemotePrompt(this.domWin, args);
+ return;
+ }
+
+ let propBag = PromptUtils.objectToPropBag(args);
+ openModalWindow(this.domWin, uri, propBag);
+ PromptUtils.propBagToObject(propBag, args);
+ },
+
+
+
+ /*
+ * ---------- interface disambiguation ----------
+ *
+ * nsIPrompt and nsIAuthPrompt share 3 method names with slightly
+ * different arguments. All but prompt() have the same number of
+ * arguments, so look at the arg types to figure out how we're being
+ * called. :-(
+ */
+ prompt : function() {
+ // also, the nsIPrompt flavor has 5 args instead of 6.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_prompt.apply(this, arguments);
+ return this.nsIAuthPrompt_prompt.apply(this, arguments);
+ },
+
+ promptUsernameAndPassword : function() {
+ // Both have 6 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments);
+ return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments);
+ },
+
+ promptPassword : function() {
+ // Both have 5 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptPassword.apply(this, arguments);
+ return this.nsIAuthPrompt_promptPassword.apply(this, arguments);
+ },
+
+
+ /* ---------- nsIPrompt ---------- */
+
+
+ alert : function (title, text) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("Alert");
+
+ let args = {
+ promptType: "alert",
+ title: title,
+ text: text,
+ };
+
+ this.openPrompt(args);
+ },
+
+ alertCheck : function (title, text, checkLabel, checkValue) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("Alert");
+
+ let args = {
+ promptType: "alertCheck",
+ title: title,
+ text: text,
+ checkLabel: checkLabel,
+ checked: checkValue.value,
+ };
+
+ this.openPrompt(args);
+
+ // Checkbox state always returned, even if cancel clicked.
+ checkValue.value = args.checked;
+ },
+
+ confirm : function (title, text) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("Confirm");
+
+ let args = {
+ promptType: "confirm",
+ title: title,
+ text: text,
+ ok: false,
+ };
+
+ this.openPrompt(args);
+
+ // Did user click Ok or Cancel?
+ return args.ok;
+ },
+
+ confirmCheck : function (title, text, checkLabel, checkValue) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("ConfirmCheck");
+
+ let args = {
+ promptType: "confirmCheck",
+ title: title,
+ text: text,
+ checkLabel: checkLabel,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ this.openPrompt(args);
+
+ // Checkbox state always returned, even if cancel clicked.
+ checkValue.value = args.checked;
+
+ // Did user click Ok or Cancel?
+ return args.ok;
+ },
+
+ confirmEx : function (title, text, flags, button0, button1, button2,
+ checkLabel, checkValue) {
+
+ if (!title)
+ title = PromptUtils.getLocalizedString("Confirm");
+
+ let args = {
+ promptType: "confirmEx",
+ title: title,
+ text: text,
+ checkLabel: checkLabel,
+ checked: checkValue.value,
+ ok: false,
+ buttonNumClicked: 1,
+ };
+
+ let [label0, label1, label2, defaultButtonNum, isDelayEnabled] =
+ PromptUtils.confirmExHelper(flags, button0, button1, button2);
+
+ args.defaultButtonNum = defaultButtonNum;
+ args.enableDelay = isDelayEnabled;
+
+ if (label0) {
+ args.button0Label = label0;
+ if (label1) {
+ args.button1Label = label1;
+ if (label2) {
+ args.button2Label = label2;
+ }
+ }
+ }
+
+ this.openPrompt(args);
+
+ // Checkbox state always returned, even if cancel clicked.
+ checkValue.value = args.checked;
+
+ // Get the number of the button the user clicked.
+ return args.buttonNumClicked;
+ },
+
+ nsIPrompt_prompt : function (title, text, value, checkLabel, checkValue) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("Prompt");
+
+ let args = {
+ promptType: "prompt",
+ title: title,
+ text: text,
+ value: value.value,
+ checkLabel: checkLabel,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ this.openPrompt(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ value.value = args.value;
+ }
+
+ return ok;
+ },
+
+ nsIPrompt_promptUsernameAndPassword : function (title, text, user, pass, checkLabel, checkValue) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("PromptUsernameAndPassword2");
+
+ let args = {
+ promptType: "promptUserAndPass",
+ title: title,
+ text: text,
+ user: user.value,
+ pass: pass.value,
+ checkLabel: checkLabel,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ this.openPrompt(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ user.value = args.user;
+ pass.value = args.pass;
+ }
+
+ return ok;
+ },
+
+ nsIPrompt_promptPassword : function (title, text, pass, checkLabel, checkValue) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("PromptPassword2");
+
+ let args = {
+ promptType: "promptPassword",
+ title: title,
+ text: text,
+ pass: pass.value,
+ checkLabel: checkLabel,
+ checked: checkValue.value,
+ ok: false,
+ }
+
+ this.openPrompt(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ pass.value = args.pass;
+ }
+
+ return ok;
+ },
+
+ select : function (title, text, count, list, selected) {
+ if (!title)
+ title = PromptUtils.getLocalizedString("Select");
+
+ let args = {
+ promptType: "select",
+ title: title,
+ text: text,
+ list: list,
+ selected: -1,
+ ok: false,
+ };
+
+ this.openPrompt(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok)
+ selected.value = args.selected;
+
+ return ok;
+ },
+
+
+ /* ---------- nsIAuthPrompt ---------- */
+
+
+ nsIAuthPrompt_prompt : function (title, text, passwordRealm, savePassword, defaultText, result) {
+ // The passwordRealm and savePassword args were ignored by nsPrompt.cpp
+ if (defaultText)
+ result.value = defaultText;
+ return this.nsIPrompt_prompt(title, text, result, null, {});
+ },
+
+ nsIAuthPrompt_promptUsernameAndPassword : function (title, text, passwordRealm, savePassword, user, pass) {
+ // The passwordRealm and savePassword args were ignored by nsPrompt.cpp
+ return this.nsIPrompt_promptUsernameAndPassword(title, text, user, pass, null, {});
+ },
+
+ nsIAuthPrompt_promptPassword : function (title, text, passwordRealm, savePassword, pass) {
+ // The passwordRealm and savePassword args were ignored by nsPrompt.cpp
+ return this.nsIPrompt_promptPassword(title, text, pass, null, {});
+ },
+
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+
+ promptAuth : function (channel, level, authInfo, checkLabel, checkValue) {
+ let message = PromptUtils.makeAuthMessage(channel, authInfo);
+
+ let [username, password] = PromptUtils.getAuthInfo(authInfo);
+
+ let userParam = { value: username };
+ let passParam = { value: password };
+
+ let ok;
+ if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
+ ok = this.nsIPrompt_promptPassword(null, message, passParam, checkLabel, checkValue);
+ else
+ ok = this.nsIPrompt_promptUsernameAndPassword(null, message, userParam, passParam, checkLabel, checkValue);
+
+ if (ok)
+ PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value);
+ return ok;
+ },
+
+ asyncPromptAuth : function (channel, callback, context, level, authInfo, checkLabel, checkValue) {
+ // Nothing calls this directly; netwerk ends up going through
+ // nsIPromptService::GetPrompt, which delegates to login manager.
+ // Login manger handles the async bits itself, and only calls out
+ // promptAuth, never asyncPromptAuth.
+ //
+ // Bug 565582 will change this.
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /* ---------- nsIWritablePropertyBag2 ---------- */
+
+ // Only a partial implementation, for one specific use case...
+
+ setPropertyAsBool : function(name, value) {
+ if (name == "allowTabModal")
+ this.allowTabModal = value;
+ else
+ throw Cr.NS_ERROR_ILLEGAL_VALUE;
+ },
+};
+
+
+function AuthPromptAdapterFactory() {
+}
+AuthPromptAdapterFactory.prototype = {
+ classID : Components.ID("{6e134924-6c3a-4d86-81ac-69432dd971dc}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPromptAdapterFactory]),
+
+ /* ---------- nsIAuthPromptAdapterFactory ---------- */
+
+ createAdapter : function (oldPrompter) {
+ return new AuthPromptAdapter(oldPrompter);
+ }
+};
+
+
+// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell.
+function AuthPromptAdapter(oldPrompter) {
+ this.oldPrompter = oldPrompter;
+}
+AuthPromptAdapter.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
+ oldPrompter : null,
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth : function (channel, level, authInfo, checkLabel, checkValue) {
+ let message = PromptUtils.makeAuthMessage(channel, authInfo);
+
+ let [username, password] = PromptUtils.getAuthInfo(authInfo);
+ let userParam = { value: username };
+ let passParam = { value: password };
+
+ let [host, realm] = PromptUtils.getAuthTarget(channel, authInfo);
+ let authTarget = host + " (" + realm + ")";
+
+ let ok;
+ if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
+ ok = this.oldPrompter.promptPassword(null, message, authTarget, Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, passParam);
+ else
+ ok = this.oldPrompter.promptUsernameAndPassword(null, message, authTarget, Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, userParam, passParam);
+
+ if (ok)
+ PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value);
+ return ok;
+ },
+
+ asyncPromptAuth : function (channel, callback, context, level, authInfo, checkLabel, checkValue) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+};
+
+
+// Wrapper using the old embedding contractID, since it's already common in
+// the addon ecosystem.
+function EmbedPrompter() {
+}
+EmbedPrompter.prototype = new Prompter();
+EmbedPrompter.prototype.classID = Components.ID("{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}");
+
+var component = [Prompter, EmbedPrompter, AuthPromptAdapterFactory];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/prompts/src/nsPrompter.manifest b/toolkit/components/prompts/src/nsPrompter.manifest
new file mode 100644
index 0000000000..582f6bf597
--- /dev/null
+++ b/toolkit/components/prompts/src/nsPrompter.manifest
@@ -0,0 +1,6 @@
+component {1c978d25-b37f-43a8-a2d6-0c7a239ead87} nsPrompter.js
+contract @mozilla.org/prompter;1 {1c978d25-b37f-43a8-a2d6-0c7a239ead87}
+component {6e134924-6c3a-4d86-81ac-69432dd971dc} nsPrompter.js
+contract @mozilla.org/network/authprompt-adapter-factory;1 {6e134924-6c3a-4d86-81ac-69432dd971dc}
+component {7ad1b327-6dfa-46ec-9234-f2a620ea7e00} nsPrompter.js
+contract @mozilla.org/embedcomp/prompt-service;1 {7ad1b327-6dfa-46ec-9234-f2a620ea7e00}
diff --git a/toolkit/components/prompts/test/.eslintrc.js b/toolkit/components/prompts/test/.eslintrc.js
new file mode 100644
index 0000000000..3c788d6d68
--- /dev/null
+++ b/toolkit/components/prompts/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/prompts/test/bug619644_inner.html b/toolkit/components/prompts/test/bug619644_inner.html
new file mode 100644
index 0000000000..f929c56499
--- /dev/null
+++ b/toolkit/components/prompts/test/bug619644_inner.html
@@ -0,0 +1,7 @@
+<head></head><body><p>Original content</p>
+<script>
+ window.opener.postMessage("", "*");
+ confirm ("Message");
+ document.write ("Extra content");
+ window.opener.postMessage(document.documentElement.innerHTML, "*");
+</script></body>
diff --git a/toolkit/components/prompts/test/bug625187_iframe.html b/toolkit/components/prompts/test/bug625187_iframe.html
new file mode 100644
index 0000000000..740d59a617
--- /dev/null
+++ b/toolkit/components/prompts/test/bug625187_iframe.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <title>Test for Bug 625187 - the iframe</title>
+<!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -
+ - Contributor(s):
+ - Mihai Sucan <mihai.sucan@gmail.com>
+ -->
+</head>
+<body>
+<p><button id="btn1" onclick="alert('hello world 2')">Button 2</button></p>
+<p><button id="btn2" onclick="window.parent.alert('hello world 3')">Button 3</button></p>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/chromeScript.js b/toolkit/components/prompts/test/chromeScript.js
new file mode 100644
index 0000000000..7b2d371007
--- /dev/null
+++ b/toolkit/components/prompts/test/chromeScript.js
@@ -0,0 +1,241 @@
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Timer.jsm");
+
+// Define these to make EventUtils happy.
+let window = this;
+let parent = {};
+
+let EventUtils = {};
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+);
+
+addMessageListener("handlePrompt", msg => {
+ handlePromptWhenItAppears(msg.action, msg.isTabModal, msg.isSelect);
+});
+
+function handlePromptWhenItAppears(action, isTabModal, isSelect) {
+ let interval = setInterval(() => {
+ if (handlePrompt(action, isTabModal, isSelect)) {
+ clearInterval(interval);
+ }
+ }, 100);
+}
+
+function handlePrompt(action, isTabModal, isSelect) {
+ let ui;
+
+ if (isTabModal) {
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let gBrowser = browserWin.gBrowser;
+ let promptManager = gBrowser.getTabModalPromptBox(gBrowser.selectedBrowser);
+ let prompts = promptManager.listPrompts();
+ if (!prompts.length) {
+ return false; // try again in a bit
+ }
+
+ ui = prompts[0].Dialog.ui;
+ } else {
+ let doc = getDialogDoc();
+ if (!doc) {
+ return false; // try again in a bit
+ }
+
+ if (isSelect)
+ ui = doc;
+ else
+ ui = doc.defaultView.Dialog.ui;
+
+ }
+
+ let promptState;
+ if (isSelect) {
+ promptState = getSelectState(ui);
+ dismissSelect(ui, action);
+ } else {
+ promptState = getPromptState(ui);
+ dismissPrompt(ui, action);
+ }
+ sendAsyncMessage("promptHandled", { promptState: promptState });
+ return true;
+}
+
+function getSelectState(ui) {
+ let listbox = ui.getElementById("list");
+
+ let state = {};
+ state.msg = ui.getElementById("info.txt").value;
+ state.selectedIndex = listbox.selectedIndex;
+ state.items = [];
+
+ for (let i = 0; i < listbox.itemCount; i++) {
+ let item = listbox.getItemAtIndex(i).label;
+ state.items.push(item);
+ }
+
+ return state;
+}
+
+function getPromptState(ui) {
+ let state = {};
+ state.msg = ui.infoBody.textContent;
+ state.titleHidden = ui.infoTitle.getAttribute("hidden") == "true";
+ state.textHidden = ui.loginContainer.hidden;
+ state.passHidden = ui.password1Container.hidden;
+ state.checkHidden = ui.checkboxContainer.hidden;
+ state.checkMsg = ui.checkbox.label;
+ state.checked = ui.checkbox.checked;
+ // tab-modal prompts don't have an infoIcon
+ state.iconClass = ui.infoIcon ? ui.infoIcon.className : null;
+ state.textValue = ui.loginTextbox.getAttribute("value");
+ state.passValue = ui.password1Textbox.getAttribute("value");
+
+ state.butt0Label = ui.button0.label;
+ state.butt1Label = ui.button1.label;
+ state.butt2Label = ui.button2.label;
+
+ state.butt0Disabled = ui.button0.disabled;
+ state.butt1Disabled = ui.button1.disabled;
+ state.butt2Disabled = ui.button2.disabled;
+
+ function isDefaultButton(b) {
+ return (b.hasAttribute("default") &&
+ b.getAttribute("default") == "true");
+ }
+ state.defButton0 = isDefaultButton(ui.button0);
+ state.defButton1 = isDefaultButton(ui.button1);
+ state.defButton2 = isDefaultButton(ui.button2);
+
+ let fm = Cc["@mozilla.org/focus-manager;1"].
+ getService(Ci.nsIFocusManager);
+ let e = fm.focusedElement;
+
+ if (e == null) {
+ state.focused = null;
+ } else if (ui.button0.isSameNode(e)) {
+ state.focused = "button0";
+ } else if (ui.button1.isSameNode(e)) {
+ state.focused = "button1";
+ } else if (ui.button2.isSameNode(e)) {
+ state.focused = "button2";
+ } else if (ui.loginTextbox.inputField.isSameNode(e)) {
+ state.focused = "textField";
+ } else if (ui.password1Textbox.inputField.isSameNode(e)) {
+ state.focused = "passField";
+ } else if (ui.infoBody.isSameNode(e)) {
+ state.focused = "infoBody";
+ } else {
+ state.focused = "ERROR: unexpected element focused: " + (e ? e.localName : "<null>");
+ }
+
+ return state;
+}
+
+function dismissSelect(ui, action) {
+ let dialog = ui.getElementsByTagName("dialog")[0];
+ let listbox = ui.getElementById("list");
+
+ if (action.selectItem) {
+ listbox.selectedIndex = 1;
+ }
+
+ if (action.buttonClick == "ok") {
+ dialog.acceptDialog();
+ } else if (action.buttonClick == "cancel") {
+ dialog.cancelDialog();
+ }
+}
+
+function dismissPrompt(ui, action) {
+ if (action.setCheckbox) {
+ // Annoyingly, the prompt code is driven by oncommand.
+ ui.checkbox.setChecked(true);
+ ui.checkbox.doCommand();
+ }
+
+ if ("textField" in action) {
+ ui.loginTextbox.setAttribute("value", action.textField);
+ }
+
+ if ("passField" in action) {
+ ui.password1Textbox.setAttribute("value", action.passField);
+ }
+
+ switch (action.buttonClick) {
+ case "ok":
+ case 0:
+ ui.button0.click();
+ break;
+ case "cancel":
+ case 1:
+ ui.button1.click();
+ break;
+ case 2:
+ ui.button2.click();
+ break;
+ case "ESC":
+ // XXX This is assuming tab-modal.
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ EventUtils.synthesizeKey("KEY_Escape", { code: "Escape" }, browserWin);
+ break;
+ case "pollOK":
+ // Buttons are disabled at the moment, poll until they're reenabled.
+ // Can't use setInterval here, because the window's in a modal state
+ // and thus DOM events are suppressed.
+ let interval = setInterval(() => {
+ if (ui.button0.disabled)
+ return;
+ ui.button0.click();
+ clearInterval(interval);
+ }, 100);
+ break;
+
+ default:
+ throw "dismissPrompt action listed unknown button.";
+ }
+}
+
+function getDialogDoc() {
+ // Trudge through all the open windows, until we find the one
+ // that has either commonDialog.xul or selectDialog.xul loaded.
+ var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ // var enumerator = wm.getEnumerator("navigator:browser");
+ var enumerator = wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ var containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ var childDocShell = containedDocShells.getNext();
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
+ continue;
+ var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ if (childDoc.location.href != "chrome://global/content/commonDialog.xul" &&
+ childDoc.location.href != "chrome://global/content/selectDialog.xul")
+ continue;
+
+ // We're expecting the dialog to be focused. If it's not yet, try later.
+ // (In particular, this is needed on Linux to reliably check focused elements.)
+ let fm = Cc["@mozilla.org/focus-manager;1"].
+ getService(Ci.nsIFocusManager);
+ if (fm.focusedWindow != childDoc.defaultView)
+ continue;
+
+ return childDoc;
+ }
+ }
+
+ return null;
+}
diff --git a/toolkit/components/prompts/test/mochitest.ini b/toolkit/components/prompts/test/mochitest.ini
new file mode 100644
index 0000000000..7f87650d6b
--- /dev/null
+++ b/toolkit/components/prompts/test/mochitest.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files =
+ ../../passwordmgr/test/authenticate.sjs
+ bug619644_inner.html
+ bug625187_iframe.html
+ prompt_common.js
+ chromeScript.js
+
+[test_bug619644.html]
+[test_bug620145.html]
+skip-if = toolkit == 'android' #TIMED_OUT
+[test_subresources_prompts.html]
+skip-if = toolkit == 'android'
+[test_dom_prompts.html]
+skip-if = toolkit == 'android' #android: bug 1267092
+[test_modal_prompts.html]
+skip-if = toolkit == 'android' || (os == 'linux' && (debug || asan)) #android: TIMED_OUT (For Linux : 950636)
+[test_modal_select.html]
+skip-if = toolkit == 'android' #android: TIMED_OUT
diff --git a/toolkit/components/prompts/test/prompt_common.js b/toolkit/components/prompts/test/prompt_common.js
new file mode 100644
index 0000000000..e3a69b3474
--- /dev/null
+++ b/toolkit/components/prompts/test/prompt_common.js
@@ -0,0 +1,158 @@
+const Ci = SpecialPowers.Ci;
+const Cc = SpecialPowers.Cc;
+ok(Ci != null, "Access Ci");
+ok(Cc != null, "Access Cc");
+
+function hasTabModalPrompts() {
+ var prefName = "prompts.tab_modal.enabled";
+ var Services = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm").Services;
+ return Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL &&
+ Services.prefs.getBoolPref(prefName);
+}
+var isTabModal = hasTabModalPrompts();
+var isSelectDialog = false;
+var isOSX = ("nsILocalFileMac" in SpecialPowers.Ci);
+var isE10S = SpecialPowers.Services.appinfo.processType == 2;
+
+
+var gChromeScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("chromeScript.js"));
+SimpleTest.registerCleanupFunction(() => gChromeScript.destroy());
+
+function onloadPromiseFor(id) {
+ var iframe = document.getElementById(id);
+ return new Promise(resolve => {
+ iframe.addEventListener("load", function onload(e) {
+ iframe.removeEventListener("load", onload);
+ resolve(true);
+ });
+ });
+}
+
+function handlePrompt(state, action) {
+ return new Promise(resolve => {
+ gChromeScript.addMessageListener("promptHandled", function handled(msg) {
+ gChromeScript.removeMessageListener("promptHandled", handled);
+ checkPromptState(msg.promptState, state);
+ resolve(true);
+ });
+ gChromeScript.sendAsyncMessage("handlePrompt", { action: action, isTabModal: isTabModal});
+ });
+}
+
+function checkPromptState(promptState, expectedState) {
+ // XXX check title? OS X has title in content
+ is(promptState.msg, expectedState.msg, "Checking expected message");
+ if (isOSX && !isTabModal)
+ ok(!promptState.titleHidden, "Checking title always visible on OS X");
+ else
+ is(promptState.titleHidden, expectedState.titleHidden, "Checking title visibility");
+ is(promptState.textHidden, expectedState.textHidden, "Checking textbox visibility");
+ is(promptState.passHidden, expectedState.passHidden, "Checking passbox visibility");
+ is(promptState.checkHidden, expectedState.checkHidden, "Checking checkbox visibility");
+ is(promptState.checkMsg, expectedState.checkMsg, "Checking checkbox label");
+ is(promptState.checked, expectedState.checked, "Checking checkbox checked");
+ if (!isTabModal)
+ is(promptState.iconClass, "spaced " + expectedState.iconClass, "Checking expected icon CSS class");
+ is(promptState.textValue, expectedState.textValue, "Checking textbox value");
+ is(promptState.passValue, expectedState.passValue, "Checking passbox value");
+
+ if (expectedState.butt0Label) {
+ is(promptState.butt0Label, expectedState.butt0Label, "Checking accept-button label");
+ }
+ if (expectedState.butt1Label) {
+ is(promptState.butt1Label, expectedState.butt1Label, "Checking cancel-button label");
+ }
+ if (expectedState.butt2Label) {
+ is(promptState.butt2Label, expectedState.butt2Label, "Checking extra1-button label");
+ }
+
+ // For prompts with a time-delay button.
+ if (expectedState.butt0Disabled) {
+ is(promptState.butt0Disabled, true, "Checking accept-button is disabled");
+ is(promptState.butt1Disabled, false, "Checking cancel-button isn't disabled");
+ }
+
+ is(promptState.defButton0, expectedState.defButton == "button0", "checking button0 default");
+ is(promptState.defButton1, expectedState.defButton == "button1", "checking button1 default");
+ is(promptState.defButton2, expectedState.defButton == "button2", "checking button2 default");
+
+ if (isOSX && expectedState.focused && expectedState.focused.startsWith("button")) {
+ is(promptState.focused, "infoBody", "buttons don't focus on OS X, but infoBody does instead");
+ } else {
+ is(promptState.focused, expectedState.focused, "Checking focused element");
+ }
+}
+
+function checkEchoedAuthInfo(expectedState, doc) {
+ // The server echos back the HTTP auth info it received.
+ let username = doc.getElementById("user").textContent;
+ let password = doc.getElementById("pass").textContent;
+ let authok = doc.getElementById("ok").textContent;
+
+ is(authok, "PASS", "Checking for successful authentication");
+ is(username, expectedState.user, "Checking for echoed username");
+ is(password, expectedState.pass, "Checking for echoed password");
+}
+
+/**
+ * Create a Proxy to relay method calls on an nsIAuthPrompt[2] prompter to a chrome script which can
+ * perform the calls in the parent. Out and inout params will be copied back from the parent to
+ * content.
+ *
+ * @param chromeScript The reference to the chrome script that will listen to `proxyPrompter`
+ * messages in the parent and call the `methodName` method.
+ * The return value from the message handler should be an object with properties:
+ * `rv` - containing the return value of the method call.
+ * `args` - containing the array of arguments passed to the method since out or inout ones could have
+ * been modified.
+ */
+function PrompterProxy(chromeScript) {
+ return new Proxy({}, {
+ get(target, prop, receiver) {
+ return (...args) => {
+ // Array of indices of out/inout params to copy from the parent back to the caller.
+ let outParams = [];
+
+ switch (prop) {
+ case "prompt": {
+ outParams = [/* result */ 5];
+ break;
+ }
+ case "promptAuth": {
+ outParams = [];
+ break;
+ }
+ case "promptPassword": {
+ outParams = [/* pwd */ 4];
+ break;
+ }
+ case "promptUsernameAndPassword": {
+ outParams = [/* user */ 4, /* pwd */ 5];
+ break;
+ }
+ default: {
+ throw new Error("Unknown nsIAuthPrompt method");
+ }
+ }
+
+ let result = chromeScript.sendSyncMessage("proxyPrompter", {
+ args,
+ methodName: prop,
+ })[0][0];
+
+ for (let outParam of outParams) {
+ // Copy the out or inout param value over the original
+ args[outParam].value = result.args[outParam].value;
+ }
+
+ if (prop == "promptAuth") {
+ args[2].username = result.args[2].username;
+ args[2].password = result.args[2].password;
+ args[2].domain = result.args[2].domain;
+ }
+
+ return result.rv;
+ };
+ },
+ });
+}
diff --git a/toolkit/components/prompts/test/test_bug619644.html b/toolkit/components/prompts/test/test_bug619644.html
new file mode 100644
index 0000000000..9f61eb18bd
--- /dev/null
+++ b/toolkit/components/prompts/test/test_bug619644.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=619644
+-->
+<head>
+ <title>Test for Bug 619644</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=619644">Mozilla Bug 619644</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// This is a little yucky, but it works
+// The contents of bug619644_inner.html
+const expectedFinalDoc =
+"<head><\/head><body><p>Original content<\/p>\n<script>\n window.opener.postMessage(\"\", \"*\");\n confirm (\"Message\");\n document.write (\"Extra content\");\n window.opener.postMessage(document.documentElement.innerHTML, \"*\");\n<\/script>Extra content<\/body>";
+
+if (!isTabModal) {
+ todo(false, "Test disabled when tab modal prompts are not enabled.");
+} else {
+ inittest();
+}
+
+var promptDone;
+
+function inittest() {
+ window.addEventListener("message", runtest, false);
+ window.open("bug619644_inner.html", "619644");
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+function runtest(e) {
+ window.removeEventListener("message", runtest, false);
+ window.addEventListener("message", checktest, false);
+
+ let state = {
+ msg : "Message",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ let action = {
+ buttonClick: "ESC",
+ };
+
+ promptDone = handlePrompt(state, action);
+}
+
+function checktest(e) {
+ is(e.data, expectedFinalDoc, "ESC press should not abort document load");
+ e.source.close();
+ promptDone.then(endtest);
+}
+
+function endtest() {
+ info("Ending test");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_bug620145.html b/toolkit/components/prompts/test/test_bug620145.html
new file mode 100644
index 0000000000..bb4470259d
--- /dev/null
+++ b/toolkit/components/prompts/test/test_bug620145.html
@@ -0,0 +1,105 @@
+<html>
+<head>
+ <title>Test for Bug 620145</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620145">Mozilla Bug 620145</a>
+<pre id="test">
+</pre>
+
+<div id="text" style="max-width: 100px" onmouseup="openAlert()">
+ This is a short piece of text used for testing that mouse selecting is
+ stopped when an alert appears.
+</div>
+<div id="text2" style="max-width: 100px">
+ This is another short piece of text used for testing that mouse selecting is
+ stopped when an alert appears.
+</div>
+<button id="button" onmouseup="openAlert()">Button</button>
+
+<script class="testbody" type="text/javascript">
+var selectionTest = false;
+
+function openAlert() {
+ info("opening alert...");
+ alert("hello!");
+ info("...alert done.");
+}
+
+add_task(function* runTest() {
+ var state, action;
+ // The <button> in this test's HTML opens a prompt when clicked.
+ // Here we send the events to simulate clicking it.
+ info("isTabModal? " + isTabModal);
+ selectionTest = isTabModal;
+
+ state = {
+ msg : "hello!",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ var button = $("button");
+ dispatchMouseEvent(button, "mousedown");
+ dispatchMouseEvent(button, "mouseup");
+ // alert appears at this point, to be closed by the chrome script.
+
+ yield promptDone;
+ checkSelection();
+
+ // using same state and action.
+ promptDone = handlePrompt(state, action);
+
+ var text = $("text");
+ dispatchMouseEvent(text, "mousedown");
+ dispatchMouseEvent(text, "mouseup");
+ // alert appears at this point, to be closed by the chrome script.
+
+ yield promptDone;
+ checkSelection();
+});
+
+function dispatchMouseEvent(target, type)
+{
+ var win = target.ownerDocument.defaultView;
+ e = document.createEvent("MouseEvent");
+ e.initEvent(type, false, false, win, 0, 1, 1, 1, 1,
+ false, false, false, false, 0, null);
+ var utils = SpecialPowers.getDOMWindowUtils(win);
+ utils.dispatchDOMEventViaPresShell(target, e, true);
+ ok(true, type + " sent to " + target.id);
+}
+
+function checkSelection()
+{
+ if (!selectionTest) {
+ todo(false, "Selection test is disabled when tab modal prompts are not enabled.");
+ } else {
+ synthesizeMouse($("text"), 25, 55, { type: "mousemove" });
+ is(window.getSelection().toString(), "", "selection not made");
+ }
+}
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_dom_prompts.html b/toolkit/components/prompts/test/test_dom_prompts.html
new file mode 100644
index 0000000000..413ed8fd51
--- /dev/null
+++ b/toolkit/components/prompts/test/test_dom_prompts.html
@@ -0,0 +1,208 @@
+<html>
+<head>
+ <title>Test for DOM prompts</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+var rv;
+var state, action;
+
+add_task(function* test_alert_ok() {
+ info("Starting test: Alert");
+ state = {
+ msg : "This is the alert text.",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ alert("This is the alert text.");
+
+ yield promptDone;
+});
+
+// bug 861605 made the arguments to alert/confirm optional (prompt already was).
+add_task(function* test_alert_noargs() {
+ info("Starting test: Alert with no args");
+ state = {
+ msg : "",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ try {
+ alert();
+ ok(true, "alert() without arguments should not throw!");
+ } catch (e) {
+ ok(false, "alert() without arguments should not throw!");
+ }
+
+ yield promptDone;
+});
+
+
+add_task(function* test_confirm_ok() {
+ info("Starting test: Confirm");
+ state = {
+ msg : "This is the confirm text.",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ rv = confirm("This is the confirm text.");
+ is(rv, true, "check prompt return value");
+
+ yield promptDone;
+});
+
+// bug 861605 made the arguments to alert/confirm optional (prompt already was).
+add_task(function* test_confirm_noargs() {
+ info("Starting test: Confirm with no args");
+ state = {
+ msg : "",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ try {
+ rv = confirm();
+ ok(true, "confirm() without arguments should not throw!");
+ } catch (e) {
+ ok(false, "confirm() without arguments should not throw!");
+ }
+ is(rv, true, "check prompt return value");
+
+ yield promptDone;
+});
+
+
+add_task(function* test_prompt_ok() {
+ info("Starting test: Prompt");
+ state = {
+ msg : "This is the Prompt text.",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ rv = prompt("This is the Prompt text.");
+ is(rv, "", "check prompt return value");
+
+ yield promptDone;
+});
+
+// bug 861605 made the arguments to alert/confirm optional (prompt already was).
+add_task(function* test_prompt_noargs() {
+ info("Starting test: Prompt with no args");
+ state = {
+ msg : "",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ try {
+ rv = prompt();
+ ok(true, "prompt() without arguments should not throw!");
+ } catch (e) {
+ ok(false, "prompt() without arguments should not throw!");
+ }
+ is(rv, "", "check prompt return value");
+
+ yield promptDone;
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_modal_prompts.html b/toolkit/components/prompts/test/test_modal_prompts.html
new file mode 100644
index 0000000000..42e6be52c3
--- /dev/null
+++ b/toolkit/components/prompts/test/test_modal_prompts.html
@@ -0,0 +1,1184 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Modal Prompts Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Prompter tests: modal prompts
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.8">
+
+function* runTests() {
+ const { NetUtil } = SpecialPowers.Cu.import('resource://gre/modules/NetUtil.jsm');
+ let state, action;
+ ok(true, "Running tests (isTabModal=" + isTabModal + ", usePromptService=" + usePromptService + ")");
+
+ let prompter, promptArgs;
+ if (usePromptService) {
+ prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+ } else {
+ prompter = Cc["@mozilla.org/prompter;1"].
+ getService(Ci.nsIPromptFactory).
+ getPrompt(window, Ci.nsIPrompt);
+ if (isTabModal) {
+ let bag = prompter.QueryInterface(Ci.nsIWritablePropertyBag2);
+ bag.setPropertyAsBool("allowTabModal", true);
+ }
+ }
+
+ let checkVal = {};
+ let textVal = {};
+ let passVal = {};
+ let flags;
+ let isOK, clickedButton;
+
+ // =====
+ info("Starting test: Alert");
+ state = {
+ msg : "This is the alert text.",
+ title : "TestTitle",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the alert text."];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ prompter.alert.apply(null, promptArgs);
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: AlertCheck (null checkbox label, so it's hidden)");
+ state = {
+ msg : "This is the alertCheck text.",
+ title : "TestTitle",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the alertCheck text.", null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ prompter.alertCheck.apply(null, promptArgs);
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: AlertCheck");
+ state = {
+ msg : "This is the alertCheck text.",
+ title : "TestTitle",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the alertCheck text.", "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ prompter.alertCheck.apply(null, promptArgs);
+ is(checkVal.value, true, "checkbox was checked");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Confirm (ok)");
+ state = {
+ msg : "This is the confirm text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the confirm text."];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.confirm.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Confirm (cancel)");
+ state = {
+ msg : "This is the confirm text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the confirm text."];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.confirm.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (ok, null checkbox label)");
+ state = {
+ msg : "This is the confirmCheck text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the confirmCheck text.", null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.confirmCheck.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (cancel, null checkbox label)");
+ state = {
+ msg : "This is the confirmCheck text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the confirmCheck text.", null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.confirmCheck.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (ok)");
+ state = {
+ msg : "This is the confirmCheck text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the confirmCheck text.", "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.confirmCheck.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (cancel)");
+ state = {
+ msg : "This is the confirmCheck text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the confirmCheck text.", "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.confirmCheck.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Prompt (ok, no default text)");
+ state = {
+ msg : "This is the prompt text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ textField : "bacon",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "";
+ promptArgs = ["TestTitle", "This is the prompt text.", textVal, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.prompt.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(textVal.value, "bacon", "checking expected text value");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Prompt (ok, default text)");
+ state = {
+ msg : "This is the prompt text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "kittens",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "kittens";
+ promptArgs = ["TestTitle", "This is the prompt text.", textVal, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.prompt.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(textVal.value, "kittens", "checking expected text value");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Prompt (cancel, default text)");
+ state = {
+ msg : "This is the prompt text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "puppies",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "puppies";
+ promptArgs = ["TestTitle", "This is the prompt text.", textVal, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.prompt.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+ is(textVal.value, "puppies", "checking expected text value");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Prompt (cancel, default text modified)");
+ state = {
+ msg : "This is the prompt text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "puppies",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ textField : "bacon",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "puppies";
+ promptArgs = ["TestTitle", "This is the prompt text.", textVal, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.prompt.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+ is(textVal.value, "puppies", "checking expected text value");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Prompt (ok, with checkbox)");
+ state = {
+ msg : "This is the prompt text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "tribbles",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "tribbles";
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the prompt text.", textVal, "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.prompt.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(textVal.value, "tribbles", "checking expected text value");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: Prompt (cancel, with checkbox)");
+ state = {
+ msg : "This is the prompt text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "tribbles",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "tribbles";
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the prompt text.", textVal, "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.prompt.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+ is(textVal.value, "tribbles", "checking expected text value");
+ is(checkVal.value, false, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ // Just two tests for this, since password manager already tests this extensively.
+ info("Starting test: PromptUsernameAndPassword (ok)");
+ state = {
+ msg : "This is the pUAP text.",
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "usr",
+ passValue : "ssh",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ textField: "newusr",
+ passField: "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "usr";
+ passVal.value = "ssh";
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the pUAP text.", textVal, passVal, "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.promptUsernameAndPassword.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(textVal.value, "newusr", "checking expected text value");
+ is(passVal.value, "newssh", "checking expected pass value");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: PromptUsernameAndPassword (cancel)");
+ state = {
+ msg : "This is the pUAP text.",
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "usr",
+ passValue : "ssh",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ setCheckbox : true,
+ textField : "newusr",
+ passField : "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "usr";
+ passVal.value = "ssh";
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the pUAP text.", textVal, passVal, "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.promptUsernameAndPassword.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+ is(textVal.value, "usr", "checking expected text value");
+ is(passVal.value, "ssh", "checking expected pass value");
+ is(checkVal.value, false, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: PromptPassword (ok)");
+ state = {
+ msg : "This is the promptPassword text.",
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "",
+ passValue : "ssh",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ setCheckbox : true,
+ passField : "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ passVal.value = "ssh";
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the promptPassword text.", passVal, "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.promptPassword.apply(null, promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(passVal.value, "newssh", "checking expected pass value");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: PromptPassword (cancel)");
+ state = {
+ msg : "This is the promptPassword text.",
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "",
+ passValue : "ssh",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "passField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "cancel",
+ setCheckbox : true,
+ passField : "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ passVal.value = "ssh";
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the promptPassword text.", passVal, "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ isOK = prompter.promptPassword.apply(null, promptArgs);
+ is(isOK, false, "checked expected retval");
+ is(passVal.value, "ssh", "checking expected pass value");
+ is(checkVal.value, false, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (ok/cancel, ok)");
+ state = {
+ msg : "This is the confirmEx text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ butt0Label : "OK",
+ butt1Label : "Cancel",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ flags = Ci.nsIPromptService.STD_OK_CANCEL_BUTTONS;
+ promptArgs = ["TestTitle", "This is the confirmEx text.", flags, null, null, null, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ clickedButton = prompter.confirmEx.apply(null, promptArgs);
+ is(clickedButton, 0, "checked expected button num click");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (yes/no, cancel)");
+ state = {
+ msg : "This is the confirmEx text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ butt0Label : "Yes",
+ butt1Label : "No",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ flags = Ci.nsIPromptService.STD_YES_NO_BUTTONS;
+ promptArgs = ["TestTitle", "This is the confirmEx text.", flags, null, null, null, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ clickedButton = prompter.confirmEx.apply(null, promptArgs);
+ is(clickedButton, 1, "checked expected button num click");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (buttons from args, checkbox, ok)");
+ state = {
+ msg : "This is the confirmEx text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ butt0Label : "butt0",
+ butt1Label : "butt1",
+ butt2Label : "butt2",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ let b = Ci.nsIPromptService.BUTTON_TITLE_IS_STRING;
+ flags = b * Ci.nsIPromptService.BUTTON_POS_2 +
+ b * Ci.nsIPromptService.BUTTON_POS_1 +
+ b * Ci.nsIPromptService.BUTTON_POS_0;
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the confirmEx text.", flags,
+ "butt0", "butt1", "butt2", "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ clickedButton = prompter.confirmEx.apply(null, promptArgs);
+ is(clickedButton, 0, "checked expected button num click");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (buttons from args, checkbox, cancel)");
+ state = {
+ msg : "This is the confirmEx text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "button1", // Default changed!
+ defButton : "button1",
+ butt0Label : "butt0",
+ butt1Label : "butt1",
+ butt2Label : "butt2",
+ };
+ action = {
+ buttonClick: "cancel",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ b = Ci.nsIPromptService.BUTTON_TITLE_IS_STRING;
+ flags = b * Ci.nsIPromptService.BUTTON_POS_2 +
+ b * Ci.nsIPromptService.BUTTON_POS_1 +
+ b * Ci.nsIPromptService.BUTTON_POS_0;
+ flags ^= Ci.nsIPromptService.BUTTON_POS_1_DEFAULT;
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the confirmEx text.", flags,
+ "butt0", "butt1", "butt2", "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ clickedButton = prompter.confirmEx.apply(null, promptArgs);
+ is(clickedButton, 1, "checked expected button num click");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (buttons from args, checkbox, button3)");
+ state = {
+ msg : "This is the confirmEx text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "button2", // Default changed!
+ defButton : "button2",
+ butt0Label : "butt0",
+ butt1Label : "butt1",
+ butt2Label : "butt2",
+ };
+ action = {
+ buttonClick: 2,
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ b = Ci.nsIPromptService.BUTTON_TITLE_IS_STRING;
+ flags = b * Ci.nsIPromptService.BUTTON_POS_2 +
+ b * Ci.nsIPromptService.BUTTON_POS_1 +
+ b * Ci.nsIPromptService.BUTTON_POS_0;
+ flags ^= Ci.nsIPromptService.BUTTON_POS_2_DEFAULT;
+ checkVal.value = false;
+ promptArgs = ["TestTitle", "This is the confirmEx text.", flags,
+ "butt0", "butt1", "butt2", "Check me out!", checkVal];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ clickedButton = prompter.confirmEx.apply(null, promptArgs);
+ is(clickedButton, 2, "checked expected button num click");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+
+ // =====
+ // (skipped for E10S and tabmodal tests: window is required)
+ info("Starting test: Alert, no window");
+ state = {
+ msg : "This is the alert text.",
+ title : "TestTitle",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ if (!isTabModal && !isE10S) {
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the alert text."];
+ if (usePromptService)
+ promptArgs.unshift(null);
+ prompter.alert.apply(null, promptArgs);
+
+ yield promptDone;
+ }
+
+
+ // =====
+ // (skipped for tabmodal tests: delay not supported)
+ info("Starting test: ConfirmEx (delay, ok)");
+ state = {
+ msg : "This is the confirmEx delay text.",
+ title : "TestTitle",
+ iconClass : "question-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : null, // nothing focused until after delay fires
+ defButton : "button0",
+ butt0Label : "OK",
+ butt1Label : "Cancel",
+ butt0Disabled: true,
+ };
+
+ // OS X doesn't initially focus the button, but rather the infoBody.
+ // The focus stays there even after the button-enable delay has fired.
+ if (isOSX)
+ state.focused = "infoBody";
+
+ action = {
+ buttonClick: "pollOK",
+ };
+ if (!isTabModal) {
+ promptDone = handlePrompt(state, action);
+
+ flags = (Ci.nsIPromptService.STD_OK_CANCEL_BUTTONS | Ci.nsIPromptService.BUTTON_DELAY_ENABLE);
+ promptArgs = ["TestTitle", "This is the confirmEx delay text.", flags, null, null, null, null, {}];
+ if (usePromptService)
+ promptArgs.unshift(window);
+ clickedButton = prompter.confirmEx.apply(null, promptArgs);
+ is(clickedButton, 0, "checked expected button num click");
+
+ yield promptDone;
+ }
+
+ // promptAuth already tested via password manager but do a few specific things here.
+ var channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true
+ });
+
+ var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+ var authinfo = {
+ username : "",
+ password : "",
+ domain : "",
+ flags : Ci.nsIAuthInformation.AUTH_HOST,
+ authenticationScheme : "basic",
+ realm : ""
+ };
+
+
+ // =====
+ // (promptAuth is only accessible from the prompt service)
+ info("Starting test: promptAuth with empty realm");
+ state = {
+ msg : 'http://example.com is requesting your username and password.',
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ setCheckbox : true,
+ textField : "username",
+ passField : "password",
+ };
+ if (usePromptService) {
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+ }
+
+
+ // =====
+ // (promptAuth is only accessible from the prompt service)
+ info("Starting test: promptAuth with long realm");
+ state = {
+ msg : 'http://example.com is requesting your username and password. The site ' +
+ 'says: \u201cabcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi ' +
+ 'abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi ' +
+ 'abcdefghi \u2026\u201d',
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ setCheckbox : true,
+ textField : "username",
+ passField : "password",
+ };
+ if (usePromptService) {
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ var longString = "";
+ for (var i = 0; i < 20; i++)
+ longString += "abcdefghi "; // 200 chars long
+ authinfo.realm = longString;
+ authinfo.username = "";
+ authinfo.password = "";
+ isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+ is(checkVal.value, true, "expected checkbox setting");
+
+ yield promptDone;
+ }
+
+ info("Starting test: promptAuth for a cross-origin and a empty realm");
+ authinfo = {
+ username : "",
+ password : "",
+ domain : "",
+ flags : Ci. nsIAuthInformation.AUTH_HOST |
+ Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE,
+ authenticationScheme : "basic",
+ realm : ""
+ }
+ state = {
+ msg : 'http://example.com is requesting your username and password. ' +
+ 'WARNING: Your password will not be sent to the website you are currently visiting!',
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ setCheckbox : false,
+ textField : "username",
+ passField : "password",
+ };
+ if (usePromptService) {
+ promptDone = handlePrompt(state, action);
+ checkVal.value = false;
+ isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+ is(checkVal.value, false, "expected checkbox setting");
+
+ yield promptDone;
+ }
+
+ info("Starting test: promptAuth for a cross-origin with realm");
+ authinfo = {
+ username : "",
+ password : "",
+ domain : "",
+ flags : Ci. nsIAuthInformation.AUTH_HOST | Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE,
+ authenticationScheme : "basic",
+ realm : "Something!!!"
+ }
+ state = {
+ msg : 'http://example.com is requesting your username and password. ' +
+ 'WARNING: Your password will not be sent to the website you are currently visiting!',
+ title : "TestTitle",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : false,
+ textValue : "",
+ passValue : "",
+ checkMsg : "Check me out!",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick : "ok",
+ setCheckbox : false,
+ textField : "username",
+ passField : "password",
+ };
+ if (usePromptService) {
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+ is(checkVal.value, false, "expected checkbox setting");
+
+ yield promptDone;
+ }
+}
+
+let usePromptService;
+
+/*
+ * Run the body of the 3 times:
+ * - 1st pass: with window-modal prompts, using nsIPromptService
+ * - 2nd pass: still window-modal, using nsIPrompt directly (via nsIPromptFactory)
+ * - 3rd pass: with tab-modal prompts. Can't opt into these via * nsIPromptService.
+ */
+
+add_task(function* runPromptTests() {
+ info("Process type: " + SpecialPowers.Services.appinfo.processType);
+
+ isTabModal = false; usePromptService = true;
+ info("Running tests with: isTabModal=" + isTabModal + ", usePromptService=" + usePromptService);
+ yield* runTests();
+
+ isTabModal = false; usePromptService = false;
+ info("Running tests with: isTabModal=" + isTabModal + ", usePromptService=" + usePromptService);
+ yield* runTests();
+
+ if (SpecialPowers.getBoolPref("prompts.tab_modal.enabled")) {
+ isTabModal = true; usePromptService = false;
+ info("Running tests with: isTabModal=" + isTabModal + ", usePromptService=" + usePromptService);
+ yield* runTests();
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_modal_select.html b/toolkit/components/prompts/test/test_modal_select.html
new file mode 100644
index 0000000000..1e008d0f44
--- /dev/null
+++ b/toolkit/components/prompts/test/test_modal_select.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Modal Prompts Test</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Prompter tests: modal prompts
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.8">
+
+let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+
+function checkPromptState(promptState, expectedState) {
+ // XXX check title? OS X has title in content
+ // XXX check focused element
+ // XXX check button labels?
+
+ is(promptState.msg, expectedState.msg, "Checking expected message");
+
+ // Compare listbox contents
+ is(promptState.items.length, expectedState.items.length, "Checking listbox length");
+
+ if (promptState.items.length)
+ is(promptState.selectedIndex, 0, "Checking selected index");
+
+ for (let i = 0; i < promptState.items; i++) {
+ is(promptState.items[i], expectedState.items[i], "Checking list item #" + i);
+ }
+}
+
+let selectVal = {};
+let isOK;
+let state, action;
+
+function handlePrompt(state, action) {
+ return new Promise(resolve => {
+ gChromeScript.addMessageListener("promptHandled", function handled(msg) {
+ gChromeScript.removeMessageListener("promptHandled", handled);
+ checkPromptState(msg.promptState, state);
+ resolve(true);
+ });
+ gChromeScript.sendAsyncMessage("handlePrompt", { action: action, isSelect: true});
+ });
+}
+
+
+// =====
+add_task(function* test_select_empty_list() {
+ info("Starting test: Select (0 items, ok)");
+ state = {
+ msg : "This is the select text.",
+ title : "TestTitle",
+ items : [],
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ promptDone = handlePrompt(state, action);
+ items = [];
+ selectVal.value = null; // outparam, just making sure.
+ isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal);
+ is(isOK, true, "checked expected retval");
+ is(selectVal.value, -1, "checking selected index");
+
+ yield promptDone;
+});
+
+// =====
+add_task(function* test_select_ok() {
+ info("Starting test: Select (3 items, ok)");
+ state = {
+ msg : "This is the select text.",
+ title : "TestTitle",
+ items : ["one", "two", "three"],
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ promptDone = handlePrompt(state, action);
+ items = ["one", "two", "three"];
+ selectVal.value = null; // outparam, just making sure.
+ isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal);
+ is(isOK, true, "checked expected retval");
+ is(selectVal.value, 0, "checking selected index");
+
+ yield promptDone;
+});
+
+// =====
+add_task(function* test_select_item() {
+ info("Starting test: Select (3 items, selection changed, ok)");
+ state = {
+ msg : "This is the select text.",
+ title : "TestTitle",
+ items : ["one", "two", "three"],
+ };
+ action = {
+ buttonClick: "ok",
+ selectItem: 1,
+ };
+ promptDone = handlePrompt(state, action);
+ items = ["one", "two", "three"];
+ selectVal.value = null; // outparam, just making sure.
+ isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal);
+ is(isOK, true, "checked expected retval");
+ is(selectVal.value, 1, "checking selected index");
+
+ yield promptDone;
+});
+
+// =====
+add_task(function* test_cancel_prompt() {
+ info("Starting test: Select (3 items, cancel)");
+ state = {
+ msg : "This is the select text.",
+ title : "TestTitle",
+ items : ["one", "two", "three"],
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ items = ["one", "two", "three"];
+ selectVal.value = null; // outparam, just making sure.
+ isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal);
+ is(isOK, false, "checked expected retval");
+ is(selectVal.value, 0, "checking selected index");
+
+ yield promptDone;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_subresources_prompts.html b/toolkit/components/prompts/test/test_subresources_prompts.html
new file mode 100644
index 0000000000..241ce430f9
--- /dev/null
+++ b/toolkit/components/prompts/test/test_subresources_prompts.html
@@ -0,0 +1,202 @@
+<html>
+<head>
+ <title>Test subresources prompts (Bug 625187 and bug 1230462)</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -
+ - Contributor(s):
+ - Mihai Sucan <mihai.sucan@gmail.com>
+ -->
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=625187">Mozilla Bug 625187</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230462">Mozilla Bug 1230462</a>
+
+<p><button onclick="alert('hello world')">Button</button></p>
+
+<iframe id="iframe_diff_origin" src="http://example.com/tests/toolkit/components/prompts/test/bug625187_iframe.html"></iframe>
+
+<iframe id="iframe_same_origin" src="bug625187_iframe.html"></iframe>
+
+<iframe id="iframe_prompt"></iframe>
+
+<pre id="test"></pre>
+
+<script class="testbody" type="text/javascript">
+var iframe1Loaded = onloadPromiseFor("iframe_diff_origin");
+var iframe2Loaded = onloadPromiseFor("iframe_same_origin");
+var iframe_prompt = document.getElementById("iframe_prompt");
+
+add_task(function* runTest()
+{
+ // This test depends on tab modal prompts being enabled.
+ if (!isTabModal) {
+ todo(false, "Test disabled when tab modal prompts are not enabled.");
+ return;
+ }
+
+ info("Ensuring iframe1 has loaded...");
+ yield iframe1Loaded;
+ info("Ensuring iframe2 has loaded...");
+ yield iframe2Loaded;
+ let state, action;
+
+ state = {
+ msg : "hello world",
+ iconClass : "alert-icon",
+ titleHidden : true,
+ textHidden : true,
+ passHidden : true,
+ checkHidden : true,
+ textValue : "",
+ passValue : "",
+ checkMsg : "",
+ checked : false,
+ focused : "button0",
+ defButton : "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ var button = document.querySelector("button");
+ dispatchMouseEvent(button, "click");
+
+ yield promptDone;
+
+
+ // mostly reusing same state/action
+ state.titleHidden = false;
+ state.msg = "hello world 2";
+ promptDone = handlePrompt(state, action);
+
+ var iframe = document.getElementById("iframe_diff_origin");
+ button = SpecialPowers.wrap(iframe.contentWindow).document.getElementById("btn1");
+ dispatchMouseEvent(button, "click");
+
+ yield promptDone;
+
+
+ // mostly reusing same state/action
+ state.titleHidden = true;
+ state.msg = "hello world 2";
+ promptDone = handlePrompt(state, action);
+
+ iframe = document.getElementById("iframe_same_origin");
+ button = iframe.contentWindow.document.getElementById("btn1");
+ dispatchMouseEvent(button, "click");
+
+ yield promptDone;
+
+
+ // mostly reusing same state/action
+ state.msg = "hello world 3";
+ promptDone = handlePrompt(state, action);
+
+ button = iframe.contentWindow.document.getElementById("btn2");
+ dispatchMouseEvent(button, "click");
+
+ yield promptDone;
+});
+
+add_task(function* runTestAuth()
+{
+ // Following tests chack prompt message for a cross-origin and not
+ // cross-origin subresources load
+
+ // Force parent to not look for tab-modal prompts, as they're not
+ // used for auth prompts.
+ isTabModal = false;
+
+ state = {
+ msg : "http://mochi.test:8888 is requesting your username " +
+ "and password. The site says: “mochitest”",
+ title : "Authentication Required",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+
+ action = {
+ buttonClick : "ok",
+ setCheckbox : false,
+ textField : "mochiuser1",
+ passField : "mochipass1",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ var iframe3Loaded = onloadPromiseFor("iframe_prompt");
+ iframe_prompt.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ yield promptDone;
+ yield iframe3Loaded;
+ checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe_prompt.contentDocument);
+
+ // Cross-origin subresourse test.
+
+ // Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+ isTabModal =false;
+ state = {
+ msg : "http://example.com is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!",
+ title : "Authentication Required",
+ textValue : "",
+ passValue : "",
+ iconClass : "authentication-icon question-icon",
+ titleHidden : true,
+ textHidden : false,
+ passHidden : false,
+ checkHidden : true,
+ checkMsg : "",
+ checked : false,
+ focused : "textField",
+ defButton : "button0",
+ };
+
+ action = {
+ buttonClick : "ok",
+ setCheckbox : false,
+ textField : "mochiuser2",
+ passField : "mochipass2",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ iframe3Loaded = onloadPromiseFor("iframe_prompt");
+ iframe_prompt.src = "http://example.com/tests/toolkit/components/prompts/test/authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest";
+ yield promptDone;
+ yield iframe3Loaded;
+ checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
+ SpecialPowers.wrap(iframe_prompt.contentWindow).document);
+});
+
+function dispatchMouseEvent(target, type)
+{
+ var win = SpecialPowers.unwrap(target.ownerDocument.defaultView);
+ var e = document.createEvent("MouseEvent");
+ e.initEvent(type, false, false, win, 0, 1, 1, 1, 1,
+ false, false, false, false, 0, null);
+ var utils = SpecialPowers.getDOMWindowUtils(win);
+ utils.dispatchDOMEventViaPresShell(SpecialPowers.unwrap(target), e, true);
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/components/protobuf/COPYING.txt b/toolkit/components/protobuf/COPYING.txt
new file mode 100644
index 0000000000..705db579c9
--- /dev/null
+++ b/toolkit/components/protobuf/COPYING.txt
@@ -0,0 +1,33 @@
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it. This code is not
+standalone and requires a support library to be linked with it. This
+support library is itself covered by the above license.
diff --git a/toolkit/components/protobuf/README.txt b/toolkit/components/protobuf/README.txt
new file mode 100644
index 0000000000..c63704ac50
--- /dev/null
+++ b/toolkit/components/protobuf/README.txt
@@ -0,0 +1,25 @@
+Protocol Buffers (protobuf) source is available (via svn) at:
+
+ svn checkout http://protobuf.googlecode.com/svn/trunk/ protobuf-read-only
+
+Or via git at:
+
+ https://github.com/google/protobuf
+
+This code is covered under the BSD license (see COPYING.txt). Documentation is
+available at http://code.google.com/p/protobuf.
+
+The tree's current version of the protobuf library is 2.6.1.
+
+We do not include the protobuf tests or the protoc compiler.
+
+--------------------------------------------------------------------------------
+
+# Upgrading the Protobuf Library
+
+1. Get a new protobuf release from https://github.com/google/protobuf/releases
+
+2. Run `$ ./toolkit/components/protobuf/upgrade_protobuf.sh ~/path/to/release/checkout/of/protobuf`.
+
+3. Update the moz.build to export the new set of headers and add any new .cc
+ files to the unified sources and remove old ones.
diff --git a/toolkit/components/protobuf/m-c-changes.patch b/toolkit/components/protobuf/m-c-changes.patch
new file mode 100644
index 0000000000..d09e7c5a82
--- /dev/null
+++ b/toolkit/components/protobuf/m-c-changes.patch
@@ -0,0 +1,410 @@
+--- a/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.h
++++ b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.h
+@@ -35,16 +35,17 @@
+ // Sanjay Ghemawat, Jeff Dean, and others.
+ //
+ // This header is logically internal, but is made public because it is used
+ // from protocol-compiler-generated code, which may reside in other components.
+
+ #ifndef GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_H__
+ #define GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_H__
+
++#include <algorithm>
+ #include <string>
+ #include <google/protobuf/stubs/common.h>
+ #include <google/protobuf/message_lite.h>
+ #include <google/protobuf/io/coded_stream.h> // for CodedOutputStream::Varint32Size
+
+ namespace google {
+
+ namespace protobuf {
+--- a/toolkit/components/protobuf/src/google/protobuf/wire_format.cc
++++ b/toolkit/components/protobuf/src/google/protobuf/wire_format.cc
+@@ -819,30 +819,35 @@ void WireFormat::SerializeFieldWithCachedSizes(
+ HANDLE_PRIMITIVE_TYPE(SFIXED64, int64, SFixed64, Int64)
+
+ HANDLE_PRIMITIVE_TYPE(FLOAT , float , Float , Float )
+ HANDLE_PRIMITIVE_TYPE(DOUBLE, double, Double, Double)
+
+ HANDLE_PRIMITIVE_TYPE(BOOL, bool, Bool, Bool)
+ #undef HANDLE_PRIMITIVE_TYPE
+
+-#define HANDLE_TYPE(TYPE, TYPE_METHOD, CPPTYPE_METHOD) \
+- case FieldDescriptor::TYPE_##TYPE: \
+- WireFormatLite::Write##TYPE_METHOD( \
+- field->number(), \
+- field->is_repeated() ? \
+- message_reflection->GetRepeated##CPPTYPE_METHOD( \
+- message, field, j) : \
+- message_reflection->Get##CPPTYPE_METHOD(message, field), \
+- output); \
++ case FieldDescriptor::TYPE_GROUP:
++ WireFormatLite::WriteGroup(
++ field->number(),
++ field->is_repeated() ?
++ message_reflection->GetRepeatedMessage(
++ message, field, j) :
++ message_reflection->GetMessage(message, field),
++ output);
+ break;
+
+- HANDLE_TYPE(GROUP , Group , Message)
+- HANDLE_TYPE(MESSAGE, Message, Message)
+-#undef HANDLE_TYPE
++ case FieldDescriptor::TYPE_MESSAGE:
++ WireFormatLite::WriteMessage(
++ field->number(),
++ field->is_repeated() ?
++ message_reflection->GetRepeatedMessage(
++ message, field, j) :
++ message_reflection->GetMessage(message, field),
++ output);
++ break;
+
+ case FieldDescriptor::TYPE_ENUM: {
+ const EnumValueDescriptor* value = field->is_repeated() ?
+ message_reflection->GetRepeatedEnum(message, field, j) :
+ message_reflection->GetEnum(message, field);
+ if (is_packed) {
+ WireFormatLite::WriteEnumNoTag(value->number(), output);
+ } else {
+--- b/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.cc
++++ a/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.cc
+@@ -28,17 +28,16 @@
+ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ // Author: brianolson@google.com (Brian Olson)
+ //
+ // This file contains the implementation of classes GzipInputStream and
+ // GzipOutputStream.
+
+-#include "config.h"
+
+ #if HAVE_ZLIB
+ #include <google/protobuf/io/gzip_stream.h>
+
+ #include <google/protobuf/stubs/common.h>
+
+ namespace google {
+ namespace protobuf {
+--- b/toolkit/components/protobuf/src/google/protobuf/stubs/common.cc
++++ a/toolkit/components/protobuf/src/google/protobuf/stubs/common.cc
+@@ -31,23 +31,22 @@
+ // Author: kenton@google.com (Kenton Varda)
+
+ #include <google/protobuf/stubs/common.h>
+ #include <google/protobuf/stubs/once.h>
+ #include <stdio.h>
+ #include <errno.h>
+ #include <vector>
+
+-#include "config.h"
+
+ #ifdef _WIN32
+ #define WIN32_LEAN_AND_MEAN // We only need minimal includes
+ #include <windows.h>
+ #define snprintf _snprintf // see comment in strutil.cc
++#elif defined(HAVE_PTHREAD_H)
+-#elif defined(HAVE_PTHREAD)
+ #include <pthread.h>
+ #else
+ #error "No suitable threading library available."
+ #endif
+
+ namespace google {
+ namespace protobuf {
+
+--- b/toolkit/components/protobuf/src/google/protobuf/stubs/common.h
++++ a/toolkit/components/protobuf/src/google/protobuf/stubs/common.h
+@@ -363,71 +363,20 @@
+ // or to make sure a struct is smaller than a certain size:
+ //
+ // COMPILE_ASSERT(sizeof(foo) < 128, foo_too_large);
+ //
+ // The second argument to the macro is the name of the variable. If
+ // the expression is false, most compilers will issue a warning/error
+ // containing the name of the variable.
+
++#define GOOGLE_COMPILE_ASSERT(expr, msg) static_assert(expr, #msg)
+-namespace internal {
+-
+-template <bool>
+-struct CompileAssert {
+-};
+-
+-} // namespace internal
+
+-#undef GOOGLE_COMPILE_ASSERT
+-#define GOOGLE_COMPILE_ASSERT(expr, msg) \
+- typedef ::google::protobuf::internal::CompileAssert<(bool(expr))> \
+- msg[bool(expr) ? 1 : -1]
+
+
+-// Implementation details of COMPILE_ASSERT:
+-//
+-// - COMPILE_ASSERT works by defining an array type that has -1
+-// elements (and thus is invalid) when the expression is false.
+-//
+-// - The simpler definition
+-//
+-// #define COMPILE_ASSERT(expr, msg) typedef char msg[(expr) ? 1 : -1]
+-//
+-// does not work, as gcc supports variable-length arrays whose sizes
+-// are determined at run-time (this is gcc's extension and not part
+-// of the C++ standard). As a result, gcc fails to reject the
+-// following code with the simple definition:
+-//
+-// int foo;
+-// COMPILE_ASSERT(foo, msg); // not supposed to compile as foo is
+-// // not a compile-time constant.
+-//
+-// - By using the type CompileAssert<(bool(expr))>, we ensures that
+-// expr is a compile-time constant. (Template arguments must be
+-// determined at compile-time.)
+-//
+-// - The outter parentheses in CompileAssert<(bool(expr))> are necessary
+-// to work around a bug in gcc 3.4.4 and 4.0.1. If we had written
+-//
+-// CompileAssert<bool(expr)>
+-//
+-// instead, these compilers will refuse to compile
+-//
+-// COMPILE_ASSERT(5 > 0, some_message);
+-//
+-// (They seem to think the ">" in "5 > 0" marks the end of the
+-// template argument list.)
+-//
+-// - The array size is (bool(expr) ? 1 : -1), instead of simply
+-//
+-// ((expr) ? 1 : -1).
+-//
+-// This is to avoid running into a bug in MS VC 7.1, which
+-// causes ((0.0) ? 1 : -1) to incorrectly evaluate to 1.
+-
+ // ===================================================================
+ // from google3/base/scoped_ptr.h
+
+ namespace internal {
+
+ // This is an implementation designed to match the anticipated future TR2
+ // implementation of the scoped_ptr class, and its closely-related brethren,
+ // scoped_array, scoped_ptr_malloc, and make_scoped_ptr.
+@@ -582,16 +582,27 @@ enum LogLevel {
+ // in the code which calls the library, especially when
+ // compiled in debug mode.
+
+ #ifdef NDEBUG
+ LOGLEVEL_DFATAL = LOGLEVEL_ERROR
+ #else
+ LOGLEVEL_DFATAL = LOGLEVEL_FATAL
+ #endif
++
++#ifdef ERROR
++ // ERROR is defined as 0 on some windows builds, so `GOOGLE_LOG(ERROR, ...)`
++ // expands into `GOOGLE_LOG(0, ...)` which then expands into
++ // `someGoogleLogging(LOGLEVEL_0, ...)`. This is not ideal, because the
++ // GOOGLE_LOG macro expects to expand itself into
++ // `someGoogleLogging(LOGLEVEL_ERROR, ...)` instead. The workaround to get
++ // everything building is to simply define LOGLEVEL_0 as LOGLEVEL_ERROR and
++ // move on with our lives.
++ , LOGLEVEL_0 = LOGLEVEL_ERROR
++#endif
+ };
+
+ namespace internal {
+
+ class LogFinisher;
+
+ class LIBPROTOBUF_EXPORT LogMessage {
+ public:
+--- b/toolkit/components/protobuf/src/google/protobuf/stubs/hash.h
++++ a/toolkit/components/protobuf/src/google/protobuf/stubs/hash.h
+@@ -32,17 +32,16 @@
+ //
+ // Deals with the fact that hash_map is not defined everywhere.
+
+ #ifndef GOOGLE_PROTOBUF_STUBS_HASH_H__
+ #define GOOGLE_PROTOBUF_STUBS_HASH_H__
+
+ #include <string.h>
+ #include <google/protobuf/stubs/common.h>
+-#include "config.h"
+
+ #if defined(HAVE_HASH_MAP) && defined(HAVE_HASH_SET)
+ #include HASH_MAP_H
+ #include HASH_SET_H
+ #else
+ #define MISSING_HASH
+ #include <map>
+ #include <set>
+--- b/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.cc
++++ a/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.cc
+@@ -32,17 +32,16 @@
+
+ #include <google/protobuf/stubs/stringprintf.h>
+
+ #include <errno.h>
+ #include <stdarg.h> // For va_list and related operations
+ #include <stdio.h> // MSVC requires this for _vsnprintf
+ #include <vector>
+ #include <google/protobuf/stubs/common.h>
+-#include <google/protobuf/testing/googletest.h>
+
+ namespace google {
+ namespace protobuf {
+
+ #ifdef _MSC_VER
+ enum { IS_COMPILER_MSVC = 1 };
+ #ifndef va_copy
+ // Define va_copy for MSVC. This is a hack, assuming va_list is simply a
+--- b/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.h
++++ a/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.h
+@@ -332,16 +332,17 @@
+ return strtoul(nptr, endptr, base);
+ else
+ return strtou32_adaptor(nptr, endptr, base);
+ }
+
+ // For now, long long is 64-bit on all the platforms we care about, so these
+ // functions can simply pass the call to strto[u]ll.
+ inline int64 strto64(const char *nptr, char **endptr, int base) {
++ static_assert(sizeof(int64) == sizeof(long long), "Protobuf needs sizeof(int64) == sizeof(long long)");
+ GOOGLE_COMPILE_ASSERT(sizeof(int64) == sizeof(long long),
+ sizeof_int64_is_not_sizeof_long_long);
+ return strtoll(nptr, endptr, base);
+ }
+
+ inline uint64 strtou64(const char *nptr, char **endptr, int base) {
+ GOOGLE_COMPILE_ASSERT(sizeof(uint64) == sizeof(unsigned long long),
+ sizeof_uint64_is_not_sizeof_long_long);
+--- a/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.cc
++++ b/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.cc
+@@ -33,16 +33,18 @@
+ #include <google/protobuf/stubs/strutil.h>
+ #include <errno.h>
+ #include <float.h> // FLT_DIG and DBL_DIG
+ #include <limits>
+ #include <limits.h>
+ #include <stdio.h>
+ #include <iterator>
+
++#include "mozilla/FloatingPoint.h"
++
+ #ifdef _WIN32
+ // MSVC has only _snprintf, not snprintf.
+ //
+ // MinGW has both snprintf and _snprintf, but they appear to be different
+ // functions. The former is buggy. When invoked like so:
+ // char buffer[32];
+ // snprintf(buffer, 32, "%.*g\n", FLT_DIG, 1.23e10f);
+ // it prints "1.23000e+10". This is plainly wrong: %g should never print
+@@ -51,18 +53,17 @@
+ // right thing, so we use it.
+ #define snprintf _snprintf
+ #endif
+
+ namespace google {
+ namespace protobuf {
+
+ inline bool IsNaN(double value) {
+- // NaN is never equal to anything, even itself.
+- return value != value;
++ return ::mozilla::IsNaN(value);
+ }
+
+ // These are defined as macros on some platforms. #undef them so that we can
+ // redefine them.
+ #undef isxdigit
+ #undef isprint
+
+ // The definitions of these in ctype.h change based on locale. Since our
+--- b/toolkit/components/protobuf/src/google/protobuf/stubs/type_traits.h
++++ a/toolkit/components/protobuf/src/google/protobuf/stubs/type_traits.h
+@@ -107,20 +107,18 @@
+ template<> struct is_integral<wchar_t> : true_type { };
+ #endif
+ template<> struct is_integral<short> : true_type { };
+ template<> struct is_integral<unsigned short> : true_type { };
+ template<> struct is_integral<int> : true_type { };
+ template<> struct is_integral<unsigned int> : true_type { };
+ template<> struct is_integral<long> : true_type { };
+ template<> struct is_integral<unsigned long> : true_type { };
+-#ifdef HAVE_LONG_LONG
+ template<> struct is_integral<long long> : true_type { };
+ template<> struct is_integral<unsigned long long> : true_type { };
+-#endif
+ template <class T> struct is_integral<const T> : is_integral<T> { };
+ template <class T> struct is_integral<volatile T> : is_integral<T> { };
+ template <class T> struct is_integral<const volatile T> : is_integral<T> { };
+
+ // is_floating_point is false except for the built-in floating-point types.
+ // A cv-qualified type is integral if and only if the underlying type is.
+ template <class T> struct is_floating_point : false_type { };
+ template<> struct is_floating_point<float> : true_type { };
+--- a/toolkit/components/protobuf/src/google/protobuf/wire_format_lite_inl.h
++++ b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite_inl.h
+@@ -375,17 +375,17 @@ inline bool WireFormatLite::ReadPackedFixedSizePrimitive(
+ void* dest = reinterpret_cast<void*>(values->mutable_data() + old_entries);
+ if (!input->ReadRaw(dest, new_bytes)) {
+ values->Truncate(old_entries);
+ return false;
+ }
+ #else
+ values->Reserve(old_entries + new_entries);
+ CType value;
+- for (int i = 0; i < new_entries; ++i) {
++ for (uint32 i = 0; i < new_entries; ++i) {
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value)) return false;
+ values->AddAlreadyReserved(value);
+ }
+ #endif
+ } else {
+ // This is the slow-path case where "length" may be too large to
+ // safely allocate. We read as much as we can into *values
+ // without pre-allocating "length" bytes.
+--- a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h
++++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h
+@@ -39,16 +39,17 @@
+ // streams. Of course, many users will probably want to write their own
+ // implementations of these interfaces specific to the particular I/O
+ // abstractions they prefer to use, but these should cover the most common
+ // cases.
+
+ #ifndef GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_LITE_H__
+ #define GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_LITE_H__
+
++#include <vector> /* See Bug 1186561 */
+ #include <string>
+ #include <iosfwd>
+ #include <google/protobuf/io/zero_copy_stream.h>
+ #include <google/protobuf/stubs/common.h>
+ #include <google/protobuf/stubs/stl_util.h>
+
+
+ namespace google {
+diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h
+--- a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h
++++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h
+@@ -73,17 +73,21 @@ typedef int32 Atomic32;
+ typedef int64 Atomic64;
+ #else
+ typedef intptr_t Atomic64;
+ #endif
+ #endif
+
+ // Use AtomicWord for a machine-sized pointer. It will use the Atomic32 or
+ // Atomic64 routines below, depending on your architecture.
++#if defined(__OpenBSD__) && !defined(GOOGLE_PROTOBUF_ARCH_64_BIT)
++typedef Atomic32 AtomicWord;
++#else
+ typedef intptr_t AtomicWord;
++#endif
+
+ // Atomically execute:
+ // result = *ptr;
+ // if (*ptr == old_value)
+ // *ptr = new_value;
+ // return result;
+ //
+ // I.e., replace "*ptr" with "new_value" if "*ptr" used to be "old_value".
diff --git a/toolkit/components/protobuf/moz.build b/toolkit/components/protobuf/moz.build
new file mode 100644
index 0000000000..b5015eb678
--- /dev/null
+++ b/toolkit/components/protobuf/moz.build
@@ -0,0 +1,137 @@
+# -*- 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.google.protobuf += [
+ 'src/google/protobuf/descriptor.h',
+ 'src/google/protobuf/descriptor.pb.h',
+ 'src/google/protobuf/descriptor_database.h',
+ 'src/google/protobuf/dynamic_message.h',
+ 'src/google/protobuf/extension_set.h',
+ 'src/google/protobuf/generated_enum_reflection.h',
+ 'src/google/protobuf/generated_message_reflection.h',
+ 'src/google/protobuf/generated_message_util.h',
+ 'src/google/protobuf/message.h',
+ 'src/google/protobuf/message_lite.h',
+ 'src/google/protobuf/package_info.h',
+ 'src/google/protobuf/reflection_ops.h',
+ 'src/google/protobuf/repeated_field.h',
+ 'src/google/protobuf/service.h',
+ 'src/google/protobuf/text_format.h',
+ 'src/google/protobuf/unknown_field_set.h',
+ 'src/google/protobuf/wire_format.h',
+ 'src/google/protobuf/wire_format_lite.h',
+ 'src/google/protobuf/wire_format_lite_inl.h',
+]
+
+EXPORTS.google.protobuf.io += [
+ 'src/google/protobuf/io/coded_stream.h',
+ 'src/google/protobuf/io/coded_stream_inl.h',
+ 'src/google/protobuf/io/gzip_stream.h',
+ 'src/google/protobuf/io/package_info.h',
+ 'src/google/protobuf/io/printer.h',
+ 'src/google/protobuf/io/strtod.h',
+ 'src/google/protobuf/io/tokenizer.h',
+ 'src/google/protobuf/io/zero_copy_stream.h',
+ 'src/google/protobuf/io/zero_copy_stream_impl.h',
+ 'src/google/protobuf/io/zero_copy_stream_impl_lite.h',
+]
+
+EXPORTS.google.protobuf.stubs += [
+ 'src/google/protobuf/stubs/atomicops.h',
+ 'src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h',
+ 'src/google/protobuf/stubs/atomicops_internals_arm_gcc.h',
+ 'src/google/protobuf/stubs/atomicops_internals_arm_qnx.h',
+ 'src/google/protobuf/stubs/atomicops_internals_atomicword_compat.h',
+ 'src/google/protobuf/stubs/atomicops_internals_generic_gcc.h',
+ 'src/google/protobuf/stubs/atomicops_internals_macosx.h',
+ 'src/google/protobuf/stubs/atomicops_internals_mips_gcc.h',
+ 'src/google/protobuf/stubs/atomicops_internals_pnacl.h',
+ 'src/google/protobuf/stubs/atomicops_internals_solaris.h',
+ 'src/google/protobuf/stubs/atomicops_internals_tsan.h',
+ 'src/google/protobuf/stubs/atomicops_internals_x86_gcc.h',
+ 'src/google/protobuf/stubs/atomicops_internals_x86_msvc.h',
+ 'src/google/protobuf/stubs/common.h',
+ 'src/google/protobuf/stubs/hash.h',
+ 'src/google/protobuf/stubs/map_util.h',
+ 'src/google/protobuf/stubs/once.h',
+ 'src/google/protobuf/stubs/platform_macros.h',
+ 'src/google/protobuf/stubs/shared_ptr.h',
+ 'src/google/protobuf/stubs/stl_util.h',
+ 'src/google/protobuf/stubs/stringprintf.h',
+ 'src/google/protobuf/stubs/strutil.h',
+ 'src/google/protobuf/stubs/substitute.h',
+ 'src/google/protobuf/stubs/template_util.h',
+ 'src/google/protobuf/stubs/type_traits.h',
+]
+
+UNIFIED_SOURCES += [
+ 'src/google/protobuf/descriptor.cc',
+ 'src/google/protobuf/descriptor.pb.cc',
+ 'src/google/protobuf/descriptor_database.cc',
+ 'src/google/protobuf/dynamic_message.cc',
+ 'src/google/protobuf/extension_set.cc',
+ 'src/google/protobuf/generated_message_reflection.cc',
+ 'src/google/protobuf/generated_message_util.cc',
+ 'src/google/protobuf/io/coded_stream.cc',
+ 'src/google/protobuf/io/gzip_stream.cc',
+ 'src/google/protobuf/io/printer.cc',
+ 'src/google/protobuf/io/strtod.cc',
+ 'src/google/protobuf/io/tokenizer.cc',
+ 'src/google/protobuf/io/zero_copy_stream.cc',
+ 'src/google/protobuf/io/zero_copy_stream_impl.cc',
+ 'src/google/protobuf/io/zero_copy_stream_impl_lite.cc',
+ 'src/google/protobuf/message.cc',
+ 'src/google/protobuf/message_lite.cc',
+ 'src/google/protobuf/reflection_ops.cc',
+ 'src/google/protobuf/repeated_field.cc',
+ 'src/google/protobuf/service.cc',
+ 'src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc',
+ 'src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc',
+ 'src/google/protobuf/stubs/common.cc',
+ 'src/google/protobuf/stubs/once.cc',
+ 'src/google/protobuf/stubs/stringprintf.cc',
+ 'src/google/protobuf/stubs/structurally_valid.cc',
+ 'src/google/protobuf/stubs/strutil.cc',
+ 'src/google/protobuf/stubs/substitute.cc',
+ 'src/google/protobuf/unknown_field_set.cc',
+ 'src/google/protobuf/wire_format_lite.cc',
+]
+
+SOURCES += [
+ 'src/google/protobuf/extension_set_heavy.cc',
+ 'src/google/protobuf/text_format.cc',
+ 'src/google/protobuf/wire_format.cc',
+]
+
+# We allow warnings for third-party code that can be updated from upstream.
+ALLOW_COMPILER_WARNINGS = True
+
+FINAL_LIBRARY = 'xul'
+
+DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
+DEFINES['GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER'] = True
+
+# Suppress warnings in third-party code.
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += [
+ '-Wno-null-conversion',
+ '-Wno-return-type',
+ '-Wno-sign-compare',
+ ]
+elif CONFIG['_MSC_VER']:
+ CXXFLAGS += [
+ '-wd4005', # 'WIN32_LEAN_AND_MEAN' : macro redefinition
+ '-wd4018', # '<' : signed/unsigned mismatch
+ '-wd4099', # mismatched class/struct tags
+ ]
+
+if CONFIG['MOZ_USE_PTHREADS']:
+ DEFINES['HAVE_PTHREAD'] = True
+
+# Needed for the gzip streams.
+DEFINES['HAVE_ZLIB'] = True
+
+CXXFLAGS += CONFIG['TK_CFLAGS']
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor.cc b/toolkit/components/protobuf/src/google/protobuf/descriptor.cc
new file mode 100644
index 0000000000..21dda5987f
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor.cc
@@ -0,0 +1,5420 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/stubs/hash.h>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+#include <algorithm>
+#include <limits>
+
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor_database.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/dynamic_message.h>
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/text_format.h>
+#include <google/protobuf/unknown_field_set.h>
+#include <google/protobuf/wire_format.h>
+#include <google/protobuf/io/strtod.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/tokenizer.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/stubs/strutil.h>
+#include <google/protobuf/stubs/substitute.h>
+#include <google/protobuf/stubs/map_util.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+#undef PACKAGE // autoheader #defines this. :(
+
+namespace google {
+namespace protobuf {
+
+const FieldDescriptor::CppType
+FieldDescriptor::kTypeToCppTypeMap[MAX_TYPE + 1] = {
+ static_cast<CppType>(0), // 0 is reserved for errors
+
+ CPPTYPE_DOUBLE, // TYPE_DOUBLE
+ CPPTYPE_FLOAT, // TYPE_FLOAT
+ CPPTYPE_INT64, // TYPE_INT64
+ CPPTYPE_UINT64, // TYPE_UINT64
+ CPPTYPE_INT32, // TYPE_INT32
+ CPPTYPE_UINT64, // TYPE_FIXED64
+ CPPTYPE_UINT32, // TYPE_FIXED32
+ CPPTYPE_BOOL, // TYPE_BOOL
+ CPPTYPE_STRING, // TYPE_STRING
+ CPPTYPE_MESSAGE, // TYPE_GROUP
+ CPPTYPE_MESSAGE, // TYPE_MESSAGE
+ CPPTYPE_STRING, // TYPE_BYTES
+ CPPTYPE_UINT32, // TYPE_UINT32
+ CPPTYPE_ENUM, // TYPE_ENUM
+ CPPTYPE_INT32, // TYPE_SFIXED32
+ CPPTYPE_INT64, // TYPE_SFIXED64
+ CPPTYPE_INT32, // TYPE_SINT32
+ CPPTYPE_INT64, // TYPE_SINT64
+};
+
+const char * const FieldDescriptor::kTypeToName[MAX_TYPE + 1] = {
+ "ERROR", // 0 is reserved for errors
+
+ "double", // TYPE_DOUBLE
+ "float", // TYPE_FLOAT
+ "int64", // TYPE_INT64
+ "uint64", // TYPE_UINT64
+ "int32", // TYPE_INT32
+ "fixed64", // TYPE_FIXED64
+ "fixed32", // TYPE_FIXED32
+ "bool", // TYPE_BOOL
+ "string", // TYPE_STRING
+ "group", // TYPE_GROUP
+ "message", // TYPE_MESSAGE
+ "bytes", // TYPE_BYTES
+ "uint32", // TYPE_UINT32
+ "enum", // TYPE_ENUM
+ "sfixed32", // TYPE_SFIXED32
+ "sfixed64", // TYPE_SFIXED64
+ "sint32", // TYPE_SINT32
+ "sint64", // TYPE_SINT64
+};
+
+const char * const FieldDescriptor::kCppTypeToName[MAX_CPPTYPE + 1] = {
+ "ERROR", // 0 is reserved for errors
+
+ "int32", // CPPTYPE_INT32
+ "int64", // CPPTYPE_INT64
+ "uint32", // CPPTYPE_UINT32
+ "uint64", // CPPTYPE_UINT64
+ "double", // CPPTYPE_DOUBLE
+ "float", // CPPTYPE_FLOAT
+ "bool", // CPPTYPE_BOOL
+ "enum", // CPPTYPE_ENUM
+ "string", // CPPTYPE_STRING
+ "message", // CPPTYPE_MESSAGE
+};
+
+const char * const FieldDescriptor::kLabelToName[MAX_LABEL + 1] = {
+ "ERROR", // 0 is reserved for errors
+
+ "optional", // LABEL_OPTIONAL
+ "required", // LABEL_REQUIRED
+ "repeated", // LABEL_REPEATED
+};
+
+static const char * const kNonLinkedWeakMessageReplacementName = "google.protobuf.Empty";
+
+#ifndef _MSC_VER // MSVC doesn't need these and won't even accept them.
+const int FieldDescriptor::kMaxNumber;
+const int FieldDescriptor::kFirstReservedNumber;
+const int FieldDescriptor::kLastReservedNumber;
+#endif
+
+namespace {
+
+string ToCamelCase(const string& input) {
+ bool capitalize_next = false;
+ string result;
+ result.reserve(input.size());
+
+ for (int i = 0; i < input.size(); i++) {
+ if (input[i] == '_') {
+ capitalize_next = true;
+ } else if (capitalize_next) {
+ // Note: I distrust ctype.h due to locales.
+ if ('a' <= input[i] && input[i] <= 'z') {
+ result.push_back(input[i] - 'a' + 'A');
+ } else {
+ result.push_back(input[i]);
+ }
+ capitalize_next = false;
+ } else {
+ result.push_back(input[i]);
+ }
+ }
+
+ // Lower-case the first letter.
+ if (!result.empty() && 'A' <= result[0] && result[0] <= 'Z') {
+ result[0] = result[0] - 'A' + 'a';
+ }
+
+ return result;
+}
+
+// A DescriptorPool contains a bunch of hash_maps to implement the
+// various Find*By*() methods. Since hashtable lookups are O(1), it's
+// most efficient to construct a fixed set of large hash_maps used by
+// all objects in the pool rather than construct one or more small
+// hash_maps for each object.
+//
+// The keys to these hash_maps are (parent, name) or (parent, number)
+// pairs. Unfortunately STL doesn't provide hash functions for pair<>,
+// so we must invent our own.
+//
+// TODO(kenton): Use StringPiece rather than const char* in keys? It would
+// be a lot cleaner but we'd just have to convert it back to const char*
+// for the open source release.
+
+typedef pair<const void*, const char*> PointerStringPair;
+
+struct PointerStringPairEqual {
+ inline bool operator()(const PointerStringPair& a,
+ const PointerStringPair& b) const {
+ return a.first == b.first && strcmp(a.second, b.second) == 0;
+ }
+};
+
+template<typename PairType>
+struct PointerIntegerPairHash {
+ size_t operator()(const PairType& p) const {
+ // FIXME(kenton): What is the best way to compute this hash? I have
+ // no idea! This seems a bit better than an XOR.
+ return reinterpret_cast<intptr_t>(p.first) * ((1 << 16) - 1) + p.second;
+ }
+
+#ifdef _MSC_VER
+ // Used only by MSVC and platforms where hash_map is not available.
+ static const size_t bucket_size = 4;
+ static const size_t min_buckets = 8;
+#endif
+ inline bool operator()(const PairType& a, const PairType& b) const {
+ return a.first < b.first ||
+ (a.first == b.first && a.second < b.second);
+ }
+};
+
+typedef pair<const Descriptor*, int> DescriptorIntPair;
+typedef pair<const EnumDescriptor*, int> EnumIntPair;
+
+struct PointerStringPairHash {
+ size_t operator()(const PointerStringPair& p) const {
+ // FIXME(kenton): What is the best way to compute this hash? I have
+ // no idea! This seems a bit better than an XOR.
+ hash<const char*> cstring_hash;
+ return reinterpret_cast<intptr_t>(p.first) * ((1 << 16) - 1) +
+ cstring_hash(p.second);
+ }
+
+#ifdef _MSC_VER
+ // Used only by MSVC and platforms where hash_map is not available.
+ static const size_t bucket_size = 4;
+ static const size_t min_buckets = 8;
+#endif
+ inline bool operator()(const PointerStringPair& a,
+ const PointerStringPair& b) const {
+ if (a.first < b.first) return true;
+ if (a.first > b.first) return false;
+ return strcmp(a.second, b.second) < 0;
+ }
+};
+
+
+struct Symbol {
+ enum Type {
+ NULL_SYMBOL, MESSAGE, FIELD, ONEOF, ENUM, ENUM_VALUE, SERVICE, METHOD,
+ PACKAGE
+ };
+ Type type;
+ union {
+ const Descriptor* descriptor;
+ const FieldDescriptor* field_descriptor;
+ const OneofDescriptor* oneof_descriptor;
+ const EnumDescriptor* enum_descriptor;
+ const EnumValueDescriptor* enum_value_descriptor;
+ const ServiceDescriptor* service_descriptor;
+ const MethodDescriptor* method_descriptor;
+ const FileDescriptor* package_file_descriptor;
+ };
+
+ inline Symbol() : type(NULL_SYMBOL) { descriptor = NULL; }
+ inline bool IsNull() const { return type == NULL_SYMBOL; }
+ inline bool IsType() const {
+ return type == MESSAGE || type == ENUM;
+ }
+ inline bool IsAggregate() const {
+ return type == MESSAGE || type == PACKAGE
+ || type == ENUM || type == SERVICE;
+ }
+
+#define CONSTRUCTOR(TYPE, TYPE_CONSTANT, FIELD) \
+ inline explicit Symbol(const TYPE* value) { \
+ type = TYPE_CONSTANT; \
+ this->FIELD = value; \
+ }
+
+ CONSTRUCTOR(Descriptor , MESSAGE , descriptor )
+ CONSTRUCTOR(FieldDescriptor , FIELD , field_descriptor )
+ CONSTRUCTOR(OneofDescriptor , ONEOF , oneof_descriptor )
+ CONSTRUCTOR(EnumDescriptor , ENUM , enum_descriptor )
+ CONSTRUCTOR(EnumValueDescriptor, ENUM_VALUE, enum_value_descriptor )
+ CONSTRUCTOR(ServiceDescriptor , SERVICE , service_descriptor )
+ CONSTRUCTOR(MethodDescriptor , METHOD , method_descriptor )
+ CONSTRUCTOR(FileDescriptor , PACKAGE , package_file_descriptor)
+#undef CONSTRUCTOR
+
+ const FileDescriptor* GetFile() const {
+ switch (type) {
+ case NULL_SYMBOL: return NULL;
+ case MESSAGE : return descriptor ->file();
+ case FIELD : return field_descriptor ->file();
+ case ONEOF : return oneof_descriptor ->containing_type()->file();
+ case ENUM : return enum_descriptor ->file();
+ case ENUM_VALUE : return enum_value_descriptor->type()->file();
+ case SERVICE : return service_descriptor ->file();
+ case METHOD : return method_descriptor ->service()->file();
+ case PACKAGE : return package_file_descriptor;
+ }
+ return NULL;
+ }
+};
+
+const Symbol kNullSymbol;
+
+typedef hash_map<const char*, Symbol,
+ hash<const char*>, streq>
+ SymbolsByNameMap;
+typedef hash_map<PointerStringPair, Symbol,
+ PointerStringPairHash, PointerStringPairEqual>
+ SymbolsByParentMap;
+typedef hash_map<const char*, const FileDescriptor*,
+ hash<const char*>, streq>
+ FilesByNameMap;
+typedef hash_map<PointerStringPair, const FieldDescriptor*,
+ PointerStringPairHash, PointerStringPairEqual>
+ FieldsByNameMap;
+typedef hash_map<DescriptorIntPair, const FieldDescriptor*,
+ PointerIntegerPairHash<DescriptorIntPair> >
+ FieldsByNumberMap;
+typedef hash_map<EnumIntPair, const EnumValueDescriptor*,
+ PointerIntegerPairHash<EnumIntPair> >
+ EnumValuesByNumberMap;
+// This is a map rather than a hash_map, since we use it to iterate
+// through all the extensions that extend a given Descriptor, and an
+// ordered data structure that implements lower_bound is convenient
+// for that.
+typedef map<DescriptorIntPair, const FieldDescriptor*>
+ ExtensionsGroupedByDescriptorMap;
+typedef hash_map<string, const SourceCodeInfo_Location*> LocationsByPathMap;
+} // anonymous namespace
+
+// ===================================================================
+// DescriptorPool::Tables
+
+class DescriptorPool::Tables {
+ public:
+ Tables();
+ ~Tables();
+
+ // Record the current state of the tables to the stack of checkpoints.
+ // Each call to AddCheckpoint() must be paired with exactly one call to either
+ // ClearLastCheckpoint() or RollbackToLastCheckpoint().
+ //
+ // This is used when building files, since some kinds of validation errors
+ // cannot be detected until the file's descriptors have already been added to
+ // the tables.
+ //
+ // This supports recursive checkpoints, since building a file may trigger
+ // recursive building of other files. Note that recursive checkpoints are not
+ // normally necessary; explicit dependencies are built prior to checkpointing.
+ // So although we recursively build transitive imports, there is at most one
+ // checkpoint in the stack during dependency building.
+ //
+ // Recursive checkpoints only arise during cross-linking of the descriptors.
+ // Symbol references must be resolved, via DescriptorBuilder::FindSymbol and
+ // friends. If the pending file references an unknown symbol
+ // (e.g., it is not defined in the pending file's explicit dependencies), and
+ // the pool is using a fallback database, and that database contains a file
+ // defining that symbol, and that file has not yet been built by the pool,
+ // the pool builds the file during cross-linking, leading to another
+ // checkpoint.
+ void AddCheckpoint();
+
+ // Mark the last checkpoint as having cleared successfully, removing it from
+ // the stack. If the stack is empty, all pending symbols will be committed.
+ //
+ // Note that this does not guarantee that the symbols added since the last
+ // checkpoint won't be rolled back: if a checkpoint gets rolled back,
+ // everything past that point gets rolled back, including symbols added after
+ // checkpoints that were pushed onto the stack after it and marked as cleared.
+ void ClearLastCheckpoint();
+
+ // Roll back the Tables to the state of the checkpoint at the top of the
+ // stack, removing everything that was added after that point.
+ void RollbackToLastCheckpoint();
+
+ // The stack of files which are currently being built. Used to detect
+ // cyclic dependencies when loading files from a DescriptorDatabase. Not
+ // used when fallback_database_ == NULL.
+ vector<string> pending_files_;
+
+ // A set of files which we have tried to load from the fallback database
+ // and encountered errors. We will not attempt to load them again during
+ // execution of the current public API call, but for compatibility with
+ // legacy clients, this is cleared at the beginning of each public API call.
+ // Not used when fallback_database_ == NULL.
+ hash_set<string> known_bad_files_;
+
+ // A set of symbols which we have tried to load from the fallback database
+ // and encountered errors. We will not attempt to load them again during
+ // execution of the current public API call, but for compatibility with
+ // legacy clients, this is cleared at the beginning of each public API call.
+ hash_set<string> known_bad_symbols_;
+
+ // The set of descriptors for which we've already loaded the full
+ // set of extensions numbers from fallback_database_.
+ hash_set<const Descriptor*> extensions_loaded_from_db_;
+
+ // -----------------------------------------------------------------
+ // Finding items.
+
+ // Find symbols. This returns a null Symbol (symbol.IsNull() is true)
+ // if not found.
+ inline Symbol FindSymbol(const string& key) const;
+
+ // This implements the body of DescriptorPool::Find*ByName(). It should
+ // really be a private method of DescriptorPool, but that would require
+ // declaring Symbol in descriptor.h, which would drag all kinds of other
+ // stuff into the header. Yay C++.
+ Symbol FindByNameHelper(
+ const DescriptorPool* pool, const string& name);
+
+ // These return NULL if not found.
+ inline const FileDescriptor* FindFile(const string& key) const;
+ inline const FieldDescriptor* FindExtension(const Descriptor* extendee,
+ int number);
+ inline void FindAllExtensions(const Descriptor* extendee,
+ vector<const FieldDescriptor*>* out) const;
+
+ // -----------------------------------------------------------------
+ // Adding items.
+
+ // These add items to the corresponding tables. They return false if
+ // the key already exists in the table. For AddSymbol(), the string passed
+ // in must be one that was constructed using AllocateString(), as it will
+ // be used as a key in the symbols_by_name_ map without copying.
+ bool AddSymbol(const string& full_name, Symbol symbol);
+ bool AddFile(const FileDescriptor* file);
+ bool AddExtension(const FieldDescriptor* field);
+
+ // -----------------------------------------------------------------
+ // Allocating memory.
+
+ // Allocate an object which will be reclaimed when the pool is
+ // destroyed. Note that the object's destructor will never be called,
+ // so its fields must be plain old data (primitive data types and
+ // pointers). All of the descriptor types are such objects.
+ template<typename Type> Type* Allocate();
+
+ // Allocate an array of objects which will be reclaimed when the
+ // pool in destroyed. Again, destructors are never called.
+ template<typename Type> Type* AllocateArray(int count);
+
+ // Allocate a string which will be destroyed when the pool is destroyed.
+ // The string is initialized to the given value for convenience.
+ string* AllocateString(const string& value);
+
+ // Allocate a protocol message object. Some older versions of GCC have
+ // trouble understanding explicit template instantiations in some cases, so
+ // in those cases we have to pass a dummy pointer of the right type as the
+ // parameter instead of specifying the type explicitly.
+ template<typename Type> Type* AllocateMessage(Type* dummy = NULL);
+
+ // Allocate a FileDescriptorTables object.
+ FileDescriptorTables* AllocateFileTables();
+
+ private:
+ vector<string*> strings_; // All strings in the pool.
+ vector<Message*> messages_; // All messages in the pool.
+ vector<FileDescriptorTables*> file_tables_; // All file tables in the pool.
+ vector<void*> allocations_; // All other memory allocated in the pool.
+
+ SymbolsByNameMap symbols_by_name_;
+ FilesByNameMap files_by_name_;
+ ExtensionsGroupedByDescriptorMap extensions_;
+
+ struct CheckPoint {
+ explicit CheckPoint(const Tables* tables)
+ : strings_before_checkpoint(tables->strings_.size()),
+ messages_before_checkpoint(tables->messages_.size()),
+ file_tables_before_checkpoint(tables->file_tables_.size()),
+ allocations_before_checkpoint(tables->allocations_.size()),
+ pending_symbols_before_checkpoint(
+ tables->symbols_after_checkpoint_.size()),
+ pending_files_before_checkpoint(
+ tables->files_after_checkpoint_.size()),
+ pending_extensions_before_checkpoint(
+ tables->extensions_after_checkpoint_.size()) {
+ }
+ int strings_before_checkpoint;
+ int messages_before_checkpoint;
+ int file_tables_before_checkpoint;
+ int allocations_before_checkpoint;
+ int pending_symbols_before_checkpoint;
+ int pending_files_before_checkpoint;
+ int pending_extensions_before_checkpoint;
+ };
+ vector<CheckPoint> checkpoints_;
+ vector<const char* > symbols_after_checkpoint_;
+ vector<const char* > files_after_checkpoint_;
+ vector<DescriptorIntPair> extensions_after_checkpoint_;
+
+ // Allocate some bytes which will be reclaimed when the pool is
+ // destroyed.
+ void* AllocateBytes(int size);
+};
+
+// Contains tables specific to a particular file. These tables are not
+// modified once the file has been constructed, so they need not be
+// protected by a mutex. This makes operations that depend only on the
+// contents of a single file -- e.g. Descriptor::FindFieldByName() --
+// lock-free.
+//
+// For historical reasons, the definitions of the methods of
+// FileDescriptorTables and DescriptorPool::Tables are interleaved below.
+// These used to be a single class.
+class FileDescriptorTables {
+ public:
+ FileDescriptorTables();
+ ~FileDescriptorTables();
+
+ // Empty table, used with placeholder files.
+ static const FileDescriptorTables kEmpty;
+
+ // -----------------------------------------------------------------
+ // Finding items.
+
+ // Find symbols. These return a null Symbol (symbol.IsNull() is true)
+ // if not found.
+ inline Symbol FindNestedSymbol(const void* parent,
+ const string& name) const;
+ inline Symbol FindNestedSymbolOfType(const void* parent,
+ const string& name,
+ const Symbol::Type type) const;
+
+ // These return NULL if not found.
+ inline const FieldDescriptor* FindFieldByNumber(
+ const Descriptor* parent, int number) const;
+ inline const FieldDescriptor* FindFieldByLowercaseName(
+ const void* parent, const string& lowercase_name) const;
+ inline const FieldDescriptor* FindFieldByCamelcaseName(
+ const void* parent, const string& camelcase_name) const;
+ inline const EnumValueDescriptor* FindEnumValueByNumber(
+ const EnumDescriptor* parent, int number) const;
+
+ // -----------------------------------------------------------------
+ // Adding items.
+
+ // These add items to the corresponding tables. They return false if
+ // the key already exists in the table. For AddAliasUnderParent(), the
+ // string passed in must be one that was constructed using AllocateString(),
+ // as it will be used as a key in the symbols_by_parent_ map without copying.
+ bool AddAliasUnderParent(const void* parent, const string& name,
+ Symbol symbol);
+ bool AddFieldByNumber(const FieldDescriptor* field);
+ bool AddEnumValueByNumber(const EnumValueDescriptor* value);
+
+ // Adds the field to the lowercase_name and camelcase_name maps. Never
+ // fails because we allow duplicates; the first field by the name wins.
+ void AddFieldByStylizedNames(const FieldDescriptor* field);
+
+ // Populates p->first->locations_by_path_ from p->second.
+ // Unusual signature dictated by GoogleOnceDynamic.
+ static void BuildLocationsByPath(
+ pair<const FileDescriptorTables*, const SourceCodeInfo*>* p);
+
+ // Returns the location denoted by the specified path through info,
+ // or NULL if not found.
+ // The value of info must be that of the corresponding FileDescriptor.
+ // (Conceptually a pure function, but stateful as an optimisation.)
+ const SourceCodeInfo_Location* GetSourceLocation(
+ const vector<int>& path, const SourceCodeInfo* info) const;
+
+ private:
+ SymbolsByParentMap symbols_by_parent_;
+ FieldsByNameMap fields_by_lowercase_name_;
+ FieldsByNameMap fields_by_camelcase_name_;
+ FieldsByNumberMap fields_by_number_; // Not including extensions.
+ EnumValuesByNumberMap enum_values_by_number_;
+
+ // Populated on first request to save space, hence constness games.
+ mutable GoogleOnceDynamic locations_by_path_once_;
+ mutable LocationsByPathMap locations_by_path_;
+};
+
+DescriptorPool::Tables::Tables()
+ // Start some hash_map and hash_set objects with a small # of buckets
+ : known_bad_files_(3),
+ known_bad_symbols_(3),
+ extensions_loaded_from_db_(3),
+ symbols_by_name_(3),
+ files_by_name_(3) {}
+
+
+DescriptorPool::Tables::~Tables() {
+ GOOGLE_DCHECK(checkpoints_.empty());
+ // Note that the deletion order is important, since the destructors of some
+ // messages may refer to objects in allocations_.
+ STLDeleteElements(&messages_);
+ for (int i = 0; i < allocations_.size(); i++) {
+ operator delete(allocations_[i]);
+ }
+ STLDeleteElements(&strings_);
+ STLDeleteElements(&file_tables_);
+}
+
+FileDescriptorTables::FileDescriptorTables()
+ // Initialize all the hash tables to start out with a small # of buckets
+ : symbols_by_parent_(3),
+ fields_by_lowercase_name_(3),
+ fields_by_camelcase_name_(3),
+ fields_by_number_(3),
+ enum_values_by_number_(3) {
+}
+
+FileDescriptorTables::~FileDescriptorTables() {}
+
+const FileDescriptorTables FileDescriptorTables::kEmpty;
+
+void DescriptorPool::Tables::AddCheckpoint() {
+ checkpoints_.push_back(CheckPoint(this));
+}
+
+void DescriptorPool::Tables::ClearLastCheckpoint() {
+ GOOGLE_DCHECK(!checkpoints_.empty());
+ checkpoints_.pop_back();
+ if (checkpoints_.empty()) {
+ // All checkpoints have been cleared: we can now commit all of the pending
+ // data.
+ symbols_after_checkpoint_.clear();
+ files_after_checkpoint_.clear();
+ extensions_after_checkpoint_.clear();
+ }
+}
+
+void DescriptorPool::Tables::RollbackToLastCheckpoint() {
+ GOOGLE_DCHECK(!checkpoints_.empty());
+ const CheckPoint& checkpoint = checkpoints_.back();
+
+ for (int i = checkpoint.pending_symbols_before_checkpoint;
+ i < symbols_after_checkpoint_.size();
+ i++) {
+ symbols_by_name_.erase(symbols_after_checkpoint_[i]);
+ }
+ for (int i = checkpoint.pending_files_before_checkpoint;
+ i < files_after_checkpoint_.size();
+ i++) {
+ files_by_name_.erase(files_after_checkpoint_[i]);
+ }
+ for (int i = checkpoint.pending_extensions_before_checkpoint;
+ i < extensions_after_checkpoint_.size();
+ i++) {
+ extensions_.erase(extensions_after_checkpoint_[i]);
+ }
+
+ symbols_after_checkpoint_.resize(
+ checkpoint.pending_symbols_before_checkpoint);
+ files_after_checkpoint_.resize(checkpoint.pending_files_before_checkpoint);
+ extensions_after_checkpoint_.resize(
+ checkpoint.pending_extensions_before_checkpoint);
+
+ STLDeleteContainerPointers(
+ strings_.begin() + checkpoint.strings_before_checkpoint, strings_.end());
+ STLDeleteContainerPointers(
+ messages_.begin() + checkpoint.messages_before_checkpoint,
+ messages_.end());
+ STLDeleteContainerPointers(
+ file_tables_.begin() + checkpoint.file_tables_before_checkpoint,
+ file_tables_.end());
+ for (int i = checkpoint.allocations_before_checkpoint;
+ i < allocations_.size();
+ i++) {
+ operator delete(allocations_[i]);
+ }
+
+ strings_.resize(checkpoint.strings_before_checkpoint);
+ messages_.resize(checkpoint.messages_before_checkpoint);
+ file_tables_.resize(checkpoint.file_tables_before_checkpoint);
+ allocations_.resize(checkpoint.allocations_before_checkpoint);
+ checkpoints_.pop_back();
+}
+
+// -------------------------------------------------------------------
+
+inline Symbol DescriptorPool::Tables::FindSymbol(const string& key) const {
+ const Symbol* result = FindOrNull(symbols_by_name_, key.c_str());
+ if (result == NULL) {
+ return kNullSymbol;
+ } else {
+ return *result;
+ }
+}
+
+inline Symbol FileDescriptorTables::FindNestedSymbol(
+ const void* parent, const string& name) const {
+ const Symbol* result =
+ FindOrNull(symbols_by_parent_, PointerStringPair(parent, name.c_str()));
+ if (result == NULL) {
+ return kNullSymbol;
+ } else {
+ return *result;
+ }
+}
+
+inline Symbol FileDescriptorTables::FindNestedSymbolOfType(
+ const void* parent, const string& name, const Symbol::Type type) const {
+ Symbol result = FindNestedSymbol(parent, name);
+ if (result.type != type) return kNullSymbol;
+ return result;
+}
+
+Symbol DescriptorPool::Tables::FindByNameHelper(
+ const DescriptorPool* pool, const string& name) {
+ MutexLockMaybe lock(pool->mutex_);
+ known_bad_symbols_.clear();
+ known_bad_files_.clear();
+ Symbol result = FindSymbol(name);
+
+ if (result.IsNull() && pool->underlay_ != NULL) {
+ // Symbol not found; check the underlay.
+ result =
+ pool->underlay_->tables_->FindByNameHelper(pool->underlay_, name);
+ }
+
+ if (result.IsNull()) {
+ // Symbol still not found, so check fallback database.
+ if (pool->TryFindSymbolInFallbackDatabase(name)) {
+ result = FindSymbol(name);
+ }
+ }
+
+ return result;
+}
+
+inline const FileDescriptor* DescriptorPool::Tables::FindFile(
+ const string& key) const {
+ return FindPtrOrNull(files_by_name_, key.c_str());
+}
+
+inline const FieldDescriptor* FileDescriptorTables::FindFieldByNumber(
+ const Descriptor* parent, int number) const {
+ return FindPtrOrNull(fields_by_number_, make_pair(parent, number));
+}
+
+inline const FieldDescriptor* FileDescriptorTables::FindFieldByLowercaseName(
+ const void* parent, const string& lowercase_name) const {
+ return FindPtrOrNull(fields_by_lowercase_name_,
+ PointerStringPair(parent, lowercase_name.c_str()));
+}
+
+inline const FieldDescriptor* FileDescriptorTables::FindFieldByCamelcaseName(
+ const void* parent, const string& camelcase_name) const {
+ return FindPtrOrNull(fields_by_camelcase_name_,
+ PointerStringPair(parent, camelcase_name.c_str()));
+}
+
+inline const EnumValueDescriptor* FileDescriptorTables::FindEnumValueByNumber(
+ const EnumDescriptor* parent, int number) const {
+ return FindPtrOrNull(enum_values_by_number_, make_pair(parent, number));
+}
+
+inline const FieldDescriptor* DescriptorPool::Tables::FindExtension(
+ const Descriptor* extendee, int number) {
+ return FindPtrOrNull(extensions_, make_pair(extendee, number));
+}
+
+inline void DescriptorPool::Tables::FindAllExtensions(
+ const Descriptor* extendee, vector<const FieldDescriptor*>* out) const {
+ ExtensionsGroupedByDescriptorMap::const_iterator it =
+ extensions_.lower_bound(make_pair(extendee, 0));
+ for (; it != extensions_.end() && it->first.first == extendee; ++it) {
+ out->push_back(it->second);
+ }
+}
+
+// -------------------------------------------------------------------
+
+bool DescriptorPool::Tables::AddSymbol(
+ const string& full_name, Symbol symbol) {
+ if (InsertIfNotPresent(&symbols_by_name_, full_name.c_str(), symbol)) {
+ symbols_after_checkpoint_.push_back(full_name.c_str());
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool FileDescriptorTables::AddAliasUnderParent(
+ const void* parent, const string& name, Symbol symbol) {
+ PointerStringPair by_parent_key(parent, name.c_str());
+ return InsertIfNotPresent(&symbols_by_parent_, by_parent_key, symbol);
+}
+
+bool DescriptorPool::Tables::AddFile(const FileDescriptor* file) {
+ if (InsertIfNotPresent(&files_by_name_, file->name().c_str(), file)) {
+ files_after_checkpoint_.push_back(file->name().c_str());
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void FileDescriptorTables::AddFieldByStylizedNames(
+ const FieldDescriptor* field) {
+ const void* parent;
+ if (field->is_extension()) {
+ if (field->extension_scope() == NULL) {
+ parent = field->file();
+ } else {
+ parent = field->extension_scope();
+ }
+ } else {
+ parent = field->containing_type();
+ }
+
+ PointerStringPair lowercase_key(parent, field->lowercase_name().c_str());
+ InsertIfNotPresent(&fields_by_lowercase_name_, lowercase_key, field);
+
+ PointerStringPair camelcase_key(parent, field->camelcase_name().c_str());
+ InsertIfNotPresent(&fields_by_camelcase_name_, camelcase_key, field);
+}
+
+bool FileDescriptorTables::AddFieldByNumber(const FieldDescriptor* field) {
+ DescriptorIntPair key(field->containing_type(), field->number());
+ return InsertIfNotPresent(&fields_by_number_, key, field);
+}
+
+bool FileDescriptorTables::AddEnumValueByNumber(
+ const EnumValueDescriptor* value) {
+ EnumIntPair key(value->type(), value->number());
+ return InsertIfNotPresent(&enum_values_by_number_, key, value);
+}
+
+bool DescriptorPool::Tables::AddExtension(const FieldDescriptor* field) {
+ DescriptorIntPair key(field->containing_type(), field->number());
+ if (InsertIfNotPresent(&extensions_, key, field)) {
+ extensions_after_checkpoint_.push_back(key);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+// -------------------------------------------------------------------
+
+template<typename Type>
+Type* DescriptorPool::Tables::Allocate() {
+ return reinterpret_cast<Type*>(AllocateBytes(sizeof(Type)));
+}
+
+template<typename Type>
+Type* DescriptorPool::Tables::AllocateArray(int count) {
+ return reinterpret_cast<Type*>(AllocateBytes(sizeof(Type) * count));
+}
+
+string* DescriptorPool::Tables::AllocateString(const string& value) {
+ string* result = new string(value);
+ strings_.push_back(result);
+ return result;
+}
+
+template<typename Type>
+Type* DescriptorPool::Tables::AllocateMessage(Type* /* dummy */) {
+ Type* result = new Type;
+ messages_.push_back(result);
+ return result;
+}
+
+FileDescriptorTables* DescriptorPool::Tables::AllocateFileTables() {
+ FileDescriptorTables* result = new FileDescriptorTables;
+ file_tables_.push_back(result);
+ return result;
+}
+
+void* DescriptorPool::Tables::AllocateBytes(int size) {
+ // TODO(kenton): Would it be worthwhile to implement this in some more
+ // sophisticated way? Probably not for the open source release, but for
+ // internal use we could easily plug in one of our existing memory pool
+ // allocators...
+ if (size == 0) return NULL;
+
+ void* result = operator new(size);
+ allocations_.push_back(result);
+ return result;
+}
+
+void FileDescriptorTables::BuildLocationsByPath(
+ pair<const FileDescriptorTables*, const SourceCodeInfo*>* p) {
+ for (int i = 0, len = p->second->location_size(); i < len; ++i) {
+ const SourceCodeInfo_Location* loc = &p->second->location().Get(i);
+ p->first->locations_by_path_[Join(loc->path(), ",")] = loc;
+ }
+}
+
+const SourceCodeInfo_Location* FileDescriptorTables::GetSourceLocation(
+ const vector<int>& path, const SourceCodeInfo* info) const {
+ pair<const FileDescriptorTables*, const SourceCodeInfo*> p(
+ make_pair(this, info));
+ locations_by_path_once_.Init(&FileDescriptorTables::BuildLocationsByPath, &p);
+ return FindPtrOrNull(locations_by_path_, Join(path, ","));
+}
+
+// ===================================================================
+// DescriptorPool
+
+DescriptorPool::ErrorCollector::~ErrorCollector() {}
+
+DescriptorPool::DescriptorPool()
+ : mutex_(NULL),
+ fallback_database_(NULL),
+ default_error_collector_(NULL),
+ underlay_(NULL),
+ tables_(new Tables),
+ enforce_dependencies_(true),
+ allow_unknown_(false),
+ enforce_weak_(false) {}
+
+DescriptorPool::DescriptorPool(DescriptorDatabase* fallback_database,
+ ErrorCollector* error_collector)
+ : mutex_(new Mutex),
+ fallback_database_(fallback_database),
+ default_error_collector_(error_collector),
+ underlay_(NULL),
+ tables_(new Tables),
+ enforce_dependencies_(true),
+ allow_unknown_(false),
+ enforce_weak_(false) {
+}
+
+DescriptorPool::DescriptorPool(const DescriptorPool* underlay)
+ : mutex_(NULL),
+ fallback_database_(NULL),
+ default_error_collector_(NULL),
+ underlay_(underlay),
+ tables_(new Tables),
+ enforce_dependencies_(true),
+ allow_unknown_(false),
+ enforce_weak_(false) {}
+
+DescriptorPool::~DescriptorPool() {
+ if (mutex_ != NULL) delete mutex_;
+}
+
+// DescriptorPool::BuildFile() defined later.
+// DescriptorPool::BuildFileCollectingErrors() defined later.
+
+void DescriptorPool::InternalDontEnforceDependencies() {
+ enforce_dependencies_ = false;
+}
+
+void DescriptorPool::AddUnusedImportTrackFile(const string& file_name) {
+ unused_import_track_files_.insert(file_name);
+}
+
+void DescriptorPool::ClearUnusedImportTrackFiles() {
+ unused_import_track_files_.clear();
+}
+
+bool DescriptorPool::InternalIsFileLoaded(const string& filename) const {
+ MutexLockMaybe lock(mutex_);
+ return tables_->FindFile(filename) != NULL;
+}
+
+// generated_pool ====================================================
+
+namespace {
+
+
+EncodedDescriptorDatabase* generated_database_ = NULL;
+DescriptorPool* generated_pool_ = NULL;
+GOOGLE_PROTOBUF_DECLARE_ONCE(generated_pool_init_);
+
+void DeleteGeneratedPool() {
+ delete generated_database_;
+ generated_database_ = NULL;
+ delete generated_pool_;
+ generated_pool_ = NULL;
+}
+
+static void InitGeneratedPool() {
+ generated_database_ = new EncodedDescriptorDatabase;
+ generated_pool_ = new DescriptorPool(generated_database_);
+
+ internal::OnShutdown(&DeleteGeneratedPool);
+}
+
+inline void InitGeneratedPoolOnce() {
+ ::google::protobuf::GoogleOnceInit(&generated_pool_init_, &InitGeneratedPool);
+}
+
+} // anonymous namespace
+
+const DescriptorPool* DescriptorPool::generated_pool() {
+ InitGeneratedPoolOnce();
+ return generated_pool_;
+}
+
+DescriptorPool* DescriptorPool::internal_generated_pool() {
+ InitGeneratedPoolOnce();
+ return generated_pool_;
+}
+
+void DescriptorPool::InternalAddGeneratedFile(
+ const void* encoded_file_descriptor, int size) {
+ // So, this function is called in the process of initializing the
+ // descriptors for generated proto classes. Each generated .pb.cc file
+ // has an internal procedure called AddDescriptors() which is called at
+ // process startup, and that function calls this one in order to register
+ // the raw bytes of the FileDescriptorProto representing the file.
+ //
+ // We do not actually construct the descriptor objects right away. We just
+ // hang on to the bytes until they are actually needed. We actually construct
+ // the descriptor the first time one of the following things happens:
+ // * Someone calls a method like descriptor(), GetDescriptor(), or
+ // GetReflection() on the generated types, which requires returning the
+ // descriptor or an object based on it.
+ // * Someone looks up the descriptor in DescriptorPool::generated_pool().
+ //
+ // Once one of these happens, the DescriptorPool actually parses the
+ // FileDescriptorProto and generates a FileDescriptor (and all its children)
+ // based on it.
+ //
+ // Note that FileDescriptorProto is itself a generated protocol message.
+ // Therefore, when we parse one, we have to be very careful to avoid using
+ // any descriptor-based operations, since this might cause infinite recursion
+ // or deadlock.
+ InitGeneratedPoolOnce();
+ GOOGLE_CHECK(generated_database_->Add(encoded_file_descriptor, size));
+}
+
+
+// Find*By* methods ==================================================
+
+// TODO(kenton): There's a lot of repeated code here, but I'm not sure if
+// there's any good way to factor it out. Think about this some time when
+// there's nothing more important to do (read: never).
+
+const FileDescriptor* DescriptorPool::FindFileByName(const string& name) const {
+ MutexLockMaybe lock(mutex_);
+ tables_->known_bad_symbols_.clear();
+ tables_->known_bad_files_.clear();
+ const FileDescriptor* result = tables_->FindFile(name);
+ if (result != NULL) return result;
+ if (underlay_ != NULL) {
+ result = underlay_->FindFileByName(name);
+ if (result != NULL) return result;
+ }
+ if (TryFindFileInFallbackDatabase(name)) {
+ result = tables_->FindFile(name);
+ if (result != NULL) return result;
+ }
+ return NULL;
+}
+
+const FileDescriptor* DescriptorPool::FindFileContainingSymbol(
+ const string& symbol_name) const {
+ MutexLockMaybe lock(mutex_);
+ tables_->known_bad_symbols_.clear();
+ tables_->known_bad_files_.clear();
+ Symbol result = tables_->FindSymbol(symbol_name);
+ if (!result.IsNull()) return result.GetFile();
+ if (underlay_ != NULL) {
+ const FileDescriptor* file_result =
+ underlay_->FindFileContainingSymbol(symbol_name);
+ if (file_result != NULL) return file_result;
+ }
+ if (TryFindSymbolInFallbackDatabase(symbol_name)) {
+ result = tables_->FindSymbol(symbol_name);
+ if (!result.IsNull()) return result.GetFile();
+ }
+ return NULL;
+}
+
+const Descriptor* DescriptorPool::FindMessageTypeByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ return (result.type == Symbol::MESSAGE) ? result.descriptor : NULL;
+}
+
+const FieldDescriptor* DescriptorPool::FindFieldByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ if (result.type == Symbol::FIELD &&
+ !result.field_descriptor->is_extension()) {
+ return result.field_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const FieldDescriptor* DescriptorPool::FindExtensionByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ if (result.type == Symbol::FIELD &&
+ result.field_descriptor->is_extension()) {
+ return result.field_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const OneofDescriptor* DescriptorPool::FindOneofByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ return (result.type == Symbol::ONEOF) ? result.oneof_descriptor : NULL;
+}
+
+const EnumDescriptor* DescriptorPool::FindEnumTypeByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ return (result.type == Symbol::ENUM) ? result.enum_descriptor : NULL;
+}
+
+const EnumValueDescriptor* DescriptorPool::FindEnumValueByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ return (result.type == Symbol::ENUM_VALUE) ?
+ result.enum_value_descriptor : NULL;
+}
+
+const ServiceDescriptor* DescriptorPool::FindServiceByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ return (result.type == Symbol::SERVICE) ? result.service_descriptor : NULL;
+}
+
+const MethodDescriptor* DescriptorPool::FindMethodByName(
+ const string& name) const {
+ Symbol result = tables_->FindByNameHelper(this, name);
+ return (result.type == Symbol::METHOD) ? result.method_descriptor : NULL;
+}
+
+const FieldDescriptor* DescriptorPool::FindExtensionByNumber(
+ const Descriptor* extendee, int number) const {
+ MutexLockMaybe lock(mutex_);
+ tables_->known_bad_symbols_.clear();
+ tables_->known_bad_files_.clear();
+ const FieldDescriptor* result = tables_->FindExtension(extendee, number);
+ if (result != NULL) {
+ return result;
+ }
+ if (underlay_ != NULL) {
+ result = underlay_->FindExtensionByNumber(extendee, number);
+ if (result != NULL) return result;
+ }
+ if (TryFindExtensionInFallbackDatabase(extendee, number)) {
+ result = tables_->FindExtension(extendee, number);
+ if (result != NULL) {
+ return result;
+ }
+ }
+ return NULL;
+}
+
+void DescriptorPool::FindAllExtensions(
+ const Descriptor* extendee, vector<const FieldDescriptor*>* out) const {
+ MutexLockMaybe lock(mutex_);
+ tables_->known_bad_symbols_.clear();
+ tables_->known_bad_files_.clear();
+
+ // Initialize tables_->extensions_ from the fallback database first
+ // (but do this only once per descriptor).
+ if (fallback_database_ != NULL &&
+ tables_->extensions_loaded_from_db_.count(extendee) == 0) {
+ vector<int> numbers;
+ if (fallback_database_->FindAllExtensionNumbers(extendee->full_name(),
+ &numbers)) {
+ for (int i = 0; i < numbers.size(); ++i) {
+ int number = numbers[i];
+ if (tables_->FindExtension(extendee, number) == NULL) {
+ TryFindExtensionInFallbackDatabase(extendee, number);
+ }
+ }
+ tables_->extensions_loaded_from_db_.insert(extendee);
+ }
+ }
+
+ tables_->FindAllExtensions(extendee, out);
+ if (underlay_ != NULL) {
+ underlay_->FindAllExtensions(extendee, out);
+ }
+}
+
+
+// -------------------------------------------------------------------
+
+const FieldDescriptor*
+Descriptor::FindFieldByNumber(int key) const {
+ const FieldDescriptor* result =
+ file()->tables_->FindFieldByNumber(this, key);
+ if (result == NULL || result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const FieldDescriptor*
+Descriptor::FindFieldByLowercaseName(const string& key) const {
+ const FieldDescriptor* result =
+ file()->tables_->FindFieldByLowercaseName(this, key);
+ if (result == NULL || result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const FieldDescriptor*
+Descriptor::FindFieldByCamelcaseName(const string& key) const {
+ const FieldDescriptor* result =
+ file()->tables_->FindFieldByCamelcaseName(this, key);
+ if (result == NULL || result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const FieldDescriptor*
+Descriptor::FindFieldByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::FIELD);
+ if (!result.IsNull() && !result.field_descriptor->is_extension()) {
+ return result.field_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const OneofDescriptor*
+Descriptor::FindOneofByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::ONEOF);
+ if (!result.IsNull()) {
+ return result.oneof_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const FieldDescriptor*
+Descriptor::FindExtensionByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::FIELD);
+ if (!result.IsNull() && result.field_descriptor->is_extension()) {
+ return result.field_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const FieldDescriptor*
+Descriptor::FindExtensionByLowercaseName(const string& key) const {
+ const FieldDescriptor* result =
+ file()->tables_->FindFieldByLowercaseName(this, key);
+ if (result == NULL || !result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const FieldDescriptor*
+Descriptor::FindExtensionByCamelcaseName(const string& key) const {
+ const FieldDescriptor* result =
+ file()->tables_->FindFieldByCamelcaseName(this, key);
+ if (result == NULL || !result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const Descriptor*
+Descriptor::FindNestedTypeByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::MESSAGE);
+ if (!result.IsNull()) {
+ return result.descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const EnumDescriptor*
+Descriptor::FindEnumTypeByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::ENUM);
+ if (!result.IsNull()) {
+ return result.enum_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const EnumValueDescriptor*
+Descriptor::FindEnumValueByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::ENUM_VALUE);
+ if (!result.IsNull()) {
+ return result.enum_value_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const EnumValueDescriptor*
+EnumDescriptor::FindValueByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::ENUM_VALUE);
+ if (!result.IsNull()) {
+ return result.enum_value_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const EnumValueDescriptor*
+EnumDescriptor::FindValueByNumber(int key) const {
+ return file()->tables_->FindEnumValueByNumber(this, key);
+}
+
+const MethodDescriptor*
+ServiceDescriptor::FindMethodByName(const string& key) const {
+ Symbol result =
+ file()->tables_->FindNestedSymbolOfType(this, key, Symbol::METHOD);
+ if (!result.IsNull()) {
+ return result.method_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const Descriptor*
+FileDescriptor::FindMessageTypeByName(const string& key) const {
+ Symbol result = tables_->FindNestedSymbolOfType(this, key, Symbol::MESSAGE);
+ if (!result.IsNull()) {
+ return result.descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const EnumDescriptor*
+FileDescriptor::FindEnumTypeByName(const string& key) const {
+ Symbol result = tables_->FindNestedSymbolOfType(this, key, Symbol::ENUM);
+ if (!result.IsNull()) {
+ return result.enum_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const EnumValueDescriptor*
+FileDescriptor::FindEnumValueByName(const string& key) const {
+ Symbol result =
+ tables_->FindNestedSymbolOfType(this, key, Symbol::ENUM_VALUE);
+ if (!result.IsNull()) {
+ return result.enum_value_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const ServiceDescriptor*
+FileDescriptor::FindServiceByName(const string& key) const {
+ Symbol result = tables_->FindNestedSymbolOfType(this, key, Symbol::SERVICE);
+ if (!result.IsNull()) {
+ return result.service_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const FieldDescriptor*
+FileDescriptor::FindExtensionByName(const string& key) const {
+ Symbol result = tables_->FindNestedSymbolOfType(this, key, Symbol::FIELD);
+ if (!result.IsNull() && result.field_descriptor->is_extension()) {
+ return result.field_descriptor;
+ } else {
+ return NULL;
+ }
+}
+
+const FieldDescriptor*
+FileDescriptor::FindExtensionByLowercaseName(const string& key) const {
+ const FieldDescriptor* result = tables_->FindFieldByLowercaseName(this, key);
+ if (result == NULL || !result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const FieldDescriptor*
+FileDescriptor::FindExtensionByCamelcaseName(const string& key) const {
+ const FieldDescriptor* result = tables_->FindFieldByCamelcaseName(this, key);
+ if (result == NULL || !result->is_extension()) {
+ return NULL;
+ } else {
+ return result;
+ }
+}
+
+const Descriptor::ExtensionRange*
+Descriptor::FindExtensionRangeContainingNumber(int number) const {
+ // Linear search should be fine because we don't expect a message to have
+ // more than a couple extension ranges.
+ for (int i = 0; i < extension_range_count(); i++) {
+ if (number >= extension_range(i)->start &&
+ number < extension_range(i)->end) {
+ return extension_range(i);
+ }
+ }
+ return NULL;
+}
+
+// -------------------------------------------------------------------
+
+bool DescriptorPool::TryFindFileInFallbackDatabase(const string& name) const {
+ if (fallback_database_ == NULL) return false;
+
+ if (tables_->known_bad_files_.count(name) > 0) return false;
+
+ FileDescriptorProto file_proto;
+ if (!fallback_database_->FindFileByName(name, &file_proto) ||
+ BuildFileFromDatabase(file_proto) == NULL) {
+ tables_->known_bad_files_.insert(name);
+ return false;
+ }
+ return true;
+}
+
+bool DescriptorPool::IsSubSymbolOfBuiltType(const string& name) const {
+ string prefix = name;
+ for (;;) {
+ string::size_type dot_pos = prefix.find_last_of('.');
+ if (dot_pos == string::npos) {
+ break;
+ }
+ prefix = prefix.substr(0, dot_pos);
+ Symbol symbol = tables_->FindSymbol(prefix);
+ // If the symbol type is anything other than PACKAGE, then its complete
+ // definition is already known.
+ if (!symbol.IsNull() && symbol.type != Symbol::PACKAGE) {
+ return true;
+ }
+ }
+ if (underlay_ != NULL) {
+ // Check to see if any prefix of this symbol exists in the underlay.
+ return underlay_->IsSubSymbolOfBuiltType(name);
+ }
+ return false;
+}
+
+bool DescriptorPool::TryFindSymbolInFallbackDatabase(const string& name) const {
+ if (fallback_database_ == NULL) return false;
+
+ if (tables_->known_bad_symbols_.count(name) > 0) return false;
+
+ FileDescriptorProto file_proto;
+ if (// We skip looking in the fallback database if the name is a sub-symbol
+ // of any descriptor that already exists in the descriptor pool (except
+ // for package descriptors). This is valid because all symbols except
+ // for packages are defined in a single file, so if the symbol exists
+ // then we should already have its definition.
+ //
+ // The other reason to do this is to support "overriding" type
+ // definitions by merging two databases that define the same type. (Yes,
+ // people do this.) The main difficulty with making this work is that
+ // FindFileContainingSymbol() is allowed to return both false positives
+ // (e.g., SimpleDescriptorDatabase, UpgradedDescriptorDatabase) and false
+ // negatives (e.g. ProtoFileParser, SourceTreeDescriptorDatabase).
+ // When two such databases are merged, looking up a non-existent
+ // sub-symbol of a type that already exists in the descriptor pool can
+ // result in an attempt to load multiple definitions of the same type.
+ // The check below avoids this.
+ IsSubSymbolOfBuiltType(name)
+
+ // Look up file containing this symbol in fallback database.
+ || !fallback_database_->FindFileContainingSymbol(name, &file_proto)
+
+ // Check if we've already built this file. If so, it apparently doesn't
+ // contain the symbol we're looking for. Some DescriptorDatabases
+ // return false positives.
+ || tables_->FindFile(file_proto.name()) != NULL
+
+ // Build the file.
+ || BuildFileFromDatabase(file_proto) == NULL) {
+ tables_->known_bad_symbols_.insert(name);
+ return false;
+ }
+
+ return true;
+}
+
+bool DescriptorPool::TryFindExtensionInFallbackDatabase(
+ const Descriptor* containing_type, int field_number) const {
+ if (fallback_database_ == NULL) return false;
+
+ FileDescriptorProto file_proto;
+ if (!fallback_database_->FindFileContainingExtension(
+ containing_type->full_name(), field_number, &file_proto)) {
+ return false;
+ }
+
+ if (tables_->FindFile(file_proto.name()) != NULL) {
+ // We've already loaded this file, and it apparently doesn't contain the
+ // extension we're looking for. Some DescriptorDatabases return false
+ // positives.
+ return false;
+ }
+
+ if (BuildFileFromDatabase(file_proto) == NULL) {
+ return false;
+ }
+
+ return true;
+}
+
+// ===================================================================
+
+string FieldDescriptor::DefaultValueAsString(bool quote_string_type) const {
+ GOOGLE_CHECK(has_default_value()) << "No default value";
+ switch (cpp_type()) {
+ case CPPTYPE_INT32:
+ return SimpleItoa(default_value_int32());
+ break;
+ case CPPTYPE_INT64:
+ return SimpleItoa(default_value_int64());
+ break;
+ case CPPTYPE_UINT32:
+ return SimpleItoa(default_value_uint32());
+ break;
+ case CPPTYPE_UINT64:
+ return SimpleItoa(default_value_uint64());
+ break;
+ case CPPTYPE_FLOAT:
+ return SimpleFtoa(default_value_float());
+ break;
+ case CPPTYPE_DOUBLE:
+ return SimpleDtoa(default_value_double());
+ break;
+ case CPPTYPE_BOOL:
+ return default_value_bool() ? "true" : "false";
+ break;
+ case CPPTYPE_STRING:
+ if (quote_string_type) {
+ return "\"" + CEscape(default_value_string()) + "\"";
+ } else {
+ if (type() == TYPE_BYTES) {
+ return CEscape(default_value_string());
+ } else {
+ return default_value_string();
+ }
+ }
+ break;
+ case CPPTYPE_ENUM:
+ return default_value_enum()->name();
+ break;
+ case CPPTYPE_MESSAGE:
+ GOOGLE_LOG(DFATAL) << "Messages can't have default values!";
+ break;
+ }
+ GOOGLE_LOG(FATAL) << "Can't get here: failed to get default value as string";
+ return "";
+}
+
+// CopyTo methods ====================================================
+
+void FileDescriptor::CopyTo(FileDescriptorProto* proto) const {
+ proto->set_name(name());
+ if (!package().empty()) proto->set_package(package());
+
+ for (int i = 0; i < dependency_count(); i++) {
+ proto->add_dependency(dependency(i)->name());
+ }
+
+ for (int i = 0; i < public_dependency_count(); i++) {
+ proto->add_public_dependency(public_dependencies_[i]);
+ }
+
+ for (int i = 0; i < weak_dependency_count(); i++) {
+ proto->add_weak_dependency(weak_dependencies_[i]);
+ }
+
+ for (int i = 0; i < message_type_count(); i++) {
+ message_type(i)->CopyTo(proto->add_message_type());
+ }
+ for (int i = 0; i < enum_type_count(); i++) {
+ enum_type(i)->CopyTo(proto->add_enum_type());
+ }
+ for (int i = 0; i < service_count(); i++) {
+ service(i)->CopyTo(proto->add_service());
+ }
+ for (int i = 0; i < extension_count(); i++) {
+ extension(i)->CopyTo(proto->add_extension());
+ }
+
+ if (&options() != &FileOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+void FileDescriptor::CopySourceCodeInfoTo(FileDescriptorProto* proto) const {
+ if (source_code_info_ != &SourceCodeInfo::default_instance()) {
+ proto->mutable_source_code_info()->CopyFrom(*source_code_info_);
+ }
+}
+
+void Descriptor::CopyTo(DescriptorProto* proto) const {
+ proto->set_name(name());
+
+ for (int i = 0; i < field_count(); i++) {
+ field(i)->CopyTo(proto->add_field());
+ }
+ for (int i = 0; i < oneof_decl_count(); i++) {
+ oneof_decl(i)->CopyTo(proto->add_oneof_decl());
+ }
+ for (int i = 0; i < nested_type_count(); i++) {
+ nested_type(i)->CopyTo(proto->add_nested_type());
+ }
+ for (int i = 0; i < enum_type_count(); i++) {
+ enum_type(i)->CopyTo(proto->add_enum_type());
+ }
+ for (int i = 0; i < extension_range_count(); i++) {
+ DescriptorProto::ExtensionRange* range = proto->add_extension_range();
+ range->set_start(extension_range(i)->start);
+ range->set_end(extension_range(i)->end);
+ }
+ for (int i = 0; i < extension_count(); i++) {
+ extension(i)->CopyTo(proto->add_extension());
+ }
+
+ if (&options() != &MessageOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+void FieldDescriptor::CopyTo(FieldDescriptorProto* proto) const {
+ proto->set_name(name());
+ proto->set_number(number());
+
+ // Some compilers do not allow static_cast directly between two enum types,
+ // so we must cast to int first.
+ proto->set_label(static_cast<FieldDescriptorProto::Label>(
+ implicit_cast<int>(label())));
+ proto->set_type(static_cast<FieldDescriptorProto::Type>(
+ implicit_cast<int>(type())));
+
+ if (is_extension()) {
+ if (!containing_type()->is_unqualified_placeholder_) {
+ proto->set_extendee(".");
+ }
+ proto->mutable_extendee()->append(containing_type()->full_name());
+ }
+
+ if (cpp_type() == CPPTYPE_MESSAGE) {
+ if (message_type()->is_placeholder_) {
+ // We don't actually know if the type is a message type. It could be
+ // an enum.
+ proto->clear_type();
+ }
+
+ if (!message_type()->is_unqualified_placeholder_) {
+ proto->set_type_name(".");
+ }
+ proto->mutable_type_name()->append(message_type()->full_name());
+ } else if (cpp_type() == CPPTYPE_ENUM) {
+ if (!enum_type()->is_unqualified_placeholder_) {
+ proto->set_type_name(".");
+ }
+ proto->mutable_type_name()->append(enum_type()->full_name());
+ }
+
+ if (has_default_value()) {
+ proto->set_default_value(DefaultValueAsString(false));
+ }
+
+ if (containing_oneof() != NULL && !is_extension()) {
+ proto->set_oneof_index(containing_oneof()->index());
+ }
+
+ if (&options() != &FieldOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+void OneofDescriptor::CopyTo(OneofDescriptorProto* proto) const {
+ proto->set_name(name());
+}
+
+void EnumDescriptor::CopyTo(EnumDescriptorProto* proto) const {
+ proto->set_name(name());
+
+ for (int i = 0; i < value_count(); i++) {
+ value(i)->CopyTo(proto->add_value());
+ }
+
+ if (&options() != &EnumOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+void EnumValueDescriptor::CopyTo(EnumValueDescriptorProto* proto) const {
+ proto->set_name(name());
+ proto->set_number(number());
+
+ if (&options() != &EnumValueOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+void ServiceDescriptor::CopyTo(ServiceDescriptorProto* proto) const {
+ proto->set_name(name());
+
+ for (int i = 0; i < method_count(); i++) {
+ method(i)->CopyTo(proto->add_method());
+ }
+
+ if (&options() != &ServiceOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+void MethodDescriptor::CopyTo(MethodDescriptorProto* proto) const {
+ proto->set_name(name());
+
+ if (!input_type()->is_unqualified_placeholder_) {
+ proto->set_input_type(".");
+ }
+ proto->mutable_input_type()->append(input_type()->full_name());
+
+ if (!output_type()->is_unqualified_placeholder_) {
+ proto->set_output_type(".");
+ }
+ proto->mutable_output_type()->append(output_type()->full_name());
+
+ if (&options() != &MethodOptions::default_instance()) {
+ proto->mutable_options()->CopyFrom(options());
+ }
+}
+
+// DebugString methods ===============================================
+
+namespace {
+
+// Used by each of the option formatters.
+bool RetrieveOptions(int depth,
+ const Message &options,
+ vector<string> *option_entries) {
+ option_entries->clear();
+ const Reflection* reflection = options.GetReflection();
+ vector<const FieldDescriptor*> fields;
+ reflection->ListFields(options, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ int count = 1;
+ bool repeated = false;
+ if (fields[i]->is_repeated()) {
+ count = reflection->FieldSize(options, fields[i]);
+ repeated = true;
+ }
+ for (int j = 0; j < count; j++) {
+ string fieldval;
+ if (fields[i]->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ string tmp;
+ TextFormat::Printer printer;
+ printer.SetInitialIndentLevel(depth + 1);
+ printer.PrintFieldValueToString(options, fields[i],
+ repeated ? j : -1, &tmp);
+ fieldval.append("{\n");
+ fieldval.append(tmp);
+ fieldval.append(depth * 2, ' ');
+ fieldval.append("}");
+ } else {
+ TextFormat::PrintFieldValueToString(options, fields[i],
+ repeated ? j : -1, &fieldval);
+ }
+ string name;
+ if (fields[i]->is_extension()) {
+ name = "(." + fields[i]->full_name() + ")";
+ } else {
+ name = fields[i]->name();
+ }
+ option_entries->push_back(name + " = " + fieldval);
+ }
+ }
+ return !option_entries->empty();
+}
+
+// Formats options that all appear together in brackets. Does not include
+// brackets.
+bool FormatBracketedOptions(int depth, const Message &options, string *output) {
+ vector<string> all_options;
+ if (RetrieveOptions(depth, options, &all_options)) {
+ output->append(Join(all_options, ", "));
+ }
+ return !all_options.empty();
+}
+
+// Formats options one per line
+bool FormatLineOptions(int depth, const Message &options, string *output) {
+ string prefix(depth * 2, ' ');
+ vector<string> all_options;
+ if (RetrieveOptions(depth, options, &all_options)) {
+ for (int i = 0; i < all_options.size(); i++) {
+ strings::SubstituteAndAppend(output, "$0option $1;\n",
+ prefix, all_options[i]);
+ }
+ }
+ return !all_options.empty();
+}
+
+} // anonymous namespace
+
+string FileDescriptor::DebugString() const {
+ string contents = "syntax = \"proto2\";\n\n";
+
+ set<int> public_dependencies;
+ set<int> weak_dependencies;
+ public_dependencies.insert(public_dependencies_,
+ public_dependencies_ + public_dependency_count_);
+ weak_dependencies.insert(weak_dependencies_,
+ weak_dependencies_ + weak_dependency_count_);
+
+ for (int i = 0; i < dependency_count(); i++) {
+ if (public_dependencies.count(i) > 0) {
+ strings::SubstituteAndAppend(&contents, "import public \"$0\";\n",
+ dependency(i)->name());
+ } else if (weak_dependencies.count(i) > 0) {
+ strings::SubstituteAndAppend(&contents, "import weak \"$0\";\n",
+ dependency(i)->name());
+ } else {
+ strings::SubstituteAndAppend(&contents, "import \"$0\";\n",
+ dependency(i)->name());
+ }
+ }
+
+ if (!package().empty()) {
+ strings::SubstituteAndAppend(&contents, "package $0;\n\n", package());
+ }
+
+ if (FormatLineOptions(0, options(), &contents)) {
+ contents.append("\n"); // add some space if we had options
+ }
+
+ for (int i = 0; i < enum_type_count(); i++) {
+ enum_type(i)->DebugString(0, &contents);
+ contents.append("\n");
+ }
+
+ // Find all the 'group' type extensions; we will not output their nested
+ // definitions (those will be done with their group field descriptor).
+ set<const Descriptor*> groups;
+ for (int i = 0; i < extension_count(); i++) {
+ if (extension(i)->type() == FieldDescriptor::TYPE_GROUP) {
+ groups.insert(extension(i)->message_type());
+ }
+ }
+
+ for (int i = 0; i < message_type_count(); i++) {
+ if (groups.count(message_type(i)) == 0) {
+ strings::SubstituteAndAppend(&contents, "message $0",
+ message_type(i)->name());
+ message_type(i)->DebugString(0, &contents);
+ contents.append("\n");
+ }
+ }
+
+ for (int i = 0; i < service_count(); i++) {
+ service(i)->DebugString(&contents);
+ contents.append("\n");
+ }
+
+ const Descriptor* containing_type = NULL;
+ for (int i = 0; i < extension_count(); i++) {
+ if (extension(i)->containing_type() != containing_type) {
+ if (i > 0) contents.append("}\n\n");
+ containing_type = extension(i)->containing_type();
+ strings::SubstituteAndAppend(&contents, "extend .$0 {\n",
+ containing_type->full_name());
+ }
+ extension(i)->DebugString(1, FieldDescriptor::PRINT_LABEL, &contents);
+ }
+ if (extension_count() > 0) contents.append("}\n\n");
+
+ return contents;
+}
+
+string Descriptor::DebugString() const {
+ string contents;
+ strings::SubstituteAndAppend(&contents, "message $0", name());
+ DebugString(0, &contents);
+ return contents;
+}
+
+void Descriptor::DebugString(int depth, string *contents) const {
+ string prefix(depth * 2, ' ');
+ ++depth;
+ contents->append(" {\n");
+
+ FormatLineOptions(depth, options(), contents);
+
+ // Find all the 'group' types for fields and extensions; we will not output
+ // their nested definitions (those will be done with their group field
+ // descriptor).
+ set<const Descriptor*> groups;
+ for (int i = 0; i < field_count(); i++) {
+ if (field(i)->type() == FieldDescriptor::TYPE_GROUP) {
+ groups.insert(field(i)->message_type());
+ }
+ }
+ for (int i = 0; i < extension_count(); i++) {
+ if (extension(i)->type() == FieldDescriptor::TYPE_GROUP) {
+ groups.insert(extension(i)->message_type());
+ }
+ }
+
+ for (int i = 0; i < nested_type_count(); i++) {
+ if (groups.count(nested_type(i)) == 0) {
+ strings::SubstituteAndAppend(contents, "$0 message $1",
+ prefix, nested_type(i)->name());
+ nested_type(i)->DebugString(depth, contents);
+ }
+ }
+ for (int i = 0; i < enum_type_count(); i++) {
+ enum_type(i)->DebugString(depth, contents);
+ }
+ for (int i = 0; i < field_count(); i++) {
+ if (field(i)->containing_oneof() == NULL) {
+ field(i)->DebugString(depth, FieldDescriptor::PRINT_LABEL, contents);
+ } else if (field(i)->containing_oneof()->field(0) == field(i)) {
+ // This is the first field in this oneof, so print the whole oneof.
+ field(i)->containing_oneof()->DebugString(depth, contents);
+ }
+ }
+
+ for (int i = 0; i < extension_range_count(); i++) {
+ strings::SubstituteAndAppend(contents, "$0 extensions $1 to $2;\n",
+ prefix,
+ extension_range(i)->start,
+ extension_range(i)->end - 1);
+ }
+
+ // Group extensions by what they extend, so they can be printed out together.
+ const Descriptor* containing_type = NULL;
+ for (int i = 0; i < extension_count(); i++) {
+ if (extension(i)->containing_type() != containing_type) {
+ if (i > 0) strings::SubstituteAndAppend(contents, "$0 }\n", prefix);
+ containing_type = extension(i)->containing_type();
+ strings::SubstituteAndAppend(contents, "$0 extend .$1 {\n",
+ prefix, containing_type->full_name());
+ }
+ extension(i)->DebugString(
+ depth + 1, FieldDescriptor::PRINT_LABEL, contents);
+ }
+ if (extension_count() > 0)
+ strings::SubstituteAndAppend(contents, "$0 }\n", prefix);
+
+ strings::SubstituteAndAppend(contents, "$0}\n", prefix);
+}
+
+string FieldDescriptor::DebugString() const {
+ string contents;
+ int depth = 0;
+ if (is_extension()) {
+ strings::SubstituteAndAppend(&contents, "extend .$0 {\n",
+ containing_type()->full_name());
+ depth = 1;
+ }
+ DebugString(depth, PRINT_LABEL, &contents);
+ if (is_extension()) {
+ contents.append("}\n");
+ }
+ return contents;
+}
+
+void FieldDescriptor::DebugString(int depth,
+ PrintLabelFlag print_label_flag,
+ string *contents) const {
+ string prefix(depth * 2, ' ');
+ string field_type;
+ switch (type()) {
+ case TYPE_MESSAGE:
+ field_type = "." + message_type()->full_name();
+ break;
+ case TYPE_ENUM:
+ field_type = "." + enum_type()->full_name();
+ break;
+ default:
+ field_type = kTypeToName[type()];
+ }
+
+ string label;
+ if (print_label_flag == PRINT_LABEL) {
+ label = kLabelToName[this->label()];
+ label.push_back(' ');
+ }
+
+ strings::SubstituteAndAppend(contents, "$0$1$2 $3 = $4",
+ prefix,
+ label,
+ field_type,
+ type() == TYPE_GROUP ? message_type()->name() :
+ name(),
+ number());
+
+ bool bracketed = false;
+ if (has_default_value()) {
+ bracketed = true;
+ strings::SubstituteAndAppend(contents, " [default = $0",
+ DefaultValueAsString(true));
+ }
+
+ string formatted_options;
+ if (FormatBracketedOptions(depth, options(), &formatted_options)) {
+ contents->append(bracketed ? ", " : " [");
+ bracketed = true;
+ contents->append(formatted_options);
+ }
+
+ if (bracketed) {
+ contents->append("]");
+ }
+
+ if (type() == TYPE_GROUP) {
+ message_type()->DebugString(depth, contents);
+ } else {
+ contents->append(";\n");
+ }
+}
+
+string OneofDescriptor::DebugString() const {
+ string contents;
+ DebugString(0, &contents);
+ return contents;
+}
+
+void OneofDescriptor::DebugString(int depth, string* contents) const {
+ string prefix(depth * 2, ' ');
+ ++depth;
+ strings::SubstituteAndAppend(
+ contents, "$0 oneof $1 {\n", prefix, name());
+ for (int i = 0; i < field_count(); i++) {
+ field(i)->DebugString(depth, FieldDescriptor::OMIT_LABEL, contents);
+ }
+ strings::SubstituteAndAppend(contents, "$0}\n", prefix);
+}
+
+string EnumDescriptor::DebugString() const {
+ string contents;
+ DebugString(0, &contents);
+ return contents;
+}
+
+void EnumDescriptor::DebugString(int depth, string *contents) const {
+ string prefix(depth * 2, ' ');
+ ++depth;
+ strings::SubstituteAndAppend(contents, "$0enum $1 {\n",
+ prefix, name());
+
+ FormatLineOptions(depth, options(), contents);
+
+ for (int i = 0; i < value_count(); i++) {
+ value(i)->DebugString(depth, contents);
+ }
+ strings::SubstituteAndAppend(contents, "$0}\n", prefix);
+}
+
+string EnumValueDescriptor::DebugString() const {
+ string contents;
+ DebugString(0, &contents);
+ return contents;
+}
+
+void EnumValueDescriptor::DebugString(int depth, string *contents) const {
+ string prefix(depth * 2, ' ');
+ strings::SubstituteAndAppend(contents, "$0$1 = $2",
+ prefix, name(), number());
+
+ string formatted_options;
+ if (FormatBracketedOptions(depth, options(), &formatted_options)) {
+ strings::SubstituteAndAppend(contents, " [$0]", formatted_options);
+ }
+ contents->append(";\n");
+}
+
+string ServiceDescriptor::DebugString() const {
+ string contents;
+ DebugString(&contents);
+ return contents;
+}
+
+void ServiceDescriptor::DebugString(string *contents) const {
+ strings::SubstituteAndAppend(contents, "service $0 {\n", name());
+
+ FormatLineOptions(1, options(), contents);
+
+ for (int i = 0; i < method_count(); i++) {
+ method(i)->DebugString(1, contents);
+ }
+
+ contents->append("}\n");
+}
+
+string MethodDescriptor::DebugString() const {
+ string contents;
+ DebugString(0, &contents);
+ return contents;
+}
+
+void MethodDescriptor::DebugString(int depth, string *contents) const {
+ string prefix(depth * 2, ' ');
+ ++depth;
+ strings::SubstituteAndAppend(contents, "$0rpc $1(.$2) returns (.$3)",
+ prefix, name(),
+ input_type()->full_name(),
+ output_type()->full_name());
+
+ string formatted_options;
+ if (FormatLineOptions(depth, options(), &formatted_options)) {
+ strings::SubstituteAndAppend(contents, " {\n$0$1}\n",
+ formatted_options, prefix);
+ } else {
+ contents->append(";\n");
+ }
+}
+
+
+// Location methods ===============================================
+
+bool FileDescriptor::GetSourceLocation(const vector<int>& path,
+ SourceLocation* out_location) const {
+ GOOGLE_CHECK_NOTNULL(out_location);
+ if (source_code_info_) {
+ if (const SourceCodeInfo_Location* loc =
+ tables_->GetSourceLocation(path, source_code_info_)) {
+ const RepeatedField<int32>& span = loc->span();
+ if (span.size() == 3 || span.size() == 4) {
+ out_location->start_line = span.Get(0);
+ out_location->start_column = span.Get(1);
+ out_location->end_line = span.Get(span.size() == 3 ? 0 : 2);
+ out_location->end_column = span.Get(span.size() - 1);
+
+ out_location->leading_comments = loc->leading_comments();
+ out_location->trailing_comments = loc->trailing_comments();
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool FieldDescriptor::is_packed() const {
+ return is_packable() && (options_ != NULL) && options_->packed();
+}
+
+bool Descriptor::GetSourceLocation(SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return file()->GetSourceLocation(path, out_location);
+}
+
+bool FieldDescriptor::GetSourceLocation(SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return file()->GetSourceLocation(path, out_location);
+}
+
+bool OneofDescriptor::GetSourceLocation(SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return containing_type()->file()->GetSourceLocation(path, out_location);
+}
+
+bool EnumDescriptor::GetSourceLocation(SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return file()->GetSourceLocation(path, out_location);
+}
+
+bool MethodDescriptor::GetSourceLocation(SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return service()->file()->GetSourceLocation(path, out_location);
+}
+
+bool ServiceDescriptor::GetSourceLocation(SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return file()->GetSourceLocation(path, out_location);
+}
+
+bool EnumValueDescriptor::GetSourceLocation(
+ SourceLocation* out_location) const {
+ vector<int> path;
+ GetLocationPath(&path);
+ return type()->file()->GetSourceLocation(path, out_location);
+}
+
+void Descriptor::GetLocationPath(vector<int>* output) const {
+ if (containing_type()) {
+ containing_type()->GetLocationPath(output);
+ output->push_back(DescriptorProto::kNestedTypeFieldNumber);
+ output->push_back(index());
+ } else {
+ output->push_back(FileDescriptorProto::kMessageTypeFieldNumber);
+ output->push_back(index());
+ }
+}
+
+void FieldDescriptor::GetLocationPath(vector<int>* output) const {
+ if (is_extension()) {
+ if (extension_scope() == NULL) {
+ output->push_back(FileDescriptorProto::kExtensionFieldNumber);
+ output->push_back(index());
+ } else {
+ extension_scope()->GetLocationPath(output);
+ output->push_back(DescriptorProto::kExtensionFieldNumber);
+ output->push_back(index());
+ }
+ } else {
+ containing_type()->GetLocationPath(output);
+ output->push_back(DescriptorProto::kFieldFieldNumber);
+ output->push_back(index());
+ }
+}
+
+void OneofDescriptor::GetLocationPath(vector<int>* output) const {
+ containing_type()->GetLocationPath(output);
+ output->push_back(DescriptorProto::kOneofDeclFieldNumber);
+ output->push_back(index());
+}
+
+void EnumDescriptor::GetLocationPath(vector<int>* output) const {
+ if (containing_type()) {
+ containing_type()->GetLocationPath(output);
+ output->push_back(DescriptorProto::kEnumTypeFieldNumber);
+ output->push_back(index());
+ } else {
+ output->push_back(FileDescriptorProto::kEnumTypeFieldNumber);
+ output->push_back(index());
+ }
+}
+
+void EnumValueDescriptor::GetLocationPath(vector<int>* output) const {
+ type()->GetLocationPath(output);
+ output->push_back(EnumDescriptorProto::kValueFieldNumber);
+ output->push_back(index());
+}
+
+void ServiceDescriptor::GetLocationPath(vector<int>* output) const {
+ output->push_back(FileDescriptorProto::kServiceFieldNumber);
+ output->push_back(index());
+}
+
+void MethodDescriptor::GetLocationPath(vector<int>* output) const {
+ service()->GetLocationPath(output);
+ output->push_back(ServiceDescriptorProto::kMethodFieldNumber);
+ output->push_back(index());
+}
+
+// ===================================================================
+
+namespace {
+
+// Represents an options message to interpret. Extension names in the option
+// name are respolved relative to name_scope. element_name and orig_opt are
+// used only for error reporting (since the parser records locations against
+// pointers in the original options, not the mutable copy). The Message must be
+// one of the Options messages in descriptor.proto.
+struct OptionsToInterpret {
+ OptionsToInterpret(const string& ns,
+ const string& el,
+ const Message* orig_opt,
+ Message* opt)
+ : name_scope(ns),
+ element_name(el),
+ original_options(orig_opt),
+ options(opt) {
+ }
+ string name_scope;
+ string element_name;
+ const Message* original_options;
+ Message* options;
+};
+
+} // namespace
+
+class DescriptorBuilder {
+ public:
+ DescriptorBuilder(const DescriptorPool* pool,
+ DescriptorPool::Tables* tables,
+ DescriptorPool::ErrorCollector* error_collector);
+ ~DescriptorBuilder();
+
+ const FileDescriptor* BuildFile(const FileDescriptorProto& proto);
+
+ private:
+ friend class OptionInterpreter;
+
+ const DescriptorPool* pool_;
+ DescriptorPool::Tables* tables_; // for convenience
+ DescriptorPool::ErrorCollector* error_collector_;
+
+ // As we build descriptors we store copies of the options messages in
+ // them. We put pointers to those copies in this vector, as we build, so we
+ // can later (after cross-linking) interpret those options.
+ vector<OptionsToInterpret> options_to_interpret_;
+
+ bool had_errors_;
+ string filename_;
+ FileDescriptor* file_;
+ FileDescriptorTables* file_tables_;
+ set<const FileDescriptor*> dependencies_;
+
+ // unused_dependency_ is used to record the unused imported files.
+ // Note: public import is not considered.
+ set<const FileDescriptor*> unused_dependency_;
+
+ // If LookupSymbol() finds a symbol that is in a file which is not a declared
+ // dependency of this file, it will fail, but will set
+ // possible_undeclared_dependency_ to point at that file. This is only used
+ // by AddNotDefinedError() to report a more useful error message.
+ // possible_undeclared_dependency_name_ is the name of the symbol that was
+ // actually found in possible_undeclared_dependency_, which may be a parent
+ // of the symbol actually looked for.
+ const FileDescriptor* possible_undeclared_dependency_;
+ string possible_undeclared_dependency_name_;
+
+ // If LookupSymbol() could resolve a symbol which is not defined,
+ // record the resolved name. This is only used by AddNotDefinedError()
+ // to report a more useful error message.
+ string undefine_resolved_name_;
+
+ void AddError(const string& element_name,
+ const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& error);
+ void AddError(const string& element_name,
+ const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const char* error);
+ void AddRecursiveImportError(const FileDescriptorProto& proto, int from_here);
+ void AddTwiceListedError(const FileDescriptorProto& proto, int index);
+ void AddImportError(const FileDescriptorProto& proto, int index);
+
+ // Adds an error indicating that undefined_symbol was not defined. Must
+ // only be called after LookupSymbol() fails.
+ void AddNotDefinedError(
+ const string& element_name,
+ const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& undefined_symbol);
+
+ void AddWarning(const string& element_name, const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& error);
+
+ // Silly helper which determines if the given file is in the given package.
+ // I.e., either file->package() == package_name or file->package() is a
+ // nested package within package_name.
+ bool IsInPackage(const FileDescriptor* file, const string& package_name);
+
+ // Helper function which finds all public dependencies of the given file, and
+ // stores the them in the dependencies_ set in the builder.
+ void RecordPublicDependencies(const FileDescriptor* file);
+
+ // Like tables_->FindSymbol(), but additionally:
+ // - Search the pool's underlay if not found in tables_.
+ // - Insure that the resulting Symbol is from one of the file's declared
+ // dependencies.
+ Symbol FindSymbol(const string& name);
+
+ // Like FindSymbol() but does not require that the symbol is in one of the
+ // file's declared dependencies.
+ Symbol FindSymbolNotEnforcingDeps(const string& name);
+
+ // This implements the body of FindSymbolNotEnforcingDeps().
+ Symbol FindSymbolNotEnforcingDepsHelper(const DescriptorPool* pool,
+ const string& name);
+
+ // Like FindSymbol(), but looks up the name relative to some other symbol
+ // name. This first searches siblings of relative_to, then siblings of its
+ // parents, etc. For example, LookupSymbol("foo.bar", "baz.qux.corge") makes
+ // the following calls, returning the first non-null result:
+ // FindSymbol("baz.qux.foo.bar"), FindSymbol("baz.foo.bar"),
+ // FindSymbol("foo.bar"). If AllowUnknownDependencies() has been called
+ // on the DescriptorPool, this will generate a placeholder type if
+ // the name is not found (unless the name itself is malformed). The
+ // placeholder_type parameter indicates what kind of placeholder should be
+ // constructed in this case. The resolve_mode parameter determines whether
+ // any symbol is returned, or only symbols that are types. Note, however,
+ // that LookupSymbol may still return a non-type symbol in LOOKUP_TYPES mode,
+ // if it believes that's all it could refer to. The caller should always
+ // check that it receives the type of symbol it was expecting.
+ enum PlaceholderType {
+ PLACEHOLDER_MESSAGE,
+ PLACEHOLDER_ENUM,
+ PLACEHOLDER_EXTENDABLE_MESSAGE
+ };
+ enum ResolveMode {
+ LOOKUP_ALL, LOOKUP_TYPES
+ };
+ Symbol LookupSymbol(const string& name, const string& relative_to,
+ PlaceholderType placeholder_type = PLACEHOLDER_MESSAGE,
+ ResolveMode resolve_mode = LOOKUP_ALL);
+
+ // Like LookupSymbol() but will not return a placeholder even if
+ // AllowUnknownDependencies() has been used.
+ Symbol LookupSymbolNoPlaceholder(const string& name,
+ const string& relative_to,
+ ResolveMode resolve_mode = LOOKUP_ALL);
+
+ // Creates a placeholder type suitable for return from LookupSymbol(). May
+ // return kNullSymbol if the name is not a valid type name.
+ Symbol NewPlaceholder(const string& name, PlaceholderType placeholder_type);
+
+ // Creates a placeholder file. Never returns NULL. This is used when an
+ // import is not found and AllowUnknownDependencies() is enabled.
+ const FileDescriptor* NewPlaceholderFile(const string& name);
+
+ // Calls tables_->AddSymbol() and records an error if it fails. Returns
+ // true if successful or false if failed, though most callers can ignore
+ // the return value since an error has already been recorded.
+ bool AddSymbol(const string& full_name,
+ const void* parent, const string& name,
+ const Message& proto, Symbol symbol);
+
+ // Like AddSymbol(), but succeeds if the symbol is already defined as long
+ // as the existing definition is also a package (because it's OK to define
+ // the same package in two different files). Also adds all parents of the
+ // packgae to the symbol table (e.g. AddPackage("foo.bar", ...) will add
+ // "foo.bar" and "foo" to the table).
+ void AddPackage(const string& name, const Message& proto,
+ const FileDescriptor* file);
+
+ // Checks that the symbol name contains only alphanumeric characters and
+ // underscores. Records an error otherwise.
+ void ValidateSymbolName(const string& name, const string& full_name,
+ const Message& proto);
+
+ // Like ValidateSymbolName(), but the name is allowed to contain periods and
+ // an error is indicated by returning false (not recording the error).
+ bool ValidateQualifiedName(const string& name);
+
+ // Used by BUILD_ARRAY macro (below) to avoid having to have the type
+ // specified as a macro parameter.
+ template <typename Type>
+ inline void AllocateArray(int size, Type** output) {
+ *output = tables_->AllocateArray<Type>(size);
+ }
+
+ // Allocates a copy of orig_options in tables_ and stores it in the
+ // descriptor. Remembers its uninterpreted options, to be interpreted
+ // later. DescriptorT must be one of the Descriptor messages from
+ // descriptor.proto.
+ template<class DescriptorT> void AllocateOptions(
+ const typename DescriptorT::OptionsType& orig_options,
+ DescriptorT* descriptor);
+ // Specialization for FileOptions.
+ void AllocateOptions(const FileOptions& orig_options,
+ FileDescriptor* descriptor);
+
+ // Implementation for AllocateOptions(). Don't call this directly.
+ template<class DescriptorT> void AllocateOptionsImpl(
+ const string& name_scope,
+ const string& element_name,
+ const typename DescriptorT::OptionsType& orig_options,
+ DescriptorT* descriptor);
+
+ // These methods all have the same signature for the sake of the BUILD_ARRAY
+ // macro, below.
+ void BuildMessage(const DescriptorProto& proto,
+ const Descriptor* parent,
+ Descriptor* result);
+ void BuildFieldOrExtension(const FieldDescriptorProto& proto,
+ const Descriptor* parent,
+ FieldDescriptor* result,
+ bool is_extension);
+ void BuildField(const FieldDescriptorProto& proto,
+ const Descriptor* parent,
+ FieldDescriptor* result) {
+ BuildFieldOrExtension(proto, parent, result, false);
+ }
+ void BuildExtension(const FieldDescriptorProto& proto,
+ const Descriptor* parent,
+ FieldDescriptor* result) {
+ BuildFieldOrExtension(proto, parent, result, true);
+ }
+ void BuildExtensionRange(const DescriptorProto::ExtensionRange& proto,
+ const Descriptor* parent,
+ Descriptor::ExtensionRange* result);
+ void BuildOneof(const OneofDescriptorProto& proto,
+ Descriptor* parent,
+ OneofDescriptor* result);
+ void BuildEnum(const EnumDescriptorProto& proto,
+ const Descriptor* parent,
+ EnumDescriptor* result);
+ void BuildEnumValue(const EnumValueDescriptorProto& proto,
+ const EnumDescriptor* parent,
+ EnumValueDescriptor* result);
+ void BuildService(const ServiceDescriptorProto& proto,
+ const void* dummy,
+ ServiceDescriptor* result);
+ void BuildMethod(const MethodDescriptorProto& proto,
+ const ServiceDescriptor* parent,
+ MethodDescriptor* result);
+
+ void LogUnusedDependency(const FileDescriptor* result);
+
+ // Must be run only after building.
+ //
+ // NOTE: Options will not be available during cross-linking, as they
+ // have not yet been interpreted. Defer any handling of options to the
+ // Validate*Options methods.
+ void CrossLinkFile(FileDescriptor* file, const FileDescriptorProto& proto);
+ void CrossLinkMessage(Descriptor* message, const DescriptorProto& proto);
+ void CrossLinkField(FieldDescriptor* field,
+ const FieldDescriptorProto& proto);
+ void CrossLinkEnum(EnumDescriptor* enum_type,
+ const EnumDescriptorProto& proto);
+ void CrossLinkEnumValue(EnumValueDescriptor* enum_value,
+ const EnumValueDescriptorProto& proto);
+ void CrossLinkService(ServiceDescriptor* service,
+ const ServiceDescriptorProto& proto);
+ void CrossLinkMethod(MethodDescriptor* method,
+ const MethodDescriptorProto& proto);
+
+ // Must be run only after cross-linking.
+ void InterpretOptions();
+
+ // A helper class for interpreting options.
+ class OptionInterpreter {
+ public:
+ // Creates an interpreter that operates in the context of the pool of the
+ // specified builder, which must not be NULL. We don't take ownership of the
+ // builder.
+ explicit OptionInterpreter(DescriptorBuilder* builder);
+
+ ~OptionInterpreter();
+
+ // Interprets the uninterpreted options in the specified Options message.
+ // On error, calls AddError() on the underlying builder and returns false.
+ // Otherwise returns true.
+ bool InterpretOptions(OptionsToInterpret* options_to_interpret);
+
+ class AggregateOptionFinder;
+
+ private:
+ // Interprets uninterpreted_option_ on the specified message, which
+ // must be the mutable copy of the original options message to which
+ // uninterpreted_option_ belongs.
+ bool InterpretSingleOption(Message* options);
+
+ // Adds the uninterpreted_option to the given options message verbatim.
+ // Used when AllowUnknownDependencies() is in effect and we can't find
+ // the option's definition.
+ void AddWithoutInterpreting(const UninterpretedOption& uninterpreted_option,
+ Message* options);
+
+ // A recursive helper function that drills into the intermediate fields
+ // in unknown_fields to check if field innermost_field is set on the
+ // innermost message. Returns false and sets an error if so.
+ bool ExamineIfOptionIsSet(
+ vector<const FieldDescriptor*>::const_iterator intermediate_fields_iter,
+ vector<const FieldDescriptor*>::const_iterator intermediate_fields_end,
+ const FieldDescriptor* innermost_field, const string& debug_msg_name,
+ const UnknownFieldSet& unknown_fields);
+
+ // Validates the value for the option field of the currently interpreted
+ // option and then sets it on the unknown_field.
+ bool SetOptionValue(const FieldDescriptor* option_field,
+ UnknownFieldSet* unknown_fields);
+
+ // Parses an aggregate value for a CPPTYPE_MESSAGE option and
+ // saves it into *unknown_fields.
+ bool SetAggregateOption(const FieldDescriptor* option_field,
+ UnknownFieldSet* unknown_fields);
+
+ // Convenience functions to set an int field the right way, depending on
+ // its wire type (a single int CppType can represent multiple wire types).
+ void SetInt32(int number, int32 value, FieldDescriptor::Type type,
+ UnknownFieldSet* unknown_fields);
+ void SetInt64(int number, int64 value, FieldDescriptor::Type type,
+ UnknownFieldSet* unknown_fields);
+ void SetUInt32(int number, uint32 value, FieldDescriptor::Type type,
+ UnknownFieldSet* unknown_fields);
+ void SetUInt64(int number, uint64 value, FieldDescriptor::Type type,
+ UnknownFieldSet* unknown_fields);
+
+ // A helper function that adds an error at the specified location of the
+ // option we're currently interpreting, and returns false.
+ bool AddOptionError(DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& msg) {
+ builder_->AddError(options_to_interpret_->element_name,
+ *uninterpreted_option_, location, msg);
+ return false;
+ }
+
+ // A helper function that adds an error at the location of the option name
+ // and returns false.
+ bool AddNameError(const string& msg) {
+ return AddOptionError(DescriptorPool::ErrorCollector::OPTION_NAME, msg);
+ }
+
+ // A helper function that adds an error at the location of the option name
+ // and returns false.
+ bool AddValueError(const string& msg) {
+ return AddOptionError(DescriptorPool::ErrorCollector::OPTION_VALUE, msg);
+ }
+
+ // We interpret against this builder's pool. Is never NULL. We don't own
+ // this pointer.
+ DescriptorBuilder* builder_;
+
+ // The options we're currently interpreting, or NULL if we're not in a call
+ // to InterpretOptions.
+ const OptionsToInterpret* options_to_interpret_;
+
+ // The option we're currently interpreting within options_to_interpret_, or
+ // NULL if we're not in a call to InterpretOptions(). This points to a
+ // submessage of the original option, not the mutable copy. Therefore we
+ // can use it to find locations recorded by the parser.
+ const UninterpretedOption* uninterpreted_option_;
+
+ // Factory used to create the dynamic messages we need to parse
+ // any aggregate option values we encounter.
+ DynamicMessageFactory dynamic_factory_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(OptionInterpreter);
+ };
+
+ // Work-around for broken compilers: According to the C++ standard,
+ // OptionInterpreter should have access to the private members of any class
+ // which has declared DescriptorBuilder as a friend. Unfortunately some old
+ // versions of GCC and other compilers do not implement this correctly. So,
+ // we have to have these intermediate methods to provide access. We also
+ // redundantly declare OptionInterpreter a friend just to make things extra
+ // clear for these bad compilers.
+ friend class OptionInterpreter;
+ friend class OptionInterpreter::AggregateOptionFinder;
+
+ static inline bool get_allow_unknown(const DescriptorPool* pool) {
+ return pool->allow_unknown_;
+ }
+ static inline bool get_enforce_weak(const DescriptorPool* pool) {
+ return pool->enforce_weak_;
+ }
+ static inline bool get_is_placeholder(const Descriptor* descriptor) {
+ return descriptor->is_placeholder_;
+ }
+ static inline void assert_mutex_held(const DescriptorPool* pool) {
+ if (pool->mutex_ != NULL) {
+ pool->mutex_->AssertHeld();
+ }
+ }
+
+ // Must be run only after options have been interpreted.
+ //
+ // NOTE: Validation code must only reference the options in the mutable
+ // descriptors, which are the ones that have been interpreted. The const
+ // proto references are passed in only so they can be provided to calls to
+ // AddError(). Do not look at their options, which have not been interpreted.
+ void ValidateFileOptions(FileDescriptor* file,
+ const FileDescriptorProto& proto);
+ void ValidateMessageOptions(Descriptor* message,
+ const DescriptorProto& proto);
+ void ValidateFieldOptions(FieldDescriptor* field,
+ const FieldDescriptorProto& proto);
+ void ValidateEnumOptions(EnumDescriptor* enm,
+ const EnumDescriptorProto& proto);
+ void ValidateEnumValueOptions(EnumValueDescriptor* enum_value,
+ const EnumValueDescriptorProto& proto);
+ void ValidateServiceOptions(ServiceDescriptor* service,
+ const ServiceDescriptorProto& proto);
+ void ValidateMethodOptions(MethodDescriptor* method,
+ const MethodDescriptorProto& proto);
+
+ void ValidateMapKey(FieldDescriptor* field,
+ const FieldDescriptorProto& proto);
+
+};
+
+const FileDescriptor* DescriptorPool::BuildFile(
+ const FileDescriptorProto& proto) {
+ GOOGLE_CHECK(fallback_database_ == NULL)
+ << "Cannot call BuildFile on a DescriptorPool that uses a "
+ "DescriptorDatabase. You must instead find a way to get your file "
+ "into the underlying database.";
+ GOOGLE_CHECK(mutex_ == NULL); // Implied by the above GOOGLE_CHECK.
+ tables_->known_bad_symbols_.clear();
+ tables_->known_bad_files_.clear();
+ return DescriptorBuilder(this, tables_.get(), NULL).BuildFile(proto);
+}
+
+const FileDescriptor* DescriptorPool::BuildFileCollectingErrors(
+ const FileDescriptorProto& proto,
+ ErrorCollector* error_collector) {
+ GOOGLE_CHECK(fallback_database_ == NULL)
+ << "Cannot call BuildFile on a DescriptorPool that uses a "
+ "DescriptorDatabase. You must instead find a way to get your file "
+ "into the underlying database.";
+ GOOGLE_CHECK(mutex_ == NULL); // Implied by the above GOOGLE_CHECK.
+ tables_->known_bad_symbols_.clear();
+ tables_->known_bad_files_.clear();
+ return DescriptorBuilder(this, tables_.get(),
+ error_collector).BuildFile(proto);
+}
+
+const FileDescriptor* DescriptorPool::BuildFileFromDatabase(
+ const FileDescriptorProto& proto) const {
+ mutex_->AssertHeld();
+ if (tables_->known_bad_files_.count(proto.name()) > 0) {
+ return NULL;
+ }
+ const FileDescriptor* result =
+ DescriptorBuilder(this, tables_.get(),
+ default_error_collector_).BuildFile(proto);
+ if (result == NULL) {
+ tables_->known_bad_files_.insert(proto.name());
+ }
+ return result;
+}
+
+DescriptorBuilder::DescriptorBuilder(
+ const DescriptorPool* pool,
+ DescriptorPool::Tables* tables,
+ DescriptorPool::ErrorCollector* error_collector)
+ : pool_(pool),
+ tables_(tables),
+ error_collector_(error_collector),
+ had_errors_(false),
+ possible_undeclared_dependency_(NULL),
+ undefine_resolved_name_("") {}
+
+DescriptorBuilder::~DescriptorBuilder() {}
+
+void DescriptorBuilder::AddError(
+ const string& element_name,
+ const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& error) {
+ if (error_collector_ == NULL) {
+ if (!had_errors_) {
+ GOOGLE_LOG(ERROR) << "Invalid proto descriptor for file \"" << filename_
+ << "\":";
+ }
+ GOOGLE_LOG(ERROR) << " " << element_name << ": " << error;
+ } else {
+ error_collector_->AddError(filename_, element_name,
+ &descriptor, location, error);
+ }
+ had_errors_ = true;
+}
+
+void DescriptorBuilder::AddError(
+ const string& element_name,
+ const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const char* error) {
+ AddError(element_name, descriptor, location, string(error));
+}
+
+void DescriptorBuilder::AddNotDefinedError(
+ const string& element_name,
+ const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& undefined_symbol) {
+ if (possible_undeclared_dependency_ == NULL &&
+ undefine_resolved_name_.empty()) {
+ AddError(element_name, descriptor, location,
+ "\"" + undefined_symbol + "\" is not defined.");
+ } else {
+ if (possible_undeclared_dependency_ != NULL) {
+ AddError(element_name, descriptor, location,
+ "\"" + possible_undeclared_dependency_name_ +
+ "\" seems to be defined in \"" +
+ possible_undeclared_dependency_->name() + "\", which is not "
+ "imported by \"" + filename_ + "\". To use it here, please "
+ "add the necessary import.");
+ }
+ if (!undefine_resolved_name_.empty()) {
+ AddError(element_name, descriptor, location,
+ "\"" + undefined_symbol + "\" is resolved to \"" +
+ undefine_resolved_name_ + "\", which is not defined. "
+ "The innermost scope is searched first in name resolution. "
+ "Consider using a leading '.'(i.e., \"."
+ + undefined_symbol +
+ "\") to start from the outermost scope.");
+ }
+ }
+}
+
+void DescriptorBuilder::AddWarning(
+ const string& element_name, const Message& descriptor,
+ DescriptorPool::ErrorCollector::ErrorLocation location,
+ const string& error) {
+ if (error_collector_ == NULL) {
+ GOOGLE_LOG(WARNING) << filename_ << " " << element_name << ": " << error;
+ } else {
+ error_collector_->AddWarning(filename_, element_name, &descriptor, location,
+ error);
+ }
+}
+
+bool DescriptorBuilder::IsInPackage(const FileDescriptor* file,
+ const string& package_name) {
+ return HasPrefixString(file->package(), package_name) &&
+ (file->package().size() == package_name.size() ||
+ file->package()[package_name.size()] == '.');
+}
+
+void DescriptorBuilder::RecordPublicDependencies(const FileDescriptor* file) {
+ if (file == NULL || !dependencies_.insert(file).second) return;
+ for (int i = 0; file != NULL && i < file->public_dependency_count(); i++) {
+ RecordPublicDependencies(file->public_dependency(i));
+ }
+}
+
+Symbol DescriptorBuilder::FindSymbolNotEnforcingDepsHelper(
+ const DescriptorPool* pool, const string& name) {
+ // If we are looking at an underlay, we must lock its mutex_, since we are
+ // accessing the underlay's tables_ directly.
+ MutexLockMaybe lock((pool == pool_) ? NULL : pool->mutex_);
+
+ Symbol result = pool->tables_->FindSymbol(name);
+ if (result.IsNull() && pool->underlay_ != NULL) {
+ // Symbol not found; check the underlay.
+ result = FindSymbolNotEnforcingDepsHelper(pool->underlay_, name);
+ }
+
+ if (result.IsNull()) {
+ // In theory, we shouldn't need to check fallback_database_ because the
+ // symbol should be in one of its file's direct dependencies, and we have
+ // already loaded those by the time we get here. But we check anyway so
+ // that we can generate better error message when dependencies are missing
+ // (i.e., "missing dependency" rather than "type is not defined").
+ if (pool->TryFindSymbolInFallbackDatabase(name)) {
+ result = pool->tables_->FindSymbol(name);
+ }
+ }
+
+ return result;
+}
+
+Symbol DescriptorBuilder::FindSymbolNotEnforcingDeps(const string& name) {
+ return FindSymbolNotEnforcingDepsHelper(pool_, name);
+}
+
+Symbol DescriptorBuilder::FindSymbol(const string& name) {
+ Symbol result = FindSymbolNotEnforcingDeps(name);
+
+ if (result.IsNull()) return result;
+
+ if (!pool_->enforce_dependencies_) {
+ // Hack for CompilerUpgrader.
+ return result;
+ }
+
+ // Only find symbols which were defined in this file or one of its
+ // dependencies.
+ const FileDescriptor* file = result.GetFile();
+ if (file == file_ || dependencies_.count(file) > 0) {
+ unused_dependency_.erase(file);
+ return result;
+ }
+
+ if (result.type == Symbol::PACKAGE) {
+ // Arg, this is overcomplicated. The symbol is a package name. It could
+ // be that the package was defined in multiple files. result.GetFile()
+ // returns the first file we saw that used this package. We've determined
+ // that that file is not a direct dependency of the file we are currently
+ // building, but it could be that some other file which *is* a direct
+ // dependency also defines the same package. We can't really rule out this
+ // symbol unless none of the dependencies define it.
+ if (IsInPackage(file_, name)) return result;
+ for (set<const FileDescriptor*>::const_iterator it = dependencies_.begin();
+ it != dependencies_.end(); ++it) {
+ // Note: A dependency may be NULL if it was not found or had errors.
+ if (*it != NULL && IsInPackage(*it, name)) return result;
+ }
+ }
+
+ possible_undeclared_dependency_ = file;
+ possible_undeclared_dependency_name_ = name;
+ return kNullSymbol;
+}
+
+Symbol DescriptorBuilder::LookupSymbolNoPlaceholder(
+ const string& name, const string& relative_to, ResolveMode resolve_mode) {
+ possible_undeclared_dependency_ = NULL;
+ undefine_resolved_name_.clear();
+
+ if (name.size() > 0 && name[0] == '.') {
+ // Fully-qualified name.
+ return FindSymbol(name.substr(1));
+ }
+
+ // If name is something like "Foo.Bar.baz", and symbols named "Foo" are
+ // defined in multiple parent scopes, we only want to find "Bar.baz" in the
+ // innermost one. E.g., the following should produce an error:
+ // message Bar { message Baz {} }
+ // message Foo {
+ // message Bar {
+ // }
+ // optional Bar.Baz baz = 1;
+ // }
+ // So, we look for just "Foo" first, then look for "Bar.baz" within it if
+ // found.
+ string::size_type name_dot_pos = name.find_first_of('.');
+ string first_part_of_name;
+ if (name_dot_pos == string::npos) {
+ first_part_of_name = name;
+ } else {
+ first_part_of_name = name.substr(0, name_dot_pos);
+ }
+
+ string scope_to_try(relative_to);
+
+ while (true) {
+ // Chop off the last component of the scope.
+ string::size_type dot_pos = scope_to_try.find_last_of('.');
+ if (dot_pos == string::npos) {
+ return FindSymbol(name);
+ } else {
+ scope_to_try.erase(dot_pos);
+ }
+
+ // Append ".first_part_of_name" and try to find.
+ string::size_type old_size = scope_to_try.size();
+ scope_to_try.append(1, '.');
+ scope_to_try.append(first_part_of_name);
+ Symbol result = FindSymbol(scope_to_try);
+ if (!result.IsNull()) {
+ if (first_part_of_name.size() < name.size()) {
+ // name is a compound symbol, of which we only found the first part.
+ // Now try to look up the rest of it.
+ if (result.IsAggregate()) {
+ scope_to_try.append(name, first_part_of_name.size(),
+ name.size() - first_part_of_name.size());
+ result = FindSymbol(scope_to_try);
+ if (result.IsNull()) {
+ undefine_resolved_name_ = scope_to_try;
+ }
+ return result;
+ } else {
+ // We found a symbol but it's not an aggregate. Continue the loop.
+ }
+ } else {
+ if (resolve_mode == LOOKUP_TYPES && !result.IsType()) {
+ // We found a symbol but it's not a type. Continue the loop.
+ } else {
+ return result;
+ }
+ }
+ }
+
+ // Not found. Remove the name so we can try again.
+ scope_to_try.erase(old_size);
+ }
+}
+
+Symbol DescriptorBuilder::LookupSymbol(
+ const string& name, const string& relative_to,
+ PlaceholderType placeholder_type, ResolveMode resolve_mode) {
+ Symbol result = LookupSymbolNoPlaceholder(
+ name, relative_to, resolve_mode);
+ if (result.IsNull() && pool_->allow_unknown_) {
+ // Not found, but AllowUnknownDependencies() is enabled. Return a
+ // placeholder instead.
+ result = NewPlaceholder(name, placeholder_type);
+ }
+ return result;
+}
+
+Symbol DescriptorBuilder::NewPlaceholder(const string& name,
+ PlaceholderType placeholder_type) {
+ // Compute names.
+ const string* placeholder_full_name;
+ const string* placeholder_name;
+ const string* placeholder_package;
+
+ if (!ValidateQualifiedName(name)) return kNullSymbol;
+ if (name[0] == '.') {
+ // Fully-qualified.
+ placeholder_full_name = tables_->AllocateString(name.substr(1));
+ } else {
+ placeholder_full_name = tables_->AllocateString(name);
+ }
+
+ string::size_type dotpos = placeholder_full_name->find_last_of('.');
+ if (dotpos != string::npos) {
+ placeholder_package = tables_->AllocateString(
+ placeholder_full_name->substr(0, dotpos));
+ placeholder_name = tables_->AllocateString(
+ placeholder_full_name->substr(dotpos + 1));
+ } else {
+ placeholder_package = &internal::GetEmptyString();
+ placeholder_name = placeholder_full_name;
+ }
+
+ // Create the placeholders.
+ FileDescriptor* placeholder_file = tables_->Allocate<FileDescriptor>();
+ memset(placeholder_file, 0, sizeof(*placeholder_file));
+
+ placeholder_file->source_code_info_ = &SourceCodeInfo::default_instance();
+
+ placeholder_file->name_ =
+ tables_->AllocateString(*placeholder_full_name + ".placeholder.proto");
+ placeholder_file->package_ = placeholder_package;
+ placeholder_file->pool_ = pool_;
+ placeholder_file->options_ = &FileOptions::default_instance();
+ placeholder_file->tables_ = &FileDescriptorTables::kEmpty;
+ placeholder_file->is_placeholder_ = true;
+ // All other fields are zero or NULL.
+
+ if (placeholder_type == PLACEHOLDER_ENUM) {
+ placeholder_file->enum_type_count_ = 1;
+ placeholder_file->enum_types_ =
+ tables_->AllocateArray<EnumDescriptor>(1);
+
+ EnumDescriptor* placeholder_enum = &placeholder_file->enum_types_[0];
+ memset(placeholder_enum, 0, sizeof(*placeholder_enum));
+
+ placeholder_enum->full_name_ = placeholder_full_name;
+ placeholder_enum->name_ = placeholder_name;
+ placeholder_enum->file_ = placeholder_file;
+ placeholder_enum->options_ = &EnumOptions::default_instance();
+ placeholder_enum->is_placeholder_ = true;
+ placeholder_enum->is_unqualified_placeholder_ = (name[0] != '.');
+
+ // Enums must have at least one value.
+ placeholder_enum->value_count_ = 1;
+ placeholder_enum->values_ = tables_->AllocateArray<EnumValueDescriptor>(1);
+
+ EnumValueDescriptor* placeholder_value = &placeholder_enum->values_[0];
+ memset(placeholder_value, 0, sizeof(*placeholder_value));
+
+ placeholder_value->name_ = tables_->AllocateString("PLACEHOLDER_VALUE");
+ // Note that enum value names are siblings of their type, not children.
+ placeholder_value->full_name_ =
+ placeholder_package->empty() ? placeholder_value->name_ :
+ tables_->AllocateString(*placeholder_package + ".PLACEHOLDER_VALUE");
+
+ placeholder_value->number_ = 0;
+ placeholder_value->type_ = placeholder_enum;
+ placeholder_value->options_ = &EnumValueOptions::default_instance();
+
+ return Symbol(placeholder_enum);
+ } else {
+ placeholder_file->message_type_count_ = 1;
+ placeholder_file->message_types_ =
+ tables_->AllocateArray<Descriptor>(1);
+
+ Descriptor* placeholder_message = &placeholder_file->message_types_[0];
+ memset(placeholder_message, 0, sizeof(*placeholder_message));
+
+ placeholder_message->full_name_ = placeholder_full_name;
+ placeholder_message->name_ = placeholder_name;
+ placeholder_message->file_ = placeholder_file;
+ placeholder_message->options_ = &MessageOptions::default_instance();
+ placeholder_message->is_placeholder_ = true;
+ placeholder_message->is_unqualified_placeholder_ = (name[0] != '.');
+
+ if (placeholder_type == PLACEHOLDER_EXTENDABLE_MESSAGE) {
+ placeholder_message->extension_range_count_ = 1;
+ placeholder_message->extension_ranges_ =
+ tables_->AllocateArray<Descriptor::ExtensionRange>(1);
+ placeholder_message->extension_ranges_->start = 1;
+ // kMaxNumber + 1 because ExtensionRange::end is exclusive.
+ placeholder_message->extension_ranges_->end =
+ FieldDescriptor::kMaxNumber + 1;
+ }
+
+ return Symbol(placeholder_message);
+ }
+}
+
+const FileDescriptor* DescriptorBuilder::NewPlaceholderFile(
+ const string& name) {
+ FileDescriptor* placeholder = tables_->Allocate<FileDescriptor>();
+ memset(placeholder, 0, sizeof(*placeholder));
+
+ placeholder->name_ = tables_->AllocateString(name);
+ placeholder->package_ = &internal::GetEmptyString();
+ placeholder->pool_ = pool_;
+ placeholder->options_ = &FileOptions::default_instance();
+ placeholder->tables_ = &FileDescriptorTables::kEmpty;
+ placeholder->is_placeholder_ = true;
+ // All other fields are zero or NULL.
+
+ return placeholder;
+}
+
+bool DescriptorBuilder::AddSymbol(
+ const string& full_name, const void* parent, const string& name,
+ const Message& proto, Symbol symbol) {
+ // If the caller passed NULL for the parent, the symbol is at file scope.
+ // Use its file as the parent instead.
+ if (parent == NULL) parent = file_;
+
+ if (tables_->AddSymbol(full_name, symbol)) {
+ if (!file_tables_->AddAliasUnderParent(parent, name, symbol)) {
+ GOOGLE_LOG(DFATAL) << "\"" << full_name << "\" not previously defined in "
+ "symbols_by_name_, but was defined in symbols_by_parent_; "
+ "this shouldn't be possible.";
+ return false;
+ }
+ return true;
+ } else {
+ const FileDescriptor* other_file = tables_->FindSymbol(full_name).GetFile();
+ if (other_file == file_) {
+ string::size_type dot_pos = full_name.find_last_of('.');
+ if (dot_pos == string::npos) {
+ AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
+ "\"" + full_name + "\" is already defined.");
+ } else {
+ AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
+ "\"" + full_name.substr(dot_pos + 1) +
+ "\" is already defined in \"" +
+ full_name.substr(0, dot_pos) + "\".");
+ }
+ } else {
+ // Symbol seems to have been defined in a different file.
+ AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
+ "\"" + full_name + "\" is already defined in file \"" +
+ other_file->name() + "\".");
+ }
+ return false;
+ }
+}
+
+void DescriptorBuilder::AddPackage(
+ const string& name, const Message& proto, const FileDescriptor* file) {
+ if (tables_->AddSymbol(name, Symbol(file))) {
+ // Success. Also add parent package, if any.
+ string::size_type dot_pos = name.find_last_of('.');
+ if (dot_pos == string::npos) {
+ // No parents.
+ ValidateSymbolName(name, name, proto);
+ } else {
+ // Has parent.
+ string* parent_name = tables_->AllocateString(name.substr(0, dot_pos));
+ AddPackage(*parent_name, proto, file);
+ ValidateSymbolName(name.substr(dot_pos + 1), name, proto);
+ }
+ } else {
+ Symbol existing_symbol = tables_->FindSymbol(name);
+ // It's OK to redefine a package.
+ if (existing_symbol.type != Symbol::PACKAGE) {
+ // Symbol seems to have been defined in a different file.
+ AddError(name, proto, DescriptorPool::ErrorCollector::NAME,
+ "\"" + name + "\" is already defined (as something other than "
+ "a package) in file \"" + existing_symbol.GetFile()->name() +
+ "\".");
+ }
+ }
+}
+
+void DescriptorBuilder::ValidateSymbolName(
+ const string& name, const string& full_name, const Message& proto) {
+ if (name.empty()) {
+ AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
+ "Missing name.");
+ } else {
+ for (int i = 0; i < name.size(); i++) {
+ // I don't trust isalnum() due to locales. :(
+ if ((name[i] < 'a' || 'z' < name[i]) &&
+ (name[i] < 'A' || 'Z' < name[i]) &&
+ (name[i] < '0' || '9' < name[i]) &&
+ (name[i] != '_')) {
+ AddError(full_name, proto, DescriptorPool::ErrorCollector::NAME,
+ "\"" + name + "\" is not a valid identifier.");
+ }
+ }
+ }
+}
+
+bool DescriptorBuilder::ValidateQualifiedName(const string& name) {
+ bool last_was_period = false;
+
+ for (int i = 0; i < name.size(); i++) {
+ // I don't trust isalnum() due to locales. :(
+ if (('a' <= name[i] && name[i] <= 'z') ||
+ ('A' <= name[i] && name[i] <= 'Z') ||
+ ('0' <= name[i] && name[i] <= '9') ||
+ (name[i] == '_')) {
+ last_was_period = false;
+ } else if (name[i] == '.') {
+ if (last_was_period) return false;
+ last_was_period = true;
+ } else {
+ return false;
+ }
+ }
+
+ return !name.empty() && !last_was_period;
+}
+
+// -------------------------------------------------------------------
+
+// This generic implementation is good for all descriptors except
+// FileDescriptor.
+template<class DescriptorT> void DescriptorBuilder::AllocateOptions(
+ const typename DescriptorT::OptionsType& orig_options,
+ DescriptorT* descriptor) {
+ AllocateOptionsImpl(descriptor->full_name(), descriptor->full_name(),
+ orig_options, descriptor);
+}
+
+// We specialize for FileDescriptor.
+void DescriptorBuilder::AllocateOptions(const FileOptions& orig_options,
+ FileDescriptor* descriptor) {
+ // We add the dummy token so that LookupSymbol does the right thing.
+ AllocateOptionsImpl(descriptor->package() + ".dummy", descriptor->name(),
+ orig_options, descriptor);
+}
+
+template<class DescriptorT> void DescriptorBuilder::AllocateOptionsImpl(
+ const string& name_scope,
+ const string& element_name,
+ const typename DescriptorT::OptionsType& orig_options,
+ DescriptorT* descriptor) {
+ // We need to use a dummy pointer to work around a bug in older versions of
+ // GCC. Otherwise, the following two lines could be replaced with:
+ // typename DescriptorT::OptionsType* options =
+ // tables_->AllocateMessage<typename DescriptorT::OptionsType>();
+ typename DescriptorT::OptionsType* const dummy = NULL;
+ typename DescriptorT::OptionsType* options = tables_->AllocateMessage(dummy);
+ // Avoid using MergeFrom()/CopyFrom() in this class to make it -fno-rtti
+ // friendly. Without RTTI, MergeFrom() and CopyFrom() will fallback to the
+ // reflection based method, which requires the Descriptor. However, we are in
+ // the middle of building the descriptors, thus the deadlock.
+ options->ParseFromString(orig_options.SerializeAsString());
+ descriptor->options_ = options;
+
+ // Don't add to options_to_interpret_ unless there were uninterpreted
+ // options. This not only avoids unnecessary work, but prevents a
+ // bootstrapping problem when building descriptors for descriptor.proto.
+ // descriptor.proto does not contain any uninterpreted options, but
+ // attempting to interpret options anyway will cause
+ // OptionsType::GetDescriptor() to be called which may then deadlock since
+ // we're still trying to build it.
+ if (options->uninterpreted_option_size() > 0) {
+ options_to_interpret_.push_back(
+ OptionsToInterpret(name_scope, element_name, &orig_options, options));
+ }
+}
+
+
+// A common pattern: We want to convert a repeated field in the descriptor
+// to an array of values, calling some method to build each value.
+#define BUILD_ARRAY(INPUT, OUTPUT, NAME, METHOD, PARENT) \
+ OUTPUT->NAME##_count_ = INPUT.NAME##_size(); \
+ AllocateArray(INPUT.NAME##_size(), &OUTPUT->NAME##s_); \
+ for (int i = 0; i < INPUT.NAME##_size(); i++) { \
+ METHOD(INPUT.NAME(i), PARENT, OUTPUT->NAME##s_ + i); \
+ }
+
+void DescriptorBuilder::AddRecursiveImportError(
+ const FileDescriptorProto& proto, int from_here) {
+ string error_message("File recursively imports itself: ");
+ for (int i = from_here; i < tables_->pending_files_.size(); i++) {
+ error_message.append(tables_->pending_files_[i]);
+ error_message.append(" -> ");
+ }
+ error_message.append(proto.name());
+
+ AddError(proto.name(), proto, DescriptorPool::ErrorCollector::OTHER,
+ error_message);
+}
+
+void DescriptorBuilder::AddTwiceListedError(const FileDescriptorProto& proto,
+ int index) {
+ AddError(proto.name(), proto, DescriptorPool::ErrorCollector::OTHER,
+ "Import \"" + proto.dependency(index) + "\" was listed twice.");
+}
+
+void DescriptorBuilder::AddImportError(const FileDescriptorProto& proto,
+ int index) {
+ string message;
+ if (pool_->fallback_database_ == NULL) {
+ message = "Import \"" + proto.dependency(index) +
+ "\" has not been loaded.";
+ } else {
+ message = "Import \"" + proto.dependency(index) +
+ "\" was not found or had errors.";
+ }
+ AddError(proto.name(), proto, DescriptorPool::ErrorCollector::OTHER, message);
+}
+
+static bool ExistingFileMatchesProto(const FileDescriptor* existing_file,
+ const FileDescriptorProto& proto) {
+ FileDescriptorProto existing_proto;
+ existing_file->CopyTo(&existing_proto);
+ return existing_proto.SerializeAsString() == proto.SerializeAsString();
+}
+
+const FileDescriptor* DescriptorBuilder::BuildFile(
+ const FileDescriptorProto& proto) {
+ filename_ = proto.name();
+
+ // Check if the file already exists and is identical to the one being built.
+ // Note: This only works if the input is canonical -- that is, it
+ // fully-qualifies all type names, has no UninterpretedOptions, etc.
+ // This is fine, because this idempotency "feature" really only exists to
+ // accomodate one hack in the proto1->proto2 migration layer.
+ const FileDescriptor* existing_file = tables_->FindFile(filename_);
+ if (existing_file != NULL) {
+ // File already in pool. Compare the existing one to the input.
+ if (ExistingFileMatchesProto(existing_file, proto)) {
+ // They're identical. Return the existing descriptor.
+ return existing_file;
+ }
+
+ // Not a match. The error will be detected and handled later.
+ }
+
+ // Check to see if this file is already on the pending files list.
+ // TODO(kenton): Allow recursive imports? It may not work with some
+ // (most?) programming languages. E.g., in C++, a forward declaration
+ // of a type is not sufficient to allow it to be used even in a
+ // generated header file due to inlining. This could perhaps be
+ // worked around using tricks involving inserting #include statements
+ // mid-file, but that's pretty ugly, and I'm pretty sure there are
+ // some languages out there that do not allow recursive dependencies
+ // at all.
+ for (int i = 0; i < tables_->pending_files_.size(); i++) {
+ if (tables_->pending_files_[i] == proto.name()) {
+ AddRecursiveImportError(proto, i);
+ return NULL;
+ }
+ }
+
+ // If we have a fallback_database_, attempt to load all dependencies now,
+ // before checkpointing tables_. This avoids confusion with recursive
+ // checkpoints.
+ if (pool_->fallback_database_ != NULL) {
+ tables_->pending_files_.push_back(proto.name());
+ for (int i = 0; i < proto.dependency_size(); i++) {
+ if (tables_->FindFile(proto.dependency(i)) == NULL &&
+ (pool_->underlay_ == NULL ||
+ pool_->underlay_->FindFileByName(proto.dependency(i)) == NULL)) {
+ // We don't care what this returns since we'll find out below anyway.
+ pool_->TryFindFileInFallbackDatabase(proto.dependency(i));
+ }
+ }
+ tables_->pending_files_.pop_back();
+ }
+
+ // Checkpoint the tables so that we can roll back if something goes wrong.
+ tables_->AddCheckpoint();
+
+ FileDescriptor* result = tables_->Allocate<FileDescriptor>();
+ file_ = result;
+
+ result->is_placeholder_ = false;
+ if (proto.has_source_code_info()) {
+ SourceCodeInfo *info = tables_->AllocateMessage<SourceCodeInfo>();
+ info->CopyFrom(proto.source_code_info());
+ result->source_code_info_ = info;
+ } else {
+ result->source_code_info_ = &SourceCodeInfo::default_instance();
+ }
+
+ file_tables_ = tables_->AllocateFileTables();
+ file_->tables_ = file_tables_;
+
+ if (!proto.has_name()) {
+ AddError("", proto, DescriptorPool::ErrorCollector::OTHER,
+ "Missing field: FileDescriptorProto.name.");
+ }
+
+ result->name_ = tables_->AllocateString(proto.name());
+ if (proto.has_package()) {
+ result->package_ = tables_->AllocateString(proto.package());
+ } else {
+ // We cannot rely on proto.package() returning a valid string if
+ // proto.has_package() is false, because we might be running at static
+ // initialization time, in which case default values have not yet been
+ // initialized.
+ result->package_ = tables_->AllocateString("");
+ }
+ result->pool_ = pool_;
+
+ // Add to tables.
+ if (!tables_->AddFile(result)) {
+ AddError(proto.name(), proto, DescriptorPool::ErrorCollector::OTHER,
+ "A file with this name is already in the pool.");
+ // Bail out early so that if this is actually the exact same file, we
+ // don't end up reporting that every single symbol is already defined.
+ tables_->RollbackToLastCheckpoint();
+ return NULL;
+ }
+ if (!result->package().empty()) {
+ AddPackage(result->package(), proto, result);
+ }
+
+ // Make sure all dependencies are loaded.
+ set<string> seen_dependencies;
+ result->dependency_count_ = proto.dependency_size();
+ result->dependencies_ =
+ tables_->AllocateArray<const FileDescriptor*>(proto.dependency_size());
+ unused_dependency_.clear();
+ set<int> weak_deps;
+ for (int i = 0; i < proto.weak_dependency_size(); ++i) {
+ weak_deps.insert(proto.weak_dependency(i));
+ }
+ for (int i = 0; i < proto.dependency_size(); i++) {
+ if (!seen_dependencies.insert(proto.dependency(i)).second) {
+ AddTwiceListedError(proto, i);
+ }
+
+ const FileDescriptor* dependency = tables_->FindFile(proto.dependency(i));
+ if (dependency == NULL && pool_->underlay_ != NULL) {
+ dependency = pool_->underlay_->FindFileByName(proto.dependency(i));
+ }
+
+ if (dependency == NULL) {
+ if (pool_->allow_unknown_ ||
+ (!pool_->enforce_weak_ && weak_deps.find(i) != weak_deps.end())) {
+ dependency = NewPlaceholderFile(proto.dependency(i));
+ } else {
+ AddImportError(proto, i);
+ }
+ } else {
+ // Add to unused_dependency_ to track unused imported files.
+ // Note: do not track unused imported files for public import.
+ if (pool_->enforce_dependencies_ &&
+ (pool_->unused_import_track_files_.find(proto.name()) !=
+ pool_->unused_import_track_files_.end()) &&
+ (dependency->public_dependency_count() == 0)) {
+ unused_dependency_.insert(dependency);
+ }
+ }
+
+ result->dependencies_[i] = dependency;
+ }
+
+ // Check public dependencies.
+ int public_dependency_count = 0;
+ result->public_dependencies_ = tables_->AllocateArray<int>(
+ proto.public_dependency_size());
+ for (int i = 0; i < proto.public_dependency_size(); i++) {
+ // Only put valid public dependency indexes.
+ int index = proto.public_dependency(i);
+ if (index >= 0 && index < proto.dependency_size()) {
+ result->public_dependencies_[public_dependency_count++] = index;
+ // Do not track unused imported files for public import.
+ unused_dependency_.erase(result->dependency(index));
+ } else {
+ AddError(proto.name(), proto,
+ DescriptorPool::ErrorCollector::OTHER,
+ "Invalid public dependency index.");
+ }
+ }
+ result->public_dependency_count_ = public_dependency_count;
+
+ // Build dependency set
+ dependencies_.clear();
+ for (int i = 0; i < result->dependency_count(); i++) {
+ RecordPublicDependencies(result->dependency(i));
+ }
+
+ // Check weak dependencies.
+ int weak_dependency_count = 0;
+ result->weak_dependencies_ = tables_->AllocateArray<int>(
+ proto.weak_dependency_size());
+ for (int i = 0; i < proto.weak_dependency_size(); i++) {
+ int index = proto.weak_dependency(i);
+ if (index >= 0 && index < proto.dependency_size()) {
+ result->weak_dependencies_[weak_dependency_count++] = index;
+ } else {
+ AddError(proto.name(), proto,
+ DescriptorPool::ErrorCollector::OTHER,
+ "Invalid weak dependency index.");
+ }
+ }
+ result->weak_dependency_count_ = weak_dependency_count;
+
+ // Convert children.
+ BUILD_ARRAY(proto, result, message_type, BuildMessage , NULL);
+ BUILD_ARRAY(proto, result, enum_type , BuildEnum , NULL);
+ BUILD_ARRAY(proto, result, service , BuildService , NULL);
+ BUILD_ARRAY(proto, result, extension , BuildExtension, NULL);
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ // Note that the following steps must occur in exactly the specified order.
+
+ // Cross-link.
+ CrossLinkFile(result, proto);
+
+ // Interpret any remaining uninterpreted options gathered into
+ // options_to_interpret_ during descriptor building. Cross-linking has made
+ // extension options known, so all interpretations should now succeed.
+ if (!had_errors_) {
+ OptionInterpreter option_interpreter(this);
+ for (vector<OptionsToInterpret>::iterator iter =
+ options_to_interpret_.begin();
+ iter != options_to_interpret_.end(); ++iter) {
+ option_interpreter.InterpretOptions(&(*iter));
+ }
+ options_to_interpret_.clear();
+ }
+
+ // Validate options.
+ if (!had_errors_) {
+ ValidateFileOptions(result, proto);
+ }
+
+
+ if (!unused_dependency_.empty()) {
+ LogUnusedDependency(result);
+ }
+
+ if (had_errors_) {
+ tables_->RollbackToLastCheckpoint();
+ return NULL;
+ } else {
+ tables_->ClearLastCheckpoint();
+ return result;
+ }
+}
+
+void DescriptorBuilder::BuildMessage(const DescriptorProto& proto,
+ const Descriptor* parent,
+ Descriptor* result) {
+ const string& scope = (parent == NULL) ?
+ file_->package() : parent->full_name();
+ string* full_name = tables_->AllocateString(scope);
+ if (!full_name->empty()) full_name->append(1, '.');
+ full_name->append(proto.name());
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ result->name_ = tables_->AllocateString(proto.name());
+ result->full_name_ = full_name;
+ result->file_ = file_;
+ result->containing_type_ = parent;
+ result->is_placeholder_ = false;
+ result->is_unqualified_placeholder_ = false;
+
+ // Build oneofs first so that fields and extension ranges can refer to them.
+ BUILD_ARRAY(proto, result, oneof_decl , BuildOneof , result);
+ BUILD_ARRAY(proto, result, field , BuildField , result);
+ BUILD_ARRAY(proto, result, nested_type , BuildMessage , result);
+ BUILD_ARRAY(proto, result, enum_type , BuildEnum , result);
+ BUILD_ARRAY(proto, result, extension_range, BuildExtensionRange, result);
+ BUILD_ARRAY(proto, result, extension , BuildExtension , result);
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ AddSymbol(result->full_name(), parent, result->name(),
+ proto, Symbol(result));
+
+ // Check that no fields have numbers in extension ranges.
+ for (int i = 0; i < result->field_count(); i++) {
+ const FieldDescriptor* field = result->field(i);
+ for (int j = 0; j < result->extension_range_count(); j++) {
+ const Descriptor::ExtensionRange* range = result->extension_range(j);
+ if (range->start <= field->number() && field->number() < range->end) {
+ AddError(field->full_name(), proto.extension_range(j),
+ DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute(
+ "Extension range $0 to $1 includes field \"$2\" ($3).",
+ range->start, range->end - 1,
+ field->name(), field->number()));
+ }
+ }
+ }
+
+ // Check that extension ranges don't overlap.
+ for (int i = 0; i < result->extension_range_count(); i++) {
+ const Descriptor::ExtensionRange* range1 = result->extension_range(i);
+ for (int j = i + 1; j < result->extension_range_count(); j++) {
+ const Descriptor::ExtensionRange* range2 = result->extension_range(j);
+ if (range1->end > range2->start && range2->end > range1->start) {
+ AddError(result->full_name(), proto.extension_range(j),
+ DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute("Extension range $0 to $1 overlaps with "
+ "already-defined range $2 to $3.",
+ range2->start, range2->end - 1,
+ range1->start, range1->end - 1));
+ }
+ }
+ }
+}
+
+void DescriptorBuilder::BuildFieldOrExtension(const FieldDescriptorProto& proto,
+ const Descriptor* parent,
+ FieldDescriptor* result,
+ bool is_extension) {
+ const string& scope = (parent == NULL) ?
+ file_->package() : parent->full_name();
+ string* full_name = tables_->AllocateString(scope);
+ if (!full_name->empty()) full_name->append(1, '.');
+ full_name->append(proto.name());
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ result->name_ = tables_->AllocateString(proto.name());
+ result->full_name_ = full_name;
+ result->file_ = file_;
+ result->number_ = proto.number();
+ result->is_extension_ = is_extension;
+
+ // If .proto files follow the style guide then the name should already be
+ // lower-cased. If that's the case we can just reuse the string we already
+ // allocated rather than allocate a new one.
+ string lowercase_name(proto.name());
+ LowerString(&lowercase_name);
+ if (lowercase_name == proto.name()) {
+ result->lowercase_name_ = result->name_;
+ } else {
+ result->lowercase_name_ = tables_->AllocateString(lowercase_name);
+ }
+
+ // Don't bother with the above optimization for camel-case names since
+ // .proto files that follow the guide shouldn't be using names in this
+ // format, so the optimization wouldn't help much.
+ result->camelcase_name_ = tables_->AllocateString(ToCamelCase(proto.name()));
+
+ // Some compilers do not allow static_cast directly between two enum types,
+ // so we must cast to int first.
+ result->type_ = static_cast<FieldDescriptor::Type>(
+ implicit_cast<int>(proto.type()));
+ result->label_ = static_cast<FieldDescriptor::Label>(
+ implicit_cast<int>(proto.label()));
+
+ // An extension cannot have a required field (b/13365836).
+ if (result->is_extension_ &&
+ result->label_ == FieldDescriptor::LABEL_REQUIRED) {
+ AddError(result->full_name(), proto,
+ // Error location `TYPE`: we would really like to indicate
+ // `LABEL`, but the `ErrorLocation` enum has no entry for this, and
+ // we don't necessarily know about all implementations of the
+ // `ErrorCollector` interface to extend them to handle the new
+ // error location type properly.
+ DescriptorPool::ErrorCollector::TYPE,
+ "Message extensions cannot have required fields.");
+ }
+
+ // Some of these may be filled in when cross-linking.
+ result->containing_type_ = NULL;
+ result->extension_scope_ = NULL;
+ result->experimental_map_key_ = NULL;
+ result->message_type_ = NULL;
+ result->enum_type_ = NULL;
+
+ result->has_default_value_ = proto.has_default_value();
+ if (proto.has_default_value() && result->is_repeated()) {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Repeated fields can't have default values.");
+ }
+
+ if (proto.has_type()) {
+ if (proto.has_default_value()) {
+ char* end_pos = NULL;
+ switch (result->cpp_type()) {
+ case FieldDescriptor::CPPTYPE_INT32:
+ result->default_value_int32_ =
+ strtol(proto.default_value().c_str(), &end_pos, 0);
+ break;
+ case FieldDescriptor::CPPTYPE_INT64:
+ result->default_value_int64_ =
+ strto64(proto.default_value().c_str(), &end_pos, 0);
+ break;
+ case FieldDescriptor::CPPTYPE_UINT32:
+ result->default_value_uint32_ =
+ strtoul(proto.default_value().c_str(), &end_pos, 0);
+ break;
+ case FieldDescriptor::CPPTYPE_UINT64:
+ result->default_value_uint64_ =
+ strtou64(proto.default_value().c_str(), &end_pos, 0);
+ break;
+ case FieldDescriptor::CPPTYPE_FLOAT:
+ if (proto.default_value() == "inf") {
+ result->default_value_float_ = numeric_limits<float>::infinity();
+ } else if (proto.default_value() == "-inf") {
+ result->default_value_float_ = -numeric_limits<float>::infinity();
+ } else if (proto.default_value() == "nan") {
+ result->default_value_float_ = numeric_limits<float>::quiet_NaN();
+ } else {
+ result->default_value_float_ =
+ io::NoLocaleStrtod(proto.default_value().c_str(), &end_pos);
+ }
+ break;
+ case FieldDescriptor::CPPTYPE_DOUBLE:
+ if (proto.default_value() == "inf") {
+ result->default_value_double_ = numeric_limits<double>::infinity();
+ } else if (proto.default_value() == "-inf") {
+ result->default_value_double_ = -numeric_limits<double>::infinity();
+ } else if (proto.default_value() == "nan") {
+ result->default_value_double_ = numeric_limits<double>::quiet_NaN();
+ } else {
+ result->default_value_double_ =
+ io::NoLocaleStrtod(proto.default_value().c_str(), &end_pos);
+ }
+ break;
+ case FieldDescriptor::CPPTYPE_BOOL:
+ if (proto.default_value() == "true") {
+ result->default_value_bool_ = true;
+ } else if (proto.default_value() == "false") {
+ result->default_value_bool_ = false;
+ } else {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Boolean default must be true or false.");
+ }
+ break;
+ case FieldDescriptor::CPPTYPE_ENUM:
+ // This will be filled in when cross-linking.
+ result->default_value_enum_ = NULL;
+ break;
+ case FieldDescriptor::CPPTYPE_STRING:
+ if (result->type() == FieldDescriptor::TYPE_BYTES) {
+ result->default_value_string_ = tables_->AllocateString(
+ UnescapeCEscapeString(proto.default_value()));
+ } else {
+ result->default_value_string_ =
+ tables_->AllocateString(proto.default_value());
+ }
+ break;
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Messages can't have default values.");
+ result->has_default_value_ = false;
+ break;
+ }
+
+ if (end_pos != NULL) {
+ // end_pos is only set non-NULL by the parsers for numeric types, above.
+ // This checks that the default was non-empty and had no extra junk
+ // after the end of the number.
+ if (proto.default_value().empty() || *end_pos != '\0') {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Couldn't parse default value \"" + proto.default_value() +
+ "\".");
+ }
+ }
+ } else {
+ // No explicit default value
+ switch (result->cpp_type()) {
+ case FieldDescriptor::CPPTYPE_INT32:
+ result->default_value_int32_ = 0;
+ break;
+ case FieldDescriptor::CPPTYPE_INT64:
+ result->default_value_int64_ = 0;
+ break;
+ case FieldDescriptor::CPPTYPE_UINT32:
+ result->default_value_uint32_ = 0;
+ break;
+ case FieldDescriptor::CPPTYPE_UINT64:
+ result->default_value_uint64_ = 0;
+ break;
+ case FieldDescriptor::CPPTYPE_FLOAT:
+ result->default_value_float_ = 0.0f;
+ break;
+ case FieldDescriptor::CPPTYPE_DOUBLE:
+ result->default_value_double_ = 0.0;
+ break;
+ case FieldDescriptor::CPPTYPE_BOOL:
+ result->default_value_bool_ = false;
+ break;
+ case FieldDescriptor::CPPTYPE_ENUM:
+ // This will be filled in when cross-linking.
+ result->default_value_enum_ = NULL;
+ break;
+ case FieldDescriptor::CPPTYPE_STRING:
+ result->default_value_string_ = &internal::GetEmptyString();
+ break;
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ break;
+ }
+ }
+ }
+
+ if (result->number() <= 0) {
+ AddError(result->full_name(), proto, DescriptorPool::ErrorCollector::NUMBER,
+ "Field numbers must be positive integers.");
+ } else if (!is_extension && result->number() > FieldDescriptor::kMaxNumber) {
+ // Only validate that the number is within the valid field range if it is
+ // not an extension. Since extension numbers are validated with the
+ // extendee's valid set of extension numbers, and those are in turn
+ // validated against the max allowed number, the check is unnecessary for
+ // extension fields.
+ // This avoids cross-linking issues that arise when attempting to check if
+ // the extendee is a message_set_wire_format message, which has a higher max
+ // on extension numbers.
+ AddError(result->full_name(), proto, DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute("Field numbers cannot be greater than $0.",
+ FieldDescriptor::kMaxNumber));
+ } else if (result->number() >= FieldDescriptor::kFirstReservedNumber &&
+ result->number() <= FieldDescriptor::kLastReservedNumber) {
+ AddError(result->full_name(), proto, DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute(
+ "Field numbers $0 through $1 are reserved for the protocol "
+ "buffer library implementation.",
+ FieldDescriptor::kFirstReservedNumber,
+ FieldDescriptor::kLastReservedNumber));
+ }
+
+ if (is_extension) {
+ if (!proto.has_extendee()) {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::EXTENDEE,
+ "FieldDescriptorProto.extendee not set for extension field.");
+ }
+
+ result->extension_scope_ = parent;
+
+ if (proto.has_oneof_index()) {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::OTHER,
+ "FieldDescriptorProto.oneof_index should not be set for "
+ "extensions.");
+ }
+
+ // Fill in later (maybe).
+ result->containing_oneof_ = NULL;
+ } else {
+ if (proto.has_extendee()) {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::EXTENDEE,
+ "FieldDescriptorProto.extendee set for non-extension field.");
+ }
+
+ result->containing_type_ = parent;
+
+ if (proto.has_oneof_index()) {
+ if (proto.oneof_index() < 0 ||
+ proto.oneof_index() >= parent->oneof_decl_count()) {
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::OTHER,
+ strings::Substitute("FieldDescriptorProto.oneof_index $0 is "
+ "out of range for type \"$1\".",
+ proto.oneof_index(),
+ parent->name()));
+ result->containing_oneof_ = NULL;
+ } else {
+ result->containing_oneof_ = parent->oneof_decl(proto.oneof_index());
+ }
+ } else {
+ result->containing_oneof_ = NULL;
+ }
+ }
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ AddSymbol(result->full_name(), parent, result->name(),
+ proto, Symbol(result));
+}
+
+void DescriptorBuilder::BuildExtensionRange(
+ const DescriptorProto::ExtensionRange& proto,
+ const Descriptor* parent,
+ Descriptor::ExtensionRange* result) {
+ result->start = proto.start();
+ result->end = proto.end();
+ if (result->start <= 0) {
+ AddError(parent->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER,
+ "Extension numbers must be positive integers.");
+ }
+
+ // Checking of the upper bound of the extension range is deferred until after
+ // options interpreting. This allows messages with message_set_wire_format to
+ // have extensions beyond FieldDescriptor::kMaxNumber, since the extension
+ // numbers are actually used as int32s in the message_set_wire_format.
+
+ if (result->start >= result->end) {
+ AddError(parent->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER,
+ "Extension range end number must be greater than start number.");
+ }
+}
+
+void DescriptorBuilder::BuildOneof(const OneofDescriptorProto& proto,
+ Descriptor* parent,
+ OneofDescriptor* result) {
+ string* full_name = tables_->AllocateString(parent->full_name());
+ full_name->append(1, '.');
+ full_name->append(proto.name());
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ result->name_ = tables_->AllocateString(proto.name());
+ result->full_name_ = full_name;
+
+ result->containing_type_ = parent;
+
+ // We need to fill these in later.
+ result->field_count_ = 0;
+ result->fields_ = NULL;
+
+ AddSymbol(result->full_name(), parent, result->name(),
+ proto, Symbol(result));
+}
+
+void DescriptorBuilder::BuildEnum(const EnumDescriptorProto& proto,
+ const Descriptor* parent,
+ EnumDescriptor* result) {
+ const string& scope = (parent == NULL) ?
+ file_->package() : parent->full_name();
+ string* full_name = tables_->AllocateString(scope);
+ if (!full_name->empty()) full_name->append(1, '.');
+ full_name->append(proto.name());
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ result->name_ = tables_->AllocateString(proto.name());
+ result->full_name_ = full_name;
+ result->file_ = file_;
+ result->containing_type_ = parent;
+ result->is_placeholder_ = false;
+ result->is_unqualified_placeholder_ = false;
+
+ if (proto.value_size() == 0) {
+ // We cannot allow enums with no values because this would mean there
+ // would be no valid default value for fields of this type.
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::NAME,
+ "Enums must contain at least one value.");
+ }
+
+ BUILD_ARRAY(proto, result, value, BuildEnumValue, result);
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ AddSymbol(result->full_name(), parent, result->name(),
+ proto, Symbol(result));
+}
+
+void DescriptorBuilder::BuildEnumValue(const EnumValueDescriptorProto& proto,
+ const EnumDescriptor* parent,
+ EnumValueDescriptor* result) {
+ result->name_ = tables_->AllocateString(proto.name());
+ result->number_ = proto.number();
+ result->type_ = parent;
+
+ // Note: full_name for enum values is a sibling to the parent's name, not a
+ // child of it.
+ string* full_name = tables_->AllocateString(*parent->full_name_);
+ full_name->resize(full_name->size() - parent->name_->size());
+ full_name->append(*result->name_);
+ result->full_name_ = full_name;
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ // Again, enum values are weird because we makes them appear as siblings
+ // of the enum type instead of children of it. So, we use
+ // parent->containing_type() as the value's parent.
+ bool added_to_outer_scope =
+ AddSymbol(result->full_name(), parent->containing_type(), result->name(),
+ proto, Symbol(result));
+
+ // However, we also want to be able to search for values within a single
+ // enum type, so we add it as a child of the enum type itself, too.
+ // Note: This could fail, but if it does, the error has already been
+ // reported by the above AddSymbol() call, so we ignore the return code.
+ bool added_to_inner_scope =
+ file_tables_->AddAliasUnderParent(parent, result->name(), Symbol(result));
+
+ if (added_to_inner_scope && !added_to_outer_scope) {
+ // This value did not conflict with any values defined in the same enum,
+ // but it did conflict with some other symbol defined in the enum type's
+ // scope. Let's print an additional error to explain this.
+ string outer_scope;
+ if (parent->containing_type() == NULL) {
+ outer_scope = file_->package();
+ } else {
+ outer_scope = parent->containing_type()->full_name();
+ }
+
+ if (outer_scope.empty()) {
+ outer_scope = "the global scope";
+ } else {
+ outer_scope = "\"" + outer_scope + "\"";
+ }
+
+ AddError(result->full_name(), proto,
+ DescriptorPool::ErrorCollector::NAME,
+ "Note that enum values use C++ scoping rules, meaning that "
+ "enum values are siblings of their type, not children of it. "
+ "Therefore, \"" + result->name() + "\" must be unique within "
+ + outer_scope + ", not just within \"" + parent->name() + "\".");
+ }
+
+ // An enum is allowed to define two numbers that refer to the same value.
+ // FindValueByNumber() should return the first such value, so we simply
+ // ignore AddEnumValueByNumber()'s return code.
+ file_tables_->AddEnumValueByNumber(result);
+}
+
+void DescriptorBuilder::BuildService(const ServiceDescriptorProto& proto,
+ const void* /* dummy */,
+ ServiceDescriptor* result) {
+ string* full_name = tables_->AllocateString(file_->package());
+ if (!full_name->empty()) full_name->append(1, '.');
+ full_name->append(proto.name());
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ result->name_ = tables_->AllocateString(proto.name());
+ result->full_name_ = full_name;
+ result->file_ = file_;
+
+ BUILD_ARRAY(proto, result, method, BuildMethod, result);
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ AddSymbol(result->full_name(), NULL, result->name(),
+ proto, Symbol(result));
+}
+
+void DescriptorBuilder::BuildMethod(const MethodDescriptorProto& proto,
+ const ServiceDescriptor* parent,
+ MethodDescriptor* result) {
+ result->name_ = tables_->AllocateString(proto.name());
+ result->service_ = parent;
+
+ string* full_name = tables_->AllocateString(parent->full_name());
+ full_name->append(1, '.');
+ full_name->append(*result->name_);
+ result->full_name_ = full_name;
+
+ ValidateSymbolName(proto.name(), *full_name, proto);
+
+ // These will be filled in when cross-linking.
+ result->input_type_ = NULL;
+ result->output_type_ = NULL;
+
+ // Copy options.
+ if (!proto.has_options()) {
+ result->options_ = NULL; // Will set to default_instance later.
+ } else {
+ AllocateOptions(proto.options(), result);
+ }
+
+ AddSymbol(result->full_name(), parent, result->name(),
+ proto, Symbol(result));
+}
+
+#undef BUILD_ARRAY
+
+// -------------------------------------------------------------------
+
+void DescriptorBuilder::CrossLinkFile(
+ FileDescriptor* file, const FileDescriptorProto& proto) {
+ if (file->options_ == NULL) {
+ file->options_ = &FileOptions::default_instance();
+ }
+
+ for (int i = 0; i < file->message_type_count(); i++) {
+ CrossLinkMessage(&file->message_types_[i], proto.message_type(i));
+ }
+
+ for (int i = 0; i < file->extension_count(); i++) {
+ CrossLinkField(&file->extensions_[i], proto.extension(i));
+ }
+
+ for (int i = 0; i < file->enum_type_count(); i++) {
+ CrossLinkEnum(&file->enum_types_[i], proto.enum_type(i));
+ }
+
+ for (int i = 0; i < file->service_count(); i++) {
+ CrossLinkService(&file->services_[i], proto.service(i));
+ }
+}
+
+void DescriptorBuilder::CrossLinkMessage(
+ Descriptor* message, const DescriptorProto& proto) {
+ if (message->options_ == NULL) {
+ message->options_ = &MessageOptions::default_instance();
+ }
+
+ for (int i = 0; i < message->nested_type_count(); i++) {
+ CrossLinkMessage(&message->nested_types_[i], proto.nested_type(i));
+ }
+
+ for (int i = 0; i < message->enum_type_count(); i++) {
+ CrossLinkEnum(&message->enum_types_[i], proto.enum_type(i));
+ }
+
+ for (int i = 0; i < message->field_count(); i++) {
+ CrossLinkField(&message->fields_[i], proto.field(i));
+ }
+
+ for (int i = 0; i < message->extension_count(); i++) {
+ CrossLinkField(&message->extensions_[i], proto.extension(i));
+ }
+
+ // Set up field array for each oneof.
+
+ // First count the number of fields per oneof.
+ for (int i = 0; i < message->field_count(); i++) {
+ const OneofDescriptor* oneof_decl = message->field(i)->containing_oneof();
+ if (oneof_decl != NULL) {
+ // Must go through oneof_decls_ array to get a non-const version of the
+ // OneofDescriptor.
+ ++message->oneof_decls_[oneof_decl->index()].field_count_;
+ }
+ }
+
+ // Then allocate the arrays.
+ for (int i = 0; i < message->oneof_decl_count(); i++) {
+ OneofDescriptor* oneof_decl = &message->oneof_decls_[i];
+
+ if (oneof_decl->field_count() == 0) {
+ AddError(message->full_name() + "." + oneof_decl->name(),
+ proto.oneof_decl(i),
+ DescriptorPool::ErrorCollector::NAME,
+ "Oneof must have at least one field.");
+ }
+
+ oneof_decl->fields_ =
+ tables_->AllocateArray<const FieldDescriptor*>(oneof_decl->field_count_);
+ oneof_decl->field_count_ = 0;
+ }
+
+ // Then fill them in.
+ for (int i = 0; i < message->field_count(); i++) {
+ const OneofDescriptor* oneof_decl = message->field(i)->containing_oneof();
+ if (oneof_decl != NULL) {
+ OneofDescriptor* mutable_oneof_decl =
+ &message->oneof_decls_[oneof_decl->index()];
+ message->fields_[i].index_in_oneof_ = mutable_oneof_decl->field_count_;
+ mutable_oneof_decl->fields_[mutable_oneof_decl->field_count_++] =
+ message->field(i);
+ }
+ }
+}
+
+void DescriptorBuilder::CrossLinkField(
+ FieldDescriptor* field, const FieldDescriptorProto& proto) {
+ if (field->options_ == NULL) {
+ field->options_ = &FieldOptions::default_instance();
+ }
+
+ if (proto.has_extendee()) {
+ Symbol extendee = LookupSymbol(proto.extendee(), field->full_name(),
+ PLACEHOLDER_EXTENDABLE_MESSAGE);
+ if (extendee.IsNull()) {
+ AddNotDefinedError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::EXTENDEE,
+ proto.extendee());
+ return;
+ } else if (extendee.type != Symbol::MESSAGE) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::EXTENDEE,
+ "\"" + proto.extendee() + "\" is not a message type.");
+ return;
+ }
+ field->containing_type_ = extendee.descriptor;
+
+ const Descriptor::ExtensionRange* extension_range = field->containing_type()
+ ->FindExtensionRangeContainingNumber(field->number());
+
+ if (extension_range == NULL) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute("\"$0\" does not declare $1 as an "
+ "extension number.",
+ field->containing_type()->full_name(),
+ field->number()));
+ }
+ }
+
+ if (field->containing_oneof() != NULL) {
+ if (field->label() != FieldDescriptor::LABEL_OPTIONAL) {
+ // Note that this error will never happen when parsing .proto files.
+ // It can only happen if you manually construct a FileDescriptorProto
+ // that is incorrect.
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::NAME,
+ "Fields of oneofs must themselves have label LABEL_OPTIONAL.");
+ }
+ }
+
+ if (proto.has_type_name()) {
+ // Assume we are expecting a message type unless the proto contains some
+ // evidence that it expects an enum type. This only makes a difference if
+ // we end up creating a placeholder.
+ bool expecting_enum = (proto.type() == FieldDescriptorProto::TYPE_ENUM) ||
+ proto.has_default_value();
+
+ Symbol type =
+ LookupSymbol(proto.type_name(), field->full_name(),
+ expecting_enum ? PLACEHOLDER_ENUM : PLACEHOLDER_MESSAGE,
+ LOOKUP_TYPES);
+
+ // If the type is a weak type, we change the type to a google.protobuf.Empty field.
+ if (type.IsNull() && !pool_->enforce_weak_ && proto.options().weak()) {
+ type = FindSymbol(kNonLinkedWeakMessageReplacementName);
+ }
+
+ if (type.IsNull()) {
+ AddNotDefinedError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ proto.type_name());
+ return;
+ }
+
+ if (!proto.has_type()) {
+ // Choose field type based on symbol.
+ if (type.type == Symbol::MESSAGE) {
+ field->type_ = FieldDescriptor::TYPE_MESSAGE;
+ } else if (type.type == Symbol::ENUM) {
+ field->type_ = FieldDescriptor::TYPE_ENUM;
+ } else {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ "\"" + proto.type_name() + "\" is not a type.");
+ return;
+ }
+ }
+
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ if (type.type != Symbol::MESSAGE) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ "\"" + proto.type_name() + "\" is not a message type.");
+ return;
+ }
+ field->message_type_ = type.descriptor;
+
+ if (field->has_default_value()) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Messages can't have default values.");
+ }
+ } else if (field->cpp_type() == FieldDescriptor::CPPTYPE_ENUM) {
+ if (type.type != Symbol::ENUM) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ "\"" + proto.type_name() + "\" is not an enum type.");
+ return;
+ }
+ field->enum_type_ = type.enum_descriptor;
+
+ if (field->enum_type()->is_placeholder_) {
+ // We can't look up default values for placeholder types. We'll have
+ // to just drop them.
+ field->has_default_value_ = false;
+ }
+
+ if (field->has_default_value()) {
+ // Ensure that the default value is an identifier. Parser cannot always
+ // verify this because it does not have complete type information.
+ // N.B. that this check yields better error messages but is not
+ // necessary for correctness (an enum symbol must be a valid identifier
+ // anyway), only for better errors.
+ if (!io::Tokenizer::IsIdentifier(proto.default_value())) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Default value for an enum field must be an identifier.");
+ } else {
+ // We can't just use field->enum_type()->FindValueByName() here
+ // because that locks the pool's mutex, which we have already locked
+ // at this point.
+ Symbol default_value =
+ LookupSymbolNoPlaceholder(proto.default_value(),
+ field->enum_type()->full_name());
+
+ if (default_value.type == Symbol::ENUM_VALUE &&
+ default_value.enum_value_descriptor->type() ==
+ field->enum_type()) {
+ field->default_value_enum_ = default_value.enum_value_descriptor;
+ } else {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::DEFAULT_VALUE,
+ "Enum type \"" + field->enum_type()->full_name() +
+ "\" has no value named \"" + proto.default_value() +
+ "\".");
+ }
+ }
+ } else if (field->enum_type()->value_count() > 0) {
+ // All enums must have at least one value, or we would have reported
+ // an error elsewhere. We use the first defined value as the default
+ // if a default is not explicitly defined.
+ field->default_value_enum_ = field->enum_type()->value(0);
+ }
+ } else {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "Field with primitive type has type_name.");
+ }
+ } else {
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE ||
+ field->cpp_type() == FieldDescriptor::CPPTYPE_ENUM) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "Field with message or enum type missing type_name.");
+ }
+ }
+
+ // Add the field to the fields-by-number table.
+ // Note: We have to do this *after* cross-linking because extensions do not
+ // know their containing type until now.
+ if (!file_tables_->AddFieldByNumber(field)) {
+ const FieldDescriptor* conflicting_field =
+ file_tables_->FindFieldByNumber(field->containing_type(),
+ field->number());
+ if (field->is_extension()) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute("Extension number $0 has already been used "
+ "in \"$1\" by extension \"$2\".",
+ field->number(),
+ field->containing_type()->full_name(),
+ conflicting_field->full_name()));
+ } else {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute("Field number $0 has already been used in "
+ "\"$1\" by field \"$2\".",
+ field->number(),
+ field->containing_type()->full_name(),
+ conflicting_field->name()));
+ }
+ } else {
+ if (field->is_extension()) {
+ if (!tables_->AddExtension(field)) {
+ const FieldDescriptor* conflicting_field =
+ tables_->FindExtension(field->containing_type(), field->number());
+ string error_msg = strings::Substitute(
+ "Extension number $0 has already been used in \"$1\" by extension "
+ "\"$2\" defined in $3.",
+ field->number(),
+ field->containing_type()->full_name(),
+ conflicting_field->full_name(),
+ conflicting_field->file()->name());
+ // Conflicting extension numbers should be an error. However, before
+ // turning this into an error we need to fix all existing broken
+ // protos first.
+ // TODO(xiaofeng): Change this to an error.
+ AddWarning(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER, error_msg);
+ }
+ }
+ }
+
+ // Add the field to the lowercase-name and camelcase-name tables.
+ file_tables_->AddFieldByStylizedNames(field);
+}
+
+void DescriptorBuilder::CrossLinkEnum(
+ EnumDescriptor* enum_type, const EnumDescriptorProto& proto) {
+ if (enum_type->options_ == NULL) {
+ enum_type->options_ = &EnumOptions::default_instance();
+ }
+
+ for (int i = 0; i < enum_type->value_count(); i++) {
+ CrossLinkEnumValue(&enum_type->values_[i], proto.value(i));
+ }
+}
+
+void DescriptorBuilder::CrossLinkEnumValue(
+ EnumValueDescriptor* enum_value,
+ const EnumValueDescriptorProto& /* proto */) {
+ if (enum_value->options_ == NULL) {
+ enum_value->options_ = &EnumValueOptions::default_instance();
+ }
+}
+
+void DescriptorBuilder::CrossLinkService(
+ ServiceDescriptor* service, const ServiceDescriptorProto& proto) {
+ if (service->options_ == NULL) {
+ service->options_ = &ServiceOptions::default_instance();
+ }
+
+ for (int i = 0; i < service->method_count(); i++) {
+ CrossLinkMethod(&service->methods_[i], proto.method(i));
+ }
+}
+
+void DescriptorBuilder::CrossLinkMethod(
+ MethodDescriptor* method, const MethodDescriptorProto& proto) {
+ if (method->options_ == NULL) {
+ method->options_ = &MethodOptions::default_instance();
+ }
+
+ Symbol input_type = LookupSymbol(proto.input_type(), method->full_name());
+ if (input_type.IsNull()) {
+ AddNotDefinedError(method->full_name(), proto,
+ DescriptorPool::ErrorCollector::INPUT_TYPE,
+ proto.input_type());
+ } else if (input_type.type != Symbol::MESSAGE) {
+ AddError(method->full_name(), proto,
+ DescriptorPool::ErrorCollector::INPUT_TYPE,
+ "\"" + proto.input_type() + "\" is not a message type.");
+ } else {
+ method->input_type_ = input_type.descriptor;
+ }
+
+ Symbol output_type = LookupSymbol(proto.output_type(), method->full_name());
+ if (output_type.IsNull()) {
+ AddNotDefinedError(method->full_name(), proto,
+ DescriptorPool::ErrorCollector::OUTPUT_TYPE,
+ proto.output_type());
+ } else if (output_type.type != Symbol::MESSAGE) {
+ AddError(method->full_name(), proto,
+ DescriptorPool::ErrorCollector::OUTPUT_TYPE,
+ "\"" + proto.output_type() + "\" is not a message type.");
+ } else {
+ method->output_type_ = output_type.descriptor;
+ }
+}
+
+// -------------------------------------------------------------------
+
+#define VALIDATE_OPTIONS_FROM_ARRAY(descriptor, array_name, type) \
+ for (int i = 0; i < descriptor->array_name##_count(); ++i) { \
+ Validate##type##Options(descriptor->array_name##s_ + i, \
+ proto.array_name(i)); \
+ }
+
+// Determine if the file uses optimize_for = LITE_RUNTIME, being careful to
+// avoid problems that exist at init time.
+static bool IsLite(const FileDescriptor* file) {
+ // TODO(kenton): I don't even remember how many of these conditions are
+ // actually possible. I'm just being super-safe.
+ return file != NULL &&
+ &file->options() != &FileOptions::default_instance() &&
+ file->options().optimize_for() == FileOptions::LITE_RUNTIME;
+}
+
+void DescriptorBuilder::ValidateFileOptions(FileDescriptor* file,
+ const FileDescriptorProto& proto) {
+ VALIDATE_OPTIONS_FROM_ARRAY(file, message_type, Message);
+ VALIDATE_OPTIONS_FROM_ARRAY(file, enum_type, Enum);
+ VALIDATE_OPTIONS_FROM_ARRAY(file, service, Service);
+ VALIDATE_OPTIONS_FROM_ARRAY(file, extension, Field);
+
+ // Lite files can only be imported by other Lite files.
+ if (!IsLite(file)) {
+ for (int i = 0; i < file->dependency_count(); i++) {
+ if (IsLite(file->dependency(i))) {
+ AddError(
+ file->name(), proto,
+ DescriptorPool::ErrorCollector::OTHER,
+ "Files that do not use optimize_for = LITE_RUNTIME cannot import "
+ "files which do use this option. This file is not lite, but it "
+ "imports \"" + file->dependency(i)->name() + "\" which is.");
+ break;
+ }
+ }
+ }
+}
+
+
+void DescriptorBuilder::ValidateMessageOptions(Descriptor* message,
+ const DescriptorProto& proto) {
+ VALIDATE_OPTIONS_FROM_ARRAY(message, field, Field);
+ VALIDATE_OPTIONS_FROM_ARRAY(message, nested_type, Message);
+ VALIDATE_OPTIONS_FROM_ARRAY(message, enum_type, Enum);
+ VALIDATE_OPTIONS_FROM_ARRAY(message, extension, Field);
+
+ const int64 max_extension_range =
+ static_cast<int64>(message->options().message_set_wire_format() ?
+ kint32max :
+ FieldDescriptor::kMaxNumber);
+ for (int i = 0; i < message->extension_range_count(); ++i) {
+ if (message->extension_range(i)->end > max_extension_range + 1) {
+ AddError(
+ message->full_name(), proto.extension_range(i),
+ DescriptorPool::ErrorCollector::NUMBER,
+ strings::Substitute("Extension numbers cannot be greater than $0.",
+ max_extension_range));
+ }
+ }
+}
+
+void DescriptorBuilder::ValidateFieldOptions(FieldDescriptor* field,
+ const FieldDescriptorProto& proto) {
+ if (field->options().has_experimental_map_key()) {
+ ValidateMapKey(field, proto);
+ }
+
+ // Only message type fields may be lazy.
+ if (field->options().lazy()) {
+ if (field->type() != FieldDescriptor::TYPE_MESSAGE) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ "[lazy = true] can only be specified for submessage fields.");
+ }
+ }
+
+ // Only repeated primitive fields may be packed.
+ if (field->options().packed() && !field->is_packable()) {
+ AddError(
+ field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ "[packed = true] can only be specified for repeated primitive fields.");
+ }
+
+ // Note: Default instance may not yet be initialized here, so we have to
+ // avoid reading from it.
+ if (field->containing_type_ != NULL &&
+ &field->containing_type()->options() !=
+ &MessageOptions::default_instance() &&
+ field->containing_type()->options().message_set_wire_format()) {
+ if (field->is_extension()) {
+ if (!field->is_optional() ||
+ field->type() != FieldDescriptor::TYPE_MESSAGE) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::TYPE,
+ "Extensions of MessageSets must be optional messages.");
+ }
+ } else {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::NAME,
+ "MessageSets cannot have fields, only extensions.");
+ }
+ }
+
+ // Lite extensions can only be of Lite types.
+ if (IsLite(field->file()) &&
+ field->containing_type_ != NULL &&
+ !IsLite(field->containing_type()->file())) {
+ AddError(field->full_name(), proto,
+ DescriptorPool::ErrorCollector::EXTENDEE,
+ "Extensions to non-lite types can only be declared in non-lite "
+ "files. Note that you cannot extend a non-lite type to contain "
+ "a lite type, but the reverse is allowed.");
+ }
+
+}
+
+void DescriptorBuilder::ValidateEnumOptions(EnumDescriptor* enm,
+ const EnumDescriptorProto& proto) {
+ VALIDATE_OPTIONS_FROM_ARRAY(enm, value, EnumValue);
+ if (!enm->options().has_allow_alias() || !enm->options().allow_alias()) {
+ map<int, string> used_values;
+ for (int i = 0; i < enm->value_count(); ++i) {
+ const EnumValueDescriptor* enum_value = enm->value(i);
+ if (used_values.find(enum_value->number()) != used_values.end()) {
+ string error =
+ "\"" + enum_value->full_name() +
+ "\" uses the same enum value as \"" +
+ used_values[enum_value->number()] + "\". If this is intended, set "
+ "'option allow_alias = true;' to the enum definition.";
+ if (!enm->options().allow_alias()) {
+ // Generate error if duplicated enum values are explicitly disallowed.
+ AddError(enm->full_name(), proto,
+ DescriptorPool::ErrorCollector::NUMBER,
+ error);
+ } else {
+ // Generate warning if duplicated values are found but the option
+ // isn't set.
+ GOOGLE_LOG(ERROR) << error;
+ }
+ } else {
+ used_values[enum_value->number()] = enum_value->full_name();
+ }
+ }
+ }
+}
+
+void DescriptorBuilder::ValidateEnumValueOptions(
+ EnumValueDescriptor* /* enum_value */,
+ const EnumValueDescriptorProto& /* proto */) {
+ // Nothing to do so far.
+}
+void DescriptorBuilder::ValidateServiceOptions(ServiceDescriptor* service,
+ const ServiceDescriptorProto& proto) {
+ if (IsLite(service->file()) &&
+ (service->file()->options().cc_generic_services() ||
+ service->file()->options().java_generic_services())) {
+ AddError(service->full_name(), proto,
+ DescriptorPool::ErrorCollector::NAME,
+ "Files with optimize_for = LITE_RUNTIME cannot define services "
+ "unless you set both options cc_generic_services and "
+ "java_generic_sevices to false.");
+ }
+
+ VALIDATE_OPTIONS_FROM_ARRAY(service, method, Method);
+}
+
+void DescriptorBuilder::ValidateMethodOptions(MethodDescriptor* /* method */,
+ const MethodDescriptorProto& /* proto */) {
+ // Nothing to do so far.
+}
+
+void DescriptorBuilder::ValidateMapKey(FieldDescriptor* field,
+ const FieldDescriptorProto& proto) {
+ if (!field->is_repeated()) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "map type is only allowed for repeated fields.");
+ return;
+ }
+
+ if (field->cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "map type is only allowed for fields with a message type.");
+ return;
+ }
+
+ const Descriptor* item_type = field->message_type();
+ if (item_type == NULL) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "Could not find field type.");
+ return;
+ }
+
+ // Find the field in item_type named by "experimental_map_key"
+ const string& key_name = field->options().experimental_map_key();
+ const Symbol key_symbol = LookupSymbol(
+ key_name,
+ // We append ".key_name" to the containing type's name since
+ // LookupSymbol() searches for peers of the supplied name, not
+ // children of the supplied name.
+ item_type->full_name() + "." + key_name);
+
+ if (key_symbol.IsNull() || key_symbol.field_descriptor->is_extension()) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "Could not find field named \"" + key_name + "\" in type \"" +
+ item_type->full_name() + "\".");
+ return;
+ }
+ const FieldDescriptor* key_field = key_symbol.field_descriptor;
+
+ if (key_field->is_repeated()) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "map_key must not name a repeated field.");
+ return;
+ }
+
+ if (key_field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ AddError(field->full_name(), proto, DescriptorPool::ErrorCollector::TYPE,
+ "map key must name a scalar or string field.");
+ return;
+ }
+
+ field->experimental_map_key_ = key_field;
+}
+
+
+#undef VALIDATE_OPTIONS_FROM_ARRAY
+
+// -------------------------------------------------------------------
+
+DescriptorBuilder::OptionInterpreter::OptionInterpreter(
+ DescriptorBuilder* builder) : builder_(builder) {
+ GOOGLE_CHECK(builder_);
+}
+
+DescriptorBuilder::OptionInterpreter::~OptionInterpreter() {
+}
+
+bool DescriptorBuilder::OptionInterpreter::InterpretOptions(
+ OptionsToInterpret* options_to_interpret) {
+ // Note that these may be in different pools, so we can't use the same
+ // descriptor and reflection objects on both.
+ Message* options = options_to_interpret->options;
+ const Message* original_options = options_to_interpret->original_options;
+
+ bool failed = false;
+ options_to_interpret_ = options_to_interpret;
+
+ // Find the uninterpreted_option field in the mutable copy of the options
+ // and clear them, since we're about to interpret them.
+ const FieldDescriptor* uninterpreted_options_field =
+ options->GetDescriptor()->FindFieldByName("uninterpreted_option");
+ GOOGLE_CHECK(uninterpreted_options_field != NULL)
+ << "No field named \"uninterpreted_option\" in the Options proto.";
+ options->GetReflection()->ClearField(options, uninterpreted_options_field);
+
+ // Find the uninterpreted_option field in the original options.
+ const FieldDescriptor* original_uninterpreted_options_field =
+ original_options->GetDescriptor()->
+ FindFieldByName("uninterpreted_option");
+ GOOGLE_CHECK(original_uninterpreted_options_field != NULL)
+ << "No field named \"uninterpreted_option\" in the Options proto.";
+
+ const int num_uninterpreted_options = original_options->GetReflection()->
+ FieldSize(*original_options, original_uninterpreted_options_field);
+ for (int i = 0; i < num_uninterpreted_options; ++i) {
+ uninterpreted_option_ = down_cast<const UninterpretedOption*>(
+ &original_options->GetReflection()->GetRepeatedMessage(
+ *original_options, original_uninterpreted_options_field, i));
+ if (!InterpretSingleOption(options)) {
+ // Error already added by InterpretSingleOption().
+ failed = true;
+ break;
+ }
+ }
+ // Reset these, so we don't have any dangling pointers.
+ uninterpreted_option_ = NULL;
+ options_to_interpret_ = NULL;
+
+ if (!failed) {
+ // InterpretSingleOption() added the interpreted options in the
+ // UnknownFieldSet, in case the option isn't yet known to us. Now we
+ // serialize the options message and deserialize it back. That way, any
+ // option fields that we do happen to know about will get moved from the
+ // UnknownFieldSet into the real fields, and thus be available right away.
+ // If they are not known, that's OK too. They will get reparsed into the
+ // UnknownFieldSet and wait there until the message is parsed by something
+ // that does know about the options.
+ string buf;
+ options->AppendToString(&buf);
+ GOOGLE_CHECK(options->ParseFromString(buf))
+ << "Protocol message serialized itself in invalid fashion.";
+ }
+
+ return !failed;
+}
+
+bool DescriptorBuilder::OptionInterpreter::InterpretSingleOption(
+ Message* options) {
+ // First do some basic validation.
+ if (uninterpreted_option_->name_size() == 0) {
+ // This should never happen unless the parser has gone seriously awry or
+ // someone has manually created the uninterpreted option badly.
+ return AddNameError("Option must have a name.");
+ }
+ if (uninterpreted_option_->name(0).name_part() == "uninterpreted_option") {
+ return AddNameError("Option must not use reserved name "
+ "\"uninterpreted_option\".");
+ }
+
+ const Descriptor* options_descriptor = NULL;
+ // Get the options message's descriptor from the builder's pool, so that we
+ // get the version that knows about any extension options declared in the
+ // file we're currently building. The descriptor should be there as long as
+ // the file we're building imported "google/protobuf/descriptors.proto".
+
+ // Note that we use DescriptorBuilder::FindSymbolNotEnforcingDeps(), not
+ // DescriptorPool::FindMessageTypeByName() because we're already holding the
+ // pool's mutex, and the latter method locks it again. We don't use
+ // FindSymbol() because files that use custom options only need to depend on
+ // the file that defines the option, not descriptor.proto itself.
+ Symbol symbol = builder_->FindSymbolNotEnforcingDeps(
+ options->GetDescriptor()->full_name());
+ if (!symbol.IsNull() && symbol.type == Symbol::MESSAGE) {
+ options_descriptor = symbol.descriptor;
+ } else {
+ // The options message's descriptor was not in the builder's pool, so use
+ // the standard version from the generated pool. We're not holding the
+ // generated pool's mutex, so we can search it the straightforward way.
+ options_descriptor = options->GetDescriptor();
+ }
+ GOOGLE_CHECK(options_descriptor);
+
+ // We iterate over the name parts to drill into the submessages until we find
+ // the leaf field for the option. As we drill down we remember the current
+ // submessage's descriptor in |descriptor| and the next field in that
+ // submessage in |field|. We also track the fields we're drilling down
+ // through in |intermediate_fields|. As we go, we reconstruct the full option
+ // name in |debug_msg_name|, for use in error messages.
+ const Descriptor* descriptor = options_descriptor;
+ const FieldDescriptor* field = NULL;
+ vector<const FieldDescriptor*> intermediate_fields;
+ string debug_msg_name = "";
+
+ for (int i = 0; i < uninterpreted_option_->name_size(); ++i) {
+ const string& name_part = uninterpreted_option_->name(i).name_part();
+ if (debug_msg_name.size() > 0) {
+ debug_msg_name += ".";
+ }
+ if (uninterpreted_option_->name(i).is_extension()) {
+ debug_msg_name += "(" + name_part + ")";
+ // Search for the extension's descriptor as an extension in the builder's
+ // pool. Note that we use DescriptorBuilder::LookupSymbol(), not
+ // DescriptorPool::FindExtensionByName(), for two reasons: 1) It allows
+ // relative lookups, and 2) because we're already holding the pool's
+ // mutex, and the latter method locks it again.
+ symbol = builder_->LookupSymbol(name_part,
+ options_to_interpret_->name_scope);
+ if (!symbol.IsNull() && symbol.type == Symbol::FIELD) {
+ field = symbol.field_descriptor;
+ }
+ // If we don't find the field then the field's descriptor was not in the
+ // builder's pool, but there's no point in looking in the generated
+ // pool. We require that you import the file that defines any extensions
+ // you use, so they must be present in the builder's pool.
+ } else {
+ debug_msg_name += name_part;
+ // Search for the field's descriptor as a regular field.
+ field = descriptor->FindFieldByName(name_part);
+ }
+
+ if (field == NULL) {
+ if (get_allow_unknown(builder_->pool_)) {
+ // We can't find the option, but AllowUnknownDependencies() is enabled,
+ // so we will just leave it as uninterpreted.
+ AddWithoutInterpreting(*uninterpreted_option_, options);
+ return true;
+ } else if (!(builder_->undefine_resolved_name_).empty()) {
+ // Option is resolved to a name which is not defined.
+ return AddNameError(
+ "Option \"" + debug_msg_name + "\" is resolved to \"(" +
+ builder_->undefine_resolved_name_ +
+ ")\", which is not defined. The innermost scope is searched first "
+ "in name resolution. Consider using a leading '.'(i.e., \"(." +
+ debug_msg_name.substr(1) +
+ "\") to start from the outermost scope.");
+ } else {
+ return AddNameError("Option \"" + debug_msg_name + "\" unknown.");
+ }
+ } else if (field->containing_type() != descriptor) {
+ if (get_is_placeholder(field->containing_type())) {
+ // The field is an extension of a placeholder type, so we can't
+ // reliably verify whether it is a valid extension to use here (e.g.
+ // we don't know if it is an extension of the correct *Options message,
+ // or if it has a valid field number, etc.). Just leave it as
+ // uninterpreted instead.
+ AddWithoutInterpreting(*uninterpreted_option_, options);
+ return true;
+ } else {
+ // This can only happen if, due to some insane misconfiguration of the
+ // pools, we find the options message in one pool but the field in
+ // another. This would probably imply a hefty bug somewhere.
+ return AddNameError("Option field \"" + debug_msg_name +
+ "\" is not a field or extension of message \"" +
+ descriptor->name() + "\".");
+ }
+ } else if (i < uninterpreted_option_->name_size() - 1) {
+ if (field->cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE) {
+ return AddNameError("Option \"" + debug_msg_name +
+ "\" is an atomic type, not a message.");
+ } else if (field->is_repeated()) {
+ return AddNameError("Option field \"" + debug_msg_name +
+ "\" is a repeated message. Repeated message "
+ "options must be initialized using an "
+ "aggregate value.");
+ } else {
+ // Drill down into the submessage.
+ intermediate_fields.push_back(field);
+ descriptor = field->message_type();
+ }
+ }
+ }
+
+ // We've found the leaf field. Now we use UnknownFieldSets to set its value
+ // on the options message. We do so because the message may not yet know
+ // about its extension fields, so we may not be able to set the fields
+ // directly. But the UnknownFieldSets will serialize to the same wire-format
+ // message, so reading that message back in once the extension fields are
+ // known will populate them correctly.
+
+ // First see if the option is already set.
+ if (!field->is_repeated() && !ExamineIfOptionIsSet(
+ intermediate_fields.begin(),
+ intermediate_fields.end(),
+ field, debug_msg_name,
+ options->GetReflection()->GetUnknownFields(*options))) {
+ return false; // ExamineIfOptionIsSet() already added the error.
+ }
+
+
+ // First set the value on the UnknownFieldSet corresponding to the
+ // innermost message.
+ scoped_ptr<UnknownFieldSet> unknown_fields(new UnknownFieldSet());
+ if (!SetOptionValue(field, unknown_fields.get())) {
+ return false; // SetOptionValue() already added the error.
+ }
+
+ // Now wrap the UnknownFieldSet with UnknownFieldSets corresponding to all
+ // the intermediate messages.
+ for (vector<const FieldDescriptor*>::reverse_iterator iter =
+ intermediate_fields.rbegin();
+ iter != intermediate_fields.rend(); ++iter) {
+ scoped_ptr<UnknownFieldSet> parent_unknown_fields(new UnknownFieldSet());
+ switch ((*iter)->type()) {
+ case FieldDescriptor::TYPE_MESSAGE: {
+ io::StringOutputStream outstr(
+ parent_unknown_fields->AddLengthDelimited((*iter)->number()));
+ io::CodedOutputStream out(&outstr);
+ internal::WireFormat::SerializeUnknownFields(*unknown_fields, &out);
+ GOOGLE_CHECK(!out.HadError())
+ << "Unexpected failure while serializing option submessage "
+ << debug_msg_name << "\".";
+ break;
+ }
+
+ case FieldDescriptor::TYPE_GROUP: {
+ parent_unknown_fields->AddGroup((*iter)->number())
+ ->MergeFrom(*unknown_fields);
+ break;
+ }
+
+ default:
+ GOOGLE_LOG(FATAL) << "Invalid wire type for CPPTYPE_MESSAGE: "
+ << (*iter)->type();
+ return false;
+ }
+ unknown_fields.reset(parent_unknown_fields.release());
+ }
+
+ // Now merge the UnknownFieldSet corresponding to the top-level message into
+ // the options message.
+ options->GetReflection()->MutableUnknownFields(options)->MergeFrom(
+ *unknown_fields);
+
+ return true;
+}
+
+void DescriptorBuilder::OptionInterpreter::AddWithoutInterpreting(
+ const UninterpretedOption& uninterpreted_option, Message* options) {
+ const FieldDescriptor* field =
+ options->GetDescriptor()->FindFieldByName("uninterpreted_option");
+ GOOGLE_CHECK(field != NULL);
+
+ options->GetReflection()->AddMessage(options, field)
+ ->CopyFrom(uninterpreted_option);
+}
+
+bool DescriptorBuilder::OptionInterpreter::ExamineIfOptionIsSet(
+ vector<const FieldDescriptor*>::const_iterator intermediate_fields_iter,
+ vector<const FieldDescriptor*>::const_iterator intermediate_fields_end,
+ const FieldDescriptor* innermost_field, const string& debug_msg_name,
+ const UnknownFieldSet& unknown_fields) {
+ // We do linear searches of the UnknownFieldSet and its sub-groups. This
+ // should be fine since it's unlikely that any one options structure will
+ // contain more than a handful of options.
+
+ if (intermediate_fields_iter == intermediate_fields_end) {
+ // We're at the innermost submessage.
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ if (unknown_fields.field(i).number() == innermost_field->number()) {
+ return AddNameError("Option \"" + debug_msg_name +
+ "\" was already set.");
+ }
+ }
+ return true;
+ }
+
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ if (unknown_fields.field(i).number() ==
+ (*intermediate_fields_iter)->number()) {
+ const UnknownField* unknown_field = &unknown_fields.field(i);
+ FieldDescriptor::Type type = (*intermediate_fields_iter)->type();
+ // Recurse into the next submessage.
+ switch (type) {
+ case FieldDescriptor::TYPE_MESSAGE:
+ if (unknown_field->type() == UnknownField::TYPE_LENGTH_DELIMITED) {
+ UnknownFieldSet intermediate_unknown_fields;
+ if (intermediate_unknown_fields.ParseFromString(
+ unknown_field->length_delimited()) &&
+ !ExamineIfOptionIsSet(intermediate_fields_iter + 1,
+ intermediate_fields_end,
+ innermost_field, debug_msg_name,
+ intermediate_unknown_fields)) {
+ return false; // Error already added.
+ }
+ }
+ break;
+
+ case FieldDescriptor::TYPE_GROUP:
+ if (unknown_field->type() == UnknownField::TYPE_GROUP) {
+ if (!ExamineIfOptionIsSet(intermediate_fields_iter + 1,
+ intermediate_fields_end,
+ innermost_field, debug_msg_name,
+ unknown_field->group())) {
+ return false; // Error already added.
+ }
+ }
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Invalid wire type for CPPTYPE_MESSAGE: " << type;
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool DescriptorBuilder::OptionInterpreter::SetOptionValue(
+ const FieldDescriptor* option_field,
+ UnknownFieldSet* unknown_fields) {
+ // We switch on the CppType to validate.
+ switch (option_field->cpp_type()) {
+
+ case FieldDescriptor::CPPTYPE_INT32:
+ if (uninterpreted_option_->has_positive_int_value()) {
+ if (uninterpreted_option_->positive_int_value() >
+ static_cast<uint64>(kint32max)) {
+ return AddValueError("Value out of range for int32 option \"" +
+ option_field->full_name() + "\".");
+ } else {
+ SetInt32(option_field->number(),
+ uninterpreted_option_->positive_int_value(),
+ option_field->type(), unknown_fields);
+ }
+ } else if (uninterpreted_option_->has_negative_int_value()) {
+ if (uninterpreted_option_->negative_int_value() <
+ static_cast<int64>(kint32min)) {
+ return AddValueError("Value out of range for int32 option \"" +
+ option_field->full_name() + "\".");
+ } else {
+ SetInt32(option_field->number(),
+ uninterpreted_option_->negative_int_value(),
+ option_field->type(), unknown_fields);
+ }
+ } else {
+ return AddValueError("Value must be integer for int32 option \"" +
+ option_field->full_name() + "\".");
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_INT64:
+ if (uninterpreted_option_->has_positive_int_value()) {
+ if (uninterpreted_option_->positive_int_value() >
+ static_cast<uint64>(kint64max)) {
+ return AddValueError("Value out of range for int64 option \"" +
+ option_field->full_name() + "\".");
+ } else {
+ SetInt64(option_field->number(),
+ uninterpreted_option_->positive_int_value(),
+ option_field->type(), unknown_fields);
+ }
+ } else if (uninterpreted_option_->has_negative_int_value()) {
+ SetInt64(option_field->number(),
+ uninterpreted_option_->negative_int_value(),
+ option_field->type(), unknown_fields);
+ } else {
+ return AddValueError("Value must be integer for int64 option \"" +
+ option_field->full_name() + "\".");
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_UINT32:
+ if (uninterpreted_option_->has_positive_int_value()) {
+ if (uninterpreted_option_->positive_int_value() > kuint32max) {
+ return AddValueError("Value out of range for uint32 option \"" +
+ option_field->name() + "\".");
+ } else {
+ SetUInt32(option_field->number(),
+ uninterpreted_option_->positive_int_value(),
+ option_field->type(), unknown_fields);
+ }
+ } else {
+ return AddValueError("Value must be non-negative integer for uint32 "
+ "option \"" + option_field->full_name() + "\".");
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_UINT64:
+ if (uninterpreted_option_->has_positive_int_value()) {
+ SetUInt64(option_field->number(),
+ uninterpreted_option_->positive_int_value(),
+ option_field->type(), unknown_fields);
+ } else {
+ return AddValueError("Value must be non-negative integer for uint64 "
+ "option \"" + option_field->full_name() + "\".");
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_FLOAT: {
+ float value;
+ if (uninterpreted_option_->has_double_value()) {
+ value = uninterpreted_option_->double_value();
+ } else if (uninterpreted_option_->has_positive_int_value()) {
+ value = uninterpreted_option_->positive_int_value();
+ } else if (uninterpreted_option_->has_negative_int_value()) {
+ value = uninterpreted_option_->negative_int_value();
+ } else {
+ return AddValueError("Value must be number for float option \"" +
+ option_field->full_name() + "\".");
+ }
+ unknown_fields->AddFixed32(option_field->number(),
+ google::protobuf::internal::WireFormatLite::EncodeFloat(value));
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_DOUBLE: {
+ double value;
+ if (uninterpreted_option_->has_double_value()) {
+ value = uninterpreted_option_->double_value();
+ } else if (uninterpreted_option_->has_positive_int_value()) {
+ value = uninterpreted_option_->positive_int_value();
+ } else if (uninterpreted_option_->has_negative_int_value()) {
+ value = uninterpreted_option_->negative_int_value();
+ } else {
+ return AddValueError("Value must be number for double option \"" +
+ option_field->full_name() + "\".");
+ }
+ unknown_fields->AddFixed64(option_field->number(),
+ google::protobuf::internal::WireFormatLite::EncodeDouble(value));
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_BOOL:
+ uint64 value;
+ if (!uninterpreted_option_->has_identifier_value()) {
+ return AddValueError("Value must be identifier for boolean option "
+ "\"" + option_field->full_name() + "\".");
+ }
+ if (uninterpreted_option_->identifier_value() == "true") {
+ value = 1;
+ } else if (uninterpreted_option_->identifier_value() == "false") {
+ value = 0;
+ } else {
+ return AddValueError("Value must be \"true\" or \"false\" for boolean "
+ "option \"" + option_field->full_name() + "\".");
+ }
+ unknown_fields->AddVarint(option_field->number(), value);
+ break;
+
+ case FieldDescriptor::CPPTYPE_ENUM: {
+ if (!uninterpreted_option_->has_identifier_value()) {
+ return AddValueError("Value must be identifier for enum-valued option "
+ "\"" + option_field->full_name() + "\".");
+ }
+ const EnumDescriptor* enum_type = option_field->enum_type();
+ const string& value_name = uninterpreted_option_->identifier_value();
+ const EnumValueDescriptor* enum_value = NULL;
+
+ if (enum_type->file()->pool() != DescriptorPool::generated_pool()) {
+ // Note that the enum value's fully-qualified name is a sibling of the
+ // enum's name, not a child of it.
+ string fully_qualified_name = enum_type->full_name();
+ fully_qualified_name.resize(fully_qualified_name.size() -
+ enum_type->name().size());
+ fully_qualified_name += value_name;
+
+ // Search for the enum value's descriptor in the builder's pool. Note
+ // that we use DescriptorBuilder::FindSymbolNotEnforcingDeps(), not
+ // DescriptorPool::FindEnumValueByName() because we're already holding
+ // the pool's mutex, and the latter method locks it again.
+ Symbol symbol =
+ builder_->FindSymbolNotEnforcingDeps(fully_qualified_name);
+ if (!symbol.IsNull() && symbol.type == Symbol::ENUM_VALUE) {
+ if (symbol.enum_value_descriptor->type() != enum_type) {
+ return AddValueError("Enum type \"" + enum_type->full_name() +
+ "\" has no value named \"" + value_name + "\" for option \"" +
+ option_field->full_name() +
+ "\". This appears to be a value from a sibling type.");
+ } else {
+ enum_value = symbol.enum_value_descriptor;
+ }
+ }
+ } else {
+ // The enum type is in the generated pool, so we can search for the
+ // value there.
+ enum_value = enum_type->FindValueByName(value_name);
+ }
+
+ if (enum_value == NULL) {
+ return AddValueError("Enum type \"" +
+ option_field->enum_type()->full_name() +
+ "\" has no value named \"" + value_name + "\" for "
+ "option \"" + option_field->full_name() + "\".");
+ } else {
+ // Sign-extension is not a problem, since we cast directly from int32 to
+ // uint64, without first going through uint32.
+ unknown_fields->AddVarint(option_field->number(),
+ static_cast<uint64>(static_cast<int64>(enum_value->number())));
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ if (!uninterpreted_option_->has_string_value()) {
+ return AddValueError("Value must be quoted string for string option "
+ "\"" + option_field->full_name() + "\".");
+ }
+ // The string has already been unquoted and unescaped by the parser.
+ unknown_fields->AddLengthDelimited(option_field->number(),
+ uninterpreted_option_->string_value());
+ break;
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ if (!SetAggregateOption(option_field, unknown_fields)) {
+ return false;
+ }
+ break;
+ }
+
+ return true;
+}
+
+class DescriptorBuilder::OptionInterpreter::AggregateOptionFinder
+ : public TextFormat::Finder {
+ public:
+ DescriptorBuilder* builder_;
+
+ virtual const FieldDescriptor* FindExtension(
+ Message* message, const string& name) const {
+ assert_mutex_held(builder_->pool_);
+ const Descriptor* descriptor = message->GetDescriptor();
+ Symbol result = builder_->LookupSymbolNoPlaceholder(
+ name, descriptor->full_name());
+ if (result.type == Symbol::FIELD &&
+ result.field_descriptor->is_extension()) {
+ return result.field_descriptor;
+ } else if (result.type == Symbol::MESSAGE &&
+ descriptor->options().message_set_wire_format()) {
+ const Descriptor* foreign_type = result.descriptor;
+ // The text format allows MessageSet items to be specified using
+ // the type name, rather than the extension identifier. If the symbol
+ // lookup returned a Message, and the enclosing Message has
+ // message_set_wire_format = true, then return the message set
+ // extension, if one exists.
+ for (int i = 0; i < foreign_type->extension_count(); i++) {
+ const FieldDescriptor* extension = foreign_type->extension(i);
+ if (extension->containing_type() == descriptor &&
+ extension->type() == FieldDescriptor::TYPE_MESSAGE &&
+ extension->is_optional() &&
+ extension->message_type() == foreign_type) {
+ // Found it.
+ return extension;
+ }
+ }
+ }
+ return NULL;
+ }
+};
+
+// A custom error collector to record any text-format parsing errors
+namespace {
+class AggregateErrorCollector : public io::ErrorCollector {
+ public:
+ string error_;
+
+ virtual void AddError(int /* line */, int /* column */,
+ const string& message) {
+ if (!error_.empty()) {
+ error_ += "; ";
+ }
+ error_ += message;
+ }
+
+ virtual void AddWarning(int /* line */, int /* column */,
+ const string& /* message */) {
+ // Ignore warnings
+ }
+};
+}
+
+// We construct a dynamic message of the type corresponding to
+// option_field, parse the supplied text-format string into this
+// message, and serialize the resulting message to produce the value.
+bool DescriptorBuilder::OptionInterpreter::SetAggregateOption(
+ const FieldDescriptor* option_field,
+ UnknownFieldSet* unknown_fields) {
+ if (!uninterpreted_option_->has_aggregate_value()) {
+ return AddValueError("Option \"" + option_field->full_name() +
+ "\" is a message. To set the entire message, use "
+ "syntax like \"" + option_field->name() +
+ " = { <proto text format> }\". "
+ "To set fields within it, use "
+ "syntax like \"" + option_field->name() +
+ ".foo = value\".");
+ }
+
+ const Descriptor* type = option_field->message_type();
+ scoped_ptr<Message> dynamic(dynamic_factory_.GetPrototype(type)->New());
+ GOOGLE_CHECK(dynamic.get() != NULL)
+ << "Could not create an instance of " << option_field->DebugString();
+
+ AggregateErrorCollector collector;
+ AggregateOptionFinder finder;
+ finder.builder_ = builder_;
+ TextFormat::Parser parser;
+ parser.RecordErrorsTo(&collector);
+ parser.SetFinder(&finder);
+ if (!parser.ParseFromString(uninterpreted_option_->aggregate_value(),
+ dynamic.get())) {
+ AddValueError("Error while parsing option value for \"" +
+ option_field->name() + "\": " + collector.error_);
+ return false;
+ } else {
+ string serial;
+ dynamic->SerializeToString(&serial); // Never fails
+ if (option_field->type() == FieldDescriptor::TYPE_MESSAGE) {
+ unknown_fields->AddLengthDelimited(option_field->number(), serial);
+ } else {
+ GOOGLE_CHECK_EQ(option_field->type(), FieldDescriptor::TYPE_GROUP);
+ UnknownFieldSet* group = unknown_fields->AddGroup(option_field->number());
+ group->ParseFromString(serial);
+ }
+ return true;
+ }
+}
+
+void DescriptorBuilder::OptionInterpreter::SetInt32(int number, int32 value,
+ FieldDescriptor::Type type, UnknownFieldSet* unknown_fields) {
+ switch (type) {
+ case FieldDescriptor::TYPE_INT32:
+ unknown_fields->AddVarint(number,
+ static_cast<uint64>(static_cast<int64>(value)));
+ break;
+
+ case FieldDescriptor::TYPE_SFIXED32:
+ unknown_fields->AddFixed32(number, static_cast<uint32>(value));
+ break;
+
+ case FieldDescriptor::TYPE_SINT32:
+ unknown_fields->AddVarint(number,
+ google::protobuf::internal::WireFormatLite::ZigZagEncode32(value));
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Invalid wire type for CPPTYPE_INT32: " << type;
+ break;
+ }
+}
+
+void DescriptorBuilder::OptionInterpreter::SetInt64(int number, int64 value,
+ FieldDescriptor::Type type, UnknownFieldSet* unknown_fields) {
+ switch (type) {
+ case FieldDescriptor::TYPE_INT64:
+ unknown_fields->AddVarint(number, static_cast<uint64>(value));
+ break;
+
+ case FieldDescriptor::TYPE_SFIXED64:
+ unknown_fields->AddFixed64(number, static_cast<uint64>(value));
+ break;
+
+ case FieldDescriptor::TYPE_SINT64:
+ unknown_fields->AddVarint(number,
+ google::protobuf::internal::WireFormatLite::ZigZagEncode64(value));
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Invalid wire type for CPPTYPE_INT64: " << type;
+ break;
+ }
+}
+
+void DescriptorBuilder::OptionInterpreter::SetUInt32(int number, uint32 value,
+ FieldDescriptor::Type type, UnknownFieldSet* unknown_fields) {
+ switch (type) {
+ case FieldDescriptor::TYPE_UINT32:
+ unknown_fields->AddVarint(number, static_cast<uint64>(value));
+ break;
+
+ case FieldDescriptor::TYPE_FIXED32:
+ unknown_fields->AddFixed32(number, static_cast<uint32>(value));
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Invalid wire type for CPPTYPE_UINT32: " << type;
+ break;
+ }
+}
+
+void DescriptorBuilder::OptionInterpreter::SetUInt64(int number, uint64 value,
+ FieldDescriptor::Type type, UnknownFieldSet* unknown_fields) {
+ switch (type) {
+ case FieldDescriptor::TYPE_UINT64:
+ unknown_fields->AddVarint(number, value);
+ break;
+
+ case FieldDescriptor::TYPE_FIXED64:
+ unknown_fields->AddFixed64(number, value);
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Invalid wire type for CPPTYPE_UINT64: " << type;
+ break;
+ }
+}
+
+void DescriptorBuilder::LogUnusedDependency(const FileDescriptor* result) {
+
+ if (!unused_dependency_.empty()) {
+ std::set<string> annotation_extensions;
+ annotation_extensions.insert("google.protobuf.MessageOptions");
+ annotation_extensions.insert("google.protobuf.FileOptions");
+ annotation_extensions.insert("google.protobuf.FieldOptions");
+ annotation_extensions.insert("google.protobuf.EnumOptions");
+ annotation_extensions.insert("google.protobuf.EnumValueOptions");
+ annotation_extensions.insert("google.protobuf.ServiceOptions");
+ annotation_extensions.insert("google.protobuf.MethodOptions");
+ annotation_extensions.insert("google.protobuf.StreamOptions");
+ for (set<const FileDescriptor*>::const_iterator
+ it = unused_dependency_.begin();
+ it != unused_dependency_.end(); ++it) {
+ // Do not log warnings for proto files which extend annotations.
+ int i;
+ for (i = 0 ; i < (*it)->extension_count(); ++i) {
+ if (annotation_extensions.find(
+ (*it)->extension(i)->containing_type()->full_name())
+ != annotation_extensions.end()) {
+ break;
+ }
+ }
+ // Log warnings for unused imported files.
+ if (i == (*it)->extension_count()) {
+ GOOGLE_LOG(WARNING) << "Warning: Unused import: \"" << result->name()
+ << "\" imports \"" << (*it)->name()
+ << "\" which is not used.";
+ }
+ }
+ }
+}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor.h b/toolkit/components/protobuf/src/google/protobuf/descriptor.h
new file mode 100644
index 0000000000..67afc774db
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor.h
@@ -0,0 +1,1691 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file contains classes which describe a type of protocol message.
+// You can use a message's descriptor to learn at runtime what fields
+// it contains and what the types of those fields are. The Message
+// interface also allows you to dynamically access and modify individual
+// fields by passing the FieldDescriptor of the field you are interested
+// in.
+//
+// Most users will not care about descriptors, because they will write
+// code specific to certain protocol types and will simply use the classes
+// generated by the protocol compiler directly. Advanced users who want
+// to operate on arbitrary types (not known at compile time) may want to
+// read descriptors in order to learn about the contents of a message.
+// A very small number of users will want to construct their own
+// Descriptors, either because they are implementing Message manually or
+// because they are writing something like the protocol compiler.
+//
+// For an example of how you might use descriptors, see the code example
+// at the top of message.h.
+
+#ifndef GOOGLE_PROTOBUF_DESCRIPTOR_H__
+#define GOOGLE_PROTOBUF_DESCRIPTOR_H__
+
+#include <set>
+#include <string>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+
+
+namespace google {
+namespace protobuf {
+
+// Defined in this file.
+class Descriptor;
+class FieldDescriptor;
+class OneofDescriptor;
+class EnumDescriptor;
+class EnumValueDescriptor;
+class ServiceDescriptor;
+class MethodDescriptor;
+class FileDescriptor;
+class DescriptorDatabase;
+class DescriptorPool;
+
+// Defined in descriptor.proto
+class DescriptorProto;
+class FieldDescriptorProto;
+class OneofDescriptorProto;
+class EnumDescriptorProto;
+class EnumValueDescriptorProto;
+class ServiceDescriptorProto;
+class MethodDescriptorProto;
+class FileDescriptorProto;
+class MessageOptions;
+class FieldOptions;
+class EnumOptions;
+class EnumValueOptions;
+class ServiceOptions;
+class MethodOptions;
+class FileOptions;
+class UninterpretedOption;
+class SourceCodeInfo;
+
+// Defined in message.h
+class Message;
+
+// Defined in descriptor.cc
+class DescriptorBuilder;
+class FileDescriptorTables;
+
+// Defined in unknown_field_set.h.
+class UnknownField;
+
+// NB, all indices are zero-based.
+struct SourceLocation {
+ int start_line;
+ int end_line;
+ int start_column;
+ int end_column;
+
+ // Doc comments found at the source location.
+ // TODO(kenton): Maybe this struct should have been named SourceInfo or
+ // something instead. Oh well.
+ string leading_comments;
+ string trailing_comments;
+};
+
+// Describes a type of protocol message, or a particular group within a
+// message. To obtain the Descriptor for a given message object, call
+// Message::GetDescriptor(). Generated message classes also have a
+// static method called descriptor() which returns the type's descriptor.
+// Use DescriptorPool to construct your own descriptors.
+class LIBPROTOBUF_EXPORT Descriptor {
+ public:
+ // The name of the message type, not including its scope.
+ const string& name() const;
+
+ // The fully-qualified name of the message type, scope delimited by
+ // periods. For example, message type "Foo" which is declared in package
+ // "bar" has full name "bar.Foo". If a type "Baz" is nested within
+ // Foo, Baz's full_name is "bar.Foo.Baz". To get only the part that
+ // comes after the last '.', use name().
+ const string& full_name() const;
+
+ // Index of this descriptor within the file or containing type's message
+ // type array.
+ int index() const;
+
+ // The .proto file in which this message type was defined. Never NULL.
+ const FileDescriptor* file() const;
+
+ // If this Descriptor describes a nested type, this returns the type
+ // in which it is nested. Otherwise, returns NULL.
+ const Descriptor* containing_type() const;
+
+ // Get options for this message type. These are specified in the .proto file
+ // by placing lines like "option foo = 1234;" in the message definition.
+ // Allowed options are defined by MessageOptions in
+ // google/protobuf/descriptor.proto, and any available extensions of that
+ // message.
+ const MessageOptions& options() const;
+
+ // Write the contents of this Descriptor into the given DescriptorProto.
+ // The target DescriptorProto must be clear before calling this; if it
+ // isn't, the result may be garbage.
+ void CopyTo(DescriptorProto* proto) const;
+
+ // Write the contents of this decriptor in a human-readable form. Output
+ // will be suitable for re-parsing.
+ string DebugString() const;
+
+ // Returns true if this is a placeholder for an unknown type. This will
+ // only be the case if this descriptor comes from a DescriptorPool
+ // with AllowUnknownDependencies() set.
+ bool is_placeholder() const;
+
+ // Field stuff -----------------------------------------------------
+
+ // The number of fields in this message type.
+ int field_count() const;
+ // Gets a field by index, where 0 <= index < field_count().
+ // These are returned in the order they were defined in the .proto file.
+ const FieldDescriptor* field(int index) const;
+
+ // Looks up a field by declared tag number. Returns NULL if no such field
+ // exists.
+ const FieldDescriptor* FindFieldByNumber(int number) const;
+ // Looks up a field by name. Returns NULL if no such field exists.
+ const FieldDescriptor* FindFieldByName(const string& name) const;
+
+ // Looks up a field by lowercased name (as returned by lowercase_name()).
+ // This lookup may be ambiguous if multiple field names differ only by case,
+ // in which case the field returned is chosen arbitrarily from the matches.
+ const FieldDescriptor* FindFieldByLowercaseName(
+ const string& lowercase_name) const;
+
+ // Looks up a field by camel-case name (as returned by camelcase_name()).
+ // This lookup may be ambiguous if multiple field names differ in a way that
+ // leads them to have identical camel-case names, in which case the field
+ // returned is chosen arbitrarily from the matches.
+ const FieldDescriptor* FindFieldByCamelcaseName(
+ const string& camelcase_name) const;
+
+ // The number of oneofs in this message type.
+ int oneof_decl_count() const;
+ // Get a oneof by index, where 0 <= index < oneof_decl_count().
+ // These are returned in the order they were defined in the .proto file.
+ const OneofDescriptor* oneof_decl(int index) const;
+
+ // Looks up a oneof by name. Returns NULL if no such oneof exists.
+ const OneofDescriptor* FindOneofByName(const string& name) const;
+
+ // Nested type stuff -----------------------------------------------
+
+ // The number of nested types in this message type.
+ int nested_type_count() const;
+ // Gets a nested type by index, where 0 <= index < nested_type_count().
+ // These are returned in the order they were defined in the .proto file.
+ const Descriptor* nested_type(int index) const;
+
+ // Looks up a nested type by name. Returns NULL if no such nested type
+ // exists.
+ const Descriptor* FindNestedTypeByName(const string& name) const;
+
+ // Enum stuff ------------------------------------------------------
+
+ // The number of enum types in this message type.
+ int enum_type_count() const;
+ // Gets an enum type by index, where 0 <= index < enum_type_count().
+ // These are returned in the order they were defined in the .proto file.
+ const EnumDescriptor* enum_type(int index) const;
+
+ // Looks up an enum type by name. Returns NULL if no such enum type exists.
+ const EnumDescriptor* FindEnumTypeByName(const string& name) const;
+
+ // Looks up an enum value by name, among all enum types in this message.
+ // Returns NULL if no such value exists.
+ const EnumValueDescriptor* FindEnumValueByName(const string& name) const;
+
+ // Extensions ------------------------------------------------------
+
+ // A range of field numbers which are designated for third-party
+ // extensions.
+ struct ExtensionRange {
+ int start; // inclusive
+ int end; // exclusive
+ };
+
+ // The number of extension ranges in this message type.
+ int extension_range_count() const;
+ // Gets an extension range by index, where 0 <= index <
+ // extension_range_count(). These are returned in the order they were defined
+ // in the .proto file.
+ const ExtensionRange* extension_range(int index) const;
+
+ // Returns true if the number is in one of the extension ranges.
+ bool IsExtensionNumber(int number) const;
+
+ // Returns NULL if no extension range contains the given number.
+ const ExtensionRange* FindExtensionRangeContainingNumber(int number) const;
+
+ // The number of extensions -- extending *other* messages -- that were
+ // defined nested within this message type's scope.
+ int extension_count() const;
+ // Get an extension by index, where 0 <= index < extension_count().
+ // These are returned in the order they were defined in the .proto file.
+ const FieldDescriptor* extension(int index) const;
+
+ // Looks up a named extension (which extends some *other* message type)
+ // defined within this message type's scope.
+ const FieldDescriptor* FindExtensionByName(const string& name) const;
+
+ // Similar to FindFieldByLowercaseName(), but finds extensions defined within
+ // this message type's scope.
+ const FieldDescriptor* FindExtensionByLowercaseName(const string& name) const;
+
+ // Similar to FindFieldByCamelcaseName(), but finds extensions defined within
+ // this message type's scope.
+ const FieldDescriptor* FindExtensionByCamelcaseName(const string& name) const;
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this message declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ typedef MessageOptions OptionsType;
+
+ // Internal version of DebugString; controls the level of indenting for
+ // correct depth
+ void DebugString(int depth, string *contents) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ const FileDescriptor* file_;
+ const Descriptor* containing_type_;
+ const MessageOptions* options_;
+
+ // True if this is a placeholder for an unknown type.
+ bool is_placeholder_;
+ // True if this is a placeholder and the type name wasn't fully-qualified.
+ bool is_unqualified_placeholder_;
+
+ int field_count_;
+ FieldDescriptor* fields_;
+ int oneof_decl_count_;
+ OneofDescriptor* oneof_decls_;
+ int nested_type_count_;
+ Descriptor* nested_types_;
+ int enum_type_count_;
+ EnumDescriptor* enum_types_;
+ int extension_range_count_;
+ ExtensionRange* extension_ranges_;
+ int extension_count_;
+ FieldDescriptor* extensions_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<Descriptor>() and AllocateArray<Descriptor>() in descriptor.cc
+ // and update them to initialize the field.
+
+ // Must be constructed using DescriptorPool.
+ Descriptor() {}
+ friend class DescriptorBuilder;
+ friend class EnumDescriptor;
+ friend class FieldDescriptor;
+ friend class OneofDescriptor;
+ friend class MethodDescriptor;
+ friend class FileDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Descriptor);
+};
+
+// Describes a single field of a message. To get the descriptor for a given
+// field, first get the Descriptor for the message in which it is defined,
+// then call Descriptor::FindFieldByName(). To get a FieldDescriptor for
+// an extension, do one of the following:
+// - Get the Descriptor or FileDescriptor for its containing scope, then
+// call Descriptor::FindExtensionByName() or
+// FileDescriptor::FindExtensionByName().
+// - Given a DescriptorPool, call DescriptorPool::FindExtensionByNumber().
+// - Given a Reflection for a message object, call
+// Reflection::FindKnownExtensionByName() or
+// Reflection::FindKnownExtensionByNumber().
+// Use DescriptorPool to construct your own descriptors.
+class LIBPROTOBUF_EXPORT FieldDescriptor {
+ public:
+ // Identifies a field type. 0 is reserved for errors. The order is weird
+ // for historical reasons. Types 12 and up are new in proto2.
+ enum Type {
+ TYPE_DOUBLE = 1, // double, exactly eight bytes on the wire.
+ TYPE_FLOAT = 2, // float, exactly four bytes on the wire.
+ TYPE_INT64 = 3, // int64, varint on the wire. Negative numbers
+ // take 10 bytes. Use TYPE_SINT64 if negative
+ // values are likely.
+ TYPE_UINT64 = 4, // uint64, varint on the wire.
+ TYPE_INT32 = 5, // int32, varint on the wire. Negative numbers
+ // take 10 bytes. Use TYPE_SINT32 if negative
+ // values are likely.
+ TYPE_FIXED64 = 6, // uint64, exactly eight bytes on the wire.
+ TYPE_FIXED32 = 7, // uint32, exactly four bytes on the wire.
+ TYPE_BOOL = 8, // bool, varint on the wire.
+ TYPE_STRING = 9, // UTF-8 text.
+ TYPE_GROUP = 10, // Tag-delimited message. Deprecated.
+ TYPE_MESSAGE = 11, // Length-delimited message.
+
+ TYPE_BYTES = 12, // Arbitrary byte array.
+ TYPE_UINT32 = 13, // uint32, varint on the wire
+ TYPE_ENUM = 14, // Enum, varint on the wire
+ TYPE_SFIXED32 = 15, // int32, exactly four bytes on the wire
+ TYPE_SFIXED64 = 16, // int64, exactly eight bytes on the wire
+ TYPE_SINT32 = 17, // int32, ZigZag-encoded varint on the wire
+ TYPE_SINT64 = 18, // int64, ZigZag-encoded varint on the wire
+
+ MAX_TYPE = 18, // Constant useful for defining lookup tables
+ // indexed by Type.
+ };
+
+ // Specifies the C++ data type used to represent the field. There is a
+ // fixed mapping from Type to CppType where each Type maps to exactly one
+ // CppType. 0 is reserved for errors.
+ enum CppType {
+ CPPTYPE_INT32 = 1, // TYPE_INT32, TYPE_SINT32, TYPE_SFIXED32
+ CPPTYPE_INT64 = 2, // TYPE_INT64, TYPE_SINT64, TYPE_SFIXED64
+ CPPTYPE_UINT32 = 3, // TYPE_UINT32, TYPE_FIXED32
+ CPPTYPE_UINT64 = 4, // TYPE_UINT64, TYPE_FIXED64
+ CPPTYPE_DOUBLE = 5, // TYPE_DOUBLE
+ CPPTYPE_FLOAT = 6, // TYPE_FLOAT
+ CPPTYPE_BOOL = 7, // TYPE_BOOL
+ CPPTYPE_ENUM = 8, // TYPE_ENUM
+ CPPTYPE_STRING = 9, // TYPE_STRING, TYPE_BYTES
+ CPPTYPE_MESSAGE = 10, // TYPE_MESSAGE, TYPE_GROUP
+
+ MAX_CPPTYPE = 10, // Constant useful for defining lookup tables
+ // indexed by CppType.
+ };
+
+ // Identifies whether the field is optional, required, or repeated. 0 is
+ // reserved for errors.
+ enum Label {
+ LABEL_OPTIONAL = 1, // optional
+ LABEL_REQUIRED = 2, // required
+ LABEL_REPEATED = 3, // repeated
+
+ MAX_LABEL = 3, // Constant useful for defining lookup tables
+ // indexed by Label.
+ };
+
+ // Valid field numbers are positive integers up to kMaxNumber.
+ static const int kMaxNumber = (1 << 29) - 1;
+
+ // First field number reserved for the protocol buffer library implementation.
+ // Users may not declare fields that use reserved numbers.
+ static const int kFirstReservedNumber = 19000;
+ // Last field number reserved for the protocol buffer library implementation.
+ // Users may not declare fields that use reserved numbers.
+ static const int kLastReservedNumber = 19999;
+
+ const string& name() const; // Name of this field within the message.
+ const string& full_name() const; // Fully-qualified name of the field.
+ const FileDescriptor* file() const;// File in which this field was defined.
+ bool is_extension() const; // Is this an extension field?
+ int number() const; // Declared tag number.
+
+ // Same as name() except converted to lower-case. This (and especially the
+ // FindFieldByLowercaseName() method) can be useful when parsing formats
+ // which prefer to use lowercase naming style. (Although, technically
+ // field names should be lowercased anyway according to the protobuf style
+ // guide, so this only makes a difference when dealing with old .proto files
+ // which do not follow the guide.)
+ const string& lowercase_name() const;
+
+ // Same as name() except converted to camel-case. In this conversion, any
+ // time an underscore appears in the name, it is removed and the next
+ // letter is capitalized. Furthermore, the first letter of the name is
+ // lower-cased. Examples:
+ // FooBar -> fooBar
+ // foo_bar -> fooBar
+ // fooBar -> fooBar
+ // This (and especially the FindFieldByCamelcaseName() method) can be useful
+ // when parsing formats which prefer to use camel-case naming style.
+ const string& camelcase_name() const;
+
+ Type type() const; // Declared type of this field.
+ const char* type_name() const; // Name of the declared type.
+ CppType cpp_type() const; // C++ type of this field.
+ const char* cpp_type_name() const; // Name of the C++ type.
+ Label label() const; // optional/required/repeated
+
+ bool is_required() const; // shorthand for label() == LABEL_REQUIRED
+ bool is_optional() const; // shorthand for label() == LABEL_OPTIONAL
+ bool is_repeated() const; // shorthand for label() == LABEL_REPEATED
+ bool is_packable() const; // shorthand for is_repeated() &&
+ // IsTypePackable(type())
+ bool is_packed() const; // shorthand for is_packable() &&
+ // options().packed()
+
+ // Index of this field within the message's field array, or the file or
+ // extension scope's extensions array.
+ int index() const;
+
+ // Does this field have an explicitly-declared default value?
+ bool has_default_value() const;
+
+ // Get the field default value if cpp_type() == CPPTYPE_INT32. If no
+ // explicit default was defined, the default is 0.
+ int32 default_value_int32() const;
+ // Get the field default value if cpp_type() == CPPTYPE_INT64. If no
+ // explicit default was defined, the default is 0.
+ int64 default_value_int64() const;
+ // Get the field default value if cpp_type() == CPPTYPE_UINT32. If no
+ // explicit default was defined, the default is 0.
+ uint32 default_value_uint32() const;
+ // Get the field default value if cpp_type() == CPPTYPE_UINT64. If no
+ // explicit default was defined, the default is 0.
+ uint64 default_value_uint64() const;
+ // Get the field default value if cpp_type() == CPPTYPE_FLOAT. If no
+ // explicit default was defined, the default is 0.0.
+ float default_value_float() const;
+ // Get the field default value if cpp_type() == CPPTYPE_DOUBLE. If no
+ // explicit default was defined, the default is 0.0.
+ double default_value_double() const;
+ // Get the field default value if cpp_type() == CPPTYPE_BOOL. If no
+ // explicit default was defined, the default is false.
+ bool default_value_bool() const;
+ // Get the field default value if cpp_type() == CPPTYPE_ENUM. If no
+ // explicit default was defined, the default is the first value defined
+ // in the enum type (all enum types are required to have at least one value).
+ // This never returns NULL.
+ const EnumValueDescriptor* default_value_enum() const;
+ // Get the field default value if cpp_type() == CPPTYPE_STRING. If no
+ // explicit default was defined, the default is the empty string.
+ const string& default_value_string() const;
+
+ // The Descriptor for the message of which this is a field. For extensions,
+ // this is the extended type. Never NULL.
+ const Descriptor* containing_type() const;
+
+ // If the field is a member of a oneof, this is the one, otherwise this is
+ // NULL.
+ const OneofDescriptor* containing_oneof() const;
+
+ // If the field is a member of a oneof, returns the index in that oneof.
+ int index_in_oneof() const;
+
+ // An extension may be declared within the scope of another message. If this
+ // field is an extension (is_extension() is true), then extension_scope()
+ // returns that message, or NULL if the extension was declared at global
+ // scope. If this is not an extension, extension_scope() is undefined (may
+ // assert-fail).
+ const Descriptor* extension_scope() const;
+
+ // If type is TYPE_MESSAGE or TYPE_GROUP, returns a descriptor for the
+ // message or the group type. Otherwise, returns null.
+ const Descriptor* message_type() const;
+ // If type is TYPE_ENUM, returns a descriptor for the enum. Otherwise,
+ // returns null.
+ const EnumDescriptor* enum_type() const;
+
+ // EXPERIMENTAL; DO NOT USE.
+ // If this field is a map field, experimental_map_key() is the field
+ // that is the key for this map.
+ // experimental_map_key()->containing_type() is the same as message_type().
+ const FieldDescriptor* experimental_map_key() const;
+
+ // Get the FieldOptions for this field. This includes things listed in
+ // square brackets after the field definition. E.g., the field:
+ // optional string text = 1 [ctype=CORD];
+ // has the "ctype" option set. Allowed options are defined by FieldOptions
+ // in google/protobuf/descriptor.proto, and any available extensions of that
+ // message.
+ const FieldOptions& options() const;
+
+ // See Descriptor::CopyTo().
+ void CopyTo(FieldDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Helper method to get the CppType for a particular Type.
+ static CppType TypeToCppType(Type type);
+
+ // Helper method to get the name of a Type.
+ static const char* TypeName(Type type);
+
+ // Helper method to get the name of a CppType.
+ static const char* CppTypeName(CppType cpp_type);
+
+ // Return true iff [packed = true] is valid for fields of this type.
+ static inline bool IsTypePackable(Type field_type);
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this field declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ typedef FieldOptions OptionsType;
+
+ // See Descriptor::DebugString().
+ enum PrintLabelFlag { PRINT_LABEL, OMIT_LABEL };
+ void DebugString(int depth, PrintLabelFlag print_label_flag,
+ string* contents) const;
+
+ // formats the default value appropriately and returns it as a string.
+ // Must have a default value to call this. If quote_string_type is true, then
+ // types of CPPTYPE_STRING whill be surrounded by quotes and CEscaped.
+ string DefaultValueAsString(bool quote_string_type) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ const string* lowercase_name_;
+ const string* camelcase_name_;
+ const FileDescriptor* file_;
+ int number_;
+ Type type_;
+ Label label_;
+ bool is_extension_;
+ int index_in_oneof_;
+ const Descriptor* containing_type_;
+ const OneofDescriptor* containing_oneof_;
+ const Descriptor* extension_scope_;
+ const Descriptor* message_type_;
+ const EnumDescriptor* enum_type_;
+ const FieldDescriptor* experimental_map_key_;
+ const FieldOptions* options_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<FieldDescriptor>() and AllocateArray<FieldDescriptor>() in
+ // descriptor.cc and update them to initialize the field.
+
+ bool has_default_value_;
+ union {
+ int32 default_value_int32_;
+ int64 default_value_int64_;
+ uint32 default_value_uint32_;
+ uint64 default_value_uint64_;
+ float default_value_float_;
+ double default_value_double_;
+ bool default_value_bool_;
+
+ const EnumValueDescriptor* default_value_enum_;
+ const string* default_value_string_;
+ };
+
+ static const CppType kTypeToCppTypeMap[MAX_TYPE + 1];
+
+ static const char * const kTypeToName[MAX_TYPE + 1];
+
+ static const char * const kCppTypeToName[MAX_CPPTYPE + 1];
+
+ static const char * const kLabelToName[MAX_LABEL + 1];
+
+ // Must be constructed using DescriptorPool.
+ FieldDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class FileDescriptor;
+ friend class Descriptor;
+ friend class OneofDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(FieldDescriptor);
+};
+
+// Describes a oneof defined in a message type.
+class LIBPROTOBUF_EXPORT OneofDescriptor {
+ public:
+ const string& name() const; // Name of this oneof.
+ const string& full_name() const; // Fully-qualified name of the oneof.
+
+ // Index of this oneof within the message's oneof array.
+ int index() const;
+
+ // The Descriptor for the message containing this oneof.
+ const Descriptor* containing_type() const;
+
+ // The number of (non-extension) fields which are members of this oneof.
+ int field_count() const;
+ // Get a member of this oneof, in the order in which they were declared in the
+ // .proto file. Does not include extensions.
+ const FieldDescriptor* field(int index) const;
+
+ // See Descriptor::CopyTo().
+ void CopyTo(OneofDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this oneof declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ // See Descriptor::DebugString().
+ void DebugString(int depth, string* contents) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ const Descriptor* containing_type_;
+ bool is_extendable_;
+ int field_count_;
+ const FieldDescriptor** fields_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<OneofDescriptor>() and AllocateArray<OneofDescriptor>()
+ // in descriptor.cc and update them to initialize the field.
+
+ // Must be constructed using DescriptorPool.
+ OneofDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class Descriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(OneofDescriptor);
+};
+
+// Describes an enum type defined in a .proto file. To get the EnumDescriptor
+// for a generated enum type, call TypeName_descriptor(). Use DescriptorPool
+// to construct your own descriptors.
+class LIBPROTOBUF_EXPORT EnumDescriptor {
+ public:
+ // The name of this enum type in the containing scope.
+ const string& name() const;
+
+ // The fully-qualified name of the enum type, scope delimited by periods.
+ const string& full_name() const;
+
+ // Index of this enum within the file or containing message's enum array.
+ int index() const;
+
+ // The .proto file in which this enum type was defined. Never NULL.
+ const FileDescriptor* file() const;
+
+ // The number of values for this EnumDescriptor. Guaranteed to be greater
+ // than zero.
+ int value_count() const;
+ // Gets a value by index, where 0 <= index < value_count().
+ // These are returned in the order they were defined in the .proto file.
+ const EnumValueDescriptor* value(int index) const;
+
+ // Looks up a value by name. Returns NULL if no such value exists.
+ const EnumValueDescriptor* FindValueByName(const string& name) const;
+ // Looks up a value by number. Returns NULL if no such value exists. If
+ // multiple values have this number, the first one defined is returned.
+ const EnumValueDescriptor* FindValueByNumber(int number) const;
+
+ // If this enum type is nested in a message type, this is that message type.
+ // Otherwise, NULL.
+ const Descriptor* containing_type() const;
+
+ // Get options for this enum type. These are specified in the .proto file by
+ // placing lines like "option foo = 1234;" in the enum definition. Allowed
+ // options are defined by EnumOptions in google/protobuf/descriptor.proto,
+ // and any available extensions of that message.
+ const EnumOptions& options() const;
+
+ // See Descriptor::CopyTo().
+ void CopyTo(EnumDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Returns true if this is a placeholder for an unknown enum. This will
+ // only be the case if this descriptor comes from a DescriptorPool
+ // with AllowUnknownDependencies() set.
+ bool is_placeholder() const;
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this enum declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ typedef EnumOptions OptionsType;
+
+ // See Descriptor::DebugString().
+ void DebugString(int depth, string *contents) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ const FileDescriptor* file_;
+ const Descriptor* containing_type_;
+ const EnumOptions* options_;
+
+ // True if this is a placeholder for an unknown type.
+ bool is_placeholder_;
+ // True if this is a placeholder and the type name wasn't fully-qualified.
+ bool is_unqualified_placeholder_;
+
+ int value_count_;
+ EnumValueDescriptor* values_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<EnumDescriptor>() and AllocateArray<EnumDescriptor>() in
+ // descriptor.cc and update them to initialize the field.
+
+ // Must be constructed using DescriptorPool.
+ EnumDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class Descriptor;
+ friend class FieldDescriptor;
+ friend class EnumValueDescriptor;
+ friend class FileDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(EnumDescriptor);
+};
+
+// Describes an individual enum constant of a particular type. To get the
+// EnumValueDescriptor for a given enum value, first get the EnumDescriptor
+// for its type, then use EnumDescriptor::FindValueByName() or
+// EnumDescriptor::FindValueByNumber(). Use DescriptorPool to construct
+// your own descriptors.
+class LIBPROTOBUF_EXPORT EnumValueDescriptor {
+ public:
+ const string& name() const; // Name of this enum constant.
+ int index() const; // Index within the enums's Descriptor.
+ int number() const; // Numeric value of this enum constant.
+
+ // The full_name of an enum value is a sibling symbol of the enum type.
+ // e.g. the full name of FieldDescriptorProto::TYPE_INT32 is actually
+ // "google.protobuf.FieldDescriptorProto.TYPE_INT32", NOT
+ // "google.protobuf.FieldDescriptorProto.Type.TYPE_INT32". This is to conform
+ // with C++ scoping rules for enums.
+ const string& full_name() const;
+
+ // The type of this value. Never NULL.
+ const EnumDescriptor* type() const;
+
+ // Get options for this enum value. These are specified in the .proto file
+ // by adding text like "[foo = 1234]" after an enum value definition.
+ // Allowed options are defined by EnumValueOptions in
+ // google/protobuf/descriptor.proto, and any available extensions of that
+ // message.
+ const EnumValueOptions& options() const;
+
+ // See Descriptor::CopyTo().
+ void CopyTo(EnumValueDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this enum value declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ typedef EnumValueOptions OptionsType;
+
+ // See Descriptor::DebugString().
+ void DebugString(int depth, string *contents) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ int number_;
+ const EnumDescriptor* type_;
+ const EnumValueOptions* options_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<EnumValueDescriptor>() and AllocateArray<EnumValueDescriptor>()
+ // in descriptor.cc and update them to initialize the field.
+
+ // Must be constructed using DescriptorPool.
+ EnumValueDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class EnumDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(EnumValueDescriptor);
+};
+
+// Describes an RPC service. To get the ServiceDescriptor for a service,
+// call Service::GetDescriptor(). Generated service classes also have a
+// static method called descriptor() which returns the type's
+// ServiceDescriptor. Use DescriptorPool to construct your own descriptors.
+class LIBPROTOBUF_EXPORT ServiceDescriptor {
+ public:
+ // The name of the service, not including its containing scope.
+ const string& name() const;
+ // The fully-qualified name of the service, scope delimited by periods.
+ const string& full_name() const;
+ // Index of this service within the file's services array.
+ int index() const;
+
+ // The .proto file in which this service was defined. Never NULL.
+ const FileDescriptor* file() const;
+
+ // Get options for this service type. These are specified in the .proto file
+ // by placing lines like "option foo = 1234;" in the service definition.
+ // Allowed options are defined by ServiceOptions in
+ // google/protobuf/descriptor.proto, and any available extensions of that
+ // message.
+ const ServiceOptions& options() const;
+
+ // The number of methods this service defines.
+ int method_count() const;
+ // Gets a MethodDescriptor by index, where 0 <= index < method_count().
+ // These are returned in the order they were defined in the .proto file.
+ const MethodDescriptor* method(int index) const;
+
+ // Look up a MethodDescriptor by name.
+ const MethodDescriptor* FindMethodByName(const string& name) const;
+ // See Descriptor::CopyTo().
+ void CopyTo(ServiceDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this service declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ typedef ServiceOptions OptionsType;
+
+ // See Descriptor::DebugString().
+ void DebugString(string *contents) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ const FileDescriptor* file_;
+ const ServiceOptions* options_;
+ int method_count_;
+ MethodDescriptor* methods_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<ServiceDescriptor>() and AllocateArray<ServiceDescriptor>() in
+ // descriptor.cc and update them to initialize the field.
+
+ // Must be constructed using DescriptorPool.
+ ServiceDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class FileDescriptor;
+ friend class MethodDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ServiceDescriptor);
+};
+
+// Describes an individual service method. To obtain a MethodDescriptor given
+// a service, first get its ServiceDescriptor, then call
+// ServiceDescriptor::FindMethodByName(). Use DescriptorPool to construct your
+// own descriptors.
+class LIBPROTOBUF_EXPORT MethodDescriptor {
+ public:
+ // Name of this method, not including containing scope.
+ const string& name() const;
+ // The fully-qualified name of the method, scope delimited by periods.
+ const string& full_name() const;
+ // Index within the service's Descriptor.
+ int index() const;
+
+ // Gets the service to which this method belongs. Never NULL.
+ const ServiceDescriptor* service() const;
+
+ // Gets the type of protocol message which this method accepts as input.
+ const Descriptor* input_type() const;
+ // Gets the type of protocol message which this message produces as output.
+ const Descriptor* output_type() const;
+
+ // Get options for this method. These are specified in the .proto file by
+ // placing lines like "option foo = 1234;" in curly-braces after a method
+ // declaration. Allowed options are defined by MethodOptions in
+ // google/protobuf/descriptor.proto, and any available extensions of that
+ // message.
+ const MethodOptions& options() const;
+
+ // See Descriptor::CopyTo().
+ void CopyTo(MethodDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of this method declaration. Returns false and leaves
+ // |*out_location| unchanged iff location information was not available.
+ bool GetSourceLocation(SourceLocation* out_location) const;
+
+ private:
+ typedef MethodOptions OptionsType;
+
+ // See Descriptor::DebugString().
+ void DebugString(int depth, string *contents) const;
+
+ // Walks up the descriptor tree to generate the source location path
+ // to this descriptor from the file root.
+ void GetLocationPath(vector<int>* output) const;
+
+ const string* name_;
+ const string* full_name_;
+ const ServiceDescriptor* service_;
+ const Descriptor* input_type_;
+ const Descriptor* output_type_;
+ const MethodOptions* options_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<MethodDescriptor>() and AllocateArray<MethodDescriptor>() in
+ // descriptor.cc and update them to initialize the field.
+
+ // Must be constructed using DescriptorPool.
+ MethodDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class ServiceDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MethodDescriptor);
+};
+
+
+// Describes a whole .proto file. To get the FileDescriptor for a compiled-in
+// file, get the descriptor for something defined in that file and call
+// descriptor->file(). Use DescriptorPool to construct your own descriptors.
+class LIBPROTOBUF_EXPORT FileDescriptor {
+ public:
+ // The filename, relative to the source tree.
+ // e.g. "google/protobuf/descriptor.proto"
+ const string& name() const;
+
+ // The package, e.g. "google.protobuf.compiler".
+ const string& package() const;
+
+ // The DescriptorPool in which this FileDescriptor and all its contents were
+ // allocated. Never NULL.
+ const DescriptorPool* pool() const;
+
+ // The number of files imported by this one.
+ int dependency_count() const;
+ // Gets an imported file by index, where 0 <= index < dependency_count().
+ // These are returned in the order they were defined in the .proto file.
+ const FileDescriptor* dependency(int index) const;
+
+ // The number of files public imported by this one.
+ // The public dependency list is a subset of the dependency list.
+ int public_dependency_count() const;
+ // Gets a public imported file by index, where 0 <= index <
+ // public_dependency_count().
+ // These are returned in the order they were defined in the .proto file.
+ const FileDescriptor* public_dependency(int index) const;
+
+ // The number of files that are imported for weak fields.
+ // The weak dependency list is a subset of the dependency list.
+ int weak_dependency_count() const;
+ // Gets a weak imported file by index, where 0 <= index <
+ // weak_dependency_count().
+ // These are returned in the order they were defined in the .proto file.
+ const FileDescriptor* weak_dependency(int index) const;
+
+ // Number of top-level message types defined in this file. (This does not
+ // include nested types.)
+ int message_type_count() const;
+ // Gets a top-level message type, where 0 <= index < message_type_count().
+ // These are returned in the order they were defined in the .proto file.
+ const Descriptor* message_type(int index) const;
+
+ // Number of top-level enum types defined in this file. (This does not
+ // include nested types.)
+ int enum_type_count() const;
+ // Gets a top-level enum type, where 0 <= index < enum_type_count().
+ // These are returned in the order they were defined in the .proto file.
+ const EnumDescriptor* enum_type(int index) const;
+
+ // Number of services defined in this file.
+ int service_count() const;
+ // Gets a service, where 0 <= index < service_count().
+ // These are returned in the order they were defined in the .proto file.
+ const ServiceDescriptor* service(int index) const;
+
+ // Number of extensions defined at file scope. (This does not include
+ // extensions nested within message types.)
+ int extension_count() const;
+ // Gets an extension's descriptor, where 0 <= index < extension_count().
+ // These are returned in the order they were defined in the .proto file.
+ const FieldDescriptor* extension(int index) const;
+
+ // Get options for this file. These are specified in the .proto file by
+ // placing lines like "option foo = 1234;" at the top level, outside of any
+ // other definitions. Allowed options are defined by FileOptions in
+ // google/protobuf/descriptor.proto, and any available extensions of that
+ // message.
+ const FileOptions& options() const;
+
+ // Find a top-level message type by name. Returns NULL if not found.
+ const Descriptor* FindMessageTypeByName(const string& name) const;
+ // Find a top-level enum type by name. Returns NULL if not found.
+ const EnumDescriptor* FindEnumTypeByName(const string& name) const;
+ // Find an enum value defined in any top-level enum by name. Returns NULL if
+ // not found.
+ const EnumValueDescriptor* FindEnumValueByName(const string& name) const;
+ // Find a service definition by name. Returns NULL if not found.
+ const ServiceDescriptor* FindServiceByName(const string& name) const;
+ // Find a top-level extension definition by name. Returns NULL if not found.
+ const FieldDescriptor* FindExtensionByName(const string& name) const;
+ // Similar to FindExtensionByName(), but searches by lowercased-name. See
+ // Descriptor::FindFieldByLowercaseName().
+ const FieldDescriptor* FindExtensionByLowercaseName(const string& name) const;
+ // Similar to FindExtensionByName(), but searches by camelcased-name. See
+ // Descriptor::FindFieldByCamelcaseName().
+ const FieldDescriptor* FindExtensionByCamelcaseName(const string& name) const;
+
+ // See Descriptor::CopyTo().
+ // Notes:
+ // - This method does NOT copy source code information since it is relatively
+ // large and rarely needed. See CopySourceCodeInfoTo() below.
+ void CopyTo(FileDescriptorProto* proto) const;
+ // Write the source code information of this FileDescriptor into the given
+ // FileDescriptorProto. See CopyTo() above.
+ void CopySourceCodeInfoTo(FileDescriptorProto* proto) const;
+
+ // See Descriptor::DebugString().
+ string DebugString() const;
+
+ // Returns true if this is a placeholder for an unknown file. This will
+ // only be the case if this descriptor comes from a DescriptorPool
+ // with AllowUnknownDependencies() set.
+ bool is_placeholder() const;
+
+ private:
+ // Source Location ---------------------------------------------------
+
+ // Updates |*out_location| to the source location of the complete
+ // extent of the declaration or declaration-part denoted by |path|.
+ // Returns false and leaves |*out_location| unchanged iff location
+ // information was not available. (See SourceCodeInfo for
+ // description of path encoding.)
+ bool GetSourceLocation(const vector<int>& path,
+ SourceLocation* out_location) const;
+
+ typedef FileOptions OptionsType;
+
+ const string* name_;
+ const string* package_;
+ const DescriptorPool* pool_;
+ int dependency_count_;
+ const FileDescriptor** dependencies_;
+ int public_dependency_count_;
+ int* public_dependencies_;
+ int weak_dependency_count_;
+ int* weak_dependencies_;
+ int message_type_count_;
+ Descriptor* message_types_;
+ int enum_type_count_;
+ EnumDescriptor* enum_types_;
+ int service_count_;
+ ServiceDescriptor* services_;
+ int extension_count_;
+ bool is_placeholder_;
+ FieldDescriptor* extensions_;
+ const FileOptions* options_;
+
+ const FileDescriptorTables* tables_;
+ const SourceCodeInfo* source_code_info_;
+ // IMPORTANT: If you add a new field, make sure to search for all instances
+ // of Allocate<FileDescriptor>() and AllocateArray<FileDescriptor>() in
+ // descriptor.cc and update them to initialize the field.
+
+ FileDescriptor() {}
+ friend class DescriptorBuilder;
+ friend class Descriptor;
+ friend class FieldDescriptor;
+ friend class OneofDescriptor;
+ friend class EnumDescriptor;
+ friend class EnumValueDescriptor;
+ friend class MethodDescriptor;
+ friend class ServiceDescriptor;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(FileDescriptor);
+};
+
+// ===================================================================
+
+// Used to construct descriptors.
+//
+// Normally you won't want to build your own descriptors. Message classes
+// constructed by the protocol compiler will provide them for you. However,
+// if you are implementing Message on your own, or if you are writing a
+// program which can operate on totally arbitrary types and needs to load
+// them from some sort of database, you might need to.
+//
+// Since Descriptors are composed of a whole lot of cross-linked bits of
+// data that would be a pain to put together manually, the
+// DescriptorPool class is provided to make the process easier. It can
+// take a FileDescriptorProto (defined in descriptor.proto), validate it,
+// and convert it to a set of nicely cross-linked Descriptors.
+//
+// DescriptorPool also helps with memory management. Descriptors are
+// composed of many objects containing static data and pointers to each
+// other. In all likelihood, when it comes time to delete this data,
+// you'll want to delete it all at once. In fact, it is not uncommon to
+// have a whole pool of descriptors all cross-linked with each other which
+// you wish to delete all at once. This class represents such a pool, and
+// handles the memory management for you.
+//
+// You can also search for descriptors within a DescriptorPool by name, and
+// extensions by number.
+class LIBPROTOBUF_EXPORT DescriptorPool {
+ public:
+ // Create a normal, empty DescriptorPool.
+ DescriptorPool();
+
+ // Constructs a DescriptorPool that, when it can't find something among the
+ // descriptors already in the pool, looks for it in the given
+ // DescriptorDatabase.
+ // Notes:
+ // - If a DescriptorPool is constructed this way, its BuildFile*() methods
+ // must not be called (they will assert-fail). The only way to populate
+ // the pool with descriptors is to call the Find*By*() methods.
+ // - The Find*By*() methods may block the calling thread if the
+ // DescriptorDatabase blocks. This in turn means that parsing messages
+ // may block if they need to look up extensions.
+ // - The Find*By*() methods will use mutexes for thread-safety, thus making
+ // them slower even when they don't have to fall back to the database.
+ // In fact, even the Find*By*() methods of descriptor objects owned by
+ // this pool will be slower, since they will have to obtain locks too.
+ // - An ErrorCollector may optionally be given to collect validation errors
+ // in files loaded from the database. If not given, errors will be printed
+ // to GOOGLE_LOG(ERROR). Remember that files are built on-demand, so this
+ // ErrorCollector may be called from any thread that calls one of the
+ // Find*By*() methods.
+ // - The DescriptorDatabase must not be mutated during the lifetime of
+ // the DescriptorPool. Even if the client takes care to avoid data races,
+ // changes to the content of the DescriptorDatabase may not be reflected
+ // in subsequent lookups in the DescriptorPool.
+ class ErrorCollector;
+ explicit DescriptorPool(DescriptorDatabase* fallback_database,
+ ErrorCollector* error_collector = NULL);
+
+ ~DescriptorPool();
+
+ // Get a pointer to the generated pool. Generated protocol message classes
+ // which are compiled into the binary will allocate their descriptors in
+ // this pool. Do not add your own descriptors to this pool.
+ static const DescriptorPool* generated_pool();
+
+ // Find a FileDescriptor in the pool by file name. Returns NULL if not
+ // found.
+ const FileDescriptor* FindFileByName(const string& name) const;
+
+ // Find the FileDescriptor in the pool which defines the given symbol.
+ // If any of the Find*ByName() methods below would succeed, then this is
+ // equivalent to calling that method and calling the result's file() method.
+ // Otherwise this returns NULL.
+ const FileDescriptor* FindFileContainingSymbol(
+ const string& symbol_name) const;
+
+ // Looking up descriptors ------------------------------------------
+ // These find descriptors by fully-qualified name. These will find both
+ // top-level descriptors and nested descriptors. They return NULL if not
+ // found.
+
+ const Descriptor* FindMessageTypeByName(const string& name) const;
+ const FieldDescriptor* FindFieldByName(const string& name) const;
+ const FieldDescriptor* FindExtensionByName(const string& name) const;
+ const OneofDescriptor* FindOneofByName(const string& name) const;
+ const EnumDescriptor* FindEnumTypeByName(const string& name) const;
+ const EnumValueDescriptor* FindEnumValueByName(const string& name) const;
+ const ServiceDescriptor* FindServiceByName(const string& name) const;
+ const MethodDescriptor* FindMethodByName(const string& name) const;
+
+ // Finds an extension of the given type by number. The extendee must be
+ // a member of this DescriptorPool or one of its underlays.
+ const FieldDescriptor* FindExtensionByNumber(const Descriptor* extendee,
+ int number) const;
+
+ // Finds extensions of extendee. The extensions will be appended to
+ // out in an undefined order. Only extensions defined directly in
+ // this DescriptorPool or one of its underlays are guaranteed to be
+ // found: extensions defined in the fallback database might not be found
+ // depending on the database implementation.
+ void FindAllExtensions(const Descriptor* extendee,
+ vector<const FieldDescriptor*>* out) const;
+
+ // Building descriptors --------------------------------------------
+
+ // When converting a FileDescriptorProto to a FileDescriptor, various
+ // errors might be detected in the input. The caller may handle these
+ // programmatically by implementing an ErrorCollector.
+ class LIBPROTOBUF_EXPORT ErrorCollector {
+ public:
+ inline ErrorCollector() {}
+ virtual ~ErrorCollector();
+
+ // These constants specify what exact part of the construct is broken.
+ // This is useful e.g. for mapping the error back to an exact location
+ // in a .proto file.
+ enum ErrorLocation {
+ NAME, // the symbol name, or the package name for files
+ NUMBER, // field or extension range number
+ TYPE, // field type
+ EXTENDEE, // field extendee
+ DEFAULT_VALUE, // field default value
+ INPUT_TYPE, // method input type
+ OUTPUT_TYPE, // method output type
+ OPTION_NAME, // name in assignment
+ OPTION_VALUE, // value in option assignment
+ OTHER // some other problem
+ };
+
+ // Reports an error in the FileDescriptorProto. Use this function if the
+ // problem occured should interrupt building the FileDescriptorProto.
+ virtual void AddError(
+ const string& filename, // File name in which the error occurred.
+ const string& element_name, // Full name of the erroneous element.
+ const Message* descriptor, // Descriptor of the erroneous element.
+ ErrorLocation location, // One of the location constants, above.
+ const string& message // Human-readable error message.
+ ) = 0;
+
+ // Reports a warning in the FileDescriptorProto. Use this function if the
+ // problem occured should NOT interrupt building the FileDescriptorProto.
+ virtual void AddWarning(
+ const string& filename, // File name in which the error occurred.
+ const string& element_name, // Full name of the erroneous element.
+ const Message* descriptor, // Descriptor of the erroneous element.
+ ErrorLocation location, // One of the location constants, above.
+ const string& message // Human-readable error message.
+ ) {}
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ErrorCollector);
+ };
+
+ // Convert the FileDescriptorProto to real descriptors and place them in
+ // this DescriptorPool. All dependencies of the file must already be in
+ // the pool. Returns the resulting FileDescriptor, or NULL if there were
+ // problems with the input (e.g. the message was invalid, or dependencies
+ // were missing). Details about the errors are written to GOOGLE_LOG(ERROR).
+ const FileDescriptor* BuildFile(const FileDescriptorProto& proto);
+
+ // Same as BuildFile() except errors are sent to the given ErrorCollector.
+ const FileDescriptor* BuildFileCollectingErrors(
+ const FileDescriptorProto& proto,
+ ErrorCollector* error_collector);
+
+ // By default, it is an error if a FileDescriptorProto contains references
+ // to types or other files that are not found in the DescriptorPool (or its
+ // backing DescriptorDatabase, if any). If you call
+ // AllowUnknownDependencies(), however, then unknown types and files
+ // will be replaced by placeholder descriptors (which can be identified by
+ // the is_placeholder() method). This can allow you to
+ // perform some useful operations with a .proto file even if you do not
+ // have access to other .proto files on which it depends. However, some
+ // heuristics must be used to fill in the gaps in information, and these
+ // can lead to descriptors which are inaccurate. For example, the
+ // DescriptorPool may be forced to guess whether an unknown type is a message
+ // or an enum, as well as what package it resides in. Furthermore,
+ // placeholder types will not be discoverable via FindMessageTypeByName()
+ // and similar methods, which could confuse some descriptor-based algorithms.
+ // Generally, the results of this option should be handled with extreme care.
+ void AllowUnknownDependencies() { allow_unknown_ = true; }
+
+ // By default, weak imports are allowed to be missing, in which case we will
+ // use a placeholder for the dependency and convert the field to be an Empty
+ // message field. If you call EnforceWeakDependencies(true), however, the
+ // DescriptorPool will report a import not found error.
+ void EnforceWeakDependencies(bool enforce) { enforce_weak_ = enforce; }
+
+ // Internal stuff --------------------------------------------------
+ // These methods MUST NOT be called from outside the proto2 library.
+ // These methods may contain hidden pitfalls and may be removed in a
+ // future library version.
+
+ // Create a DescriptorPool which is overlaid on top of some other pool.
+ // If you search for a descriptor in the overlay and it is not found, the
+ // underlay will be searched as a backup. If the underlay has its own
+ // underlay, that will be searched next, and so on. This also means that
+ // files built in the overlay will be cross-linked with the underlay's
+ // descriptors if necessary. The underlay remains property of the caller;
+ // it must remain valid for the lifetime of the newly-constructed pool.
+ //
+ // Example: Say you want to parse a .proto file at runtime in order to use
+ // its type with a DynamicMessage. Say this .proto file has dependencies,
+ // but you know that all the dependencies will be things that are already
+ // compiled into the binary. For ease of use, you'd like to load the types
+ // right out of generated_pool() rather than have to parse redundant copies
+ // of all these .protos and runtime. But, you don't want to add the parsed
+ // types directly into generated_pool(): this is not allowed, and would be
+ // bad design anyway. So, instead, you could use generated_pool() as an
+ // underlay for a new DescriptorPool in which you add only the new file.
+ //
+ // WARNING: Use of underlays can lead to many subtle gotchas. Instead,
+ // try to formulate what you want to do in terms of DescriptorDatabases.
+ explicit DescriptorPool(const DescriptorPool* underlay);
+
+ // Called by generated classes at init time to add their descriptors to
+ // generated_pool. Do NOT call this in your own code! filename must be a
+ // permanent string (e.g. a string literal).
+ static void InternalAddGeneratedFile(
+ const void* encoded_file_descriptor, int size);
+
+
+ // For internal use only: Gets a non-const pointer to the generated pool.
+ // This is called at static-initialization time only, so thread-safety is
+ // not a concern. If both an underlay and a fallback database are present,
+ // the underlay takes precedence.
+ static DescriptorPool* internal_generated_pool();
+
+ // For internal use only: Changes the behavior of BuildFile() such that it
+ // allows the file to make reference to message types declared in other files
+ // which it did not officially declare as dependencies.
+ void InternalDontEnforceDependencies();
+
+ // For internal use only.
+ void internal_set_underlay(const DescriptorPool* underlay) {
+ underlay_ = underlay;
+ }
+
+ // For internal (unit test) use only: Returns true if a FileDescriptor has
+ // been constructed for the given file, false otherwise. Useful for testing
+ // lazy descriptor initialization behavior.
+ bool InternalIsFileLoaded(const string& filename) const;
+
+
+ // Add a file to unused_import_track_files_. DescriptorBuilder will log
+ // warnings for those files if there is any unused import.
+ void AddUnusedImportTrackFile(const string& file_name);
+ void ClearUnusedImportTrackFiles();
+
+ private:
+ friend class Descriptor;
+ friend class FieldDescriptor;
+ friend class EnumDescriptor;
+ friend class ServiceDescriptor;
+ friend class FileDescriptor;
+ friend class DescriptorBuilder;
+
+ // Return true if the given name is a sub-symbol of any non-package
+ // descriptor that already exists in the descriptor pool. (The full
+ // definition of such types is already known.)
+ bool IsSubSymbolOfBuiltType(const string& name) const;
+
+ // Tries to find something in the fallback database and link in the
+ // corresponding proto file. Returns true if successful, in which case
+ // the caller should search for the thing again. These are declared
+ // const because they are called by (semantically) const methods.
+ bool TryFindFileInFallbackDatabase(const string& name) const;
+ bool TryFindSymbolInFallbackDatabase(const string& name) const;
+ bool TryFindExtensionInFallbackDatabase(const Descriptor* containing_type,
+ int field_number) const;
+
+ // Like BuildFile() but called internally when the file has been loaded from
+ // fallback_database_. Declared const because it is called by (semantically)
+ // const methods.
+ const FileDescriptor* BuildFileFromDatabase(
+ const FileDescriptorProto& proto) const;
+
+ // If fallback_database_ is NULL, this is NULL. Otherwise, this is a mutex
+ // which must be locked while accessing tables_.
+ Mutex* mutex_;
+
+ // See constructor.
+ DescriptorDatabase* fallback_database_;
+ ErrorCollector* default_error_collector_;
+ const DescriptorPool* underlay_;
+
+ // This class contains a lot of hash maps with complicated types that
+ // we'd like to keep out of the header.
+ class Tables;
+ scoped_ptr<Tables> tables_;
+
+ bool enforce_dependencies_;
+ bool allow_unknown_;
+ bool enforce_weak_;
+ std::set<string> unused_import_track_files_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DescriptorPool);
+};
+
+// inline methods ====================================================
+
+// These macros makes this repetitive code more readable.
+#define PROTOBUF_DEFINE_ACCESSOR(CLASS, FIELD, TYPE) \
+ inline TYPE CLASS::FIELD() const { return FIELD##_; }
+
+// Strings fields are stored as pointers but returned as const references.
+#define PROTOBUF_DEFINE_STRING_ACCESSOR(CLASS, FIELD) \
+ inline const string& CLASS::FIELD() const { return *FIELD##_; }
+
+// Arrays take an index parameter, obviously.
+#define PROTOBUF_DEFINE_ARRAY_ACCESSOR(CLASS, FIELD, TYPE) \
+ inline TYPE CLASS::FIELD(int index) const { return FIELD##s_ + index; }
+
+#define PROTOBUF_DEFINE_OPTIONS_ACCESSOR(CLASS, TYPE) \
+ inline const TYPE& CLASS::options() const { return *options_; }
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(Descriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(Descriptor, full_name)
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, file, const FileDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, containing_type, const Descriptor*)
+
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, field_count, int)
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, oneof_decl_count, int)
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, nested_type_count, int)
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, enum_type_count, int)
+
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, field, const FieldDescriptor*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, oneof_decl, const OneofDescriptor*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, nested_type, const Descriptor*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, enum_type, const EnumDescriptor*)
+
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, extension_range_count, int)
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, extension_count, int)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, extension_range,
+ const Descriptor::ExtensionRange*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, extension,
+ const FieldDescriptor*)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(Descriptor, MessageOptions);
+PROTOBUF_DEFINE_ACCESSOR(Descriptor, is_placeholder, bool)
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(FieldDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(FieldDescriptor, full_name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(FieldDescriptor, lowercase_name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(FieldDescriptor, camelcase_name)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, file, const FileDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, is_extension, bool)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, type, FieldDescriptor::Type)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, label, FieldDescriptor::Label)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, containing_type, const Descriptor*)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, containing_oneof,
+ const OneofDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, index_in_oneof, int)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, extension_scope, const Descriptor*)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, message_type, const Descriptor*)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, enum_type, const EnumDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, experimental_map_key,
+ const FieldDescriptor*)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(FieldDescriptor, FieldOptions)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, has_default_value, bool)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_int32 , int32 )
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_int64 , int64 )
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_uint32, uint32)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_uint64, uint64)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_float , float )
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_double, double)
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_bool , bool )
+PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_enum,
+ const EnumValueDescriptor*)
+PROTOBUF_DEFINE_STRING_ACCESSOR(FieldDescriptor, default_value_string)
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(OneofDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(OneofDescriptor, full_name)
+PROTOBUF_DEFINE_ACCESSOR(OneofDescriptor, containing_type, const Descriptor*)
+PROTOBUF_DEFINE_ACCESSOR(OneofDescriptor, field_count, int)
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(EnumDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(EnumDescriptor, full_name)
+PROTOBUF_DEFINE_ACCESSOR(EnumDescriptor, file, const FileDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(EnumDescriptor, containing_type, const Descriptor*)
+PROTOBUF_DEFINE_ACCESSOR(EnumDescriptor, value_count, int)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(EnumDescriptor, value,
+ const EnumValueDescriptor*)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(EnumDescriptor, EnumOptions);
+PROTOBUF_DEFINE_ACCESSOR(EnumDescriptor, is_placeholder, bool)
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(EnumValueDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(EnumValueDescriptor, full_name)
+PROTOBUF_DEFINE_ACCESSOR(EnumValueDescriptor, number, int)
+PROTOBUF_DEFINE_ACCESSOR(EnumValueDescriptor, type, const EnumDescriptor*)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(EnumValueDescriptor, EnumValueOptions)
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(ServiceDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(ServiceDescriptor, full_name)
+PROTOBUF_DEFINE_ACCESSOR(ServiceDescriptor, file, const FileDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(ServiceDescriptor, method_count, int)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(ServiceDescriptor, method,
+ const MethodDescriptor*)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(ServiceDescriptor, ServiceOptions);
+
+PROTOBUF_DEFINE_STRING_ACCESSOR(MethodDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(MethodDescriptor, full_name)
+PROTOBUF_DEFINE_ACCESSOR(MethodDescriptor, service, const ServiceDescriptor*)
+PROTOBUF_DEFINE_ACCESSOR(MethodDescriptor, input_type, const Descriptor*)
+PROTOBUF_DEFINE_ACCESSOR(MethodDescriptor, output_type, const Descriptor*)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(MethodDescriptor, MethodOptions);
+PROTOBUF_DEFINE_STRING_ACCESSOR(FileDescriptor, name)
+PROTOBUF_DEFINE_STRING_ACCESSOR(FileDescriptor, package)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, pool, const DescriptorPool*)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, dependency_count, int)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, public_dependency_count, int)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, weak_dependency_count, int)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, message_type_count, int)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, enum_type_count, int)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, service_count, int)
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, extension_count, int)
+PROTOBUF_DEFINE_OPTIONS_ACCESSOR(FileDescriptor, FileOptions);
+PROTOBUF_DEFINE_ACCESSOR(FileDescriptor, is_placeholder, bool)
+
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(FileDescriptor, message_type, const Descriptor*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(FileDescriptor, enum_type, const EnumDescriptor*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(FileDescriptor, service,
+ const ServiceDescriptor*)
+PROTOBUF_DEFINE_ARRAY_ACCESSOR(FileDescriptor, extension,
+ const FieldDescriptor*)
+
+#undef PROTOBUF_DEFINE_ACCESSOR
+#undef PROTOBUF_DEFINE_STRING_ACCESSOR
+#undef PROTOBUF_DEFINE_ARRAY_ACCESSOR
+
+// A few accessors differ from the macros...
+
+inline bool Descriptor::IsExtensionNumber(int number) const {
+ return FindExtensionRangeContainingNumber(number) != NULL;
+}
+
+inline bool FieldDescriptor::is_required() const {
+ return label() == LABEL_REQUIRED;
+}
+
+inline bool FieldDescriptor::is_optional() const {
+ return label() == LABEL_OPTIONAL;
+}
+
+inline bool FieldDescriptor::is_repeated() const {
+ return label() == LABEL_REPEATED;
+}
+
+inline bool FieldDescriptor::is_packable() const {
+ return is_repeated() && IsTypePackable(type());
+}
+
+// To save space, index() is computed by looking at the descriptor's position
+// in the parent's array of children.
+inline int FieldDescriptor::index() const {
+ if (!is_extension_) {
+ return static_cast<int>(this - containing_type_->fields_);
+ } else if (extension_scope_ != NULL) {
+ return static_cast<int>(this - extension_scope_->extensions_);
+ } else {
+ return static_cast<int>(this - file_->extensions_);
+ }
+}
+
+inline int Descriptor::index() const {
+ if (containing_type_ == NULL) {
+ return static_cast<int>(this - file_->message_types_);
+ } else {
+ return static_cast<int>(this - containing_type_->nested_types_);
+ }
+}
+
+inline int OneofDescriptor::index() const {
+ return static_cast<int>(this - containing_type_->oneof_decls_);
+}
+
+inline int EnumDescriptor::index() const {
+ if (containing_type_ == NULL) {
+ return static_cast<int>(this - file_->enum_types_);
+ } else {
+ return static_cast<int>(this - containing_type_->enum_types_);
+ }
+}
+
+inline int EnumValueDescriptor::index() const {
+ return static_cast<int>(this - type_->values_);
+}
+
+inline int ServiceDescriptor::index() const {
+ return static_cast<int>(this - file_->services_);
+}
+
+inline int MethodDescriptor::index() const {
+ return static_cast<int>(this - service_->methods_);
+}
+
+inline const char* FieldDescriptor::type_name() const {
+ return kTypeToName[type_];
+}
+
+inline FieldDescriptor::CppType FieldDescriptor::cpp_type() const {
+ return kTypeToCppTypeMap[type_];
+}
+
+inline const char* FieldDescriptor::cpp_type_name() const {
+ return kCppTypeToName[kTypeToCppTypeMap[type_]];
+}
+
+inline FieldDescriptor::CppType FieldDescriptor::TypeToCppType(Type type) {
+ return kTypeToCppTypeMap[type];
+}
+
+inline const char* FieldDescriptor::TypeName(Type type) {
+ return kTypeToName[type];
+}
+
+inline const char* FieldDescriptor::CppTypeName(CppType cpp_type) {
+ return kCppTypeToName[cpp_type];
+}
+
+inline bool FieldDescriptor::IsTypePackable(Type field_type) {
+ return (field_type != FieldDescriptor::TYPE_STRING &&
+ field_type != FieldDescriptor::TYPE_GROUP &&
+ field_type != FieldDescriptor::TYPE_MESSAGE &&
+ field_type != FieldDescriptor::TYPE_BYTES);
+}
+
+inline const FileDescriptor* FileDescriptor::dependency(int index) const {
+ return dependencies_[index];
+}
+
+inline const FileDescriptor* FileDescriptor::public_dependency(
+ int index) const {
+ return dependencies_[public_dependencies_[index]];
+}
+
+inline const FileDescriptor* FileDescriptor::weak_dependency(
+ int index) const {
+ return dependencies_[weak_dependencies_[index]];
+}
+
+// Can't use PROTOBUF_DEFINE_ARRAY_ACCESSOR because fields_ is actually an array
+// of pointers rather than the usual array of objects.
+inline const FieldDescriptor* OneofDescriptor::field(int index) const {
+ return fields_[index];
+}
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_DESCRIPTOR_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor.pb.cc b/toolkit/components/protobuf/src/google/protobuf/descriptor.pb.cc
new file mode 100644
index 0000000000..c3aa2fb68f
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor.pb.cc
@@ -0,0 +1,9135 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/protobuf/descriptor.proto
+
+#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION
+#include "google/protobuf/descriptor.pb.h"
+
+#include <algorithm>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/generated_message_reflection.h>
+#include <google/protobuf/reflection_ops.h>
+#include <google/protobuf/wire_format.h>
+// @@protoc_insertion_point(includes)
+
+namespace google {
+namespace protobuf {
+
+namespace {
+
+const ::google::protobuf::Descriptor* FileDescriptorSet_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ FileDescriptorSet_reflection_ = NULL;
+const ::google::protobuf::Descriptor* FileDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ FileDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* DescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ DescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* DescriptorProto_ExtensionRange_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ DescriptorProto_ExtensionRange_reflection_ = NULL;
+const ::google::protobuf::Descriptor* FieldDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ FieldDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::EnumDescriptor* FieldDescriptorProto_Type_descriptor_ = NULL;
+const ::google::protobuf::EnumDescriptor* FieldDescriptorProto_Label_descriptor_ = NULL;
+const ::google::protobuf::Descriptor* OneofDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ OneofDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* EnumDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ EnumDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* EnumValueDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ EnumValueDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* ServiceDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ ServiceDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* MethodDescriptorProto_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ MethodDescriptorProto_reflection_ = NULL;
+const ::google::protobuf::Descriptor* FileOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ FileOptions_reflection_ = NULL;
+const ::google::protobuf::EnumDescriptor* FileOptions_OptimizeMode_descriptor_ = NULL;
+const ::google::protobuf::Descriptor* MessageOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ MessageOptions_reflection_ = NULL;
+const ::google::protobuf::Descriptor* FieldOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ FieldOptions_reflection_ = NULL;
+const ::google::protobuf::EnumDescriptor* FieldOptions_CType_descriptor_ = NULL;
+const ::google::protobuf::Descriptor* EnumOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ EnumOptions_reflection_ = NULL;
+const ::google::protobuf::Descriptor* EnumValueOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ EnumValueOptions_reflection_ = NULL;
+const ::google::protobuf::Descriptor* ServiceOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ ServiceOptions_reflection_ = NULL;
+const ::google::protobuf::Descriptor* MethodOptions_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ MethodOptions_reflection_ = NULL;
+const ::google::protobuf::Descriptor* UninterpretedOption_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ UninterpretedOption_reflection_ = NULL;
+const ::google::protobuf::Descriptor* UninterpretedOption_NamePart_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ UninterpretedOption_NamePart_reflection_ = NULL;
+const ::google::protobuf::Descriptor* SourceCodeInfo_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ SourceCodeInfo_reflection_ = NULL;
+const ::google::protobuf::Descriptor* SourceCodeInfo_Location_descriptor_ = NULL;
+const ::google::protobuf::internal::GeneratedMessageReflection*
+ SourceCodeInfo_Location_reflection_ = NULL;
+
+} // namespace
+
+
+void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto() {
+ protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ const ::google::protobuf::FileDescriptor* file =
+ ::google::protobuf::DescriptorPool::generated_pool()->FindFileByName(
+ "google/protobuf/descriptor.proto");
+ GOOGLE_CHECK(file != NULL);
+ FileDescriptorSet_descriptor_ = file->message_type(0);
+ static const int FileDescriptorSet_offsets_[1] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorSet, file_),
+ };
+ FileDescriptorSet_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ FileDescriptorSet_descriptor_,
+ FileDescriptorSet::default_instance_,
+ FileDescriptorSet_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorSet, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorSet, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(FileDescriptorSet));
+ FileDescriptorProto_descriptor_ = file->message_type(1);
+ static const int FileDescriptorProto_offsets_[11] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, package_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, dependency_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, public_dependency_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, weak_dependency_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, message_type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, enum_type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, service_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, extension_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, options_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, source_code_info_),
+ };
+ FileDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ FileDescriptorProto_descriptor_,
+ FileDescriptorProto::default_instance_,
+ FileDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(FileDescriptorProto));
+ DescriptorProto_descriptor_ = file->message_type(2);
+ static const int DescriptorProto_offsets_[8] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, field_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, extension_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, nested_type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, enum_type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, extension_range_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, oneof_decl_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, options_),
+ };
+ DescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ DescriptorProto_descriptor_,
+ DescriptorProto::default_instance_,
+ DescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(DescriptorProto));
+ DescriptorProto_ExtensionRange_descriptor_ = DescriptorProto_descriptor_->nested_type(0);
+ static const int DescriptorProto_ExtensionRange_offsets_[2] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto_ExtensionRange, start_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto_ExtensionRange, end_),
+ };
+ DescriptorProto_ExtensionRange_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ DescriptorProto_ExtensionRange_descriptor_,
+ DescriptorProto_ExtensionRange::default_instance_,
+ DescriptorProto_ExtensionRange_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto_ExtensionRange, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(DescriptorProto_ExtensionRange, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(DescriptorProto_ExtensionRange));
+ FieldDescriptorProto_descriptor_ = file->message_type(3);
+ static const int FieldDescriptorProto_offsets_[9] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, number_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, label_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, type_name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, extendee_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, default_value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, oneof_index_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, options_),
+ };
+ FieldDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ FieldDescriptorProto_descriptor_,
+ FieldDescriptorProto::default_instance_,
+ FieldDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(FieldDescriptorProto));
+ FieldDescriptorProto_Type_descriptor_ = FieldDescriptorProto_descriptor_->enum_type(0);
+ FieldDescriptorProto_Label_descriptor_ = FieldDescriptorProto_descriptor_->enum_type(1);
+ OneofDescriptorProto_descriptor_ = file->message_type(4);
+ static const int OneofDescriptorProto_offsets_[1] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(OneofDescriptorProto, name_),
+ };
+ OneofDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ OneofDescriptorProto_descriptor_,
+ OneofDescriptorProto::default_instance_,
+ OneofDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(OneofDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(OneofDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(OneofDescriptorProto));
+ EnumDescriptorProto_descriptor_ = file->message_type(5);
+ static const int EnumDescriptorProto_offsets_[3] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumDescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumDescriptorProto, value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumDescriptorProto, options_),
+ };
+ EnumDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ EnumDescriptorProto_descriptor_,
+ EnumDescriptorProto::default_instance_,
+ EnumDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(EnumDescriptorProto));
+ EnumValueDescriptorProto_descriptor_ = file->message_type(6);
+ static const int EnumValueDescriptorProto_offsets_[3] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueDescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueDescriptorProto, number_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueDescriptorProto, options_),
+ };
+ EnumValueDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ EnumValueDescriptorProto_descriptor_,
+ EnumValueDescriptorProto::default_instance_,
+ EnumValueDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(EnumValueDescriptorProto));
+ ServiceDescriptorProto_descriptor_ = file->message_type(7);
+ static const int ServiceDescriptorProto_offsets_[3] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceDescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceDescriptorProto, method_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceDescriptorProto, options_),
+ };
+ ServiceDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ ServiceDescriptorProto_descriptor_,
+ ServiceDescriptorProto::default_instance_,
+ ServiceDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(ServiceDescriptorProto));
+ MethodDescriptorProto_descriptor_ = file->message_type(8);
+ static const int MethodDescriptorProto_offsets_[4] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodDescriptorProto, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodDescriptorProto, input_type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodDescriptorProto, output_type_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodDescriptorProto, options_),
+ };
+ MethodDescriptorProto_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ MethodDescriptorProto_descriptor_,
+ MethodDescriptorProto::default_instance_,
+ MethodDescriptorProto_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodDescriptorProto, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodDescriptorProto, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(MethodDescriptorProto));
+ FileOptions_descriptor_ = file->message_type(9);
+ static const int FileOptions_offsets_[12] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, java_package_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, java_outer_classname_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, java_multiple_files_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, java_generate_equals_and_hash_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, java_string_check_utf8_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, optimize_for_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, go_package_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, cc_generic_services_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, java_generic_services_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, py_generic_services_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, uninterpreted_option_),
+ };
+ FileOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ FileOptions_descriptor_,
+ FileOptions::default_instance_,
+ FileOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FileOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(FileOptions));
+ FileOptions_OptimizeMode_descriptor_ = FileOptions_descriptor_->enum_type(0);
+ MessageOptions_descriptor_ = file->message_type(10);
+ static const int MessageOptions_offsets_[4] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, message_set_wire_format_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, no_standard_descriptor_accessor_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, uninterpreted_option_),
+ };
+ MessageOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ MessageOptions_descriptor_,
+ MessageOptions::default_instance_,
+ MessageOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MessageOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(MessageOptions));
+ FieldOptions_descriptor_ = file->message_type(11);
+ static const int FieldOptions_offsets_[7] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, ctype_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, packed_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, lazy_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, experimental_map_key_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, weak_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, uninterpreted_option_),
+ };
+ FieldOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ FieldOptions_descriptor_,
+ FieldOptions::default_instance_,
+ FieldOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(FieldOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(FieldOptions));
+ FieldOptions_CType_descriptor_ = FieldOptions_descriptor_->enum_type(0);
+ EnumOptions_descriptor_ = file->message_type(12);
+ static const int EnumOptions_offsets_[3] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumOptions, allow_alias_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumOptions, uninterpreted_option_),
+ };
+ EnumOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ EnumOptions_descriptor_,
+ EnumOptions::default_instance_,
+ EnumOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(EnumOptions));
+ EnumValueOptions_descriptor_ = file->message_type(13);
+ static const int EnumValueOptions_offsets_[2] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueOptions, uninterpreted_option_),
+ };
+ EnumValueOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ EnumValueOptions_descriptor_,
+ EnumValueOptions::default_instance_,
+ EnumValueOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(EnumValueOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(EnumValueOptions));
+ ServiceOptions_descriptor_ = file->message_type(14);
+ static const int ServiceOptions_offsets_[2] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceOptions, uninterpreted_option_),
+ };
+ ServiceOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ ServiceOptions_descriptor_,
+ ServiceOptions::default_instance_,
+ ServiceOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(ServiceOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(ServiceOptions));
+ MethodOptions_descriptor_ = file->message_type(15);
+ static const int MethodOptions_offsets_[2] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodOptions, deprecated_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodOptions, uninterpreted_option_),
+ };
+ MethodOptions_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ MethodOptions_descriptor_,
+ MethodOptions::default_instance_,
+ MethodOptions_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodOptions, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodOptions, _unknown_fields_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(MethodOptions, _extensions_),
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(MethodOptions));
+ UninterpretedOption_descriptor_ = file->message_type(16);
+ static const int UninterpretedOption_offsets_[7] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, name_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, identifier_value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, positive_int_value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, negative_int_value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, double_value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, string_value_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, aggregate_value_),
+ };
+ UninterpretedOption_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ UninterpretedOption_descriptor_,
+ UninterpretedOption::default_instance_,
+ UninterpretedOption_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(UninterpretedOption));
+ UninterpretedOption_NamePart_descriptor_ = UninterpretedOption_descriptor_->nested_type(0);
+ static const int UninterpretedOption_NamePart_offsets_[2] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption_NamePart, name_part_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption_NamePart, is_extension_),
+ };
+ UninterpretedOption_NamePart_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ UninterpretedOption_NamePart_descriptor_,
+ UninterpretedOption_NamePart::default_instance_,
+ UninterpretedOption_NamePart_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption_NamePart, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(UninterpretedOption_NamePart, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(UninterpretedOption_NamePart));
+ SourceCodeInfo_descriptor_ = file->message_type(17);
+ static const int SourceCodeInfo_offsets_[1] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo, location_),
+ };
+ SourceCodeInfo_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ SourceCodeInfo_descriptor_,
+ SourceCodeInfo::default_instance_,
+ SourceCodeInfo_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(SourceCodeInfo));
+ SourceCodeInfo_Location_descriptor_ = SourceCodeInfo_descriptor_->nested_type(0);
+ static const int SourceCodeInfo_Location_offsets_[4] = {
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo_Location, path_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo_Location, span_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo_Location, leading_comments_),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo_Location, trailing_comments_),
+ };
+ SourceCodeInfo_Location_reflection_ =
+ new ::google::protobuf::internal::GeneratedMessageReflection(
+ SourceCodeInfo_Location_descriptor_,
+ SourceCodeInfo_Location::default_instance_,
+ SourceCodeInfo_Location_offsets_,
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo_Location, _has_bits_[0]),
+ GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(SourceCodeInfo_Location, _unknown_fields_),
+ -1,
+ ::google::protobuf::DescriptorPool::generated_pool(),
+ ::google::protobuf::MessageFactory::generated_factory(),
+ sizeof(SourceCodeInfo_Location));
+}
+
+namespace {
+
+GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AssignDescriptors_once_);
+inline void protobuf_AssignDescriptorsOnce() {
+ ::google::protobuf::GoogleOnceInit(&protobuf_AssignDescriptors_once_,
+ &protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto);
+}
+
+void protobuf_RegisterTypes(const ::std::string&) {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ FileDescriptorSet_descriptor_, &FileDescriptorSet::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ FileDescriptorProto_descriptor_, &FileDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ DescriptorProto_descriptor_, &DescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ DescriptorProto_ExtensionRange_descriptor_, &DescriptorProto_ExtensionRange::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ FieldDescriptorProto_descriptor_, &FieldDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ OneofDescriptorProto_descriptor_, &OneofDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ EnumDescriptorProto_descriptor_, &EnumDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ EnumValueDescriptorProto_descriptor_, &EnumValueDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ ServiceDescriptorProto_descriptor_, &ServiceDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ MethodDescriptorProto_descriptor_, &MethodDescriptorProto::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ FileOptions_descriptor_, &FileOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ MessageOptions_descriptor_, &MessageOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ FieldOptions_descriptor_, &FieldOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ EnumOptions_descriptor_, &EnumOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ EnumValueOptions_descriptor_, &EnumValueOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ ServiceOptions_descriptor_, &ServiceOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ MethodOptions_descriptor_, &MethodOptions::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ UninterpretedOption_descriptor_, &UninterpretedOption::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ UninterpretedOption_NamePart_descriptor_, &UninterpretedOption_NamePart::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ SourceCodeInfo_descriptor_, &SourceCodeInfo::default_instance());
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
+ SourceCodeInfo_Location_descriptor_, &SourceCodeInfo_Location::default_instance());
+}
+
+} // namespace
+
+void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto() {
+ delete FileDescriptorSet::default_instance_;
+ delete FileDescriptorSet_reflection_;
+ delete FileDescriptorProto::default_instance_;
+ delete FileDescriptorProto_reflection_;
+ delete DescriptorProto::default_instance_;
+ delete DescriptorProto_reflection_;
+ delete DescriptorProto_ExtensionRange::default_instance_;
+ delete DescriptorProto_ExtensionRange_reflection_;
+ delete FieldDescriptorProto::default_instance_;
+ delete FieldDescriptorProto_reflection_;
+ delete OneofDescriptorProto::default_instance_;
+ delete OneofDescriptorProto_reflection_;
+ delete EnumDescriptorProto::default_instance_;
+ delete EnumDescriptorProto_reflection_;
+ delete EnumValueDescriptorProto::default_instance_;
+ delete EnumValueDescriptorProto_reflection_;
+ delete ServiceDescriptorProto::default_instance_;
+ delete ServiceDescriptorProto_reflection_;
+ delete MethodDescriptorProto::default_instance_;
+ delete MethodDescriptorProto_reflection_;
+ delete FileOptions::default_instance_;
+ delete FileOptions_reflection_;
+ delete MessageOptions::default_instance_;
+ delete MessageOptions_reflection_;
+ delete FieldOptions::default_instance_;
+ delete FieldOptions_reflection_;
+ delete EnumOptions::default_instance_;
+ delete EnumOptions_reflection_;
+ delete EnumValueOptions::default_instance_;
+ delete EnumValueOptions_reflection_;
+ delete ServiceOptions::default_instance_;
+ delete ServiceOptions_reflection_;
+ delete MethodOptions::default_instance_;
+ delete MethodOptions_reflection_;
+ delete UninterpretedOption::default_instance_;
+ delete UninterpretedOption_reflection_;
+ delete UninterpretedOption_NamePart::default_instance_;
+ delete UninterpretedOption_NamePart_reflection_;
+ delete SourceCodeInfo::default_instance_;
+ delete SourceCodeInfo_reflection_;
+ delete SourceCodeInfo_Location::default_instance_;
+ delete SourceCodeInfo_Location_reflection_;
+}
+
+void protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto() {
+ static bool already_here = false;
+ if (already_here) return;
+ already_here = true;
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+ ::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
+ "\n google/protobuf/descriptor.proto\022\017goog"
+ "le.protobuf\"G\n\021FileDescriptorSet\0222\n\004file"
+ "\030\001 \003(\0132$.google.protobuf.FileDescriptorP"
+ "roto\"\313\003\n\023FileDescriptorProto\022\014\n\004name\030\001 \001"
+ "(\t\022\017\n\007package\030\002 \001(\t\022\022\n\ndependency\030\003 \003(\t\022"
+ "\031\n\021public_dependency\030\n \003(\005\022\027\n\017weak_depen"
+ "dency\030\013 \003(\005\0226\n\014message_type\030\004 \003(\0132 .goog"
+ "le.protobuf.DescriptorProto\0227\n\tenum_type"
+ "\030\005 \003(\0132$.google.protobuf.EnumDescriptorP"
+ "roto\0228\n\007service\030\006 \003(\0132\'.google.protobuf."
+ "ServiceDescriptorProto\0228\n\textension\030\007 \003("
+ "\0132%.google.protobuf.FieldDescriptorProto"
+ "\022-\n\007options\030\010 \001(\0132\034.google.protobuf.File"
+ "Options\0229\n\020source_code_info\030\t \001(\0132\037.goog"
+ "le.protobuf.SourceCodeInfo\"\344\003\n\017Descripto"
+ "rProto\022\014\n\004name\030\001 \001(\t\0224\n\005field\030\002 \003(\0132%.go"
+ "ogle.protobuf.FieldDescriptorProto\0228\n\tex"
+ "tension\030\006 \003(\0132%.google.protobuf.FieldDes"
+ "criptorProto\0225\n\013nested_type\030\003 \003(\0132 .goog"
+ "le.protobuf.DescriptorProto\0227\n\tenum_type"
+ "\030\004 \003(\0132$.google.protobuf.EnumDescriptorP"
+ "roto\022H\n\017extension_range\030\005 \003(\0132/.google.p"
+ "rotobuf.DescriptorProto.ExtensionRange\0229"
+ "\n\noneof_decl\030\010 \003(\0132%.google.protobuf.One"
+ "ofDescriptorProto\0220\n\007options\030\007 \001(\0132\037.goo"
+ "gle.protobuf.MessageOptions\032,\n\016Extension"
+ "Range\022\r\n\005start\030\001 \001(\005\022\013\n\003end\030\002 \001(\005\"\251\005\n\024Fi"
+ "eldDescriptorProto\022\014\n\004name\030\001 \001(\t\022\016\n\006numb"
+ "er\030\003 \001(\005\022:\n\005label\030\004 \001(\0162+.google.protobu"
+ "f.FieldDescriptorProto.Label\0228\n\004type\030\005 \001"
+ "(\0162*.google.protobuf.FieldDescriptorProt"
+ "o.Type\022\021\n\ttype_name\030\006 \001(\t\022\020\n\010extendee\030\002 "
+ "\001(\t\022\025\n\rdefault_value\030\007 \001(\t\022\023\n\013oneof_inde"
+ "x\030\t \001(\005\022.\n\007options\030\010 \001(\0132\035.google.protob"
+ "uf.FieldOptions\"\266\002\n\004Type\022\017\n\013TYPE_DOUBLE\020"
+ "\001\022\016\n\nTYPE_FLOAT\020\002\022\016\n\nTYPE_INT64\020\003\022\017\n\013TYP"
+ "E_UINT64\020\004\022\016\n\nTYPE_INT32\020\005\022\020\n\014TYPE_FIXED"
+ "64\020\006\022\020\n\014TYPE_FIXED32\020\007\022\r\n\tTYPE_BOOL\020\010\022\017\n"
+ "\013TYPE_STRING\020\t\022\016\n\nTYPE_GROUP\020\n\022\020\n\014TYPE_M"
+ "ESSAGE\020\013\022\016\n\nTYPE_BYTES\020\014\022\017\n\013TYPE_UINT32\020"
+ "\r\022\r\n\tTYPE_ENUM\020\016\022\021\n\rTYPE_SFIXED32\020\017\022\021\n\rT"
+ "YPE_SFIXED64\020\020\022\017\n\013TYPE_SINT32\020\021\022\017\n\013TYPE_"
+ "SINT64\020\022\"C\n\005Label\022\022\n\016LABEL_OPTIONAL\020\001\022\022\n"
+ "\016LABEL_REQUIRED\020\002\022\022\n\016LABEL_REPEATED\020\003\"$\n"
+ "\024OneofDescriptorProto\022\014\n\004name\030\001 \001(\t\"\214\001\n\023"
+ "EnumDescriptorProto\022\014\n\004name\030\001 \001(\t\0228\n\005val"
+ "ue\030\002 \003(\0132).google.protobuf.EnumValueDesc"
+ "riptorProto\022-\n\007options\030\003 \001(\0132\034.google.pr"
+ "otobuf.EnumOptions\"l\n\030EnumValueDescripto"
+ "rProto\022\014\n\004name\030\001 \001(\t\022\016\n\006number\030\002 \001(\005\0222\n\007"
+ "options\030\003 \001(\0132!.google.protobuf.EnumValu"
+ "eOptions\"\220\001\n\026ServiceDescriptorProto\022\014\n\004n"
+ "ame\030\001 \001(\t\0226\n\006method\030\002 \003(\0132&.google.proto"
+ "buf.MethodDescriptorProto\0220\n\007options\030\003 \001"
+ "(\0132\037.google.protobuf.ServiceOptions\"\177\n\025M"
+ "ethodDescriptorProto\022\014\n\004name\030\001 \001(\t\022\022\n\nin"
+ "put_type\030\002 \001(\t\022\023\n\013output_type\030\003 \001(\t\022/\n\007o"
+ "ptions\030\004 \001(\0132\036.google.protobuf.MethodOpt"
+ "ions\"\253\004\n\013FileOptions\022\024\n\014java_package\030\001 \001"
+ "(\t\022\034\n\024java_outer_classname\030\010 \001(\t\022\"\n\023java"
+ "_multiple_files\030\n \001(\010:\005false\022,\n\035java_gen"
+ "erate_equals_and_hash\030\024 \001(\010:\005false\022%\n\026ja"
+ "va_string_check_utf8\030\033 \001(\010:\005false\022F\n\014opt"
+ "imize_for\030\t \001(\0162).google.protobuf.FileOp"
+ "tions.OptimizeMode:\005SPEED\022\022\n\ngo_package\030"
+ "\013 \001(\t\022\"\n\023cc_generic_services\030\020 \001(\010:\005fals"
+ "e\022$\n\025java_generic_services\030\021 \001(\010:\005false\022"
+ "\"\n\023py_generic_services\030\022 \001(\010:\005false\022\031\n\nd"
+ "eprecated\030\027 \001(\010:\005false\022C\n\024uninterpreted_"
+ "option\030\347\007 \003(\0132$.google.protobuf.Uninterp"
+ "retedOption\":\n\014OptimizeMode\022\t\n\005SPEED\020\001\022\r"
+ "\n\tCODE_SIZE\020\002\022\020\n\014LITE_RUNTIME\020\003*\t\010\350\007\020\200\200\200"
+ "\200\002\"\323\001\n\016MessageOptions\022&\n\027message_set_wir"
+ "e_format\030\001 \001(\010:\005false\022.\n\037no_standard_des"
+ "criptor_accessor\030\002 \001(\010:\005false\022\031\n\ndepreca"
+ "ted\030\003 \001(\010:\005false\022C\n\024uninterpreted_option"
+ "\030\347\007 \003(\0132$.google.protobuf.UninterpretedO"
+ "ption*\t\010\350\007\020\200\200\200\200\002\"\276\002\n\014FieldOptions\022:\n\005cty"
+ "pe\030\001 \001(\0162#.google.protobuf.FieldOptions."
+ "CType:\006STRING\022\016\n\006packed\030\002 \001(\010\022\023\n\004lazy\030\005 "
+ "\001(\010:\005false\022\031\n\ndeprecated\030\003 \001(\010:\005false\022\034\n"
+ "\024experimental_map_key\030\t \001(\t\022\023\n\004weak\030\n \001("
+ "\010:\005false\022C\n\024uninterpreted_option\030\347\007 \003(\0132"
+ "$.google.protobuf.UninterpretedOption\"/\n"
+ "\005CType\022\n\n\006STRING\020\000\022\010\n\004CORD\020\001\022\020\n\014STRING_P"
+ "IECE\020\002*\t\010\350\007\020\200\200\200\200\002\"\215\001\n\013EnumOptions\022\023\n\013all"
+ "ow_alias\030\002 \001(\010\022\031\n\ndeprecated\030\003 \001(\010:\005fals"
+ "e\022C\n\024uninterpreted_option\030\347\007 \003(\0132$.googl"
+ "e.protobuf.UninterpretedOption*\t\010\350\007\020\200\200\200\200"
+ "\002\"}\n\020EnumValueOptions\022\031\n\ndeprecated\030\001 \001("
+ "\010:\005false\022C\n\024uninterpreted_option\030\347\007 \003(\0132"
+ "$.google.protobuf.UninterpretedOption*\t\010"
+ "\350\007\020\200\200\200\200\002\"{\n\016ServiceOptions\022\031\n\ndeprecated"
+ "\030! \001(\010:\005false\022C\n\024uninterpreted_option\030\347\007"
+ " \003(\0132$.google.protobuf.UninterpretedOpti"
+ "on*\t\010\350\007\020\200\200\200\200\002\"z\n\rMethodOptions\022\031\n\ndeprec"
+ "ated\030! \001(\010:\005false\022C\n\024uninterpreted_optio"
+ "n\030\347\007 \003(\0132$.google.protobuf.Uninterpreted"
+ "Option*\t\010\350\007\020\200\200\200\200\002\"\236\002\n\023UninterpretedOptio"
+ "n\022;\n\004name\030\002 \003(\0132-.google.protobuf.Uninte"
+ "rpretedOption.NamePart\022\030\n\020identifier_val"
+ "ue\030\003 \001(\t\022\032\n\022positive_int_value\030\004 \001(\004\022\032\n\022"
+ "negative_int_value\030\005 \001(\003\022\024\n\014double_value"
+ "\030\006 \001(\001\022\024\n\014string_value\030\007 \001(\014\022\027\n\017aggregat"
+ "e_value\030\010 \001(\t\0323\n\010NamePart\022\021\n\tname_part\030\001"
+ " \002(\t\022\024\n\014is_extension\030\002 \002(\010\"\261\001\n\016SourceCod"
+ "eInfo\022:\n\010location\030\001 \003(\0132(.google.protobu"
+ "f.SourceCodeInfo.Location\032c\n\010Location\022\020\n"
+ "\004path\030\001 \003(\005B\002\020\001\022\020\n\004span\030\002 \003(\005B\002\020\001\022\030\n\020lea"
+ "ding_comments\030\003 \001(\t\022\031\n\021trailing_comments"
+ "\030\004 \001(\tB)\n\023com.google.protobufB\020Descripto"
+ "rProtosH\001", 4449);
+ ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
+ "google/protobuf/descriptor.proto", &protobuf_RegisterTypes);
+ FileDescriptorSet::default_instance_ = new FileDescriptorSet();
+ FileDescriptorProto::default_instance_ = new FileDescriptorProto();
+ DescriptorProto::default_instance_ = new DescriptorProto();
+ DescriptorProto_ExtensionRange::default_instance_ = new DescriptorProto_ExtensionRange();
+ FieldDescriptorProto::default_instance_ = new FieldDescriptorProto();
+ OneofDescriptorProto::default_instance_ = new OneofDescriptorProto();
+ EnumDescriptorProto::default_instance_ = new EnumDescriptorProto();
+ EnumValueDescriptorProto::default_instance_ = new EnumValueDescriptorProto();
+ ServiceDescriptorProto::default_instance_ = new ServiceDescriptorProto();
+ MethodDescriptorProto::default_instance_ = new MethodDescriptorProto();
+ FileOptions::default_instance_ = new FileOptions();
+ MessageOptions::default_instance_ = new MessageOptions();
+ FieldOptions::default_instance_ = new FieldOptions();
+ EnumOptions::default_instance_ = new EnumOptions();
+ EnumValueOptions::default_instance_ = new EnumValueOptions();
+ ServiceOptions::default_instance_ = new ServiceOptions();
+ MethodOptions::default_instance_ = new MethodOptions();
+ UninterpretedOption::default_instance_ = new UninterpretedOption();
+ UninterpretedOption_NamePart::default_instance_ = new UninterpretedOption_NamePart();
+ SourceCodeInfo::default_instance_ = new SourceCodeInfo();
+ SourceCodeInfo_Location::default_instance_ = new SourceCodeInfo_Location();
+ FileDescriptorSet::default_instance_->InitAsDefaultInstance();
+ FileDescriptorProto::default_instance_->InitAsDefaultInstance();
+ DescriptorProto::default_instance_->InitAsDefaultInstance();
+ DescriptorProto_ExtensionRange::default_instance_->InitAsDefaultInstance();
+ FieldDescriptorProto::default_instance_->InitAsDefaultInstance();
+ OneofDescriptorProto::default_instance_->InitAsDefaultInstance();
+ EnumDescriptorProto::default_instance_->InitAsDefaultInstance();
+ EnumValueDescriptorProto::default_instance_->InitAsDefaultInstance();
+ ServiceDescriptorProto::default_instance_->InitAsDefaultInstance();
+ MethodDescriptorProto::default_instance_->InitAsDefaultInstance();
+ FileOptions::default_instance_->InitAsDefaultInstance();
+ MessageOptions::default_instance_->InitAsDefaultInstance();
+ FieldOptions::default_instance_->InitAsDefaultInstance();
+ EnumOptions::default_instance_->InitAsDefaultInstance();
+ EnumValueOptions::default_instance_->InitAsDefaultInstance();
+ ServiceOptions::default_instance_->InitAsDefaultInstance();
+ MethodOptions::default_instance_->InitAsDefaultInstance();
+ UninterpretedOption::default_instance_->InitAsDefaultInstance();
+ UninterpretedOption_NamePart::default_instance_->InitAsDefaultInstance();
+ SourceCodeInfo::default_instance_->InitAsDefaultInstance();
+ SourceCodeInfo_Location::default_instance_->InitAsDefaultInstance();
+ ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto);
+}
+
+// Force AddDescriptors() to be called at static initialization time.
+struct StaticDescriptorInitializer_google_2fprotobuf_2fdescriptor_2eproto {
+ StaticDescriptorInitializer_google_2fprotobuf_2fdescriptor_2eproto() {
+ protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ }
+} static_descriptor_initializer_google_2fprotobuf_2fdescriptor_2eproto_;
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FileDescriptorSet::kFileFieldNumber;
+#endif // !_MSC_VER
+
+FileDescriptorSet::FileDescriptorSet()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.FileDescriptorSet)
+}
+
+void FileDescriptorSet::InitAsDefaultInstance() {
+}
+
+FileDescriptorSet::FileDescriptorSet(const FileDescriptorSet& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.FileDescriptorSet)
+}
+
+void FileDescriptorSet::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FileDescriptorSet::~FileDescriptorSet() {
+ // @@protoc_insertion_point(destructor:google.protobuf.FileDescriptorSet)
+ SharedDtor();
+}
+
+void FileDescriptorSet::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void FileDescriptorSet::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* FileDescriptorSet::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FileDescriptorSet_descriptor_;
+}
+
+const FileDescriptorSet& FileDescriptorSet::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+FileDescriptorSet* FileDescriptorSet::default_instance_ = NULL;
+
+FileDescriptorSet* FileDescriptorSet::New() const {
+ return new FileDescriptorSet;
+}
+
+void FileDescriptorSet::Clear() {
+ file_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool FileDescriptorSet::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.FileDescriptorSet)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .google.protobuf.FileDescriptorProto file = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_file:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_file()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_file;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.FileDescriptorSet)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.FileDescriptorSet)
+ return false;
+#undef DO_
+}
+
+void FileDescriptorSet::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.FileDescriptorSet)
+ // repeated .google.protobuf.FileDescriptorProto file = 1;
+ for (int i = 0; i < this->file_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 1, this->file(i), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.FileDescriptorSet)
+}
+
+::google::protobuf::uint8* FileDescriptorSet::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.FileDescriptorSet)
+ // repeated .google.protobuf.FileDescriptorProto file = 1;
+ for (int i = 0; i < this->file_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 1, this->file(i), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.FileDescriptorSet)
+ return target;
+}
+
+int FileDescriptorSet::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .google.protobuf.FileDescriptorProto file = 1;
+ total_size += 1 * this->file_size();
+ for (int i = 0; i < this->file_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->file(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FileDescriptorSet::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const FileDescriptorSet* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const FileDescriptorSet*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void FileDescriptorSet::MergeFrom(const FileDescriptorSet& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ file_.MergeFrom(from.file_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void FileDescriptorSet::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void FileDescriptorSet::CopyFrom(const FileDescriptorSet& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FileDescriptorSet::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->file())) return false;
+ return true;
+}
+
+void FileDescriptorSet::Swap(FileDescriptorSet* other) {
+ if (other != this) {
+ file_.Swap(&other->file_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata FileDescriptorSet::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = FileDescriptorSet_descriptor_;
+ metadata.reflection = FileDescriptorSet_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FileDescriptorProto::kNameFieldNumber;
+const int FileDescriptorProto::kPackageFieldNumber;
+const int FileDescriptorProto::kDependencyFieldNumber;
+const int FileDescriptorProto::kPublicDependencyFieldNumber;
+const int FileDescriptorProto::kWeakDependencyFieldNumber;
+const int FileDescriptorProto::kMessageTypeFieldNumber;
+const int FileDescriptorProto::kEnumTypeFieldNumber;
+const int FileDescriptorProto::kServiceFieldNumber;
+const int FileDescriptorProto::kExtensionFieldNumber;
+const int FileDescriptorProto::kOptionsFieldNumber;
+const int FileDescriptorProto::kSourceCodeInfoFieldNumber;
+#endif // !_MSC_VER
+
+FileDescriptorProto::FileDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.FileDescriptorProto)
+}
+
+void FileDescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::FileOptions*>(&::google::protobuf::FileOptions::default_instance());
+ source_code_info_ = const_cast< ::google::protobuf::SourceCodeInfo*>(&::google::protobuf::SourceCodeInfo::default_instance());
+}
+
+FileDescriptorProto::FileDescriptorProto(const FileDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.FileDescriptorProto)
+}
+
+void FileDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ options_ = NULL;
+ source_code_info_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FileDescriptorProto::~FileDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.FileDescriptorProto)
+ SharedDtor();
+}
+
+void FileDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete package_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ delete source_code_info_;
+ }
+}
+
+void FileDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* FileDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FileDescriptorProto_descriptor_;
+}
+
+const FileDescriptorProto& FileDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+FileDescriptorProto* FileDescriptorProto::default_instance_ = NULL;
+
+FileDescriptorProto* FileDescriptorProto::New() const {
+ return new FileDescriptorProto;
+}
+
+void FileDescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_package()) {
+ if (package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ package_->clear();
+ }
+ }
+ }
+ if (_has_bits_[8 / 32] & 1536) {
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::FileOptions::Clear();
+ }
+ if (has_source_code_info()) {
+ if (source_code_info_ != NULL) source_code_info_->::google::protobuf::SourceCodeInfo::Clear();
+ }
+ }
+ dependency_.Clear();
+ public_dependency_.Clear();
+ weak_dependency_.Clear();
+ message_type_.Clear();
+ enum_type_.Clear();
+ service_.Clear();
+ extension_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool FileDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.FileDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_package;
+ break;
+ }
+
+ // optional string package = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_package:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_package()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->package().data(), this->package().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "package");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_dependency;
+ break;
+ }
+
+ // repeated string dependency = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_dependency:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->add_dependency()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->dependency(this->dependency_size() - 1).data(),
+ this->dependency(this->dependency_size() - 1).length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "dependency");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_dependency;
+ if (input->ExpectTag(34)) goto parse_message_type;
+ break;
+ }
+
+ // repeated .google.protobuf.DescriptorProto message_type = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_message_type:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_message_type()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_message_type;
+ if (input->ExpectTag(42)) goto parse_enum_type;
+ break;
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_enum_type:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_enum_type()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_enum_type;
+ if (input->ExpectTag(50)) goto parse_service;
+ break;
+ }
+
+ // repeated .google.protobuf.ServiceDescriptorProto service = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_service:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_service()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_service;
+ if (input->ExpectTag(58)) goto parse_extension;
+ break;
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_extension:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_extension()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_extension;
+ if (input->ExpectTag(66)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.FileOptions options = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(74)) goto parse_source_code_info;
+ break;
+ }
+
+ // optional .google.protobuf.SourceCodeInfo source_code_info = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_source_code_info:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_source_code_info()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_public_dependency;
+ break;
+ }
+
+ // repeated int32 public_dependency = 10;
+ case 10: {
+ if (tag == 80) {
+ parse_public_dependency:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ 1, 80, input, this->mutable_public_dependency())));
+ } else if (tag == 82) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitiveNoInline<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, this->mutable_public_dependency())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_public_dependency;
+ if (input->ExpectTag(88)) goto parse_weak_dependency;
+ break;
+ }
+
+ // repeated int32 weak_dependency = 11;
+ case 11: {
+ if (tag == 88) {
+ parse_weak_dependency:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ 1, 88, input, this->mutable_weak_dependency())));
+ } else if (tag == 90) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitiveNoInline<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, this->mutable_weak_dependency())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(88)) goto parse_weak_dependency;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.FileDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.FileDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void FileDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.FileDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional string package = 2;
+ if (has_package()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->package().data(), this->package().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "package");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->package(), output);
+ }
+
+ // repeated string dependency = 3;
+ for (int i = 0; i < this->dependency_size(); i++) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->dependency(i).data(), this->dependency(i).length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "dependency");
+ ::google::protobuf::internal::WireFormatLite::WriteString(
+ 3, this->dependency(i), output);
+ }
+
+ // repeated .google.protobuf.DescriptorProto message_type = 4;
+ for (int i = 0; i < this->message_type_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 4, this->message_type(i), output);
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 5;
+ for (int i = 0; i < this->enum_type_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 5, this->enum_type(i), output);
+ }
+
+ // repeated .google.protobuf.ServiceDescriptorProto service = 6;
+ for (int i = 0; i < this->service_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 6, this->service(i), output);
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 7;
+ for (int i = 0; i < this->extension_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 7, this->extension(i), output);
+ }
+
+ // optional .google.protobuf.FileOptions options = 8;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 8, this->options(), output);
+ }
+
+ // optional .google.protobuf.SourceCodeInfo source_code_info = 9;
+ if (has_source_code_info()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 9, this->source_code_info(), output);
+ }
+
+ // repeated int32 public_dependency = 10;
+ for (int i = 0; i < this->public_dependency_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(
+ 10, this->public_dependency(i), output);
+ }
+
+ // repeated int32 weak_dependency = 11;
+ for (int i = 0; i < this->weak_dependency_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(
+ 11, this->weak_dependency(i), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.FileDescriptorProto)
+}
+
+::google::protobuf::uint8* FileDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.FileDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // optional string package = 2;
+ if (has_package()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->package().data(), this->package().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "package");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 2, this->package(), target);
+ }
+
+ // repeated string dependency = 3;
+ for (int i = 0; i < this->dependency_size(); i++) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->dependency(i).data(), this->dependency(i).length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "dependency");
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteStringToArray(3, this->dependency(i), target);
+ }
+
+ // repeated .google.protobuf.DescriptorProto message_type = 4;
+ for (int i = 0; i < this->message_type_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 4, this->message_type(i), target);
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 5;
+ for (int i = 0; i < this->enum_type_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 5, this->enum_type(i), target);
+ }
+
+ // repeated .google.protobuf.ServiceDescriptorProto service = 6;
+ for (int i = 0; i < this->service_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 6, this->service(i), target);
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 7;
+ for (int i = 0; i < this->extension_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 7, this->extension(i), target);
+ }
+
+ // optional .google.protobuf.FileOptions options = 8;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 8, this->options(), target);
+ }
+
+ // optional .google.protobuf.SourceCodeInfo source_code_info = 9;
+ if (has_source_code_info()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 9, this->source_code_info(), target);
+ }
+
+ // repeated int32 public_dependency = 10;
+ for (int i = 0; i < this->public_dependency_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteInt32ToArray(10, this->public_dependency(i), target);
+ }
+
+ // repeated int32 weak_dependency = 11;
+ for (int i = 0; i < this->weak_dependency_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteInt32ToArray(11, this->weak_dependency(i), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.FileDescriptorProto)
+ return target;
+}
+
+int FileDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional string package = 2;
+ if (has_package()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->package());
+ }
+
+ }
+ if (_has_bits_[9 / 32] & (0xffu << (9 % 32))) {
+ // optional .google.protobuf.FileOptions options = 8;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ // optional .google.protobuf.SourceCodeInfo source_code_info = 9;
+ if (has_source_code_info()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->source_code_info());
+ }
+
+ }
+ // repeated string dependency = 3;
+ total_size += 1 * this->dependency_size();
+ for (int i = 0; i < this->dependency_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->dependency(i));
+ }
+
+ // repeated int32 public_dependency = 10;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->public_dependency_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ Int32Size(this->public_dependency(i));
+ }
+ total_size += 1 * this->public_dependency_size() + data_size;
+ }
+
+ // repeated int32 weak_dependency = 11;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->weak_dependency_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ Int32Size(this->weak_dependency(i));
+ }
+ total_size += 1 * this->weak_dependency_size() + data_size;
+ }
+
+ // repeated .google.protobuf.DescriptorProto message_type = 4;
+ total_size += 1 * this->message_type_size();
+ for (int i = 0; i < this->message_type_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->message_type(i));
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 5;
+ total_size += 1 * this->enum_type_size();
+ for (int i = 0; i < this->enum_type_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->enum_type(i));
+ }
+
+ // repeated .google.protobuf.ServiceDescriptorProto service = 6;
+ total_size += 1 * this->service_size();
+ for (int i = 0; i < this->service_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->service(i));
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 7;
+ total_size += 1 * this->extension_size();
+ for (int i = 0; i < this->extension_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->extension(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FileDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const FileDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const FileDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void FileDescriptorProto::MergeFrom(const FileDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ dependency_.MergeFrom(from.dependency_);
+ public_dependency_.MergeFrom(from.public_dependency_);
+ weak_dependency_.MergeFrom(from.weak_dependency_);
+ message_type_.MergeFrom(from.message_type_);
+ enum_type_.MergeFrom(from.enum_type_);
+ service_.MergeFrom(from.service_);
+ extension_.MergeFrom(from.extension_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_package()) {
+ set_package(from.package());
+ }
+ }
+ if (from._has_bits_[9 / 32] & (0xffu << (9 % 32))) {
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::FileOptions::MergeFrom(from.options());
+ }
+ if (from.has_source_code_info()) {
+ mutable_source_code_info()->::google::protobuf::SourceCodeInfo::MergeFrom(from.source_code_info());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void FileDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void FileDescriptorProto::CopyFrom(const FileDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FileDescriptorProto::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->message_type())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->enum_type())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->service())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->extension())) return false;
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void FileDescriptorProto::Swap(FileDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(package_, other->package_);
+ dependency_.Swap(&other->dependency_);
+ public_dependency_.Swap(&other->public_dependency_);
+ weak_dependency_.Swap(&other->weak_dependency_);
+ message_type_.Swap(&other->message_type_);
+ enum_type_.Swap(&other->enum_type_);
+ service_.Swap(&other->service_);
+ extension_.Swap(&other->extension_);
+ std::swap(options_, other->options_);
+ std::swap(source_code_info_, other->source_code_info_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata FileDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = FileDescriptorProto_descriptor_;
+ metadata.reflection = FileDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int DescriptorProto_ExtensionRange::kStartFieldNumber;
+const int DescriptorProto_ExtensionRange::kEndFieldNumber;
+#endif // !_MSC_VER
+
+DescriptorProto_ExtensionRange::DescriptorProto_ExtensionRange()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.DescriptorProto.ExtensionRange)
+}
+
+void DescriptorProto_ExtensionRange::InitAsDefaultInstance() {
+}
+
+DescriptorProto_ExtensionRange::DescriptorProto_ExtensionRange(const DescriptorProto_ExtensionRange& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.DescriptorProto.ExtensionRange)
+}
+
+void DescriptorProto_ExtensionRange::SharedCtor() {
+ _cached_size_ = 0;
+ start_ = 0;
+ end_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+DescriptorProto_ExtensionRange::~DescriptorProto_ExtensionRange() {
+ // @@protoc_insertion_point(destructor:google.protobuf.DescriptorProto.ExtensionRange)
+ SharedDtor();
+}
+
+void DescriptorProto_ExtensionRange::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void DescriptorProto_ExtensionRange::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* DescriptorProto_ExtensionRange::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return DescriptorProto_ExtensionRange_descriptor_;
+}
+
+const DescriptorProto_ExtensionRange& DescriptorProto_ExtensionRange::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+DescriptorProto_ExtensionRange* DescriptorProto_ExtensionRange::default_instance_ = NULL;
+
+DescriptorProto_ExtensionRange* DescriptorProto_ExtensionRange::New() const {
+ return new DescriptorProto_ExtensionRange;
+}
+
+void DescriptorProto_ExtensionRange::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<DescriptorProto_ExtensionRange*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ ZR_(start_, end_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool DescriptorProto_ExtensionRange::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.DescriptorProto.ExtensionRange)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int32 start = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &start_)));
+ set_has_start();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_end;
+ break;
+ }
+
+ // optional int32 end = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_end:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &end_)));
+ set_has_end();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.DescriptorProto.ExtensionRange)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.DescriptorProto.ExtensionRange)
+ return false;
+#undef DO_
+}
+
+void DescriptorProto_ExtensionRange::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.DescriptorProto.ExtensionRange)
+ // optional int32 start = 1;
+ if (has_start()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->start(), output);
+ }
+
+ // optional int32 end = 2;
+ if (has_end()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->end(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.DescriptorProto.ExtensionRange)
+}
+
+::google::protobuf::uint8* DescriptorProto_ExtensionRange::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.DescriptorProto.ExtensionRange)
+ // optional int32 start = 1;
+ if (has_start()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteInt32ToArray(1, this->start(), target);
+ }
+
+ // optional int32 end = 2;
+ if (has_end()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteInt32ToArray(2, this->end(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.DescriptorProto.ExtensionRange)
+ return target;
+}
+
+int DescriptorProto_ExtensionRange::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int32 start = 1;
+ if (has_start()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->start());
+ }
+
+ // optional int32 end = 2;
+ if (has_end()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->end());
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void DescriptorProto_ExtensionRange::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const DescriptorProto_ExtensionRange* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const DescriptorProto_ExtensionRange*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void DescriptorProto_ExtensionRange::MergeFrom(const DescriptorProto_ExtensionRange& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_start()) {
+ set_start(from.start());
+ }
+ if (from.has_end()) {
+ set_end(from.end());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void DescriptorProto_ExtensionRange::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void DescriptorProto_ExtensionRange::CopyFrom(const DescriptorProto_ExtensionRange& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool DescriptorProto_ExtensionRange::IsInitialized() const {
+
+ return true;
+}
+
+void DescriptorProto_ExtensionRange::Swap(DescriptorProto_ExtensionRange* other) {
+ if (other != this) {
+ std::swap(start_, other->start_);
+ std::swap(end_, other->end_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata DescriptorProto_ExtensionRange::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = DescriptorProto_ExtensionRange_descriptor_;
+ metadata.reflection = DescriptorProto_ExtensionRange_reflection_;
+ return metadata;
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int DescriptorProto::kNameFieldNumber;
+const int DescriptorProto::kFieldFieldNumber;
+const int DescriptorProto::kExtensionFieldNumber;
+const int DescriptorProto::kNestedTypeFieldNumber;
+const int DescriptorProto::kEnumTypeFieldNumber;
+const int DescriptorProto::kExtensionRangeFieldNumber;
+const int DescriptorProto::kOneofDeclFieldNumber;
+const int DescriptorProto::kOptionsFieldNumber;
+#endif // !_MSC_VER
+
+DescriptorProto::DescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.DescriptorProto)
+}
+
+void DescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::MessageOptions*>(&::google::protobuf::MessageOptions::default_instance());
+}
+
+DescriptorProto::DescriptorProto(const DescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.DescriptorProto)
+}
+
+void DescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ options_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+DescriptorProto::~DescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.DescriptorProto)
+ SharedDtor();
+}
+
+void DescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ }
+}
+
+void DescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* DescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return DescriptorProto_descriptor_;
+}
+
+const DescriptorProto& DescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+DescriptorProto* DescriptorProto::default_instance_ = NULL;
+
+DescriptorProto* DescriptorProto::New() const {
+ return new DescriptorProto;
+}
+
+void DescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 129) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::MessageOptions::Clear();
+ }
+ }
+ field_.Clear();
+ extension_.Clear();
+ nested_type_.Clear();
+ enum_type_.Clear();
+ extension_range_.Clear();
+ oneof_decl_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool DescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.DescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_field;
+ break;
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto field = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_field:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_field()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_field;
+ if (input->ExpectTag(26)) goto parse_nested_type;
+ break;
+ }
+
+ // repeated .google.protobuf.DescriptorProto nested_type = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_nested_type:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_nested_type()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_nested_type;
+ if (input->ExpectTag(34)) goto parse_enum_type;
+ break;
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_enum_type:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_enum_type()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_enum_type;
+ if (input->ExpectTag(42)) goto parse_extension_range;
+ break;
+ }
+
+ // repeated .google.protobuf.DescriptorProto.ExtensionRange extension_range = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_extension_range:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_extension_range()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_extension_range;
+ if (input->ExpectTag(50)) goto parse_extension;
+ break;
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_extension:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_extension()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_extension;
+ if (input->ExpectTag(58)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.MessageOptions options = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_oneof_decl;
+ break;
+ }
+
+ // repeated .google.protobuf.OneofDescriptorProto oneof_decl = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_oneof_decl:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_oneof_decl()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_oneof_decl;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.DescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.DescriptorProto)
+ return false;
+#undef DO_
+}
+
+void DescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.DescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto field = 2;
+ for (int i = 0; i < this->field_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 2, this->field(i), output);
+ }
+
+ // repeated .google.protobuf.DescriptorProto nested_type = 3;
+ for (int i = 0; i < this->nested_type_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 3, this->nested_type(i), output);
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 4;
+ for (int i = 0; i < this->enum_type_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 4, this->enum_type(i), output);
+ }
+
+ // repeated .google.protobuf.DescriptorProto.ExtensionRange extension_range = 5;
+ for (int i = 0; i < this->extension_range_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 5, this->extension_range(i), output);
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 6;
+ for (int i = 0; i < this->extension_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 6, this->extension(i), output);
+ }
+
+ // optional .google.protobuf.MessageOptions options = 7;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 7, this->options(), output);
+ }
+
+ // repeated .google.protobuf.OneofDescriptorProto oneof_decl = 8;
+ for (int i = 0; i < this->oneof_decl_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 8, this->oneof_decl(i), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.DescriptorProto)
+}
+
+::google::protobuf::uint8* DescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.DescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto field = 2;
+ for (int i = 0; i < this->field_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 2, this->field(i), target);
+ }
+
+ // repeated .google.protobuf.DescriptorProto nested_type = 3;
+ for (int i = 0; i < this->nested_type_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 3, this->nested_type(i), target);
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 4;
+ for (int i = 0; i < this->enum_type_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 4, this->enum_type(i), target);
+ }
+
+ // repeated .google.protobuf.DescriptorProto.ExtensionRange extension_range = 5;
+ for (int i = 0; i < this->extension_range_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 5, this->extension_range(i), target);
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 6;
+ for (int i = 0; i < this->extension_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 6, this->extension(i), target);
+ }
+
+ // optional .google.protobuf.MessageOptions options = 7;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 7, this->options(), target);
+ }
+
+ // repeated .google.protobuf.OneofDescriptorProto oneof_decl = 8;
+ for (int i = 0; i < this->oneof_decl_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 8, this->oneof_decl(i), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.DescriptorProto)
+ return target;
+}
+
+int DescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional .google.protobuf.MessageOptions options = 7;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ }
+ // repeated .google.protobuf.FieldDescriptorProto field = 2;
+ total_size += 1 * this->field_size();
+ for (int i = 0; i < this->field_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->field(i));
+ }
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 6;
+ total_size += 1 * this->extension_size();
+ for (int i = 0; i < this->extension_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->extension(i));
+ }
+
+ // repeated .google.protobuf.DescriptorProto nested_type = 3;
+ total_size += 1 * this->nested_type_size();
+ for (int i = 0; i < this->nested_type_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->nested_type(i));
+ }
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 4;
+ total_size += 1 * this->enum_type_size();
+ for (int i = 0; i < this->enum_type_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->enum_type(i));
+ }
+
+ // repeated .google.protobuf.DescriptorProto.ExtensionRange extension_range = 5;
+ total_size += 1 * this->extension_range_size();
+ for (int i = 0; i < this->extension_range_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->extension_range(i));
+ }
+
+ // repeated .google.protobuf.OneofDescriptorProto oneof_decl = 8;
+ total_size += 1 * this->oneof_decl_size();
+ for (int i = 0; i < this->oneof_decl_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->oneof_decl(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void DescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const DescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const DescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void DescriptorProto::MergeFrom(const DescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ field_.MergeFrom(from.field_);
+ extension_.MergeFrom(from.extension_);
+ nested_type_.MergeFrom(from.nested_type_);
+ enum_type_.MergeFrom(from.enum_type_);
+ extension_range_.MergeFrom(from.extension_range_);
+ oneof_decl_.MergeFrom(from.oneof_decl_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::MessageOptions::MergeFrom(from.options());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void DescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void DescriptorProto::CopyFrom(const DescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool DescriptorProto::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->field())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->extension())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->nested_type())) return false;
+ if (!::google::protobuf::internal::AllAreInitialized(this->enum_type())) return false;
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void DescriptorProto::Swap(DescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ field_.Swap(&other->field_);
+ extension_.Swap(&other->extension_);
+ nested_type_.Swap(&other->nested_type_);
+ enum_type_.Swap(&other->enum_type_);
+ extension_range_.Swap(&other->extension_range_);
+ oneof_decl_.Swap(&other->oneof_decl_);
+ std::swap(options_, other->options_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata DescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = DescriptorProto_descriptor_;
+ metadata.reflection = DescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+const ::google::protobuf::EnumDescriptor* FieldDescriptorProto_Type_descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FieldDescriptorProto_Type_descriptor_;
+}
+bool FieldDescriptorProto_Type_IsValid(int value) {
+ switch(value) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ case 16:
+ case 17:
+ case 18:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_DOUBLE;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_FLOAT;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_INT64;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_UINT64;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_INT32;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_FIXED64;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_FIXED32;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_BOOL;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_STRING;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_GROUP;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_MESSAGE;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_BYTES;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_UINT32;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_ENUM;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_SFIXED32;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_SFIXED64;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_SINT32;
+const FieldDescriptorProto_Type FieldDescriptorProto::TYPE_SINT64;
+const FieldDescriptorProto_Type FieldDescriptorProto::Type_MIN;
+const FieldDescriptorProto_Type FieldDescriptorProto::Type_MAX;
+const int FieldDescriptorProto::Type_ARRAYSIZE;
+#endif // _MSC_VER
+const ::google::protobuf::EnumDescriptor* FieldDescriptorProto_Label_descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FieldDescriptorProto_Label_descriptor_;
+}
+bool FieldDescriptorProto_Label_IsValid(int value) {
+ switch(value) {
+ case 1:
+ case 2:
+ case 3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const FieldDescriptorProto_Label FieldDescriptorProto::LABEL_OPTIONAL;
+const FieldDescriptorProto_Label FieldDescriptorProto::LABEL_REQUIRED;
+const FieldDescriptorProto_Label FieldDescriptorProto::LABEL_REPEATED;
+const FieldDescriptorProto_Label FieldDescriptorProto::Label_MIN;
+const FieldDescriptorProto_Label FieldDescriptorProto::Label_MAX;
+const int FieldDescriptorProto::Label_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int FieldDescriptorProto::kNameFieldNumber;
+const int FieldDescriptorProto::kNumberFieldNumber;
+const int FieldDescriptorProto::kLabelFieldNumber;
+const int FieldDescriptorProto::kTypeFieldNumber;
+const int FieldDescriptorProto::kTypeNameFieldNumber;
+const int FieldDescriptorProto::kExtendeeFieldNumber;
+const int FieldDescriptorProto::kDefaultValueFieldNumber;
+const int FieldDescriptorProto::kOneofIndexFieldNumber;
+const int FieldDescriptorProto::kOptionsFieldNumber;
+#endif // !_MSC_VER
+
+FieldDescriptorProto::FieldDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.FieldDescriptorProto)
+}
+
+void FieldDescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::FieldOptions*>(&::google::protobuf::FieldOptions::default_instance());
+}
+
+FieldDescriptorProto::FieldDescriptorProto(const FieldDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.FieldDescriptorProto)
+}
+
+void FieldDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ number_ = 0;
+ label_ = 1;
+ type_ = 1;
+ type_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ extendee_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ default_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ oneof_index_ = 0;
+ options_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FieldDescriptorProto::~FieldDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.FieldDescriptorProto)
+ SharedDtor();
+}
+
+void FieldDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (type_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete type_name_;
+ }
+ if (extendee_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete extendee_;
+ }
+ if (default_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete default_value_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ }
+}
+
+void FieldDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* FieldDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FieldDescriptorProto_descriptor_;
+}
+
+const FieldDescriptorProto& FieldDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+FieldDescriptorProto* FieldDescriptorProto::default_instance_ = NULL;
+
+FieldDescriptorProto* FieldDescriptorProto::New() const {
+ return new FieldDescriptorProto;
+}
+
+void FieldDescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 255) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ number_ = 0;
+ label_ = 1;
+ type_ = 1;
+ if (has_type_name()) {
+ if (type_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ type_name_->clear();
+ }
+ }
+ if (has_extendee()) {
+ if (extendee_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ extendee_->clear();
+ }
+ }
+ if (has_default_value()) {
+ if (default_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ default_value_->clear();
+ }
+ }
+ oneof_index_ = 0;
+ }
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::FieldOptions::Clear();
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool FieldDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.FieldDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_extendee;
+ break;
+ }
+
+ // optional string extendee = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_extendee:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_extendee()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->extendee().data(), this->extendee().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "extendee");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_number;
+ break;
+ }
+
+ // optional int32 number = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_number:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &number_)));
+ set_has_number();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_label;
+ break;
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Label label = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_label:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::google::protobuf::FieldDescriptorProto_Label_IsValid(value)) {
+ set_label(static_cast< ::google::protobuf::FieldDescriptorProto_Label >(value));
+ } else {
+ mutable_unknown_fields()->AddVarint(4, value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_type;
+ break;
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Type type = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::google::protobuf::FieldDescriptorProto_Type_IsValid(value)) {
+ set_type(static_cast< ::google::protobuf::FieldDescriptorProto_Type >(value));
+ } else {
+ mutable_unknown_fields()->AddVarint(5, value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_type_name;
+ break;
+ }
+
+ // optional string type_name = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_type_name:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_type_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->type_name().data(), this->type_name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "type_name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_default_value;
+ break;
+ }
+
+ // optional string default_value = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_default_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_default_value()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->default_value().data(), this->default_value().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "default_value");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.FieldOptions options = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(72)) goto parse_oneof_index;
+ break;
+ }
+
+ // optional int32 oneof_index = 9;
+ case 9: {
+ if (tag == 72) {
+ parse_oneof_index:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &oneof_index_)));
+ set_has_oneof_index();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.FieldDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.FieldDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void FieldDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.FieldDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional string extendee = 2;
+ if (has_extendee()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->extendee().data(), this->extendee().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "extendee");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->extendee(), output);
+ }
+
+ // optional int32 number = 3;
+ if (has_number()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(3, this->number(), output);
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Label label = 4;
+ if (has_label()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 4, this->label(), output);
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Type type = 5;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 5, this->type(), output);
+ }
+
+ // optional string type_name = 6;
+ if (has_type_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->type_name().data(), this->type_name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "type_name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 6, this->type_name(), output);
+ }
+
+ // optional string default_value = 7;
+ if (has_default_value()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->default_value().data(), this->default_value().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "default_value");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 7, this->default_value(), output);
+ }
+
+ // optional .google.protobuf.FieldOptions options = 8;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 8, this->options(), output);
+ }
+
+ // optional int32 oneof_index = 9;
+ if (has_oneof_index()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(9, this->oneof_index(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.FieldDescriptorProto)
+}
+
+::google::protobuf::uint8* FieldDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.FieldDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // optional string extendee = 2;
+ if (has_extendee()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->extendee().data(), this->extendee().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "extendee");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 2, this->extendee(), target);
+ }
+
+ // optional int32 number = 3;
+ if (has_number()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteInt32ToArray(3, this->number(), target);
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Label label = 4;
+ if (has_label()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteEnumToArray(
+ 4, this->label(), target);
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Type type = 5;
+ if (has_type()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteEnumToArray(
+ 5, this->type(), target);
+ }
+
+ // optional string type_name = 6;
+ if (has_type_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->type_name().data(), this->type_name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "type_name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 6, this->type_name(), target);
+ }
+
+ // optional string default_value = 7;
+ if (has_default_value()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->default_value().data(), this->default_value().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "default_value");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 7, this->default_value(), target);
+ }
+
+ // optional .google.protobuf.FieldOptions options = 8;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 8, this->options(), target);
+ }
+
+ // optional int32 oneof_index = 9;
+ if (has_oneof_index()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteInt32ToArray(9, this->oneof_index(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.FieldDescriptorProto)
+ return target;
+}
+
+int FieldDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional int32 number = 3;
+ if (has_number()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->number());
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Label label = 4;
+ if (has_label()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->label());
+ }
+
+ // optional .google.protobuf.FieldDescriptorProto.Type type = 5;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+ }
+
+ // optional string type_name = 6;
+ if (has_type_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->type_name());
+ }
+
+ // optional string extendee = 2;
+ if (has_extendee()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->extendee());
+ }
+
+ // optional string default_value = 7;
+ if (has_default_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->default_value());
+ }
+
+ // optional int32 oneof_index = 9;
+ if (has_oneof_index()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->oneof_index());
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional .google.protobuf.FieldOptions options = 8;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FieldDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const FieldDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const FieldDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void FieldDescriptorProto::MergeFrom(const FieldDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_number()) {
+ set_number(from.number());
+ }
+ if (from.has_label()) {
+ set_label(from.label());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_type_name()) {
+ set_type_name(from.type_name());
+ }
+ if (from.has_extendee()) {
+ set_extendee(from.extendee());
+ }
+ if (from.has_default_value()) {
+ set_default_value(from.default_value());
+ }
+ if (from.has_oneof_index()) {
+ set_oneof_index(from.oneof_index());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::FieldOptions::MergeFrom(from.options());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void FieldDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void FieldDescriptorProto::CopyFrom(const FieldDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FieldDescriptorProto::IsInitialized() const {
+
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void FieldDescriptorProto::Swap(FieldDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(number_, other->number_);
+ std::swap(label_, other->label_);
+ std::swap(type_, other->type_);
+ std::swap(type_name_, other->type_name_);
+ std::swap(extendee_, other->extendee_);
+ std::swap(default_value_, other->default_value_);
+ std::swap(oneof_index_, other->oneof_index_);
+ std::swap(options_, other->options_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata FieldDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = FieldDescriptorProto_descriptor_;
+ metadata.reflection = FieldDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int OneofDescriptorProto::kNameFieldNumber;
+#endif // !_MSC_VER
+
+OneofDescriptorProto::OneofDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.OneofDescriptorProto)
+}
+
+void OneofDescriptorProto::InitAsDefaultInstance() {
+}
+
+OneofDescriptorProto::OneofDescriptorProto(const OneofDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.OneofDescriptorProto)
+}
+
+void OneofDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+OneofDescriptorProto::~OneofDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.OneofDescriptorProto)
+ SharedDtor();
+}
+
+void OneofDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void OneofDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* OneofDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return OneofDescriptorProto_descriptor_;
+}
+
+const OneofDescriptorProto& OneofDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+OneofDescriptorProto* OneofDescriptorProto::default_instance_ = NULL;
+
+OneofDescriptorProto* OneofDescriptorProto::New() const {
+ return new OneofDescriptorProto;
+}
+
+void OneofDescriptorProto::Clear() {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool OneofDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.OneofDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.OneofDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.OneofDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void OneofDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.OneofDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.OneofDescriptorProto)
+}
+
+::google::protobuf::uint8* OneofDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.OneofDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.OneofDescriptorProto)
+ return target;
+}
+
+int OneofDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void OneofDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const OneofDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const OneofDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void OneofDescriptorProto::MergeFrom(const OneofDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void OneofDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void OneofDescriptorProto::CopyFrom(const OneofDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool OneofDescriptorProto::IsInitialized() const {
+
+ return true;
+}
+
+void OneofDescriptorProto::Swap(OneofDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata OneofDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = OneofDescriptorProto_descriptor_;
+ metadata.reflection = OneofDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int EnumDescriptorProto::kNameFieldNumber;
+const int EnumDescriptorProto::kValueFieldNumber;
+const int EnumDescriptorProto::kOptionsFieldNumber;
+#endif // !_MSC_VER
+
+EnumDescriptorProto::EnumDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.EnumDescriptorProto)
+}
+
+void EnumDescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::EnumOptions*>(&::google::protobuf::EnumOptions::default_instance());
+}
+
+EnumDescriptorProto::EnumDescriptorProto(const EnumDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.EnumDescriptorProto)
+}
+
+void EnumDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ options_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+EnumDescriptorProto::~EnumDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.EnumDescriptorProto)
+ SharedDtor();
+}
+
+void EnumDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ }
+}
+
+void EnumDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* EnumDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return EnumDescriptorProto_descriptor_;
+}
+
+const EnumDescriptorProto& EnumDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+EnumDescriptorProto* EnumDescriptorProto::default_instance_ = NULL;
+
+EnumDescriptorProto* EnumDescriptorProto::New() const {
+ return new EnumDescriptorProto;
+}
+
+void EnumDescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 5) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::EnumOptions::Clear();
+ }
+ }
+ value_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool EnumDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.EnumDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ break;
+ }
+
+ // repeated .google.protobuf.EnumValueDescriptorProto value = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ if (input->ExpectTag(26)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.EnumOptions options = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.EnumDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.EnumDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void EnumDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.EnumDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // repeated .google.protobuf.EnumValueDescriptorProto value = 2;
+ for (int i = 0; i < this->value_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 2, this->value(i), output);
+ }
+
+ // optional .google.protobuf.EnumOptions options = 3;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 3, this->options(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.EnumDescriptorProto)
+}
+
+::google::protobuf::uint8* EnumDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.EnumDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // repeated .google.protobuf.EnumValueDescriptorProto value = 2;
+ for (int i = 0; i < this->value_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 2, this->value(i), target);
+ }
+
+ // optional .google.protobuf.EnumOptions options = 3;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 3, this->options(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.EnumDescriptorProto)
+ return target;
+}
+
+int EnumDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional .google.protobuf.EnumOptions options = 3;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ }
+ // repeated .google.protobuf.EnumValueDescriptorProto value = 2;
+ total_size += 1 * this->value_size();
+ for (int i = 0; i < this->value_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->value(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void EnumDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const EnumDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const EnumDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void EnumDescriptorProto::MergeFrom(const EnumDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ value_.MergeFrom(from.value_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::EnumOptions::MergeFrom(from.options());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void EnumDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void EnumDescriptorProto::CopyFrom(const EnumDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool EnumDescriptorProto::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->value())) return false;
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void EnumDescriptorProto::Swap(EnumDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ value_.Swap(&other->value_);
+ std::swap(options_, other->options_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata EnumDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = EnumDescriptorProto_descriptor_;
+ metadata.reflection = EnumDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int EnumValueDescriptorProto::kNameFieldNumber;
+const int EnumValueDescriptorProto::kNumberFieldNumber;
+const int EnumValueDescriptorProto::kOptionsFieldNumber;
+#endif // !_MSC_VER
+
+EnumValueDescriptorProto::EnumValueDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.EnumValueDescriptorProto)
+}
+
+void EnumValueDescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::EnumValueOptions*>(&::google::protobuf::EnumValueOptions::default_instance());
+}
+
+EnumValueDescriptorProto::EnumValueDescriptorProto(const EnumValueDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.EnumValueDescriptorProto)
+}
+
+void EnumValueDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ number_ = 0;
+ options_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+EnumValueDescriptorProto::~EnumValueDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.EnumValueDescriptorProto)
+ SharedDtor();
+}
+
+void EnumValueDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ }
+}
+
+void EnumValueDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* EnumValueDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return EnumValueDescriptorProto_descriptor_;
+}
+
+const EnumValueDescriptorProto& EnumValueDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+EnumValueDescriptorProto* EnumValueDescriptorProto::default_instance_ = NULL;
+
+EnumValueDescriptorProto* EnumValueDescriptorProto::New() const {
+ return new EnumValueDescriptorProto;
+}
+
+void EnumValueDescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 7) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ number_ = 0;
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::EnumValueOptions::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool EnumValueDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.EnumValueDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_number;
+ break;
+ }
+
+ // optional int32 number = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_number:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &number_)));
+ set_has_number();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.EnumValueOptions options = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.EnumValueDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.EnumValueDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void EnumValueDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.EnumValueDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional int32 number = 2;
+ if (has_number()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->number(), output);
+ }
+
+ // optional .google.protobuf.EnumValueOptions options = 3;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 3, this->options(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.EnumValueDescriptorProto)
+}
+
+::google::protobuf::uint8* EnumValueDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.EnumValueDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // optional int32 number = 2;
+ if (has_number()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteInt32ToArray(2, this->number(), target);
+ }
+
+ // optional .google.protobuf.EnumValueOptions options = 3;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 3, this->options(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.EnumValueDescriptorProto)
+ return target;
+}
+
+int EnumValueDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional int32 number = 2;
+ if (has_number()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->number());
+ }
+
+ // optional .google.protobuf.EnumValueOptions options = 3;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void EnumValueDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const EnumValueDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const EnumValueDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void EnumValueDescriptorProto::MergeFrom(const EnumValueDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_number()) {
+ set_number(from.number());
+ }
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::EnumValueOptions::MergeFrom(from.options());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void EnumValueDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void EnumValueDescriptorProto::CopyFrom(const EnumValueDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool EnumValueDescriptorProto::IsInitialized() const {
+
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void EnumValueDescriptorProto::Swap(EnumValueDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(number_, other->number_);
+ std::swap(options_, other->options_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata EnumValueDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = EnumValueDescriptorProto_descriptor_;
+ metadata.reflection = EnumValueDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ServiceDescriptorProto::kNameFieldNumber;
+const int ServiceDescriptorProto::kMethodFieldNumber;
+const int ServiceDescriptorProto::kOptionsFieldNumber;
+#endif // !_MSC_VER
+
+ServiceDescriptorProto::ServiceDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.ServiceDescriptorProto)
+}
+
+void ServiceDescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::ServiceOptions*>(&::google::protobuf::ServiceOptions::default_instance());
+}
+
+ServiceDescriptorProto::ServiceDescriptorProto(const ServiceDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.ServiceDescriptorProto)
+}
+
+void ServiceDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ options_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ServiceDescriptorProto::~ServiceDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.ServiceDescriptorProto)
+ SharedDtor();
+}
+
+void ServiceDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ }
+}
+
+void ServiceDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* ServiceDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return ServiceDescriptorProto_descriptor_;
+}
+
+const ServiceDescriptorProto& ServiceDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+ServiceDescriptorProto* ServiceDescriptorProto::default_instance_ = NULL;
+
+ServiceDescriptorProto* ServiceDescriptorProto::New() const {
+ return new ServiceDescriptorProto;
+}
+
+void ServiceDescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 5) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::ServiceOptions::Clear();
+ }
+ }
+ method_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool ServiceDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.ServiceDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_method;
+ break;
+ }
+
+ // repeated .google.protobuf.MethodDescriptorProto method = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_method:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_method()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_method;
+ if (input->ExpectTag(26)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.ServiceOptions options = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.ServiceDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.ServiceDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void ServiceDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.ServiceDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // repeated .google.protobuf.MethodDescriptorProto method = 2;
+ for (int i = 0; i < this->method_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 2, this->method(i), output);
+ }
+
+ // optional .google.protobuf.ServiceOptions options = 3;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 3, this->options(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.ServiceDescriptorProto)
+}
+
+::google::protobuf::uint8* ServiceDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.ServiceDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // repeated .google.protobuf.MethodDescriptorProto method = 2;
+ for (int i = 0; i < this->method_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 2, this->method(i), target);
+ }
+
+ // optional .google.protobuf.ServiceOptions options = 3;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 3, this->options(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.ServiceDescriptorProto)
+ return target;
+}
+
+int ServiceDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional .google.protobuf.ServiceOptions options = 3;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ }
+ // repeated .google.protobuf.MethodDescriptorProto method = 2;
+ total_size += 1 * this->method_size();
+ for (int i = 0; i < this->method_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->method(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ServiceDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const ServiceDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const ServiceDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void ServiceDescriptorProto::MergeFrom(const ServiceDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ method_.MergeFrom(from.method_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::ServiceOptions::MergeFrom(from.options());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void ServiceDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void ServiceDescriptorProto::CopyFrom(const ServiceDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ServiceDescriptorProto::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->method())) return false;
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void ServiceDescriptorProto::Swap(ServiceDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ method_.Swap(&other->method_);
+ std::swap(options_, other->options_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata ServiceDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = ServiceDescriptorProto_descriptor_;
+ metadata.reflection = ServiceDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int MethodDescriptorProto::kNameFieldNumber;
+const int MethodDescriptorProto::kInputTypeFieldNumber;
+const int MethodDescriptorProto::kOutputTypeFieldNumber;
+const int MethodDescriptorProto::kOptionsFieldNumber;
+#endif // !_MSC_VER
+
+MethodDescriptorProto::MethodDescriptorProto()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.MethodDescriptorProto)
+}
+
+void MethodDescriptorProto::InitAsDefaultInstance() {
+ options_ = const_cast< ::google::protobuf::MethodOptions*>(&::google::protobuf::MethodOptions::default_instance());
+}
+
+MethodDescriptorProto::MethodDescriptorProto(const MethodDescriptorProto& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.MethodDescriptorProto)
+}
+
+void MethodDescriptorProto::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ input_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ output_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ options_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+MethodDescriptorProto::~MethodDescriptorProto() {
+ // @@protoc_insertion_point(destructor:google.protobuf.MethodDescriptorProto)
+ SharedDtor();
+}
+
+void MethodDescriptorProto::SharedDtor() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (input_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete input_type_;
+ }
+ if (output_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete output_type_;
+ }
+ if (this != default_instance_) {
+ delete options_;
+ }
+}
+
+void MethodDescriptorProto::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* MethodDescriptorProto::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return MethodDescriptorProto_descriptor_;
+}
+
+const MethodDescriptorProto& MethodDescriptorProto::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+MethodDescriptorProto* MethodDescriptorProto::default_instance_ = NULL;
+
+MethodDescriptorProto* MethodDescriptorProto::New() const {
+ return new MethodDescriptorProto;
+}
+
+void MethodDescriptorProto::Clear() {
+ if (_has_bits_[0 / 32] & 15) {
+ if (has_name()) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ }
+ if (has_input_type()) {
+ if (input_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ input_type_->clear();
+ }
+ }
+ if (has_output_type()) {
+ if (output_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ output_type_->clear();
+ }
+ }
+ if (has_options()) {
+ if (options_ != NULL) options_->::google::protobuf::MethodOptions::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool MethodDescriptorProto::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.MethodDescriptorProto)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string name = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_input_type;
+ break;
+ }
+
+ // optional string input_type = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_input_type:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_input_type()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->input_type().data(), this->input_type().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "input_type");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_output_type;
+ break;
+ }
+
+ // optional string output_type = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_output_type:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_output_type()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->output_type().data(), this->output_type().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "output_type");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_options;
+ break;
+ }
+
+ // optional .google.protobuf.MethodOptions options = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_options:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_options()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.MethodDescriptorProto)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.MethodDescriptorProto)
+ return false;
+#undef DO_
+}
+
+void MethodDescriptorProto::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.MethodDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name(), output);
+ }
+
+ // optional string input_type = 2;
+ if (has_input_type()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->input_type().data(), this->input_type().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "input_type");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->input_type(), output);
+ }
+
+ // optional string output_type = 3;
+ if (has_output_type()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->output_type().data(), this->output_type().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "output_type");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->output_type(), output);
+ }
+
+ // optional .google.protobuf.MethodOptions options = 4;
+ if (has_options()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 4, this->options(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.MethodDescriptorProto)
+}
+
+::google::protobuf::uint8* MethodDescriptorProto::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.MethodDescriptorProto)
+ // optional string name = 1;
+ if (has_name()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name().data(), this->name().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name(), target);
+ }
+
+ // optional string input_type = 2;
+ if (has_input_type()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->input_type().data(), this->input_type().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "input_type");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 2, this->input_type(), target);
+ }
+
+ // optional string output_type = 3;
+ if (has_output_type()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->output_type().data(), this->output_type().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "output_type");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 3, this->output_type(), target);
+ }
+
+ // optional .google.protobuf.MethodOptions options = 4;
+ if (has_options()) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 4, this->options(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.MethodDescriptorProto)
+ return target;
+}
+
+int MethodDescriptorProto::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string name = 1;
+ if (has_name()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name());
+ }
+
+ // optional string input_type = 2;
+ if (has_input_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->input_type());
+ }
+
+ // optional string output_type = 3;
+ if (has_output_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->output_type());
+ }
+
+ // optional .google.protobuf.MethodOptions options = 4;
+ if (has_options()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->options());
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void MethodDescriptorProto::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const MethodDescriptorProto* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const MethodDescriptorProto*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void MethodDescriptorProto::MergeFrom(const MethodDescriptorProto& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name()) {
+ set_name(from.name());
+ }
+ if (from.has_input_type()) {
+ set_input_type(from.input_type());
+ }
+ if (from.has_output_type()) {
+ set_output_type(from.output_type());
+ }
+ if (from.has_options()) {
+ mutable_options()->::google::protobuf::MethodOptions::MergeFrom(from.options());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void MethodDescriptorProto::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void MethodDescriptorProto::CopyFrom(const MethodDescriptorProto& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool MethodDescriptorProto::IsInitialized() const {
+
+ if (has_options()) {
+ if (!this->options().IsInitialized()) return false;
+ }
+ return true;
+}
+
+void MethodDescriptorProto::Swap(MethodDescriptorProto* other) {
+ if (other != this) {
+ std::swap(name_, other->name_);
+ std::swap(input_type_, other->input_type_);
+ std::swap(output_type_, other->output_type_);
+ std::swap(options_, other->options_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata MethodDescriptorProto::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = MethodDescriptorProto_descriptor_;
+ metadata.reflection = MethodDescriptorProto_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+const ::google::protobuf::EnumDescriptor* FileOptions_OptimizeMode_descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FileOptions_OptimizeMode_descriptor_;
+}
+bool FileOptions_OptimizeMode_IsValid(int value) {
+ switch(value) {
+ case 1:
+ case 2:
+ case 3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const FileOptions_OptimizeMode FileOptions::SPEED;
+const FileOptions_OptimizeMode FileOptions::CODE_SIZE;
+const FileOptions_OptimizeMode FileOptions::LITE_RUNTIME;
+const FileOptions_OptimizeMode FileOptions::OptimizeMode_MIN;
+const FileOptions_OptimizeMode FileOptions::OptimizeMode_MAX;
+const int FileOptions::OptimizeMode_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int FileOptions::kJavaPackageFieldNumber;
+const int FileOptions::kJavaOuterClassnameFieldNumber;
+const int FileOptions::kJavaMultipleFilesFieldNumber;
+const int FileOptions::kJavaGenerateEqualsAndHashFieldNumber;
+const int FileOptions::kJavaStringCheckUtf8FieldNumber;
+const int FileOptions::kOptimizeForFieldNumber;
+const int FileOptions::kGoPackageFieldNumber;
+const int FileOptions::kCcGenericServicesFieldNumber;
+const int FileOptions::kJavaGenericServicesFieldNumber;
+const int FileOptions::kPyGenericServicesFieldNumber;
+const int FileOptions::kDeprecatedFieldNumber;
+const int FileOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+FileOptions::FileOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.FileOptions)
+}
+
+void FileOptions::InitAsDefaultInstance() {
+}
+
+FileOptions::FileOptions(const FileOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.FileOptions)
+}
+
+void FileOptions::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ java_package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ java_outer_classname_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ java_multiple_files_ = false;
+ java_generate_equals_and_hash_ = false;
+ java_string_check_utf8_ = false;
+ optimize_for_ = 1;
+ go_package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ cc_generic_services_ = false;
+ java_generic_services_ = false;
+ py_generic_services_ = false;
+ deprecated_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FileOptions::~FileOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.FileOptions)
+ SharedDtor();
+}
+
+void FileOptions::SharedDtor() {
+ if (java_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete java_package_;
+ }
+ if (java_outer_classname_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete java_outer_classname_;
+ }
+ if (go_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete go_package_;
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void FileOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* FileOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FileOptions_descriptor_;
+}
+
+const FileOptions& FileOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+FileOptions* FileOptions::default_instance_ = NULL;
+
+FileOptions* FileOptions::New() const {
+ return new FileOptions;
+}
+
+void FileOptions::Clear() {
+ _extensions_.Clear();
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<FileOptions*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 255) {
+ ZR_(java_multiple_files_, cc_generic_services_);
+ if (has_java_package()) {
+ if (java_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_package_->clear();
+ }
+ }
+ if (has_java_outer_classname()) {
+ if (java_outer_classname_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_outer_classname_->clear();
+ }
+ }
+ optimize_for_ = 1;
+ if (has_go_package()) {
+ if (go_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ go_package_->clear();
+ }
+ }
+ }
+ ZR_(java_generic_services_, deprecated_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool FileOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.FileOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string java_package = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_java_package()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->java_package().data(), this->java_package().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "java_package");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_java_outer_classname;
+ break;
+ }
+
+ // optional string java_outer_classname = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_java_outer_classname:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_java_outer_classname()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->java_outer_classname().data(), this->java_outer_classname().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "java_outer_classname");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(72)) goto parse_optimize_for;
+ break;
+ }
+
+ // optional .google.protobuf.FileOptions.OptimizeMode optimize_for = 9 [default = SPEED];
+ case 9: {
+ if (tag == 72) {
+ parse_optimize_for:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::google::protobuf::FileOptions_OptimizeMode_IsValid(value)) {
+ set_optimize_for(static_cast< ::google::protobuf::FileOptions_OptimizeMode >(value));
+ } else {
+ mutable_unknown_fields()->AddVarint(9, value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_java_multiple_files;
+ break;
+ }
+
+ // optional bool java_multiple_files = 10 [default = false];
+ case 10: {
+ if (tag == 80) {
+ parse_java_multiple_files:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &java_multiple_files_)));
+ set_has_java_multiple_files();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(90)) goto parse_go_package;
+ break;
+ }
+
+ // optional string go_package = 11;
+ case 11: {
+ if (tag == 90) {
+ parse_go_package:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_go_package()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->go_package().data(), this->go_package().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "go_package");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(128)) goto parse_cc_generic_services;
+ break;
+ }
+
+ // optional bool cc_generic_services = 16 [default = false];
+ case 16: {
+ if (tag == 128) {
+ parse_cc_generic_services:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &cc_generic_services_)));
+ set_has_cc_generic_services();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(136)) goto parse_java_generic_services;
+ break;
+ }
+
+ // optional bool java_generic_services = 17 [default = false];
+ case 17: {
+ if (tag == 136) {
+ parse_java_generic_services:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &java_generic_services_)));
+ set_has_java_generic_services();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(144)) goto parse_py_generic_services;
+ break;
+ }
+
+ // optional bool py_generic_services = 18 [default = false];
+ case 18: {
+ if (tag == 144) {
+ parse_py_generic_services:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &py_generic_services_)));
+ set_has_py_generic_services();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(160)) goto parse_java_generate_equals_and_hash;
+ break;
+ }
+
+ // optional bool java_generate_equals_and_hash = 20 [default = false];
+ case 20: {
+ if (tag == 160) {
+ parse_java_generate_equals_and_hash:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &java_generate_equals_and_hash_)));
+ set_has_java_generate_equals_and_hash();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(184)) goto parse_deprecated;
+ break;
+ }
+
+ // optional bool deprecated = 23 [default = false];
+ case 23: {
+ if (tag == 184) {
+ parse_deprecated:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(216)) goto parse_java_string_check_utf8;
+ break;
+ }
+
+ // optional bool java_string_check_utf8 = 27 [default = false];
+ case 27: {
+ if (tag == 216) {
+ parse_java_string_check_utf8:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &java_string_check_utf8_)));
+ set_has_java_string_check_utf8();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.FileOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.FileOptions)
+ return false;
+#undef DO_
+}
+
+void FileOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.FileOptions)
+ // optional string java_package = 1;
+ if (has_java_package()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->java_package().data(), this->java_package().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "java_package");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->java_package(), output);
+ }
+
+ // optional string java_outer_classname = 8;
+ if (has_java_outer_classname()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->java_outer_classname().data(), this->java_outer_classname().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "java_outer_classname");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 8, this->java_outer_classname(), output);
+ }
+
+ // optional .google.protobuf.FileOptions.OptimizeMode optimize_for = 9 [default = SPEED];
+ if (has_optimize_for()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 9, this->optimize_for(), output);
+ }
+
+ // optional bool java_multiple_files = 10 [default = false];
+ if (has_java_multiple_files()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(10, this->java_multiple_files(), output);
+ }
+
+ // optional string go_package = 11;
+ if (has_go_package()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->go_package().data(), this->go_package().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "go_package");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 11, this->go_package(), output);
+ }
+
+ // optional bool cc_generic_services = 16 [default = false];
+ if (has_cc_generic_services()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(16, this->cc_generic_services(), output);
+ }
+
+ // optional bool java_generic_services = 17 [default = false];
+ if (has_java_generic_services()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(17, this->java_generic_services(), output);
+ }
+
+ // optional bool py_generic_services = 18 [default = false];
+ if (has_py_generic_services()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(18, this->py_generic_services(), output);
+ }
+
+ // optional bool java_generate_equals_and_hash = 20 [default = false];
+ if (has_java_generate_equals_and_hash()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(20, this->java_generate_equals_and_hash(), output);
+ }
+
+ // optional bool deprecated = 23 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(23, this->deprecated(), output);
+ }
+
+ // optional bool java_string_check_utf8 = 27 [default = false];
+ if (has_java_string_check_utf8()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(27, this->java_string_check_utf8(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.FileOptions)
+}
+
+::google::protobuf::uint8* FileOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.FileOptions)
+ // optional string java_package = 1;
+ if (has_java_package()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->java_package().data(), this->java_package().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "java_package");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->java_package(), target);
+ }
+
+ // optional string java_outer_classname = 8;
+ if (has_java_outer_classname()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->java_outer_classname().data(), this->java_outer_classname().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "java_outer_classname");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 8, this->java_outer_classname(), target);
+ }
+
+ // optional .google.protobuf.FileOptions.OptimizeMode optimize_for = 9 [default = SPEED];
+ if (has_optimize_for()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteEnumToArray(
+ 9, this->optimize_for(), target);
+ }
+
+ // optional bool java_multiple_files = 10 [default = false];
+ if (has_java_multiple_files()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(10, this->java_multiple_files(), target);
+ }
+
+ // optional string go_package = 11;
+ if (has_go_package()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->go_package().data(), this->go_package().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "go_package");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 11, this->go_package(), target);
+ }
+
+ // optional bool cc_generic_services = 16 [default = false];
+ if (has_cc_generic_services()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(16, this->cc_generic_services(), target);
+ }
+
+ // optional bool java_generic_services = 17 [default = false];
+ if (has_java_generic_services()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(17, this->java_generic_services(), target);
+ }
+
+ // optional bool py_generic_services = 18 [default = false];
+ if (has_py_generic_services()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(18, this->py_generic_services(), target);
+ }
+
+ // optional bool java_generate_equals_and_hash = 20 [default = false];
+ if (has_java_generate_equals_and_hash()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(20, this->java_generate_equals_and_hash(), target);
+ }
+
+ // optional bool deprecated = 23 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(23, this->deprecated(), target);
+ }
+
+ // optional bool java_string_check_utf8 = 27 [default = false];
+ if (has_java_string_check_utf8()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(27, this->java_string_check_utf8(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.FileOptions)
+ return target;
+}
+
+int FileOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string java_package = 1;
+ if (has_java_package()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->java_package());
+ }
+
+ // optional string java_outer_classname = 8;
+ if (has_java_outer_classname()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->java_outer_classname());
+ }
+
+ // optional bool java_multiple_files = 10 [default = false];
+ if (has_java_multiple_files()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool java_generate_equals_and_hash = 20 [default = false];
+ if (has_java_generate_equals_and_hash()) {
+ total_size += 2 + 1;
+ }
+
+ // optional bool java_string_check_utf8 = 27 [default = false];
+ if (has_java_string_check_utf8()) {
+ total_size += 2 + 1;
+ }
+
+ // optional .google.protobuf.FileOptions.OptimizeMode optimize_for = 9 [default = SPEED];
+ if (has_optimize_for()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->optimize_for());
+ }
+
+ // optional string go_package = 11;
+ if (has_go_package()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->go_package());
+ }
+
+ // optional bool cc_generic_services = 16 [default = false];
+ if (has_cc_generic_services()) {
+ total_size += 2 + 1;
+ }
+
+ }
+ if (_has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ // optional bool java_generic_services = 17 [default = false];
+ if (has_java_generic_services()) {
+ total_size += 2 + 1;
+ }
+
+ // optional bool py_generic_services = 18 [default = false];
+ if (has_py_generic_services()) {
+ total_size += 2 + 1;
+ }
+
+ // optional bool deprecated = 23 [default = false];
+ if (has_deprecated()) {
+ total_size += 2 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FileOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const FileOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const FileOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void FileOptions::MergeFrom(const FileOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_java_package()) {
+ set_java_package(from.java_package());
+ }
+ if (from.has_java_outer_classname()) {
+ set_java_outer_classname(from.java_outer_classname());
+ }
+ if (from.has_java_multiple_files()) {
+ set_java_multiple_files(from.java_multiple_files());
+ }
+ if (from.has_java_generate_equals_and_hash()) {
+ set_java_generate_equals_and_hash(from.java_generate_equals_and_hash());
+ }
+ if (from.has_java_string_check_utf8()) {
+ set_java_string_check_utf8(from.java_string_check_utf8());
+ }
+ if (from.has_optimize_for()) {
+ set_optimize_for(from.optimize_for());
+ }
+ if (from.has_go_package()) {
+ set_go_package(from.go_package());
+ }
+ if (from.has_cc_generic_services()) {
+ set_cc_generic_services(from.cc_generic_services());
+ }
+ }
+ if (from._has_bits_[8 / 32] & (0xffu << (8 % 32))) {
+ if (from.has_java_generic_services()) {
+ set_java_generic_services(from.java_generic_services());
+ }
+ if (from.has_py_generic_services()) {
+ set_py_generic_services(from.py_generic_services());
+ }
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void FileOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void FileOptions::CopyFrom(const FileOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FileOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void FileOptions::Swap(FileOptions* other) {
+ if (other != this) {
+ std::swap(java_package_, other->java_package_);
+ std::swap(java_outer_classname_, other->java_outer_classname_);
+ std::swap(java_multiple_files_, other->java_multiple_files_);
+ std::swap(java_generate_equals_and_hash_, other->java_generate_equals_and_hash_);
+ std::swap(java_string_check_utf8_, other->java_string_check_utf8_);
+ std::swap(optimize_for_, other->optimize_for_);
+ std::swap(go_package_, other->go_package_);
+ std::swap(cc_generic_services_, other->cc_generic_services_);
+ std::swap(java_generic_services_, other->java_generic_services_);
+ std::swap(py_generic_services_, other->py_generic_services_);
+ std::swap(deprecated_, other->deprecated_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata FileOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = FileOptions_descriptor_;
+ metadata.reflection = FileOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int MessageOptions::kMessageSetWireFormatFieldNumber;
+const int MessageOptions::kNoStandardDescriptorAccessorFieldNumber;
+const int MessageOptions::kDeprecatedFieldNumber;
+const int MessageOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+MessageOptions::MessageOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.MessageOptions)
+}
+
+void MessageOptions::InitAsDefaultInstance() {
+}
+
+MessageOptions::MessageOptions(const MessageOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.MessageOptions)
+}
+
+void MessageOptions::SharedCtor() {
+ _cached_size_ = 0;
+ message_set_wire_format_ = false;
+ no_standard_descriptor_accessor_ = false;
+ deprecated_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+MessageOptions::~MessageOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.MessageOptions)
+ SharedDtor();
+}
+
+void MessageOptions::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void MessageOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* MessageOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return MessageOptions_descriptor_;
+}
+
+const MessageOptions& MessageOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+MessageOptions* MessageOptions::default_instance_ = NULL;
+
+MessageOptions* MessageOptions::New() const {
+ return new MessageOptions;
+}
+
+void MessageOptions::Clear() {
+ _extensions_.Clear();
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<MessageOptions*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ ZR_(message_set_wire_format_, deprecated_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool MessageOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.MessageOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bool message_set_wire_format = 1 [default = false];
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &message_set_wire_format_)));
+ set_has_message_set_wire_format();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_no_standard_descriptor_accessor;
+ break;
+ }
+
+ // optional bool no_standard_descriptor_accessor = 2 [default = false];
+ case 2: {
+ if (tag == 16) {
+ parse_no_standard_descriptor_accessor:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &no_standard_descriptor_accessor_)));
+ set_has_no_standard_descriptor_accessor();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_deprecated;
+ break;
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ case 3: {
+ if (tag == 24) {
+ parse_deprecated:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.MessageOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.MessageOptions)
+ return false;
+#undef DO_
+}
+
+void MessageOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.MessageOptions)
+ // optional bool message_set_wire_format = 1 [default = false];
+ if (has_message_set_wire_format()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(1, this->message_set_wire_format(), output);
+ }
+
+ // optional bool no_standard_descriptor_accessor = 2 [default = false];
+ if (has_no_standard_descriptor_accessor()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->no_standard_descriptor_accessor(), output);
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(3, this->deprecated(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.MessageOptions)
+}
+
+::google::protobuf::uint8* MessageOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.MessageOptions)
+ // optional bool message_set_wire_format = 1 [default = false];
+ if (has_message_set_wire_format()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(1, this->message_set_wire_format(), target);
+ }
+
+ // optional bool no_standard_descriptor_accessor = 2 [default = false];
+ if (has_no_standard_descriptor_accessor()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(2, this->no_standard_descriptor_accessor(), target);
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(3, this->deprecated(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.MessageOptions)
+ return target;
+}
+
+int MessageOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bool message_set_wire_format = 1 [default = false];
+ if (has_message_set_wire_format()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool no_standard_descriptor_accessor = 2 [default = false];
+ if (has_no_standard_descriptor_accessor()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void MessageOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const MessageOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const MessageOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void MessageOptions::MergeFrom(const MessageOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_message_set_wire_format()) {
+ set_message_set_wire_format(from.message_set_wire_format());
+ }
+ if (from.has_no_standard_descriptor_accessor()) {
+ set_no_standard_descriptor_accessor(from.no_standard_descriptor_accessor());
+ }
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void MessageOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void MessageOptions::CopyFrom(const MessageOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool MessageOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void MessageOptions::Swap(MessageOptions* other) {
+ if (other != this) {
+ std::swap(message_set_wire_format_, other->message_set_wire_format_);
+ std::swap(no_standard_descriptor_accessor_, other->no_standard_descriptor_accessor_);
+ std::swap(deprecated_, other->deprecated_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata MessageOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = MessageOptions_descriptor_;
+ metadata.reflection = MessageOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+const ::google::protobuf::EnumDescriptor* FieldOptions_CType_descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FieldOptions_CType_descriptor_;
+}
+bool FieldOptions_CType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const FieldOptions_CType FieldOptions::STRING;
+const FieldOptions_CType FieldOptions::CORD;
+const FieldOptions_CType FieldOptions::STRING_PIECE;
+const FieldOptions_CType FieldOptions::CType_MIN;
+const FieldOptions_CType FieldOptions::CType_MAX;
+const int FieldOptions::CType_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int FieldOptions::kCtypeFieldNumber;
+const int FieldOptions::kPackedFieldNumber;
+const int FieldOptions::kLazyFieldNumber;
+const int FieldOptions::kDeprecatedFieldNumber;
+const int FieldOptions::kExperimentalMapKeyFieldNumber;
+const int FieldOptions::kWeakFieldNumber;
+const int FieldOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+FieldOptions::FieldOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.FieldOptions)
+}
+
+void FieldOptions::InitAsDefaultInstance() {
+}
+
+FieldOptions::FieldOptions(const FieldOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.FieldOptions)
+}
+
+void FieldOptions::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ ctype_ = 0;
+ packed_ = false;
+ lazy_ = false;
+ deprecated_ = false;
+ experimental_map_key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ weak_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FieldOptions::~FieldOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.FieldOptions)
+ SharedDtor();
+}
+
+void FieldOptions::SharedDtor() {
+ if (experimental_map_key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete experimental_map_key_;
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void FieldOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* FieldOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return FieldOptions_descriptor_;
+}
+
+const FieldOptions& FieldOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+FieldOptions* FieldOptions::default_instance_ = NULL;
+
+FieldOptions* FieldOptions::New() const {
+ return new FieldOptions;
+}
+
+void FieldOptions::Clear() {
+ _extensions_.Clear();
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<FieldOptions*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 63) {
+ ZR_(ctype_, weak_);
+ if (has_experimental_map_key()) {
+ if (experimental_map_key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ experimental_map_key_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool FieldOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.FieldOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .google.protobuf.FieldOptions.CType ctype = 1 [default = STRING];
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::google::protobuf::FieldOptions_CType_IsValid(value)) {
+ set_ctype(static_cast< ::google::protobuf::FieldOptions_CType >(value));
+ } else {
+ mutable_unknown_fields()->AddVarint(1, value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_packed;
+ break;
+ }
+
+ // optional bool packed = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_packed:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &packed_)));
+ set_has_packed();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_deprecated;
+ break;
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ case 3: {
+ if (tag == 24) {
+ parse_deprecated:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_lazy;
+ break;
+ }
+
+ // optional bool lazy = 5 [default = false];
+ case 5: {
+ if (tag == 40) {
+ parse_lazy:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &lazy_)));
+ set_has_lazy();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(74)) goto parse_experimental_map_key;
+ break;
+ }
+
+ // optional string experimental_map_key = 9;
+ case 9: {
+ if (tag == 74) {
+ parse_experimental_map_key:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_experimental_map_key()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->experimental_map_key().data(), this->experimental_map_key().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "experimental_map_key");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(80)) goto parse_weak;
+ break;
+ }
+
+ // optional bool weak = 10 [default = false];
+ case 10: {
+ if (tag == 80) {
+ parse_weak:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &weak_)));
+ set_has_weak();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.FieldOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.FieldOptions)
+ return false;
+#undef DO_
+}
+
+void FieldOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.FieldOptions)
+ // optional .google.protobuf.FieldOptions.CType ctype = 1 [default = STRING];
+ if (has_ctype()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->ctype(), output);
+ }
+
+ // optional bool packed = 2;
+ if (has_packed()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->packed(), output);
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(3, this->deprecated(), output);
+ }
+
+ // optional bool lazy = 5 [default = false];
+ if (has_lazy()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(5, this->lazy(), output);
+ }
+
+ // optional string experimental_map_key = 9;
+ if (has_experimental_map_key()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->experimental_map_key().data(), this->experimental_map_key().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "experimental_map_key");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 9, this->experimental_map_key(), output);
+ }
+
+ // optional bool weak = 10 [default = false];
+ if (has_weak()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(10, this->weak(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.FieldOptions)
+}
+
+::google::protobuf::uint8* FieldOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.FieldOptions)
+ // optional .google.protobuf.FieldOptions.CType ctype = 1 [default = STRING];
+ if (has_ctype()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteEnumToArray(
+ 1, this->ctype(), target);
+ }
+
+ // optional bool packed = 2;
+ if (has_packed()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(2, this->packed(), target);
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(3, this->deprecated(), target);
+ }
+
+ // optional bool lazy = 5 [default = false];
+ if (has_lazy()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(5, this->lazy(), target);
+ }
+
+ // optional string experimental_map_key = 9;
+ if (has_experimental_map_key()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->experimental_map_key().data(), this->experimental_map_key().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "experimental_map_key");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 9, this->experimental_map_key(), target);
+ }
+
+ // optional bool weak = 10 [default = false];
+ if (has_weak()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(10, this->weak(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.FieldOptions)
+ return target;
+}
+
+int FieldOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .google.protobuf.FieldOptions.CType ctype = 1 [default = STRING];
+ if (has_ctype()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->ctype());
+ }
+
+ // optional bool packed = 2;
+ if (has_packed()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool lazy = 5 [default = false];
+ if (has_lazy()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ total_size += 1 + 1;
+ }
+
+ // optional string experimental_map_key = 9;
+ if (has_experimental_map_key()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->experimental_map_key());
+ }
+
+ // optional bool weak = 10 [default = false];
+ if (has_weak()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FieldOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const FieldOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const FieldOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void FieldOptions::MergeFrom(const FieldOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_ctype()) {
+ set_ctype(from.ctype());
+ }
+ if (from.has_packed()) {
+ set_packed(from.packed());
+ }
+ if (from.has_lazy()) {
+ set_lazy(from.lazy());
+ }
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ if (from.has_experimental_map_key()) {
+ set_experimental_map_key(from.experimental_map_key());
+ }
+ if (from.has_weak()) {
+ set_weak(from.weak());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void FieldOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void FieldOptions::CopyFrom(const FieldOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FieldOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void FieldOptions::Swap(FieldOptions* other) {
+ if (other != this) {
+ std::swap(ctype_, other->ctype_);
+ std::swap(packed_, other->packed_);
+ std::swap(lazy_, other->lazy_);
+ std::swap(deprecated_, other->deprecated_);
+ std::swap(experimental_map_key_, other->experimental_map_key_);
+ std::swap(weak_, other->weak_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata FieldOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = FieldOptions_descriptor_;
+ metadata.reflection = FieldOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int EnumOptions::kAllowAliasFieldNumber;
+const int EnumOptions::kDeprecatedFieldNumber;
+const int EnumOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+EnumOptions::EnumOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.EnumOptions)
+}
+
+void EnumOptions::InitAsDefaultInstance() {
+}
+
+EnumOptions::EnumOptions(const EnumOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.EnumOptions)
+}
+
+void EnumOptions::SharedCtor() {
+ _cached_size_ = 0;
+ allow_alias_ = false;
+ deprecated_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+EnumOptions::~EnumOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.EnumOptions)
+ SharedDtor();
+}
+
+void EnumOptions::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void EnumOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* EnumOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return EnumOptions_descriptor_;
+}
+
+const EnumOptions& EnumOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+EnumOptions* EnumOptions::default_instance_ = NULL;
+
+EnumOptions* EnumOptions::New() const {
+ return new EnumOptions;
+}
+
+void EnumOptions::Clear() {
+ _extensions_.Clear();
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<EnumOptions*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ ZR_(allow_alias_, deprecated_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool EnumOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.EnumOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bool allow_alias = 2;
+ case 2: {
+ if (tag == 16) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &allow_alias_)));
+ set_has_allow_alias();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_deprecated;
+ break;
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ case 3: {
+ if (tag == 24) {
+ parse_deprecated:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.EnumOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.EnumOptions)
+ return false;
+#undef DO_
+}
+
+void EnumOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.EnumOptions)
+ // optional bool allow_alias = 2;
+ if (has_allow_alias()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->allow_alias(), output);
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(3, this->deprecated(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.EnumOptions)
+}
+
+::google::protobuf::uint8* EnumOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.EnumOptions)
+ // optional bool allow_alias = 2;
+ if (has_allow_alias()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(2, this->allow_alias(), target);
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(3, this->deprecated(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.EnumOptions)
+ return target;
+}
+
+int EnumOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bool allow_alias = 2;
+ if (has_allow_alias()) {
+ total_size += 1 + 1;
+ }
+
+ // optional bool deprecated = 3 [default = false];
+ if (has_deprecated()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void EnumOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const EnumOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const EnumOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void EnumOptions::MergeFrom(const EnumOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_allow_alias()) {
+ set_allow_alias(from.allow_alias());
+ }
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void EnumOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void EnumOptions::CopyFrom(const EnumOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool EnumOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void EnumOptions::Swap(EnumOptions* other) {
+ if (other != this) {
+ std::swap(allow_alias_, other->allow_alias_);
+ std::swap(deprecated_, other->deprecated_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata EnumOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = EnumOptions_descriptor_;
+ metadata.reflection = EnumOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int EnumValueOptions::kDeprecatedFieldNumber;
+const int EnumValueOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+EnumValueOptions::EnumValueOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.EnumValueOptions)
+}
+
+void EnumValueOptions::InitAsDefaultInstance() {
+}
+
+EnumValueOptions::EnumValueOptions(const EnumValueOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.EnumValueOptions)
+}
+
+void EnumValueOptions::SharedCtor() {
+ _cached_size_ = 0;
+ deprecated_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+EnumValueOptions::~EnumValueOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.EnumValueOptions)
+ SharedDtor();
+}
+
+void EnumValueOptions::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void EnumValueOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* EnumValueOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return EnumValueOptions_descriptor_;
+}
+
+const EnumValueOptions& EnumValueOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+EnumValueOptions* EnumValueOptions::default_instance_ = NULL;
+
+EnumValueOptions* EnumValueOptions::New() const {
+ return new EnumValueOptions;
+}
+
+void EnumValueOptions::Clear() {
+ _extensions_.Clear();
+ deprecated_ = false;
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool EnumValueOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.EnumValueOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bool deprecated = 1 [default = false];
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.EnumValueOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.EnumValueOptions)
+ return false;
+#undef DO_
+}
+
+void EnumValueOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.EnumValueOptions)
+ // optional bool deprecated = 1 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(1, this->deprecated(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.EnumValueOptions)
+}
+
+::google::protobuf::uint8* EnumValueOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.EnumValueOptions)
+ // optional bool deprecated = 1 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(1, this->deprecated(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.EnumValueOptions)
+ return target;
+}
+
+int EnumValueOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bool deprecated = 1 [default = false];
+ if (has_deprecated()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void EnumValueOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const EnumValueOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const EnumValueOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void EnumValueOptions::MergeFrom(const EnumValueOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void EnumValueOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void EnumValueOptions::CopyFrom(const EnumValueOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool EnumValueOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void EnumValueOptions::Swap(EnumValueOptions* other) {
+ if (other != this) {
+ std::swap(deprecated_, other->deprecated_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata EnumValueOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = EnumValueOptions_descriptor_;
+ metadata.reflection = EnumValueOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ServiceOptions::kDeprecatedFieldNumber;
+const int ServiceOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+ServiceOptions::ServiceOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.ServiceOptions)
+}
+
+void ServiceOptions::InitAsDefaultInstance() {
+}
+
+ServiceOptions::ServiceOptions(const ServiceOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.ServiceOptions)
+}
+
+void ServiceOptions::SharedCtor() {
+ _cached_size_ = 0;
+ deprecated_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ServiceOptions::~ServiceOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.ServiceOptions)
+ SharedDtor();
+}
+
+void ServiceOptions::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void ServiceOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* ServiceOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return ServiceOptions_descriptor_;
+}
+
+const ServiceOptions& ServiceOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+ServiceOptions* ServiceOptions::default_instance_ = NULL;
+
+ServiceOptions* ServiceOptions::New() const {
+ return new ServiceOptions;
+}
+
+void ServiceOptions::Clear() {
+ _extensions_.Clear();
+ deprecated_ = false;
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool ServiceOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.ServiceOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bool deprecated = 33 [default = false];
+ case 33: {
+ if (tag == 264) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.ServiceOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.ServiceOptions)
+ return false;
+#undef DO_
+}
+
+void ServiceOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.ServiceOptions)
+ // optional bool deprecated = 33 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(33, this->deprecated(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.ServiceOptions)
+}
+
+::google::protobuf::uint8* ServiceOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.ServiceOptions)
+ // optional bool deprecated = 33 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(33, this->deprecated(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.ServiceOptions)
+ return target;
+}
+
+int ServiceOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bool deprecated = 33 [default = false];
+ if (has_deprecated()) {
+ total_size += 2 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ServiceOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const ServiceOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const ServiceOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void ServiceOptions::MergeFrom(const ServiceOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void ServiceOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void ServiceOptions::CopyFrom(const ServiceOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ServiceOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void ServiceOptions::Swap(ServiceOptions* other) {
+ if (other != this) {
+ std::swap(deprecated_, other->deprecated_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata ServiceOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = ServiceOptions_descriptor_;
+ metadata.reflection = ServiceOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int MethodOptions::kDeprecatedFieldNumber;
+const int MethodOptions::kUninterpretedOptionFieldNumber;
+#endif // !_MSC_VER
+
+MethodOptions::MethodOptions()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.MethodOptions)
+}
+
+void MethodOptions::InitAsDefaultInstance() {
+}
+
+MethodOptions::MethodOptions(const MethodOptions& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.MethodOptions)
+}
+
+void MethodOptions::SharedCtor() {
+ _cached_size_ = 0;
+ deprecated_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+MethodOptions::~MethodOptions() {
+ // @@protoc_insertion_point(destructor:google.protobuf.MethodOptions)
+ SharedDtor();
+}
+
+void MethodOptions::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void MethodOptions::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* MethodOptions::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return MethodOptions_descriptor_;
+}
+
+const MethodOptions& MethodOptions::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+MethodOptions* MethodOptions::default_instance_ = NULL;
+
+MethodOptions* MethodOptions::New() const {
+ return new MethodOptions;
+}
+
+void MethodOptions::Clear() {
+ _extensions_.Clear();
+ deprecated_ = false;
+ uninterpreted_option_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool MethodOptions::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.MethodOptions)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(16383);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bool deprecated = 33 [default = false];
+ case 33: {
+ if (tag == 264) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &deprecated_)));
+ set_has_deprecated();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ break;
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ case 999: {
+ if (tag == 7994) {
+ parse_uninterpreted_option:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_uninterpreted_option()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(7994)) goto parse_uninterpreted_option;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ if ((8000u <= tag)) {
+ DO_(_extensions_.ParseField(tag, input, default_instance_,
+ mutable_unknown_fields()));
+ continue;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.MethodOptions)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.MethodOptions)
+ return false;
+#undef DO_
+}
+
+void MethodOptions::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.MethodOptions)
+ // optional bool deprecated = 33 [default = false];
+ if (has_deprecated()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(33, this->deprecated(), output);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 999, this->uninterpreted_option(i), output);
+ }
+
+ // Extension range [1000, 536870912)
+ _extensions_.SerializeWithCachedSizes(
+ 1000, 536870912, output);
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.MethodOptions)
+}
+
+::google::protobuf::uint8* MethodOptions::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.MethodOptions)
+ // optional bool deprecated = 33 [default = false];
+ if (has_deprecated()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(33, this->deprecated(), target);
+ }
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 999, this->uninterpreted_option(i), target);
+ }
+
+ // Extension range [1000, 536870912)
+ target = _extensions_.SerializeWithCachedSizesToArray(
+ 1000, 536870912, target);
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.MethodOptions)
+ return target;
+}
+
+int MethodOptions::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bool deprecated = 33 [default = false];
+ if (has_deprecated()) {
+ total_size += 2 + 1;
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ total_size += 2 * this->uninterpreted_option_size();
+ for (int i = 0; i < this->uninterpreted_option_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->uninterpreted_option(i));
+ }
+
+ total_size += _extensions_.ByteSize();
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void MethodOptions::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const MethodOptions* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const MethodOptions*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void MethodOptions::MergeFrom(const MethodOptions& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ uninterpreted_option_.MergeFrom(from.uninterpreted_option_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_deprecated()) {
+ set_deprecated(from.deprecated());
+ }
+ }
+ _extensions_.MergeFrom(from._extensions_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void MethodOptions::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void MethodOptions::CopyFrom(const MethodOptions& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool MethodOptions::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->uninterpreted_option())) return false;
+
+ if (!_extensions_.IsInitialized()) return false; return true;
+}
+
+void MethodOptions::Swap(MethodOptions* other) {
+ if (other != this) {
+ std::swap(deprecated_, other->deprecated_);
+ uninterpreted_option_.Swap(&other->uninterpreted_option_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ _extensions_.Swap(&other->_extensions_);
+ }
+}
+
+::google::protobuf::Metadata MethodOptions::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = MethodOptions_descriptor_;
+ metadata.reflection = MethodOptions_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int UninterpretedOption_NamePart::kNamePartFieldNumber;
+const int UninterpretedOption_NamePart::kIsExtensionFieldNumber;
+#endif // !_MSC_VER
+
+UninterpretedOption_NamePart::UninterpretedOption_NamePart()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.UninterpretedOption.NamePart)
+}
+
+void UninterpretedOption_NamePart::InitAsDefaultInstance() {
+}
+
+UninterpretedOption_NamePart::UninterpretedOption_NamePart(const UninterpretedOption_NamePart& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.UninterpretedOption.NamePart)
+}
+
+void UninterpretedOption_NamePart::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ name_part_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ is_extension_ = false;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+UninterpretedOption_NamePart::~UninterpretedOption_NamePart() {
+ // @@protoc_insertion_point(destructor:google.protobuf.UninterpretedOption.NamePart)
+ SharedDtor();
+}
+
+void UninterpretedOption_NamePart::SharedDtor() {
+ if (name_part_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_part_;
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void UninterpretedOption_NamePart::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* UninterpretedOption_NamePart::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return UninterpretedOption_NamePart_descriptor_;
+}
+
+const UninterpretedOption_NamePart& UninterpretedOption_NamePart::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+UninterpretedOption_NamePart* UninterpretedOption_NamePart::default_instance_ = NULL;
+
+UninterpretedOption_NamePart* UninterpretedOption_NamePart::New() const {
+ return new UninterpretedOption_NamePart;
+}
+
+void UninterpretedOption_NamePart::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_name_part()) {
+ if (name_part_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_part_->clear();
+ }
+ }
+ is_extension_ = false;
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool UninterpretedOption_NamePart::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.UninterpretedOption.NamePart)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // required string name_part = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_name_part()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name_part().data(), this->name_part().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "name_part");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_is_extension;
+ break;
+ }
+
+ // required bool is_extension = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_is_extension:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+ input, &is_extension_)));
+ set_has_is_extension();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.UninterpretedOption.NamePart)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.UninterpretedOption.NamePart)
+ return false;
+#undef DO_
+}
+
+void UninterpretedOption_NamePart::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.UninterpretedOption.NamePart)
+ // required string name_part = 1;
+ if (has_name_part()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name_part().data(), this->name_part().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name_part");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->name_part(), output);
+ }
+
+ // required bool is_extension = 2;
+ if (has_is_extension()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->is_extension(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.UninterpretedOption.NamePart)
+}
+
+::google::protobuf::uint8* UninterpretedOption_NamePart::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.UninterpretedOption.NamePart)
+ // required string name_part = 1;
+ if (has_name_part()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->name_part().data(), this->name_part().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "name_part");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 1, this->name_part(), target);
+ }
+
+ // required bool is_extension = 2;
+ if (has_is_extension()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteBoolToArray(2, this->is_extension(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.UninterpretedOption.NamePart)
+ return target;
+}
+
+int UninterpretedOption_NamePart::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // required string name_part = 1;
+ if (has_name_part()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->name_part());
+ }
+
+ // required bool is_extension = 2;
+ if (has_is_extension()) {
+ total_size += 1 + 1;
+ }
+
+ }
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void UninterpretedOption_NamePart::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const UninterpretedOption_NamePart* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const UninterpretedOption_NamePart*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void UninterpretedOption_NamePart::MergeFrom(const UninterpretedOption_NamePart& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_name_part()) {
+ set_name_part(from.name_part());
+ }
+ if (from.has_is_extension()) {
+ set_is_extension(from.is_extension());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void UninterpretedOption_NamePart::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void UninterpretedOption_NamePart::CopyFrom(const UninterpretedOption_NamePart& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool UninterpretedOption_NamePart::IsInitialized() const {
+ if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+
+ return true;
+}
+
+void UninterpretedOption_NamePart::Swap(UninterpretedOption_NamePart* other) {
+ if (other != this) {
+ std::swap(name_part_, other->name_part_);
+ std::swap(is_extension_, other->is_extension_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata UninterpretedOption_NamePart::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = UninterpretedOption_NamePart_descriptor_;
+ metadata.reflection = UninterpretedOption_NamePart_reflection_;
+ return metadata;
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int UninterpretedOption::kNameFieldNumber;
+const int UninterpretedOption::kIdentifierValueFieldNumber;
+const int UninterpretedOption::kPositiveIntValueFieldNumber;
+const int UninterpretedOption::kNegativeIntValueFieldNumber;
+const int UninterpretedOption::kDoubleValueFieldNumber;
+const int UninterpretedOption::kStringValueFieldNumber;
+const int UninterpretedOption::kAggregateValueFieldNumber;
+#endif // !_MSC_VER
+
+UninterpretedOption::UninterpretedOption()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.UninterpretedOption)
+}
+
+void UninterpretedOption::InitAsDefaultInstance() {
+}
+
+UninterpretedOption::UninterpretedOption(const UninterpretedOption& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.UninterpretedOption)
+}
+
+void UninterpretedOption::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ identifier_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ positive_int_value_ = GOOGLE_ULONGLONG(0);
+ negative_int_value_ = GOOGLE_LONGLONG(0);
+ double_value_ = 0;
+ string_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ aggregate_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+UninterpretedOption::~UninterpretedOption() {
+ // @@protoc_insertion_point(destructor:google.protobuf.UninterpretedOption)
+ SharedDtor();
+}
+
+void UninterpretedOption::SharedDtor() {
+ if (identifier_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete identifier_value_;
+ }
+ if (string_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete string_value_;
+ }
+ if (aggregate_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete aggregate_value_;
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void UninterpretedOption::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* UninterpretedOption::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return UninterpretedOption_descriptor_;
+}
+
+const UninterpretedOption& UninterpretedOption::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+UninterpretedOption* UninterpretedOption::default_instance_ = NULL;
+
+UninterpretedOption* UninterpretedOption::New() const {
+ return new UninterpretedOption;
+}
+
+void UninterpretedOption::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<UninterpretedOption*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 126) {
+ ZR_(positive_int_value_, double_value_);
+ if (has_identifier_value()) {
+ if (identifier_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ identifier_value_->clear();
+ }
+ }
+ if (has_string_value()) {
+ if (string_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ string_value_->clear();
+ }
+ }
+ if (has_aggregate_value()) {
+ if (aggregate_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ aggregate_value_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ name_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool UninterpretedOption::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.UninterpretedOption)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .google.protobuf.UninterpretedOption.NamePart name = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_name:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_name()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_name;
+ if (input->ExpectTag(26)) goto parse_identifier_value;
+ break;
+ }
+
+ // optional string identifier_value = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_identifier_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_identifier_value()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->identifier_value().data(), this->identifier_value().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "identifier_value");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_positive_int_value;
+ break;
+ }
+
+ // optional uint64 positive_int_value = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_positive_int_value:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+ input, &positive_int_value_)));
+ set_has_positive_int_value();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_negative_int_value;
+ break;
+ }
+
+ // optional int64 negative_int_value = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_negative_int_value:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &negative_int_value_)));
+ set_has_negative_int_value();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(49)) goto parse_double_value;
+ break;
+ }
+
+ // optional double double_value = 6;
+ case 6: {
+ if (tag == 49) {
+ parse_double_value:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ double, ::google::protobuf::internal::WireFormatLite::TYPE_DOUBLE>(
+ input, &double_value_)));
+ set_has_double_value();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(58)) goto parse_string_value;
+ break;
+ }
+
+ // optional bytes string_value = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_string_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_string_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_aggregate_value;
+ break;
+ }
+
+ // optional string aggregate_value = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_aggregate_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_aggregate_value()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->aggregate_value().data(), this->aggregate_value().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "aggregate_value");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.UninterpretedOption)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.UninterpretedOption)
+ return false;
+#undef DO_
+}
+
+void UninterpretedOption::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.UninterpretedOption)
+ // repeated .google.protobuf.UninterpretedOption.NamePart name = 2;
+ for (int i = 0; i < this->name_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 2, this->name(i), output);
+ }
+
+ // optional string identifier_value = 3;
+ if (has_identifier_value()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->identifier_value().data(), this->identifier_value().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "identifier_value");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->identifier_value(), output);
+ }
+
+ // optional uint64 positive_int_value = 4;
+ if (has_positive_int_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteUInt64(4, this->positive_int_value(), output);
+ }
+
+ // optional int64 negative_int_value = 5;
+ if (has_negative_int_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(5, this->negative_int_value(), output);
+ }
+
+ // optional double double_value = 6;
+ if (has_double_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteDouble(6, this->double_value(), output);
+ }
+
+ // optional bytes string_value = 7;
+ if (has_string_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 7, this->string_value(), output);
+ }
+
+ // optional string aggregate_value = 8;
+ if (has_aggregate_value()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->aggregate_value().data(), this->aggregate_value().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "aggregate_value");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 8, this->aggregate_value(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.UninterpretedOption)
+}
+
+::google::protobuf::uint8* UninterpretedOption::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.UninterpretedOption)
+ // repeated .google.protobuf.UninterpretedOption.NamePart name = 2;
+ for (int i = 0; i < this->name_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 2, this->name(i), target);
+ }
+
+ // optional string identifier_value = 3;
+ if (has_identifier_value()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->identifier_value().data(), this->identifier_value().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "identifier_value");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 3, this->identifier_value(), target);
+ }
+
+ // optional uint64 positive_int_value = 4;
+ if (has_positive_int_value()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteUInt64ToArray(4, this->positive_int_value(), target);
+ }
+
+ // optional int64 negative_int_value = 5;
+ if (has_negative_int_value()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteInt64ToArray(5, this->negative_int_value(), target);
+ }
+
+ // optional double double_value = 6;
+ if (has_double_value()) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteDoubleToArray(6, this->double_value(), target);
+ }
+
+ // optional bytes string_value = 7;
+ if (has_string_value()) {
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteBytesToArray(
+ 7, this->string_value(), target);
+ }
+
+ // optional string aggregate_value = 8;
+ if (has_aggregate_value()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->aggregate_value().data(), this->aggregate_value().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "aggregate_value");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 8, this->aggregate_value(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.UninterpretedOption)
+ return target;
+}
+
+int UninterpretedOption::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ // optional string identifier_value = 3;
+ if (has_identifier_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->identifier_value());
+ }
+
+ // optional uint64 positive_int_value = 4;
+ if (has_positive_int_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::UInt64Size(
+ this->positive_int_value());
+ }
+
+ // optional int64 negative_int_value = 5;
+ if (has_negative_int_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->negative_int_value());
+ }
+
+ // optional double double_value = 6;
+ if (has_double_value()) {
+ total_size += 1 + 8;
+ }
+
+ // optional bytes string_value = 7;
+ if (has_string_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->string_value());
+ }
+
+ // optional string aggregate_value = 8;
+ if (has_aggregate_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->aggregate_value());
+ }
+
+ }
+ // repeated .google.protobuf.UninterpretedOption.NamePart name = 2;
+ total_size += 1 * this->name_size();
+ for (int i = 0; i < this->name_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->name(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void UninterpretedOption::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const UninterpretedOption* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const UninterpretedOption*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void UninterpretedOption::MergeFrom(const UninterpretedOption& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ name_.MergeFrom(from.name_);
+ if (from._has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ if (from.has_identifier_value()) {
+ set_identifier_value(from.identifier_value());
+ }
+ if (from.has_positive_int_value()) {
+ set_positive_int_value(from.positive_int_value());
+ }
+ if (from.has_negative_int_value()) {
+ set_negative_int_value(from.negative_int_value());
+ }
+ if (from.has_double_value()) {
+ set_double_value(from.double_value());
+ }
+ if (from.has_string_value()) {
+ set_string_value(from.string_value());
+ }
+ if (from.has_aggregate_value()) {
+ set_aggregate_value(from.aggregate_value());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void UninterpretedOption::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void UninterpretedOption::CopyFrom(const UninterpretedOption& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool UninterpretedOption::IsInitialized() const {
+
+ if (!::google::protobuf::internal::AllAreInitialized(this->name())) return false;
+ return true;
+}
+
+void UninterpretedOption::Swap(UninterpretedOption* other) {
+ if (other != this) {
+ name_.Swap(&other->name_);
+ std::swap(identifier_value_, other->identifier_value_);
+ std::swap(positive_int_value_, other->positive_int_value_);
+ std::swap(negative_int_value_, other->negative_int_value_);
+ std::swap(double_value_, other->double_value_);
+ std::swap(string_value_, other->string_value_);
+ std::swap(aggregate_value_, other->aggregate_value_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata UninterpretedOption::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = UninterpretedOption_descriptor_;
+ metadata.reflection = UninterpretedOption_reflection_;
+ return metadata;
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int SourceCodeInfo_Location::kPathFieldNumber;
+const int SourceCodeInfo_Location::kSpanFieldNumber;
+const int SourceCodeInfo_Location::kLeadingCommentsFieldNumber;
+const int SourceCodeInfo_Location::kTrailingCommentsFieldNumber;
+#endif // !_MSC_VER
+
+SourceCodeInfo_Location::SourceCodeInfo_Location()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.SourceCodeInfo.Location)
+}
+
+void SourceCodeInfo_Location::InitAsDefaultInstance() {
+}
+
+SourceCodeInfo_Location::SourceCodeInfo_Location(const SourceCodeInfo_Location& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.SourceCodeInfo.Location)
+}
+
+void SourceCodeInfo_Location::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ leading_comments_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ trailing_comments_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+SourceCodeInfo_Location::~SourceCodeInfo_Location() {
+ // @@protoc_insertion_point(destructor:google.protobuf.SourceCodeInfo.Location)
+ SharedDtor();
+}
+
+void SourceCodeInfo_Location::SharedDtor() {
+ if (leading_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete leading_comments_;
+ }
+ if (trailing_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete trailing_comments_;
+ }
+ if (this != default_instance_) {
+ }
+}
+
+void SourceCodeInfo_Location::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* SourceCodeInfo_Location::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return SourceCodeInfo_Location_descriptor_;
+}
+
+const SourceCodeInfo_Location& SourceCodeInfo_Location::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+SourceCodeInfo_Location* SourceCodeInfo_Location::default_instance_ = NULL;
+
+SourceCodeInfo_Location* SourceCodeInfo_Location::New() const {
+ return new SourceCodeInfo_Location;
+}
+
+void SourceCodeInfo_Location::Clear() {
+ if (_has_bits_[0 / 32] & 12) {
+ if (has_leading_comments()) {
+ if (leading_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ leading_comments_->clear();
+ }
+ }
+ if (has_trailing_comments()) {
+ if (trailing_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ trailing_comments_->clear();
+ }
+ }
+ }
+ path_.Clear();
+ span_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool SourceCodeInfo_Location::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.SourceCodeInfo.Location)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated int32 path = 1 [packed = true];
+ case 1: {
+ if (tag == 10) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, this->mutable_path())));
+ } else if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitiveNoInline<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ 1, 10, input, this->mutable_path())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_span;
+ break;
+ }
+
+ // repeated int32 span = 2 [packed = true];
+ case 2: {
+ if (tag == 18) {
+ parse_span:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, this->mutable_span())));
+ } else if (tag == 16) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitiveNoInline<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ 1, 18, input, this->mutable_span())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_leading_comments;
+ break;
+ }
+
+ // optional string leading_comments = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_leading_comments:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_leading_comments()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->leading_comments().data(), this->leading_comments().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "leading_comments");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_trailing_comments;
+ break;
+ }
+
+ // optional string trailing_comments = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_trailing_comments:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_trailing_comments()));
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->trailing_comments().data(), this->trailing_comments().length(),
+ ::google::protobuf::internal::WireFormat::PARSE,
+ "trailing_comments");
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.SourceCodeInfo.Location)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.SourceCodeInfo.Location)
+ return false;
+#undef DO_
+}
+
+void SourceCodeInfo_Location::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.SourceCodeInfo.Location)
+ // repeated int32 path = 1 [packed = true];
+ if (this->path_size() > 0) {
+ ::google::protobuf::internal::WireFormatLite::WriteTag(1, ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED, output);
+ output->WriteVarint32(_path_cached_byte_size_);
+ }
+ for (int i = 0; i < this->path_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32NoTag(
+ this->path(i), output);
+ }
+
+ // repeated int32 span = 2 [packed = true];
+ if (this->span_size() > 0) {
+ ::google::protobuf::internal::WireFormatLite::WriteTag(2, ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED, output);
+ output->WriteVarint32(_span_cached_byte_size_);
+ }
+ for (int i = 0; i < this->span_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32NoTag(
+ this->span(i), output);
+ }
+
+ // optional string leading_comments = 3;
+ if (has_leading_comments()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->leading_comments().data(), this->leading_comments().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "leading_comments");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->leading_comments(), output);
+ }
+
+ // optional string trailing_comments = 4;
+ if (has_trailing_comments()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->trailing_comments().data(), this->trailing_comments().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "trailing_comments");
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->trailing_comments(), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.SourceCodeInfo.Location)
+}
+
+::google::protobuf::uint8* SourceCodeInfo_Location::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.SourceCodeInfo.Location)
+ // repeated int32 path = 1 [packed = true];
+ if (this->path_size() > 0) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteTagToArray(
+ 1,
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED,
+ target);
+ target = ::google::protobuf::io::CodedOutputStream::WriteVarint32ToArray(
+ _path_cached_byte_size_, target);
+ }
+ for (int i = 0; i < this->path_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteInt32NoTagToArray(this->path(i), target);
+ }
+
+ // repeated int32 span = 2 [packed = true];
+ if (this->span_size() > 0) {
+ target = ::google::protobuf::internal::WireFormatLite::WriteTagToArray(
+ 2,
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED,
+ target);
+ target = ::google::protobuf::io::CodedOutputStream::WriteVarint32ToArray(
+ _span_cached_byte_size_, target);
+ }
+ for (int i = 0; i < this->span_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteInt32NoTagToArray(this->span(i), target);
+ }
+
+ // optional string leading_comments = 3;
+ if (has_leading_comments()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->leading_comments().data(), this->leading_comments().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "leading_comments");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 3, this->leading_comments(), target);
+ }
+
+ // optional string trailing_comments = 4;
+ if (has_trailing_comments()) {
+ ::google::protobuf::internal::WireFormat::VerifyUTF8StringNamedField(
+ this->trailing_comments().data(), this->trailing_comments().length(),
+ ::google::protobuf::internal::WireFormat::SERIALIZE,
+ "trailing_comments");
+ target =
+ ::google::protobuf::internal::WireFormatLite::WriteStringToArray(
+ 4, this->trailing_comments(), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.SourceCodeInfo.Location)
+ return target;
+}
+
+int SourceCodeInfo_Location::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[2 / 32] & (0xffu << (2 % 32))) {
+ // optional string leading_comments = 3;
+ if (has_leading_comments()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->leading_comments());
+ }
+
+ // optional string trailing_comments = 4;
+ if (has_trailing_comments()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->trailing_comments());
+ }
+
+ }
+ // repeated int32 path = 1 [packed = true];
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->path_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ Int32Size(this->path(i));
+ }
+ if (data_size > 0) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(data_size);
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _path_cached_byte_size_ = data_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ total_size += data_size;
+ }
+
+ // repeated int32 span = 2 [packed = true];
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->span_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ Int32Size(this->span(i));
+ }
+ if (data_size > 0) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(data_size);
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _span_cached_byte_size_ = data_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ total_size += data_size;
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void SourceCodeInfo_Location::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const SourceCodeInfo_Location* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const SourceCodeInfo_Location*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void SourceCodeInfo_Location::MergeFrom(const SourceCodeInfo_Location& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ path_.MergeFrom(from.path_);
+ span_.MergeFrom(from.span_);
+ if (from._has_bits_[2 / 32] & (0xffu << (2 % 32))) {
+ if (from.has_leading_comments()) {
+ set_leading_comments(from.leading_comments());
+ }
+ if (from.has_trailing_comments()) {
+ set_trailing_comments(from.trailing_comments());
+ }
+ }
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void SourceCodeInfo_Location::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void SourceCodeInfo_Location::CopyFrom(const SourceCodeInfo_Location& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool SourceCodeInfo_Location::IsInitialized() const {
+
+ return true;
+}
+
+void SourceCodeInfo_Location::Swap(SourceCodeInfo_Location* other) {
+ if (other != this) {
+ path_.Swap(&other->path_);
+ span_.Swap(&other->span_);
+ std::swap(leading_comments_, other->leading_comments_);
+ std::swap(trailing_comments_, other->trailing_comments_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata SourceCodeInfo_Location::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = SourceCodeInfo_Location_descriptor_;
+ metadata.reflection = SourceCodeInfo_Location_reflection_;
+ return metadata;
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int SourceCodeInfo::kLocationFieldNumber;
+#endif // !_MSC_VER
+
+SourceCodeInfo::SourceCodeInfo()
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:google.protobuf.SourceCodeInfo)
+}
+
+void SourceCodeInfo::InitAsDefaultInstance() {
+}
+
+SourceCodeInfo::SourceCodeInfo(const SourceCodeInfo& from)
+ : ::google::protobuf::Message() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:google.protobuf.SourceCodeInfo)
+}
+
+void SourceCodeInfo::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+SourceCodeInfo::~SourceCodeInfo() {
+ // @@protoc_insertion_point(destructor:google.protobuf.SourceCodeInfo)
+ SharedDtor();
+}
+
+void SourceCodeInfo::SharedDtor() {
+ if (this != default_instance_) {
+ }
+}
+
+void SourceCodeInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ::google::protobuf::Descriptor* SourceCodeInfo::descriptor() {
+ protobuf_AssignDescriptorsOnce();
+ return SourceCodeInfo_descriptor_;
+}
+
+const SourceCodeInfo& SourceCodeInfo::default_instance() {
+ if (default_instance_ == NULL) protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ return *default_instance_;
+}
+
+SourceCodeInfo* SourceCodeInfo::default_instance_ = NULL;
+
+SourceCodeInfo* SourceCodeInfo::New() const {
+ return new SourceCodeInfo;
+}
+
+void SourceCodeInfo::Clear() {
+ location_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->Clear();
+}
+
+bool SourceCodeInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ // @@protoc_insertion_point(parse_start:google.protobuf.SourceCodeInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .google.protobuf.SourceCodeInfo.Location location = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_location:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_location()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_location;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormat::SkipField(
+ input, tag, mutable_unknown_fields()));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:google.protobuf.SourceCodeInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:google.protobuf.SourceCodeInfo)
+ return false;
+#undef DO_
+}
+
+void SourceCodeInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:google.protobuf.SourceCodeInfo)
+ // repeated .google.protobuf.SourceCodeInfo.Location location = 1;
+ for (int i = 0; i < this->location_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessageMaybeToArray(
+ 1, this->location(i), output);
+ }
+
+ if (!unknown_fields().empty()) {
+ ::google::protobuf::internal::WireFormat::SerializeUnknownFields(
+ unknown_fields(), output);
+ }
+ // @@protoc_insertion_point(serialize_end:google.protobuf.SourceCodeInfo)
+}
+
+::google::protobuf::uint8* SourceCodeInfo::SerializeWithCachedSizesToArray(
+ ::google::protobuf::uint8* target) const {
+ // @@protoc_insertion_point(serialize_to_array_start:google.protobuf.SourceCodeInfo)
+ // repeated .google.protobuf.SourceCodeInfo.Location location = 1;
+ for (int i = 0; i < this->location_size(); i++) {
+ target = ::google::protobuf::internal::WireFormatLite::
+ WriteMessageNoVirtualToArray(
+ 1, this->location(i), target);
+ }
+
+ if (!unknown_fields().empty()) {
+ target = ::google::protobuf::internal::WireFormat::SerializeUnknownFieldsToArray(
+ unknown_fields(), target);
+ }
+ // @@protoc_insertion_point(serialize_to_array_end:google.protobuf.SourceCodeInfo)
+ return target;
+}
+
+int SourceCodeInfo::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .google.protobuf.SourceCodeInfo.Location location = 1;
+ total_size += 1 * this->location_size();
+ for (int i = 0; i < this->location_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->location(i));
+ }
+
+ if (!unknown_fields().empty()) {
+ total_size +=
+ ::google::protobuf::internal::WireFormat::ComputeUnknownFieldsSize(
+ unknown_fields());
+ }
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void SourceCodeInfo::MergeFrom(const ::google::protobuf::Message& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ const SourceCodeInfo* source =
+ ::google::protobuf::internal::dynamic_cast_if_available<const SourceCodeInfo*>(
+ &from);
+ if (source == NULL) {
+ ::google::protobuf::internal::ReflectionOps::Merge(from, this);
+ } else {
+ MergeFrom(*source);
+ }
+}
+
+void SourceCodeInfo::MergeFrom(const SourceCodeInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ location_.MergeFrom(from.location_);
+ mutable_unknown_fields()->MergeFrom(from.unknown_fields());
+}
+
+void SourceCodeInfo::CopyFrom(const ::google::protobuf::Message& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+void SourceCodeInfo::CopyFrom(const SourceCodeInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool SourceCodeInfo::IsInitialized() const {
+
+ return true;
+}
+
+void SourceCodeInfo::Swap(SourceCodeInfo* other) {
+ if (other != this) {
+ location_.Swap(&other->location_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.Swap(&other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::google::protobuf::Metadata SourceCodeInfo::GetMetadata() const {
+ protobuf_AssignDescriptorsOnce();
+ ::google::protobuf::Metadata metadata;
+ metadata.descriptor = SourceCodeInfo_descriptor_;
+ metadata.reflection = SourceCodeInfo_reflection_;
+ return metadata;
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace protobuf
+} // namespace google
+
+// @@protoc_insertion_point(global_scope)
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor.pb.h b/toolkit/components/protobuf/src/google/protobuf/descriptor.pb.h
new file mode 100644
index 0000000000..45521812c3
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor.pb.h
@@ -0,0 +1,6761 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: google/protobuf/descriptor.proto
+
+#ifndef PROTOBUF_google_2fprotobuf_2fdescriptor_2eproto__INCLUDED
+#define PROTOBUF_google_2fprotobuf_2fdescriptor_2eproto__INCLUDED
+
+#include <string>
+
+#include <google/protobuf/stubs/common.h>
+
+#if GOOGLE_PROTOBUF_VERSION < 2006000
+#error This file was generated by a newer version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please update
+#error your headers.
+#endif
+#if 2006001 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION
+#error This file was generated by an older version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please
+#error regenerate this file with a newer version of protoc.
+#endif
+
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/generated_enum_reflection.h>
+#include <google/protobuf/unknown_field_set.h>
+// @@protoc_insertion_point(includes)
+
+namespace google {
+namespace protobuf {
+
+// Internal implementation detail -- do not call these.
+void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+class FileDescriptorSet;
+class FileDescriptorProto;
+class DescriptorProto;
+class DescriptorProto_ExtensionRange;
+class FieldDescriptorProto;
+class OneofDescriptorProto;
+class EnumDescriptorProto;
+class EnumValueDescriptorProto;
+class ServiceDescriptorProto;
+class MethodDescriptorProto;
+class FileOptions;
+class MessageOptions;
+class FieldOptions;
+class EnumOptions;
+class EnumValueOptions;
+class ServiceOptions;
+class MethodOptions;
+class UninterpretedOption;
+class UninterpretedOption_NamePart;
+class SourceCodeInfo;
+class SourceCodeInfo_Location;
+
+enum FieldDescriptorProto_Type {
+ FieldDescriptorProto_Type_TYPE_DOUBLE = 1,
+ FieldDescriptorProto_Type_TYPE_FLOAT = 2,
+ FieldDescriptorProto_Type_TYPE_INT64 = 3,
+ FieldDescriptorProto_Type_TYPE_UINT64 = 4,
+ FieldDescriptorProto_Type_TYPE_INT32 = 5,
+ FieldDescriptorProto_Type_TYPE_FIXED64 = 6,
+ FieldDescriptorProto_Type_TYPE_FIXED32 = 7,
+ FieldDescriptorProto_Type_TYPE_BOOL = 8,
+ FieldDescriptorProto_Type_TYPE_STRING = 9,
+ FieldDescriptorProto_Type_TYPE_GROUP = 10,
+ FieldDescriptorProto_Type_TYPE_MESSAGE = 11,
+ FieldDescriptorProto_Type_TYPE_BYTES = 12,
+ FieldDescriptorProto_Type_TYPE_UINT32 = 13,
+ FieldDescriptorProto_Type_TYPE_ENUM = 14,
+ FieldDescriptorProto_Type_TYPE_SFIXED32 = 15,
+ FieldDescriptorProto_Type_TYPE_SFIXED64 = 16,
+ FieldDescriptorProto_Type_TYPE_SINT32 = 17,
+ FieldDescriptorProto_Type_TYPE_SINT64 = 18
+};
+LIBPROTOBUF_EXPORT bool FieldDescriptorProto_Type_IsValid(int value);
+const FieldDescriptorProto_Type FieldDescriptorProto_Type_Type_MIN = FieldDescriptorProto_Type_TYPE_DOUBLE;
+const FieldDescriptorProto_Type FieldDescriptorProto_Type_Type_MAX = FieldDescriptorProto_Type_TYPE_SINT64;
+const int FieldDescriptorProto_Type_Type_ARRAYSIZE = FieldDescriptorProto_Type_Type_MAX + 1;
+
+LIBPROTOBUF_EXPORT const ::google::protobuf::EnumDescriptor* FieldDescriptorProto_Type_descriptor();
+inline const ::std::string& FieldDescriptorProto_Type_Name(FieldDescriptorProto_Type value) {
+ return ::google::protobuf::internal::NameOfEnum(
+ FieldDescriptorProto_Type_descriptor(), value);
+}
+inline bool FieldDescriptorProto_Type_Parse(
+ const ::std::string& name, FieldDescriptorProto_Type* value) {
+ return ::google::protobuf::internal::ParseNamedEnum<FieldDescriptorProto_Type>(
+ FieldDescriptorProto_Type_descriptor(), name, value);
+}
+enum FieldDescriptorProto_Label {
+ FieldDescriptorProto_Label_LABEL_OPTIONAL = 1,
+ FieldDescriptorProto_Label_LABEL_REQUIRED = 2,
+ FieldDescriptorProto_Label_LABEL_REPEATED = 3
+};
+LIBPROTOBUF_EXPORT bool FieldDescriptorProto_Label_IsValid(int value);
+const FieldDescriptorProto_Label FieldDescriptorProto_Label_Label_MIN = FieldDescriptorProto_Label_LABEL_OPTIONAL;
+const FieldDescriptorProto_Label FieldDescriptorProto_Label_Label_MAX = FieldDescriptorProto_Label_LABEL_REPEATED;
+const int FieldDescriptorProto_Label_Label_ARRAYSIZE = FieldDescriptorProto_Label_Label_MAX + 1;
+
+LIBPROTOBUF_EXPORT const ::google::protobuf::EnumDescriptor* FieldDescriptorProto_Label_descriptor();
+inline const ::std::string& FieldDescriptorProto_Label_Name(FieldDescriptorProto_Label value) {
+ return ::google::protobuf::internal::NameOfEnum(
+ FieldDescriptorProto_Label_descriptor(), value);
+}
+inline bool FieldDescriptorProto_Label_Parse(
+ const ::std::string& name, FieldDescriptorProto_Label* value) {
+ return ::google::protobuf::internal::ParseNamedEnum<FieldDescriptorProto_Label>(
+ FieldDescriptorProto_Label_descriptor(), name, value);
+}
+enum FileOptions_OptimizeMode {
+ FileOptions_OptimizeMode_SPEED = 1,
+ FileOptions_OptimizeMode_CODE_SIZE = 2,
+ FileOptions_OptimizeMode_LITE_RUNTIME = 3
+};
+LIBPROTOBUF_EXPORT bool FileOptions_OptimizeMode_IsValid(int value);
+const FileOptions_OptimizeMode FileOptions_OptimizeMode_OptimizeMode_MIN = FileOptions_OptimizeMode_SPEED;
+const FileOptions_OptimizeMode FileOptions_OptimizeMode_OptimizeMode_MAX = FileOptions_OptimizeMode_LITE_RUNTIME;
+const int FileOptions_OptimizeMode_OptimizeMode_ARRAYSIZE = FileOptions_OptimizeMode_OptimizeMode_MAX + 1;
+
+LIBPROTOBUF_EXPORT const ::google::protobuf::EnumDescriptor* FileOptions_OptimizeMode_descriptor();
+inline const ::std::string& FileOptions_OptimizeMode_Name(FileOptions_OptimizeMode value) {
+ return ::google::protobuf::internal::NameOfEnum(
+ FileOptions_OptimizeMode_descriptor(), value);
+}
+inline bool FileOptions_OptimizeMode_Parse(
+ const ::std::string& name, FileOptions_OptimizeMode* value) {
+ return ::google::protobuf::internal::ParseNamedEnum<FileOptions_OptimizeMode>(
+ FileOptions_OptimizeMode_descriptor(), name, value);
+}
+enum FieldOptions_CType {
+ FieldOptions_CType_STRING = 0,
+ FieldOptions_CType_CORD = 1,
+ FieldOptions_CType_STRING_PIECE = 2
+};
+LIBPROTOBUF_EXPORT bool FieldOptions_CType_IsValid(int value);
+const FieldOptions_CType FieldOptions_CType_CType_MIN = FieldOptions_CType_STRING;
+const FieldOptions_CType FieldOptions_CType_CType_MAX = FieldOptions_CType_STRING_PIECE;
+const int FieldOptions_CType_CType_ARRAYSIZE = FieldOptions_CType_CType_MAX + 1;
+
+LIBPROTOBUF_EXPORT const ::google::protobuf::EnumDescriptor* FieldOptions_CType_descriptor();
+inline const ::std::string& FieldOptions_CType_Name(FieldOptions_CType value) {
+ return ::google::protobuf::internal::NameOfEnum(
+ FieldOptions_CType_descriptor(), value);
+}
+inline bool FieldOptions_CType_Parse(
+ const ::std::string& name, FieldOptions_CType* value) {
+ return ::google::protobuf::internal::ParseNamedEnum<FieldOptions_CType>(
+ FieldOptions_CType_descriptor(), name, value);
+}
+// ===================================================================
+
+class LIBPROTOBUF_EXPORT FileDescriptorSet : public ::google::protobuf::Message {
+ public:
+ FileDescriptorSet();
+ virtual ~FileDescriptorSet();
+
+ FileDescriptorSet(const FileDescriptorSet& from);
+
+ inline FileDescriptorSet& operator=(const FileDescriptorSet& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const FileDescriptorSet& default_instance();
+
+ void Swap(FileDescriptorSet* other);
+
+ // implements Message ----------------------------------------------
+
+ FileDescriptorSet* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const FileDescriptorSet& from);
+ void MergeFrom(const FileDescriptorSet& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated .google.protobuf.FileDescriptorProto file = 1;
+ inline int file_size() const;
+ inline void clear_file();
+ static const int kFileFieldNumber = 1;
+ inline const ::google::protobuf::FileDescriptorProto& file(int index) const;
+ inline ::google::protobuf::FileDescriptorProto* mutable_file(int index);
+ inline ::google::protobuf::FileDescriptorProto* add_file();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FileDescriptorProto >&
+ file() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FileDescriptorProto >*
+ mutable_file();
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.FileDescriptorSet)
+ private:
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::FileDescriptorProto > file_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static FileDescriptorSet* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT FileDescriptorProto : public ::google::protobuf::Message {
+ public:
+ FileDescriptorProto();
+ virtual ~FileDescriptorProto();
+
+ FileDescriptorProto(const FileDescriptorProto& from);
+
+ inline FileDescriptorProto& operator=(const FileDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const FileDescriptorProto& default_instance();
+
+ void Swap(FileDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ FileDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const FileDescriptorProto& from);
+ void MergeFrom(const FileDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional string package = 2;
+ inline bool has_package() const;
+ inline void clear_package();
+ static const int kPackageFieldNumber = 2;
+ inline const ::std::string& package() const;
+ inline void set_package(const ::std::string& value);
+ inline void set_package(const char* value);
+ inline void set_package(const char* value, size_t size);
+ inline ::std::string* mutable_package();
+ inline ::std::string* release_package();
+ inline void set_allocated_package(::std::string* package);
+
+ // repeated string dependency = 3;
+ inline int dependency_size() const;
+ inline void clear_dependency();
+ static const int kDependencyFieldNumber = 3;
+ inline const ::std::string& dependency(int index) const;
+ inline ::std::string* mutable_dependency(int index);
+ inline void set_dependency(int index, const ::std::string& value);
+ inline void set_dependency(int index, const char* value);
+ inline void set_dependency(int index, const char* value, size_t size);
+ inline ::std::string* add_dependency();
+ inline void add_dependency(const ::std::string& value);
+ inline void add_dependency(const char* value);
+ inline void add_dependency(const char* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& dependency() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_dependency();
+
+ // repeated int32 public_dependency = 10;
+ inline int public_dependency_size() const;
+ inline void clear_public_dependency();
+ static const int kPublicDependencyFieldNumber = 10;
+ inline ::google::protobuf::int32 public_dependency(int index) const;
+ inline void set_public_dependency(int index, ::google::protobuf::int32 value);
+ inline void add_public_dependency(::google::protobuf::int32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ public_dependency() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ mutable_public_dependency();
+
+ // repeated int32 weak_dependency = 11;
+ inline int weak_dependency_size() const;
+ inline void clear_weak_dependency();
+ static const int kWeakDependencyFieldNumber = 11;
+ inline ::google::protobuf::int32 weak_dependency(int index) const;
+ inline void set_weak_dependency(int index, ::google::protobuf::int32 value);
+ inline void add_weak_dependency(::google::protobuf::int32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ weak_dependency() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ mutable_weak_dependency();
+
+ // repeated .google.protobuf.DescriptorProto message_type = 4;
+ inline int message_type_size() const;
+ inline void clear_message_type();
+ static const int kMessageTypeFieldNumber = 4;
+ inline const ::google::protobuf::DescriptorProto& message_type(int index) const;
+ inline ::google::protobuf::DescriptorProto* mutable_message_type(int index);
+ inline ::google::protobuf::DescriptorProto* add_message_type();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >&
+ message_type() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >*
+ mutable_message_type();
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 5;
+ inline int enum_type_size() const;
+ inline void clear_enum_type();
+ static const int kEnumTypeFieldNumber = 5;
+ inline const ::google::protobuf::EnumDescriptorProto& enum_type(int index) const;
+ inline ::google::protobuf::EnumDescriptorProto* mutable_enum_type(int index);
+ inline ::google::protobuf::EnumDescriptorProto* add_enum_type();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >&
+ enum_type() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >*
+ mutable_enum_type();
+
+ // repeated .google.protobuf.ServiceDescriptorProto service = 6;
+ inline int service_size() const;
+ inline void clear_service();
+ static const int kServiceFieldNumber = 6;
+ inline const ::google::protobuf::ServiceDescriptorProto& service(int index) const;
+ inline ::google::protobuf::ServiceDescriptorProto* mutable_service(int index);
+ inline ::google::protobuf::ServiceDescriptorProto* add_service();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::ServiceDescriptorProto >&
+ service() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::ServiceDescriptorProto >*
+ mutable_service();
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 7;
+ inline int extension_size() const;
+ inline void clear_extension();
+ static const int kExtensionFieldNumber = 7;
+ inline const ::google::protobuf::FieldDescriptorProto& extension(int index) const;
+ inline ::google::protobuf::FieldDescriptorProto* mutable_extension(int index);
+ inline ::google::protobuf::FieldDescriptorProto* add_extension();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >&
+ extension() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >*
+ mutable_extension();
+
+ // optional .google.protobuf.FileOptions options = 8;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 8;
+ inline const ::google::protobuf::FileOptions& options() const;
+ inline ::google::protobuf::FileOptions* mutable_options();
+ inline ::google::protobuf::FileOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::FileOptions* options);
+
+ // optional .google.protobuf.SourceCodeInfo source_code_info = 9;
+ inline bool has_source_code_info() const;
+ inline void clear_source_code_info();
+ static const int kSourceCodeInfoFieldNumber = 9;
+ inline const ::google::protobuf::SourceCodeInfo& source_code_info() const;
+ inline ::google::protobuf::SourceCodeInfo* mutable_source_code_info();
+ inline ::google::protobuf::SourceCodeInfo* release_source_code_info();
+ inline void set_allocated_source_code_info(::google::protobuf::SourceCodeInfo* source_code_info);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.FileDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_package();
+ inline void clear_has_package();
+ inline void set_has_options();
+ inline void clear_has_options();
+ inline void set_has_source_code_info();
+ inline void clear_has_source_code_info();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::std::string* package_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> dependency_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::int32 > public_dependency_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::int32 > weak_dependency_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto > message_type_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto > enum_type_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::ServiceDescriptorProto > service_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto > extension_;
+ ::google::protobuf::FileOptions* options_;
+ ::google::protobuf::SourceCodeInfo* source_code_info_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static FileDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT DescriptorProto_ExtensionRange : public ::google::protobuf::Message {
+ public:
+ DescriptorProto_ExtensionRange();
+ virtual ~DescriptorProto_ExtensionRange();
+
+ DescriptorProto_ExtensionRange(const DescriptorProto_ExtensionRange& from);
+
+ inline DescriptorProto_ExtensionRange& operator=(const DescriptorProto_ExtensionRange& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const DescriptorProto_ExtensionRange& default_instance();
+
+ void Swap(DescriptorProto_ExtensionRange* other);
+
+ // implements Message ----------------------------------------------
+
+ DescriptorProto_ExtensionRange* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const DescriptorProto_ExtensionRange& from);
+ void MergeFrom(const DescriptorProto_ExtensionRange& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int32 start = 1;
+ inline bool has_start() const;
+ inline void clear_start();
+ static const int kStartFieldNumber = 1;
+ inline ::google::protobuf::int32 start() const;
+ inline void set_start(::google::protobuf::int32 value);
+
+ // optional int32 end = 2;
+ inline bool has_end() const;
+ inline void clear_end();
+ static const int kEndFieldNumber = 2;
+ inline ::google::protobuf::int32 end() const;
+ inline void set_end(::google::protobuf::int32 value);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.DescriptorProto.ExtensionRange)
+ private:
+ inline void set_has_start();
+ inline void clear_has_start();
+ inline void set_has_end();
+ inline void clear_has_end();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::int32 start_;
+ ::google::protobuf::int32 end_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static DescriptorProto_ExtensionRange* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT DescriptorProto : public ::google::protobuf::Message {
+ public:
+ DescriptorProto();
+ virtual ~DescriptorProto();
+
+ DescriptorProto(const DescriptorProto& from);
+
+ inline DescriptorProto& operator=(const DescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const DescriptorProto& default_instance();
+
+ void Swap(DescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ DescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const DescriptorProto& from);
+ void MergeFrom(const DescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef DescriptorProto_ExtensionRange ExtensionRange;
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // repeated .google.protobuf.FieldDescriptorProto field = 2;
+ inline int field_size() const;
+ inline void clear_field();
+ static const int kFieldFieldNumber = 2;
+ inline const ::google::protobuf::FieldDescriptorProto& field(int index) const;
+ inline ::google::protobuf::FieldDescriptorProto* mutable_field(int index);
+ inline ::google::protobuf::FieldDescriptorProto* add_field();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >&
+ field() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >*
+ mutable_field();
+
+ // repeated .google.protobuf.FieldDescriptorProto extension = 6;
+ inline int extension_size() const;
+ inline void clear_extension();
+ static const int kExtensionFieldNumber = 6;
+ inline const ::google::protobuf::FieldDescriptorProto& extension(int index) const;
+ inline ::google::protobuf::FieldDescriptorProto* mutable_extension(int index);
+ inline ::google::protobuf::FieldDescriptorProto* add_extension();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >&
+ extension() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >*
+ mutable_extension();
+
+ // repeated .google.protobuf.DescriptorProto nested_type = 3;
+ inline int nested_type_size() const;
+ inline void clear_nested_type();
+ static const int kNestedTypeFieldNumber = 3;
+ inline const ::google::protobuf::DescriptorProto& nested_type(int index) const;
+ inline ::google::protobuf::DescriptorProto* mutable_nested_type(int index);
+ inline ::google::protobuf::DescriptorProto* add_nested_type();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >&
+ nested_type() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >*
+ mutable_nested_type();
+
+ // repeated .google.protobuf.EnumDescriptorProto enum_type = 4;
+ inline int enum_type_size() const;
+ inline void clear_enum_type();
+ static const int kEnumTypeFieldNumber = 4;
+ inline const ::google::protobuf::EnumDescriptorProto& enum_type(int index) const;
+ inline ::google::protobuf::EnumDescriptorProto* mutable_enum_type(int index);
+ inline ::google::protobuf::EnumDescriptorProto* add_enum_type();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >&
+ enum_type() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >*
+ mutable_enum_type();
+
+ // repeated .google.protobuf.DescriptorProto.ExtensionRange extension_range = 5;
+ inline int extension_range_size() const;
+ inline void clear_extension_range();
+ static const int kExtensionRangeFieldNumber = 5;
+ inline const ::google::protobuf::DescriptorProto_ExtensionRange& extension_range(int index) const;
+ inline ::google::protobuf::DescriptorProto_ExtensionRange* mutable_extension_range(int index);
+ inline ::google::protobuf::DescriptorProto_ExtensionRange* add_extension_range();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto_ExtensionRange >&
+ extension_range() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto_ExtensionRange >*
+ mutable_extension_range();
+
+ // repeated .google.protobuf.OneofDescriptorProto oneof_decl = 8;
+ inline int oneof_decl_size() const;
+ inline void clear_oneof_decl();
+ static const int kOneofDeclFieldNumber = 8;
+ inline const ::google::protobuf::OneofDescriptorProto& oneof_decl(int index) const;
+ inline ::google::protobuf::OneofDescriptorProto* mutable_oneof_decl(int index);
+ inline ::google::protobuf::OneofDescriptorProto* add_oneof_decl();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::OneofDescriptorProto >&
+ oneof_decl() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::OneofDescriptorProto >*
+ mutable_oneof_decl();
+
+ // optional .google.protobuf.MessageOptions options = 7;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 7;
+ inline const ::google::protobuf::MessageOptions& options() const;
+ inline ::google::protobuf::MessageOptions* mutable_options();
+ inline ::google::protobuf::MessageOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::MessageOptions* options);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.DescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_options();
+ inline void clear_has_options();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto > field_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto > extension_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto > nested_type_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto > enum_type_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto_ExtensionRange > extension_range_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::OneofDescriptorProto > oneof_decl_;
+ ::google::protobuf::MessageOptions* options_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static DescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT FieldDescriptorProto : public ::google::protobuf::Message {
+ public:
+ FieldDescriptorProto();
+ virtual ~FieldDescriptorProto();
+
+ FieldDescriptorProto(const FieldDescriptorProto& from);
+
+ inline FieldDescriptorProto& operator=(const FieldDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const FieldDescriptorProto& default_instance();
+
+ void Swap(FieldDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ FieldDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const FieldDescriptorProto& from);
+ void MergeFrom(const FieldDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FieldDescriptorProto_Type Type;
+ static const Type TYPE_DOUBLE = FieldDescriptorProto_Type_TYPE_DOUBLE;
+ static const Type TYPE_FLOAT = FieldDescriptorProto_Type_TYPE_FLOAT;
+ static const Type TYPE_INT64 = FieldDescriptorProto_Type_TYPE_INT64;
+ static const Type TYPE_UINT64 = FieldDescriptorProto_Type_TYPE_UINT64;
+ static const Type TYPE_INT32 = FieldDescriptorProto_Type_TYPE_INT32;
+ static const Type TYPE_FIXED64 = FieldDescriptorProto_Type_TYPE_FIXED64;
+ static const Type TYPE_FIXED32 = FieldDescriptorProto_Type_TYPE_FIXED32;
+ static const Type TYPE_BOOL = FieldDescriptorProto_Type_TYPE_BOOL;
+ static const Type TYPE_STRING = FieldDescriptorProto_Type_TYPE_STRING;
+ static const Type TYPE_GROUP = FieldDescriptorProto_Type_TYPE_GROUP;
+ static const Type TYPE_MESSAGE = FieldDescriptorProto_Type_TYPE_MESSAGE;
+ static const Type TYPE_BYTES = FieldDescriptorProto_Type_TYPE_BYTES;
+ static const Type TYPE_UINT32 = FieldDescriptorProto_Type_TYPE_UINT32;
+ static const Type TYPE_ENUM = FieldDescriptorProto_Type_TYPE_ENUM;
+ static const Type TYPE_SFIXED32 = FieldDescriptorProto_Type_TYPE_SFIXED32;
+ static const Type TYPE_SFIXED64 = FieldDescriptorProto_Type_TYPE_SFIXED64;
+ static const Type TYPE_SINT32 = FieldDescriptorProto_Type_TYPE_SINT32;
+ static const Type TYPE_SINT64 = FieldDescriptorProto_Type_TYPE_SINT64;
+ static inline bool Type_IsValid(int value) {
+ return FieldDescriptorProto_Type_IsValid(value);
+ }
+ static const Type Type_MIN =
+ FieldDescriptorProto_Type_Type_MIN;
+ static const Type Type_MAX =
+ FieldDescriptorProto_Type_Type_MAX;
+ static const int Type_ARRAYSIZE =
+ FieldDescriptorProto_Type_Type_ARRAYSIZE;
+ static inline const ::google::protobuf::EnumDescriptor*
+ Type_descriptor() {
+ return FieldDescriptorProto_Type_descriptor();
+ }
+ static inline const ::std::string& Type_Name(Type value) {
+ return FieldDescriptorProto_Type_Name(value);
+ }
+ static inline bool Type_Parse(const ::std::string& name,
+ Type* value) {
+ return FieldDescriptorProto_Type_Parse(name, value);
+ }
+
+ typedef FieldDescriptorProto_Label Label;
+ static const Label LABEL_OPTIONAL = FieldDescriptorProto_Label_LABEL_OPTIONAL;
+ static const Label LABEL_REQUIRED = FieldDescriptorProto_Label_LABEL_REQUIRED;
+ static const Label LABEL_REPEATED = FieldDescriptorProto_Label_LABEL_REPEATED;
+ static inline bool Label_IsValid(int value) {
+ return FieldDescriptorProto_Label_IsValid(value);
+ }
+ static const Label Label_MIN =
+ FieldDescriptorProto_Label_Label_MIN;
+ static const Label Label_MAX =
+ FieldDescriptorProto_Label_Label_MAX;
+ static const int Label_ARRAYSIZE =
+ FieldDescriptorProto_Label_Label_ARRAYSIZE;
+ static inline const ::google::protobuf::EnumDescriptor*
+ Label_descriptor() {
+ return FieldDescriptorProto_Label_descriptor();
+ }
+ static inline const ::std::string& Label_Name(Label value) {
+ return FieldDescriptorProto_Label_Name(value);
+ }
+ static inline bool Label_Parse(const ::std::string& name,
+ Label* value) {
+ return FieldDescriptorProto_Label_Parse(name, value);
+ }
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional int32 number = 3;
+ inline bool has_number() const;
+ inline void clear_number();
+ static const int kNumberFieldNumber = 3;
+ inline ::google::protobuf::int32 number() const;
+ inline void set_number(::google::protobuf::int32 value);
+
+ // optional .google.protobuf.FieldDescriptorProto.Label label = 4;
+ inline bool has_label() const;
+ inline void clear_label();
+ static const int kLabelFieldNumber = 4;
+ inline ::google::protobuf::FieldDescriptorProto_Label label() const;
+ inline void set_label(::google::protobuf::FieldDescriptorProto_Label value);
+
+ // optional .google.protobuf.FieldDescriptorProto.Type type = 5;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 5;
+ inline ::google::protobuf::FieldDescriptorProto_Type type() const;
+ inline void set_type(::google::protobuf::FieldDescriptorProto_Type value);
+
+ // optional string type_name = 6;
+ inline bool has_type_name() const;
+ inline void clear_type_name();
+ static const int kTypeNameFieldNumber = 6;
+ inline const ::std::string& type_name() const;
+ inline void set_type_name(const ::std::string& value);
+ inline void set_type_name(const char* value);
+ inline void set_type_name(const char* value, size_t size);
+ inline ::std::string* mutable_type_name();
+ inline ::std::string* release_type_name();
+ inline void set_allocated_type_name(::std::string* type_name);
+
+ // optional string extendee = 2;
+ inline bool has_extendee() const;
+ inline void clear_extendee();
+ static const int kExtendeeFieldNumber = 2;
+ inline const ::std::string& extendee() const;
+ inline void set_extendee(const ::std::string& value);
+ inline void set_extendee(const char* value);
+ inline void set_extendee(const char* value, size_t size);
+ inline ::std::string* mutable_extendee();
+ inline ::std::string* release_extendee();
+ inline void set_allocated_extendee(::std::string* extendee);
+
+ // optional string default_value = 7;
+ inline bool has_default_value() const;
+ inline void clear_default_value();
+ static const int kDefaultValueFieldNumber = 7;
+ inline const ::std::string& default_value() const;
+ inline void set_default_value(const ::std::string& value);
+ inline void set_default_value(const char* value);
+ inline void set_default_value(const char* value, size_t size);
+ inline ::std::string* mutable_default_value();
+ inline ::std::string* release_default_value();
+ inline void set_allocated_default_value(::std::string* default_value);
+
+ // optional int32 oneof_index = 9;
+ inline bool has_oneof_index() const;
+ inline void clear_oneof_index();
+ static const int kOneofIndexFieldNumber = 9;
+ inline ::google::protobuf::int32 oneof_index() const;
+ inline void set_oneof_index(::google::protobuf::int32 value);
+
+ // optional .google.protobuf.FieldOptions options = 8;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 8;
+ inline const ::google::protobuf::FieldOptions& options() const;
+ inline ::google::protobuf::FieldOptions* mutable_options();
+ inline ::google::protobuf::FieldOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::FieldOptions* options);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.FieldDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_number();
+ inline void clear_has_number();
+ inline void set_has_label();
+ inline void clear_has_label();
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_type_name();
+ inline void clear_has_type_name();
+ inline void set_has_extendee();
+ inline void clear_has_extendee();
+ inline void set_has_default_value();
+ inline void clear_has_default_value();
+ inline void set_has_oneof_index();
+ inline void clear_has_oneof_index();
+ inline void set_has_options();
+ inline void clear_has_options();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::int32 number_;
+ int label_;
+ ::std::string* type_name_;
+ ::std::string* extendee_;
+ int type_;
+ ::google::protobuf::int32 oneof_index_;
+ ::std::string* default_value_;
+ ::google::protobuf::FieldOptions* options_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static FieldDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT OneofDescriptorProto : public ::google::protobuf::Message {
+ public:
+ OneofDescriptorProto();
+ virtual ~OneofDescriptorProto();
+
+ OneofDescriptorProto(const OneofDescriptorProto& from);
+
+ inline OneofDescriptorProto& operator=(const OneofDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const OneofDescriptorProto& default_instance();
+
+ void Swap(OneofDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ OneofDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const OneofDescriptorProto& from);
+ void MergeFrom(const OneofDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.OneofDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static OneofDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT EnumDescriptorProto : public ::google::protobuf::Message {
+ public:
+ EnumDescriptorProto();
+ virtual ~EnumDescriptorProto();
+
+ EnumDescriptorProto(const EnumDescriptorProto& from);
+
+ inline EnumDescriptorProto& operator=(const EnumDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const EnumDescriptorProto& default_instance();
+
+ void Swap(EnumDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ EnumDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const EnumDescriptorProto& from);
+ void MergeFrom(const EnumDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // repeated .google.protobuf.EnumValueDescriptorProto value = 2;
+ inline int value_size() const;
+ inline void clear_value();
+ static const int kValueFieldNumber = 2;
+ inline const ::google::protobuf::EnumValueDescriptorProto& value(int index) const;
+ inline ::google::protobuf::EnumValueDescriptorProto* mutable_value(int index);
+ inline ::google::protobuf::EnumValueDescriptorProto* add_value();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumValueDescriptorProto >&
+ value() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumValueDescriptorProto >*
+ mutable_value();
+
+ // optional .google.protobuf.EnumOptions options = 3;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 3;
+ inline const ::google::protobuf::EnumOptions& options() const;
+ inline ::google::protobuf::EnumOptions* mutable_options();
+ inline ::google::protobuf::EnumOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::EnumOptions* options);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.EnumDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_options();
+ inline void clear_has_options();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumValueDescriptorProto > value_;
+ ::google::protobuf::EnumOptions* options_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static EnumDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT EnumValueDescriptorProto : public ::google::protobuf::Message {
+ public:
+ EnumValueDescriptorProto();
+ virtual ~EnumValueDescriptorProto();
+
+ EnumValueDescriptorProto(const EnumValueDescriptorProto& from);
+
+ inline EnumValueDescriptorProto& operator=(const EnumValueDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const EnumValueDescriptorProto& default_instance();
+
+ void Swap(EnumValueDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ EnumValueDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const EnumValueDescriptorProto& from);
+ void MergeFrom(const EnumValueDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional int32 number = 2;
+ inline bool has_number() const;
+ inline void clear_number();
+ static const int kNumberFieldNumber = 2;
+ inline ::google::protobuf::int32 number() const;
+ inline void set_number(::google::protobuf::int32 value);
+
+ // optional .google.protobuf.EnumValueOptions options = 3;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 3;
+ inline const ::google::protobuf::EnumValueOptions& options() const;
+ inline ::google::protobuf::EnumValueOptions* mutable_options();
+ inline ::google::protobuf::EnumValueOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::EnumValueOptions* options);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.EnumValueDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_number();
+ inline void clear_has_number();
+ inline void set_has_options();
+ inline void clear_has_options();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::EnumValueOptions* options_;
+ ::google::protobuf::int32 number_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static EnumValueDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT ServiceDescriptorProto : public ::google::protobuf::Message {
+ public:
+ ServiceDescriptorProto();
+ virtual ~ServiceDescriptorProto();
+
+ ServiceDescriptorProto(const ServiceDescriptorProto& from);
+
+ inline ServiceDescriptorProto& operator=(const ServiceDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const ServiceDescriptorProto& default_instance();
+
+ void Swap(ServiceDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ ServiceDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const ServiceDescriptorProto& from);
+ void MergeFrom(const ServiceDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // repeated .google.protobuf.MethodDescriptorProto method = 2;
+ inline int method_size() const;
+ inline void clear_method();
+ static const int kMethodFieldNumber = 2;
+ inline const ::google::protobuf::MethodDescriptorProto& method(int index) const;
+ inline ::google::protobuf::MethodDescriptorProto* mutable_method(int index);
+ inline ::google::protobuf::MethodDescriptorProto* add_method();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::MethodDescriptorProto >&
+ method() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::MethodDescriptorProto >*
+ mutable_method();
+
+ // optional .google.protobuf.ServiceOptions options = 3;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 3;
+ inline const ::google::protobuf::ServiceOptions& options() const;
+ inline ::google::protobuf::ServiceOptions* mutable_options();
+ inline ::google::protobuf::ServiceOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::ServiceOptions* options);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.ServiceDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_options();
+ inline void clear_has_options();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::MethodDescriptorProto > method_;
+ ::google::protobuf::ServiceOptions* options_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static ServiceDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT MethodDescriptorProto : public ::google::protobuf::Message {
+ public:
+ MethodDescriptorProto();
+ virtual ~MethodDescriptorProto();
+
+ MethodDescriptorProto(const MethodDescriptorProto& from);
+
+ inline MethodDescriptorProto& operator=(const MethodDescriptorProto& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const MethodDescriptorProto& default_instance();
+
+ void Swap(MethodDescriptorProto* other);
+
+ // implements Message ----------------------------------------------
+
+ MethodDescriptorProto* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const MethodDescriptorProto& from);
+ void MergeFrom(const MethodDescriptorProto& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string name = 1;
+ inline bool has_name() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 1;
+ inline const ::std::string& name() const;
+ inline void set_name(const ::std::string& value);
+ inline void set_name(const char* value);
+ inline void set_name(const char* value, size_t size);
+ inline ::std::string* mutable_name();
+ inline ::std::string* release_name();
+ inline void set_allocated_name(::std::string* name);
+
+ // optional string input_type = 2;
+ inline bool has_input_type() const;
+ inline void clear_input_type();
+ static const int kInputTypeFieldNumber = 2;
+ inline const ::std::string& input_type() const;
+ inline void set_input_type(const ::std::string& value);
+ inline void set_input_type(const char* value);
+ inline void set_input_type(const char* value, size_t size);
+ inline ::std::string* mutable_input_type();
+ inline ::std::string* release_input_type();
+ inline void set_allocated_input_type(::std::string* input_type);
+
+ // optional string output_type = 3;
+ inline bool has_output_type() const;
+ inline void clear_output_type();
+ static const int kOutputTypeFieldNumber = 3;
+ inline const ::std::string& output_type() const;
+ inline void set_output_type(const ::std::string& value);
+ inline void set_output_type(const char* value);
+ inline void set_output_type(const char* value, size_t size);
+ inline ::std::string* mutable_output_type();
+ inline ::std::string* release_output_type();
+ inline void set_allocated_output_type(::std::string* output_type);
+
+ // optional .google.protobuf.MethodOptions options = 4;
+ inline bool has_options() const;
+ inline void clear_options();
+ static const int kOptionsFieldNumber = 4;
+ inline const ::google::protobuf::MethodOptions& options() const;
+ inline ::google::protobuf::MethodOptions* mutable_options();
+ inline ::google::protobuf::MethodOptions* release_options();
+ inline void set_allocated_options(::google::protobuf::MethodOptions* options);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.MethodDescriptorProto)
+ private:
+ inline void set_has_name();
+ inline void clear_has_name();
+ inline void set_has_input_type();
+ inline void clear_has_input_type();
+ inline void set_has_output_type();
+ inline void clear_has_output_type();
+ inline void set_has_options();
+ inline void clear_has_options();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_;
+ ::std::string* input_type_;
+ ::std::string* output_type_;
+ ::google::protobuf::MethodOptions* options_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static MethodDescriptorProto* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT FileOptions : public ::google::protobuf::Message {
+ public:
+ FileOptions();
+ virtual ~FileOptions();
+
+ FileOptions(const FileOptions& from);
+
+ inline FileOptions& operator=(const FileOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const FileOptions& default_instance();
+
+ void Swap(FileOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ FileOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const FileOptions& from);
+ void MergeFrom(const FileOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FileOptions_OptimizeMode OptimizeMode;
+ static const OptimizeMode SPEED = FileOptions_OptimizeMode_SPEED;
+ static const OptimizeMode CODE_SIZE = FileOptions_OptimizeMode_CODE_SIZE;
+ static const OptimizeMode LITE_RUNTIME = FileOptions_OptimizeMode_LITE_RUNTIME;
+ static inline bool OptimizeMode_IsValid(int value) {
+ return FileOptions_OptimizeMode_IsValid(value);
+ }
+ static const OptimizeMode OptimizeMode_MIN =
+ FileOptions_OptimizeMode_OptimizeMode_MIN;
+ static const OptimizeMode OptimizeMode_MAX =
+ FileOptions_OptimizeMode_OptimizeMode_MAX;
+ static const int OptimizeMode_ARRAYSIZE =
+ FileOptions_OptimizeMode_OptimizeMode_ARRAYSIZE;
+ static inline const ::google::protobuf::EnumDescriptor*
+ OptimizeMode_descriptor() {
+ return FileOptions_OptimizeMode_descriptor();
+ }
+ static inline const ::std::string& OptimizeMode_Name(OptimizeMode value) {
+ return FileOptions_OptimizeMode_Name(value);
+ }
+ static inline bool OptimizeMode_Parse(const ::std::string& name,
+ OptimizeMode* value) {
+ return FileOptions_OptimizeMode_Parse(name, value);
+ }
+
+ // accessors -------------------------------------------------------
+
+ // optional string java_package = 1;
+ inline bool has_java_package() const;
+ inline void clear_java_package();
+ static const int kJavaPackageFieldNumber = 1;
+ inline const ::std::string& java_package() const;
+ inline void set_java_package(const ::std::string& value);
+ inline void set_java_package(const char* value);
+ inline void set_java_package(const char* value, size_t size);
+ inline ::std::string* mutable_java_package();
+ inline ::std::string* release_java_package();
+ inline void set_allocated_java_package(::std::string* java_package);
+
+ // optional string java_outer_classname = 8;
+ inline bool has_java_outer_classname() const;
+ inline void clear_java_outer_classname();
+ static const int kJavaOuterClassnameFieldNumber = 8;
+ inline const ::std::string& java_outer_classname() const;
+ inline void set_java_outer_classname(const ::std::string& value);
+ inline void set_java_outer_classname(const char* value);
+ inline void set_java_outer_classname(const char* value, size_t size);
+ inline ::std::string* mutable_java_outer_classname();
+ inline ::std::string* release_java_outer_classname();
+ inline void set_allocated_java_outer_classname(::std::string* java_outer_classname);
+
+ // optional bool java_multiple_files = 10 [default = false];
+ inline bool has_java_multiple_files() const;
+ inline void clear_java_multiple_files();
+ static const int kJavaMultipleFilesFieldNumber = 10;
+ inline bool java_multiple_files() const;
+ inline void set_java_multiple_files(bool value);
+
+ // optional bool java_generate_equals_and_hash = 20 [default = false];
+ inline bool has_java_generate_equals_and_hash() const;
+ inline void clear_java_generate_equals_and_hash();
+ static const int kJavaGenerateEqualsAndHashFieldNumber = 20;
+ inline bool java_generate_equals_and_hash() const;
+ inline void set_java_generate_equals_and_hash(bool value);
+
+ // optional bool java_string_check_utf8 = 27 [default = false];
+ inline bool has_java_string_check_utf8() const;
+ inline void clear_java_string_check_utf8();
+ static const int kJavaStringCheckUtf8FieldNumber = 27;
+ inline bool java_string_check_utf8() const;
+ inline void set_java_string_check_utf8(bool value);
+
+ // optional .google.protobuf.FileOptions.OptimizeMode optimize_for = 9 [default = SPEED];
+ inline bool has_optimize_for() const;
+ inline void clear_optimize_for();
+ static const int kOptimizeForFieldNumber = 9;
+ inline ::google::protobuf::FileOptions_OptimizeMode optimize_for() const;
+ inline void set_optimize_for(::google::protobuf::FileOptions_OptimizeMode value);
+
+ // optional string go_package = 11;
+ inline bool has_go_package() const;
+ inline void clear_go_package();
+ static const int kGoPackageFieldNumber = 11;
+ inline const ::std::string& go_package() const;
+ inline void set_go_package(const ::std::string& value);
+ inline void set_go_package(const char* value);
+ inline void set_go_package(const char* value, size_t size);
+ inline ::std::string* mutable_go_package();
+ inline ::std::string* release_go_package();
+ inline void set_allocated_go_package(::std::string* go_package);
+
+ // optional bool cc_generic_services = 16 [default = false];
+ inline bool has_cc_generic_services() const;
+ inline void clear_cc_generic_services();
+ static const int kCcGenericServicesFieldNumber = 16;
+ inline bool cc_generic_services() const;
+ inline void set_cc_generic_services(bool value);
+
+ // optional bool java_generic_services = 17 [default = false];
+ inline bool has_java_generic_services() const;
+ inline void clear_java_generic_services();
+ static const int kJavaGenericServicesFieldNumber = 17;
+ inline bool java_generic_services() const;
+ inline void set_java_generic_services(bool value);
+
+ // optional bool py_generic_services = 18 [default = false];
+ inline bool has_py_generic_services() const;
+ inline void clear_py_generic_services();
+ static const int kPyGenericServicesFieldNumber = 18;
+ inline bool py_generic_services() const;
+ inline void set_py_generic_services(bool value);
+
+ // optional bool deprecated = 23 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 23;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(FileOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.FileOptions)
+ private:
+ inline void set_has_java_package();
+ inline void clear_has_java_package();
+ inline void set_has_java_outer_classname();
+ inline void clear_has_java_outer_classname();
+ inline void set_has_java_multiple_files();
+ inline void clear_has_java_multiple_files();
+ inline void set_has_java_generate_equals_and_hash();
+ inline void clear_has_java_generate_equals_and_hash();
+ inline void set_has_java_string_check_utf8();
+ inline void clear_has_java_string_check_utf8();
+ inline void set_has_optimize_for();
+ inline void clear_has_optimize_for();
+ inline void set_has_go_package();
+ inline void clear_has_go_package();
+ inline void set_has_cc_generic_services();
+ inline void clear_has_cc_generic_services();
+ inline void set_has_java_generic_services();
+ inline void clear_has_java_generic_services();
+ inline void set_has_py_generic_services();
+ inline void clear_has_py_generic_services();
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* java_package_;
+ ::std::string* java_outer_classname_;
+ bool java_multiple_files_;
+ bool java_generate_equals_and_hash_;
+ bool java_string_check_utf8_;
+ bool cc_generic_services_;
+ int optimize_for_;
+ ::std::string* go_package_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ bool java_generic_services_;
+ bool py_generic_services_;
+ bool deprecated_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static FileOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT MessageOptions : public ::google::protobuf::Message {
+ public:
+ MessageOptions();
+ virtual ~MessageOptions();
+
+ MessageOptions(const MessageOptions& from);
+
+ inline MessageOptions& operator=(const MessageOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const MessageOptions& default_instance();
+
+ void Swap(MessageOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ MessageOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const MessageOptions& from);
+ void MergeFrom(const MessageOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bool message_set_wire_format = 1 [default = false];
+ inline bool has_message_set_wire_format() const;
+ inline void clear_message_set_wire_format();
+ static const int kMessageSetWireFormatFieldNumber = 1;
+ inline bool message_set_wire_format() const;
+ inline void set_message_set_wire_format(bool value);
+
+ // optional bool no_standard_descriptor_accessor = 2 [default = false];
+ inline bool has_no_standard_descriptor_accessor() const;
+ inline void clear_no_standard_descriptor_accessor();
+ static const int kNoStandardDescriptorAccessorFieldNumber = 2;
+ inline bool no_standard_descriptor_accessor() const;
+ inline void set_no_standard_descriptor_accessor(bool value);
+
+ // optional bool deprecated = 3 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 3;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(MessageOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.MessageOptions)
+ private:
+ inline void set_has_message_set_wire_format();
+ inline void clear_has_message_set_wire_format();
+ inline void set_has_no_standard_descriptor_accessor();
+ inline void clear_has_no_standard_descriptor_accessor();
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ bool message_set_wire_format_;
+ bool no_standard_descriptor_accessor_;
+ bool deprecated_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static MessageOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT FieldOptions : public ::google::protobuf::Message {
+ public:
+ FieldOptions();
+ virtual ~FieldOptions();
+
+ FieldOptions(const FieldOptions& from);
+
+ inline FieldOptions& operator=(const FieldOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const FieldOptions& default_instance();
+
+ void Swap(FieldOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ FieldOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const FieldOptions& from);
+ void MergeFrom(const FieldOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FieldOptions_CType CType;
+ static const CType STRING = FieldOptions_CType_STRING;
+ static const CType CORD = FieldOptions_CType_CORD;
+ static const CType STRING_PIECE = FieldOptions_CType_STRING_PIECE;
+ static inline bool CType_IsValid(int value) {
+ return FieldOptions_CType_IsValid(value);
+ }
+ static const CType CType_MIN =
+ FieldOptions_CType_CType_MIN;
+ static const CType CType_MAX =
+ FieldOptions_CType_CType_MAX;
+ static const int CType_ARRAYSIZE =
+ FieldOptions_CType_CType_ARRAYSIZE;
+ static inline const ::google::protobuf::EnumDescriptor*
+ CType_descriptor() {
+ return FieldOptions_CType_descriptor();
+ }
+ static inline const ::std::string& CType_Name(CType value) {
+ return FieldOptions_CType_Name(value);
+ }
+ static inline bool CType_Parse(const ::std::string& name,
+ CType* value) {
+ return FieldOptions_CType_Parse(name, value);
+ }
+
+ // accessors -------------------------------------------------------
+
+ // optional .google.protobuf.FieldOptions.CType ctype = 1 [default = STRING];
+ inline bool has_ctype() const;
+ inline void clear_ctype();
+ static const int kCtypeFieldNumber = 1;
+ inline ::google::protobuf::FieldOptions_CType ctype() const;
+ inline void set_ctype(::google::protobuf::FieldOptions_CType value);
+
+ // optional bool packed = 2;
+ inline bool has_packed() const;
+ inline void clear_packed();
+ static const int kPackedFieldNumber = 2;
+ inline bool packed() const;
+ inline void set_packed(bool value);
+
+ // optional bool lazy = 5 [default = false];
+ inline bool has_lazy() const;
+ inline void clear_lazy();
+ static const int kLazyFieldNumber = 5;
+ inline bool lazy() const;
+ inline void set_lazy(bool value);
+
+ // optional bool deprecated = 3 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 3;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // optional string experimental_map_key = 9;
+ inline bool has_experimental_map_key() const;
+ inline void clear_experimental_map_key();
+ static const int kExperimentalMapKeyFieldNumber = 9;
+ inline const ::std::string& experimental_map_key() const;
+ inline void set_experimental_map_key(const ::std::string& value);
+ inline void set_experimental_map_key(const char* value);
+ inline void set_experimental_map_key(const char* value, size_t size);
+ inline ::std::string* mutable_experimental_map_key();
+ inline ::std::string* release_experimental_map_key();
+ inline void set_allocated_experimental_map_key(::std::string* experimental_map_key);
+
+ // optional bool weak = 10 [default = false];
+ inline bool has_weak() const;
+ inline void clear_weak();
+ static const int kWeakFieldNumber = 10;
+ inline bool weak() const;
+ inline void set_weak(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(FieldOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.FieldOptions)
+ private:
+ inline void set_has_ctype();
+ inline void clear_has_ctype();
+ inline void set_has_packed();
+ inline void clear_has_packed();
+ inline void set_has_lazy();
+ inline void clear_has_lazy();
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+ inline void set_has_experimental_map_key();
+ inline void clear_has_experimental_map_key();
+ inline void set_has_weak();
+ inline void clear_has_weak();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int ctype_;
+ bool packed_;
+ bool lazy_;
+ bool deprecated_;
+ bool weak_;
+ ::std::string* experimental_map_key_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static FieldOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT EnumOptions : public ::google::protobuf::Message {
+ public:
+ EnumOptions();
+ virtual ~EnumOptions();
+
+ EnumOptions(const EnumOptions& from);
+
+ inline EnumOptions& operator=(const EnumOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const EnumOptions& default_instance();
+
+ void Swap(EnumOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ EnumOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const EnumOptions& from);
+ void MergeFrom(const EnumOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bool allow_alias = 2;
+ inline bool has_allow_alias() const;
+ inline void clear_allow_alias();
+ static const int kAllowAliasFieldNumber = 2;
+ inline bool allow_alias() const;
+ inline void set_allow_alias(bool value);
+
+ // optional bool deprecated = 3 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 3;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(EnumOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.EnumOptions)
+ private:
+ inline void set_has_allow_alias();
+ inline void clear_has_allow_alias();
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ bool allow_alias_;
+ bool deprecated_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static EnumOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT EnumValueOptions : public ::google::protobuf::Message {
+ public:
+ EnumValueOptions();
+ virtual ~EnumValueOptions();
+
+ EnumValueOptions(const EnumValueOptions& from);
+
+ inline EnumValueOptions& operator=(const EnumValueOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const EnumValueOptions& default_instance();
+
+ void Swap(EnumValueOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ EnumValueOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const EnumValueOptions& from);
+ void MergeFrom(const EnumValueOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bool deprecated = 1 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 1;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(EnumValueOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.EnumValueOptions)
+ private:
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ bool deprecated_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static EnumValueOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT ServiceOptions : public ::google::protobuf::Message {
+ public:
+ ServiceOptions();
+ virtual ~ServiceOptions();
+
+ ServiceOptions(const ServiceOptions& from);
+
+ inline ServiceOptions& operator=(const ServiceOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const ServiceOptions& default_instance();
+
+ void Swap(ServiceOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ ServiceOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const ServiceOptions& from);
+ void MergeFrom(const ServiceOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bool deprecated = 33 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 33;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(ServiceOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.ServiceOptions)
+ private:
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ bool deprecated_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static ServiceOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT MethodOptions : public ::google::protobuf::Message {
+ public:
+ MethodOptions();
+ virtual ~MethodOptions();
+
+ MethodOptions(const MethodOptions& from);
+
+ inline MethodOptions& operator=(const MethodOptions& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const MethodOptions& default_instance();
+
+ void Swap(MethodOptions* other);
+
+ // implements Message ----------------------------------------------
+
+ MethodOptions* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const MethodOptions& from);
+ void MergeFrom(const MethodOptions& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bool deprecated = 33 [default = false];
+ inline bool has_deprecated() const;
+ inline void clear_deprecated();
+ static const int kDeprecatedFieldNumber = 33;
+ inline bool deprecated() const;
+ inline void set_deprecated(bool value);
+
+ // repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+ inline int uninterpreted_option_size() const;
+ inline void clear_uninterpreted_option();
+ static const int kUninterpretedOptionFieldNumber = 999;
+ inline const ::google::protobuf::UninterpretedOption& uninterpreted_option(int index) const;
+ inline ::google::protobuf::UninterpretedOption* mutable_uninterpreted_option(int index);
+ inline ::google::protobuf::UninterpretedOption* add_uninterpreted_option();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ uninterpreted_option() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ mutable_uninterpreted_option();
+
+ GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(MethodOptions)
+ // @@protoc_insertion_point(class_scope:google.protobuf.MethodOptions)
+ private:
+ inline void set_has_deprecated();
+ inline void clear_has_deprecated();
+
+ ::google::protobuf::internal::ExtensionSet _extensions_;
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption > uninterpreted_option_;
+ bool deprecated_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static MethodOptions* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT UninterpretedOption_NamePart : public ::google::protobuf::Message {
+ public:
+ UninterpretedOption_NamePart();
+ virtual ~UninterpretedOption_NamePart();
+
+ UninterpretedOption_NamePart(const UninterpretedOption_NamePart& from);
+
+ inline UninterpretedOption_NamePart& operator=(const UninterpretedOption_NamePart& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const UninterpretedOption_NamePart& default_instance();
+
+ void Swap(UninterpretedOption_NamePart* other);
+
+ // implements Message ----------------------------------------------
+
+ UninterpretedOption_NamePart* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const UninterpretedOption_NamePart& from);
+ void MergeFrom(const UninterpretedOption_NamePart& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // required string name_part = 1;
+ inline bool has_name_part() const;
+ inline void clear_name_part();
+ static const int kNamePartFieldNumber = 1;
+ inline const ::std::string& name_part() const;
+ inline void set_name_part(const ::std::string& value);
+ inline void set_name_part(const char* value);
+ inline void set_name_part(const char* value, size_t size);
+ inline ::std::string* mutable_name_part();
+ inline ::std::string* release_name_part();
+ inline void set_allocated_name_part(::std::string* name_part);
+
+ // required bool is_extension = 2;
+ inline bool has_is_extension() const;
+ inline void clear_is_extension();
+ static const int kIsExtensionFieldNumber = 2;
+ inline bool is_extension() const;
+ inline void set_is_extension(bool value);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.UninterpretedOption.NamePart)
+ private:
+ inline void set_has_name_part();
+ inline void clear_has_name_part();
+ inline void set_has_is_extension();
+ inline void clear_has_is_extension();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* name_part_;
+ bool is_extension_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static UninterpretedOption_NamePart* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT UninterpretedOption : public ::google::protobuf::Message {
+ public:
+ UninterpretedOption();
+ virtual ~UninterpretedOption();
+
+ UninterpretedOption(const UninterpretedOption& from);
+
+ inline UninterpretedOption& operator=(const UninterpretedOption& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const UninterpretedOption& default_instance();
+
+ void Swap(UninterpretedOption* other);
+
+ // implements Message ----------------------------------------------
+
+ UninterpretedOption* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const UninterpretedOption& from);
+ void MergeFrom(const UninterpretedOption& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef UninterpretedOption_NamePart NamePart;
+
+ // accessors -------------------------------------------------------
+
+ // repeated .google.protobuf.UninterpretedOption.NamePart name = 2;
+ inline int name_size() const;
+ inline void clear_name();
+ static const int kNameFieldNumber = 2;
+ inline const ::google::protobuf::UninterpretedOption_NamePart& name(int index) const;
+ inline ::google::protobuf::UninterpretedOption_NamePart* mutable_name(int index);
+ inline ::google::protobuf::UninterpretedOption_NamePart* add_name();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption_NamePart >&
+ name() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption_NamePart >*
+ mutable_name();
+
+ // optional string identifier_value = 3;
+ inline bool has_identifier_value() const;
+ inline void clear_identifier_value();
+ static const int kIdentifierValueFieldNumber = 3;
+ inline const ::std::string& identifier_value() const;
+ inline void set_identifier_value(const ::std::string& value);
+ inline void set_identifier_value(const char* value);
+ inline void set_identifier_value(const char* value, size_t size);
+ inline ::std::string* mutable_identifier_value();
+ inline ::std::string* release_identifier_value();
+ inline void set_allocated_identifier_value(::std::string* identifier_value);
+
+ // optional uint64 positive_int_value = 4;
+ inline bool has_positive_int_value() const;
+ inline void clear_positive_int_value();
+ static const int kPositiveIntValueFieldNumber = 4;
+ inline ::google::protobuf::uint64 positive_int_value() const;
+ inline void set_positive_int_value(::google::protobuf::uint64 value);
+
+ // optional int64 negative_int_value = 5;
+ inline bool has_negative_int_value() const;
+ inline void clear_negative_int_value();
+ static const int kNegativeIntValueFieldNumber = 5;
+ inline ::google::protobuf::int64 negative_int_value() const;
+ inline void set_negative_int_value(::google::protobuf::int64 value);
+
+ // optional double double_value = 6;
+ inline bool has_double_value() const;
+ inline void clear_double_value();
+ static const int kDoubleValueFieldNumber = 6;
+ inline double double_value() const;
+ inline void set_double_value(double value);
+
+ // optional bytes string_value = 7;
+ inline bool has_string_value() const;
+ inline void clear_string_value();
+ static const int kStringValueFieldNumber = 7;
+ inline const ::std::string& string_value() const;
+ inline void set_string_value(const ::std::string& value);
+ inline void set_string_value(const char* value);
+ inline void set_string_value(const void* value, size_t size);
+ inline ::std::string* mutable_string_value();
+ inline ::std::string* release_string_value();
+ inline void set_allocated_string_value(::std::string* string_value);
+
+ // optional string aggregate_value = 8;
+ inline bool has_aggregate_value() const;
+ inline void clear_aggregate_value();
+ static const int kAggregateValueFieldNumber = 8;
+ inline const ::std::string& aggregate_value() const;
+ inline void set_aggregate_value(const ::std::string& value);
+ inline void set_aggregate_value(const char* value);
+ inline void set_aggregate_value(const char* value, size_t size);
+ inline ::std::string* mutable_aggregate_value();
+ inline ::std::string* release_aggregate_value();
+ inline void set_allocated_aggregate_value(::std::string* aggregate_value);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.UninterpretedOption)
+ private:
+ inline void set_has_identifier_value();
+ inline void clear_has_identifier_value();
+ inline void set_has_positive_int_value();
+ inline void clear_has_positive_int_value();
+ inline void set_has_negative_int_value();
+ inline void clear_has_negative_int_value();
+ inline void set_has_double_value();
+ inline void clear_has_double_value();
+ inline void set_has_string_value();
+ inline void clear_has_string_value();
+ inline void set_has_aggregate_value();
+ inline void clear_has_aggregate_value();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption_NamePart > name_;
+ ::std::string* identifier_value_;
+ ::google::protobuf::uint64 positive_int_value_;
+ ::google::protobuf::int64 negative_int_value_;
+ double double_value_;
+ ::std::string* string_value_;
+ ::std::string* aggregate_value_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static UninterpretedOption* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT SourceCodeInfo_Location : public ::google::protobuf::Message {
+ public:
+ SourceCodeInfo_Location();
+ virtual ~SourceCodeInfo_Location();
+
+ SourceCodeInfo_Location(const SourceCodeInfo_Location& from);
+
+ inline SourceCodeInfo_Location& operator=(const SourceCodeInfo_Location& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const SourceCodeInfo_Location& default_instance();
+
+ void Swap(SourceCodeInfo_Location* other);
+
+ // implements Message ----------------------------------------------
+
+ SourceCodeInfo_Location* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const SourceCodeInfo_Location& from);
+ void MergeFrom(const SourceCodeInfo_Location& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated int32 path = 1 [packed = true];
+ inline int path_size() const;
+ inline void clear_path();
+ static const int kPathFieldNumber = 1;
+ inline ::google::protobuf::int32 path(int index) const;
+ inline void set_path(int index, ::google::protobuf::int32 value);
+ inline void add_path(::google::protobuf::int32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ path() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ mutable_path();
+
+ // repeated int32 span = 2 [packed = true];
+ inline int span_size() const;
+ inline void clear_span();
+ static const int kSpanFieldNumber = 2;
+ inline ::google::protobuf::int32 span(int index) const;
+ inline void set_span(int index, ::google::protobuf::int32 value);
+ inline void add_span(::google::protobuf::int32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ span() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ mutable_span();
+
+ // optional string leading_comments = 3;
+ inline bool has_leading_comments() const;
+ inline void clear_leading_comments();
+ static const int kLeadingCommentsFieldNumber = 3;
+ inline const ::std::string& leading_comments() const;
+ inline void set_leading_comments(const ::std::string& value);
+ inline void set_leading_comments(const char* value);
+ inline void set_leading_comments(const char* value, size_t size);
+ inline ::std::string* mutable_leading_comments();
+ inline ::std::string* release_leading_comments();
+ inline void set_allocated_leading_comments(::std::string* leading_comments);
+
+ // optional string trailing_comments = 4;
+ inline bool has_trailing_comments() const;
+ inline void clear_trailing_comments();
+ static const int kTrailingCommentsFieldNumber = 4;
+ inline const ::std::string& trailing_comments() const;
+ inline void set_trailing_comments(const ::std::string& value);
+ inline void set_trailing_comments(const char* value);
+ inline void set_trailing_comments(const char* value, size_t size);
+ inline ::std::string* mutable_trailing_comments();
+ inline ::std::string* release_trailing_comments();
+ inline void set_allocated_trailing_comments(::std::string* trailing_comments);
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.SourceCodeInfo.Location)
+ private:
+ inline void set_has_leading_comments();
+ inline void clear_has_leading_comments();
+ inline void set_has_trailing_comments();
+ inline void clear_has_trailing_comments();
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::int32 > path_;
+ mutable int _path_cached_byte_size_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::int32 > span_;
+ mutable int _span_cached_byte_size_;
+ ::std::string* leading_comments_;
+ ::std::string* trailing_comments_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static SourceCodeInfo_Location* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class LIBPROTOBUF_EXPORT SourceCodeInfo : public ::google::protobuf::Message {
+ public:
+ SourceCodeInfo();
+ virtual ~SourceCodeInfo();
+
+ SourceCodeInfo(const SourceCodeInfo& from);
+
+ inline SourceCodeInfo& operator=(const SourceCodeInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ::google::protobuf::Descriptor* descriptor();
+ static const SourceCodeInfo& default_instance();
+
+ void Swap(SourceCodeInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ SourceCodeInfo* New() const;
+ void CopyFrom(const ::google::protobuf::Message& from);
+ void MergeFrom(const ::google::protobuf::Message& from);
+ void CopyFrom(const SourceCodeInfo& from);
+ void MergeFrom(const SourceCodeInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ ::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::google::protobuf::Metadata GetMetadata() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef SourceCodeInfo_Location Location;
+
+ // accessors -------------------------------------------------------
+
+ // repeated .google.protobuf.SourceCodeInfo.Location location = 1;
+ inline int location_size() const;
+ inline void clear_location();
+ static const int kLocationFieldNumber = 1;
+ inline const ::google::protobuf::SourceCodeInfo_Location& location(int index) const;
+ inline ::google::protobuf::SourceCodeInfo_Location* mutable_location(int index);
+ inline ::google::protobuf::SourceCodeInfo_Location* add_location();
+ inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::SourceCodeInfo_Location >&
+ location() const;
+ inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::SourceCodeInfo_Location >*
+ mutable_location();
+
+ // @@protoc_insertion_point(class_scope:google.protobuf.SourceCodeInfo)
+ private:
+
+ ::google::protobuf::UnknownFieldSet _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::google::protobuf::SourceCodeInfo_Location > location_;
+ friend void LIBPROTOBUF_EXPORT protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_AssignDesc_google_2fprotobuf_2fdescriptor_2eproto();
+ friend void protobuf_ShutdownFile_google_2fprotobuf_2fdescriptor_2eproto();
+
+ void InitAsDefaultInstance();
+ static SourceCodeInfo* default_instance_;
+};
+// ===================================================================
+
+
+// ===================================================================
+
+// FileDescriptorSet
+
+// repeated .google.protobuf.FileDescriptorProto file = 1;
+inline int FileDescriptorSet::file_size() const {
+ return file_.size();
+}
+inline void FileDescriptorSet::clear_file() {
+ file_.Clear();
+}
+inline const ::google::protobuf::FileDescriptorProto& FileDescriptorSet::file(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorSet.file)
+ return file_.Get(index);
+}
+inline ::google::protobuf::FileDescriptorProto* FileDescriptorSet::mutable_file(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorSet.file)
+ return file_.Mutable(index);
+}
+inline ::google::protobuf::FileDescriptorProto* FileDescriptorSet::add_file() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorSet.file)
+ return file_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FileDescriptorProto >&
+FileDescriptorSet::file() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorSet.file)
+ return file_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FileDescriptorProto >*
+FileDescriptorSet::mutable_file() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorSet.file)
+ return &file_;
+}
+
+// -------------------------------------------------------------------
+
+// FileDescriptorProto
+
+// optional string name = 1;
+inline bool FileDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FileDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FileDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FileDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& FileDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.name)
+ return *name_;
+}
+inline void FileDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileDescriptorProto.name)
+}
+inline void FileDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FileDescriptorProto.name)
+}
+inline void FileDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FileDescriptorProto.name)
+}
+inline ::std::string* FileDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* FileDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FileDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileDescriptorProto.name)
+}
+
+// optional string package = 2;
+inline bool FileDescriptorProto::has_package() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FileDescriptorProto::set_has_package() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FileDescriptorProto::clear_has_package() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FileDescriptorProto::clear_package() {
+ if (package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ package_->clear();
+ }
+ clear_has_package();
+}
+inline const ::std::string& FileDescriptorProto::package() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.package)
+ return *package_;
+}
+inline void FileDescriptorProto::set_package(const ::std::string& value) {
+ set_has_package();
+ if (package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ package_ = new ::std::string;
+ }
+ package_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileDescriptorProto.package)
+}
+inline void FileDescriptorProto::set_package(const char* value) {
+ set_has_package();
+ if (package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ package_ = new ::std::string;
+ }
+ package_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FileDescriptorProto.package)
+}
+inline void FileDescriptorProto::set_package(const char* value, size_t size) {
+ set_has_package();
+ if (package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ package_ = new ::std::string;
+ }
+ package_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FileDescriptorProto.package)
+}
+inline ::std::string* FileDescriptorProto::mutable_package() {
+ set_has_package();
+ if (package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ package_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.package)
+ return package_;
+}
+inline ::std::string* FileDescriptorProto::release_package() {
+ clear_has_package();
+ if (package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = package_;
+ package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FileDescriptorProto::set_allocated_package(::std::string* package) {
+ if (package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete package_;
+ }
+ if (package) {
+ set_has_package();
+ package_ = package;
+ } else {
+ clear_has_package();
+ package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileDescriptorProto.package)
+}
+
+// repeated string dependency = 3;
+inline int FileDescriptorProto::dependency_size() const {
+ return dependency_.size();
+}
+inline void FileDescriptorProto::clear_dependency() {
+ dependency_.Clear();
+}
+inline const ::std::string& FileDescriptorProto::dependency(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.dependency)
+ return dependency_.Get(index);
+}
+inline ::std::string* FileDescriptorProto::mutable_dependency(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.dependency)
+ return dependency_.Mutable(index);
+}
+inline void FileDescriptorProto::set_dependency(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:google.protobuf.FileDescriptorProto.dependency)
+ dependency_.Mutable(index)->assign(value);
+}
+inline void FileDescriptorProto::set_dependency(int index, const char* value) {
+ dependency_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FileDescriptorProto.dependency)
+}
+inline void FileDescriptorProto::set_dependency(int index, const char* value, size_t size) {
+ dependency_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FileDescriptorProto.dependency)
+}
+inline ::std::string* FileDescriptorProto::add_dependency() {
+ return dependency_.Add();
+}
+inline void FileDescriptorProto::add_dependency(const ::std::string& value) {
+ dependency_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.dependency)
+}
+inline void FileDescriptorProto::add_dependency(const char* value) {
+ dependency_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:google.protobuf.FileDescriptorProto.dependency)
+}
+inline void FileDescriptorProto::add_dependency(const char* value, size_t size) {
+ dependency_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:google.protobuf.FileDescriptorProto.dependency)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+FileDescriptorProto::dependency() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.dependency)
+ return dependency_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+FileDescriptorProto::mutable_dependency() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.dependency)
+ return &dependency_;
+}
+
+// repeated int32 public_dependency = 10;
+inline int FileDescriptorProto::public_dependency_size() const {
+ return public_dependency_.size();
+}
+inline void FileDescriptorProto::clear_public_dependency() {
+ public_dependency_.Clear();
+}
+inline ::google::protobuf::int32 FileDescriptorProto::public_dependency(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.public_dependency)
+ return public_dependency_.Get(index);
+}
+inline void FileDescriptorProto::set_public_dependency(int index, ::google::protobuf::int32 value) {
+ public_dependency_.Set(index, value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileDescriptorProto.public_dependency)
+}
+inline void FileDescriptorProto::add_public_dependency(::google::protobuf::int32 value) {
+ public_dependency_.Add(value);
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.public_dependency)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+FileDescriptorProto::public_dependency() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.public_dependency)
+ return public_dependency_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+FileDescriptorProto::mutable_public_dependency() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.public_dependency)
+ return &public_dependency_;
+}
+
+// repeated int32 weak_dependency = 11;
+inline int FileDescriptorProto::weak_dependency_size() const {
+ return weak_dependency_.size();
+}
+inline void FileDescriptorProto::clear_weak_dependency() {
+ weak_dependency_.Clear();
+}
+inline ::google::protobuf::int32 FileDescriptorProto::weak_dependency(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.weak_dependency)
+ return weak_dependency_.Get(index);
+}
+inline void FileDescriptorProto::set_weak_dependency(int index, ::google::protobuf::int32 value) {
+ weak_dependency_.Set(index, value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileDescriptorProto.weak_dependency)
+}
+inline void FileDescriptorProto::add_weak_dependency(::google::protobuf::int32 value) {
+ weak_dependency_.Add(value);
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.weak_dependency)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+FileDescriptorProto::weak_dependency() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.weak_dependency)
+ return weak_dependency_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+FileDescriptorProto::mutable_weak_dependency() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.weak_dependency)
+ return &weak_dependency_;
+}
+
+// repeated .google.protobuf.DescriptorProto message_type = 4;
+inline int FileDescriptorProto::message_type_size() const {
+ return message_type_.size();
+}
+inline void FileDescriptorProto::clear_message_type() {
+ message_type_.Clear();
+}
+inline const ::google::protobuf::DescriptorProto& FileDescriptorProto::message_type(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.message_type)
+ return message_type_.Get(index);
+}
+inline ::google::protobuf::DescriptorProto* FileDescriptorProto::mutable_message_type(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.message_type)
+ return message_type_.Mutable(index);
+}
+inline ::google::protobuf::DescriptorProto* FileDescriptorProto::add_message_type() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.message_type)
+ return message_type_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >&
+FileDescriptorProto::message_type() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.message_type)
+ return message_type_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >*
+FileDescriptorProto::mutable_message_type() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.message_type)
+ return &message_type_;
+}
+
+// repeated .google.protobuf.EnumDescriptorProto enum_type = 5;
+inline int FileDescriptorProto::enum_type_size() const {
+ return enum_type_.size();
+}
+inline void FileDescriptorProto::clear_enum_type() {
+ enum_type_.Clear();
+}
+inline const ::google::protobuf::EnumDescriptorProto& FileDescriptorProto::enum_type(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.enum_type)
+ return enum_type_.Get(index);
+}
+inline ::google::protobuf::EnumDescriptorProto* FileDescriptorProto::mutable_enum_type(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.enum_type)
+ return enum_type_.Mutable(index);
+}
+inline ::google::protobuf::EnumDescriptorProto* FileDescriptorProto::add_enum_type() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.enum_type)
+ return enum_type_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >&
+FileDescriptorProto::enum_type() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.enum_type)
+ return enum_type_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >*
+FileDescriptorProto::mutable_enum_type() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.enum_type)
+ return &enum_type_;
+}
+
+// repeated .google.protobuf.ServiceDescriptorProto service = 6;
+inline int FileDescriptorProto::service_size() const {
+ return service_.size();
+}
+inline void FileDescriptorProto::clear_service() {
+ service_.Clear();
+}
+inline const ::google::protobuf::ServiceDescriptorProto& FileDescriptorProto::service(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.service)
+ return service_.Get(index);
+}
+inline ::google::protobuf::ServiceDescriptorProto* FileDescriptorProto::mutable_service(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.service)
+ return service_.Mutable(index);
+}
+inline ::google::protobuf::ServiceDescriptorProto* FileDescriptorProto::add_service() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.service)
+ return service_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::ServiceDescriptorProto >&
+FileDescriptorProto::service() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.service)
+ return service_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::ServiceDescriptorProto >*
+FileDescriptorProto::mutable_service() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.service)
+ return &service_;
+}
+
+// repeated .google.protobuf.FieldDescriptorProto extension = 7;
+inline int FileDescriptorProto::extension_size() const {
+ return extension_.size();
+}
+inline void FileDescriptorProto::clear_extension() {
+ extension_.Clear();
+}
+inline const ::google::protobuf::FieldDescriptorProto& FileDescriptorProto::extension(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.extension)
+ return extension_.Get(index);
+}
+inline ::google::protobuf::FieldDescriptorProto* FileDescriptorProto::mutable_extension(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.extension)
+ return extension_.Mutable(index);
+}
+inline ::google::protobuf::FieldDescriptorProto* FileDescriptorProto::add_extension() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FileDescriptorProto.extension)
+ return extension_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >&
+FileDescriptorProto::extension() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileDescriptorProto.extension)
+ return extension_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >*
+FileDescriptorProto::mutable_extension() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileDescriptorProto.extension)
+ return &extension_;
+}
+
+// optional .google.protobuf.FileOptions options = 8;
+inline bool FileDescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void FileDescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void FileDescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void FileDescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::FileOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::FileOptions& FileDescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::FileOptions* FileDescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::FileOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::FileOptions* FileDescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::FileOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void FileDescriptorProto::set_allocated_options(::google::protobuf::FileOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileDescriptorProto.options)
+}
+
+// optional .google.protobuf.SourceCodeInfo source_code_info = 9;
+inline bool FileDescriptorProto::has_source_code_info() const {
+ return (_has_bits_[0] & 0x00000400u) != 0;
+}
+inline void FileDescriptorProto::set_has_source_code_info() {
+ _has_bits_[0] |= 0x00000400u;
+}
+inline void FileDescriptorProto::clear_has_source_code_info() {
+ _has_bits_[0] &= ~0x00000400u;
+}
+inline void FileDescriptorProto::clear_source_code_info() {
+ if (source_code_info_ != NULL) source_code_info_->::google::protobuf::SourceCodeInfo::Clear();
+ clear_has_source_code_info();
+}
+inline const ::google::protobuf::SourceCodeInfo& FileDescriptorProto::source_code_info() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileDescriptorProto.source_code_info)
+ return source_code_info_ != NULL ? *source_code_info_ : *default_instance_->source_code_info_;
+}
+inline ::google::protobuf::SourceCodeInfo* FileDescriptorProto::mutable_source_code_info() {
+ set_has_source_code_info();
+ if (source_code_info_ == NULL) source_code_info_ = new ::google::protobuf::SourceCodeInfo;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileDescriptorProto.source_code_info)
+ return source_code_info_;
+}
+inline ::google::protobuf::SourceCodeInfo* FileDescriptorProto::release_source_code_info() {
+ clear_has_source_code_info();
+ ::google::protobuf::SourceCodeInfo* temp = source_code_info_;
+ source_code_info_ = NULL;
+ return temp;
+}
+inline void FileDescriptorProto::set_allocated_source_code_info(::google::protobuf::SourceCodeInfo* source_code_info) {
+ delete source_code_info_;
+ source_code_info_ = source_code_info;
+ if (source_code_info) {
+ set_has_source_code_info();
+ } else {
+ clear_has_source_code_info();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileDescriptorProto.source_code_info)
+}
+
+// -------------------------------------------------------------------
+
+// DescriptorProto_ExtensionRange
+
+// optional int32 start = 1;
+inline bool DescriptorProto_ExtensionRange::has_start() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void DescriptorProto_ExtensionRange::set_has_start() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void DescriptorProto_ExtensionRange::clear_has_start() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void DescriptorProto_ExtensionRange::clear_start() {
+ start_ = 0;
+ clear_has_start();
+}
+inline ::google::protobuf::int32 DescriptorProto_ExtensionRange::start() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.ExtensionRange.start)
+ return start_;
+}
+inline void DescriptorProto_ExtensionRange::set_start(::google::protobuf::int32 value) {
+ set_has_start();
+ start_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.DescriptorProto.ExtensionRange.start)
+}
+
+// optional int32 end = 2;
+inline bool DescriptorProto_ExtensionRange::has_end() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void DescriptorProto_ExtensionRange::set_has_end() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void DescriptorProto_ExtensionRange::clear_has_end() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void DescriptorProto_ExtensionRange::clear_end() {
+ end_ = 0;
+ clear_has_end();
+}
+inline ::google::protobuf::int32 DescriptorProto_ExtensionRange::end() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.ExtensionRange.end)
+ return end_;
+}
+inline void DescriptorProto_ExtensionRange::set_end(::google::protobuf::int32 value) {
+ set_has_end();
+ end_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.DescriptorProto.ExtensionRange.end)
+}
+
+// -------------------------------------------------------------------
+
+// DescriptorProto
+
+// optional string name = 1;
+inline bool DescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void DescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void DescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void DescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& DescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.name)
+ return *name_;
+}
+inline void DescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.DescriptorProto.name)
+}
+inline void DescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.DescriptorProto.name)
+}
+inline void DescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.DescriptorProto.name)
+}
+inline ::std::string* DescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.name)
+ return name_;
+}
+inline ::std::string* DescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void DescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.DescriptorProto.name)
+}
+
+// repeated .google.protobuf.FieldDescriptorProto field = 2;
+inline int DescriptorProto::field_size() const {
+ return field_.size();
+}
+inline void DescriptorProto::clear_field() {
+ field_.Clear();
+}
+inline const ::google::protobuf::FieldDescriptorProto& DescriptorProto::field(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.field)
+ return field_.Get(index);
+}
+inline ::google::protobuf::FieldDescriptorProto* DescriptorProto::mutable_field(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.field)
+ return field_.Mutable(index);
+}
+inline ::google::protobuf::FieldDescriptorProto* DescriptorProto::add_field() {
+ // @@protoc_insertion_point(field_add:google.protobuf.DescriptorProto.field)
+ return field_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >&
+DescriptorProto::field() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.DescriptorProto.field)
+ return field_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >*
+DescriptorProto::mutable_field() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.DescriptorProto.field)
+ return &field_;
+}
+
+// repeated .google.protobuf.FieldDescriptorProto extension = 6;
+inline int DescriptorProto::extension_size() const {
+ return extension_.size();
+}
+inline void DescriptorProto::clear_extension() {
+ extension_.Clear();
+}
+inline const ::google::protobuf::FieldDescriptorProto& DescriptorProto::extension(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.extension)
+ return extension_.Get(index);
+}
+inline ::google::protobuf::FieldDescriptorProto* DescriptorProto::mutable_extension(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.extension)
+ return extension_.Mutable(index);
+}
+inline ::google::protobuf::FieldDescriptorProto* DescriptorProto::add_extension() {
+ // @@protoc_insertion_point(field_add:google.protobuf.DescriptorProto.extension)
+ return extension_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >&
+DescriptorProto::extension() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.DescriptorProto.extension)
+ return extension_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::FieldDescriptorProto >*
+DescriptorProto::mutable_extension() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.DescriptorProto.extension)
+ return &extension_;
+}
+
+// repeated .google.protobuf.DescriptorProto nested_type = 3;
+inline int DescriptorProto::nested_type_size() const {
+ return nested_type_.size();
+}
+inline void DescriptorProto::clear_nested_type() {
+ nested_type_.Clear();
+}
+inline const ::google::protobuf::DescriptorProto& DescriptorProto::nested_type(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.nested_type)
+ return nested_type_.Get(index);
+}
+inline ::google::protobuf::DescriptorProto* DescriptorProto::mutable_nested_type(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.nested_type)
+ return nested_type_.Mutable(index);
+}
+inline ::google::protobuf::DescriptorProto* DescriptorProto::add_nested_type() {
+ // @@protoc_insertion_point(field_add:google.protobuf.DescriptorProto.nested_type)
+ return nested_type_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >&
+DescriptorProto::nested_type() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.DescriptorProto.nested_type)
+ return nested_type_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto >*
+DescriptorProto::mutable_nested_type() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.DescriptorProto.nested_type)
+ return &nested_type_;
+}
+
+// repeated .google.protobuf.EnumDescriptorProto enum_type = 4;
+inline int DescriptorProto::enum_type_size() const {
+ return enum_type_.size();
+}
+inline void DescriptorProto::clear_enum_type() {
+ enum_type_.Clear();
+}
+inline const ::google::protobuf::EnumDescriptorProto& DescriptorProto::enum_type(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.enum_type)
+ return enum_type_.Get(index);
+}
+inline ::google::protobuf::EnumDescriptorProto* DescriptorProto::mutable_enum_type(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.enum_type)
+ return enum_type_.Mutable(index);
+}
+inline ::google::protobuf::EnumDescriptorProto* DescriptorProto::add_enum_type() {
+ // @@protoc_insertion_point(field_add:google.protobuf.DescriptorProto.enum_type)
+ return enum_type_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >&
+DescriptorProto::enum_type() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.DescriptorProto.enum_type)
+ return enum_type_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumDescriptorProto >*
+DescriptorProto::mutable_enum_type() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.DescriptorProto.enum_type)
+ return &enum_type_;
+}
+
+// repeated .google.protobuf.DescriptorProto.ExtensionRange extension_range = 5;
+inline int DescriptorProto::extension_range_size() const {
+ return extension_range_.size();
+}
+inline void DescriptorProto::clear_extension_range() {
+ extension_range_.Clear();
+}
+inline const ::google::protobuf::DescriptorProto_ExtensionRange& DescriptorProto::extension_range(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.extension_range)
+ return extension_range_.Get(index);
+}
+inline ::google::protobuf::DescriptorProto_ExtensionRange* DescriptorProto::mutable_extension_range(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.extension_range)
+ return extension_range_.Mutable(index);
+}
+inline ::google::protobuf::DescriptorProto_ExtensionRange* DescriptorProto::add_extension_range() {
+ // @@protoc_insertion_point(field_add:google.protobuf.DescriptorProto.extension_range)
+ return extension_range_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto_ExtensionRange >&
+DescriptorProto::extension_range() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.DescriptorProto.extension_range)
+ return extension_range_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::DescriptorProto_ExtensionRange >*
+DescriptorProto::mutable_extension_range() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.DescriptorProto.extension_range)
+ return &extension_range_;
+}
+
+// repeated .google.protobuf.OneofDescriptorProto oneof_decl = 8;
+inline int DescriptorProto::oneof_decl_size() const {
+ return oneof_decl_.size();
+}
+inline void DescriptorProto::clear_oneof_decl() {
+ oneof_decl_.Clear();
+}
+inline const ::google::protobuf::OneofDescriptorProto& DescriptorProto::oneof_decl(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.oneof_decl)
+ return oneof_decl_.Get(index);
+}
+inline ::google::protobuf::OneofDescriptorProto* DescriptorProto::mutable_oneof_decl(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.oneof_decl)
+ return oneof_decl_.Mutable(index);
+}
+inline ::google::protobuf::OneofDescriptorProto* DescriptorProto::add_oneof_decl() {
+ // @@protoc_insertion_point(field_add:google.protobuf.DescriptorProto.oneof_decl)
+ return oneof_decl_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::OneofDescriptorProto >&
+DescriptorProto::oneof_decl() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.DescriptorProto.oneof_decl)
+ return oneof_decl_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::OneofDescriptorProto >*
+DescriptorProto::mutable_oneof_decl() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.DescriptorProto.oneof_decl)
+ return &oneof_decl_;
+}
+
+// optional .google.protobuf.MessageOptions options = 7;
+inline bool DescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void DescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void DescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void DescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::MessageOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::MessageOptions& DescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.DescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::MessageOptions* DescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::MessageOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.DescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::MessageOptions* DescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::MessageOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void DescriptorProto::set_allocated_options(::google::protobuf::MessageOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.DescriptorProto.options)
+}
+
+// -------------------------------------------------------------------
+
+// FieldDescriptorProto
+
+// optional string name = 1;
+inline bool FieldDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FieldDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FieldDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FieldDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& FieldDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.name)
+ return *name_;
+}
+inline void FieldDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.name)
+}
+inline void FieldDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FieldDescriptorProto.name)
+}
+inline void FieldDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FieldDescriptorProto.name)
+}
+inline ::std::string* FieldDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* FieldDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FieldDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FieldDescriptorProto.name)
+}
+
+// optional int32 number = 3;
+inline bool FieldDescriptorProto::has_number() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FieldDescriptorProto::set_has_number() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FieldDescriptorProto::clear_has_number() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FieldDescriptorProto::clear_number() {
+ number_ = 0;
+ clear_has_number();
+}
+inline ::google::protobuf::int32 FieldDescriptorProto::number() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.number)
+ return number_;
+}
+inline void FieldDescriptorProto::set_number(::google::protobuf::int32 value) {
+ set_has_number();
+ number_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.number)
+}
+
+// optional .google.protobuf.FieldDescriptorProto.Label label = 4;
+inline bool FieldDescriptorProto::has_label() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FieldDescriptorProto::set_has_label() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FieldDescriptorProto::clear_has_label() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FieldDescriptorProto::clear_label() {
+ label_ = 1;
+ clear_has_label();
+}
+inline ::google::protobuf::FieldDescriptorProto_Label FieldDescriptorProto::label() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.label)
+ return static_cast< ::google::protobuf::FieldDescriptorProto_Label >(label_);
+}
+inline void FieldDescriptorProto::set_label(::google::protobuf::FieldDescriptorProto_Label value) {
+ assert(::google::protobuf::FieldDescriptorProto_Label_IsValid(value));
+ set_has_label();
+ label_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.label)
+}
+
+// optional .google.protobuf.FieldDescriptorProto.Type type = 5;
+inline bool FieldDescriptorProto::has_type() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void FieldDescriptorProto::set_has_type() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void FieldDescriptorProto::clear_has_type() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void FieldDescriptorProto::clear_type() {
+ type_ = 1;
+ clear_has_type();
+}
+inline ::google::protobuf::FieldDescriptorProto_Type FieldDescriptorProto::type() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.type)
+ return static_cast< ::google::protobuf::FieldDescriptorProto_Type >(type_);
+}
+inline void FieldDescriptorProto::set_type(::google::protobuf::FieldDescriptorProto_Type value) {
+ assert(::google::protobuf::FieldDescriptorProto_Type_IsValid(value));
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.type)
+}
+
+// optional string type_name = 6;
+inline bool FieldDescriptorProto::has_type_name() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void FieldDescriptorProto::set_has_type_name() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void FieldDescriptorProto::clear_has_type_name() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void FieldDescriptorProto::clear_type_name() {
+ if (type_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ type_name_->clear();
+ }
+ clear_has_type_name();
+}
+inline const ::std::string& FieldDescriptorProto::type_name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.type_name)
+ return *type_name_;
+}
+inline void FieldDescriptorProto::set_type_name(const ::std::string& value) {
+ set_has_type_name();
+ if (type_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ type_name_ = new ::std::string;
+ }
+ type_name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.type_name)
+}
+inline void FieldDescriptorProto::set_type_name(const char* value) {
+ set_has_type_name();
+ if (type_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ type_name_ = new ::std::string;
+ }
+ type_name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FieldDescriptorProto.type_name)
+}
+inline void FieldDescriptorProto::set_type_name(const char* value, size_t size) {
+ set_has_type_name();
+ if (type_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ type_name_ = new ::std::string;
+ }
+ type_name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FieldDescriptorProto.type_name)
+}
+inline ::std::string* FieldDescriptorProto::mutable_type_name() {
+ set_has_type_name();
+ if (type_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ type_name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldDescriptorProto.type_name)
+ return type_name_;
+}
+inline ::std::string* FieldDescriptorProto::release_type_name() {
+ clear_has_type_name();
+ if (type_name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = type_name_;
+ type_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FieldDescriptorProto::set_allocated_type_name(::std::string* type_name) {
+ if (type_name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete type_name_;
+ }
+ if (type_name) {
+ set_has_type_name();
+ type_name_ = type_name;
+ } else {
+ clear_has_type_name();
+ type_name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FieldDescriptorProto.type_name)
+}
+
+// optional string extendee = 2;
+inline bool FieldDescriptorProto::has_extendee() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void FieldDescriptorProto::set_has_extendee() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void FieldDescriptorProto::clear_has_extendee() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void FieldDescriptorProto::clear_extendee() {
+ if (extendee_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ extendee_->clear();
+ }
+ clear_has_extendee();
+}
+inline const ::std::string& FieldDescriptorProto::extendee() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.extendee)
+ return *extendee_;
+}
+inline void FieldDescriptorProto::set_extendee(const ::std::string& value) {
+ set_has_extendee();
+ if (extendee_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ extendee_ = new ::std::string;
+ }
+ extendee_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.extendee)
+}
+inline void FieldDescriptorProto::set_extendee(const char* value) {
+ set_has_extendee();
+ if (extendee_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ extendee_ = new ::std::string;
+ }
+ extendee_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FieldDescriptorProto.extendee)
+}
+inline void FieldDescriptorProto::set_extendee(const char* value, size_t size) {
+ set_has_extendee();
+ if (extendee_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ extendee_ = new ::std::string;
+ }
+ extendee_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FieldDescriptorProto.extendee)
+}
+inline ::std::string* FieldDescriptorProto::mutable_extendee() {
+ set_has_extendee();
+ if (extendee_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ extendee_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldDescriptorProto.extendee)
+ return extendee_;
+}
+inline ::std::string* FieldDescriptorProto::release_extendee() {
+ clear_has_extendee();
+ if (extendee_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = extendee_;
+ extendee_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FieldDescriptorProto::set_allocated_extendee(::std::string* extendee) {
+ if (extendee_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete extendee_;
+ }
+ if (extendee) {
+ set_has_extendee();
+ extendee_ = extendee;
+ } else {
+ clear_has_extendee();
+ extendee_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FieldDescriptorProto.extendee)
+}
+
+// optional string default_value = 7;
+inline bool FieldDescriptorProto::has_default_value() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void FieldDescriptorProto::set_has_default_value() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void FieldDescriptorProto::clear_has_default_value() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void FieldDescriptorProto::clear_default_value() {
+ if (default_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ default_value_->clear();
+ }
+ clear_has_default_value();
+}
+inline const ::std::string& FieldDescriptorProto::default_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.default_value)
+ return *default_value_;
+}
+inline void FieldDescriptorProto::set_default_value(const ::std::string& value) {
+ set_has_default_value();
+ if (default_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ default_value_ = new ::std::string;
+ }
+ default_value_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.default_value)
+}
+inline void FieldDescriptorProto::set_default_value(const char* value) {
+ set_has_default_value();
+ if (default_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ default_value_ = new ::std::string;
+ }
+ default_value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FieldDescriptorProto.default_value)
+}
+inline void FieldDescriptorProto::set_default_value(const char* value, size_t size) {
+ set_has_default_value();
+ if (default_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ default_value_ = new ::std::string;
+ }
+ default_value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FieldDescriptorProto.default_value)
+}
+inline ::std::string* FieldDescriptorProto::mutable_default_value() {
+ set_has_default_value();
+ if (default_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ default_value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldDescriptorProto.default_value)
+ return default_value_;
+}
+inline ::std::string* FieldDescriptorProto::release_default_value() {
+ clear_has_default_value();
+ if (default_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = default_value_;
+ default_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FieldDescriptorProto::set_allocated_default_value(::std::string* default_value) {
+ if (default_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete default_value_;
+ }
+ if (default_value) {
+ set_has_default_value();
+ default_value_ = default_value;
+ } else {
+ clear_has_default_value();
+ default_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FieldDescriptorProto.default_value)
+}
+
+// optional int32 oneof_index = 9;
+inline bool FieldDescriptorProto::has_oneof_index() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void FieldDescriptorProto::set_has_oneof_index() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void FieldDescriptorProto::clear_has_oneof_index() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void FieldDescriptorProto::clear_oneof_index() {
+ oneof_index_ = 0;
+ clear_has_oneof_index();
+}
+inline ::google::protobuf::int32 FieldDescriptorProto::oneof_index() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.oneof_index)
+ return oneof_index_;
+}
+inline void FieldDescriptorProto::set_oneof_index(::google::protobuf::int32 value) {
+ set_has_oneof_index();
+ oneof_index_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldDescriptorProto.oneof_index)
+}
+
+// optional .google.protobuf.FieldOptions options = 8;
+inline bool FieldDescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void FieldDescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void FieldDescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void FieldDescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::FieldOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::FieldOptions& FieldDescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldDescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::FieldOptions* FieldDescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::FieldOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldDescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::FieldOptions* FieldDescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::FieldOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void FieldDescriptorProto::set_allocated_options(::google::protobuf::FieldOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FieldDescriptorProto.options)
+}
+
+// -------------------------------------------------------------------
+
+// OneofDescriptorProto
+
+// optional string name = 1;
+inline bool OneofDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void OneofDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void OneofDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void OneofDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& OneofDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.OneofDescriptorProto.name)
+ return *name_;
+}
+inline void OneofDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.OneofDescriptorProto.name)
+}
+inline void OneofDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.OneofDescriptorProto.name)
+}
+inline void OneofDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.OneofDescriptorProto.name)
+}
+inline ::std::string* OneofDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.OneofDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* OneofDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void OneofDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.OneofDescriptorProto.name)
+}
+
+// -------------------------------------------------------------------
+
+// EnumDescriptorProto
+
+// optional string name = 1;
+inline bool EnumDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void EnumDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void EnumDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void EnumDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& EnumDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumDescriptorProto.name)
+ return *name_;
+}
+inline void EnumDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.EnumDescriptorProto.name)
+}
+inline void EnumDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.EnumDescriptorProto.name)
+}
+inline void EnumDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.EnumDescriptorProto.name)
+}
+inline ::std::string* EnumDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* EnumDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void EnumDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.EnumDescriptorProto.name)
+}
+
+// repeated .google.protobuf.EnumValueDescriptorProto value = 2;
+inline int EnumDescriptorProto::value_size() const {
+ return value_.size();
+}
+inline void EnumDescriptorProto::clear_value() {
+ value_.Clear();
+}
+inline const ::google::protobuf::EnumValueDescriptorProto& EnumDescriptorProto::value(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumDescriptorProto.value)
+ return value_.Get(index);
+}
+inline ::google::protobuf::EnumValueDescriptorProto* EnumDescriptorProto::mutable_value(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumDescriptorProto.value)
+ return value_.Mutable(index);
+}
+inline ::google::protobuf::EnumValueDescriptorProto* EnumDescriptorProto::add_value() {
+ // @@protoc_insertion_point(field_add:google.protobuf.EnumDescriptorProto.value)
+ return value_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumValueDescriptorProto >&
+EnumDescriptorProto::value() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.EnumDescriptorProto.value)
+ return value_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::EnumValueDescriptorProto >*
+EnumDescriptorProto::mutable_value() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.EnumDescriptorProto.value)
+ return &value_;
+}
+
+// optional .google.protobuf.EnumOptions options = 3;
+inline bool EnumDescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void EnumDescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void EnumDescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void EnumDescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::EnumOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::EnumOptions& EnumDescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumDescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::EnumOptions* EnumDescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::EnumOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumDescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::EnumOptions* EnumDescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::EnumOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void EnumDescriptorProto::set_allocated_options(::google::protobuf::EnumOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.EnumDescriptorProto.options)
+}
+
+// -------------------------------------------------------------------
+
+// EnumValueDescriptorProto
+
+// optional string name = 1;
+inline bool EnumValueDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void EnumValueDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void EnumValueDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void EnumValueDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& EnumValueDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumValueDescriptorProto.name)
+ return *name_;
+}
+inline void EnumValueDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.EnumValueDescriptorProto.name)
+}
+inline void EnumValueDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.EnumValueDescriptorProto.name)
+}
+inline void EnumValueDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.EnumValueDescriptorProto.name)
+}
+inline ::std::string* EnumValueDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumValueDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* EnumValueDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void EnumValueDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.EnumValueDescriptorProto.name)
+}
+
+// optional int32 number = 2;
+inline bool EnumValueDescriptorProto::has_number() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void EnumValueDescriptorProto::set_has_number() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void EnumValueDescriptorProto::clear_has_number() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void EnumValueDescriptorProto::clear_number() {
+ number_ = 0;
+ clear_has_number();
+}
+inline ::google::protobuf::int32 EnumValueDescriptorProto::number() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumValueDescriptorProto.number)
+ return number_;
+}
+inline void EnumValueDescriptorProto::set_number(::google::protobuf::int32 value) {
+ set_has_number();
+ number_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.EnumValueDescriptorProto.number)
+}
+
+// optional .google.protobuf.EnumValueOptions options = 3;
+inline bool EnumValueDescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void EnumValueDescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void EnumValueDescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void EnumValueDescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::EnumValueOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::EnumValueOptions& EnumValueDescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumValueDescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::EnumValueOptions* EnumValueDescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::EnumValueOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumValueDescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::EnumValueOptions* EnumValueDescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::EnumValueOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void EnumValueDescriptorProto::set_allocated_options(::google::protobuf::EnumValueOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.EnumValueDescriptorProto.options)
+}
+
+// -------------------------------------------------------------------
+
+// ServiceDescriptorProto
+
+// optional string name = 1;
+inline bool ServiceDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ServiceDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ServiceDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ServiceDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& ServiceDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.ServiceDescriptorProto.name)
+ return *name_;
+}
+inline void ServiceDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.ServiceDescriptorProto.name)
+}
+inline void ServiceDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.ServiceDescriptorProto.name)
+}
+inline void ServiceDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.ServiceDescriptorProto.name)
+}
+inline ::std::string* ServiceDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.ServiceDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* ServiceDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ServiceDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.ServiceDescriptorProto.name)
+}
+
+// repeated .google.protobuf.MethodDescriptorProto method = 2;
+inline int ServiceDescriptorProto::method_size() const {
+ return method_.size();
+}
+inline void ServiceDescriptorProto::clear_method() {
+ method_.Clear();
+}
+inline const ::google::protobuf::MethodDescriptorProto& ServiceDescriptorProto::method(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.ServiceDescriptorProto.method)
+ return method_.Get(index);
+}
+inline ::google::protobuf::MethodDescriptorProto* ServiceDescriptorProto::mutable_method(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.ServiceDescriptorProto.method)
+ return method_.Mutable(index);
+}
+inline ::google::protobuf::MethodDescriptorProto* ServiceDescriptorProto::add_method() {
+ // @@protoc_insertion_point(field_add:google.protobuf.ServiceDescriptorProto.method)
+ return method_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::MethodDescriptorProto >&
+ServiceDescriptorProto::method() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.ServiceDescriptorProto.method)
+ return method_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::MethodDescriptorProto >*
+ServiceDescriptorProto::mutable_method() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.ServiceDescriptorProto.method)
+ return &method_;
+}
+
+// optional .google.protobuf.ServiceOptions options = 3;
+inline bool ServiceDescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ServiceDescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ServiceDescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ServiceDescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::ServiceOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::ServiceOptions& ServiceDescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.ServiceDescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::ServiceOptions* ServiceDescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::ServiceOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.ServiceDescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::ServiceOptions* ServiceDescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::ServiceOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void ServiceDescriptorProto::set_allocated_options(::google::protobuf::ServiceOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.ServiceDescriptorProto.options)
+}
+
+// -------------------------------------------------------------------
+
+// MethodDescriptorProto
+
+// optional string name = 1;
+inline bool MethodDescriptorProto::has_name() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void MethodDescriptorProto::set_has_name() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void MethodDescriptorProto::clear_has_name() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void MethodDescriptorProto::clear_name() {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_->clear();
+ }
+ clear_has_name();
+}
+inline const ::std::string& MethodDescriptorProto::name() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MethodDescriptorProto.name)
+ return *name_;
+}
+inline void MethodDescriptorProto::set_name(const ::std::string& value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.MethodDescriptorProto.name)
+}
+inline void MethodDescriptorProto::set_name(const char* value) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.MethodDescriptorProto.name)
+}
+inline void MethodDescriptorProto::set_name(const char* value, size_t size) {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ name_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.MethodDescriptorProto.name)
+}
+inline ::std::string* MethodDescriptorProto::mutable_name() {
+ set_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.MethodDescriptorProto.name)
+ return name_;
+}
+inline ::std::string* MethodDescriptorProto::release_name() {
+ clear_has_name();
+ if (name_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_;
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void MethodDescriptorProto::set_allocated_name(::std::string* name) {
+ if (name_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_;
+ }
+ if (name) {
+ set_has_name();
+ name_ = name;
+ } else {
+ clear_has_name();
+ name_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.MethodDescriptorProto.name)
+}
+
+// optional string input_type = 2;
+inline bool MethodDescriptorProto::has_input_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void MethodDescriptorProto::set_has_input_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void MethodDescriptorProto::clear_has_input_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void MethodDescriptorProto::clear_input_type() {
+ if (input_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ input_type_->clear();
+ }
+ clear_has_input_type();
+}
+inline const ::std::string& MethodDescriptorProto::input_type() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MethodDescriptorProto.input_type)
+ return *input_type_;
+}
+inline void MethodDescriptorProto::set_input_type(const ::std::string& value) {
+ set_has_input_type();
+ if (input_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ input_type_ = new ::std::string;
+ }
+ input_type_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.MethodDescriptorProto.input_type)
+}
+inline void MethodDescriptorProto::set_input_type(const char* value) {
+ set_has_input_type();
+ if (input_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ input_type_ = new ::std::string;
+ }
+ input_type_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.MethodDescriptorProto.input_type)
+}
+inline void MethodDescriptorProto::set_input_type(const char* value, size_t size) {
+ set_has_input_type();
+ if (input_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ input_type_ = new ::std::string;
+ }
+ input_type_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.MethodDescriptorProto.input_type)
+}
+inline ::std::string* MethodDescriptorProto::mutable_input_type() {
+ set_has_input_type();
+ if (input_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ input_type_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.MethodDescriptorProto.input_type)
+ return input_type_;
+}
+inline ::std::string* MethodDescriptorProto::release_input_type() {
+ clear_has_input_type();
+ if (input_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = input_type_;
+ input_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void MethodDescriptorProto::set_allocated_input_type(::std::string* input_type) {
+ if (input_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete input_type_;
+ }
+ if (input_type) {
+ set_has_input_type();
+ input_type_ = input_type;
+ } else {
+ clear_has_input_type();
+ input_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.MethodDescriptorProto.input_type)
+}
+
+// optional string output_type = 3;
+inline bool MethodDescriptorProto::has_output_type() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void MethodDescriptorProto::set_has_output_type() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void MethodDescriptorProto::clear_has_output_type() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void MethodDescriptorProto::clear_output_type() {
+ if (output_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ output_type_->clear();
+ }
+ clear_has_output_type();
+}
+inline const ::std::string& MethodDescriptorProto::output_type() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MethodDescriptorProto.output_type)
+ return *output_type_;
+}
+inline void MethodDescriptorProto::set_output_type(const ::std::string& value) {
+ set_has_output_type();
+ if (output_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ output_type_ = new ::std::string;
+ }
+ output_type_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.MethodDescriptorProto.output_type)
+}
+inline void MethodDescriptorProto::set_output_type(const char* value) {
+ set_has_output_type();
+ if (output_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ output_type_ = new ::std::string;
+ }
+ output_type_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.MethodDescriptorProto.output_type)
+}
+inline void MethodDescriptorProto::set_output_type(const char* value, size_t size) {
+ set_has_output_type();
+ if (output_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ output_type_ = new ::std::string;
+ }
+ output_type_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.MethodDescriptorProto.output_type)
+}
+inline ::std::string* MethodDescriptorProto::mutable_output_type() {
+ set_has_output_type();
+ if (output_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ output_type_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.MethodDescriptorProto.output_type)
+ return output_type_;
+}
+inline ::std::string* MethodDescriptorProto::release_output_type() {
+ clear_has_output_type();
+ if (output_type_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = output_type_;
+ output_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void MethodDescriptorProto::set_allocated_output_type(::std::string* output_type) {
+ if (output_type_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete output_type_;
+ }
+ if (output_type) {
+ set_has_output_type();
+ output_type_ = output_type;
+ } else {
+ clear_has_output_type();
+ output_type_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.MethodDescriptorProto.output_type)
+}
+
+// optional .google.protobuf.MethodOptions options = 4;
+inline bool MethodDescriptorProto::has_options() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void MethodDescriptorProto::set_has_options() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void MethodDescriptorProto::clear_has_options() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void MethodDescriptorProto::clear_options() {
+ if (options_ != NULL) options_->::google::protobuf::MethodOptions::Clear();
+ clear_has_options();
+}
+inline const ::google::protobuf::MethodOptions& MethodDescriptorProto::options() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MethodDescriptorProto.options)
+ return options_ != NULL ? *options_ : *default_instance_->options_;
+}
+inline ::google::protobuf::MethodOptions* MethodDescriptorProto::mutable_options() {
+ set_has_options();
+ if (options_ == NULL) options_ = new ::google::protobuf::MethodOptions;
+ // @@protoc_insertion_point(field_mutable:google.protobuf.MethodDescriptorProto.options)
+ return options_;
+}
+inline ::google::protobuf::MethodOptions* MethodDescriptorProto::release_options() {
+ clear_has_options();
+ ::google::protobuf::MethodOptions* temp = options_;
+ options_ = NULL;
+ return temp;
+}
+inline void MethodDescriptorProto::set_allocated_options(::google::protobuf::MethodOptions* options) {
+ delete options_;
+ options_ = options;
+ if (options) {
+ set_has_options();
+ } else {
+ clear_has_options();
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.MethodDescriptorProto.options)
+}
+
+// -------------------------------------------------------------------
+
+// FileOptions
+
+// optional string java_package = 1;
+inline bool FileOptions::has_java_package() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FileOptions::set_has_java_package() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FileOptions::clear_has_java_package() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FileOptions::clear_java_package() {
+ if (java_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_package_->clear();
+ }
+ clear_has_java_package();
+}
+inline const ::std::string& FileOptions::java_package() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.java_package)
+ return *java_package_;
+}
+inline void FileOptions::set_java_package(const ::std::string& value) {
+ set_has_java_package();
+ if (java_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_package_ = new ::std::string;
+ }
+ java_package_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.java_package)
+}
+inline void FileOptions::set_java_package(const char* value) {
+ set_has_java_package();
+ if (java_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_package_ = new ::std::string;
+ }
+ java_package_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FileOptions.java_package)
+}
+inline void FileOptions::set_java_package(const char* value, size_t size) {
+ set_has_java_package();
+ if (java_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_package_ = new ::std::string;
+ }
+ java_package_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FileOptions.java_package)
+}
+inline ::std::string* FileOptions::mutable_java_package() {
+ set_has_java_package();
+ if (java_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_package_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileOptions.java_package)
+ return java_package_;
+}
+inline ::std::string* FileOptions::release_java_package() {
+ clear_has_java_package();
+ if (java_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = java_package_;
+ java_package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FileOptions::set_allocated_java_package(::std::string* java_package) {
+ if (java_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete java_package_;
+ }
+ if (java_package) {
+ set_has_java_package();
+ java_package_ = java_package;
+ } else {
+ clear_has_java_package();
+ java_package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileOptions.java_package)
+}
+
+// optional string java_outer_classname = 8;
+inline bool FileOptions::has_java_outer_classname() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FileOptions::set_has_java_outer_classname() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FileOptions::clear_has_java_outer_classname() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FileOptions::clear_java_outer_classname() {
+ if (java_outer_classname_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_outer_classname_->clear();
+ }
+ clear_has_java_outer_classname();
+}
+inline const ::std::string& FileOptions::java_outer_classname() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.java_outer_classname)
+ return *java_outer_classname_;
+}
+inline void FileOptions::set_java_outer_classname(const ::std::string& value) {
+ set_has_java_outer_classname();
+ if (java_outer_classname_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_outer_classname_ = new ::std::string;
+ }
+ java_outer_classname_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.java_outer_classname)
+}
+inline void FileOptions::set_java_outer_classname(const char* value) {
+ set_has_java_outer_classname();
+ if (java_outer_classname_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_outer_classname_ = new ::std::string;
+ }
+ java_outer_classname_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FileOptions.java_outer_classname)
+}
+inline void FileOptions::set_java_outer_classname(const char* value, size_t size) {
+ set_has_java_outer_classname();
+ if (java_outer_classname_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_outer_classname_ = new ::std::string;
+ }
+ java_outer_classname_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FileOptions.java_outer_classname)
+}
+inline ::std::string* FileOptions::mutable_java_outer_classname() {
+ set_has_java_outer_classname();
+ if (java_outer_classname_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ java_outer_classname_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileOptions.java_outer_classname)
+ return java_outer_classname_;
+}
+inline ::std::string* FileOptions::release_java_outer_classname() {
+ clear_has_java_outer_classname();
+ if (java_outer_classname_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = java_outer_classname_;
+ java_outer_classname_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FileOptions::set_allocated_java_outer_classname(::std::string* java_outer_classname) {
+ if (java_outer_classname_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete java_outer_classname_;
+ }
+ if (java_outer_classname) {
+ set_has_java_outer_classname();
+ java_outer_classname_ = java_outer_classname;
+ } else {
+ clear_has_java_outer_classname();
+ java_outer_classname_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileOptions.java_outer_classname)
+}
+
+// optional bool java_multiple_files = 10 [default = false];
+inline bool FileOptions::has_java_multiple_files() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FileOptions::set_has_java_multiple_files() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FileOptions::clear_has_java_multiple_files() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FileOptions::clear_java_multiple_files() {
+ java_multiple_files_ = false;
+ clear_has_java_multiple_files();
+}
+inline bool FileOptions::java_multiple_files() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.java_multiple_files)
+ return java_multiple_files_;
+}
+inline void FileOptions::set_java_multiple_files(bool value) {
+ set_has_java_multiple_files();
+ java_multiple_files_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.java_multiple_files)
+}
+
+// optional bool java_generate_equals_and_hash = 20 [default = false];
+inline bool FileOptions::has_java_generate_equals_and_hash() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void FileOptions::set_has_java_generate_equals_and_hash() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void FileOptions::clear_has_java_generate_equals_and_hash() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void FileOptions::clear_java_generate_equals_and_hash() {
+ java_generate_equals_and_hash_ = false;
+ clear_has_java_generate_equals_and_hash();
+}
+inline bool FileOptions::java_generate_equals_and_hash() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.java_generate_equals_and_hash)
+ return java_generate_equals_and_hash_;
+}
+inline void FileOptions::set_java_generate_equals_and_hash(bool value) {
+ set_has_java_generate_equals_and_hash();
+ java_generate_equals_and_hash_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.java_generate_equals_and_hash)
+}
+
+// optional bool java_string_check_utf8 = 27 [default = false];
+inline bool FileOptions::has_java_string_check_utf8() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void FileOptions::set_has_java_string_check_utf8() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void FileOptions::clear_has_java_string_check_utf8() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void FileOptions::clear_java_string_check_utf8() {
+ java_string_check_utf8_ = false;
+ clear_has_java_string_check_utf8();
+}
+inline bool FileOptions::java_string_check_utf8() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.java_string_check_utf8)
+ return java_string_check_utf8_;
+}
+inline void FileOptions::set_java_string_check_utf8(bool value) {
+ set_has_java_string_check_utf8();
+ java_string_check_utf8_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.java_string_check_utf8)
+}
+
+// optional .google.protobuf.FileOptions.OptimizeMode optimize_for = 9 [default = SPEED];
+inline bool FileOptions::has_optimize_for() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void FileOptions::set_has_optimize_for() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void FileOptions::clear_has_optimize_for() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void FileOptions::clear_optimize_for() {
+ optimize_for_ = 1;
+ clear_has_optimize_for();
+}
+inline ::google::protobuf::FileOptions_OptimizeMode FileOptions::optimize_for() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.optimize_for)
+ return static_cast< ::google::protobuf::FileOptions_OptimizeMode >(optimize_for_);
+}
+inline void FileOptions::set_optimize_for(::google::protobuf::FileOptions_OptimizeMode value) {
+ assert(::google::protobuf::FileOptions_OptimizeMode_IsValid(value));
+ set_has_optimize_for();
+ optimize_for_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.optimize_for)
+}
+
+// optional string go_package = 11;
+inline bool FileOptions::has_go_package() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void FileOptions::set_has_go_package() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void FileOptions::clear_has_go_package() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void FileOptions::clear_go_package() {
+ if (go_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ go_package_->clear();
+ }
+ clear_has_go_package();
+}
+inline const ::std::string& FileOptions::go_package() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.go_package)
+ return *go_package_;
+}
+inline void FileOptions::set_go_package(const ::std::string& value) {
+ set_has_go_package();
+ if (go_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ go_package_ = new ::std::string;
+ }
+ go_package_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.go_package)
+}
+inline void FileOptions::set_go_package(const char* value) {
+ set_has_go_package();
+ if (go_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ go_package_ = new ::std::string;
+ }
+ go_package_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FileOptions.go_package)
+}
+inline void FileOptions::set_go_package(const char* value, size_t size) {
+ set_has_go_package();
+ if (go_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ go_package_ = new ::std::string;
+ }
+ go_package_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FileOptions.go_package)
+}
+inline ::std::string* FileOptions::mutable_go_package() {
+ set_has_go_package();
+ if (go_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ go_package_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileOptions.go_package)
+ return go_package_;
+}
+inline ::std::string* FileOptions::release_go_package() {
+ clear_has_go_package();
+ if (go_package_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = go_package_;
+ go_package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FileOptions::set_allocated_go_package(::std::string* go_package) {
+ if (go_package_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete go_package_;
+ }
+ if (go_package) {
+ set_has_go_package();
+ go_package_ = go_package;
+ } else {
+ clear_has_go_package();
+ go_package_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FileOptions.go_package)
+}
+
+// optional bool cc_generic_services = 16 [default = false];
+inline bool FileOptions::has_cc_generic_services() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void FileOptions::set_has_cc_generic_services() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void FileOptions::clear_has_cc_generic_services() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void FileOptions::clear_cc_generic_services() {
+ cc_generic_services_ = false;
+ clear_has_cc_generic_services();
+}
+inline bool FileOptions::cc_generic_services() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.cc_generic_services)
+ return cc_generic_services_;
+}
+inline void FileOptions::set_cc_generic_services(bool value) {
+ set_has_cc_generic_services();
+ cc_generic_services_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.cc_generic_services)
+}
+
+// optional bool java_generic_services = 17 [default = false];
+inline bool FileOptions::has_java_generic_services() const {
+ return (_has_bits_[0] & 0x00000100u) != 0;
+}
+inline void FileOptions::set_has_java_generic_services() {
+ _has_bits_[0] |= 0x00000100u;
+}
+inline void FileOptions::clear_has_java_generic_services() {
+ _has_bits_[0] &= ~0x00000100u;
+}
+inline void FileOptions::clear_java_generic_services() {
+ java_generic_services_ = false;
+ clear_has_java_generic_services();
+}
+inline bool FileOptions::java_generic_services() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.java_generic_services)
+ return java_generic_services_;
+}
+inline void FileOptions::set_java_generic_services(bool value) {
+ set_has_java_generic_services();
+ java_generic_services_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.java_generic_services)
+}
+
+// optional bool py_generic_services = 18 [default = false];
+inline bool FileOptions::has_py_generic_services() const {
+ return (_has_bits_[0] & 0x00000200u) != 0;
+}
+inline void FileOptions::set_has_py_generic_services() {
+ _has_bits_[0] |= 0x00000200u;
+}
+inline void FileOptions::clear_has_py_generic_services() {
+ _has_bits_[0] &= ~0x00000200u;
+}
+inline void FileOptions::clear_py_generic_services() {
+ py_generic_services_ = false;
+ clear_has_py_generic_services();
+}
+inline bool FileOptions::py_generic_services() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.py_generic_services)
+ return py_generic_services_;
+}
+inline void FileOptions::set_py_generic_services(bool value) {
+ set_has_py_generic_services();
+ py_generic_services_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.py_generic_services)
+}
+
+// optional bool deprecated = 23 [default = false];
+inline bool FileOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000400u) != 0;
+}
+inline void FileOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000400u;
+}
+inline void FileOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000400u;
+}
+inline void FileOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool FileOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.deprecated)
+ return deprecated_;
+}
+inline void FileOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FileOptions.deprecated)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int FileOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void FileOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& FileOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FileOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* FileOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FileOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* FileOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FileOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+FileOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FileOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+FileOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FileOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// MessageOptions
+
+// optional bool message_set_wire_format = 1 [default = false];
+inline bool MessageOptions::has_message_set_wire_format() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void MessageOptions::set_has_message_set_wire_format() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void MessageOptions::clear_has_message_set_wire_format() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void MessageOptions::clear_message_set_wire_format() {
+ message_set_wire_format_ = false;
+ clear_has_message_set_wire_format();
+}
+inline bool MessageOptions::message_set_wire_format() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MessageOptions.message_set_wire_format)
+ return message_set_wire_format_;
+}
+inline void MessageOptions::set_message_set_wire_format(bool value) {
+ set_has_message_set_wire_format();
+ message_set_wire_format_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.MessageOptions.message_set_wire_format)
+}
+
+// optional bool no_standard_descriptor_accessor = 2 [default = false];
+inline bool MessageOptions::has_no_standard_descriptor_accessor() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void MessageOptions::set_has_no_standard_descriptor_accessor() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void MessageOptions::clear_has_no_standard_descriptor_accessor() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void MessageOptions::clear_no_standard_descriptor_accessor() {
+ no_standard_descriptor_accessor_ = false;
+ clear_has_no_standard_descriptor_accessor();
+}
+inline bool MessageOptions::no_standard_descriptor_accessor() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MessageOptions.no_standard_descriptor_accessor)
+ return no_standard_descriptor_accessor_;
+}
+inline void MessageOptions::set_no_standard_descriptor_accessor(bool value) {
+ set_has_no_standard_descriptor_accessor();
+ no_standard_descriptor_accessor_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.MessageOptions.no_standard_descriptor_accessor)
+}
+
+// optional bool deprecated = 3 [default = false];
+inline bool MessageOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void MessageOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void MessageOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void MessageOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool MessageOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MessageOptions.deprecated)
+ return deprecated_;
+}
+inline void MessageOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.MessageOptions.deprecated)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int MessageOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void MessageOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& MessageOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MessageOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* MessageOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.MessageOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* MessageOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.MessageOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+MessageOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.MessageOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+MessageOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.MessageOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// FieldOptions
+
+// optional .google.protobuf.FieldOptions.CType ctype = 1 [default = STRING];
+inline bool FieldOptions::has_ctype() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FieldOptions::set_has_ctype() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FieldOptions::clear_has_ctype() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FieldOptions::clear_ctype() {
+ ctype_ = 0;
+ clear_has_ctype();
+}
+inline ::google::protobuf::FieldOptions_CType FieldOptions::ctype() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.ctype)
+ return static_cast< ::google::protobuf::FieldOptions_CType >(ctype_);
+}
+inline void FieldOptions::set_ctype(::google::protobuf::FieldOptions_CType value) {
+ assert(::google::protobuf::FieldOptions_CType_IsValid(value));
+ set_has_ctype();
+ ctype_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldOptions.ctype)
+}
+
+// optional bool packed = 2;
+inline bool FieldOptions::has_packed() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FieldOptions::set_has_packed() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FieldOptions::clear_has_packed() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FieldOptions::clear_packed() {
+ packed_ = false;
+ clear_has_packed();
+}
+inline bool FieldOptions::packed() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.packed)
+ return packed_;
+}
+inline void FieldOptions::set_packed(bool value) {
+ set_has_packed();
+ packed_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldOptions.packed)
+}
+
+// optional bool lazy = 5 [default = false];
+inline bool FieldOptions::has_lazy() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FieldOptions::set_has_lazy() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FieldOptions::clear_has_lazy() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FieldOptions::clear_lazy() {
+ lazy_ = false;
+ clear_has_lazy();
+}
+inline bool FieldOptions::lazy() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.lazy)
+ return lazy_;
+}
+inline void FieldOptions::set_lazy(bool value) {
+ set_has_lazy();
+ lazy_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldOptions.lazy)
+}
+
+// optional bool deprecated = 3 [default = false];
+inline bool FieldOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void FieldOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void FieldOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void FieldOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool FieldOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.deprecated)
+ return deprecated_;
+}
+inline void FieldOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldOptions.deprecated)
+}
+
+// optional string experimental_map_key = 9;
+inline bool FieldOptions::has_experimental_map_key() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void FieldOptions::set_has_experimental_map_key() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void FieldOptions::clear_has_experimental_map_key() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void FieldOptions::clear_experimental_map_key() {
+ if (experimental_map_key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ experimental_map_key_->clear();
+ }
+ clear_has_experimental_map_key();
+}
+inline const ::std::string& FieldOptions::experimental_map_key() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.experimental_map_key)
+ return *experimental_map_key_;
+}
+inline void FieldOptions::set_experimental_map_key(const ::std::string& value) {
+ set_has_experimental_map_key();
+ if (experimental_map_key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ experimental_map_key_ = new ::std::string;
+ }
+ experimental_map_key_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldOptions.experimental_map_key)
+}
+inline void FieldOptions::set_experimental_map_key(const char* value) {
+ set_has_experimental_map_key();
+ if (experimental_map_key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ experimental_map_key_ = new ::std::string;
+ }
+ experimental_map_key_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.FieldOptions.experimental_map_key)
+}
+inline void FieldOptions::set_experimental_map_key(const char* value, size_t size) {
+ set_has_experimental_map_key();
+ if (experimental_map_key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ experimental_map_key_ = new ::std::string;
+ }
+ experimental_map_key_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.FieldOptions.experimental_map_key)
+}
+inline ::std::string* FieldOptions::mutable_experimental_map_key() {
+ set_has_experimental_map_key();
+ if (experimental_map_key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ experimental_map_key_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldOptions.experimental_map_key)
+ return experimental_map_key_;
+}
+inline ::std::string* FieldOptions::release_experimental_map_key() {
+ clear_has_experimental_map_key();
+ if (experimental_map_key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = experimental_map_key_;
+ experimental_map_key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FieldOptions::set_allocated_experimental_map_key(::std::string* experimental_map_key) {
+ if (experimental_map_key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete experimental_map_key_;
+ }
+ if (experimental_map_key) {
+ set_has_experimental_map_key();
+ experimental_map_key_ = experimental_map_key;
+ } else {
+ clear_has_experimental_map_key();
+ experimental_map_key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.FieldOptions.experimental_map_key)
+}
+
+// optional bool weak = 10 [default = false];
+inline bool FieldOptions::has_weak() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void FieldOptions::set_has_weak() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void FieldOptions::clear_has_weak() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void FieldOptions::clear_weak() {
+ weak_ = false;
+ clear_has_weak();
+}
+inline bool FieldOptions::weak() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.weak)
+ return weak_;
+}
+inline void FieldOptions::set_weak(bool value) {
+ set_has_weak();
+ weak_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.FieldOptions.weak)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int FieldOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void FieldOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& FieldOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.FieldOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* FieldOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.FieldOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* FieldOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.FieldOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+FieldOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.FieldOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+FieldOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.FieldOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// EnumOptions
+
+// optional bool allow_alias = 2;
+inline bool EnumOptions::has_allow_alias() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void EnumOptions::set_has_allow_alias() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void EnumOptions::clear_has_allow_alias() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void EnumOptions::clear_allow_alias() {
+ allow_alias_ = false;
+ clear_has_allow_alias();
+}
+inline bool EnumOptions::allow_alias() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumOptions.allow_alias)
+ return allow_alias_;
+}
+inline void EnumOptions::set_allow_alias(bool value) {
+ set_has_allow_alias();
+ allow_alias_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.EnumOptions.allow_alias)
+}
+
+// optional bool deprecated = 3 [default = false];
+inline bool EnumOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void EnumOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void EnumOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void EnumOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool EnumOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumOptions.deprecated)
+ return deprecated_;
+}
+inline void EnumOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.EnumOptions.deprecated)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int EnumOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void EnumOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& EnumOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* EnumOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* EnumOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.EnumOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+EnumOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.EnumOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+EnumOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.EnumOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// EnumValueOptions
+
+// optional bool deprecated = 1 [default = false];
+inline bool EnumValueOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void EnumValueOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void EnumValueOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void EnumValueOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool EnumValueOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumValueOptions.deprecated)
+ return deprecated_;
+}
+inline void EnumValueOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.EnumValueOptions.deprecated)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int EnumValueOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void EnumValueOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& EnumValueOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.EnumValueOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* EnumValueOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.EnumValueOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* EnumValueOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.EnumValueOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+EnumValueOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.EnumValueOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+EnumValueOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.EnumValueOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// ServiceOptions
+
+// optional bool deprecated = 33 [default = false];
+inline bool ServiceOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ServiceOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ServiceOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ServiceOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool ServiceOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.ServiceOptions.deprecated)
+ return deprecated_;
+}
+inline void ServiceOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.ServiceOptions.deprecated)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int ServiceOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void ServiceOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& ServiceOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.ServiceOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* ServiceOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.ServiceOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* ServiceOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.ServiceOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+ServiceOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.ServiceOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+ServiceOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.ServiceOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// MethodOptions
+
+// optional bool deprecated = 33 [default = false];
+inline bool MethodOptions::has_deprecated() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void MethodOptions::set_has_deprecated() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void MethodOptions::clear_has_deprecated() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void MethodOptions::clear_deprecated() {
+ deprecated_ = false;
+ clear_has_deprecated();
+}
+inline bool MethodOptions::deprecated() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MethodOptions.deprecated)
+ return deprecated_;
+}
+inline void MethodOptions::set_deprecated(bool value) {
+ set_has_deprecated();
+ deprecated_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.MethodOptions.deprecated)
+}
+
+// repeated .google.protobuf.UninterpretedOption uninterpreted_option = 999;
+inline int MethodOptions::uninterpreted_option_size() const {
+ return uninterpreted_option_.size();
+}
+inline void MethodOptions::clear_uninterpreted_option() {
+ uninterpreted_option_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption& MethodOptions::uninterpreted_option(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.MethodOptions.uninterpreted_option)
+ return uninterpreted_option_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption* MethodOptions::mutable_uninterpreted_option(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.MethodOptions.uninterpreted_option)
+ return uninterpreted_option_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption* MethodOptions::add_uninterpreted_option() {
+ // @@protoc_insertion_point(field_add:google.protobuf.MethodOptions.uninterpreted_option)
+ return uninterpreted_option_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >&
+MethodOptions::uninterpreted_option() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.MethodOptions.uninterpreted_option)
+ return uninterpreted_option_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption >*
+MethodOptions::mutable_uninterpreted_option() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.MethodOptions.uninterpreted_option)
+ return &uninterpreted_option_;
+}
+
+// -------------------------------------------------------------------
+
+// UninterpretedOption_NamePart
+
+// required string name_part = 1;
+inline bool UninterpretedOption_NamePart::has_name_part() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void UninterpretedOption_NamePart::set_has_name_part() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void UninterpretedOption_NamePart::clear_has_name_part() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void UninterpretedOption_NamePart::clear_name_part() {
+ if (name_part_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_part_->clear();
+ }
+ clear_has_name_part();
+}
+inline const ::std::string& UninterpretedOption_NamePart::name_part() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.NamePart.name_part)
+ return *name_part_;
+}
+inline void UninterpretedOption_NamePart::set_name_part(const ::std::string& value) {
+ set_has_name_part();
+ if (name_part_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_part_ = new ::std::string;
+ }
+ name_part_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.NamePart.name_part)
+}
+inline void UninterpretedOption_NamePart::set_name_part(const char* value) {
+ set_has_name_part();
+ if (name_part_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_part_ = new ::std::string;
+ }
+ name_part_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.UninterpretedOption.NamePart.name_part)
+}
+inline void UninterpretedOption_NamePart::set_name_part(const char* value, size_t size) {
+ set_has_name_part();
+ if (name_part_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_part_ = new ::std::string;
+ }
+ name_part_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.UninterpretedOption.NamePart.name_part)
+}
+inline ::std::string* UninterpretedOption_NamePart::mutable_name_part() {
+ set_has_name_part();
+ if (name_part_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ name_part_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.UninterpretedOption.NamePart.name_part)
+ return name_part_;
+}
+inline ::std::string* UninterpretedOption_NamePart::release_name_part() {
+ clear_has_name_part();
+ if (name_part_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = name_part_;
+ name_part_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void UninterpretedOption_NamePart::set_allocated_name_part(::std::string* name_part) {
+ if (name_part_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete name_part_;
+ }
+ if (name_part) {
+ set_has_name_part();
+ name_part_ = name_part;
+ } else {
+ clear_has_name_part();
+ name_part_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.UninterpretedOption.NamePart.name_part)
+}
+
+// required bool is_extension = 2;
+inline bool UninterpretedOption_NamePart::has_is_extension() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void UninterpretedOption_NamePart::set_has_is_extension() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void UninterpretedOption_NamePart::clear_has_is_extension() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void UninterpretedOption_NamePart::clear_is_extension() {
+ is_extension_ = false;
+ clear_has_is_extension();
+}
+inline bool UninterpretedOption_NamePart::is_extension() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.NamePart.is_extension)
+ return is_extension_;
+}
+inline void UninterpretedOption_NamePart::set_is_extension(bool value) {
+ set_has_is_extension();
+ is_extension_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.NamePart.is_extension)
+}
+
+// -------------------------------------------------------------------
+
+// UninterpretedOption
+
+// repeated .google.protobuf.UninterpretedOption.NamePart name = 2;
+inline int UninterpretedOption::name_size() const {
+ return name_.size();
+}
+inline void UninterpretedOption::clear_name() {
+ name_.Clear();
+}
+inline const ::google::protobuf::UninterpretedOption_NamePart& UninterpretedOption::name(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.name)
+ return name_.Get(index);
+}
+inline ::google::protobuf::UninterpretedOption_NamePart* UninterpretedOption::mutable_name(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.UninterpretedOption.name)
+ return name_.Mutable(index);
+}
+inline ::google::protobuf::UninterpretedOption_NamePart* UninterpretedOption::add_name() {
+ // @@protoc_insertion_point(field_add:google.protobuf.UninterpretedOption.name)
+ return name_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption_NamePart >&
+UninterpretedOption::name() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.UninterpretedOption.name)
+ return name_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::UninterpretedOption_NamePart >*
+UninterpretedOption::mutable_name() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.UninterpretedOption.name)
+ return &name_;
+}
+
+// optional string identifier_value = 3;
+inline bool UninterpretedOption::has_identifier_value() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void UninterpretedOption::set_has_identifier_value() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void UninterpretedOption::clear_has_identifier_value() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void UninterpretedOption::clear_identifier_value() {
+ if (identifier_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ identifier_value_->clear();
+ }
+ clear_has_identifier_value();
+}
+inline const ::std::string& UninterpretedOption::identifier_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.identifier_value)
+ return *identifier_value_;
+}
+inline void UninterpretedOption::set_identifier_value(const ::std::string& value) {
+ set_has_identifier_value();
+ if (identifier_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ identifier_value_ = new ::std::string;
+ }
+ identifier_value_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.identifier_value)
+}
+inline void UninterpretedOption::set_identifier_value(const char* value) {
+ set_has_identifier_value();
+ if (identifier_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ identifier_value_ = new ::std::string;
+ }
+ identifier_value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.UninterpretedOption.identifier_value)
+}
+inline void UninterpretedOption::set_identifier_value(const char* value, size_t size) {
+ set_has_identifier_value();
+ if (identifier_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ identifier_value_ = new ::std::string;
+ }
+ identifier_value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.UninterpretedOption.identifier_value)
+}
+inline ::std::string* UninterpretedOption::mutable_identifier_value() {
+ set_has_identifier_value();
+ if (identifier_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ identifier_value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.UninterpretedOption.identifier_value)
+ return identifier_value_;
+}
+inline ::std::string* UninterpretedOption::release_identifier_value() {
+ clear_has_identifier_value();
+ if (identifier_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = identifier_value_;
+ identifier_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void UninterpretedOption::set_allocated_identifier_value(::std::string* identifier_value) {
+ if (identifier_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete identifier_value_;
+ }
+ if (identifier_value) {
+ set_has_identifier_value();
+ identifier_value_ = identifier_value;
+ } else {
+ clear_has_identifier_value();
+ identifier_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.UninterpretedOption.identifier_value)
+}
+
+// optional uint64 positive_int_value = 4;
+inline bool UninterpretedOption::has_positive_int_value() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void UninterpretedOption::set_has_positive_int_value() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void UninterpretedOption::clear_has_positive_int_value() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void UninterpretedOption::clear_positive_int_value() {
+ positive_int_value_ = GOOGLE_ULONGLONG(0);
+ clear_has_positive_int_value();
+}
+inline ::google::protobuf::uint64 UninterpretedOption::positive_int_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.positive_int_value)
+ return positive_int_value_;
+}
+inline void UninterpretedOption::set_positive_int_value(::google::protobuf::uint64 value) {
+ set_has_positive_int_value();
+ positive_int_value_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.positive_int_value)
+}
+
+// optional int64 negative_int_value = 5;
+inline bool UninterpretedOption::has_negative_int_value() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void UninterpretedOption::set_has_negative_int_value() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void UninterpretedOption::clear_has_negative_int_value() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void UninterpretedOption::clear_negative_int_value() {
+ negative_int_value_ = GOOGLE_LONGLONG(0);
+ clear_has_negative_int_value();
+}
+inline ::google::protobuf::int64 UninterpretedOption::negative_int_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.negative_int_value)
+ return negative_int_value_;
+}
+inline void UninterpretedOption::set_negative_int_value(::google::protobuf::int64 value) {
+ set_has_negative_int_value();
+ negative_int_value_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.negative_int_value)
+}
+
+// optional double double_value = 6;
+inline bool UninterpretedOption::has_double_value() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void UninterpretedOption::set_has_double_value() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void UninterpretedOption::clear_has_double_value() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void UninterpretedOption::clear_double_value() {
+ double_value_ = 0;
+ clear_has_double_value();
+}
+inline double UninterpretedOption::double_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.double_value)
+ return double_value_;
+}
+inline void UninterpretedOption::set_double_value(double value) {
+ set_has_double_value();
+ double_value_ = value;
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.double_value)
+}
+
+// optional bytes string_value = 7;
+inline bool UninterpretedOption::has_string_value() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void UninterpretedOption::set_has_string_value() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void UninterpretedOption::clear_has_string_value() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void UninterpretedOption::clear_string_value() {
+ if (string_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ string_value_->clear();
+ }
+ clear_has_string_value();
+}
+inline const ::std::string& UninterpretedOption::string_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.string_value)
+ return *string_value_;
+}
+inline void UninterpretedOption::set_string_value(const ::std::string& value) {
+ set_has_string_value();
+ if (string_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ string_value_ = new ::std::string;
+ }
+ string_value_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.string_value)
+}
+inline void UninterpretedOption::set_string_value(const char* value) {
+ set_has_string_value();
+ if (string_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ string_value_ = new ::std::string;
+ }
+ string_value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.UninterpretedOption.string_value)
+}
+inline void UninterpretedOption::set_string_value(const void* value, size_t size) {
+ set_has_string_value();
+ if (string_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ string_value_ = new ::std::string;
+ }
+ string_value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.UninterpretedOption.string_value)
+}
+inline ::std::string* UninterpretedOption::mutable_string_value() {
+ set_has_string_value();
+ if (string_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ string_value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.UninterpretedOption.string_value)
+ return string_value_;
+}
+inline ::std::string* UninterpretedOption::release_string_value() {
+ clear_has_string_value();
+ if (string_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = string_value_;
+ string_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void UninterpretedOption::set_allocated_string_value(::std::string* string_value) {
+ if (string_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete string_value_;
+ }
+ if (string_value) {
+ set_has_string_value();
+ string_value_ = string_value;
+ } else {
+ clear_has_string_value();
+ string_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.UninterpretedOption.string_value)
+}
+
+// optional string aggregate_value = 8;
+inline bool UninterpretedOption::has_aggregate_value() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void UninterpretedOption::set_has_aggregate_value() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void UninterpretedOption::clear_has_aggregate_value() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void UninterpretedOption::clear_aggregate_value() {
+ if (aggregate_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ aggregate_value_->clear();
+ }
+ clear_has_aggregate_value();
+}
+inline const ::std::string& UninterpretedOption::aggregate_value() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.UninterpretedOption.aggregate_value)
+ return *aggregate_value_;
+}
+inline void UninterpretedOption::set_aggregate_value(const ::std::string& value) {
+ set_has_aggregate_value();
+ if (aggregate_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ aggregate_value_ = new ::std::string;
+ }
+ aggregate_value_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.UninterpretedOption.aggregate_value)
+}
+inline void UninterpretedOption::set_aggregate_value(const char* value) {
+ set_has_aggregate_value();
+ if (aggregate_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ aggregate_value_ = new ::std::string;
+ }
+ aggregate_value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.UninterpretedOption.aggregate_value)
+}
+inline void UninterpretedOption::set_aggregate_value(const char* value, size_t size) {
+ set_has_aggregate_value();
+ if (aggregate_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ aggregate_value_ = new ::std::string;
+ }
+ aggregate_value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.UninterpretedOption.aggregate_value)
+}
+inline ::std::string* UninterpretedOption::mutable_aggregate_value() {
+ set_has_aggregate_value();
+ if (aggregate_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ aggregate_value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.UninterpretedOption.aggregate_value)
+ return aggregate_value_;
+}
+inline ::std::string* UninterpretedOption::release_aggregate_value() {
+ clear_has_aggregate_value();
+ if (aggregate_value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = aggregate_value_;
+ aggregate_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void UninterpretedOption::set_allocated_aggregate_value(::std::string* aggregate_value) {
+ if (aggregate_value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete aggregate_value_;
+ }
+ if (aggregate_value) {
+ set_has_aggregate_value();
+ aggregate_value_ = aggregate_value;
+ } else {
+ clear_has_aggregate_value();
+ aggregate_value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.UninterpretedOption.aggregate_value)
+}
+
+// -------------------------------------------------------------------
+
+// SourceCodeInfo_Location
+
+// repeated int32 path = 1 [packed = true];
+inline int SourceCodeInfo_Location::path_size() const {
+ return path_.size();
+}
+inline void SourceCodeInfo_Location::clear_path() {
+ path_.Clear();
+}
+inline ::google::protobuf::int32 SourceCodeInfo_Location::path(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.SourceCodeInfo.Location.path)
+ return path_.Get(index);
+}
+inline void SourceCodeInfo_Location::set_path(int index, ::google::protobuf::int32 value) {
+ path_.Set(index, value);
+ // @@protoc_insertion_point(field_set:google.protobuf.SourceCodeInfo.Location.path)
+}
+inline void SourceCodeInfo_Location::add_path(::google::protobuf::int32 value) {
+ path_.Add(value);
+ // @@protoc_insertion_point(field_add:google.protobuf.SourceCodeInfo.Location.path)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+SourceCodeInfo_Location::path() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.SourceCodeInfo.Location.path)
+ return path_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+SourceCodeInfo_Location::mutable_path() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.SourceCodeInfo.Location.path)
+ return &path_;
+}
+
+// repeated int32 span = 2 [packed = true];
+inline int SourceCodeInfo_Location::span_size() const {
+ return span_.size();
+}
+inline void SourceCodeInfo_Location::clear_span() {
+ span_.Clear();
+}
+inline ::google::protobuf::int32 SourceCodeInfo_Location::span(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.SourceCodeInfo.Location.span)
+ return span_.Get(index);
+}
+inline void SourceCodeInfo_Location::set_span(int index, ::google::protobuf::int32 value) {
+ span_.Set(index, value);
+ // @@protoc_insertion_point(field_set:google.protobuf.SourceCodeInfo.Location.span)
+}
+inline void SourceCodeInfo_Location::add_span(::google::protobuf::int32 value) {
+ span_.Add(value);
+ // @@protoc_insertion_point(field_add:google.protobuf.SourceCodeInfo.Location.span)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+SourceCodeInfo_Location::span() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.SourceCodeInfo.Location.span)
+ return span_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+SourceCodeInfo_Location::mutable_span() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.SourceCodeInfo.Location.span)
+ return &span_;
+}
+
+// optional string leading_comments = 3;
+inline bool SourceCodeInfo_Location::has_leading_comments() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void SourceCodeInfo_Location::set_has_leading_comments() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void SourceCodeInfo_Location::clear_has_leading_comments() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void SourceCodeInfo_Location::clear_leading_comments() {
+ if (leading_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ leading_comments_->clear();
+ }
+ clear_has_leading_comments();
+}
+inline const ::std::string& SourceCodeInfo_Location::leading_comments() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.SourceCodeInfo.Location.leading_comments)
+ return *leading_comments_;
+}
+inline void SourceCodeInfo_Location::set_leading_comments(const ::std::string& value) {
+ set_has_leading_comments();
+ if (leading_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ leading_comments_ = new ::std::string;
+ }
+ leading_comments_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.SourceCodeInfo.Location.leading_comments)
+}
+inline void SourceCodeInfo_Location::set_leading_comments(const char* value) {
+ set_has_leading_comments();
+ if (leading_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ leading_comments_ = new ::std::string;
+ }
+ leading_comments_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.SourceCodeInfo.Location.leading_comments)
+}
+inline void SourceCodeInfo_Location::set_leading_comments(const char* value, size_t size) {
+ set_has_leading_comments();
+ if (leading_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ leading_comments_ = new ::std::string;
+ }
+ leading_comments_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.SourceCodeInfo.Location.leading_comments)
+}
+inline ::std::string* SourceCodeInfo_Location::mutable_leading_comments() {
+ set_has_leading_comments();
+ if (leading_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ leading_comments_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.SourceCodeInfo.Location.leading_comments)
+ return leading_comments_;
+}
+inline ::std::string* SourceCodeInfo_Location::release_leading_comments() {
+ clear_has_leading_comments();
+ if (leading_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = leading_comments_;
+ leading_comments_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void SourceCodeInfo_Location::set_allocated_leading_comments(::std::string* leading_comments) {
+ if (leading_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete leading_comments_;
+ }
+ if (leading_comments) {
+ set_has_leading_comments();
+ leading_comments_ = leading_comments;
+ } else {
+ clear_has_leading_comments();
+ leading_comments_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.SourceCodeInfo.Location.leading_comments)
+}
+
+// optional string trailing_comments = 4;
+inline bool SourceCodeInfo_Location::has_trailing_comments() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void SourceCodeInfo_Location::set_has_trailing_comments() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void SourceCodeInfo_Location::clear_has_trailing_comments() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void SourceCodeInfo_Location::clear_trailing_comments() {
+ if (trailing_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ trailing_comments_->clear();
+ }
+ clear_has_trailing_comments();
+}
+inline const ::std::string& SourceCodeInfo_Location::trailing_comments() const {
+ // @@protoc_insertion_point(field_get:google.protobuf.SourceCodeInfo.Location.trailing_comments)
+ return *trailing_comments_;
+}
+inline void SourceCodeInfo_Location::set_trailing_comments(const ::std::string& value) {
+ set_has_trailing_comments();
+ if (trailing_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ trailing_comments_ = new ::std::string;
+ }
+ trailing_comments_->assign(value);
+ // @@protoc_insertion_point(field_set:google.protobuf.SourceCodeInfo.Location.trailing_comments)
+}
+inline void SourceCodeInfo_Location::set_trailing_comments(const char* value) {
+ set_has_trailing_comments();
+ if (trailing_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ trailing_comments_ = new ::std::string;
+ }
+ trailing_comments_->assign(value);
+ // @@protoc_insertion_point(field_set_char:google.protobuf.SourceCodeInfo.Location.trailing_comments)
+}
+inline void SourceCodeInfo_Location::set_trailing_comments(const char* value, size_t size) {
+ set_has_trailing_comments();
+ if (trailing_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ trailing_comments_ = new ::std::string;
+ }
+ trailing_comments_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:google.protobuf.SourceCodeInfo.Location.trailing_comments)
+}
+inline ::std::string* SourceCodeInfo_Location::mutable_trailing_comments() {
+ set_has_trailing_comments();
+ if (trailing_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ trailing_comments_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:google.protobuf.SourceCodeInfo.Location.trailing_comments)
+ return trailing_comments_;
+}
+inline ::std::string* SourceCodeInfo_Location::release_trailing_comments() {
+ clear_has_trailing_comments();
+ if (trailing_comments_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = trailing_comments_;
+ trailing_comments_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void SourceCodeInfo_Location::set_allocated_trailing_comments(::std::string* trailing_comments) {
+ if (trailing_comments_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete trailing_comments_;
+ }
+ if (trailing_comments) {
+ set_has_trailing_comments();
+ trailing_comments_ = trailing_comments;
+ } else {
+ clear_has_trailing_comments();
+ trailing_comments_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:google.protobuf.SourceCodeInfo.Location.trailing_comments)
+}
+
+// -------------------------------------------------------------------
+
+// SourceCodeInfo
+
+// repeated .google.protobuf.SourceCodeInfo.Location location = 1;
+inline int SourceCodeInfo::location_size() const {
+ return location_.size();
+}
+inline void SourceCodeInfo::clear_location() {
+ location_.Clear();
+}
+inline const ::google::protobuf::SourceCodeInfo_Location& SourceCodeInfo::location(int index) const {
+ // @@protoc_insertion_point(field_get:google.protobuf.SourceCodeInfo.location)
+ return location_.Get(index);
+}
+inline ::google::protobuf::SourceCodeInfo_Location* SourceCodeInfo::mutable_location(int index) {
+ // @@protoc_insertion_point(field_mutable:google.protobuf.SourceCodeInfo.location)
+ return location_.Mutable(index);
+}
+inline ::google::protobuf::SourceCodeInfo_Location* SourceCodeInfo::add_location() {
+ // @@protoc_insertion_point(field_add:google.protobuf.SourceCodeInfo.location)
+ return location_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::google::protobuf::SourceCodeInfo_Location >&
+SourceCodeInfo::location() const {
+ // @@protoc_insertion_point(field_list:google.protobuf.SourceCodeInfo.location)
+ return location_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::google::protobuf::SourceCodeInfo_Location >*
+SourceCodeInfo::mutable_location() {
+ // @@protoc_insertion_point(field_mutable_list:google.protobuf.SourceCodeInfo.location)
+ return &location_;
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace protobuf
+} // namespace google
+
+#ifndef SWIG
+namespace google {
+namespace protobuf {
+
+template <> struct is_proto_enum< ::google::protobuf::FieldDescriptorProto_Type> : ::google::protobuf::internal::true_type {};
+template <>
+inline const EnumDescriptor* GetEnumDescriptor< ::google::protobuf::FieldDescriptorProto_Type>() {
+ return ::google::protobuf::FieldDescriptorProto_Type_descriptor();
+}
+template <> struct is_proto_enum< ::google::protobuf::FieldDescriptorProto_Label> : ::google::protobuf::internal::true_type {};
+template <>
+inline const EnumDescriptor* GetEnumDescriptor< ::google::protobuf::FieldDescriptorProto_Label>() {
+ return ::google::protobuf::FieldDescriptorProto_Label_descriptor();
+}
+template <> struct is_proto_enum< ::google::protobuf::FileOptions_OptimizeMode> : ::google::protobuf::internal::true_type {};
+template <>
+inline const EnumDescriptor* GetEnumDescriptor< ::google::protobuf::FileOptions_OptimizeMode>() {
+ return ::google::protobuf::FileOptions_OptimizeMode_descriptor();
+}
+template <> struct is_proto_enum< ::google::protobuf::FieldOptions_CType> : ::google::protobuf::internal::true_type {};
+template <>
+inline const EnumDescriptor* GetEnumDescriptor< ::google::protobuf::FieldOptions_CType>() {
+ return ::google::protobuf::FieldOptions_CType_descriptor();
+}
+
+} // namespace google
+} // namespace protobuf
+#endif // SWIG
+
+// @@protoc_insertion_point(global_scope)
+
+#endif // PROTOBUF_google_2fprotobuf_2fdescriptor_2eproto__INCLUDED
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor.proto b/toolkit/components/protobuf/src/google/protobuf/descriptor.proto
new file mode 100644
index 0000000000..a753601f39
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor.proto
@@ -0,0 +1,687 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// The messages in this file describe the definitions found in .proto files.
+// A valid .proto file can be translated directly to a FileDescriptorProto
+// without any other information (e.g. without reading its imports).
+
+
+
+package google.protobuf;
+option java_package = "com.google.protobuf";
+option java_outer_classname = "DescriptorProtos";
+
+// descriptor.proto must be optimized for speed because reflection-based
+// algorithms don't work during bootstrapping.
+option optimize_for = SPEED;
+
+// The protocol compiler can output a FileDescriptorSet containing the .proto
+// files it parses.
+message FileDescriptorSet {
+ repeated FileDescriptorProto file = 1;
+}
+
+// Describes a complete .proto file.
+message FileDescriptorProto {
+ optional string name = 1; // file name, relative to root of source tree
+ optional string package = 2; // e.g. "foo", "foo.bar", etc.
+
+ // Names of files imported by this file.
+ repeated string dependency = 3;
+ // Indexes of the public imported files in the dependency list above.
+ repeated int32 public_dependency = 10;
+ // Indexes of the weak imported files in the dependency list.
+ // For Google-internal migration only. Do not use.
+ repeated int32 weak_dependency = 11;
+
+ // All top-level definitions in this file.
+ repeated DescriptorProto message_type = 4;
+ repeated EnumDescriptorProto enum_type = 5;
+ repeated ServiceDescriptorProto service = 6;
+ repeated FieldDescriptorProto extension = 7;
+
+ optional FileOptions options = 8;
+
+ // This field contains optional information about the original source code.
+ // You may safely remove this entire field whithout harming runtime
+ // functionality of the descriptors -- the information is needed only by
+ // development tools.
+ optional SourceCodeInfo source_code_info = 9;
+}
+
+// Describes a message type.
+message DescriptorProto {
+ optional string name = 1;
+
+ repeated FieldDescriptorProto field = 2;
+ repeated FieldDescriptorProto extension = 6;
+
+ repeated DescriptorProto nested_type = 3;
+ repeated EnumDescriptorProto enum_type = 4;
+
+ message ExtensionRange {
+ optional int32 start = 1;
+ optional int32 end = 2;
+ }
+ repeated ExtensionRange extension_range = 5;
+
+ repeated OneofDescriptorProto oneof_decl = 8;
+
+ optional MessageOptions options = 7;
+}
+
+// Describes a field within a message.
+message FieldDescriptorProto {
+ enum Type {
+ // 0 is reserved for errors.
+ // Order is weird for historical reasons.
+ TYPE_DOUBLE = 1;
+ TYPE_FLOAT = 2;
+ // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if
+ // negative values are likely.
+ TYPE_INT64 = 3;
+ TYPE_UINT64 = 4;
+ // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if
+ // negative values are likely.
+ TYPE_INT32 = 5;
+ TYPE_FIXED64 = 6;
+ TYPE_FIXED32 = 7;
+ TYPE_BOOL = 8;
+ TYPE_STRING = 9;
+ TYPE_GROUP = 10; // Tag-delimited aggregate.
+ TYPE_MESSAGE = 11; // Length-delimited aggregate.
+
+ // New in version 2.
+ TYPE_BYTES = 12;
+ TYPE_UINT32 = 13;
+ TYPE_ENUM = 14;
+ TYPE_SFIXED32 = 15;
+ TYPE_SFIXED64 = 16;
+ TYPE_SINT32 = 17; // Uses ZigZag encoding.
+ TYPE_SINT64 = 18; // Uses ZigZag encoding.
+ };
+
+ enum Label {
+ // 0 is reserved for errors
+ LABEL_OPTIONAL = 1;
+ LABEL_REQUIRED = 2;
+ LABEL_REPEATED = 3;
+ // TODO(sanjay): Should we add LABEL_MAP?
+ };
+
+ optional string name = 1;
+ optional int32 number = 3;
+ optional Label label = 4;
+
+ // If type_name is set, this need not be set. If both this and type_name
+ // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP.
+ optional Type type = 5;
+
+ // For message and enum types, this is the name of the type. If the name
+ // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping
+ // rules are used to find the type (i.e. first the nested types within this
+ // message are searched, then within the parent, on up to the root
+ // namespace).
+ optional string type_name = 6;
+
+ // For extensions, this is the name of the type being extended. It is
+ // resolved in the same manner as type_name.
+ optional string extendee = 2;
+
+ // For numeric types, contains the original text representation of the value.
+ // For booleans, "true" or "false".
+ // For strings, contains the default text contents (not escaped in any way).
+ // For bytes, contains the C escaped value. All bytes >= 128 are escaped.
+ // TODO(kenton): Base-64 encode?
+ optional string default_value = 7;
+
+ // If set, gives the index of a oneof in the containing type's oneof_decl
+ // list. This field is a member of that oneof. Extensions of a oneof should
+ // not set this since the oneof to which they belong will be inferred based
+ // on the extension range containing the extension's field number.
+ optional int32 oneof_index = 9;
+
+ optional FieldOptions options = 8;
+}
+
+// Describes a oneof.
+message OneofDescriptorProto {
+ optional string name = 1;
+}
+
+// Describes an enum type.
+message EnumDescriptorProto {
+ optional string name = 1;
+
+ repeated EnumValueDescriptorProto value = 2;
+
+ optional EnumOptions options = 3;
+}
+
+// Describes a value within an enum.
+message EnumValueDescriptorProto {
+ optional string name = 1;
+ optional int32 number = 2;
+
+ optional EnumValueOptions options = 3;
+}
+
+// Describes a service.
+message ServiceDescriptorProto {
+ optional string name = 1;
+ repeated MethodDescriptorProto method = 2;
+
+ optional ServiceOptions options = 3;
+}
+
+// Describes a method of a service.
+message MethodDescriptorProto {
+ optional string name = 1;
+
+ // Input and output type names. These are resolved in the same way as
+ // FieldDescriptorProto.type_name, but must refer to a message type.
+ optional string input_type = 2;
+ optional string output_type = 3;
+
+ optional MethodOptions options = 4;
+}
+
+
+// ===================================================================
+// Options
+
+// Each of the definitions above may have "options" attached. These are
+// just annotations which may cause code to be generated slightly differently
+// or may contain hints for code that manipulates protocol messages.
+//
+// Clients may define custom options as extensions of the *Options messages.
+// These extensions may not yet be known at parsing time, so the parser cannot
+// store the values in them. Instead it stores them in a field in the *Options
+// message called uninterpreted_option. This field must have the same name
+// across all *Options messages. We then use this field to populate the
+// extensions when we build a descriptor, at which point all protos have been
+// parsed and so all extensions are known.
+//
+// Extension numbers for custom options may be chosen as follows:
+// * For options which will only be used within a single application or
+// organization, or for experimental options, use field numbers 50000
+// through 99999. It is up to you to ensure that you do not use the
+// same number for multiple options.
+// * For options which will be published and used publicly by multiple
+// independent entities, e-mail protobuf-global-extension-registry@google.com
+// to reserve extension numbers. Simply provide your project name (e.g.
+// Object-C plugin) and your porject website (if available) -- there's no need
+// to explain how you intend to use them. Usually you only need one extension
+// number. You can declare multiple options with only one extension number by
+// putting them in a sub-message. See the Custom Options section of the docs
+// for examples:
+// https://developers.google.com/protocol-buffers/docs/proto#options
+// If this turns out to be popular, a web service will be set up
+// to automatically assign option numbers.
+
+
+message FileOptions {
+
+ // Sets the Java package where classes generated from this .proto will be
+ // placed. By default, the proto package is used, but this is often
+ // inappropriate because proto packages do not normally start with backwards
+ // domain names.
+ optional string java_package = 1;
+
+
+ // If set, all the classes from the .proto file are wrapped in a single
+ // outer class with the given name. This applies to both Proto1
+ // (equivalent to the old "--one_java_file" option) and Proto2 (where
+ // a .proto always translates to a single class, but you may want to
+ // explicitly choose the class name).
+ optional string java_outer_classname = 8;
+
+ // If set true, then the Java code generator will generate a separate .java
+ // file for each top-level message, enum, and service defined in the .proto
+ // file. Thus, these types will *not* be nested inside the outer class
+ // named by java_outer_classname. However, the outer class will still be
+ // generated to contain the file's getDescriptor() method as well as any
+ // top-level extensions defined in the file.
+ optional bool java_multiple_files = 10 [default=false];
+
+ // If set true, then the Java code generator will generate equals() and
+ // hashCode() methods for all messages defined in the .proto file.
+ // - In the full runtime, this is purely a speed optimization, as the
+ // AbstractMessage base class includes reflection-based implementations of
+ // these methods.
+ //- In the lite runtime, setting this option changes the semantics of
+ // equals() and hashCode() to more closely match those of the full runtime;
+ // the generated methods compute their results based on field values rather
+ // than object identity. (Implementations should not assume that hashcodes
+ // will be consistent across runtimes or versions of the protocol compiler.)
+ optional bool java_generate_equals_and_hash = 20 [default=false];
+
+ // If set true, then the Java2 code generator will generate code that
+ // throws an exception whenever an attempt is made to assign a non-UTF-8
+ // byte sequence to a string field.
+ // Message reflection will do the same.
+ // However, an extension field still accepts non-UTF-8 byte sequences.
+ // This option has no effect on when used with the lite runtime.
+ optional bool java_string_check_utf8 = 27 [default=false];
+
+
+ // Generated classes can be optimized for speed or code size.
+ enum OptimizeMode {
+ SPEED = 1; // Generate complete code for parsing, serialization,
+ // etc.
+ CODE_SIZE = 2; // Use ReflectionOps to implement these methods.
+ LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime.
+ }
+ optional OptimizeMode optimize_for = 9 [default=SPEED];
+
+ // Sets the Go package where structs generated from this .proto will be
+ // placed. There is no default.
+ optional string go_package = 11;
+
+
+
+ // Should generic services be generated in each language? "Generic" services
+ // are not specific to any particular RPC system. They are generated by the
+ // main code generators in each language (without additional plugins).
+ // Generic services were the only kind of service generation supported by
+ // early versions of proto2.
+ //
+ // Generic services are now considered deprecated in favor of using plugins
+ // that generate code specific to your particular RPC system. Therefore,
+ // these default to false. Old code which depends on generic services should
+ // explicitly set them to true.
+ optional bool cc_generic_services = 16 [default=false];
+ optional bool java_generic_services = 17 [default=false];
+ optional bool py_generic_services = 18 [default=false];
+
+ // Is this file deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for everything in the file, or it will be completely ignored; in the very
+ // least, this is a formalization for deprecating files.
+ optional bool deprecated = 23 [default=false];
+
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message MessageOptions {
+ // Set true to use the old proto1 MessageSet wire format for extensions.
+ // This is provided for backwards-compatibility with the MessageSet wire
+ // format. You should not use this for any other reason: It's less
+ // efficient, has fewer features, and is more complicated.
+ //
+ // The message must be defined exactly as follows:
+ // message Foo {
+ // option message_set_wire_format = true;
+ // extensions 4 to max;
+ // }
+ // Note that the message cannot have any defined fields; MessageSets only
+ // have extensions.
+ //
+ // All extensions of your type must be singular messages; e.g. they cannot
+ // be int32s, enums, or repeated messages.
+ //
+ // Because this is an option, the above two restrictions are not enforced by
+ // the protocol compiler.
+ optional bool message_set_wire_format = 1 [default=false];
+
+ // Disables the generation of the standard "descriptor()" accessor, which can
+ // conflict with a field of the same name. This is meant to make migration
+ // from proto1 easier; new code should avoid fields named "descriptor".
+ optional bool no_standard_descriptor_accessor = 2 [default=false];
+
+ // Is this message deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for the message, or it will be completely ignored; in the very least,
+ // this is a formalization for deprecating messages.
+ optional bool deprecated = 3 [default=false];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message FieldOptions {
+ // The ctype option instructs the C++ code generator to use a different
+ // representation of the field than it normally would. See the specific
+ // options below. This option is not yet implemented in the open source
+ // release -- sorry, we'll try to include it in a future version!
+ optional CType ctype = 1 [default = STRING];
+ enum CType {
+ // Default mode.
+ STRING = 0;
+
+ CORD = 1;
+
+ STRING_PIECE = 2;
+ }
+ // The packed option can be enabled for repeated primitive fields to enable
+ // a more efficient representation on the wire. Rather than repeatedly
+ // writing the tag and type for each element, the entire array is encoded as
+ // a single length-delimited blob.
+ optional bool packed = 2;
+
+
+
+ // Should this field be parsed lazily? Lazy applies only to message-type
+ // fields. It means that when the outer message is initially parsed, the
+ // inner message's contents will not be parsed but instead stored in encoded
+ // form. The inner message will actually be parsed when it is first accessed.
+ //
+ // This is only a hint. Implementations are free to choose whether to use
+ // eager or lazy parsing regardless of the value of this option. However,
+ // setting this option true suggests that the protocol author believes that
+ // using lazy parsing on this field is worth the additional bookkeeping
+ // overhead typically needed to implement it.
+ //
+ // This option does not affect the public interface of any generated code;
+ // all method signatures remain the same. Furthermore, thread-safety of the
+ // interface is not affected by this option; const methods remain safe to
+ // call from multiple threads concurrently, while non-const methods continue
+ // to require exclusive access.
+ //
+ //
+ // Note that implementations may choose not to check required fields within
+ // a lazy sub-message. That is, calling IsInitialized() on the outher message
+ // may return true even if the inner message has missing required fields.
+ // This is necessary because otherwise the inner message would have to be
+ // parsed in order to perform the check, defeating the purpose of lazy
+ // parsing. An implementation which chooses not to check required fields
+ // must be consistent about it. That is, for any particular sub-message, the
+ // implementation must either *always* check its required fields, or *never*
+ // check its required fields, regardless of whether or not the message has
+ // been parsed.
+ optional bool lazy = 5 [default=false];
+
+ // Is this field deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for accessors, or it will be completely ignored; in the very least, this
+ // is a formalization for deprecating fields.
+ optional bool deprecated = 3 [default=false];
+
+ // EXPERIMENTAL. DO NOT USE.
+ // For "map" fields, the name of the field in the enclosed type that
+ // is the key for this map. For example, suppose we have:
+ // message Item {
+ // required string name = 1;
+ // required string value = 2;
+ // }
+ // message Config {
+ // repeated Item items = 1 [experimental_map_key="name"];
+ // }
+ // In this situation, the map key for Item will be set to "name".
+ // TODO: Fully-implement this, then remove the "experimental_" prefix.
+ optional string experimental_map_key = 9;
+
+ // For Google-internal migration only. Do not use.
+ optional bool weak = 10 [default=false];
+
+
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message EnumOptions {
+
+ // Set this option to true to allow mapping different tag names to the same
+ // value.
+ optional bool allow_alias = 2;
+
+ // Is this enum deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for the enum, or it will be completely ignored; in the very least, this
+ // is a formalization for deprecating enums.
+ optional bool deprecated = 3 [default=false];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message EnumValueOptions {
+ // Is this enum value deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for the enum value, or it will be completely ignored; in the very least,
+ // this is a formalization for deprecating enum values.
+ optional bool deprecated = 1 [default=false];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message ServiceOptions {
+
+ // Note: Field numbers 1 through 32 are reserved for Google's internal RPC
+ // framework. We apologize for hoarding these numbers to ourselves, but
+ // we were already using them long before we decided to release Protocol
+ // Buffers.
+
+ // Is this service deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for the service, or it will be completely ignored; in the very least,
+ // this is a formalization for deprecating services.
+ optional bool deprecated = 33 [default=false];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+message MethodOptions {
+
+ // Note: Field numbers 1 through 32 are reserved for Google's internal RPC
+ // framework. We apologize for hoarding these numbers to ourselves, but
+ // we were already using them long before we decided to release Protocol
+ // Buffers.
+
+ // Is this method deprecated?
+ // Depending on the target platform, this can emit Deprecated annotations
+ // for the method, or it will be completely ignored; in the very least,
+ // this is a formalization for deprecating methods.
+ optional bool deprecated = 33 [default=false];
+
+ // The parser stores options it doesn't recognize here. See above.
+ repeated UninterpretedOption uninterpreted_option = 999;
+
+ // Clients can define custom options in extensions of this message. See above.
+ extensions 1000 to max;
+}
+
+
+// A message representing a option the parser does not recognize. This only
+// appears in options protos created by the compiler::Parser class.
+// DescriptorPool resolves these when building Descriptor objects. Therefore,
+// options protos in descriptor objects (e.g. returned by Descriptor::options(),
+// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions
+// in them.
+message UninterpretedOption {
+ // The name of the uninterpreted option. Each string represents a segment in
+ // a dot-separated name. is_extension is true iff a segment represents an
+ // extension (denoted with parentheses in options specs in .proto files).
+ // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents
+ // "foo.(bar.baz).qux".
+ message NamePart {
+ required string name_part = 1;
+ required bool is_extension = 2;
+ }
+ repeated NamePart name = 2;
+
+ // The value of the uninterpreted option, in whatever type the tokenizer
+ // identified it as during parsing. Exactly one of these should be set.
+ optional string identifier_value = 3;
+ optional uint64 positive_int_value = 4;
+ optional int64 negative_int_value = 5;
+ optional double double_value = 6;
+ optional bytes string_value = 7;
+ optional string aggregate_value = 8;
+}
+
+// ===================================================================
+// Optional source code info
+
+// Encapsulates information about the original source file from which a
+// FileDescriptorProto was generated.
+message SourceCodeInfo {
+ // A Location identifies a piece of source code in a .proto file which
+ // corresponds to a particular definition. This information is intended
+ // to be useful to IDEs, code indexers, documentation generators, and similar
+ // tools.
+ //
+ // For example, say we have a file like:
+ // message Foo {
+ // optional string foo = 1;
+ // }
+ // Let's look at just the field definition:
+ // optional string foo = 1;
+ // ^ ^^ ^^ ^ ^^^
+ // a bc de f ghi
+ // We have the following locations:
+ // span path represents
+ // [a,i) [ 4, 0, 2, 0 ] The whole field definition.
+ // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional).
+ // [c,d) [ 4, 0, 2, 0, 5 ] The type (string).
+ // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo).
+ // [g,h) [ 4, 0, 2, 0, 3 ] The number (1).
+ //
+ // Notes:
+ // - A location may refer to a repeated field itself (i.e. not to any
+ // particular index within it). This is used whenever a set of elements are
+ // logically enclosed in a single code segment. For example, an entire
+ // extend block (possibly containing multiple extension definitions) will
+ // have an outer location whose path refers to the "extensions" repeated
+ // field without an index.
+ // - Multiple locations may have the same path. This happens when a single
+ // logical declaration is spread out across multiple places. The most
+ // obvious example is the "extend" block again -- there may be multiple
+ // extend blocks in the same scope, each of which will have the same path.
+ // - A location's span is not always a subset of its parent's span. For
+ // example, the "extendee" of an extension declaration appears at the
+ // beginning of the "extend" block and is shared by all extensions within
+ // the block.
+ // - Just because a location's span is a subset of some other location's span
+ // does not mean that it is a descendent. For example, a "group" defines
+ // both a type and a field in a single declaration. Thus, the locations
+ // corresponding to the type and field and their components will overlap.
+ // - Code which tries to interpret locations should probably be designed to
+ // ignore those that it doesn't understand, as more types of locations could
+ // be recorded in the future.
+ repeated Location location = 1;
+ message Location {
+ // Identifies which part of the FileDescriptorProto was defined at this
+ // location.
+ //
+ // Each element is a field number or an index. They form a path from
+ // the root FileDescriptorProto to the place where the definition. For
+ // example, this path:
+ // [ 4, 3, 2, 7, 1 ]
+ // refers to:
+ // file.message_type(3) // 4, 3
+ // .field(7) // 2, 7
+ // .name() // 1
+ // This is because FileDescriptorProto.message_type has field number 4:
+ // repeated DescriptorProto message_type = 4;
+ // and DescriptorProto.field has field number 2:
+ // repeated FieldDescriptorProto field = 2;
+ // and FieldDescriptorProto.name has field number 1:
+ // optional string name = 1;
+ //
+ // Thus, the above path gives the location of a field name. If we removed
+ // the last element:
+ // [ 4, 3, 2, 7 ]
+ // this path refers to the whole field declaration (from the beginning
+ // of the label to the terminating semicolon).
+ repeated int32 path = 1 [packed=true];
+
+ // Always has exactly three or four elements: start line, start column,
+ // end line (optional, otherwise assumed same as start line), end column.
+ // These are packed into a single field for efficiency. Note that line
+ // and column numbers are zero-based -- typically you will want to add
+ // 1 to each before displaying to a user.
+ repeated int32 span = 2 [packed=true];
+
+ // If this SourceCodeInfo represents a complete declaration, these are any
+ // comments appearing before and after the declaration which appear to be
+ // attached to the declaration.
+ //
+ // A series of line comments appearing on consecutive lines, with no other
+ // tokens appearing on those lines, will be treated as a single comment.
+ //
+ // Only the comment content is provided; comment markers (e.g. //) are
+ // stripped out. For block comments, leading whitespace and an asterisk
+ // will be stripped from the beginning of each line other than the first.
+ // Newlines are included in the output.
+ //
+ // Examples:
+ //
+ // optional int32 foo = 1; // Comment attached to foo.
+ // // Comment attached to bar.
+ // optional int32 bar = 2;
+ //
+ // optional string baz = 3;
+ // // Comment attached to baz.
+ // // Another line attached to baz.
+ //
+ // // Comment attached to qux.
+ // //
+ // // Another line attached to qux.
+ // optional double qux = 4;
+ //
+ // optional string corge = 5;
+ // /* Block comment attached
+ // * to corge. Leading asterisks
+ // * will be removed. */
+ // /* Block comment attached to
+ // * grault. */
+ // optional int32 grault = 6;
+ optional string leading_comments = 3;
+ optional string trailing_comments = 4;
+ }
+}
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor_database.cc b/toolkit/components/protobuf/src/google/protobuf/descriptor_database.cc
new file mode 100644
index 0000000000..d024eab13a
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor_database.cc
@@ -0,0 +1,543 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/descriptor_database.h>
+
+#include <set>
+
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/stubs/strutil.h>
+#include <google/protobuf/stubs/stl_util.h>
+#include <google/protobuf/stubs/map_util.h>
+
+namespace google {
+namespace protobuf {
+
+DescriptorDatabase::~DescriptorDatabase() {}
+
+// ===================================================================
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::AddFile(
+ const FileDescriptorProto& file,
+ Value value) {
+ if (!InsertIfNotPresent(&by_name_, file.name(), value)) {
+ GOOGLE_LOG(ERROR) << "File already exists in database: " << file.name();
+ return false;
+ }
+
+ // We must be careful here -- calling file.package() if file.has_package() is
+ // false could access an uninitialized static-storage variable if we are being
+ // run at startup time.
+ string path = file.has_package() ? file.package() : string();
+ if (!path.empty()) path += '.';
+
+ for (int i = 0; i < file.message_type_size(); i++) {
+ if (!AddSymbol(path + file.message_type(i).name(), value)) return false;
+ if (!AddNestedExtensions(file.message_type(i), value)) return false;
+ }
+ for (int i = 0; i < file.enum_type_size(); i++) {
+ if (!AddSymbol(path + file.enum_type(i).name(), value)) return false;
+ }
+ for (int i = 0; i < file.extension_size(); i++) {
+ if (!AddSymbol(path + file.extension(i).name(), value)) return false;
+ if (!AddExtension(file.extension(i), value)) return false;
+ }
+ for (int i = 0; i < file.service_size(); i++) {
+ if (!AddSymbol(path + file.service(i).name(), value)) return false;
+ }
+
+ return true;
+}
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::AddSymbol(
+ const string& name, Value value) {
+ // We need to make sure not to violate our map invariant.
+
+ // If the symbol name is invalid it could break our lookup algorithm (which
+ // relies on the fact that '.' sorts before all other characters that are
+ // valid in symbol names).
+ if (!ValidateSymbolName(name)) {
+ GOOGLE_LOG(ERROR) << "Invalid symbol name: " << name;
+ return false;
+ }
+
+ // Try to look up the symbol to make sure a super-symbol doesn't already
+ // exist.
+ typename map<string, Value>::iterator iter = FindLastLessOrEqual(name);
+
+ if (iter == by_symbol_.end()) {
+ // Apparently the map is currently empty. Just insert and be done with it.
+ by_symbol_.insert(typename map<string, Value>::value_type(name, value));
+ return true;
+ }
+
+ if (IsSubSymbol(iter->first, name)) {
+ GOOGLE_LOG(ERROR) << "Symbol name \"" << name << "\" conflicts with the existing "
+ "symbol \"" << iter->first << "\".";
+ return false;
+ }
+
+ // OK, that worked. Now we have to make sure that no symbol in the map is
+ // a sub-symbol of the one we are inserting. The only symbol which could
+ // be so is the first symbol that is greater than the new symbol. Since
+ // |iter| points at the last symbol that is less than or equal, we just have
+ // to increment it.
+ ++iter;
+
+ if (iter != by_symbol_.end() && IsSubSymbol(name, iter->first)) {
+ GOOGLE_LOG(ERROR) << "Symbol name \"" << name << "\" conflicts with the existing "
+ "symbol \"" << iter->first << "\".";
+ return false;
+ }
+
+ // OK, no conflicts.
+
+ // Insert the new symbol using the iterator as a hint, the new entry will
+ // appear immediately before the one the iterator is pointing at.
+ by_symbol_.insert(iter, typename map<string, Value>::value_type(name, value));
+
+ return true;
+}
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::AddNestedExtensions(
+ const DescriptorProto& message_type,
+ Value value) {
+ for (int i = 0; i < message_type.nested_type_size(); i++) {
+ if (!AddNestedExtensions(message_type.nested_type(i), value)) return false;
+ }
+ for (int i = 0; i < message_type.extension_size(); i++) {
+ if (!AddExtension(message_type.extension(i), value)) return false;
+ }
+ return true;
+}
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::AddExtension(
+ const FieldDescriptorProto& field,
+ Value value) {
+ if (!field.extendee().empty() && field.extendee()[0] == '.') {
+ // The extension is fully-qualified. We can use it as a lookup key in
+ // the by_symbol_ table.
+ if (!InsertIfNotPresent(&by_extension_,
+ make_pair(field.extendee().substr(1),
+ field.number()),
+ value)) {
+ GOOGLE_LOG(ERROR) << "Extension conflicts with extension already in database: "
+ "extend " << field.extendee() << " { "
+ << field.name() << " = " << field.number() << " }";
+ return false;
+ }
+ } else {
+ // Not fully-qualified. We can't really do anything here, unfortunately.
+ // We don't consider this an error, though, because the descriptor is
+ // valid.
+ }
+ return true;
+}
+
+template <typename Value>
+Value SimpleDescriptorDatabase::DescriptorIndex<Value>::FindFile(
+ const string& filename) {
+ return FindWithDefault(by_name_, filename, Value());
+}
+
+template <typename Value>
+Value SimpleDescriptorDatabase::DescriptorIndex<Value>::FindSymbol(
+ const string& name) {
+ typename map<string, Value>::iterator iter = FindLastLessOrEqual(name);
+
+ return (iter != by_symbol_.end() && IsSubSymbol(iter->first, name)) ?
+ iter->second : Value();
+}
+
+template <typename Value>
+Value SimpleDescriptorDatabase::DescriptorIndex<Value>::FindExtension(
+ const string& containing_type,
+ int field_number) {
+ return FindWithDefault(by_extension_,
+ make_pair(containing_type, field_number),
+ Value());
+}
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::FindAllExtensionNumbers(
+ const string& containing_type,
+ vector<int>* output) {
+ typename map<pair<string, int>, Value >::const_iterator it =
+ by_extension_.lower_bound(make_pair(containing_type, 0));
+ bool success = false;
+
+ for (; it != by_extension_.end() && it->first.first == containing_type;
+ ++it) {
+ output->push_back(it->first.second);
+ success = true;
+ }
+
+ return success;
+}
+
+template <typename Value>
+typename map<string, Value>::iterator
+SimpleDescriptorDatabase::DescriptorIndex<Value>::FindLastLessOrEqual(
+ const string& name) {
+ // Find the last key in the map which sorts less than or equal to the
+ // symbol name. Since upper_bound() returns the *first* key that sorts
+ // *greater* than the input, we want the element immediately before that.
+ typename map<string, Value>::iterator iter = by_symbol_.upper_bound(name);
+ if (iter != by_symbol_.begin()) --iter;
+ return iter;
+}
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::IsSubSymbol(
+ const string& sub_symbol, const string& super_symbol) {
+ return sub_symbol == super_symbol ||
+ (HasPrefixString(super_symbol, sub_symbol) &&
+ super_symbol[sub_symbol.size()] == '.');
+}
+
+template <typename Value>
+bool SimpleDescriptorDatabase::DescriptorIndex<Value>::ValidateSymbolName(
+ const string& name) {
+ for (int i = 0; i < name.size(); i++) {
+ // I don't trust ctype.h due to locales. :(
+ if (name[i] != '.' && name[i] != '_' &&
+ (name[i] < '0' || name[i] > '9') &&
+ (name[i] < 'A' || name[i] > 'Z') &&
+ (name[i] < 'a' || name[i] > 'z')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// -------------------------------------------------------------------
+
+SimpleDescriptorDatabase::SimpleDescriptorDatabase() {}
+SimpleDescriptorDatabase::~SimpleDescriptorDatabase() {
+ STLDeleteElements(&files_to_delete_);
+}
+
+bool SimpleDescriptorDatabase::Add(const FileDescriptorProto& file) {
+ FileDescriptorProto* new_file = new FileDescriptorProto;
+ new_file->CopyFrom(file);
+ return AddAndOwn(new_file);
+}
+
+bool SimpleDescriptorDatabase::AddAndOwn(const FileDescriptorProto* file) {
+ files_to_delete_.push_back(file);
+ return index_.AddFile(*file, file);
+}
+
+bool SimpleDescriptorDatabase::FindFileByName(
+ const string& filename,
+ FileDescriptorProto* output) {
+ return MaybeCopy(index_.FindFile(filename), output);
+}
+
+bool SimpleDescriptorDatabase::FindFileContainingSymbol(
+ const string& symbol_name,
+ FileDescriptorProto* output) {
+ return MaybeCopy(index_.FindSymbol(symbol_name), output);
+}
+
+bool SimpleDescriptorDatabase::FindFileContainingExtension(
+ const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output) {
+ return MaybeCopy(index_.FindExtension(containing_type, field_number), output);
+}
+
+bool SimpleDescriptorDatabase::FindAllExtensionNumbers(
+ const string& extendee_type,
+ vector<int>* output) {
+ return index_.FindAllExtensionNumbers(extendee_type, output);
+}
+
+
+bool SimpleDescriptorDatabase::MaybeCopy(const FileDescriptorProto* file,
+ FileDescriptorProto* output) {
+ if (file == NULL) return false;
+ output->CopyFrom(*file);
+ return true;
+}
+
+// -------------------------------------------------------------------
+
+EncodedDescriptorDatabase::EncodedDescriptorDatabase() {}
+EncodedDescriptorDatabase::~EncodedDescriptorDatabase() {
+ for (int i = 0; i < files_to_delete_.size(); i++) {
+ operator delete(files_to_delete_[i]);
+ }
+}
+
+bool EncodedDescriptorDatabase::Add(
+ const void* encoded_file_descriptor, int size) {
+ FileDescriptorProto file;
+ if (file.ParseFromArray(encoded_file_descriptor, size)) {
+ return index_.AddFile(file, make_pair(encoded_file_descriptor, size));
+ } else {
+ GOOGLE_LOG(ERROR) << "Invalid file descriptor data passed to "
+ "EncodedDescriptorDatabase::Add().";
+ return false;
+ }
+}
+
+bool EncodedDescriptorDatabase::AddCopy(
+ const void* encoded_file_descriptor, int size) {
+ void* copy = operator new(size);
+ memcpy(copy, encoded_file_descriptor, size);
+ files_to_delete_.push_back(copy);
+ return Add(copy, size);
+}
+
+bool EncodedDescriptorDatabase::FindFileByName(
+ const string& filename,
+ FileDescriptorProto* output) {
+ return MaybeParse(index_.FindFile(filename), output);
+}
+
+bool EncodedDescriptorDatabase::FindFileContainingSymbol(
+ const string& symbol_name,
+ FileDescriptorProto* output) {
+ return MaybeParse(index_.FindSymbol(symbol_name), output);
+}
+
+bool EncodedDescriptorDatabase::FindNameOfFileContainingSymbol(
+ const string& symbol_name,
+ string* output) {
+ pair<const void*, int> encoded_file = index_.FindSymbol(symbol_name);
+ if (encoded_file.first == NULL) return false;
+
+ // Optimization: The name should be the first field in the encoded message.
+ // Try to just read it directly.
+ io::CodedInputStream input(reinterpret_cast<const uint8*>(encoded_file.first),
+ encoded_file.second);
+
+ const uint32 kNameTag = internal::WireFormatLite::MakeTag(
+ FileDescriptorProto::kNameFieldNumber,
+ internal::WireFormatLite::WIRETYPE_LENGTH_DELIMITED);
+
+ if (input.ReadTag() == kNameTag) {
+ // Success!
+ return internal::WireFormatLite::ReadString(&input, output);
+ } else {
+ // Slow path. Parse whole message.
+ FileDescriptorProto file_proto;
+ if (!file_proto.ParseFromArray(encoded_file.first, encoded_file.second)) {
+ return false;
+ }
+ *output = file_proto.name();
+ return true;
+ }
+}
+
+bool EncodedDescriptorDatabase::FindFileContainingExtension(
+ const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output) {
+ return MaybeParse(index_.FindExtension(containing_type, field_number),
+ output);
+}
+
+bool EncodedDescriptorDatabase::FindAllExtensionNumbers(
+ const string& extendee_type,
+ vector<int>* output) {
+ return index_.FindAllExtensionNumbers(extendee_type, output);
+}
+
+bool EncodedDescriptorDatabase::MaybeParse(
+ pair<const void*, int> encoded_file,
+ FileDescriptorProto* output) {
+ if (encoded_file.first == NULL) return false;
+ return output->ParseFromArray(encoded_file.first, encoded_file.second);
+}
+
+// ===================================================================
+
+DescriptorPoolDatabase::DescriptorPoolDatabase(const DescriptorPool& pool)
+ : pool_(pool) {}
+DescriptorPoolDatabase::~DescriptorPoolDatabase() {}
+
+bool DescriptorPoolDatabase::FindFileByName(
+ const string& filename,
+ FileDescriptorProto* output) {
+ const FileDescriptor* file = pool_.FindFileByName(filename);
+ if (file == NULL) return false;
+ output->Clear();
+ file->CopyTo(output);
+ return true;
+}
+
+bool DescriptorPoolDatabase::FindFileContainingSymbol(
+ const string& symbol_name,
+ FileDescriptorProto* output) {
+ const FileDescriptor* file = pool_.FindFileContainingSymbol(symbol_name);
+ if (file == NULL) return false;
+ output->Clear();
+ file->CopyTo(output);
+ return true;
+}
+
+bool DescriptorPoolDatabase::FindFileContainingExtension(
+ const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output) {
+ const Descriptor* extendee = pool_.FindMessageTypeByName(containing_type);
+ if (extendee == NULL) return false;
+
+ const FieldDescriptor* extension =
+ pool_.FindExtensionByNumber(extendee, field_number);
+ if (extension == NULL) return false;
+
+ output->Clear();
+ extension->file()->CopyTo(output);
+ return true;
+}
+
+bool DescriptorPoolDatabase::FindAllExtensionNumbers(
+ const string& extendee_type,
+ vector<int>* output) {
+ const Descriptor* extendee = pool_.FindMessageTypeByName(extendee_type);
+ if (extendee == NULL) return false;
+
+ vector<const FieldDescriptor*> extensions;
+ pool_.FindAllExtensions(extendee, &extensions);
+
+ for (int i = 0; i < extensions.size(); ++i) {
+ output->push_back(extensions[i]->number());
+ }
+
+ return true;
+}
+
+// ===================================================================
+
+MergedDescriptorDatabase::MergedDescriptorDatabase(
+ DescriptorDatabase* source1,
+ DescriptorDatabase* source2) {
+ sources_.push_back(source1);
+ sources_.push_back(source2);
+}
+MergedDescriptorDatabase::MergedDescriptorDatabase(
+ const vector<DescriptorDatabase*>& sources)
+ : sources_(sources) {}
+MergedDescriptorDatabase::~MergedDescriptorDatabase() {}
+
+bool MergedDescriptorDatabase::FindFileByName(
+ const string& filename,
+ FileDescriptorProto* output) {
+ for (int i = 0; i < sources_.size(); i++) {
+ if (sources_[i]->FindFileByName(filename, output)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MergedDescriptorDatabase::FindFileContainingSymbol(
+ const string& symbol_name,
+ FileDescriptorProto* output) {
+ for (int i = 0; i < sources_.size(); i++) {
+ if (sources_[i]->FindFileContainingSymbol(symbol_name, output)) {
+ // The symbol was found in source i. However, if one of the previous
+ // sources defines a file with the same name (which presumably doesn't
+ // contain the symbol, since it wasn't found in that source), then we
+ // must hide it from the caller.
+ FileDescriptorProto temp;
+ for (int j = 0; j < i; j++) {
+ if (sources_[j]->FindFileByName(output->name(), &temp)) {
+ // Found conflicting file in a previous source.
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MergedDescriptorDatabase::FindFileContainingExtension(
+ const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output) {
+ for (int i = 0; i < sources_.size(); i++) {
+ if (sources_[i]->FindFileContainingExtension(
+ containing_type, field_number, output)) {
+ // The symbol was found in source i. However, if one of the previous
+ // sources defines a file with the same name (which presumably doesn't
+ // contain the symbol, since it wasn't found in that source), then we
+ // must hide it from the caller.
+ FileDescriptorProto temp;
+ for (int j = 0; j < i; j++) {
+ if (sources_[j]->FindFileByName(output->name(), &temp)) {
+ // Found conflicting file in a previous source.
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MergedDescriptorDatabase::FindAllExtensionNumbers(
+ const string& extendee_type,
+ vector<int>* output) {
+ set<int> merged_results;
+ vector<int> results;
+ bool success = false;
+
+ for (int i = 0; i < sources_.size(); i++) {
+ if (sources_[i]->FindAllExtensionNumbers(extendee_type, &results)) {
+ copy(results.begin(), results.end(),
+ insert_iterator<set<int> >(merged_results, merged_results.begin()));
+ success = true;
+ }
+ results.clear();
+ }
+
+ copy(merged_results.begin(), merged_results.end(),
+ insert_iterator<vector<int> >(*output, output->end()));
+
+ return success;
+}
+
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/descriptor_database.h b/toolkit/components/protobuf/src/google/protobuf/descriptor_database.h
new file mode 100644
index 0000000000..934e4022be
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/descriptor_database.h
@@ -0,0 +1,369 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Interface for manipulating databases of descriptors.
+
+#ifndef GOOGLE_PROTOBUF_DESCRIPTOR_DATABASE_H__
+#define GOOGLE_PROTOBUF_DESCRIPTOR_DATABASE_H__
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/descriptor.h>
+
+namespace google {
+namespace protobuf {
+
+// Defined in this file.
+class DescriptorDatabase;
+class SimpleDescriptorDatabase;
+class EncodedDescriptorDatabase;
+class DescriptorPoolDatabase;
+class MergedDescriptorDatabase;
+
+// Abstract interface for a database of descriptors.
+//
+// This is useful if you want to create a DescriptorPool which loads
+// descriptors on-demand from some sort of large database. If the database
+// is large, it may be inefficient to enumerate every .proto file inside it
+// calling DescriptorPool::BuildFile() for each one. Instead, a DescriptorPool
+// can be created which wraps a DescriptorDatabase and only builds particular
+// descriptors when they are needed.
+class LIBPROTOBUF_EXPORT DescriptorDatabase {
+ public:
+ inline DescriptorDatabase() {}
+ virtual ~DescriptorDatabase();
+
+ // Find a file by file name. Fills in in *output and returns true if found.
+ // Otherwise, returns false, leaving the contents of *output undefined.
+ virtual bool FindFileByName(const string& filename,
+ FileDescriptorProto* output) = 0;
+
+ // Find the file that declares the given fully-qualified symbol name.
+ // If found, fills in *output and returns true, otherwise returns false
+ // and leaves *output undefined.
+ virtual bool FindFileContainingSymbol(const string& symbol_name,
+ FileDescriptorProto* output) = 0;
+
+ // Find the file which defines an extension extending the given message type
+ // with the given field number. If found, fills in *output and returns true,
+ // otherwise returns false and leaves *output undefined. containing_type
+ // must be a fully-qualified type name.
+ virtual bool FindFileContainingExtension(const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output) = 0;
+
+ // Finds the tag numbers used by all known extensions of
+ // extendee_type, and appends them to output in an undefined
+ // order. This method is best-effort: it's not guaranteed that the
+ // database will find all extensions, and it's not guaranteed that
+ // FindFileContainingExtension will return true on all of the found
+ // numbers. Returns true if the search was successful, otherwise
+ // returns false and leaves output unchanged.
+ //
+ // This method has a default implementation that always returns
+ // false.
+ virtual bool FindAllExtensionNumbers(const string& /* extendee_type */,
+ vector<int>* /* output */) {
+ return false;
+ }
+
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DescriptorDatabase);
+};
+
+// A DescriptorDatabase into which you can insert files manually.
+//
+// FindFileContainingSymbol() is fully-implemented. When you add a file, its
+// symbols will be indexed for this purpose. Note that the implementation
+// may return false positives, but only if it isn't possible for the symbol
+// to be defined in any other file. In particular, if a file defines a symbol
+// "Foo", then searching for "Foo.[anything]" will match that file. This way,
+// the database does not need to aggressively index all children of a symbol.
+//
+// FindFileContainingExtension() is mostly-implemented. It works if and only
+// if the original FieldDescriptorProto defining the extension has a
+// fully-qualified type name in its "extendee" field (i.e. starts with a '.').
+// If the extendee is a relative name, SimpleDescriptorDatabase will not
+// attempt to resolve the type, so it will not know what type the extension is
+// extending. Therefore, calling FindFileContainingExtension() with the
+// extension's containing type will never actually find that extension. Note
+// that this is an unlikely problem, as all FileDescriptorProtos created by the
+// protocol compiler (as well as ones created by calling
+// FileDescriptor::CopyTo()) will always use fully-qualified names for all
+// types. You only need to worry if you are constructing FileDescriptorProtos
+// yourself, or are calling compiler::Parser directly.
+class LIBPROTOBUF_EXPORT SimpleDescriptorDatabase : public DescriptorDatabase {
+ public:
+ SimpleDescriptorDatabase();
+ ~SimpleDescriptorDatabase();
+
+ // Adds the FileDescriptorProto to the database, making a copy. The object
+ // can be deleted after Add() returns. Returns false if the file conflicted
+ // with a file already in the database, in which case an error will have
+ // been written to GOOGLE_LOG(ERROR).
+ bool Add(const FileDescriptorProto& file);
+
+ // Adds the FileDescriptorProto to the database and takes ownership of it.
+ bool AddAndOwn(const FileDescriptorProto* file);
+
+ // implements DescriptorDatabase -----------------------------------
+ bool FindFileByName(const string& filename,
+ FileDescriptorProto* output);
+ bool FindFileContainingSymbol(const string& symbol_name,
+ FileDescriptorProto* output);
+ bool FindFileContainingExtension(const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output);
+ bool FindAllExtensionNumbers(const string& extendee_type,
+ vector<int>* output);
+
+ private:
+ // So that it can use DescriptorIndex.
+ friend class EncodedDescriptorDatabase;
+
+ // An index mapping file names, symbol names, and extension numbers to
+ // some sort of values.
+ template <typename Value>
+ class DescriptorIndex {
+ public:
+ // Helpers to recursively add particular descriptors and all their contents
+ // to the index.
+ bool AddFile(const FileDescriptorProto& file,
+ Value value);
+ bool AddSymbol(const string& name, Value value);
+ bool AddNestedExtensions(const DescriptorProto& message_type,
+ Value value);
+ bool AddExtension(const FieldDescriptorProto& field,
+ Value value);
+
+ Value FindFile(const string& filename);
+ Value FindSymbol(const string& name);
+ Value FindExtension(const string& containing_type, int field_number);
+ bool FindAllExtensionNumbers(const string& containing_type,
+ vector<int>* output);
+
+ private:
+ map<string, Value> by_name_;
+ map<string, Value> by_symbol_;
+ map<pair<string, int>, Value> by_extension_;
+
+ // Invariant: The by_symbol_ map does not contain any symbols which are
+ // prefixes of other symbols in the map. For example, "foo.bar" is a
+ // prefix of "foo.bar.baz" (but is not a prefix of "foo.barbaz").
+ //
+ // This invariant is important because it means that given a symbol name,
+ // we can find a key in the map which is a prefix of the symbol in O(lg n)
+ // time, and we know that there is at most one such key.
+ //
+ // The prefix lookup algorithm works like so:
+ // 1) Find the last key in the map which is less than or equal to the
+ // search key.
+ // 2) If the found key is a prefix of the search key, then return it.
+ // Otherwise, there is no match.
+ //
+ // I am sure this algorithm has been described elsewhere, but since I
+ // wasn't able to find it quickly I will instead prove that it works
+ // myself. The key to the algorithm is that if a match exists, step (1)
+ // will find it. Proof:
+ // 1) Define the "search key" to be the key we are looking for, the "found
+ // key" to be the key found in step (1), and the "match key" to be the
+ // key which actually matches the serach key (i.e. the key we're trying
+ // to find).
+ // 2) The found key must be less than or equal to the search key by
+ // definition.
+ // 3) The match key must also be less than or equal to the search key
+ // (because it is a prefix).
+ // 4) The match key cannot be greater than the found key, because if it
+ // were, then step (1) of the algorithm would have returned the match
+ // key instead (since it finds the *greatest* key which is less than or
+ // equal to the search key).
+ // 5) Therefore, the found key must be between the match key and the search
+ // key, inclusive.
+ // 6) Since the search key must be a sub-symbol of the match key, if it is
+ // not equal to the match key, then search_key[match_key.size()] must
+ // be '.'.
+ // 7) Since '.' sorts before any other character that is valid in a symbol
+ // name, then if the found key is not equal to the match key, then
+ // found_key[match_key.size()] must also be '.', because any other value
+ // would make it sort after the search key.
+ // 8) Therefore, if the found key is not equal to the match key, then the
+ // found key must be a sub-symbol of the match key. However, this would
+ // contradict our map invariant which says that no symbol in the map is
+ // a sub-symbol of any other.
+ // 9) Therefore, the found key must match the match key.
+ //
+ // The above proof assumes the match key exists. In the case that the
+ // match key does not exist, then step (1) will return some other symbol.
+ // That symbol cannot be a super-symbol of the search key since if it were,
+ // then it would be a match, and we're assuming the match key doesn't exist.
+ // Therefore, step 2 will correctly return no match.
+
+ // Find the last entry in the by_symbol_ map whose key is less than or
+ // equal to the given name.
+ typename map<string, Value>::iterator FindLastLessOrEqual(
+ const string& name);
+
+ // True if either the arguments are equal or super_symbol identifies a
+ // parent symbol of sub_symbol (e.g. "foo.bar" is a parent of
+ // "foo.bar.baz", but not a parent of "foo.barbaz").
+ bool IsSubSymbol(const string& sub_symbol, const string& super_symbol);
+
+ // Returns true if and only if all characters in the name are alphanumerics,
+ // underscores, or periods.
+ bool ValidateSymbolName(const string& name);
+ };
+
+
+ DescriptorIndex<const FileDescriptorProto*> index_;
+ vector<const FileDescriptorProto*> files_to_delete_;
+
+ // If file is non-NULL, copy it into *output and return true, otherwise
+ // return false.
+ bool MaybeCopy(const FileDescriptorProto* file,
+ FileDescriptorProto* output);
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(SimpleDescriptorDatabase);
+};
+
+// Very similar to SimpleDescriptorDatabase, but stores all the descriptors
+// as raw bytes and generally tries to use as little memory as possible.
+//
+// The same caveats regarding FindFileContainingExtension() apply as with
+// SimpleDescriptorDatabase.
+class LIBPROTOBUF_EXPORT EncodedDescriptorDatabase : public DescriptorDatabase {
+ public:
+ EncodedDescriptorDatabase();
+ ~EncodedDescriptorDatabase();
+
+ // Adds the FileDescriptorProto to the database. The descriptor is provided
+ // in encoded form. The database does not make a copy of the bytes, nor
+ // does it take ownership; it's up to the caller to make sure the bytes
+ // remain valid for the life of the database. Returns false and logs an error
+ // if the bytes are not a valid FileDescriptorProto or if the file conflicted
+ // with a file already in the database.
+ bool Add(const void* encoded_file_descriptor, int size);
+
+ // Like Add(), but makes a copy of the data, so that the caller does not
+ // need to keep it around.
+ bool AddCopy(const void* encoded_file_descriptor, int size);
+
+ // Like FindFileContainingSymbol but returns only the name of the file.
+ bool FindNameOfFileContainingSymbol(const string& symbol_name,
+ string* output);
+
+ // implements DescriptorDatabase -----------------------------------
+ bool FindFileByName(const string& filename,
+ FileDescriptorProto* output);
+ bool FindFileContainingSymbol(const string& symbol_name,
+ FileDescriptorProto* output);
+ bool FindFileContainingExtension(const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output);
+ bool FindAllExtensionNumbers(const string& extendee_type,
+ vector<int>* output);
+
+ private:
+ SimpleDescriptorDatabase::DescriptorIndex<pair<const void*, int> > index_;
+ vector<void*> files_to_delete_;
+
+ // If encoded_file.first is non-NULL, parse the data into *output and return
+ // true, otherwise return false.
+ bool MaybeParse(pair<const void*, int> encoded_file,
+ FileDescriptorProto* output);
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(EncodedDescriptorDatabase);
+};
+
+// A DescriptorDatabase that fetches files from a given pool.
+class LIBPROTOBUF_EXPORT DescriptorPoolDatabase : public DescriptorDatabase {
+ public:
+ DescriptorPoolDatabase(const DescriptorPool& pool);
+ ~DescriptorPoolDatabase();
+
+ // implements DescriptorDatabase -----------------------------------
+ bool FindFileByName(const string& filename,
+ FileDescriptorProto* output);
+ bool FindFileContainingSymbol(const string& symbol_name,
+ FileDescriptorProto* output);
+ bool FindFileContainingExtension(const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output);
+ bool FindAllExtensionNumbers(const string& extendee_type,
+ vector<int>* output);
+
+ private:
+ const DescriptorPool& pool_;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DescriptorPoolDatabase);
+};
+
+// A DescriptorDatabase that wraps two or more others. It first searches the
+// first database and, if that fails, tries the second, and so on.
+class LIBPROTOBUF_EXPORT MergedDescriptorDatabase : public DescriptorDatabase {
+ public:
+ // Merge just two databases. The sources remain property of the caller.
+ MergedDescriptorDatabase(DescriptorDatabase* source1,
+ DescriptorDatabase* source2);
+ // Merge more than two databases. The sources remain property of the caller.
+ // The vector may be deleted after the constructor returns but the
+ // DescriptorDatabases need to stick around.
+ MergedDescriptorDatabase(const vector<DescriptorDatabase*>& sources);
+ ~MergedDescriptorDatabase();
+
+ // implements DescriptorDatabase -----------------------------------
+ bool FindFileByName(const string& filename,
+ FileDescriptorProto* output);
+ bool FindFileContainingSymbol(const string& symbol_name,
+ FileDescriptorProto* output);
+ bool FindFileContainingExtension(const string& containing_type,
+ int field_number,
+ FileDescriptorProto* output);
+ // Merges the results of calling all databases. Returns true iff any
+ // of the databases returned true.
+ bool FindAllExtensionNumbers(const string& extendee_type,
+ vector<int>* output);
+
+
+ private:
+ vector<DescriptorDatabase*> sources_;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MergedDescriptorDatabase);
+};
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_DESCRIPTOR_DATABASE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/dynamic_message.cc b/toolkit/components/protobuf/src/google/protobuf/dynamic_message.cc
new file mode 100644
index 0000000000..4cca98691b
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/dynamic_message.cc
@@ -0,0 +1,764 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// DynamicMessage is implemented by constructing a data structure which
+// has roughly the same memory layout as a generated message would have.
+// Then, we use GeneratedMessageReflection to implement our reflection
+// interface. All the other operations we need to implement (e.g.
+// parsing, copying, etc.) are already implemented in terms of
+// Reflection, so the rest is easy.
+//
+// The up side of this strategy is that it's very efficient. We don't
+// need to use hash_maps or generic representations of fields. The
+// down side is that this is a low-level memory management hack which
+// can be tricky to get right.
+//
+// As mentioned in the header, we only expose a DynamicMessageFactory
+// publicly, not the DynamicMessage class itself. This is because
+// GenericMessageReflection wants to have a pointer to a "default"
+// copy of the class, with all fields initialized to their default
+// values. We only want to construct one of these per message type,
+// so DynamicMessageFactory stores a cache of default messages for
+// each type it sees (each unique Descriptor pointer). The code
+// refers to the "default" copy of the class as the "prototype".
+//
+// Note on memory allocation: This module often calls "operator new()"
+// to allocate untyped memory, rather than calling something like
+// "new uint8[]". This is because "operator new()" means "Give me some
+// space which I can use as I please." while "new uint8[]" means "Give
+// me an array of 8-bit integers.". In practice, the later may return
+// a pointer that is not aligned correctly for general use. I believe
+// Item 8 of "More Effective C++" discusses this in more detail, though
+// I don't have the book on me right now so I'm not sure.
+
+#include <algorithm>
+#include <google/protobuf/stubs/hash.h>
+
+#include <google/protobuf/stubs/common.h>
+
+#include <google/protobuf/dynamic_message.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/generated_message_reflection.h>
+#include <google/protobuf/reflection_ops.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/wire_format.h>
+
+namespace google {
+namespace protobuf {
+
+using internal::WireFormat;
+using internal::ExtensionSet;
+using internal::GeneratedMessageReflection;
+
+
+// ===================================================================
+// Some helper tables and functions...
+
+namespace {
+
+// Compute the byte size of the in-memory representation of the field.
+int FieldSpaceUsed(const FieldDescriptor* field) {
+ typedef FieldDescriptor FD; // avoid line wrapping
+ if (field->label() == FD::LABEL_REPEATED) {
+ switch (field->cpp_type()) {
+ case FD::CPPTYPE_INT32 : return sizeof(RepeatedField<int32 >);
+ case FD::CPPTYPE_INT64 : return sizeof(RepeatedField<int64 >);
+ case FD::CPPTYPE_UINT32 : return sizeof(RepeatedField<uint32 >);
+ case FD::CPPTYPE_UINT64 : return sizeof(RepeatedField<uint64 >);
+ case FD::CPPTYPE_DOUBLE : return sizeof(RepeatedField<double >);
+ case FD::CPPTYPE_FLOAT : return sizeof(RepeatedField<float >);
+ case FD::CPPTYPE_BOOL : return sizeof(RepeatedField<bool >);
+ case FD::CPPTYPE_ENUM : return sizeof(RepeatedField<int >);
+ case FD::CPPTYPE_MESSAGE: return sizeof(RepeatedPtrField<Message>);
+
+ case FD::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ return sizeof(RepeatedPtrField<string>);
+ }
+ break;
+ }
+ } else {
+ switch (field->cpp_type()) {
+ case FD::CPPTYPE_INT32 : return sizeof(int32 );
+ case FD::CPPTYPE_INT64 : return sizeof(int64 );
+ case FD::CPPTYPE_UINT32 : return sizeof(uint32 );
+ case FD::CPPTYPE_UINT64 : return sizeof(uint64 );
+ case FD::CPPTYPE_DOUBLE : return sizeof(double );
+ case FD::CPPTYPE_FLOAT : return sizeof(float );
+ case FD::CPPTYPE_BOOL : return sizeof(bool );
+ case FD::CPPTYPE_ENUM : return sizeof(int );
+
+ case FD::CPPTYPE_MESSAGE:
+ return sizeof(Message*);
+
+ case FD::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ return sizeof(string*);
+ }
+ break;
+ }
+ }
+
+ GOOGLE_LOG(DFATAL) << "Can't get here.";
+ return 0;
+}
+
+// Compute the byte size of in-memory representation of the oneof fields
+// in default oneof instance.
+int OneofFieldSpaceUsed(const FieldDescriptor* field) {
+ typedef FieldDescriptor FD; // avoid line wrapping
+ switch (field->cpp_type()) {
+ case FD::CPPTYPE_INT32 : return sizeof(int32 );
+ case FD::CPPTYPE_INT64 : return sizeof(int64 );
+ case FD::CPPTYPE_UINT32 : return sizeof(uint32 );
+ case FD::CPPTYPE_UINT64 : return sizeof(uint64 );
+ case FD::CPPTYPE_DOUBLE : return sizeof(double );
+ case FD::CPPTYPE_FLOAT : return sizeof(float );
+ case FD::CPPTYPE_BOOL : return sizeof(bool );
+ case FD::CPPTYPE_ENUM : return sizeof(int );
+
+ case FD::CPPTYPE_MESSAGE:
+ return sizeof(Message*);
+
+ case FD::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default:
+ case FieldOptions::STRING:
+ return sizeof(string*);
+ }
+ break;
+ }
+
+ GOOGLE_LOG(DFATAL) << "Can't get here.";
+ return 0;
+}
+
+inline int DivideRoundingUp(int i, int j) {
+ return (i + (j - 1)) / j;
+}
+
+static const int kSafeAlignment = sizeof(uint64);
+static const int kMaxOneofUnionSize = sizeof(uint64);
+
+inline int AlignTo(int offset, int alignment) {
+ return DivideRoundingUp(offset, alignment) * alignment;
+}
+
+// Rounds the given byte offset up to the next offset aligned such that any
+// type may be stored at it.
+inline int AlignOffset(int offset) {
+ return AlignTo(offset, kSafeAlignment);
+}
+
+#define bitsizeof(T) (sizeof(T) * 8)
+
+} // namespace
+
+// ===================================================================
+
+class DynamicMessage : public Message {
+ public:
+ struct TypeInfo {
+ int size;
+ int has_bits_offset;
+ int oneof_case_offset;
+ int unknown_fields_offset;
+ int extensions_offset;
+
+ // Not owned by the TypeInfo.
+ DynamicMessageFactory* factory; // The factory that created this object.
+ const DescriptorPool* pool; // The factory's DescriptorPool.
+ const Descriptor* type; // Type of this DynamicMessage.
+
+ // Warning: The order in which the following pointers are defined is
+ // important (the prototype must be deleted *before* the offsets).
+ scoped_array<int> offsets;
+ scoped_ptr<const GeneratedMessageReflection> reflection;
+ // Don't use a scoped_ptr to hold the prototype: the destructor for
+ // DynamicMessage needs to know whether it is the prototype, and does so by
+ // looking back at this field. This would assume details about the
+ // implementation of scoped_ptr.
+ const DynamicMessage* prototype;
+ void* default_oneof_instance;
+
+ TypeInfo() : prototype(NULL), default_oneof_instance(NULL) {}
+
+ ~TypeInfo() {
+ delete prototype;
+ operator delete(default_oneof_instance);
+ }
+ };
+
+ DynamicMessage(const TypeInfo* type_info);
+ ~DynamicMessage();
+
+ // Called on the prototype after construction to initialize message fields.
+ void CrossLinkPrototypes();
+
+ // implements Message ----------------------------------------------
+
+ Message* New() const;
+
+ int GetCachedSize() const;
+ void SetCachedSize(int size) const;
+
+ Metadata GetMetadata() const;
+
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DynamicMessage);
+
+ inline bool is_prototype() const {
+ return type_info_->prototype == this ||
+ // If type_info_->prototype is NULL, then we must be constructing
+ // the prototype now, which means we must be the prototype.
+ type_info_->prototype == NULL;
+ }
+
+ inline void* OffsetToPointer(int offset) {
+ return reinterpret_cast<uint8*>(this) + offset;
+ }
+ inline const void* OffsetToPointer(int offset) const {
+ return reinterpret_cast<const uint8*>(this) + offset;
+ }
+
+ const TypeInfo* type_info_;
+
+ // TODO(kenton): Make this an atomic<int> when C++ supports it.
+ mutable int cached_byte_size_;
+};
+
+DynamicMessage::DynamicMessage(const TypeInfo* type_info)
+ : type_info_(type_info),
+ cached_byte_size_(0) {
+ // We need to call constructors for various fields manually and set
+ // default values where appropriate. We use placement new to call
+ // constructors. If you haven't heard of placement new, I suggest Googling
+ // it now. We use placement new even for primitive types that don't have
+ // constructors for consistency. (In theory, placement new should be used
+ // any time you are trying to convert untyped memory to typed memory, though
+ // in practice that's not strictly necessary for types that don't have a
+ // constructor.)
+
+ const Descriptor* descriptor = type_info_->type;
+
+ // Initialize oneof cases.
+ for (int i = 0 ; i < descriptor->oneof_decl_count(); ++i) {
+ new(OffsetToPointer(type_info_->oneof_case_offset + sizeof(uint32) * i))
+ uint32(0);
+ }
+
+ new(OffsetToPointer(type_info_->unknown_fields_offset)) UnknownFieldSet;
+
+ if (type_info_->extensions_offset != -1) {
+ new(OffsetToPointer(type_info_->extensions_offset)) ExtensionSet;
+ }
+
+ for (int i = 0; i < descriptor->field_count(); i++) {
+ const FieldDescriptor* field = descriptor->field(i);
+ void* field_ptr = OffsetToPointer(type_info_->offsets[i]);
+ if (field->containing_oneof()) {
+ continue;
+ }
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ if (!field->is_repeated()) { \
+ new(field_ptr) TYPE(field->default_value_##TYPE()); \
+ } else { \
+ new(field_ptr) RepeatedField<TYPE>(); \
+ } \
+ break;
+
+ HANDLE_TYPE(INT32 , int32 );
+ HANDLE_TYPE(INT64 , int64 );
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE(FLOAT , float );
+ HANDLE_TYPE(BOOL , bool );
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_ENUM:
+ if (!field->is_repeated()) {
+ new(field_ptr) int(field->default_value_enum()->number());
+ } else {
+ new(field_ptr) RepeatedField<int>();
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ if (!field->is_repeated()) {
+ if (is_prototype()) {
+ new(field_ptr) const string*(&field->default_value_string());
+ } else {
+ string* default_value =
+ *reinterpret_cast<string* const*>(
+ type_info_->prototype->OffsetToPointer(
+ type_info_->offsets[i]));
+ new(field_ptr) string*(default_value);
+ }
+ } else {
+ new(field_ptr) RepeatedPtrField<string>();
+ }
+ break;
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_MESSAGE: {
+ if (!field->is_repeated()) {
+ new(field_ptr) Message*(NULL);
+ } else {
+ new(field_ptr) RepeatedPtrField<Message>();
+ }
+ break;
+ }
+ }
+ }
+}
+
+DynamicMessage::~DynamicMessage() {
+ const Descriptor* descriptor = type_info_->type;
+
+ reinterpret_cast<UnknownFieldSet*>(
+ OffsetToPointer(type_info_->unknown_fields_offset))->~UnknownFieldSet();
+
+ if (type_info_->extensions_offset != -1) {
+ reinterpret_cast<ExtensionSet*>(
+ OffsetToPointer(type_info_->extensions_offset))->~ExtensionSet();
+ }
+
+ // We need to manually run the destructors for repeated fields and strings,
+ // just as we ran their constructors in the the DynamicMessage constructor.
+ // We also need to manually delete oneof fields if it is set and is string
+ // or message.
+ // Additionally, if any singular embedded messages have been allocated, we
+ // need to delete them, UNLESS we are the prototype message of this type,
+ // in which case any embedded messages are other prototypes and shouldn't
+ // be touched.
+ for (int i = 0; i < descriptor->field_count(); i++) {
+ const FieldDescriptor* field = descriptor->field(i);
+ if (field->containing_oneof()) {
+ void* field_ptr = OffsetToPointer(
+ type_info_->oneof_case_offset
+ + sizeof(uint32) * field->containing_oneof()->index());
+ if (*(reinterpret_cast<const uint32*>(field_ptr)) ==
+ field->number()) {
+ field_ptr = OffsetToPointer(type_info_->offsets[
+ descriptor->field_count() + field->containing_oneof()->index()]);
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_STRING) {
+ switch (field->options().ctype()) {
+ default:
+ case FieldOptions::STRING:
+ delete *reinterpret_cast<string**>(field_ptr);
+ break;
+ }
+ } else if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ delete *reinterpret_cast<Message**>(field_ptr);
+ }
+ }
+ continue;
+ }
+ void* field_ptr = OffsetToPointer(type_info_->offsets[i]);
+
+ if (field->is_repeated()) {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE : \
+ reinterpret_cast<RepeatedField<LOWERCASE>*>(field_ptr) \
+ ->~RepeatedField<LOWERCASE>(); \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, int);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ reinterpret_cast<RepeatedPtrField<string>*>(field_ptr)
+ ->~RepeatedPtrField<string>();
+ break;
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ reinterpret_cast<RepeatedPtrField<Message>*>(field_ptr)
+ ->~RepeatedPtrField<Message>();
+ break;
+ }
+
+ } else if (field->cpp_type() == FieldDescriptor::CPPTYPE_STRING) {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING: {
+ string* ptr = *reinterpret_cast<string**>(field_ptr);
+ if (ptr != &field->default_value_string()) {
+ delete ptr;
+ }
+ break;
+ }
+ }
+ } else if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ if (!is_prototype()) {
+ Message* message = *reinterpret_cast<Message**>(field_ptr);
+ if (message != NULL) {
+ delete message;
+ }
+ }
+ }
+ }
+}
+
+void DynamicMessage::CrossLinkPrototypes() {
+ // This should only be called on the prototype message.
+ GOOGLE_CHECK(is_prototype());
+
+ DynamicMessageFactory* factory = type_info_->factory;
+ const Descriptor* descriptor = type_info_->type;
+
+ // Cross-link default messages.
+ for (int i = 0; i < descriptor->field_count(); i++) {
+ const FieldDescriptor* field = descriptor->field(i);
+ void* field_ptr = OffsetToPointer(type_info_->offsets[i]);
+ if (field->containing_oneof()) {
+ field_ptr = reinterpret_cast<uint8*>(
+ type_info_->default_oneof_instance) + type_info_->offsets[i];
+ }
+
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE &&
+ !field->is_repeated()) {
+ // For fields with message types, we need to cross-link with the
+ // prototype for the field's type.
+ // For singular fields, the field is just a pointer which should
+ // point to the prototype.
+ *reinterpret_cast<const Message**>(field_ptr) =
+ factory->GetPrototypeNoLock(field->message_type());
+ }
+ }
+}
+
+Message* DynamicMessage::New() const {
+ void* new_base = operator new(type_info_->size);
+ memset(new_base, 0, type_info_->size);
+ return new(new_base) DynamicMessage(type_info_);
+}
+
+int DynamicMessage::GetCachedSize() const {
+ return cached_byte_size_;
+}
+
+void DynamicMessage::SetCachedSize(int size) const {
+ // This is theoretically not thread-compatible, but in practice it works
+ // because if multiple threads write this simultaneously, they will be
+ // writing the exact same value.
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ cached_byte_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+
+Metadata DynamicMessage::GetMetadata() const {
+ Metadata metadata;
+ metadata.descriptor = type_info_->type;
+ metadata.reflection = type_info_->reflection.get();
+ return metadata;
+}
+
+// ===================================================================
+
+struct DynamicMessageFactory::PrototypeMap {
+ typedef hash_map<const Descriptor*, const DynamicMessage::TypeInfo*> Map;
+ Map map_;
+};
+
+DynamicMessageFactory::DynamicMessageFactory()
+ : pool_(NULL), delegate_to_generated_factory_(false),
+ prototypes_(new PrototypeMap) {
+}
+
+DynamicMessageFactory::DynamicMessageFactory(const DescriptorPool* pool)
+ : pool_(pool), delegate_to_generated_factory_(false),
+ prototypes_(new PrototypeMap) {
+}
+
+DynamicMessageFactory::~DynamicMessageFactory() {
+ for (PrototypeMap::Map::iterator iter = prototypes_->map_.begin();
+ iter != prototypes_->map_.end(); ++iter) {
+ DeleteDefaultOneofInstance(iter->second->type,
+ iter->second->offsets.get(),
+ iter->second->default_oneof_instance);
+ delete iter->second;
+ }
+}
+
+const Message* DynamicMessageFactory::GetPrototype(const Descriptor* type) {
+ MutexLock lock(&prototypes_mutex_);
+ return GetPrototypeNoLock(type);
+}
+
+const Message* DynamicMessageFactory::GetPrototypeNoLock(
+ const Descriptor* type) {
+ if (delegate_to_generated_factory_ &&
+ type->file()->pool() == DescriptorPool::generated_pool()) {
+ return MessageFactory::generated_factory()->GetPrototype(type);
+ }
+
+ const DynamicMessage::TypeInfo** target = &prototypes_->map_[type];
+ if (*target != NULL) {
+ // Already exists.
+ return (*target)->prototype;
+ }
+
+ DynamicMessage::TypeInfo* type_info = new DynamicMessage::TypeInfo;
+ *target = type_info;
+
+ type_info->type = type;
+ type_info->pool = (pool_ == NULL) ? type->file()->pool() : pool_;
+ type_info->factory = this;
+
+ // We need to construct all the structures passed to
+ // GeneratedMessageReflection's constructor. This includes:
+ // - A block of memory that contains space for all the message's fields.
+ // - An array of integers indicating the byte offset of each field within
+ // this block.
+ // - A big bitfield containing a bit for each field indicating whether
+ // or not that field is set.
+
+ // Compute size and offsets.
+ int* offsets = new int[type->field_count() + type->oneof_decl_count()];
+ type_info->offsets.reset(offsets);
+
+ // Decide all field offsets by packing in order.
+ // We place the DynamicMessage object itself at the beginning of the allocated
+ // space.
+ int size = sizeof(DynamicMessage);
+ size = AlignOffset(size);
+
+ // Next the has_bits, which is an array of uint32s.
+ type_info->has_bits_offset = size;
+ int has_bits_array_size =
+ DivideRoundingUp(type->field_count(), bitsizeof(uint32));
+ size += has_bits_array_size * sizeof(uint32);
+ size = AlignOffset(size);
+
+ // The oneof_case, if any. It is an array of uint32s.
+ if (type->oneof_decl_count() > 0) {
+ type_info->oneof_case_offset = size;
+ size += type->oneof_decl_count() * sizeof(uint32);
+ size = AlignOffset(size);
+ }
+
+ // The ExtensionSet, if any.
+ if (type->extension_range_count() > 0) {
+ type_info->extensions_offset = size;
+ size += sizeof(ExtensionSet);
+ size = AlignOffset(size);
+ } else {
+ // No extensions.
+ type_info->extensions_offset = -1;
+ }
+
+ // All the fields.
+ for (int i = 0; i < type->field_count(); i++) {
+ // Make sure field is aligned to avoid bus errors.
+ // Oneof fields do not use any space.
+ if (!type->field(i)->containing_oneof()) {
+ int field_size = FieldSpaceUsed(type->field(i));
+ size = AlignTo(size, min(kSafeAlignment, field_size));
+ offsets[i] = size;
+ size += field_size;
+ }
+ }
+
+ // The oneofs.
+ for (int i = 0; i < type->oneof_decl_count(); i++) {
+ size = AlignTo(size, kSafeAlignment);
+ offsets[type->field_count() + i] = size;
+ size += kMaxOneofUnionSize;
+ }
+
+ // Add the UnknownFieldSet to the end.
+ size = AlignOffset(size);
+ type_info->unknown_fields_offset = size;
+ size += sizeof(UnknownFieldSet);
+
+ // Align the final size to make sure no clever allocators think that
+ // alignment is not necessary.
+ size = AlignOffset(size);
+ type_info->size = size;
+
+ // Allocate the prototype.
+ void* base = operator new(size);
+ memset(base, 0, size);
+ DynamicMessage* prototype = new(base) DynamicMessage(type_info);
+ type_info->prototype = prototype;
+
+ // Construct the reflection object.
+ if (type->oneof_decl_count() > 0) {
+ // Compute the size of default oneof instance and offsets of default
+ // oneof fields.
+ int oneof_size = 0;
+ for (int i = 0; i < type->oneof_decl_count(); i++) {
+ for (int j = 0; j < type->oneof_decl(i)->field_count(); j++) {
+ const FieldDescriptor* field = type->oneof_decl(i)->field(j);
+ int field_size = OneofFieldSpaceUsed(field);
+ oneof_size = AlignTo(oneof_size, min(kSafeAlignment, field_size));
+ offsets[field->index()] = oneof_size;
+ oneof_size += field_size;
+ }
+ }
+ // Construct default oneof instance.
+ type_info->default_oneof_instance = ::operator new(oneof_size);
+ ConstructDefaultOneofInstance(type_info->type,
+ type_info->offsets.get(),
+ type_info->default_oneof_instance);
+ type_info->reflection.reset(
+ new GeneratedMessageReflection(
+ type_info->type,
+ type_info->prototype,
+ type_info->offsets.get(),
+ type_info->has_bits_offset,
+ type_info->unknown_fields_offset,
+ type_info->extensions_offset,
+ type_info->default_oneof_instance,
+ type_info->oneof_case_offset,
+ type_info->pool,
+ this,
+ type_info->size));
+ } else {
+ type_info->reflection.reset(
+ new GeneratedMessageReflection(
+ type_info->type,
+ type_info->prototype,
+ type_info->offsets.get(),
+ type_info->has_bits_offset,
+ type_info->unknown_fields_offset,
+ type_info->extensions_offset,
+ type_info->pool,
+ this,
+ type_info->size));
+ }
+ // Cross link prototypes.
+ prototype->CrossLinkPrototypes();
+
+ return prototype;
+}
+
+void DynamicMessageFactory::ConstructDefaultOneofInstance(
+ const Descriptor* type,
+ const int offsets[],
+ void* default_oneof_instance) {
+ for (int i = 0; i < type->oneof_decl_count(); i++) {
+ for (int j = 0; j < type->oneof_decl(i)->field_count(); j++) {
+ const FieldDescriptor* field = type->oneof_decl(i)->field(j);
+ void* field_ptr = reinterpret_cast<uint8*>(
+ default_oneof_instance) + offsets[field->index()];
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ new(field_ptr) TYPE(field->default_value_##TYPE()); \
+ break;
+
+ HANDLE_TYPE(INT32 , int32 );
+ HANDLE_TYPE(INT64 , int64 );
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE(FLOAT , float );
+ HANDLE_TYPE(BOOL , bool );
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_ENUM:
+ new(field_ptr) int(field->default_value_enum()->number());
+ break;
+ case FieldDescriptor::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default:
+ case FieldOptions::STRING:
+ if (field->has_default_value()) {
+ new(field_ptr) const string*(&field->default_value_string());
+ } else {
+ new(field_ptr) string*(
+ const_cast<string*>(&internal::GetEmptyString()));
+ }
+ break;
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_MESSAGE: {
+ new(field_ptr) Message*(NULL);
+ break;
+ }
+ }
+ }
+ }
+}
+
+void DynamicMessageFactory::DeleteDefaultOneofInstance(
+ const Descriptor* type,
+ const int offsets[],
+ void* default_oneof_instance) {
+ for (int i = 0; i < type->oneof_decl_count(); i++) {
+ for (int j = 0; j < type->oneof_decl(i)->field_count(); j++) {
+ const FieldDescriptor* field = type->oneof_decl(i)->field(j);
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_STRING) {
+ switch (field->options().ctype()) {
+ default:
+ case FieldOptions::STRING:
+ break;
+ }
+ }
+ }
+ }
+}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/dynamic_message.h b/toolkit/components/protobuf/src/google/protobuf/dynamic_message.h
new file mode 100644
index 0000000000..10ed70051e
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/dynamic_message.h
@@ -0,0 +1,148 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Defines an implementation of Message which can emulate types which are not
+// known at compile-time.
+
+#ifndef GOOGLE_PROTOBUF_DYNAMIC_MESSAGE_H__
+#define GOOGLE_PROTOBUF_DYNAMIC_MESSAGE_H__
+
+#include <memory>
+
+#include <google/protobuf/message.h>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+// Defined in other files.
+class Descriptor; // descriptor.h
+class DescriptorPool; // descriptor.h
+
+// Constructs implementations of Message which can emulate types which are not
+// known at compile-time.
+//
+// Sometimes you want to be able to manipulate protocol types that you don't
+// know about at compile time. It would be nice to be able to construct
+// a Message object which implements the message type given by any arbitrary
+// Descriptor. DynamicMessage provides this.
+//
+// As it turns out, a DynamicMessage needs to construct extra
+// information about its type in order to operate. Most of this information
+// can be shared between all DynamicMessages of the same type. But, caching
+// this information in some sort of global map would be a bad idea, since
+// the cached information for a particular descriptor could outlive the
+// descriptor itself. To avoid this problem, DynamicMessageFactory
+// encapsulates this "cache". All DynamicMessages of the same type created
+// from the same factory will share the same support data. Any Descriptors
+// used with a particular factory must outlive the factory.
+class LIBPROTOBUF_EXPORT DynamicMessageFactory : public MessageFactory {
+ public:
+ // Construct a DynamicMessageFactory that will search for extensions in
+ // the DescriptorPool in which the extendee is defined.
+ DynamicMessageFactory();
+
+ // Construct a DynamicMessageFactory that will search for extensions in
+ // the given DescriptorPool.
+ //
+ // DEPRECATED: Use CodedInputStream::SetExtensionRegistry() to tell the
+ // parser to look for extensions in an alternate pool. However, note that
+ // this is almost never what you want to do. Almost all users should use
+ // the zero-arg constructor.
+ DynamicMessageFactory(const DescriptorPool* pool);
+
+ ~DynamicMessageFactory();
+
+ // Call this to tell the DynamicMessageFactory that if it is given a
+ // Descriptor d for which:
+ // d->file()->pool() == DescriptorPool::generated_pool(),
+ // then it should delegate to MessageFactory::generated_factory() instead
+ // of constructing a dynamic implementation of the message. In theory there
+ // is no down side to doing this, so it may become the default in the future.
+ void SetDelegateToGeneratedFactory(bool enable) {
+ delegate_to_generated_factory_ = enable;
+ }
+
+ // implements MessageFactory ---------------------------------------
+
+ // Given a Descriptor, constructs the default (prototype) Message of that
+ // type. You can then call that message's New() method to construct a
+ // mutable message of that type.
+ //
+ // Calling this method twice with the same Descriptor returns the same
+ // object. The returned object remains property of the factory and will
+ // be destroyed when the factory is destroyed. Also, any objects created
+ // by calling the prototype's New() method share some data with the
+ // prototype, so these must be destroyed before the DynamicMessageFactory
+ // is destroyed.
+ //
+ // The given descriptor must outlive the returned message, and hence must
+ // outlive the DynamicMessageFactory.
+ //
+ // The method is thread-safe.
+ const Message* GetPrototype(const Descriptor* type);
+
+ private:
+ const DescriptorPool* pool_;
+ bool delegate_to_generated_factory_;
+
+ // This struct just contains a hash_map. We can't #include <google/protobuf/stubs/hash.h> from
+ // this header due to hacks needed for hash_map portability in the open source
+ // release. Namely, stubs/hash.h, which defines hash_map portably, is not a
+ // public header (for good reason), but dynamic_message.h is, and public
+ // headers may only #include other public headers.
+ struct PrototypeMap;
+ scoped_ptr<PrototypeMap> prototypes_;
+ mutable Mutex prototypes_mutex_;
+
+ friend class DynamicMessage;
+ const Message* GetPrototypeNoLock(const Descriptor* type);
+
+ // Construct default oneof instance for reflection usage if oneof
+ // is defined.
+ static void ConstructDefaultOneofInstance(const Descriptor* type,
+ const int offsets[],
+ void* default_oneof_instance);
+ // Delete default oneof instance. Called by ~DynamicMessageFactory.
+ static void DeleteDefaultOneofInstance(const Descriptor* type,
+ const int offsets[],
+ void* default_oneof_instance);
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DynamicMessageFactory);
+};
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_DYNAMIC_MESSAGE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/extension_set.cc b/toolkit/components/protobuf/src/google/protobuf/extension_set.cc
new file mode 100644
index 0000000000..274554b5f7
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/extension_set.cc
@@ -0,0 +1,1663 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/stubs/hash.h>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/message_lite.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/stubs/map_util.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+namespace {
+
+inline WireFormatLite::FieldType real_type(FieldType type) {
+ GOOGLE_DCHECK(type > 0 && type <= WireFormatLite::MAX_FIELD_TYPE);
+ return static_cast<WireFormatLite::FieldType>(type);
+}
+
+inline WireFormatLite::CppType cpp_type(FieldType type) {
+ return WireFormatLite::FieldTypeToCppType(real_type(type));
+}
+
+inline bool is_packable(WireFormatLite::WireType type) {
+ switch (type) {
+ case WireFormatLite::WIRETYPE_VARINT:
+ case WireFormatLite::WIRETYPE_FIXED64:
+ case WireFormatLite::WIRETYPE_FIXED32:
+ return true;
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED:
+ case WireFormatLite::WIRETYPE_START_GROUP:
+ case WireFormatLite::WIRETYPE_END_GROUP:
+ return false;
+
+ // Do not add a default statement. Let the compiler complain when someone
+ // adds a new wire type.
+ }
+}
+
+// Registry stuff.
+typedef hash_map<pair<const MessageLite*, int>,
+ ExtensionInfo> ExtensionRegistry;
+ExtensionRegistry* registry_ = NULL;
+GOOGLE_PROTOBUF_DECLARE_ONCE(registry_init_);
+
+void DeleteRegistry() {
+ delete registry_;
+ registry_ = NULL;
+}
+
+void InitRegistry() {
+ registry_ = new ExtensionRegistry;
+ OnShutdown(&DeleteRegistry);
+}
+
+// This function is only called at startup, so there is no need for thread-
+// safety.
+void Register(const MessageLite* containing_type,
+ int number, ExtensionInfo info) {
+ ::google::protobuf::GoogleOnceInit(&registry_init_, &InitRegistry);
+
+ if (!InsertIfNotPresent(registry_, make_pair(containing_type, number),
+ info)) {
+ GOOGLE_LOG(FATAL) << "Multiple extension registrations for type \""
+ << containing_type->GetTypeName()
+ << "\", field number " << number << ".";
+ }
+}
+
+const ExtensionInfo* FindRegisteredExtension(
+ const MessageLite* containing_type, int number) {
+ return (registry_ == NULL) ? NULL :
+ FindOrNull(*registry_, make_pair(containing_type, number));
+}
+
+} // namespace
+
+ExtensionFinder::~ExtensionFinder() {}
+
+bool GeneratedExtensionFinder::Find(int number, ExtensionInfo* output) {
+ const ExtensionInfo* extension =
+ FindRegisteredExtension(containing_type_, number);
+ if (extension == NULL) {
+ return false;
+ } else {
+ *output = *extension;
+ return true;
+ }
+}
+
+void ExtensionSet::RegisterExtension(const MessageLite* containing_type,
+ int number, FieldType type,
+ bool is_repeated, bool is_packed) {
+ GOOGLE_CHECK_NE(type, WireFormatLite::TYPE_ENUM);
+ GOOGLE_CHECK_NE(type, WireFormatLite::TYPE_MESSAGE);
+ GOOGLE_CHECK_NE(type, WireFormatLite::TYPE_GROUP);
+ ExtensionInfo info(type, is_repeated, is_packed);
+ Register(containing_type, number, info);
+}
+
+static bool CallNoArgValidityFunc(const void* arg, int number) {
+ // Note: Must use C-style cast here rather than reinterpret_cast because
+ // the C++ standard at one point did not allow casts between function and
+ // data pointers and some compilers enforce this for C++-style casts. No
+ // compiler enforces it for C-style casts since lots of C-style code has
+ // relied on these kinds of casts for a long time, despite being
+ // technically undefined. See:
+ // http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#195
+ // Also note: Some compilers do not allow function pointers to be "const".
+ // Which makes sense, I suppose, because it's meaningless.
+ return ((EnumValidityFunc*)arg)(number);
+}
+
+void ExtensionSet::RegisterEnumExtension(const MessageLite* containing_type,
+ int number, FieldType type,
+ bool is_repeated, bool is_packed,
+ EnumValidityFunc* is_valid) {
+ GOOGLE_CHECK_EQ(type, WireFormatLite::TYPE_ENUM);
+ ExtensionInfo info(type, is_repeated, is_packed);
+ info.enum_validity_check.func = CallNoArgValidityFunc;
+ // See comment in CallNoArgValidityFunc() about why we use a c-style cast.
+ info.enum_validity_check.arg = (void*)is_valid;
+ Register(containing_type, number, info);
+}
+
+void ExtensionSet::RegisterMessageExtension(const MessageLite* containing_type,
+ int number, FieldType type,
+ bool is_repeated, bool is_packed,
+ const MessageLite* prototype) {
+ GOOGLE_CHECK(type == WireFormatLite::TYPE_MESSAGE ||
+ type == WireFormatLite::TYPE_GROUP);
+ ExtensionInfo info(type, is_repeated, is_packed);
+ info.message_prototype = prototype;
+ Register(containing_type, number, info);
+}
+
+
+// ===================================================================
+// Constructors and basic methods.
+
+ExtensionSet::ExtensionSet() {}
+
+ExtensionSet::~ExtensionSet() {
+ for (map<int, Extension>::iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ iter->second.Free();
+ }
+}
+
+// Defined in extension_set_heavy.cc.
+// void ExtensionSet::AppendToList(const Descriptor* containing_type,
+// const DescriptorPool* pool,
+// vector<const FieldDescriptor*>* output) const
+
+bool ExtensionSet::Has(int number) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) return false;
+ GOOGLE_DCHECK(!iter->second.is_repeated);
+ return !iter->second.is_cleared;
+}
+
+int ExtensionSet::NumExtensions() const {
+ int result = 0;
+ for (map<int, Extension>::const_iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ if (!iter->second.is_cleared) {
+ ++result;
+ }
+ }
+ return result;
+}
+
+int ExtensionSet::ExtensionSize(int number) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) return false;
+ return iter->second.GetSize();
+}
+
+FieldType ExtensionSet::ExtensionType(int number) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) {
+ GOOGLE_LOG(DFATAL) << "Don't lookup extension types if they aren't present (1). ";
+ return 0;
+ }
+ if (iter->second.is_cleared) {
+ GOOGLE_LOG(DFATAL) << "Don't lookup extension types if they aren't present (2). ";
+ }
+ return iter->second.type;
+}
+
+void ExtensionSet::ClearExtension(int number) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) return;
+ iter->second.Clear();
+}
+
+// ===================================================================
+// Field accessors
+
+namespace {
+
+enum Cardinality {
+ REPEATED,
+ OPTIONAL
+};
+
+} // namespace
+
+#define GOOGLE_DCHECK_TYPE(EXTENSION, LABEL, CPPTYPE) \
+ GOOGLE_DCHECK_EQ((EXTENSION).is_repeated ? REPEATED : OPTIONAL, LABEL); \
+ GOOGLE_DCHECK_EQ(cpp_type((EXTENSION).type), WireFormatLite::CPPTYPE_##CPPTYPE)
+
+// -------------------------------------------------------------------
+// Primitives
+
+#define PRIMITIVE_ACCESSORS(UPPERCASE, LOWERCASE, CAMELCASE) \
+ \
+LOWERCASE ExtensionSet::Get##CAMELCASE(int number, \
+ LOWERCASE default_value) const { \
+ map<int, Extension>::const_iterator iter = extensions_.find(number); \
+ if (iter == extensions_.end() || iter->second.is_cleared) { \
+ return default_value; \
+ } else { \
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, UPPERCASE); \
+ return iter->second.LOWERCASE##_value; \
+ } \
+} \
+ \
+void ExtensionSet::Set##CAMELCASE(int number, FieldType type, \
+ LOWERCASE value, \
+ const FieldDescriptor* descriptor) { \
+ Extension* extension; \
+ if (MaybeNewExtension(number, descriptor, &extension)) { \
+ extension->type = type; \
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_##UPPERCASE); \
+ extension->is_repeated = false; \
+ } else { \
+ GOOGLE_DCHECK_TYPE(*extension, OPTIONAL, UPPERCASE); \
+ } \
+ extension->is_cleared = false; \
+ extension->LOWERCASE##_value = value; \
+} \
+ \
+LOWERCASE ExtensionSet::GetRepeated##CAMELCASE(int number, int index) const { \
+ map<int, Extension>::const_iterator iter = extensions_.find(number); \
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty)."; \
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, UPPERCASE); \
+ return iter->second.repeated_##LOWERCASE##_value->Get(index); \
+} \
+ \
+void ExtensionSet::SetRepeated##CAMELCASE( \
+ int number, int index, LOWERCASE value) { \
+ map<int, Extension>::iterator iter = extensions_.find(number); \
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty)."; \
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, UPPERCASE); \
+ iter->second.repeated_##LOWERCASE##_value->Set(index, value); \
+} \
+ \
+void ExtensionSet::Add##CAMELCASE(int number, FieldType type, \
+ bool packed, LOWERCASE value, \
+ const FieldDescriptor* descriptor) { \
+ Extension* extension; \
+ if (MaybeNewExtension(number, descriptor, &extension)) { \
+ extension->type = type; \
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_##UPPERCASE); \
+ extension->is_repeated = true; \
+ extension->is_packed = packed; \
+ extension->repeated_##LOWERCASE##_value = new RepeatedField<LOWERCASE>(); \
+ } else { \
+ GOOGLE_DCHECK_TYPE(*extension, REPEATED, UPPERCASE); \
+ GOOGLE_DCHECK_EQ(extension->is_packed, packed); \
+ } \
+ extension->repeated_##LOWERCASE##_value->Add(value); \
+}
+
+PRIMITIVE_ACCESSORS( INT32, int32, Int32)
+PRIMITIVE_ACCESSORS( INT64, int64, Int64)
+PRIMITIVE_ACCESSORS(UINT32, uint32, UInt32)
+PRIMITIVE_ACCESSORS(UINT64, uint64, UInt64)
+PRIMITIVE_ACCESSORS( FLOAT, float, Float)
+PRIMITIVE_ACCESSORS(DOUBLE, double, Double)
+PRIMITIVE_ACCESSORS( BOOL, bool, Bool)
+
+#undef PRIMITIVE_ACCESSORS
+
+const void* ExtensionSet::GetRawRepeatedField(int number,
+ const void* default_value) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) {
+ return default_value;
+ }
+ // We assume that all the RepeatedField<>* pointers have the same
+ // size and alignment within the anonymous union in Extension.
+ return iter->second.repeated_int32_value;
+}
+
+void* ExtensionSet::MutableRawRepeatedField(int number, FieldType field_type,
+ bool packed,
+ const FieldDescriptor* desc) {
+ Extension* extension;
+
+ // We instantiate an empty Repeated{,Ptr}Field if one doesn't exist for this
+ // extension.
+ if (MaybeNewExtension(number, desc, &extension)) {
+ extension->is_repeated = true;
+ extension->type = field_type;
+ extension->is_packed = packed;
+
+ switch (WireFormatLite::FieldTypeToCppType(
+ static_cast<WireFormatLite::FieldType>(field_type))) {
+ case WireFormatLite::CPPTYPE_INT32:
+ extension->repeated_int32_value = new RepeatedField<int32>();
+ break;
+ case WireFormatLite::CPPTYPE_INT64:
+ extension->repeated_int64_value = new RepeatedField<int64>();
+ break;
+ case WireFormatLite::CPPTYPE_UINT32:
+ extension->repeated_uint32_value = new RepeatedField<uint32>();
+ break;
+ case WireFormatLite::CPPTYPE_UINT64:
+ extension->repeated_uint64_value = new RepeatedField<uint64>();
+ break;
+ case WireFormatLite::CPPTYPE_DOUBLE:
+ extension->repeated_double_value = new RepeatedField<double>();
+ break;
+ case WireFormatLite::CPPTYPE_FLOAT:
+ extension->repeated_float_value = new RepeatedField<float>();
+ break;
+ case WireFormatLite::CPPTYPE_BOOL:
+ extension->repeated_bool_value = new RepeatedField<bool>();
+ break;
+ case WireFormatLite::CPPTYPE_ENUM:
+ extension->repeated_enum_value = new RepeatedField<int>();
+ break;
+ case WireFormatLite::CPPTYPE_STRING:
+ extension->repeated_string_value = new RepeatedPtrField< ::std::string>();
+ break;
+ case WireFormatLite::CPPTYPE_MESSAGE:
+ extension->repeated_message_value = new RepeatedPtrField<MessageLite>();
+ break;
+ }
+ }
+
+ // We assume that all the RepeatedField<>* pointers have the same
+ // size and alignment within the anonymous union in Extension.
+ return extension->repeated_int32_value;
+}
+
+// Compatible version using old call signature. Does not create extensions when
+// the don't already exist; instead, just GOOGLE_CHECK-fails.
+void* ExtensionSet::MutableRawRepeatedField(int number) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter == extensions_.end()) << "Extension not found.";
+ // We assume that all the RepeatedField<>* pointers have the same
+ // size and alignment within the anonymous union in Extension.
+ return iter->second.repeated_int32_value;
+}
+
+
+// -------------------------------------------------------------------
+// Enums
+
+int ExtensionSet::GetEnum(int number, int default_value) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end() || iter->second.is_cleared) {
+ // Not present. Return the default value.
+ return default_value;
+ } else {
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, ENUM);
+ return iter->second.enum_value;
+ }
+}
+
+void ExtensionSet::SetEnum(int number, FieldType type, int value,
+ const FieldDescriptor* descriptor) {
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_ENUM);
+ extension->is_repeated = false;
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, OPTIONAL, ENUM);
+ }
+ extension->is_cleared = false;
+ extension->enum_value = value;
+}
+
+int ExtensionSet::GetRepeatedEnum(int number, int index) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, ENUM);
+ return iter->second.repeated_enum_value->Get(index);
+}
+
+void ExtensionSet::SetRepeatedEnum(int number, int index, int value) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, ENUM);
+ iter->second.repeated_enum_value->Set(index, value);
+}
+
+void ExtensionSet::AddEnum(int number, FieldType type,
+ bool packed, int value,
+ const FieldDescriptor* descriptor) {
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_ENUM);
+ extension->is_repeated = true;
+ extension->is_packed = packed;
+ extension->repeated_enum_value = new RepeatedField<int>();
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, REPEATED, ENUM);
+ GOOGLE_DCHECK_EQ(extension->is_packed, packed);
+ }
+ extension->repeated_enum_value->Add(value);
+}
+
+// -------------------------------------------------------------------
+// Strings
+
+const string& ExtensionSet::GetString(int number,
+ const string& default_value) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end() || iter->second.is_cleared) {
+ // Not present. Return the default value.
+ return default_value;
+ } else {
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, STRING);
+ return *iter->second.string_value;
+ }
+}
+
+string* ExtensionSet::MutableString(int number, FieldType type,
+ const FieldDescriptor* descriptor) {
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_STRING);
+ extension->is_repeated = false;
+ extension->string_value = new string;
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, OPTIONAL, STRING);
+ }
+ extension->is_cleared = false;
+ return extension->string_value;
+}
+
+const string& ExtensionSet::GetRepeatedString(int number, int index) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, STRING);
+ return iter->second.repeated_string_value->Get(index);
+}
+
+string* ExtensionSet::MutableRepeatedString(int number, int index) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, STRING);
+ return iter->second.repeated_string_value->Mutable(index);
+}
+
+string* ExtensionSet::AddString(int number, FieldType type,
+ const FieldDescriptor* descriptor) {
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_STRING);
+ extension->is_repeated = true;
+ extension->is_packed = false;
+ extension->repeated_string_value = new RepeatedPtrField<string>();
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, REPEATED, STRING);
+ }
+ return extension->repeated_string_value->Add();
+}
+
+// -------------------------------------------------------------------
+// Messages
+
+const MessageLite& ExtensionSet::GetMessage(
+ int number, const MessageLite& default_value) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) {
+ // Not present. Return the default value.
+ return default_value;
+ } else {
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, MESSAGE);
+ if (iter->second.is_lazy) {
+ return iter->second.lazymessage_value->GetMessage(default_value);
+ } else {
+ return *iter->second.message_value;
+ }
+ }
+}
+
+// Defined in extension_set_heavy.cc.
+// const MessageLite& ExtensionSet::GetMessage(int number,
+// const Descriptor* message_type,
+// MessageFactory* factory) const
+
+MessageLite* ExtensionSet::MutableMessage(int number, FieldType type,
+ const MessageLite& prototype,
+ const FieldDescriptor* descriptor) {
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_MESSAGE);
+ extension->is_repeated = false;
+ extension->is_lazy = false;
+ extension->message_value = prototype.New();
+ extension->is_cleared = false;
+ return extension->message_value;
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, OPTIONAL, MESSAGE);
+ extension->is_cleared = false;
+ if (extension->is_lazy) {
+ return extension->lazymessage_value->MutableMessage(prototype);
+ } else {
+ return extension->message_value;
+ }
+ }
+}
+
+// Defined in extension_set_heavy.cc.
+// MessageLite* ExtensionSet::MutableMessage(int number, FieldType type,
+// const Descriptor* message_type,
+// MessageFactory* factory)
+
+void ExtensionSet::SetAllocatedMessage(int number, FieldType type,
+ const FieldDescriptor* descriptor,
+ MessageLite* message) {
+ if (message == NULL) {
+ ClearExtension(number);
+ return;
+ }
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_MESSAGE);
+ extension->is_repeated = false;
+ extension->is_lazy = false;
+ extension->message_value = message;
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, OPTIONAL, MESSAGE);
+ if (extension->is_lazy) {
+ extension->lazymessage_value->SetAllocatedMessage(message);
+ } else {
+ delete extension->message_value;
+ extension->message_value = message;
+ }
+ }
+ extension->is_cleared = false;
+}
+
+MessageLite* ExtensionSet::ReleaseMessage(int number,
+ const MessageLite& prototype) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ if (iter == extensions_.end()) {
+ // Not present. Return NULL.
+ return NULL;
+ } else {
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, MESSAGE);
+ MessageLite* ret = NULL;
+ if (iter->second.is_lazy) {
+ ret = iter->second.lazymessage_value->ReleaseMessage(prototype);
+ delete iter->second.lazymessage_value;
+ } else {
+ ret = iter->second.message_value;
+ }
+ extensions_.erase(number);
+ return ret;
+ }
+}
+
+// Defined in extension_set_heavy.cc.
+// MessageLite* ExtensionSet::ReleaseMessage(const FieldDescriptor* descriptor,
+// MessageFactory* factory);
+
+const MessageLite& ExtensionSet::GetRepeatedMessage(
+ int number, int index) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, MESSAGE);
+ return iter->second.repeated_message_value->Get(index);
+}
+
+MessageLite* ExtensionSet::MutableRepeatedMessage(int number, int index) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+ GOOGLE_DCHECK_TYPE(iter->second, REPEATED, MESSAGE);
+ return iter->second.repeated_message_value->Mutable(index);
+}
+
+MessageLite* ExtensionSet::AddMessage(int number, FieldType type,
+ const MessageLite& prototype,
+ const FieldDescriptor* descriptor) {
+ Extension* extension;
+ if (MaybeNewExtension(number, descriptor, &extension)) {
+ extension->type = type;
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), WireFormatLite::CPPTYPE_MESSAGE);
+ extension->is_repeated = true;
+ extension->repeated_message_value =
+ new RepeatedPtrField<MessageLite>();
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, REPEATED, MESSAGE);
+ }
+
+ // RepeatedPtrField<MessageLite> does not know how to Add() since it cannot
+ // allocate an abstract object, so we have to be tricky.
+ MessageLite* result = extension->repeated_message_value
+ ->AddFromCleared<GenericTypeHandler<MessageLite> >();
+ if (result == NULL) {
+ result = prototype.New();
+ extension->repeated_message_value->AddAllocated(result);
+ }
+ return result;
+}
+
+// Defined in extension_set_heavy.cc.
+// MessageLite* ExtensionSet::AddMessage(int number, FieldType type,
+// const Descriptor* message_type,
+// MessageFactory* factory)
+
+#undef GOOGLE_DCHECK_TYPE
+
+void ExtensionSet::RemoveLast(int number) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+
+ Extension* extension = &iter->second;
+ GOOGLE_DCHECK(extension->is_repeated);
+
+ switch(cpp_type(extension->type)) {
+ case WireFormatLite::CPPTYPE_INT32:
+ extension->repeated_int32_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_INT64:
+ extension->repeated_int64_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_UINT32:
+ extension->repeated_uint32_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_UINT64:
+ extension->repeated_uint64_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_FLOAT:
+ extension->repeated_float_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_DOUBLE:
+ extension->repeated_double_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_BOOL:
+ extension->repeated_bool_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_ENUM:
+ extension->repeated_enum_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_STRING:
+ extension->repeated_string_value->RemoveLast();
+ break;
+ case WireFormatLite::CPPTYPE_MESSAGE:
+ extension->repeated_message_value->RemoveLast();
+ break;
+ }
+}
+
+MessageLite* ExtensionSet::ReleaseLast(int number) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+
+ Extension* extension = &iter->second;
+ GOOGLE_DCHECK(extension->is_repeated);
+ GOOGLE_DCHECK(cpp_type(extension->type) == WireFormatLite::CPPTYPE_MESSAGE);
+ return extension->repeated_message_value->ReleaseLast();
+}
+
+void ExtensionSet::SwapElements(int number, int index1, int index2) {
+ map<int, Extension>::iterator iter = extensions_.find(number);
+ GOOGLE_CHECK(iter != extensions_.end()) << "Index out-of-bounds (field is empty).";
+
+ Extension* extension = &iter->second;
+ GOOGLE_DCHECK(extension->is_repeated);
+
+ switch(cpp_type(extension->type)) {
+ case WireFormatLite::CPPTYPE_INT32:
+ extension->repeated_int32_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_INT64:
+ extension->repeated_int64_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_UINT32:
+ extension->repeated_uint32_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_UINT64:
+ extension->repeated_uint64_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_FLOAT:
+ extension->repeated_float_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_DOUBLE:
+ extension->repeated_double_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_BOOL:
+ extension->repeated_bool_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_ENUM:
+ extension->repeated_enum_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_STRING:
+ extension->repeated_string_value->SwapElements(index1, index2);
+ break;
+ case WireFormatLite::CPPTYPE_MESSAGE:
+ extension->repeated_message_value->SwapElements(index1, index2);
+ break;
+ }
+}
+
+// ===================================================================
+
+void ExtensionSet::Clear() {
+ for (map<int, Extension>::iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ iter->second.Clear();
+ }
+}
+
+void ExtensionSet::MergeFrom(const ExtensionSet& other) {
+ for (map<int, Extension>::const_iterator iter = other.extensions_.begin();
+ iter != other.extensions_.end(); ++iter) {
+ const Extension& other_extension = iter->second;
+
+ if (other_extension.is_repeated) {
+ Extension* extension;
+ bool is_new = MaybeNewExtension(iter->first, other_extension.descriptor,
+ &extension);
+ if (is_new) {
+ // Extension did not already exist in set.
+ extension->type = other_extension.type;
+ extension->is_packed = other_extension.is_packed;
+ extension->is_repeated = true;
+ } else {
+ GOOGLE_DCHECK_EQ(extension->type, other_extension.type);
+ GOOGLE_DCHECK_EQ(extension->is_packed, other_extension.is_packed);
+ GOOGLE_DCHECK(extension->is_repeated);
+ }
+
+ switch (cpp_type(other_extension.type)) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE, REPEATED_TYPE) \
+ case WireFormatLite::CPPTYPE_##UPPERCASE: \
+ if (is_new) { \
+ extension->repeated_##LOWERCASE##_value = \
+ new REPEATED_TYPE; \
+ } \
+ extension->repeated_##LOWERCASE##_value->MergeFrom( \
+ *other_extension.repeated_##LOWERCASE##_value); \
+ break;
+
+ HANDLE_TYPE( INT32, int32, RepeatedField < int32>);
+ HANDLE_TYPE( INT64, int64, RepeatedField < int64>);
+ HANDLE_TYPE( UINT32, uint32, RepeatedField < uint32>);
+ HANDLE_TYPE( UINT64, uint64, RepeatedField < uint64>);
+ HANDLE_TYPE( FLOAT, float, RepeatedField < float>);
+ HANDLE_TYPE( DOUBLE, double, RepeatedField < double>);
+ HANDLE_TYPE( BOOL, bool, RepeatedField < bool>);
+ HANDLE_TYPE( ENUM, enum, RepeatedField < int>);
+ HANDLE_TYPE( STRING, string, RepeatedPtrField< string>);
+#undef HANDLE_TYPE
+
+ case WireFormatLite::CPPTYPE_MESSAGE:
+ if (is_new) {
+ extension->repeated_message_value =
+ new RepeatedPtrField<MessageLite>();
+ }
+ // We can't call RepeatedPtrField<MessageLite>::MergeFrom() because
+ // it would attempt to allocate new objects.
+ RepeatedPtrField<MessageLite>* other_repeated_message =
+ other_extension.repeated_message_value;
+ for (int i = 0; i < other_repeated_message->size(); i++) {
+ const MessageLite& other_message = other_repeated_message->Get(i);
+ MessageLite* target = extension->repeated_message_value
+ ->AddFromCleared<GenericTypeHandler<MessageLite> >();
+ if (target == NULL) {
+ target = other_message.New();
+ extension->repeated_message_value->AddAllocated(target);
+ }
+ target->CheckTypeAndMergeFrom(other_message);
+ }
+ break;
+ }
+ } else {
+ if (!other_extension.is_cleared) {
+ switch (cpp_type(other_extension.type)) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE, CAMELCASE) \
+ case WireFormatLite::CPPTYPE_##UPPERCASE: \
+ Set##CAMELCASE(iter->first, other_extension.type, \
+ other_extension.LOWERCASE##_value, \
+ other_extension.descriptor); \
+ break;
+
+ HANDLE_TYPE( INT32, int32, Int32);
+ HANDLE_TYPE( INT64, int64, Int64);
+ HANDLE_TYPE(UINT32, uint32, UInt32);
+ HANDLE_TYPE(UINT64, uint64, UInt64);
+ HANDLE_TYPE( FLOAT, float, Float);
+ HANDLE_TYPE(DOUBLE, double, Double);
+ HANDLE_TYPE( BOOL, bool, Bool);
+ HANDLE_TYPE( ENUM, enum, Enum);
+#undef HANDLE_TYPE
+ case WireFormatLite::CPPTYPE_STRING:
+ SetString(iter->first, other_extension.type,
+ *other_extension.string_value,
+ other_extension.descriptor);
+ break;
+ case WireFormatLite::CPPTYPE_MESSAGE: {
+ Extension* extension;
+ bool is_new = MaybeNewExtension(iter->first,
+ other_extension.descriptor,
+ &extension);
+ if (is_new) {
+ extension->type = other_extension.type;
+ extension->is_packed = other_extension.is_packed;
+ extension->is_repeated = false;
+ if (other_extension.is_lazy) {
+ extension->is_lazy = true;
+ extension->lazymessage_value =
+ other_extension.lazymessage_value->New();
+ extension->lazymessage_value->MergeFrom(
+ *other_extension.lazymessage_value);
+ } else {
+ extension->is_lazy = false;
+ extension->message_value =
+ other_extension.message_value->New();
+ extension->message_value->CheckTypeAndMergeFrom(
+ *other_extension.message_value);
+ }
+ } else {
+ GOOGLE_DCHECK_EQ(extension->type, other_extension.type);
+ GOOGLE_DCHECK_EQ(extension->is_packed,other_extension.is_packed);
+ GOOGLE_DCHECK(!extension->is_repeated);
+ if (other_extension.is_lazy) {
+ if (extension->is_lazy) {
+ extension->lazymessage_value->MergeFrom(
+ *other_extension.lazymessage_value);
+ } else {
+ extension->message_value->CheckTypeAndMergeFrom(
+ other_extension.lazymessage_value->GetMessage(
+ *extension->message_value));
+ }
+ } else {
+ if (extension->is_lazy) {
+ extension->lazymessage_value->MutableMessage(
+ *other_extension.message_value)->CheckTypeAndMergeFrom(
+ *other_extension.message_value);
+ } else {
+ extension->message_value->CheckTypeAndMergeFrom(
+ *other_extension.message_value);
+ }
+ }
+ }
+ extension->is_cleared = false;
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+void ExtensionSet::Swap(ExtensionSet* x) {
+ extensions_.swap(x->extensions_);
+}
+
+void ExtensionSet::SwapExtension(ExtensionSet* other,
+ int number) {
+ if (this == other) return;
+ map<int, Extension>::iterator this_iter = extensions_.find(number);
+ map<int, Extension>::iterator other_iter = other->extensions_.find(number);
+
+ if (this_iter == extensions_.end() &&
+ other_iter == other->extensions_.end()) {
+ return;
+ }
+
+ if (this_iter != extensions_.end() &&
+ other_iter != other->extensions_.end()) {
+ std::swap(this_iter->second, other_iter->second);
+ return;
+ }
+
+ if (this_iter == extensions_.end()) {
+ extensions_.insert(make_pair(number, other_iter->second));
+ other->extensions_.erase(number);
+ return;
+ }
+
+ if (other_iter == other->extensions_.end()) {
+ other->extensions_.insert(make_pair(number, this_iter->second));
+ extensions_.erase(number);
+ return;
+ }
+}
+
+bool ExtensionSet::IsInitialized() const {
+ // Extensions are never required. However, we need to check that all
+ // embedded messages are initialized.
+ for (map<int, Extension>::const_iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ const Extension& extension = iter->second;
+ if (cpp_type(extension.type) == WireFormatLite::CPPTYPE_MESSAGE) {
+ if (extension.is_repeated) {
+ for (int i = 0; i < extension.repeated_message_value->size(); i++) {
+ if (!extension.repeated_message_value->Get(i).IsInitialized()) {
+ return false;
+ }
+ }
+ } else {
+ if (!extension.is_cleared) {
+ if (extension.is_lazy) {
+ if (!extension.lazymessage_value->IsInitialized()) return false;
+ } else {
+ if (!extension.message_value->IsInitialized()) return false;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+bool ExtensionSet::FindExtensionInfoFromTag(
+ uint32 tag, ExtensionFinder* extension_finder, int* field_number,
+ ExtensionInfo* extension, bool* was_packed_on_wire) {
+ *field_number = WireFormatLite::GetTagFieldNumber(tag);
+ WireFormatLite::WireType wire_type = WireFormatLite::GetTagWireType(tag);
+ return FindExtensionInfoFromFieldNumber(wire_type, *field_number,
+ extension_finder, extension,
+ was_packed_on_wire);
+}
+
+bool ExtensionSet::FindExtensionInfoFromFieldNumber(
+ int wire_type, int field_number, ExtensionFinder* extension_finder,
+ ExtensionInfo* extension, bool* was_packed_on_wire) {
+ if (!extension_finder->Find(field_number, extension)) {
+ return false;
+ }
+
+ WireFormatLite::WireType expected_wire_type =
+ WireFormatLite::WireTypeForFieldType(real_type(extension->type));
+
+ // Check if this is a packed field.
+ *was_packed_on_wire = false;
+ if (extension->is_repeated &&
+ wire_type == WireFormatLite::WIRETYPE_LENGTH_DELIMITED &&
+ is_packable(expected_wire_type)) {
+ *was_packed_on_wire = true;
+ return true;
+ }
+ // Otherwise the wire type must match.
+ return expected_wire_type == wire_type;
+}
+
+bool ExtensionSet::ParseField(uint32 tag, io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ FieldSkipper* field_skipper) {
+ int number;
+ bool was_packed_on_wire;
+ ExtensionInfo extension;
+ if (!FindExtensionInfoFromTag(
+ tag, extension_finder, &number, &extension, &was_packed_on_wire)) {
+ return field_skipper->SkipField(input, tag);
+ } else {
+ return ParseFieldWithExtensionInfo(
+ number, was_packed_on_wire, extension, input, field_skipper);
+ }
+}
+
+bool ExtensionSet::ParseFieldWithExtensionInfo(
+ int number, bool was_packed_on_wire, const ExtensionInfo& extension,
+ io::CodedInputStream* input,
+ FieldSkipper* field_skipper) {
+ // Explicitly not read extension.is_packed, instead check whether the field
+ // was encoded in packed form on the wire.
+ if (was_packed_on_wire) {
+ uint32 size;
+ if (!input->ReadVarint32(&size)) return false;
+ io::CodedInputStream::Limit limit = input->PushLimit(size);
+
+ switch (extension.type) {
+#define HANDLE_TYPE(UPPERCASE, CPP_CAMELCASE, CPP_LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ while (input->BytesUntilLimit() > 0) { \
+ CPP_LOWERCASE value; \
+ if (!WireFormatLite::ReadPrimitive< \
+ CPP_LOWERCASE, WireFormatLite::TYPE_##UPPERCASE>( \
+ input, &value)) return false; \
+ Add##CPP_CAMELCASE(number, WireFormatLite::TYPE_##UPPERCASE, \
+ extension.is_packed, value, \
+ extension.descriptor); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, Int32, int32);
+ HANDLE_TYPE( SINT64, Int64, int64);
+ HANDLE_TYPE( FIXED32, UInt32, uint32);
+ HANDLE_TYPE( FIXED64, UInt64, uint64);
+ HANDLE_TYPE(SFIXED32, Int32, int32);
+ HANDLE_TYPE(SFIXED64, Int64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+#undef HANDLE_TYPE
+
+ case WireFormatLite::TYPE_ENUM:
+ while (input->BytesUntilLimit() > 0) {
+ int value;
+ if (!WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_ENUM>(
+ input, &value)) return false;
+ if (extension.enum_validity_check.func(
+ extension.enum_validity_check.arg, value)) {
+ AddEnum(number, WireFormatLite::TYPE_ENUM, extension.is_packed,
+ value, extension.descriptor);
+ }
+ }
+ break;
+
+ case WireFormatLite::TYPE_STRING:
+ case WireFormatLite::TYPE_BYTES:
+ case WireFormatLite::TYPE_GROUP:
+ case WireFormatLite::TYPE_MESSAGE:
+ GOOGLE_LOG(FATAL) << "Non-primitive types can't be packed.";
+ break;
+ }
+
+ input->PopLimit(limit);
+ } else {
+ switch (extension.type) {
+#define HANDLE_TYPE(UPPERCASE, CPP_CAMELCASE, CPP_LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: { \
+ CPP_LOWERCASE value; \
+ if (!WireFormatLite::ReadPrimitive< \
+ CPP_LOWERCASE, WireFormatLite::TYPE_##UPPERCASE>( \
+ input, &value)) return false; \
+ if (extension.is_repeated) { \
+ Add##CPP_CAMELCASE(number, WireFormatLite::TYPE_##UPPERCASE, \
+ extension.is_packed, value, \
+ extension.descriptor); \
+ } else { \
+ Set##CPP_CAMELCASE(number, WireFormatLite::TYPE_##UPPERCASE, value, \
+ extension.descriptor); \
+ } \
+ } break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, Int32, int32);
+ HANDLE_TYPE( SINT64, Int64, int64);
+ HANDLE_TYPE( FIXED32, UInt32, uint32);
+ HANDLE_TYPE( FIXED64, UInt64, uint64);
+ HANDLE_TYPE(SFIXED32, Int32, int32);
+ HANDLE_TYPE(SFIXED64, Int64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+#undef HANDLE_TYPE
+
+ case WireFormatLite::TYPE_ENUM: {
+ int value;
+ if (!WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_ENUM>(
+ input, &value)) return false;
+
+ if (!extension.enum_validity_check.func(
+ extension.enum_validity_check.arg, value)) {
+ // Invalid value. Treat as unknown.
+ field_skipper->SkipUnknownEnum(number, value);
+ } else if (extension.is_repeated) {
+ AddEnum(number, WireFormatLite::TYPE_ENUM, extension.is_packed, value,
+ extension.descriptor);
+ } else {
+ SetEnum(number, WireFormatLite::TYPE_ENUM, value,
+ extension.descriptor);
+ }
+ break;
+ }
+
+ case WireFormatLite::TYPE_STRING: {
+ string* value = extension.is_repeated ?
+ AddString(number, WireFormatLite::TYPE_STRING, extension.descriptor) :
+ MutableString(number, WireFormatLite::TYPE_STRING,
+ extension.descriptor);
+ if (!WireFormatLite::ReadString(input, value)) return false;
+ break;
+ }
+
+ case WireFormatLite::TYPE_BYTES: {
+ string* value = extension.is_repeated ?
+ AddString(number, WireFormatLite::TYPE_BYTES, extension.descriptor) :
+ MutableString(number, WireFormatLite::TYPE_BYTES,
+ extension.descriptor);
+ if (!WireFormatLite::ReadBytes(input, value)) return false;
+ break;
+ }
+
+ case WireFormatLite::TYPE_GROUP: {
+ MessageLite* value = extension.is_repeated ?
+ AddMessage(number, WireFormatLite::TYPE_GROUP,
+ *extension.message_prototype, extension.descriptor) :
+ MutableMessage(number, WireFormatLite::TYPE_GROUP,
+ *extension.message_prototype, extension.descriptor);
+ if (!WireFormatLite::ReadGroup(number, input, value)) return false;
+ break;
+ }
+
+ case WireFormatLite::TYPE_MESSAGE: {
+ MessageLite* value = extension.is_repeated ?
+ AddMessage(number, WireFormatLite::TYPE_MESSAGE,
+ *extension.message_prototype, extension.descriptor) :
+ MutableMessage(number, WireFormatLite::TYPE_MESSAGE,
+ *extension.message_prototype, extension.descriptor);
+ if (!WireFormatLite::ReadMessage(input, value)) return false;
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool ExtensionSet::ParseField(uint32 tag, io::CodedInputStream* input,
+ const MessageLite* containing_type) {
+ FieldSkipper skipper;
+ GeneratedExtensionFinder finder(containing_type);
+ return ParseField(tag, input, &finder, &skipper);
+}
+
+bool ExtensionSet::ParseField(uint32 tag, io::CodedInputStream* input,
+ const MessageLite* containing_type,
+ io::CodedOutputStream* unknown_fields) {
+ CodedOutputStreamFieldSkipper skipper(unknown_fields);
+ GeneratedExtensionFinder finder(containing_type);
+ return ParseField(tag, input, &finder, &skipper);
+}
+
+// Defined in extension_set_heavy.cc.
+// bool ExtensionSet::ParseField(uint32 tag, io::CodedInputStream* input,
+// const MessageLite* containing_type,
+// UnknownFieldSet* unknown_fields)
+
+// Defined in extension_set_heavy.cc.
+// bool ExtensionSet::ParseMessageSet(io::CodedInputStream* input,
+// const MessageLite* containing_type,
+// UnknownFieldSet* unknown_fields);
+
+void ExtensionSet::SerializeWithCachedSizes(
+ int start_field_number, int end_field_number,
+ io::CodedOutputStream* output) const {
+ map<int, Extension>::const_iterator iter;
+ for (iter = extensions_.lower_bound(start_field_number);
+ iter != extensions_.end() && iter->first < end_field_number;
+ ++iter) {
+ iter->second.SerializeFieldWithCachedSizes(iter->first, output);
+ }
+}
+
+int ExtensionSet::ByteSize() const {
+ int total_size = 0;
+
+ for (map<int, Extension>::const_iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ total_size += iter->second.ByteSize(iter->first);
+ }
+
+ return total_size;
+}
+
+// Defined in extension_set_heavy.cc.
+// int ExtensionSet::SpaceUsedExcludingSelf() const
+
+bool ExtensionSet::MaybeNewExtension(int number,
+ const FieldDescriptor* descriptor,
+ Extension** result) {
+ pair<map<int, Extension>::iterator, bool> insert_result =
+ extensions_.insert(make_pair(number, Extension()));
+ *result = &insert_result.first->second;
+ (*result)->descriptor = descriptor;
+ return insert_result.second;
+}
+
+// ===================================================================
+// Methods of ExtensionSet::Extension
+
+void ExtensionSet::Extension::Clear() {
+ if (is_repeated) {
+ switch (cpp_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case WireFormatLite::CPPTYPE_##UPPERCASE: \
+ repeated_##LOWERCASE##_value->Clear(); \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE( UINT32, uint32);
+ HANDLE_TYPE( UINT64, uint64);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( DOUBLE, double);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, enum);
+ HANDLE_TYPE( STRING, string);
+ HANDLE_TYPE(MESSAGE, message);
+#undef HANDLE_TYPE
+ }
+ } else {
+ if (!is_cleared) {
+ switch (cpp_type(type)) {
+ case WireFormatLite::CPPTYPE_STRING:
+ string_value->clear();
+ break;
+ case WireFormatLite::CPPTYPE_MESSAGE:
+ if (is_lazy) {
+ lazymessage_value->Clear();
+ } else {
+ message_value->Clear();
+ }
+ break;
+ default:
+ // No need to do anything. Get*() will return the default value
+ // as long as is_cleared is true and Set*() will overwrite the
+ // previous value.
+ break;
+ }
+
+ is_cleared = true;
+ }
+ }
+}
+
+void ExtensionSet::Extension::SerializeFieldWithCachedSizes(
+ int number,
+ io::CodedOutputStream* output) const {
+ if (is_repeated) {
+ if (is_packed) {
+ if (cached_size == 0) return;
+
+ WireFormatLite::WriteTag(number,
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED, output);
+ output->WriteVarint32(cached_size);
+
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ for (int i = 0; i < repeated_##LOWERCASE##_value->size(); i++) { \
+ WireFormatLite::Write##CAMELCASE##NoTag( \
+ repeated_##LOWERCASE##_value->Get(i), output); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, SInt32, int32);
+ HANDLE_TYPE( SINT64, SInt64, int64);
+ HANDLE_TYPE( FIXED32, Fixed32, uint32);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+ HANDLE_TYPE( ENUM, Enum, enum);
+#undef HANDLE_TYPE
+
+ case WireFormatLite::TYPE_STRING:
+ case WireFormatLite::TYPE_BYTES:
+ case WireFormatLite::TYPE_GROUP:
+ case WireFormatLite::TYPE_MESSAGE:
+ GOOGLE_LOG(FATAL) << "Non-primitive types can't be packed.";
+ break;
+ }
+ } else {
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ for (int i = 0; i < repeated_##LOWERCASE##_value->size(); i++) { \
+ WireFormatLite::Write##CAMELCASE(number, \
+ repeated_##LOWERCASE##_value->Get(i), output); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, SInt32, int32);
+ HANDLE_TYPE( SINT64, SInt64, int64);
+ HANDLE_TYPE( FIXED32, Fixed32, uint32);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+ HANDLE_TYPE( STRING, String, string);
+ HANDLE_TYPE( BYTES, Bytes, string);
+ HANDLE_TYPE( ENUM, Enum, enum);
+ HANDLE_TYPE( GROUP, Group, message);
+ HANDLE_TYPE( MESSAGE, Message, message);
+#undef HANDLE_TYPE
+ }
+ }
+ } else if (!is_cleared) {
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, VALUE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ WireFormatLite::Write##CAMELCASE(number, VALUE, output); \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32_value);
+ HANDLE_TYPE( INT64, Int64, int64_value);
+ HANDLE_TYPE( UINT32, UInt32, uint32_value);
+ HANDLE_TYPE( UINT64, UInt64, uint64_value);
+ HANDLE_TYPE( SINT32, SInt32, int32_value);
+ HANDLE_TYPE( SINT64, SInt64, int64_value);
+ HANDLE_TYPE( FIXED32, Fixed32, uint32_value);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64_value);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32_value);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64_value);
+ HANDLE_TYPE( FLOAT, Float, float_value);
+ HANDLE_TYPE( DOUBLE, Double, double_value);
+ HANDLE_TYPE( BOOL, Bool, bool_value);
+ HANDLE_TYPE( STRING, String, *string_value);
+ HANDLE_TYPE( BYTES, Bytes, *string_value);
+ HANDLE_TYPE( ENUM, Enum, enum_value);
+ HANDLE_TYPE( GROUP, Group, *message_value);
+#undef HANDLE_TYPE
+ case WireFormatLite::TYPE_MESSAGE:
+ if (is_lazy) {
+ lazymessage_value->WriteMessage(number, output);
+ } else {
+ WireFormatLite::WriteMessage(number, *message_value, output);
+ }
+ break;
+ }
+ }
+}
+
+int ExtensionSet::Extension::ByteSize(int number) const {
+ int result = 0;
+
+ if (is_repeated) {
+ if (is_packed) {
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ for (int i = 0; i < repeated_##LOWERCASE##_value->size(); i++) { \
+ result += WireFormatLite::CAMELCASE##Size( \
+ repeated_##LOWERCASE##_value->Get(i)); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, SInt32, int32);
+ HANDLE_TYPE( SINT64, SInt64, int64);
+ HANDLE_TYPE( ENUM, Enum, enum);
+#undef HANDLE_TYPE
+
+ // Stuff with fixed size.
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ result += WireFormatLite::k##CAMELCASE##Size * \
+ repeated_##LOWERCASE##_value->size(); \
+ break
+ HANDLE_TYPE( FIXED32, Fixed32, uint32);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+#undef HANDLE_TYPE
+
+ case WireFormatLite::TYPE_STRING:
+ case WireFormatLite::TYPE_BYTES:
+ case WireFormatLite::TYPE_GROUP:
+ case WireFormatLite::TYPE_MESSAGE:
+ GOOGLE_LOG(FATAL) << "Non-primitive types can't be packed.";
+ break;
+ }
+
+ cached_size = result;
+ if (result > 0) {
+ result += io::CodedOutputStream::VarintSize32(result);
+ result += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(number,
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ }
+ } else {
+ int tag_size = WireFormatLite::TagSize(number, real_type(type));
+
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ result += tag_size * repeated_##LOWERCASE##_value->size(); \
+ for (int i = 0; i < repeated_##LOWERCASE##_value->size(); i++) { \
+ result += WireFormatLite::CAMELCASE##Size( \
+ repeated_##LOWERCASE##_value->Get(i)); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, SInt32, int32);
+ HANDLE_TYPE( SINT64, SInt64, int64);
+ HANDLE_TYPE( STRING, String, string);
+ HANDLE_TYPE( BYTES, Bytes, string);
+ HANDLE_TYPE( ENUM, Enum, enum);
+ HANDLE_TYPE( GROUP, Group, message);
+ HANDLE_TYPE( MESSAGE, Message, message);
+#undef HANDLE_TYPE
+
+ // Stuff with fixed size.
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ result += (tag_size + WireFormatLite::k##CAMELCASE##Size) * \
+ repeated_##LOWERCASE##_value->size(); \
+ break
+ HANDLE_TYPE( FIXED32, Fixed32, uint32);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+#undef HANDLE_TYPE
+ }
+ }
+ } else if (!is_cleared) {
+ result += WireFormatLite::TagSize(number, real_type(type));
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ result += WireFormatLite::CAMELCASE##Size(LOWERCASE); \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32_value);
+ HANDLE_TYPE( INT64, Int64, int64_value);
+ HANDLE_TYPE( UINT32, UInt32, uint32_value);
+ HANDLE_TYPE( UINT64, UInt64, uint64_value);
+ HANDLE_TYPE( SINT32, SInt32, int32_value);
+ HANDLE_TYPE( SINT64, SInt64, int64_value);
+ HANDLE_TYPE( STRING, String, *string_value);
+ HANDLE_TYPE( BYTES, Bytes, *string_value);
+ HANDLE_TYPE( ENUM, Enum, enum_value);
+ HANDLE_TYPE( GROUP, Group, *message_value);
+#undef HANDLE_TYPE
+ case WireFormatLite::TYPE_MESSAGE: {
+ if (is_lazy) {
+ int size = lazymessage_value->ByteSize();
+ result += io::CodedOutputStream::VarintSize32(size) + size;
+ } else {
+ result += WireFormatLite::MessageSize(*message_value);
+ }
+ break;
+ }
+
+ // Stuff with fixed size.
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE) \
+ case WireFormatLite::TYPE_##UPPERCASE: \
+ result += WireFormatLite::k##CAMELCASE##Size; \
+ break
+ HANDLE_TYPE( FIXED32, Fixed32);
+ HANDLE_TYPE( FIXED64, Fixed64);
+ HANDLE_TYPE(SFIXED32, SFixed32);
+ HANDLE_TYPE(SFIXED64, SFixed64);
+ HANDLE_TYPE( FLOAT, Float);
+ HANDLE_TYPE( DOUBLE, Double);
+ HANDLE_TYPE( BOOL, Bool);
+#undef HANDLE_TYPE
+ }
+ }
+
+ return result;
+}
+
+int ExtensionSet::Extension::GetSize() const {
+ GOOGLE_DCHECK(is_repeated);
+ switch (cpp_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case WireFormatLite::CPPTYPE_##UPPERCASE: \
+ return repeated_##LOWERCASE##_value->size()
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE( UINT32, uint32);
+ HANDLE_TYPE( UINT64, uint64);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( DOUBLE, double);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, enum);
+ HANDLE_TYPE( STRING, string);
+ HANDLE_TYPE(MESSAGE, message);
+#undef HANDLE_TYPE
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+ return 0;
+}
+
+void ExtensionSet::Extension::Free() {
+ if (is_repeated) {
+ switch (cpp_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case WireFormatLite::CPPTYPE_##UPPERCASE: \
+ delete repeated_##LOWERCASE##_value; \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE( UINT32, uint32);
+ HANDLE_TYPE( UINT64, uint64);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( DOUBLE, double);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, enum);
+ HANDLE_TYPE( STRING, string);
+ HANDLE_TYPE(MESSAGE, message);
+#undef HANDLE_TYPE
+ }
+ } else {
+ switch (cpp_type(type)) {
+ case WireFormatLite::CPPTYPE_STRING:
+ delete string_value;
+ break;
+ case WireFormatLite::CPPTYPE_MESSAGE:
+ if (is_lazy) {
+ delete lazymessage_value;
+ } else {
+ delete message_value;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+// Defined in extension_set_heavy.cc.
+// int ExtensionSet::Extension::SpaceUsedExcludingSelf() const
+
+// ==================================================================
+// Default repeated field instances for iterator-compatible accessors
+
+const RepeatedStringTypeTraits::RepeatedFieldType*
+RepeatedStringTypeTraits::default_repeated_field_ = NULL;
+
+const RepeatedMessageGenericTypeTraits::RepeatedFieldType*
+RepeatedMessageGenericTypeTraits::default_repeated_field_ = NULL;
+
+#define PROTOBUF_DEFINE_DEFAULT_REPEATED(TYPE) \
+ const RepeatedField<TYPE>* \
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_##TYPE##_ = NULL;
+
+PROTOBUF_DEFINE_DEFAULT_REPEATED(int32)
+PROTOBUF_DEFINE_DEFAULT_REPEATED(int64)
+PROTOBUF_DEFINE_DEFAULT_REPEATED(uint32)
+PROTOBUF_DEFINE_DEFAULT_REPEATED(uint64)
+PROTOBUF_DEFINE_DEFAULT_REPEATED(double)
+PROTOBUF_DEFINE_DEFAULT_REPEATED(float)
+PROTOBUF_DEFINE_DEFAULT_REPEATED(bool)
+
+#undef PROTOBUF_DEFINE_DEFAULT_REPEATED
+
+struct StaticDefaultRepeatedFieldsInitializer {
+ StaticDefaultRepeatedFieldsInitializer() {
+ InitializeDefaultRepeatedFields();
+ OnShutdown(&DestroyDefaultRepeatedFields);
+ }
+} static_repeated_fields_initializer;
+
+void InitializeDefaultRepeatedFields() {
+ RepeatedStringTypeTraits::default_repeated_field_ =
+ new RepeatedStringTypeTraits::RepeatedFieldType;
+ RepeatedMessageGenericTypeTraits::default_repeated_field_ =
+ new RepeatedMessageGenericTypeTraits::RepeatedFieldType;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_int32_ =
+ new RepeatedField<int32>;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_int64_ =
+ new RepeatedField<int64>;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_uint32_ =
+ new RepeatedField<uint32>;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_uint64_ =
+ new RepeatedField<uint64>;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_double_ =
+ new RepeatedField<double>;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_float_ =
+ new RepeatedField<float>;
+ RepeatedPrimitiveGenericTypeTraits::default_repeated_field_bool_ =
+ new RepeatedField<bool>;
+}
+
+void DestroyDefaultRepeatedFields() {
+ delete RepeatedStringTypeTraits::default_repeated_field_;
+ delete RepeatedMessageGenericTypeTraits::default_repeated_field_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_int32_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_int64_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_uint32_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_uint64_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_double_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_float_;
+ delete RepeatedPrimitiveGenericTypeTraits::default_repeated_field_bool_;
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/extension_set.h b/toolkit/components/protobuf/src/google/protobuf/extension_set.h
new file mode 100644
index 0000000000..d7ec519247
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/extension_set.h
@@ -0,0 +1,1234 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This header is logically internal, but is made public because it is used
+// from protocol-compiler-generated code, which may reside in other components.
+
+#ifndef GOOGLE_PROTOBUF_EXTENSION_SET_H__
+#define GOOGLE_PROTOBUF_EXTENSION_SET_H__
+
+#include <vector>
+#include <map>
+#include <utility>
+#include <string>
+
+
+#include <google/protobuf/stubs/common.h>
+
+#include <google/protobuf/repeated_field.h>
+
+namespace google {
+
+namespace protobuf {
+ class Descriptor; // descriptor.h
+ class FieldDescriptor; // descriptor.h
+ class DescriptorPool; // descriptor.h
+ class MessageLite; // message_lite.h
+ class Message; // message.h
+ class MessageFactory; // message.h
+ class UnknownFieldSet; // unknown_field_set.h
+ namespace io {
+ class CodedInputStream; // coded_stream.h
+ class CodedOutputStream; // coded_stream.h
+ }
+ namespace internal {
+ class FieldSkipper; // wire_format_lite.h
+ }
+}
+
+namespace protobuf {
+namespace internal {
+
+// Used to store values of type WireFormatLite::FieldType without having to
+// #include wire_format_lite.h. Also, ensures that we use only one byte to
+// store these values, which is important to keep the layout of
+// ExtensionSet::Extension small.
+typedef uint8 FieldType;
+
+// A function which, given an integer value, returns true if the number
+// matches one of the defined values for the corresponding enum type. This
+// is used with RegisterEnumExtension, below.
+typedef bool EnumValidityFunc(int number);
+
+// Version of the above which takes an argument. This is needed to deal with
+// extensions that are not compiled in.
+typedef bool EnumValidityFuncWithArg(const void* arg, int number);
+
+// Information about a registered extension.
+struct ExtensionInfo {
+ inline ExtensionInfo() {}
+ inline ExtensionInfo(FieldType type_param, bool isrepeated, bool ispacked)
+ : type(type_param), is_repeated(isrepeated), is_packed(ispacked),
+ descriptor(NULL) {}
+
+ FieldType type;
+ bool is_repeated;
+ bool is_packed;
+
+ struct EnumValidityCheck {
+ EnumValidityFuncWithArg* func;
+ const void* arg;
+ };
+
+ union {
+ EnumValidityCheck enum_validity_check;
+ const MessageLite* message_prototype;
+ };
+
+ // The descriptor for this extension, if one exists and is known. May be
+ // NULL. Must not be NULL if the descriptor for the extension does not
+ // live in the same pool as the descriptor for the containing type.
+ const FieldDescriptor* descriptor;
+};
+
+// Abstract interface for an object which looks up extension definitions. Used
+// when parsing.
+class LIBPROTOBUF_EXPORT ExtensionFinder {
+ public:
+ virtual ~ExtensionFinder();
+
+ // Find the extension with the given containing type and number.
+ virtual bool Find(int number, ExtensionInfo* output) = 0;
+};
+
+// Implementation of ExtensionFinder which finds extensions defined in .proto
+// files which have been compiled into the binary.
+class LIBPROTOBUF_EXPORT GeneratedExtensionFinder : public ExtensionFinder {
+ public:
+ GeneratedExtensionFinder(const MessageLite* containing_type)
+ : containing_type_(containing_type) {}
+ virtual ~GeneratedExtensionFinder() {}
+
+ // Returns true and fills in *output if found, otherwise returns false.
+ virtual bool Find(int number, ExtensionInfo* output);
+
+ private:
+ const MessageLite* containing_type_;
+};
+
+// A FieldSkipper used for parsing MessageSet.
+class MessageSetFieldSkipper;
+
+// Note: extension_set_heavy.cc defines DescriptorPoolExtensionFinder for
+// finding extensions from a DescriptorPool.
+
+// This is an internal helper class intended for use within the protocol buffer
+// library and generated classes. Clients should not use it directly. Instead,
+// use the generated accessors such as GetExtension() of the class being
+// extended.
+//
+// This class manages extensions for a protocol message object. The
+// message's HasExtension(), GetExtension(), MutableExtension(), and
+// ClearExtension() methods are just thin wrappers around the embedded
+// ExtensionSet. When parsing, if a tag number is encountered which is
+// inside one of the message type's extension ranges, the tag is passed
+// off to the ExtensionSet for parsing. Etc.
+class LIBPROTOBUF_EXPORT ExtensionSet {
+ public:
+ ExtensionSet();
+ ~ExtensionSet();
+
+ // These are called at startup by protocol-compiler-generated code to
+ // register known extensions. The registrations are used by ParseField()
+ // to look up extensions for parsed field numbers. Note that dynamic parsing
+ // does not use ParseField(); only protocol-compiler-generated parsing
+ // methods do.
+ static void RegisterExtension(const MessageLite* containing_type,
+ int number, FieldType type,
+ bool is_repeated, bool is_packed);
+ static void RegisterEnumExtension(const MessageLite* containing_type,
+ int number, FieldType type,
+ bool is_repeated, bool is_packed,
+ EnumValidityFunc* is_valid);
+ static void RegisterMessageExtension(const MessageLite* containing_type,
+ int number, FieldType type,
+ bool is_repeated, bool is_packed,
+ const MessageLite* prototype);
+
+ // =================================================================
+
+ // Add all fields which are currently present to the given vector. This
+ // is useful to implement Reflection::ListFields().
+ void AppendToList(const Descriptor* containing_type,
+ const DescriptorPool* pool,
+ vector<const FieldDescriptor*>* output) const;
+
+ // =================================================================
+ // Accessors
+ //
+ // Generated message classes include type-safe templated wrappers around
+ // these methods. Generally you should use those rather than call these
+ // directly, unless you are doing low-level memory management.
+ //
+ // When calling any of these accessors, the extension number requested
+ // MUST exist in the DescriptorPool provided to the constructor. Otheriwse,
+ // the method will fail an assert. Normally, though, you would not call
+ // these directly; you would either call the generated accessors of your
+ // message class (e.g. GetExtension()) or you would call the accessors
+ // of the reflection interface. In both cases, it is impossible to
+ // trigger this assert failure: the generated accessors only accept
+ // linked-in extension types as parameters, while the Reflection interface
+ // requires you to provide the FieldDescriptor describing the extension.
+ //
+ // When calling any of these accessors, a protocol-compiler-generated
+ // implementation of the extension corresponding to the number MUST
+ // be linked in, and the FieldDescriptor used to refer to it MUST be
+ // the one generated by that linked-in code. Otherwise, the method will
+ // die on an assert failure. The message objects returned by the message
+ // accessors are guaranteed to be of the correct linked-in type.
+ //
+ // These methods pretty much match Reflection except that:
+ // - They're not virtual.
+ // - They identify fields by number rather than FieldDescriptors.
+ // - They identify enum values using integers rather than descriptors.
+ // - Strings provide Mutable() in addition to Set() accessors.
+
+ bool Has(int number) const;
+ int ExtensionSize(int number) const; // Size of a repeated extension.
+ int NumExtensions() const; // The number of extensions
+ FieldType ExtensionType(int number) const;
+ void ClearExtension(int number);
+
+ // singular fields -------------------------------------------------
+
+ int32 GetInt32 (int number, int32 default_value) const;
+ int64 GetInt64 (int number, int64 default_value) const;
+ uint32 GetUInt32(int number, uint32 default_value) const;
+ uint64 GetUInt64(int number, uint64 default_value) const;
+ float GetFloat (int number, float default_value) const;
+ double GetDouble(int number, double default_value) const;
+ bool GetBool (int number, bool default_value) const;
+ int GetEnum (int number, int default_value) const;
+ const string & GetString (int number, const string& default_value) const;
+ const MessageLite& GetMessage(int number,
+ const MessageLite& default_value) const;
+ const MessageLite& GetMessage(int number, const Descriptor* message_type,
+ MessageFactory* factory) const;
+
+ // |descriptor| may be NULL so long as it is known that the descriptor for
+ // the extension lives in the same pool as the descriptor for the containing
+ // type.
+#define desc const FieldDescriptor* descriptor // avoid line wrapping
+ void SetInt32 (int number, FieldType type, int32 value, desc);
+ void SetInt64 (int number, FieldType type, int64 value, desc);
+ void SetUInt32(int number, FieldType type, uint32 value, desc);
+ void SetUInt64(int number, FieldType type, uint64 value, desc);
+ void SetFloat (int number, FieldType type, float value, desc);
+ void SetDouble(int number, FieldType type, double value, desc);
+ void SetBool (int number, FieldType type, bool value, desc);
+ void SetEnum (int number, FieldType type, int value, desc);
+ void SetString(int number, FieldType type, const string& value, desc);
+ string * MutableString (int number, FieldType type, desc);
+ MessageLite* MutableMessage(int number, FieldType type,
+ const MessageLite& prototype, desc);
+ MessageLite* MutableMessage(const FieldDescriptor* decsriptor,
+ MessageFactory* factory);
+ // Adds the given message to the ExtensionSet, taking ownership of the
+ // message object. Existing message with the same number will be deleted.
+ // If "message" is NULL, this is equivalent to "ClearExtension(number)".
+ void SetAllocatedMessage(int number, FieldType type,
+ const FieldDescriptor* descriptor,
+ MessageLite* message);
+ MessageLite* ReleaseMessage(int number, const MessageLite& prototype);
+ MessageLite* ReleaseMessage(const FieldDescriptor* descriptor,
+ MessageFactory* factory);
+#undef desc
+
+ // repeated fields -------------------------------------------------
+
+ // Fetches a RepeatedField extension by number; returns |default_value|
+ // if no such extension exists. User should not touch this directly; it is
+ // used by the GetRepeatedExtension() method.
+ const void* GetRawRepeatedField(int number, const void* default_value) const;
+ // Fetches a mutable version of a RepeatedField extension by number,
+ // instantiating one if none exists. Similar to above, user should not use
+ // this directly; it underlies MutableRepeatedExtension().
+ void* MutableRawRepeatedField(int number, FieldType field_type,
+ bool packed, const FieldDescriptor* desc);
+
+ // This is an overload of MutableRawRepeatedField to maintain compatibility
+ // with old code using a previous API. This version of
+ // MutableRawRepeatedField() will GOOGLE_CHECK-fail on a missing extension.
+ // (E.g.: borg/clients/internal/proto1/proto2_reflection.cc.)
+ void* MutableRawRepeatedField(int number);
+
+ int32 GetRepeatedInt32 (int number, int index) const;
+ int64 GetRepeatedInt64 (int number, int index) const;
+ uint32 GetRepeatedUInt32(int number, int index) const;
+ uint64 GetRepeatedUInt64(int number, int index) const;
+ float GetRepeatedFloat (int number, int index) const;
+ double GetRepeatedDouble(int number, int index) const;
+ bool GetRepeatedBool (int number, int index) const;
+ int GetRepeatedEnum (int number, int index) const;
+ const string & GetRepeatedString (int number, int index) const;
+ const MessageLite& GetRepeatedMessage(int number, int index) const;
+
+ void SetRepeatedInt32 (int number, int index, int32 value);
+ void SetRepeatedInt64 (int number, int index, int64 value);
+ void SetRepeatedUInt32(int number, int index, uint32 value);
+ void SetRepeatedUInt64(int number, int index, uint64 value);
+ void SetRepeatedFloat (int number, int index, float value);
+ void SetRepeatedDouble(int number, int index, double value);
+ void SetRepeatedBool (int number, int index, bool value);
+ void SetRepeatedEnum (int number, int index, int value);
+ void SetRepeatedString(int number, int index, const string& value);
+ string * MutableRepeatedString (int number, int index);
+ MessageLite* MutableRepeatedMessage(int number, int index);
+
+#define desc const FieldDescriptor* descriptor // avoid line wrapping
+ void AddInt32 (int number, FieldType type, bool packed, int32 value, desc);
+ void AddInt64 (int number, FieldType type, bool packed, int64 value, desc);
+ void AddUInt32(int number, FieldType type, bool packed, uint32 value, desc);
+ void AddUInt64(int number, FieldType type, bool packed, uint64 value, desc);
+ void AddFloat (int number, FieldType type, bool packed, float value, desc);
+ void AddDouble(int number, FieldType type, bool packed, double value, desc);
+ void AddBool (int number, FieldType type, bool packed, bool value, desc);
+ void AddEnum (int number, FieldType type, bool packed, int value, desc);
+ void AddString(int number, FieldType type, const string& value, desc);
+ string * AddString (int number, FieldType type, desc);
+ MessageLite* AddMessage(int number, FieldType type,
+ const MessageLite& prototype, desc);
+ MessageLite* AddMessage(const FieldDescriptor* descriptor,
+ MessageFactory* factory);
+#undef desc
+
+ void RemoveLast(int number);
+ MessageLite* ReleaseLast(int number);
+ void SwapElements(int number, int index1, int index2);
+
+ // -----------------------------------------------------------------
+ // TODO(kenton): Hardcore memory management accessors
+
+ // =================================================================
+ // convenience methods for implementing methods of Message
+ //
+ // These could all be implemented in terms of the other methods of this
+ // class, but providing them here helps keep the generated code size down.
+
+ void Clear();
+ void MergeFrom(const ExtensionSet& other);
+ void Swap(ExtensionSet* other);
+ void SwapExtension(ExtensionSet* other, int number);
+ bool IsInitialized() const;
+
+ // Parses a single extension from the input. The input should start out
+ // positioned immediately after the tag.
+ bool ParseField(uint32 tag, io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ FieldSkipper* field_skipper);
+
+ // Specific versions for lite or full messages (constructs the appropriate
+ // FieldSkipper automatically). |containing_type| is the default
+ // instance for the containing message; it is used only to look up the
+ // extension by number. See RegisterExtension(), above. Unlike the other
+ // methods of ExtensionSet, this only works for generated message types --
+ // it looks up extensions registered using RegisterExtension().
+ bool ParseField(uint32 tag, io::CodedInputStream* input,
+ const MessageLite* containing_type);
+ bool ParseField(uint32 tag, io::CodedInputStream* input,
+ const Message* containing_type,
+ UnknownFieldSet* unknown_fields);
+ bool ParseField(uint32 tag, io::CodedInputStream* input,
+ const MessageLite* containing_type,
+ io::CodedOutputStream* unknown_fields);
+
+ // Parse an entire message in MessageSet format. Such messages have no
+ // fields, only extensions.
+ bool ParseMessageSet(io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ MessageSetFieldSkipper* field_skipper);
+
+ // Specific versions for lite or full messages (constructs the appropriate
+ // FieldSkipper automatically).
+ bool ParseMessageSet(io::CodedInputStream* input,
+ const MessageLite* containing_type);
+ bool ParseMessageSet(io::CodedInputStream* input,
+ const Message* containing_type,
+ UnknownFieldSet* unknown_fields);
+
+ // Write all extension fields with field numbers in the range
+ // [start_field_number, end_field_number)
+ // to the output stream, using the cached sizes computed when ByteSize() was
+ // last called. Note that the range bounds are inclusive-exclusive.
+ void SerializeWithCachedSizes(int start_field_number,
+ int end_field_number,
+ io::CodedOutputStream* output) const;
+
+ // Same as SerializeWithCachedSizes, but without any bounds checking.
+ // The caller must ensure that target has sufficient capacity for the
+ // serialized extensions.
+ //
+ // Returns a pointer past the last written byte.
+ uint8* SerializeWithCachedSizesToArray(int start_field_number,
+ int end_field_number,
+ uint8* target) const;
+
+ // Like above but serializes in MessageSet format.
+ void SerializeMessageSetWithCachedSizes(io::CodedOutputStream* output) const;
+ uint8* SerializeMessageSetWithCachedSizesToArray(uint8* target) const;
+
+ // Returns the total serialized size of all the extensions.
+ int ByteSize() const;
+
+ // Like ByteSize() but uses MessageSet format.
+ int MessageSetByteSize() const;
+
+ // Returns (an estimate of) the total number of bytes used for storing the
+ // extensions in memory, excluding sizeof(*this). If the ExtensionSet is
+ // for a lite message (and thus possibly contains lite messages), the results
+ // are undefined (might work, might crash, might corrupt data, might not even
+ // be linked in). It's up to the protocol compiler to avoid calling this on
+ // such ExtensionSets (easy enough since lite messages don't implement
+ // SpaceUsed()).
+ int SpaceUsedExcludingSelf() const;
+
+ private:
+
+ // Interface of a lazily parsed singular message extension.
+ class LIBPROTOBUF_EXPORT LazyMessageExtension {
+ public:
+ LazyMessageExtension() {}
+ virtual ~LazyMessageExtension() {}
+
+ virtual LazyMessageExtension* New() const = 0;
+ virtual const MessageLite& GetMessage(
+ const MessageLite& prototype) const = 0;
+ virtual MessageLite* MutableMessage(const MessageLite& prototype) = 0;
+ virtual void SetAllocatedMessage(MessageLite *message) = 0;
+ virtual MessageLite* ReleaseMessage(const MessageLite& prototype) = 0;
+
+ virtual bool IsInitialized() const = 0;
+ virtual int ByteSize() const = 0;
+ virtual int SpaceUsed() const = 0;
+
+ virtual void MergeFrom(const LazyMessageExtension& other) = 0;
+ virtual void Clear() = 0;
+
+ virtual bool ReadMessage(const MessageLite& prototype,
+ io::CodedInputStream* input) = 0;
+ virtual void WriteMessage(int number,
+ io::CodedOutputStream* output) const = 0;
+ virtual uint8* WriteMessageToArray(int number, uint8* target) const = 0;
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(LazyMessageExtension);
+ };
+ struct Extension {
+ // The order of these fields packs Extension into 24 bytes when using 8
+ // byte alignment. Consider this when adding or removing fields here.
+ union {
+ int32 int32_value;
+ int64 int64_value;
+ uint32 uint32_value;
+ uint64 uint64_value;
+ float float_value;
+ double double_value;
+ bool bool_value;
+ int enum_value;
+ string* string_value;
+ MessageLite* message_value;
+ LazyMessageExtension* lazymessage_value;
+
+ RepeatedField <int32 >* repeated_int32_value;
+ RepeatedField <int64 >* repeated_int64_value;
+ RepeatedField <uint32 >* repeated_uint32_value;
+ RepeatedField <uint64 >* repeated_uint64_value;
+ RepeatedField <float >* repeated_float_value;
+ RepeatedField <double >* repeated_double_value;
+ RepeatedField <bool >* repeated_bool_value;
+ RepeatedField <int >* repeated_enum_value;
+ RepeatedPtrField<string >* repeated_string_value;
+ RepeatedPtrField<MessageLite>* repeated_message_value;
+ };
+
+ FieldType type;
+ bool is_repeated;
+
+ // For singular types, indicates if the extension is "cleared". This
+ // happens when an extension is set and then later cleared by the caller.
+ // We want to keep the Extension object around for reuse, so instead of
+ // removing it from the map, we just set is_cleared = true. This has no
+ // meaning for repeated types; for those, the size of the RepeatedField
+ // simply becomes zero when cleared.
+ bool is_cleared : 4;
+
+ // For singular message types, indicates whether lazy parsing is enabled
+ // for this extension. This field is only valid when type == TYPE_MESSAGE
+ // and !is_repeated because we only support lazy parsing for singular
+ // message types currently. If is_lazy = true, the extension is stored in
+ // lazymessage_value. Otherwise, the extension will be message_value.
+ bool is_lazy : 4;
+
+ // For repeated types, this indicates if the [packed=true] option is set.
+ bool is_packed;
+
+ // For packed fields, the size of the packed data is recorded here when
+ // ByteSize() is called then used during serialization.
+ // TODO(kenton): Use atomic<int> when C++ supports it.
+ mutable int cached_size;
+
+ // The descriptor for this extension, if one exists and is known. May be
+ // NULL. Must not be NULL if the descriptor for the extension does not
+ // live in the same pool as the descriptor for the containing type.
+ const FieldDescriptor* descriptor;
+
+ // Some helper methods for operations on a single Extension.
+ void SerializeFieldWithCachedSizes(
+ int number,
+ io::CodedOutputStream* output) const;
+ uint8* SerializeFieldWithCachedSizesToArray(
+ int number,
+ uint8* target) const;
+ void SerializeMessageSetItemWithCachedSizes(
+ int number,
+ io::CodedOutputStream* output) const;
+ uint8* SerializeMessageSetItemWithCachedSizesToArray(
+ int number,
+ uint8* target) const;
+ int ByteSize(int number) const;
+ int MessageSetItemByteSize(int number) const;
+ void Clear();
+ int GetSize() const;
+ void Free();
+ int SpaceUsedExcludingSelf() const;
+ };
+
+
+ // Returns true and fills field_number and extension if extension is found.
+ // Note to support packed repeated field compatibility, it also fills whether
+ // the tag on wire is packed, which can be different from
+ // extension->is_packed (whether packed=true is specified).
+ bool FindExtensionInfoFromTag(uint32 tag, ExtensionFinder* extension_finder,
+ int* field_number, ExtensionInfo* extension,
+ bool* was_packed_on_wire);
+
+ // Returns true and fills extension if extension is found.
+ // Note to support packed repeated field compatibility, it also fills whether
+ // the tag on wire is packed, which can be different from
+ // extension->is_packed (whether packed=true is specified).
+ bool FindExtensionInfoFromFieldNumber(int wire_type, int field_number,
+ ExtensionFinder* extension_finder,
+ ExtensionInfo* extension,
+ bool* was_packed_on_wire);
+
+ // Parses a single extension from the input. The input should start out
+ // positioned immediately after the wire tag. This method is called in
+ // ParseField() after field number and was_packed_on_wire is extracted from
+ // the wire tag and ExtensionInfo is found by the field number.
+ bool ParseFieldWithExtensionInfo(int field_number,
+ bool was_packed_on_wire,
+ const ExtensionInfo& extension,
+ io::CodedInputStream* input,
+ FieldSkipper* field_skipper);
+
+ // Like ParseField(), but this method may parse singular message extensions
+ // lazily depending on the value of FLAGS_eagerly_parse_message_sets.
+ bool ParseFieldMaybeLazily(int wire_type, int field_number,
+ io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ MessageSetFieldSkipper* field_skipper);
+
+ // Gets the extension with the given number, creating it if it does not
+ // already exist. Returns true if the extension did not already exist.
+ bool MaybeNewExtension(int number, const FieldDescriptor* descriptor,
+ Extension** result);
+
+ // Parse a single MessageSet item -- called just after the item group start
+ // tag has been read.
+ bool ParseMessageSetItem(io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ MessageSetFieldSkipper* field_skipper);
+
+
+ // Hack: RepeatedPtrFieldBase declares ExtensionSet as a friend. This
+ // friendship should automatically extend to ExtensionSet::Extension, but
+ // unfortunately some older compilers (e.g. GCC 3.4.4) do not implement this
+ // correctly. So, we must provide helpers for calling methods of that
+ // class.
+
+ // Defined in extension_set_heavy.cc.
+ static inline int RepeatedMessage_SpaceUsedExcludingSelf(
+ RepeatedPtrFieldBase* field);
+
+ // The Extension struct is small enough to be passed by value, so we use it
+ // directly as the value type in the map rather than use pointers. We use
+ // a map rather than hash_map here because we expect most ExtensionSets will
+ // only contain a small number of extensions whereas hash_map is optimized
+ // for 100 elements or more. Also, we want AppendToList() to order fields
+ // by field number.
+ std::map<int, Extension> extensions_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ExtensionSet);
+};
+
+// These are just for convenience...
+inline void ExtensionSet::SetString(int number, FieldType type,
+ const string& value,
+ const FieldDescriptor* descriptor) {
+ MutableString(number, type, descriptor)->assign(value);
+}
+inline void ExtensionSet::SetRepeatedString(int number, int index,
+ const string& value) {
+ MutableRepeatedString(number, index)->assign(value);
+}
+inline void ExtensionSet::AddString(int number, FieldType type,
+ const string& value,
+ const FieldDescriptor* descriptor) {
+ AddString(number, type, descriptor)->assign(value);
+}
+
+// ===================================================================
+// Glue for generated extension accessors
+
+// -------------------------------------------------------------------
+// Template magic
+
+// First we have a set of classes representing "type traits" for different
+// field types. A type traits class knows how to implement basic accessors
+// for extensions of a particular type given an ExtensionSet. The signature
+// for a type traits class looks like this:
+//
+// class TypeTraits {
+// public:
+// typedef ? ConstType;
+// typedef ? MutableType;
+// // TypeTraits for singular fields and repeated fields will define the
+// // symbol "Singular" or "Repeated" respectively. These two symbols will
+// // be used in extension accessors to distinguish between singular
+// // extensions and repeated extensions. If the TypeTraits for the passed
+// // in extension doesn't have the expected symbol defined, it means the
+// // user is passing a repeated extension to a singular accessor, or the
+// // opposite. In that case the C++ compiler will generate an error
+// // message "no matching member function" to inform the user.
+// typedef ? Singular
+// typedef ? Repeated
+//
+// static inline ConstType Get(int number, const ExtensionSet& set);
+// static inline void Set(int number, ConstType value, ExtensionSet* set);
+// static inline MutableType Mutable(int number, ExtensionSet* set);
+//
+// // Variants for repeated fields.
+// static inline ConstType Get(int number, const ExtensionSet& set,
+// int index);
+// static inline void Set(int number, int index,
+// ConstType value, ExtensionSet* set);
+// static inline MutableType Mutable(int number, int index,
+// ExtensionSet* set);
+// static inline void Add(int number, ConstType value, ExtensionSet* set);
+// static inline MutableType Add(int number, ExtensionSet* set);
+// };
+//
+// Not all of these methods make sense for all field types. For example, the
+// "Mutable" methods only make sense for strings and messages, and the
+// repeated methods only make sense for repeated types. So, each type
+// traits class implements only the set of methods from this signature that it
+// actually supports. This will cause a compiler error if the user tries to
+// access an extension using a method that doesn't make sense for its type.
+// For example, if "foo" is an extension of type "optional int32", then if you
+// try to write code like:
+// my_message.MutableExtension(foo)
+// you will get a compile error because PrimitiveTypeTraits<int32> does not
+// have a "Mutable()" method.
+
+// -------------------------------------------------------------------
+// PrimitiveTypeTraits
+
+// Since the ExtensionSet has different methods for each primitive type,
+// we must explicitly define the methods of the type traits class for each
+// known type.
+template <typename Type>
+class PrimitiveTypeTraits {
+ public:
+ typedef Type ConstType;
+ typedef Type MutableType;
+ typedef PrimitiveTypeTraits<Type> Singular;
+
+ static inline ConstType Get(int number, const ExtensionSet& set,
+ ConstType default_value);
+ static inline void Set(int number, FieldType field_type,
+ ConstType value, ExtensionSet* set);
+};
+
+template <typename Type>
+class RepeatedPrimitiveTypeTraits {
+ public:
+ typedef Type ConstType;
+ typedef Type MutableType;
+ typedef RepeatedPrimitiveTypeTraits<Type> Repeated;
+
+ typedef RepeatedField<Type> RepeatedFieldType;
+
+ static inline Type Get(int number, const ExtensionSet& set, int index);
+ static inline void Set(int number, int index, Type value, ExtensionSet* set);
+ static inline void Add(int number, FieldType field_type,
+ bool is_packed, Type value, ExtensionSet* set);
+
+ static inline const RepeatedField<ConstType>&
+ GetRepeated(int number, const ExtensionSet& set);
+ static inline RepeatedField<Type>*
+ MutableRepeated(int number, FieldType field_type,
+ bool is_packed, ExtensionSet* set);
+
+ static const RepeatedFieldType* GetDefaultRepeatedField();
+};
+
+// Declared here so that this can be friended below.
+void InitializeDefaultRepeatedFields();
+void DestroyDefaultRepeatedFields();
+
+class LIBPROTOBUF_EXPORT RepeatedPrimitiveGenericTypeTraits {
+ private:
+ template<typename Type> friend class RepeatedPrimitiveTypeTraits;
+ friend void InitializeDefaultRepeatedFields();
+ friend void DestroyDefaultRepeatedFields();
+ static const RepeatedField<int32>* default_repeated_field_int32_;
+ static const RepeatedField<int64>* default_repeated_field_int64_;
+ static const RepeatedField<uint32>* default_repeated_field_uint32_;
+ static const RepeatedField<uint64>* default_repeated_field_uint64_;
+ static const RepeatedField<double>* default_repeated_field_double_;
+ static const RepeatedField<float>* default_repeated_field_float_;
+ static const RepeatedField<bool>* default_repeated_field_bool_;
+};
+
+#define PROTOBUF_DEFINE_PRIMITIVE_TYPE(TYPE, METHOD) \
+template<> inline TYPE PrimitiveTypeTraits<TYPE>::Get( \
+ int number, const ExtensionSet& set, TYPE default_value) { \
+ return set.Get##METHOD(number, default_value); \
+} \
+template<> inline void PrimitiveTypeTraits<TYPE>::Set( \
+ int number, FieldType field_type, TYPE value, ExtensionSet* set) { \
+ set->Set##METHOD(number, field_type, value, NULL); \
+} \
+ \
+template<> inline TYPE RepeatedPrimitiveTypeTraits<TYPE>::Get( \
+ int number, const ExtensionSet& set, int index) { \
+ return set.GetRepeated##METHOD(number, index); \
+} \
+template<> inline void RepeatedPrimitiveTypeTraits<TYPE>::Set( \
+ int number, int index, TYPE value, ExtensionSet* set) { \
+ set->SetRepeated##METHOD(number, index, value); \
+} \
+template<> inline void RepeatedPrimitiveTypeTraits<TYPE>::Add( \
+ int number, FieldType field_type, bool is_packed, \
+ TYPE value, ExtensionSet* set) { \
+ set->Add##METHOD(number, field_type, is_packed, value, NULL); \
+} \
+template<> inline const RepeatedField<TYPE>* \
+ RepeatedPrimitiveTypeTraits<TYPE>::GetDefaultRepeatedField() { \
+ return RepeatedPrimitiveGenericTypeTraits:: \
+ default_repeated_field_##TYPE##_; \
+} \
+template<> inline const RepeatedField<TYPE>& \
+ RepeatedPrimitiveTypeTraits<TYPE>::GetRepeated(int number, \
+ const ExtensionSet& set) { \
+ return *reinterpret_cast<const RepeatedField<TYPE>*>( \
+ set.GetRawRepeatedField( \
+ number, GetDefaultRepeatedField())); \
+} \
+template<> inline RepeatedField<TYPE>* \
+ RepeatedPrimitiveTypeTraits<TYPE>::MutableRepeated(int number, \
+ FieldType field_type, \
+ bool is_packed, \
+ ExtensionSet* set) { \
+ return reinterpret_cast<RepeatedField<TYPE>*>( \
+ set->MutableRawRepeatedField(number, field_type, is_packed, NULL)); \
+}
+
+PROTOBUF_DEFINE_PRIMITIVE_TYPE( int32, Int32)
+PROTOBUF_DEFINE_PRIMITIVE_TYPE( int64, Int64)
+PROTOBUF_DEFINE_PRIMITIVE_TYPE(uint32, UInt32)
+PROTOBUF_DEFINE_PRIMITIVE_TYPE(uint64, UInt64)
+PROTOBUF_DEFINE_PRIMITIVE_TYPE( float, Float)
+PROTOBUF_DEFINE_PRIMITIVE_TYPE(double, Double)
+PROTOBUF_DEFINE_PRIMITIVE_TYPE( bool, Bool)
+
+#undef PROTOBUF_DEFINE_PRIMITIVE_TYPE
+
+// -------------------------------------------------------------------
+// StringTypeTraits
+
+// Strings support both Set() and Mutable().
+class LIBPROTOBUF_EXPORT StringTypeTraits {
+ public:
+ typedef const string& ConstType;
+ typedef string* MutableType;
+ typedef StringTypeTraits Singular;
+
+ static inline const string& Get(int number, const ExtensionSet& set,
+ ConstType default_value) {
+ return set.GetString(number, default_value);
+ }
+ static inline void Set(int number, FieldType field_type,
+ const string& value, ExtensionSet* set) {
+ set->SetString(number, field_type, value, NULL);
+ }
+ static inline string* Mutable(int number, FieldType field_type,
+ ExtensionSet* set) {
+ return set->MutableString(number, field_type, NULL);
+ }
+};
+
+class LIBPROTOBUF_EXPORT RepeatedStringTypeTraits {
+ public:
+ typedef const string& ConstType;
+ typedef string* MutableType;
+ typedef RepeatedStringTypeTraits Repeated;
+
+ typedef RepeatedPtrField<string> RepeatedFieldType;
+
+ static inline const string& Get(int number, const ExtensionSet& set,
+ int index) {
+ return set.GetRepeatedString(number, index);
+ }
+ static inline void Set(int number, int index,
+ const string& value, ExtensionSet* set) {
+ set->SetRepeatedString(number, index, value);
+ }
+ static inline string* Mutable(int number, int index, ExtensionSet* set) {
+ return set->MutableRepeatedString(number, index);
+ }
+ static inline void Add(int number, FieldType field_type,
+ bool /*is_packed*/, const string& value,
+ ExtensionSet* set) {
+ set->AddString(number, field_type, value, NULL);
+ }
+ static inline string* Add(int number, FieldType field_type,
+ ExtensionSet* set) {
+ return set->AddString(number, field_type, NULL);
+ }
+ static inline const RepeatedPtrField<string>&
+ GetRepeated(int number, const ExtensionSet& set) {
+ return *reinterpret_cast<const RepeatedPtrField<string>*>(
+ set.GetRawRepeatedField(number, GetDefaultRepeatedField()));
+ }
+
+ static inline RepeatedPtrField<string>*
+ MutableRepeated(int number, FieldType field_type,
+ bool is_packed, ExtensionSet* set) {
+ return reinterpret_cast<RepeatedPtrField<string>*>(
+ set->MutableRawRepeatedField(number, field_type,
+ is_packed, NULL));
+ }
+
+ static const RepeatedFieldType* GetDefaultRepeatedField() {
+ return default_repeated_field_;
+ }
+
+ private:
+ friend void InitializeDefaultRepeatedFields();
+ friend void DestroyDefaultRepeatedFields();
+ static const RepeatedFieldType *default_repeated_field_;
+};
+
+// -------------------------------------------------------------------
+// EnumTypeTraits
+
+// ExtensionSet represents enums using integers internally, so we have to
+// static_cast around.
+template <typename Type, bool IsValid(int)>
+class EnumTypeTraits {
+ public:
+ typedef Type ConstType;
+ typedef Type MutableType;
+ typedef EnumTypeTraits<Type, IsValid> Singular;
+
+ static inline ConstType Get(int number, const ExtensionSet& set,
+ ConstType default_value) {
+ return static_cast<Type>(set.GetEnum(number, default_value));
+ }
+ static inline void Set(int number, FieldType field_type,
+ ConstType value, ExtensionSet* set) {
+ GOOGLE_DCHECK(IsValid(value));
+ set->SetEnum(number, field_type, value, NULL);
+ }
+};
+
+template <typename Type, bool IsValid(int)>
+class RepeatedEnumTypeTraits {
+ public:
+ typedef Type ConstType;
+ typedef Type MutableType;
+ typedef RepeatedEnumTypeTraits<Type, IsValid> Repeated;
+
+ typedef RepeatedField<Type> RepeatedFieldType;
+
+ static inline ConstType Get(int number, const ExtensionSet& set, int index) {
+ return static_cast<Type>(set.GetRepeatedEnum(number, index));
+ }
+ static inline void Set(int number, int index,
+ ConstType value, ExtensionSet* set) {
+ GOOGLE_DCHECK(IsValid(value));
+ set->SetRepeatedEnum(number, index, value);
+ }
+ static inline void Add(int number, FieldType field_type,
+ bool is_packed, ConstType value, ExtensionSet* set) {
+ GOOGLE_DCHECK(IsValid(value));
+ set->AddEnum(number, field_type, is_packed, value, NULL);
+ }
+ static inline const RepeatedField<Type>& GetRepeated(int number,
+ const ExtensionSet&
+ set) {
+ // Hack: the `Extension` struct stores a RepeatedField<int> for enums.
+ // RepeatedField<int> cannot implicitly convert to RepeatedField<EnumType>
+ // so we need to do some casting magic. See message.h for similar
+ // contortions for non-extension fields.
+ return *reinterpret_cast<const RepeatedField<Type>*>(
+ set.GetRawRepeatedField(number, GetDefaultRepeatedField()));
+ }
+
+ static inline RepeatedField<Type>* MutableRepeated(int number,
+ FieldType field_type,
+ bool is_packed,
+ ExtensionSet* set) {
+ return reinterpret_cast<RepeatedField<Type>*>(
+ set->MutableRawRepeatedField(number, field_type, is_packed, NULL));
+ }
+
+ static const RepeatedFieldType* GetDefaultRepeatedField() {
+ // Hack: as noted above, repeated enum fields are internally stored as a
+ // RepeatedField<int>. We need to be able to instantiate global static
+ // objects to return as default (empty) repeated fields on non-existent
+ // extensions. We would not be able to know a-priori all of the enum types
+ // (values of |Type|) to instantiate all of these, so we just re-use int32's
+ // default repeated field object.
+ return reinterpret_cast<const RepeatedField<Type>*>(
+ RepeatedPrimitiveTypeTraits<int32>::GetDefaultRepeatedField());
+ }
+};
+
+// -------------------------------------------------------------------
+// MessageTypeTraits
+
+// ExtensionSet guarantees that when manipulating extensions with message
+// types, the implementation used will be the compiled-in class representing
+// that type. So, we can static_cast down to the exact type we expect.
+template <typename Type>
+class MessageTypeTraits {
+ public:
+ typedef const Type& ConstType;
+ typedef Type* MutableType;
+ typedef MessageTypeTraits<Type> Singular;
+
+ static inline ConstType Get(int number, const ExtensionSet& set,
+ ConstType default_value) {
+ return static_cast<const Type&>(
+ set.GetMessage(number, default_value));
+ }
+ static inline MutableType Mutable(int number, FieldType field_type,
+ ExtensionSet* set) {
+ return static_cast<Type*>(
+ set->MutableMessage(number, field_type, Type::default_instance(), NULL));
+ }
+ static inline void SetAllocated(int number, FieldType field_type,
+ MutableType message, ExtensionSet* set) {
+ set->SetAllocatedMessage(number, field_type, NULL, message);
+ }
+ static inline MutableType Release(int number, FieldType /* field_type */,
+ ExtensionSet* set) {
+ return static_cast<Type*>(set->ReleaseMessage(
+ number, Type::default_instance()));
+ }
+};
+
+// forward declaration
+class RepeatedMessageGenericTypeTraits;
+
+template <typename Type>
+class RepeatedMessageTypeTraits {
+ public:
+ typedef const Type& ConstType;
+ typedef Type* MutableType;
+ typedef RepeatedMessageTypeTraits<Type> Repeated;
+
+ typedef RepeatedPtrField<Type> RepeatedFieldType;
+
+ static inline ConstType Get(int number, const ExtensionSet& set, int index) {
+ return static_cast<const Type&>(set.GetRepeatedMessage(number, index));
+ }
+ static inline MutableType Mutable(int number, int index, ExtensionSet* set) {
+ return static_cast<Type*>(set->MutableRepeatedMessage(number, index));
+ }
+ static inline MutableType Add(int number, FieldType field_type,
+ ExtensionSet* set) {
+ return static_cast<Type*>(
+ set->AddMessage(number, field_type, Type::default_instance(), NULL));
+ }
+ static inline const RepeatedPtrField<Type>& GetRepeated(int number,
+ const ExtensionSet&
+ set) {
+ // See notes above in RepeatedEnumTypeTraits::GetRepeated(): same
+ // casting hack applies here, because a RepeatedPtrField<MessageLite>
+ // cannot naturally become a RepeatedPtrType<Type> even though Type is
+ // presumably a message. google::protobuf::Message goes through similar contortions
+ // with a reinterpret_cast<>.
+ return *reinterpret_cast<const RepeatedPtrField<Type>*>(
+ set.GetRawRepeatedField(number, GetDefaultRepeatedField()));
+ }
+ static inline RepeatedPtrField<Type>* MutableRepeated(int number,
+ FieldType field_type,
+ bool is_packed,
+ ExtensionSet* set) {
+ return reinterpret_cast<RepeatedPtrField<Type>*>(
+ set->MutableRawRepeatedField(number, field_type, is_packed, NULL));
+ }
+
+ static const RepeatedFieldType* GetDefaultRepeatedField();
+};
+
+// This class exists only to hold a generic default empty repeated field for all
+// message-type repeated field extensions.
+class LIBPROTOBUF_EXPORT RepeatedMessageGenericTypeTraits {
+ public:
+ typedef RepeatedPtrField< ::google::protobuf::MessageLite*> RepeatedFieldType;
+ private:
+ template<typename Type> friend class RepeatedMessageTypeTraits;
+ friend void InitializeDefaultRepeatedFields();
+ friend void DestroyDefaultRepeatedFields();
+ static const RepeatedFieldType* default_repeated_field_;
+};
+
+template<typename Type> inline
+ const typename RepeatedMessageTypeTraits<Type>::RepeatedFieldType*
+ RepeatedMessageTypeTraits<Type>::GetDefaultRepeatedField() {
+ return reinterpret_cast<const RepeatedFieldType*>(
+ RepeatedMessageGenericTypeTraits::default_repeated_field_);
+}
+
+// -------------------------------------------------------------------
+// ExtensionIdentifier
+
+// This is the type of actual extension objects. E.g. if you have:
+// extends Foo with optional int32 bar = 1234;
+// then "bar" will be defined in C++ as:
+// ExtensionIdentifier<Foo, PrimitiveTypeTraits<int32>, 1, false> bar(1234);
+//
+// Note that we could, in theory, supply the field number as a template
+// parameter, and thus make an instance of ExtensionIdentifier have no
+// actual contents. However, if we did that, then using at extension
+// identifier would not necessarily cause the compiler to output any sort
+// of reference to any simple defined in the extension's .pb.o file. Some
+// linkers will actually drop object files that are not explicitly referenced,
+// but that would be bad because it would cause this extension to not be
+// registered at static initialization, and therefore using it would crash.
+
+template <typename ExtendeeType, typename TypeTraitsType,
+ FieldType field_type, bool is_packed>
+class ExtensionIdentifier {
+ public:
+ typedef TypeTraitsType TypeTraits;
+ typedef ExtendeeType Extendee;
+
+ ExtensionIdentifier(int number, typename TypeTraits::ConstType default_value)
+ : number_(number), default_value_(default_value) {}
+ inline int number() const { return number_; }
+ typename TypeTraits::ConstType default_value() const {
+ return default_value_;
+ }
+
+ private:
+ const int number_;
+ typename TypeTraits::ConstType default_value_;
+};
+
+// -------------------------------------------------------------------
+// Generated accessors
+
+// This macro should be expanded in the context of a generated type which
+// has extensions.
+//
+// We use "_proto_TypeTraits" as a type name below because "TypeTraits"
+// causes problems if the class has a nested message or enum type with that
+// name and "_TypeTraits" is technically reserved for the C++ library since
+// it starts with an underscore followed by a capital letter.
+//
+// For similar reason, we use "_field_type" and "_is_packed" as parameter names
+// below, so that "field_type" and "is_packed" can be used as field names.
+#define GOOGLE_PROTOBUF_EXTENSION_ACCESSORS(CLASSNAME) \
+ /* Has, Size, Clear */ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline bool HasExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) const { \
+ return _extensions_.Has(id.number()); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline void ClearExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) { \
+ _extensions_.ClearExtension(id.number()); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline int ExtensionSize( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) const { \
+ return _extensions_.ExtensionSize(id.number()); \
+ } \
+ \
+ /* Singular accessors */ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Singular::ConstType GetExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) const { \
+ return _proto_TypeTraits::Get(id.number(), _extensions_, \
+ id.default_value()); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Singular::MutableType MutableExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) { \
+ return _proto_TypeTraits::Mutable(id.number(), _field_type, \
+ &_extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline void SetExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id, \
+ typename _proto_TypeTraits::Singular::ConstType value) { \
+ _proto_TypeTraits::Set(id.number(), _field_type, value, &_extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline void SetAllocatedExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id, \
+ typename _proto_TypeTraits::Singular::MutableType value) { \
+ _proto_TypeTraits::SetAllocated(id.number(), _field_type, \
+ value, &_extensions_); \
+ } \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Singular::MutableType ReleaseExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) { \
+ return _proto_TypeTraits::Release(id.number(), _field_type, \
+ &_extensions_); \
+ } \
+ \
+ /* Repeated accessors */ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Repeated::ConstType GetExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id, \
+ int index) const { \
+ return _proto_TypeTraits::Get(id.number(), _extensions_, index); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Repeated::MutableType MutableExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id, \
+ int index) { \
+ return _proto_TypeTraits::Mutable(id.number(), index, &_extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline void SetExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id, \
+ int index, typename _proto_TypeTraits::Repeated::ConstType value) { \
+ _proto_TypeTraits::Set(id.number(), index, value, &_extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Repeated::MutableType AddExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id) { \
+ return _proto_TypeTraits::Add(id.number(), _field_type, &_extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline void AddExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, _is_packed>& id, \
+ typename _proto_TypeTraits::Repeated::ConstType value) { \
+ _proto_TypeTraits::Add(id.number(), _field_type, _is_packed, \
+ value, &_extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline const typename _proto_TypeTraits::Repeated::RepeatedFieldType& \
+ GetRepeatedExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, \
+ _is_packed>& id) const { \
+ return _proto_TypeTraits::GetRepeated(id.number(), _extensions_); \
+ } \
+ \
+ template <typename _proto_TypeTraits, \
+ ::google::protobuf::internal::FieldType _field_type, \
+ bool _is_packed> \
+ inline typename _proto_TypeTraits::Repeated::RepeatedFieldType* \
+ MutableRepeatedExtension( \
+ const ::google::protobuf::internal::ExtensionIdentifier< \
+ CLASSNAME, _proto_TypeTraits, _field_type, \
+ _is_packed>& id) { \
+ return _proto_TypeTraits::MutableRepeated(id.number(), _field_type, \
+ _is_packed, &_extensions_); \
+ }
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_EXTENSION_SET_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/extension_set_heavy.cc b/toolkit/components/protobuf/src/google/protobuf/extension_set_heavy.cc
new file mode 100644
index 0000000000..eae4d574f8
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/extension_set_heavy.cc
@@ -0,0 +1,734 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Contains methods defined in extension_set.h which cannot be part of the
+// lite library because they use descriptors or reflection.
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/wire_format.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+
+namespace google {
+
+namespace protobuf {
+namespace internal {
+
+// A FieldSkipper used to store unknown MessageSet fields into UnknownFieldSet.
+class MessageSetFieldSkipper
+ : public UnknownFieldSetFieldSkipper {
+ public:
+ explicit MessageSetFieldSkipper(UnknownFieldSet* unknown_fields)
+ : UnknownFieldSetFieldSkipper(unknown_fields) {}
+ virtual ~MessageSetFieldSkipper() {}
+
+ virtual bool SkipMessageSetField(io::CodedInputStream* input,
+ int field_number);
+};
+bool MessageSetFieldSkipper::SkipMessageSetField(
+ io::CodedInputStream* input, int field_number) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (unknown_fields_ == NULL) {
+ return input->Skip(length);
+ } else {
+ return input->ReadString(
+ unknown_fields_->AddLengthDelimited(field_number), length);
+ }
+}
+
+
+// Implementation of ExtensionFinder which finds extensions in a given
+// DescriptorPool, using the given MessageFactory to construct sub-objects.
+// This class is implemented in extension_set_heavy.cc.
+class DescriptorPoolExtensionFinder : public ExtensionFinder {
+ public:
+ DescriptorPoolExtensionFinder(const DescriptorPool* pool,
+ MessageFactory* factory,
+ const Descriptor* containing_type)
+ : pool_(pool), factory_(factory), containing_type_(containing_type) {}
+ virtual ~DescriptorPoolExtensionFinder() {}
+
+ virtual bool Find(int number, ExtensionInfo* output);
+
+ private:
+ const DescriptorPool* pool_;
+ MessageFactory* factory_;
+ const Descriptor* containing_type_;
+};
+
+void ExtensionSet::AppendToList(const Descriptor* containing_type,
+ const DescriptorPool* pool,
+ vector<const FieldDescriptor*>* output) const {
+ for (map<int, Extension>::const_iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ bool has = false;
+ if (iter->second.is_repeated) {
+ has = iter->second.GetSize() > 0;
+ } else {
+ has = !iter->second.is_cleared;
+ }
+
+ if (has) {
+ // TODO(kenton): Looking up each field by number is somewhat unfortunate.
+ // Is there a better way? The problem is that descriptors are lazily-
+ // initialized, so they might not even be constructed until
+ // AppendToList() is called.
+
+ if (iter->second.descriptor == NULL) {
+ output->push_back(pool->FindExtensionByNumber(
+ containing_type, iter->first));
+ } else {
+ output->push_back(iter->second.descriptor);
+ }
+ }
+ }
+}
+
+inline FieldDescriptor::Type real_type(FieldType type) {
+ GOOGLE_DCHECK(type > 0 && type <= FieldDescriptor::MAX_TYPE);
+ return static_cast<FieldDescriptor::Type>(type);
+}
+
+inline FieldDescriptor::CppType cpp_type(FieldType type) {
+ return FieldDescriptor::TypeToCppType(
+ static_cast<FieldDescriptor::Type>(type));
+}
+
+inline WireFormatLite::FieldType field_type(FieldType type) {
+ GOOGLE_DCHECK(type > 0 && type <= WireFormatLite::MAX_FIELD_TYPE);
+ return static_cast<WireFormatLite::FieldType>(type);
+}
+
+#define GOOGLE_DCHECK_TYPE(EXTENSION, LABEL, CPPTYPE) \
+ GOOGLE_DCHECK_EQ((EXTENSION).is_repeated ? FieldDescriptor::LABEL_REPEATED \
+ : FieldDescriptor::LABEL_OPTIONAL, \
+ FieldDescriptor::LABEL_##LABEL); \
+ GOOGLE_DCHECK_EQ(cpp_type((EXTENSION).type), FieldDescriptor::CPPTYPE_##CPPTYPE)
+
+const MessageLite& ExtensionSet::GetMessage(int number,
+ const Descriptor* message_type,
+ MessageFactory* factory) const {
+ map<int, Extension>::const_iterator iter = extensions_.find(number);
+ if (iter == extensions_.end() || iter->second.is_cleared) {
+ // Not present. Return the default value.
+ return *factory->GetPrototype(message_type);
+ } else {
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, MESSAGE);
+ if (iter->second.is_lazy) {
+ return iter->second.lazymessage_value->GetMessage(
+ *factory->GetPrototype(message_type));
+ } else {
+ return *iter->second.message_value;
+ }
+ }
+}
+
+MessageLite* ExtensionSet::MutableMessage(const FieldDescriptor* descriptor,
+ MessageFactory* factory) {
+ Extension* extension;
+ if (MaybeNewExtension(descriptor->number(), descriptor, &extension)) {
+ extension->type = descriptor->type();
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), FieldDescriptor::CPPTYPE_MESSAGE);
+ extension->is_repeated = false;
+ extension->is_packed = false;
+ const MessageLite* prototype =
+ factory->GetPrototype(descriptor->message_type());
+ extension->is_lazy = false;
+ extension->message_value = prototype->New();
+ extension->is_cleared = false;
+ return extension->message_value;
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, OPTIONAL, MESSAGE);
+ extension->is_cleared = false;
+ if (extension->is_lazy) {
+ return extension->lazymessage_value->MutableMessage(
+ *factory->GetPrototype(descriptor->message_type()));
+ } else {
+ return extension->message_value;
+ }
+ }
+}
+
+MessageLite* ExtensionSet::ReleaseMessage(const FieldDescriptor* descriptor,
+ MessageFactory* factory) {
+ map<int, Extension>::iterator iter = extensions_.find(descriptor->number());
+ if (iter == extensions_.end()) {
+ // Not present. Return NULL.
+ return NULL;
+ } else {
+ GOOGLE_DCHECK_TYPE(iter->second, OPTIONAL, MESSAGE);
+ MessageLite* ret = NULL;
+ if (iter->second.is_lazy) {
+ ret = iter->second.lazymessage_value->ReleaseMessage(
+ *factory->GetPrototype(descriptor->message_type()));
+ delete iter->second.lazymessage_value;
+ } else {
+ ret = iter->second.message_value;
+ }
+ extensions_.erase(descriptor->number());
+ return ret;
+ }
+}
+
+MessageLite* ExtensionSet::AddMessage(const FieldDescriptor* descriptor,
+ MessageFactory* factory) {
+ Extension* extension;
+ if (MaybeNewExtension(descriptor->number(), descriptor, &extension)) {
+ extension->type = descriptor->type();
+ GOOGLE_DCHECK_EQ(cpp_type(extension->type), FieldDescriptor::CPPTYPE_MESSAGE);
+ extension->is_repeated = true;
+ extension->repeated_message_value =
+ new RepeatedPtrField<MessageLite>();
+ } else {
+ GOOGLE_DCHECK_TYPE(*extension, REPEATED, MESSAGE);
+ }
+
+ // RepeatedPtrField<Message> does not know how to Add() since it cannot
+ // allocate an abstract object, so we have to be tricky.
+ MessageLite* result = extension->repeated_message_value
+ ->AddFromCleared<GenericTypeHandler<MessageLite> >();
+ if (result == NULL) {
+ const MessageLite* prototype;
+ if (extension->repeated_message_value->size() == 0) {
+ prototype = factory->GetPrototype(descriptor->message_type());
+ GOOGLE_CHECK(prototype != NULL);
+ } else {
+ prototype = &extension->repeated_message_value->Get(0);
+ }
+ result = prototype->New();
+ extension->repeated_message_value->AddAllocated(result);
+ }
+ return result;
+}
+
+static bool ValidateEnumUsingDescriptor(const void* arg, int number) {
+ return reinterpret_cast<const EnumDescriptor*>(arg)
+ ->FindValueByNumber(number) != NULL;
+}
+
+bool DescriptorPoolExtensionFinder::Find(int number, ExtensionInfo* output) {
+ const FieldDescriptor* extension =
+ pool_->FindExtensionByNumber(containing_type_, number);
+ if (extension == NULL) {
+ return false;
+ } else {
+ output->type = extension->type();
+ output->is_repeated = extension->is_repeated();
+ output->is_packed = extension->options().packed();
+ output->descriptor = extension;
+ if (extension->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ output->message_prototype =
+ factory_->GetPrototype(extension->message_type());
+ GOOGLE_CHECK(output->message_prototype != NULL)
+ << "Extension factory's GetPrototype() returned NULL for extension: "
+ << extension->full_name();
+ } else if (extension->cpp_type() == FieldDescriptor::CPPTYPE_ENUM) {
+ output->enum_validity_check.func = ValidateEnumUsingDescriptor;
+ output->enum_validity_check.arg = extension->enum_type();
+ }
+
+ return true;
+ }
+}
+
+bool ExtensionSet::ParseField(uint32 tag, io::CodedInputStream* input,
+ const Message* containing_type,
+ UnknownFieldSet* unknown_fields) {
+ UnknownFieldSetFieldSkipper skipper(unknown_fields);
+ if (input->GetExtensionPool() == NULL) {
+ GeneratedExtensionFinder finder(containing_type);
+ return ParseField(tag, input, &finder, &skipper);
+ } else {
+ DescriptorPoolExtensionFinder finder(input->GetExtensionPool(),
+ input->GetExtensionFactory(),
+ containing_type->GetDescriptor());
+ return ParseField(tag, input, &finder, &skipper);
+ }
+}
+
+bool ExtensionSet::ParseMessageSet(io::CodedInputStream* input,
+ const Message* containing_type,
+ UnknownFieldSet* unknown_fields) {
+ MessageSetFieldSkipper skipper(unknown_fields);
+ if (input->GetExtensionPool() == NULL) {
+ GeneratedExtensionFinder finder(containing_type);
+ return ParseMessageSet(input, &finder, &skipper);
+ } else {
+ DescriptorPoolExtensionFinder finder(input->GetExtensionPool(),
+ input->GetExtensionFactory(),
+ containing_type->GetDescriptor());
+ return ParseMessageSet(input, &finder, &skipper);
+ }
+}
+
+int ExtensionSet::SpaceUsedExcludingSelf() const {
+ int total_size =
+ extensions_.size() * sizeof(map<int, Extension>::value_type);
+ for (map<int, Extension>::const_iterator iter = extensions_.begin(),
+ end = extensions_.end();
+ iter != end;
+ ++iter) {
+ total_size += iter->second.SpaceUsedExcludingSelf();
+ }
+ return total_size;
+}
+
+inline int ExtensionSet::RepeatedMessage_SpaceUsedExcludingSelf(
+ RepeatedPtrFieldBase* field) {
+ return field->SpaceUsedExcludingSelf<GenericTypeHandler<Message> >();
+}
+
+int ExtensionSet::Extension::SpaceUsedExcludingSelf() const {
+ int total_size = 0;
+ if (is_repeated) {
+ switch (cpp_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE: \
+ total_size += sizeof(*repeated_##LOWERCASE##_value) + \
+ repeated_##LOWERCASE##_value->SpaceUsedExcludingSelf();\
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE( UINT32, uint32);
+ HANDLE_TYPE( UINT64, uint64);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( DOUBLE, double);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, enum);
+ HANDLE_TYPE( STRING, string);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ // repeated_message_value is actually a RepeatedPtrField<MessageLite>,
+ // but MessageLite has no SpaceUsed(), so we must directly call
+ // RepeatedPtrFieldBase::SpaceUsedExcludingSelf() with a different type
+ // handler.
+ total_size += sizeof(*repeated_message_value) +
+ RepeatedMessage_SpaceUsedExcludingSelf(repeated_message_value);
+ break;
+ }
+ } else {
+ switch (cpp_type(type)) {
+ case FieldDescriptor::CPPTYPE_STRING:
+ total_size += sizeof(*string_value) +
+ StringSpaceUsedExcludingSelf(*string_value);
+ break;
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ if (is_lazy) {
+ total_size += lazymessage_value->SpaceUsed();
+ } else {
+ total_size += down_cast<Message*>(message_value)->SpaceUsed();
+ }
+ break;
+ default:
+ // No extra storage costs for primitive types.
+ break;
+ }
+ }
+ return total_size;
+}
+
+// The Serialize*ToArray methods are only needed in the heavy library, as
+// the lite library only generates SerializeWithCachedSizes.
+uint8* ExtensionSet::SerializeWithCachedSizesToArray(
+ int start_field_number, int end_field_number,
+ uint8* target) const {
+ map<int, Extension>::const_iterator iter;
+ for (iter = extensions_.lower_bound(start_field_number);
+ iter != extensions_.end() && iter->first < end_field_number;
+ ++iter) {
+ target = iter->second.SerializeFieldWithCachedSizesToArray(iter->first,
+ target);
+ }
+ return target;
+}
+
+uint8* ExtensionSet::SerializeMessageSetWithCachedSizesToArray(
+ uint8* target) const {
+ map<int, Extension>::const_iterator iter;
+ for (iter = extensions_.begin(); iter != extensions_.end(); ++iter) {
+ target = iter->second.SerializeMessageSetItemWithCachedSizesToArray(
+ iter->first, target);
+ }
+ return target;
+}
+
+uint8* ExtensionSet::Extension::SerializeFieldWithCachedSizesToArray(
+ int number, uint8* target) const {
+ if (is_repeated) {
+ if (is_packed) {
+ if (cached_size == 0) return target;
+
+ target = WireFormatLite::WriteTagToArray(number,
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED, target);
+ target = WireFormatLite::WriteInt32NoTagToArray(cached_size, target);
+
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case FieldDescriptor::TYPE_##UPPERCASE: \
+ for (int i = 0; i < repeated_##LOWERCASE##_value->size(); i++) { \
+ target = WireFormatLite::Write##CAMELCASE##NoTagToArray( \
+ repeated_##LOWERCASE##_value->Get(i), target); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, SInt32, int32);
+ HANDLE_TYPE( SINT64, SInt64, int64);
+ HANDLE_TYPE( FIXED32, Fixed32, uint32);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+ HANDLE_TYPE( ENUM, Enum, enum);
+#undef HANDLE_TYPE
+
+ case WireFormatLite::TYPE_STRING:
+ case WireFormatLite::TYPE_BYTES:
+ case WireFormatLite::TYPE_GROUP:
+ case WireFormatLite::TYPE_MESSAGE:
+ GOOGLE_LOG(FATAL) << "Non-primitive types can't be packed.";
+ break;
+ }
+ } else {
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, LOWERCASE) \
+ case FieldDescriptor::TYPE_##UPPERCASE: \
+ for (int i = 0; i < repeated_##LOWERCASE##_value->size(); i++) { \
+ target = WireFormatLite::Write##CAMELCASE##ToArray(number, \
+ repeated_##LOWERCASE##_value->Get(i), target); \
+ } \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32);
+ HANDLE_TYPE( INT64, Int64, int64);
+ HANDLE_TYPE( UINT32, UInt32, uint32);
+ HANDLE_TYPE( UINT64, UInt64, uint64);
+ HANDLE_TYPE( SINT32, SInt32, int32);
+ HANDLE_TYPE( SINT64, SInt64, int64);
+ HANDLE_TYPE( FIXED32, Fixed32, uint32);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64);
+ HANDLE_TYPE( FLOAT, Float, float);
+ HANDLE_TYPE( DOUBLE, Double, double);
+ HANDLE_TYPE( BOOL, Bool, bool);
+ HANDLE_TYPE( STRING, String, string);
+ HANDLE_TYPE( BYTES, Bytes, string);
+ HANDLE_TYPE( ENUM, Enum, enum);
+ HANDLE_TYPE( GROUP, Group, message);
+ HANDLE_TYPE( MESSAGE, Message, message);
+#undef HANDLE_TYPE
+ }
+ }
+ } else if (!is_cleared) {
+ switch (real_type(type)) {
+#define HANDLE_TYPE(UPPERCASE, CAMELCASE, VALUE) \
+ case FieldDescriptor::TYPE_##UPPERCASE: \
+ target = WireFormatLite::Write##CAMELCASE##ToArray( \
+ number, VALUE, target); \
+ break
+
+ HANDLE_TYPE( INT32, Int32, int32_value);
+ HANDLE_TYPE( INT64, Int64, int64_value);
+ HANDLE_TYPE( UINT32, UInt32, uint32_value);
+ HANDLE_TYPE( UINT64, UInt64, uint64_value);
+ HANDLE_TYPE( SINT32, SInt32, int32_value);
+ HANDLE_TYPE( SINT64, SInt64, int64_value);
+ HANDLE_TYPE( FIXED32, Fixed32, uint32_value);
+ HANDLE_TYPE( FIXED64, Fixed64, uint64_value);
+ HANDLE_TYPE(SFIXED32, SFixed32, int32_value);
+ HANDLE_TYPE(SFIXED64, SFixed64, int64_value);
+ HANDLE_TYPE( FLOAT, Float, float_value);
+ HANDLE_TYPE( DOUBLE, Double, double_value);
+ HANDLE_TYPE( BOOL, Bool, bool_value);
+ HANDLE_TYPE( STRING, String, *string_value);
+ HANDLE_TYPE( BYTES, Bytes, *string_value);
+ HANDLE_TYPE( ENUM, Enum, enum_value);
+ HANDLE_TYPE( GROUP, Group, *message_value);
+#undef HANDLE_TYPE
+ case FieldDescriptor::TYPE_MESSAGE:
+ if (is_lazy) {
+ target = lazymessage_value->WriteMessageToArray(number, target);
+ } else {
+ target = WireFormatLite::WriteMessageToArray(
+ number, *message_value, target);
+ }
+ break;
+ }
+ }
+ return target;
+}
+
+uint8* ExtensionSet::Extension::SerializeMessageSetItemWithCachedSizesToArray(
+ int number,
+ uint8* target) const {
+ if (type != WireFormatLite::TYPE_MESSAGE || is_repeated) {
+ // Not a valid MessageSet extension, but serialize it the normal way.
+ GOOGLE_LOG(WARNING) << "Invalid message set extension.";
+ return SerializeFieldWithCachedSizesToArray(number, target);
+ }
+
+ if (is_cleared) return target;
+
+ // Start group.
+ target = io::CodedOutputStream::WriteTagToArray(
+ WireFormatLite::kMessageSetItemStartTag, target);
+ // Write type ID.
+ target = WireFormatLite::WriteUInt32ToArray(
+ WireFormatLite::kMessageSetTypeIdNumber, number, target);
+ // Write message.
+ if (is_lazy) {
+ target = lazymessage_value->WriteMessageToArray(
+ WireFormatLite::kMessageSetMessageNumber, target);
+ } else {
+ target = WireFormatLite::WriteMessageToArray(
+ WireFormatLite::kMessageSetMessageNumber, *message_value, target);
+ }
+ // End group.
+ target = io::CodedOutputStream::WriteTagToArray(
+ WireFormatLite::kMessageSetItemEndTag, target);
+ return target;
+}
+
+
+bool ExtensionSet::ParseFieldMaybeLazily(
+ int wire_type, int field_number, io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ MessageSetFieldSkipper* field_skipper) {
+ return ParseField(WireFormatLite::MakeTag(
+ field_number, static_cast<WireFormatLite::WireType>(wire_type)),
+ input, extension_finder, field_skipper);
+}
+
+bool ExtensionSet::ParseMessageSet(io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ MessageSetFieldSkipper* field_skipper) {
+ while (true) {
+ const uint32 tag = input->ReadTag();
+ switch (tag) {
+ case 0:
+ return true;
+ case WireFormatLite::kMessageSetItemStartTag:
+ if (!ParseMessageSetItem(input, extension_finder, field_skipper)) {
+ return false;
+ }
+ break;
+ default:
+ if (!ParseField(tag, input, extension_finder, field_skipper)) {
+ return false;
+ }
+ break;
+ }
+ }
+}
+
+bool ExtensionSet::ParseMessageSet(io::CodedInputStream* input,
+ const MessageLite* containing_type) {
+ MessageSetFieldSkipper skipper(NULL);
+ GeneratedExtensionFinder finder(containing_type);
+ return ParseMessageSet(input, &finder, &skipper);
+}
+
+bool ExtensionSet::ParseMessageSetItem(io::CodedInputStream* input,
+ ExtensionFinder* extension_finder,
+ MessageSetFieldSkipper* field_skipper) {
+ // TODO(kenton): It would be nice to share code between this and
+ // WireFormatLite::ParseAndMergeMessageSetItem(), but I think the
+ // differences would be hard to factor out.
+
+ // This method parses a group which should contain two fields:
+ // required int32 type_id = 2;
+ // required data message = 3;
+
+ uint32 last_type_id = 0;
+
+ // If we see message data before the type_id, we'll append it to this so
+ // we can parse it later.
+ string message_data;
+
+ while (true) {
+ const uint32 tag = input->ReadTag();
+ if (tag == 0) return false;
+
+ switch (tag) {
+ case WireFormatLite::kMessageSetTypeIdTag: {
+ uint32 type_id;
+ if (!input->ReadVarint32(&type_id)) return false;
+ last_type_id = type_id;
+
+ if (!message_data.empty()) {
+ // We saw some message data before the type_id. Have to parse it
+ // now.
+ io::CodedInputStream sub_input(
+ reinterpret_cast<const uint8*>(message_data.data()),
+ message_data.size());
+ if (!ParseFieldMaybeLazily(WireFormatLite::WIRETYPE_LENGTH_DELIMITED,
+ last_type_id, &sub_input,
+ extension_finder, field_skipper)) {
+ return false;
+ }
+ message_data.clear();
+ }
+
+ break;
+ }
+
+ case WireFormatLite::kMessageSetMessageTag: {
+ if (last_type_id == 0) {
+ // We haven't seen a type_id yet. Append this data to message_data.
+ string temp;
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (!input->ReadString(&temp, length)) return false;
+ io::StringOutputStream output_stream(&message_data);
+ io::CodedOutputStream coded_output(&output_stream);
+ coded_output.WriteVarint32(length);
+ coded_output.WriteString(temp);
+ } else {
+ // Already saw type_id, so we can parse this directly.
+ if (!ParseFieldMaybeLazily(WireFormatLite::WIRETYPE_LENGTH_DELIMITED,
+ last_type_id, input,
+ extension_finder, field_skipper)) {
+ return false;
+ }
+ }
+
+ break;
+ }
+
+ case WireFormatLite::kMessageSetItemEndTag: {
+ return true;
+ }
+
+ default: {
+ if (!field_skipper->SkipField(input, tag)) return false;
+ }
+ }
+ }
+}
+
+void ExtensionSet::Extension::SerializeMessageSetItemWithCachedSizes(
+ int number,
+ io::CodedOutputStream* output) const {
+ if (type != WireFormatLite::TYPE_MESSAGE || is_repeated) {
+ // Not a valid MessageSet extension, but serialize it the normal way.
+ SerializeFieldWithCachedSizes(number, output);
+ return;
+ }
+
+ if (is_cleared) return;
+
+ // Start group.
+ output->WriteTag(WireFormatLite::kMessageSetItemStartTag);
+
+ // Write type ID.
+ WireFormatLite::WriteUInt32(WireFormatLite::kMessageSetTypeIdNumber,
+ number,
+ output);
+ // Write message.
+ if (is_lazy) {
+ lazymessage_value->WriteMessage(
+ WireFormatLite::kMessageSetMessageNumber, output);
+ } else {
+ WireFormatLite::WriteMessageMaybeToArray(
+ WireFormatLite::kMessageSetMessageNumber,
+ *message_value,
+ output);
+ }
+
+ // End group.
+ output->WriteTag(WireFormatLite::kMessageSetItemEndTag);
+}
+
+int ExtensionSet::Extension::MessageSetItemByteSize(int number) const {
+ if (type != WireFormatLite::TYPE_MESSAGE || is_repeated) {
+ // Not a valid MessageSet extension, but compute the byte size for it the
+ // normal way.
+ return ByteSize(number);
+ }
+
+ if (is_cleared) return 0;
+
+ int our_size = WireFormatLite::kMessageSetItemTagsSize;
+
+ // type_id
+ our_size += io::CodedOutputStream::VarintSize32(number);
+
+ // message
+ int message_size = 0;
+ if (is_lazy) {
+ message_size = lazymessage_value->ByteSize();
+ } else {
+ message_size = message_value->ByteSize();
+ }
+
+ our_size += io::CodedOutputStream::VarintSize32(message_size);
+ our_size += message_size;
+
+ return our_size;
+}
+
+void ExtensionSet::SerializeMessageSetWithCachedSizes(
+ io::CodedOutputStream* output) const {
+ for (map<int, Extension>::const_iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ iter->second.SerializeMessageSetItemWithCachedSizes(iter->first, output);
+ }
+}
+
+int ExtensionSet::MessageSetByteSize() const {
+ int total_size = 0;
+
+ for (map<int, Extension>::const_iterator iter = extensions_.begin();
+ iter != extensions_.end(); ++iter) {
+ total_size += iter->second.MessageSetItemByteSize(iter->first);
+ }
+
+ return total_size;
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/generated_enum_reflection.h b/toolkit/components/protobuf/src/google/protobuf/generated_enum_reflection.h
new file mode 100644
index 0000000000..3852cea580
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/generated_enum_reflection.h
@@ -0,0 +1,91 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: jasonh@google.com (Jason Hsueh)
+//
+// This header is logically internal, but is made public because it is used
+// from protocol-compiler-generated code, which may reside in other components.
+// It provides reflection support for generated enums, and is included in
+// generated .pb.h files and should have minimal dependencies. The methods are
+// implemented in generated_message_reflection.cc.
+
+#ifndef GOOGLE_PROTOBUF_GENERATED_ENUM_REFLECTION_H__
+#define GOOGLE_PROTOBUF_GENERATED_ENUM_REFLECTION_H__
+
+#include <string>
+
+#include <google/protobuf/stubs/template_util.h>
+
+namespace google {
+namespace protobuf {
+ class EnumDescriptor;
+} // namespace protobuf
+
+namespace protobuf {
+
+// This type trait can be used to cause templates to only match proto2 enum
+// types.
+template <typename T> struct is_proto_enum : ::google::protobuf::internal::false_type {};
+
+// Returns the EnumDescriptor for enum type E, which must be a
+// proto-declared enum type. Code generated by the protocol compiler
+// will include specializations of this template for each enum type declared.
+template <typename E>
+const EnumDescriptor* GetEnumDescriptor();
+
+namespace internal {
+
+// Helper for EnumType_Parse functions: try to parse the string 'name' as an
+// enum name of the given type, returning true and filling in value on success,
+// or returning false and leaving value unchanged on failure.
+LIBPROTOBUF_EXPORT bool ParseNamedEnum(const EnumDescriptor* descriptor,
+ const string& name,
+ int* value);
+
+template<typename EnumType>
+bool ParseNamedEnum(const EnumDescriptor* descriptor,
+ const string& name,
+ EnumType* value) {
+ int tmp;
+ if (!ParseNamedEnum(descriptor, name, &tmp)) return false;
+ *value = static_cast<EnumType>(tmp);
+ return true;
+}
+
+// Just a wrapper around printing the name of a value. The main point of this
+// function is not to be inlined, so that you can do this without including
+// descriptor.h.
+LIBPROTOBUF_EXPORT const string& NameOfEnum(const EnumDescriptor* descriptor, int value);
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_GENERATED_ENUM_REFLECTION_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.cc b/toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.cc
new file mode 100644
index 0000000000..536de7d92f
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.cc
@@ -0,0 +1,1683 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <algorithm>
+#include <set>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/generated_message_reflection.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/extension_set.h>
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/stubs/common.h>
+
+#define GOOGLE_PROTOBUF_HAS_ONEOF
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+int StringSpaceUsedExcludingSelf(const string& str) {
+ const void* start = &str;
+ const void* end = &str + 1;
+
+ if (start <= str.data() && str.data() < end) {
+ // The string's data is stored inside the string object itself.
+ return 0;
+ } else {
+ return str.capacity();
+ }
+}
+
+bool ParseNamedEnum(const EnumDescriptor* descriptor,
+ const string& name,
+ int* value) {
+ const EnumValueDescriptor* d = descriptor->FindValueByName(name);
+ if (d == NULL) return false;
+ *value = d->number();
+ return true;
+}
+
+const string& NameOfEnum(const EnumDescriptor* descriptor, int value) {
+ const EnumValueDescriptor* d = descriptor->FindValueByNumber(value);
+ return (d == NULL ? GetEmptyString() : d->name());
+}
+
+// ===================================================================
+// Helpers for reporting usage errors (e.g. trying to use GetInt32() on
+// a string field).
+
+namespace {
+
+void ReportReflectionUsageError(
+ const Descriptor* descriptor, const FieldDescriptor* field,
+ const char* method, const char* description) {
+ GOOGLE_LOG(FATAL)
+ << "Protocol Buffer reflection usage error:\n"
+ " Method : google::protobuf::Reflection::" << method << "\n"
+ " Message type: " << descriptor->full_name() << "\n"
+ " Field : " << field->full_name() << "\n"
+ " Problem : " << description;
+}
+
+const char* cpptype_names_[FieldDescriptor::MAX_CPPTYPE + 1] = {
+ "INVALID_CPPTYPE",
+ "CPPTYPE_INT32",
+ "CPPTYPE_INT64",
+ "CPPTYPE_UINT32",
+ "CPPTYPE_UINT64",
+ "CPPTYPE_DOUBLE",
+ "CPPTYPE_FLOAT",
+ "CPPTYPE_BOOL",
+ "CPPTYPE_ENUM",
+ "CPPTYPE_STRING",
+ "CPPTYPE_MESSAGE"
+};
+
+static void ReportReflectionUsageTypeError(
+ const Descriptor* descriptor, const FieldDescriptor* field,
+ const char* method,
+ FieldDescriptor::CppType expected_type) {
+ GOOGLE_LOG(FATAL)
+ << "Protocol Buffer reflection usage error:\n"
+ " Method : google::protobuf::Reflection::" << method << "\n"
+ " Message type: " << descriptor->full_name() << "\n"
+ " Field : " << field->full_name() << "\n"
+ " Problem : Field is not the right type for this message:\n"
+ " Expected : " << cpptype_names_[expected_type] << "\n"
+ " Field type: " << cpptype_names_[field->cpp_type()];
+}
+
+static void ReportReflectionUsageEnumTypeError(
+ const Descriptor* descriptor, const FieldDescriptor* field,
+ const char* method, const EnumValueDescriptor* value) {
+ GOOGLE_LOG(FATAL)
+ << "Protocol Buffer reflection usage error:\n"
+ " Method : google::protobuf::Reflection::" << method << "\n"
+ " Message type: " << descriptor->full_name() << "\n"
+ " Field : " << field->full_name() << "\n"
+ " Problem : Enum value did not match field type:\n"
+ " Expected : " << field->enum_type()->full_name() << "\n"
+ " Actual : " << value->full_name();
+}
+
+#define USAGE_CHECK(CONDITION, METHOD, ERROR_DESCRIPTION) \
+ if (!(CONDITION)) \
+ ReportReflectionUsageError(descriptor_, field, #METHOD, ERROR_DESCRIPTION)
+#define USAGE_CHECK_EQ(A, B, METHOD, ERROR_DESCRIPTION) \
+ USAGE_CHECK((A) == (B), METHOD, ERROR_DESCRIPTION)
+#define USAGE_CHECK_NE(A, B, METHOD, ERROR_DESCRIPTION) \
+ USAGE_CHECK((A) != (B), METHOD, ERROR_DESCRIPTION)
+
+#define USAGE_CHECK_TYPE(METHOD, CPPTYPE) \
+ if (field->cpp_type() != FieldDescriptor::CPPTYPE_##CPPTYPE) \
+ ReportReflectionUsageTypeError(descriptor_, field, #METHOD, \
+ FieldDescriptor::CPPTYPE_##CPPTYPE)
+
+#define USAGE_CHECK_ENUM_VALUE(METHOD) \
+ if (value->type() != field->enum_type()) \
+ ReportReflectionUsageEnumTypeError(descriptor_, field, #METHOD, value)
+
+#define USAGE_CHECK_MESSAGE_TYPE(METHOD) \
+ USAGE_CHECK_EQ(field->containing_type(), descriptor_, \
+ METHOD, "Field does not match message type.");
+#define USAGE_CHECK_SINGULAR(METHOD) \
+ USAGE_CHECK_NE(field->label(), FieldDescriptor::LABEL_REPEATED, METHOD, \
+ "Field is repeated; the method requires a singular field.")
+#define USAGE_CHECK_REPEATED(METHOD) \
+ USAGE_CHECK_EQ(field->label(), FieldDescriptor::LABEL_REPEATED, METHOD, \
+ "Field is singular; the method requires a repeated field.")
+
+#define USAGE_CHECK_ALL(METHOD, LABEL, CPPTYPE) \
+ USAGE_CHECK_MESSAGE_TYPE(METHOD); \
+ USAGE_CHECK_##LABEL(METHOD); \
+ USAGE_CHECK_TYPE(METHOD, CPPTYPE)
+
+} // namespace
+
+// ===================================================================
+
+GeneratedMessageReflection::GeneratedMessageReflection(
+ const Descriptor* descriptor,
+ const Message* default_instance,
+ const int offsets[],
+ int has_bits_offset,
+ int unknown_fields_offset,
+ int extensions_offset,
+ const DescriptorPool* descriptor_pool,
+ MessageFactory* factory,
+ int object_size)
+ : descriptor_ (descriptor),
+ default_instance_ (default_instance),
+ offsets_ (offsets),
+ has_bits_offset_ (has_bits_offset),
+ unknown_fields_offset_(unknown_fields_offset),
+ extensions_offset_(extensions_offset),
+ object_size_ (object_size),
+ descriptor_pool_ ((descriptor_pool == NULL) ?
+ DescriptorPool::generated_pool() :
+ descriptor_pool),
+ message_factory_ (factory) {
+}
+
+GeneratedMessageReflection::GeneratedMessageReflection(
+ const Descriptor* descriptor,
+ const Message* default_instance,
+ const int offsets[],
+ int has_bits_offset,
+ int unknown_fields_offset,
+ int extensions_offset,
+ const void* default_oneof_instance,
+ int oneof_case_offset,
+ const DescriptorPool* descriptor_pool,
+ MessageFactory* factory,
+ int object_size)
+ : descriptor_ (descriptor),
+ default_instance_ (default_instance),
+ default_oneof_instance_ (default_oneof_instance),
+ offsets_ (offsets),
+ has_bits_offset_ (has_bits_offset),
+ oneof_case_offset_(oneof_case_offset),
+ unknown_fields_offset_(unknown_fields_offset),
+ extensions_offset_(extensions_offset),
+ object_size_ (object_size),
+ descriptor_pool_ ((descriptor_pool == NULL) ?
+ DescriptorPool::generated_pool() :
+ descriptor_pool),
+ message_factory_ (factory) {
+}
+
+GeneratedMessageReflection::~GeneratedMessageReflection() {}
+
+const UnknownFieldSet& GeneratedMessageReflection::GetUnknownFields(
+ const Message& message) const {
+ const void* ptr = reinterpret_cast<const uint8*>(&message) +
+ unknown_fields_offset_;
+ return *reinterpret_cast<const UnknownFieldSet*>(ptr);
+}
+UnknownFieldSet* GeneratedMessageReflection::MutableUnknownFields(
+ Message* message) const {
+ void* ptr = reinterpret_cast<uint8*>(message) + unknown_fields_offset_;
+ return reinterpret_cast<UnknownFieldSet*>(ptr);
+}
+
+int GeneratedMessageReflection::SpaceUsed(const Message& message) const {
+ // object_size_ already includes the in-memory representation of each field
+ // in the message, so we only need to account for additional memory used by
+ // the fields.
+ int total_size = object_size_;
+
+ total_size += GetUnknownFields(message).SpaceUsedExcludingSelf();
+
+ if (extensions_offset_ != -1) {
+ total_size += GetExtensionSet(message).SpaceUsedExcludingSelf();
+ }
+
+ for (int i = 0; i < descriptor_->field_count(); i++) {
+ const FieldDescriptor* field = descriptor_->field(i);
+
+ if (field->is_repeated()) {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE : \
+ total_size += GetRaw<RepeatedField<LOWERCASE> >(message, field) \
+ .SpaceUsedExcludingSelf(); \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, int);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ total_size += GetRaw<RepeatedPtrField<string> >(message, field)
+ .SpaceUsedExcludingSelf();
+ break;
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ // We don't know which subclass of RepeatedPtrFieldBase the type is,
+ // so we use RepeatedPtrFieldBase directly.
+ total_size +=
+ GetRaw<RepeatedPtrFieldBase>(message, field)
+ .SpaceUsedExcludingSelf<GenericTypeHandler<Message> >();
+ break;
+ }
+ } else {
+ if (field->containing_oneof() && !HasOneofField(message, field)) {
+ continue;
+ }
+ switch (field->cpp_type()) {
+ case FieldDescriptor::CPPTYPE_INT32 :
+ case FieldDescriptor::CPPTYPE_INT64 :
+ case FieldDescriptor::CPPTYPE_UINT32:
+ case FieldDescriptor::CPPTYPE_UINT64:
+ case FieldDescriptor::CPPTYPE_DOUBLE:
+ case FieldDescriptor::CPPTYPE_FLOAT :
+ case FieldDescriptor::CPPTYPE_BOOL :
+ case FieldDescriptor::CPPTYPE_ENUM :
+ // Field is inline, so we've already counted it.
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING: {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING: {
+ const string* ptr = GetField<const string*>(message, field);
+
+ // Initially, the string points to the default value stored in
+ // the prototype. Only count the string if it has been changed
+ // from the default value.
+ const string* default_ptr = DefaultRaw<const string*>(field);
+
+ if (ptr != default_ptr) {
+ // string fields are represented by just a pointer, so also
+ // include sizeof(string) as well.
+ total_size += sizeof(*ptr) + StringSpaceUsedExcludingSelf(*ptr);
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ if (&message == default_instance_) {
+ // For singular fields, the prototype just stores a pointer to the
+ // external type's prototype, so there is no extra memory usage.
+ } else {
+ const Message* sub_message = GetRaw<const Message*>(message, field);
+ if (sub_message != NULL) {
+ total_size += sub_message->SpaceUsed();
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ return total_size;
+}
+
+void GeneratedMessageReflection::SwapField(
+ Message* message1,
+ Message* message2,
+ const FieldDescriptor* field) const {
+ if (field->is_repeated()) {
+ switch (field->cpp_type()) {
+#define SWAP_ARRAYS(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ MutableRaw<RepeatedField<TYPE> >(message1, field)->Swap( \
+ MutableRaw<RepeatedField<TYPE> >(message2, field)); \
+ break;
+
+ SWAP_ARRAYS(INT32 , int32 );
+ SWAP_ARRAYS(INT64 , int64 );
+ SWAP_ARRAYS(UINT32, uint32);
+ SWAP_ARRAYS(UINT64, uint64);
+ SWAP_ARRAYS(FLOAT , float );
+ SWAP_ARRAYS(DOUBLE, double);
+ SWAP_ARRAYS(BOOL , bool );
+ SWAP_ARRAYS(ENUM , int );
+#undef SWAP_ARRAYS
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ MutableRaw<RepeatedPtrFieldBase>(message1, field)->Swap(
+ MutableRaw<RepeatedPtrFieldBase>(message2, field));
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Unimplemented type: " << field->cpp_type();
+ }
+ } else {
+ switch (field->cpp_type()) {
+#define SWAP_VALUES(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ std::swap(*MutableRaw<TYPE>(message1, field), \
+ *MutableRaw<TYPE>(message2, field)); \
+ break;
+
+ SWAP_VALUES(INT32 , int32 );
+ SWAP_VALUES(INT64 , int64 );
+ SWAP_VALUES(UINT32, uint32);
+ SWAP_VALUES(UINT64, uint64);
+ SWAP_VALUES(FLOAT , float );
+ SWAP_VALUES(DOUBLE, double);
+ SWAP_VALUES(BOOL , bool );
+ SWAP_VALUES(ENUM , int );
+#undef SWAP_VALUES
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ std::swap(*MutableRaw<Message*>(message1, field),
+ *MutableRaw<Message*>(message2, field));
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ std::swap(*MutableRaw<string*>(message1, field),
+ *MutableRaw<string*>(message2, field));
+ break;
+ }
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Unimplemented type: " << field->cpp_type();
+ }
+ }
+}
+
+void GeneratedMessageReflection::SwapOneofField(
+ Message* message1,
+ Message* message2,
+ const OneofDescriptor* oneof_descriptor) const {
+ uint32 oneof_case1 = GetOneofCase(*message1, oneof_descriptor);
+ uint32 oneof_case2 = GetOneofCase(*message2, oneof_descriptor);
+
+ int32 temp_int32;
+ int64 temp_int64;
+ uint32 temp_uint32;
+ uint64 temp_uint64;
+ float temp_float;
+ double temp_double;
+ bool temp_bool;
+ int temp_int;
+ Message* temp_message;
+ string temp_string;
+
+ // Stores message1's oneof field to a temp variable.
+ const FieldDescriptor* field1;
+ if (oneof_case1 > 0) {
+ field1 = descriptor_->FindFieldByNumber(oneof_case1);
+ //oneof_descriptor->field(oneof_case1);
+ switch (field1->cpp_type()) {
+#define GET_TEMP_VALUE(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ temp_##TYPE = GetField<TYPE>(*message1, field1); \
+ break;
+
+ GET_TEMP_VALUE(INT32 , int32 );
+ GET_TEMP_VALUE(INT64 , int64 );
+ GET_TEMP_VALUE(UINT32, uint32);
+ GET_TEMP_VALUE(UINT64, uint64);
+ GET_TEMP_VALUE(FLOAT , float );
+ GET_TEMP_VALUE(DOUBLE, double);
+ GET_TEMP_VALUE(BOOL , bool );
+ GET_TEMP_VALUE(ENUM , int );
+#undef GET_TEMP_VALUE
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ temp_message = ReleaseMessage(message1, field1);
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ temp_string = GetString(*message1, field1);
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Unimplemented type: " << field1->cpp_type();
+ }
+ }
+
+ // Sets message1's oneof field from the message2's oneof field.
+ if (oneof_case2 > 0) {
+ const FieldDescriptor* field2 =
+ descriptor_->FindFieldByNumber(oneof_case2);
+ switch (field2->cpp_type()) {
+#define SET_ONEOF_VALUE1(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ SetField<TYPE>(message1, field2, GetField<TYPE>(*message2, field2)); \
+ break;
+
+ SET_ONEOF_VALUE1(INT32 , int32 );
+ SET_ONEOF_VALUE1(INT64 , int64 );
+ SET_ONEOF_VALUE1(UINT32, uint32);
+ SET_ONEOF_VALUE1(UINT64, uint64);
+ SET_ONEOF_VALUE1(FLOAT , float );
+ SET_ONEOF_VALUE1(DOUBLE, double);
+ SET_ONEOF_VALUE1(BOOL , bool );
+ SET_ONEOF_VALUE1(ENUM , int );
+#undef SET_ONEOF_VALUE1
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ SetAllocatedMessage(message1,
+ ReleaseMessage(message2, field2),
+ field2);
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ SetString(message1, field2, GetString(*message2, field2));
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Unimplemented type: " << field2->cpp_type();
+ }
+ } else {
+ ClearOneof(message1, oneof_descriptor);
+ }
+
+ // Sets message2's oneof field from the temp variable.
+ if (oneof_case1 > 0) {
+ switch (field1->cpp_type()) {
+#define SET_ONEOF_VALUE2(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ SetField<TYPE>(message2, field1, temp_##TYPE); \
+ break;
+
+ SET_ONEOF_VALUE2(INT32 , int32 );
+ SET_ONEOF_VALUE2(INT64 , int64 );
+ SET_ONEOF_VALUE2(UINT32, uint32);
+ SET_ONEOF_VALUE2(UINT64, uint64);
+ SET_ONEOF_VALUE2(FLOAT , float );
+ SET_ONEOF_VALUE2(DOUBLE, double);
+ SET_ONEOF_VALUE2(BOOL , bool );
+ SET_ONEOF_VALUE2(ENUM , int );
+#undef SET_ONEOF_VALUE2
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ SetAllocatedMessage(message2, temp_message, field1);
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ SetString(message2, field1, temp_string);
+ break;
+
+ default:
+ GOOGLE_LOG(FATAL) << "Unimplemented type: " << field1->cpp_type();
+ }
+ } else {
+ ClearOneof(message2, oneof_descriptor);
+ }
+}
+
+void GeneratedMessageReflection::Swap(
+ Message* message1,
+ Message* message2) const {
+ if (message1 == message2) return;
+
+ // TODO(kenton): Other Reflection methods should probably check this too.
+ GOOGLE_CHECK_EQ(message1->GetReflection(), this)
+ << "First argument to Swap() (of type \""
+ << message1->GetDescriptor()->full_name()
+ << "\") is not compatible with this reflection object (which is for type \""
+ << descriptor_->full_name()
+ << "\"). Note that the exact same class is required; not just the same "
+ "descriptor.";
+ GOOGLE_CHECK_EQ(message2->GetReflection(), this)
+ << "Second argument to Swap() (of type \""
+ << message2->GetDescriptor()->full_name()
+ << "\") is not compatible with this reflection object (which is for type \""
+ << descriptor_->full_name()
+ << "\"). Note that the exact same class is required; not just the same "
+ "descriptor.";
+
+ uint32* has_bits1 = MutableHasBits(message1);
+ uint32* has_bits2 = MutableHasBits(message2);
+ int has_bits_size = (descriptor_->field_count() + 31) / 32;
+
+ for (int i = 0; i < has_bits_size; i++) {
+ std::swap(has_bits1[i], has_bits2[i]);
+ }
+
+ for (int i = 0; i < descriptor_->field_count(); i++) {
+ const FieldDescriptor* field = descriptor_->field(i);
+ if (!field->containing_oneof()) {
+ SwapField(message1, message2, field);
+ }
+ }
+
+ for (int i = 0; i < descriptor_->oneof_decl_count(); i++) {
+ SwapOneofField(message1, message2, descriptor_->oneof_decl(i));
+ }
+
+ if (extensions_offset_ != -1) {
+ MutableExtensionSet(message1)->Swap(MutableExtensionSet(message2));
+ }
+
+ MutableUnknownFields(message1)->Swap(MutableUnknownFields(message2));
+}
+
+void GeneratedMessageReflection::SwapFields(
+ Message* message1,
+ Message* message2,
+ const vector<const FieldDescriptor*>& fields) const {
+ if (message1 == message2) return;
+
+ // TODO(kenton): Other Reflection methods should probably check this too.
+ GOOGLE_CHECK_EQ(message1->GetReflection(), this)
+ << "First argument to SwapFields() (of type \""
+ << message1->GetDescriptor()->full_name()
+ << "\") is not compatible with this reflection object (which is for type \""
+ << descriptor_->full_name()
+ << "\"). Note that the exact same class is required; not just the same "
+ "descriptor.";
+ GOOGLE_CHECK_EQ(message2->GetReflection(), this)
+ << "Second argument to SwapFields() (of type \""
+ << message2->GetDescriptor()->full_name()
+ << "\") is not compatible with this reflection object (which is for type \""
+ << descriptor_->full_name()
+ << "\"). Note that the exact same class is required; not just the same "
+ "descriptor.";
+
+ std::set<int> swapped_oneof;
+
+ for (int i = 0; i < fields.size(); i++) {
+ const FieldDescriptor* field = fields[i];
+ if (field->is_extension()) {
+ MutableExtensionSet(message1)->SwapExtension(
+ MutableExtensionSet(message2),
+ field->number());
+ } else {
+ if (field->containing_oneof()) {
+ int oneof_index = field->containing_oneof()->index();
+ // Only swap the oneof field once.
+ if (swapped_oneof.find(oneof_index) != swapped_oneof.end()) {
+ continue;
+ }
+ swapped_oneof.insert(oneof_index);
+ SwapOneofField(message1, message2, field->containing_oneof());
+ } else {
+ // Swap has bit.
+ SwapBit(message1, message2, field);
+ // Swap field.
+ SwapField(message1, message2, field);
+ }
+ }
+ }
+}
+
+// -------------------------------------------------------------------
+
+bool GeneratedMessageReflection::HasField(const Message& message,
+ const FieldDescriptor* field) const {
+ USAGE_CHECK_MESSAGE_TYPE(HasField);
+ USAGE_CHECK_SINGULAR(HasField);
+
+ if (field->is_extension()) {
+ return GetExtensionSet(message).Has(field->number());
+ } else {
+ if (field->containing_oneof()) {
+ return HasOneofField(message, field);
+ } else {
+ return HasBit(message, field);
+ }
+ }
+}
+
+int GeneratedMessageReflection::FieldSize(const Message& message,
+ const FieldDescriptor* field) const {
+ USAGE_CHECK_MESSAGE_TYPE(FieldSize);
+ USAGE_CHECK_REPEATED(FieldSize);
+
+ if (field->is_extension()) {
+ return GetExtensionSet(message).ExtensionSize(field->number());
+ } else {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE : \
+ return GetRaw<RepeatedField<LOWERCASE> >(message, field).size()
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, int);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ return GetRaw<RepeatedPtrFieldBase>(message, field).size();
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+ return 0;
+ }
+}
+
+void GeneratedMessageReflection::ClearField(
+ Message* message, const FieldDescriptor* field) const {
+ USAGE_CHECK_MESSAGE_TYPE(ClearField);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->ClearExtension(field->number());
+ } else if (!field->is_repeated()) {
+ if (field->containing_oneof()) {
+ ClearOneofField(message, field);
+ return;
+ }
+
+ if (HasBit(*message, field)) {
+ ClearBit(message, field);
+
+ // We need to set the field back to its default value.
+ switch (field->cpp_type()) {
+#define CLEAR_TYPE(CPPTYPE, TYPE) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ *MutableRaw<TYPE>(message, field) = \
+ field->default_value_##TYPE(); \
+ break;
+
+ CLEAR_TYPE(INT32 , int32 );
+ CLEAR_TYPE(INT64 , int64 );
+ CLEAR_TYPE(UINT32, uint32);
+ CLEAR_TYPE(UINT64, uint64);
+ CLEAR_TYPE(FLOAT , float );
+ CLEAR_TYPE(DOUBLE, double);
+ CLEAR_TYPE(BOOL , bool );
+#undef CLEAR_TYPE
+
+ case FieldDescriptor::CPPTYPE_ENUM:
+ *MutableRaw<int>(message, field) =
+ field->default_value_enum()->number();
+ break;
+
+ case FieldDescriptor::CPPTYPE_STRING: {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ const string* default_ptr = DefaultRaw<const string*>(field);
+ string** value = MutableRaw<string*>(message, field);
+ if (*value != default_ptr) {
+ if (field->has_default_value()) {
+ (*value)->assign(field->default_value_string());
+ } else {
+ (*value)->clear();
+ }
+ }
+ break;
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ (*MutableRaw<Message*>(message, field))->Clear();
+ break;
+ }
+ }
+ } else {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE : \
+ MutableRaw<RepeatedField<LOWERCASE> >(message, field)->Clear(); \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, int);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_STRING: {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ MutableRaw<RepeatedPtrField<string> >(message, field)->Clear();
+ break;
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_MESSAGE: {
+ // We don't know which subclass of RepeatedPtrFieldBase the type is,
+ // so we use RepeatedPtrFieldBase directly.
+ MutableRaw<RepeatedPtrFieldBase>(message, field)
+ ->Clear<GenericTypeHandler<Message> >();
+ break;
+ }
+ }
+ }
+}
+
+void GeneratedMessageReflection::RemoveLast(
+ Message* message,
+ const FieldDescriptor* field) const {
+ USAGE_CHECK_MESSAGE_TYPE(RemoveLast);
+ USAGE_CHECK_REPEATED(RemoveLast);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->RemoveLast(field->number());
+ } else {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE : \
+ MutableRaw<RepeatedField<LOWERCASE> >(message, field)->RemoveLast(); \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, int);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ MutableRaw<RepeatedPtrField<string> >(message, field)->RemoveLast();
+ break;
+ }
+ break;
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ MutableRaw<RepeatedPtrFieldBase>(message, field)
+ ->RemoveLast<GenericTypeHandler<Message> >();
+ break;
+ }
+ }
+}
+
+Message* GeneratedMessageReflection::ReleaseLast(
+ Message* message,
+ const FieldDescriptor* field) const {
+ USAGE_CHECK_ALL(ReleaseLast, REPEATED, MESSAGE);
+
+ if (field->is_extension()) {
+ return static_cast<Message*>(
+ MutableExtensionSet(message)->ReleaseLast(field->number()));
+ } else {
+ return MutableRaw<RepeatedPtrFieldBase>(message, field)
+ ->ReleaseLast<GenericTypeHandler<Message> >();
+ }
+}
+
+void GeneratedMessageReflection::SwapElements(
+ Message* message,
+ const FieldDescriptor* field,
+ int index1,
+ int index2) const {
+ USAGE_CHECK_MESSAGE_TYPE(Swap);
+ USAGE_CHECK_REPEATED(Swap);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->SwapElements(field->number(), index1, index2);
+ } else {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(UPPERCASE, LOWERCASE) \
+ case FieldDescriptor::CPPTYPE_##UPPERCASE : \
+ MutableRaw<RepeatedField<LOWERCASE> >(message, field) \
+ ->SwapElements(index1, index2); \
+ break
+
+ HANDLE_TYPE( INT32, int32);
+ HANDLE_TYPE( INT64, int64);
+ HANDLE_TYPE(UINT32, uint32);
+ HANDLE_TYPE(UINT64, uint64);
+ HANDLE_TYPE(DOUBLE, double);
+ HANDLE_TYPE( FLOAT, float);
+ HANDLE_TYPE( BOOL, bool);
+ HANDLE_TYPE( ENUM, int);
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_STRING:
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ MutableRaw<RepeatedPtrFieldBase>(message, field)
+ ->SwapElements(index1, index2);
+ break;
+ }
+ }
+}
+
+namespace {
+// Comparison functor for sorting FieldDescriptors by field number.
+struct FieldNumberSorter {
+ bool operator()(const FieldDescriptor* left,
+ const FieldDescriptor* right) const {
+ return left->number() < right->number();
+ }
+};
+} // namespace
+
+void GeneratedMessageReflection::ListFields(
+ const Message& message,
+ vector<const FieldDescriptor*>* output) const {
+ output->clear();
+
+ // Optimization: The default instance never has any fields set.
+ if (&message == default_instance_) return;
+
+ for (int i = 0; i < descriptor_->field_count(); i++) {
+ const FieldDescriptor* field = descriptor_->field(i);
+ if (field->is_repeated()) {
+ if (FieldSize(message, field) > 0) {
+ output->push_back(field);
+ }
+ } else {
+ if (field->containing_oneof()) {
+ if (HasOneofField(message, field)) {
+ output->push_back(field);
+ }
+ } else if (HasBit(message, field)) {
+ output->push_back(field);
+ }
+ }
+ }
+
+ if (extensions_offset_ != -1) {
+ GetExtensionSet(message).AppendToList(descriptor_, descriptor_pool_,
+ output);
+ }
+
+ // ListFields() must sort output by field number.
+ sort(output->begin(), output->end(), FieldNumberSorter());
+}
+
+// -------------------------------------------------------------------
+
+#undef DEFINE_PRIMITIVE_ACCESSORS
+#define DEFINE_PRIMITIVE_ACCESSORS(TYPENAME, TYPE, PASSTYPE, CPPTYPE) \
+ PASSTYPE GeneratedMessageReflection::Get##TYPENAME( \
+ const Message& message, const FieldDescriptor* field) const { \
+ USAGE_CHECK_ALL(Get##TYPENAME, SINGULAR, CPPTYPE); \
+ if (field->is_extension()) { \
+ return GetExtensionSet(message).Get##TYPENAME( \
+ field->number(), field->default_value_##PASSTYPE()); \
+ } else { \
+ return GetField<TYPE>(message, field); \
+ } \
+ } \
+ \
+ void GeneratedMessageReflection::Set##TYPENAME( \
+ Message* message, const FieldDescriptor* field, \
+ PASSTYPE value) const { \
+ USAGE_CHECK_ALL(Set##TYPENAME, SINGULAR, CPPTYPE); \
+ if (field->is_extension()) { \
+ return MutableExtensionSet(message)->Set##TYPENAME( \
+ field->number(), field->type(), value, field); \
+ } else { \
+ SetField<TYPE>(message, field, value); \
+ } \
+ } \
+ \
+ PASSTYPE GeneratedMessageReflection::GetRepeated##TYPENAME( \
+ const Message& message, \
+ const FieldDescriptor* field, int index) const { \
+ USAGE_CHECK_ALL(GetRepeated##TYPENAME, REPEATED, CPPTYPE); \
+ if (field->is_extension()) { \
+ return GetExtensionSet(message).GetRepeated##TYPENAME( \
+ field->number(), index); \
+ } else { \
+ return GetRepeatedField<TYPE>(message, field, index); \
+ } \
+ } \
+ \
+ void GeneratedMessageReflection::SetRepeated##TYPENAME( \
+ Message* message, const FieldDescriptor* field, \
+ int index, PASSTYPE value) const { \
+ USAGE_CHECK_ALL(SetRepeated##TYPENAME, REPEATED, CPPTYPE); \
+ if (field->is_extension()) { \
+ MutableExtensionSet(message)->SetRepeated##TYPENAME( \
+ field->number(), index, value); \
+ } else { \
+ SetRepeatedField<TYPE>(message, field, index, value); \
+ } \
+ } \
+ \
+ void GeneratedMessageReflection::Add##TYPENAME( \
+ Message* message, const FieldDescriptor* field, \
+ PASSTYPE value) const { \
+ USAGE_CHECK_ALL(Add##TYPENAME, REPEATED, CPPTYPE); \
+ if (field->is_extension()) { \
+ MutableExtensionSet(message)->Add##TYPENAME( \
+ field->number(), field->type(), field->options().packed(), value, \
+ field); \
+ } else { \
+ AddField<TYPE>(message, field, value); \
+ } \
+ }
+
+DEFINE_PRIMITIVE_ACCESSORS(Int32 , int32 , int32 , INT32 )
+DEFINE_PRIMITIVE_ACCESSORS(Int64 , int64 , int64 , INT64 )
+DEFINE_PRIMITIVE_ACCESSORS(UInt32, uint32, uint32, UINT32)
+DEFINE_PRIMITIVE_ACCESSORS(UInt64, uint64, uint64, UINT64)
+DEFINE_PRIMITIVE_ACCESSORS(Float , float , float , FLOAT )
+DEFINE_PRIMITIVE_ACCESSORS(Double, double, double, DOUBLE)
+DEFINE_PRIMITIVE_ACCESSORS(Bool , bool , bool , BOOL )
+#undef DEFINE_PRIMITIVE_ACCESSORS
+
+// -------------------------------------------------------------------
+
+string GeneratedMessageReflection::GetString(
+ const Message& message, const FieldDescriptor* field) const {
+ USAGE_CHECK_ALL(GetString, SINGULAR, STRING);
+ if (field->is_extension()) {
+ return GetExtensionSet(message).GetString(field->number(),
+ field->default_value_string());
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ return *GetField<const string*>(message, field);
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+ return GetEmptyString(); // Make compiler happy.
+ }
+}
+
+const string& GeneratedMessageReflection::GetStringReference(
+ const Message& message,
+ const FieldDescriptor* field, string* scratch) const {
+ USAGE_CHECK_ALL(GetStringReference, SINGULAR, STRING);
+ if (field->is_extension()) {
+ return GetExtensionSet(message).GetString(field->number(),
+ field->default_value_string());
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ return *GetField<const string*>(message, field);
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+ return GetEmptyString(); // Make compiler happy.
+ }
+}
+
+
+void GeneratedMessageReflection::SetString(
+ Message* message, const FieldDescriptor* field,
+ const string& value) const {
+ USAGE_CHECK_ALL(SetString, SINGULAR, STRING);
+ if (field->is_extension()) {
+ return MutableExtensionSet(message)->SetString(field->number(),
+ field->type(), value, field);
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING: {
+ if (field->containing_oneof() && !HasOneofField(*message, field)) {
+ ClearOneof(message, field->containing_oneof());
+ *MutableField<string*>(message, field) = new string;
+ }
+ string** ptr = MutableField<string*>(message, field);
+ if (*ptr == DefaultRaw<const string*>(field)) {
+ *ptr = new string(value);
+ } else {
+ (*ptr)->assign(value);
+ }
+ break;
+ }
+ }
+ }
+}
+
+
+string GeneratedMessageReflection::GetRepeatedString(
+ const Message& message, const FieldDescriptor* field, int index) const {
+ USAGE_CHECK_ALL(GetRepeatedString, REPEATED, STRING);
+ if (field->is_extension()) {
+ return GetExtensionSet(message).GetRepeatedString(field->number(), index);
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ return GetRepeatedPtrField<string>(message, field, index);
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+ return GetEmptyString(); // Make compiler happy.
+ }
+}
+
+const string& GeneratedMessageReflection::GetRepeatedStringReference(
+ const Message& message, const FieldDescriptor* field,
+ int index, string* scratch) const {
+ USAGE_CHECK_ALL(GetRepeatedStringReference, REPEATED, STRING);
+ if (field->is_extension()) {
+ return GetExtensionSet(message).GetRepeatedString(field->number(), index);
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ return GetRepeatedPtrField<string>(message, field, index);
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+ return GetEmptyString(); // Make compiler happy.
+ }
+}
+
+
+void GeneratedMessageReflection::SetRepeatedString(
+ Message* message, const FieldDescriptor* field,
+ int index, const string& value) const {
+ USAGE_CHECK_ALL(SetRepeatedString, REPEATED, STRING);
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->SetRepeatedString(
+ field->number(), index, value);
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ *MutableRepeatedField<string>(message, field, index) = value;
+ break;
+ }
+ }
+}
+
+
+void GeneratedMessageReflection::AddString(
+ Message* message, const FieldDescriptor* field,
+ const string& value) const {
+ USAGE_CHECK_ALL(AddString, REPEATED, STRING);
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->AddString(field->number(),
+ field->type(), value, field);
+ } else {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ *AddField<string>(message, field) = value;
+ break;
+ }
+ }
+}
+
+
+// -------------------------------------------------------------------
+
+const EnumValueDescriptor* GeneratedMessageReflection::GetEnum(
+ const Message& message, const FieldDescriptor* field) const {
+ USAGE_CHECK_ALL(GetEnum, SINGULAR, ENUM);
+
+ int value;
+ if (field->is_extension()) {
+ value = GetExtensionSet(message).GetEnum(
+ field->number(), field->default_value_enum()->number());
+ } else {
+ value = GetField<int>(message, field);
+ }
+ const EnumValueDescriptor* result =
+ field->enum_type()->FindValueByNumber(value);
+ GOOGLE_CHECK(result != NULL) << "Value " << value << " is not valid for field "
+ << field->full_name() << " of type "
+ << field->enum_type()->full_name() << ".";
+ return result;
+}
+
+void GeneratedMessageReflection::SetEnum(
+ Message* message, const FieldDescriptor* field,
+ const EnumValueDescriptor* value) const {
+ USAGE_CHECK_ALL(SetEnum, SINGULAR, ENUM);
+ USAGE_CHECK_ENUM_VALUE(SetEnum);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->SetEnum(field->number(), field->type(),
+ value->number(), field);
+ } else {
+ SetField<int>(message, field, value->number());
+ }
+}
+
+const EnumValueDescriptor* GeneratedMessageReflection::GetRepeatedEnum(
+ const Message& message, const FieldDescriptor* field, int index) const {
+ USAGE_CHECK_ALL(GetRepeatedEnum, REPEATED, ENUM);
+
+ int value;
+ if (field->is_extension()) {
+ value = GetExtensionSet(message).GetRepeatedEnum(field->number(), index);
+ } else {
+ value = GetRepeatedField<int>(message, field, index);
+ }
+ const EnumValueDescriptor* result =
+ field->enum_type()->FindValueByNumber(value);
+ GOOGLE_CHECK(result != NULL) << "Value " << value << " is not valid for field "
+ << field->full_name() << " of type "
+ << field->enum_type()->full_name() << ".";
+ return result;
+}
+
+void GeneratedMessageReflection::SetRepeatedEnum(
+ Message* message,
+ const FieldDescriptor* field, int index,
+ const EnumValueDescriptor* value) const {
+ USAGE_CHECK_ALL(SetRepeatedEnum, REPEATED, ENUM);
+ USAGE_CHECK_ENUM_VALUE(SetRepeatedEnum);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->SetRepeatedEnum(
+ field->number(), index, value->number());
+ } else {
+ SetRepeatedField<int>(message, field, index, value->number());
+ }
+}
+
+void GeneratedMessageReflection::AddEnum(
+ Message* message, const FieldDescriptor* field,
+ const EnumValueDescriptor* value) const {
+ USAGE_CHECK_ALL(AddEnum, REPEATED, ENUM);
+ USAGE_CHECK_ENUM_VALUE(AddEnum);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->AddEnum(field->number(), field->type(),
+ field->options().packed(),
+ value->number(), field);
+ } else {
+ AddField<int>(message, field, value->number());
+ }
+}
+
+// -------------------------------------------------------------------
+
+const Message& GeneratedMessageReflection::GetMessage(
+ const Message& message, const FieldDescriptor* field,
+ MessageFactory* factory) const {
+ USAGE_CHECK_ALL(GetMessage, SINGULAR, MESSAGE);
+
+ if (factory == NULL) factory = message_factory_;
+
+ if (field->is_extension()) {
+ return static_cast<const Message&>(
+ GetExtensionSet(message).GetMessage(
+ field->number(), field->message_type(), factory));
+ } else {
+ const Message* result;
+ result = GetRaw<const Message*>(message, field);
+ if (result == NULL) {
+ result = DefaultRaw<const Message*>(field);
+ }
+ return *result;
+ }
+}
+
+Message* GeneratedMessageReflection::MutableMessage(
+ Message* message, const FieldDescriptor* field,
+ MessageFactory* factory) const {
+ if (factory == NULL) factory = message_factory_;
+
+ if (field->is_extension()) {
+ return static_cast<Message*>(
+ MutableExtensionSet(message)->MutableMessage(field, factory));
+ } else {
+ Message* result;
+ Message** result_holder = MutableRaw<Message*>(message, field);
+
+ if (field->containing_oneof()) {
+ if (!HasOneofField(*message, field)) {
+ ClearOneof(message, field->containing_oneof());
+ result_holder = MutableField<Message*>(message, field);
+ const Message* default_message = DefaultRaw<const Message*>(field);
+ *result_holder = default_message->New();
+ }
+ } else {
+ SetBit(message, field);
+ }
+
+ if (*result_holder == NULL) {
+ const Message* default_message = DefaultRaw<const Message*>(field);
+ *result_holder = default_message->New();
+ }
+ result = *result_holder;
+ return result;
+ }
+}
+
+void GeneratedMessageReflection::SetAllocatedMessage(
+ Message* message,
+ Message* sub_message,
+ const FieldDescriptor* field) const {
+ USAGE_CHECK_ALL(SetAllocatedMessage, SINGULAR, MESSAGE);
+
+ if (field->is_extension()) {
+ MutableExtensionSet(message)->SetAllocatedMessage(
+ field->number(), field->type(), field, sub_message);
+ } else {
+ if (field->containing_oneof()) {
+ if (sub_message == NULL) {
+ ClearOneof(message, field->containing_oneof());
+ return;
+ }
+ ClearOneof(message, field->containing_oneof());
+ *MutableRaw<Message*>(message, field) = sub_message;
+ SetOneofCase(message, field);
+ return;
+ }
+
+ if (sub_message == NULL) {
+ ClearBit(message, field);
+ } else {
+ SetBit(message, field);
+ }
+ Message** sub_message_holder = MutableRaw<Message*>(message, field);
+ delete *sub_message_holder;
+ *sub_message_holder = sub_message;
+ }
+}
+
+Message* GeneratedMessageReflection::ReleaseMessage(
+ Message* message,
+ const FieldDescriptor* field,
+ MessageFactory* factory) const {
+ USAGE_CHECK_ALL(ReleaseMessage, SINGULAR, MESSAGE);
+
+ if (factory == NULL) factory = message_factory_;
+
+ if (field->is_extension()) {
+ return static_cast<Message*>(
+ MutableExtensionSet(message)->ReleaseMessage(field, factory));
+ } else {
+ ClearBit(message, field);
+ if (field->containing_oneof()) {
+ if (HasOneofField(*message, field)) {
+ *MutableOneofCase(message, field->containing_oneof()) = 0;
+ } else {
+ return NULL;
+ }
+ }
+ Message** result = MutableRaw<Message*>(message, field);
+ Message* ret = *result;
+ *result = NULL;
+ return ret;
+ }
+}
+
+const Message& GeneratedMessageReflection::GetRepeatedMessage(
+ const Message& message, const FieldDescriptor* field, int index) const {
+ USAGE_CHECK_ALL(GetRepeatedMessage, REPEATED, MESSAGE);
+
+ if (field->is_extension()) {
+ return static_cast<const Message&>(
+ GetExtensionSet(message).GetRepeatedMessage(field->number(), index));
+ } else {
+ return GetRaw<RepeatedPtrFieldBase>(message, field)
+ .Get<GenericTypeHandler<Message> >(index);
+ }
+}
+
+Message* GeneratedMessageReflection::MutableRepeatedMessage(
+ Message* message, const FieldDescriptor* field, int index) const {
+ USAGE_CHECK_ALL(MutableRepeatedMessage, REPEATED, MESSAGE);
+
+ if (field->is_extension()) {
+ return static_cast<Message*>(
+ MutableExtensionSet(message)->MutableRepeatedMessage(
+ field->number(), index));
+ } else {
+ return MutableRaw<RepeatedPtrFieldBase>(message, field)
+ ->Mutable<GenericTypeHandler<Message> >(index);
+ }
+}
+
+Message* GeneratedMessageReflection::AddMessage(
+ Message* message, const FieldDescriptor* field,
+ MessageFactory* factory) const {
+ USAGE_CHECK_ALL(AddMessage, REPEATED, MESSAGE);
+
+ if (factory == NULL) factory = message_factory_;
+
+ if (field->is_extension()) {
+ return static_cast<Message*>(
+ MutableExtensionSet(message)->AddMessage(field, factory));
+ } else {
+ // We can't use AddField<Message>() because RepeatedPtrFieldBase doesn't
+ // know how to allocate one.
+ RepeatedPtrFieldBase* repeated =
+ MutableRaw<RepeatedPtrFieldBase>(message, field);
+ Message* result = repeated->AddFromCleared<GenericTypeHandler<Message> >();
+ if (result == NULL) {
+ // We must allocate a new object.
+ const Message* prototype;
+ if (repeated->size() == 0) {
+ prototype = factory->GetPrototype(field->message_type());
+ } else {
+ prototype = &repeated->Get<GenericTypeHandler<Message> >(0);
+ }
+ result = prototype->New();
+ repeated->AddAllocated<GenericTypeHandler<Message> >(result);
+ }
+ return result;
+ }
+}
+
+void* GeneratedMessageReflection::MutableRawRepeatedField(
+ Message* message, const FieldDescriptor* field,
+ FieldDescriptor::CppType cpptype,
+ int ctype, const Descriptor* desc) const {
+ USAGE_CHECK_REPEATED("MutableRawRepeatedField");
+ if (field->cpp_type() != cpptype)
+ ReportReflectionUsageTypeError(descriptor_,
+ field, "MutableRawRepeatedField", cpptype);
+ if (ctype >= 0)
+ GOOGLE_CHECK_EQ(field->options().ctype(), ctype) << "subtype mismatch";
+ if (desc != NULL)
+ GOOGLE_CHECK_EQ(field->message_type(), desc) << "wrong submessage type";
+ if (field->is_extension())
+ return MutableExtensionSet(message)->MutableRawRepeatedField(
+ field->number(), field->type(), field->is_packed(), field);
+ else
+ return reinterpret_cast<uint8*>(message) + offsets_[field->index()];
+}
+
+const FieldDescriptor* GeneratedMessageReflection::GetOneofFieldDescriptor(
+ const Message& message,
+ const OneofDescriptor* oneof_descriptor) const {
+ uint32 field_number = GetOneofCase(message, oneof_descriptor);
+ if (field_number == 0) {
+ return NULL;
+ }
+ return descriptor_->FindFieldByNumber(field_number);
+}
+
+// -----------------------------------------------------------------------------
+
+const FieldDescriptor* GeneratedMessageReflection::FindKnownExtensionByName(
+ const string& name) const {
+ if (extensions_offset_ == -1) return NULL;
+
+ const FieldDescriptor* result = descriptor_pool_->FindExtensionByName(name);
+ if (result != NULL && result->containing_type() == descriptor_) {
+ return result;
+ }
+
+ if (descriptor_->options().message_set_wire_format()) {
+ // MessageSet extensions may be identified by type name.
+ const Descriptor* type = descriptor_pool_->FindMessageTypeByName(name);
+ if (type != NULL) {
+ // Look for a matching extension in the foreign type's scope.
+ for (int i = 0; i < type->extension_count(); i++) {
+ const FieldDescriptor* extension = type->extension(i);
+ if (extension->containing_type() == descriptor_ &&
+ extension->type() == FieldDescriptor::TYPE_MESSAGE &&
+ extension->is_optional() &&
+ extension->message_type() == type) {
+ // Found it.
+ return extension;
+ }
+ }
+ }
+ }
+
+ return NULL;
+}
+
+const FieldDescriptor* GeneratedMessageReflection::FindKnownExtensionByNumber(
+ int number) const {
+ if (extensions_offset_ == -1) return NULL;
+ return descriptor_pool_->FindExtensionByNumber(descriptor_, number);
+}
+
+// ===================================================================
+// Some private helpers.
+
+// These simple template accessors obtain pointers (or references) to
+// the given field.
+template <typename Type>
+inline const Type& GeneratedMessageReflection::GetRaw(
+ const Message& message, const FieldDescriptor* field) const {
+ if (field->containing_oneof() && !HasOneofField(message, field)) {
+ return DefaultRaw<Type>(field);
+ }
+ int index = field->containing_oneof() ?
+ descriptor_->field_count() + field->containing_oneof()->index() :
+ field->index();
+ const void* ptr = reinterpret_cast<const uint8*>(&message) +
+ offsets_[index];
+ return *reinterpret_cast<const Type*>(ptr);
+}
+
+template <typename Type>
+inline Type* GeneratedMessageReflection::MutableRaw(
+ Message* message, const FieldDescriptor* field) const {
+ int index = field->containing_oneof() ?
+ descriptor_->field_count() + field->containing_oneof()->index() :
+ field->index();
+ void* ptr = reinterpret_cast<uint8*>(message) + offsets_[index];
+ return reinterpret_cast<Type*>(ptr);
+}
+
+template <typename Type>
+inline const Type& GeneratedMessageReflection::DefaultRaw(
+ const FieldDescriptor* field) const {
+ const void* ptr = field->containing_oneof() ?
+ reinterpret_cast<const uint8*>(default_oneof_instance_) +
+ offsets_[field->index()] :
+ reinterpret_cast<const uint8*>(default_instance_) +
+ offsets_[field->index()];
+ return *reinterpret_cast<const Type*>(ptr);
+}
+
+inline const uint32* GeneratedMessageReflection::GetHasBits(
+ const Message& message) const {
+ const void* ptr = reinterpret_cast<const uint8*>(&message) + has_bits_offset_;
+ return reinterpret_cast<const uint32*>(ptr);
+}
+inline uint32* GeneratedMessageReflection::MutableHasBits(
+ Message* message) const {
+ void* ptr = reinterpret_cast<uint8*>(message) + has_bits_offset_;
+ return reinterpret_cast<uint32*>(ptr);
+}
+
+inline uint32 GeneratedMessageReflection::GetOneofCase(
+ const Message& message,
+ const OneofDescriptor* oneof_descriptor) const {
+ const void* ptr = reinterpret_cast<const uint8*>(&message)
+ + oneof_case_offset_;
+ return reinterpret_cast<const uint32*>(ptr)[oneof_descriptor->index()];
+}
+
+inline uint32* GeneratedMessageReflection::MutableOneofCase(
+ Message* message,
+ const OneofDescriptor* oneof_descriptor) const {
+ void* ptr = reinterpret_cast<uint8*>(message) + oneof_case_offset_;
+ return &(reinterpret_cast<uint32*>(ptr)[oneof_descriptor->index()]);
+}
+
+inline const ExtensionSet& GeneratedMessageReflection::GetExtensionSet(
+ const Message& message) const {
+ GOOGLE_DCHECK_NE(extensions_offset_, -1);
+ const void* ptr = reinterpret_cast<const uint8*>(&message) +
+ extensions_offset_;
+ return *reinterpret_cast<const ExtensionSet*>(ptr);
+}
+inline ExtensionSet* GeneratedMessageReflection::MutableExtensionSet(
+ Message* message) const {
+ GOOGLE_DCHECK_NE(extensions_offset_, -1);
+ void* ptr = reinterpret_cast<uint8*>(message) + extensions_offset_;
+ return reinterpret_cast<ExtensionSet*>(ptr);
+}
+
+// Simple accessors for manipulating has_bits_.
+inline bool GeneratedMessageReflection::HasBit(
+ const Message& message, const FieldDescriptor* field) const {
+ return GetHasBits(message)[field->index() / 32] &
+ (1 << (field->index() % 32));
+}
+
+inline void GeneratedMessageReflection::SetBit(
+ Message* message, const FieldDescriptor* field) const {
+ MutableHasBits(message)[field->index() / 32] |= (1 << (field->index() % 32));
+}
+
+inline void GeneratedMessageReflection::ClearBit(
+ Message* message, const FieldDescriptor* field) const {
+ MutableHasBits(message)[field->index() / 32] &= ~(1 << (field->index() % 32));
+}
+
+inline void GeneratedMessageReflection::SwapBit(
+ Message* message1, Message* message2, const FieldDescriptor* field) const {
+ bool temp_has_bit = HasBit(*message1, field);
+ if (HasBit(*message2, field)) {
+ SetBit(message1, field);
+ } else {
+ ClearBit(message1, field);
+ }
+ if (temp_has_bit) {
+ SetBit(message2, field);
+ } else {
+ ClearBit(message2, field);
+ }
+}
+
+inline bool GeneratedMessageReflection::HasOneof(
+ const Message& message, const OneofDescriptor* oneof_descriptor) const {
+ return (GetOneofCase(message, oneof_descriptor) > 0);
+}
+
+inline bool GeneratedMessageReflection::HasOneofField(
+ const Message& message, const FieldDescriptor* field) const {
+ return (GetOneofCase(message, field->containing_oneof()) == field->number());
+}
+
+inline void GeneratedMessageReflection::SetOneofCase(
+ Message* message, const FieldDescriptor* field) const {
+ *MutableOneofCase(message, field->containing_oneof()) = field->number();
+}
+
+inline void GeneratedMessageReflection::ClearOneofField(
+ Message* message, const FieldDescriptor* field) const {
+ if (HasOneofField(*message, field)) {
+ ClearOneof(message, field->containing_oneof());
+ }
+}
+
+inline void GeneratedMessageReflection::ClearOneof(
+ Message* message, const OneofDescriptor* oneof_descriptor) const {
+ // TODO(jieluo): Consider to cache the unused object instead of deleting
+ // it. It will be much faster if an aplication switches a lot from
+ // a few oneof fields. Time/space tradeoff
+ uint32 oneof_case = GetOneofCase(*message, oneof_descriptor);
+ if (oneof_case > 0) {
+ const FieldDescriptor* field = descriptor_->FindFieldByNumber(oneof_case);
+ switch (field->cpp_type()) {
+ case FieldDescriptor::CPPTYPE_STRING: {
+ switch (field->options().ctype()) {
+ default: // TODO(kenton): Support other string reps.
+ case FieldOptions::STRING:
+ delete *MutableRaw<string*>(message, field);
+ break;
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ delete *MutableRaw<Message*>(message, field);
+ break;
+ default:
+ break;
+ }
+
+ *MutableOneofCase(message, oneof_descriptor) = 0;
+ }
+}
+
+// Template implementations of basic accessors. Inline because each
+// template instance is only called from one location. These are
+// used for all types except messages.
+template <typename Type>
+inline const Type& GeneratedMessageReflection::GetField(
+ const Message& message, const FieldDescriptor* field) const {
+ return GetRaw<Type>(message, field);
+}
+
+template <typename Type>
+inline void GeneratedMessageReflection::SetField(
+ Message* message, const FieldDescriptor* field, const Type& value) const {
+ if (field->containing_oneof() && !HasOneofField(*message, field)) {
+ ClearOneof(message, field->containing_oneof());
+ }
+ *MutableRaw<Type>(message, field) = value;
+ field->containing_oneof() ?
+ SetOneofCase(message, field) : SetBit(message, field);
+}
+
+template <typename Type>
+inline Type* GeneratedMessageReflection::MutableField(
+ Message* message, const FieldDescriptor* field) const {
+ field->containing_oneof() ?
+ SetOneofCase(message, field) : SetBit(message, field);
+ return MutableRaw<Type>(message, field);
+}
+
+template <typename Type>
+inline const Type& GeneratedMessageReflection::GetRepeatedField(
+ const Message& message, const FieldDescriptor* field, int index) const {
+ return GetRaw<RepeatedField<Type> >(message, field).Get(index);
+}
+
+template <typename Type>
+inline const Type& GeneratedMessageReflection::GetRepeatedPtrField(
+ const Message& message, const FieldDescriptor* field, int index) const {
+ return GetRaw<RepeatedPtrField<Type> >(message, field).Get(index);
+}
+
+template <typename Type>
+inline void GeneratedMessageReflection::SetRepeatedField(
+ Message* message, const FieldDescriptor* field,
+ int index, Type value) const {
+ MutableRaw<RepeatedField<Type> >(message, field)->Set(index, value);
+}
+
+template <typename Type>
+inline Type* GeneratedMessageReflection::MutableRepeatedField(
+ Message* message, const FieldDescriptor* field, int index) const {
+ RepeatedPtrField<Type>* repeated =
+ MutableRaw<RepeatedPtrField<Type> >(message, field);
+ return repeated->Mutable(index);
+}
+
+template <typename Type>
+inline void GeneratedMessageReflection::AddField(
+ Message* message, const FieldDescriptor* field, const Type& value) const {
+ MutableRaw<RepeatedField<Type> >(message, field)->Add(value);
+}
+
+template <typename Type>
+inline Type* GeneratedMessageReflection::AddField(
+ Message* message, const FieldDescriptor* field) const {
+ RepeatedPtrField<Type>* repeated =
+ MutableRaw<RepeatedPtrField<Type> >(message, field);
+ return repeated->Add();
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.h b/toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.h
new file mode 100644
index 0000000000..b6671ad06b
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/generated_message_reflection.h
@@ -0,0 +1,504 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This header is logically internal, but is made public because it is used
+// from protocol-compiler-generated code, which may reside in other components.
+
+#ifndef GOOGLE_PROTOBUF_GENERATED_MESSAGE_REFLECTION_H__
+#define GOOGLE_PROTOBUF_GENERATED_MESSAGE_REFLECTION_H__
+
+#include <string>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+// TODO(jasonh): Remove this once the compiler change to directly include this
+// is released to components.
+#include <google/protobuf/generated_enum_reflection.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/unknown_field_set.h>
+
+
+namespace google {
+namespace upb {
+namespace google_opensource {
+class GMR_Handlers;
+} // namespace google_opensource
+} // namespace upb
+
+namespace protobuf {
+ class DescriptorPool;
+}
+
+namespace protobuf {
+namespace internal {
+class DefaultEmptyOneof;
+
+// Defined in this file.
+class GeneratedMessageReflection;
+
+// Defined in other files.
+class ExtensionSet; // extension_set.h
+
+// THIS CLASS IS NOT INTENDED FOR DIRECT USE. It is intended for use
+// by generated code. This class is just a big hack that reduces code
+// size.
+//
+// A GeneratedMessageReflection is an implementation of Reflection
+// which expects all fields to be backed by simple variables located in
+// memory. The locations are given using a base pointer and a set of
+// offsets.
+//
+// It is required that the user represents fields of each type in a standard
+// way, so that GeneratedMessageReflection can cast the void* pointer to
+// the appropriate type. For primitive fields and string fields, each field
+// should be represented using the obvious C++ primitive type. Enums and
+// Messages are different:
+// - Singular Message fields are stored as a pointer to a Message. These
+// should start out NULL, except for in the default instance where they
+// should start out pointing to other default instances.
+// - Enum fields are stored as an int. This int must always contain
+// a valid value, such that EnumDescriptor::FindValueByNumber() would
+// not return NULL.
+// - Repeated fields are stored as RepeatedFields or RepeatedPtrFields
+// of whatever type the individual field would be. Strings and
+// Messages use RepeatedPtrFields while everything else uses
+// RepeatedFields.
+class LIBPROTOBUF_EXPORT GeneratedMessageReflection : public Reflection {
+ public:
+ // Constructs a GeneratedMessageReflection.
+ // Parameters:
+ // descriptor: The descriptor for the message type being implemented.
+ // default_instance: The default instance of the message. This is only
+ // used to obtain pointers to default instances of embedded
+ // messages, which GetMessage() will return if the particular
+ // sub-message has not been initialized yet. (Thus, all
+ // embedded message fields *must* have non-NULL pointers
+ // in the default instance.)
+ // offsets: An array of ints giving the byte offsets, relative to
+ // the start of the message object, of each field. These can
+ // be computed at compile time using the
+ // GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET() macro, defined
+ // below.
+ // has_bits_offset: Offset in the message of an array of uint32s of size
+ // descriptor->field_count()/32, rounded up. This is a
+ // bitfield where each bit indicates whether or not the
+ // corresponding field of the message has been initialized.
+ // The bit for field index i is obtained by the expression:
+ // has_bits[i / 32] & (1 << (i % 32))
+ // unknown_fields_offset: Offset in the message of the UnknownFieldSet for
+ // the message.
+ // extensions_offset: Offset in the message of the ExtensionSet for the
+ // message, or -1 if the message type has no extension
+ // ranges.
+ // pool: DescriptorPool to search for extension definitions. Only
+ // used by FindKnownExtensionByName() and
+ // FindKnownExtensionByNumber().
+ // factory: MessageFactory to use to construct extension messages.
+ // object_size: The size of a message object of this type, as measured
+ // by sizeof().
+ GeneratedMessageReflection(const Descriptor* descriptor,
+ const Message* default_instance,
+ const int offsets[],
+ int has_bits_offset,
+ int unknown_fields_offset,
+ int extensions_offset,
+ const DescriptorPool* pool,
+ MessageFactory* factory,
+ int object_size);
+
+ // Similar with the construction above. Call this construction if the
+ // message has oneof definition.
+ // Parameters:
+ // offsets: An array of ints giving the byte offsets.
+ // For each oneof field, the offset is relative to the
+ // default_oneof_instance. These can be computed at compile
+ // time using the
+ // PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET() macro.
+ // For each none oneof field, the offset is related to
+ // the start of the message object. These can be computed
+ // at compile time using the
+ // GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET() macro.
+ // Besides offsets for all fields, this array also contains
+ // offsets for oneof unions. The offset of the i-th oneof
+ // union is offsets[descriptor->field_count() + i].
+ // default_oneof_instance: The default instance of the oneofs. It is a
+ // struct holding the default value of all oneof fields
+ // for this message. It is only used to obtain pointers
+ // to default instances of oneof fields, which Get
+ // methods will return if the field is not set.
+ // oneof_case_offset: Offset in the message of an array of uint32s of
+ // size descriptor->oneof_decl_count(). Each uint32
+ // indicates what field is set for each oneof.
+ // other parameters are the same with the construction above.
+ GeneratedMessageReflection(const Descriptor* descriptor,
+ const Message* default_instance,
+ const int offsets[],
+ int has_bits_offset,
+ int unknown_fields_offset,
+ int extensions_offset,
+ const void* default_oneof_instance,
+ int oneof_case_offset,
+ const DescriptorPool* pool,
+ MessageFactory* factory,
+ int object_size);
+ ~GeneratedMessageReflection();
+
+ // implements Reflection -------------------------------------------
+
+ const UnknownFieldSet& GetUnknownFields(const Message& message) const;
+ UnknownFieldSet* MutableUnknownFields(Message* message) const;
+
+ int SpaceUsed(const Message& message) const;
+
+ bool HasField(const Message& message, const FieldDescriptor* field) const;
+ int FieldSize(const Message& message, const FieldDescriptor* field) const;
+ void ClearField(Message* message, const FieldDescriptor* field) const;
+ bool HasOneof(const Message& message,
+ const OneofDescriptor* oneof_descriptor) const;
+ void ClearOneof(Message* message, const OneofDescriptor* field) const;
+ void RemoveLast(Message* message, const FieldDescriptor* field) const;
+ Message* ReleaseLast(Message* message, const FieldDescriptor* field) const;
+ void Swap(Message* message1, Message* message2) const;
+ void SwapFields(Message* message1, Message* message2,
+ const vector<const FieldDescriptor*>& fields) const;
+ void SwapElements(Message* message, const FieldDescriptor* field,
+ int index1, int index2) const;
+ void ListFields(const Message& message,
+ vector<const FieldDescriptor*>* output) const;
+
+ int32 GetInt32 (const Message& message,
+ const FieldDescriptor* field) const;
+ int64 GetInt64 (const Message& message,
+ const FieldDescriptor* field) const;
+ uint32 GetUInt32(const Message& message,
+ const FieldDescriptor* field) const;
+ uint64 GetUInt64(const Message& message,
+ const FieldDescriptor* field) const;
+ float GetFloat (const Message& message,
+ const FieldDescriptor* field) const;
+ double GetDouble(const Message& message,
+ const FieldDescriptor* field) const;
+ bool GetBool (const Message& message,
+ const FieldDescriptor* field) const;
+ string GetString(const Message& message,
+ const FieldDescriptor* field) const;
+ const string& GetStringReference(const Message& message,
+ const FieldDescriptor* field,
+ string* scratch) const;
+ const EnumValueDescriptor* GetEnum(const Message& message,
+ const FieldDescriptor* field) const;
+ const Message& GetMessage(const Message& message,
+ const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const;
+
+ const FieldDescriptor* GetOneofFieldDescriptor(
+ const Message& message,
+ const OneofDescriptor* oneof_descriptor) const;
+
+ public:
+ void SetInt32 (Message* message,
+ const FieldDescriptor* field, int32 value) const;
+ void SetInt64 (Message* message,
+ const FieldDescriptor* field, int64 value) const;
+ void SetUInt32(Message* message,
+ const FieldDescriptor* field, uint32 value) const;
+ void SetUInt64(Message* message,
+ const FieldDescriptor* field, uint64 value) const;
+ void SetFloat (Message* message,
+ const FieldDescriptor* field, float value) const;
+ void SetDouble(Message* message,
+ const FieldDescriptor* field, double value) const;
+ void SetBool (Message* message,
+ const FieldDescriptor* field, bool value) const;
+ void SetString(Message* message,
+ const FieldDescriptor* field,
+ const string& value) const;
+ void SetEnum (Message* message, const FieldDescriptor* field,
+ const EnumValueDescriptor* value) const;
+ Message* MutableMessage(Message* message, const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const;
+ void SetAllocatedMessage(Message* message,
+ Message* sub_message,
+ const FieldDescriptor* field) const;
+ Message* ReleaseMessage(Message* message, const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const;
+
+ int32 GetRepeatedInt32 (const Message& message,
+ const FieldDescriptor* field, int index) const;
+ int64 GetRepeatedInt64 (const Message& message,
+ const FieldDescriptor* field, int index) const;
+ uint32 GetRepeatedUInt32(const Message& message,
+ const FieldDescriptor* field, int index) const;
+ uint64 GetRepeatedUInt64(const Message& message,
+ const FieldDescriptor* field, int index) const;
+ float GetRepeatedFloat (const Message& message,
+ const FieldDescriptor* field, int index) const;
+ double GetRepeatedDouble(const Message& message,
+ const FieldDescriptor* field, int index) const;
+ bool GetRepeatedBool (const Message& message,
+ const FieldDescriptor* field, int index) const;
+ string GetRepeatedString(const Message& message,
+ const FieldDescriptor* field, int index) const;
+ const string& GetRepeatedStringReference(const Message& message,
+ const FieldDescriptor* field,
+ int index, string* scratch) const;
+ const EnumValueDescriptor* GetRepeatedEnum(const Message& message,
+ const FieldDescriptor* field,
+ int index) const;
+ const Message& GetRepeatedMessage(const Message& message,
+ const FieldDescriptor* field,
+ int index) const;
+
+ // Set the value of a field.
+ void SetRepeatedInt32 (Message* message,
+ const FieldDescriptor* field, int index, int32 value) const;
+ void SetRepeatedInt64 (Message* message,
+ const FieldDescriptor* field, int index, int64 value) const;
+ void SetRepeatedUInt32(Message* message,
+ const FieldDescriptor* field, int index, uint32 value) const;
+ void SetRepeatedUInt64(Message* message,
+ const FieldDescriptor* field, int index, uint64 value) const;
+ void SetRepeatedFloat (Message* message,
+ const FieldDescriptor* field, int index, float value) const;
+ void SetRepeatedDouble(Message* message,
+ const FieldDescriptor* field, int index, double value) const;
+ void SetRepeatedBool (Message* message,
+ const FieldDescriptor* field, int index, bool value) const;
+ void SetRepeatedString(Message* message,
+ const FieldDescriptor* field, int index,
+ const string& value) const;
+ void SetRepeatedEnum(Message* message, const FieldDescriptor* field,
+ int index, const EnumValueDescriptor* value) const;
+ // Get a mutable pointer to a field with a message type.
+ Message* MutableRepeatedMessage(Message* message,
+ const FieldDescriptor* field,
+ int index) const;
+
+ void AddInt32 (Message* message,
+ const FieldDescriptor* field, int32 value) const;
+ void AddInt64 (Message* message,
+ const FieldDescriptor* field, int64 value) const;
+ void AddUInt32(Message* message,
+ const FieldDescriptor* field, uint32 value) const;
+ void AddUInt64(Message* message,
+ const FieldDescriptor* field, uint64 value) const;
+ void AddFloat (Message* message,
+ const FieldDescriptor* field, float value) const;
+ void AddDouble(Message* message,
+ const FieldDescriptor* field, double value) const;
+ void AddBool (Message* message,
+ const FieldDescriptor* field, bool value) const;
+ void AddString(Message* message,
+ const FieldDescriptor* field, const string& value) const;
+ void AddEnum(Message* message,
+ const FieldDescriptor* field,
+ const EnumValueDescriptor* value) const;
+ Message* AddMessage(Message* message, const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const;
+
+ const FieldDescriptor* FindKnownExtensionByName(const string& name) const;
+ const FieldDescriptor* FindKnownExtensionByNumber(int number) const;
+
+ protected:
+ virtual void* MutableRawRepeatedField(
+ Message* message, const FieldDescriptor* field, FieldDescriptor::CppType,
+ int ctype, const Descriptor* desc) const;
+
+ private:
+ friend class GeneratedMessage;
+
+ // To parse directly into a proto2 generated class, the class GMR_Handlers
+ // needs access to member offsets and hasbits.
+ friend class LIBPROTOBUF_EXPORT upb::google_opensource::GMR_Handlers;
+
+ const Descriptor* descriptor_;
+ const Message* default_instance_;
+ const void* default_oneof_instance_;
+ const int* offsets_;
+
+ int has_bits_offset_;
+ int oneof_case_offset_;
+ int unknown_fields_offset_;
+ int extensions_offset_;
+ int object_size_;
+
+ const DescriptorPool* descriptor_pool_;
+ MessageFactory* message_factory_;
+
+ template <typename Type>
+ inline const Type& GetRaw(const Message& message,
+ const FieldDescriptor* field) const;
+ template <typename Type>
+ inline Type* MutableRaw(Message* message,
+ const FieldDescriptor* field) const;
+ template <typename Type>
+ inline const Type& DefaultRaw(const FieldDescriptor* field) const;
+ template <typename Type>
+ inline const Type& DefaultOneofRaw(const FieldDescriptor* field) const;
+
+ inline const uint32* GetHasBits(const Message& message) const;
+ inline uint32* MutableHasBits(Message* message) const;
+ inline uint32 GetOneofCase(
+ const Message& message,
+ const OneofDescriptor* oneof_descriptor) const;
+ inline uint32* MutableOneofCase(
+ Message* message,
+ const OneofDescriptor* oneof_descriptor) const;
+ inline const ExtensionSet& GetExtensionSet(const Message& message) const;
+ inline ExtensionSet* MutableExtensionSet(Message* message) const;
+
+ inline bool HasBit(const Message& message,
+ const FieldDescriptor* field) const;
+ inline void SetBit(Message* message,
+ const FieldDescriptor* field) const;
+ inline void ClearBit(Message* message,
+ const FieldDescriptor* field) const;
+ inline void SwapBit(Message* message1,
+ Message* message2,
+ const FieldDescriptor* field) const;
+
+ // This function only swaps the field. Should swap corresponding has_bit
+ // before or after using this function.
+ void SwapField(Message* message1,
+ Message* message2,
+ const FieldDescriptor* field) const;
+
+ void SwapOneofField(Message* message1,
+ Message* message2,
+ const OneofDescriptor* oneof_descriptor) const;
+
+ inline bool HasOneofField(const Message& message,
+ const FieldDescriptor* field) const;
+ inline void SetOneofCase(Message* message,
+ const FieldDescriptor* field) const;
+ inline void ClearOneofField(Message* message,
+ const FieldDescriptor* field) const;
+
+ template <typename Type>
+ inline const Type& GetField(const Message& message,
+ const FieldDescriptor* field) const;
+ template <typename Type>
+ inline void SetField(Message* message,
+ const FieldDescriptor* field, const Type& value) const;
+ template <typename Type>
+ inline Type* MutableField(Message* message,
+ const FieldDescriptor* field) const;
+ template <typename Type>
+ inline const Type& GetRepeatedField(const Message& message,
+ const FieldDescriptor* field,
+ int index) const;
+ template <typename Type>
+ inline const Type& GetRepeatedPtrField(const Message& message,
+ const FieldDescriptor* field,
+ int index) const;
+ template <typename Type>
+ inline void SetRepeatedField(Message* message,
+ const FieldDescriptor* field, int index,
+ Type value) const;
+ template <typename Type>
+ inline Type* MutableRepeatedField(Message* message,
+ const FieldDescriptor* field,
+ int index) const;
+ template <typename Type>
+ inline void AddField(Message* message,
+ const FieldDescriptor* field, const Type& value) const;
+ template <typename Type>
+ inline Type* AddField(Message* message,
+ const FieldDescriptor* field) const;
+
+ int GetExtensionNumberOrDie(const Descriptor* type) const;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(GeneratedMessageReflection);
+};
+
+// Returns the offset of the given field within the given aggregate type.
+// This is equivalent to the ANSI C offsetof() macro. However, according
+// to the C++ standard, offsetof() only works on POD types, and GCC
+// enforces this requirement with a warning. In practice, this rule is
+// unnecessarily strict; there is probably no compiler or platform on
+// which the offsets of the direct fields of a class are non-constant.
+// Fields inherited from superclasses *can* have non-constant offsets,
+// but that's not what this macro will be used for.
+//
+// Note that we calculate relative to the pointer value 16 here since if we
+// just use zero, GCC complains about dereferencing a NULL pointer. We
+// choose 16 rather than some other number just in case the compiler would
+// be confused by an unaligned pointer.
+#define GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(TYPE, FIELD) \
+ static_cast<int>( \
+ reinterpret_cast<const char*>( \
+ &reinterpret_cast<const TYPE*>(16)->FIELD) - \
+ reinterpret_cast<const char*>(16))
+
+#define PROTO2_GENERATED_DEFAULT_ONEOF_FIELD_OFFSET(ONEOF, FIELD) \
+ static_cast<int>( \
+ reinterpret_cast<const char*>(&(ONEOF->FIELD)) \
+ - reinterpret_cast<const char*>(ONEOF))
+
+// There are some places in proto2 where dynamic_cast would be useful as an
+// optimization. For example, take Message::MergeFrom(const Message& other).
+// For a given generated message FooMessage, we generate these two methods:
+// void MergeFrom(const FooMessage& other);
+// void MergeFrom(const Message& other);
+// The former method can be implemented directly in terms of FooMessage's
+// inline accessors, but the latter method must work with the reflection
+// interface. However, if the parameter to the latter method is actually of
+// type FooMessage, then we'd like to be able to just call the other method
+// as an optimization. So, we use dynamic_cast to check this.
+//
+// That said, dynamic_cast requires RTTI, which many people like to disable
+// for performance and code size reasons. When RTTI is not available, we
+// still need to produce correct results. So, in this case we have to fall
+// back to using reflection, which is what we would have done anyway if the
+// objects were not of the exact same class.
+//
+// dynamic_cast_if_available() implements this logic. If RTTI is
+// enabled, it does a dynamic_cast. If RTTI is disabled, it just returns
+// NULL.
+//
+// If you need to compile without RTTI, simply #define GOOGLE_PROTOBUF_NO_RTTI.
+// On MSVC, this should be detected automatically.
+template<typename To, typename From>
+inline To dynamic_cast_if_available(From from) {
+#if defined(GOOGLE_PROTOBUF_NO_RTTI) || (defined(_MSC_VER)&&!defined(_CPPRTTI))
+ return NULL;
+#else
+ return dynamic_cast<To>(from);
+#endif
+}
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_GENERATED_MESSAGE_REFLECTION_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/generated_message_util.cc b/toolkit/components/protobuf/src/google/protobuf/generated_message_util.cc
new file mode 100644
index 0000000000..0c20d81c8c
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/generated_message_util.cc
@@ -0,0 +1,65 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/generated_message_util.h>
+
+#include <limits>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+double Infinity() {
+ return std::numeric_limits<double>::infinity();
+}
+double NaN() {
+ return std::numeric_limits<double>::quiet_NaN();
+}
+
+const ::std::string* empty_string_;
+GOOGLE_PROTOBUF_DECLARE_ONCE(empty_string_once_init_);
+
+void DeleteEmptyString() {
+ delete empty_string_;
+}
+
+void InitEmptyString() {
+ empty_string_ = new string;
+ OnShutdown(&DeleteEmptyString);
+}
+
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/generated_message_util.h b/toolkit/components/protobuf/src/google/protobuf/generated_message_util.h
new file mode 100644
index 0000000000..678f92a7ee
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/generated_message_util.h
@@ -0,0 +1,113 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file contains miscellaneous helper code used by generated code --
+// including lite types -- but which should not be used directly by users.
+
+#ifndef GOOGLE_PROTOBUF_GENERATED_MESSAGE_UTIL_H__
+#define GOOGLE_PROTOBUF_GENERATED_MESSAGE_UTIL_H__
+
+#include <assert.h>
+#include <string>
+
+#include <google/protobuf/stubs/once.h>
+
+#include <google/protobuf/stubs/common.h>
+namespace google {
+
+namespace protobuf {
+namespace internal {
+
+
+// Annotation for the compiler to emit a deprecation message if a field marked
+// with option 'deprecated=true' is used in the code, or for other things in
+// generated code which are deprecated.
+//
+// For internal use in the pb.cc files, deprecation warnings are suppressed
+// there.
+#undef DEPRECATED_PROTOBUF_FIELD
+#define PROTOBUF_DEPRECATED
+
+
+// Constants for special floating point values.
+LIBPROTOBUF_EXPORT double Infinity();
+LIBPROTOBUF_EXPORT double NaN();
+
+// TODO(jieluo): Change to template. We have tried to use template,
+// but it causes net/rpc/python:rpcutil_test fail (the empty string will
+// init twice). It may related to swig. Change to template after we
+// found the solution.
+
+// Default empty string object. Don't use the pointer directly. Instead, call
+// GetEmptyString() to get the reference.
+LIBPROTOBUF_EXPORT extern const ::std::string* empty_string_;
+LIBPROTOBUF_EXPORT extern ProtobufOnceType empty_string_once_init_;
+LIBPROTOBUF_EXPORT void InitEmptyString();
+
+
+LIBPROTOBUF_EXPORT inline const ::std::string& GetEmptyStringAlreadyInited() {
+ assert(empty_string_ != NULL);
+ return *empty_string_;
+}
+LIBPROTOBUF_EXPORT inline const ::std::string& GetEmptyString() {
+ ::google::protobuf::GoogleOnceInit(&empty_string_once_init_, &InitEmptyString);
+ return GetEmptyStringAlreadyInited();
+}
+
+// Defined in generated_message_reflection.cc -- not actually part of the lite
+// library.
+//
+// TODO(jasonh): The various callers get this declaration from a variety of
+// places: probably in most cases repeated_field.h. Clean these up so they all
+// get the declaration from this file.
+LIBPROTOBUF_EXPORT int StringSpaceUsedExcludingSelf(const string& str);
+
+
+// True if IsInitialized() is true for all elements of t. Type is expected
+// to be a RepeatedPtrField<some message type>. It's useful to have this
+// helper here to keep the protobuf compiler from ever having to emit loops in
+// IsInitialized() methods. We want the C++ compiler to inline this or not
+// as it sees fit.
+template <class Type> bool AllAreInitialized(const Type& t) {
+ for (int i = t.size(); --i >= 0; ) {
+ if (!t.Get(i).IsInitialized()) return false;
+ }
+ return true;
+}
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_GENERATED_MESSAGE_UTIL_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/coded_stream.cc b/toolkit/components/protobuf/src/google/protobuf/io/coded_stream.cc
new file mode 100644
index 0000000000..53449755c1
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/coded_stream.cc
@@ -0,0 +1,914 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This implementation is heavily optimized to make reads and writes
+// of small values (especially varints) as fast as possible. In
+// particular, we optimize for the common case that a read or a write
+// will not cross the end of the buffer, since we can avoid a lot
+// of branching in this case.
+
+#include <google/protobuf/io/coded_stream_inl.h>
+#include <algorithm>
+#include <limits.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+namespace {
+
+static const int kMaxVarintBytes = 10;
+static const int kMaxVarint32Bytes = 5;
+
+
+inline bool NextNonEmpty(ZeroCopyInputStream* input,
+ const void** data, int* size) {
+ bool success;
+ do {
+ success = input->Next(data, size);
+ } while (success && *size == 0);
+ return success;
+}
+
+} // namespace
+
+// CodedInputStream ==================================================
+
+CodedInputStream::~CodedInputStream() {
+ if (input_ != NULL) {
+ BackUpInputToCurrentPosition();
+ }
+
+ if (total_bytes_warning_threshold_ == -2) {
+ GOOGLE_LOG(WARNING) << "The total number of bytes read was " << total_bytes_read_;
+ }
+}
+
+// Static.
+int CodedInputStream::default_recursion_limit_ = 100;
+
+
+void CodedOutputStream::EnableAliasing(bool enabled) {
+ aliasing_enabled_ = enabled && output_->AllowsAliasing();
+}
+
+void CodedInputStream::BackUpInputToCurrentPosition() {
+ int backup_bytes = BufferSize() + buffer_size_after_limit_ + overflow_bytes_;
+ if (backup_bytes > 0) {
+ input_->BackUp(backup_bytes);
+
+ // total_bytes_read_ doesn't include overflow_bytes_.
+ total_bytes_read_ -= BufferSize() + buffer_size_after_limit_;
+ buffer_end_ = buffer_;
+ buffer_size_after_limit_ = 0;
+ overflow_bytes_ = 0;
+ }
+}
+
+inline void CodedInputStream::RecomputeBufferLimits() {
+ buffer_end_ += buffer_size_after_limit_;
+ int closest_limit = min(current_limit_, total_bytes_limit_);
+ if (closest_limit < total_bytes_read_) {
+ // The limit position is in the current buffer. We must adjust
+ // the buffer size accordingly.
+ buffer_size_after_limit_ = total_bytes_read_ - closest_limit;
+ buffer_end_ -= buffer_size_after_limit_;
+ } else {
+ buffer_size_after_limit_ = 0;
+ }
+}
+
+CodedInputStream::Limit CodedInputStream::PushLimit(int byte_limit) {
+ // Current position relative to the beginning of the stream.
+ int current_position = CurrentPosition();
+
+ Limit old_limit = current_limit_;
+
+ // security: byte_limit is possibly evil, so check for negative values
+ // and overflow.
+ if (byte_limit >= 0 &&
+ byte_limit <= INT_MAX - current_position) {
+ current_limit_ = current_position + byte_limit;
+ } else {
+ // Negative or overflow.
+ current_limit_ = INT_MAX;
+ }
+
+ // We need to enforce all limits, not just the new one, so if the previous
+ // limit was before the new requested limit, we continue to enforce the
+ // previous limit.
+ current_limit_ = min(current_limit_, old_limit);
+
+ RecomputeBufferLimits();
+ return old_limit;
+}
+
+void CodedInputStream::PopLimit(Limit limit) {
+ // The limit passed in is actually the *old* limit, which we returned from
+ // PushLimit().
+ current_limit_ = limit;
+ RecomputeBufferLimits();
+
+ // We may no longer be at a legitimate message end. ReadTag() needs to be
+ // called again to find out.
+ legitimate_message_end_ = false;
+}
+
+int CodedInputStream::BytesUntilLimit() const {
+ if (current_limit_ == INT_MAX) return -1;
+ int current_position = CurrentPosition();
+
+ return current_limit_ - current_position;
+}
+
+void CodedInputStream::SetTotalBytesLimit(
+ int total_bytes_limit, int warning_threshold) {
+ // Make sure the limit isn't already past, since this could confuse other
+ // code.
+ int current_position = CurrentPosition();
+ total_bytes_limit_ = max(current_position, total_bytes_limit);
+ if (warning_threshold >= 0) {
+ total_bytes_warning_threshold_ = warning_threshold;
+ } else {
+ // warning_threshold is negative
+ total_bytes_warning_threshold_ = -1;
+ }
+ RecomputeBufferLimits();
+}
+
+int CodedInputStream::BytesUntilTotalBytesLimit() const {
+ if (total_bytes_limit_ == INT_MAX) return -1;
+ return total_bytes_limit_ - CurrentPosition();
+}
+
+void CodedInputStream::PrintTotalBytesLimitError() {
+ GOOGLE_LOG(ERROR) << "A protocol message was rejected because it was too "
+ "big (more than " << total_bytes_limit_
+ << " bytes). To increase the limit (or to disable these "
+ "warnings), see CodedInputStream::SetTotalBytesLimit() "
+ "in google/protobuf/io/coded_stream.h.";
+}
+
+bool CodedInputStream::Skip(int count) {
+ if (count < 0) return false; // security: count is often user-supplied
+
+ const int original_buffer_size = BufferSize();
+
+ if (count <= original_buffer_size) {
+ // Just skipping within the current buffer. Easy.
+ Advance(count);
+ return true;
+ }
+
+ if (buffer_size_after_limit_ > 0) {
+ // We hit a limit inside this buffer. Advance to the limit and fail.
+ Advance(original_buffer_size);
+ return false;
+ }
+
+ count -= original_buffer_size;
+ buffer_ = NULL;
+ buffer_end_ = buffer_;
+
+ // Make sure this skip doesn't try to skip past the current limit.
+ int closest_limit = min(current_limit_, total_bytes_limit_);
+ int bytes_until_limit = closest_limit - total_bytes_read_;
+ if (bytes_until_limit < count) {
+ // We hit the limit. Skip up to it then fail.
+ if (bytes_until_limit > 0) {
+ total_bytes_read_ = closest_limit;
+ input_->Skip(bytes_until_limit);
+ }
+ return false;
+ }
+
+ total_bytes_read_ += count;
+ return input_->Skip(count);
+}
+
+bool CodedInputStream::GetDirectBufferPointer(const void** data, int* size) {
+ if (BufferSize() == 0 && !Refresh()) return false;
+
+ *data = buffer_;
+ *size = BufferSize();
+ return true;
+}
+
+bool CodedInputStream::ReadRaw(void* buffer, int size) {
+ int current_buffer_size;
+ while ((current_buffer_size = BufferSize()) < size) {
+ // Reading past end of buffer. Copy what we have, then refresh.
+ memcpy(buffer, buffer_, current_buffer_size);
+ buffer = reinterpret_cast<uint8*>(buffer) + current_buffer_size;
+ size -= current_buffer_size;
+ Advance(current_buffer_size);
+ if (!Refresh()) return false;
+ }
+
+ memcpy(buffer, buffer_, size);
+ Advance(size);
+
+ return true;
+}
+
+bool CodedInputStream::ReadString(string* buffer, int size) {
+ if (size < 0) return false; // security: size is often user-supplied
+ return InternalReadStringInline(buffer, size);
+}
+
+bool CodedInputStream::ReadStringFallback(string* buffer, int size) {
+ if (!buffer->empty()) {
+ buffer->clear();
+ }
+
+ int closest_limit = min(current_limit_, total_bytes_limit_);
+ if (closest_limit != INT_MAX) {
+ int bytes_to_limit = closest_limit - CurrentPosition();
+ if (bytes_to_limit > 0 && size > 0 && size <= bytes_to_limit) {
+ buffer->reserve(size);
+ }
+ }
+
+ int current_buffer_size;
+ while ((current_buffer_size = BufferSize()) < size) {
+ // Some STL implementations "helpfully" crash on buffer->append(NULL, 0).
+ if (current_buffer_size != 0) {
+ // Note: string1.append(string2) is O(string2.size()) (as opposed to
+ // O(string1.size() + string2.size()), which would be bad).
+ buffer->append(reinterpret_cast<const char*>(buffer_),
+ current_buffer_size);
+ }
+ size -= current_buffer_size;
+ Advance(current_buffer_size);
+ if (!Refresh()) return false;
+ }
+
+ buffer->append(reinterpret_cast<const char*>(buffer_), size);
+ Advance(size);
+
+ return true;
+}
+
+
+bool CodedInputStream::ReadLittleEndian32Fallback(uint32* value) {
+ uint8 bytes[sizeof(*value)];
+
+ const uint8* ptr;
+ if (BufferSize() >= sizeof(*value)) {
+ // Fast path: Enough bytes in the buffer to read directly.
+ ptr = buffer_;
+ Advance(sizeof(*value));
+ } else {
+ // Slow path: Had to read past the end of the buffer.
+ if (!ReadRaw(bytes, sizeof(*value))) return false;
+ ptr = bytes;
+ }
+ ReadLittleEndian32FromArray(ptr, value);
+ return true;
+}
+
+bool CodedInputStream::ReadLittleEndian64Fallback(uint64* value) {
+ uint8 bytes[sizeof(*value)];
+
+ const uint8* ptr;
+ if (BufferSize() >= sizeof(*value)) {
+ // Fast path: Enough bytes in the buffer to read directly.
+ ptr = buffer_;
+ Advance(sizeof(*value));
+ } else {
+ // Slow path: Had to read past the end of the buffer.
+ if (!ReadRaw(bytes, sizeof(*value))) return false;
+ ptr = bytes;
+ }
+ ReadLittleEndian64FromArray(ptr, value);
+ return true;
+}
+
+namespace {
+
+inline const uint8* ReadVarint32FromArray(
+ const uint8* buffer, uint32* value) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+inline const uint8* ReadVarint32FromArray(const uint8* buffer, uint32* value) {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this read won't cross the end, so we can skip the checks.
+ const uint8* ptr = buffer;
+ uint32 b;
+ uint32 result;
+
+ b = *(ptr++); result = b ; if (!(b & 0x80)) goto done;
+ result -= 0x80;
+ b = *(ptr++); result += b << 7; if (!(b & 0x80)) goto done;
+ result -= 0x80 << 7;
+ b = *(ptr++); result += b << 14; if (!(b & 0x80)) goto done;
+ result -= 0x80 << 14;
+ b = *(ptr++); result += b << 21; if (!(b & 0x80)) goto done;
+ result -= 0x80 << 21;
+ b = *(ptr++); result += b << 28; if (!(b & 0x80)) goto done;
+ // "result -= 0x80 << 28" is irrevelant.
+
+ // If the input is larger than 32 bits, we still need to read it all
+ // and discard the high-order bits.
+ for (int i = 0; i < kMaxVarintBytes - kMaxVarint32Bytes; i++) {
+ b = *(ptr++); if (!(b & 0x80)) goto done;
+ }
+
+ // We have overrun the maximum size of a varint (10 bytes). Assume
+ // the data is corrupt.
+ return NULL;
+
+ done:
+ *value = result;
+ return ptr;
+}
+
+} // namespace
+
+bool CodedInputStream::ReadVarint32Slow(uint32* value) {
+ uint64 result;
+ // Directly invoke ReadVarint64Fallback, since we already tried to optimize
+ // for one-byte varints.
+ if (!ReadVarint64Fallback(&result)) return false;
+ *value = (uint32)result;
+ return true;
+}
+
+bool CodedInputStream::ReadVarint32Fallback(uint32* value) {
+ if (BufferSize() >= kMaxVarintBytes ||
+ // Optimization: We're also safe if the buffer is non-empty and it ends
+ // with a byte that would terminate a varint.
+ (buffer_end_ > buffer_ && !(buffer_end_[-1] & 0x80))) {
+ const uint8* end = ReadVarint32FromArray(buffer_, value);
+ if (end == NULL) return false;
+ buffer_ = end;
+ return true;
+ } else {
+ // Really slow case: we will incur the cost of an extra function call here,
+ // but moving this out of line reduces the size of this function, which
+ // improves the common case. In micro benchmarks, this is worth about 10-15%
+ return ReadVarint32Slow(value);
+ }
+}
+
+uint32 CodedInputStream::ReadTagSlow() {
+ if (buffer_ == buffer_end_) {
+ // Call refresh.
+ if (!Refresh()) {
+ // Refresh failed. Make sure that it failed due to EOF, not because
+ // we hit total_bytes_limit_, which, unlike normal limits, is not a
+ // valid place to end a message.
+ int current_position = total_bytes_read_ - buffer_size_after_limit_;
+ if (current_position >= total_bytes_limit_) {
+ // Hit total_bytes_limit_. But if we also hit the normal limit,
+ // we're still OK.
+ legitimate_message_end_ = current_limit_ == total_bytes_limit_;
+ } else {
+ legitimate_message_end_ = true;
+ }
+ return 0;
+ }
+ }
+
+ // For the slow path, just do a 64-bit read. Try to optimize for one-byte tags
+ // again, since we have now refreshed the buffer.
+ uint64 result = 0;
+ if (!ReadVarint64(&result)) return 0;
+ return static_cast<uint32>(result);
+}
+
+uint32 CodedInputStream::ReadTagFallback() {
+ const int buf_size = BufferSize();
+ if (buf_size >= kMaxVarintBytes ||
+ // Optimization: We're also safe if the buffer is non-empty and it ends
+ // with a byte that would terminate a varint.
+ (buf_size > 0 && !(buffer_end_[-1] & 0x80))) {
+ uint32 tag;
+ const uint8* end = ReadVarint32FromArray(buffer_, &tag);
+ if (end == NULL) {
+ return 0;
+ }
+ buffer_ = end;
+ return tag;
+ } else {
+ // We are commonly at a limit when attempting to read tags. Try to quickly
+ // detect this case without making another function call.
+ if ((buf_size == 0) &&
+ ((buffer_size_after_limit_ > 0) ||
+ (total_bytes_read_ == current_limit_)) &&
+ // Make sure that the limit we hit is not total_bytes_limit_, since
+ // in that case we still need to call Refresh() so that it prints an
+ // error.
+ total_bytes_read_ - buffer_size_after_limit_ < total_bytes_limit_) {
+ // We hit a byte limit.
+ legitimate_message_end_ = true;
+ return 0;
+ }
+ return ReadTagSlow();
+ }
+}
+
+bool CodedInputStream::ReadVarint64Slow(uint64* value) {
+ // Slow path: This read might cross the end of the buffer, so we
+ // need to check and refresh the buffer if and when it does.
+
+ uint64 result = 0;
+ int count = 0;
+ uint32 b;
+
+ do {
+ if (count == kMaxVarintBytes) return false;
+ while (buffer_ == buffer_end_) {
+ if (!Refresh()) return false;
+ }
+ b = *buffer_;
+ result |= static_cast<uint64>(b & 0x7F) << (7 * count);
+ Advance(1);
+ ++count;
+ } while (b & 0x80);
+
+ *value = result;
+ return true;
+}
+
+bool CodedInputStream::ReadVarint64Fallback(uint64* value) {
+ if (BufferSize() >= kMaxVarintBytes ||
+ // Optimization: We're also safe if the buffer is non-empty and it ends
+ // with a byte that would terminate a varint.
+ (buffer_end_ > buffer_ && !(buffer_end_[-1] & 0x80))) {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this read won't cross the end, so we can skip the checks.
+
+ const uint8* ptr = buffer_;
+ uint32 b;
+
+ // Splitting into 32-bit pieces gives better performance on 32-bit
+ // processors.
+ uint32 part0 = 0, part1 = 0, part2 = 0;
+
+ b = *(ptr++); part0 = b ; if (!(b & 0x80)) goto done;
+ part0 -= 0x80;
+ b = *(ptr++); part0 += b << 7; if (!(b & 0x80)) goto done;
+ part0 -= 0x80 << 7;
+ b = *(ptr++); part0 += b << 14; if (!(b & 0x80)) goto done;
+ part0 -= 0x80 << 14;
+ b = *(ptr++); part0 += b << 21; if (!(b & 0x80)) goto done;
+ part0 -= 0x80 << 21;
+ b = *(ptr++); part1 = b ; if (!(b & 0x80)) goto done;
+ part1 -= 0x80;
+ b = *(ptr++); part1 += b << 7; if (!(b & 0x80)) goto done;
+ part1 -= 0x80 << 7;
+ b = *(ptr++); part1 += b << 14; if (!(b & 0x80)) goto done;
+ part1 -= 0x80 << 14;
+ b = *(ptr++); part1 += b << 21; if (!(b & 0x80)) goto done;
+ part1 -= 0x80 << 21;
+ b = *(ptr++); part2 = b ; if (!(b & 0x80)) goto done;
+ part2 -= 0x80;
+ b = *(ptr++); part2 += b << 7; if (!(b & 0x80)) goto done;
+ // "part2 -= 0x80 << 7" is irrelevant because (0x80 << 7) << 56 is 0.
+
+ // We have overrun the maximum size of a varint (10 bytes). The data
+ // must be corrupt.
+ return false;
+
+ done:
+ Advance(ptr - buffer_);
+ *value = (static_cast<uint64>(part0) ) |
+ (static_cast<uint64>(part1) << 28) |
+ (static_cast<uint64>(part2) << 56);
+ return true;
+ } else {
+ return ReadVarint64Slow(value);
+ }
+}
+
+bool CodedInputStream::Refresh() {
+ GOOGLE_DCHECK_EQ(0, BufferSize());
+
+ if (buffer_size_after_limit_ > 0 || overflow_bytes_ > 0 ||
+ total_bytes_read_ == current_limit_) {
+ // We've hit a limit. Stop.
+ int current_position = total_bytes_read_ - buffer_size_after_limit_;
+
+ if (current_position >= total_bytes_limit_ &&
+ total_bytes_limit_ != current_limit_) {
+ // Hit total_bytes_limit_.
+ PrintTotalBytesLimitError();
+ }
+
+ return false;
+ }
+
+ if (total_bytes_warning_threshold_ >= 0 &&
+ total_bytes_read_ >= total_bytes_warning_threshold_) {
+ GOOGLE_LOG(WARNING) << "Reading dangerously large protocol message. If the "
+ "message turns out to be larger than "
+ << total_bytes_limit_ << " bytes, parsing will be halted "
+ "for security reasons. To increase the limit (or to "
+ "disable these warnings), see "
+ "CodedInputStream::SetTotalBytesLimit() in "
+ "google/protobuf/io/coded_stream.h.";
+
+ // Don't warn again for this stream, and print total size at the end.
+ total_bytes_warning_threshold_ = -2;
+ }
+
+ const void* void_buffer;
+ int buffer_size;
+ if (NextNonEmpty(input_, &void_buffer, &buffer_size)) {
+ buffer_ = reinterpret_cast<const uint8*>(void_buffer);
+ buffer_end_ = buffer_ + buffer_size;
+ GOOGLE_CHECK_GE(buffer_size, 0);
+
+ if (total_bytes_read_ <= INT_MAX - buffer_size) {
+ total_bytes_read_ += buffer_size;
+ } else {
+ // Overflow. Reset buffer_end_ to not include the bytes beyond INT_MAX.
+ // We can't get that far anyway, because total_bytes_limit_ is guaranteed
+ // to be less than it. We need to keep track of the number of bytes
+ // we discarded, though, so that we can call input_->BackUp() to back
+ // up over them on destruction.
+
+ // The following line is equivalent to:
+ // overflow_bytes_ = total_bytes_read_ + buffer_size - INT_MAX;
+ // except that it avoids overflows. Signed integer overflow has
+ // undefined results according to the C standard.
+ overflow_bytes_ = total_bytes_read_ - (INT_MAX - buffer_size);
+ buffer_end_ -= overflow_bytes_;
+ total_bytes_read_ = INT_MAX;
+ }
+
+ RecomputeBufferLimits();
+ return true;
+ } else {
+ buffer_ = NULL;
+ buffer_end_ = NULL;
+ return false;
+ }
+}
+
+// CodedOutputStream =================================================
+
+CodedOutputStream::CodedOutputStream(ZeroCopyOutputStream* output)
+ : output_(output),
+ buffer_(NULL),
+ buffer_size_(0),
+ total_bytes_(0),
+ had_error_(false),
+ aliasing_enabled_(false) {
+ // Eagerly Refresh() so buffer space is immediately available.
+ Refresh();
+ // The Refresh() may have failed. If the client doesn't write any data,
+ // though, don't consider this an error. If the client does write data, then
+ // another Refresh() will be attempted and it will set the error once again.
+ had_error_ = false;
+}
+
+CodedOutputStream::~CodedOutputStream() {
+ if (buffer_size_ > 0) {
+ output_->BackUp(buffer_size_);
+ }
+}
+
+bool CodedOutputStream::Skip(int count) {
+ if (count < 0) return false;
+
+ while (count > buffer_size_) {
+ count -= buffer_size_;
+ if (!Refresh()) return false;
+ }
+
+ Advance(count);
+ return true;
+}
+
+bool CodedOutputStream::GetDirectBufferPointer(void** data, int* size) {
+ if (buffer_size_ == 0 && !Refresh()) return false;
+
+ *data = buffer_;
+ *size = buffer_size_;
+ return true;
+}
+
+void CodedOutputStream::WriteRaw(const void* data, int size) {
+ while (buffer_size_ < size) {
+ memcpy(buffer_, data, buffer_size_);
+ size -= buffer_size_;
+ data = reinterpret_cast<const uint8*>(data) + buffer_size_;
+ if (!Refresh()) return;
+ }
+
+ memcpy(buffer_, data, size);
+ Advance(size);
+}
+
+uint8* CodedOutputStream::WriteRawToArray(
+ const void* data, int size, uint8* target) {
+ memcpy(target, data, size);
+ return target + size;
+}
+
+
+void CodedOutputStream::WriteAliasedRaw(const void* data, int size) {
+ if (size < buffer_size_
+ ) {
+ WriteRaw(data, size);
+ } else {
+ if (buffer_size_ > 0) {
+ output_->BackUp(buffer_size_);
+ total_bytes_ -= buffer_size_;
+ buffer_ = NULL;
+ buffer_size_ = 0;
+ }
+
+ total_bytes_ += size;
+ had_error_ |= !output_->WriteAliasedRaw(data, size);
+ }
+}
+
+void CodedOutputStream::WriteLittleEndian32(uint32 value) {
+ uint8 bytes[sizeof(value)];
+
+ bool use_fast = buffer_size_ >= sizeof(value);
+ uint8* ptr = use_fast ? buffer_ : bytes;
+
+ WriteLittleEndian32ToArray(value, ptr);
+
+ if (use_fast) {
+ Advance(sizeof(value));
+ } else {
+ WriteRaw(bytes, sizeof(value));
+ }
+}
+
+void CodedOutputStream::WriteLittleEndian64(uint64 value) {
+ uint8 bytes[sizeof(value)];
+
+ bool use_fast = buffer_size_ >= sizeof(value);
+ uint8* ptr = use_fast ? buffer_ : bytes;
+
+ WriteLittleEndian64ToArray(value, ptr);
+
+ if (use_fast) {
+ Advance(sizeof(value));
+ } else {
+ WriteRaw(bytes, sizeof(value));
+ }
+}
+
+inline uint8* CodedOutputStream::WriteVarint32FallbackToArrayInline(
+ uint32 value, uint8* target) {
+ target[0] = static_cast<uint8>(value | 0x80);
+ if (value >= (1 << 7)) {
+ target[1] = static_cast<uint8>((value >> 7) | 0x80);
+ if (value >= (1 << 14)) {
+ target[2] = static_cast<uint8>((value >> 14) | 0x80);
+ if (value >= (1 << 21)) {
+ target[3] = static_cast<uint8>((value >> 21) | 0x80);
+ if (value >= (1 << 28)) {
+ target[4] = static_cast<uint8>(value >> 28);
+ return target + 5;
+ } else {
+ target[3] &= 0x7F;
+ return target + 4;
+ }
+ } else {
+ target[2] &= 0x7F;
+ return target + 3;
+ }
+ } else {
+ target[1] &= 0x7F;
+ return target + 2;
+ }
+ } else {
+ target[0] &= 0x7F;
+ return target + 1;
+ }
+}
+
+void CodedOutputStream::WriteVarint32(uint32 value) {
+ if (buffer_size_ >= kMaxVarint32Bytes) {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this write won't cross the end, so we can skip the checks.
+ uint8* target = buffer_;
+ uint8* end = WriteVarint32FallbackToArrayInline(value, target);
+ int size = end - target;
+ Advance(size);
+ } else {
+ // Slow path: This write might cross the end of the buffer, so we
+ // compose the bytes first then use WriteRaw().
+ uint8 bytes[kMaxVarint32Bytes];
+ int size = 0;
+ while (value > 0x7F) {
+ bytes[size++] = (static_cast<uint8>(value) & 0x7F) | 0x80;
+ value >>= 7;
+ }
+ bytes[size++] = static_cast<uint8>(value) & 0x7F;
+ WriteRaw(bytes, size);
+ }
+}
+
+uint8* CodedOutputStream::WriteVarint32FallbackToArray(
+ uint32 value, uint8* target) {
+ return WriteVarint32FallbackToArrayInline(value, target);
+}
+
+inline uint8* CodedOutputStream::WriteVarint64ToArrayInline(
+ uint64 value, uint8* target) {
+ // Splitting into 32-bit pieces gives better performance on 32-bit
+ // processors.
+ uint32 part0 = static_cast<uint32>(value );
+ uint32 part1 = static_cast<uint32>(value >> 28);
+ uint32 part2 = static_cast<uint32>(value >> 56);
+
+ int size;
+
+ // Here we can't really optimize for small numbers, since the value is
+ // split into three parts. Cheking for numbers < 128, for instance,
+ // would require three comparisons, since you'd have to make sure part1
+ // and part2 are zero. However, if the caller is using 64-bit integers,
+ // it is likely that they expect the numbers to often be very large, so
+ // we probably don't want to optimize for small numbers anyway. Thus,
+ // we end up with a hardcoded binary search tree...
+ if (part2 == 0) {
+ if (part1 == 0) {
+ if (part0 < (1 << 14)) {
+ if (part0 < (1 << 7)) {
+ size = 1; goto size1;
+ } else {
+ size = 2; goto size2;
+ }
+ } else {
+ if (part0 < (1 << 21)) {
+ size = 3; goto size3;
+ } else {
+ size = 4; goto size4;
+ }
+ }
+ } else {
+ if (part1 < (1 << 14)) {
+ if (part1 < (1 << 7)) {
+ size = 5; goto size5;
+ } else {
+ size = 6; goto size6;
+ }
+ } else {
+ if (part1 < (1 << 21)) {
+ size = 7; goto size7;
+ } else {
+ size = 8; goto size8;
+ }
+ }
+ }
+ } else {
+ if (part2 < (1 << 7)) {
+ size = 9; goto size9;
+ } else {
+ size = 10; goto size10;
+ }
+ }
+
+ GOOGLE_LOG(FATAL) << "Can't get here.";
+
+ size10: target[9] = static_cast<uint8>((part2 >> 7) | 0x80);
+ size9 : target[8] = static_cast<uint8>((part2 ) | 0x80);
+ size8 : target[7] = static_cast<uint8>((part1 >> 21) | 0x80);
+ size7 : target[6] = static_cast<uint8>((part1 >> 14) | 0x80);
+ size6 : target[5] = static_cast<uint8>((part1 >> 7) | 0x80);
+ size5 : target[4] = static_cast<uint8>((part1 ) | 0x80);
+ size4 : target[3] = static_cast<uint8>((part0 >> 21) | 0x80);
+ size3 : target[2] = static_cast<uint8>((part0 >> 14) | 0x80);
+ size2 : target[1] = static_cast<uint8>((part0 >> 7) | 0x80);
+ size1 : target[0] = static_cast<uint8>((part0 ) | 0x80);
+
+ target[size-1] &= 0x7F;
+ return target + size;
+}
+
+void CodedOutputStream::WriteVarint64(uint64 value) {
+ if (buffer_size_ >= kMaxVarintBytes) {
+ // Fast path: We have enough bytes left in the buffer to guarantee that
+ // this write won't cross the end, so we can skip the checks.
+ uint8* target = buffer_;
+
+ uint8* end = WriteVarint64ToArrayInline(value, target);
+ int size = end - target;
+ Advance(size);
+ } else {
+ // Slow path: This write might cross the end of the buffer, so we
+ // compose the bytes first then use WriteRaw().
+ uint8 bytes[kMaxVarintBytes];
+ int size = 0;
+ while (value > 0x7F) {
+ bytes[size++] = (static_cast<uint8>(value) & 0x7F) | 0x80;
+ value >>= 7;
+ }
+ bytes[size++] = static_cast<uint8>(value) & 0x7F;
+ WriteRaw(bytes, size);
+ }
+}
+
+uint8* CodedOutputStream::WriteVarint64ToArray(
+ uint64 value, uint8* target) {
+ return WriteVarint64ToArrayInline(value, target);
+}
+
+bool CodedOutputStream::Refresh() {
+ void* void_buffer;
+ if (output_->Next(&void_buffer, &buffer_size_)) {
+ buffer_ = reinterpret_cast<uint8*>(void_buffer);
+ total_bytes_ += buffer_size_;
+ return true;
+ } else {
+ buffer_ = NULL;
+ buffer_size_ = 0;
+ had_error_ = true;
+ return false;
+ }
+}
+
+int CodedOutputStream::VarintSize32Fallback(uint32 value) {
+ if (value < (1 << 7)) {
+ return 1;
+ } else if (value < (1 << 14)) {
+ return 2;
+ } else if (value < (1 << 21)) {
+ return 3;
+ } else if (value < (1 << 28)) {
+ return 4;
+ } else {
+ return 5;
+ }
+}
+
+int CodedOutputStream::VarintSize64(uint64 value) {
+ if (value < (1ull << 35)) {
+ if (value < (1ull << 7)) {
+ return 1;
+ } else if (value < (1ull << 14)) {
+ return 2;
+ } else if (value < (1ull << 21)) {
+ return 3;
+ } else if (value < (1ull << 28)) {
+ return 4;
+ } else {
+ return 5;
+ }
+ } else {
+ if (value < (1ull << 42)) {
+ return 6;
+ } else if (value < (1ull << 49)) {
+ return 7;
+ } else if (value < (1ull << 56)) {
+ return 8;
+ } else if (value < (1ull << 63)) {
+ return 9;
+ } else {
+ return 10;
+ }
+ }
+}
+
+uint8* CodedOutputStream::WriteStringWithSizeToArray(const string& str,
+ uint8* target) {
+ GOOGLE_DCHECK_LE(str.size(), kuint32max);
+ target = WriteVarint32ToArray(str.size(), target);
+ return WriteStringToArray(str, target);
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/coded_stream.h b/toolkit/components/protobuf/src/google/protobuf/io/coded_stream.h
new file mode 100644
index 0000000000..81fabb1d84
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/coded_stream.h
@@ -0,0 +1,1220 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file contains the CodedInputStream and CodedOutputStream classes,
+// which wrap a ZeroCopyInputStream or ZeroCopyOutputStream, respectively,
+// and allow you to read or write individual pieces of data in various
+// formats. In particular, these implement the varint encoding for
+// integers, a simple variable-length encoding in which smaller numbers
+// take fewer bytes.
+//
+// Typically these classes will only be used internally by the protocol
+// buffer library in order to encode and decode protocol buffers. Clients
+// of the library only need to know about this class if they wish to write
+// custom message parsing or serialization procedures.
+//
+// CodedOutputStream example:
+// // Write some data to "myfile". First we write a 4-byte "magic number"
+// // to identify the file type, then write a length-delimited string. The
+// // string is composed of a varint giving the length followed by the raw
+// // bytes.
+// int fd = open("myfile", O_WRONLY);
+// ZeroCopyOutputStream* raw_output = new FileOutputStream(fd);
+// CodedOutputStream* coded_output = new CodedOutputStream(raw_output);
+//
+// int magic_number = 1234;
+// char text[] = "Hello world!";
+// coded_output->WriteLittleEndian32(magic_number);
+// coded_output->WriteVarint32(strlen(text));
+// coded_output->WriteRaw(text, strlen(text));
+//
+// delete coded_output;
+// delete raw_output;
+// close(fd);
+//
+// CodedInputStream example:
+// // Read a file created by the above code.
+// int fd = open("myfile", O_RDONLY);
+// ZeroCopyInputStream* raw_input = new FileInputStream(fd);
+// CodedInputStream coded_input = new CodedInputStream(raw_input);
+//
+// coded_input->ReadLittleEndian32(&magic_number);
+// if (magic_number != 1234) {
+// cerr << "File not in expected format." << endl;
+// return;
+// }
+//
+// uint32 size;
+// coded_input->ReadVarint32(&size);
+//
+// char* text = new char[size + 1];
+// coded_input->ReadRaw(buffer, size);
+// text[size] = '\0';
+//
+// delete coded_input;
+// delete raw_input;
+// close(fd);
+//
+// cout << "Text is: " << text << endl;
+// delete [] text;
+//
+// For those who are interested, varint encoding is defined as follows:
+//
+// The encoding operates on unsigned integers of up to 64 bits in length.
+// Each byte of the encoded value has the format:
+// * bits 0-6: Seven bits of the number being encoded.
+// * bit 7: Zero if this is the last byte in the encoding (in which
+// case all remaining bits of the number are zero) or 1 if
+// more bytes follow.
+// The first byte contains the least-significant 7 bits of the number, the
+// second byte (if present) contains the next-least-significant 7 bits,
+// and so on. So, the binary number 1011000101011 would be encoded in two
+// bytes as "10101011 00101100".
+//
+// In theory, varint could be used to encode integers of any length.
+// However, for practicality we set a limit at 64 bits. The maximum encoded
+// length of a number is thus 10 bytes.
+
+#ifndef GOOGLE_PROTOBUF_IO_CODED_STREAM_H__
+#define GOOGLE_PROTOBUF_IO_CODED_STREAM_H__
+
+#include <string>
+#ifdef _MSC_VER
+ #if defined(_M_IX86) && \
+ !defined(PROTOBUF_DISABLE_LITTLE_ENDIAN_OPT_FOR_TEST)
+ #define PROTOBUF_LITTLE_ENDIAN 1
+ #endif
+ #if _MSC_VER >= 1300
+ // If MSVC has "/RTCc" set, it will complain about truncating casts at
+ // runtime. This file contains some intentional truncating casts.
+ #pragma runtime_checks("c", off)
+ #endif
+#else
+ #include <sys/param.h> // __BYTE_ORDER
+ #if defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN && \
+ !defined(PROTOBUF_DISABLE_LITTLE_ENDIAN_OPT_FOR_TEST)
+ #define PROTOBUF_LITTLE_ENDIAN 1
+ #endif
+#endif
+#include <google/protobuf/stubs/common.h>
+
+
+namespace google {
+namespace protobuf {
+
+class DescriptorPool;
+class MessageFactory;
+
+namespace io {
+
+// Defined in this file.
+class CodedInputStream;
+class CodedOutputStream;
+
+// Defined in other files.
+class ZeroCopyInputStream; // zero_copy_stream.h
+class ZeroCopyOutputStream; // zero_copy_stream.h
+
+// Class which reads and decodes binary data which is composed of varint-
+// encoded integers and fixed-width pieces. Wraps a ZeroCopyInputStream.
+// Most users will not need to deal with CodedInputStream.
+//
+// Most methods of CodedInputStream that return a bool return false if an
+// underlying I/O error occurs or if the data is malformed. Once such a
+// failure occurs, the CodedInputStream is broken and is no longer useful.
+class LIBPROTOBUF_EXPORT CodedInputStream {
+ public:
+ // Create a CodedInputStream that reads from the given ZeroCopyInputStream.
+ explicit CodedInputStream(ZeroCopyInputStream* input);
+
+ // Create a CodedInputStream that reads from the given flat array. This is
+ // faster than using an ArrayInputStream. PushLimit(size) is implied by
+ // this constructor.
+ explicit CodedInputStream(const uint8* buffer, int size);
+
+ // Destroy the CodedInputStream and position the underlying
+ // ZeroCopyInputStream at the first unread byte. If an error occurred while
+ // reading (causing a method to return false), then the exact position of
+ // the input stream may be anywhere between the last value that was read
+ // successfully and the stream's byte limit.
+ ~CodedInputStream();
+
+ // Return true if this CodedInputStream reads from a flat array instead of
+ // a ZeroCopyInputStream.
+ inline bool IsFlat() const;
+
+ // Skips a number of bytes. Returns false if an underlying read error
+ // occurs.
+ bool Skip(int count);
+
+ // Sets *data to point directly at the unread part of the CodedInputStream's
+ // underlying buffer, and *size to the size of that buffer, but does not
+ // advance the stream's current position. This will always either produce
+ // a non-empty buffer or return false. If the caller consumes any of
+ // this data, it should then call Skip() to skip over the consumed bytes.
+ // This may be useful for implementing external fast parsing routines for
+ // types of data not covered by the CodedInputStream interface.
+ bool GetDirectBufferPointer(const void** data, int* size);
+
+ // Like GetDirectBufferPointer, but this method is inlined, and does not
+ // attempt to Refresh() if the buffer is currently empty.
+ inline void GetDirectBufferPointerInline(const void** data,
+ int* size) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // Read raw bytes, copying them into the given buffer.
+ bool ReadRaw(void* buffer, int size);
+
+ // Like ReadRaw, but reads into a string.
+ //
+ // Implementation Note: ReadString() grows the string gradually as it
+ // reads in the data, rather than allocating the entire requested size
+ // upfront. This prevents denial-of-service attacks in which a client
+ // could claim that a string is going to be MAX_INT bytes long in order to
+ // crash the server because it can't allocate this much space at once.
+ bool ReadString(string* buffer, int size);
+ // Like the above, with inlined optimizations. This should only be used
+ // by the protobuf implementation.
+ inline bool InternalReadStringInline(string* buffer,
+ int size) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+
+ // Read a 32-bit little-endian integer.
+ bool ReadLittleEndian32(uint32* value);
+ // Read a 64-bit little-endian integer.
+ bool ReadLittleEndian64(uint64* value);
+
+ // These methods read from an externally provided buffer. The caller is
+ // responsible for ensuring that the buffer has sufficient space.
+ // Read a 32-bit little-endian integer.
+ static const uint8* ReadLittleEndian32FromArray(const uint8* buffer,
+ uint32* value);
+ // Read a 64-bit little-endian integer.
+ static const uint8* ReadLittleEndian64FromArray(const uint8* buffer,
+ uint64* value);
+
+ // Read an unsigned integer with Varint encoding, truncating to 32 bits.
+ // Reading a 32-bit value is equivalent to reading a 64-bit one and casting
+ // it to uint32, but may be more efficient.
+ bool ReadVarint32(uint32* value);
+ // Read an unsigned integer with Varint encoding.
+ bool ReadVarint64(uint64* value);
+
+ // Read a tag. This calls ReadVarint32() and returns the result, or returns
+ // zero (which is not a valid tag) if ReadVarint32() fails. Also, it updates
+ // the last tag value, which can be checked with LastTagWas().
+ // Always inline because this is only called in one place per parse loop
+ // but it is called for every iteration of said loop, so it should be fast.
+ // GCC doesn't want to inline this by default.
+ uint32 ReadTag() GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // This usually a faster alternative to ReadTag() when cutoff is a manifest
+ // constant. It does particularly well for cutoff >= 127. The first part
+ // of the return value is the tag that was read, though it can also be 0 in
+ // the cases where ReadTag() would return 0. If the second part is true
+ // then the tag is known to be in [0, cutoff]. If not, the tag either is
+ // above cutoff or is 0. (There's intentional wiggle room when tag is 0,
+ // because that can arise in several ways, and for best performance we want
+ // to avoid an extra "is tag == 0?" check here.)
+ inline std::pair<uint32, bool> ReadTagWithCutoff(uint32 cutoff)
+ GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // Usually returns true if calling ReadVarint32() now would produce the given
+ // value. Will always return false if ReadVarint32() would not return the
+ // given value. If ExpectTag() returns true, it also advances past
+ // the varint. For best performance, use a compile-time constant as the
+ // parameter.
+ // Always inline because this collapses to a small number of instructions
+ // when given a constant parameter, but GCC doesn't want to inline by default.
+ bool ExpectTag(uint32 expected) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // Like above, except this reads from the specified buffer. The caller is
+ // responsible for ensuring that the buffer is large enough to read a varint
+ // of the expected size. For best performance, use a compile-time constant as
+ // the expected tag parameter.
+ //
+ // Returns a pointer beyond the expected tag if it was found, or NULL if it
+ // was not.
+ static const uint8* ExpectTagFromArray(
+ const uint8* buffer,
+ uint32 expected) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // Usually returns true if no more bytes can be read. Always returns false
+ // if more bytes can be read. If ExpectAtEnd() returns true, a subsequent
+ // call to LastTagWas() will act as if ReadTag() had been called and returned
+ // zero, and ConsumedEntireMessage() will return true.
+ bool ExpectAtEnd();
+
+ // If the last call to ReadTag() or ReadTagWithCutoff() returned the
+ // given value, returns true. Otherwise, returns false;
+ //
+ // This is needed because parsers for some types of embedded messages
+ // (with field type TYPE_GROUP) don't actually know that they've reached the
+ // end of a message until they see an ENDGROUP tag, which was actually part
+ // of the enclosing message. The enclosing message would like to check that
+ // tag to make sure it had the right number, so it calls LastTagWas() on
+ // return from the embedded parser to check.
+ bool LastTagWas(uint32 expected);
+
+ // When parsing message (but NOT a group), this method must be called
+ // immediately after MergeFromCodedStream() returns (if it returns true)
+ // to further verify that the message ended in a legitimate way. For
+ // example, this verifies that parsing did not end on an end-group tag.
+ // It also checks for some cases where, due to optimizations,
+ // MergeFromCodedStream() can incorrectly return true.
+ bool ConsumedEntireMessage();
+
+ // Limits ----------------------------------------------------------
+ // Limits are used when parsing length-delimited embedded messages.
+ // After the message's length is read, PushLimit() is used to prevent
+ // the CodedInputStream from reading beyond that length. Once the
+ // embedded message has been parsed, PopLimit() is called to undo the
+ // limit.
+
+ // Opaque type used with PushLimit() and PopLimit(). Do not modify
+ // values of this type yourself. The only reason that this isn't a
+ // struct with private internals is for efficiency.
+ typedef int Limit;
+
+ // Places a limit on the number of bytes that the stream may read,
+ // starting from the current position. Once the stream hits this limit,
+ // it will act like the end of the input has been reached until PopLimit()
+ // is called.
+ //
+ // As the names imply, the stream conceptually has a stack of limits. The
+ // shortest limit on the stack is always enforced, even if it is not the
+ // top limit.
+ //
+ // The value returned by PushLimit() is opaque to the caller, and must
+ // be passed unchanged to the corresponding call to PopLimit().
+ Limit PushLimit(int byte_limit);
+
+ // Pops the last limit pushed by PushLimit(). The input must be the value
+ // returned by that call to PushLimit().
+ void PopLimit(Limit limit);
+
+ // Returns the number of bytes left until the nearest limit on the
+ // stack is hit, or -1 if no limits are in place.
+ int BytesUntilLimit() const;
+
+ // Returns current position relative to the beginning of the input stream.
+ int CurrentPosition() const;
+
+ // Total Bytes Limit -----------------------------------------------
+ // To prevent malicious users from sending excessively large messages
+ // and causing integer overflows or memory exhaustion, CodedInputStream
+ // imposes a hard limit on the total number of bytes it will read.
+
+ // Sets the maximum number of bytes that this CodedInputStream will read
+ // before refusing to continue. To prevent integer overflows in the
+ // protocol buffers implementation, as well as to prevent servers from
+ // allocating enormous amounts of memory to hold parsed messages, the
+ // maximum message length should be limited to the shortest length that
+ // will not harm usability. The theoretical shortest message that could
+ // cause integer overflows is 512MB. The default limit is 64MB. Apps
+ // should set shorter limits if possible. If warning_threshold is not -1,
+ // a warning will be printed to stderr after warning_threshold bytes are
+ // read. For backwards compatibility all negative values get squashed to -1,
+ // as other negative values might have special internal meanings.
+ // An error will always be printed to stderr if the limit is reached.
+ //
+ // This is unrelated to PushLimit()/PopLimit().
+ //
+ // Hint: If you are reading this because your program is printing a
+ // warning about dangerously large protocol messages, you may be
+ // confused about what to do next. The best option is to change your
+ // design such that excessively large messages are not necessary.
+ // For example, try to design file formats to consist of many small
+ // messages rather than a single large one. If this is infeasible,
+ // you will need to increase the limit. Chances are, though, that
+ // your code never constructs a CodedInputStream on which the limit
+ // can be set. You probably parse messages by calling things like
+ // Message::ParseFromString(). In this case, you will need to change
+ // your code to instead construct some sort of ZeroCopyInputStream
+ // (e.g. an ArrayInputStream), construct a CodedInputStream around
+ // that, then call Message::ParseFromCodedStream() instead. Then
+ // you can adjust the limit. Yes, it's more work, but you're doing
+ // something unusual.
+ void SetTotalBytesLimit(int total_bytes_limit, int warning_threshold);
+
+ // The Total Bytes Limit minus the Current Position, or -1 if there
+ // is no Total Bytes Limit.
+ int BytesUntilTotalBytesLimit() const;
+
+ // Recursion Limit -------------------------------------------------
+ // To prevent corrupt or malicious messages from causing stack overflows,
+ // we must keep track of the depth of recursion when parsing embedded
+ // messages and groups. CodedInputStream keeps track of this because it
+ // is the only object that is passed down the stack during parsing.
+
+ // Sets the maximum recursion depth. The default is 100.
+ void SetRecursionLimit(int limit);
+
+
+ // Increments the current recursion depth. Returns true if the depth is
+ // under the limit, false if it has gone over.
+ bool IncrementRecursionDepth();
+
+ // Decrements the recursion depth.
+ void DecrementRecursionDepth();
+
+ // Extension Registry ----------------------------------------------
+ // ADVANCED USAGE: 99.9% of people can ignore this section.
+ //
+ // By default, when parsing extensions, the parser looks for extension
+ // definitions in the pool which owns the outer message's Descriptor.
+ // However, you may call SetExtensionRegistry() to provide an alternative
+ // pool instead. This makes it possible, for example, to parse a message
+ // using a generated class, but represent some extensions using
+ // DynamicMessage.
+
+ // Set the pool used to look up extensions. Most users do not need to call
+ // this as the correct pool will be chosen automatically.
+ //
+ // WARNING: It is very easy to misuse this. Carefully read the requirements
+ // below. Do not use this unless you are sure you need it. Almost no one
+ // does.
+ //
+ // Let's say you are parsing a message into message object m, and you want
+ // to take advantage of SetExtensionRegistry(). You must follow these
+ // requirements:
+ //
+ // The given DescriptorPool must contain m->GetDescriptor(). It is not
+ // sufficient for it to simply contain a descriptor that has the same name
+ // and content -- it must be the *exact object*. In other words:
+ // assert(pool->FindMessageTypeByName(m->GetDescriptor()->full_name()) ==
+ // m->GetDescriptor());
+ // There are two ways to satisfy this requirement:
+ // 1) Use m->GetDescriptor()->pool() as the pool. This is generally useless
+ // because this is the pool that would be used anyway if you didn't call
+ // SetExtensionRegistry() at all.
+ // 2) Use a DescriptorPool which has m->GetDescriptor()->pool() as an
+ // "underlay". Read the documentation for DescriptorPool for more
+ // information about underlays.
+ //
+ // You must also provide a MessageFactory. This factory will be used to
+ // construct Message objects representing extensions. The factory's
+ // GetPrototype() MUST return non-NULL for any Descriptor which can be found
+ // through the provided pool.
+ //
+ // If the provided factory might return instances of protocol-compiler-
+ // generated (i.e. compiled-in) types, or if the outer message object m is
+ // a generated type, then the given factory MUST have this property: If
+ // GetPrototype() is given a Descriptor which resides in
+ // DescriptorPool::generated_pool(), the factory MUST return the same
+ // prototype which MessageFactory::generated_factory() would return. That
+ // is, given a descriptor for a generated type, the factory must return an
+ // instance of the generated class (NOT DynamicMessage). However, when
+ // given a descriptor for a type that is NOT in generated_pool, the factory
+ // is free to return any implementation.
+ //
+ // The reason for this requirement is that generated sub-objects may be
+ // accessed via the standard (non-reflection) extension accessor methods,
+ // and these methods will down-cast the object to the generated class type.
+ // If the object is not actually of that type, the results would be undefined.
+ // On the other hand, if an extension is not compiled in, then there is no
+ // way the code could end up accessing it via the standard accessors -- the
+ // only way to access the extension is via reflection. When using reflection,
+ // DynamicMessage and generated messages are indistinguishable, so it's fine
+ // if these objects are represented using DynamicMessage.
+ //
+ // Using DynamicMessageFactory on which you have called
+ // SetDelegateToGeneratedFactory(true) should be sufficient to satisfy the
+ // above requirement.
+ //
+ // If either pool or factory is NULL, both must be NULL.
+ //
+ // Note that this feature is ignored when parsing "lite" messages as they do
+ // not have descriptors.
+ void SetExtensionRegistry(const DescriptorPool* pool,
+ MessageFactory* factory);
+
+ // Get the DescriptorPool set via SetExtensionRegistry(), or NULL if no pool
+ // has been provided.
+ const DescriptorPool* GetExtensionPool();
+
+ // Get the MessageFactory set via SetExtensionRegistry(), or NULL if no
+ // factory has been provided.
+ MessageFactory* GetExtensionFactory();
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CodedInputStream);
+
+ ZeroCopyInputStream* input_;
+ const uint8* buffer_;
+ const uint8* buffer_end_; // pointer to the end of the buffer.
+ int total_bytes_read_; // total bytes read from input_, including
+ // the current buffer
+
+ // If total_bytes_read_ surpasses INT_MAX, we record the extra bytes here
+ // so that we can BackUp() on destruction.
+ int overflow_bytes_;
+
+ // LastTagWas() stuff.
+ uint32 last_tag_; // result of last ReadTag() or ReadTagWithCutoff().
+
+ // This is set true by ReadTag{Fallback/Slow}() if it is called when exactly
+ // at EOF, or by ExpectAtEnd() when it returns true. This happens when we
+ // reach the end of a message and attempt to read another tag.
+ bool legitimate_message_end_;
+
+ // See EnableAliasing().
+ bool aliasing_enabled_;
+
+ // Limits
+ Limit current_limit_; // if position = -1, no limit is applied
+
+ // For simplicity, if the current buffer crosses a limit (either a normal
+ // limit created by PushLimit() or the total bytes limit), buffer_size_
+ // only tracks the number of bytes before that limit. This field
+ // contains the number of bytes after it. Note that this implies that if
+ // buffer_size_ == 0 and buffer_size_after_limit_ > 0, we know we've
+ // hit a limit. However, if both are zero, it doesn't necessarily mean
+ // we aren't at a limit -- the buffer may have ended exactly at the limit.
+ int buffer_size_after_limit_;
+
+ // Maximum number of bytes to read, period. This is unrelated to
+ // current_limit_. Set using SetTotalBytesLimit().
+ int total_bytes_limit_;
+
+ // If positive/0: Limit for bytes read after which a warning due to size
+ // should be logged.
+ // If -1: Printing of warning disabled. Can be set by client.
+ // If -2: Internal: Limit has been reached, print full size when destructing.
+ int total_bytes_warning_threshold_;
+
+ // Current recursion depth, controlled by IncrementRecursionDepth() and
+ // DecrementRecursionDepth().
+ int recursion_depth_;
+ // Recursion depth limit, set by SetRecursionLimit().
+ int recursion_limit_;
+
+ // See SetExtensionRegistry().
+ const DescriptorPool* extension_pool_;
+ MessageFactory* extension_factory_;
+
+ // Private member functions.
+
+ // Advance the buffer by a given number of bytes.
+ void Advance(int amount);
+
+ // Back up input_ to the current buffer position.
+ void BackUpInputToCurrentPosition();
+
+ // Recomputes the value of buffer_size_after_limit_. Must be called after
+ // current_limit_ or total_bytes_limit_ changes.
+ void RecomputeBufferLimits();
+
+ // Writes an error message saying that we hit total_bytes_limit_.
+ void PrintTotalBytesLimitError();
+
+ // Called when the buffer runs out to request more data. Implies an
+ // Advance(BufferSize()).
+ bool Refresh();
+
+ // When parsing varints, we optimize for the common case of small values, and
+ // then optimize for the case when the varint fits within the current buffer
+ // piece. The Fallback method is used when we can't use the one-byte
+ // optimization. The Slow method is yet another fallback when the buffer is
+ // not large enough. Making the slow path out-of-line speeds up the common
+ // case by 10-15%. The slow path is fairly uncommon: it only triggers when a
+ // message crosses multiple buffers.
+ bool ReadVarint32Fallback(uint32* value);
+ bool ReadVarint64Fallback(uint64* value);
+ bool ReadVarint32Slow(uint32* value);
+ bool ReadVarint64Slow(uint64* value);
+ bool ReadLittleEndian32Fallback(uint32* value);
+ bool ReadLittleEndian64Fallback(uint64* value);
+ // Fallback/slow methods for reading tags. These do not update last_tag_,
+ // but will set legitimate_message_end_ if we are at the end of the input
+ // stream.
+ uint32 ReadTagFallback();
+ uint32 ReadTagSlow();
+ bool ReadStringFallback(string* buffer, int size);
+
+ // Return the size of the buffer.
+ int BufferSize() const;
+
+ static const int kDefaultTotalBytesLimit = 64 << 20; // 64MB
+
+ static const int kDefaultTotalBytesWarningThreshold = 32 << 20; // 32MB
+
+ static int default_recursion_limit_; // 100 by default.
+};
+
+// Class which encodes and writes binary data which is composed of varint-
+// encoded integers and fixed-width pieces. Wraps a ZeroCopyOutputStream.
+// Most users will not need to deal with CodedOutputStream.
+//
+// Most methods of CodedOutputStream which return a bool return false if an
+// underlying I/O error occurs. Once such a failure occurs, the
+// CodedOutputStream is broken and is no longer useful. The Write* methods do
+// not return the stream status, but will invalidate the stream if an error
+// occurs. The client can probe HadError() to determine the status.
+//
+// Note that every method of CodedOutputStream which writes some data has
+// a corresponding static "ToArray" version. These versions write directly
+// to the provided buffer, returning a pointer past the last written byte.
+// They require that the buffer has sufficient capacity for the encoded data.
+// This allows an optimization where we check if an output stream has enough
+// space for an entire message before we start writing and, if there is, we
+// call only the ToArray methods to avoid doing bound checks for each
+// individual value.
+// i.e., in the example above:
+//
+// CodedOutputStream coded_output = new CodedOutputStream(raw_output);
+// int magic_number = 1234;
+// char text[] = "Hello world!";
+//
+// int coded_size = sizeof(magic_number) +
+// CodedOutputStream::VarintSize32(strlen(text)) +
+// strlen(text);
+//
+// uint8* buffer =
+// coded_output->GetDirectBufferForNBytesAndAdvance(coded_size);
+// if (buffer != NULL) {
+// // The output stream has enough space in the buffer: write directly to
+// // the array.
+// buffer = CodedOutputStream::WriteLittleEndian32ToArray(magic_number,
+// buffer);
+// buffer = CodedOutputStream::WriteVarint32ToArray(strlen(text), buffer);
+// buffer = CodedOutputStream::WriteRawToArray(text, strlen(text), buffer);
+// } else {
+// // Make bound-checked writes, which will ask the underlying stream for
+// // more space as needed.
+// coded_output->WriteLittleEndian32(magic_number);
+// coded_output->WriteVarint32(strlen(text));
+// coded_output->WriteRaw(text, strlen(text));
+// }
+//
+// delete coded_output;
+class LIBPROTOBUF_EXPORT CodedOutputStream {
+ public:
+ // Create an CodedOutputStream that writes to the given ZeroCopyOutputStream.
+ explicit CodedOutputStream(ZeroCopyOutputStream* output);
+
+ // Destroy the CodedOutputStream and position the underlying
+ // ZeroCopyOutputStream immediately after the last byte written.
+ ~CodedOutputStream();
+
+ // Skips a number of bytes, leaving the bytes unmodified in the underlying
+ // buffer. Returns false if an underlying write error occurs. This is
+ // mainly useful with GetDirectBufferPointer().
+ bool Skip(int count);
+
+ // Sets *data to point directly at the unwritten part of the
+ // CodedOutputStream's underlying buffer, and *size to the size of that
+ // buffer, but does not advance the stream's current position. This will
+ // always either produce a non-empty buffer or return false. If the caller
+ // writes any data to this buffer, it should then call Skip() to skip over
+ // the consumed bytes. This may be useful for implementing external fast
+ // serialization routines for types of data not covered by the
+ // CodedOutputStream interface.
+ bool GetDirectBufferPointer(void** data, int* size);
+
+ // If there are at least "size" bytes available in the current buffer,
+ // returns a pointer directly into the buffer and advances over these bytes.
+ // The caller may then write directly into this buffer (e.g. using the
+ // *ToArray static methods) rather than go through CodedOutputStream. If
+ // there are not enough bytes available, returns NULL. The return pointer is
+ // invalidated as soon as any other non-const method of CodedOutputStream
+ // is called.
+ inline uint8* GetDirectBufferForNBytesAndAdvance(int size);
+
+ // Write raw bytes, copying them from the given buffer.
+ void WriteRaw(const void* buffer, int size);
+ // Like WriteRaw() but will try to write aliased data if aliasing is
+ // turned on.
+ void WriteRawMaybeAliased(const void* data, int size);
+ // Like WriteRaw() but writing directly to the target array.
+ // This is _not_ inlined, as the compiler often optimizes memcpy into inline
+ // copy loops. Since this gets called by every field with string or bytes
+ // type, inlining may lead to a significant amount of code bloat, with only a
+ // minor performance gain.
+ static uint8* WriteRawToArray(const void* buffer, int size, uint8* target);
+
+ // Equivalent to WriteRaw(str.data(), str.size()).
+ void WriteString(const string& str);
+ // Like WriteString() but writing directly to the target array.
+ static uint8* WriteStringToArray(const string& str, uint8* target);
+ // Write the varint-encoded size of str followed by str.
+ static uint8* WriteStringWithSizeToArray(const string& str, uint8* target);
+
+
+ // Instructs the CodedOutputStream to allow the underlying
+ // ZeroCopyOutputStream to hold pointers to the original structure instead of
+ // copying, if it supports it (i.e. output->AllowsAliasing() is true). If the
+ // underlying stream does not support aliasing, then enabling it has no
+ // affect. For now, this only affects the behavior of
+ // WriteRawMaybeAliased().
+ //
+ // NOTE: It is caller's responsibility to ensure that the chunk of memory
+ // remains live until all of the data has been consumed from the stream.
+ void EnableAliasing(bool enabled);
+
+ // Write a 32-bit little-endian integer.
+ void WriteLittleEndian32(uint32 value);
+ // Like WriteLittleEndian32() but writing directly to the target array.
+ static uint8* WriteLittleEndian32ToArray(uint32 value, uint8* target);
+ // Write a 64-bit little-endian integer.
+ void WriteLittleEndian64(uint64 value);
+ // Like WriteLittleEndian64() but writing directly to the target array.
+ static uint8* WriteLittleEndian64ToArray(uint64 value, uint8* target);
+
+ // Write an unsigned integer with Varint encoding. Writing a 32-bit value
+ // is equivalent to casting it to uint64 and writing it as a 64-bit value,
+ // but may be more efficient.
+ void WriteVarint32(uint32 value);
+ // Like WriteVarint32() but writing directly to the target array.
+ static uint8* WriteVarint32ToArray(uint32 value, uint8* target);
+ // Write an unsigned integer with Varint encoding.
+ void WriteVarint64(uint64 value);
+ // Like WriteVarint64() but writing directly to the target array.
+ static uint8* WriteVarint64ToArray(uint64 value, uint8* target);
+
+ // Equivalent to WriteVarint32() except when the value is negative,
+ // in which case it must be sign-extended to a full 10 bytes.
+ void WriteVarint32SignExtended(int32 value);
+ // Like WriteVarint32SignExtended() but writing directly to the target array.
+ static uint8* WriteVarint32SignExtendedToArray(int32 value, uint8* target);
+
+ // This is identical to WriteVarint32(), but optimized for writing tags.
+ // In particular, if the input is a compile-time constant, this method
+ // compiles down to a couple instructions.
+ // Always inline because otherwise the aformentioned optimization can't work,
+ // but GCC by default doesn't want to inline this.
+ void WriteTag(uint32 value);
+ // Like WriteTag() but writing directly to the target array.
+ static uint8* WriteTagToArray(
+ uint32 value, uint8* target) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // Returns the number of bytes needed to encode the given value as a varint.
+ static int VarintSize32(uint32 value);
+ // Returns the number of bytes needed to encode the given value as a varint.
+ static int VarintSize64(uint64 value);
+
+ // If negative, 10 bytes. Otheriwse, same as VarintSize32().
+ static int VarintSize32SignExtended(int32 value);
+
+ // Compile-time equivalent of VarintSize32().
+ template <uint32 Value>
+ struct StaticVarintSize32 {
+ static const int value =
+ (Value < (1 << 7))
+ ? 1
+ : (Value < (1 << 14))
+ ? 2
+ : (Value < (1 << 21))
+ ? 3
+ : (Value < (1 << 28))
+ ? 4
+ : 5;
+ };
+
+ // Returns the total number of bytes written since this object was created.
+ inline int ByteCount() const;
+
+ // Returns true if there was an underlying I/O error since this object was
+ // created.
+ bool HadError() const { return had_error_; }
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CodedOutputStream);
+
+ ZeroCopyOutputStream* output_;
+ uint8* buffer_;
+ int buffer_size_;
+ int total_bytes_; // Sum of sizes of all buffers seen so far.
+ bool had_error_; // Whether an error occurred during output.
+ bool aliasing_enabled_; // See EnableAliasing().
+
+ // Advance the buffer by a given number of bytes.
+ void Advance(int amount);
+
+ // Called when the buffer runs out to request more data. Implies an
+ // Advance(buffer_size_).
+ bool Refresh();
+
+ // Like WriteRaw() but may avoid copying if the underlying
+ // ZeroCopyOutputStream supports it.
+ void WriteAliasedRaw(const void* buffer, int size);
+
+ static uint8* WriteVarint32FallbackToArray(uint32 value, uint8* target);
+
+ // Always-inlined versions of WriteVarint* functions so that code can be
+ // reused, while still controlling size. For instance, WriteVarint32ToArray()
+ // should not directly call this: since it is inlined itself, doing so
+ // would greatly increase the size of generated code. Instead, it should call
+ // WriteVarint32FallbackToArray. Meanwhile, WriteVarint32() is already
+ // out-of-line, so it should just invoke this directly to avoid any extra
+ // function call overhead.
+ static uint8* WriteVarint32FallbackToArrayInline(
+ uint32 value, uint8* target) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+ static uint8* WriteVarint64ToArrayInline(
+ uint64 value, uint8* target) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ static int VarintSize32Fallback(uint32 value);
+};
+
+// inline methods ====================================================
+// The vast majority of varints are only one byte. These inline
+// methods optimize for that case.
+
+inline bool CodedInputStream::ReadVarint32(uint32* value) {
+ if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_) && *buffer_ < 0x80) {
+ *value = *buffer_;
+ Advance(1);
+ return true;
+ } else {
+ return ReadVarint32Fallback(value);
+ }
+}
+
+inline bool CodedInputStream::ReadVarint64(uint64* value) {
+ if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_) && *buffer_ < 0x80) {
+ *value = *buffer_;
+ Advance(1);
+ return true;
+ } else {
+ return ReadVarint64Fallback(value);
+ }
+}
+
+// static
+inline const uint8* CodedInputStream::ReadLittleEndian32FromArray(
+ const uint8* buffer,
+ uint32* value) {
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ memcpy(value, buffer, sizeof(*value));
+ return buffer + sizeof(*value);
+#else
+ *value = (static_cast<uint32>(buffer[0]) ) |
+ (static_cast<uint32>(buffer[1]) << 8) |
+ (static_cast<uint32>(buffer[2]) << 16) |
+ (static_cast<uint32>(buffer[3]) << 24);
+ return buffer + sizeof(*value);
+#endif
+}
+// static
+inline const uint8* CodedInputStream::ReadLittleEndian64FromArray(
+ const uint8* buffer,
+ uint64* value) {
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ memcpy(value, buffer, sizeof(*value));
+ return buffer + sizeof(*value);
+#else
+ uint32 part0 = (static_cast<uint32>(buffer[0]) ) |
+ (static_cast<uint32>(buffer[1]) << 8) |
+ (static_cast<uint32>(buffer[2]) << 16) |
+ (static_cast<uint32>(buffer[3]) << 24);
+ uint32 part1 = (static_cast<uint32>(buffer[4]) ) |
+ (static_cast<uint32>(buffer[5]) << 8) |
+ (static_cast<uint32>(buffer[6]) << 16) |
+ (static_cast<uint32>(buffer[7]) << 24);
+ *value = static_cast<uint64>(part0) |
+ (static_cast<uint64>(part1) << 32);
+ return buffer + sizeof(*value);
+#endif
+}
+
+inline bool CodedInputStream::ReadLittleEndian32(uint32* value) {
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ if (GOOGLE_PREDICT_TRUE(BufferSize() >= static_cast<int>(sizeof(*value)))) {
+ memcpy(value, buffer_, sizeof(*value));
+ Advance(sizeof(*value));
+ return true;
+ } else {
+ return ReadLittleEndian32Fallback(value);
+ }
+#else
+ return ReadLittleEndian32Fallback(value);
+#endif
+}
+
+inline bool CodedInputStream::ReadLittleEndian64(uint64* value) {
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ if (GOOGLE_PREDICT_TRUE(BufferSize() >= static_cast<int>(sizeof(*value)))) {
+ memcpy(value, buffer_, sizeof(*value));
+ Advance(sizeof(*value));
+ return true;
+ } else {
+ return ReadLittleEndian64Fallback(value);
+ }
+#else
+ return ReadLittleEndian64Fallback(value);
+#endif
+}
+
+inline uint32 CodedInputStream::ReadTag() {
+ if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_) && buffer_[0] < 0x80) {
+ last_tag_ = buffer_[0];
+ Advance(1);
+ return last_tag_;
+ } else {
+ last_tag_ = ReadTagFallback();
+ return last_tag_;
+ }
+}
+
+inline std::pair<uint32, bool> CodedInputStream::ReadTagWithCutoff(
+ uint32 cutoff) {
+ // In performance-sensitive code we can expect cutoff to be a compile-time
+ // constant, and things like "cutoff >= kMax1ByteVarint" to be evaluated at
+ // compile time.
+ if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_)) {
+ // Hot case: buffer_ non_empty, buffer_[0] in [1, 128).
+ // TODO(gpike): Is it worth rearranging this? E.g., if the number of fields
+ // is large enough then is it better to check for the two-byte case first?
+ if (static_cast<int8>(buffer_[0]) > 0) {
+ const uint32 kMax1ByteVarint = 0x7f;
+ uint32 tag = last_tag_ = buffer_[0];
+ Advance(1);
+ return make_pair(tag, cutoff >= kMax1ByteVarint || tag <= cutoff);
+ }
+ // Other hot case: cutoff >= 0x80, buffer_ has at least two bytes available,
+ // and tag is two bytes. The latter is tested by bitwise-and-not of the
+ // first byte and the second byte.
+ if (cutoff >= 0x80 &&
+ GOOGLE_PREDICT_TRUE(buffer_ + 1 < buffer_end_) &&
+ GOOGLE_PREDICT_TRUE((buffer_[0] & ~buffer_[1]) >= 0x80)) {
+ const uint32 kMax2ByteVarint = (0x7f << 7) + 0x7f;
+ uint32 tag = last_tag_ = (1u << 7) * buffer_[1] + (buffer_[0] - 0x80);
+ Advance(2);
+ // It might make sense to test for tag == 0 now, but it is so rare that
+ // that we don't bother. A varint-encoded 0 should be one byte unless
+ // the encoder lost its mind. The second part of the return value of
+ // this function is allowed to be either true or false if the tag is 0,
+ // so we don't have to check for tag == 0. We may need to check whether
+ // it exceeds cutoff.
+ bool at_or_below_cutoff = cutoff >= kMax2ByteVarint || tag <= cutoff;
+ return make_pair(tag, at_or_below_cutoff);
+ }
+ }
+ // Slow path
+ last_tag_ = ReadTagFallback();
+ return make_pair(last_tag_, static_cast<uint32>(last_tag_ - 1) < cutoff);
+}
+
+inline bool CodedInputStream::LastTagWas(uint32 expected) {
+ return last_tag_ == expected;
+}
+
+inline bool CodedInputStream::ConsumedEntireMessage() {
+ return legitimate_message_end_;
+}
+
+inline bool CodedInputStream::ExpectTag(uint32 expected) {
+ if (expected < (1 << 7)) {
+ if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_) && buffer_[0] == expected) {
+ Advance(1);
+ return true;
+ } else {
+ return false;
+ }
+ } else if (expected < (1 << 14)) {
+ if (GOOGLE_PREDICT_TRUE(BufferSize() >= 2) &&
+ buffer_[0] == static_cast<uint8>(expected | 0x80) &&
+ buffer_[1] == static_cast<uint8>(expected >> 7)) {
+ Advance(2);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ // Don't bother optimizing for larger values.
+ return false;
+ }
+}
+
+inline const uint8* CodedInputStream::ExpectTagFromArray(
+ const uint8* buffer, uint32 expected) {
+ if (expected < (1 << 7)) {
+ if (buffer[0] == expected) {
+ return buffer + 1;
+ }
+ } else if (expected < (1 << 14)) {
+ if (buffer[0] == static_cast<uint8>(expected | 0x80) &&
+ buffer[1] == static_cast<uint8>(expected >> 7)) {
+ return buffer + 2;
+ }
+ }
+ return NULL;
+}
+
+inline void CodedInputStream::GetDirectBufferPointerInline(const void** data,
+ int* size) {
+ *data = buffer_;
+ *size = buffer_end_ - buffer_;
+}
+
+inline bool CodedInputStream::ExpectAtEnd() {
+ // If we are at a limit we know no more bytes can be read. Otherwise, it's
+ // hard to say without calling Refresh(), and we'd rather not do that.
+
+ if (buffer_ == buffer_end_ &&
+ ((buffer_size_after_limit_ != 0) ||
+ (total_bytes_read_ == current_limit_))) {
+ last_tag_ = 0; // Pretend we called ReadTag()...
+ legitimate_message_end_ = true; // ... and it hit EOF.
+ return true;
+ } else {
+ return false;
+ }
+}
+
+inline int CodedInputStream::CurrentPosition() const {
+ return total_bytes_read_ - (BufferSize() + buffer_size_after_limit_);
+}
+
+inline uint8* CodedOutputStream::GetDirectBufferForNBytesAndAdvance(int size) {
+ if (buffer_size_ < size) {
+ return NULL;
+ } else {
+ uint8* result = buffer_;
+ Advance(size);
+ return result;
+ }
+}
+
+inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value,
+ uint8* target) {
+ if (value < 0x80) {
+ *target = value;
+ return target + 1;
+ } else {
+ return WriteVarint32FallbackToArray(value, target);
+ }
+}
+
+inline void CodedOutputStream::WriteVarint32SignExtended(int32 value) {
+ if (value < 0) {
+ WriteVarint64(static_cast<uint64>(value));
+ } else {
+ WriteVarint32(static_cast<uint32>(value));
+ }
+}
+
+inline uint8* CodedOutputStream::WriteVarint32SignExtendedToArray(
+ int32 value, uint8* target) {
+ if (value < 0) {
+ return WriteVarint64ToArray(static_cast<uint64>(value), target);
+ } else {
+ return WriteVarint32ToArray(static_cast<uint32>(value), target);
+ }
+}
+
+inline uint8* CodedOutputStream::WriteLittleEndian32ToArray(uint32 value,
+ uint8* target) {
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ memcpy(target, &value, sizeof(value));
+#else
+ target[0] = static_cast<uint8>(value);
+ target[1] = static_cast<uint8>(value >> 8);
+ target[2] = static_cast<uint8>(value >> 16);
+ target[3] = static_cast<uint8>(value >> 24);
+#endif
+ return target + sizeof(value);
+}
+
+inline uint8* CodedOutputStream::WriteLittleEndian64ToArray(uint64 value,
+ uint8* target) {
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ memcpy(target, &value, sizeof(value));
+#else
+ uint32 part0 = static_cast<uint32>(value);
+ uint32 part1 = static_cast<uint32>(value >> 32);
+
+ target[0] = static_cast<uint8>(part0);
+ target[1] = static_cast<uint8>(part0 >> 8);
+ target[2] = static_cast<uint8>(part0 >> 16);
+ target[3] = static_cast<uint8>(part0 >> 24);
+ target[4] = static_cast<uint8>(part1);
+ target[5] = static_cast<uint8>(part1 >> 8);
+ target[6] = static_cast<uint8>(part1 >> 16);
+ target[7] = static_cast<uint8>(part1 >> 24);
+#endif
+ return target + sizeof(value);
+}
+
+inline void CodedOutputStream::WriteTag(uint32 value) {
+ WriteVarint32(value);
+}
+
+inline uint8* CodedOutputStream::WriteTagToArray(
+ uint32 value, uint8* target) {
+ if (value < (1 << 7)) {
+ target[0] = value;
+ return target + 1;
+ } else if (value < (1 << 14)) {
+ target[0] = static_cast<uint8>(value | 0x80);
+ target[1] = static_cast<uint8>(value >> 7);
+ return target + 2;
+ } else {
+ return WriteVarint32FallbackToArray(value, target);
+ }
+}
+
+inline int CodedOutputStream::VarintSize32(uint32 value) {
+ if (value < (1 << 7)) {
+ return 1;
+ } else {
+ return VarintSize32Fallback(value);
+ }
+}
+
+inline int CodedOutputStream::VarintSize32SignExtended(int32 value) {
+ if (value < 0) {
+ return 10; // TODO(kenton): Make this a symbolic constant.
+ } else {
+ return VarintSize32(static_cast<uint32>(value));
+ }
+}
+
+inline void CodedOutputStream::WriteString(const string& str) {
+ WriteRaw(str.data(), static_cast<int>(str.size()));
+}
+
+inline void CodedOutputStream::WriteRawMaybeAliased(
+ const void* data, int size) {
+ if (aliasing_enabled_) {
+ WriteAliasedRaw(data, size);
+ } else {
+ WriteRaw(data, size);
+ }
+}
+
+inline uint8* CodedOutputStream::WriteStringToArray(
+ const string& str, uint8* target) {
+ return WriteRawToArray(str.data(), static_cast<int>(str.size()), target);
+}
+
+inline int CodedOutputStream::ByteCount() const {
+ return total_bytes_ - buffer_size_;
+}
+
+inline void CodedInputStream::Advance(int amount) {
+ buffer_ += amount;
+}
+
+inline void CodedOutputStream::Advance(int amount) {
+ buffer_ += amount;
+ buffer_size_ -= amount;
+}
+
+inline void CodedInputStream::SetRecursionLimit(int limit) {
+ recursion_limit_ = limit;
+}
+
+inline bool CodedInputStream::IncrementRecursionDepth() {
+ ++recursion_depth_;
+ return recursion_depth_ <= recursion_limit_;
+}
+
+inline void CodedInputStream::DecrementRecursionDepth() {
+ if (recursion_depth_ > 0) --recursion_depth_;
+}
+
+inline void CodedInputStream::SetExtensionRegistry(const DescriptorPool* pool,
+ MessageFactory* factory) {
+ extension_pool_ = pool;
+ extension_factory_ = factory;
+}
+
+inline const DescriptorPool* CodedInputStream::GetExtensionPool() {
+ return extension_pool_;
+}
+
+inline MessageFactory* CodedInputStream::GetExtensionFactory() {
+ return extension_factory_;
+}
+
+inline int CodedInputStream::BufferSize() const {
+ return buffer_end_ - buffer_;
+}
+
+inline CodedInputStream::CodedInputStream(ZeroCopyInputStream* input)
+ : input_(input),
+ buffer_(NULL),
+ buffer_end_(NULL),
+ total_bytes_read_(0),
+ overflow_bytes_(0),
+ last_tag_(0),
+ legitimate_message_end_(false),
+ aliasing_enabled_(false),
+ current_limit_(kint32max),
+ buffer_size_after_limit_(0),
+ total_bytes_limit_(kDefaultTotalBytesLimit),
+ total_bytes_warning_threshold_(kDefaultTotalBytesWarningThreshold),
+ recursion_depth_(0),
+ recursion_limit_(default_recursion_limit_),
+ extension_pool_(NULL),
+ extension_factory_(NULL) {
+ // Eagerly Refresh() so buffer space is immediately available.
+ Refresh();
+}
+
+inline CodedInputStream::CodedInputStream(const uint8* buffer, int size)
+ : input_(NULL),
+ buffer_(buffer),
+ buffer_end_(buffer + size),
+ total_bytes_read_(size),
+ overflow_bytes_(0),
+ last_tag_(0),
+ legitimate_message_end_(false),
+ aliasing_enabled_(false),
+ current_limit_(size),
+ buffer_size_after_limit_(0),
+ total_bytes_limit_(kDefaultTotalBytesLimit),
+ total_bytes_warning_threshold_(kDefaultTotalBytesWarningThreshold),
+ recursion_depth_(0),
+ recursion_limit_(default_recursion_limit_),
+ extension_pool_(NULL),
+ extension_factory_(NULL) {
+ // Note that setting current_limit_ == size is important to prevent some
+ // code paths from trying to access input_ and segfaulting.
+}
+
+inline bool CodedInputStream::IsFlat() const {
+ return input_ == NULL;
+}
+
+} // namespace io
+} // namespace protobuf
+
+
+#if defined(_MSC_VER) && _MSC_VER >= 1300
+ #pragma runtime_checks("c", restore)
+#endif // _MSC_VER
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_CODED_STREAM_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/coded_stream_inl.h b/toolkit/components/protobuf/src/google/protobuf/io/coded_stream_inl.h
new file mode 100644
index 0000000000..88c14cab40
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/coded_stream_inl.h
@@ -0,0 +1,69 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: jasonh@google.com (Jason Hsueh)
+//
+// Implements methods of coded_stream.h that need to be inlined for performance
+// reasons, but should not be defined in a public header.
+
+#ifndef GOOGLE_PROTOBUF_IO_CODED_STREAM_INL_H__
+#define GOOGLE_PROTOBUF_IO_CODED_STREAM_INL_H__
+
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+#include <string>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+inline bool CodedInputStream::InternalReadStringInline(string* buffer,
+ int size) {
+ if (size < 0) return false; // security: size is often user-supplied
+
+ if (BufferSize() >= size) {
+ STLStringResizeUninitialized(buffer, size);
+ // When buffer is empty, string_as_array(buffer) will return NULL but memcpy
+ // requires non-NULL pointers even when size is 0. Hench this check.
+ if (size > 0) {
+ memcpy(mutable_string_data(buffer), buffer_, size);
+ Advance(size);
+ }
+ return true;
+ }
+
+ return ReadStringFallback(buffer, size);
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_CODED_STREAM_INL_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.cc b/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.cc
new file mode 100644
index 0000000000..6e4054edaf
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.cc
@@ -0,0 +1,325 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: brianolson@google.com (Brian Olson)
+//
+// This file contains the implementation of classes GzipInputStream and
+// GzipOutputStream.
+
+
+#if HAVE_ZLIB
+#include <google/protobuf/io/gzip_stream.h>
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+static const int kDefaultBufferSize = 65536;
+
+GzipInputStream::GzipInputStream(
+ ZeroCopyInputStream* sub_stream, Format format, int buffer_size)
+ : format_(format), sub_stream_(sub_stream), zerror_(Z_OK) {
+ zcontext_.zalloc = Z_NULL;
+ zcontext_.zfree = Z_NULL;
+ zcontext_.opaque = Z_NULL;
+ zcontext_.total_out = 0;
+ zcontext_.next_in = NULL;
+ zcontext_.avail_in = 0;
+ zcontext_.total_in = 0;
+ zcontext_.msg = NULL;
+ if (buffer_size == -1) {
+ output_buffer_length_ = kDefaultBufferSize;
+ } else {
+ output_buffer_length_ = buffer_size;
+ }
+ output_buffer_ = operator new(output_buffer_length_);
+ GOOGLE_CHECK(output_buffer_ != NULL);
+ zcontext_.next_out = static_cast<Bytef*>(output_buffer_);
+ zcontext_.avail_out = output_buffer_length_;
+ output_position_ = output_buffer_;
+}
+GzipInputStream::~GzipInputStream() {
+ operator delete(output_buffer_);
+ zerror_ = inflateEnd(&zcontext_);
+}
+
+static inline int internalInflateInit2(
+ z_stream* zcontext, GzipInputStream::Format format) {
+ int windowBitsFormat = 0;
+ switch (format) {
+ case GzipInputStream::GZIP: windowBitsFormat = 16; break;
+ case GzipInputStream::AUTO: windowBitsFormat = 32; break;
+ case GzipInputStream::ZLIB: windowBitsFormat = 0; break;
+ }
+ return inflateInit2(zcontext, /* windowBits */15 | windowBitsFormat);
+}
+
+int GzipInputStream::Inflate(int flush) {
+ if ((zerror_ == Z_OK) && (zcontext_.avail_out == 0)) {
+ // previous inflate filled output buffer. don't change input params yet.
+ } else if (zcontext_.avail_in == 0) {
+ const void* in;
+ int in_size;
+ bool first = zcontext_.next_in == NULL;
+ bool ok = sub_stream_->Next(&in, &in_size);
+ if (!ok) {
+ zcontext_.next_out = NULL;
+ zcontext_.avail_out = 0;
+ return Z_STREAM_END;
+ }
+ zcontext_.next_in = static_cast<Bytef*>(const_cast<void*>(in));
+ zcontext_.avail_in = in_size;
+ if (first) {
+ int error = internalInflateInit2(&zcontext_, format_);
+ if (error != Z_OK) {
+ return error;
+ }
+ }
+ }
+ zcontext_.next_out = static_cast<Bytef*>(output_buffer_);
+ zcontext_.avail_out = output_buffer_length_;
+ output_position_ = output_buffer_;
+ int error = inflate(&zcontext_, flush);
+ return error;
+}
+
+void GzipInputStream::DoNextOutput(const void** data, int* size) {
+ *data = output_position_;
+ *size = ((uintptr_t)zcontext_.next_out) - ((uintptr_t)output_position_);
+ output_position_ = zcontext_.next_out;
+}
+
+// implements ZeroCopyInputStream ----------------------------------
+bool GzipInputStream::Next(const void** data, int* size) {
+ bool ok = (zerror_ == Z_OK) || (zerror_ == Z_STREAM_END)
+ || (zerror_ == Z_BUF_ERROR);
+ if ((!ok) || (zcontext_.next_out == NULL)) {
+ return false;
+ }
+ if (zcontext_.next_out != output_position_) {
+ DoNextOutput(data, size);
+ return true;
+ }
+ if (zerror_ == Z_STREAM_END) {
+ if (zcontext_.next_out != NULL) {
+ // sub_stream_ may have concatenated streams to follow
+ zerror_ = inflateEnd(&zcontext_);
+ if (zerror_ != Z_OK) {
+ return false;
+ }
+ zerror_ = internalInflateInit2(&zcontext_, format_);
+ if (zerror_ != Z_OK) {
+ return false;
+ }
+ } else {
+ *data = NULL;
+ *size = 0;
+ return false;
+ }
+ }
+ zerror_ = Inflate(Z_NO_FLUSH);
+ if ((zerror_ == Z_STREAM_END) && (zcontext_.next_out == NULL)) {
+ // The underlying stream's Next returned false inside Inflate.
+ return false;
+ }
+ ok = (zerror_ == Z_OK) || (zerror_ == Z_STREAM_END)
+ || (zerror_ == Z_BUF_ERROR);
+ if (!ok) {
+ return false;
+ }
+ DoNextOutput(data, size);
+ return true;
+}
+void GzipInputStream::BackUp(int count) {
+ output_position_ = reinterpret_cast<void*>(
+ reinterpret_cast<uintptr_t>(output_position_) - count);
+}
+bool GzipInputStream::Skip(int count) {
+ const void* data;
+ int size;
+ bool ok = Next(&data, &size);
+ while (ok && (size < count)) {
+ count -= size;
+ ok = Next(&data, &size);
+ }
+ if (size > count) {
+ BackUp(size - count);
+ }
+ return ok;
+}
+int64 GzipInputStream::ByteCount() const {
+ return zcontext_.total_out +
+ (((uintptr_t)zcontext_.next_out) - ((uintptr_t)output_position_));
+}
+
+// =========================================================================
+
+GzipOutputStream::Options::Options()
+ : format(GZIP),
+ buffer_size(kDefaultBufferSize),
+ compression_level(Z_DEFAULT_COMPRESSION),
+ compression_strategy(Z_DEFAULT_STRATEGY) {}
+
+GzipOutputStream::GzipOutputStream(ZeroCopyOutputStream* sub_stream) {
+ Init(sub_stream, Options());
+}
+
+GzipOutputStream::GzipOutputStream(ZeroCopyOutputStream* sub_stream,
+ const Options& options) {
+ Init(sub_stream, options);
+}
+
+void GzipOutputStream::Init(ZeroCopyOutputStream* sub_stream,
+ const Options& options) {
+ sub_stream_ = sub_stream;
+ sub_data_ = NULL;
+ sub_data_size_ = 0;
+
+ input_buffer_length_ = options.buffer_size;
+ input_buffer_ = operator new(input_buffer_length_);
+ GOOGLE_CHECK(input_buffer_ != NULL);
+
+ zcontext_.zalloc = Z_NULL;
+ zcontext_.zfree = Z_NULL;
+ zcontext_.opaque = Z_NULL;
+ zcontext_.next_out = NULL;
+ zcontext_.avail_out = 0;
+ zcontext_.total_out = 0;
+ zcontext_.next_in = NULL;
+ zcontext_.avail_in = 0;
+ zcontext_.total_in = 0;
+ zcontext_.msg = NULL;
+ // default to GZIP format
+ int windowBitsFormat = 16;
+ if (options.format == ZLIB) {
+ windowBitsFormat = 0;
+ }
+ zerror_ = deflateInit2(
+ &zcontext_,
+ options.compression_level,
+ Z_DEFLATED,
+ /* windowBits */15 | windowBitsFormat,
+ /* memLevel (default) */8,
+ options.compression_strategy);
+}
+
+GzipOutputStream::~GzipOutputStream() {
+ Close();
+ if (input_buffer_ != NULL) {
+ operator delete(input_buffer_);
+ }
+}
+
+// private
+int GzipOutputStream::Deflate(int flush) {
+ int error = Z_OK;
+ do {
+ if ((sub_data_ == NULL) || (zcontext_.avail_out == 0)) {
+ bool ok = sub_stream_->Next(&sub_data_, &sub_data_size_);
+ if (!ok) {
+ sub_data_ = NULL;
+ sub_data_size_ = 0;
+ return Z_BUF_ERROR;
+ }
+ GOOGLE_CHECK_GT(sub_data_size_, 0);
+ zcontext_.next_out = static_cast<Bytef*>(sub_data_);
+ zcontext_.avail_out = sub_data_size_;
+ }
+ error = deflate(&zcontext_, flush);
+ } while (error == Z_OK && zcontext_.avail_out == 0);
+ if ((flush == Z_FULL_FLUSH) || (flush == Z_FINISH)) {
+ // Notify lower layer of data.
+ sub_stream_->BackUp(zcontext_.avail_out);
+ // We don't own the buffer anymore.
+ sub_data_ = NULL;
+ sub_data_size_ = 0;
+ }
+ return error;
+}
+
+// implements ZeroCopyOutputStream ---------------------------------
+bool GzipOutputStream::Next(void** data, int* size) {
+ if ((zerror_ != Z_OK) && (zerror_ != Z_BUF_ERROR)) {
+ return false;
+ }
+ if (zcontext_.avail_in != 0) {
+ zerror_ = Deflate(Z_NO_FLUSH);
+ if (zerror_ != Z_OK) {
+ return false;
+ }
+ }
+ if (zcontext_.avail_in == 0) {
+ // all input was consumed. reset the buffer.
+ zcontext_.next_in = static_cast<Bytef*>(input_buffer_);
+ zcontext_.avail_in = input_buffer_length_;
+ *data = input_buffer_;
+ *size = input_buffer_length_;
+ } else {
+ // The loop in Deflate should consume all avail_in
+ GOOGLE_LOG(DFATAL) << "Deflate left bytes unconsumed";
+ }
+ return true;
+}
+void GzipOutputStream::BackUp(int count) {
+ GOOGLE_CHECK_GE(zcontext_.avail_in, count);
+ zcontext_.avail_in -= count;
+}
+int64 GzipOutputStream::ByteCount() const {
+ return zcontext_.total_in + zcontext_.avail_in;
+}
+
+bool GzipOutputStream::Flush() {
+ zerror_ = Deflate(Z_FULL_FLUSH);
+ // Return true if the flush succeeded or if it was a no-op.
+ return (zerror_ == Z_OK) ||
+ (zerror_ == Z_BUF_ERROR && zcontext_.avail_in == 0 &&
+ zcontext_.avail_out != 0);
+}
+
+bool GzipOutputStream::Close() {
+ if ((zerror_ != Z_OK) && (zerror_ != Z_BUF_ERROR)) {
+ return false;
+ }
+ do {
+ zerror_ = Deflate(Z_FINISH);
+ } while (zerror_ == Z_OK);
+ zerror_ = deflateEnd(&zcontext_);
+ bool ok = zerror_ == Z_OK;
+ zerror_ = Z_STREAM_END;
+ return ok;
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
+
+#endif // HAVE_ZLIB
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.h b/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.h
new file mode 100644
index 0000000000..c7ccc260d0
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/gzip_stream.h
@@ -0,0 +1,209 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: brianolson@google.com (Brian Olson)
+//
+// This file contains the definition for classes GzipInputStream and
+// GzipOutputStream.
+//
+// GzipInputStream decompresses data from an underlying
+// ZeroCopyInputStream and provides the decompressed data as a
+// ZeroCopyInputStream.
+//
+// GzipOutputStream is an ZeroCopyOutputStream that compresses data to
+// an underlying ZeroCopyOutputStream.
+
+#ifndef GOOGLE_PROTOBUF_IO_GZIP_STREAM_H__
+#define GOOGLE_PROTOBUF_IO_GZIP_STREAM_H__
+
+#include <zlib.h>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+// A ZeroCopyInputStream that reads compressed data through zlib
+class LIBPROTOBUF_EXPORT GzipInputStream : public ZeroCopyInputStream {
+ public:
+ // Format key for constructor
+ enum Format {
+ // zlib will autodetect gzip header or deflate stream
+ AUTO = 0,
+
+ // GZIP streams have some extra header data for file attributes.
+ GZIP = 1,
+
+ // Simpler zlib stream format.
+ ZLIB = 2,
+ };
+
+ // buffer_size and format may be -1 for default of 64kB and GZIP format
+ explicit GzipInputStream(
+ ZeroCopyInputStream* sub_stream,
+ Format format = AUTO,
+ int buffer_size = -1);
+ virtual ~GzipInputStream();
+
+ // Return last error message or NULL if no error.
+ inline const char* ZlibErrorMessage() const {
+ return zcontext_.msg;
+ }
+ inline int ZlibErrorCode() const {
+ return zerror_;
+ }
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+ private:
+ Format format_;
+
+ ZeroCopyInputStream* sub_stream_;
+
+ z_stream zcontext_;
+ int zerror_;
+
+ void* output_buffer_;
+ void* output_position_;
+ size_t output_buffer_length_;
+
+ int Inflate(int flush);
+ void DoNextOutput(const void** data, int* size);
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(GzipInputStream);
+};
+
+
+class LIBPROTOBUF_EXPORT GzipOutputStream : public ZeroCopyOutputStream {
+ public:
+ // Format key for constructor
+ enum Format {
+ // GZIP streams have some extra header data for file attributes.
+ GZIP = 1,
+
+ // Simpler zlib stream format.
+ ZLIB = 2,
+ };
+
+ struct Options {
+ // Defaults to GZIP.
+ Format format;
+
+ // What size buffer to use internally. Defaults to 64kB.
+ int buffer_size;
+
+ // A number between 0 and 9, where 0 is no compression and 9 is best
+ // compression. Defaults to Z_DEFAULT_COMPRESSION (see zlib.h).
+ int compression_level;
+
+ // Defaults to Z_DEFAULT_STRATEGY. Can also be set to Z_FILTERED,
+ // Z_HUFFMAN_ONLY, or Z_RLE. See the documentation for deflateInit2 in
+ // zlib.h for definitions of these constants.
+ int compression_strategy;
+
+ Options(); // Initializes with default values.
+ };
+
+ // Create a GzipOutputStream with default options.
+ explicit GzipOutputStream(ZeroCopyOutputStream* sub_stream);
+
+ // Create a GzipOutputStream with the given options.
+ GzipOutputStream(
+ ZeroCopyOutputStream* sub_stream,
+ const Options& options);
+
+ virtual ~GzipOutputStream();
+
+ // Return last error message or NULL if no error.
+ inline const char* ZlibErrorMessage() const {
+ return zcontext_.msg;
+ }
+ inline int ZlibErrorCode() const {
+ return zerror_;
+ }
+
+ // Flushes data written so far to zipped data in the underlying stream.
+ // It is the caller's responsibility to flush the underlying stream if
+ // necessary.
+ // Compression may be less efficient stopping and starting around flushes.
+ // Returns true if no error.
+ //
+ // Please ensure that block size is > 6. Here is an excerpt from the zlib
+ // doc that explains why:
+ //
+ // In the case of a Z_FULL_FLUSH or Z_SYNC_FLUSH, make sure that avail_out
+ // is greater than six to avoid repeated flush markers due to
+ // avail_out == 0 on return.
+ bool Flush();
+
+ // Writes out all data and closes the gzip stream.
+ // It is the caller's responsibility to close the underlying stream if
+ // necessary.
+ // Returns true if no error.
+ bool Close();
+
+ // implements ZeroCopyOutputStream ---------------------------------
+ bool Next(void** data, int* size);
+ void BackUp(int count);
+ int64 ByteCount() const;
+
+ private:
+ ZeroCopyOutputStream* sub_stream_;
+ // Result from calling Next() on sub_stream_
+ void* sub_data_;
+ int sub_data_size_;
+
+ z_stream zcontext_;
+ int zerror_;
+ void* input_buffer_;
+ size_t input_buffer_length_;
+
+ // Shared constructor code.
+ void Init(ZeroCopyOutputStream* sub_stream, const Options& options);
+
+ // Do some compression.
+ // Takes zlib flush mode.
+ // Returns zlib error code.
+ int Deflate(int flush);
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(GzipOutputStream);
+};
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_GZIP_STREAM_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/package_info.h b/toolkit/components/protobuf/src/google/protobuf/io/package_info.h
new file mode 100644
index 0000000000..dc1fc91e5b
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/package_info.h
@@ -0,0 +1,54 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file exists solely to document the google::protobuf::io namespace.
+// It is not compiled into anything, but it may be read by an automated
+// documentation generator.
+
+namespace google {
+
+namespace protobuf {
+
+// Auxiliary classes used for I/O.
+//
+// The Protocol Buffer library uses the classes in this package to deal with
+// I/O and encoding/decoding raw bytes. Most users will not need to
+// deal with this package. However, users who want to adapt the system to
+// work with their own I/O abstractions -- e.g., to allow Protocol Buffers
+// to be read from a different kind of input stream without the need for a
+// temporary buffer -- should take a closer look.
+namespace io {}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/printer.cc b/toolkit/components/protobuf/src/google/protobuf/io/printer.cc
new file mode 100644
index 0000000000..c8df41778e
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/printer.cc
@@ -0,0 +1,198 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/io/printer.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+Printer::Printer(ZeroCopyOutputStream* output, char variable_delimiter)
+ : variable_delimiter_(variable_delimiter),
+ output_(output),
+ buffer_(NULL),
+ buffer_size_(0),
+ at_start_of_line_(true),
+ failed_(false) {
+}
+
+Printer::~Printer() {
+ // Only BackUp() if we have called Next() at least once and never failed.
+ if (buffer_size_ > 0 && !failed_) {
+ output_->BackUp(buffer_size_);
+ }
+}
+
+void Printer::Print(const map<string, string>& variables, const char* text) {
+ int size = strlen(text);
+ int pos = 0; // The number of bytes we've written so far.
+
+ for (int i = 0; i < size; i++) {
+ if (text[i] == '\n') {
+ // Saw newline. If there is more text, we may need to insert an indent
+ // here. So, write what we have so far, including the '\n'.
+ WriteRaw(text + pos, i - pos + 1);
+ pos = i + 1;
+
+ // Setting this true will cause the next WriteRaw() to insert an indent
+ // first.
+ at_start_of_line_ = true;
+
+ } else if (text[i] == variable_delimiter_) {
+ // Saw the start of a variable name.
+
+ // Write what we have so far.
+ WriteRaw(text + pos, i - pos);
+ pos = i + 1;
+
+ // Find closing delimiter.
+ const char* end = strchr(text + pos, variable_delimiter_);
+ if (end == NULL) {
+ GOOGLE_LOG(DFATAL) << " Unclosed variable name.";
+ end = text + pos;
+ }
+ int endpos = end - text;
+
+ string varname(text + pos, endpos - pos);
+ if (varname.empty()) {
+ // Two delimiters in a row reduce to a literal delimiter character.
+ WriteRaw(&variable_delimiter_, 1);
+ } else {
+ // Replace with the variable's value.
+ map<string, string>::const_iterator iter = variables.find(varname);
+ if (iter == variables.end()) {
+ GOOGLE_LOG(DFATAL) << " Undefined variable: " << varname;
+ } else {
+ WriteRaw(iter->second.data(), iter->second.size());
+ }
+ }
+
+ // Advance past this variable.
+ i = endpos;
+ pos = endpos + 1;
+ }
+ }
+
+ // Write the rest.
+ WriteRaw(text + pos, size - pos);
+}
+
+void Printer::Print(const char* text) {
+ static map<string, string> empty;
+ Print(empty, text);
+}
+
+void Printer::Print(const char* text,
+ const char* variable, const string& value) {
+ map<string, string> vars;
+ vars[variable] = value;
+ Print(vars, text);
+}
+
+void Printer::Print(const char* text,
+ const char* variable1, const string& value1,
+ const char* variable2, const string& value2) {
+ map<string, string> vars;
+ vars[variable1] = value1;
+ vars[variable2] = value2;
+ Print(vars, text);
+}
+
+void Printer::Print(const char* text,
+ const char* variable1, const string& value1,
+ const char* variable2, const string& value2,
+ const char* variable3, const string& value3) {
+ map<string, string> vars;
+ vars[variable1] = value1;
+ vars[variable2] = value2;
+ vars[variable3] = value3;
+ Print(vars, text);
+}
+
+void Printer::Indent() {
+ indent_ += " ";
+}
+
+void Printer::Outdent() {
+ if (indent_.empty()) {
+ GOOGLE_LOG(DFATAL) << " Outdent() without matching Indent().";
+ return;
+ }
+
+ indent_.resize(indent_.size() - 2);
+}
+
+void Printer::PrintRaw(const string& data) {
+ WriteRaw(data.data(), data.size());
+}
+
+void Printer::PrintRaw(const char* data) {
+ if (failed_) return;
+ WriteRaw(data, strlen(data));
+}
+
+void Printer::WriteRaw(const char* data, int size) {
+ if (failed_) return;
+ if (size == 0) return;
+
+ if (at_start_of_line_ && (size > 0) && (data[0] != '\n')) {
+ // Insert an indent.
+ at_start_of_line_ = false;
+ WriteRaw(indent_.data(), indent_.size());
+ if (failed_) return;
+ }
+
+ while (size > buffer_size_) {
+ // Data exceeds space in the buffer. Copy what we can and request a
+ // new buffer.
+ memcpy(buffer_, data, buffer_size_);
+ data += buffer_size_;
+ size -= buffer_size_;
+ void* void_buffer;
+ failed_ = !output_->Next(&void_buffer, &buffer_size_);
+ if (failed_) return;
+ buffer_ = reinterpret_cast<char*>(void_buffer);
+ }
+
+ // Buffer is big enough to receive the data; copy it.
+ memcpy(buffer_, data, size);
+ buffer_ += size;
+ buffer_size_ -= size;
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/printer.h b/toolkit/components/protobuf/src/google/protobuf/io/printer.h
new file mode 100644
index 0000000000..f06cbf2f0c
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/printer.h
@@ -0,0 +1,136 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Utility class for writing text to a ZeroCopyOutputStream.
+
+#ifndef GOOGLE_PROTOBUF_IO_PRINTER_H__
+#define GOOGLE_PROTOBUF_IO_PRINTER_H__
+
+#include <string>
+#include <map>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+class ZeroCopyOutputStream; // zero_copy_stream.h
+
+// This simple utility class assists in code generation. It basically
+// allows the caller to define a set of variables and then output some
+// text with variable substitutions. Example usage:
+//
+// Printer printer(output, '$');
+// map<string, string> vars;
+// vars["name"] = "Bob";
+// printer.Print(vars, "My name is $name$.");
+//
+// The above writes "My name is Bob." to the output stream.
+//
+// Printer aggressively enforces correct usage, crashing (with assert failures)
+// in the case of undefined variables in debug builds. This helps greatly in
+// debugging code which uses it.
+class LIBPROTOBUF_EXPORT Printer {
+ public:
+ // Create a printer that writes text to the given output stream. Use the
+ // given character as the delimiter for variables.
+ Printer(ZeroCopyOutputStream* output, char variable_delimiter);
+ ~Printer();
+
+ // Print some text after applying variable substitutions. If a particular
+ // variable in the text is not defined, this will crash. Variables to be
+ // substituted are identified by their names surrounded by delimiter
+ // characters (as given to the constructor). The variable bindings are
+ // defined by the given map.
+ void Print(const map<string, string>& variables, const char* text);
+
+ // Like the first Print(), except the substitutions are given as parameters.
+ void Print(const char* text);
+ // Like the first Print(), except the substitutions are given as parameters.
+ void Print(const char* text, const char* variable, const string& value);
+ // Like the first Print(), except the substitutions are given as parameters.
+ void Print(const char* text, const char* variable1, const string& value1,
+ const char* variable2, const string& value2);
+ // Like the first Print(), except the substitutions are given as parameters.
+ void Print(const char* text, const char* variable1, const string& value1,
+ const char* variable2, const string& value2,
+ const char* variable3, const string& value3);
+ // TODO(kenton): Overloaded versions with more variables? Three seems
+ // to be enough.
+
+ // Indent text by two spaces. After calling Indent(), two spaces will be
+ // inserted at the beginning of each line of text. Indent() may be called
+ // multiple times to produce deeper indents.
+ void Indent();
+
+ // Reduces the current indent level by two spaces, or crashes if the indent
+ // level is zero.
+ void Outdent();
+
+ // Write a string to the output buffer.
+ // This method does not look for newlines to add indentation.
+ void PrintRaw(const string& data);
+
+ // Write a zero-delimited string to output buffer.
+ // This method does not look for newlines to add indentation.
+ void PrintRaw(const char* data);
+
+ // Write some bytes to the output buffer.
+ // This method does not look for newlines to add indentation.
+ void WriteRaw(const char* data, int size);
+
+ // True if any write to the underlying stream failed. (We don't just
+ // crash in this case because this is an I/O failure, not a programming
+ // error.)
+ bool failed() const { return failed_; }
+
+ private:
+ const char variable_delimiter_;
+
+ ZeroCopyOutputStream* const output_;
+ char* buffer_;
+ int buffer_size_;
+
+ string indent_;
+ bool at_start_of_line_;
+ bool failed_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Printer);
+};
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_PRINTER_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/strtod.cc b/toolkit/components/protobuf/src/google/protobuf/io/strtod.cc
new file mode 100644
index 0000000000..56973439d9
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/strtod.cc
@@ -0,0 +1,113 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <google/protobuf/io/strtod.h>
+
+#include <cstdio>
+#include <cstring>
+#include <string>
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+// ----------------------------------------------------------------------
+// NoLocaleStrtod()
+// This code will make you cry.
+// ----------------------------------------------------------------------
+
+namespace {
+
+// Returns a string identical to *input except that the character pointed to
+// by radix_pos (which should be '.') is replaced with the locale-specific
+// radix character.
+string LocalizeRadix(const char* input, const char* radix_pos) {
+ // Determine the locale-specific radix character by calling sprintf() to
+ // print the number 1.5, then stripping off the digits. As far as I can
+ // tell, this is the only portable, thread-safe way to get the C library
+ // to divuldge the locale's radix character. No, localeconv() is NOT
+ // thread-safe.
+ char temp[16];
+ int size = sprintf(temp, "%.1f", 1.5);
+ GOOGLE_CHECK_EQ(temp[0], '1');
+ GOOGLE_CHECK_EQ(temp[size-1], '5');
+ GOOGLE_CHECK_LE(size, 6);
+
+ // Now replace the '.' in the input with it.
+ string result;
+ result.reserve(strlen(input) + size - 3);
+ result.append(input, radix_pos);
+ result.append(temp + 1, size - 2);
+ result.append(radix_pos + 1);
+ return result;
+}
+
+} // namespace
+
+double NoLocaleStrtod(const char* text, char** original_endptr) {
+ // We cannot simply set the locale to "C" temporarily with setlocale()
+ // as this is not thread-safe. Instead, we try to parse in the current
+ // locale first. If parsing stops at a '.' character, then this is a
+ // pretty good hint that we're actually in some other locale in which
+ // '.' is not the radix character.
+
+ char* temp_endptr;
+ double result = strtod(text, &temp_endptr);
+ if (original_endptr != NULL) *original_endptr = temp_endptr;
+ if (*temp_endptr != '.') return result;
+
+ // Parsing halted on a '.'. Perhaps we're in a different locale? Let's
+ // try to replace the '.' with a locale-specific radix character and
+ // try again.
+ string localized = LocalizeRadix(text, temp_endptr);
+ const char* localized_cstr = localized.c_str();
+ char* localized_endptr;
+ result = strtod(localized_cstr, &localized_endptr);
+ if ((localized_endptr - localized_cstr) >
+ (temp_endptr - text)) {
+ // This attempt got further, so replacing the decimal must have helped.
+ // Update original_endptr to point at the right location.
+ if (original_endptr != NULL) {
+ // size_diff is non-zero if the localized radix has multiple bytes.
+ int size_diff = localized.size() - strlen(text);
+ // const_cast is necessary to match the strtod() interface.
+ *original_endptr = const_cast<char*>(
+ text + (localized_endptr - localized_cstr - size_diff));
+ }
+ }
+
+ return result;
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/strtod.h b/toolkit/components/protobuf/src/google/protobuf/io/strtod.h
new file mode 100644
index 0000000000..c2efc8d3e4
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/strtod.h
@@ -0,0 +1,50 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// A locale-independent version of strtod(), used to parse floating
+// point default values in .proto files, where the decimal separator
+// is always a dot.
+
+#ifndef GOOGLE_PROTOBUF_IO_STRTOD_H__
+#define GOOGLE_PROTOBUF_IO_STRTOD_H__
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+// A locale-independent version of the standard strtod(), which always
+// uses a dot as the decimal separator.
+double NoLocaleStrtod(const char* str, char** endptr);
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_STRTOD_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/tokenizer.cc b/toolkit/components/protobuf/src/google/protobuf/io/tokenizer.cc
new file mode 100644
index 0000000000..ef2de300bf
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/tokenizer.cc
@@ -0,0 +1,1127 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Here we have a hand-written lexer. At first you might ask yourself,
+// "Hand-written text processing? Is Kenton crazy?!" Well, first of all,
+// yes I am crazy, but that's beside the point. There are actually reasons
+// why I ended up writing this this way.
+//
+// The traditional approach to lexing is to use lex to generate a lexer for
+// you. Unfortunately, lex's output is ridiculously ugly and difficult to
+// integrate cleanly with C++ code, especially abstract code or code meant
+// as a library. Better parser-generators exist but would add dependencies
+// which most users won't already have, which we'd like to avoid. (GNU flex
+// has a C++ output option, but it's still ridiculously ugly, non-abstract,
+// and not library-friendly.)
+//
+// The next approach that any good software engineer should look at is to
+// use regular expressions. And, indeed, I did. I have code which
+// implements this same class using regular expressions. It's about 200
+// lines shorter. However:
+// - Rather than error messages telling you "This string has an invalid
+// escape sequence at line 5, column 45", you get error messages like
+// "Parse error on line 5". Giving more precise errors requires adding
+// a lot of code that ends up basically as complex as the hand-coded
+// version anyway.
+// - The regular expression to match a string literal looks like this:
+// kString = new RE("(\"([^\"\\\\]|" // non-escaped
+// "\\\\[abfnrtv?\"'\\\\0-7]|" // normal escape
+// "\\\\x[0-9a-fA-F])*\"|" // hex escape
+// "\'([^\'\\\\]|" // Also support single-quotes.
+// "\\\\[abfnrtv?\"'\\\\0-7]|"
+// "\\\\x[0-9a-fA-F])*\')");
+// Verifying the correctness of this line noise is actually harder than
+// verifying the correctness of ConsumeString(), defined below. I'm not
+// even confident that the above is correct, after staring at it for some
+// time.
+// - PCRE is fast, but there's still more overhead involved than the code
+// below.
+// - Sadly, regular expressions are not part of the C standard library, so
+// using them would require depending on some other library. For the
+// open source release, this could be really annoying. Nobody likes
+// downloading one piece of software just to find that they need to
+// download something else to make it work, and in all likelihood
+// people downloading Protocol Buffers will already be doing so just
+// to make something else work. We could include a copy of PCRE with
+// our code, but that obligates us to keep it up-to-date and just seems
+// like a big waste just to save 200 lines of code.
+//
+// On a similar but unrelated note, I'm even scared to use ctype.h.
+// Apparently functions like isalpha() are locale-dependent. So, if we used
+// that, then if this code is being called from some program that doesn't
+// have its locale set to "C", it would behave strangely. We can't just set
+// the locale to "C" ourselves since we might break the calling program that
+// way, particularly if it is multi-threaded. WTF? Someone please let me
+// (Kenton) know if I'm missing something here...
+//
+// I'd love to hear about other alternatives, though, as this code isn't
+// exactly pretty.
+
+#include <google/protobuf/io/tokenizer.h>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/stringprintf.h>
+#include <google/protobuf/io/strtod.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/stubs/strutil.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+namespace {
+
+// As mentioned above, I don't trust ctype.h due to the presence of "locales".
+// So, I have written replacement functions here. Someone please smack me if
+// this is a bad idea or if there is some way around this.
+//
+// These "character classes" are designed to be used in template methods.
+// For instance, Tokenizer::ConsumeZeroOrMore<Whitespace>() will eat
+// whitespace.
+
+// Note: No class is allowed to contain '\0', since this is used to mark end-
+// of-input and is handled specially.
+
+#define CHARACTER_CLASS(NAME, EXPRESSION) \
+ class NAME { \
+ public: \
+ static inline bool InClass(char c) { \
+ return EXPRESSION; \
+ } \
+ }
+
+CHARACTER_CLASS(Whitespace, c == ' ' || c == '\n' || c == '\t' ||
+ c == '\r' || c == '\v' || c == '\f');
+CHARACTER_CLASS(WhitespaceNoNewline, c == ' ' || c == '\t' ||
+ c == '\r' || c == '\v' || c == '\f');
+
+CHARACTER_CLASS(Unprintable, c < ' ' && c > '\0');
+
+CHARACTER_CLASS(Digit, '0' <= c && c <= '9');
+CHARACTER_CLASS(OctalDigit, '0' <= c && c <= '7');
+CHARACTER_CLASS(HexDigit, ('0' <= c && c <= '9') ||
+ ('a' <= c && c <= 'f') ||
+ ('A' <= c && c <= 'F'));
+
+CHARACTER_CLASS(Letter, ('a' <= c && c <= 'z') ||
+ ('A' <= c && c <= 'Z') ||
+ (c == '_'));
+
+CHARACTER_CLASS(Alphanumeric, ('a' <= c && c <= 'z') ||
+ ('A' <= c && c <= 'Z') ||
+ ('0' <= c && c <= '9') ||
+ (c == '_'));
+
+CHARACTER_CLASS(Escape, c == 'a' || c == 'b' || c == 'f' || c == 'n' ||
+ c == 'r' || c == 't' || c == 'v' || c == '\\' ||
+ c == '?' || c == '\'' || c == '\"');
+
+#undef CHARACTER_CLASS
+
+// Given a char, interpret it as a numeric digit and return its value.
+// This supports any number base up to 36.
+inline int DigitValue(char digit) {
+ if ('0' <= digit && digit <= '9') return digit - '0';
+ if ('a' <= digit && digit <= 'z') return digit - 'a' + 10;
+ if ('A' <= digit && digit <= 'Z') return digit - 'A' + 10;
+ return -1;
+}
+
+// Inline because it's only used in one place.
+inline char TranslateEscape(char c) {
+ switch (c) {
+ case 'a': return '\a';
+ case 'b': return '\b';
+ case 'f': return '\f';
+ case 'n': return '\n';
+ case 'r': return '\r';
+ case 't': return '\t';
+ case 'v': return '\v';
+ case '\\': return '\\';
+ case '?': return '\?'; // Trigraphs = :(
+ case '\'': return '\'';
+ case '"': return '\"';
+
+ // We expect escape sequences to have been validated separately.
+ default: return '?';
+ }
+}
+
+} // anonymous namespace
+
+ErrorCollector::~ErrorCollector() {}
+
+// ===================================================================
+
+Tokenizer::Tokenizer(ZeroCopyInputStream* input,
+ ErrorCollector* error_collector)
+ : input_(input),
+ error_collector_(error_collector),
+ buffer_(NULL),
+ buffer_size_(0),
+ buffer_pos_(0),
+ read_error_(false),
+ line_(0),
+ column_(0),
+ record_target_(NULL),
+ record_start_(-1),
+ allow_f_after_float_(false),
+ comment_style_(CPP_COMMENT_STYLE),
+ require_space_after_number_(true),
+ allow_multiline_strings_(false) {
+
+ current_.line = 0;
+ current_.column = 0;
+ current_.end_column = 0;
+ current_.type = TYPE_START;
+
+ Refresh();
+}
+
+Tokenizer::~Tokenizer() {
+ // If we had any buffer left unread, return it to the underlying stream
+ // so that someone else can read it.
+ if (buffer_size_ > buffer_pos_) {
+ input_->BackUp(buffer_size_ - buffer_pos_);
+ }
+}
+
+// -------------------------------------------------------------------
+// Internal helpers.
+
+void Tokenizer::NextChar() {
+ // Update our line and column counters based on the character being
+ // consumed.
+ if (current_char_ == '\n') {
+ ++line_;
+ column_ = 0;
+ } else if (current_char_ == '\t') {
+ column_ += kTabWidth - column_ % kTabWidth;
+ } else {
+ ++column_;
+ }
+
+ // Advance to the next character.
+ ++buffer_pos_;
+ if (buffer_pos_ < buffer_size_) {
+ current_char_ = buffer_[buffer_pos_];
+ } else {
+ Refresh();
+ }
+}
+
+void Tokenizer::Refresh() {
+ if (read_error_) {
+ current_char_ = '\0';
+ return;
+ }
+
+ // If we're in a token, append the rest of the buffer to it.
+ if (record_target_ != NULL && record_start_ < buffer_size_) {
+ record_target_->append(buffer_ + record_start_, buffer_size_ - record_start_);
+ record_start_ = 0;
+ }
+
+ const void* data = NULL;
+ buffer_ = NULL;
+ buffer_pos_ = 0;
+ do {
+ if (!input_->Next(&data, &buffer_size_)) {
+ // end of stream (or read error)
+ buffer_size_ = 0;
+ read_error_ = true;
+ current_char_ = '\0';
+ return;
+ }
+ } while (buffer_size_ == 0);
+
+ buffer_ = static_cast<const char*>(data);
+
+ current_char_ = buffer_[0];
+}
+
+inline void Tokenizer::RecordTo(string* target) {
+ record_target_ = target;
+ record_start_ = buffer_pos_;
+}
+
+inline void Tokenizer::StopRecording() {
+ // Note: The if() is necessary because some STL implementations crash when
+ // you call string::append(NULL, 0), presumably because they are trying to
+ // be helpful by detecting the NULL pointer, even though there's nothing
+ // wrong with reading zero bytes from NULL.
+ if (buffer_pos_ != record_start_) {
+ record_target_->append(buffer_ + record_start_, buffer_pos_ - record_start_);
+ }
+ record_target_ = NULL;
+ record_start_ = -1;
+}
+
+inline void Tokenizer::StartToken() {
+ current_.type = TYPE_START; // Just for the sake of initializing it.
+ current_.text.clear();
+ current_.line = line_;
+ current_.column = column_;
+ RecordTo(&current_.text);
+}
+
+inline void Tokenizer::EndToken() {
+ StopRecording();
+ current_.end_column = column_;
+}
+
+// -------------------------------------------------------------------
+// Helper methods that consume characters.
+
+template<typename CharacterClass>
+inline bool Tokenizer::LookingAt() {
+ return CharacterClass::InClass(current_char_);
+}
+
+template<typename CharacterClass>
+inline bool Tokenizer::TryConsumeOne() {
+ if (CharacterClass::InClass(current_char_)) {
+ NextChar();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+inline bool Tokenizer::TryConsume(char c) {
+ if (current_char_ == c) {
+ NextChar();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+template<typename CharacterClass>
+inline void Tokenizer::ConsumeZeroOrMore() {
+ while (CharacterClass::InClass(current_char_)) {
+ NextChar();
+ }
+}
+
+template<typename CharacterClass>
+inline void Tokenizer::ConsumeOneOrMore(const char* error) {
+ if (!CharacterClass::InClass(current_char_)) {
+ AddError(error);
+ } else {
+ do {
+ NextChar();
+ } while (CharacterClass::InClass(current_char_));
+ }
+}
+
+// -------------------------------------------------------------------
+// Methods that read whole patterns matching certain kinds of tokens
+// or comments.
+
+void Tokenizer::ConsumeString(char delimiter) {
+ while (true) {
+ switch (current_char_) {
+ case '\0':
+ AddError("Unexpected end of string.");
+ return;
+
+ case '\n': {
+ if (!allow_multiline_strings_) {
+ AddError("String literals cannot cross line boundaries.");
+ return;
+ }
+ NextChar();
+ break;
+ }
+
+ case '\\': {
+ // An escape sequence.
+ NextChar();
+ if (TryConsumeOne<Escape>()) {
+ // Valid escape sequence.
+ } else if (TryConsumeOne<OctalDigit>()) {
+ // Possibly followed by two more octal digits, but these will
+ // just be consumed by the main loop anyway so we don't need
+ // to do so explicitly here.
+ } else if (TryConsume('x') || TryConsume('X')) {
+ if (!TryConsumeOne<HexDigit>()) {
+ AddError("Expected hex digits for escape sequence.");
+ }
+ // Possibly followed by another hex digit, but again we don't care.
+ } else if (TryConsume('u')) {
+ if (!TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>()) {
+ AddError("Expected four hex digits for \\u escape sequence.");
+ }
+ } else if (TryConsume('U')) {
+ // We expect 8 hex digits; but only the range up to 0x10ffff is
+ // legal.
+ if (!TryConsume('0') ||
+ !TryConsume('0') ||
+ !(TryConsume('0') || TryConsume('1')) ||
+ !TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>() ||
+ !TryConsumeOne<HexDigit>()) {
+ AddError("Expected eight hex digits up to 10ffff for \\U escape "
+ "sequence");
+ }
+ } else {
+ AddError("Invalid escape sequence in string literal.");
+ }
+ break;
+ }
+
+ default: {
+ if (current_char_ == delimiter) {
+ NextChar();
+ return;
+ }
+ NextChar();
+ break;
+ }
+ }
+ }
+}
+
+Tokenizer::TokenType Tokenizer::ConsumeNumber(bool started_with_zero,
+ bool started_with_dot) {
+ bool is_float = false;
+
+ if (started_with_zero && (TryConsume('x') || TryConsume('X'))) {
+ // A hex number (started with "0x").
+ ConsumeOneOrMore<HexDigit>("\"0x\" must be followed by hex digits.");
+
+ } else if (started_with_zero && LookingAt<Digit>()) {
+ // An octal number (had a leading zero).
+ ConsumeZeroOrMore<OctalDigit>();
+ if (LookingAt<Digit>()) {
+ AddError("Numbers starting with leading zero must be in octal.");
+ ConsumeZeroOrMore<Digit>();
+ }
+
+ } else {
+ // A decimal number.
+ if (started_with_dot) {
+ is_float = true;
+ ConsumeZeroOrMore<Digit>();
+ } else {
+ ConsumeZeroOrMore<Digit>();
+
+ if (TryConsume('.')) {
+ is_float = true;
+ ConsumeZeroOrMore<Digit>();
+ }
+ }
+
+ if (TryConsume('e') || TryConsume('E')) {
+ is_float = true;
+ TryConsume('-') || TryConsume('+');
+ ConsumeOneOrMore<Digit>("\"e\" must be followed by exponent.");
+ }
+
+ if (allow_f_after_float_ && (TryConsume('f') || TryConsume('F'))) {
+ is_float = true;
+ }
+ }
+
+ if (LookingAt<Letter>() && require_space_after_number_) {
+ AddError("Need space between number and identifier.");
+ } else if (current_char_ == '.') {
+ if (is_float) {
+ AddError(
+ "Already saw decimal point or exponent; can't have another one.");
+ } else {
+ AddError("Hex and octal numbers must be integers.");
+ }
+ }
+
+ return is_float ? TYPE_FLOAT : TYPE_INTEGER;
+}
+
+void Tokenizer::ConsumeLineComment(string* content) {
+ if (content != NULL) RecordTo(content);
+
+ while (current_char_ != '\0' && current_char_ != '\n') {
+ NextChar();
+ }
+ TryConsume('\n');
+
+ if (content != NULL) StopRecording();
+}
+
+void Tokenizer::ConsumeBlockComment(string* content) {
+ int start_line = line_;
+ int start_column = column_ - 2;
+
+ if (content != NULL) RecordTo(content);
+
+ while (true) {
+ while (current_char_ != '\0' &&
+ current_char_ != '*' &&
+ current_char_ != '/' &&
+ current_char_ != '\n') {
+ NextChar();
+ }
+
+ if (TryConsume('\n')) {
+ if (content != NULL) StopRecording();
+
+ // Consume leading whitespace and asterisk;
+ ConsumeZeroOrMore<WhitespaceNoNewline>();
+ if (TryConsume('*')) {
+ if (TryConsume('/')) {
+ // End of comment.
+ break;
+ }
+ }
+
+ if (content != NULL) RecordTo(content);
+ } else if (TryConsume('*') && TryConsume('/')) {
+ // End of comment.
+ if (content != NULL) {
+ StopRecording();
+ // Strip trailing "*/".
+ content->erase(content->size() - 2);
+ }
+ break;
+ } else if (TryConsume('/') && current_char_ == '*') {
+ // Note: We didn't consume the '*' because if there is a '/' after it
+ // we want to interpret that as the end of the comment.
+ AddError(
+ "\"/*\" inside block comment. Block comments cannot be nested.");
+ } else if (current_char_ == '\0') {
+ AddError("End-of-file inside block comment.");
+ error_collector_->AddError(
+ start_line, start_column, " Comment started here.");
+ if (content != NULL) StopRecording();
+ break;
+ }
+ }
+}
+
+Tokenizer::NextCommentStatus Tokenizer::TryConsumeCommentStart() {
+ if (comment_style_ == CPP_COMMENT_STYLE && TryConsume('/')) {
+ if (TryConsume('/')) {
+ return LINE_COMMENT;
+ } else if (TryConsume('*')) {
+ return BLOCK_COMMENT;
+ } else {
+ // Oops, it was just a slash. Return it.
+ current_.type = TYPE_SYMBOL;
+ current_.text = "/";
+ current_.line = line_;
+ current_.column = column_ - 1;
+ current_.end_column = column_;
+ return SLASH_NOT_COMMENT;
+ }
+ } else if (comment_style_ == SH_COMMENT_STYLE && TryConsume('#')) {
+ return LINE_COMMENT;
+ } else {
+ return NO_COMMENT;
+ }
+}
+
+// -------------------------------------------------------------------
+
+bool Tokenizer::Next() {
+ previous_ = current_;
+
+ while (!read_error_) {
+ ConsumeZeroOrMore<Whitespace>();
+
+ switch (TryConsumeCommentStart()) {
+ case LINE_COMMENT:
+ ConsumeLineComment(NULL);
+ continue;
+ case BLOCK_COMMENT:
+ ConsumeBlockComment(NULL);
+ continue;
+ case SLASH_NOT_COMMENT:
+ return true;
+ case NO_COMMENT:
+ break;
+ }
+
+ // Check for EOF before continuing.
+ if (read_error_) break;
+
+ if (LookingAt<Unprintable>() || current_char_ == '\0') {
+ AddError("Invalid control characters encountered in text.");
+ NextChar();
+ // Skip more unprintable characters, too. But, remember that '\0' is
+ // also what current_char_ is set to after EOF / read error. We have
+ // to be careful not to go into an infinite loop of trying to consume
+ // it, so make sure to check read_error_ explicitly before consuming
+ // '\0'.
+ while (TryConsumeOne<Unprintable>() ||
+ (!read_error_ && TryConsume('\0'))) {
+ // Ignore.
+ }
+
+ } else {
+ // Reading some sort of token.
+ StartToken();
+
+ if (TryConsumeOne<Letter>()) {
+ ConsumeZeroOrMore<Alphanumeric>();
+ current_.type = TYPE_IDENTIFIER;
+ } else if (TryConsume('0')) {
+ current_.type = ConsumeNumber(true, false);
+ } else if (TryConsume('.')) {
+ // This could be the beginning of a floating-point number, or it could
+ // just be a '.' symbol.
+
+ if (TryConsumeOne<Digit>()) {
+ // It's a floating-point number.
+ if (previous_.type == TYPE_IDENTIFIER &&
+ current_.line == previous_.line &&
+ current_.column == previous_.end_column) {
+ // We don't accept syntax like "blah.123".
+ error_collector_->AddError(line_, column_ - 2,
+ "Need space between identifier and decimal point.");
+ }
+ current_.type = ConsumeNumber(false, true);
+ } else {
+ current_.type = TYPE_SYMBOL;
+ }
+ } else if (TryConsumeOne<Digit>()) {
+ current_.type = ConsumeNumber(false, false);
+ } else if (TryConsume('\"')) {
+ ConsumeString('\"');
+ current_.type = TYPE_STRING;
+ } else if (TryConsume('\'')) {
+ ConsumeString('\'');
+ current_.type = TYPE_STRING;
+ } else {
+ // Check if the high order bit is set.
+ if (current_char_ & 0x80) {
+ error_collector_->AddError(line_, column_,
+ StringPrintf("Interpreting non ascii codepoint %d.",
+ static_cast<unsigned char>(current_char_)));
+ }
+ NextChar();
+ current_.type = TYPE_SYMBOL;
+ }
+
+ EndToken();
+ return true;
+ }
+ }
+
+ // EOF
+ current_.type = TYPE_END;
+ current_.text.clear();
+ current_.line = line_;
+ current_.column = column_;
+ current_.end_column = column_;
+ return false;
+}
+
+namespace {
+
+// Helper class for collecting comments and putting them in the right places.
+//
+// This basically just buffers the most recent comment until it can be decided
+// exactly where that comment should be placed. When Flush() is called, the
+// current comment goes into either prev_trailing_comments or detached_comments.
+// When the CommentCollector is destroyed, the last buffered comment goes into
+// next_leading_comments.
+class CommentCollector {
+ public:
+ CommentCollector(string* prev_trailing_comments,
+ vector<string>* detached_comments,
+ string* next_leading_comments)
+ : prev_trailing_comments_(prev_trailing_comments),
+ detached_comments_(detached_comments),
+ next_leading_comments_(next_leading_comments),
+ has_comment_(false),
+ is_line_comment_(false),
+ can_attach_to_prev_(true) {
+ if (prev_trailing_comments != NULL) prev_trailing_comments->clear();
+ if (detached_comments != NULL) detached_comments->clear();
+ if (next_leading_comments != NULL) next_leading_comments->clear();
+ }
+
+ ~CommentCollector() {
+ // Whatever is in the buffer is a leading comment.
+ if (next_leading_comments_ != NULL && has_comment_) {
+ comment_buffer_.swap(*next_leading_comments_);
+ }
+ }
+
+ // About to read a line comment. Get the comment buffer pointer in order to
+ // read into it.
+ string* GetBufferForLineComment() {
+ // We want to combine with previous line comments, but not block comments.
+ if (has_comment_ && !is_line_comment_) {
+ Flush();
+ }
+ has_comment_ = true;
+ is_line_comment_ = true;
+ return &comment_buffer_;
+ }
+
+ // About to read a block comment. Get the comment buffer pointer in order to
+ // read into it.
+ string* GetBufferForBlockComment() {
+ if (has_comment_) {
+ Flush();
+ }
+ has_comment_ = true;
+ is_line_comment_ = false;
+ return &comment_buffer_;
+ }
+
+ void ClearBuffer() {
+ comment_buffer_.clear();
+ has_comment_ = false;
+ }
+
+ // Called once we know that the comment buffer is complete and is *not*
+ // connected to the next token.
+ void Flush() {
+ if (has_comment_) {
+ if (can_attach_to_prev_) {
+ if (prev_trailing_comments_ != NULL) {
+ prev_trailing_comments_->append(comment_buffer_);
+ }
+ can_attach_to_prev_ = false;
+ } else {
+ if (detached_comments_ != NULL) {
+ detached_comments_->push_back(comment_buffer_);
+ }
+ }
+ ClearBuffer();
+ }
+ }
+
+ void DetachFromPrev() {
+ can_attach_to_prev_ = false;
+ }
+
+ private:
+ string* prev_trailing_comments_;
+ vector<string>* detached_comments_;
+ string* next_leading_comments_;
+
+ string comment_buffer_;
+
+ // True if any comments were read into comment_buffer_. This can be true even
+ // if comment_buffer_ is empty, namely if the comment was "/**/".
+ bool has_comment_;
+
+ // Is the comment in the comment buffer a line comment?
+ bool is_line_comment_;
+
+ // Is it still possible that we could be reading a comment attached to the
+ // previous token?
+ bool can_attach_to_prev_;
+};
+
+} // namespace
+
+bool Tokenizer::NextWithComments(string* prev_trailing_comments,
+ vector<string>* detached_comments,
+ string* next_leading_comments) {
+ CommentCollector collector(prev_trailing_comments, detached_comments,
+ next_leading_comments);
+
+ if (current_.type == TYPE_START) {
+ collector.DetachFromPrev();
+ } else {
+ // A comment appearing on the same line must be attached to the previous
+ // declaration.
+ ConsumeZeroOrMore<WhitespaceNoNewline>();
+ switch (TryConsumeCommentStart()) {
+ case LINE_COMMENT:
+ ConsumeLineComment(collector.GetBufferForLineComment());
+
+ // Don't allow comments on subsequent lines to be attached to a trailing
+ // comment.
+ collector.Flush();
+ break;
+ case BLOCK_COMMENT:
+ ConsumeBlockComment(collector.GetBufferForBlockComment());
+
+ ConsumeZeroOrMore<WhitespaceNoNewline>();
+ if (!TryConsume('\n')) {
+ // Oops, the next token is on the same line. If we recorded a comment
+ // we really have no idea which token it should be attached to.
+ collector.ClearBuffer();
+ return Next();
+ }
+
+ // Don't allow comments on subsequent lines to be attached to a trailing
+ // comment.
+ collector.Flush();
+ break;
+ case SLASH_NOT_COMMENT:
+ return true;
+ case NO_COMMENT:
+ if (!TryConsume('\n')) {
+ // The next token is on the same line. There are no comments.
+ return Next();
+ }
+ break;
+ }
+ }
+
+ // OK, we are now on the line *after* the previous token.
+ while (true) {
+ ConsumeZeroOrMore<WhitespaceNoNewline>();
+
+ switch (TryConsumeCommentStart()) {
+ case LINE_COMMENT:
+ ConsumeLineComment(collector.GetBufferForLineComment());
+ break;
+ case BLOCK_COMMENT:
+ ConsumeBlockComment(collector.GetBufferForBlockComment());
+
+ // Consume the rest of the line so that we don't interpret it as a
+ // blank line the next time around the loop.
+ ConsumeZeroOrMore<WhitespaceNoNewline>();
+ TryConsume('\n');
+ break;
+ case SLASH_NOT_COMMENT:
+ return true;
+ case NO_COMMENT:
+ if (TryConsume('\n')) {
+ // Completely blank line.
+ collector.Flush();
+ collector.DetachFromPrev();
+ } else {
+ bool result = Next();
+ if (!result ||
+ current_.text == "}" ||
+ current_.text == "]" ||
+ current_.text == ")") {
+ // It looks like we're at the end of a scope. In this case it
+ // makes no sense to attach a comment to the following token.
+ collector.Flush();
+ }
+ return result;
+ }
+ break;
+ }
+ }
+}
+
+// -------------------------------------------------------------------
+// Token-parsing helpers. Remember that these don't need to report
+// errors since any errors should already have been reported while
+// tokenizing. Also, these can assume that whatever text they
+// are given is text that the tokenizer actually parsed as a token
+// of the given type.
+
+bool Tokenizer::ParseInteger(const string& text, uint64 max_value,
+ uint64* output) {
+ // Sadly, we can't just use strtoul() since it is only 32-bit and strtoull()
+ // is non-standard. I hate the C standard library. :(
+
+// return strtoull(text.c_str(), NULL, 0);
+
+ const char* ptr = text.c_str();
+ int base = 10;
+ if (ptr[0] == '0') {
+ if (ptr[1] == 'x' || ptr[1] == 'X') {
+ // This is hex.
+ base = 16;
+ ptr += 2;
+ } else {
+ // This is octal.
+ base = 8;
+ }
+ }
+
+ uint64 result = 0;
+ for (; *ptr != '\0'; ptr++) {
+ int digit = DigitValue(*ptr);
+ GOOGLE_LOG_IF(DFATAL, digit < 0 || digit >= base)
+ << " Tokenizer::ParseInteger() passed text that could not have been"
+ " tokenized as an integer: " << CEscape(text);
+ if (digit > max_value || result > (max_value - digit) / base) {
+ // Overflow.
+ return false;
+ }
+ result = result * base + digit;
+ }
+
+ *output = result;
+ return true;
+}
+
+double Tokenizer::ParseFloat(const string& text) {
+ const char* start = text.c_str();
+ char* end;
+ double result = NoLocaleStrtod(start, &end);
+
+ // "1e" is not a valid float, but if the tokenizer reads it, it will
+ // report an error but still return it as a valid token. We need to
+ // accept anything the tokenizer could possibly return, error or not.
+ if (*end == 'e' || *end == 'E') {
+ ++end;
+ if (*end == '-' || *end == '+') ++end;
+ }
+
+ // If the Tokenizer had allow_f_after_float_ enabled, the float may be
+ // suffixed with the letter 'f'.
+ if (*end == 'f' || *end == 'F') {
+ ++end;
+ }
+
+ GOOGLE_LOG_IF(DFATAL, end - start != text.size() || *start == '-')
+ << " Tokenizer::ParseFloat() passed text that could not have been"
+ " tokenized as a float: " << CEscape(text);
+ return result;
+}
+
+// Helper to append a Unicode code point to a string as UTF8, without bringing
+// in any external dependencies.
+static void AppendUTF8(uint32 code_point, string* output) {
+ uint32 tmp = 0;
+ int len = 0;
+ if (code_point <= 0x7f) {
+ tmp = code_point;
+ len = 1;
+ } else if (code_point <= 0x07ff) {
+ tmp = 0x0000c080 |
+ ((code_point & 0x07c0) << 2) |
+ (code_point & 0x003f);
+ len = 2;
+ } else if (code_point <= 0xffff) {
+ tmp = 0x00e08080 |
+ ((code_point & 0xf000) << 4) |
+ ((code_point & 0x0fc0) << 2) |
+ (code_point & 0x003f);
+ len = 3;
+ } else if (code_point <= 0x1fffff) {
+ tmp = 0xf0808080 |
+ ((code_point & 0x1c0000) << 6) |
+ ((code_point & 0x03f000) << 4) |
+ ((code_point & 0x000fc0) << 2) |
+ (code_point & 0x003f);
+ len = 4;
+ } else {
+ // UTF-16 is only defined for code points up to 0x10FFFF, and UTF-8 is
+ // normally only defined up to there as well.
+ StringAppendF(output, "\\U%08x", code_point);
+ return;
+ }
+ tmp = ghtonl(tmp);
+ output->append(reinterpret_cast<const char*>(&tmp) + sizeof(tmp) - len, len);
+}
+
+// Try to read <len> hex digits from ptr, and stuff the numeric result into
+// *result. Returns true if that many digits were successfully consumed.
+static bool ReadHexDigits(const char* ptr, int len, uint32* result) {
+ *result = 0;
+ if (len == 0) return false;
+ for (const char* end = ptr + len; ptr < end; ++ptr) {
+ if (*ptr == '\0') return false;
+ *result = (*result << 4) + DigitValue(*ptr);
+ }
+ return true;
+}
+
+// Handling UTF-16 surrogate pairs. UTF-16 encodes code points in the range
+// 0x10000...0x10ffff as a pair of numbers, a head surrogate followed by a trail
+// surrogate. These numbers are in a reserved range of Unicode code points, so
+// if we encounter such a pair we know how to parse it and convert it into a
+// single code point.
+static const uint32 kMinHeadSurrogate = 0xd800;
+static const uint32 kMaxHeadSurrogate = 0xdc00;
+static const uint32 kMinTrailSurrogate = 0xdc00;
+static const uint32 kMaxTrailSurrogate = 0xe000;
+
+static inline bool IsHeadSurrogate(uint32 code_point) {
+ return (code_point >= kMinHeadSurrogate) && (code_point < kMaxHeadSurrogate);
+}
+
+static inline bool IsTrailSurrogate(uint32 code_point) {
+ return (code_point >= kMinTrailSurrogate) &&
+ (code_point < kMaxTrailSurrogate);
+}
+
+// Combine a head and trail surrogate into a single Unicode code point.
+static uint32 AssembleUTF16(uint32 head_surrogate, uint32 trail_surrogate) {
+ GOOGLE_DCHECK(IsHeadSurrogate(head_surrogate));
+ GOOGLE_DCHECK(IsTrailSurrogate(trail_surrogate));
+ return 0x10000 + (((head_surrogate - kMinHeadSurrogate) << 10) |
+ (trail_surrogate - kMinTrailSurrogate));
+}
+
+// Convert the escape sequence parameter to a number of expected hex digits.
+static inline int UnicodeLength(char key) {
+ if (key == 'u') return 4;
+ if (key == 'U') return 8;
+ return 0;
+}
+
+// Given a pointer to the 'u' or 'U' starting a Unicode escape sequence, attempt
+// to parse that sequence. On success, returns a pointer to the first char
+// beyond that sequence, and fills in *code_point. On failure, returns ptr
+// itself.
+static const char* FetchUnicodePoint(const char* ptr, uint32* code_point) {
+ const char* p = ptr;
+ // Fetch the code point.
+ const int len = UnicodeLength(*p++);
+ if (!ReadHexDigits(p, len, code_point))
+ return ptr;
+ p += len;
+
+ // Check if the code point we read is a "head surrogate." If so, then we
+ // expect it to be immediately followed by another code point which is a valid
+ // "trail surrogate," and together they form a UTF-16 pair which decodes into
+ // a single Unicode point. Trail surrogates may only use \u, not \U.
+ if (IsHeadSurrogate(*code_point) && *p == '\\' && *(p + 1) == 'u') {
+ uint32 trail_surrogate;
+ if (ReadHexDigits(p + 2, 4, &trail_surrogate) &&
+ IsTrailSurrogate(trail_surrogate)) {
+ *code_point = AssembleUTF16(*code_point, trail_surrogate);
+ p += 6;
+ }
+ // If this failed, then we just emit the head surrogate as a code point.
+ // It's bogus, but so is the string.
+ }
+
+ return p;
+}
+
+// The text string must begin and end with single or double quote
+// characters.
+void Tokenizer::ParseStringAppend(const string& text, string* output) {
+ // Reminder: text[0] is always a quote character. (If text is
+ // empty, it's invalid, so we'll just return).
+ const size_t text_size = text.size();
+ if (text_size == 0) {
+ GOOGLE_LOG(DFATAL)
+ << " Tokenizer::ParseStringAppend() passed text that could not"
+ " have been tokenized as a string: " << CEscape(text);
+ return;
+ }
+
+ // Reserve room for new string. The branch is necessary because if
+ // there is already space available the reserve() call might
+ // downsize the output.
+ const size_t new_len = text_size + output->size();
+ if (new_len > output->capacity()) {
+ output->reserve(new_len);
+ }
+
+ // Loop through the string copying characters to "output" and
+ // interpreting escape sequences. Note that any invalid escape
+ // sequences or other errors were already reported while tokenizing.
+ // In this case we do not need to produce valid results.
+ for (const char* ptr = text.c_str() + 1; *ptr != '\0'; ptr++) {
+ if (*ptr == '\\' && ptr[1] != '\0') {
+ // An escape sequence.
+ ++ptr;
+
+ if (OctalDigit::InClass(*ptr)) {
+ // An octal escape. May one, two, or three digits.
+ int code = DigitValue(*ptr);
+ if (OctalDigit::InClass(ptr[1])) {
+ ++ptr;
+ code = code * 8 + DigitValue(*ptr);
+ }
+ if (OctalDigit::InClass(ptr[1])) {
+ ++ptr;
+ code = code * 8 + DigitValue(*ptr);
+ }
+ output->push_back(static_cast<char>(code));
+
+ } else if (*ptr == 'x') {
+ // A hex escape. May zero, one, or two digits. (The zero case
+ // will have been caught as an error earlier.)
+ int code = 0;
+ if (HexDigit::InClass(ptr[1])) {
+ ++ptr;
+ code = DigitValue(*ptr);
+ }
+ if (HexDigit::InClass(ptr[1])) {
+ ++ptr;
+ code = code * 16 + DigitValue(*ptr);
+ }
+ output->push_back(static_cast<char>(code));
+
+ } else if (*ptr == 'u' || *ptr == 'U') {
+ uint32 unicode;
+ const char* end = FetchUnicodePoint(ptr, &unicode);
+ if (end == ptr) {
+ // Failure: Just dump out what we saw, don't try to parse it.
+ output->push_back(*ptr);
+ } else {
+ AppendUTF8(unicode, output);
+ ptr = end - 1; // Because we're about to ++ptr.
+ }
+ } else {
+ // Some other escape code.
+ output->push_back(TranslateEscape(*ptr));
+ }
+
+ } else if (*ptr == text[0] && ptr[1] == '\0') {
+ // Ignore final quote matching the starting quote.
+ } else {
+ output->push_back(*ptr);
+ }
+ }
+}
+
+template<typename CharacterClass>
+static bool AllInClass(const string& s) {
+ for (int i = 0; i < s.size(); ++i) {
+ if (!CharacterClass::InClass(s[i]))
+ return false;
+ }
+ return true;
+}
+
+bool Tokenizer::IsIdentifier(const string& text) {
+ // Mirrors IDENTIFIER definition in Tokenizer::Next() above.
+ if (text.size() == 0)
+ return false;
+ if (!Letter::InClass(text.at(0)))
+ return false;
+ if (!AllInClass<Alphanumeric>(text.substr(1)))
+ return false;
+ return true;
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/tokenizer.h b/toolkit/components/protobuf/src/google/protobuf/io/tokenizer.h
new file mode 100644
index 0000000000..8c6220a1d0
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/tokenizer.h
@@ -0,0 +1,402 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Class for parsing tokenized text from a ZeroCopyInputStream.
+
+#ifndef GOOGLE_PROTOBUF_IO_TOKENIZER_H__
+#define GOOGLE_PROTOBUF_IO_TOKENIZER_H__
+
+#include <string>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+class ZeroCopyInputStream; // zero_copy_stream.h
+
+// Defined in this file.
+class ErrorCollector;
+class Tokenizer;
+
+// Abstract interface for an object which collects the errors that occur
+// during parsing. A typical implementation might simply print the errors
+// to stdout.
+class LIBPROTOBUF_EXPORT ErrorCollector {
+ public:
+ inline ErrorCollector() {}
+ virtual ~ErrorCollector();
+
+ // Indicates that there was an error in the input at the given line and
+ // column numbers. The numbers are zero-based, so you may want to add
+ // 1 to each before printing them.
+ virtual void AddError(int line, int column, const string& message) = 0;
+
+ // Indicates that there was a warning in the input at the given line and
+ // column numbers. The numbers are zero-based, so you may want to add
+ // 1 to each before printing them.
+ virtual void AddWarning(int /* line */, int /* column */,
+ const string& /* message */) { }
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ErrorCollector);
+};
+
+// This class converts a stream of raw text into a stream of tokens for
+// the protocol definition parser to parse. The tokens recognized are
+// similar to those that make up the C language; see the TokenType enum for
+// precise descriptions. Whitespace and comments are skipped. By default,
+// C- and C++-style comments are recognized, but other styles can be used by
+// calling set_comment_style().
+class LIBPROTOBUF_EXPORT Tokenizer {
+ public:
+ // Construct a Tokenizer that reads and tokenizes text from the given
+ // input stream and writes errors to the given error_collector.
+ // The caller keeps ownership of input and error_collector.
+ Tokenizer(ZeroCopyInputStream* input, ErrorCollector* error_collector);
+ ~Tokenizer();
+
+ enum TokenType {
+ TYPE_START, // Next() has not yet been called.
+ TYPE_END, // End of input reached. "text" is empty.
+
+ TYPE_IDENTIFIER, // A sequence of letters, digits, and underscores, not
+ // starting with a digit. It is an error for a number
+ // to be followed by an identifier with no space in
+ // between.
+ TYPE_INTEGER, // A sequence of digits representing an integer. Normally
+ // the digits are decimal, but a prefix of "0x" indicates
+ // a hex number and a leading zero indicates octal, just
+ // like with C numeric literals. A leading negative sign
+ // is NOT included in the token; it's up to the parser to
+ // interpret the unary minus operator on its own.
+ TYPE_FLOAT, // A floating point literal, with a fractional part and/or
+ // an exponent. Always in decimal. Again, never
+ // negative.
+ TYPE_STRING, // A quoted sequence of escaped characters. Either single
+ // or double quotes can be used, but they must match.
+ // A string literal cannot cross a line break.
+ TYPE_SYMBOL, // Any other printable character, like '!' or '+'.
+ // Symbols are always a single character, so "!+$%" is
+ // four tokens.
+ };
+
+ // Structure representing a token read from the token stream.
+ struct Token {
+ TokenType type;
+ string text; // The exact text of the token as it appeared in
+ // the input. e.g. tokens of TYPE_STRING will still
+ // be escaped and in quotes.
+
+ // "line" and "column" specify the position of the first character of
+ // the token within the input stream. They are zero-based.
+ int line;
+ int column;
+ int end_column;
+ };
+
+ // Get the current token. This is updated when Next() is called. Before
+ // the first call to Next(), current() has type TYPE_START and no contents.
+ const Token& current();
+
+ // Return the previous token -- i.e. what current() returned before the
+ // previous call to Next().
+ const Token& previous();
+
+ // Advance to the next token. Returns false if the end of the input is
+ // reached.
+ bool Next();
+
+ // Like Next(), but also collects comments which appear between the previous
+ // and next tokens.
+ //
+ // Comments which appear to be attached to the previous token are stored
+ // in *prev_tailing_comments. Comments which appear to be attached to the
+ // next token are stored in *next_leading_comments. Comments appearing in
+ // between which do not appear to be attached to either will be added to
+ // detached_comments. Any of these parameters can be NULL to simply discard
+ // the comments.
+ //
+ // A series of line comments appearing on consecutive lines, with no other
+ // tokens appearing on those lines, will be treated as a single comment.
+ //
+ // Only the comment content is returned; comment markers (e.g. //) are
+ // stripped out. For block comments, leading whitespace and an asterisk will
+ // be stripped from the beginning of each line other than the first. Newlines
+ // are included in the output.
+ //
+ // Examples:
+ //
+ // optional int32 foo = 1; // Comment attached to foo.
+ // // Comment attached to bar.
+ // optional int32 bar = 2;
+ //
+ // optional string baz = 3;
+ // // Comment attached to baz.
+ // // Another line attached to baz.
+ //
+ // // Comment attached to qux.
+ // //
+ // // Another line attached to qux.
+ // optional double qux = 4;
+ //
+ // // Detached comment. This is not attached to qux or corge
+ // // because there are blank lines separating it from both.
+ //
+ // optional string corge = 5;
+ // /* Block comment attached
+ // * to corge. Leading asterisks
+ // * will be removed. */
+ // /* Block comment attached to
+ // * grault. */
+ // optional int32 grault = 6;
+ bool NextWithComments(string* prev_trailing_comments,
+ vector<string>* detached_comments,
+ string* next_leading_comments);
+
+ // Parse helpers ---------------------------------------------------
+
+ // Parses a TYPE_FLOAT token. This never fails, so long as the text actually
+ // comes from a TYPE_FLOAT token parsed by Tokenizer. If it doesn't, the
+ // result is undefined (possibly an assert failure).
+ static double ParseFloat(const string& text);
+
+ // Parses a TYPE_STRING token. This never fails, so long as the text actually
+ // comes from a TYPE_STRING token parsed by Tokenizer. If it doesn't, the
+ // result is undefined (possibly an assert failure).
+ static void ParseString(const string& text, string* output);
+
+ // Identical to ParseString, but appends to output.
+ static void ParseStringAppend(const string& text, string* output);
+
+ // Parses a TYPE_INTEGER token. Returns false if the result would be
+ // greater than max_value. Otherwise, returns true and sets *output to the
+ // result. If the text is not from a Token of type TYPE_INTEGER originally
+ // parsed by a Tokenizer, the result is undefined (possibly an assert
+ // failure).
+ static bool ParseInteger(const string& text, uint64 max_value,
+ uint64* output);
+
+ // Options ---------------------------------------------------------
+
+ // Set true to allow floats to be suffixed with the letter 'f'. Tokens
+ // which would otherwise be integers but which have the 'f' suffix will be
+ // forced to be interpreted as floats. For all other purposes, the 'f' is
+ // ignored.
+ void set_allow_f_after_float(bool value) { allow_f_after_float_ = value; }
+
+ // Valid values for set_comment_style().
+ enum CommentStyle {
+ // Line comments begin with "//", block comments are delimited by "/*" and
+ // "*/".
+ CPP_COMMENT_STYLE,
+ // Line comments begin with "#". No way to write block comments.
+ SH_COMMENT_STYLE
+ };
+
+ // Sets the comment style.
+ void set_comment_style(CommentStyle style) { comment_style_ = style; }
+
+ // Whether to require whitespace between a number and a field name.
+ // Default is true. Do not use this; for Google-internal cleanup only.
+ void set_require_space_after_number(bool require) {
+ require_space_after_number_ = require;
+ }
+
+ // Whether to allow string literals to span multiple lines. Default is false.
+ // Do not use this; for Google-internal cleanup only.
+ void set_allow_multiline_strings(bool allow) {
+ allow_multiline_strings_ = allow;
+ }
+
+ // External helper: validate an identifier.
+ static bool IsIdentifier(const string& text);
+
+ // -----------------------------------------------------------------
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Tokenizer);
+
+ Token current_; // Returned by current().
+ Token previous_; // Returned by previous().
+
+ ZeroCopyInputStream* input_;
+ ErrorCollector* error_collector_;
+
+ char current_char_; // == buffer_[buffer_pos_], updated by NextChar().
+ const char* buffer_; // Current buffer returned from input_.
+ int buffer_size_; // Size of buffer_.
+ int buffer_pos_; // Current position within the buffer.
+ bool read_error_; // Did we previously encounter a read error?
+
+ // Line and column number of current_char_ within the whole input stream.
+ int line_;
+ int column_;
+
+ // String to which text should be appended as we advance through it.
+ // Call RecordTo(&str) to start recording and StopRecording() to stop.
+ // E.g. StartToken() calls RecordTo(&current_.text). record_start_ is the
+ // position within the current buffer where recording started.
+ string* record_target_;
+ int record_start_;
+
+ // Options.
+ bool allow_f_after_float_;
+ CommentStyle comment_style_;
+ bool require_space_after_number_;
+ bool allow_multiline_strings_;
+
+ // Since we count columns we need to interpret tabs somehow. We'll take
+ // the standard 8-character definition for lack of any way to do better.
+ static const int kTabWidth = 8;
+
+ // -----------------------------------------------------------------
+ // Helper methods.
+
+ // Consume this character and advance to the next one.
+ void NextChar();
+
+ // Read a new buffer from the input.
+ void Refresh();
+
+ inline void RecordTo(string* target);
+ inline void StopRecording();
+
+ // Called when the current character is the first character of a new
+ // token (not including whitespace or comments).
+ inline void StartToken();
+ // Called when the current character is the first character after the
+ // end of the last token. After this returns, current_.text will
+ // contain all text consumed since StartToken() was called.
+ inline void EndToken();
+
+ // Convenience method to add an error at the current line and column.
+ void AddError(const string& message) {
+ error_collector_->AddError(line_, column_, message);
+ }
+
+ // -----------------------------------------------------------------
+ // The following four methods are used to consume tokens of specific
+ // types. They are actually used to consume all characters *after*
+ // the first, since the calling function consumes the first character
+ // in order to decide what kind of token is being read.
+
+ // Read and consume a string, ending when the given delimiter is
+ // consumed.
+ void ConsumeString(char delimiter);
+
+ // Read and consume a number, returning TYPE_FLOAT or TYPE_INTEGER
+ // depending on what was read. This needs to know if the first
+ // character was a zero in order to correctly recognize hex and octal
+ // numbers.
+ // It also needs to know if the first characted was a . to parse floating
+ // point correctly.
+ TokenType ConsumeNumber(bool started_with_zero, bool started_with_dot);
+
+ // Consume the rest of a line.
+ void ConsumeLineComment(string* content);
+ // Consume until "*/".
+ void ConsumeBlockComment(string* content);
+
+ enum NextCommentStatus {
+ // Started a line comment.
+ LINE_COMMENT,
+
+ // Started a block comment.
+ BLOCK_COMMENT,
+
+ // Consumed a slash, then realized it wasn't a comment. current_ has
+ // been filled in with a slash token. The caller should return it.
+ SLASH_NOT_COMMENT,
+
+ // We do not appear to be starting a comment here.
+ NO_COMMENT
+ };
+
+ // If we're at the start of a new comment, consume it and return what kind
+ // of comment it is.
+ NextCommentStatus TryConsumeCommentStart();
+
+ // -----------------------------------------------------------------
+ // These helper methods make the parsing code more readable. The
+ // "character classes" refered to are defined at the top of the .cc file.
+ // Basically it is a C++ class with one method:
+ // static bool InClass(char c);
+ // The method returns true if c is a member of this "class", like "Letter"
+ // or "Digit".
+
+ // Returns true if the current character is of the given character
+ // class, but does not consume anything.
+ template<typename CharacterClass>
+ inline bool LookingAt();
+
+ // If the current character is in the given class, consume it and return
+ // true. Otherwise return false.
+ // e.g. TryConsumeOne<Letter>()
+ template<typename CharacterClass>
+ inline bool TryConsumeOne();
+
+ // Like above, but try to consume the specific character indicated.
+ inline bool TryConsume(char c);
+
+ // Consume zero or more of the given character class.
+ template<typename CharacterClass>
+ inline void ConsumeZeroOrMore();
+
+ // Consume one or more of the given character class or log the given
+ // error message.
+ // e.g. ConsumeOneOrMore<Digit>("Expected digits.");
+ template<typename CharacterClass>
+ inline void ConsumeOneOrMore(const char* error);
+};
+
+// inline methods ====================================================
+inline const Tokenizer::Token& Tokenizer::current() {
+ return current_;
+}
+
+inline const Tokenizer::Token& Tokenizer::previous() {
+ return previous_;
+}
+
+inline void Tokenizer::ParseString(const string& text, string* output) {
+ output->clear();
+ ParseStringAppend(text, output);
+}
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_TOKENIZER_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.cc b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.cc
new file mode 100644
index 0000000000..f77c768fc9
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.cc
@@ -0,0 +1,57 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/io/zero_copy_stream.h>
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+ZeroCopyInputStream::~ZeroCopyInputStream() {}
+ZeroCopyOutputStream::~ZeroCopyOutputStream() {}
+
+
+bool ZeroCopyOutputStream::WriteAliasedRaw(const void* /* data */,
+ int /* size */) {
+ GOOGLE_LOG(FATAL) << "This ZeroCopyOutputStream doesn't support aliasing. "
+ "Reaching here usually means a ZeroCopyOutputStream "
+ "implementation bug.";
+ return false;
+}
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.h b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.h
new file mode 100644
index 0000000000..52650fc668
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream.h
@@ -0,0 +1,248 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file contains the ZeroCopyInputStream and ZeroCopyOutputStream
+// interfaces, which represent abstract I/O streams to and from which
+// protocol buffers can be read and written. For a few simple
+// implementations of these interfaces, see zero_copy_stream_impl.h.
+//
+// These interfaces are different from classic I/O streams in that they
+// try to minimize the amount of data copying that needs to be done.
+// To accomplish this, responsibility for allocating buffers is moved to
+// the stream object, rather than being the responsibility of the caller.
+// So, the stream can return a buffer which actually points directly into
+// the final data structure where the bytes are to be stored, and the caller
+// can interact directly with that buffer, eliminating an intermediate copy
+// operation.
+//
+// As an example, consider the common case in which you are reading bytes
+// from an array that is already in memory (or perhaps an mmap()ed file).
+// With classic I/O streams, you would do something like:
+// char buffer[BUFFER_SIZE];
+// input->Read(buffer, BUFFER_SIZE);
+// DoSomething(buffer, BUFFER_SIZE);
+// Then, the stream basically just calls memcpy() to copy the data from
+// the array into your buffer. With a ZeroCopyInputStream, you would do
+// this instead:
+// const void* buffer;
+// int size;
+// input->Next(&buffer, &size);
+// DoSomething(buffer, size);
+// Here, no copy is performed. The input stream returns a pointer directly
+// into the backing array, and the caller ends up reading directly from it.
+//
+// If you want to be able to read the old-fashion way, you can create
+// a CodedInputStream or CodedOutputStream wrapping these objects and use
+// their ReadRaw()/WriteRaw() methods. These will, of course, add a copy
+// step, but Coded*Stream will handle buffering so at least it will be
+// reasonably efficient.
+//
+// ZeroCopyInputStream example:
+// // Read in a file and print its contents to stdout.
+// int fd = open("myfile", O_RDONLY);
+// ZeroCopyInputStream* input = new FileInputStream(fd);
+//
+// const void* buffer;
+// int size;
+// while (input->Next(&buffer, &size)) {
+// cout.write(buffer, size);
+// }
+//
+// delete input;
+// close(fd);
+//
+// ZeroCopyOutputStream example:
+// // Copy the contents of "infile" to "outfile", using plain read() for
+// // "infile" but a ZeroCopyOutputStream for "outfile".
+// int infd = open("infile", O_RDONLY);
+// int outfd = open("outfile", O_WRONLY);
+// ZeroCopyOutputStream* output = new FileOutputStream(outfd);
+//
+// void* buffer;
+// int size;
+// while (output->Next(&buffer, &size)) {
+// int bytes = read(infd, buffer, size);
+// if (bytes < size) {
+// // Reached EOF.
+// output->BackUp(size - bytes);
+// break;
+// }
+// }
+//
+// delete output;
+// close(infd);
+// close(outfd);
+
+#ifndef GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_H__
+#define GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_H__
+
+#include <string>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+
+namespace protobuf {
+namespace io {
+
+// Defined in this file.
+class ZeroCopyInputStream;
+class ZeroCopyOutputStream;
+
+// Abstract interface similar to an input stream but designed to minimize
+// copying.
+class LIBPROTOBUF_EXPORT ZeroCopyInputStream {
+ public:
+ inline ZeroCopyInputStream() {}
+ virtual ~ZeroCopyInputStream();
+
+ // Obtains a chunk of data from the stream.
+ //
+ // Preconditions:
+ // * "size" and "data" are not NULL.
+ //
+ // Postconditions:
+ // * If the returned value is false, there is no more data to return or
+ // an error occurred. All errors are permanent.
+ // * Otherwise, "size" points to the actual number of bytes read and "data"
+ // points to a pointer to a buffer containing these bytes.
+ // * Ownership of this buffer remains with the stream, and the buffer
+ // remains valid only until some other method of the stream is called
+ // or the stream is destroyed.
+ // * It is legal for the returned buffer to have zero size, as long
+ // as repeatedly calling Next() eventually yields a buffer with non-zero
+ // size.
+ virtual bool Next(const void** data, int* size) = 0;
+
+ // Backs up a number of bytes, so that the next call to Next() returns
+ // data again that was already returned by the last call to Next(). This
+ // is useful when writing procedures that are only supposed to read up
+ // to a certain point in the input, then return. If Next() returns a
+ // buffer that goes beyond what you wanted to read, you can use BackUp()
+ // to return to the point where you intended to finish.
+ //
+ // Preconditions:
+ // * The last method called must have been Next().
+ // * count must be less than or equal to the size of the last buffer
+ // returned by Next().
+ //
+ // Postconditions:
+ // * The last "count" bytes of the last buffer returned by Next() will be
+ // pushed back into the stream. Subsequent calls to Next() will return
+ // the same data again before producing new data.
+ virtual void BackUp(int count) = 0;
+
+ // Skips a number of bytes. Returns false if the end of the stream is
+ // reached or some input error occurred. In the end-of-stream case, the
+ // stream is advanced to the end of the stream (so ByteCount() will return
+ // the total size of the stream).
+ virtual bool Skip(int count) = 0;
+
+ // Returns the total number of bytes read since this object was created.
+ virtual int64 ByteCount() const = 0;
+
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ZeroCopyInputStream);
+};
+
+// Abstract interface similar to an output stream but designed to minimize
+// copying.
+class LIBPROTOBUF_EXPORT ZeroCopyOutputStream {
+ public:
+ inline ZeroCopyOutputStream() {}
+ virtual ~ZeroCopyOutputStream();
+
+ // Obtains a buffer into which data can be written. Any data written
+ // into this buffer will eventually (maybe instantly, maybe later on)
+ // be written to the output.
+ //
+ // Preconditions:
+ // * "size" and "data" are not NULL.
+ //
+ // Postconditions:
+ // * If the returned value is false, an error occurred. All errors are
+ // permanent.
+ // * Otherwise, "size" points to the actual number of bytes in the buffer
+ // and "data" points to the buffer.
+ // * Ownership of this buffer remains with the stream, and the buffer
+ // remains valid only until some other method of the stream is called
+ // or the stream is destroyed.
+ // * Any data which the caller stores in this buffer will eventually be
+ // written to the output (unless BackUp() is called).
+ // * It is legal for the returned buffer to have zero size, as long
+ // as repeatedly calling Next() eventually yields a buffer with non-zero
+ // size.
+ virtual bool Next(void** data, int* size) = 0;
+
+ // Backs up a number of bytes, so that the end of the last buffer returned
+ // by Next() is not actually written. This is needed when you finish
+ // writing all the data you want to write, but the last buffer was bigger
+ // than you needed. You don't want to write a bunch of garbage after the
+ // end of your data, so you use BackUp() to back up.
+ //
+ // Preconditions:
+ // * The last method called must have been Next().
+ // * count must be less than or equal to the size of the last buffer
+ // returned by Next().
+ // * The caller must not have written anything to the last "count" bytes
+ // of that buffer.
+ //
+ // Postconditions:
+ // * The last "count" bytes of the last buffer returned by Next() will be
+ // ignored.
+ virtual void BackUp(int count) = 0;
+
+ // Returns the total number of bytes written since this object was created.
+ virtual int64 ByteCount() const = 0;
+
+ // Write a given chunk of data to the output. Some output streams may
+ // implement this in a way that avoids copying. Check AllowsAliasing() before
+ // calling WriteAliasedRaw(). It will GOOGLE_CHECK fail if WriteAliasedRaw() is
+ // called on a stream that does not allow aliasing.
+ //
+ // NOTE: It is caller's responsibility to ensure that the chunk of memory
+ // remains live until all of the data has been consumed from the stream.
+ virtual bool WriteAliasedRaw(const void* data, int size);
+ virtual bool AllowsAliasing() const { return false; }
+
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ZeroCopyOutputStream);
+};
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.cc b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.cc
new file mode 100644
index 0000000000..f7901b2797
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.cc
@@ -0,0 +1,473 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#ifdef _MSC_VER
+#include <io.h>
+#else
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#endif
+#include <errno.h>
+#include <iostream>
+#include <algorithm>
+
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+#ifdef _WIN32
+// Win32 lseek is broken: If invoked on a non-seekable file descriptor, its
+// return value is undefined. We re-define it to always produce an error.
+#define lseek(fd, offset, origin) ((off_t)-1)
+#endif
+
+namespace {
+
+// EINTR sucks.
+int close_no_eintr(int fd) {
+ int result;
+ do {
+ result = close(fd);
+ } while (result < 0 && errno == EINTR);
+ return result;
+}
+
+} // namespace
+
+
+// ===================================================================
+
+FileInputStream::FileInputStream(int file_descriptor, int block_size)
+ : copying_input_(file_descriptor),
+ impl_(&copying_input_, block_size) {
+}
+
+FileInputStream::~FileInputStream() {}
+
+bool FileInputStream::Close() {
+ return copying_input_.Close();
+}
+
+bool FileInputStream::Next(const void** data, int* size) {
+ return impl_.Next(data, size);
+}
+
+void FileInputStream::BackUp(int count) {
+ impl_.BackUp(count);
+}
+
+bool FileInputStream::Skip(int count) {
+ return impl_.Skip(count);
+}
+
+int64 FileInputStream::ByteCount() const {
+ return impl_.ByteCount();
+}
+
+FileInputStream::CopyingFileInputStream::CopyingFileInputStream(
+ int file_descriptor)
+ : file_(file_descriptor),
+ close_on_delete_(false),
+ is_closed_(false),
+ errno_(0),
+ previous_seek_failed_(false) {
+}
+
+FileInputStream::CopyingFileInputStream::~CopyingFileInputStream() {
+ if (close_on_delete_) {
+ if (!Close()) {
+ GOOGLE_LOG(ERROR) << "close() failed: " << strerror(errno_);
+ }
+ }
+}
+
+bool FileInputStream::CopyingFileInputStream::Close() {
+ GOOGLE_CHECK(!is_closed_);
+
+ is_closed_ = true;
+ if (close_no_eintr(file_) != 0) {
+ // The docs on close() do not specify whether a file descriptor is still
+ // open after close() fails with EIO. However, the glibc source code
+ // seems to indicate that it is not.
+ errno_ = errno;
+ return false;
+ }
+
+ return true;
+}
+
+int FileInputStream::CopyingFileInputStream::Read(void* buffer, int size) {
+ GOOGLE_CHECK(!is_closed_);
+
+ int result;
+ do {
+ result = read(file_, buffer, size);
+ } while (result < 0 && errno == EINTR);
+
+ if (result < 0) {
+ // Read error (not EOF).
+ errno_ = errno;
+ }
+
+ return result;
+}
+
+int FileInputStream::CopyingFileInputStream::Skip(int count) {
+ GOOGLE_CHECK(!is_closed_);
+
+ if (!previous_seek_failed_ &&
+ lseek(file_, count, SEEK_CUR) != (off_t)-1) {
+ // Seek succeeded.
+ return count;
+ } else {
+ // Failed to seek.
+
+ // Note to self: Don't seek again. This file descriptor doesn't
+ // support it.
+ previous_seek_failed_ = true;
+
+ // Use the default implementation.
+ return CopyingInputStream::Skip(count);
+ }
+}
+
+// ===================================================================
+
+FileOutputStream::FileOutputStream(int file_descriptor, int block_size)
+ : copying_output_(file_descriptor),
+ impl_(&copying_output_, block_size) {
+}
+
+FileOutputStream::~FileOutputStream() {
+ impl_.Flush();
+}
+
+bool FileOutputStream::Close() {
+ bool flush_succeeded = impl_.Flush();
+ return copying_output_.Close() && flush_succeeded;
+}
+
+bool FileOutputStream::Flush() {
+ return impl_.Flush();
+}
+
+bool FileOutputStream::Next(void** data, int* size) {
+ return impl_.Next(data, size);
+}
+
+void FileOutputStream::BackUp(int count) {
+ impl_.BackUp(count);
+}
+
+int64 FileOutputStream::ByteCount() const {
+ return impl_.ByteCount();
+}
+
+FileOutputStream::CopyingFileOutputStream::CopyingFileOutputStream(
+ int file_descriptor)
+ : file_(file_descriptor),
+ close_on_delete_(false),
+ is_closed_(false),
+ errno_(0) {
+}
+
+FileOutputStream::CopyingFileOutputStream::~CopyingFileOutputStream() {
+ if (close_on_delete_) {
+ if (!Close()) {
+ GOOGLE_LOG(ERROR) << "close() failed: " << strerror(errno_);
+ }
+ }
+}
+
+bool FileOutputStream::CopyingFileOutputStream::Close() {
+ GOOGLE_CHECK(!is_closed_);
+
+ is_closed_ = true;
+ if (close_no_eintr(file_) != 0) {
+ // The docs on close() do not specify whether a file descriptor is still
+ // open after close() fails with EIO. However, the glibc source code
+ // seems to indicate that it is not.
+ errno_ = errno;
+ return false;
+ }
+
+ return true;
+}
+
+bool FileOutputStream::CopyingFileOutputStream::Write(
+ const void* buffer, int size) {
+ GOOGLE_CHECK(!is_closed_);
+ int total_written = 0;
+
+ const uint8* buffer_base = reinterpret_cast<const uint8*>(buffer);
+
+ while (total_written < size) {
+ int bytes;
+ do {
+ bytes = write(file_, buffer_base + total_written, size - total_written);
+ } while (bytes < 0 && errno == EINTR);
+
+ if (bytes <= 0) {
+ // Write error.
+
+ // FIXME(kenton): According to the man page, if write() returns zero,
+ // there was no error; write() simply did not write anything. It's
+ // unclear under what circumstances this might happen, but presumably
+ // errno won't be set in this case. I am confused as to how such an
+ // event should be handled. For now I'm treating it as an error, since
+ // retrying seems like it could lead to an infinite loop. I suspect
+ // this never actually happens anyway.
+
+ if (bytes < 0) {
+ errno_ = errno;
+ }
+ return false;
+ }
+ total_written += bytes;
+ }
+
+ return true;
+}
+
+// ===================================================================
+
+IstreamInputStream::IstreamInputStream(istream* input, int block_size)
+ : copying_input_(input),
+ impl_(&copying_input_, block_size) {
+}
+
+IstreamInputStream::~IstreamInputStream() {}
+
+bool IstreamInputStream::Next(const void** data, int* size) {
+ return impl_.Next(data, size);
+}
+
+void IstreamInputStream::BackUp(int count) {
+ impl_.BackUp(count);
+}
+
+bool IstreamInputStream::Skip(int count) {
+ return impl_.Skip(count);
+}
+
+int64 IstreamInputStream::ByteCount() const {
+ return impl_.ByteCount();
+}
+
+IstreamInputStream::CopyingIstreamInputStream::CopyingIstreamInputStream(
+ istream* input)
+ : input_(input) {
+}
+
+IstreamInputStream::CopyingIstreamInputStream::~CopyingIstreamInputStream() {}
+
+int IstreamInputStream::CopyingIstreamInputStream::Read(
+ void* buffer, int size) {
+ input_->read(reinterpret_cast<char*>(buffer), size);
+ int result = input_->gcount();
+ if (result == 0 && input_->fail() && !input_->eof()) {
+ return -1;
+ }
+ return result;
+}
+
+// ===================================================================
+
+OstreamOutputStream::OstreamOutputStream(ostream* output, int block_size)
+ : copying_output_(output),
+ impl_(&copying_output_, block_size) {
+}
+
+OstreamOutputStream::~OstreamOutputStream() {
+ impl_.Flush();
+}
+
+bool OstreamOutputStream::Next(void** data, int* size) {
+ return impl_.Next(data, size);
+}
+
+void OstreamOutputStream::BackUp(int count) {
+ impl_.BackUp(count);
+}
+
+int64 OstreamOutputStream::ByteCount() const {
+ return impl_.ByteCount();
+}
+
+OstreamOutputStream::CopyingOstreamOutputStream::CopyingOstreamOutputStream(
+ ostream* output)
+ : output_(output) {
+}
+
+OstreamOutputStream::CopyingOstreamOutputStream::~CopyingOstreamOutputStream() {
+}
+
+bool OstreamOutputStream::CopyingOstreamOutputStream::Write(
+ const void* buffer, int size) {
+ output_->write(reinterpret_cast<const char*>(buffer), size);
+ return output_->good();
+}
+
+// ===================================================================
+
+ConcatenatingInputStream::ConcatenatingInputStream(
+ ZeroCopyInputStream* const streams[], int count)
+ : streams_(streams), stream_count_(count), bytes_retired_(0) {
+}
+
+ConcatenatingInputStream::~ConcatenatingInputStream() {
+}
+
+bool ConcatenatingInputStream::Next(const void** data, int* size) {
+ while (stream_count_ > 0) {
+ if (streams_[0]->Next(data, size)) return true;
+
+ // That stream is done. Advance to the next one.
+ bytes_retired_ += streams_[0]->ByteCount();
+ ++streams_;
+ --stream_count_;
+ }
+
+ // No more streams.
+ return false;
+}
+
+void ConcatenatingInputStream::BackUp(int count) {
+ if (stream_count_ > 0) {
+ streams_[0]->BackUp(count);
+ } else {
+ GOOGLE_LOG(DFATAL) << "Can't BackUp() after failed Next().";
+ }
+}
+
+bool ConcatenatingInputStream::Skip(int count) {
+ while (stream_count_ > 0) {
+ // Assume that ByteCount() can be used to find out how much we actually
+ // skipped when Skip() fails.
+ int64 target_byte_count = streams_[0]->ByteCount() + count;
+ if (streams_[0]->Skip(count)) return true;
+
+ // Hit the end of the stream. Figure out how many more bytes we still have
+ // to skip.
+ int64 final_byte_count = streams_[0]->ByteCount();
+ GOOGLE_DCHECK_LT(final_byte_count, target_byte_count);
+ count = target_byte_count - final_byte_count;
+
+ // That stream is done. Advance to the next one.
+ bytes_retired_ += final_byte_count;
+ ++streams_;
+ --stream_count_;
+ }
+
+ return false;
+}
+
+int64 ConcatenatingInputStream::ByteCount() const {
+ if (stream_count_ == 0) {
+ return bytes_retired_;
+ } else {
+ return bytes_retired_ + streams_[0]->ByteCount();
+ }
+}
+
+
+// ===================================================================
+
+LimitingInputStream::LimitingInputStream(ZeroCopyInputStream* input,
+ int64 limit)
+ : input_(input), limit_(limit) {
+ prior_bytes_read_ = input_->ByteCount();
+}
+
+LimitingInputStream::~LimitingInputStream() {
+ // If we overshot the limit, back up.
+ if (limit_ < 0) input_->BackUp(-limit_);
+}
+
+bool LimitingInputStream::Next(const void** data, int* size) {
+ if (limit_ <= 0) return false;
+ if (!input_->Next(data, size)) return false;
+
+ limit_ -= *size;
+ if (limit_ < 0) {
+ // We overshot the limit. Reduce *size to hide the rest of the buffer.
+ *size += limit_;
+ }
+ return true;
+}
+
+void LimitingInputStream::BackUp(int count) {
+ if (limit_ < 0) {
+ input_->BackUp(count - limit_);
+ limit_ = count;
+ } else {
+ input_->BackUp(count);
+ limit_ += count;
+ }
+}
+
+bool LimitingInputStream::Skip(int count) {
+ if (count > limit_) {
+ if (limit_ < 0) return false;
+ input_->Skip(limit_);
+ limit_ = 0;
+ return false;
+ } else {
+ if (!input_->Skip(count)) return false;
+ limit_ -= count;
+ return true;
+ }
+}
+
+int64 LimitingInputStream::ByteCount() const {
+ if (limit_ < 0) {
+ return input_->ByteCount() + limit_ - prior_bytes_read_;
+ } else {
+ return input_->ByteCount() - prior_bytes_read_;
+ }
+}
+
+
+// ===================================================================
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.h b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.h
new file mode 100644
index 0000000000..0746fa6afe
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl.h
@@ -0,0 +1,358 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file contains common implementations of the interfaces defined in
+// zero_copy_stream.h which are only included in the full (non-lite)
+// protobuf library. These implementations include Unix file descriptors
+// and C++ iostreams. See also: zero_copy_stream_impl_lite.h
+
+#ifndef GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_H__
+#define GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_H__
+
+#include <string>
+#include <iosfwd>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+#include <google/protobuf/stubs/common.h>
+
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+
+// ===================================================================
+
+// A ZeroCopyInputStream which reads from a file descriptor.
+//
+// FileInputStream is preferred over using an ifstream with IstreamInputStream.
+// The latter will introduce an extra layer of buffering, harming performance.
+// Also, it's conceivable that FileInputStream could someday be enhanced
+// to use zero-copy file descriptors on OSs which support them.
+class LIBPROTOBUF_EXPORT FileInputStream : public ZeroCopyInputStream {
+ public:
+ // Creates a stream that reads from the given Unix file descriptor.
+ // If a block_size is given, it specifies the number of bytes that
+ // should be read and returned with each call to Next(). Otherwise,
+ // a reasonable default is used.
+ explicit FileInputStream(int file_descriptor, int block_size = -1);
+ ~FileInputStream();
+
+ // Flushes any buffers and closes the underlying file. Returns false if
+ // an error occurs during the process; use GetErrno() to examine the error.
+ // Even if an error occurs, the file descriptor is closed when this returns.
+ bool Close();
+
+ // By default, the file descriptor is not closed when the stream is
+ // destroyed. Call SetCloseOnDelete(true) to change that. WARNING:
+ // This leaves no way for the caller to detect if close() fails. If
+ // detecting close() errors is important to you, you should arrange
+ // to close the descriptor yourself.
+ void SetCloseOnDelete(bool value) { copying_input_.SetCloseOnDelete(value); }
+
+ // If an I/O error has occurred on this file descriptor, this is the
+ // errno from that error. Otherwise, this is zero. Once an error
+ // occurs, the stream is broken and all subsequent operations will
+ // fail.
+ int GetErrno() { return copying_input_.GetErrno(); }
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+ private:
+ class LIBPROTOBUF_EXPORT CopyingFileInputStream : public CopyingInputStream {
+ public:
+ CopyingFileInputStream(int file_descriptor);
+ ~CopyingFileInputStream();
+
+ bool Close();
+ void SetCloseOnDelete(bool value) { close_on_delete_ = value; }
+ int GetErrno() { return errno_; }
+
+ // implements CopyingInputStream ---------------------------------
+ int Read(void* buffer, int size);
+ int Skip(int count);
+
+ private:
+ // The file descriptor.
+ const int file_;
+ bool close_on_delete_;
+ bool is_closed_;
+
+ // The errno of the I/O error, if one has occurred. Otherwise, zero.
+ int errno_;
+
+ // Did we try to seek once and fail? If so, we assume this file descriptor
+ // doesn't support seeking and won't try again.
+ bool previous_seek_failed_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CopyingFileInputStream);
+ };
+
+ CopyingFileInputStream copying_input_;
+ CopyingInputStreamAdaptor impl_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(FileInputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyOutputStream which writes to a file descriptor.
+//
+// FileOutputStream is preferred over using an ofstream with
+// OstreamOutputStream. The latter will introduce an extra layer of buffering,
+// harming performance. Also, it's conceivable that FileOutputStream could
+// someday be enhanced to use zero-copy file descriptors on OSs which
+// support them.
+class LIBPROTOBUF_EXPORT FileOutputStream : public ZeroCopyOutputStream {
+ public:
+ // Creates a stream that writes to the given Unix file descriptor.
+ // If a block_size is given, it specifies the size of the buffers
+ // that should be returned by Next(). Otherwise, a reasonable default
+ // is used.
+ explicit FileOutputStream(int file_descriptor, int block_size = -1);
+ ~FileOutputStream();
+
+ // Flushes any buffers and closes the underlying file. Returns false if
+ // an error occurs during the process; use GetErrno() to examine the error.
+ // Even if an error occurs, the file descriptor is closed when this returns.
+ bool Close();
+
+ // Flushes FileOutputStream's buffers but does not close the
+ // underlying file. No special measures are taken to ensure that
+ // underlying operating system file object is synchronized to disk.
+ bool Flush();
+
+ // By default, the file descriptor is not closed when the stream is
+ // destroyed. Call SetCloseOnDelete(true) to change that. WARNING:
+ // This leaves no way for the caller to detect if close() fails. If
+ // detecting close() errors is important to you, you should arrange
+ // to close the descriptor yourself.
+ void SetCloseOnDelete(bool value) { copying_output_.SetCloseOnDelete(value); }
+
+ // If an I/O error has occurred on this file descriptor, this is the
+ // errno from that error. Otherwise, this is zero. Once an error
+ // occurs, the stream is broken and all subsequent operations will
+ // fail.
+ int GetErrno() { return copying_output_.GetErrno(); }
+
+ // implements ZeroCopyOutputStream ---------------------------------
+ bool Next(void** data, int* size);
+ void BackUp(int count);
+ int64 ByteCount() const;
+
+ private:
+ class LIBPROTOBUF_EXPORT CopyingFileOutputStream : public CopyingOutputStream {
+ public:
+ CopyingFileOutputStream(int file_descriptor);
+ ~CopyingFileOutputStream();
+
+ bool Close();
+ void SetCloseOnDelete(bool value) { close_on_delete_ = value; }
+ int GetErrno() { return errno_; }
+
+ // implements CopyingOutputStream --------------------------------
+ bool Write(const void* buffer, int size);
+
+ private:
+ // The file descriptor.
+ const int file_;
+ bool close_on_delete_;
+ bool is_closed_;
+
+ // The errno of the I/O error, if one has occurred. Otherwise, zero.
+ int errno_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CopyingFileOutputStream);
+ };
+
+ CopyingFileOutputStream copying_output_;
+ CopyingOutputStreamAdaptor impl_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(FileOutputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyInputStream which reads from a C++ istream.
+//
+// Note that for reading files (or anything represented by a file descriptor),
+// FileInputStream is more efficient.
+class LIBPROTOBUF_EXPORT IstreamInputStream : public ZeroCopyInputStream {
+ public:
+ // Creates a stream that reads from the given C++ istream.
+ // If a block_size is given, it specifies the number of bytes that
+ // should be read and returned with each call to Next(). Otherwise,
+ // a reasonable default is used.
+ explicit IstreamInputStream(istream* stream, int block_size = -1);
+ ~IstreamInputStream();
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+ private:
+ class LIBPROTOBUF_EXPORT CopyingIstreamInputStream : public CopyingInputStream {
+ public:
+ CopyingIstreamInputStream(istream* input);
+ ~CopyingIstreamInputStream();
+
+ // implements CopyingInputStream ---------------------------------
+ int Read(void* buffer, int size);
+ // (We use the default implementation of Skip().)
+
+ private:
+ // The stream.
+ istream* input_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CopyingIstreamInputStream);
+ };
+
+ CopyingIstreamInputStream copying_input_;
+ CopyingInputStreamAdaptor impl_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(IstreamInputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyOutputStream which writes to a C++ ostream.
+//
+// Note that for writing files (or anything represented by a file descriptor),
+// FileOutputStream is more efficient.
+class LIBPROTOBUF_EXPORT OstreamOutputStream : public ZeroCopyOutputStream {
+ public:
+ // Creates a stream that writes to the given C++ ostream.
+ // If a block_size is given, it specifies the size of the buffers
+ // that should be returned by Next(). Otherwise, a reasonable default
+ // is used.
+ explicit OstreamOutputStream(ostream* stream, int block_size = -1);
+ ~OstreamOutputStream();
+
+ // implements ZeroCopyOutputStream ---------------------------------
+ bool Next(void** data, int* size);
+ void BackUp(int count);
+ int64 ByteCount() const;
+
+ private:
+ class LIBPROTOBUF_EXPORT CopyingOstreamOutputStream : public CopyingOutputStream {
+ public:
+ CopyingOstreamOutputStream(ostream* output);
+ ~CopyingOstreamOutputStream();
+
+ // implements CopyingOutputStream --------------------------------
+ bool Write(const void* buffer, int size);
+
+ private:
+ // The stream.
+ ostream* output_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CopyingOstreamOutputStream);
+ };
+
+ CopyingOstreamOutputStream copying_output_;
+ CopyingOutputStreamAdaptor impl_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(OstreamOutputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyInputStream which reads from several other streams in sequence.
+// ConcatenatingInputStream is unable to distinguish between end-of-stream
+// and read errors in the underlying streams, so it assumes any errors mean
+// end-of-stream. So, if the underlying streams fail for any other reason,
+// ConcatenatingInputStream may do odd things. It is suggested that you do
+// not use ConcatenatingInputStream on streams that might produce read errors
+// other than end-of-stream.
+class LIBPROTOBUF_EXPORT ConcatenatingInputStream : public ZeroCopyInputStream {
+ public:
+ // All streams passed in as well as the array itself must remain valid
+ // until the ConcatenatingInputStream is destroyed.
+ ConcatenatingInputStream(ZeroCopyInputStream* const streams[], int count);
+ ~ConcatenatingInputStream();
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+
+ private:
+ // As streams are retired, streams_ is incremented and count_ is
+ // decremented.
+ ZeroCopyInputStream* const* streams_;
+ int stream_count_;
+ int64 bytes_retired_; // Bytes read from previous streams.
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ConcatenatingInputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyInputStream which wraps some other stream and limits it to
+// a particular byte count.
+class LIBPROTOBUF_EXPORT LimitingInputStream : public ZeroCopyInputStream {
+ public:
+ LimitingInputStream(ZeroCopyInputStream* input, int64 limit);
+ ~LimitingInputStream();
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+
+ private:
+ ZeroCopyInputStream* input_;
+ int64 limit_; // Decreases as we go, becomes negative if we overshoot.
+ int64 prior_bytes_read_; // Bytes read on underlying stream at construction
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(LimitingInputStream);
+};
+
+// ===================================================================
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.cc b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.cc
new file mode 100644
index 0000000000..58aff0e256
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.cc
@@ -0,0 +1,405 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+#include <algorithm>
+#include <limits>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+namespace {
+
+// Default block size for Copying{In,Out}putStreamAdaptor.
+static const int kDefaultBlockSize = 8192;
+
+} // namespace
+
+// ===================================================================
+
+ArrayInputStream::ArrayInputStream(const void* data, int size,
+ int block_size)
+ : data_(reinterpret_cast<const uint8*>(data)),
+ size_(size),
+ block_size_(block_size > 0 ? block_size : size),
+ position_(0),
+ last_returned_size_(0) {
+}
+
+ArrayInputStream::~ArrayInputStream() {
+}
+
+bool ArrayInputStream::Next(const void** data, int* size) {
+ if (position_ < size_) {
+ last_returned_size_ = min(block_size_, size_ - position_);
+ *data = data_ + position_;
+ *size = last_returned_size_;
+ position_ += last_returned_size_;
+ return true;
+ } else {
+ // We're at the end of the array.
+ last_returned_size_ = 0; // Don't let caller back up.
+ return false;
+ }
+}
+
+void ArrayInputStream::BackUp(int count) {
+ GOOGLE_CHECK_GT(last_returned_size_, 0)
+ << "BackUp() can only be called after a successful Next().";
+ GOOGLE_CHECK_LE(count, last_returned_size_);
+ GOOGLE_CHECK_GE(count, 0);
+ position_ -= count;
+ last_returned_size_ = 0; // Don't let caller back up further.
+}
+
+bool ArrayInputStream::Skip(int count) {
+ GOOGLE_CHECK_GE(count, 0);
+ last_returned_size_ = 0; // Don't let caller back up.
+ if (count > size_ - position_) {
+ position_ = size_;
+ return false;
+ } else {
+ position_ += count;
+ return true;
+ }
+}
+
+int64 ArrayInputStream::ByteCount() const {
+ return position_;
+}
+
+
+// ===================================================================
+
+ArrayOutputStream::ArrayOutputStream(void* data, int size, int block_size)
+ : data_(reinterpret_cast<uint8*>(data)),
+ size_(size),
+ block_size_(block_size > 0 ? block_size : size),
+ position_(0),
+ last_returned_size_(0) {
+}
+
+ArrayOutputStream::~ArrayOutputStream() {
+}
+
+bool ArrayOutputStream::Next(void** data, int* size) {
+ if (position_ < size_) {
+ last_returned_size_ = min(block_size_, size_ - position_);
+ *data = data_ + position_;
+ *size = last_returned_size_;
+ position_ += last_returned_size_;
+ return true;
+ } else {
+ // We're at the end of the array.
+ last_returned_size_ = 0; // Don't let caller back up.
+ return false;
+ }
+}
+
+void ArrayOutputStream::BackUp(int count) {
+ GOOGLE_CHECK_GT(last_returned_size_, 0)
+ << "BackUp() can only be called after a successful Next().";
+ GOOGLE_CHECK_LE(count, last_returned_size_);
+ GOOGLE_CHECK_GE(count, 0);
+ position_ -= count;
+ last_returned_size_ = 0; // Don't let caller back up further.
+}
+
+int64 ArrayOutputStream::ByteCount() const {
+ return position_;
+}
+
+// ===================================================================
+
+StringOutputStream::StringOutputStream(string* target)
+ : target_(target) {
+}
+
+StringOutputStream::~StringOutputStream() {
+}
+
+bool StringOutputStream::Next(void** data, int* size) {
+ int old_size = target_->size();
+
+ // Grow the string.
+ if (old_size < target_->capacity()) {
+ // Resize the string to match its capacity, since we can get away
+ // without a memory allocation this way.
+ STLStringResizeUninitialized(target_, target_->capacity());
+ } else {
+ // Size has reached capacity, try to double the size.
+ if (old_size > std::numeric_limits<int>::max() / 2) {
+ // Can not double the size otherwise it is going to cause integer
+ // overflow in the expression below: old_size * 2 ";
+ GOOGLE_LOG(ERROR) << "Cannot allocate buffer larger than kint32max for "
+ << "StringOutputStream.";
+ return false;
+ }
+ // Double the size, also make sure that the new size is at least
+ // kMinimumSize.
+ STLStringResizeUninitialized(
+ target_,
+ max(old_size * 2,
+ kMinimumSize + 0)); // "+ 0" works around GCC4 weirdness.
+ }
+
+ *data = mutable_string_data(target_) + old_size;
+ *size = target_->size() - old_size;
+ return true;
+}
+
+void StringOutputStream::BackUp(int count) {
+ GOOGLE_CHECK_GE(count, 0);
+ GOOGLE_CHECK_LE(count, target_->size());
+ target_->resize(target_->size() - count);
+}
+
+int64 StringOutputStream::ByteCount() const {
+ return target_->size();
+}
+
+// ===================================================================
+
+CopyingInputStream::~CopyingInputStream() {}
+
+int CopyingInputStream::Skip(int count) {
+ char junk[4096];
+ int skipped = 0;
+ while (skipped < count) {
+ int bytes = Read(junk, min(count - skipped,
+ implicit_cast<int>(sizeof(junk))));
+ if (bytes <= 0) {
+ // EOF or read error.
+ return skipped;
+ }
+ skipped += bytes;
+ }
+ return skipped;
+}
+
+CopyingInputStreamAdaptor::CopyingInputStreamAdaptor(
+ CopyingInputStream* copying_stream, int block_size)
+ : copying_stream_(copying_stream),
+ owns_copying_stream_(false),
+ failed_(false),
+ position_(0),
+ buffer_size_(block_size > 0 ? block_size : kDefaultBlockSize),
+ buffer_used_(0),
+ backup_bytes_(0) {
+}
+
+CopyingInputStreamAdaptor::~CopyingInputStreamAdaptor() {
+ if (owns_copying_stream_) {
+ delete copying_stream_;
+ }
+}
+
+bool CopyingInputStreamAdaptor::Next(const void** data, int* size) {
+ if (failed_) {
+ // Already failed on a previous read.
+ return false;
+ }
+
+ AllocateBufferIfNeeded();
+
+ if (backup_bytes_ > 0) {
+ // We have data left over from a previous BackUp(), so just return that.
+ *data = buffer_.get() + buffer_used_ - backup_bytes_;
+ *size = backup_bytes_;
+ backup_bytes_ = 0;
+ return true;
+ }
+
+ // Read new data into the buffer.
+ buffer_used_ = copying_stream_->Read(buffer_.get(), buffer_size_);
+ if (buffer_used_ <= 0) {
+ // EOF or read error. We don't need the buffer anymore.
+ if (buffer_used_ < 0) {
+ // Read error (not EOF).
+ failed_ = true;
+ }
+ FreeBuffer();
+ return false;
+ }
+ position_ += buffer_used_;
+
+ *size = buffer_used_;
+ *data = buffer_.get();
+ return true;
+}
+
+void CopyingInputStreamAdaptor::BackUp(int count) {
+ GOOGLE_CHECK(backup_bytes_ == 0 && buffer_.get() != NULL)
+ << " BackUp() can only be called after Next().";
+ GOOGLE_CHECK_LE(count, buffer_used_)
+ << " Can't back up over more bytes than were returned by the last call"
+ " to Next().";
+ GOOGLE_CHECK_GE(count, 0)
+ << " Parameter to BackUp() can't be negative.";
+
+ backup_bytes_ = count;
+}
+
+bool CopyingInputStreamAdaptor::Skip(int count) {
+ GOOGLE_CHECK_GE(count, 0);
+
+ if (failed_) {
+ // Already failed on a previous read.
+ return false;
+ }
+
+ // First skip any bytes left over from a previous BackUp().
+ if (backup_bytes_ >= count) {
+ // We have more data left over than we're trying to skip. Just chop it.
+ backup_bytes_ -= count;
+ return true;
+ }
+
+ count -= backup_bytes_;
+ backup_bytes_ = 0;
+
+ int skipped = copying_stream_->Skip(count);
+ position_ += skipped;
+ return skipped == count;
+}
+
+int64 CopyingInputStreamAdaptor::ByteCount() const {
+ return position_ - backup_bytes_;
+}
+
+void CopyingInputStreamAdaptor::AllocateBufferIfNeeded() {
+ if (buffer_.get() == NULL) {
+ buffer_.reset(new uint8[buffer_size_]);
+ }
+}
+
+void CopyingInputStreamAdaptor::FreeBuffer() {
+ GOOGLE_CHECK_EQ(backup_bytes_, 0);
+ buffer_used_ = 0;
+ buffer_.reset();
+}
+
+// ===================================================================
+
+CopyingOutputStream::~CopyingOutputStream() {}
+
+CopyingOutputStreamAdaptor::CopyingOutputStreamAdaptor(
+ CopyingOutputStream* copying_stream, int block_size)
+ : copying_stream_(copying_stream),
+ owns_copying_stream_(false),
+ failed_(false),
+ position_(0),
+ buffer_size_(block_size > 0 ? block_size : kDefaultBlockSize),
+ buffer_used_(0) {
+}
+
+CopyingOutputStreamAdaptor::~CopyingOutputStreamAdaptor() {
+ WriteBuffer();
+ if (owns_copying_stream_) {
+ delete copying_stream_;
+ }
+}
+
+bool CopyingOutputStreamAdaptor::Flush() {
+ return WriteBuffer();
+}
+
+bool CopyingOutputStreamAdaptor::Next(void** data, int* size) {
+ if (buffer_used_ == buffer_size_) {
+ if (!WriteBuffer()) return false;
+ }
+
+ AllocateBufferIfNeeded();
+
+ *data = buffer_.get() + buffer_used_;
+ *size = buffer_size_ - buffer_used_;
+ buffer_used_ = buffer_size_;
+ return true;
+}
+
+void CopyingOutputStreamAdaptor::BackUp(int count) {
+ GOOGLE_CHECK_GE(count, 0);
+ GOOGLE_CHECK_EQ(buffer_used_, buffer_size_)
+ << " BackUp() can only be called after Next().";
+ GOOGLE_CHECK_LE(count, buffer_used_)
+ << " Can't back up over more bytes than were returned by the last call"
+ " to Next().";
+
+ buffer_used_ -= count;
+}
+
+int64 CopyingOutputStreamAdaptor::ByteCount() const {
+ return position_ + buffer_used_;
+}
+
+bool CopyingOutputStreamAdaptor::WriteBuffer() {
+ if (failed_) {
+ // Already failed on a previous write.
+ return false;
+ }
+
+ if (buffer_used_ == 0) return true;
+
+ if (copying_stream_->Write(buffer_.get(), buffer_used_)) {
+ position_ += buffer_used_;
+ buffer_used_ = 0;
+ return true;
+ } else {
+ failed_ = true;
+ FreeBuffer();
+ return false;
+ }
+}
+
+void CopyingOutputStreamAdaptor::AllocateBufferIfNeeded() {
+ if (buffer_ == NULL) {
+ buffer_.reset(new uint8[buffer_size_]);
+ }
+}
+
+void CopyingOutputStreamAdaptor::FreeBuffer() {
+ buffer_used_ = 0;
+ buffer_.reset();
+}
+
+// ===================================================================
+
+} // namespace io
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h
new file mode 100644
index 0000000000..44c6e7727f
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h
@@ -0,0 +1,355 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file contains common implementations of the interfaces defined in
+// zero_copy_stream.h which are included in the "lite" protobuf library.
+// These implementations cover I/O on raw arrays and strings, as well as
+// adaptors which make it easy to implement streams based on traditional
+// streams. Of course, many users will probably want to write their own
+// implementations of these interfaces specific to the particular I/O
+// abstractions they prefer to use, but these should cover the most common
+// cases.
+
+#ifndef GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_LITE_H__
+#define GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_LITE_H__
+
+#include <vector> /* See Bug 1186561 */
+#include <string>
+#include <iosfwd>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+
+namespace google {
+namespace protobuf {
+namespace io {
+
+// ===================================================================
+
+// A ZeroCopyInputStream backed by an in-memory array of bytes.
+class LIBPROTOBUF_EXPORT ArrayInputStream : public ZeroCopyInputStream {
+ public:
+ // Create an InputStream that returns the bytes pointed to by "data".
+ // "data" remains the property of the caller but must remain valid until
+ // the stream is destroyed. If a block_size is given, calls to Next()
+ // will return data blocks no larger than the given size. Otherwise, the
+ // first call to Next() returns the entire array. block_size is mainly
+ // useful for testing; in production you would probably never want to set
+ // it.
+ ArrayInputStream(const void* data, int size, int block_size = -1);
+ ~ArrayInputStream();
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+
+ private:
+ const uint8* const data_; // The byte array.
+ const int size_; // Total size of the array.
+ const int block_size_; // How many bytes to return at a time.
+
+ int position_;
+ int last_returned_size_; // How many bytes we returned last time Next()
+ // was called (used for error checking only).
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ArrayInputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyOutputStream backed by an in-memory array of bytes.
+class LIBPROTOBUF_EXPORT ArrayOutputStream : public ZeroCopyOutputStream {
+ public:
+ // Create an OutputStream that writes to the bytes pointed to by "data".
+ // "data" remains the property of the caller but must remain valid until
+ // the stream is destroyed. If a block_size is given, calls to Next()
+ // will return data blocks no larger than the given size. Otherwise, the
+ // first call to Next() returns the entire array. block_size is mainly
+ // useful for testing; in production you would probably never want to set
+ // it.
+ ArrayOutputStream(void* data, int size, int block_size = -1);
+ ~ArrayOutputStream();
+
+ // implements ZeroCopyOutputStream ---------------------------------
+ bool Next(void** data, int* size);
+ void BackUp(int count);
+ int64 ByteCount() const;
+
+ private:
+ uint8* const data_; // The byte array.
+ const int size_; // Total size of the array.
+ const int block_size_; // How many bytes to return at a time.
+
+ int position_;
+ int last_returned_size_; // How many bytes we returned last time Next()
+ // was called (used for error checking only).
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ArrayOutputStream);
+};
+
+// ===================================================================
+
+// A ZeroCopyOutputStream which appends bytes to a string.
+class LIBPROTOBUF_EXPORT StringOutputStream : public ZeroCopyOutputStream {
+ public:
+ // Create a StringOutputStream which appends bytes to the given string.
+ // The string remains property of the caller, but it MUST NOT be accessed
+ // in any way until the stream is destroyed.
+ //
+ // Hint: If you call target->reserve(n) before creating the stream,
+ // the first call to Next() will return at least n bytes of buffer
+ // space.
+ explicit StringOutputStream(string* target);
+ ~StringOutputStream();
+
+ // implements ZeroCopyOutputStream ---------------------------------
+ bool Next(void** data, int* size);
+ void BackUp(int count);
+ int64 ByteCount() const;
+
+ private:
+ static const int kMinimumSize = 16;
+
+ string* target_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(StringOutputStream);
+};
+
+// Note: There is no StringInputStream. Instead, just create an
+// ArrayInputStream as follows:
+// ArrayInputStream input(str.data(), str.size());
+
+// ===================================================================
+
+// A generic traditional input stream interface.
+//
+// Lots of traditional input streams (e.g. file descriptors, C stdio
+// streams, and C++ iostreams) expose an interface where every read
+// involves copying bytes into a buffer. If you want to take such an
+// interface and make a ZeroCopyInputStream based on it, simply implement
+// CopyingInputStream and then use CopyingInputStreamAdaptor.
+//
+// CopyingInputStream implementations should avoid buffering if possible.
+// CopyingInputStreamAdaptor does its own buffering and will read data
+// in large blocks.
+class LIBPROTOBUF_EXPORT CopyingInputStream {
+ public:
+ virtual ~CopyingInputStream();
+
+ // Reads up to "size" bytes into the given buffer. Returns the number of
+ // bytes read. Read() waits until at least one byte is available, or
+ // returns zero if no bytes will ever become available (EOF), or -1 if a
+ // permanent read error occurred.
+ virtual int Read(void* buffer, int size) = 0;
+
+ // Skips the next "count" bytes of input. Returns the number of bytes
+ // actually skipped. This will always be exactly equal to "count" unless
+ // EOF was reached or a permanent read error occurred.
+ //
+ // The default implementation just repeatedly calls Read() into a scratch
+ // buffer.
+ virtual int Skip(int count);
+};
+
+// A ZeroCopyInputStream which reads from a CopyingInputStream. This is
+// useful for implementing ZeroCopyInputStreams that read from traditional
+// streams. Note that this class is not really zero-copy.
+//
+// If you want to read from file descriptors or C++ istreams, this is
+// already implemented for you: use FileInputStream or IstreamInputStream
+// respectively.
+class LIBPROTOBUF_EXPORT CopyingInputStreamAdaptor : public ZeroCopyInputStream {
+ public:
+ // Creates a stream that reads from the given CopyingInputStream.
+ // If a block_size is given, it specifies the number of bytes that
+ // should be read and returned with each call to Next(). Otherwise,
+ // a reasonable default is used. The caller retains ownership of
+ // copying_stream unless SetOwnsCopyingStream(true) is called.
+ explicit CopyingInputStreamAdaptor(CopyingInputStream* copying_stream,
+ int block_size = -1);
+ ~CopyingInputStreamAdaptor();
+
+ // Call SetOwnsCopyingStream(true) to tell the CopyingInputStreamAdaptor to
+ // delete the underlying CopyingInputStream when it is destroyed.
+ void SetOwnsCopyingStream(bool value) { owns_copying_stream_ = value; }
+
+ // implements ZeroCopyInputStream ----------------------------------
+ bool Next(const void** data, int* size);
+ void BackUp(int count);
+ bool Skip(int count);
+ int64 ByteCount() const;
+
+ private:
+ // Insures that buffer_ is not NULL.
+ void AllocateBufferIfNeeded();
+ // Frees the buffer and resets buffer_used_.
+ void FreeBuffer();
+
+ // The underlying copying stream.
+ CopyingInputStream* copying_stream_;
+ bool owns_copying_stream_;
+
+ // True if we have seen a permenant error from the underlying stream.
+ bool failed_;
+
+ // The current position of copying_stream_, relative to the point where
+ // we started reading.
+ int64 position_;
+
+ // Data is read into this buffer. It may be NULL if no buffer is currently
+ // in use. Otherwise, it points to an array of size buffer_size_.
+ scoped_array<uint8> buffer_;
+ const int buffer_size_;
+
+ // Number of valid bytes currently in the buffer (i.e. the size last
+ // returned by Next()). 0 <= buffer_used_ <= buffer_size_.
+ int buffer_used_;
+
+ // Number of bytes in the buffer which were backed up over by a call to
+ // BackUp(). These need to be returned again.
+ // 0 <= backup_bytes_ <= buffer_used_
+ int backup_bytes_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CopyingInputStreamAdaptor);
+};
+
+// ===================================================================
+
+// A generic traditional output stream interface.
+//
+// Lots of traditional output streams (e.g. file descriptors, C stdio
+// streams, and C++ iostreams) expose an interface where every write
+// involves copying bytes from a buffer. If you want to take such an
+// interface and make a ZeroCopyOutputStream based on it, simply implement
+// CopyingOutputStream and then use CopyingOutputStreamAdaptor.
+//
+// CopyingOutputStream implementations should avoid buffering if possible.
+// CopyingOutputStreamAdaptor does its own buffering and will write data
+// in large blocks.
+class LIBPROTOBUF_EXPORT CopyingOutputStream {
+ public:
+ virtual ~CopyingOutputStream();
+
+ // Writes "size" bytes from the given buffer to the output. Returns true
+ // if successful, false on a write error.
+ virtual bool Write(const void* buffer, int size) = 0;
+};
+
+// A ZeroCopyOutputStream which writes to a CopyingOutputStream. This is
+// useful for implementing ZeroCopyOutputStreams that write to traditional
+// streams. Note that this class is not really zero-copy.
+//
+// If you want to write to file descriptors or C++ ostreams, this is
+// already implemented for you: use FileOutputStream or OstreamOutputStream
+// respectively.
+class LIBPROTOBUF_EXPORT CopyingOutputStreamAdaptor : public ZeroCopyOutputStream {
+ public:
+ // Creates a stream that writes to the given Unix file descriptor.
+ // If a block_size is given, it specifies the size of the buffers
+ // that should be returned by Next(). Otherwise, a reasonable default
+ // is used.
+ explicit CopyingOutputStreamAdaptor(CopyingOutputStream* copying_stream,
+ int block_size = -1);
+ ~CopyingOutputStreamAdaptor();
+
+ // Writes all pending data to the underlying stream. Returns false if a
+ // write error occurred on the underlying stream. (The underlying
+ // stream itself is not necessarily flushed.)
+ bool Flush();
+
+ // Call SetOwnsCopyingStream(true) to tell the CopyingOutputStreamAdaptor to
+ // delete the underlying CopyingOutputStream when it is destroyed.
+ void SetOwnsCopyingStream(bool value) { owns_copying_stream_ = value; }
+
+ // implements ZeroCopyOutputStream ---------------------------------
+ bool Next(void** data, int* size);
+ void BackUp(int count);
+ int64 ByteCount() const;
+
+ private:
+ // Write the current buffer, if it is present.
+ bool WriteBuffer();
+ // Insures that buffer_ is not NULL.
+ void AllocateBufferIfNeeded();
+ // Frees the buffer.
+ void FreeBuffer();
+
+ // The underlying copying stream.
+ CopyingOutputStream* copying_stream_;
+ bool owns_copying_stream_;
+
+ // True if we have seen a permenant error from the underlying stream.
+ bool failed_;
+
+ // The current position of copying_stream_, relative to the point where
+ // we started writing.
+ int64 position_;
+
+ // Data is written from this buffer. It may be NULL if no buffer is
+ // currently in use. Otherwise, it points to an array of size buffer_size_.
+ scoped_array<uint8> buffer_;
+ const int buffer_size_;
+
+ // Number of valid bytes currently in the buffer (i.e. the size last
+ // returned by Next()). When BackUp() is called, we just reduce this.
+ // 0 <= buffer_used_ <= buffer_size_.
+ int buffer_used_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(CopyingOutputStreamAdaptor);
+};
+
+// ===================================================================
+
+// Return a pointer to mutable characters underlying the given string. The
+// return value is valid until the next time the string is resized. We
+// trust the caller to treat the return value as an array of length s->size().
+inline char* mutable_string_data(string* s) {
+#ifdef LANG_CXX11
+ // This should be simpler & faster than string_as_array() because the latter
+ // is guaranteed to return NULL when *s is empty, so it has to check for that.
+ return &(*s)[0];
+#else
+ return string_as_array(s);
+#endif
+}
+
+} // namespace io
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_IO_ZERO_COPY_STREAM_IMPL_LITE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/message.cc b/toolkit/components/protobuf/src/google/protobuf/message.cc
new file mode 100644
index 0000000000..1324ed9b17
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/message.cc
@@ -0,0 +1,358 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <iostream>
+#include <stack>
+#include <google/protobuf/stubs/hash.h>
+
+#include <google/protobuf/message.h>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/reflection_ops.h>
+#include <google/protobuf/wire_format.h>
+#include <google/protobuf/stubs/strutil.h>
+#include <google/protobuf/stubs/map_util.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+
+using internal::WireFormat;
+using internal::ReflectionOps;
+
+Message::~Message() {}
+
+void Message::MergeFrom(const Message& from) {
+ const Descriptor* descriptor = GetDescriptor();
+ GOOGLE_CHECK_EQ(from.GetDescriptor(), descriptor)
+ << ": Tried to merge from a message with a different type. "
+ "to: " << descriptor->full_name() << ", "
+ "from:" << from.GetDescriptor()->full_name();
+ ReflectionOps::Merge(from, this);
+}
+
+void Message::CheckTypeAndMergeFrom(const MessageLite& other) {
+ MergeFrom(*down_cast<const Message*>(&other));
+}
+
+void Message::CopyFrom(const Message& from) {
+ const Descriptor* descriptor = GetDescriptor();
+ GOOGLE_CHECK_EQ(from.GetDescriptor(), descriptor)
+ << ": Tried to copy from a message with a different type. "
+ "to: " << descriptor->full_name() << ", "
+ "from:" << from.GetDescriptor()->full_name();
+ ReflectionOps::Copy(from, this);
+}
+
+string Message::GetTypeName() const {
+ return GetDescriptor()->full_name();
+}
+
+void Message::Clear() {
+ ReflectionOps::Clear(this);
+}
+
+bool Message::IsInitialized() const {
+ return ReflectionOps::IsInitialized(*this);
+}
+
+void Message::FindInitializationErrors(vector<string>* errors) const {
+ return ReflectionOps::FindInitializationErrors(*this, "", errors);
+}
+
+string Message::InitializationErrorString() const {
+ vector<string> errors;
+ FindInitializationErrors(&errors);
+ return Join(errors, ", ");
+}
+
+void Message::CheckInitialized() const {
+ GOOGLE_CHECK(IsInitialized())
+ << "Message of type \"" << GetDescriptor()->full_name()
+ << "\" is missing required fields: " << InitializationErrorString();
+}
+
+void Message::DiscardUnknownFields() {
+ return ReflectionOps::DiscardUnknownFields(this);
+}
+
+bool Message::MergePartialFromCodedStream(io::CodedInputStream* input) {
+ return WireFormat::ParseAndMergePartial(input, this);
+}
+
+bool Message::ParseFromFileDescriptor(int file_descriptor) {
+ io::FileInputStream input(file_descriptor);
+ return ParseFromZeroCopyStream(&input) && input.GetErrno() == 0;
+}
+
+bool Message::ParsePartialFromFileDescriptor(int file_descriptor) {
+ io::FileInputStream input(file_descriptor);
+ return ParsePartialFromZeroCopyStream(&input) && input.GetErrno() == 0;
+}
+
+bool Message::ParseFromIstream(istream* input) {
+ io::IstreamInputStream zero_copy_input(input);
+ return ParseFromZeroCopyStream(&zero_copy_input) && input->eof();
+}
+
+bool Message::ParsePartialFromIstream(istream* input) {
+ io::IstreamInputStream zero_copy_input(input);
+ return ParsePartialFromZeroCopyStream(&zero_copy_input) && input->eof();
+}
+
+
+void Message::SerializeWithCachedSizes(
+ io::CodedOutputStream* output) const {
+ WireFormat::SerializeWithCachedSizes(*this, GetCachedSize(), output);
+}
+
+int Message::ByteSize() const {
+ int size = WireFormat::ByteSize(*this);
+ SetCachedSize(size);
+ return size;
+}
+
+void Message::SetCachedSize(int /* size */) const {
+ GOOGLE_LOG(FATAL) << "Message class \"" << GetDescriptor()->full_name()
+ << "\" implements neither SetCachedSize() nor ByteSize(). "
+ "Must implement one or the other.";
+}
+
+int Message::SpaceUsed() const {
+ return GetReflection()->SpaceUsed(*this);
+}
+
+bool Message::SerializeToFileDescriptor(int file_descriptor) const {
+ io::FileOutputStream output(file_descriptor);
+ return SerializeToZeroCopyStream(&output);
+}
+
+bool Message::SerializePartialToFileDescriptor(int file_descriptor) const {
+ io::FileOutputStream output(file_descriptor);
+ return SerializePartialToZeroCopyStream(&output);
+}
+
+bool Message::SerializeToOstream(ostream* output) const {
+ {
+ io::OstreamOutputStream zero_copy_output(output);
+ if (!SerializeToZeroCopyStream(&zero_copy_output)) return false;
+ }
+ return output->good();
+}
+
+bool Message::SerializePartialToOstream(ostream* output) const {
+ io::OstreamOutputStream zero_copy_output(output);
+ return SerializePartialToZeroCopyStream(&zero_copy_output);
+}
+
+
+// =============================================================================
+// Reflection and associated Template Specializations
+
+Reflection::~Reflection() {}
+
+#define HANDLE_TYPE(TYPE, CPPTYPE, CTYPE) \
+template<> \
+const RepeatedField<TYPE>& Reflection::GetRepeatedField<TYPE>( \
+ const Message& message, const FieldDescriptor* field) const { \
+ return *static_cast<RepeatedField<TYPE>* >( \
+ MutableRawRepeatedField(const_cast<Message*>(&message), \
+ field, CPPTYPE, CTYPE, NULL)); \
+} \
+ \
+template<> \
+RepeatedField<TYPE>* Reflection::MutableRepeatedField<TYPE>( \
+ Message* message, const FieldDescriptor* field) const { \
+ return static_cast<RepeatedField<TYPE>* >( \
+ MutableRawRepeatedField(message, field, CPPTYPE, CTYPE, NULL)); \
+}
+
+HANDLE_TYPE(int32, FieldDescriptor::CPPTYPE_INT32, -1);
+HANDLE_TYPE(int64, FieldDescriptor::CPPTYPE_INT64, -1);
+HANDLE_TYPE(uint32, FieldDescriptor::CPPTYPE_UINT32, -1);
+HANDLE_TYPE(uint64, FieldDescriptor::CPPTYPE_UINT64, -1);
+HANDLE_TYPE(float, FieldDescriptor::CPPTYPE_FLOAT, -1);
+HANDLE_TYPE(double, FieldDescriptor::CPPTYPE_DOUBLE, -1);
+HANDLE_TYPE(bool, FieldDescriptor::CPPTYPE_BOOL, -1);
+
+
+#undef HANDLE_TYPE
+
+void* Reflection::MutableRawRepeatedString(
+ Message* message, const FieldDescriptor* field, bool is_string) const {
+ return MutableRawRepeatedField(message, field,
+ FieldDescriptor::CPPTYPE_STRING, FieldOptions::STRING, NULL);
+}
+
+
+// =============================================================================
+// MessageFactory
+
+MessageFactory::~MessageFactory() {}
+
+namespace {
+
+class GeneratedMessageFactory : public MessageFactory {
+ public:
+ GeneratedMessageFactory();
+ ~GeneratedMessageFactory();
+
+ static GeneratedMessageFactory* singleton();
+
+ typedef void RegistrationFunc(const string&);
+ void RegisterFile(const char* file, RegistrationFunc* registration_func);
+ void RegisterType(const Descriptor* descriptor, const Message* prototype);
+
+ // implements MessageFactory ---------------------------------------
+ const Message* GetPrototype(const Descriptor* type);
+
+ private:
+ // Only written at static init time, so does not require locking.
+ hash_map<const char*, RegistrationFunc*,
+ hash<const char*>, streq> file_map_;
+
+ // Initialized lazily, so requires locking.
+ Mutex mutex_;
+ hash_map<const Descriptor*, const Message*> type_map_;
+};
+
+GeneratedMessageFactory* generated_message_factory_ = NULL;
+GOOGLE_PROTOBUF_DECLARE_ONCE(generated_message_factory_once_init_);
+
+void ShutdownGeneratedMessageFactory() {
+ delete generated_message_factory_;
+}
+
+void InitGeneratedMessageFactory() {
+ generated_message_factory_ = new GeneratedMessageFactory;
+ internal::OnShutdown(&ShutdownGeneratedMessageFactory);
+}
+
+GeneratedMessageFactory::GeneratedMessageFactory() {}
+GeneratedMessageFactory::~GeneratedMessageFactory() {}
+
+GeneratedMessageFactory* GeneratedMessageFactory::singleton() {
+ ::google::protobuf::GoogleOnceInit(&generated_message_factory_once_init_,
+ &InitGeneratedMessageFactory);
+ return generated_message_factory_;
+}
+
+void GeneratedMessageFactory::RegisterFile(
+ const char* file, RegistrationFunc* registration_func) {
+ if (!InsertIfNotPresent(&file_map_, file, registration_func)) {
+ GOOGLE_LOG(FATAL) << "File is already registered: " << file;
+ }
+}
+
+void GeneratedMessageFactory::RegisterType(const Descriptor* descriptor,
+ const Message* prototype) {
+ GOOGLE_DCHECK_EQ(descriptor->file()->pool(), DescriptorPool::generated_pool())
+ << "Tried to register a non-generated type with the generated "
+ "type registry.";
+
+ // This should only be called as a result of calling a file registration
+ // function during GetPrototype(), in which case we already have locked
+ // the mutex.
+ mutex_.AssertHeld();
+ if (!InsertIfNotPresent(&type_map_, descriptor, prototype)) {
+ GOOGLE_LOG(DFATAL) << "Type is already registered: " << descriptor->full_name();
+ }
+}
+
+
+const Message* GeneratedMessageFactory::GetPrototype(const Descriptor* type) {
+ {
+ ReaderMutexLock lock(&mutex_);
+ const Message* result = FindPtrOrNull(type_map_, type);
+ if (result != NULL) return result;
+ }
+
+ // If the type is not in the generated pool, then we can't possibly handle
+ // it.
+ if (type->file()->pool() != DescriptorPool::generated_pool()) return NULL;
+
+ // Apparently the file hasn't been registered yet. Let's do that now.
+ RegistrationFunc* registration_func =
+ FindPtrOrNull(file_map_, type->file()->name().c_str());
+ if (registration_func == NULL) {
+ GOOGLE_LOG(DFATAL) << "File appears to be in generated pool but wasn't "
+ "registered: " << type->file()->name();
+ return NULL;
+ }
+
+ WriterMutexLock lock(&mutex_);
+
+ // Check if another thread preempted us.
+ const Message* result = FindPtrOrNull(type_map_, type);
+ if (result == NULL) {
+ // Nope. OK, register everything.
+ registration_func(type->file()->name());
+ // Should be here now.
+ result = FindPtrOrNull(type_map_, type);
+ }
+
+ if (result == NULL) {
+ GOOGLE_LOG(DFATAL) << "Type appears to be in generated pool but wasn't "
+ << "registered: " << type->full_name();
+ }
+
+ return result;
+}
+
+} // namespace
+
+MessageFactory* MessageFactory::generated_factory() {
+ return GeneratedMessageFactory::singleton();
+}
+
+void MessageFactory::InternalRegisterGeneratedFile(
+ const char* filename, void (*register_messages)(const string&)) {
+ GeneratedMessageFactory::singleton()->RegisterFile(filename,
+ register_messages);
+}
+
+void MessageFactory::InternalRegisterGeneratedMessage(
+ const Descriptor* descriptor, const Message* prototype) {
+ GeneratedMessageFactory::singleton()->RegisterType(descriptor, prototype);
+}
+
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/message.h b/toolkit/components/protobuf/src/google/protobuf/message.h
new file mode 100644
index 0000000000..9593560531
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/message.h
@@ -0,0 +1,866 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Defines Message, the abstract interface implemented by non-lite
+// protocol message objects. Although it's possible to implement this
+// interface manually, most users will use the protocol compiler to
+// generate implementations.
+//
+// Example usage:
+//
+// Say you have a message defined as:
+//
+// message Foo {
+// optional string text = 1;
+// repeated int32 numbers = 2;
+// }
+//
+// Then, if you used the protocol compiler to generate a class from the above
+// definition, you could use it like so:
+//
+// string data; // Will store a serialized version of the message.
+//
+// {
+// // Create a message and serialize it.
+// Foo foo;
+// foo.set_text("Hello World!");
+// foo.add_numbers(1);
+// foo.add_numbers(5);
+// foo.add_numbers(42);
+//
+// foo.SerializeToString(&data);
+// }
+//
+// {
+// // Parse the serialized message and check that it contains the
+// // correct data.
+// Foo foo;
+// foo.ParseFromString(data);
+//
+// assert(foo.text() == "Hello World!");
+// assert(foo.numbers_size() == 3);
+// assert(foo.numbers(0) == 1);
+// assert(foo.numbers(1) == 5);
+// assert(foo.numbers(2) == 42);
+// }
+//
+// {
+// // Same as the last block, but do it dynamically via the Message
+// // reflection interface.
+// Message* foo = new Foo;
+// const Descriptor* descriptor = foo->GetDescriptor();
+//
+// // Get the descriptors for the fields we're interested in and verify
+// // their types.
+// const FieldDescriptor* text_field = descriptor->FindFieldByName("text");
+// assert(text_field != NULL);
+// assert(text_field->type() == FieldDescriptor::TYPE_STRING);
+// assert(text_field->label() == FieldDescriptor::LABEL_OPTIONAL);
+// const FieldDescriptor* numbers_field = descriptor->
+// FindFieldByName("numbers");
+// assert(numbers_field != NULL);
+// assert(numbers_field->type() == FieldDescriptor::TYPE_INT32);
+// assert(numbers_field->label() == FieldDescriptor::LABEL_REPEATED);
+//
+// // Parse the message.
+// foo->ParseFromString(data);
+//
+// // Use the reflection interface to examine the contents.
+// const Reflection* reflection = foo->GetReflection();
+// assert(reflection->GetString(foo, text_field) == "Hello World!");
+// assert(reflection->FieldSize(foo, numbers_field) == 3);
+// assert(reflection->GetRepeatedInt32(foo, numbers_field, 0) == 1);
+// assert(reflection->GetRepeatedInt32(foo, numbers_field, 1) == 5);
+// assert(reflection->GetRepeatedInt32(foo, numbers_field, 2) == 42);
+//
+// delete foo;
+// }
+
+#ifndef GOOGLE_PROTOBUF_MESSAGE_H__
+#define GOOGLE_PROTOBUF_MESSAGE_H__
+
+#include <iosfwd>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/message_lite.h>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/descriptor.h>
+
+
+#define GOOGLE_PROTOBUF_HAS_ONEOF
+
+namespace google {
+namespace protobuf {
+
+// Defined in this file.
+class Message;
+class Reflection;
+class MessageFactory;
+
+// Defined in other files.
+class UnknownFieldSet; // unknown_field_set.h
+namespace io {
+ class ZeroCopyInputStream; // zero_copy_stream.h
+ class ZeroCopyOutputStream; // zero_copy_stream.h
+ class CodedInputStream; // coded_stream.h
+ class CodedOutputStream; // coded_stream.h
+}
+
+
+template<typename T>
+class RepeatedField; // repeated_field.h
+
+template<typename T>
+class RepeatedPtrField; // repeated_field.h
+
+// A container to hold message metadata.
+struct Metadata {
+ const Descriptor* descriptor;
+ const Reflection* reflection;
+};
+
+// Abstract interface for protocol messages.
+//
+// See also MessageLite, which contains most every-day operations. Message
+// adds descriptors and reflection on top of that.
+//
+// The methods of this class that are virtual but not pure-virtual have
+// default implementations based on reflection. Message classes which are
+// optimized for speed will want to override these with faster implementations,
+// but classes optimized for code size may be happy with keeping them. See
+// the optimize_for option in descriptor.proto.
+class LIBPROTOBUF_EXPORT Message : public MessageLite {
+ public:
+ inline Message() {}
+ virtual ~Message();
+
+ // Basic Operations ------------------------------------------------
+
+ // Construct a new instance of the same type. Ownership is passed to the
+ // caller. (This is also defined in MessageLite, but is defined again here
+ // for return-type covariance.)
+ virtual Message* New() const = 0;
+
+ // Make this message into a copy of the given message. The given message
+ // must have the same descriptor, but need not necessarily be the same class.
+ // By default this is just implemented as "Clear(); MergeFrom(from);".
+ virtual void CopyFrom(const Message& from);
+
+ // Merge the fields from the given message into this message. Singular
+ // fields will be overwritten, if specified in from, except for embedded
+ // messages which will be merged. Repeated fields will be concatenated.
+ // The given message must be of the same type as this message (i.e. the
+ // exact same class).
+ virtual void MergeFrom(const Message& from);
+
+ // Verifies that IsInitialized() returns true. GOOGLE_CHECK-fails otherwise, with
+ // a nice error message.
+ void CheckInitialized() const;
+
+ // Slowly build a list of all required fields that are not set.
+ // This is much, much slower than IsInitialized() as it is implemented
+ // purely via reflection. Generally, you should not call this unless you
+ // have already determined that an error exists by calling IsInitialized().
+ void FindInitializationErrors(vector<string>* errors) const;
+
+ // Like FindInitializationErrors, but joins all the strings, delimited by
+ // commas, and returns them.
+ string InitializationErrorString() const;
+
+ // Clears all unknown fields from this message and all embedded messages.
+ // Normally, if unknown tag numbers are encountered when parsing a message,
+ // the tag and value are stored in the message's UnknownFieldSet and
+ // then written back out when the message is serialized. This allows servers
+ // which simply route messages to other servers to pass through messages
+ // that have new field definitions which they don't yet know about. However,
+ // this behavior can have security implications. To avoid it, call this
+ // method after parsing.
+ //
+ // See Reflection::GetUnknownFields() for more on unknown fields.
+ virtual void DiscardUnknownFields();
+
+ // Computes (an estimate of) the total number of bytes currently used for
+ // storing the message in memory. The default implementation calls the
+ // Reflection object's SpaceUsed() method.
+ virtual int SpaceUsed() const;
+
+ // Debugging & Testing----------------------------------------------
+
+ // Generates a human readable form of this message, useful for debugging
+ // and other purposes.
+ string DebugString() const;
+ // Like DebugString(), but with less whitespace.
+ string ShortDebugString() const;
+ // Like DebugString(), but do not escape UTF-8 byte sequences.
+ string Utf8DebugString() const;
+ // Convenience function useful in GDB. Prints DebugString() to stdout.
+ void PrintDebugString() const;
+
+ // Heavy I/O -------------------------------------------------------
+ // Additional parsing and serialization methods not implemented by
+ // MessageLite because they are not supported by the lite library.
+
+ // Parse a protocol buffer from a file descriptor. If successful, the entire
+ // input will be consumed.
+ bool ParseFromFileDescriptor(int file_descriptor);
+ // Like ParseFromFileDescriptor(), but accepts messages that are missing
+ // required fields.
+ bool ParsePartialFromFileDescriptor(int file_descriptor);
+ // Parse a protocol buffer from a C++ istream. If successful, the entire
+ // input will be consumed.
+ bool ParseFromIstream(istream* input);
+ // Like ParseFromIstream(), but accepts messages that are missing
+ // required fields.
+ bool ParsePartialFromIstream(istream* input);
+
+ // Serialize the message and write it to the given file descriptor. All
+ // required fields must be set.
+ bool SerializeToFileDescriptor(int file_descriptor) const;
+ // Like SerializeToFileDescriptor(), but allows missing required fields.
+ bool SerializePartialToFileDescriptor(int file_descriptor) const;
+ // Serialize the message and write it to the given C++ ostream. All
+ // required fields must be set.
+ bool SerializeToOstream(ostream* output) const;
+ // Like SerializeToOstream(), but allows missing required fields.
+ bool SerializePartialToOstream(ostream* output) const;
+
+
+ // Reflection-based methods ----------------------------------------
+ // These methods are pure-virtual in MessageLite, but Message provides
+ // reflection-based default implementations.
+
+ virtual string GetTypeName() const;
+ virtual void Clear();
+ virtual bool IsInitialized() const;
+ virtual void CheckTypeAndMergeFrom(const MessageLite& other);
+ virtual bool MergePartialFromCodedStream(io::CodedInputStream* input);
+ virtual int ByteSize() const;
+ virtual void SerializeWithCachedSizes(io::CodedOutputStream* output) const;
+
+ private:
+ // This is called only by the default implementation of ByteSize(), to
+ // update the cached size. If you override ByteSize(), you do not need
+ // to override this. If you do not override ByteSize(), you MUST override
+ // this; the default implementation will crash.
+ //
+ // The method is private because subclasses should never call it; only
+ // override it. Yes, C++ lets you do that. Crazy, huh?
+ virtual void SetCachedSize(int size) const;
+
+ public:
+
+ // Introspection ---------------------------------------------------
+
+ // Typedef for backwards-compatibility.
+ typedef google::protobuf::Reflection Reflection;
+
+ // Get a Descriptor for this message's type. This describes what
+ // fields the message contains, the types of those fields, etc.
+ const Descriptor* GetDescriptor() const { return GetMetadata().descriptor; }
+
+ // Get the Reflection interface for this Message, which can be used to
+ // read and modify the fields of the Message dynamically (in other words,
+ // without knowing the message type at compile time). This object remains
+ // property of the Message.
+ //
+ // This method remains virtual in case a subclass does not implement
+ // reflection and wants to override the default behavior.
+ virtual const Reflection* GetReflection() const {
+ return GetMetadata().reflection;
+ }
+
+ protected:
+ // Get a struct containing the metadata for the Message. Most subclasses only
+ // need to implement this method, rather than the GetDescriptor() and
+ // GetReflection() wrappers.
+ virtual Metadata GetMetadata() const = 0;
+
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Message);
+};
+
+// This interface contains methods that can be used to dynamically access
+// and modify the fields of a protocol message. Their semantics are
+// similar to the accessors the protocol compiler generates.
+//
+// To get the Reflection for a given Message, call Message::GetReflection().
+//
+// This interface is separate from Message only for efficiency reasons;
+// the vast majority of implementations of Message will share the same
+// implementation of Reflection (GeneratedMessageReflection,
+// defined in generated_message.h), and all Messages of a particular class
+// should share the same Reflection object (though you should not rely on
+// the latter fact).
+//
+// There are several ways that these methods can be used incorrectly. For
+// example, any of the following conditions will lead to undefined
+// results (probably assertion failures):
+// - The FieldDescriptor is not a field of this message type.
+// - The method called is not appropriate for the field's type. For
+// each field type in FieldDescriptor::TYPE_*, there is only one
+// Get*() method, one Set*() method, and one Add*() method that is
+// valid for that type. It should be obvious which (except maybe
+// for TYPE_BYTES, which are represented using strings in C++).
+// - A Get*() or Set*() method for singular fields is called on a repeated
+// field.
+// - GetRepeated*(), SetRepeated*(), or Add*() is called on a non-repeated
+// field.
+// - The Message object passed to any method is not of the right type for
+// this Reflection object (i.e. message.GetReflection() != reflection).
+//
+// You might wonder why there is not any abstract representation for a field
+// of arbitrary type. E.g., why isn't there just a "GetField()" method that
+// returns "const Field&", where "Field" is some class with accessors like
+// "GetInt32Value()". The problem is that someone would have to deal with
+// allocating these Field objects. For generated message classes, having to
+// allocate space for an additional object to wrap every field would at least
+// double the message's memory footprint, probably worse. Allocating the
+// objects on-demand, on the other hand, would be expensive and prone to
+// memory leaks. So, instead we ended up with this flat interface.
+//
+// TODO(kenton): Create a utility class which callers can use to read and
+// write fields from a Reflection without paying attention to the type.
+class LIBPROTOBUF_EXPORT Reflection {
+ public:
+ inline Reflection() {}
+ virtual ~Reflection();
+
+ // Get the UnknownFieldSet for the message. This contains fields which
+ // were seen when the Message was parsed but were not recognized according
+ // to the Message's definition.
+ virtual const UnknownFieldSet& GetUnknownFields(
+ const Message& message) const = 0;
+ // Get a mutable pointer to the UnknownFieldSet for the message. This
+ // contains fields which were seen when the Message was parsed but were not
+ // recognized according to the Message's definition.
+ virtual UnknownFieldSet* MutableUnknownFields(Message* message) const = 0;
+
+ // Estimate the amount of memory used by the message object.
+ virtual int SpaceUsed(const Message& message) const = 0;
+
+ // Check if the given non-repeated field is set.
+ virtual bool HasField(const Message& message,
+ const FieldDescriptor* field) const = 0;
+
+ // Get the number of elements of a repeated field.
+ virtual int FieldSize(const Message& message,
+ const FieldDescriptor* field) const = 0;
+
+ // Clear the value of a field, so that HasField() returns false or
+ // FieldSize() returns zero.
+ virtual void ClearField(Message* message,
+ const FieldDescriptor* field) const = 0;
+
+ // Check if the oneof is set. Returns ture if any field in oneof
+ // is set, false otherwise.
+ // TODO(jieluo) - make it pure virtual after updating all
+ // the subclasses.
+ virtual bool HasOneof(const Message& message,
+ const OneofDescriptor* oneof_descriptor) const {
+ return false;
+ }
+
+ virtual void ClearOneof(Message* message,
+ const OneofDescriptor* oneof_descriptor) const {}
+
+ // Returns the field descriptor if the oneof is set. NULL otherwise.
+ // TODO(jieluo) - make it pure virtual.
+ virtual const FieldDescriptor* GetOneofFieldDescriptor(
+ const Message& message,
+ const OneofDescriptor* oneof_descriptor) const {
+ return NULL;
+ }
+
+ // Removes the last element of a repeated field.
+ // We don't provide a way to remove any element other than the last
+ // because it invites inefficient use, such as O(n^2) filtering loops
+ // that should have been O(n). If you want to remove an element other
+ // than the last, the best way to do it is to re-arrange the elements
+ // (using Swap()) so that the one you want removed is at the end, then
+ // call RemoveLast().
+ virtual void RemoveLast(Message* message,
+ const FieldDescriptor* field) const = 0;
+ // Removes the last element of a repeated message field, and returns the
+ // pointer to the caller. Caller takes ownership of the returned pointer.
+ virtual Message* ReleaseLast(Message* message,
+ const FieldDescriptor* field) const = 0;
+
+ // Swap the complete contents of two messages.
+ virtual void Swap(Message* message1, Message* message2) const = 0;
+
+ // Swap fields listed in fields vector of two messages.
+ virtual void SwapFields(Message* message1,
+ Message* message2,
+ const vector<const FieldDescriptor*>& fields)
+ const = 0;
+
+ // Swap two elements of a repeated field.
+ virtual void SwapElements(Message* message,
+ const FieldDescriptor* field,
+ int index1,
+ int index2) const = 0;
+
+ // List all fields of the message which are currently set. This includes
+ // extensions. Singular fields will only be listed if HasField(field) would
+ // return true and repeated fields will only be listed if FieldSize(field)
+ // would return non-zero. Fields (both normal fields and extension fields)
+ // will be listed ordered by field number.
+ virtual void ListFields(const Message& message,
+ vector<const FieldDescriptor*>* output) const = 0;
+
+ // Singular field getters ------------------------------------------
+ // These get the value of a non-repeated field. They return the default
+ // value for fields that aren't set.
+
+ virtual int32 GetInt32 (const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual int64 GetInt64 (const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual uint32 GetUInt32(const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual uint64 GetUInt64(const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual float GetFloat (const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual double GetDouble(const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual bool GetBool (const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual string GetString(const Message& message,
+ const FieldDescriptor* field) const = 0;
+ virtual const EnumValueDescriptor* GetEnum(
+ const Message& message, const FieldDescriptor* field) const = 0;
+ // See MutableMessage() for the meaning of the "factory" parameter.
+ virtual const Message& GetMessage(const Message& message,
+ const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const = 0;
+
+ // Get a string value without copying, if possible.
+ //
+ // GetString() necessarily returns a copy of the string. This can be
+ // inefficient when the string is already stored in a string object in the
+ // underlying message. GetStringReference() will return a reference to the
+ // underlying string in this case. Otherwise, it will copy the string into
+ // *scratch and return that.
+ //
+ // Note: It is perfectly reasonable and useful to write code like:
+ // str = reflection->GetStringReference(field, &str);
+ // This line would ensure that only one copy of the string is made
+ // regardless of the field's underlying representation. When initializing
+ // a newly-constructed string, though, it's just as fast and more readable
+ // to use code like:
+ // string str = reflection->GetString(field);
+ virtual const string& GetStringReference(const Message& message,
+ const FieldDescriptor* field,
+ string* scratch) const = 0;
+
+
+ // Singular field mutators -----------------------------------------
+ // These mutate the value of a non-repeated field.
+
+ virtual void SetInt32 (Message* message,
+ const FieldDescriptor* field, int32 value) const = 0;
+ virtual void SetInt64 (Message* message,
+ const FieldDescriptor* field, int64 value) const = 0;
+ virtual void SetUInt32(Message* message,
+ const FieldDescriptor* field, uint32 value) const = 0;
+ virtual void SetUInt64(Message* message,
+ const FieldDescriptor* field, uint64 value) const = 0;
+ virtual void SetFloat (Message* message,
+ const FieldDescriptor* field, float value) const = 0;
+ virtual void SetDouble(Message* message,
+ const FieldDescriptor* field, double value) const = 0;
+ virtual void SetBool (Message* message,
+ const FieldDescriptor* field, bool value) const = 0;
+ virtual void SetString(Message* message,
+ const FieldDescriptor* field,
+ const string& value) const = 0;
+ virtual void SetEnum (Message* message,
+ const FieldDescriptor* field,
+ const EnumValueDescriptor* value) const = 0;
+ // Get a mutable pointer to a field with a message type. If a MessageFactory
+ // is provided, it will be used to construct instances of the sub-message;
+ // otherwise, the default factory is used. If the field is an extension that
+ // does not live in the same pool as the containing message's descriptor (e.g.
+ // it lives in an overlay pool), then a MessageFactory must be provided.
+ // If you have no idea what that meant, then you probably don't need to worry
+ // about it (don't provide a MessageFactory). WARNING: If the
+ // FieldDescriptor is for a compiled-in extension, then
+ // factory->GetPrototype(field->message_type() MUST return an instance of the
+ // compiled-in class for this type, NOT DynamicMessage.
+ virtual Message* MutableMessage(Message* message,
+ const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const = 0;
+ // Replaces the message specified by 'field' with the already-allocated object
+ // sub_message, passing ownership to the message. If the field contained a
+ // message, that message is deleted. If sub_message is NULL, the field is
+ // cleared.
+ virtual void SetAllocatedMessage(Message* message,
+ Message* sub_message,
+ const FieldDescriptor* field) const = 0;
+ // Releases the message specified by 'field' and returns the pointer,
+ // ReleaseMessage() will return the message the message object if it exists.
+ // Otherwise, it may or may not return NULL. In any case, if the return value
+ // is non-NULL, the caller takes ownership of the pointer.
+ // If the field existed (HasField() is true), then the returned pointer will
+ // be the same as the pointer returned by MutableMessage().
+ // This function has the same effect as ClearField().
+ virtual Message* ReleaseMessage(Message* message,
+ const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const = 0;
+
+
+ // Repeated field getters ------------------------------------------
+ // These get the value of one element of a repeated field.
+
+ virtual int32 GetRepeatedInt32 (const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual int64 GetRepeatedInt64 (const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual uint32 GetRepeatedUInt32(const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual uint64 GetRepeatedUInt64(const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual float GetRepeatedFloat (const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual double GetRepeatedDouble(const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual bool GetRepeatedBool (const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual string GetRepeatedString(const Message& message,
+ const FieldDescriptor* field,
+ int index) const = 0;
+ virtual const EnumValueDescriptor* GetRepeatedEnum(
+ const Message& message,
+ const FieldDescriptor* field, int index) const = 0;
+ virtual const Message& GetRepeatedMessage(
+ const Message& message,
+ const FieldDescriptor* field, int index) const = 0;
+
+ // See GetStringReference(), above.
+ virtual const string& GetRepeatedStringReference(
+ const Message& message, const FieldDescriptor* field,
+ int index, string* scratch) const = 0;
+
+
+ // Repeated field mutators -----------------------------------------
+ // These mutate the value of one element of a repeated field.
+
+ virtual void SetRepeatedInt32 (Message* message,
+ const FieldDescriptor* field,
+ int index, int32 value) const = 0;
+ virtual void SetRepeatedInt64 (Message* message,
+ const FieldDescriptor* field,
+ int index, int64 value) const = 0;
+ virtual void SetRepeatedUInt32(Message* message,
+ const FieldDescriptor* field,
+ int index, uint32 value) const = 0;
+ virtual void SetRepeatedUInt64(Message* message,
+ const FieldDescriptor* field,
+ int index, uint64 value) const = 0;
+ virtual void SetRepeatedFloat (Message* message,
+ const FieldDescriptor* field,
+ int index, float value) const = 0;
+ virtual void SetRepeatedDouble(Message* message,
+ const FieldDescriptor* field,
+ int index, double value) const = 0;
+ virtual void SetRepeatedBool (Message* message,
+ const FieldDescriptor* field,
+ int index, bool value) const = 0;
+ virtual void SetRepeatedString(Message* message,
+ const FieldDescriptor* field,
+ int index, const string& value) const = 0;
+ virtual void SetRepeatedEnum(Message* message,
+ const FieldDescriptor* field, int index,
+ const EnumValueDescriptor* value) const = 0;
+ // Get a mutable pointer to an element of a repeated field with a message
+ // type.
+ virtual Message* MutableRepeatedMessage(
+ Message* message, const FieldDescriptor* field, int index) const = 0;
+
+
+ // Repeated field adders -------------------------------------------
+ // These add an element to a repeated field.
+
+ virtual void AddInt32 (Message* message,
+ const FieldDescriptor* field, int32 value) const = 0;
+ virtual void AddInt64 (Message* message,
+ const FieldDescriptor* field, int64 value) const = 0;
+ virtual void AddUInt32(Message* message,
+ const FieldDescriptor* field, uint32 value) const = 0;
+ virtual void AddUInt64(Message* message,
+ const FieldDescriptor* field, uint64 value) const = 0;
+ virtual void AddFloat (Message* message,
+ const FieldDescriptor* field, float value) const = 0;
+ virtual void AddDouble(Message* message,
+ const FieldDescriptor* field, double value) const = 0;
+ virtual void AddBool (Message* message,
+ const FieldDescriptor* field, bool value) const = 0;
+ virtual void AddString(Message* message,
+ const FieldDescriptor* field,
+ const string& value) const = 0;
+ virtual void AddEnum (Message* message,
+ const FieldDescriptor* field,
+ const EnumValueDescriptor* value) const = 0;
+ // See MutableMessage() for comments on the "factory" parameter.
+ virtual Message* AddMessage(Message* message,
+ const FieldDescriptor* field,
+ MessageFactory* factory = NULL) const = 0;
+
+
+ // Repeated field accessors -------------------------------------------------
+ // The methods above, e.g. GetRepeatedInt32(msg, fd, index), provide singular
+ // access to the data in a RepeatedField. The methods below provide aggregate
+ // access by exposing the RepeatedField object itself with the Message.
+ // Applying these templates to inappropriate types will lead to an undefined
+ // reference at link time (e.g. GetRepeatedField<***double>), or possibly a
+ // template matching error at compile time (e.g. GetRepeatedPtrField<File>).
+ //
+ // Usage example: my_doubs = refl->GetRepeatedField<double>(msg, fd);
+
+ // for T = Cord and all protobuf scalar types except enums.
+ template<typename T>
+ const RepeatedField<T>& GetRepeatedField(
+ const Message&, const FieldDescriptor*) const;
+
+ // for T = Cord and all protobuf scalar types except enums.
+ template<typename T>
+ RepeatedField<T>* MutableRepeatedField(
+ Message*, const FieldDescriptor*) const;
+
+ // for T = string, google::protobuf::internal::StringPieceField
+ // google::protobuf::Message & descendants.
+ template<typename T>
+ const RepeatedPtrField<T>& GetRepeatedPtrField(
+ const Message&, const FieldDescriptor*) const;
+
+ // for T = string, google::protobuf::internal::StringPieceField
+ // google::protobuf::Message & descendants.
+ template<typename T>
+ RepeatedPtrField<T>* MutableRepeatedPtrField(
+ Message*, const FieldDescriptor*) const;
+
+ // Extensions ----------------------------------------------------------------
+
+ // Try to find an extension of this message type by fully-qualified field
+ // name. Returns NULL if no extension is known for this name or number.
+ virtual const FieldDescriptor* FindKnownExtensionByName(
+ const string& name) const = 0;
+
+ // Try to find an extension of this message type by field number.
+ // Returns NULL if no extension is known for this name or number.
+ virtual const FieldDescriptor* FindKnownExtensionByNumber(
+ int number) const = 0;
+
+ // ---------------------------------------------------------------------------
+
+ protected:
+ // Obtain a pointer to a Repeated Field Structure and do some type checking:
+ // on field->cpp_type(),
+ // on field->field_option().ctype() (if ctype >= 0)
+ // of field->message_type() (if message_type != NULL).
+ // We use 1 routine rather than 4 (const vs mutable) x (scalar vs pointer).
+ virtual void* MutableRawRepeatedField(
+ Message* message, const FieldDescriptor* field, FieldDescriptor::CppType,
+ int ctype, const Descriptor* message_type) const = 0;
+
+ private:
+ // Special version for specialized implementations of string. We can't call
+ // MutableRawRepeatedField directly here because we don't have access to
+ // FieldOptions::* which are defined in descriptor.pb.h. Including that
+ // file here is not possible because it would cause a circular include cycle.
+ void* MutableRawRepeatedString(
+ Message* message, const FieldDescriptor* field, bool is_string) const;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Reflection);
+};
+
+// Abstract interface for a factory for message objects.
+class LIBPROTOBUF_EXPORT MessageFactory {
+ public:
+ inline MessageFactory() {}
+ virtual ~MessageFactory();
+
+ // Given a Descriptor, gets or constructs the default (prototype) Message
+ // of that type. You can then call that message's New() method to construct
+ // a mutable message of that type.
+ //
+ // Calling this method twice with the same Descriptor returns the same
+ // object. The returned object remains property of the factory. Also, any
+ // objects created by calling the prototype's New() method share some data
+ // with the prototype, so these must be destroyed before the MessageFactory
+ // is destroyed.
+ //
+ // The given descriptor must outlive the returned message, and hence must
+ // outlive the MessageFactory.
+ //
+ // Some implementations do not support all types. GetPrototype() will
+ // return NULL if the descriptor passed in is not supported.
+ //
+ // This method may or may not be thread-safe depending on the implementation.
+ // Each implementation should document its own degree thread-safety.
+ virtual const Message* GetPrototype(const Descriptor* type) = 0;
+
+ // Gets a MessageFactory which supports all generated, compiled-in messages.
+ // In other words, for any compiled-in type FooMessage, the following is true:
+ // MessageFactory::generated_factory()->GetPrototype(
+ // FooMessage::descriptor()) == FooMessage::default_instance()
+ // This factory supports all types which are found in
+ // DescriptorPool::generated_pool(). If given a descriptor from any other
+ // pool, GetPrototype() will return NULL. (You can also check if a
+ // descriptor is for a generated message by checking if
+ // descriptor->file()->pool() == DescriptorPool::generated_pool().)
+ //
+ // This factory is 100% thread-safe; calling GetPrototype() does not modify
+ // any shared data.
+ //
+ // This factory is a singleton. The caller must not delete the object.
+ static MessageFactory* generated_factory();
+
+ // For internal use only: Registers a .proto file at static initialization
+ // time, to be placed in generated_factory. The first time GetPrototype()
+ // is called with a descriptor from this file, |register_messages| will be
+ // called, with the file name as the parameter. It must call
+ // InternalRegisterGeneratedMessage() (below) to register each message type
+ // in the file. This strange mechanism is necessary because descriptors are
+ // built lazily, so we can't register types by their descriptor until we
+ // know that the descriptor exists. |filename| must be a permanent string.
+ static void InternalRegisterGeneratedFile(
+ const char* filename, void (*register_messages)(const string&));
+
+ // For internal use only: Registers a message type. Called only by the
+ // functions which are registered with InternalRegisterGeneratedFile(),
+ // above.
+ static void InternalRegisterGeneratedMessage(const Descriptor* descriptor,
+ const Message* prototype);
+
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MessageFactory);
+};
+
+#define DECLARE_GET_REPEATED_FIELD(TYPE) \
+template<> \
+LIBPROTOBUF_EXPORT \
+const RepeatedField<TYPE>& Reflection::GetRepeatedField<TYPE>( \
+ const Message& message, const FieldDescriptor* field) const; \
+ \
+template<> \
+RepeatedField<TYPE>* Reflection::MutableRepeatedField<TYPE>( \
+ Message* message, const FieldDescriptor* field) const;
+
+DECLARE_GET_REPEATED_FIELD(int32)
+DECLARE_GET_REPEATED_FIELD(int64)
+DECLARE_GET_REPEATED_FIELD(uint32)
+DECLARE_GET_REPEATED_FIELD(uint64)
+DECLARE_GET_REPEATED_FIELD(float)
+DECLARE_GET_REPEATED_FIELD(double)
+DECLARE_GET_REPEATED_FIELD(bool)
+
+#undef DECLARE_GET_REPEATED_FIELD
+
+// =============================================================================
+// Implementation details for {Get,Mutable}RawRepeatedPtrField. We provide
+// specializations for <string>, <StringPieceField> and <Message> and handle
+// everything else with the default template which will match any type having
+// a method with signature "static const google::protobuf::Descriptor* descriptor()".
+// Such a type presumably is a descendant of google::protobuf::Message.
+
+template<>
+inline const RepeatedPtrField<string>& Reflection::GetRepeatedPtrField<string>(
+ const Message& message, const FieldDescriptor* field) const {
+ return *static_cast<RepeatedPtrField<string>* >(
+ MutableRawRepeatedString(const_cast<Message*>(&message), field, true));
+}
+
+template<>
+inline RepeatedPtrField<string>* Reflection::MutableRepeatedPtrField<string>(
+ Message* message, const FieldDescriptor* field) const {
+ return static_cast<RepeatedPtrField<string>* >(
+ MutableRawRepeatedString(message, field, true));
+}
+
+
+// -----
+
+template<>
+inline const RepeatedPtrField<Message>& Reflection::GetRepeatedPtrField(
+ const Message& message, const FieldDescriptor* field) const {
+ return *static_cast<RepeatedPtrField<Message>* >(
+ MutableRawRepeatedField(const_cast<Message*>(&message), field,
+ FieldDescriptor::CPPTYPE_MESSAGE, -1,
+ NULL));
+}
+
+template<>
+inline RepeatedPtrField<Message>* Reflection::MutableRepeatedPtrField(
+ Message* message, const FieldDescriptor* field) const {
+ return static_cast<RepeatedPtrField<Message>* >(
+ MutableRawRepeatedField(message, field,
+ FieldDescriptor::CPPTYPE_MESSAGE, -1,
+ NULL));
+}
+
+template<typename PB>
+inline const RepeatedPtrField<PB>& Reflection::GetRepeatedPtrField(
+ const Message& message, const FieldDescriptor* field) const {
+ return *static_cast<RepeatedPtrField<PB>* >(
+ MutableRawRepeatedField(const_cast<Message*>(&message), field,
+ FieldDescriptor::CPPTYPE_MESSAGE, -1,
+ PB::default_instance().GetDescriptor()));
+}
+
+template<typename PB>
+inline RepeatedPtrField<PB>* Reflection::MutableRepeatedPtrField(
+ Message* message, const FieldDescriptor* field) const {
+ return static_cast<RepeatedPtrField<PB>* >(
+ MutableRawRepeatedField(message, field,
+ FieldDescriptor::CPPTYPE_MESSAGE, -1,
+ PB::default_instance().GetDescriptor()));
+}
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_MESSAGE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/message_lite.cc b/toolkit/components/protobuf/src/google/protobuf/message_lite.cc
new file mode 100644
index 0000000000..14cdc91fdd
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/message_lite.cc
@@ -0,0 +1,335 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Authors: wink@google.com (Wink Saville),
+// kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/message_lite.h>
+#include <string>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+
+MessageLite::~MessageLite() {}
+
+string MessageLite::InitializationErrorString() const {
+ return "(cannot determine missing fields for lite message)";
+}
+
+namespace {
+
+// When serializing, we first compute the byte size, then serialize the message.
+// If serialization produces a different number of bytes than expected, we
+// call this function, which crashes. The problem could be due to a bug in the
+// protobuf implementation but is more likely caused by concurrent modification
+// of the message. This function attempts to distinguish between the two and
+// provide a useful error message.
+void ByteSizeConsistencyError(int byte_size_before_serialization,
+ int byte_size_after_serialization,
+ int bytes_produced_by_serialization) {
+ GOOGLE_CHECK_EQ(byte_size_before_serialization, byte_size_after_serialization)
+ << "Protocol message was modified concurrently during serialization.";
+ GOOGLE_CHECK_EQ(bytes_produced_by_serialization, byte_size_before_serialization)
+ << "Byte size calculation and serialization were inconsistent. This "
+ "may indicate a bug in protocol buffers or it may be caused by "
+ "concurrent modification of the message.";
+ GOOGLE_LOG(FATAL) << "This shouldn't be called if all the sizes are equal.";
+}
+
+string InitializationErrorMessage(const char* action,
+ const MessageLite& message) {
+ // Note: We want to avoid depending on strutil in the lite library, otherwise
+ // we'd use:
+ //
+ // return strings::Substitute(
+ // "Can't $0 message of type \"$1\" because it is missing required "
+ // "fields: $2",
+ // action, message.GetTypeName(),
+ // message.InitializationErrorString());
+
+ string result;
+ result += "Can't ";
+ result += action;
+ result += " message of type \"";
+ result += message.GetTypeName();
+ result += "\" because it is missing required fields: ";
+ result += message.InitializationErrorString();
+ return result;
+}
+
+// Several of the Parse methods below just do one thing and then call another
+// method. In a naive implementation, we might have ParseFromString() call
+// ParseFromArray() which would call ParseFromZeroCopyStream() which would call
+// ParseFromCodedStream() which would call MergeFromCodedStream() which would
+// call MergePartialFromCodedStream(). However, when parsing very small
+// messages, every function call introduces significant overhead. To avoid
+// this without reproducing code, we use these forced-inline helpers.
+//
+// Note: GCC only allows GOOGLE_ATTRIBUTE_ALWAYS_INLINE on declarations, not
+// definitions.
+inline bool InlineMergeFromCodedStream(io::CodedInputStream* input,
+ MessageLite* message)
+ GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+inline bool InlineParseFromCodedStream(io::CodedInputStream* input,
+ MessageLite* message)
+ GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+inline bool InlineParsePartialFromCodedStream(io::CodedInputStream* input,
+ MessageLite* message)
+ GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+inline bool InlineParseFromArray(const void* data, int size,
+ MessageLite* message)
+ GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+inline bool InlineParsePartialFromArray(const void* data, int size,
+ MessageLite* message)
+ GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+bool InlineMergeFromCodedStream(io::CodedInputStream* input,
+ MessageLite* message) {
+ if (!message->MergePartialFromCodedStream(input)) return false;
+ if (!message->IsInitialized()) {
+ GOOGLE_LOG(ERROR) << InitializationErrorMessage("parse", *message);
+ return false;
+ }
+ return true;
+}
+
+bool InlineParseFromCodedStream(io::CodedInputStream* input,
+ MessageLite* message) {
+ message->Clear();
+ return InlineMergeFromCodedStream(input, message);
+}
+
+bool InlineParsePartialFromCodedStream(io::CodedInputStream* input,
+ MessageLite* message) {
+ message->Clear();
+ return message->MergePartialFromCodedStream(input);
+}
+
+bool InlineParseFromArray(const void* data, int size, MessageLite* message) {
+ io::CodedInputStream input(reinterpret_cast<const uint8*>(data), size);
+ return InlineParseFromCodedStream(&input, message) &&
+ input.ConsumedEntireMessage();
+}
+
+bool InlineParsePartialFromArray(const void* data, int size,
+ MessageLite* message) {
+ io::CodedInputStream input(reinterpret_cast<const uint8*>(data), size);
+ return InlineParsePartialFromCodedStream(&input, message) &&
+ input.ConsumedEntireMessage();
+}
+
+} // namespace
+
+bool MessageLite::MergeFromCodedStream(io::CodedInputStream* input) {
+ return InlineMergeFromCodedStream(input, this);
+}
+
+bool MessageLite::ParseFromCodedStream(io::CodedInputStream* input) {
+ return InlineParseFromCodedStream(input, this);
+}
+
+bool MessageLite::ParsePartialFromCodedStream(io::CodedInputStream* input) {
+ return InlineParsePartialFromCodedStream(input, this);
+}
+
+bool MessageLite::ParseFromZeroCopyStream(io::ZeroCopyInputStream* input) {
+ io::CodedInputStream decoder(input);
+ return ParseFromCodedStream(&decoder) && decoder.ConsumedEntireMessage();
+}
+
+bool MessageLite::ParsePartialFromZeroCopyStream(
+ io::ZeroCopyInputStream* input) {
+ io::CodedInputStream decoder(input);
+ return ParsePartialFromCodedStream(&decoder) &&
+ decoder.ConsumedEntireMessage();
+}
+
+bool MessageLite::ParseFromBoundedZeroCopyStream(
+ io::ZeroCopyInputStream* input, int size) {
+ io::CodedInputStream decoder(input);
+ decoder.PushLimit(size);
+ return ParseFromCodedStream(&decoder) &&
+ decoder.ConsumedEntireMessage() &&
+ decoder.BytesUntilLimit() == 0;
+}
+
+bool MessageLite::ParsePartialFromBoundedZeroCopyStream(
+ io::ZeroCopyInputStream* input, int size) {
+ io::CodedInputStream decoder(input);
+ decoder.PushLimit(size);
+ return ParsePartialFromCodedStream(&decoder) &&
+ decoder.ConsumedEntireMessage() &&
+ decoder.BytesUntilLimit() == 0;
+}
+
+bool MessageLite::ParseFromString(const string& data) {
+ return InlineParseFromArray(data.data(), data.size(), this);
+}
+
+bool MessageLite::ParsePartialFromString(const string& data) {
+ return InlineParsePartialFromArray(data.data(), data.size(), this);
+}
+
+bool MessageLite::ParseFromArray(const void* data, int size) {
+ return InlineParseFromArray(data, size, this);
+}
+
+bool MessageLite::ParsePartialFromArray(const void* data, int size) {
+ return InlineParsePartialFromArray(data, size, this);
+}
+
+
+// ===================================================================
+
+uint8* MessageLite::SerializeWithCachedSizesToArray(uint8* target) const {
+ // We only optimize this when using optimize_for = SPEED. In other cases
+ // we just use the CodedOutputStream path.
+ int size = GetCachedSize();
+ io::ArrayOutputStream out(target, size);
+ io::CodedOutputStream coded_out(&out);
+ SerializeWithCachedSizes(&coded_out);
+ GOOGLE_CHECK(!coded_out.HadError());
+ return target + size;
+}
+
+bool MessageLite::SerializeToCodedStream(io::CodedOutputStream* output) const {
+ GOOGLE_DCHECK(IsInitialized()) << InitializationErrorMessage("serialize", *this);
+ return SerializePartialToCodedStream(output);
+}
+
+bool MessageLite::SerializePartialToCodedStream(
+ io::CodedOutputStream* output) const {
+ const int size = ByteSize(); // Force size to be cached.
+ uint8* buffer = output->GetDirectBufferForNBytesAndAdvance(size);
+ if (buffer != NULL) {
+ uint8* end = SerializeWithCachedSizesToArray(buffer);
+ if (end - buffer != size) {
+ ByteSizeConsistencyError(size, ByteSize(), end - buffer);
+ }
+ return true;
+ } else {
+ int original_byte_count = output->ByteCount();
+ SerializeWithCachedSizes(output);
+ if (output->HadError()) {
+ return false;
+ }
+ int final_byte_count = output->ByteCount();
+
+ if (final_byte_count - original_byte_count != size) {
+ ByteSizeConsistencyError(size, ByteSize(),
+ final_byte_count - original_byte_count);
+ }
+
+ return true;
+ }
+}
+
+bool MessageLite::SerializeToZeroCopyStream(
+ io::ZeroCopyOutputStream* output) const {
+ io::CodedOutputStream encoder(output);
+ return SerializeToCodedStream(&encoder);
+}
+
+bool MessageLite::SerializePartialToZeroCopyStream(
+ io::ZeroCopyOutputStream* output) const {
+ io::CodedOutputStream encoder(output);
+ return SerializePartialToCodedStream(&encoder);
+}
+
+bool MessageLite::AppendToString(string* output) const {
+ GOOGLE_DCHECK(IsInitialized()) << InitializationErrorMessage("serialize", *this);
+ return AppendPartialToString(output);
+}
+
+bool MessageLite::AppendPartialToString(string* output) const {
+ int old_size = output->size();
+ int byte_size = ByteSize();
+ STLStringResizeUninitialized(output, old_size + byte_size);
+ uint8* start =
+ reinterpret_cast<uint8*>(io::mutable_string_data(output) + old_size);
+ uint8* end = SerializeWithCachedSizesToArray(start);
+ if (end - start != byte_size) {
+ ByteSizeConsistencyError(byte_size, ByteSize(), end - start);
+ }
+ return true;
+}
+
+bool MessageLite::SerializeToString(string* output) const {
+ output->clear();
+ return AppendToString(output);
+}
+
+bool MessageLite::SerializePartialToString(string* output) const {
+ output->clear();
+ return AppendPartialToString(output);
+}
+
+bool MessageLite::SerializeToArray(void* data, int size) const {
+ GOOGLE_DCHECK(IsInitialized()) << InitializationErrorMessage("serialize", *this);
+ return SerializePartialToArray(data, size);
+}
+
+bool MessageLite::SerializePartialToArray(void* data, int size) const {
+ int byte_size = ByteSize();
+ if (size < byte_size) return false;
+ uint8* start = reinterpret_cast<uint8*>(data);
+ uint8* end = SerializeWithCachedSizesToArray(start);
+ if (end - start != byte_size) {
+ ByteSizeConsistencyError(byte_size, ByteSize(), end - start);
+ }
+ return true;
+}
+
+string MessageLite::SerializeAsString() const {
+ // If the compiler implements the (Named) Return Value Optimization,
+ // the local variable 'result' will not actually reside on the stack
+ // of this function, but will be overlaid with the object that the
+ // caller supplied for the return value to be constructed in.
+ string output;
+ if (!AppendToString(&output))
+ output.clear();
+ return output;
+}
+
+string MessageLite::SerializePartialAsString() const {
+ string output;
+ if (!AppendPartialToString(&output))
+ output.clear();
+ return output;
+}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/message_lite.h b/toolkit/components/protobuf/src/google/protobuf/message_lite.h
new file mode 100644
index 0000000000..027cabf919
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/message_lite.h
@@ -0,0 +1,247 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Authors: wink@google.com (Wink Saville),
+// kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Defines MessageLite, the abstract interface implemented by all (lite
+// and non-lite) protocol message objects.
+
+#ifndef GOOGLE_PROTOBUF_MESSAGE_LITE_H__
+#define GOOGLE_PROTOBUF_MESSAGE_LITE_H__
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+namespace io {
+ class CodedInputStream;
+ class CodedOutputStream;
+ class ZeroCopyInputStream;
+ class ZeroCopyOutputStream;
+}
+
+// Interface to light weight protocol messages.
+//
+// This interface is implemented by all protocol message objects. Non-lite
+// messages additionally implement the Message interface, which is a
+// subclass of MessageLite. Use MessageLite instead when you only need
+// the subset of features which it supports -- namely, nothing that uses
+// descriptors or reflection. You can instruct the protocol compiler
+// to generate classes which implement only MessageLite, not the full
+// Message interface, by adding the following line to the .proto file:
+//
+// option optimize_for = LITE_RUNTIME;
+//
+// This is particularly useful on resource-constrained systems where
+// the full protocol buffers runtime library is too big.
+//
+// Note that on non-constrained systems (e.g. servers) when you need
+// to link in lots of protocol definitions, a better way to reduce
+// total code footprint is to use optimize_for = CODE_SIZE. This
+// will make the generated code smaller while still supporting all the
+// same features (at the expense of speed). optimize_for = LITE_RUNTIME
+// is best when you only have a small number of message types linked
+// into your binary, in which case the size of the protocol buffers
+// runtime itself is the biggest problem.
+class LIBPROTOBUF_EXPORT MessageLite {
+ public:
+ inline MessageLite() {}
+ virtual ~MessageLite();
+
+ // Basic Operations ------------------------------------------------
+
+ // Get the name of this message type, e.g. "foo.bar.BazProto".
+ virtual string GetTypeName() const = 0;
+
+ // Construct a new instance of the same type. Ownership is passed to the
+ // caller.
+ virtual MessageLite* New() const = 0;
+
+ // Clear all fields of the message and set them to their default values.
+ // Clear() avoids freeing memory, assuming that any memory allocated
+ // to hold parts of the message will be needed again to hold the next
+ // message. If you actually want to free the memory used by a Message,
+ // you must delete it.
+ virtual void Clear() = 0;
+
+ // Quickly check if all required fields have values set.
+ virtual bool IsInitialized() const = 0;
+
+ // This is not implemented for Lite messages -- it just returns "(cannot
+ // determine missing fields for lite message)". However, it is implemented
+ // for full messages. See message.h.
+ virtual string InitializationErrorString() const;
+
+ // If |other| is the exact same class as this, calls MergeFrom(). Otherwise,
+ // results are undefined (probably crash).
+ virtual void CheckTypeAndMergeFrom(const MessageLite& other) = 0;
+
+ // Parsing ---------------------------------------------------------
+ // Methods for parsing in protocol buffer format. Most of these are
+ // just simple wrappers around MergeFromCodedStream(). Clear() will be called
+ // before merging the input.
+
+ // Fill the message with a protocol buffer parsed from the given input
+ // stream. Returns false on a read error or if the input is in the
+ // wrong format.
+ bool ParseFromCodedStream(io::CodedInputStream* input);
+ // Like ParseFromCodedStream(), but accepts messages that are missing
+ // required fields.
+ bool ParsePartialFromCodedStream(io::CodedInputStream* input);
+ // Read a protocol buffer from the given zero-copy input stream. If
+ // successful, the entire input will be consumed.
+ bool ParseFromZeroCopyStream(io::ZeroCopyInputStream* input);
+ // Like ParseFromZeroCopyStream(), but accepts messages that are missing
+ // required fields.
+ bool ParsePartialFromZeroCopyStream(io::ZeroCopyInputStream* input);
+ // Read a protocol buffer from the given zero-copy input stream, expecting
+ // the message to be exactly "size" bytes long. If successful, exactly
+ // this many bytes will have been consumed from the input.
+ bool ParseFromBoundedZeroCopyStream(io::ZeroCopyInputStream* input, int size);
+ // Like ParseFromBoundedZeroCopyStream(), but accepts messages that are
+ // missing required fields.
+ bool ParsePartialFromBoundedZeroCopyStream(io::ZeroCopyInputStream* input,
+ int size);
+ // Parse a protocol buffer contained in a string.
+ bool ParseFromString(const string& data);
+ // Like ParseFromString(), but accepts messages that are missing
+ // required fields.
+ bool ParsePartialFromString(const string& data);
+ // Parse a protocol buffer contained in an array of bytes.
+ bool ParseFromArray(const void* data, int size);
+ // Like ParseFromArray(), but accepts messages that are missing
+ // required fields.
+ bool ParsePartialFromArray(const void* data, int size);
+
+
+ // Reads a protocol buffer from the stream and merges it into this
+ // Message. Singular fields read from the input overwrite what is
+ // already in the Message and repeated fields are appended to those
+ // already present.
+ //
+ // It is the responsibility of the caller to call input->LastTagWas()
+ // (for groups) or input->ConsumedEntireMessage() (for non-groups) after
+ // this returns to verify that the message's end was delimited correctly.
+ //
+ // ParsefromCodedStream() is implemented as Clear() followed by
+ // MergeFromCodedStream().
+ bool MergeFromCodedStream(io::CodedInputStream* input);
+
+ // Like MergeFromCodedStream(), but succeeds even if required fields are
+ // missing in the input.
+ //
+ // MergeFromCodedStream() is just implemented as MergePartialFromCodedStream()
+ // followed by IsInitialized().
+ virtual bool MergePartialFromCodedStream(io::CodedInputStream* input) = 0;
+
+
+ // Serialization ---------------------------------------------------
+ // Methods for serializing in protocol buffer format. Most of these
+ // are just simple wrappers around ByteSize() and SerializeWithCachedSizes().
+
+ // Write a protocol buffer of this message to the given output. Returns
+ // false on a write error. If the message is missing required fields,
+ // this may GOOGLE_CHECK-fail.
+ bool SerializeToCodedStream(io::CodedOutputStream* output) const;
+ // Like SerializeToCodedStream(), but allows missing required fields.
+ bool SerializePartialToCodedStream(io::CodedOutputStream* output) const;
+ // Write the message to the given zero-copy output stream. All required
+ // fields must be set.
+ bool SerializeToZeroCopyStream(io::ZeroCopyOutputStream* output) const;
+ // Like SerializeToZeroCopyStream(), but allows missing required fields.
+ bool SerializePartialToZeroCopyStream(io::ZeroCopyOutputStream* output) const;
+ // Serialize the message and store it in the given string. All required
+ // fields must be set.
+ bool SerializeToString(string* output) const;
+ // Like SerializeToString(), but allows missing required fields.
+ bool SerializePartialToString(string* output) const;
+ // Serialize the message and store it in the given byte array. All required
+ // fields must be set.
+ bool SerializeToArray(void* data, int size) const;
+ // Like SerializeToArray(), but allows missing required fields.
+ bool SerializePartialToArray(void* data, int size) const;
+
+ // Make a string encoding the message. Is equivalent to calling
+ // SerializeToString() on a string and using that. Returns the empty
+ // string if SerializeToString() would have returned an error.
+ // Note: If you intend to generate many such strings, you may
+ // reduce heap fragmentation by instead re-using the same string
+ // object with calls to SerializeToString().
+ string SerializeAsString() const;
+ // Like SerializeAsString(), but allows missing required fields.
+ string SerializePartialAsString() const;
+
+ // Like SerializeToString(), but appends to the data to the string's existing
+ // contents. All required fields must be set.
+ bool AppendToString(string* output) const;
+ // Like AppendToString(), but allows missing required fields.
+ bool AppendPartialToString(string* output) const;
+
+ // Computes the serialized size of the message. This recursively calls
+ // ByteSize() on all embedded messages. If a subclass does not override
+ // this, it MUST override SetCachedSize().
+ virtual int ByteSize() const = 0;
+
+ // Serializes the message without recomputing the size. The message must
+ // not have changed since the last call to ByteSize(); if it has, the results
+ // are undefined.
+ virtual void SerializeWithCachedSizes(
+ io::CodedOutputStream* output) const = 0;
+
+ // Like SerializeWithCachedSizes, but writes directly to *target, returning
+ // a pointer to the byte immediately after the last byte written. "target"
+ // must point at a byte array of at least ByteSize() bytes.
+ virtual uint8* SerializeWithCachedSizesToArray(uint8* target) const;
+
+ // Returns the result of the last call to ByteSize(). An embedded message's
+ // size is needed both to serialize it (because embedded messages are
+ // length-delimited) and to compute the outer message's size. Caching
+ // the size avoids computing it multiple times.
+ //
+ // ByteSize() does not automatically use the cached size when available
+ // because this would require invalidating it every time the message was
+ // modified, which would be too hard and expensive. (E.g. if a deeply-nested
+ // sub-message is changed, all of its parents' cached sizes would need to be
+ // invalidated, which is too much work for an otherwise inlined setter
+ // method.)
+ virtual int GetCachedSize() const = 0;
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MessageLite);
+};
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_MESSAGE_LITE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/package_info.h b/toolkit/components/protobuf/src/google/protobuf/package_info.h
new file mode 100644
index 0000000000..935e96396d
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/package_info.h
@@ -0,0 +1,64 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This file exists solely to document the google::protobuf namespace.
+// It is not compiled into anything, but it may be read by an automated
+// documentation generator.
+
+namespace google {
+
+// Core components of the Protocol Buffers runtime library.
+//
+// The files in this package represent the core of the Protocol Buffer
+// system. All of them are part of the libprotobuf library.
+//
+// A note on thread-safety:
+//
+// Thread-safety in the Protocol Buffer library follows a simple rule:
+// unless explicitly noted otherwise, it is always safe to use an object
+// from multiple threads simultaneously as long as the object is declared
+// const in all threads (or, it is only used in ways that would be allowed
+// if it were declared const). However, if an object is accessed in one
+// thread in a way that would not be allowed if it were const, then it is
+// not safe to access that object in any other thread simultaneously.
+//
+// Put simply, read-only access to an object can happen in multiple threads
+// simultaneously, but write access can only happen in a single thread at
+// a time.
+//
+// The implementation does contain some "const" methods which actually modify
+// the object behind the scenes -- e.g., to cache results -- but in these cases
+// mutex locking is used to make the access thread-safe.
+namespace protobuf {}
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/reflection_ops.cc b/toolkit/components/protobuf/src/google/protobuf/reflection_ops.cc
new file mode 100644
index 0000000000..4629dec251
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/reflection_ops.cc
@@ -0,0 +1,269 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <string>
+#include <vector>
+
+#include <google/protobuf/reflection_ops.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/unknown_field_set.h>
+#include <google/protobuf/stubs/strutil.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+void ReflectionOps::Copy(const Message& from, Message* to) {
+ if (&from == to) return;
+ Clear(to);
+ Merge(from, to);
+}
+
+void ReflectionOps::Merge(const Message& from, Message* to) {
+ GOOGLE_CHECK_NE(&from, to);
+
+ const Descriptor* descriptor = from.GetDescriptor();
+ GOOGLE_CHECK_EQ(to->GetDescriptor(), descriptor)
+ << "Tried to merge messages of different types "
+ << "(merge " << descriptor->full_name()
+ << " to " << to->GetDescriptor()->full_name() << ")";
+
+ const Reflection* from_reflection = from.GetReflection();
+ const Reflection* to_reflection = to->GetReflection();
+
+ vector<const FieldDescriptor*> fields;
+ from_reflection->ListFields(from, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ const FieldDescriptor* field = fields[i];
+
+ if (field->is_repeated()) {
+ int count = from_reflection->FieldSize(from, field);
+ for (int j = 0; j < count; j++) {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(CPPTYPE, METHOD) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ to_reflection->Add##METHOD(to, field, \
+ from_reflection->GetRepeated##METHOD(from, field, j)); \
+ break;
+
+ HANDLE_TYPE(INT32 , Int32 );
+ HANDLE_TYPE(INT64 , Int64 );
+ HANDLE_TYPE(UINT32, UInt32);
+ HANDLE_TYPE(UINT64, UInt64);
+ HANDLE_TYPE(FLOAT , Float );
+ HANDLE_TYPE(DOUBLE, Double);
+ HANDLE_TYPE(BOOL , Bool );
+ HANDLE_TYPE(STRING, String);
+ HANDLE_TYPE(ENUM , Enum );
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ to_reflection->AddMessage(to, field)->MergeFrom(
+ from_reflection->GetRepeatedMessage(from, field, j));
+ break;
+ }
+ }
+ } else {
+ switch (field->cpp_type()) {
+#define HANDLE_TYPE(CPPTYPE, METHOD) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ to_reflection->Set##METHOD(to, field, \
+ from_reflection->Get##METHOD(from, field)); \
+ break;
+
+ HANDLE_TYPE(INT32 , Int32 );
+ HANDLE_TYPE(INT64 , Int64 );
+ HANDLE_TYPE(UINT32, UInt32);
+ HANDLE_TYPE(UINT64, UInt64);
+ HANDLE_TYPE(FLOAT , Float );
+ HANDLE_TYPE(DOUBLE, Double);
+ HANDLE_TYPE(BOOL , Bool );
+ HANDLE_TYPE(STRING, String);
+ HANDLE_TYPE(ENUM , Enum );
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ to_reflection->MutableMessage(to, field)->MergeFrom(
+ from_reflection->GetMessage(from, field));
+ break;
+ }
+ }
+ }
+
+ to_reflection->MutableUnknownFields(to)->MergeFrom(
+ from_reflection->GetUnknownFields(from));
+}
+
+void ReflectionOps::Clear(Message* message) {
+ const Reflection* reflection = message->GetReflection();
+
+ vector<const FieldDescriptor*> fields;
+ reflection->ListFields(*message, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ reflection->ClearField(message, fields[i]);
+ }
+
+ reflection->MutableUnknownFields(message)->Clear();
+}
+
+bool ReflectionOps::IsInitialized(const Message& message) {
+ const Descriptor* descriptor = message.GetDescriptor();
+ const Reflection* reflection = message.GetReflection();
+
+ // Check required fields of this message.
+ for (int i = 0; i < descriptor->field_count(); i++) {
+ if (descriptor->field(i)->is_required()) {
+ if (!reflection->HasField(message, descriptor->field(i))) {
+ return false;
+ }
+ }
+ }
+
+ // Check that sub-messages are initialized.
+ vector<const FieldDescriptor*> fields;
+ reflection->ListFields(message, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ const FieldDescriptor* field = fields[i];
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+
+ if (field->is_repeated()) {
+ int size = reflection->FieldSize(message, field);
+
+ for (int j = 0; j < size; j++) {
+ if (!reflection->GetRepeatedMessage(message, field, j)
+ .IsInitialized()) {
+ return false;
+ }
+ }
+ } else {
+ if (!reflection->GetMessage(message, field).IsInitialized()) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+void ReflectionOps::DiscardUnknownFields(Message* message) {
+ const Reflection* reflection = message->GetReflection();
+
+ reflection->MutableUnknownFields(message)->Clear();
+
+ vector<const FieldDescriptor*> fields;
+ reflection->ListFields(*message, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ const FieldDescriptor* field = fields[i];
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ if (field->is_repeated()) {
+ int size = reflection->FieldSize(*message, field);
+ for (int j = 0; j < size; j++) {
+ reflection->MutableRepeatedMessage(message, field, j)
+ ->DiscardUnknownFields();
+ }
+ } else {
+ reflection->MutableMessage(message, field)->DiscardUnknownFields();
+ }
+ }
+ }
+}
+
+static string SubMessagePrefix(const string& prefix,
+ const FieldDescriptor* field,
+ int index) {
+ string result(prefix);
+ if (field->is_extension()) {
+ result.append("(");
+ result.append(field->full_name());
+ result.append(")");
+ } else {
+ result.append(field->name());
+ }
+ if (index != -1) {
+ result.append("[");
+ result.append(SimpleItoa(index));
+ result.append("]");
+ }
+ result.append(".");
+ return result;
+}
+
+void ReflectionOps::FindInitializationErrors(
+ const Message& message,
+ const string& prefix,
+ vector<string>* errors) {
+ const Descriptor* descriptor = message.GetDescriptor();
+ const Reflection* reflection = message.GetReflection();
+
+ // Check required fields of this message.
+ for (int i = 0; i < descriptor->field_count(); i++) {
+ if (descriptor->field(i)->is_required()) {
+ if (!reflection->HasField(message, descriptor->field(i))) {
+ errors->push_back(prefix + descriptor->field(i)->name());
+ }
+ }
+ }
+
+ // Check sub-messages.
+ vector<const FieldDescriptor*> fields;
+ reflection->ListFields(message, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ const FieldDescriptor* field = fields[i];
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+
+ if (field->is_repeated()) {
+ int size = reflection->FieldSize(message, field);
+
+ for (int j = 0; j < size; j++) {
+ const Message& sub_message =
+ reflection->GetRepeatedMessage(message, field, j);
+ FindInitializationErrors(sub_message,
+ SubMessagePrefix(prefix, field, j),
+ errors);
+ }
+ } else {
+ const Message& sub_message = reflection->GetMessage(message, field);
+ FindInitializationErrors(sub_message,
+ SubMessagePrefix(prefix, field, -1),
+ errors);
+ }
+ }
+ }
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/reflection_ops.h b/toolkit/components/protobuf/src/google/protobuf/reflection_ops.h
new file mode 100644
index 0000000000..4775911e84
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/reflection_ops.h
@@ -0,0 +1,81 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This header is logically internal, but is made public because it is used
+// from protocol-compiler-generated code, which may reside in other components.
+
+#ifndef GOOGLE_PROTOBUF_REFLECTION_OPS_H__
+#define GOOGLE_PROTOBUF_REFLECTION_OPS_H__
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/message.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// Basic operations that can be performed using reflection.
+// These can be used as a cheap way to implement the corresponding
+// methods of the Message interface, though they are likely to be
+// slower than implementations tailored for the specific message type.
+//
+// This class should stay limited to operations needed to implement
+// the Message interface.
+//
+// This class is really a namespace that contains only static methods.
+class LIBPROTOBUF_EXPORT ReflectionOps {
+ public:
+ static void Copy(const Message& from, Message* to);
+ static void Merge(const Message& from, Message* to);
+ static void Clear(Message* message);
+ static bool IsInitialized(const Message& message);
+ static void DiscardUnknownFields(Message* message);
+
+ // Finds all unset required fields in the message and adds their full
+ // paths (e.g. "foo.bar[5].baz") to *names. "prefix" will be attached to
+ // the front of each name.
+ static void FindInitializationErrors(const Message& message,
+ const string& prefix,
+ vector<string>* errors);
+
+ private:
+ // All methods are static. No need to construct.
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ReflectionOps);
+};
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_REFLECTION_OPS_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/repeated_field.cc b/toolkit/components/protobuf/src/google/protobuf/repeated_field.cc
new file mode 100644
index 0000000000..b400732fe0
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/repeated_field.cc
@@ -0,0 +1,87 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <algorithm>
+
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+namespace internal {
+
+void RepeatedPtrFieldBase::Reserve(int new_size) {
+ if (total_size_ >= new_size) return;
+
+ void** old_elements = elements_;
+ total_size_ = max(kMinRepeatedFieldAllocationSize,
+ max(total_size_ * 2, new_size));
+ elements_ = new void*[total_size_];
+ if (old_elements != NULL) {
+ memcpy(elements_, old_elements, allocated_size_ * sizeof(elements_[0]));
+ delete [] old_elements;
+ }
+}
+
+void RepeatedPtrFieldBase::Swap(RepeatedPtrFieldBase* other) {
+ if (this == other) return;
+ void** swap_elements = elements_;
+ int swap_current_size = current_size_;
+ int swap_allocated_size = allocated_size_;
+ int swap_total_size = total_size_;
+
+ elements_ = other->elements_;
+ current_size_ = other->current_size_;
+ allocated_size_ = other->allocated_size_;
+ total_size_ = other->total_size_;
+
+ other->elements_ = swap_elements;
+ other->current_size_ = swap_current_size;
+ other->allocated_size_ = swap_allocated_size;
+ other->total_size_ = swap_total_size;
+}
+
+string* StringTypeHandlerBase::New() {
+ return new string;
+}
+void StringTypeHandlerBase::Delete(string* value) {
+ delete value;
+}
+
+} // namespace internal
+
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/repeated_field.h b/toolkit/components/protobuf/src/google/protobuf/repeated_field.h
new file mode 100644
index 0000000000..50051831d6
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/repeated_field.h
@@ -0,0 +1,1603 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// RepeatedField and RepeatedPtrField are used by generated protocol message
+// classes to manipulate repeated fields. These classes are very similar to
+// STL's vector, but include a number of optimizations found to be useful
+// specifically in the case of Protocol Buffers. RepeatedPtrField is
+// particularly different from STL vector as it manages ownership of the
+// pointers that it contains.
+//
+// Typically, clients should not need to access RepeatedField objects directly,
+// but should instead use the accessor functions generated automatically by the
+// protocol compiler.
+
+#ifndef GOOGLE_PROTOBUF_REPEATED_FIELD_H__
+#define GOOGLE_PROTOBUF_REPEATED_FIELD_H__
+
+#ifdef _MSC_VER
+// This is required for min/max on VS2013 only.
+#include <algorithm>
+#endif
+
+#include <string>
+#include <iterator>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/type_traits.h>
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/message_lite.h>
+
+namespace google {
+
+namespace upb {
+namespace google_opensource {
+class GMR_Handlers;
+} // namespace google_opensource
+} // namespace upb
+
+namespace protobuf {
+
+class Message;
+
+namespace internal {
+
+static const int kMinRepeatedFieldAllocationSize = 4;
+
+// A utility function for logging that doesn't need any template types.
+void LogIndexOutOfBounds(int index, int size);
+
+template <typename Iter>
+inline int CalculateReserve(Iter begin, Iter end, std::forward_iterator_tag) {
+ return std::distance(begin, end);
+}
+
+template <typename Iter>
+inline int CalculateReserve(Iter begin, Iter end, std::input_iterator_tag) {
+ return -1;
+}
+
+template <typename Iter>
+inline int CalculateReserve(Iter begin, Iter end) {
+ typedef typename std::iterator_traits<Iter>::iterator_category Category;
+ return CalculateReserve(begin, end, Category());
+}
+} // namespace internal
+
+
+// RepeatedField is used to represent repeated fields of a primitive type (in
+// other words, everything except strings and nested Messages). Most users will
+// not ever use a RepeatedField directly; they will use the get-by-index,
+// set-by-index, and add accessors that are generated for all repeated fields.
+template <typename Element>
+class RepeatedField {
+ public:
+ RepeatedField();
+ RepeatedField(const RepeatedField& other);
+ template <typename Iter>
+ RepeatedField(Iter begin, const Iter& end);
+ ~RepeatedField();
+
+ RepeatedField& operator=(const RepeatedField& other);
+
+ bool empty() const;
+ int size() const;
+
+ const Element& Get(int index) const;
+ Element* Mutable(int index);
+ void Set(int index, const Element& value);
+ void Add(const Element& value);
+ Element* Add();
+ // Remove the last element in the array.
+ void RemoveLast();
+
+ // Extract elements with indices in "[start .. start+num-1]".
+ // Copy them into "elements[0 .. num-1]" if "elements" is not NULL.
+ // Caution: implementation also moves elements with indices [start+num ..].
+ // Calling this routine inside a loop can cause quadratic behavior.
+ void ExtractSubrange(int start, int num, Element* elements);
+
+ void Clear();
+ void MergeFrom(const RepeatedField& other);
+ void CopyFrom(const RepeatedField& other);
+
+ // Reserve space to expand the field to at least the given size. If the
+ // array is grown, it will always be at least doubled in size.
+ void Reserve(int new_size);
+
+ // Resize the RepeatedField to a new, smaller size. This is O(1).
+ void Truncate(int new_size);
+
+ void AddAlreadyReserved(const Element& value);
+ Element* AddAlreadyReserved();
+ int Capacity() const;
+
+ // Like STL resize. Uses value to fill appended elements.
+ // Like Truncate() if new_size <= size(), otherwise this is
+ // O(new_size - size()).
+ void Resize(int new_size, const Element& value);
+
+ // Gets the underlying array. This pointer is possibly invalidated by
+ // any add or remove operation.
+ Element* mutable_data();
+ const Element* data() const;
+
+ // Swap entire contents with "other".
+ void Swap(RepeatedField* other);
+
+ // Swap two elements.
+ void SwapElements(int index1, int index2);
+
+ // STL-like iterator support
+ typedef Element* iterator;
+ typedef const Element* const_iterator;
+ typedef Element value_type;
+ typedef value_type& reference;
+ typedef const value_type& const_reference;
+ typedef value_type* pointer;
+ typedef const value_type* const_pointer;
+ typedef int size_type;
+ typedef ptrdiff_t difference_type;
+
+ iterator begin();
+ const_iterator begin() const;
+ iterator end();
+ const_iterator end() const;
+
+ // Reverse iterator support
+ typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+ typedef std::reverse_iterator<iterator> reverse_iterator;
+ reverse_iterator rbegin() {
+ return reverse_iterator(end());
+ }
+ const_reverse_iterator rbegin() const {
+ return const_reverse_iterator(end());
+ }
+ reverse_iterator rend() {
+ return reverse_iterator(begin());
+ }
+ const_reverse_iterator rend() const {
+ return const_reverse_iterator(begin());
+ }
+
+ // Returns the number of bytes used by the repeated field, excluding
+ // sizeof(*this)
+ int SpaceUsedExcludingSelf() const;
+
+ private:
+ static const int kInitialSize = 0;
+
+ Element* elements_;
+ int current_size_;
+ int total_size_;
+
+ // Move the contents of |from| into |to|, possibly clobbering |from| in the
+ // process. For primitive types this is just a memcpy(), but it could be
+ // specialized for non-primitive types to, say, swap each element instead.
+ void MoveArray(Element to[], Element from[], int size);
+
+ // Copy the elements of |from| into |to|.
+ void CopyArray(Element to[], const Element from[], int size);
+};
+
+namespace internal {
+template <typename It> class RepeatedPtrIterator;
+template <typename It, typename VoidPtr> class RepeatedPtrOverPtrsIterator;
+} // namespace internal
+
+namespace internal {
+
+// This is a helper template to copy an array of elements effeciently when they
+// have a trivial copy constructor, and correctly otherwise. This really
+// shouldn't be necessary, but our compiler doesn't optimize std::copy very
+// effectively.
+template <typename Element,
+ bool HasTrivialCopy = has_trivial_copy<Element>::value>
+struct ElementCopier {
+ void operator()(Element to[], const Element from[], int array_size);
+};
+
+} // namespace internal
+
+namespace internal {
+
+// This is the common base class for RepeatedPtrFields. It deals only in void*
+// pointers. Users should not use this interface directly.
+//
+// The methods of this interface correspond to the methods of RepeatedPtrField,
+// but may have a template argument called TypeHandler. Its signature is:
+// class TypeHandler {
+// public:
+// typedef MyType Type;
+// static Type* New();
+// static void Delete(Type*);
+// static void Clear(Type*);
+// static void Merge(const Type& from, Type* to);
+//
+// // Only needs to be implemented if SpaceUsedExcludingSelf() is called.
+// static int SpaceUsed(const Type&);
+// };
+class LIBPROTOBUF_EXPORT RepeatedPtrFieldBase {
+ protected:
+ // The reflection implementation needs to call protected methods directly,
+ // reinterpreting pointers as being to Message instead of a specific Message
+ // subclass.
+ friend class GeneratedMessageReflection;
+
+ // ExtensionSet stores repeated message extensions as
+ // RepeatedPtrField<MessageLite>, but non-lite ExtensionSets need to
+ // implement SpaceUsed(), and thus need to call SpaceUsedExcludingSelf()
+ // reinterpreting MessageLite as Message. ExtensionSet also needs to make
+ // use of AddFromCleared(), which is not part of the public interface.
+ friend class ExtensionSet;
+
+ // To parse directly into a proto2 generated class, the upb class GMR_Handlers
+ // needs to be able to modify a RepeatedPtrFieldBase directly.
+ friend class LIBPROTOBUF_EXPORT upb::google_opensource::GMR_Handlers;
+
+ RepeatedPtrFieldBase();
+
+ // Must be called from destructor.
+ template <typename TypeHandler>
+ void Destroy();
+
+ bool empty() const;
+ int size() const;
+
+ template <typename TypeHandler>
+ const typename TypeHandler::Type& Get(int index) const;
+ template <typename TypeHandler>
+ typename TypeHandler::Type* Mutable(int index);
+ template <typename TypeHandler>
+ typename TypeHandler::Type* Add();
+ template <typename TypeHandler>
+ void RemoveLast();
+ template <typename TypeHandler>
+ void Clear();
+ template <typename TypeHandler>
+ void MergeFrom(const RepeatedPtrFieldBase& other);
+ template <typename TypeHandler>
+ void CopyFrom(const RepeatedPtrFieldBase& other);
+
+ void CloseGap(int start, int num) {
+ // Close up a gap of "num" elements starting at offset "start".
+ for (int i = start + num; i < allocated_size_; ++i)
+ elements_[i - num] = elements_[i];
+ current_size_ -= num;
+ allocated_size_ -= num;
+ }
+
+ void Reserve(int new_size);
+
+ int Capacity() const;
+
+ // Used for constructing iterators.
+ void* const* raw_data() const;
+ void** raw_mutable_data() const;
+
+ template <typename TypeHandler>
+ typename TypeHandler::Type** mutable_data();
+ template <typename TypeHandler>
+ const typename TypeHandler::Type* const* data() const;
+
+ void Swap(RepeatedPtrFieldBase* other);
+
+ void SwapElements(int index1, int index2);
+
+ template <typename TypeHandler>
+ int SpaceUsedExcludingSelf() const;
+
+
+ // Advanced memory management --------------------------------------
+
+ // Like Add(), but if there are no cleared objects to use, returns NULL.
+ template <typename TypeHandler>
+ typename TypeHandler::Type* AddFromCleared();
+
+ template <typename TypeHandler>
+ void AddAllocated(typename TypeHandler::Type* value);
+ template <typename TypeHandler>
+ typename TypeHandler::Type* ReleaseLast();
+
+ int ClearedCount() const;
+ template <typename TypeHandler>
+ void AddCleared(typename TypeHandler::Type* value);
+ template <typename TypeHandler>
+ typename TypeHandler::Type* ReleaseCleared();
+
+ private:
+ static const int kInitialSize = 0;
+
+ void** elements_;
+ int current_size_;
+ int allocated_size_;
+ int total_size_;
+
+ template <typename TypeHandler>
+ static inline typename TypeHandler::Type* cast(void* element) {
+ return reinterpret_cast<typename TypeHandler::Type*>(element);
+ }
+ template <typename TypeHandler>
+ static inline const typename TypeHandler::Type* cast(const void* element) {
+ return reinterpret_cast<const typename TypeHandler::Type*>(element);
+ }
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(RepeatedPtrFieldBase);
+};
+
+template <typename GenericType>
+class GenericTypeHandler {
+ public:
+ typedef GenericType Type;
+ static GenericType* New() { return new GenericType; }
+ static void Delete(GenericType* value) { delete value; }
+ static void Clear(GenericType* value) { value->Clear(); }
+ static void Merge(const GenericType& from, GenericType* to) {
+ to->MergeFrom(from);
+ }
+ static int SpaceUsed(const GenericType& value) { return value.SpaceUsed(); }
+ static const Type& default_instance() { return Type::default_instance(); }
+};
+
+template <>
+inline void GenericTypeHandler<MessageLite>::Merge(
+ const MessageLite& from, MessageLite* to) {
+ to->CheckTypeAndMergeFrom(from);
+}
+
+template <>
+inline const MessageLite& GenericTypeHandler<MessageLite>::default_instance() {
+ // Yes, the behavior of the code is undefined, but this function is only
+ // called when we're already deep into the world of undefined, because the
+ // caller called Get(index) out of bounds.
+ MessageLite* null = NULL;
+ return *null;
+}
+
+template <>
+inline const Message& GenericTypeHandler<Message>::default_instance() {
+ // Yes, the behavior of the code is undefined, but this function is only
+ // called when we're already deep into the world of undefined, because the
+ // caller called Get(index) out of bounds.
+ Message* null = NULL;
+ return *null;
+}
+
+
+// HACK: If a class is declared as DLL-exported in MSVC, it insists on
+// generating copies of all its methods -- even inline ones -- to include
+// in the DLL. But SpaceUsed() calls StringSpaceUsedExcludingSelf() which
+// isn't in the lite library, therefore the lite library cannot link if
+// StringTypeHandler is exported. So, we factor out StringTypeHandlerBase,
+// export that, then make StringTypeHandler be a subclass which is NOT
+// exported.
+// TODO(kenton): There has to be a better way.
+class LIBPROTOBUF_EXPORT StringTypeHandlerBase {
+ public:
+ typedef string Type;
+ static string* New();
+ static void Delete(string* value);
+ static void Clear(string* value) { value->clear(); }
+ static void Merge(const string& from, string* to) { *to = from; }
+ static const Type& default_instance() {
+ return ::google::protobuf::internal::GetEmptyString();
+ }
+};
+
+class StringTypeHandler : public StringTypeHandlerBase {
+ public:
+ static int SpaceUsed(const string& value) {
+ return sizeof(value) + StringSpaceUsedExcludingSelf(value);
+ }
+};
+
+
+} // namespace internal
+
+// RepeatedPtrField is like RepeatedField, but used for repeated strings or
+// Messages.
+template <typename Element>
+class RepeatedPtrField : public internal::RepeatedPtrFieldBase {
+ public:
+ RepeatedPtrField();
+ RepeatedPtrField(const RepeatedPtrField& other);
+ template <typename Iter>
+ RepeatedPtrField(Iter begin, const Iter& end);
+ ~RepeatedPtrField();
+
+ RepeatedPtrField& operator=(const RepeatedPtrField& other);
+
+ bool empty() const;
+ int size() const;
+
+ const Element& Get(int index) const;
+ Element* Mutable(int index);
+ Element* Add();
+
+ // Remove the last element in the array.
+ // Ownership of the element is retained by the array.
+ void RemoveLast();
+
+ // Delete elements with indices in the range [start .. start+num-1].
+ // Caution: implementation moves all elements with indices [start+num .. ].
+ // Calling this routine inside a loop can cause quadratic behavior.
+ void DeleteSubrange(int start, int num);
+
+ void Clear();
+ void MergeFrom(const RepeatedPtrField& other);
+ void CopyFrom(const RepeatedPtrField& other);
+
+ // Reserve space to expand the field to at least the given size. This only
+ // resizes the pointer array; it doesn't allocate any objects. If the
+ // array is grown, it will always be at least doubled in size.
+ void Reserve(int new_size);
+
+ int Capacity() const;
+
+ // Gets the underlying array. This pointer is possibly invalidated by
+ // any add or remove operation.
+ Element** mutable_data();
+ const Element* const* data() const;
+
+ // Swap entire contents with "other".
+ void Swap(RepeatedPtrField* other);
+
+ // Swap two elements.
+ void SwapElements(int index1, int index2);
+
+ // STL-like iterator support
+ typedef internal::RepeatedPtrIterator<Element> iterator;
+ typedef internal::RepeatedPtrIterator<const Element> const_iterator;
+ typedef Element value_type;
+ typedef value_type& reference;
+ typedef const value_type& const_reference;
+ typedef value_type* pointer;
+ typedef const value_type* const_pointer;
+ typedef int size_type;
+ typedef ptrdiff_t difference_type;
+
+ iterator begin();
+ const_iterator begin() const;
+ iterator end();
+ const_iterator end() const;
+
+ // Reverse iterator support
+ typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+ typedef std::reverse_iterator<iterator> reverse_iterator;
+ reverse_iterator rbegin() {
+ return reverse_iterator(end());
+ }
+ const_reverse_iterator rbegin() const {
+ return const_reverse_iterator(end());
+ }
+ reverse_iterator rend() {
+ return reverse_iterator(begin());
+ }
+ const_reverse_iterator rend() const {
+ return const_reverse_iterator(begin());
+ }
+
+ // Custom STL-like iterator that iterates over and returns the underlying
+ // pointers to Element rather than Element itself.
+ typedef internal::RepeatedPtrOverPtrsIterator<Element, void*>
+ pointer_iterator;
+ typedef internal::RepeatedPtrOverPtrsIterator<const Element, const void*>
+ const_pointer_iterator;
+ pointer_iterator pointer_begin();
+ const_pointer_iterator pointer_begin() const;
+ pointer_iterator pointer_end();
+ const_pointer_iterator pointer_end() const;
+
+ // Returns (an estimate of) the number of bytes used by the repeated field,
+ // excluding sizeof(*this).
+ int SpaceUsedExcludingSelf() const;
+
+ // Advanced memory management --------------------------------------
+ // When hardcore memory management becomes necessary -- as it sometimes
+ // does here at Google -- the following methods may be useful.
+
+ // Add an already-allocated object, passing ownership to the
+ // RepeatedPtrField.
+ void AddAllocated(Element* value);
+ // Remove the last element and return it, passing ownership to the caller.
+ // Requires: size() > 0
+ Element* ReleaseLast();
+
+ // Extract elements with indices in the range "[start .. start+num-1]".
+ // The caller assumes ownership of the extracted elements and is responsible
+ // for deleting them when they are no longer needed.
+ // If "elements" is non-NULL, then pointers to the extracted elements
+ // are stored in "elements[0 .. num-1]" for the convenience of the caller.
+ // If "elements" is NULL, then the caller must use some other mechanism
+ // to perform any further operations (like deletion) on these elements.
+ // Caution: implementation also moves elements with indices [start+num ..].
+ // Calling this routine inside a loop can cause quadratic behavior.
+ void ExtractSubrange(int start, int num, Element** elements);
+
+ // When elements are removed by calls to RemoveLast() or Clear(), they
+ // are not actually freed. Instead, they are cleared and kept so that
+ // they can be reused later. This can save lots of CPU time when
+ // repeatedly reusing a protocol message for similar purposes.
+ //
+ // Hardcore programs may choose to manipulate these cleared objects
+ // to better optimize memory management using the following routines.
+
+ // Get the number of cleared objects that are currently being kept
+ // around for reuse.
+ int ClearedCount() const;
+ // Add an element to the pool of cleared objects, passing ownership to
+ // the RepeatedPtrField. The element must be cleared prior to calling
+ // this method.
+ void AddCleared(Element* value);
+ // Remove a single element from the cleared pool and return it, passing
+ // ownership to the caller. The element is guaranteed to be cleared.
+ // Requires: ClearedCount() > 0
+ Element* ReleaseCleared();
+
+ protected:
+ // Note: RepeatedPtrField SHOULD NOT be subclassed by users. We only
+ // subclass it in one place as a hack for compatibility with proto1. The
+ // subclass needs to know about TypeHandler in order to call protected
+ // methods on RepeatedPtrFieldBase.
+ class TypeHandler;
+
+};
+
+// implementation ====================================================
+
+template <typename Element>
+inline RepeatedField<Element>::RepeatedField()
+ : elements_(NULL),
+ current_size_(0),
+ total_size_(kInitialSize) {
+}
+
+template <typename Element>
+inline RepeatedField<Element>::RepeatedField(const RepeatedField& other)
+ : elements_(NULL),
+ current_size_(0),
+ total_size_(kInitialSize) {
+ CopyFrom(other);
+}
+
+template <typename Element>
+template <typename Iter>
+inline RepeatedField<Element>::RepeatedField(Iter begin, const Iter& end)
+ : elements_(NULL),
+ current_size_(0),
+ total_size_(kInitialSize) {
+ int reserve = internal::CalculateReserve(begin, end);
+ if (reserve != -1) {
+ Reserve(reserve);
+ for (; begin != end; ++begin) {
+ AddAlreadyReserved(*begin);
+ }
+ } else {
+ for (; begin != end; ++begin) {
+ Add(*begin);
+ }
+ }
+}
+
+template <typename Element>
+RepeatedField<Element>::~RepeatedField() {
+ delete [] elements_;
+}
+
+template <typename Element>
+inline RepeatedField<Element>&
+RepeatedField<Element>::operator=(const RepeatedField& other) {
+ if (this != &other)
+ CopyFrom(other);
+ return *this;
+}
+
+template <typename Element>
+inline bool RepeatedField<Element>::empty() const {
+ return current_size_ == 0;
+}
+
+template <typename Element>
+inline int RepeatedField<Element>::size() const {
+ return current_size_;
+}
+
+template <typename Element>
+inline int RepeatedField<Element>::Capacity() const {
+ return total_size_;
+}
+
+template<typename Element>
+inline void RepeatedField<Element>::AddAlreadyReserved(const Element& value) {
+ GOOGLE_DCHECK_LT(size(), Capacity());
+ elements_[current_size_++] = value;
+}
+
+template<typename Element>
+inline Element* RepeatedField<Element>::AddAlreadyReserved() {
+ GOOGLE_DCHECK_LT(size(), Capacity());
+ return &elements_[current_size_++];
+}
+
+template<typename Element>
+inline void RepeatedField<Element>::Resize(int new_size, const Element& value) {
+ GOOGLE_DCHECK_GE(new_size, 0);
+ if (new_size > size()) {
+ Reserve(new_size);
+ std::fill(&elements_[current_size_], &elements_[new_size], value);
+ }
+ current_size_ = new_size;
+}
+
+template <typename Element>
+inline const Element& RepeatedField<Element>::Get(int index) const {
+ GOOGLE_DCHECK_GE(index, 0);
+ GOOGLE_DCHECK_LT(index, size());
+ return elements_[index];
+}
+
+template <typename Element>
+inline Element* RepeatedField<Element>::Mutable(int index) {
+ GOOGLE_DCHECK_GE(index, 0);
+ GOOGLE_DCHECK_LT(index, size());
+ return elements_ + index;
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::Set(int index, const Element& value) {
+ GOOGLE_DCHECK_GE(index, 0);
+ GOOGLE_DCHECK_LT(index, size());
+ elements_[index] = value;
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::Add(const Element& value) {
+ if (current_size_ == total_size_) Reserve(total_size_ + 1);
+ elements_[current_size_++] = value;
+}
+
+template <typename Element>
+inline Element* RepeatedField<Element>::Add() {
+ if (current_size_ == total_size_) Reserve(total_size_ + 1);
+ return &elements_[current_size_++];
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::RemoveLast() {
+ GOOGLE_DCHECK_GT(current_size_, 0);
+ --current_size_;
+}
+
+template <typename Element>
+void RepeatedField<Element>::ExtractSubrange(
+ int start, int num, Element* elements) {
+ GOOGLE_DCHECK_GE(start, 0);
+ GOOGLE_DCHECK_GE(num, 0);
+ GOOGLE_DCHECK_LE(start + num, this->size());
+
+ // Save the values of the removed elements if requested.
+ if (elements != NULL) {
+ for (int i = 0; i < num; ++i)
+ elements[i] = this->Get(i + start);
+ }
+
+ // Slide remaining elements down to fill the gap.
+ if (num > 0) {
+ for (int i = start + num; i < this->size(); ++i)
+ this->Set(i - num, this->Get(i));
+ this->Truncate(this->size() - num);
+ }
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::Clear() {
+ current_size_ = 0;
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::MergeFrom(const RepeatedField& other) {
+ GOOGLE_CHECK_NE(&other, this);
+ if (other.current_size_ != 0) {
+ Reserve(current_size_ + other.current_size_);
+ CopyArray(elements_ + current_size_, other.elements_, other.current_size_);
+ current_size_ += other.current_size_;
+ }
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::CopyFrom(const RepeatedField& other) {
+ if (&other == this) return;
+ Clear();
+ MergeFrom(other);
+}
+
+template <typename Element>
+inline Element* RepeatedField<Element>::mutable_data() {
+ return elements_;
+}
+
+template <typename Element>
+inline const Element* RepeatedField<Element>::data() const {
+ return elements_;
+}
+
+
+template <typename Element>
+void RepeatedField<Element>::Swap(RepeatedField* other) {
+ if (this == other) return;
+ Element* swap_elements = elements_;
+ int swap_current_size = current_size_;
+ int swap_total_size = total_size_;
+
+ elements_ = other->elements_;
+ current_size_ = other->current_size_;
+ total_size_ = other->total_size_;
+
+ other->elements_ = swap_elements;
+ other->current_size_ = swap_current_size;
+ other->total_size_ = swap_total_size;
+}
+
+template <typename Element>
+void RepeatedField<Element>::SwapElements(int index1, int index2) {
+ using std::swap; // enable ADL with fallback
+ swap(elements_[index1], elements_[index2]);
+}
+
+template <typename Element>
+inline typename RepeatedField<Element>::iterator
+RepeatedField<Element>::begin() {
+ return elements_;
+}
+template <typename Element>
+inline typename RepeatedField<Element>::const_iterator
+RepeatedField<Element>::begin() const {
+ return elements_;
+}
+template <typename Element>
+inline typename RepeatedField<Element>::iterator
+RepeatedField<Element>::end() {
+ return elements_ + current_size_;
+}
+template <typename Element>
+inline typename RepeatedField<Element>::const_iterator
+RepeatedField<Element>::end() const {
+ return elements_ + current_size_;
+}
+
+template <typename Element>
+inline int RepeatedField<Element>::SpaceUsedExcludingSelf() const {
+ return (elements_ != NULL) ? total_size_ * sizeof(elements_[0]) : 0;
+}
+
+// Avoid inlining of Reserve(): new, copy, and delete[] lead to a significant
+// amount of code bloat.
+template <typename Element>
+void RepeatedField<Element>::Reserve(int new_size) {
+ if (total_size_ >= new_size) return;
+
+ Element* old_elements = elements_;
+ total_size_ = max(google::protobuf::internal::kMinRepeatedFieldAllocationSize,
+ max(total_size_ * 2, new_size));
+ elements_ = new Element[total_size_];
+ if (old_elements != NULL) {
+ MoveArray(elements_, old_elements, current_size_);
+ delete [] old_elements;
+ }
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::Truncate(int new_size) {
+ GOOGLE_DCHECK_LE(new_size, current_size_);
+ current_size_ = new_size;
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::MoveArray(
+ Element to[], Element from[], int array_size) {
+ CopyArray(to, from, array_size);
+}
+
+template <typename Element>
+inline void RepeatedField<Element>::CopyArray(
+ Element to[], const Element from[], int array_size) {
+ internal::ElementCopier<Element>()(to, from, array_size);
+}
+
+namespace internal {
+
+template <typename Element, bool HasTrivialCopy>
+void ElementCopier<Element, HasTrivialCopy>::operator()(
+ Element to[], const Element from[], int array_size) {
+ std::copy(from, from + array_size, to);
+}
+
+template <typename Element>
+struct ElementCopier<Element, true> {
+ void operator()(Element to[], const Element from[], int array_size) {
+ memcpy(to, from, array_size * sizeof(Element));
+ }
+};
+
+} // namespace internal
+
+
+// -------------------------------------------------------------------
+
+namespace internal {
+
+inline RepeatedPtrFieldBase::RepeatedPtrFieldBase()
+ : elements_(NULL),
+ current_size_(0),
+ allocated_size_(0),
+ total_size_(kInitialSize) {
+}
+
+template <typename TypeHandler>
+void RepeatedPtrFieldBase::Destroy() {
+ for (int i = 0; i < allocated_size_; i++) {
+ TypeHandler::Delete(cast<TypeHandler>(elements_[i]));
+ }
+ delete [] elements_;
+}
+
+inline bool RepeatedPtrFieldBase::empty() const {
+ return current_size_ == 0;
+}
+
+inline int RepeatedPtrFieldBase::size() const {
+ return current_size_;
+}
+
+template <typename TypeHandler>
+inline const typename TypeHandler::Type&
+RepeatedPtrFieldBase::Get(int index) const {
+ GOOGLE_DCHECK_GE(index, 0);
+ GOOGLE_DCHECK_LT(index, size());
+ return *cast<TypeHandler>(elements_[index]);
+}
+
+
+template <typename TypeHandler>
+inline typename TypeHandler::Type*
+RepeatedPtrFieldBase::Mutable(int index) {
+ GOOGLE_DCHECK_GE(index, 0);
+ GOOGLE_DCHECK_LT(index, size());
+ return cast<TypeHandler>(elements_[index]);
+}
+
+template <typename TypeHandler>
+inline typename TypeHandler::Type* RepeatedPtrFieldBase::Add() {
+ if (current_size_ < allocated_size_) {
+ return cast<TypeHandler>(elements_[current_size_++]);
+ }
+ if (allocated_size_ == total_size_) Reserve(total_size_ + 1);
+ typename TypeHandler::Type* result = TypeHandler::New();
+ ++allocated_size_;
+ elements_[current_size_++] = result;
+ return result;
+}
+
+template <typename TypeHandler>
+inline void RepeatedPtrFieldBase::RemoveLast() {
+ GOOGLE_DCHECK_GT(current_size_, 0);
+ TypeHandler::Clear(cast<TypeHandler>(elements_[--current_size_]));
+}
+
+template <typename TypeHandler>
+void RepeatedPtrFieldBase::Clear() {
+ for (int i = 0; i < current_size_; i++) {
+ TypeHandler::Clear(cast<TypeHandler>(elements_[i]));
+ }
+ current_size_ = 0;
+}
+
+template <typename TypeHandler>
+inline void RepeatedPtrFieldBase::MergeFrom(const RepeatedPtrFieldBase& other) {
+ GOOGLE_CHECK_NE(&other, this);
+ Reserve(current_size_ + other.current_size_);
+ for (int i = 0; i < other.current_size_; i++) {
+ TypeHandler::Merge(other.template Get<TypeHandler>(i), Add<TypeHandler>());
+ }
+}
+
+template <typename TypeHandler>
+inline void RepeatedPtrFieldBase::CopyFrom(const RepeatedPtrFieldBase& other) {
+ if (&other == this) return;
+ RepeatedPtrFieldBase::Clear<TypeHandler>();
+ RepeatedPtrFieldBase::MergeFrom<TypeHandler>(other);
+}
+
+inline int RepeatedPtrFieldBase::Capacity() const {
+ return total_size_;
+}
+
+inline void* const* RepeatedPtrFieldBase::raw_data() const {
+ return elements_;
+}
+
+inline void** RepeatedPtrFieldBase::raw_mutable_data() const {
+ return elements_;
+}
+
+template <typename TypeHandler>
+inline typename TypeHandler::Type** RepeatedPtrFieldBase::mutable_data() {
+ // TODO(kenton): Breaks C++ aliasing rules. We should probably remove this
+ // method entirely.
+ return reinterpret_cast<typename TypeHandler::Type**>(elements_);
+}
+
+template <typename TypeHandler>
+inline const typename TypeHandler::Type* const*
+RepeatedPtrFieldBase::data() const {
+ // TODO(kenton): Breaks C++ aliasing rules. We should probably remove this
+ // method entirely.
+ return reinterpret_cast<const typename TypeHandler::Type* const*>(elements_);
+}
+
+inline void RepeatedPtrFieldBase::SwapElements(int index1, int index2) {
+ using std::swap; // enable ADL with fallback
+ swap(elements_[index1], elements_[index2]);
+}
+
+template <typename TypeHandler>
+inline int RepeatedPtrFieldBase::SpaceUsedExcludingSelf() const {
+ int allocated_bytes =
+ (elements_ != NULL) ? total_size_ * sizeof(elements_[0]) : 0;
+ for (int i = 0; i < allocated_size_; ++i) {
+ allocated_bytes += TypeHandler::SpaceUsed(*cast<TypeHandler>(elements_[i]));
+ }
+ return allocated_bytes;
+}
+
+template <typename TypeHandler>
+inline typename TypeHandler::Type* RepeatedPtrFieldBase::AddFromCleared() {
+ if (current_size_ < allocated_size_) {
+ return cast<TypeHandler>(elements_[current_size_++]);
+ } else {
+ return NULL;
+ }
+}
+
+template <typename TypeHandler>
+void RepeatedPtrFieldBase::AddAllocated(
+ typename TypeHandler::Type* value) {
+ // Make room for the new pointer.
+ if (current_size_ == total_size_) {
+ // The array is completely full with no cleared objects, so grow it.
+ Reserve(total_size_ + 1);
+ ++allocated_size_;
+ } else if (allocated_size_ == total_size_) {
+ // There is no more space in the pointer array because it contains some
+ // cleared objects awaiting reuse. We don't want to grow the array in this
+ // case because otherwise a loop calling AddAllocated() followed by Clear()
+ // would leak memory.
+ TypeHandler::Delete(cast<TypeHandler>(elements_[current_size_]));
+ } else if (current_size_ < allocated_size_) {
+ // We have some cleared objects. We don't care about their order, so we
+ // can just move the first one to the end to make space.
+ elements_[allocated_size_] = elements_[current_size_];
+ ++allocated_size_;
+ } else {
+ // There are no cleared objects.
+ ++allocated_size_;
+ }
+
+ elements_[current_size_++] = value;
+}
+
+template <typename TypeHandler>
+inline typename TypeHandler::Type* RepeatedPtrFieldBase::ReleaseLast() {
+ GOOGLE_DCHECK_GT(current_size_, 0);
+ typename TypeHandler::Type* result =
+ cast<TypeHandler>(elements_[--current_size_]);
+ --allocated_size_;
+ if (current_size_ < allocated_size_) {
+ // There are cleared elements on the end; replace the removed element
+ // with the last allocated element.
+ elements_[current_size_] = elements_[allocated_size_];
+ }
+ return result;
+}
+
+inline int RepeatedPtrFieldBase::ClearedCount() const {
+ return allocated_size_ - current_size_;
+}
+
+template <typename TypeHandler>
+inline void RepeatedPtrFieldBase::AddCleared(
+ typename TypeHandler::Type* value) {
+ if (allocated_size_ == total_size_) Reserve(total_size_ + 1);
+ elements_[allocated_size_++] = value;
+}
+
+template <typename TypeHandler>
+inline typename TypeHandler::Type* RepeatedPtrFieldBase::ReleaseCleared() {
+ GOOGLE_DCHECK_GT(allocated_size_, current_size_);
+ return cast<TypeHandler>(elements_[--allocated_size_]);
+}
+
+} // namespace internal
+
+// -------------------------------------------------------------------
+
+template <typename Element>
+class RepeatedPtrField<Element>::TypeHandler
+ : public internal::GenericTypeHandler<Element> {
+};
+
+template <>
+class RepeatedPtrField<string>::TypeHandler
+ : public internal::StringTypeHandler {
+};
+
+
+template <typename Element>
+inline RepeatedPtrField<Element>::RepeatedPtrField() {}
+
+template <typename Element>
+inline RepeatedPtrField<Element>::RepeatedPtrField(
+ const RepeatedPtrField& other)
+ : RepeatedPtrFieldBase() {
+ CopyFrom(other);
+}
+
+template <typename Element>
+template <typename Iter>
+inline RepeatedPtrField<Element>::RepeatedPtrField(
+ Iter begin, const Iter& end) {
+ int reserve = internal::CalculateReserve(begin, end);
+ if (reserve != -1) {
+ Reserve(reserve);
+ }
+ for (; begin != end; ++begin) {
+ *Add() = *begin;
+ }
+}
+
+template <typename Element>
+RepeatedPtrField<Element>::~RepeatedPtrField() {
+ Destroy<TypeHandler>();
+}
+
+template <typename Element>
+inline RepeatedPtrField<Element>& RepeatedPtrField<Element>::operator=(
+ const RepeatedPtrField& other) {
+ if (this != &other)
+ CopyFrom(other);
+ return *this;
+}
+
+template <typename Element>
+inline bool RepeatedPtrField<Element>::empty() const {
+ return RepeatedPtrFieldBase::empty();
+}
+
+template <typename Element>
+inline int RepeatedPtrField<Element>::size() const {
+ return RepeatedPtrFieldBase::size();
+}
+
+template <typename Element>
+inline const Element& RepeatedPtrField<Element>::Get(int index) const {
+ return RepeatedPtrFieldBase::Get<TypeHandler>(index);
+}
+
+
+template <typename Element>
+inline Element* RepeatedPtrField<Element>::Mutable(int index) {
+ return RepeatedPtrFieldBase::Mutable<TypeHandler>(index);
+}
+
+template <typename Element>
+inline Element* RepeatedPtrField<Element>::Add() {
+ return RepeatedPtrFieldBase::Add<TypeHandler>();
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::RemoveLast() {
+ RepeatedPtrFieldBase::RemoveLast<TypeHandler>();
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::DeleteSubrange(int start, int num) {
+ GOOGLE_DCHECK_GE(start, 0);
+ GOOGLE_DCHECK_GE(num, 0);
+ GOOGLE_DCHECK_LE(start + num, size());
+ for (int i = 0; i < num; ++i)
+ delete RepeatedPtrFieldBase::Mutable<TypeHandler>(start + i);
+ ExtractSubrange(start, num, NULL);
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::ExtractSubrange(
+ int start, int num, Element** elements) {
+ GOOGLE_DCHECK_GE(start, 0);
+ GOOGLE_DCHECK_GE(num, 0);
+ GOOGLE_DCHECK_LE(start + num, size());
+
+ if (num > 0) {
+ // Save the values of the removed elements if requested.
+ if (elements != NULL) {
+ for (int i = 0; i < num; ++i)
+ elements[i] = RepeatedPtrFieldBase::Mutable<TypeHandler>(i + start);
+ }
+ CloseGap(start, num);
+ }
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::Clear() {
+ RepeatedPtrFieldBase::Clear<TypeHandler>();
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::MergeFrom(
+ const RepeatedPtrField& other) {
+ RepeatedPtrFieldBase::MergeFrom<TypeHandler>(other);
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::CopyFrom(
+ const RepeatedPtrField& other) {
+ RepeatedPtrFieldBase::CopyFrom<TypeHandler>(other);
+}
+
+template <typename Element>
+inline Element** RepeatedPtrField<Element>::mutable_data() {
+ return RepeatedPtrFieldBase::mutable_data<TypeHandler>();
+}
+
+template <typename Element>
+inline const Element* const* RepeatedPtrField<Element>::data() const {
+ return RepeatedPtrFieldBase::data<TypeHandler>();
+}
+
+template <typename Element>
+void RepeatedPtrField<Element>::Swap(RepeatedPtrField* other) {
+ RepeatedPtrFieldBase::Swap(other);
+}
+
+template <typename Element>
+void RepeatedPtrField<Element>::SwapElements(int index1, int index2) {
+ RepeatedPtrFieldBase::SwapElements(index1, index2);
+}
+
+template <typename Element>
+inline int RepeatedPtrField<Element>::SpaceUsedExcludingSelf() const {
+ return RepeatedPtrFieldBase::SpaceUsedExcludingSelf<TypeHandler>();
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::AddAllocated(Element* value) {
+ RepeatedPtrFieldBase::AddAllocated<TypeHandler>(value);
+}
+
+template <typename Element>
+inline Element* RepeatedPtrField<Element>::ReleaseLast() {
+ return RepeatedPtrFieldBase::ReleaseLast<TypeHandler>();
+}
+
+
+template <typename Element>
+inline int RepeatedPtrField<Element>::ClearedCount() const {
+ return RepeatedPtrFieldBase::ClearedCount();
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::AddCleared(Element* value) {
+ return RepeatedPtrFieldBase::AddCleared<TypeHandler>(value);
+}
+
+template <typename Element>
+inline Element* RepeatedPtrField<Element>::ReleaseCleared() {
+ return RepeatedPtrFieldBase::ReleaseCleared<TypeHandler>();
+}
+
+template <typename Element>
+inline void RepeatedPtrField<Element>::Reserve(int new_size) {
+ return RepeatedPtrFieldBase::Reserve(new_size);
+}
+
+template <typename Element>
+inline int RepeatedPtrField<Element>::Capacity() const {
+ return RepeatedPtrFieldBase::Capacity();
+}
+
+// -------------------------------------------------------------------
+
+namespace internal {
+
+// STL-like iterator implementation for RepeatedPtrField. You should not
+// refer to this class directly; use RepeatedPtrField<T>::iterator instead.
+//
+// The iterator for RepeatedPtrField<T>, RepeatedPtrIterator<T>, is
+// very similar to iterator_ptr<T**> in util/gtl/iterator_adaptors.h,
+// but adds random-access operators and is modified to wrap a void** base
+// iterator (since RepeatedPtrField stores its array as a void* array and
+// casting void** to T** would violate C++ aliasing rules).
+//
+// This code based on net/proto/proto-array-internal.h by Jeffrey Yasskin
+// (jyasskin@google.com).
+template<typename Element>
+class RepeatedPtrIterator
+ : public std::iterator<
+ std::random_access_iterator_tag, Element> {
+ public:
+ typedef RepeatedPtrIterator<Element> iterator;
+ typedef std::iterator<
+ std::random_access_iterator_tag, Element> superclass;
+
+ // Shadow the value_type in std::iterator<> because const_iterator::value_type
+ // needs to be T, not const T.
+ typedef typename remove_const<Element>::type value_type;
+
+ // Let the compiler know that these are type names, so we don't have to
+ // write "typename" in front of them everywhere.
+ typedef typename superclass::reference reference;
+ typedef typename superclass::pointer pointer;
+ typedef typename superclass::difference_type difference_type;
+
+ RepeatedPtrIterator() : it_(NULL) {}
+ explicit RepeatedPtrIterator(void* const* it) : it_(it) {}
+
+ // Allow "upcasting" from RepeatedPtrIterator<T**> to
+ // RepeatedPtrIterator<const T*const*>.
+ template<typename OtherElement>
+ RepeatedPtrIterator(const RepeatedPtrIterator<OtherElement>& other)
+ : it_(other.it_) {
+ // Force a compiler error if the other type is not convertible to ours.
+ if (false) {
+ implicit_cast<Element*, OtherElement*>(0);
+ }
+ }
+
+ // dereferenceable
+ reference operator*() const { return *reinterpret_cast<Element*>(*it_); }
+ pointer operator->() const { return &(operator*()); }
+
+ // {inc,dec}rementable
+ iterator& operator++() { ++it_; return *this; }
+ iterator operator++(int) { return iterator(it_++); }
+ iterator& operator--() { --it_; return *this; }
+ iterator operator--(int) { return iterator(it_--); }
+
+ // equality_comparable
+ bool operator==(const iterator& x) const { return it_ == x.it_; }
+ bool operator!=(const iterator& x) const { return it_ != x.it_; }
+
+ // less_than_comparable
+ bool operator<(const iterator& x) const { return it_ < x.it_; }
+ bool operator<=(const iterator& x) const { return it_ <= x.it_; }
+ bool operator>(const iterator& x) const { return it_ > x.it_; }
+ bool operator>=(const iterator& x) const { return it_ >= x.it_; }
+
+ // addable, subtractable
+ iterator& operator+=(difference_type d) {
+ it_ += d;
+ return *this;
+ }
+ friend iterator operator+(iterator it, difference_type d) {
+ it += d;
+ return it;
+ }
+ friend iterator operator+(difference_type d, iterator it) {
+ it += d;
+ return it;
+ }
+ iterator& operator-=(difference_type d) {
+ it_ -= d;
+ return *this;
+ }
+ friend iterator operator-(iterator it, difference_type d) {
+ it -= d;
+ return it;
+ }
+
+ // indexable
+ reference operator[](difference_type d) const { return *(*this + d); }
+
+ // random access iterator
+ difference_type operator-(const iterator& x) const { return it_ - x.it_; }
+
+ private:
+ template<typename OtherElement>
+ friend class RepeatedPtrIterator;
+
+ // The internal iterator.
+ void* const* it_;
+};
+
+// Provide an iterator that operates on pointers to the underlying objects
+// rather than the objects themselves as RepeatedPtrIterator does.
+// Consider using this when working with stl algorithms that change
+// the array.
+// The VoidPtr template parameter holds the type-agnostic pointer value
+// referenced by the iterator. It should either be "void *" for a mutable
+// iterator, or "const void *" for a constant iterator.
+template<typename Element, typename VoidPtr>
+class RepeatedPtrOverPtrsIterator
+ : public std::iterator<std::random_access_iterator_tag, Element*> {
+ public:
+ typedef RepeatedPtrOverPtrsIterator<Element, VoidPtr> iterator;
+ typedef std::iterator<
+ std::random_access_iterator_tag, Element*> superclass;
+
+ // Shadow the value_type in std::iterator<> because const_iterator::value_type
+ // needs to be T, not const T.
+ typedef typename remove_const<Element*>::type value_type;
+
+ // Let the compiler know that these are type names, so we don't have to
+ // write "typename" in front of them everywhere.
+ typedef typename superclass::reference reference;
+ typedef typename superclass::pointer pointer;
+ typedef typename superclass::difference_type difference_type;
+
+ RepeatedPtrOverPtrsIterator() : it_(NULL) {}
+ explicit RepeatedPtrOverPtrsIterator(VoidPtr* it) : it_(it) {}
+
+ // dereferenceable
+ reference operator*() const { return *reinterpret_cast<Element**>(it_); }
+ pointer operator->() const { return &(operator*()); }
+
+ // {inc,dec}rementable
+ iterator& operator++() { ++it_; return *this; }
+ iterator operator++(int) { return iterator(it_++); }
+ iterator& operator--() { --it_; return *this; }
+ iterator operator--(int) { return iterator(it_--); }
+
+ // equality_comparable
+ bool operator==(const iterator& x) const { return it_ == x.it_; }
+ bool operator!=(const iterator& x) const { return it_ != x.it_; }
+
+ // less_than_comparable
+ bool operator<(const iterator& x) const { return it_ < x.it_; }
+ bool operator<=(const iterator& x) const { return it_ <= x.it_; }
+ bool operator>(const iterator& x) const { return it_ > x.it_; }
+ bool operator>=(const iterator& x) const { return it_ >= x.it_; }
+
+ // addable, subtractable
+ iterator& operator+=(difference_type d) {
+ it_ += d;
+ return *this;
+ }
+ friend iterator operator+(iterator it, difference_type d) {
+ it += d;
+ return it;
+ }
+ friend iterator operator+(difference_type d, iterator it) {
+ it += d;
+ return it;
+ }
+ iterator& operator-=(difference_type d) {
+ it_ -= d;
+ return *this;
+ }
+ friend iterator operator-(iterator it, difference_type d) {
+ it -= d;
+ return it;
+ }
+
+ // indexable
+ reference operator[](difference_type d) const { return *(*this + d); }
+
+ // random access iterator
+ difference_type operator-(const iterator& x) const { return it_ - x.it_; }
+
+ private:
+ template<typename OtherElement>
+ friend class RepeatedPtrIterator;
+
+ // The internal iterator.
+ VoidPtr* it_;
+};
+
+} // namespace internal
+
+template <typename Element>
+inline typename RepeatedPtrField<Element>::iterator
+RepeatedPtrField<Element>::begin() {
+ return iterator(raw_data());
+}
+template <typename Element>
+inline typename RepeatedPtrField<Element>::const_iterator
+RepeatedPtrField<Element>::begin() const {
+ return iterator(raw_data());
+}
+template <typename Element>
+inline typename RepeatedPtrField<Element>::iterator
+RepeatedPtrField<Element>::end() {
+ return iterator(raw_data() + size());
+}
+template <typename Element>
+inline typename RepeatedPtrField<Element>::const_iterator
+RepeatedPtrField<Element>::end() const {
+ return iterator(raw_data() + size());
+}
+
+template <typename Element>
+inline typename RepeatedPtrField<Element>::pointer_iterator
+RepeatedPtrField<Element>::pointer_begin() {
+ return pointer_iterator(raw_mutable_data());
+}
+template <typename Element>
+inline typename RepeatedPtrField<Element>::const_pointer_iterator
+RepeatedPtrField<Element>::pointer_begin() const {
+ return const_pointer_iterator(const_cast<const void**>(raw_mutable_data()));
+}
+template <typename Element>
+inline typename RepeatedPtrField<Element>::pointer_iterator
+RepeatedPtrField<Element>::pointer_end() {
+ return pointer_iterator(raw_mutable_data() + size());
+}
+template <typename Element>
+inline typename RepeatedPtrField<Element>::const_pointer_iterator
+RepeatedPtrField<Element>::pointer_end() const {
+ return const_pointer_iterator(
+ const_cast<const void**>(raw_mutable_data() + size()));
+}
+
+
+// Iterators and helper functions that follow the spirit of the STL
+// std::back_insert_iterator and std::back_inserter but are tailor-made
+// for RepeatedField and RepatedPtrField. Typical usage would be:
+//
+// std::copy(some_sequence.begin(), some_sequence.end(),
+// google::protobuf::RepeatedFieldBackInserter(proto.mutable_sequence()));
+//
+// Ported by johannes from util/gtl/proto-array-iterators.h
+
+namespace internal {
+// A back inserter for RepeatedField objects.
+template<typename T> class RepeatedFieldBackInsertIterator
+ : public std::iterator<std::output_iterator_tag, T> {
+ public:
+ explicit RepeatedFieldBackInsertIterator(
+ RepeatedField<T>* const mutable_field)
+ : field_(mutable_field) {
+ }
+ RepeatedFieldBackInsertIterator<T>& operator=(const T& value) {
+ field_->Add(value);
+ return *this;
+ }
+ RepeatedFieldBackInsertIterator<T>& operator*() {
+ return *this;
+ }
+ RepeatedFieldBackInsertIterator<T>& operator++() {
+ return *this;
+ }
+ RepeatedFieldBackInsertIterator<T>& operator++(int /* unused */) {
+ return *this;
+ }
+
+ private:
+ RepeatedField<T>* field_;
+};
+
+// A back inserter for RepeatedPtrField objects.
+template<typename T> class RepeatedPtrFieldBackInsertIterator
+ : public std::iterator<std::output_iterator_tag, T> {
+ public:
+ RepeatedPtrFieldBackInsertIterator(
+ RepeatedPtrField<T>* const mutable_field)
+ : field_(mutable_field) {
+ }
+ RepeatedPtrFieldBackInsertIterator<T>& operator=(const T& value) {
+ *field_->Add() = value;
+ return *this;
+ }
+ RepeatedPtrFieldBackInsertIterator<T>& operator=(
+ const T* const ptr_to_value) {
+ *field_->Add() = *ptr_to_value;
+ return *this;
+ }
+ RepeatedPtrFieldBackInsertIterator<T>& operator*() {
+ return *this;
+ }
+ RepeatedPtrFieldBackInsertIterator<T>& operator++() {
+ return *this;
+ }
+ RepeatedPtrFieldBackInsertIterator<T>& operator++(int /* unused */) {
+ return *this;
+ }
+
+ private:
+ RepeatedPtrField<T>* field_;
+};
+
+// A back inserter for RepeatedPtrFields that inserts by transfering ownership
+// of a pointer.
+template<typename T> class AllocatedRepeatedPtrFieldBackInsertIterator
+ : public std::iterator<std::output_iterator_tag, T> {
+ public:
+ explicit AllocatedRepeatedPtrFieldBackInsertIterator(
+ RepeatedPtrField<T>* const mutable_field)
+ : field_(mutable_field) {
+ }
+ AllocatedRepeatedPtrFieldBackInsertIterator<T>& operator=(
+ T* const ptr_to_value) {
+ field_->AddAllocated(ptr_to_value);
+ return *this;
+ }
+ AllocatedRepeatedPtrFieldBackInsertIterator<T>& operator*() {
+ return *this;
+ }
+ AllocatedRepeatedPtrFieldBackInsertIterator<T>& operator++() {
+ return *this;
+ }
+ AllocatedRepeatedPtrFieldBackInsertIterator<T>& operator++(
+ int /* unused */) {
+ return *this;
+ }
+
+ private:
+ RepeatedPtrField<T>* field_;
+};
+} // namespace internal
+
+// Provides a back insert iterator for RepeatedField instances,
+// similar to std::back_inserter().
+template<typename T> internal::RepeatedFieldBackInsertIterator<T>
+RepeatedFieldBackInserter(RepeatedField<T>* const mutable_field) {
+ return internal::RepeatedFieldBackInsertIterator<T>(mutable_field);
+}
+
+// Provides a back insert iterator for RepeatedPtrField instances,
+// similar to std::back_inserter().
+template<typename T> internal::RepeatedPtrFieldBackInsertIterator<T>
+RepeatedPtrFieldBackInserter(RepeatedPtrField<T>* const mutable_field) {
+ return internal::RepeatedPtrFieldBackInsertIterator<T>(mutable_field);
+}
+
+// Special back insert iterator for RepeatedPtrField instances, just in
+// case someone wants to write generic template code that can access both
+// RepeatedFields and RepeatedPtrFields using a common name.
+template<typename T> internal::RepeatedPtrFieldBackInsertIterator<T>
+RepeatedFieldBackInserter(RepeatedPtrField<T>* const mutable_field) {
+ return internal::RepeatedPtrFieldBackInsertIterator<T>(mutable_field);
+}
+
+// Provides a back insert iterator for RepeatedPtrField instances
+// similar to std::back_inserter() which transfers the ownership while
+// copying elements.
+template<typename T> internal::AllocatedRepeatedPtrFieldBackInsertIterator<T>
+AllocatedRepeatedPtrFieldBackInserter(
+ RepeatedPtrField<T>* const mutable_field) {
+ return internal::AllocatedRepeatedPtrFieldBackInsertIterator<T>(
+ mutable_field);
+}
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_REPEATED_FIELD_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/service.cc b/toolkit/components/protobuf/src/google/protobuf/service.cc
new file mode 100644
index 0000000000..ffa919daa7
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/service.cc
@@ -0,0 +1,46 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/service.h>
+
+namespace google {
+namespace protobuf {
+
+Service::~Service() {}
+RpcChannel::~RpcChannel() {}
+RpcController::~RpcController() {}
+
+} // namespace protobuf
+
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/service.h b/toolkit/components/protobuf/src/google/protobuf/service.h
new file mode 100644
index 0000000000..cc0b45d410
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/service.h
@@ -0,0 +1,291 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// DEPRECATED: This module declares the abstract interfaces underlying proto2
+// RPC services. These are intented to be independent of any particular RPC
+// implementation, so that proto2 services can be used on top of a variety
+// of implementations. Starting with version 2.3.0, RPC implementations should
+// not try to build on these, but should instead provide code generator plugins
+// which generate code specific to the particular RPC implementation. This way
+// the generated code can be more appropriate for the implementation in use
+// and can avoid unnecessary layers of indirection.
+//
+//
+// When you use the protocol compiler to compile a service definition, it
+// generates two classes: An abstract interface for the service (with
+// methods matching the service definition) and a "stub" implementation.
+// A stub is just a type-safe wrapper around an RpcChannel which emulates a
+// local implementation of the service.
+//
+// For example, the service definition:
+// service MyService {
+// rpc Foo(MyRequest) returns(MyResponse);
+// }
+// will generate abstract interface "MyService" and class "MyService::Stub".
+// You could implement a MyService as follows:
+// class MyServiceImpl : public MyService {
+// public:
+// MyServiceImpl() {}
+// ~MyServiceImpl() {}
+//
+// // implements MyService ---------------------------------------
+//
+// void Foo(google::protobuf::RpcController* controller,
+// const MyRequest* request,
+// MyResponse* response,
+// Closure* done) {
+// // ... read request and fill in response ...
+// done->Run();
+// }
+// };
+// You would then register an instance of MyServiceImpl with your RPC server
+// implementation. (How to do that depends on the implementation.)
+//
+// To call a remote MyServiceImpl, first you need an RpcChannel connected to it.
+// How to construct a channel depends, again, on your RPC implementation.
+// Here we use a hypothentical "MyRpcChannel" as an example:
+// MyRpcChannel channel("rpc:hostname:1234/myservice");
+// MyRpcController controller;
+// MyServiceImpl::Stub stub(&channel);
+// FooRequest request;
+// FooRespnose response;
+//
+// // ... fill in request ...
+//
+// stub.Foo(&controller, request, &response, NewCallback(HandleResponse));
+//
+// On Thread-Safety:
+//
+// Different RPC implementations may make different guarantees about what
+// threads they may run callbacks on, and what threads the application is
+// allowed to use to call the RPC system. Portable software should be ready
+// for callbacks to be called on any thread, but should not try to call the
+// RPC system from any thread except for the ones on which it received the
+// callbacks. Realistically, though, simple software will probably want to
+// use a single-threaded RPC system while high-end software will want to
+// use multiple threads. RPC implementations should provide multiple
+// choices.
+
+#ifndef GOOGLE_PROTOBUF_SERVICE_H__
+#define GOOGLE_PROTOBUF_SERVICE_H__
+
+#include <string>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+// Defined in this file.
+class Service;
+class RpcController;
+class RpcChannel;
+
+// Defined in other files.
+class Descriptor; // descriptor.h
+class ServiceDescriptor; // descriptor.h
+class MethodDescriptor; // descriptor.h
+class Message; // message.h
+
+// Abstract base interface for protocol-buffer-based RPC services. Services
+// themselves are abstract interfaces (implemented either by servers or as
+// stubs), but they subclass this base interface. The methods of this
+// interface can be used to call the methods of the Service without knowing
+// its exact type at compile time (analogous to Reflection).
+class LIBPROTOBUF_EXPORT Service {
+ public:
+ inline Service() {}
+ virtual ~Service();
+
+ // When constructing a stub, you may pass STUB_OWNS_CHANNEL as the second
+ // parameter to the constructor to tell it to delete its RpcChannel when
+ // destroyed.
+ enum ChannelOwnership {
+ STUB_OWNS_CHANNEL,
+ STUB_DOESNT_OWN_CHANNEL
+ };
+
+ // Get the ServiceDescriptor describing this service and its methods.
+ virtual const ServiceDescriptor* GetDescriptor() = 0;
+
+ // Call a method of the service specified by MethodDescriptor. This is
+ // normally implemented as a simple switch() that calls the standard
+ // definitions of the service's methods.
+ //
+ // Preconditions:
+ // * method->service() == GetDescriptor()
+ // * request and response are of the exact same classes as the objects
+ // returned by GetRequestPrototype(method) and
+ // GetResponsePrototype(method).
+ // * After the call has started, the request must not be modified and the
+ // response must not be accessed at all until "done" is called.
+ // * "controller" is of the correct type for the RPC implementation being
+ // used by this Service. For stubs, the "correct type" depends on the
+ // RpcChannel which the stub is using. Server-side Service
+ // implementations are expected to accept whatever type of RpcController
+ // the server-side RPC implementation uses.
+ //
+ // Postconditions:
+ // * "done" will be called when the method is complete. This may be
+ // before CallMethod() returns or it may be at some point in the future.
+ // * If the RPC succeeded, "response" contains the response returned by
+ // the server.
+ // * If the RPC failed, "response"'s contents are undefined. The
+ // RpcController can be queried to determine if an error occurred and
+ // possibly to get more information about the error.
+ virtual void CallMethod(const MethodDescriptor* method,
+ RpcController* controller,
+ const Message* request,
+ Message* response,
+ Closure* done) = 0;
+
+ // CallMethod() requires that the request and response passed in are of a
+ // particular subclass of Message. GetRequestPrototype() and
+ // GetResponsePrototype() get the default instances of these required types.
+ // You can then call Message::New() on these instances to construct mutable
+ // objects which you can then pass to CallMethod().
+ //
+ // Example:
+ // const MethodDescriptor* method =
+ // service->GetDescriptor()->FindMethodByName("Foo");
+ // Message* request = stub->GetRequestPrototype (method)->New();
+ // Message* response = stub->GetResponsePrototype(method)->New();
+ // request->ParseFromString(input);
+ // service->CallMethod(method, *request, response, callback);
+ virtual const Message& GetRequestPrototype(
+ const MethodDescriptor* method) const = 0;
+ virtual const Message& GetResponsePrototype(
+ const MethodDescriptor* method) const = 0;
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Service);
+};
+
+// An RpcController mediates a single method call. The primary purpose of
+// the controller is to provide a way to manipulate settings specific to the
+// RPC implementation and to find out about RPC-level errors.
+//
+// The methods provided by the RpcController interface are intended to be a
+// "least common denominator" set of features which we expect all
+// implementations to support. Specific implementations may provide more
+// advanced features (e.g. deadline propagation).
+class LIBPROTOBUF_EXPORT RpcController {
+ public:
+ inline RpcController() {}
+ virtual ~RpcController();
+
+ // Client-side methods ---------------------------------------------
+ // These calls may be made from the client side only. Their results
+ // are undefined on the server side (may crash).
+
+ // Resets the RpcController to its initial state so that it may be reused in
+ // a new call. Must not be called while an RPC is in progress.
+ virtual void Reset() = 0;
+
+ // After a call has finished, returns true if the call failed. The possible
+ // reasons for failure depend on the RPC implementation. Failed() must not
+ // be called before a call has finished. If Failed() returns true, the
+ // contents of the response message are undefined.
+ virtual bool Failed() const = 0;
+
+ // If Failed() is true, returns a human-readable description of the error.
+ virtual string ErrorText() const = 0;
+
+ // Advises the RPC system that the caller desires that the RPC call be
+ // canceled. The RPC system may cancel it immediately, may wait awhile and
+ // then cancel it, or may not even cancel the call at all. If the call is
+ // canceled, the "done" callback will still be called and the RpcController
+ // will indicate that the call failed at that time.
+ virtual void StartCancel() = 0;
+
+ // Server-side methods ---------------------------------------------
+ // These calls may be made from the server side only. Their results
+ // are undefined on the client side (may crash).
+
+ // Causes Failed() to return true on the client side. "reason" will be
+ // incorporated into the message returned by ErrorText(). If you find
+ // you need to return machine-readable information about failures, you
+ // should incorporate it into your response protocol buffer and should
+ // NOT call SetFailed().
+ virtual void SetFailed(const string& reason) = 0;
+
+ // If true, indicates that the client canceled the RPC, so the server may
+ // as well give up on replying to it. The server should still call the
+ // final "done" callback.
+ virtual bool IsCanceled() const = 0;
+
+ // Asks that the given callback be called when the RPC is canceled. The
+ // callback will always be called exactly once. If the RPC completes without
+ // being canceled, the callback will be called after completion. If the RPC
+ // has already been canceled when NotifyOnCancel() is called, the callback
+ // will be called immediately.
+ //
+ // NotifyOnCancel() must be called no more than once per request.
+ virtual void NotifyOnCancel(Closure* callback) = 0;
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(RpcController);
+};
+
+// Abstract interface for an RPC channel. An RpcChannel represents a
+// communication line to a Service which can be used to call that Service's
+// methods. The Service may be running on another machine. Normally, you
+// should not call an RpcChannel directly, but instead construct a stub Service
+// wrapping it. Example:
+// RpcChannel* channel = new MyRpcChannel("remotehost.example.com:1234");
+// MyService* service = new MyService::Stub(channel);
+// service->MyMethod(request, &response, callback);
+class LIBPROTOBUF_EXPORT RpcChannel {
+ public:
+ inline RpcChannel() {}
+ virtual ~RpcChannel();
+
+ // Call the given method of the remote service. The signature of this
+ // procedure looks the same as Service::CallMethod(), but the requirements
+ // are less strict in one important way: the request and response objects
+ // need not be of any specific class as long as their descriptors are
+ // method->input_type() and method->output_type().
+ virtual void CallMethod(const MethodDescriptor* method,
+ RpcController* controller,
+ const Message* request,
+ Message* response,
+ Closure* done) = 0;
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(RpcChannel);
+};
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_SERVICE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h
new file mode 100644
index 0000000000..e6da1fce22
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops.h
@@ -0,0 +1,231 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// The routines exported by this module are subtle. If you use them, even if
+// you get the code right, it will depend on careful reasoning about atomicity
+// and memory ordering; it will be less readable, and harder to maintain. If
+// you plan to use these routines, you should have a good reason, such as solid
+// evidence that performance would otherwise suffer, or there being no
+// alternative. You should assume only properties explicitly guaranteed by the
+// specifications in this file. You are almost certainly _not_ writing code
+// just for the x86; if you assume x86 semantics, x86 hardware bugs and
+// implementations on other archtectures will cause your code to break. If you
+// do not know what you are doing, avoid these routines, and use a Mutex.
+//
+// It is incorrect to make direct assignments to/from an atomic variable.
+// You should use one of the Load or Store routines. The NoBarrier
+// versions are provided when no barriers are needed:
+// NoBarrier_Store()
+// NoBarrier_Load()
+// Although there are currently no compiler enforcement, you are encouraged
+// to use these.
+
+// This header and the implementations for each platform (located in
+// atomicops_internals_*) must be kept in sync with the upstream code (V8).
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_H_
+
+// Don't include this file for people not concerned about thread safety.
+#ifndef GOOGLE_PROTOBUF_NO_THREAD_SAFETY
+
+#include <google/protobuf/stubs/platform_macros.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+typedef int32 Atomic32;
+#ifdef GOOGLE_PROTOBUF_ARCH_64_BIT
+// We need to be able to go between Atomic64 and AtomicWord implicitly. This
+// means Atomic64 and AtomicWord should be the same type on 64-bit.
+#if defined(__ILP32__) || defined(GOOGLE_PROTOBUF_OS_NACL) || defined(GOOGLE_PROTOBUF_ARCH_SPARC)
+// NaCl's intptr_t is not actually 64-bits on 64-bit!
+// http://code.google.com/p/nativeclient/issues/detail?id=1162
+// sparcv9's pointer type is 32bits
+typedef int64 Atomic64;
+#else
+typedef intptr_t Atomic64;
+#endif
+#endif
+
+// Use AtomicWord for a machine-sized pointer. It will use the Atomic32 or
+// Atomic64 routines below, depending on your architecture.
+#if defined(__OpenBSD__) && !defined(GOOGLE_PROTOBUF_ARCH_64_BIT)
+typedef Atomic32 AtomicWord;
+#else
+typedef intptr_t AtomicWord;
+#endif
+
+// Atomically execute:
+// result = *ptr;
+// if (*ptr == old_value)
+// *ptr = new_value;
+// return result;
+//
+// I.e., replace "*ptr" with "new_value" if "*ptr" used to be "old_value".
+// Always return the old value of "*ptr"
+//
+// This routine implies no memory barriers.
+Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value);
+
+// Atomically store new_value into *ptr, returning the previous value held in
+// *ptr. This routine implies no memory barriers.
+Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr, Atomic32 new_value);
+
+// Atomically increment *ptr by "increment". Returns the new value of
+// *ptr with the increment applied. This routine implies no memory barriers.
+Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr, Atomic32 increment);
+
+Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment);
+
+// These following lower-level operations are typically useful only to people
+// implementing higher-level synchronization operations like spinlocks,
+// mutexes, and condition-variables. They combine CompareAndSwap(), a load, or
+// a store with appropriate memory-ordering instructions. "Acquire" operations
+// ensure that no later memory access can be reordered ahead of the operation.
+// "Release" operations ensure that no previous memory access can be reordered
+// after the operation. "Barrier" operations have both "Acquire" and "Release"
+// semantics. A MemoryBarrier() has "Barrier" semantics, but does no memory
+// access.
+Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value);
+Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value);
+
+#if defined(__MINGW32__) && defined(MemoryBarrier)
+#undef MemoryBarrier
+#endif
+void MemoryBarrier();
+void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value);
+void Acquire_Store(volatile Atomic32* ptr, Atomic32 value);
+void Release_Store(volatile Atomic32* ptr, Atomic32 value);
+
+Atomic32 NoBarrier_Load(volatile const Atomic32* ptr);
+Atomic32 Acquire_Load(volatile const Atomic32* ptr);
+Atomic32 Release_Load(volatile const Atomic32* ptr);
+
+// 64-bit atomic operations (only available on 64-bit processors).
+#ifdef GOOGLE_PROTOBUF_ARCH_64_BIT
+Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value);
+Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr, Atomic64 new_value);
+Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr, Atomic64 increment);
+Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr, Atomic64 increment);
+
+Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value);
+Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value);
+void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value);
+void Acquire_Store(volatile Atomic64* ptr, Atomic64 value);
+void Release_Store(volatile Atomic64* ptr, Atomic64 value);
+Atomic64 NoBarrier_Load(volatile const Atomic64* ptr);
+Atomic64 Acquire_Load(volatile const Atomic64* ptr);
+Atomic64 Release_Load(volatile const Atomic64* ptr);
+#endif // GOOGLE_PROTOBUF_ARCH_64_BIT
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+// Include our platform specific implementation.
+#define GOOGLE_PROTOBUF_ATOMICOPS_ERROR \
+#error "Atomic operations are not supported on your platform"
+
+// ThreadSanitizer, http://clang.llvm.org/docs/ThreadSanitizer.html.
+#if defined(THREAD_SANITIZER)
+#include <google/protobuf/stubs/atomicops_internals_tsan.h>
+// MSVC.
+#elif defined(_MSC_VER)
+#if defined(GOOGLE_PROTOBUF_ARCH_IA32) || defined(GOOGLE_PROTOBUF_ARCH_X64)
+#include <google/protobuf/stubs/atomicops_internals_x86_msvc.h>
+#else
+GOOGLE_PROTOBUF_ATOMICOPS_ERROR
+#endif
+
+// Solaris
+#elif defined(GOOGLE_PROTOBUF_OS_SOLARIS)
+#include <google/protobuf/stubs/atomicops_internals_solaris.h>
+
+// Apple.
+#elif defined(GOOGLE_PROTOBUF_OS_APPLE)
+#include <google/protobuf/stubs/atomicops_internals_macosx.h>
+
+// GCC.
+#elif defined(__GNUC__)
+#if defined(GOOGLE_PROTOBUF_ARCH_IA32) || defined(GOOGLE_PROTOBUF_ARCH_X64)
+#include <google/protobuf/stubs/atomicops_internals_x86_gcc.h>
+#elif defined(GOOGLE_PROTOBUF_ARCH_ARM) && defined(__linux__)
+#include <google/protobuf/stubs/atomicops_internals_arm_gcc.h>
+#elif defined(GOOGLE_PROTOBUF_ARCH_AARCH64)
+#include <google/protobuf/stubs/atomicops_internals_arm64_gcc.h>
+#elif defined(GOOGLE_PROTOBUF_ARCH_ARM_QNX)
+#include <google/protobuf/stubs/atomicops_internals_arm_qnx.h>
+#elif defined(GOOGLE_PROTOBUF_ARCH_MIPS) || defined(GOOGLE_PROTOBUF_ARCH_MIPS64)
+#include <google/protobuf/stubs/atomicops_internals_mips_gcc.h>
+#elif defined(__native_client__)
+#include <google/protobuf/stubs/atomicops_internals_pnacl.h>
+#elif (((__GNUC__ == 4) && (__GNUC_MINOR__ >= 7)) || (__GNUC__ > 4))
+#include <google/protobuf/stubs/atomicops_internals_generic_gcc.h>
+#elif defined(__clang__)
+#if __has_extension(c_atomic)
+#include <google/protobuf/stubs/atomicops_internals_generic_gcc.h>
+#else
+GOOGLE_PROTOBUF_ATOMICOPS_ERROR
+#endif
+#else
+GOOGLE_PROTOBUF_ATOMICOPS_ERROR
+#endif
+
+// Unknown.
+#else
+GOOGLE_PROTOBUF_ATOMICOPS_ERROR
+#endif
+
+// On some platforms we need additional declarations to make AtomicWord
+// compatible with our other Atomic* types.
+#if defined(GOOGLE_PROTOBUF_OS_APPLE)
+#include <google/protobuf/stubs/atomicops_internals_atomicword_compat.h>
+#endif
+
+#undef GOOGLE_PROTOBUF_ATOMICOPS_ERROR
+
+#endif // GOOGLE_PROTOBUF_NO_THREAD_SAFETY
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h
new file mode 100644
index 0000000000..0a2d2b894b
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm64_gcc.h
@@ -0,0 +1,325 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM64_GCC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM64_GCC_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline void MemoryBarrier() {
+ __asm__ __volatile__ ("dmb ish" ::: "memory"); // NOLINT
+}
+
+// NoBarrier versions of the operation include "memory" in the clobber list.
+// This is not required for direct usage of the NoBarrier versions of the
+// operations. However this is required for correctness when they are used as
+// part of the Acquire or Release versions, to ensure that nothing from outside
+// the call is reordered between the operation and the memory barrier. This does
+// not change the code generated, so has no or minimal impact on the
+// NoBarrier operations.
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev;
+ int32_t temp;
+
+ __asm__ __volatile__ ( // NOLINT
+ "0: \n\t"
+ "ldxr %w[prev], %[ptr] \n\t" // Load the previous value.
+ "cmp %w[prev], %w[old_value] \n\t"
+ "bne 1f \n\t"
+ "stxr %w[temp], %w[new_value], %[ptr] \n\t" // Try to store the new value.
+ "cbnz %w[temp], 0b \n\t" // Retry if it did not work.
+ "1: \n\t"
+ : [prev]"=&r" (prev),
+ [temp]"=&r" (temp),
+ [ptr]"+Q" (*ptr)
+ : [old_value]"IJr" (old_value),
+ [new_value]"r" (new_value)
+ : "cc", "memory"
+ ); // NOLINT
+
+ return prev;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ Atomic32 result;
+ int32_t temp;
+
+ __asm__ __volatile__ ( // NOLINT
+ "0: \n\t"
+ "ldxr %w[result], %[ptr] \n\t" // Load the previous value.
+ "stxr %w[temp], %w[new_value], %[ptr] \n\t" // Try to store the new value.
+ "cbnz %w[temp], 0b \n\t" // Retry if it did not work.
+ : [result]"=&r" (result),
+ [temp]"=&r" (temp),
+ [ptr]"+Q" (*ptr)
+ : [new_value]"r" (new_value)
+ : "memory"
+ ); // NOLINT
+
+ return result;
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ Atomic32 result;
+ int32_t temp;
+
+ __asm__ __volatile__ ( // NOLINT
+ "0: \n\t"
+ "ldxr %w[result], %[ptr] \n\t" // Load the previous value.
+ "add %w[result], %w[result], %w[increment]\n\t"
+ "stxr %w[temp], %w[result], %[ptr] \n\t" // Try to store the result.
+ "cbnz %w[temp], 0b \n\t" // Retry on failure.
+ : [result]"=&r" (result),
+ [temp]"=&r" (temp),
+ [ptr]"+Q" (*ptr)
+ : [increment]"IJr" (increment)
+ : "memory"
+ ); // NOLINT
+
+ return result;
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ MemoryBarrier();
+ Atomic32 result = NoBarrier_AtomicIncrement(ptr, increment);
+ MemoryBarrier();
+
+ return result;
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ MemoryBarrier();
+
+ return prev;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ MemoryBarrier();
+ Atomic32 prev = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+
+ return prev;
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ __asm__ __volatile__ ( // NOLINT
+ "stlr %w[value], %[ptr] \n\t"
+ : [ptr]"=Q" (*ptr)
+ : [value]"r" (value)
+ : "memory"
+ ); // NOLINT
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value;
+
+ __asm__ __volatile__ ( // NOLINT
+ "ldar %w[value], %[ptr] \n\t"
+ : [value]"=r" (value)
+ : [ptr]"Q" (*ptr)
+ : "memory"
+ ); // NOLINT
+
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+// 64-bit versions of the operations.
+// See the 32-bit versions for comments.
+
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 prev;
+ int32_t temp;
+
+ __asm__ __volatile__ ( // NOLINT
+ "0: \n\t"
+ "ldxr %[prev], %[ptr] \n\t"
+ "cmp %[prev], %[old_value] \n\t"
+ "bne 1f \n\t"
+ "stxr %w[temp], %[new_value], %[ptr] \n\t"
+ "cbnz %w[temp], 0b \n\t"
+ "1: \n\t"
+ : [prev]"=&r" (prev),
+ [temp]"=&r" (temp),
+ [ptr]"+Q" (*ptr)
+ : [old_value]"IJr" (old_value),
+ [new_value]"r" (new_value)
+ : "cc", "memory"
+ ); // NOLINT
+
+ return prev;
+}
+
+inline Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr,
+ Atomic64 new_value) {
+ Atomic64 result;
+ int32_t temp;
+
+ __asm__ __volatile__ ( // NOLINT
+ "0: \n\t"
+ "ldxr %[result], %[ptr] \n\t"
+ "stxr %w[temp], %[new_value], %[ptr] \n\t"
+ "cbnz %w[temp], 0b \n\t"
+ : [result]"=&r" (result),
+ [temp]"=&r" (temp),
+ [ptr]"+Q" (*ptr)
+ : [new_value]"r" (new_value)
+ : "memory"
+ ); // NOLINT
+
+ return result;
+}
+
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ Atomic64 result;
+ int32_t temp;
+
+ __asm__ __volatile__ ( // NOLINT
+ "0: \n\t"
+ "ldxr %[result], %[ptr] \n\t"
+ "add %[result], %[result], %[increment] \n\t"
+ "stxr %w[temp], %[result], %[ptr] \n\t"
+ "cbnz %w[temp], 0b \n\t"
+ : [result]"=&r" (result),
+ [temp]"=&r" (temp),
+ [ptr]"+Q" (*ptr)
+ : [increment]"IJr" (increment)
+ : "memory"
+ ); // NOLINT
+
+ return result;
+}
+
+inline Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ MemoryBarrier();
+ Atomic64 result = NoBarrier_AtomicIncrement(ptr, increment);
+ MemoryBarrier();
+
+ return result;
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 prev = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ MemoryBarrier();
+
+ return prev;
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ MemoryBarrier();
+ Atomic64 prev = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+
+ return prev;
+}
+
+inline void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ __asm__ __volatile__ ( // NOLINT
+ "stlr %x[value], %[ptr] \n\t"
+ : [ptr]"=Q" (*ptr)
+ : [value]"r" (value)
+ : "memory"
+ ); // NOLINT
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64* ptr) {
+ return *ptr;
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ Atomic64 value;
+
+ __asm__ __volatile__ ( // NOLINT
+ "ldar %x[value], %[ptr] \n\t"
+ : [value]"=r" (value)
+ : [ptr]"Q" (*ptr)
+ : "memory"
+ ); // NOLINT
+
+ return value;
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM64_GCC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h
new file mode 100644
index 0000000000..90e727b0bc
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_gcc.h
@@ -0,0 +1,151 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+//
+// LinuxKernelCmpxchg and Barrier_AtomicIncrement are from Google Gears.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM_GCC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM_GCC_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// 0xffff0fc0 is the hard coded address of a function provided by
+// the kernel which implements an atomic compare-exchange. On older
+// ARM architecture revisions (pre-v6) this may be implemented using
+// a syscall. This address is stable, and in active use (hard coded)
+// by at least glibc-2.7 and the Android C library.
+typedef Atomic32 (*LinuxKernelCmpxchgFunc)(Atomic32 old_value,
+ Atomic32 new_value,
+ volatile Atomic32* ptr);
+LinuxKernelCmpxchgFunc pLinuxKernelCmpxchg __attribute__((weak)) =
+ (LinuxKernelCmpxchgFunc) 0xffff0fc0;
+
+typedef void (*LinuxKernelMemoryBarrierFunc)(void);
+LinuxKernelMemoryBarrierFunc pLinuxKernelMemoryBarrier __attribute__((weak)) =
+ (LinuxKernelMemoryBarrierFunc) 0xffff0fa0;
+
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev_value = *ptr;
+ do {
+ if (!pLinuxKernelCmpxchg(old_value, new_value,
+ const_cast<Atomic32*>(ptr))) {
+ return old_value;
+ }
+ prev_value = *ptr;
+ } while (prev_value == old_value);
+ return prev_value;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ Atomic32 old_value;
+ do {
+ old_value = *ptr;
+ } while (pLinuxKernelCmpxchg(old_value, new_value,
+ const_cast<Atomic32*>(ptr)));
+ return old_value;
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return Barrier_AtomicIncrement(ptr, increment);
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ for (;;) {
+ // Atomic exchange the old value with an incremented one.
+ Atomic32 old_value = *ptr;
+ Atomic32 new_value = old_value + increment;
+ if (pLinuxKernelCmpxchg(old_value, new_value,
+ const_cast<Atomic32*>(ptr)) == 0) {
+ // The exchange took place as expected.
+ return new_value;
+ }
+ // Otherwise, *ptr changed mid-loop and we need to retry.
+ }
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void MemoryBarrier() {
+ pLinuxKernelMemoryBarrier();
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM_GCC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_qnx.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_qnx.h
new file mode 100644
index 0000000000..17dfaa5182
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_arm_qnx.h
@@ -0,0 +1,146 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM_QNX_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM_QNX_H_
+
+// For _smp_cmpxchg()
+#include <pthread.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 QNXCmpxchg(Atomic32 old_value,
+ Atomic32 new_value,
+ volatile Atomic32* ptr) {
+ return static_cast<Atomic32>(
+ _smp_cmpxchg((volatile unsigned *)ptr,
+ (unsigned)old_value,
+ (unsigned)new_value));
+}
+
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev_value = *ptr;
+ do {
+ if (!QNXCmpxchg(old_value, new_value,
+ const_cast<Atomic32*>(ptr))) {
+ return old_value;
+ }
+ prev_value = *ptr;
+ } while (prev_value == old_value);
+ return prev_value;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ Atomic32 old_value;
+ do {
+ old_value = *ptr;
+ } while (QNXCmpxchg(old_value, new_value,
+ const_cast<Atomic32*>(ptr)));
+ return old_value;
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return Barrier_AtomicIncrement(ptr, increment);
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ for (;;) {
+ // Atomic exchange the old value with an incremented one.
+ Atomic32 old_value = *ptr;
+ Atomic32 new_value = old_value + increment;
+ if (QNXCmpxchg(old_value, new_value,
+ const_cast<Atomic32*>(ptr)) == 0) {
+ // The exchange took place as expected.
+ return new_value;
+ }
+ // Otherwise, *ptr changed mid-loop and we need to retry.
+ }
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void MemoryBarrier() {
+ __sync_synchronize();
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ARM_QNX_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_atomicword_compat.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_atomicword_compat.h
new file mode 100644
index 0000000000..eb198ff5cc
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_atomicword_compat.h
@@ -0,0 +1,122 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ATOMICWORD_COMPAT_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ATOMICWORD_COMPAT_H_
+
+// AtomicWord is a synonym for intptr_t, and Atomic32 is a synonym for int32,
+// which in turn means int. On some LP32 platforms, intptr_t is an int, but
+// on others, it's a long. When AtomicWord and Atomic32 are based on different
+// fundamental types, their pointers are incompatible.
+//
+// This file defines function overloads to allow both AtomicWord and Atomic32
+// data to be used with this interface.
+//
+// On LP64 platforms, AtomicWord and Atomic64 are both always long,
+// so this problem doesn't occur.
+
+#if !defined(GOOGLE_PROTOBUF_ARCH_64_BIT)
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline AtomicWord NoBarrier_CompareAndSwap(volatile AtomicWord* ptr,
+ AtomicWord old_value,
+ AtomicWord new_value) {
+ return NoBarrier_CompareAndSwap(
+ reinterpret_cast<volatile Atomic32*>(ptr), old_value, new_value);
+}
+
+inline AtomicWord NoBarrier_AtomicExchange(volatile AtomicWord* ptr,
+ AtomicWord new_value) {
+ return NoBarrier_AtomicExchange(
+ reinterpret_cast<volatile Atomic32*>(ptr), new_value);
+}
+
+inline AtomicWord NoBarrier_AtomicIncrement(volatile AtomicWord* ptr,
+ AtomicWord increment) {
+ return NoBarrier_AtomicIncrement(
+ reinterpret_cast<volatile Atomic32*>(ptr), increment);
+}
+
+inline AtomicWord Barrier_AtomicIncrement(volatile AtomicWord* ptr,
+ AtomicWord increment) {
+ return Barrier_AtomicIncrement(
+ reinterpret_cast<volatile Atomic32*>(ptr), increment);
+}
+
+inline AtomicWord Acquire_CompareAndSwap(volatile AtomicWord* ptr,
+ AtomicWord old_value,
+ AtomicWord new_value) {
+ return Acquire_CompareAndSwap(
+ reinterpret_cast<volatile Atomic32*>(ptr), old_value, new_value);
+}
+
+inline AtomicWord Release_CompareAndSwap(volatile AtomicWord* ptr,
+ AtomicWord old_value,
+ AtomicWord new_value) {
+ return Release_CompareAndSwap(
+ reinterpret_cast<volatile Atomic32*>(ptr), old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile AtomicWord *ptr, AtomicWord value) {
+ NoBarrier_Store(reinterpret_cast<volatile Atomic32*>(ptr), value);
+}
+
+inline void Acquire_Store(volatile AtomicWord* ptr, AtomicWord value) {
+ return Acquire_Store(reinterpret_cast<volatile Atomic32*>(ptr), value);
+}
+
+inline void Release_Store(volatile AtomicWord* ptr, AtomicWord value) {
+ return Release_Store(reinterpret_cast<volatile Atomic32*>(ptr), value);
+}
+
+inline AtomicWord NoBarrier_Load(volatile const AtomicWord *ptr) {
+ return NoBarrier_Load(reinterpret_cast<volatile const Atomic32*>(ptr));
+}
+
+inline AtomicWord Acquire_Load(volatile const AtomicWord* ptr) {
+ return Acquire_Load(reinterpret_cast<volatile const Atomic32*>(ptr));
+}
+
+inline AtomicWord Release_Load(volatile const AtomicWord* ptr) {
+ return Release_Load(reinterpret_cast<volatile const Atomic32*>(ptr));
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // !defined(GOOGLE_PROTOBUF_ARCH_64_BIT)
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_ATOMICWORD_COMPAT_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h
new file mode 100644
index 0000000000..dd7abf6f17
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_generic_gcc.h
@@ -0,0 +1,137 @@
+// Copyright 2013 Red Hat Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Red Hat Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_GENERIC_GCC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_GENERIC_GCC_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ __atomic_compare_exchange_n(ptr, &old_value, new_value, true,
+ __ATOMIC_RELAXED, __ATOMIC_RELAXED);
+ return old_value;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ return __atomic_exchange_n(ptr, new_value, __ATOMIC_RELAXED);
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return __atomic_add_fetch(ptr, increment, __ATOMIC_RELAXED);
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return __atomic_add_fetch(ptr, increment, __ATOMIC_SEQ_CST);
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ __atomic_compare_exchange(ptr, &old_value, &new_value, true,
+ __ATOMIC_ACQUIRE, __ATOMIC_ACQUIRE);
+ return old_value;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ __atomic_compare_exchange_n(ptr, &old_value, new_value, true,
+ __ATOMIC_RELEASE, __ATOMIC_ACQUIRE);
+ return old_value;
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ __atomic_store_n(ptr, value, __ATOMIC_RELAXED);
+}
+
+inline void MemoryBarrier() {
+ __sync_synchronize();
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ __atomic_store_n(ptr, value, __ATOMIC_SEQ_CST);
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ __atomic_store_n(ptr, value, __ATOMIC_RELEASE);
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return __atomic_load_n(ptr, __ATOMIC_RELAXED);
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ return __atomic_load_n(ptr, __ATOMIC_SEQ_CST);
+}
+
+#ifdef __LP64__
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ __atomic_store_n(ptr, value, __ATOMIC_RELEASE);
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ __atomic_compare_exchange_n(ptr, &old_value, new_value, true,
+ __ATOMIC_ACQUIRE, __ATOMIC_ACQUIRE);
+ return old_value;
+}
+
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ __atomic_compare_exchange_n(ptr, &old_value, new_value, true,
+ __ATOMIC_RELAXED, __ATOMIC_RELAXED);
+ return old_value;
+}
+
+#endif // defined(__LP64__)
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_GENERIC_GCC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_macosx.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_macosx.h
new file mode 100644
index 0000000000..796332417f
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_macosx.h
@@ -0,0 +1,225 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_MACOSX_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_MACOSX_H_
+
+#include <libkern/OSAtomic.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev_value;
+ do {
+ if (OSAtomicCompareAndSwap32(old_value, new_value,
+ const_cast<Atomic32*>(ptr))) {
+ return old_value;
+ }
+ prev_value = *ptr;
+ } while (prev_value == old_value);
+ return prev_value;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ Atomic32 old_value;
+ do {
+ old_value = *ptr;
+ } while (!OSAtomicCompareAndSwap32(old_value, new_value,
+ const_cast<Atomic32*>(ptr)));
+ return old_value;
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return OSAtomicAdd32(increment, const_cast<Atomic32*>(ptr));
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return OSAtomicAdd32Barrier(increment, const_cast<Atomic32*>(ptr));
+}
+
+inline void MemoryBarrier() {
+ OSMemoryBarrier();
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev_value;
+ do {
+ if (OSAtomicCompareAndSwap32Barrier(old_value, new_value,
+ const_cast<Atomic32*>(ptr))) {
+ return old_value;
+ }
+ prev_value = *ptr;
+ } while (prev_value == old_value);
+ return prev_value;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return Acquire_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+#ifdef __LP64__
+
+// 64-bit implementation on 64-bit platform
+
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 prev_value;
+ do {
+ if (OSAtomicCompareAndSwap64(old_value, new_value,
+ reinterpret_cast<volatile int64_t*>(ptr))) {
+ return old_value;
+ }
+ prev_value = *ptr;
+ } while (prev_value == old_value);
+ return prev_value;
+}
+
+inline Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr,
+ Atomic64 new_value) {
+ Atomic64 old_value;
+ do {
+ old_value = *ptr;
+ } while (!OSAtomicCompareAndSwap64(old_value, new_value,
+ reinterpret_cast<volatile int64_t*>(ptr)));
+ return old_value;
+}
+
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ return OSAtomicAdd64(increment, reinterpret_cast<volatile int64_t*>(ptr));
+}
+
+inline Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ return OSAtomicAdd64Barrier(increment,
+ reinterpret_cast<volatile int64_t*>(ptr));
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 prev_value;
+ do {
+ if (OSAtomicCompareAndSwap64Barrier(
+ old_value, new_value, reinterpret_cast<volatile int64_t*>(ptr))) {
+ return old_value;
+ }
+ prev_value = *ptr;
+ } while (prev_value == old_value);
+ return prev_value;
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ // The lib kern interface does not distinguish between
+ // Acquire and Release memory barriers; they are equivalent.
+ return Acquire_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64* ptr) {
+ return *ptr;
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ Atomic64 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+#endif // defined(__LP64__)
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_MACOSX_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h
new file mode 100644
index 0000000000..e3cd14cf80
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_mips_gcc.h
@@ -0,0 +1,313 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_MIPS_GCC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_MIPS_GCC_H_
+
+#define ATOMICOPS_COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory")
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// Atomically execute:
+// result = *ptr;
+// if (*ptr == old_value)
+// *ptr = new_value;
+// return result;
+//
+// I.e., replace "*ptr" with "new_value" if "*ptr" used to be "old_value".
+// Always return the old value of "*ptr"
+//
+// This routine implies no memory barriers.
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev, tmp;
+ __asm__ __volatile__(".set push\n"
+ ".set noreorder\n"
+ "1:\n"
+ "ll %0, %5\n" // prev = *ptr
+ "bne %0, %3, 2f\n" // if (prev != old_value) goto 2
+ "move %2, %4\n" // tmp = new_value
+ "sc %2, %1\n" // *ptr = tmp (with atomic check)
+ "beqz %2, 1b\n" // start again on atomic error
+ "nop\n" // delay slot nop
+ "2:\n"
+ ".set pop\n"
+ : "=&r" (prev), "=m" (*ptr), "=&r" (tmp)
+ : "Ir" (old_value), "r" (new_value), "m" (*ptr)
+ : "memory");
+ return prev;
+}
+
+// Atomically store new_value into *ptr, returning the previous value held in
+// *ptr. This routine implies no memory barriers.
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ Atomic32 temp, old;
+ __asm__ __volatile__(".set push\n"
+ ".set noreorder\n"
+ "1:\n"
+ "ll %1, %4\n" // old = *ptr
+ "move %0, %3\n" // temp = new_value
+ "sc %0, %2\n" // *ptr = temp (with atomic check)
+ "beqz %0, 1b\n" // start again on atomic error
+ "nop\n" // delay slot nop
+ ".set pop\n"
+ : "=&r" (temp), "=&r" (old), "=m" (*ptr)
+ : "r" (new_value), "m" (*ptr)
+ : "memory");
+
+ return old;
+}
+
+// Atomically increment *ptr by "increment". Returns the new value of
+// *ptr with the increment applied. This routine implies no memory barriers.
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ Atomic32 temp, temp2;
+
+ __asm__ __volatile__(".set push\n"
+ ".set noreorder\n"
+ "1:\n"
+ "ll %0, %4\n" // temp = *ptr
+ "addu %1, %0, %3\n" // temp2 = temp + increment
+ "sc %1, %2\n" // *ptr = temp2 (with atomic check)
+ "beqz %1, 1b\n" // start again on atomic error
+ "addu %1, %0, %3\n" // temp2 = temp + increment
+ ".set pop\n"
+ : "=&r" (temp), "=&r" (temp2), "=m" (*ptr)
+ : "Ir" (increment), "m" (*ptr)
+ : "memory");
+ // temp2 now holds the final value.
+ return temp2;
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ ATOMICOPS_COMPILER_BARRIER();
+ Atomic32 res = NoBarrier_AtomicIncrement(ptr, increment);
+ ATOMICOPS_COMPILER_BARRIER();
+ return res;
+}
+
+// "Acquire" operations
+// ensure that no later memory access can be reordered ahead of the operation.
+// "Release" operations ensure that no previous memory access can be reordered
+// after the operation. "Barrier" operations have both "Acquire" and "Release"
+// semantics. A MemoryBarrier() has "Barrier" semantics, but does no memory
+// access.
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ ATOMICOPS_COMPILER_BARRIER();
+ Atomic32 res = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ ATOMICOPS_COMPILER_BARRIER();
+ return res;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ ATOMICOPS_COMPILER_BARRIER();
+ Atomic32 res = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ ATOMICOPS_COMPILER_BARRIER();
+ return res;
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void MemoryBarrier() {
+ __asm__ __volatile__("sync" : : : "memory");
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+#if defined(__LP64__)
+// 64-bit versions of the atomic ops.
+
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 prev, tmp;
+ __asm__ __volatile__(".set push\n"
+ ".set noreorder\n"
+ "1:\n"
+ "lld %0, %5\n" // prev = *ptr
+ "bne %0, %3, 2f\n" // if (prev != old_value) goto 2
+ "move %2, %4\n" // tmp = new_value
+ "scd %2, %1\n" // *ptr = tmp (with atomic check)
+ "beqz %2, 1b\n" // start again on atomic error
+ "nop\n" // delay slot nop
+ "2:\n"
+ ".set pop\n"
+ : "=&r" (prev), "=m" (*ptr), "=&r" (tmp)
+ : "Ir" (old_value), "r" (new_value), "m" (*ptr)
+ : "memory");
+ return prev;
+}
+
+// Atomically store new_value into *ptr, returning the previous value held in
+// *ptr. This routine implies no memory barriers.
+inline Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr,
+ Atomic64 new_value) {
+ Atomic64 temp, old;
+ __asm__ __volatile__(".set push\n"
+ ".set noreorder\n"
+ "1:\n"
+ "lld %1, %4\n" // old = *ptr
+ "move %0, %3\n" // temp = new_value
+ "scd %0, %2\n" // *ptr = temp (with atomic check)
+ "beqz %0, 1b\n" // start again on atomic error
+ "nop\n" // delay slot nop
+ ".set pop\n"
+ : "=&r" (temp), "=&r" (old), "=m" (*ptr)
+ : "r" (new_value), "m" (*ptr)
+ : "memory");
+
+ return old;
+}
+
+// Atomically increment *ptr by "increment". Returns the new value of
+// *ptr with the increment applied. This routine implies no memory barriers.
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ Atomic64 temp, temp2;
+
+ __asm__ __volatile__(".set push\n"
+ ".set noreorder\n"
+ "1:\n"
+ "lld %0, %4\n" // temp = *ptr
+ "daddu %1, %0, %3\n" // temp2 = temp + increment
+ "scd %1, %2\n" // *ptr = temp2 (with atomic check)
+ "beqz %1, 1b\n" // start again on atomic error
+ "daddu %1, %0, %3\n" // temp2 = temp + increment
+ ".set pop\n"
+ : "=&r" (temp), "=&r" (temp2), "=m" (*ptr)
+ : "Ir" (increment), "m" (*ptr)
+ : "memory");
+ // temp2 now holds the final value.
+ return temp2;
+}
+
+inline Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ MemoryBarrier();
+ Atomic64 res = NoBarrier_AtomicIncrement(ptr, increment);
+ MemoryBarrier();
+ return res;
+}
+
+// "Acquire" operations
+// ensure that no later memory access can be reordered ahead of the operation.
+// "Release" operations ensure that no previous memory access can be reordered
+// after the operation. "Barrier" operations have both "Acquire" and "Release"
+// semantics. A MemoryBarrier() has "Barrier" semantics, but does no memory
+// access.
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 res = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ MemoryBarrier();
+ return res;
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ MemoryBarrier();
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64* ptr) {
+ return *ptr;
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ Atomic64 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+#endif
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#undef ATOMICOPS_COMPILER_BARRIER
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_MIPS_GCC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_pnacl.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_pnacl.h
new file mode 100644
index 0000000000..b10ac02c40
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_pnacl.h
@@ -0,0 +1,73 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_PNACL_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_PNACL_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return __sync_val_compare_and_swap(ptr, old_value, new_value);
+}
+
+inline void MemoryBarrier() {
+ __sync_synchronize();
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 ret = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ MemoryBarrier();
+ return ret;
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ MemoryBarrier();
+ *ptr = value;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr;
+ MemoryBarrier();
+ return value;
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_PNACL_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_solaris.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_solaris.h
new file mode 100644
index 0000000000..d8057ecdea
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_solaris.h
@@ -0,0 +1,188 @@
+// Copyright 2014 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_SPARC_GCC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_SPARC_GCC_H_
+
+#include <atomic.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return (Atomic32)atomic_cas_32((volatile uint32_t*)ptr, (uint32_t)old_value, (uint32_t)new_value);
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ return (Atomic32)atomic_swap_32((volatile uint32_t*)ptr, (uint32_t)new_value);
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return (Atomic32)atomic_add_32_nv((volatile uint32_t*)ptr, (uint32_t)increment);
+}
+
+inline void MemoryBarrier(void) {
+ membar_producer();
+ membar_consumer();
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ MemoryBarrier();
+ Atomic32 ret = NoBarrier_AtomicIncrement(ptr, increment);
+ MemoryBarrier();
+
+ return ret;
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 ret = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ MemoryBarrier();
+
+ return ret;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ MemoryBarrier();
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ membar_producer();
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ membar_consumer();
+ *ptr = value;
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 val = *ptr;
+ membar_consumer();
+ return val;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ membar_producer();
+ return *ptr;
+}
+
+#ifdef GOOGLE_PROTOBUF_ARCH_64_BIT
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ return atomic_cas_64((volatile uint64_t*)ptr, (uint64_t)old_value, (uint64_t)new_value);
+}
+
+inline Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr, Atomic64 new_value) {
+ return atomic_swap_64((volatile uint64_t*)ptr, (uint64_t)new_value);
+}
+
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr, Atomic64 increment) {
+ return atomic_add_64_nv((volatile uint64_t*)ptr, increment);
+}
+
+inline Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr, Atomic64 increment) {
+ MemoryBarrier();
+ Atomic64 ret = atomic_add_64_nv((volatile uint64_t*)ptr, increment);
+ MemoryBarrier();
+ return ret;
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 ret = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ MemoryBarrier();
+ return ret;
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ MemoryBarrier();
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+ membar_producer();
+}
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ membar_consumer();
+ *ptr = value;
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64* ptr) {
+ return *ptr;
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ Atomic64 ret = *ptr;
+ membar_consumer();
+ return ret;
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64* ptr) {
+ membar_producer();
+ return *ptr;
+}
+#endif
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_SPARC_GCC_H_
+
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_tsan.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_tsan.h
new file mode 100644
index 0000000000..0c903545cd
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_tsan.h
@@ -0,0 +1,219 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2013 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation for compiler-based
+// ThreadSanitizer (http://clang.llvm.org/docs/ThreadSanitizer.html).
+// Use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_TSAN_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_TSAN_H_
+
+#define ATOMICOPS_COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory")
+
+#include <sanitizer/tsan_interface_atomic.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32 *ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 cmp = old_value;
+ __tsan_atomic32_compare_exchange_strong(ptr, &cmp, new_value,
+ __tsan_memory_order_relaxed, __tsan_memory_order_relaxed);
+ return cmp;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32 *ptr,
+ Atomic32 new_value) {
+ return __tsan_atomic32_exchange(ptr, new_value,
+ __tsan_memory_order_relaxed);
+}
+
+inline Atomic32 Acquire_AtomicExchange(volatile Atomic32 *ptr,
+ Atomic32 new_value) {
+ return __tsan_atomic32_exchange(ptr, new_value,
+ __tsan_memory_order_acquire);
+}
+
+inline Atomic32 Release_AtomicExchange(volatile Atomic32 *ptr,
+ Atomic32 new_value) {
+ return __tsan_atomic32_exchange(ptr, new_value,
+ __tsan_memory_order_release);
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32 *ptr,
+ Atomic32 increment) {
+ return increment + __tsan_atomic32_fetch_add(ptr, increment,
+ __tsan_memory_order_relaxed);
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32 *ptr,
+ Atomic32 increment) {
+ return increment + __tsan_atomic32_fetch_add(ptr, increment,
+ __tsan_memory_order_acq_rel);
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32 *ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 cmp = old_value;
+ __tsan_atomic32_compare_exchange_strong(ptr, &cmp, new_value,
+ __tsan_memory_order_acquire, __tsan_memory_order_acquire);
+ return cmp;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32 *ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 cmp = old_value;
+ __tsan_atomic32_compare_exchange_strong(ptr, &cmp, new_value,
+ __tsan_memory_order_release, __tsan_memory_order_relaxed);
+ return cmp;
+}
+
+inline void NoBarrier_Store(volatile Atomic32 *ptr, Atomic32 value) {
+ __tsan_atomic32_store(ptr, value, __tsan_memory_order_relaxed);
+}
+
+inline void Acquire_Store(volatile Atomic32 *ptr, Atomic32 value) {
+ __tsan_atomic32_store(ptr, value, __tsan_memory_order_relaxed);
+ __tsan_atomic_thread_fence(__tsan_memory_order_seq_cst);
+}
+
+inline void Release_Store(volatile Atomic32 *ptr, Atomic32 value) {
+ __tsan_atomic32_store(ptr, value, __tsan_memory_order_release);
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32 *ptr) {
+ return __tsan_atomic32_load(ptr, __tsan_memory_order_relaxed);
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32 *ptr) {
+ return __tsan_atomic32_load(ptr, __tsan_memory_order_acquire);
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32 *ptr) {
+ __tsan_atomic_thread_fence(__tsan_memory_order_seq_cst);
+ return __tsan_atomic32_load(ptr, __tsan_memory_order_relaxed);
+}
+
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64 *ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 cmp = old_value;
+ __tsan_atomic64_compare_exchange_strong(ptr, &cmp, new_value,
+ __tsan_memory_order_relaxed, __tsan_memory_order_relaxed);
+ return cmp;
+}
+
+inline Atomic64 NoBarrier_AtomicExchange(volatile Atomic64 *ptr,
+ Atomic64 new_value) {
+ return __tsan_atomic64_exchange(ptr, new_value, __tsan_memory_order_relaxed);
+}
+
+inline Atomic64 Acquire_AtomicExchange(volatile Atomic64 *ptr,
+ Atomic64 new_value) {
+ return __tsan_atomic64_exchange(ptr, new_value, __tsan_memory_order_acquire);
+}
+
+inline Atomic64 Release_AtomicExchange(volatile Atomic64 *ptr,
+ Atomic64 new_value) {
+ return __tsan_atomic64_exchange(ptr, new_value, __tsan_memory_order_release);
+}
+
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64 *ptr,
+ Atomic64 increment) {
+ return increment + __tsan_atomic64_fetch_add(ptr, increment,
+ __tsan_memory_order_relaxed);
+}
+
+inline Atomic64 Barrier_AtomicIncrement(volatile Atomic64 *ptr,
+ Atomic64 increment) {
+ return increment + __tsan_atomic64_fetch_add(ptr, increment,
+ __tsan_memory_order_acq_rel);
+}
+
+inline void NoBarrier_Store(volatile Atomic64 *ptr, Atomic64 value) {
+ __tsan_atomic64_store(ptr, value, __tsan_memory_order_relaxed);
+}
+
+inline void Acquire_Store(volatile Atomic64 *ptr, Atomic64 value) {
+ __tsan_atomic64_store(ptr, value, __tsan_memory_order_relaxed);
+ __tsan_atomic_thread_fence(__tsan_memory_order_seq_cst);
+}
+
+inline void Release_Store(volatile Atomic64 *ptr, Atomic64 value) {
+ __tsan_atomic64_store(ptr, value, __tsan_memory_order_release);
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64 *ptr) {
+ return __tsan_atomic64_load(ptr, __tsan_memory_order_relaxed);
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64 *ptr) {
+ return __tsan_atomic64_load(ptr, __tsan_memory_order_acquire);
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64 *ptr) {
+ __tsan_atomic_thread_fence(__tsan_memory_order_seq_cst);
+ return __tsan_atomic64_load(ptr, __tsan_memory_order_relaxed);
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64 *ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 cmp = old_value;
+ __tsan_atomic64_compare_exchange_strong(ptr, &cmp, new_value,
+ __tsan_memory_order_acquire, __tsan_memory_order_acquire);
+ return cmp;
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64 *ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 cmp = old_value;
+ __tsan_atomic64_compare_exchange_strong(ptr, &cmp, new_value,
+ __tsan_memory_order_release, __tsan_memory_order_relaxed);
+ return cmp;
+}
+
+inline void MemoryBarrier() {
+ __tsan_atomic_thread_fence(__tsan_memory_order_seq_cst);
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#undef ATOMICOPS_COMPILER_BARRIER
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_TSAN_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
new file mode 100644
index 0000000000..53c9eae0fa
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.cc
@@ -0,0 +1,137 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This module gets enough CPU information to optimize the
+// atomicops module on x86.
+
+#include <cstring>
+
+#include <google/protobuf/stubs/atomicops.h>
+
+// This file only makes sense with atomicops_internals_x86_gcc.h -- it
+// depends on structs that are defined in that file. If atomicops.h
+// doesn't sub-include that file, then we aren't needed, and shouldn't
+// try to do anything.
+#ifdef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_GCC_H_
+
+// Inline cpuid instruction. In PIC compilations, %ebx contains the address
+// of the global offset table. To avoid breaking such executables, this code
+// must preserve that register's value across cpuid instructions.
+#if defined(__i386__)
+#define cpuid(a, b, c, d, inp) \
+ asm("mov %%ebx, %%edi\n" \
+ "cpuid\n" \
+ "xchg %%edi, %%ebx\n" \
+ : "=a" (a), "=D" (b), "=c" (c), "=d" (d) : "a" (inp))
+#elif defined(__x86_64__)
+#define cpuid(a, b, c, d, inp) \
+ asm("mov %%rbx, %%rdi\n" \
+ "cpuid\n" \
+ "xchg %%rdi, %%rbx\n" \
+ : "=a" (a), "=D" (b), "=c" (c), "=d" (d) : "a" (inp))
+#endif
+
+#if defined(cpuid) // initialize the struct only on x86
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// Set the flags so that code will run correctly and conservatively, so even
+// if we haven't been initialized yet, we're probably single threaded, and our
+// default values should hopefully be pretty safe.
+struct AtomicOps_x86CPUFeatureStruct AtomicOps_Internalx86CPUFeatures = {
+ false, // bug can't exist before process spawns multiple threads
+ false, // no SSE2
+};
+
+namespace {
+
+// Initialize the AtomicOps_Internalx86CPUFeatures struct.
+void AtomicOps_Internalx86CPUFeaturesInit() {
+ uint32_t eax;
+ uint32_t ebx;
+ uint32_t ecx;
+ uint32_t edx;
+
+ // Get vendor string (issue CPUID with eax = 0)
+ cpuid(eax, ebx, ecx, edx, 0);
+ char vendor[13];
+ memcpy(vendor, &ebx, 4);
+ memcpy(vendor + 4, &edx, 4);
+ memcpy(vendor + 8, &ecx, 4);
+ vendor[12] = 0;
+
+ // get feature flags in ecx/edx, and family/model in eax
+ cpuid(eax, ebx, ecx, edx, 1);
+
+ int family = (eax >> 8) & 0xf; // family and model fields
+ int model = (eax >> 4) & 0xf;
+ if (family == 0xf) { // use extended family and model fields
+ family += (eax >> 20) & 0xff;
+ model += ((eax >> 16) & 0xf) << 4;
+ }
+
+ // Opteron Rev E has a bug in which on very rare occasions a locked
+ // instruction doesn't act as a read-acquire barrier if followed by a
+ // non-locked read-modify-write instruction. Rev F has this bug in
+ // pre-release versions, but not in versions released to customers,
+ // so we test only for Rev E, which is family 15, model 32..63 inclusive.
+ if (strcmp(vendor, "AuthenticAMD") == 0 && // AMD
+ family == 15 &&
+ 32 <= model && model <= 63) {
+ AtomicOps_Internalx86CPUFeatures.has_amd_lock_mb_bug = true;
+ } else {
+ AtomicOps_Internalx86CPUFeatures.has_amd_lock_mb_bug = false;
+ }
+
+ // edx bit 26 is SSE2 which we use to tell use whether we can use mfence
+ AtomicOps_Internalx86CPUFeatures.has_sse2 = ((edx >> 26) & 1);
+}
+
+class AtomicOpsx86Initializer {
+ public:
+ AtomicOpsx86Initializer() {
+ AtomicOps_Internalx86CPUFeaturesInit();
+ }
+};
+
+// A global to get use initialized on startup via static initialization :/
+AtomicOpsx86Initializer g_initer;
+
+} // namespace
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // __i386__
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_GCC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h
new file mode 100644
index 0000000000..edccc59dee
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_gcc.h
@@ -0,0 +1,293 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_GCC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_GCC_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// This struct is not part of the public API of this module; clients may not
+// use it.
+// Features of this x86. Values may not be correct before main() is run,
+// but are set conservatively.
+struct AtomicOps_x86CPUFeatureStruct {
+ bool has_amd_lock_mb_bug; // Processor has AMD memory-barrier bug; do lfence
+ // after acquire compare-and-swap.
+ bool has_sse2; // Processor has SSE2.
+};
+extern struct AtomicOps_x86CPUFeatureStruct AtomicOps_Internalx86CPUFeatures;
+
+#define ATOMICOPS_COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory")
+
+// 32-bit low-level operations on any platform.
+
+inline Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 prev;
+ __asm__ __volatile__("lock; cmpxchgl %1,%2"
+ : "=a" (prev)
+ : "q" (new_value), "m" (*ptr), "0" (old_value)
+ : "memory");
+ return prev;
+}
+
+inline Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ __asm__ __volatile__("xchgl %1,%0" // The lock prefix is implicit for xchg.
+ : "=r" (new_value)
+ : "m" (*ptr), "0" (new_value)
+ : "memory");
+ return new_value; // Now it's the previous value.
+}
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ Atomic32 temp = increment;
+ __asm__ __volatile__("lock; xaddl %0,%1"
+ : "+r" (temp), "+m" (*ptr)
+ : : "memory");
+ // temp now holds the old value of *ptr
+ return temp + increment;
+}
+
+inline Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ Atomic32 temp = increment;
+ __asm__ __volatile__("lock; xaddl %0,%1"
+ : "+r" (temp), "+m" (*ptr)
+ : : "memory");
+ // temp now holds the old value of *ptr
+ if (AtomicOps_Internalx86CPUFeatures.has_amd_lock_mb_bug) {
+ __asm__ __volatile__("lfence" : : : "memory");
+ }
+ return temp + increment;
+}
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ Atomic32 x = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ if (AtomicOps_Internalx86CPUFeatures.has_amd_lock_mb_bug) {
+ __asm__ __volatile__("lfence" : : : "memory");
+ }
+ return x;
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+#if defined(__x86_64__)
+
+// 64-bit implementations of memory barrier can be simpler, because it
+// "mfence" is guaranteed to exist.
+inline void MemoryBarrier() {
+ __asm__ __volatile__("mfence" : : : "memory");
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+#else
+
+inline void MemoryBarrier() {
+ if (AtomicOps_Internalx86CPUFeatures.has_sse2) {
+ __asm__ __volatile__("mfence" : : : "memory");
+ } else { // mfence is faster but not present on PIII
+ Atomic32 x = 0;
+ NoBarrier_AtomicExchange(&x, 0); // acts as a barrier on PIII
+ }
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ if (AtomicOps_Internalx86CPUFeatures.has_sse2) {
+ *ptr = value;
+ __asm__ __volatile__("mfence" : : : "memory");
+ } else {
+ NoBarrier_AtomicExchange(ptr, value);
+ // acts as a barrier on PIII
+ }
+}
+#endif
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ ATOMICOPS_COMPILER_BARRIER();
+ *ptr = value; // An x86 store acts as a release barrier.
+ // See comments in Atomic64 version of Release_Store(), below.
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr; // An x86 load acts as a acquire barrier.
+ // See comments in Atomic64 version of Release_Store(), below.
+ ATOMICOPS_COMPILER_BARRIER();
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+#if defined(__x86_64__)
+
+// 64-bit low-level operations on 64-bit platform.
+
+inline Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 prev;
+ __asm__ __volatile__("lock; cmpxchgq %1,%2"
+ : "=a" (prev)
+ : "q" (new_value), "m" (*ptr), "0" (old_value)
+ : "memory");
+ return prev;
+}
+
+inline Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr,
+ Atomic64 new_value) {
+ __asm__ __volatile__("xchgq %1,%0" // The lock prefix is implicit for xchg.
+ : "=r" (new_value)
+ : "m" (*ptr), "0" (new_value)
+ : "memory");
+ return new_value; // Now it's the previous value.
+}
+
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ Atomic64 temp = increment;
+ __asm__ __volatile__("lock; xaddq %0,%1"
+ : "+r" (temp), "+m" (*ptr)
+ : : "memory");
+ // temp now contains the previous value of *ptr
+ return temp + increment;
+}
+
+inline Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ Atomic64 temp = increment;
+ __asm__ __volatile__("lock; xaddq %0,%1"
+ : "+r" (temp), "+m" (*ptr)
+ : : "memory");
+ // temp now contains the previous value of *ptr
+ if (AtomicOps_Internalx86CPUFeatures.has_amd_lock_mb_bug) {
+ __asm__ __volatile__("lfence" : : : "memory");
+ }
+ return temp + increment;
+}
+
+inline void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+ MemoryBarrier();
+}
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ ATOMICOPS_COMPILER_BARRIER();
+
+ *ptr = value; // An x86 store acts as a release barrier
+ // for current AMD/Intel chips as of Jan 2008.
+ // See also Acquire_Load(), below.
+
+ // When new chips come out, check:
+ // IA-32 Intel Architecture Software Developer's Manual, Volume 3:
+ // System Programming Guide, Chatper 7: Multiple-processor management,
+ // Section 7.2, Memory Ordering.
+ // Last seen at:
+ // http://developer.intel.com/design/pentium4/manuals/index_new.htm
+ //
+ // x86 stores/loads fail to act as barriers for a few instructions (clflush
+ // maskmovdqu maskmovq movntdq movnti movntpd movntps movntq) but these are
+ // not generated by the compiler, and are rare. Users of these instructions
+ // need to know about cache behaviour in any case since all of these involve
+ // either flushing cache lines or non-temporal cache hints.
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64* ptr) {
+ return *ptr;
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ Atomic64 value = *ptr; // An x86 load acts as a acquire barrier,
+ // for current AMD/Intel chips as of Jan 2008.
+ // See also Release_Store(), above.
+ ATOMICOPS_COMPILER_BARRIER();
+ return value;
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ Atomic64 x = NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+ if (AtomicOps_Internalx86CPUFeatures.has_amd_lock_mb_bug) {
+ __asm__ __volatile__("lfence" : : : "memory");
+ }
+ return x;
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+#endif // defined(__x86_64__)
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#undef ATOMICOPS_COMPILER_BARRIER
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_GCC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc
new file mode 100644
index 0000000000..741b164f0f
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.cc
@@ -0,0 +1,112 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// The compilation of extension_set.cc fails when windows.h is included.
+// Therefore we move the code depending on windows.h to this separate cc file.
+
+// Don't compile this file for people not concerned about thread safety.
+#ifndef GOOGLE_PROTOBUF_NO_THREAD_SAFETY
+
+#include <google/protobuf/stubs/atomicops.h>
+
+#ifdef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_MSVC_H_
+
+#include <windows.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline void MemoryBarrier() {
+ // We use MemoryBarrier from WinNT.h
+ ::MemoryBarrier();
+}
+
+Atomic32 NoBarrier_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ LONG result = InterlockedCompareExchange(
+ reinterpret_cast<volatile LONG*>(ptr),
+ static_cast<LONG>(new_value),
+ static_cast<LONG>(old_value));
+ return static_cast<Atomic32>(result);
+}
+
+Atomic32 NoBarrier_AtomicExchange(volatile Atomic32* ptr,
+ Atomic32 new_value) {
+ LONG result = InterlockedExchange(
+ reinterpret_cast<volatile LONG*>(ptr),
+ static_cast<LONG>(new_value));
+ return static_cast<Atomic32>(result);
+}
+
+Atomic32 Barrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return InterlockedExchangeAdd(
+ reinterpret_cast<volatile LONG*>(ptr),
+ static_cast<LONG>(increment)) + increment;
+}
+
+#if defined(_WIN64)
+
+// 64-bit low-level operations on 64-bit platform.
+
+Atomic64 NoBarrier_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ PVOID result = InterlockedCompareExchangePointer(
+ reinterpret_cast<volatile PVOID*>(ptr),
+ reinterpret_cast<PVOID>(new_value), reinterpret_cast<PVOID>(old_value));
+ return reinterpret_cast<Atomic64>(result);
+}
+
+Atomic64 NoBarrier_AtomicExchange(volatile Atomic64* ptr,
+ Atomic64 new_value) {
+ PVOID result = InterlockedExchangePointer(
+ reinterpret_cast<volatile PVOID*>(ptr),
+ reinterpret_cast<PVOID>(new_value));
+ return reinterpret_cast<Atomic64>(result);
+}
+
+Atomic64 Barrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ return InterlockedExchangeAdd64(
+ reinterpret_cast<volatile LONGLONG*>(ptr),
+ static_cast<LONGLONG>(increment)) + increment;
+}
+
+#endif // defined(_WIN64)
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_MSVC_H_
+#endif // GOOGLE_PROTOBUF_NO_THREAD_SAFETY
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.h b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.h
new file mode 100644
index 0000000000..e53a641f09
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/atomicops_internals_x86_msvc.h
@@ -0,0 +1,150 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This file is an internal atomic implementation, use atomicops.h instead.
+
+#ifndef GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_MSVC_H_
+#define GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_MSVC_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+inline Atomic32 NoBarrier_AtomicIncrement(volatile Atomic32* ptr,
+ Atomic32 increment) {
+ return Barrier_AtomicIncrement(ptr, increment);
+}
+
+#if !(defined(_MSC_VER) && _MSC_VER >= 1400)
+#error "We require at least vs2005 for MemoryBarrier"
+#endif
+
+inline Atomic32 Acquire_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline Atomic32 Release_CompareAndSwap(volatile Atomic32* ptr,
+ Atomic32 old_value,
+ Atomic32 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline void NoBarrier_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic32* ptr, Atomic32 value) {
+ NoBarrier_AtomicExchange(ptr, value);
+ // acts as a barrier in this implementation
+}
+
+inline void Release_Store(volatile Atomic32* ptr, Atomic32 value) {
+ *ptr = value; // works w/o barrier for current Intel chips as of June 2005
+ // See comments in Atomic64 version of Release_Store() below.
+}
+
+inline Atomic32 NoBarrier_Load(volatile const Atomic32* ptr) {
+ return *ptr;
+}
+
+inline Atomic32 Acquire_Load(volatile const Atomic32* ptr) {
+ Atomic32 value = *ptr;
+ return value;
+}
+
+inline Atomic32 Release_Load(volatile const Atomic32* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+#if defined(_WIN64)
+
+// 64-bit low-level operations on 64-bit platform.
+
+inline Atomic64 NoBarrier_AtomicIncrement(volatile Atomic64* ptr,
+ Atomic64 increment) {
+ return Barrier_AtomicIncrement(ptr, increment);
+}
+
+inline void NoBarrier_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value;
+}
+
+inline void Acquire_Store(volatile Atomic64* ptr, Atomic64 value) {
+ NoBarrier_AtomicExchange(ptr, value);
+ // acts as a barrier in this implementation
+}
+
+inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
+ *ptr = value; // works w/o barrier for current Intel chips as of June 2005
+
+ // When new chips come out, check:
+ // IA-32 Intel Architecture Software Developer's Manual, Volume 3:
+ // System Programming Guide, Chatper 7: Multiple-processor management,
+ // Section 7.2, Memory Ordering.
+ // Last seen at:
+ // http://developer.intel.com/design/pentium4/manuals/index_new.htm
+}
+
+inline Atomic64 NoBarrier_Load(volatile const Atomic64* ptr) {
+ return *ptr;
+}
+
+inline Atomic64 Acquire_Load(volatile const Atomic64* ptr) {
+ Atomic64 value = *ptr;
+ return value;
+}
+
+inline Atomic64 Release_Load(volatile const Atomic64* ptr) {
+ MemoryBarrier();
+ return *ptr;
+}
+
+inline Atomic64 Acquire_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+inline Atomic64 Release_CompareAndSwap(volatile Atomic64* ptr,
+ Atomic64 old_value,
+ Atomic64 new_value) {
+ return NoBarrier_CompareAndSwap(ptr, old_value, new_value);
+}
+
+#endif // defined(_WIN64)
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_ATOMICOPS_INTERNALS_X86_MSVC_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/common.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/common.cc
new file mode 100644
index 0000000000..8995859963
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/common.cc
@@ -0,0 +1,394 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <stdio.h>
+#include <errno.h>
+#include <vector>
+
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN // We only need minimal includes
+#include <windows.h>
+#define snprintf _snprintf // see comment in strutil.cc
+#elif defined(HAVE_PTHREAD_H)
+#include <pthread.h>
+#else
+#error "No suitable threading library available."
+#endif
+
+namespace google {
+namespace protobuf {
+
+namespace internal {
+
+void VerifyVersion(int headerVersion,
+ int minLibraryVersion,
+ const char* filename) {
+ if (GOOGLE_PROTOBUF_VERSION < minLibraryVersion) {
+ // Library is too old for headers.
+ GOOGLE_LOG(FATAL)
+ << "This program requires version " << VersionString(minLibraryVersion)
+ << " of the Protocol Buffer runtime library, but the installed version "
+ "is " << VersionString(GOOGLE_PROTOBUF_VERSION) << ". Please update "
+ "your library. If you compiled the program yourself, make sure that "
+ "your headers are from the same version of Protocol Buffers as your "
+ "link-time library. (Version verification failed in \""
+ << filename << "\".)";
+ }
+ if (headerVersion < kMinHeaderVersionForLibrary) {
+ // Headers are too old for library.
+ GOOGLE_LOG(FATAL)
+ << "This program was compiled against version "
+ << VersionString(headerVersion) << " of the Protocol Buffer runtime "
+ "library, which is not compatible with the installed version ("
+ << VersionString(GOOGLE_PROTOBUF_VERSION) << "). Contact the program "
+ "author for an update. If you compiled the program yourself, make "
+ "sure that your headers are from the same version of Protocol Buffers "
+ "as your link-time library. (Version verification failed in \""
+ << filename << "\".)";
+ }
+}
+
+string VersionString(int version) {
+ int major = version / 1000000;
+ int minor = (version / 1000) % 1000;
+ int micro = version % 1000;
+
+ // 128 bytes should always be enough, but we use snprintf() anyway to be
+ // safe.
+ char buffer[128];
+ snprintf(buffer, sizeof(buffer), "%d.%d.%d", major, minor, micro);
+
+ // Guard against broken MSVC snprintf().
+ buffer[sizeof(buffer)-1] = '\0';
+
+ return buffer;
+}
+
+} // namespace internal
+
+// ===================================================================
+// emulates google3/base/logging.cc
+
+namespace internal {
+
+void DefaultLogHandler(LogLevel level, const char* filename, int line,
+ const string& message) {
+ static const char* level_names[] = { "INFO", "WARNING", "ERROR", "FATAL" };
+
+ // We use fprintf() instead of cerr because we want this to work at static
+ // initialization time.
+ fprintf(stderr, "[libprotobuf %s %s:%d] %s\n",
+ level_names[level], filename, line, message.c_str());
+ fflush(stderr); // Needed on MSVC.
+}
+
+void NullLogHandler(LogLevel /* level */, const char* /* filename */,
+ int /* line */, const string& /* message */) {
+ // Nothing.
+}
+
+static LogHandler* log_handler_ = &DefaultLogHandler;
+static int log_silencer_count_ = 0;
+
+static Mutex* log_silencer_count_mutex_ = NULL;
+GOOGLE_PROTOBUF_DECLARE_ONCE(log_silencer_count_init_);
+
+void DeleteLogSilencerCount() {
+ delete log_silencer_count_mutex_;
+ log_silencer_count_mutex_ = NULL;
+}
+void InitLogSilencerCount() {
+ log_silencer_count_mutex_ = new Mutex;
+ OnShutdown(&DeleteLogSilencerCount);
+}
+void InitLogSilencerCountOnce() {
+ GoogleOnceInit(&log_silencer_count_init_, &InitLogSilencerCount);
+}
+
+LogMessage& LogMessage::operator<<(const string& value) {
+ message_ += value;
+ return *this;
+}
+
+LogMessage& LogMessage::operator<<(const char* value) {
+ message_ += value;
+ return *this;
+}
+
+// Since this is just for logging, we don't care if the current locale changes
+// the results -- in fact, we probably prefer that. So we use snprintf()
+// instead of Simple*toa().
+#undef DECLARE_STREAM_OPERATOR
+#define DECLARE_STREAM_OPERATOR(TYPE, FORMAT) \
+ LogMessage& LogMessage::operator<<(TYPE value) { \
+ /* 128 bytes should be big enough for any of the primitive */ \
+ /* values which we print with this, but well use snprintf() */ \
+ /* anyway to be extra safe. */ \
+ char buffer[128]; \
+ snprintf(buffer, sizeof(buffer), FORMAT, value); \
+ /* Guard against broken MSVC snprintf(). */ \
+ buffer[sizeof(buffer)-1] = '\0'; \
+ message_ += buffer; \
+ return *this; \
+ }
+
+DECLARE_STREAM_OPERATOR(char , "%c" )
+DECLARE_STREAM_OPERATOR(int , "%d" )
+DECLARE_STREAM_OPERATOR(uint , "%u" )
+DECLARE_STREAM_OPERATOR(long , "%ld")
+DECLARE_STREAM_OPERATOR(unsigned long, "%lu")
+DECLARE_STREAM_OPERATOR(double , "%g" )
+#undef DECLARE_STREAM_OPERATOR
+
+LogMessage::LogMessage(LogLevel level, const char* filename, int line)
+ : level_(level), filename_(filename), line_(line) {}
+LogMessage::~LogMessage() {}
+
+void LogMessage::Finish() {
+ bool suppress = false;
+
+ if (level_ != LOGLEVEL_FATAL) {
+ InitLogSilencerCountOnce();
+ MutexLock lock(log_silencer_count_mutex_);
+ suppress = log_silencer_count_ > 0;
+ }
+
+ if (!suppress) {
+ log_handler_(level_, filename_, line_, message_);
+ }
+
+ if (level_ == LOGLEVEL_FATAL) {
+#if PROTOBUF_USE_EXCEPTIONS
+ throw FatalException(filename_, line_, message_);
+#else
+ abort();
+#endif
+ }
+}
+
+void LogFinisher::operator=(LogMessage& other) {
+ other.Finish();
+}
+
+} // namespace internal
+
+LogHandler* SetLogHandler(LogHandler* new_func) {
+ LogHandler* old = internal::log_handler_;
+ if (old == &internal::NullLogHandler) {
+ old = NULL;
+ }
+ if (new_func == NULL) {
+ internal::log_handler_ = &internal::NullLogHandler;
+ } else {
+ internal::log_handler_ = new_func;
+ }
+ return old;
+}
+
+LogSilencer::LogSilencer() {
+ internal::InitLogSilencerCountOnce();
+ MutexLock lock(internal::log_silencer_count_mutex_);
+ ++internal::log_silencer_count_;
+};
+
+LogSilencer::~LogSilencer() {
+ internal::InitLogSilencerCountOnce();
+ MutexLock lock(internal::log_silencer_count_mutex_);
+ --internal::log_silencer_count_;
+};
+
+// ===================================================================
+// emulates google3/base/callback.cc
+
+Closure::~Closure() {}
+
+namespace internal { FunctionClosure0::~FunctionClosure0() {} }
+
+void DoNothing() {}
+
+// ===================================================================
+// emulates google3/base/mutex.cc
+
+#ifdef _WIN32
+
+struct Mutex::Internal {
+ CRITICAL_SECTION mutex;
+#ifndef NDEBUG
+ // Used only to implement AssertHeld().
+ DWORD thread_id;
+#endif
+};
+
+Mutex::Mutex()
+ : mInternal(new Internal) {
+ InitializeCriticalSection(&mInternal->mutex);
+}
+
+Mutex::~Mutex() {
+ DeleteCriticalSection(&mInternal->mutex);
+ delete mInternal;
+}
+
+void Mutex::Lock() {
+ EnterCriticalSection(&mInternal->mutex);
+#ifndef NDEBUG
+ mInternal->thread_id = GetCurrentThreadId();
+#endif
+}
+
+void Mutex::Unlock() {
+#ifndef NDEBUG
+ mInternal->thread_id = 0;
+#endif
+ LeaveCriticalSection(&mInternal->mutex);
+}
+
+void Mutex::AssertHeld() {
+#ifndef NDEBUG
+ GOOGLE_DCHECK_EQ(mInternal->thread_id, GetCurrentThreadId());
+#endif
+}
+
+#elif defined(HAVE_PTHREAD)
+
+struct Mutex::Internal {
+ pthread_mutex_t mutex;
+};
+
+Mutex::Mutex()
+ : mInternal(new Internal) {
+ pthread_mutex_init(&mInternal->mutex, NULL);
+}
+
+Mutex::~Mutex() {
+ pthread_mutex_destroy(&mInternal->mutex);
+ delete mInternal;
+}
+
+void Mutex::Lock() {
+ int result = pthread_mutex_lock(&mInternal->mutex);
+ if (result != 0) {
+ GOOGLE_LOG(FATAL) << "pthread_mutex_lock: " << strerror(result);
+ }
+}
+
+void Mutex::Unlock() {
+ int result = pthread_mutex_unlock(&mInternal->mutex);
+ if (result != 0) {
+ GOOGLE_LOG(FATAL) << "pthread_mutex_unlock: " << strerror(result);
+ }
+}
+
+void Mutex::AssertHeld() {
+ // pthreads dosn't provide a way to check which thread holds the mutex.
+ // TODO(kenton): Maybe keep track of locking thread ID like with WIN32?
+}
+
+#endif
+
+// ===================================================================
+// emulates google3/util/endian/endian.h
+//
+// TODO(xiaofeng): PROTOBUF_LITTLE_ENDIAN is unfortunately defined in
+// google/protobuf/io/coded_stream.h and therefore can not be used here.
+// Maybe move that macro definition here in the furture.
+uint32 ghtonl(uint32 x) {
+ union {
+ uint32 result;
+ uint8 result_array[4];
+ };
+ result_array[0] = static_cast<uint8>(x >> 24);
+ result_array[1] = static_cast<uint8>((x >> 16) & 0xFF);
+ result_array[2] = static_cast<uint8>((x >> 8) & 0xFF);
+ result_array[3] = static_cast<uint8>(x & 0xFF);
+ return result;
+}
+
+// ===================================================================
+// Shutdown support.
+
+namespace internal {
+
+typedef void OnShutdownFunc();
+vector<void (*)()>* shutdown_functions = NULL;
+Mutex* shutdown_functions_mutex = NULL;
+GOOGLE_PROTOBUF_DECLARE_ONCE(shutdown_functions_init);
+
+void InitShutdownFunctions() {
+ shutdown_functions = new vector<void (*)()>;
+ shutdown_functions_mutex = new Mutex;
+}
+
+inline void InitShutdownFunctionsOnce() {
+ GoogleOnceInit(&shutdown_functions_init, &InitShutdownFunctions);
+}
+
+void OnShutdown(void (*func)()) {
+ InitShutdownFunctionsOnce();
+ MutexLock lock(shutdown_functions_mutex);
+ shutdown_functions->push_back(func);
+}
+
+} // namespace internal
+
+void ShutdownProtobufLibrary() {
+ internal::InitShutdownFunctionsOnce();
+
+ // We don't need to lock shutdown_functions_mutex because it's up to the
+ // caller to make sure that no one is using the library before this is
+ // called.
+
+ // Make it safe to call this multiple times.
+ if (internal::shutdown_functions == NULL) return;
+
+ for (int i = 0; i < internal::shutdown_functions->size(); i++) {
+ internal::shutdown_functions->at(i)();
+ }
+ delete internal::shutdown_functions;
+ internal::shutdown_functions = NULL;
+ delete internal::shutdown_functions_mutex;
+ internal::shutdown_functions_mutex = NULL;
+}
+
+#if PROTOBUF_USE_EXCEPTIONS
+FatalException::~FatalException() throw() {}
+
+const char* FatalException::what() const throw() {
+ return message_.c_str();
+}
+#endif
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/common.h b/toolkit/components/protobuf/src/google/protobuf/stubs/common.h
new file mode 100644
index 0000000000..fa6fe3ce97
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/common.h
@@ -0,0 +1,1186 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda) and others
+//
+// Contains basic types and utilities used by the rest of the library.
+
+#ifndef GOOGLE_PROTOBUF_COMMON_H__
+#define GOOGLE_PROTOBUF_COMMON_H__
+
+#include <assert.h>
+#include <stdlib.h>
+#include <cstddef>
+#include <string>
+#include <string.h>
+#if defined(__osf__)
+// Tru64 lacks stdint.h, but has inttypes.h which defines a superset of
+// what stdint.h would define.
+#include <inttypes.h>
+#elif !defined(_MSC_VER)
+#include <stdint.h>
+#endif
+
+#ifndef PROTOBUF_USE_EXCEPTIONS
+#if defined(_MSC_VER) && defined(_CPPUNWIND)
+ #define PROTOBUF_USE_EXCEPTIONS 1
+#elif defined(__EXCEPTIONS)
+ #define PROTOBUF_USE_EXCEPTIONS 1
+#else
+ #define PROTOBUF_USE_EXCEPTIONS 0
+#endif
+#endif
+
+#if PROTOBUF_USE_EXCEPTIONS
+#include <exception>
+#endif
+
+#if defined(_WIN32) && defined(GetMessage)
+// Allow GetMessage to be used as a valid method name in protobuf classes.
+// windows.h defines GetMessage() as a macro. Let's re-define it as an inline
+// function. The inline function should be equivalent for C++ users.
+inline BOOL GetMessage_Win32(
+ LPMSG lpMsg, HWND hWnd,
+ UINT wMsgFilterMin, UINT wMsgFilterMax) {
+ return GetMessage(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
+}
+#undef GetMessage
+inline BOOL GetMessage(
+ LPMSG lpMsg, HWND hWnd,
+ UINT wMsgFilterMin, UINT wMsgFilterMax) {
+ return GetMessage_Win32(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
+}
+#endif
+
+
+namespace std {}
+
+namespace google {
+namespace protobuf {
+
+#undef GOOGLE_DISALLOW_EVIL_CONSTRUCTORS
+#define GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(TypeName) \
+ TypeName(const TypeName&); \
+ void operator=(const TypeName&)
+
+#if defined(_MSC_VER) && defined(PROTOBUF_USE_DLLS)
+ #ifdef LIBPROTOBUF_EXPORTS
+ #define LIBPROTOBUF_EXPORT __declspec(dllexport)
+ #else
+ #define LIBPROTOBUF_EXPORT __declspec(dllimport)
+ #endif
+ #ifdef LIBPROTOC_EXPORTS
+ #define LIBPROTOC_EXPORT __declspec(dllexport)
+ #else
+ #define LIBPROTOC_EXPORT __declspec(dllimport)
+ #endif
+#else
+ #define LIBPROTOBUF_EXPORT
+ #define LIBPROTOC_EXPORT
+#endif
+
+namespace internal {
+
+// Some of these constants are macros rather than const ints so that they can
+// be used in #if directives.
+
+// The current version, represented as a single integer to make comparison
+// easier: major * 10^6 + minor * 10^3 + micro
+#define GOOGLE_PROTOBUF_VERSION 2006001
+
+// The minimum library version which works with the current version of the
+// headers.
+#define GOOGLE_PROTOBUF_MIN_LIBRARY_VERSION 2006000
+
+// The minimum header version which works with the current version of
+// the library. This constant should only be used by protoc's C++ code
+// generator.
+static const int kMinHeaderVersionForLibrary = 2006000;
+
+// The minimum protoc version which works with the current version of the
+// headers.
+#define GOOGLE_PROTOBUF_MIN_PROTOC_VERSION 2006000
+
+// The minimum header version which works with the current version of
+// protoc. This constant should only be used in VerifyVersion().
+static const int kMinHeaderVersionForProtoc = 2006000;
+
+// Verifies that the headers and libraries are compatible. Use the macro
+// below to call this.
+void LIBPROTOBUF_EXPORT VerifyVersion(int headerVersion, int minLibraryVersion,
+ const char* filename);
+
+// Converts a numeric version number to a string.
+std::string LIBPROTOBUF_EXPORT VersionString(int version);
+
+} // namespace internal
+
+// Place this macro in your main() function (or somewhere before you attempt
+// to use the protobuf library) to verify that the version you link against
+// matches the headers you compiled against. If a version mismatch is
+// detected, the process will abort.
+#define GOOGLE_PROTOBUF_VERIFY_VERSION \
+ ::google::protobuf::internal::VerifyVersion( \
+ GOOGLE_PROTOBUF_VERSION, GOOGLE_PROTOBUF_MIN_LIBRARY_VERSION, \
+ __FILE__)
+
+// ===================================================================
+// from google3/base/port.h
+
+typedef unsigned int uint;
+
+#ifdef _MSC_VER
+typedef __int8 int8;
+typedef __int16 int16;
+typedef __int32 int32;
+typedef __int64 int64;
+
+typedef unsigned __int8 uint8;
+typedef unsigned __int16 uint16;
+typedef unsigned __int32 uint32;
+typedef unsigned __int64 uint64;
+#else
+typedef int8_t int8;
+typedef int16_t int16;
+typedef int32_t int32;
+typedef int64_t int64;
+
+typedef uint8_t uint8;
+typedef uint16_t uint16;
+typedef uint32_t uint32;
+typedef uint64_t uint64;
+#endif
+
+// long long macros to be used because gcc and vc++ use different suffixes,
+// and different size specifiers in format strings
+#undef GOOGLE_LONGLONG
+#undef GOOGLE_ULONGLONG
+#undef GOOGLE_LL_FORMAT
+
+#ifdef _MSC_VER
+#define GOOGLE_LONGLONG(x) x##I64
+#define GOOGLE_ULONGLONG(x) x##UI64
+#define GOOGLE_LL_FORMAT "I64" // As in printf("%I64d", ...)
+#else
+#define GOOGLE_LONGLONG(x) x##LL
+#define GOOGLE_ULONGLONG(x) x##ULL
+#define GOOGLE_LL_FORMAT "ll" // As in "%lld". Note that "q" is poor form also.
+#endif
+
+static const int32 kint32max = 0x7FFFFFFF;
+static const int32 kint32min = -kint32max - 1;
+static const int64 kint64max = GOOGLE_LONGLONG(0x7FFFFFFFFFFFFFFF);
+static const int64 kint64min = -kint64max - 1;
+static const uint32 kuint32max = 0xFFFFFFFFu;
+static const uint64 kuint64max = GOOGLE_ULONGLONG(0xFFFFFFFFFFFFFFFF);
+
+// -------------------------------------------------------------------
+// Annotations: Some parts of the code have been annotated in ways that might
+// be useful to some compilers or tools, but are not supported universally.
+// You can #define these annotations yourself if the default implementation
+// is not right for you.
+
+#ifndef GOOGLE_ATTRIBUTE_ALWAYS_INLINE
+#if defined(__GNUC__) && (__GNUC__ > 3 ||(__GNUC__ == 3 && __GNUC_MINOR__ >= 1))
+// For functions we want to force inline.
+// Introduced in gcc 3.1.
+#define GOOGLE_ATTRIBUTE_ALWAYS_INLINE __attribute__ ((always_inline))
+#else
+// Other compilers will have to figure it out for themselves.
+#define GOOGLE_ATTRIBUTE_ALWAYS_INLINE
+#endif
+#endif
+
+#ifndef GOOGLE_ATTRIBUTE_DEPRECATED
+#ifdef __GNUC__
+// If the method/variable/type is used anywhere, produce a warning.
+#define GOOGLE_ATTRIBUTE_DEPRECATED __attribute__((deprecated))
+#else
+#define GOOGLE_ATTRIBUTE_DEPRECATED
+#endif
+#endif
+
+#ifndef GOOGLE_PREDICT_TRUE
+#ifdef __GNUC__
+// Provided at least since GCC 3.0.
+#define GOOGLE_PREDICT_TRUE(x) (__builtin_expect(!!(x), 1))
+#else
+#define GOOGLE_PREDICT_TRUE
+#endif
+#endif
+
+// Delimits a block of code which may write to memory which is simultaneously
+// written by other threads, but which has been determined to be thread-safe
+// (e.g. because it is an idempotent write).
+#ifndef GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN
+#define GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN()
+#endif
+#ifndef GOOGLE_SAFE_CONCURRENT_WRITES_END
+#define GOOGLE_SAFE_CONCURRENT_WRITES_END()
+#endif
+
+// ===================================================================
+// from google3/base/basictypes.h
+
+// The GOOGLE_ARRAYSIZE(arr) macro returns the # of elements in an array arr.
+// The expression is a compile-time constant, and therefore can be
+// used in defining new arrays, for example.
+//
+// GOOGLE_ARRAYSIZE catches a few type errors. If you see a compiler error
+//
+// "warning: division by zero in ..."
+//
+// when using GOOGLE_ARRAYSIZE, you are (wrongfully) giving it a pointer.
+// You should only use GOOGLE_ARRAYSIZE on statically allocated arrays.
+//
+// The following comments are on the implementation details, and can
+// be ignored by the users.
+//
+// ARRAYSIZE(arr) works by inspecting sizeof(arr) (the # of bytes in
+// the array) and sizeof(*(arr)) (the # of bytes in one array
+// element). If the former is divisible by the latter, perhaps arr is
+// indeed an array, in which case the division result is the # of
+// elements in the array. Otherwise, arr cannot possibly be an array,
+// and we generate a compiler error to prevent the code from
+// compiling.
+//
+// Since the size of bool is implementation-defined, we need to cast
+// !(sizeof(a) & sizeof(*(a))) to size_t in order to ensure the final
+// result has type size_t.
+//
+// This macro is not perfect as it wrongfully accepts certain
+// pointers, namely where the pointer size is divisible by the pointee
+// size. Since all our code has to go through a 32-bit compiler,
+// where a pointer is 4 bytes, this means all pointers to a type whose
+// size is 3 or greater than 4 will be (righteously) rejected.
+//
+// Kudos to Jorg Brown for this simple and elegant implementation.
+
+#undef GOOGLE_ARRAYSIZE
+#define GOOGLE_ARRAYSIZE(a) \
+ ((sizeof(a) / sizeof(*(a))) / \
+ static_cast<size_t>(!(sizeof(a) % sizeof(*(a)))))
+
+namespace internal {
+
+// Use implicit_cast as a safe version of static_cast or const_cast
+// for upcasting in the type hierarchy (i.e. casting a pointer to Foo
+// to a pointer to SuperclassOfFoo or casting a pointer to Foo to
+// a const pointer to Foo).
+// When you use implicit_cast, the compiler checks that the cast is safe.
+// Such explicit implicit_casts are necessary in surprisingly many
+// situations where C++ demands an exact type match instead of an
+// argument type convertable to a target type.
+//
+// The From type can be inferred, so the preferred syntax for using
+// implicit_cast is the same as for static_cast etc.:
+//
+// implicit_cast<ToType>(expr)
+//
+// implicit_cast would have been part of the C++ standard library,
+// but the proposal was submitted too late. It will probably make
+// its way into the language in the future.
+template<typename To, typename From>
+inline To implicit_cast(From const &f) {
+ return f;
+}
+
+// When you upcast (that is, cast a pointer from type Foo to type
+// SuperclassOfFoo), it's fine to use implicit_cast<>, since upcasts
+// always succeed. When you downcast (that is, cast a pointer from
+// type Foo to type SubclassOfFoo), static_cast<> isn't safe, because
+// how do you know the pointer is really of type SubclassOfFoo? It
+// could be a bare Foo, or of type DifferentSubclassOfFoo. Thus,
+// when you downcast, you should use this macro. In debug mode, we
+// use dynamic_cast<> to double-check the downcast is legal (we die
+// if it's not). In normal mode, we do the efficient static_cast<>
+// instead. Thus, it's important to test in debug mode to make sure
+// the cast is legal!
+// This is the only place in the code we should use dynamic_cast<>.
+// In particular, you SHOULDN'T be using dynamic_cast<> in order to
+// do RTTI (eg code like this:
+// if (dynamic_cast<Subclass1>(foo)) HandleASubclass1Object(foo);
+// if (dynamic_cast<Subclass2>(foo)) HandleASubclass2Object(foo);
+// You should design the code some other way not to need this.
+
+template<typename To, typename From> // use like this: down_cast<T*>(foo);
+inline To down_cast(From* f) { // so we only accept pointers
+ // Ensures that To is a sub-type of From *. This test is here only
+ // for compile-time type checking, and has no overhead in an
+ // optimized build at run-time, as it will be optimized away
+ // completely.
+ if (false) {
+ implicit_cast<From*, To>(0);
+ }
+
+#if !defined(NDEBUG) && !defined(GOOGLE_PROTOBUF_NO_RTTI)
+ assert(f == NULL || dynamic_cast<To>(f) != NULL); // RTTI: debug mode only!
+#endif
+ return static_cast<To>(f);
+}
+
+} // namespace internal
+
+// We made these internal so that they would show up as such in the docs,
+// but we don't want to stick "internal::" in front of them everywhere.
+using internal::implicit_cast;
+using internal::down_cast;
+
+// The COMPILE_ASSERT macro can be used to verify that a compile time
+// expression is true. For example, you could use it to verify the
+// size of a static array:
+//
+// COMPILE_ASSERT(ARRAYSIZE(content_type_names) == CONTENT_NUM_TYPES,
+// content_type_names_incorrect_size);
+//
+// or to make sure a struct is smaller than a certain size:
+//
+// COMPILE_ASSERT(sizeof(foo) < 128, foo_too_large);
+//
+// The second argument to the macro is the name of the variable. If
+// the expression is false, most compilers will issue a warning/error
+// containing the name of the variable.
+
+#define GOOGLE_COMPILE_ASSERT(expr, msg) static_assert(expr, #msg)
+
+
+
+// ===================================================================
+// from google3/base/scoped_ptr.h
+
+namespace internal {
+
+// This is an implementation designed to match the anticipated future TR2
+// implementation of the scoped_ptr class, and its closely-related brethren,
+// scoped_array, scoped_ptr_malloc, and make_scoped_ptr.
+
+template <class C> class scoped_ptr;
+template <class C> class scoped_array;
+
+// A scoped_ptr<T> is like a T*, except that the destructor of scoped_ptr<T>
+// automatically deletes the pointer it holds (if any).
+// That is, scoped_ptr<T> owns the T object that it points to.
+// Like a T*, a scoped_ptr<T> may hold either NULL or a pointer to a T object.
+//
+// The size of a scoped_ptr is small:
+// sizeof(scoped_ptr<C>) == sizeof(C*)
+template <class C>
+class scoped_ptr {
+ public:
+
+ // The element type
+ typedef C element_type;
+
+ // Constructor. Defaults to intializing with NULL.
+ // There is no way to create an uninitialized scoped_ptr.
+ // The input parameter must be allocated with new.
+ explicit scoped_ptr(C* p = NULL) : ptr_(p) { }
+
+ // Destructor. If there is a C object, delete it.
+ // We don't need to test ptr_ == NULL because C++ does that for us.
+ ~scoped_ptr() {
+ enum { type_must_be_complete = sizeof(C) };
+ delete ptr_;
+ }
+
+ // Reset. Deletes the current owned object, if any.
+ // Then takes ownership of a new object, if given.
+ // this->reset(this->get()) works.
+ void reset(C* p = NULL) {
+ if (p != ptr_) {
+ enum { type_must_be_complete = sizeof(C) };
+ delete ptr_;
+ ptr_ = p;
+ }
+ }
+
+ // Accessors to get the owned object.
+ // operator* and operator-> will assert() if there is no current object.
+ C& operator*() const {
+ assert(ptr_ != NULL);
+ return *ptr_;
+ }
+ C* operator->() const {
+ assert(ptr_ != NULL);
+ return ptr_;
+ }
+ C* get() const { return ptr_; }
+
+ // Comparison operators.
+ // These return whether two scoped_ptr refer to the same object, not just to
+ // two different but equal objects.
+ bool operator==(C* p) const { return ptr_ == p; }
+ bool operator!=(C* p) const { return ptr_ != p; }
+
+ // Swap two scoped pointers.
+ void swap(scoped_ptr& p2) {
+ C* tmp = ptr_;
+ ptr_ = p2.ptr_;
+ p2.ptr_ = tmp;
+ }
+
+ // Release a pointer.
+ // The return value is the current pointer held by this object.
+ // If this object holds a NULL pointer, the return value is NULL.
+ // After this operation, this object will hold a NULL pointer,
+ // and will not own the object any more.
+ C* release() {
+ C* retVal = ptr_;
+ ptr_ = NULL;
+ return retVal;
+ }
+
+ private:
+ C* ptr_;
+
+ // Forbid comparison of scoped_ptr types. If C2 != C, it totally doesn't
+ // make sense, and if C2 == C, it still doesn't make sense because you should
+ // never have the same object owned by two different scoped_ptrs.
+ template <class C2> bool operator==(scoped_ptr<C2> const& p2) const;
+ template <class C2> bool operator!=(scoped_ptr<C2> const& p2) const;
+
+ // Disallow evil constructors
+ scoped_ptr(const scoped_ptr&);
+ void operator=(const scoped_ptr&);
+};
+
+// scoped_array<C> is like scoped_ptr<C>, except that the caller must allocate
+// with new [] and the destructor deletes objects with delete [].
+//
+// As with scoped_ptr<C>, a scoped_array<C> either points to an object
+// or is NULL. A scoped_array<C> owns the object that it points to.
+//
+// Size: sizeof(scoped_array<C>) == sizeof(C*)
+template <class C>
+class scoped_array {
+ public:
+
+ // The element type
+ typedef C element_type;
+
+ // Constructor. Defaults to intializing with NULL.
+ // There is no way to create an uninitialized scoped_array.
+ // The input parameter must be allocated with new [].
+ explicit scoped_array(C* p = NULL) : array_(p) { }
+
+ // Destructor. If there is a C object, delete it.
+ // We don't need to test ptr_ == NULL because C++ does that for us.
+ ~scoped_array() {
+ enum { type_must_be_complete = sizeof(C) };
+ delete[] array_;
+ }
+
+ // Reset. Deletes the current owned object, if any.
+ // Then takes ownership of a new object, if given.
+ // this->reset(this->get()) works.
+ void reset(C* p = NULL) {
+ if (p != array_) {
+ enum { type_must_be_complete = sizeof(C) };
+ delete[] array_;
+ array_ = p;
+ }
+ }
+
+ // Get one element of the current object.
+ // Will assert() if there is no current object, or index i is negative.
+ C& operator[](std::ptrdiff_t i) const {
+ assert(i >= 0);
+ assert(array_ != NULL);
+ return array_[i];
+ }
+
+ // Get a pointer to the zeroth element of the current object.
+ // If there is no current object, return NULL.
+ C* get() const {
+ return array_;
+ }
+
+ // Comparison operators.
+ // These return whether two scoped_array refer to the same object, not just to
+ // two different but equal objects.
+ bool operator==(C* p) const { return array_ == p; }
+ bool operator!=(C* p) const { return array_ != p; }
+
+ // Swap two scoped arrays.
+ void swap(scoped_array& p2) {
+ C* tmp = array_;
+ array_ = p2.array_;
+ p2.array_ = tmp;
+ }
+
+ // Release an array.
+ // The return value is the current pointer held by this object.
+ // If this object holds a NULL pointer, the return value is NULL.
+ // After this operation, this object will hold a NULL pointer,
+ // and will not own the object any more.
+ C* release() {
+ C* retVal = array_;
+ array_ = NULL;
+ return retVal;
+ }
+
+ private:
+ C* array_;
+
+ // Forbid comparison of different scoped_array types.
+ template <class C2> bool operator==(scoped_array<C2> const& p2) const;
+ template <class C2> bool operator!=(scoped_array<C2> const& p2) const;
+
+ // Disallow evil constructors
+ scoped_array(const scoped_array&);
+ void operator=(const scoped_array&);
+};
+
+} // namespace internal
+
+// We made these internal so that they would show up as such in the docs,
+// but we don't want to stick "internal::" in front of them everywhere.
+using internal::scoped_ptr;
+using internal::scoped_array;
+
+// ===================================================================
+// emulates google3/base/logging.h
+
+enum LogLevel {
+ LOGLEVEL_INFO, // Informational. This is never actually used by
+ // libprotobuf.
+ LOGLEVEL_WARNING, // Warns about issues that, although not technically a
+ // problem now, could cause problems in the future. For
+ // example, a // warning will be printed when parsing a
+ // message that is near the message size limit.
+ LOGLEVEL_ERROR, // An error occurred which should never happen during
+ // normal use.
+ LOGLEVEL_FATAL, // An error occurred from which the library cannot
+ // recover. This usually indicates a programming error
+ // in the code which calls the library, especially when
+ // compiled in debug mode.
+
+#ifdef NDEBUG
+ LOGLEVEL_DFATAL = LOGLEVEL_ERROR
+#else
+ LOGLEVEL_DFATAL = LOGLEVEL_FATAL
+#endif
+
+#ifdef ERROR
+ // ERROR is defined as 0 on some windows builds, so `GOOGLE_LOG(ERROR, ...)`
+ // expands into `GOOGLE_LOG(0, ...)` which then expands into
+ // `someGoogleLogging(LOGLEVEL_0, ...)`. This is not ideal, because the
+ // GOOGLE_LOG macro expects to expand itself into
+ // `someGoogleLogging(LOGLEVEL_ERROR, ...)` instead. The workaround to get
+ // everything building is to simply define LOGLEVEL_0 as LOGLEVEL_ERROR and
+ // move on with our lives.
+ , LOGLEVEL_0 = LOGLEVEL_ERROR
+#endif
+};
+
+namespace internal {
+
+class LogFinisher;
+
+class LIBPROTOBUF_EXPORT LogMessage {
+ public:
+ LogMessage(LogLevel level, const char* filename, int line);
+ ~LogMessage();
+
+ LogMessage& operator<<(const std::string& value);
+ LogMessage& operator<<(const char* value);
+ LogMessage& operator<<(char value);
+ LogMessage& operator<<(int value);
+ LogMessage& operator<<(uint value);
+ LogMessage& operator<<(long value);
+ LogMessage& operator<<(unsigned long value);
+ LogMessage& operator<<(double value);
+
+ private:
+ friend class LogFinisher;
+ void Finish();
+
+ LogLevel level_;
+ const char* filename_;
+ int line_;
+ std::string message_;
+};
+
+// Used to make the entire "LOG(BLAH) << etc." expression have a void return
+// type and print a newline after each message.
+class LIBPROTOBUF_EXPORT LogFinisher {
+ public:
+ void operator=(LogMessage& other);
+};
+
+} // namespace internal
+
+// Undef everything in case we're being mixed with some other Google library
+// which already defined them itself. Presumably all Google libraries will
+// support the same syntax for these so it should not be a big deal if they
+// end up using our definitions instead.
+#undef GOOGLE_LOG
+#undef GOOGLE_LOG_IF
+
+#undef GOOGLE_CHECK
+#undef GOOGLE_CHECK_OK
+#undef GOOGLE_CHECK_EQ
+#undef GOOGLE_CHECK_NE
+#undef GOOGLE_CHECK_LT
+#undef GOOGLE_CHECK_LE
+#undef GOOGLE_CHECK_GT
+#undef GOOGLE_CHECK_GE
+#undef GOOGLE_CHECK_NOTNULL
+
+#undef GOOGLE_DLOG
+#undef GOOGLE_DCHECK
+#undef GOOGLE_DCHECK_EQ
+#undef GOOGLE_DCHECK_NE
+#undef GOOGLE_DCHECK_LT
+#undef GOOGLE_DCHECK_LE
+#undef GOOGLE_DCHECK_GT
+#undef GOOGLE_DCHECK_GE
+
+#define GOOGLE_LOG(LEVEL) \
+ ::google::protobuf::internal::LogFinisher() = \
+ ::google::protobuf::internal::LogMessage( \
+ ::google::protobuf::LOGLEVEL_##LEVEL, __FILE__, __LINE__)
+#define GOOGLE_LOG_IF(LEVEL, CONDITION) \
+ !(CONDITION) ? (void)0 : GOOGLE_LOG(LEVEL)
+
+#define GOOGLE_CHECK(EXPRESSION) \
+ GOOGLE_LOG_IF(FATAL, !(EXPRESSION)) << "CHECK failed: " #EXPRESSION ": "
+#define GOOGLE_CHECK_OK(A) GOOGLE_CHECK(A)
+#define GOOGLE_CHECK_EQ(A, B) GOOGLE_CHECK((A) == (B))
+#define GOOGLE_CHECK_NE(A, B) GOOGLE_CHECK((A) != (B))
+#define GOOGLE_CHECK_LT(A, B) GOOGLE_CHECK((A) < (B))
+#define GOOGLE_CHECK_LE(A, B) GOOGLE_CHECK((A) <= (B))
+#define GOOGLE_CHECK_GT(A, B) GOOGLE_CHECK((A) > (B))
+#define GOOGLE_CHECK_GE(A, B) GOOGLE_CHECK((A) >= (B))
+
+namespace internal {
+template<typename T>
+T* CheckNotNull(const char* /* file */, int /* line */,
+ const char* name, T* val) {
+ if (val == NULL) {
+ GOOGLE_LOG(FATAL) << name;
+ }
+ return val;
+}
+} // namespace internal
+#define GOOGLE_CHECK_NOTNULL(A) \
+ internal::CheckNotNull(__FILE__, __LINE__, "'" #A "' must not be NULL", (A))
+
+#ifdef NDEBUG
+
+#define GOOGLE_DLOG GOOGLE_LOG_IF(INFO, false)
+
+#define GOOGLE_DCHECK(EXPRESSION) while(false) GOOGLE_CHECK(EXPRESSION)
+#define GOOGLE_DCHECK_EQ(A, B) GOOGLE_DCHECK((A) == (B))
+#define GOOGLE_DCHECK_NE(A, B) GOOGLE_DCHECK((A) != (B))
+#define GOOGLE_DCHECK_LT(A, B) GOOGLE_DCHECK((A) < (B))
+#define GOOGLE_DCHECK_LE(A, B) GOOGLE_DCHECK((A) <= (B))
+#define GOOGLE_DCHECK_GT(A, B) GOOGLE_DCHECK((A) > (B))
+#define GOOGLE_DCHECK_GE(A, B) GOOGLE_DCHECK((A) >= (B))
+
+#else // NDEBUG
+
+#define GOOGLE_DLOG GOOGLE_LOG
+
+#define GOOGLE_DCHECK GOOGLE_CHECK
+#define GOOGLE_DCHECK_EQ GOOGLE_CHECK_EQ
+#define GOOGLE_DCHECK_NE GOOGLE_CHECK_NE
+#define GOOGLE_DCHECK_LT GOOGLE_CHECK_LT
+#define GOOGLE_DCHECK_LE GOOGLE_CHECK_LE
+#define GOOGLE_DCHECK_GT GOOGLE_CHECK_GT
+#define GOOGLE_DCHECK_GE GOOGLE_CHECK_GE
+
+#endif // !NDEBUG
+
+typedef void LogHandler(LogLevel level, const char* filename, int line,
+ const std::string& message);
+
+// The protobuf library sometimes writes warning and error messages to
+// stderr. These messages are primarily useful for developers, but may
+// also help end users figure out a problem. If you would prefer that
+// these messages be sent somewhere other than stderr, call SetLogHandler()
+// to set your own handler. This returns the old handler. Set the handler
+// to NULL to ignore log messages (but see also LogSilencer, below).
+//
+// Obviously, SetLogHandler is not thread-safe. You should only call it
+// at initialization time, and probably not from library code. If you
+// simply want to suppress log messages temporarily (e.g. because you
+// have some code that tends to trigger them frequently and you know
+// the warnings are not important to you), use the LogSilencer class
+// below.
+LIBPROTOBUF_EXPORT LogHandler* SetLogHandler(LogHandler* new_func);
+
+// Create a LogSilencer if you want to temporarily suppress all log
+// messages. As long as any LogSilencer objects exist, non-fatal
+// log messages will be discarded (the current LogHandler will *not*
+// be called). Constructing a LogSilencer is thread-safe. You may
+// accidentally suppress log messages occurring in another thread, but
+// since messages are generally for debugging purposes only, this isn't
+// a big deal. If you want to intercept log messages, use SetLogHandler().
+class LIBPROTOBUF_EXPORT LogSilencer {
+ public:
+ LogSilencer();
+ ~LogSilencer();
+};
+
+// ===================================================================
+// emulates google3/base/callback.h
+
+// Abstract interface for a callback. When calling an RPC, you must provide
+// a Closure to call when the procedure completes. See the Service interface
+// in service.h.
+//
+// To automatically construct a Closure which calls a particular function or
+// method with a particular set of parameters, use the NewCallback() function.
+// Example:
+// void FooDone(const FooResponse* response) {
+// ...
+// }
+//
+// void CallFoo() {
+// ...
+// // When done, call FooDone() and pass it a pointer to the response.
+// Closure* callback = NewCallback(&FooDone, response);
+// // Make the call.
+// service->Foo(controller, request, response, callback);
+// }
+//
+// Example that calls a method:
+// class Handler {
+// public:
+// ...
+//
+// void FooDone(const FooResponse* response) {
+// ...
+// }
+//
+// void CallFoo() {
+// ...
+// // When done, call FooDone() and pass it a pointer to the response.
+// Closure* callback = NewCallback(this, &Handler::FooDone, response);
+// // Make the call.
+// service->Foo(controller, request, response, callback);
+// }
+// };
+//
+// Currently NewCallback() supports binding zero, one, or two arguments.
+//
+// Callbacks created with NewCallback() automatically delete themselves when
+// executed. They should be used when a callback is to be called exactly
+// once (usually the case with RPC callbacks). If a callback may be called
+// a different number of times (including zero), create it with
+// NewPermanentCallback() instead. You are then responsible for deleting the
+// callback (using the "delete" keyword as normal).
+//
+// Note that NewCallback() is a bit touchy regarding argument types. Generally,
+// the values you provide for the parameter bindings must exactly match the
+// types accepted by the callback function. For example:
+// void Foo(string s);
+// NewCallback(&Foo, "foo"); // WON'T WORK: const char* != string
+// NewCallback(&Foo, string("foo")); // WORKS
+// Also note that the arguments cannot be references:
+// void Foo(const string& s);
+// string my_str;
+// NewCallback(&Foo, my_str); // WON'T WORK: Can't use referecnes.
+// However, correctly-typed pointers will work just fine.
+class LIBPROTOBUF_EXPORT Closure {
+ public:
+ Closure() {}
+ virtual ~Closure();
+
+ virtual void Run() = 0;
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Closure);
+};
+
+namespace internal {
+
+class LIBPROTOBUF_EXPORT FunctionClosure0 : public Closure {
+ public:
+ typedef void (*FunctionType)();
+
+ FunctionClosure0(FunctionType function, bool self_deleting)
+ : function_(function), self_deleting_(self_deleting) {}
+ ~FunctionClosure0();
+
+ void Run() {
+ bool needs_delete = self_deleting_; // read in case callback deletes
+ function_();
+ if (needs_delete) delete this;
+ }
+
+ private:
+ FunctionType function_;
+ bool self_deleting_;
+};
+
+template <typename Class>
+class MethodClosure0 : public Closure {
+ public:
+ typedef void (Class::*MethodType)();
+
+ MethodClosure0(Class* object, MethodType method, bool self_deleting)
+ : object_(object), method_(method), self_deleting_(self_deleting) {}
+ ~MethodClosure0() {}
+
+ void Run() {
+ bool needs_delete = self_deleting_; // read in case callback deletes
+ (object_->*method_)();
+ if (needs_delete) delete this;
+ }
+
+ private:
+ Class* object_;
+ MethodType method_;
+ bool self_deleting_;
+};
+
+template <typename Arg1>
+class FunctionClosure1 : public Closure {
+ public:
+ typedef void (*FunctionType)(Arg1 arg1);
+
+ FunctionClosure1(FunctionType function, bool self_deleting,
+ Arg1 arg1)
+ : function_(function), self_deleting_(self_deleting),
+ arg1_(arg1) {}
+ ~FunctionClosure1() {}
+
+ void Run() {
+ bool needs_delete = self_deleting_; // read in case callback deletes
+ function_(arg1_);
+ if (needs_delete) delete this;
+ }
+
+ private:
+ FunctionType function_;
+ bool self_deleting_;
+ Arg1 arg1_;
+};
+
+template <typename Class, typename Arg1>
+class MethodClosure1 : public Closure {
+ public:
+ typedef void (Class::*MethodType)(Arg1 arg1);
+
+ MethodClosure1(Class* object, MethodType method, bool self_deleting,
+ Arg1 arg1)
+ : object_(object), method_(method), self_deleting_(self_deleting),
+ arg1_(arg1) {}
+ ~MethodClosure1() {}
+
+ void Run() {
+ bool needs_delete = self_deleting_; // read in case callback deletes
+ (object_->*method_)(arg1_);
+ if (needs_delete) delete this;
+ }
+
+ private:
+ Class* object_;
+ MethodType method_;
+ bool self_deleting_;
+ Arg1 arg1_;
+};
+
+template <typename Arg1, typename Arg2>
+class FunctionClosure2 : public Closure {
+ public:
+ typedef void (*FunctionType)(Arg1 arg1, Arg2 arg2);
+
+ FunctionClosure2(FunctionType function, bool self_deleting,
+ Arg1 arg1, Arg2 arg2)
+ : function_(function), self_deleting_(self_deleting),
+ arg1_(arg1), arg2_(arg2) {}
+ ~FunctionClosure2() {}
+
+ void Run() {
+ bool needs_delete = self_deleting_; // read in case callback deletes
+ function_(arg1_, arg2_);
+ if (needs_delete) delete this;
+ }
+
+ private:
+ FunctionType function_;
+ bool self_deleting_;
+ Arg1 arg1_;
+ Arg2 arg2_;
+};
+
+template <typename Class, typename Arg1, typename Arg2>
+class MethodClosure2 : public Closure {
+ public:
+ typedef void (Class::*MethodType)(Arg1 arg1, Arg2 arg2);
+
+ MethodClosure2(Class* object, MethodType method, bool self_deleting,
+ Arg1 arg1, Arg2 arg2)
+ : object_(object), method_(method), self_deleting_(self_deleting),
+ arg1_(arg1), arg2_(arg2) {}
+ ~MethodClosure2() {}
+
+ void Run() {
+ bool needs_delete = self_deleting_; // read in case callback deletes
+ (object_->*method_)(arg1_, arg2_);
+ if (needs_delete) delete this;
+ }
+
+ private:
+ Class* object_;
+ MethodType method_;
+ bool self_deleting_;
+ Arg1 arg1_;
+ Arg2 arg2_;
+};
+
+} // namespace internal
+
+// See Closure.
+inline Closure* NewCallback(void (*function)()) {
+ return new internal::FunctionClosure0(function, true);
+}
+
+// See Closure.
+inline Closure* NewPermanentCallback(void (*function)()) {
+ return new internal::FunctionClosure0(function, false);
+}
+
+// See Closure.
+template <typename Class>
+inline Closure* NewCallback(Class* object, void (Class::*method)()) {
+ return new internal::MethodClosure0<Class>(object, method, true);
+}
+
+// See Closure.
+template <typename Class>
+inline Closure* NewPermanentCallback(Class* object, void (Class::*method)()) {
+ return new internal::MethodClosure0<Class>(object, method, false);
+}
+
+// See Closure.
+template <typename Arg1>
+inline Closure* NewCallback(void (*function)(Arg1),
+ Arg1 arg1) {
+ return new internal::FunctionClosure1<Arg1>(function, true, arg1);
+}
+
+// See Closure.
+template <typename Arg1>
+inline Closure* NewPermanentCallback(void (*function)(Arg1),
+ Arg1 arg1) {
+ return new internal::FunctionClosure1<Arg1>(function, false, arg1);
+}
+
+// See Closure.
+template <typename Class, typename Arg1>
+inline Closure* NewCallback(Class* object, void (Class::*method)(Arg1),
+ Arg1 arg1) {
+ return new internal::MethodClosure1<Class, Arg1>(object, method, true, arg1);
+}
+
+// See Closure.
+template <typename Class, typename Arg1>
+inline Closure* NewPermanentCallback(Class* object, void (Class::*method)(Arg1),
+ Arg1 arg1) {
+ return new internal::MethodClosure1<Class, Arg1>(object, method, false, arg1);
+}
+
+// See Closure.
+template <typename Arg1, typename Arg2>
+inline Closure* NewCallback(void (*function)(Arg1, Arg2),
+ Arg1 arg1, Arg2 arg2) {
+ return new internal::FunctionClosure2<Arg1, Arg2>(
+ function, true, arg1, arg2);
+}
+
+// See Closure.
+template <typename Arg1, typename Arg2>
+inline Closure* NewPermanentCallback(void (*function)(Arg1, Arg2),
+ Arg1 arg1, Arg2 arg2) {
+ return new internal::FunctionClosure2<Arg1, Arg2>(
+ function, false, arg1, arg2);
+}
+
+// See Closure.
+template <typename Class, typename Arg1, typename Arg2>
+inline Closure* NewCallback(Class* object, void (Class::*method)(Arg1, Arg2),
+ Arg1 arg1, Arg2 arg2) {
+ return new internal::MethodClosure2<Class, Arg1, Arg2>(
+ object, method, true, arg1, arg2);
+}
+
+// See Closure.
+template <typename Class, typename Arg1, typename Arg2>
+inline Closure* NewPermanentCallback(
+ Class* object, void (Class::*method)(Arg1, Arg2),
+ Arg1 arg1, Arg2 arg2) {
+ return new internal::MethodClosure2<Class, Arg1, Arg2>(
+ object, method, false, arg1, arg2);
+}
+
+// A function which does nothing. Useful for creating no-op callbacks, e.g.:
+// Closure* nothing = NewCallback(&DoNothing);
+void LIBPROTOBUF_EXPORT DoNothing();
+
+// ===================================================================
+// emulates google3/base/mutex.h
+
+namespace internal {
+
+// A Mutex is a non-reentrant (aka non-recursive) mutex. At most one thread T
+// may hold a mutex at a given time. If T attempts to Lock() the same Mutex
+// while holding it, T will deadlock.
+class LIBPROTOBUF_EXPORT Mutex {
+ public:
+ // Create a Mutex that is not held by anybody.
+ Mutex();
+
+ // Destructor
+ ~Mutex();
+
+ // Block if necessary until this Mutex is free, then acquire it exclusively.
+ void Lock();
+
+ // Release this Mutex. Caller must hold it exclusively.
+ void Unlock();
+
+ // Crash if this Mutex is not held exclusively by this thread.
+ // May fail to crash when it should; will never crash when it should not.
+ void AssertHeld();
+
+ private:
+ struct Internal;
+ Internal* mInternal;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Mutex);
+};
+
+// MutexLock(mu) acquires mu when constructed and releases it when destroyed.
+class LIBPROTOBUF_EXPORT MutexLock {
+ public:
+ explicit MutexLock(Mutex *mu) : mu_(mu) { this->mu_->Lock(); }
+ ~MutexLock() { this->mu_->Unlock(); }
+ private:
+ Mutex *const mu_;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MutexLock);
+};
+
+// TODO(kenton): Implement these? Hard to implement portably.
+typedef MutexLock ReaderMutexLock;
+typedef MutexLock WriterMutexLock;
+
+// MutexLockMaybe is like MutexLock, but is a no-op when mu is NULL.
+class LIBPROTOBUF_EXPORT MutexLockMaybe {
+ public:
+ explicit MutexLockMaybe(Mutex *mu) :
+ mu_(mu) { if (this->mu_ != NULL) { this->mu_->Lock(); } }
+ ~MutexLockMaybe() { if (this->mu_ != NULL) { this->mu_->Unlock(); } }
+ private:
+ Mutex *const mu_;
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MutexLockMaybe);
+};
+
+} // namespace internal
+
+// We made these internal so that they would show up as such in the docs,
+// but we don't want to stick "internal::" in front of them everywhere.
+using internal::Mutex;
+using internal::MutexLock;
+using internal::ReaderMutexLock;
+using internal::WriterMutexLock;
+using internal::MutexLockMaybe;
+
+// ===================================================================
+// from google3/util/utf8/public/unilib.h
+
+namespace internal {
+
+// Checks if the buffer contains structurally-valid UTF-8. Implemented in
+// structurally_valid.cc.
+LIBPROTOBUF_EXPORT bool IsStructurallyValidUTF8(const char* buf, int len);
+
+} // namespace internal
+
+// ===================================================================
+// from google3/util/endian/endian.h
+LIBPROTOBUF_EXPORT uint32 ghtonl(uint32 x);
+
+// ===================================================================
+// Shutdown support.
+
+// Shut down the entire protocol buffers library, deleting all static-duration
+// objects allocated by the library or by generated .pb.cc files.
+//
+// There are two reasons you might want to call this:
+// * You use a draconian definition of "memory leak" in which you expect
+// every single malloc() to have a corresponding free(), even for objects
+// which live until program exit.
+// * You are writing a dynamically-loaded library which needs to clean up
+// after itself when the library is unloaded.
+//
+// It is safe to call this multiple times. However, it is not safe to use
+// any other part of the protocol buffers library after
+// ShutdownProtobufLibrary() has been called.
+LIBPROTOBUF_EXPORT void ShutdownProtobufLibrary();
+
+namespace internal {
+
+// Register a function to be called when ShutdownProtocolBuffers() is called.
+LIBPROTOBUF_EXPORT void OnShutdown(void (*func)());
+
+} // namespace internal
+
+#if PROTOBUF_USE_EXCEPTIONS
+class FatalException : public std::exception {
+ public:
+ FatalException(const char* filename, int line, const std::string& message)
+ : filename_(filename), line_(line), message_(message) {}
+ virtual ~FatalException() throw();
+
+ virtual const char* what() const throw();
+
+ const char* filename() const { return filename_; }
+ int line() const { return line_; }
+ const std::string& message() const { return message_; }
+
+ private:
+ const char* filename_;
+ const int line_;
+ const std::string message_;
+};
+#endif
+
+// This is at the end of the file instead of the beginning to work around a bug
+// in some versions of MSVC.
+using namespace std; // Don't do this at home, kids.
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_COMMON_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/hash.h b/toolkit/components/protobuf/src/google/protobuf/stubs/hash.h
new file mode 100644
index 0000000000..3640b8982d
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/hash.h
@@ -0,0 +1,231 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+//
+// Deals with the fact that hash_map is not defined everywhere.
+
+#ifndef GOOGLE_PROTOBUF_STUBS_HASH_H__
+#define GOOGLE_PROTOBUF_STUBS_HASH_H__
+
+#include <string.h>
+#include <google/protobuf/stubs/common.h>
+
+#if defined(HAVE_HASH_MAP) && defined(HAVE_HASH_SET)
+#include HASH_MAP_H
+#include HASH_SET_H
+#else
+#define MISSING_HASH
+#include <map>
+#include <set>
+#endif
+
+namespace google {
+namespace protobuf {
+
+#ifdef MISSING_HASH
+
+// This system doesn't have hash_map or hash_set. Emulate them using map and
+// set.
+
+// Make hash<T> be the same as less<T>. Note that everywhere where custom
+// hash functions are defined in the protobuf code, they are also defined such
+// that they can be used as "less" functions, which is required by MSVC anyway.
+template <typename Key>
+struct hash {
+ // Dummy, just to make derivative hash functions compile.
+ int operator()(const Key& key) {
+ GOOGLE_LOG(FATAL) << "Should never be called.";
+ return 0;
+ }
+
+ inline bool operator()(const Key& a, const Key& b) const {
+ return a < b;
+ }
+};
+
+// Make sure char* is compared by value.
+template <>
+struct hash<const char*> {
+ // Dummy, just to make derivative hash functions compile.
+ int operator()(const char* key) {
+ GOOGLE_LOG(FATAL) << "Should never be called.";
+ return 0;
+ }
+
+ inline bool operator()(const char* a, const char* b) const {
+ return strcmp(a, b) < 0;
+ }
+};
+
+template <typename Key, typename Data,
+ typename HashFcn = hash<Key>,
+ typename EqualKey = int >
+class hash_map : public std::map<Key, Data, HashFcn> {
+ public:
+ hash_map(int = 0) {}
+};
+
+template <typename Key,
+ typename HashFcn = hash<Key>,
+ typename EqualKey = int >
+class hash_set : public std::set<Key, HashFcn> {
+ public:
+ hash_set(int = 0) {}
+};
+
+#elif defined(_MSC_VER) && !defined(_STLPORT_VERSION)
+
+template <typename Key>
+struct hash : public HASH_NAMESPACE::hash_compare<Key> {
+};
+
+// MSVC's hash_compare<const char*> hashes based on the string contents but
+// compares based on the string pointer. WTF?
+class CstringLess {
+ public:
+ inline bool operator()(const char* a, const char* b) const {
+ return strcmp(a, b) < 0;
+ }
+};
+
+template <>
+struct hash<const char*>
+ : public HASH_NAMESPACE::hash_compare<const char*, CstringLess> {
+};
+
+template <typename Key, typename Data,
+ typename HashFcn = hash<Key>,
+ typename EqualKey = int >
+class hash_map : public HASH_NAMESPACE::hash_map<
+ Key, Data, HashFcn> {
+ public:
+ hash_map(int = 0) {}
+};
+
+template <typename Key,
+ typename HashFcn = hash<Key>,
+ typename EqualKey = int >
+class hash_set : public HASH_NAMESPACE::hash_set<
+ Key, HashFcn> {
+ public:
+ hash_set(int = 0) {}
+};
+
+#else
+
+template <typename Key>
+struct hash : public HASH_NAMESPACE::hash<Key> {
+};
+
+template <typename Key>
+struct hash<const Key*> {
+ inline size_t operator()(const Key* key) const {
+ return reinterpret_cast<size_t>(key);
+ }
+};
+
+// Unlike the old SGI version, the TR1 "hash" does not special-case char*. So,
+// we go ahead and provide our own implementation.
+template <>
+struct hash<const char*> {
+ inline size_t operator()(const char* str) const {
+ size_t result = 0;
+ for (; *str != '\0'; str++) {
+ result = 5 * result + *str;
+ }
+ return result;
+ }
+};
+
+template <typename Key, typename Data,
+ typename HashFcn = hash<Key>,
+ typename EqualKey = std::equal_to<Key> >
+class hash_map : public HASH_NAMESPACE::HASH_MAP_CLASS<
+ Key, Data, HashFcn, EqualKey> {
+ public:
+ hash_map(int = 0) {}
+};
+
+template <typename Key,
+ typename HashFcn = hash<Key>,
+ typename EqualKey = std::equal_to<Key> >
+class hash_set : public HASH_NAMESPACE::HASH_SET_CLASS<
+ Key, HashFcn, EqualKey> {
+ public:
+ hash_set(int = 0) {}
+};
+
+#endif
+
+template <>
+struct hash<string> {
+ inline size_t operator()(const string& key) const {
+ return hash<const char*>()(key.c_str());
+ }
+
+ static const size_t bucket_size = 4;
+ static const size_t min_buckets = 8;
+ inline size_t operator()(const string& a, const string& b) const {
+ return a < b;
+ }
+};
+
+template <typename First, typename Second>
+struct hash<pair<First, Second> > {
+ inline size_t operator()(const pair<First, Second>& key) const {
+ size_t first_hash = hash<First>()(key.first);
+ size_t second_hash = hash<Second>()(key.second);
+
+ // FIXME(kenton): What is the best way to compute this hash? I have
+ // no idea! This seems a bit better than an XOR.
+ return first_hash * ((1 << 16) - 1) + second_hash;
+ }
+
+ static const size_t bucket_size = 4;
+ static const size_t min_buckets = 8;
+ inline size_t operator()(const pair<First, Second>& a,
+ const pair<First, Second>& b) const {
+ return a < b;
+ }
+};
+
+// Used by GCC/SGI STL only. (Why isn't this provided by the standard
+// library? :( )
+struct streq {
+ inline bool operator()(const char* a, const char* b) const {
+ return strcmp(a, b) == 0;
+ }
+};
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_HASH_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/map_util.h b/toolkit/components/protobuf/src/google/protobuf/stubs/map_util.h
new file mode 100644
index 0000000000..7495cb6aec
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/map_util.h
@@ -0,0 +1,771 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2014 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/util/gtl/map_util.h
+// Author: Anton Carver
+
+#ifndef GOOGLE_PROTOBUF_STUBS_MAP_UTIL_H__
+#define GOOGLE_PROTOBUF_STUBS_MAP_UTIL_H__
+
+#include <stddef.h>
+#include <iterator>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+// Local implementation of RemoveConst to avoid including base/type_traits.h.
+template <class T> struct RemoveConst { typedef T type; };
+template <class T> struct RemoveConst<const T> : RemoveConst<T> {};
+} // namespace internal
+
+//
+// Find*()
+//
+
+// Returns a const reference to the value associated with the given key if it
+// exists. Crashes otherwise.
+//
+// This is intended as a replacement for operator[] as an rvalue (for reading)
+// when the key is guaranteed to exist.
+//
+// operator[] for lookup is discouraged for several reasons:
+// * It has a side-effect of inserting missing keys
+// * It is not thread-safe (even when it is not inserting, it can still
+// choose to resize the underlying storage)
+// * It invalidates iterators (when it chooses to resize)
+// * It default constructs a value object even if it doesn't need to
+//
+// This version assumes the key is printable, and includes it in the fatal log
+// message.
+template <class Collection>
+const typename Collection::value_type::second_type&
+FindOrDie(const Collection& collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::const_iterator it = collection.find(key);
+ GOOGLE_CHECK(it != collection.end()) << "Map key not found: " << key;
+ return it->second;
+}
+
+// Same as above, but returns a non-const reference.
+template <class Collection>
+typename Collection::value_type::second_type&
+FindOrDie(Collection& collection, // NOLINT
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::iterator it = collection.find(key);
+ GOOGLE_CHECK(it != collection.end()) << "Map key not found: " << key;
+ return it->second;
+}
+
+// Same as FindOrDie above, but doesn't log the key on failure.
+template <class Collection>
+const typename Collection::value_type::second_type&
+FindOrDieNoPrint(const Collection& collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::const_iterator it = collection.find(key);
+ GOOGLE_CHECK(it != collection.end()) << "Map key not found";
+ return it->second;
+}
+
+// Same as above, but returns a non-const reference.
+template <class Collection>
+typename Collection::value_type::second_type&
+FindOrDieNoPrint(Collection& collection, // NOLINT
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::iterator it = collection.find(key);
+ GOOGLE_CHECK(it != collection.end()) << "Map key not found";
+ return it->second;
+}
+
+// Returns a const reference to the value associated with the given key if it
+// exists, otherwise returns a const reference to the provided default value.
+//
+// WARNING: If a temporary object is passed as the default "value,"
+// this function will return a reference to that temporary object,
+// which will be destroyed at the end of the statement. A common
+// example: if you have a map with string values, and you pass a char*
+// as the default "value," either use the returned value immediately
+// or store it in a string (not string&).
+// Details: http://go/findwithdefault
+template <class Collection>
+const typename Collection::value_type::second_type&
+FindWithDefault(const Collection& collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& value) {
+ typename Collection::const_iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return value;
+ }
+ return it->second;
+}
+
+// Returns a pointer to the const value associated with the given key if it
+// exists, or NULL otherwise.
+template <class Collection>
+const typename Collection::value_type::second_type*
+FindOrNull(const Collection& collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::const_iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return 0;
+ }
+ return &it->second;
+}
+
+// Same as above but returns a pointer to the non-const value.
+template <class Collection>
+typename Collection::value_type::second_type*
+FindOrNull(Collection& collection, // NOLINT
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return 0;
+ }
+ return &it->second;
+}
+
+// Returns the pointer value associated with the given key. If none is found,
+// NULL is returned. The function is designed to be used with a map of keys to
+// pointers.
+//
+// This function does not distinguish between a missing key and a key mapped
+// to a NULL value.
+template <class Collection>
+typename Collection::value_type::second_type
+FindPtrOrNull(const Collection& collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::const_iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return typename Collection::value_type::second_type();
+ }
+ return it->second;
+}
+
+// Same as above, except takes non-const reference to collection.
+//
+// This function is needed for containers that propagate constness to the
+// pointee, such as boost::ptr_map.
+template <class Collection>
+typename Collection::value_type::second_type
+FindPtrOrNull(Collection& collection, // NOLINT
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return typename Collection::value_type::second_type();
+ }
+ return it->second;
+}
+
+// Finds the pointer value associated with the given key in a map whose values
+// are linked_ptrs. Returns NULL if key is not found.
+template <class Collection>
+typename Collection::value_type::second_type::element_type*
+FindLinkedPtrOrNull(const Collection& collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::const_iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return 0;
+ }
+ // Since linked_ptr::get() is a const member returning a non const,
+ // we do not need a version of this function taking a non const collection.
+ return it->second.get();
+}
+
+// Same as above, but dies if the key is not found.
+template <class Collection>
+typename Collection::value_type::second_type::element_type&
+FindLinkedPtrOrDie(const Collection& collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::const_iterator it = collection.find(key);
+ CHECK(it != collection.end()) << "key not found: " << key;
+ // Since linked_ptr::operator*() is a const member returning a non const,
+ // we do not need a version of this function taking a non const collection.
+ return *it->second;
+}
+
+// Finds the value associated with the given key and copies it to *value (if not
+// NULL). Returns false if the key was not found, true otherwise.
+template <class Collection, class Key, class Value>
+bool FindCopy(const Collection& collection,
+ const Key& key,
+ Value* const value) {
+ typename Collection::const_iterator it = collection.find(key);
+ if (it == collection.end()) {
+ return false;
+ }
+ if (value) {
+ *value = it->second;
+ }
+ return true;
+}
+
+//
+// Contains*()
+//
+
+// Returns true if and only if the given collection contains the given key.
+template <class Collection, class Key>
+bool ContainsKey(const Collection& collection, const Key& key) {
+ return collection.find(key) != collection.end();
+}
+
+// Returns true if and only if the given collection contains the given key-value
+// pair.
+template <class Collection, class Key, class Value>
+bool ContainsKeyValuePair(const Collection& collection,
+ const Key& key,
+ const Value& value) {
+ typedef typename Collection::const_iterator const_iterator;
+ std::pair<const_iterator, const_iterator> range = collection.equal_range(key);
+ for (const_iterator it = range.first; it != range.second; ++it) {
+ if (it->second == value) {
+ return true;
+ }
+ }
+ return false;
+}
+
+//
+// Insert*()
+//
+
+// Inserts the given key-value pair into the collection. Returns true if and
+// only if the key from the given pair didn't previously exist. Otherwise, the
+// value in the map is replaced with the value from the given pair.
+template <class Collection>
+bool InsertOrUpdate(Collection* const collection,
+ const typename Collection::value_type& vt) {
+ std::pair<typename Collection::iterator, bool> ret = collection->insert(vt);
+ if (!ret.second) {
+ // update
+ ret.first->second = vt.second;
+ return false;
+ }
+ return true;
+}
+
+// Same as above, except that the key and value are passed separately.
+template <class Collection>
+bool InsertOrUpdate(Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& value) {
+ return InsertOrUpdate(
+ collection, typename Collection::value_type(key, value));
+}
+
+// Inserts/updates all the key-value pairs from the range defined by the
+// iterators "first" and "last" into the given collection.
+template <class Collection, class InputIterator>
+void InsertOrUpdateMany(Collection* const collection,
+ InputIterator first, InputIterator last) {
+ for (; first != last; ++first) {
+ InsertOrUpdate(collection, *first);
+ }
+}
+
+// Change the value associated with a particular key in a map or hash_map
+// of the form map<Key, Value*> which owns the objects pointed to by the
+// value pointers. If there was an existing value for the key, it is deleted.
+// True indicates an insert took place, false indicates an update + delete.
+template <class Collection>
+bool InsertAndDeleteExisting(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& value) {
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(key, value));
+ if (!ret.second) {
+ delete ret.first->second;
+ ret.first->second = value;
+ return false;
+ }
+ return true;
+}
+
+// Inserts the given key and value into the given collection if and only if the
+// given key did NOT already exist in the collection. If the key previously
+// existed in the collection, the value is not changed. Returns true if the
+// key-value pair was inserted; returns false if the key was already present.
+template <class Collection>
+bool InsertIfNotPresent(Collection* const collection,
+ const typename Collection::value_type& vt) {
+ return collection->insert(vt).second;
+}
+
+// Same as above except the key and value are passed separately.
+template <class Collection>
+bool InsertIfNotPresent(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& value) {
+ return InsertIfNotPresent(
+ collection, typename Collection::value_type(key, value));
+}
+
+// Same as above except dies if the key already exists in the collection.
+template <class Collection>
+void InsertOrDie(Collection* const collection,
+ const typename Collection::value_type& value) {
+ CHECK(InsertIfNotPresent(collection, value)) << "duplicate value: " << value;
+}
+
+// Same as above except doesn't log the value on error.
+template <class Collection>
+void InsertOrDieNoPrint(Collection* const collection,
+ const typename Collection::value_type& value) {
+ CHECK(InsertIfNotPresent(collection, value)) << "duplicate value.";
+}
+
+// Inserts the key-value pair into the collection. Dies if key was already
+// present.
+template <class Collection>
+void InsertOrDie(Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& data) {
+ typedef typename Collection::value_type value_type;
+ GOOGLE_CHECK(InsertIfNotPresent(collection, key, data))
+ << "duplicate key: " << key;
+}
+
+// Same as above except doesn't log the key on error.
+template <class Collection>
+void InsertOrDieNoPrint(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& data) {
+ typedef typename Collection::value_type value_type;
+ GOOGLE_CHECK(InsertIfNotPresent(collection, key, data)) << "duplicate key.";
+}
+
+// Inserts a new key and default-initialized value. Dies if the key was already
+// present. Returns a reference to the value. Example usage:
+//
+// map<int, SomeProto> m;
+// SomeProto& proto = InsertKeyOrDie(&m, 3);
+// proto.set_field("foo");
+template <class Collection>
+typename Collection::value_type::second_type& InsertKeyOrDie(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key) {
+ typedef typename Collection::value_type value_type;
+ std::pair<typename Collection::iterator, bool> res =
+ collection->insert(value_type(key, typename value_type::second_type()));
+ GOOGLE_CHECK(res.second) << "duplicate key: " << key;
+ return res.first->second;
+}
+
+//
+// Lookup*()
+//
+
+// Looks up a given key and value pair in a collection and inserts the key-value
+// pair if it's not already present. Returns a reference to the value associated
+// with the key.
+template <class Collection>
+typename Collection::value_type::second_type&
+LookupOrInsert(Collection* const collection,
+ const typename Collection::value_type& vt) {
+ return collection->insert(vt).first->second;
+}
+
+// Same as above except the key-value are passed separately.
+template <class Collection>
+typename Collection::value_type::second_type&
+LookupOrInsert(Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& value) {
+ return LookupOrInsert(
+ collection, typename Collection::value_type(key, value));
+}
+
+// Counts the number of equivalent elements in the given "sequence", and stores
+// the results in "count_map" with element as the key and count as the value.
+//
+// Example:
+// vector<string> v = {"a", "b", "c", "a", "b"};
+// map<string, int> m;
+// AddTokenCounts(v, 1, &m);
+// assert(m["a"] == 2);
+// assert(m["b"] == 2);
+// assert(m["c"] == 1);
+template <typename Sequence, typename Collection>
+void AddTokenCounts(
+ const Sequence& sequence,
+ const typename Collection::value_type::second_type& increment,
+ Collection* const count_map) {
+ for (typename Sequence::const_iterator it = sequence.begin();
+ it != sequence.end(); ++it) {
+ typename Collection::value_type::second_type& value =
+ LookupOrInsert(count_map, *it,
+ typename Collection::value_type::second_type());
+ value += increment;
+ }
+}
+
+// Returns a reference to the value associated with key. If not found, a value
+// is default constructed on the heap and added to the map.
+//
+// This function is useful for containers of the form map<Key, Value*>, where
+// inserting a new key, value pair involves constructing a new heap-allocated
+// Value, and storing a pointer to that in the collection.
+template <class Collection>
+typename Collection::value_type::second_type&
+LookupOrInsertNew(Collection* const collection,
+ const typename Collection::value_type::first_type& key) {
+ typedef typename std::iterator_traits<
+ typename Collection::value_type::second_type>::value_type Element;
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(
+ key,
+ static_cast<typename Collection::value_type::second_type>(NULL)));
+ if (ret.second) {
+ ret.first->second = new Element();
+ }
+ return ret.first->second;
+}
+
+// Same as above but constructs the value using the single-argument constructor
+// and the given "arg".
+template <class Collection, class Arg>
+typename Collection::value_type::second_type&
+LookupOrInsertNew(Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const Arg& arg) {
+ typedef typename std::iterator_traits<
+ typename Collection::value_type::second_type>::value_type Element;
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(
+ key,
+ static_cast<typename Collection::value_type::second_type>(NULL)));
+ if (ret.second) {
+ ret.first->second = new Element(arg);
+ }
+ return ret.first->second;
+}
+
+// Lookup of linked/shared pointers is used in two scenarios:
+//
+// Use LookupOrInsertNewLinkedPtr if the container owns the elements.
+// In this case it is fine working with the raw pointer as long as it is
+// guaranteed that no other thread can delete/update an accessed element.
+// A mutex will need to lock the container operation as well as the use
+// of the returned elements. Finding an element may be performed using
+// FindLinkedPtr*().
+//
+// Use LookupOrInsertNewSharedPtr if the container does not own the elements
+// for their whole lifetime. This is typically the case when a reader allows
+// parallel updates to the container. In this case a Mutex only needs to lock
+// container operations, but all element operations must be performed on the
+// shared pointer. Finding an element must be performed using FindPtr*() and
+// cannot be done with FindLinkedPtr*() even though it compiles.
+
+// Lookup a key in a map or hash_map whose values are linked_ptrs. If it is
+// missing, set collection[key].reset(new Value::element_type) and return that.
+// Value::element_type must be default constructable.
+template <class Collection>
+typename Collection::value_type::second_type::element_type*
+LookupOrInsertNewLinkedPtr(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key) {
+ typedef typename Collection::value_type::second_type Value;
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(key, Value()));
+ if (ret.second) {
+ ret.first->second.reset(new typename Value::element_type);
+ }
+ return ret.first->second.get();
+}
+
+// A variant of LookupOrInsertNewLinkedPtr where the value is constructed using
+// a single-parameter constructor. Note: the constructor argument is computed
+// even if it will not be used, so only values cheap to compute should be passed
+// here. On the other hand it does not matter how expensive the construction of
+// the actual stored value is, as that only occurs if necessary.
+template <class Collection, class Arg>
+typename Collection::value_type::second_type::element_type*
+LookupOrInsertNewLinkedPtr(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const Arg& arg) {
+ typedef typename Collection::value_type::second_type Value;
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(key, Value()));
+ if (ret.second) {
+ ret.first->second.reset(new typename Value::element_type(arg));
+ }
+ return ret.first->second.get();
+}
+
+// Lookup a key in a map or hash_map whose values are shared_ptrs. If it is
+// missing, set collection[key].reset(new Value::element_type). Unlike
+// LookupOrInsertNewLinkedPtr, this function returns the shared_ptr instead of
+// the raw pointer. Value::element_type must be default constructable.
+template <class Collection>
+typename Collection::value_type::second_type&
+LookupOrInsertNewSharedPtr(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key) {
+ typedef typename Collection::value_type::second_type SharedPtr;
+ typedef typename Collection::value_type::second_type::element_type Element;
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(key, SharedPtr()));
+ if (ret.second) {
+ ret.first->second.reset(new Element());
+ }
+ return ret.first->second;
+}
+
+// A variant of LookupOrInsertNewSharedPtr where the value is constructed using
+// a single-parameter constructor. Note: the constructor argument is computed
+// even if it will not be used, so only values cheap to compute should be passed
+// here. On the other hand it does not matter how expensive the construction of
+// the actual stored value is, as that only occurs if necessary.
+template <class Collection, class Arg>
+typename Collection::value_type::second_type&
+LookupOrInsertNewSharedPtr(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const Arg& arg) {
+ typedef typename Collection::value_type::second_type SharedPtr;
+ typedef typename Collection::value_type::second_type::element_type Element;
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(key, SharedPtr()));
+ if (ret.second) {
+ ret.first->second.reset(new Element(arg));
+ }
+ return ret.first->second;
+}
+
+//
+// Misc Utility Functions
+//
+
+// Updates the value associated with the given key. If the key was not already
+// present, then the key-value pair are inserted and "previous" is unchanged. If
+// the key was already present, the value is updated and "*previous" will
+// contain a copy of the old value.
+//
+// InsertOrReturnExisting has complementary behavior that returns the
+// address of an already existing value, rather than updating it.
+template <class Collection>
+bool UpdateReturnCopy(Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& value,
+ typename Collection::value_type::second_type* previous) {
+ std::pair<typename Collection::iterator, bool> ret =
+ collection->insert(typename Collection::value_type(key, value));
+ if (!ret.second) {
+ // update
+ if (previous) {
+ *previous = ret.first->second;
+ }
+ ret.first->second = value;
+ return true;
+ }
+ return false;
+}
+
+// Same as above except that the key and value are passed as a pair.
+template <class Collection>
+bool UpdateReturnCopy(Collection* const collection,
+ const typename Collection::value_type& vt,
+ typename Collection::value_type::second_type* previous) {
+ std::pair<typename Collection::iterator, bool> ret = collection->insert(vt);
+ if (!ret.second) {
+ // update
+ if (previous) {
+ *previous = ret.first->second;
+ }
+ ret.first->second = vt.second;
+ return true;
+ }
+ return false;
+}
+
+// Tries to insert the given key-value pair into the collection. Returns NULL if
+// the insert succeeds. Otherwise, returns a pointer to the existing value.
+//
+// This complements UpdateReturnCopy in that it allows to update only after
+// verifying the old value and still insert quickly without having to look up
+// twice. Unlike UpdateReturnCopy this also does not come with the issue of an
+// undefined previous* in case new data was inserted.
+template <class Collection>
+typename Collection::value_type::second_type* const
+InsertOrReturnExisting(Collection* const collection,
+ const typename Collection::value_type& vt) {
+ std::pair<typename Collection::iterator, bool> ret = collection->insert(vt);
+ if (ret.second) {
+ return NULL; // Inserted, no existing previous value.
+ } else {
+ return &ret.first->second; // Return address of already existing value.
+ }
+}
+
+// Same as above, except for explicit key and data.
+template <class Collection>
+typename Collection::value_type::second_type* const
+InsertOrReturnExisting(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key,
+ const typename Collection::value_type::second_type& data) {
+ return InsertOrReturnExisting(collection,
+ typename Collection::value_type(key, data));
+}
+
+// Erases the collection item identified by the given key, and returns the value
+// associated with that key. It is assumed that the value (i.e., the
+// mapped_type) is a pointer. Returns NULL if the key was not found in the
+// collection.
+//
+// Examples:
+// map<string, MyType*> my_map;
+//
+// One line cleanup:
+// delete EraseKeyReturnValuePtr(&my_map, "abc");
+//
+// Use returned value:
+// scoped_ptr<MyType> value_ptr(EraseKeyReturnValuePtr(&my_map, "abc"));
+// if (value_ptr.get())
+// value_ptr->DoSomething();
+//
+template <class Collection>
+typename Collection::value_type::second_type EraseKeyReturnValuePtr(
+ Collection* const collection,
+ const typename Collection::value_type::first_type& key) {
+ typename Collection::iterator it = collection->find(key);
+ if (it == collection->end()) {
+ return NULL;
+ }
+ typename Collection::value_type::second_type v = it->second;
+ collection->erase(it);
+ return v;
+}
+
+// Inserts all the keys from map_container into key_container, which must
+// support insert(MapContainer::key_type).
+//
+// Note: any initial contents of the key_container are not cleared.
+template <class MapContainer, class KeyContainer>
+void InsertKeysFromMap(const MapContainer& map_container,
+ KeyContainer* key_container) {
+ GOOGLE_CHECK(key_container != NULL);
+ for (typename MapContainer::const_iterator it = map_container.begin();
+ it != map_container.end(); ++it) {
+ key_container->insert(it->first);
+ }
+}
+
+// Appends all the keys from map_container into key_container, which must
+// support push_back(MapContainer::key_type).
+//
+// Note: any initial contents of the key_container are not cleared.
+template <class MapContainer, class KeyContainer>
+void AppendKeysFromMap(const MapContainer& map_container,
+ KeyContainer* key_container) {
+ GOOGLE_CHECK(key_container != NULL);
+ for (typename MapContainer::const_iterator it = map_container.begin();
+ it != map_container.end(); ++it) {
+ key_container->push_back(it->first);
+ }
+}
+
+// A more specialized overload of AppendKeysFromMap to optimize reallocations
+// for the common case in which we're appending keys to a vector and hence can
+// (and sometimes should) call reserve() first.
+//
+// (It would be possible to play SFINAE games to call reserve() for any
+// container that supports it, but this seems to get us 99% of what we need
+// without the complexity of a SFINAE-based solution.)
+template <class MapContainer, class KeyType>
+void AppendKeysFromMap(const MapContainer& map_container,
+ vector<KeyType>* key_container) {
+ GOOGLE_CHECK(key_container != NULL);
+ // We now have the opportunity to call reserve(). Calling reserve() every
+ // time is a bad idea for some use cases: libstdc++'s implementation of
+ // vector<>::reserve() resizes the vector's backing store to exactly the
+ // given size (unless it's already at least that big). Because of this,
+ // the use case that involves appending a lot of small maps (total size
+ // N) one by one to a vector would be O(N^2). But never calling reserve()
+ // loses the opportunity to improve the use case of adding from a large
+ // map to an empty vector (this improves performance by up to 33%). A
+ // number of heuristics are possible; see the discussion in
+ // cl/34081696. Here we use the simplest one.
+ if (key_container->empty()) {
+ key_container->reserve(map_container.size());
+ }
+ for (typename MapContainer::const_iterator it = map_container.begin();
+ it != map_container.end(); ++it) {
+ key_container->push_back(it->first);
+ }
+}
+
+// Inserts all the values from map_container into value_container, which must
+// support push_back(MapContainer::mapped_type).
+//
+// Note: any initial contents of the value_container are not cleared.
+template <class MapContainer, class ValueContainer>
+void AppendValuesFromMap(const MapContainer& map_container,
+ ValueContainer* value_container) {
+ GOOGLE_CHECK(value_container != NULL);
+ for (typename MapContainer::const_iterator it = map_container.begin();
+ it != map_container.end(); ++it) {
+ value_container->push_back(it->second);
+ }
+}
+
+// A more specialized overload of AppendValuesFromMap to optimize reallocations
+// for the common case in which we're appending values to a vector and hence
+// can (and sometimes should) call reserve() first.
+//
+// (It would be possible to play SFINAE games to call reserve() for any
+// container that supports it, but this seems to get us 99% of what we need
+// without the complexity of a SFINAE-based solution.)
+template <class MapContainer, class ValueType>
+void AppendValuesFromMap(const MapContainer& map_container,
+ vector<ValueType>* value_container) {
+ GOOGLE_CHECK(value_container != NULL);
+ // See AppendKeysFromMap for why this is done.
+ if (value_container->empty()) {
+ value_container->reserve(map_container.size());
+ }
+ for (typename MapContainer::const_iterator it = map_container.begin();
+ it != map_container.end(); ++it) {
+ value_container->push_back(it->second);
+ }
+}
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_MAP_UTIL_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/once.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/once.cc
new file mode 100644
index 0000000000..889c647660
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/once.cc
@@ -0,0 +1,99 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+//
+// emulates google3/base/once.h
+//
+// This header is intended to be included only by internal .cc files and
+// generated .pb.cc files. Users should not use this directly.
+
+#include <google/protobuf/stubs/once.h>
+
+#ifndef GOOGLE_PROTOBUF_NO_THREAD_SAFETY
+
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <sched.h>
+#endif
+
+#include <google/protobuf/stubs/atomicops.h>
+
+namespace google {
+namespace protobuf {
+
+namespace {
+
+void SchedYield() {
+#ifdef _WIN32
+ Sleep(0);
+#else // POSIX
+ sched_yield();
+#endif
+}
+
+} // namespace
+
+void GoogleOnceInitImpl(ProtobufOnceType* once, Closure* closure) {
+ internal::AtomicWord state = internal::Acquire_Load(once);
+ // Fast path. The provided closure was already executed.
+ if (state == ONCE_STATE_DONE) {
+ return;
+ }
+ // The closure execution did not complete yet. The once object can be in one
+ // of the two following states:
+ // - UNINITIALIZED: We are the first thread calling this function.
+ // - EXECUTING_CLOSURE: Another thread is already executing the closure.
+ //
+ // First, try to change the state from UNINITIALIZED to EXECUTING_CLOSURE
+ // atomically.
+ state = internal::Acquire_CompareAndSwap(
+ once, ONCE_STATE_UNINITIALIZED, ONCE_STATE_EXECUTING_CLOSURE);
+ if (state == ONCE_STATE_UNINITIALIZED) {
+ // We are the first thread to call this function, so we have to call the
+ // closure.
+ closure->Run();
+ internal::Release_Store(once, ONCE_STATE_DONE);
+ } else {
+ // Another thread has already started executing the closure. We need to
+ // wait until it completes the initialization.
+ while (state == ONCE_STATE_EXECUTING_CLOSURE) {
+ // Note that futex() could be used here on Linux as an improvement.
+ SchedYield();
+ state = internal::Acquire_Load(once);
+ }
+ }
+}
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_NO_THREAD_SAFETY
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/once.h b/toolkit/components/protobuf/src/google/protobuf/stubs/once.h
new file mode 100644
index 0000000000..cc62bbaab3
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/once.h
@@ -0,0 +1,166 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+//
+// emulates google3/base/once.h
+//
+// This header is intended to be included only by internal .cc files and
+// generated .pb.cc files. Users should not use this directly.
+//
+// This is basically a portable version of pthread_once().
+//
+// This header declares:
+// * A type called ProtobufOnceType.
+// * A macro GOOGLE_PROTOBUF_DECLARE_ONCE() which declares a variable of type
+// ProtobufOnceType. This is the only legal way to declare such a variable.
+// The macro may only be used at the global scope (you cannot create local or
+// class member variables of this type).
+// * A function GoogleOnceInit(ProtobufOnceType* once, void (*init_func)()).
+// This function, when invoked multiple times given the same ProtobufOnceType
+// object, will invoke init_func on the first call only, and will make sure
+// none of the calls return before that first call to init_func has finished.
+// * The user can provide a parameter which GoogleOnceInit() forwards to the
+// user-provided function when it is called. Usage example:
+// int a = 10;
+// GoogleOnceInit(&my_once, &MyFunctionExpectingIntArgument, &a);
+// * This implementation guarantees that ProtobufOnceType is a POD (i.e. no
+// static initializer generated).
+//
+// This implements a way to perform lazy initialization. It's more efficient
+// than using mutexes as no lock is needed if initialization has already
+// happened.
+//
+// Example usage:
+// void Init();
+// GOOGLE_PROTOBUF_DECLARE_ONCE(once_init);
+//
+// // Calls Init() exactly once.
+// void InitOnce() {
+// GoogleOnceInit(&once_init, &Init);
+// }
+//
+// Note that if GoogleOnceInit() is called before main() has begun, it must
+// only be called by the thread that will eventually call main() -- that is,
+// the thread that performs dynamic initialization. In general this is a safe
+// assumption since people don't usually construct threads before main() starts,
+// but it is technically not guaranteed. Unfortunately, Win32 provides no way
+// whatsoever to statically-initialize its synchronization primitives, so our
+// only choice is to assume that dynamic initialization is single-threaded.
+
+#ifndef GOOGLE_PROTOBUF_STUBS_ONCE_H__
+#define GOOGLE_PROTOBUF_STUBS_ONCE_H__
+
+#include <google/protobuf/stubs/atomicops.h>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+#ifdef GOOGLE_PROTOBUF_NO_THREAD_SAFETY
+
+typedef bool ProtobufOnceType;
+
+#define GOOGLE_PROTOBUF_ONCE_INIT false
+
+inline void GoogleOnceInit(ProtobufOnceType* once, void (*init_func)()) {
+ if (!*once) {
+ *once = true;
+ init_func();
+ }
+}
+
+template <typename Arg>
+inline void GoogleOnceInit(ProtobufOnceType* once, void (*init_func)(Arg),
+ Arg arg) {
+ if (!*once) {
+ *once = true;
+ init_func(arg);
+ }
+}
+
+#else
+
+enum {
+ ONCE_STATE_UNINITIALIZED = 0,
+ ONCE_STATE_EXECUTING_CLOSURE = 1,
+ ONCE_STATE_DONE = 2
+};
+
+typedef internal::AtomicWord ProtobufOnceType;
+
+#define GOOGLE_PROTOBUF_ONCE_INIT ::google::protobuf::ONCE_STATE_UNINITIALIZED
+
+LIBPROTOBUF_EXPORT
+void GoogleOnceInitImpl(ProtobufOnceType* once, Closure* closure);
+
+inline void GoogleOnceInit(ProtobufOnceType* once, void (*init_func)()) {
+ if (internal::Acquire_Load(once) != ONCE_STATE_DONE) {
+ internal::FunctionClosure0 func(init_func, false);
+ GoogleOnceInitImpl(once, &func);
+ }
+}
+
+template <typename Arg>
+inline void GoogleOnceInit(ProtobufOnceType* once, void (*init_func)(Arg*),
+ Arg* arg) {
+ if (internal::Acquire_Load(once) != ONCE_STATE_DONE) {
+ internal::FunctionClosure1<Arg*> func(init_func, false, arg);
+ GoogleOnceInitImpl(once, &func);
+ }
+}
+
+#endif // GOOGLE_PROTOBUF_NO_THREAD_SAFETY
+
+class GoogleOnceDynamic {
+ public:
+ GoogleOnceDynamic() : state_(GOOGLE_PROTOBUF_ONCE_INIT) { }
+
+ // If this->Init() has not been called before by any thread,
+ // execute (*func_with_arg)(arg) then return.
+ // Otherwise, wait until that prior invocation has finished
+ // executing its function, then return.
+ template<typename T>
+ void Init(void (*func_with_arg)(T*), T* arg) {
+ GoogleOnceInit<T>(&this->state_,
+ func_with_arg,
+ arg);
+ }
+ private:
+ ProtobufOnceType state_;
+};
+
+#define GOOGLE_PROTOBUF_DECLARE_ONCE(NAME) \
+ ::google::protobuf::ProtobufOnceType NAME = GOOGLE_PROTOBUF_ONCE_INIT
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_ONCE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/platform_macros.h b/toolkit/components/protobuf/src/google/protobuf/stubs/platform_macros.h
new file mode 100644
index 0000000000..7956d076dc
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/platform_macros.h
@@ -0,0 +1,103 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef GOOGLE_PROTOBUF_PLATFORM_MACROS_H_
+#define GOOGLE_PROTOBUF_PLATFORM_MACROS_H_
+
+#include <google/protobuf/stubs/common.h>
+
+#define GOOGLE_PROTOBUF_PLATFORM_ERROR \
+#error "Host platform was not detected as supported by protobuf"
+
+// Processor architecture detection. For more info on what's defined, see:
+// http://msdn.microsoft.com/en-us/library/b0084kay.aspx
+// http://www.agner.org/optimize/calling_conventions.pdf
+// or with gcc, run: "echo | gcc -E -dM -"
+#if defined(_M_X64) || defined(__x86_64__)
+#define GOOGLE_PROTOBUF_ARCH_X64 1
+#define GOOGLE_PROTOBUF_ARCH_64_BIT 1
+#elif defined(_M_IX86) || defined(__i386__)
+#define GOOGLE_PROTOBUF_ARCH_IA32 1
+#define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#elif defined(__QNX__)
+#define GOOGLE_PROTOBUF_ARCH_ARM_QNX 1
+#define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#elif defined(__ARMEL__)
+#define GOOGLE_PROTOBUF_ARCH_ARM 1
+#define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#elif defined(__aarch64__)
+#define GOOGLE_PROTOBUF_ARCH_AARCH64 1
+#define GOOGLE_PROTOBUF_ARCH_64_BIT 1
+#elif defined(__MIPSEL__)
+#if defined(__LP64__)
+#define GOOGLE_PROTOBUF_ARCH_MIPS64 1
+#define GOOGLE_PROTOBUF_ARCH_64_BIT 1
+#else
+#define GOOGLE_PROTOBUF_ARCH_MIPS 1
+#define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#endif
+#elif defined(__pnacl__)
+#define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#elif defined(sparc)
+#define GOOGLE_PROTOBUF_ARCH_SPARC 1
+#ifdef SOLARIS_64BIT_ENABLED
+#define GOOGLE_PROTOBUF_ARCH_64_BIT 1
+#else
+#define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+#endif
+#elif defined(__GNUC__)
+# if (((__GNUC__ == 4) && (__GNUC_MINOR__ >= 7)) || (__GNUC__ > 4))
+// We fallback to the generic Clang/GCC >= 4.7 implementation in atomicops.h
+# elif defined(__clang__)
+# if !__has_extension(c_atomic)
+GOOGLE_PROTOBUF_PLATFORM_ERROR
+# endif
+// We fallback to the generic Clang/GCC >= 4.7 implementation in atomicops.h
+# endif
+# if __LP64__
+# define GOOGLE_PROTOBUF_ARCH_64_BIT 1
+# else
+# define GOOGLE_PROTOBUF_ARCH_32_BIT 1
+# endif
+#else
+GOOGLE_PROTOBUF_PLATFORM_ERROR
+#endif
+
+#if defined(__APPLE__)
+#define GOOGLE_PROTOBUF_OS_APPLE
+#elif defined(__native_client__)
+#define GOOGLE_PROTOBUF_OS_NACL
+#elif defined(sun)
+#define GOOGLE_PROTOBUF_OS_SOLARIS
+#endif
+
+#undef GOOGLE_PROTOBUF_PLATFORM_ERROR
+
+#endif // GOOGLE_PROTOBUF_PLATFORM_MACROS_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/shared_ptr.h b/toolkit/components/protobuf/src/google/protobuf/stubs/shared_ptr.h
new file mode 100644
index 0000000000..d250bf4d33
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/shared_ptr.h
@@ -0,0 +1,470 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2014 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/util/gtl/shared_ptr.h
+
+#ifndef GOOGLE_PROTOBUF_STUBS_SHARED_PTR_H__
+#define GOOGLE_PROTOBUF_STUBS_SHARED_PTR_H__
+
+#include <google/protobuf/stubs/atomicops.h>
+
+#include <algorithm> // for swap
+#include <stddef.h>
+#include <memory>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// Alias to std::shared_ptr for any C++11 platform,
+// and for any supported MSVC compiler.
+#if !defined(UTIL_GTL_USE_STD_SHARED_PTR) && \
+ (defined(COMPILER_MSVC) || defined(LANG_CXX11))
+#define UTIL_GTL_USE_STD_SHARED_PTR 1
+#endif
+
+#if defined(UTIL_GTL_USE_STD_SHARED_PTR) && UTIL_GTL_USE_STD_SHARED_PTR
+
+// These are transitional. They will be going away soon.
+// Please just #include <memory> and just type std::shared_ptr yourself, instead
+// of relying on this file.
+//
+// Migration doc: http://go/std-shared-ptr-lsc
+using std::enable_shared_from_this;
+using std::shared_ptr;
+using std::static_pointer_cast;
+using std::weak_ptr;
+
+#else // below, UTIL_GTL_USE_STD_SHARED_PTR not set or set to 0.
+
+// For everything else there is the google3 implementation.
+inline bool RefCountDec(volatile Atomic32 *ptr) {
+ return Barrier_AtomicIncrement(ptr, -1) != 0;
+}
+
+inline void RefCountInc(volatile Atomic32 *ptr) {
+ NoBarrier_AtomicIncrement(ptr, 1);
+}
+
+template <typename T> class shared_ptr;
+template <typename T> class weak_ptr;
+
+// This class is an internal implementation detail for shared_ptr. If two
+// shared_ptrs point to the same object, they also share a control block.
+// An "empty" shared_pointer refers to NULL and also has a NULL control block.
+// It contains all of the state that's needed for reference counting or any
+// other kind of resource management. In this implementation the control block
+// happens to consist of two atomic words, the reference count (the number
+// of shared_ptrs that share ownership of the object) and the weak count
+// (the number of weak_ptrs that observe the object, plus 1 if the
+// refcount is nonzero).
+//
+// The "plus 1" is to prevent a race condition in the shared_ptr and
+// weak_ptr destructors. We need to make sure the control block is
+// only deleted once, so we need to make sure that at most one
+// object sees the weak count decremented from 1 to 0.
+class SharedPtrControlBlock {
+ template <typename T> friend class shared_ptr;
+ template <typename T> friend class weak_ptr;
+ private:
+ SharedPtrControlBlock() : refcount_(1), weak_count_(1) { }
+ Atomic32 refcount_;
+ Atomic32 weak_count_;
+};
+
+// Forward declaration. The class is defined below.
+template <typename T> class enable_shared_from_this;
+
+template <typename T>
+class shared_ptr {
+ template <typename U> friend class weak_ptr;
+ public:
+ typedef T element_type;
+
+ shared_ptr() : ptr_(NULL), control_block_(NULL) {}
+
+ explicit shared_ptr(T* ptr)
+ : ptr_(ptr),
+ control_block_(ptr != NULL ? new SharedPtrControlBlock : NULL) {
+ // If p is non-null and T inherits from enable_shared_from_this, we
+ // set up the data that shared_from_this needs.
+ MaybeSetupWeakThis(ptr);
+ }
+
+ // Copy constructor: makes this object a copy of ptr, and increments
+ // the reference count.
+ template <typename U>
+ shared_ptr(const shared_ptr<U>& ptr)
+ : ptr_(NULL),
+ control_block_(NULL) {
+ Initialize(ptr);
+ }
+ // Need non-templated version to prevent the compiler-generated default
+ shared_ptr(const shared_ptr<T>& ptr)
+ : ptr_(NULL),
+ control_block_(NULL) {
+ Initialize(ptr);
+ }
+
+ // Assignment operator. Replaces the existing shared_ptr with ptr.
+ // Increment ptr's reference count and decrement the one being replaced.
+ template <typename U>
+ shared_ptr<T>& operator=(const shared_ptr<U>& ptr) {
+ if (ptr_ != ptr.ptr_) {
+ shared_ptr<T> me(ptr); // will hold our previous state to be destroyed.
+ swap(me);
+ }
+ return *this;
+ }
+
+ // Need non-templated version to prevent the compiler-generated default
+ shared_ptr<T>& operator=(const shared_ptr<T>& ptr) {
+ if (ptr_ != ptr.ptr_) {
+ shared_ptr<T> me(ptr); // will hold our previous state to be destroyed.
+ swap(me);
+ }
+ return *this;
+ }
+
+ // TODO(austern): Consider providing this constructor. The draft C++ standard
+ // (20.8.10.2.1) includes it. However, it says that this constructor throws
+ // a bad_weak_ptr exception when ptr is expired. Is it better to provide this
+ // constructor and make it do something else, like fail with a CHECK, or to
+ // leave this constructor out entirely?
+ //
+ // template <typename U>
+ // shared_ptr(const weak_ptr<U>& ptr);
+
+ ~shared_ptr() {
+ if (ptr_ != NULL) {
+ if (!RefCountDec(&control_block_->refcount_)) {
+ delete ptr_;
+
+ // weak_count_ is defined as the number of weak_ptrs that observe
+ // ptr_, plus 1 if refcount_ is nonzero.
+ if (!RefCountDec(&control_block_->weak_count_)) {
+ delete control_block_;
+ }
+ }
+ }
+ }
+
+ // Replaces underlying raw pointer with the one passed in. The reference
+ // count is set to one (or zero if the pointer is NULL) for the pointer
+ // being passed in and decremented for the one being replaced.
+ //
+ // If you have a compilation error with this code, make sure you aren't
+ // passing NULL, nullptr, or 0 to this function. Call reset without an
+ // argument to reset to a null ptr.
+ template <typename Y>
+ void reset(Y* p) {
+ if (p != ptr_) {
+ shared_ptr<T> tmp(p);
+ tmp.swap(*this);
+ }
+ }
+
+ void reset() {
+ reset(static_cast<T*>(NULL));
+ }
+
+ // Exchanges the contents of this with the contents of r. This function
+ // supports more efficient swapping since it eliminates the need for a
+ // temporary shared_ptr object.
+ void swap(shared_ptr<T>& r) {
+ using std::swap; // http://go/using-std-swap
+ swap(ptr_, r.ptr_);
+ swap(control_block_, r.control_block_);
+ }
+
+ // The following function is useful for gaining access to the underlying
+ // pointer when a shared_ptr remains in scope so the reference-count is
+ // known to be > 0 (e.g. for parameter passing).
+ T* get() const {
+ return ptr_;
+ }
+
+ T& operator*() const {
+ return *ptr_;
+ }
+
+ T* operator->() const {
+ return ptr_;
+ }
+
+ long use_count() const {
+ return control_block_ ? control_block_->refcount_ : 1;
+ }
+
+ bool unique() const {
+ return use_count() == 1;
+ }
+
+ private:
+ // If r is non-empty, initialize *this to share ownership with r,
+ // increasing the underlying reference count.
+ // If r is empty, *this remains empty.
+ // Requires: this is empty, namely this->ptr_ == NULL.
+ template <typename U>
+ void Initialize(const shared_ptr<U>& r) {
+ // This performs a static_cast on r.ptr_ to U*, which is a no-op since it
+ // is already a U*. So initialization here requires that r.ptr_ is
+ // implicitly convertible to T*.
+ InitializeWithStaticCast<U>(r);
+ }
+
+ // Initializes *this as described in Initialize, but additionally performs a
+ // static_cast from r.ptr_ (V*) to U*.
+ // NOTE(gfc): We'd need a more general form to support const_pointer_cast and
+ // dynamic_pointer_cast, but those operations are sufficiently discouraged
+ // that supporting static_pointer_cast is sufficient.
+ template <typename U, typename V>
+ void InitializeWithStaticCast(const shared_ptr<V>& r) {
+ if (r.control_block_ != NULL) {
+ RefCountInc(&r.control_block_->refcount_);
+
+ ptr_ = static_cast<U*>(r.ptr_);
+ control_block_ = r.control_block_;
+ }
+ }
+
+ // Helper function for the constructor that takes a raw pointer. If T
+ // doesn't inherit from enable_shared_from_this<T> then we have nothing to
+ // do, so this function is trivial and inline. The other version is declared
+ // out of line, after the class definition of enable_shared_from_this.
+ void MaybeSetupWeakThis(enable_shared_from_this<T>* ptr);
+ void MaybeSetupWeakThis(...) { }
+
+ T* ptr_;
+ SharedPtrControlBlock* control_block_;
+
+#ifndef SWIG
+ template <typename U>
+ friend class shared_ptr;
+
+ template <typename U, typename V>
+ friend shared_ptr<U> static_pointer_cast(const shared_ptr<V>& rhs);
+#endif
+};
+
+// Matches the interface of std::swap as an aid to generic programming.
+template <typename T> void swap(shared_ptr<T>& r, shared_ptr<T>& s) {
+ r.swap(s);
+}
+
+template <typename T, typename U>
+shared_ptr<T> static_pointer_cast(const shared_ptr<U>& rhs) {
+ shared_ptr<T> lhs;
+ lhs.template InitializeWithStaticCast<T>(rhs);
+ return lhs;
+}
+
+// See comments at the top of the file for a description of why this
+// class exists, and the draft C++ standard (as of July 2009 the
+// latest draft is N2914) for the detailed specification.
+template <typename T>
+class weak_ptr {
+ template <typename U> friend class weak_ptr;
+ public:
+ typedef T element_type;
+
+ // Create an empty (i.e. already expired) weak_ptr.
+ weak_ptr() : ptr_(NULL), control_block_(NULL) { }
+
+ // Create a weak_ptr that observes the same object that ptr points
+ // to. Note that there is no race condition here: we know that the
+ // control block can't disappear while we're looking at it because
+ // it is owned by at least one shared_ptr, ptr.
+ template <typename U> weak_ptr(const shared_ptr<U>& ptr) {
+ CopyFrom(ptr.ptr_, ptr.control_block_);
+ }
+
+ // Copy a weak_ptr. The object it points to might disappear, but we
+ // don't care: we're only working with the control block, and it can't
+ // disappear while we're looking at because it's owned by at least one
+ // weak_ptr, ptr.
+ template <typename U> weak_ptr(const weak_ptr<U>& ptr) {
+ CopyFrom(ptr.ptr_, ptr.control_block_);
+ }
+
+ // Need non-templated version to prevent default copy constructor
+ weak_ptr(const weak_ptr& ptr) {
+ CopyFrom(ptr.ptr_, ptr.control_block_);
+ }
+
+ // Destroy the weak_ptr. If no shared_ptr owns the control block, and if
+ // we are the last weak_ptr to own it, then it can be deleted. Note that
+ // weak_count_ is defined as the number of weak_ptrs sharing this control
+ // block, plus 1 if there are any shared_ptrs. We therefore know that it's
+ // safe to delete the control block when weak_count_ reaches 0, without
+ // having to perform any additional tests.
+ ~weak_ptr() {
+ if (control_block_ != NULL &&
+ !RefCountDec(&control_block_->weak_count_)) {
+ delete control_block_;
+ }
+ }
+
+ weak_ptr& operator=(const weak_ptr& ptr) {
+ if (&ptr != this) {
+ weak_ptr tmp(ptr);
+ tmp.swap(*this);
+ }
+ return *this;
+ }
+ template <typename U> weak_ptr& operator=(const weak_ptr<U>& ptr) {
+ weak_ptr tmp(ptr);
+ tmp.swap(*this);
+ return *this;
+ }
+ template <typename U> weak_ptr& operator=(const shared_ptr<U>& ptr) {
+ weak_ptr tmp(ptr);
+ tmp.swap(*this);
+ return *this;
+ }
+
+ void swap(weak_ptr& ptr) {
+ using std::swap; // http://go/using-std-swap
+ swap(ptr_, ptr.ptr_);
+ swap(control_block_, ptr.control_block_);
+ }
+
+ void reset() {
+ weak_ptr tmp;
+ tmp.swap(*this);
+ }
+
+ // Return the number of shared_ptrs that own the object we are observing.
+ // Note that this number can be 0 (if this pointer has expired).
+ long use_count() const {
+ return control_block_ != NULL ? control_block_->refcount_ : 0;
+ }
+
+ bool expired() const { return use_count() == 0; }
+
+ // Return a shared_ptr that owns the object we are observing. If we
+ // have expired, the shared_ptr will be empty. We have to be careful
+ // about concurrency, though, since some other thread might be
+ // destroying the last owning shared_ptr while we're in this
+ // function. We want to increment the refcount only if it's nonzero
+ // and get the new value, and we want that whole operation to be
+ // atomic.
+ shared_ptr<T> lock() const {
+ shared_ptr<T> result;
+ if (control_block_ != NULL) {
+ Atomic32 old_refcount;
+ do {
+ old_refcount = control_block_->refcount_;
+ if (old_refcount == 0)
+ break;
+ } while (old_refcount !=
+ NoBarrier_CompareAndSwap(
+ &control_block_->refcount_, old_refcount,
+ old_refcount + 1));
+ if (old_refcount > 0) {
+ result.ptr_ = ptr_;
+ result.control_block_ = control_block_;
+ }
+ }
+
+ return result;
+ }
+
+ private:
+ void CopyFrom(T* ptr, SharedPtrControlBlock* control_block) {
+ ptr_ = ptr;
+ control_block_ = control_block;
+ if (control_block_ != NULL)
+ RefCountInc(&control_block_->weak_count_);
+ }
+
+ private:
+ element_type* ptr_;
+ SharedPtrControlBlock* control_block_;
+};
+
+template <typename T> void swap(weak_ptr<T>& r, weak_ptr<T>& s) {
+ r.swap(s);
+}
+
+// See comments at the top of the file for a description of why this class
+// exists, and section 20.8.10.5 of the draft C++ standard (as of July 2009
+// the latest draft is N2914) for the detailed specification.
+template <typename T>
+class enable_shared_from_this {
+ friend class shared_ptr<T>;
+ public:
+ // Precondition: there must be a shared_ptr that owns *this and that was
+ // created, directly or indirectly, from a raw pointer of type T*. (The
+ // latter part of the condition is technical but not quite redundant; it
+ // rules out some complicated uses involving inheritance hierarchies.)
+ shared_ptr<T> shared_from_this() {
+ // Behavior is undefined if the precondition isn't satisfied; we choose
+ // to die with a CHECK failure.
+ CHECK(!weak_this_.expired()) << "No shared_ptr owns this object";
+ return weak_this_.lock();
+ }
+ shared_ptr<const T> shared_from_this() const {
+ CHECK(!weak_this_.expired()) << "No shared_ptr owns this object";
+ return weak_this_.lock();
+ }
+
+ protected:
+ enable_shared_from_this() { }
+ enable_shared_from_this(const enable_shared_from_this& other) { }
+ enable_shared_from_this& operator=(const enable_shared_from_this& other) {
+ return *this;
+ }
+ ~enable_shared_from_this() { }
+
+ private:
+ weak_ptr<T> weak_this_;
+};
+
+// This is a helper function called by shared_ptr's constructor from a raw
+// pointer. If T inherits from enable_shared_from_this<T>, it sets up
+// weak_this_ so that shared_from_this works correctly. If T does not inherit
+// from weak_this we get a different overload, defined inline, which does
+// nothing.
+template<typename T>
+void shared_ptr<T>::MaybeSetupWeakThis(enable_shared_from_this<T>* ptr) {
+ if (ptr) {
+ CHECK(ptr->weak_this_.expired()) << "Object already owned by a shared_ptr";
+ ptr->weak_this_ = *this;
+ }
+}
+
+#endif // UTIL_GTL_USE_STD_SHARED_PTR
+
+} // internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_SHARED_PTR_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/stl_util.h b/toolkit/components/protobuf/src/google/protobuf/stubs/stl_util.h
new file mode 100644
index 0000000000..9e4c82a4c3
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/stl_util.h
@@ -0,0 +1,121 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/util/gtl/stl_util.h
+
+#ifndef GOOGLE_PROTOBUF_STUBS_STL_UTIL_H__
+#define GOOGLE_PROTOBUF_STUBS_STL_UTIL_H__
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+// STLDeleteContainerPointers()
+// For a range within a container of pointers, calls delete
+// (non-array version) on these pointers.
+// NOTE: for these three functions, we could just implement a DeleteObject
+// functor and then call for_each() on the range and functor, but this
+// requires us to pull in all of algorithm.h, which seems expensive.
+// For hash_[multi]set, it is important that this deletes behind the iterator
+// because the hash_set may call the hash function on the iterator when it is
+// advanced, which could result in the hash function trying to deference a
+// stale pointer.
+template <class ForwardIterator>
+void STLDeleteContainerPointers(ForwardIterator begin,
+ ForwardIterator end) {
+ while (begin != end) {
+ ForwardIterator temp = begin;
+ ++begin;
+ delete *temp;
+ }
+}
+
+// Inside Google, this function implements a horrible, disgusting hack in which
+// we reach into the string's private implementation and resize it without
+// initializing the new bytes. In some cases doing this can significantly
+// improve performance. However, since it's totally non-portable it has no
+// place in open source code. Feel free to fill this function in with your
+// own disgusting hack if you want the perf boost.
+inline void STLStringResizeUninitialized(string* s, size_t new_size) {
+ s->resize(new_size);
+}
+
+// Return a mutable char* pointing to a string's internal buffer,
+// which may not be null-terminated. Writing through this pointer will
+// modify the string.
+//
+// string_as_array(&str)[i] is valid for 0 <= i < str.size() until the
+// next call to a string method that invalidates iterators.
+//
+// As of 2006-04, there is no standard-blessed way of getting a
+// mutable reference to a string's internal buffer. However, issue 530
+// (http://www.open-std.org/JTC1/SC22/WG21/docs/lwg-active.html#530)
+// proposes this as the method. According to Matt Austern, this should
+// already work on all current implementations.
+inline char* string_as_array(string* str) {
+ // DO NOT USE const_cast<char*>(str->data())! See the unittest for why.
+ return str->empty() ? NULL : &*str->begin();
+}
+
+// STLDeleteElements() deletes all the elements in an STL container and clears
+// the container. This function is suitable for use with a vector, set,
+// hash_set, or any other STL container which defines sensible begin(), end(),
+// and clear() methods.
+//
+// If container is NULL, this function is a no-op.
+//
+// As an alternative to calling STLDeleteElements() directly, consider
+// ElementDeleter (defined below), which ensures that your container's elements
+// are deleted when the ElementDeleter goes out of scope.
+template <class T>
+void STLDeleteElements(T *container) {
+ if (!container) return;
+ STLDeleteContainerPointers(container->begin(), container->end());
+ container->clear();
+}
+
+// Given an STL container consisting of (key, value) pairs, STLDeleteValues
+// deletes all the "value" components and clears the container. Does nothing
+// in the case it's given a NULL pointer.
+
+template <class T>
+void STLDeleteValues(T *v) {
+ if (!v) return;
+ for (typename T::iterator i = v->begin(); i != v->end(); ++i) {
+ delete i->second;
+ }
+ v->clear();
+}
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_STL_UTIL_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.cc
new file mode 100644
index 0000000000..83fdfe454e
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.cc
@@ -0,0 +1,174 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/base/stringprintf.cc
+
+#include <google/protobuf/stubs/stringprintf.h>
+
+#include <errno.h>
+#include <stdarg.h> // For va_list and related operations
+#include <stdio.h> // MSVC requires this for _vsnprintf
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+#ifdef _MSC_VER
+enum { IS_COMPILER_MSVC = 1 };
+#ifndef va_copy
+// Define va_copy for MSVC. This is a hack, assuming va_list is simply a
+// pointer into the stack and is safe to copy.
+#define va_copy(dest, src) ((dest) = (src))
+#endif
+#else
+enum { IS_COMPILER_MSVC = 0 };
+#endif
+
+void StringAppendV(string* dst, const char* format, va_list ap) {
+ // First try with a small fixed size buffer
+ static const int kSpaceLength = 1024;
+ char space[kSpaceLength];
+
+ // It's possible for methods that use a va_list to invalidate
+ // the data in it upon use. The fix is to make a copy
+ // of the structure before using it and use that copy instead.
+ va_list backup_ap;
+ va_copy(backup_ap, ap);
+ int result = vsnprintf(space, kSpaceLength, format, backup_ap);
+ va_end(backup_ap);
+
+ if (result < kSpaceLength) {
+ if (result >= 0) {
+ // Normal case -- everything fit.
+ dst->append(space, result);
+ return;
+ }
+
+ if (IS_COMPILER_MSVC) {
+ // Error or MSVC running out of space. MSVC 8.0 and higher
+ // can be asked about space needed with the special idiom below:
+ va_copy(backup_ap, ap);
+ result = vsnprintf(NULL, 0, format, backup_ap);
+ va_end(backup_ap);
+ }
+
+ if (result < 0) {
+ // Just an error.
+ return;
+ }
+ }
+
+ // Increase the buffer size to the size requested by vsnprintf,
+ // plus one for the closing \0.
+ int length = result+1;
+ char* buf = new char[length];
+
+ // Restore the va_list before we use it again
+ va_copy(backup_ap, ap);
+ result = vsnprintf(buf, length, format, backup_ap);
+ va_end(backup_ap);
+
+ if (result >= 0 && result < length) {
+ // It fit
+ dst->append(buf, result);
+ }
+ delete[] buf;
+}
+
+
+string StringPrintf(const char* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ string result;
+ StringAppendV(&result, format, ap);
+ va_end(ap);
+ return result;
+}
+
+const string& SStringPrintf(string* dst, const char* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ dst->clear();
+ StringAppendV(dst, format, ap);
+ va_end(ap);
+ return *dst;
+}
+
+void StringAppendF(string* dst, const char* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ StringAppendV(dst, format, ap);
+ va_end(ap);
+}
+
+// Max arguments supported by StringPrintVector
+const int kStringPrintfVectorMaxArgs = 32;
+
+// An empty block of zero for filler arguments. This is const so that if
+// printf tries to write to it (via %n) then the program gets a SIGSEGV
+// and we can fix the problem or protect against an attack.
+static const char string_printf_empty_block[256] = { '\0' };
+
+string StringPrintfVector(const char* format, const vector<string>& v) {
+ GOOGLE_CHECK_LE(v.size(), kStringPrintfVectorMaxArgs)
+ << "StringPrintfVector currently only supports up to "
+ << kStringPrintfVectorMaxArgs << " arguments. "
+ << "Feel free to add support for more if you need it.";
+
+ // Add filler arguments so that bogus format+args have a harder time
+ // crashing the program, corrupting the program (%n),
+ // or displaying random chunks of memory to users.
+
+ const char* cstr[kStringPrintfVectorMaxArgs];
+ for (int i = 0; i < v.size(); ++i) {
+ cstr[i] = v[i].c_str();
+ }
+ for (int i = v.size(); i < GOOGLE_ARRAYSIZE(cstr); ++i) {
+ cstr[i] = &string_printf_empty_block[0];
+ }
+
+ // I do not know any way to pass kStringPrintfVectorMaxArgs arguments,
+ // or any way to build a va_list by hand, or any API for printf
+ // that accepts an array of arguments. The best I can do is stick
+ // this COMPILE_ASSERT right next to the actual statement.
+
+ GOOGLE_COMPILE_ASSERT(kStringPrintfVectorMaxArgs == 32, arg_count_mismatch);
+ return StringPrintf(format,
+ cstr[0], cstr[1], cstr[2], cstr[3], cstr[4],
+ cstr[5], cstr[6], cstr[7], cstr[8], cstr[9],
+ cstr[10], cstr[11], cstr[12], cstr[13], cstr[14],
+ cstr[15], cstr[16], cstr[17], cstr[18], cstr[19],
+ cstr[20], cstr[21], cstr[22], cstr[23], cstr[24],
+ cstr[25], cstr[26], cstr[27], cstr[28], cstr[29],
+ cstr[30], cstr[31]);
+}
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.h b/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.h
new file mode 100644
index 0000000000..ab1ab55832
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/stringprintf.h
@@ -0,0 +1,76 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2012 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/base/stringprintf.h
+//
+// Printf variants that place their output in a C++ string.
+//
+// Usage:
+// string result = StringPrintf("%d %s\n", 10, "hello");
+// SStringPrintf(&result, "%d %s\n", 10, "hello");
+// StringAppendF(&result, "%d %s\n", 20, "there");
+
+#ifndef GOOGLE_PROTOBUF_STUBS_STRINGPRINTF_H
+#define GOOGLE_PROTOBUF_STUBS_STRINGPRINTF_H
+
+#include <stdarg.h>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+// Return a C++ string
+LIBPROTOBUF_EXPORT extern string StringPrintf(const char* format, ...);
+
+// Store result into a supplied string and return it
+LIBPROTOBUF_EXPORT extern const string& SStringPrintf(string* dst, const char* format, ...);
+
+// Append result to a supplied string
+LIBPROTOBUF_EXPORT extern void StringAppendF(string* dst, const char* format, ...);
+
+// Lower-level routine that takes a va_list and appends to a specified
+// string. All other routines are just convenience wrappers around it.
+LIBPROTOBUF_EXPORT extern void StringAppendV(string* dst, const char* format, va_list ap);
+
+// The max arguments supported by StringPrintfVector
+LIBPROTOBUF_EXPORT extern const int kStringPrintfVectorMaxArgs;
+
+// You can use this version when all your arguments are strings, but
+// you don't know how many arguments you'll have at compile time.
+// StringPrintfVector will LOG(FATAL) if v.size() > kStringPrintfVectorMaxArgs
+LIBPROTOBUF_EXPORT extern string StringPrintfVector(const char* format, const vector<string>& v);
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_STRINGPRINTF_H
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/structurally_valid.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/structurally_valid.cc
new file mode 100644
index 0000000000..0f6afe6dc8
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/structurally_valid.cc
@@ -0,0 +1,536 @@
+// Copyright 2005-2008 Google Inc. All Rights Reserved.
+// Author: jrm@google.com (Jim Meehan)
+
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// These four-byte entries compactly encode how many bytes 0..255 to delete
+// in making a string replacement, how many bytes to add 0..255, and the offset
+// 0..64k-1 of the replacement string in remap_string.
+struct RemapEntry {
+ uint8 delete_bytes;
+ uint8 add_bytes;
+ uint16 bytes_offset;
+};
+
+// Exit type codes for state tables. All but the first get stuffed into
+// signed one-byte entries. The first is only generated by executable code.
+// To distinguish from next-state entries, these must be contiguous and
+// all <= kExitNone
+typedef enum {
+ kExitDstSpaceFull = 239,
+ kExitIllegalStructure, // 240
+ kExitOK, // 241
+ kExitReject, // ...
+ kExitReplace1,
+ kExitReplace2,
+ kExitReplace3,
+ kExitReplace21,
+ kExitReplace31,
+ kExitReplace32,
+ kExitReplaceOffset1,
+ kExitReplaceOffset2,
+ kExitReplace1S0,
+ kExitSpecial,
+ kExitDoAgain,
+ kExitRejectAlt,
+ kExitNone // 255
+} ExitReason;
+
+
+// This struct represents one entire state table. The three initialized byte
+// areas are state_table, remap_base, and remap_string. state0 and state0_size
+// give the byte offset and length within state_table of the initial state --
+// table lookups are expected to start and end in this state, but for
+// truncated UTF-8 strings, may end in a different state. These allow a quick
+// test for that condition. entry_shift is 8 for tables subscripted by a full
+// byte value and 6 for space-optimized tables subscripted by only six
+// significant bits in UTF-8 continuation bytes.
+typedef struct {
+ const uint32 state0;
+ const uint32 state0_size;
+ const uint32 total_size;
+ const int max_expand;
+ const int entry_shift;
+ const int bytes_per_entry;
+ const uint32 losub;
+ const uint32 hiadd;
+ const uint8* state_table;
+ const RemapEntry* remap_base;
+ const uint8* remap_string;
+ const uint8* fast_state;
+} UTF8StateMachineObj;
+
+typedef UTF8StateMachineObj UTF8ScanObj;
+
+#define X__ (kExitIllegalStructure)
+#define RJ_ (kExitReject)
+#define S1_ (kExitReplace1)
+#define S2_ (kExitReplace2)
+#define S3_ (kExitReplace3)
+#define S21 (kExitReplace21)
+#define S31 (kExitReplace31)
+#define S32 (kExitReplace32)
+#define T1_ (kExitReplaceOffset1)
+#define T2_ (kExitReplaceOffset2)
+#define S11 (kExitReplace1S0)
+#define SP_ (kExitSpecial)
+#define D__ (kExitDoAgain)
+#define RJA (kExitRejectAlt)
+
+// Entire table has 9 state blocks of 256 entries each
+static const unsigned int utf8acceptnonsurrogates_STATE0 = 0; // state[0]
+static const unsigned int utf8acceptnonsurrogates_STATE0_SIZE = 256; // =[1]
+static const unsigned int utf8acceptnonsurrogates_TOTAL_SIZE = 2304;
+static const unsigned int utf8acceptnonsurrogates_MAX_EXPAND_X4 = 0;
+static const unsigned int utf8acceptnonsurrogates_SHIFT = 8;
+static const unsigned int utf8acceptnonsurrogates_BYTES = 1;
+static const unsigned int utf8acceptnonsurrogates_LOSUB = 0x20202020;
+static const unsigned int utf8acceptnonsurrogates_HIADD = 0x00000000;
+
+static const uint8 utf8acceptnonsurrogates[] = {
+// state[0] 0x000000 Byte 1
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 3,
+ 4, 5, 5, 5, 6, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[1] 0x000080 Byte 2 of 2
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[2] 0x000000 Byte 2 of 3
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[3] 0x001000 Byte 2 of 3
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[4] 0x000000 Byte 2 of 4
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[5] 0x040000 Byte 2 of 4
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[6] 0x100000 Byte 2 of 4
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[7] 0x00d000 Byte 2 of 3
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+// state[8] 0x00d800 Byte 3 of 3
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+
+RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_,
+RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_,
+RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_,
+RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_, RJ_,
+
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__, X__,
+};
+
+// Remap base[0] = (del, add, string_offset)
+static const RemapEntry utf8acceptnonsurrogates_remap_base[] = {
+{0, 0, 0} };
+
+// Remap string[0]
+static const unsigned char utf8acceptnonsurrogates_remap_string[] = {
+0 };
+
+static const unsigned char utf8acceptnonsurrogates_fast[256] = {
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+};
+
+static const UTF8ScanObj utf8acceptnonsurrogates_obj = {
+ utf8acceptnonsurrogates_STATE0,
+ utf8acceptnonsurrogates_STATE0_SIZE,
+ utf8acceptnonsurrogates_TOTAL_SIZE,
+ utf8acceptnonsurrogates_MAX_EXPAND_X4,
+ utf8acceptnonsurrogates_SHIFT,
+ utf8acceptnonsurrogates_BYTES,
+ utf8acceptnonsurrogates_LOSUB,
+ utf8acceptnonsurrogates_HIADD,
+ utf8acceptnonsurrogates,
+ utf8acceptnonsurrogates_remap_base,
+ utf8acceptnonsurrogates_remap_string,
+ utf8acceptnonsurrogates_fast
+};
+
+
+#undef X__
+#undef RJ_
+#undef S1_
+#undef S2_
+#undef S3_
+#undef S21
+#undef S31
+#undef S32
+#undef T1_
+#undef T2_
+#undef S11
+#undef SP_
+#undef D__
+#undef RJA
+
+// Return true if current Tbl pointer is within state0 range
+// Note that unsigned compare checks both ends of range simultaneously
+static inline bool InStateZero(const UTF8ScanObj* st, const uint8* Tbl) {
+ const uint8* Tbl0 = &st->state_table[st->state0];
+ return (static_cast<uint32>(Tbl - Tbl0) < st->state0_size);
+}
+
+// Scan a UTF-8 string based on state table.
+// Always scan complete UTF-8 characters
+// Set number of bytes scanned. Return reason for exiting
+int UTF8GenericScan(const UTF8ScanObj* st,
+ const char * str,
+ int str_length,
+ int* bytes_consumed) {
+ *bytes_consumed = 0;
+ if (str_length == 0) return kExitOK;
+
+ int eshift = st->entry_shift;
+ const uint8* isrc = reinterpret_cast<const uint8*>(str);
+ const uint8* src = isrc;
+ const uint8* srclimit = isrc + str_length;
+ const uint8* srclimit8 = srclimit - 7;
+ const uint8* Tbl_0 = &st->state_table[st->state0];
+
+ DoAgain:
+ // Do state-table scan
+ int e = 0;
+ uint8 c;
+ const uint8* Tbl2 = &st->fast_state[0];
+ const uint32 losub = st->losub;
+ const uint32 hiadd = st->hiadd;
+ // Check initial few bytes one at a time until 8-byte aligned
+ //----------------------------
+ while ((((uintptr_t)src & 0x07) != 0) &&
+ (src < srclimit) &&
+ Tbl2[src[0]] == 0) {
+ src++;
+ }
+ if (((uintptr_t)src & 0x07) == 0) {
+ // Do fast for groups of 8 identity bytes.
+ // This covers a lot of 7-bit ASCII ~8x faster then the 1-byte loop,
+ // including slowing slightly on cr/lf/ht
+ //----------------------------
+ while (src < srclimit8) {
+ uint32 s0123 = (reinterpret_cast<const uint32 *>(src))[0];
+ uint32 s4567 = (reinterpret_cast<const uint32 *>(src))[1];
+ src += 8;
+ // This is a fast range check for all bytes in [lowsub..0x80-hiadd)
+ uint32 temp = (s0123 - losub) | (s0123 + hiadd) |
+ (s4567 - losub) | (s4567 + hiadd);
+ if ((temp & 0x80808080) != 0) {
+ // We typically end up here on cr/lf/ht; src was incremented
+ int e0123 = (Tbl2[src[-8]] | Tbl2[src[-7]]) |
+ (Tbl2[src[-6]] | Tbl2[src[-5]]);
+ if (e0123 != 0) {
+ src -= 8;
+ break;
+ } // Exit on Non-interchange
+ e0123 = (Tbl2[src[-4]] | Tbl2[src[-3]]) |
+ (Tbl2[src[-2]] | Tbl2[src[-1]]);
+ if (e0123 != 0) {
+ src -= 4;
+ break;
+ } // Exit on Non-interchange
+ // Else OK, go around again
+ }
+ }
+ }
+ //----------------------------
+
+ // Byte-at-a-time scan
+ //----------------------------
+ const uint8* Tbl = Tbl_0;
+ while (src < srclimit) {
+ c = *src;
+ e = Tbl[c];
+ src++;
+ if (e >= kExitIllegalStructure) {break;}
+ Tbl = &Tbl_0[e << eshift];
+ }
+ //----------------------------
+
+
+ // Exit posibilities:
+ // Some exit code, !state0, back up over last char
+ // Some exit code, state0, back up one byte exactly
+ // source consumed, !state0, back up over partial char
+ // source consumed, state0, exit OK
+ // For illegal byte in state0, avoid backup up over PREVIOUS char
+ // For truncated last char, back up to beginning of it
+
+ if (e >= kExitIllegalStructure) {
+ // Back up over exactly one byte of rejected/illegal UTF-8 character
+ src--;
+ // Back up more if needed
+ if (!InStateZero(st, Tbl)) {
+ do {
+ src--;
+ } while ((src > isrc) && ((src[0] & 0xc0) == 0x80));
+ }
+ } else if (!InStateZero(st, Tbl)) {
+ // Back up over truncated UTF-8 character
+ e = kExitIllegalStructure;
+ do {
+ src--;
+ } while ((src > isrc) && ((src[0] & 0xc0) == 0x80));
+ } else {
+ // Normal termination, source fully consumed
+ e = kExitOK;
+ }
+
+ if (e == kExitDoAgain) {
+ // Loop back up to the fast scan
+ goto DoAgain;
+ }
+
+ *bytes_consumed = src - isrc;
+ return e;
+}
+
+int UTF8GenericScanFastAscii(const UTF8ScanObj* st,
+ const char * str,
+ int str_length,
+ int* bytes_consumed) {
+ *bytes_consumed = 0;
+ if (str_length == 0) return kExitOK;
+
+ const uint8* isrc = reinterpret_cast<const uint8*>(str);
+ const uint8* src = isrc;
+ const uint8* srclimit = isrc + str_length;
+ const uint8* srclimit8 = srclimit - 7;
+ int n;
+ int rest_consumed;
+ int exit_reason;
+ do {
+ // Check initial few bytes one at a time until 8-byte aligned
+ while ((((uintptr_t)src & 0x07) != 0) &&
+ (src < srclimit) && (src[0] < 0x80)) {
+ src++;
+ }
+ if (((uintptr_t)src & 0x07) == 0) {
+ while ((src < srclimit8) &&
+ (((reinterpret_cast<const uint32*>(src)[0] |
+ reinterpret_cast<const uint32*>(src)[1]) & 0x80808080) == 0)) {
+ src += 8;
+ }
+ }
+ while ((src < srclimit) && (src[0] < 0x80)) {
+ src++;
+ }
+ // Run state table on the rest
+ n = src - isrc;
+ exit_reason = UTF8GenericScan(st, str + n, str_length - n, &rest_consumed);
+ src += rest_consumed;
+ } while ( exit_reason == kExitDoAgain );
+
+ *bytes_consumed = src - isrc;
+ return exit_reason;
+}
+
+// Hack: On some compilers the static tables are initialized at startup.
+// We can't use them until they are initialized. However, some Protocol
+// Buffer parsing happens at static init time and may try to validate
+// UTF-8 strings. Since UTF-8 validation is only used for debugging
+// anyway, we simply always return success if initialization hasn't
+// occurred yet.
+namespace {
+
+bool module_initialized_ = false;
+
+struct InitDetector {
+ InitDetector() {
+ module_initialized_ = true;
+ }
+};
+InitDetector init_detector;
+
+} // namespace
+
+bool IsStructurallyValidUTF8(const char* buf, int len) {
+ if (!module_initialized_) return true;
+
+ int bytes_consumed = 0;
+ UTF8GenericScanFastAscii(&utf8acceptnonsurrogates_obj,
+ buf, len, &bytes_consumed);
+ return (bytes_consumed == len);
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.cc
new file mode 100644
index 0000000000..d7f673d10a
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.cc
@@ -0,0 +1,1280 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/strings/strutil.cc
+
+#include <google/protobuf/stubs/strutil.h>
+#include <errno.h>
+#include <float.h> // FLT_DIG and DBL_DIG
+#include <limits>
+#include <limits.h>
+#include <stdio.h>
+#include <iterator>
+
+#include "mozilla/FloatingPoint.h"
+
+#ifdef _WIN32
+// MSVC has only _snprintf, not snprintf.
+//
+// MinGW has both snprintf and _snprintf, but they appear to be different
+// functions. The former is buggy. When invoked like so:
+// char buffer[32];
+// snprintf(buffer, 32, "%.*g\n", FLT_DIG, 1.23e10f);
+// it prints "1.23000e+10". This is plainly wrong: %g should never print
+// trailing zeros after the decimal point. For some reason this bug only
+// occurs with some input values, not all. In any case, _snprintf does the
+// right thing, so we use it.
+#define snprintf _snprintf
+#endif
+
+namespace google {
+namespace protobuf {
+
+inline bool IsNaN(double value) {
+ return ::mozilla::IsNaN(value);
+}
+
+// These are defined as macros on some platforms. #undef them so that we can
+// redefine them.
+#undef isxdigit
+#undef isprint
+
+// The definitions of these in ctype.h change based on locale. Since our
+// string manipulation is all in relation to the protocol buffer and C++
+// languages, we always want to use the C locale. So, we re-define these
+// exactly as we want them.
+inline bool isxdigit(char c) {
+ return ('0' <= c && c <= '9') ||
+ ('a' <= c && c <= 'f') ||
+ ('A' <= c && c <= 'F');
+}
+
+inline bool isprint(char c) {
+ return c >= 0x20 && c <= 0x7E;
+}
+
+// ----------------------------------------------------------------------
+// StripString
+// Replaces any occurrence of the character 'remove' (or the characters
+// in 'remove') with the character 'replacewith'.
+// ----------------------------------------------------------------------
+void StripString(string* s, const char* remove, char replacewith) {
+ const char * str_start = s->c_str();
+ const char * str = str_start;
+ for (str = strpbrk(str, remove);
+ str != NULL;
+ str = strpbrk(str + 1, remove)) {
+ (*s)[str - str_start] = replacewith;
+ }
+}
+
+// ----------------------------------------------------------------------
+// StringReplace()
+// Replace the "old" pattern with the "new" pattern in a string,
+// and append the result to "res". If replace_all is false,
+// it only replaces the first instance of "old."
+// ----------------------------------------------------------------------
+
+void StringReplace(const string& s, const string& oldsub,
+ const string& newsub, bool replace_all,
+ string* res) {
+ if (oldsub.empty()) {
+ res->append(s); // if empty, append the given string.
+ return;
+ }
+
+ string::size_type start_pos = 0;
+ string::size_type pos;
+ do {
+ pos = s.find(oldsub, start_pos);
+ if (pos == string::npos) {
+ break;
+ }
+ res->append(s, start_pos, pos - start_pos);
+ res->append(newsub);
+ start_pos = pos + oldsub.size(); // start searching again after the "old"
+ } while (replace_all);
+ res->append(s, start_pos, s.length() - start_pos);
+}
+
+// ----------------------------------------------------------------------
+// StringReplace()
+// Give me a string and two patterns "old" and "new", and I replace
+// the first instance of "old" in the string with "new", if it
+// exists. If "global" is true; call this repeatedly until it
+// fails. RETURN a new string, regardless of whether the replacement
+// happened or not.
+// ----------------------------------------------------------------------
+
+string StringReplace(const string& s, const string& oldsub,
+ const string& newsub, bool replace_all) {
+ string ret;
+ StringReplace(s, oldsub, newsub, replace_all, &ret);
+ return ret;
+}
+
+// ----------------------------------------------------------------------
+// SplitStringUsing()
+// Split a string using a character delimiter. Append the components
+// to 'result'.
+//
+// Note: For multi-character delimiters, this routine will split on *ANY* of
+// the characters in the string, not the entire string as a single delimiter.
+// ----------------------------------------------------------------------
+template <typename ITR>
+static inline
+void SplitStringToIteratorUsing(const string& full,
+ const char* delim,
+ ITR& result) {
+ // Optimize the common case where delim is a single character.
+ if (delim[0] != '\0' && delim[1] == '\0') {
+ char c = delim[0];
+ const char* p = full.data();
+ const char* end = p + full.size();
+ while (p != end) {
+ if (*p == c) {
+ ++p;
+ } else {
+ const char* start = p;
+ while (++p != end && *p != c);
+ *result++ = string(start, p - start);
+ }
+ }
+ return;
+ }
+
+ string::size_type begin_index, end_index;
+ begin_index = full.find_first_not_of(delim);
+ while (begin_index != string::npos) {
+ end_index = full.find_first_of(delim, begin_index);
+ if (end_index == string::npos) {
+ *result++ = full.substr(begin_index);
+ return;
+ }
+ *result++ = full.substr(begin_index, (end_index - begin_index));
+ begin_index = full.find_first_not_of(delim, end_index);
+ }
+}
+
+void SplitStringUsing(const string& full,
+ const char* delim,
+ vector<string>* result) {
+ back_insert_iterator< vector<string> > it(*result);
+ SplitStringToIteratorUsing(full, delim, it);
+}
+
+// Split a string using a character delimiter. Append the components
+// to 'result'. If there are consecutive delimiters, this function
+// will return corresponding empty strings. The string is split into
+// at most the specified number of pieces greedily. This means that the
+// last piece may possibly be split further. To split into as many pieces
+// as possible, specify 0 as the number of pieces.
+//
+// If "full" is the empty string, yields an empty string as the only value.
+//
+// If "pieces" is negative for some reason, it returns the whole string
+// ----------------------------------------------------------------------
+template <typename StringType, typename ITR>
+static inline
+void SplitStringToIteratorAllowEmpty(const StringType& full,
+ const char* delim,
+ int pieces,
+ ITR& result) {
+ string::size_type begin_index, end_index;
+ begin_index = 0;
+
+ for (int i = 0; (i < pieces-1) || (pieces == 0); i++) {
+ end_index = full.find_first_of(delim, begin_index);
+ if (end_index == string::npos) {
+ *result++ = full.substr(begin_index);
+ return;
+ }
+ *result++ = full.substr(begin_index, (end_index - begin_index));
+ begin_index = end_index + 1;
+ }
+ *result++ = full.substr(begin_index);
+}
+
+void SplitStringAllowEmpty(const string& full, const char* delim,
+ vector<string>* result) {
+ back_insert_iterator<vector<string> > it(*result);
+ SplitStringToIteratorAllowEmpty(full, delim, 0, it);
+}
+
+// ----------------------------------------------------------------------
+// JoinStrings()
+// This merges a vector of string components with delim inserted
+// as separaters between components.
+//
+// ----------------------------------------------------------------------
+template <class ITERATOR>
+static void JoinStringsIterator(const ITERATOR& start,
+ const ITERATOR& end,
+ const char* delim,
+ string* result) {
+ GOOGLE_CHECK(result != NULL);
+ result->clear();
+ int delim_length = strlen(delim);
+
+ // Precompute resulting length so we can reserve() memory in one shot.
+ int length = 0;
+ for (ITERATOR iter = start; iter != end; ++iter) {
+ if (iter != start) {
+ length += delim_length;
+ }
+ length += iter->size();
+ }
+ result->reserve(length);
+
+ // Now combine everything.
+ for (ITERATOR iter = start; iter != end; ++iter) {
+ if (iter != start) {
+ result->append(delim, delim_length);
+ }
+ result->append(iter->data(), iter->size());
+ }
+}
+
+void JoinStrings(const vector<string>& components,
+ const char* delim,
+ string * result) {
+ JoinStringsIterator(components.begin(), components.end(), delim, result);
+}
+
+// ----------------------------------------------------------------------
+// UnescapeCEscapeSequences()
+// This does all the unescaping that C does: \ooo, \r, \n, etc
+// Returns length of resulting string.
+// The implementation of \x parses any positive number of hex digits,
+// but it is an error if the value requires more than 8 bits, and the
+// result is truncated to 8 bits.
+//
+// The second call stores its errors in a supplied string vector.
+// If the string vector pointer is NULL, it reports the errors with LOG().
+// ----------------------------------------------------------------------
+
+#define IS_OCTAL_DIGIT(c) (((c) >= '0') && ((c) <= '7'))
+
+inline int hex_digit_to_int(char c) {
+ /* Assume ASCII. */
+ assert('0' == 0x30 && 'A' == 0x41 && 'a' == 0x61);
+ assert(isxdigit(c));
+ int x = static_cast<unsigned char>(c);
+ if (x > '9') {
+ x += 9;
+ }
+ return x & 0xf;
+}
+
+// Protocol buffers doesn't ever care about errors, but I don't want to remove
+// the code.
+#define LOG_STRING(LEVEL, VECTOR) GOOGLE_LOG_IF(LEVEL, false)
+
+int UnescapeCEscapeSequences(const char* source, char* dest) {
+ return UnescapeCEscapeSequences(source, dest, NULL);
+}
+
+int UnescapeCEscapeSequences(const char* source, char* dest,
+ vector<string> *errors) {
+ GOOGLE_DCHECK(errors == NULL) << "Error reporting not implemented.";
+
+ char* d = dest;
+ const char* p = source;
+
+ // Small optimization for case where source = dest and there's no escaping
+ while ( p == d && *p != '\0' && *p != '\\' )
+ p++, d++;
+
+ while (*p != '\0') {
+ if (*p != '\\') {
+ *d++ = *p++;
+ } else {
+ switch ( *++p ) { // skip past the '\\'
+ case '\0':
+ LOG_STRING(ERROR, errors) << "String cannot end with \\";
+ *d = '\0';
+ return d - dest; // we're done with p
+ case 'a': *d++ = '\a'; break;
+ case 'b': *d++ = '\b'; break;
+ case 'f': *d++ = '\f'; break;
+ case 'n': *d++ = '\n'; break;
+ case 'r': *d++ = '\r'; break;
+ case 't': *d++ = '\t'; break;
+ case 'v': *d++ = '\v'; break;
+ case '\\': *d++ = '\\'; break;
+ case '?': *d++ = '\?'; break; // \? Who knew?
+ case '\'': *d++ = '\''; break;
+ case '"': *d++ = '\"'; break;
+ case '0': case '1': case '2': case '3': // octal digit: 1 to 3 digits
+ case '4': case '5': case '6': case '7': {
+ char ch = *p - '0';
+ if ( IS_OCTAL_DIGIT(p[1]) )
+ ch = ch * 8 + *++p - '0';
+ if ( IS_OCTAL_DIGIT(p[1]) ) // safe (and easy) to do this twice
+ ch = ch * 8 + *++p - '0'; // now points at last digit
+ *d++ = ch;
+ break;
+ }
+ case 'x': case 'X': {
+ if (!isxdigit(p[1])) {
+ if (p[1] == '\0') {
+ LOG_STRING(ERROR, errors) << "String cannot end with \\x";
+ } else {
+ LOG_STRING(ERROR, errors) <<
+ "\\x cannot be followed by non-hex digit: \\" << *p << p[1];
+ }
+ break;
+ }
+ unsigned int ch = 0;
+ const char *hex_start = p;
+ while (isxdigit(p[1])) // arbitrarily many hex digits
+ ch = (ch << 4) + hex_digit_to_int(*++p);
+ if (ch > 0xFF)
+ LOG_STRING(ERROR, errors) << "Value of " <<
+ "\\" << string(hex_start, p+1-hex_start) << " exceeds 8 bits";
+ *d++ = ch;
+ break;
+ }
+#if 0 // TODO(kenton): Support \u and \U? Requires runetochar().
+ case 'u': {
+ // \uhhhh => convert 4 hex digits to UTF-8
+ char32 rune = 0;
+ const char *hex_start = p;
+ for (int i = 0; i < 4; ++i) {
+ if (isxdigit(p[1])) { // Look one char ahead.
+ rune = (rune << 4) + hex_digit_to_int(*++p); // Advance p.
+ } else {
+ LOG_STRING(ERROR, errors)
+ << "\\u must be followed by 4 hex digits: \\"
+ << string(hex_start, p+1-hex_start);
+ break;
+ }
+ }
+ d += runetochar(d, &rune);
+ break;
+ }
+ case 'U': {
+ // \Uhhhhhhhh => convert 8 hex digits to UTF-8
+ char32 rune = 0;
+ const char *hex_start = p;
+ for (int i = 0; i < 8; ++i) {
+ if (isxdigit(p[1])) { // Look one char ahead.
+ // Don't change rune until we're sure this
+ // is within the Unicode limit, but do advance p.
+ char32 newrune = (rune << 4) + hex_digit_to_int(*++p);
+ if (newrune > 0x10FFFF) {
+ LOG_STRING(ERROR, errors)
+ << "Value of \\"
+ << string(hex_start, p + 1 - hex_start)
+ << " exceeds Unicode limit (0x10FFFF)";
+ break;
+ } else {
+ rune = newrune;
+ }
+ } else {
+ LOG_STRING(ERROR, errors)
+ << "\\U must be followed by 8 hex digits: \\"
+ << string(hex_start, p+1-hex_start);
+ break;
+ }
+ }
+ d += runetochar(d, &rune);
+ break;
+ }
+#endif
+ default:
+ LOG_STRING(ERROR, errors) << "Unknown escape sequence: \\" << *p;
+ }
+ p++; // read past letter we escaped
+ }
+ }
+ *d = '\0';
+ return d - dest;
+}
+
+// ----------------------------------------------------------------------
+// UnescapeCEscapeString()
+// This does the same thing as UnescapeCEscapeSequences, but creates
+// a new string. The caller does not need to worry about allocating
+// a dest buffer. This should be used for non performance critical
+// tasks such as printing debug messages. It is safe for src and dest
+// to be the same.
+//
+// The second call stores its errors in a supplied string vector.
+// If the string vector pointer is NULL, it reports the errors with LOG().
+//
+// In the first and second calls, the length of dest is returned. In the
+// the third call, the new string is returned.
+// ----------------------------------------------------------------------
+int UnescapeCEscapeString(const string& src, string* dest) {
+ return UnescapeCEscapeString(src, dest, NULL);
+}
+
+int UnescapeCEscapeString(const string& src, string* dest,
+ vector<string> *errors) {
+ scoped_array<char> unescaped(new char[src.size() + 1]);
+ int len = UnescapeCEscapeSequences(src.c_str(), unescaped.get(), errors);
+ GOOGLE_CHECK(dest);
+ dest->assign(unescaped.get(), len);
+ return len;
+}
+
+string UnescapeCEscapeString(const string& src) {
+ scoped_array<char> unescaped(new char[src.size() + 1]);
+ int len = UnescapeCEscapeSequences(src.c_str(), unescaped.get(), NULL);
+ return string(unescaped.get(), len);
+}
+
+// ----------------------------------------------------------------------
+// CEscapeString()
+// CHexEscapeString()
+// Copies 'src' to 'dest', escaping dangerous characters using
+// C-style escape sequences. This is very useful for preparing query
+// flags. 'src' and 'dest' should not overlap. The 'Hex' version uses
+// hexadecimal rather than octal sequences.
+// Returns the number of bytes written to 'dest' (not including the \0)
+// or -1 if there was insufficient space.
+//
+// Currently only \n, \r, \t, ", ', \ and !isprint() chars are escaped.
+// ----------------------------------------------------------------------
+int CEscapeInternal(const char* src, int src_len, char* dest,
+ int dest_len, bool use_hex, bool utf8_safe) {
+ const char* src_end = src + src_len;
+ int used = 0;
+ bool last_hex_escape = false; // true if last output char was \xNN
+
+ for (; src < src_end; src++) {
+ if (dest_len - used < 2) // Need space for two letter escape
+ return -1;
+
+ bool is_hex_escape = false;
+ switch (*src) {
+ case '\n': dest[used++] = '\\'; dest[used++] = 'n'; break;
+ case '\r': dest[used++] = '\\'; dest[used++] = 'r'; break;
+ case '\t': dest[used++] = '\\'; dest[used++] = 't'; break;
+ case '\"': dest[used++] = '\\'; dest[used++] = '\"'; break;
+ case '\'': dest[used++] = '\\'; dest[used++] = '\''; break;
+ case '\\': dest[used++] = '\\'; dest[used++] = '\\'; break;
+ default:
+ // Note that if we emit \xNN and the src character after that is a hex
+ // digit then that digit must be escaped too to prevent it being
+ // interpreted as part of the character code by C.
+ if ((!utf8_safe || static_cast<uint8>(*src) < 0x80) &&
+ (!isprint(*src) ||
+ (last_hex_escape && isxdigit(*src)))) {
+ if (dest_len - used < 4) // need space for 4 letter escape
+ return -1;
+ sprintf(dest + used, (use_hex ? "\\x%02x" : "\\%03o"),
+ static_cast<uint8>(*src));
+ is_hex_escape = use_hex;
+ used += 4;
+ } else {
+ dest[used++] = *src; break;
+ }
+ }
+ last_hex_escape = is_hex_escape;
+ }
+
+ if (dest_len - used < 1) // make sure that there is room for \0
+ return -1;
+
+ dest[used] = '\0'; // doesn't count towards return value though
+ return used;
+}
+
+int CEscapeString(const char* src, int src_len, char* dest, int dest_len) {
+ return CEscapeInternal(src, src_len, dest, dest_len, false, false);
+}
+
+// ----------------------------------------------------------------------
+// CEscape()
+// CHexEscape()
+// Copies 'src' to result, escaping dangerous characters using
+// C-style escape sequences. This is very useful for preparing query
+// flags. 'src' and 'dest' should not overlap. The 'Hex' version
+// hexadecimal rather than octal sequences.
+//
+// Currently only \n, \r, \t, ", ', \ and !isprint() chars are escaped.
+// ----------------------------------------------------------------------
+string CEscape(const string& src) {
+ const int dest_length = src.size() * 4 + 1; // Maximum possible expansion
+ scoped_array<char> dest(new char[dest_length]);
+ const int len = CEscapeInternal(src.data(), src.size(),
+ dest.get(), dest_length, false, false);
+ GOOGLE_DCHECK_GE(len, 0);
+ return string(dest.get(), len);
+}
+
+namespace strings {
+
+string Utf8SafeCEscape(const string& src) {
+ const int dest_length = src.size() * 4 + 1; // Maximum possible expansion
+ scoped_array<char> dest(new char[dest_length]);
+ const int len = CEscapeInternal(src.data(), src.size(),
+ dest.get(), dest_length, false, true);
+ GOOGLE_DCHECK_GE(len, 0);
+ return string(dest.get(), len);
+}
+
+string CHexEscape(const string& src) {
+ const int dest_length = src.size() * 4 + 1; // Maximum possible expansion
+ scoped_array<char> dest(new char[dest_length]);
+ const int len = CEscapeInternal(src.data(), src.size(),
+ dest.get(), dest_length, true, false);
+ GOOGLE_DCHECK_GE(len, 0);
+ return string(dest.get(), len);
+}
+
+} // namespace strings
+
+// ----------------------------------------------------------------------
+// strto32_adaptor()
+// strtou32_adaptor()
+// Implementation of strto[u]l replacements that have identical
+// overflow and underflow characteristics for both ILP-32 and LP-64
+// platforms, including errno preservation in error-free calls.
+// ----------------------------------------------------------------------
+
+int32 strto32_adaptor(const char *nptr, char **endptr, int base) {
+ const int saved_errno = errno;
+ errno = 0;
+ const long result = strtol(nptr, endptr, base);
+ if (errno == ERANGE && result == LONG_MIN) {
+ return kint32min;
+ } else if (errno == ERANGE && result == LONG_MAX) {
+ return kint32max;
+ } else if (errno == 0 && result < kint32min) {
+ errno = ERANGE;
+ return kint32min;
+ } else if (errno == 0 && result > kint32max) {
+ errno = ERANGE;
+ return kint32max;
+ }
+ if (errno == 0)
+ errno = saved_errno;
+ return static_cast<int32>(result);
+}
+
+uint32 strtou32_adaptor(const char *nptr, char **endptr, int base) {
+ const int saved_errno = errno;
+ errno = 0;
+ const unsigned long result = strtoul(nptr, endptr, base);
+ if (errno == ERANGE && result == ULONG_MAX) {
+ return kuint32max;
+ } else if (errno == 0 && result > kuint32max) {
+ errno = ERANGE;
+ return kuint32max;
+ }
+ if (errno == 0)
+ errno = saved_errno;
+ return static_cast<uint32>(result);
+}
+
+inline bool safe_parse_sign(string* text /*inout*/,
+ bool* negative_ptr /*output*/) {
+ const char* start = text->data();
+ const char* end = start + text->size();
+
+ // Consume whitespace.
+ while (start < end && (start[0] == ' ')) {
+ ++start;
+ }
+ while (start < end && (end[-1] == ' ')) {
+ --end;
+ }
+ if (start >= end) {
+ return false;
+ }
+
+ // Consume sign.
+ *negative_ptr = (start[0] == '-');
+ if (*negative_ptr || start[0] == '+') {
+ ++start;
+ if (start >= end) {
+ return false;
+ }
+ }
+ *text = text->substr(start - text->data(), end - start);
+ return true;
+}
+
+inline bool safe_parse_positive_int(
+ string text, int32* value_p) {
+ int base = 10;
+ int32 value = 0;
+ const int32 vmax = std::numeric_limits<int32>::max();
+ assert(vmax > 0);
+ assert(vmax >= base);
+ const int32 vmax_over_base = vmax / base;
+ const char* start = text.data();
+ const char* end = start + text.size();
+ // loop over digits
+ for (; start < end; ++start) {
+ unsigned char c = static_cast<unsigned char>(start[0]);
+ int digit = c - '0';
+ if (digit >= base || digit < 0) {
+ *value_p = value;
+ return false;
+ }
+ if (value > vmax_over_base) {
+ *value_p = vmax;
+ return false;
+ }
+ value *= base;
+ if (value > vmax - digit) {
+ *value_p = vmax;
+ return false;
+ }
+ value += digit;
+ }
+ *value_p = value;
+ return true;
+}
+
+inline bool safe_parse_negative_int(
+ string text, int32* value_p) {
+ int base = 10;
+ int32 value = 0;
+ const int32 vmin = std::numeric_limits<int32>::min();
+ assert(vmin < 0);
+ assert(vmin <= 0 - base);
+ int32 vmin_over_base = vmin / base;
+ // 2003 c++ standard [expr.mul]
+ // "... the sign of the remainder is implementation-defined."
+ // Although (vmin/base)*base + vmin%base is always vmin.
+ // 2011 c++ standard tightens the spec but we cannot rely on it.
+ if (vmin % base > 0) {
+ vmin_over_base += 1;
+ }
+ const char* start = text.data();
+ const char* end = start + text.size();
+ // loop over digits
+ for (; start < end; ++start) {
+ unsigned char c = static_cast<unsigned char>(start[0]);
+ int digit = c - '0';
+ if (digit >= base || digit < 0) {
+ *value_p = value;
+ return false;
+ }
+ if (value < vmin_over_base) {
+ *value_p = vmin;
+ return false;
+ }
+ value *= base;
+ if (value < vmin + digit) {
+ *value_p = vmin;
+ return false;
+ }
+ value -= digit;
+ }
+ *value_p = value;
+ return true;
+}
+
+bool safe_int(string text, int32* value_p) {
+ *value_p = 0;
+ bool negative;
+ if (!safe_parse_sign(&text, &negative)) {
+ return false;
+ }
+ if (!negative) {
+ return safe_parse_positive_int(text, value_p);
+ } else {
+ return safe_parse_negative_int(text, value_p);
+ }
+}
+
+// ----------------------------------------------------------------------
+// FastIntToBuffer()
+// FastInt64ToBuffer()
+// FastHexToBuffer()
+// FastHex64ToBuffer()
+// FastHex32ToBuffer()
+// ----------------------------------------------------------------------
+
+// Offset into buffer where FastInt64ToBuffer places the end of string
+// null character. Also used by FastInt64ToBufferLeft.
+static const int kFastInt64ToBufferOffset = 21;
+
+char *FastInt64ToBuffer(int64 i, char* buffer) {
+ // We could collapse the positive and negative sections, but that
+ // would be slightly slower for positive numbers...
+ // 22 bytes is enough to store -2**64, -18446744073709551616.
+ char* p = buffer + kFastInt64ToBufferOffset;
+ *p-- = '\0';
+ if (i >= 0) {
+ do {
+ *p-- = '0' + i % 10;
+ i /= 10;
+ } while (i > 0);
+ return p + 1;
+ } else {
+ // On different platforms, % and / have different behaviors for
+ // negative numbers, so we need to jump through hoops to make sure
+ // we don't divide negative numbers.
+ if (i > -10) {
+ i = -i;
+ *p-- = '0' + i;
+ *p = '-';
+ return p;
+ } else {
+ // Make sure we aren't at MIN_INT, in which case we can't say i = -i
+ i = i + 10;
+ i = -i;
+ *p-- = '0' + i % 10;
+ // Undo what we did a moment ago
+ i = i / 10 + 1;
+ do {
+ *p-- = '0' + i % 10;
+ i /= 10;
+ } while (i > 0);
+ *p = '-';
+ return p;
+ }
+ }
+}
+
+// Offset into buffer where FastInt32ToBuffer places the end of string
+// null character. Also used by FastInt32ToBufferLeft
+static const int kFastInt32ToBufferOffset = 11;
+
+// Yes, this is a duplicate of FastInt64ToBuffer. But, we need this for the
+// compiler to generate 32 bit arithmetic instructions. It's much faster, at
+// least with 32 bit binaries.
+char *FastInt32ToBuffer(int32 i, char* buffer) {
+ // We could collapse the positive and negative sections, but that
+ // would be slightly slower for positive numbers...
+ // 12 bytes is enough to store -2**32, -4294967296.
+ char* p = buffer + kFastInt32ToBufferOffset;
+ *p-- = '\0';
+ if (i >= 0) {
+ do {
+ *p-- = '0' + i % 10;
+ i /= 10;
+ } while (i > 0);
+ return p + 1;
+ } else {
+ // On different platforms, % and / have different behaviors for
+ // negative numbers, so we need to jump through hoops to make sure
+ // we don't divide negative numbers.
+ if (i > -10) {
+ i = -i;
+ *p-- = '0' + i;
+ *p = '-';
+ return p;
+ } else {
+ // Make sure we aren't at MIN_INT, in which case we can't say i = -i
+ i = i + 10;
+ i = -i;
+ *p-- = '0' + i % 10;
+ // Undo what we did a moment ago
+ i = i / 10 + 1;
+ do {
+ *p-- = '0' + i % 10;
+ i /= 10;
+ } while (i > 0);
+ *p = '-';
+ return p;
+ }
+ }
+}
+
+char *FastHexToBuffer(int i, char* buffer) {
+ GOOGLE_CHECK(i >= 0) << "FastHexToBuffer() wants non-negative integers, not " << i;
+
+ static const char *hexdigits = "0123456789abcdef";
+ char *p = buffer + 21;
+ *p-- = '\0';
+ do {
+ *p-- = hexdigits[i & 15]; // mod by 16
+ i >>= 4; // divide by 16
+ } while (i > 0);
+ return p + 1;
+}
+
+char *InternalFastHexToBuffer(uint64 value, char* buffer, int num_byte) {
+ static const char *hexdigits = "0123456789abcdef";
+ buffer[num_byte] = '\0';
+ for (int i = num_byte - 1; i >= 0; i--) {
+#ifdef _M_X64
+ // MSVC x64 platform has a bug optimizing the uint32(value) in the #else
+ // block. Given that the uint32 cast was to improve performance on 32-bit
+ // platforms, we use 64-bit '&' directly.
+ buffer[i] = hexdigits[value & 0xf];
+#else
+ buffer[i] = hexdigits[uint32(value) & 0xf];
+#endif
+ value >>= 4;
+ }
+ return buffer;
+}
+
+char *FastHex64ToBuffer(uint64 value, char* buffer) {
+ return InternalFastHexToBuffer(value, buffer, 16);
+}
+
+char *FastHex32ToBuffer(uint32 value, char* buffer) {
+ return InternalFastHexToBuffer(value, buffer, 8);
+}
+
+static inline char* PlaceNum(char* p, int num, char prev_sep) {
+ *p-- = '0' + num % 10;
+ *p-- = '0' + num / 10;
+ *p-- = prev_sep;
+ return p;
+}
+
+// ----------------------------------------------------------------------
+// FastInt32ToBufferLeft()
+// FastUInt32ToBufferLeft()
+// FastInt64ToBufferLeft()
+// FastUInt64ToBufferLeft()
+//
+// Like the Fast*ToBuffer() functions above, these are intended for speed.
+// Unlike the Fast*ToBuffer() functions, however, these functions write
+// their output to the beginning of the buffer (hence the name, as the
+// output is left-aligned). The caller is responsible for ensuring that
+// the buffer has enough space to hold the output.
+//
+// Returns a pointer to the end of the string (i.e. the null character
+// terminating the string).
+// ----------------------------------------------------------------------
+
+static const char two_ASCII_digits[100][2] = {
+ {'0','0'}, {'0','1'}, {'0','2'}, {'0','3'}, {'0','4'},
+ {'0','5'}, {'0','6'}, {'0','7'}, {'0','8'}, {'0','9'},
+ {'1','0'}, {'1','1'}, {'1','2'}, {'1','3'}, {'1','4'},
+ {'1','5'}, {'1','6'}, {'1','7'}, {'1','8'}, {'1','9'},
+ {'2','0'}, {'2','1'}, {'2','2'}, {'2','3'}, {'2','4'},
+ {'2','5'}, {'2','6'}, {'2','7'}, {'2','8'}, {'2','9'},
+ {'3','0'}, {'3','1'}, {'3','2'}, {'3','3'}, {'3','4'},
+ {'3','5'}, {'3','6'}, {'3','7'}, {'3','8'}, {'3','9'},
+ {'4','0'}, {'4','1'}, {'4','2'}, {'4','3'}, {'4','4'},
+ {'4','5'}, {'4','6'}, {'4','7'}, {'4','8'}, {'4','9'},
+ {'5','0'}, {'5','1'}, {'5','2'}, {'5','3'}, {'5','4'},
+ {'5','5'}, {'5','6'}, {'5','7'}, {'5','8'}, {'5','9'},
+ {'6','0'}, {'6','1'}, {'6','2'}, {'6','3'}, {'6','4'},
+ {'6','5'}, {'6','6'}, {'6','7'}, {'6','8'}, {'6','9'},
+ {'7','0'}, {'7','1'}, {'7','2'}, {'7','3'}, {'7','4'},
+ {'7','5'}, {'7','6'}, {'7','7'}, {'7','8'}, {'7','9'},
+ {'8','0'}, {'8','1'}, {'8','2'}, {'8','3'}, {'8','4'},
+ {'8','5'}, {'8','6'}, {'8','7'}, {'8','8'}, {'8','9'},
+ {'9','0'}, {'9','1'}, {'9','2'}, {'9','3'}, {'9','4'},
+ {'9','5'}, {'9','6'}, {'9','7'}, {'9','8'}, {'9','9'}
+};
+
+char* FastUInt32ToBufferLeft(uint32 u, char* buffer) {
+ int digits;
+ const char *ASCII_digits = NULL;
+ // The idea of this implementation is to trim the number of divides to as few
+ // as possible by using multiplication and subtraction rather than mod (%),
+ // and by outputting two digits at a time rather than one.
+ // The huge-number case is first, in the hopes that the compiler will output
+ // that case in one branch-free block of code, and only output conditional
+ // branches into it from below.
+ if (u >= 1000000000) { // >= 1,000,000,000
+ digits = u / 100000000; // 100,000,000
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+sublt100_000_000:
+ u -= digits * 100000000; // 100,000,000
+lt100_000_000:
+ digits = u / 1000000; // 1,000,000
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+sublt1_000_000:
+ u -= digits * 1000000; // 1,000,000
+lt1_000_000:
+ digits = u / 10000; // 10,000
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+sublt10_000:
+ u -= digits * 10000; // 10,000
+lt10_000:
+ digits = u / 100;
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+sublt100:
+ u -= digits * 100;
+lt100:
+ digits = u;
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+done:
+ *buffer = 0;
+ return buffer;
+ }
+
+ if (u < 100) {
+ digits = u;
+ if (u >= 10) goto lt100;
+ *buffer++ = '0' + digits;
+ goto done;
+ }
+ if (u < 10000) { // 10,000
+ if (u >= 1000) goto lt10_000;
+ digits = u / 100;
+ *buffer++ = '0' + digits;
+ goto sublt100;
+ }
+ if (u < 1000000) { // 1,000,000
+ if (u >= 100000) goto lt1_000_000;
+ digits = u / 10000; // 10,000
+ *buffer++ = '0' + digits;
+ goto sublt10_000;
+ }
+ if (u < 100000000) { // 100,000,000
+ if (u >= 10000000) goto lt100_000_000;
+ digits = u / 1000000; // 1,000,000
+ *buffer++ = '0' + digits;
+ goto sublt1_000_000;
+ }
+ // we already know that u < 1,000,000,000
+ digits = u / 100000000; // 100,000,000
+ *buffer++ = '0' + digits;
+ goto sublt100_000_000;
+}
+
+char* FastInt32ToBufferLeft(int32 i, char* buffer) {
+ uint32 u = i;
+ if (i < 0) {
+ *buffer++ = '-';
+ u = -i;
+ }
+ return FastUInt32ToBufferLeft(u, buffer);
+}
+
+char* FastUInt64ToBufferLeft(uint64 u64, char* buffer) {
+ int digits;
+ const char *ASCII_digits = NULL;
+
+ uint32 u = static_cast<uint32>(u64);
+ if (u == u64) return FastUInt32ToBufferLeft(u, buffer);
+
+ uint64 top_11_digits = u64 / 1000000000;
+ buffer = FastUInt64ToBufferLeft(top_11_digits, buffer);
+ u = u64 - (top_11_digits * 1000000000);
+
+ digits = u / 10000000; // 10,000,000
+ GOOGLE_DCHECK_LT(digits, 100);
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+ u -= digits * 10000000; // 10,000,000
+ digits = u / 100000; // 100,000
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+ u -= digits * 100000; // 100,000
+ digits = u / 1000; // 1,000
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+ u -= digits * 1000; // 1,000
+ digits = u / 10;
+ ASCII_digits = two_ASCII_digits[digits];
+ buffer[0] = ASCII_digits[0];
+ buffer[1] = ASCII_digits[1];
+ buffer += 2;
+ u -= digits * 10;
+ digits = u;
+ *buffer++ = '0' + digits;
+ *buffer = 0;
+ return buffer;
+}
+
+char* FastInt64ToBufferLeft(int64 i, char* buffer) {
+ uint64 u = i;
+ if (i < 0) {
+ *buffer++ = '-';
+ u = -i;
+ }
+ return FastUInt64ToBufferLeft(u, buffer);
+}
+
+// ----------------------------------------------------------------------
+// SimpleItoa()
+// Description: converts an integer to a string.
+//
+// Return value: string
+// ----------------------------------------------------------------------
+
+string SimpleItoa(int i) {
+ char buffer[kFastToBufferSize];
+ return (sizeof(i) == 4) ?
+ FastInt32ToBuffer(i, buffer) :
+ FastInt64ToBuffer(i, buffer);
+}
+
+string SimpleItoa(unsigned int i) {
+ char buffer[kFastToBufferSize];
+ return string(buffer, (sizeof(i) == 4) ?
+ FastUInt32ToBufferLeft(i, buffer) :
+ FastUInt64ToBufferLeft(i, buffer));
+}
+
+string SimpleItoa(long i) {
+ char buffer[kFastToBufferSize];
+ return (sizeof(i) == 4) ?
+ FastInt32ToBuffer(i, buffer) :
+ FastInt64ToBuffer(i, buffer);
+}
+
+string SimpleItoa(unsigned long i) {
+ char buffer[kFastToBufferSize];
+ return string(buffer, (sizeof(i) == 4) ?
+ FastUInt32ToBufferLeft(i, buffer) :
+ FastUInt64ToBufferLeft(i, buffer));
+}
+
+string SimpleItoa(long long i) {
+ char buffer[kFastToBufferSize];
+ return (sizeof(i) == 4) ?
+ FastInt32ToBuffer(i, buffer) :
+ FastInt64ToBuffer(i, buffer);
+}
+
+string SimpleItoa(unsigned long long i) {
+ char buffer[kFastToBufferSize];
+ return string(buffer, (sizeof(i) == 4) ?
+ FastUInt32ToBufferLeft(i, buffer) :
+ FastUInt64ToBufferLeft(i, buffer));
+}
+
+// ----------------------------------------------------------------------
+// SimpleDtoa()
+// SimpleFtoa()
+// DoubleToBuffer()
+// FloatToBuffer()
+// We want to print the value without losing precision, but we also do
+// not want to print more digits than necessary. This turns out to be
+// trickier than it sounds. Numbers like 0.2 cannot be represented
+// exactly in binary. If we print 0.2 with a very large precision,
+// e.g. "%.50g", we get "0.2000000000000000111022302462515654042363167".
+// On the other hand, if we set the precision too low, we lose
+// significant digits when printing numbers that actually need them.
+// It turns out there is no precision value that does the right thing
+// for all numbers.
+//
+// Our strategy is to first try printing with a precision that is never
+// over-precise, then parse the result with strtod() to see if it
+// matches. If not, we print again with a precision that will always
+// give a precise result, but may use more digits than necessary.
+//
+// An arguably better strategy would be to use the algorithm described
+// in "How to Print Floating-Point Numbers Accurately" by Steele &
+// White, e.g. as implemented by David M. Gay's dtoa(). It turns out,
+// however, that the following implementation is about as fast as
+// DMG's code. Furthermore, DMG's code locks mutexes, which means it
+// will not scale well on multi-core machines. DMG's code is slightly
+// more accurate (in that it will never use more digits than
+// necessary), but this is probably irrelevant for most users.
+//
+// Rob Pike and Ken Thompson also have an implementation of dtoa() in
+// third_party/fmt/fltfmt.cc. Their implementation is similar to this
+// one in that it makes guesses and then uses strtod() to check them.
+// Their implementation is faster because they use their own code to
+// generate the digits in the first place rather than use snprintf(),
+// thus avoiding format string parsing overhead. However, this makes
+// it considerably more complicated than the following implementation,
+// and it is embedded in a larger library. If speed turns out to be
+// an issue, we could re-implement this in terms of their
+// implementation.
+// ----------------------------------------------------------------------
+
+string SimpleDtoa(double value) {
+ char buffer[kDoubleToBufferSize];
+ return DoubleToBuffer(value, buffer);
+}
+
+string SimpleFtoa(float value) {
+ char buffer[kFloatToBufferSize];
+ return FloatToBuffer(value, buffer);
+}
+
+static inline bool IsValidFloatChar(char c) {
+ return ('0' <= c && c <= '9') ||
+ c == 'e' || c == 'E' ||
+ c == '+' || c == '-';
+}
+
+void DelocalizeRadix(char* buffer) {
+ // Fast check: if the buffer has a normal decimal point, assume no
+ // translation is needed.
+ if (strchr(buffer, '.') != NULL) return;
+
+ // Find the first unknown character.
+ while (IsValidFloatChar(*buffer)) ++buffer;
+
+ if (*buffer == '\0') {
+ // No radix character found.
+ return;
+ }
+
+ // We are now pointing at the locale-specific radix character. Replace it
+ // with '.'.
+ *buffer = '.';
+ ++buffer;
+
+ if (!IsValidFloatChar(*buffer) && *buffer != '\0') {
+ // It appears the radix was a multi-byte character. We need to remove the
+ // extra bytes.
+ char* target = buffer;
+ do { ++buffer; } while (!IsValidFloatChar(*buffer) && *buffer != '\0');
+ memmove(target, buffer, strlen(buffer) + 1);
+ }
+}
+
+char* DoubleToBuffer(double value, char* buffer) {
+ // DBL_DIG is 15 for IEEE-754 doubles, which are used on almost all
+ // platforms these days. Just in case some system exists where DBL_DIG
+ // is significantly larger -- and risks overflowing our buffer -- we have
+ // this assert.
+ GOOGLE_COMPILE_ASSERT(DBL_DIG < 20, DBL_DIG_is_too_big);
+
+ if (value == numeric_limits<double>::infinity()) {
+ strcpy(buffer, "inf");
+ return buffer;
+ } else if (value == -numeric_limits<double>::infinity()) {
+ strcpy(buffer, "-inf");
+ return buffer;
+ } else if (IsNaN(value)) {
+ strcpy(buffer, "nan");
+ return buffer;
+ }
+
+ int snprintf_result =
+ snprintf(buffer, kDoubleToBufferSize, "%.*g", DBL_DIG, value);
+
+ // The snprintf should never overflow because the buffer is significantly
+ // larger than the precision we asked for.
+ GOOGLE_DCHECK(snprintf_result > 0 && snprintf_result < kDoubleToBufferSize);
+
+ // We need to make parsed_value volatile in order to force the compiler to
+ // write it out to the stack. Otherwise, it may keep the value in a
+ // register, and if it does that, it may keep it as a long double instead
+ // of a double. This long double may have extra bits that make it compare
+ // unequal to "value" even though it would be exactly equal if it were
+ // truncated to a double.
+ volatile double parsed_value = strtod(buffer, NULL);
+ if (parsed_value != value) {
+ int snprintf_result =
+ snprintf(buffer, kDoubleToBufferSize, "%.*g", DBL_DIG+2, value);
+
+ // Should never overflow; see above.
+ GOOGLE_DCHECK(snprintf_result > 0 && snprintf_result < kDoubleToBufferSize);
+ }
+
+ DelocalizeRadix(buffer);
+ return buffer;
+}
+
+bool safe_strtof(const char* str, float* value) {
+ char* endptr;
+ errno = 0; // errno only gets set on errors
+#if defined(_WIN32) || defined (__hpux) // has no strtof()
+ *value = strtod(str, &endptr);
+#else
+ *value = strtof(str, &endptr);
+#endif
+ return *str != 0 && *endptr == 0 && errno == 0;
+}
+
+char* FloatToBuffer(float value, char* buffer) {
+ // FLT_DIG is 6 for IEEE-754 floats, which are used on almost all
+ // platforms these days. Just in case some system exists where FLT_DIG
+ // is significantly larger -- and risks overflowing our buffer -- we have
+ // this assert.
+ GOOGLE_COMPILE_ASSERT(FLT_DIG < 10, FLT_DIG_is_too_big);
+
+ if (value == numeric_limits<double>::infinity()) {
+ strcpy(buffer, "inf");
+ return buffer;
+ } else if (value == -numeric_limits<double>::infinity()) {
+ strcpy(buffer, "-inf");
+ return buffer;
+ } else if (IsNaN(value)) {
+ strcpy(buffer, "nan");
+ return buffer;
+ }
+
+ int snprintf_result =
+ snprintf(buffer, kFloatToBufferSize, "%.*g", FLT_DIG, value);
+
+ // The snprintf should never overflow because the buffer is significantly
+ // larger than the precision we asked for.
+ GOOGLE_DCHECK(snprintf_result > 0 && snprintf_result < kFloatToBufferSize);
+
+ float parsed_value;
+ if (!safe_strtof(buffer, &parsed_value) || parsed_value != value) {
+ int snprintf_result =
+ snprintf(buffer, kFloatToBufferSize, "%.*g", FLT_DIG+2, value);
+
+ // Should never overflow; see above.
+ GOOGLE_DCHECK(snprintf_result > 0 && snprintf_result < kFloatToBufferSize);
+ }
+
+ DelocalizeRadix(buffer);
+ return buffer;
+}
+
+string ToHex(uint64 num) {
+ if (num == 0) {
+ return string("0");
+ }
+
+ // Compute hex bytes in reverse order, writing to the back of the
+ // buffer.
+ char buf[16]; // No more than 16 hex digits needed.
+ char* bufptr = buf + 16;
+ static const char kHexChars[] = "0123456789abcdef";
+ while (num != 0) {
+ *--bufptr = kHexChars[num & 0xf];
+ num >>= 4;
+ }
+
+ return string(bufptr, buf + 16 - bufptr);
+}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.h b/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.h
new file mode 100644
index 0000000000..6cf23821e9
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/strutil.h
@@ -0,0 +1,563 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// from google3/strings/strutil.h
+
+#ifndef GOOGLE_PROTOBUF_STUBS_STRUTIL_H__
+#define GOOGLE_PROTOBUF_STUBS_STRUTIL_H__
+
+#include <stdlib.h>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+
+#ifdef _MSC_VER
+#define strtoll _strtoi64
+#define strtoull _strtoui64
+#elif defined(__DECCXX) && defined(__osf__)
+// HP C++ on Tru64 does not have strtoll, but strtol is already 64-bit.
+#define strtoll strtol
+#define strtoull strtoul
+#endif
+
+// ----------------------------------------------------------------------
+// ascii_isalnum()
+// Check if an ASCII character is alphanumeric. We can't use ctype's
+// isalnum() because it is affected by locale. This function is applied
+// to identifiers in the protocol buffer language, not to natural-language
+// strings, so locale should not be taken into account.
+// ascii_isdigit()
+// Like above, but only accepts digits.
+// ----------------------------------------------------------------------
+
+inline bool ascii_isalnum(char c) {
+ return ('a' <= c && c <= 'z') ||
+ ('A' <= c && c <= 'Z') ||
+ ('0' <= c && c <= '9');
+}
+
+inline bool ascii_isdigit(char c) {
+ return ('0' <= c && c <= '9');
+}
+
+// ----------------------------------------------------------------------
+// HasPrefixString()
+// Check if a string begins with a given prefix.
+// StripPrefixString()
+// Given a string and a putative prefix, returns the string minus the
+// prefix string if the prefix matches, otherwise the original
+// string.
+// ----------------------------------------------------------------------
+inline bool HasPrefixString(const string& str,
+ const string& prefix) {
+ return str.size() >= prefix.size() &&
+ str.compare(0, prefix.size(), prefix) == 0;
+}
+
+inline string StripPrefixString(const string& str, const string& prefix) {
+ if (HasPrefixString(str, prefix)) {
+ return str.substr(prefix.size());
+ } else {
+ return str;
+ }
+}
+
+// ----------------------------------------------------------------------
+// HasSuffixString()
+// Return true if str ends in suffix.
+// StripSuffixString()
+// Given a string and a putative suffix, returns the string minus the
+// suffix string if the suffix matches, otherwise the original
+// string.
+// ----------------------------------------------------------------------
+inline bool HasSuffixString(const string& str,
+ const string& suffix) {
+ return str.size() >= suffix.size() &&
+ str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
+}
+
+inline string StripSuffixString(const string& str, const string& suffix) {
+ if (HasSuffixString(str, suffix)) {
+ return str.substr(0, str.size() - suffix.size());
+ } else {
+ return str;
+ }
+}
+
+// ----------------------------------------------------------------------
+// StripString
+// Replaces any occurrence of the character 'remove' (or the characters
+// in 'remove') with the character 'replacewith'.
+// Good for keeping html characters or protocol characters (\t) out
+// of places where they might cause a problem.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT void StripString(string* s, const char* remove,
+ char replacewith);
+
+// ----------------------------------------------------------------------
+// LowerString()
+// UpperString()
+// ToUpper()
+// Convert the characters in "s" to lowercase or uppercase. ASCII-only:
+// these functions intentionally ignore locale because they are applied to
+// identifiers used in the Protocol Buffer language, not to natural-language
+// strings.
+// ----------------------------------------------------------------------
+
+inline void LowerString(string * s) {
+ string::iterator end = s->end();
+ for (string::iterator i = s->begin(); i != end; ++i) {
+ // tolower() changes based on locale. We don't want this!
+ if ('A' <= *i && *i <= 'Z') *i += 'a' - 'A';
+ }
+}
+
+inline void UpperString(string * s) {
+ string::iterator end = s->end();
+ for (string::iterator i = s->begin(); i != end; ++i) {
+ // toupper() changes based on locale. We don't want this!
+ if ('a' <= *i && *i <= 'z') *i += 'A' - 'a';
+ }
+}
+
+inline string ToUpper(const string& s) {
+ string out = s;
+ UpperString(&out);
+ return out;
+}
+
+// ----------------------------------------------------------------------
+// StringReplace()
+// Give me a string and two patterns "old" and "new", and I replace
+// the first instance of "old" in the string with "new", if it
+// exists. RETURN a new string, regardless of whether the replacement
+// happened or not.
+// ----------------------------------------------------------------------
+
+LIBPROTOBUF_EXPORT string StringReplace(const string& s, const string& oldsub,
+ const string& newsub, bool replace_all);
+
+// ----------------------------------------------------------------------
+// SplitStringUsing()
+// Split a string using a character delimiter. Append the components
+// to 'result'. If there are consecutive delimiters, this function skips
+// over all of them.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT void SplitStringUsing(const string& full, const char* delim,
+ vector<string>* res);
+
+// Split a string using one or more byte delimiters, presented
+// as a nul-terminated c string. Append the components to 'result'.
+// If there are consecutive delimiters, this function will return
+// corresponding empty strings. If you want to drop the empty
+// strings, try SplitStringUsing().
+//
+// If "full" is the empty string, yields an empty string as the only value.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT void SplitStringAllowEmpty(const string& full,
+ const char* delim,
+ vector<string>* result);
+
+// ----------------------------------------------------------------------
+// Split()
+// Split a string using a character delimiter.
+// ----------------------------------------------------------------------
+inline vector<string> Split(
+ const string& full, const char* delim, bool skip_empty = true) {
+ vector<string> result;
+ if (skip_empty) {
+ SplitStringUsing(full, delim, &result);
+ } else {
+ SplitStringAllowEmpty(full, delim, &result);
+ }
+ return result;
+}
+
+// ----------------------------------------------------------------------
+// JoinStrings()
+// These methods concatenate a vector of strings into a C++ string, using
+// the C-string "delim" as a separator between components. There are two
+// flavors of the function, one flavor returns the concatenated string,
+// another takes a pointer to the target string. In the latter case the
+// target string is cleared and overwritten.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT void JoinStrings(const vector<string>& components,
+ const char* delim, string* result);
+
+inline string JoinStrings(const vector<string>& components,
+ const char* delim) {
+ string result;
+ JoinStrings(components, delim, &result);
+ return result;
+}
+
+// ----------------------------------------------------------------------
+// UnescapeCEscapeSequences()
+// Copies "source" to "dest", rewriting C-style escape sequences
+// -- '\n', '\r', '\\', '\ooo', etc -- to their ASCII
+// equivalents. "dest" must be sufficiently large to hold all
+// the characters in the rewritten string (i.e. at least as large
+// as strlen(source) + 1 should be safe, since the replacements
+// are always shorter than the original escaped sequences). It's
+// safe for source and dest to be the same. RETURNS the length
+// of dest.
+//
+// It allows hex sequences \xhh, or generally \xhhhhh with an
+// arbitrary number of hex digits, but all of them together must
+// specify a value of a single byte (e.g. \x0045 is equivalent
+// to \x45, and \x1234 is erroneous).
+//
+// It also allows escape sequences of the form \uhhhh (exactly four
+// hex digits, upper or lower case) or \Uhhhhhhhh (exactly eight
+// hex digits, upper or lower case) to specify a Unicode code
+// point. The dest array will contain the UTF8-encoded version of
+// that code-point (e.g., if source contains \u2019, then dest will
+// contain the three bytes 0xE2, 0x80, and 0x99).
+//
+// Errors: In the first form of the call, errors are reported with
+// LOG(ERROR). The same is true for the second form of the call if
+// the pointer to the string vector is NULL; otherwise, error
+// messages are stored in the vector. In either case, the effect on
+// the dest array is not defined, but rest of the source will be
+// processed.
+// ----------------------------------------------------------------------
+
+LIBPROTOBUF_EXPORT int UnescapeCEscapeSequences(const char* source, char* dest);
+LIBPROTOBUF_EXPORT int UnescapeCEscapeSequences(const char* source, char* dest,
+ vector<string> *errors);
+
+// ----------------------------------------------------------------------
+// UnescapeCEscapeString()
+// This does the same thing as UnescapeCEscapeSequences, but creates
+// a new string. The caller does not need to worry about allocating
+// a dest buffer. This should be used for non performance critical
+// tasks such as printing debug messages. It is safe for src and dest
+// to be the same.
+//
+// The second call stores its errors in a supplied string vector.
+// If the string vector pointer is NULL, it reports the errors with LOG().
+//
+// In the first and second calls, the length of dest is returned. In the
+// the third call, the new string is returned.
+// ----------------------------------------------------------------------
+
+LIBPROTOBUF_EXPORT int UnescapeCEscapeString(const string& src, string* dest);
+LIBPROTOBUF_EXPORT int UnescapeCEscapeString(const string& src, string* dest,
+ vector<string> *errors);
+LIBPROTOBUF_EXPORT string UnescapeCEscapeString(const string& src);
+
+// ----------------------------------------------------------------------
+// CEscapeString()
+// Copies 'src' to 'dest', escaping dangerous characters using
+// C-style escape sequences. This is very useful for preparing query
+// flags. 'src' and 'dest' should not overlap.
+// Returns the number of bytes written to 'dest' (not including the \0)
+// or -1 if there was insufficient space.
+//
+// Currently only \n, \r, \t, ", ', \ and !isprint() chars are escaped.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT int CEscapeString(const char* src, int src_len,
+ char* dest, int dest_len);
+
+// ----------------------------------------------------------------------
+// CEscape()
+// More convenient form of CEscapeString: returns result as a "string".
+// This version is slower than CEscapeString() because it does more
+// allocation. However, it is much more convenient to use in
+// non-speed-critical code like logging messages etc.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT string CEscape(const string& src);
+
+namespace strings {
+// Like CEscape() but does not escape bytes with the upper bit set.
+LIBPROTOBUF_EXPORT string Utf8SafeCEscape(const string& src);
+
+// Like CEscape() but uses hex (\x) escapes instead of octals.
+LIBPROTOBUF_EXPORT string CHexEscape(const string& src);
+} // namespace strings
+
+// ----------------------------------------------------------------------
+// strto32()
+// strtou32()
+// strto64()
+// strtou64()
+// Architecture-neutral plug compatible replacements for strtol() and
+// strtoul(). Long's have different lengths on ILP-32 and LP-64
+// platforms, so using these is safer, from the point of view of
+// overflow behavior, than using the standard libc functions.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT int32 strto32_adaptor(const char *nptr, char **endptr,
+ int base);
+LIBPROTOBUF_EXPORT uint32 strtou32_adaptor(const char *nptr, char **endptr,
+ int base);
+
+inline int32 strto32(const char *nptr, char **endptr, int base) {
+ if (sizeof(int32) == sizeof(long))
+ return strtol(nptr, endptr, base);
+ else
+ return strto32_adaptor(nptr, endptr, base);
+}
+
+inline uint32 strtou32(const char *nptr, char **endptr, int base) {
+ if (sizeof(uint32) == sizeof(unsigned long))
+ return strtoul(nptr, endptr, base);
+ else
+ return strtou32_adaptor(nptr, endptr, base);
+}
+
+// For now, long long is 64-bit on all the platforms we care about, so these
+// functions can simply pass the call to strto[u]ll.
+inline int64 strto64(const char *nptr, char **endptr, int base) {
+ static_assert(sizeof(int64) == sizeof(long long), "Protobuf needs sizeof(int64) == sizeof(long long)");
+ GOOGLE_COMPILE_ASSERT(sizeof(int64) == sizeof(long long),
+ sizeof_int64_is_not_sizeof_long_long);
+ return strtoll(nptr, endptr, base);
+}
+
+inline uint64 strtou64(const char *nptr, char **endptr, int base) {
+ GOOGLE_COMPILE_ASSERT(sizeof(uint64) == sizeof(unsigned long long),
+ sizeof_uint64_is_not_sizeof_long_long);
+ return strtoull(nptr, endptr, base);
+}
+
+// ----------------------------------------------------------------------
+// safe_strto32()
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT bool safe_int(string text, int32* value_p);
+
+inline bool safe_strto32(string text, int32* value) {
+ return safe_int(text, value);
+}
+
+// ----------------------------------------------------------------------
+// FastIntToBuffer()
+// FastHexToBuffer()
+// FastHex64ToBuffer()
+// FastHex32ToBuffer()
+// FastTimeToBuffer()
+// These are intended for speed. FastIntToBuffer() assumes the
+// integer is non-negative. FastHexToBuffer() puts output in
+// hex rather than decimal. FastTimeToBuffer() puts the output
+// into RFC822 format.
+//
+// FastHex64ToBuffer() puts a 64-bit unsigned value in hex-format,
+// padded to exactly 16 bytes (plus one byte for '\0')
+//
+// FastHex32ToBuffer() puts a 32-bit unsigned value in hex-format,
+// padded to exactly 8 bytes (plus one byte for '\0')
+//
+// All functions take the output buffer as an arg.
+// They all return a pointer to the beginning of the output,
+// which may not be the beginning of the input buffer.
+// ----------------------------------------------------------------------
+
+// Suggested buffer size for FastToBuffer functions. Also works with
+// DoubleToBuffer() and FloatToBuffer().
+static const int kFastToBufferSize = 32;
+
+LIBPROTOBUF_EXPORT char* FastInt32ToBuffer(int32 i, char* buffer);
+LIBPROTOBUF_EXPORT char* FastInt64ToBuffer(int64 i, char* buffer);
+char* FastUInt32ToBuffer(uint32 i, char* buffer); // inline below
+char* FastUInt64ToBuffer(uint64 i, char* buffer); // inline below
+LIBPROTOBUF_EXPORT char* FastHexToBuffer(int i, char* buffer);
+LIBPROTOBUF_EXPORT char* FastHex64ToBuffer(uint64 i, char* buffer);
+LIBPROTOBUF_EXPORT char* FastHex32ToBuffer(uint32 i, char* buffer);
+
+// at least 22 bytes long
+inline char* FastIntToBuffer(int i, char* buffer) {
+ return (sizeof(i) == 4 ?
+ FastInt32ToBuffer(i, buffer) : FastInt64ToBuffer(i, buffer));
+}
+inline char* FastUIntToBuffer(unsigned int i, char* buffer) {
+ return (sizeof(i) == 4 ?
+ FastUInt32ToBuffer(i, buffer) : FastUInt64ToBuffer(i, buffer));
+}
+inline char* FastLongToBuffer(long i, char* buffer) {
+ return (sizeof(i) == 4 ?
+ FastInt32ToBuffer(i, buffer) : FastInt64ToBuffer(i, buffer));
+}
+inline char* FastULongToBuffer(unsigned long i, char* buffer) {
+ return (sizeof(i) == 4 ?
+ FastUInt32ToBuffer(i, buffer) : FastUInt64ToBuffer(i, buffer));
+}
+
+// ----------------------------------------------------------------------
+// FastInt32ToBufferLeft()
+// FastUInt32ToBufferLeft()
+// FastInt64ToBufferLeft()
+// FastUInt64ToBufferLeft()
+//
+// Like the Fast*ToBuffer() functions above, these are intended for speed.
+// Unlike the Fast*ToBuffer() functions, however, these functions write
+// their output to the beginning of the buffer (hence the name, as the
+// output is left-aligned). The caller is responsible for ensuring that
+// the buffer has enough space to hold the output.
+//
+// Returns a pointer to the end of the string (i.e. the null character
+// terminating the string).
+// ----------------------------------------------------------------------
+
+LIBPROTOBUF_EXPORT char* FastInt32ToBufferLeft(int32 i, char* buffer);
+LIBPROTOBUF_EXPORT char* FastUInt32ToBufferLeft(uint32 i, char* buffer);
+LIBPROTOBUF_EXPORT char* FastInt64ToBufferLeft(int64 i, char* buffer);
+LIBPROTOBUF_EXPORT char* FastUInt64ToBufferLeft(uint64 i, char* buffer);
+
+// Just define these in terms of the above.
+inline char* FastUInt32ToBuffer(uint32 i, char* buffer) {
+ FastUInt32ToBufferLeft(i, buffer);
+ return buffer;
+}
+inline char* FastUInt64ToBuffer(uint64 i, char* buffer) {
+ FastUInt64ToBufferLeft(i, buffer);
+ return buffer;
+}
+
+// ----------------------------------------------------------------------
+// SimpleItoa()
+// Description: converts an integer to a string.
+//
+// Return value: string
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT string SimpleItoa(int i);
+LIBPROTOBUF_EXPORT string SimpleItoa(unsigned int i);
+LIBPROTOBUF_EXPORT string SimpleItoa(long i);
+LIBPROTOBUF_EXPORT string SimpleItoa(unsigned long i);
+LIBPROTOBUF_EXPORT string SimpleItoa(long long i);
+LIBPROTOBUF_EXPORT string SimpleItoa(unsigned long long i);
+
+// ----------------------------------------------------------------------
+// SimpleDtoa()
+// SimpleFtoa()
+// DoubleToBuffer()
+// FloatToBuffer()
+// Description: converts a double or float to a string which, if
+// passed to NoLocaleStrtod(), will produce the exact same original double
+// (except in case of NaN; all NaNs are considered the same value).
+// We try to keep the string short but it's not guaranteed to be as
+// short as possible.
+//
+// DoubleToBuffer() and FloatToBuffer() write the text to the given
+// buffer and return it. The buffer must be at least
+// kDoubleToBufferSize bytes for doubles and kFloatToBufferSize
+// bytes for floats. kFastToBufferSize is also guaranteed to be large
+// enough to hold either.
+//
+// Return value: string
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT string SimpleDtoa(double value);
+LIBPROTOBUF_EXPORT string SimpleFtoa(float value);
+
+LIBPROTOBUF_EXPORT char* DoubleToBuffer(double i, char* buffer);
+LIBPROTOBUF_EXPORT char* FloatToBuffer(float i, char* buffer);
+
+// In practice, doubles should never need more than 24 bytes and floats
+// should never need more than 14 (including null terminators), but we
+// overestimate to be safe.
+static const int kDoubleToBufferSize = 32;
+static const int kFloatToBufferSize = 24;
+
+// ----------------------------------------------------------------------
+// ToString() are internal help methods used in StrCat() and Join()
+// ----------------------------------------------------------------------
+namespace internal {
+inline string ToString(int i) {
+ return SimpleItoa(i);
+}
+
+inline string ToString(string a) {
+ return a;
+}
+} // namespace internal
+
+// ----------------------------------------------------------------------
+// StrCat()
+// These methods join some strings together.
+// ----------------------------------------------------------------------
+template <typename T1, typename T2, typename T3, typename T4, typename T5>
+string StrCat(
+ const T1& a, const T2& b, const T3& c, const T4& d, const T5& e) {
+ return internal::ToString(a) + internal::ToString(b) +
+ internal::ToString(c) + internal::ToString(d) + internal::ToString(e);
+}
+
+template <typename T1, typename T2, typename T3, typename T4>
+string StrCat(
+ const T1& a, const T2& b, const T3& c, const T4& d) {
+ return internal::ToString(a) + internal::ToString(b) +
+ internal::ToString(c) + internal::ToString(d);
+}
+
+template <typename T1, typename T2, typename T3>
+string StrCat(const T1& a, const T2& b, const T3& c) {
+ return internal::ToString(a) + internal::ToString(b) +
+ internal::ToString(c);
+}
+
+template <typename T1, typename T2>
+string StrCat(const T1& a, const T2& b) {
+ return internal::ToString(a) + internal::ToString(b);
+}
+
+// ----------------------------------------------------------------------
+// Join()
+// These methods concatenate a range of components into a C++ string, using
+// the C-string "delim" as a separator between components.
+// ----------------------------------------------------------------------
+template <typename Iterator>
+void Join(Iterator start, Iterator end,
+ const char* delim, string* result) {
+ for (Iterator it = start; it != end; ++it) {
+ if (it != start) {
+ result->append(delim);
+ }
+ result->append(internal::ToString(*it));
+ }
+}
+
+template <typename Range>
+string Join(const Range& components,
+ const char* delim) {
+ string result;
+ Join(components.begin(), components.end(), delim, &result);
+ return result;
+}
+
+// ----------------------------------------------------------------------
+// ToHex()
+// Return a lower-case hex string representation of the given integer.
+// ----------------------------------------------------------------------
+LIBPROTOBUF_EXPORT string ToHex(uint64 num);
+
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_STRUTIL_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/substitute.cc b/toolkit/components/protobuf/src/google/protobuf/stubs/substitute.cc
new file mode 100644
index 0000000000..c9d95899f5
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/substitute.cc
@@ -0,0 +1,134 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+
+#include <google/protobuf/stubs/substitute.h>
+#include <google/protobuf/stubs/strutil.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+namespace strings {
+
+using internal::SubstituteArg;
+
+// Returns the number of args in arg_array which were passed explicitly
+// to Substitute().
+static int CountSubstituteArgs(const SubstituteArg* const* args_array) {
+ int count = 0;
+ while (args_array[count] != NULL && args_array[count]->size() != -1) {
+ ++count;
+ }
+ return count;
+}
+
+string Substitute(
+ const char* format,
+ const SubstituteArg& arg0, const SubstituteArg& arg1,
+ const SubstituteArg& arg2, const SubstituteArg& arg3,
+ const SubstituteArg& arg4, const SubstituteArg& arg5,
+ const SubstituteArg& arg6, const SubstituteArg& arg7,
+ const SubstituteArg& arg8, const SubstituteArg& arg9) {
+ string result;
+ SubstituteAndAppend(&result, format, arg0, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8, arg9);
+ return result;
+}
+
+void SubstituteAndAppend(
+ string* output, const char* format,
+ const SubstituteArg& arg0, const SubstituteArg& arg1,
+ const SubstituteArg& arg2, const SubstituteArg& arg3,
+ const SubstituteArg& arg4, const SubstituteArg& arg5,
+ const SubstituteArg& arg6, const SubstituteArg& arg7,
+ const SubstituteArg& arg8, const SubstituteArg& arg9) {
+ const SubstituteArg* const args_array[] = {
+ &arg0, &arg1, &arg2, &arg3, &arg4, &arg5, &arg6, &arg7, &arg8, &arg9, NULL
+ };
+
+ // Determine total size needed.
+ int size = 0;
+ for (int i = 0; format[i] != '\0'; i++) {
+ if (format[i] == '$') {
+ if (ascii_isdigit(format[i+1])) {
+ int index = format[i+1] - '0';
+ if (args_array[index]->size() == -1) {
+ GOOGLE_LOG(DFATAL)
+ << "strings::Substitute format string invalid: asked for \"$"
+ << index << "\", but only " << CountSubstituteArgs(args_array)
+ << " args were given. Full format string was: \""
+ << CEscape(format) << "\".";
+ return;
+ }
+ size += args_array[index]->size();
+ ++i; // Skip next char.
+ } else if (format[i+1] == '$') {
+ ++size;
+ ++i; // Skip next char.
+ } else {
+ GOOGLE_LOG(DFATAL)
+ << "Invalid strings::Substitute() format string: \""
+ << CEscape(format) << "\".";
+ return;
+ }
+ } else {
+ ++size;
+ }
+ }
+
+ if (size == 0) return;
+
+ // Build the string.
+ int original_size = output->size();
+ STLStringResizeUninitialized(output, original_size + size);
+ char* target = string_as_array(output) + original_size;
+ for (int i = 0; format[i] != '\0'; i++) {
+ if (format[i] == '$') {
+ if (ascii_isdigit(format[i+1])) {
+ const SubstituteArg* src = args_array[format[i+1] - '0'];
+ memcpy(target, src->data(), src->size());
+ target += src->size();
+ ++i; // Skip next char.
+ } else if (format[i+1] == '$') {
+ *target++ = '$';
+ ++i; // Skip next char.
+ }
+ } else {
+ *target++ = format[i];
+ }
+ }
+
+ GOOGLE_DCHECK_EQ(target - output->data(), output->size());
+}
+
+} // namespace strings
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/substitute.h b/toolkit/components/protobuf/src/google/protobuf/stubs/substitute.h
new file mode 100644
index 0000000000..7ee442af77
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/substitute.h
@@ -0,0 +1,170 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// from google3/strings/substitute.h
+
+#include <string>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/strutil.h>
+
+#ifndef GOOGLE_PROTOBUF_STUBS_SUBSTITUTE_H_
+#define GOOGLE_PROTOBUF_STUBS_SUBSTITUTE_H_
+
+namespace google {
+namespace protobuf {
+namespace strings {
+
+// ----------------------------------------------------------------------
+// strings::Substitute()
+// strings::SubstituteAndAppend()
+// Kind of like StringPrintf, but different.
+//
+// Example:
+// string GetMessage(string first_name, string last_name, int age) {
+// return strings::Substitute("My name is $0 $1 and I am $2 years old.",
+// first_name, last_name, age);
+// }
+//
+// Differences from StringPrintf:
+// * The format string does not identify the types of arguments.
+// Instead, the magic of C++ deals with this for us. See below
+// for a list of accepted types.
+// * Substitutions in the format string are identified by a '$'
+// followed by a digit. So, you can use arguments out-of-order and
+// use the same argument multiple times.
+// * It's much faster than StringPrintf.
+//
+// Supported types:
+// * Strings (const char*, const string&)
+// * Note that this means you do not have to add .c_str() to all of
+// your strings. In fact, you shouldn't; it will be slower.
+// * int32, int64, uint32, uint64: Formatted using SimpleItoa().
+// * float, double: Formatted using SimpleFtoa() and SimpleDtoa().
+// * bool: Printed as "true" or "false".
+//
+// SubstituteAndAppend() is like Substitute() but appends the result to
+// *output. Example:
+//
+// string str;
+// strings::SubstituteAndAppend(&str,
+// "My name is $0 $1 and I am $2 years old.",
+// first_name, last_name, age);
+//
+// Substitute() is significantly faster than StringPrintf(). For very
+// large strings, it may be orders of magnitude faster.
+// ----------------------------------------------------------------------
+
+namespace internal { // Implementation details.
+
+class SubstituteArg {
+ public:
+ inline SubstituteArg(const char* value)
+ : text_(value), size_(strlen(text_)) {}
+ inline SubstituteArg(const string& value)
+ : text_(value.data()), size_(value.size()) {}
+
+ // Indicates that no argument was given.
+ inline explicit SubstituteArg()
+ : text_(NULL), size_(-1) {}
+
+ // Primitives
+ // We don't overload for signed and unsigned char because if people are
+ // explicitly declaring their chars as signed or unsigned then they are
+ // probably actually using them as 8-bit integers and would probably
+ // prefer an integer representation. But, we don't really know. So, we
+ // make the caller decide what to do.
+ inline SubstituteArg(char value)
+ : text_(scratch_), size_(1) { scratch_[0] = value; }
+ inline SubstituteArg(short value)
+ : text_(FastInt32ToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(unsigned short value)
+ : text_(FastUInt32ToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(int value)
+ : text_(FastInt32ToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(unsigned int value)
+ : text_(FastUInt32ToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(long value)
+ : text_(FastLongToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(unsigned long value)
+ : text_(FastULongToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(long long value)
+ : text_(FastInt64ToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(unsigned long long value)
+ : text_(FastUInt64ToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(float value)
+ : text_(FloatToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(double value)
+ : text_(DoubleToBuffer(value, scratch_)), size_(strlen(text_)) {}
+ inline SubstituteArg(bool value)
+ : text_(value ? "true" : "false"), size_(strlen(text_)) {}
+
+ inline const char* data() const { return text_; }
+ inline int size() const { return size_; }
+
+ private:
+ const char* text_;
+ int size_;
+ char scratch_[kFastToBufferSize];
+};
+
+} // namespace internal
+
+LIBPROTOBUF_EXPORT string Substitute(
+ const char* format,
+ const internal::SubstituteArg& arg0 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg1 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg2 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg3 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg4 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg5 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg6 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg7 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg8 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg9 = internal::SubstituteArg());
+
+LIBPROTOBUF_EXPORT void SubstituteAndAppend(
+ string* output, const char* format,
+ const internal::SubstituteArg& arg0 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg1 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg2 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg3 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg4 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg5 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg6 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg7 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg8 = internal::SubstituteArg(),
+ const internal::SubstituteArg& arg9 = internal::SubstituteArg());
+
+} // namespace strings
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_STUBS_SUBSTITUTE_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/template_util.h b/toolkit/components/protobuf/src/google/protobuf/stubs/template_util.h
new file mode 100644
index 0000000000..4f30ffa3bb
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/template_util.h
@@ -0,0 +1,138 @@
+// Copyright 2005 Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// ----
+// Author: lar@google.com (Laramie Leavitt)
+//
+// Template metaprogramming utility functions.
+//
+// This code is compiled directly on many platforms, including client
+// platforms like Windows, Mac, and embedded systems. Before making
+// any changes here, make sure that you're not breaking any platforms.
+//
+//
+// The names choosen here reflect those used in tr1 and the boost::mpl
+// library, there are similar operations used in the Loki library as
+// well. I prefer the boost names for 2 reasons:
+// 1. I think that portions of the Boost libraries are more likely to
+// be included in the c++ standard.
+// 2. It is not impossible that some of the boost libraries will be
+// included in our own build in the future.
+// Both of these outcomes means that we may be able to directly replace
+// some of these with boost equivalents.
+//
+#ifndef GOOGLE_PROTOBUF_TEMPLATE_UTIL_H_
+#define GOOGLE_PROTOBUF_TEMPLATE_UTIL_H_
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// Types small_ and big_ are guaranteed such that sizeof(small_) <
+// sizeof(big_)
+typedef char small_;
+
+struct big_ {
+ char dummy[2];
+};
+
+// Identity metafunction.
+template <class T>
+struct identity_ {
+ typedef T type;
+};
+
+// integral_constant, defined in tr1, is a wrapper for an integer
+// value. We don't really need this generality; we could get away
+// with hardcoding the integer type to bool. We use the fully
+// general integer_constant for compatibility with tr1.
+
+template<class T, T v>
+struct integral_constant {
+ static const T value = v;
+ typedef T value_type;
+ typedef integral_constant<T, v> type;
+};
+
+template <class T, T v> const T integral_constant<T, v>::value;
+
+
+// Abbreviations: true_type and false_type are structs that represent boolean
+// true and false values. Also define the boost::mpl versions of those names,
+// true_ and false_.
+typedef integral_constant<bool, true> true_type;
+typedef integral_constant<bool, false> false_type;
+typedef true_type true_;
+typedef false_type false_;
+
+// if_ is a templatized conditional statement.
+// if_<cond, A, B> is a compile time evaluation of cond.
+// if_<>::type contains A if cond is true, B otherwise.
+template<bool cond, typename A, typename B>
+struct if_{
+ typedef A type;
+};
+
+template<typename A, typename B>
+struct if_<false, A, B> {
+ typedef B type;
+};
+
+
+// type_equals_ is a template type comparator, similar to Loki IsSameType.
+// type_equals_<A, B>::value is true iff "A" is the same type as "B".
+//
+// New code should prefer base::is_same, defined in base/type_traits.h.
+// It is functionally identical, but is_same is the standard spelling.
+template<typename A, typename B>
+struct type_equals_ : public false_ {
+};
+
+template<typename A>
+struct type_equals_<A, A> : public true_ {
+};
+
+// and_ is a template && operator.
+// and_<A, B>::value evaluates "A::value && B::value".
+template<typename A, typename B>
+struct and_ : public integral_constant<bool, (A::value && B::value)> {
+};
+
+// or_ is a template || operator.
+// or_<A, B>::value evaluates "A::value || B::value".
+template<typename A, typename B>
+struct or_ : public integral_constant<bool, (A::value || B::value)> {
+};
+
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_TEMPLATE_UTIL_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/stubs/type_traits.h b/toolkit/components/protobuf/src/google/protobuf/stubs/type_traits.h
new file mode 100644
index 0000000000..0ef166b7ab
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/stubs/type_traits.h
@@ -0,0 +1,334 @@
+// Copyright (c) 2006, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// ----
+// Author: Matt Austern
+//
+// This code is compiled directly on many platforms, including client
+// platforms like Windows, Mac, and embedded systems. Before making
+// any changes here, make sure that you're not breaking any platforms.
+//
+// Define a small subset of tr1 type traits. The traits we define are:
+// is_integral
+// is_floating_point
+// is_pointer
+// is_enum
+// is_reference
+// is_pod
+// has_trivial_constructor
+// has_trivial_copy
+// has_trivial_assign
+// has_trivial_destructor
+// remove_const
+// remove_volatile
+// remove_cv
+// remove_reference
+// add_reference
+// remove_pointer
+// is_same
+// is_convertible
+// We can add more type traits as required.
+
+#ifndef GOOGLE_PROTOBUF_TYPE_TRAITS_H_
+#define GOOGLE_PROTOBUF_TYPE_TRAITS_H_
+
+#include <utility> // For pair
+
+#include <google/protobuf/stubs/template_util.h> // For true_type and false_type
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+template <class T> struct is_integral;
+template <class T> struct is_floating_point;
+template <class T> struct is_pointer;
+// MSVC can't compile this correctly, and neither can gcc 3.3.5 (at least)
+#if !defined(_MSC_VER) && !(defined(__GNUC__) && __GNUC__ <= 3)
+// is_enum uses is_convertible, which is not available on MSVC.
+template <class T> struct is_enum;
+#endif
+template <class T> struct is_reference;
+template <class T> struct is_pod;
+template <class T> struct has_trivial_constructor;
+template <class T> struct has_trivial_copy;
+template <class T> struct has_trivial_assign;
+template <class T> struct has_trivial_destructor;
+template <class T> struct remove_const;
+template <class T> struct remove_volatile;
+template <class T> struct remove_cv;
+template <class T> struct remove_reference;
+template <class T> struct add_reference;
+template <class T> struct remove_pointer;
+template <class T, class U> struct is_same;
+#if !defined(_MSC_VER) && !(defined(__GNUC__) && __GNUC__ <= 3)
+template <class From, class To> struct is_convertible;
+#endif
+
+// is_integral is false except for the built-in integer types. A
+// cv-qualified type is integral if and only if the underlying type is.
+template <class T> struct is_integral : false_type { };
+template<> struct is_integral<bool> : true_type { };
+template<> struct is_integral<char> : true_type { };
+template<> struct is_integral<unsigned char> : true_type { };
+template<> struct is_integral<signed char> : true_type { };
+#if defined(_MSC_VER)
+// wchar_t is not by default a distinct type from unsigned short in
+// Microsoft C.
+// See http://msdn2.microsoft.com/en-us/library/dh8che7s(VS.80).aspx
+template<> struct is_integral<__wchar_t> : true_type { };
+#else
+template<> struct is_integral<wchar_t> : true_type { };
+#endif
+template<> struct is_integral<short> : true_type { };
+template<> struct is_integral<unsigned short> : true_type { };
+template<> struct is_integral<int> : true_type { };
+template<> struct is_integral<unsigned int> : true_type { };
+template<> struct is_integral<long> : true_type { };
+template<> struct is_integral<unsigned long> : true_type { };
+template<> struct is_integral<long long> : true_type { };
+template<> struct is_integral<unsigned long long> : true_type { };
+template <class T> struct is_integral<const T> : is_integral<T> { };
+template <class T> struct is_integral<volatile T> : is_integral<T> { };
+template <class T> struct is_integral<const volatile T> : is_integral<T> { };
+
+// is_floating_point is false except for the built-in floating-point types.
+// A cv-qualified type is integral if and only if the underlying type is.
+template <class T> struct is_floating_point : false_type { };
+template<> struct is_floating_point<float> : true_type { };
+template<> struct is_floating_point<double> : true_type { };
+template<> struct is_floating_point<long double> : true_type { };
+template <class T> struct is_floating_point<const T>
+ : is_floating_point<T> { };
+template <class T> struct is_floating_point<volatile T>
+ : is_floating_point<T> { };
+template <class T> struct is_floating_point<const volatile T>
+ : is_floating_point<T> { };
+
+// is_pointer is false except for pointer types. A cv-qualified type (e.g.
+// "int* const", as opposed to "int const*") is cv-qualified if and only if
+// the underlying type is.
+template <class T> struct is_pointer : false_type { };
+template <class T> struct is_pointer<T*> : true_type { };
+template <class T> struct is_pointer<const T> : is_pointer<T> { };
+template <class T> struct is_pointer<volatile T> : is_pointer<T> { };
+template <class T> struct is_pointer<const volatile T> : is_pointer<T> { };
+
+#if !defined(_MSC_VER) && !(defined(__GNUC__) && __GNUC__ <= 3)
+
+namespace internal {
+
+template <class T> struct is_class_or_union {
+ template <class U> static small_ tester(void (U::*)());
+ template <class U> static big_ tester(...);
+ static const bool value = sizeof(tester<T>(0)) == sizeof(small_);
+};
+
+// is_convertible chokes if the first argument is an array. That's why
+// we use add_reference here.
+template <bool NotUnum, class T> struct is_enum_impl
+ : is_convertible<typename add_reference<T>::type, int> { };
+
+template <class T> struct is_enum_impl<true, T> : false_type { };
+
+} // namespace internal
+
+// Specified by TR1 [4.5.1] primary type categories.
+
+// Implementation note:
+//
+// Each type is either void, integral, floating point, array, pointer,
+// reference, member object pointer, member function pointer, enum,
+// union or class. Out of these, only integral, floating point, reference,
+// class and enum types are potentially convertible to int. Therefore,
+// if a type is not a reference, integral, floating point or class and
+// is convertible to int, it's a enum. Adding cv-qualification to a type
+// does not change whether it's an enum.
+//
+// Is-convertible-to-int check is done only if all other checks pass,
+// because it can't be used with some types (e.g. void or classes with
+// inaccessible conversion operators).
+template <class T> struct is_enum
+ : internal::is_enum_impl<
+ is_same<T, void>::value ||
+ is_integral<T>::value ||
+ is_floating_point<T>::value ||
+ is_reference<T>::value ||
+ internal::is_class_or_union<T>::value,
+ T> { };
+
+template <class T> struct is_enum<const T> : is_enum<T> { };
+template <class T> struct is_enum<volatile T> : is_enum<T> { };
+template <class T> struct is_enum<const volatile T> : is_enum<T> { };
+
+#endif
+
+// is_reference is false except for reference types.
+template<typename T> struct is_reference : false_type {};
+template<typename T> struct is_reference<T&> : true_type {};
+
+
+// We can't get is_pod right without compiler help, so fail conservatively.
+// We will assume it's false except for arithmetic types, enumerations,
+// pointers and cv-qualified versions thereof. Note that std::pair<T,U>
+// is not a POD even if T and U are PODs.
+template <class T> struct is_pod
+ : integral_constant<bool, (is_integral<T>::value ||
+ is_floating_point<T>::value ||
+#if !defined(_MSC_VER) && !(defined(__GNUC__) && __GNUC__ <= 3)
+ // is_enum is not available on MSVC.
+ is_enum<T>::value ||
+#endif
+ is_pointer<T>::value)> { };
+template <class T> struct is_pod<const T> : is_pod<T> { };
+template <class T> struct is_pod<volatile T> : is_pod<T> { };
+template <class T> struct is_pod<const volatile T> : is_pod<T> { };
+
+
+// We can't get has_trivial_constructor right without compiler help, so
+// fail conservatively. We will assume it's false except for: (1) types
+// for which is_pod is true. (2) std::pair of types with trivial
+// constructors. (3) array of a type with a trivial constructor.
+// (4) const versions thereof.
+template <class T> struct has_trivial_constructor : is_pod<T> { };
+template <class T, class U> struct has_trivial_constructor<std::pair<T, U> >
+ : integral_constant<bool,
+ (has_trivial_constructor<T>::value &&
+ has_trivial_constructor<U>::value)> { };
+template <class A, int N> struct has_trivial_constructor<A[N]>
+ : has_trivial_constructor<A> { };
+template <class T> struct has_trivial_constructor<const T>
+ : has_trivial_constructor<T> { };
+
+// We can't get has_trivial_copy right without compiler help, so fail
+// conservatively. We will assume it's false except for: (1) types
+// for which is_pod is true. (2) std::pair of types with trivial copy
+// constructors. (3) array of a type with a trivial copy constructor.
+// (4) const versions thereof.
+template <class T> struct has_trivial_copy : is_pod<T> { };
+template <class T, class U> struct has_trivial_copy<std::pair<T, U> >
+ : integral_constant<bool,
+ (has_trivial_copy<T>::value &&
+ has_trivial_copy<U>::value)> { };
+template <class A, int N> struct has_trivial_copy<A[N]>
+ : has_trivial_copy<A> { };
+template <class T> struct has_trivial_copy<const T> : has_trivial_copy<T> { };
+
+// We can't get has_trivial_assign right without compiler help, so fail
+// conservatively. We will assume it's false except for: (1) types
+// for which is_pod is true. (2) std::pair of types with trivial copy
+// constructors. (3) array of a type with a trivial assign constructor.
+template <class T> struct has_trivial_assign : is_pod<T> { };
+template <class T, class U> struct has_trivial_assign<std::pair<T, U> >
+ : integral_constant<bool,
+ (has_trivial_assign<T>::value &&
+ has_trivial_assign<U>::value)> { };
+template <class A, int N> struct has_trivial_assign<A[N]>
+ : has_trivial_assign<A> { };
+
+// We can't get has_trivial_destructor right without compiler help, so
+// fail conservatively. We will assume it's false except for: (1) types
+// for which is_pod is true. (2) std::pair of types with trivial
+// destructors. (3) array of a type with a trivial destructor.
+// (4) const versions thereof.
+template <class T> struct has_trivial_destructor : is_pod<T> { };
+template <class T, class U> struct has_trivial_destructor<std::pair<T, U> >
+ : integral_constant<bool,
+ (has_trivial_destructor<T>::value &&
+ has_trivial_destructor<U>::value)> { };
+template <class A, int N> struct has_trivial_destructor<A[N]>
+ : has_trivial_destructor<A> { };
+template <class T> struct has_trivial_destructor<const T>
+ : has_trivial_destructor<T> { };
+
+// Specified by TR1 [4.7.1]
+template<typename T> struct remove_const { typedef T type; };
+template<typename T> struct remove_const<T const> { typedef T type; };
+template<typename T> struct remove_volatile { typedef T type; };
+template<typename T> struct remove_volatile<T volatile> { typedef T type; };
+template<typename T> struct remove_cv {
+ typedef typename remove_const<typename remove_volatile<T>::type>::type type;
+};
+
+
+// Specified by TR1 [4.7.2] Reference modifications.
+template<typename T> struct remove_reference { typedef T type; };
+template<typename T> struct remove_reference<T&> { typedef T type; };
+
+template <typename T> struct add_reference { typedef T& type; };
+template <typename T> struct add_reference<T&> { typedef T& type; };
+
+// Specified by TR1 [4.7.4] Pointer modifications.
+template<typename T> struct remove_pointer { typedef T type; };
+template<typename T> struct remove_pointer<T*> { typedef T type; };
+template<typename T> struct remove_pointer<T* const> { typedef T type; };
+template<typename T> struct remove_pointer<T* volatile> { typedef T type; };
+template<typename T> struct remove_pointer<T* const volatile> {
+ typedef T type; };
+
+// Specified by TR1 [4.6] Relationships between types
+template<typename T, typename U> struct is_same : public false_type { };
+template<typename T> struct is_same<T, T> : public true_type { };
+
+// Specified by TR1 [4.6] Relationships between types
+#if !defined(_MSC_VER) && !(defined(__GNUC__) && __GNUC__ <= 3)
+namespace internal {
+
+// This class is an implementation detail for is_convertible, and you
+// don't need to know how it works to use is_convertible. For those
+// who care: we declare two different functions, one whose argument is
+// of type To and one with a variadic argument list. We give them
+// return types of different size, so we can use sizeof to trick the
+// compiler into telling us which function it would have chosen if we
+// had called it with an argument of type From. See Alexandrescu's
+// _Modern C++ Design_ for more details on this sort of trick.
+
+template <typename From, typename To>
+struct ConvertHelper {
+ static small_ Test(To);
+ static big_ Test(...);
+ static From Create();
+};
+} // namespace internal
+
+// Inherits from true_type if From is convertible to To, false_type otherwise.
+template <typename From, typename To>
+struct is_convertible
+ : integral_constant<bool,
+ sizeof(internal::ConvertHelper<From, To>::Test(
+ internal::ConvertHelper<From, To>::Create()))
+ == sizeof(small_)> {
+};
+#endif
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
+
+#endif // GOOGLE_PROTOBUF_TYPE_TRAITS_H_
diff --git a/toolkit/components/protobuf/src/google/protobuf/text_format.cc b/toolkit/components/protobuf/src/google/protobuf/text_format.cc
new file mode 100644
index 0000000000..84cdbb57e4
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/text_format.cc
@@ -0,0 +1,1746 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: jschorr@google.com (Joseph Schorr)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <algorithm>
+#include <float.h>
+#include <math.h>
+#include <stdio.h>
+#include <stack>
+#include <limits>
+#include <vector>
+
+#include <google/protobuf/text_format.h>
+
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/wire_format_lite.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <google/protobuf/unknown_field_set.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/io/tokenizer.h>
+#include <google/protobuf/stubs/strutil.h>
+#include <google/protobuf/stubs/map_util.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+
+namespace {
+
+inline bool IsHexNumber(const string& str) {
+ return (str.length() >= 2 && str[0] == '0' &&
+ (str[1] == 'x' || str[1] == 'X'));
+}
+
+inline bool IsOctNumber(const string& str) {
+ return (str.length() >= 2 && str[0] == '0' &&
+ (str[1] >= '0' && str[1] < '8'));
+}
+
+} // namespace
+
+string Message::DebugString() const {
+ string debug_string;
+
+ TextFormat::PrintToString(*this, &debug_string);
+
+ return debug_string;
+}
+
+string Message::ShortDebugString() const {
+ string debug_string;
+
+ TextFormat::Printer printer;
+ printer.SetSingleLineMode(true);
+
+ printer.PrintToString(*this, &debug_string);
+ // Single line mode currently might have an extra space at the end.
+ if (debug_string.size() > 0 &&
+ debug_string[debug_string.size() - 1] == ' ') {
+ debug_string.resize(debug_string.size() - 1);
+ }
+
+ return debug_string;
+}
+
+string Message::Utf8DebugString() const {
+ string debug_string;
+
+ TextFormat::Printer printer;
+ printer.SetUseUtf8StringEscaping(true);
+
+ printer.PrintToString(*this, &debug_string);
+
+ return debug_string;
+}
+
+void Message::PrintDebugString() const {
+ printf("%s", DebugString().c_str());
+}
+
+
+// ===========================================================================
+// Implementation of the parse information tree class.
+TextFormat::ParseInfoTree::ParseInfoTree() { }
+
+TextFormat::ParseInfoTree::~ParseInfoTree() {
+ // Remove any nested information trees, as they are owned by this tree.
+ for (NestedMap::iterator it = nested_.begin(); it != nested_.end(); ++it) {
+ STLDeleteElements(&(it->second));
+ }
+}
+
+void TextFormat::ParseInfoTree::RecordLocation(
+ const FieldDescriptor* field,
+ TextFormat::ParseLocation location) {
+ locations_[field].push_back(location);
+}
+
+TextFormat::ParseInfoTree* TextFormat::ParseInfoTree::CreateNested(
+ const FieldDescriptor* field) {
+ // Owned by us in the map.
+ TextFormat::ParseInfoTree* instance = new TextFormat::ParseInfoTree();
+ vector<TextFormat::ParseInfoTree*>* trees = &nested_[field];
+ GOOGLE_CHECK(trees);
+ trees->push_back(instance);
+ return instance;
+}
+
+void CheckFieldIndex(const FieldDescriptor* field, int index) {
+ if (field == NULL) { return; }
+
+ if (field->is_repeated() && index == -1) {
+ GOOGLE_LOG(DFATAL) << "Index must be in range of repeated field values. "
+ << "Field: " << field->name();
+ } else if (!field->is_repeated() && index != -1) {
+ GOOGLE_LOG(DFATAL) << "Index must be -1 for singular fields."
+ << "Field: " << field->name();
+ }
+}
+
+TextFormat::ParseLocation TextFormat::ParseInfoTree::GetLocation(
+ const FieldDescriptor* field, int index) const {
+ CheckFieldIndex(field, index);
+ if (index == -1) { index = 0; }
+
+ const vector<TextFormat::ParseLocation>* locations =
+ FindOrNull(locations_, field);
+ if (locations == NULL || index >= locations->size()) {
+ return TextFormat::ParseLocation();
+ }
+
+ return (*locations)[index];
+}
+
+TextFormat::ParseInfoTree* TextFormat::ParseInfoTree::GetTreeForNested(
+ const FieldDescriptor* field, int index) const {
+ CheckFieldIndex(field, index);
+ if (index == -1) { index = 0; }
+
+ const vector<TextFormat::ParseInfoTree*>* trees = FindOrNull(nested_, field);
+ if (trees == NULL || index >= trees->size()) {
+ return NULL;
+ }
+
+ return (*trees)[index];
+}
+
+
+// ===========================================================================
+// Internal class for parsing an ASCII representation of a Protocol Message.
+// This class makes use of the Protocol Message compiler's tokenizer found
+// in //google/protobuf/io/tokenizer.h. Note that class's Parse
+// method is *not* thread-safe and should only be used in a single thread at
+// a time.
+
+// Makes code slightly more readable. The meaning of "DO(foo)" is
+// "Execute foo and fail if it fails.", where failure is indicated by
+// returning false. Borrowed from parser.cc (Thanks Kenton!).
+#define DO(STATEMENT) if (STATEMENT) {} else return false
+
+class TextFormat::Parser::ParserImpl {
+ public:
+
+ // Determines if repeated values for non-repeated fields and
+ // oneofs are permitted, e.g., the string "foo: 1 foo: 2" for a
+ // required/optional field named "foo", or "baz: 1 qux: 2"
+ // where "baz" and "qux" are members of the same oneof.
+ enum SingularOverwritePolicy {
+ ALLOW_SINGULAR_OVERWRITES = 0, // the last value is retained
+ FORBID_SINGULAR_OVERWRITES = 1, // an error is issued
+ };
+
+ ParserImpl(const Descriptor* root_message_type,
+ io::ZeroCopyInputStream* input_stream,
+ io::ErrorCollector* error_collector,
+ TextFormat::Finder* finder,
+ ParseInfoTree* parse_info_tree,
+ SingularOverwritePolicy singular_overwrite_policy,
+ bool allow_case_insensitive_field,
+ bool allow_unknown_field,
+ bool allow_unknown_enum,
+ bool allow_field_number,
+ bool allow_relaxed_whitespace)
+ : error_collector_(error_collector),
+ finder_(finder),
+ parse_info_tree_(parse_info_tree),
+ tokenizer_error_collector_(this),
+ tokenizer_(input_stream, &tokenizer_error_collector_),
+ root_message_type_(root_message_type),
+ singular_overwrite_policy_(singular_overwrite_policy),
+ allow_case_insensitive_field_(allow_case_insensitive_field),
+ allow_unknown_field_(allow_unknown_field),
+ allow_unknown_enum_(allow_unknown_enum),
+ allow_field_number_(allow_field_number),
+ had_errors_(false) {
+ // For backwards-compatibility with proto1, we need to allow the 'f' suffix
+ // for floats.
+ tokenizer_.set_allow_f_after_float(true);
+
+ // '#' starts a comment.
+ tokenizer_.set_comment_style(io::Tokenizer::SH_COMMENT_STYLE);
+
+ if (allow_relaxed_whitespace) {
+ tokenizer_.set_require_space_after_number(false);
+ tokenizer_.set_allow_multiline_strings(true);
+ }
+
+ // Consume the starting token.
+ tokenizer_.Next();
+ }
+ ~ParserImpl() { }
+
+ // Parses the ASCII representation specified in input and saves the
+ // information into the output pointer (a Message). Returns
+ // false if an error occurs (an error will also be logged to
+ // GOOGLE_LOG(ERROR)).
+ bool Parse(Message* output) {
+ // Consume fields until we cannot do so anymore.
+ while (true) {
+ if (LookingAtType(io::Tokenizer::TYPE_END)) {
+ return !had_errors_;
+ }
+
+ DO(ConsumeField(output));
+ }
+ }
+
+ bool ParseField(const FieldDescriptor* field, Message* output) {
+ bool suc;
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ suc = ConsumeFieldMessage(output, output->GetReflection(), field);
+ } else {
+ suc = ConsumeFieldValue(output, output->GetReflection(), field);
+ }
+ return suc && LookingAtType(io::Tokenizer::TYPE_END);
+ }
+
+ void ReportError(int line, int col, const string& message) {
+ had_errors_ = true;
+ if (error_collector_ == NULL) {
+ if (line >= 0) {
+ GOOGLE_LOG(ERROR) << "Error parsing text-format "
+ << root_message_type_->full_name()
+ << ": " << (line + 1) << ":"
+ << (col + 1) << ": " << message;
+ } else {
+ GOOGLE_LOG(ERROR) << "Error parsing text-format "
+ << root_message_type_->full_name()
+ << ": " << message;
+ }
+ } else {
+ error_collector_->AddError(line, col, message);
+ }
+ }
+
+ void ReportWarning(int line, int col, const string& message) {
+ if (error_collector_ == NULL) {
+ if (line >= 0) {
+ GOOGLE_LOG(WARNING) << "Warning parsing text-format "
+ << root_message_type_->full_name()
+ << ": " << (line + 1) << ":"
+ << (col + 1) << ": " << message;
+ } else {
+ GOOGLE_LOG(WARNING) << "Warning parsing text-format "
+ << root_message_type_->full_name()
+ << ": " << message;
+ }
+ } else {
+ error_collector_->AddWarning(line, col, message);
+ }
+ }
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ParserImpl);
+
+ // Reports an error with the given message with information indicating
+ // the position (as derived from the current token).
+ void ReportError(const string& message) {
+ ReportError(tokenizer_.current().line, tokenizer_.current().column,
+ message);
+ }
+
+ // Reports a warning with the given message with information indicating
+ // the position (as derived from the current token).
+ void ReportWarning(const string& message) {
+ ReportWarning(tokenizer_.current().line, tokenizer_.current().column,
+ message);
+ }
+
+ // Consumes the specified message with the given starting delimeter.
+ // This method checks to see that the end delimeter at the conclusion of
+ // the consumption matches the starting delimeter passed in here.
+ bool ConsumeMessage(Message* message, const string delimeter) {
+ while (!LookingAt(">") && !LookingAt("}")) {
+ DO(ConsumeField(message));
+ }
+
+ // Confirm that we have a valid ending delimeter.
+ DO(Consume(delimeter));
+
+ return true;
+ }
+
+
+ // Consumes the current field (as returned by the tokenizer) on the
+ // passed in message.
+ bool ConsumeField(Message* message) {
+ const Reflection* reflection = message->GetReflection();
+ const Descriptor* descriptor = message->GetDescriptor();
+
+ string field_name;
+
+ const FieldDescriptor* field = NULL;
+ int start_line = tokenizer_.current().line;
+ int start_column = tokenizer_.current().column;
+
+ if (TryConsume("[")) {
+ // Extension.
+ DO(ConsumeIdentifier(&field_name));
+ while (TryConsume(".")) {
+ string part;
+ DO(ConsumeIdentifier(&part));
+ field_name += ".";
+ field_name += part;
+ }
+ DO(Consume("]"));
+
+ field = (finder_ != NULL
+ ? finder_->FindExtension(message, field_name)
+ : reflection->FindKnownExtensionByName(field_name));
+
+ if (field == NULL) {
+ if (!allow_unknown_field_) {
+ ReportError("Extension \"" + field_name + "\" is not defined or "
+ "is not an extension of \"" +
+ descriptor->full_name() + "\".");
+ return false;
+ } else {
+ ReportWarning("Extension \"" + field_name + "\" is not defined or "
+ "is not an extension of \"" +
+ descriptor->full_name() + "\".");
+ }
+ }
+ } else {
+ DO(ConsumeIdentifier(&field_name));
+
+ int32 field_number;
+ if (allow_field_number_ && safe_strto32(field_name, &field_number)) {
+ if (descriptor->IsExtensionNumber(field_number)) {
+ field = reflection->FindKnownExtensionByNumber(field_number);
+ } else {
+ field = descriptor->FindFieldByNumber(field_number);
+ }
+ } else {
+ field = descriptor->FindFieldByName(field_name);
+ // Group names are expected to be capitalized as they appear in the
+ // .proto file, which actually matches their type names, not their
+ // field names.
+ if (field == NULL) {
+ string lower_field_name = field_name;
+ LowerString(&lower_field_name);
+ field = descriptor->FindFieldByName(lower_field_name);
+ // If the case-insensitive match worked but the field is NOT a group,
+ if (field != NULL && field->type() != FieldDescriptor::TYPE_GROUP) {
+ field = NULL;
+ }
+ }
+ // Again, special-case group names as described above.
+ if (field != NULL && field->type() == FieldDescriptor::TYPE_GROUP
+ && field->message_type()->name() != field_name) {
+ field = NULL;
+ }
+
+ if (field == NULL && allow_case_insensitive_field_) {
+ string lower_field_name = field_name;
+ LowerString(&lower_field_name);
+ field = descriptor->FindFieldByLowercaseName(lower_field_name);
+ }
+ }
+
+ if (field == NULL) {
+ if (!allow_unknown_field_) {
+ ReportError("Message type \"" + descriptor->full_name() +
+ "\" has no field named \"" + field_name + "\".");
+ return false;
+ } else {
+ ReportWarning("Message type \"" + descriptor->full_name() +
+ "\" has no field named \"" + field_name + "\".");
+ }
+ }
+ }
+
+ // Skips unknown field.
+ if (field == NULL) {
+ GOOGLE_CHECK(allow_unknown_field_);
+ // Try to guess the type of this field.
+ // If this field is not a message, there should be a ":" between the
+ // field name and the field value and also the field value should not
+ // start with "{" or "<" which indicates the begining of a message body.
+ // If there is no ":" or there is a "{" or "<" after ":", this field has
+ // to be a message or the input is ill-formed.
+ if (TryConsume(":") && !LookingAt("{") && !LookingAt("<")) {
+ return SkipFieldValue();
+ } else {
+ return SkipFieldMessage();
+ }
+ }
+
+ if (singular_overwrite_policy_ == FORBID_SINGULAR_OVERWRITES) {
+ // Fail if the field is not repeated and it has already been specified.
+ if (!field->is_repeated() && reflection->HasField(*message, field)) {
+ ReportError("Non-repeated field \"" + field_name +
+ "\" is specified multiple times.");
+ return false;
+ }
+ // Fail if the field is a member of a oneof and another member has already
+ // been specified.
+ const OneofDescriptor* oneof = field->containing_oneof();
+ if (oneof != NULL && reflection->HasOneof(*message, oneof)) {
+ const FieldDescriptor* other_field =
+ reflection->GetOneofFieldDescriptor(*message, oneof);
+ ReportError("Field \"" + field_name + "\" is specified along with "
+ "field \"" + other_field->name() + "\", another member "
+ "of oneof \"" + oneof->name() + "\".");
+ return false;
+ }
+ }
+
+ // Perform special handling for embedded message types.
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ // ':' is optional here.
+ TryConsume(":");
+ } else {
+ // ':' is required here.
+ DO(Consume(":"));
+ }
+
+ if (field->is_repeated() && TryConsume("[")) {
+ // Short repeated format, e.g. "foo: [1, 2, 3]"
+ while (true) {
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ // Perform special handling for embedded message types.
+ DO(ConsumeFieldMessage(message, reflection, field));
+ } else {
+ DO(ConsumeFieldValue(message, reflection, field));
+ }
+ if (TryConsume("]")) {
+ break;
+ }
+ DO(Consume(","));
+ }
+ } else if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ DO(ConsumeFieldMessage(message, reflection, field));
+ } else {
+ DO(ConsumeFieldValue(message, reflection, field));
+ }
+
+ // For historical reasons, fields may optionally be separated by commas or
+ // semicolons.
+ TryConsume(";") || TryConsume(",");
+
+ if (field->options().deprecated()) {
+ ReportWarning("text format contains deprecated field \""
+ + field_name + "\"");
+ }
+
+ // If a parse info tree exists, add the location for the parsed
+ // field.
+ if (parse_info_tree_ != NULL) {
+ RecordLocation(parse_info_tree_, field,
+ ParseLocation(start_line, start_column));
+ }
+
+ return true;
+ }
+
+ // Skips the next field including the field's name and value.
+ bool SkipField() {
+ string field_name;
+ if (TryConsume("[")) {
+ // Extension name.
+ DO(ConsumeIdentifier(&field_name));
+ while (TryConsume(".")) {
+ string part;
+ DO(ConsumeIdentifier(&part));
+ field_name += ".";
+ field_name += part;
+ }
+ DO(Consume("]"));
+ } else {
+ DO(ConsumeIdentifier(&field_name));
+ }
+
+ // Try to guess the type of this field.
+ // If this field is not a message, there should be a ":" between the
+ // field name and the field value and also the field value should not
+ // start with "{" or "<" which indicates the begining of a message body.
+ // If there is no ":" or there is a "{" or "<" after ":", this field has
+ // to be a message or the input is ill-formed.
+ if (TryConsume(":") && !LookingAt("{") && !LookingAt("<")) {
+ DO(SkipFieldValue());
+ } else {
+ DO(SkipFieldMessage());
+ }
+ // For historical reasons, fields may optionally be separated by commas or
+ // semicolons.
+ TryConsume(";") || TryConsume(",");
+ return true;
+ }
+
+ bool ConsumeFieldMessage(Message* message,
+ const Reflection* reflection,
+ const FieldDescriptor* field) {
+
+ // If the parse information tree is not NULL, create a nested one
+ // for the nested message.
+ ParseInfoTree* parent = parse_info_tree_;
+ if (parent != NULL) {
+ parse_info_tree_ = CreateNested(parent, field);
+ }
+
+ string delimeter;
+ if (TryConsume("<")) {
+ delimeter = ">";
+ } else {
+ DO(Consume("{"));
+ delimeter = "}";
+ }
+
+ if (field->is_repeated()) {
+ DO(ConsumeMessage(reflection->AddMessage(message, field), delimeter));
+ } else {
+ DO(ConsumeMessage(reflection->MutableMessage(message, field),
+ delimeter));
+ }
+
+ // Reset the parse information tree.
+ parse_info_tree_ = parent;
+ return true;
+ }
+
+ // Skips the whole body of a message including the begining delimeter and
+ // the ending delimeter.
+ bool SkipFieldMessage() {
+ string delimeter;
+ if (TryConsume("<")) {
+ delimeter = ">";
+ } else {
+ DO(Consume("{"));
+ delimeter = "}";
+ }
+ while (!LookingAt(">") && !LookingAt("}")) {
+ DO(SkipField());
+ }
+ DO(Consume(delimeter));
+ return true;
+ }
+
+ bool ConsumeFieldValue(Message* message,
+ const Reflection* reflection,
+ const FieldDescriptor* field) {
+
+// Define an easy to use macro for setting fields. This macro checks
+// to see if the field is repeated (in which case we need to use the Add
+// methods or not (in which case we need to use the Set methods).
+#define SET_FIELD(CPPTYPE, VALUE) \
+ if (field->is_repeated()) { \
+ reflection->Add##CPPTYPE(message, field, VALUE); \
+ } else { \
+ reflection->Set##CPPTYPE(message, field, VALUE); \
+ } \
+
+ switch(field->cpp_type()) {
+ case FieldDescriptor::CPPTYPE_INT32: {
+ int64 value;
+ DO(ConsumeSignedInteger(&value, kint32max));
+ SET_FIELD(Int32, static_cast<int32>(value));
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_UINT32: {
+ uint64 value;
+ DO(ConsumeUnsignedInteger(&value, kuint32max));
+ SET_FIELD(UInt32, static_cast<uint32>(value));
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_INT64: {
+ int64 value;
+ DO(ConsumeSignedInteger(&value, kint64max));
+ SET_FIELD(Int64, value);
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_UINT64: {
+ uint64 value;
+ DO(ConsumeUnsignedInteger(&value, kuint64max));
+ SET_FIELD(UInt64, value);
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_FLOAT: {
+ double value;
+ DO(ConsumeDouble(&value));
+ SET_FIELD(Float, static_cast<float>(value));
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_DOUBLE: {
+ double value;
+ DO(ConsumeDouble(&value));
+ SET_FIELD(Double, value);
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_STRING: {
+ string value;
+ DO(ConsumeString(&value));
+ SET_FIELD(String, value);
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_BOOL: {
+ if (LookingAtType(io::Tokenizer::TYPE_INTEGER)) {
+ uint64 value;
+ DO(ConsumeUnsignedInteger(&value, 1));
+ SET_FIELD(Bool, value);
+ } else {
+ string value;
+ DO(ConsumeIdentifier(&value));
+ if (value == "true" || value == "True" || value == "t") {
+ SET_FIELD(Bool, true);
+ } else if (value == "false" || value == "False" || value == "f") {
+ SET_FIELD(Bool, false);
+ } else {
+ ReportError("Invalid value for boolean field \"" + field->name()
+ + "\". Value: \"" + value + "\".");
+ return false;
+ }
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_ENUM: {
+ string value;
+ const EnumDescriptor* enum_type = field->enum_type();
+ const EnumValueDescriptor* enum_value = NULL;
+
+ if (LookingAtType(io::Tokenizer::TYPE_IDENTIFIER)) {
+ DO(ConsumeIdentifier(&value));
+ // Find the enumeration value.
+ enum_value = enum_type->FindValueByName(value);
+
+ } else if (LookingAt("-") ||
+ LookingAtType(io::Tokenizer::TYPE_INTEGER)) {
+ int64 int_value;
+ DO(ConsumeSignedInteger(&int_value, kint32max));
+ value = SimpleItoa(int_value); // for error reporting
+ enum_value = enum_type->FindValueByNumber(int_value);
+ } else {
+ ReportError("Expected integer or identifier.");
+ return false;
+ }
+
+ if (enum_value == NULL) {
+ if (!allow_unknown_enum_) {
+ ReportError("Unknown enumeration value of \"" + value + "\" for "
+ "field \"" + field->name() + "\".");
+ return false;
+ } else {
+ ReportWarning("Unknown enumeration value of \"" + value + "\" for "
+ "field \"" + field->name() + "\".");
+ return true;
+ }
+ }
+
+ SET_FIELD(Enum, enum_value);
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_MESSAGE: {
+ // We should never get here. Put here instead of a default
+ // so that if new types are added, we get a nice compiler warning.
+ GOOGLE_LOG(FATAL) << "Reached an unintended state: CPPTYPE_MESSAGE";
+ break;
+ }
+ }
+#undef SET_FIELD
+ return true;
+ }
+
+ bool SkipFieldValue() {
+ if (LookingAtType(io::Tokenizer::TYPE_STRING)) {
+ while (LookingAtType(io::Tokenizer::TYPE_STRING)) {
+ tokenizer_.Next();
+ }
+ return true;
+ }
+ // Possible field values other than string:
+ // 12345 => TYPE_INTEGER
+ // -12345 => TYPE_SYMBOL + TYPE_INTEGER
+ // 1.2345 => TYPE_FLOAT
+ // -1.2345 => TYPE_SYMBOL + TYPE_FLOAT
+ // inf => TYPE_IDENTIFIER
+ // -inf => TYPE_SYMBOL + TYPE_IDENTIFIER
+ // TYPE_INTEGER => TYPE_IDENTIFIER
+ // Divides them into two group, one with TYPE_SYMBOL
+ // and the other without:
+ // Group one:
+ // 12345 => TYPE_INTEGER
+ // 1.2345 => TYPE_FLOAT
+ // inf => TYPE_IDENTIFIER
+ // TYPE_INTEGER => TYPE_IDENTIFIER
+ // Group two:
+ // -12345 => TYPE_SYMBOL + TYPE_INTEGER
+ // -1.2345 => TYPE_SYMBOL + TYPE_FLOAT
+ // -inf => TYPE_SYMBOL + TYPE_IDENTIFIER
+ // As we can see, the field value consists of an optional '-' and one of
+ // TYPE_INTEGER, TYPE_FLOAT and TYPE_IDENTIFIER.
+ bool has_minus = TryConsume("-");
+ if (!LookingAtType(io::Tokenizer::TYPE_INTEGER) &&
+ !LookingAtType(io::Tokenizer::TYPE_FLOAT) &&
+ !LookingAtType(io::Tokenizer::TYPE_IDENTIFIER)) {
+ return false;
+ }
+ // Combination of '-' and TYPE_IDENTIFIER may result in an invalid field
+ // value while other combinations all generate valid values.
+ // We check if the value of this combination is valid here.
+ // TYPE_IDENTIFIER after a '-' should be one of the float values listed
+ // below:
+ // inf, inff, infinity, nan
+ if (has_minus && LookingAtType(io::Tokenizer::TYPE_IDENTIFIER)) {
+ string text = tokenizer_.current().text;
+ LowerString(&text);
+ if (text != "inf" &&
+ text != "infinity" &&
+ text != "nan") {
+ ReportError("Invalid float number: " + text);
+ return false;
+ }
+ }
+ tokenizer_.Next();
+ return true;
+ }
+
+ // Returns true if the current token's text is equal to that specified.
+ bool LookingAt(const string& text) {
+ return tokenizer_.current().text == text;
+ }
+
+ // Returns true if the current token's type is equal to that specified.
+ bool LookingAtType(io::Tokenizer::TokenType token_type) {
+ return tokenizer_.current().type == token_type;
+ }
+
+ // Consumes an identifier and saves its value in the identifier parameter.
+ // Returns false if the token is not of type IDENTFIER.
+ bool ConsumeIdentifier(string* identifier) {
+ if (LookingAtType(io::Tokenizer::TYPE_IDENTIFIER)) {
+ *identifier = tokenizer_.current().text;
+ tokenizer_.Next();
+ return true;
+ }
+
+ // If allow_field_numer_ or allow_unknown_field_ is true, we should able
+ // to parse integer identifiers.
+ if ((allow_field_number_ || allow_unknown_field_)
+ && LookingAtType(io::Tokenizer::TYPE_INTEGER)) {
+ *identifier = tokenizer_.current().text;
+ tokenizer_.Next();
+ return true;
+ }
+
+ ReportError("Expected identifier.");
+ return false;
+ }
+
+ // Consumes a string and saves its value in the text parameter.
+ // Returns false if the token is not of type STRING.
+ bool ConsumeString(string* text) {
+ if (!LookingAtType(io::Tokenizer::TYPE_STRING)) {
+ ReportError("Expected string.");
+ return false;
+ }
+
+ text->clear();
+ while (LookingAtType(io::Tokenizer::TYPE_STRING)) {
+ io::Tokenizer::ParseStringAppend(tokenizer_.current().text, text);
+
+ tokenizer_.Next();
+ }
+
+ return true;
+ }
+
+ // Consumes a uint64 and saves its value in the value parameter.
+ // Returns false if the token is not of type INTEGER.
+ bool ConsumeUnsignedInteger(uint64* value, uint64 max_value) {
+ if (!LookingAtType(io::Tokenizer::TYPE_INTEGER)) {
+ ReportError("Expected integer.");
+ return false;
+ }
+
+ if (!io::Tokenizer::ParseInteger(tokenizer_.current().text,
+ max_value, value)) {
+ ReportError("Integer out of range.");
+ return false;
+ }
+
+ tokenizer_.Next();
+ return true;
+ }
+
+ // Consumes an int64 and saves its value in the value parameter.
+ // Note that since the tokenizer does not support negative numbers,
+ // we actually may consume an additional token (for the minus sign) in this
+ // method. Returns false if the token is not an integer
+ // (signed or otherwise).
+ bool ConsumeSignedInteger(int64* value, uint64 max_value) {
+ bool negative = false;
+
+ if (TryConsume("-")) {
+ negative = true;
+ // Two's complement always allows one more negative integer than
+ // positive.
+ ++max_value;
+ }
+
+ uint64 unsigned_value;
+
+ DO(ConsumeUnsignedInteger(&unsigned_value, max_value));
+
+ *value = static_cast<int64>(unsigned_value);
+
+ if (negative) {
+ *value = -*value;
+ }
+
+ return true;
+ }
+
+ // Consumes a uint64 and saves its value in the value parameter.
+ // Accepts decimal numbers only, rejects hex or oct numbers.
+ bool ConsumeUnsignedDecimalInteger(uint64* value, uint64 max_value) {
+ if (!LookingAtType(io::Tokenizer::TYPE_INTEGER)) {
+ ReportError("Expected integer.");
+ return false;
+ }
+
+ const string& text = tokenizer_.current().text;
+ if (IsHexNumber(text) || IsOctNumber(text)) {
+ ReportError("Expect a decimal number.");
+ return false;
+ }
+
+ if (!io::Tokenizer::ParseInteger(text, max_value, value)) {
+ ReportError("Integer out of range.");
+ return false;
+ }
+
+ tokenizer_.Next();
+ return true;
+ }
+
+ // Consumes a double and saves its value in the value parameter.
+ // Note that since the tokenizer does not support negative numbers,
+ // we actually may consume an additional token (for the minus sign) in this
+ // method. Returns false if the token is not a double
+ // (signed or otherwise).
+ bool ConsumeDouble(double* value) {
+ bool negative = false;
+
+ if (TryConsume("-")) {
+ negative = true;
+ }
+
+ // A double can actually be an integer, according to the tokenizer.
+ // Therefore, we must check both cases here.
+ if (LookingAtType(io::Tokenizer::TYPE_INTEGER)) {
+ // We have found an integer value for the double.
+ uint64 integer_value;
+ DO(ConsumeUnsignedDecimalInteger(&integer_value, kuint64max));
+
+ *value = static_cast<double>(integer_value);
+ } else if (LookingAtType(io::Tokenizer::TYPE_FLOAT)) {
+ // We have found a float value for the double.
+ *value = io::Tokenizer::ParseFloat(tokenizer_.current().text);
+
+ // Mark the current token as consumed.
+ tokenizer_.Next();
+ } else if (LookingAtType(io::Tokenizer::TYPE_IDENTIFIER)) {
+ string text = tokenizer_.current().text;
+ LowerString(&text);
+ if (text == "inf" ||
+ text == "infinity") {
+ *value = std::numeric_limits<double>::infinity();
+ tokenizer_.Next();
+ } else if (text == "nan") {
+ *value = std::numeric_limits<double>::quiet_NaN();
+ tokenizer_.Next();
+ } else {
+ ReportError("Expected double.");
+ return false;
+ }
+ } else {
+ ReportError("Expected double.");
+ return false;
+ }
+
+ if (negative) {
+ *value = -*value;
+ }
+
+ return true;
+ }
+
+ // Consumes a token and confirms that it matches that specified in the
+ // value parameter. Returns false if the token found does not match that
+ // which was specified.
+ bool Consume(const string& value) {
+ const string& current_value = tokenizer_.current().text;
+
+ if (current_value != value) {
+ ReportError("Expected \"" + value + "\", found \"" + current_value
+ + "\".");
+ return false;
+ }
+
+ tokenizer_.Next();
+
+ return true;
+ }
+
+ // Attempts to consume the supplied value. Returns false if a the
+ // token found does not match the value specified.
+ bool TryConsume(const string& value) {
+ if (tokenizer_.current().text == value) {
+ tokenizer_.Next();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // An internal instance of the Tokenizer's error collector, used to
+ // collect any base-level parse errors and feed them to the ParserImpl.
+ class ParserErrorCollector : public io::ErrorCollector {
+ public:
+ explicit ParserErrorCollector(TextFormat::Parser::ParserImpl* parser) :
+ parser_(parser) { }
+
+ virtual ~ParserErrorCollector() { }
+
+ virtual void AddError(int line, int column, const string& message) {
+ parser_->ReportError(line, column, message);
+ }
+
+ virtual void AddWarning(int line, int column, const string& message) {
+ parser_->ReportWarning(line, column, message);
+ }
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ParserErrorCollector);
+ TextFormat::Parser::ParserImpl* parser_;
+ };
+
+ io::ErrorCollector* error_collector_;
+ TextFormat::Finder* finder_;
+ ParseInfoTree* parse_info_tree_;
+ ParserErrorCollector tokenizer_error_collector_;
+ io::Tokenizer tokenizer_;
+ const Descriptor* root_message_type_;
+ SingularOverwritePolicy singular_overwrite_policy_;
+ const bool allow_case_insensitive_field_;
+ const bool allow_unknown_field_;
+ const bool allow_unknown_enum_;
+ const bool allow_field_number_;
+ bool had_errors_;
+};
+
+#undef DO
+
+// ===========================================================================
+// Internal class for writing text to the io::ZeroCopyOutputStream. Adapted
+// from the Printer found in //google/protobuf/io/printer.h
+class TextFormat::Printer::TextGenerator {
+ public:
+ explicit TextGenerator(io::ZeroCopyOutputStream* output,
+ int initial_indent_level)
+ : output_(output),
+ buffer_(NULL),
+ buffer_size_(0),
+ at_start_of_line_(true),
+ failed_(false),
+ indent_(""),
+ initial_indent_level_(initial_indent_level) {
+ indent_.resize(initial_indent_level_ * 2, ' ');
+ }
+
+ ~TextGenerator() {
+ // Only BackUp() if we're sure we've successfully called Next() at least
+ // once.
+ if (!failed_ && buffer_size_ > 0) {
+ output_->BackUp(buffer_size_);
+ }
+ }
+
+ // Indent text by two spaces. After calling Indent(), two spaces will be
+ // inserted at the beginning of each line of text. Indent() may be called
+ // multiple times to produce deeper indents.
+ void Indent() {
+ indent_ += " ";
+ }
+
+ // Reduces the current indent level by two spaces, or crashes if the indent
+ // level is zero.
+ void Outdent() {
+ if (indent_.empty() ||
+ indent_.size() < initial_indent_level_ * 2) {
+ GOOGLE_LOG(DFATAL) << " Outdent() without matching Indent().";
+ return;
+ }
+
+ indent_.resize(indent_.size() - 2);
+ }
+
+ // Print text to the output stream.
+ void Print(const string& str) {
+ Print(str.data(), str.size());
+ }
+
+ // Print text to the output stream.
+ void Print(const char* text) {
+ Print(text, strlen(text));
+ }
+
+ // Print text to the output stream.
+ void Print(const char* text, int size) {
+ int pos = 0; // The number of bytes we've written so far.
+
+ for (int i = 0; i < size; i++) {
+ if (text[i] == '\n') {
+ // Saw newline. If there is more text, we may need to insert an indent
+ // here. So, write what we have so far, including the '\n'.
+ Write(text + pos, i - pos + 1);
+ pos = i + 1;
+
+ // Setting this true will cause the next Write() to insert an indent
+ // first.
+ at_start_of_line_ = true;
+ }
+ }
+
+ // Write the rest.
+ Write(text + pos, size - pos);
+ }
+
+ // True if any write to the underlying stream failed. (We don't just
+ // crash in this case because this is an I/O failure, not a programming
+ // error.)
+ bool failed() const { return failed_; }
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(TextGenerator);
+
+ void Write(const char* data, int size) {
+ if (failed_) return;
+ if (size == 0) return;
+
+ if (at_start_of_line_) {
+ // Insert an indent.
+ at_start_of_line_ = false;
+ Write(indent_.data(), indent_.size());
+ if (failed_) return;
+ }
+
+ while (size > buffer_size_) {
+ // Data exceeds space in the buffer. Copy what we can and request a
+ // new buffer.
+ memcpy(buffer_, data, buffer_size_);
+ data += buffer_size_;
+ size -= buffer_size_;
+ void* void_buffer;
+ failed_ = !output_->Next(&void_buffer, &buffer_size_);
+ if (failed_) return;
+ buffer_ = reinterpret_cast<char*>(void_buffer);
+ }
+
+ // Buffer is big enough to receive the data; copy it.
+ memcpy(buffer_, data, size);
+ buffer_ += size;
+ buffer_size_ -= size;
+ }
+
+ io::ZeroCopyOutputStream* const output_;
+ char* buffer_;
+ int buffer_size_;
+ bool at_start_of_line_;
+ bool failed_;
+
+ string indent_;
+ int initial_indent_level_;
+};
+
+// ===========================================================================
+
+TextFormat::Finder::~Finder() {
+}
+
+TextFormat::Parser::Parser()
+ : error_collector_(NULL),
+ finder_(NULL),
+ parse_info_tree_(NULL),
+ allow_partial_(false),
+ allow_case_insensitive_field_(false),
+ allow_unknown_field_(false),
+ allow_unknown_enum_(false),
+ allow_field_number_(false),
+ allow_relaxed_whitespace_(false),
+ allow_singular_overwrites_(false) {
+}
+
+TextFormat::Parser::~Parser() {}
+
+bool TextFormat::Parser::Parse(io::ZeroCopyInputStream* input,
+ Message* output) {
+ output->Clear();
+
+ ParserImpl::SingularOverwritePolicy overwrites_policy =
+ allow_singular_overwrites_
+ ? ParserImpl::ALLOW_SINGULAR_OVERWRITES
+ : ParserImpl::FORBID_SINGULAR_OVERWRITES;
+
+ ParserImpl parser(output->GetDescriptor(), input, error_collector_,
+ finder_, parse_info_tree_,
+ overwrites_policy,
+ allow_case_insensitive_field_, allow_unknown_field_,
+ allow_unknown_enum_, allow_field_number_,
+ allow_relaxed_whitespace_);
+ return MergeUsingImpl(input, output, &parser);
+}
+
+bool TextFormat::Parser::ParseFromString(const string& input,
+ Message* output) {
+ io::ArrayInputStream input_stream(input.data(), input.size());
+ return Parse(&input_stream, output);
+}
+
+bool TextFormat::Parser::Merge(io::ZeroCopyInputStream* input,
+ Message* output) {
+ ParserImpl parser(output->GetDescriptor(), input, error_collector_,
+ finder_, parse_info_tree_,
+ ParserImpl::ALLOW_SINGULAR_OVERWRITES,
+ allow_case_insensitive_field_, allow_unknown_field_,
+ allow_unknown_enum_, allow_field_number_,
+ allow_relaxed_whitespace_);
+ return MergeUsingImpl(input, output, &parser);
+}
+
+bool TextFormat::Parser::MergeFromString(const string& input,
+ Message* output) {
+ io::ArrayInputStream input_stream(input.data(), input.size());
+ return Merge(&input_stream, output);
+}
+
+bool TextFormat::Parser::MergeUsingImpl(io::ZeroCopyInputStream* /* input */,
+ Message* output,
+ ParserImpl* parser_impl) {
+ if (!parser_impl->Parse(output)) return false;
+ if (!allow_partial_ && !output->IsInitialized()) {
+ vector<string> missing_fields;
+ output->FindInitializationErrors(&missing_fields);
+ parser_impl->ReportError(-1, 0, "Message missing required fields: " +
+ Join(missing_fields, ", "));
+ return false;
+ }
+ return true;
+}
+
+bool TextFormat::Parser::ParseFieldValueFromString(
+ const string& input,
+ const FieldDescriptor* field,
+ Message* output) {
+ io::ArrayInputStream input_stream(input.data(), input.size());
+ ParserImpl parser(output->GetDescriptor(), &input_stream, error_collector_,
+ finder_, parse_info_tree_,
+ ParserImpl::ALLOW_SINGULAR_OVERWRITES,
+ allow_case_insensitive_field_, allow_unknown_field_,
+ allow_unknown_enum_, allow_field_number_,
+ allow_relaxed_whitespace_);
+ return parser.ParseField(field, output);
+}
+
+/* static */ bool TextFormat::Parse(io::ZeroCopyInputStream* input,
+ Message* output) {
+ return Parser().Parse(input, output);
+}
+
+/* static */ bool TextFormat::Merge(io::ZeroCopyInputStream* input,
+ Message* output) {
+ return Parser().Merge(input, output);
+}
+
+/* static */ bool TextFormat::ParseFromString(const string& input,
+ Message* output) {
+ return Parser().ParseFromString(input, output);
+}
+
+/* static */ bool TextFormat::MergeFromString(const string& input,
+ Message* output) {
+ return Parser().MergeFromString(input, output);
+}
+
+// ===========================================================================
+
+// The default implementation for FieldValuePrinter. The base class just
+// does simple formatting. That way, deriving classes could decide to fallback
+// to that behavior.
+TextFormat::FieldValuePrinter::FieldValuePrinter() {}
+TextFormat::FieldValuePrinter::~FieldValuePrinter() {}
+string TextFormat::FieldValuePrinter::PrintBool(bool val) const {
+ return val ? "true" : "false";
+}
+string TextFormat::FieldValuePrinter::PrintInt32(int32 val) const {
+ return SimpleItoa(val);
+}
+string TextFormat::FieldValuePrinter::PrintUInt32(uint32 val) const {
+ return SimpleItoa(val);
+}
+string TextFormat::FieldValuePrinter::PrintInt64(int64 val) const {
+ return SimpleItoa(val);
+}
+string TextFormat::FieldValuePrinter::PrintUInt64(uint64 val) const {
+ return SimpleItoa(val);
+}
+string TextFormat::FieldValuePrinter::PrintFloat(float val) const {
+ return SimpleFtoa(val);
+}
+string TextFormat::FieldValuePrinter::PrintDouble(double val) const {
+ return SimpleDtoa(val);
+}
+string TextFormat::FieldValuePrinter::PrintString(const string& val) const {
+ return StrCat("\"", CEscape(val), "\"");
+}
+string TextFormat::FieldValuePrinter::PrintBytes(const string& val) const {
+ return PrintString(val);
+}
+string TextFormat::FieldValuePrinter::PrintEnum(int32 val,
+ const string& name) const {
+ return name;
+}
+string TextFormat::FieldValuePrinter::PrintFieldName(
+ const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field) const {
+ if (field->is_extension()) {
+ // We special-case MessageSet elements for compatibility with proto1.
+ if (field->containing_type()->options().message_set_wire_format()
+ && field->type() == FieldDescriptor::TYPE_MESSAGE
+ && field->is_optional()
+ && field->extension_scope() == field->message_type()) {
+ return StrCat("[", field->message_type()->full_name(), "]");
+ } else {
+ return StrCat("[", field->full_name(), "]");
+ }
+ } else if (field->type() == FieldDescriptor::TYPE_GROUP) {
+ // Groups must be serialized with their original capitalization.
+ return field->message_type()->name();
+ } else {
+ return field->name();
+ }
+}
+string TextFormat::FieldValuePrinter::PrintMessageStart(
+ const Message& message,
+ int field_index,
+ int field_count,
+ bool single_line_mode) const {
+ return single_line_mode ? " { " : " {\n";
+}
+string TextFormat::FieldValuePrinter::PrintMessageEnd(
+ const Message& message,
+ int field_index,
+ int field_count,
+ bool single_line_mode) const {
+ return single_line_mode ? "} " : "}\n";
+}
+
+namespace {
+// Our own specialization: for UTF8 escaped strings.
+class FieldValuePrinterUtf8Escaping : public TextFormat::FieldValuePrinter {
+ public:
+ virtual string PrintString(const string& val) const {
+ return StrCat("\"", strings::Utf8SafeCEscape(val), "\"");
+ }
+ virtual string PrintBytes(const string& val) const {
+ return TextFormat::FieldValuePrinter::PrintString(val);
+ }
+};
+
+} // namespace
+
+TextFormat::Printer::Printer()
+ : initial_indent_level_(0),
+ single_line_mode_(false),
+ use_field_number_(false),
+ use_short_repeated_primitives_(false),
+ hide_unknown_fields_(false),
+ print_message_fields_in_index_order_(false) {
+ SetUseUtf8StringEscaping(false);
+}
+
+TextFormat::Printer::~Printer() {
+ STLDeleteValues(&custom_printers_);
+}
+
+void TextFormat::Printer::SetUseUtf8StringEscaping(bool as_utf8) {
+ SetDefaultFieldValuePrinter(as_utf8
+ ? new FieldValuePrinterUtf8Escaping()
+ : new FieldValuePrinter());
+}
+
+void TextFormat::Printer::SetDefaultFieldValuePrinter(
+ const FieldValuePrinter* printer) {
+ default_field_value_printer_.reset(printer);
+}
+
+bool TextFormat::Printer::RegisterFieldValuePrinter(
+ const FieldDescriptor* field,
+ const FieldValuePrinter* printer) {
+ return field != NULL
+ && printer != NULL
+ && custom_printers_.insert(make_pair(field, printer)).second;
+}
+
+bool TextFormat::Printer::PrintToString(const Message& message,
+ string* output) const {
+ GOOGLE_DCHECK(output) << "output specified is NULL";
+
+ output->clear();
+ io::StringOutputStream output_stream(output);
+
+ return Print(message, &output_stream);
+}
+
+bool TextFormat::Printer::PrintUnknownFieldsToString(
+ const UnknownFieldSet& unknown_fields,
+ string* output) const {
+ GOOGLE_DCHECK(output) << "output specified is NULL";
+
+ output->clear();
+ io::StringOutputStream output_stream(output);
+ return PrintUnknownFields(unknown_fields, &output_stream);
+}
+
+bool TextFormat::Printer::Print(const Message& message,
+ io::ZeroCopyOutputStream* output) const {
+ TextGenerator generator(output, initial_indent_level_);
+
+ Print(message, generator);
+
+ // Output false if the generator failed internally.
+ return !generator.failed();
+}
+
+bool TextFormat::Printer::PrintUnknownFields(
+ const UnknownFieldSet& unknown_fields,
+ io::ZeroCopyOutputStream* output) const {
+ TextGenerator generator(output, initial_indent_level_);
+
+ PrintUnknownFields(unknown_fields, generator);
+
+ // Output false if the generator failed internally.
+ return !generator.failed();
+}
+
+namespace {
+// Comparison functor for sorting FieldDescriptors by field index.
+struct FieldIndexSorter {
+ bool operator()(const FieldDescriptor* left,
+ const FieldDescriptor* right) const {
+ return left->index() < right->index();
+ }
+};
+} // namespace
+
+void TextFormat::Printer::Print(const Message& message,
+ TextGenerator& generator) const {
+ const Reflection* reflection = message.GetReflection();
+ vector<const FieldDescriptor*> fields;
+ reflection->ListFields(message, &fields);
+ if (print_message_fields_in_index_order_) {
+ sort(fields.begin(), fields.end(), FieldIndexSorter());
+ }
+ for (int i = 0; i < fields.size(); i++) {
+ PrintField(message, reflection, fields[i], generator);
+ }
+ if (!hide_unknown_fields_) {
+ PrintUnknownFields(reflection->GetUnknownFields(message), generator);
+ }
+}
+
+void TextFormat::Printer::PrintFieldValueToString(
+ const Message& message,
+ const FieldDescriptor* field,
+ int index,
+ string* output) const {
+
+ GOOGLE_DCHECK(output) << "output specified is NULL";
+
+ output->clear();
+ io::StringOutputStream output_stream(output);
+ TextGenerator generator(&output_stream, initial_indent_level_);
+
+ PrintFieldValue(message, message.GetReflection(), field, index, generator);
+}
+
+void TextFormat::Printer::PrintField(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ TextGenerator& generator) const {
+ if (use_short_repeated_primitives_ &&
+ field->is_repeated() &&
+ field->cpp_type() != FieldDescriptor::CPPTYPE_STRING &&
+ field->cpp_type() != FieldDescriptor::CPPTYPE_MESSAGE) {
+ PrintShortRepeatedField(message, reflection, field, generator);
+ return;
+ }
+
+ int count = 0;
+
+ if (field->is_repeated()) {
+ count = reflection->FieldSize(message, field);
+ } else if (reflection->HasField(message, field)) {
+ count = 1;
+ }
+
+ for (int j = 0; j < count; ++j) {
+ const int field_index = field->is_repeated() ? j : -1;
+
+ PrintFieldName(message, reflection, field, generator);
+
+ if (field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+ const FieldValuePrinter* printer = FindWithDefault(
+ custom_printers_, field, default_field_value_printer_.get());
+ const Message& sub_message =
+ field->is_repeated()
+ ? reflection->GetRepeatedMessage(message, field, j)
+ : reflection->GetMessage(message, field);
+ generator.Print(
+ printer->PrintMessageStart(
+ sub_message, field_index, count, single_line_mode_));
+ generator.Indent();
+ Print(sub_message, generator);
+ generator.Outdent();
+ generator.Print(
+ printer->PrintMessageEnd(
+ sub_message, field_index, count, single_line_mode_));
+ } else {
+ generator.Print(": ");
+ // Write the field value.
+ PrintFieldValue(message, reflection, field, field_index, generator);
+ if (single_line_mode_) {
+ generator.Print(" ");
+ } else {
+ generator.Print("\n");
+ }
+ }
+ }
+}
+
+void TextFormat::Printer::PrintShortRepeatedField(
+ const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ TextGenerator& generator) const {
+ // Print primitive repeated field in short form.
+ PrintFieldName(message, reflection, field, generator);
+
+ int size = reflection->FieldSize(message, field);
+ generator.Print(": [");
+ for (int i = 0; i < size; i++) {
+ if (i > 0) generator.Print(", ");
+ PrintFieldValue(message, reflection, field, i, generator);
+ }
+ if (single_line_mode_) {
+ generator.Print("] ");
+ } else {
+ generator.Print("]\n");
+ }
+}
+
+void TextFormat::Printer::PrintFieldName(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ TextGenerator& generator) const {
+ // if use_field_number_ is true, prints field number instead
+ // of field name.
+ if (use_field_number_) {
+ generator.Print(SimpleItoa(field->number()));
+ return;
+ }
+
+ const FieldValuePrinter* printer = FindWithDefault(
+ custom_printers_, field, default_field_value_printer_.get());
+ generator.Print(printer->PrintFieldName(message, reflection, field));
+}
+
+void TextFormat::Printer::PrintFieldValue(
+ const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ int index,
+ TextGenerator& generator) const {
+ GOOGLE_DCHECK(field->is_repeated() || (index == -1))
+ << "Index must be -1 for non-repeated fields";
+
+ const FieldValuePrinter* printer
+ = FindWithDefault(custom_printers_, field,
+ default_field_value_printer_.get());
+
+ switch (field->cpp_type()) {
+#define OUTPUT_FIELD(CPPTYPE, METHOD) \
+ case FieldDescriptor::CPPTYPE_##CPPTYPE: \
+ generator.Print(printer->Print##METHOD(field->is_repeated() \
+ ? reflection->GetRepeated##METHOD(message, field, index) \
+ : reflection->Get##METHOD(message, field))); \
+ break
+
+ OUTPUT_FIELD( INT32, Int32);
+ OUTPUT_FIELD( INT64, Int64);
+ OUTPUT_FIELD(UINT32, UInt32);
+ OUTPUT_FIELD(UINT64, UInt64);
+ OUTPUT_FIELD( FLOAT, Float);
+ OUTPUT_FIELD(DOUBLE, Double);
+ OUTPUT_FIELD( BOOL, Bool);
+#undef OUTPUT_FIELD
+
+ case FieldDescriptor::CPPTYPE_STRING: {
+ string scratch;
+ const string& value = field->is_repeated()
+ ? reflection->GetRepeatedStringReference(
+ message, field, index, &scratch)
+ : reflection->GetStringReference(message, field, &scratch);
+ if (field->type() == FieldDescriptor::TYPE_STRING) {
+ generator.Print(printer->PrintString(value));
+ } else {
+ GOOGLE_DCHECK_EQ(field->type(), FieldDescriptor::TYPE_BYTES);
+ generator.Print(printer->PrintBytes(value));
+ }
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_ENUM: {
+ const EnumValueDescriptor *enum_val = field->is_repeated()
+ ? reflection->GetRepeatedEnum(message, field, index)
+ : reflection->GetEnum(message, field);
+ generator.Print(printer->PrintEnum(enum_val->number(), enum_val->name()));
+ break;
+ }
+
+ case FieldDescriptor::CPPTYPE_MESSAGE:
+ Print(field->is_repeated()
+ ? reflection->GetRepeatedMessage(message, field, index)
+ : reflection->GetMessage(message, field),
+ generator);
+ break;
+ }
+}
+
+/* static */ bool TextFormat::Print(const Message& message,
+ io::ZeroCopyOutputStream* output) {
+ return Printer().Print(message, output);
+}
+
+/* static */ bool TextFormat::PrintUnknownFields(
+ const UnknownFieldSet& unknown_fields,
+ io::ZeroCopyOutputStream* output) {
+ return Printer().PrintUnknownFields(unknown_fields, output);
+}
+
+/* static */ bool TextFormat::PrintToString(
+ const Message& message, string* output) {
+ return Printer().PrintToString(message, output);
+}
+
+/* static */ bool TextFormat::PrintUnknownFieldsToString(
+ const UnknownFieldSet& unknown_fields, string* output) {
+ return Printer().PrintUnknownFieldsToString(unknown_fields, output);
+}
+
+/* static */ void TextFormat::PrintFieldValueToString(
+ const Message& message,
+ const FieldDescriptor* field,
+ int index,
+ string* output) {
+ return Printer().PrintFieldValueToString(message, field, index, output);
+}
+
+/* static */ bool TextFormat::ParseFieldValueFromString(
+ const string& input,
+ const FieldDescriptor* field,
+ Message* message) {
+ return Parser().ParseFieldValueFromString(input, field, message);
+}
+
+// Prints an integer as hex with a fixed number of digits dependent on the
+// integer type.
+template<typename IntType>
+static string PaddedHex(IntType value) {
+ string result;
+ result.reserve(sizeof(value) * 2);
+ for (int i = sizeof(value) * 2 - 1; i >= 0; i--) {
+ result.push_back(int_to_hex_digit(value >> (i*4) & 0x0F));
+ }
+ return result;
+}
+
+void TextFormat::Printer::PrintUnknownFields(
+ const UnknownFieldSet& unknown_fields, TextGenerator& generator) const {
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+ string field_number = SimpleItoa(field.number());
+
+ switch (field.type()) {
+ case UnknownField::TYPE_VARINT:
+ generator.Print(field_number);
+ generator.Print(": ");
+ generator.Print(SimpleItoa(field.varint()));
+ if (single_line_mode_) {
+ generator.Print(" ");
+ } else {
+ generator.Print("\n");
+ }
+ break;
+ case UnknownField::TYPE_FIXED32: {
+ generator.Print(field_number);
+ generator.Print(": 0x");
+ char buffer[kFastToBufferSize];
+ generator.Print(FastHex32ToBuffer(field.fixed32(), buffer));
+ if (single_line_mode_) {
+ generator.Print(" ");
+ } else {
+ generator.Print("\n");
+ }
+ break;
+ }
+ case UnknownField::TYPE_FIXED64: {
+ generator.Print(field_number);
+ generator.Print(": 0x");
+ char buffer[kFastToBufferSize];
+ generator.Print(FastHex64ToBuffer(field.fixed64(), buffer));
+ if (single_line_mode_) {
+ generator.Print(" ");
+ } else {
+ generator.Print("\n");
+ }
+ break;
+ }
+ case UnknownField::TYPE_LENGTH_DELIMITED: {
+ generator.Print(field_number);
+ const string& value = field.length_delimited();
+ UnknownFieldSet embedded_unknown_fields;
+ if (!value.empty() && embedded_unknown_fields.ParseFromString(value)) {
+ // This field is parseable as a Message.
+ // So it is probably an embedded message.
+ if (single_line_mode_) {
+ generator.Print(" { ");
+ } else {
+ generator.Print(" {\n");
+ generator.Indent();
+ }
+ PrintUnknownFields(embedded_unknown_fields, generator);
+ if (single_line_mode_) {
+ generator.Print("} ");
+ } else {
+ generator.Outdent();
+ generator.Print("}\n");
+ }
+ } else {
+ // This field is not parseable as a Message.
+ // So it is probably just a plain string.
+ generator.Print(": \"");
+ generator.Print(CEscape(value));
+ generator.Print("\"");
+ if (single_line_mode_) {
+ generator.Print(" ");
+ } else {
+ generator.Print("\n");
+ }
+ }
+ break;
+ }
+ case UnknownField::TYPE_GROUP:
+ generator.Print(field_number);
+ if (single_line_mode_) {
+ generator.Print(" { ");
+ } else {
+ generator.Print(" {\n");
+ generator.Indent();
+ }
+ PrintUnknownFields(field.group(), generator);
+ if (single_line_mode_) {
+ generator.Print("} ");
+ } else {
+ generator.Outdent();
+ generator.Print("}\n");
+ }
+ break;
+ }
+ }
+}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/text_format.h b/toolkit/components/protobuf/src/google/protobuf/text_format.h
new file mode 100644
index 0000000000..2954941082
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/text_format.h
@@ -0,0 +1,473 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: jschorr@google.com (Joseph Schorr)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Utilities for printing and parsing protocol messages in a human-readable,
+// text-based format.
+
+#ifndef GOOGLE_PROTOBUF_TEXT_FORMAT_H__
+#define GOOGLE_PROTOBUF_TEXT_FORMAT_H__
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/message.h>
+
+namespace google {
+namespace protobuf {
+
+namespace io {
+ class ErrorCollector; // tokenizer.h
+}
+
+// This class implements protocol buffer text format. Printing and parsing
+// protocol messages in text format is useful for debugging and human editing
+// of messages.
+//
+// This class is really a namespace that contains only static methods.
+class LIBPROTOBUF_EXPORT TextFormat {
+ public:
+ // Outputs a textual representation of the given message to the given
+ // output stream.
+ static bool Print(const Message& message, io::ZeroCopyOutputStream* output);
+
+ // Print the fields in an UnknownFieldSet. They are printed by tag number
+ // only. Embedded messages are heuristically identified by attempting to
+ // parse them.
+ static bool PrintUnknownFields(const UnknownFieldSet& unknown_fields,
+ io::ZeroCopyOutputStream* output);
+
+ // Like Print(), but outputs directly to a string.
+ static bool PrintToString(const Message& message, string* output);
+
+ // Like PrintUnknownFields(), but outputs directly to a string.
+ static bool PrintUnknownFieldsToString(const UnknownFieldSet& unknown_fields,
+ string* output);
+
+ // Outputs a textual representation of the value of the field supplied on
+ // the message supplied. For non-repeated fields, an index of -1 must
+ // be supplied. Note that this method will print the default value for a
+ // field if it is not set.
+ static void PrintFieldValueToString(const Message& message,
+ const FieldDescriptor* field,
+ int index,
+ string* output);
+
+ // The default printer that converts scalar values from fields into
+ // their string representation.
+ // You can derive from this FieldValuePrinter if you want to have
+ // fields to be printed in a different way and register it at the
+ // Printer.
+ class LIBPROTOBUF_EXPORT FieldValuePrinter {
+ public:
+ FieldValuePrinter();
+ virtual ~FieldValuePrinter();
+ virtual string PrintBool(bool val) const;
+ virtual string PrintInt32(int32 val) const;
+ virtual string PrintUInt32(uint32 val) const;
+ virtual string PrintInt64(int64 val) const;
+ virtual string PrintUInt64(uint64 val) const;
+ virtual string PrintFloat(float val) const;
+ virtual string PrintDouble(double val) const;
+ virtual string PrintString(const string& val) const;
+ virtual string PrintBytes(const string& val) const;
+ virtual string PrintEnum(int32 val, const string& name) const;
+ virtual string PrintFieldName(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field) const;
+ virtual string PrintMessageStart(const Message& message,
+ int field_index,
+ int field_count,
+ bool single_line_mode) const;
+ virtual string PrintMessageEnd(const Message& message,
+ int field_index,
+ int field_count,
+ bool single_line_mode) const;
+
+ private:
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(FieldValuePrinter);
+ };
+
+ // Class for those users which require more fine-grained control over how
+ // a protobuffer message is printed out.
+ class LIBPROTOBUF_EXPORT Printer {
+ public:
+ Printer();
+ ~Printer();
+
+ // Like TextFormat::Print
+ bool Print(const Message& message, io::ZeroCopyOutputStream* output) const;
+ // Like TextFormat::PrintUnknownFields
+ bool PrintUnknownFields(const UnknownFieldSet& unknown_fields,
+ io::ZeroCopyOutputStream* output) const;
+ // Like TextFormat::PrintToString
+ bool PrintToString(const Message& message, string* output) const;
+ // Like TextFormat::PrintUnknownFieldsToString
+ bool PrintUnknownFieldsToString(const UnknownFieldSet& unknown_fields,
+ string* output) const;
+ // Like TextFormat::PrintFieldValueToString
+ void PrintFieldValueToString(const Message& message,
+ const FieldDescriptor* field,
+ int index,
+ string* output) const;
+
+ // Adjust the initial indent level of all output. Each indent level is
+ // equal to two spaces.
+ void SetInitialIndentLevel(int indent_level) {
+ initial_indent_level_ = indent_level;
+ }
+
+ // If printing in single line mode, then the entire message will be output
+ // on a single line with no line breaks.
+ void SetSingleLineMode(bool single_line_mode) {
+ single_line_mode_ = single_line_mode;
+ }
+
+ bool IsInSingleLineMode() {
+ return single_line_mode_;
+ }
+
+ // If use_field_number is true, uses field number instead of field name.
+ void SetUseFieldNumber(bool use_field_number) {
+ use_field_number_ = use_field_number;
+ }
+
+ // Set true to print repeated primitives in a format like:
+ // field_name: [1, 2, 3, 4]
+ // instead of printing each value on its own line. Short format applies
+ // only to primitive values -- i.e. everything except strings and
+ // sub-messages/groups.
+ void SetUseShortRepeatedPrimitives(bool use_short_repeated_primitives) {
+ use_short_repeated_primitives_ = use_short_repeated_primitives;
+ }
+
+ // Set true to output UTF-8 instead of ASCII. The only difference
+ // is that bytes >= 0x80 in string fields will not be escaped,
+ // because they are assumed to be part of UTF-8 multi-byte
+ // sequences. This will change the default FieldValuePrinter.
+ void SetUseUtf8StringEscaping(bool as_utf8);
+
+ // Set the default FieldValuePrinter that is used for all fields that
+ // don't have a field-specific printer registered.
+ // Takes ownership of the printer.
+ void SetDefaultFieldValuePrinter(const FieldValuePrinter* printer);
+
+ // Sets whether we want to hide unknown fields or not.
+ // Usually unknown fields are printed in a generic way that includes the
+ // tag number of the field instead of field name. However, sometimes it
+ // is useful to be able to print the message without unknown fields (e.g.
+ // for the python protobuf version to maintain consistency between its pure
+ // python and c++ implementations).
+ void SetHideUnknownFields(bool hide) {
+ hide_unknown_fields_ = hide;
+ }
+
+ // If print_message_fields_in_index_order is true, print fields of a proto
+ // message using the order defined in source code instead of the field
+ // number. By default, use the field number order.
+ void SetPrintMessageFieldsInIndexOrder(
+ bool print_message_fields_in_index_order) {
+ print_message_fields_in_index_order_ =
+ print_message_fields_in_index_order;
+ }
+
+ // Register a custom field-specific FieldValuePrinter for fields
+ // with a particular FieldDescriptor.
+ // Returns "true" if the registration succeeded, or "false", if there is
+ // already a printer for that FieldDescriptor.
+ // Takes ownership of the printer on successful registration.
+ bool RegisterFieldValuePrinter(const FieldDescriptor* field,
+ const FieldValuePrinter* printer);
+
+ private:
+ // Forward declaration of an internal class used to print the text
+ // output to the OutputStream (see text_format.cc for implementation).
+ class TextGenerator;
+
+ // Internal Print method, used for writing to the OutputStream via
+ // the TextGenerator class.
+ void Print(const Message& message,
+ TextGenerator& generator) const;
+
+ // Print a single field.
+ void PrintField(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ TextGenerator& generator) const;
+
+ // Print a repeated primitive field in short form.
+ void PrintShortRepeatedField(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ TextGenerator& generator) const;
+
+ // Print the name of a field -- i.e. everything that comes before the
+ // ':' for a single name/value pair.
+ void PrintFieldName(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ TextGenerator& generator) const;
+
+ // Outputs a textual representation of the value of the field supplied on
+ // the message supplied or the default value if not set.
+ void PrintFieldValue(const Message& message,
+ const Reflection* reflection,
+ const FieldDescriptor* field,
+ int index,
+ TextGenerator& generator) const;
+
+ // Print the fields in an UnknownFieldSet. They are printed by tag number
+ // only. Embedded messages are heuristically identified by attempting to
+ // parse them.
+ void PrintUnknownFields(const UnknownFieldSet& unknown_fields,
+ TextGenerator& generator) const;
+
+ int initial_indent_level_;
+
+ bool single_line_mode_;
+
+ bool use_field_number_;
+
+ bool use_short_repeated_primitives_;
+
+ bool hide_unknown_fields_;
+
+ bool print_message_fields_in_index_order_;
+
+ scoped_ptr<const FieldValuePrinter> default_field_value_printer_;
+ typedef map<const FieldDescriptor*,
+ const FieldValuePrinter*> CustomPrinterMap;
+ CustomPrinterMap custom_printers_;
+ };
+
+ // Parses a text-format protocol message from the given input stream to
+ // the given message object. This function parses the format written
+ // by Print().
+ static bool Parse(io::ZeroCopyInputStream* input, Message* output);
+ // Like Parse(), but reads directly from a string.
+ static bool ParseFromString(const string& input, Message* output);
+
+ // Like Parse(), but the data is merged into the given message, as if
+ // using Message::MergeFrom().
+ static bool Merge(io::ZeroCopyInputStream* input, Message* output);
+ // Like Merge(), but reads directly from a string.
+ static bool MergeFromString(const string& input, Message* output);
+
+ // Parse the given text as a single field value and store it into the
+ // given field of the given message. If the field is a repeated field,
+ // the new value will be added to the end
+ static bool ParseFieldValueFromString(const string& input,
+ const FieldDescriptor* field,
+ Message* message);
+
+ // Interface that TextFormat::Parser can use to find extensions.
+ // This class may be extended in the future to find more information
+ // like fields, etc.
+ class LIBPROTOBUF_EXPORT Finder {
+ public:
+ virtual ~Finder();
+
+ // Try to find an extension of *message by fully-qualified field
+ // name. Returns NULL if no extension is known for this name or number.
+ virtual const FieldDescriptor* FindExtension(
+ Message* message,
+ const string& name) const = 0;
+ };
+
+ // A location in the parsed text.
+ struct ParseLocation {
+ int line;
+ int column;
+
+ ParseLocation() : line(-1), column(-1) {}
+ ParseLocation(int line_param, int column_param)
+ : line(line_param), column(column_param) {}
+ };
+
+ // Data structure which is populated with the locations of each field
+ // value parsed from the text.
+ class LIBPROTOBUF_EXPORT ParseInfoTree {
+ public:
+ ParseInfoTree();
+ ~ParseInfoTree();
+
+ // Returns the parse location for index-th value of the field in the parsed
+ // text. If none exists, returns a location with line = -1. Index should be
+ // -1 for not-repeated fields.
+ ParseLocation GetLocation(const FieldDescriptor* field, int index) const;
+
+ // Returns the parse info tree for the given field, which must be a message
+ // type. The nested information tree is owned by the root tree and will be
+ // deleted when it is deleted.
+ ParseInfoTree* GetTreeForNested(const FieldDescriptor* field,
+ int index) const;
+
+ private:
+ // Allow the text format parser to record information into the tree.
+ friend class TextFormat;
+
+ // Records the starting location of a single value for a field.
+ void RecordLocation(const FieldDescriptor* field, ParseLocation location);
+
+ // Create and records a nested tree for a nested message field.
+ ParseInfoTree* CreateNested(const FieldDescriptor* field);
+
+ // Defines the map from the index-th field descriptor to its parse location.
+ typedef map<const FieldDescriptor*, vector<ParseLocation> > LocationMap;
+
+ // Defines the map from the index-th field descriptor to the nested parse
+ // info tree.
+ typedef map<const FieldDescriptor*, vector<ParseInfoTree*> > NestedMap;
+
+ LocationMap locations_;
+ NestedMap nested_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(ParseInfoTree);
+ };
+
+ // For more control over parsing, use this class.
+ class LIBPROTOBUF_EXPORT Parser {
+ public:
+ Parser();
+ ~Parser();
+
+ // Like TextFormat::Parse().
+ bool Parse(io::ZeroCopyInputStream* input, Message* output);
+ // Like TextFormat::ParseFromString().
+ bool ParseFromString(const string& input, Message* output);
+ // Like TextFormat::Merge().
+ bool Merge(io::ZeroCopyInputStream* input, Message* output);
+ // Like TextFormat::MergeFromString().
+ bool MergeFromString(const string& input, Message* output);
+
+ // Set where to report parse errors. If NULL (the default), errors will
+ // be printed to stderr.
+ void RecordErrorsTo(io::ErrorCollector* error_collector) {
+ error_collector_ = error_collector;
+ }
+
+ // Set how parser finds extensions. If NULL (the default), the
+ // parser will use the standard Reflection object associated with
+ // the message being parsed.
+ void SetFinder(Finder* finder) {
+ finder_ = finder;
+ }
+
+ // Sets where location information about the parse will be written. If NULL
+ // (the default), then no location will be written.
+ void WriteLocationsTo(ParseInfoTree* tree) {
+ parse_info_tree_ = tree;
+ }
+
+ // Normally parsing fails if, after parsing, output->IsInitialized()
+ // returns false. Call AllowPartialMessage(true) to skip this check.
+ void AllowPartialMessage(bool allow) {
+ allow_partial_ = allow;
+ }
+
+ // Allow field names to be matched case-insensitively.
+ // This is not advisable if there are fields that only differ in case, or
+ // if you want to enforce writing in the canonical form.
+ // This is 'false' by default.
+ void AllowCaseInsensitiveField(bool allow) {
+ allow_case_insensitive_field_ = allow;
+ }
+
+ // Like TextFormat::ParseFieldValueFromString
+ bool ParseFieldValueFromString(const string& input,
+ const FieldDescriptor* field,
+ Message* output);
+
+
+ void AllowFieldNumber(bool allow) {
+ allow_field_number_ = allow;
+ }
+
+ private:
+ // Forward declaration of an internal class used to parse text
+ // representations (see text_format.cc for implementation).
+ class ParserImpl;
+
+ // Like TextFormat::Merge(). The provided implementation is used
+ // to do the parsing.
+ bool MergeUsingImpl(io::ZeroCopyInputStream* input,
+ Message* output,
+ ParserImpl* parser_impl);
+
+ io::ErrorCollector* error_collector_;
+ Finder* finder_;
+ ParseInfoTree* parse_info_tree_;
+ bool allow_partial_;
+ bool allow_case_insensitive_field_;
+ bool allow_unknown_field_;
+ bool allow_unknown_enum_;
+ bool allow_field_number_;
+ bool allow_relaxed_whitespace_;
+ bool allow_singular_overwrites_;
+ };
+
+
+ private:
+ // Hack: ParseInfoTree declares TextFormat as a friend which should extend
+ // the friendship to TextFormat::Parser::ParserImpl, but unfortunately some
+ // old compilers (e.g. GCC 3.4.6) don't implement this correctly. We provide
+ // helpers for ParserImpl to call methods of ParseInfoTree.
+ static inline void RecordLocation(ParseInfoTree* info_tree,
+ const FieldDescriptor* field,
+ ParseLocation location);
+ static inline ParseInfoTree* CreateNested(ParseInfoTree* info_tree,
+ const FieldDescriptor* field);
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(TextFormat);
+};
+
+inline void TextFormat::RecordLocation(ParseInfoTree* info_tree,
+ const FieldDescriptor* field,
+ ParseLocation location) {
+ info_tree->RecordLocation(field, location);
+}
+
+
+inline TextFormat::ParseInfoTree* TextFormat::CreateNested(
+ ParseInfoTree* info_tree, const FieldDescriptor* field) {
+ return info_tree->CreateNested(field);
+}
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_TEXT_FORMAT_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/unknown_field_set.cc b/toolkit/components/protobuf/src/google/protobuf/unknown_field_set.cc
new file mode 100644
index 0000000000..020a323b34
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/unknown_field_set.cc
@@ -0,0 +1,265 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/unknown_field_set.h>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <google/protobuf/wire_format.h>
+#include <google/protobuf/stubs/stl_util.h>
+
+namespace google {
+namespace protobuf {
+
+UnknownFieldSet::UnknownFieldSet()
+ : fields_(NULL) {}
+
+UnknownFieldSet::~UnknownFieldSet() {
+ Clear();
+ delete fields_;
+}
+
+void UnknownFieldSet::ClearFallback() {
+ GOOGLE_DCHECK(fields_ != NULL);
+ for (int i = 0; i < fields_->size(); i++) {
+ (*fields_)[i].Delete();
+ }
+ fields_->clear();
+}
+
+void UnknownFieldSet::ClearAndFreeMemory() {
+ if (fields_ != NULL) {
+ Clear();
+ delete fields_;
+ fields_ = NULL;
+ }
+}
+
+void UnknownFieldSet::MergeFrom(const UnknownFieldSet& other) {
+ for (int i = 0; i < other.field_count(); i++) {
+ AddField(other.field(i));
+ }
+}
+
+int UnknownFieldSet::SpaceUsedExcludingSelf() const {
+ if (fields_ == NULL) return 0;
+
+ int total_size = sizeof(*fields_) + sizeof(UnknownField) * fields_->size();
+ for (int i = 0; i < fields_->size(); i++) {
+ const UnknownField& field = (*fields_)[i];
+ switch (field.type()) {
+ case UnknownField::TYPE_LENGTH_DELIMITED:
+ total_size += sizeof(*field.length_delimited_.string_value_) +
+ internal::StringSpaceUsedExcludingSelf(
+ *field.length_delimited_.string_value_);
+ break;
+ case UnknownField::TYPE_GROUP:
+ total_size += field.group_->SpaceUsed();
+ break;
+ default:
+ break;
+ }
+ }
+ return total_size;
+}
+
+int UnknownFieldSet::SpaceUsed() const {
+ return sizeof(*this) + SpaceUsedExcludingSelf();
+}
+
+void UnknownFieldSet::AddVarint(int number, uint64 value) {
+ if (fields_ == NULL) fields_ = new vector<UnknownField>;
+ UnknownField field;
+ field.number_ = number;
+ field.SetType(UnknownField::TYPE_VARINT);
+ field.varint_ = value;
+ fields_->push_back(field);
+}
+
+void UnknownFieldSet::AddFixed32(int number, uint32 value) {
+ if (fields_ == NULL) fields_ = new vector<UnknownField>;
+ UnknownField field;
+ field.number_ = number;
+ field.SetType(UnknownField::TYPE_FIXED32);
+ field.fixed32_ = value;
+ fields_->push_back(field);
+}
+
+void UnknownFieldSet::AddFixed64(int number, uint64 value) {
+ if (fields_ == NULL) fields_ = new vector<UnknownField>;
+ UnknownField field;
+ field.number_ = number;
+ field.SetType(UnknownField::TYPE_FIXED64);
+ field.fixed64_ = value;
+ fields_->push_back(field);
+}
+
+string* UnknownFieldSet::AddLengthDelimited(int number) {
+ if (fields_ == NULL) fields_ = new vector<UnknownField>;
+ UnknownField field;
+ field.number_ = number;
+ field.SetType(UnknownField::TYPE_LENGTH_DELIMITED);
+ field.length_delimited_.string_value_ = new string;
+ fields_->push_back(field);
+ return field.length_delimited_.string_value_;
+}
+
+
+UnknownFieldSet* UnknownFieldSet::AddGroup(int number) {
+ if (fields_ == NULL) fields_ = new vector<UnknownField>;
+ UnknownField field;
+ field.number_ = number;
+ field.SetType(UnknownField::TYPE_GROUP);
+ field.group_ = new UnknownFieldSet;
+ fields_->push_back(field);
+ return field.group_;
+}
+
+void UnknownFieldSet::AddField(const UnknownField& field) {
+ if (fields_ == NULL) fields_ = new vector<UnknownField>;
+ fields_->push_back(field);
+ fields_->back().DeepCopy();
+}
+
+void UnknownFieldSet::DeleteSubrange(int start, int num) {
+ GOOGLE_DCHECK(fields_ != NULL);
+ // Delete the specified fields.
+ for (int i = 0; i < num; ++i) {
+ (*fields_)[i + start].Delete();
+ }
+ // Slide down the remaining fields.
+ for (int i = start + num; i < fields_->size(); ++i) {
+ (*fields_)[i - num] = (*fields_)[i];
+ }
+ // Pop off the # of deleted fields.
+ for (int i = 0; i < num; ++i) {
+ fields_->pop_back();
+ }
+}
+
+void UnknownFieldSet::DeleteByNumber(int number) {
+ if (fields_ == NULL) return;
+ int left = 0; // The number of fields left after deletion.
+ for (int i = 0; i < fields_->size(); ++i) {
+ UnknownField* field = &(*fields_)[i];
+ if (field->number() == number) {
+ field->Delete();
+ } else {
+ if (i != left) {
+ (*fields_)[left] = (*fields_)[i];
+ }
+ ++left;
+ }
+ }
+ fields_->resize(left);
+}
+
+bool UnknownFieldSet::MergeFromCodedStream(io::CodedInputStream* input) {
+ UnknownFieldSet other;
+ if (internal::WireFormat::SkipMessage(input, &other) &&
+ input->ConsumedEntireMessage()) {
+ MergeFrom(other);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool UnknownFieldSet::ParseFromCodedStream(io::CodedInputStream* input) {
+ Clear();
+ return MergeFromCodedStream(input);
+}
+
+bool UnknownFieldSet::ParseFromZeroCopyStream(io::ZeroCopyInputStream* input) {
+ io::CodedInputStream coded_input(input);
+ return (ParseFromCodedStream(&coded_input) &&
+ coded_input.ConsumedEntireMessage());
+}
+
+bool UnknownFieldSet::ParseFromArray(const void* data, int size) {
+ io::ArrayInputStream input(data, size);
+ return ParseFromZeroCopyStream(&input);
+}
+
+void UnknownField::Delete() {
+ switch (type()) {
+ case UnknownField::TYPE_LENGTH_DELIMITED:
+ delete length_delimited_.string_value_;
+ break;
+ case UnknownField::TYPE_GROUP:
+ delete group_;
+ break;
+ default:
+ break;
+ }
+}
+
+void UnknownField::DeepCopy() {
+ switch (type()) {
+ case UnknownField::TYPE_LENGTH_DELIMITED:
+ length_delimited_.string_value_ = new string(
+ *length_delimited_.string_value_);
+ break;
+ case UnknownField::TYPE_GROUP: {
+ UnknownFieldSet* group = new UnknownFieldSet;
+ group->MergeFrom(*group_);
+ group_ = group;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+
+void UnknownField::SerializeLengthDelimitedNoTag(
+ io::CodedOutputStream* output) const {
+ GOOGLE_DCHECK_EQ(TYPE_LENGTH_DELIMITED, type());
+ const string& data = *length_delimited_.string_value_;
+ output->WriteVarint32(data.size());
+ output->WriteRawMaybeAliased(data.data(), data.size());
+}
+
+uint8* UnknownField::SerializeLengthDelimitedNoTagToArray(uint8* target) const {
+ GOOGLE_DCHECK_EQ(TYPE_LENGTH_DELIMITED, type());
+ const string& data = *length_delimited_.string_value_;
+ target = io::CodedOutputStream::WriteVarint32ToArray(data.size(), target);
+ target = io::CodedOutputStream::WriteStringToArray(data, target);
+ return target;
+}
+
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/unknown_field_set.h b/toolkit/components/protobuf/src/google/protobuf/unknown_field_set.h
new file mode 100644
index 0000000000..ba202eb686
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/unknown_field_set.h
@@ -0,0 +1,318 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// Contains classes used to keep track of unrecognized fields seen while
+// parsing a protocol message.
+
+#ifndef GOOGLE_PROTOBUF_UNKNOWN_FIELD_SET_H__
+#define GOOGLE_PROTOBUF_UNKNOWN_FIELD_SET_H__
+
+#include <assert.h>
+#include <string>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+
+namespace google {
+namespace protobuf {
+ namespace io {
+ class CodedInputStream; // coded_stream.h
+ class CodedOutputStream; // coded_stream.h
+ class ZeroCopyInputStream; // zero_copy_stream.h
+ }
+ namespace internal {
+ class WireFormat; // wire_format.h
+ class MessageSetFieldSkipperUsingCord;
+ // extension_set_heavy.cc
+ }
+
+class Message; // message.h
+class UnknownField; // below
+
+// An UnknownFieldSet contains fields that were encountered while parsing a
+// message but were not defined by its type. Keeping track of these can be
+// useful, especially in that they may be written if the message is serialized
+// again without being cleared in between. This means that software which
+// simply receives messages and forwards them to other servers does not need
+// to be updated every time a new field is added to the message definition.
+//
+// To get the UnknownFieldSet attached to any message, call
+// Reflection::GetUnknownFields().
+//
+// This class is necessarily tied to the protocol buffer wire format, unlike
+// the Reflection interface which is independent of any serialization scheme.
+class LIBPROTOBUF_EXPORT UnknownFieldSet {
+ public:
+ UnknownFieldSet();
+ ~UnknownFieldSet();
+
+ // Remove all fields.
+ inline void Clear();
+
+ // Remove all fields and deallocate internal data objects
+ void ClearAndFreeMemory();
+
+ // Is this set empty?
+ inline bool empty() const;
+
+ // Merge the contents of some other UnknownFieldSet with this one.
+ void MergeFrom(const UnknownFieldSet& other);
+
+ // Swaps the contents of some other UnknownFieldSet with this one.
+ inline void Swap(UnknownFieldSet* x);
+
+ // Computes (an estimate of) the total number of bytes currently used for
+ // storing the unknown fields in memory. Does NOT include
+ // sizeof(*this) in the calculation.
+ int SpaceUsedExcludingSelf() const;
+
+ // Version of SpaceUsed() including sizeof(*this).
+ int SpaceUsed() const;
+
+ // Returns the number of fields present in the UnknownFieldSet.
+ inline int field_count() const;
+ // Get a field in the set, where 0 <= index < field_count(). The fields
+ // appear in the order in which they were added.
+ inline const UnknownField& field(int index) const;
+ // Get a mutable pointer to a field in the set, where
+ // 0 <= index < field_count(). The fields appear in the order in which
+ // they were added.
+ inline UnknownField* mutable_field(int index);
+
+ // Adding fields ---------------------------------------------------
+
+ void AddVarint(int number, uint64 value);
+ void AddFixed32(int number, uint32 value);
+ void AddFixed64(int number, uint64 value);
+ void AddLengthDelimited(int number, const string& value);
+ string* AddLengthDelimited(int number);
+ UnknownFieldSet* AddGroup(int number);
+
+ // Adds an unknown field from another set.
+ void AddField(const UnknownField& field);
+
+ // Delete fields with indices in the range [start .. start+num-1].
+ // Caution: implementation moves all fields with indices [start+num .. ].
+ void DeleteSubrange(int start, int num);
+
+ // Delete all fields with a specific field number. The order of left fields
+ // is preserved.
+ // Caution: implementation moves all fields after the first deleted field.
+ void DeleteByNumber(int number);
+
+ // Parsing helpers -------------------------------------------------
+ // These work exactly like the similarly-named methods of Message.
+
+ bool MergeFromCodedStream(io::CodedInputStream* input);
+ bool ParseFromCodedStream(io::CodedInputStream* input);
+ bool ParseFromZeroCopyStream(io::ZeroCopyInputStream* input);
+ bool ParseFromArray(const void* data, int size);
+ inline bool ParseFromString(const string& data) {
+ return ParseFromArray(data.data(), static_cast<int>(data.size()));
+ }
+
+ private:
+
+ void ClearFallback();
+
+ vector<UnknownField>* fields_;
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(UnknownFieldSet);
+};
+
+// Represents one field in an UnknownFieldSet.
+class LIBPROTOBUF_EXPORT UnknownField {
+ public:
+ enum Type {
+ TYPE_VARINT,
+ TYPE_FIXED32,
+ TYPE_FIXED64,
+ TYPE_LENGTH_DELIMITED,
+ TYPE_GROUP
+ };
+
+ // The field's tag number, as seen on the wire.
+ inline int number() const;
+
+ // The field type.
+ inline Type type() const;
+
+ // Accessors -------------------------------------------------------
+ // Each method works only for UnknownFields of the corresponding type.
+
+ inline uint64 varint() const;
+ inline uint32 fixed32() const;
+ inline uint64 fixed64() const;
+ inline const string& length_delimited() const;
+ inline const UnknownFieldSet& group() const;
+
+ inline void set_varint(uint64 value);
+ inline void set_fixed32(uint32 value);
+ inline void set_fixed64(uint64 value);
+ inline void set_length_delimited(const string& value);
+ inline string* mutable_length_delimited();
+ inline UnknownFieldSet* mutable_group();
+
+ // Serialization API.
+ // These methods can take advantage of the underlying implementation and may
+ // archieve a better performance than using getters to retrieve the data and
+ // do the serialization yourself.
+ void SerializeLengthDelimitedNoTag(io::CodedOutputStream* output) const;
+ uint8* SerializeLengthDelimitedNoTagToArray(uint8* target) const;
+
+ inline int GetLengthDelimitedSize() const;
+
+ private:
+ friend class UnknownFieldSet;
+
+ // If this UnknownField contains a pointer, delete it.
+ void Delete();
+
+ // Make a deep copy of any pointers in this UnknownField.
+ void DeepCopy();
+
+ // Set the wire type of this UnknownField. Should only be used when this
+ // UnknownField is being created.
+ inline void SetType(Type type);
+
+ uint32 number_;
+ uint32 type_;
+ union {
+ uint64 varint_;
+ uint32 fixed32_;
+ uint64 fixed64_;
+ mutable union {
+ string* string_value_;
+ } length_delimited_;
+ UnknownFieldSet* group_;
+ };
+};
+
+// ===================================================================
+// inline implementations
+
+inline void UnknownFieldSet::Clear() {
+ if (fields_ != NULL) {
+ ClearFallback();
+ }
+}
+
+inline bool UnknownFieldSet::empty() const {
+ return fields_ == NULL || fields_->empty();
+}
+
+inline void UnknownFieldSet::Swap(UnknownFieldSet* x) {
+ std::swap(fields_, x->fields_);
+}
+
+inline int UnknownFieldSet::field_count() const {
+ return (fields_ == NULL) ? 0 : static_cast<int>(fields_->size());
+}
+inline const UnknownField& UnknownFieldSet::field(int index) const {
+ return (*fields_)[index];
+}
+inline UnknownField* UnknownFieldSet::mutable_field(int index) {
+ return &(*fields_)[index];
+}
+
+inline void UnknownFieldSet::AddLengthDelimited(
+ int number, const string& value) {
+ AddLengthDelimited(number)->assign(value);
+}
+
+
+inline int UnknownField::number() const { return number_; }
+inline UnknownField::Type UnknownField::type() const {
+ return static_cast<Type>(type_);
+}
+
+inline uint64 UnknownField::varint() const {
+ assert(type() == TYPE_VARINT);
+ return varint_;
+}
+inline uint32 UnknownField::fixed32() const {
+ assert(type() == TYPE_FIXED32);
+ return fixed32_;
+}
+inline uint64 UnknownField::fixed64() const {
+ assert(type() == TYPE_FIXED64);
+ return fixed64_;
+}
+inline const string& UnknownField::length_delimited() const {
+ assert(type() == TYPE_LENGTH_DELIMITED);
+ return *length_delimited_.string_value_;
+}
+inline const UnknownFieldSet& UnknownField::group() const {
+ assert(type() == TYPE_GROUP);
+ return *group_;
+}
+
+inline void UnknownField::set_varint(uint64 value) {
+ assert(type() == TYPE_VARINT);
+ varint_ = value;
+}
+inline void UnknownField::set_fixed32(uint32 value) {
+ assert(type() == TYPE_FIXED32);
+ fixed32_ = value;
+}
+inline void UnknownField::set_fixed64(uint64 value) {
+ assert(type() == TYPE_FIXED64);
+ fixed64_ = value;
+}
+inline void UnknownField::set_length_delimited(const string& value) {
+ assert(type() == TYPE_LENGTH_DELIMITED);
+ length_delimited_.string_value_->assign(value);
+}
+inline string* UnknownField::mutable_length_delimited() {
+ assert(type() == TYPE_LENGTH_DELIMITED);
+ return length_delimited_.string_value_;
+}
+inline UnknownFieldSet* UnknownField::mutable_group() {
+ assert(type() == TYPE_GROUP);
+ return group_;
+}
+
+inline int UnknownField::GetLengthDelimitedSize() const {
+ GOOGLE_DCHECK_EQ(TYPE_LENGTH_DELIMITED, type());
+ return static_cast<int>(length_delimited_.string_value_->size());
+}
+
+inline void UnknownField::SetType(Type type) {
+ type_ = type;
+}
+
+
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_UNKNOWN_FIELD_SET_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/wire_format.cc b/toolkit/components/protobuf/src/google/protobuf/wire_format.cc
new file mode 100644
index 0000000000..fc6210c284
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/wire_format.cc
@@ -0,0 +1,1106 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <stack>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/wire_format.h>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/stringprintf.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <google/protobuf/unknown_field_set.h>
+
+
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+namespace {
+
+// This function turns out to be convenient when using some macros later.
+inline int GetEnumNumber(const EnumValueDescriptor* descriptor) {
+ return descriptor->number();
+}
+
+} // anonymous namespace
+
+// ===================================================================
+
+bool UnknownFieldSetFieldSkipper::SkipField(
+ io::CodedInputStream* input, uint32 tag) {
+ return WireFormat::SkipField(input, tag, unknown_fields_);
+}
+
+bool UnknownFieldSetFieldSkipper::SkipMessage(io::CodedInputStream* input) {
+ return WireFormat::SkipMessage(input, unknown_fields_);
+}
+
+void UnknownFieldSetFieldSkipper::SkipUnknownEnum(
+ int field_number, int value) {
+ unknown_fields_->AddVarint(field_number, value);
+}
+
+bool WireFormat::SkipField(io::CodedInputStream* input, uint32 tag,
+ UnknownFieldSet* unknown_fields) {
+ int number = WireFormatLite::GetTagFieldNumber(tag);
+
+ switch (WireFormatLite::GetTagWireType(tag)) {
+ case WireFormatLite::WIRETYPE_VARINT: {
+ uint64 value;
+ if (!input->ReadVarint64(&value)) return false;
+ if (unknown_fields != NULL) unknown_fields->AddVarint(number, value);
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_FIXED64: {
+ uint64 value;
+ if (!input->ReadLittleEndian64(&value)) return false;
+ if (unknown_fields != NULL) unknown_fields->AddFixed64(number, value);
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED: {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (unknown_fields == NULL) {
+ if (!input->Skip(length)) return false;
+ } else {
+ if (!input->ReadString(unknown_fields->AddLengthDelimited(number),
+ length)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_START_GROUP: {
+ if (!input->IncrementRecursionDepth()) return false;
+ if (!SkipMessage(input, (unknown_fields == NULL) ?
+ NULL : unknown_fields->AddGroup(number))) {
+ return false;
+ }
+ input->DecrementRecursionDepth();
+ // Check that the ending tag matched the starting tag.
+ if (!input->LastTagWas(WireFormatLite::MakeTag(
+ WireFormatLite::GetTagFieldNumber(tag),
+ WireFormatLite::WIRETYPE_END_GROUP))) {
+ return false;
+ }
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_END_GROUP: {
+ return false;
+ }
+ case WireFormatLite::WIRETYPE_FIXED32: {
+ uint32 value;
+ if (!input->ReadLittleEndian32(&value)) return false;
+ if (unknown_fields != NULL) unknown_fields->AddFixed32(number, value);
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+}
+
+bool WireFormat::SkipMessage(io::CodedInputStream* input,
+ UnknownFieldSet* unknown_fields) {
+ while(true) {
+ uint32 tag = input->ReadTag();
+ if (tag == 0) {
+ // End of input. This is a valid place to end, so return true.
+ return true;
+ }
+
+ WireFormatLite::WireType wire_type = WireFormatLite::GetTagWireType(tag);
+
+ if (wire_type == WireFormatLite::WIRETYPE_END_GROUP) {
+ // Must be the end of the message.
+ return true;
+ }
+
+ if (!SkipField(input, tag, unknown_fields)) return false;
+ }
+}
+
+void WireFormat::SerializeUnknownFields(const UnknownFieldSet& unknown_fields,
+ io::CodedOutputStream* output) {
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+ switch (field.type()) {
+ case UnknownField::TYPE_VARINT:
+ output->WriteVarint32(WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_VARINT));
+ output->WriteVarint64(field.varint());
+ break;
+ case UnknownField::TYPE_FIXED32:
+ output->WriteVarint32(WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_FIXED32));
+ output->WriteLittleEndian32(field.fixed32());
+ break;
+ case UnknownField::TYPE_FIXED64:
+ output->WriteVarint32(WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_FIXED64));
+ output->WriteLittleEndian64(field.fixed64());
+ break;
+ case UnknownField::TYPE_LENGTH_DELIMITED:
+ output->WriteVarint32(WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ output->WriteVarint32(field.length_delimited().size());
+ output->WriteRawMaybeAliased(field.length_delimited().data(),
+ field.length_delimited().size());
+ break;
+ case UnknownField::TYPE_GROUP:
+ output->WriteVarint32(WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_START_GROUP));
+ SerializeUnknownFields(field.group(), output);
+ output->WriteVarint32(WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_END_GROUP));
+ break;
+ }
+ }
+}
+
+uint8* WireFormat::SerializeUnknownFieldsToArray(
+ const UnknownFieldSet& unknown_fields,
+ uint8* target) {
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+
+ switch (field.type()) {
+ case UnknownField::TYPE_VARINT:
+ target = WireFormatLite::WriteInt64ToArray(
+ field.number(), field.varint(), target);
+ break;
+ case UnknownField::TYPE_FIXED32:
+ target = WireFormatLite::WriteFixed32ToArray(
+ field.number(), field.fixed32(), target);
+ break;
+ case UnknownField::TYPE_FIXED64:
+ target = WireFormatLite::WriteFixed64ToArray(
+ field.number(), field.fixed64(), target);
+ break;
+ case UnknownField::TYPE_LENGTH_DELIMITED:
+ target = WireFormatLite::WriteBytesToArray(
+ field.number(), field.length_delimited(), target);
+ break;
+ case UnknownField::TYPE_GROUP:
+ target = WireFormatLite::WriteTagToArray(
+ field.number(), WireFormatLite::WIRETYPE_START_GROUP, target);
+ target = SerializeUnknownFieldsToArray(field.group(), target);
+ target = WireFormatLite::WriteTagToArray(
+ field.number(), WireFormatLite::WIRETYPE_END_GROUP, target);
+ break;
+ }
+ }
+ return target;
+}
+
+void WireFormat::SerializeUnknownMessageSetItems(
+ const UnknownFieldSet& unknown_fields,
+ io::CodedOutputStream* output) {
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+ // The only unknown fields that are allowed to exist in a MessageSet are
+ // messages, which are length-delimited.
+ if (field.type() == UnknownField::TYPE_LENGTH_DELIMITED) {
+ // Start group.
+ output->WriteVarint32(WireFormatLite::kMessageSetItemStartTag);
+
+ // Write type ID.
+ output->WriteVarint32(WireFormatLite::kMessageSetTypeIdTag);
+ output->WriteVarint32(field.number());
+
+ // Write message.
+ output->WriteVarint32(WireFormatLite::kMessageSetMessageTag);
+ field.SerializeLengthDelimitedNoTag(output);
+
+ // End group.
+ output->WriteVarint32(WireFormatLite::kMessageSetItemEndTag);
+ }
+ }
+}
+
+uint8* WireFormat::SerializeUnknownMessageSetItemsToArray(
+ const UnknownFieldSet& unknown_fields,
+ uint8* target) {
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+
+ // The only unknown fields that are allowed to exist in a MessageSet are
+ // messages, which are length-delimited.
+ if (field.type() == UnknownField::TYPE_LENGTH_DELIMITED) {
+ // Start group.
+ target = io::CodedOutputStream::WriteTagToArray(
+ WireFormatLite::kMessageSetItemStartTag, target);
+
+ // Write type ID.
+ target = io::CodedOutputStream::WriteTagToArray(
+ WireFormatLite::kMessageSetTypeIdTag, target);
+ target = io::CodedOutputStream::WriteVarint32ToArray(
+ field.number(), target);
+
+ // Write message.
+ target = io::CodedOutputStream::WriteTagToArray(
+ WireFormatLite::kMessageSetMessageTag, target);
+ target = field.SerializeLengthDelimitedNoTagToArray(target);
+
+ // End group.
+ target = io::CodedOutputStream::WriteTagToArray(
+ WireFormatLite::kMessageSetItemEndTag, target);
+ }
+ }
+
+ return target;
+}
+
+int WireFormat::ComputeUnknownFieldsSize(
+ const UnknownFieldSet& unknown_fields) {
+ int size = 0;
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+
+ switch (field.type()) {
+ case UnknownField::TYPE_VARINT:
+ size += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_VARINT));
+ size += io::CodedOutputStream::VarintSize64(field.varint());
+ break;
+ case UnknownField::TYPE_FIXED32:
+ size += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_FIXED32));
+ size += sizeof(int32);
+ break;
+ case UnknownField::TYPE_FIXED64:
+ size += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_FIXED64));
+ size += sizeof(int64);
+ break;
+ case UnknownField::TYPE_LENGTH_DELIMITED:
+ size += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED));
+ size += io::CodedOutputStream::VarintSize32(
+ field.length_delimited().size());
+ size += field.length_delimited().size();
+ break;
+ case UnknownField::TYPE_GROUP:
+ size += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_START_GROUP));
+ size += ComputeUnknownFieldsSize(field.group());
+ size += io::CodedOutputStream::VarintSize32(
+ WireFormatLite::MakeTag(field.number(),
+ WireFormatLite::WIRETYPE_END_GROUP));
+ break;
+ }
+ }
+
+ return size;
+}
+
+int WireFormat::ComputeUnknownMessageSetItemsSize(
+ const UnknownFieldSet& unknown_fields) {
+ int size = 0;
+ for (int i = 0; i < unknown_fields.field_count(); i++) {
+ const UnknownField& field = unknown_fields.field(i);
+
+ // The only unknown fields that are allowed to exist in a MessageSet are
+ // messages, which are length-delimited.
+ if (field.type() == UnknownField::TYPE_LENGTH_DELIMITED) {
+ size += WireFormatLite::kMessageSetItemTagsSize;
+ size += io::CodedOutputStream::VarintSize32(field.number());
+
+ int field_size = field.GetLengthDelimitedSize();
+ size += io::CodedOutputStream::VarintSize32(field_size);
+ size += field_size;
+ }
+ }
+
+ return size;
+}
+
+// ===================================================================
+
+bool WireFormat::ParseAndMergePartial(io::CodedInputStream* input,
+ Message* message) {
+ const Descriptor* descriptor = message->GetDescriptor();
+ const Reflection* message_reflection = message->GetReflection();
+
+ while(true) {
+ uint32 tag = input->ReadTag();
+ if (tag == 0) {
+ // End of input. This is a valid place to end, so return true.
+ return true;
+ }
+
+ if (WireFormatLite::GetTagWireType(tag) ==
+ WireFormatLite::WIRETYPE_END_GROUP) {
+ // Must be the end of the message.
+ return true;
+ }
+
+ const FieldDescriptor* field = NULL;
+
+ if (descriptor != NULL) {
+ int field_number = WireFormatLite::GetTagFieldNumber(tag);
+ field = descriptor->FindFieldByNumber(field_number);
+
+ // If that failed, check if the field is an extension.
+ if (field == NULL && descriptor->IsExtensionNumber(field_number)) {
+ if (input->GetExtensionPool() == NULL) {
+ field = message_reflection->FindKnownExtensionByNumber(field_number);
+ } else {
+ field = input->GetExtensionPool()
+ ->FindExtensionByNumber(descriptor, field_number);
+ }
+ }
+
+ // If that failed, but we're a MessageSet, and this is the tag for a
+ // MessageSet item, then parse that.
+ if (field == NULL &&
+ descriptor->options().message_set_wire_format() &&
+ tag == WireFormatLite::kMessageSetItemStartTag) {
+ if (!ParseAndMergeMessageSetItem(input, message)) {
+ return false;
+ }
+ continue; // Skip ParseAndMergeField(); already taken care of.
+ }
+ }
+
+ if (!ParseAndMergeField(tag, field, message, input)) {
+ return false;
+ }
+ }
+}
+
+bool WireFormat::SkipMessageSetField(io::CodedInputStream* input,
+ uint32 field_number,
+ UnknownFieldSet* unknown_fields) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ return input->ReadString(
+ unknown_fields->AddLengthDelimited(field_number), length);
+}
+
+bool WireFormat::ParseAndMergeMessageSetField(uint32 field_number,
+ const FieldDescriptor* field,
+ Message* message,
+ io::CodedInputStream* input) {
+ const Reflection* message_reflection = message->GetReflection();
+ if (field == NULL) {
+ // We store unknown MessageSet extensions as groups.
+ return SkipMessageSetField(
+ input, field_number, message_reflection->MutableUnknownFields(message));
+ } else if (field->is_repeated() ||
+ field->type() != FieldDescriptor::TYPE_MESSAGE) {
+ // This shouldn't happen as we only allow optional message extensions to
+ // MessageSet.
+ GOOGLE_LOG(ERROR) << "Extensions of MessageSets must be optional messages.";
+ return false;
+ } else {
+ Message* sub_message = message_reflection->MutableMessage(
+ message, field, input->GetExtensionFactory());
+ return WireFormatLite::ReadMessage(input, sub_message);
+ }
+}
+
+bool WireFormat::ParseAndMergeField(
+ uint32 tag,
+ const FieldDescriptor* field, // May be NULL for unknown
+ Message* message,
+ io::CodedInputStream* input) {
+ const Reflection* message_reflection = message->GetReflection();
+
+ enum { UNKNOWN, NORMAL_FORMAT, PACKED_FORMAT } value_format;
+
+ if (field == NULL) {
+ value_format = UNKNOWN;
+ } else if (WireFormatLite::GetTagWireType(tag) ==
+ WireTypeForFieldType(field->type())) {
+ value_format = NORMAL_FORMAT;
+ } else if (field->is_packable() &&
+ WireFormatLite::GetTagWireType(tag) ==
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED) {
+ value_format = PACKED_FORMAT;
+ } else {
+ // We don't recognize this field. Either the field number is unknown
+ // or the wire type doesn't match. Put it in our unknown field set.
+ value_format = UNKNOWN;
+ }
+
+ if (value_format == UNKNOWN) {
+ return SkipField(input, tag,
+ message_reflection->MutableUnknownFields(message));
+ } else if (value_format == PACKED_FORMAT) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ io::CodedInputStream::Limit limit = input->PushLimit(length);
+
+ switch (field->type()) {
+#define HANDLE_PACKED_TYPE(TYPE, CPPTYPE, CPPTYPE_METHOD) \
+ case FieldDescriptor::TYPE_##TYPE: { \
+ while (input->BytesUntilLimit() > 0) { \
+ CPPTYPE value; \
+ if (!WireFormatLite::ReadPrimitive< \
+ CPPTYPE, WireFormatLite::TYPE_##TYPE>(input, &value)) \
+ return false; \
+ message_reflection->Add##CPPTYPE_METHOD(message, field, value); \
+ } \
+ break; \
+ }
+
+ HANDLE_PACKED_TYPE( INT32, int32, Int32)
+ HANDLE_PACKED_TYPE( INT64, int64, Int64)
+ HANDLE_PACKED_TYPE(SINT32, int32, Int32)
+ HANDLE_PACKED_TYPE(SINT64, int64, Int64)
+ HANDLE_PACKED_TYPE(UINT32, uint32, UInt32)
+ HANDLE_PACKED_TYPE(UINT64, uint64, UInt64)
+
+ HANDLE_PACKED_TYPE( FIXED32, uint32, UInt32)
+ HANDLE_PACKED_TYPE( FIXED64, uint64, UInt64)
+ HANDLE_PACKED_TYPE(SFIXED32, int32, Int32)
+ HANDLE_PACKED_TYPE(SFIXED64, int64, Int64)
+
+ HANDLE_PACKED_TYPE(FLOAT , float , Float )
+ HANDLE_PACKED_TYPE(DOUBLE, double, Double)
+
+ HANDLE_PACKED_TYPE(BOOL, bool, Bool)
+#undef HANDLE_PACKED_TYPE
+
+ case FieldDescriptor::TYPE_ENUM: {
+ while (input->BytesUntilLimit() > 0) {
+ int value;
+ if (!WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_ENUM>(
+ input, &value)) return false;
+ const EnumValueDescriptor* enum_value =
+ field->enum_type()->FindValueByNumber(value);
+ if (enum_value != NULL) {
+ message_reflection->AddEnum(message, field, enum_value);
+ }
+ }
+
+ break;
+ }
+
+ case FieldDescriptor::TYPE_STRING:
+ case FieldDescriptor::TYPE_GROUP:
+ case FieldDescriptor::TYPE_MESSAGE:
+ case FieldDescriptor::TYPE_BYTES:
+ // Can't have packed fields of these types: these should be caught by
+ // the protocol compiler.
+ return false;
+ break;
+ }
+
+ input->PopLimit(limit);
+ } else {
+ // Non-packed value (value_format == NORMAL_FORMAT)
+ switch (field->type()) {
+#define HANDLE_TYPE(TYPE, CPPTYPE, CPPTYPE_METHOD) \
+ case FieldDescriptor::TYPE_##TYPE: { \
+ CPPTYPE value; \
+ if (!WireFormatLite::ReadPrimitive< \
+ CPPTYPE, WireFormatLite::TYPE_##TYPE>(input, &value)) \
+ return false; \
+ if (field->is_repeated()) { \
+ message_reflection->Add##CPPTYPE_METHOD(message, field, value); \
+ } else { \
+ message_reflection->Set##CPPTYPE_METHOD(message, field, value); \
+ } \
+ break; \
+ }
+
+ HANDLE_TYPE( INT32, int32, Int32)
+ HANDLE_TYPE( INT64, int64, Int64)
+ HANDLE_TYPE(SINT32, int32, Int32)
+ HANDLE_TYPE(SINT64, int64, Int64)
+ HANDLE_TYPE(UINT32, uint32, UInt32)
+ HANDLE_TYPE(UINT64, uint64, UInt64)
+
+ HANDLE_TYPE( FIXED32, uint32, UInt32)
+ HANDLE_TYPE( FIXED64, uint64, UInt64)
+ HANDLE_TYPE(SFIXED32, int32, Int32)
+ HANDLE_TYPE(SFIXED64, int64, Int64)
+
+ HANDLE_TYPE(FLOAT , float , Float )
+ HANDLE_TYPE(DOUBLE, double, Double)
+
+ HANDLE_TYPE(BOOL, bool, Bool)
+#undef HANDLE_TYPE
+
+ case FieldDescriptor::TYPE_ENUM: {
+ int value;
+ if (!WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_ENUM>(
+ input, &value)) return false;
+ const EnumValueDescriptor* enum_value =
+ field->enum_type()->FindValueByNumber(value);
+ if (enum_value != NULL) {
+ if (field->is_repeated()) {
+ message_reflection->AddEnum(message, field, enum_value);
+ } else {
+ message_reflection->SetEnum(message, field, enum_value);
+ }
+ } else {
+ // The enum value is not one of the known values. Add it to the
+ // UnknownFieldSet.
+ int64 sign_extended_value = static_cast<int64>(value);
+ message_reflection->MutableUnknownFields(message)
+ ->AddVarint(WireFormatLite::GetTagFieldNumber(tag),
+ sign_extended_value);
+ }
+ break;
+ }
+
+ // Handle strings separately so that we can optimize the ctype=CORD case.
+ case FieldDescriptor::TYPE_STRING: {
+ string value;
+ if (!WireFormatLite::ReadString(input, &value)) return false;
+ VerifyUTF8StringNamedField(value.data(), value.length(), PARSE,
+ field->name().c_str());
+ if (field->is_repeated()) {
+ message_reflection->AddString(message, field, value);
+ } else {
+ message_reflection->SetString(message, field, value);
+ }
+ break;
+ }
+
+ case FieldDescriptor::TYPE_BYTES: {
+ string value;
+ if (!WireFormatLite::ReadBytes(input, &value)) return false;
+ if (field->is_repeated()) {
+ message_reflection->AddString(message, field, value);
+ } else {
+ message_reflection->SetString(message, field, value);
+ }
+ break;
+ }
+
+ case FieldDescriptor::TYPE_GROUP: {
+ Message* sub_message;
+ if (field->is_repeated()) {
+ sub_message = message_reflection->AddMessage(
+ message, field, input->GetExtensionFactory());
+ } else {
+ sub_message = message_reflection->MutableMessage(
+ message, field, input->GetExtensionFactory());
+ }
+
+ if (!WireFormatLite::ReadGroup(WireFormatLite::GetTagFieldNumber(tag),
+ input, sub_message))
+ return false;
+ break;
+ }
+
+ case FieldDescriptor::TYPE_MESSAGE: {
+ Message* sub_message;
+ if (field->is_repeated()) {
+ sub_message = message_reflection->AddMessage(
+ message, field, input->GetExtensionFactory());
+ } else {
+ sub_message = message_reflection->MutableMessage(
+ message, field, input->GetExtensionFactory());
+ }
+
+ if (!WireFormatLite::ReadMessage(input, sub_message)) return false;
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool WireFormat::ParseAndMergeMessageSetItem(
+ io::CodedInputStream* input,
+ Message* message) {
+ const Reflection* message_reflection = message->GetReflection();
+
+ // This method parses a group which should contain two fields:
+ // required int32 type_id = 2;
+ // required data message = 3;
+
+ uint32 last_type_id = 0;
+
+ // Once we see a type_id, we'll look up the FieldDescriptor for the
+ // extension.
+ const FieldDescriptor* field = NULL;
+
+ // If we see message data before the type_id, we'll append it to this so
+ // we can parse it later.
+ string message_data;
+
+ while (true) {
+ uint32 tag = input->ReadTag();
+ if (tag == 0) return false;
+
+ switch (tag) {
+ case WireFormatLite::kMessageSetTypeIdTag: {
+ uint32 type_id;
+ if (!input->ReadVarint32(&type_id)) return false;
+ last_type_id = type_id;
+ field = message_reflection->FindKnownExtensionByNumber(type_id);
+
+ if (!message_data.empty()) {
+ // We saw some message data before the type_id. Have to parse it
+ // now.
+ io::ArrayInputStream raw_input(message_data.data(),
+ message_data.size());
+ io::CodedInputStream sub_input(&raw_input);
+ if (!ParseAndMergeMessageSetField(last_type_id, field, message,
+ &sub_input)) {
+ return false;
+ }
+ message_data.clear();
+ }
+
+ break;
+ }
+
+ case WireFormatLite::kMessageSetMessageTag: {
+ if (last_type_id == 0) {
+ // We haven't seen a type_id yet. Append this data to message_data.
+ string temp;
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (!input->ReadString(&temp, length)) return false;
+ io::StringOutputStream output_stream(&message_data);
+ io::CodedOutputStream coded_output(&output_stream);
+ coded_output.WriteVarint32(length);
+ coded_output.WriteString(temp);
+ } else {
+ // Already saw type_id, so we can parse this directly.
+ if (!ParseAndMergeMessageSetField(last_type_id, field, message,
+ input)) {
+ return false;
+ }
+ }
+
+ break;
+ }
+
+ case WireFormatLite::kMessageSetItemEndTag: {
+ return true;
+ }
+
+ default: {
+ if (!SkipField(input, tag, NULL)) return false;
+ }
+ }
+ }
+}
+
+// ===================================================================
+
+void WireFormat::SerializeWithCachedSizes(
+ const Message& message,
+ int size, io::CodedOutputStream* output) {
+ const Descriptor* descriptor = message.GetDescriptor();
+ const Reflection* message_reflection = message.GetReflection();
+ int expected_endpoint = output->ByteCount() + size;
+
+ vector<const FieldDescriptor*> fields;
+ message_reflection->ListFields(message, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ SerializeFieldWithCachedSizes(fields[i], message, output);
+ }
+
+ if (descriptor->options().message_set_wire_format()) {
+ SerializeUnknownMessageSetItems(
+ message_reflection->GetUnknownFields(message), output);
+ } else {
+ SerializeUnknownFields(
+ message_reflection->GetUnknownFields(message), output);
+ }
+
+ GOOGLE_CHECK_EQ(output->ByteCount(), expected_endpoint)
+ << ": Protocol message serialized to a size different from what was "
+ "originally expected. Perhaps it was modified by another thread "
+ "during serialization?";
+}
+
+void WireFormat::SerializeFieldWithCachedSizes(
+ const FieldDescriptor* field,
+ const Message& message,
+ io::CodedOutputStream* output) {
+ const Reflection* message_reflection = message.GetReflection();
+
+ if (field->is_extension() &&
+ field->containing_type()->options().message_set_wire_format() &&
+ field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE &&
+ !field->is_repeated()) {
+ SerializeMessageSetItemWithCachedSizes(field, message, output);
+ return;
+ }
+
+ int count = 0;
+
+ if (field->is_repeated()) {
+ count = message_reflection->FieldSize(message, field);
+ } else if (message_reflection->HasField(message, field)) {
+ count = 1;
+ }
+
+ const bool is_packed = field->options().packed();
+ if (is_packed && count > 0) {
+ WireFormatLite::WriteTag(field->number(),
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED, output);
+ const int data_size = FieldDataOnlyByteSize(field, message);
+ output->WriteVarint32(data_size);
+ }
+
+ for (int j = 0; j < count; j++) {
+ switch (field->type()) {
+#define HANDLE_PRIMITIVE_TYPE(TYPE, CPPTYPE, TYPE_METHOD, CPPTYPE_METHOD) \
+ case FieldDescriptor::TYPE_##TYPE: { \
+ const CPPTYPE value = field->is_repeated() ? \
+ message_reflection->GetRepeated##CPPTYPE_METHOD( \
+ message, field, j) : \
+ message_reflection->Get##CPPTYPE_METHOD( \
+ message, field); \
+ if (is_packed) { \
+ WireFormatLite::Write##TYPE_METHOD##NoTag(value, output); \
+ } else { \
+ WireFormatLite::Write##TYPE_METHOD(field->number(), value, output); \
+ } \
+ break; \
+ }
+
+ HANDLE_PRIMITIVE_TYPE( INT32, int32, Int32, Int32)
+ HANDLE_PRIMITIVE_TYPE( INT64, int64, Int64, Int64)
+ HANDLE_PRIMITIVE_TYPE(SINT32, int32, SInt32, Int32)
+ HANDLE_PRIMITIVE_TYPE(SINT64, int64, SInt64, Int64)
+ HANDLE_PRIMITIVE_TYPE(UINT32, uint32, UInt32, UInt32)
+ HANDLE_PRIMITIVE_TYPE(UINT64, uint64, UInt64, UInt64)
+
+ HANDLE_PRIMITIVE_TYPE( FIXED32, uint32, Fixed32, UInt32)
+ HANDLE_PRIMITIVE_TYPE( FIXED64, uint64, Fixed64, UInt64)
+ HANDLE_PRIMITIVE_TYPE(SFIXED32, int32, SFixed32, Int32)
+ HANDLE_PRIMITIVE_TYPE(SFIXED64, int64, SFixed64, Int64)
+
+ HANDLE_PRIMITIVE_TYPE(FLOAT , float , Float , Float )
+ HANDLE_PRIMITIVE_TYPE(DOUBLE, double, Double, Double)
+
+ HANDLE_PRIMITIVE_TYPE(BOOL, bool, Bool, Bool)
+#undef HANDLE_PRIMITIVE_TYPE
+
+ case FieldDescriptor::TYPE_GROUP:
+ WireFormatLite::WriteGroup(
+ field->number(),
+ field->is_repeated() ?
+ message_reflection->GetRepeatedMessage(
+ message, field, j) :
+ message_reflection->GetMessage(message, field),
+ output);
+ break;
+
+ case FieldDescriptor::TYPE_MESSAGE:
+ WireFormatLite::WriteMessage(
+ field->number(),
+ field->is_repeated() ?
+ message_reflection->GetRepeatedMessage(
+ message, field, j) :
+ message_reflection->GetMessage(message, field),
+ output);
+ break;
+
+ case FieldDescriptor::TYPE_ENUM: {
+ const EnumValueDescriptor* value = field->is_repeated() ?
+ message_reflection->GetRepeatedEnum(message, field, j) :
+ message_reflection->GetEnum(message, field);
+ if (is_packed) {
+ WireFormatLite::WriteEnumNoTag(value->number(), output);
+ } else {
+ WireFormatLite::WriteEnum(field->number(), value->number(), output);
+ }
+ break;
+ }
+
+ // Handle strings separately so that we can get string references
+ // instead of copying.
+ case FieldDescriptor::TYPE_STRING: {
+ string scratch;
+ const string& value = field->is_repeated() ?
+ message_reflection->GetRepeatedStringReference(
+ message, field, j, &scratch) :
+ message_reflection->GetStringReference(message, field, &scratch);
+ VerifyUTF8StringNamedField(value.data(), value.length(), SERIALIZE,
+ field->name().c_str());
+ WireFormatLite::WriteString(field->number(), value, output);
+ break;
+ }
+
+ case FieldDescriptor::TYPE_BYTES: {
+ string scratch;
+ const string& value = field->is_repeated() ?
+ message_reflection->GetRepeatedStringReference(
+ message, field, j, &scratch) :
+ message_reflection->GetStringReference(message, field, &scratch);
+ WireFormatLite::WriteBytes(field->number(), value, output);
+ break;
+ }
+ }
+ }
+}
+
+void WireFormat::SerializeMessageSetItemWithCachedSizes(
+ const FieldDescriptor* field,
+ const Message& message,
+ io::CodedOutputStream* output) {
+ const Reflection* message_reflection = message.GetReflection();
+
+ // Start group.
+ output->WriteVarint32(WireFormatLite::kMessageSetItemStartTag);
+
+ // Write type ID.
+ output->WriteVarint32(WireFormatLite::kMessageSetTypeIdTag);
+ output->WriteVarint32(field->number());
+
+ // Write message.
+ output->WriteVarint32(WireFormatLite::kMessageSetMessageTag);
+
+ const Message& sub_message = message_reflection->GetMessage(message, field);
+ output->WriteVarint32(sub_message.GetCachedSize());
+ sub_message.SerializeWithCachedSizes(output);
+
+ // End group.
+ output->WriteVarint32(WireFormatLite::kMessageSetItemEndTag);
+}
+
+// ===================================================================
+
+int WireFormat::ByteSize(const Message& message) {
+ const Descriptor* descriptor = message.GetDescriptor();
+ const Reflection* message_reflection = message.GetReflection();
+
+ int our_size = 0;
+
+ vector<const FieldDescriptor*> fields;
+ message_reflection->ListFields(message, &fields);
+ for (int i = 0; i < fields.size(); i++) {
+ our_size += FieldByteSize(fields[i], message);
+ }
+
+ if (descriptor->options().message_set_wire_format()) {
+ our_size += ComputeUnknownMessageSetItemsSize(
+ message_reflection->GetUnknownFields(message));
+ } else {
+ our_size += ComputeUnknownFieldsSize(
+ message_reflection->GetUnknownFields(message));
+ }
+
+ return our_size;
+}
+
+int WireFormat::FieldByteSize(
+ const FieldDescriptor* field,
+ const Message& message) {
+ const Reflection* message_reflection = message.GetReflection();
+
+ if (field->is_extension() &&
+ field->containing_type()->options().message_set_wire_format() &&
+ field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE &&
+ !field->is_repeated()) {
+ return MessageSetItemByteSize(field, message);
+ }
+
+ int count = 0;
+ if (field->is_repeated()) {
+ count = message_reflection->FieldSize(message, field);
+ } else if (message_reflection->HasField(message, field)) {
+ count = 1;
+ }
+
+ const int data_size = FieldDataOnlyByteSize(field, message);
+ int our_size = data_size;
+ if (field->options().packed()) {
+ if (data_size > 0) {
+ // Packed fields get serialized like a string, not their native type.
+ // Technically this doesn't really matter; the size only changes if it's
+ // a GROUP
+ our_size += TagSize(field->number(), FieldDescriptor::TYPE_STRING);
+ our_size += io::CodedOutputStream::VarintSize32(data_size);
+ }
+ } else {
+ our_size += count * TagSize(field->number(), field->type());
+ }
+ return our_size;
+}
+
+int WireFormat::FieldDataOnlyByteSize(
+ const FieldDescriptor* field,
+ const Message& message) {
+ const Reflection* message_reflection = message.GetReflection();
+
+ int count = 0;
+ if (field->is_repeated()) {
+ count = message_reflection->FieldSize(message, field);
+ } else if (message_reflection->HasField(message, field)) {
+ count = 1;
+ }
+
+ int data_size = 0;
+ switch (field->type()) {
+#define HANDLE_TYPE(TYPE, TYPE_METHOD, CPPTYPE_METHOD) \
+ case FieldDescriptor::TYPE_##TYPE: \
+ if (field->is_repeated()) { \
+ for (int j = 0; j < count; j++) { \
+ data_size += WireFormatLite::TYPE_METHOD##Size( \
+ message_reflection->GetRepeated##CPPTYPE_METHOD( \
+ message, field, j)); \
+ } \
+ } else { \
+ data_size += WireFormatLite::TYPE_METHOD##Size( \
+ message_reflection->Get##CPPTYPE_METHOD(message, field)); \
+ } \
+ break;
+
+#define HANDLE_FIXED_TYPE(TYPE, TYPE_METHOD) \
+ case FieldDescriptor::TYPE_##TYPE: \
+ data_size += count * WireFormatLite::k##TYPE_METHOD##Size; \
+ break;
+
+ HANDLE_TYPE( INT32, Int32, Int32)
+ HANDLE_TYPE( INT64, Int64, Int64)
+ HANDLE_TYPE(SINT32, SInt32, Int32)
+ HANDLE_TYPE(SINT64, SInt64, Int64)
+ HANDLE_TYPE(UINT32, UInt32, UInt32)
+ HANDLE_TYPE(UINT64, UInt64, UInt64)
+
+ HANDLE_FIXED_TYPE( FIXED32, Fixed32)
+ HANDLE_FIXED_TYPE( FIXED64, Fixed64)
+ HANDLE_FIXED_TYPE(SFIXED32, SFixed32)
+ HANDLE_FIXED_TYPE(SFIXED64, SFixed64)
+
+ HANDLE_FIXED_TYPE(FLOAT , Float )
+ HANDLE_FIXED_TYPE(DOUBLE, Double)
+
+ HANDLE_FIXED_TYPE(BOOL, Bool)
+
+ HANDLE_TYPE(GROUP , Group , Message)
+ HANDLE_TYPE(MESSAGE, Message, Message)
+#undef HANDLE_TYPE
+#undef HANDLE_FIXED_TYPE
+
+ case FieldDescriptor::TYPE_ENUM: {
+ if (field->is_repeated()) {
+ for (int j = 0; j < count; j++) {
+ data_size += WireFormatLite::EnumSize(
+ message_reflection->GetRepeatedEnum(message, field, j)->number());
+ }
+ } else {
+ data_size += WireFormatLite::EnumSize(
+ message_reflection->GetEnum(message, field)->number());
+ }
+ break;
+ }
+
+ // Handle strings separately so that we can get string references
+ // instead of copying.
+ case FieldDescriptor::TYPE_STRING:
+ case FieldDescriptor::TYPE_BYTES: {
+ for (int j = 0; j < count; j++) {
+ string scratch;
+ const string& value = field->is_repeated() ?
+ message_reflection->GetRepeatedStringReference(
+ message, field, j, &scratch) :
+ message_reflection->GetStringReference(message, field, &scratch);
+ data_size += WireFormatLite::StringSize(value);
+ }
+ break;
+ }
+ }
+ return data_size;
+}
+
+int WireFormat::MessageSetItemByteSize(
+ const FieldDescriptor* field,
+ const Message& message) {
+ const Reflection* message_reflection = message.GetReflection();
+
+ int our_size = WireFormatLite::kMessageSetItemTagsSize;
+
+ // type_id
+ our_size += io::CodedOutputStream::VarintSize32(field->number());
+
+ // message
+ const Message& sub_message = message_reflection->GetMessage(message, field);
+ int message_size = sub_message.ByteSize();
+
+ our_size += io::CodedOutputStream::VarintSize32(message_size);
+ our_size += message_size;
+
+ return our_size;
+}
+
+void WireFormat::VerifyUTF8StringFallback(const char* data,
+ int size,
+ Operation op,
+ const char* field_name) {
+ if (!IsStructurallyValidUTF8(data, size)) {
+ const char* operation_str = NULL;
+ switch (op) {
+ case PARSE:
+ operation_str = "parsing";
+ break;
+ case SERIALIZE:
+ operation_str = "serializing";
+ break;
+ // no default case: have the compiler warn if a case is not covered.
+ }
+ string quoted_field_name = "";
+ if (field_name != NULL) {
+ quoted_field_name = StringPrintf(" '%s'", field_name);
+ }
+ // no space below to avoid double space when the field name is missing.
+ GOOGLE_LOG(ERROR) << "String field" << quoted_field_name << " contains invalid "
+ << "UTF-8 data when " << operation_str << " a protocol "
+ << "buffer. Use the 'bytes' type if you intend to send raw "
+ << "bytes. ";
+ }
+}
+
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/wire_format.h b/toolkit/components/protobuf/src/google/protobuf/wire_format.h
new file mode 100644
index 0000000000..9f26eb29bd
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/wire_format.h
@@ -0,0 +1,336 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// atenasio@google.com (Chris Atenasio) (ZigZag transform)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This header is logically internal, but is made public because it is used
+// from protocol-compiler-generated code, which may reside in other components.
+
+#ifndef GOOGLE_PROTOBUF_WIRE_FORMAT_H__
+#define GOOGLE_PROTOBUF_WIRE_FORMAT_H__
+
+#include <string>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/wire_format_lite.h>
+
+// Do UTF-8 validation on string type in Debug build only
+#ifndef NDEBUG
+#define GOOGLE_PROTOBUF_UTF8_VALIDATION_ENABLED
+#endif
+
+namespace google {
+namespace protobuf {
+ namespace io {
+ class CodedInputStream; // coded_stream.h
+ class CodedOutputStream; // coded_stream.h
+ }
+ class UnknownFieldSet; // unknown_field_set.h
+}
+
+namespace protobuf {
+namespace internal {
+
+// This class is for internal use by the protocol buffer library and by
+// protocol-complier-generated message classes. It must not be called
+// directly by clients.
+//
+// This class contains code for implementing the binary protocol buffer
+// wire format via reflection. The WireFormatLite class implements the
+// non-reflection based routines.
+//
+// This class is really a namespace that contains only static methods
+class LIBPROTOBUF_EXPORT WireFormat {
+ public:
+
+ // Given a field return its WireType
+ static inline WireFormatLite::WireType WireTypeForField(
+ const FieldDescriptor* field);
+
+ // Given a FieldDescriptor::Type return its WireType
+ static inline WireFormatLite::WireType WireTypeForFieldType(
+ FieldDescriptor::Type type);
+
+ // Compute the byte size of a tag. For groups, this includes both the start
+ // and end tags.
+ static inline int TagSize(int field_number, FieldDescriptor::Type type);
+
+ // These procedures can be used to implement the methods of Message which
+ // handle parsing and serialization of the protocol buffer wire format
+ // using only the Reflection interface. When you ask the protocol
+ // compiler to optimize for code size rather than speed, it will implement
+ // those methods in terms of these procedures. Of course, these are much
+ // slower than the specialized implementations which the protocol compiler
+ // generates when told to optimize for speed.
+
+ // Read a message in protocol buffer wire format.
+ //
+ // This procedure reads either to the end of the input stream or through
+ // a WIRETYPE_END_GROUP tag ending the message, whichever comes first.
+ // It returns false if the input is invalid.
+ //
+ // Required fields are NOT checked by this method. You must call
+ // IsInitialized() on the resulting message yourself.
+ static bool ParseAndMergePartial(io::CodedInputStream* input,
+ Message* message);
+
+ // Serialize a message in protocol buffer wire format.
+ //
+ // Any embedded messages within the message must have their correct sizes
+ // cached. However, the top-level message need not; its size is passed as
+ // a parameter to this procedure.
+ //
+ // These return false iff the underlying stream returns a write error.
+ static void SerializeWithCachedSizes(
+ const Message& message,
+ int size, io::CodedOutputStream* output);
+
+ // Implements Message::ByteSize() via reflection. WARNING: The result
+ // of this method is *not* cached anywhere. However, all embedded messages
+ // will have their ByteSize() methods called, so their sizes will be cached.
+ // Therefore, calling this method is sufficient to allow you to call
+ // WireFormat::SerializeWithCachedSizes() on the same object.
+ static int ByteSize(const Message& message);
+
+ // -----------------------------------------------------------------
+ // Helpers for dealing with unknown fields
+
+ // Skips a field value of the given WireType. The input should start
+ // positioned immediately after the tag. If unknown_fields is non-NULL,
+ // the contents of the field will be added to it.
+ static bool SkipField(io::CodedInputStream* input, uint32 tag,
+ UnknownFieldSet* unknown_fields);
+
+ // Reads and ignores a message from the input. If unknown_fields is non-NULL,
+ // the contents will be added to it.
+ static bool SkipMessage(io::CodedInputStream* input,
+ UnknownFieldSet* unknown_fields);
+
+ // Write the contents of an UnknownFieldSet to the output.
+ static void SerializeUnknownFields(const UnknownFieldSet& unknown_fields,
+ io::CodedOutputStream* output);
+ // Same as above, except writing directly to the provided buffer.
+ // Requires that the buffer have sufficient capacity for
+ // ComputeUnknownFieldsSize(unknown_fields).
+ //
+ // Returns a pointer past the last written byte.
+ static uint8* SerializeUnknownFieldsToArray(
+ const UnknownFieldSet& unknown_fields,
+ uint8* target);
+
+ // Same thing except for messages that have the message_set_wire_format
+ // option.
+ static void SerializeUnknownMessageSetItems(
+ const UnknownFieldSet& unknown_fields,
+ io::CodedOutputStream* output);
+ // Same as above, except writing directly to the provided buffer.
+ // Requires that the buffer have sufficient capacity for
+ // ComputeUnknownMessageSetItemsSize(unknown_fields).
+ //
+ // Returns a pointer past the last written byte.
+ static uint8* SerializeUnknownMessageSetItemsToArray(
+ const UnknownFieldSet& unknown_fields,
+ uint8* target);
+
+ // Compute the size of the UnknownFieldSet on the wire.
+ static int ComputeUnknownFieldsSize(const UnknownFieldSet& unknown_fields);
+
+ // Same thing except for messages that have the message_set_wire_format
+ // option.
+ static int ComputeUnknownMessageSetItemsSize(
+ const UnknownFieldSet& unknown_fields);
+
+
+ // Helper functions for encoding and decoding tags. (Inlined below and in
+ // _inl.h)
+ //
+ // This is different from MakeTag(field->number(), field->type()) in the case
+ // of packed repeated fields.
+ static uint32 MakeTag(const FieldDescriptor* field);
+
+ // Parse a single field. The input should start out positioned immediately
+ // after the tag.
+ static bool ParseAndMergeField(
+ uint32 tag,
+ const FieldDescriptor* field, // May be NULL for unknown
+ Message* message,
+ io::CodedInputStream* input);
+
+ // Serialize a single field.
+ static void SerializeFieldWithCachedSizes(
+ const FieldDescriptor* field, // Cannot be NULL
+ const Message& message,
+ io::CodedOutputStream* output);
+
+ // Compute size of a single field. If the field is a message type, this
+ // will call ByteSize() for the embedded message, insuring that it caches
+ // its size.
+ static int FieldByteSize(
+ const FieldDescriptor* field, // Cannot be NULL
+ const Message& message);
+
+ // Parse/serialize a MessageSet::Item group. Used with messages that use
+ // opion message_set_wire_format = true.
+ static bool ParseAndMergeMessageSetItem(
+ io::CodedInputStream* input,
+ Message* message);
+ static void SerializeMessageSetItemWithCachedSizes(
+ const FieldDescriptor* field,
+ const Message& message,
+ io::CodedOutputStream* output);
+ static int MessageSetItemByteSize(
+ const FieldDescriptor* field,
+ const Message& message);
+
+ // Computes the byte size of a field, excluding tags. For packed fields, it
+ // only includes the size of the raw data, and not the size of the total
+ // length, but for other length-delimited types, the size of the length is
+ // included.
+ static int FieldDataOnlyByteSize(
+ const FieldDescriptor* field, // Cannot be NULL
+ const Message& message);
+
+ enum Operation {
+ PARSE,
+ SERIALIZE,
+ };
+
+ // Verifies that a string field is valid UTF8, logging an error if not.
+ // This function will not be called by newly generated protobuf code
+ // but remains present to support existing code.
+ static void VerifyUTF8String(const char* data, int size, Operation op);
+ // The NamedField variant takes a field name in order to produce an
+ // informative error message if verification fails.
+ static void VerifyUTF8StringNamedField(const char* data,
+ int size,
+ Operation op,
+ const char* field_name);
+
+ private:
+ // Verifies that a string field is valid UTF8, logging an error if not.
+ static void VerifyUTF8StringFallback(
+ const char* data,
+ int size,
+ Operation op,
+ const char* field_name);
+
+ // Skip a MessageSet field.
+ static bool SkipMessageSetField(io::CodedInputStream* input,
+ uint32 field_number,
+ UnknownFieldSet* unknown_fields);
+
+ // Parse a MessageSet field.
+ static bool ParseAndMergeMessageSetField(uint32 field_number,
+ const FieldDescriptor* field,
+ Message* message,
+ io::CodedInputStream* input);
+
+
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(WireFormat);
+};
+
+// Subclass of FieldSkipper which saves skipped fields to an UnknownFieldSet.
+class LIBPROTOBUF_EXPORT UnknownFieldSetFieldSkipper : public FieldSkipper {
+ public:
+ UnknownFieldSetFieldSkipper(UnknownFieldSet* unknown_fields)
+ : unknown_fields_(unknown_fields) {}
+ virtual ~UnknownFieldSetFieldSkipper() {}
+
+ // implements FieldSkipper -----------------------------------------
+ virtual bool SkipField(io::CodedInputStream* input, uint32 tag);
+ virtual bool SkipMessage(io::CodedInputStream* input);
+ virtual void SkipUnknownEnum(int field_number, int value);
+
+ protected:
+ UnknownFieldSet* unknown_fields_;
+};
+
+// inline methods ====================================================
+
+inline WireFormatLite::WireType WireFormat::WireTypeForField(
+ const FieldDescriptor* field) {
+ if (field->options().packed()) {
+ return WireFormatLite::WIRETYPE_LENGTH_DELIMITED;
+ } else {
+ return WireTypeForFieldType(field->type());
+ }
+}
+
+inline WireFormatLite::WireType WireFormat::WireTypeForFieldType(
+ FieldDescriptor::Type type) {
+ // Some compilers don't like enum -> enum casts, so we implicit_cast to
+ // int first.
+ return WireFormatLite::WireTypeForFieldType(
+ static_cast<WireFormatLite::FieldType>(
+ implicit_cast<int>(type)));
+}
+
+inline uint32 WireFormat::MakeTag(const FieldDescriptor* field) {
+ return WireFormatLite::MakeTag(field->number(), WireTypeForField(field));
+}
+
+inline int WireFormat::TagSize(int field_number, FieldDescriptor::Type type) {
+ // Some compilers don't like enum -> enum casts, so we implicit_cast to
+ // int first.
+ return WireFormatLite::TagSize(field_number,
+ static_cast<WireFormatLite::FieldType>(
+ implicit_cast<int>(type)));
+}
+
+inline void WireFormat::VerifyUTF8String(const char* data, int size,
+ WireFormat::Operation op) {
+#ifdef GOOGLE_PROTOBUF_UTF8_VALIDATION_ENABLED
+ WireFormat::VerifyUTF8StringFallback(data, size, op, NULL);
+#else
+ // Avoid the compiler warning about unsued variables.
+ (void)data; (void)size; (void)op;
+#endif
+}
+
+inline void WireFormat::VerifyUTF8StringNamedField(
+ const char* data, int size, WireFormat::Operation op,
+ const char* field_name) {
+#ifdef GOOGLE_PROTOBUF_UTF8_VALIDATION_ENABLED
+ WireFormat::VerifyUTF8StringFallback(data, size, op, field_name);
+#endif
+}
+
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_WIRE_FORMAT_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.cc b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.cc
new file mode 100644
index 0000000000..8de827849d
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.cc
@@ -0,0 +1,471 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#include <google/protobuf/wire_format_lite_inl.h>
+
+#include <stack>
+#include <string>
+#include <vector>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/io/coded_stream_inl.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+#ifndef _MSC_VER // MSVC doesn't like definitions of inline constants, GCC
+ // requires them.
+const int WireFormatLite::kMessageSetItemStartTag;
+const int WireFormatLite::kMessageSetItemEndTag;
+const int WireFormatLite::kMessageSetTypeIdTag;
+const int WireFormatLite::kMessageSetMessageTag;
+
+#endif
+
+const int WireFormatLite::kMessageSetItemTagsSize =
+ io::CodedOutputStream::StaticVarintSize32<kMessageSetItemStartTag>::value +
+ io::CodedOutputStream::StaticVarintSize32<kMessageSetItemEndTag>::value +
+ io::CodedOutputStream::StaticVarintSize32<kMessageSetTypeIdTag>::value +
+ io::CodedOutputStream::StaticVarintSize32<kMessageSetMessageTag>::value;
+
+const WireFormatLite::CppType
+WireFormatLite::kFieldTypeToCppTypeMap[MAX_FIELD_TYPE + 1] = {
+ static_cast<CppType>(0), // 0 is reserved for errors
+
+ CPPTYPE_DOUBLE, // TYPE_DOUBLE
+ CPPTYPE_FLOAT, // TYPE_FLOAT
+ CPPTYPE_INT64, // TYPE_INT64
+ CPPTYPE_UINT64, // TYPE_UINT64
+ CPPTYPE_INT32, // TYPE_INT32
+ CPPTYPE_UINT64, // TYPE_FIXED64
+ CPPTYPE_UINT32, // TYPE_FIXED32
+ CPPTYPE_BOOL, // TYPE_BOOL
+ CPPTYPE_STRING, // TYPE_STRING
+ CPPTYPE_MESSAGE, // TYPE_GROUP
+ CPPTYPE_MESSAGE, // TYPE_MESSAGE
+ CPPTYPE_STRING, // TYPE_BYTES
+ CPPTYPE_UINT32, // TYPE_UINT32
+ CPPTYPE_ENUM, // TYPE_ENUM
+ CPPTYPE_INT32, // TYPE_SFIXED32
+ CPPTYPE_INT64, // TYPE_SFIXED64
+ CPPTYPE_INT32, // TYPE_SINT32
+ CPPTYPE_INT64, // TYPE_SINT64
+};
+
+const WireFormatLite::WireType
+WireFormatLite::kWireTypeForFieldType[MAX_FIELD_TYPE + 1] = {
+ static_cast<WireFormatLite::WireType>(-1), // invalid
+ WireFormatLite::WIRETYPE_FIXED64, // TYPE_DOUBLE
+ WireFormatLite::WIRETYPE_FIXED32, // TYPE_FLOAT
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_INT64
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_UINT64
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_INT32
+ WireFormatLite::WIRETYPE_FIXED64, // TYPE_FIXED64
+ WireFormatLite::WIRETYPE_FIXED32, // TYPE_FIXED32
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_BOOL
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED, // TYPE_STRING
+ WireFormatLite::WIRETYPE_START_GROUP, // TYPE_GROUP
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED, // TYPE_MESSAGE
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED, // TYPE_BYTES
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_UINT32
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_ENUM
+ WireFormatLite::WIRETYPE_FIXED32, // TYPE_SFIXED32
+ WireFormatLite::WIRETYPE_FIXED64, // TYPE_SFIXED64
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_SINT32
+ WireFormatLite::WIRETYPE_VARINT, // TYPE_SINT64
+};
+
+bool WireFormatLite::SkipField(
+ io::CodedInputStream* input, uint32 tag) {
+ switch (WireFormatLite::GetTagWireType(tag)) {
+ case WireFormatLite::WIRETYPE_VARINT: {
+ uint64 value;
+ if (!input->ReadVarint64(&value)) return false;
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_FIXED64: {
+ uint64 value;
+ if (!input->ReadLittleEndian64(&value)) return false;
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED: {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (!input->Skip(length)) return false;
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_START_GROUP: {
+ if (!input->IncrementRecursionDepth()) return false;
+ if (!SkipMessage(input)) return false;
+ input->DecrementRecursionDepth();
+ // Check that the ending tag matched the starting tag.
+ if (!input->LastTagWas(WireFormatLite::MakeTag(
+ WireFormatLite::GetTagFieldNumber(tag),
+ WireFormatLite::WIRETYPE_END_GROUP))) {
+ return false;
+ }
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_END_GROUP: {
+ return false;
+ }
+ case WireFormatLite::WIRETYPE_FIXED32: {
+ uint32 value;
+ if (!input->ReadLittleEndian32(&value)) return false;
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+}
+
+bool WireFormatLite::SkipField(
+ io::CodedInputStream* input, uint32 tag, io::CodedOutputStream* output) {
+ switch (WireFormatLite::GetTagWireType(tag)) {
+ case WireFormatLite::WIRETYPE_VARINT: {
+ uint64 value;
+ if (!input->ReadVarint64(&value)) return false;
+ output->WriteVarint32(tag);
+ output->WriteVarint64(value);
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_FIXED64: {
+ uint64 value;
+ if (!input->ReadLittleEndian64(&value)) return false;
+ output->WriteVarint32(tag);
+ output->WriteLittleEndian64(value);
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_LENGTH_DELIMITED: {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ output->WriteVarint32(tag);
+ output->WriteVarint32(length);
+ // TODO(mkilavuz): Provide API to prevent extra string copying.
+ string temp;
+ if (!input->ReadString(&temp, length)) return false;
+ output->WriteString(temp);
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_START_GROUP: {
+ output->WriteVarint32(tag);
+ if (!input->IncrementRecursionDepth()) return false;
+ if (!SkipMessage(input, output)) return false;
+ input->DecrementRecursionDepth();
+ // Check that the ending tag matched the starting tag.
+ if (!input->LastTagWas(WireFormatLite::MakeTag(
+ WireFormatLite::GetTagFieldNumber(tag),
+ WireFormatLite::WIRETYPE_END_GROUP))) {
+ return false;
+ }
+ return true;
+ }
+ case WireFormatLite::WIRETYPE_END_GROUP: {
+ return false;
+ }
+ case WireFormatLite::WIRETYPE_FIXED32: {
+ uint32 value;
+ if (!input->ReadLittleEndian32(&value)) return false;
+ output->WriteVarint32(tag);
+ output->WriteLittleEndian32(value);
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+}
+
+bool WireFormatLite::SkipMessage(io::CodedInputStream* input) {
+ while (true) {
+ uint32 tag = input->ReadTag();
+ if (tag == 0) {
+ // End of input. This is a valid place to end, so return true.
+ return true;
+ }
+
+ WireFormatLite::WireType wire_type = WireFormatLite::GetTagWireType(tag);
+
+ if (wire_type == WireFormatLite::WIRETYPE_END_GROUP) {
+ // Must be the end of the message.
+ return true;
+ }
+
+ if (!SkipField(input, tag)) return false;
+ }
+}
+
+bool WireFormatLite::SkipMessage(io::CodedInputStream* input,
+ io::CodedOutputStream* output) {
+ while (true) {
+ uint32 tag = input->ReadTag();
+ if (tag == 0) {
+ // End of input. This is a valid place to end, so return true.
+ return true;
+ }
+
+ WireFormatLite::WireType wire_type = WireFormatLite::GetTagWireType(tag);
+
+ if (wire_type == WireFormatLite::WIRETYPE_END_GROUP) {
+ output->WriteVarint32(tag);
+ // Must be the end of the message.
+ return true;
+ }
+
+ if (!SkipField(input, tag, output)) return false;
+ }
+}
+
+bool FieldSkipper::SkipField(
+ io::CodedInputStream* input, uint32 tag) {
+ return WireFormatLite::SkipField(input, tag);
+}
+
+bool FieldSkipper::SkipMessage(io::CodedInputStream* input) {
+ return WireFormatLite::SkipMessage(input);
+}
+
+void FieldSkipper::SkipUnknownEnum(
+ int /* field_number */, int /* value */) {
+ // Nothing.
+}
+
+bool CodedOutputStreamFieldSkipper::SkipField(
+ io::CodedInputStream* input, uint32 tag) {
+ return WireFormatLite::SkipField(input, tag, unknown_fields_);
+}
+
+bool CodedOutputStreamFieldSkipper::SkipMessage(io::CodedInputStream* input) {
+ return WireFormatLite::SkipMessage(input, unknown_fields_);
+}
+
+void CodedOutputStreamFieldSkipper::SkipUnknownEnum(
+ int field_number, int value) {
+ unknown_fields_->WriteVarint32(field_number);
+ unknown_fields_->WriteVarint64(value);
+}
+
+bool WireFormatLite::ReadPackedEnumNoInline(io::CodedInputStream* input,
+ bool (*is_valid)(int),
+ RepeatedField<int>* values) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ io::CodedInputStream::Limit limit = input->PushLimit(length);
+ while (input->BytesUntilLimit() > 0) {
+ int value;
+ if (!google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, WireFormatLite::TYPE_ENUM>(input, &value)) {
+ return false;
+ }
+ if (is_valid(value)) {
+ values->Add(value);
+ }
+ }
+ input->PopLimit(limit);
+ return true;
+}
+
+void WireFormatLite::WriteInt32(int field_number, int32 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteInt32NoTag(value, output);
+}
+void WireFormatLite::WriteInt64(int field_number, int64 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteInt64NoTag(value, output);
+}
+void WireFormatLite::WriteUInt32(int field_number, uint32 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteUInt32NoTag(value, output);
+}
+void WireFormatLite::WriteUInt64(int field_number, uint64 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteUInt64NoTag(value, output);
+}
+void WireFormatLite::WriteSInt32(int field_number, int32 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteSInt32NoTag(value, output);
+}
+void WireFormatLite::WriteSInt64(int field_number, int64 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteSInt64NoTag(value, output);
+}
+void WireFormatLite::WriteFixed32(int field_number, uint32 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_FIXED32, output);
+ WriteFixed32NoTag(value, output);
+}
+void WireFormatLite::WriteFixed64(int field_number, uint64 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_FIXED64, output);
+ WriteFixed64NoTag(value, output);
+}
+void WireFormatLite::WriteSFixed32(int field_number, int32 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_FIXED32, output);
+ WriteSFixed32NoTag(value, output);
+}
+void WireFormatLite::WriteSFixed64(int field_number, int64 value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_FIXED64, output);
+ WriteSFixed64NoTag(value, output);
+}
+void WireFormatLite::WriteFloat(int field_number, float value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_FIXED32, output);
+ WriteFloatNoTag(value, output);
+}
+void WireFormatLite::WriteDouble(int field_number, double value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_FIXED64, output);
+ WriteDoubleNoTag(value, output);
+}
+void WireFormatLite::WriteBool(int field_number, bool value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteBoolNoTag(value, output);
+}
+void WireFormatLite::WriteEnum(int field_number, int value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_VARINT, output);
+ WriteEnumNoTag(value, output);
+}
+
+void WireFormatLite::WriteString(int field_number, const string& value,
+ io::CodedOutputStream* output) {
+ // String is for UTF-8 text only
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ GOOGLE_CHECK(value.size() <= kint32max);
+ output->WriteVarint32(value.size());
+ output->WriteString(value);
+}
+void WireFormatLite::WriteStringMaybeAliased(
+ int field_number, const string& value,
+ io::CodedOutputStream* output) {
+ // String is for UTF-8 text only
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ GOOGLE_CHECK(value.size() <= kint32max);
+ output->WriteVarint32(value.size());
+ output->WriteRawMaybeAliased(value.data(), value.size());
+}
+void WireFormatLite::WriteBytes(int field_number, const string& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ GOOGLE_CHECK(value.size() <= kint32max);
+ output->WriteVarint32(value.size());
+ output->WriteString(value);
+}
+void WireFormatLite::WriteBytesMaybeAliased(
+ int field_number, const string& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ GOOGLE_CHECK(value.size() <= kint32max);
+ output->WriteVarint32(value.size());
+ output->WriteRawMaybeAliased(value.data(), value.size());
+}
+
+
+void WireFormatLite::WriteGroup(int field_number,
+ const MessageLite& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_START_GROUP, output);
+ value.SerializeWithCachedSizes(output);
+ WriteTag(field_number, WIRETYPE_END_GROUP, output);
+}
+
+void WireFormatLite::WriteMessage(int field_number,
+ const MessageLite& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ const int size = value.GetCachedSize();
+ output->WriteVarint32(size);
+ value.SerializeWithCachedSizes(output);
+}
+
+void WireFormatLite::WriteGroupMaybeToArray(int field_number,
+ const MessageLite& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_START_GROUP, output);
+ const int size = value.GetCachedSize();
+ uint8* target = output->GetDirectBufferForNBytesAndAdvance(size);
+ if (target != NULL) {
+ uint8* end = value.SerializeWithCachedSizesToArray(target);
+ GOOGLE_DCHECK_EQ(end - target, size);
+ } else {
+ value.SerializeWithCachedSizes(output);
+ }
+ WriteTag(field_number, WIRETYPE_END_GROUP, output);
+}
+
+void WireFormatLite::WriteMessageMaybeToArray(int field_number,
+ const MessageLite& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ const int size = value.GetCachedSize();
+ output->WriteVarint32(size);
+ uint8* target = output->GetDirectBufferForNBytesAndAdvance(size);
+ if (target != NULL) {
+ uint8* end = value.SerializeWithCachedSizesToArray(target);
+ GOOGLE_DCHECK_EQ(end - target, size);
+ } else {
+ value.SerializeWithCachedSizes(output);
+ }
+}
+
+bool WireFormatLite::ReadString(io::CodedInputStream* input,
+ string* value) {
+ // String is for UTF-8 text only
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (!input->InternalReadStringInline(value, length)) return false;
+ return true;
+}
+bool WireFormatLite::ReadBytes(io::CodedInputStream* input,
+ string* value) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ return input->InternalReadStringInline(value, length);
+}
+
+} // namespace internal
+} // namespace protobuf
+} // namespace google
diff --git a/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.h b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.h
new file mode 100644
index 0000000000..14b3feb6ab
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite.h
@@ -0,0 +1,662 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// atenasio@google.com (Chris Atenasio) (ZigZag transform)
+// wink@google.com (Wink Saville) (refactored from wire_format.h)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+//
+// This header is logically internal, but is made public because it is used
+// from protocol-compiler-generated code, which may reside in other components.
+
+#ifndef GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_H__
+#define GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_H__
+
+#include <algorithm>
+#include <string>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/message_lite.h>
+#include <google/protobuf/io/coded_stream.h> // for CodedOutputStream::Varint32Size
+
+namespace google {
+
+namespace protobuf {
+ template <typename T> class RepeatedField; // repeated_field.h
+}
+
+namespace protobuf {
+namespace internal {
+
+class StringPieceField;
+
+// This class is for internal use by the protocol buffer library and by
+// protocol-complier-generated message classes. It must not be called
+// directly by clients.
+//
+// This class contains helpers for implementing the binary protocol buffer
+// wire format without the need for reflection. Use WireFormat when using
+// reflection.
+//
+// This class is really a namespace that contains only static methods.
+class LIBPROTOBUF_EXPORT WireFormatLite {
+ public:
+
+ // -----------------------------------------------------------------
+ // Helper constants and functions related to the format. These are
+ // mostly meant for internal and generated code to use.
+
+ // The wire format is composed of a sequence of tag/value pairs, each
+ // of which contains the value of one field (or one element of a repeated
+ // field). Each tag is encoded as a varint. The lower bits of the tag
+ // identify its wire type, which specifies the format of the data to follow.
+ // The rest of the bits contain the field number. Each type of field (as
+ // declared by FieldDescriptor::Type, in descriptor.h) maps to one of
+ // these wire types. Immediately following each tag is the field's value,
+ // encoded in the format specified by the wire type. Because the tag
+ // identifies the encoding of this data, it is possible to skip
+ // unrecognized fields for forwards compatibility.
+
+ enum WireType {
+ WIRETYPE_VARINT = 0,
+ WIRETYPE_FIXED64 = 1,
+ WIRETYPE_LENGTH_DELIMITED = 2,
+ WIRETYPE_START_GROUP = 3,
+ WIRETYPE_END_GROUP = 4,
+ WIRETYPE_FIXED32 = 5,
+ };
+
+ // Lite alternative to FieldDescriptor::Type. Must be kept in sync.
+ enum FieldType {
+ TYPE_DOUBLE = 1,
+ TYPE_FLOAT = 2,
+ TYPE_INT64 = 3,
+ TYPE_UINT64 = 4,
+ TYPE_INT32 = 5,
+ TYPE_FIXED64 = 6,
+ TYPE_FIXED32 = 7,
+ TYPE_BOOL = 8,
+ TYPE_STRING = 9,
+ TYPE_GROUP = 10,
+ TYPE_MESSAGE = 11,
+ TYPE_BYTES = 12,
+ TYPE_UINT32 = 13,
+ TYPE_ENUM = 14,
+ TYPE_SFIXED32 = 15,
+ TYPE_SFIXED64 = 16,
+ TYPE_SINT32 = 17,
+ TYPE_SINT64 = 18,
+ MAX_FIELD_TYPE = 18,
+ };
+
+ // Lite alternative to FieldDescriptor::CppType. Must be kept in sync.
+ enum CppType {
+ CPPTYPE_INT32 = 1,
+ CPPTYPE_INT64 = 2,
+ CPPTYPE_UINT32 = 3,
+ CPPTYPE_UINT64 = 4,
+ CPPTYPE_DOUBLE = 5,
+ CPPTYPE_FLOAT = 6,
+ CPPTYPE_BOOL = 7,
+ CPPTYPE_ENUM = 8,
+ CPPTYPE_STRING = 9,
+ CPPTYPE_MESSAGE = 10,
+ MAX_CPPTYPE = 10,
+ };
+
+ // Helper method to get the CppType for a particular Type.
+ static CppType FieldTypeToCppType(FieldType type);
+
+ // Given a FieldSescriptor::Type return its WireType
+ static inline WireFormatLite::WireType WireTypeForFieldType(
+ WireFormatLite::FieldType type) {
+ return kWireTypeForFieldType[type];
+ }
+
+ // Number of bits in a tag which identify the wire type.
+ static const int kTagTypeBits = 3;
+ // Mask for those bits.
+ static const uint32 kTagTypeMask = (1 << kTagTypeBits) - 1;
+
+ // Helper functions for encoding and decoding tags. (Inlined below and in
+ // _inl.h)
+ //
+ // This is different from MakeTag(field->number(), field->type()) in the case
+ // of packed repeated fields.
+ static uint32 MakeTag(int field_number, WireType type);
+ static WireType GetTagWireType(uint32 tag);
+ static int GetTagFieldNumber(uint32 tag);
+
+ // Compute the byte size of a tag. For groups, this includes both the start
+ // and end tags.
+ static inline int TagSize(int field_number, WireFormatLite::FieldType type);
+
+ // Skips a field value with the given tag. The input should start
+ // positioned immediately after the tag. Skipped values are simply discarded,
+ // not recorded anywhere. See WireFormat::SkipField() for a version that
+ // records to an UnknownFieldSet.
+ static bool SkipField(io::CodedInputStream* input, uint32 tag);
+
+ // Skips a field value with the given tag. The input should start
+ // positioned immediately after the tag. Skipped values are recorded to a
+ // CodedOutputStream.
+ static bool SkipField(io::CodedInputStream* input, uint32 tag,
+ io::CodedOutputStream* output);
+
+ // Reads and ignores a message from the input. Skipped values are simply
+ // discarded, not recorded anywhere. See WireFormat::SkipMessage() for a
+ // version that records to an UnknownFieldSet.
+ static bool SkipMessage(io::CodedInputStream* input);
+
+ // Reads and ignores a message from the input. Skipped values are recorded
+ // to a CodedOutputStream.
+ static bool SkipMessage(io::CodedInputStream* input,
+ io::CodedOutputStream* output);
+
+// This macro does the same thing as WireFormatLite::MakeTag(), but the
+// result is usable as a compile-time constant, which makes it usable
+// as a switch case or a template input. WireFormatLite::MakeTag() is more
+// type-safe, though, so prefer it if possible.
+#define GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(FIELD_NUMBER, TYPE) \
+ static_cast<uint32>( \
+ ((FIELD_NUMBER) << ::google::protobuf::internal::WireFormatLite::kTagTypeBits) \
+ | (TYPE))
+
+ // These are the tags for the old MessageSet format, which was defined as:
+ // message MessageSet {
+ // repeated group Item = 1 {
+ // required int32 type_id = 2;
+ // required string message = 3;
+ // }
+ // }
+ static const int kMessageSetItemNumber = 1;
+ static const int kMessageSetTypeIdNumber = 2;
+ static const int kMessageSetMessageNumber = 3;
+ static const int kMessageSetItemStartTag =
+ GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(kMessageSetItemNumber,
+ WireFormatLite::WIRETYPE_START_GROUP);
+ static const int kMessageSetItemEndTag =
+ GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(kMessageSetItemNumber,
+ WireFormatLite::WIRETYPE_END_GROUP);
+ static const int kMessageSetTypeIdTag =
+ GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(kMessageSetTypeIdNumber,
+ WireFormatLite::WIRETYPE_VARINT);
+ static const int kMessageSetMessageTag =
+ GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(kMessageSetMessageNumber,
+ WireFormatLite::WIRETYPE_LENGTH_DELIMITED);
+
+ // Byte size of all tags of a MessageSet::Item combined.
+ static const int kMessageSetItemTagsSize;
+
+ // Helper functions for converting between floats/doubles and IEEE-754
+ // uint32s/uint64s so that they can be written. (Assumes your platform
+ // uses IEEE-754 floats.)
+ static uint32 EncodeFloat(float value);
+ static float DecodeFloat(uint32 value);
+ static uint64 EncodeDouble(double value);
+ static double DecodeDouble(uint64 value);
+
+ // Helper functions for mapping signed integers to unsigned integers in
+ // such a way that numbers with small magnitudes will encode to smaller
+ // varints. If you simply static_cast a negative number to an unsigned
+ // number and varint-encode it, it will always take 10 bytes, defeating
+ // the purpose of varint. So, for the "sint32" and "sint64" field types,
+ // we ZigZag-encode the values.
+ static uint32 ZigZagEncode32(int32 n);
+ static int32 ZigZagDecode32(uint32 n);
+ static uint64 ZigZagEncode64(int64 n);
+ static int64 ZigZagDecode64(uint64 n);
+
+ // =================================================================
+ // Methods for reading/writing individual field. The implementations
+ // of these methods are defined in wire_format_lite_inl.h; you must #include
+ // that file to use these.
+
+// Avoid ugly line wrapping
+#define input io::CodedInputStream* input_arg
+#define output io::CodedOutputStream* output_arg
+#define field_number int field_number_arg
+#define INL GOOGLE_ATTRIBUTE_ALWAYS_INLINE
+
+ // Read fields, not including tags. The assumption is that you already
+ // read the tag to determine what field to read.
+
+ // For primitive fields, we just use a templatized routine parameterized by
+ // the represented type and the FieldType. These are specialized with the
+ // appropriate definition for each declared type.
+ template <typename CType, enum FieldType DeclaredType>
+ static inline bool ReadPrimitive(input, CType* value) INL;
+
+ // Reads repeated primitive values, with optimizations for repeats.
+ // tag_size and tag should both be compile-time constants provided by the
+ // protocol compiler.
+ template <typename CType, enum FieldType DeclaredType>
+ static inline bool ReadRepeatedPrimitive(int tag_size,
+ uint32 tag,
+ input,
+ RepeatedField<CType>* value) INL;
+
+ // Identical to ReadRepeatedPrimitive, except will not inline the
+ // implementation.
+ template <typename CType, enum FieldType DeclaredType>
+ static bool ReadRepeatedPrimitiveNoInline(int tag_size,
+ uint32 tag,
+ input,
+ RepeatedField<CType>* value);
+
+ // Reads a primitive value directly from the provided buffer. It returns a
+ // pointer past the segment of data that was read.
+ //
+ // This is only implemented for the types with fixed wire size, e.g.
+ // float, double, and the (s)fixed* types.
+ template <typename CType, enum FieldType DeclaredType>
+ static inline const uint8* ReadPrimitiveFromArray(const uint8* buffer,
+ CType* value) INL;
+
+ // Reads a primitive packed field.
+ //
+ // This is only implemented for packable types.
+ template <typename CType, enum FieldType DeclaredType>
+ static inline bool ReadPackedPrimitive(input,
+ RepeatedField<CType>* value) INL;
+
+ // Identical to ReadPackedPrimitive, except will not inline the
+ // implementation.
+ template <typename CType, enum FieldType DeclaredType>
+ static bool ReadPackedPrimitiveNoInline(input, RepeatedField<CType>* value);
+
+ // Read a packed enum field. Values for which is_valid() returns false are
+ // dropped.
+ static bool ReadPackedEnumNoInline(input,
+ bool (*is_valid)(int),
+ RepeatedField<int>* value);
+
+ static bool ReadString(input, string* value);
+ static bool ReadBytes (input, string* value);
+
+ static inline bool ReadGroup (field_number, input, MessageLite* value);
+ static inline bool ReadMessage(input, MessageLite* value);
+
+ // Like above, but de-virtualize the call to MergePartialFromCodedStream().
+ // The pointer must point at an instance of MessageType, *not* a subclass (or
+ // the subclass must not override MergePartialFromCodedStream()).
+ template<typename MessageType>
+ static inline bool ReadGroupNoVirtual(field_number, input,
+ MessageType* value);
+ template<typename MessageType>
+ static inline bool ReadMessageNoVirtual(input, MessageType* value);
+
+ // Write a tag. The Write*() functions typically include the tag, so
+ // normally there's no need to call this unless using the Write*NoTag()
+ // variants.
+ static inline void WriteTag(field_number, WireType type, output) INL;
+
+ // Write fields, without tags.
+ static inline void WriteInt32NoTag (int32 value, output) INL;
+ static inline void WriteInt64NoTag (int64 value, output) INL;
+ static inline void WriteUInt32NoTag (uint32 value, output) INL;
+ static inline void WriteUInt64NoTag (uint64 value, output) INL;
+ static inline void WriteSInt32NoTag (int32 value, output) INL;
+ static inline void WriteSInt64NoTag (int64 value, output) INL;
+ static inline void WriteFixed32NoTag (uint32 value, output) INL;
+ static inline void WriteFixed64NoTag (uint64 value, output) INL;
+ static inline void WriteSFixed32NoTag(int32 value, output) INL;
+ static inline void WriteSFixed64NoTag(int64 value, output) INL;
+ static inline void WriteFloatNoTag (float value, output) INL;
+ static inline void WriteDoubleNoTag (double value, output) INL;
+ static inline void WriteBoolNoTag (bool value, output) INL;
+ static inline void WriteEnumNoTag (int value, output) INL;
+
+ // Write fields, including tags.
+ static void WriteInt32 (field_number, int32 value, output);
+ static void WriteInt64 (field_number, int64 value, output);
+ static void WriteUInt32 (field_number, uint32 value, output);
+ static void WriteUInt64 (field_number, uint64 value, output);
+ static void WriteSInt32 (field_number, int32 value, output);
+ static void WriteSInt64 (field_number, int64 value, output);
+ static void WriteFixed32 (field_number, uint32 value, output);
+ static void WriteFixed64 (field_number, uint64 value, output);
+ static void WriteSFixed32(field_number, int32 value, output);
+ static void WriteSFixed64(field_number, int64 value, output);
+ static void WriteFloat (field_number, float value, output);
+ static void WriteDouble (field_number, double value, output);
+ static void WriteBool (field_number, bool value, output);
+ static void WriteEnum (field_number, int value, output);
+
+ static void WriteString(field_number, const string& value, output);
+ static void WriteBytes (field_number, const string& value, output);
+ static void WriteStringMaybeAliased(
+ field_number, const string& value, output);
+ static void WriteBytesMaybeAliased(
+ field_number, const string& value, output);
+
+ static void WriteGroup(
+ field_number, const MessageLite& value, output);
+ static void WriteMessage(
+ field_number, const MessageLite& value, output);
+ // Like above, but these will check if the output stream has enough
+ // space to write directly to a flat array.
+ static void WriteGroupMaybeToArray(
+ field_number, const MessageLite& value, output);
+ static void WriteMessageMaybeToArray(
+ field_number, const MessageLite& value, output);
+
+ // Like above, but de-virtualize the call to SerializeWithCachedSizes(). The
+ // pointer must point at an instance of MessageType, *not* a subclass (or
+ // the subclass must not override SerializeWithCachedSizes()).
+ template<typename MessageType>
+ static inline void WriteGroupNoVirtual(
+ field_number, const MessageType& value, output);
+ template<typename MessageType>
+ static inline void WriteMessageNoVirtual(
+ field_number, const MessageType& value, output);
+
+#undef output
+#define output uint8* target
+
+ // Like above, but use only *ToArray methods of CodedOutputStream.
+ static inline uint8* WriteTagToArray(field_number, WireType type, output) INL;
+
+ // Write fields, without tags.
+ static inline uint8* WriteInt32NoTagToArray (int32 value, output) INL;
+ static inline uint8* WriteInt64NoTagToArray (int64 value, output) INL;
+ static inline uint8* WriteUInt32NoTagToArray (uint32 value, output) INL;
+ static inline uint8* WriteUInt64NoTagToArray (uint64 value, output) INL;
+ static inline uint8* WriteSInt32NoTagToArray (int32 value, output) INL;
+ static inline uint8* WriteSInt64NoTagToArray (int64 value, output) INL;
+ static inline uint8* WriteFixed32NoTagToArray (uint32 value, output) INL;
+ static inline uint8* WriteFixed64NoTagToArray (uint64 value, output) INL;
+ static inline uint8* WriteSFixed32NoTagToArray(int32 value, output) INL;
+ static inline uint8* WriteSFixed64NoTagToArray(int64 value, output) INL;
+ static inline uint8* WriteFloatNoTagToArray (float value, output) INL;
+ static inline uint8* WriteDoubleNoTagToArray (double value, output) INL;
+ static inline uint8* WriteBoolNoTagToArray (bool value, output) INL;
+ static inline uint8* WriteEnumNoTagToArray (int value, output) INL;
+
+ // Write fields, including tags.
+ static inline uint8* WriteInt32ToArray(
+ field_number, int32 value, output) INL;
+ static inline uint8* WriteInt64ToArray(
+ field_number, int64 value, output) INL;
+ static inline uint8* WriteUInt32ToArray(
+ field_number, uint32 value, output) INL;
+ static inline uint8* WriteUInt64ToArray(
+ field_number, uint64 value, output) INL;
+ static inline uint8* WriteSInt32ToArray(
+ field_number, int32 value, output) INL;
+ static inline uint8* WriteSInt64ToArray(
+ field_number, int64 value, output) INL;
+ static inline uint8* WriteFixed32ToArray(
+ field_number, uint32 value, output) INL;
+ static inline uint8* WriteFixed64ToArray(
+ field_number, uint64 value, output) INL;
+ static inline uint8* WriteSFixed32ToArray(
+ field_number, int32 value, output) INL;
+ static inline uint8* WriteSFixed64ToArray(
+ field_number, int64 value, output) INL;
+ static inline uint8* WriteFloatToArray(
+ field_number, float value, output) INL;
+ static inline uint8* WriteDoubleToArray(
+ field_number, double value, output) INL;
+ static inline uint8* WriteBoolToArray(
+ field_number, bool value, output) INL;
+ static inline uint8* WriteEnumToArray(
+ field_number, int value, output) INL;
+
+ static inline uint8* WriteStringToArray(
+ field_number, const string& value, output) INL;
+ static inline uint8* WriteBytesToArray(
+ field_number, const string& value, output) INL;
+
+ static inline uint8* WriteGroupToArray(
+ field_number, const MessageLite& value, output) INL;
+ static inline uint8* WriteMessageToArray(
+ field_number, const MessageLite& value, output) INL;
+
+ // Like above, but de-virtualize the call to SerializeWithCachedSizes(). The
+ // pointer must point at an instance of MessageType, *not* a subclass (or
+ // the subclass must not override SerializeWithCachedSizes()).
+ template<typename MessageType>
+ static inline uint8* WriteGroupNoVirtualToArray(
+ field_number, const MessageType& value, output) INL;
+ template<typename MessageType>
+ static inline uint8* WriteMessageNoVirtualToArray(
+ field_number, const MessageType& value, output) INL;
+
+#undef output
+#undef input
+#undef INL
+
+#undef field_number
+
+ // Compute the byte size of a field. The XxSize() functions do NOT include
+ // the tag, so you must also call TagSize(). (This is because, for repeated
+ // fields, you should only call TagSize() once and multiply it by the element
+ // count, but you may have to call XxSize() for each individual element.)
+ static inline int Int32Size ( int32 value);
+ static inline int Int64Size ( int64 value);
+ static inline int UInt32Size (uint32 value);
+ static inline int UInt64Size (uint64 value);
+ static inline int SInt32Size ( int32 value);
+ static inline int SInt64Size ( int64 value);
+ static inline int EnumSize ( int value);
+
+ // These types always have the same size.
+ static const int kFixed32Size = 4;
+ static const int kFixed64Size = 8;
+ static const int kSFixed32Size = 4;
+ static const int kSFixed64Size = 8;
+ static const int kFloatSize = 4;
+ static const int kDoubleSize = 8;
+ static const int kBoolSize = 1;
+
+ static inline int StringSize(const string& value);
+ static inline int BytesSize (const string& value);
+
+ static inline int GroupSize (const MessageLite& value);
+ static inline int MessageSize(const MessageLite& value);
+
+ // Like above, but de-virtualize the call to ByteSize(). The
+ // pointer must point at an instance of MessageType, *not* a subclass (or
+ // the subclass must not override ByteSize()).
+ template<typename MessageType>
+ static inline int GroupSizeNoVirtual (const MessageType& value);
+ template<typename MessageType>
+ static inline int MessageSizeNoVirtual(const MessageType& value);
+
+ // Given the length of data, calculate the byte size of the data on the
+ // wire if we encode the data as a length delimited field.
+ static inline int LengthDelimitedSize(int length);
+
+ private:
+ // A helper method for the repeated primitive reader. This method has
+ // optimizations for primitive types that have fixed size on the wire, and
+ // can be read using potentially faster paths.
+ template <typename CType, enum FieldType DeclaredType>
+ static inline bool ReadRepeatedFixedSizePrimitive(
+ int tag_size,
+ uint32 tag,
+ google::protobuf::io::CodedInputStream* input,
+ RepeatedField<CType>* value) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ // Like ReadRepeatedFixedSizePrimitive but for packed primitive fields.
+ template <typename CType, enum FieldType DeclaredType>
+ static inline bool ReadPackedFixedSizePrimitive(
+ google::protobuf::io::CodedInputStream* input,
+ RepeatedField<CType>* value) GOOGLE_ATTRIBUTE_ALWAYS_INLINE;
+
+ static const CppType kFieldTypeToCppTypeMap[];
+ static const WireFormatLite::WireType kWireTypeForFieldType[];
+
+ GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(WireFormatLite);
+};
+
+// A class which deals with unknown values. The default implementation just
+// discards them. WireFormat defines a subclass which writes to an
+// UnknownFieldSet. This class is used by ExtensionSet::ParseField(), since
+// ExtensionSet is part of the lite library but UnknownFieldSet is not.
+class LIBPROTOBUF_EXPORT FieldSkipper {
+ public:
+ FieldSkipper() {}
+ virtual ~FieldSkipper() {}
+
+ // Skip a field whose tag has already been consumed.
+ virtual bool SkipField(io::CodedInputStream* input, uint32 tag);
+
+ // Skip an entire message or group, up to an end-group tag (which is consumed)
+ // or end-of-stream.
+ virtual bool SkipMessage(io::CodedInputStream* input);
+
+ // Deal with an already-parsed unrecognized enum value. The default
+ // implementation does nothing, but the UnknownFieldSet-based implementation
+ // saves it as an unknown varint.
+ virtual void SkipUnknownEnum(int field_number, int value);
+};
+
+// Subclass of FieldSkipper which saves skipped fields to a CodedOutputStream.
+
+class LIBPROTOBUF_EXPORT CodedOutputStreamFieldSkipper : public FieldSkipper {
+ public:
+ explicit CodedOutputStreamFieldSkipper(io::CodedOutputStream* unknown_fields)
+ : unknown_fields_(unknown_fields) {}
+ virtual ~CodedOutputStreamFieldSkipper() {}
+
+ // implements FieldSkipper -----------------------------------------
+ virtual bool SkipField(io::CodedInputStream* input, uint32 tag);
+ virtual bool SkipMessage(io::CodedInputStream* input);
+ virtual void SkipUnknownEnum(int field_number, int value);
+
+ protected:
+ io::CodedOutputStream* unknown_fields_;
+};
+
+
+// inline methods ====================================================
+
+inline WireFormatLite::CppType
+WireFormatLite::FieldTypeToCppType(FieldType type) {
+ return kFieldTypeToCppTypeMap[type];
+}
+
+inline uint32 WireFormatLite::MakeTag(int field_number, WireType type) {
+ return GOOGLE_PROTOBUF_WIRE_FORMAT_MAKE_TAG(field_number, type);
+}
+
+inline WireFormatLite::WireType WireFormatLite::GetTagWireType(uint32 tag) {
+ return static_cast<WireType>(tag & kTagTypeMask);
+}
+
+inline int WireFormatLite::GetTagFieldNumber(uint32 tag) {
+ return static_cast<int>(tag >> kTagTypeBits);
+}
+
+inline int WireFormatLite::TagSize(int field_number,
+ WireFormatLite::FieldType type) {
+ int result = io::CodedOutputStream::VarintSize32(
+ field_number << kTagTypeBits);
+ if (type == TYPE_GROUP) {
+ // Groups have both a start and an end tag.
+ return result * 2;
+ } else {
+ return result;
+ }
+}
+
+inline uint32 WireFormatLite::EncodeFloat(float value) {
+ union {float f; uint32 i;};
+ f = value;
+ return i;
+}
+
+inline float WireFormatLite::DecodeFloat(uint32 value) {
+ union {float f; uint32 i;};
+ i = value;
+ return f;
+}
+
+inline uint64 WireFormatLite::EncodeDouble(double value) {
+ union {double f; uint64 i;};
+ f = value;
+ return i;
+}
+
+inline double WireFormatLite::DecodeDouble(uint64 value) {
+ union {double f; uint64 i;};
+ i = value;
+ return f;
+}
+
+// ZigZag Transform: Encodes signed integers so that they can be
+// effectively used with varint encoding.
+//
+// varint operates on unsigned integers, encoding smaller numbers into
+// fewer bytes. If you try to use it on a signed integer, it will treat
+// this number as a very large unsigned integer, which means that even
+// small signed numbers like -1 will take the maximum number of bytes
+// (10) to encode. ZigZagEncode() maps signed integers to unsigned
+// in such a way that those with a small absolute value will have smaller
+// encoded values, making them appropriate for encoding using varint.
+//
+// int32 -> uint32
+// -------------------------
+// 0 -> 0
+// -1 -> 1
+// 1 -> 2
+// -2 -> 3
+// ... -> ...
+// 2147483647 -> 4294967294
+// -2147483648 -> 4294967295
+//
+// >> encode >>
+// << decode <<
+
+inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
+ // Note: the right-shift must be arithmetic
+ return (n << 1) ^ (n >> 31);
+}
+
+inline int32 WireFormatLite::ZigZagDecode32(uint32 n) {
+ return (n >> 1) ^ -static_cast<int32>(n & 1);
+}
+
+inline uint64 WireFormatLite::ZigZagEncode64(int64 n) {
+ // Note: the right-shift must be arithmetic
+ return (n << 1) ^ (n >> 63);
+}
+
+inline int64 WireFormatLite::ZigZagDecode64(uint64 n) {
+ return (n >> 1) ^ -static_cast<int64>(n & 1);
+}
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_H__
diff --git a/toolkit/components/protobuf/src/google/protobuf/wire_format_lite_inl.h b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite_inl.h
new file mode 100644
index 0000000000..feb2254043
--- /dev/null
+++ b/toolkit/components/protobuf/src/google/protobuf/wire_format_lite_inl.h
@@ -0,0 +1,860 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc. All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Author: kenton@google.com (Kenton Varda)
+// wink@google.com (Wink Saville) (refactored from wire_format.h)
+// Based on original Protocol Buffers design by
+// Sanjay Ghemawat, Jeff Dean, and others.
+
+#ifndef GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_INL_H__
+#define GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_INL_H__
+
+#ifdef _MSC_VER
+// This is required for min/max on VS2013 only.
+#include <algorithm>
+#endif
+
+#include <string>
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/message_lite.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/wire_format_lite.h>
+#include <google/protobuf/io/coded_stream.h>
+
+
+namespace google {
+namespace protobuf {
+namespace internal {
+
+// Implementation details of ReadPrimitive.
+
+template <>
+inline bool WireFormatLite::ReadPrimitive<int32, WireFormatLite::TYPE_INT32>(
+ io::CodedInputStream* input,
+ int32* value) {
+ uint32 temp;
+ if (!input->ReadVarint32(&temp)) return false;
+ *value = static_cast<int32>(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<int64, WireFormatLite::TYPE_INT64>(
+ io::CodedInputStream* input,
+ int64* value) {
+ uint64 temp;
+ if (!input->ReadVarint64(&temp)) return false;
+ *value = static_cast<int64>(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<uint32, WireFormatLite::TYPE_UINT32>(
+ io::CodedInputStream* input,
+ uint32* value) {
+ return input->ReadVarint32(value);
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<uint64, WireFormatLite::TYPE_UINT64>(
+ io::CodedInputStream* input,
+ uint64* value) {
+ return input->ReadVarint64(value);
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<int32, WireFormatLite::TYPE_SINT32>(
+ io::CodedInputStream* input,
+ int32* value) {
+ uint32 temp;
+ if (!input->ReadVarint32(&temp)) return false;
+ *value = ZigZagDecode32(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<int64, WireFormatLite::TYPE_SINT64>(
+ io::CodedInputStream* input,
+ int64* value) {
+ uint64 temp;
+ if (!input->ReadVarint64(&temp)) return false;
+ *value = ZigZagDecode64(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<uint32, WireFormatLite::TYPE_FIXED32>(
+ io::CodedInputStream* input,
+ uint32* value) {
+ return input->ReadLittleEndian32(value);
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<uint64, WireFormatLite::TYPE_FIXED64>(
+ io::CodedInputStream* input,
+ uint64* value) {
+ return input->ReadLittleEndian64(value);
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<int32, WireFormatLite::TYPE_SFIXED32>(
+ io::CodedInputStream* input,
+ int32* value) {
+ uint32 temp;
+ if (!input->ReadLittleEndian32(&temp)) return false;
+ *value = static_cast<int32>(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<int64, WireFormatLite::TYPE_SFIXED64>(
+ io::CodedInputStream* input,
+ int64* value) {
+ uint64 temp;
+ if (!input->ReadLittleEndian64(&temp)) return false;
+ *value = static_cast<int64>(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<float, WireFormatLite::TYPE_FLOAT>(
+ io::CodedInputStream* input,
+ float* value) {
+ uint32 temp;
+ if (!input->ReadLittleEndian32(&temp)) return false;
+ *value = DecodeFloat(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<double, WireFormatLite::TYPE_DOUBLE>(
+ io::CodedInputStream* input,
+ double* value) {
+ uint64 temp;
+ if (!input->ReadLittleEndian64(&temp)) return false;
+ *value = DecodeDouble(temp);
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<bool, WireFormatLite::TYPE_BOOL>(
+ io::CodedInputStream* input,
+ bool* value) {
+ uint64 temp;
+ if (!input->ReadVarint64(&temp)) return false;
+ *value = temp != 0;
+ return true;
+}
+template <>
+inline bool WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_ENUM>(
+ io::CodedInputStream* input,
+ int* value) {
+ uint32 temp;
+ if (!input->ReadVarint32(&temp)) return false;
+ *value = static_cast<int>(temp);
+ return true;
+}
+
+template <>
+inline const uint8* WireFormatLite::ReadPrimitiveFromArray<
+ uint32, WireFormatLite::TYPE_FIXED32>(
+ const uint8* buffer,
+ uint32* value) {
+ return io::CodedInputStream::ReadLittleEndian32FromArray(buffer, value);
+}
+template <>
+inline const uint8* WireFormatLite::ReadPrimitiveFromArray<
+ uint64, WireFormatLite::TYPE_FIXED64>(
+ const uint8* buffer,
+ uint64* value) {
+ return io::CodedInputStream::ReadLittleEndian64FromArray(buffer, value);
+}
+template <>
+inline const uint8* WireFormatLite::ReadPrimitiveFromArray<
+ int32, WireFormatLite::TYPE_SFIXED32>(
+ const uint8* buffer,
+ int32* value) {
+ uint32 temp;
+ buffer = io::CodedInputStream::ReadLittleEndian32FromArray(buffer, &temp);
+ *value = static_cast<int32>(temp);
+ return buffer;
+}
+template <>
+inline const uint8* WireFormatLite::ReadPrimitiveFromArray<
+ int64, WireFormatLite::TYPE_SFIXED64>(
+ const uint8* buffer,
+ int64* value) {
+ uint64 temp;
+ buffer = io::CodedInputStream::ReadLittleEndian64FromArray(buffer, &temp);
+ *value = static_cast<int64>(temp);
+ return buffer;
+}
+template <>
+inline const uint8* WireFormatLite::ReadPrimitiveFromArray<
+ float, WireFormatLite::TYPE_FLOAT>(
+ const uint8* buffer,
+ float* value) {
+ uint32 temp;
+ buffer = io::CodedInputStream::ReadLittleEndian32FromArray(buffer, &temp);
+ *value = DecodeFloat(temp);
+ return buffer;
+}
+template <>
+inline const uint8* WireFormatLite::ReadPrimitiveFromArray<
+ double, WireFormatLite::TYPE_DOUBLE>(
+ const uint8* buffer,
+ double* value) {
+ uint64 temp;
+ buffer = io::CodedInputStream::ReadLittleEndian64FromArray(buffer, &temp);
+ *value = DecodeDouble(temp);
+ return buffer;
+}
+
+template <typename CType, enum WireFormatLite::FieldType DeclaredType>
+inline bool WireFormatLite::ReadRepeatedPrimitive(
+ int, // tag_size, unused.
+ uint32 tag,
+ io::CodedInputStream* input,
+ RepeatedField<CType>* values) {
+ CType value;
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value)) return false;
+ values->Add(value);
+ int elements_already_reserved = values->Capacity() - values->size();
+ while (elements_already_reserved > 0 && input->ExpectTag(tag)) {
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value)) return false;
+ values->AddAlreadyReserved(value);
+ elements_already_reserved--;
+ }
+ return true;
+}
+
+template <typename CType, enum WireFormatLite::FieldType DeclaredType>
+inline bool WireFormatLite::ReadRepeatedFixedSizePrimitive(
+ int tag_size,
+ uint32 tag,
+ io::CodedInputStream* input,
+ RepeatedField<CType>* values) {
+ GOOGLE_DCHECK_EQ(UInt32Size(tag), tag_size);
+ CType value;
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value))
+ return false;
+ values->Add(value);
+
+ // For fixed size values, repeated values can be read more quickly by
+ // reading directly from a raw array.
+ //
+ // We can get a tight loop by only reading as many elements as can be
+ // added to the RepeatedField without having to do any resizing. Additionally,
+ // we only try to read as many elements as are available from the current
+ // buffer space. Doing so avoids having to perform boundary checks when
+ // reading the value: the maximum number of elements that can be read is
+ // known outside of the loop.
+ const void* void_pointer;
+ int size;
+ input->GetDirectBufferPointerInline(&void_pointer, &size);
+ if (size > 0) {
+ const uint8* buffer = reinterpret_cast<const uint8*>(void_pointer);
+ // The number of bytes each type occupies on the wire.
+ const int per_value_size = tag_size + sizeof(value);
+
+ int elements_available = min(values->Capacity() - values->size(),
+ size / per_value_size);
+ int num_read = 0;
+ while (num_read < elements_available &&
+ (buffer = io::CodedInputStream::ExpectTagFromArray(
+ buffer, tag)) != NULL) {
+ buffer = ReadPrimitiveFromArray<CType, DeclaredType>(buffer, &value);
+ values->AddAlreadyReserved(value);
+ ++num_read;
+ }
+ const int read_bytes = num_read * per_value_size;
+ if (read_bytes > 0) {
+ input->Skip(read_bytes);
+ }
+ }
+ return true;
+}
+
+// Specializations of ReadRepeatedPrimitive for the fixed size types, which use
+// the optimized code path.
+#define READ_REPEATED_FIXED_SIZE_PRIMITIVE(CPPTYPE, DECLARED_TYPE) \
+template <> \
+inline bool WireFormatLite::ReadRepeatedPrimitive< \
+ CPPTYPE, WireFormatLite::DECLARED_TYPE>( \
+ int tag_size, \
+ uint32 tag, \
+ io::CodedInputStream* input, \
+ RepeatedField<CPPTYPE>* values) { \
+ return ReadRepeatedFixedSizePrimitive< \
+ CPPTYPE, WireFormatLite::DECLARED_TYPE>( \
+ tag_size, tag, input, values); \
+}
+
+READ_REPEATED_FIXED_SIZE_PRIMITIVE(uint32, TYPE_FIXED32)
+READ_REPEATED_FIXED_SIZE_PRIMITIVE(uint64, TYPE_FIXED64)
+READ_REPEATED_FIXED_SIZE_PRIMITIVE(int32, TYPE_SFIXED32)
+READ_REPEATED_FIXED_SIZE_PRIMITIVE(int64, TYPE_SFIXED64)
+READ_REPEATED_FIXED_SIZE_PRIMITIVE(float, TYPE_FLOAT)
+READ_REPEATED_FIXED_SIZE_PRIMITIVE(double, TYPE_DOUBLE)
+
+#undef READ_REPEATED_FIXED_SIZE_PRIMITIVE
+
+template <typename CType, enum WireFormatLite::FieldType DeclaredType>
+bool WireFormatLite::ReadRepeatedPrimitiveNoInline(
+ int tag_size,
+ uint32 tag,
+ io::CodedInputStream* input,
+ RepeatedField<CType>* value) {
+ return ReadRepeatedPrimitive<CType, DeclaredType>(
+ tag_size, tag, input, value);
+}
+
+template <typename CType, enum WireFormatLite::FieldType DeclaredType>
+inline bool WireFormatLite::ReadPackedPrimitive(io::CodedInputStream* input,
+ RepeatedField<CType>* values) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ io::CodedInputStream::Limit limit = input->PushLimit(length);
+ while (input->BytesUntilLimit() > 0) {
+ CType value;
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value)) return false;
+ values->Add(value);
+ }
+ input->PopLimit(limit);
+ return true;
+}
+
+template <typename CType, enum WireFormatLite::FieldType DeclaredType>
+inline bool WireFormatLite::ReadPackedFixedSizePrimitive(
+ io::CodedInputStream* input, RepeatedField<CType>* values) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ const uint32 old_entries = values->size();
+ const uint32 new_entries = length / sizeof(CType);
+ const uint32 new_bytes = new_entries * sizeof(CType);
+ if (new_bytes != length) return false;
+ // We would *like* to pre-allocate the buffer to write into (for
+ // speed), but *must* avoid performing a very large allocation due
+ // to a malicious user-supplied "length" above. So we have a fast
+ // path that pre-allocates when the "length" is less than a bound.
+ // We determine the bound by calling BytesUntilTotalBytesLimit() and
+ // BytesUntilLimit(). These return -1 to mean "no limit set".
+ // There are four cases:
+ // TotalBytesLimit Limit
+ // -1 -1 Use slow path.
+ // -1 >= 0 Use fast path if length <= Limit.
+ // >= 0 -1 Use slow path.
+ // >= 0 >= 0 Use fast path if length <= min(both limits).
+ int64 bytes_limit = input->BytesUntilTotalBytesLimit();
+ if (bytes_limit == -1) {
+ bytes_limit = input->BytesUntilLimit();
+ } else {
+ bytes_limit =
+ min(bytes_limit, static_cast<int64>(input->BytesUntilLimit()));
+ }
+ if (bytes_limit >= new_bytes) {
+ // Fast-path that pre-allocates *values to the final size.
+#if defined(PROTOBUF_LITTLE_ENDIAN)
+ values->Resize(old_entries + new_entries, 0);
+ // values->mutable_data() may change after Resize(), so do this after:
+ void* dest = reinterpret_cast<void*>(values->mutable_data() + old_entries);
+ if (!input->ReadRaw(dest, new_bytes)) {
+ values->Truncate(old_entries);
+ return false;
+ }
+#else
+ values->Reserve(old_entries + new_entries);
+ CType value;
+ for (uint32 i = 0; i < new_entries; ++i) {
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value)) return false;
+ values->AddAlreadyReserved(value);
+ }
+#endif
+ } else {
+ // This is the slow-path case where "length" may be too large to
+ // safely allocate. We read as much as we can into *values
+ // without pre-allocating "length" bytes.
+ CType value;
+ for (uint32 i = 0; i < new_entries; ++i) {
+ if (!ReadPrimitive<CType, DeclaredType>(input, &value)) return false;
+ values->Add(value);
+ }
+ }
+ return true;
+}
+
+// Specializations of ReadPackedPrimitive for the fixed size types, which use
+// an optimized code path.
+#define READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(CPPTYPE, DECLARED_TYPE) \
+template <> \
+inline bool WireFormatLite::ReadPackedPrimitive< \
+ CPPTYPE, WireFormatLite::DECLARED_TYPE>( \
+ io::CodedInputStream* input, \
+ RepeatedField<CPPTYPE>* values) { \
+ return ReadPackedFixedSizePrimitive< \
+ CPPTYPE, WireFormatLite::DECLARED_TYPE>(input, values); \
+}
+
+READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(uint32, TYPE_FIXED32);
+READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(uint64, TYPE_FIXED64);
+READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(int32, TYPE_SFIXED32);
+READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(int64, TYPE_SFIXED64);
+READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(float, TYPE_FLOAT);
+READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE(double, TYPE_DOUBLE);
+
+#undef READ_REPEATED_PACKED_FIXED_SIZE_PRIMITIVE
+
+template <typename CType, enum WireFormatLite::FieldType DeclaredType>
+bool WireFormatLite::ReadPackedPrimitiveNoInline(io::CodedInputStream* input,
+ RepeatedField<CType>* values) {
+ return ReadPackedPrimitive<CType, DeclaredType>(input, values);
+}
+
+
+inline bool WireFormatLite::ReadGroup(int field_number,
+ io::CodedInputStream* input,
+ MessageLite* value) {
+ if (!input->IncrementRecursionDepth()) return false;
+ if (!value->MergePartialFromCodedStream(input)) return false;
+ input->DecrementRecursionDepth();
+ // Make sure the last thing read was an end tag for this group.
+ if (!input->LastTagWas(MakeTag(field_number, WIRETYPE_END_GROUP))) {
+ return false;
+ }
+ return true;
+}
+inline bool WireFormatLite::ReadMessage(io::CodedInputStream* input,
+ MessageLite* value) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (!input->IncrementRecursionDepth()) return false;
+ io::CodedInputStream::Limit limit = input->PushLimit(length);
+ if (!value->MergePartialFromCodedStream(input)) return false;
+ // Make sure that parsing stopped when the limit was hit, not at an endgroup
+ // tag.
+ if (!input->ConsumedEntireMessage()) return false;
+ input->PopLimit(limit);
+ input->DecrementRecursionDepth();
+ return true;
+}
+
+// We name the template parameter something long and extremely unlikely to occur
+// elsewhere because a *qualified* member access expression designed to avoid
+// virtual dispatch, C++03 [basic.lookup.classref] 3.4.5/4 requires that the
+// name of the qualifying class to be looked up both in the context of the full
+// expression (finding the template parameter) and in the context of the object
+// whose member we are accessing. This could potentially find a nested type
+// within that object. The standard goes on to require these names to refer to
+// the same entity, which this collision would violate. The lack of a safe way
+// to avoid this collision appears to be a defect in the standard, but until it
+// is corrected, we choose the name to avoid accidental collisions.
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline bool WireFormatLite::ReadGroupNoVirtual(
+ int field_number, io::CodedInputStream* input,
+ MessageType_WorkAroundCppLookupDefect* value) {
+ if (!input->IncrementRecursionDepth()) return false;
+ if (!value->
+ MessageType_WorkAroundCppLookupDefect::MergePartialFromCodedStream(input))
+ return false;
+ input->DecrementRecursionDepth();
+ // Make sure the last thing read was an end tag for this group.
+ if (!input->LastTagWas(MakeTag(field_number, WIRETYPE_END_GROUP))) {
+ return false;
+ }
+ return true;
+}
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline bool WireFormatLite::ReadMessageNoVirtual(
+ io::CodedInputStream* input, MessageType_WorkAroundCppLookupDefect* value) {
+ uint32 length;
+ if (!input->ReadVarint32(&length)) return false;
+ if (!input->IncrementRecursionDepth()) return false;
+ io::CodedInputStream::Limit limit = input->PushLimit(length);
+ if (!value->
+ MessageType_WorkAroundCppLookupDefect::MergePartialFromCodedStream(input))
+ return false;
+ // Make sure that parsing stopped when the limit was hit, not at an endgroup
+ // tag.
+ if (!input->ConsumedEntireMessage()) return false;
+ input->PopLimit(limit);
+ input->DecrementRecursionDepth();
+ return true;
+}
+
+// ===================================================================
+
+inline void WireFormatLite::WriteTag(int field_number, WireType type,
+ io::CodedOutputStream* output) {
+ output->WriteTag(MakeTag(field_number, type));
+}
+
+inline void WireFormatLite::WriteInt32NoTag(int32 value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint32SignExtended(value);
+}
+inline void WireFormatLite::WriteInt64NoTag(int64 value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint64(static_cast<uint64>(value));
+}
+inline void WireFormatLite::WriteUInt32NoTag(uint32 value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint32(value);
+}
+inline void WireFormatLite::WriteUInt64NoTag(uint64 value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint64(value);
+}
+inline void WireFormatLite::WriteSInt32NoTag(int32 value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint32(ZigZagEncode32(value));
+}
+inline void WireFormatLite::WriteSInt64NoTag(int64 value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint64(ZigZagEncode64(value));
+}
+inline void WireFormatLite::WriteFixed32NoTag(uint32 value,
+ io::CodedOutputStream* output) {
+ output->WriteLittleEndian32(value);
+}
+inline void WireFormatLite::WriteFixed64NoTag(uint64 value,
+ io::CodedOutputStream* output) {
+ output->WriteLittleEndian64(value);
+}
+inline void WireFormatLite::WriteSFixed32NoTag(int32 value,
+ io::CodedOutputStream* output) {
+ output->WriteLittleEndian32(static_cast<uint32>(value));
+}
+inline void WireFormatLite::WriteSFixed64NoTag(int64 value,
+ io::CodedOutputStream* output) {
+ output->WriteLittleEndian64(static_cast<uint64>(value));
+}
+inline void WireFormatLite::WriteFloatNoTag(float value,
+ io::CodedOutputStream* output) {
+ output->WriteLittleEndian32(EncodeFloat(value));
+}
+inline void WireFormatLite::WriteDoubleNoTag(double value,
+ io::CodedOutputStream* output) {
+ output->WriteLittleEndian64(EncodeDouble(value));
+}
+inline void WireFormatLite::WriteBoolNoTag(bool value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint32(value ? 1 : 0);
+}
+inline void WireFormatLite::WriteEnumNoTag(int value,
+ io::CodedOutputStream* output) {
+ output->WriteVarint32SignExtended(value);
+}
+
+// See comment on ReadGroupNoVirtual to understand the need for this template
+// parameter name.
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline void WireFormatLite::WriteGroupNoVirtual(
+ int field_number, const MessageType_WorkAroundCppLookupDefect& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_START_GROUP, output);
+ value.MessageType_WorkAroundCppLookupDefect::SerializeWithCachedSizes(output);
+ WriteTag(field_number, WIRETYPE_END_GROUP, output);
+}
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline void WireFormatLite::WriteMessageNoVirtual(
+ int field_number, const MessageType_WorkAroundCppLookupDefect& value,
+ io::CodedOutputStream* output) {
+ WriteTag(field_number, WIRETYPE_LENGTH_DELIMITED, output);
+ output->WriteVarint32(
+ value.MessageType_WorkAroundCppLookupDefect::GetCachedSize());
+ value.MessageType_WorkAroundCppLookupDefect::SerializeWithCachedSizes(output);
+}
+
+// ===================================================================
+
+inline uint8* WireFormatLite::WriteTagToArray(int field_number,
+ WireType type,
+ uint8* target) {
+ return io::CodedOutputStream::WriteTagToArray(MakeTag(field_number, type),
+ target);
+}
+
+inline uint8* WireFormatLite::WriteInt32NoTagToArray(int32 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint32SignExtendedToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteInt64NoTagToArray(int64 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint64ToArray(
+ static_cast<uint64>(value), target);
+}
+inline uint8* WireFormatLite::WriteUInt32NoTagToArray(uint32 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint32ToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteUInt64NoTagToArray(uint64 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint64ToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteSInt32NoTagToArray(int32 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint32ToArray(ZigZagEncode32(value),
+ target);
+}
+inline uint8* WireFormatLite::WriteSInt64NoTagToArray(int64 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint64ToArray(ZigZagEncode64(value),
+ target);
+}
+inline uint8* WireFormatLite::WriteFixed32NoTagToArray(uint32 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteLittleEndian32ToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteFixed64NoTagToArray(uint64 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteLittleEndian64ToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteLittleEndian32ToArray(
+ static_cast<uint32>(value), target);
+}
+inline uint8* WireFormatLite::WriteSFixed64NoTagToArray(int64 value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteLittleEndian64ToArray(
+ static_cast<uint64>(value), target);
+}
+inline uint8* WireFormatLite::WriteFloatNoTagToArray(float value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteLittleEndian32ToArray(EncodeFloat(value),
+ target);
+}
+inline uint8* WireFormatLite::WriteDoubleNoTagToArray(double value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteLittleEndian64ToArray(EncodeDouble(value),
+ target);
+}
+inline uint8* WireFormatLite::WriteBoolNoTagToArray(bool value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint32ToArray(value ? 1 : 0, target);
+}
+inline uint8* WireFormatLite::WriteEnumNoTagToArray(int value,
+ uint8* target) {
+ return io::CodedOutputStream::WriteVarint32SignExtendedToArray(value, target);
+}
+
+inline uint8* WireFormatLite::WriteInt32ToArray(int field_number,
+ int32 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteInt32NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteInt64ToArray(int field_number,
+ int64 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteInt64NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteUInt32ToArray(int field_number,
+ uint32 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteUInt32NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteUInt64ToArray(int field_number,
+ uint64 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteUInt64NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteSInt32ToArray(int field_number,
+ int32 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteSInt32NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteSInt64ToArray(int field_number,
+ int64 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteSInt64NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteFixed32ToArray(int field_number,
+ uint32 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
+ return WriteFixed32NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteFixed64ToArray(int field_number,
+ uint64 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_FIXED64, target);
+ return WriteFixed64NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
+ int32 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
+ return WriteSFixed32NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteSFixed64ToArray(int field_number,
+ int64 value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_FIXED64, target);
+ return WriteSFixed64NoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteFloatToArray(int field_number,
+ float value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
+ return WriteFloatNoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteDoubleToArray(int field_number,
+ double value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_FIXED64, target);
+ return WriteDoubleNoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteBoolToArray(int field_number,
+ bool value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteBoolNoTagToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteEnumToArray(int field_number,
+ int value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
+ return WriteEnumNoTagToArray(value, target);
+}
+
+inline uint8* WireFormatLite::WriteStringToArray(int field_number,
+ const string& value,
+ uint8* target) {
+ // String is for UTF-8 text only
+ // WARNING: In wire_format.cc, both strings and bytes are handled by
+ // WriteString() to avoid code duplication. If the implementations become
+ // different, you will need to update that usage.
+ target = WriteTagToArray(field_number, WIRETYPE_LENGTH_DELIMITED, target);
+ return io::CodedOutputStream::WriteStringWithSizeToArray(value, target);
+}
+inline uint8* WireFormatLite::WriteBytesToArray(int field_number,
+ const string& value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_LENGTH_DELIMITED, target);
+ return io::CodedOutputStream::WriteStringWithSizeToArray(value, target);
+}
+
+
+inline uint8* WireFormatLite::WriteGroupToArray(int field_number,
+ const MessageLite& value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_START_GROUP, target);
+ target = value.SerializeWithCachedSizesToArray(target);
+ return WriteTagToArray(field_number, WIRETYPE_END_GROUP, target);
+}
+inline uint8* WireFormatLite::WriteMessageToArray(int field_number,
+ const MessageLite& value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_LENGTH_DELIMITED, target);
+ target = io::CodedOutputStream::WriteVarint32ToArray(
+ value.GetCachedSize(), target);
+ return value.SerializeWithCachedSizesToArray(target);
+}
+
+// See comment on ReadGroupNoVirtual to understand the need for this template
+// parameter name.
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline uint8* WireFormatLite::WriteGroupNoVirtualToArray(
+ int field_number, const MessageType_WorkAroundCppLookupDefect& value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_START_GROUP, target);
+ target = value.MessageType_WorkAroundCppLookupDefect
+ ::SerializeWithCachedSizesToArray(target);
+ return WriteTagToArray(field_number, WIRETYPE_END_GROUP, target);
+}
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline uint8* WireFormatLite::WriteMessageNoVirtualToArray(
+ int field_number, const MessageType_WorkAroundCppLookupDefect& value,
+ uint8* target) {
+ target = WriteTagToArray(field_number, WIRETYPE_LENGTH_DELIMITED, target);
+ target = io::CodedOutputStream::WriteVarint32ToArray(
+ value.MessageType_WorkAroundCppLookupDefect::GetCachedSize(), target);
+ return value.MessageType_WorkAroundCppLookupDefect
+ ::SerializeWithCachedSizesToArray(target);
+}
+
+// ===================================================================
+
+inline int WireFormatLite::Int32Size(int32 value) {
+ return io::CodedOutputStream::VarintSize32SignExtended(value);
+}
+inline int WireFormatLite::Int64Size(int64 value) {
+ return io::CodedOutputStream::VarintSize64(static_cast<uint64>(value));
+}
+inline int WireFormatLite::UInt32Size(uint32 value) {
+ return io::CodedOutputStream::VarintSize32(value);
+}
+inline int WireFormatLite::UInt64Size(uint64 value) {
+ return io::CodedOutputStream::VarintSize64(value);
+}
+inline int WireFormatLite::SInt32Size(int32 value) {
+ return io::CodedOutputStream::VarintSize32(ZigZagEncode32(value));
+}
+inline int WireFormatLite::SInt64Size(int64 value) {
+ return io::CodedOutputStream::VarintSize64(ZigZagEncode64(value));
+}
+inline int WireFormatLite::EnumSize(int value) {
+ return io::CodedOutputStream::VarintSize32SignExtended(value);
+}
+
+inline int WireFormatLite::StringSize(const string& value) {
+ return io::CodedOutputStream::VarintSize32(value.size()) +
+ value.size();
+}
+inline int WireFormatLite::BytesSize(const string& value) {
+ return io::CodedOutputStream::VarintSize32(value.size()) +
+ value.size();
+}
+
+
+inline int WireFormatLite::GroupSize(const MessageLite& value) {
+ return value.ByteSize();
+}
+inline int WireFormatLite::MessageSize(const MessageLite& value) {
+ return LengthDelimitedSize(value.ByteSize());
+}
+
+// See comment on ReadGroupNoVirtual to understand the need for this template
+// parameter name.
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline int WireFormatLite::GroupSizeNoVirtual(
+ const MessageType_WorkAroundCppLookupDefect& value) {
+ return value.MessageType_WorkAroundCppLookupDefect::ByteSize();
+}
+template<typename MessageType_WorkAroundCppLookupDefect>
+inline int WireFormatLite::MessageSizeNoVirtual(
+ const MessageType_WorkAroundCppLookupDefect& value) {
+ return LengthDelimitedSize(
+ value.MessageType_WorkAroundCppLookupDefect::ByteSize());
+}
+
+inline int WireFormatLite::LengthDelimitedSize(int length) {
+ return io::CodedOutputStream::VarintSize32(length) + length;
+}
+
+} // namespace internal
+} // namespace protobuf
+
+} // namespace google
+#endif // GOOGLE_PROTOBUF_WIRE_FORMAT_LITE_INL_H__
diff --git a/toolkit/components/protobuf/upgrade_protobuf.sh b/toolkit/components/protobuf/upgrade_protobuf.sh
new file mode 100755
index 0000000000..bdaa4ce633
--- /dev/null
+++ b/toolkit/components/protobuf/upgrade_protobuf.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+
+set -e
+
+usage() {
+ echo "Usage: upgrade_protobuf.sh path/to/protobuf"
+ echo
+ echo " Upgrades mozilla-central's copy of the protobuf library."
+ echo
+ echo " Get a protobuf release from here:"
+ echo " https://github.com/google/protobuf/releases"
+}
+
+if [[ "$#" -ne 1 ]]; then
+ usage
+ exit 1
+fi
+
+PROTOBUF_LIB_PATH=$1
+
+if [[ ! -d "$PROTOBUF_LIB_PATH" ]]; then
+ echo No such directory: $PROTOBUF_LIB_PATH
+ echo
+ usage
+ exit 1
+fi
+
+realpath() {
+ if [[ $1 = /* ]]; then
+ echo "$1"
+ else
+ echo "$PWD/${1#./}"
+ fi
+}
+
+PROTOBUF_LIB_PATH=$(realpath $PROTOBUF_LIB_PATH)
+
+cd $(dirname $0)
+
+# Remove the old protobuf sources.
+rm -rf src/google/*
+
+# Add all the new protobuf sources.
+cp -r $PROTOBUF_LIB_PATH/src/google/* src/google/
+
+# Remove compiler sources.
+rm -rf src/google/protobuf/compiler
+
+# Remove test files.
+find src/google -name '*test*' | xargs rm -rf
+
+# Remove protobuf's build files.
+find src/google/ -name '.deps' | xargs rm -rf
+find src/google/ -name '.dirstamp' | xargs rm -rf
+rm -rf src/google/protobuf/SEBS
+
+# Apply custom changes for building as part of mozilla-central.
+
+cd ../../.. # Top level.
+
+echo
+echo Applying custom changes for mozilla-central. If this fails, you need to
+echo edit the 'toolkit/components/protobuf/src/google/*' sources manually and
+echo update the 'toolkit/components/protobuf/m-c-changes.patch' patch file
+echo accordingly.
+echo
+
+patch -p1 < toolkit/components/protobuf/m-c-changes.patch
+
+echo
+echo Successfully upgraded the protobuf lib!
diff --git a/toolkit/components/reader/.eslintrc.js b/toolkit/components/reader/.eslintrc.js
new file mode 100644
index 0000000000..1c09e0bf79
--- /dev/null
+++ b/toolkit/components/reader/.eslintrc.js
@@ -0,0 +1,199 @@
+"use strict";
+
+module.exports = {
+ "rules": {
+ // Braces only needed for multi-line arrow function blocks
+ // "arrow-body-style": ["error", "as-needed"],
+
+ // Require spacing around =>
+ // "arrow-spacing": "error",
+
+ // Always require spacing around a single line block
+ // "block-spacing": "warn",
+
+ // No newline before open brace for a block
+ "brace-style": "error",
+
+ // No space before always a space after a comma
+ "comma-spacing": ["error", {"before": false, "after": true}],
+
+ // Commas at the end of the line not the start
+ // "comma-style": "error",
+
+ // Don't require spaces around computed properties
+ // "computed-property-spacing": ["error", "never"],
+
+ // Functions must always return something or nothing
+ "consistent-return": "error",
+
+ // Require braces around blocks that start a new line
+ // Note that this rule is likely to be overridden on a per-directory basis
+ // very frequently.
+ // "curly": ["error", "multi-line"],
+
+ // Always require a trailing EOL
+ "eol-last": "error",
+
+ // Require function* name()
+ // "generator-star-spacing": ["error", {"before": false, "after": true}],
+
+ // Two space indent
+ "indent": ["error", 2, { "SwitchCase": 1 }],
+
+ // Space after colon not before in property declarations
+ "key-spacing": ["error", { "beforeColon": false, "afterColon": true, "mode": "minimum" }],
+
+ // Unix linebreaks
+ "linebreak-style": ["error", "unix"],
+
+ // Always require parenthesis for new calls
+ "new-parens": "error",
+
+ // Use [] instead of Array()
+ // "no-array-constructor": "error",
+
+ // No duplicate arguments in function declarations
+ "no-dupe-args": "error",
+
+ // No duplicate keys in object declarations
+ "no-dupe-keys": "error",
+
+ // No duplicate cases in switch statements
+ "no-duplicate-case": "error",
+
+ // No labels
+ "no-labels": "error",
+
+ // If an if block ends with a return no need for an else block
+ "no-else-return": "error",
+
+ // No empty statements
+ "no-empty": "error",
+
+ // No empty character classes in regex
+ "no-empty-character-class": "error",
+
+ // Disallow empty destructuring
+ "no-empty-pattern": "error",
+
+ // No assiging to exception variable
+ // "no-ex-assign": "error",
+
+ // No using !! where casting to boolean is already happening
+ // "no-extra-boolean-cast": "error",
+
+ // No double semicolon
+ "no-extra-semi": "error",
+
+ // No overwriting defined functions
+ "no-func-assign": "error",
+
+ // Declarations in Program or Function Body
+ "no-inner-declarations": "error",
+
+ // No invalid regular expresions
+ "no-invalid-regexp": "error",
+
+ // No odd whitespace characters
+ "no-irregular-whitespace": "error",
+
+ // No single if block inside an else block
+ "no-lonely-if": "error",
+
+ // No mixing spaces and tabs in indent
+ "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+
+ // No unnecessary spacing
+ "no-multi-spaces": ["error", { exceptions: { "AssignmentExpression": true, "VariableDeclarator": true, "ArrayExpression": true, "ObjectExpression": true } }],
+
+ // No reassigning native JS objects
+ "no-native-reassign": "error",
+
+ // No (!foo in bar)
+ "no-negated-in-lhs": "error",
+
+ // Nested ternary statements are confusing
+ "no-nested-ternary": "error",
+
+ // Use {} instead of new Object()
+ // "no-new-object": "error",
+
+ // No Math() or JSON()
+ "no-obj-calls": "error",
+
+ // No octal literals
+ "no-octal": "error",
+
+ // No redeclaring variables
+ "no-redeclare": "error",
+
+ // No unnecessary comparisons
+ "no-self-compare": "error",
+
+ // No declaring variables from an outer scope
+ "no-shadow": "error",
+
+ // No declaring variables that hide things like arguments
+ "no-shadow-restricted-names": "error",
+
+ // No spaces between function name and parentheses
+ "no-spaced-func": "error",
+
+ // No trailing whitespace
+ "no-trailing-spaces": "error",
+
+ // No using undeclared variables
+ // "no-undef": "error",
+
+ // Error on newline where a semicolon is needed
+ "no-unexpected-multiline": "error",
+
+ // No unreachable statements
+ "no-unreachable": "error",
+
+ // No expressions where a statement is expected
+ // "no-unused-expressions": "error",
+
+ // No declaring variables that are never used
+ "no-unused-vars": ["error", {"vars": "all", "args": "none"}],
+
+ // No using variables before defined
+ // "no-use-before-define": ["error", "nofunc"],
+
+ // No using with
+ "no-with": "error",
+
+ // Always require semicolon at end of statement
+ "semi": ["error", "always"],
+
+ // Require space after keywords
+ "keyword-spacing": "error",
+
+ // Require space before blocks
+ "space-before-blocks": "error",
+
+ // Never use spaces before function parentheses
+ // "space-before-function-paren": ["error", { "anonymous": "always", "named": "never" }],
+
+ // Require spaces before finally, catch, etc.
+ // "space-before-keywords": ["error", "always"],
+
+ // No space padding in parentheses
+ // "space-in-parens": ["error", "never"],
+
+ // Require spaces around operators
+ // "space-infix-ops": "error",
+
+ // Require spaces after return, throw and case
+ // "space-return-throw-case": "error",
+
+ // ++ and -- should not need spacing
+ // "space-unary-ops": ["error", { "words": true, "nonwords": false }],
+
+ // No comparisons to NaN
+ "use-isnan": "error",
+
+ // Only check typeof against valid results
+ "valid-typeof": "error",
+ },
+}
diff --git a/toolkit/components/reader/AboutReader.jsm b/toolkit/components/reader/AboutReader.jsm
new file mode 100644
index 0000000000..1fb9db1233
--- /dev/null
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -0,0 +1,997 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AboutReader" ];
+
+Cu.import("resource://gre/modules/ReaderMode.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncPrefs", "resource://gre/modules/AsyncPrefs.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NarrateControls", "resource://gre/modules/narrate/NarrateControls.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+var gStrings = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
+
+var AboutReader = function(mm, win, articlePromise) {
+ let url = this._getOriginalUrl(win);
+ if (!(url.startsWith("http://") || url.startsWith("https://"))) {
+ let errorMsg = "Only http:// and https:// URLs can be loaded in about:reader.";
+ if (Services.prefs.getBoolPref("reader.errors.includeURLs"))
+ errorMsg += " Tried to load: " + url + ".";
+ Cu.reportError(errorMsg);
+ win.location.href = "about:blank";
+ return;
+ }
+
+ let doc = win.document;
+
+ this._mm = mm;
+ this._mm.addMessageListener("Reader:CloseDropdown", this);
+ this._mm.addMessageListener("Reader:AddButton", this);
+ this._mm.addMessageListener("Reader:RemoveButton", this);
+ this._mm.addMessageListener("Reader:GetStoredArticleData", this);
+
+ this._docRef = Cu.getWeakReference(doc);
+ this._winRef = Cu.getWeakReference(win);
+ this._innerWindowId = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+
+ this._article = null;
+
+ if (articlePromise) {
+ this._articlePromise = articlePromise;
+ }
+
+ this._headerElementRef = Cu.getWeakReference(doc.getElementById("reader-header"));
+ this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
+ this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
+ this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
+ this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content"));
+ this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
+ this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
+
+ this._scrollOffset = win.pageYOffset;
+
+ doc.addEventListener("click", this, false);
+
+ win.addEventListener("pagehide", this, false);
+ win.addEventListener("scroll", this, false);
+ win.addEventListener("resize", this, false);
+
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+
+ doc.addEventListener("visibilitychange", this, false);
+
+ this._setupStyleDropdown();
+ this._setupButton("close-button", this._onReaderClose.bind(this), "aboutReader.toolbar.close");
+
+ const gIsFirefoxDesktop = Services.appinfo.ID == "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+ if (gIsFirefoxDesktop) {
+ // we're ready for any external setup, send a signal for that.
+ this._mm.sendAsyncMessage("Reader:OnSetup");
+ }
+
+ let colorSchemeValues = JSON.parse(Services.prefs.getCharPref("reader.color_scheme.values"));
+ let colorSchemeOptions = colorSchemeValues.map((value) => {
+ return { name: gStrings.GetStringFromName("aboutReader.colorScheme." + value),
+ value: value,
+ itemClass: value + "-button" };
+ });
+
+ let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
+ this._setupSegmentedButton("color-scheme-buttons", colorSchemeOptions, colorScheme, this._setColorSchemePref.bind(this));
+ this._setColorSchemePref(colorScheme);
+
+ let fontTypeSample = gStrings.GetStringFromName("aboutReader.fontTypeSample");
+ let fontTypeOptions = [
+ { name: fontTypeSample,
+ description: gStrings.GetStringFromName("aboutReader.fontType.sans-serif"),
+ value: "sans-serif",
+ itemClass: "sans-serif-button"
+ },
+ { name: fontTypeSample,
+ description: gStrings.GetStringFromName("aboutReader.fontType.serif"),
+ value: "serif",
+ itemClass: "serif-button" },
+ ];
+
+ let fontType = Services.prefs.getCharPref("reader.font_type");
+ this._setupSegmentedButton("font-type-buttons", fontTypeOptions, fontType, this._setFontType.bind(this));
+ this._setFontType(fontType);
+
+ this._setupFontSizeButtons();
+
+ this._setupContentWidthButtons();
+
+ this._setupLineHeightButtons();
+
+ if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
+ new NarrateControls(mm, win);
+ }
+
+ this._loadArticle();
+};
+
+AboutReader.prototype = {
+ _BLOCK_IMAGES_SELECTOR: ".content p > img:only-child, " +
+ ".content p > a:only-child > img:only-child, " +
+ ".content .wp-caption img, " +
+ ".content figure img",
+
+ get _doc() {
+ return this._docRef.get();
+ },
+
+ get _win() {
+ return this._winRef.get();
+ },
+
+ get _headerElement() {
+ return this._headerElementRef.get();
+ },
+
+ get _domainElement() {
+ return this._domainElementRef.get();
+ },
+
+ get _titleElement() {
+ return this._titleElementRef.get();
+ },
+
+ get _creditsElement() {
+ return this._creditsElementRef.get();
+ },
+
+ get _contentElement() {
+ return this._contentElementRef.get();
+ },
+
+ get _toolbarElement() {
+ return this._toolbarElementRef.get();
+ },
+
+ get _messageElement() {
+ return this._messageElementRef.get();
+ },
+
+ get _isToolbarVertical() {
+ if (this._toolbarVertical !== undefined) {
+ return this._toolbarVertical;
+ }
+ return this._toolbarVertical = Services.prefs.getBoolPref("reader.toolbar.vertical");
+ },
+
+ // Provides unique view Id.
+ get viewId() {
+ let _viewId = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ Object.defineProperty(this, "viewId", { value: _viewId });
+
+ return _viewId;
+ },
+
+ receiveMessage: function (message) {
+ switch (message.name) {
+ // Triggered by Android user pressing BACK while the banner font-dropdown is open.
+ case "Reader:CloseDropdown": {
+ // Just close it.
+ this._closeDropdowns();
+ break;
+ }
+
+ case "Reader:AddButton": {
+ if (message.data.id && message.data.image &&
+ !this._doc.getElementById(message.data.id)) {
+ let btn = this._doc.createElement("button");
+ btn.setAttribute("class", "button");
+ btn.setAttribute("style", "background-image: url('" + message.data.image + "')");
+ btn.setAttribute("id", message.data.id);
+ if (message.data.title)
+ btn.setAttribute("title", message.data.title);
+ if (message.data.text)
+ btn.textContent = message.data.text;
+ let tb = this._doc.getElementById("reader-toolbar");
+ tb.appendChild(btn);
+ this._setupButton(message.data.id, button => {
+ this._mm.sendAsyncMessage("Reader:Clicked-" + button.getAttribute("id"), { article: this._article });
+ });
+ }
+ break;
+ }
+ case "Reader:RemoveButton": {
+ if (message.data.id) {
+ let btn = this._doc.getElementById(message.data.id);
+ if (btn)
+ btn.remove();
+ }
+ break;
+ }
+ case "Reader:GetStoredArticleData": {
+ this._mm.sendAsyncMessage("Reader:StoredArticleData", { article: this._article });
+ }
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+
+ switch (aEvent.type) {
+ case "click":
+ let target = aEvent.target;
+ if (target.classList.contains('dropdown-toggle')) {
+ this._toggleDropdownClicked(aEvent);
+ } else if (!target.closest('.dropdown-popup')) {
+ this._closeDropdowns();
+ }
+ break;
+ case "scroll":
+ this._closeDropdowns(true);
+ let isScrollingUp = this._scrollOffset > aEvent.pageY;
+ this._setSystemUIVisibility(isScrollingUp);
+ this._scrollOffset = aEvent.pageY;
+ break;
+ case "resize":
+ this._updateImageMargins();
+ if (this._isToolbarVertical) {
+ this._win.setTimeout(() => {
+ for (let dropdown of this._doc.querySelectorAll('.dropdown.open')) {
+ this._updatePopupPosition(dropdown);
+ }
+ }, 0);
+ }
+ break;
+
+ case "devicelight":
+ this._handleDeviceLight(aEvent.value);
+ break;
+
+ case "visibilitychange":
+ this._handleVisibilityChange();
+ break;
+
+ case "pagehide":
+ // Close the Banners Font-dropdown, cleanup Android BackPressListener.
+ this._closeDropdowns();
+
+ this._mm.removeMessageListener("Reader:CloseDropdown", this);
+ this._mm.removeMessageListener("Reader:AddButton", this);
+ this._mm.removeMessageListener("Reader:RemoveButton", this);
+ this._mm.removeMessageListener("Reader:GetStoredArticleData", this);
+ this._windowUnloaded = true;
+ break;
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ if (subject.QueryInterface(Ci.nsISupportsPRUint64).data != this._innerWindowId) {
+ return;
+ }
+
+ Services.obs.removeObserver(this, "inner-window-destroyed", false);
+
+ this._mm.removeMessageListener("Reader:CloseDropdown", this);
+ this._mm.removeMessageListener("Reader:AddButton", this);
+ this._mm.removeMessageListener("Reader:RemoveButton", this);
+ this._windowUnloaded = true;
+ },
+
+ _onReaderClose: function() {
+ ReaderMode.leaveReaderMode(this._mm.docShell, this._win);
+ },
+
+ _setFontSize: function(newFontSize) {
+ let containerClasses = this._doc.getElementById("container").classList;
+
+ if (this._fontSize > 0)
+ containerClasses.remove("font-size" + this._fontSize);
+
+ this._fontSize = newFontSize;
+ containerClasses.add("font-size" + this._fontSize);
+ return AsyncPrefs.set("reader.font_size", this._fontSize);
+ },
+
+ _setupFontSizeButtons: function() {
+ const FONT_SIZE_MIN = 1;
+ const FONT_SIZE_MAX = 9;
+
+ // Sample text shown in Android UI.
+ let sampleText = this._doc.getElementById("font-size-sample");
+ sampleText.textContent = gStrings.GetStringFromName("aboutReader.fontTypeSample");
+
+ let currentSize = Services.prefs.getIntPref("reader.font_size");
+ currentSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize));
+
+ let plusButton = this._doc.getElementById("font-size-plus");
+ let minusButton = this._doc.getElementById("font-size-minus");
+
+ function updateControls() {
+ if (currentSize === FONT_SIZE_MIN) {
+ minusButton.setAttribute("disabled", true);
+ } else {
+ minusButton.removeAttribute("disabled");
+ }
+ if (currentSize === FONT_SIZE_MAX) {
+ plusButton.setAttribute("disabled", true);
+ } else {
+ plusButton.removeAttribute("disabled");
+ }
+ }
+
+ updateControls();
+ this._setFontSize(currentSize);
+
+ plusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentSize >= FONT_SIZE_MAX) {
+ return;
+ }
+
+ currentSize++;
+ updateControls();
+ this._setFontSize(currentSize);
+ }, true);
+
+ minusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentSize <= FONT_SIZE_MIN) {
+ return;
+ }
+
+ currentSize--;
+ updateControls();
+ this._setFontSize(currentSize);
+ }, true);
+ },
+
+ _setContentWidth: function(newContentWidth) {
+ let containerClasses = this._doc.getElementById("container").classList;
+
+ if (this._contentWidth > 0)
+ containerClasses.remove("content-width" + this._contentWidth);
+
+ this._contentWidth = newContentWidth;
+ containerClasses.add("content-width" + this._contentWidth);
+ return AsyncPrefs.set("reader.content_width", this._contentWidth);
+ },
+
+ _setupContentWidthButtons: function() {
+ const CONTENT_WIDTH_MIN = 1;
+ const CONTENT_WIDTH_MAX = 9;
+
+ let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
+ currentContentWidth = Math.max(CONTENT_WIDTH_MIN, Math.min(CONTENT_WIDTH_MAX, currentContentWidth));
+
+ let plusButton = this._doc.getElementById("content-width-plus");
+ let minusButton = this._doc.getElementById("content-width-minus");
+
+ function updateControls() {
+ if (currentContentWidth === CONTENT_WIDTH_MIN) {
+ minusButton.setAttribute("disabled", true);
+ } else {
+ minusButton.removeAttribute("disabled");
+ }
+ if (currentContentWidth === CONTENT_WIDTH_MAX) {
+ plusButton.setAttribute("disabled", true);
+ } else {
+ plusButton.removeAttribute("disabled");
+ }
+ }
+
+ updateControls();
+ this._setContentWidth(currentContentWidth);
+
+ plusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentContentWidth >= CONTENT_WIDTH_MAX) {
+ return;
+ }
+
+ currentContentWidth++;
+ updateControls();
+ this._setContentWidth(currentContentWidth);
+ }, true);
+
+ minusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentContentWidth <= CONTENT_WIDTH_MIN) {
+ return;
+ }
+
+ currentContentWidth--;
+ updateControls();
+ this._setContentWidth(currentContentWidth);
+ }, true);
+ },
+
+ _setLineHeight: function(newLineHeight) {
+ let contentClasses = this._doc.getElementById("moz-reader-content").classList;
+
+ if (this._lineHeight > 0)
+ contentClasses.remove("line-height" + this._lineHeight);
+
+ this._lineHeight = newLineHeight;
+ contentClasses.add("line-height" + this._lineHeight);
+ return AsyncPrefs.set("reader.line_height", this._lineHeight);
+ },
+
+ _setupLineHeightButtons: function() {
+ const LINE_HEIGHT_MIN = 1;
+ const LINE_HEIGHT_MAX = 9;
+
+ let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
+ currentLineHeight = Math.max(LINE_HEIGHT_MIN, Math.min(LINE_HEIGHT_MAX, currentLineHeight));
+
+ let plusButton = this._doc.getElementById("line-height-plus");
+ let minusButton = this._doc.getElementById("line-height-minus");
+
+ function updateControls() {
+ if (currentLineHeight === LINE_HEIGHT_MIN) {
+ minusButton.setAttribute("disabled", true);
+ } else {
+ minusButton.removeAttribute("disabled");
+ }
+ if (currentLineHeight === LINE_HEIGHT_MAX) {
+ plusButton.setAttribute("disabled", true);
+ } else {
+ plusButton.removeAttribute("disabled");
+ }
+ }
+
+ updateControls();
+ this._setLineHeight(currentLineHeight);
+
+ plusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentLineHeight >= LINE_HEIGHT_MAX) {
+ return;
+ }
+
+ currentLineHeight++;
+ updateControls();
+ this._setLineHeight(currentLineHeight);
+ }, true);
+
+ minusButton.addEventListener("click", (event) => {
+ if (!event.isTrusted) {
+ return;
+ }
+ event.stopPropagation();
+
+ if (currentLineHeight <= LINE_HEIGHT_MIN) {
+ return;
+ }
+
+ currentLineHeight--;
+ updateControls();
+ this._setLineHeight(currentLineHeight);
+ }, true);
+ },
+
+ _handleDeviceLight: function(newLux) {
+ // Desired size of the this._luxValues array.
+ let luxValuesSize = 10;
+ // Add new lux value at the front of the array.
+ this._luxValues.unshift(newLux);
+ // Add new lux value to this._totalLux for averaging later.
+ this._totalLux += newLux;
+
+ // Don't update when length of array is less than luxValuesSize except when it is 1.
+ if (this._luxValues.length < luxValuesSize) {
+ // Use the first lux value to set the color scheme until our array equals luxValuesSize.
+ if (this._luxValues.length == 1) {
+ this._updateColorScheme(newLux);
+ }
+ return;
+ }
+ // Holds the average of the lux values collected in this._luxValues.
+ let averageLuxValue = this._totalLux/luxValuesSize;
+
+ this._updateColorScheme(averageLuxValue);
+ // Pop the oldest value off the array.
+ let oldLux = this._luxValues.pop();
+ // Subtract oldLux since it has been discarded from the array.
+ this._totalLux -= oldLux;
+ },
+
+ _handleVisibilityChange: function() {
+ let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
+ if (colorScheme != "auto") {
+ return;
+ }
+
+ // Turn off the ambient light sensor if the page is hidden
+ this._enableAmbientLighting(!this._doc.hidden);
+ },
+
+ // Setup or teardown the ambient light tracking system.
+ _enableAmbientLighting: function(enable) {
+ if (enable) {
+ this._win.addEventListener("devicelight", this, false);
+ this._luxValues = [];
+ this._totalLux = 0;
+ } else {
+ this._win.removeEventListener("devicelight", this, false);
+ delete this._luxValues;
+ delete this._totalLux;
+ }
+ },
+
+ _updateColorScheme: function(luxValue) {
+ // Upper bound value for "dark" color scheme beyond which it changes to "light".
+ let upperBoundDark = 50;
+ // Lower bound value for "light" color scheme beyond which it changes to "dark".
+ let lowerBoundLight = 10;
+ // Threshold for color scheme change.
+ let colorChangeThreshold = 20;
+
+ // Ignore changes that are within a certain threshold of previous lux values.
+ if ((this._colorScheme === "dark" && luxValue < upperBoundDark) ||
+ (this._colorScheme === "light" && luxValue > lowerBoundLight))
+ return;
+
+ if (luxValue < colorChangeThreshold)
+ this._setColorScheme("dark");
+ else
+ this._setColorScheme("light");
+ },
+
+ _setColorScheme: function(newColorScheme) {
+ // "auto" is not a real color scheme
+ if (this._colorScheme === newColorScheme || newColorScheme === "auto")
+ return;
+
+ let bodyClasses = this._doc.body.classList;
+
+ if (this._colorScheme)
+ bodyClasses.remove(this._colorScheme);
+
+ this._colorScheme = newColorScheme;
+ bodyClasses.add(this._colorScheme);
+ },
+
+ // Pref values include "dark", "light", and "auto", which automatically switches
+ // between light and dark color schemes based on the ambient light level.
+ _setColorSchemePref: function(colorSchemePref) {
+ this._enableAmbientLighting(colorSchemePref === "auto");
+ this._setColorScheme(colorSchemePref);
+
+ AsyncPrefs.set("reader.color_scheme", colorSchemePref);
+ },
+
+ _setFontType: function(newFontType) {
+ if (this._fontType === newFontType)
+ return;
+
+ let bodyClasses = this._doc.body.classList;
+
+ if (this._fontType)
+ bodyClasses.remove(this._fontType);
+
+ this._fontType = newFontType;
+ bodyClasses.add(this._fontType);
+
+ AsyncPrefs.set("reader.font_type", this._fontType);
+ },
+
+ _setSystemUIVisibility: function(visible) {
+ this._mm.sendAsyncMessage("Reader:SystemUIVisibility", { visible: visible });
+ },
+
+ _loadArticle: Task.async(function* () {
+ let url = this._getOriginalUrl();
+ this._showProgressDelayed();
+
+ let article;
+ if (this._articlePromise) {
+ article = yield this._articlePromise;
+ } else {
+ try {
+ article = yield this._getArticle(url);
+ } catch (e) {
+ if (e && e.newURL) {
+ let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
+ this._win.location.replace(readerURL);
+ return;
+ }
+ }
+ }
+
+ if (this._windowUnloaded) {
+ return;
+ }
+
+ // Replace the loading message with an error message if there's a failure.
+ // Users are supposed to navigate away by themselves (because we cannot
+ // remove ourselves from session history.)
+ if (!article) {
+ this._showError();
+ return;
+ }
+
+ this._showContent(article);
+ }),
+
+ _getArticle: function(url) {
+ return new Promise((resolve, reject) => {
+ let listener = (message) => {
+ this._mm.removeMessageListener("Reader:ArticleData", listener);
+ if (message.data.newURL) {
+ reject({ newURL: message.data.newURL });
+ return;
+ }
+ resolve(message.data.article);
+ };
+ this._mm.addMessageListener("Reader:ArticleData", listener);
+ this._mm.sendAsyncMessage("Reader:ArticleGet", { url: url });
+ });
+ },
+
+ _requestFavicon: function() {
+ let handleFaviconReturn = (message) => {
+ this._mm.removeMessageListener("Reader:FaviconReturn", handleFaviconReturn);
+ this._loadFavicon(message.data.url, message.data.faviconUrl);
+ };
+
+ this._mm.addMessageListener("Reader:FaviconReturn", handleFaviconReturn);
+ this._mm.sendAsyncMessage("Reader:FaviconRequest", { url: this._article.url });
+ },
+
+ _loadFavicon: function(url, faviconUrl) {
+ if (this._article.url !== url)
+ return;
+
+ let doc = this._doc;
+
+ let link = doc.createElement('link');
+ link.rel = 'shortcut icon';
+ link.href = faviconUrl;
+
+ doc.getElementsByTagName('head')[0].appendChild(link);
+ },
+
+ _updateImageMargins: function() {
+ let windowWidth = this._win.innerWidth;
+ let bodyWidth = this._doc.body.clientWidth;
+
+ let setImageMargins = function(img) {
+ // If the image is at least as wide as the window, make it fill edge-to-edge on mobile.
+ if (img.naturalWidth >= windowWidth) {
+ img.setAttribute("moz-reader-full-width", true);
+ } else {
+ img.removeAttribute("moz-reader-full-width");
+ }
+
+ // If the image is at least half as wide as the body, center it on desktop.
+ if (img.naturalWidth >= bodyWidth/2) {
+ img.setAttribute("moz-reader-center", true);
+ } else {
+ img.removeAttribute("moz-reader-center");
+ }
+ };
+
+ let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR);
+ for (let i = imgs.length; --i >= 0;) {
+ let img = imgs[i];
+
+ if (img.naturalWidth > 0) {
+ setImageMargins(img);
+ } else {
+ img.onload = function() {
+ setImageMargins(img);
+ };
+ }
+ }
+ },
+
+ _maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
+ if (!article.dir)
+ return;
+
+ // Set "dir" attribute on content
+ this._contentElement.setAttribute("dir", article.dir);
+ this._headerElement.setAttribute("dir", article.dir);
+ },
+
+ _fixLocalLinks() {
+ // We need to do this because preprocessing the content through nsIParserUtils
+ // gives back a DOM with a <base> element. That influences how these URLs get
+ // resolved, making them no longer match the document URI (which is
+ // about:reader?url=...). To fix this, make all the hash URIs absolute. This
+ // is hacky, but the alternative of removing the base element has potential
+ // security implications if Readability has not successfully made all the URLs
+ // absolute, so we pick just fixing these in-document links explicitly.
+ let localLinks = this._contentElement.querySelectorAll("a[href^='#']");
+ for (let localLink of localLinks) {
+ // Have to get the attribute because .href provides an absolute URI.
+ localLink.href = this._doc.documentURI + localLink.getAttribute("href");
+ }
+ },
+
+ _showError: function() {
+ this._headerElement.style.display = "none";
+ this._contentElement.style.display = "none";
+
+ let errorMessage = gStrings.GetStringFromName("aboutReader.loadError");
+ this._messageElement.textContent = errorMessage;
+ this._messageElement.style.display = "block";
+
+ this._doc.title = errorMessage;
+
+ this._error = true;
+ },
+
+ // This function is the JS version of Java's StringUtils.stripCommonSubdomains.
+ _stripHost: function(host) {
+ if (!host)
+ return host;
+
+ let start = 0;
+
+ if (host.startsWith("www."))
+ start = 4;
+ else if (host.startsWith("m."))
+ start = 2;
+ else if (host.startsWith("mobile."))
+ start = 7;
+
+ return host.substring(start);
+ },
+
+ _showContent: function(article) {
+ this._messageElement.style.display = "none";
+
+ this._article = article;
+
+ this._domainElement.href = article.url;
+ let articleUri = Services.io.newURI(article.url, null, null);
+ this._domainElement.textContent = this._stripHost(articleUri.host);
+ this._creditsElement.textContent = article.byline;
+
+ this._titleElement.textContent = article.title;
+ this._doc.title = article.title;
+
+ this._headerElement.style.display = "block";
+
+ let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
+ let contentFragment = parserUtils.parseFragment(article.content,
+ Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle,
+ false, articleUri, this._contentElement);
+ this._contentElement.innerHTML = "";
+ this._contentElement.appendChild(contentFragment);
+ this._fixLocalLinks();
+ this._maybeSetTextDirection(article);
+
+ this._contentElement.style.display = "block";
+ this._updateImageMargins();
+
+ this._requestFavicon();
+ this._doc.body.classList.add("loaded");
+
+ this._goToReference(articleUri.ref);
+
+ Services.obs.notifyObservers(this._win, "AboutReader:Ready", "");
+
+ this._doc.dispatchEvent(
+ new this._win.CustomEvent("AboutReaderContentReady", { bubbles: true, cancelable: false }));
+ },
+
+ _hideContent: function() {
+ this._headerElement.style.display = "none";
+ this._contentElement.style.display = "none";
+ },
+
+ _showProgressDelayed: function() {
+ this._win.setTimeout(function() {
+ // No need to show progress if the article has been loaded,
+ // if the window has been unloaded, or if there was an error
+ // trying to load the article.
+ if (this._article || this._windowUnloaded || this._error) {
+ return;
+ }
+
+ this._headerElement.style.display = "none";
+ this._contentElement.style.display = "none";
+
+ this._messageElement.textContent = gStrings.GetStringFromName("aboutReader.loading2");
+ this._messageElement.style.display = "block";
+ }.bind(this), 300);
+ },
+
+ /**
+ * Returns the original article URL for this about:reader view.
+ */
+ _getOriginalUrl: function(win) {
+ let url = win ? win.location.href : this._win.location.href;
+ return ReaderMode.getOriginalUrl(url) || url;
+ },
+
+ _setupSegmentedButton: function(id, options, initialValue, callback) {
+ let doc = this._doc;
+ let segmentedButton = doc.getElementById(id);
+
+ for (let i = 0; i < options.length; i++) {
+ let option = options[i];
+
+ let item = doc.createElement("button");
+
+ // Put the name in a div so that Android can hide it.
+ let div = doc.createElement("div");
+ div.textContent = option.name;
+ div.classList.add("name");
+ item.appendChild(div);
+
+ if (option.itemClass !== undefined)
+ item.classList.add(option.itemClass);
+
+ if (option.description !== undefined) {
+ let description = doc.createElement("div");
+ description.textContent = option.description;
+ description.classList.add("description");
+ item.appendChild(description);
+ }
+
+ segmentedButton.appendChild(item);
+
+ item.addEventListener("click", function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+
+ aEvent.stopPropagation();
+
+ // Just pass the ID of the button as an extra and hope the ID doesn't change
+ // unless the context changes
+ UITelemetry.addEvent("action.1", "button", null, id);
+
+ let items = segmentedButton.children;
+ for (let j = items.length - 1; j >= 0; j--) {
+ items[j].classList.remove("selected");
+ }
+
+ item.classList.add("selected");
+ callback(option.value);
+ }.bind(this), true);
+
+ if (option.value === initialValue)
+ item.classList.add("selected");
+ }
+ },
+
+ _setupButton: function(id, callback, titleEntity, textEntity) {
+ if (titleEntity) {
+ this._setButtonTip(id, titleEntity);
+ }
+
+ let button = this._doc.getElementById(id);
+ if (textEntity) {
+ button.textContent = gStrings.GetStringFromName(textEntity);
+ }
+ button.removeAttribute("hidden");
+ button.addEventListener("click", function(aEvent) {
+ if (!aEvent.isTrusted)
+ return;
+
+ aEvent.stopPropagation();
+ let btn = aEvent.target;
+ callback(btn);
+ }, true);
+ },
+
+ /**
+ * Sets a toolTip for a button. Performed at initial button setup
+ * and dynamically as button state changes.
+ * @param Localizable string providing UI element usage tip.
+ */
+ _setButtonTip: function(id, titleEntity) {
+ let button = this._doc.getElementById(id);
+ button.setAttribute("title", gStrings.GetStringFromName(titleEntity));
+ },
+
+ _setupStyleDropdown: function() {
+ let dropdownToggle = this._doc.querySelector("#style-dropdown .dropdown-toggle");
+ dropdownToggle.setAttribute("title", gStrings.GetStringFromName("aboutReader.toolbar.typeControls"));
+ },
+
+ _updatePopupPosition: function(dropdown) {
+ let dropdownToggle = dropdown.querySelector(".dropdown-toggle");
+ let dropdownPopup = dropdown.querySelector(".dropdown-popup");
+
+ let toggleHeight = dropdownToggle.offsetHeight;
+ let toggleTop = dropdownToggle.offsetTop;
+ let popupTop = toggleTop - toggleHeight / 2;
+
+ dropdownPopup.style.top = popupTop + "px";
+ },
+
+ _toggleDropdownClicked: function(event) {
+ let dropdown = event.target.closest('.dropdown');
+
+ if (!dropdown)
+ return;
+
+ event.stopPropagation();
+
+ if (dropdown.classList.contains("open")) {
+ this._closeDropdowns();
+ } else {
+ this._openDropdown(dropdown);
+ if (this._isToolbarVertical) {
+ this._updatePopupPosition(dropdown);
+ }
+ }
+ },
+
+ /*
+ * If the ReaderView banner font-dropdown is closed, open it.
+ */
+ _openDropdown: function(dropdown) {
+ if (dropdown.classList.contains("open")) {
+ return;
+ }
+
+ this._closeDropdowns();
+
+ // Trigger BackPressListener initialization in Android.
+ dropdown.classList.add("open");
+ this._mm.sendAsyncMessage("Reader:DropdownOpened", this.viewId);
+ },
+
+ /*
+ * If the ReaderView has open dropdowns, close them. If we are closing the
+ * dropdowns because the page is scrolling, allow popups to stay open with
+ * the keep-open class.
+ */
+ _closeDropdowns: function(scrolling) {
+ let selector = ".dropdown.open";
+ if (scrolling) {
+ selector += ":not(.keep-open)";
+ }
+
+ let openDropdowns = this._doc.querySelectorAll(selector);
+ for (let dropdown of openDropdowns) {
+ dropdown.classList.remove("open");
+ }
+
+ // Trigger BackPressListener cleanup in Android.
+ if (openDropdowns.length) {
+ this._mm.sendAsyncMessage("Reader:DropdownClosed", this.viewId);
+ }
+ },
+
+ /*
+ * Scroll reader view to a reference
+ */
+ _goToReference(ref) {
+ if (ref) {
+ this._win.location.hash = ref;
+ }
+ }
+};
diff --git a/toolkit/components/reader/JSDOMParser.js b/toolkit/components/reader/JSDOMParser.js
new file mode 100644
index 0000000000..853649775e
--- /dev/null
+++ b/toolkit/components/reader/JSDOMParser.js
@@ -0,0 +1,1195 @@
+/*eslint-env es6:false*/
+/*
+ * DO NOT MODIFY THIS FILE DIRECTLY!
+ *
+ * This is a shared library that is maintained in an external repo:
+ * https://github.com/mozilla/readability
+ */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 a relatively lightweight DOMParser that is safe to use in a web
+ * worker. This is far from a complete DOM implementation; however, it should
+ * contain the minimal set of functionality necessary for Readability.js.
+ *
+ * Aside from not implementing the full DOM API, there are other quirks to be
+ * aware of when using the JSDOMParser:
+ *
+ * 1) Properly formed HTML/XML must be used. This means you should be extra
+ * careful when using this parser on anything received directly from an
+ * XMLHttpRequest. Providing a serialized string from an XMLSerializer,
+ * however, should be safe (since the browser's XMLSerializer should
+ * generate valid HTML/XML). Therefore, if parsing a document from an XHR,
+ * the recommended approach is to do the XHR in the main thread, use
+ * XMLSerializer.serializeToString() on the responseXML, and pass the
+ * resulting string to the worker.
+ *
+ * 2) Live NodeLists are not supported. DOM methods and properties such as
+ * getElementsByTagName() and childNodes return standard arrays. If you
+ * want these lists to be updated when nodes are removed or added to the
+ * document, you must take care to manually update them yourself.
+ */
+(function (global) {
+
+ // XML only defines these and the numeric ones:
+
+ var entityTable = {
+ "lt": "<",
+ "gt": ">",
+ "amp": "&",
+ "quot": '"',
+ "apos": "'",
+ };
+
+ var reverseEntityTable = {
+ "<": "&lt;",
+ ">": "&gt;",
+ "&": "&amp;",
+ '"': "&quot;",
+ "'": "&apos;",
+ };
+
+ function encodeTextContentHTML(s) {
+ return s.replace(/[&<>]/g, function(x) {
+ return reverseEntityTable[x];
+ });
+ }
+
+ function encodeHTML(s) {
+ return s.replace(/[&<>'"]/g, function(x) {
+ return reverseEntityTable[x];
+ });
+ }
+
+ function decodeHTML(str) {
+ return str.replace(/&(quot|amp|apos|lt|gt);/g, function(match, tag) {
+ return entityTable[tag];
+ }).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(match, hex, numStr) {
+ var num = parseInt(hex || numStr, hex ? 16 : 10); // read num
+ return String.fromCharCode(num);
+ });
+ }
+
+ // When a style is set in JS, map it to the corresponding CSS attribute
+ var styleMap = {
+ "alignmentBaseline": "alignment-baseline",
+ "background": "background",
+ "backgroundAttachment": "background-attachment",
+ "backgroundClip": "background-clip",
+ "backgroundColor": "background-color",
+ "backgroundImage": "background-image",
+ "backgroundOrigin": "background-origin",
+ "backgroundPosition": "background-position",
+ "backgroundPositionX": "background-position-x",
+ "backgroundPositionY": "background-position-y",
+ "backgroundRepeat": "background-repeat",
+ "backgroundRepeatX": "background-repeat-x",
+ "backgroundRepeatY": "background-repeat-y",
+ "backgroundSize": "background-size",
+ "baselineShift": "baseline-shift",
+ "border": "border",
+ "borderBottom": "border-bottom",
+ "borderBottomColor": "border-bottom-color",
+ "borderBottomLeftRadius": "border-bottom-left-radius",
+ "borderBottomRightRadius": "border-bottom-right-radius",
+ "borderBottomStyle": "border-bottom-style",
+ "borderBottomWidth": "border-bottom-width",
+ "borderCollapse": "border-collapse",
+ "borderColor": "border-color",
+ "borderImage": "border-image",
+ "borderImageOutset": "border-image-outset",
+ "borderImageRepeat": "border-image-repeat",
+ "borderImageSlice": "border-image-slice",
+ "borderImageSource": "border-image-source",
+ "borderImageWidth": "border-image-width",
+ "borderLeft": "border-left",
+ "borderLeftColor": "border-left-color",
+ "borderLeftStyle": "border-left-style",
+ "borderLeftWidth": "border-left-width",
+ "borderRadius": "border-radius",
+ "borderRight": "border-right",
+ "borderRightColor": "border-right-color",
+ "borderRightStyle": "border-right-style",
+ "borderRightWidth": "border-right-width",
+ "borderSpacing": "border-spacing",
+ "borderStyle": "border-style",
+ "borderTop": "border-top",
+ "borderTopColor": "border-top-color",
+ "borderTopLeftRadius": "border-top-left-radius",
+ "borderTopRightRadius": "border-top-right-radius",
+ "borderTopStyle": "border-top-style",
+ "borderTopWidth": "border-top-width",
+ "borderWidth": "border-width",
+ "bottom": "bottom",
+ "boxShadow": "box-shadow",
+ "boxSizing": "box-sizing",
+ "captionSide": "caption-side",
+ "clear": "clear",
+ "clip": "clip",
+ "clipPath": "clip-path",
+ "clipRule": "clip-rule",
+ "color": "color",
+ "colorInterpolation": "color-interpolation",
+ "colorInterpolationFilters": "color-interpolation-filters",
+ "colorProfile": "color-profile",
+ "colorRendering": "color-rendering",
+ "content": "content",
+ "counterIncrement": "counter-increment",
+ "counterReset": "counter-reset",
+ "cursor": "cursor",
+ "direction": "direction",
+ "display": "display",
+ "dominantBaseline": "dominant-baseline",
+ "emptyCells": "empty-cells",
+ "enableBackground": "enable-background",
+ "fill": "fill",
+ "fillOpacity": "fill-opacity",
+ "fillRule": "fill-rule",
+ "filter": "filter",
+ "cssFloat": "float",
+ "floodColor": "flood-color",
+ "floodOpacity": "flood-opacity",
+ "font": "font",
+ "fontFamily": "font-family",
+ "fontSize": "font-size",
+ "fontStretch": "font-stretch",
+ "fontStyle": "font-style",
+ "fontVariant": "font-variant",
+ "fontWeight": "font-weight",
+ "glyphOrientationHorizontal": "glyph-orientation-horizontal",
+ "glyphOrientationVertical": "glyph-orientation-vertical",
+ "height": "height",
+ "imageRendering": "image-rendering",
+ "kerning": "kerning",
+ "left": "left",
+ "letterSpacing": "letter-spacing",
+ "lightingColor": "lighting-color",
+ "lineHeight": "line-height",
+ "listStyle": "list-style",
+ "listStyleImage": "list-style-image",
+ "listStylePosition": "list-style-position",
+ "listStyleType": "list-style-type",
+ "margin": "margin",
+ "marginBottom": "margin-bottom",
+ "marginLeft": "margin-left",
+ "marginRight": "margin-right",
+ "marginTop": "margin-top",
+ "marker": "marker",
+ "markerEnd": "marker-end",
+ "markerMid": "marker-mid",
+ "markerStart": "marker-start",
+ "mask": "mask",
+ "maxHeight": "max-height",
+ "maxWidth": "max-width",
+ "minHeight": "min-height",
+ "minWidth": "min-width",
+ "opacity": "opacity",
+ "orphans": "orphans",
+ "outline": "outline",
+ "outlineColor": "outline-color",
+ "outlineOffset": "outline-offset",
+ "outlineStyle": "outline-style",
+ "outlineWidth": "outline-width",
+ "overflow": "overflow",
+ "overflowX": "overflow-x",
+ "overflowY": "overflow-y",
+ "padding": "padding",
+ "paddingBottom": "padding-bottom",
+ "paddingLeft": "padding-left",
+ "paddingRight": "padding-right",
+ "paddingTop": "padding-top",
+ "page": "page",
+ "pageBreakAfter": "page-break-after",
+ "pageBreakBefore": "page-break-before",
+ "pageBreakInside": "page-break-inside",
+ "pointerEvents": "pointer-events",
+ "position": "position",
+ "quotes": "quotes",
+ "resize": "resize",
+ "right": "right",
+ "shapeRendering": "shape-rendering",
+ "size": "size",
+ "speak": "speak",
+ "src": "src",
+ "stopColor": "stop-color",
+ "stopOpacity": "stop-opacity",
+ "stroke": "stroke",
+ "strokeDasharray": "stroke-dasharray",
+ "strokeDashoffset": "stroke-dashoffset",
+ "strokeLinecap": "stroke-linecap",
+ "strokeLinejoin": "stroke-linejoin",
+ "strokeMiterlimit": "stroke-miterlimit",
+ "strokeOpacity": "stroke-opacity",
+ "strokeWidth": "stroke-width",
+ "tableLayout": "table-layout",
+ "textAlign": "text-align",
+ "textAnchor": "text-anchor",
+ "textDecoration": "text-decoration",
+ "textIndent": "text-indent",
+ "textLineThrough": "text-line-through",
+ "textLineThroughColor": "text-line-through-color",
+ "textLineThroughMode": "text-line-through-mode",
+ "textLineThroughStyle": "text-line-through-style",
+ "textLineThroughWidth": "text-line-through-width",
+ "textOverflow": "text-overflow",
+ "textOverline": "text-overline",
+ "textOverlineColor": "text-overline-color",
+ "textOverlineMode": "text-overline-mode",
+ "textOverlineStyle": "text-overline-style",
+ "textOverlineWidth": "text-overline-width",
+ "textRendering": "text-rendering",
+ "textShadow": "text-shadow",
+ "textTransform": "text-transform",
+ "textUnderline": "text-underline",
+ "textUnderlineColor": "text-underline-color",
+ "textUnderlineMode": "text-underline-mode",
+ "textUnderlineStyle": "text-underline-style",
+ "textUnderlineWidth": "text-underline-width",
+ "top": "top",
+ "unicodeBidi": "unicode-bidi",
+ "unicodeRange": "unicode-range",
+ "vectorEffect": "vector-effect",
+ "verticalAlign": "vertical-align",
+ "visibility": "visibility",
+ "whiteSpace": "white-space",
+ "widows": "widows",
+ "width": "width",
+ "wordBreak": "word-break",
+ "wordSpacing": "word-spacing",
+ "wordWrap": "word-wrap",
+ "writingMode": "writing-mode",
+ "zIndex": "z-index",
+ "zoom": "zoom",
+ };
+
+ // Elements that can be self-closing
+ var voidElems = {
+ "area": true,
+ "base": true,
+ "br": true,
+ "col": true,
+ "command": true,
+ "embed": true,
+ "hr": true,
+ "img": true,
+ "input": true,
+ "link": true,
+ "meta": true,
+ "param": true,
+ "source": true,
+ "wbr": true
+ };
+
+ var whitespace = [" ", "\t", "\n", "\r"];
+
+ // See http://www.w3schools.com/dom/dom_nodetype.asp
+ var nodeTypes = {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ };
+
+ function getElementsByTagName(tag) {
+ tag = tag.toUpperCase();
+ var elems = [];
+ var allTags = (tag === "*");
+ function getElems(node) {
+ var length = node.children.length;
+ for (var i = 0; i < length; i++) {
+ var child = node.children[i];
+ if (allTags || (child.tagName === tag))
+ elems.push(child);
+ getElems(child);
+ }
+ }
+ getElems(this);
+ return elems;
+ }
+
+ var Node = function () {};
+
+ Node.prototype = {
+ attributes: null,
+ childNodes: null,
+ localName: null,
+ nodeName: null,
+ parentNode: null,
+ textContent: null,
+ nextSibling: null,
+ previousSibling: null,
+
+ get firstChild() {
+ return this.childNodes[0] || null;
+ },
+
+ get firstElementChild() {
+ return this.children[0] || null;
+ },
+
+ get lastChild() {
+ return this.childNodes[this.childNodes.length - 1] || null;
+ },
+
+ get lastElementChild() {
+ return this.children[this.children.length - 1] || null;
+ },
+
+ appendChild: function (child) {
+ if (child.parentNode) {
+ child.parentNode.removeChild(child);
+ }
+
+ var last = this.lastChild;
+ if (last)
+ last.nextSibling = child;
+ child.previousSibling = last;
+
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ child.previousElementSibling = this.children[this.children.length - 1] || null;
+ this.children.push(child);
+ child.previousElementSibling && (child.previousElementSibling.nextElementSibling = child);
+ }
+ this.childNodes.push(child);
+ child.parentNode = this;
+ },
+
+ removeChild: function (child) {
+ var childNodes = this.childNodes;
+ var childIndex = childNodes.indexOf(child);
+ if (childIndex === -1) {
+ throw "removeChild: node not found";
+ } else {
+ child.parentNode = null;
+ var prev = child.previousSibling;
+ var next = child.nextSibling;
+ if (prev)
+ prev.nextSibling = next;
+ if (next)
+ next.previousSibling = prev;
+
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ prev = child.previousElementSibling;
+ next = child.nextElementSibling;
+ if (prev)
+ prev.nextElementSibling = next;
+ if (next)
+ next.previousElementSibling = prev;
+ this.children.splice(this.children.indexOf(child), 1);
+ }
+
+ child.previousSibling = child.nextSibling = null;
+ child.previousElementSibling = child.nextElementSibling = null;
+
+ return childNodes.splice(childIndex, 1)[0];
+ }
+ },
+
+ replaceChild: function (newNode, oldNode) {
+ var childNodes = this.childNodes;
+ var childIndex = childNodes.indexOf(oldNode);
+ if (childIndex === -1) {
+ throw "replaceChild: node not found";
+ } else {
+ // This will take care of updating the new node if it was somewhere else before:
+ if (newNode.parentNode)
+ newNode.parentNode.removeChild(newNode);
+
+ childNodes[childIndex] = newNode;
+
+ // update the new node's sibling properties, and its new siblings' sibling properties
+ newNode.nextSibling = oldNode.nextSibling;
+ newNode.previousSibling = oldNode.previousSibling;
+ if (newNode.nextSibling)
+ newNode.nextSibling.previousSibling = newNode;
+ if (newNode.previousSibling)
+ newNode.previousSibling.nextSibling = newNode;
+
+ newNode.parentNode = this;
+
+ // Now deal with elements before we clear out those values for the old node,
+ // because it can help us take shortcuts here:
+ if (newNode.nodeType === Node.ELEMENT_NODE) {
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
+ // Both were elements, which makes this easier, we just swap things out:
+ newNode.previousElementSibling = oldNode.previousElementSibling;
+ newNode.nextElementSibling = oldNode.nextElementSibling;
+ if (newNode.previousElementSibling)
+ newNode.previousElementSibling.nextElementSibling = newNode;
+ if (newNode.nextElementSibling)
+ newNode.nextElementSibling.previousElementSibling = newNode;
+ this.children[this.children.indexOf(oldNode)] = newNode;
+ } else {
+ // Hard way:
+ newNode.previousElementSibling = (function() {
+ for (var i = childIndex - 1; i >= 0; i--) {
+ if (childNodes[i].nodeType === Node.ELEMENT_NODE)
+ return childNodes[i];
+ }
+ return null;
+ })();
+ if (newNode.previousElementSibling) {
+ newNode.nextElementSibling = newNode.previousElementSibling.nextElementSibling;
+ } else {
+ newNode.nextElementSibling = (function() {
+ for (var i = childIndex + 1; i < childNodes.length; i++) {
+ if (childNodes[i].nodeType === Node.ELEMENT_NODE)
+ return childNodes[i];
+ }
+ return null;
+ })();
+ }
+ if (newNode.previousElementSibling)
+ newNode.previousElementSibling.nextElementSibling = newNode;
+ if (newNode.nextElementSibling)
+ newNode.nextElementSibling.previousElementSibling = newNode;
+
+ if (newNode.nextElementSibling)
+ this.children.splice(this.children.indexOf(newNode.nextElementSibling), 0, newNode);
+ else
+ this.children.push(newNode);
+ }
+ } else if (oldNode.nodeType === Node.ELEMENT_NODE) {
+ // new node is not an element node.
+ // if the old one was, update its element siblings:
+ if (oldNode.previousElementSibling)
+ oldNode.previousElementSibling.nextElementSibling = oldNode.nextElementSibling;
+ if (oldNode.nextElementSibling)
+ oldNode.nextElementSibling.previousElementSibling = oldNode.previousElementSibling;
+ this.children.splice(this.children.indexOf(oldNode), 1);
+
+ // If the old node wasn't an element, neither the new nor the old node was an element,
+ // and the children array and its members shouldn't need any updating.
+ }
+
+
+ oldNode.parentNode = null;
+ oldNode.previousSibling = null;
+ oldNode.nextSibling = null;
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
+ oldNode.previousElementSibling = null;
+ oldNode.nextElementSibling = null;
+ }
+ return oldNode;
+ }
+ },
+
+ __JSDOMParser__: true,
+ };
+
+ for (var nodeType in nodeTypes) {
+ Node[nodeType] = Node.prototype[nodeType] = nodeTypes[nodeType];
+ }
+
+ var Attribute = function (name, value) {
+ this.name = name;
+ this._value = value;
+ };
+
+ Attribute.prototype = {
+ get value() {
+ return this._value;
+ },
+ setValue: function(newValue) {
+ this._value = newValue;
+ delete this._decodedValue;
+ },
+ setDecodedValue: function(newValue) {
+ this._value = encodeHTML(newValue);
+ this._decodedValue = newValue;
+ },
+ getDecodedValue: function() {
+ if (typeof this._decodedValue === "undefined") {
+ this._decodedValue = (this._value && decodeHTML(this._value)) || "";
+ }
+ return this._decodedValue;
+ },
+ };
+
+ var Comment = function () {
+ this.childNodes = [];
+ };
+
+ Comment.prototype = {
+ __proto__: Node.prototype,
+
+ nodeName: "#comment",
+ nodeType: Node.COMMENT_NODE
+ };
+
+ var Text = function () {
+ this.childNodes = [];
+ };
+
+ Text.prototype = {
+ __proto__: Node.prototype,
+
+ nodeName: "#text",
+ nodeType: Node.TEXT_NODE,
+ get textContent() {
+ if (typeof this._textContent === "undefined") {
+ this._textContent = decodeHTML(this._innerHTML || "");
+ }
+ return this._textContent;
+ },
+ get innerHTML() {
+ if (typeof this._innerHTML === "undefined") {
+ this._innerHTML = encodeTextContentHTML(this._textContent || "");
+ }
+ return this._innerHTML;
+ },
+
+ set innerHTML(newHTML) {
+ this._innerHTML = newHTML;
+ delete this._textContent;
+ },
+ set textContent(newText) {
+ this._textContent = newText;
+ delete this._innerHTML;
+ },
+ };
+
+ var Document = function () {
+ this.styleSheets = [];
+ this.childNodes = [];
+ this.children = [];
+ };
+
+ Document.prototype = {
+ __proto__: Node.prototype,
+
+ nodeName: "#document",
+ nodeType: Node.DOCUMENT_NODE,
+ title: "",
+
+ getElementsByTagName: getElementsByTagName,
+
+ getElementById: function (id) {
+ function getElem(node) {
+ var length = node.children.length;
+ if (node.id === id)
+ return node;
+ for (var i = 0; i < length; i++) {
+ var el = getElem(node.children[i]);
+ if (el)
+ return el;
+ }
+ return null;
+ }
+ return getElem(this);
+ },
+
+ createElement: function (tag) {
+ var node = new Element(tag);
+ return node;
+ },
+
+ createTextNode: function (text) {
+ var node = new Text();
+ node.textContent = text;
+ return node;
+ },
+ };
+
+ var Element = function (tag) {
+ this.attributes = [];
+ this.childNodes = [];
+ this.children = [];
+ this.nextElementSibling = this.previousElementSibling = null;
+ this.localName = tag.toLowerCase();
+ this.tagName = tag.toUpperCase();
+ this.style = new Style(this);
+ };
+
+ Element.prototype = {
+ __proto__: Node.prototype,
+
+ nodeType: Node.ELEMENT_NODE,
+
+ getElementsByTagName: getElementsByTagName,
+
+ get className() {
+ return this.getAttribute("class") || "";
+ },
+
+ set className(str) {
+ this.setAttribute("class", str);
+ },
+
+ get id() {
+ return this.getAttribute("id") || "";
+ },
+
+ set id(str) {
+ this.setAttribute("id", str);
+ },
+
+ get href() {
+ return this.getAttribute("href") || "";
+ },
+
+ set href(str) {
+ this.setAttribute("href", str);
+ },
+
+ get src() {
+ return this.getAttribute("src") || "";
+ },
+
+ set src(str) {
+ this.setAttribute("src", str);
+ },
+
+ get nodeName() {
+ return this.tagName;
+ },
+
+ get innerHTML() {
+ function getHTML(node) {
+ var i = 0;
+ for (i = 0; i < node.childNodes.length; i++) {
+ var child = node.childNodes[i];
+ if (child.localName) {
+ arr.push("<" + child.localName);
+
+ // serialize attribute list
+ for (var j = 0; j < child.attributes.length; j++) {
+ var attr = child.attributes[j];
+ // the attribute value will be HTML escaped.
+ var val = attr.value;
+ var quote = (val.indexOf('"') === -1 ? '"' : "'");
+ arr.push(" " + attr.name + '=' + quote + val + quote);
+ }
+
+ if (child.localName in voidElems && !child.childNodes.length) {
+ // if this is a self-closing element, end it here
+ arr.push("/>");
+ } else {
+ // otherwise, add its children
+ arr.push(">");
+ getHTML(child);
+ arr.push("</" + child.localName + ">");
+ }
+ } else {
+ // This is a text node, so asking for innerHTML won't recurse.
+ arr.push(child.innerHTML);
+ }
+ }
+ }
+
+ // Using Array.join() avoids the overhead from lazy string concatenation.
+ // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
+ var arr = [];
+ getHTML(this);
+ return arr.join("");
+ },
+
+ set innerHTML(html) {
+ var parser = new JSDOMParser();
+ var node = parser.parse(html);
+ var i;
+ for (i = this.childNodes.length; --i >= 0;) {
+ this.childNodes[i].parentNode = null;
+ }
+ this.childNodes = node.childNodes;
+ this.children = node.children;
+ for (i = this.childNodes.length; --i >= 0;) {
+ this.childNodes[i].parentNode = this;
+ }
+ },
+
+ set textContent(text) {
+ // clear parentNodes for existing children
+ for (var i = this.childNodes.length; --i >= 0;) {
+ this.childNodes[i].parentNode = null;
+ }
+
+ var node = new Text();
+ this.childNodes = [ node ];
+ this.children = [];
+ node.textContent = text;
+ node.parentNode = this;
+ },
+
+ get textContent() {
+ function getText(node) {
+ var nodes = node.childNodes;
+ for (var i = 0; i < nodes.length; i++) {
+ var child = nodes[i];
+ if (child.nodeType === 3) {
+ text.push(child.textContent);
+ } else {
+ getText(child);
+ }
+ }
+ }
+
+ // Using Array.join() avoids the overhead from lazy string concatenation.
+ // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
+ var text = [];
+ getText(this);
+ return text.join("");
+ },
+
+ getAttribute: function (name) {
+ for (var i = this.attributes.length; --i >= 0;) {
+ var attr = this.attributes[i];
+ if (attr.name === name)
+ return attr.getDecodedValue();
+ }
+ return undefined;
+ },
+
+ setAttribute: function (name, value) {
+ for (var i = this.attributes.length; --i >= 0;) {
+ var attr = this.attributes[i];
+ if (attr.name === name) {
+ attr.setDecodedValue(value);
+ return;
+ }
+ }
+ this.attributes.push(new Attribute(name, encodeHTML(value)));
+ },
+
+ removeAttribute: function (name) {
+ for (var i = this.attributes.length; --i >= 0;) {
+ var attr = this.attributes[i];
+ if (attr.name === name) {
+ this.attributes.splice(i, 1);
+ break;
+ }
+ }
+ }
+ };
+
+ var Style = function (node) {
+ this.node = node;
+ };
+
+ // getStyle() and setStyle() use the style attribute string directly. This
+ // won't be very efficient if there are a lot of style manipulations, but
+ // it's the easiest way to make sure the style attribute string and the JS
+ // style property stay in sync. Readability.js doesn't do many style
+ // manipulations, so this should be okay.
+ Style.prototype = {
+ getStyle: function (styleName) {
+ var attr = this.node.getAttribute("style");
+ if (!attr)
+ return undefined;
+
+ var styles = attr.split(";");
+ for (var i = 0; i < styles.length; i++) {
+ var style = styles[i].split(":");
+ var name = style[0].trim();
+ if (name === styleName)
+ return style[1].trim();
+ }
+
+ return undefined;
+ },
+
+ setStyle: function (styleName, styleValue) {
+ var value = this.node.getAttribute("style") || "";
+ var index = 0;
+ do {
+ var next = value.indexOf(";", index) + 1;
+ var length = next - index - 1;
+ var style = (length > 0 ? value.substr(index, length) : value.substr(index));
+ if (style.substr(0, style.indexOf(":")).trim() === styleName) {
+ value = value.substr(0, index).trim() + (next ? " " + value.substr(next).trim() : "");
+ break;
+ }
+ index = next;
+ } while (index);
+
+ value += " " + styleName + ": " + styleValue + ";";
+ this.node.setAttribute("style", value.trim());
+ }
+ };
+
+ // For each item in styleMap, define a getter and setter on the style
+ // property.
+ for (var jsName in styleMap) {
+ (function (cssName) {
+ Style.prototype.__defineGetter__(jsName, function () {
+ return this.getStyle(cssName);
+ });
+ Style.prototype.__defineSetter__(jsName, function (value) {
+ this.setStyle(cssName, value);
+ });
+ })(styleMap[jsName]);
+ }
+
+ var JSDOMParser = function () {
+ this.currentChar = 0;
+
+ // In makeElementNode() we build up many strings one char at a time. Using
+ // += for this results in lots of short-lived intermediate strings. It's
+ // better to build an array of single-char strings and then join() them
+ // together at the end. And reusing a single array (i.e. |this.strBuf|)
+ // over and over for this purpose uses less memory than using a new array
+ // for each string.
+ this.strBuf = [];
+
+ // Similarly, we reuse this array to return the two arguments from
+ // makeElementNode(), which saves us from having to allocate a new array
+ // every time.
+ this.retPair = [];
+
+ this.errorState = "";
+ };
+
+ JSDOMParser.prototype = {
+ error: function(m) {
+ dump("JSDOMParser error: " + m + "\n");
+ this.errorState += m + "\n";
+ },
+
+ /**
+ * Look at the next character without advancing the index.
+ */
+ peekNext: function () {
+ return this.html[this.currentChar];
+ },
+
+ /**
+ * Get the next character and advance the index.
+ */
+ nextChar: function () {
+ return this.html[this.currentChar++];
+ },
+
+ /**
+ * Called after a quote character is read. This finds the next quote
+ * character and returns the text string in between.
+ */
+ readString: function (quote) {
+ var str;
+ var n = this.html.indexOf(quote, this.currentChar);
+ if (n === -1) {
+ this.currentChar = this.html.length;
+ str = null;
+ } else {
+ str = this.html.substring(this.currentChar, n);
+ this.currentChar = n + 1;
+ }
+
+ return str;
+ },
+
+ /**
+ * Called when parsing a node. This finds the next name/value attribute
+ * pair and adds the result to the attributes list.
+ */
+ readAttribute: function (node) {
+ var name = "";
+
+ var n = this.html.indexOf("=", this.currentChar);
+ if (n === -1) {
+ this.currentChar = this.html.length;
+ } else {
+ // Read until a '=' character is hit; this will be the attribute key
+ name = this.html.substring(this.currentChar, n);
+ this.currentChar = n + 1;
+ }
+
+ if (!name)
+ return;
+
+ // After a '=', we should see a '"' for the attribute value
+ var c = this.nextChar();
+ if (c !== '"' && c !== "'") {
+ this.error("Error reading attribute " + name + ", expecting '\"'");
+ return;
+ }
+
+ // Read the attribute value (and consume the matching quote)
+ var value = this.readString(c);
+
+ node.attributes.push(new Attribute(name, value));
+
+ return;
+ },
+
+ /**
+ * Parses and returns an Element node. This is called after a '<' has been
+ * read.
+ *
+ * @returns an array; the first index of the array is the parsed node;
+ * the second index is a boolean indicating whether this is a void
+ * Element
+ */
+ makeElementNode: function (retPair) {
+ var c = this.nextChar();
+
+ // Read the Element tag name
+ var strBuf = this.strBuf;
+ strBuf.length = 0;
+ while (whitespace.indexOf(c) == -1 && c !== ">" && c !== "/") {
+ if (c === undefined)
+ return false;
+ strBuf.push(c);
+ c = this.nextChar();
+ }
+ var tag = strBuf.join('');
+
+ if (!tag)
+ return false;
+
+ var node = new Element(tag);
+
+ // Read Element attributes
+ while (c !== "/" && c !== ">") {
+ if (c === undefined)
+ return false;
+ while (whitespace.indexOf(this.html[this.currentChar++]) != -1);
+ this.currentChar--;
+ c = this.nextChar();
+ if (c !== "/" && c !== ">") {
+ --this.currentChar;
+ this.readAttribute(node);
+ }
+ }
+
+ // If this is a self-closing tag, read '/>'
+ var closed = false;
+ if (c === "/") {
+ closed = true;
+ c = this.nextChar();
+ if (c !== ">") {
+ this.error("expected '>' to close " + tag);
+ return false;
+ }
+ }
+
+ retPair[0] = node;
+ retPair[1] = closed;
+ return true;
+ },
+
+ /**
+ * If the current input matches this string, advance the input index;
+ * otherwise, do nothing.
+ *
+ * @returns whether input matched string
+ */
+ match: function (str) {
+ var strlen = str.length;
+ if (this.html.substr(this.currentChar, strlen).toLowerCase() === str.toLowerCase()) {
+ this.currentChar += strlen;
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Searches the input until a string is found and discards all input up to
+ * and including the matched string.
+ */
+ discardTo: function (str) {
+ var index = this.html.indexOf(str, this.currentChar) + str.length;
+ if (index === -1)
+ this.currentChar = this.html.length;
+ this.currentChar = index;
+ },
+
+ /**
+ * Reads child nodes for the given node.
+ */
+ readChildren: function (node) {
+ var child;
+ while ((child = this.readNode())) {
+ // Don't keep Comment nodes
+ if (child.nodeType !== 8) {
+ node.appendChild(child);
+ }
+ }
+ },
+
+ readScript: function (node) {
+ while (this.currentChar < this.html.length) {
+ var c = this.nextChar();
+ var nextC = this.peekNext();
+ if (c === "<") {
+ if (nextC === "!" || nextC === "?") {
+ // We're still before the ! or ? that is starting this comment:
+ this.currentChar++;
+ node.appendChild(this.discardNextComment());
+ continue;
+ }
+ if (nextC === "/" && this.html.substr(this.currentChar, 8 /*"/script>".length */).toLowerCase() == "/script>") {
+ // Go back before the '<' so we find the end tag.
+ this.currentChar--;
+ // Done with this script tag, the caller will close:
+ return;
+ }
+ }
+ // Either c wasn't a '<' or it was but we couldn't find either a comment
+ // or a closing script tag, so we should just parse as text until the next one
+ // comes along:
+
+ var haveTextNode = node.lastChild && node.lastChild.nodeType === Node.TEXT_NODE;
+ var textNode = haveTextNode ? node.lastChild : new Text();
+ var n = this.html.indexOf("<", this.currentChar);
+ // Decrement this to include the current character *afterwards* so we don't get stuck
+ // looking for the same < all the time.
+ this.currentChar--;
+ if (n === -1) {
+ textNode.innerHTML += this.html.substring(this.currentChar, this.html.length);
+ this.currentChar = this.html.length;
+ } else {
+ textNode.innerHTML += this.html.substring(this.currentChar, n);
+ this.currentChar = n;
+ }
+ if (!haveTextNode)
+ node.appendChild(textNode);
+ }
+ },
+
+ discardNextComment: function() {
+ if (this.match("--")) {
+ this.discardTo("-->");
+ } else {
+ var c = this.nextChar();
+ while (c !== ">") {
+ if (c === undefined)
+ return null;
+ if (c === '"' || c === "'")
+ this.readString(c);
+ c = this.nextChar();
+ }
+ }
+ return new Comment();
+ },
+
+
+ /**
+ * Reads the next child node from the input. If we're reading a closing
+ * tag, or if we've reached the end of input, return null.
+ *
+ * @returns the node
+ */
+ readNode: function () {
+ var c = this.nextChar();
+
+ if (c === undefined)
+ return null;
+
+ // Read any text as Text node
+ if (c !== "<") {
+ --this.currentChar;
+ var textNode = new Text();
+ var n = this.html.indexOf("<", this.currentChar);
+ if (n === -1) {
+ textNode.innerHTML = this.html.substring(this.currentChar, this.html.length);
+ this.currentChar = this.html.length;
+ } else {
+ textNode.innerHTML = this.html.substring(this.currentChar, n);
+ this.currentChar = n;
+ }
+ return textNode;
+ }
+
+ c = this.peekNext();
+
+ // Read Comment node. Normally, Comment nodes know their inner
+ // textContent, but we don't really care about Comment nodes (we throw
+ // them away in readChildren()). So just returning an empty Comment node
+ // here is sufficient.
+ if (c === "!" || c === "?") {
+ // We're still before the ! or ? that is starting this comment:
+ this.currentChar++;
+ return this.discardNextComment();
+ }
+
+ // If we're reading a closing tag, return null. This means we've reached
+ // the end of this set of child nodes.
+ if (c === "/") {
+ --this.currentChar;
+ return null;
+ }
+
+ // Otherwise, we're looking at an Element node
+ var result = this.makeElementNode(this.retPair);
+ if (!result)
+ return null;
+
+ var node = this.retPair[0];
+ var closed = this.retPair[1];
+ var localName = node.localName;
+
+ // If this isn't a void Element, read its child nodes
+ if (!closed) {
+ if (localName == "script") {
+ this.readScript(node);
+ } else {
+ this.readChildren(node);
+ }
+ var closingTag = "</" + localName + ">";
+ if (!this.match(closingTag)) {
+ this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length));
+ return null;
+ }
+ }
+
+ // Only use the first title, because SVG might have other
+ // title elements which we don't care about (medium.com
+ // does this, at least).
+ if (localName === "title" && !this.doc.title) {
+ this.doc.title = node.textContent.trim();
+ } else if (localName === "head") {
+ this.doc.head = node;
+ } else if (localName === "body") {
+ this.doc.body = node;
+ } else if (localName === "html") {
+ this.doc.documentElement = node;
+ }
+
+ return node;
+ },
+
+ /**
+ * Parses an HTML string and returns a JS implementation of the Document.
+ */
+ parse: function (html) {
+ this.html = html;
+ var doc = this.doc = new Document();
+ this.readChildren(doc);
+
+ // If this is an HTML document, remove root-level children except for the
+ // <html> node
+ if (doc.documentElement) {
+ for (var i = doc.childNodes.length; --i >= 0;) {
+ var child = doc.childNodes[i];
+ if (child !== doc.documentElement) {
+ doc.removeChild(child);
+ }
+ }
+ }
+
+ return doc;
+ }
+ };
+
+ // Attach the standard DOM types to the global scope
+ global.Node = Node;
+ global.Comment = Comment;
+ global.Document = Document;
+ global.Element = Element;
+ global.Text = Text;
+
+ // Attach JSDOMParser to the global scope
+ global.JSDOMParser = JSDOMParser;
+
+})(this);
diff --git a/toolkit/components/reader/Readability.js b/toolkit/components/reader/Readability.js
new file mode 100644
index 0000000000..491461a8e0
--- /dev/null
+++ b/toolkit/components/reader/Readability.js
@@ -0,0 +1,1863 @@
+/*eslint-env es6:false*/
+/*
+ * DO NOT MODIFY THIS FILE DIRECTLY!
+ *
+ * This is a shared library that is maintained in an external repo:
+ * https://github.com/mozilla/readability
+ */
+
+/*
+ * Copyright (c) 2010 Arc90 Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This code is heavily based on Arc90's readability.js (1.7.1) script
+ * available at: http://code.google.com/p/arc90labs-readability
+ */
+
+/**
+ * Public constructor.
+ * @param {Object} uri The URI descriptor object.
+ * @param {HTMLDocument} doc The document to parse.
+ * @param {Object} options The options object.
+ */
+function Readability(uri, doc, options) {
+ options = options || {};
+
+ this._uri = uri;
+ this._doc = doc;
+ this._biggestFrame = false;
+ this._articleByline = null;
+ this._articleDir = null;
+
+ // Configureable options
+ this._debug = !!options.debug;
+ this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE;
+ this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES;
+ this._maxPages = options.maxPages || this.DEFAULT_MAX_PAGES;
+
+ // Start with all flags set
+ this._flags = this.FLAG_STRIP_UNLIKELYS |
+ this.FLAG_WEIGHT_CLASSES |
+ this.FLAG_CLEAN_CONDITIONALLY;
+
+ // The list of pages we've parsed in this call of readability,
+ // for autopaging. As a key store for easier searching.
+ this._parsedPages = {};
+
+ // A list of the ETag headers of pages we've parsed, in case they happen to match,
+ // we'll know it's a duplicate.
+ this._pageETags = {};
+
+ // Make an AJAX request for each page and append it to the document.
+ this._curPageNum = 1;
+
+ var logEl;
+
+ // Control whether log messages are sent to the console
+ if (this._debug) {
+ logEl = function(e) {
+ var rv = e.nodeName + " ";
+ if (e.nodeType == e.TEXT_NODE) {
+ return rv + '("' + e.textContent + '")';
+ }
+ var classDesc = e.className && ("." + e.className.replace(/ /g, "."));
+ var elDesc = "";
+ if (e.id)
+ elDesc = "(#" + e.id + classDesc + ")";
+ else if (classDesc)
+ elDesc = "(" + classDesc + ")";
+ return rv + elDesc;
+ };
+ this.log = function () {
+ if (typeof dump !== undefined) {
+ var msg = Array.prototype.map.call(arguments, function(x) {
+ return (x && x.nodeName) ? logEl(x) : x;
+ }).join(" ");
+ dump("Reader: (Readability) " + msg + "\n");
+ } else if (typeof console !== undefined) {
+ var args = ["Reader: (Readability) "].concat(arguments);
+ console.log.apply(console, args);
+ }
+ };
+ } else {
+ this.log = function () {};
+ }
+}
+
+Readability.prototype = {
+ FLAG_STRIP_UNLIKELYS: 0x1,
+ FLAG_WEIGHT_CLASSES: 0x2,
+ FLAG_CLEAN_CONDITIONALLY: 0x4,
+
+ // Max number of nodes supported by this parser. Default: 0 (no limit)
+ DEFAULT_MAX_ELEMS_TO_PARSE: 0,
+
+ // The number of top candidates to consider when analysing how
+ // tight the competition is among candidates.
+ DEFAULT_N_TOP_CANDIDATES: 5,
+
+ // The maximum number of pages to loop through before we call
+ // it quits and just show a link.
+ DEFAULT_MAX_PAGES: 5,
+
+ // Element tags to score by default.
+ DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),
+
+ // All of the regular expressions in use within readability.
+ // Defined up here so we don't instantiate them repeatedly in loops.
+ REGEXPS: {
+ unlikelyCandidates: /banner|combx|comment|community|disqus|extra|foot|header|menu|modal|related|remark|rss|share|shoutbox|sidebar|skyscraper|sponsor|ad-break|agegate|pagination|pager|popup/i,
+ okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
+ positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
+ negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|modal|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,
+ extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,
+ byline: /byline|author|dateline|writtenby|p-author/i,
+ replaceFonts: /<(\/?)font[^>]*>/gi,
+ normalize: /\s{2,}/g,
+ videos: /\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i,
+ nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
+ prevLink: /(prev|earl|old|new|<|«)/i,
+ whitespace: /^\s*$/,
+ hasContent: /\S$/,
+ },
+
+ DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ],
+
+ ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"],
+
+ /**
+ * Run any post-process modifications to article content as necessary.
+ *
+ * @param Element
+ * @return void
+ **/
+ _postProcessContent: function(articleContent) {
+ // Readability cannot open relative uris so we convert them to absolute uris.
+ this._fixRelativeUris(articleContent);
+ },
+
+ /**
+ * Iterates over a NodeList, calls `filterFn` for each node and removes node
+ * if function returned `true`.
+ *
+ * If function is not passed, removes all the nodes in node list.
+ *
+ * @param NodeList nodeList The no
+ * @param Function filterFn
+ * @return void
+ */
+ _removeNodes: function(nodeList, filterFn) {
+ for (var i = nodeList.length - 1; i >= 0; i--) {
+ var node = nodeList[i];
+ var parentNode = node.parentNode;
+ if (parentNode) {
+ if (!filterFn || filterFn.call(this, node, i, nodeList)) {
+ parentNode.removeChild(node);
+ }
+ }
+ }
+ },
+
+ /**
+ * Iterate over a NodeList, which doesn't natively fully implement the Array
+ * interface.
+ *
+ * For convenience, the current object context is applied to the provided
+ * iterate function.
+ *
+ * @param NodeList nodeList The NodeList.
+ * @param Function fn The iterate function.
+ * @param Boolean backward Whether to use backward iteration.
+ * @return void
+ */
+ _forEachNode: function(nodeList, fn, backward) {
+ Array.prototype.forEach.call(nodeList, fn, this);
+ },
+
+ /**
+ * Iterate over a NodeList, return true if any of the provided iterate
+ * function calls returns true, false otherwise.
+ *
+ * For convenience, the current object context is applied to the
+ * provided iterate function.
+ *
+ * @param NodeList nodeList The NodeList.
+ * @param Function fn The iterate function.
+ * @return Boolean
+ */
+ _someNode: function(nodeList, fn) {
+ return Array.prototype.some.call(nodeList, fn, this);
+ },
+
+ /**
+ * Concat all nodelists passed as arguments.
+ *
+ * @return ...NodeList
+ * @return Array
+ */
+ _concatNodeLists: function() {
+ var slice = Array.prototype.slice;
+ var args = slice.call(arguments);
+ var nodeLists = args.map(function(list) {
+ return slice.call(list);
+ });
+ return Array.prototype.concat.apply([], nodeLists);
+ },
+
+ _getAllNodesWithTag: function(node, tagNames) {
+ if (node.querySelectorAll) {
+ return node.querySelectorAll(tagNames.join(','));
+ }
+ return [].concat.apply([], tagNames.map(function(tag) {
+ var collection = node.getElementsByTagName(tag);
+ return Array.isArray(collection) ? collection : Array.from(collection);
+ }));
+ },
+
+ /**
+ * Converts each <a> and <img> uri in the given element to an absolute URI,
+ * ignoring #ref URIs.
+ *
+ * @param Element
+ * @return void
+ */
+ _fixRelativeUris: function(articleContent) {
+ var scheme = this._uri.scheme;
+ var prePath = this._uri.prePath;
+ var pathBase = this._uri.pathBase;
+
+ function toAbsoluteURI(uri) {
+ // If this is already an absolute URI, return it.
+ if (/^[a-zA-Z][a-zA-Z0-9\+\-\.]*:/.test(uri))
+ return uri;
+
+ // Scheme-rooted relative URI.
+ if (uri.substr(0, 2) == "//")
+ return scheme + "://" + uri.substr(2);
+
+ // Prepath-rooted relative URI.
+ if (uri[0] == "/")
+ return prePath + uri;
+
+ // Dotslash relative URI.
+ if (uri.indexOf("./") === 0)
+ return pathBase + uri.slice(2);
+
+ // Ignore hash URIs:
+ if (uri[0] == "#")
+ return uri;
+
+ // Standard relative URI; add entire path. pathBase already includes a
+ // trailing "/".
+ return pathBase + uri;
+ }
+
+ var links = articleContent.getElementsByTagName("a");
+ this._forEachNode(links, function(link) {
+ var href = link.getAttribute("href");
+ if (href) {
+ // Replace links with javascript: URIs with text content, since
+ // they won't work after scripts have been removed from the page.
+ if (href.indexOf("javascript:") === 0) {
+ var text = this._doc.createTextNode(link.textContent);
+ link.parentNode.replaceChild(text, link);
+ } else {
+ link.setAttribute("href", toAbsoluteURI(href));
+ }
+ }
+ });
+
+ var imgs = articleContent.getElementsByTagName("img");
+ this._forEachNode(imgs, function(img) {
+ var src = img.getAttribute("src");
+ if (src) {
+ img.setAttribute("src", toAbsoluteURI(src));
+ }
+ });
+ },
+
+ /**
+ * Get the article title as an H1.
+ *
+ * @return void
+ **/
+ _getArticleTitle: function() {
+ var doc = this._doc;
+ var curTitle = "";
+ var origTitle = "";
+
+ try {
+ curTitle = origTitle = doc.title;
+
+ // If they had an element with id "title" in their HTML
+ if (typeof curTitle !== "string")
+ curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]);
+ } catch (e) {/* ignore exceptions setting the title. */}
+
+ if (curTitle.match(/ [\|\-] /)) {
+ curTitle = origTitle.replace(/(.*)[\|\-] .*/gi, '$1');
+
+ if (curTitle.split(' ').length < 3)
+ curTitle = origTitle.replace(/[^\|\-]*[\|\-](.*)/gi, '$1');
+ } else if (curTitle.indexOf(': ') !== -1) {
+ // Check if we have an heading containing this exact string, so we
+ // could assume it's the full title.
+ var headings = this._concatNodeLists(
+ doc.getElementsByTagName('h1'),
+ doc.getElementsByTagName('h2')
+ );
+ var match = this._someNode(headings, function(heading) {
+ return heading.textContent === curTitle;
+ });
+
+ // If we don't, let's extract the title out of the original title string.
+ if (!match) {
+ curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1);
+
+ // If the title is now too short, try the first colon instead:
+ if (curTitle.split(' ').length < 3)
+ curTitle = origTitle.substring(origTitle.indexOf(':') + 1);
+ }
+ } else if (curTitle.length > 150 || curTitle.length < 15) {
+ var hOnes = doc.getElementsByTagName('h1');
+
+ if (hOnes.length === 1)
+ curTitle = this._getInnerText(hOnes[0]);
+ }
+
+ curTitle = curTitle.trim();
+
+ if (curTitle.split(' ').length <= 4)
+ curTitle = origTitle;
+
+ return curTitle;
+ },
+
+ /**
+ * Prepare the HTML document for readability to scrape it.
+ * This includes things like stripping javascript, CSS, and handling terrible markup.
+ *
+ * @return void
+ **/
+ _prepDocument: function() {
+ var doc = this._doc;
+
+ // Remove all style tags in head
+ this._removeNodes(doc.getElementsByTagName("style"));
+
+ if (doc.body) {
+ this._replaceBrs(doc.body);
+ }
+
+ this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) {
+ this._setNodeTag(fontNode, "SPAN");
+ });
+ },
+
+ /**
+ * Finds the next element, starting from the given node, and ignoring
+ * whitespace in between. If the given node is an element, the same node is
+ * returned.
+ */
+ _nextElement: function (node) {
+ var next = node;
+ while (next
+ && (next.nodeType != Node.ELEMENT_NODE)
+ && this.REGEXPS.whitespace.test(next.textContent)) {
+ next = next.nextSibling;
+ }
+ return next;
+ },
+
+ /**
+ * Replaces 2 or more successive <br> elements with a single <p>.
+ * Whitespace between <br> elements are ignored. For example:
+ * <div>foo<br>bar<br> <br><br>abc</div>
+ * will become:
+ * <div>foo<br>bar<p>abc</p></div>
+ */
+ _replaceBrs: function (elem) {
+ this._forEachNode(this._getAllNodesWithTag(elem, ["br"]), function(br) {
+ var next = br.nextSibling;
+
+ // Whether 2 or more <br> elements have been found and replaced with a
+ // <p> block.
+ var replaced = false;
+
+ // If we find a <br> chain, remove the <br>s until we hit another element
+ // or non-whitespace. This leaves behind the first <br> in the chain
+ // (which will be replaced with a <p> later).
+ while ((next = this._nextElement(next)) && (next.tagName == "BR")) {
+ replaced = true;
+ var brSibling = next.nextSibling;
+ next.parentNode.removeChild(next);
+ next = brSibling;
+ }
+
+ // If we removed a <br> chain, replace the remaining <br> with a <p>. Add
+ // all sibling nodes as children of the <p> until we hit another <br>
+ // chain.
+ if (replaced) {
+ var p = this._doc.createElement("p");
+ br.parentNode.replaceChild(p, br);
+
+ next = p.nextSibling;
+ while (next) {
+ // If we've hit another <br><br>, we're done adding children to this <p>.
+ if (next.tagName == "BR") {
+ var nextElem = this._nextElement(next);
+ if (nextElem && nextElem.tagName == "BR")
+ break;
+ }
+
+ // Otherwise, make this node a child of the new <p>.
+ var sibling = next.nextSibling;
+ p.appendChild(next);
+ next = sibling;
+ }
+ }
+ });
+ },
+
+ _setNodeTag: function (node, tag) {
+ this.log("_setNodeTag", node, tag);
+ if (node.__JSDOMParser__) {
+ node.localName = tag.toLowerCase();
+ node.tagName = tag.toUpperCase();
+ return node;
+ }
+
+ var replacement = node.ownerDocument.createElement(tag);
+ while (node.firstChild) {
+ replacement.appendChild(node.firstChild);
+ }
+ node.parentNode.replaceChild(replacement, node);
+ if (node.readability)
+ replacement.readability = node.readability;
+
+ for (var i = 0; i < node.attributes.length; i++) {
+ replacement.setAttribute(node.attributes[i].name, node.attributes[i].value);
+ }
+ return replacement;
+ },
+
+ /**
+ * Prepare the article node for display. Clean out any inline styles,
+ * iframes, forms, strip extraneous <p> tags, etc.
+ *
+ * @param Element
+ * @return void
+ **/
+ _prepArticle: function(articleContent) {
+ this._cleanStyles(articleContent);
+
+ // Clean out junk from the article content
+ this._cleanConditionally(articleContent, "form");
+ this._clean(articleContent, "object");
+ this._clean(articleContent, "embed");
+ this._clean(articleContent, "h1");
+ this._clean(articleContent, "footer");
+
+ // If there is only one h2, they are probably using it as a header
+ // and not a subheader, so remove it since we already have a header.
+ if (articleContent.getElementsByTagName('h2').length === 1)
+ this._clean(articleContent, "h2");
+
+ this._clean(articleContent, "iframe");
+ this._cleanHeaders(articleContent);
+
+ // Do these last as the previous stuff may have removed junk
+ // that will affect these
+ this._cleanConditionally(articleContent, "table");
+ this._cleanConditionally(articleContent, "ul");
+ this._cleanConditionally(articleContent, "div");
+
+ // Remove extra paragraphs
+ this._removeNodes(articleContent.getElementsByTagName('p'), function (paragraph) {
+ var imgCount = paragraph.getElementsByTagName('img').length;
+ var embedCount = paragraph.getElementsByTagName('embed').length;
+ var objectCount = paragraph.getElementsByTagName('object').length;
+ // At this point, nasty iframes have been removed, only remain embedded video ones.
+ var iframeCount = paragraph.getElementsByTagName('iframe').length;
+ var totalCount = imgCount + embedCount + objectCount + iframeCount;
+
+ return totalCount === 0 && !this._getInnerText(paragraph, false);
+ });
+
+ this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) {
+ var next = this._nextElement(br.nextSibling);
+ if (next && next.tagName == "P")
+ br.parentNode.removeChild(br);
+ });
+ },
+
+ /**
+ * Initialize a node with the readability object. Also checks the
+ * className/id for special names to add to its score.
+ *
+ * @param Element
+ * @return void
+ **/
+ _initializeNode: function(node) {
+ node.readability = {"contentScore": 0};
+
+ switch (node.tagName) {
+ case 'DIV':
+ node.readability.contentScore += 5;
+ break;
+
+ case 'PRE':
+ case 'TD':
+ case 'BLOCKQUOTE':
+ node.readability.contentScore += 3;
+ break;
+
+ case 'ADDRESS':
+ case 'OL':
+ case 'UL':
+ case 'DL':
+ case 'DD':
+ case 'DT':
+ case 'LI':
+ case 'FORM':
+ node.readability.contentScore -= 3;
+ break;
+
+ case 'H1':
+ case 'H2':
+ case 'H3':
+ case 'H4':
+ case 'H5':
+ case 'H6':
+ case 'TH':
+ node.readability.contentScore -= 5;
+ break;
+ }
+
+ node.readability.contentScore += this._getClassWeight(node);
+ },
+
+ _removeAndGetNext: function(node) {
+ var nextNode = this._getNextNode(node, true);
+ node.parentNode.removeChild(node);
+ return nextNode;
+ },
+
+ /**
+ * Traverse the DOM from node to node, starting at the node passed in.
+ * Pass true for the second parameter to indicate this node itself
+ * (and its kids) are going away, and we want the next node over.
+ *
+ * Calling this in a loop will traverse the DOM depth-first.
+ */
+ _getNextNode: function(node, ignoreSelfAndKids) {
+ // First check for kids if those aren't being ignored
+ if (!ignoreSelfAndKids && node.firstElementChild) {
+ return node.firstElementChild;
+ }
+ // Then for siblings...
+ if (node.nextElementSibling) {
+ return node.nextElementSibling;
+ }
+ // And finally, move up the parent chain *and* find a sibling
+ // (because this is depth-first traversal, we will have already
+ // seen the parent nodes themselves).
+ do {
+ node = node.parentNode;
+ } while (node && !node.nextElementSibling);
+ return node && node.nextElementSibling;
+ },
+
+ /**
+ * Like _getNextNode, but for DOM implementations with no
+ * firstElementChild/nextElementSibling functionality...
+ */
+ _getNextNodeNoElementProperties: function(node, ignoreSelfAndKids) {
+ function nextSiblingEl(n) {
+ do {
+ n = n.nextSibling;
+ } while (n && n.nodeType !== n.ELEMENT_NODE);
+ return n;
+ }
+ // First check for kids if those aren't being ignored
+ if (!ignoreSelfAndKids && node.children[0]) {
+ return node.children[0];
+ }
+ // Then for siblings...
+ var next = nextSiblingEl(node);
+ if (next) {
+ return next;
+ }
+ // And finally, move up the parent chain *and* find a sibling
+ // (because this is depth-first traversal, we will have already
+ // seen the parent nodes themselves).
+ do {
+ node = node.parentNode;
+ if (node)
+ next = nextSiblingEl(node);
+ } while (node && !next);
+ return node && next;
+ },
+
+ _checkByline: function(node, matchString) {
+ if (this._articleByline) {
+ return false;
+ }
+
+ if (node.getAttribute !== undefined) {
+ var rel = node.getAttribute("rel");
+ }
+
+ if ((rel === "author" || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) {
+ this._articleByline = node.textContent.trim();
+ return true;
+ }
+
+ return false;
+ },
+
+ _getNodeAncestors: function(node, maxDepth) {
+ maxDepth = maxDepth || 0;
+ var i = 0, ancestors = [];
+ while (node.parentNode) {
+ ancestors.push(node.parentNode);
+ if (maxDepth && ++i === maxDepth)
+ break;
+ node = node.parentNode;
+ }
+ return ancestors;
+ },
+
+ /***
+ * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
+ * most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
+ *
+ * @param page a document to run upon. Needs to be a full document, complete with body.
+ * @return Element
+ **/
+ _grabArticle: function (page) {
+ this.log("**** grabArticle ****");
+ var doc = this._doc;
+ var isPaging = (page !== null ? true: false);
+ page = page ? page : this._doc.body;
+
+ // We can't grab an article if we don't have a page!
+ if (!page) {
+ this.log("No body found in document. Abort.");
+ return null;
+ }
+
+ var pageCacheHtml = page.innerHTML;
+
+ // Check if any "dir" is set on the toplevel document element
+ this._articleDir = doc.documentElement.getAttribute("dir");
+
+ while (true) {
+ var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS);
+
+ // First, node prepping. Trash nodes that look cruddy (like ones with the
+ // class name "comment", etc), and turn divs into P tags where they have been
+ // used inappropriately (as in, where they contain no other block level elements.)
+ var elementsToScore = [];
+ var node = this._doc.documentElement;
+
+ while (node) {
+ var matchString = node.className + " " + node.id;
+
+ // Check to see if this node is a byline, and remove it if it is.
+ if (this._checkByline(node, matchString)) {
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+
+ // Remove unlikely candidates
+ if (stripUnlikelyCandidates) {
+ if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
+ !this.REGEXPS.okMaybeItsACandidate.test(matchString) &&
+ node.tagName !== "BODY" &&
+ node.tagName !== "A") {
+ this.log("Removing unlikely candidate - " + matchString);
+ node = this._removeAndGetNext(node);
+ continue;
+ }
+ }
+
+ if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) {
+ elementsToScore.push(node);
+ }
+
+ // Turn all divs that don't have children block level elements into p's
+ if (node.tagName === "DIV") {
+ // Sites like http://mobile.slate.com encloses each paragraph with a DIV
+ // element. DIVs with only a P element inside and no text content can be
+ // safely converted into plain P elements to avoid confusing the scoring
+ // algorithm with DIVs with are, in practice, paragraphs.
+ if (this._hasSinglePInsideElement(node)) {
+ var newNode = node.children[0];
+ node.parentNode.replaceChild(newNode, node);
+ node = newNode;
+ } else if (!this._hasChildBlockElement(node)) {
+ node = this._setNodeTag(node, "P");
+ elementsToScore.push(node);
+ } else {
+ // EXPERIMENTAL
+ this._forEachNode(node.childNodes, function(childNode) {
+ if (childNode.nodeType === Node.TEXT_NODE) {
+ var p = doc.createElement('p');
+ p.textContent = childNode.textContent;
+ p.style.display = 'inline';
+ p.className = 'readability-styled';
+ node.replaceChild(p, childNode);
+ }
+ });
+ }
+ }
+ node = this._getNextNode(node);
+ }
+
+ /**
+ * Loop through all paragraphs, and assign a score to them based on how content-y they look.
+ * Then add their score to their parent node.
+ *
+ * A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
+ **/
+ var candidates = [];
+ this._forEachNode(elementsToScore, function(elementToScore) {
+ if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === 'undefined')
+ return;
+
+ // If this paragraph is less than 25 characters, don't even count it.
+ var innerText = this._getInnerText(elementToScore);
+ if (innerText.length < 25)
+ return;
+
+ // Exclude nodes with no ancestor.
+ var ancestors = this._getNodeAncestors(elementToScore, 3);
+ if (ancestors.length === 0)
+ return;
+
+ var contentScore = 0;
+
+ // Add a point for the paragraph itself as a base.
+ contentScore += 1;
+
+ // Add points for any commas within this paragraph.
+ contentScore += innerText.split(',').length;
+
+ // For every 100 characters in this paragraph, add another point. Up to 3 points.
+ contentScore += Math.min(Math.floor(innerText.length / 100), 3);
+
+ // Initialize and score ancestors.
+ this._forEachNode(ancestors, function(ancestor, level) {
+ if (!ancestor.tagName)
+ return;
+
+ if (typeof(ancestor.readability) === 'undefined') {
+ this._initializeNode(ancestor);
+ candidates.push(ancestor);
+ }
+
+ // Node score divider:
+ // - parent: 1 (no division)
+ // - grandparent: 2
+ // - great grandparent+: ancestor level * 3
+ if (level === 0)
+ var scoreDivider = 1;
+ else if (level === 1)
+ scoreDivider = 2;
+ else
+ scoreDivider = level * 3;
+ ancestor.readability.contentScore += contentScore / scoreDivider;
+ });
+ });
+
+ // After we've calculated scores, loop through all of the possible
+ // candidate nodes we found and find the one with the highest score.
+ var topCandidates = [];
+ for (var c = 0, cl = candidates.length; c < cl; c += 1) {
+ var candidate = candidates[c];
+
+ // Scale the final candidates score based on link density. Good content
+ // should have a relatively small link density (5% or less) and be mostly
+ // unaffected by this operation.
+ var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate));
+ candidate.readability.contentScore = candidateScore;
+
+ this.log('Candidate:', candidate, "with score " + candidateScore);
+
+ for (var t = 0; t < this._nbTopCandidates; t++) {
+ var aTopCandidate = topCandidates[t];
+
+ if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) {
+ topCandidates.splice(t, 0, candidate);
+ if (topCandidates.length > this._nbTopCandidates)
+ topCandidates.pop();
+ break;
+ }
+ }
+ }
+
+ var topCandidate = topCandidates[0] || null;
+ var neededToCreateTopCandidate = false;
+
+ // If we still have no top candidate, just use the body as a last resort.
+ // We also have to copy the body node so it is something we can modify.
+ if (topCandidate === null || topCandidate.tagName === "BODY") {
+ // Move all of the page's children into topCandidate
+ topCandidate = doc.createElement("DIV");
+ neededToCreateTopCandidate = true;
+ // Move everything (not just elements, also text nodes etc.) into the container
+ // so we even include text directly in the body:
+ var kids = page.childNodes;
+ while (kids.length) {
+ this.log("Moving child out:", kids[0]);
+ topCandidate.appendChild(kids[0]);
+ }
+
+ page.appendChild(topCandidate);
+
+ this._initializeNode(topCandidate);
+ } else if (topCandidate) {
+ // Because of our bonus system, parents of candidates might have scores
+ // themselves. They get half of the node. There won't be nodes with higher
+ // scores than our topCandidate, but if we see the score going *up* in the first
+ // few steps up the tree, that's a decent sign that there might be more content
+ // lurking in other places that we want to unify in. The sibling stuff
+ // below does some of that - but only if we've looked high enough up the DOM
+ // tree.
+ var parentOfTopCandidate = topCandidate.parentNode;
+ var lastScore = topCandidate.readability.contentScore;
+ // The scores shouldn't get too low.
+ var scoreThreshold = lastScore / 3;
+ while (parentOfTopCandidate && parentOfTopCandidate.readability) {
+ var parentScore = parentOfTopCandidate.readability.contentScore;
+ if (parentScore < scoreThreshold)
+ break;
+ if (parentScore > lastScore) {
+ // Alright! We found a better parent to use.
+ topCandidate = parentOfTopCandidate;
+ break;
+ }
+ lastScore = parentOfTopCandidate.readability.contentScore;
+ parentOfTopCandidate = parentOfTopCandidate.parentNode;
+ }
+ }
+
+ // Now that we have the top candidate, look through its siblings for content
+ // that might also be related. Things like preambles, content split by ads
+ // that we removed, etc.
+ var articleContent = doc.createElement("DIV");
+ if (isPaging)
+ articleContent.id = "readability-content";
+
+ var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2);
+ var siblings = topCandidate.parentNode.children;
+
+ for (var s = 0, sl = siblings.length; s < sl; s++) {
+ var sibling = siblings[s];
+ var append = false;
+
+ this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : '');
+ this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : 'Unknown');
+
+ if (sibling === topCandidate) {
+ append = true;
+ } else {
+ var contentBonus = 0;
+
+ // Give a bonus if sibling nodes and top candidates have the example same classname
+ if (sibling.className === topCandidate.className && topCandidate.className !== "")
+ contentBonus += topCandidate.readability.contentScore * 0.2;
+
+ if (sibling.readability &&
+ ((sibling.readability.contentScore + contentBonus) >= siblingScoreThreshold)) {
+ append = true;
+ } else if (sibling.nodeName === "P") {
+ var linkDensity = this._getLinkDensity(sibling);
+ var nodeContent = this._getInnerText(sibling);
+ var nodeLength = nodeContent.length;
+
+ if (nodeLength > 80 && linkDensity < 0.25) {
+ append = true;
+ } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 &&
+ nodeContent.search(/\.( |$)/) !== -1) {
+ append = true;
+ }
+ }
+ }
+
+ if (append) {
+ this.log("Appending node:", sibling);
+
+ if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) {
+ // We have a node that isn't a common block level element, like a form or td tag.
+ // Turn it into a div so it doesn't get filtered out later by accident.
+ this.log("Altering sibling:", sibling, 'to div.');
+
+ sibling = this._setNodeTag(sibling, "DIV");
+ }
+
+ articleContent.appendChild(sibling);
+ // siblings is a reference to the children array, and
+ // sibling is removed from the array when we call appendChild().
+ // As a result, we must revisit this index since the nodes
+ // have been shifted.
+ s -= 1;
+ sl -= 1;
+ }
+ }
+
+ if (this._debug)
+ this.log("Article content pre-prep: " + articleContent.innerHTML);
+ // So we have all of the content that we need. Now we clean it up for presentation.
+ this._prepArticle(articleContent);
+ if (this._debug)
+ this.log("Article content post-prep: " + articleContent.innerHTML);
+
+ if (this._curPageNum === 1) {
+ if (neededToCreateTopCandidate) {
+ // We already created a fake div thing, and there wouldn't have been any siblings left
+ // for the previous loop, so there's no point trying to create a new div, and then
+ // move all the children over. Just assign IDs and class names here. No need to append
+ // because that already happened anyway.
+ topCandidate.id = "readability-page-1";
+ topCandidate.className = "page";
+ } else {
+ var div = doc.createElement("DIV");
+ div.id = "readability-page-1";
+ div.className = "page";
+ var children = articleContent.childNodes;
+ while (children.length) {
+ div.appendChild(children[0]);
+ }
+ articleContent.appendChild(div);
+ }
+ }
+
+ if (this._debug)
+ this.log("Article content after paging: " + articleContent.innerHTML);
+
+ // Now that we've gone through the full algorithm, check to see if
+ // we got any meaningful content. If we didn't, we may need to re-run
+ // grabArticle with different flags set. This gives us a higher likelihood of
+ // finding the content, and the sieve approach gives us a higher likelihood of
+ // finding the -right- content.
+ if (this._getInnerText(articleContent, true).length < 500) {
+ page.innerHTML = pageCacheHtml;
+
+ if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) {
+ this._removeFlag(this.FLAG_STRIP_UNLIKELYS);
+ } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) {
+ this._removeFlag(this.FLAG_WEIGHT_CLASSES);
+ } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) {
+ this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY);
+ } else {
+ return null;
+ }
+ } else {
+ return articleContent;
+ }
+ }
+ },
+
+ /**
+ * Check whether the input string could be a byline.
+ * This verifies that the input is a string, and that the length
+ * is less than 100 chars.
+ *
+ * @param possibleByline {string} - a string to check whether its a byline.
+ * @return Boolean - whether the input string is a byline.
+ */
+ _isValidByline: function(byline) {
+ if (typeof byline == 'string' || byline instanceof String) {
+ byline = byline.trim();
+ return (byline.length > 0) && (byline.length < 100);
+ }
+ return false;
+ },
+
+ /**
+ * Attempts to get excerpt and byline metadata for the article.
+ *
+ * @return Object with optional "excerpt" and "byline" properties
+ */
+ _getArticleMetadata: function() {
+ var metadata = {};
+ var values = {};
+ var metaElements = this._doc.getElementsByTagName("meta");
+
+ // Match "description", or Twitter's "twitter:description" (Cards)
+ // in name attribute.
+ var namePattern = /^\s*((twitter)\s*:\s*)?(description|title)\s*$/gi;
+
+ // Match Facebook's Open Graph title & description properties.
+ var propertyPattern = /^\s*og\s*:\s*(description|title)\s*$/gi;
+
+ // Find description tags.
+ this._forEachNode(metaElements, function(element) {
+ var elementName = element.getAttribute("name");
+ var elementProperty = element.getAttribute("property");
+
+ if ([elementName, elementProperty].indexOf("author") !== -1) {
+ metadata.byline = element.getAttribute("content");
+ return;
+ }
+
+ var name = null;
+ if (namePattern.test(elementName)) {
+ name = elementName;
+ } else if (propertyPattern.test(elementProperty)) {
+ name = elementProperty;
+ }
+
+ if (name) {
+ var content = element.getAttribute("content");
+ if (content) {
+ // Convert to lowercase and remove any whitespace
+ // so we can match below.
+ name = name.toLowerCase().replace(/\s/g, '');
+ values[name] = content.trim();
+ }
+ }
+ });
+
+ if ("description" in values) {
+ metadata.excerpt = values["description"];
+ } else if ("og:description" in values) {
+ // Use facebook open graph description.
+ metadata.excerpt = values["og:description"];
+ } else if ("twitter:description" in values) {
+ // Use twitter cards description.
+ metadata.excerpt = values["twitter:description"];
+ }
+
+ if ("og:title" in values) {
+ // Use facebook open graph title.
+ metadata.title = values["og:title"];
+ } else if ("twitter:title" in values) {
+ // Use twitter cards title.
+ metadata.title = values["twitter:title"];
+ }
+
+ return metadata;
+ },
+
+ /**
+ * Removes script tags from the document.
+ *
+ * @param Element
+ **/
+ _removeScripts: function(doc) {
+ this._removeNodes(doc.getElementsByTagName('script'), function(scriptNode) {
+ scriptNode.nodeValue = "";
+ scriptNode.removeAttribute('src');
+ return true;
+ });
+ this._removeNodes(doc.getElementsByTagName('noscript'));
+ },
+
+ /**
+ * Check if this node has only whitespace and a single P element
+ * Returns false if the DIV node contains non-empty text nodes
+ * or if it contains no P or more than 1 element.
+ *
+ * @param Element
+ **/
+ _hasSinglePInsideElement: function(element) {
+ // There should be exactly 1 element child which is a P:
+ if (element.children.length != 1 || element.children[0].tagName !== "P") {
+ return false;
+ }
+
+ // And there should be no text nodes with real content
+ return !this._someNode(element.childNodes, function(node) {
+ return node.nodeType === Node.TEXT_NODE &&
+ this.REGEXPS.hasContent.test(node.textContent);
+ });
+ },
+
+ /**
+ * Determine whether element has any children block level elements.
+ *
+ * @param Element
+ */
+ _hasChildBlockElement: function (element) {
+ return this._someNode(element.childNodes, function(node) {
+ return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 ||
+ this._hasChildBlockElement(node);
+ });
+ },
+
+ /**
+ * Get the inner text of a node - cross browser compatibly.
+ * This also strips out any excess whitespace to be found.
+ *
+ * @param Element
+ * @param Boolean normalizeSpaces (default: true)
+ * @return string
+ **/
+ _getInnerText: function(e, normalizeSpaces) {
+ normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
+ var textContent = e.textContent.trim();
+
+ if (normalizeSpaces) {
+ return textContent.replace(this.REGEXPS.normalize, " ");
+ }
+ return textContent;
+ },
+
+ /**
+ * Get the number of times a string s appears in the node e.
+ *
+ * @param Element
+ * @param string - what to split on. Default is ","
+ * @return number (integer)
+ **/
+ _getCharCount: function(e, s) {
+ s = s || ",";
+ return this._getInnerText(e).split(s).length - 1;
+ },
+
+ /**
+ * Remove the style attribute on every e and under.
+ * TODO: Test if getElementsByTagName(*) is faster.
+ *
+ * @param Element
+ * @return void
+ **/
+ _cleanStyles: function(e) {
+ e = e || this._doc;
+ if (!e)
+ return;
+ var cur = e.firstChild;
+
+ // Remove any root styles, if we're able.
+ if (typeof e.removeAttribute === 'function' && e.className !== 'readability-styled')
+ e.removeAttribute('style');
+
+ // Go until there are no more child nodes
+ while (cur !== null) {
+ if (cur.nodeType === cur.ELEMENT_NODE) {
+ // Remove style attribute(s) :
+ if (cur.className !== "readability-styled")
+ cur.removeAttribute("style");
+
+ this._cleanStyles(cur);
+ }
+
+ cur = cur.nextSibling;
+ }
+ },
+
+ /**
+ * Get the density of links as a percentage of the content
+ * This is the amount of text that is inside a link divided by the total text in the node.
+ *
+ * @param Element
+ * @return number (float)
+ **/
+ _getLinkDensity: function(element) {
+ var textLength = this._getInnerText(element).length;
+ if (textLength === 0)
+ return 0;
+
+ var linkLength = 0;
+
+ // XXX implement _reduceNodeList?
+ this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
+ linkLength += this._getInnerText(linkNode).length;
+ });
+
+ return linkLength / textLength;
+ },
+
+ /**
+ * Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness.
+ *
+ * @author Dan Lacy
+ * @return string the base url
+ **/
+ _findBaseUrl: function() {
+ var uri = this._uri;
+ var noUrlParams = uri.path.split("?")[0];
+ var urlSlashes = noUrlParams.split("/").reverse();
+ var cleanedSegments = [];
+ var possibleType = "";
+
+ for (var i = 0, slashLen = urlSlashes.length; i < slashLen; i += 1) {
+ var segment = urlSlashes[i];
+
+ // Split off and save anything that looks like a file type.
+ if (segment.indexOf(".") !== -1) {
+ possibleType = segment.split(".")[1];
+
+ // If the type isn't alpha-only, it's probably not actually a file extension.
+ if (!possibleType.match(/[^a-zA-Z]/))
+ segment = segment.split(".")[0];
+ }
+
+ // EW-CMS specific segment replacement. Ugly.
+ // Example: http://www.ew.com/ew/article/0,,20313460_20369436,00.html
+ if (segment.indexOf(',00') !== -1)
+ segment = segment.replace(',00', '');
+
+ // If our first or second segment has anything looking like a page number, remove it.
+ if (segment.match(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i) && ((i === 1) || (i === 0)))
+ segment = segment.replace(/((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$/i, "");
+
+ var del = false;
+
+ // If this is purely a number, and it's the first or second segment,
+ // it's probably a page number. Remove it.
+ if (i < 2 && segment.match(/^\d{1,2}$/))
+ del = true;
+
+ // If this is the first segment and it's just "index", remove it.
+ if (i === 0 && segment.toLowerCase() === "index")
+ del = true;
+
+ // If our first or second segment is smaller than 3 characters,
+ // and the first segment was purely alphas, remove it.
+ if (i < 2 && segment.length < 3 && !urlSlashes[0].match(/[a-z]/i))
+ del = true;
+
+ // If it's not marked for deletion, push it to cleanedSegments.
+ if (!del)
+ cleanedSegments.push(segment);
+ }
+
+ // This is our final, cleaned, base article URL.
+ return uri.scheme + "://" + uri.host + cleanedSegments.reverse().join("/");
+ },
+
+ /**
+ * Look for any paging links that may occur within the document.
+ *
+ * @param body
+ * @return object (array)
+ **/
+ _findNextPageLink: function(elem) {
+ var uri = this._uri;
+ var possiblePages = {};
+ var allLinks = elem.getElementsByTagName('a');
+ var articleBaseUrl = this._findBaseUrl();
+
+ // Loop through all links, looking for hints that they may be next-page links.
+ // Things like having "page" in their textContent, className or id, or being a child
+ // of a node with a page-y className or id.
+ //
+ // Also possible: levenshtein distance? longest common subsequence?
+ //
+ // After we do that, assign each page a score, and
+ for (var i = 0, il = allLinks.length; i < il; i += 1) {
+ var link = allLinks[i];
+ var linkHref = allLinks[i].href.replace(/#.*$/, '').replace(/\/$/, '');
+
+ // If we've already seen this page, ignore it.
+ if (linkHref === "" ||
+ linkHref === articleBaseUrl ||
+ linkHref === uri.spec ||
+ linkHref in this._parsedPages) {
+ continue;
+ }
+
+ // If it's on a different domain, skip it.
+ if (uri.host !== linkHref.split(/\/+/g)[1])
+ continue;
+
+ var linkText = this._getInnerText(link);
+
+ // If the linkText looks like it's not the next page, skip it.
+ if (linkText.match(this.REGEXPS.extraneous) || linkText.length > 25)
+ continue;
+
+ // If the leftovers of the URL after removing the base URL don't contain
+ // any digits, it's certainly not a next page link.
+ var linkHrefLeftover = linkHref.replace(articleBaseUrl, '');
+ if (!linkHrefLeftover.match(/\d/))
+ continue;
+
+ if (!(linkHref in possiblePages)) {
+ possiblePages[linkHref] = {"score": 0, "linkText": linkText, "href": linkHref};
+ } else {
+ possiblePages[linkHref].linkText += ' | ' + linkText;
+ }
+
+ var linkObj = possiblePages[linkHref];
+
+ // If the articleBaseUrl isn't part of this URL, penalize this link. It could
+ // still be the link, but the odds are lower.
+ // Example: http://www.actionscript.org/resources/articles/745/1/JavaScript-and-VBScript-Injection-in-ActionScript-3/Page1.html
+ if (linkHref.indexOf(articleBaseUrl) !== 0)
+ linkObj.score -= 25;
+
+ var linkData = linkText + ' ' + link.className + ' ' + link.id;
+ if (linkData.match(this.REGEXPS.nextLink))
+ linkObj.score += 50;
+
+ if (linkData.match(/pag(e|ing|inat)/i))
+ linkObj.score += 25;
+
+ if (linkData.match(/(first|last)/i)) {
+ // -65 is enough to negate any bonuses gotten from a > or » in the text,
+ // If we already matched on "next", last is probably fine.
+ // If we didn't, then it's bad. Penalize.
+ if (!linkObj.linkText.match(this.REGEXPS.nextLink))
+ linkObj.score -= 65;
+ }
+
+ if (linkData.match(this.REGEXPS.negative) || linkData.match(this.REGEXPS.extraneous))
+ linkObj.score -= 50;
+
+ if (linkData.match(this.REGEXPS.prevLink))
+ linkObj.score -= 200;
+
+ // If a parentNode contains page or paging or paginat
+ var parentNode = link.parentNode;
+ var positiveNodeMatch = false;
+ var negativeNodeMatch = false;
+
+ while (parentNode) {
+ var parentNodeClassAndId = parentNode.className + ' ' + parentNode.id;
+
+ if (!positiveNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(/pag(e|ing|inat)/i)) {
+ positiveNodeMatch = true;
+ linkObj.score += 25;
+ }
+
+ if (!negativeNodeMatch && parentNodeClassAndId && parentNodeClassAndId.match(this.REGEXPS.negative)) {
+ // If this is just something like "footer", give it a negative.
+ // If it's something like "body-and-footer", leave it be.
+ if (!parentNodeClassAndId.match(this.REGEXPS.positive)) {
+ linkObj.score -= 25;
+ negativeNodeMatch = true;
+ }
+ }
+
+ parentNode = parentNode.parentNode;
+ }
+
+ // If the URL looks like it has paging in it, add to the score.
+ // Things like /page/2/, /pagenum/2, ?p=3, ?page=11, ?pagination=34
+ if (linkHref.match(/p(a|g|ag)?(e|ing|ination)?(=|\/)[0-9]{1,2}/i) || linkHref.match(/(page|paging)/i))
+ linkObj.score += 25;
+
+ // If the URL contains negative values, give a slight decrease.
+ if (linkHref.match(this.REGEXPS.extraneous))
+ linkObj.score -= 15;
+
+ /**
+ * Minor punishment to anything that doesn't match our current URL.
+ * NOTE: I'm finding this to cause more harm than good where something is exactly 50 points.
+ * Dan, can you show me a counterexample where this is necessary?
+ * if (linkHref.indexOf(window.location.href) !== 0) {
+ * linkObj.score -= 1;
+ * }
+ **/
+
+ // If the link text can be parsed as a number, give it a minor bonus, with a slight
+ // bias towards lower numbered pages. This is so that pages that might not have 'next'
+ // in their text can still get scored, and sorted properly by score.
+ var linkTextAsNumber = parseInt(linkText, 10);
+ if (linkTextAsNumber) {
+ // Punish 1 since we're either already there, or it's probably
+ // before what we want anyways.
+ if (linkTextAsNumber === 1) {
+ linkObj.score -= 10;
+ } else {
+ linkObj.score += Math.max(0, 10 - linkTextAsNumber);
+ }
+ }
+ }
+
+ // Loop thrugh all of our possible pages from above and find our top
+ // candidate for the next page URL. Require at least a score of 50, which
+ // is a relatively high confidence that this page is the next link.
+ var topPage = null;
+ for (var page in possiblePages) {
+ if (possiblePages.hasOwnProperty(page)) {
+ if (possiblePages[page].score >= 50 &&
+ (!topPage || topPage.score < possiblePages[page].score))
+ topPage = possiblePages[page];
+ }
+ }
+
+ var nextHref = null;
+ if (topPage) {
+ nextHref = topPage.href.replace(/\/$/, '');
+
+ this.log('NEXT PAGE IS ' + nextHref);
+ this._parsedPages[nextHref] = true;
+ }
+ return nextHref;
+ },
+
+ _successfulRequest: function(request) {
+ return (request.status >= 200 && request.status < 300) ||
+ request.status === 304 ||
+ (request.status === 0 && request.responseText);
+ },
+
+ _ajax: function(url, options) {
+ var request = new XMLHttpRequest();
+
+ function respondToReadyState(readyState) {
+ if (request.readyState === 4) {
+ if (this._successfulRequest(request)) {
+ if (options.success)
+ options.success(request);
+ } else if (options.error) {
+ options.error(request);
+ }
+ }
+ }
+
+ if (typeof options === 'undefined')
+ options = {};
+
+ request.onreadystatechange = respondToReadyState;
+
+ request.open('get', url, true);
+ request.setRequestHeader('Accept', 'text/html');
+
+ try {
+ request.send(options.postBody);
+ } catch (e) {
+ if (options.error)
+ options.error();
+ }
+
+ return request;
+ },
+
+ _appendNextPage: function(nextPageLink) {
+ var doc = this._doc;
+ this._curPageNum += 1;
+
+ var articlePage = doc.createElement("DIV");
+ articlePage.id = 'readability-page-' + this._curPageNum;
+ articlePage.className = 'page';
+ articlePage.innerHTML = '<p class="page-separator" title="Page ' + this._curPageNum + '">&sect;</p>';
+
+ doc.getElementById("readability-content").appendChild(articlePage);
+
+ if (this._curPageNum > this._maxPages) {
+ var nextPageMarkup = "<div style='text-align: center'><a href='" + nextPageLink + "'>View Next Page</a></div>";
+ articlePage.innerHTML = articlePage.innerHTML + nextPageMarkup;
+ return;
+ }
+
+ // Now that we've built the article page DOM element, get the page content
+ // asynchronously and load the cleaned content into the div we created for it.
+ (function(pageUrl, thisPage) {
+ this._ajax(pageUrl, {
+ success: function(r) {
+
+ // First, check to see if we have a matching ETag in headers - if we do, this is a duplicate page.
+ var eTag = r.getResponseHeader('ETag');
+ if (eTag) {
+ if (eTag in this._pageETags) {
+ this.log("Exact duplicate page found via ETag. Aborting.");
+ articlePage.style.display = 'none';
+ return;
+ }
+ this._pageETags[eTag] = 1;
+ }
+
+ // TODO: this ends up doubling up page numbers on NYTimes articles. Need to generically parse those away.
+ var page = doc.createElement("DIV");
+
+ // Do some preprocessing to our HTML to make it ready for appending.
+ // - Remove any script tags. Swap and reswap newlines with a unicode
+ // character because multiline regex doesn't work in javascript.
+ // - Turn any noscript tags into divs so that we can parse them. This
+ // allows us to find any next page links hidden via javascript.
+ // - Turn all double br's into p's - was handled by prepDocument in the original view.
+ // Maybe in the future abstract out prepDocument to work for both the original document
+ // and AJAX-added pages.
+ var responseHtml = r.responseText.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
+ responseHtml = responseHtml.replace(/\n/g, '\uffff').replace(/<script.*?>.*?<\/script>/gi, '');
+ responseHtml = responseHtml.replace(/\uffff/g, '\n').replace(/<(\/?)noscript/gi, '<$1div');
+ responseHtml = responseHtml.replace(this.REGEXPS.replaceFonts, '<$1span>');
+
+ page.innerHTML = responseHtml;
+ this._replaceBrs(page);
+
+ // Reset all flags for the next page, as they will search through it and
+ // disable as necessary at the end of grabArticle.
+ this._flags = 0x1 | 0x2 | 0x4;
+
+ var secondNextPageLink = this._findNextPageLink(page);
+
+ // NOTE: if we end up supporting _appendNextPage(), we'll need to
+ // change this call to be async
+ var content = this._grabArticle(page);
+
+ if (!content) {
+ this.log("No content found in page to append. Aborting.");
+ return;
+ }
+
+ // Anti-duplicate mechanism. Essentially, get the first paragraph of our new page.
+ // Compare it against all of the the previous document's we've gotten. If the previous
+ // document contains exactly the innerHTML of this first paragraph, it's probably a duplicate.
+ var firstP = content.getElementsByTagName("P").length ? content.getElementsByTagName("P")[0] : null;
+ if (firstP && firstP.innerHTML.length > 100) {
+ for (var i = 1; i <= this._curPageNum; i += 1) {
+ var rPage = doc.getElementById('readability-page-' + i);
+ if (rPage && rPage.innerHTML.indexOf(firstP.innerHTML) !== -1) {
+ this.log('Duplicate of page ' + i + ' - skipping.');
+ articlePage.style.display = 'none';
+ this._parsedPages[pageUrl] = true;
+ return;
+ }
+ }
+ }
+
+ this._removeScripts(content);
+
+ thisPage.innerHTML = thisPage.innerHTML + content.innerHTML;
+
+ // After the page has rendered, post process the content. This delay is necessary because,
+ // in webkit at least, offsetWidth is not set in time to determine image width. We have to
+ // wait a little bit for reflow to finish before we can fix floating images.
+ setTimeout((function() {
+ this._postProcessContent(thisPage);
+ }).bind(this), 500);
+
+
+ if (secondNextPageLink)
+ this._appendNextPage(secondNextPageLink);
+ }
+ });
+ }).bind(this)(nextPageLink, articlePage);
+ },
+
+ /**
+ * Get an elements class/id weight. Uses regular expressions to tell if this
+ * element looks good or bad.
+ *
+ * @param Element
+ * @return number (Integer)
+ **/
+ _getClassWeight: function(e) {
+ if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES))
+ return 0;
+
+ var weight = 0;
+
+ // Look for a special classname
+ if (typeof(e.className) === 'string' && e.className !== '') {
+ if (this.REGEXPS.negative.test(e.className))
+ weight -= 25;
+
+ if (this.REGEXPS.positive.test(e.className))
+ weight += 25;
+ }
+
+ // Look for a special ID
+ if (typeof(e.id) === 'string' && e.id !== '') {
+ if (this.REGEXPS.negative.test(e.id))
+ weight -= 25;
+
+ if (this.REGEXPS.positive.test(e.id))
+ weight += 25;
+ }
+
+ return weight;
+ },
+
+ /**
+ * Clean a node of all elements of type "tag".
+ * (Unless it's a youtube/vimeo video. People love movies.)
+ *
+ * @param Element
+ * @param string tag to clean
+ * @return void
+ **/
+ _clean: function(e, tag) {
+ var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1;
+
+ this._removeNodes(e.getElementsByTagName(tag), function(element) {
+ // Allow youtube and vimeo videos through as people usually want to see those.
+ if (isEmbed) {
+ var attributeValues = [].map.call(element.attributes, function(attr) {
+ return attr.value;
+ }).join("|");
+
+ // First, check the elements attributes to see if any of them contain youtube or vimeo
+ if (this.REGEXPS.videos.test(attributeValues))
+ return false;
+
+ // Then check the elements inside this element for the same.
+ if (this.REGEXPS.videos.test(element.innerHTML))
+ return false;
+ }
+
+ return true;
+ });
+ },
+
+ /**
+ * Check if a given node has one of its ancestor tag name matching the
+ * provided one.
+ * @param HTMLElement node
+ * @param String tagName
+ * @param Number maxDepth
+ * @return Boolean
+ */
+ _hasAncestorTag: function(node, tagName, maxDepth) {
+ maxDepth = maxDepth || 3;
+ tagName = tagName.toUpperCase();
+ var depth = 0;
+ while (node.parentNode) {
+ if (depth > maxDepth)
+ return false;
+ if (node.parentNode.tagName === tagName)
+ return true;
+ node = node.parentNode;
+ depth++;
+ }
+ return false;
+ },
+
+ /**
+ * Clean an element of all tags of type "tag" if they look fishy.
+ * "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
+ *
+ * @return void
+ **/
+ _cleanConditionally: function(e, tag) {
+ if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY))
+ return;
+
+ var isList = tag === "ul" || tag === "ol";
+
+ // Gather counts for other typical elements embedded within.
+ // Traverse backwards so we can remove nodes at the same time
+ // without effecting the traversal.
+ //
+ // TODO: Consider taking into account original contentScore here.
+ this._removeNodes(e.getElementsByTagName(tag), function(node) {
+ var weight = this._getClassWeight(node);
+ var contentScore = 0;
+
+ this.log("Cleaning Conditionally", node);
+
+ if (weight + contentScore < 0) {
+ return true;
+ }
+
+ if (this._getCharCount(node, ',') < 10) {
+ // If there are not very many commas, and the number of
+ // non-paragraph elements is more than paragraphs or other
+ // ominous signs, remove the element.
+ var p = node.getElementsByTagName("p").length;
+ var img = node.getElementsByTagName("img").length;
+ var li = node.getElementsByTagName("li").length-100;
+ var input = node.getElementsByTagName("input").length;
+
+ var embedCount = 0;
+ var embeds = node.getElementsByTagName("embed");
+ for (var ei = 0, il = embeds.length; ei < il; ei += 1) {
+ if (!this.REGEXPS.videos.test(embeds[ei].src))
+ embedCount += 1;
+ }
+
+ var linkDensity = this._getLinkDensity(node);
+ var contentLength = this._getInnerText(node).length;
+
+ var haveToRemove =
+ // Make an exception for elements with no p's and exactly 1 img.
+ (img > p && !this._hasAncestorTag(node, "figure")) ||
+ (!isList && li > p) ||
+ (input > Math.floor(p/3)) ||
+ (!isList && contentLength < 25 && (img === 0 || img > 2)) ||
+ (!isList && weight < 25 && linkDensity > 0.2) ||
+ (weight >= 25 && linkDensity > 0.5) ||
+ ((embedCount === 1 && contentLength < 75) || embedCount > 1);
+ return haveToRemove;
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Clean out spurious headers from an Element. Checks things like classnames and link density.
+ *
+ * @param Element
+ * @return void
+ **/
+ _cleanHeaders: function(e) {
+ for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) {
+ this._removeNodes(e.getElementsByTagName('h' + headerIndex), function (header) {
+ return this._getClassWeight(header) < 0;
+ });
+ }
+ },
+
+ _flagIsActive: function(flag) {
+ return (this._flags & flag) > 0;
+ },
+
+ _addFlag: function(flag) {
+ this._flags = this._flags | flag;
+ },
+
+ _removeFlag: function(flag) {
+ this._flags = this._flags & ~flag;
+ },
+
+ /**
+ * Decides whether or not the document is reader-able without parsing the whole thing.
+ *
+ * @return boolean Whether or not we suspect parse() will suceeed at returning an article object.
+ */
+ isProbablyReaderable: function(helperIsVisible) {
+ var nodes = this._getAllNodesWithTag(this._doc, ["p", "pre"]);
+
+ // Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
+ // Some articles' DOM structures might look like
+ // <div>
+ // Sentences<br>
+ // <br>
+ // Sentences<br>
+ // </div>
+ var brNodes = this._getAllNodesWithTag(this._doc, ["div > br"]);
+ if (brNodes.length) {
+ var set = new Set();
+ [].forEach.call(brNodes, function(node) {
+ set.add(node.parentNode);
+ });
+ nodes = [].concat.apply(Array.from(set), nodes);
+ }
+
+ // FIXME we should have a fallback for helperIsVisible, but this is
+ // problematic because of jsdom's elem.style handling - see
+ // https://github.com/mozilla/readability/pull/186 for context.
+
+ var score = 0;
+ // This is a little cheeky, we use the accumulator 'score' to decide what to return from
+ // this callback:
+ return this._someNode(nodes, function(node) {
+ if (helperIsVisible && !helperIsVisible(node))
+ return false;
+ var matchString = node.className + " " + node.id;
+
+ if (this.REGEXPS.unlikelyCandidates.test(matchString) &&
+ !this.REGEXPS.okMaybeItsACandidate.test(matchString)) {
+ return false;
+ }
+
+ if (node.matches && node.matches("li p")) {
+ return false;
+ }
+
+ var textContentLength = node.textContent.trim().length;
+ if (textContentLength < 140) {
+ return false;
+ }
+
+ score += Math.sqrt(textContentLength - 140);
+
+ if (score > 20) {
+ return true;
+ }
+ return false;
+ });
+ },
+
+ /**
+ * Runs readability.
+ *
+ * Workflow:
+ * 1. Prep the document by removing script tags, css, etc.
+ * 2. Build readability's DOM tree.
+ * 3. Grab the article content from the current dom tree.
+ * 4. Replace the current DOM tree with the new one.
+ * 5. Read peacefully.
+ *
+ * @return void
+ **/
+ parse: function () {
+ // Avoid parsing too large documents, as per configuration option
+ if (this._maxElemsToParse > 0) {
+ var numTags = this._doc.getElementsByTagName("*").length;
+ if (numTags > this._maxElemsToParse) {
+ throw new Error("Aborting parsing document; " + numTags + " elements found");
+ }
+ }
+
+ if (typeof this._doc.documentElement.firstElementChild === "undefined") {
+ this._getNextNode = this._getNextNodeNoElementProperties;
+ }
+ // Remove script tags from the document.
+ this._removeScripts(this._doc);
+
+ // FIXME: Disabled multi-page article support for now as it
+ // needs more work on infrastructure.
+
+ // Make sure this document is added to the list of parsed pages first,
+ // so we don't double up on the first page.
+ // this._parsedPages[uri.spec.replace(/\/$/, '')] = true;
+
+ // Pull out any possible next page link first.
+ // var nextPageLink = this._findNextPageLink(doc.body);
+
+ this._prepDocument();
+
+ var metadata = this._getArticleMetadata();
+ var articleTitle = metadata.title || this._getArticleTitle();
+
+ var articleContent = this._grabArticle();
+ if (!articleContent)
+ return null;
+
+ this.log("Grabbed: " + articleContent.innerHTML);
+
+ this._postProcessContent(articleContent);
+
+ // if (nextPageLink) {
+ // // Append any additional pages after a small timeout so that people
+ // // can start reading without having to wait for this to finish processing.
+ // setTimeout((function() {
+ // this._appendNextPage(nextPageLink);
+ // }).bind(this), 500);
+ // }
+
+ // If we haven't found an excerpt in the article's metadata, use the article's
+ // first paragraph as the excerpt. This is used for displaying a preview of
+ // the article's content.
+ if (!metadata.excerpt) {
+ var paragraphs = articleContent.getElementsByTagName("p");
+ if (paragraphs.length > 0) {
+ metadata.excerpt = paragraphs[0].textContent.trim();
+ }
+ }
+
+ var textContent = articleContent.textContent;
+ return {
+ uri: this._uri,
+ title: articleTitle,
+ byline: metadata.byline || this._articleByline,
+ dir: this._articleDir,
+ content: articleContent.innerHTML,
+ textContent: textContent,
+ length: textContent.length,
+ excerpt: metadata.excerpt,
+ };
+ }
+};
diff --git a/toolkit/components/reader/ReaderMode.jsm b/toolkit/components/reader/ReaderMode.jsm
new file mode 100644
index 0000000000..033a024892
--- /dev/null
+++ b/toolkit/components/reader/ReaderMode.jsm
@@ -0,0 +1,514 @@
+// -*- 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/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ReaderMode"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+// Constants for telemetry.
+const DOWNLOAD_SUCCESS = 0;
+const DOWNLOAD_ERROR_XHR = 1;
+const DOWNLOAD_ERROR_NO_DOC = 2;
+
+const PARSE_SUCCESS = 0;
+const PARSE_ERROR_TOO_MANY_ELEMENTS = 1;
+const PARSE_ERROR_WORKER = 2;
+const PARSE_ERROR_NO_ARTICLE = 3;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Readability", function() {
+ let scope = {};
+ scope.dump = this.dump;
+ Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
+ return scope["Readability"];
+});
+
+this.ReaderMode = {
+ // Version of the cache schema.
+ CACHE_VERSION: 1,
+
+ DEBUG: 0,
+
+ // Don't try to parse the page if it has too many elements (for memory and
+ // performance reasons)
+ get maxElemsToParse() {
+ delete this.parseNodeLimit;
+
+ Services.prefs.addObserver("reader.parse-node-limit", this, false);
+ return this.parseNodeLimit = Services.prefs.getIntPref("reader.parse-node-limit");
+ },
+
+ get isEnabledForParseOnLoad() {
+ delete this.isEnabledForParseOnLoad;
+
+ // Listen for future pref changes.
+ Services.prefs.addObserver("reader.parse-on-load.", this, false);
+
+ return this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
+ },
+
+ get isOnLowMemoryPlatform() {
+ let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
+ delete this.isOnLowMemoryPlatform;
+ return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
+ },
+
+ _getStateForParseOnLoad: function () {
+ let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
+ let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
+ // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
+ return isForceEnabled || (isEnabled && !this.isOnLowMemoryPlatform);
+ },
+
+ observe: function(aMessage, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ if (aData.startsWith("reader.parse-on-load.")) {
+ this.isEnabledForParseOnLoad = this._getStateForParseOnLoad();
+ } else if (aData === "reader.parse-node-limit") {
+ this.parseNodeLimit = Services.prefs.getIntPref(aData);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Enter the reader mode by going forward one step in history if applicable,
+ * if not, append the about:reader page in the history instead.
+ */
+ enterReaderMode: function(docShell, win) {
+ let url = win.document.location.href;
+ let readerURL = "about:reader?url=" + encodeURIComponent(url);
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let sh = webNav.sessionHistory;
+ if (webNav.canGoForward) {
+ let forwardEntry = sh.getEntryAtIndex(sh.index + 1, false);
+ let forwardURL = forwardEntry.URI.spec;
+ if (forwardURL && (forwardURL == readerURL || !readerURL)) {
+ webNav.goForward();
+ return;
+ }
+ }
+
+ win.document.location = readerURL;
+ },
+
+ /**
+ * Exit the reader mode by going back one step in history if applicable,
+ * if not, append the original page in the history instead.
+ */
+ leaveReaderMode: function(docShell, win) {
+ let url = win.document.location.href;
+ let originalURL = this.getOriginalUrl(url);
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ let sh = webNav.sessionHistory;
+ if (webNav.canGoBack) {
+ let prevEntry = sh.getEntryAtIndex(sh.index - 1, false);
+ let prevURL = prevEntry.URI.spec;
+ if (prevURL && (prevURL == originalURL || !originalURL)) {
+ webNav.goBack();
+ return;
+ }
+ }
+
+ win.document.location = originalURL;
+ },
+
+ /**
+ * Returns original URL from an about:reader URL.
+ *
+ * @param url An about:reader URL.
+ * @return The original URL for the article, or null if we did not find
+ * a properly formatted about:reader URL.
+ */
+ getOriginalUrl: function(url) {
+ if (!url.startsWith("about:reader?")) {
+ return null;
+ }
+
+ let outerHash = "";
+ try {
+ let uriObj = Services.io.newURI(url, null, null);
+ url = uriObj.specIgnoringRef;
+ outerHash = uriObj.ref;
+ } catch (ex) { /* ignore, use the raw string */ }
+
+ let searchParams = new URLSearchParams(url.substring("about:reader?".length));
+ if (!searchParams.has("url")) {
+ return null;
+ }
+ let originalUrl = searchParams.get("url");
+ if (outerHash) {
+ try {
+ let uriObj = Services.io.newURI(originalUrl, null, null);
+ uriObj = Services.io.newURI('#' + outerHash, null, uriObj);
+ originalUrl = uriObj.spec;
+ } catch (ex) {}
+ }
+ return originalUrl;
+ },
+
+ /**
+ * Decides whether or not a document is reader-able without parsing the whole thing.
+ *
+ * @param doc A document to parse.
+ * @return boolean Whether or not we should show the reader mode button.
+ */
+ isProbablyReaderable: function(doc) {
+ // Only care about 'real' HTML documents:
+ if (doc.mozSyntheticDocument || !(doc instanceof doc.defaultView.HTMLDocument)) {
+ return false;
+ }
+
+ let uri = Services.io.newURI(doc.location.href, null, null);
+ if (!this._shouldCheckUri(uri)) {
+ return false;
+ }
+
+ let utils = this.getUtilsForWin(doc.defaultView);
+ // We pass in a helper function to determine if a node is visible, because
+ // it uses gecko APIs that the engine-agnostic readability code can't rely
+ // upon.
+ return new Readability(uri, doc).isProbablyReaderable(this.isNodeVisible.bind(this, utils));
+ },
+
+ isNodeVisible: function(utils, node) {
+ let bounds = utils.getBoundsWithoutFlushing(node);
+ return bounds.height > 0 && bounds.width > 0;
+ },
+
+ getUtilsForWin: function(win) {
+ return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ /**
+ * Gets an article from a loaded browser's document. This method will not attempt
+ * to parse certain URIs (e.g. about: URIs).
+ *
+ * @param doc A document to parse.
+ * @return {Promise}
+ * @resolves JS object representing the article, or null if no article is found.
+ */
+ parseDocument: Task.async(function* (doc) {
+ let documentURI = Services.io.newURI(doc.documentURI, null, null);
+ let baseURI = Services.io.newURI(doc.baseURI, null, null);
+ if (!this._shouldCheckUri(documentURI) || !this._shouldCheckUri(baseURI, true)) {
+ this.log("Reader mode disabled for URI");
+ return null;
+ }
+
+ return yield this._readerParse(baseURI, doc);
+ }),
+
+ /**
+ * Downloads and parses a document from a URL.
+ *
+ * @param url URL to download and parse.
+ * @return {Promise}
+ * @resolves JS object representing the article, or null if no article is found.
+ */
+ downloadAndParseDocument: Task.async(function* (url) {
+ let doc = yield this._downloadDocument(url);
+ let uri = Services.io.newURI(doc.baseURI, null, null);
+ if (!this._shouldCheckUri(uri, true)) {
+ this.log("Reader mode disabled for URI");
+ return null;
+ }
+
+ return yield this._readerParse(uri, doc);
+ }),
+
+ _downloadDocument: function (url) {
+ let histogram = Services.telemetry.getHistogramById("READER_MODE_DOWNLOAD_RESULT");
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.onerror = evt => reject(evt.error);
+ xhr.responseType = "document";
+ xhr.onload = evt => {
+ if (xhr.status !== 200) {
+ reject("Reader mode XHR failed with status: " + xhr.status);
+ histogram.add(DOWNLOAD_ERROR_XHR);
+ return;
+ }
+
+ let doc = xhr.responseXML;
+ if (!doc) {
+ reject("Reader mode XHR didn't return a document");
+ histogram.add(DOWNLOAD_ERROR_NO_DOC);
+ return;
+ }
+
+ // Manually follow a meta refresh tag if one exists.
+ let meta = doc.querySelector("meta[http-equiv=refresh]");
+ if (meta) {
+ let content = meta.getAttribute("content");
+ if (content) {
+ let urlIndex = content.toUpperCase().indexOf("URL=");
+ if (urlIndex > -1) {
+ let baseURI = Services.io.newURI(url, null, null);
+ let newURI = Services.io.newURI(content.substring(urlIndex + 4), null, baseURI);
+ let newURL = newURI.spec;
+ let ssm = Services.scriptSecurityManager;
+ let flags = ssm.LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ ssm.DISALLOW_INHERIT_PRINCIPAL;
+ try {
+ ssm.checkLoadURIStrWithPrincipal(doc.nodePrincipal, newURL, flags);
+ } catch (ex) {
+ let errorMsg = "Reader mode disallowed meta refresh (reason: " + ex + ").";
+
+ if (Services.prefs.getBoolPref("reader.errors.includeURLs"))
+ errorMsg += " Refresh target URI: '" + newURL + "'.";
+ reject(errorMsg);
+ return;
+ }
+ // Otherwise, pass an object indicating our new URL:
+ if (!baseURI.equalsExceptRef(newURI)) {
+ reject({newURL});
+ return;
+ }
+ }
+ }
+ }
+ let responseURL = xhr.responseURL;
+ let givenURL = url;
+ // Convert these to real URIs to make sure the escaping (or lack
+ // thereof) is identical:
+ try {
+ responseURL = Services.io.newURI(responseURL, null, null).specIgnoringRef;
+ } catch (ex) { /* Ignore errors - we'll use what we had before */ }
+ try {
+ givenURL = Services.io.newURI(givenURL, null, null).specIgnoringRef;
+ } catch (ex) { /* Ignore errors - we'll use what we had before */ }
+
+ if (responseURL != givenURL) {
+ // We were redirected without a meta refresh tag.
+ // Force redirect to the correct place:
+ reject({newURL: xhr.responseURL});
+ return;
+ }
+ resolve(doc);
+ histogram.add(DOWNLOAD_SUCCESS);
+ };
+ xhr.send();
+ });
+ },
+
+
+ /**
+ * Retrieves an article from the cache given an article URI.
+ *
+ * @param url The article URL.
+ * @return {Promise}
+ * @resolves JS object representing the article, or null if no article is found.
+ * @rejects OS.File.Error
+ */
+ getArticleFromCache: Task.async(function* (url) {
+ let path = this._toHashedPath(url);
+ try {
+ let array = yield OS.File.read(path);
+ return JSON.parse(new TextDecoder().decode(array));
+ } catch (e) {
+ if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile)
+ throw e;
+ return null;
+ }
+ }),
+
+ /**
+ * Stores an article in the cache.
+ *
+ * @param article JS object representing article.
+ * @return {Promise}
+ * @resolves When the article is stored.
+ * @rejects OS.File.Error
+ */
+ storeArticleInCache: Task.async(function* (article) {
+ let array = new TextEncoder().encode(JSON.stringify(article));
+ let path = this._toHashedPath(article.url);
+ yield this._ensureCacheDir();
+ return OS.File.writeAtomic(path, array, { tmpPath: path + ".tmp" })
+ .then(success => {
+ OS.File.stat(path).then(info => {
+ return Messaging.sendRequest({
+ type: "Reader:AddedToCache",
+ url: article.url,
+ size: info.size,
+ path: path,
+ });
+ });
+ });
+ }),
+
+ /**
+ * Removes an article from the cache given an article URI.
+ *
+ * @param url The article URL.
+ * @return {Promise}
+ * @resolves When the article is removed.
+ * @rejects OS.File.Error
+ */
+ removeArticleFromCache: Task.async(function* (url) {
+ let path = this._toHashedPath(url);
+ yield OS.File.remove(path);
+ }),
+
+ log: function(msg) {
+ if (this.DEBUG)
+ dump("Reader: " + msg);
+ },
+
+ _blockedHosts: [
+ "mail.google.com",
+ "github.com",
+ "pinterest.com",
+ "reddit.com",
+ "twitter.com",
+ "youtube.com",
+ ],
+
+ _shouldCheckUri: function (uri, isBaseUri = false) {
+ if (!(uri.schemeIs("http") || uri.schemeIs("https"))) {
+ this.log("Not parsing URI scheme: " + uri.scheme);
+ return false;
+ }
+
+ try {
+ uri.QueryInterface(Ci.nsIURL);
+ } catch (ex) {
+ // If this doesn't work, presumably the URL is not well-formed or something
+ return false;
+ }
+ // Sadly, some high-profile pages have false positives, so bail early for those:
+ let asciiHost = uri.asciiHost;
+ if (!isBaseUri && this._blockedHosts.some(blockedHost => asciiHost.endsWith(blockedHost))) {
+ return false;
+ }
+
+ if (!isBaseUri && (!uri.filePath || uri.filePath == "/")) {
+ this.log("Not parsing home page: " + uri.spec);
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Attempts to parse a document into an article. Heavy lifting happens
+ * in readerWorker.js.
+ *
+ * @param uri The base URI of the article.
+ * @param doc The document to parse.
+ * @return {Promise}
+ * @resolves JS object representing the article, or null if no article is found.
+ */
+ _readerParse: Task.async(function* (uri, doc) {
+ let histogram = Services.telemetry.getHistogramById("READER_MODE_PARSE_RESULT");
+ if (this.parseNodeLimit) {
+ let numTags = doc.getElementsByTagName("*").length;
+ if (numTags > this.parseNodeLimit) {
+ this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
+ histogram.add(PARSE_ERROR_TOO_MANY_ELEMENTS);
+ return null;
+ }
+ }
+
+ let uriParam = {
+ spec: uri.spec,
+ host: uri.host,
+ prePath: uri.prePath,
+ scheme: uri.scheme,
+ pathBase: Services.io.newURI(".", null, uri).spec
+ };
+
+ let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
+ createInstance(Ci.nsIDOMSerializer);
+ let serializedDoc = serializer.serializeToString(doc);
+
+ let article = null;
+ try {
+ article = yield ReaderWorker.post("parseDocument", [uriParam, serializedDoc]);
+ } catch (e) {
+ Cu.reportError("Error in ReaderWorker: " + e);
+ histogram.add(PARSE_ERROR_WORKER);
+ }
+
+ if (!article) {
+ this.log("Worker did not return an article");
+ histogram.add(PARSE_ERROR_NO_ARTICLE);
+ return null;
+ }
+
+ // Readability returns a URI object, but we only care about the URL.
+ article.url = article.uri.spec;
+ delete article.uri;
+
+ let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
+ article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
+ .convertToPlainText(article.title, flags, 0);
+
+ histogram.add(PARSE_SUCCESS);
+ return article;
+ }),
+
+ get _cryptoHash() {
+ delete this._cryptoHash;
+ return this._cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ },
+
+ get _unicodeConverter() {
+ delete this._unicodeConverter;
+ this._unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ this._unicodeConverter.charset = "utf8";
+ return this._unicodeConverter;
+ },
+
+ /**
+ * Calculate the hashed path for a stripped article URL.
+ *
+ * @param url The article URL. This should have referrers removed.
+ * @return The file path to the cached article.
+ */
+ _toHashedPath: function (url) {
+ let value = this._unicodeConverter.convertToByteArray(url);
+ this._cryptoHash.init(this._cryptoHash.MD5);
+ this._cryptoHash.update(value, value.length);
+
+ let hash = CommonUtils.encodeBase32(this._cryptoHash.finish(false));
+ let fileName = hash.substring(0, hash.indexOf("=")) + ".json";
+ return OS.Path.join(OS.Constants.Path.profileDir, "readercache", fileName);
+ },
+
+ /**
+ * Ensures the cache directory exists.
+ *
+ * @return Promise
+ * @resolves When the cache directory exists.
+ * @rejects OS.File.Error
+ */
+ _ensureCacheDir: function () {
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "readercache");
+ return OS.File.exists(dir).then(exists => {
+ if (!exists) {
+ return OS.File.makeDir(dir);
+ }
+ return undefined;
+ });
+ }
+};
diff --git a/toolkit/components/reader/ReaderWorker.js b/toolkit/components/reader/ReaderWorker.js
new file mode 100644
index 0000000000..20023d4e09
--- /dev/null
+++ b/toolkit/components/reader/ReaderWorker.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/. */
+
+"use strict";
+
+/**
+ * A worker dedicated to handle parsing documents for reader view.
+ */
+
+importScripts("resource://gre/modules/workers/require.js",
+ "resource://gre/modules/reader/JSDOMParser.js",
+ "resource://gre/modules/reader/Readability.js");
+
+var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+const DEBUG = false;
+
+var worker = new PromiseWorker.AbstractWorker();
+worker.dispatch = function(method, args = []) {
+ return Agent[method](...args);
+};
+worker.postMessage = function(result, ...transfers) {
+ self.postMessage(result, ...transfers);
+};
+worker.close = function() {
+ self.close();
+};
+worker.log = function(...args) {
+ if (DEBUG) {
+ dump("ReaderWorker: " + args.join(" ") + "\n");
+ }
+};
+
+self.addEventListener("message", msg => worker.handleMessage(msg));
+
+var Agent = {
+ /**
+ * Parses structured article data from a document.
+ *
+ * @param {object} uri URI data for the document.
+ * @param {string} serializedDoc The serialized document.
+ *
+ * @return {object} Article object returned from Readability.
+ */
+ parseDocument: function (uri, serializedDoc) {
+ let doc = new JSDOMParser().parse(serializedDoc);
+ return new Readability(uri, doc).parse();
+ },
+};
diff --git a/toolkit/components/reader/ReaderWorker.jsm b/toolkit/components/reader/ReaderWorker.jsm
new file mode 100644
index 0000000000..ed0ea9aeab
--- /dev/null
+++ b/toolkit/components/reader/ReaderWorker.jsm
@@ -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/. */
+
+"use strict";
+
+/**
+ * Interface to a dedicated thread handling readability parsing.
+ */
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
+
+this.EXPORTED_SYMBOLS = ["ReaderWorker"];
+
+this.ReaderWorker = new BasePromiseWorker("resource://gre/modules/reader/ReaderWorker.js");
diff --git a/toolkit/components/reader/content/aboutReader.html b/toolkit/components/reader/content/aboutReader.html
new file mode 100644
index 0000000000..b9c1139f6e
--- /dev/null
+++ b/toolkit/components/reader/content/aboutReader.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+ <meta name="viewport" content="width=device-width; user-scalable=0" />
+
+ <link rel="stylesheet" href="chrome://global/skin/aboutReader.css" type="text/css"/>
+
+ <script type="text/javascript;version=1.8" src="chrome://global/content/reader/aboutReader.js"></script>
+</head>
+
+<body>
+ <div id="container" class="container">
+ <div id="reader-header" class="header">
+ <style scoped>
+ @import url("chrome://global/skin/aboutReaderControls.css");
+ </style>
+ <a id="reader-domain" class="domain"></a>
+ <div class="domain-border"></div>
+ <h1 id="reader-title"></h1>
+ <div id="reader-credits" class="credits"></div>
+ </div>
+
+ <div class="content">
+ <style scoped>
+ @import url("chrome://global/skin/aboutReaderContent.css");
+ </style>
+ <div id="moz-reader-content"></div>
+ </div>
+
+ <div>
+ <style scoped>
+ @import url("chrome://global/skin/aboutReaderControls.css");
+ </style>
+ <div id="reader-message"></div>
+ </div>
+ </div>
+
+ <ul id="reader-toolbar" class="toolbar">
+ <style scoped>
+ @import url("chrome://global/skin/aboutReaderControls.css");
+ </style>
+ <li><button id="close-button" class="button close-button"/></li>
+ <ul id="style-dropdown" class="dropdown">
+ <li><button class="dropdown-toggle button style-button"/></li>
+ <li id="reader-popup" class="dropdown-popup">
+ <div id="font-type-buttons"></div>
+ <hr></hr>
+ <div id="font-size-buttons">
+ <button id="font-size-minus" class="minus-button"/>
+ <button id="font-size-sample"/>
+ <button id="font-size-plus" class="plus-button"/>
+ </div>
+ <hr></hr>
+ <div id="content-width-buttons">
+ <button id="content-width-minus" class="content-width-minus-button"/>
+ <button id="content-width-plus" class="content-width-plus-button"/>
+ </div>
+ <hr></hr>
+ <div id="line-height-buttons">
+ <button id="line-height-minus" class="line-height-minus-button"/>
+ <button id="line-height-plus" class="line-height-plus-button"/>
+ </div>
+ <hr></hr>
+ <div id="color-scheme-buttons"></div>
+ <div class="dropdown-arrow"/>
+ </li>
+ </ul>
+ </ul>
+
+</body>
+
+</html>
diff --git a/toolkit/components/reader/content/aboutReader.js b/toolkit/components/reader/content/aboutReader.js
new file mode 100644
index 0000000000..17133e69dc
--- /dev/null
+++ b/toolkit/components/reader/content/aboutReader.js
@@ -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/. */
+
+"use strict";
+
+window.addEventListener("DOMContentLoaded", function () {
+ document.dispatchEvent(new CustomEvent("AboutReaderContentLoaded", { bubbles: true }));
+});
diff --git a/toolkit/components/reader/jar.mn b/toolkit/components/reader/jar.mn
new file mode 100644
index 0000000000..241f1e6937
--- /dev/null
+++ b/toolkit/components/reader/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/.
+
+toolkit.jar:
+ content/global/reader/aboutReader.html (content/aboutReader.html)
+ content/global/reader/aboutReader.js (content/aboutReader.js)
diff --git a/toolkit/components/reader/moz.build b/toolkit/components/reader/moz.build
new file mode 100644
index 0000000000..6863d65427
--- /dev/null
+++ b/toolkit/components/reader/moz.build
@@ -0,0 +1,26 @@
+# -*- 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 += [
+ 'AboutReader.jsm',
+ 'ReaderMode.jsm'
+]
+
+EXTRA_JS_MODULES.reader = [
+ 'JSDOMParser.js',
+ 'Readability.js',
+ 'ReaderWorker.js',
+ 'ReaderWorker.jsm'
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'test/browser.ini'
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Reader Mode')
diff --git a/toolkit/components/reader/test/browser.ini b/toolkit/components/reader/test/browser.ini
new file mode 100644
index 0000000000..4f9df23b37
--- /dev/null
+++ b/toolkit/components/reader/test/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files = head.js
+[browser_readerMode.js]
+support-files =
+ readerModeArticle.html
+ readerModeArticleHiddenNodes.html
+[browser_readerMode_hidden_nodes.js]
+support-files =
+ readerModeArticleHiddenNodes.html
+[browser_readerMode_with_anchor.js]
+support-files =
+ readerModeArticle.html
+[browser_bug1124271_readerModePinnedTab.js]
+support-files =
+ readerModeArticle.html
diff --git a/toolkit/components/reader/test/browser_bug1124271_readerModePinnedTab.js b/toolkit/components/reader/test/browser_bug1124271_readerModePinnedTab.js
new file mode 100644
index 0000000000..39913aa3ed
--- /dev/null
+++ b/toolkit/components/reader/test/browser_bug1124271_readerModePinnedTab.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/. */
+
+// Test that the reader mode button won't open in a new tab when clicked from a pinned tab
+
+const PREF = "reader.parse-on-load.enabled";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+
+var readerButton = document.getElementById("reader-mode-button");
+
+add_task(function* () {
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(PREF);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ // Enable the reader mode button.
+ Services.prefs.setBoolPref(PREF, true);
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.pinTab(tab);
+
+ let initialTabsCount = gBrowser.tabs.length;
+
+ // Point tab to a test page that is reader-able.
+ let url = TEST_PATH + "readerModeArticle.html";
+ yield promiseTabLoadEvent(tab, url);
+ yield promiseWaitForCondition(() => !readerButton.hidden);
+
+ readerButton.click();
+ yield promiseTabLoadEvent(tab);
+
+ // Ensure no new tabs are opened when exiting reader mode in a pinned tab
+ is(gBrowser.tabs.length, initialTabsCount, "No additional tabs were opened.");
+
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ readerButton.click();
+ yield pageShownPromise;
+ // Ensure no new tabs are opened when exiting reader mode in a pinned tab
+ is(gBrowser.tabs.length, initialTabsCount, "No additional tabs were opened.");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/reader/test/browser_readerMode.js b/toolkit/components/reader/test/browser_readerMode.js
new file mode 100644
index 0000000000..70290c3b5f
--- /dev/null
+++ b/toolkit/components/reader/test/browser_readerMode.js
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 that the reader mode button appears and works properly on
+ * reader-able content.
+ */
+const TEST_PREFS = [
+ ["reader.parse-on-load.enabled", true],
+];
+
+const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+
+var readerButton = document.getElementById("reader-mode-button");
+
+add_task(function* test_reader_button() {
+ registerCleanupFunction(function() {
+ // Reset test prefs.
+ TEST_PREFS.forEach(([name, value]) => {
+ Services.prefs.clearUserPref(name);
+ });
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ // Set required test prefs.
+ TEST_PREFS.forEach(([name, value]) => {
+ Services.prefs.setBoolPref(name, value);
+ });
+ Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", false);
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
+ ok(!UITour.isInfoOnTarget(window, "readerMode-urlBar"),
+ "Info panel shouldn't appear without the reader mode button");
+ ok(!Services.prefs.getBoolPref("browser.reader.detectedFirstArticle"),
+ "Shouldn't have detected the first article");
+
+ // We're going to show the reader mode intro popup, make sure we wait for it:
+ let tourPopupShownPromise =
+ BrowserTestUtils.waitForEvent(document.getElementById("UITourTooltip"), "popupshown");
+ // Point tab to a test page that is reader-able.
+ let url = TEST_PATH + "readerModeArticle.html";
+ yield promiseTabLoadEvent(tab, url);
+ yield promiseWaitForCondition(() => !readerButton.hidden);
+ yield tourPopupShownPromise;
+ is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
+ ok(UITour.isInfoOnTarget(window, "readerMode-urlBar"),
+ "Info panel should be anchored at the reader mode button");
+ ok(Services.prefs.getBoolPref("browser.reader.detectedFirstArticle"),
+ "Should have detected the first article");
+
+ // Switch page into reader mode.
+ readerButton.click();
+ yield promiseTabLoadEvent(tab);
+ ok(!UITour.isInfoOnTarget(window, "readerMode-urlBar"), "Info panel should have closed");
+
+ let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
+ ok(readerUrl.startsWith("about:reader"), "about:reader loaded after clicking reader mode button");
+ is_element_visible(readerButton, "Reader mode button is present on about:reader");
+
+ is(gURLBar.value, readerUrl, "gURLBar value is about:reader URL");
+ is(gURLBar.textValue, url.substring("http://".length), "gURLBar is displaying original article URL");
+
+ // Check selected value for URL bar
+ yield new Promise((resolve, reject) => {
+ waitForClipboard(url, function () {
+ gURLBar.focus();
+ gURLBar.select();
+ goDoCommand("cmd_copy");
+ }, resolve, reject);
+ });
+
+ info("Got correct URL when copying");
+
+ // Switch page back out of reader mode.
+ let promisePageShow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ readerButton.click();
+ yield promisePageShow;
+ is(gBrowser.selectedBrowser.currentURI.spec, url,
+ "Back to the original page after clicking active reader mode button");
+ ok(gBrowser.selectedBrowser.canGoForward,
+ "Moved one step back in the session history.");
+
+ // Load a new tab that is NOT reader-able.
+ let newTab = gBrowser.selectedTab = gBrowser.addTab();
+ yield promiseTabLoadEvent(newTab, "about:robots");
+ yield promiseWaitForCondition(() => readerButton.hidden);
+ is_element_hidden(readerButton, "Reader mode button is not present on a non-reader-able page");
+
+ // Switch back to the original tab to make sure reader mode button is still visible.
+ gBrowser.removeCurrentTab();
+ yield promiseWaitForCondition(() => !readerButton.hidden);
+ is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
+});
+
+add_task(function* test_getOriginalUrl() {
+ let { ReaderMode } = Cu.import("resource://gre/modules/ReaderMode.jsm", {});
+ let url = "http://foo.com/article.html";
+
+ is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(url)), url, "Found original URL from encoded URL");
+ is(ReaderMode.getOriginalUrl("about:reader?foobar"), null, "Did not find original URL from malformed reader URL");
+ is(ReaderMode.getOriginalUrl(url), null, "Did not find original URL from non-reader URL");
+
+ let badUrl = "http://foo.com/?;$%^^";
+ is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(badUrl)), badUrl, "Found original URL from encoded malformed URL");
+ is(ReaderMode.getOriginalUrl("about:reader?url=" + badUrl), badUrl, "Found original URL from non-encoded malformed URL");
+});
+
+add_task(function* test_reader_view_element_attribute_transform() {
+ registerCleanupFunction(function() {
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ function observeAttribute(element, attribute, triggerFn, checkFn) {
+ return new Promise(resolve => {
+ let observer = new MutationObserver((mutations) => {
+ mutations.forEach( mu => {
+ if (element.getAttribute(attribute) !== mu.oldValue) {
+ checkFn();
+ resolve();
+ observer.disconnect();
+ }
+ });
+ });
+
+ observer.observe(element, {
+ attributes: true,
+ attributeOldValue: true,
+ attributeFilter: [attribute]
+ });
+
+ triggerFn();
+ });
+ }
+
+ let command = document.getElementById("View:ReaderView");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+ is(command.hidden, true, "Command element should have the hidden attribute");
+
+ info("Navigate a reader-able page");
+ let waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ yield observeAttribute(command, "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticle.html";
+ tab.linkedBrowser.loadURI(url);
+ },
+ () => {
+ is(command.hidden, false, "Command's hidden attribute should be false on a reader-able page");
+ }
+ );
+ yield waitForPageshow;
+
+ info("Navigate a non-reader-able page");
+ waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ yield observeAttribute(command, "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+ tab.linkedBrowser.loadURI(url);
+ },
+ () => {
+ is(command.hidden, true, "Command's hidden attribute should be true on a non-reader-able page");
+ }
+ );
+ yield waitForPageshow;
+
+ info("Navigate a reader-able page");
+ waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ yield observeAttribute(command, "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticle.html";
+ tab.linkedBrowser.loadURI(url);
+ },
+ () => {
+ is(command.hidden, false, "Command's hidden attribute should be false on a reader-able page");
+ }
+ );
+ yield waitForPageshow;
+
+ info("Enter Reader Mode");
+ waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ yield observeAttribute(readerButton, "readeractive",
+ () => {
+ readerButton.click();
+ },
+ () => {
+ is(readerButton.getAttribute("readeractive"), "true", "readerButton's readeractive attribute should be true when entering reader mode");
+ }
+ );
+ yield waitForPageshow;
+
+ info("Exit Reader Mode");
+ waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ yield observeAttribute(readerButton, "readeractive",
+ () => {
+ readerButton.click();
+ },
+ () => {
+ is(readerButton.getAttribute("readeractive"), "", "readerButton's readeractive attribute should be empty when reader mode is exited");
+ }
+ );
+ yield waitForPageshow;
+
+ info("Navigate a non-reader-able page");
+ waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+ yield observeAttribute(command, "hidden",
+ () => {
+ let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+ tab.linkedBrowser.loadURI(url);
+ },
+ () => {
+ is(command.hidden, true, "Command's hidden attribute should be true on a non-reader-able page");
+ }
+ );
+ yield waitForPageshow;
+});
diff --git a/toolkit/components/reader/test/browser_readerMode_hidden_nodes.js b/toolkit/components/reader/test/browser_readerMode_hidden_nodes.js
new file mode 100644
index 0000000000..b73eab58de
--- /dev/null
+++ b/toolkit/components/reader/test/browser_readerMode_hidden_nodes.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/. */
+
+/**
+ * Test that the reader mode button appears and works properly on
+ * reader-able content.
+ */
+const TEST_PREFS = [
+ ["reader.parse-on-load.enabled", true],
+ ["browser.reader.detectedFirstArticle", false],
+];
+
+const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+
+var readerButton = document.getElementById("reader-mode-button");
+
+add_task(function* test_reader_button() {
+ registerCleanupFunction(function() {
+ // Reset test prefs.
+ TEST_PREFS.forEach(([name, value]) => {
+ Services.prefs.clearUserPref(name);
+ });
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+ });
+
+ // Set required test prefs.
+ TEST_PREFS.forEach(([name, value]) => {
+ Services.prefs.setBoolPref(name, value);
+ });
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab();
+ is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
+ // Point tab to a test page that is not reader-able due to hidden nodes.
+ let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+ let paintPromise = ContentTask.spawn(tab.linkedBrowser, "", function() {
+ return new Promise(resolve => {
+ addEventListener("DOMContentLoaded", function onDCL() {
+ removeEventListener("DOMContentLoaded", onDCL);
+ addEventListener("MozAfterPaint", function onPaint() {
+ removeEventListener("MozAfterPaint", onPaint);
+ resolve();
+ });
+ });
+ });
+ });
+ tab.linkedBrowser.loadURI(url);
+ yield paintPromise;
+
+ is_element_hidden(readerButton, "Reader mode button is still not present on tab with unreadable content.");
+});
diff --git a/toolkit/components/reader/test/browser_readerMode_with_anchor.js b/toolkit/components/reader/test/browser_readerMode_with_anchor.js
new file mode 100644
index 0000000000..24c23c49f0
--- /dev/null
+++ b/toolkit/components/reader/test/browser_readerMode_with_anchor.js
@@ -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/. */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab(TEST_PATH + "readerModeArticle.html#foo", function* (browser) {
+ let pageShownPromise = BrowserTestUtils.waitForContentEvent(browser, "AboutReaderContentReady");
+ let readerButton = document.getElementById("reader-mode-button");
+ readerButton.click();
+ yield pageShownPromise;
+ yield ContentTask.spawn(browser, null, function* () {
+ // Check if offset != 0
+ ok(content.document.getElementById("foo") !== null, "foo element should be in document");
+ ok(content.pageYOffset != 0, "pageYOffset should be > 0");
+ });
+ });
+});
diff --git a/toolkit/components/reader/test/head.js b/toolkit/components/reader/test/head.js
new file mode 100644
index 0000000000..3d8d989bc0
--- /dev/null
+++ b/toolkit/components/reader/test/head.js
@@ -0,0 +1,126 @@
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+/* exported promiseTabLoadEvent, promiseWaitForCondition, is_element_visible, is_element_hidden */
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+ let deferred = Promise.defer();
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ // Create two promises: one resolved from the content process when the page
+ // loads and one that is rejected if we take too long to load the url.
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ let timeout = setTimeout(() => {
+ deferred.reject(new Error("Timed out while waiting for a 'load' event"));
+ }, 30000);
+
+ loaded.then(() => {
+ clearTimeout(timeout);
+ deferred.resolve();
+ });
+
+ if (url)
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+
+ // Promise.all rejects if either promise rejects (i.e. if we time out) and
+ // if our loaded promise resolves before the timeout, then we resolve the
+ // timeout promise as well, causing the all promise to resolve.
+ return Promise.all([deferred.promise, loaded]);
+}
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== 'undefined' ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function() {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+ let deferred = Promise.defer();
+ waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
+ return deferred.promise;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_visible(element), msg || "Element should be visible");
+
+}
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg || "Element should be hidden");
+}
+
+function is_visible(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return false;
+ if (style.visibility != "visible")
+ return false;
+ if (style.display == "-moz-popup" && element.state != "open")
+ return false;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_visible(element.parentNode);
+
+ return true;
+}
+
+function is_hidden(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return true;
+ if (style.visibility != "visible")
+ return true;
+ if (style.display == "-moz-popup")
+ return ["hiding", "closed"].indexOf(element.state) != -1;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_hidden(element.parentNode);
+
+ return false;
+}
diff --git a/toolkit/components/reader/test/readerModeArticle.html b/toolkit/components/reader/test/readerModeArticle.html
new file mode 100644
index 0000000000..7c5033d5b7
--- /dev/null
+++ b/toolkit/components/reader/test/readerModeArticle.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<div id="foo">by John Doe</div>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/toolkit/components/reader/test/readerModeArticleHiddenNodes.html b/toolkit/components/reader/test/readerModeArticleHiddenNodes.html
new file mode 100644
index 0000000000..92441b7978
--- /dev/null
+++ b/toolkit/components/reader/test/readerModeArticleHiddenNodes.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<style>
+p { display: none }
+</style>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/toolkit/components/reflect/moz.build b/toolkit/components/reflect/moz.build
new file mode 100644
index 0000000000..12c76aaf9a
--- /dev/null
+++ b/toolkit/components/reflect/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/.
+
+SOURCES += [
+ 'reflect.cpp',
+]
+
+EXTRA_JS_MODULES += [
+ 'reflect.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/reflect/reflect.cpp b/toolkit/components/reflect/reflect.cpp
new file mode 100644
index 0000000000..cd46baf7cb
--- /dev/null
+++ b/toolkit/components/reflect/reflect.cpp
@@ -0,0 +1,77 @@
+/* -*- 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 "reflect.h"
+#include "jsapi.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsMemory.h"
+#include "nsString.h"
+#include "nsNativeCharsetUtils.h"
+#include "xpc_make_class.h"
+
+#define JSREFLECT_CONTRACTID \
+ "@mozilla.org/jsreflect;1"
+
+#define JSREFLECT_CID \
+{ 0x1a817186, 0x357a, 0x47cd, { 0x8a, 0xea, 0x28, 0x50, 0xd6, 0x0e, 0x95, 0x9e } }
+
+namespace mozilla {
+namespace reflect {
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(Module)
+
+NS_IMPL_ISUPPORTS(Module, nsIXPCScriptable)
+
+Module::Module()
+{
+}
+
+Module::~Module()
+{
+}
+
+#define XPC_MAP_CLASSNAME Module
+#define XPC_MAP_QUOTED_CLASSNAME "Module"
+#define XPC_MAP_WANT_CALL
+#define XPC_MAP_FLAGS nsIXPCScriptable::WANT_CALL
+#include "xpc_map_end.h"
+
+NS_IMETHODIMP
+Module::Call(nsIXPConnectWrappedNative* wrapper,
+ JSContext* cx,
+ JSObject* obj,
+ const JS::CallArgs& args,
+ bool* _retval)
+{
+ JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
+ if (!global)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ *_retval = JS_InitReflectParse(cx, global);
+ return NS_OK;
+}
+
+} // namespace reflect
+} // namespace mozilla
+
+NS_DEFINE_NAMED_CID(JSREFLECT_CID);
+
+static const mozilla::Module::CIDEntry kReflectCIDs[] = {
+ { &kJSREFLECT_CID, false, nullptr, mozilla::reflect::ModuleConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kReflectContracts[] = {
+ { JSREFLECT_CONTRACTID, &kJSREFLECT_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kReflectModule = {
+ mozilla::Module::kVersion,
+ kReflectCIDs,
+ kReflectContracts
+};
+
+NSMODULE_DEFN(jsreflect) = &kReflectModule;
diff --git a/toolkit/components/reflect/reflect.h b/toolkit/components/reflect/reflect.h
new file mode 100644
index 0000000000..6f5237104c
--- /dev/null
+++ b/toolkit/components/reflect/reflect.h
@@ -0,0 +1,30 @@
+/* -*- 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 COMPONENTS_REFLECT_H
+#define COMPONENTS_REFLECT_H
+
+#include "nsIXPCScriptable.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+namespace reflect {
+
+class Module final : public nsIXPCScriptable
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIXPCSCRIPTABLE
+
+ Module();
+
+private:
+ ~Module();
+};
+
+} // namespace reflect
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/reflect/reflect.jsm b/toolkit/components/reflect/reflect.jsm
new file mode 100644
index 0000000000..fd1729dd9e
--- /dev/null
+++ b/toolkit/components/reflect/reflect.jsm
@@ -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/. */
+
+this.EXPORTED_SYMBOLS = [ "Reflect" ];
+
+/*
+ * This is the js module for Reflect. Import it like so:
+ * Components.utils.import("resource://gre/modules/reflect.jsm");
+ *
+ * This will create a 'Reflect' object, which provides an interface to the
+ * SpiderMonkey parser API.
+ *
+ * For documentation on the API, see:
+ * https://developer.mozilla.org/en/SpiderMonkey/Parser_API
+ *
+ */
+
+
+// Initialize the ctypes object. You do not need to do this yourself.
+const init = Components.classes["@mozilla.org/jsreflect;1"].createInstance();
+init();
+this.Reflect = Reflect;
diff --git a/toolkit/components/remote/moz.build b/toolkit/components/remote/moz.build
new file mode 100644
index 0000000000..faa119eeb5
--- /dev/null
+++ b/toolkit/components/remote/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 += [
+ 'nsIRemoteService.idl',
+]
+
+XPIDL_MODULE = 'toolkitremote'
+
+SOURCES += [
+ 'nsXRemoteService.cpp',
+]
+
+if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
+ SOURCES += [
+ 'nsGTKRemoteService.cpp',
+ ]
+
+FINAL_LIBRARY = 'xul'
+
+CXXFLAGS += CONFIG['TK_CFLAGS']
diff --git a/toolkit/components/remote/nsGTKRemoteService.cpp b/toolkit/components/remote/nsGTKRemoteService.cpp
new file mode 100644
index 0000000000..860efe0150
--- /dev/null
+++ b/toolkit/components/remote/nsGTKRemoteService.cpp
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "nsGTKRemoteService.h"
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+
+#include "nsIBaseWindow.h"
+#include "nsIDocShell.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsIServiceManager.h"
+#include "nsIWeakReference.h"
+#include "nsIWidget.h"
+#include "nsIAppShellService.h"
+#include "nsAppShellCID.h"
+
+#include "nsCOMPtr.h"
+
+#include "nsGTKToolkit.h"
+
+NS_IMPL_ISUPPORTS(nsGTKRemoteService,
+ nsIRemoteService,
+ nsIObserver)
+
+NS_IMETHODIMP
+nsGTKRemoteService::Startup(const char* aAppName, const char* aProfileName)
+{
+ NS_ASSERTION(aAppName, "Don't pass a null appname!");
+ sRemoteImplementation = this;
+
+ if (mServerWindow) return NS_ERROR_ALREADY_INITIALIZED;
+
+ XRemoteBaseStartup(aAppName, aProfileName);
+
+ mServerWindow = gtk_invisible_new();
+ gtk_widget_realize(mServerWindow);
+ HandleCommandsFor(mServerWindow, nullptr);
+
+ for (auto iter = mWindows.Iter(); !iter.Done(); iter.Next()) {
+ HandleCommandsFor(iter.Key(), iter.UserData());
+ }
+
+ return NS_OK;
+}
+
+static nsIWidget* GetMainWidget(nsPIDOMWindowInner* aWindow)
+{
+ // get the native window for this instance
+ nsCOMPtr<nsIBaseWindow> baseWindow
+ (do_QueryInterface(aWindow->GetDocShell()));
+ NS_ENSURE_TRUE(baseWindow, nullptr);
+
+ nsCOMPtr<nsIWidget> mainWidget;
+ baseWindow->GetMainWidget(getter_AddRefs(mainWidget));
+ return mainWidget;
+}
+
+NS_IMETHODIMP
+nsGTKRemoteService::RegisterWindow(mozIDOMWindow* aWindow)
+{
+ nsIWidget* mainWidget = GetMainWidget(nsPIDOMWindowInner::From(aWindow));
+ NS_ENSURE_TRUE(mainWidget, NS_ERROR_FAILURE);
+
+ GtkWidget* widget =
+ (GtkWidget*) mainWidget->GetNativeData(NS_NATIVE_SHELLWIDGET);
+ NS_ENSURE_TRUE(widget, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIWeakReference> weak = do_GetWeakReference(aWindow);
+ NS_ENSURE_TRUE(weak, NS_ERROR_FAILURE);
+
+ mWindows.Put(widget, weak);
+
+ // If Startup() has already been called, immediately register this window.
+ if (mServerWindow) {
+ HandleCommandsFor(widget, weak);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGTKRemoteService::Shutdown()
+{
+ if (!mServerWindow)
+ return NS_ERROR_NOT_INITIALIZED;
+
+ gtk_widget_destroy(mServerWindow);
+ mServerWindow = nullptr;
+ return NS_OK;
+}
+
+// Set desktop startup ID to the passed ID, if there is one, so that any created
+// windows get created with the right window manager metadata, and any windows
+// that get new tabs and are activated also get the right WM metadata.
+// The timestamp will be used if there is no desktop startup ID, or if we're
+// raising an existing window rather than showing a new window for the first time.
+void
+nsGTKRemoteService::SetDesktopStartupIDOrTimestamp(const nsACString& aDesktopStartupID,
+ uint32_t aTimestamp) {
+ nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit();
+ if (!toolkit)
+ return;
+
+ if (!aDesktopStartupID.IsEmpty()) {
+ toolkit->SetDesktopStartupID(aDesktopStartupID);
+ }
+
+ toolkit->SetFocusTimestamp(aTimestamp);
+}
+
+
+void
+nsGTKRemoteService::HandleCommandsFor(GtkWidget* widget,
+ nsIWeakReference* aWindow)
+{
+ g_signal_connect(G_OBJECT(widget), "property_notify_event",
+ G_CALLBACK(HandlePropertyChange), aWindow);
+
+ gtk_widget_add_events(widget, GDK_PROPERTY_CHANGE_MASK);
+
+#if (MOZ_WIDGET_GTK == 2)
+ Window window = GDK_WINDOW_XWINDOW(widget->window);
+#else
+ Window window = gdk_x11_window_get_xid(gtk_widget_get_window(widget));
+#endif
+ nsXRemoteService::HandleCommandsFor(window);
+
+}
+
+gboolean
+nsGTKRemoteService::HandlePropertyChange(GtkWidget *aWidget,
+ GdkEventProperty *pevent,
+ nsIWeakReference *aThis)
+{
+ if (pevent->state == GDK_PROPERTY_NEW_VALUE) {
+ Atom changedAtom = gdk_x11_atom_to_xatom(pevent->atom);
+
+#if (MOZ_WIDGET_GTK == 2)
+ XID window = GDK_WINDOW_XWINDOW(pevent->window);
+#else
+ XID window = gdk_x11_window_get_xid(gtk_widget_get_window(aWidget));
+#endif
+ return HandleNewProperty(window,
+ GDK_DISPLAY_XDISPLAY(gdk_display_get_default()),
+ pevent->time, changedAtom, aThis);
+ }
+ return FALSE;
+}
+
+
+// {C0773E90-5799-4eff-AD03-3EBCD85624AC}
+#define NS_REMOTESERVICE_CID \
+ { 0xc0773e90, 0x5799, 0x4eff, { 0xad, 0x3, 0x3e, 0xbc, 0xd8, 0x56, 0x24, 0xac } }
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsGTKRemoteService)
+NS_DEFINE_NAMED_CID(NS_REMOTESERVICE_CID);
+
+static const mozilla::Module::CIDEntry kRemoteCIDs[] = {
+ { &kNS_REMOTESERVICE_CID, false, nullptr, nsGTKRemoteServiceConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kRemoteContracts[] = {
+ { "@mozilla.org/toolkit/remote-service;1", &kNS_REMOTESERVICE_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kRemoteModule = {
+ mozilla::Module::kVersion,
+ kRemoteCIDs,
+ kRemoteContracts
+};
+
+NSMODULE_DEFN(RemoteServiceModule) = &kRemoteModule;
diff --git a/toolkit/components/remote/nsGTKRemoteService.h b/toolkit/components/remote/nsGTKRemoteService.h
new file mode 100644
index 0000000000..034a77a24c
--- /dev/null
+++ b/toolkit/components/remote/nsGTKRemoteService.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 __nsGTKRemoteService_h__
+#define __nsGTKRemoteService_h__
+
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gtk/gtk.h>
+
+#include "nsInterfaceHashtable.h"
+#include "nsXRemoteService.h"
+#include "mozilla/Attributes.h"
+
+class nsGTKRemoteService final : public nsXRemoteService
+{
+public:
+ // We will be a static singleton, so don't use the ordinary methods.
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREMOTESERVICE
+
+
+ nsGTKRemoteService() :
+ mServerWindow(nullptr) { }
+
+private:
+ ~nsGTKRemoteService() { }
+
+ void HandleCommandsFor(GtkWidget* aWidget,
+ nsIWeakReference* aWindow);
+
+
+ static gboolean HandlePropertyChange(GtkWidget *widget,
+ GdkEventProperty *event,
+ nsIWeakReference* aThis);
+
+
+ virtual void SetDesktopStartupIDOrTimestamp(const nsACString& aDesktopStartupID,
+ uint32_t aTimestamp) override;
+
+ nsInterfaceHashtable<nsPtrHashKey<GtkWidget>, nsIWeakReference> mWindows;
+ GtkWidget* mServerWindow;
+};
+
+#endif // __nsGTKRemoteService_h__
diff --git a/toolkit/components/remote/nsIRemoteService.idl b/toolkit/components/remote/nsIRemoteService.idl
new file mode 100644
index 0000000000..e8ba2f1c85
--- /dev/null
+++ b/toolkit/components/remote/nsIRemoteService.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindow;
+
+/**
+ * Start and stop the remote service (xremote/phremote), and register
+ * windows with the service for backwards compatibility with old xremote
+ * clients.
+ *
+ * @status FLUID This interface is not frozen and is not intended for embedders
+ * who want a frozen API. If you are an embedder and need this
+ * functionality, contact Benjamin Smedberg about the possibility
+ * of freezing the functionality you need.
+ */
+
+[scriptable, uuid(bf23f1c3-7012-42dd-b0bb-a84060ccc52e)]
+interface nsIRemoteService : nsISupports
+{
+ /**
+ * Start the remote service. This should not be done until app startup
+ * appears to have been successful.
+ *
+ * @param appName (Required) Sets a window property identifying the
+ * application.
+ * @param profileName (May be null) Sets a window property identifying the
+ * profile name.
+ */
+ void startup(in string appName, in string profileName);
+
+ /**
+ * Register a XUL window with the xremote service. The window will be
+ * configured to accept incoming remote requests. If this method is called
+ * before startup(), the registration will happen once startup() is called.
+ */
+ void registerWindow(in mozIDOMWindow aWindow);
+
+ /**
+ * Stop the remote service from accepting additional requests.
+ */
+ void shutdown();
+};
diff --git a/toolkit/components/remote/nsXRemoteService.cpp b/toolkit/components/remote/nsXRemoteService.cpp
new file mode 100644
index 0000000000..41a40e4719
--- /dev/null
+++ b/toolkit/components/remote/nsXRemoteService.cpp
@@ -0,0 +1,324 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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 "mozilla/ArrayUtils.h"
+
+#include "nsXRemoteService.h"
+#include "nsIObserverService.h"
+#include "nsCOMPtr.h"
+#include "nsIServiceManager.h"
+#include "nsICommandLineRunner.h"
+#include "nsICommandLine.h"
+
+#include "nsIBaseWindow.h"
+#include "nsIDocShell.h"
+#include "nsIFile.h"
+#include "nsIServiceManager.h"
+#include "nsIWeakReference.h"
+#include "nsIWidget.h"
+#include "nsIAppShellService.h"
+#include "nsAppShellCID.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/X11Util.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "prprf.h"
+#include "prenv.h"
+#include "nsCRT.h"
+
+#include "nsXULAppAPI.h"
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+
+using namespace mozilla;
+
+#define MOZILLA_VERSION_PROP "_MOZILLA_VERSION"
+#define MOZILLA_LOCK_PROP "_MOZILLA_LOCK"
+#define MOZILLA_RESPONSE_PROP "_MOZILLA_RESPONSE"
+#define MOZILLA_USER_PROP "_MOZILLA_USER"
+#define MOZILLA_PROFILE_PROP "_MOZILLA_PROFILE"
+#define MOZILLA_PROGRAM_PROP "_MOZILLA_PROGRAM"
+#define MOZILLA_COMMANDLINE_PROP "_MOZILLA_COMMANDLINE"
+
+const unsigned char kRemoteVersion[] = "5.1";
+
+#ifdef IS_BIG_ENDIAN
+#define TO_LITTLE_ENDIAN32(x) \
+ ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >> 8) | \
+ (((x) & 0x0000ff00) << 8) | (((x) & 0x000000ff) << 24))
+#else
+#define TO_LITTLE_ENDIAN32(x) (x)
+#endif
+
+// Minimize the roundtrips to the X server by getting all the atoms at once
+static const char *XAtomNames[] = {
+ MOZILLA_VERSION_PROP,
+ MOZILLA_LOCK_PROP,
+ MOZILLA_RESPONSE_PROP,
+ MOZILLA_USER_PROP,
+ MOZILLA_PROFILE_PROP,
+ MOZILLA_PROGRAM_PROP,
+ MOZILLA_COMMANDLINE_PROP
+};
+static Atom XAtoms[MOZ_ARRAY_LENGTH(XAtomNames)];
+
+Atom nsXRemoteService::sMozVersionAtom;
+Atom nsXRemoteService::sMozLockAtom;
+Atom nsXRemoteService::sMozResponseAtom;
+Atom nsXRemoteService::sMozUserAtom;
+Atom nsXRemoteService::sMozProfileAtom;
+Atom nsXRemoteService::sMozProgramAtom;
+Atom nsXRemoteService::sMozCommandLineAtom;
+
+nsXRemoteService * nsXRemoteService::sRemoteImplementation = 0;
+
+
+static bool
+FindExtensionParameterInCommand(const char* aParameterName,
+ const nsACString& aCommand,
+ char aSeparator,
+ nsACString* aValue)
+{
+ nsAutoCString searchFor;
+ searchFor.Append(aSeparator);
+ searchFor.Append(aParameterName);
+ searchFor.Append('=');
+
+ nsACString::const_iterator start, end;
+ aCommand.BeginReading(start);
+ aCommand.EndReading(end);
+ if (!FindInReadable(searchFor, start, end))
+ return false;
+
+ nsACString::const_iterator charStart, charEnd;
+ charStart = end;
+ aCommand.EndReading(charEnd);
+ nsACString::const_iterator idStart = charStart, idEnd;
+ if (FindCharInReadable(aSeparator, charStart, charEnd)) {
+ idEnd = charStart;
+ } else {
+ idEnd = charEnd;
+ }
+ *aValue = nsDependentCSubstring(idStart, idEnd);
+ return true;
+}
+
+
+nsXRemoteService::nsXRemoteService()
+{
+}
+
+void
+nsXRemoteService::XRemoteBaseStartup(const char *aAppName, const char *aProfileName)
+{
+ EnsureAtoms();
+
+ mAppName = aAppName;
+ ToLowerCase(mAppName);
+
+ mProfileName = aProfileName;
+
+ nsCOMPtr<nsIObserverService> obs(do_GetService("@mozilla.org/observer-service;1"));
+ if (obs) {
+ obs->AddObserver(this, "xpcom-shutdown", false);
+ obs->AddObserver(this, "quit-application", false);
+ }
+}
+
+void
+nsXRemoteService::HandleCommandsFor(Window aWindowId)
+{
+ // set our version
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozVersionAtom, XA_STRING,
+ 8, PropModeReplace, kRemoteVersion, sizeof(kRemoteVersion) - 1);
+
+ // get our username
+ unsigned char *logname;
+ logname = (unsigned char*) PR_GetEnv("LOGNAME");
+ if (logname) {
+ // set the property on the window if it's available
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozUserAtom, XA_STRING,
+ 8, PropModeReplace, logname, strlen((char*) logname));
+ }
+
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozProgramAtom, XA_STRING,
+ 8, PropModeReplace, (unsigned char*) mAppName.get(), mAppName.Length());
+
+ if (!mProfileName.IsEmpty()) {
+ XChangeProperty(mozilla::DefaultXDisplay(),
+ aWindowId, sMozProfileAtom, XA_STRING,
+ 8, PropModeReplace,
+ (unsigned char*) mProfileName.get(), mProfileName.Length());
+ }
+
+}
+
+NS_IMETHODIMP
+nsXRemoteService::Observe(nsISupports* aSubject,
+ const char *aTopic,
+ const char16_t *aData)
+{
+ // This can be xpcom-shutdown or quit-application, but it's the same either
+ // way.
+ Shutdown();
+ return NS_OK;
+}
+
+bool
+nsXRemoteService::HandleNewProperty(XID aWindowId, Display* aDisplay,
+ Time aEventTime,
+ Atom aChangedAtom,
+ nsIWeakReference* aDomWindow)
+{
+
+ nsCOMPtr<nsIDOMWindow> window (do_QueryReferent(aDomWindow));
+
+ if (aChangedAtom == sMozCommandLineAtom) {
+ // We got a new command atom.
+ int result;
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ char *data = 0;
+
+ result = XGetWindowProperty (aDisplay,
+ aWindowId,
+ aChangedAtom,
+ 0, /* long_offset */
+ (65536 / sizeof (long)), /* long_length */
+ True, /* atomic delete after */
+ XA_STRING, /* req_type */
+ &actual_type, /* actual_type return */
+ &actual_format, /* actual_format_return */
+ &nitems, /* nitems_return */
+ &bytes_after, /* bytes_after_return */
+ (unsigned char **)&data); /* prop_return
+ (we only care
+ about the first ) */
+
+ // Failed to get property off the window?
+ if (result != Success)
+ return false;
+
+ // Failed to get the data off the window or it was the wrong type?
+ if (!data || !TO_LITTLE_ENDIAN32(*reinterpret_cast<int32_t*>(data)))
+ return false;
+
+ // cool, we got the property data.
+ const char *response = HandleCommandLine(data, window, aEventTime);
+
+ // put the property onto the window as the response
+ XChangeProperty (aDisplay, aWindowId,
+ sMozResponseAtom, XA_STRING,
+ 8, PropModeReplace,
+ (const unsigned char *)response,
+ strlen (response));
+ XFree(data);
+ return true;
+ }
+
+ else if (aChangedAtom == sMozResponseAtom) {
+ // client accepted the response. party on wayne.
+ return true;
+ }
+
+ else if (aChangedAtom == sMozLockAtom) {
+ // someone locked the window
+ return true;
+ }
+
+ return false;
+}
+
+const char*
+nsXRemoteService::HandleCommandLine(char* aBuffer, nsIDOMWindow* aWindow,
+ uint32_t aTimestamp)
+{
+ nsresult rv;
+
+ nsCOMPtr<nsICommandLineRunner> cmdline
+ (do_CreateInstance("@mozilla.org/toolkit/command-line;1", &rv));
+ if (NS_FAILED(rv))
+ return "509 internal error";
+
+ // the commandline property is constructed as an array of int32_t
+ // followed by a series of null-terminated strings:
+ //
+ // [argc][offsetargv0][offsetargv1...]<workingdir>\0<argv[0]>\0argv[1]...\0
+ // (offset is from the beginning of the buffer)
+
+ int32_t argc = TO_LITTLE_ENDIAN32(*reinterpret_cast<int32_t*>(aBuffer));
+ char *wd = aBuffer + ((argc + 1) * sizeof(int32_t));
+
+ nsCOMPtr<nsIFile> lf;
+ rv = NS_NewNativeLocalFile(nsDependentCString(wd), true,
+ getter_AddRefs(lf));
+ if (NS_FAILED(rv))
+ return "509 internal error";
+
+ nsAutoCString desktopStartupID;
+
+ char **argv = (char**) malloc(sizeof(char*) * argc);
+ if (!argv) return "509 internal error";
+
+ int32_t *offset = reinterpret_cast<int32_t*>(aBuffer) + 1;
+
+ for (int i = 0; i < argc; ++i) {
+ argv[i] = aBuffer + TO_LITTLE_ENDIAN32(offset[i]);
+
+ if (i == 0) {
+ nsDependentCString cmd(argv[0]);
+ FindExtensionParameterInCommand("DESKTOP_STARTUP_ID",
+ cmd, ' ',
+ &desktopStartupID);
+ }
+ }
+
+ rv = cmdline->Init(argc, argv, lf, nsICommandLine::STATE_REMOTE_AUTO);
+
+ free (argv);
+ if (NS_FAILED(rv)) {
+ return "509 internal error";
+ }
+
+ if (aWindow)
+ cmdline->SetWindowContext(aWindow);
+
+ if (sRemoteImplementation)
+ sRemoteImplementation->SetDesktopStartupIDOrTimestamp(desktopStartupID, aTimestamp);
+
+ rv = cmdline->Run();
+
+ if (NS_ERROR_ABORT == rv)
+ return "500 command not parseable";
+
+ if (NS_FAILED(rv))
+ return "509 internal error";
+
+ return "200 executed command";
+}
+
+void
+nsXRemoteService::EnsureAtoms(void)
+{
+ if (sMozVersionAtom)
+ return;
+
+ XInternAtoms(mozilla::DefaultXDisplay(), const_cast<char**>(XAtomNames),
+ ArrayLength(XAtomNames), False, XAtoms);
+
+ int i = 0;
+ sMozVersionAtom = XAtoms[i++];
+ sMozLockAtom = XAtoms[i++];
+ sMozResponseAtom = XAtoms[i++];
+ sMozUserAtom = XAtoms[i++];
+ sMozProfileAtom = XAtoms[i++];
+ sMozProgramAtom = XAtoms[i++];
+ sMozCommandLineAtom = XAtoms[i++];
+}
diff --git a/toolkit/components/remote/nsXRemoteService.h b/toolkit/components/remote/nsXRemoteService.h
new file mode 100644
index 0000000000..7186336751
--- /dev/null
+++ b/toolkit/components/remote/nsXRemoteService.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef NSXREMOTESERVICE_H
+#define NSXREMOTESERVICE_H
+
+#include "nsString.h"
+
+#include "nsIRemoteService.h"
+#include "nsIObserver.h"
+#include <X11/Xlib.h>
+#include <X11/X.h>
+
+class nsIDOMWindow;
+class nsIWeakReference;
+
+/**
+ Base class for GTK/Qt remote service
+*/
+class nsXRemoteService : public nsIRemoteService,
+ public nsIObserver
+{
+public:
+ NS_DECL_NSIOBSERVER
+
+
+protected:
+ nsXRemoteService();
+
+ static bool HandleNewProperty(Window aWindowId,Display* aDisplay,
+ Time aEventTime, Atom aChangedAtom,
+ nsIWeakReference* aDomWindow);
+
+ void XRemoteBaseStartup(const char *aAppName, const char *aProfileName);
+
+ void HandleCommandsFor(Window aWindowId);
+ static nsXRemoteService *sRemoteImplementation;
+private:
+ void EnsureAtoms();
+ static const char* HandleCommandLine(char* aBuffer, nsIDOMWindow* aWindow,
+ uint32_t aTimestamp);
+
+ virtual void SetDesktopStartupIDOrTimestamp(const nsACString& aDesktopStartupID,
+ uint32_t aTimestamp) = 0;
+
+ nsCString mAppName;
+ nsCString mProfileName;
+
+ static Atom sMozVersionAtom;
+ static Atom sMozLockAtom;
+ static Atom sMozResponseAtom;
+ static Atom sMozUserAtom;
+ static Atom sMozProfileAtom;
+ static Atom sMozProgramAtom;
+ static Atom sMozCommandLineAtom;
+};
+
+#endif // NSXREMOTESERVICE_H
diff --git a/toolkit/components/remotebrowserutils/RemoteWebNavigation.js b/toolkit/components/remotebrowserutils/RemoteWebNavigation.js
new file mode 100644
index 0000000000..5790c00043
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.js
@@ -0,0 +1,139 @@
+// -*- 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 { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+function makeURI(url)
+{
+ return Services.io.newURI(url, null, null);
+}
+
+function readInputStreamToString(aStream)
+{
+ return NetUtil.readInputStreamToString(aStream, aStream.available());
+}
+
+function RemoteWebNavigation()
+{
+ this.wrappedJSObject = this;
+}
+
+RemoteWebNavigation.prototype = {
+ classDescription: "nsIWebNavigation for remote browsers",
+ classID: Components.ID("{4b56964e-cdf3-4bb8-830c-0e2dad3f4ebd}"),
+ contractID: "@mozilla.org/remote-web-navigation;1",
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebNavigation, Ci.nsISupports]),
+
+ swapBrowser: function(aBrowser) {
+ this._browser = aBrowser;
+ },
+
+ LOAD_FLAGS_MASK: 65535,
+ LOAD_FLAGS_NONE: 0,
+ LOAD_FLAGS_IS_REFRESH: 16,
+ LOAD_FLAGS_IS_LINK: 32,
+ LOAD_FLAGS_BYPASS_HISTORY: 64,
+ LOAD_FLAGS_REPLACE_HISTORY: 128,
+ LOAD_FLAGS_BYPASS_CACHE: 256,
+ LOAD_FLAGS_BYPASS_PROXY: 512,
+ LOAD_FLAGS_CHARSET_CHANGE: 1024,
+ LOAD_FLAGS_STOP_CONTENT: 2048,
+ LOAD_FLAGS_FROM_EXTERNAL: 4096,
+ LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP: 8192,
+ LOAD_FLAGS_FIRST_LOAD: 16384,
+ LOAD_FLAGS_ALLOW_POPUPS: 32768,
+ LOAD_FLAGS_BYPASS_CLASSIFIER: 65536,
+ LOAD_FLAGS_FORCE_ALLOW_COOKIES: 131072,
+
+ STOP_NETWORK: 1,
+ STOP_CONTENT: 2,
+ STOP_ALL: 3,
+
+ canGoBack: false,
+ canGoForward: false,
+ goBack: function() {
+ this._sendMessage("WebNavigation:GoBack", {});
+ },
+ goForward: function() {
+ this._sendMessage("WebNavigation:GoForward", {});
+ },
+ gotoIndex: function(aIndex) {
+ this._sendMessage("WebNavigation:GotoIndex", {index: aIndex});
+ },
+ loadURI: function(aURI, aLoadFlags, aReferrer, aPostData, aHeaders) {
+ this.loadURIWithOptions(aURI, aLoadFlags, aReferrer,
+ Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+ aPostData, aHeaders, null);
+ },
+ loadURIWithOptions: function(aURI, aLoadFlags, aReferrer, aReferrerPolicy,
+ aPostData, aHeaders, aBaseURI) {
+ this._sendMessage("WebNavigation:LoadURI", {
+ uri: aURI,
+ flags: aLoadFlags,
+ referrer: aReferrer ? aReferrer.spec : null,
+ referrerPolicy: aReferrerPolicy,
+ postData: aPostData ? readInputStreamToString(aPostData) : null,
+ headers: aHeaders ? readInputStreamToString(aHeaders) : null,
+ baseURI: aBaseURI ? aBaseURI.spec : null,
+ });
+ },
+ setOriginAttributesBeforeLoading: function(aOriginAttributes) {
+ this._sendMessage("WebNavigation:SetOriginAttributes", {
+ originAttributes: aOriginAttributes,
+ });
+ },
+ reload: function(aReloadFlags) {
+ this._sendMessage("WebNavigation:Reload", {flags: aReloadFlags});
+ },
+ stop: function(aStopFlags) {
+ this._sendMessage("WebNavigation:Stop", {flags: aStopFlags});
+ },
+
+ get document() {
+ return this._browser.contentDocument;
+ },
+
+ _currentURI: null,
+ get currentURI() {
+ if (!this._currentURI) {
+ this._currentURI = makeURI("about:blank");
+ }
+
+ return this._currentURI;
+ },
+ set currentURI(aURI) {
+ this.loadURI(aURI.spec, null, null, null);
+ },
+
+ referringURI: null,
+
+ // Bug 1233803 - accessing the sessionHistory of remote browsers should be
+ // done in content scripts.
+ get sessionHistory() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ set sessionHistory(aValue) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ _sendMessage: function(aMessage, aData) {
+ try {
+ this._browser.messageManager.sendAsyncMessage(aMessage, aData);
+ }
+ catch (e) {
+ Cu.reportError(e);
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RemoteWebNavigation]);
diff --git a/toolkit/components/remotebrowserutils/moz.build b/toolkit/components/remotebrowserutils/moz.build
new file mode 100644
index 0000000000..9cfc4a9762
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/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 += [
+ 'remotebrowserutils.manifest',
+ 'RemoteWebNavigation.js',
+]
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
diff --git a/toolkit/components/remotebrowserutils/remotebrowserutils.manifest b/toolkit/components/remotebrowserutils/remotebrowserutils.manifest
new file mode 100644
index 0000000000..d762d65a0c
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/remotebrowserutils.manifest
@@ -0,0 +1,2 @@
+component {4b56964e-cdf3-4bb8-830c-0e2dad3f4ebd} RemoteWebNavigation.js process=main
+contract @mozilla.org/remote-web-navigation;1 {4b56964e-cdf3-4bb8-830c-0e2dad3f4ebd} process=main \ No newline at end of file
diff --git a/toolkit/components/remotebrowserutils/tests/browser/.eslintrc.js b/toolkit/components/remotebrowserutils/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser.ini b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
new file mode 100644
index 0000000000..916d0f9cb6
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+run-if = e10s
+support-files =
+ dummy_page.html
+
+[browser_RemoteWebNavigation.js]
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js
new file mode 100644
index 0000000000..106758e81a
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js
@@ -0,0 +1,156 @@
+/* eslint-env mozilla/frame-script */
+
+const DUMMY1 = "http://example.com/browser/toolkit/modules/tests/browser/dummy_page.html";
+const DUMMY2 = "http://example.org/browser/toolkit/modules/tests/browser/dummy_page.html"
+
+function waitForLoad(uri) {
+ return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
+}
+
+function waitForPageShow(browser = gBrowser.selectedBrowser) {
+ return BrowserTestUtils.waitForContentEvent(browser, "pageshow", true);
+}
+
+function makeURI(url) {
+ return Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).
+ newURI(url, null, null);
+}
+
+// Tests that loadURI accepts a referrer and it is included in the load.
+add_task(function* test_referrer() {
+ gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(DUMMY1,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ makeURI(DUMMY2),
+ null, null);
+ yield waitForLoad(DUMMY1);
+
+ yield ContentTask.spawn(browser, [ DUMMY1, DUMMY2 ], function([dummy1, dummy2]) {
+ is(content.location.href, dummy1, "Should have loaded the right URL");
+ is(content.document.referrer, dummy2, "Should have the right referrer");
+ });
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that remote access to webnavigation.sessionHistory works.
+add_task(function* test_history() {
+ function checkHistoryIndex(browser, n) {
+ return ContentTask.spawn(browser, n, function(n) {
+ let history = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+ is(history.index, n, "Should be at the right place in history");
+ });
+ }
+ gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(DUMMY1,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+ yield waitForLoad(DUMMY1);
+
+ browser.webNavigation.loadURI(DUMMY2,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+ yield waitForLoad(DUMMY2);
+
+ yield ContentTask.spawn(browser, [DUMMY1, DUMMY2], function([dummy1, dummy2]) {
+ let history = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+ is(history.count, 2, "Should be two history items");
+ is(history.index, 1, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(0, false);
+ is(entry.URI.spec, dummy1, "Should have the right history entry");
+ entry = history.getEntryAtIndex(1, false);
+ is(entry.URI.spec, dummy2, "Should have the right history entry");
+ });
+
+ let promise = waitForPageShow();
+ browser.webNavigation.goBack();
+ yield promise;
+ yield checkHistoryIndex(browser, 0);
+
+ promise = waitForPageShow();
+ browser.webNavigation.goForward();
+ yield promise;
+ yield checkHistoryIndex(browser, 1);
+
+ promise = waitForPageShow();
+ browser.webNavigation.gotoIndex(0);
+ yield promise;
+ yield checkHistoryIndex(browser, 0);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that load flags are passed through to the content process.
+add_task(function* test_flags() {
+ function checkHistory(browser, { count, index }) {
+ return ContentTask.spawn(browser, [ DUMMY2, count, index ],
+ function([ dummy2, count, index ]) {
+ let history = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISHistory);
+ is(history.count, count, "Should be one history item");
+ is(history.index, index, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(index, false);
+ is(entry.URI.spec, dummy2, "Should have the right history entry");
+ });
+ }
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(DUMMY1,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+ yield waitForLoad(DUMMY1);
+
+ browser.webNavigation.loadURI(DUMMY2,
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ null, null, null);
+ yield waitForLoad(DUMMY2);
+ yield checkHistory(browser, { count: 1, index: 0 });
+
+ browser.webNavigation.loadURI(DUMMY1,
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ null, null, null);
+ yield waitForLoad(DUMMY1);
+ yield checkHistory(browser, { count: 1, index: 0 });
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that attempts to use unsupported arguments throw an exception.
+add_task(function* test_badarguments() {
+ if (!gMultiProcessBrowser)
+ return;
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ let browser = gBrowser.selectedBrowser;
+
+ try {
+ browser.webNavigation.loadURI(DUMMY1,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, {}, null);
+ ok(false, "Should have seen an exception from trying to pass some postdata");
+ }
+ catch (e) {
+ ok(true, "Should have seen an exception from trying to pass some postdata");
+ }
+
+ try {
+ browser.webNavigation.loadURI(DUMMY1,
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, {});
+ ok(false, "Should have seen an exception from trying to pass some headers");
+ }
+ catch (e) {
+ ok(true, "Should have seen an exception from trying to pass some headers");
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html
new file mode 100644
index 0000000000..c1c9a4e043
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p>Page</p>
+</body>
+</html>
diff --git a/toolkit/components/satchel/AutoCompletePopup.jsm b/toolkit/components/satchel/AutoCompletePopup.jsm
new file mode 100644
index 0000000000..7604e7bd54
--- /dev/null
+++ b/toolkit/components/satchel/AutoCompletePopup.jsm
@@ -0,0 +1,317 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// AutoCompleteResultView is an abstraction around a list of results
+// we got back up from browser-content.js. It implements enough of
+// nsIAutoCompleteController and nsIAutoCompleteInput to make the
+// richlistbox popup work.
+var AutoCompleteResultView = {
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteController,
+ Ci.nsIAutoCompleteInput]),
+
+ // Private variables
+ results: [],
+
+ // nsIAutoCompleteController
+ get matchCount() {
+ return this.results.length;
+ },
+
+ getValueAt(index) {
+ return this.results[index].value;
+ },
+
+ getLabelAt(index) {
+ // Unused by richlist autocomplete - see getCommentAt.
+ return "";
+ },
+
+ getCommentAt(index) {
+ // The richlist autocomplete popup uses comment for its main
+ // display of an item, which is why we're returning the label
+ // here instead.
+ return this.results[index].label;
+ },
+
+ getStyleAt(index) {
+ return this.results[index].style;
+ },
+
+ getImageAt(index) {
+ return this.results[index].image;
+ },
+
+ handleEnter: function(aIsPopupSelection) {
+ AutoCompletePopup.handleEnter(aIsPopupSelection);
+ },
+
+ stopSearch: function() {},
+
+ searchString: "",
+
+ // nsIAutoCompleteInput
+ get controller() {
+ return this;
+ },
+
+ get popup() {
+ return null;
+ },
+
+ _focus() {
+ AutoCompletePopup.requestFocus();
+ },
+
+ // Internal JS-only API
+ clearResults: function() {
+ this.results = [];
+ },
+
+ setResults: function(results) {
+ this.results = results;
+ },
+};
+
+this.AutoCompletePopup = {
+ MESSAGES: [
+ "FormAutoComplete:SelectBy",
+ "FormAutoComplete:GetSelectedIndex",
+ "FormAutoComplete:SetSelectedIndex",
+ "FormAutoComplete:MaybeOpenPopup",
+ "FormAutoComplete:ClosePopup",
+ "FormAutoComplete:Disconnect",
+ "FormAutoComplete:RemoveEntry",
+ "FormAutoComplete:Invalidate",
+ ],
+
+ init: function() {
+ for (let msg of this.MESSAGES) {
+ Services.mm.addMessageListener(msg, this);
+ }
+ },
+
+ uninit: function() {
+ for (let msg of this.MESSAGES) {
+ Services.mm.removeMessageListener(msg, this);
+ }
+ },
+
+ handleEvent: function(evt) {
+ switch (evt.type) {
+ case "popupshowing": {
+ this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
+ break;
+ }
+
+ case "popuphidden": {
+ AutoCompleteResultView.clearResults();
+ this.sendMessageToBrowser("FormAutoComplete:PopupClosed");
+ // adjustHeight clears the height from the popup so that
+ // we don't have a big shrink effect if we closed with a
+ // large list, and then open on a small one.
+ this.openedPopup.adjustHeight();
+ this.openedPopup = null;
+ this.weakBrowser = null;
+ evt.target.removeEventListener("popuphidden", this);
+ evt.target.removeEventListener("popupshowing", this);
+ break;
+ }
+ }
+ },
+
+ // Along with being called internally by the receiveMessage handler,
+ // this function is also called directly by the login manager, which
+ // uses a single message to fill in the autocomplete results. See
+ // "RemoteLogins:autoCompleteLogins".
+ showPopupWithResults: function({ browser, rect, dir, results }) {
+ if (!results.length || this.openedPopup) {
+ // We shouldn't ever be showing an empty popup, and if we
+ // already have a popup open, the old one needs to close before
+ // we consider opening a new one.
+ return;
+ }
+
+ let window = browser.ownerDocument.defaultView;
+ let tabbrowser = window.gBrowser;
+ if (Services.focus.activeWindow != window ||
+ tabbrowser.selectedBrowser != browser) {
+ // We were sent a message from a window or tab that went into the
+ // background, so we'll ignore it for now.
+ return;
+ }
+
+ this.weakBrowser = Cu.getWeakReference(browser);
+ this.openedPopup = browser.autoCompletePopup;
+ this.openedPopup.hidden = false;
+ // don't allow the popup to become overly narrow
+ this.openedPopup.setAttribute("width", Math.max(100, rect.width));
+ this.openedPopup.style.direction = dir;
+
+ AutoCompleteResultView.setResults(results);
+ this.openedPopup.view = AutoCompleteResultView;
+ this.openedPopup.selectedIndex = -1;
+
+ if (results.length) {
+ // Reset fields that were set from the last time the search popup was open
+ this.openedPopup.mInput = AutoCompleteResultView;
+ this.openedPopup.showCommentColumn = false;
+ this.openedPopup.showImageColumn = false;
+ this.openedPopup.addEventListener("popuphidden", this);
+ this.openedPopup.addEventListener("popupshowing", this);
+ this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
+ rect.width, rect.height, false,
+ false);
+ this.openedPopup.invalidate();
+ } else {
+ this.closePopup();
+ }
+ },
+
+ invalidate(results) {
+ if (!this.openedPopup) {
+ return;
+ }
+
+ if (!results.length) {
+ this.closePopup();
+ } else {
+ AutoCompleteResultView.setResults(results);
+ this.openedPopup.invalidate();
+ }
+ },
+
+ closePopup() {
+ if (this.openedPopup) {
+ // Note that hidePopup() closes the popup immediately,
+ // so popuphiding or popuphidden events will be fired
+ // and handled during this call.
+ this.openedPopup.hidePopup();
+ }
+ },
+
+ removeLogin(login) {
+ Services.logins.removeLogin(login);
+ },
+
+ receiveMessage: function(message) {
+ if (!message.target.autoCompletePopup) {
+ // Returning false to pacify ESLint, but this return value is
+ // ignored by the messaging infrastructure.
+ return false;
+ }
+
+ switch (message.name) {
+ case "FormAutoComplete:SelectBy": {
+ if (this.openedPopup) {
+ this.openedPopup.selectBy(message.data.reverse, message.data.page);
+ }
+ break;
+ }
+
+ case "FormAutoComplete:GetSelectedIndex": {
+ if (this.openedPopup) {
+ return this.openedPopup.selectedIndex;
+ }
+ // If the popup was closed, then the selection
+ // has not changed.
+ return -1;
+ }
+
+ case "FormAutoComplete:SetSelectedIndex": {
+ let { index } = message.data;
+ if (this.openedPopup) {
+ this.openedPopup.selectedIndex = index;
+ }
+ break;
+ }
+
+ case "FormAutoComplete:MaybeOpenPopup": {
+ let { results, rect, dir } = message.data;
+ this.showPopupWithResults({ browser: message.target, rect, dir,
+ results });
+ break;
+ }
+
+ case "FormAutoComplete:Invalidate": {
+ let { results } = message.data;
+ this.invalidate(results);
+ break;
+ }
+
+ case "FormAutoComplete:ClosePopup": {
+ this.closePopup();
+ break;
+ }
+
+ case "FormAutoComplete:Disconnect": {
+ // The controller stopped controlling the current input, so clear
+ // any cached data. This is necessary cause otherwise we'd clear data
+ // only when starting a new search, but the next input could not support
+ // autocomplete and it would end up inheriting the existing data.
+ AutoCompleteResultView.clearResults();
+ break;
+ }
+ }
+ // Returning false to pacify ESLint, but this return value is
+ // ignored by the messaging infrastructure.
+ return false;
+ },
+
+ /**
+ * Despite its name, this handleEnter is only called when the user clicks on
+ * one of the items in the popup since the popup is rendered in the parent process.
+ * The real controller's handleEnter is called directly in the content process
+ * for other methods of completing a selection (e.g. using the tab or enter
+ * keys) since the field with focus is in that process.
+ */
+ handleEnter(aIsPopupSelection) {
+ if (this.openedPopup) {
+ this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
+ selectedIndex: this.openedPopup.selectedIndex,
+ isPopupSelection: aIsPopupSelection,
+ });
+ }
+ },
+
+ /**
+ * If a browser exists that AutoCompletePopup knows about,
+ * sends it a message. Otherwise, this is a no-op.
+ *
+ * @param {string} msgName
+ * The name of the message to send.
+ * @param {object} data
+ * The optional data to send with the message.
+ */
+ sendMessageToBrowser(msgName, data) {
+ let browser = this.weakBrowser ? this.weakBrowser.get()
+ : null;
+ if (browser) {
+ browser.messageManager.sendAsyncMessage(msgName, data);
+ }
+ },
+
+ stopSearch: function() {},
+
+ /**
+ * Sends a message to the browser requesting that the input
+ * that the AutoCompletePopup is open for be focused.
+ */
+ requestFocus: function() {
+ if (this.openedPopup) {
+ this.sendMessageToBrowser("FormAutoComplete:Focus");
+ }
+ },
+}
diff --git a/toolkit/components/satchel/FormHistory.jsm b/toolkit/components/satchel/FormHistory.jsm
new file mode 100644
index 0000000000..3d4a9fc436
--- /dev/null
+++ b/toolkit/components/satchel/FormHistory.jsm
@@ -0,0 +1,1119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * FormHistory
+ *
+ * Used to store values that have been entered into forms which may later
+ * be used to automatically fill in the values when the form is visited again.
+ *
+ * search(terms, queryData, callback)
+ * Look up values that have been previously stored.
+ * terms - array of terms to return data for
+ * queryData - object that contains the query terms
+ * The query object contains properties for each search criteria to match, where the value
+ * of the property specifies the value that term must have. For example,
+ * { term1: value1, term2: value2 }
+ * callback - callback that is called when results are available or an error occurs.
+ * The callback is passed a result array containing each found entry. Each element in
+ * the array is an object containing a property for each search term specified by 'terms'.
+ * count(queryData, callback)
+ * Find the number of stored entries that match the given criteria.
+ * queryData - array of objects that indicate the query. See the search method for details.
+ * callback - callback that is called when results are available or an error occurs.
+ * The callback is passed the number of found entries.
+ * update(changes, callback)
+ * Write data to form history storage.
+ * changes - an array of changes to be made. If only one change is to be made, it
+ * may be passed as an object rather than a one-element array.
+ * Each change object is of the form:
+ * { op: operation, term1: value1, term2: value2, ... }
+ * Valid operations are:
+ * add - add a new entry
+ * update - update an existing entry
+ * remove - remove an entry
+ * bump - update the last accessed time on an entry
+ * The terms specified allow matching of one or more specific entries. If no terms
+ * are specified then all entries are matched. This means that { op: "remove" } is
+ * used to remove all entries and clear the form history.
+ * callback - callback that is called when results have been stored.
+ * getAutoCompeteResults(searchString, params, callback)
+ * Retrieve an array of form history values suitable for display in an autocomplete list.
+ * Returns an mozIStoragePendingStatement that can be used to cancel the operation if
+ * needed.
+ * searchString - the string to search for, typically the entered value of a textbox
+ * params - zero or more filter arguments:
+ * fieldname - form field name
+ * agedWeight
+ * bucketSize
+ * expiryDate
+ * maxTimeGroundings
+ * timeGroupingSize
+ * prefixWeight
+ * boundaryWeight
+ * callback - callback that is called with the array of results. Each result in the array
+ * is an object with four arguments:
+ * text, textLowerCase, frecency, totalScore
+ * schemaVersion
+ * This property holds the version of the database schema
+ *
+ * Terms:
+ * guid - entry identifier. For 'add', a guid will be generated.
+ * fieldname - form field name
+ * value - form value
+ * timesUsed - the number of times the entry has been accessed
+ * firstUsed - the time the the entry was first created
+ * lastUsed - the time the entry was last accessed
+ * firstUsedStart - search for entries created after or at this time
+ * firstUsedEnd - search for entries created before or at this time
+ * lastUsedStart - search for entries last accessed after or at this time
+ * lastUsedEnd - search for entries last accessed before or at this time
+ * newGuid - a special case valid only for 'update' and allows the guid for
+ * an existing record to be updated. The 'guid' term is the only
+ * other term which can be used (ie, you can not also specify a
+ * fieldname, value etc) and indicates the guid of the existing
+ * record that should be updated.
+ *
+ * In all of the above methods, the callback argument should be an object with
+ * handleResult(result), handleFailure(error) and handleCompletion(reason) functions.
+ * For search and getAutoCompeteResults, result is an object containing the desired
+ * properties. For count, result is the integer count. For, update, handleResult is
+ * not called. For handleCompletion, reason is either 0 if successful or 1 if
+ * an error occurred.
+ */
+
+this.EXPORTED_SYMBOLS = ["FormHistory"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidService",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+const DB_SCHEMA_VERSION = 4;
+const DAY_IN_MS = 86400000; // 1 day in milliseconds
+const MAX_SEARCH_TOKENS = 10;
+const NOOP = function noop() {};
+
+var supportsDeletedTable = AppConstants.platform == "android";
+
+var Prefs = {
+ initialized: false,
+
+ get debug() { this.ensureInitialized(); return this._debug; },
+ get enabled() { this.ensureInitialized(); return this._enabled; },
+ get expireDays() { this.ensureInitialized(); return this._expireDays; },
+
+ ensureInitialized: function() {
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ this._debug = Services.prefs.getBoolPref("browser.formfill.debug");
+ this._enabled = Services.prefs.getBoolPref("browser.formfill.enable");
+ this._expireDays = Services.prefs.getIntPref("browser.formfill.expire_days");
+ }
+};
+
+function log(aMessage) {
+ if (Prefs.debug) {
+ Services.console.logStringMessage("FormHistory: " + aMessage);
+ }
+}
+
+function sendNotification(aType, aData) {
+ if (typeof aData == "string") {
+ let strWrapper = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ strWrapper.data = aData;
+ aData = strWrapper;
+ }
+ else if (typeof aData == "number") {
+ let intWrapper = Cc["@mozilla.org/supports-PRInt64;1"].
+ createInstance(Ci.nsISupportsPRInt64);
+ intWrapper.data = aData;
+ aData = intWrapper;
+ }
+ else if (aData) {
+ throw Components.Exception("Invalid type " + (typeof aType) + " passed to sendNotification",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ Services.obs.notifyObservers(aData, "satchel-storage-changed", aType);
+}
+
+/**
+ * Current database schema
+ */
+
+const dbSchema = {
+ tables : {
+ moz_formhistory : {
+ "id" : "INTEGER PRIMARY KEY",
+ "fieldname" : "TEXT NOT NULL",
+ "value" : "TEXT NOT NULL",
+ "timesUsed" : "INTEGER",
+ "firstUsed" : "INTEGER",
+ "lastUsed" : "INTEGER",
+ "guid" : "TEXT",
+ },
+ moz_deleted_formhistory: {
+ "id" : "INTEGER PRIMARY KEY",
+ "timeDeleted" : "INTEGER",
+ "guid" : "TEXT"
+ }
+ },
+ indices : {
+ moz_formhistory_index : {
+ table : "moz_formhistory",
+ columns : [ "fieldname" ]
+ },
+ moz_formhistory_lastused_index : {
+ table : "moz_formhistory",
+ columns : [ "lastUsed" ]
+ },
+ moz_formhistory_guid_index : {
+ table : "moz_formhistory",
+ columns : [ "guid" ]
+ },
+ }
+};
+
+/**
+ * Validating and processing API querying data
+ */
+
+const validFields = [
+ "fieldname",
+ "value",
+ "timesUsed",
+ "firstUsed",
+ "lastUsed",
+ "guid",
+];
+
+const searchFilters = [
+ "firstUsedStart",
+ "firstUsedEnd",
+ "lastUsedStart",
+ "lastUsedEnd",
+];
+
+function validateOpData(aData, aDataType) {
+ let thisValidFields = validFields;
+ // A special case to update the GUID - in this case there can be a 'newGuid'
+ // field and of the normally valid fields, only 'guid' is accepted.
+ if (aDataType == "Update" && "newGuid" in aData) {
+ thisValidFields = ["guid", "newGuid"];
+ }
+ for (let field in aData) {
+ if (field != "op" && thisValidFields.indexOf(field) == -1) {
+ throw Components.Exception(
+ aDataType + " query contains an unrecognized field: " + field,
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ }
+ return aData;
+}
+
+function validateSearchData(aData, aDataType) {
+ for (let field in aData) {
+ if (field != "op" && validFields.indexOf(field) == -1 && searchFilters.indexOf(field) == -1) {
+ throw Components.Exception(
+ aDataType + " query contains an unrecognized field: " + field,
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ }
+}
+
+function makeQueryPredicates(aQueryData, delimiter = ' AND ') {
+ return Object.keys(aQueryData).map(function(field) {
+ if (field == "firstUsedStart") {
+ return "firstUsed >= :" + field;
+ } else if (field == "firstUsedEnd") {
+ return "firstUsed <= :" + field;
+ } else if (field == "lastUsedStart") {
+ return "lastUsed >= :" + field;
+ } else if (field == "lastUsedEnd") {
+ return "lastUsed <= :" + field;
+ }
+ return field + " = :" + field;
+ }).join(delimiter);
+}
+
+/**
+ * Storage statement creation and parameter binding
+ */
+
+function makeCountStatement(aSearchData) {
+ let query = "SELECT COUNT(*) AS numEntries FROM moz_formhistory";
+ let queryTerms = makeQueryPredicates(aSearchData);
+ if (queryTerms) {
+ query += " WHERE " + queryTerms;
+ }
+ return dbCreateAsyncStatement(query, aSearchData);
+}
+
+function makeSearchStatement(aSearchData, aSelectTerms) {
+ let query = "SELECT " + aSelectTerms.join(", ") + " FROM moz_formhistory";
+ let queryTerms = makeQueryPredicates(aSearchData);
+ if (queryTerms) {
+ query += " WHERE " + queryTerms;
+ }
+
+ return dbCreateAsyncStatement(query, aSearchData);
+}
+
+function makeAddStatement(aNewData, aNow, aBindingArrays) {
+ let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
+ "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
+
+ aNewData.timesUsed = aNewData.timesUsed || 1;
+ aNewData.firstUsed = aNewData.firstUsed || aNow;
+ aNewData.lastUsed = aNewData.lastUsed || aNow;
+ return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
+}
+
+function makeBumpStatement(aGuid, aNow, aBindingArrays) {
+ let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE guid = :guid";
+ let queryParams = {
+ lastUsed : aNow,
+ guid : aGuid,
+ };
+
+ return dbCreateAsyncStatement(query, queryParams, aBindingArrays);
+}
+
+function makeRemoveStatement(aSearchData, aBindingArrays) {
+ let query = "DELETE FROM moz_formhistory";
+ let queryTerms = makeQueryPredicates(aSearchData);
+
+ if (queryTerms) {
+ log("removeEntries");
+ query += " WHERE " + queryTerms;
+ } else {
+ log("removeAllEntries");
+ // Not specifying any fields means we should remove all entries. We
+ // won't need to modify the query in this case.
+ }
+
+ return dbCreateAsyncStatement(query, aSearchData, aBindingArrays);
+}
+
+function makeUpdateStatement(aGuid, aNewData, aBindingArrays) {
+ let query = "UPDATE moz_formhistory SET ";
+ let queryTerms = makeQueryPredicates(aNewData, ', ');
+
+ if (!queryTerms) {
+ throw Components.Exception("Update query must define fields to modify.",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ query += queryTerms + " WHERE guid = :existing_guid";
+ aNewData["existing_guid"] = aGuid;
+
+ return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
+}
+
+function makeMoveToDeletedStatement(aGuid, aNow, aData, aBindingArrays) {
+ if (supportsDeletedTable) {
+ let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted)";
+ let queryTerms = makeQueryPredicates(aData);
+
+ if (aGuid) {
+ query += " VALUES (:guid, :timeDeleted)";
+ } else {
+ // TODO: Add these items to the deleted items table once we've sorted
+ // out the issues from bug 756701
+ if (!queryTerms)
+ return undefined;
+
+ query += " SELECT guid, :timeDeleted FROM moz_formhistory WHERE " + queryTerms;
+ }
+
+ aData.timeDeleted = aNow;
+
+ return dbCreateAsyncStatement(query, aData, aBindingArrays);
+ }
+
+ return null;
+}
+
+function generateGUID() {
+ // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}"
+ let uuid = uuidService.generateUUID().toString();
+ let raw = ""; // A string with the low bytes set to random values
+ let bytes = 0;
+ for (let i = 1; bytes < 12 ; i+= 2) {
+ // Skip dashes
+ if (uuid[i] == "-")
+ i++;
+ let hexVal = parseInt(uuid[i] + uuid[i + 1], 16);
+ raw += String.fromCharCode(hexVal);
+ bytes++;
+ }
+ return btoa(raw);
+}
+
+/**
+ * Database creation and access
+ */
+
+var _dbConnection = null;
+XPCOMUtils.defineLazyGetter(this, "dbConnection", function() {
+ let dbFile;
+
+ try {
+ dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+ dbFile.append("formhistory.sqlite");
+ log("Opening database at " + dbFile.path);
+
+ _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+ dbInit();
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
+ throw e;
+ dbCleanup(dbFile);
+ _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+ dbInit();
+ }
+
+ return _dbConnection;
+});
+
+
+var dbStmts = new Map();
+
+/*
+ * dbCreateAsyncStatement
+ *
+ * Creates a statement, wraps it, and then does parameter replacement
+ */
+function dbCreateAsyncStatement(aQuery, aParams, aBindingArrays) {
+ if (!aQuery)
+ return null;
+
+ let stmt = dbStmts.get(aQuery);
+ if (!stmt) {
+ log("Creating new statement for query: " + aQuery);
+ stmt = dbConnection.createAsyncStatement(aQuery);
+ dbStmts.set(aQuery, stmt);
+ }
+
+ if (aBindingArrays) {
+ let bindingArray = aBindingArrays.get(stmt);
+ if (!bindingArray) {
+ // first time using a particular statement in update
+ bindingArray = stmt.newBindingParamsArray();
+ aBindingArrays.set(stmt, bindingArray);
+ }
+
+ if (aParams) {
+ let bindingParams = bindingArray.newBindingParams();
+ for (let field in aParams) {
+ bindingParams.bindByName(field, aParams[field]);
+ }
+ bindingArray.addParams(bindingParams);
+ }
+ } else if (aParams) {
+ for (let field in aParams) {
+ stmt.params[field] = aParams[field];
+ }
+ }
+
+ return stmt;
+}
+
+/**
+ * dbInit
+ *
+ * Attempts to initialize the database. This creates the file if it doesn't
+ * exist, performs any migrations, etc.
+ */
+function dbInit() {
+ log("Initializing Database");
+
+ if (!_dbConnection.tableExists("moz_formhistory")) {
+ dbCreate();
+ return;
+ }
+
+ // When FormHistory is released, we will no longer support the various schema versions prior to
+ // this release that nsIFormHistory2 once did.
+ let version = _dbConnection.schemaVersion;
+ if (version < 3) {
+ throw Components.Exception("DB version is unsupported.",
+ Cr.NS_ERROR_FILE_CORRUPTED);
+ } else if (version != DB_SCHEMA_VERSION) {
+ dbMigrate(version);
+ }
+}
+
+function dbCreate() {
+ log("Creating DB -- tables");
+ for (let name in dbSchema.tables) {
+ let table = dbSchema.tables[name];
+ let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
+ log("Creating table " + name + " with " + tSQL);
+ _dbConnection.createTable(name, tSQL);
+ }
+
+ log("Creating DB -- indices");
+ for (let name in dbSchema.indices) {
+ let index = dbSchema.indices[name];
+ let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
+ "(" + index.columns.join(", ") + ")";
+ _dbConnection.executeSimpleSQL(statement);
+ }
+
+ _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+}
+
+function dbMigrate(oldVersion) {
+ log("Attempting to migrate from version " + oldVersion);
+
+ if (oldVersion > DB_SCHEMA_VERSION) {
+ log("Downgrading to version " + DB_SCHEMA_VERSION);
+ // User's DB is newer. Sanity check that our expected columns are
+ // present, and if so mark the lower version and merrily continue
+ // on. If the columns are borked, something is wrong so blow away
+ // the DB and start from scratch. [Future incompatible upgrades
+ // should switch to a different table or file.]
+
+ if (!dbAreExpectedColumnsPresent()) {
+ throw Components.Exception("DB is missing expected columns",
+ Cr.NS_ERROR_FILE_CORRUPTED);
+ }
+
+ // Change the stored version to the current version. If the user
+ // runs the newer code again, it will see the lower version number
+ // and re-upgrade (to fixup any entries the old code added).
+ _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+ return;
+ }
+
+ // Note that migration is currently performed synchronously.
+ _dbConnection.beginTransaction();
+
+ try {
+ for (let v = oldVersion + 1; v <= DB_SCHEMA_VERSION; v++) {
+ this.log("Upgrading to version " + v + "...");
+ Migrators["dbMigrateToVersion" + v]();
+ }
+ } catch (e) {
+ this.log("Migration failed: " + e);
+ this.dbConnection.rollbackTransaction();
+ throw e;
+ }
+
+ _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+ _dbConnection.commitTransaction();
+
+ log("DB migration completed.");
+}
+
+var Migrators = {
+ /*
+ * Updates the DB schema to v3 (bug 506402).
+ * Adds deleted form history table.
+ */
+ dbMigrateToVersion4: function dbMigrateToVersion4() {
+ if (!_dbConnection.tableExists("moz_deleted_formhistory")) {
+ let table = dbSchema.tables["moz_deleted_formhistory"];
+ let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
+ _dbConnection.createTable("moz_deleted_formhistory", tSQL);
+ }
+ }
+};
+
+/**
+ * dbAreExpectedColumnsPresent
+ *
+ * Sanity check to ensure that the columns this version of the code expects
+ * are present in the DB we're using.
+ */
+function dbAreExpectedColumnsPresent() {
+ for (let name in dbSchema.tables) {
+ let table = dbSchema.tables[name];
+ let query = "SELECT " +
+ Object.keys(table).join(", ") +
+ " FROM " + name;
+ try {
+ let stmt = _dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ } catch (e) {
+ return false;
+ }
+ }
+
+ log("verified that expected columns are present in DB.");
+ return true;
+}
+
+/**
+ * dbCleanup
+ *
+ * Called when database creation fails. Finalizes database statements,
+ * closes the database connection, deletes the database file.
+ */
+function dbCleanup(dbFile) {
+ log("Cleaning up DB file - close & remove & backup");
+
+ // Create backup file
+ let backupFile = dbFile.leafName + ".corrupt";
+ Services.storage.backupDatabaseFile(dbFile, backupFile);
+
+ dbClose(false);
+ dbFile.remove(false);
+}
+
+function dbClose(aShutdown) {
+ log("dbClose(" + aShutdown + ")");
+
+ if (aShutdown) {
+ sendNotification("formhistory-shutdown", null);
+ }
+
+ // Connection may never have been created if say open failed but we still
+ // end up calling dbClose as part of the rest of dbCleanup.
+ if (!_dbConnection) {
+ return;
+ }
+
+ log("dbClose finalize statements");
+ for (let stmt of dbStmts.values()) {
+ stmt.finalize();
+ }
+
+ dbStmts = new Map();
+
+ let closed = false;
+ _dbConnection.asyncClose(() => closed = true);
+
+ if (!aShutdown) {
+ let thread = Services.tm.currentThread;
+ while (!closed) {
+ thread.processNextEvent(true);
+ }
+ }
+}
+
+/**
+ * updateFormHistoryWrite
+ *
+ * Constructs and executes database statements from a pre-processed list of
+ * inputted changes.
+ */
+function updateFormHistoryWrite(aChanges, aCallbacks) {
+ log("updateFormHistoryWrite " + aChanges.length);
+
+ // pass 'now' down so that every entry in the batch has the same timestamp
+ let now = Date.now() * 1000;
+
+ // for each change, we either create and append a new storage statement to
+ // stmts or bind a new set of parameters to an existing storage statement.
+ // stmts and bindingArrays are updated when makeXXXStatement eventually
+ // calls dbCreateAsyncStatement.
+ let stmts = [];
+ let notifications = [];
+ let bindingArrays = new Map();
+
+ for (let change of aChanges) {
+ let operation = change.op;
+ delete change.op;
+ let stmt;
+ switch (operation) {
+ case "remove":
+ log("Remove from form history " + change);
+ let delStmt = makeMoveToDeletedStatement(change.guid, now, change, bindingArrays);
+ if (delStmt && stmts.indexOf(delStmt) == -1)
+ stmts.push(delStmt);
+ if ("timeDeleted" in change)
+ delete change.timeDeleted;
+ stmt = makeRemoveStatement(change, bindingArrays);
+ notifications.push([ "formhistory-remove", change.guid ]);
+ break;
+ case "update":
+ log("Update form history " + change);
+ let guid = change.guid;
+ delete change.guid;
+ // a special case for updating the GUID - the new value can be
+ // specified in newGuid.
+ if (change.newGuid) {
+ change.guid = change.newGuid
+ delete change.newGuid;
+ }
+ stmt = makeUpdateStatement(guid, change, bindingArrays);
+ notifications.push([ "formhistory-update", guid ]);
+ break;
+ case "bump":
+ log("Bump form history " + change);
+ if (change.guid) {
+ stmt = makeBumpStatement(change.guid, now, bindingArrays);
+ notifications.push([ "formhistory-update", change.guid ]);
+ } else {
+ change.guid = generateGUID();
+ stmt = makeAddStatement(change, now, bindingArrays);
+ notifications.push([ "formhistory-add", change.guid ]);
+ }
+ break;
+ case "add":
+ log("Add to form history " + change);
+ change.guid = generateGUID();
+ stmt = makeAddStatement(change, now, bindingArrays);
+ notifications.push([ "formhistory-add", change.guid ]);
+ break;
+ default:
+ // We should've already guaranteed that change.op is one of the above
+ throw Components.Exception("Invalid operation " + operation,
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ // As identical statements are reused, only add statements if they aren't already present.
+ if (stmt && stmts.indexOf(stmt) == -1) {
+ stmts.push(stmt);
+ }
+ }
+
+ for (let stmt of stmts) {
+ stmt.bindParameters(bindingArrays.get(stmt));
+ }
+
+ let handlers = {
+ handleCompletion : function(aReason) {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ for (let [notification, param] of notifications) {
+ // We're either sending a GUID or nothing at all.
+ sendNotification(notification, param);
+ }
+ }
+
+ if (aCallbacks && aCallbacks.handleCompletion) {
+ aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+ }
+ },
+ handleError : function(aError) {
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError(aError);
+ }
+ },
+ handleResult : NOOP
+ };
+
+ dbConnection.executeAsync(stmts, stmts.length, handlers);
+}
+
+/**
+ * Functions that expire entries in form history and shrinks database
+ * afterwards as necessary initiated by expireOldEntries.
+ */
+
+/**
+ * expireOldEntriesDeletion
+ *
+ * Removes entries from database.
+ */
+function expireOldEntriesDeletion(aExpireTime, aBeginningCount) {
+ log("expireOldEntriesDeletion(" + aExpireTime + "," + aBeginningCount + ")");
+
+ FormHistory.update([
+ {
+ op: "remove",
+ lastUsedEnd : aExpireTime,
+ }], {
+ handleCompletion: function() {
+ expireOldEntriesVacuum(aExpireTime, aBeginningCount);
+ },
+ handleError: function(aError) {
+ log("expireOldEntriesDeletionFailure");
+ }
+ });
+}
+
+/**
+ * expireOldEntriesVacuum
+ *
+ * Counts number of entries removed and shrinks database as necessary.
+ */
+function expireOldEntriesVacuum(aExpireTime, aBeginningCount) {
+ FormHistory.count({}, {
+ handleResult: function(aEndingCount) {
+ if (aBeginningCount - aEndingCount > 500) {
+ log("expireOldEntriesVacuum");
+
+ let stmt = dbCreateAsyncStatement("VACUUM");
+ stmt.executeAsync({
+ handleResult : NOOP,
+ handleError : function(aError) {
+ log("expireVacuumError");
+ },
+ handleCompletion : NOOP
+ });
+ }
+
+ sendNotification("formhistory-expireoldentries", aExpireTime);
+ },
+ handleError: function(aError) {
+ log("expireEndCountFailure");
+ }
+ });
+}
+
+this.FormHistory = {
+ get enabled() {
+ return Prefs.enabled;
+ },
+
+ search : function formHistorySearch(aSelectTerms, aSearchData, aCallbacks) {
+ // if no terms selected, select everything
+ aSelectTerms = (aSelectTerms) ? aSelectTerms : validFields;
+ validateSearchData(aSearchData, "Search");
+
+ let stmt = makeSearchStatement(aSearchData, aSelectTerms);
+
+ let handlers = {
+ handleResult : function(aResultSet) {
+ for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
+ let result = {};
+ for (let field of aSelectTerms) {
+ result[field] = row.getResultByName(field);
+ }
+
+ if (aCallbacks && aCallbacks.handleResult) {
+ aCallbacks.handleResult(result);
+ }
+ }
+ },
+
+ handleError : function(aError) {
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError(aError);
+ }
+ },
+
+ handleCompletion : function searchCompletionHandler(aReason) {
+ if (aCallbacks && aCallbacks.handleCompletion) {
+ aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+ }
+ }
+ };
+
+ stmt.executeAsync(handlers);
+ },
+
+ count : function formHistoryCount(aSearchData, aCallbacks) {
+ validateSearchData(aSearchData, "Count");
+ let stmt = makeCountStatement(aSearchData);
+ let handlers = {
+ handleResult : function countResultHandler(aResultSet) {
+ let row = aResultSet.getNextRow();
+ let count = row.getResultByName("numEntries");
+ if (aCallbacks && aCallbacks.handleResult) {
+ aCallbacks.handleResult(count);
+ }
+ },
+
+ handleError : function(aError) {
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError(aError);
+ }
+ },
+
+ handleCompletion : function searchCompletionHandler(aReason) {
+ if (aCallbacks && aCallbacks.handleCompletion) {
+ aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+ }
+ }
+ };
+
+ stmt.executeAsync(handlers);
+ },
+
+ update : function formHistoryUpdate(aChanges, aCallbacks) {
+ // Used to keep track of how many searches have been started. When that number
+ // are finished, updateFormHistoryWrite can be called.
+ let numSearches = 0;
+ let completedSearches = 0;
+ let searchFailed = false;
+
+ function validIdentifier(change) {
+ // The identifier is only valid if one of either the guid or the (fieldname/value) are set
+ return Boolean(change.guid) != Boolean(change.fieldname && change.value);
+ }
+
+ if (!("length" in aChanges))
+ aChanges = [aChanges];
+
+ let isRemoveOperation = aChanges.every(change => change && change.op && change.op == "remove");
+ if (!Prefs.enabled && !isRemoveOperation) {
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError({
+ message: "Form history is disabled, only remove operations are allowed",
+ result: Ci.mozIStorageError.MISUSE
+ });
+ }
+ if (aCallbacks && aCallbacks.handleCompletion) {
+ aCallbacks.handleCompletion(1);
+ }
+ return;
+ }
+
+ for (let change of aChanges) {
+ switch (change.op) {
+ case "remove":
+ validateSearchData(change, "Remove");
+ continue;
+ case "update":
+ if (validIdentifier(change)) {
+ validateOpData(change, "Update");
+ if (change.guid) {
+ continue;
+ }
+ } else {
+ throw Components.Exception(
+ "update op='update' does not correctly reference a entry.",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ break;
+ case "bump":
+ if (validIdentifier(change)) {
+ validateOpData(change, "Bump");
+ if (change.guid) {
+ continue;
+ }
+ } else {
+ throw Components.Exception(
+ "update op='bump' does not correctly reference a entry.",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ break;
+ case "add":
+ if (change.guid) {
+ throw Components.Exception(
+ "op='add' cannot contain field 'guid'. Either use op='update' " +
+ "explicitly or make 'guid' undefined.",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ } else if (change.fieldname && change.value) {
+ validateOpData(change, "Add");
+ }
+ break;
+ default:
+ throw Components.Exception(
+ "update does not recognize op='" + change.op + "'",
+ Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ numSearches++;
+ let changeToUpdate = change;
+ FormHistory.search(
+ [ "guid" ],
+ {
+ fieldname : change.fieldname,
+ value : change.value
+ }, {
+ foundResult : false,
+ handleResult : function(aResult) {
+ if (this.foundResult) {
+ log("Database contains multiple entries with the same fieldname/value pair.");
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError({
+ message :
+ "Database contains multiple entries with the same fieldname/value pair.",
+ result : 19 // Constraint violation
+ });
+ }
+
+ searchFailed = true;
+ return;
+ }
+
+ this.foundResult = true;
+ changeToUpdate.guid = aResult["guid"];
+ },
+
+ handleError : function(aError) {
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError(aError);
+ }
+ },
+
+ handleCompletion : function(aReason) {
+ completedSearches++;
+ if (completedSearches == numSearches) {
+ if (!aReason && !searchFailed) {
+ updateFormHistoryWrite(aChanges, aCallbacks);
+ }
+ else if (aCallbacks && aCallbacks.handleCompletion) {
+ aCallbacks.handleCompletion(1);
+ }
+ }
+ }
+ });
+ }
+
+ if (numSearches == 0) {
+ // We don't have to wait for any statements to return.
+ updateFormHistoryWrite(aChanges, aCallbacks);
+ }
+ },
+
+ getAutoCompleteResults: function getAutoCompleteResults(searchString, params, aCallbacks) {
+ // only do substring matching when the search string contains more than one character
+ let searchTokens;
+ let where = ""
+ let boundaryCalc = "";
+ if (searchString.length > 1) {
+ searchTokens = searchString.split(/\s+/);
+
+ // build up the word boundary and prefix match bonus calculation
+ boundaryCalc = "MAX(1, :prefixWeight * (value LIKE :valuePrefix ESCAPE '/') + (";
+ // for each word, calculate word boundary weights for the SELECT clause and
+ // add word to the WHERE clause of the query
+ let tokenCalc = [];
+ let searchTokenCount = Math.min(searchTokens.length, MAX_SEARCH_TOKENS);
+ for (let i = 0; i < searchTokenCount; i++) {
+ tokenCalc.push("(value LIKE :tokenBegin" + i + " ESCAPE '/') + " +
+ "(value LIKE :tokenBoundary" + i + " ESCAPE '/')");
+ where += "AND (value LIKE :tokenContains" + i + " ESCAPE '/') ";
+ }
+ // add more weight if we have a traditional prefix match and
+ // multiply boundary bonuses by boundary weight
+ boundaryCalc += tokenCalc.join(" + ") + ") * :boundaryWeight)";
+ } else if (searchString.length == 1) {
+ where = "AND (value LIKE :valuePrefix ESCAPE '/') ";
+ boundaryCalc = "1";
+ delete params.prefixWeight;
+ delete params.boundaryWeight;
+ } else {
+ where = "";
+ boundaryCalc = "1";
+ delete params.prefixWeight;
+ delete params.boundaryWeight;
+ }
+
+ params.now = Date.now() * 1000; // convert from ms to microseconds
+
+ /* Three factors in the frecency calculation for an entry (in order of use in calculation):
+ * 1) average number of times used - items used more are ranked higher
+ * 2) how recently it was last used - items used recently are ranked higher
+ * 3) additional weight for aged entries surviving expiry - these entries are relevant
+ * since they have been used multiple times over a large time span so rank them higher
+ * The score is then divided by the bucket size and we round the result so that entries
+ * with a very similar frecency are bucketed together with an alphabetical sort. This is
+ * to reduce the amount of moving around by entries while typing.
+ */
+
+ let query = "/* do not warn (bug 496471): can't use an index */ " +
+ "SELECT value, " +
+ "ROUND( " +
+ "timesUsed / MAX(1.0, (lastUsed - firstUsed) / :timeGroupingSize) * " +
+ "MAX(1.0, :maxTimeGroupings - (:now - lastUsed) / :timeGroupingSize) * "+
+ "MAX(1.0, :agedWeight * (firstUsed < :expiryDate)) / " +
+ ":bucketSize "+
+ ", 3) AS frecency, " +
+ boundaryCalc + " AS boundaryBonuses " +
+ "FROM moz_formhistory " +
+ "WHERE fieldname=:fieldname " + where +
+ "ORDER BY ROUND(frecency * boundaryBonuses) DESC, UPPER(value) ASC";
+
+ let stmt = dbCreateAsyncStatement(query, params);
+
+ // Chicken and egg problem: Need the statement to escape the params we
+ // pass to the function that gives us the statement. So, fix it up now.
+ if (searchString.length >= 1)
+ stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
+ if (searchString.length > 1) {
+ let searchTokenCount = Math.min(searchTokens.length, MAX_SEARCH_TOKENS);
+ for (let i = 0; i < searchTokenCount; i++) {
+ let escapedToken = stmt.escapeStringForLIKE(searchTokens[i], "/");
+ stmt.params["tokenBegin" + i] = escapedToken + "%";
+ stmt.params["tokenBoundary" + i] = "% " + escapedToken + "%";
+ stmt.params["tokenContains" + i] = "%" + escapedToken + "%";
+ }
+ } else {
+ // no additional params need to be substituted into the query when the
+ // length is zero or one
+ }
+
+ let pending = stmt.executeAsync({
+ handleResult : function (aResultSet) {
+ for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
+ let value = row.getResultByName("value");
+ let frecency = row.getResultByName("frecency");
+ let entry = {
+ text : value,
+ textLowerCase : value.toLowerCase(),
+ frecency : frecency,
+ totalScore : Math.round(frecency * row.getResultByName("boundaryBonuses"))
+ };
+ if (aCallbacks && aCallbacks.handleResult) {
+ aCallbacks.handleResult(entry);
+ }
+ }
+ },
+
+ handleError : function (aError) {
+ if (aCallbacks && aCallbacks.handleError) {
+ aCallbacks.handleError(aError);
+ }
+ },
+
+ handleCompletion : function (aReason) {
+ if (aCallbacks && aCallbacks.handleCompletion) {
+ aCallbacks.handleCompletion(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ? 0 : 1);
+ }
+ }
+ });
+ return pending;
+ },
+
+ get schemaVersion() {
+ return dbConnection.schemaVersion;
+ },
+
+ // This is used only so that the test can verify deleted table support.
+ get _supportsDeletedTable() {
+ return supportsDeletedTable;
+ },
+ set _supportsDeletedTable(val) {
+ supportsDeletedTable = val;
+ },
+
+ // The remaining methods are called by FormHistoryStartup.js
+ updatePrefs: function updatePrefs() {
+ Prefs.initialized = false;
+ },
+
+ expireOldEntries: function expireOldEntries() {
+ log("expireOldEntries");
+
+ // Determine how many days of history we're supposed to keep.
+ // Calculate expireTime in microseconds
+ let expireTime = (Date.now() - Prefs.expireDays * DAY_IN_MS) * 1000;
+
+ sendNotification("formhistory-beforeexpireoldentries", expireTime);
+
+ FormHistory.count({}, {
+ handleResult: function(aBeginningCount) {
+ expireOldEntriesDeletion(expireTime, aBeginningCount);
+ },
+ handleError: function(aError) {
+ log("expireStartCountFailure");
+ }
+ });
+ },
+
+ shutdown: function shutdown() { dbClose(true); }
+};
+
+// Prevent add-ons from redefining this API
+Object.freeze(FormHistory);
diff --git a/toolkit/components/satchel/FormHistoryStartup.js b/toolkit/components/satchel/FormHistoryStartup.js
new file mode 100644
index 0000000000..05b6545602
--- /dev/null
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+function FormHistoryStartup() { }
+
+FormHistoryStartup.prototype = {
+ classID: Components.ID("{3A0012EB-007F-4BB8-AA81-A07385F77A25}"),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIFrameMessageListener
+ ]),
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "nsPref:changed":
+ FormHistory.updatePrefs();
+ break;
+ case "idle-daily":
+ case "formhistory-expire-now":
+ FormHistory.expireOldEntries();
+ break;
+ case "profile-before-change":
+ FormHistory.shutdown();
+ break;
+ case "profile-after-change":
+ this.init();
+ default:
+ break;
+ }
+ },
+
+ inited: false,
+ pendingQuery: null,
+
+ init: function()
+ {
+ if (this.inited)
+ return;
+ this.inited = true;
+
+ Services.prefs.addObserver("browser.formfill.", this, true);
+
+ // triggers needed service cleanup and db shutdown
+ Services.obs.addObserver(this, "profile-before-change", true);
+ Services.obs.addObserver(this, "formhistory-expire-now", true);
+
+ let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
+ getService(Ci.nsIMessageListenerManager);
+ messageManager.loadFrameScript("chrome://satchel/content/formSubmitListener.js", true);
+ messageManager.addMessageListener("FormHistory:FormSubmitEntries", this);
+
+ // For each of these messages, we could receive them from content,
+ // or we might receive them from the ppmm if the searchbar is
+ // having its history queried.
+ for (let manager of [messageManager, Services.ppmm]) {
+ manager.addMessageListener("FormHistory:AutoCompleteSearchAsync", this);
+ manager.addMessageListener("FormHistory:RemoveEntry", this);
+ }
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "FormHistory:FormSubmitEntries": {
+ let entries = message.data;
+ let changes = entries.map(function(entry) {
+ return {
+ op : "bump",
+ fieldname : entry.name,
+ value : entry.value,
+ }
+ });
+
+ FormHistory.update(changes);
+ break;
+ }
+
+ case "FormHistory:AutoCompleteSearchAsync": {
+ let { id, searchString, params } = message.data;
+
+ if (this.pendingQuery) {
+ this.pendingQuery.cancel();
+ this.pendingQuery = null;
+ }
+
+ let mm;
+ if (message.target instanceof Ci.nsIMessageListenerManager) {
+ // The target is the PPMM, meaning that the parent process
+ // is requesting FormHistory data on the searchbar.
+ mm = message.target;
+ } else {
+ // Otherwise, the target is a <xul:browser>.
+ mm = message.target.messageManager;
+ }
+
+ let results = [];
+ let processResults = {
+ handleResult: aResult => {
+ results.push(aResult);
+ },
+ handleCompletion: aReason => {
+ // Check that the current query is still the one we created. Our
+ // query might have been canceled shortly before completing, in
+ // that case we don't want to call the callback anymore.
+ if (query == this.pendingQuery) {
+ this.pendingQuery = null;
+ if (!aReason) {
+ mm.sendAsyncMessage("FormHistory:AutoCompleteSearchResults",
+ { id, results });
+ }
+ }
+ }
+ };
+
+ let query = FormHistory.getAutoCompleteResults(searchString, params,
+ processResults);
+ this.pendingQuery = query;
+ break;
+ }
+
+ case "FormHistory:RemoveEntry": {
+ let { inputName, value } = message.data;
+ FormHistory.update({
+ op: "remove",
+ fieldname: inputName,
+ value,
+ });
+ break;
+ }
+
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormHistoryStartup]);
diff --git a/toolkit/components/satchel/formSubmitListener.js b/toolkit/components/satchel/formSubmitListener.js
new file mode 100644
index 0000000000..ec2c18f6c7
--- /dev/null
+++ b/toolkit/components/satchel/formSubmitListener.js
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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() {
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+var satchelFormListener = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ debug : true,
+ enabled : true,
+ saveHttpsForms : true,
+
+ init : function() {
+ Services.obs.addObserver(this, "earlyformsubmit", false);
+ Services.prefs.addObserver("browser.formfill.", this, false);
+ this.updatePrefs();
+ addEventListener("unload", this, false);
+ },
+
+ updatePrefs : function () {
+ this.debug = Services.prefs.getBoolPref("browser.formfill.debug");
+ this.enabled = Services.prefs.getBoolPref("browser.formfill.enable");
+ this.saveHttpsForms = Services.prefs.getBoolPref("browser.formfill.saveHttpsForms");
+ },
+
+ // Implements the Luhn checksum algorithm as described at
+ // http://wikipedia.org/wiki/Luhn_algorithm
+ isValidCCNumber : function (ccNumber) {
+ // Remove dashes and whitespace
+ ccNumber = ccNumber.replace(/[\-\s]/g, '');
+
+ let len = ccNumber.length;
+ if (len != 9 && len != 15 && len != 16)
+ return false;
+
+ if (!/^\d+$/.test(ccNumber))
+ return false;
+
+ let total = 0;
+ for (let i = 0; i < len; i++) {
+ let ch = parseInt(ccNumber[len - i - 1]);
+ if (i % 2 == 1) {
+ // Double it, add digits together if > 10
+ ch *= 2;
+ if (ch > 9)
+ ch -= 9;
+ }
+ total += ch;
+ }
+ return total % 10 == 0;
+ },
+
+ log : function (message) {
+ if (!this.debug)
+ return;
+ dump("satchelFormListener: " + message + "\n");
+ Services.console.logStringMessage("satchelFormListener: " + message);
+ },
+
+ /* ---- dom event handler ---- */
+
+ handleEvent: function(e) {
+ switch (e.type) {
+ case "unload":
+ Services.obs.removeObserver(this, "earlyformsubmit");
+ Services.prefs.removeObserver("browser.formfill.", this);
+ break;
+
+ default:
+ this.log("Oops! Unexpected event: " + e.type);
+ break;
+ }
+ },
+
+ /* ---- nsIObserver interface ---- */
+
+ observe : function (subject, topic, data) {
+ if (topic == "nsPref:changed")
+ this.updatePrefs();
+ else
+ this.log("Oops! Unexpected notification: " + topic);
+ },
+
+ /* ---- nsIFormSubmitObserver interfaces ---- */
+
+ notify : function(form, domWin, actionURI, cancelSubmit) {
+ try {
+ // Even though the global context is for a specific browser, we
+ // can receive observer events from other tabs! Ensure this event
+ // is about our content.
+ if (domWin.top != content)
+ return;
+ if (!this.enabled)
+ return;
+
+ if (PrivateBrowsingUtils.isContentWindowPrivate(domWin))
+ return;
+
+ this.log("Form submit observer notified.");
+
+ if (!this.saveHttpsForms) {
+ if (actionURI.schemeIs("https"))
+ return;
+ if (form.ownerDocument.documentURIObject.schemeIs("https"))
+ return;
+ }
+
+ if (form.hasAttribute("autocomplete") &&
+ form.getAttribute("autocomplete").toLowerCase() == "off")
+ return;
+
+ let entries = [];
+ for (let i = 0; i < form.elements.length; i++) {
+ let input = form.elements[i];
+ if (!(input instanceof Ci.nsIDOMHTMLInputElement))
+ continue;
+
+ // Only use inputs that hold text values (not including type="password")
+ if (!input.mozIsTextField(true))
+ continue;
+
+ // Bug 394612: If Login Manager marked this input, don't save it.
+ // The login manager will deal with remembering it.
+
+ // Don't save values when autocomplete=off is present.
+ if (input.hasAttribute("autocomplete") &&
+ input.getAttribute("autocomplete").toLowerCase() == "off")
+ continue;
+
+ let value = input.value.trim();
+
+ // Don't save empty or unchanged values.
+ if (!value || value == input.defaultValue.trim())
+ continue;
+
+ // Don't save credit card numbers.
+ if (this.isValidCCNumber(value)) {
+ this.log("skipping saving a credit card number");
+ continue;
+ }
+
+ let name = input.name || input.id;
+ if (!name)
+ continue;
+
+ if (name == 'searchbar-history') {
+ this.log('addEntry for input name "' + name + '" is denied')
+ continue;
+ }
+
+ // Limit stored data to 200 characters.
+ if (name.length > 200 || value.length > 200) {
+ this.log("skipping input that has a name/value too large");
+ continue;
+ }
+
+ // Limit number of fields stored per form.
+ if (entries.length >= 100) {
+ this.log("not saving any more entries for this form.");
+ break;
+ }
+
+ entries.push({ name: name, value: value });
+ }
+
+ if (entries.length) {
+ this.log("sending entries to parent process for form " + form.id);
+ sendAsyncMessage("FormHistory:FormSubmitEntries", entries);
+ }
+ }
+ catch (e) {
+ this.log("notify failed: " + e);
+ }
+ }
+};
+
+satchelFormListener.init();
+
+})();
diff --git a/toolkit/components/satchel/jar.mn b/toolkit/components/satchel/jar.mn
new file mode 100644
index 0000000000..4b37d5dc5d
--- /dev/null
+++ b/toolkit/components/satchel/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/.
+
+toolkit.jar:
+% content satchel %content/satchel/
+ content/satchel/formSubmitListener.js
diff --git a/toolkit/components/satchel/moz.build b/toolkit/components/satchel/moz.build
new file mode 100644
index 0000000000..239f412bcf
--- /dev/null
+++ b/toolkit/components/satchel/moz.build
@@ -0,0 +1,44 @@
+# -*- 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_MANIFESTS += ['test/mochitest.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+
+XPIDL_SOURCES += [
+ 'nsIFormAutoComplete.idl',
+ 'nsIFormFillController.idl',
+ 'nsIFormHistory.idl',
+ 'nsIInputListAutoComplete.idl',
+]
+
+XPIDL_MODULE = 'satchel'
+
+SOURCES += [
+ 'nsFormFillController.cpp',
+]
+
+LOCAL_INCLUDES += [
+ '../build',
+]
+
+EXTRA_COMPONENTS += [
+ 'FormHistoryStartup.js',
+ 'nsFormAutoComplete.js',
+ 'nsFormHistory.js',
+ 'nsInputListAutoComplete.js',
+ 'satchel.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'AutoCompletePopup.jsm',
+ 'FormHistory.jsm',
+ 'nsFormAutoCompleteResult.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/satchel/nsFormAutoComplete.js b/toolkit/components/satchel/nsFormAutoComplete.js
new file mode 100644
index 0000000000..aa090479ac
--- /dev/null
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -0,0 +1,624 @@
+/* vim: set ts=4 sts=4 sw=4 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+function isAutocompleteDisabled(aField) {
+ if (aField.autocomplete !== "") {
+ return aField.autocomplete === "off";
+ }
+
+ return aField.form && aField.form.autocomplete === "off";
+}
+
+/**
+ * An abstraction to talk with the FormHistory database over
+ * the message layer. FormHistoryClient will take care of
+ * figuring out the most appropriate message manager to use,
+ * and what things to send.
+ *
+ * It is assumed that nsFormAutoComplete will only ever use
+ * one instance at a time, and will not attempt to perform more
+ * than one search request with the same instance at a time.
+ * However, nsFormAutoComplete might call remove() any number of
+ * times with the same instance of the client.
+ *
+ * @param Object with the following properties:
+ *
+ * formField (DOM node):
+ * A DOM node that we're requesting form history for.
+ *
+ * inputName (string):
+ * The name of the input to do the FormHistory look-up
+ * with. If this is searchbar-history, then formField
+ * needs to be null, otherwise constructing will throw.
+ */
+function FormHistoryClient({ formField, inputName }) {
+ if (formField && inputName != this.SEARCHBAR_ID) {
+ let window = formField.ownerDocument.defaultView;
+ let topDocShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell);
+ this.mm = topDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ } else {
+ if (inputName == this.SEARCHBAR_ID) {
+ if (formField) {
+ throw new Error("FormHistoryClient constructed with both a " +
+ "formField and an inputName. This is not " +
+ "supported, and only empty results will be " +
+ "returned.");
+ }
+ }
+ this.mm = Services.cpmm;
+ }
+
+ this.inputName = inputName;
+ this.id = FormHistoryClient.nextRequestID++;
+}
+
+FormHistoryClient.prototype = {
+ SEARCHBAR_ID: "searchbar-history",
+
+ // It is assumed that nsFormAutoComplete only uses / cares about
+ // one FormHistoryClient at a time, and won't attempt to have
+ // multiple in-flight searches occurring with the same FormHistoryClient.
+ // We use an ID number per instantiated FormHistoryClient to make
+ // sure we only respond to messages that were meant for us.
+ id: 0,
+ callback: null,
+ inputName: "",
+ mm: null,
+
+ /**
+ * Query FormHistory for some results.
+ *
+ * @param searchString (string)
+ * The string to search FormHistory for. See
+ * FormHistory.getAutoCompleteResults.
+ * @param params (object)
+ * An Object with search properties. See
+ * FormHistory.getAutoCompleteResults.
+ * @param callback
+ * A callback function that will take a single
+ * argument (the found entries).
+ */
+ requestAutoCompleteResults(searchString, params, callback) {
+ this.mm.sendAsyncMessage("FormHistory:AutoCompleteSearchAsync", {
+ id: this.id,
+ searchString,
+ params,
+ });
+
+ this.mm.addMessageListener("FormHistory:AutoCompleteSearchResults",
+ this);
+ this.callback = callback;
+ },
+
+ /**
+ * Cancel an in-flight results request. This ensures that the
+ * callback that requestAutoCompleteResults was passed is never
+ * called from this FormHistoryClient.
+ */
+ cancel() {
+ this.clearListeners();
+ },
+
+ /**
+ * Remove an item from FormHistory.
+ *
+ * @param value (string)
+ *
+ * The value to remove for this particular
+ * field.
+ */
+ remove(value) {
+ this.mm.sendAsyncMessage("FormHistory:RemoveEntry", {
+ inputName: this.inputName,
+ value,
+ });
+ },
+
+ // Private methods
+
+ receiveMessage(msg) {
+ let { id, results } = msg.data;
+ if (id != this.id) {
+ return;
+ }
+ if (!this.callback) {
+ Cu.reportError("FormHistoryClient received message with no " +
+ "callback");
+ return;
+ }
+ this.callback(results);
+ this.clearListeners();
+ },
+
+ clearListeners() {
+ this.mm.removeMessageListener("FormHistory:AutoCompleteSearchResults",
+ this);
+ this.callback = null;
+ },
+};
+
+FormHistoryClient.nextRequestID = 1;
+
+
+function FormAutoComplete() {
+ this.init();
+}
+
+/**
+ * FormAutoComplete
+ *
+ * Implements the nsIFormAutoComplete interface in the main process.
+ */
+FormAutoComplete.prototype = {
+ classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
+
+ _prefBranch : null,
+ _debug : true, // mirrors browser.formfill.debug
+ _enabled : true, // mirrors browser.formfill.enable preference
+ _agedWeight : 2,
+ _bucketSize : 1,
+ _maxTimeGroupings : 25,
+ _timeGroupingSize : 7 * 24 * 60 * 60 * 1000 * 1000,
+ _expireDays : null,
+ _boundaryWeight : 25,
+ _prefixWeight : 5,
+
+ // Only one query via FormHistoryClient is performed at a time, and the
+ // most recent FormHistoryClient which will be stored in _pendingClient
+ // while the query is being performed. It will be cleared when the query
+ // finishes, is cancelled, or an error occurs. If a new query occurs while
+ // one is already pending, the existing one is cancelled.
+ _pendingClient : null,
+
+ init : function() {
+ // Preferences. Add observer so we get notified of changes.
+ this._prefBranch = Services.prefs.getBranch("browser.formfill.");
+ this._prefBranch.addObserver("", this.observer, true);
+ this.observer._self = this;
+
+ this._debug = this._prefBranch.getBoolPref("debug");
+ this._enabled = this._prefBranch.getBoolPref("enable");
+ this._agedWeight = this._prefBranch.getIntPref("agedWeight");
+ this._bucketSize = this._prefBranch.getIntPref("bucketSize");
+ this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
+ this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
+ this._expireDays = this._prefBranch.getIntPref("expire_days");
+ },
+
+ observer : {
+ _self : null,
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ let self = this._self;
+ if (topic == "nsPref:changed") {
+ let prefName = data;
+ self.log("got change to " + prefName + " preference");
+
+ switch (prefName) {
+ case "agedWeight":
+ self._agedWeight = self._prefBranch.getIntPref(prefName);
+ break;
+ case "debug":
+ self._debug = self._prefBranch.getBoolPref(prefName);
+ break;
+ case "enable":
+ self._enabled = self._prefBranch.getBoolPref(prefName);
+ break;
+ case "maxTimeGroupings":
+ self._maxTimeGroupings = self._prefBranch.getIntPref(prefName);
+ break;
+ case "timeGroupingSize":
+ self._timeGroupingSize = self._prefBranch.getIntPref(prefName) * 1000 * 1000;
+ break;
+ case "bucketSize":
+ self._bucketSize = self._prefBranch.getIntPref(prefName);
+ break;
+ case "boundaryWeight":
+ self._boundaryWeight = self._prefBranch.getIntPref(prefName);
+ break;
+ case "prefixWeight":
+ self._prefixWeight = self._prefBranch.getIntPref(prefName);
+ break;
+ default:
+ self.log("Oops! Pref not handled, change ignored.");
+ }
+ }
+ }
+ },
+
+ // AutoCompleteE10S needs to be able to call autoCompleteSearchAsync without
+ // going through IDL in order to pass a mock DOM object field.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /*
+ * log
+ *
+ * Internal function for logging debug messages to the Error Console
+ * window
+ */
+ log : function (message) {
+ if (!this._debug)
+ return;
+ dump("FormAutoComplete: " + message + "\n");
+ Services.console.logStringMessage("FormAutoComplete: " + message);
+ },
+
+ /*
+ * autoCompleteSearchAsync
+ *
+ * aInputName -- |name| attribute from the form input being autocompleted.
+ * aUntrimmedSearchString -- current value of the input
+ * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
+ * aPreviousResult -- previous search result, if any.
+ * aDatalistResult -- results from list=datalist for aField.
+ * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
+ * that may be returned asynchronously.
+ */
+ autoCompleteSearchAsync : function (aInputName,
+ aUntrimmedSearchString,
+ aField,
+ aPreviousResult,
+ aDatalistResult,
+ aListener) {
+ function sortBytotalScore (a, b) {
+ return b.totalScore - a.totalScore;
+ }
+
+ // Guard against void DOM strings filtering into this code.
+ if (typeof aInputName === "object") {
+ aInputName = "";
+ }
+ if (typeof aUntrimmedSearchString === "object") {
+ aUntrimmedSearchString = "";
+ }
+
+ let client = new FormHistoryClient({ formField: aField, inputName: aInputName });
+
+ // If we have datalist results, they become our "empty" result.
+ let emptyResult = aDatalistResult ||
+ new FormAutoCompleteResult(client, [],
+ aInputName,
+ aUntrimmedSearchString,
+ null);
+ if (!this._enabled) {
+ if (aListener) {
+ aListener.onSearchCompletion(emptyResult);
+ }
+ return;
+ }
+
+ // don't allow form inputs (aField != null) to get results from search bar history
+ if (aInputName == 'searchbar-history' && aField) {
+ this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
+ if (aListener) {
+ aListener.onSearchCompletion(emptyResult);
+ }
+ return;
+ }
+
+ if (aField && isAutocompleteDisabled(aField)) {
+ this.log('autoCompleteSearch not allowed due to autcomplete=off');
+ if (aListener) {
+ aListener.onSearchCompletion(emptyResult);
+ }
+ return;
+ }
+
+ this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
+ let searchString = aUntrimmedSearchString.trim().toLowerCase();
+
+ // reuse previous results if:
+ // a) length greater than one character (others searches are special cases) AND
+ // b) the the new results will be a subset of the previous results
+ if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
+ searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) {
+ this.log("Using previous autocomplete result");
+ let result = aPreviousResult;
+ let wrappedResult = result.wrappedJSObject;
+ wrappedResult.searchString = aUntrimmedSearchString;
+
+ // Leaky abstraction alert: it would be great to be able to split
+ // this code between nsInputListAutoComplete and here but because of
+ // the way we abuse the formfill autocomplete API in e10s, we have
+ // to deal with the <datalist> results here as well (and down below
+ // in mergeResults).
+ // If there were datalist results result is a FormAutoCompleteResult
+ // as defined in nsFormAutoCompleteResult.jsm with the entire list
+ // of results in wrappedResult._values and only the results from
+ // form history in wrappedResult.entries.
+ // First, grab the entire list of old results.
+ let allResults = wrappedResult._labels;
+ let datalistResults, datalistLabels;
+ if (allResults) {
+ // We have datalist results, extract them from the values array.
+ // Both allResults and values arrays are in the form of:
+ // |--wR.entries--|
+ // <history entries><datalist entries>
+ let oldLabels = allResults.slice(wrappedResult.entries.length);
+ let oldValues = wrappedResult._values.slice(wrappedResult.entries.length);
+
+ datalistLabels = [];
+ datalistResults = [];
+ for (let i = 0; i < oldLabels.length; ++i) {
+ if (oldLabels[i].toLowerCase().includes(searchString)) {
+ datalistLabels.push(oldLabels[i]);
+ datalistResults.push(oldValues[i]);
+ }
+ }
+ }
+
+ let searchTokens = searchString.split(/\s+/);
+ // We have a list of results for a shorter search string, so just
+ // filter them further based on the new search string and add to a new array.
+ let entries = wrappedResult.entries;
+ let filteredEntries = [];
+ for (let i = 0; i < entries.length; i++) {
+ let entry = entries[i];
+ // Remove results that do not contain the token
+ // XXX bug 394604 -- .toLowerCase can be wrong for some intl chars
+ if (searchTokens.some(tok => entry.textLowerCase.indexOf(tok) < 0))
+ continue;
+ this._calculateScore(entry, searchString, searchTokens);
+ this.log("Reusing autocomplete entry '" + entry.text +
+ "' (" + entry.frecency +" / " + entry.totalScore + ")");
+ filteredEntries.push(entry);
+ }
+ filteredEntries.sort(sortBytotalScore);
+ wrappedResult.entries = filteredEntries;
+
+ // If we had datalistResults, re-merge them back into the filtered
+ // entries.
+ if (datalistResults) {
+ filteredEntries = filteredEntries.map(elt => elt.text);
+
+ let comments = new Array(filteredEntries.length + datalistResults.length).fill("");
+ comments[filteredEntries.length] = "separator";
+
+ // History entries don't have labels (their labels would be read
+ // from their values). Pad out the labels array so the datalist
+ // results (which do have separate values and labels) line up.
+ datalistLabels = new Array(filteredEntries.length).fill("").concat(datalistLabels);
+ wrappedResult._values = filteredEntries.concat(datalistResults);
+ wrappedResult._labels = datalistLabels;
+ wrappedResult._comments = comments;
+ }
+
+ if (aListener) {
+ aListener.onSearchCompletion(result);
+ }
+ } else {
+ this.log("Creating new autocomplete search result.");
+
+ // Start with an empty list.
+ let result = aDatalistResult ?
+ new FormAutoCompleteResult(client, [], aInputName, aUntrimmedSearchString, null) :
+ emptyResult;
+
+ let processEntry = (aEntries) => {
+ if (aField && aField.maxLength > -1) {
+ result.entries =
+ aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
+ } else {
+ result.entries = aEntries;
+ }
+
+ if (aDatalistResult && aDatalistResult.matchCount > 0) {
+ result = this.mergeResults(result, aDatalistResult);
+ }
+
+ if (aListener) {
+ aListener.onSearchCompletion(result);
+ }
+ }
+
+ this.getAutoCompleteValues(client, aInputName, searchString, processEntry);
+ }
+ },
+
+ mergeResults(historyResult, datalistResult) {
+ let values = datalistResult.wrappedJSObject._values;
+ let labels = datalistResult.wrappedJSObject._labels;
+ let comments = new Array(values.length).fill("");
+
+ // historyResult will be null if form autocomplete is disabled. We
+ // still want the list values to display.
+ let entries = historyResult.wrappedJSObject.entries;
+ let historyResults = entries.map(entry => entry.text);
+ let historyComments = new Array(entries.length).fill("");
+
+ // now put the history results above the datalist suggestions
+ let finalValues = historyResults.concat(values);
+ let finalLabels = historyResults.concat(labels);
+ let finalComments = historyComments.concat(comments);
+
+ // This is ugly: there are two FormAutoCompleteResult classes in the
+ // tree, one in a module and one in this file. Datalist results need to
+ // use the one defined in the module but the rest of this file assumes
+ // that we use the one defined here. To get around that, we explicitly
+ // import the module here, out of the way of the other uses of
+ // FormAutoCompleteResult.
+ let {FormAutoCompleteResult} = Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm", {});
+ return new FormAutoCompleteResult(datalistResult.searchString,
+ Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+ 0, "", finalValues, finalLabels,
+ finalComments, historyResult);
+ },
+
+ stopAutoCompleteSearch : function () {
+ if (this._pendingClient) {
+ this._pendingClient.cancel();
+ this._pendingClient = null;
+ }
+ },
+
+ /*
+ * Get the values for an autocomplete list given a search string.
+ *
+ * client - a FormHistoryClient instance to perform the search with
+ * fieldName - fieldname field within form history (the form input name)
+ * searchString - string to search for
+ * callback - called when the values are available. Passed an array of objects,
+ * containing properties for each result. The callback is only called
+ * when successful.
+ */
+ getAutoCompleteValues : function (client, fieldName, searchString, callback) {
+ let params = {
+ agedWeight: this._agedWeight,
+ bucketSize: this._bucketSize,
+ expiryDate: 1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
+ fieldname: fieldName,
+ maxTimeGroupings: this._maxTimeGroupings,
+ timeGroupingSize: this._timeGroupingSize,
+ prefixWeight: this._prefixWeight,
+ boundaryWeight: this._boundaryWeight
+ }
+
+ this.stopAutoCompleteSearch();
+ client.requestAutoCompleteResults(searchString, params, (entries) => {
+ this._pendingClient = null;
+ callback(entries);
+ });
+ this._pendingClient = client;
+ },
+
+ /*
+ * _calculateScore
+ *
+ * entry -- an nsIAutoCompleteResult entry
+ * aSearchString -- current value of the input (lowercase)
+ * searchTokens -- array of tokens of the search string
+ *
+ * Returns: an int
+ */
+ _calculateScore : function (entry, aSearchString, searchTokens) {
+ let boundaryCalc = 0;
+ // for each word, calculate word boundary weights
+ for (let token of searchTokens) {
+ boundaryCalc += (entry.textLowerCase.indexOf(token) == 0);
+ boundaryCalc += (entry.textLowerCase.indexOf(" " + token) >= 0);
+ }
+ boundaryCalc = boundaryCalc * this._boundaryWeight;
+ // now add more weight if we have a traditional prefix match and
+ // multiply boundary bonuses by boundary weight
+ boundaryCalc += this._prefixWeight *
+ (entry.textLowerCase.
+ indexOf(aSearchString) == 0);
+ entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
+ }
+
+}; // end of FormAutoComplete implementation
+
+// nsIAutoCompleteResult implementation
+function FormAutoCompleteResult(client,
+ entries,
+ fieldName,
+ searchString,
+ messageManager) {
+ this.client = client;
+ this.entries = entries;
+ this.fieldName = fieldName;
+ this.searchString = searchString;
+ this.messageManager = messageManager;
+}
+
+FormAutoCompleteResult.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
+ Ci.nsISupportsWeakReference]),
+
+ // private
+ client : null,
+ entries : null,
+ fieldName : null,
+
+ _checkIndexBounds : function (index) {
+ if (index < 0 || index >= this.entries.length)
+ throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
+ },
+
+ // Allow autoCompleteSearch to get at the JS object so it can
+ // modify some readonly properties for internal use.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Interfaces from idl...
+ searchString : "",
+ errorDescription : "",
+ get defaultIndex() {
+ if (this.entries.length == 0)
+ return -1;
+ return 0;
+ },
+ get searchResult() {
+ if (this.entries.length == 0)
+ return Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
+ return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ },
+ get matchCount() {
+ return this.entries.length;
+ },
+
+ getValueAt : function (index) {
+ this._checkIndexBounds(index);
+ return this.entries[index].text;
+ },
+
+ getLabelAt: function(index) {
+ return this.getValueAt(index);
+ },
+
+ getCommentAt : function (index) {
+ this._checkIndexBounds(index);
+ return "";
+ },
+
+ getStyleAt : function (index) {
+ this._checkIndexBounds(index);
+ return "";
+ },
+
+ getImageAt : function (index) {
+ this._checkIndexBounds(index);
+ return "";
+ },
+
+ getFinalCompleteValueAt : function (index) {
+ return this.getValueAt(index);
+ },
+
+ removeValueAt : function (index, removeFromDB) {
+ this._checkIndexBounds(index);
+
+ let [removedEntry] = this.entries.splice(index, 1);
+
+ if (removeFromDB) {
+ this.client.remove(removedEntry.text);
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FormAutoComplete]);
diff --git a/toolkit/components/satchel/nsFormAutoCompleteResult.jsm b/toolkit/components/satchel/nsFormAutoCompleteResult.jsm
new file mode 100644
index 0000000000..07ef15fca0
--- /dev/null
+++ b/toolkit/components/satchel/nsFormAutoCompleteResult.jsm
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = [ "FormAutoCompleteResult" ];
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.FormAutoCompleteResult =
+ function FormAutoCompleteResult(searchString,
+ searchResult,
+ defaultIndex,
+ errorDescription,
+ values,
+ labels,
+ comments,
+ prevResult) {
+ this.searchString = searchString;
+ this._searchResult = searchResult;
+ this._defaultIndex = defaultIndex;
+ this._errorDescription = errorDescription;
+ this._values = values;
+ this._labels = labels;
+ this._comments = comments;
+ this._formHistResult = prevResult;
+
+ if (prevResult) {
+ this.entries = prevResult.wrappedJSObject.entries;
+ } else {
+ this.entries = [];
+ }
+}
+
+FormAutoCompleteResult.prototype = {
+
+ // The user's query string
+ searchString: "",
+
+ // The result code of this result object, see |get searchResult| for possible values.
+ _searchResult: 0,
+
+ // The default item that should be entered if none is selected
+ _defaultIndex: 0,
+
+ // The reason the search failed
+ _errorDescription: "",
+
+ /**
+ * A reference to the form history nsIAutocompleteResult that we're wrapping.
+ * We use this to forward removeEntryAt calls as needed.
+ */
+ _formHistResult: null,
+
+ entries: null,
+
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /**
+ * @return the result code of this result object, either:
+ * RESULT_IGNORED (invalid searchString)
+ * RESULT_FAILURE (failure)
+ * RESULT_NOMATCH (no matches found)
+ * RESULT_SUCCESS (matches found)
+ */
+ get searchResult() {
+ return this._searchResult;
+ },
+
+ /**
+ * @return the default item that should be entered if none is selected
+ */
+ get defaultIndex() {
+ return this._defaultIndex;
+ },
+
+ /**
+ * @return the reason the search failed
+ */
+ get errorDescription() {
+ return this._errorDescription;
+ },
+
+ /**
+ * @return the number of results
+ */
+ get matchCount() {
+ return this._values.length;
+ },
+
+ _checkIndexBounds : function (index) {
+ if (index < 0 || index >= this._values.length) {
+ throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ },
+
+ /**
+ * Retrieves a result
+ * @param index the index of the result requested
+ * @return the result at the specified index
+ */
+ getValueAt: function(index) {
+ this._checkIndexBounds(index);
+ return this._values[index];
+ },
+
+ getLabelAt: function(index) {
+ this._checkIndexBounds(index);
+ return this._labels[index] || this._values[index];
+ },
+
+ /**
+ * Retrieves a comment (metadata instance)
+ * @param index the index of the comment requested
+ * @return the comment at the specified index
+ */
+ getCommentAt: function(index) {
+ this._checkIndexBounds(index);
+ return this._comments[index];
+ },
+
+ /**
+ * Retrieves a style hint specific to a particular index.
+ * @param index the index of the style hint requested
+ * @return the style hint at the specified index
+ */
+ getStyleAt: function(index) {
+ this._checkIndexBounds(index);
+
+ if (this._formHistResult && index < this._formHistResult.matchCount) {
+ return "fromhistory";
+ }
+
+ if (this._formHistResult &&
+ this._formHistResult.matchCount > 0 &&
+ index == this._formHistResult.matchCount) {
+ return "datalist-first";
+ }
+
+ return null;
+ },
+
+ /**
+ * Retrieves an image url.
+ * @param index the index of the image url requested
+ * @return the image url at the specified index
+ */
+ getImageAt: function(index) {
+ this._checkIndexBounds(index);
+ return "";
+ },
+
+ /**
+ * Retrieves a result
+ * @param index the index of the result requested
+ * @return the result at the specified index
+ */
+ getFinalCompleteValueAt: function(index) {
+ return this.getValueAt(index);
+ },
+
+ /**
+ * Removes a result from the resultset
+ * @param index the index of the result to remove
+ */
+ removeValueAt: function(index, removeFromDatabase) {
+ this._checkIndexBounds(index);
+ // Forward the removeValueAt call to the underlying result if we have one
+ // Note: this assumes that the form history results were added to the top
+ // of our arrays.
+ if (removeFromDatabase && this._formHistResult &&
+ index < this._formHistResult.matchCount) {
+ // Delete the history result from the DB
+ this._formHistResult.removeValueAt(index, true);
+ }
+ this._values.splice(index, 1);
+ this._labels.splice(index, 1);
+ this._comments.splice(index, 1);
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult])
+};
diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp
new file mode 100644
index 0000000000..d70036635d
--- /dev/null
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -0,0 +1,1382 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsFormFillController.h"
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
+#include "nsIFormAutoComplete.h"
+#include "nsIInputListAutoComplete.h"
+#include "nsIAutoCompleteSimpleResult.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsIServiceManager.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWebNavigation.h"
+#include "nsIContentViewer.h"
+#include "nsIDOMKeyEvent.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMElement.h"
+#include "nsIFormControl.h"
+#include "nsIDocument.h"
+#include "nsIContent.h"
+#include "nsIPresShell.h"
+#include "nsRect.h"
+#include "nsIDOMHTMLFormElement.h"
+#include "nsILoginManager.h"
+#include "nsIDOMMouseEvent.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsToolkitCompsCID.h"
+#include "nsEmbedCID.h"
+#include "nsIDOMNSEditableElement.h"
+#include "nsContentUtils.h"
+#include "nsILoadContext.h"
+#include "nsIFrame.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsFocusManager.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION(nsFormFillController,
+ mController, mLoginManager, mFocusedPopup, mDocShells,
+ mPopups, mLastListener, mLastFormAutoComplete)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController)
+ NS_INTERFACE_MAP_ENTRY(nsIFormFillController)
+ NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteInput)
+ NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRY(nsIFormAutoCompleteObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFormFillController)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFormFillController)
+
+
+
+nsFormFillController::nsFormFillController() :
+ mFocusedInput(nullptr),
+ mFocusedInputNode(nullptr),
+ mListNode(nullptr),
+ mTimeout(50),
+ mMinResultsForPopup(1),
+ mMaxRows(0),
+ mContextMenuFiredBeforeFocus(false),
+ mDisableAutoComplete(false),
+ mCompleteDefaultIndex(false),
+ mCompleteSelectedIndex(false),
+ mForceComplete(false),
+ mSuppressOnInput(false)
+{
+ mController = do_GetService("@mozilla.org/autocomplete/controller;1");
+ MOZ_ASSERT(mController);
+}
+
+nsFormFillController::~nsFormFillController()
+{
+ if (mListNode) {
+ mListNode->RemoveMutationObserver(this);
+ mListNode = nullptr;
+ }
+ if (mFocusedInputNode) {
+ MaybeRemoveMutationObserver(mFocusedInputNode);
+ mFocusedInputNode = nullptr;
+ mFocusedInput = nullptr;
+ }
+ RemoveForDocument(nullptr);
+
+ // Remove ourselves as a focus listener from all cached docShells
+ uint32_t count = mDocShells.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(mDocShells[i]);
+ RemoveWindowListeners(window);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIMutationObserver
+//
+
+void
+nsFormFillController::AttributeChanged(nsIDocument* aDocument,
+ mozilla::dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsIAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue)
+{
+ if ((aAttribute == nsGkAtoms::type || aAttribute == nsGkAtoms::readonly ||
+ aAttribute == nsGkAtoms::autocomplete) &&
+ aNameSpaceID == kNameSpaceID_None) {
+ nsCOMPtr<nsIDOMHTMLInputElement> focusedInput(mFocusedInput);
+ // Reset the current state of the controller, unconditionally.
+ StopControllingInput();
+ // Then restart based on the new values. We have to delay this
+ // to avoid ending up in an endless loop due to re-registering our
+ // mutation observer (which would notify us again for *this* event).
+ nsCOMPtr<nsIRunnable> event =
+ mozilla::NewRunnableMethod<nsCOMPtr<nsIDOMHTMLInputElement>>
+ (this, &nsFormFillController::MaybeStartControllingInput, focusedInput);
+ NS_DispatchToCurrentThread(event);
+ }
+
+ if (mListNode && mListNode->Contains(aElement)) {
+ RevalidateDataList();
+ }
+}
+
+void
+nsFormFillController::ContentAppended(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexInContainer)
+{
+ if (mListNode && mListNode->Contains(aContainer)) {
+ RevalidateDataList();
+ }
+}
+
+void
+nsFormFillController::ContentInserted(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexInContainer)
+{
+ if (mListNode && mListNode->Contains(aContainer)) {
+ RevalidateDataList();
+ }
+}
+
+void
+nsFormFillController::ContentRemoved(nsIDocument* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexInContainer,
+ nsIContent* aPreviousSibling)
+{
+ if (mListNode && mListNode->Contains(aContainer)) {
+ RevalidateDataList();
+ }
+}
+
+void
+nsFormFillController::CharacterDataWillChange(nsIDocument* aDocument,
+ nsIContent* aContent,
+ CharacterDataChangeInfo* aInfo)
+{
+}
+
+void
+nsFormFillController::CharacterDataChanged(nsIDocument* aDocument,
+ nsIContent* aContent,
+ CharacterDataChangeInfo* aInfo)
+{
+}
+
+void
+nsFormFillController::AttributeWillChange(nsIDocument* aDocument,
+ mozilla::dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsIAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aNewValue)
+{
+}
+
+void
+nsFormFillController::NativeAnonymousChildListChange(nsIDocument* aDocument,
+ nsIContent* aContent,
+ bool aIsRemove)
+{
+}
+
+void
+nsFormFillController::ParentChainChanged(nsIContent* aContent)
+{
+}
+
+void
+nsFormFillController::NodeWillBeDestroyed(const nsINode* aNode)
+{
+ mPwmgrInputs.Remove(aNode);
+ if (aNode == mListNode) {
+ mListNode = nullptr;
+ RevalidateDataList();
+ } else if (aNode == mFocusedInputNode) {
+ mFocusedInputNode = nullptr;
+ mFocusedInput = nullptr;
+ }
+}
+
+void
+nsFormFillController::MaybeRemoveMutationObserver(nsINode* aNode)
+{
+ // Nodes being tracked in mPwmgrInputs will have their observers removed when
+ // they stop being tracked.
+ if (!mPwmgrInputs.Get(aNode)) {
+ aNode->RemoveMutationObserver(this);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIFormFillController
+
+NS_IMETHODIMP
+nsFormFillController::AttachToBrowser(nsIDocShell *aDocShell, nsIAutoCompletePopup *aPopup)
+{
+ NS_ENSURE_TRUE(aDocShell && aPopup, NS_ERROR_ILLEGAL_VALUE);
+
+ mDocShells.AppendElement(aDocShell);
+ mPopups.AppendElement(aPopup);
+
+ // Listen for focus events on the domWindow of the docShell
+ nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(aDocShell);
+ AddWindowListeners(window);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::DetachFromBrowser(nsIDocShell *aDocShell)
+{
+ int32_t index = GetIndexOfDocShell(aDocShell);
+ NS_ENSURE_TRUE(index >= 0, NS_ERROR_FAILURE);
+
+ // Stop listening for focus events on the domWindow of the docShell
+ nsCOMPtr<nsPIDOMWindowOuter> window =
+ GetWindowForDocShell(mDocShells.SafeElementAt(index));
+ RemoveWindowListeners(window);
+
+ mDocShells.RemoveElementAt(index);
+ mPopups.RemoveElementAt(index);
+
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsFormFillController::MarkAsLoginManagerField(nsIDOMHTMLInputElement *aInput)
+{
+ /*
+ * The Login Manager can supply autocomplete results for username fields,
+ * when a user has multiple logins stored for a site. It uses this
+ * interface to indicate that the form manager shouldn't handle the
+ * autocomplete. The form manager also checks for this tag when saving
+ * form history (so it doesn't save usernames).
+ */
+ nsCOMPtr<nsINode> node = do_QueryInterface(aInput);
+ NS_ENSURE_STATE(node);
+
+ // If the field was already marked, we don't want to show the popup again.
+ if (mPwmgrInputs.Get(node)) {
+ return NS_OK;
+ }
+
+ mPwmgrInputs.Put(node, true);
+ node->AddMutationObserverUnlessExists(this);
+
+ nsFocusManager *fm = nsFocusManager::GetFocusManager();
+ if (fm) {
+ nsCOMPtr<nsIContent> focusedContent = fm->GetFocusedContent();
+ if (SameCOMIdentity(focusedContent, node)) {
+ nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(node);
+ if (!mFocusedInput) {
+ MaybeStartControllingInput(input);
+ }
+ }
+ }
+
+ if (!mLoginManager)
+ mLoginManager = do_GetService("@mozilla.org/login-manager;1");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetFocusedInput(nsIDOMHTMLInputElement **aInput) {
+ *aInput = mFocusedInput;
+ NS_IF_ADDREF(*aInput);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIAutoCompleteInput
+
+NS_IMETHODIMP
+nsFormFillController::GetPopup(nsIAutoCompletePopup **aPopup)
+{
+ *aPopup = mFocusedPopup;
+ NS_IF_ADDREF(*aPopup);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetController(nsIAutoCompleteController **aController)
+{
+ *aController = mController;
+ NS_IF_ADDREF(*aController);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetPopupOpen(bool *aPopupOpen)
+{
+ if (mFocusedPopup)
+ mFocusedPopup->GetPopupOpen(aPopupOpen);
+ else
+ *aPopupOpen = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetPopupOpen(bool aPopupOpen)
+{
+ if (mFocusedPopup) {
+ if (aPopupOpen) {
+ // make sure input field is visible before showing popup (bug 320938)
+ nsCOMPtr<nsIContent> content = do_QueryInterface(mFocusedInput);
+ NS_ENSURE_STATE(content);
+ nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(mFocusedInput);
+ NS_ENSURE_STATE(docShell);
+ nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
+ NS_ENSURE_STATE(presShell);
+ presShell->ScrollContentIntoView(content,
+ nsIPresShell::ScrollAxis(
+ nsIPresShell::SCROLL_MINIMUM,
+ nsIPresShell::SCROLL_IF_NOT_VISIBLE),
+ nsIPresShell::ScrollAxis(
+ nsIPresShell::SCROLL_MINIMUM,
+ nsIPresShell::SCROLL_IF_NOT_VISIBLE),
+ nsIPresShell::SCROLL_OVERFLOW_HIDDEN);
+ // mFocusedPopup can be destroyed after ScrollContentIntoView, see bug 420089
+ if (mFocusedPopup) {
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
+ mFocusedPopup->OpenAutocompletePopup(this, element);
+ }
+ } else
+ mFocusedPopup->ClosePopup();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetDisableAutoComplete(bool *aDisableAutoComplete)
+{
+ *aDisableAutoComplete = mDisableAutoComplete;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetDisableAutoComplete(bool aDisableAutoComplete)
+{
+ mDisableAutoComplete = aDisableAutoComplete;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetCompleteDefaultIndex(bool *aCompleteDefaultIndex)
+{
+ *aCompleteDefaultIndex = mCompleteDefaultIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetCompleteDefaultIndex(bool aCompleteDefaultIndex)
+{
+ mCompleteDefaultIndex = aCompleteDefaultIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetCompleteSelectedIndex(bool *aCompleteSelectedIndex)
+{
+ *aCompleteSelectedIndex = mCompleteSelectedIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetCompleteSelectedIndex(bool aCompleteSelectedIndex)
+{
+ mCompleteSelectedIndex = aCompleteSelectedIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetForceComplete(bool *aForceComplete)
+{
+ *aForceComplete = mForceComplete;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFormFillController::SetForceComplete(bool aForceComplete)
+{
+ mForceComplete = aForceComplete;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetMinResultsForPopup(uint32_t *aMinResultsForPopup)
+{
+ *aMinResultsForPopup = mMinResultsForPopup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFormFillController::SetMinResultsForPopup(uint32_t aMinResultsForPopup)
+{
+ mMinResultsForPopup = aMinResultsForPopup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetMaxRows(uint32_t *aMaxRows)
+{
+ *aMaxRows = mMaxRows;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetMaxRows(uint32_t aMaxRows)
+{
+ mMaxRows = aMaxRows;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetShowImageColumn(bool *aShowImageColumn)
+{
+ *aShowImageColumn = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFormFillController::SetShowImageColumn(bool aShowImageColumn)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+
+NS_IMETHODIMP
+nsFormFillController::GetShowCommentColumn(bool *aShowCommentColumn)
+{
+ *aShowCommentColumn = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFormFillController::SetShowCommentColumn(bool aShowCommentColumn)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetTimeout(uint32_t *aTimeout)
+{
+ *aTimeout = mTimeout;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsFormFillController::SetTimeout(uint32_t aTimeout)
+{
+ mTimeout = aTimeout;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetSearchParam(const nsAString &aSearchParam)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetSearchParam(nsAString &aSearchParam)
+{
+ if (!mFocusedInput) {
+ NS_WARNING("mFocusedInput is null for some reason! avoiding a crash. should find out why... - ben");
+ return NS_ERROR_FAILURE; // XXX why? fix me.
+ }
+
+ mFocusedInput->GetName(aSearchParam);
+ if (aSearchParam.IsEmpty()) {
+ nsCOMPtr<nsIDOMHTMLElement> element = do_QueryInterface(mFocusedInput);
+ element->GetId(aSearchParam);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetSearchCount(uint32_t *aSearchCount)
+{
+ *aSearchCount = 1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetSearchAt(uint32_t index, nsACString & _retval)
+{
+ _retval.AssignLiteral("form-history");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetTextValue(nsAString & aTextValue)
+{
+ if (mFocusedInput) {
+ nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
+ input->GetValue(aTextValue);
+ } else {
+ aTextValue.Truncate();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetTextValue(const nsAString & aTextValue)
+{
+ nsCOMPtr<nsIDOMNSEditableElement> editable = do_QueryInterface(mFocusedInput);
+ if (editable) {
+ mSuppressOnInput = true;
+ editable->SetUserInput(aTextValue);
+ mSuppressOnInput = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SetTextValueWithReason(const nsAString & aTextValue,
+ uint16_t aReason)
+{
+ return SetTextValue(aTextValue);
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetSelectionStart(int32_t *aSelectionStart)
+{
+ if (mFocusedInput) {
+ nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
+ input->GetSelectionStart(aSelectionStart);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetSelectionEnd(int32_t *aSelectionEnd)
+{
+ if (mFocusedInput) {
+ nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
+ input->GetSelectionEnd(aSelectionEnd);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::SelectTextRange(int32_t aStartIndex, int32_t aEndIndex)
+{
+ if (mFocusedInput) {
+ nsCOMPtr<nsIDOMHTMLInputElement> input = mFocusedInput;
+ input->SetSelectionRange(aStartIndex, aEndIndex, EmptyString());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::OnSearchBegin()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::OnSearchComplete()
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::OnTextEntered(nsIDOMEvent* aEvent,
+ bool* aPrevent)
+{
+ NS_ENSURE_ARG(aPrevent);
+ NS_ENSURE_TRUE(mFocusedInput, NS_OK);
+ // Fire off a DOMAutoComplete event
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
+ element->GetOwnerDocument(getter_AddRefs(domDoc));
+ NS_ENSURE_STATE(domDoc);
+
+ nsCOMPtr<nsIDOMEvent> event;
+ domDoc->CreateEvent(NS_LITERAL_STRING("Events"), getter_AddRefs(event));
+ NS_ENSURE_STATE(event);
+
+ event->InitEvent(NS_LITERAL_STRING("DOMAutoComplete"), true, true);
+
+ // XXXjst: We mark this event as a trusted event, it's up to the
+ // callers of this to ensure that it's only called from trusted
+ // code.
+ event->SetTrusted(true);
+
+ nsCOMPtr<EventTarget> targ = do_QueryInterface(mFocusedInput);
+
+ bool defaultActionEnabled;
+ targ->DispatchEvent(event, &defaultActionEnabled);
+ *aPrevent = !defaultActionEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::OnTextReverted(bool *_retval)
+{
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetConsumeRollupEvent(bool *aConsumeRollupEvent)
+{
+ *aConsumeRollupEvent = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetInPrivateContext(bool *aInPrivateContext)
+{
+ if (!mFocusedInput) {
+ *aInPrivateContext = false;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMDocument> inputDoc;
+ nsCOMPtr<nsIDOMElement> element = do_QueryInterface(mFocusedInput);
+ element->GetOwnerDocument(getter_AddRefs(inputDoc));
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(inputDoc);
+ nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
+ *aInPrivateContext = loadContext && loadContext->UsePrivateBrowsing();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetNoRollupOnCaretMove(bool *aNoRollupOnCaretMove)
+{
+ *aNoRollupOnCaretMove = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFormFillController::GetUserContextId(uint32_t* aUserContextId)
+{
+ *aUserContextId = nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIAutoCompleteSearch
+
+NS_IMETHODIMP
+nsFormFillController::StartSearch(const nsAString &aSearchString, const nsAString &aSearchParam,
+ nsIAutoCompleteResult *aPreviousResult, nsIAutoCompleteObserver *aListener)
+{
+ nsresult rv;
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode);
+
+ // If the login manager has indicated it's responsible for this field, let it
+ // handle the autocomplete. Otherwise, handle with form history.
+ // This method is sometimes called in unit tests and from XUL without a focused node.
+ if (mFocusedInputNode && (mPwmgrInputs.Get(mFocusedInputNode) ||
+ formControl->GetType() == NS_FORM_INPUT_PASSWORD)) {
+
+ // Handle the case where a password field is focused but
+ // MarkAsLoginManagerField wasn't called because password manager is disabled.
+ if (!mLoginManager) {
+ mLoginManager = do_GetService("@mozilla.org/login-manager;1");
+ }
+
+ if (NS_WARN_IF(!mLoginManager)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX aPreviousResult shouldn't ever be a historyResult type, since we're not letting
+ // satchel manage the field?
+ mLastListener = aListener;
+ rv = mLoginManager->AutoCompleteSearchAsync(aSearchString,
+ aPreviousResult,
+ mFocusedInput,
+ this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ mLastListener = aListener;
+
+ nsCOMPtr<nsIAutoCompleteResult> datalistResult;
+ if (mFocusedInput) {
+ rv = PerformInputListAutoComplete(aSearchString,
+ getter_AddRefs(datalistResult));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
+ do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ formAutoComplete->AutoCompleteSearchAsync(aSearchParam,
+ aSearchString,
+ mFocusedInput,
+ aPreviousResult,
+ datalistResult,
+ this);
+ mLastFormAutoComplete = formAutoComplete;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsFormFillController::PerformInputListAutoComplete(const nsAString& aSearch,
+ nsIAutoCompleteResult** aResult)
+{
+ // If an <input> is focused, check if it has a list="<datalist>" which can
+ // provide the list of suggestions.
+
+ MOZ_ASSERT(!mPwmgrInputs.Get(mFocusedInputNode));
+ nsresult rv;
+
+ nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
+ do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = inputListAutoComplete->AutoCompleteSearch(aSearch,
+ mFocusedInput,
+ aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mFocusedInput) {
+ nsCOMPtr<nsIDOMHTMLElement> list;
+ mFocusedInput->GetList(getter_AddRefs(list));
+
+ // Add a mutation observer to check for changes to the items in the <datalist>
+ // and update the suggestions accordingly.
+ nsCOMPtr<nsINode> node = do_QueryInterface(list);
+ if (mListNode != node) {
+ if (mListNode) {
+ mListNode->RemoveMutationObserver(this);
+ mListNode = nullptr;
+ }
+ if (node) {
+ node->AddMutationObserverUnlessExists(this);
+ mListNode = node;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+class UpdateSearchResultRunnable : public mozilla::Runnable
+{
+public:
+ UpdateSearchResultRunnable(nsIAutoCompleteObserver* aObserver,
+ nsIAutoCompleteSearch* aSearch,
+ nsIAutoCompleteResult* aResult)
+ : mObserver(aObserver)
+ , mSearch(aSearch)
+ , mResult(aResult)
+ {
+ MOZ_ASSERT(mResult, "Should have a valid result");
+ MOZ_ASSERT(mObserver, "You shouldn't call this runnable with a null observer!");
+ }
+
+ NS_IMETHOD Run() override {
+ mObserver->OnUpdateSearchResult(mSearch, mResult);
+ return NS_OK;
+ }
+
+private:
+ nsCOMPtr<nsIAutoCompleteObserver> mObserver;
+ nsCOMPtr<nsIAutoCompleteSearch> mSearch;
+ nsCOMPtr<nsIAutoCompleteResult> mResult;
+};
+
+void nsFormFillController::RevalidateDataList()
+{
+ if (!mLastListener) {
+ return;
+ }
+
+ if (XRE_IsContentProcess()) {
+ nsCOMPtr<nsIAutoCompleteController> controller(do_QueryInterface(mLastListener));
+ if (!controller) {
+ return;
+ }
+
+ controller->StartSearch(mLastSearchString);
+ return;
+ }
+
+ nsresult rv;
+ nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
+ do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
+
+ nsCOMPtr<nsIAutoCompleteResult> result;
+
+ rv = inputListAutoComplete->AutoCompleteSearch(mLastSearchString,
+ mFocusedInput,
+ getter_AddRefs(result));
+
+ nsCOMPtr<nsIRunnable> event =
+ new UpdateSearchResultRunnable(mLastListener, this, result);
+ NS_DispatchToCurrentThread(event);
+}
+
+NS_IMETHODIMP
+nsFormFillController::StopSearch()
+{
+ // Make sure to stop and clear this, otherwise the controller will prevent
+ // mLastFormAutoComplete from being deleted.
+ if (mLastFormAutoComplete) {
+ mLastFormAutoComplete->StopAutoCompleteSearch();
+ mLastFormAutoComplete = nullptr;
+ } else if (mLoginManager) {
+ mLoginManager->StopSearch();
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIFormAutoCompleteObserver
+
+NS_IMETHODIMP
+nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult *aResult)
+{
+ nsAutoString searchString;
+ aResult->GetSearchString(searchString);
+
+ mLastSearchString = searchString;
+
+ if (mLastListener) {
+ mLastListener->OnSearchResult(this, aResult);
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsIDOMEventListener
+
+NS_IMETHODIMP
+nsFormFillController::HandleEvent(nsIDOMEvent* aEvent)
+{
+ nsAutoString type;
+ aEvent->GetType(type);
+
+ if (type.EqualsLiteral("focus")) {
+ return Focus(aEvent);
+ }
+ if (type.EqualsLiteral("mousedown")) {
+ return MouseDown(aEvent);
+ }
+ if (type.EqualsLiteral("keypress")) {
+ return KeyPress(aEvent);
+ }
+ if (type.EqualsLiteral("input")) {
+ bool unused = false;
+ return (!mSuppressOnInput && mController && mFocusedInput) ?
+ mController->HandleText(&unused) : NS_OK;
+ }
+ if (type.EqualsLiteral("blur")) {
+ if (mFocusedInput)
+ StopControllingInput();
+ return NS_OK;
+ }
+ if (type.EqualsLiteral("compositionstart")) {
+ NS_ASSERTION(mController, "should have a controller!");
+ if (mController && mFocusedInput)
+ mController->HandleStartComposition();
+ return NS_OK;
+ }
+ if (type.EqualsLiteral("compositionend")) {
+ NS_ASSERTION(mController, "should have a controller!");
+ if (mController && mFocusedInput)
+ mController->HandleEndComposition();
+ return NS_OK;
+ }
+ if (type.EqualsLiteral("contextmenu")) {
+ mContextMenuFiredBeforeFocus = true;
+ if (mFocusedPopup)
+ mFocusedPopup->ClosePopup();
+ return NS_OK;
+ }
+ if (type.EqualsLiteral("pagehide")) {
+
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(
+ aEvent->InternalDOMEvent()->GetTarget());
+ if (!doc)
+ return NS_OK;
+
+ if (mFocusedInput) {
+ if (doc == mFocusedInputNode->OwnerDoc())
+ StopControllingInput();
+ }
+
+ RemoveForDocument(doc);
+ }
+
+ return NS_OK;
+}
+
+void
+nsFormFillController::RemoveForDocument(nsIDocument* aDoc)
+{
+ for (auto iter = mPwmgrInputs.Iter(); !iter.Done(); iter.Next()) {
+ const nsINode* key = iter.Key();
+ if (key && (!aDoc || key->OwnerDoc() == aDoc)) {
+ // mFocusedInputNode's observer is tracked separately, so don't remove it
+ // here.
+ if (key != mFocusedInputNode) {
+ const_cast<nsINode*>(key)->RemoveMutationObserver(this);
+ }
+ iter.Remove();
+ }
+ }
+}
+
+void
+nsFormFillController::MaybeStartControllingInput(nsIDOMHTMLInputElement* aInput)
+{
+ nsCOMPtr<nsINode> inputNode = do_QueryInterface(aInput);
+ if (!inputNode)
+ return;
+
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aInput);
+ if (!formControl || !formControl->IsSingleLineTextControl(false))
+ return;
+
+ bool isReadOnly = false;
+ aInput->GetReadOnly(&isReadOnly);
+ if (isReadOnly)
+ return;
+
+ bool autocomplete = nsContentUtils::IsAutocompleteEnabled(aInput);
+
+ nsCOMPtr<nsIDOMHTMLElement> datalist;
+ aInput->GetList(getter_AddRefs(datalist));
+ bool hasList = datalist != nullptr;
+
+ bool isPwmgrInput = false;
+ if (mPwmgrInputs.Get(inputNode) ||
+ formControl->GetType() == NS_FORM_INPUT_PASSWORD) {
+ isPwmgrInput = true;
+ }
+
+ if (isPwmgrInput || hasList || autocomplete) {
+ StartControllingInput(aInput);
+ }
+}
+
+nsresult
+nsFormFillController::Focus(nsIDOMEvent* aEvent)
+{
+ nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(
+ aEvent->InternalDOMEvent()->GetTarget());
+ MaybeStartControllingInput(input);
+
+ // Bail if we didn't start controlling the input.
+ if (!mFocusedInputNode) {
+ mContextMenuFiredBeforeFocus = false;
+ return NS_OK;
+ }
+
+#ifndef ANDROID
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode);
+ MOZ_ASSERT(formControl);
+
+ // If this focus doesn't immediately follow a contextmenu event then show
+ // the autocomplete popup for all password fields.
+ if (!mContextMenuFiredBeforeFocus
+ && formControl->GetType() == NS_FORM_INPUT_PASSWORD) {
+ ShowPopup();
+ }
+#endif
+
+ mContextMenuFiredBeforeFocus = false;
+ return NS_OK;
+}
+
+nsresult
+nsFormFillController::KeyPress(nsIDOMEvent* aEvent)
+{
+ NS_ASSERTION(mController, "should have a controller!");
+ if (!mFocusedInput || !mController)
+ return NS_OK;
+
+ nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
+ if (!keyEvent)
+ return NS_ERROR_FAILURE;
+
+ bool cancel = false;
+ bool unused = false;
+
+ uint32_t k;
+ keyEvent->GetKeyCode(&k);
+ switch (k) {
+ case nsIDOMKeyEvent::DOM_VK_DELETE:
+#ifndef XP_MACOSX
+ mController->HandleDelete(&cancel);
+ break;
+ case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
+ mController->HandleText(&unused);
+ break;
+#else
+ case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
+ {
+ bool isShift = false;
+ keyEvent->GetShiftKey(&isShift);
+
+ if (isShift) {
+ mController->HandleDelete(&cancel);
+ } else {
+ mController->HandleText(&unused);
+ }
+
+ break;
+ }
+#endif
+ case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
+ case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
+ {
+ bool isCtrl, isAlt, isMeta;
+ keyEvent->GetCtrlKey(&isCtrl);
+ keyEvent->GetAltKey(&isAlt);
+ keyEvent->GetMetaKey(&isMeta);
+ if (isCtrl || isAlt || isMeta)
+ break;
+ }
+ MOZ_FALLTHROUGH;
+ case nsIDOMKeyEvent::DOM_VK_UP:
+ case nsIDOMKeyEvent::DOM_VK_DOWN:
+ case nsIDOMKeyEvent::DOM_VK_LEFT:
+ case nsIDOMKeyEvent::DOM_VK_RIGHT:
+ {
+ // Get the writing-mode of the relevant input element,
+ // so that we can remap arrow keys if necessary.
+ mozilla::WritingMode wm;
+ if (mFocusedInputNode && mFocusedInputNode->IsElement()) {
+ mozilla::dom::Element *elem = mFocusedInputNode->AsElement();
+ nsIFrame *frame = elem->GetPrimaryFrame();
+ if (frame) {
+ wm = frame->GetWritingMode();
+ }
+ }
+ if (wm.IsVertical()) {
+ switch (k) {
+ case nsIDOMKeyEvent::DOM_VK_LEFT:
+ k = wm.IsVerticalLR() ? nsIDOMKeyEvent::DOM_VK_UP
+ : nsIDOMKeyEvent::DOM_VK_DOWN;
+ break;
+ case nsIDOMKeyEvent::DOM_VK_RIGHT:
+ k = wm.IsVerticalLR() ? nsIDOMKeyEvent::DOM_VK_DOWN
+ : nsIDOMKeyEvent::DOM_VK_UP;
+ break;
+ case nsIDOMKeyEvent::DOM_VK_UP:
+ k = nsIDOMKeyEvent::DOM_VK_LEFT;
+ break;
+ case nsIDOMKeyEvent::DOM_VK_DOWN:
+ k = nsIDOMKeyEvent::DOM_VK_RIGHT;
+ break;
+ }
+ }
+ }
+ mController->HandleKeyNavigation(k, &cancel);
+ break;
+ case nsIDOMKeyEvent::DOM_VK_ESCAPE:
+ mController->HandleEscape(&cancel);
+ break;
+ case nsIDOMKeyEvent::DOM_VK_TAB:
+ mController->HandleTab();
+ cancel = false;
+ break;
+ case nsIDOMKeyEvent::DOM_VK_RETURN:
+ mController->HandleEnter(false, aEvent, &cancel);
+ break;
+ }
+
+ if (cancel) {
+ aEvent->PreventDefault();
+ // Don't let the page see the RETURN event when the popup is open
+ // (indicated by cancel=true) so sites don't manually submit forms
+ // (e.g. via submit.click()) without the autocompleted value being filled.
+ // Bug 286933 will fix this for other key events.
+ if (k == nsIDOMKeyEvent::DOM_VK_RETURN) {
+ aEvent->StopPropagation();
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsFormFillController::MouseDown(nsIDOMEvent* aEvent)
+{
+ nsCOMPtr<nsIDOMMouseEvent> mouseEvent(do_QueryInterface(aEvent));
+ if (!mouseEvent)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIDOMHTMLInputElement> targetInput = do_QueryInterface(
+ aEvent->InternalDOMEvent()->GetTarget());
+ if (!targetInput)
+ return NS_OK;
+
+ int16_t button;
+ mouseEvent->GetButton(&button);
+ if (button != 0)
+ return NS_OK;
+
+ return ShowPopup();
+}
+
+NS_IMETHODIMP
+nsFormFillController::ShowPopup()
+{
+ bool isOpen = false;
+ GetPopupOpen(&isOpen);
+ if (isOpen) {
+ return SetPopupOpen(false);
+ }
+
+ nsCOMPtr<nsIAutoCompleteInput> input;
+ mController->GetInput(getter_AddRefs(input));
+ if (!input)
+ return NS_OK;
+
+ nsAutoString value;
+ input->GetTextValue(value);
+ if (value.Length() > 0) {
+ // Show the popup with a filtered result set
+ mController->SetSearchString(EmptyString());
+ bool unused = false;
+ mController->HandleText(&unused);
+ } else {
+ // Show the popup with the complete result set. Can't use HandleText()
+ // because it doesn't display the popup if the input is blank.
+ bool cancel = false;
+ mController->HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel);
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+//// nsFormFillController
+
+void
+nsFormFillController::AddWindowListeners(nsPIDOMWindowOuter* aWindow)
+{
+ if (!aWindow)
+ return;
+
+ EventTarget* target = aWindow->GetChromeEventHandler();
+
+ if (!target)
+ return;
+
+ target->AddEventListener(NS_LITERAL_STRING("focus"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("blur"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("pagehide"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("mousedown"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("input"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("keypress"), this, true, false);
+ target->AddEventListener(NS_LITERAL_STRING("compositionstart"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("compositionend"), this,
+ true, false);
+ target->AddEventListener(NS_LITERAL_STRING("contextmenu"), this,
+ true, false);
+
+ // Note that any additional listeners added should ensure that they ignore
+ // untrusted events, which might be sent by content that's up to no good.
+}
+
+void
+nsFormFillController::RemoveWindowListeners(nsPIDOMWindowOuter* aWindow)
+{
+ if (!aWindow)
+ return;
+
+ StopControllingInput();
+
+ nsCOMPtr<nsIDocument> doc = aWindow->GetDoc();
+ RemoveForDocument(doc);
+
+ EventTarget* target = aWindow->GetChromeEventHandler();
+
+ if (!target)
+ return;
+
+ target->RemoveEventListener(NS_LITERAL_STRING("focus"), this, true);
+ target->RemoveEventListener(NS_LITERAL_STRING("blur"), this, true);
+ target->RemoveEventListener(NS_LITERAL_STRING("pagehide"), this, true);
+ target->RemoveEventListener(NS_LITERAL_STRING("mousedown"), this, true);
+ target->RemoveEventListener(NS_LITERAL_STRING("input"), this, true);
+ target->RemoveEventListener(NS_LITERAL_STRING("keypress"), this, true);
+ target->RemoveEventListener(NS_LITERAL_STRING("compositionstart"), this,
+ true);
+ target->RemoveEventListener(NS_LITERAL_STRING("compositionend"), this,
+ true);
+ target->RemoveEventListener(NS_LITERAL_STRING("contextmenu"), this, true);
+}
+
+void
+nsFormFillController::StartControllingInput(nsIDOMHTMLInputElement *aInput)
+{
+ // Make sure we're not still attached to an input
+ StopControllingInput();
+
+ if (!mController) {
+ return;
+ }
+
+ // Find the currently focused docShell
+ nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(aInput);
+ int32_t index = GetIndexOfDocShell(docShell);
+ if (index < 0)
+ return;
+
+ // Cache the popup for the focused docShell
+ mFocusedPopup = mPopups.SafeElementAt(index);
+
+ nsCOMPtr<nsINode> node = do_QueryInterface(aInput);
+ if (!node) {
+ return;
+ }
+
+ node->AddMutationObserverUnlessExists(this);
+ mFocusedInputNode = node;
+ mFocusedInput = aInput;
+
+ nsCOMPtr<nsIDOMHTMLElement> list;
+ mFocusedInput->GetList(getter_AddRefs(list));
+ nsCOMPtr<nsINode> listNode = do_QueryInterface(list);
+ if (listNode) {
+ listNode->AddMutationObserverUnlessExists(this);
+ mListNode = listNode;
+ }
+
+ mController->SetInput(this);
+}
+
+void
+nsFormFillController::StopControllingInput()
+{
+ if (mListNode) {
+ mListNode->RemoveMutationObserver(this);
+ mListNode = nullptr;
+ }
+
+ if (mController) {
+ // Reset the controller's input, but not if it has been switched
+ // to another input already, which might happen if the user switches
+ // focus by clicking another autocomplete textbox
+ nsCOMPtr<nsIAutoCompleteInput> input;
+ mController->GetInput(getter_AddRefs(input));
+ if (input == this)
+ mController->SetInput(nullptr);
+ }
+
+ if (mFocusedInputNode) {
+ MaybeRemoveMutationObserver(mFocusedInputNode);
+
+ nsresult rv;
+ nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
+ do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
+ if (formAutoComplete) {
+ formAutoComplete->StopControllingInput(mFocusedInput);
+ }
+
+ mFocusedInputNode = nullptr;
+ mFocusedInput = nullptr;
+ }
+
+ if (mFocusedPopup) {
+ mFocusedPopup->ClosePopup();
+ }
+ mFocusedPopup = nullptr;
+}
+
+nsIDocShell *
+nsFormFillController::GetDocShellForInput(nsIDOMHTMLInputElement *aInput)
+{
+ nsCOMPtr<nsINode> node = do_QueryInterface(aInput);
+ NS_ENSURE_TRUE(node, nullptr);
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = node->OwnerDoc()->GetWindow();
+ NS_ENSURE_TRUE(win, nullptr);
+
+ return win->GetDocShell();
+}
+
+nsPIDOMWindowOuter*
+nsFormFillController::GetWindowForDocShell(nsIDocShell *aDocShell)
+{
+ nsCOMPtr<nsIContentViewer> contentViewer;
+ aDocShell->GetContentViewer(getter_AddRefs(contentViewer));
+ NS_ENSURE_TRUE(contentViewer, nullptr);
+
+ nsCOMPtr<nsIDOMDocument> domDoc;
+ contentViewer->GetDOMDocument(getter_AddRefs(domDoc));
+ nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
+ NS_ENSURE_TRUE(doc, nullptr);
+
+ return doc->GetWindow();
+}
+
+int32_t
+nsFormFillController::GetIndexOfDocShell(nsIDocShell *aDocShell)
+{
+ if (!aDocShell)
+ return -1;
+
+ // Loop through our cached docShells looking for the given docShell
+ uint32_t count = mDocShells.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ if (mDocShells[i] == aDocShell)
+ return i;
+ }
+
+ // Recursively check the parent docShell of this one
+ nsCOMPtr<nsIDocShellTreeItem> treeItem = do_QueryInterface(aDocShell);
+ nsCOMPtr<nsIDocShellTreeItem> parentItem;
+ treeItem->GetParent(getter_AddRefs(parentItem));
+ if (parentItem) {
+ nsCOMPtr<nsIDocShell> parentShell = do_QueryInterface(parentItem);
+ return GetIndexOfDocShell(parentShell);
+ }
+
+ return -1;
+}
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsFormFillController)
+
+NS_DEFINE_NAMED_CID(NS_FORMFILLCONTROLLER_CID);
+
+static const mozilla::Module::CIDEntry kSatchelCIDs[] = {
+ { &kNS_FORMFILLCONTROLLER_CID, false, nullptr, nsFormFillControllerConstructor },
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kSatchelContracts[] = {
+ { "@mozilla.org/satchel/form-fill-controller;1", &kNS_FORMFILLCONTROLLER_CID },
+ { NS_FORMHISTORYAUTOCOMPLETE_CONTRACTID, &kNS_FORMFILLCONTROLLER_CID },
+ { nullptr }
+};
+
+static const mozilla::Module kSatchelModule = {
+ mozilla::Module::kVersion,
+ kSatchelCIDs,
+ kSatchelContracts
+};
+
+NSMODULE_DEFN(satchel) = &kSatchelModule;
diff --git a/toolkit/components/satchel/nsFormFillController.h b/toolkit/components/satchel/nsFormFillController.h
new file mode 100644
index 0000000000..27fb1edbd9
--- /dev/null
+++ b/toolkit/components/satchel/nsFormFillController.h
@@ -0,0 +1,125 @@
+/* -*- 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 __nsFormFillController__
+#define __nsFormFillController__
+
+#include "nsIFormFillController.h"
+#include "nsIAutoCompleteInput.h"
+#include "nsIAutoCompleteSearch.h"
+#include "nsIAutoCompleteController.h"
+#include "nsIAutoCompletePopup.h"
+#include "nsIFormAutoComplete.h"
+#include "nsIDOMEventListener.h"
+#include "nsCOMPtr.h"
+#include "nsDataHashtable.h"
+#include "nsIDocShell.h"
+#include "nsIDOMHTMLInputElement.h"
+#include "nsILoginManager.h"
+#include "nsIMutationObserver.h"
+#include "nsTArray.h"
+#include "nsCycleCollectionParticipant.h"
+
+// X.h defines KeyPress
+#ifdef KeyPress
+#undef KeyPress
+#endif
+
+class nsFormHistory;
+class nsINode;
+class nsPIDOMWindowOuter;
+
+class nsFormFillController final : public nsIFormFillController,
+ public nsIAutoCompleteInput,
+ public nsIAutoCompleteSearch,
+ public nsIDOMEventListener,
+ public nsIFormAutoCompleteObserver,
+ public nsIMutationObserver
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSIFORMFILLCONTROLLER
+ NS_DECL_NSIAUTOCOMPLETESEARCH
+ NS_DECL_NSIAUTOCOMPLETEINPUT
+ NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER
+ NS_DECL_NSIDOMEVENTLISTENER
+ NS_DECL_NSIMUTATIONOBSERVER
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsFormFillController, nsIFormFillController)
+
+ nsresult Focus(nsIDOMEvent* aEvent);
+ nsresult KeyPress(nsIDOMEvent* aKeyEvent);
+ nsresult MouseDown(nsIDOMEvent* aMouseEvent);
+
+ nsFormFillController();
+
+protected:
+ virtual ~nsFormFillController();
+
+ void AddWindowListeners(nsPIDOMWindowOuter* aWindow);
+ void RemoveWindowListeners(nsPIDOMWindowOuter* aWindow);
+
+ void AddKeyListener(nsINode* aInput);
+ void RemoveKeyListener();
+
+ void StartControllingInput(nsIDOMHTMLInputElement *aInput);
+ void StopControllingInput();
+ /**
+ * Checks that aElement is a type of element we want to fill, then calls
+ * StartControllingInput on it.
+ */
+ void MaybeStartControllingInput(nsIDOMHTMLInputElement* aElement);
+
+ nsresult PerformInputListAutoComplete(const nsAString& aSearch,
+ nsIAutoCompleteResult** aResult);
+
+ void RevalidateDataList();
+ bool RowMatch(nsFormHistory *aHistory, uint32_t aIndex, const nsAString &aInputName, const nsAString &aInputValue);
+
+ inline nsIDocShell *GetDocShellForInput(nsIDOMHTMLInputElement *aInput);
+ inline nsPIDOMWindowOuter *GetWindowForDocShell(nsIDocShell *aDocShell);
+ inline int32_t GetIndexOfDocShell(nsIDocShell *aDocShell);
+
+ void MaybeRemoveMutationObserver(nsINode* aNode);
+
+ void RemoveForDocument(nsIDocument* aDoc);
+ bool IsEventTrusted(nsIDOMEvent *aEvent);
+ // members //////////////////////////////////////////
+
+ nsCOMPtr<nsIAutoCompleteController> mController;
+ nsCOMPtr<nsILoginManager> mLoginManager;
+ nsIDOMHTMLInputElement* mFocusedInput;
+ nsINode* mFocusedInputNode;
+
+ // mListNode is a <datalist> element which, is set, has the form fill controller
+ // as a mutation observer for it.
+ nsINode* mListNode;
+ nsCOMPtr<nsIAutoCompletePopup> mFocusedPopup;
+
+ nsTArray<nsCOMPtr<nsIDocShell> > mDocShells;
+ nsTArray<nsCOMPtr<nsIAutoCompletePopup> > mPopups;
+
+ // The observer passed to StartSearch. It will be notified when the search is
+ // complete or the data from a datalist changes.
+ nsCOMPtr<nsIAutoCompleteObserver> mLastListener;
+
+ // This is cleared by StopSearch().
+ nsCOMPtr<nsIFormAutoComplete> mLastFormAutoComplete;
+ nsString mLastSearchString;
+
+ nsDataHashtable<nsPtrHashKey<const nsINode>, bool> mPwmgrInputs;
+
+ uint32_t mTimeout;
+ uint32_t mMinResultsForPopup;
+ uint32_t mMaxRows;
+ bool mContextMenuFiredBeforeFocus;
+ bool mDisableAutoComplete;
+ bool mCompleteDefaultIndex;
+ bool mCompleteSelectedIndex;
+ bool mForceComplete;
+ bool mSuppressOnInput;
+};
+
+#endif // __nsFormFillController__
diff --git a/toolkit/components/satchel/nsFormHistory.js b/toolkit/components/satchel/nsFormHistory.js
new file mode 100644
index 0000000000..d68be2d587
--- /dev/null
+++ b/toolkit/components/satchel/nsFormHistory.js
@@ -0,0 +1,894 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+
+const DB_VERSION = 4;
+const DAY_IN_MS = 86400000; // 1 day in milliseconds
+
+function FormHistory() {
+ Deprecated.warning(
+ "nsIFormHistory2 is deprecated and will be removed in a future version",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=879118");
+ this.init();
+}
+
+FormHistory.prototype = {
+ classID : Components.ID("{0c1bb408-71a2-403f-854a-3a0659829ded}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormHistory2,
+ Ci.nsIObserver,
+ Ci.nsIMessageListener,
+ Ci.nsISupportsWeakReference,
+ ]),
+
+ debug : true,
+ enabled : true,
+
+ // The current database schema.
+ dbSchema : {
+ tables : {
+ moz_formhistory: {
+ "id" : "INTEGER PRIMARY KEY",
+ "fieldname" : "TEXT NOT NULL",
+ "value" : "TEXT NOT NULL",
+ "timesUsed" : "INTEGER",
+ "firstUsed" : "INTEGER",
+ "lastUsed" : "INTEGER",
+ "guid" : "TEXT"
+ },
+ moz_deleted_formhistory: {
+ "id" : "INTEGER PRIMARY KEY",
+ "timeDeleted" : "INTEGER",
+ "guid" : "TEXT"
+ }
+ },
+ indices : {
+ moz_formhistory_index : {
+ table : "moz_formhistory",
+ columns : ["fieldname"]
+ },
+ moz_formhistory_lastused_index : {
+ table : "moz_formhistory",
+ columns : ["lastUsed"]
+ },
+ moz_formhistory_guid_index : {
+ table : "moz_formhistory",
+ columns : ["guid"]
+ },
+ }
+ },
+ dbStmts : null, // Database statements for memoization
+ dbFile : null,
+
+ _uuidService: null,
+ get uuidService() {
+ if (!this._uuidService)
+ this._uuidService = Cc["@mozilla.org/uuid-generator;1"].
+ getService(Ci.nsIUUIDGenerator);
+ return this._uuidService;
+ },
+
+ log : function log(message) {
+ if (!this.debug)
+ return;
+ dump("FormHistory: " + message + "\n");
+ Services.console.logStringMessage("FormHistory: " + message);
+ },
+
+
+ init : function init() {
+ this.updatePrefs();
+
+ this.dbStmts = {};
+
+ // Add observer
+ Services.obs.addObserver(this, "profile-before-change", true);
+ },
+
+ /* ---- nsIFormHistory2 interfaces ---- */
+
+
+ get hasEntries() {
+ return (this.countAllEntries() > 0);
+ },
+
+
+ addEntry : function addEntry(name, value) {
+ if (!this.enabled)
+ return;
+
+ this.log("addEntry for " + name + "=" + value);
+
+ let now = Date.now() * 1000; // microseconds
+
+ let [id, guid] = this.getExistingEntryID(name, value);
+ let stmt;
+
+ if (id != -1) {
+ // Update existing entry.
+ let query = "UPDATE moz_formhistory SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE id = :id";
+ let params = {
+ lastUsed : now,
+ id : id
+ };
+
+ try {
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ this.sendStringNotification("modifyEntry", name, value, guid);
+ } catch (e) {
+ this.log("addEntry (modify) failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ } else {
+ // Add new entry.
+ guid = this.generateGUID();
+
+ let query = "INSERT INTO moz_formhistory (fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
+ "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
+ let params = {
+ fieldname : name,
+ value : value,
+ timesUsed : 1,
+ firstUsed : now,
+ lastUsed : now,
+ guid : guid
+ };
+
+ try {
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ this.sendStringNotification("addEntry", name, value, guid);
+ } catch (e) {
+ this.log("addEntry (create) failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+
+ removeEntry : function removeEntry(name, value) {
+ this.log("removeEntry for " + name + "=" + value);
+
+ let [id, guid] = this.getExistingEntryID(name, value);
+ this.sendStringNotification("before-removeEntry", name, value, guid);
+
+ let stmt;
+ let query = "DELETE FROM moz_formhistory WHERE id = :id";
+ let params = { id : id };
+ let existingTransactionInProgress;
+
+ try {
+ // Don't start a transaction if one is already in progress since we can't nest them.
+ existingTransactionInProgress = this.dbConnection.transactionInProgress;
+ if (!existingTransactionInProgress)
+ this.dbConnection.beginTransaction();
+ this.moveToDeletedTable("VALUES (:guid, :timeDeleted)", {
+ guid: guid,
+ timeDeleted: Date.now()
+ });
+
+ // remove from the formhistory database
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ this.sendStringNotification("removeEntry", name, value, guid);
+ } catch (e) {
+ if (!existingTransactionInProgress)
+ this.dbConnection.rollbackTransaction();
+ this.log("removeEntry failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ if (!existingTransactionInProgress)
+ this.dbConnection.commitTransaction();
+ },
+
+
+ removeEntriesForName : function removeEntriesForName(name) {
+ this.log("removeEntriesForName with name=" + name);
+
+ this.sendStringNotification("before-removeEntriesForName", name);
+
+ let stmt;
+ let query = "DELETE FROM moz_formhistory WHERE fieldname = :fieldname";
+ let params = { fieldname : name };
+ let existingTransactionInProgress;
+
+ try {
+ // Don't start a transaction if one is already in progress since we can't nest them.
+ existingTransactionInProgress = this.dbConnection.transactionInProgress;
+ if (!existingTransactionInProgress)
+ this.dbConnection.beginTransaction();
+ this.moveToDeletedTable(
+ "SELECT guid, :timeDeleted FROM moz_formhistory " +
+ "WHERE fieldname = :fieldname", {
+ fieldname: name,
+ timeDeleted: Date.now()
+ });
+
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ this.sendStringNotification("removeEntriesForName", name);
+ } catch (e) {
+ if (!existingTransactionInProgress)
+ this.dbConnection.rollbackTransaction();
+ this.log("removeEntriesForName failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ if (!existingTransactionInProgress)
+ this.dbConnection.commitTransaction();
+ },
+
+
+ removeAllEntries : function removeAllEntries() {
+ this.log("removeAllEntries");
+
+ this.sendNotification("before-removeAllEntries", null);
+
+ let stmt;
+ let query = "DELETE FROM moz_formhistory";
+ let existingTransactionInProgress;
+
+ try {
+ // Don't start a transaction if one is already in progress since we can't nest them.
+ existingTransactionInProgress = this.dbConnection.transactionInProgress;
+ if (!existingTransactionInProgress)
+ this.dbConnection.beginTransaction();
+ // TODO: Add these items to the deleted items table once we've sorted
+ // out the issues from bug 756701
+ stmt = this.dbCreateStatement(query);
+ stmt.execute();
+ this.sendNotification("removeAllEntries", null);
+ } catch (e) {
+ if (!existingTransactionInProgress)
+ this.dbConnection.rollbackTransaction();
+ this.log("removeAllEntries failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ if (!existingTransactionInProgress)
+ this.dbConnection.commitTransaction();
+ },
+
+
+ nameExists : function nameExists(name) {
+ this.log("nameExists for name=" + name);
+ let stmt;
+ let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory WHERE fieldname = :fieldname";
+ let params = { fieldname : name };
+ try {
+ stmt = this.dbCreateStatement(query, params);
+ stmt.executeStep();
+ return (stmt.row.numEntries > 0);
+ } catch (e) {
+ this.log("nameExists failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ },
+
+ entryExists : function entryExists(name, value) {
+ this.log("entryExists for " + name + "=" + value);
+ let [id] = this.getExistingEntryID(name, value);
+ this.log("entryExists: id=" + id);
+ return (id != -1);
+ },
+
+ removeEntriesByTimeframe : function removeEntriesByTimeframe(beginTime, endTime) {
+ this.log("removeEntriesByTimeframe for " + beginTime + " to " + endTime);
+
+ this.sendIntNotification("before-removeEntriesByTimeframe", beginTime, endTime);
+
+ let stmt;
+ let query = "DELETE FROM moz_formhistory WHERE firstUsed >= :beginTime AND firstUsed <= :endTime";
+ let params = {
+ beginTime : beginTime,
+ endTime : endTime
+ };
+ let existingTransactionInProgress;
+
+ try {
+ // Don't start a transaction if one is already in progress since we can't nest them.
+ existingTransactionInProgress = this.dbConnection.transactionInProgress;
+ if (!existingTransactionInProgress)
+ this.dbConnection.beginTransaction();
+ this.moveToDeletedTable(
+ "SELECT guid, :timeDeleted FROM moz_formhistory " +
+ "WHERE firstUsed >= :beginTime AND firstUsed <= :endTime", {
+ beginTime: beginTime,
+ endTime: endTime
+ });
+
+ stmt = this.dbCreateStatement(query, params);
+ stmt.executeStep();
+ this.sendIntNotification("removeEntriesByTimeframe", beginTime, endTime);
+ } catch (e) {
+ if (!existingTransactionInProgress)
+ this.dbConnection.rollbackTransaction();
+ this.log("removeEntriesByTimeframe failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ if (!existingTransactionInProgress)
+ this.dbConnection.commitTransaction();
+ },
+
+ moveToDeletedTable : function moveToDeletedTable(values, params) {
+ if (AppConstants.platform == "android") {
+ this.log("Moving entries to deleted table.");
+
+ let stmt;
+
+ try {
+ // Move the entries to the deleted items table.
+ let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted) ";
+ if (values) query += values;
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Moving deleted entries failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+ get dbConnection() {
+ // Make sure dbConnection can't be called from now to prevent infinite loops.
+ delete FormHistory.prototype.dbConnection;
+
+ try {
+ this.dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+ this.dbFile.append("formhistory.sqlite");
+ this.log("Opening database at " + this.dbFile.path);
+
+ FormHistory.prototype.dbConnection = this.dbOpen();
+ this.dbInit();
+ } catch (e) {
+ this.log("Initialization failed: " + e);
+ // If dbInit fails...
+ if (e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
+ this.dbCleanup();
+ FormHistory.prototype.dbConnection = this.dbOpen();
+ this.dbInit();
+ } else {
+ throw "Initialization failed";
+ }
+ }
+
+ return FormHistory.prototype.dbConnection;
+ },
+
+ get DBConnection() {
+ return this.dbConnection;
+ },
+
+
+ /* ---- nsIObserver interface ---- */
+
+
+ observe : function observe(subject, topic, data) {
+ switch (topic) {
+ case "nsPref:changed":
+ this.updatePrefs();
+ break;
+ case "profile-before-change":
+ this._dbClose(false);
+ break;
+ default:
+ this.log("Oops! Unexpected notification: " + topic);
+ break;
+ }
+ },
+
+
+ /* ---- helpers ---- */
+
+
+ generateGUID : function() {
+ // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}"
+ let uuid = this.uuidService.generateUUID().toString();
+ let raw = ""; // A string with the low bytes set to random values
+ let bytes = 0;
+ for (let i = 1; bytes < 12 ; i+= 2) {
+ // Skip dashes
+ if (uuid[i] == "-")
+ i++;
+ let hexVal = parseInt(uuid[i] + uuid[i + 1], 16);
+ raw += String.fromCharCode(hexVal);
+ bytes++;
+ }
+ return btoa(raw);
+ },
+
+
+ sendStringNotification : function (changeType, str1, str2, str3) {
+ function wrapit(str) {
+ let wrapper = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ wrapper.data = str;
+ return wrapper;
+ }
+
+ let strData;
+ if (arguments.length == 2) {
+ // Just 1 string, no need to put it in an array
+ strData = wrapit(str1);
+ } else {
+ // 3 strings, put them in an array.
+ strData = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ strData.appendElement(wrapit(str1), false);
+ strData.appendElement(wrapit(str2), false);
+ strData.appendElement(wrapit(str3), false);
+ }
+ this.sendNotification(changeType, strData);
+ },
+
+
+ sendIntNotification : function (changeType, int1, int2) {
+ function wrapit(int) {
+ let wrapper = Cc["@mozilla.org/supports-PRInt64;1"].
+ createInstance(Ci.nsISupportsPRInt64);
+ wrapper.data = int;
+ return wrapper;
+ }
+
+ let intData;
+ if (arguments.length == 2) {
+ // Just 1 int, no need for an array
+ intData = wrapit(int1);
+ } else {
+ // 2 ints, put them in an array.
+ intData = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ intData.appendElement(wrapit(int1), false);
+ intData.appendElement(wrapit(int2), false);
+ }
+ this.sendNotification(changeType, intData);
+ },
+
+
+ sendNotification : function (changeType, data) {
+ Services.obs.notifyObservers(data, "satchel-storage-changed", changeType);
+ },
+
+
+ getExistingEntryID : function (name, value) {
+ let id = -1, guid = null;
+ let stmt;
+ let query = "SELECT id, guid FROM moz_formhistory WHERE fieldname = :fieldname AND value = :value";
+ let params = {
+ fieldname : name,
+ value : value
+ };
+ try {
+ stmt = this.dbCreateStatement(query, params);
+ if (stmt.executeStep()) {
+ id = stmt.row.id;
+ guid = stmt.row.guid;
+ }
+ } catch (e) {
+ this.log("getExistingEntryID failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ return [id, guid];
+ },
+
+
+ countAllEntries : function () {
+ let query = "SELECT COUNT(1) AS numEntries FROM moz_formhistory";
+
+ let stmt, numEntries;
+ try {
+ stmt = this.dbCreateStatement(query, null);
+ stmt.executeStep();
+ numEntries = stmt.row.numEntries;
+ } catch (e) {
+ this.log("countAllEntries failed: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ this.log("countAllEntries: counted entries: " + numEntries);
+ return numEntries;
+ },
+
+
+ updatePrefs : function () {
+ this.debug = Services.prefs.getBoolPref("browser.formfill.debug");
+ this.enabled = Services.prefs.getBoolPref("browser.formfill.enable");
+ },
+
+ // Database Creation & Access
+
+ /*
+ * dbCreateStatement
+ *
+ * Creates a statement, wraps it, and then does parameter replacement
+ * Will use memoization so that statements can be reused.
+ */
+ dbCreateStatement : function (query, params) {
+ let stmt = this.dbStmts[query];
+ // Memoize the statements
+ if (!stmt) {
+ this.log("Creating new statement for query: " + query);
+ stmt = this.dbConnection.createStatement(query);
+ this.dbStmts[query] = stmt;
+ }
+ // Replace parameters, must be done 1 at a time
+ if (params)
+ for (let i in params)
+ stmt.params[i] = params[i];
+ return stmt;
+ },
+
+ /*
+ * dbOpen
+ *
+ * Open a connection with the database and returns it.
+ *
+ * @returns a db connection object.
+ */
+ dbOpen : function () {
+ this.log("Open Database");
+
+ let storage = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ return storage.openDatabase(this.dbFile);
+ },
+
+ /*
+ * dbInit
+ *
+ * Attempts to initialize the database. This creates the file if it doesn't
+ * exist, performs any migrations, etc.
+ */
+ dbInit : function () {
+ this.log("Initializing Database");
+
+ let version = this.dbConnection.schemaVersion;
+
+ // Note: Firefox 3 didn't set a schema value, so it started from 0.
+ // So we can't depend on a simple version == 0 check
+ if (version == 0 && !this.dbConnection.tableExists("moz_formhistory"))
+ this.dbCreate();
+ else if (version != DB_VERSION)
+ this.dbMigrate(version);
+ },
+
+
+ dbCreate: function () {
+ this.log("Creating DB -- tables");
+ for (let name in this.dbSchema.tables) {
+ let table = this.dbSchema.tables[name];
+ this.dbCreateTable(name, table);
+ }
+
+ this.log("Creating DB -- indices");
+ for (let name in this.dbSchema.indices) {
+ let index = this.dbSchema.indices[name];
+ let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
+ "(" + index.columns.join(", ") + ")";
+ this.dbConnection.executeSimpleSQL(statement);
+ }
+
+ this.dbConnection.schemaVersion = DB_VERSION;
+ },
+
+ dbCreateTable: function(name, table) {
+ let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
+ this.log("Creating table " + name + " with " + tSQL);
+ this.dbConnection.createTable(name, tSQL);
+ },
+
+ dbMigrate : function (oldVersion) {
+ this.log("Attempting to migrate from version " + oldVersion);
+
+ if (oldVersion > DB_VERSION) {
+ this.log("Downgrading to version " + DB_VERSION);
+ // User's DB is newer. Sanity check that our expected columns are
+ // present, and if so mark the lower version and merrily continue
+ // on. If the columns are borked, something is wrong so blow away
+ // the DB and start from scratch. [Future incompatible upgrades
+ // should swtich to a different table or file.]
+
+ if (!this.dbAreExpectedColumnsPresent())
+ throw Components.Exception("DB is missing expected columns",
+ Cr.NS_ERROR_FILE_CORRUPTED);
+
+ // Change the stored version to the current version. If the user
+ // runs the newer code again, it will see the lower version number
+ // and re-upgrade (to fixup any entries the old code added).
+ this.dbConnection.schemaVersion = DB_VERSION;
+ return;
+ }
+
+ // Upgrade to newer version...
+
+ this.dbConnection.beginTransaction();
+
+ try {
+ for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
+ this.log("Upgrading to version " + v + "...");
+ let migrateFunction = "dbMigrateToVersion" + v;
+ this[migrateFunction]();
+ }
+ } catch (e) {
+ this.log("Migration failed: " + e);
+ this.dbConnection.rollbackTransaction();
+ throw e;
+ }
+
+ this.dbConnection.schemaVersion = DB_VERSION;
+ this.dbConnection.commitTransaction();
+ this.log("DB migration completed.");
+ },
+
+
+ /*
+ * dbMigrateToVersion1
+ *
+ * Updates the DB schema to v1 (bug 463154).
+ * Adds firstUsed, lastUsed, timesUsed columns.
+ */
+ dbMigrateToVersion1 : function () {
+ // Check to see if the new columns already exist (could be a v1 DB that
+ // was downgraded to v0). If they exist, we don't need to add them.
+ let query;
+ ["timesUsed", "firstUsed", "lastUsed"].forEach(function(column) {
+ if (!this.dbColumnExists(column)) {
+ query = "ALTER TABLE moz_formhistory ADD COLUMN " + column + " INTEGER";
+ this.dbConnection.executeSimpleSQL(query);
+ }
+ }, this);
+
+ // Set the default values for the new columns.
+ //
+ // Note that we set the timestamps to 24 hours in the past. We want a
+ // timestamp that's recent (so that "keep form history for 90 days"
+ // doesn't expire things surprisingly soon), but not so recent that
+ // "forget the last hour of stuff" deletes all freshly migrated data.
+ let stmt;
+ query = "UPDATE moz_formhistory " +
+ "SET timesUsed = 1, firstUsed = :time, lastUsed = :time " +
+ "WHERE timesUsed isnull OR firstUsed isnull or lastUsed isnull";
+ let params = { time: (Date.now() - DAY_IN_MS) * 1000 }
+ try {
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting timestamps: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ },
+
+
+ /*
+ * dbMigrateToVersion2
+ *
+ * Updates the DB schema to v2 (bug 243136).
+ * Adds lastUsed index, removes moz_dummy_table
+ */
+ dbMigrateToVersion2 : function () {
+ let query = "DROP TABLE IF EXISTS moz_dummy_table";
+ this.dbConnection.executeSimpleSQL(query);
+
+ query = "CREATE INDEX IF NOT EXISTS moz_formhistory_lastused_index ON moz_formhistory (lastUsed)";
+ this.dbConnection.executeSimpleSQL(query);
+ },
+
+
+ /*
+ * dbMigrateToVersion3
+ *
+ * Updates the DB schema to v3 (bug 506402).
+ * Adds guid column and index.
+ */
+ dbMigrateToVersion3 : function () {
+ // Check to see if GUID column already exists, add if needed
+ let query;
+ if (!this.dbColumnExists("guid")) {
+ query = "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT";
+ this.dbConnection.executeSimpleSQL(query);
+
+ query = "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index ON moz_formhistory (guid)";
+ this.dbConnection.executeSimpleSQL(query);
+ }
+
+ // Get a list of IDs for existing logins
+ let ids = [];
+ query = "SELECT id FROM moz_formhistory WHERE guid isnull";
+ let stmt;
+ try {
+ stmt = this.dbCreateStatement(query);
+ while (stmt.executeStep())
+ ids.push(stmt.row.id);
+ } catch (e) {
+ this.log("Failed getting IDs: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+
+ // Generate a GUID for each login and update the DB.
+ query = "UPDATE moz_formhistory SET guid = :guid WHERE id = :id";
+ for (let id of ids) {
+ let params = {
+ id : id,
+ guid : this.generateGUID()
+ };
+
+ try {
+ stmt = this.dbCreateStatement(query, params);
+ stmt.execute();
+ } catch (e) {
+ this.log("Failed setting GUID: " + e);
+ throw e;
+ } finally {
+ if (stmt) {
+ stmt.reset();
+ }
+ }
+ }
+ },
+
+ dbMigrateToVersion4 : function () {
+ if (!this.dbConnection.tableExists("moz_deleted_formhistory")) {
+ this.dbCreateTable("moz_deleted_formhistory", this.dbSchema.tables.moz_deleted_formhistory);
+ }
+ },
+
+ /*
+ * dbAreExpectedColumnsPresent
+ *
+ * Sanity check to ensure that the columns this version of the code expects
+ * are present in the DB we're using.
+ */
+ dbAreExpectedColumnsPresent : function () {
+ for (let name in this.dbSchema.tables) {
+ let table = this.dbSchema.tables[name];
+ let query = "SELECT " +
+ Object.keys(table).join(", ") +
+ " FROM " + name;
+ try {
+ let stmt = this.dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ } catch (e) {
+ return false;
+ }
+ }
+
+ this.log("verified that expected columns are present in DB.");
+ return true;
+ },
+
+
+ /*
+ * dbColumnExists
+ *
+ * Checks to see if the named column already exists.
+ */
+ dbColumnExists : function (columnName) {
+ let query = "SELECT " + columnName + " FROM moz_formhistory";
+ try {
+ let stmt = this.dbConnection.createStatement(query);
+ // (no need to execute statement, if it compiled we're good)
+ stmt.finalize();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ /**
+ * _dbClose
+ *
+ * Finalize all statements and close the connection.
+ *
+ * @param aBlocking - Should we spin the loop waiting for the db to be
+ * closed.
+ */
+ _dbClose : function FH__dbClose(aBlocking) {
+ for (let query in this.dbStmts) {
+ let stmt = this.dbStmts[query];
+ stmt.finalize();
+ }
+ this.dbStmts = {};
+
+ let connectionDescriptor = Object.getOwnPropertyDescriptor(FormHistory.prototype, "dbConnection");
+ // Return if the database hasn't been opened.
+ if (!connectionDescriptor || connectionDescriptor.value === undefined)
+ return;
+
+ let completed = false;
+ try {
+ this.dbConnection.asyncClose(function () { completed = true; });
+ } catch (e) {
+ completed = true;
+ Components.utils.reportError(e);
+ }
+
+ let thread = Services.tm.currentThread;
+ while (aBlocking && !completed) {
+ thread.processNextEvent(true);
+ }
+ },
+
+ /*
+ * dbCleanup
+ *
+ * Called when database creation fails. Finalizes database statements,
+ * closes the database connection, deletes the database file.
+ */
+ dbCleanup : function () {
+ this.log("Cleaning up DB file - close & remove & backup")
+
+ // Create backup file
+ let storage = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ let backupFile = this.dbFile.leafName + ".corrupt";
+ storage.backupDatabaseFile(this.dbFile, backupFile);
+
+ this._dbClose(true);
+ this.dbFile.remove(false);
+ }
+};
+
+var component = [FormHistory];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/satchel/nsIFormAutoComplete.idl b/toolkit/components/satchel/nsIFormAutoComplete.idl
new file mode 100644
index 0000000000..6ce8563be2
--- /dev/null
+++ b/toolkit/components/satchel/nsIFormAutoComplete.idl
@@ -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/. */
+
+
+#include "nsISupports.idl"
+
+interface nsIAutoCompleteResult;
+interface nsIFormAutoCompleteObserver;
+interface nsIDOMHTMLInputElement;
+
+[scriptable, uuid(bfd9b82b-0ab3-4b6b-9e54-aa961ff4b732)]
+interface nsIFormAutoComplete: nsISupports {
+ /**
+ * Generate results for a form input autocomplete menu asynchronously.
+ */
+ void autoCompleteSearchAsync(in AString aInputName,
+ in AString aSearchString,
+ in nsIDOMHTMLInputElement aField,
+ in nsIAutoCompleteResult aPreviousResult,
+ in nsIAutoCompleteResult aDatalistResult,
+ in nsIFormAutoCompleteObserver aListener);
+
+ /**
+ * If a search is in progress, stop it. Otherwise, do nothing. This is used
+ * to cancel an existing search, for example, in preparation for a new search.
+ */
+ void stopAutoCompleteSearch();
+
+ /**
+ * Since the controller is disconnecting, any related data must be cleared.
+ */
+ void stopControllingInput(in nsIDOMHTMLInputElement aField);
+};
+
+[scriptable, function, uuid(604419ab-55a0-4831-9eca-1b9e67cc4751)]
+interface nsIFormAutoCompleteObserver : nsISupports
+{
+ /*
+ * Called when a search is complete and the results are ready even if the
+ * result set is empty. If the search is cancelled or a new search is
+ * started, this is not called.
+ *
+ * @param result - The search result object
+ */
+ void onSearchCompletion(in nsIAutoCompleteResult result);
+};
diff --git a/toolkit/components/satchel/nsIFormFillController.idl b/toolkit/components/satchel/nsIFormFillController.idl
new file mode 100644
index 0000000000..34104c91fc
--- /dev/null
+++ b/toolkit/components/satchel/nsIFormFillController.idl
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIDocShell;
+interface nsIAutoCompletePopup;
+interface nsIDOMHTMLInputElement;
+
+/*
+ * nsIFormFillController is an interface for controlling form fill behavior
+ * on HTML documents. Any number of docShells can be controller concurrently.
+ * While a docShell is attached, all HTML documents that are loaded within it
+ * will have a focus listener attached that will listen for when a text input
+ * is focused. When this happens, the input will be bound to the
+ * global nsIAutoCompleteController service.
+ */
+
+[scriptable, uuid(07f0a0dc-f6e9-4cdd-a55f-56d770523a4c)]
+interface nsIFormFillController : nsISupports
+{
+ /*
+ * The input element the form fill controller is currently bound to.
+ */
+ readonly attribute nsIDOMHTMLInputElement focusedInput;
+
+ /*
+ * Start controlling form fill behavior for the given browser
+ *
+ * @param docShell - The docShell to attach to
+ * @param popup - The popup to show when autocomplete results are available
+ */
+ void attachToBrowser(in nsIDocShell docShell, in nsIAutoCompletePopup popup);
+
+ /*
+ * Stop controlling form fill behavior for the given browser
+ *
+ * @param docShell - The docShell to detach from
+ */
+ void detachFromBrowser(in nsIDocShell docShell);
+
+ /*
+ * Mark the specified <input> element as being managed by password manager.
+ * Autocomplete requests will be handed off to the password manager, and will
+ * not be stored in form history.
+ *
+ * @param aInput - The HTML <input> element to tag
+ */
+ void markAsLoginManagerField(in nsIDOMHTMLInputElement aInput);
+
+ /*
+ * Open the autocomplete popup, if possible.
+ */
+ void showPopup();
+};
diff --git a/toolkit/components/satchel/nsIFormHistory.idl b/toolkit/components/satchel/nsIFormHistory.idl
new file mode 100644
index 0000000000..ac78451e90
--- /dev/null
+++ b/toolkit/components/satchel/nsIFormHistory.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+interface nsIFile;
+interface mozIStorageConnection;
+
+/**
+ * The nsIFormHistory object is a service which holds a set of name/value
+ * pairs. The names correspond to form field names, and the values correspond
+ * to values the user has submitted. So, several values may exist for a single
+ * name.
+ *
+ * Note: this interface provides no means to access stored values.
+ * Stored values are used by the FormFillController to generate
+ * autocomplete matches.
+ *
+ * @deprecated use FormHistory.jsm instead.
+ */
+
+[scriptable, uuid(5d7d84d1-9798-4016-bf61-a32acf09b29d)]
+interface nsIFormHistory2 : nsISupports
+{
+ /**
+ * Returns true if the form history has any entries.
+ */
+ readonly attribute boolean hasEntries;
+
+ /**
+ * Adds a name and value pair to the form history.
+ */
+ void addEntry(in AString name, in AString value);
+
+ /**
+ * Removes a name and value pair from the form history.
+ */
+ void removeEntry(in AString name, in AString value);
+
+ /**
+ * Removes all entries that are paired with a name.
+ */
+ void removeEntriesForName(in AString name);
+
+ /**
+ * Removes all entries in the entire form history.
+ */
+ void removeAllEntries();
+
+ /**
+ * Returns true if there is no entry that is paired with a name.
+ */
+ boolean nameExists(in AString name);
+
+ /**
+ * Gets whether a name and value pair exists in the form history.
+ */
+ boolean entryExists(in AString name, in AString value);
+
+ /**
+ * Removes entries that were created between the specified times.
+ *
+ * @param aBeginTime
+ * The beginning of the timeframe, in microseconds
+ * @param aEndTime
+ * The end of the timeframe, in microseconds
+ */
+ void removeEntriesByTimeframe(in long long aBeginTime, in long long aEndTime);
+
+ /**
+ * Returns the underlying DB connection the form history module is using.
+ */
+ readonly attribute mozIStorageConnection DBConnection;
+};
diff --git a/toolkit/components/satchel/nsIInputListAutoComplete.idl b/toolkit/components/satchel/nsIInputListAutoComplete.idl
new file mode 100644
index 0000000000..6f0f492b09
--- /dev/null
+++ b/toolkit/components/satchel/nsIInputListAutoComplete.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIAutoCompleteResult;
+interface nsIDOMHTMLInputElement;
+
+[scriptable, uuid(0e33de3e-4faf-4a1a-b96e-24115b8bfd45)]
+interface nsIInputListAutoComplete: nsISupports {
+ /**
+ * Generate results for a form input autocomplete menu.
+ */
+ nsIAutoCompleteResult autoCompleteSearch(in AString aSearchString,
+ in nsIDOMHTMLInputElement aField);
+};
diff --git a/toolkit/components/satchel/nsInputListAutoComplete.js b/toolkit/components/satchel/nsInputListAutoComplete.js
new file mode 100644
index 0000000000..f42427862c
--- /dev/null
+++ b/toolkit/components/satchel/nsInputListAutoComplete.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/. */
+
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
+
+function InputListAutoComplete() {}
+
+InputListAutoComplete.prototype = {
+ classID : Components.ID("{bf1e01d0-953e-11df-981c-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInputListAutoComplete]),
+
+ autoCompleteSearch : function (aUntrimmedSearchString, aField) {
+ let [values, labels] = this.getListSuggestions(aField);
+ let searchResult = values.length > 0 ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
+ : Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
+ let defaultIndex = values.length > 0 ? 0 : -1;
+ return new FormAutoCompleteResult(aUntrimmedSearchString,
+ searchResult, defaultIndex, "",
+ values, labels, [], null);
+ },
+
+ getListSuggestions : function (aField) {
+ let values = [];
+ let labels = [];
+
+ if (aField) {
+ let filter = !aField.hasAttribute("mozNoFilter");
+ let lowerFieldValue = aField.value.toLowerCase();
+
+ if (aField.list) {
+ let options = aField.list.options;
+ let length = options.length;
+ for (let i = 0; i < length; i++) {
+ let item = options.item(i);
+ let label = "";
+ if (item.label) {
+ label = item.label;
+ } else if (item.text) {
+ label = item.text;
+ } else {
+ label = item.value;
+ }
+
+ if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1) {
+ continue;
+ }
+
+ labels.push(label);
+ values.push(item.value);
+ }
+ }
+ }
+
+ return [values, labels];
+ }
+};
+
+var component = [InputListAutoComplete];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/satchel/satchel.manifest b/toolkit/components/satchel/satchel.manifest
new file mode 100644
index 0000000000..5afc0a38c0
--- /dev/null
+++ b/toolkit/components/satchel/satchel.manifest
@@ -0,0 +1,10 @@
+component {0c1bb408-71a2-403f-854a-3a0659829ded} nsFormHistory.js
+contract @mozilla.org/satchel/form-history;1 {0c1bb408-71a2-403f-854a-3a0659829ded}
+component {c11c21b2-71c9-4f87-a0f8-5e13f50495fd} nsFormAutoComplete.js
+contract @mozilla.org/satchel/form-autocomplete;1 {c11c21b2-71c9-4f87-a0f8-5e13f50495fd}
+component {bf1e01d0-953e-11df-981c-0800200c9a66} nsInputListAutoComplete.js
+contract @mozilla.org/satchel/inputlist-autocomplete;1 {bf1e01d0-953e-11df-981c-0800200c9a66}
+component {3a0012eb-007f-4bb8-aa81-a07385f77a25} FormHistoryStartup.js
+contract @mozilla.org/satchel/form-history-startup;1 {3a0012eb-007f-4bb8-aa81-a07385f77a25}
+category profile-after-change formHistoryStartup @mozilla.org/satchel/form-history-startup;1
+category idle-daily formHistoryStartup @mozilla.org/satchel/form-history-startup;1
diff --git a/toolkit/components/satchel/test/.eslintrc.js b/toolkit/components/satchel/test/.eslintrc.js
new file mode 100644
index 0000000000..3c788d6d68
--- /dev/null
+++ b/toolkit/components/satchel/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/satchel/test/browser/.eslintrc.js b/toolkit/components/satchel/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/satchel/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/satchel/test/browser/browser.ini b/toolkit/components/satchel/test/browser/browser.ini
new file mode 100644
index 0000000000..6a3fc452e3
--- /dev/null
+++ b/toolkit/components/satchel/test/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ !/toolkit/components/satchel/test/subtst_privbrowsing.html
+
+[browser_privbrowsing_perwindowpb.js]
diff --git a/toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.js b/toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.js
new file mode 100644
index 0000000000..982480648f
--- /dev/null
+++ b/toolkit/components/satchel/test/browser/browser_privbrowsing_perwindowpb.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/. */
+
+var FormHistory = (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
+
+/** Test for Bug 472396 **/
+add_task(function* test() {
+ // initialization
+ let windowsToClose = [];
+ let testURI =
+ "http://example.com/tests/toolkit/components/satchel/test/subtst_privbrowsing.html";
+
+ function* doTest(aShouldValueExist, aWindow) {
+ let browser = aWindow.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURI(browser, testURI);
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ // Wait for the page to reload itself.
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ let count = 0;
+ let doneCounting = {};
+ doneCounting.promise = new Promise(resolve => doneCounting.resolve = resolve);
+ FormHistory.count({ fieldname: "field", value: "value" },
+ {
+ handleResult(result) {
+ count = result;
+ },
+ handleError(error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion(num) {
+ if (aShouldValueExist) {
+ is(count, 1, "In non-PB mode, we add a single entry");
+ } else {
+ is(count, 0, "In PB mode, we don't add any entries");
+ }
+
+ doneCounting.resolve();
+ }
+ });
+ yield doneCounting.promise;
+ }
+
+ function testOnWindow(aOptions, aCallback) {
+ return BrowserTestUtils.openNewBrowserWindow(aOptions)
+ .then(win => { windowsToClose.push(win); return win; });
+ }
+
+
+ yield testOnWindow({private: true}).then((aWin) => {
+ return Task.spawn(doTest(false, aWin));
+ });
+
+ // Test when not on private mode after visiting a site on private
+ // mode. The form history should not exist.
+ yield testOnWindow({}).then((aWin) => {
+ return Task.spawn(doTest(true, aWin));
+ });
+
+ yield Promise.all(windowsToClose.map(win => BrowserTestUtils.closeWindow(win)));
+});
diff --git a/toolkit/components/satchel/test/mochitest.ini b/toolkit/components/satchel/test/mochitest.ini
new file mode 100644
index 0000000000..5a65baeb6f
--- /dev/null
+++ b/toolkit/components/satchel/test/mochitest.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+skip-if = toolkit == 'android' || os == 'linux' # linux - bug 1022386
+support-files =
+ satchel_common.js
+ subtst_form_submission_1.html
+ subtst_privbrowsing.html
+ parent_utils.js
+
+[test_bug_511615.html]
+[test_bug_787624.html]
+[test_datalist_with_caching.html]
+[test_form_autocomplete.html]
+[test_form_autocomplete_with_list.html]
+[test_form_submission.html]
+[test_form_submission_cap.html]
+[test_form_submission_cap2.html]
+[test_password_autocomplete.html]
+[test_popup_direction.html]
+[test_popup_enter_event.html]
diff --git a/toolkit/components/satchel/test/parent_utils.js b/toolkit/components/satchel/test/parent_utils.js
new file mode 100644
index 0000000000..87738bdb5a
--- /dev/null
+++ b/toolkit/components/satchel/test/parent_utils.js
@@ -0,0 +1,149 @@
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/FormHistory.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/ContentTaskUtils.jsm");
+
+var gAutocompletePopup = Services.ww.activeWindow.
+ document.
+ getElementById("PopupAutoComplete");
+assert.ok(gAutocompletePopup, "Got autocomplete popup");
+
+var ParentUtils = {
+ getMenuEntries() {
+ let entries = [];
+ let numRows = gAutocompletePopup.view.matchCount;
+ for (let i = 0; i < numRows; i++) {
+ entries.push(gAutocompletePopup.view.getValueAt(i));
+ }
+ return entries;
+ },
+
+ cleanUpFormHist() {
+ FormHistory.update({ op: "remove" });
+ },
+
+ updateFormHistory(changes) {
+ let handler = {
+ handleError: function (error) {
+ assert.ok(false, error);
+ sendAsyncMessage("formHistoryUpdated", { ok: false });
+ },
+ handleCompletion: function (reason) {
+ if (!reason)
+ sendAsyncMessage("formHistoryUpdated", { ok: true });
+ },
+ };
+ FormHistory.update(changes, handler);
+ },
+
+ popupshownListener() {
+ let results = this.getMenuEntries();
+ sendAsyncMessage("onpopupshown", { results });
+ },
+
+ countEntries(name, value) {
+ let obj = {};
+ if (name)
+ obj.fieldname = name;
+ if (value)
+ obj.value = value;
+
+ let count = 0;
+ let listener = {
+ handleResult(result) { count = result },
+ handleError(error) {
+ assert.ok(false, error);
+ sendAsyncMessage("entriesCounted", { ok: false });
+ },
+ handleCompletion(reason) {
+ if (!reason) {
+ sendAsyncMessage("entriesCounted", { ok: true, count });
+ }
+ }
+ };
+
+ FormHistory.count(obj, listener);
+ },
+
+ checkRowCount(expectedCount, expectedFirstValue = null) {
+ ContentTaskUtils.waitForCondition(() => {
+ // This may be called before gAutocompletePopup has initialised
+ // which causes it to throw
+ try {
+ return gAutocompletePopup.view.matchCount === expectedCount &&
+ (!expectedFirstValue ||
+ expectedCount <= 1 ||
+ gAutocompletePopup.view.getValueAt(0) === expectedFirstValue);
+ } catch (e) {
+ return false;
+ }
+ }, "Waiting for row count change: " + expectedCount + " First value: " + expectedFirstValue).then(() => {
+ let results = this.getMenuEntries();
+ sendAsyncMessage("gotMenuChange", { results });
+ });
+ },
+
+ checkSelectedIndex(expectedIndex) {
+ ContentTaskUtils.waitForCondition(() => {
+ return gAutocompletePopup.popupOpen &&
+ gAutocompletePopup.selectedIndex === expectedIndex;
+ }, "Checking selected index").then(() => {
+ sendAsyncMessage("gotSelectedIndex");
+ });
+ },
+
+ getPopupState() {
+ sendAsyncMessage("gotPopupState", {
+ open: gAutocompletePopup.popupOpen,
+ selectedIndex: gAutocompletePopup.selectedIndex,
+ direction: gAutocompletePopup.style.direction,
+ });
+ },
+
+ observe(subject, topic, data) {
+ assert.ok(topic === "satchel-storage-changed");
+ sendAsyncMessage("satchel-storage-changed", { subject: null, topic, data });
+ },
+
+ cleanup() {
+ gAutocompletePopup.removeEventListener("popupshown", this._popupshownListener);
+ this.cleanUpFormHist();
+ }
+};
+
+ParentUtils._popupshownListener =
+ ParentUtils.popupshownListener.bind(ParentUtils);
+gAutocompletePopup.addEventListener("popupshown", ParentUtils._popupshownListener);
+ParentUtils.cleanUpFormHist();
+
+addMessageListener("updateFormHistory", (msg) => {
+ ParentUtils.updateFormHistory(msg.changes);
+});
+
+addMessageListener("countEntries", ({ name, value }) => {
+ ParentUtils.countEntries(name, value);
+});
+
+addMessageListener("waitForMenuChange", ({ expectedCount, expectedFirstValue }) => {
+ ParentUtils.checkRowCount(expectedCount, expectedFirstValue);
+});
+
+addMessageListener("waitForSelectedIndex", ({ expectedIndex }) => {
+ ParentUtils.checkSelectedIndex(expectedIndex);
+});
+
+addMessageListener("getPopupState", () => {
+ ParentUtils.getPopupState();
+});
+
+addMessageListener("addObserver", () => {
+ Services.obs.addObserver(ParentUtils, "satchel-storage-changed", false);
+});
+addMessageListener("removeObserver", () => {
+ Services.obs.removeObserver(ParentUtils, "satchel-storage-changed");
+});
+
+addMessageListener("cleanup", () => {
+ ParentUtils.cleanup();
+});
diff --git a/toolkit/components/satchel/test/satchel_common.js b/toolkit/components/satchel/test/satchel_common.js
new file mode 100644
index 0000000000..c047f40af2
--- /dev/null
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 gPopupShownExpected = false;
+var gPopupShownListener;
+var gLastAutoCompleteResults;
+var gChromeScript;
+
+/*
+ * Returns the element with the specified |name| attribute.
+ */
+function $_(formNum, name) {
+ var form = document.getElementById("form" + formNum);
+ if (!form) {
+ ok(false, "$_ couldn't find requested form " + formNum);
+ return null;
+ }
+
+ var element = form.elements.namedItem(name);
+ if (!element) {
+ ok(false, "$_ couldn't find requested element " + name);
+ return null;
+ }
+
+ // Note that namedItem is a bit stupid, and will prefer an
+ // |id| attribute over a |name| attribute when looking for
+ // the element.
+
+ if (element.hasAttribute("name") && element.getAttribute("name") != name) {
+ ok(false, "$_ got confused.");
+ return null;
+ }
+
+ return element;
+}
+
+// Mochitest gives us a sendKey(), but it's targeted to a specific element.
+// This basically sends an untargeted key event, to whatever's focused.
+function doKey(aKey, modifier) {
+ var keyName = "DOM_VK_" + aKey.toUpperCase();
+ var key = SpecialPowers.Ci.nsIDOMKeyEvent[keyName];
+
+ // undefined --> null
+ if (!modifier)
+ modifier = null;
+
+ // Window utils for sending fake key events.
+ var wutils = SpecialPowers.getDOMWindowUtils(window);
+
+ if (wutils.sendKeyEvent("keydown", key, 0, modifier)) {
+ wutils.sendKeyEvent("keypress", key, 0, modifier);
+ }
+ wutils.sendKeyEvent("keyup", key, 0, modifier);
+}
+
+function registerPopupShownListener(listener) {
+ if (gPopupShownListener) {
+ ok(false, "got too many popupshownlisteners");
+ return;
+ }
+ gPopupShownListener = listener;
+}
+
+function getMenuEntries() {
+ if (!gLastAutoCompleteResults) {
+ throw new Error("no autocomplete results");
+ }
+
+ var results = gLastAutoCompleteResults;
+ gLastAutoCompleteResults = null;
+ return results;
+}
+
+function checkArrayValues(actualValues, expectedValues, msg) {
+ is(actualValues.length, expectedValues.length, "Checking array values: " + msg);
+ for (var i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], msg + " Checking array entry #" + i);
+}
+
+var checkObserver = {
+ verifyStack: [],
+ callback: null,
+
+ init() {
+ gChromeScript.sendAsyncMessage("addObserver");
+ gChromeScript.addMessageListener("satchel-storage-changed", this.observe.bind(this));
+ },
+
+ uninit() {
+ gChromeScript.sendAsyncMessage("removeObserver");
+ },
+
+ waitForChecks: function(callback) {
+ if (this.verifyStack.length == 0)
+ callback();
+ else
+ this.callback = callback;
+ },
+
+ observe: function({ subject, topic, data }) {
+ if (data != "formhistory-add" && data != "formhistory-update")
+ return;
+ ok(this.verifyStack.length > 0, "checking if saved form data was expected");
+
+ // Make sure that every piece of data we expect to be saved is saved, and no
+ // more. Here it is assumed that for every entry satchel saves or modifies, a
+ // message is sent.
+ //
+ // We don't actually check the content of the message, but just that the right
+ // quantity of messages is received.
+ // - if there are too few messages, test will time out
+ // - if there are too many messages, test will error out here
+ //
+ var expected = this.verifyStack.shift();
+
+ countEntries(expected.name, expected.value,
+ function(num) {
+ ok(num > 0, expected.message);
+ if (checkObserver.verifyStack.length == 0) {
+ var callback = checkObserver.callback;
+ checkObserver.callback = null;
+ callback();
+ }
+ });
+ }
+};
+
+function checkForSave(name, value, message) {
+ checkObserver.verifyStack.push({ name : name, value: value, message: message });
+}
+
+function getFormSubmitButton(formNum) {
+ var form = $("form" + formNum); // by id, not name
+ ok(form != null, "getting form " + formNum);
+
+ // we can't just call form.submit(), because that doesn't seem to
+ // invoke the form onsubmit handler.
+ var button = form.firstChild;
+ while (button && button.type != "submit") { button = button.nextSibling; }
+ ok(button != null, "getting form submit button");
+
+ return button;
+}
+
+// Count the number of entries with the given name and value, and call then(number)
+// when done. If name or value is null, then the value of that field does not matter.
+function countEntries(name, value, then = null) {
+ return new Promise(resolve => {
+ gChromeScript.sendAsyncMessage("countEntries", { name, value });
+ gChromeScript.addMessageListener("entriesCounted", function counted(data) {
+ gChromeScript.removeMessageListener("entriesCounted", counted);
+ if (!data.ok) {
+ ok(false, "Error occurred counting form history");
+ SimpleTest.finish();
+ return;
+ }
+
+ if (then) {
+ then(data.count);
+ }
+ resolve(data.count);
+ });
+ });
+}
+
+// Wrapper around FormHistory.update which handles errors. Calls then() when done.
+function updateFormHistory(changes, then = null) {
+ return new Promise(resolve => {
+ gChromeScript.sendAsyncMessage("updateFormHistory", { changes });
+ gChromeScript.addMessageListener("formHistoryUpdated", function updated({ ok }) {
+ gChromeScript.removeMessageListener("formHistoryUpdated", updated);
+ if (!ok) {
+ ok(false, "Error occurred updating form history");
+ SimpleTest.finish();
+ return;
+ }
+
+ if (then) {
+ then();
+ }
+ resolve();
+ });
+ });
+}
+
+function notifyMenuChanged(expectedCount, expectedFirstValue, then = null) {
+ return new Promise(resolve => {
+ gChromeScript.sendAsyncMessage("waitForMenuChange",
+ { expectedCount,
+ expectedFirstValue });
+ gChromeScript.addMessageListener("gotMenuChange", function changed({ results }) {
+ gChromeScript.removeMessageListener("gotMenuChange", changed);
+ gLastAutoCompleteResults = results;
+ if (then) {
+ then(results);
+ }
+ resolve(results);
+ });
+ });
+}
+
+function notifySelectedIndex(expectedIndex, then = null) {
+ return new Promise(resolve => {
+ gChromeScript.sendAsyncMessage("waitForSelectedIndex", { expectedIndex });
+ gChromeScript.addMessageListener("gotSelectedIndex", function changed() {
+ gChromeScript.removeMessageListener("gotSelectedIndex", changed);
+ if (then) {
+ then();
+ }
+ resolve();
+ });
+ });
+}
+
+function getPopupState(then = null) {
+ return new Promise(resolve => {
+ gChromeScript.sendAsyncMessage("getPopupState");
+ gChromeScript.addMessageListener("gotPopupState", function listener(state) {
+ gChromeScript.removeMessageListener("gotPopupState", listener);
+ if (then) {
+ then(state);
+ }
+ resolve(state);
+ });
+ });
+}
+
+function listenForUnexpectedPopupShown() {
+ gPopupShownListener = function onPopupShown() {
+ if (!gPopupShownExpected) {
+ ok(false, "Unexpected autocomplete popupshown event");
+ }
+ };
+}
+
+function* promiseNoUnexpectedPopupShown() {
+ gPopupShownExpected = false;
+ listenForUnexpectedPopupShown();
+ SimpleTest.requestFlakyTimeout("Giving a chance for an unexpected popupshown to occur");
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+}
+
+/**
+ * Resolve at the next popupshown event for the autocomplete popup
+ * @return {Promise} with the results
+ */
+function promiseACShown() {
+ gPopupShownExpected = true;
+ return new Promise(resolve => {
+ gPopupShownListener = ({ results }) => {
+ gPopupShownExpected = false;
+ resolve(results);
+ };
+ });
+}
+
+function satchelCommonSetup() {
+ var chromeURL = SimpleTest.getTestFileURL("parent_utils.js");
+ gChromeScript = SpecialPowers.loadChromeScript(chromeURL);
+ gChromeScript.addMessageListener("onpopupshown", ({ results }) => {
+ gLastAutoCompleteResults = results;
+ if (gPopupShownListener)
+ gPopupShownListener({results});
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ gChromeScript.sendAsyncMessage("cleanup");
+ gChromeScript.destroy();
+ });
+}
+
+
+satchelCommonSetup();
diff --git a/toolkit/components/satchel/test/subtst_form_submission_1.html b/toolkit/components/satchel/test/subtst_form_submission_1.html
new file mode 100644
index 0000000000..f7441668a1
--- /dev/null
+++ b/toolkit/components/satchel/test/subtst_form_submission_1.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+</head>
+
+<body>
+
+<form id="subform1" onsubmit="return checkSubmit(21)">
+ <input id="subtest1" type="text" name="subtest1">
+ <button type="submit">Submit</button>
+</form>
+
+<form id="subform2" onsubmit="return checkSubmit(100)">
+ <input id="subtest2" type="text" name="subtest2">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+ function checkSubmit(num) {
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+ return parent.checkSubmit(num);
+ }
+
+ function clickButton(num) {
+ if (num == 21)
+ document.querySelectorAll("button")[0].click();
+ else if (num == 100)
+ document.querySelectorAll("button")[1].click();
+ }
+
+ // set the input's value (can't use a default value, as satchel will ignore it)
+ document.getElementById("subtest1").value = "subtestValue";
+ document.getElementById("subtest2").value = "subtestValue";
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/subtst_privbrowsing.html b/toolkit/components/satchel/test/subtst_privbrowsing.html
new file mode 100644
index 0000000000..b53e0b2291
--- /dev/null
+++ b/toolkit/components/satchel/test/subtst_privbrowsing.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+ <meta charset=UTF-8>
+ <title>Subtest for bug 472396</title>
+ <script>
+ function submitForm() {
+ if (location.search.indexOf("field") == -1) {
+ var form = document.getElementById("form");
+ var field = document.getElementById("field");
+ field.value = "value";
+ form.submit();
+ }
+ }
+ </script>
+</head>
+<body onload="submitForm();">
+ <h2>Subtest for bug 472396</h2>
+ <form id="form">
+ <input name="field" id="field">
+ </form>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_bug_511615.html b/toolkit/components/satchel/test/test_bug_511615.html
new file mode 100644
index 0000000000..66972d9b30
--- /dev/null
+++ b/toolkit/components/satchel/test/test_bug_511615.html
@@ -0,0 +1,194 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Form History Autocomplete Untrusted Events: Bug 511615</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Test for Form History Autocomplete Untrusted Events: Bug 511615
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <!-- normal, basic form -->
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var resolvePopupShownListener;
+registerPopupShownListener(() => resolvePopupShownListener());
+
+function waitForNextPopup() {
+ return new Promise(resolve => { resolvePopupShownListener = resolve; });
+}
+
+/**
+ * Indicates the time to wait before checking that the state of the autocomplete
+ * popup, including whether it is open, has not changed in response to events.
+ *
+ * Manual testing on a fast machine revealed that 80ms was still unreliable,
+ * while 100ms detected a simulated failure reliably. Unfortunately, this means
+ * that to take into account slower machines we should use a larger value.
+ *
+ * Note that if a machine takes more than this time to show the popup, this
+ * would not cause a failure, conversely the machine would not be able to detect
+ * whether the test should have failed. In other words, this use of timeouts is
+ * never expected to cause intermittent failures with test automation.
+ */
+const POPUP_RESPONSE_WAIT_TIME_MS = 200;
+
+SimpleTest.requestFlakyTimeout("Must ensure that an event does not happen.");
+
+/**
+ * Checks that the popup does not open in response to the given function.
+ */
+function expectPopupDoesNotOpen(triggerFn) {
+ let popupShown = waitForNextPopup();
+ triggerFn();
+ return Promise.race([
+ popupShown.then(() => Promise.reject("Popup opened unexpectedly.")),
+ new Promise(resolve => setTimeout(resolve, POPUP_RESPONSE_WAIT_TIME_MS)),
+ ]);
+}
+
+/**
+ * Checks that the selected index in the popup still matches the given value.
+ */
+function checkSelectedIndexAfterResponseTime(expectedIndex) {
+ return new Promise(resolve => {
+ setTimeout(() => getPopupState(resolve), POPUP_RESPONSE_WAIT_TIME_MS);
+ }).then(popupState => {
+ is(popupState.open, true, "Popup should still be open.");
+ is(popupState.selectedIndex, expectedIndex, "Selected index should match.");
+ });
+}
+
+function doKeyUnprivileged(key) {
+ let keyName = "DOM_VK_" + key.toUpperCase();
+ let keycode, charcode;
+
+ if (key.length == 1) {
+ keycode = 0;
+ charcode = key.charCodeAt(0);
+ alwaysval = charcode;
+ } else {
+ keycode = KeyEvent[keyName];
+ if (!keycode)
+ throw "invalid keyname in test";
+ charcode = 0;
+ alwaysval = keycode;
+ }
+
+ let dnEvent = document.createEvent('KeyboardEvent');
+ let prEvent = document.createEvent('KeyboardEvent');
+ let upEvent = document.createEvent('KeyboardEvent');
+
+ dnEvent.initKeyEvent("keydown", true, true, null, false, false, false, false, alwaysval, 0);
+ prEvent.initKeyEvent("keypress", true, true, null, false, false, false, false, keycode, charcode);
+ upEvent.initKeyEvent("keyup", true, true, null, false, false, false, false, alwaysval, 0);
+
+ input.dispatchEvent(dnEvent);
+ input.dispatchEvent(prEvent);
+ input.dispatchEvent(upEvent);
+}
+
+function doClickWithMouseEventUnprivileged() {
+ let dnEvent = document.createEvent('MouseEvent');
+ let upEvent = document.createEvent('MouseEvent');
+ let ckEvent = document.createEvent('MouseEvent');
+
+ dnEvent.initMouseEvent("mousedown", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+ upEvent.initMouseEvent("mouseup", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+ ckEvent.initMouseEvent("mouseclick", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+
+ input.dispatchEvent(dnEvent);
+ input.dispatchEvent(upEvent);
+ input.dispatchEvent(ckEvent);
+}
+
+let input = $_(1, "field1");
+
+add_task(function* test_initialize() {
+ yield new Promise(resolve => updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "value1" },
+ { op : "add", fieldname : "field1", value : "value2" },
+ { op : "add", fieldname : "field1", value : "value3" },
+ { op : "add", fieldname : "field1", value : "value4" },
+ { op : "add", fieldname : "field1", value : "value5" },
+ { op : "add", fieldname : "field1", value : "value6" },
+ { op : "add", fieldname : "field1", value : "value7" },
+ { op : "add", fieldname : "field1", value : "value8" },
+ { op : "add", fieldname : "field1", value : "value9" },
+ ], resolve));
+});
+
+add_task(function* test_untrusted_events_ignored() {
+ // The autocomplete popup should not open from untrusted events.
+ for (let triggerFn of [
+ () => input.focus(),
+ () => input.click(),
+ () => doClickWithMouseEventUnprivileged(),
+ () => doKeyUnprivileged("down"),
+ () => doKeyUnprivileged("page_down"),
+ () => doKeyUnprivileged("return"),
+ () => doKeyUnprivileged("v"),
+ () => doKeyUnprivileged(" "),
+ () => doKeyUnprivileged("back_space"),
+ ]) {
+ // We must wait for the entire timeout for each individual test, because the
+ // next event in the list might prevent the popup from opening.
+ yield expectPopupDoesNotOpen(triggerFn);
+ }
+
+ // A privileged key press will actually open the popup.
+ let popupShown = waitForNextPopup();
+ doKey("down");
+ yield popupShown;
+
+ // The selected autocomplete item should not change from untrusted events.
+ for (let triggerFn of [
+ () => doKeyUnprivileged("down"),
+ () => doKeyUnprivileged("page_down"),
+ ]) {
+ triggerFn();
+ yield checkSelectedIndexAfterResponseTime(-1);
+ }
+
+ // A privileged key press will actually change the selected index.
+ let indexChanged = new Promise(resolve => notifySelectedIndex(0, resolve));
+ doKey("down");
+ yield indexChanged;
+
+ // The selected autocomplete item should not change and it should not be
+ // possible to use it from untrusted events.
+ for (let triggerFn of [
+ () => doKeyUnprivileged("down"),
+ () => doKeyUnprivileged("page_down"),
+ () => doKeyUnprivileged("right"),
+ () => doKeyUnprivileged(" "),
+ () => doKeyUnprivileged("back_space"),
+ () => doKeyUnprivileged("back_space"),
+ () => doKeyUnprivileged("return"),
+ ]) {
+ triggerFn();
+ yield checkSelectedIndexAfterResponseTime(0);
+ is(input.value, "", "The selected item should not have been used.");
+ }
+
+ // Close the popup.
+ input.blur();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_bug_787624.html b/toolkit/components/satchel/test/test_bug_787624.html
new file mode 100644
index 0000000000..6ca5136cd8
--- /dev/null
+++ b/toolkit/components/satchel/test/test_bug_787624.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Layout of Form History Autocomplete: Bug 787624</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <style>
+ .container {
+ border: 1px solid #333;
+ width: 80px;
+ height: 26px;
+ position: absolute;
+ z-index: 2;
+ }
+
+ .subcontainer {
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .subcontainer input {
+ width: 120px;
+ margin: 2px 6px;
+ padding-right: 4px;
+ border: none;
+ height: 22px;
+ z-index: 1;
+ outline: 1px dashed #555
+ }
+ </style>
+</head>
+<body>
+Form History Layout test: form field autocomplete: Bug 787624
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <!-- in this form, the input field is partially hidden and can scroll -->
+ <div class="container">
+ <div class="subcontainer">
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+ </div>
+ </div>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Form History autocomplete Layout: Bug 787624 **/
+
+var resolvePopupShownListener;
+registerPopupShownListener(() => resolvePopupShownListener());
+
+function waitForNextPopup() {
+ return new Promise(resolve => { resolvePopupShownListener = resolve; });
+}
+
+add_task(function* test_popup_not_move_input() {
+ var input = $_(1, "field1");
+ var rect = input.getBoundingClientRect();
+
+ yield new Promise(resolve => updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "value1" },
+ { op : "add", fieldname : "field1", value : "value2" },
+ ], resolve));
+
+ let popupShown = waitForNextPopup();
+ input.focus();
+ doKey("down");
+ yield popupShown;
+
+ var newRect = input.getBoundingClientRect();
+ is(newRect.left, rect.left,
+ "autocomplete popup does not disturb the input position");
+ is(newRect.top, rect.top,
+ "autocomplete popup does not disturb the input position");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_datalist_with_caching.html b/toolkit/components/satchel/test/test_datalist_with_caching.html
new file mode 100644
index 0000000000..8445cb1591
--- /dev/null
+++ b/toolkit/components/satchel/test/test_datalist_with_caching.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Form History Autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form History test: form field autocomplete
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- normal, basic form -->
+ <form id="form1" onsubmit="return false;">
+ <input list="suggest" type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <datalist id="suggest">
+ <option value="First"></option>
+ <option value="Second"></option>
+ <option value="Secomundo"></option>
+ </datalist>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var input = $_(1, "field1");
+
+function setupFormHistory(aCallback) {
+ updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "Sec" },
+ ], () => {
+ spawn_task(aCallback);
+ });
+}
+
+function setForm(value) {
+ input.value = value;
+ input.focus();
+}
+
+// Restore the form to the default state.
+function restoreForm() {
+ setForm("");
+}
+
+// Check for expected form data.
+function checkForm(expectedValue) {
+ var formID = input.parentNode.id;
+ is(input.value, expectedValue, "Checking " + formID + " input");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var expectingPopup = null;
+
+function expectPopup() {
+ info("expecting a popup");
+ return new Promise(resolve => {
+ expectingPopup = resolve;
+ });
+}
+
+var testNum = 0;
+
+function popupShownListener() {
+ info("popup shown for test " + testNum);
+ if (expectingPopup) {
+ expectingPopup();
+ expectingPopup = null;
+ }
+ else {
+ ok(false, "Autocomplete popup not expected during test " + testNum);
+ }
+}
+
+function waitForMenuChange(expectedCount) {
+ return new Promise(resolve => {
+ notifyMenuChanged(expectedCount, null, resolve);
+ });
+}
+
+registerPopupShownListener(popupShownListener);
+
+function checkMenuEntries(expectedValues) {
+ var actualValues = getMenuEntries();
+ is(actualValues.length, expectedValues.length, testNum + " Checking length of expected menu");
+ for (var i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], testNum + " Checking menu entry #"+i);
+}
+
+function* runTests() {
+ testNum++;
+ restoreForm();
+ doKey("down");
+ yield expectPopup();
+
+ checkMenuEntries(["Sec", "First", "Second", "Secomundo"]);
+ doKey("down");
+ doKey("return");
+ checkForm("Sec");
+
+ testNum++;
+ restoreForm();
+ sendString("Sec");
+ doKey("down");
+ yield expectPopup();
+
+ testNum++;
+ checkMenuEntries(["Sec", "Second", "Secomundo"]);
+ sendString("o");
+ yield waitForMenuChange(2);
+
+ testNum++;
+ checkMenuEntries(["Second", "Secomundo"]);
+ doKey("down");
+ doKey("return");
+ checkForm("Second");
+ SimpleTest.finish();
+}
+
+function startTest() {
+ setupFormHistory(runTests);
+}
+
+window.onload = startTest;
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_form_autocomplete.html b/toolkit/components/satchel/test/test_form_autocomplete.html
new file mode 100644
index 0000000000..4cf09117a0
--- /dev/null
+++ b/toolkit/components/satchel/test/test_form_autocomplete.html
@@ -0,0 +1,1076 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Form History Autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form History test: form field autocomplete
+<p id="display"></p>
+
+<!-- We presumably can't hide the content for this test. The large top padding is to allow
+ listening for scrolls to occur. -->
+<div id="content" style="padding-top: 20000px;">
+
+ <!-- normal, basic form -->
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- normal, basic form (new fieldname) -->
+ <form id="form2" onsubmit="return false;">
+ <input type="text" name="field2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with autocomplete=off on input -->
+ <form id="form3" onsubmit="return false;">
+ <input type="text" name="field2" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with autocomplete=off on form -->
+ <form id="form4" autocomplete="off" onsubmit="return false;">
+ <input type="text" name="field2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- normal form for testing filtering -->
+ <form id="form5" onsubmit="return false;">
+ <input type="text" name="field3">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- normal form for testing word boundary filtering -->
+ <form id="form6" onsubmit="return false;">
+ <input type="text" name="field4">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with maxlength attribute on input -->
+ <form id="form7" onsubmit="return false;">
+ <input type="text" name="field5" maxlength="10">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='email' -->
+ <form id="form8" onsubmit="return false;">
+ <input type="email" name="field6">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='tel' -->
+ <form id="form9" onsubmit="return false;">
+ <input type="tel" name="field7">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='url' -->
+ <form id="form10" onsubmit="return false;">
+ <input type="url" name="field8">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='search' -->
+ <form id="form11" onsubmit="return false;">
+ <input type="search" name="field9">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='number' -->
+ <form id="form12" onsubmit="return false;">
+ <input type="text" name="field10"> <!-- TODO: change back to type=number -->
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- normal, basic form (with fieldname='searchbar-history') -->
+ <form id="form13" onsubmit="return false;">
+ <input type="text" name="searchbar-history">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='date' -->
+ <form id="form14" onsubmit="return false;">
+ <input type="date" name="field11">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='time' -->
+ <form id="form15" onsubmit="return false;">
+ <input type="time" name="field12">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='range' -->
+ <form id="form16" onsubmit="return false;">
+ <input type="range" name="field13" max="64">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='color' -->
+ <form id="form17" onsubmit="return false;">
+ <input type="color" name="field14">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='month' -->
+ <form id="form18" onsubmit="return false;">
+ <input type="month" name="field15">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='week' -->
+ <form id="form19" onsubmit="return false;">
+ <input type="week" name="field16">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with input type='datetime-local' -->
+ <form id="form20" onsubmit="return false;">
+ <input type="datetime-local" name="field17">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Form History autocomplete **/
+
+var input = $_(1, "field1");
+const shiftModifier = Event.SHIFT_MASK;
+
+function setupFormHistory(aCallback) {
+ updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "value1" },
+ { op : "add", fieldname : "field1", value : "value2" },
+ { op : "add", fieldname : "field1", value : "value3" },
+ { op : "add", fieldname : "field1", value : "value4" },
+ { op : "add", fieldname : "field2", value : "value1" },
+ { op : "add", fieldname : "field3", value : "a" },
+ { op : "add", fieldname : "field3", value : "aa" },
+ { op : "add", fieldname : "field3", value : "aaz" },
+ { op : "add", fieldname : "field3", value : "aa\xe6" }, // 0xae == latin ae pair (0xc6 == AE)
+ { op : "add", fieldname : "field3", value : "az" },
+ { op : "add", fieldname : "field3", value : "z" },
+ { op : "add", fieldname : "field4", value : "a\xe6" },
+ { op : "add", fieldname : "field4", value : "aa a\xe6" },
+ { op : "add", fieldname : "field4", value : "aba\xe6" },
+ { op : "add", fieldname : "field4", value : "bc d\xe6" },
+ { op : "add", fieldname : "field5", value : "1" },
+ { op : "add", fieldname : "field5", value : "12" },
+ { op : "add", fieldname : "field5", value : "123" },
+ { op : "add", fieldname : "field5", value : "1234" },
+ { op : "add", fieldname : "field6", value : "value" },
+ { op : "add", fieldname : "field7", value : "value" },
+ { op : "add", fieldname : "field8", value : "value" },
+ { op : "add", fieldname : "field9", value : "value" },
+ { op : "add", fieldname : "field10", value : "42" },
+ { op : "add", fieldname : "field11", value : "2010-10-10" },
+ { op : "add", fieldname : "field12", value : "21:21" }, // not used, since type=time doesn't have autocomplete currently
+ { op : "add", fieldname : "field13", value : "32" }, // not used, since type=range doesn't have a drop down menu
+ { op : "add", fieldname : "field14", value : "#ffffff" }, // not used, since type=color doesn't have autocomplete currently
+ { op : "add", fieldname : "field15", value : "2016-08" },
+ { op : "add", fieldname : "field16", value : "2016-W32" },
+ { op : "add", fieldname : "field17", value : "2016-10-21T10:10" },
+ { op : "add", fieldname : "searchbar-history", value : "blacklist test" },
+ ], aCallback);
+}
+
+function setForm(value) {
+ input.value = value;
+ input.focus();
+}
+
+// Restore the form to the default state.
+function restoreForm() {
+ setForm("");
+}
+
+// Check for expected form data.
+function checkForm(expectedValue) {
+ var formID = input.parentNode.id;
+ is(input.value, expectedValue, "Checking " + formID + " input");
+}
+
+var testNum = 0;
+var expectingPopup = false;
+
+function expectPopup()
+{
+ info("expecting popup for test " + testNum);
+ expectingPopup = true;
+}
+
+function popupShownListener()
+{
+ info("popup shown for test " + testNum);
+ if (expectingPopup) {
+ expectingPopup = false;
+ SimpleTest.executeSoon(runTest);
+ }
+ else {
+ ok(false, "Autocomplete popup not expected during test " + testNum);
+ }
+}
+
+registerPopupShownListener(popupShownListener);
+
+/*
+ * Main section of test...
+ *
+ * This is a bit hacky, as many operations happen asynchronously.
+ * Various mechanisms call runTests as a result of operations:
+ * - set expectingPopup to true, and the next test will occur when the autocomplete popup is shown
+ * - call waitForMenuChange(x) to run the next test when the autocomplete popup to have x items in it
+ * - addEntry calls runs the test when an entry has been added
+ * - some tests scroll the window. This is because the form fill controller happens to scroll
+ * the field into view near the end of the search, and there isn't any other good notification
+ * to listen to for when the search is complete.
+ * - some items still use setTimeout
+ */
+function runTest() {
+ testNum++;
+
+ ok(true, "Starting test #" + testNum);
+
+ switch (testNum) {
+ case 1:
+ // Make sure initial form is empty.
+ checkForm("");
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 2:
+ checkMenuEntries(["value1", "value2", "value3", "value4"], testNum);
+ // Check first entry
+ doKey("down");
+ checkForm(""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ checkForm("value1");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 3:
+ // Check second entry
+ doKey("down");
+ doKey("down");
+ doKey("return"); // not "enter"!
+ checkForm("value2");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 4:
+ // Check third entry
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("value3");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 5:
+ // Check fourth entry
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("value4");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 6:
+ // Check first entry (wraparound)
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("down"); // deselects
+ doKey("down");
+ doKey("return");
+ checkForm("value1");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 7:
+ // Check the last entry via arrow-up
+ doKey("up");
+ doKey("return");
+ checkForm("value4");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 8:
+ // Check the last entry via arrow-up
+ doKey("down"); // select first entry
+ doKey("up"); // selects nothing!
+ doKey("up"); // select last entry
+ doKey("return");
+ checkForm("value4");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 9:
+ // Check the last entry via arrow-up (wraparound)
+ doKey("down");
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("up");
+ doKey("up");
+ doKey("up"); // first entry
+ doKey("up"); // deselects
+ doKey("up"); // last entry
+ doKey("return");
+ checkForm("value4");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 10:
+ // Set first entry w/o triggering autocomplete
+ doKey("down");
+ doKey("right");
+ checkForm("value1");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 11:
+ // Set first entry w/o triggering autocomplete
+ doKey("down");
+ doKey("left");
+ checkForm("value1");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 12:
+ // Check first entry (page up)
+ doKey("down");
+ doKey("down");
+ doKey("page_up");
+ doKey("return");
+ checkForm("value1");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 13:
+ // Check last entry (page down)
+ doKey("down");
+ doKey("page_down");
+ doKey("return");
+ checkForm("value4");
+
+ // Trigger autocomplete popup
+ testNum = 49;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ /* Test removing entries from the dropdown */
+
+ case 50:
+ checkMenuEntries(["value1", "value2", "value3", "value4"], testNum);
+ // Delete the first entry (of 4)
+ setForm("value");
+ doKey("down");
+
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ if (SpecialPowers.OS == "Darwin")
+ doKey("back_space", shiftModifier);
+ else
+ doKey("delete", shiftModifier);
+
+ // This tests that on OS X shift-backspace didn't delete the last character
+ // in the input (bug 480262).
+ waitForMenuChange(3);
+ break;
+
+ case 51:
+ checkForm("value");
+ countEntries("field1", "value1",
+ function (num) {
+ ok(!num, testNum + " checking that f1/v1 was deleted");
+ runTest();
+ });
+ break;
+
+ case 52:
+ doKey("return");
+ checkForm("value2");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 53:
+ checkMenuEntries(["value2", "value3", "value4"], testNum);
+ // Check the new first entry (of 3)
+ doKey("down");
+ doKey("return");
+ checkForm("value2");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 54:
+ // Delete the second entry (of 3)
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ waitForMenuChange(2);
+ break;
+
+ case 55:
+ checkForm("");
+ countEntries("field1", "value3",
+ function (num) {
+ ok(!num, testNum + " checking that f1/v3 was deleted");
+ runTest();
+ });
+ break;
+
+ case 56:
+ doKey("return");
+ checkForm("value4")
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 57:
+ checkMenuEntries(["value2", "value4"], testNum);
+ // Check the new first entry (of 2)
+ doKey("down");
+ doKey("return");
+ checkForm("value2");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 58:
+ // Delete the last entry (of 2)
+ doKey("down");
+ doKey("down");
+ doKey("delete", shiftModifier);
+ checkForm("");
+ waitForMenuChange(1);
+ break;
+
+ case 59:
+ countEntries("field1", "value4",
+ function (num) {
+ ok(!num, testNum + " checking that f1/v4 was deleted");
+ runTest();
+ });
+ break;
+
+ case 60:
+ doKey("return");
+ checkForm("value2");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 61:
+ checkMenuEntries(["value2"], testNum);
+ // Check the new first entry (of 1)
+ doKey("down");
+ doKey("return");
+ checkForm("value2");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 62:
+ // Delete the only remaining entry
+ doKey("down");
+ doKey("delete", shiftModifier);
+ waitForMenuChange(0);
+ break;
+
+ case 63:
+ checkForm("");
+ countEntries("field1", "value2",
+ function (num) {
+ ok(!num, testNum + " checking that f1/v2 was deleted");
+ runTest();
+ });
+ break;
+
+ case 64:
+ // Look at form 2, trigger autocomplete popup
+ input = $_(2, "field2");
+ testNum = 99;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ /* Test entries with autocomplete=off */
+
+ case 100:
+ // Select first entry
+ doKey("down");
+ doKey("return");
+ checkForm("value1");
+
+ // Look at form 3, try to trigger autocomplete popup
+ input = $_(3, "field2");
+ restoreForm();
+ // Sometimes, this will fail if scrollTo(0, 0) is called, so that doesn't
+ // happen here. Fortunately, a different input is used from the last test,
+ // so a scroll should still occur.
+ doKey("down");
+ waitForScroll();
+ break;
+
+ case 101:
+ // Ensure there's no autocomplete dropdown (autocomplete=off is present)
+ doKey("down");
+ doKey("return");
+ checkForm("");
+
+ // Look at form 4, try to trigger autocomplete popup
+ input = $_(4, "field2");
+ restoreForm();
+ doKey("down");
+ waitForMenuChange(0);
+ break;
+
+ case 102:
+ // Ensure there's no autocomplete dropdown (autocomplete=off is present)
+ doKey("down");
+ doKey("return");
+ checkForm("");
+
+ // Look at form 5, try to trigger autocomplete popup
+ input = $_(5, "field3");
+ restoreForm();
+ testNum = 199;
+ expectPopup();
+ input.focus();
+ sendChar("a");
+ break;
+
+ /* Test filtering as characters are typed. */
+
+ case 200:
+ checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"], testNum);
+ input.focus();
+ sendChar("a");
+ waitForMenuChange(3);
+ break;
+
+ case 201:
+ checkMenuEntries(["aa", "aaz", "aa\xe6"], testNum);
+ input.focus();
+ sendChar("\xc6");
+ waitForMenuChange(1);
+ break;
+
+ case 202:
+ checkMenuEntries(["aa\xe6"], testNum);
+ doKey("back_space");
+ waitForMenuChange(3);
+ break;
+
+ case 203:
+ checkMenuEntries(["aa", "aaz", "aa\xe6"], testNum);
+ doKey("back_space");
+ waitForMenuChange(5);
+ break;
+
+ case 204:
+ checkMenuEntries(["a", "aa", "aaz", "aa\xe6", "az"], testNum);
+ input.focus();
+ sendChar("z");
+ waitForMenuChange(2);
+ break;
+
+ case 205:
+ checkMenuEntries(["az", "aaz"], testNum);
+ input.focus();
+ doKey("left");
+ expectPopup();
+ // Check case-insensitivity.
+ sendChar("A");
+ break;
+
+ case 206:
+ checkMenuEntries(["aaz"], testNum);
+ addEntry("field3", "aazq");
+ break;
+
+ case 207:
+ // check that results were cached
+ input.focus();
+ doKey("right");
+ sendChar("q");
+ waitForMenuChange(0);
+ break;
+
+ case 208:
+ // check that results were cached
+ checkMenuEntries([], testNum);
+ addEntry("field3", "aazqq");
+ break;
+
+ case 209:
+ input.focus();
+ window.scrollTo(0, 0);
+ sendChar("q");
+ waitForMenuChange(0);
+ break;
+
+ case 210:
+ // check that empty results were cached - bug 496466
+ checkMenuEntries([], testNum);
+ doKey("escape");
+
+ // Look at form 6, try to trigger autocomplete popup
+ input = $_(6, "field4");
+ restoreForm();
+ testNum = 249;
+ expectPopup();
+ input.focus();
+ sendChar("a");
+ break;
+
+ /* Test substring matches and word boundary bonuses */
+
+ case 250:
+ // alphabetical results for first character
+ checkMenuEntries(["aa a\xe6", "aba\xe6", "a\xe6"], testNum);
+ input.focus();
+
+ sendChar("\xe6");
+ waitForMenuChange(3, "a\xe6");
+ break;
+
+ case 251:
+ // prefix match comes first, then word boundary match
+ // followed by substring match
+ checkMenuEntries(["a\xe6", "aa a\xe6", "aba\xe6"], testNum);
+
+ restoreForm();
+ input.focus();
+ sendChar("b");
+ waitForMenuChange(1, "bc d\xe6");
+ break;
+
+ case 252:
+ checkMenuEntries(["bc d\xe6"], testNum);
+ input.focus();
+ sendChar(" ");
+ waitForMenuChange(1);
+ break;
+
+ case 253:
+ // check that trailing space has no effect after single char.
+ checkMenuEntries(["bc d\xe6"], testNum);
+ input.focus();
+ sendChar("\xc6");
+ waitForMenuChange(2);
+ break;
+
+ case 254:
+ // check multi-word substring matches
+ checkMenuEntries(["bc d\xe6", "aba\xe6"]);
+ input.focus();
+ expectPopup();
+ doKey("left");
+ sendChar("d");
+ break;
+
+ case 255:
+ // check inserting in multi-word searches
+ checkMenuEntries(["bc d\xe6"], testNum);
+ input.focus();
+ sendChar("z");
+ waitForMenuChange(0);
+ break;
+
+ case 256:
+ checkMenuEntries([], testNum);
+
+ // Look at form 7, try to trigger autocomplete popup
+ input = $_(7, "field5");
+ testNum = 299;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 300:
+ checkMenuEntries(["1", "12", "123", "1234"], testNum);
+ input.maxLength = 4;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 301:
+ checkMenuEntries(["1", "12", "123", "1234"], testNum);
+ input.maxLength = 3;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 302:
+ checkMenuEntries(["1", "12", "123"], testNum);
+ input.maxLength = 2;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 303:
+ checkMenuEntries(["1", "12"], testNum);
+ input.maxLength = 1;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 304:
+ checkMenuEntries(["1"], testNum);
+ input.maxLength = 0;
+ doKey("escape");
+ doKey("down");
+ waitForMenuChange(0);
+ break;
+
+ case 305:
+ checkMenuEntries([], testNum);
+ input.maxLength = 4;
+
+ // now again with a character typed
+ input.focus();
+ sendChar("1");
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 306:
+ checkMenuEntries(["1", "12", "123", "1234"], testNum);
+ input.maxLength = 3;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 307:
+ checkMenuEntries(["1", "12", "123"], testNum);
+ input.maxLength = 2;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 308:
+ checkMenuEntries(["1", "12"], testNum);
+ input.maxLength = 1;
+ expectPopup();
+ doKey("escape");
+ doKey("down");
+ break;
+
+ case 309:
+ checkMenuEntries(["1"], testNum);
+ input.maxLength = 0;
+ doKey("escape");
+ doKey("down");
+ waitForMenuChange(0);
+ break;
+
+ case 310:
+ checkMenuEntries([], testNum);
+
+ input = $_(8, "field6");
+ testNum = 399;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 400:
+ case 401:
+ case 402:
+ case 403:
+ checkMenuEntries(["value"], testNum);
+ doKey("down");
+ doKey("return");
+ checkForm("value");
+
+ if (testNum == 400) {
+ input = $_(9, "field7");
+ } else if (testNum == 401) {
+ input = $_(10, "field8");
+ } else if (testNum == 402) {
+ input = $_(11, "field9");
+ } else if (testNum == 403) {
+ todo(false, "Fix input type=number");
+ input = $_(12, "field10");
+ }
+
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 404:
+ checkMenuEntries(["42"], testNum);
+ doKey("down");
+ doKey("return");
+ checkForm("42");
+
+ input = $_(14, "field11");
+ restoreForm();
+ expectPopup();
+ doKey("down");
+ break;
+
+ case 405:
+ checkMenuEntries(["2010-10-10"]);
+ doKey("down");
+ doKey("return");
+ checkForm("2010-10-10");
+
+ input = $_(15, "field12");
+ restoreForm();
+ waitForMenuChange(0);
+ break;
+
+ case 406:
+ checkMenuEntries([]); // type=time with it's own control frame does not
+ // have a drop down menu for now
+ checkForm("");
+
+ input = $_(16, "field13");
+ restoreForm();
+ doKey("down");
+ waitForMenuChange(0);
+ break;
+
+ case 407:
+ checkMenuEntries([]); // type=range does not have a drop down menu
+ doKey("down");
+ doKey("return");
+ checkForm("30"); // default (midway between minimum (0) and maximum (64)) - step
+
+ input = $_(17, "field14");
+ restoreForm();
+ waitForMenuChange(0);
+ break;
+
+ case 408:
+ checkMenuEntries([]); // type=color does not have a drop down menu
+ checkForm("#000000"); // default color value
+
+ input = $_(18, "field15");
+ restoreForm();
+ expectPopup();
+ doKey("down");
+ break;
+
+ case 409:
+ checkMenuEntries(["2016-08"]);
+ doKey("down");
+ doKey("return");
+ checkForm("2016-08");
+
+ input = $_(19, "field16");
+ restoreForm();
+ expectPopup();
+ doKey("down");
+ break;
+
+ case 410:
+ checkMenuEntries(["2016-W32"]);
+ doKey("down");
+ doKey("return");
+ checkForm("2016-W32");
+
+ input = $_(20, "field17");
+ restoreForm();
+ expectPopup();
+ doKey("down");
+ break;
+
+ case 411:
+ checkMenuEntries(["2016-10-21T10:10"]);
+ doKey("down");
+ doKey("return");
+ checkForm("2016-10-21T10:10");
+
+ addEntry("field1", "value1");
+ break;
+
+ case 412:
+ input = $_(1, "field1");
+ // Go to test 500.
+ testNum = 499;
+
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ // Check that the input event is fired.
+ case 500:
+ input.addEventListener("input", function(event) {
+ input.removeEventListener("input", arguments.callee, false);
+ ok(true, testNum + " oninput should have been received");
+ ok(event.bubbles, testNum + " input event should bubble");
+ ok(event.cancelable, testNum + " input event should be cancelable");
+ }, false);
+
+ doKey("down");
+ checkForm("");
+ doKey("return");
+ checkForm("value1");
+ testNum = 599;
+ setTimeout(runTest, 100);
+ break;
+
+ case 600:
+ // check we don't show autocomplete for searchbar-history
+ input = $_(13, "searchbar-history");
+
+ // Trigger autocomplete popup
+ checkForm("");
+ restoreForm();
+ doKey("down");
+ waitForMenuChange(0);
+ break;
+
+ case 601:
+ checkMenuEntries([], testNum);
+ input.blur();
+ SimpleTest.finish();
+ return;
+
+ default:
+ ok(false, "Unexpected invocation of test #" + testNum);
+ SimpleTest.finish();
+ return;
+ }
+}
+
+function addEntry(name, value)
+{
+ updateFormHistory({ op : "add", fieldname : name, value: value }, runTest);
+}
+
+// Runs the next test when scroll event occurs
+function waitForScroll()
+{
+ addEventListener("scroll", function() {
+ if (!window.pageYOffset)
+ return;
+
+ removeEventListener("scroll", arguments.callee, false);
+ setTimeout(runTest, 100);
+ }, false);
+}
+
+function waitForMenuChange(expectedCount, expectedFirstValue)
+{
+ notifyMenuChanged(expectedCount, expectedFirstValue, runTest);
+}
+
+function checkMenuEntries(expectedValues, testNumber) {
+ var actualValues = getMenuEntries();
+ is(actualValues.length, expectedValues.length, testNumber + " Checking length of expected menu");
+ for (var i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], testNumber + " Checking menu entry #"+i);
+}
+
+function startTest() {
+ setupFormHistory(function() {
+ runTest();
+ });
+}
+
+window.onload = startTest;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/toolkit/components/satchel/test/test_form_autocomplete_with_list.html b/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
new file mode 100644
index 0000000000..04fb080c93
--- /dev/null
+++ b/toolkit/components/satchel/test/test_form_autocomplete_with_list.html
@@ -0,0 +1,506 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Form History Autocomplete</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form History test: form field autocomplete
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- normal, basic form -->
+ <form id="form1" onsubmit="return false;">
+ <input list="suggest" type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with autocomplete=off on input -->
+ <form id="form3" onsubmit="return false;">
+ <input list="suggest" type="text" name="field2" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form with autocomplete=off on form -->
+ <form id="form4" autocomplete="off" onsubmit="return false;">
+ <input list="suggest" type="text" name="field2">
+ <button type="submit">Submit</button>
+ </form>
+
+ <datalist id="suggest">
+ <option value="Google" label="PASS1">FAIL</option>
+ <option value="Reddit">PASS2</option>
+ <option value="final"></option>
+ </datalist>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Form History autocomplete **/
+
+var input = $_(1, "field1");
+const shiftModifier = Components.interfaces.nsIDOMEvent.SHIFT_MASK;
+
+function setupFormHistory(aCallback) {
+ updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "historyvalue" },
+ { op : "add", fieldname : "field2", value : "othervalue" },
+ ], aCallback);
+}
+
+function setForm(value) {
+ input.value = value;
+ input.focus();
+}
+
+// Restore the form to the default state.
+function restoreForm() {
+ setForm("");
+}
+
+// Check for expected form data.
+function checkForm(expectedValue) {
+ var formID = input.parentNode.id;
+ is(input.value, expectedValue, "Checking " + formID + " input");
+}
+
+var testNum = 0;
+var prevValue;
+var expectingPopup = false;
+
+function expectPopup() {
+ info("expecting popup for test " + testNum);
+ expectingPopup = true;
+}
+
+function popupShownListener() {
+ info("popup shown for test " + testNum);
+ if (expectingPopup) {
+ expectingPopup = false;
+ SimpleTest.executeSoon(runTest);
+ }
+ else {
+ ok(false, "Autocomplete popup not expected during test " + testNum);
+ }
+}
+
+registerPopupShownListener(popupShownListener);
+
+/*
+* Main section of test...
+*
+* This is a bit hacky, as many operations happen asynchronously.
+* Various mechanisms call runTests as a result of operations:
+* - set expectingPopup to true, and the next test will occur when the autocomplete popup is shown
+* - call waitForMenuChange(x) to run the next test when the autocomplete popup to have x items in it
+*/
+function runTest() {
+ testNum++;
+
+ info("Starting test #" + testNum);
+
+ switch (testNum) {
+ case 1:
+ // Make sure initial form is empty.
+ checkForm("");
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+ case 2:
+ checkMenuEntries(["historyvalue", "PASS1", "PASS2", "final"], testNum);
+ // Check first entry
+ doKey("down");
+ checkForm(""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ checkForm("historyvalue");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 3:
+ // Check second entry
+ doKey("down");
+ doKey("down");
+ doKey("return"); // not "enter"!
+ checkForm("Google");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 4:
+ // Check third entry
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("Reddit");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 5:
+ // Check fourth entry
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("final");
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 6:
+ // Delete the first entry (of 3)
+ doKey("down");
+ doKey("delete", shiftModifier);
+ waitForMenuChange(3);
+ break;
+
+ case 7:
+ checkForm("");
+ countEntries("field1", "historyvalue",
+ function (num) {
+ ok(!num, testNum + " checking that form history value was deleted");
+ runTest();
+ });
+ break;
+
+ case 8:
+ doKey("return");
+ checkForm("Google")
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 9:
+ // Test deletion
+ checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
+ // Check the new first entry (of 3)
+ doKey("down");
+ doKey("return");
+ checkForm("Google");
+
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 10:
+ // Test autocompletion of datalists with cached results.
+ sendString("PAS");
+ waitForMenuChange(2);
+ break;
+
+ case 11:
+ // Continuation of test 10
+ sendString("S1");
+ waitForMenuChange(1);
+ break;
+
+ case 12:
+ doKey("down");
+ doKey("return");
+ checkForm("Google");
+
+ // Trigger autocomplete popup
+ // Look at form 3, try to trigger autocomplete popup
+ input.value = "";
+ input = $_(3, "field2");
+ testNum = 99;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 100:
+ checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
+ // Check first entry
+ doKey("down");
+ checkForm(""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ checkForm("Google");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 101:
+ // Check second entry
+ doKey("down");
+ doKey("down");
+ doKey("return"); // not "enter"!
+ checkForm("Reddit");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 102:
+ // Check third entry
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("final");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 103:
+ checkMenuEntries(["PASS1", "PASS2", "final"], testNum);
+ // Check first entry
+ doKey("down");
+ checkForm(""); // value shouldn't update
+ doKey("return"); // not "enter"!
+ checkForm("Google");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 104:
+ // Check second entry
+ doKey("down");
+ doKey("down");
+ doKey("return"); // not "enter"!
+ checkForm("Reddit");
+
+ // Trigger autocomplete popup
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 105:
+ // Check third entry
+ doKey("down");
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("final");
+
+ testNum = 199;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ // Test dynamic updates.
+ // For some reasons, when there is an update of the list, the selection is
+ // lost so we need to go down like if we were at the beginning of the list
+ // again.
+ case 200:
+ // Removing the second element while on the first then going down and
+ // push enter. Value should be one from the third suggesion.
+ doKey("down");
+ var datalist = document.getElementById('suggest');
+ var toRemove = datalist.children[1]
+ datalist.removeChild(toRemove);
+
+ SimpleTest.executeSoon(function() {
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("final");
+
+ // Restore the element.
+ datalist.insertBefore(toRemove, datalist.children[1]);
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ });
+ break;
+
+ case 201:
+ // Adding an attribute after the first one while on the first then going
+ // down and push enter. Value should be the on from the new suggestion.
+ doKey("down");
+ datalist = document.getElementById('suggest');
+ var added = new Option("Foo");
+ datalist.insertBefore(added, datalist.children[1]);
+ waitForMenuChange(4);
+ break;
+
+ case 202:
+ doKey("down");
+ doKey("down");
+ doKey("return");
+ checkForm("Foo");
+
+ // Remove the element.
+ datalist = document.getElementById('suggest');
+ datalist.removeChild(datalist.children[1]);
+ waitForMenuChange(0);
+ break;
+
+ case 203:
+ // Change the first element value attribute.
+ restoreForm();
+ datalist = document.getElementById('suggest');
+ prevValue = datalist.children[0].value;
+ datalist.children[0].value = "foo";
+ expectPopup();
+ break;
+
+ case 204:
+ doKey("down");
+ doKey("return");
+ checkForm("foo");
+
+ datalist = document.getElementById('suggest');
+ datalist.children[0].value = prevValue;
+ waitForMenuChange(0);
+ break;
+
+ case 205:
+ // Change the textContent to update the value attribute.
+ restoreForm();
+ datalist = document.getElementById('suggest');
+ prevValue = datalist.children[0].getAttribute('value');
+ datalist.children[0].removeAttribute('value');
+ datalist.children[0].textContent = "foobar";
+ expectPopup();
+ break;
+
+ case 206:
+ doKey("down");
+ doKey("return");
+ checkForm("foobar");
+
+ datalist = document.getElementById('suggest');
+ datalist.children[0].setAttribute('value', prevValue);
+ testNum = 299;
+ waitForMenuChange(0);
+ break;
+
+ // Tests for filtering (or not).
+ case 300:
+ // Filters with first letter of the word.
+ restoreForm();
+ synthesizeKey("f", {});
+ expectPopup();
+ break;
+
+ case 301:
+ doKey("down");
+ doKey("return");
+ checkForm("final");
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 302:
+ // Filter with a letter in the middle of the word.
+ synthesizeKey("i", {});
+ synthesizeKey("n", {});
+ waitForMenuChange(1);
+ break;
+
+ case 303:
+ // Continuation of test 302.
+ doKey("down");
+ doKey("return");
+ checkForm("final");
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 304:
+ // Filter is disabled with mozNoFilter.
+ input.setAttribute('mozNoFilter', 'true');
+ synthesizeKey("f", {});
+ waitForMenuChange(3); // no change
+ break;
+
+ case 305:
+ // Continuation of test 304.
+ doKey("down");
+ doKey("return");
+ checkForm("Google");
+ input.removeAttribute('mozNoFilter');
+ testNum = 399;
+ expectPopup();
+ restoreForm();
+ doKey("down");
+ break;
+
+ case 400:
+ // Check that the input event is fired.
+ input.addEventListener("input", function(event) {
+ input.removeEventListener("input", arguments.callee, false);
+ ok(true, "oninput should have been received");
+ ok(event.bubbles, "input event should bubble");
+ ok(event.cancelable, "input event should be cancelable");
+ checkForm("Google");
+ input.blur();
+ SimpleTest.finish();
+ }, false);
+
+ doKey("down");
+ checkForm("");
+ doKey("return");
+ break;
+
+ default:
+ ok(false, "Unexpected invocation of test #" + testNum);
+ SimpleTest.finish();
+ return;
+ }
+}
+
+function waitForMenuChange(expectedCount) {
+ notifyMenuChanged(expectedCount, null, runTest);
+}
+
+function checkMenuEntries(expectedValues, testNumber) {
+ var actualValues = getMenuEntries();
+ is(actualValues.length, expectedValues.length, testNumber + " Checking length of expected menu");
+ for (var i = 0; i < expectedValues.length; i++)
+ is(actualValues[i], expectedValues[i], testNumber + " Checking menu entry #"+i);
+}
+
+function startTest() {
+ setupFormHistory(runTest);
+}
+
+window.onload = startTest;
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_form_submission.html b/toolkit/components/satchel/test/test_form_submission.html
new file mode 100644
index 0000000000..ecccabcafd
--- /dev/null
+++ b/toolkit/components/satchel/test/test_form_submission.html
@@ -0,0 +1,537 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Satchel Test for Form Submisstion</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="iframe" src="https://example.com/tests/toolkit/components/satchel/test/subtst_form_submission_1.html"></iframe>
+<div id="content" style="display: none">
+
+ <!-- ===== Things that should not be saved. ===== -->
+
+ <!-- autocomplete=off for input -->
+ <form id="form1" onsubmit="return checkSubmit(1)">
+ <input type="text" name="test1" autocomplete="off">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- autocomplete=off for form -->
+ <form id="form2" onsubmit="return checkSubmit(2)" autocomplete="off">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- don't save type=hidden -->
+ <form id="form3" onsubmit="return checkSubmit(3)">
+ <input type="hidden" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- don't save type=checkbox -->
+ <form id="form4" onsubmit="return checkSubmit(4)">
+ <input type="checkbox" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- Don't save empty values. -->
+ <form id="form5" onsubmit="return checkSubmit(5)">
+ <input type="text" name="test1" value="originalValue">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- Don't save unchanged values. -->
+ <form id="form6" onsubmit="return checkSubmit(6)">
+ <input type="text" name="test1" value="dontSaveThis">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- Don't save unchanged values. (.value not touched) -->
+ <form id="form7" onsubmit="return checkSubmit(7)">
+ <input type="text" name="test1" value="dontSaveThis">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- No field name or ID. -->
+ <form id="form8" onsubmit="return checkSubmit(8)">
+ <input type="text">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- Nothing to save! -->
+ <form id="form9" onsubmit="return checkSubmit(9)">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with name too long (300 chars.) -->
+ <form id="form10" onsubmit="return checkSubmit(10)">
+ <input type="text" name="12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with value too long (300 chars.) -->
+ <form id="form11" onsubmit="return checkSubmit(11)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with value of one space (which should be trimmed) -->
+ <form id="form12" onsubmit="return checkSubmit(12)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password field -->
+ <form id="form13" onsubmit="return checkSubmit(13)">
+ <input type="password" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password field (type changed after pageload) -->
+ <form id="form14" onsubmit="return checkSubmit(14)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with sensitive data (16 digit credit card number) -->
+ <form id="form15" onsubmit="return checkSubmit(15)">
+ <script type="text/javascript">
+ var form = document.getElementById('form15');
+ for (let i = 0; i != 10; i++)
+ {
+ let input = document.createElement('input');
+ input.type = 'text';
+ input.name = 'test' + (i + 1);
+ form.appendChild(input);
+ }
+ </script>
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with sensitive data (15 digit credit card number) -->
+ <form id="form16" onsubmit="return checkSubmit(16)">
+ <script type="text/javascript">
+ form = document.getElementById('form16');
+ for (let i = 0; i != 10; i++)
+ {
+ let input = document.createElement('input');
+ input.type = 'text';
+ input.name = 'test' + (i + 1);
+ form.appendChild(input);
+ }
+ </script>
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with sensitive data (9 digit credit card number) -->
+ <form id="form17" onsubmit="return checkSubmit(17)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with sensitive data (16 digit hyphenated credit card number) -->
+ <form id="form18" onsubmit="return checkSubmit(18)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with sensitive data (15 digit whitespace-separated credit card number) -->
+ <form id="form19" onsubmit="return checkSubmit(19)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form data submitted through HTTPS, when browser.formfill.saveHttpsForms is false -->
+ <form id="form20" action="https://www.example.com/" onsubmit="return checkSubmit(20)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- Form 21 is submitted into an iframe, not declared here. -->
+
+ <!-- Don't save values if the form is invalid. -->
+ <form id="form22" onsubmit="return checkSubmit(22);">
+ <input type='email' name='test1' oninvalid="return checkSubmit(22);">
+ <button type='submit'>Submit</button>
+ </form>
+
+ <!-- Don't save values if the form is invalid. -->
+ <form id="form23" onsubmit="return checkSubmit(23);">
+ <input type='email' value='foo' oninvalid="return checkSubmit(23);">
+ <input type='text' name='test1'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <!-- Don't save values if the input name is 'searchbar-history' -->
+ <form id="form24" onsubmit="return checkSubmit(24);">
+ <input type='text' name='searchbar-history'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <!-- ===== Things that should be saved ===== -->
+
+ <!-- Form 100 is submitted into an iframe, not declared here. -->
+
+ <!-- input with no default value -->
+ <form id="form101" onsubmit="return checkSubmit(101)">
+ <input type="text" name="test1">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with a default value -->
+ <form id="form102" onsubmit="return checkSubmit(102)">
+ <input type="text" name="test2" value="originalValue">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input uses id but not name -->
+ <form id="form103" onsubmit="return checkSubmit(103)">
+ <input type="text" id="test3">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with leading and trailing space -->
+ <form id="form104" onsubmit="return checkSubmit(104)">
+ <input type="text" name="test4">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input with leading and trailing whitespace -->
+ <form id="form105" onsubmit="return checkSubmit(105)">
+ <input type="text" name="test5">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input that looks like sensitive data but doesn't
+ satisfy the requirements (incorrect length) -->
+ <form id="form106" onsubmit="return checkSubmit(106)">
+ <input type="text" name="test6">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input that looks like sensitive data but doesn't
+ satisfy the requirements (Luhn check fails for 16 chars) -->
+ <form id="form107" onsubmit="return checkSubmit(107)">
+ <script type="text/javascript">
+ form = document.getElementById('form107');
+ for (let i = 0; i != 10; i++)
+ {
+ let input = document.createElement('input');
+ input.type = 'text';
+ input.name = 'test7_' + (i + 1);
+ form.appendChild(input);
+ }
+ </script>
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- input that looks like sensitive data but doesn't
+ satisfy the requirements (Luhn check fails for 15 chars) -->
+ <form id="form108" onsubmit="return checkSubmit(108)">
+ <script type="text/javascript">
+ form = document.getElementById('form108');
+ for (let i = 0; i != 10; i++)
+ {
+ let input = document.createElement('input');
+ input.type = 'text';
+ input.name = 'test8_' + (i + 1);
+ form.appendChild(input);
+ }
+ </script>
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form data submitted through HTTPS, when browser.formfill.saveHttpsForms is true -->
+ <form id="form109" action="https://www.example.com/" onsubmit="return checkSubmit(109)">
+ <input type="text" name="test9">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- regular form data, when browser.formfill.saveHttpsForms is false -->
+ <form id="form110" onsubmit="return checkSubmit(110)">
+ <input type="text" name="test10">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var numSubmittedForms = 0;
+
+var ccNumbers = {
+ valid15: [
+ "930771457288760", "474915027480942",
+ "924894781317325", "714816113937185",
+ "790466087343106", "474320195408363",
+ "219211148122351", "633038472250799",
+ "354236732906484", "095347810189325",
+ ],
+ valid16: [
+ "3091269135815020", "5471839082338112",
+ "0580828863575793", "5015290610002932",
+ "9465714503078607", "4302068493801686",
+ "2721398408985465", "6160334316984331",
+ "8643619970075142", "0218246069710785"
+ ],
+ invalid15: [
+ "526931005800649", "724952425140686",
+ "379761391174135", "030551436468583",
+ "947377014076746", "254848023655752",
+ "226871580283345", "708025346034339",
+ "917585839076788", "918632588027666"
+ ],
+ invalid16: [
+ "9946177098017064", "4081194386488872",
+ "3095975979578034", "3662215692222536",
+ "6723210018630429", "4411962856225025",
+ "8276996369036686", "4449796938248871",
+ "3350852696538147", "5011802870046957"
+ ],
+};
+
+function checkInitialState() {
+ countEntries(null, null,
+ function (num) {
+ ok(!num, "checking for initially empty storage");
+ startTest();
+ });
+}
+
+function startTest() {
+ // Fill in values for the various fields. We could just set the <input>'s
+ // value attribute, but we don't save default form values (and we want to
+ // ensure unsaved values are because of autocomplete=off or whatever).
+ $_(1, "test1").value = "dontSaveThis";
+ $_(2, "test1").value = "dontSaveThis";
+ $_(3, "test1").value = "dontSaveThis";
+ $_(4, "test1").value = "dontSaveThis";
+ $_(5, "test1").value = "";
+ $_(6, "test1").value = "dontSaveThis";
+ // Form 7 deliberately left untouched.
+ // Form 8 has an input with no name or input attribute.
+ let input = document.getElementById("form8").elements[0];
+ is(input.type, "text", "checking we got unidentified input");
+ input.value = "dontSaveThis";
+ // Form 9 has nothing to modify.
+ $_(10, "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890").value = "dontSaveThis";
+ $_(11, "test1").value = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
+ $_(12, "test1").value = " ";
+ $_(13, "test1").value = "dontSaveThis";
+ $_(14, "test1").type = "password";
+ $_(14, "test1").value = "dontSaveThis";
+
+ var testData = ccNumbers.valid16;
+ for (let i = 0; i != testData.length; i++) {
+ $_(15, "test" + (i + 1)).value = testData[i];
+ }
+
+ testData = ccNumbers.valid15;
+ for (let i = 0; i != testData.length; i++) {
+ $_(16, "test" + (i + 1)).value = testData[i];
+ }
+ $_(17, "test1").value = "001064088";
+ $_(18, "test1").value = "0000-0000-0080-4609";
+ $_(19, "test1").value = "0000 0000 0222 331";
+ $_(20, "test1").value = "dontSaveThis";
+ $_(22, "test1").value = "dontSaveThis";
+ $_(23, "test1").value = "dontSaveThis";
+ $_(24, "searchbar-history").value = "dontSaveThis";
+
+ $_(101, "test1").value = "savedValue";
+ $_(102, "test2").value = "savedValue";
+ $_(103, "test3").value = "savedValue";
+ $_(104, "test4").value = " trimTrailingAndLeadingSpace ";
+ $_(105, "test5").value = "\t trimTrailingAndLeadingWhitespace\t ";
+ $_(106, "test6").value = "00000000109181";
+
+ testData = ccNumbers.invalid16;
+ for (let i = 0; i != testData.length; i++) {
+ $_(107, "test7_" + (i + 1)).value = testData[i];
+ }
+
+ testData = ccNumbers.invalid15;
+ for (let i = 0; i != testData.length; i++) {
+ $_(108, "test8_" + (i + 1)).value = testData[i];
+ }
+
+ $_(109, "test9").value = "savedValue";
+ $_(110, "test10").value = "savedValue";
+
+ // submit the first form.
+ var button = getFormSubmitButton(1);
+ button.click();
+}
+
+
+// Called by each form's onsubmit handler.
+function checkSubmit(formNum) {
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+
+ ok(true, "form " + formNum + " submitted");
+ numSubmittedForms++;
+
+ // Check for expected storage state.
+ switch (formNum) {
+ // Test 1-24 should not save anything.
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ case 16:
+ case 17:
+ case 18:
+ case 19:
+ case 20:
+ case 21:
+ case 22:
+ case 23:
+ case 24:
+ countEntries(null, null,
+ function (num) {
+ ok(!num, "checking for empty storage");
+ submitForm(formNum);
+ });
+ return false;
+ case 100:
+ checkForSave("subtest2", "subtestValue", "checking saved subtest value");
+ break;
+ case 101:
+ checkForSave("test1", "savedValue", "checking saved value");
+ break;
+ case 102:
+ checkForSave("test2", "savedValue", "checking saved value");
+ break;
+ case 103:
+ checkForSave("test3", "savedValue", "checking saved value");
+ break;
+ case 104:
+ checkForSave("test4", "trimTrailingAndLeadingSpace", "checking saved value is trimmed on both sides");
+ break;
+ case 105:
+ checkForSave("test5", "trimTrailingAndLeadingWhitespace", "checking saved value is trimmed on both sides");
+ break;
+ case 106:
+ checkForSave("test6", "00000000109181", "checking saved value");
+ break;
+ case 107:
+ for (let i = 0; i != ccNumbers.invalid16.length; i++) {
+ checkForSave("test7_" + (i + 1), ccNumbers.invalid16[i], "checking saved value");
+ }
+ break;
+ case 108:
+ for (let i = 0; i != ccNumbers.invalid15.length; i++) {
+ checkForSave("test8_" + (i + 1), ccNumbers.invalid15[i], "checking saved value");
+ }
+ break;
+ case 109:
+ checkForSave("test9", "savedValue", "checking saved value");
+ break;
+ case 110:
+ checkForSave("test10", "savedValue", "checking saved value");
+ break;
+ default:
+ ok(false, "Unexpected form submission");
+ break;
+ }
+
+ return submitForm(formNum);
+}
+
+function submitForm(formNum)
+{
+ // Forms 13 and 14 would trigger a save-password notification. Temporarily
+ // disable pwmgr, then reenable it.
+ if (formNum == 12)
+ SpecialPowers.setBoolPref("signon.rememberSignons", false);
+ if (formNum == 14)
+ SpecialPowers.clearUserPref("signon.rememberSignons");
+
+ // Forms 20 and 21 requires browser.formfill.saveHttpsForms to be false
+ if (formNum == 19)
+ SpecialPowers.setBoolPref("browser.formfill.saveHttpsForms", false);
+ // Reset preference now that 20 and 21 are over
+ if (formNum == 21)
+ SpecialPowers.clearUserPref("browser.formfill.saveHttpsForms");
+
+ // End the test now on SeaMonkey.
+ if (formNum == 21 && navigator.userAgent.match(/ SeaMonkey\//)) {
+ checkObserver.uninit();
+ is(numSubmittedForms, 21, "Ensuring all forms were submitted.");
+
+ todo(false, "Skipping remaining checks on SeaMonkey ftb. (Bug 589471)");
+ // finish(), yet let the test actually end first, to be safe.
+ SimpleTest.executeSoon(SimpleTest.finish);
+
+ return false; // return false to cancel current form submission
+ }
+
+ // Form 109 requires browser.formfill.save_https_forms to be true;
+ // Form 110 requires it to be false.
+ if (formNum == 108)
+ SpecialPowers.setBoolPref("browser.formfill.saveHttpsForms", true);
+ if (formNum == 109)
+ SpecialPowers.setBoolPref("browser.formfill.saveHttpsForms", false);
+ if (formNum == 110)
+ SpecialPowers.clearUserPref("browser.formfill.saveHttpsForms");
+
+ // End the test at the last form.
+ if (formNum == 110) {
+ is(numSubmittedForms, 35, "Ensuring all forms were submitted.");
+ checkObserver.uninit();
+ SimpleTest.finish();
+ return false; // return false to cancel current form submission
+ }
+
+ // This timeout is here so that button.click() is never called before this
+ // function returns. If button.click() is called before returning, a long
+ // chain of submits will happen recursively since the submit is dispatched
+ // immediately.
+ //
+ // This in itself is fine, but if there are errors in the code, mochitests
+ // will in some cases give you "server too busy", which is hard to debug!
+ //
+ setTimeout(function() {
+ checkObserver.waitForChecks(function() {
+ var nextFormNum = formNum == 24 ? 100 : (formNum + 1);
+
+ // Submit the next form. Special cases are Forms 21 and 100, which happen
+ // from an HTTPS domain in an iframe.
+ if (nextFormNum == 21 || nextFormNum == 100) {
+ ok(true, "submitting iframe test " + nextFormNum);
+ document.getElementById("iframe").contentWindow.clickButton(nextFormNum);
+ }
+ else {
+ var button = getFormSubmitButton(nextFormNum);
+ button.click();
+ }
+ });
+ }, 0);
+
+ return false; // cancel current form submission
+}
+
+checkObserver.init();
+
+window.onload = checkInitialState;
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_form_submission_cap.html b/toolkit/components/satchel/test/test_form_submission_cap.html
new file mode 100644
index 0000000000..96112f1c1c
--- /dev/null
+++ b/toolkit/components/satchel/test/test_form_submission_cap.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Satchel Test for Form Submisstion Field Cap</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+ <form id="form1" onsubmit="return checkSubmit(1)">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for bug 492701.
+ Save only the first MAX_FIELDS_SAVED changed fields in a form.
+ Generate numInputFields = MAX_FIELDS_SAVED + 1 fields, change all values,
+ and test that only MAX_FIELDS_SAVED are actually saved and that
+ field # numInputFields was not saved.
+*/
+
+var numSubmittedForms = 0;
+var numInputFields = 101;
+
+function checkInitialState() {
+ countEntries(null, null,
+ function (num) {
+ ok(!num, "checking for initially empty storage");
+ startTest();
+ });
+}
+
+function startTest() {
+ var form = document.getElementById("form1");
+ for (i = 1; i <= numInputFields; i++) {
+ var newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "test" + i);
+ form.appendChild(newField);
+ }
+
+ // Fill in values for the various fields. We could just set the <input>'s
+ // value attribute, but we don't save default form values (and we want to
+ // ensure unsaved values are because of autocomplete=off or whatever).
+ for (i = 1; i <= numInputFields; i++) {
+ $_(1, "test" + i).value = i;
+ }
+
+ // submit the first form.
+ var button = getFormSubmitButton(1);
+ button.click();
+}
+
+
+// Called by each form's onsubmit handler.
+function checkSubmit(formNum) {
+ ok(true, "form " + formNum + " submitted");
+ numSubmittedForms++;
+
+ // check that the first (numInputFields - 1) CHANGED fields are saved
+ for (i = 1; i < numInputFields; i++) { // check all but last
+ checkForSave("test" + i, i, "checking saved value " + i);
+ }
+
+ // End the test.
+ is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
+ SimpleTest.finish();
+ return false; // return false to cancel current form submission
+}
+
+
+window.onload = checkInitialState;
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_form_submission_cap2.html b/toolkit/components/satchel/test/test_form_submission_cap2.html
new file mode 100644
index 0000000000..f51fb5f47d
--- /dev/null
+++ b/toolkit/components/satchel/test/test_form_submission_cap2.html
@@ -0,0 +1,190 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Satchel Test for Form Submisstion Field Cap</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<!--
+ Test for bug 492701.
+ Only change field # numInputFields (= MAX_FIELDS_SAVED + 1)
+ and test that it is actually saved and the other (unmodified) ones are not.
+-->
+ <form id="form1" onsubmit="return checkSubmit(1)">
+ <input type="text" name="test1" value="1">
+ <input type="text" name="test2" value="2">
+ <input type="text" name="test3" value="3">
+ <input type="text" name="test4" value="4">
+ <input type="text" name="test5" value="5">
+ <input type="text" name="test6" value="6">
+ <input type="text" name="test7" value="7">
+ <input type="text" name="test8" value="8">
+ <input type="text" name="test9" value="9">
+ <input type="text" name="test10" value="10">
+ <input type="text" name="test11" value="11">
+ <input type="text" name="test12" value="12">
+ <input type="text" name="test13" value="13">
+ <input type="text" name="test14" value="14">
+ <input type="text" name="test15" value="15">
+ <input type="text" name="test16" value="16">
+ <input type="text" name="test17" value="17">
+ <input type="text" name="test18" value="18">
+ <input type="text" name="test19" value="19">
+ <input type="text" name="test20" value="20">
+ <input type="text" name="test21" value="21">
+ <input type="text" name="test22" value="22">
+ <input type="text" name="test23" value="23">
+ <input type="text" name="test24" value="24">
+ <input type="text" name="test25" value="25">
+ <input type="text" name="test26" value="26">
+ <input type="text" name="test27" value="27">
+ <input type="text" name="test28" value="28">
+ <input type="text" name="test29" value="29">
+ <input type="text" name="test30" value="30">
+ <input type="text" name="test31" value="31">
+ <input type="text" name="test32" value="32">
+ <input type="text" name="test33" value="33">
+ <input type="text" name="test34" value="34">
+ <input type="text" name="test35" value="35">
+ <input type="text" name="test36" value="36">
+ <input type="text" name="test37" value="37">
+ <input type="text" name="test38" value="38">
+ <input type="text" name="test39" value="39">
+ <input type="text" name="test40" value="40">
+ <input type="text" name="test41" value="41">
+ <input type="text" name="test42" value="42">
+ <input type="text" name="test43" value="43">
+ <input type="text" name="test44" value="44">
+ <input type="text" name="test45" value="45">
+ <input type="text" name="test46" value="46">
+ <input type="text" name="test47" value="47">
+ <input type="text" name="test48" value="48">
+ <input type="text" name="test49" value="49">
+ <input type="text" name="test50" value="50">
+ <input type="text" name="test51" value="51">
+ <input type="text" name="test52" value="52">
+ <input type="text" name="test53" value="53">
+ <input type="text" name="test54" value="54">
+ <input type="text" name="test55" value="55">
+ <input type="text" name="test56" value="56">
+ <input type="text" name="test57" value="57">
+ <input type="text" name="test58" value="58">
+ <input type="text" name="test59" value="59">
+ <input type="text" name="test60" value="60">
+ <input type="text" name="test61" value="61">
+ <input type="text" name="test62" value="62">
+ <input type="text" name="test63" value="63">
+ <input type="text" name="test64" value="64">
+ <input type="text" name="test65" value="65">
+ <input type="text" name="test66" value="66">
+ <input type="text" name="test67" value="67">
+ <input type="text" name="test68" value="68">
+ <input type="text" name="test69" value="69">
+ <input type="text" name="test70" value="70">
+ <input type="text" name="test71" value="71">
+ <input type="text" name="test72" value="72">
+ <input type="text" name="test73" value="73">
+ <input type="text" name="test74" value="74">
+ <input type="text" name="test75" value="75">
+ <input type="text" name="test76" value="76">
+ <input type="text" name="test77" value="77">
+ <input type="text" name="test78" value="78">
+ <input type="text" name="test79" value="79">
+ <input type="text" name="test80" value="80">
+ <input type="text" name="test81" value="81">
+ <input type="text" name="test82" value="82">
+ <input type="text" name="test83" value="83">
+ <input type="text" name="test84" value="84">
+ <input type="text" name="test85" value="85">
+ <input type="text" name="test86" value="86">
+ <input type="text" name="test87" value="87">
+ <input type="text" name="test88" value="88">
+ <input type="text" name="test89" value="89">
+ <input type="text" name="test90" value="90">
+ <input type="text" name="test91" value="91">
+ <input type="text" name="test92" value="92">
+ <input type="text" name="test93" value="93">
+ <input type="text" name="test94" value="94">
+ <input type="text" name="test95" value="95">
+ <input type="text" name="test96" value="96">
+ <input type="text" name="test97" value="97">
+ <input type="text" name="test98" value="98">
+ <input type="text" name="test99" value="99">
+ <input type="text" name="test100" value="100">
+ <input type="text" name="test101" value="101">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var numSubmittedForms = 0;
+var numInputFields = 101;
+
+function checkInitialState() {
+ countEntries(null, null,
+ function (num) {
+ ok(!num, "checking for initially empty storage");
+ startTest();
+ });
+}
+
+function startTest() {
+ // Fill in values for the various fields. We could just set the <input>'s
+ // value attribute, but we don't save default form values (and we want to
+ // ensure unsaved values are because of autocomplete=off or whatever).
+ $_(1, "test" + numInputFields).value = numInputFields + " changed";
+
+ // submit the first form.
+ var button = getFormSubmitButton(1);
+ button.click();
+}
+
+// Make sure that the first (numInputFields - 1) were not saved (as they were not changed).
+// Call done() when finished.
+function checkCountEntries(formNum, index, done)
+{
+ countEntries("test" + index, index,
+ function (num) {
+ ok(!num, "checking unsaved value " + index);
+ if (index < numInputFields) {
+ checkCountEntries(formNum, index + 1, done);
+ }
+ else {
+ done(formNum);
+ }
+ });
+}
+
+// Called by each form's onsubmit handler.
+function checkSubmit(formNum) {
+ ok(true, "form " + formNum + " submitted");
+ numSubmittedForms++;
+
+ // make sure that the field # numInputFields was saved
+ checkForSave("test" + numInputFields, numInputFields + " changed", "checking saved value " + numInputFields);
+
+ checkCountEntries(formNum, 1, checkSubmitCounted);
+
+ return false; // cancel current form submission
+}
+
+function checkSubmitCounted(formNum) {
+ is(numSubmittedForms, 1, "Ensuring all forms were submitted.");
+ SimpleTest.finish();
+ return false;
+}
+
+window.onload = checkInitialState;
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_password_autocomplete.html b/toolkit/components/satchel/test/test_password_autocomplete.html
new file mode 100644
index 0000000000..82781ae355
--- /dev/null
+++ b/toolkit/components/satchel/test/test_password_autocomplete.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for form history on type=password</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ Test for form history on type=password
+ (based on test_bug_511615.html)
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <datalist id="datalist1">
+ <option>value10</option>
+ <option>value11</option>
+ <option>value12</option>
+ </datalist>
+ <form id="form1" onsubmit="return false;">
+ <!-- Don't set the type to password until rememberSignons is false since we
+ want to test when rememberSignons is false. -->
+ <input type="to-be-password" name="field1" list="datalist1">
+ <button type="submit">Submit</button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from satchel_common.js */
+
+var resolvePopupShownListener;
+registerPopupShownListener(() => resolvePopupShownListener());
+
+function waitForNextPopup() {
+ return new Promise(resolve => { resolvePopupShownListener = resolve; });
+}
+
+/**
+ * Indicates the time to wait before checking that the state of the autocomplete
+ * popup, including whether it is open, has not changed in response to events.
+ *
+ * Manual testing on a fast machine revealed that 80ms was still unreliable,
+ * while 100ms detected a simulated failure reliably. Unfortunately, this means
+ * that to take into account slower machines we should use a larger value.
+ *
+ * Note that if a machine takes more than this time to show the popup, this
+ * would not cause a failure, conversely the machine would not be able to detect
+ * whether the test should have failed. In other words, this use of timeouts is
+ * never expected to cause intermittent failures with test automation.
+ */
+const POPUP_RESPONSE_WAIT_TIME_MS = 200;
+
+SimpleTest.requestFlakyTimeout("Must ensure that an event does not happen.");
+
+/**
+ * Checks that the popup does not open in response to the given function.
+ */
+function expectPopupDoesNotOpen(triggerFn) {
+ let popupShown = waitForNextPopup();
+ triggerFn();
+ return Promise.race([
+ popupShown.then(() => Promise.reject("Popup opened unexpectedly.")),
+ new Promise(resolve => setTimeout(resolve, POPUP_RESPONSE_WAIT_TIME_MS)),
+ ]);
+}
+
+add_task(function* test_initialize() {
+ yield SpecialPowers.pushPrefEnv({"set": [["signon.rememberSignons", false]]});
+
+ // Now that rememberSignons is false, create the password field
+ $_(1, "field1").type = "password";
+
+ yield new Promise(resolve => updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "value1" },
+ { op : "add", fieldname : "field1", value : "value2" },
+ { op : "add", fieldname : "field1", value : "value3" },
+ { op : "add", fieldname : "field1", value : "value4" },
+ { op : "add", fieldname : "field1", value : "value5" },
+ { op : "add", fieldname : "field1", value : "value6" },
+ { op : "add", fieldname : "field1", value : "value7" },
+ { op : "add", fieldname : "field1", value : "value8" },
+ { op : "add", fieldname : "field1", value : "value9" },
+ ], resolve));
+});
+
+add_task(function* test_insecure_focusWarning() {
+ // The form is insecure so should show the warning even if password manager is disabled.
+ let input = $_(1, "field1");
+ let shownPromise = waitForNextPopup();
+ input.focus();
+ yield shownPromise;
+
+ ok(getMenuEntries()[0].includes("Logins entered here could be compromised"),
+ "Check warning is first")
+
+ // Close the popup
+ input.blur();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_popup_direction.html b/toolkit/components/satchel/test/test_popup_direction.html
new file mode 100644
index 0000000000..02e044bbdb
--- /dev/null
+++ b/toolkit/components/satchel/test/test_popup_direction.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Popup Direction</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Test for Popup Direction
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <!-- normal, basic form -->
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var resolvePopupShownListener;
+registerPopupShownListener(() => resolvePopupShownListener());
+
+function waitForNextPopup() {
+ return new Promise(resolve => { resolvePopupShownListener = resolve; });
+}
+
+add_task(function* test_popup_direction() {
+ var input = $_(1, "field1");
+
+ yield new Promise(resolve => updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "value1" },
+ { op : "add", fieldname : "field1", value : "value2" },
+ ], resolve));
+
+ for (let direction of ["ltr", "rtl"]) {
+ document.getElementById("content").style.direction = direction;
+
+ let popupShown = waitForNextPopup();
+ input.focus();
+ doKey("down");
+ yield popupShown;
+
+ let popupState = yield new Promise(resolve => getPopupState(resolve));
+ is(popupState.direction, direction, "Direction should match.");
+
+ // Close the popup.
+ input.blur();
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/test_popup_enter_event.html b/toolkit/components/satchel/test/test_popup_enter_event.html
new file mode 100644
index 0000000000..1a7aa8c196
--- /dev/null
+++ b/toolkit/components/satchel/test/test_popup_enter_event.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for events while the form history popup is open</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Form History test: Test for events while the form history popup is open
+<p id="display"></p>
+
+<div id="content">
+ <form id="form1">
+ <input type="text" name="field1">
+ <button type="submit">Submit</button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody">
+var form = document.getElementById("form1");
+var input = $_(1, "field1");
+var expectedValue = "value1";
+
+function setupFormHistory(aCallback) {
+ updateFormHistory([
+ { op : "remove" },
+ { op : "add", fieldname : "field1", value : "value1" },
+ ], aCallback);
+}
+
+registerPopupShownListener(popupShownListener);
+
+function handleEnter(evt) {
+ if (evt.keyCode != KeyEvent.DOM_VK_RETURN) {
+ return;
+ }
+
+ info("RETURN received for phase: " + evt.eventPhase);
+ if (input.value == expectedValue) {
+ ok(true, "RETURN should be received when the popup is closed");
+ is(input.value, expectedValue, "Check input value when enter is pressed the 2nd time");
+ info("form should submit with the default handler");
+ } else {
+ ok(false, "RETURN keypress shouldn't have been received when a popup item is selected");
+ }
+}
+
+function popupShownListener(evt) {
+ doKey("down");
+ doKey("return"); // select the first entry in the popup
+ doKey("return"); // try to submit the form with the filled value
+}
+
+function runTest() {
+ input.addEventListener("keypress", handleEnter, true);
+ form.addEventListener("submit", evt => {
+ is(input.value, expectedValue, "Check input value in the submit handler");
+ evt.preventDefault();
+ SimpleTest.finish();
+ });
+
+ // Focus the input before adjusting.value so that the caret goes to the end
+ // (since OS X doesn't show the dropdown otherwise).
+ input.focus();
+ input.value = "value"
+ input.focus();
+ doKey("down");
+}
+
+function startTest() {
+ setupFormHistory(function() {
+ runTest();
+ });
+}
+
+window.onload = startTest;
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/satchel/test/unit/.eslintrc.js b/toolkit/components/satchel/test/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite b/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite
new file mode 100644
index 0000000000..07b43c2096
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/asyncformhistory_expire.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_1000.sqlite b/toolkit/components/satchel/test/unit/formhistory_1000.sqlite
new file mode 100644
index 0000000000..5eeab074fd
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_1000.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite b/toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite
new file mode 100644
index 0000000000..5f7498bfc2
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_CORRUPT.sqlite
@@ -0,0 +1 @@
+BACON
diff --git a/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite b/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite
new file mode 100644
index 0000000000..00daf03c27
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_apitest.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite b/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite
new file mode 100644
index 0000000000..724cff73f6
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_autocomplete.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v3.sqlite b/toolkit/components/satchel/test/unit/formhistory_v3.sqlite
new file mode 100644
index 0000000000..e0e8fe2468
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v3.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite b/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite
new file mode 100644
index 0000000000..8eab177e97
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v3v4.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite b/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite
new file mode 100644
index 0000000000..14279f05ff
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v999a.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite b/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite
new file mode 100644
index 0000000000..21d9c1f1cc
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/formhistory_v999b.sqlite
Binary files differ
diff --git a/toolkit/components/satchel/test/unit/head_satchel.js b/toolkit/components/satchel/test/unit/head_satchel.js
new file mode 100644
index 0000000000..282d07ba52
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/head_satchel.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/FormHistory.jsm");
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+
+const CURRENT_SCHEMA = 4;
+const PR_HOURS = 60 * 60 * 1000000;
+
+do_get_profile();
+
+var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+
+// Send the profile-after-change notification to the form history component to ensure
+// that it has been initialized.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+ getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
+function getDBVersion(dbfile) {
+ var ss = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+ var dbConnection = ss.openDatabase(dbfile);
+ var version = dbConnection.schemaVersion;
+ dbConnection.close();
+
+ return version;
+}
+
+const isGUID = /[A-Za-z0-9\+\/]{16}/;
+
+// Find form history entries.
+function searchEntries(terms, params, iter) {
+ let results = [];
+ FormHistory.search(terms, params, { handleResult: result => results.push(result),
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) { if (!reason) iter.next(results); }
+ });
+}
+
+// Count the number of entries with the given name and value, and call then(number)
+// when done. If name or value is null, then the value of that field does not matter.
+function countEntries(name, value, then) {
+ var obj = {};
+ if (name !== null)
+ obj.fieldname = name;
+ if (value !== null)
+ obj.value = value;
+
+ let count = 0;
+ FormHistory.count(obj, { handleResult: result => count = result,
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function (reason) { if (!reason) then(count); }
+ });
+}
+
+// Perform a single form history update and call then() when done.
+function updateEntry(op, name, value, then) {
+ var obj = { op: op };
+ if (name !== null)
+ obj.fieldname = name;
+ if (value !== null)
+ obj.value = value;
+ updateFormHistory(obj, then);
+}
+
+// Add a single form history entry with the current time and call then() when done.
+function addEntry(name, value, then) {
+ let now = Date.now() * 1000;
+ updateFormHistory({ op: "add", fieldname: name, value: value, timesUsed: 1,
+ firstUsed: now, lastUsed: now }, then);
+}
+
+// Wrapper around FormHistory.update which handles errors. Calls then() when done.
+function updateFormHistory(changes, then) {
+ FormHistory.update(changes, { handleError: function (error) {
+ do_throw("Error occurred updating form history: " + error);
+ },
+ handleCompletion: function (reason) { if (!reason) then(); },
+ });
+}
+
+/**
+ * Logs info to the console in the standard way (includes the filename).
+ *
+ * @param aMessage
+ * The message to log to the console.
+ */
+function do_log_info(aMessage) {
+ print("TEST-INFO | " + _TEST_FILE + " | " + aMessage);
+}
diff --git a/toolkit/components/satchel/test/unit/perf_autocomplete.js b/toolkit/components/satchel/test/unit/perf_autocomplete.js
new file mode 100644
index 0000000000..6e8bb5125c
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/perf_autocomplete.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 testnum = 0;
+var fh;
+var fac;
+var prefs;
+
+function countAllEntries() {
+ let stmt = fh.DBConnection.createStatement("SELECT COUNT(*) as numEntries FROM moz_formhistory");
+ do_check_true(stmt.executeStep());
+ let numEntries = stmt.row.numEntries;
+ stmt.finalize();
+ return numEntries;
+}
+
+function do_AC_search(searchTerm, previousResult) {
+ var duration = 0;
+ var searchCount = 5;
+ var tempPrevious = null;
+ var startTime;
+ for (var i = 0; i < searchCount; i++) {
+ if (previousResult !== null)
+ tempPrevious = fac.autoCompleteSearch("searchbar-history", previousResult, null, null);
+ startTime = Date.now();
+ results = fac.autoCompleteSearch("searchbar-history", searchTerm, null, tempPrevious);
+ duration += Date.now() - startTime;
+ }
+ dump("[autoCompleteSearch][test " + testnum + "] for '" + searchTerm + "' ");
+ if (previousResult !== null)
+ dump("with '" + previousResult + "' previous result ");
+ else
+ dump("w/o previous result ");
+ dump("took " + duration + " ms with " + results.matchCount + " matches. ");
+ dump("Average of " + Math.round(duration / searchCount) + " ms per search\n");
+ return results;
+}
+
+function run_test() {
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_1000.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+ var results;
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+
+ fh = Cc["@mozilla.org/satchel/form-history;1"].
+ getService(Ci.nsIFormHistory2);
+ fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].
+ getService(Ci.nsIFormAutoComplete);
+ prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
+ maxTimeGroupings = prefs.getIntPref("browser.formfill.maxTimeGroupings");
+ bucketSize = prefs.getIntPref("browser.formfill.bucketSize");
+
+ // ===== 1 =====
+ // Check initial state is as expected
+ testnum++;
+ do_check_true(fh.hasEntries);
+ do_check_eq(1000, countAllEntries());
+ fac.autoCompleteSearch("searchbar-history", "zzzzzzzzzz", null, null); // warm-up search
+ do_check_true(fh.nameExists("searchbar-history"));
+
+ // ===== 2 =====
+ // Search for '' with no previous result
+ testnum++;
+ results = do_AC_search("", null);
+ do_check_true(results.matchCount > 0);
+
+ // ===== 3 =====
+ // Search for 'r' with no previous result
+ testnum++;
+ results = do_AC_search("r", null);
+ do_check_true(results.matchCount > 0);
+
+ // ===== 4 =====
+ // Search for 'r' with '' previous result
+ testnum++;
+ results = do_AC_search("r", "");
+ do_check_true(results.matchCount > 0);
+
+ // ===== 5 =====
+ // Search for 're' with no previous result
+ testnum++;
+ results = do_AC_search("re", null);
+ do_check_true(results.matchCount > 0);
+
+ // ===== 6 =====
+ // Search for 're' with 'r' previous result
+ testnum++;
+ results = do_AC_search("re", "r");
+ do_check_true(results.matchCount > 0);
+
+ // ===== 7 =====
+ // Search for 'rea' without previous result
+ testnum++;
+ results = do_AC_search("rea", null);
+ let countREA = results.matchCount;
+
+ // ===== 8 =====
+ // Search for 'rea' with 're' previous result
+ testnum++;
+ results = do_AC_search("rea", "re");
+ do_check_eq(countREA, results.matchCount);
+
+ // ===== 9 =====
+ // Search for 'real' with 'rea' previous result
+ testnum++;
+ results = do_AC_search("real", "rea");
+ let countREAL = results.matchCount;
+ do_check_true(results.matchCount <= countREA);
+
+ // ===== 10 =====
+ // Search for 'real' with 're' previous result
+ testnum++;
+ results = do_AC_search("real", "re");
+ do_check_eq(countREAL, results.matchCount);
+
+ // ===== 11 =====
+ // Search for 'real' with no previous result
+ testnum++;
+ results = do_AC_search("real", null);
+ do_check_eq(countREAL, results.matchCount);
+
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+}
diff --git a/toolkit/components/satchel/test/unit/test_async_expire.js b/toolkit/components/satchel/test/unit/test_async_expire.js
new file mode 100644
index 0000000000..501cbdfe55
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_async_expire.js
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 dbFile, oldSize;
+var currentTestIndex = 0;
+
+function triggerExpiration() {
+ // We can't easily fake a "daily idle" event, so for testing purposes form
+ // history listens for another notification to trigger an immediate
+ // expiration.
+ Services.obs.notifyObservers(null, "formhistory-expire-now", null);
+}
+
+var checkExists = function(num) { do_check_true(num > 0); next_test(); }
+var checkNotExists = function(num) { do_check_true(!num); next_test(); }
+
+var TestObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ do_check_eq(topic, "satchel-storage-changed");
+
+ if (data == "formhistory-expireoldentries") {
+ next_test();
+ }
+ }
+};
+
+function test_finished() {
+ // Make sure we always reset prefs.
+ if (Services.prefs.prefHasUserValue("browser.formfill.expire_days"))
+ Services.prefs.clearUserPref("browser.formfill.expire_days");
+
+ do_test_finished();
+}
+
+var iter = tests();
+
+function run_test()
+{
+ do_test_pending();
+ iter.next();
+}
+
+function next_test()
+{
+ iter.next();
+}
+
+function* tests()
+{
+ Services.obs.addObserver(TestObserver, "satchel-storage-changed", true);
+
+ // ===== test init =====
+ var testfile = do_get_file("asyncformhistory_expire.sqlite");
+ var profileDir = do_get_profile();
+
+ // Cleanup from any previous tests or failures.
+ dbFile = profileDir.clone();
+ dbFile.append("formhistory.sqlite");
+ if (dbFile.exists())
+ dbFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_true(dbFile.exists());
+
+ // We're going to clear this at the end, so it better have the default value now.
+ do_check_false(Services.prefs.prefHasUserValue("browser.formfill.expire_days"));
+
+ // Sanity check initial state
+ yield countEntries(null, null, function(num) { do_check_eq(508, num); next_test(); });
+ yield countEntries("name-A", "value-A", checkExists); // lastUsed == distant past
+ yield countEntries("name-B", "value-B", checkExists); // lastUsed == distant future
+
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // Add a new entry
+ yield countEntries("name-C", "value-C", checkNotExists);
+ yield addEntry("name-C", "value-C", next_test);
+ yield countEntries("name-C", "value-C", checkExists);
+
+ // Update some existing entries to have ages relative to when the test runs.
+ var now = 1000 * Date.now();
+ let updateLastUsed = function updateLastUsedFn(results, age)
+ {
+ let lastUsed = now - age * 24 * PR_HOURS;
+
+ let changes = [ ];
+ for (let r = 0; r < results.length; r++) {
+ changes.push({ op: "update", lastUsed: lastUsed, guid: results[r].guid });
+ }
+
+ return changes;
+ }
+
+ let results = yield searchEntries(["guid"], { lastUsed: 181 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 181), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 179 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 179), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 31 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 31), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 29 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 29), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 9999 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 11), next_test);
+
+ results = yield searchEntries(["guid"], { lastUsed: 9 }, iter);
+ yield updateFormHistory(updateLastUsed(results, 9), next_test);
+
+ yield countEntries("name-A", "value-A", checkExists);
+ yield countEntries("181DaysOld", "foo", checkExists);
+ yield countEntries("179DaysOld", "foo", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(509, num); next_test(); });
+
+ // 2 entries are expected to expire.
+ triggerExpiration();
+ yield;
+
+ yield countEntries("name-A", "value-A", checkNotExists);
+ yield countEntries("181DaysOld", "foo", checkNotExists);
+ yield countEntries("179DaysOld", "foo", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+ // And again. No change expected.
+ triggerExpiration();
+ yield;
+
+ yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+ // Set formfill pref to 30 days.
+ Services.prefs.setIntPref("browser.formfill.expire_days", 30);
+ yield countEntries("179DaysOld", "foo", checkExists);
+ yield countEntries("bar", "31days", checkExists);
+ yield countEntries("bar", "29days", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(507, num); next_test(); });
+
+ triggerExpiration();
+ yield;
+
+ yield countEntries("179DaysOld", "foo", checkNotExists);
+ yield countEntries("bar", "31days", checkNotExists);
+ yield countEntries("bar", "29days", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(505, num); next_test(); });
+
+ // Set override pref to 10 days and expire. This expires a large batch of
+ // entries, and should trigger a VACCUM to reduce file size.
+ Services.prefs.setIntPref("browser.formfill.expire_days", 10);
+
+ yield countEntries("bar", "29days", checkExists);
+ yield countEntries("9DaysOld", "foo", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(505, num); next_test(); });
+
+ triggerExpiration();
+ yield;
+
+ yield countEntries("bar", "29days", checkNotExists);
+ yield countEntries("9DaysOld", "foo", checkExists);
+ yield countEntries("name-B", "value-B", checkExists);
+ yield countEntries("name-C", "value-C", checkExists);
+ yield countEntries(null, null, function(num) { do_check_eq(3, num); next_test(); });
+
+ test_finished();
+}
diff --git a/toolkit/components/satchel/test/unit/test_autocomplete.js b/toolkit/components/satchel/test/unit/test_autocomplete.js
new file mode 100644
index 0000000000..2117538099
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_autocomplete.js
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 testnum = 0;
+var fac;
+var prefs;
+
+var numRecords, timeGroupingSize, now;
+
+const DEFAULT_EXPIRE_DAYS = 180;
+
+function padLeft(number, length) {
+ var str = number + '';
+ while (str.length < length)
+ str = '0' + str;
+ return str;
+}
+
+function getFormExpiryDays() {
+ if (prefs.prefHasUserValue("browser.formfill.expire_days")) {
+ return prefs.getIntPref("browser.formfill.expire_days");
+ }
+ return DEFAULT_EXPIRE_DAYS;
+}
+
+function run_test() {
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_autocomplete.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+
+ fac = Cc["@mozilla.org/satchel/form-autocomplete;1"].
+ getService(Ci.nsIFormAutoComplete);
+ prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ timeGroupingSize = prefs.getIntPref("browser.formfill.timeGroupingSize") * 1000 * 1000;
+
+ run_next_test();
+}
+
+add_test(function test0() {
+ var maxTimeGroupings = prefs.getIntPref("browser.formfill.maxTimeGroupings");
+ var bucketSize = prefs.getIntPref("browser.formfill.bucketSize");
+
+ // ===== Tests with constant timesUsed and varying lastUsed date =====
+ // insert 2 records per bucket to check alphabetical sort within
+ now = 1000 * Date.now();
+ numRecords = Math.ceil(maxTimeGroupings / bucketSize) * 2;
+
+ let changes = [ ];
+ for (let i = 0; i < numRecords; i+=2) {
+ let useDate = now - (i/2 * bucketSize * timeGroupingSize);
+
+ changes.push({ op : "add", fieldname: "field1", value: "value" + padLeft(numRecords - 1 - i, 2),
+ timesUsed: 1, firstUsed: useDate, lastUsed: useDate });
+ changes.push({ op : "add", fieldname: "field1", value: "value" + padLeft(numRecords - 2 - i, 2),
+ timesUsed: 1, firstUsed: useDate, lastUsed: useDate });
+ }
+
+ updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test1() {
+ do_log_info("Check initial state is as expected");
+
+ countEntries(null, null, function () {
+ countEntries("field1", null, function (count) {
+ do_check_true(count > 0);
+ run_next_test();
+ });
+ });
+});
+
+add_test(function test2() {
+ do_log_info("Check search contains all entries");
+
+ fac.autoCompleteSearchAsync("field1", "", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(numRecords, aResults.matchCount);
+ run_next_test();
+ }
+ });
+});
+
+add_test(function test3() {
+ do_log_info("Check search result ordering with empty search term");
+
+ let lastFound = numRecords;
+ fac.autoCompleteSearchAsync("field1", "", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < numRecords; i+=2) {
+ do_check_eq(parseInt(aResults.getValueAt(i + 1).substr(5), 10), --lastFound);
+ do_check_eq(parseInt(aResults.getValueAt(i).substr(5), 10), --lastFound);
+ }
+ run_next_test();
+ }
+ });
+});
+
+add_test(function test4() {
+ do_log_info("Check search result ordering with \"v\"");
+
+ let lastFound = numRecords;
+ fac.autoCompleteSearchAsync("field1", "v", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < numRecords; i+=2) {
+ do_check_eq(parseInt(aResults.getValueAt(i + 1).substr(5), 10), --lastFound);
+ do_check_eq(parseInt(aResults.getValueAt(i).substr(5), 10), --lastFound);
+ }
+ run_next_test();
+ }
+ });
+});
+
+const timesUsedSamples = 20;
+
+add_test(function test5() {
+ do_log_info("Begin tests with constant use dates and varying timesUsed");
+
+ let changes = [];
+ for (let i = 0; i < timesUsedSamples; i++) {
+ let timesUsed = (timesUsedSamples - i);
+ let change = { op : "add", fieldname: "field2", value: "value" + (timesUsedSamples - 1 - i),
+ timesUsed: timesUsed * timeGroupingSize, firstUsed: now, lastUsed: now };
+ changes.push(change);
+ }
+ updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test6() {
+ do_log_info("Check search result ordering with empty search term");
+
+ let lastFound = timesUsedSamples;
+ fac.autoCompleteSearchAsync("field2", "", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < timesUsedSamples; i++) {
+ do_check_eq(parseInt(aResults.getValueAt(i).substr(5)), --lastFound);
+ }
+ run_next_test();
+ }
+ });
+});
+
+add_test(function test7() {
+ do_log_info("Check search result ordering with \"v\"");
+
+ let lastFound = timesUsedSamples;
+ fac.autoCompleteSearchAsync("field2", "v", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ for (let i = 0; i < timesUsedSamples; i++) {
+ do_check_eq(parseInt(aResults.getValueAt(i).substr(5)), --lastFound);
+ }
+ run_next_test();
+ }
+ });
+});
+
+add_test(function test8() {
+ do_log_info("Check that \"senior citizen\" entries get a bonus (browser.formfill.agedBonus)");
+
+ let agedDate = 1000 * (Date.now() - getFormExpiryDays() * 24 * 60 * 60 * 1000);
+
+ let changes = [ ];
+ changes.push({ op : "add", fieldname: "field3", value: "old but not senior",
+ timesUsed: 100, firstUsed: (agedDate + 60 * 1000 * 1000), lastUsed: now });
+ changes.push({ op : "add", fieldname: "field3", value: "senior citizen",
+ timesUsed: 100, firstUsed: (agedDate - 60 * 1000 * 1000), lastUsed: now });
+ updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test9() {
+ fac.autoCompleteSearchAsync("field3", "", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(aResults.getValueAt(0), "senior citizen");
+ do_check_eq(aResults.getValueAt(1), "old but not senior");
+ run_next_test();
+ }
+ });
+});
+
+add_test(function test10() {
+ do_log_info("Check entries that are really old or in the future");
+
+ let changes = [ ];
+ changes.push({ op : "add", fieldname: "field4", value: "date of 0",
+ timesUsed: 1, firstUsed: 0, lastUsed: 0 });
+ changes.push({ op : "add", fieldname: "field4", value: "in the future 1",
+ timesUsed: 1, firstUsed: 0, lastUsed: now * 2 });
+ changes.push({ op : "add", fieldname: "field4", value: "in the future 2",
+ timesUsed: 1, firstUsed: now * 2, lastUsed: now * 2 });
+ updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test11() {
+ fac.autoCompleteSearchAsync("field4", "", null, null, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(aResults.matchCount, 3);
+ run_next_test();
+ }
+ });
+});
+
+var syncValues = ["sync1", "sync1a", "sync2", "sync3"]
+
+add_test(function test12() {
+ do_log_info("Check old synchronous api");
+
+ let changes = [ ];
+ for (let value of syncValues) {
+ changes.push({ op : "add", fieldname: "field5", value: value });
+ }
+ updateFormHistory(changes, run_next_test);
+});
+
+add_test(function test_token_limit_DB() {
+ function test_token_limit_previousResult(previousResult) {
+ do_log_info("Check that the number of tokens used in a search is not capped to " +
+ "MAX_SEARCH_TOKENS when using a previousResult");
+ // This provide more accuracy since performance is less of an issue.
+ // Search for a string where the first 10 tokens match the previous value but the 11th does not
+ // when re-using a previous result.
+ fac.autoCompleteSearchAsync("field_token_cap",
+ "a b c d e f g h i j .",
+ null, previousResult, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(aResults.matchCount, 0,
+ "All search tokens should be used with " +
+ "previous results");
+ run_next_test();
+ }
+ });
+ }
+
+ do_log_info("Check that the number of tokens used in a search is capped to MAX_SEARCH_TOKENS " +
+ "for performance when querying the DB");
+ let changes = [ ];
+ changes.push({ op : "add", fieldname: "field_token_cap",
+ // value with 15 unique tokens
+ value: "a b c d e f g h i j k l m n o",
+ timesUsed: 1, firstUsed: 0, lastUsed: 0 });
+ updateFormHistory(changes, () => {
+ // Search for a string where the first 10 tokens match the value above but the 11th does not
+ // (which would prevent the result from being returned if the 11th term was used).
+ fac.autoCompleteSearchAsync("field_token_cap",
+ "a b c d e f g h i j .",
+ null, null, null, {
+ onSearchCompletion : function(aResults) {
+ do_check_eq(aResults.matchCount, 1,
+ "Only the first MAX_SEARCH_TOKENS tokens " +
+ "should be used for DB queries");
+ test_token_limit_previousResult(aResults);
+ }
+ });
+ });
+});
diff --git a/toolkit/components/satchel/test/unit/test_db_corrupt.js b/toolkit/components/satchel/test/unit/test_db_corrupt.js
new file mode 100644
index 0000000000..a6fdc4c02e
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_corrupt.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 bakFile;
+
+function run_test() {
+ // ===== test init =====
+ let testfile = do_get_file("formhistory_CORRUPT.sqlite");
+ let profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ let destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ bakFile = profileDir.clone();
+ bakFile.append("formhistory.sqlite.corrupt");
+ if (bakFile.exists())
+ bakFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ run_next_test();
+}
+
+add_test(function test_corruptFormHistoryDB_lazyCorruptInit1() {
+ do_log_info("ensure FormHistory backs up a corrupt DB on initialization.");
+
+ // DB init is done lazily so the DB shouldn't be created yet.
+ do_check_false(bakFile.exists());
+ // Doing any request to the DB should create it.
+ countEntries(null, null, run_next_test);
+});
+
+add_test(function test_corruptFormHistoryDB_lazyCorruptInit2() {
+ do_check_true(bakFile.exists());
+ bakFile.remove(false);
+ run_next_test();
+});
+
+
+add_test(function test_corruptFormHistoryDB_emptyInit() {
+ do_log_info("test that FormHistory initializes an empty DB in place of corrupt DB.");
+
+ FormHistory.count({}, {
+ handleResult : function(aNumEntries) {
+ do_check_true(aNumEntries == 0);
+ FormHistory.count({ fieldname : "name-A", value : "value-A" }, {
+ handleResult : function(aNumEntries2) {
+ do_check_true(aNumEntries2 == 0);
+ run_next_test();
+ },
+ handleError : function(aError2) {
+ do_throw("DB initialized after reading a corrupt DB file found an entry.");
+ }
+ });
+ },
+ handleError : function (aError) {
+ do_throw("DB initialized after reading a corrupt DB file is not empty.");
+ }
+ });
+});
+
+add_test(function test_corruptFormHistoryDB_addEntry() {
+ do_log_info("test adding an entry to the empty DB.");
+
+ updateEntry("add", "name-A", "value-A",
+ function() {
+ countEntries("name-A", "value-A",
+ function(count) {
+ do_check_true(count == 1);
+ run_next_test();
+ });
+ });
+ });
+
+add_test(function test_corruptFormHistoryDB_removeEntry() {
+ do_log_info("test removing an entry to the empty DB.");
+
+ updateEntry("remove", "name-A", "value-A",
+ function() {
+ countEntries("name-A", "value-A",
+ function(count) {
+ do_check_true(count == 0);
+ run_next_test();
+ });
+ });
+ });
diff --git a/toolkit/components/satchel/test/unit/test_db_update_v4.js b/toolkit/components/satchel/test/unit/test_db_update_v4.js
new file mode 100644
index 0000000000..84b17e8a06
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4.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 testnum = 0;
+
+var iter;
+
+function run_test()
+{
+ do_test_pending();
+ iter = next_test();
+ iter.next();
+}
+
+function* next_test()
+{
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v3.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(3, getDBVersion(testfile));
+
+ // ===== 1 =====
+ testnum++;
+
+ destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ let dbConnection = Services.storage.openUnsharedDatabase(destFile);
+
+ // check for upgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // Check that the index was added
+ do_check_true(dbConnection.tableExists("moz_deleted_formhistory"));
+ dbConnection.close();
+
+ // check for upgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+ // check that an entry still exists
+ yield countEntries("name-A", "value-A",
+ function (num) {
+ do_check_true(num > 0);
+ do_test_finished();
+ }
+ );
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+}
diff --git a/toolkit/components/satchel/test/unit/test_db_update_v4b.js b/toolkit/components/satchel/test/unit/test_db_update_v4b.js
new file mode 100644
index 0000000000..356d34a48b
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4b.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/. */
+
+var testnum = 0;
+
+var iter;
+
+function run_test()
+{
+ do_test_pending();
+ iter = next_test();
+ iter.next();
+}
+
+function* next_test()
+{
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v3v4.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(3, getDBVersion(testfile));
+
+ // ===== 1 =====
+ testnum++;
+
+ destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ dbConnection = Services.storage.openUnsharedDatabase(destFile);
+
+ // check for upgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // Check that the index was added
+ do_check_true(dbConnection.tableExists("moz_deleted_formhistory"));
+ dbConnection.close();
+
+ // check that an entry still exists
+ yield countEntries("name-A", "value-A",
+ function (num) {
+ do_check_true(num > 0);
+ do_test_finished();
+ }
+ );
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+}
diff --git a/toolkit/components/satchel/test/unit/test_db_update_v999a.js b/toolkit/components/satchel/test/unit/test_db_update_v999a.js
new file mode 100644
index 0000000000..0a44d06aac
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999a.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test uses a formhistory.sqlite with schema version set to 999 (a
+ * future version). This exercies the code that allows using a future schema
+ * version as long as the expected columns are present.
+ *
+ * Part A tests this when the columns do match, so the DB is used.
+ * Part B tests this when the columns do *not* match, so the DB is reset.
+ */
+
+var iter = tests();
+
+function run_test()
+{
+ do_test_pending();
+ iter.next();
+}
+
+function next_test()
+{
+ iter.next();
+}
+
+function* tests()
+{
+ try {
+ var testnum = 0;
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v999a.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(999, getDBVersion(testfile));
+
+ let checkZero = function(num) { do_check_eq(num, 0); next_test(); }
+ let checkOne = function(num) { do_check_eq(num, 1); next_test(); }
+
+ // ===== 1 =====
+ testnum++;
+ // Check for expected contents.
+ yield countEntries(null, null, function(num) { do_check_true(num > 0); next_test(); });
+ yield countEntries("name-A", "value-A", checkOne);
+ yield countEntries("name-B", "value-B", checkOne);
+ yield countEntries("name-C", "value-C1", checkOne);
+ yield countEntries("name-C", "value-C2", checkOne);
+ yield countEntries("name-E", "value-E", checkOne);
+
+ // check for downgraded schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // ===== 2 =====
+ testnum++;
+ // Exercise adding and removing a name/value pair
+ yield countEntries("name-D", "value-D", checkZero);
+ yield updateEntry("add", "name-D", "value-D", next_test);
+ yield countEntries("name-D", "value-D", checkOne);
+ yield updateEntry("remove", "name-D", "value-D", next_test);
+ yield countEntries("name-D", "value-D", checkZero);
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+
+ do_test_finished();
+}
diff --git a/toolkit/components/satchel/test/unit/test_db_update_v999b.js b/toolkit/components/satchel/test/unit/test_db_update_v999b.js
new file mode 100644
index 0000000000..fb0ecd1b7e
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999b.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 test uses a formhistory.sqlite with schema version set to 999 (a
+ * future version). This exercies the code that allows using a future schema
+ * version as long as the expected columns are present.
+ *
+ * Part A tests this when the columns do match, so the DB is used.
+ * Part B tests this when the columns do *not* match, so the DB is reset.
+ */
+
+var iter = tests();
+
+function run_test()
+{
+ do_test_pending();
+ iter.next();
+}
+
+function next_test()
+{
+ iter.next();
+}
+
+function* tests()
+{
+ try {
+ var testnum = 0;
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_v999b.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ var bakFile = profileDir.clone();
+ bakFile.append("formhistory.sqlite.corrupt");
+ if (bakFile.exists())
+ bakFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+ do_check_eq(999, getDBVersion(testfile));
+
+ let checkZero = function(num) { do_check_eq(num, 0); next_test(); }
+ let checkOne = function(num) { do_check_eq(num, 1); next_test(); }
+
+ // ===== 1 =====
+ testnum++;
+
+ // Open the DB, ensure that a backup of the corrupt DB is made.
+ // DB init is done lazily so the DB shouldn't be created yet.
+ do_check_false(bakFile.exists());
+ // Doing any request to the DB should create it.
+ yield countEntries("", "", next_test);
+
+ do_check_true(bakFile.exists());
+ bakFile.remove(false);
+
+ // ===== 2 =====
+ testnum++;
+ // File should be empty
+ yield countEntries(null, null, function(num) { do_check_false(num); next_test(); });
+ yield countEntries("name-A", "value-A", checkZero);
+ // check for current schema.
+ do_check_eq(CURRENT_SCHEMA, FormHistory.schemaVersion);
+
+ // ===== 3 =====
+ testnum++;
+ // Try adding an entry
+ yield updateEntry("add", "name-A", "value-A", next_test);
+ yield countEntries(null, null, checkOne);
+ yield countEntries("name-A", "value-A", checkOne);
+
+ // ===== 4 =====
+ testnum++;
+ // Try removing an entry
+ yield updateEntry("remove", "name-A", "value-A", next_test);
+ yield countEntries(null, null, checkZero);
+ yield countEntries("name-A", "value-A", checkZero);
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+
+ do_test_finished();
+}
diff --git a/toolkit/components/satchel/test/unit/test_history_api.js b/toolkit/components/satchel/test/unit/test_history_api.js
new file mode 100644
index 0000000000..7535041830
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_history_api.js
@@ -0,0 +1,457 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 testnum = 0;
+var dbConnection; // used for deleted table tests
+
+Cu.import("resource://gre/modules/Promise.jsm");
+
+function countDeletedEntries(expected)
+{
+ let deferred = Promise.defer();
+ let stmt = dbConnection.createAsyncStatement("SELECT COUNT(*) AS numEntries FROM moz_deleted_formhistory");
+ stmt.executeAsync({
+ handleResult: function(resultSet) {
+ do_check_eq(expected, resultSet.getNextRow().getResultByName("numEntries"));
+ deferred.resolve();
+ },
+ handleError : function () {
+ do_throw("Error occurred counting deleted entries: " + error);
+ deferred.reject();
+ },
+ handleCompletion : function () {
+ stmt.finalize();
+ }
+ });
+ return deferred.promise;
+}
+
+function checkTimeDeleted(guid, checkFunction)
+{
+ let deferred = Promise.defer();
+ let stmt = dbConnection.createAsyncStatement("SELECT timeDeleted FROM moz_deleted_formhistory WHERE guid = :guid");
+ stmt.params.guid = guid;
+ stmt.executeAsync({
+ handleResult: function(resultSet) {
+ checkFunction(resultSet.getNextRow().getResultByName("timeDeleted"));
+ deferred.resolve();
+ },
+ handleError : function () {
+ do_throw("Error occurred getting deleted entries: " + error);
+ deferred.reject();
+ },
+ handleCompletion : function () {
+ stmt.finalize();
+ }
+ });
+ return deferred.promise;
+}
+
+function promiseUpdateEntry(op, name, value)
+{
+ var change = { op: op };
+ if (name !== null)
+ change.fieldname = name;
+ if (value !== null)
+ change.value = value;
+ return promiseUpdate(change);
+}
+
+function promiseUpdate(change) {
+ return new Promise((resolve, reject) => {
+ FormHistory.update(change, {
+ handleError(error) {
+ this._error = error;
+ },
+ handleCompletion(reason) {
+ if (reason) {
+ reject(this._error);
+ } else {
+ resolve();
+ }
+ }
+ });
+ });
+}
+
+function promiseSearchEntries(terms, params)
+{
+ let deferred = Promise.defer();
+ let results = [];
+ FormHistory.search(terms, params,
+ { handleResult: result => results.push(result),
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ deferred.reject(error);
+ },
+ handleCompletion: function (reason) { if (!reason) deferred.resolve(results); }
+ });
+ return deferred.promise;
+}
+
+function promiseCountEntries(name, value, checkFn)
+{
+ let deferred = Promise.defer();
+ countEntries(name, value, function (result) { checkFn(result); deferred.resolve(); } );
+ return deferred.promise;
+}
+
+add_task(function* ()
+{
+ let oldSupportsDeletedTable = FormHistory._supportsDeletedTable;
+ FormHistory._supportsDeletedTable = true;
+
+ try {
+
+ // ===== test init =====
+ var testfile = do_get_file("formhistory_apitest.sqlite");
+ var profileDir = dirSvc.get("ProfD", Ci.nsIFile);
+
+ // Cleanup from any previous tests or failures.
+ var destFile = profileDir.clone();
+ destFile.append("formhistory.sqlite");
+ if (destFile.exists())
+ destFile.remove(false);
+
+ testfile.copyTo(profileDir, "formhistory.sqlite");
+
+ function checkExists(num) { do_check_true(num > 0); }
+ function checkNotExists(num) { do_check_true(num == 0); }
+
+ // ===== 1 =====
+ // Check initial state is as expected
+ testnum++;
+ yield promiseCountEntries("name-A", null, checkExists);
+ yield promiseCountEntries("name-B", null, checkExists);
+ yield promiseCountEntries("name-C", null, checkExists);
+ yield promiseCountEntries("name-D", null, checkExists);
+ yield promiseCountEntries("name-A", "value-A", checkExists);
+ yield promiseCountEntries("name-B", "value-B1", checkExists);
+ yield promiseCountEntries("name-B", "value-B2", checkExists);
+ yield promiseCountEntries("name-C", "value-C", checkExists);
+ yield promiseCountEntries("name-D", "value-D", checkExists);
+ // time-A/B/C/D checked below.
+
+ // Delete anything from the deleted table
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+ dbFile.append("formhistory.sqlite");
+ dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+
+ let deferred = Promise.defer();
+
+ let stmt = dbConnection.createAsyncStatement("DELETE FROM moz_deleted_formhistory");
+ stmt.executeAsync({
+ handleResult: function(resultSet) { },
+ handleError : function () {
+ do_throw("Error occurred counting deleted all entries: " + error);
+ },
+ handleCompletion : function () {
+ stmt.finalize();
+ deferred.resolve();
+ }
+ });
+ yield deferred.promise;
+
+ // ===== 2 =====
+ // Test looking for nonexistent / bogus data.
+ testnum++;
+ yield promiseCountEntries("blah", null, checkNotExists);
+ yield promiseCountEntries("", null, checkNotExists);
+ yield promiseCountEntries("name-A", "blah", checkNotExists);
+ yield promiseCountEntries("name-A", "", checkNotExists);
+ yield promiseCountEntries("name-A", null, checkExists);
+ yield promiseCountEntries("blah", "value-A", checkNotExists);
+ yield promiseCountEntries("", "value-A", checkNotExists);
+ yield promiseCountEntries(null, "value-A", checkExists);
+
+ // Cannot use promiseCountEntries when name and value are null because it treats null values as not set
+ // and here a search should be done explicity for null.
+ deferred = Promise.defer();
+ yield FormHistory.count({ fieldname: null, value: null },
+ { handleResult: result => checkNotExists(result),
+ handleError: function (error) {
+ do_throw("Error occurred searching form history: " + error);
+ },
+ handleCompletion: function(reason) { if (!reason) deferred.resolve() }
+ });
+ yield deferred.promise;
+
+ // ===== 3 =====
+ // Test removeEntriesForName with a single matching value
+ testnum++;
+ yield promiseUpdateEntry("remove", "name-A", null);
+
+ yield promiseCountEntries("name-A", "value-A", checkNotExists);
+ yield promiseCountEntries("name-B", "value-B1", checkExists);
+ yield promiseCountEntries("name-B", "value-B2", checkExists);
+ yield promiseCountEntries("name-C", "value-C", checkExists);
+ yield promiseCountEntries("name-D", "value-D", checkExists);
+ yield countDeletedEntries(1);
+
+ // ===== 4 =====
+ // Test removeEntriesForName with multiple matching values
+ testnum++;
+ yield promiseUpdateEntry("remove", "name-B", null);
+
+ yield promiseCountEntries("name-A", "value-A", checkNotExists);
+ yield promiseCountEntries("name-B", "value-B1", checkNotExists);
+ yield promiseCountEntries("name-B", "value-B2", checkNotExists);
+ yield promiseCountEntries("name-C", "value-C", checkExists);
+ yield promiseCountEntries("name-D", "value-D", checkExists);
+ yield countDeletedEntries(3);
+
+ // ===== 5 =====
+ // Test removing by time range (single entry, not surrounding entries)
+ testnum++;
+ yield promiseCountEntries("time-A", null, checkExists); // firstUsed=1000, lastUsed=1000
+ yield promiseCountEntries("time-B", null, checkExists); // firstUsed=1000, lastUsed=1099
+ yield promiseCountEntries("time-C", null, checkExists); // firstUsed=1099, lastUsed=1099
+ yield promiseCountEntries("time-D", null, checkExists); // firstUsed=2001, lastUsed=2001
+ yield promiseUpdate({ op : "remove", firstUsedStart: 1050, firstUsedEnd: 2000 });
+
+ yield promiseCountEntries("time-A", null, checkExists);
+ yield promiseCountEntries("time-B", null, checkExists);
+ yield promiseCountEntries("time-C", null, checkNotExists);
+ yield promiseCountEntries("time-D", null, checkExists);
+ yield countDeletedEntries(4);
+
+ // ===== 6 =====
+ // Test removing by time range (multiple entries)
+ testnum++;
+ yield promiseUpdate({ op : "remove", firstUsedStart: 1000, firstUsedEnd: 2000 });
+
+ yield promiseCountEntries("time-A", null, checkNotExists);
+ yield promiseCountEntries("time-B", null, checkNotExists);
+ yield promiseCountEntries("time-C", null, checkNotExists);
+ yield promiseCountEntries("time-D", null, checkExists);
+ yield countDeletedEntries(6);
+
+ // ===== 7 =====
+ // test removeAllEntries
+ testnum++;
+ yield promiseUpdateEntry("remove", null, null);
+
+ yield promiseCountEntries("name-C", null, checkNotExists);
+ yield promiseCountEntries("name-D", null, checkNotExists);
+ yield promiseCountEntries("name-C", "value-C", checkNotExists);
+ yield promiseCountEntries("name-D", "value-D", checkNotExists);
+
+ yield promiseCountEntries(null, null, checkNotExists);
+ yield countDeletedEntries(6);
+
+ // ===== 8 =====
+ // Add a single entry back
+ testnum++;
+ yield promiseUpdateEntry("add", "newname-A", "newvalue-A");
+ yield promiseCountEntries("newname-A", "newvalue-A", checkExists);
+
+ // ===== 9 =====
+ // Remove the single entry
+ testnum++;
+ yield promiseUpdateEntry("remove", "newname-A", "newvalue-A");
+ yield promiseCountEntries("newname-A", "newvalue-A", checkNotExists);
+
+ // ===== 10 =====
+ // Add a single entry
+ testnum++;
+ yield promiseUpdateEntry("add", "field1", "value1");
+ yield promiseCountEntries("field1", "value1", checkExists);
+
+ let processFirstResult = function processResults(results)
+ {
+ // Only handle the first result
+ if (results.length > 0) {
+ let result = results[0];
+ return [result.timesUsed, result.firstUsed, result.lastUsed, result.guid];
+ }
+ return undefined;
+ }
+
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field1", value: "value1" });
+ let [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(1, timesUsed);
+ do_check_true(firstUsed > 0);
+ do_check_true(lastUsed > 0);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 1));
+
+ // ===== 11 =====
+ // Add another single entry
+ testnum++;
+ yield promiseUpdateEntry("add", "field1", "value1b");
+ yield promiseCountEntries("field1", "value1", checkExists);
+ yield promiseCountEntries("field1", "value1b", checkExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 2));
+
+ // ===== 12 =====
+ // Update a single entry
+ testnum++;
+
+ results = yield promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1" });
+ let guid = processFirstResult(results)[3];
+
+ yield promiseUpdate({ op : "update", guid: guid, value: "modifiedValue" });
+ yield promiseCountEntries("field1", "modifiedValue", checkExists);
+ yield promiseCountEntries("field1", "value1", checkNotExists);
+ yield promiseCountEntries("field1", "value1b", checkExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 2));
+
+ // ===== 13 =====
+ // Add a single entry with times
+ testnum++;
+ yield promiseUpdate({ op : "add", fieldname: "field2", value: "value2",
+ timesUsed: 20, firstUsed: 100, lastUsed: 500 });
+
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field2", value: "value2" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+
+ do_check_eq(20, timesUsed);
+ do_check_eq(100, firstUsed);
+ do_check_eq(500, lastUsed);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 3));
+
+ // ===== 14 =====
+ // Bump an entry, which updates its lastUsed field
+ testnum++;
+ yield promiseUpdate({ op : "bump", fieldname: "field2", value: "value2",
+ timesUsed: 20, firstUsed: 100, lastUsed: 500 });
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field2", value: "value2" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(21, timesUsed);
+ do_check_eq(100, firstUsed);
+ do_check_true(lastUsed > 500);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 3));
+
+ // ===== 15 =====
+ // Bump an entry that does not exist
+ testnum++;
+ yield promiseUpdate({ op : "bump", fieldname: "field3", value: "value3",
+ timesUsed: 10, firstUsed: 50, lastUsed: 400 });
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field3", value: "value3" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(10, timesUsed);
+ do_check_eq(50, firstUsed);
+ do_check_eq(400, lastUsed);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 16 =====
+ // Bump an entry with a guid
+ testnum++;
+ results = yield promiseSearchEntries(["guid"], { fieldname: "field3", value: "value3" });
+ guid = processFirstResult(results)[3];
+ yield promiseUpdate({ op : "bump", guid: guid, timesUsed: 20, firstUsed: 55, lastUsed: 400 });
+ results = yield promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+ { fieldname: "field3", value: "value3" });
+ [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
+ do_check_eq(11, timesUsed);
+ do_check_eq(50, firstUsed);
+ do_check_true(lastUsed > 400);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 17 =====
+ // Remove an entry
+ testnum++;
+ yield countDeletedEntries(7);
+
+ results = yield promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1b" });
+ guid = processFirstResult(results)[3];
+
+ yield promiseUpdate({ op : "remove", guid: guid});
+ yield promiseCountEntries("field1", "modifiedValue", checkExists);
+ yield promiseCountEntries("field1", "value1b", checkNotExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 3));
+
+ yield countDeletedEntries(8);
+ yield checkTimeDeleted(guid, timeDeleted => do_check_true(timeDeleted > 10000));
+
+ // ===== 18 =====
+ // Add yet another single entry
+ testnum++;
+ yield promiseUpdate({ op : "add", fieldname: "field4", value: "value4",
+ timesUsed: 5, firstUsed: 230, lastUsed: 600 });
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 19 =====
+ // Remove an entry by time
+ testnum++;
+ yield promiseUpdate({ op : "remove", firstUsedStart: 60, firstUsedEnd: 250 });
+ yield promiseCountEntries("field1", "modifiedValue", checkExists);
+ yield promiseCountEntries("field2", "value2", checkNotExists);
+ yield promiseCountEntries("field3", "value3", checkExists);
+ yield promiseCountEntries("field4", "value4", checkNotExists);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 2));
+ yield countDeletedEntries(10);
+
+ // ===== 20 =====
+ // Bump multiple existing entries at once
+ testnum++;
+
+ yield promiseUpdate([{ op : "add", fieldname: "field5", value: "value5",
+ timesUsed: 5, firstUsed: 230, lastUsed: 600 },
+ { op : "add", fieldname: "field6", value: "value6",
+ timesUsed: 12, firstUsed: 430, lastUsed: 700 }]);
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ yield promiseUpdate([
+ { op : "bump", fieldname: "field5", value: "value5" },
+ { op : "bump", fieldname: "field6", value: "value6" }]);
+ results = yield promiseSearchEntries(["fieldname", "timesUsed", "firstUsed", "lastUsed"], { });
+
+ do_check_eq(6, results[2].timesUsed);
+ do_check_eq(13, results[3].timesUsed);
+ do_check_eq(230, results[2].firstUsed);
+ do_check_eq(430, results[3].firstUsed);
+ do_check_true(results[2].lastUsed > 600);
+ do_check_true(results[3].lastUsed > 700);
+
+ yield promiseCountEntries(null, null, num => do_check_eq(num, 4));
+
+ // ===== 21 =====
+ // Check update fails if form history is disabled and the operation is not a
+ // pure removal.
+ testnum++;
+ Services.prefs.setBoolPref("browser.formfill.enable", false);
+
+ // Cannot use arrow functions, see bug 1237961.
+ Assert.rejects(promiseUpdate(
+ { op : "bump", fieldname: "field5", value: "value5" }),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "bumping when form history is disabled should fail");
+ Assert.rejects(promiseUpdate(
+ { op : "add", fieldname: "field5", value: "value5" }),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "Adding when form history is disabled should fail");
+ Assert.rejects(promiseUpdate([
+ { op : "update", fieldname: "field5", value: "value5" },
+ { op : "remove", fieldname: "field5", value: "value5" }
+ ]),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "mixed operations when form history is disabled should fail");
+ Assert.rejects(promiseUpdate([
+ null, undefined, "", 1, {},
+ { op : "remove", fieldname: "field5", value: "value5" }
+ ]),
+ function(err) { return err.result == Ci.mozIStorageError.MISUSE; },
+ "Invalid entries when form history is disabled should fail");
+
+ // Remove should work though.
+ yield promiseUpdate([{ op: "remove", fieldname: "field5", value: null },
+ { op: "remove", fieldname: null, value: null }]);
+ Services.prefs.clearUserPref("browser.formfill.enable");
+
+ } catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + e;
+ }
+ finally {
+ FormHistory._supportsDeletedTable = oldSupportsDeletedTable;
+ dbConnection.asyncClose(do_test_finished);
+ }
+});
+
+function run_test() {
+ return run_next_test();
+}
diff --git a/toolkit/components/satchel/test/unit/test_notify.js b/toolkit/components/satchel/test/unit/test_notify.js
new file mode 100644
index 0000000000..556ecd4b08
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_notify.js
@@ -0,0 +1,158 @@
+/*
+ * Test suite for satchel notifications
+ *
+ * Tests notifications dispatched when modifying form history.
+ *
+ */
+
+var expectedNotification;
+var expectedData;
+
+var TestObserver = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe : function (subject, topic, data) {
+ do_check_eq(topic, "satchel-storage-changed");
+ do_check_eq(data, expectedNotification);
+
+ switch (data) {
+ case "formhistory-add":
+ case "formhistory-update":
+ do_check_true(subject instanceof Ci.nsISupportsString);
+ do_check_true(isGUID.test(subject.toString()));
+ break;
+ case "formhistory-remove":
+ do_check_eq(null, subject);
+ break;
+ default:
+ do_throw("Unhandled notification: " + data + " / " + topic);
+ }
+
+ expectedNotification = null;
+ expectedData = null;
+ }
+};
+
+var testIterator = null;
+
+function run_test() {
+ do_test_pending();
+ testIterator = run_test_steps();
+ testIterator.next();
+}
+
+function next_test()
+{
+ testIterator.next();
+}
+
+function* run_test_steps() {
+
+try {
+
+var testnum = 0;
+var testdesc = "Setup of test form history entries";
+
+var entry1 = ["entry1", "value1"];
+
+/* ========== 1 ========== */
+testnum = 1;
+testdesc = "Initial connection to storage module"
+
+yield updateEntry("remove", null, null, next_test);
+yield countEntries(null, null, function (num) { do_check_false(num, "Checking initial DB is empty"); next_test(); });
+
+// Add the observer
+var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+os.addObserver(TestObserver, "satchel-storage-changed", false);
+
+/* ========== 2 ========== */
+testnum++;
+testdesc = "addEntry";
+
+expectedNotification = "formhistory-add";
+expectedData = entry1;
+
+yield updateEntry("add", entry1[0], entry1[1], next_test);
+do_check_eq(expectedNotification, null); // check that observer got a notification
+
+yield countEntries(entry1[0], entry1[1], function (num) { do_check_true(num > 0); next_test(); });
+
+/* ========== 3 ========== */
+testnum++;
+testdesc = "modifyEntry";
+
+expectedNotification = "formhistory-update";
+expectedData = entry1;
+// will update previous entry
+yield updateEntry("update", entry1[0], entry1[1], next_test);
+yield countEntries(entry1[0], entry1[1], function (num) { do_check_true(num > 0); next_test(); });
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 4 ========== */
+testnum++;
+testdesc = "removeEntry";
+
+expectedNotification = "formhistory-remove";
+expectedData = entry1;
+yield updateEntry("remove", entry1[0], entry1[1], next_test);
+
+do_check_eq(expectedNotification, null);
+yield countEntries(entry1[0], entry1[1], function(num) { do_check_false(num, "doesn't exist after remove"); next_test(); });
+
+/* ========== 5 ========== */
+testnum++;
+testdesc = "removeAllEntries";
+
+expectedNotification = "formhistory-remove";
+expectedData = null; // no data expected
+yield updateEntry("remove", null, null, next_test);
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 6 ========== */
+testnum++;
+testdesc = "removeAllEntries (again)";
+
+expectedNotification = "formhistory-remove";
+expectedData = null;
+yield updateEntry("remove", null, null, next_test);
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 7 ========== */
+testnum++;
+testdesc = "removeEntriesForName";
+
+expectedNotification = "formhistory-remove";
+expectedData = "field2";
+yield updateEntry("remove", null, "field2", next_test);
+
+do_check_eq(expectedNotification, null);
+
+/* ========== 8 ========== */
+testnum++;
+testdesc = "removeEntriesByTimeframe";
+
+expectedNotification = "formhistory-remove";
+expectedData = [10, 99999999999];
+
+yield FormHistory.update({ op: "remove", firstUsedStart: expectedData[0], firstUsedEnd: expectedData[1] },
+ { handleCompletion: function(reason) { if (!reason) next_test() },
+ handleErrors: function (error) {
+ do_throw("Error occurred updating form history: " + error);
+ }
+ });
+
+do_check_eq(expectedNotification, null);
+
+os.removeObserver(TestObserver, "satchel-storage-changed", false);
+
+do_test_finished();
+
+} catch (e) {
+ throw "FAILED in test #" + testnum + " -- " + testdesc + ": " + e;
+}
+}
diff --git a/toolkit/components/satchel/test/unit/test_previous_result.js b/toolkit/components/satchel/test/unit/test_previous_result.js
new file mode 100644
index 0000000000..06e5a385b2
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/test_previous_result.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/. */
+
+var aaaListener = {
+ onSearchResult: function(search, result) {
+ do_check_eq(result.searchString, "aaa");
+ do_test_finished();
+ }
+};
+
+var aaListener = {
+ onSearchResult: function(search, result) {
+ do_check_eq(result.searchString, "aa");
+ search.startSearch("aaa", "", result, aaaListener);
+ }
+};
+
+function run_test()
+{
+ do_test_pending();
+ let search = Cc['@mozilla.org/autocomplete/search;1?name=form-history'].
+ getService(Components.interfaces.nsIAutoCompleteSearch);
+ search.startSearch("aa", "", null, aaListener);
+}
diff --git a/toolkit/components/satchel/test/unit/xpcshell.ini b/toolkit/components/satchel/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..4a41b47d65
--- /dev/null
+++ b/toolkit/components/satchel/test/unit/xpcshell.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+head = head_satchel.js
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ asyncformhistory_expire.sqlite
+ formhistory_1000.sqlite
+ formhistory_CORRUPT.sqlite
+ formhistory_apitest.sqlite
+ formhistory_autocomplete.sqlite
+ formhistory_v3.sqlite
+ formhistory_v3v4.sqlite
+ formhistory_v999a.sqlite
+ formhistory_v999b.sqlite
+ perf_autocomplete.js
+
+[test_async_expire.js]
+[test_autocomplete.js]
+[test_db_corrupt.js]
+[test_db_update_v4.js]
+[test_db_update_v4b.js]
+[test_db_update_v999a.js]
+[test_db_update_v999b.js]
+[test_history_api.js]
+[test_notify.js]
+[test_previous_result.js]
diff --git a/toolkit/components/satchel/towel b/toolkit/components/satchel/towel
new file mode 100644
index 0000000000..c26c7a8b28
--- /dev/null
+++ b/toolkit/components/satchel/towel
@@ -0,0 +1,5 @@
+"Any man who can hitch the length and breadth of the galaxy, rough it,
+slum it, struggle against terrible odds, win through, and still knows
+where his towel is is clearly a man to be reckoned with."
+
+ - Douglas Adams
diff --git a/toolkit/components/search/SearchStaticData.jsm b/toolkit/components/search/SearchStaticData.jsm
new file mode 100644
index 0000000000..de2be695c3
--- /dev/null
+++ b/toolkit/components/search/SearchStaticData.jsm
@@ -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/. */
+
+/*
+ * This module contains additional data about default search engines that is the
+ * same across all languages. This information is defined outside of the actual
+ * search engine definition files, so that localizers don't need to update them
+ * when a change is made.
+ *
+ * This separate module is also easily overridable, in case a hotfix is needed.
+ * No high-level processing logic is applied here.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "SearchStaticData",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// To update this list of known alternate domains, just cut-and-paste from
+// https://www.google.com/supported_domains
+const gGoogleDomainsSource = ".google.com .google.ad .google.ae .google.com.af .google.com.ag .google.com.ai .google.al .google.am .google.co.ao .google.com.ar .google.as .google.at .google.com.au .google.az .google.ba .google.com.bd .google.be .google.bf .google.bg .google.com.bh .google.bi .google.bj .google.com.bn .google.com.bo .google.com.br .google.bs .google.bt .google.co.bw .google.by .google.com.bz .google.ca .google.cd .google.cf .google.cg .google.ch .google.ci .google.co.ck .google.cl .google.cm .google.cn .google.com.co .google.co.cr .google.com.cu .google.cv .google.com.cy .google.cz .google.de .google.dj .google.dk .google.dm .google.com.do .google.dz .google.com.ec .google.ee .google.com.eg .google.es .google.com.et .google.fi .google.com.fj .google.fm .google.fr .google.ga .google.ge .google.gg .google.com.gh .google.com.gi .google.gl .google.gm .google.gp .google.gr .google.com.gt .google.gy .google.com.hk .google.hn .google.hr .google.ht .google.hu .google.co.id .google.ie .google.co.il .google.im .google.co.in .google.iq .google.is .google.it .google.je .google.com.jm .google.jo .google.co.jp .google.co.ke .google.com.kh .google.ki .google.kg .google.co.kr .google.com.kw .google.kz .google.la .google.com.lb .google.li .google.lk .google.co.ls .google.lt .google.lu .google.lv .google.com.ly .google.co.ma .google.md .google.me .google.mg .google.mk .google.ml .google.com.mm .google.mn .google.ms .google.com.mt .google.mu .google.mv .google.mw .google.com.mx .google.com.my .google.co.mz .google.com.na .google.com.nf .google.com.ng .google.com.ni .google.ne .google.nl .google.no .google.com.np .google.nr .google.nu .google.co.nz .google.com.om .google.com.pa .google.com.pe .google.com.pg .google.com.ph .google.com.pk .google.pl .google.pn .google.com.pr .google.ps .google.pt .google.com.py .google.com.qa .google.ro .google.ru .google.rw .google.com.sa .google.com.sb .google.sc .google.se .google.com.sg .google.sh .google.si .google.sk .google.com.sl .google.sn .google.so .google.sm .google.sr .google.st .google.com.sv .google.td .google.tg .google.co.th .google.com.tj .google.tk .google.tl .google.tm .google.tn .google.to .google.com.tr .google.tt .google.com.tw .google.co.tz .google.com.ua .google.co.ug .google.co.uk .google.com.uy .google.co.uz .google.com.vc .google.co.ve .google.vg .google.co.vi .google.com.vn .google.vu .google.ws .google.rs .google.co.za .google.co.zm .google.co.zw .google.cat";
+const gGoogleDomains = gGoogleDomainsSource.split(" ").map(d => "www" + d);
+
+this.SearchStaticData = {
+ /**
+ * Returns a list of alternate domains for a given search engine domain.
+ *
+ * @param aDomain
+ * Lowercase host name to look up. For example, if this argument is
+ * "www.google.com" or "www.google.co.uk", the function returns the
+ * full list of supported Google domains.
+ *
+ * @return Array containing one entry for each alternate host name, or empty
+ * array if none is known. The returned array should not be modified.
+ */
+ getAlternateDomains: function (aDomain) {
+ return gGoogleDomains.indexOf(aDomain) == -1 ? [] : gGoogleDomains;
+ },
+};
diff --git a/toolkit/components/search/SearchSuggestionController.jsm b/toolkit/components/search/SearchSuggestionController.jsm
new file mode 100644
index 0000000000..952838c0c5
--- /dev/null
+++ b/toolkit/components/search/SearchSuggestionController.jsm
@@ -0,0 +1,398 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["SearchSuggestionController"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NS_ASSERT", "resource://gre/modules/debug.js");
+
+const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
+const DEFAULT_FORM_HISTORY_PARAM = "searchbar-history";
+const HTTP_OK = 200;
+const REMOTE_TIMEOUT = 500; // maximum time (ms) to wait before giving up on a remote suggestions
+const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
+
+/**
+ * Remote search suggestions will be shown if gRemoteSuggestionsEnabled
+ * is true. Global because only one pref observer is needed for all instances.
+ */
+var gRemoteSuggestionsEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
+Services.prefs.addObserver(BROWSER_SUGGEST_PREF, function(aSubject, aTopic, aData) {
+ gRemoteSuggestionsEnabled = Services.prefs.getBoolPref(BROWSER_SUGGEST_PREF);
+}, false);
+
+/**
+ * SearchSuggestionController.jsm exists as a helper module to allow multiple consumers to request and display
+ * search suggestions from a given engine, regardless of the base implementation. Much of this
+ * code was originally in nsSearchSuggestions.js until it was refactored to separate it from the
+ * nsIAutoCompleteSearch dependency.
+ * One instance of SearchSuggestionController should be used per field since form history results are cached.
+ */
+
+/**
+ * @param {function} [callback] - Callback for search suggestion results. You can use the promise
+ * returned by the search method instead if you prefer.
+ * @constructor
+ */
+this.SearchSuggestionController = function SearchSuggestionController(callback = null) {
+ this._callback = callback;
+};
+
+this.SearchSuggestionController.prototype = {
+ /**
+ * The maximum number of local form history results to return. This limit is
+ * only enforced if remote results are also returned.
+ */
+ maxLocalResults: 5,
+
+ /**
+ * The maximum number of remote search engine results to return.
+ * We'll actually only display at most
+ * maxRemoteResults - <displayed local results count> remote results.
+ */
+ maxRemoteResults: 10,
+
+ /**
+ * The maximum time (ms) to wait before giving up on a remote suggestions.
+ */
+ remoteTimeout: REMOTE_TIMEOUT,
+
+ /**
+ * The additional parameter used when searching form history.
+ */
+ formHistoryParam: DEFAULT_FORM_HISTORY_PARAM,
+
+ // Private properties
+ /**
+ * The last form history result used to improve the performance of subsequent searches.
+ * This shouldn't be used for any other purpose as it is never cleared and therefore could be stale.
+ */
+ _formHistoryResult: null,
+
+ /**
+ * The remote server timeout timer, if applicable. The timer starts when form history
+ * search is completed.
+ */
+ _remoteResultTimer: null,
+
+ /**
+ * The deferred for the remote results before its promise is resolved.
+ */
+ _deferredRemoteResult: null,
+
+ /**
+ * The optional result callback registered from the constructor.
+ */
+ _callback: null,
+
+ /**
+ * The XMLHttpRequest object for remote results.
+ */
+ _request: null,
+
+ // Public methods
+
+ /**
+ * Fetch search suggestions from all of the providers. Fetches in progress will be stopped and
+ * results from them will not be provided.
+ *
+ * @param {string} searchTerm - the term to provide suggestions for
+ * @param {bool} privateMode - whether the request is being made in the context of private browsing
+ * @param {nsISearchEngine} engine - search engine for the suggestions.
+ * @param {int} userContextId - the userContextId of the selected tab.
+ *
+ * @return {Promise} resolving to an object containing results or null.
+ */
+ fetch: function(searchTerm, privateMode, engine, userContextId) {
+ // There is no smart filtering from previous results here (as there is when looking through
+ // history/form data) because the result set returned by the server is different for every typed
+ // value - e.g. "ocean breathes" does not return a subset of the results returned for "ocean".
+
+ this.stop();
+
+ if (!Services.search.isInitialized) {
+ throw new Error("Search not initialized yet (how did you get here?)");
+ }
+ if (typeof privateMode === "undefined") {
+ throw new Error("The privateMode argument is required to avoid unintentional privacy leaks");
+ }
+ if (!(engine instanceof Ci.nsISearchEngine)) {
+ throw new Error("Invalid search engine");
+ }
+ if (!this.maxLocalResults && !this.maxRemoteResults) {
+ throw new Error("Zero results expected, what are you trying to do?");
+ }
+ if (this.maxLocalResults < 0 || this.maxRemoteResults < 0) {
+ throw new Error("Number of requested results must be positive");
+ }
+
+ // Array of promises to resolve before returning results.
+ let promises = [];
+ this._searchString = searchTerm;
+
+ // Remote results
+ if (searchTerm && gRemoteSuggestionsEnabled && this.maxRemoteResults &&
+ engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON)) {
+ this._deferredRemoteResult = this._fetchRemote(searchTerm, engine, privateMode, userContextId);
+ promises.push(this._deferredRemoteResult.promise);
+ }
+
+ // Local results from form history
+ if (this.maxLocalResults) {
+ let deferredHistoryResult = this._fetchFormHistory(searchTerm);
+ promises.push(deferredHistoryResult.promise);
+ }
+
+ function handleRejection(reason) {
+ if (reason == "HTTP request aborted") {
+ // Do nothing since this is normal.
+ return null;
+ }
+ Cu.reportError("SearchSuggestionController rejection: " + reason);
+ return null;
+ }
+ return Promise.all(promises).then(this._dedupeAndReturnResults.bind(this), handleRejection);
+ },
+
+ /**
+ * Stop pending fetches so no results are returned from them.
+ *
+ * Note: If there was no remote results fetched, the fetching cannot be stopped and local results
+ * will still be returned because stopping relies on aborting the XMLHTTPRequest to reject the
+ * promise for Promise.all.
+ */
+ stop: function() {
+ if (this._request) {
+ this._request.abort();
+ } else if (!this.maxRemoteResults) {
+ Cu.reportError("SearchSuggestionController: Cannot stop fetching if remote results were not "+
+ "requested");
+ }
+ this._reset();
+ },
+
+ // Private methods
+
+ _fetchFormHistory: function(searchTerm) {
+ let deferredFormHistory = Promise.defer();
+
+ let acSearchObserver = {
+ // Implements nsIAutoCompleteSearch
+ onSearchResult: (search, result) => {
+ this._formHistoryResult = result;
+
+ if (this._request) {
+ this._remoteResultTimer = Cc["@mozilla.org/timer;1"].
+ createInstance(Ci.nsITimer);
+ this._remoteResultTimer.initWithCallback(this._onRemoteTimeout.bind(this),
+ this.remoteTimeout || REMOTE_TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+
+ switch (result.searchResult) {
+ case Ci.nsIAutoCompleteResult.RESULT_SUCCESS:
+ case Ci.nsIAutoCompleteResult.RESULT_NOMATCH:
+ if (result.searchString !== this._searchString) {
+ deferredFormHistory.resolve("Unexpected response, this._searchString does not match form history response");
+ return;
+ }
+ let fhEntries = [];
+ for (let i = 0; i < result.matchCount; ++i) {
+ fhEntries.push(result.getValueAt(i));
+ }
+ deferredFormHistory.resolve({
+ result: fhEntries,
+ formHistoryResult: result,
+ });
+ break;
+ case Ci.nsIAutoCompleteResult.RESULT_FAILURE:
+ case Ci.nsIAutoCompleteResult.RESULT_IGNORED:
+ deferredFormHistory.resolve("Form History returned RESULT_FAILURE or RESULT_IGNORED");
+ break;
+ }
+ },
+ };
+
+ let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
+ createInstance(Ci.nsIAutoCompleteSearch);
+ formHistory.startSearch(searchTerm, this.formHistoryParam || DEFAULT_FORM_HISTORY_PARAM,
+ this._formHistoryResult,
+ acSearchObserver);
+ return deferredFormHistory;
+ },
+
+ /**
+ * Fetch suggestions from the search engine over the network.
+ */
+ _fetchRemote: function(searchTerm, engine, privateMode, userContextId) {
+ let deferredResponse = Promise.defer();
+ this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ let submission = engine.getSubmission(searchTerm,
+ SEARCH_RESPONSE_SUGGESTION_JSON);
+ let method = (submission.postData ? "POST" : "GET");
+ this._request.open(method, submission.uri.spec, true);
+
+ this._request.setOriginAttributes({userContextId,
+ privateBrowsingId: privateMode ? 1 : 0});
+
+ this._request.mozBackgroundRequest = true; // suppress dialogs and fail silently
+
+ this._request.addEventListener("load", this._onRemoteLoaded.bind(this, deferredResponse));
+ this._request.addEventListener("error", (evt) => deferredResponse.resolve("HTTP error"));
+ // Reject for an abort assuming it's always from .stop() in which case we shouldn't return local
+ // or remote results for existing searches.
+ this._request.addEventListener("abort", (evt) => deferredResponse.reject("HTTP request aborted"));
+
+ this._request.send(submission.postData);
+
+ return deferredResponse;
+ },
+
+ /**
+ * Called when the request completed successfully (thought the HTTP status could be anything)
+ * so we can handle the response data.
+ * @private
+ */
+ _onRemoteLoaded: function(deferredResponse) {
+ if (!this._request) {
+ deferredResponse.resolve("Got HTTP response after the request was cancelled");
+ return;
+ }
+
+ let status, serverResults;
+ try {
+ status = this._request.status;
+ } catch (e) {
+ // The XMLHttpRequest can throw NS_ERROR_NOT_AVAILABLE.
+ deferredResponse.resolve("Unknown HTTP status: " + e);
+ return;
+ }
+
+ if (status != HTTP_OK || this._request.responseText == "") {
+ deferredResponse.resolve("Non-200 status or empty HTTP response: " + status);
+ return;
+ }
+
+ try {
+ serverResults = JSON.parse(this._request.responseText);
+ } catch (ex) {
+ deferredResponse.resolve("Failed to parse suggestion JSON: " + ex);
+ return;
+ }
+
+ if (!serverResults[0] ||
+ this._searchString.localeCompare(serverResults[0], undefined,
+ { sensitivity: "base" })) {
+ // something is wrong here so drop remote results
+ deferredResponse.resolve("Unexpected response, this._searchString does not match remote response");
+ return;
+ }
+ let results = serverResults[1] || [];
+ deferredResponse.resolve({ result: results });
+ },
+
+ /**
+ * Called when this._remoteResultTimer fires indicating the remote request took too long.
+ */
+ _onRemoteTimeout: function () {
+ this._request = null;
+
+ // FIXME: bug 387341
+ // Need to break the cycle between us and the timer.
+ this._remoteResultTimer = null;
+
+ // The XMLHTTPRequest for suggest results is taking too long
+ // so send out the form history results and cancel the request.
+ if (this._deferredRemoteResult) {
+ this._deferredRemoteResult.resolve("HTTP Timeout");
+ this._deferredRemoteResult = null;
+ }
+ },
+
+ /**
+ * @param {Array} suggestResults - an array of result objects from different sources (local or remote)
+ * @return {Object}
+ */
+ _dedupeAndReturnResults: function(suggestResults) {
+ if (this._searchString === null) {
+ // _searchString can be null if stop() was called and remote suggestions
+ // were disabled (stopping if we are fetching remote suggestions will
+ // cause a promise rejection before we reach _dedupeAndReturnResults).
+ return null;
+ }
+
+ let results = {
+ term: this._searchString,
+ remote: [],
+ local: [],
+ formHistoryResult: null,
+ };
+
+ for (let result of suggestResults) {
+ if (typeof result === "string") { // Failure message
+ Cu.reportError("SearchSuggestionController: " + result);
+ } else if (result.formHistoryResult) { // Local results have a formHistoryResult property.
+ results.formHistoryResult = result.formHistoryResult;
+ results.local = result.result || [];
+ } else { // Remote result
+ results.remote = result.result || [];
+ }
+ }
+
+ // If we have remote results, cap the number of local results
+ if (results.remote.length) {
+ results.local = results.local.slice(0, this.maxLocalResults);
+ }
+
+ // We don't want things to appear in both history and suggestions so remove entries from
+ // remote results that are already in local.
+ if (results.remote.length && results.local.length) {
+ for (let i = 0; i < results.local.length; ++i) {
+ let term = results.local[i];
+ let dupIndex = results.remote.indexOf(term);
+ if (dupIndex != -1) {
+ results.remote.splice(dupIndex, 1);
+ }
+ }
+ }
+
+ // Trim the number of results to the maximum requested (now that we've pruned dupes).
+ results.remote =
+ results.remote.slice(0, this.maxRemoteResults - results.local.length);
+
+ if (this._callback) {
+ this._callback(results);
+ }
+ this._reset();
+
+ return results;
+ },
+
+ _reset: function() {
+ this._request = null;
+ if (this._remoteResultTimer) {
+ this._remoteResultTimer.cancel();
+ this._remoteResultTimer = null;
+ }
+ this._deferredRemoteResult = null;
+ this._searchString = null;
+ },
+};
+
+/**
+ * Determines whether the given engine offers search suggestions.
+ *
+ * @param {nsISearchEngine} engine - The search engine
+ * @return {boolean} True if the engine offers suggestions and false otherwise.
+ */
+this.SearchSuggestionController.engineOffersSuggestions = function(engine) {
+ return engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON);
+};
diff --git a/toolkit/components/search/moz.build b/toolkit/components/search/moz.build
new file mode 100644
index 0000000000..98ccf2b8d0
--- /dev/null
+++ b/toolkit/components/search/moz.build
@@ -0,0 +1,33 @@
+# -*- 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/xpcshell/xpcshell.ini']
+
+EXTRA_COMPONENTS += [
+ 'nsSearchService.js',
+ 'nsSearchSuggestions.js',
+]
+
+if CONFIG['MOZ_BUILD_APP'] in ['browser', 'mobile/android', 'xulrunner']:
+ DEFINES['HAVE_SIDEBAR'] = True
+ EXTRA_COMPONENTS += [
+ 'nsSidebar.js',
+ ]
+
+EXTRA_JS_MODULES += [
+ 'SearchSuggestionController.jsm',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'toolkitsearch.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'SearchStaticData.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Search')
diff --git a/toolkit/components/search/nsSearchService.js b/toolkit/components/search/nsSearchService.js
new file mode 100644
index 0000000000..bbe66ba7ef
--- /dev/null
+++ b/toolkit/components/search/nsSearchService.js
@@ -0,0 +1,4789 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/debug.js");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SearchStaticData",
+ "resource://gre/modules/SearchStaticData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Lz4",
+ "resource://gre/modules/lz4.js");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gTextToSubURI",
+ "@mozilla.org/intl/texttosuburi;1",
+ "nsITextToSubURI");
+XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment",
+ "@mozilla.org/process/environment;1",
+ "nsIEnvironment");
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+// A text encoder to UTF8, used whenever we commit the cache to disk.
+XPCOMUtils.defineLazyGetter(this, "gEncoder",
+ function() {
+ return new TextEncoder();
+ });
+
+const MODE_RDONLY = 0x01;
+const MODE_WRONLY = 0x02;
+const MODE_CREATE = 0x08;
+const MODE_APPEND = 0x10;
+const MODE_TRUNCATE = 0x20;
+
+// Directory service keys
+const NS_APP_SEARCH_DIR_LIST = "SrchPluginsDL";
+const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+const NS_APP_SEARCH_DIR = "SrchPlugns";
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+
+// Loading plugins from NS_APP_SEARCH_DIR is no longer supported.
+// Instead, we now load plugins from APP_SEARCH_PREFIX, where a
+// list.txt file needs to exist to list available engines.
+const APP_SEARCH_PREFIX = "resource://search-plugins/";
+
+// See documentation in nsIBrowserSearchService.idl.
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+const QUIT_APPLICATION_TOPIC = "quit-application";
+
+const SEARCH_ENGINE_REMOVED = "engine-removed";
+const SEARCH_ENGINE_ADDED = "engine-added";
+const SEARCH_ENGINE_CHANGED = "engine-changed";
+const SEARCH_ENGINE_LOADED = "engine-loaded";
+const SEARCH_ENGINE_CURRENT = "engine-current";
+const SEARCH_ENGINE_DEFAULT = "engine-default";
+
+// The following constants are left undocumented in nsIBrowserSearchService.idl
+// For the moment, they are meant for testing/debugging purposes only.
+
+/**
+ * Topic used for events involving the service itself.
+ */
+const SEARCH_SERVICE_TOPIC = "browser-search-service";
+
+/**
+ * Sent whenever the cache is fully written to disk.
+ */
+const SEARCH_SERVICE_CACHE_WRITTEN = "write-cache-to-disk-complete";
+
+// Delay for lazy serialization (ms)
+const LAZY_SERIALIZE_DELAY = 100;
+
+// Delay for batching invalidation of the JSON cache (ms)
+const CACHE_INVALIDATION_DELAY = 1000;
+
+// Current cache version. This should be incremented if the format of the cache
+// file is modified.
+const CACHE_VERSION = 1;
+
+const CACHE_FILENAME = "search.json.mozlz4";
+
+const NEW_LINES = /(\r\n|\r|\n)/;
+
+// Set an arbitrary cap on the maximum icon size. Without this, large icons can
+// cause big delays when loading them at startup.
+const MAX_ICON_SIZE = 10000;
+
+// Default charset to use for sending search parameters. ISO-8859-1 is used to
+// match previous nsInternetSearchService behavior.
+const DEFAULT_QUERY_CHARSET = "ISO-8859-1";
+
+const SEARCH_BUNDLE = "chrome://global/locale/search/search.properties";
+const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
+
+const OPENSEARCH_NS_10 = "http://a9.com/-/spec/opensearch/1.0/";
+const OPENSEARCH_NS_11 = "http://a9.com/-/spec/opensearch/1.1/";
+
+// Although the specification at http://opensearch.a9.com/spec/1.1/description/
+// gives the namespace names defined above, many existing OpenSearch engines
+// are using the following versions. We therefore allow either.
+const OPENSEARCH_NAMESPACES = [
+ OPENSEARCH_NS_11, OPENSEARCH_NS_10,
+ "http://a9.com/-/spec/opensearchdescription/1.1/",
+ "http://a9.com/-/spec/opensearchdescription/1.0/"
+];
+
+const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
+
+const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/";
+const MOZSEARCH_LOCALNAME = "SearchPlugin";
+
+const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
+const URLTYPE_SEARCH_HTML = "text/html";
+const URLTYPE_OPENSEARCH = "application/opensearchdescription+xml";
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+const LOCALE_PREF = "general.useragent.locale";
+
+const USER_DEFINED = "{searchTerms}";
+
+// Custom search parameters
+const MOZ_OFFICIAL = AppConstants.MOZ_OFFICIAL_BRANDING ? "official" : "unofficial";
+
+const MOZ_PARAM_LOCALE = /\{moz:locale\}/g;
+const MOZ_PARAM_DIST_ID = /\{moz:distributionID\}/g;
+const MOZ_PARAM_OFFICIAL = /\{moz:official\}/g;
+
+// Supported OpenSearch parameters
+// See http://opensearch.a9.com/spec/1.1/querysyntax/#core
+const OS_PARAM_USER_DEFINED = /\{searchTerms\??\}/g;
+const OS_PARAM_INPUT_ENCODING = /\{inputEncoding\??\}/g;
+const OS_PARAM_LANGUAGE = /\{language\??\}/g;
+const OS_PARAM_OUTPUT_ENCODING = /\{outputEncoding\??\}/g;
+
+// Default values
+const OS_PARAM_LANGUAGE_DEF = "*";
+const OS_PARAM_OUTPUT_ENCODING_DEF = "UTF-8";
+const OS_PARAM_INPUT_ENCODING_DEF = "UTF-8";
+
+// "Unsupported" OpenSearch parameters. For example, we don't support
+// page-based results, so if the engine requires that we send the "page index"
+// parameter, we'll always send "1".
+const OS_PARAM_COUNT = /\{count\??\}/g;
+const OS_PARAM_START_INDEX = /\{startIndex\??\}/g;
+const OS_PARAM_START_PAGE = /\{startPage\??\}/g;
+
+// Default values
+const OS_PARAM_COUNT_DEF = "20"; // 20 results
+const OS_PARAM_START_INDEX_DEF = "1"; // start at 1st result
+const OS_PARAM_START_PAGE_DEF = "1"; // 1st page
+
+// Optional parameter
+const OS_PARAM_OPTIONAL = /\{(?:\w+:)?\w+\?\}/g;
+
+// A array of arrays containing parameters that we don't fully support, and
+// their default values. We will only send values for these parameters if
+// required, since our values are just really arbitrary "guesses" that should
+// give us the output we want.
+var OS_UNSUPPORTED_PARAMS = [
+ [OS_PARAM_COUNT, OS_PARAM_COUNT_DEF],
+ [OS_PARAM_START_INDEX, OS_PARAM_START_INDEX_DEF],
+ [OS_PARAM_START_PAGE, OS_PARAM_START_PAGE_DEF],
+];
+
+// The default engine update interval, in days. This is only used if an engine
+// specifies an updateURL, but not an updateInterval.
+const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
+
+// The default interval before checking again for the name of the
+// default engine for the region, in seconds. Only used if the response
+// from the server doesn't specify an interval.
+const SEARCH_GEO_DEFAULT_UPDATE_INTERVAL = 2592000; // 30 days.
+
+this.__defineGetter__("FileUtils", function() {
+ delete this.FileUtils;
+ Components.utils.import("resource://gre/modules/FileUtils.jsm");
+ return FileUtils;
+});
+
+this.__defineGetter__("NetUtil", function() {
+ delete this.NetUtil;
+ Components.utils.import("resource://gre/modules/NetUtil.jsm");
+ return NetUtil;
+});
+
+this.__defineGetter__("gChromeReg", function() {
+ delete this.gChromeReg;
+ return this.gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIChromeRegistry);
+});
+
+/**
+ * Prefixed to all search debug output.
+ */
+const SEARCH_LOG_PREFIX = "*** Search: ";
+
+/**
+ * Outputs aText to the JavaScript console as well as to stdout.
+ */
+function DO_LOG(aText) {
+ dump(SEARCH_LOG_PREFIX + aText + "\n");
+ Services.console.logStringMessage(aText);
+}
+
+/**
+ * In debug builds, use a live, pref-based (browser.search.log) LOG function
+ * to allow enabling/disabling without a restart. Otherwise, don't log at all by
+ * default. This can be overridden at startup by the pref, see SearchService's
+ * _init method.
+ */
+var LOG = function() {};
+
+if (AppConstants.DEBUG) {
+ LOG = function (aText) {
+ if (getBoolPref(BROWSER_SEARCH_PREF + "log", false)) {
+ DO_LOG(aText);
+ }
+ };
+}
+
+/**
+ * Presents an assertion dialog in non-release builds and throws.
+ * @param message
+ * A message to display
+ * @param resultCode
+ * The NS_ERROR_* value to throw.
+ * @throws resultCode
+ */
+function ERROR(message, resultCode) {
+ NS_ASSERT(false, SEARCH_LOG_PREFIX + message);
+ throw Components.Exception(message, resultCode);
+}
+
+/**
+ * Logs the failure message (if browser.search.log is enabled) and throws.
+ * @param message
+ * A message to display
+ * @param resultCode
+ * The NS_ERROR_* value to throw.
+ * @throws resultCode or NS_ERROR_INVALID_ARG if resultCode isn't specified.
+ */
+function FAIL(message, resultCode) {
+ LOG(message);
+ throw Components.Exception(message, resultCode || Cr.NS_ERROR_INVALID_ARG);
+}
+
+/**
+ * Truncates big blobs of (data-)URIs to console-friendly sizes
+ * @param str
+ * String to tone down
+ * @param len
+ * Maximum length of the string to return. Defaults to the length of a tweet.
+ */
+function limitURILength(str, len) {
+ len = len || 140;
+ if (str.length > len)
+ return str.slice(0, len) + "...";
+ return str;
+}
+
+/**
+ * Ensures an assertion is met before continuing. Should be used to indicate
+ * fatal errors.
+ * @param assertion
+ * An assertion that must be met
+ * @param message
+ * A message to display if the assertion is not met
+ * @param resultCode
+ * The NS_ERROR_* value to throw if the assertion is not met
+ * @throws resultCode
+ */
+function ENSURE_WARN(assertion, message, resultCode) {
+ NS_ASSERT(assertion, SEARCH_LOG_PREFIX + message);
+ if (!assertion)
+ throw Components.Exception(message, resultCode);
+}
+
+function loadListener(aChannel, aEngine, aCallback) {
+ this._channel = aChannel;
+ this._bytes = [];
+ this._engine = aEngine;
+ this._callback = aCallback;
+}
+loadListener.prototype = {
+ _callback: null,
+ _channel: null,
+ _countRead: 0,
+ _engine: null,
+ _stream: null,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIRequestObserver,
+ Ci.nsIStreamListener,
+ Ci.nsIChannelEventSink,
+ Ci.nsIInterfaceRequestor,
+ // See FIXME comment below.
+ Ci.nsIHttpEventSink,
+ Ci.nsIProgressEventSink
+ ]),
+
+ // nsIRequestObserver
+ onStartRequest: function SRCH_loadStartR(aRequest, aContext) {
+ LOG("loadListener: Starting request: " + aRequest.name);
+ this._stream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ },
+
+ onStopRequest: function SRCH_loadStopR(aRequest, aContext, aStatusCode) {
+ LOG("loadListener: Stopping request: " + aRequest.name);
+
+ var requestFailed = !Components.isSuccessCode(aStatusCode);
+ if (!requestFailed && (aRequest instanceof Ci.nsIHttpChannel))
+ requestFailed = !aRequest.requestSucceeded;
+
+ if (requestFailed || this._countRead == 0) {
+ LOG("loadListener: request failed!");
+ // send null so the callback can deal with the failure
+ this._callback(null, this._engine);
+ } else
+ this._callback(this._bytes, this._engine);
+ this._channel = null;
+ this._engine = null;
+ },
+
+ // nsIStreamListener
+ onDataAvailable: function SRCH_loadDAvailable(aRequest, aContext,
+ aInputStream, aOffset,
+ aCount) {
+ this._stream.setInputStream(aInputStream);
+
+ // Get a byte array of the data
+ this._bytes = this._bytes.concat(this._stream.readByteArray(aCount));
+ this._countRead += aCount;
+ },
+
+ // nsIChannelEventSink
+ asyncOnChannelRedirect: function SRCH_loadCRedirect(aOldChannel, aNewChannel,
+ aFlags, callback) {
+ this._channel = aNewChannel;
+ callback.onRedirectVerifyCallback(Components.results.NS_OK);
+ },
+
+ // nsIInterfaceRequestor
+ getInterface: function SRCH_load_GI(aIID) {
+ return this.QueryInterface(aIID);
+ },
+
+ // FIXME: bug 253127
+ // nsIHttpEventSink
+ onRedirect: function (aChannel, aNewChannel) {},
+ // nsIProgressEventSink
+ onProgress: function (aRequest, aContext, aProgress, aProgressMax) {},
+ onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
+}
+
+function isPartnerBuild() {
+ try {
+ let distroID = Services.prefs.getCharPref("distribution.id");
+
+ // Mozilla-provided builds (i.e. funnelcake) are not partner builds
+ if (distroID && !distroID.startsWith("mozilla")) {
+ return true;
+ }
+ } catch (e) {}
+
+ return false;
+}
+
+// Method to determine if we should be using geo-specific defaults
+function geoSpecificDefaultsEnabled() {
+ let geoSpecificDefaults = false;
+ try {
+ geoSpecificDefaults = Services.prefs.getBoolPref("browser.search.geoSpecificDefaults");
+ } catch (e) {}
+
+ return geoSpecificDefaults;
+}
+
+// Some notes on countryCode and region prefs:
+// * A "countryCode" pref is set via a geoip lookup. It always reflects the
+// result of that geoip request.
+// * A "region" pref, once set, is the region actually used for search. In
+// most cases it will be identical to the countryCode pref.
+// * The value of "region" and "countryCode" will only not agree in one edge
+// case - 34/35 users who have previously been configured to use US defaults
+// based purely on a timezone check will have "region" forced to US,
+// regardless of what countryCode geoip returns.
+// * We may want to know if we are in the US before we have *either*
+// countryCode or region - in which case we fallback to a timezone check,
+// but we don't persist that value anywhere in the expectation we will
+// eventually get a countryCode/region.
+
+// A method that "migrates" prefs if necessary.
+function migrateRegionPrefs() {
+ // If we already have a "region" pref there's nothing to do.
+ if (Services.prefs.prefHasUserValue("browser.search.region")) {
+ return;
+ }
+
+ // If we have 'isUS' but no 'countryCode' then we are almost certainly
+ // a profile from Fx 34/35 that set 'isUS' based purely on a timezone
+ // check. If this said they were US, we force region to be US.
+ // (But if isUS was false, we leave region alone - we will do a geoip request
+ // and set the region accordingly)
+ try {
+ if (Services.prefs.getBoolPref("browser.search.isUS") &&
+ !Services.prefs.prefHasUserValue("browser.search.countryCode")) {
+ Services.prefs.setCharPref("browser.search.region", "US");
+ }
+ } catch (ex) {
+ // no isUS pref, nothing to do.
+ }
+ // If we have a countryCode pref but no region pref, just force region
+ // to be the countryCode.
+ try {
+ let countryCode = Services.prefs.getCharPref("browser.search.countryCode");
+ if (!Services.prefs.prefHasUserValue("browser.search.region")) {
+ Services.prefs.setCharPref("browser.search.region", countryCode);
+ }
+ } catch (ex) {
+ // no countryCode pref, nothing to do.
+ }
+}
+
+// A method to determine if we are in the United States (US) for the search
+// service.
+// It uses a browser.search.region pref (which typically comes from a geoip
+// request) or if that doesn't exist, falls back to a hacky timezone check.
+function getIsUS() {
+ // Regardless of the region or countryCode, non en-US builds are not
+ // considered to be in the US from the POV of the search service.
+ if (getLocale() != "en-US") {
+ return false;
+ }
+
+ // If we've got a region pref, trust it.
+ try {
+ return Services.prefs.getCharPref("browser.search.region") == "US";
+ } catch (e) {}
+
+ // So we are en-US but have no region pref - fallback to hacky timezone check.
+ let isNA = isUSTimezone();
+ LOG("getIsUS() fell back to a timezone check with the result=" + isNA);
+ return isNA;
+}
+
+// Helper method to modify preference keys with geo-specific modifiers, if needed.
+function getGeoSpecificPrefName(basepref) {
+ if (!geoSpecificDefaultsEnabled() || isPartnerBuild())
+ return basepref;
+ if (getIsUS())
+ return basepref + ".US";
+ return basepref;
+}
+
+// A method that tries to determine if this user is in a US geography.
+function isUSTimezone() {
+ // Timezone assumptions! We assume that if the system clock's timezone is
+ // between Newfoundland and Hawaii, that the user is in North America.
+
+ // This includes all of South America as well, but we have relatively few
+ // en-US users there, so that's OK.
+
+ // 150 minutes = 2.5 hours (UTC-2.5), which is
+ // Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
+
+ // 600 minutes = 10 hours (UTC-10), which is
+ // Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
+
+ let UTCOffset = (new Date()).getTimezoneOffset();
+ return UTCOffset >= 150 && UTCOffset <= 600;
+}
+
+// A less hacky method that tries to determine our country-code via an XHR
+// geoip lookup.
+// If this succeeds and we are using an en-US locale, we set the pref used by
+// the hacky method above, so isUS() can avoid the hacky timezone method.
+// If it fails we don't touch that pref so isUS() does its normal thing.
+var ensureKnownCountryCode = Task.async(function* (ss) {
+ // If we have a country-code already stored in our prefs we trust it.
+ let countryCode;
+ try {
+ countryCode = Services.prefs.getCharPref("browser.search.countryCode");
+ } catch (e) {}
+
+ if (!countryCode) {
+ // We don't have it cached, so fetch it. fetchCountryCode() will call
+ // storeCountryCode if it gets a result (even if that happens after the
+ // promise resolves) and fetchRegionDefault.
+ yield fetchCountryCode(ss);
+ } else {
+ // if nothing to do, return early.
+ if (!geoSpecificDefaultsEnabled())
+ return;
+
+ let expir = ss.getGlobalAttr("searchDefaultExpir") || 0;
+ if (expir > Date.now()) {
+ // The territory default we have already fetched hasn't expired yet.
+ // If we have a default engine or a list of visible default engines
+ // saved, the hashes should be valid, verify them now so that we can
+ // refetch if they have been tampered with.
+ let defaultEngine = ss.getVerifiedGlobalAttr("searchDefault");
+ let visibleDefaultEngines = ss.getVerifiedGlobalAttr("visibleDefaultEngines");
+ if ((defaultEngine || defaultEngine === undefined) &&
+ (visibleDefaultEngines || visibleDefaultEngines === undefined)) {
+ // No geo defaults, or valid hashes; nothing to do.
+ return;
+ }
+ }
+
+ yield new Promise(resolve => {
+ let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
+ let timerId = setTimeout(() => {
+ timerId = null;
+ resolve();
+ }, timeoutMS);
+
+ let callback = () => {
+ clearTimeout(timerId);
+ resolve();
+ };
+ fetchRegionDefault(ss).then(callback).catch(err => {
+ Components.utils.reportError(err);
+ callback();
+ });
+ });
+ }
+
+ // If gInitialized is true then the search service was forced to perform
+ // a sync initialization during our XHRs - capture this via telemetry.
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT").add(gInitialized);
+});
+
+// Store the result of the geoip request as well as any other values and
+// telemetry which depend on it.
+function storeCountryCode(cc) {
+ // Set the country-code itself.
+ Services.prefs.setCharPref("browser.search.countryCode", cc);
+ // And set the region pref if we don't already have a value.
+ if (!Services.prefs.prefHasUserValue("browser.search.region")) {
+ Services.prefs.setCharPref("browser.search.region", cc);
+ }
+ // and telemetry...
+ let isTimezoneUS = isUSTimezone();
+ if (cc == "US" && !isTimezoneUS) {
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE").add(1);
+ }
+ if (cc != "US" && isTimezoneUS) {
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY").add(1);
+ }
+ // telemetry to compare our geoip response with platform-specific country data.
+ // On Mac and Windows, we can get a country code via sysinfo
+ let platformCC = Services.sysinfo.get("countryCode");
+ if (platformCC) {
+ let probeUSMismatched, probeNonUSMismatched;
+ switch (Services.appinfo.OS) {
+ case "Darwin":
+ probeUSMismatched = "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX";
+ probeNonUSMismatched = "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX";
+ break;
+ case "WINNT":
+ probeUSMismatched = "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN";
+ probeNonUSMismatched = "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN";
+ break;
+ default:
+ Cu.reportError("Platform " + Services.appinfo.OS + " has system country code but no search service telemetry probes");
+ break;
+ }
+ if (probeUSMismatched && probeNonUSMismatched) {
+ if (cc == "US" || platformCC == "US") {
+ // one of the 2 said US, so record if they are the same.
+ Services.telemetry.getHistogramById(probeUSMismatched).add(cc != platformCC);
+ } else {
+ // different country - record if they are the same
+ Services.telemetry.getHistogramById(probeNonUSMismatched).add(cc != platformCC);
+ }
+ }
+ }
+}
+
+// Get the country we are in via a XHR geoip request.
+function fetchCountryCode(ss) {
+ // values for the SEARCH_SERVICE_COUNTRY_FETCH_RESULT 'enum' telemetry probe.
+ const TELEMETRY_RESULT_ENUM = {
+ SUCCESS: 0,
+ SUCCESS_WITHOUT_DATA: 1,
+ XHRTIMEOUT: 2,
+ ERROR: 3,
+ // Note that we expect to add finer-grained error types here later (eg,
+ // dns error, network error, ssl error, etc) with .ERROR remaining as the
+ // generic catch-all that doesn't fit into other categories.
+ };
+ let endpoint = Services.urlFormatter.formatURLPref("browser.search.geoip.url");
+ LOG("_fetchCountryCode starting with endpoint " + endpoint);
+ // As an escape hatch, no endpoint means no geoip.
+ if (!endpoint) {
+ return Promise.resolve();
+ }
+ let startTime = Date.now();
+ return new Promise(resolve => {
+ // Instead of using a timeout on the xhr object itself, we simulate one
+ // using a timer and let the XHR request complete. This allows us to
+ // capture reliable telemetry on what timeout value should actually be
+ // used to ensure most users don't see one while not making it so large
+ // that many users end up doing a sync init of the search service and thus
+ // would see the jank that implies.
+ // (Note we do actually use a timeout on the XHR, but that's set to be a
+ // large value just incase the request never completes - we don't want the
+ // XHR object to live forever)
+ let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
+ let geoipTimeoutPossible = true;
+ let timerId = setTimeout(() => {
+ LOG("_fetchCountryCode: timeout fetching country information");
+ if (geoipTimeoutPossible)
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
+ timerId = null;
+ resolve();
+ }, timeoutMS);
+
+ let resolveAndReportSuccess = (result, reason) => {
+ // Even if we timed out, we want to save the country code and everything
+ // related so next startup sees the value and doesn't retry this dance.
+ if (result) {
+ storeCountryCode(result);
+ }
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_RESULT").add(reason);
+
+ // This notification is just for tests...
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-complete");
+
+ if (timerId) {
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
+ geoipTimeoutPossible = false;
+ }
+
+ let callback = () => {
+ // If we've already timed out then we've already resolved the promise,
+ // so there's nothing else to do.
+ if (timerId == null) {
+ return;
+ }
+ clearTimeout(timerId);
+ resolve();
+ };
+
+ if (result && geoSpecificDefaultsEnabled()) {
+ fetchRegionDefault(ss).then(callback).catch(err => {
+ Components.utils.reportError(err);
+ callback();
+ });
+ } else {
+ callback();
+ }
+ };
+
+ let request = new XMLHttpRequest();
+ // This notification is just for tests...
+ Services.obs.notifyObservers(request, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-starting");
+ request.timeout = 100000; // 100 seconds as the last-chance fallback
+ request.onload = function(event) {
+ let took = Date.now() - startTime;
+ let cc = event.target.response && event.target.response.country_code;
+ LOG("_fetchCountryCode got success response in " + took + "ms: " + cc);
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS").add(took);
+ let reason = cc ? TELEMETRY_RESULT_ENUM.SUCCESS : TELEMETRY_RESULT_ENUM.SUCCESS_WITHOUT_DATA;
+ resolveAndReportSuccess(cc, reason);
+ };
+ request.ontimeout = function(event) {
+ LOG("_fetchCountryCode: XHR finally timed-out fetching country information");
+ resolveAndReportSuccess(null, TELEMETRY_RESULT_ENUM.XHRTIMEOUT);
+ };
+ request.onerror = function(event) {
+ LOG("_fetchCountryCode: failed to retrieve country information");
+ resolveAndReportSuccess(null, TELEMETRY_RESULT_ENUM.ERROR);
+ };
+ request.open("POST", endpoint, true);
+ request.setRequestHeader("Content-Type", "application/json");
+ request.responseType = "json";
+ request.send("{}");
+ });
+}
+
+// This will make an HTTP request to a Mozilla server that will return
+// JSON data telling us what engine should be set as the default for
+// the current region, and how soon we should check again.
+//
+// The optional cohort value returned by the server is to be kept locally
+// and sent to the server the next time we ping it. It lets the server
+// identify profiles that have been part of a specific experiment.
+//
+// This promise may take up to 100s to resolve, it's the caller's
+// responsibility to ensure with a timer that we are not going to
+// block the async init for too long.
+var fetchRegionDefault = (ss) => new Promise(resolve => {
+ let urlTemplate = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .getCharPref("geoSpecificDefaults.url");
+ let endpoint = Services.urlFormatter.formatURL(urlTemplate);
+
+ // As an escape hatch, no endpoint means no region specific defaults.
+ if (!endpoint) {
+ resolve();
+ return;
+ }
+
+ // Append the optional cohort value.
+ const cohortPref = "browser.search.cohort";
+ let cohort;
+ try {
+ cohort = Services.prefs.getCharPref(cohortPref);
+ } catch (e) {}
+ if (cohort)
+ endpoint += "/" + cohort;
+
+ LOG("fetchRegionDefault starting with endpoint " + endpoint);
+
+ let startTime = Date.now();
+ let request = new XMLHttpRequest();
+ request.timeout = 100000; // 100 seconds as the last-chance fallback
+ request.onload = function(event) {
+ let took = Date.now() - startTime;
+
+ let status = event.target.status;
+ if (status != 200) {
+ LOG("fetchRegionDefault failed with HTTP code " + status);
+ let retryAfter = request.getResponseHeader("retry-after");
+ if (retryAfter) {
+ ss.setGlobalAttr("searchDefaultExpir", Date.now() + retryAfter * 1000);
+ }
+ resolve();
+ return;
+ }
+
+ let response = event.target.response || {};
+ LOG("received " + response.toSource());
+
+ if (response.cohort) {
+ Services.prefs.setCharPref(cohortPref, response.cohort);
+ } else {
+ Services.prefs.clearUserPref(cohortPref);
+ }
+
+ if (response.settings && response.settings.searchDefault) {
+ let defaultEngine = response.settings.searchDefault;
+ ss.setVerifiedGlobalAttr("searchDefault", defaultEngine);
+ LOG("fetchRegionDefault saved searchDefault: " + defaultEngine);
+ }
+
+ if (response.settings && response.settings.visibleDefaultEngines) {
+ let visibleDefaultEngines = response.settings.visibleDefaultEngines;
+ let string = visibleDefaultEngines.join(",");
+ ss.setVerifiedGlobalAttr("visibleDefaultEngines", string);
+ LOG("fetchRegionDefault saved visibleDefaultEngines: " + string);
+ }
+
+ let interval = response.interval || SEARCH_GEO_DEFAULT_UPDATE_INTERVAL;
+ let milliseconds = interval * 1000; // |interval| is in seconds.
+ ss.setGlobalAttr("searchDefaultExpir", Date.now() + milliseconds);
+
+ LOG("fetchRegionDefault got success response in " + took + "ms");
+ resolve();
+ };
+ request.ontimeout = function(event) {
+ LOG("fetchRegionDefault: XHR finally timed-out");
+ resolve();
+ };
+ request.onerror = function(event) {
+ LOG("fetchRegionDefault: failed to retrieve territory default information");
+ resolve();
+ };
+ request.open("GET", endpoint, true);
+ request.setRequestHeader("Content-Type", "application/json");
+ request.responseType = "json";
+ request.send();
+});
+
+function getVerificationHash(aName) {
+ let disclaimer = "By modifying this file, I agree that I am doing so " +
+ "only within $appName itself, using official, user-driven search " +
+ "engine selection processes, and in a way which does not circumvent " +
+ "user consent. I acknowledge that any attempt to change this file " +
+ "from outside of $appName is a malicious act, and will be responded " +
+ "to accordingly."
+
+ let salt = OS.Path.basename(OS.Constants.Path.profileDir) + aName +
+ disclaimer.replace(/\$appName/g, Services.appinfo.name);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ // Data is an array of bytes.
+ let data = converter.convertToByteArray(salt, {});
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+}
+
+/**
+ * Safely close a nsISafeOutputStream.
+ * @param aFOS
+ * The file output stream to close.
+ */
+function closeSafeOutputStream(aFOS) {
+ if (aFOS instanceof Ci.nsISafeOutputStream) {
+ try {
+ aFOS.finish();
+ return;
+ } catch (e) { }
+ }
+ aFOS.close();
+}
+
+/**
+ * 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 NetUtil.newURI(aURLSpec, aCharset);
+ } catch (ex) { }
+
+ return null;
+}
+
+/**
+ * Wrapper function for nsIIOService::newChannel2.
+ * @param url
+ * The URL string from which to create an nsIChannel.
+ * @returns an nsIChannel object, or null if the url is invalid.
+ */
+function makeChannel(url) {
+ try {
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true
+ });
+ } catch (ex) { }
+
+ return null;
+}
+
+/**
+ * Gets a directory from the directory service.
+ * @param aKey
+ * The directory service key indicating the directory to get.
+ */
+function getDir(aKey, aIFace) {
+ if (!aKey)
+ FAIL("getDir requires a directory key!");
+
+ return Services.dirsvc.get(aKey, aIFace || Ci.nsIFile);
+}
+
+/**
+ * Gets the current value of the locale. It's possible for this preference to
+ * be localized, so we have to do a little extra work here. Similar code
+ * exists in nsHttpHandler.cpp when building the UA string.
+ */
+function getLocale() {
+ let locale = getLocalizedPref(LOCALE_PREF);
+ if (locale)
+ return locale;
+
+ // Not localized.
+ return Services.prefs.getCharPref(LOCALE_PREF);
+}
+
+/**
+ * Wrapper for nsIPrefBranch::getComplexValue.
+ * @param aPrefName
+ * The name of the pref to get.
+ * @returns aDefault if the requested pref doesn't exist.
+ */
+function getLocalizedPref(aPrefName, aDefault) {
+ const nsIPLS = Ci.nsIPrefLocalizedString;
+ try {
+ return Services.prefs.getComplexValue(aPrefName, nsIPLS).data;
+ } catch (ex) {}
+
+ return aDefault;
+}
+
+/**
+ * Wrapper for nsIPrefBranch::getBoolPref.
+ * @param aPrefName
+ * The name of the pref to get.
+ * @returns aDefault if the requested pref doesn't exist.
+ */
+function getBoolPref(aName, aDefault) {
+ if (Services.prefs.getPrefType(aName) != Ci.nsIPrefBranch.PREF_BOOL)
+ return aDefault;
+ return Services.prefs.getBoolPref(aName);
+}
+
+/**
+ * @return a sanitized name to be used as a filename, or a random name
+ * if a sanitized name cannot be obtained (if aName contains
+ * no valid characters).
+ */
+function sanitizeName(aName) {
+ const maxLength = 60;
+ const minLength = 1;
+ var name = aName.toLowerCase();
+ name = name.replace(/\s+/g, "-");
+ name = name.replace(/[^-a-z0-9]/g, "");
+
+ // Use a random name if our input had no valid characters.
+ if (name.length < minLength)
+ name = Math.random().toString(36).replace(/^.*\./, '');
+
+ // Force max length.
+ return name.substring(0, maxLength);
+}
+
+/**
+ * Retrieve a pref from the search param branch.
+ *
+ * @param prefName
+ * The name of the pref.
+ **/
+function getMozParamPref(prefName) {
+ let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF + "param.");
+ return encodeURIComponent(branch.getCharPref(prefName));
+}
+
+/**
+ * Notifies watchers of SEARCH_ENGINE_TOPIC about changes to an engine or to
+ * the state of the search service.
+ *
+ * @param aEngine
+ * The nsISearchEngine object to which the change applies.
+ * @param aVerb
+ * A verb describing the change.
+ *
+ * @see nsIBrowserSearchService.idl
+ */
+var gInitialized = false;
+function notifyAction(aEngine, aVerb) {
+ if (gInitialized) {
+ LOG("NOTIFY: Engine: \"" + aEngine.name + "\"; Verb: \"" + aVerb + "\"");
+ Services.obs.notifyObservers(aEngine, SEARCH_ENGINE_TOPIC, aVerb);
+ }
+}
+
+function parseJsonFromStream(aInputStream) {
+ const json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+ const data = json.decodeFromStream(aInputStream, aInputStream.available());
+ return data;
+}
+
+/**
+ * Simple object representing a name/value pair.
+ */
+function QueryParameter(aName, aValue, aPurpose) {
+ if (!aName || (aValue == null))
+ FAIL("missing name or value for QueryParameter!");
+
+ this.name = aName;
+ this.value = aValue;
+ this.purpose = aPurpose;
+}
+
+/**
+ * Perform OpenSearch parameter substitution on aParamValue.
+ *
+ * @param aParamValue
+ * A string containing OpenSearch search parameters.
+ * @param aSearchTerms
+ * The user-provided search terms. This string will inserted into
+ * aParamValue as the value of the OS_PARAM_USER_DEFINED parameter.
+ * This value must already be escaped appropriately - it is inserted
+ * as-is.
+ * @param aEngine
+ * The engine which owns the string being acted on.
+ *
+ * @see http://opensearch.a9.com/spec/1.1/querysyntax/#core
+ */
+function ParamSubstitution(aParamValue, aSearchTerms, aEngine) {
+ var value = aParamValue;
+
+ var distributionID = Services.appinfo.distributionID;
+ try {
+ distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID");
+ }
+ catch (ex) { }
+ var official = MOZ_OFFICIAL;
+ try {
+ if (Services.prefs.getBoolPref(BROWSER_SEARCH_PREF + "official"))
+ official = "official";
+ else
+ official = "unofficial";
+ }
+ catch (ex) { }
+
+ // Custom search parameters. These are only available to default search
+ // engines.
+ if (aEngine._isDefault) {
+ value = value.replace(MOZ_PARAM_LOCALE, getLocale());
+ value = value.replace(MOZ_PARAM_DIST_ID, distributionID);
+ value = value.replace(MOZ_PARAM_OFFICIAL, official);
+ }
+
+ // Insert the OpenSearch parameters we're confident about
+ value = value.replace(OS_PARAM_USER_DEFINED, aSearchTerms);
+ value = value.replace(OS_PARAM_INPUT_ENCODING, aEngine.queryCharset);
+ value = value.replace(OS_PARAM_LANGUAGE,
+ getLocale() || OS_PARAM_LANGUAGE_DEF);
+ value = value.replace(OS_PARAM_OUTPUT_ENCODING,
+ OS_PARAM_OUTPUT_ENCODING_DEF);
+
+ // Replace any optional parameters
+ value = value.replace(OS_PARAM_OPTIONAL, "");
+
+ // Insert any remaining required params with our default values
+ for (var i = 0; i < OS_UNSUPPORTED_PARAMS.length; ++i) {
+ value = value.replace(OS_UNSUPPORTED_PARAMS[i][0],
+ OS_UNSUPPORTED_PARAMS[i][1]);
+ }
+
+ return value;
+}
+
+/**
+ * Creates an engineURL object, which holds the query URL and all parameters.
+ *
+ * @param aType
+ * A string containing the name of the MIME type of the search results
+ * returned by this URL.
+ * @param aMethod
+ * The HTTP request method. Must be a case insensitive value of either
+ * "GET" or "POST".
+ * @param aTemplate
+ * The URL to which search queries should be sent. For GET requests,
+ * must contain the string "{searchTerms}", to indicate where the user
+ * entered search terms should be inserted.
+ * @param aResultDomain
+ * The root domain for this URL. Defaults to the template's host.
+ *
+ * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag
+ *
+ * @throws NS_ERROR_NOT_IMPLEMENTED if aType is unsupported.
+ */
+function EngineURL(aType, aMethod, aTemplate, aResultDomain) {
+ if (!aType || !aMethod || !aTemplate)
+ FAIL("missing type, method or template for EngineURL!");
+
+ var method = aMethod.toUpperCase();
+ var type = aType.toLowerCase();
+
+ if (method != "GET" && method != "POST")
+ FAIL("method passed to EngineURL must be \"GET\" or \"POST\"");
+
+ this.type = type;
+ this.method = method;
+ this.params = [];
+ this.rels = [];
+ // Don't serialize expanded mozparams
+ this.mozparams = {};
+
+ var templateURI = makeURI(aTemplate);
+ if (!templateURI)
+ FAIL("new EngineURL: template is not a valid URI!", Cr.NS_ERROR_FAILURE);
+
+ switch (templateURI.scheme) {
+ case "http":
+ case "https":
+ // Disable these for now, see bug 295018
+ // case "file":
+ // case "resource":
+ this.template = aTemplate;
+ break;
+ default:
+ FAIL("new EngineURL: template uses invalid scheme!", Cr.NS_ERROR_FAILURE);
+ }
+
+ // If no resultDomain was specified in the engine definition file, use the
+ // host from the template.
+ this.resultDomain = aResultDomain || templateURI.host;
+ // We never want to return a "www." prefix, so eventually strip it.
+ if (this.resultDomain.startsWith("www.")) {
+ this.resultDomain = this.resultDomain.substr(4);
+ }
+}
+EngineURL.prototype = {
+
+ addParam: function SRCH_EURL_addParam(aName, aValue, aPurpose) {
+ this.params.push(new QueryParameter(aName, aValue, aPurpose));
+ },
+
+ // Note: This method requires that aObj has a unique name or the previous MozParams entry with
+ // that name will be overwritten.
+ _addMozParam: function SRCH_EURL__addMozParam(aObj) {
+ aObj.mozparam = true;
+ this.mozparams[aObj.name] = aObj;
+ },
+
+ getSubmission: function SRCH_EURL_getSubmission(aSearchTerms, aEngine, aPurpose) {
+ var url = ParamSubstitution(this.template, aSearchTerms, aEngine);
+ // Default to searchbar if the purpose is not provided
+ var purpose = aPurpose || "searchbar";
+
+ // If a particular purpose isn't defined in the plugin, fallback to 'searchbar'.
+ if (!this.params.some(p => p.purpose !== undefined && p.purpose == purpose))
+ purpose = "searchbar";
+
+ // Create an application/x-www-form-urlencoded representation of our params
+ // (name=value&name=value&name=value)
+ var dataString = "";
+ for (var i = 0; i < this.params.length; ++i) {
+ var param = this.params[i];
+
+ // If this parameter has a purpose, only add it if the purpose matches
+ if (param.purpose !== undefined && param.purpose != purpose)
+ continue;
+
+ var value = ParamSubstitution(param.value, aSearchTerms, aEngine);
+
+ dataString += (i > 0 ? "&" : "") + param.name + "=" + value;
+ }
+
+ var postData = null;
+ if (this.method == "GET") {
+ // GET method requests have no post data, and append the encoded
+ // query string to the url...
+ if (url.indexOf("?") == -1 && dataString)
+ url += "?";
+ url += dataString;
+ } else if (this.method == "POST") {
+ // POST method requests must wrap the encoded text in a MIME
+ // stream and supply that as POSTDATA.
+ var stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = dataString;
+
+ postData = Cc["@mozilla.org/network/mime-input-stream;1"].
+ createInstance(Ci.nsIMIMEInputStream);
+ postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
+ postData.addContentLength = true;
+ postData.setData(stringStream);
+ }
+
+ return new Submission(makeURI(url), postData);
+ },
+
+ _getTermsParameterName: function SRCH_EURL__getTermsParameterName() {
+ let queryParam = this.params.find(p => p.value == USER_DEFINED);
+ return queryParam ? queryParam.name : "";
+ },
+
+ _hasRelation: function SRC_EURL__hasRelation(aRel) {
+ return this.rels.some(e => e == aRel.toLowerCase());
+ },
+
+ _initWithJSON: function SRC_EURL__initWithJSON(aJson, aEngine) {
+ if (!aJson.params)
+ return;
+
+ this.rels = aJson.rels;
+
+ for (let i = 0; i < aJson.params.length; ++i) {
+ let param = aJson.params[i];
+ if (param.mozparam) {
+ if (param.condition == "pref") {
+ let value = getMozParamPref(param.pref);
+ this.addParam(param.name, value);
+ }
+ this._addMozParam(param);
+ }
+ else
+ this.addParam(param.name, param.value, param.purpose || undefined);
+ }
+ },
+
+ /**
+ * Creates a JavaScript object that represents this URL.
+ * @returns An object suitable for serialization as JSON.
+ **/
+ toJSON: function SRCH_EURL_toJSON() {
+ var json = {
+ template: this.template,
+ rels: this.rels,
+ resultDomain: this.resultDomain
+ };
+
+ if (this.type != URLTYPE_SEARCH_HTML)
+ json.type = this.type;
+ if (this.method != "GET")
+ json.method = this.method;
+
+ function collapseMozParams(aParam) {
+ return this.mozparams[aParam.name] || aParam;
+ }
+ json.params = this.params.map(collapseMozParams, this);
+
+ return json;
+ }
+};
+
+/**
+ * nsISearchEngine constructor.
+ * @param aLocation
+ * A nsILocalFile or nsIURI object representing the location of the
+ * search engine data file.
+ * @param aIsReadOnly
+ * Boolean indicating whether the engine should be treated as read-only.
+ */
+function Engine(aLocation, aIsReadOnly) {
+ this._readOnly = aIsReadOnly;
+ this._urls = [];
+ this._metaData = {};
+
+ let file, uri;
+ if (typeof aLocation == "string") {
+ this._shortName = aLocation;
+ } else if (aLocation instanceof Ci.nsILocalFile) {
+ if (!aIsReadOnly) {
+ // This is an engine that was installed in NS_APP_USER_SEARCH_DIR by a
+ // previous version. We are converting the file to an engine stored only
+ // in JSON, but we need to keep the reference to the profile file to
+ // remove it if the user ever removes the engine.
+ this._filePath = aLocation.persistentDescriptor;
+ }
+ file = aLocation;
+ } else if (aLocation instanceof Ci.nsIURI) {
+ switch (aLocation.scheme) {
+ case "https":
+ case "http":
+ case "ftp":
+ case "data":
+ case "file":
+ case "resource":
+ case "chrome":
+ uri = aLocation;
+ break;
+ default:
+ ERROR("Invalid URI passed to the nsISearchEngine constructor",
+ Cr.NS_ERROR_INVALID_ARG);
+ }
+ } else
+ ERROR("Engine location is neither a File nor a URI object",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!this._shortName) {
+ // If we don't have a shortName at this point, it's the first time we load
+ // this engine, so let's generate the shortName, id and loadPath values.
+ let shortName;
+ if (file) {
+ shortName = file.leafName;
+ }
+ else if (uri && uri instanceof Ci.nsIURL) {
+ if (aIsReadOnly || (gEnvironment.get("XPCSHELL_TEST_PROFILE_DIR") &&
+ uri.scheme == "resource")) {
+ shortName = uri.fileName;
+ }
+ }
+ if (shortName && shortName.endsWith(".xml")) {
+ this._shortName = shortName.slice(0, -4);
+ }
+ this._loadPath = this.getAnonymizedLoadPath(file, uri);
+
+ if (!shortName && !aIsReadOnly) {
+ // We are in the process of downloading and installing the engine.
+ // We'll have the shortName and id once we are done parsing it.
+ return;
+ }
+
+ // Build the id used for the legacy metadata storage, so that we
+ // can do a one-time import of data from old profiles.
+ if (this._isDefault ||
+ (uri && uri.spec.startsWith(APP_SEARCH_PREFIX))) {
+ // The second part of the check is to catch engines from language packs.
+ // They aren't default engines (because they aren't app-shipped), but we
+ // still need to give their id an [app] prefix for backward compat.
+ this._id = "[app]/" + this._shortName + ".xml";
+ }
+ else if (!aIsReadOnly) {
+ this._id = "[profile]/" + this._shortName + ".xml";
+ }
+ else {
+ // If the engine is neither a default one, nor a user-installed one,
+ // it must be extension-shipped, so use the full path as id.
+ LOG("Setting _id to full path for engine from " + this._loadPath);
+ this._id = file ? file.path : uri.spec;
+ }
+ }
+}
+
+Engine.prototype = {
+ // Data set by the user.
+ _metaData: null,
+ // The data describing the engine, in the form of an XML document element.
+ _data: null,
+ // Whether or not the engine is readonly.
+ _readOnly: true,
+ // Anonymized path of where we initially loaded the engine from.
+ // This will stay null for engines installed in the profile before we moved
+ // to a JSON storage.
+ _loadPath: null,
+ // The engine's description
+ _description: "",
+ // Used to store the engine to replace, if we're an update to an existing
+ // engine.
+ _engineToUpdate: null,
+ // Set to true if the engine has a preferred icon (an icon that should not be
+ // overridden by a non-preferred icon).
+ _hasPreferredIcon: null,
+ // The engine's name.
+ _name: null,
+ // The name of the charset used to submit the search terms.
+ _queryCharset: null,
+ // The engine's raw SearchForm value (URL string pointing to a search form).
+ __searchForm: null,
+ get _searchForm() {
+ return this.__searchForm;
+ },
+ set _searchForm(aValue) {
+ if (/^https?:/i.test(aValue))
+ this.__searchForm = aValue;
+ else
+ LOG("_searchForm: Invalid URL dropped for " + this._name ||
+ "the current engine");
+ },
+ // Whether to obtain user confirmation before adding the engine. This is only
+ // used when the engine is first added to the list.
+ _confirm: false,
+ // Whether to set this as the current engine as soon as it is loaded. This
+ // is only used when the engine is first added to the list.
+ _useNow: false,
+ // A function to be invoked when this engine object's addition completes (or
+ // fails). Only used for installation via addEngine.
+ _installCallback: null,
+ // The number of days between update checks for new versions
+ _updateInterval: null,
+ // The url to check at for a new update
+ _updateURL: null,
+ // The url to check for a new icon
+ _iconUpdateURL: null,
+ /* The extension ID if added by an extension. */
+ _extensionID: null,
+
+ /**
+ * Retrieves the data from the engine's file.
+ * The document element is placed in the engine's data field.
+ */
+ _initFromFile: function SRCH_ENG_initFromFile(file) {
+ if (!file || !file.exists())
+ FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
+
+ var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+
+ fileInStream.init(file, MODE_RDONLY, FileUtils.PERMS_FILE, false);
+
+ var domParser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = domParser.parseFromStream(fileInStream, "UTF-8",
+ file.fileSize,
+ "text/xml");
+
+ this._data = doc.documentElement;
+ fileInStream.close();
+
+ // Now that the data is loaded, initialize the engine object
+ this._initFromData();
+ },
+
+ /**
+ * Retrieves the data from the engine's file asynchronously.
+ * The document element is placed in the engine's data field.
+ *
+ * @param file The file to load the search plugin from.
+ *
+ * @returns {Promise} A promise, resolved successfully if initializing from
+ * data succeeds, rejected if it fails.
+ */
+ _asyncInitFromFile: Task.async(function* (file) {
+ if (!file || !(yield OS.File.exists(file.path)))
+ FAIL("File must exist before calling initFromFile!", Cr.NS_ERROR_UNEXPECTED);
+
+ let fileURI = NetUtil.ioService.newFileURI(file);
+ yield this._retrieveSearchXMLData(fileURI.spec);
+
+ // Now that the data is loaded, initialize the engine object
+ this._initFromData();
+ }),
+
+ /**
+ * Retrieves the engine data from a URI. Initializes the engine, flushes to
+ * disk, and notifies the search service once initialization is complete.
+ *
+ * @param uri The uri to load the search plugin from.
+ */
+ _initFromURIAndLoad: function SRCH_ENG_initFromURIAndLoad(uri) {
+ ENSURE_WARN(uri instanceof Ci.nsIURI,
+ "Must have URI when calling _initFromURIAndLoad!",
+ Cr.NS_ERROR_UNEXPECTED);
+
+ LOG("_initFromURIAndLoad: Downloading engine from: \"" + uri.spec + "\".");
+
+ var chan = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true
+ });
+
+ if (this._engineToUpdate && (chan instanceof Ci.nsIHttpChannel)) {
+ var lastModified = this._engineToUpdate.getAttr("updatelastmodified");
+ if (lastModified)
+ chan.setRequestHeader("If-Modified-Since", lastModified, false);
+ }
+ this._uri = uri;
+ var listener = new loadListener(chan, this, this._onLoad);
+ chan.notificationCallbacks = listener;
+ chan.asyncOpen2(listener);
+ },
+
+ /**
+ * Retrieves the engine data from a URI asynchronously and initializes it.
+ *
+ * @param uri The uri to load the search plugin from.
+ *
+ * @returns {Promise} A promise, resolved successfully if retrieveing data
+ * succeeds.
+ */
+ _asyncInitFromURI: Task.async(function* (uri) {
+ LOG("_asyncInitFromURI: Loading engine from: \"" + uri.spec + "\".");
+ yield this._retrieveSearchXMLData(uri.spec);
+ // Now that the data is loaded, initialize the engine object
+ this._initFromData();
+ }),
+
+ /**
+ * Retrieves the engine data for a given URI asynchronously.
+ *
+ * @returns {Promise} A promise, resolved successfully if retrieveing data
+ * succeeds.
+ */
+ _retrieveSearchXMLData: function SRCH_ENG__retrieveSearchXMLData(aURL) {
+ let deferred = Promise.defer();
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ request.overrideMimeType("text/xml");
+ request.onload = (aEvent) => {
+ let responseXML = aEvent.target.responseXML;
+ this._data = responseXML.documentElement;
+ deferred.resolve();
+ };
+ request.onerror = function(aEvent) {
+ deferred.resolve();
+ };
+ request.open("GET", aURL, true);
+ request.send();
+
+ return deferred.promise;
+ },
+
+ _initFromURISync: function SRCH_ENG_initFromURISync(uri) {
+ ENSURE_WARN(uri instanceof Ci.nsIURI,
+ "Must have URI when calling _initFromURISync!",
+ Cr.NS_ERROR_UNEXPECTED);
+
+ ENSURE_WARN(uri.schemeIs("resource"), "_initFromURISync called for non-resource URI",
+ Cr.NS_ERROR_FAILURE);
+
+ LOG("_initFromURISync: Loading engine from: \"" + uri.spec + "\".");
+
+ var chan = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true
+ });
+
+ var stream = chan.open2();
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromStream(stream, "UTF-8", stream.available(), "text/xml");
+
+ this._data = doc.documentElement;
+
+ // Now that the data is loaded, initialize the engine object
+ this._initFromData();
+ },
+
+ /**
+ * Attempts to find an EngineURL object in the set of EngineURLs for
+ * this Engine that has the given type string. (This corresponds to the
+ * "type" attribute in the "Url" node in the OpenSearch spec.)
+ * This method will return the first matching URL object found, or null
+ * if no matching URL is found.
+ *
+ * @param aType string to match the EngineURL's type attribute
+ * @param aRel [optional] only return URLs that with this rel value
+ */
+ _getURLOfType: function SRCH_ENG__getURLOfType(aType, aRel) {
+ for (var i = 0; i < this._urls.length; ++i) {
+ if (this._urls[i].type == aType && (!aRel || this._urls[i]._hasRelation(aRel)))
+ return this._urls[i];
+ }
+
+ return null;
+ },
+
+ _confirmAddEngine: function SRCH_SVC_confirmAddEngine() {
+ var stringBundle = Services.strings.createBundle(SEARCH_BUNDLE);
+ var titleMessage = stringBundle.GetStringFromName("addEngineConfirmTitle");
+
+ // Display only the hostname portion of the URL.
+ var dialogMessage =
+ stringBundle.formatStringFromName("addEngineConfirmation",
+ [this._name, this._uri.host], 2);
+ var checkboxMessage = null;
+ if (!getBoolPref(BROWSER_SEARCH_PREF + "noCurrentEngine", false))
+ checkboxMessage = stringBundle.GetStringFromName("addEngineAsCurrentText");
+
+ var addButtonLabel =
+ stringBundle.GetStringFromName("addEngineAddButtonLabel");
+
+ var ps = Services.prompt;
+ var buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
+ (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1) +
+ ps.BUTTON_POS_0_DEFAULT;
+
+ var checked = {value: false};
+ // confirmEx returns the index of the button that was pressed. Since "Add"
+ // is button 0, we want to return the negation of that value.
+ var confirm = !ps.confirmEx(null,
+ titleMessage,
+ dialogMessage,
+ buttonFlags,
+ addButtonLabel,
+ null, null, // button 1 & 2 names not used
+ checkboxMessage,
+ checked);
+
+ return {confirmed: confirm, useNow: checked.value};
+ },
+
+ /**
+ * Handle the successful download of an engine. Initializes the engine and
+ * triggers parsing of the data. The engine is then flushed to disk. Notifies
+ * the search service once initialization is complete.
+ */
+ _onLoad: function SRCH_ENG_onLoad(aBytes, aEngine) {
+ /**
+ * Handle an error during the load of an engine by notifying the engine's
+ * error callback, if any.
+ */
+ function onError(errorCode = Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE) {
+ // Notify the callback of the failure
+ if (aEngine._installCallback) {
+ aEngine._installCallback(errorCode);
+ }
+ }
+
+ function promptError(strings = {}, error = undefined) {
+ onError(error);
+
+ if (aEngine._engineToUpdate) {
+ // We're in an update, so just fail quietly
+ LOG("updating " + aEngine._engineToUpdate.name + " failed");
+ return;
+ }
+ var brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
+ var brandName = brandBundle.GetStringFromName("brandShortName");
+
+ var searchBundle = Services.strings.createBundle(SEARCH_BUNDLE);
+ var msgStringName = strings.error || "error_loading_engine_msg2";
+ var titleStringName = strings.title || "error_loading_engine_title";
+ var title = searchBundle.GetStringFromName(titleStringName);
+ var text = searchBundle.formatStringFromName(msgStringName,
+ [brandName, aEngine._location],
+ 2);
+
+ Services.ww.getNewPrompter(null).alert(title, text);
+ }
+
+ if (!aBytes) {
+ promptError();
+ return;
+ }
+
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml");
+ aEngine._data = doc.documentElement;
+
+ try {
+ // Initialize the engine from the obtained data
+ aEngine._initFromData();
+ } catch (ex) {
+ LOG("_onLoad: Failed to init engine!\n" + ex);
+ // Report an error to the user
+ promptError();
+ return;
+ }
+
+ if (aEngine._engineToUpdate) {
+ let engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
+
+ // Make this new engine use the old engine's shortName, and preserve
+ // metadata.
+ aEngine._shortName = engineToUpdate._shortName;
+ Object.keys(engineToUpdate._metaData).forEach(key => {
+ aEngine.setAttr(key, engineToUpdate.getAttr(key));
+ });
+ aEngine._loadPath = engineToUpdate._loadPath;
+
+ // Keep track of the last modified date, so that we can make conditional
+ // requests for future updates.
+ aEngine.setAttr("updatelastmodified", (new Date()).toUTCString());
+
+ // Set the new engine's icon, if it doesn't yet have one.
+ if (!aEngine._iconURI && engineToUpdate._iconURI)
+ aEngine._iconURI = engineToUpdate._iconURI;
+ } else {
+ // Check that when adding a new engine (e.g., not updating an
+ // existing one), a duplicate engine does not already exist.
+ if (Services.search.getEngineByName(aEngine.name)) {
+ // If we're confirming the engine load, then display a "this is a
+ // duplicate engine" prompt; otherwise, fail silently.
+ if (aEngine._confirm) {
+ promptError({ error: "error_duplicate_engine_msg",
+ title: "error_invalid_engine_title"
+ }, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
+ } else {
+ onError(Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
+ }
+ LOG("_onLoad: duplicate engine found, bailing");
+ return;
+ }
+
+ // If requested, confirm the addition now that we have the title.
+ // This property is only ever true for engines added via
+ // nsIBrowserSearchService::addEngine.
+ if (aEngine._confirm) {
+ var confirmation = aEngine._confirmAddEngine();
+ LOG("_onLoad: confirm is " + confirmation.confirmed +
+ "; useNow is " + confirmation.useNow);
+ if (!confirmation.confirmed) {
+ onError();
+ return;
+ }
+ aEngine._useNow = confirmation.useNow;
+ }
+
+ aEngine._shortName = sanitizeName(aEngine.name);
+ aEngine._loadPath = aEngine.getAnonymizedLoadPath(null, aEngine._uri);
+ aEngine.setAttr("loadPathHash", getVerificationHash(aEngine._loadPath));
+ }
+
+ // Notify the search service of the successful load. It will deal with
+ // updates by checking aEngine._engineToUpdate.
+ notifyAction(aEngine, SEARCH_ENGINE_LOADED);
+
+ // Notify the callback if needed
+ if (aEngine._installCallback) {
+ aEngine._installCallback();
+ }
+ },
+
+ /**
+ * Creates a key by serializing an object that contains the icon's width
+ * and height.
+ *
+ * @param aWidth
+ * Width of the icon.
+ * @param aHeight
+ * Height of the icon.
+ * @returns key string
+ */
+ _getIconKey: function SRCH_ENG_getIconKey(aWidth, aHeight) {
+ let keyObj = {
+ width: aWidth,
+ height: aHeight
+ };
+
+ return JSON.stringify(keyObj);
+ },
+
+ /**
+ * Add an icon to the icon map used by getIconURIBySize() and getIcons().
+ *
+ * @param aWidth
+ * Width of the icon.
+ * @param aHeight
+ * Height of the icon.
+ * @param aURISpec
+ * String with the icon's URI.
+ */
+ _addIconToMap: function SRCH_ENG_addIconToMap(aWidth, aHeight, aURISpec) {
+ if (aWidth == 16 && aHeight == 16) {
+ // The 16x16 icon is stored in _iconURL, we don't need to store it twice.
+ return;
+ }
+
+ // Use an object instead of a Map() because it needs to be serializable.
+ this._iconMapObj = this._iconMapObj || {};
+ let key = this._getIconKey(aWidth, aHeight);
+ this._iconMapObj[key] = aURISpec;
+ },
+
+ /**
+ * Sets the .iconURI property of the engine. If both aWidth and aHeight are
+ * provided an entry will be added to _iconMapObj that will enable accessing
+ * icon's data through getIcons() and getIconURIBySize() APIs.
+ *
+ * @param aIconURL
+ * A URI string pointing to the engine's icon. Must have a http[s],
+ * ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be
+ * downloaded and converted to data URIs for storage in the engine
+ * XML files, if the engine is not readonly.
+ * @param aIsPreferred
+ * Whether or not this icon is to be preferred. Preferred icons can
+ * override non-preferred icons.
+ * @param aWidth (optional)
+ * Width of the icon.
+ * @param aHeight (optional)
+ * Height of the icon.
+ */
+ _setIcon: function SRCH_ENG_setIcon(aIconURL, aIsPreferred, aWidth, aHeight) {
+ var uri = makeURI(aIconURL);
+
+ // Ignore bad URIs
+ if (!uri)
+ return;
+
+ LOG("_setIcon: Setting icon url \"" + limitURILength(uri.spec) + "\" for engine \""
+ + this.name + "\".");
+ // Only accept remote icons from http[s] or ftp
+ switch (uri.scheme) {
+ case "resource":
+ case "chrome":
+ // We only allow chrome and resource icon URLs for built-in search engines
+ if (!this._isDefault) {
+ return;
+ }
+ // Fall through to the data case
+ case "data":
+ if (!this._hasPreferredIcon || aIsPreferred) {
+ this._iconURI = uri;
+ notifyAction(this, SEARCH_ENGINE_CHANGED);
+ this._hasPreferredIcon = aIsPreferred;
+ }
+
+ if (aWidth && aHeight) {
+ this._addIconToMap(aWidth, aHeight, aIconURL)
+ }
+ break;
+ case "http":
+ case "https":
+ case "ftp":
+ LOG("_setIcon: Downloading icon: \"" + uri.spec +
+ "\" for engine: \"" + this.name + "\"");
+ var chan = NetUtil.newChannel({
+ uri: uri,
+ loadUsingSystemPrincipal: true
+ });
+
+ let iconLoadCallback = function (aByteArray, aEngine) {
+ // This callback may run after we've already set a preferred icon,
+ // so check again.
+ if (aEngine._hasPreferredIcon && !aIsPreferred)
+ return;
+
+ if (!aByteArray || aByteArray.length > MAX_ICON_SIZE) {
+ LOG("iconLoadCallback: load failed, or the icon was too large!");
+ return;
+ }
+
+ let type = chan.contentType;
+ if (!type.startsWith("image/"))
+ type = "image/x-icon";
+ let dataURL = "data:" + type + ";base64," +
+ btoa(String.fromCharCode.apply(null, aByteArray));
+
+ aEngine._iconURI = makeURI(dataURL);
+
+ if (aWidth && aHeight) {
+ aEngine._addIconToMap(aWidth, aHeight, dataURL)
+ }
+
+ notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
+ aEngine._hasPreferredIcon = aIsPreferred;
+ };
+
+ // If we're currently acting as an "update engine", then the callback
+ // should set the icon on the engine we're updating and not us, since
+ // |this| might be gone by the time the callback runs.
+ var engineToSet = this._engineToUpdate || this;
+
+ var listener = new loadListener(chan, engineToSet, iconLoadCallback);
+ chan.notificationCallbacks = listener;
+ chan.asyncOpen2(listener);
+ break;
+ }
+ },
+
+ /**
+ * Initialize this Engine object from the collected data.
+ */
+ _initFromData: function SRCH_ENG_initFromData() {
+ ENSURE_WARN(this._data, "Can't init an engine with no data!",
+ Cr.NS_ERROR_UNEXPECTED);
+
+ // Ensure we have a supported engine type before attempting to parse it.
+ let element = this._data;
+ if ((element.localName == MOZSEARCH_LOCALNAME &&
+ element.namespaceURI == MOZSEARCH_NS_10) ||
+ (element.localName == OPENSEARCH_LOCALNAME &&
+ OPENSEARCH_NAMESPACES.indexOf(element.namespaceURI) != -1)) {
+ LOG("_init: Initing search plugin from " + this._location);
+
+ this._parse();
+
+ } else
+ FAIL(this._location + " is not a valid search plugin.", Cr.NS_ERROR_FAILURE);
+
+ // No need to keep a ref to our data (which in some cases can be a document
+ // element) past this point
+ this._data = null;
+ },
+
+ /**
+ * Initialize this Engine object from a collection of metadata.
+ */
+ _initFromMetadata: function SRCH_ENG_initMetaData(aName, aIconURL, aAlias,
+ aDescription, aMethod,
+ aTemplate, aExtensionID) {
+ ENSURE_WARN(!this._readOnly,
+ "Can't call _initFromMetaData on a readonly engine!",
+ Cr.NS_ERROR_FAILURE);
+
+ this._urls.push(new EngineURL(URLTYPE_SEARCH_HTML, aMethod, aTemplate));
+
+ this._name = aName;
+ this.alias = aAlias;
+ this._description = aDescription;
+ this._setIcon(aIconURL, true);
+ this._extensionID = aExtensionID;
+ },
+
+ /**
+ * Extracts data from an OpenSearch URL element and creates an EngineURL
+ * object which is then added to the engine's list of URLs.
+ *
+ * @throws NS_ERROR_FAILURE if a URL object could not be created.
+ *
+ * @see http://opensearch.a9.com/spec/1.1/querysyntax/#urltag.
+ * @see EngineURL()
+ */
+ _parseURL: function SRCH_ENG_parseURL(aElement) {
+ var type = aElement.getAttribute("type");
+ // According to the spec, method is optional, defaulting to "GET" if not
+ // specified
+ var method = aElement.getAttribute("method") || "GET";
+ var template = aElement.getAttribute("template");
+ var resultDomain = aElement.getAttribute("resultdomain");
+
+ try {
+ var url = new EngineURL(type, method, template, resultDomain);
+ } catch (ex) {
+ FAIL("_parseURL: failed to add " + template + " as a URL",
+ Cr.NS_ERROR_FAILURE);
+ }
+
+ if (aElement.hasAttribute("rel"))
+ url.rels = aElement.getAttribute("rel").toLowerCase().split(/\s+/);
+
+ for (var i = 0; i < aElement.childNodes.length; ++i) {
+ var param = aElement.childNodes[i];
+ if (param.localName == "Param") {
+ try {
+ url.addParam(param.getAttribute("name"), param.getAttribute("value"));
+ } catch (ex) {
+ // Ignore failure
+ LOG("_parseURL: Url element has an invalid param");
+ }
+ } else if (param.localName == "MozParam" &&
+ // We only support MozParams for default search engines
+ this._isDefault) {
+ var value;
+ let condition = param.getAttribute("condition");
+
+ // MozParams must have a condition to be valid
+ if (!condition) {
+ let engineLoc = this._location;
+ let paramName = param.getAttribute("name");
+ LOG("_parseURL: MozParam (" + paramName + ") without a condition attribute found parsing engine: " + engineLoc);
+ continue;
+ }
+
+ switch (condition) {
+ case "purpose":
+ url.addParam(param.getAttribute("name"),
+ param.getAttribute("value"),
+ param.getAttribute("purpose"));
+ // _addMozParam is not needed here since it can be serialized fine without. _addMozParam
+ // also requires a unique "name" which is not normally the case when @purpose is used.
+ break;
+ case "pref":
+ try {
+ value = getMozParamPref(param.getAttribute("pref"), value);
+ url.addParam(param.getAttribute("name"), value);
+ url._addMozParam({"pref": param.getAttribute("pref"),
+ "name": param.getAttribute("name"),
+ "condition": "pref"});
+ } catch (e) { }
+ break;
+ default:
+ let engineLoc = this._location;
+ let paramName = param.getAttribute("name");
+ LOG("_parseURL: MozParam (" + paramName + ") has an unknown condition: " + condition + ". Found parsing engine: " + engineLoc);
+ break;
+ }
+ }
+ }
+
+ this._urls.push(url);
+ },
+
+ /**
+ * Get the icon from an OpenSearch Image element.
+ * @see http://opensearch.a9.com/spec/1.1/description/#image
+ */
+ _parseImage: function SRCH_ENG_parseImage(aElement) {
+ LOG("_parseImage: Image textContent: \"" + limitURILength(aElement.textContent) + "\"");
+
+ let width = parseInt(aElement.getAttribute("width"), 10);
+ let height = parseInt(aElement.getAttribute("height"), 10);
+ let isPrefered = width == 16 && height == 16;
+
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <=0) {
+ LOG("OpenSearch image element must have positive width and height.");
+ return;
+ }
+
+ this._setIcon(aElement.textContent, isPrefered, width, height);
+ },
+
+ /**
+ * Extract search engine information from the collected data to initialize
+ * the engine object.
+ */
+ _parse: function SRCH_ENG_parse() {
+ var doc = this._data;
+
+ // The OpenSearch spec sets a default value for the input encoding.
+ this._queryCharset = OS_PARAM_INPUT_ENCODING_DEF;
+
+ for (var i = 0; i < doc.childNodes.length; ++i) {
+ var child = doc.childNodes[i];
+ switch (child.localName) {
+ case "ShortName":
+ this._name = child.textContent;
+ break;
+ case "Description":
+ this._description = child.textContent;
+ break;
+ case "Url":
+ try {
+ this._parseURL(child);
+ } catch (ex) {
+ // Parsing of the element failed, just skip it.
+ LOG("_parse: failed to parse URL child: " + ex);
+ }
+ break;
+ case "Image":
+ this._parseImage(child);
+ break;
+ case "InputEncoding":
+ this._queryCharset = child.textContent.toUpperCase();
+ break;
+
+ // Non-OpenSearch elements
+ case "SearchForm":
+ this._searchForm = child.textContent;
+ break;
+ case "UpdateUrl":
+ this._updateURL = child.textContent;
+ break;
+ case "UpdateInterval":
+ this._updateInterval = parseInt(child.textContent);
+ break;
+ case "IconUpdateUrl":
+ this._iconUpdateURL = child.textContent;
+ break;
+ case "ExtensionID":
+ this._extensionID = child.textContent;
+ break;
+ }
+ }
+ if (!this.name || (this._urls.length == 0))
+ FAIL("_parse: No name, or missing URL!", Cr.NS_ERROR_FAILURE);
+ if (!this.supportsResponseType(URLTYPE_SEARCH_HTML))
+ FAIL("_parse: No text/html result type!", Cr.NS_ERROR_FAILURE);
+ },
+
+ /**
+ * Init from a JSON record.
+ **/
+ _initWithJSON: function SRCH_ENG__initWithJSON(aJson) {
+ this._name = aJson._name;
+ this._shortName = aJson._shortName;
+ this._loadPath = aJson._loadPath;
+ this._description = aJson.description;
+ this._hasPreferredIcon = aJson._hasPreferredIcon == undefined;
+ this._queryCharset = aJson.queryCharset || DEFAULT_QUERY_CHARSET;
+ this.__searchForm = aJson.__searchForm;
+ this._updateInterval = aJson._updateInterval || null;
+ this._updateURL = aJson._updateURL || null;
+ this._iconUpdateURL = aJson._iconUpdateURL || null;
+ this._readOnly = aJson._readOnly == undefined;
+ this._iconURI = makeURI(aJson._iconURL);
+ this._iconMapObj = aJson._iconMapObj;
+ this._metaData = aJson._metaData || {};
+ if (aJson.filePath) {
+ this._filePath = aJson.filePath;
+ }
+ if (aJson.dirPath) {
+ this._dirPath = aJson.dirPath;
+ this._dirLastModifiedTime = aJson.dirLastModifiedTime;
+ }
+ if (aJson.extensionID) {
+ this._extensionID = aJson.extensionID;
+ }
+ for (let i = 0; i < aJson._urls.length; ++i) {
+ let url = aJson._urls[i];
+ let engineURL = new EngineURL(url.type || URLTYPE_SEARCH_HTML,
+ url.method || "GET", url.template,
+ url.resultDomain || undefined);
+ engineURL._initWithJSON(url, this);
+ this._urls.push(engineURL);
+ }
+ },
+
+ /**
+ * Creates a JavaScript object that represents this engine.
+ * @returns An object suitable for serialization as JSON.
+ **/
+ toJSON: function SRCH_ENG_toJSON() {
+ var json = {
+ _name: this._name,
+ _shortName: this._shortName,
+ _loadPath: this._loadPath,
+ description: this.description,
+ __searchForm: this.__searchForm,
+ _iconURL: this._iconURL,
+ _iconMapObj: this._iconMapObj,
+ _metaData: this._metaData,
+ _urls: this._urls
+ };
+
+ if (this._updateInterval)
+ json._updateInterval = this._updateInterval;
+ if (this._updateURL)
+ json._updateURL = this._updateURL;
+ if (this._iconUpdateURL)
+ json._iconUpdateURL = this._iconUpdateURL;
+ if (!this._hasPreferredIcon)
+ json._hasPreferredIcon = this._hasPreferredIcon;
+ if (this.queryCharset != DEFAULT_QUERY_CHARSET)
+ json.queryCharset = this.queryCharset;
+ if (!this._readOnly)
+ json._readOnly = this._readOnly;
+ if (this._filePath) {
+ // File path is stored so that we can remove legacy xml files
+ // from the profile if the user removes the engine.
+ json.filePath = this._filePath;
+ }
+ if (this._dirPath) {
+ // The directory path is only stored for extension-shipped engines,
+ // it's used to invalidate the cache.
+ json.dirPath = this._dirPath;
+ json.dirLastModifiedTime = this._dirLastModifiedTime;
+ }
+ if (this._extensionID) {
+ json.extensionID = this._extensionID;
+ }
+
+ return json;
+ },
+
+ setAttr(name, val) {
+ this._metaData[name] = val;
+ },
+
+ getAttr(name) {
+ return this._metaData[name] || undefined;
+ },
+
+ // nsISearchEngine
+ get alias() {
+ return this.getAttr("alias");
+ },
+ set alias(val) {
+ var value = val ? val.trim() : null;
+ this.setAttr("alias", value);
+ notifyAction(this, SEARCH_ENGINE_CHANGED);
+ },
+
+ /**
+ * Return the built-in identifier of app-provided engines.
+ *
+ * Note that this identifier is substantially similar to _id, with the
+ * following exceptions:
+ *
+ * * There is no trailing file extension.
+ * * There is no [app] prefix.
+ *
+ * @return a string identifier, or null.
+ */
+ get identifier() {
+ // No identifier if If the engine isn't app-provided
+ return this._isDefault ? this._shortName : null;
+ },
+
+ get description() {
+ return this._description;
+ },
+
+ get hidden() {
+ return this.getAttr("hidden") || false;
+ },
+ set hidden(val) {
+ var value = !!val;
+ if (value != this.hidden) {
+ this.setAttr("hidden", value);
+ notifyAction(this, SEARCH_ENGINE_CHANGED);
+ }
+ },
+
+ get iconURI() {
+ if (this._iconURI)
+ return this._iconURI;
+ return null;
+ },
+
+ get _iconURL() {
+ if (!this._iconURI)
+ return "";
+ return this._iconURI.spec;
+ },
+
+ // Where the engine is being loaded from: will return the URI's spec if the
+ // engine is being downloaded and does not yet have a file. This is only used
+ // for logging and error messages.
+ get _location() {
+ if (this._uri)
+ return this._uri.spec;
+
+ return this._loadPath;
+ },
+
+ // This indicates where we found the .xml file to load the engine,
+ // and attempts to hide user-identifiable data (such as username).
+ getAnonymizedLoadPath(file, uri) {
+ /* Examples of expected output:
+ * jar:[app]/omni.ja!browser/engine.xml
+ * 'browser' here is the name of the chrome package, not a folder.
+ * [profile]/searchplugins/engine.xml
+ * [distribution]/searchplugins/common/engine.xml
+ * [other]/engine.xml
+ */
+
+ const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+ const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+ const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+
+ const knownDirs = {
+ app: NS_XPCOM_CURRENT_PROCESS_DIR,
+ profile: NS_APP_USER_PROFILE_50_DIR,
+ distribution: XRE_APP_DISTRIBUTION_DIR
+ };
+
+ let leafName = this._shortName;
+ if (!leafName)
+ return "null";
+ leafName += ".xml";
+
+ let prefix = "", suffix = "";
+ if (!file) {
+ if (uri.schemeIs("resource")) {
+ uri = makeURI(Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsISubstitutingProtocolHandler)
+ .resolveURI(uri));
+ }
+ let scheme = uri.scheme;
+ let packageName = "";
+ if (scheme == "chrome") {
+ packageName = uri.hostPort;
+ uri = gChromeReg.convertChromeURL(uri);
+ }
+
+ if (AppConstants.platform == "android") {
+ // On Android the omni.ja file isn't at the same path as the binary
+ // used to start the process. We tweak the path here so that the code
+ // shared with Desktop will correctly identify files from the omni.ja
+ // file as coming from the [app] folder.
+ let appPath = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler)
+ .getSubstitution("android");
+ if (appPath) {
+ appPath = appPath.spec;
+ let spec = uri.spec;
+ if (spec.includes(appPath)) {
+ let appURI = Services.io.newFileURI(getDir(knownDirs["app"]));
+ uri = NetUtil.newURI(spec.replace(appPath, appURI.spec));
+ }
+ }
+ }
+
+ if (uri instanceof Ci.nsINestedURI) {
+ prefix = "jar:";
+ suffix = "!" + packageName + "/" + leafName;
+ uri = uri.innermostURI;
+ }
+ if (uri instanceof Ci.nsIFileURL) {
+ file = uri.file;
+ } else {
+ let path = "[" + scheme + "]";
+ if (/^(?:https?|ftp)$/.test(scheme)) {
+ path += uri.host;
+ }
+ return path + "/" + leafName;
+ }
+ }
+
+ let id;
+ let enginePath = file.path;
+
+ for (let key in knownDirs) {
+ let path;
+ try {
+ path = getDir(knownDirs[key]).path;
+ } catch (e) {
+ // Getting XRE_APP_DISTRIBUTION_DIR throws during unit tests.
+ continue;
+ }
+ if (enginePath.startsWith(path)) {
+ id = "[" + key + "]" + enginePath.slice(path.length).replace(/\\/g, "/");
+ break;
+ }
+ }
+
+ // If the folder doesn't have a known ancestor, don't record its path to
+ // avoid leaking user identifiable data.
+ if (!id)
+ id = "[other]/" + file.leafName;
+
+ return prefix + id + suffix;
+ },
+
+ get _isDefault() {
+ // If we don't have a shortName, the engine is being parsed from a
+ // downloaded file, so this can't be a default engine.
+ if (!this._shortName)
+ return false;
+
+ // An engine is a default one if we initially loaded it from the application
+ // or distribution directory.
+ if (/^(?:jar:)?(?:\[app\]|\[distribution\])/.test(this._loadPath))
+ return true;
+
+ // If we are using a non-default locale or in the xpcshell test case,
+ // we'll accept as a 'default' engine anything that has been registered at
+ // resource://search-plugins/ even if the file doesn't come from the
+ // application folder. If not, skip costly additional checks.
+ if (!Services.prefs.prefHasUserValue(LOCALE_PREF) &&
+ !gEnvironment.get("XPCSHELL_TEST_PROFILE_DIR"))
+ return false;
+
+ // Some xpcshell tests use the search service without registering
+ // resource://search-plugins/.
+ if (!Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler)
+ .hasSubstitution("search-plugins"))
+ return false;
+
+ let uri = makeURI(APP_SEARCH_PREFIX + this._shortName + ".xml");
+ if (this.getAnonymizedLoadPath(null, uri) == this._loadPath) {
+ // This isn't a real default engine, but it's very close.
+ LOG("_isDefault, pretending " + this._loadPath + " is a default engine");
+ return true;
+ }
+
+ return false;
+ },
+
+ get _hasUpdates() {
+ // Whether or not the engine has an update URL
+ let selfURL = this._getURLOfType(URLTYPE_OPENSEARCH, "self");
+ return !!(this._updateURL || this._iconUpdateURL || selfURL);
+ },
+
+ get name() {
+ return this._name;
+ },
+
+ get searchForm() {
+ return this._getSearchFormWithPurpose();
+ },
+
+ _getSearchFormWithPurpose(aPurpose = "") {
+ // First look for a <Url rel="searchform">
+ var searchFormURL = this._getURLOfType(URLTYPE_SEARCH_HTML, "searchform");
+ if (searchFormURL) {
+ let submission = searchFormURL.getSubmission("", this, aPurpose);
+
+ // If the rel=searchform URL is not type="get" (i.e. has postData),
+ // ignore it, since we can only return a URL.
+ if (!submission.postData)
+ return submission.uri.spec;
+ }
+
+ if (!this._searchForm) {
+ // No SearchForm specified in the engine definition file, use the prePath
+ // (e.g. https://foo.com for https://foo.com/search.php?q=bar).
+ var htmlUrl = this._getURLOfType(URLTYPE_SEARCH_HTML);
+ ENSURE_WARN(htmlUrl, "Engine has no HTML URL!", Cr.NS_ERROR_UNEXPECTED);
+ this._searchForm = makeURI(htmlUrl.template).prePath;
+ }
+
+ return ParamSubstitution(this._searchForm, "", this);
+ },
+
+ get queryCharset() {
+ if (this._queryCharset)
+ return this._queryCharset;
+ return this._queryCharset = "windows-1252"; // the default
+ },
+
+ // from nsISearchEngine
+ addParam: function SRCH_ENG_addParam(aName, aValue, aResponseType) {
+ if (!aName || (aValue == null))
+ FAIL("missing name or value for nsISearchEngine::addParam!");
+ ENSURE_WARN(!this._readOnly,
+ "called nsISearchEngine::addParam on a read-only engine!",
+ Cr.NS_ERROR_FAILURE);
+ if (!aResponseType)
+ aResponseType = URLTYPE_SEARCH_HTML;
+
+ var url = this._getURLOfType(aResponseType);
+ if (!url)
+ FAIL("Engine object has no URL for response type " + aResponseType,
+ Cr.NS_ERROR_FAILURE);
+
+ url.addParam(aName, aValue);
+ },
+
+ get _defaultMobileResponseType() {
+ let type = URLTYPE_SEARCH_HTML;
+
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ let isTablet = sysInfo.get("tablet");
+ if (isTablet && this.supportsResponseType("application/x-moz-tabletsearch")) {
+ // Check for a tablet-specific search URL override
+ type = "application/x-moz-tabletsearch";
+ } else if (!isTablet && this.supportsResponseType("application/x-moz-phonesearch")) {
+ // Check for a phone-specific search URL override
+ type = "application/x-moz-phonesearch";
+ }
+
+ delete this._defaultMobileResponseType;
+ return this._defaultMobileResponseType = type;
+ },
+
+ get _isWhiteListed() {
+ let url = this._getURLOfType(URLTYPE_SEARCH_HTML).template;
+ let hostname = makeURI(url).host;
+ let whitelist = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .getCharPref("reset.whitelist")
+ .split(",");
+ if (whitelist.includes(hostname)) {
+ LOG("The hostname " + hostname + " is white listed, " +
+ "we won't show the search reset prompt");
+ return true;
+ }
+
+ return false;
+ },
+
+ // from nsISearchEngine
+ getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType, aPurpose) {
+ if (!aResponseType) {
+ aResponseType = AppConstants.platform == "android" ? this._defaultMobileResponseType :
+ URLTYPE_SEARCH_HTML;
+ }
+
+ if (aResponseType == URLTYPE_SEARCH_HTML &&
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).getBoolPref("reset.enabled") &&
+ this.name == Services.search.currentEngine.name &&
+ !this._isDefault &&
+ this.name != Services.search.originalDefaultEngine.name &&
+ (!this.getAttr("loadPathHash") ||
+ this.getAttr("loadPathHash") != getVerificationHash(this._loadPath)) &&
+ !this._isWhiteListed) {
+ let url = "about:searchreset";
+ let data = [];
+ if (aData)
+ data.push("data=" + encodeURIComponent(aData));
+ if (aPurpose)
+ data.push("purpose=" + aPurpose);
+ if (data.length)
+ url += "?" + data.join("&");
+ return new Submission(makeURI(url));
+ }
+
+ var url = this._getURLOfType(aResponseType);
+
+ if (!url)
+ return null;
+
+ if (!aData) {
+ // Return a dummy submission object with our searchForm attribute
+ return new Submission(makeURI(this._getSearchFormWithPurpose(aPurpose)));
+ }
+
+ LOG("getSubmission: In data: \"" + aData + "\"; Purpose: \"" + aPurpose + "\"");
+ var data = "";
+ try {
+ data = gTextToSubURI.ConvertAndEscape(this.queryCharset, aData);
+ } catch (ex) {
+ LOG("getSubmission: Falling back to default queryCharset!");
+ data = gTextToSubURI.ConvertAndEscape(DEFAULT_QUERY_CHARSET, aData);
+ }
+ LOG("getSubmission: Out data: \"" + data + "\"");
+ return url.getSubmission(data, this, aPurpose);
+ },
+
+ // from nsISearchEngine
+ supportsResponseType: function SRCH_ENG_supportsResponseType(type) {
+ return (this._getURLOfType(type) != null);
+ },
+
+ // from nsISearchEngine
+ getResultDomain: function SRCH_ENG_getResultDomain(aResponseType) {
+ if (!aResponseType) {
+ aResponseType = AppConstants.platform == "android" ? this._defaultMobileResponseType :
+ URLTYPE_SEARCH_HTML;
+ }
+
+ LOG("getResultDomain: responseType: \"" + aResponseType + "\"");
+
+ let url = this._getURLOfType(aResponseType);
+ if (url)
+ return url.resultDomain;
+ return "";
+ },
+
+ /**
+ * Returns URL parsing properties used by _buildParseSubmissionMap.
+ */
+ getURLParsingInfo: function () {
+ let responseType = AppConstants.platform == "android" ? this._defaultMobileResponseType :
+ URLTYPE_SEARCH_HTML;
+
+ LOG("getURLParsingInfo: responseType: \"" + responseType + "\"");
+
+ let url = this._getURLOfType(responseType);
+ if (!url || url.method != "GET") {
+ return null;
+ }
+
+ let termsParameterName = url._getTermsParameterName();
+ if (!termsParameterName) {
+ return null;
+ }
+
+ let templateUrl = NetUtil.newURI(url.template).QueryInterface(Ci.nsIURL);
+ return {
+ mainDomain: templateUrl.host,
+ path: templateUrl.filePath.toLowerCase(),
+ termsParameterName: termsParameterName,
+ };
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISearchEngine]),
+
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /**
+ * Returns a string with the URL to an engine's icon matching both width and
+ * height. Returns null if icon with specified dimensions is not found.
+ *
+ * @param width
+ * Width of the requested icon.
+ * @param height
+ * Height of the requested icon.
+ */
+ getIconURLBySize: function SRCH_ENG_getIconURLBySize(aWidth, aHeight) {
+ if (aWidth == 16 && aHeight == 16)
+ return this._iconURL;
+
+ if (!this._iconMapObj)
+ return null;
+
+ let key = this._getIconKey(aWidth, aHeight);
+ if (key in this._iconMapObj) {
+ return this._iconMapObj[key];
+ }
+ return null;
+ },
+
+ /**
+ * Gets an array of all available icons. Each entry is an object with
+ * width, height and url properties. width and height are numeric and
+ * represent the icon's dimensions. url is a string with the URL for
+ * the icon.
+ */
+ getIcons: function SRCH_ENG_getIcons() {
+ let result = [];
+ if (this._iconURL)
+ result.push({width: 16, height: 16, url: this._iconURL});
+
+ if (!this._iconMapObj)
+ return result;
+
+ for (let key of Object.keys(this._iconMapObj)) {
+ let iconSize = JSON.parse(key);
+ result.push({
+ width: iconSize.width,
+ height: iconSize.height,
+ url: this._iconMapObj[key]
+ });
+ }
+
+ return result;
+ },
+
+ /**
+ * Opens a speculative connection to the engine's search URI
+ * (and suggest URI, if different) to reduce request latency
+ *
+ * @param options
+ * An object that must contain the following fields:
+ * {window} the content window for the window performing the search
+ *
+ * @throws NS_ERROR_INVALID_ARG if options is omitted or lacks required
+ * elemeents
+ */
+ speculativeConnect: function SRCH_ENG_speculativeConnect(options) {
+ if (!options || !options.window) {
+ Cu.reportError("invalid options arg passed to nsISearchEngine.speculativeConnect");
+ throw Cr.NS_ERROR_INVALID_ARG;
+ }
+ let connector =
+ Services.io.QueryInterface(Components.interfaces.nsISpeculativeConnect);
+
+ let searchURI = this.getSubmission("dummy").uri;
+
+ let callbacks = options.window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsILoadContext);
+
+ connector.speculativeConnect(searchURI, callbacks);
+
+ if (this.supportsResponseType(URLTYPE_SUGGEST_JSON)) {
+ let suggestURI = this.getSubmission("dummy", URLTYPE_SUGGEST_JSON).uri;
+ if (suggestURI.prePath != searchURI.prePath)
+ connector.speculativeConnect(suggestURI, callbacks);
+ }
+ },
+};
+
+// nsISearchSubmission
+function Submission(aURI, aPostData = null) {
+ this._uri = aURI;
+ this._postData = aPostData;
+}
+Submission.prototype = {
+ get uri() {
+ return this._uri;
+ },
+ get postData() {
+ return this._postData;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISearchSubmission])
+}
+
+// nsISearchParseSubmissionResult
+function ParseSubmissionResult(aEngine, aTerms, aTermsOffset, aTermsLength) {
+ this._engine = aEngine;
+ this._terms = aTerms;
+ this._termsOffset = aTermsOffset;
+ this._termsLength = aTermsLength;
+}
+ParseSubmissionResult.prototype = {
+ get engine() {
+ return this._engine;
+ },
+ get terms() {
+ return this._terms;
+ },
+ get termsOffset() {
+ return this._termsOffset;
+ },
+ get termsLength() {
+ return this._termsLength;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISearchParseSubmissionResult]),
+}
+
+const gEmptyParseSubmissionResult =
+ Object.freeze(new ParseSubmissionResult(null, "", -1, 0));
+
+function executeSoon(func) {
+ Services.tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+/**
+ * Check for sync initialization has completed or not.
+ *
+ * @param {aPromise} A promise.
+ *
+ * @returns the value returned by the invoked method.
+ * @throws NS_ERROR_ALREADY_INITIALIZED if sync initialization has completed.
+ */
+function checkForSyncCompletion(aPromise) {
+ return aPromise.then(function(aValue) {
+ if (gInitialized) {
+ throw Components.Exception("Synchronous fallback was called and has " +
+ "finished so no need to pursue asynchronous " +
+ "initialization",
+ Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+ return aValue;
+ });
+}
+
+// nsIBrowserSearchService
+function SearchService() {
+ // Replace empty LOG function with the useful one if the log pref is set.
+ if (getBoolPref(BROWSER_SEARCH_PREF + "log", false))
+ LOG = DO_LOG;
+
+ this._initObservers = Promise.defer();
+}
+
+SearchService.prototype = {
+ classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
+
+ // The current status of initialization. Note that it does not determine if
+ // initialization is complete, only if an error has been encountered so far.
+ _initRV: Cr.NS_OK,
+
+ // The boolean indicates that the initialization has started or not.
+ _initStarted: null,
+
+ // Reading the JSON cache file is the first thing done during initialization.
+ // During the async init, we save it in a field so that if we have to do a
+ // sync init before the async init finishes, we can avoid reading the cache
+ // with sync disk I/O and handling lz4 decompression synchronously.
+ // This is set back to null as soon as the initialization is finished.
+ _cacheFileJSON: null,
+
+ // If initialization has not been completed yet, perform synchronous
+ // initialization.
+ // Throws in case of initialization error.
+ _ensureInitialized: function SRCH_SVC__ensureInitialized() {
+ if (gInitialized) {
+ if (!Components.isSuccessCode(this._initRV)) {
+ LOG("_ensureInitialized: failure");
+ throw this._initRV;
+ }
+ return;
+ }
+
+ let warning =
+ "Search service falling back to synchronous initialization. " +
+ "This is generally the consequence of an add-on using a deprecated " +
+ "search service API.";
+ Deprecated.warning(warning, "https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIBrowserSearchService#async_warning");
+ LOG(warning);
+
+ this._syncInit();
+ if (!Components.isSuccessCode(this._initRV)) {
+ throw this._initRV;
+ }
+ },
+
+ // Synchronous implementation of the initializer.
+ // Used by |_ensureInitialized| as a fallback if initialization is not
+ // complete.
+ _syncInit: function SRCH_SVC__syncInit() {
+ LOG("_syncInit start");
+ this._initStarted = true;
+ migrateRegionPrefs();
+
+ let cache = this._readCacheFile();
+ if (cache.metaData)
+ this._metaData = cache.metaData;
+
+ try {
+ this._syncLoadEngines(cache);
+ } catch (ex) {
+ this._initRV = Cr.NS_ERROR_FAILURE;
+ LOG("_syncInit: failure loading engines: " + ex);
+ }
+ this._addObservers();
+
+ gInitialized = true;
+ this._cacheFileJSON = null;
+
+ this._initObservers.resolve(this._initRV);
+
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_INIT_SYNC").add(true);
+ this._recordEngineTelemetry();
+
+ LOG("_syncInit end");
+ },
+
+ /**
+ * Asynchronous implementation of the initializer.
+ *
+ * @returns {Promise} A promise, resolved successfully if the initialization
+ * succeeds.
+ */
+ _asyncInit: Task.async(function* () {
+ LOG("_asyncInit start");
+
+ migrateRegionPrefs();
+
+ // See if we have a cache file so we don't have to parse a bunch of XML.
+ let cache = {};
+ // Not using checkForSyncCompletion here because we want to ensure we
+ // fetch the country code and geo specific defaults asynchronously even
+ // if a sync init has been forced.
+ cache = yield this._asyncReadCacheFile();
+
+ if (!gInitialized && cache.metaData)
+ this._metaData = cache.metaData;
+
+ try {
+ yield checkForSyncCompletion(ensureKnownCountryCode(this));
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ throw ex;
+ }
+ LOG("_asyncInit: failure determining country code: " + ex);
+ }
+ try {
+ yield checkForSyncCompletion(this._asyncLoadEngines(cache));
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ throw ex;
+ }
+ this._initRV = Cr.NS_ERROR_FAILURE;
+ LOG("_asyncInit: failure loading engines: " + ex);
+ }
+ this._addObservers();
+ gInitialized = true;
+ this._cacheFileJSON = null;
+ this._initObservers.resolve(this._initRV);
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_INIT_SYNC").add(false);
+ this._recordEngineTelemetry();
+
+ LOG("_asyncInit: Completed _asyncInit");
+ }),
+
+ _metaData: { },
+ setGlobalAttr(name, val) {
+ this._metaData[name] = val;
+ this.batchTask.disarm();
+ this.batchTask.arm();
+ },
+ setVerifiedGlobalAttr(name, val) {
+ this.setGlobalAttr(name, val);
+ this.setGlobalAttr(name + "Hash", getVerificationHash(val));
+ },
+
+ getGlobalAttr(name) {
+ return this._metaData[name] || undefined;
+ },
+ getVerifiedGlobalAttr(name) {
+ let val = this.getGlobalAttr(name);
+ if (val && this.getGlobalAttr(name + "Hash") != getVerificationHash(val)) {
+ LOG("getVerifiedGlobalAttr, invalid hash for " + name);
+ return "";
+ }
+ return val;
+ },
+
+ _engines: { },
+ __sortedEngines: null,
+ _visibleDefaultEngines: [],
+ get _sortedEngines() {
+ if (!this.__sortedEngines)
+ return this._buildSortedEngineList();
+ return this.__sortedEngines;
+ },
+
+ // Get the original Engine object that is the default for this region,
+ // ignoring changes the user may have subsequently made.
+ get originalDefaultEngine() {
+ let defaultEngine = this.getVerifiedGlobalAttr("searchDefault");
+ if (!defaultEngine) {
+ let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let nsIPLS = Ci.nsIPrefLocalizedString;
+
+ let defPref = getGeoSpecificPrefName("defaultenginename");
+ try {
+ defaultEngine = defaultPrefB.getComplexValue(defPref, nsIPLS).data;
+ } catch (ex) {
+ // If the default pref is invalid (e.g. an add-on set it to a bogus value)
+ // getEngineByName will just return null, which is the best we can do.
+ }
+ }
+
+ return this.getEngineByName(defaultEngine);
+ },
+
+ resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
+ this.currentEngine = this.originalDefaultEngine;
+ },
+
+ _buildCache: function SRCH_SVC__buildCache() {
+ if (this._batchTask)
+ this._batchTask.disarm();
+
+ let cache = {};
+ let locale = getLocale();
+ let buildID = Services.appinfo.platformBuildID;
+
+ // Allows us to force a cache refresh should the cache format change.
+ cache.version = CACHE_VERSION;
+ // We don't want to incur the costs of stat()ing each plugin on every
+ // startup when the only (supported) time they will change is during
+ // app updates (where the buildID is obviously going to change).
+ // Extension-shipped plugins are the only exception to this, but their
+ // directories are blown away during updates, so we'll detect their changes.
+ cache.buildID = buildID;
+ cache.locale = locale;
+
+ cache.visibleDefaultEngines = this._visibleDefaultEngines;
+ cache.metaData = this._metaData;
+ cache.engines = [];
+
+ for (let name in this._engines) {
+ cache.engines.push(this._engines[name]);
+ }
+
+ try {
+ if (!cache.engines.length)
+ throw "cannot write without any engine.";
+
+ LOG("_buildCache: Writing to cache file.");
+ let path = OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME);
+ let data = gEncoder.encode(JSON.stringify(cache));
+ let promise = OS.File.writeAtomic(path, data, {compression: "lz4",
+ tmpPath: path + ".tmp"});
+
+ promise.then(
+ function onSuccess() {
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, SEARCH_SERVICE_CACHE_WRITTEN);
+ },
+ function onError(e) {
+ LOG("_buildCache: failure during writeAtomic: " + e);
+ }
+ );
+ } catch (ex) {
+ LOG("_buildCache: Could not write to cache file: " + ex);
+ }
+ },
+
+ _syncLoadEngines: function SRCH_SVC__syncLoadEngines(cache) {
+ LOG("_syncLoadEngines: start");
+ // See if we have a cache file so we don't have to parse a bunch of XML.
+ let chromeURIs = this._findJAREngines();
+
+ let distDirs = [];
+ let locations;
+ try {
+ locations = getDir(NS_APP_DISTRIBUTION_SEARCH_DIR_LIST,
+ Ci.nsISimpleEnumerator);
+ } catch (e) {
+ // NS_APP_DISTRIBUTION_SEARCH_DIR_LIST is defined by each app
+ // so this throws during unit tests (but not xpcshell tests).
+ locations = {hasMoreElements: () => false};
+ }
+ while (locations.hasMoreElements()) {
+ let dir = locations.getNext().QueryInterface(Ci.nsIFile);
+ if (dir.directoryEntries.hasMoreElements())
+ distDirs.push(dir);
+ }
+
+ let otherDirs = [];
+ let userSearchDir = getDir(NS_APP_USER_SEARCH_DIR);
+ locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
+ while (locations.hasMoreElements()) {
+ let dir = locations.getNext().QueryInterface(Ci.nsIFile);
+ if ((!cache.engines || !dir.equals(userSearchDir)) &&
+ dir.directoryEntries.hasMoreElements())
+ otherDirs.push(dir);
+ }
+
+ function modifiedDir(aDir) {
+ return cacheOtherPaths.get(aDir.path) != aDir.lastModifiedTime;
+ }
+
+ function notInCacheVisibleEngines(aEngineName) {
+ return cache.visibleDefaultEngines.indexOf(aEngineName) == -1;
+ }
+
+ let buildID = Services.appinfo.platformBuildID;
+ let cacheOtherPaths = new Map();
+ if (cache.engines) {
+ for (let engine of cache.engines) {
+ if (engine._dirPath) {
+ cacheOtherPaths.set(engine._dirPath, engine._dirLastModifiedTime);
+ }
+ }
+ }
+
+ let rebuildCache = !cache.engines ||
+ cache.version != CACHE_VERSION ||
+ cache.locale != getLocale() ||
+ cache.buildID != buildID ||
+ cacheOtherPaths.size != otherDirs.length ||
+ otherDirs.some(d => !cacheOtherPaths.has(d.path)) ||
+ cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
+ this._visibleDefaultEngines.some(notInCacheVisibleEngines) ||
+ otherDirs.some(modifiedDir);
+
+ if (rebuildCache) {
+ LOG("_loadEngines: Absent or outdated cache. Loading engines from disk.");
+ distDirs.forEach(this._loadEnginesFromDir, this);
+
+ this._loadFromChromeURLs(chromeURIs);
+
+ LOG("_loadEngines: load user-installed engines from the obsolete cache");
+ this._loadEnginesFromCache(cache, true);
+
+ otherDirs.forEach(this._loadEnginesFromDir, this);
+
+ this._loadEnginesMetadataFromCache(cache);
+ this._buildCache();
+ return;
+ }
+
+ LOG("_loadEngines: loading from cache directories");
+ this._loadEnginesFromCache(cache);
+
+ LOG("_loadEngines: done");
+ },
+
+ /**
+ * Loads engines asynchronously.
+ *
+ * @returns {Promise} A promise, resolved successfully if loading data
+ * succeeds.
+ */
+ _asyncLoadEngines: Task.async(function* (cache) {
+ LOG("_asyncLoadEngines: start");
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "find-jar-engines");
+ let chromeURIs =
+ yield checkForSyncCompletion(this._asyncFindJAREngines());
+
+ // Get the non-empty distribution directories into distDirs...
+ let distDirs = [];
+ let locations;
+ try {
+ locations = getDir(NS_APP_DISTRIBUTION_SEARCH_DIR_LIST,
+ Ci.nsISimpleEnumerator);
+ } catch (e) {
+ // NS_APP_DISTRIBUTION_SEARCH_DIR_LIST is defined by each app
+ // so this throws during unit tests (but not xpcshell tests).
+ locations = {hasMoreElements: () => false};
+ }
+ while (locations.hasMoreElements()) {
+ let dir = locations.getNext().QueryInterface(Ci.nsIFile);
+ let iterator = new OS.File.DirectoryIterator(dir.path,
+ { winPattern: "*.xml" });
+ try {
+ // Add dir to distDirs if it contains any files.
+ yield checkForSyncCompletion(iterator.next());
+ distDirs.push(dir);
+ } catch (ex) {
+ // Catch for StopIteration exception.
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ throw ex;
+ }
+ } finally {
+ iterator.close();
+ }
+ }
+
+ // Add the non-empty directories of NS_APP_SEARCH_DIR_LIST to
+ // otherDirs...
+ let otherDirs = [];
+ let userSearchDir = getDir(NS_APP_USER_SEARCH_DIR);
+ locations = getDir(NS_APP_SEARCH_DIR_LIST, Ci.nsISimpleEnumerator);
+ while (locations.hasMoreElements()) {
+ let dir = locations.getNext().QueryInterface(Ci.nsIFile);
+ if (cache.engines && dir.equals(userSearchDir))
+ continue;
+ let iterator = new OS.File.DirectoryIterator(dir.path,
+ { winPattern: "*.xml" });
+ try {
+ // Add dir to otherDirs if it contains any files.
+ yield checkForSyncCompletion(iterator.next());
+ otherDirs.push(dir);
+ } catch (ex) {
+ // Catch for StopIteration exception.
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ throw ex;
+ }
+ } finally {
+ iterator.close();
+ }
+ }
+
+ let hasModifiedDir = Task.async(function* (aList) {
+ let modifiedDir = false;
+
+ for (let dir of aList) {
+ let lastModifiedTime = cacheOtherPaths.get(dir.path);
+ if (!lastModifiedTime) {
+ continue;
+ }
+
+ let info = yield OS.File.stat(dir.path);
+ if (lastModifiedTime != info.lastModificationDate.getTime()) {
+ modifiedDir = true;
+ break;
+ }
+ }
+ return modifiedDir;
+ });
+
+ function notInCacheVisibleEngines(aEngineName) {
+ return cache.visibleDefaultEngines.indexOf(aEngineName) == -1;
+ }
+
+ let buildID = Services.appinfo.platformBuildID;
+ let cacheOtherPaths = new Map();
+ if (cache.engines) {
+ for (let engine of cache.engines) {
+ if (engine._dirPath) {
+ cacheOtherPaths.set(engine._dirPath, engine._dirLastModifiedTime);
+ }
+ }
+ }
+
+ let rebuildCache = !cache.engines ||
+ cache.version != CACHE_VERSION ||
+ cache.locale != getLocale() ||
+ cache.buildID != buildID ||
+ cacheOtherPaths.size != otherDirs.length ||
+ otherDirs.some(d => !cacheOtherPaths.has(d.path)) ||
+ cache.visibleDefaultEngines.length != this._visibleDefaultEngines.length ||
+ this._visibleDefaultEngines.some(notInCacheVisibleEngines) ||
+ (yield checkForSyncCompletion(hasModifiedDir(otherDirs)));
+
+ if (rebuildCache) {
+ LOG("_asyncLoadEngines: Absent or outdated cache. Loading engines from disk.");
+ for (let loadDir of distDirs) {
+ let enginesFromDir =
+ yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
+ enginesFromDir.forEach(this._addEngineToStore, this);
+ }
+ let enginesFromURLs =
+ yield checkForSyncCompletion(this._asyncLoadFromChromeURLs(chromeURIs));
+ enginesFromURLs.forEach(this._addEngineToStore, this);
+
+ LOG("_asyncLoadEngines: loading user-installed engines from the obsolete cache");
+ this._loadEnginesFromCache(cache, true);
+
+ for (let loadDir of otherDirs) {
+ let enginesFromDir =
+ yield checkForSyncCompletion(this._asyncLoadEnginesFromDir(loadDir));
+ enginesFromDir.forEach(this._addEngineToStore, this);
+ }
+
+ this._loadEnginesMetadataFromCache(cache);
+ this._buildCache();
+ return;
+ }
+
+ LOG("_asyncLoadEngines: loading from cache directories");
+ this._loadEnginesFromCache(cache);
+
+ LOG("_asyncLoadEngines: done");
+ }),
+
+ _asyncReInit: function () {
+ LOG("_asyncReInit");
+ // Start by clearing the initialized state, so we don't abort early.
+ gInitialized = false;
+
+ Task.spawn(function* () {
+ try {
+ if (this._batchTask) {
+ LOG("finalizing batch task");
+ let task = this._batchTask;
+ this._batchTask = null;
+ yield task.finalize();
+ }
+
+ // Clear the engines, too, so we don't stick with the stale ones.
+ this._engines = {};
+ this.__sortedEngines = null;
+ this._currentEngine = null;
+ this._visibleDefaultEngines = [];
+ this._metaData = {};
+ this._cacheFileJSON = null;
+
+ // Tests that want to force a synchronous re-initialization need to
+ // be notified when we are done uninitializing.
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC,
+ "uninit-complete");
+
+ let cache = {};
+ cache = yield this._asyncReadCacheFile();
+ if (!gInitialized && cache.metaData)
+ this._metaData = cache.metaData;
+
+ yield ensureKnownCountryCode(this);
+ // Due to the HTTP requests done by ensureKnownCountryCode, it's possible that
+ // at this point a synchronous init has been forced by other code.
+ if (!gInitialized)
+ yield this._asyncLoadEngines(cache);
+
+ // Typically we'll re-init as a result of a pref observer,
+ // so signal to 'callers' that we're done.
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
+ this._recordEngineTelemetry();
+ gInitialized = true;
+ } catch (err) {
+ LOG("Reinit failed: " + err);
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "reinit-failed");
+ } finally {
+ Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "reinit-complete");
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Read the cache file synchronously. This also imports data from the old
+ * search-metadata.json file if needed.
+ *
+ * @returns A JS object containing the cached data.
+ */
+ _readCacheFile: function SRCH_SVC__readCacheFile() {
+ if (this._cacheFileJSON) {
+ return this._cacheFileJSON;
+ }
+
+ let cacheFile = getDir(NS_APP_USER_PROFILE_50_DIR);
+ cacheFile.append(CACHE_FILENAME);
+
+ let stream;
+ try {
+ stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ stream.init(cacheFile, MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+
+ let bis = Cc["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Ci.nsIBinaryInputStream);
+ bis.setInputStream(stream);
+
+ let count = stream.available();
+ let array = new Uint8Array(count);
+ bis.readArrayBuffer(count, array.buffer);
+
+ let bytes = Lz4.decompressFileContent(array);
+ let json = JSON.parse(new TextDecoder().decode(bytes));
+ if (!json.engines || !json.engines.length)
+ throw "no engine in the file";
+ return json;
+ } catch (ex) {
+ LOG("_readCacheFile: Error reading cache file: " + ex);
+ } finally {
+ stream.close();
+ }
+
+ try {
+ cacheFile.leafName = "search-metadata.json";
+ stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ stream.init(cacheFile, MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ let metadata = parseJsonFromStream(stream);
+ let json = {};
+ if ("[global]" in metadata) {
+ LOG("_readCacheFile: migrating metadata from search-metadata.json");
+ let data = metadata["[global]"];
+ json.metaData = {};
+ let fields = ["searchDefault", "searchDefaultHash", "searchDefaultExpir",
+ "current", "hash",
+ "visibleDefaultEngines", "visibleDefaultEnginesHash"];
+ for (let field of fields) {
+ let name = field.toLowerCase();
+ if (name in data)
+ json.metaData[field] = data[name];
+ }
+ }
+ delete metadata["[global]"];
+ json._oldMetadata = metadata;
+
+ return json;
+ } catch (ex) {
+ LOG("_readCacheFile: failed to read old metadata: " + ex);
+ return {};
+ } finally {
+ stream.close();
+ }
+ },
+
+ /**
+ * Read the cache file asynchronously. This also imports data from the old
+ * search-metadata.json file if needed.
+ *
+ * @returns {Promise} A promise, resolved successfully if retrieveing data
+ * succeeds.
+ */
+ _asyncReadCacheFile: Task.async(function* () {
+ let json;
+ try {
+ let cacheFilePath = OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME);
+ let bytes = yield OS.File.read(cacheFilePath, {compression: "lz4"});
+ json = JSON.parse(new TextDecoder().decode(bytes));
+ if (!json.engines || !json.engines.length)
+ throw "no engine in the file";
+ this._cacheFileJSON = json;
+ } catch (ex) {
+ LOG("_asyncReadCacheFile: Error reading cache file: " + ex);
+ json = {};
+
+ let oldMetadata =
+ OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
+ try {
+ let bytes = yield OS.File.read(oldMetadata);
+ let metadata = JSON.parse(new TextDecoder().decode(bytes));
+ if ("[global]" in metadata) {
+ LOG("_asyncReadCacheFile: migrating metadata from search-metadata.json");
+ let data = metadata["[global]"];
+ json.metaData = {};
+ let fields = ["searchDefault", "searchDefaultHash", "searchDefaultExpir",
+ "current", "hash",
+ "visibleDefaultEngines", "visibleDefaultEnginesHash"];
+ for (let field of fields) {
+ let name = field.toLowerCase();
+ if (name in data)
+ json.metaData[field] = data[name];
+ }
+ }
+ delete metadata["[global]"];
+ json._oldMetadata = metadata;
+ } catch (ex) {}
+ }
+ return json;
+ }),
+
+ _batchTask: null,
+ get batchTask() {
+ if (!this._batchTask) {
+ let task = function taskCallback() {
+ LOG("batchTask: Invalidating engine cache");
+ this._buildCache();
+ }.bind(this);
+ this._batchTask = new DeferredTask(task, CACHE_INVALIDATION_DELAY);
+ }
+ return this._batchTask;
+ },
+
+ _addEngineToStore: function SRCH_SVC_addEngineToStore(aEngine) {
+ LOG("_addEngineToStore: Adding engine: \"" + aEngine.name + "\"");
+
+ // See if there is an existing engine with the same name. However, if this
+ // engine is updating another engine, it's allowed to have the same name.
+ var hasSameNameAsUpdate = (aEngine._engineToUpdate &&
+ aEngine.name == aEngine._engineToUpdate.name);
+ if (aEngine.name in this._engines && !hasSameNameAsUpdate) {
+ LOG("_addEngineToStore: Duplicate engine found, aborting!");
+ return;
+ }
+
+ if (aEngine._engineToUpdate) {
+ // We need to replace engineToUpdate with the engine that just loaded.
+ var oldEngine = aEngine._engineToUpdate;
+
+ // Remove the old engine from the hash, since it's keyed by name, and our
+ // name might change (the update might have a new name).
+ delete this._engines[oldEngine.name];
+
+ // Hack: we want to replace the old engine with the new one, but since
+ // people may be holding refs to the nsISearchEngine objects themselves,
+ // we'll just copy over all "private" properties (those without a getter
+ // or setter) from one object to the other.
+ for (var p in aEngine) {
+ if (!(aEngine.__lookupGetter__(p) || aEngine.__lookupSetter__(p)))
+ oldEngine[p] = aEngine[p];
+ }
+ aEngine = oldEngine;
+ aEngine._engineToUpdate = null;
+
+ // Add the engine back
+ this._engines[aEngine.name] = aEngine;
+ notifyAction(aEngine, SEARCH_ENGINE_CHANGED);
+ } else {
+ // Not an update, just add the new engine.
+ this._engines[aEngine.name] = aEngine;
+ // Only add the engine to the list of sorted engines if the initial list
+ // has already been built (i.e. if this.__sortedEngines is non-null). If
+ // it hasn't, we're loading engines from disk and the sorted engine list
+ // will be built once we need it.
+ if (this.__sortedEngines) {
+ this.__sortedEngines.push(aEngine);
+ this._saveSortedEngineList();
+ }
+ notifyAction(aEngine, SEARCH_ENGINE_ADDED);
+ }
+
+ if (aEngine._hasUpdates) {
+ // Schedule the engine's next update, if it isn't already.
+ if (!aEngine.getAttr("updateexpir"))
+ engineUpdateService.scheduleNextUpdate(aEngine);
+ }
+ },
+
+ _loadEnginesMetadataFromCache: function SRCH_SVC__loadEnginesMetadataFromCache(cache) {
+ if (cache._oldMetadata) {
+ // If we have old metadata in the cache, we had no valid cache
+ // file and read data from search-metadata.json.
+ for (let name in this._engines) {
+ let engine = this._engines[name];
+ if (engine._id && cache._oldMetadata[engine._id])
+ engine._metaData = cache._oldMetadata[engine._id];
+ }
+ return;
+ }
+
+ if (!cache.engines)
+ return;
+
+ for (let engine of cache.engines) {
+ let name = engine._name;
+ if (name in this._engines) {
+ LOG("_loadEnginesMetadataFromCache, transfering metadata for " + name);
+ this._engines[name]._metaData = engine._metaData;
+ }
+ }
+ },
+
+ _loadEnginesFromCache: function SRCH_SVC__loadEnginesFromCache(cache,
+ skipReadOnly) {
+ if (!cache.engines)
+ return;
+
+ LOG("_loadEnginesFromCache: Loading " +
+ cache.engines.length + " engines from cache");
+
+ let skippedEngines = 0;
+ for (let engine of cache.engines) {
+ if (skipReadOnly && engine._readOnly == undefined) {
+ ++skippedEngines;
+ continue;
+ }
+
+ this._loadEngineFromCache(engine);
+ }
+
+ if (skippedEngines) {
+ LOG("_loadEnginesFromCache: skipped " + skippedEngines + " read-only engines.");
+ }
+ },
+
+ _loadEngineFromCache: function SRCH_SVC__loadEngineFromCache(json) {
+ try {
+ let engine = new Engine(json._shortName, json._readOnly == undefined);
+ engine._initWithJSON(json);
+ this._addEngineToStore(engine);
+ } catch (ex) {
+ LOG("Failed to load " + json._name + " from cache: " + ex);
+ LOG("Engine JSON: " + json.toSource());
+ }
+ },
+
+ _loadEnginesFromDir: function SRCH_SVC__loadEnginesFromDir(aDir) {
+ LOG("_loadEnginesFromDir: Searching in " + aDir.path + " for search engines.");
+
+ // Check whether aDir is the user profile dir
+ var isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
+
+ var files = aDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+
+ while (files.hasMoreElements()) {
+ var file = files.nextFile;
+
+ // Ignore hidden and empty files, and directories
+ if (!file.isFile() || file.fileSize == 0 || file.isHidden())
+ continue;
+
+ var fileURL = NetUtil.ioService.newFileURI(file).QueryInterface(Ci.nsIURL);
+ var fileExtension = fileURL.fileExtension.toLowerCase();
+
+ if (fileExtension != "xml") {
+ // Not an engine
+ continue;
+ }
+
+ var addedEngine = null;
+ try {
+ addedEngine = new Engine(file, !isInProfile);
+ addedEngine._initFromFile(file);
+ if (!isInProfile && !addedEngine._isDefault) {
+ addedEngine._dirPath = aDir.path;
+ addedEngine._dirLastModifiedTime = aDir.lastModifiedTime;
+ }
+ } catch (ex) {
+ LOG("_loadEnginesFromDir: Failed to load " + file.path + "!\n" + ex);
+ continue;
+ }
+
+ this._addEngineToStore(addedEngine);
+ }
+ },
+
+ /**
+ * Loads engines from a given directory asynchronously.
+ *
+ * @param aDir the directory.
+ *
+ * @returns {Promise} A promise, resolved successfully if retrieveing data
+ * succeeds.
+ */
+ _asyncLoadEnginesFromDir: Task.async(function* (aDir) {
+ LOG("_asyncLoadEnginesFromDir: Searching in " + aDir.path + " for search engines.");
+
+ // Check whether aDir is the user profile dir
+ let isInProfile = aDir.equals(getDir(NS_APP_USER_SEARCH_DIR));
+ let dirPath = aDir.path;
+ let iterator = new OS.File.DirectoryIterator(dirPath);
+
+ let osfiles = yield iterator.nextBatch();
+ iterator.close();
+
+ let engines = [];
+ for (let osfile of osfiles) {
+ if (osfile.isDir || osfile.isSymLink)
+ continue;
+
+ let fileInfo = yield OS.File.stat(osfile.path);
+ if (fileInfo.size == 0)
+ continue;
+
+ let parts = osfile.path.split(".");
+ if (parts.length <= 1 || (parts.pop()).toLowerCase() != "xml") {
+ // Not an engine
+ continue;
+ }
+
+ let addedEngine = null;
+ try {
+ let file = new FileUtils.File(osfile.path);
+ addedEngine = new Engine(file, !isInProfile);
+ yield checkForSyncCompletion(addedEngine._asyncInitFromFile(file));
+ if (!isInProfile && !addedEngine._isDefault) {
+ addedEngine._dirPath = dirPath;
+ let info = yield OS.File.stat(dirPath);
+ addedEngine._dirLastModifiedTime =
+ info.lastModificationDate.getTime();
+ }
+ engines.push(addedEngine);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ throw ex;
+ }
+ LOG("_asyncLoadEnginesFromDir: Failed to load " + osfile.path + "!\n" + ex);
+ }
+ }
+ return engines;
+ }),
+
+ _loadFromChromeURLs: function SRCH_SVC_loadFromChromeURLs(aURLs) {
+ aURLs.forEach(function (url) {
+ try {
+ LOG("_loadFromChromeURLs: loading engine from chrome url: " + url);
+
+ let uri = makeURI(url);
+ let engine = new Engine(uri, true);
+
+ engine._initFromURISync(uri);
+
+ this._addEngineToStore(engine);
+ } catch (ex) {
+ LOG("_loadFromChromeURLs: failed to load engine: " + ex);
+ }
+ }, this);
+ },
+
+ /**
+ * Loads engines from Chrome URLs asynchronously.
+ *
+ * @param aURLs a list of URLs.
+ *
+ * @returns {Promise} A promise, resolved successfully if loading data
+ * succeeds.
+ */
+ _asyncLoadFromChromeURLs: Task.async(function* (aURLs) {
+ let engines = [];
+ for (let url of aURLs) {
+ try {
+ LOG("_asyncLoadFromChromeURLs: loading engine from chrome url: " + url);
+ let uri = NetUtil.newURI(url);
+ let engine = new Engine(uri, true);
+ yield checkForSyncCompletion(engine._asyncInitFromURI(uri));
+ engines.push(engine);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ throw ex;
+ }
+ LOG("_asyncLoadFromChromeURLs: failed to load engine: " + ex);
+ }
+ }
+ return engines;
+ }),
+
+ _convertChannelToFile: function(chan) {
+ let fileURI = chan.URI;
+ while (fileURI instanceof Ci.nsIJARURI)
+ fileURI = fileURI.JARFile;
+ fileURI.QueryInterface(Ci.nsIFileURL);
+
+ return fileURI.file;
+ },
+
+ _findJAREngines: function SRCH_SVC_findJAREngines() {
+ LOG("_findJAREngines: looking for engines in JARs")
+
+ let chan = makeChannel(APP_SEARCH_PREFIX + "list.json");
+ if (!chan) {
+ LOG("_findJAREngines: " + APP_SEARCH_PREFIX + " isn't registered");
+ return [];
+ }
+
+ let uris = [];
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ try {
+ sis.init(chan.open2());
+ this._parseListJSON(sis.read(sis.available()), uris);
+ // parseListJSON will catch its own errors, so we
+ // should only go into this catch if list.json
+ // doesn't exist
+ } catch (e) {
+ chan = makeChannel(APP_SEARCH_PREFIX + "list.txt");
+ sis.init(chan.open2());
+ this._parseListTxt(sis.read(sis.available()), uris);
+ }
+ return uris;
+ },
+
+ /**
+ * Loads jar engines asynchronously.
+ *
+ * @returns {Promise} A promise, resolved successfully if finding jar engines
+ * succeeds.
+ */
+ _asyncFindJAREngines: Task.async(function* () {
+ LOG("_asyncFindJAREngines: looking for engines in JARs")
+
+ let listURL = APP_SEARCH_PREFIX + "list.json";
+ let chan = makeChannel(listURL);
+ if (!chan) {
+ LOG("_asyncFindJAREngines: " + APP_SEARCH_PREFIX + " isn't registered");
+ return [];
+ }
+
+ let uris = [];
+
+ // Read list.json to find the engines we need to load.
+ let deferred = Promise.defer();
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsIXMLHttpRequest);
+ request.overrideMimeType("text/plain");
+ request.onload = function(aEvent) {
+ deferred.resolve(aEvent.target.responseText);
+ };
+ request.onerror = function(aEvent) {
+ LOG("_asyncFindJAREngines: failed to read " + listURL);
+ // Couldn't find list.json, try list.txt
+ request.onerror = function(aEvent) {
+ LOG("_asyncFindJAREngines: failed to read " + APP_SEARCH_PREFIX + "list.txt");
+ deferred.resolve("");
+ }
+ request.open("GET", NetUtil.newURI(APP_SEARCH_PREFIX + "list.txt").spec, true);
+ request.send();
+ };
+ request.open("GET", NetUtil.newURI(listURL).spec, true);
+ request.send();
+ let list = yield deferred.promise;
+
+ if (request.responseURL.endsWith(".txt")) {
+ this._parseListTxt(list, uris);
+ } else {
+ this._parseListJSON(list, uris);
+ }
+ return uris;
+ }),
+
+ _parseListJSON: function SRCH_SVC_parseListJSON(list, uris) {
+ let searchSettings;
+ try {
+ searchSettings = JSON.parse(list);
+ } catch (e) {
+ LOG("failing to parse list.json: " + e);
+ return;
+ }
+
+ let jarNames = new Set();
+ for (let region in searchSettings) {
+ // Artifact builds use the full list.json which parses
+ // slightly differently
+ if (!("visibleDefaultEngines" in searchSettings[region])) {
+ continue;
+ }
+ for (let engine of searchSettings[region]["visibleDefaultEngines"]) {
+ jarNames.add(engine);
+ }
+ }
+
+ // Check if we have a useable country specific list of visible default engines.
+ let engineNames;
+ let visibleDefaultEngines = this.getVerifiedGlobalAttr("visibleDefaultEngines");
+ if (visibleDefaultEngines) {
+ engineNames = visibleDefaultEngines.split(",");
+ for (let engineName of engineNames) {
+ // If all engineName values are part of jarNames,
+ // then we can use the country specific list, otherwise ignore it.
+ // The visibleDefaultEngines string containing the name of an engine we
+ // don't ship indicates the server is misconfigured to answer requests
+ // from the specific Firefox version we are running, so ignoring the
+ // value altogether is safer.
+ if (!jarNames.has(engineName)) {
+ LOG("_parseListJSON: ignoring visibleDefaultEngines value because " +
+ engineName + " is not in the jar engines we have found");
+ engineNames = null;
+ break;
+ }
+ }
+ }
+
+ // Fallback to building a list based on the regions in the JSON
+ if (!engineNames || !engineNames.length) {
+ let region;
+ if (Services.prefs.prefHasUserValue("browser.search.region")) {
+ region = Services.prefs.getCharPref("browser.search.region");
+ }
+ if (!region || !(region in searchSettings)) {
+ region = "default";
+ }
+ engineNames = searchSettings[region]["visibleDefaultEngines"];
+ }
+
+ for (let name of engineNames) {
+ uris.push(APP_SEARCH_PREFIX + name + ".xml");
+ }
+
+ // Store this so that it can be used while writing the cache file.
+ this._visibleDefaultEngines = engineNames;
+ },
+
+ _parseListTxt: function SRCH_SVC_parseListTxt(list, uris) {
+ let names = list.split("\n").filter(n => !!n);
+ // This maps the names of our built-in engines to a boolean
+ // indicating whether it should be hidden by default.
+ let jarNames = new Map();
+ for (let name of names) {
+ if (name.endsWith(":hidden")) {
+ name = name.split(":")[0];
+ jarNames.set(name, true);
+ } else {
+ jarNames.set(name, false);
+ }
+ }
+
+ // Check if we have a useable country specific list of visible default engines.
+ let engineNames;
+ let visibleDefaultEngines = this.getVerifiedGlobalAttr("visibleDefaultEngines");
+ if (visibleDefaultEngines) {
+ engineNames = visibleDefaultEngines.split(",");
+
+ for (let engineName of engineNames) {
+ // If all engineName values are part of jarNames,
+ // then we can use the country specific list, otherwise ignore it.
+ // The visibleDefaultEngines string containing the name of an engine we
+ // don't ship indicates the server is misconfigured to answer requests
+ // from the specific Firefox version we are running, so ignoring the
+ // value altogether is safer.
+ if (!jarNames.has(engineName)) {
+ LOG("_parseListTxt: ignoring visibleDefaultEngines value because " +
+ engineName + " is not in the jar engines we have found");
+ engineNames = null;
+ break;
+ }
+ }
+ }
+
+ // Fallback to building a list based on the :hidden suffixes found in list.txt.
+ if (!engineNames) {
+ engineNames = [];
+ for (let [name, hidden] of jarNames) {
+ if (!hidden)
+ engineNames.push(name);
+ }
+ }
+
+ for (let name of engineNames) {
+ uris.push(APP_SEARCH_PREFIX + name + ".xml");
+ }
+
+ // Store this so that it can be used while writing the cache file.
+ this._visibleDefaultEngines = engineNames;
+ },
+
+
+ _saveSortedEngineList: function SRCH_SVC_saveSortedEngineList() {
+ LOG("SRCH_SVC_saveSortedEngineList: starting");
+
+ // Set the useDB pref to indicate that from now on we should use the order
+ // information stored in the database.
+ Services.prefs.setBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", true);
+
+ var engines = this._getSortedEngines(true);
+
+ for (var i = 0; i < engines.length; ++i) {
+ engines[i].setAttr("order", i + 1);
+ }
+
+ LOG("SRCH_SVC_saveSortedEngineList: done");
+ },
+
+ _buildSortedEngineList: function SRCH_SVC_buildSortedEngineList() {
+ LOG("_buildSortedEngineList: building list");
+ var addedEngines = { };
+ this.__sortedEngines = [];
+ var engine;
+
+ // If the user has specified a custom engine order, read the order
+ // information from the metadata instead of the default prefs.
+ if (getBoolPref(BROWSER_SEARCH_PREF + "useDBForOrder", false)) {
+ LOG("_buildSortedEngineList: using db for order");
+
+ // Flag to keep track of whether or not we need to call _saveSortedEngineList.
+ let needToSaveEngineList = false;
+
+ for (let name in this._engines) {
+ let engine = this._engines[name];
+ var orderNumber = engine.getAttr("order");
+
+ // Since the DB isn't regularly cleared, and engine files may disappear
+ // without us knowing, we may already have an engine in this slot. If
+ // that happens, we just skip it - it will be added later on as an
+ // unsorted engine.
+ if (orderNumber && !this.__sortedEngines[orderNumber-1]) {
+ this.__sortedEngines[orderNumber-1] = engine;
+ addedEngines[engine.name] = engine;
+ } else {
+ // We need to call _saveSortedEngineList so this gets sorted out.
+ needToSaveEngineList = true;
+ }
+ }
+
+ // Filter out any nulls for engines that may have been removed
+ var filteredEngines = this.__sortedEngines.filter(function(a) { return !!a; });
+ if (this.__sortedEngines.length != filteredEngines.length)
+ needToSaveEngineList = true;
+ this.__sortedEngines = filteredEngines;
+
+ if (needToSaveEngineList)
+ this._saveSortedEngineList();
+ } else {
+ // The DB isn't being used, so just read the engine order from the prefs
+ var i = 0;
+ var engineName;
+ var prefName;
+
+ try {
+ var extras =
+ Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
+
+ for (prefName of extras) {
+ engineName = Services.prefs.getCharPref(prefName);
+
+ engine = this._engines[engineName];
+ if (!engine || engine.name in addedEngines)
+ continue;
+
+ this.__sortedEngines.push(engine);
+ addedEngines[engine.name] = engine;
+ }
+ }
+ catch (e) { }
+
+ let prefNameBase = getGeoSpecificPrefName(BROWSER_SEARCH_PREF + "order");
+ while (true) {
+ prefName = prefNameBase + "." + (++i);
+ engineName = getLocalizedPref(prefName);
+ if (!engineName)
+ break;
+
+ engine = this._engines[engineName];
+ if (!engine || engine.name in addedEngines)
+ continue;
+
+ this.__sortedEngines.push(engine);
+ addedEngines[engine.name] = engine;
+ }
+ }
+
+ // Array for the remaining engines, alphabetically sorted.
+ let alphaEngines = [];
+
+ for (let name in this._engines) {
+ let engine = this._engines[name];
+ if (!(engine.name in addedEngines))
+ alphaEngines.push(this._engines[engine.name]);
+ }
+
+ let locale = Cc["@mozilla.org/intl/nslocaleservice;1"]
+ .getService(Ci.nsILocaleService)
+ .newLocale(getLocale());
+ let collation = Cc["@mozilla.org/intl/collation-factory;1"]
+ .createInstance(Ci.nsICollationFactory)
+ .CreateCollation(locale);
+ const strength = Ci.nsICollation.kCollationCaseInsensitiveAscii;
+ let comparator = (a, b) => collation.compareString(strength, a.name, b.name);
+ alphaEngines.sort(comparator);
+ return this.__sortedEngines = this.__sortedEngines.concat(alphaEngines);
+ },
+
+ /**
+ * Get a sorted array of engines.
+ * @param aWithHidden
+ * True if hidden plugins should be included in the result.
+ */
+ _getSortedEngines: function SRCH_SVC_getSorted(aWithHidden) {
+ if (aWithHidden)
+ return this._sortedEngines;
+
+ return this._sortedEngines.filter(function (engine) {
+ return !engine.hidden;
+ });
+ },
+
+ // nsIBrowserSearchService
+ init: function SRCH_SVC_init(observer) {
+ LOG("SearchService.init");
+ let self = this;
+ if (!this._initStarted) {
+ TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
+ this._initStarted = true;
+ Task.spawn(function* task() {
+ try {
+ // Complete initialization by calling asynchronous initializer.
+ yield self._asyncInit();
+ TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+ // No need to pursue asynchronous because synchronous fallback was
+ // called and has finished.
+ TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
+ } else {
+ self._initObservers.reject(ex);
+ TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
+ }
+ }
+ });
+ }
+ if (observer) {
+ this._initObservers.promise.then(
+ function onSuccess() {
+ try {
+ observer.onInitComplete(self._initRV);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+ function onError(aReason) {
+ Cu.reportError("Internal error while initializing SearchService: " + aReason);
+ observer.onInitComplete(Components.results.NS_ERROR_UNEXPECTED);
+ }
+ );
+ }
+ },
+
+ get isInitialized() {
+ return gInitialized;
+ },
+
+ getEngines: function SRCH_SVC_getEngines(aCount) {
+ this._ensureInitialized();
+ LOG("getEngines: getting all engines");
+ var engines = this._getSortedEngines(true);
+ aCount.value = engines.length;
+ return engines;
+ },
+
+ getVisibleEngines: function SRCH_SVC_getVisible(aCount) {
+ this._ensureInitialized();
+ LOG("getVisibleEngines: getting all visible engines");
+ var engines = this._getSortedEngines(false);
+ aCount.value = engines.length;
+ return engines;
+ },
+
+ getDefaultEngines: function SRCH_SVC_getDefault(aCount) {
+ this._ensureInitialized();
+ function isDefault(engine) {
+ return engine._isDefault;
+ }
+ var engines = this._sortedEngines.filter(isDefault);
+ var engineOrder = {};
+ var engineName;
+ var i = 1;
+
+ // Build a list of engines which we have ordering information for.
+ // We're rebuilding the list here because _sortedEngines contain the
+ // current order, but we want the original order.
+
+ // First, look at the "browser.search.order.extra" branch.
+ try {
+ var extras = Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
+
+ for (var prefName of extras) {
+ engineName = Services.prefs.getCharPref(prefName);
+
+ if (!(engineName in engineOrder))
+ engineOrder[engineName] = i++;
+ }
+ } catch (e) {
+ LOG("Getting extra order prefs failed: " + e);
+ }
+
+ // Now look through the "browser.search.order" branch.
+ let prefNameBase = getGeoSpecificPrefName(BROWSER_SEARCH_PREF + "order");
+ for (var j = 1; ; j++) {
+ let prefName = prefNameBase + "." + j;
+ engineName = getLocalizedPref(prefName);
+ if (!engineName)
+ break;
+
+ if (!(engineName in engineOrder))
+ engineOrder[engineName] = i++;
+ }
+
+ LOG("getDefaultEngines: engineOrder: " + engineOrder.toSource());
+
+ function compareEngines (a, b) {
+ var aIdx = engineOrder[a.name];
+ var bIdx = engineOrder[b.name];
+
+ if (aIdx && bIdx)
+ return aIdx - bIdx;
+ if (aIdx)
+ return -1;
+ if (bIdx)
+ return 1;
+
+ return a.name.localeCompare(b.name);
+ }
+ engines.sort(compareEngines);
+
+ aCount.value = engines.length;
+ return engines;
+ },
+
+ getEngineByName: function SRCH_SVC_getEngineByName(aEngineName) {
+ this._ensureInitialized();
+ return this._engines[aEngineName] || null;
+ },
+
+ getEngineByAlias: function SRCH_SVC_getEngineByAlias(aAlias) {
+ this._ensureInitialized();
+ for (var engineName in this._engines) {
+ var engine = this._engines[engineName];
+ if (engine && engine.alias == aAlias)
+ return engine;
+ }
+ return null;
+ },
+
+ addEngineWithDetails: function SRCH_SVC_addEWD(aName, aIconURL, aAlias,
+ aDescription, aMethod,
+ aTemplate, aExtensionID) {
+ this._ensureInitialized();
+ if (!aName)
+ FAIL("Invalid name passed to addEngineWithDetails!");
+ if (!aMethod)
+ FAIL("Invalid method passed to addEngineWithDetails!");
+ if (!aTemplate)
+ FAIL("Invalid template passed to addEngineWithDetails!");
+ if (this._engines[aName])
+ FAIL("An engine with that name already exists!", Cr.NS_ERROR_FILE_ALREADY_EXISTS);
+
+ var engine = new Engine(sanitizeName(aName), false);
+ engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
+ aMethod, aTemplate, aExtensionID);
+ engine._loadPath = "[other]addEngineWithDetails";
+ this._addEngineToStore(engine);
+ },
+
+ addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL,
+ aConfirm, aCallback) {
+ LOG("addEngine: Adding \"" + aEngineURL + "\".");
+ this._ensureInitialized();
+ try {
+ var uri = makeURI(aEngineURL);
+ var engine = new Engine(uri, false);
+ if (aCallback) {
+ engine._installCallback = function (errorCode) {
+ try {
+ if (errorCode == null)
+ aCallback.onSuccess(engine);
+ else
+ aCallback.onError(errorCode);
+ } catch (ex) {
+ Cu.reportError("Error invoking addEngine install callback: " + ex);
+ }
+ // Clear the reference to the callback now that it's been invoked.
+ engine._installCallback = null;
+ };
+ }
+ engine._initFromURIAndLoad(uri);
+ } catch (ex) {
+ // Drop the reference to the callback, if set
+ if (engine)
+ engine._installCallback = null;
+ FAIL("addEngine: Error adding engine:\n" + ex, Cr.NS_ERROR_FAILURE);
+ }
+ engine._setIcon(aIconURL, false);
+ engine._confirm = aConfirm;
+ },
+
+ removeEngine: function SRCH_SVC_removeEngine(aEngine) {
+ this._ensureInitialized();
+ if (!aEngine)
+ FAIL("no engine passed to removeEngine!");
+
+ var engineToRemove = null;
+ for (var e in this._engines) {
+ if (aEngine.wrappedJSObject == this._engines[e])
+ engineToRemove = this._engines[e];
+ }
+
+ if (!engineToRemove)
+ FAIL("removeEngine: Can't find engine to remove!", Cr.NS_ERROR_FILE_NOT_FOUND);
+
+ if (engineToRemove == this.currentEngine) {
+ this._currentEngine = null;
+ }
+
+ if (engineToRemove._readOnly) {
+ // Just hide it (the "hidden" setter will notify) and remove its alias to
+ // avoid future conflicts with other engines.
+ engineToRemove.hidden = true;
+ engineToRemove.alias = null;
+ } else {
+ // Remove the engine file from disk if we had a legacy file in the profile.
+ if (engineToRemove._filePath) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.persistentDescriptor = engineToRemove._filePath;
+ if (file.exists()) {
+ file.remove(false);
+ }
+ engineToRemove._filePath = null;
+ }
+
+ // Remove the engine from _sortedEngines
+ var index = this._sortedEngines.indexOf(engineToRemove);
+ if (index == -1)
+ FAIL("Can't find engine to remove in _sortedEngines!", Cr.NS_ERROR_FAILURE);
+ this.__sortedEngines.splice(index, 1);
+
+ // Remove the engine from the internal store
+ delete this._engines[engineToRemove.name];
+
+ // Since we removed an engine, we need to update the preferences.
+ this._saveSortedEngineList();
+ }
+ notifyAction(engineToRemove, SEARCH_ENGINE_REMOVED);
+ },
+
+ moveEngine: function SRCH_SVC_moveEngine(aEngine, aNewIndex) {
+ this._ensureInitialized();
+ if ((aNewIndex > this._sortedEngines.length) || (aNewIndex < 0))
+ FAIL("SRCH_SVC_moveEngine: Index out of bounds!");
+ if (!(aEngine instanceof Ci.nsISearchEngine))
+ FAIL("SRCH_SVC_moveEngine: Invalid engine passed to moveEngine!");
+ if (aEngine.hidden)
+ FAIL("moveEngine: Can't move a hidden engine!", Cr.NS_ERROR_FAILURE);
+
+ var engine = aEngine.wrappedJSObject;
+
+ var currentIndex = this._sortedEngines.indexOf(engine);
+ if (currentIndex == -1)
+ FAIL("moveEngine: Can't find engine to move!", Cr.NS_ERROR_UNEXPECTED);
+
+ // Our callers only take into account non-hidden engines when calculating
+ // aNewIndex, but we need to move it in the array of all engines, so we
+ // need to adjust aNewIndex accordingly. To do this, we count the number
+ // of hidden engines in the list before the engine that we're taking the
+ // place of. We do this by first finding newIndexEngine (the engine that
+ // we were supposed to replace) and then iterating through the complete
+ // engine list until we reach it, increasing aNewIndex for each hidden
+ // engine we find on our way there.
+ //
+ // This could be further simplified by having our caller pass in
+ // newIndexEngine directly instead of aNewIndex.
+ var newIndexEngine = this._getSortedEngines(false)[aNewIndex];
+ if (!newIndexEngine)
+ FAIL("moveEngine: Can't find engine to replace!", Cr.NS_ERROR_UNEXPECTED);
+
+ for (var i = 0; i < this._sortedEngines.length; ++i) {
+ if (newIndexEngine == this._sortedEngines[i])
+ break;
+ if (this._sortedEngines[i].hidden)
+ aNewIndex++;
+ }
+
+ if (currentIndex == aNewIndex)
+ return; // nothing to do!
+
+ // Move the engine
+ var movedEngine = this.__sortedEngines.splice(currentIndex, 1)[0];
+ this.__sortedEngines.splice(aNewIndex, 0, movedEngine);
+
+ notifyAction(engine, SEARCH_ENGINE_CHANGED);
+
+ // Since we moved an engine, we need to update the preferences.
+ this._saveSortedEngineList();
+ },
+
+ restoreDefaultEngines: function SRCH_SVC_resetDefaultEngines() {
+ this._ensureInitialized();
+ for (let name in this._engines) {
+ let e = this._engines[name];
+ // Unhide all default engines
+ if (e.hidden && e._isDefault)
+ e.hidden = false;
+ }
+ },
+
+ get defaultEngine() { return this.currentEngine; },
+
+ set defaultEngine(val) {
+ this.currentEngine = val;
+ },
+
+ get currentEngine() {
+ this._ensureInitialized();
+ if (!this._currentEngine) {
+ let name = this.getGlobalAttr("current");
+ let engine = this.getEngineByName(name);
+ if (engine && (this.getGlobalAttr("hash") == getVerificationHash(name) ||
+ engine._isDefault)) {
+ // If the current engine is a default one, we can relax the
+ // verification hash check to reduce the annoyance for users who
+ // backup/sync their profile in custom ways.
+ this._currentEngine = engine;
+ }
+ if (!name)
+ this._currentEngine = this.originalDefaultEngine;
+ }
+
+ // If the current engine is not set or hidden, we fallback...
+ if (!this._currentEngine || this._currentEngine.hidden) {
+ // first to the original default engine
+ let originalDefault = this.originalDefaultEngine;
+ if (!originalDefault || originalDefault.hidden) {
+ // then to the first visible engine
+ let firstVisible = this._getSortedEngines(false)[0];
+ if (firstVisible && !firstVisible.hidden) {
+ this.currentEngine = firstVisible;
+ return firstVisible;
+ }
+ // and finally as a last resort we unhide the original default engine.
+ if (originalDefault)
+ originalDefault.hidden = false;
+ }
+ if (!originalDefault)
+ return null;
+
+ // If the current engine wasn't set or was hidden, we used a fallback
+ // to pick a new current engine. As soon as we return it, this new
+ // current engine will become user-visible, so we should persist it.
+ // by calling the setter.
+ this.currentEngine = originalDefault;
+ }
+
+ return this._currentEngine;
+ },
+
+ set currentEngine(val) {
+ this._ensureInitialized();
+ // Sometimes we get wrapped nsISearchEngine objects (external XPCOM callers),
+ // and sometimes we get raw Engine JS objects (callers in this file), so
+ // handle both.
+ if (!(val instanceof Ci.nsISearchEngine) && !(val instanceof Engine))
+ FAIL("Invalid argument passed to currentEngine setter");
+
+ var newCurrentEngine = this.getEngineByName(val.name);
+ if (!newCurrentEngine)
+ FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
+
+ if (!newCurrentEngine._isDefault) {
+ // If a non default engine is being set as the current engine, ensure
+ // its loadPath has a verification hash.
+ if (!newCurrentEngine._loadPath)
+ newCurrentEngine._loadPath = "[other]unknown";
+ let loadPathHash = getVerificationHash(newCurrentEngine._loadPath);
+ let currentHash = newCurrentEngine.getAttr("loadPathHash");
+ if (!currentHash || currentHash != loadPathHash) {
+ newCurrentEngine.setAttr("loadPathHash", loadPathHash);
+ notifyAction(newCurrentEngine, SEARCH_ENGINE_CHANGED);
+ }
+ }
+
+ if (newCurrentEngine == this._currentEngine)
+ return;
+
+ this._currentEngine = newCurrentEngine;
+
+ // If we change the default engine in the future, that change should impact
+ // users who have switched away from and then back to the build's "default"
+ // engine. So clear the user pref when the currentEngine is set to the
+ // build's default engine, so that the currentEngine getter falls back to
+ // whatever the default is.
+ let newName = this._currentEngine.name;
+ if (this._currentEngine == this.originalDefaultEngine) {
+ newName = "";
+ }
+
+ this.setGlobalAttr("current", newName);
+ this.setGlobalAttr("hash", getVerificationHash(newName));
+
+ notifyAction(this._currentEngine, SEARCH_ENGINE_DEFAULT);
+ notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
+ },
+
+ getDefaultEngineInfo() {
+ let result = {};
+
+ let engine;
+ try {
+ engine = this.defaultEngine;
+ } catch (e) {
+ // The defaultEngine getter will throw if there's no engine at all,
+ // which shouldn't happen unless an add-on or a test deleted all of them.
+ // Our preferences UI doesn't let users do that.
+ Cu.reportError("getDefaultEngineInfo: No default engine");
+ }
+
+ if (!engine) {
+ result.name = "NONE";
+ } else {
+ if (engine.name)
+ result.name = engine.name;
+
+ result.loadPath = engine._loadPath;
+
+ let origin;
+ if (engine._isDefault)
+ origin = "default";
+ else {
+ let currentHash = engine.getAttr("loadPathHash");
+ if (!currentHash)
+ origin = "unverified";
+ else {
+ let loadPathHash = getVerificationHash(engine._loadPath);
+ origin = currentHash == loadPathHash ? "verified" : "invalid";
+ }
+ }
+ result.origin = origin;
+
+ // For privacy, we only collect the submission URL for default engines...
+ let sendSubmissionURL = engine._isDefault;
+
+ // ... or engines sorted by default near the top of the list.
+ if (!sendSubmissionURL) {
+ let extras =
+ Services.prefs.getChildList(BROWSER_SEARCH_PREF + "order.extra.");
+
+ for (let prefName of extras) {
+ try {
+ if (result.name == Services.prefs.getCharPref(prefName)) {
+ sendSubmissionURL = true;
+ break;
+ }
+ } catch (e) {}
+ }
+
+ let prefNameBase = getGeoSpecificPrefName(BROWSER_SEARCH_PREF + "order");
+ let i = 0;
+ while (!sendSubmissionURL) {
+ let prefName = prefNameBase + "." + (++i);
+ let engineName = getLocalizedPref(prefName);
+ if (!engineName)
+ break;
+ if (result.name == engineName) {
+ sendSubmissionURL = true;
+ break;
+ }
+ }
+ }
+
+ if (sendSubmissionURL) {
+ let uri = engine._getURLOfType("text/html")
+ .getSubmission("", engine, "searchbar").uri;
+ uri.userPass = ""; // Avoid reporting a username or password.
+ result.submissionURL = uri.spec;
+ }
+ }
+
+ return result;
+ },
+
+ _recordEngineTelemetry: function() {
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_ENGINE_COUNT")
+ .add(Object.keys(this._engines).length);
+ let hasUpdates = false;
+ let hasIconUpdates = false;
+ for (let name in this._engines) {
+ let engine = this._engines[name];
+ if (engine._hasUpdates) {
+ hasUpdates = true;
+ if (engine._iconUpdateURL) {
+ hasIconUpdates = true;
+ break;
+ }
+ }
+ }
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_HAS_UPDATES").add(hasUpdates);
+ Services.telemetry.getHistogramById("SEARCH_SERVICE_HAS_ICON_UPDATES").add(hasIconUpdates);
+ },
+
+ /**
+ * This map is built lazily after the available search engines change. It
+ * allows quick parsing of an URL representing a search submission into the
+ * search engine name and original terms.
+ *
+ * The keys are strings containing the domain name and lowercase path of the
+ * engine submission, for example "www.google.com/search".
+ *
+ * The values are objects with these properties:
+ * {
+ * engine: The associated nsISearchEngine.
+ * termsParameterName: Name of the URL parameter containing the search
+ * terms, for example "q".
+ * }
+ */
+ _parseSubmissionMap: null,
+
+ _buildParseSubmissionMap: function SRCH_SVC__buildParseSubmissionMap() {
+ LOG("_buildParseSubmissionMap");
+ this._parseSubmissionMap = new Map();
+
+ // Used only while building the map, indicates which entries do not refer to
+ // the main domain of the engine but to an alternate domain, for example
+ // "www.google.fr" for the "www.google.com" search engine.
+ let keysOfAlternates = new Set();
+
+ for (let engine of this._sortedEngines) {
+ LOG("Processing engine: " + engine.name);
+
+ if (engine.hidden) {
+ LOG("Engine is hidden.");
+ continue;
+ }
+
+ let urlParsingInfo = engine.getURLParsingInfo();
+ if (!urlParsingInfo) {
+ LOG("Engine does not support URL parsing.");
+ continue;
+ }
+
+ // Store the same object on each matching map key, as an optimization.
+ let mapValueForEngine = {
+ engine: engine,
+ termsParameterName: urlParsingInfo.termsParameterName,
+ };
+
+ let processDomain = (domain, isAlternate) => {
+ let key = domain + urlParsingInfo.path;
+
+ // Apply the logic for which main domains take priority over alternate
+ // domains, even if they are found later in the ordered engine list.
+ let existingEntry = this._parseSubmissionMap.get(key);
+ if (!existingEntry) {
+ LOG("Adding new entry: " + key);
+ if (isAlternate) {
+ keysOfAlternates.add(key);
+ }
+ } else if (!isAlternate && keysOfAlternates.has(key)) {
+ LOG("Overriding alternate entry: " + key +
+ " (" + existingEntry.engine.name + ")");
+ keysOfAlternates.delete(key);
+ } else {
+ LOG("Keeping existing entry: " + key +
+ " (" + existingEntry.engine.name + ")");
+ return;
+ }
+
+ this._parseSubmissionMap.set(key, mapValueForEngine);
+ };
+
+ processDomain(urlParsingInfo.mainDomain, false);
+ SearchStaticData.getAlternateDomains(urlParsingInfo.mainDomain)
+ .forEach(d => processDomain(d, true));
+ }
+ },
+
+ /**
+ * Checks to see if any engine has an EngineURL of type URLTYPE_SEARCH_HTML
+ * for this request-method, template URL, and query params.
+ */
+ hasEngineWithURL: function(method, template, formData) {
+ this._ensureInitialized();
+
+ // Quick helper method to ensure formData filtered/sorted for compares.
+ let getSortedFormData = data => {
+ return data.filter(a => a.name && a.value).sort((a, b) => {
+ if (a.name > b.name) {
+ return 1;
+ } else if (b.name > a.name) {
+ return -1;
+ } else if (a.value > b.value) {
+ return 1;
+ }
+ return (b.value > a.value) ? -1 : 0;
+ });
+ };
+
+ // Sanitize method, ensure formData is pre-sorted.
+ let methodUpper = method.toUpperCase();
+ let sortedFormData = getSortedFormData(formData);
+ let sortedFormLength = sortedFormData.length;
+
+ return this._getSortedEngines(false).some(engine => {
+ return engine._urls.some(url => {
+ // Not an engineURL match if type, method, url, #params don't match.
+ if (url.type != URLTYPE_SEARCH_HTML ||
+ url.method != methodUpper ||
+ url.template != template ||
+ url.params.length != sortedFormLength) {
+ return false;
+ }
+
+ // Ensure engineURL formData is pre-sorted. Then, we're
+ // not an engineURL match if any queryParam doesn't compare.
+ let sortedParams = getSortedFormData(url.params);
+ for (let i = 0; i < sortedFormLength; i++) {
+ let formData = sortedFormData[i];
+ let param = sortedParams[i];
+ if (param.name != formData.name ||
+ param.value != formData.value ||
+ param.purpose != formData.purpose) {
+ return false;
+ }
+ }
+ // Else we're a match.
+ return true;
+ });
+ });
+ },
+
+ parseSubmissionURL: function SRCH_SVC_parseSubmissionURL(aURL) {
+ this._ensureInitialized();
+ LOG("parseSubmissionURL: Parsing \"" + aURL + "\".");
+
+ if (!this._parseSubmissionMap) {
+ this._buildParseSubmissionMap();
+ }
+
+ // Extract the elements of the provided URL first.
+ let soughtKey, soughtQuery;
+ try {
+ let soughtUrl = NetUtil.newURI(aURL).QueryInterface(Ci.nsIURL);
+
+ // Exclude any URL that is not HTTP or HTTPS from the beginning.
+ if (soughtUrl.scheme != "http" && soughtUrl.scheme != "https") {
+ LOG("The URL scheme is not HTTP or HTTPS.");
+ return gEmptyParseSubmissionResult;
+ }
+
+ // Reading these URL properties may fail and raise an exception.
+ soughtKey = soughtUrl.host + soughtUrl.filePath.toLowerCase();
+ soughtQuery = soughtUrl.query;
+ } catch (ex) {
+ // Errors while parsing the URL or accessing the properties are not fatal.
+ LOG("The value does not look like a structured URL.");
+ return gEmptyParseSubmissionResult;
+ }
+
+ // Look up the domain and path in the map to identify the search engine.
+ let mapEntry = this._parseSubmissionMap.get(soughtKey);
+ if (!mapEntry) {
+ LOG("No engine associated with domain and path: " + soughtKey);
+ return gEmptyParseSubmissionResult;
+ }
+
+ // Extract the search terms from the parameter, for example "caff%C3%A8"
+ // from the URL "https://www.google.com/search?q=caff%C3%A8&client=firefox".
+ let encodedTerms = null;
+ for (let param of soughtQuery.split("&")) {
+ let equalPos = param.indexOf("=");
+ if (equalPos != -1 &&
+ param.substr(0, equalPos) == mapEntry.termsParameterName) {
+ // This is the parameter we are looking for.
+ encodedTerms = param.substr(equalPos + 1);
+ break;
+ }
+ }
+ if (encodedTerms === null) {
+ LOG("Missing terms parameter: " + mapEntry.termsParameterName);
+ return gEmptyParseSubmissionResult;
+ }
+
+ let length = 0;
+ let offset = aURL.indexOf("?") + 1;
+ let query = aURL.slice(offset);
+ // Iterate a second time over the original input string to determine the
+ // correct search term offset and length in the original encoding.
+ for (let param of query.split("&")) {
+ let equalPos = param.indexOf("=");
+ if (equalPos != -1 &&
+ param.substr(0, equalPos) == mapEntry.termsParameterName) {
+ // This is the parameter we are looking for.
+ offset += equalPos + 1;
+ length = param.length - equalPos - 1;
+ break;
+ }
+ offset += param.length + 1;
+ }
+
+ // Decode the terms using the charset defined in the search engine.
+ let terms;
+ try {
+ terms = gTextToSubURI.UnEscapeAndConvert(
+ mapEntry.engine.queryCharset,
+ encodedTerms.replace(/\+/g, " "));
+ } catch (ex) {
+ // Decoding errors will cause this match to be ignored.
+ LOG("Parameter decoding failed. Charset: " +
+ mapEntry.engine.queryCharset);
+ return gEmptyParseSubmissionResult;
+ }
+
+ LOG("Match found. Terms: " + terms);
+ return new ParseSubmissionResult(mapEntry.engine, terms, offset, length);
+ },
+
+ // nsIObserver
+ observe: function SRCH_SVC_observe(aEngine, aTopic, aVerb) {
+ switch (aTopic) {
+ case SEARCH_ENGINE_TOPIC:
+ switch (aVerb) {
+ case SEARCH_ENGINE_LOADED:
+ var engine = aEngine.QueryInterface(Ci.nsISearchEngine);
+ LOG("nsSearchService::observe: Done installation of " + engine.name
+ + ".");
+ this._addEngineToStore(engine.wrappedJSObject);
+ if (engine.wrappedJSObject._useNow) {
+ LOG("nsSearchService::observe: setting current");
+ this.currentEngine = aEngine;
+ }
+ // The addition of the engine to the store always triggers an ADDED
+ // or a CHANGED notification, that will trigger the task below.
+ break;
+ case SEARCH_ENGINE_ADDED:
+ case SEARCH_ENGINE_CHANGED:
+ case SEARCH_ENGINE_REMOVED:
+ this.batchTask.disarm();
+ this.batchTask.arm();
+ // Invalidate the map used to parse URLs to search engines.
+ this._parseSubmissionMap = null;
+ break;
+ }
+ break;
+
+ case QUIT_APPLICATION_TOPIC:
+ this._removeObservers();
+ break;
+
+ case "nsPref:changed":
+ if (aVerb == LOCALE_PREF) {
+ // Locale changed. Re-init. We rely on observers, because we can't
+ // return this promise to anyone.
+ this._asyncReInit();
+ break;
+ }
+ }
+ },
+
+ // nsITimerCallback
+ notify: function SRCH_SVC_notify(aTimer) {
+ LOG("_notify: checking for updates");
+
+ if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true))
+ return;
+
+ // Our timer has expired, but unfortunately, we can't get any data from it.
+ // Therefore, we need to walk our engine-list, looking for expired engines
+ var currentTime = Date.now();
+ LOG("currentTime: " + currentTime);
+ for (let name in this._engines) {
+ let engine = this._engines[name].wrappedJSObject;
+ if (!engine._hasUpdates)
+ continue;
+
+ LOG("checking " + engine.name);
+
+ var expirTime = engine.getAttr("updateexpir");
+ LOG("expirTime: " + expirTime + "\nupdateURL: " + engine._updateURL +
+ "\niconUpdateURL: " + engine._iconUpdateURL);
+
+ var engineExpired = expirTime <= currentTime;
+
+ if (!expirTime || !engineExpired) {
+ LOG("skipping engine");
+ continue;
+ }
+
+ LOG(engine.name + " has expired");
+
+ engineUpdateService.update(engine);
+
+ // Schedule the next update
+ engineUpdateService.scheduleNextUpdate(engine);
+
+ } // end engine iteration
+ },
+
+ _addObservers: function SRCH_SVC_addObservers() {
+ if (this._observersAdded) {
+ // There might be a race between synchronous and asynchronous
+ // initialization for which we try to register the observers twice.
+ return;
+ }
+ this._observersAdded = true;
+
+ Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false);
+ Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false);
+
+ if (AppConstants.MOZ_BUILD_APP == "mobile/android") {
+ Services.prefs.addObserver(LOCALE_PREF, this, false);
+ }
+
+ // The current stage of shutdown. Used to help analyze crash
+ // signatures in case of shutdown timeout.
+ let shutdownState = {
+ step: "Not started",
+ latestError: {
+ message: undefined,
+ stack: undefined
+ }
+ };
+ OS.File.profileBeforeChange.addBlocker(
+ "Search service: shutting down",
+ () => Task.spawn(function* () {
+ if (this._batchTask) {
+ shutdownState.step = "Finalizing batched task";
+ try {
+ yield this._batchTask.finalize();
+ shutdownState.step = "Batched task finalized";
+ } catch (ex) {
+ shutdownState.step = "Batched task failed to finalize";
+
+ shutdownState.latestError.message = "" + ex;
+ if (ex && typeof ex == "object") {
+ shutdownState.latestError.stack = ex.stack || undefined;
+ }
+
+ // Ensure that error is reported and that it causes tests
+ // to fail.
+ Promise.reject(ex);
+ }
+ }
+ }.bind(this)),
+
+ () => shutdownState
+ );
+ },
+ _observersAdded: false,
+
+ _removeObservers: function SRCH_SVC_removeObservers() {
+ Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
+ Services.obs.removeObserver(this, QUIT_APPLICATION_TOPIC);
+
+ if (AppConstants.MOZ_BUILD_APP == "mobile/android") {
+ Services.prefs.removeObserver(LOCALE_PREF, this);
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIBrowserSearchService,
+ Ci.nsIObserver,
+ Ci.nsITimerCallback
+ ])
+};
+
+
+const SEARCH_UPDATE_LOG_PREFIX = "*** Search update: ";
+
+/**
+ * Outputs aText to the JavaScript console as well as to stdout, if the search
+ * logging pref (browser.search.update.log) is set to true.
+ */
+function ULOG(aText) {
+ if (getBoolPref(BROWSER_SEARCH_PREF + "update.log", false)) {
+ dump(SEARCH_UPDATE_LOG_PREFIX + aText + "\n");
+ Services.console.logStringMessage(aText);
+ }
+}
+
+var engineUpdateService = {
+ scheduleNextUpdate: function eus_scheduleNextUpdate(aEngine) {
+ var interval = aEngine._updateInterval || SEARCH_DEFAULT_UPDATE_INTERVAL;
+ var milliseconds = interval * 86400000; // |interval| is in days
+ aEngine.setAttr("updateexpir", Date.now() + milliseconds);
+ },
+
+ update: function eus_Update(aEngine) {
+ let engine = aEngine.wrappedJSObject;
+ ULOG("update called for " + aEngine._name);
+ if (!getBoolPref(BROWSER_SEARCH_PREF + "update", true) || !engine._hasUpdates)
+ return;
+
+ let testEngine = null;
+ let updateURL = engine._getURLOfType(URLTYPE_OPENSEARCH);
+ let updateURI = (updateURL && updateURL._hasRelation("self")) ?
+ updateURL.getSubmission("", engine).uri :
+ makeURI(engine._updateURL);
+ if (updateURI) {
+ if (engine._isDefault && !updateURI.schemeIs("https")) {
+ ULOG("Invalid scheme for default engine update");
+ return;
+ }
+
+ ULOG("updating " + engine.name + " from " + updateURI.spec);
+ testEngine = new Engine(updateURI, false);
+ testEngine._engineToUpdate = engine;
+ testEngine._initFromURIAndLoad(updateURI);
+ } else
+ ULOG("invalid updateURI");
+
+ if (engine._iconUpdateURL) {
+ // If we're updating the engine too, use the new engine object,
+ // otherwise use the existing engine object.
+ (testEngine || engine)._setIcon(engine._iconUpdateURL, true);
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SearchService]);
diff --git a/toolkit/components/search/nsSearchSuggestions.js b/toolkit/components/search/nsSearchSuggestions.js
new file mode 100644
index 0000000000..a05d8b4b45
--- /dev/null
+++ b/toolkit/components/search/nsSearchSuggestions.js
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
+ "resource://gre/modules/SearchSuggestionController.jsm");
+
+/**
+ * SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
+ * and can collect results for a given search by using this._suggestionController.
+ * We do it this way since the AutoCompleteController in Mozilla requires a
+ * unique XPCOM Service for every search provider, even if the logic for two
+ * providers is identical.
+ * @constructor
+ */
+function SuggestAutoComplete() {
+ this._init();
+}
+SuggestAutoComplete.prototype = {
+
+ _init: function() {
+ this._suggestionController = new SearchSuggestionController(obj => this.onResultsReturned(obj));
+ this._suggestionController.maxLocalResults = this._historyLimit;
+ },
+
+ get _suggestionLabel() {
+ let bundle = Services.strings.createBundle("chrome://global/locale/search/search.properties");
+ let label = bundle.GetStringFromName("suggestion_label");
+ Object.defineProperty(SuggestAutoComplete.prototype, "_suggestionLabel", {value: label});
+ return label;
+ },
+
+ /**
+ * The object implementing nsIAutoCompleteObserver that we notify when
+ * we have found results
+ * @private
+ */
+ _listener: null,
+
+ /**
+ * Maximum number of history items displayed. This is capped at 7
+ * because the primary consumer (Firefox search bar) displays 10 rows
+ * by default, and so we want to leave some space for suggestions
+ * to be visible.
+ */
+ _historyLimit: 7,
+
+ /**
+ * Callback for handling results from SearchSuggestionController.jsm
+ * @private
+ */
+ onResultsReturned: function(results) {
+ let finalResults = [];
+ let finalComments = [];
+
+ // If form history has results, add them to the list.
+ for (let i = 0; i < results.local.length; ++i) {
+ finalResults.push(results.local[i]);
+ finalComments.push("");
+ }
+
+ // If there are remote matches, add them.
+ if (results.remote.length) {
+ // "comments" column values for suggestions starts as empty strings
+ let comments = new Array(results.remote.length).fill("", 1);
+ comments[0] = this._suggestionLabel;
+ // now put the history results above the suggestions
+ finalResults = finalResults.concat(results.remote);
+ finalComments = finalComments.concat(comments);
+ }
+
+ // Notify the FE of our new results
+ this.onResultsReady(results.term, finalResults, finalComments, results.formHistoryResult);
+ },
+
+ /**
+ * Notifies the front end of new results.
+ * @param searchString the user's query string
+ * @param results an array of results to the search
+ * @param comments an array of metadata corresponding to the results
+ * @private
+ */
+ onResultsReady: function(searchString, results, comments, formHistoryResult) {
+ if (this._listener) {
+ // Create a copy of the results array to use as labels, since
+ // FormAutoCompleteResult doesn't like being passed the same array
+ // for both.
+ let labels = results.slice();
+ let result = new FormAutoCompleteResult(
+ searchString,
+ Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+ 0,
+ "",
+ results,
+ labels,
+ comments,
+ formHistoryResult);
+
+ this._listener.onSearchResult(this, result);
+
+ // Null out listener to make sure we don't notify it twice
+ this._listener = null;
+ }
+ },
+
+ /**
+ * Initiates the search result gathering process. Part of
+ * nsIAutoCompleteSearch implementation.
+ *
+ * @param searchString the user's query string
+ * @param searchParam unused, "an extra parameter"; even though
+ * this parameter and the next are unused, pass
+ * them through in case the form history
+ * service wants them
+ * @param previousResult unused, a client-cached store of the previous
+ * generated resultset for faster searching.
+ * @param listener object implementing nsIAutoCompleteObserver which
+ * we notify when results are ready.
+ */
+ startSearch: function(searchString, searchParam, previousResult, listener) {
+ // Don't reuse a previous form history result when it no longer applies.
+ if (!previousResult)
+ this._formHistoryResult = null;
+
+ var formHistorySearchParam = searchParam.split("|")[0];
+
+ // Receive the information about the privacy mode of the window to which
+ // this search box belongs. The front-end's search.xml bindings passes this
+ // information in the searchParam parameter. The alternative would have
+ // been to modify nsIAutoCompleteSearch to add an argument to startSearch
+ // and patch all of autocomplete to be aware of this, but the searchParam
+ // argument is already an opaque argument, so this solution is hopefully
+ // less hackish (although still gross.)
+ var privacyMode = (searchParam.split("|")[1] == "private");
+
+ // Start search immediately if possible, otherwise once the search
+ // service is initialized
+ if (Services.search.isInitialized) {
+ this._triggerSearch(searchString, formHistorySearchParam, listener, privacyMode);
+ return;
+ }
+
+ Services.search.init((function startSearch_cb(aResult) {
+ if (!Components.isSuccessCode(aResult)) {
+ Cu.reportError("Could not initialize search service, bailing out: " + aResult);
+ return;
+ }
+ this._triggerSearch(searchString, formHistorySearchParam, listener, privacyMode);
+ }).bind(this));
+ },
+
+ /**
+ * Actual implementation of search.
+ */
+ _triggerSearch: function(searchString, searchParam, listener, privacyMode) {
+ this._listener = listener;
+ this._suggestionController.fetch(searchString,
+ privacyMode,
+ Services.search.currentEngine);
+ },
+
+ /**
+ * Ends the search result gathering process. Part of nsIAutoCompleteSearch
+ * implementation.
+ */
+ stopSearch: function() {
+ this._suggestionController.stop();
+ },
+
+ // nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch,
+ Ci.nsIAutoCompleteObserver])
+};
+
+/**
+ * SearchSuggestAutoComplete is a service implementation that handles suggest
+ * results specific to web searches.
+ * @constructor
+ */
+function SearchSuggestAutoComplete() {
+ // This calls _init() in the parent class (SuggestAutoComplete) via the
+ // prototype, below.
+ this._init();
+}
+SearchSuggestAutoComplete.prototype = {
+ classID: Components.ID("{aa892eb4-ffbf-477d-9f9a-06c995ae9f27}"),
+ __proto__: SuggestAutoComplete.prototype,
+ serviceURL: ""
+};
+
+var component = [SearchSuggestAutoComplete];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/toolkit/components/search/nsSidebar.js b/toolkit/components/search/nsSidebar.js
new file mode 100644
index 0000000000..63976cba70
--- /dev/null
+++ b/toolkit/components/search/nsSidebar.js
@@ -0,0 +1,66 @@
+/* -*- 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/. */
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// File extension for Sherlock search plugin description files
+const SHERLOCK_FILE_EXT_REGEXP = /\.src$/i;
+
+function nsSidebar() {
+}
+
+nsSidebar.prototype = {
+ init: function(window) {
+ this.window = window;
+ try {
+ this.mm = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ // Deprecated, only left here to avoid breaking old browser-detection scripts.
+ addSearchEngine: function(engineURL, iconURL, suggestedTitle, suggestedCategory) {
+ if (SHERLOCK_FILE_EXT_REGEXP.test(engineURL)) {
+ Cu.reportError("Installing Sherlock search plugins is no longer supported.");
+ return;
+ }
+
+ this.AddSearchProvider(engineURL);
+ },
+
+ // This function implements window.external.AddSearchProvider().
+ // The capitalization, although nonstandard here, is to match other browsers'
+ // APIs and is therefore important.
+ AddSearchProvider: function(engineURL) {
+ if (!this.mm) {
+ Cu.reportError(`Installing a search provider from this context is not currently supported: ${Error().stack}.`);
+ return;
+ }
+
+ this.mm.sendAsyncMessage("Search:AddEngine", {
+ pageURL: this.window.document.documentURIObject.spec,
+ engineURL
+ });
+ },
+
+ // This function exists to implement window.external.IsSearchProviderInstalled(),
+ // for compatibility with other browsers. The function has been deprecated
+ // and so will not be implemented.
+ IsSearchProviderInstalled: function(engineURL) {
+ return 0;
+ },
+
+ classID: Components.ID("{22117140-9c6e-11d3-aaf1-00805f8a4905}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
+ Ci.nsIDOMGlobalPropertyInitializer])
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsSidebar]);
diff --git a/toolkit/components/search/tests/xpcshell/.eslintrc.js b/toolkit/components/search/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/search/tests/xpcshell/data/chrome.manifest b/toolkit/components/search/tests/xpcshell/data/chrome.manifest
new file mode 100644
index 0000000000..ec412e0508
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/chrome.manifest
@@ -0,0 +1,3 @@
+locale testsearchplugin ar jar:jar:searchTest.jar!/chrome/searchTest.jar!/
+content testsearchplugin ./
+
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-addon.xml b/toolkit/components/search/tests/xpcshell/data/engine-addon.xml
new file mode 100644
index 0000000000..24e53d0c1c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-addon.xml
@@ -0,0 +1,8 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>addon</ShortName>
+<Description>addon</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-app.xml b/toolkit/components/search/tests/xpcshell/data/engine-app.xml
new file mode 100644
index 0000000000..fe1b3a67c4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-app.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>TestEngineApp</ShortName>
+<Description>A test search engine installed in the application directory</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Url type="text/html" method="GET" template="http://localhost/" resultdomain="localhost">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml
new file mode 100644
index 0000000000..856732c6d6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-chromeicon</ShortName>
+<Image width="16" height="16">chrome://branding/content/icon16.png</Image>
+<Image width="32" height="32">chrome://branding/content/icon32.png</Image>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-fr.xml b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml
new file mode 100644
index 0000000000..fad3e75744
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine (fr)</ShortName>
+<Description>A test search engine (based on Google search for a different locale)</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Url type="text/html" method="GET" template="http://www.google.fr/search" resultdomain="google.fr">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="iso-8859-1"/>
+ <Param name="oe" value="iso-8859-1"/>
+</Url>
+<SearchForm>http://www.google.fr/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-override.xml b/toolkit/components/search/tests/xpcshell/data/engine-override.xml
new file mode 100644
index 0000000000..473be82fdc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-override.xml
@@ -0,0 +1,8 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>bug645970</ShortName>
+<Description>override</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-pref.xml b/toolkit/components/search/tests/xpcshell/data/engine-pref.xml
new file mode 100644
index 0000000000..0555caf3e9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-pref.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-pref</ShortName>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="code" condition="pref" pref="code"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml
new file mode 100644
index 0000000000..8b6eb7cab0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform-post.xml</ShortName>
+<Url type="text/html" method="POST" template="http://engine-rel-searchform-post.xml/POST" rel="searchform"/>
+<SearchForm>http://engine-rel-searchform-post.xml/?search</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml
new file mode 100644
index 0000000000..18026210fc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform-purpose</ShortName>
+<Url type="text/html" method="GET" template="http://www.google.com/search" resultdomain="google.com" rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="sb"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml
new file mode 100644
index 0000000000..bcd164877a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform.xml</ShortName>
+<Url type="text/html" method="GET" template="http://engine-rel-searchform.xml/?search" rel="searchform"/>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml
new file mode 100644
index 0000000000..6fb2a778df
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-resourceicon</ShortName>
+<Image width="16" height="16">resource://search-plugins/icon16.png</Image>
+<Image width="32" height="32">resource://search-plugins/icon32.png</Image>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml
new file mode 100644
index 0000000000..57ecd32d78
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-system-purpose</ShortName>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="sb"/>
+ <MozParam name="channel" condition="purpose" purpose="system" value="sys"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-update.xml b/toolkit/components/search/tests/xpcshell/data/engine-update.xml
new file mode 100644
index 0000000000..b8ef7224d0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-update.xml
@@ -0,0 +1,10 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>update</ShortName>
+<Description>update</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<UpdateUrl>http://searchtest.local/opensearch.xml</UpdateUrl>
+<IconUpdateUrl>http://searchtest.local/favicon.ico</IconUpdateUrl>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine.xml b/toolkit/components/search/tests/xpcshell/data/engine.xml
new file mode 100644
index 0000000000..e7af1d9e9e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine</ShortName>
+<Description>A test search engine (based on Google search)</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
+<Url type="application/x-suggestions+json" method="GET" template="http://suggestqueries.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="http://www.google.com/search" resultdomain="google.com">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+ <Param name="aq" value="t"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+</Url>
+<Url type="application/x-moz-default-purpose" method="GET" template="http://www.google.com/search" resultdomain="purpose.google.com">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- MozParam uses searchbar if purpose is not specified -->
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="ffsb"/>
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+</Url>
+<SearchForm>http://www.google.com/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine2.xml b/toolkit/components/search/tests/xpcshell/data/engine2.xml
new file mode 100644
index 0000000000..9957bfdf48
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine2.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>A second test engine</ShortName>
+ <Description>A second test search engine (based on DuckDuckGo)</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <LongName>A second test search engine (based on DuckDuckGo)</LongName>
+ <Image width="16" height="16">data:image/vnd.microsoft.icon;base64,AAABAAMAEBAAAAEACABoBQAANgAAACAgAAABAAgAqAgAAJ4FAAAwMAAAAQAIAKgOAABGDgAAKAAAABAAAAAgAAAAAQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUvgAAFMAAABfDAAAYwQAAGscAAB7KAAAgzAAAIcwAACHNAAAizgAAI9AAACTQAAAnzAAAJtMAACnXABktyAAALtgAAC/bAAAx3QAAM+AAADbjAAA34wAAOOQAADzYAAA65wAWPdcAADvoAABG1QC6cg0AAFXdAERS0AAAVd8AAF3cAABp5wAAcOUATGziAAB36AAAeugATHLqAF165ABlg+0A0qRkAACS7wAAlO8AeI7nAIGX6gCVndwAAKX1AACr9gAAr/YAALX4AAC3+QCdrvIAALz6AAC9+QAAv/kAAMD7AADH+wAAyvwAAND9ALHB9QAA0vwA5c68AADT/gAA1P0AANT+AADU/wAA1/8AANj/AADZ/wAA3P8AAN3/AGrV+wAA3/8AAOH/AMrS9ADs3tYA39zmAOHj8AB+6v8A5ur6AKDw/wCq8f8A9fDtAPby7wD38vAA6/D8APfz8QD69O4A8PP8APL0/AD69/IA+/fzAPr39QDd+f8A+/n1APb4/QD7+fgA+/r5AOz8/gD1/P8A+vz/AP7+/QD//v8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABpLGhoaGhQHgEAAQEBAQFpGUtoaGhoSw8CAwICAgICAiNQaGhoaC0EBAwgIBsEBAQnWmhoaGgnIjY5PUBHOyUFLWhoaGheR0MdBgYGByQrCEtoaGhoT0I3CQkJCQkJCQlQaGhoaFI/QTIzNSoXCgsLWmhoaGhoUUVEQ0RKRDEfDWhoXFxoaGhjZWRILzpJRjhoXBwcWGhoaFtbYg4QIS8waFwcKVhoaF8cHF8REREREWhnXFxoaGhfHClOEhISEhJVU2hoaGhoaF9fNBMTExMTVz5MZmhoaFdMTSYUFBQUFDxhVGhoaGhdPi4VFhYWFhZpVmBoaGhoWSgYGhoaGhppgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAEAACgAAAAgAAAAQAAAAAEACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFL8AABTAAAEWvAABF74AABbCAAAXwwABGMAAABjDAAAYxAAAGsUAABrGAAAbxgAAG8cAAh3BAAEcxQAAHMgAAR3HAAgewgAAHcoAAB/LAAAgzAAFIckAiEYiAAAhzgAAI88AACPQAAAk0AAAJc8AACXSAAUmzQAAJ9IAACfTABAqxwAAKNQAjE0rAAAp1QAAKdYAACrXAAAr2AAGLdMAAC3ZAAAu2wAAL9wAADPVAAAx3QAAMt8AADPgAAA04QAAONQAADnXAAA24wAGN94ACjfdAAA34wAAN+QACznbAAA45QAJOeAAFTzUABY81AAAOeYAFj3UAAo74AAAP9YAADrnAAs74AAAPtoABT/pAABF2wAARd0AAEbaAABI1gAASdoAEETmAABJ2wAAS98AAFHfAAFV3ABTc2EAWYA+AAFa4gAAXd0AAF3hADyeAAABZd4AAWTjADZf5wABZuIAQKETAD2jDwBGZ9gARKEYADhk6wA8pBMAPKUVAK+DbABKbN8ATGziAAF15ABTpioAaKE1AEGtJAABfOkAOrIiAH6hRQABgOkAAYLmAAGC5wACgucAXXrkAAGE5wBEr0AAAYToAAGG6gA9s0AAObkuAD20QQA5ui8AaIHhAEe3OABxgeAAAo3sAFasbABshukAOcA5AGmI7gABleoAbovsAAKZ7QCFtmEAAp7vAD7CagCBl+oAPsNuAAKo8QACqvIAA6zyAAOs9AACr/QAkKfxAGLJlQBfypUAZMqWAAO49QDSuawAc8qaAMPCngADwfgAA8H5AAPE+QAEyPsABMr7AATO/AAE0PwABND9AATR/QAE0v0ACNP9ABvW/QDj1MwAMtr9ADvc/QDK0vQA59rTAOjb1ADp3tcA6+DaAG/l/gB15v4A29/0AO3k3wDu5eAA4eT3AJDr/gDy6+cA5uj3APLs6ADj6PsA8+zpAObq+gDz7uoA8+7rANHt+gD07usA6e35APbx7wDy9PwA5fr/APv7+wD2+/8A7fz/APz8+gDw/P8A/fz7APv8/gD9/f4A+P7/AP/+/gD+/v8A////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyMgAAAAAPYyDb4HHx8fHw05dUwAAAAAAAAAAAAAAyMjIAQEBAQFhkYNyXmV3Y2h6dF1TAgEBAQEBAQEBAQEByAQEBAQEBG2Og3JedXxnWYV0XVMDBAQEBAQEBAQEBAQEBQUFBQURhIyDcl5zfGdbhXRdUwYFBQUFBQUFBQUFBQUICAgICDuijYNyXmV3ZE96dF1TBwgICAgICAgICAgICAoKCgoKYbOMg5K/x8esCg5OWFMJCgoKCgoKCgoKCgoKDAwMDAxtusfHx8fHx3gMDAwLDQwMDAwMDAwMDAwMDAwPDw8PEITHx8fHx8fHFQ8PDw8PDw8PDw8PDw8PDw8PDxISEhIgosfHx8fHx8YSEhISEhISEhISEhISEhISEhISExMTEzuzx8fHx8fHrxMTExMTExMTExMTExMTExMTExMUFBQUYbrHx8fHx8epFBQUboiXnJqGVEcUFBQUFBQUFBcXFxdtx8fHx8fHx7YbbJyYj2tigpObnIAXFxcXFxcXGBgYHYTHx8fHx8e7npybcE0wGBgYP1F+ahgYGBgYGBgaGho6osfHx8fHx6icnIcZGhoaGhoaGhoaGhoaGhoaGhwcHGGzx8fHx8fHp5ycnEgeRjErHBwcHBwcHBwcHBwcISEhbbrHx8fHx8e+oZycnJycnJyViVdEISEhISEhISEhIR+Ex8fHx8fHx8fEraCdnJycnJycnJZxSiEhISEhISMjJ6LHx8fHx8fHx8fHx8fAvcBCVXmUnJyZUiMjIyMjJSU6s8fHx6Wlx8fHx8fHx8fHxyUkJUxpmZuKJSUlJSUmJmG6x8WjIhaqx8fHx8fHtLTHJiYmJkVQZksmJiYmJigobcXHx6QiX6rHx8fHx7AiFrUoKCgoKCgoKCgoKCgoKSmEx8fHx6urx8fHx8fHsiJfeykpKSkpKSkpKSkpKSkqKoTHt8fHx8fHx8fHx8fHubk0KioqKioqKioqKioqKiwsbcemx8fHx8fHx8fHwcfHuCwsLCwsLCwsLCwsLCwsLi43s8eQkJ/Hx8fHx8eukJA5Li4uLi4uLi4uLi4uLi4uLi2Ex8fHx8fHx8fHx8fHfy4uLi4uLi4uLi4uLi4uLi8vLzOzx8fHx8fHx8fHx4svLy8vLy8vLy8vLy8vLy8vMjIyMkGEvMfHx8fHx7FWMjIyMjIyMjIyMjIyMjIyMjI2NjY2MjI+drPHx8d9NTY2NjY2NjY2NjY2NjY2NjY2Njg4NlrHx8fHx8fGSTg4ODg4ODg4ODg4ODg4ODg4ODg4yEBAPGDHx8fCXEBAQEBAQEBAQEBAQEBAQEBAQEBAQMjIyENDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0PIyMAAAAOAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAADKAAAADAAAABgAAAAAQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARwQAAE8IAABXEAAAXxQAAGcYAARvHAAAfwAAAHsoAFSucAIdGHgAAIs0Ai0kjAAAlzwAHJc8AID59AAAo0gCPTSgAACnTACQ8lwAYMsgAADDaAAI01AA+WkgAADTeABU4zgAEM+YAl1o3AAg52QAANukAADfqAAA65AA5Y0cAAEDYABc+3QAARtYANEPPAJ1lQgA6eSUAGUbeAAFK2wAVRucAMknWAAJQ2gA8hBkANoQfAB9M5QAXTeYANo0FAABT3gAmUeEAVoMvAEaYAAAAXeIASJIcAABg3gA9WeEAPZ4AADefBACneGMAA2TiAAFn3gAwX+oAU4ZkADqiDwAAauIArH9hAKx9aABWZtYAW4pfAFJp2gAAcOIAX2raAEOkJABJoi4AXKErAEtt4ABGpykANa0bAElu6gA6qiYAaXLbAAZ66ABFricAPq4tAFZ24wBseNoAeaVCALiPdwBtedsANrgeAF955gBBsjIAY6dMAFx86gBYfu0AAIjrAD61RwBCuzMAZILpAAKM6QCipWMAYrZCAL+bhwA3wjQAaIbuADy6UABrifEAAJXtAHCL6wBZtmgATLduAG2O8AACnO8APMFkAIuW4wCltXMAq7J+AIKX6AB9mOsAyamaAImb5AA5xXMAi6DjAACq9ACsuYgAT8iDAAaw8wBBzIIAjqbyAACz9wCVqu0AmK3xAJ+u8AClru4AC8P2AKa48wAAx/wAAMv5ALvQtgAJz/4AAND/ALPB9QDDzscAANP8AMDF7QAQ1PwAwsz3ACna/AC9zfoANtv8AObYzACr4sYA6NrOAMvS8wCu5M8ATuD/AGDi/wCy59MAaOP/ANPn1ABp5vwA5eHjAOzk2wDU3vgAwurYAIHq/ADr6t0Akev/AObm8wDh5vYA9e3kAOTq+gDz8OsAsPH/AOjt/QC38v8A6u//AN327QDx8f4A+PT2AM73/gDz9foA+/jzAPX2/AD8+fQA3fr8AP369QD4+v8A+v37AP/++AD8//4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/vwAAAAAAAAAAAAAAAAF2obO9vr6+vr6+vr28RwAAAAAAAAAAAAAAAAAAAAAAv7+/AAYAAAAAAAAAAAABAgGZfW18vr6+vr6+vr6pAAAAAAAAAAAAAAAAAAAAAQAGAL8BAQEBQ5Cyvru+vb1QAxiff3FgSWS+vr6+vr5HAggWDgETvb67vr2+vrWQQwEGAQEBAQFyvlUTAAEBAQEAAUukfXFgTzhMYFJKc44+ST9IKwEBAQEBAQEBAQFVvnIBAQEBAUO+IwEBAQEAAQEBAVSdf3FgT0hnZ1lNSHlpWz85LwEBAQEBAQEBAQEBI7tDAQECApBYAgICAgICAgICAnWaf3FgUjhnZ2FPP31pUzhILwICAgICAgICAgICAliQAgICArUCAgICAgICAgICApmaf3FgTzlnZ1lNSHlpUzg5LwICAgICAgICAgICAgK1AgICAr0CAgICAgICAgICS6mXf3FgTzNWZVtPMm5pWz9ILAICAgICAgICAgICAgK7AgICAr4CAgICAgICAgICVLWXf3FcdL6+vr6FBAJEUzg4JQICAgICAgICAgICAgK9AgICArsCAgICAgICAgICdb6xiqa+vr6+vrMpCgICEjU4HwICAgICAgICAgICAgK7AgIDA74DAwMDAwMDAwMDmb6+vr6+vr6+vrAKAwMDAwMSAwMDAwMDAwMDAwMDAwO+AwMDA7sDAwMDAwMDAwMDqb6+vr6+vr6+voUDAwMDAwMDAwMDAwMDAwMDAwMDAwO9AwMFBb4FBQUFBQUFBQVLtb6+vr6+vr6+vkUFBQUFBQUFBQUFBQUFBQUFBQUFBQW+BQUFBbsFBQUFBQUFBQVavr6+vr6+vr6+vikFBQUFBQUFBQUFBQUFBQUFBQUFBQW9BQUHB7sHBwcHBwcHBwd1vr6+vr6+vr6+vgcHBwcHBwcHBwcHBwcHBwcHBwcHBwe+BwcHB70HBwcHBwcHBweZvr6+vr6+vr6+vgcHBwciUXuIj4+PhmsqBwcHBwcHBwe+BwcKCr4MCgoKCgoKCgqrvr6+vr6+vr6+vgoKY4uRj5GRkYyPi4uRiDYKCgoKCgq7CgoKCr4MCgoKCgoKCku1vr6+vr6+vr6+r4aPi4twOycgIjBAcIaLj488CgoKCgq9CgoKCr4KCgoKCgoKClS+vr6+vr6+vr60kYyMiyoKCgoKCgoKCgoVKkYVCgoKCgq9CgoMDLsMDAwMDAwMDHW+vr6+vr6+vrubjI+RYw0KCgoKCgoMCgwMDAwMDAwMCgy+CgwMDL4MDAwMDAwMDJm+vr6+vr6+vr2Tj4+PiBUMDAwMDAwMDAwMDAwMDAwMDAy9DwwMDL4MDAwMDAwMIam+vr6+vr6+vrugi4+Mj4uIi4yIflEgDAwMDAwMDAwMDAy8DAwMDL4MDAwMDAwMS7W+vr6+vr6+vr6+npGLj4yPj4+Pj4+PiF8gDAwMDAwMDAy+DAwPDL0PDwwPDA8PWr6+vr6+vr6+vr6+vq2Tj4+Lj4yPj5GLj4+PgTsPDA8MDwy9DA8PD74PDw8PDw8Pdb6+vr6+vr6+vr6+vr67ua2npZyVjI+LkY+Rj4+INA8PDw++Dw8PD70PDw8PDw8Pmb6+vr6+vr6+vr6+vr69vr6+vr69VCpffoyLjI+Jj4EPDw++Dw8PD70PDw8PDw8Pq76+vqEaGqq+vr6+vr6+vr68vr6+NxEPDw82e5GMiVEPDw++Dw8REbwRERERERFLtb6+vgsQCzq+vr6+vr6+vpYaGqy7ERERERERERERERERERG9EREUEb4RFBEUERRdvr6+vhAQoma+vr6+vr6+vgkaCTq+FBEUERQRFBEUERQRFBG+ERQUFL4UFBQUFBR1vr6+vqpBQra+vr6+vr6+uxAQoWa+FBQUFBQUFBQUFBQUFBS9FBQUFL4UFBQUFBR2vr6+vr6+vr6+vr6+vr6+vqpCQrayFBQUFBQUFBQUFBQUFBS+FBQUFL4UFBQUFBRivr6qvr6+vr6+vr6+vr6+vr6+vr6CFBQUFBQUFBQUFBQUFBS9FBQXFL4UFxQXFBdLvrNXvr6+vr6+vr6+vr6+vr6+vr4UFxQXFBcUFxQXFBcUFxS+FBcUF74XFBcUFxQXvr62JFd3s72+vr6+vr67rLO7mKgXFBcUFxQXFBcUFxQXFBe+FxQXF74XFxcXFxcXrr6+vrq9vr6+vr6+vr6+uCRXljEXFxcXFxcXFxcXFxcXFxe+FxcXF74XFxcXFxcXhL2+vr6+vr6+vr6+vr6+vb69gxcXFxcXFxcXFxcXFxcXFxe+FxcXF74XFxcXFxcXG667vr6+vr6+vr6+vr6+vrupFxcXFxcXFxcXFxcXFxcXFxe+FxcXHr4eFx4XHhceFx68vb6+vr6+vr6+vr69vrUXHhceFx4XHhceFx4XHhceFx6+HhceF74XHhceFx4XHhceeLu9vb6+vr6+vb6zhxceFx4XHhceFx4XHhceFx4XHhe+Fx4eHr4eHh4eHh4eHh4eHhctXZm+vr6+sGIeHh4eHh4eHh4eHh4eHh4eHh4eHh69Hh4eHr0eHh4eHh4eHmJsdoKNo7u+vr61LR4eHh4eHh4eHh4eHh4eHh4eHh4eHh69Hh4eHLUcHhweHB4cG6u8vr6+vr6+voMeHB4cHhweHB4cHhweHB4cHhweHB4cHhy1HB4cHpJqHB4cHhweHCaZvr6+vr2DLhwcHhweHB4cHhweHB4cHhweHB4cHhweHGqSHhwcHF6+PRwcHBwcHlR6hINoTigZGRwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcPb5eHBwcHByAvW8cHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBxvvYAcHBwcHBwcXpS3vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vr6+vreUXhwcHBy/HBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHL+/vx0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dv7/AAAAAAAMAAIAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAQAAwAAAAAADAAA=</Image>
+ <Url type="text/html" method="get" template="https://duckduckgo.com/?q={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/data/engineImages.xml b/toolkit/components/search/tests/xpcshell/data/engineImages.xml
new file mode 100644
index 0000000000..65b550b31b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineImages.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>IconsTest</ShortName>
+ <Description>IconsTest. Search by Test.</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <Image width="16" height="16">data:image/x-icon;base64,ico16</Image>
+ <Image width="32" height="32">data:image/x-icon;base64,ico32</Image>
+ <Image width="74" height="74">data:image/png;base64,ico74</Image>
+ <Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="form" value="MOZW"/>
+ </Url>
+ <Url type="text/html" method="GET" template="http://www.bing.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <MozParam name="pc" condition="pref" pref="ms-pc"/>
+ <Param name="form" value="MOZW"/>
+ </Url>
+ <SearchForm>http://www.bing.com/search</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs
new file mode 100644
index 0000000000..4c432e7ee2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+/**
+ * Dynamically create a search engine offering search suggestions via searchSuggestions.sjs.
+ *
+ * The engine is constructed by passing a JSON object with engine datails as the query string.
+ */
+
+function handleRequest(request, response) {
+ let engineData = JSON.parse(unescape(request.queryString).replace("+", " "));
+
+ if (!engineData.baseURL) {
+ response.setStatusLine(request.httpVersion, 500, "baseURL required");
+ return;
+ }
+
+ engineData.name = engineData.name || "Generated test engine";
+ engineData.description = engineData.description || "Generated test engine description";
+ engineData.method = engineData.method || "GET";
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ createOpenSearchEngine(response, engineData);
+}
+
+/**
+ * Create an OpenSearch engine for the given base URL.
+ */
+function createOpenSearchEngine(response, engineData) {
+ let params = "", queryString = "";
+ if (engineData.method == "POST") {
+ params = "<Param name='q' value='{searchTerms}'/>";
+ } else {
+ queryString = "?q={searchTerms}";
+ }
+
+ let result = "<?xml version='1.0' encoding='utf-8'?>\
+<OpenSearchDescription xmlns='http://a9.com/-/spec/opensearch/1.1/'>\
+ <ShortName>" + engineData.name + "</ShortName>\
+ <Description>" + engineData.description + "</Description>\
+ <InputEncoding>UTF-8</InputEncoding>\
+ <LongName>" + engineData.name + "</LongName>\
+ <Url type='application/x-suggestions+json' method='" + engineData.method + "'\
+ template='" + engineData.baseURL + "searchSuggestions.sjs" + queryString + "'>\
+ " + params + "\
+ </Url>\
+ <Url type='text/html' method='" + engineData.method + "'\
+ template='" + engineData.baseURL + queryString + "'/>\
+</OpenSearchDescription>\
+";
+ response.write(result);
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.ico b/toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.ico
new file mode 100644
index 0000000000..442ab4dc80
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/install.rdf b/toolkit/components/search/tests/xpcshell/data/install.rdf
new file mode 100644
index 0000000000..df361ade45
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>search-engine@tests.mozilla.org</em:id>
+ <em:unpack>true</em:unpack>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Search Engine</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/search/tests/xpcshell/data/invalid-engine.xml b/toolkit/components/search/tests/xpcshell/data/invalid-engine.xml
new file mode 100644
index 0000000000..e8efce6726
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/invalid-engine.xml
@@ -0,0 +1 @@
+# An invalid xml engine file.
diff --git a/toolkit/components/search/tests/xpcshell/data/langpack-metadata.json b/toolkit/components/search/tests/xpcshell/data/langpack-metadata.json
new file mode 100644
index 0000000000..e1ff95bc00
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/langpack-metadata.json
@@ -0,0 +1,5 @@
+{
+ "[app]/bug645970.xml": {
+ "alias": "lp"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/list.json b/toolkit/components/search/tests/xpcshell/data/list.json
new file mode 100644
index 0000000000..68163bb883
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/list.json
@@ -0,0 +1,7 @@
+{
+ "default": {
+ "visibleDefaultEngines": [
+ "engine", "engine-pref", "engine-rel-searchform-purpose", "engine-system-purpose", "engine-chromeicon", "engine-resourceicon"
+ ]
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/metadata.json b/toolkit/components/search/tests/xpcshell/data/metadata.json
new file mode 100644
index 0000000000..77b003d4e4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/metadata.json
@@ -0,0 +1,30 @@
+{
+ "[global]": {
+ "searchdefaultexpir": 1471013469846
+ },
+ "[profile]\/engine.xml": {
+ "order": 1,
+ "alias": "foo"
+ },
+ "[app]\/google.xml": {
+ "order": 2
+ },
+ "[app]\/yahoo.xml": {
+ "order": 3
+ },
+ "[app]\/bing.xml": {
+ "order": 4
+ },
+ "[app]\/amazondotcom.xml": {
+ "order": 5
+ },
+ "[app]\/ddg.xml": {
+ "order": 6
+ },
+ "[app]\/twitter.xml": {
+ "order": 7
+ },
+ "[app]\/wikipedia.xml": {
+ "order": 8
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search.json b/toolkit/components/search/tests/xpcshell/data/search.json
new file mode 100644
index 0000000000..f4f9077781
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search.json
@@ -0,0 +1,86 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "extensionID": "test-addon-id@mozilla.org",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [
+ ],
+ "type": "application/x-suggestions+json",
+ "params": [
+ ]
+ },
+ {
+ "template": "http://www.google.com/search",
+ "resultDomain": "google.com",
+ "rels": [
+ ],
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "ie",
+ "value": "utf-8"
+ },
+ {
+ "name": "oe",
+ "value": "utf-8"
+ },
+ {
+ "name": "aq",
+ "value": "t"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ },
+ {
+ "template": "http://www.google.com/search",
+ "resultDomain": "purpose.google.com",
+ "rels": [
+ ],
+ "type": "application/x-moz-default-purpose",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "_readOnly": false
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search.sqlite b/toolkit/components/search/tests/xpcshell/data/search.sqlite
new file mode 100644
index 0000000000..983bb831a8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search.sqlite
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs
new file mode 100644
index 0000000000..abd94428e5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * Provide search suggestions in the OpenSearch JSON format.
+ */
+
+function handleRequest(request, response) {
+ // Get the query parameters from the query string.
+ let query = parseQueryString(request.queryString);
+
+ function writeSuggestions(query, completions = []) {
+ let result = [query, completions];
+ response.write(JSON.stringify(result));
+ return result;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ let q = request.method == "GET" ? query.q : undefined;
+ if (q == "no remote" || q == "no results") {
+ writeSuggestions(q);
+ } else if (q == "Query Mismatch") {
+ writeSuggestions("This is an incorrect query string", ["some result"]);
+ } else if (q == "Query Case Mismatch") {
+ writeSuggestions(q.toUpperCase(), [q]);
+ } else if (q == "") {
+ writeSuggestions("", ["The server should never be sent an empty query"]);
+ } else if (q && q.startsWith("mo")) {
+ writeSuggestions(q, ["Mozilla", "modern", "mom"]);
+ } else if (q && q.startsWith("I ❤️")) {
+ writeSuggestions(q, ["I ❤️ Mozilla"]);
+ } else if (q && q.startsWith("letter ")) {
+ let letters = [];
+ for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) {
+ letters.push("letter " + String.fromCharCode(charCode));
+ }
+ writeSuggestions(q, letters);
+ } else if (q && q.startsWith("HTTP ")) {
+ response.setStatusLine(request.httpVersion, q.replace("HTTP ", ""), q);
+ writeSuggestions(q, [q]);
+ } else if (q && q.startsWith("delay")) {
+ // Delay the response by 200 milliseconds (less than the timeout but hopefully enough to abort
+ // before completion).
+ response.processAsync();
+ writeSuggestions(q, [q]);
+ setTimeout(() => response.finish(), 200);
+ } else if (q && q.startsWith("slow ")) {
+ // Delay the response by 10 seconds so the client timeout is reached.
+ response.processAsync();
+ writeSuggestions(q, [q]);
+ setTimeout(() => response.finish(), 10000);
+ } else if (request.method == "POST") {
+ // This includes headers, not just the body
+ let requestText = NetUtil.readInputStreamToString(request.bodyInputStream,
+ request.bodyInputStream.available());
+ // Only use the last line which contains the encoded params
+ let requestLines = requestText.split("\n");
+ let postParams = parseQueryString(requestLines[requestLines.length - 1]);
+ writeSuggestions(postParams.q, ["Mozilla", "modern", "mom"]);
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ }
+}
+
+function parseQueryString(queryString) {
+ let query = {};
+ queryString.split('&').forEach(function (val) {
+ let [name, value] = val.split('=');
+ query[name] = unescape(value).replace(/[+]/g, " ");
+ });
+ return query;
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/searchTest.jar b/toolkit/components/search/tests/xpcshell/data/searchTest.jar
new file mode 100644
index 0000000000..8bfbe6f215
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/searchTest.jar
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/head_search.js b/toolkit/components/search/tests/xpcshell/head_search.js
new file mode 100644
index 0000000000..2f40d84f86
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -0,0 +1,544 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://testing-common/AppInfo.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+const NS_APP_SEARCH_DIR = "SrchPlugns";
+
+const MODE_RDONLY = FileUtils.MODE_RDONLY;
+const MODE_WRONLY = FileUtils.MODE_WRONLY;
+const MODE_CREATE = FileUtils.MODE_CREATE;
+const MODE_TRUNCATE = FileUtils.MODE_TRUNCATE;
+
+const CACHE_FILENAME = "search.json.mozlz4";
+
+// nsSearchService.js uses Services.appinfo.name to build a salt for a hash.
+var XULRuntime = Components.classesByID["{95d89e3e-a169-41a3-8e56-719978e15b12}"]
+ .getService(Ci.nsIXULRuntime);
+
+var isChild = XULRuntime.processType == XULRuntime.PROCESS_TYPE_CONTENT;
+
+updateAppInfo({
+ name: "XPCShell",
+ ID: "xpcshell@test.mozilla.org",
+ version: "5",
+ platformVersion: "1.9",
+ // mirror OS from the base impl as some of the "location" tests rely on it
+ OS: XULRuntime.OS,
+ // mirror processType from the base implementation
+ extraProps: {
+ processType: XULRuntime.processType,
+ },
+});
+
+var gProfD;
+if (!isChild) {
+ // Need to create and register a profile folder.
+ gProfD = do_get_profile();
+}
+
+function dumpn(text)
+{
+ dump("search test: " + text + "\n");
+}
+
+/**
+ * Configure preferences to load engines from
+ * chrome://testsearchplugin/locale/searchplugins/
+ */
+function configureToLoadJarEngines()
+{
+ let url = "chrome://testsearchplugin/locale/searchplugins/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ // Ensure a test engine exists in the app dir anyway.
+ let dir = Services.dirsvc.get(NS_APP_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-app.xml").copyTo(dir, "app.xml");
+}
+
+/**
+ * Fake the installation of an add-on in the profile, by creating the
+ * directory and registering it with the directory service.
+ */
+function installAddonEngine(name = "engine-addon")
+{
+ const XRE_EXTENSIONS_DIR_LIST = "XREExtDL";
+ const profD = do_get_profile().QueryInterface(Ci.nsILocalFile);
+
+ let dir = profD.clone();
+ dir.append("extensions");
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ dir.append("search-engine@tests.mozilla.org");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/install.rdf").copyTo(dir, "install.rdf");
+ let addonDir = dir.clone();
+ dir.append("searchplugins");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/" + name + ".xml").copyTo(dir, "bug645970.xml");
+
+ Services.dirsvc.registerProvider({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+ Ci.nsIDirectoryServiceProvider2]),
+
+ getFile: function (prop, persistant) {
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ getFiles: function (prop) {
+ let result = [];
+
+ switch (prop) {
+ case XRE_EXTENSIONS_DIR_LIST:
+ result.push(addonDir);
+ break;
+ default:
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+ hasMoreElements: () => result.length > 0,
+ getNext: () => result.shift()
+ };
+ }
+ });
+}
+
+/**
+ * Copy the engine-distribution.xml engine to a fake distribution
+ * created in the profile, and registered with the directory service.
+ */
+function installDistributionEngine()
+{
+ const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+
+ const profD = do_get_profile().QueryInterface(Ci.nsILocalFile);
+
+ let dir = profD.clone();
+ dir.append("distribution");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ let distDir = dir.clone();
+
+ dir.append("searchplugins");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ dir.append("common");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ Services.dirsvc.registerProvider({
+ getFile: function(aProp, aPersistent) {
+ aPersistent.value = true;
+ if (aProp == XRE_APP_DISTRIBUTION_DIR)
+ return distDir.clone();
+ return null;
+ }
+ });
+}
+
+/**
+ * Clean the profile of any metadata files left from a previous run.
+ */
+function removeMetadata()
+{
+ let file = gProfD.clone();
+ file.append("search-metadata.json");
+ if (file.exists()) {
+ file.remove(false);
+ }
+
+ file = gProfD.clone();
+ file.append("search.sqlite");
+ if (file.exists()) {
+ file.remove(false);
+ }
+}
+
+function promiseCacheData() {
+ return new Promise(resolve => Task.spawn(function* () {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME);
+ let bytes = yield OS.File.read(path, {compression: "lz4"});
+ resolve(JSON.parse(new TextDecoder().decode(bytes)));
+ }));
+}
+
+function promiseSaveCacheData(data) {
+ return OS.File.writeAtomic(OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME),
+ new TextEncoder().encode(JSON.stringify(data)),
+ {compression: "lz4"});
+}
+
+function promiseEngineMetadata() {
+ return new Promise(resolve => Task.spawn(function* () {
+ let cache = yield promiseCacheData();
+ let data = {};
+ for (let engine of cache.engines) {
+ data[engine._shortName] = engine._metaData;
+ }
+ resolve(data);
+ }));
+}
+
+function promiseGlobalMetadata() {
+ return new Promise(resolve => Task.spawn(function* () {
+ let cache = yield promiseCacheData();
+ resolve(cache.metaData);
+ }));
+}
+
+function promiseSaveGlobalMetadata(globalData) {
+ return new Promise(resolve => Task.spawn(function* () {
+ let data = yield promiseCacheData();
+ data.metaData = globalData;
+ yield promiseSaveCacheData(data);
+ resolve();
+ }));
+}
+
+var forceExpiration = Task.async(function* () {
+ let metadata = yield promiseGlobalMetadata();
+
+ // Make the current geodefaults expire 1s ago.
+ metadata.searchDefaultExpir = Date.now() - 1000;
+ yield promiseSaveGlobalMetadata(metadata);
+});
+
+/**
+ * Clean the profile of any cache file left from a previous run.
+ * Returns a boolean indicating if the cache file existed.
+ */
+function removeCacheFile()
+{
+ let file = gProfD.clone();
+ file.append(CACHE_FILENAME);
+ if (file.exists()) {
+ file.remove(false);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * isUSTimezone taken from nsSearchService.js
+ */
+function isUSTimezone() {
+ // Timezone assumptions! We assume that if the system clock's timezone is
+ // between Newfoundland and Hawaii, that the user is in North America.
+
+ // This includes all of South America as well, but we have relatively few
+ // en-US users there, so that's OK.
+
+ // 150 minutes = 2.5 hours (UTC-2.5), which is
+ // Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
+
+ // 600 minutes = 10 hours (UTC-10), which is
+ // Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
+
+ let UTCOffset = (new Date()).getTimezoneOffset();
+ return UTCOffset >= 150 && UTCOffset <= 600;
+}
+
+const kDefaultenginenamePref = "browser.search.defaultenginename";
+const kTestEngineName = "Test search engine";
+const kLocalePref = "general.useragent.locale";
+
+function getDefaultEngineName(isUS) {
+ const nsIPLS = Ci.nsIPrefLocalizedString;
+ // Copy the logic from nsSearchService
+ let pref = kDefaultenginenamePref;
+ if (isUS === undefined)
+ isUS = Services.prefs.getCharPref(kLocalePref) == "en-US" && isUSTimezone();
+ if (isUS) {
+ pref += ".US";
+ }
+ return Services.prefs.getComplexValue(pref, nsIPLS).data;
+}
+
+/**
+ * Waits for the cache file to be saved.
+ * @return {Promise} Resolved when the cache file is saved.
+ */
+function promiseAfterCache() {
+ return waitForSearchNotification("write-cache-to-disk-complete");
+}
+
+function parseJsonFromStream(aInputStream) {
+ const json = Cc["@mozilla.org/dom/json;1"].createInstance(Components.interfaces.nsIJSON);
+ const data = json.decodeFromStream(aInputStream, aInputStream.available());
+ return data;
+}
+
+/**
+ * Read a JSON file and return the JS object
+ */
+function readJSONFile(aFile) {
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ try {
+ stream.init(aFile, MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ return parseJsonFromStream(stream, stream.available());
+ } catch (ex) {
+ dumpn("readJSONFile: Error reading JSON file: " + ex);
+ } finally {
+ stream.close();
+ }
+ return false;
+}
+
+/**
+ * Recursively compare two objects and check that every property of expectedObj has the same value
+ * on actualObj.
+ */
+function isSubObjectOf(expectedObj, actualObj) {
+ for (let prop in expectedObj) {
+ if (expectedObj[prop] instanceof Object) {
+ do_check_eq(expectedObj[prop].length, actualObj[prop].length);
+ isSubObjectOf(expectedObj[prop], actualObj[prop]);
+ } else {
+ if (expectedObj[prop] != actualObj[prop])
+ do_print("comparing property " + prop);
+ do_check_eq(expectedObj[prop], actualObj[prop]);
+ }
+ }
+}
+
+// Can't set prefs if we're running in a child process, but the search service
+// doesn't run in child processes anyways.
+if (!isChild) {
+ // Expand the amount of information available in error logs
+ Services.prefs.setBoolPref("browser.search.log", true);
+
+ // The geo-specific search tests assume certain prefs are already setup, which
+ // might not be true when run in comm-central etc. So create them here.
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 3000);
+ // But still disable geoip lookups - tests that need it will re-configure this.
+ Services.prefs.setCharPref("browser.search.geoip.url", "");
+ // Also disable region defaults - tests using it will also re-configure it.
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref("geoSpecificDefaults.url", "");
+}
+
+/**
+ * After useHttpServer() is called, this string contains the URL of the "data"
+ * directory, including the final slash.
+ */
+var gDataUrl;
+
+/**
+ * Initializes the HTTP server and ensures that it is terminated when tests end.
+ *
+ * @return The HttpServer object in case further customization is needed.
+ */
+function useHttpServer() {
+ let httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerDirectory("/", do_get_cwd());
+ gDataUrl = "http://localhost:" + httpServer.identity.primaryPort + "/data/";
+ do_register_cleanup(() => httpServer.stop(() => {}));
+ return httpServer;
+}
+
+/**
+ * Adds test engines and returns a promise resolved when they are installed.
+ *
+ * The engines are added in the given order.
+ *
+ * @param aItems
+ * Array of objects with the following properties:
+ * {
+ * name: Engine name, used to wait for it to be loaded.
+ * xmlFileName: Name of the XML file in the "data" folder.
+ * details: Array containing the parameters of addEngineWithDetails,
+ * except for the engine name. Alternative to xmlFileName.
+ * }
+ */
+var addTestEngines = Task.async(function* (aItems) {
+ if (!gDataUrl) {
+ do_throw("useHttpServer must be called before addTestEngines.");
+ }
+
+ let engines = [];
+
+ for (let item of aItems) {
+ do_print("Adding engine: " + item.name);
+ yield new Promise((resolve, reject) => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ try {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ do_print("Observed " + data + " for " + engine.name);
+ if (data != "engine-added" || engine.name != item.name) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ engines.push(engine);
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ }, "browser-search-engine-modified", false);
+
+ if (item.xmlFileName) {
+ Services.search.addEngine(gDataUrl + item.xmlFileName,
+ null, null, false);
+ } else {
+ Services.search.addEngineWithDetails(item.name, ...item.details);
+ }
+ });
+ }
+
+ return engines;
+});
+
+/**
+ * Installs a test engine into the test profile.
+ */
+function installTestEngine() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine.xml");
+ let engineDir = engineDummyFile.parent;
+ engineDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
+
+ do_register_cleanup(function() {
+ removeMetadata();
+ removeCacheFile();
+ });
+}
+
+/**
+ * Set a localized preference on the default branch
+ * @param aPrefName
+ * The name of the pref to set.
+ */
+function setLocalizedDefaultPref(aPrefName, aValue) {
+ let value = "data:text/plain," + BROWSER_SEARCH_PREF + aPrefName + "=" + aValue;
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .setCharPref(aPrefName, value);
+}
+
+
+/**
+ * Installs two test engines, sets them as default for US vs. general.
+ */
+function setUpGeoDefaults() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine.xml");
+ let engineDir = engineDummyFile.parent;
+ engineDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
+
+ engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine2.xml");
+
+ do_get_file("data/engine2.xml").copyTo(engineDir, "engine2.xml");
+
+ setLocalizedDefaultPref("defaultenginename", "Test search engine");
+ setLocalizedDefaultPref("defaultenginename.US", "A second test engine");
+
+ do_register_cleanup(function() {
+ removeMetadata();
+ removeCacheFile();
+ });
+}
+
+/**
+ * Returns a promise that is resolved when an observer notification from the
+ * search service fires with the specified data.
+ *
+ * @param aExpectedData
+ * The value the observer notification sends that causes us to resolve
+ * the promise.
+ */
+function waitForSearchNotification(aExpectedData) {
+ return new Promise(resolve => {
+ const SEARCH_SERVICE_TOPIC = "browser-search-service";
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (aData != aExpectedData)
+ return;
+
+ Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
+ resolve(aSubject);
+ }, SEARCH_SERVICE_TOPIC, false);
+ });
+}
+
+function asyncInit() {
+ return new Promise(resolve => {
+ Services.search.init(function() {
+ do_check_true(Services.search.isInitialized);
+ resolve();
+ });
+ });
+}
+
+function asyncReInit() {
+ let promise = waitForSearchNotification("reinit-complete");
+
+ Services.search.QueryInterface(Ci.nsIObserver)
+ .observe(null, "nsPref:changed", kLocalePref);
+
+ return promise;
+}
+
+// This "enum" from nsSearchService.js
+const TELEMETRY_RESULT_ENUM = {
+ SUCCESS: 0,
+ SUCCESS_WITHOUT_DATA: 1,
+ XHRTIMEOUT: 2,
+ ERROR: 3,
+};
+
+/**
+ * Checks the value of the SEARCH_SERVICE_COUNTRY_FETCH_RESULT probe.
+ *
+ * @param aExpectedValue
+ * If a value from TELEMETRY_RESULT_ENUM, we expect to see this value
+ * recorded exactly once in the probe. If |null|, we expect to see
+ * nothing recorded in the probe at all.
+ */
+function checkCountryResultTelemetry(aExpectedValue) {
+ let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_RESULT");
+ let snapshot = histogram.snapshot();
+ // The probe is declared with 8 values, but we get 9 back from .counts
+ let expectedCounts = [0, 0, 0, 0, 0, 0, 0, 0, 0];
+ if (aExpectedValue != null) {
+ expectedCounts[aExpectedValue] = 1;
+ }
+ deepEqual(snapshot.counts, expectedCounts);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_645970.js b/toolkit/components/search/tests/xpcshell/test_645970.js
new file mode 100644
index 0000000000..3204e03d91
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_645970.js
@@ -0,0 +1,22 @@
+/* -*- 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/. */
+
+/**
+ * Test nsSearchService with nested jar: uris, without async initialization
+ */
+function run_test() {
+ updateAppInfo();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // The search service needs to be started after the jarURIs pref has been
+ // set in order to initiate it correctly
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+ Services.obs.notifyObservers(null, "quit-application", null);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js
new file mode 100644
index 0000000000..4e50ed2a96
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the SearchStaticData module.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/SearchStaticData.jsm", this);
+
+function run_test() {
+ do_check_true(SearchStaticData.getAlternateDomains("www.google.com")
+ .indexOf("www.google.fr") != -1);
+ do_check_true(SearchStaticData.getAlternateDomains("www.google.fr")
+ .indexOf("www.google.com") != -1);
+ do_check_true(SearchStaticData.getAlternateDomains("www.google.com")
+ .every(d => d.startsWith("www.google.")));
+ do_check_true(SearchStaticData.getAlternateDomains("google.com").length == 0);
+
+ // Test that methods from SearchStaticData module can be overwritten,
+ // needed for hotfixing.
+ let backup = SearchStaticData.getAlternateDomains;
+ SearchStaticData.getAlternateDomains = () => ["www.bing.fr"];
+ do_check_matches(SearchStaticData.getAlternateDomains("www.bing.com"), ["www.bing.fr"]);
+ SearchStaticData.getAlternateDomains = backup;
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js b/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js
new file mode 100644
index 0000000000..14411eaaa2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kSearchEngineID = "addEngineWithDetails_test_engine";
+const kSearchEngineURL = "http://example.com/?search={searchTerms}";
+const kSearchTerm = "foo";
+
+add_task(function* test_addEngineWithDetails() {
+ do_check_false(Services.search.isInitialized);
+
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .setBoolPref("reset.enabled", true);
+
+ yield asyncInit();
+
+ Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
+ kSearchEngineURL);
+
+ // An engine added with addEngineWithDetails should have a load path, even
+ // though we can't point to a specific file.
+ let engine = Services.search.getEngineByName(kSearchEngineID);
+ do_check_eq(engine.wrappedJSObject._loadPath, "[other]addEngineWithDetails");
+
+ // Set the engine as default; this should set a loadPath verification hash,
+ // which should ensure we don't show the search reset prompt.
+ Services.search.currentEngine = engine;
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission =
+ Services.search.currentEngine.getSubmission(kSearchTerm, null, "searchbar");
+ do_check_eq(submission.uri.spec, expectedURL);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js b/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js
new file mode 100644
index 0000000000..07eaf38bb0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests covering nsIBrowserSearchService::addEngine's optional callback.
+ */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+"use strict";
+
+// Only need to stub the methods actually called by nsSearchService
+var promptService = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: function() {}
+};
+var prompt = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
+ alert: function() {}
+};
+// Override the prompt service and nsIPrompt, since the search service currently
+// prompts in response to certain installation failures we test here
+// XXX this should disappear once bug 863474 is fixed
+MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService);
+MockRegistrar.register("@mozilla.org/prompter;1", prompt);
+
+
+// First test inits the search service
+add_test(function init_search_service() {
+ Services.search.init(function (status) {
+ if (!Components.isSuccessCode(status))
+ do_throw("Failed to initialize search service");
+
+ run_next_test();
+ });
+});
+
+// Simple test of the search callback
+add_test(function simple_callback_test() {
+ let searchCallback = {
+ onSuccess: function (engine) {
+ do_check_true(!!engine);
+ do_check_neq(engine.name, Services.search.defaultEngine.name);
+ do_check_eq(engine.wrappedJSObject._loadPath,
+ "[http]localhost/test-search-engine.xml");
+ run_next_test();
+ },
+ onError: function (errorCode) {
+ do_throw("search callback returned error: " + errorCode);
+ }
+ }
+ Services.search.addEngine(gDataUrl + "engine.xml", null,
+ null, false, searchCallback);
+});
+
+// Test of the search callback on duplicate engine failures
+add_test(function duplicate_failure_test() {
+ let searchCallback = {
+ onSuccess: function (engine) {
+ do_throw("this addition should not have succeeded");
+ },
+ onError: function (errorCode) {
+ do_check_true(!!errorCode);
+ do_check_eq(errorCode, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
+ run_next_test();
+ }
+ }
+ // Re-add the same engine added in the previous test
+ Services.search.addEngine(gDataUrl + "engine.xml", null,
+ null, false, searchCallback);
+});
+
+// Test of the search callback on failure to load the engine failures
+add_test(function load_failure_test() {
+ let searchCallback = {
+ onSuccess: function (engine) {
+ do_throw("this addition should not have succeeded");
+ },
+ onError: function (errorCode) {
+ do_check_true(!!errorCode);
+ do_check_eq(errorCode, Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE);
+ run_next_test();
+ }
+ }
+ // Try adding an engine that doesn't exist
+ Services.search.addEngine("http://invalid/data/engine.xml", null,
+ null, false, searchCallback);
+});
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async.js b/toolkit/components/search/tests/xpcshell/test_async.js
new file mode 100644
index 0000000000..58b530464a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // Check the hidden engine is not loaded.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_eq(engine, null);
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_addon.js b/toolkit/components/search/tests/xpcshell/test_async_addon.js
new file mode 100644
index 0000000000..af488f3018
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_addon.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test the add-on engine is loaded in addition to our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 2);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("addon");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "addon");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js b/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js
new file mode 100644
index 0000000000..5c48c108a8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine("engine-override");
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test the add-on engine isn't overriding our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_distribution.js b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
new file mode 100644
index 0000000000..4f3af04193
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installDistributionEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test that the engine from the distribution overrides our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // check the engine we have is actually the one from the distribution
+ do_check_eq(engine.description, "override");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_migration.js b/toolkit/components/search/tests/xpcshell/test_async_migration.js
new file mode 100644
index 0000000000..4d0335c45c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_migration.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that legacy metadata from search-metadata.json is correctly
+ * transferred to the new metadata storage. */
+
+function run_test() {
+ updateAppInfo();
+ installTestEngine();
+
+ do_get_file("data/metadata.json").copyTo(gProfD, "search-metadata.json");
+
+ run_next_test();
+}
+
+add_task(function* test_async_metadata_migration() {
+ yield asyncInit();
+ yield promiseAfterCache();
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["engine"].order, 1);
+ do_check_eq(metadata["engine"].alias, "foo");
+
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata["searchDefaultExpir"], 1471013469846);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js
new file mode 100644
index 0000000000..cbcdbdcb04
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Copy an engine in [profile]/searchplugin/ and ensure it's not
+ // overriding the same file from a jar.
+ // The description in the file we are copying is 'profile'.
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_bug930456.js b/toolkit/components/search/tests/xpcshell/test_bug930456.js
new file mode 100644
index 0000000000..1dbb06c59f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_bug930456.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test()
+{
+ if (isChild) {
+ do_check_false("@mozilla.org/browser/search-service;1" in Cc);
+ } else {
+ do_check_true("@mozilla.org/browser/search-service;1" in Cc);
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_bug930456_child.js b/toolkit/components/search/tests/xpcshell/test_bug930456_child.js
new file mode 100644
index 0000000000..8540a37f4e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_bug930456_child.js
@@ -0,0 +1,3 @@
+function run_test() {
+ run_test_in_child("test_bug930456.js");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js
new file mode 100644
index 0000000000..7d3b1698a2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that resource URLs can be used in default engines */
+
+"use strict";
+
+function run_test() {
+ updateAppInfo();
+
+ // The test engines used in this test need to be recognized as 'default'
+ // engines or the resource URL won't be used
+ let url = "resource://test/data/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ run_next_test();
+}
+
+add_task(function* test_defaultresourceicon() {
+ yield asyncInit();
+
+ let engine1 = Services.search.getEngineByName("engine-resourceicon");
+ do_check_eq(engine1.iconURI.spec, "resource://search-plugins/icon16.png");
+ do_check_eq(engine1.getIconURLBySize(32, 32), "resource://search-plugins/icon32.png");
+ let engine2 = Services.search.getEngineByName("engine-chromeicon");
+ do_check_eq(engine2.iconURI.spec, "chrome://branding/content/icon16.png");
+ do_check_eq(engine2.getIconURLBySize(32, 32), "chrome://branding/content/icon32.png");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js
new file mode 100644
index 0000000000..52aff1168d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that an installed engine can't use a resource URL for an icon */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_installedresourceicon() {
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "engine-resourceicon", xmlFileName: "engine-resourceicon.xml" },
+ { name: "engine-chromeicon", xmlFileName: "engine-chromeicon.xml" },
+ ]);
+ do_check_null(engine1.iconURI);
+ do_check_null(engine2.iconURI);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js b/toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js
new file mode 100644
index 0000000000..d4c699d972
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_check_true(Services.search.getVisibleEngines().length > 1);
+ do_check_true(Services.search.isInitialized);
+
+ // Remove the current engine...
+ let currentEngine = Services.search.currentEngine;
+ Services.search.removeEngine(currentEngine);
+
+ // ... and verify a new current engine has been set.
+ do_check_neq(Services.search.currentEngine.name, currentEngine.name);
+ do_check_true(currentEngine.hidden);
+
+ // Remove all the other engines.
+ Services.search.getVisibleEngines().forEach(Services.search.removeEngine);
+ do_check_eq(Services.search.getVisibleEngines().length, 0);
+
+ // Verify the original default engine is used as a fallback and no
+ // longer hidden.
+ do_check_eq(Services.search.currentEngine.name, currentEngine.name);
+ do_check_false(currentEngine.hidden);
+ do_check_eq(Services.search.getVisibleEngines().length, 1);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js
new file mode 100644
index 0000000000..13d9922dee
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that currentEngine and defaultEngine properties can be set and yield the
+ * proper events and behavior (search results)
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_defaultEngine() {
+ let search = Services.search;
+
+ let originalDefault = search.defaultEngine;
+
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml" },
+ ]);
+
+ search.defaultEngine = engine1;
+ do_check_eq(search.defaultEngine, engine1);
+ search.defaultEngine = engine2
+ do_check_eq(search.defaultEngine, engine2);
+ search.defaultEngine = engine1;
+ do_check_eq(search.defaultEngine, engine1);
+
+ // Test that hiding the currently-default engine affects the defaultEngine getter
+ // We fallback first to the original default...
+ engine1.hidden = true;
+ do_check_eq(search.defaultEngine, originalDefault);
+
+ // ... and then to the first visible engine in the list, so move our second
+ // engine to that position.
+ search.moveEngine(engine2, 0);
+ originalDefault.hidden = true;
+ do_check_eq(search.defaultEngine, engine2);
+
+ // Test that setting defaultEngine to an already-hidden engine works, but
+ // doesn't change the return value of the getter
+ search.defaultEngine = engine1;
+ do_check_eq(search.defaultEngine, engine2);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engineUpdate.js b/toolkit/components/search/tests/xpcshell/test_engineUpdate.js
new file mode 100644
index 0000000000..adff41ffbb
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engineUpdate.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that user-set metadata isn't lost on engine update */
+
+"use strict";
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_engineUpdate() {
+ const KEYWORD = "keyword";
+ const FILENAME = "engine.xml"
+ const TOPIC = "browser-search-engine-modified";
+ const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
+
+ yield asyncInit();
+
+ let [engine] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: FILENAME },
+ ]);
+
+ engine.alias = KEYWORD;
+ Services.search.moveEngine(engine, 0);
+ // can't have an accurate updateURL in the file since we can't know the test
+ // server origin, so manually set it
+ engine.wrappedJSObject._updateURL = gDataUrl + FILENAME;
+
+ yield new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ if (data == "engine-loaded") {
+ let loadedEngine = subject.QueryInterface(Ci.nsISearchEngine);
+ let rawEngine = loadedEngine.wrappedJSObject;
+ equal(loadedEngine.alias, KEYWORD, "Keyword not cleared by update");
+ equal(rawEngine.getAttr("order"), 1, "Order not cleared by update");
+ Services.obs.removeObserver(obs, TOPIC, false);
+ resolve();
+ }
+ }, TOPIC, false);
+
+ // set last update to 8 days ago, since the default interval is 7, then
+ // trigger an update
+ engine.wrappedJSObject.setAttr("updateexpir", Date.now() - (ONE_DAY_IN_MS * 8));
+ Services.search.QueryInterface(Components.interfaces.nsITimerCallback).notify(null);
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js
new file mode 100644
index 0000000000..b3c51caa5b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js
@@ -0,0 +1,80 @@
+"use strict";
+
+function run_test() {
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_engine_set_alias() {
+ yield asyncInit();
+ do_print("Set engine alias");
+ let [engine1] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", "b", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine1.alias, "b");
+ engine1.alias = "a";
+ Assert.equal(engine1.alias, "a");
+ Services.search.removeEngine(engine1);
+});
+
+add_task(function* test_engine_set_alias_with_left_space() {
+ do_print("Set engine alias with left space");
+ let [engine2] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", " a", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine2.alias, "a");
+ engine2.alias = " c";
+ Assert.equal(engine2.alias, "c");
+ Services.search.removeEngine(engine2);
+});
+
+add_task(function* test_engine_set_alias_with_right_space() {
+ do_print("Set engine alias with right space");
+ let [engine3] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", "c ", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine3.alias, "c");
+ engine3.alias = "o ";
+ Assert.equal(engine3.alias, "o");
+ Services.search.removeEngine(engine3);
+});
+
+add_task(function* test_engine_set_alias_with_right_left_space() {
+ do_print("Set engine alias with left and right space");
+ let [engine4] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", " o ", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine4.alias, "o");
+ engine4.alias = " n ";
+ Assert.equal(engine4.alias, "n");
+ Services.search.removeEngine(engine4);
+});
+
+add_task(function* test_engine_set_alias_with_space() {
+ do_print("Set engine alias with space");
+ let [engine5] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", " ", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine5.alias, null);
+ engine5.alias = "b";
+ Assert.equal(engine5.alias, "b");
+ engine5.alias = " ";
+ Assert.equal(engine5.alias, null);
+ Services.search.removeEngine(engine5);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_geodefaults.js b/toolkit/components/search/tests/xpcshell/test_geodefaults.js
new file mode 100644
index 0000000000..2367bbbc21
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_geodefaults.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var requests = [];
+var gServerCohort = "";
+
+const kUrlPref = "geoSpecificDefaults.url";
+
+const kDayInSeconds = 86400;
+const kYearInSeconds = kDayInSeconds * 365;
+
+function run_test() {
+ updateAppInfo();
+ installTestEngine();
+
+ let srv = new HttpServer();
+
+ srv.registerPathHandler("/lookup_defaults", (metadata, response) => {
+ response.setStatusLine("1.1", 200, "OK");
+ let data = {interval: kYearInSeconds,
+ settings: {searchDefault: "Test search engine"}};
+ if (gServerCohort)
+ data.cohort = gServerCohort;
+ response.write(JSON.stringify(data));
+ requests.push(metadata);
+ });
+
+ srv.registerPathHandler("/lookup_fail", (metadata, response) => {
+ response.setStatusLine("1.1", 404, "Not Found");
+ requests.push(metadata);
+ });
+
+ srv.registerPathHandler("/lookup_unavailable", (metadata, response) => {
+ response.setStatusLine("1.1", 503, "Service Unavailable");
+ response.setHeader("Retry-After", kDayInSeconds.toString());
+ requests.push(metadata);
+ });
+
+ srv.start(-1);
+ do_register_cleanup(() => srv.stop(() => {}));
+
+ let url = "http://localhost:" + srv.identity.primaryPort + "/lookup_defaults?";
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+ // Set a bogus user value so that running the test ensures we ignore it.
+ Services.prefs.setCharPref(BROWSER_SEARCH_PREF + kUrlPref, "about:blank");
+ Services.prefs.setCharPref("browser.search.geoip.url",
+ 'data:application/json,{"country_code": "FR"}');
+
+ run_next_test();
+}
+
+function checkNoRequest() {
+ do_check_eq(requests.length, 0);
+}
+
+function checkRequest(cohort = "") {
+ do_check_eq(requests.length, 1);
+ let req = requests.pop();
+ do_check_eq(req._method, "GET");
+ do_check_eq(req._queryString, cohort ? "/" + cohort : "");
+}
+
+add_task(function* no_request_if_prefed_off() {
+ // Disable geoSpecificDefaults and check no HTTP request is made.
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+ yield asyncInit();
+ checkNoRequest();
+ yield promiseAfterCache();
+
+ // The default engine should be set based on the prefs.
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
+
+ // Ensure nothing related to geoSpecificDefaults has been written in the metadata.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "undefined");
+ do_check_eq(typeof metadata.searchDefault, "undefined");
+ do_check_eq(typeof metadata.searchDefaultHash, "undefined");
+
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
+});
+
+add_task(function* should_get_geo_defaults_only_once() {
+ // (Re)initializing the search service should trigger a request,
+ // and set the default engine based on it.
+ // Due to the previous initialization, we expect the countryCode to already be set.
+ do_check_true(Services.prefs.prefHasUserValue("browser.search.countryCode"));
+ do_check_eq(Services.prefs.getCharPref("browser.search.countryCode"), "FR");
+ yield asyncReInit();
+ checkRequest();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Verify the metadata was written correctly.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "number");
+ do_check_true(metadata.searchDefaultExpir > Date.now());
+ do_check_eq(typeof metadata.searchDefault, "string");
+ do_check_eq(metadata.searchDefault, "Test search engine");
+ do_check_eq(typeof metadata.searchDefaultHash, "string");
+ do_check_eq(metadata.searchDefaultHash.length, 44);
+
+ // The next restart shouldn't trigger a request.
+ yield asyncReInit();
+ checkNoRequest();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+});
+
+add_task(function* should_request_when_countryCode_not_set() {
+ Services.prefs.clearUserPref("browser.search.countryCode");
+ yield asyncReInit();
+ checkRequest();
+ yield promiseAfterCache();
+});
+
+add_task(function* should_recheck_if_interval_expired() {
+ yield forceExpiration();
+
+ let date = Date.now();
+ yield asyncReInit();
+ checkRequest();
+ yield promiseAfterCache();
+
+ // Check that the expiration timestamp has been updated.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "number");
+ do_check_true(metadata.searchDefaultExpir >= date + kYearInSeconds * 1000);
+ do_check_true(metadata.searchDefaultExpir < date + (kYearInSeconds + 3600) * 1000);
+});
+
+add_task(function* should_recheck_when_broken_hash() {
+ // This test verifies both that we ignore saved geo-defaults if the
+ // hash is invalid, and that we keep the local preferences-based
+ // default for all of the session in case a synchronous
+ // initialization was triggered before our HTTP request completed.
+
+ let metadata = yield promiseGlobalMetadata();
+
+ // Break the hash.
+ let hash = metadata.searchDefaultHash;
+ metadata.searchDefaultHash = "broken";
+ yield promiseSaveGlobalMetadata(metadata);
+
+ let commitPromise = promiseAfterCache();
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+
+ // Synchronously check the current default engine, to force a sync init.
+ // The hash is wrong, so we should fallback to the default engine from prefs.
+ do_check_false(Services.search.isInitialized)
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
+ do_check_true(Services.search.isInitialized)
+
+ yield reInitPromise;
+ checkRequest();
+ yield commitPromise;
+
+ // Check that the hash is back to its previous value.
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultHash, "string");
+ if (metadata.searchDefaultHash == "broken") {
+ // If the server takes more than 1000ms to return the result,
+ // the commitPromise was resolved by a first save of the cache
+ // that saved the engines, but not the request's results.
+ do_print("waiting for the cache to be saved a second time");
+ yield promiseAfterCache();
+ metadata = yield promiseGlobalMetadata();
+ }
+ do_check_eq(metadata.searchDefaultHash, hash);
+
+ // The current default engine shouldn't change during a session.
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
+
+ // After another restart, the current engine should be back to the geo default,
+ // without doing yet another request.
+ yield asyncReInit();
+ checkNoRequest();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+});
+
+add_task(function* should_remember_cohort_id() {
+ // Check that initially the cohort pref doesn't exist.
+ const cohortPref = "browser.search.cohort";
+ do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
+
+ // Make the server send a cohort id.
+ let cohort = gServerCohort = "xpcshell";
+
+ // Trigger a new request.
+ yield forceExpiration();
+ let commitPromise = promiseAfterCache();
+ yield asyncReInit();
+ checkRequest();
+ yield commitPromise;
+
+ // Check that the cohort was saved.
+ do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_STRING);
+ do_check_eq(Services.prefs.getCharPref(cohortPref), cohort);
+
+ // Make the server stop sending the cohort.
+ gServerCohort = "";
+
+ // Check that the next request sends the previous cohort id, and
+ // will remove it from the prefs due to the server no longer sending it.
+ yield forceExpiration();
+ commitPromise = promiseAfterCache();
+ yield asyncReInit();
+ checkRequest(cohort);
+ yield commitPromise;
+ do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
+});
+
+add_task(function* should_retry_after_failure() {
+ let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let originalUrl = defaultBranch.getCharPref(kUrlPref);
+ defaultBranch.setCharPref(kUrlPref, originalUrl.replace("defaults", "fail"));
+
+ // Trigger a new request.
+ yield forceExpiration();
+ yield asyncReInit();
+ checkRequest();
+
+ // After another restart, a new request should be triggered automatically without
+ // the test having to call forceExpiration again.
+ yield asyncReInit();
+ checkRequest();
+});
+
+add_task(function* should_honor_retry_after_header() {
+ let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let originalUrl = defaultBranch.getCharPref(kUrlPref);
+ defaultBranch.setCharPref(kUrlPref, originalUrl.replace("fail", "unavailable"));
+
+ // Trigger a new request.
+ yield forceExpiration();
+ let date = Date.now();
+ let commitPromise = promiseAfterCache();
+ yield asyncReInit();
+ checkRequest();
+ yield commitPromise;
+
+ // Check that the expiration timestamp has been updated.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "number");
+ do_check_true(metadata.searchDefaultExpir >= date + kDayInSeconds * 1000);
+ do_check_true(metadata.searchDefaultExpir < date + (kDayInSeconds + 3600) * 1000);
+
+ // After another restart, a new request should not be triggered.
+ yield asyncReInit();
+ checkNoRequest();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js b/toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js
new file mode 100644
index 0000000000..e48b1673c6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the hasEngineWithURL() method of the nsIBrowserSearchService.
+ */
+function run_test() {
+ do_print("Setting up test");
+
+ updateAppInfo();
+ useHttpServer();
+
+ do_print("Test starting");
+ run_next_test();
+}
+
+
+// Return a discreet, cloned copy of an (engine) object.
+function getEngineClone(engine) {
+ return JSON.parse(JSON.stringify(engine));
+}
+
+// Check whether and engine does or doesn't exist.
+function checkEngineState(exists, engine) {
+ do_check_eq(exists, Services.search.hasEngineWithURL(engine.method,
+ engine.formURL,
+ engine.queryParams));
+}
+
+// Add a search engine for testing.
+function addEngineWithParams(engine) {
+ Services.search.addEngineWithDetails(engine.name, null, null, null,
+ engine.method, engine.formURL);
+
+ let addedEngine = Services.search.getEngineByName(engine.name);
+ for (let param of engine.queryParams) {
+ addedEngine.addParam(param.name, param.value, null);
+ }
+}
+
+// Main test.
+add_task(function* test_hasEngineWithURL() {
+ // Avoid deprecated synchronous initialization.
+ yield asyncInit();
+
+ // Setup various Engine definitions for method tests.
+ let UNSORTED_ENGINE = {
+ name: "mySearch Engine",
+ method: "GET",
+ formURL: "https://totallyNotRealSearchEngine.com/",
+ queryParams: [
+ { name: "DDs", value: "38s" },
+ { name: "DCs", value: "39s" },
+ { name: "DDs", value: "39s" },
+ { name: "DDs", value: "38s" },
+ { name: "DDs", value: "37s" },
+ { name: "DDs", value: "38s" },
+ { name: "DEs", value: "38s" },
+ { name: "DCs", value: "38s" },
+ { name: "DEs", value: "37s" },
+ ],
+ };
+
+ // Same as UNSORTED_ENGINE, but sorted.
+ let SORTED_ENGINE = {
+ name: "mySearch Engine",
+ method: "GET",
+ formURL: "https://totallyNotRealSearchEngine.com/",
+ queryParams: [
+ { name: "DCs", value: "38s" },
+ { name: "DCs", value: "39s" },
+ { name: "DDs", value: "37s" },
+ { name: "DDs", value: "38s" },
+ { name: "DDs", value: "38s" },
+ { name: "DDs", value: "38s" },
+ { name: "DDs", value: "39s" },
+ { name: "DEs", value: "37s" },
+ { name: "DEs", value: "38s" },
+ ],
+ };
+
+ // Unique variations of the SORTED_ENGINE.
+ let SORTED_ENGINE_METHOD_CHANGE = getEngineClone(SORTED_ENGINE);
+ SORTED_ENGINE_METHOD_CHANGE.method = "PoST";
+
+ let SORTED_ENGINE_FORMURL_CHANGE = getEngineClone(SORTED_ENGINE);
+ SORTED_ENGINE_FORMURL_CHANGE.formURL = "http://www.ahighrpowr.com/"
+
+ let SORTED_ENGINE_QUERYPARM_CHANGE = getEngineClone(SORTED_ENGINE);
+ SORTED_ENGINE_QUERYPARM_CHANGE.queryParams = [];
+
+ let SORTED_ENGINE_NAME_CHANGE = getEngineClone(SORTED_ENGINE);
+ SORTED_ENGINE_NAME_CHANGE.name += " 2";
+
+
+ // First ensure neither the unsorted engine, nor the same engine
+ // with a pre-sorted list of query parms matches.
+ checkEngineState(false, UNSORTED_ENGINE);
+ do_print("The unsorted version of the test engine does not exist.");
+ checkEngineState(false, SORTED_ENGINE);
+ do_print("The sorted version of the test engine does not exist.");
+
+ // Ensure variations of the engine definition do not match.
+ checkEngineState(false, SORTED_ENGINE_METHOD_CHANGE);
+ checkEngineState(false, SORTED_ENGINE_FORMURL_CHANGE);
+ checkEngineState(false, SORTED_ENGINE_QUERYPARM_CHANGE);
+ do_print("There are no modified versions of the sorted test engine.");
+
+ // Note that this method doesn't check name variations.
+ checkEngineState(false, SORTED_ENGINE_NAME_CHANGE);
+ do_print("There is no NAME modified version of the sorted test engine.");
+
+
+ // Add the unsorted engine and it's queryParams.
+ addEngineWithParams(UNSORTED_ENGINE);
+ do_print("The unsorted engine has been added.");
+
+
+ // Then, ensure we find a match for the unsorted engine, and for the
+ // same engine with a pre-sorted list of query parms.
+ checkEngineState(true, UNSORTED_ENGINE);
+ do_print("The unsorted version of the test engine now exists.");
+ checkEngineState(true, SORTED_ENGINE);
+ do_print("The sorted version of the same test engine also now exists.");
+
+ // Ensure variations of the engine definition still do not match.
+ checkEngineState(false, SORTED_ENGINE_METHOD_CHANGE);
+ checkEngineState(false, SORTED_ENGINE_FORMURL_CHANGE);
+ checkEngineState(false, SORTED_ENGINE_QUERYPARM_CHANGE);
+ do_print("There are still no modified versions of the sorted test engine.");
+
+ // Note that this method still doesn't check name variations.
+ checkEngineState(true, SORTED_ENGINE_NAME_CHANGE);
+ do_print("There IS now a NAME modified version of the sorted test engine.");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_hidden.js b/toolkit/components/search/tests/xpcshell/test_hidden.js
new file mode 100644
index 0000000000..b784f36242
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_hidden.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const kUrlPref = "geoSpecificDefaults.url";
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Geo specific defaults won't be fetched if there's no country code.
+ Services.prefs.setCharPref("browser.search.geoip.url",
+ 'data:application/json,{"country_code": "US"}');
+
+ // Make 'hidden' the only visible engine.
+ let url = "data:application/json,{\"interval\": 31536000, \"settings\": {\"searchDefault\": \"hidden\", \"visibleDefaultEngines\": [\"hidden\"]}}";
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+
+ do_check_false(Services.search.isInitialized);
+
+ run_next_test();
+}
+
+add_task(function* async_init() {
+ let commitPromise = promiseAfterCache()
+ yield asyncInit();
+
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // The default test jar engine has been hidden.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_eq(engine, null);
+
+ // The hidden engine is visible.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_neq(engine, null);
+
+ // The next test does a sync init, which won't do the geoSpecificDefaults XHR,
+ // so it depends on the metadata having been written to disk.
+ yield commitPromise;
+});
+
+add_task(function* sync_init() {
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+ do_check_false(Services.search.isInitialized);
+
+ // Synchronously check the current default engine, to force a sync init.
+ do_check_eq(Services.search.currentEngine.name, "hidden");
+ do_check_true(Services.search.isInitialized);
+
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // The default test jar engine has been hidden.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_eq(engine, null);
+
+ // The hidden engine is visible.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_neq(engine, null);
+
+ yield reInitPromise;
+});
+
+add_task(function* invalid_engine() {
+ // Trigger a new request.
+ yield forceExpiration();
+
+ // Set the visibleDefaultEngines list to something that contains a non-existent engine.
+ // This should cause the search service to ignore the list altogether and fallback to
+ // local defaults.
+ let url = "data:application/json,{\"interval\": 31536000, \"settings\": {\"searchDefault\": \"hidden\", \"visibleDefaultEngines\": [\"hidden\", \"bogus\"]}}";
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+
+ yield asyncReInit();
+
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // The default test jar engine is visible.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // The hidden engine is... hidden.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_eq(engine, null);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_identifiers.js b/toolkit/components/search/tests/xpcshell/test_identifiers.js
new file mode 100644
index 0000000000..0d5ca5b90b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_identifiers.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that a search engine's identifier can be extracted from the filename.
+ */
+
+"use strict";
+
+const SEARCH_APP_DIR = 1;
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ updateAppInfo();
+
+ run_next_test();
+}
+
+add_test(function test_identifier() {
+ let engineFile = gProfD.clone();
+ engineFile.append("searchplugins");
+ engineFile.append("test-search-engine.xml");
+ engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // Copy the test engine to the test profile.
+ let engineTemplateFile = do_get_file("data/engine.xml");
+ engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
+
+ Services.search.init(function initComplete(aResult) {
+ do_print("init'd search service");
+ do_check_true(Components.isSuccessCode(aResult));
+
+ let profileEngine = Services.search.getEngineByName("Test search engine");
+ let jarEngine = Services.search.getEngineByName("bug645970");
+
+ do_check_true(profileEngine instanceof Ci.nsISearchEngine);
+ do_check_true(jarEngine instanceof Ci.nsISearchEngine);
+
+ // An engine loaded from the profile directory won't have an identifier,
+ // because it's not built-in.
+ do_check_eq(profileEngine.identifier, null);
+
+ // An engine loaded from a JAR will have an identifier corresponding to
+ // the filename inside the JAR. (In this case it's the same as the name.)
+ do_check_eq(jarEngine.identifier, "bug645970");
+
+ removeMetadata();
+ removeCacheFile();
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_init_async_multiple.js b/toolkit/components/search/tests/xpcshell/test_init_async_multiple.js
new file mode 100644
index 0000000000..3aa3353cef
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_init_async_multiple.js
@@ -0,0 +1,55 @@
+/* -*- 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/. */
+
+/**
+ * Test nsSearchService with with the following initialization scenario:
+ * - launch asynchronous initialization several times;
+ * - all asynchronous initializations must complete.
+ *
+ * Test case comes from test_645970.js
+ */
+function run_test() {
+ do_print("Setting up test");
+
+ do_test_pending();
+ updateAppInfo();
+
+ do_print("Test starting");
+ let numberOfInitializers = 4;
+ let pending = [];
+ let numberPending = numberOfInitializers;
+
+ // Start asynchronous initializations
+ for (let i = 0; i < numberOfInitializers; ++i) {
+ let me = i;
+ pending[me] = true;
+ Services.search.init(function search_initialized_0(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ init_complete(me);
+ });
+ }
+
+ // Wait until all initializers have completed
+ let init_complete = function init_complete(i) {
+ do_check_true(pending[i]);
+ pending[i] = false;
+ numberPending--;
+ do_check_true(numberPending >= 0);
+ do_check_true(Services.search.isInitialized);
+ if (numberPending == 0) {
+ // Just check that we can access a list of engines.
+ let engines = Services.search.getEngines();
+ do_check_neq(engines, null);
+
+ // Wait a little before quitting: if some initializer is
+ // triggered twice, we want to catch that error.
+ do_timeout(1000, function() {
+ do_test_finished();
+ });
+ }
+ };
+}
+
diff --git a/toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js b/toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js
new file mode 100644
index 0000000000..ed4ecdcd8e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js
@@ -0,0 +1,68 @@
+/* -*- 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/. */
+
+/**
+ * Test nsSearchService with with the following initialization scenario:
+ * - launch asynchronous initialization several times;
+ * - force fallback to synchronous initialization.
+ * - all asynchronous initializations must complete;
+ * - no asynchronous initialization must complete more than once.
+ *
+ * Test case comes from test_645970.js
+ */
+function run_test() {
+ do_print("Setting up test");
+ do_test_pending();
+ updateAppInfo();
+
+ do_print("Test starting");
+
+ let numberOfInitializers = 4;
+ let pending = [];
+ let numberPending = numberOfInitializers;
+
+ // Start asynchronous initializations
+ for (let i = 0; i < numberOfInitializers; ++i) {
+ let me = i;
+ pending[me] = true;
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ init_complete(me);
+ });
+ }
+
+ // Ensure that all asynchronous initializers eventually complete
+ let init_complete = function init_complete(i) {
+ do_print("init complete " + i);
+ do_check_true(pending[i]);
+ pending[i] = false;
+ numberPending--;
+ do_check_true(numberPending >= 0);
+ do_check_true(Services.search.isInitialized);
+ if (numberPending != 0) {
+ do_print("Still waiting for the following initializations: " + JSON.stringify(pending));
+ return;
+ }
+ do_print("All initializations have completed");
+ // Just check that we can access a list of engines.
+ let engines = Services.search.getEngines();
+ do_check_neq(engines, null);
+
+ do_print("Waiting a second before quitting");
+ // Wait a little before quitting: if some initializer is
+ // triggered twice, we want to catch that error.
+ do_timeout(1000, function() {
+ do_print("Test is complete");
+ do_test_finished();
+ });
+ };
+
+ // ... but don't wait for asynchronous initializations to complete
+ let engines = Services.search.getEngines();
+ do_check_neq(engines, null);
+ do_print("Synchronous part of the test complete");
+}
+
diff --git a/toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js b/toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js
new file mode 100644
index 0000000000..c6455735ac
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that invalid engine files with xml extensions will not break
+ * initialization. See Bug 940446.
+ */
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineFile = gProfD.clone();
+ engineFile.append("searchplugins");
+ engineFile.append("test-search-engine.xml");
+ engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // Copy the invalid engine to the test profile.
+ let engineTemplateFile = do_get_file("data/invalid-engine.xml");
+ engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
+
+ Services.search.init(function search_initialized(aStatus) {
+ // The invalid engine should have been skipped and should not
+ // have caused an exception.
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ removeMetadata();
+ removeCacheFile();
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_json_cache.js b/toolkit/components/search/tests/xpcshell/test_json_cache.js
new file mode 100644
index 0000000000..c804b0bca5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_json_cache.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing from the search cache.
+ */
+
+"use strict";
+
+/**
+ * Gets a directory from the directory service.
+ * @param aKey
+ * The directory service key indicating the directory to get.
+ */
+var _dirSvc = null;
+function getDir(aKey, aIFace) {
+ if (!aKey) {
+ FAIL("getDir requires a directory key!");
+ }
+
+ if (!_dirSvc) {
+ _dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ }
+ return _dirSvc.get(aKey, aIFace || Ci.nsIFile);
+}
+
+function makeURI(uri) {
+ return Services.io.newURI(uri, null, null);
+}
+
+var cacheTemplate, appPluginsPath, profPlugins;
+
+/**
+ * Test reading from search.json.mozlz4
+ */
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ updateAppInfo();
+
+ let cacheTemplateFile = do_get_file("data/search.json");
+ cacheTemplate = readJSONFile(cacheTemplateFile);
+ cacheTemplate.buildID = getAppInfo().platformBuildID;
+
+ let engineFile = gProfD.clone();
+ engineFile.append("searchplugins");
+ engineFile.append("test-search-engine.xml");
+ engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // Copy the test engine to the test profile.
+ let engineTemplateFile = do_get_file("data/engine.xml");
+ engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
+
+ // The list of visibleDefaultEngines needs to match or the cache will be ignored.
+ let chan = NetUtil.newChannel({
+ uri: "resource://search-plugins/list.json",
+ loadUsingSystemPrincipal: true
+ });
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(chan.open2());
+ let list = sis.read(sis.available());
+ let searchSettings = JSON.parse(list);
+
+ cacheTemplate.visibleDefaultEngines = searchSettings["default"]["visibleDefaultEngines"];
+
+ run_next_test();
+}
+
+add_test(function prepare_test_data() {
+ OS.File.writeAtomic(OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME),
+ new TextEncoder().encode(JSON.stringify(cacheTemplate)),
+ {compression: "lz4"})
+ .then(run_next_test);
+});
+
+/**
+ * Start the search service and confirm the engine properties match the expected values.
+ */
+add_test(function test_cached_engine_properties() {
+ do_print("init search service");
+
+ Services.search.init(function initComplete(aResult) {
+ do_print("init'd search service");
+ do_check_true(Components.isSuccessCode(aResult));
+
+ let engines = Services.search.getEngines({});
+ let engine = engines[0];
+
+ do_check_true(engine instanceof Ci.nsISearchEngine);
+ isSubObjectOf(EXPECTED_ENGINE.engine, engine);
+
+ let engineFromSS = Services.search.getEngineByName(EXPECTED_ENGINE.engine.name);
+ do_check_true(!!engineFromSS);
+ isSubObjectOf(EXPECTED_ENGINE.engine, engineFromSS);
+
+ removeMetadata();
+ removeCacheFile();
+ run_next_test();
+ });
+});
+
+/**
+ * Test that the JSON cache written in the profile is correct.
+ */
+add_test(function test_cache_write() {
+ do_print("test cache writing");
+
+ let cache = gProfD.clone();
+ cache.append(CACHE_FILENAME);
+ do_check_false(cache.exists());
+
+ do_print("Next step is forcing flush");
+ do_timeout(0, function forceFlush() {
+ do_print("Forcing flush");
+ // Force flush
+ // Note: the timeout is needed, to avoid some reentrency
+ // issues in nsSearchService.
+
+ let cacheWriteObserver = {
+ observe: function cacheWriteObserver_observe(aEngine, aTopic, aVerb) {
+ if (aTopic != "browser-search-service" || aVerb != "write-cache-to-disk-complete") {
+ return;
+ }
+ Services.obs.removeObserver(cacheWriteObserver, "browser-search-service");
+ do_print("Cache write complete");
+ do_check_true(cache.exists());
+ // Check that the search.json.mozlz4 cache matches the template
+
+ promiseCacheData().then(cacheWritten => {
+ do_print("Check search.json.mozlz4");
+ isSubObjectOf(cacheTemplate, cacheWritten);
+
+ run_next_test();
+ });
+ }
+ };
+ Services.obs.addObserver(cacheWriteObserver, "browser-search-service", false);
+
+ Services.search.QueryInterface(Ci.nsIObserver).observe(null, "browser-search-engine-modified", "engine-removed");
+ });
+});
+
+var EXPECTED_ENGINE = {
+ engine: {
+ name: "Test search engine",
+ alias: null,
+ description: "A test search engine (based on Google search)",
+ searchForm: "http://www.google.com/",
+ wrappedJSObject: {
+ _extensionID: "test-addon-id@mozilla.org",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ _urls : [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template: "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox" +
+ "&hl={moz:locale}&q={searchTerms}",
+ params: "",
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "http://www.google.com/search",
+ resultDomain: "google.com",
+ params: [
+ {
+ "name": "q",
+ "value": "{searchTerms}",
+ "purpose": undefined,
+ },
+ {
+ "name": "ie",
+ "value": "utf-8",
+ "purpose": undefined,
+ },
+ {
+ "name": "oe",
+ "value": "utf-8",
+ "purpose": undefined,
+ },
+ {
+ "name": "aq",
+ "value": "t",
+ "purpose": undefined,
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword",
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu",
+ },
+ ],
+ },
+ {
+ type: "application/x-moz-default-purpose",
+ method: "GET",
+ template: "http://www.google.com/search",
+ resultDomain: "purpose.google.com",
+ params: [
+ {
+ "name": "q",
+ "value": "{searchTerms}",
+ "purpose": undefined,
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword",
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu",
+ },
+ ],
+ },
+ ],
+ },
+ },
+};
diff --git a/toolkit/components/search/tests/xpcshell/test_location.js b/toolkit/components/search/tests/xpcshell/test_location.js
new file mode 100644
index 0000000000..93e6139f64
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ installTestEngine();
+
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+ Services.search.init(() => {
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU", "got the correct country code.");
+ equal(Services.prefs.getCharPref("browser.search.region"), "AU", "region pref also set to the countryCode.")
+ // No isUS pref is ever written
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "no isUS pref")
+ // check we have "success" recorded in telemetry
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+ // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+
+ }
+
+ // simple checks for our platform-specific telemetry. We can't influence
+ // what they return (as we can't influence the countryCode the platform
+ // thinks we are in), but we can check the values are correct given reality.
+ let probeUSMismatched, probeNonUSMismatched;
+ switch (Services.appinfo.OS) {
+ case "Darwin":
+ probeUSMismatched = "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX";
+ probeNonUSMismatched = "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX";
+ break;
+ case "WINNT":
+ probeUSMismatched = "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN";
+ probeNonUSMismatched = "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN";
+ break;
+ default:
+ break;
+ }
+
+ if (probeUSMismatched && probeNonUSMismatched) {
+ let countryCode = Services.sysinfo.get("countryCode");
+ print("Platform says the country-code is", countryCode);
+ let expectedResult;
+ let hid;
+ // We know geoip said AU - if the platform thinks US then we expect
+ // probeUSMismatched with true (ie, a mismatch)
+ if (countryCode == "US") {
+ hid = probeUSMismatched;
+ expectedResult = [0, 1, 0]; // boolean probe so 3 buckets, expect 1 result for |1|.
+ } else {
+ // We are expecting probeNonUSMismatched with false if the platform
+ // says AU (not a mismatch) and true otherwise.
+ hid = probeNonUSMismatched;
+ expectedResult = countryCode == "AU" ? [1, 0, 0] : [0, 1, 0];
+ }
+
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, expectedResult);
+ }
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_error.js b/toolkit/components/search/tests/xpcshell/test_location_error.js
new file mode 100644
index 0000000000..049189351a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_error.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ installTestEngine();
+
+ // We use an invalid port that parses but won't open
+ let url = "http://localhost:0";
+
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ Services.search.init(() => {
+ try {
+ Services.prefs.getCharPref("browser.search.countryCode");
+ ok(false, "not expecting countryCode to be set");
+ } catch (ex) {}
+ // should have an error recorded.
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.ERROR);
+ // but false values for timeout and forced-sync-init.
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_funnelcake.js b/toolkit/components/search/tests/xpcshell/test_location_funnelcake.js
new file mode 100644
index 0000000000..970ba5521e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_funnelcake.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "US"}');
+ // funnelcake builds start with "mozilla"
+ Services.prefs.setCharPref("distribution.id", 'mozilla38');
+ setUpGeoDefaults();
+
+ Services.search.init(() => {
+ equal(Services.search.defaultEngine.name, "A second test engine");
+
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js b/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js
new file mode 100644
index 0000000000..b1f30ad7cc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// A console listener so we can listen for a log message from nsSearchService.
+function promiseTimezoneMessage() {
+ return new Promise(resolve => {
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
+ observe : function (msg) {
+ if (msg.message.startsWith("getIsUS() fell back to a timezone check with the result=")) {
+ Services.console.unregisterListener(listener);
+ resolve(msg);
+ }
+ }
+ };
+ Services.console.registerListener(listener);
+ });
+}
+
+function run_test() {
+ installTestEngine();
+
+ // setup a console listener for the timezone fallback message.
+ let promiseTzMessage = promiseTimezoneMessage();
+
+ // Here we have malformed JSON
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code"');
+ Services.search.init(() => {
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "should never be an isUS pref");
+ // fetch the engines - this should force the timezone check, but still
+ // doesn't persist any prefs.
+ Services.search.getEngines();
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "should never be an isUS pref");
+ // should have recorded SUCCESS_WITHOUT_DATA
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS_WITHOUT_DATA);
+ // and false values for timeout and forced-sync-init.
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+
+ // Check we saw the timezone fallback message.
+ promiseTzMessage.then(msg => {
+ print("Timezone message:", msg.message);
+ ok(msg.message.endsWith(isUSTimezone().toString()), "fell back to timezone and it matches our timezone");
+ do_test_finished();
+ run_next_test();
+ });
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js b/toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js
new file mode 100644
index 0000000000..9e1019761a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Here we are testing the "migration" when both isUS and countryCode are
+// set.
+function run_test() {
+ installTestEngine();
+
+ // Set the prefs we care about.
+ Services.prefs.setBoolPref("browser.search.isUS", true);
+ Services.prefs.setCharPref("browser.search.countryCode", "US");
+ // And the geoip request that will return AU - it shouldn't be used.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+ Services.search.init(() => {
+ // "region" and countryCode should still reflect US.
+ equal(Services.prefs.getCharPref("browser.search.region"), "US", "got the correct region.");
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "US", "got the correct country code.");
+ // should be no geoip evidence.
+ checkCountryResultTelemetry(null);
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js
new file mode 100644
index 0000000000..b294b038b7
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Here we are testing the "migration" from the isUS pref being true but when
+// no country-code exists.
+function run_test() {
+ installTestEngine();
+
+ // Set the pref we care about.
+ Services.prefs.setBoolPref("browser.search.isUS", true);
+ // And the geoip request that will return *not* US.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+ Services.search.init(() => {
+ // "region" should be set to US, but countryCode to AU.
+ equal(Services.prefs.getCharPref("browser.search.region"), "US", "got the correct region.");
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU", "got the correct country code.");
+ // check we have "success" recorded in telemetry
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+ // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js
new file mode 100644
index 0000000000..9c0b810d31
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Here we are testing the "migration" from the isUS pref being false but when
+// no country-code exists.
+function run_test() {
+ installTestEngine();
+
+ // Set the pref we care about.
+ Services.prefs.setBoolPref("browser.search.isUS", false);
+ // And the geoip request that will return US.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "US"}');
+ Services.search.init(() => {
+ // "region" and countryCode should reflect US.
+ equal(Services.prefs.getCharPref("browser.search.region"), "US", "got the correct region.");
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "US", "got the correct country code.");
+ // check we have "success" recorded in telemetry
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+ // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_partner.js b/toolkit/components/search/tests/xpcshell/test_location_partner.js
new file mode 100644
index 0000000000..9151add9a9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_partner.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "US"}');
+ Services.prefs.setCharPref("distribution.id", 'partner-1');
+ setUpGeoDefaults();
+
+ Services.search.init(() => {
+ equal(Services.search.defaultEngine.name, "Test search engine");
+
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_sync.js b/toolkit/components/search/tests/xpcshell/test_location_sync.js
new file mode 100644
index 0000000000..524a440fbd
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_sync.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getCountryCodePref() {
+ try {
+ return Services.prefs.getCharPref("browser.search.countryCode");
+ } catch (_) {
+ return undefined;
+ }
+}
+
+function getIsUSPref() {
+ try {
+ return Services.prefs.getBoolPref("browser.search.isUS");
+ } catch (_) {
+ return undefined;
+ }
+}
+
+// A console listener so we can listen for a log message from nsSearchService.
+function promiseTimezoneMessage() {
+ return new Promise(resolve => {
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
+ observe : function (msg) {
+ if (msg.message.startsWith("getIsUS() fell back to a timezone check with the result=")) {
+ Services.console.unregisterListener(listener);
+ resolve(msg);
+ }
+ }
+ };
+ Services.console.registerListener(listener);
+ });
+}
+
+function run_test() {
+ installTestEngine();
+
+ run_next_test();
+}
+
+// Force a sync init and ensure the right thing happens (ie, that no xhr
+// request is made and we fall back to the timezone-only trick)
+add_task(function* test_simple() {
+ deepEqual(getCountryCodePref(), undefined, "no countryCode pref");
+ deepEqual(getIsUSPref(), undefined, "no isUS pref");
+
+ // Still set a geoip pref so we can (indirectly) check it wasn't used.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+
+ ok(!Services.search.isInitialized);
+
+ // setup a console listener for the timezone fallback message.
+ let promiseTzMessage = promiseTimezoneMessage();
+
+ // fetching the engines forces a sync init, and should have caused us to
+ // check the timezone.
+ Services.search.getEngines();
+ ok(Services.search.isInitialized);
+
+ // a little wait to check we didn't do the xhr thang.
+ yield new Promise(resolve => {
+ do_timeout(500, resolve);
+ });
+
+ let msg = yield promiseTzMessage;
+ print("Timezone message:", msg.message);
+ ok(msg.message.endsWith(isUSTimezone().toString()), "fell back to timezone and it matches our timezone");
+
+ deepEqual(getCountryCodePref(), undefined, "didn't do the geoip xhr");
+ // and no telemetry evidence of geoip.
+ for (let hid of [
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS",
+ "SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE",
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT",
+ ]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ equal(snapshot.sum, 0, hid);
+ switch (snapshot.histogram_type) {
+ case Ci.nsITelemetry.HISTOGRAM_FLAG:
+ // flags are a special case in that they are initialized with a default
+ // of one |0|.
+ deepEqual(snapshot.counts, [1, 0, 0], hid);
+ break;
+ case Ci.nsITelemetry.HISTOGRAM_BOOLEAN:
+ // booleans aren't initialized at all, so should have all zeros.
+ deepEqual(snapshot.counts, [0, 0, 0], hid);
+ break;
+ case Ci.nsITelemetry.HISTOGRAM_EXPONENTIAL:
+ case Ci.nsITelemetry.HISTOGRAM_LINEAR:
+ equal(snapshot.counts.reduce((a, b) => a+b), 0, hid);
+ break;
+ default:
+ ok(false, "unknown histogram type " + snapshot.histogram_type + " for " + hid);
+ }
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_location_timeout.js b/toolkit/components/search/tests/xpcshell/test_location_timeout.js
new file mode 100644
index 0000000000..c1d5270e5f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is testing the "normal" timer-based timeout for the location search.
+
+function startServer(continuePromise) {
+ let srv = new HttpServer();
+ function lookupCountry(metadata, response) {
+ response.processAsync();
+ // wait for our continuePromise to resolve before writing a valid
+ // response.
+ // This will be resolved after the timeout period, so we can check
+ // the behaviour in that case.
+ continuePromise.then(() => {
+ response.setStatusLine("1.1", 200, "OK");
+ response.write('{"country_code" : "AU"}');
+ response.finish();
+ });
+ }
+ srv.registerPathHandler("/lookup_country", lookupCountry);
+ srv.start(-1);
+ return srv;
+}
+
+function getProbeSum(probe, sum) {
+ let histogram = Services.telemetry.getHistogramById(probe);
+ return histogram.snapshot().sum;
+}
+
+function run_test() {
+ installTestEngine();
+
+ let resolveContinuePromise;
+ let continuePromise = new Promise(resolve => {
+ resolveContinuePromise = resolve;
+ });
+
+ let server = startServer(continuePromise);
+ let url = "http://localhost:" + server.identity.primaryPort + "/lookup_country";
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 50);
+ Services.search.init(() => {
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ // should be no result recorded at all.
+ checkCountryResultTelemetry(null);
+
+ // should have set the flag indicating we saw a timeout.
+ let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT");
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [0, 1, 0]);
+ // should not yet have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+ // test server is still blocked on our promise.
+ equal(getProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS"), 0);
+
+ waitForSearchNotification("geoip-lookup-xhr-complete").then(() => {
+ // now we *should* have a report of how long the response took even though
+ // it timed out.
+ // The telemetry "sum" will be the actual time in ms - just check it's non-zero.
+ ok(getProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS") != 0);
+ // should have reported the fetch ended up being successful
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+
+ // and should have the result of the response that finally came in, and
+ // everything dependent should also be updated.
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU");
+ equal(Services.prefs.getCharPref("browser.search.region"), "AU");
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "should never have an isUS pref");
+
+ do_test_finished();
+ server.stop(run_next_test);
+ });
+ // now tell the server to send its response. That will end up causing the
+ // search service to notify of that the response was received.
+ resolveContinuePromise();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
new file mode 100644
index 0000000000..4054cf0c27
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is testing the long, last-resort XHR-based timeout for the location
+// search.
+
+function startServer(continuePromise) {
+ let srv = new HttpServer();
+ function lookupCountry(metadata, response) {
+ response.processAsync();
+ // wait for our continuePromise to resolve before writing a valid
+ // response.
+ // This will be resolved after the timeout period, so we can check
+ // the behaviour in that case.
+ continuePromise.then(() => {
+ response.setStatusLine("1.1", 200, "OK");
+ response.write('{"country_code" : "AU"}');
+ response.finish();
+ });
+ }
+ srv.registerPathHandler("/lookup_country", lookupCountry);
+ srv.start(-1);
+ return srv;
+}
+
+function verifyProbeSum(probe, sum) {
+ let histogram = Services.telemetry.getHistogramById(probe);
+ let snapshot = histogram.snapshot();
+ equal(snapshot.sum, sum, probe);
+}
+
+function run_test() {
+ installTestEngine();
+
+ let resolveContinuePromise;
+ let continuePromise = new Promise(resolve => {
+ resolveContinuePromise = resolve;
+ });
+
+ let server = startServer(continuePromise);
+ let url = "http://localhost:" + server.identity.primaryPort + "/lookup_country";
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ // The timeout for the timer.
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 10);
+ let promiseXHRStarted = waitForSearchNotification("geoip-lookup-xhr-starting");
+ Services.search.init(() => {
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ // should be no result recorded at all.
+ checkCountryResultTelemetry(null);
+
+ // should have set the flag indicating we saw a timeout.
+ let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT");
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [0, 1, 0]);
+
+ // should not have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+ // test server is still blocked on our promise.
+ verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+
+ promiseXHRStarted.then(xhr => {
+ // Set the timeout on the xhr object to an extremely low value, so it
+ // should timeout immediately.
+ xhr.timeout = 10;
+ // wait for the xhr timeout to fire.
+ waitForSearchNotification("geoip-lookup-xhr-complete").then(() => {
+ // should have the XHR timeout recorded.
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.XHRTIMEOUT);
+ // still should not have a report of how long the response took as we
+ // only record that on success responses.
+ verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+ // and we still don't know the country code or region.
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+
+ // unblock the server even though nothing is listening.
+ resolveContinuePromise();
+
+ do_test_finished();
+ server.stop(run_next_test);
+ });
+ });
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_migration_langpack.js b/toolkit/components/search/tests/xpcshell/test_migration_langpack.js
new file mode 100644
index 0000000000..8cb2014bd3
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_migration_langpack.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Unless we unset the XPCSHELL_TEST_PROFILE_DIR environment variable,
+ // engine._isDefault will be true for engines from the resource:// scheme,
+ // bypassing the codepath we want to test.
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ env.set("XPCSHELL_TEST_PROFILE_DIR", "");
+
+ do_get_file("data/langpack-metadata.json").copyTo(gProfD, "search-metadata.json");
+
+ do_check_false(Services.search.isInitialized);
+
+ run_next_test();
+}
+
+add_task(function* async_init() {
+ let commitPromise = promiseAfterCache()
+ yield asyncInit();
+
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+ do_check_eq(engine.wrappedJSObject._id, "[app]/bug645970.xml");
+
+ yield commitPromise;
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["bug645970"].alias, "lp");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_multipleIcons.js b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
new file mode 100644
index 0000000000..314515f6df
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getIcons() and getIconURLBySize() on engine with multiple icons.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_multipleIcons() {
+ let [engine] = yield addTestEngines([
+ { name: "IconsTest", xmlFileName: "engineImages.xml" },
+ ]);
+
+ do_print("The default should be the 16x16 icon");
+ do_check_true(engine.iconURI.spec.includes("ico16"));
+
+ do_check_true(engine.getIconURLBySize(16, 16).includes("ico16"));
+ do_check_true(engine.getIconURLBySize(32, 32).includes("ico32"));
+ do_check_true(engine.getIconURLBySize(74, 74).includes("ico74"));
+
+ do_print("Invalid dimensions should return null.");
+ do_check_null(engine.getIconURLBySize(50, 50));
+
+ let allIcons = engine.getIcons();
+
+ do_print("Check that allIcons contains expected icon sizes");
+ do_check_eq(allIcons.length, 3);
+ let expectedWidths = [16, 32, 74];
+ do_check_true(allIcons.every((item) => {
+ let width = item.width;
+ do_check_neq(expectedWidths.indexOf(width), -1);
+ do_check_eq(width, item.height);
+
+ let icon = item.url.split(",").pop();
+ do_check_eq(icon, "ico" + width);
+
+ return true;
+ }));
+});
+
+add_task(function* test_icon_not_in_file() {
+ let engineUrl = gDataUrl + "engine-fr.xml";
+ let engine = yield new Promise((resolve, reject) => {
+ Services.search.addEngine(engineUrl, null, "data:image/x-icon;base64,ico16",
+ false, {onSuccess: resolve, onError: reject});
+ });
+
+ // Even though the icon wasn't specified inside the XML file, it should be
+ // available both in the iconURI attribute and with getIconURLBySize.
+ do_check_true(engine.iconURI.spec.includes("ico16"));
+ do_check_true(engine.getIconURLBySize(16, 16).includes("ico16"));
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_nocache.js b/toolkit/components/search/tests/xpcshell/test_nocache.js
new file mode 100644
index 0000000000..42776aef04
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nocache.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_nocache: Start search engine
+ * - without search.json.mozlz4
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - search.json.mozlz4 is created.
+ */
+
+function run_test()
+{
+ removeCacheFile();
+ updateAppInfo();
+ do_load_manifest("data/chrome.manifest");
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_nocache() {
+ let search = Services.search;
+
+ let afterCachePromise = promiseAfterCache();
+
+ yield new Promise((resolve, reject) => search.init(rv => {
+ Components.isSuccessCode(rv) ? resolve() : reject();
+ }));
+
+ // Check that the cache is created at startup
+ yield afterCachePromise;
+
+ // Check that search.json has been created.
+ let cacheFile = gProfD.clone();
+ cacheFile.append(CACHE_FILENAME);
+ do_check_true(cacheFile.exists());
+
+ // Add engine and wait for cache update
+ yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ ]);
+
+ do_print("Engine has been added, let's wait for the cache to be built");
+ yield promiseAfterCache();
+
+ do_print("Searching test engine in cache");
+ let cache = yield promiseCacheData();
+ let found = false;
+ for (let engine of cache.engines) {
+ if (engine._shortName == "test-search-engine") {
+ found = true;
+ break;
+ }
+ }
+ do_check_true(found);
+
+ removeCacheFile();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_nodb.js b/toolkit/components/search/tests/xpcshell/test_nodb.js
new file mode 100644
index 0000000000..66a003a5dc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nodb.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_nodb: Start search engine
+ * - without search-metadata.json
+ * - without search.sqlite
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - no search-metadata.json is created.
+ */
+
+
+function run_test()
+{
+ removeMetadata();
+ updateAppInfo();
+
+ let search = Services.search;
+
+ do_test_pending();
+ search.init(function ss_initialized(rv) {
+ do_check_true(Components.isSuccessCode(rv));
+ do_timeout(500, function() {
+ // Check that search-metadata.json has not been
+ // created. Note that we cannot do much better
+ // than a timeout for checking a non-event.
+ let metadata = gProfD.clone();
+ metadata.append("search-metadata.json");
+ do_check_true(!metadata.exists());
+ removeMetadata();
+
+ do_test_finished();
+ });
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js
new file mode 100644
index 0000000000..e6789e9649
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+/*
+ * test_nodb: Start search engine
+ * - without search-metadata.json
+ * - without search.sqlite
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - if we change the order, search.json.mozlz4 is updated;
+ * - this search.json.mozlz4 can be parsed;
+ * - the order stored in search.json.mozlz4 is consistent.
+ *
+ * Notes:
+ * - we install the search engines of test "test_downloadAndAddEngines.js"
+ * to ensure that this test is independent from locale, commercial agreements
+ * and configuration of Firefox.
+ */
+
+function run_test() {
+ updateAppInfo();
+ do_load_manifest("data/chrome.manifest");
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_nodb_pluschanges() {
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml"},
+ ]);
+ yield promiseAfterCache();
+
+ let search = Services.search;
+
+ search.moveEngine(engine1, 0);
+ search.moveEngine(engine2, 1);
+
+ // This is needed to avoid some reentrency issues in nsSearchService.
+ do_print("Next step is forcing flush");
+ yield new Promise(resolve => do_execute_soon(resolve));
+
+ do_print("Forcing flush");
+ let promiseCommit = promiseAfterCache();
+ search.QueryInterface(Ci.nsIObserver)
+ .observe(null, "quit-application", "");
+ yield promiseCommit;
+ do_print("Commit complete");
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["test-search-engine"].order, 1);
+ do_check_eq(metadata["a-second-test-engine"].order, 2);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_notifications.js b/toolkit/components/search/tests/xpcshell/test_notifications.js
new file mode 100644
index 0000000000..3eecbf8b16
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_notifications.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestLog = [];
+
+/**
+ * The order of notifications expected for this test is:
+ * - engine-changed (while we're installing the engine, we modify it, which notifies - bug 606886)
+ * - engine-added (engine was added to the store by the search service)
+ * -> our search observer is called, which sets:
+ * - .defaultEngine, triggering engine-default
+ * - .currentEngine, triggering engine-current (after bug 493051 - for now the search service sets this after engine-added)
+ * ...and then schedules a removal
+ * - engine-loaded (the search service's observer is garanteed to fire first, which is what causes engine-added to fire)
+ * - engine-removed (due to the removal schedule above)
+ */
+var expectedLog = [
+ "engine-changed", // XXX bug 606886
+ "engine-added",
+ "engine-default",
+ "engine-current",
+ "engine-loaded",
+ "engine-removed"
+];
+
+function search_observer(subject, topic, data) {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ gTestLog.push(data + " for " + engine.name);
+
+ do_print("Observer: " + data + " for " + engine.name);
+
+ switch (data) {
+ case "engine-added":
+ let retrievedEngine = Services.search.getEngineByName("Test search engine");
+ do_check_eq(engine, retrievedEngine);
+ Services.search.defaultEngine = engine;
+ Services.search.currentEngine = engine;
+ do_execute_soon(function () {
+ Services.search.removeEngine(engine);
+ });
+ break;
+ case "engine-removed":
+ let engineNameOutput = " for Test search engine";
+ expectedLog = expectedLog.map(logLine => logLine + engineNameOutput);
+ do_print("expectedLog:\n" + expectedLog.join("\n"))
+ do_print("gTestLog:\n" + gTestLog.join("\n"))
+ for (let i = 0; i < expectedLog.length; i++) {
+ do_check_eq(gTestLog[i], expectedLog[i]);
+ }
+ do_check_eq(gTestLog.length, expectedLog.length);
+ do_test_finished();
+ break;
+ }
+}
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ do_register_cleanup(function cleanup() {
+ Services.obs.removeObserver(search_observer, "browser-search-engine-modified");
+ });
+
+ do_test_pending();
+
+ Services.obs.addObserver(search_observer, "browser-search-engine-modified", false);
+
+ Services.search.addEngine(gDataUrl + "engine.xml", null, null, false);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
new file mode 100644
index 0000000000..d6e21fc61b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getAlternateDomains API.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_parseSubmissionURL() {
+ // Hide the default engines to prevent them from being used in the search.
+ for (let engine of Services.search.getEngines()) {
+ Services.search.removeEngine(engine);
+ }
+
+ let [engine1, engine2, engine3, engine4] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "Test search engine (fr)", xmlFileName: "engine-fr.xml" },
+ { name: "bacon_addParam", details: ["", "bacon_addParam", "Search Bacon",
+ "GET", "http://www.bacon.test/find"] },
+ { name: "idn_addParam", details: ["", "idn_addParam", "Search IDN",
+ "GET", "http://www.xn--bcher-kva.ch/search"] },
+ // The following engines cannot identify the search parameter.
+ { name: "A second test engine", xmlFileName: "engine2.xml" },
+ { name: "bacon", details: ["", "bacon", "Search Bacon", "GET",
+ "http://www.bacon.moz/search?q={searchTerms}"] },
+ ]);
+
+ engine3.addParam("q", "{searchTerms}", null);
+ engine4.addParam("q", "{searchTerms}", null);
+
+ // Test the first engine, whose URLs use UTF-8 encoding.
+ let url = "http://www.google.com/search?foo=bar&q=caff%C3%A8";
+ let result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+ do_check_eq(result.termsLength, "caff%C3%A8".length);
+
+ // The second engine uses a locale-specific domain that is an alternate domain
+ // of the first one, but the second engine should get priority when matching.
+ // The URL used with this engine uses ISO-8859-1 encoding instead.
+ url = "http://www.google.fr/search?q=caff%E8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine2);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%E8"));
+ do_check_eq(result.termsLength, "caff%E8".length);
+
+ // Test a domain that is an alternate domain of those defined. In this case,
+ // the first matching engine from the ordered list should be returned.
+ url = "http://www.google.co.uk/search?q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+ do_check_eq(result.termsLength, "caff%C3%A8".length);
+
+ // We support parsing URLs from a dynamically added engine. Those engines use
+ // windows-1252 encoding by default.
+ url = "http://www.bacon.test/find?q=caff%E8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine3);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%E8"));
+ do_check_eq(result.termsLength, "caff%E8".length);
+
+ // Test URLs with unescaped unicode characters.
+ url = "http://www.google.com/search?q=foo+b\u00E4r";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "foo b\u00E4r");
+ do_check_true(url.slice(result.termsOffset).startsWith("foo+b\u00E4r"));
+ do_check_eq(result.termsLength, "foo+b\u00E4r".length);
+
+ // Test search engines with unescaped IDNs.
+ url = "http://www.b\u00FCcher.ch/search?q=foo+bar";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine4);
+ do_check_eq(result.terms, "foo bar");
+ do_check_true(url.slice(result.termsOffset).startsWith("foo+bar"));
+ do_check_eq(result.termsLength, "foo+bar".length);
+
+ // Test search engines with escaped IDNs.
+ url = "http://www.xn--bcher-kva.ch/search?q=foo+bar";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine4);
+ do_check_eq(result.terms, "foo bar");
+ do_check_true(url.slice(result.termsOffset).startsWith("foo+bar"));
+ do_check_eq(result.termsLength, "foo+bar".length);
+
+ // Parsing of parameters from an engine template URL is not supported.
+ do_check_eq(Services.search.parseSubmissionURL(
+ "http://www.bacon.moz/search?q=").engine, null);
+ do_check_eq(Services.search.parseSubmissionURL(
+ "https://duckduckgo.com?q=test").engine, null);
+ do_check_eq(Services.search.parseSubmissionURL(
+ "https://duckduckgo.com/?q=test").engine, null);
+
+ // HTTP and HTTPS schemes are interchangeable.
+ url = "https://www.google.com/search?q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+
+ // Decoding search terms with multiple spaces should work.
+ result = Services.search.parseSubmissionURL(
+ "http://www.google.com/search?q=+with++spaces+");
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, " with spaces ");
+
+ // An empty query parameter should work the same.
+ url = "http://www.google.com/search?q=";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, url.length);
+
+ // There should be no match when the path is different.
+ result = Services.search.parseSubmissionURL(
+ "http://www.google.com/search/?q=test");
+ do_check_eq(result.engine, null);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, -1);
+
+ // There should be no match when the argument is different.
+ result = Services.search.parseSubmissionURL(
+ "http://www.google.com/search?q2=test");
+ do_check_eq(result.engine, null);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, -1);
+
+ // There should be no match for URIs that are not HTTP or HTTPS.
+ result = Services.search.parseSubmissionURL(
+ "file://localhost/search?q=test");
+ do_check_eq(result.engine, null);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, -1);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_pref.js b/toolkit/components/search/tests/xpcshell/test_pref.js
new file mode 100644
index 0000000000..f51ab4ee81
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_pref.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that MozParam condition="pref" values used in search URLs are from the
+ * default branch, and that their special characters are URL encoded. */
+
+"use strict";
+
+function run_test() {
+ updateAppInfo();
+
+ // The test engines used in this test need to be recognized as 'default'
+ // engines, or their MozParams will be ignored.
+ let url = "resource://test/data/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ run_next_test();
+}
+
+add_task(function* test_pref() {
+ let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ defaultBranch.setCharPref("param.code", "good&id=unique");
+ Services.prefs.setCharPref(BROWSER_SEARCH_PREF + "param.code", "bad");
+
+ yield asyncInit();
+
+ let engine = Services.search.getEngineByName("engine-pref");
+ let base = "http://www.google.com/search?q=foo&code=";
+ do_check_eq(engine.getSubmission("foo").uri.spec,
+ base + "good%26id%3Dunique");
+
+ do_test_finished();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_purpose.js b/toolkit/components/search/tests/xpcshell/test_purpose.js
new file mode 100644
index 0000000000..46465e0a3c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_purpose.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that a search purpose can be specified and that query parameters for
+ * that purpose are included in the search URL.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+
+ // The test engines used in this test need to be recognized as 'default'
+ // engines, or their MozParams used to set the purpose will be ignored.
+ let url = "resource://test/data/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ run_next_test();
+}
+
+add_task(function* test_purpose() {
+ let engine = Services.search.getEngineByName("Test search engine");
+
+ function check_submission(aExpected, aSearchTerm, aType, aPurpose) {
+ do_check_eq(engine.getSubmission(aSearchTerm, aType, aPurpose).uri.spec,
+ base + aExpected);
+ }
+
+ let base = "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
+ check_submission("", "foo");
+ check_submission("", "foo", null);
+ check_submission("", "foo", "text/html");
+ check_submission("&channel=rcs", "foo", null, "contextmenu");
+ check_submission("&channel=rcs", "foo", "text/html", "contextmenu");
+ check_submission("&channel=fflb", "foo", null, "keyword");
+ check_submission("&channel=fflb", "foo", "text/html", "keyword");
+ check_submission("", "foo", "text/html", "invalid");
+
+ // Tests for a param that varies with a purpose but has a default value.
+ base = "http://www.google.com/search?q=foo";
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose");
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", null);
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", "");
+ check_submission("&channel=rcs", "foo", "application/x-moz-default-purpose", "contextmenu");
+ check_submission("&channel=fflb", "foo", "application/x-moz-default-purpose", "keyword");
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", "searchbar");
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", "invalid");
+
+ // Tests for a purpose on the search form (ie. empty query).
+ engine = Services.search.getEngineByName("engine-rel-searchform-purpose");
+ base = "http://www.google.com/search?q=";
+ check_submission("&channel=sb", "", null, "searchbar");
+ check_submission("&channel=sb", "", "text/html", "searchbar");
+
+ // verify that the 'system' purpose falls back to the 'searchbar' purpose.
+ base = "http://www.google.com/search?q=foo";
+ check_submission("&channel=sb", "foo", "text/html", "system");
+ check_submission("&channel=sb", "foo", "text/html", "searchbar");
+ // Use an engine that actually defines the 'system' purpose...
+ engine = Services.search.getEngineByName("engine-system-purpose");
+ // ... and check that the system purpose is used correctly.
+ check_submission("&channel=sys", "foo", "text/html", "system");
+
+ do_test_finished();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_rel_searchform.js b/toolkit/components/search/tests/xpcshell/test_rel_searchform.js
new file mode 100644
index 0000000000..79f217e0d4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_rel_searchform.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that <Url rel="searchform"/> is properly recognized as a searchForm.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_rel_searchform() {
+ let engineNames = [
+ "engine-rel-searchform.xml",
+ "engine-rel-searchform-post.xml",
+ ];
+
+ // The final searchForm of the engine should be a URL whose domain is the
+ // <ShortName> in the engine's XML and that has a ?search parameter. The
+ // point of the ?search parameter is to avoid accidentally matching the value
+ // returned as a last resort by Engine's searchForm getter, which is simply
+ // the prePath of the engine's first HTML <Url>.
+ let items = engineNames.map(e => ({ name: e, xmlFileName: e }));
+ for (let engine of yield addTestEngines(items)) {
+ do_check_eq(engine.searchForm, "http://" + engine.name + "/?search");
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
new file mode 100644
index 0000000000..3a985db9e2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+function run_test() {
+ do_test_pending();
+
+ // Copy an engine to [profile]/searchplugin/
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ let file = dir.clone();
+ file.append("bug645970.xml");
+ do_check_true(file.exists());
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test the engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // remove the engine and verify the file has been removed too.
+ Services.search.removeEngine(engine);
+ do_check_false(file.exists());
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js b/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
new file mode 100644
index 0000000000..299121c4f4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ do_check_false(Services.search.isInitialized);
+
+ run_next_test();
+}
+
+add_task(function* ignore_cache_files_without_engines() {
+ let commitPromise = promiseAfterCache()
+ yield asyncInit();
+
+ let engineCount = Services.search.getEngines().length;
+ do_check_eq(engineCount, 1);
+
+ // Wait for the file to be saved to disk, so that we can mess with it.
+ yield commitPromise;
+
+ // Remove all engines from the cache file.
+ let cache = yield promiseCacheData();
+ cache.engines = [];
+ yield promiseSaveCacheData(cache);
+
+ // Check that after an async re-initialization, we still have the same engine count.
+ commitPromise = promiseAfterCache()
+ yield asyncReInit();
+ do_check_eq(engineCount, Services.search.getEngines().length);
+ yield commitPromise;
+
+ // Check that after a sync re-initialization, we still have the same engine count.
+ yield promiseSaveCacheData(cache);
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+ do_check_false(Services.search.isInitialized);
+ // Synchronously check the engine count; will force a sync init.
+ do_check_eq(engineCount, Services.search.getEngines().length);
+ do_check_true(Services.search.isInitialized);
+ yield reInitPromise;
+});
+
+add_task(function* skip_writing_cache_without_engines() {
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+
+ // Configure so that no engines will be found.
+ do_check_true(removeCacheFile());
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI("about:blank", null, null));
+
+ // Let the async-reInit happen.
+ yield reInitPromise;
+ do_check_eq(0, Services.search.getEngines().length);
+
+ // Trigger yet another re-init, to flush of any pending cache writing task.
+ unInitPromise = waitForSearchNotification("uninit-complete");
+ reInitPromise = asyncReInit();
+ yield unInitPromise;
+
+ // Now check that a cache file doesn't exist.
+ do_check_false(removeCacheFile());
+
+ yield reInitPromise;
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_resultDomain.js b/toolkit/components/search/tests/xpcshell/test_resultDomain.js
new file mode 100644
index 0000000000..d7458a923f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_resultDomain.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getResultDomain API.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_resultDomain() {
+ let [engine1, engine2, engine3] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml" },
+ { name: "bacon", details: ["", "bacon", "Search Bacon", "GET",
+ "http://www.bacon.moz/?search={searchTerms}"] },
+ ]);
+
+ do_check_eq(engine1.getResultDomain(), "google.com");
+ do_check_eq(engine1.getResultDomain("text/html"), "google.com");
+ do_check_eq(engine1.getResultDomain("application/x-moz-default-purpose"),
+ "purpose.google.com");
+ do_check_eq(engine1.getResultDomain("fake-response-type"), "");
+ do_check_eq(engine2.getResultDomain(), "duckduckgo.com");
+ do_check_eq(engine3.getResultDomain(), "bacon.moz");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js
new file mode 100644
index 0000000000..c509c5f777
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_save_sorted_engines: Start search engine
+ * - without search-metadata.json
+ * - without search.sqlite
+ *
+ * Ensure that search-metadata.json is correct after:
+ * - moving an engine
+ * - removing an engine
+ * - adding a new engine
+ *
+ * Notes:
+ * - we install the search engines of test "test_downloadAndAddEngines.js"
+ * to ensure that this test is independent from locale, commercial agreements
+ * and configuration of Firefox.
+ */
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_save_sorted_engines() {
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml"},
+ ]);
+ yield promiseAfterCache();
+
+ let search = Services.search;
+
+ // Test moving the engines
+ search.moveEngine(engine1, 0);
+ search.moveEngine(engine2, 1);
+
+ // Changes should be commited immediately
+ yield promiseAfterCache();
+ do_print("Commit complete after moveEngine");
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["test-search-engine"].order, 1);
+ do_check_eq(metadata["a-second-test-engine"].order, 2);
+
+ // Test removing an engine
+ search.removeEngine(engine1);
+ yield promiseAfterCache();
+ do_print("Commit complete after removeEngine");
+
+ // Check that the order of the remaining engine was updated correctly
+ metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["a-second-test-engine"].order, 1);
+
+ // Test adding a new engine
+ search.addEngineWithDetails("foo", "", "foo", "", "GET",
+ "http://searchget/?search={searchTerms}");
+ yield promiseAfterCache();
+ do_print("Commit complete after addEngineWithDetails");
+
+ metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["foo"].alias, "foo");
+ do_check_true(metadata["foo"].order > 0);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchReset.js b/toolkit/components/search/tests/xpcshell/test_searchReset.js
new file mode 100644
index 0000000000..316069c957
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchReset.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+const kTestEngineShortName = "engine";
+const kWhiteListPrefName = "reset.whitelist";
+
+function run_test() {
+ // Copy an engine to [profile]/searchplugin/
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine.xml").copyTo(dir, kTestEngineShortName + ".xml");
+
+ let file = dir.clone();
+ file.append(kTestEngineShortName + ".xml");
+ do_check_true(file.exists());
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .setBoolPref("reset.enabled", true);
+
+ run_next_test();
+}
+
+function* removeLoadPathHash() {
+ // Remove the loadPathHash and re-initialize the search service.
+ let cache = yield promiseCacheData();
+ for (let engine of cache.engines) {
+ if (engine._shortName == kTestEngineShortName) {
+ delete engine._metaData["loadPathHash"];
+ break;
+ }
+ }
+ yield promiseSaveCacheData(cache);
+ yield asyncReInit();
+}
+
+add_task(function* test_no_prompt_when_valid_loadPathHash() {
+ yield asyncInit();
+
+ // test the engine is loaded ok.
+ let engine = Services.search.getEngineByName(kTestEngineName);
+ do_check_neq(engine, null);
+
+ yield promiseAfterCache();
+
+ // The test engine has been found in the profile directory and imported,
+ // so it shouldn't have a loadPathHash.
+ let metadata = yield promiseEngineMetadata();
+ do_check_true(kTestEngineShortName in metadata);
+ do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
+
+ // After making it the currentEngine with the search service API,
+ // the test engine should have a valid loadPathHash.
+ Services.search.currentEngine = engine;
+ yield promiseAfterCache();
+ metadata = yield promiseEngineMetadata();
+ do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
+ let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
+ do_check_eq(typeof loadPathHash, "string");
+ do_check_eq(loadPathHash.length, 44);
+
+ // A search should not cause the search reset prompt.
+ let submission =
+ Services.search.currentEngine.getSubmission("foo", null, "searchbar");
+ do_check_eq(submission.uri.spec,
+ "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
+});
+
+add_task(function* test_promptURLs() {
+ yield removeLoadPathHash();
+
+ // The default should still be the test engine.
+ let currentEngine = Services.search.currentEngine;
+ do_check_eq(currentEngine.name, kTestEngineName);
+ // but the submission url should be about:searchreset
+ let url = (data, purpose) =>
+ currentEngine.getSubmission(data, null, purpose).uri.spec;
+ do_check_eq(url("foo", "searchbar"),
+ "about:searchreset?data=foo&purpose=searchbar");
+ do_check_eq(url("foo"), "about:searchreset?data=foo");
+ do_check_eq(url("", "searchbar"), "about:searchreset?purpose=searchbar");
+ do_check_eq(url(""), "about:searchreset");
+ do_check_eq(url("", ""), "about:searchreset");
+
+ // Calling the currentEngine setter for the same engine should
+ // prevent further prompts.
+ Services.search.currentEngine = Services.search.currentEngine;
+ do_check_eq(url("foo", "searchbar"),
+ "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
+
+ // And the loadPathHash should be back.
+ yield promiseAfterCache();
+ let metadata = yield promiseEngineMetadata();
+ do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
+ let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
+ do_check_eq(typeof loadPathHash, "string");
+ do_check_eq(loadPathHash.length, 44);
+});
+
+add_task(function* test_whitelist() {
+ yield removeLoadPathHash();
+
+ // The default should still be the test engine.
+ let currentEngine = Services.search.currentEngine;
+ do_check_eq(currentEngine.name, kTestEngineName);
+ let expectPrompt = shouldPrompt => {
+ let expectedURL =
+ shouldPrompt ? "about:searchreset?data=foo&purpose=searchbar"
+ : "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
+ let url = currentEngine.getSubmission("foo", null, "searchbar").uri.spec;
+ do_check_eq(url, expectedURL);
+ };
+ expectPrompt(true);
+
+ // Unless we whitelist our test engine.
+ let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let initialWhiteList = branch.getCharPref(kWhiteListPrefName);
+ branch.setCharPref(kWhiteListPrefName, "example.com,test.tld");
+ expectPrompt(true);
+ branch.setCharPref(kWhiteListPrefName, "www.google.com");
+ expectPrompt(false);
+ branch.setCharPref(kWhiteListPrefName, "example.com,www.google.com,test.tld");
+ expectPrompt(false);
+
+ // The loadPathHash should not be back after the prompt was skipped due to the
+ // whitelist.
+ yield asyncReInit();
+ let metadata = yield promiseEngineMetadata();
+ do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
+
+ branch.setCharPref(kWhiteListPrefName, initialWhiteList);
+ expectPrompt(true);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
new file mode 100644
index 0000000000..9de2967fc4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
@@ -0,0 +1,572 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Testing search suggestions from SearchSuggestionController.jsm.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/FormHistory.jsm");
+Cu.import("resource://gre/modules/SearchSuggestionController.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+// We must make sure the FormHistoryStartup component is
+// initialized in order for it to respond to FormHistory
+// requests from nsFormAutoComplete.js.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+ getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
+var httpServer = new HttpServer();
+var getEngine, postEngine, unresolvableEngine;
+
+function run_test() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+
+ removeMetadata();
+ updateAppInfo();
+
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+
+ do_register_cleanup(() => Task.spawn(function* cleanup() {
+ // Remove added form history entries
+ yield updateSearchHistory("remove", null);
+ FormHistory.shutdown();
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ }));
+
+ run_next_test();
+}
+
+add_task(function* add_test_engines() {
+ let getEngineData = {
+ baseURL: gDataUrl,
+ name: "GET suggestion engine",
+ method: "GET",
+ };
+
+ let postEngineData = {
+ baseURL: gDataUrl,
+ name: "POST suggestion engine",
+ method: "POST",
+ };
+
+ let unresolvableEngineData = {
+ baseURL: "http://example.invalid/",
+ name: "Offline suggestion engine",
+ method: "GET",
+ };
+
+ [getEngine, postEngine, unresolvableEngine] = yield addTestEngines([
+ {
+ name: getEngineData.name,
+ xmlFileName: "engineMaker.sjs?" + JSON.stringify(getEngineData),
+ },
+ {
+ name: postEngineData.name,
+ xmlFileName: "engineMaker.sjs?" + JSON.stringify(postEngineData),
+ },
+ {
+ name: unresolvableEngineData.name,
+ xmlFileName: "engineMaker.sjs?" + JSON.stringify(unresolvableEngineData),
+ },
+ ]);
+});
+
+
+// Begin tests
+
+add_task(function* simple_no_result_callback() {
+ let deferred = Promise.defer();
+ let controller = new SearchSuggestionController((result) => {
+ do_check_eq(result.term, "no remote");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+ deferred.resolve();
+ });
+
+ controller.fetch("no remote", false, getEngine);
+ yield deferred.promise;
+});
+
+add_task(function* simple_no_result_callback_and_promise() {
+ // Make sure both the callback and promise get results
+ let deferred = Promise.defer();
+ let controller = new SearchSuggestionController((result) => {
+ do_check_eq(result.term, "no results");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+ deferred.resolve();
+ });
+
+ let result = yield controller.fetch("no results", false, getEngine);
+ do_check_eq(result.term, "no results");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+
+ yield deferred.promise;
+});
+
+add_task(function* simple_no_result_promise() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("no remote", false, getEngine);
+ do_check_eq(result.term, "no remote");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* simple_remote_no_local_result() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 3);
+ do_check_eq(result.remote[0], "Mozilla");
+ do_check_eq(result.remote[1], "modern");
+ do_check_eq(result.remote[2], "mom");
+});
+
+add_task(function* remote_term_case_mismatch() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("Query Case Mismatch", false, getEngine);
+ do_check_eq(result.term, "Query Case Mismatch");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "Query Case Mismatch");
+});
+
+add_task(function* simple_local_no_remote_result() {
+ yield updateSearchHistory("bump", "no remote entries");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("no remote", false, getEngine);
+ do_check_eq(result.term, "no remote");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "no remote entries");
+ do_check_eq(result.remote.length, 0);
+
+ yield updateSearchHistory("remove", "no remote entries");
+});
+
+add_task(function* simple_non_ascii() {
+ yield updateSearchHistory("bump", "I ❤️ XUL");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("I ❤️", false, getEngine);
+ do_check_eq(result.term, "I ❤️");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "I ❤️ XUL");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "I ❤️ Mozilla");
+});
+
+add_task(function* both_local_remote_result_dedupe() {
+ yield updateSearchHistory("bump", "Mozilla");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Mozilla");
+ do_check_eq(result.remote.length, 2);
+ do_check_eq(result.remote[0], "modern");
+ do_check_eq(result.remote[1], "mom");
+});
+
+add_task(function* POST_both_local_remote_result_dedupe() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, postEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Mozilla");
+ do_check_eq(result.remote.length, 2);
+ do_check_eq(result.remote[0], "modern");
+ do_check_eq(result.remote[1], "mom");
+});
+
+add_task(function* both_local_remote_result_dedupe2() {
+ yield updateSearchHistory("bump", "mom");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 2);
+ do_check_eq(result.local[0], "mom");
+ do_check_eq(result.local[1], "Mozilla");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "modern");
+});
+
+add_task(function* both_local_remote_result_dedupe3() {
+ // All of the server entries also exist locally
+ yield updateSearchHistory("bump", "modern");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 3);
+ do_check_eq(result.local[0], "modern");
+ do_check_eq(result.local[1], "mom");
+ do_check_eq(result.local[2], "Mozilla");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* fetch_twice_in_a_row() {
+ // Two entries since the first will match the first fetch but not the second.
+ yield updateSearchHistory("bump", "delay local");
+ yield updateSearchHistory("bump", "delayed local");
+
+ let controller = new SearchSuggestionController();
+ let resultPromise1 = controller.fetch("delay", false, getEngine);
+
+ // A second fetch while the server is still waiting to return results leads to an abort.
+ let resultPromise2 = controller.fetch("delayed ", false, getEngine);
+ yield resultPromise1.then((results) => do_check_null(results));
+
+ let result = yield resultPromise2;
+ do_check_eq(result.term, "delayed ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "delayed local");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "delayed ");
+});
+
+add_task(function* fetch_twice_subset_reuse_formHistoryResult() {
+ // This tests if we mess up re-using the cached form history result.
+ // Two entries since the first will match the first fetch but not the second.
+ yield updateSearchHistory("bump", "delay local");
+ yield updateSearchHistory("bump", "delayed local");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("delay", false, getEngine);
+ do_check_eq(result.term, "delay");
+ do_check_eq(result.local.length, 2);
+ do_check_eq(result.local[0], "delay local");
+ do_check_eq(result.local[1], "delayed local");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "delay");
+
+ // Remove the entry from the DB but it should remain in the cached formHistoryResult.
+ yield updateSearchHistory("remove", "delayed local");
+
+ let result2 = yield controller.fetch("delayed ", false, getEngine);
+ do_check_eq(result2.term, "delayed ");
+ do_check_eq(result2.local.length, 1);
+ do_check_eq(result2.local[0], "delayed local");
+ do_check_eq(result2.remote.length, 1);
+ do_check_eq(result2.remote[0], "delayed ");
+});
+
+add_task(function* both_identical_with_more_than_max_results() {
+ // Add letters A through Z to form history which will match the server
+ for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) {
+ yield updateSearchHistory("bump", "letter " + String.fromCharCode(charCode));
+ }
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 7;
+ controller.maxRemoteResults = 10;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 7);
+ for (let i = 0; i < controller.maxLocalResults; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.local.length + result.remote.length, 10);
+ for (let i = 0; i < result.remote.length; i++) {
+ do_check_eq(result.remote[i],
+ "letter " + String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i));
+ }
+});
+
+add_task(function* noremote_maxLocal() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2; // (should be ignored because no remote results)
+ controller.maxRemoteResults = 0;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < result.local.length; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* someremote_maxLocal() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2;
+ controller.maxRemoteResults = 4;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 2);
+ for (let i = 0; i < result.local.length; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 2);
+ // "A" and "B" will have been de-duped, start at C for remote results
+ for (let i = 0; i < result.remote.length; i++) {
+ do_check_eq(result.remote[i], "letter " + String.fromCharCode("C".charCodeAt() + i));
+ }
+});
+
+add_task(function* one_of_each() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "letter A");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "letter B");
+});
+
+add_task(function* local_result_returned_remote_result_disabled() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(function* local_result_returned_remote_result_disabled_after_creation_of_controller() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(function* one_of_each_disabled_before_creation_enabled_after_creation_of_controller() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "letter A");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "letter B");
+});
+
+add_task(function* reset_suggestions_pref() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(function* one_local_zero_remote() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 0;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* zero_local_one_remote() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 1;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "letter A");
+});
+
+add_task(function* stop_search() {
+ let controller = new SearchSuggestionController((result) => {
+ do_throw("The callback shouldn't be called after stop()");
+ });
+ let resultPromise = controller.fetch("mo", false, getEngine);
+ controller.stop();
+ yield resultPromise.then((result) => {
+ do_check_null(result);
+ });
+});
+
+add_task(function* empty_searchTerm() {
+ // Empty searches don't go to the server but still get form history.
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("", false, getEngine);
+ do_check_eq(result.term, "");
+ do_check_true(result.local.length > 0);
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* slow_timeout() {
+ let d = Promise.defer();
+ function check_result(result) {
+ do_check_eq(result.term, "slow ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "slow local result");
+ do_check_eq(result.remote.length, 0);
+ }
+ yield updateSearchHistory("bump", "slow local result");
+
+ let controller = new SearchSuggestionController();
+ setTimeout(function check_timeout() {
+ // The HTTP response takes 10 seconds so check that we already have results after 2 seconds.
+ check_result(result);
+ d.resolve();
+ }, 2000);
+ let result = yield controller.fetch("slow ", false, getEngine);
+ check_result(result);
+ yield d.promise;
+});
+
+add_task(function* slow_stop() {
+ let d = Promise.defer();
+ let controller = new SearchSuggestionController();
+ let resultPromise = controller.fetch("slow ", false, getEngine);
+ setTimeout(function check_timeout() {
+ // The HTTP response takes 10 seconds but we timeout in less than a second so just use 0.
+ controller.stop();
+ d.resolve();
+ }, 0);
+ yield resultPromise.then((result) => {
+ do_check_null(result);
+ });
+
+ yield d.promise;
+});
+
+
+// Error handling
+
+add_task(function* remote_term_mismatch() {
+ yield updateSearchHistory("bump", "Query Mismatch Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("Query Mismatch", false, getEngine);
+ do_check_eq(result.term, "Query Mismatch");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Query Mismatch Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* http_404() {
+ yield updateSearchHistory("bump", "HTTP 404 Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("HTTP 404", false, getEngine);
+ do_check_eq(result.term, "HTTP 404");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "HTTP 404 Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* http_500() {
+ yield updateSearchHistory("bump", "HTTP 500 Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("HTTP 500", false, getEngine);
+ do_check_eq(result.term, "HTTP 500");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "HTTP 500 Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* unresolvable_server() {
+ yield updateSearchHistory("bump", "Unresolvable Server Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("Unresolvable Server", false, unresolvableEngine);
+ do_check_eq(result.term, "Unresolvable Server");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Unresolvable Server Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+
+// Exception handling
+
+add_task(function* missing_pb() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No privacy");
+ }, /priva/i);
+});
+
+add_task(function* missing_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No engine", false);
+ }, /engine/i);
+});
+
+add_task(function* invalid_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("invalid engine", false, {});
+ }, /engine/i);
+});
+
+add_task(function* no_results_requested() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 0;
+ controller.fetch("No results requested", false, getEngine);
+ }, /result/i);
+});
+
+add_task(function* minus_one_results_requested() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = -1;
+ controller.fetch("-1 results requested", false, getEngine);
+ }, /result/i);
+});
+
+add_task(function* test_userContextId() {
+ let controller = new SearchSuggestionController();
+ controller._fetchRemote = function(searchTerm, engine, privateMode, userContextId) {
+ Assert.equal(userContextId, 1);
+ return Promise.defer();
+ };
+
+ controller.fetch("test", false, getEngine, 1);
+});
+
+// Helpers
+
+function updateSearchHistory(operation, value) {
+ let deferred = Promise.defer();
+ FormHistory.update({
+ op: operation,
+ fieldname: "searchbar-history",
+ value: value,
+ },
+ {
+ handleError: function (error) {
+ do_throw("Error occurred updating form history: " + error);
+ deferred.reject(error);
+ },
+ handleCompletion: function (reason) {
+ if (!reason)
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_selectedEngine.js b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
new file mode 100644
index 0000000000..a1c0f363e4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+const kSelectedEnginePref = "browser.search.selectedEngine";
+
+// Check that the default engine matches the defaultenginename pref
+add_task(function* test_defaultEngine() {
+ yield asyncInit();
+
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName());
+});
+
+// Giving prefs a user value shouldn't change the selected engine.
+add_task(function* test_selectedEngine() {
+ let defaultEngineName = getDefaultEngineName();
+ // Test the selectedEngine pref.
+ Services.prefs.setCharPref(kSelectedEnginePref, kTestEngineName);
+
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, defaultEngineName);
+
+ Services.prefs.clearUserPref(kSelectedEnginePref);
+
+ // Test the defaultenginename pref.
+ Services.prefs.setCharPref(kDefaultenginenamePref, kTestEngineName);
+
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, defaultEngineName);
+
+ Services.prefs.clearUserPref(kDefaultenginenamePref);
+});
+
+// Setting the search engine should be persisted across restarts.
+add_task(function* test_persistAcrossRestarts() {
+ // Set the engine through the API.
+ Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Check that the a hash was saved.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata.hash.length, 44);
+
+ // Re-init and check the engine is still the same.
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+
+ // Cleanup (set the engine back to default).
+ Services.search.resetToOriginalDefaultEngine();
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName());
+});
+
+// An engine set without a valid hash should be ignored.
+add_task(function* test_ignoreInvalidHash() {
+ // Set the engine through the API.
+ Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Then mess with the file (make the hash invalid).
+ let metadata = yield promiseGlobalMetadata();
+ metadata.hash = "invalid";
+ yield promiseSaveGlobalMetadata(metadata);
+
+ // Re-init the search service, and check that the json file is ignored.
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName());
+});
+
+// Resetting the engine to the default should remove the saved value.
+add_task(function* test_settingToDefault() {
+ // Set the engine through the API.
+ Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Check that the current engine was saved.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata.current, kTestEngineName);
+
+ // Then set the engine back to the default through the API.
+ Services.search.currentEngine =
+ Services.search.getEngineByName(getDefaultEngineName());
+ yield promiseAfterCache();
+
+ // Check that the current engine is no longer saved in the JSON file.
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata.current, "");
+});
+
+add_task(function* test_resetToOriginalDefaultEngine() {
+ let defaultName = getDefaultEngineName();
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+
+ Services.search.currentEngine =
+ Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ Services.search.resetToOriginalDefaultEngine();
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+ yield promiseAfterCache();
+});
+
+add_task(function* test_fallback_kept_after_restart() {
+ // Set current engine to a default engine that isn't the original default.
+ let builtInEngines = Services.search.getDefaultEngines();
+ let defaultName = getDefaultEngineName();
+ let nonDefaultBuiltInEngine;
+ for (let engine of builtInEngines) {
+ if (engine.name != defaultName) {
+ nonDefaultBuiltInEngine = engine;
+ break;
+ }
+ }
+ Services.search.currentEngine = nonDefaultBuiltInEngine;
+ do_check_eq(Services.search.currentEngine.name, nonDefaultBuiltInEngine.name);
+ yield promiseAfterCache();
+
+ // Remove that engine...
+ Services.search.removeEngine(nonDefaultBuiltInEngine);
+ // The engine being a default (built-in) one, it should be hidden
+ // rather than actually removed.
+ do_check_true(nonDefaultBuiltInEngine.hidden);
+
+ // Using the currentEngine getter should force a fallback to the
+ // original default engine.
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+
+ // Restoring the default engines should unhide our built-in test
+ // engine, but not change the value of currentEngine.
+ Services.search.restoreDefaultEngines();
+ do_check_false(nonDefaultBuiltInEngine.hidden);
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+ yield promiseAfterCache();
+
+ // After a restart, the currentEngine value should still be unchanged.
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+});
+
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine.xml");
+ let engineDir = engineDummyFile.parent;
+ engineDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
+
+ do_register_cleanup(function() {
+ removeMetadata();
+ removeCacheFile();
+ });
+
+ run_next_test();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_svg_icon.js b/toolkit/components/search/tests/xpcshell/test_svg_icon.js
new file mode 100644
index 0000000000..5fd4781a1a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_svg_icon.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var url;
+var requestHandled;
+
+const icon =
+ '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' +
+ '<svg xmlns="http://www.w3.org/2000/svg" ' +
+ 'width="16" height="16" viewBox="0 0 16 16">' +
+ '<rect x="4" y="4" width="8px" height="8px" style="fill: blue"/>' +
+ '</svg>';
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer(); // Unused, but required to call addTestEngines.
+
+ requestHandled = new Promise(resolve => {
+ let srv = new HttpServer();
+ srv.registerPathHandler("/icon.svg", (metadata, response) => {
+ response.setStatusLine("1.0", 200, "OK");
+ response.setHeader("Content-Type", "image/svg+xml", false);
+
+ response.write(icon);
+ resolve();
+ });
+ srv.start(-1);
+ do_register_cleanup(() => srv.stop(() => {}));
+
+ url = "http://localhost:" + srv.identity.primaryPort + "/icon.svg";
+ });
+
+ run_next_test();
+}
+
+add_task(function* test_svg_icon() {
+ yield asyncInit();
+
+ let [engine] = yield addTestEngines([
+ { name: "SVGIcon", details: [url, "", "SVG icon", "GET",
+ "http://icon.svg/search?q={searchTerms}"] },
+ ]);
+
+ yield requestHandled;
+ yield promiseAfterCache();
+
+ ok(engine.iconURI, "the engine has an icon");
+ ok(engine.iconURI.spec.startsWith("data:image/svg+xml"),
+ "the icon is saved as an SVG data url");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sync.js b/toolkit/components/search/tests/xpcshell/test_sync.js
new file mode 100644
index 0000000000..8f4eb22ee9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // Check the hidden engine is not loaded.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_eq(engine, null);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_addon.js b/toolkit/components/search/tests/xpcshell/test_sync_addon.js
new file mode 100644
index 0000000000..7af9575066
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_addon.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ // test the add-on engine is loaded in addition to our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 2);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("addon");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "addon");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js b/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js
new file mode 100644
index 0000000000..3f4494905b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine("engine-override");
+
+ do_check_false(Services.search.isInitialized);
+
+ // test the add-on engine isn't overriding our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js b/toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js
new file mode 100644
index 0000000000..1b41a71bf0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+ let fallback = false;
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(fallback);
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_test_finished();
+ });
+
+ // Execute test for the sync fallback while the async code is being executed.
+ Services.obs.addObserver(function searchServiceObserver(aResult, aTopic, aVerb) {
+ if (aVerb == "find-jar-engines") {
+ Services.obs.removeObserver(searchServiceObserver, aTopic);
+ fallback = true;
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_true(Services.search.isInitialized);
+ }
+ }, "browser-search-service", false);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_distribution.js b/toolkit/components/search/tests/xpcshell/test_sync_distribution.js
new file mode 100644
index 0000000000..63a8b66f0e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_distribution.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installDistributionEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ // test that the engine from the distribution overrides our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // check the engine we have is actually the one from the distribution
+ do_check_eq(engine.description, "override");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_fallback.js b/toolkit/components/search/tests/xpcshell/test_sync_fallback.js
new file mode 100644
index 0000000000..dad73fabc6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_fallback.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_test_finished();
+ });
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_migration.js b/toolkit/components/search/tests/xpcshell/test_sync_migration.js
new file mode 100644
index 0000000000..53e945dfd8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_migration.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that legacy metadata from search-metadata.json is correctly
+ * transferred to the new metadata storage. */
+
+function run_test() {
+ updateAppInfo();
+ installTestEngine();
+
+ do_get_file("data/metadata.json").copyTo(gProfD, "search-metadata.json");
+
+ run_next_test();
+}
+
+add_task(function* test_sync_metadata_migration() {
+ do_check_false(Services.search.isInitialized);
+ Services.search.getEngines();
+ do_check_true(Services.search.isInitialized);
+ yield promiseAfterCache();
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["engine"].order, 1);
+ do_check_eq(metadata["engine"].alias, "foo");
+
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata["searchDefaultExpir"], 1471013469846);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js
new file mode 100644
index 0000000000..f8d38e5719
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Copy an engine in [profile]/searchplugin/ and ensure it's not
+ // overriding the same file from a jar.
+ // The description in the file we are copying is 'profile'.
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_update_telemetry.js b/toolkit/components/search/tests/xpcshell/test_update_telemetry.js
new file mode 100644
index 0000000000..f73e765c67
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_update_telemetry.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_check_false(Services.search.isInitialized);
+
+ useHttpServer();
+ run_next_test();
+}
+
+function checkTelemetry(histogramName, expected) {
+ let histogram = Services.telemetry.getHistogramById(histogramName);
+ let snapshot = histogram.snapshot();
+ let expectedCounts = [0, 0, 0];
+ expectedCounts[expected ? 1 : 0] = 1;
+ Assert.deepEqual(snapshot.counts, expectedCounts,
+ "histogram has expected content");
+ histogram.clear();
+}
+
+add_task(function* ignore_cache_files_without_engines() {
+ yield asyncInit();
+
+ checkTelemetry("SEARCH_SERVICE_HAS_UPDATES", false);
+ checkTelemetry("SEARCH_SERVICE_HAS_ICON_UPDATES", false);
+
+ // Add an engine with update urls and re-init, as we record the presence of
+ // engine update urls only while initializing the search service.
+ yield addTestEngines([
+ { name: "update", xmlFileName: "engine-update.xml" },
+ ]);
+ yield asyncReInit();
+
+ checkTelemetry("SEARCH_SERVICE_HAS_UPDATES", true);
+ checkTelemetry("SEARCH_SERVICE_HAS_ICON_UPDATES", true);
+});
diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.ini b/toolkit/components/search/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..1fb5a3423e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,102 @@
+[DEFAULT]
+head = head_search.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ data/chrome.manifest
+ data/engine.xml
+ data/engine2.xml
+ data/engine-addon.xml
+ data/engine-override.xml
+ data/engine-app.xml
+ data/engine-fr.xml
+ data/engineMaker.sjs
+ data/engine-pref.xml
+ data/engine-rel-searchform.xml
+ data/engine-rel-searchform-post.xml
+ data/engine-rel-searchform-purpose.xml
+ data/engine-system-purpose.xml
+ data/engine-update.xml
+ data/engineImages.xml
+ data/engine-chromeicon.xml
+ data/engine-resourceicon.xml
+ data/ico-size-16x16-png.ico
+ data/invalid-engine.xml
+ data/install.rdf
+ data/list.json
+ data/langpack-metadata.json
+ data/metadata.json
+ data/search.json
+ data/search.sqlite
+ data/searchSuggestions.sjs
+ data/searchTest.jar
+
+[test_nocache.js]
+[test_645970.js]
+[test_bug930456.js]
+[test_bug930456_child.js]
+[test_engine_set_alias.js]
+[test_hasEngineWithURL.js]
+[test_identifiers.js]
+[test_invalid_engine_from_dir.js]
+[test_init_async_multiple.js]
+[test_init_async_multiple_then_sync.js]
+[test_json_cache.js]
+[test_location.js]
+[test_location_error.js]
+[test_location_malformed_json.js]
+[test_location_migrate_countrycode_isUS.js]
+[test_location_migrate_no_countrycode_isUS.js]
+[test_location_migrate_no_countrycode_notUS.js]
+[test_location_partner.js]
+[test_location_funnelcake.js]
+[test_location_sync.js]
+[test_location_timeout.js]
+[test_location_timeout_xhr.js]
+[test_nodb.js]
+[test_nodb_pluschanges.js]
+[test_save_sorted_engines.js]
+[test_pref.js]
+[test_purpose.js]
+[test_defaultEngine.js]
+[test_notifications.js]
+[test_parseSubmissionURL.js]
+[test_SearchStaticData.js]
+[test_addEngine_callback.js]
+[test_migration_langpack.js]
+[test_multipleIcons.js]
+[test_resultDomain.js]
+[test_searchSuggest.js]
+[test_async.js]
+[test_async_addon.js]
+tags = addons
+[test_async_addon_no_override.js]
+tags = addons
+[test_async_distribution.js]
+[test_async_migration.js]
+[test_async_profile_engine.js]
+[test_sync.js]
+[test_sync_addon.js]
+tags = addons
+[test_sync_addon_no_override.js]
+tags = addons
+[test_sync_distribution.js]
+[test_sync_fallback.js]
+[test_sync_delay_fallback.js]
+[test_sync_migration.js]
+[test_sync_profile_engine.js]
+[test_rel_searchform.js]
+[test_remove_profile_engine.js]
+[test_selectedEngine.js]
+[test_geodefaults.js]
+[test_hidden.js]
+[test_currentEngine_fallback.js]
+[test_require_engines_in_cache.js]
+[test_update_telemetry.js]
+[test_svg_icon.js]
+[test_searchReset.js]
+[test_addEngineWithDetails.js]
+[test_chromeresource_icon1.js]
+[test_chromeresource_icon2.js]
+[test_engineUpdate.js]
diff --git a/toolkit/components/search/toolkitsearch.manifest b/toolkit/components/search/toolkitsearch.manifest
new file mode 100644
index 0000000000..b7c55da0e9
--- /dev/null
+++ b/toolkit/components/search/toolkitsearch.manifest
@@ -0,0 +1,10 @@
+component {7319788a-fe93-4db3-9f39-818cf08f4256} nsSearchService.js process=main
+contract @mozilla.org/browser/search-service;1 {7319788a-fe93-4db3-9f39-818cf08f4256} process=main
+# 21600 == 6 hours
+category update-timer nsSearchService @mozilla.org/browser/search-service;1,getService,search-engine-update-timer,browser.search.update.interval,21600
+component {aa892eb4-ffbf-477d-9f9a-06c995ae9f27} nsSearchSuggestions.js
+contract @mozilla.org/autocomplete/search;1?name=search-autocomplete {aa892eb4-ffbf-477d-9f9a-06c995ae9f27}
+#ifdef HAVE_SIDEBAR
+component {22117140-9c6e-11d3-aaf1-00805f8a4905} nsSidebar.js
+contract @mozilla.org/sidebar;1 {22117140-9c6e-11d3-aaf1-00805f8a4905}
+#endif
diff --git a/toolkit/components/securityreporter/SecurityReporter.js b/toolkit/components/securityreporter/SecurityReporter.js
new file mode 100644
index 0000000000..9ca1e55466
--- /dev/null
+++ b/toolkit/components/securityreporter/SecurityReporter.js
@@ -0,0 +1,112 @@
+/* -*- 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 { classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.importGlobalProperties(['fetch']);
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const protocolHandler = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler);
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const TLS_ERROR_REPORT_TELEMETRY_SUCCESS = 6;
+const TLS_ERROR_REPORT_TELEMETRY_FAILURE = 7;
+const HISTOGRAM_ID = "TLS_ERROR_REPORT_UI";
+
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+function getDERString(cert)
+{
+ var length = {};
+ var derArray = cert.getRawDER(length);
+ var derString = '';
+ for (var i = 0; i < derArray.length; i++) {
+ derString += String.fromCharCode(derArray[i]);
+ }
+ return derString;
+}
+
+function SecurityReporter() { }
+
+SecurityReporter.prototype = {
+ classDescription: "Security reporter component",
+ classID: Components.ID("{8a997c9a-bea1-11e5-a1fa-be6aBc8e7f8b}"),
+ contractID: "@mozilla.org/securityreporter;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISecurityReporter]),
+ reportTLSError: function(transportSecurityInfo, hostname, port) {
+ // don't send if there's no transportSecurityInfo (since the report cannot
+ // contain anything of interest)
+ if (!transportSecurityInfo) {
+ return;
+ }
+
+ // don't send a report if the pref is not enabled
+ if (!Services.prefs.getBoolPref("security.ssl.errorReporting.enabled")) {
+ return;
+ }
+
+ // Don't send a report if the host we're connecting to is the report
+ // server (otherwise we'll get loops when this fails)
+ let endpoint =
+ Services.prefs.getCharPref("security.ssl.errorReporting.url");
+ let reportURI = Services.io.newURI(endpoint, null, null);
+
+ if (reportURI.host == hostname) {
+ return;
+ }
+
+ // Convert the nsIX509CertList into a format that can be parsed into
+ // JSON
+ let asciiCertChain = [];
+
+ if (transportSecurityInfo.failedCertChain) {
+ let certs = transportSecurityInfo.failedCertChain.getEnumerator();
+ while (certs.hasMoreElements()) {
+ let cert = certs.getNext();
+ cert.QueryInterface(Ci.nsIX509Cert);
+ asciiCertChain.push(btoa(getDERString(cert)));
+ }
+ }
+
+ let report = {
+ hostname: hostname,
+ port: port,
+ timestamp: Math.round(Date.now() / 1000),
+ errorCode: transportSecurityInfo.errorCode,
+ failedCertChain: asciiCertChain,
+ userAgent: protocolHandler.userAgent,
+ version: 1,
+ build: Services.appinfo.appBuildID,
+ product: Services.appinfo.name,
+ channel: UpdateUtils.UpdateChannel
+ }
+
+ fetch(endpoint, {
+ method: "POST",
+ body: JSON.stringify(report),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then(function (aResponse) {
+ if (!aResponse.ok) {
+ // request returned non-success status
+ Services.telemetry.getHistogramById(HISTOGRAM_ID)
+ .add(TLS_ERROR_REPORT_TELEMETRY_FAILURE);
+ } else {
+ Services.telemetry.getHistogramById(HISTOGRAM_ID)
+ .add(TLS_ERROR_REPORT_TELEMETRY_SUCCESS);
+ }
+ }).catch(function (e) {
+ // error making request to reportURL
+ Services.telemetry.getHistogramById(HISTOGRAM_ID)
+ .add(TLS_ERROR_REPORT_TELEMETRY_FAILURE);
+ });
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SecurityReporter]);
diff --git a/toolkit/components/securityreporter/SecurityReporter.manifest b/toolkit/components/securityreporter/SecurityReporter.manifest
new file mode 100644
index 0000000000..d4e080dc7c
--- /dev/null
+++ b/toolkit/components/securityreporter/SecurityReporter.manifest
@@ -0,0 +1,2 @@
+component {8a997c9a-bea1-11e5-a1fa-be6aBc8e7f8b} SecurityReporter.js
+contract @mozilla.org/securityreporter;1 {8a997c9a-bea1-11e5-a1fa-be6aBc8e7f8b}
diff --git a/toolkit/components/securityreporter/moz.build b/toolkit/components/securityreporter/moz.build
new file mode 100644
index 0000000000..7ef56a115e
--- /dev/null
+++ b/toolkit/components/securityreporter/moz.build
@@ -0,0 +1,16 @@
+# -*- 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_MODULE = 'toolkit_securityreporter'
+
+XPIDL_SOURCES += [
+ 'nsISecurityReporter.idl',
+]
+
+EXTRA_COMPONENTS += [
+ 'SecurityReporter.js',
+ 'SecurityReporter.manifest',
+]
diff --git a/toolkit/components/securityreporter/nsISecurityReporter.idl b/toolkit/components/securityreporter/nsISecurityReporter.idl
new file mode 100644
index 0000000000..462dd1e480
--- /dev/null
+++ b/toolkit/components/securityreporter/nsISecurityReporter.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+#include "nsITransportSecurityInfo.idl"
+
+[scriptable, uuid(8a997c9a-bea1-11e5-a1fa-be6aBc8e7f8b)]
+interface nsISecurityReporter : nsISupports
+{
+ void reportTLSError(in nsITransportSecurityInfo aSecurityInfo,
+ in AUTF8String aHostname,
+ in long aPort);
+};
diff --git a/toolkit/components/social/test/xpcshell/.eslintrc.js b/toolkit/components/social/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/social/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/sqlite/moz.build b/toolkit/components/sqlite/moz.build
new file mode 100644
index 0000000000..bbe5b8b969
--- /dev/null
+++ b/toolkit/components/sqlite/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+EXTRA_JS_MODULES.sqlite += [
+ 'sqlite_internal.js',
+]
diff --git a/toolkit/components/sqlite/sqlite_internal.js b/toolkit/components/sqlite/sqlite_internal.js
new file mode 100644
index 0000000000..18b07ff50d
--- /dev/null
+++ b/toolkit/components/sqlite/sqlite_internal.js
@@ -0,0 +1,323 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 file defines an Sqlite object containing js-ctypes bindings for
+ * sqlite3. It should be included from a worker thread using require.
+ *
+ * It serves the following purposes:
+ * - opens libxul;
+ * - defines sqlite3 API functions;
+ * - defines the necessary sqlite3 types.
+ */
+
+"use strict";
+
+importScripts("resource://gre/modules/workers/require.js");
+
+var SharedAll = require(
+ "resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+
+// Open the sqlite3 library.
+var path;
+if (SharedAll.Constants.Sys.Name === "Android") {
+ path = ctypes.libraryName("sqlite3");
+} else if (SharedAll.Constants.Win) {
+ path = ctypes.libraryName("nss3");
+} else {
+ path = SharedAll.Constants.Path.libxul;
+}
+
+var lib;
+try {
+ lib = ctypes.open(path);
+} catch (ex) {
+ throw new Error("Could not open system library: " + ex.message);
+}
+
+var declareLazyFFI = SharedAll.declareLazyFFI;
+
+var Type = Object.create(SharedAll.Type);
+
+/**
+ * Opaque Structure |sqlite3_ptr|.
+ * |sqlite3_ptr| is equivalent to a void*.
+ */
+Type.sqlite3_ptr = Type.voidptr_t.withName("sqlite3_ptr");
+
+/**
+ * |sqlite3_stmt_ptr| an instance of an object representing a single SQL
+ * statement.
+ * |sqlite3_stmt_ptr| is equivalent to a void*.
+ */
+Type.sqlite3_stmt_ptr = Type.voidptr_t.withName("sqlite3_stmt_ptr");
+
+/**
+ * |sqlite3_destructor_ptr| a constant defining a special destructor behaviour.
+ * |sqlite3_destructor_ptr| is equivalent to a void*.
+ */
+Type.sqlite3_destructor_ptr = Type.voidptr_t.withName(
+ "sqlite3_destructor_ptr");
+
+/**
+ * A C double.
+ */
+Type.double = new SharedAll.Type("double", ctypes.double);
+
+/**
+ * |sqlite3_int64| typedef for 64-bit integer.
+ */
+Type.sqlite3_int64 = Type.int64_t.withName("sqlite3_int64");
+
+/**
+ * Sqlite3 constants.
+ */
+var Constants = {};
+
+/**
+ * |SQLITE_STATIC| a special value for the destructor that is passed as an
+ * argument to routines like bind_blob, bind_text and bind_text16. It means that
+ * the content pointer is constant and will never change and does need to be
+ * destroyed.
+ */
+Constants.SQLITE_STATIC = Type.sqlite3_destructor_ptr.implementation(0);
+
+/**
+ * |SQLITE_TRANSIENT| a special value for the destructor that is passed as an
+ * argument to routines like bind_blob, bind_text and bind_text16. It means that
+ * the content will likely change in the near future and that SQLite should make
+ * its own private copy of the content before returning.
+ */
+Constants.SQLITE_TRANSIENT = Type.sqlite3_destructor_ptr.implementation(-1);
+
+/**
+ * |SQLITE_OK|
+ * Successful result.
+ */
+Constants.SQLITE_OK = 0;
+
+/**
+ * |SQLITE_ROW|
+ * sqlite3_step() has another row ready.
+ */
+Constants.SQLITE_ROW = 100;
+
+/**
+ * |SQLITE_DONE|
+ * sqlite3_step() has finished executing.
+ */
+Constants.SQLITE_DONE = 101;
+
+var Sqlite3 = {
+ Constants: Constants,
+ Type: Type
+};
+
+declareLazyFFI(Sqlite3, "open", lib, "sqlite3_open", null,
+ /* return*/ Type.int,
+ /* path*/ Type.char.in_ptr,
+ /* db handle*/ Type.sqlite3_ptr.out_ptr);
+
+declareLazyFFI(Sqlite3, "open_v2", lib, "sqlite3_open_v2", null,
+ /* return*/ Type.int,
+ /* path*/ Type.char.in_ptr,
+ /* db handle*/ Type.sqlite3_ptr.out_ptr,
+ /* flags*/ Type.int,
+ /* VFS*/ Type.char.in_ptr);
+
+declareLazyFFI(Sqlite3, "close", lib, "sqlite3_close", null,
+ /* return*/ Type.int,
+ /* db handle*/ Type.sqlite3_ptr);
+
+declareLazyFFI(Sqlite3, "prepare_v2", lib, "sqlite3_prepare_v2", null,
+ /* return*/ Type.int,
+ /* db handle*/ Type.sqlite3_ptr,
+ /* zSql*/ Type.char.in_ptr,
+ /* nByte*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr.out_ptr,
+ /* unused*/ Type.cstring.out_ptr);
+
+declareLazyFFI(Sqlite3, "step", lib, "sqlite3_step", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr);
+
+declareLazyFFI(Sqlite3, "finalize", lib, "sqlite3_finalize", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr);
+
+declareLazyFFI(Sqlite3, "reset", lib, "sqlite3_reset", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr);
+
+declareLazyFFI(Sqlite3, "column_int", lib, "sqlite3_column_int", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_blob", lib, "sqlite3_column_blob", null,
+ /* return*/ Type.voidptr_t,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_bytes", lib, "sqlite3_column_bytes", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_bytes16", lib, "sqlite3_column_bytes16",
+ null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_double", lib, "sqlite3_column_double", null,
+ /* return*/ Type.double,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_int64", lib, "sqlite3_column_int64", null,
+ /* return*/ Type.sqlite3_int64,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_text", lib, "sqlite3_column_text", null,
+ /* return*/ Type.cstring,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "column_text16", lib, "sqlite3_column_text16", null,
+ /* return*/ Type.wstring,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* col*/ Type.int);
+
+declareLazyFFI(Sqlite3, "bind_int", lib, "sqlite3_bind_int", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* value*/ Type.int);
+
+declareLazyFFI(Sqlite3, "bind_int64", lib, "sqlite3_bind_int64", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* value*/ Type.sqlite3_int64);
+
+declareLazyFFI(Sqlite3, "bind_double", lib, "sqlite3_bind_double", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* value*/ Type.double);
+
+declareLazyFFI(Sqlite3, "bind_null", lib, "sqlite3_bind_null", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int);
+
+declareLazyFFI(Sqlite3, "bind_zeroblob", lib, "sqlite3_bind_zeroblob", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* nBytes*/ Type.int);
+
+declareLazyFFI(Sqlite3, "bind_text", lib, "sqlite3_bind_text", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* value*/ Type.cstring,
+ /* nBytes*/ Type.int,
+ /* destructor*/ Type.sqlite3_destructor_ptr);
+
+declareLazyFFI(Sqlite3, "bind_text16", lib, "sqlite3_bind_text16", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* value*/ Type.wstring,
+ /* nBytes*/ Type.int,
+ /* destructor*/ Type.sqlite3_destructor_ptr);
+
+declareLazyFFI(Sqlite3, "bind_blob", lib, "sqlite3_bind_blob", null,
+ /* return*/ Type.int,
+ /* statement*/ Type.sqlite3_stmt_ptr,
+ /* index*/ Type.int,
+ /* value*/ Type.voidptr_t,
+ /* nBytes*/ Type.int,
+ /* destructor*/ Type.sqlite3_destructor_ptr);
+
+module.exports = {
+ get Constants() {
+ return Sqlite3.Constants;
+ },
+ get Type() {
+ return Sqlite3.Type;
+ },
+ get open() {
+ return Sqlite3.open;
+ },
+ get open_v2() {
+ return Sqlite3.open_v2;
+ },
+ get close() {
+ return Sqlite3.close;
+ },
+ get prepare_v2() {
+ return Sqlite3.prepare_v2;
+ },
+ get step() {
+ return Sqlite3.step;
+ },
+ get finalize() {
+ return Sqlite3.finalize;
+ },
+ get reset() {
+ return Sqlite3.reset;
+ },
+ get column_int() {
+ return Sqlite3.column_int;
+ },
+ get column_blob() {
+ return Sqlite3.column_blob;
+ },
+ get column_bytes() {
+ return Sqlite3.column_bytes;
+ },
+ get column_bytes16() {
+ return Sqlite3.column_bytes16;
+ },
+ get column_double() {
+ return Sqlite3.column_double;
+ },
+ get column_int64() {
+ return Sqlite3.column_int64;
+ },
+ get column_text() {
+ return Sqlite3.column_text;
+ },
+ get column_text16() {
+ return Sqlite3.column_text16;
+ },
+ get bind_int() {
+ return Sqlite3.bind_int;
+ },
+ get bind_int64() {
+ return Sqlite3.bind_int64;
+ },
+ get bind_double() {
+ return Sqlite3.bind_double;
+ },
+ get bind_null() {
+ return Sqlite3.bind_null;
+ },
+ get bind_zeroblob() {
+ return Sqlite3.bind_zeroblob;
+ },
+ get bind_text() {
+ return Sqlite3.bind_text;
+ },
+ get bind_text16() {
+ return Sqlite3.bind_text16;
+ },
+ get bind_blob() {
+ return Sqlite3.bind_blob;
+ }
+};
diff --git a/toolkit/components/sqlite/tests/xpcshell/.eslintrc.js b/toolkit/components/sqlite/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/sqlite/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/sqlite/tests/xpcshell/data/chrome.manifest b/toolkit/components/sqlite/tests/xpcshell/data/chrome.manifest
new file mode 100644
index 0000000000..92b9cf60bb
--- /dev/null
+++ b/toolkit/components/sqlite/tests/xpcshell/data/chrome.manifest
@@ -0,0 +1 @@
+content test_sqlite_internal ./
diff --git a/toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_internal.js b/toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_internal.js
new file mode 100644
index 0000000000..7f0b3af03d
--- /dev/null
+++ b/toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_internal.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts("worker_sqlite_shared.js",
+ "resource://gre/modules/workers/require.js");
+
+self.onmessage = function onmessage(msg) {
+ try {
+ run_test();
+ } catch (ex) {
+ let {message, moduleStack, moduleName, lineNumber} = ex;
+ let error = new Error(message, moduleName, lineNumber);
+ error.stack = moduleStack;
+ dump("Uncaught error: " + error + "\n");
+ dump("Full stack: " + moduleStack + "\n");
+ throw error;
+ }
+};
+
+var Sqlite;
+
+var SQLITE_OK; /* Successful result */
+var SQLITE_ROW; /* sqlite3_step() has another row ready */
+var SQLITE_DONE; /* sqlite3_step() has finished executing */
+
+function test_init() {
+ do_print("Starting test_init");
+ // Sqlite should be loaded.
+ Sqlite = require("resource://gre/modules/sqlite/sqlite_internal.js");
+ do_check_neq(typeof Sqlite, "undefined");
+ do_check_neq(typeof Sqlite.Constants, "undefined");
+ SQLITE_OK = Sqlite.Constants.SQLITE_OK;
+ SQLITE_ROW = Sqlite.Constants.SQLITE_ROW;
+ SQLITE_DONE = Sqlite.Constants.SQLITE_DONE;
+}
+
+/**
+ * Clean up the database.
+ * @param {sqlite3_ptr} db A pointer to the database.
+ */
+function cleanupDB(db) {
+ withQuery(db, "DROP TABLE IF EXISTS TEST;", SQLITE_DONE);
+}
+
+/**
+ * Open and close sqlite3 database.
+ * @param {String} open A name of the sqlite3 open function to be
+ * used.
+ * @param {Array} openArgs = [] Optional arguments to open function.
+ * @param {Function} callback = null An optional callback to be run after the
+ * database is opened but before it is
+ * closed.
+ */
+function withDB(open, openArgs = [], callback = null) {
+ let db = Sqlite.Type.sqlite3_ptr.implementation();
+ let dbPtr = db.address();
+
+ // Open database.
+ let result = Sqlite[open].apply(Sqlite, ["data/test.db", dbPtr].concat(
+ openArgs));
+ do_check_eq(result, SQLITE_OK);
+
+ // Drop the test table if it already exists.
+ cleanupDB(db);
+
+ try {
+ if (callback) {
+ callback(db);
+ }
+ } catch (ex) {
+ do_check_true(false);
+ throw ex;
+ } finally {
+ // Drop the test table if it still exists.
+ cleanupDB(db);
+ // Close data base.
+ result = Sqlite.close(db);
+ do_check_eq(result, SQLITE_OK);
+ }
+}
+
+/**
+ * Execute an SQL query using sqlite3 API.
+ * @param {sqlite3_ptr} db A pointer to the database.
+ * @param {String} sql A SQL query string.
+ * @param {Number} stepResult Expected result code after evaluating the
+ * SQL statement.
+ * @param {Function} bind An optional callback with SQL binding steps.
+ * @param {Function} callback An optional callback that runs after the SQL
+ * query completes.
+ */
+function withQuery(db, sql, stepResult, bind, callback) {
+ // Create an instance of a single SQL statement.
+ let sqlStmt = Sqlite.Type.sqlite3_stmt_ptr.implementation();
+ let sqlStmtPtr = sqlStmt.address();
+
+ // Unused portion of an SQL query.
+ let unused = Sqlite.Type.cstring.implementation();
+ let unusedPtr = unused.address();
+
+ // Compile an SQL statement.
+ let result = Sqlite.prepare_v2(db, sql, sql.length, sqlStmtPtr, unusedPtr);
+ do_check_eq(result, SQLITE_OK);
+
+ try {
+ if (bind) {
+ bind(sqlStmt);
+ }
+
+ // Evaluate an SQL statement.
+ result = Sqlite.step(sqlStmt);
+ do_check_eq(result, stepResult);
+
+ if (callback) {
+ callback(sqlStmt);
+ }
+ } catch (ex) {
+ do_check_true(false);
+ throw ex;
+ } finally {
+ // Destroy a prepared statement object.
+ result = Sqlite.finalize(sqlStmt);
+ do_check_eq(result, SQLITE_OK);
+ }
+}
+
+function test_open_close() {
+ do_print("Starting test_open_close");
+ do_check_eq(typeof Sqlite.open, "function");
+ do_check_eq(typeof Sqlite.close, "function");
+
+ withDB("open");
+}
+
+function test_open_v2_close() {
+ do_print("Starting test_open_v2_close");
+ do_check_eq(typeof Sqlite.open_v2, "function");
+
+ withDB("open_v2", [0x02, null]);
+}
+
+function createTableOnOpen(db) {
+ withQuery(db, "CREATE TABLE TEST(" +
+ "ID INT PRIMARY KEY NOT NULL," +
+ "FIELD1 INT," +
+ "FIELD2 REAL," +
+ "FIELD3 TEXT," +
+ "FIELD4 TEXT," +
+ "FIELD5 BLOB" +
+ ");", SQLITE_DONE);
+}
+
+function test_create_table() {
+ do_print("Starting test_create_table");
+ do_check_eq(typeof Sqlite.prepare_v2, "function");
+ do_check_eq(typeof Sqlite.step, "function");
+ do_check_eq(typeof Sqlite.finalize, "function");
+
+ withDB("open", [], createTableOnOpen);
+}
+
+/**
+ * Read column values after evaluating the SQL SELECT statement.
+ * @param {sqlite3_stmt_ptr} sqlStmt A pointer to the SQL statement.
+ */
+function onSqlite3Step(sqlStmt) {
+ // Get an int value from a query result from the ID (column 0).
+ let field = Sqlite.column_int(sqlStmt, 0);
+ do_check_eq(field, 3);
+
+ // Get an int value from a query result from the column 1.
+ field = Sqlite.column_int(sqlStmt, 1);
+ do_check_eq(field, 2);
+ // Get an int64 value from a query result from the column 1.
+ field = Sqlite.column_int64(sqlStmt, 1);
+ do_check_eq(field, 2);
+
+ // Get a double value from a query result from the column 2.
+ field = Sqlite.column_double(sqlStmt, 2);
+ do_check_eq(field, 1.2);
+
+ // Get a number of bytes of the value in the column 3.
+ let bytes = Sqlite.column_bytes(sqlStmt, 3);
+ do_check_eq(bytes, 4);
+ // Get a text(cstring) value from a query result from the column 3.
+ field = Sqlite.column_text(sqlStmt, 3);
+ do_check_eq(field.readString(), "DATA");
+
+ // Get a number of bytes of the UTF-16 value in the column 4.
+ bytes = Sqlite.column_bytes16(sqlStmt, 4);
+ do_check_eq(bytes, 8);
+ // Get a text16(wstring) value from a query result from the column 4.
+ field = Sqlite.column_text16(sqlStmt, 4);
+ do_check_eq(field.readString(), "TADA");
+
+ // Get a blob value from a query result from the column 5.
+ field = Sqlite.column_blob(sqlStmt, 5);
+ do_check_eq(ctypes.cast(field,
+ Sqlite.Type.cstring.implementation).readString(), "BLOB");
+}
+
+function test_insert_select() {
+ do_print("Starting test_insert_select");
+ do_check_eq(typeof Sqlite.column_int, "function");
+ do_check_eq(typeof Sqlite.column_int64, "function");
+ do_check_eq(typeof Sqlite.column_double, "function");
+ do_check_eq(typeof Sqlite.column_bytes, "function");
+ do_check_eq(typeof Sqlite.column_text, "function");
+ do_check_eq(typeof Sqlite.column_text16, "function");
+ do_check_eq(typeof Sqlite.column_blob, "function");
+
+ function onOpen(db) {
+ createTableOnOpen(db);
+ withQuery(db,
+ "INSERT INTO TEST VALUES (3, 2, 1.2, \"DATA\", \"TADA\", \"BLOB\");",
+ SQLITE_DONE);
+ withQuery(db, "SELECT * FROM TEST;", SQLITE_ROW, null, onSqlite3Step);
+ }
+
+ withDB("open", [], onOpen);
+}
+
+function test_insert_bind_select() {
+ do_print("Starting test_insert_bind_select");
+ do_check_eq(typeof Sqlite.bind_int, "function");
+ do_check_eq(typeof Sqlite.bind_int64, "function");
+ do_check_eq(typeof Sqlite.bind_double, "function");
+ do_check_eq(typeof Sqlite.bind_text, "function");
+ do_check_eq(typeof Sqlite.bind_text16, "function");
+ do_check_eq(typeof Sqlite.bind_blob, "function");
+
+ function onBind(sqlStmt) {
+ // Bind an int value to the ID (column 0).
+ let result = Sqlite.bind_int(sqlStmt, 1, 3);
+ do_check_eq(result, SQLITE_OK);
+
+ // Bind an int64 value to the FIELD1 (column 1).
+ result = Sqlite.bind_int64(sqlStmt, 2, 2);
+ do_check_eq(result, SQLITE_OK);
+
+ // Bind a double value to the FIELD2 (column 2).
+ result = Sqlite.bind_double(sqlStmt, 3, 1.2);
+ do_check_eq(result, SQLITE_OK);
+
+ // Destructor.
+ let destructor = Sqlite.Constants.SQLITE_TRANSIENT;
+ // Bind a text value to the FIELD3 (column 3).
+ result = Sqlite.bind_text(sqlStmt, 4, "DATA", 4, destructor);
+ do_check_eq(result, SQLITE_OK);
+
+ // Bind a text16 value to the FIELD4 (column 4).
+ result = Sqlite.bind_text16(sqlStmt, 5, "TADA", 8, destructor);
+ do_check_eq(result, SQLITE_OK);
+
+ // Bind a blob value to the FIELD5 (column 5).
+ result = Sqlite.bind_blob(sqlStmt, 6, ctypes.char.array()("BLOB"), 4,
+ destructor);
+ do_check_eq(result, SQLITE_OK);
+ }
+
+ function onOpen(db) {
+ createTableOnOpen(db);
+ withQuery(db, "INSERT INTO TEST VALUES (?, ?, ?, ?, ?, ?);", SQLITE_DONE,
+ onBind);
+ withQuery(db, "SELECT * FROM TEST;", SQLITE_ROW, null, onSqlite3Step);
+ }
+
+ withDB("open", [], onOpen);
+}
+
+function run_test() {
+ test_init();
+ test_open_close();
+ test_open_v2_close();
+ test_create_table();
+ test_insert_select();
+ test_insert_bind_select();
+ do_test_complete();
+}
diff --git a/toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_shared.js b/toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_shared.js
new file mode 100644
index 0000000000..54351a02a4
--- /dev/null
+++ b/toolkit/components/sqlite/tests/xpcshell/data/worker_sqlite_shared.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function send(message) {
+ self.postMessage(message);
+}
+
+function do_test_complete() {
+ send({kind: "do_test_complete", args: []});
+}
+
+function do_check_true(x) {
+ send({kind: "do_check_true", args: [!!x]});
+ if (x) {
+ dump("TEST-PASS: " + x + "\n");
+ } else {
+ throw new Error("do_check_true failed");
+ }
+}
+
+function do_check_eq(a, b) {
+ let result = a == b;
+ send({kind: "do_check_true", args: [result]});
+ if (!result) {
+ throw new Error("do_check_eq failed " + a + " != " + b);
+ }
+}
+
+function do_check_neq(a, b) {
+ let result = a != b;
+ send({kind: "do_check_true", args: [result]});
+ if (!result) {
+ throw new Error("do_check_neq failed " + a + " == " + b);
+ }
+}
+
+function do_print(x) {
+ dump("TEST-INFO: " + x + "\n");
+}
diff --git a/toolkit/components/sqlite/tests/xpcshell/test_sqlite_internal.js b/toolkit/components/sqlite/tests/xpcshell/test_sqlite_internal.js
new file mode 100644
index 0000000000..725cbfaeae
--- /dev/null
+++ b/toolkit/components/sqlite/tests/xpcshell/test_sqlite_internal.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+var WORKER_SOURCE_URI =
+ "chrome://test_sqlite_internal/content/worker_sqlite_internal.js";
+do_load_manifest("data/chrome.manifest");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function() {
+ let worker = new ChromeWorker(WORKER_SOURCE_URI);
+ let deferred = Promise.defer();
+ worker.onmessage = function(event) {
+ let data = event.data;
+ switch (data.kind) {
+ case "do_check_true":
+ try {
+ do_check_true(data.args[0]);
+ } catch (ex) {
+ // Ignore errors
+ }
+ break;
+ case "do_test_complete":
+ deferred.resolve();
+ worker.terminate();
+ break;
+ case "do_print":
+ do_print(data.args[0]);
+ break;
+ }
+ };
+ worker.onerror = function(event) {
+ let error = new Error(event.message, event.filename, event.lineno);
+ worker.terminate();
+ deferred.reject(error);
+ };
+ worker.postMessage("START");
+ return deferred.promise;
+});
diff --git a/toolkit/components/sqlite/tests/xpcshell/xpcshell.ini b/toolkit/components/sqlite/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..d652dbb1df
--- /dev/null
+++ b/toolkit/components/sqlite/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android' || (os == 'mac' && appname == 'thunderbird')
+support-files =
+ data/worker_sqlite_shared.js
+ data/worker_sqlite_internal.js
+ data/chrome.manifest
+
+[test_sqlite_internal.js]
diff --git a/toolkit/components/startup/StartupTimeline.cpp b/toolkit/components/startup/StartupTimeline.cpp
new file mode 100644
index 0000000000..e4762af15b
--- /dev/null
+++ b/toolkit/components/startup/StartupTimeline.cpp
@@ -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/. */
+
+#include "StartupTimeline.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+
+TimeStamp StartupTimeline::sStartupTimeline[StartupTimeline::MAX_EVENT_ID];
+const char *StartupTimeline::sStartupTimelineDesc[StartupTimeline::MAX_EVENT_ID] = {
+#define mozilla_StartupTimeline_Event(ev, desc) desc,
+#include "StartupTimeline.h"
+#undef mozilla_StartupTimeline_Event
+};
+
+} /* namespace mozilla */
+
+using mozilla::StartupTimeline;
+using mozilla::TimeStamp;
+
+/**
+ * The XRE_StartupTimeline_Record function is to be used by embedding
+ * applications that can't use mozilla::StartupTimeline::Record() directly.
+ *
+ * @param aEvent The event to be recorded, must correspond to an element of the
+ * mozilla::StartupTimeline::Event enumartion
+ * @param aWhen The time at which the event happened
+ */
+void
+XRE_StartupTimelineRecord(int aEvent, TimeStamp aWhen)
+{
+ StartupTimeline::Record((StartupTimeline::Event)aEvent, aWhen);
+}
diff --git a/toolkit/components/startup/StartupTimeline.h b/toolkit/components/startup/StartupTimeline.h
new file mode 100644
index 0000000000..8a966f1731
--- /dev/null
+++ b/toolkit/components/startup/StartupTimeline.h
@@ -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/. */
+
+#ifdef mozilla_StartupTimeline_Event
+mozilla_StartupTimeline_Event(PROCESS_CREATION, "process")
+mozilla_StartupTimeline_Event(START, "start")
+mozilla_StartupTimeline_Event(MAIN, "main")
+mozilla_StartupTimeline_Event(SELECT_PROFILE, "selectProfile")
+mozilla_StartupTimeline_Event(AFTER_PROFILE_LOCKED, "afterProfileLocked")
+// Record the beginning and end of startup crash detection to compare with crash stats to know whether
+// detection should be improved to start or end sooner.
+mozilla_StartupTimeline_Event(STARTUP_CRASH_DETECTION_BEGIN, "startupCrashDetectionBegin")
+mozilla_StartupTimeline_Event(STARTUP_CRASH_DETECTION_END, "startupCrashDetectionEnd")
+mozilla_StartupTimeline_Event(FIRST_PAINT, "firstPaint")
+mozilla_StartupTimeline_Event(SESSION_RESTORE_INIT, "sessionRestoreInit")
+mozilla_StartupTimeline_Event(SESSION_RESTORED, "sessionRestored")
+mozilla_StartupTimeline_Event(CREATE_TOP_LEVEL_WINDOW, "createTopLevelWindow")
+mozilla_StartupTimeline_Event(LINKER_INITIALIZED, "linkerInitialized")
+mozilla_StartupTimeline_Event(LIBRARIES_LOADED, "librariesLoaded")
+mozilla_StartupTimeline_Event(FIRST_LOAD_URI, "firstLoadURI")
+
+// The following are actually shutdown events, used to monitor the duration of shutdown
+mozilla_StartupTimeline_Event(QUIT_APPLICATION, "quitApplication")
+mozilla_StartupTimeline_Event(PROFILE_BEFORE_CHANGE, "profileBeforeChange")
+#else
+
+#ifndef mozilla_StartupTimeline
+#define mozilla_StartupTimeline
+
+#include "mozilla/TimeStamp.h"
+#include "nscore.h"
+#include "GeckoProfiler.h"
+
+#ifdef MOZ_LINKER
+extern "C" {
+/* This symbol is resolved by the custom linker. The function it resolves
+ * to dumps some statistics about the linker at the key events recorded
+ * by the startup timeline. */
+extern void __moz_linker_stats(const char *str)
+NS_VISIBILITY_DEFAULT __attribute__((weak));
+} /* extern "C" */
+#else
+
+#endif
+
+namespace mozilla {
+
+void RecordShutdownEndTimeStamp();
+void RecordShutdownStartTimeStamp();
+
+class StartupTimeline {
+public:
+ enum Event {
+ #define mozilla_StartupTimeline_Event(ev, z) ev,
+ #include "StartupTimeline.h"
+ #undef mozilla_StartupTimeline_Event
+ MAX_EVENT_ID
+ };
+
+ static TimeStamp Get(Event ev) {
+ return sStartupTimeline[ev];
+ }
+
+ static const char *Describe(Event ev) {
+ return sStartupTimelineDesc[ev];
+ }
+
+ static void Record(Event ev) {
+ PROFILER_MARKER(Describe(ev));
+ Record(ev, TimeStamp::Now());
+ }
+
+ static void Record(Event ev, TimeStamp when) {
+ sStartupTimeline[ev] = when;
+#ifdef MOZ_LINKER
+ if (__moz_linker_stats)
+ __moz_linker_stats(Describe(ev));
+#endif
+ }
+
+ static void RecordOnce(Event ev) {
+ if (!HasRecord(ev))
+ Record(ev);
+ }
+
+ static bool HasRecord(Event ev) {
+ return !sStartupTimeline[ev].IsNull();
+ }
+
+private:
+ static NS_EXTERNAL_VIS_(TimeStamp) sStartupTimeline[MAX_EVENT_ID];
+ static NS_EXTERNAL_VIS_(const char *) sStartupTimelineDesc[MAX_EVENT_ID];
+};
+
+} // namespace mozilla
+
+#endif /* mozilla_StartupTimeline */
+
+#endif /* mozilla_StartupTimeline_Event */
diff --git a/toolkit/components/startup/moz.build b/toolkit/components/startup/moz.build
new file mode 100644
index 0000000000..dbd5803846
--- /dev/null
+++ b/toolkit/components/startup/moz.build
@@ -0,0 +1,38 @@
+# -*- 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']
+
+EXPORTS.mozilla += [
+ 'StartupTimeline.h',
+]
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+UNIFIED_SOURCES += [
+ 'nsAppStartup.cpp',
+ 'StartupTimeline.cpp',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ # This file cannot be built in unified mode because of name clashes with Windows headers.
+ SOURCES += [
+ 'nsUserInfoWin.cpp',
+ ]
+elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+ UNIFIED_SOURCES += [
+ 'nsUserInfoMac.mm',
+ ]
+else:
+ UNIFIED_SOURCES += [
+ 'nsUserInfoUnix.cpp',
+ ]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Startup and Profile System')
diff --git a/toolkit/components/startup/mozprofilerprobe.mof b/toolkit/components/startup/mozprofilerprobe.mof
new file mode 100644
index 0000000000..03379ba1d1
--- /dev/null
+++ b/toolkit/components/startup/mozprofilerprobe.mof
@@ -0,0 +1,29 @@
+#pragma namespace("\\\\.\\root\\wmi")
+#pragma autorecover
+
+[dynamic: ToInstance, Description("Mozilla Generic Provider"),
+ Guid("{509962E0-406B-46F4-99BA-5A009F8D2225}")]
+class MozillaProvider : EventTrace
+{
+};
+
+[dynamic: ToInstance, Description("Mozilla Event: Places Init is complete."): Amended,
+ Guid("{A3DA04E0-57D7-482A-A1C1-61DA5F95BACB}"),
+ EventType(1)]
+class MozillaEventPlacesInit : MozillaProvider
+{
+};
+
+[dynamic: ToInstance, Description("Mozilla Event: Session Store Window Restored."): Amended,
+ Guid("{917B96B1-ECAD-4DAB-A760-8D49027748AE}"),
+ EventType(1)]
+class MozillaEventSessionStoreWindowRestored : MozillaProvider
+{
+};
+
+[dynamic: ToInstance, Description("Mozilla Event: XPCOM Shutdown."): Amended,
+ Guid("{26D1E091-0AE7-4F49-A554-4214445C505C}"),
+ EventType(1)]
+class MozillaEventXPCOMShutdown : MozillaProvider
+{
+};
diff --git a/toolkit/components/startup/nsAppStartup.cpp b/toolkit/components/startup/nsAppStartup.cpp
new file mode 100644
index 0000000000..85d5afdf97
--- /dev/null
+++ b/toolkit/components/startup/nsAppStartup.cpp
@@ -0,0 +1,1030 @@
+/* -*- 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 "nsAppStartup.h"
+
+#include "nsIAppShellService.h"
+#include "nsPIDOMWindow.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIProcess.h"
+#include "nsIPromptService.h"
+#include "nsIStringBundle.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIToolkitProfile.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsIWindowMediator.h"
+#include "nsIWindowWatcher.h"
+#include "nsIXULRuntime.h"
+#include "nsIXULWindow.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsThreadUtils.h"
+#include "nsAutoPtr.h"
+#include "nsString.h"
+#include "mozilla/Preferences.h"
+#include "GeckoProfiler.h"
+
+#include "prprf.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsWidgetsCID.h"
+#include "nsAppRunner.h"
+#include "nsAppShellCID.h"
+#include "nsXPCOMCIDInternal.h"
+#include "mozilla/Services.h"
+#include "nsIXPConnect.h"
+#include "jsapi.h"
+#include "js/Date.h"
+#include "prenv.h"
+#include "nsAppDirectoryServiceDefs.h"
+
+#if defined(XP_WIN)
+// Prevent collisions with nsAppStartup::GetStartupInfo()
+#undef GetStartupInfo
+#endif
+
+#include "mozilla/IOInterposer.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/StartupTimeline.h"
+
+static NS_DEFINE_CID(kAppShellCID, NS_APPSHELL_CID);
+
+#define kPrefLastSuccess "toolkit.startup.last_success"
+#define kPrefMaxResumedCrashes "toolkit.startup.max_resumed_crashes"
+#define kPrefRecentCrashes "toolkit.startup.recent_crashes"
+#define kPrefAlwaysUseSafeMode "toolkit.startup.always_use_safe_mode"
+
+#if defined(XP_WIN)
+#include "mozilla/perfprobe.h"
+/**
+ * Events sent to the system for profiling purposes
+ */
+//Keep them syncronized with the .mof file
+
+//Process-wide GUID, used by the OS to differentiate sources
+// {509962E0-406B-46F4-99BA-5A009F8D2225}
+//Keep it synchronized with the .mof file
+#define NS_APPLICATION_TRACING_CID \
+ { 0x509962E0, 0x406B, 0x46F4, \
+ { 0x99, 0xBA, 0x5A, 0x00, 0x9F, 0x8D, 0x22, 0x25} }
+
+//Event-specific GUIDs, used by the OS to differentiate events
+// {A3DA04E0-57D7-482A-A1C1-61DA5F95BACB}
+#define NS_PLACES_INIT_COMPLETE_EVENT_CID \
+ { 0xA3DA04E0, 0x57D7, 0x482A, \
+ { 0xA1, 0xC1, 0x61, 0xDA, 0x5F, 0x95, 0xBA, 0xCB} }
+// {917B96B1-ECAD-4DAB-A760-8D49027748AE}
+#define NS_SESSION_STORE_WINDOW_RESTORED_EVENT_CID \
+ { 0x917B96B1, 0xECAD, 0x4DAB, \
+ { 0xA7, 0x60, 0x8D, 0x49, 0x02, 0x77, 0x48, 0xAE} }
+// {26D1E091-0AE7-4F49-A554-4214445C505C}
+#define NS_XPCOM_SHUTDOWN_EVENT_CID \
+ { 0x26D1E091, 0x0AE7, 0x4F49, \
+ { 0xA5, 0x54, 0x42, 0x14, 0x44, 0x5C, 0x50, 0x5C} }
+
+static NS_DEFINE_CID(kApplicationTracingCID,
+ NS_APPLICATION_TRACING_CID);
+static NS_DEFINE_CID(kPlacesInitCompleteCID,
+ NS_PLACES_INIT_COMPLETE_EVENT_CID);
+static NS_DEFINE_CID(kSessionStoreWindowRestoredCID,
+ NS_SESSION_STORE_WINDOW_RESTORED_EVENT_CID);
+static NS_DEFINE_CID(kXPCOMShutdownCID,
+ NS_XPCOM_SHUTDOWN_EVENT_CID);
+#endif //defined(XP_WIN)
+
+using namespace mozilla;
+
+class nsAppExitEvent : public mozilla::Runnable {
+private:
+ RefPtr<nsAppStartup> mService;
+
+public:
+ explicit nsAppExitEvent(nsAppStartup *service) : mService(service) {}
+
+ NS_IMETHOD Run() override {
+ // Tell the appshell to exit
+ mService->mAppShell->Exit();
+
+ mService->mRunning = false;
+ return NS_OK;
+ }
+};
+
+/**
+ * Computes an approximation of the absolute time represented by @a stamp
+ * which is comparable to those obtained via PR_Now(). If the current absolute
+ * time varies a lot (e.g. DST adjustments) since the first call then the
+ * resulting times may be inconsistent.
+ *
+ * @param stamp The timestamp to be converted
+ * @returns The converted timestamp
+ */
+uint64_t ComputeAbsoluteTimestamp(PRTime prnow, TimeStamp now, TimeStamp stamp)
+{
+ static PRTime sAbsoluteNow = PR_Now();
+ static TimeStamp sMonotonicNow = TimeStamp::Now();
+
+ return sAbsoluteNow - (sMonotonicNow - stamp).ToMicroseconds();
+}
+
+//
+// nsAppStartup
+//
+
+nsAppStartup::nsAppStartup() :
+ mConsiderQuitStopper(0),
+ mRunning(false),
+ mShuttingDown(false),
+ mStartingUp(true),
+ mAttemptingQuit(false),
+ mRestart(false),
+ mInterrupted(false),
+ mIsSafeModeNecessary(false),
+ mStartupCrashTrackingEnded(false),
+ mRestartNotSameProfile(false)
+{ }
+
+
+nsresult
+nsAppStartup::Init()
+{
+ nsresult rv;
+
+ // Create widget application shell
+ mAppShell = do_GetService(kAppShellCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> os =
+ mozilla::services::GetObserverService();
+ if (!os)
+ return NS_ERROR_FAILURE;
+
+ os->AddObserver(this, "quit-application", true);
+ os->AddObserver(this, "quit-application-forced", true);
+ os->AddObserver(this, "sessionstore-init-started", true);
+ os->AddObserver(this, "sessionstore-windows-restored", true);
+ os->AddObserver(this, "profile-change-teardown", true);
+ os->AddObserver(this, "xul-window-registered", true);
+ os->AddObserver(this, "xul-window-destroyed", true);
+ os->AddObserver(this, "profile-before-change", true);
+ os->AddObserver(this, "xpcom-shutdown", true);
+
+#if defined(XP_WIN)
+ os->AddObserver(this, "places-init-complete", true);
+ // This last event is only interesting to us for xperf-based measures
+
+ // Initialize interaction with profiler
+ mProbesManager =
+ new ProbeManager(
+ kApplicationTracingCID,
+ NS_LITERAL_CSTRING("Application startup probe"));
+ // Note: The operation is meant mostly for in-house profiling.
+ // Therefore, we do not warn if probes manager cannot be initialized
+
+ if (mProbesManager) {
+ mPlacesInitCompleteProbe =
+ mProbesManager->
+ GetProbe(kPlacesInitCompleteCID,
+ NS_LITERAL_CSTRING("places-init-complete"));
+ NS_WARNING_ASSERTION(mPlacesInitCompleteProbe,
+ "Cannot initialize probe 'places-init-complete'");
+
+ mSessionWindowRestoredProbe =
+ mProbesManager->
+ GetProbe(kSessionStoreWindowRestoredCID,
+ NS_LITERAL_CSTRING("sessionstore-windows-restored"));
+ NS_WARNING_ASSERTION(
+ mSessionWindowRestoredProbe,
+ "Cannot initialize probe 'sessionstore-windows-restored'");
+
+ mXPCOMShutdownProbe =
+ mProbesManager->
+ GetProbe(kXPCOMShutdownCID,
+ NS_LITERAL_CSTRING("xpcom-shutdown"));
+ NS_WARNING_ASSERTION(mXPCOMShutdownProbe,
+ "Cannot initialize probe 'xpcom-shutdown'");
+
+ rv = mProbesManager->StartSession();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Cannot initialize system probe manager");
+ }
+#endif //defined(XP_WIN)
+
+ return NS_OK;
+}
+
+
+//
+// nsAppStartup->nsISupports
+//
+
+NS_IMPL_ISUPPORTS(nsAppStartup,
+ nsIAppStartup,
+ nsIWindowCreator,
+ nsIWindowCreator2,
+ nsIObserver,
+ nsISupportsWeakReference)
+
+
+//
+// nsAppStartup->nsIAppStartup
+//
+
+NS_IMETHODIMP
+nsAppStartup::CreateHiddenWindow()
+{
+#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_UIKIT)
+ return NS_OK;
+#else
+ nsCOMPtr<nsIAppShellService> appShellService
+ (do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(appShellService, NS_ERROR_FAILURE);
+
+ return appShellService->CreateHiddenWindow();
+#endif
+}
+
+
+NS_IMETHODIMP
+nsAppStartup::DestroyHiddenWindow()
+{
+#if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_UIKIT)
+ return NS_OK;
+#else
+ nsCOMPtr<nsIAppShellService> appShellService
+ (do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(appShellService, NS_ERROR_FAILURE);
+
+ return appShellService->DestroyHiddenWindow();
+#endif
+}
+
+NS_IMETHODIMP
+nsAppStartup::Run(void)
+{
+ NS_ASSERTION(!mRunning, "Reentrant appstartup->Run()");
+
+ // If we have no windows open and no explicit calls to
+ // enterLastWindowClosingSurvivalArea, or somebody has explicitly called
+ // quit, don't bother running the event loop which would probably leave us
+ // with a zombie process.
+
+ if (!mShuttingDown && mConsiderQuitStopper != 0) {
+#ifdef XP_MACOSX
+ EnterLastWindowClosingSurvivalArea();
+#endif
+
+ mRunning = true;
+
+ nsresult rv = mAppShell->Run();
+ if (NS_FAILED(rv))
+ return rv;
+ }
+
+ nsresult retval = NS_OK;
+ if (mRestart) {
+ retval = NS_SUCCESS_RESTART_APP;
+ } else if (mRestartNotSameProfile) {
+ retval = NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE;
+ }
+
+ return retval;
+}
+
+
+
+NS_IMETHODIMP
+nsAppStartup::Quit(uint32_t aMode)
+{
+ uint32_t ferocity = (aMode & 0xF);
+
+ // Quit the application. We will asynchronously call the appshell's
+ // Exit() method via nsAppExitEvent to allow one last pass
+ // through any events in the queue. This guarantees a tidy cleanup.
+ nsresult rv = NS_OK;
+ bool postedExitEvent = false;
+
+ if (mShuttingDown)
+ return NS_OK;
+
+ // If we're considering quitting, we will only do so if:
+ if (ferocity == eConsiderQuit) {
+#ifdef XP_MACOSX
+ nsCOMPtr<nsIAppShellService> appShell
+ (do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ bool hasHiddenPrivateWindow = false;
+ if (appShell) {
+ appShell->GetHasHiddenPrivateWindow(&hasHiddenPrivateWindow);
+ }
+ int32_t suspiciousCount = hasHiddenPrivateWindow ? 2 : 1;
+#endif
+
+ if (mConsiderQuitStopper == 0) {
+ // there are no windows...
+ ferocity = eAttemptQuit;
+ }
+#ifdef XP_MACOSX
+ else if (mConsiderQuitStopper == suspiciousCount) {
+ // ... or there is only a hiddenWindow left, and it's useless:
+
+ // Failure shouldn't be fatal, but will abort quit attempt:
+ if (!appShell)
+ return NS_OK;
+
+ bool usefulHiddenWindow;
+ appShell->GetApplicationProvidedHiddenWindow(&usefulHiddenWindow);
+ nsCOMPtr<nsIXULWindow> hiddenWindow;
+ appShell->GetHiddenWindow(getter_AddRefs(hiddenWindow));
+ // If the remaining windows are useful, we won't quit:
+ nsCOMPtr<nsIXULWindow> hiddenPrivateWindow;
+ if (hasHiddenPrivateWindow) {
+ appShell->GetHiddenPrivateWindow(getter_AddRefs(hiddenPrivateWindow));
+ if ((!hiddenWindow && !hiddenPrivateWindow) || usefulHiddenWindow)
+ return NS_OK;
+ } else if (!hiddenWindow || usefulHiddenWindow) {
+ return NS_OK;
+ }
+
+ ferocity = eAttemptQuit;
+ }
+#endif
+ }
+
+ nsCOMPtr<nsIObserverService> obsService;
+ if (ferocity == eAttemptQuit || ferocity == eForceQuit) {
+
+ nsCOMPtr<nsISimpleEnumerator> windowEnumerator;
+ nsCOMPtr<nsIWindowMediator> mediator (do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
+ if (mediator) {
+ mediator->GetEnumerator(nullptr, getter_AddRefs(windowEnumerator));
+ if (windowEnumerator) {
+ bool more;
+ windowEnumerator->HasMoreElements(&more);
+ // If we reported no windows, we definitely shouldn't be
+ // iterating any here.
+ MOZ_ASSERT_IF(!mConsiderQuitStopper, !more);
+
+ while (more) {
+ nsCOMPtr<nsISupports> window;
+ windowEnumerator->GetNext(getter_AddRefs(window));
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow(do_QueryInterface(window));
+ if (domWindow) {
+ MOZ_ASSERT(domWindow->IsOuterWindow());
+ if (!domWindow->CanClose())
+ return NS_OK;
+ }
+ windowEnumerator->HasMoreElements(&more);
+ }
+ }
+ }
+
+ PROFILER_MARKER("Shutdown start");
+ mozilla::RecordShutdownStartTimeStamp();
+ mShuttingDown = true;
+ if (!mRestart) {
+ mRestart = (aMode & eRestart) != 0;
+ }
+
+ if (!mRestartNotSameProfile) {
+ mRestartNotSameProfile = (aMode & eRestartNotSameProfile) != 0;
+ }
+
+ if (mRestart || mRestartNotSameProfile) {
+ // Mark the next startup as a restart.
+ PR_SetEnv("MOZ_APP_RESTART=1");
+
+ /* Firefox-restarts reuse the process so regular process start-time isn't
+ a useful indicator of startup time anymore. */
+ TimeStamp::RecordProcessRestart();
+ }
+
+ obsService = mozilla::services::GetObserverService();
+
+ if (!mAttemptingQuit) {
+ mAttemptingQuit = true;
+#ifdef XP_MACOSX
+ // now even the Mac wants to quit when the last window is closed
+ ExitLastWindowClosingSurvivalArea();
+#endif
+ if (obsService)
+ obsService->NotifyObservers(nullptr, "quit-application-granted", nullptr);
+ }
+
+ /* Enumerate through each open window and close it. It's important to do
+ this before we forcequit because this can control whether we really quit
+ at all. e.g. if one of these windows has an unload handler that
+ opens a new window. Ugh. I know. */
+ CloseAllWindows();
+
+ if (mediator) {
+ if (ferocity == eAttemptQuit) {
+ ferocity = eForceQuit; // assume success
+
+ /* Were we able to immediately close all windows? if not, eAttemptQuit
+ failed. This could happen for a variety of reasons; in fact it's
+ very likely. Perhaps we're being called from JS and the window->Close
+ method hasn't had a chance to wrap itself up yet. So give up.
+ We'll return (with eConsiderQuit) as the remaining windows are
+ closed. */
+ mediator->GetEnumerator(nullptr, getter_AddRefs(windowEnumerator));
+ if (windowEnumerator) {
+ bool more;
+ while (windowEnumerator->HasMoreElements(&more), more) {
+ /* we can't quit immediately. we'll try again as the last window
+ finally closes. */
+ ferocity = eAttemptQuit;
+ nsCOMPtr<nsISupports> window;
+ windowEnumerator->GetNext(getter_AddRefs(window));
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow = do_QueryInterface(window);
+ if (domWindow) {
+ if (!domWindow->Closed()) {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (ferocity == eForceQuit) {
+ // do it!
+
+ // No chance of the shutdown being cancelled from here on; tell people
+ // we're shutting down for sure while all services are still available.
+ if (obsService) {
+ NS_NAMED_LITERAL_STRING(shutdownStr, "shutdown");
+ NS_NAMED_LITERAL_STRING(restartStr, "restart");
+ obsService->NotifyObservers(nullptr, "quit-application",
+ (mRestart || mRestartNotSameProfile) ?
+ restartStr.get() : shutdownStr.get());
+ }
+
+ if (!mRunning) {
+ postedExitEvent = true;
+ }
+ else {
+ // no matter what, make sure we send the exit event. If
+ // worst comes to worst, we'll do a leaky shutdown but we WILL
+ // shut down. Well, assuming that all *this* stuff works ;-).
+ nsCOMPtr<nsIRunnable> event = new nsAppExitEvent(this);
+ rv = NS_DispatchToCurrentThread(event);
+ if (NS_SUCCEEDED(rv)) {
+ postedExitEvent = true;
+ }
+ else {
+ NS_WARNING("failed to dispatch nsAppExitEvent");
+ }
+ }
+ }
+
+ // turn off the reentrancy check flag, but not if we have
+ // more asynchronous work to do still.
+ if (!postedExitEvent)
+ mShuttingDown = false;
+ return rv;
+}
+
+
+void
+nsAppStartup::CloseAllWindows()
+{
+ nsCOMPtr<nsIWindowMediator> mediator
+ (do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
+
+ nsCOMPtr<nsISimpleEnumerator> windowEnumerator;
+
+ mediator->GetEnumerator(nullptr, getter_AddRefs(windowEnumerator));
+
+ if (!windowEnumerator)
+ return;
+
+ bool more;
+ while (NS_SUCCEEDED(windowEnumerator->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsISupports> isupports;
+ if (NS_FAILED(windowEnumerator->GetNext(getter_AddRefs(isupports))))
+ break;
+
+ nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(isupports);
+ NS_ASSERTION(window, "not an nsPIDOMWindow");
+ if (window) {
+ MOZ_ASSERT(window->IsOuterWindow());
+ window->ForceClose();
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsAppStartup::EnterLastWindowClosingSurvivalArea(void)
+{
+ ++mConsiderQuitStopper;
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsAppStartup::ExitLastWindowClosingSurvivalArea(void)
+{
+ NS_ASSERTION(mConsiderQuitStopper > 0, "consider quit stopper out of bounds");
+ --mConsiderQuitStopper;
+
+ if (mRunning)
+ Quit(eConsiderQuit);
+
+ return NS_OK;
+}
+
+//
+// nsAppStartup->nsIAppStartup2
+//
+
+NS_IMETHODIMP
+nsAppStartup::GetShuttingDown(bool *aResult)
+{
+ *aResult = mShuttingDown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::GetStartingUp(bool *aResult)
+{
+ *aResult = mStartingUp;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::DoneStartingUp()
+{
+ // This must be called once at most
+ MOZ_ASSERT(mStartingUp);
+
+ mStartingUp = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::GetRestarting(bool *aResult)
+{
+ *aResult = mRestart;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::GetWasRestarted(bool *aResult)
+{
+ char *mozAppRestart = PR_GetEnv("MOZ_APP_RESTART");
+
+ /* When calling PR_SetEnv() with an empty value the existing variable may
+ * be unset or set to the empty string depending on the underlying platform
+ * thus we have to check if the variable is present and not empty. */
+ *aResult = mozAppRestart && (strcmp(mozAppRestart, "") != 0);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::SetInterrupted(bool aInterrupted)
+{
+ mInterrupted = aInterrupted;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::GetInterrupted(bool *aInterrupted)
+{
+ *aInterrupted = mInterrupted;
+ return NS_OK;
+}
+
+//
+// nsAppStartup->nsIWindowCreator
+//
+
+NS_IMETHODIMP
+nsAppStartup::CreateChromeWindow(nsIWebBrowserChrome *aParent,
+ uint32_t aChromeFlags,
+ nsIWebBrowserChrome **_retval)
+{
+ bool cancel;
+ return CreateChromeWindow2(aParent, aChromeFlags, 0, nullptr, nullptr, &cancel, _retval);
+}
+
+
+//
+// nsAppStartup->nsIWindowCreator2
+//
+
+NS_IMETHODIMP
+nsAppStartup::SetScreenId(uint32_t aScreenId)
+{
+ nsCOMPtr<nsIAppShellService> appShell(do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ if (!appShell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return appShell->SetScreenId(aScreenId);
+}
+
+NS_IMETHODIMP
+nsAppStartup::CreateChromeWindow2(nsIWebBrowserChrome *aParent,
+ uint32_t aChromeFlags,
+ uint32_t aContextFlags,
+ nsITabParent *aOpeningTab,
+ mozIDOMWindowProxy* aOpener,
+ bool *aCancel,
+ nsIWebBrowserChrome **_retval)
+{
+ NS_ENSURE_ARG_POINTER(aCancel);
+ NS_ENSURE_ARG_POINTER(_retval);
+ *aCancel = false;
+ *_retval = 0;
+
+ // Non-modal windows cannot be opened if we are attempting to quit
+ if (mAttemptingQuit && (aChromeFlags & nsIWebBrowserChrome::CHROME_MODAL) == 0)
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+
+ nsCOMPtr<nsIXULWindow> newWindow;
+
+ if (aParent) {
+ nsCOMPtr<nsIXULWindow> xulParent(do_GetInterface(aParent));
+ NS_ASSERTION(xulParent, "window created using non-XUL parent. that's unexpected, but may work.");
+
+ if (xulParent)
+ xulParent->CreateNewWindow(aChromeFlags, aOpeningTab, aOpener, getter_AddRefs(newWindow));
+ // And if it fails, don't try again without a parent. It could fail
+ // intentionally (bug 115969).
+ } else { // try using basic methods:
+ /* You really shouldn't be making dependent windows without a parent.
+ But unparented modal (and therefore dependent) windows happen
+ in our codebase, so we allow it after some bellyaching: */
+ if (aChromeFlags & nsIWebBrowserChrome::CHROME_DEPENDENT)
+ NS_WARNING("dependent window created without a parent");
+
+ nsCOMPtr<nsIAppShellService> appShell(do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ if (!appShell)
+ return NS_ERROR_FAILURE;
+
+ appShell->CreateTopLevelWindow(0, 0, aChromeFlags,
+ nsIAppShellService::SIZE_TO_CONTENT,
+ nsIAppShellService::SIZE_TO_CONTENT,
+ aOpeningTab, aOpener,
+ getter_AddRefs(newWindow));
+ }
+
+ // if anybody gave us anything to work with, use it
+ if (newWindow) {
+ newWindow->SetContextFlags(aContextFlags);
+ nsCOMPtr<nsIInterfaceRequestor> thing(do_QueryInterface(newWindow));
+ if (thing)
+ CallGetInterface(thing.get(), _retval);
+ }
+
+ return *_retval ? NS_OK : NS_ERROR_FAILURE;
+}
+
+
+//
+// nsAppStartup->nsIObserver
+//
+
+NS_IMETHODIMP
+nsAppStartup::Observe(nsISupports *aSubject,
+ const char *aTopic, const char16_t *aData)
+{
+ NS_ASSERTION(mAppShell, "appshell service notified before appshell built");
+ if (!strcmp(aTopic, "quit-application-forced")) {
+ mShuttingDown = true;
+ }
+ else if (!strcmp(aTopic, "profile-change-teardown")) {
+ if (!mShuttingDown) {
+ EnterLastWindowClosingSurvivalArea();
+ CloseAllWindows();
+ ExitLastWindowClosingSurvivalArea();
+ }
+ } else if (!strcmp(aTopic, "xul-window-registered")) {
+ EnterLastWindowClosingSurvivalArea();
+ } else if (!strcmp(aTopic, "xul-window-destroyed")) {
+ ExitLastWindowClosingSurvivalArea();
+ } else if (!strcmp(aTopic, "sessionstore-windows-restored")) {
+ StartupTimeline::Record(StartupTimeline::SESSION_RESTORED);
+ IOInterposer::EnteringNextStage();
+#if defined(XP_WIN)
+ if (mSessionWindowRestoredProbe) {
+ mSessionWindowRestoredProbe->Trigger();
+ }
+ } else if (!strcmp(aTopic, "places-init-complete")) {
+ if (mPlacesInitCompleteProbe) {
+ mPlacesInitCompleteProbe->Trigger();
+ }
+#endif //defined(XP_WIN)
+ } else if (!strcmp(aTopic, "sessionstore-init-started")) {
+ StartupTimeline::Record(StartupTimeline::SESSION_RESTORE_INIT);
+ } else if (!strcmp(aTopic, "xpcom-shutdown")) {
+ IOInterposer::EnteringNextStage();
+#if defined(XP_WIN)
+ if (mXPCOMShutdownProbe) {
+ mXPCOMShutdownProbe->Trigger();
+ }
+#endif // defined(XP_WIN)
+ } else if (!strcmp(aTopic, "quit-application")) {
+ StartupTimeline::Record(StartupTimeline::QUIT_APPLICATION);
+ } else if (!strcmp(aTopic, "profile-before-change")) {
+ StartupTimeline::Record(StartupTimeline::PROFILE_BEFORE_CHANGE);
+ } else {
+ NS_ERROR("Unexpected observer topic.");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::GetStartupInfo(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval)
+{
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+
+ aRetval.setObject(*obj);
+
+ TimeStamp procTime = StartupTimeline::Get(StartupTimeline::PROCESS_CREATION);
+ TimeStamp now = TimeStamp::Now();
+ PRTime absNow = PR_Now();
+
+ if (procTime.IsNull()) {
+ bool error = false;
+
+ procTime = TimeStamp::ProcessCreation(error);
+
+ if (error) {
+ Telemetry::Accumulate(Telemetry::STARTUP_MEASUREMENT_ERRORS,
+ StartupTimeline::PROCESS_CREATION);
+ }
+
+ StartupTimeline::Record(StartupTimeline::PROCESS_CREATION, procTime);
+ }
+
+ for (int i = StartupTimeline::PROCESS_CREATION;
+ i < StartupTimeline::MAX_EVENT_ID;
+ ++i)
+ {
+ StartupTimeline::Event ev = static_cast<StartupTimeline::Event>(i);
+ TimeStamp stamp = StartupTimeline::Get(ev);
+
+ if (stamp.IsNull() && (ev == StartupTimeline::MAIN)) {
+ // Always define main to aid with bug 689256.
+ stamp = procTime;
+ MOZ_ASSERT(!stamp.IsNull());
+ Telemetry::Accumulate(Telemetry::STARTUP_MEASUREMENT_ERRORS,
+ StartupTimeline::MAIN);
+ }
+
+ if (!stamp.IsNull()) {
+ if (stamp >= procTime) {
+ PRTime prStamp = ComputeAbsoluteTimestamp(absNow, now, stamp)
+ / PR_USEC_PER_MSEC;
+ JS::Rooted<JSObject*> date(aCx, JS::NewDateObject(aCx, JS::TimeClip(prStamp)));
+ JS_DefineProperty(aCx, obj, StartupTimeline::Describe(ev), date, JSPROP_ENUMERATE);
+ } else {
+ Telemetry::Accumulate(Telemetry::STARTUP_MEASUREMENT_ERRORS, ev);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::GetAutomaticSafeModeNecessary(bool *_retval)
+{
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ bool alwaysSafe = false;
+ Preferences::GetBool(kPrefAlwaysUseSafeMode, &alwaysSafe);
+
+ if (!alwaysSafe) {
+#if DEBUG
+ mIsSafeModeNecessary = false;
+#else
+ mIsSafeModeNecessary &= !PR_GetEnv("MOZ_DISABLE_AUTO_SAFE_MODE");
+#endif
+ }
+
+ *_retval = mIsSafeModeNecessary;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::TrackStartupCrashBegin(bool *aIsSafeModeNecessary)
+{
+ const int32_t MAX_TIME_SINCE_STARTUP = 6 * 60 * 60 * 1000;
+ const int32_t MAX_STARTUP_BUFFER = 10;
+ nsresult rv;
+
+ mStartupCrashTrackingEnded = false;
+
+ StartupTimeline::Record(StartupTimeline::STARTUP_CRASH_DETECTION_BEGIN);
+
+ bool hasLastSuccess = Preferences::HasUserValue(kPrefLastSuccess);
+ if (!hasLastSuccess) {
+ // Clear so we don't get stuck with SafeModeNecessary returning true if we
+ // have had too many recent crashes and the last success pref is missing.
+ Preferences::ClearUser(kPrefRecentCrashes);
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ bool inSafeMode = false;
+ nsCOMPtr<nsIXULRuntime> xr = do_GetService(XULRUNTIME_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(xr, NS_ERROR_FAILURE);
+
+ xr->GetInSafeMode(&inSafeMode);
+
+ PRTime replacedLockTime;
+ rv = xr->GetReplacedLockTime(&replacedLockTime);
+
+ if (NS_FAILED(rv) || !replacedLockTime) {
+ if (!inSafeMode)
+ Preferences::ClearUser(kPrefRecentCrashes);
+ GetAutomaticSafeModeNecessary(aIsSafeModeNecessary);
+ return NS_OK;
+ }
+
+ // check whether safe mode is necessary
+ int32_t maxResumedCrashes = -1;
+ rv = Preferences::GetInt(kPrefMaxResumedCrashes, &maxResumedCrashes);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ int32_t recentCrashes = 0;
+ Preferences::GetInt(kPrefRecentCrashes, &recentCrashes);
+ mIsSafeModeNecessary = (recentCrashes > maxResumedCrashes && maxResumedCrashes != -1);
+
+ // Bug 731613 - Don't check if the last startup was a crash if XRE_PROFILE_PATH is set. After
+ // profile manager, the profile lock's mod. time has been changed so can't be used on this startup.
+ // After a restart, it's safe to assume the last startup was successful.
+ char *xreProfilePath = PR_GetEnv("XRE_PROFILE_PATH");
+ if (xreProfilePath) {
+ GetAutomaticSafeModeNecessary(aIsSafeModeNecessary);
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // time of last successful startup
+ int32_t lastSuccessfulStartup;
+ rv = Preferences::GetInt(kPrefLastSuccess, &lastSuccessfulStartup);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t lockSeconds = (int32_t)(replacedLockTime / PR_MSEC_PER_SEC);
+
+ // started close enough to good startup so call it good
+ if (lockSeconds <= lastSuccessfulStartup + MAX_STARTUP_BUFFER
+ && lockSeconds >= lastSuccessfulStartup - MAX_STARTUP_BUFFER) {
+ GetAutomaticSafeModeNecessary(aIsSafeModeNecessary);
+ return NS_OK;
+ }
+
+ // sanity check that the pref set at last success is not greater than the current time
+ if (PR_Now() / PR_USEC_PER_SEC <= lastSuccessfulStartup)
+ return NS_ERROR_FAILURE;
+
+ // The last startup was a crash so include it in the count regardless of when it happened.
+ Telemetry::Accumulate(Telemetry::STARTUP_CRASH_DETECTED, true);
+
+ if (inSafeMode) {
+ GetAutomaticSafeModeNecessary(aIsSafeModeNecessary);
+ return NS_OK;
+ }
+
+ PRTime now = (PR_Now() / PR_USEC_PER_MSEC);
+ // if the last startup attempt which crashed was in the last 6 hours
+ if (replacedLockTime >= now - MAX_TIME_SINCE_STARTUP) {
+ NS_WARNING("Last startup was detected as a crash.");
+ recentCrashes++;
+ rv = Preferences::SetInt(kPrefRecentCrashes, recentCrashes);
+ } else {
+ // Otherwise ignore that crash and all previous since it may not be applicable anymore
+ // and we don't want someone to get stuck in safe mode if their prefs are read-only.
+ rv = Preferences::ClearUser(kPrefRecentCrashes);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // recalculate since recent crashes count may have changed above
+ mIsSafeModeNecessary = (recentCrashes > maxResumedCrashes && maxResumedCrashes != -1);
+
+ nsCOMPtr<nsIPrefService> prefs = Preferences::GetService();
+ rv = prefs->SavePrefFile(nullptr); // flush prefs to disk since we are tracking crashes
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ GetAutomaticSafeModeNecessary(aIsSafeModeNecessary);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAppStartup::TrackStartupCrashEnd()
+{
+ bool inSafeMode = false;
+ nsCOMPtr<nsIXULRuntime> xr = do_GetService(XULRUNTIME_SERVICE_CONTRACTID);
+ if (xr)
+ xr->GetInSafeMode(&inSafeMode);
+
+ // return if we already ended or we're restarting into safe mode
+ if (mStartupCrashTrackingEnded || (mIsSafeModeNecessary && !inSafeMode))
+ return NS_OK;
+ mStartupCrashTrackingEnded = true;
+
+ StartupTimeline::Record(StartupTimeline::STARTUP_CRASH_DETECTION_END);
+
+ // Use the timestamp of XRE_main as an approximation for the lock file timestamp.
+ // See MAX_STARTUP_BUFFER for the buffer time period.
+ TimeStamp mainTime = StartupTimeline::Get(StartupTimeline::MAIN);
+ TimeStamp now = TimeStamp::Now();
+ PRTime prNow = PR_Now();
+ nsresult rv;
+
+ if (mainTime.IsNull()) {
+ NS_WARNING("Could not get StartupTimeline::MAIN time.");
+ } else {
+ uint64_t lockFileTime = ComputeAbsoluteTimestamp(prNow, now, mainTime);
+
+ rv = Preferences::SetInt(kPrefLastSuccess,
+ (int32_t)(lockFileTime / PR_USEC_PER_SEC));
+
+ if (NS_FAILED(rv))
+ NS_WARNING("Could not set startup crash detection pref.");
+ }
+
+ if (inSafeMode && mIsSafeModeNecessary) {
+ // On a successful startup in automatic safe mode, allow the user one more crash
+ // in regular mode before returning to safe mode.
+ int32_t maxResumedCrashes = 0;
+ int32_t prefType;
+ rv = Preferences::GetDefaultRootBranch()->GetPrefType(kPrefMaxResumedCrashes, &prefType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (prefType == nsIPrefBranch::PREF_INT) {
+ rv = Preferences::GetInt(kPrefMaxResumedCrashes, &maxResumedCrashes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = Preferences::SetInt(kPrefRecentCrashes, maxResumedCrashes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (!inSafeMode) {
+ // clear the count of recent crashes after a succesful startup when not in safe mode
+ rv = Preferences::ClearUser(kPrefRecentCrashes);
+ if (NS_FAILED(rv)) NS_WARNING("Could not clear startup crash count.");
+ }
+ nsCOMPtr<nsIPrefService> prefs = Preferences::GetService();
+ rv = prefs->SavePrefFile(nullptr); // flush prefs to disk since we are tracking crashes
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAppStartup::RestartInSafeMode(uint32_t aQuitMode)
+{
+ PR_SetEnv("MOZ_SAFE_MODE_RESTART=1");
+ this->Quit(aQuitMode | nsIAppStartup::eRestart);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAppStartup::CreateInstanceWithProfile(nsIToolkitProfile* aProfile)
+{
+ if (NS_WARN_IF(!aProfile)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(gAbsoluteArgv0Path.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> execPath;
+ nsresult rv = NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(gAbsoluteArgv0Path),
+ true, getter_AddRefs(execPath));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIProcess> process = do_CreateInstance(NS_PROCESS_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = process->Init(execPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString profileName;
+ rv = aProfile->GetName(profileName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ const char *args[] = { "-P", profileName.get() };
+ rv = process->Run(false, args, 2);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/startup/nsAppStartup.h b/toolkit/components/startup/nsAppStartup.h
new file mode 100644
index 0000000000..44c406ee42
--- /dev/null
+++ b/toolkit/components/startup/nsAppStartup.h
@@ -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/. */
+
+#ifndef nsAppStartup_h__
+#define nsAppStartup_h__
+
+#include "nsIAppStartup.h"
+#include "nsIWindowCreator2.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+
+#include "nsINativeAppSupport.h"
+#include "nsIAppShell.h"
+#include "mozilla/Attributes.h"
+
+#if defined(XP_WIN)
+//XPerf-backed probes
+#include "mozilla/perfprobe.h"
+#include "nsAutoPtr.h"
+#endif //defined(XP_WIN)
+
+
+// {7DD4D320-C84B-4624-8D45-7BB9B2356977}
+#define NS_TOOLKIT_APPSTARTUP_CID \
+{ 0x7dd4d320, 0xc84b, 0x4624, { 0x8d, 0x45, 0x7b, 0xb9, 0xb2, 0x35, 0x69, 0x77 } }
+
+
+class nsAppStartup final : public nsIAppStartup,
+ public nsIWindowCreator2,
+ public nsIObserver,
+ public nsSupportsWeakReference
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIAPPSTARTUP
+ NS_DECL_NSIWINDOWCREATOR
+ NS_DECL_NSIWINDOWCREATOR2
+ NS_DECL_NSIOBSERVER
+
+ nsAppStartup();
+ nsresult Init();
+
+private:
+ ~nsAppStartup() { }
+
+ void CloseAllWindows();
+
+ friend class nsAppExitEvent;
+
+ nsCOMPtr<nsIAppShell> mAppShell;
+
+ int32_t mConsiderQuitStopper; // if > 0, Quit(eConsiderQuit) fails
+ bool mRunning; // Have we started the main event loop?
+ bool mShuttingDown; // Quit method reentrancy check
+ bool mStartingUp; // Have we passed final-ui-startup?
+ bool mAttemptingQuit; // Quit(eAttemptQuit) still trying
+ bool mRestart; // Quit(eRestart)
+ bool mInterrupted; // Was startup interrupted by an interactive prompt?
+ bool mIsSafeModeNecessary; // Whether safe mode is necessary
+ bool mStartupCrashTrackingEnded; // Whether startup crash tracking has already ended
+ bool mRestartNotSameProfile; // Quit(eRestartNotSameProfile)
+
+#if defined(XP_WIN)
+ //Interaction with OS-provided profiling probes
+ typedef mozilla::probes::ProbeManager ProbeManager;
+ typedef mozilla::probes::Probe Probe;
+ RefPtr<ProbeManager> mProbesManager;
+ RefPtr<Probe> mPlacesInitCompleteProbe;
+ RefPtr<Probe> mSessionWindowRestoredProbe;
+ RefPtr<Probe> mXPCOMShutdownProbe;
+#endif
+};
+
+#endif // nsAppStartup_h__
diff --git a/toolkit/components/startup/nsUserInfo.h b/toolkit/components/startup/nsUserInfo.h
new file mode 100644
index 0000000000..49e86c64a3
--- /dev/null
+++ b/toolkit/components/startup/nsUserInfo.h
@@ -0,0 +1,23 @@
+/* -*- 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 __nsUserInfo_h
+#define __nsUserInfo_h
+
+#include "nsIUserInfo.h"
+
+class nsUserInfo: public nsIUserInfo
+
+{
+public:
+ nsUserInfo(void);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUSERINFO
+
+protected:
+ virtual ~nsUserInfo();
+};
+
+#endif /* __nsUserInfo_h */
diff --git a/toolkit/components/startup/nsUserInfoMac.h b/toolkit/components/startup/nsUserInfoMac.h
new file mode 100644
index 0000000000..822e0edd5d
--- /dev/null
+++ b/toolkit/components/startup/nsUserInfoMac.h
@@ -0,0 +1,25 @@
+/* -*- 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 __nsUserInfoMac_h
+#define __nsUserInfoMac_h
+
+#include "nsIUserInfo.h"
+#include "nsReadableUtils.h"
+
+class nsUserInfo: public nsIUserInfo
+{
+public:
+ nsUserInfo();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUSERINFO
+
+ nsresult GetPrimaryEmailAddress(nsCString &aEmailAddress);
+
+protected:
+ virtual ~nsUserInfo() {}
+};
+
+#endif /* __nsUserInfo_h */
diff --git a/toolkit/components/startup/nsUserInfoMac.mm b/toolkit/components/startup/nsUserInfoMac.mm
new file mode 100644
index 0000000000..1895cf1773
--- /dev/null
+++ b/toolkit/components/startup/nsUserInfoMac.mm
@@ -0,0 +1,84 @@
+/* -*- Mode: Objective-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 "nsUserInfoMac.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+
+#import <Cocoa/Cocoa.h>
+#import <AddressBook/AddressBook.h>
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+nsUserInfo::nsUserInfo() {}
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(char16_t **aFullname)
+{
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT
+
+ NS_ConvertUTF8toUTF16 fullName([NSFullUserName() UTF8String]);
+ *aFullname = ToNewUnicode(fullName);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(char **aUsername)
+{
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT
+
+ nsAutoCString username([NSUserName() UTF8String]);
+ *aUsername = ToNewCString(username);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT
+}
+
+nsresult
+nsUserInfo::GetPrimaryEmailAddress(nsCString &aEmailAddress)
+{
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT
+
+ // Try to get this user's primary email from the system addressbook's "me card"
+ // (if they've filled it)
+ ABPerson *me = [[ABAddressBook sharedAddressBook] me];
+ ABMultiValue *emailAddresses = [me valueForProperty:kABEmailProperty];
+ if ([emailAddresses count] > 0) {
+ // get the index of the primary email, in case there are more than one
+ int primaryEmailIndex = [emailAddresses indexForIdentifier:[emailAddresses primaryIdentifier]];
+ aEmailAddress.Assign([[emailAddresses valueAtIndex:primaryEmailIndex] UTF8String]);
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(char **aEmailAddress)
+{
+ nsAutoCString email;
+ if (NS_SUCCEEDED(GetPrimaryEmailAddress(email)))
+ *aEmailAddress = ToNewCString(email);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(char **aDomain)
+{
+ nsAutoCString email;
+ if (NS_SUCCEEDED(GetPrimaryEmailAddress(email))) {
+ int32_t index = email.FindChar('@');
+ if (index != -1) {
+ // chop off everything before, and including the '@'
+ *aDomain = ToNewCString(Substring(email, index + 1));
+ }
+ }
+ return NS_OK;
+}
diff --git a/toolkit/components/startup/nsUserInfoUnix.cpp b/toolkit/components/startup/nsUserInfoUnix.cpp
new file mode 100644
index 0000000000..71bc46da25
--- /dev/null
+++ b/toolkit/components/startup/nsUserInfoUnix.cpp
@@ -0,0 +1,167 @@
+/* -*- 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 "nsUserInfo.h"
+#include "nsCRT.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+
+#include "nsString.h"
+#include "nsXPIDLString.h"
+#include "nsReadableUtils.h"
+#include "nsNativeCharsetUtils.h"
+
+/* Some UNIXy platforms don't have pw_gecos. In this case we use pw_name */
+#if defined(NO_PW_GECOS)
+#define PW_GECOS pw_name
+#else
+#define PW_GECOS pw_gecos
+#endif
+
+nsUserInfo::nsUserInfo()
+{
+}
+
+nsUserInfo::~nsUserInfo()
+{
+}
+
+NS_IMPL_ISUPPORTS(nsUserInfo,nsIUserInfo)
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(char16_t **aFullname)
+{
+ struct passwd *pw = nullptr;
+
+ pw = getpwuid (geteuid());
+
+ if (!pw || !pw->PW_GECOS) return NS_ERROR_FAILURE;
+
+#ifdef DEBUG_sspitzer
+ printf("fullname = %s\n", pw->PW_GECOS);
+#endif
+
+ nsAutoCString fullname(pw->PW_GECOS);
+
+ // now try to parse the GECOS information, which will be in the form
+ // Full Name, <other stuff> - eliminate the ", <other stuff>
+ // also, sometimes GECOS uses "&" to mean "the user name" so do
+ // the appropriate substitution
+
+ // truncate at first comma (field delimiter)
+ int32_t index;
+ if ((index = fullname.Find(",")) != kNotFound)
+ fullname.Truncate(index);
+
+ // replace ampersand with username
+ if (pw->pw_name) {
+ nsAutoCString username(pw->pw_name);
+ if (!username.IsEmpty() && nsCRT::IsLower(username.CharAt(0)))
+ username.SetCharAt(nsCRT::ToUpper(username.CharAt(0)), 0);
+
+ fullname.ReplaceSubstring("&", username.get());
+ }
+
+ nsAutoString unicodeFullname;
+ NS_CopyNativeToUnicode(fullname, unicodeFullname);
+
+ *aFullname = ToNewUnicode(unicodeFullname);
+
+ if (*aFullname)
+ return NS_OK;
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(char * *aUsername)
+{
+ struct passwd *pw = nullptr;
+
+ // is this portable? those are POSIX compliant calls, but I need to check
+ pw = getpwuid(geteuid());
+
+ if (!pw || !pw->pw_name) return NS_ERROR_FAILURE;
+
+#ifdef DEBUG_sspitzer
+ printf("username = %s\n", pw->pw_name);
+#endif
+
+ *aUsername = strdup(pw->pw_name);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(char * *aDomain)
+{
+ nsresult rv = NS_ERROR_FAILURE;
+
+ struct utsname buf;
+ char *domainname = nullptr;
+
+ if (uname(&buf) < 0) {
+ return rv;
+ }
+
+#if defined(__linux__)
+ domainname = buf.domainname;
+#endif
+
+ if (domainname && domainname[0]) {
+ *aDomain = strdup(domainname);
+ rv = NS_OK;
+ }
+ else {
+ // try to get the hostname from the nodename
+ // on machines that use DHCP, domainname may not be set
+ // but the nodename might.
+ if (buf.nodename[0]) {
+ // if the nodename is foo.bar.org, use bar.org as the domain
+ char *pos = strchr(buf.nodename,'.');
+ if (pos) {
+ *aDomain = strdup(pos+1);
+ rv = NS_OK;
+ }
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(char * *aEmailAddress)
+{
+ // use username + "@" + domain for the email address
+
+ nsresult rv;
+
+ nsAutoCString emailAddress;
+ nsXPIDLCString username;
+ nsXPIDLCString domain;
+
+ rv = GetUsername(getter_Copies(username));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = GetDomain(getter_Copies(domain));
+ if (NS_FAILED(rv)) return rv;
+
+ if (!username.IsEmpty() && !domain.IsEmpty()) {
+ emailAddress = (const char *)username;
+ emailAddress += "@";
+ emailAddress += (const char *)domain;
+ }
+ else {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aEmailAddress = ToNewCString(emailAddress);
+
+ return NS_OK;
+}
+
diff --git a/toolkit/components/startup/nsUserInfoWin.cpp b/toolkit/components/startup/nsUserInfoWin.cpp
new file mode 100644
index 0000000000..b27a2c483b
--- /dev/null
+++ b/toolkit/components/startup/nsUserInfoWin.cpp
@@ -0,0 +1,133 @@
+/* -*- 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 "nsUserInfo.h"
+
+#include "mozilla/ArrayUtils.h" // ArrayLength
+#include "nsString.h"
+#include "windows.h"
+#include "nsCRT.h"
+#include "nsXPIDLString.h"
+
+#define SECURITY_WIN32
+#include "lm.h"
+#include "security.h"
+
+nsUserInfo::nsUserInfo()
+{
+}
+
+nsUserInfo::~nsUserInfo()
+{
+}
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(char **aUsername)
+{
+ NS_ENSURE_ARG_POINTER(aUsername);
+ *aUsername = nullptr;
+
+ // ULEN is the max username length as defined in lmcons.h
+ wchar_t username[UNLEN +1];
+ DWORD size = mozilla::ArrayLength(username);
+ if (!GetUserNameW(username, &size))
+ return NS_ERROR_FAILURE;
+
+ *aUsername = ToNewUTF8String(nsDependentString(username));
+ return (*aUsername) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(char16_t **aFullname)
+{
+ NS_ENSURE_ARG_POINTER(aFullname);
+ *aFullname = nullptr;
+
+ wchar_t fullName[512];
+ DWORD size = mozilla::ArrayLength(fullName);
+
+ if (GetUserNameExW(NameDisplay, fullName, &size)) {
+ *aFullname = ToNewUnicode(nsDependentString(fullName));
+ } else {
+ DWORD getUsernameError = GetLastError();
+
+ // Try to use the net APIs regardless of the error because it may be
+ // able to obtain the information.
+ wchar_t username[UNLEN + 1];
+ size = mozilla::ArrayLength(username);
+ if (!GetUserNameW(username, &size)) {
+ // ERROR_NONE_MAPPED means the user info is not filled out on this computer
+ return getUsernameError == ERROR_NONE_MAPPED ?
+ NS_ERROR_NOT_AVAILABLE : NS_ERROR_FAILURE;
+ }
+
+ const DWORD level = 2;
+ LPBYTE info;
+ // If the NetUserGetInfo function has no full name info it will return
+ // success with an empty string.
+ NET_API_STATUS status = NetUserGetInfo(nullptr, username, level, &info);
+ if (status != NERR_Success) {
+ // We have an error with NetUserGetInfo but we know the info is not
+ // filled in because GetUserNameExW returned ERROR_NONE_MAPPED.
+ return getUsernameError == ERROR_NONE_MAPPED ?
+ NS_ERROR_NOT_AVAILABLE : NS_ERROR_FAILURE;
+ }
+
+ nsDependentString fullName =
+ nsDependentString(reinterpret_cast<USER_INFO_2 *>(info)->usri2_full_name);
+
+ // NetUserGetInfo returns an empty string if the full name is not filled out
+ if (fullName.Length() == 0) {
+ NetApiBufferFree(info);
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ *aFullname = ToNewUnicode(fullName);
+ NetApiBufferFree(info);
+ }
+
+ return (*aFullname) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(char **aDomain)
+{
+ NS_ENSURE_ARG_POINTER(aDomain);
+ *aDomain = nullptr;
+
+ const DWORD level = 100;
+ LPBYTE info;
+ NET_API_STATUS status = NetWkstaGetInfo(nullptr, level, &info);
+ if (status == NERR_Success) {
+ *aDomain =
+ ToNewUTF8String(nsDependentString(reinterpret_cast<WKSTA_INFO_100 *>(info)->
+ wki100_langroup));
+ NetApiBufferFree(info);
+ }
+
+ return (*aDomain) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(char **aEmailAddress)
+{
+ NS_ENSURE_ARG_POINTER(aEmailAddress);
+ *aEmailAddress = nullptr;
+
+ // RFC3696 says max length of an email address is 254
+ wchar_t emailAddress[255];
+ DWORD size = mozilla::ArrayLength(emailAddress);
+
+ if (!GetUserNameExW(NameUserPrincipal, emailAddress, &size)) {
+ DWORD getUsernameError = GetLastError();
+ return getUsernameError == ERROR_NONE_MAPPED ?
+ NS_ERROR_NOT_AVAILABLE : NS_ERROR_FAILURE;
+ }
+
+ *aEmailAddress = ToNewUTF8String(nsDependentString(emailAddress));
+ return (*aEmailAddress) ? NS_OK : NS_ERROR_FAILURE;
+}
diff --git a/toolkit/components/startup/public/moz.build b/toolkit/components/startup/public/moz.build
new file mode 100644
index 0000000000..5894b6c517
--- /dev/null
+++ b/toolkit/components/startup/public/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/.
+
+XPIDL_SOURCES += [
+ 'nsIAppStartup.idl',
+ 'nsIUserInfo.idl',
+]
+
+XPIDL_MODULE = 'appstartup'
+
diff --git a/toolkit/components/startup/public/nsIAppStartup.idl b/toolkit/components/startup/public/nsIAppStartup.idl
new file mode 100644
index 0000000000..34705d39f8
--- /dev/null
+++ b/toolkit/components/startup/public/nsIAppStartup.idl
@@ -0,0 +1,195 @@
+/* -*- Mode: IDL; tab-width: 8; 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 "nsISupports.idl"
+
+interface nsICmdLineService;
+interface nsIToolkitProfile;
+
+[scriptable, uuid(6621f6d5-6c04-4a0e-9e74-447db221484e)]
+
+interface nsIAppStartup : nsISupports
+{
+ /**
+ * Create the hidden window.
+ */
+ void createHiddenWindow();
+
+ /**
+ * Destroys the hidden window. This will have no effect if the hidden window
+ * has not yet been created.
+ */
+ void destroyHiddenWindow();
+
+ /**
+ * Runs an application event loop: normally the main event pump which
+ * defines the lifetime of the application. If there are no windows open
+ * and no outstanding calls to enterLastWindowClosingSurvivalArea this
+ * method will exit immediately.
+ *
+ * @returnCode NS_SUCCESS_RESTART_APP
+ * This return code indicates that the application should be
+ * restarted because quit was called with the eRestart flag.
+
+ * @returnCode NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE
+ * This return code indicates that the application should be
+ * restarted without necessarily using the same profile because
+ * quit was called with the eRestartNotSameProfile flag.
+ */
+ void run();
+
+ /**
+ * There are situations where all application windows will be
+ * closed but we don't want to take this as a signal to quit the
+ * app. Bracket the code where the last window could close with
+ * these.
+ */
+ void enterLastWindowClosingSurvivalArea();
+ void exitLastWindowClosingSurvivalArea();
+
+ /**
+ * Startup Crash Detection
+ *
+ * Keeps track of application startup begining and success using flags to
+ * determine whether the application is crashing on startup.
+ * When the number of crashes crosses the acceptable threshold, safe mode
+ * or other repair procedures are performed.
+ */
+
+ /**
+ * Whether automatic safe mode is necessary at this time. This gets set
+ * in trackStartupCrashBegin.
+ *
+ * @see trackStartupCrashBegin
+ */
+ readonly attribute boolean automaticSafeModeNecessary;
+
+ /**
+ * Restart the application in safe mode
+ * @param aQuitMode
+ * This parameter modifies how the app is shutdown.
+ * @see nsIAppStartup::quit
+ */
+ void restartInSafeMode(in uint32_t aQuitMode);
+
+ /**
+ * Run a new instance of this app with a specified profile
+ * @param aProfile
+ * The profile we want to use.
+ * @see nsIAppStartup::quit
+ */
+ void createInstanceWithProfile(in nsIToolkitProfile aProfile);
+
+ /**
+ * If the last startup crashed then increment a counter.
+ * Set a flag so on next startup we can detect whether TrackStartupCrashEnd
+ * was called (and therefore the application crashed).
+ * @return whether safe mode is necessary
+ */
+ bool trackStartupCrashBegin();
+
+ /**
+ * We have succesfully started without crashing. Clear flags that were
+ * tracking past crashes.
+ */
+ void trackStartupCrashEnd();
+
+ /**
+ * The following flags may be passed as the aMode parameter to the quit
+ * method. One and only one of the "Quit" flags must be specified. The
+ * eRestart flag may be bit-wise combined with one of the "Quit" flags to
+ * cause the application to restart after it quits.
+ */
+
+ /**
+ * Attempt to quit if all windows are closed.
+ */
+ const uint32_t eConsiderQuit = 0x01;
+
+ /**
+ * Try to close all windows, then quit if successful.
+ */
+ const uint32_t eAttemptQuit = 0x02;
+
+ /**
+ * Quit, damnit!
+ */
+ const uint32_t eForceQuit = 0x03;
+
+ /**
+ * Restart the application after quitting. The application will be
+ * restarted with the same profile and an empty command line.
+ */
+ const uint32_t eRestart = 0x10;
+
+ /**
+ * When restarting attempt to start in the i386 architecture. Only supported
+ * on OSX.
+ */
+ const uint32_t eRestarti386 = 0x20;
+
+ /**
+ * When restarting attempt to start in the x86_64 architecture. Only
+ * supported on OSX.
+ */
+ const uint32_t eRestartx86_64 = 0x40;
+
+ /**
+ * Restart the application after quitting. The application will be
+ * restarted with an empty command line and the normal profile selection
+ * process will take place on startup.
+ */
+ const uint32_t eRestartNotSameProfile = 0x100;
+
+ /**
+ * Exit the event loop, and shut down the app.
+ *
+ * @param aMode
+ * This parameter modifies how the app is shutdown, and it is
+ * constructed from the constants defined above.
+ */
+ void quit(in uint32_t aMode);
+
+ /**
+ * True if the application is in the process of shutting down.
+ */
+ readonly attribute boolean shuttingDown;
+
+ /**
+ * True if the application is in the process of starting up.
+ *
+ * Startup is complete once all observers of final-ui-startup have returned.
+ */
+ readonly attribute boolean startingUp;
+
+ /**
+ * Mark the startup as completed.
+ *
+ * Called at the end of startup by nsAppRunner.
+ */
+ [noscript] void doneStartingUp();
+
+ /**
+ * True if the application is being restarted
+ */
+ readonly attribute boolean restarting;
+
+ /**
+ * True if this is the startup following restart, i.e. if the application
+ * was restarted using quit(eRestart*).
+ */
+ readonly attribute boolean wasRestarted;
+
+ /**
+ * Returns an object with main, process, firstPaint, sessionRestored properties.
+ * Properties may not be available depending on platform or application
+ */
+ [implicit_jscontext] jsval getStartupInfo();
+
+ /**
+ * True if startup was interrupted by an interactive prompt.
+ */
+ attribute boolean interrupted;
+};
diff --git a/toolkit/components/startup/public/nsIUserInfo.idl b/toolkit/components/startup/public/nsIUserInfo.idl
new file mode 100644
index 0000000000..1838cc69ce
--- /dev/null
+++ b/toolkit/components/startup/public/nsIUserInfo.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(6c1034f0-1dd2-11b2-aa14-e6657ed7bb0b)]
+interface nsIUserInfo : nsISupports
+{
+ /* these are things the system may know about the current user */
+
+ readonly attribute wstring fullname;
+
+ readonly attribute string emailAddress;
+
+ /* should this be a wstring? */
+ readonly attribute string username;
+
+ readonly attribute string domain;
+};
+
+%{C++
+
+// 14c13684-1dd2-11b2-9463-bb10ba742554
+#define NS_USERINFO_CID \
+{ 0x14c13684, 0x1dd2, 0x11b2, \
+ {0x94, 0x63, 0xbb, 0x10, 0xba, 0x74, 0x25, 0x54}}
+
+#define NS_USERINFO_CONTRACTID "@mozilla.org/userinfo;1"
+
+%}
diff --git a/toolkit/components/startup/tests/browser/.eslintrc.js b/toolkit/components/startup/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/startup/tests/browser/beforeunload.html b/toolkit/components/startup/tests/browser/beforeunload.html
new file mode 100644
index 0000000000..93ddd5f143
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/beforeunload.html
@@ -0,0 +1,10 @@
+<html>
+ <script>
+ window.onbeforeunload = function(event) {
+ event.returnValue = 'Test beforeunload handler';
+ }
+ </script>
+ <body>
+ Test page
+ </body>
+</html>
diff --git a/toolkit/components/startup/tests/browser/browser.ini b/toolkit/components/startup/tests/browser/browser.ini
new file mode 100644
index 0000000000..0c02f73b68
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/browser.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+ head.js
+ beforeunload.html
+
+[browser_bug511456.js]
+[browser_bug537449.js]
+[browser_crash_detection.js]
diff --git a/toolkit/components/startup/tests/browser/browser_bug511456.js b/toolkit/components/startup/tests/browser/browser_bug511456.js
new file mode 100644
index 0000000000..652a34ea23
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/browser_bug511456.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/. */
+
+"use strict";
+
+const TEST_URL = "http://example.com/browser/toolkit/components/startup/tests/browser/beforeunload.html";
+
+SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]});
+
+function test() {
+ waitForExplicitFinish();
+ ignoreAllUncaughtExceptions();
+
+ let win2 = window.openDialog(location, "", "chrome,all,dialog=no", "about:blank");
+ win2.addEventListener("load", function onLoad() {
+ win2.removeEventListener("load", onLoad);
+
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
+ let browser = gBrowser.selectedBrowser;
+
+ whenBrowserLoaded(browser, function () {
+ let seenDialog = false;
+
+ // Cancel the prompt the first time.
+ waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => {
+ seenDialog = true;
+ btnStay.click();
+ });
+
+ let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
+ getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);
+ ok(seenDialog, "Should have seen a prompt dialog");
+ ok(!win2.closed, "Shouldn't have closed the additional window");
+ win2.close();
+
+ // Leave the page the second time.
+ waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => {
+ btnLeave.click();
+ });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ executeSoon(finish);
+ });
+ });
+}
diff --git a/toolkit/components/startup/tests/browser/browser_bug537449.js b/toolkit/components/startup/tests/browser/browser_bug537449.js
new file mode 100644
index 0000000000..ed3446f8dd
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/browser_bug537449.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/. */
+
+"use strict";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]});
+
+const TEST_URL = "http://example.com/browser/toolkit/components/startup/tests/browser/beforeunload.html";
+
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
+ let browser = gBrowser.selectedBrowser;
+
+ whenBrowserLoaded(browser, function () {
+ let seenDialog = false;
+
+ // Cancel the prompt the first time.
+ waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => {
+ seenDialog = true;
+ btnStay.click();
+ });
+
+ let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
+ getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);
+ ok(seenDialog, "Should have seen a prompt dialog");
+ ok(!window.closed, "Shouldn't have closed the window");
+
+ let win2 = window.openDialog(location, "", "chrome,all,dialog=no", "about:blank");
+ ok(win2 != null, "Should have been able to open a new window");
+ win2.addEventListener("load", function onLoad() {
+ win2.removeEventListener("load", onLoad);
+ win2.close();
+
+ // Leave the page the second time.
+ waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => {
+ btnLeave.click();
+ });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+ executeSoon(finish);
+ });
+ });
+}
diff --git a/toolkit/components/startup/tests/browser/browser_crash_detection.js b/toolkit/components/startup/tests/browser/browser_crash_detection.js
new file mode 100644
index 0000000000..039f80dde7
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/browser_crash_detection.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ function checkLastSuccess() {
+ let lastSuccess = Services.prefs.getIntPref("toolkit.startup.last_success");
+ let si = Services.startup.getStartupInfo();
+ is(lastSuccess, parseInt(si["main"].getTime() / 1000, 10),
+ "Startup tracking pref should be set after a delay at the end of startup");
+ finish();
+ }
+
+ if (Services.prefs.getPrefType("toolkit.startup.max_resumed_crashes") == Services.prefs.PREF_INVALID) {
+ info("Skipping this test since startup crash detection is disabled");
+ return;
+ }
+
+ const startupCrashEndDelay = 35 * 1000;
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+ setTimeout(checkLastSuccess, startupCrashEndDelay);
+}
diff --git a/toolkit/components/startup/tests/browser/head.js b/toolkit/components/startup/tests/browser/head.js
new file mode 100644
index 0000000000..c17da2ff78
--- /dev/null
+++ b/toolkit/components/startup/tests/browser/head.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+function whenBrowserLoaded(browser, callback) {
+ return BrowserTestUtils.browserLoaded(browser).then(callback);
+}
+
+function waitForOnBeforeUnloadDialog(browser, callback) {
+ browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog(event) {
+ if (Cu.isCrossProcessWrapper(event.target)) {
+ // This event fires in both the content and chrome processes. We
+ // want to ignore the one in the content process.
+ return;
+ }
+
+ browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true);
+
+ executeSoon(() => {
+ let stack = browser.parentNode;
+ let dialogs = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
+ let {button0, button1} = dialogs[0].ui;
+ callback(button0, button1);
+ });
+ }, true);
+}
diff --git a/toolkit/components/startup/tests/unit/.eslintrc.js b/toolkit/components/startup/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/startup/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/startup/tests/unit/head_startup.js b/toolkit/components/startup/tests/unit/head_startup.js
new file mode 100644
index 0000000000..2466f70eed
--- /dev/null
+++ b/toolkit/components/startup/tests/unit/head_startup.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const XULRUNTIME_CONTRACTID = "@mozilla.org/xre/runtime;1";
+const XULRUNTIME_CID = Components.ID("7685dac8-3637-4660-a544-928c5ec0e714}");
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var gAppInfo = null;
+
+function createAppInfo(ID, name, version, platformVersion="1.0") {
+ let tmp = {};
+ Components.utils.import("resource://testing-common/AppInfo.jsm", tmp);
+ gAppInfo = tmp.newAppInfo({
+ ID, name, version, platformVersion,
+ crashReporter: true,
+ replacedLockTime: 0,
+ });
+
+ let XULAppInfoFactory = {
+ createInstance: function (outer, iid) {
+ if (outer != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ return gAppInfo.QueryInterface(iid);
+ }
+ };
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(XULRUNTIME_CID, "XULRuntime",
+ XULRUNTIME_CONTRACTID, XULAppInfoFactory);
+}
diff --git a/toolkit/components/startup/tests/unit/test_startup_crash.js b/toolkit/components/startup/tests/unit/test_startup_crash.js
new file mode 100644
index 0000000000..2836330866
--- /dev/null
+++ b/toolkit/components/startup/tests/unit/test_startup_crash.js
@@ -0,0 +1,300 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "10.0");
+
+var prefService = Services.prefs;
+var appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].
+ getService(Ci.nsIAppStartup);
+
+const pref_last_success = "toolkit.startup.last_success";
+const pref_recent_crashes = "toolkit.startup.recent_crashes";
+const pref_max_resumed_crashes = "toolkit.startup.max_resumed_crashes";
+const pref_always_use_safe_mode = "toolkit.startup.always_use_safe_mode";
+
+function run_test() {
+ prefService.setBoolPref(pref_always_use_safe_mode, true);
+
+ resetTestEnv(0);
+
+ test_trackStartupCrashBegin();
+ test_trackStartupCrashEnd();
+ test_trackStartupCrashBegin_safeMode();
+ test_trackStartupCrashEnd_safeMode();
+ test_maxResumed();
+ resetTestEnv(0);
+
+ prefService.clearUserPref(pref_always_use_safe_mode);
+}
+
+// reset prefs to default
+function resetTestEnv(replacedLockTime) {
+ try {
+ // call begin to reset mStartupCrashTrackingEnded
+ appStartup.trackStartupCrashBegin();
+ } catch (x) { }
+ prefService.setIntPref(pref_max_resumed_crashes, 2);
+ prefService.clearUserPref(pref_recent_crashes);
+ gAppInfo.replacedLockTime = replacedLockTime;
+ prefService.clearUserPref(pref_last_success);
+}
+
+function now_seconds() {
+ return ms_to_s(Date.now());
+}
+
+function ms_to_s(ms) {
+ return Math.floor(ms / 1000);
+}
+
+function test_trackStartupCrashBegin() {
+ let max_resumed = prefService.getIntPref(pref_max_resumed_crashes);
+ do_check_false(gAppInfo.inSafeMode);
+
+ // first run with startup crash tracking - existing profile lock
+ let replacedLockTime = Date.now();
+ resetTestEnv(replacedLockTime);
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(prefService.prefHasUserValue(pref_last_success));
+ do_check_eq(replacedLockTime, gAppInfo.replacedLockTime);
+ try {
+ do_check_false(appStartup.trackStartupCrashBegin());
+ do_throw("Should have thrown since last_success is not set");
+ } catch (x) { }
+
+ do_check_false(prefService.prefHasUserValue(pref_last_success));
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // first run with startup crash tracking - no existing profile lock
+ replacedLockTime = 0;
+ resetTestEnv(replacedLockTime);
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(prefService.prefHasUserValue(pref_last_success));
+ do_check_eq(replacedLockTime, gAppInfo.replacedLockTime);
+ try {
+ do_check_false(appStartup.trackStartupCrashBegin());
+ do_throw("Should have thrown since last_success is not set");
+ } catch (x) { }
+
+ do_check_false(prefService.prefHasUserValue(pref_last_success));
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup - last startup was success
+ replacedLockTime = Date.now();
+ resetTestEnv(replacedLockTime);
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ do_check_eq(ms_to_s(replacedLockTime), prefService.getIntPref(pref_last_success));
+ do_check_false(appStartup.trackStartupCrashBegin());
+ do_check_eq(ms_to_s(replacedLockTime), prefService.getIntPref(pref_last_success));
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup with 1 recent crash
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_recent_crashes, 1);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ do_check_false(appStartup.trackStartupCrashBegin());
+ do_check_eq(ms_to_s(replacedLockTime), prefService.getIntPref(pref_last_success));
+ do_check_eq(1, prefService.getIntPref(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup with max_resumed_crashes crash
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_recent_crashes, max_resumed);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ do_check_false(appStartup.trackStartupCrashBegin());
+ do_check_eq(ms_to_s(replacedLockTime), prefService.getIntPref(pref_last_success));
+ do_check_eq(max_resumed, prefService.getIntPref(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup with too many recent crashes
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_recent_crashes, max_resumed + 1);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ do_check_true(appStartup.trackStartupCrashBegin());
+ // should remain the same since the last startup was not a crash
+ do_check_eq(max_resumed + 1, prefService.getIntPref(pref_recent_crashes));
+ do_check_true(appStartup.automaticSafeModeNecessary);
+
+ // normal startup with too many recent crashes and startup crash tracking disabled
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_max_resumed_crashes, -1);
+ prefService.setIntPref(pref_recent_crashes, max_resumed + 1);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ do_check_false(appStartup.trackStartupCrashBegin());
+ // should remain the same since the last startup was not a crash
+ do_check_eq(max_resumed + 1, prefService.getIntPref(pref_recent_crashes));
+ // returns false when disabled
+ do_check_false(appStartup.automaticSafeModeNecessary);
+ do_check_eq(-1, prefService.getIntPref(pref_max_resumed_crashes));
+
+ // normal startup after 1 non-recent crash (1 year ago), no other recent
+ replacedLockTime = Date.now() - 365 * 24 * 60 * 60 * 1000;
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 365 * 24 * 60 * 60);
+ do_check_false(appStartup.trackStartupCrashBegin());
+ // recent crash count pref should be unset since the last crash was not recent
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup after 1 crash (1 minute ago), no other recent
+ replacedLockTime = Date.now() - 60 * 1000;
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 60 * 60); // last success - 1 hour ago
+ do_check_false(appStartup.trackStartupCrashBegin());
+ // recent crash count pref should be created with value 1
+ do_check_eq(1, prefService.getIntPref(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup after another crash (1 minute ago), 1 already
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 60 * 60); // last success - 1 hour ago
+ replacedLockTime = Date.now() - 60 * 1000;
+ gAppInfo.replacedLockTime = replacedLockTime;
+ do_check_false(appStartup.trackStartupCrashBegin());
+ // recent crash count pref should be incremented by 1
+ do_check_eq(2, prefService.getIntPref(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // normal startup after another crash (1 minute ago), 2 already
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 60 * 60); // last success - 1 hour ago
+ do_check_true(appStartup.trackStartupCrashBegin());
+ // recent crash count pref should be incremented by 1
+ do_check_eq(3, prefService.getIntPref(pref_recent_crashes));
+ do_check_true(appStartup.automaticSafeModeNecessary);
+
+ // normal startup after 1 non-recent crash (1 year ago), 3 crashes already
+ replacedLockTime = Date.now() - 365 * 24 * 60 * 60 * 1000;
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 60 * 60); // last success - 1 hour ago
+ do_check_false(appStartup.trackStartupCrashBegin());
+ // recent crash count should be unset since the last crash was not recent
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+}
+
+function test_trackStartupCrashEnd() {
+ // successful startup with no last_success (upgrade test)
+ let replacedLockTime = Date.now() - 10 * 1000; // 10s ago
+ resetTestEnv(replacedLockTime);
+ try {
+ appStartup.trackStartupCrashBegin(); // required to be called before end
+ do_throw("Should have thrown since last_success is not set");
+ } catch (x) { }
+ appStartup.trackStartupCrashEnd();
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_false(prefService.prefHasUserValue(pref_last_success));
+
+ // successful startup - should set last_success
+ replacedLockTime = Date.now() - 10 * 1000; // 10s ago
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ appStartup.trackStartupCrashBegin(); // required to be called before end
+ appStartup.trackStartupCrashEnd();
+ // ensure last_success was set since we have declared a succesful startup
+ // main timestamp doesn't get set in XPCShell so approximate with now
+ do_check_true(prefService.getIntPref(pref_last_success) <= now_seconds());
+ do_check_true(prefService.getIntPref(pref_last_success) >= now_seconds() - 4 * 60 * 60);
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+
+ // successful startup with 1 recent crash
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ prefService.setIntPref(pref_recent_crashes, 1);
+ appStartup.trackStartupCrashBegin(); // required to be called before end
+ appStartup.trackStartupCrashEnd();
+ // ensure recent_crashes was cleared since we have declared a succesful startup
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+}
+
+function test_trackStartupCrashBegin_safeMode() {
+ gAppInfo.inSafeMode = true;
+ resetTestEnv(0);
+ let max_resumed = prefService.getIntPref(pref_max_resumed_crashes);
+
+ // check manual safe mode doesn't change prefs without crash
+ let replacedLockTime = Date.now() - 10 * 1000; // 10s ago
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_true(prefService.prefHasUserValue(pref_last_success));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+ do_check_false(appStartup.trackStartupCrashBegin());
+ do_check_false(prefService.prefHasUserValue(pref_recent_crashes));
+ do_check_true(prefService.prefHasUserValue(pref_last_success));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+
+ // check forced safe mode doesn't change prefs without crash
+ replacedLockTime = Date.now() - 10 * 1000; // 10s ago
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime));
+ prefService.setIntPref(pref_recent_crashes, max_resumed + 1);
+
+ do_check_eq(max_resumed + 1, prefService.getIntPref(pref_recent_crashes));
+ do_check_true(prefService.prefHasUserValue(pref_last_success));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+ do_check_true(appStartup.trackStartupCrashBegin());
+ do_check_eq(max_resumed + 1, prefService.getIntPref(pref_recent_crashes));
+ do_check_true(prefService.prefHasUserValue(pref_last_success));
+ do_check_true(appStartup.automaticSafeModeNecessary);
+
+ // check forced safe mode after old crash
+ replacedLockTime = Date.now() - 365 * 24 * 60 * 60 * 1000;
+ resetTestEnv(replacedLockTime);
+ // one year ago
+ let last_success = ms_to_s(replacedLockTime) - 365 * 24 * 60 * 60;
+ prefService.setIntPref(pref_last_success, last_success);
+ prefService.setIntPref(pref_recent_crashes, max_resumed + 1);
+ do_check_eq(max_resumed + 1, prefService.getIntPref(pref_recent_crashes));
+ do_check_true(prefService.prefHasUserValue(pref_last_success));
+ do_check_true(appStartup.automaticSafeModeNecessary);
+ do_check_true(appStartup.trackStartupCrashBegin());
+ do_check_eq(max_resumed + 1, prefService.getIntPref(pref_recent_crashes));
+ do_check_eq(last_success, prefService.getIntPref(pref_last_success));
+ do_check_true(appStartup.automaticSafeModeNecessary);
+}
+
+function test_trackStartupCrashEnd_safeMode() {
+ gAppInfo.inSafeMode = true;
+ let replacedLockTime = Date.now();
+ resetTestEnv(replacedLockTime);
+ let max_resumed = prefService.getIntPref(pref_max_resumed_crashes);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 24 * 60 * 60);
+
+ // ensure recent_crashes were not cleared in manual safe mode
+ prefService.setIntPref(pref_recent_crashes, 1);
+ appStartup.trackStartupCrashBegin(); // required to be called before end
+ appStartup.trackStartupCrashEnd();
+ do_check_eq(1, prefService.getIntPref(pref_recent_crashes));
+
+ // recent_crashes should be set to max_resumed in forced safe mode to allow the user
+ // to try and start in regular mode after making changes.
+ prefService.setIntPref(pref_recent_crashes, max_resumed + 1);
+ appStartup.trackStartupCrashBegin(); // required to be called before end
+ appStartup.trackStartupCrashEnd();
+ do_check_eq(max_resumed, prefService.getIntPref(pref_recent_crashes));
+}
+
+function test_maxResumed() {
+ resetTestEnv(0);
+ gAppInfo.inSafeMode = false;
+ let max_resumed = prefService.getIntPref(pref_max_resumed_crashes);
+ let replacedLockTime = Date.now();
+ resetTestEnv(replacedLockTime);
+ prefService.setIntPref(pref_max_resumed_crashes, -1);
+
+ prefService.setIntPref(pref_recent_crashes, max_resumed + 1);
+ prefService.setIntPref(pref_last_success, ms_to_s(replacedLockTime) - 24 * 60 * 60);
+ appStartup.trackStartupCrashBegin();
+ // should remain the same since the last startup was not a crash
+ do_check_eq(max_resumed + 2, prefService.getIntPref(pref_recent_crashes));
+ do_check_false(appStartup.automaticSafeModeNecessary);
+}
diff --git a/toolkit/components/startup/tests/unit/xpcshell.ini b/toolkit/components/startup/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..294800ee30
--- /dev/null
+++ b/toolkit/components/startup/tests/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head_startup.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_startup_crash.js]
diff --git a/toolkit/components/statusfilter/moz.build b/toolkit/components/statusfilter/moz.build
new file mode 100644
index 0000000000..68f4810a29
--- /dev/null
+++ b/toolkit/components/statusfilter/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/.
+
+SOURCES += [
+ 'nsBrowserStatusFilter.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
new file mode 100644
index 0000000000..f607e01b49
--- /dev/null
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
@@ -0,0 +1,392 @@
+/* -*- Mode: C++; tab-width: 8; 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 "nsBrowserStatusFilter.h"
+#include "nsIChannel.h"
+#include "nsITimer.h"
+#include "nsIServiceManager.h"
+#include "nsString.h"
+
+// XXX
+// XXX DO NOT TOUCH THIS CODE UNLESS YOU KNOW WHAT YOU'RE DOING !!!
+// XXX
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter <public>
+//-----------------------------------------------------------------------------
+
+nsBrowserStatusFilter::nsBrowserStatusFilter()
+ : mCurProgress(0)
+ , mMaxProgress(0)
+ , mStatusIsDirty(true)
+ , mCurrentPercentage(0)
+ , mTotalRequests(0)
+ , mFinishedRequests(0)
+ , mUseRealProgressFlag(false)
+ , mDelayedStatus(false)
+ , mDelayedProgress(false)
+{
+}
+
+nsBrowserStatusFilter::~nsBrowserStatusFilter()
+{
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsBrowserStatusFilter,
+ nsIWebProgress,
+ nsIWebProgressListener,
+ nsIWebProgressListener2,
+ nsISupportsWeakReference)
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter::nsIWebProgress
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::AddProgressListener(nsIWebProgressListener *aListener,
+ uint32_t aNotifyMask)
+{
+ mListener = aListener;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::RemoveProgressListener(nsIWebProgressListener *aListener)
+{
+ if (aListener == mListener)
+ mListener = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::GetDOMWindow(mozIDOMWindowProxy **aResult)
+{
+ NS_NOTREACHED("nsBrowserStatusFilter::GetDOMWindow");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::GetDOMWindowID(uint64_t *aResult)
+{
+ *aResult = 0;
+ NS_NOTREACHED("nsBrowserStatusFilter::GetDOMWindowID");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::GetIsTopLevel(bool *aIsTopLevel)
+{
+ *aIsTopLevel = false;
+ NS_NOTREACHED("nsBrowserStatusFilter::GetIsTopLevel");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::GetIsLoadingDocument(bool *aIsLoadingDocument)
+{
+ NS_NOTREACHED("nsBrowserStatusFilter::GetIsLoadingDocument");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::GetLoadType(uint32_t *aLoadType)
+{
+ *aLoadType = 0;
+ NS_NOTREACHED("nsBrowserStatusFilter::GetLoadType");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter::nsIWebProgressListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnStateChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus)
+{
+ if (!mListener)
+ return NS_OK;
+
+ if (aStateFlags & STATE_START) {
+ if (aStateFlags & STATE_IS_NETWORK) {
+ ResetMembers();
+ }
+ if (aStateFlags & STATE_IS_REQUEST) {
+ ++mTotalRequests;
+
+ // if the total requests exceeds 1, then we'll base our progress
+ // notifications on the percentage of completed requests.
+ // otherwise, progress for the single request will be reported.
+ mUseRealProgressFlag = (mTotalRequests == 1);
+ }
+ }
+ else if (aStateFlags & STATE_STOP) {
+ if (aStateFlags & STATE_IS_REQUEST) {
+ ++mFinishedRequests;
+ // Note: Do not return from here. This is necessary so that the
+ // STATE_STOP can still be relayed to the listener if needed
+ // (bug 209330)
+ if (!mUseRealProgressFlag && mTotalRequests)
+ OnProgressChange(nullptr, nullptr, 0, 0,
+ mFinishedRequests, mTotalRequests);
+ }
+ }
+ else if (aStateFlags & STATE_TRANSFERRING) {
+ if (aStateFlags & STATE_IS_REQUEST) {
+ if (!mUseRealProgressFlag && mTotalRequests)
+ return OnProgressChange(nullptr, nullptr, 0, 0,
+ mFinishedRequests, mTotalRequests);
+ }
+
+ // no need to forward this state change
+ return NS_OK;
+ } else {
+ // no need to forward this state change
+ return NS_OK;
+ }
+
+ // If we're here, we have either STATE_START or STATE_STOP. The
+ // listener only cares about these in certain conditions.
+ bool isLoadingDocument = false;
+ if ((aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK ||
+ (aStateFlags & nsIWebProgressListener::STATE_IS_REQUEST &&
+ mFinishedRequests == mTotalRequests &&
+ (aWebProgress->GetIsLoadingDocument(&isLoadingDocument),
+ !isLoadingDocument)))) {
+ if (mTimer && (aStateFlags & nsIWebProgressListener::STATE_STOP)) {
+ mTimer->Cancel();
+ ProcessTimeout();
+ }
+
+ return mListener->OnStateChange(aWebProgress, aRequest, aStateFlags,
+ aStatus);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress)
+{
+ if (!mListener)
+ return NS_OK;
+
+ if (!mUseRealProgressFlag && aRequest)
+ return NS_OK;
+
+ //
+ // limit frequency of calls to OnProgressChange
+ //
+
+ mCurProgress = (int64_t)aCurTotalProgress;
+ mMaxProgress = (int64_t)aMaxTotalProgress;
+
+ if (mDelayedProgress)
+ return NS_OK;
+
+ if (!mDelayedStatus) {
+ MaybeSendProgress();
+ StartDelayTimer();
+ }
+
+ mDelayedProgress = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsIURI *aLocation,
+ uint32_t aFlags)
+{
+ if (!mListener)
+ return NS_OK;
+
+ return mListener->OnLocationChange(aWebProgress, aRequest, aLocation,
+ aFlags);
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnStatusChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsresult aStatus,
+ const char16_t *aMessage)
+{
+ if (!mListener)
+ return NS_OK;
+
+ //
+ // limit frequency of calls to OnStatusChange
+ //
+ if (mStatusIsDirty || !mCurrentStatusMsg.Equals(aMessage)) {
+ mStatusIsDirty = true;
+ mStatusMsg = aMessage;
+ }
+
+ if (mDelayedStatus)
+ return NS_OK;
+
+ if (!mDelayedProgress) {
+ MaybeSendStatus();
+ StartDelayTimer();
+ }
+
+ mDelayedStatus = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnSecurityChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ uint32_t aState)
+{
+ if (!mListener)
+ return NS_OK;
+
+ return mListener->OnSecurityChange(aWebProgress, aRequest, aState);
+}
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter::nsIWebProgressListener2
+//-----------------------------------------------------------------------------
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnProgressChange64(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ int64_t aCurSelfProgress,
+ int64_t aMaxSelfProgress,
+ int64_t aCurTotalProgress,
+ int64_t aMaxTotalProgress)
+{
+ // XXX truncates 64-bit to 32-bit
+ return OnProgressChange(aWebProgress, aRequest,
+ (int32_t)aCurSelfProgress,
+ (int32_t)aMaxSelfProgress,
+ (int32_t)aCurTotalProgress,
+ (int32_t)aMaxTotalProgress);
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnRefreshAttempted(nsIWebProgress *aWebProgress,
+ nsIURI *aUri,
+ int32_t aDelay,
+ bool aSameUri,
+ bool *allowRefresh)
+{
+ nsCOMPtr<nsIWebProgressListener2> listener =
+ do_QueryInterface(mListener);
+ if (!listener) {
+ *allowRefresh = true;
+ return NS_OK;
+ }
+
+ return listener->OnRefreshAttempted(aWebProgress, aUri, aDelay, aSameUri,
+ allowRefresh);
+}
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter <private>
+//-----------------------------------------------------------------------------
+
+void
+nsBrowserStatusFilter::ResetMembers()
+{
+ mTotalRequests = 0;
+ mFinishedRequests = 0;
+ mUseRealProgressFlag = false;
+ mMaxProgress = 0;
+ mCurProgress = 0;
+ mCurrentPercentage = 0;
+ mStatusIsDirty = true;
+}
+
+void
+nsBrowserStatusFilter::MaybeSendProgress()
+{
+ if (mCurProgress > mMaxProgress || mCurProgress <= 0)
+ return;
+
+ // check our percentage
+ int32_t percentage = (int32_t) double(mCurProgress) * 100 / mMaxProgress;
+
+ // The progress meter only updates for increases greater than 3 percent
+ if (percentage > (mCurrentPercentage + 3)) {
+ mCurrentPercentage = percentage;
+ // XXX truncates 64-bit to 32-bit
+ mListener->OnProgressChange(nullptr, nullptr, 0, 0,
+ (int32_t)mCurProgress,
+ (int32_t)mMaxProgress);
+ }
+}
+
+void
+nsBrowserStatusFilter::MaybeSendStatus()
+{
+ if (mStatusIsDirty) {
+ mListener->OnStatusChange(nullptr, nullptr, NS_OK, mStatusMsg.get());
+ mCurrentStatusMsg = mStatusMsg;
+ mStatusIsDirty = false;
+ }
+}
+
+nsresult
+nsBrowserStatusFilter::StartDelayTimer()
+{
+ NS_ASSERTION(!DelayInEffect(), "delay should not be in effect");
+
+ mTimer = do_CreateInstance("@mozilla.org/timer;1");
+ if (!mTimer)
+ return NS_ERROR_FAILURE;
+
+ return mTimer->InitWithNamedFuncCallback(
+ TimeoutHandler, this, 160, nsITimer::TYPE_ONE_SHOT,
+ "nsBrowserStatusFilter::TimeoutHandler");
+}
+
+void
+nsBrowserStatusFilter::ProcessTimeout()
+{
+ mTimer = nullptr;
+
+ if (!mListener)
+ return;
+
+ if (mDelayedStatus) {
+ mDelayedStatus = false;
+ MaybeSendStatus();
+ }
+
+ if (mDelayedProgress) {
+ mDelayedProgress = false;
+ MaybeSendProgress();
+ }
+}
+
+void
+nsBrowserStatusFilter::TimeoutHandler(nsITimer *aTimer, void *aClosure)
+{
+ nsBrowserStatusFilter *self = reinterpret_cast<nsBrowserStatusFilter *>(aClosure);
+ if (!self) {
+ NS_ERROR("no self");
+ return;
+ }
+
+ self->ProcessTimeout();
+}
diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.h b/toolkit/components/statusfilter/nsBrowserStatusFilter.h
new file mode 100644
index 0000000000..0eb6364fde
--- /dev/null
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.h
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsBrowserStatusFilter_h__
+#define nsBrowserStatusFilter_h__
+
+#include "nsIWebProgressListener.h"
+#include "nsIWebProgressListener2.h"
+#include "nsIWebProgress.h"
+#include "nsWeakReference.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+//-----------------------------------------------------------------------------
+// nsBrowserStatusFilter - a web progress listener implementation designed to
+// sit between nsDocLoader and nsBrowserStatusHandler to filter out and limit
+// the frequency of certain events to improve page load performance.
+//-----------------------------------------------------------------------------
+
+class nsBrowserStatusFilter : public nsIWebProgress
+ , public nsIWebProgressListener2
+ , public nsSupportsWeakReference
+{
+public:
+ nsBrowserStatusFilter();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWEBPROGRESS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIWEBPROGRESSLISTENER2
+
+protected:
+ virtual ~nsBrowserStatusFilter();
+
+private:
+ nsresult StartDelayTimer();
+ void ProcessTimeout();
+ void MaybeSendProgress();
+ void MaybeSendStatus();
+ void ResetMembers();
+ bool DelayInEffect() { return mDelayedStatus || mDelayedProgress; }
+
+ static void TimeoutHandler(nsITimer *aTimer, void *aClosure);
+
+private:
+ nsCOMPtr<nsIWebProgressListener> mListener;
+ nsCOMPtr<nsITimer> mTimer;
+
+ // delayed values
+ nsString mStatusMsg;
+ int64_t mCurProgress;
+ int64_t mMaxProgress;
+
+ nsString mCurrentStatusMsg;
+ bool mStatusIsDirty;
+ int32_t mCurrentPercentage;
+
+ // used to convert OnStart/OnStop notifications into progress notifications
+ int32_t mTotalRequests;
+ int32_t mFinishedRequests;
+ bool mUseRealProgressFlag;
+
+ // indicates whether a timeout is pending
+ bool mDelayedStatus;
+ bool mDelayedProgress;
+};
+
+#define NS_BROWSERSTATUSFILTER_CONTRACTID \
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+#define NS_BROWSERSTATUSFILTER_CID \
+{ /* 6356aa16-7916-4215-a825-cbc2692ca87a */ \
+ 0x6356aa16, \
+ 0x7916, \
+ 0x4215, \
+ {0xa8, 0x25, 0xcb, 0xc2, 0x69, 0x2c, 0xa8, 0x7a} \
+}
+
+#endif // !nsBrowserStatusFilter_h__
diff --git a/toolkit/components/telemetry/EventInfo.h b/toolkit/components/telemetry/EventInfo.h
new file mode 100644
index 0000000000..b8934e2c47
--- /dev/null
+++ b/toolkit/components/telemetry/EventInfo.h
@@ -0,0 +1,52 @@
+/* -*- 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 TelemetryEventInfo_h__
+#define TelemetryEventInfo_h__
+
+// This module is internal to Telemetry. The structures here hold data that
+// describe events.
+// It should only be used by TelemetryEventData.h and TelemetryEvent.cpp.
+//
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+struct CommonEventInfo {
+ // Indices for the category and expiration strings.
+ uint32_t category_offset;
+ uint32_t expiration_version_offset;
+
+ // The index and count for the extra key offsets in the extra table.
+ uint32_t extra_index;
+ uint32_t extra_count;
+
+ // The day since UNIX epoch that this probe expires on.
+ uint32_t expiration_day;
+
+ // The dataset this event is recorded in.
+ uint32_t dataset;
+
+ // Convenience functions for accessing event strings.
+ const char* expiration_version() const;
+ const char* category() const;
+ const char* extra_key(uint32_t index) const;
+};
+
+struct EventInfo {
+ // The corresponding CommonEventInfo.
+ const CommonEventInfo& common_info;
+
+ // Indices for the method & object strings.
+ uint32_t method_offset;
+ uint32_t object_offset;
+
+ const char* method() const;
+ const char* object() const;
+};
+
+} // namespace
+
+#endif // TelemetryEventInfo_h__
diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml
new file mode 100644
index 0000000000..750a139148
--- /dev/null
+++ b/toolkit/components/telemetry/Events.yaml
@@ -0,0 +1,68 @@
+navigation:
+- methods: ["search"]
+ objects: ["about_home", "about_newtab", "contextmenu", "oneoff",
+ "suggestion", "alias", "enter", "searchbar", "urlbar"]
+ release_channel_collection: opt-in
+ description: >
+ This is recorded on each search navigation.
+ The value field records the action used to trigger the search:
+ "enter", "oneoff", "suggestion", "alias", null (for contextmenu)
+ bug_numbers: [1316281]
+ notification_emails: ["past@mozilla.com"]
+ expiry_version: "58.0"
+ extra_keys:
+ engine: The id of the search engine used.
+
+# This category contains event entries used for Telemetry tests.
+# They will not be sent out with any pings.
+telemetry.test:
+- methods: ["test1", "test2"]
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is a test entry for Telemetry.
+ expiry_date: never
+ extra_keys:
+ key1: This is just a test description.
+ key2: This is another test description.
+- methods: ["optout"]
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is an opt-out test entry.
+ expiry_date: never
+ release_channel_collection: opt-out
+ extra_keys:
+ key1: This is just a test description.
+- methods: ["expired_version"]
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is a test entry with an expired version.
+ expiry_version: "3.6"
+- methods: ["expired_date"]
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is a test entry with an expired date.
+ expiry_date: 2014-01-28
+- methods: ["not_expired_optout"]
+ objects: ["object1"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is an opt-out test entry with unexpired date and version.
+ release_channel_collection: opt-out
+ expiry_date: 2099-01-01
+ expiry_version: "999.0"
+
+# This is a secondary category used for Telemetry tests.
+# The events here will not be sent out with any pings.
+telemetry.test.second:
+- methods: ["test"]
+ objects: ["object1", "object2", "object3"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is a test entry for Telemetry.
+ expiry_date: never
+ extra_keys:
+ key1: This is just a test description.
diff --git a/toolkit/components/telemetry/GCTelemetry.jsm b/toolkit/components/telemetry/GCTelemetry.jsm
new file mode 100644
index 0000000000..43a4ea9ca7
--- /dev/null
+++ b/toolkit/components/telemetry/GCTelemetry.jsm
@@ -0,0 +1,216 @@
+/* -*- js-indent-level: 2; 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/. */
+
+"use strict";
+
+/**
+ * This module records detailed timing information about selected
+ * GCs. The data is sent back in the telemetry session ping. To avoid
+ * bloating the ping, only a few GCs are included. There are two
+ * selection strategies. We always save the five GCs with the worst
+ * max_pause time. Additionally, five collections are selected at
+ * random. If a GC runs for C milliseconds and the total time for all
+ * GCs since the session began is T milliseconds, then the GC has a
+ * 5*C/T probablility of being selected (the factor of 5 is because we
+ * save 5 of them).
+ *
+ * GCs from both the main process and all content processes are
+ * recorded. The data is cleared for each new subsession.
+ */
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+this.EXPORTED_SYMBOLS = ["GCTelemetry"];
+
+// Names of processes where we record GCs.
+const PROCESS_NAMES = ["main", "content"];
+
+// Should be the time we started up in milliseconds since the epoch.
+const BASE_TIME = Date.now() - Services.telemetry.msSinceProcessStart();
+
+// Records selected GCs. There is one instance per process type.
+class GCData {
+ constructor(kind) {
+ let numRandom = {main: 0, content: 2};
+ let numWorst = {main: 2, content: 2};
+
+ this.totalGCTime = 0;
+ this.randomlySelected = Array(numRandom[kind]).fill(null);
+ this.worst = Array(numWorst[kind]).fill(null);
+ }
+
+ // Turn absolute timestamps (in microseconds since the epoch) into
+ // milliseconds since startup.
+ rebaseTimes(data) {
+ function fixup(t) {
+ return t / 1000.0 - BASE_TIME;
+ }
+
+ data.timestamp = fixup(data.timestamp);
+
+ for (let i = 0; i < data.slices.length; i++) {
+ let slice = data.slices[i];
+ slice.start_timestamp = fixup(slice.start_timestamp);
+ slice.end_timestamp = fixup(slice.end_timestamp);
+ }
+ }
+
+ // Records a GC (represented by |data|) in the randomlySelected or
+ // worst batches depending on the criteria above.
+ record(data) {
+ this.rebaseTimes(data);
+
+ let time = data.total_time;
+ this.totalGCTime += time;
+
+ // Probability that we will replace any one of our
+ // current randomlySelected GCs with |data|.
+ let prob = time / this.totalGCTime;
+
+ // Note that we may replace multiple GCs in
+ // randomlySelected. It's easier to reason about the
+ // probabilities this way, and it's unlikely to have any effect in
+ // practice.
+ for (let i = 0; i < this.randomlySelected.length; i++) {
+ let r = Math.random();
+ if (r <= prob) {
+ this.randomlySelected[i] = data;
+ }
+ }
+
+ // Save the 5 worst GCs based on max_pause. A GC may appear in
+ // both worst and randomlySelected.
+ for (let i = 0; i < this.worst.length; i++) {
+ if (!this.worst[i]) {
+ this.worst[i] = data;
+ break;
+ }
+
+ if (this.worst[i].max_pause < data.max_pause) {
+ this.worst.splice(i, 0, data);
+ this.worst.length--;
+ break;
+ }
+ }
+ }
+
+ entries() {
+ return {
+ random: this.randomlySelected.filter(e => e !== null),
+ worst: this.worst.filter(e => e !== null),
+ };
+ }
+}
+
+// If you adjust any of the constants here (slice limit, number of keys, etc.)
+// make sure to update the JSON schema at:
+// https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/telemetry/main.schema.json
+// You should also adjust browser_TelemetryGC.js.
+const MAX_GC_KEYS = 25;
+const MAX_SLICES = 4;
+const MAX_SLICE_KEYS = 15;
+const MAX_PHASES = 65;
+
+function limitProperties(obj, count) {
+ // If there are too many properties, just delete them all. We don't
+ // expect this ever to happen.
+ if (Object.keys(obj).length > count) {
+ for (let key of Object.keys(obj)) {
+ delete obj[key];
+ }
+ }
+}
+
+function limitSize(data) {
+ // Store the number of slices so we know if we lost any at the end.
+ data.num_slices = data.slices.length;
+
+ data.slices.sort((a, b) => b.pause - a.pause);
+
+ if (data.slices.length > MAX_SLICES) {
+ // Make sure we always keep the first slice since it has the
+ // reason the GC was started.
+ let firstSliceIndex = data.slices.findIndex(s => s.slice == 0);
+ if (firstSliceIndex >= MAX_SLICES) {
+ data.slices[MAX_SLICES - 1] = data.slices[firstSliceIndex];
+ }
+
+ data.slices.length = MAX_SLICES;
+ }
+
+ data.slices.sort((a, b) => a.slice - b.slice);
+
+ limitProperties(data, MAX_GC_KEYS);
+
+ for (let slice of data.slices) {
+ limitProperties(slice, MAX_SLICE_KEYS);
+ limitProperties(slice.times, MAX_PHASES);
+ }
+
+ limitProperties(data.totals, MAX_PHASES);
+}
+
+let processData = new Map();
+for (let name of PROCESS_NAMES) {
+ processData.set(name, new GCData(name));
+}
+
+var GCTelemetry = {
+ initialized: false,
+
+ init() {
+ if (this.initialized) {
+ return false;
+ }
+
+ this.initialized = true;
+ Services.obs.addObserver(this, "garbage-collection-statistics", false);
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.ppmm.addMessageListener("Telemetry:GCStatistics", this);
+ }
+
+ return true;
+ },
+
+ shutdown() {
+ if (!this.initialized) {
+ return;
+ }
+
+ Services.obs.removeObserver(this, "garbage-collection-statistics");
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Services.ppmm.removeMessageListener("Telemetry:GCStatistics", this);
+ }
+ this.initialized = false;
+ },
+
+ observe(subject, topic, arg) {
+ let data = JSON.parse(arg);
+
+ limitSize(data);
+
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ processData.get("main").record(data);
+ } else {
+ Services.cpmm.sendAsyncMessage("Telemetry:GCStatistics", data);
+ }
+ },
+
+ receiveMessage(msg) {
+ processData.get("content").record(msg.data);
+ },
+
+ entries(kind, clear) {
+ let result = processData.get(kind).entries();
+ if (clear) {
+ processData.set(kind, new GCData(kind));
+ }
+ return result;
+ },
+};
diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json
new file mode 100644
index 0000000000..aa66fbe147
--- /dev/null
+++ b/toolkit/components/telemetry/Histograms.json
@@ -0,0 +1,11002 @@
+
+{
+ "A11Y_INSTANTIATED_FLAG": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "has accessibility support been instantiated"
+ },
+ "A11Y_CONSUMERS": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 11,
+ "description": "Accessibility client by enum id"
+ },
+ "A11Y_ISIMPLEDOM_USAGE_FLAG": {
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "have the ISimpleDOM* accessibility interfaces been used"
+ },
+ "A11Y_IATABLE_USAGE_FLAG": {
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "has the IAccessibleTable accessibility interface been used"
+ },
+ "A11Y_UPDATE_TIME": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "time spent updating accessibility (ms)"
+ },
+ "ADDON_MANAGER_UPGRADE_UI_SHOWN": {
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Recorded when the addon manager shows the modal upgrade UI. Should only be recorded once per upgrade.",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1268548],
+ "alert_emails": ["kev@mozilla.com"]
+ },
+ "ADDON_SHIM_USAGE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "keyed": true,
+ "description": "Reasons why add-on shims were used, keyed by add-on ID."
+ },
+ "ADDON_FORBIDDEN_CPOW_USAGE": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "Counts the number of times a given add-on used CPOWs when it was marked as e10s compatible.",
+ "bug_numbers": [1214824],
+ "alert_emails": ["wmccloskey@mozilla.com"]
+ },
+ "BROWSER_SHIM_USAGE_BLOCKED": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Counts the number of times a CPOW shim was blocked from being created by browser code.",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1245901],
+ "alert_emails": ["benjamin@smedbergs.us"]
+ },
+ "APPLICATION_REPUTATION_SHOULD_BLOCK": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Overall (local or remote) application reputation verdict (shouldBlock=false is OK)."
+ },
+ "APPLICATION_REPUTATION_LOCAL": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Application reputation local results (0=ALLOW, 1=BLOCK, 2=NONE)"
+ },
+ "APPLICATION_REPUTATION_SERVER": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Status of the application reputation remote lookup (0=OK, 1=failed, 2=invalid protobuf response)"
+ },
+ "APPLICATION_REPUTATION_SERVER_VERDICT": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "56",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1272788],
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Application reputation remote verdict (0=SAFE, 1=DANGEROUS, 2=UNCOMMON, 3=POTENTIALLY_UNWANTED, 4=DANGEROUS_HOST, 5=UNKNOWN)"
+ },
+ "APPLICATION_REPUTATION_COUNT": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Application reputation query count (both local and remote)"
+ },
+ "APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "56",
+ "kind": "boolean",
+ "bug_numbers": [1172689],
+ "description": "Recorded when application reputation remote lookup is performed, `true` is recorded if the lookup times out."
+ },
+ "AUDIOSTREAM_FIRST_OPEN_MS": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "The length of time (in milliseconds) for the first open of AudioStream."
+ },
+ "AUDIOSTREAM_LATER_OPEN_MS": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "The length of time (in milliseconds) for the subsequent opens of AudioStream."
+ },
+ "AUDIOSTREAM_BACKEND_USED": {
+ "alert_emails": ["padenot@mozilla.com", "kinetik@flim.org"],
+ "bug_numbers": [1280630],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "The operating system audio back-end used when successfully opening an audio stream, or whether the failure occurred on the first try or not <https://dxr.mozilla.org/mozilla-central/search?q=AUDIOSTREAM_BACKEND_ID_STR>",
+ "releaseChannelCollection": "opt-out"
+ },
+ "AUSHELPER_CPU_ERROR_CODE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1296630],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 16,
+ "releaseChannelCollection": "opt-out",
+ "description": "The error code from the aushelper system add-on when querying the registry for CPU information for bug 1296630 (see browser/extensions/aushelper/bootstrap.js)."
+ },
+ "AUSHELPER_CPU_RESULT_CODE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1296630],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 5,
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether the system is affected by bug 1296630 (1=No, 2=Yes, 3=Error, and 4=Unknown)."
+ },
+ "AUSHELPER_WEBSENSE_ERROR_CODE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1305847],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 8,
+ "releaseChannelCollection": "opt-out",
+ "description": "The error code from the aushelper system add-on when gathering information on Websense (see browser/extensions/aushelper/bootstrap.js)."
+ },
+ "AUSHELPER_WEBSENSE_REG_EXISTS": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1305847],
+ "expires_in_version": "60",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether the system has a Websense InstallVersion registry value (see browser/extensions/aushelper/bootstrap.js)."
+ },
+ "BACKGROUNDFILESAVER_THREAD_COUNT": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 21,
+ "description": "Maximum number of concurrent threads reached during a given download session"
+ },
+ "BLOCKLIST_SYNC_FILE_LOAD": {
+ "alert_emails": ["rvitillo@mozilla.com"],
+ "expires_in_version": "35",
+ "kind": "boolean",
+ "description": "blocklist.xml has been loaded synchronously *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "CHECKERBOARD_DURATION": {
+ "alert_emails": ["kgupta@mozilla.com"],
+ "bug_numbers": [1238040],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Duration of a checkerboard event in milliseconds"
+ },
+ "CHECKERBOARD_PEAK": {
+ "alert_emails": ["kgupta@mozilla.com"],
+ "bug_numbers": [1238040],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 66355200,
+ "n_buckets": 50,
+ "description": "Peak number of CSS pixels checkerboarded during a checkerboard event (the high value is the size of a 4k display with max APZ zooming)"
+ },
+ "CHECKERBOARD_POTENTIAL_DURATION": {
+ "alert_emails": ["kgupta@mozilla.com"],
+ "bug_numbers": [1238040],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "Duration of a chunk of time (in ms) that could reasonably have had checkerboarding"
+ },
+ "CHECKERBOARD_SEVERITY": {
+ "alert_emails": ["kgupta@mozilla.com"],
+ "bug_numbers": [1238040],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 1073741824,
+ "n_buckets": 50,
+ "description": "Opaque measure of the severity of a checkerboard event"
+ },
+ "COMPOSITE_TIME" : {
+ "expires_in_version": "never",
+ "description": "Composite times in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "COMPOSITE_FRAME_ROUNDTRIP_TIME" : {
+ "expires_in_version": "never",
+ "description": "Time from vsync to finishing a composite in milliseconds.",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "CONTENT_RESPONSE_DURATION" : {
+ "alert_emails": ["kgupta@mozilla.com"],
+ "bug_numbers": [1261373],
+ "expires_in_version": "55",
+ "description": "Main thread response times for APZ notifications about input events (ms)",
+ "kind" : "exponential",
+ "high": 60000,
+ "n_buckets": 50
+ },
+ "CREATE_EVENT_BEFOREUNLOADEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"beforeunloadevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_COMMANDEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"commandevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_COMMANDEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"commandevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_COMPOSITIONEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"compositionevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_CUSTOMEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"customevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_DATACONTAINEREVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"datacontainerevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_DATACONTAINEREVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"datacontainerevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_DEVICEMOTIONEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"devicemotionevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_DEVICEORIENTATIONEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"deviceorientationevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_DRAGEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"dragevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_DRAGEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"dragevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_EVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"event\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_EVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"events\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_HASHCHANGEEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"hashchangeevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_HTMLEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"htmlevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_KEYBOARDEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"keyboardevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_KEYEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"keyevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_MESSAGEEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"messageevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_MOUSEEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"mouseevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_MOUSEEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"mouseevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_MOUSESCROLLEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"mousescrollevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_MUTATIONEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"mutationevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_MUTATIONEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"mutationevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_NOTIFYPAINTEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"notifypaintevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_PAGETRANSITION" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"pagetransition\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_POPSTATEEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"popstateevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_POPUPEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"popupevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_SCROLLAREAEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"scrollareaevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_SIMPLEGESTUREEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"simplegestureevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_STORAGEEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"storageevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_SVGEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"svgevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_SVGEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"svgevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_SVGZOOMEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"svgzoomevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_SVGZOOMEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"svgzoomevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_TEXTEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"textevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_TEXTEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"textevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_TIMEEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"timeevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_TIMEEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"timeevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_TOUCHEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"touchevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_UIEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"uievent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_UIEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"uievents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_XULCOMMANDEVENT" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"xulcommandevent\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CREATE_EVENT_XULCOMMANDEVENTS" : {
+ "alert_emails": ["ayg@aryeh.name"],
+ "description": "Was document.createEvent(\"xulcommandevents\") ever called",
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1295588, 1251198]
+ },
+ "CYCLE_COLLECTOR": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent on one cycle collection (ms)"
+ },
+ "CYCLE_COLLECTOR_WORKER": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent on one cycle collection in a worker (ms)"
+ },
+ "CYCLE_COLLECTOR_FULL": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Full pause time for one cycle collection, including preparation (ms)"
+ },
+ "CYCLE_COLLECTOR_MAX_PAUSE": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Longest pause for an individual slice of one cycle collection, including preparation (ms)"
+ },
+ "CYCLE_COLLECTOR_FINISH_IGC": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Cycle collection finished an incremental GC"
+ },
+ "CYCLE_COLLECTOR_SYNC_SKIPPABLE": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Cycle collection synchronously ran forget skippable"
+ },
+ "CYCLE_COLLECTOR_VISITED_REF_COUNTED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of ref counted objects visited by the cycle collector"
+ },
+ "CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of ref counted objects visited by the cycle collector in a worker"
+ },
+ "CYCLE_COLLECTOR_VISITED_GCED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of JS objects visited by the cycle collector"
+ },
+ "CYCLE_COLLECTOR_WORKER_VISITED_GCED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of JS objects visited by the cycle collector in a worker"
+ },
+ "CYCLE_COLLECTOR_COLLECTED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Number of objects collected by the cycle collector"
+ },
+ "CYCLE_COLLECTOR_WORKER_COLLECTED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Number of objects collected by the cycle collector in a worker"
+ },
+ "CYCLE_COLLECTOR_NEED_GC": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Needed garbage collection before cycle collection."
+ },
+ "CYCLE_COLLECTOR_WORKER_NEED_GC": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Needed garbage collection before cycle collection in a worker."
+ },
+ "CYCLE_COLLECTOR_TIME_BETWEEN": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120,
+ "n_buckets": 50,
+ "description": "Time spent in between cycle collections (seconds)"
+ },
+ "CYCLE_COLLECTOR_OOM": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the cycle collector ran out of memory at some point"
+ },
+ "CYCLE_COLLECTOR_WORKER_OOM": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the cycle collector in a worker ran out of memory at some point"
+ },
+ "CYCLE_COLLECTOR_ASYNC_SNOW_WHITE_FREEING": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent on one asynchronous SnowWhite freeing (ms)"
+ },
+ "DEFERRED_FINALIZE_ASYNC": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Pause time for asynchronous deferred finalization (ms)"
+ },
+ "DEVICE_RESET_REASON": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "GPU Device Reset Reason (ok, hung, removed, reset, internal error, invalid call, out of memory)"
+ },
+ "FAMILY_SAFETY": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 16,
+ "bug_numbers": [1239166],
+ "description": "Status of Family Safety detection and remediation. See nsNSSComponent.cpp."
+ },
+ "FETCH_IS_MAINTHREAD": {
+ "expires_in_version": "50",
+ "kind": "boolean",
+ "description": "Was Fetch request initiated from the main thread?"
+ },
+ "FORCED_DEVICE_RESET_REASON": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "GPU Forced Device Reset Reason (OpenSharedHandle)"
+ },
+ "FORGET_SKIPPABLE_MAX": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Max time spent on one forget skippable (ms)"
+ },
+ "FULLSCREEN_TRANSITION_BLACK_MS": {
+ "alert_emails": ["xquan@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 5000,
+ "n_buckets": 50,
+ "bug_numbers": [1271160],
+ "description": "The time spent in the fully-black screen in fullscreen transition"
+ },
+ "FULLSCREEN_CHANGE_MS": {
+ "alert_emails": ["xquan@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 5000,
+ "n_buckets": 50,
+ "bug_numbers": [1271160],
+ "description": "The time content uses to enter/exit fullscreen regardless of fullscreen transition timeout"
+ },
+ "GC_REASON_2": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "Reason (enum value) for initiating a GC"
+ },
+ "GC_IS_COMPARTMENTAL": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Is it a zone GC?"
+ },
+ "GC_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent running JS GC (ms)"
+ },
+ "GC_BUDGET_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "Requested GC slice budget (ms)"
+ },
+ "GC_ANIMATION_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent running JS GC when animating (ms)"
+ },
+ "GC_MAX_PAUSE_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Longest GC slice in a GC (ms)"
+ },
+ "GC_MARK_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent running JS GC mark phase (ms)"
+ },
+ "GC_SWEEP_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent running JS GC sweep phase (ms)"
+ },
+ "GC_COMPACT_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent running JS GC compact phase (ms)"
+ },
+ "GC_MARK_ROOTS_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Time spent marking GC roots (ms)"
+ },
+ "GC_MARK_GRAY_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Time spent marking gray GC objects (ms)"
+ },
+ "GC_SLICE_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent running a JS GC slice (ms)"
+ },
+ "GC_SLOW_PHASE": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 75,
+ "description": "The longest phase in any slice that goes over 2x the budget"
+ },
+ "GC_MMU_50": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 20,
+ "description": "Minimum percentage of time spent outside GC over any 50ms window"
+ },
+ "GC_RESET": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Was an incremental GC canceled?"
+ },
+ "GC_RESET_REASON": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Reason for cancelling an ongoing GC (see js::gc::AbortReason)",
+ "bug_numbers": [1308116]
+ },
+ "GC_INCREMENTAL_DISABLED": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Is incremental GC permanently disabled?"
+ },
+ "GC_NON_INCREMENTAL": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Was the GC non-incremental?"
+ },
+ "GC_NON_INCREMENTAL_REASON": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Reason for performing a non-incremental GC (see js::gc::AbortReason)",
+ "bug_numbers": [1308116]
+ },
+ "GC_SCC_SWEEP_TOTAL_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Time spent sweeping compartment SCCs (ms)"
+ },
+ "GC_SCC_SWEEP_MAX_PAUSE_MS": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Time spent sweeping slowest compartment SCC (ms)"
+ },
+ "GC_MINOR_REASON": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "Reason (enum value) for initiating a minor GC"
+ },
+ "GC_MINOR_REASON_LONG": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "Reason (enum value) that caused a long (>1ms) minor GC"
+ },
+ "GC_MINOR_US": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 100,
+ "description": "Time spent running JS minor GC (us)"
+ },
+ "GC_NURSERY_BYTES": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 16777216,
+ "n_buckets": 16,
+ "bug_numbers": [1259347],
+ "description": "Size of the GC nursery (bytes)"
+ },
+ "GC_PRETENURE_COUNT": {
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "kind": "enumerated",
+ "n_values": 32,
+ "bug_numbers": [1293262],
+ "description": "How many objects groups were selected for pretenuring by a minor GC"
+ },
+ "GEOLOCATION_ACCURACY_EXPONENTIAL": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Location accuracy"
+ },
+ "GEOLOCATION_ERROR": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Has seen location error"
+ },
+ "GEOLOCATION_GETCURRENTPOSITION_SECURE_ORIGIN" : {
+ "expires_in_version" : "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1230209],
+ "description" : "Number of navigator.geolocation.getCurrentPosition() calls (0=other, 1=http, 2=https)"
+ },
+ "GEOLOCATION_REQUEST_GRANTED": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 20,
+ "bug_numbers": [1230209],
+ "description": "Geolocation requests either granted or denied (0=denied/other, 1=denied/http, 2=denied/https, ..., 10=granted/other, 11=granted/http, 12=granted/https)"
+ },
+ "GEOLOCATION_WATCHPOSITION_SECURE_ORIGIN" : {
+ "expires_in_version" : "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1230209],
+ "description" : "Number of navigator.geolocation.watchPosition() calls (0=other, 1=http, 2=https)"
+ },
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Geolocation on Win8 is either MLS or native"
+ },
+ "GEOLOCATION_OSX_SOURCE_IS_MLS": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Geolocation on OS X is either MLS or CoreLocation"
+ },
+ "GEOLOCATION_GETCURRENTPOSITION_VISIBLE": {
+ "alert_emails": ["michelangelo@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1255198],
+ "description": "This metric is recorded every time a navigator.geolocation.getCurrentPosition() request gets allowed/fulfilled. A false value is recorded if the owner is not visible according to document.isVisible."
+ },
+ "GEOLOCATION_WATCHPOSITION_VISIBLE": {
+ "alert_emails": ["michelangelo@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1255198],
+ "description": "This metric is recorded every time a navigator.geolocation.watchPosition() request gets allowed/fulfilled. A false value is recorded if the owner is not visible according to document.isVisible."
+ },
+ "GPU_PROCESS_LAUNCH_TIME_MS" : {
+ "alert_emails": ["george@mozilla.com", "danderson@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1297790],
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "GPU process launch time in milliseconds"
+ },
+ "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT": {
+ "alert_emails": ["jdemooij@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Use of SpiderMonkey's deprecated language extensions in web content: ForEach=0, DestructuringForIn=1 (obsolete), LegacyGenerator=2, ExpressionClosure=3, LetBlock=4 (obsolete), LetExpression=5 (obsolete), NoSuchMethod=6 (obsolete), FlagsArgument=7 (obsolete), RegExpSourceProp=8 (obsolete), RestoredRegExpStatics=9 (obsolete), BlockScopeFunRedecl=10"
+ },
+ "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_ADDONS": {
+ "alert_emails": ["jdemooij@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Use of SpiderMonkey's deprecated language extensions in add-ons: ForEach=0, DestructuringForIn=1 (obsolete), LegacyGenerator=2, ExpressionClosure=3, LetBlock=4 (obsolete), LetExpression=5 (obsolete), NoSuchMethod=6 (obsolete), FlagsArgument=7 (obsolete), RegExpSourceProp=8 (obsolete), RestoredRegExpStatics=9 (obsolete), BlockScopeFunRedecl=10"
+ },
+ "XUL_CACHE_DISABLED": {
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "XUL cache was disabled"
+ },
+ "MEMORY_RESIDENT_FAST": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "bug_numbers": [1226196],
+ "description": "Resident memory size (KB)"
+ },
+ "MEMORY_TOTAL": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1198209],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Total Memory Across All Processes (KB)"
+ },
+ "MEMORY_UNIQUE": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1198209],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Unique Set Size (KB)"
+ },
+ "MEMORY_VSIZE": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Virtual memory size (KB)"
+ },
+ "MEMORY_VSIZE_MAX_CONTIGUOUS": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Maximum-sized block of contiguous virtual memory (KB)"
+ },
+ "MEMORY_JS_COMPARTMENTS_SYSTEM": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Total JavaScript compartments used for add-ons and internals."
+ },
+ "MEMORY_JS_COMPARTMENTS_USER": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Total JavaScript compartments used for web pages"
+ },
+ "MEMORY_JS_GC_HEAP": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 16777216,
+ "n_buckets": 200,
+ "description": "Memory used by the garbage-collected JavaScript heap (KB)"
+ },
+ "MEMORY_STORAGE_SQLITE": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 524288,
+ "n_buckets": 50,
+ "description": "Memory used by SQLite (KB)"
+ },
+ "MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 1048576,
+ "n_buckets": 50,
+ "description": "Memory used for uncompressed, in-use content images (KB)"
+ },
+ "MEMORY_HEAP_ALLOCATED": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 16777216,
+ "n_buckets": 200,
+ "description": "Heap memory allocated (KB)"
+ },
+ "MEMORY_HEAP_COMMITTED_UNUSED": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 524288,
+ "n_buckets": 50,
+ "description": "Committed, unused heap memory (KB)"
+ },
+ "MEMORY_HEAP_OVERHEAD_FRACTION": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1252375],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Fraction of committed heap memory that is overhead (percentage)."
+ },
+ "GHOST_WINDOWS": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 128,
+ "n_buckets": 32,
+ "description": "Number of ghost windows"
+ },
+ "MEMORY_FREE_PURGED_PAGES_MS": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 10,
+ "description": "Time(ms) to purge dirty heap pages."
+ },
+ "LOW_MEMORY_EVENTS_VIRTUAL": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 21,
+ "description": "Number of low-virtual-memory events fired since last ping",
+ "cpp_guard": "XP_WIN"
+ },
+ "LOW_MEMORY_EVENTS_PHYSICAL": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 21,
+ "description": "Number of low-physical-memory events fired since last ping",
+ "cpp_guard": "XP_WIN"
+ },
+ "LOW_MEMORY_EVENTS_COMMIT_SPACE": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 21,
+ "description": "Number of low-commit-space events fired since last ping",
+ "cpp_guard": "XP_WIN"
+ },
+ "PAGE_FAULTS_HARD": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 8,
+ "high": 65536,
+ "n_buckets": 13,
+ "description": "Hard page faults (since last telemetry ping)",
+ "cpp_guard": "XP_UNIX"
+ },
+ "FONTLIST_INITOTHERFAMILYNAMES": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "Time(ms) spent on reading other family names from all fonts"
+ },
+ "FONTLIST_INITFACENAMELISTS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "Time(ms) spent on reading family names from all fonts"
+ },
+ "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxDWriteFontList::DelayedInitFontList Total (ms)",
+ "cpp_guard": "XP_WIN"
+ },
+ "DWRITEFONT_DELAYEDINITFONTLIST_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "gfxDWriteFontList::DelayedInitFontList Font Family Count",
+ "cpp_guard": "XP_WIN"
+ },
+ "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxDWriteFontList::DelayedInitFontList GetSystemFontCollection (ms)",
+ "cpp_guard": "XP_WIN"
+ },
+ "DWRITEFONT_INIT_PROBLEM": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "DirectWrite system fontlist initialization problem (1=GDI interop, 2=system font collection, 3=no fonts)",
+ "cpp_guard": "XP_WIN"
+ },
+ "GDI_INITFONTLIST_TOTAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxGDIFontList::InitFontList Total (ms)",
+ "cpp_guard": "XP_WIN"
+ },
+ "MAC_INITFONTLIST_TOTAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxMacPlatformFontList::InitFontList Total (ms)",
+ "cpp_guard": "XP_DARWIN"
+ },
+ "SYSTEM_FONT_FALLBACK": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "System font fallback (us)"
+ },
+ "SYSTEM_FONT_FALLBACK_FIRST": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 40000,
+ "n_buckets": 20,
+ "description": "System font fallback, first call (ms)"
+ },
+ "SYSTEM_FONT_FALLBACK_SCRIPT": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 110,
+ "description": "System font fallback script"
+ },
+ "GRADIENT_DURATION": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 50000000,
+ "n_buckets": 20,
+ "description": "Gradient generation time (us)"
+ },
+ "GRADIENT_RETENTION_TIME": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "Maximum retention time for the gradient cache. (ms)"
+ },
+ "STARTUP_CACHE_AGE_HOURS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 20,
+ "description": "Startup cache age (hours)"
+ },
+ "STARTUP_CACHE_INVALID": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Was the disk startup cache file detected as invalid"
+ },
+ "WORD_CACHE_HITS_CONTENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 30,
+ "description": "Word cache hits, content text (chars)"
+ },
+ "WORD_CACHE_HITS_CHROME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 30,
+ "description": "Word cache hits, chrome text (chars)"
+ },
+ "WORD_CACHE_MISSES_CONTENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 30,
+ "description": "Word cache misses, content text (chars)"
+ },
+ "WORD_CACHE_MISSES_CHROME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 30,
+ "description": "Word cache misses, chrome text (chars)"
+ },
+ "FONT_CACHE_HIT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "font cache hit"
+ },
+ "BAD_FALLBACK_FONT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "system fallback font can't be used"
+ },
+ "SHUTDOWN_OK": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Did the browser start after a successful shutdown"
+ },
+ "IMAGE_DECODE_LATENCY_US": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000000,
+ "n_buckets": 100,
+ "description": "Time spent decoding an image chunk (us)"
+ },
+ "IMAGE_DECODE_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 50000000,
+ "n_buckets": 100,
+ "description": "Time spent decoding an image (us)"
+ },
+ "IMAGE_DECODE_ON_DRAW_LATENCY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 50000000,
+ "n_buckets": 100,
+ "description": "Time from starting a decode to it showing up on the screen (us)"
+ },
+ "IMAGE_DECODE_CHUNKS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Number of chunks per decode attempt"
+ },
+ "IMAGE_DECODE_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Decode count"
+ },
+ "IMAGE_DECODE_OPAQUE_BGRA": {
+ "alert_emails": ["aosmond@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "description": "Opaque images are BGRA",
+ "bug_numbers": [1311779]
+ },
+ "IMAGE_DECODE_SPEED_JPEG": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "JPEG image decode speed (Kbytes/sec)"
+ },
+ "IMAGE_DECODE_SPEED_GIF": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "GIF image decode speed (Kbytes/sec)"
+ },
+ "IMAGE_DECODE_SPEED_PNG": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "PNG image decode speed (Kbytes/sec)"
+ },
+ "CANVAS_2D_USED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "2D canvas used"
+ },
+ "CANVAS_WEBGL_ACCL_FAILURE_ID": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bgirard@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "Track the failure IDs that lead us to reject attempting to create an accelerated context. CANVAS_WEBGL_FAILURE_ID reports the overall WebGL status with the attempt to fallback.",
+ "bug_numbers": [1272808]
+ },
+ "CANVAS_WEBGL_FAILURE_ID": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bgirard@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "WebGL runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1272808]
+ },
+ "CANVAS_WEBGL_SUCCESS": {
+ "alert_emails": ["jmuizelaar@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "WebGL1 creation success",
+ "bug_numbers": [1247327]
+ },
+ "CANVAS_WEBGL_USED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "WebGL canvas used"
+ },
+ "CANVAS_WEBGL2_SUCCESS": {
+ "alert_emails": ["jmuizelaar@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "WebGL2 creation success",
+ "bug_numbers": [1247327]
+ },
+ "TOTAL_CONTENT_PAGE_LOAD_TIME": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 30000,
+ "n_buckets": 100,
+ "description": "HTTP: Total page load time (ms)"
+ },
+ "HTTP_SUBITEM_OPEN_LATENCY_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Page start -> subitem open() (ms)"
+ },
+ "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Page start -> first byte received for subitem reply (ms)"
+ },
+ "HTTP_REQUEST_PER_PAGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "HTTP: Requests per page (count)"
+ },
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 101,
+ "description": "HTTP: Requests serviced from cache (%)"
+ },
+ "HTTP_REQUEST_PER_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "HTTP: requests per connection"
+ },
+ "HTTP_KBREAD_PER_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 50,
+ "description": "HTTP: KB read per connection"
+ },
+ "HTTP_PAGE_DNS_ISSUE_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: open() -> DNS request issued (ms)"
+ },
+ "HTTP_PAGE_DNS_LOOKUP_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: DNS lookup time (ms)"
+ },
+ "HTTP_PAGE_TCP_CONNECTION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: TCP connection setup (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_SENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Open -> first byte of request sent (ms)"
+ },
+ "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: First byte of request sent -> last byte of response received (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Open -> first byte of reply received (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Open -> cache read start (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Open -> cache read start (ms), [cache2]"
+ },
+ "HTTP_PAGE_CACHE_READ_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Cache read time (ms)"
+ },
+ "HTTP_PAGE_CACHE_READ_TIME_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Cache read time (ms) [cache2]"
+ },
+ "HTTP_PAGE_REVALIDATION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Positive cache validation time (ms)"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Overall load time - all (ms)"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Overall load time - all (ms) [cache2]"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Overall load time - cache hits (ms)"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Overall load time - cache hits (ms) [cache2]"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_NET": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Overall load time - network (ms)"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_NET_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page: Overall load time - network (ms) [cache2]"
+ },
+ "HTTP_SUB_DNS_ISSUE_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: open() -> DNS request issued (ms)"
+ },
+ "HTTP_SUB_DNS_LOOKUP_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: DNS lookup time (ms)"
+ },
+ "HTTP_SUB_TCP_CONNECTION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: TCP connection setup (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_SENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Open -> first byte of request sent (ms)"
+ },
+ "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: First byte of request sent -> last byte of response received (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_RECEIVED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Open -> first byte of reply received (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Open -> cache read start (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Open -> cache read start (ms) [cache2]"
+ },
+ "HTTP_SUB_CACHE_READ_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Cache read time (ms)"
+ },
+ "HTTP_SUB_CACHE_READ_TIME_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Cache read time (ms) [cache2]"
+ },
+ "HTTP_SUB_REVALIDATION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Positive cache validation time (ms)"
+ },
+ "HTTP_SUB_COMPLETE_LOAD": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Overall load time - all (ms)"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Overall load time - all (ms) [cache2]"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_CACHED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Overall load time - cache hits (ms)"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_CACHED_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Overall load time - cache hits (ms) [cache2]"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_NET": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Overall load time - network (ms)"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_NET_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem: Overall load time - network (ms) [cache2]"
+ },
+ "HTTP_PROXY_TYPE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "HTTP Proxy Type (none, http, socks)"
+ },
+ "HTTP_TRANSACTION_IS_SSL": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP transaction was over SSL or not."
+ },
+ "HTTP_PAGELOAD_IS_SSL": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP base page load was over SSL or not."
+ },
+ "HTTP_TRANSACTION_USE_ALTSVC": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP transaction was routed via Alt-Svc or not."
+ },
+ "HTTP_TRANSACTION_USE_ALTSVC_OE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP transaction routed via Alt-Svc was scheme=http"
+ },
+ "HTTP_SCHEME_UPGRADE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Was the URL upgraded to HTTPS? (0=already HTTPS, 1=no reason to upgrade, 2=STS upgrade blocked by pref, 3=upgraded with STS, 4=upgraded with CSP)"
+ },
+ "HTTP_RESPONSE_STATUS_CODE": {
+ "alert_emails": ["ckerschbaumer@mozilla.com"],
+ "bug_numbers": [1272345, 1296287],
+ "expires_in_version": "56",
+ "kind": "enumerated",
+ "n_values": 12,
+ "description": "Whether the URL gets redirected? (0=200, 1=301, 2=302, 3=304, 4=307, 5=308, 6=400, 7=401, 8=403, 9=404, 10=500, 11=other)"
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_ISIMG": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for images. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_NOTIMG": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for non-images. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_ISIMG": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for images. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_NOTIMG": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for non-images. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QSMALL_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and small queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QMED_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and medium queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QBIG_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and large queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QSMALL_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and small queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QMED_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and medium queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QBIG_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and large queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QSMALL_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and small queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QMED_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and medium queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QBIG_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and large queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QSMALL_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and small queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QMED_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and medium queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QBIG_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and large queue. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_SMALL_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a small size (<32K) and normal priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_MED_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a medium size (<256K) and normal priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_LARGE_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a large size (>256K) and normal priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_SMALL_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a small size (<32K) and high priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_MED_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a medium size (<256K) and high priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_LARGE_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for cache files with a large size (>256K) and high priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_SMALL_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a small size (<32K) and normal priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_MED_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a medium size (<256K) and normal priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_LARGE_NORMALPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a large size (>256K) and normal priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_SMALL_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a small size (<32K) and high priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_MED_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a medium size (<256K) and high priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_LARGE_HIGHPRI": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a large size (>256K) and high priority. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_REVALIDATED": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) revalidated cache entries. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_NOTREVALIDATED": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) not revalidated cache entries. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_REVALIDATED": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) revalidated cache entries. Offset by 500 ms."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_NOTREVALIDATED": {
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1313095],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) not revalidated cache entries. Offset by 500 ms."
+ },
+ "HTTP_AUTH_DIALOG_STATS": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Stats about what kind of resource requested http authentication. (0=top-level doc, 1=same origin subresources, 2=cross-origin subresources, 3=xhr)"
+ },
+ "HTTP_AUTH_TYPE_STATS": {
+ "alert_emails": ["rbarnes@mozilla.com"],
+ "bug_numbers": [1266571],
+ "expires_in_version": "52",
+ "kind": "enumerated",
+ "n_values": 8,
+ "releaseChannelCollection": "opt-out",
+ "description": "Recorded once for each HTTP 401 response. The value records the type of authentication and the TLS-enabled status. (0=basic/clear, 1=basic/tls, 2=digest/clear, 3=digest/tls, 4=ntlm/clear, 5=ntlm/tls, 6=negotiate/clear, 7=negotiate/tls)"
+ },
+ "TLS_EARLY_DATA_NEGOTIATED": {
+ "expires_in_version": "58",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Sending TLS early data was possible: 0 - not possible, 1 - possible but not used, 2 - possible and used.",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296288]
+ },
+ "TLS_EARLY_DATA_ACCEPTED": {
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "description": "TLS early data was used and it was accepted (true) or rejected (false) by the remote host.",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296288]
+ },
+ "TLS_EARLY_DATA_BYTES_WRITTEN": {
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Amount of bytes sent using TLS early data at the start of a TLS connection for a given channel.",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296288]
+ },
+ "SSL_HANDSHAKE_VERSION": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1250568],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Version (1=tls1, 2=tls1.1, 3=tls1.2, 4=tls1.3)"
+ },
+ "SSL_HANDSHAKE_RESULT": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1331280],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 672,
+ "releaseChannelCollection": "opt-out",
+ "description": "SSL handshake result, 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err"
+ },
+ "SSL_TIME_UNTIL_READY": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time including TCP and proxy tunneling"
+ },
+ "SSL_TIME_UNTIL_HANDSHAKE_FINISHED": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time for full handshake including TCP and proxy tunneling"
+ },
+ "SSL_BYTES_BEFORE_CERT_CALLBACK": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 32000,
+ "n_buckets": 64,
+ "description": "plaintext bytes read before a server certificate authenticated"
+ },
+ "SSL_NPN_TYPE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "NPN Results (0=none, 1=negotiated, 2=no-overlap, 3=selected(alpn))"
+ },
+ "SSL_RESUMED_SESSION": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "complete TLS connect that used TLS Sesison Resumption"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_RESULT": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "HTTP result of OCSP, etc.. (0=canceled, 1=OK, 2=FAILED, 3=internal-error)"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_CANCELED_TIME": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms elapsed time of OCSP etc.. that was canceled"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms elapsed time of OCSP etc.. that succeeded"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_FAILED_TIME": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms elapsed time of OCSP etc.. that failed"
+ },
+ "SSL_KEY_EXCHANGE_ALGORITHM_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Handshake Key Exchange Algorithm for full handshake (null=0, rsa=1, dh=2, fortezza=3, ecdh=4)"
+ },
+ "SSL_KEY_EXCHANGE_ALGORITHM_RESUMED": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Handshake Key Exchange Algorithm for resumed handshake (null=0, rsa=1, dh=2, fortezza=3, ecdh=4)"
+ },
+ "SSL_OBSERVED_END_ENTITY_CERTIFICATE_LIFETIME": {
+ "expires_in_version": "55",
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 125,
+ "releaseChannelCollection": "opt-out",
+ "description": "The lifetime of accepted HTTPS server certificates, in weeks, up to 2 years. Bucket 105 is all end-entity HTTPS server certificates with a lifetime > 2 years."
+ },
+ "KEYGEN_GENERATED_KEY_TYPE": {
+ "expires_in_version": "55",
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1191414,1284945],
+ "description": "The number of times we generate a key via keygen, keyed on algorithm and keysize. Keys include RSA with key size (512, 1024, 2048, possibly others), secp384r1, secp256r1, and 'other_ec'."
+ },
+ "WEBSOCKETS_HANDSHAKE_TYPE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "Websockets Handshake Results (ws-ok-plain, ws-ok-proxy, ws-failed-plain, ws-failed-proxy, wss-ok-plain, wss-ok-proxy, wss-failed-plain, wss-failed-proxy)"
+ },
+ "SPDY_VERSION2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 48,
+ "description": "SPDY: Protocol Version Used"
+ },
+ "HTTP_RESPONSE_VERSION": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 48,
+ "description": "HTTP: Protocol Version Used on Response from nsHttp.h"
+ },
+ "HTTP_09_INFO": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "HTTP 09 Response Breakdown: lowbit subresource, high bit nonstd port",
+ "bug_numbers": [1262572],
+ "alert_emails": ["necko@mozilla.com"]
+ },
+ "SPDY_PARALLEL_STREAMS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "SPDY: Streams concurrent active per connection"
+ },
+ "SPDY_REQUEST_PER_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "SPDY: Streams created per connection"
+ },
+ "SPDY_SERVER_INITIATED_STREAMS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 250,
+ "description": "SPDY: Streams recevied per connection"
+ },
+ "SPDY_CHUNK_RECVD": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "SPDY: Recvd Chunk Size (rounded to KB)"
+ },
+ "SPDY_SYN_SIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 20,
+ "high": 20000,
+ "n_buckets": 50,
+ "description": "SPDY: SYN Frame Header Size"
+ },
+ "SPDY_SYN_RATIO": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 99,
+ "n_buckets": 20,
+ "description": "SPDY: SYN Frame Header Ratio (lower better)"
+ },
+ "SPDY_SYN_REPLY_SIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 16,
+ "high": 20000,
+ "n_buckets": 50,
+ "description": "SPDY: SYN Reply Header Size"
+ },
+ "SPDY_SYN_REPLY_RATIO": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 99,
+ "n_buckets": 20,
+ "description": "SPDY: SYN Reply Header Ratio (lower better)"
+ },
+ "SPDY_NPN_CONNECT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "SPDY: NPN Negotiated"
+ },
+ "SPDY_NPN_JOIN": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "SPDY: Coalesce Succeeded"
+ },
+ "SPDY_KBREAD_PER_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 50,
+ "description": "SPDY: KB read per connection"
+ },
+ "SPDY_SETTINGS_UL_BW": {
+ "expires_in_version": "42",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "SPDY: Settings Upload Bandwidth"
+ },
+ "SPDY_SETTINGS_DL_BW": {
+ "expires_in_version": "42",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "SPDY: Settings Download Bandwidth"
+ },
+ "SPDY_SETTINGS_RTT": {
+ "expires_in_version": "42",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "SPDY: Settings RTT"
+ },
+ "SPDY_SETTINGS_MAX_STREAMS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "H2: Settings Max Streams parameter"
+ },
+ "SPDY_SETTINGS_CWND": {
+ "expires_in_version": "42",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "SPDY: Settings CWND (packets)"
+ },
+ "SPDY_SETTINGS_RETRANS": {
+ "expires_in_version": "42",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "SPDY: Retransmission Rate"
+ },
+ "SPDY_SETTINGS_IW": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "H2: Settings Initial Window (rounded to KB)"
+ },
+ "SPDY_GOAWAY_LOCAL": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 32,
+ "description": "H2: goaway reason client sent from rfc 7540. 31 is none sent."
+ },
+ "SPDY_GOAWAY_PEER": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 32,
+ "description": "H2: goaway reason from peer from rfc 7540. 31 is none received."
+ },
+ "HPACK_ELEMENTS_EVICTED_DECOMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Number of items removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_DECOMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 8192,
+ "n_buckets": 50,
+ "description": "HPACK: Number of bytes removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_RATIO_DECOMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Ratio of bytes evicted to bytes added (* 100)",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_COUNT_DECOMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 50,
+ "description": "HPACK: peak number of items in the dynamic table",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_SIZE_DECOMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 16384,
+ "n_buckets": 100,
+ "description": "HPACK: peak size in bytes of the table",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_ELEMENTS_EVICTED_COMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Number of items removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_COMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 8192,
+ "n_buckets": 50,
+ "description": "HPACK: Number of bytes removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_RATIO_COMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Ratio of bytes evicted to bytes added (* 100)",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_COUNT_COMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 50,
+ "description": "HPACK: peak number of items in the dynamic table",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_SIZE_COMPRESSOR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 16384,
+ "n_buckets": 100,
+ "description": "HPACK: peak size in bytes of the table",
+ "alert_emails": ["necko@mozilla.com", "hurley@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HTTP_CHANNEL_DISPOSITION" : {
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1341128],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 16,
+ "releaseChannelCollection": "opt-out",
+ "description": "Channel Disposition: 0=Cancel, 1=Disk, 2=NetOK, 3=NetEarlyFail, 4=NetlateFail, +8 for HTTPS"
+ },
+ "HTTP_CONNECTION_ENTRY_CACHE_HIT_1" : {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Fraction of sockets that used a nsConnectionEntry with history - size 300."
+ },
+ "HTTP_CACHE_DISPOSITION_2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "HTTP Cache Hit, Reval, Failed-Reval, Miss"
+ },
+ "HTTP_CACHE_DISPOSITION_2_V2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "HTTP Cache v2 Hit, Reval, Failed-Reval, Miss"
+ },
+ "HTTP_DISK_CACHE_DISPOSITION_2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "HTTP Disk Cache Hit, Reval, Failed-Reval, Miss"
+ },
+ "HTTP_CACHE_MISS_HALFLIFE_EXPERIMENT_2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "HTTP Cache v2 Miss by half-life value (5 min, 15 min, 1 hour, 6 hours)"
+ },
+ "HTTP_CACHE_ENTRY_RELOAD_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 900000,
+ "n_buckets": 50,
+ "description": "Time before we reload an HTTP cache entry again to memory"
+ },
+ "HTTP_CACHE_ENTRY_ALIVE_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 50,
+ "description": "Time for which an HTTP cache entry is kept warmed in memory"
+ },
+ "HTTP_CACHE_ENTRY_REUSE_COUNT": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 20,
+ "n_buckets": 19,
+ "description": "Reuse count of an HTTP cache entry warmed in memory"
+ },
+ "HTTP_MEMORY_CACHE_DISPOSITION_2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "HTTP Memory Cache Hit, Reval, Failed-Reval, Miss"
+ },
+ "HTTP_OFFLINE_CACHE_DISPOSITION_2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "HTTP Offline Cache Hit, Reval, Failed-Reval, Miss"
+ },
+ "HTTP_OFFLINE_CACHE_DOCUMENT_LOAD": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Rate of page load from offline cache"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_OPEN_PRIORITY": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_READ_PRIORITY": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_MANAGEMENT": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_OPEN": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_READ": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_WRITE": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_WRITE_PRIORITY": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_INDEX": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_EVICT": {
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "CACHE_DEVICE_SEARCH_2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time to search cache (ms)"
+ },
+ "CACHE_MEMORY_SEARCH_2": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time to search memory cache (ms)"
+ },
+ "CACHE_DISK_SEARCH_2": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time to search disk cache (ms)"
+ },
+ "CACHE_OFFLINE_SEARCH_2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time to search offline cache (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_HTTP": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of HTTP transaction (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_HTTP_PIPELINES": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of HTTP with pipelines transaction (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_SPDY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of SPDY transaction (ms)"
+ },
+ "HTTP_SAW_QUIC_ALT_PROTOCOL": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Fraction of responses with a quic alt-protocol advertisement."
+ },
+ "HTTP_CONTENT_ENCODING": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "encoding removed: 0=unknown, 1=gzip, 2=deflate, 3=brotli"
+ },
+ "HTTP_DISK_CACHE_OVERHEAD": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 32000000,
+ "n_buckets": 100,
+ "description": "HTTP Disk cache memory overhead (bytes)"
+ },
+ "CACHE_LM_INCONSISTENT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Cache discovered inconsistent last-modified entry"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms)"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock on the main thread (ms)"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSSETDISKSMARTSIZECALLBACK_NOTIFY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSSETDISKSMARTSIZECALLBACK_NOTIFY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSPROCESSREQUESTEVENT_RUN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSPROCESSREQUESTEVENT_RUN"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_LAZYINIT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSOUTPUTSTREAMWRAPPER_LAZYINIT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_CLOSEINTERNAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSOUTPUTSTREAMWRAPPER_CLOSEINTERNAL"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_RELEASE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSOUTPUTSTREAMWRAPPER_RELEASE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCOMPRESSOUTPUTSTREAMWRAPPER_RELEASE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCOMPRESSOUTPUTSTREAMWRAPPER_RELEASE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_LAZYINIT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSINPUTSTREAMWRAPPER_LAZYINIT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_CLOSEINTERNAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSINPUTSTREAMWRAPPER_CLOSEINTERNAL"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_RELEASE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSINPUTSTREAMWRAPPER_RELEASE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSDECOMPRESSINPUTSTREAMWRAPPER_RELEASE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSDECOMPRESSINPUTSTREAMWRAPPER_RELEASE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SHUTDOWN"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHEENABLED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETOFFLINECACHEENABLED"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHECAPACITY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETOFFLINECACHECAPACITY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETMEMORYCACHE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKSMARTSIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKSMARTSIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEENABLED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKCACHEENABLED"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHECAPACITY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_SETDISKCACHECAPACITY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_OPENCACHEENTRY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_OPENCACHEENTRY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILESHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_ONPROFILESHUTDOWN"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILECHANGED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_ONPROFILECHANGED"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_GETCACHEIOTARGET": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_GETCACHEIOTARGET"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_EVICTENTRIESFORCLIENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_EVICTENTRIESFORCLIENT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_DISKDEVICEHEAPSIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_DISKDEVICEHEAPSIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_CLOSEALLSTREAMS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_CLOSEALLSTREAMS"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOM": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_DOOM"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETPREDICTEDDATASIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETPREDICTEDDATASIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDATASIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETDATASIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEDATASIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETSTORAGEDATASIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_REQUESTDATASIZECHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_REQUESTDATASIZECHANGE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETDATASIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETDATASIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENINPUTSTREAM": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_OPENINPUTSTREAM"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENOUTPUTSTREAM": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_OPENOUTPUTSTREAM"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCACHEELEMENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETCACHEELEMENT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETCACHEELEMENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETCACHEELEMENT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEPOLICY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETSTORAGEPOLICY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSTORAGEPOLICY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETSTORAGEPOLICY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFILE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETFILE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSECURITYINFO": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETSECURITYINFO"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSECURITYINFO": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETSECURITYINFO"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOMANDFAILPENDINGREQUESTS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_DOOMANDFAILPENDINGREQUESTS"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_MARKVALID": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_MARKVALID"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_CLOSE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_CLOSE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETMETADATAELEMENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETMETADATAELEMENT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETMETADATAELEMENT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETMETADATAELEMENT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_VISITMETADATA": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_VISITMETADATA"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETEXPIRATIONTIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_SETEXPIRATIONTIME"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_ISSTREAMBASED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_ISSTREAMBASED"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTMODIFIED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETLASTMODIFIED"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETEXPIRATIONTIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETEXPIRATIONTIME"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETKEY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETKEY"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFETCHCOUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETFETCHCOUNT"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDEVICEID": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETDEVICEID"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_PROCESSREQUEST": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_PROCESSREQUEST"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_VISITENTRIES": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHESERVICE_VISITENTRIES"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETPREDICTEDDATASIZE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETPREDICTEDDATASIZE"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTFETCHED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETLASTFETCHED"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCLIENTID": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSCACHEENTRYDESCRIPTOR_GETCLIENTID"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSBLOCKONCACHETHREADEVENT_RUN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSBLOCKONCACHETHREADEVENT_RUN"
+ },
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSASYNCDOOMEVENT_RUN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent waiting on the cache service lock (ms) on the main thread in NSASYNCDOOMEVENT_RUN"
+ },
+ "DNT_USAGE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "I want to be tracked, I do NOT want to be tracked, DNT unset"
+ },
+ "DNS_LOOKUP_METHOD2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "DNS Lookup Type (hit, renewal, negative-hit, literal, overflow, network-first, network-shared)"
+ },
+ "DNS_CLEANUP_AGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1440,
+ "n_buckets": 50,
+ "description": "DNS Cache Entry Age at Removal Time (minutes)"
+ },
+ "DNS_LOOKUP_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time for a successful DNS OS resolution (msec)"
+ },
+ "DNS_RENEWAL_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time for a renewed DNS OS resolution (msec)"
+ },
+ "DNS_RENEWAL_TIME_FOR_TTL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time for a DNS OS resolution (msec) used to get TTL"
+ },
+ "DNS_FAILED_LOOKUP_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time for an unsuccessful DNS OS resolution (msec)"
+ },
+ "DNS_BLACKLIST_COUNT": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 21,
+ "n_buckets": 20,
+ "description": "The number of unusable addresses reported for each record"
+ },
+ "REFRESH_DRIVER_TICK" : {
+ "expires_in_version": "never",
+ "description": "Total time spent ticking the refresh driver in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "PAINT_BUILD_DISPLAYLIST_TIME" : {
+ "expires_in_version": "never",
+ "description": "Time spent in building displaylists in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "PAINT_RASTERIZE_TIME" : {
+ "expires_in_version": "never",
+ "description": "Time spent rasterizing each frame in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "PREDICTOR_PREDICT_ATTEMPTS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Predict is called and attempts to predict"
+ },
+ "PREDICTOR_LEARN_ATTEMPTS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Learn is called and attempts to learn"
+ },
+ "PREDICTOR_PREDICT_FULL_QUEUE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Predict doesn't continue because the queue is full"
+ },
+ "PREDICTOR_LEARN_FULL_QUEUE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Learn doesn't continue because the queue is full"
+ },
+ "PREDICTOR_WAIT_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Amount of time a predictor event waits in the queue (ms)"
+ },
+ "PREDICTOR_PREDICT_WORK_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Amount of time spent doing the work for predict (ms)"
+ },
+ "PREDICTOR_LEARN_WORK_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Amount of time spent doing the work for learn (ms)"
+ },
+ "PREDICTOR_TOTAL_PREDICTIONS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual predictions (preresolves, preconnects, ...) happen"
+ },
+ "PREDICTOR_TOTAL_PREFETCHES": {
+ "expires_in_version": "never",
+ "alert_emails": [],
+ "bug_numbers": [1016628],
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual prefetches happen"
+ },
+ "PREDICTOR_TOTAL_PREFETCHES_USED": {
+ "expires_in_version": "never",
+ "alert_emails": [],
+ "bug_numbers": [1016628],
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many prefetches are actually used by a channel"
+ },
+ "PREDICTOR_PREFETCH_TIME": {
+ "expires_in_version": "never",
+ "alert_emails": [],
+ "bug_numbers": [1016628],
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "How long it takes from OnStartRequest to OnStopRequest for a prefetch"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual preconnects happen"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS_CREATED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many preconnects actually created a speculative socket"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS_USED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many preconnects actually created a used speculative socket"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS_UNUSED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many preconnects needlessly created a speculative socket"
+ },
+ "PREDICTOR_TOTAL_PRERESOLVES": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual preresolves happen"
+ },
+ "PREDICTOR_PREDICTIONS_CALCULATED": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many prediction calculations are performed"
+ },
+ "PREDICTOR_GLOBAL_DEGRADATION": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The global degradation calculated"
+ },
+ "PREDICTOR_SUBRESOURCE_DEGRADATION": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The degradation calculated for a subresource"
+ },
+ "PREDICTOR_BASE_CONFIDENCE": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The base confidence calculated for a subresource"
+ },
+ "PREDICTOR_CONFIDENCE": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The final confidence calculated for a subresource"
+ },
+ "PREDICTOR_PREDICT_TIME_TO_ACTION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "How long it takes from the time Predict() is called to the time we take action"
+ },
+ "PREDICTOR_PREDICT_TIME_TO_INACTION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "How long it takes from the time Predict() is called to the time we figure out there's nothing to do"
+ },
+ "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many speculative http connections are created"
+ },
+ "HTTPCONNMGR_USED_SPECULATIVE_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many speculative http connections are actually used"
+ },
+ "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many speculative connections are made needlessly"
+ },
+ "TAP_TO_LOAD_IMAGE_SIZE": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 32768,
+ "n_buckets": 50,
+ "description": "The size of the image being shown, when using tap-to-load images. (kilobytes)",
+ "bug_numbers": [1208167]
+ },
+ "STS_POLL_AND_EVENTS_CYCLE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The duraion of a socketThread cycle, including polls and pending events. (ms)"
+ },
+ "STS_NUMBER_OF_PENDING_EVENTS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 100,
+ "description": "Number of pending events per SocketThread cycle."
+ },
+ "STS_POLL_CYCLE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The duration of poll. (ms)"
+ },
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The duraion of the socketThread cycle during shutdown, including polls and pending events. (ms)"
+ },
+ "STS_NUMBER_OF_PENDING_EVENTS_IN_THE_LAST_CYCLE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 100,
+ "description": "Number of pending events per SocketThread cycle during shutdown."
+ },
+ "STS_NUMBER_OF_ONSOCKETREADY_CALLS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 100,
+ "description": "Number of OnSocketReady calls during a single poll."
+ },
+ "STS_POLL_BLOCK_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked on poll (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_NORMAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect during a shutdown (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when there has been the connectiviy change in the last 60s (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when there has been a link change in the last 60s (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_OFFLINE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_NORMAL": {
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_SHUTDOWN": {
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect during a shutdown (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when there has been the connectiviy change in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_LINK_CHANGE": {
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when there has been a link change in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_OFFLINE": {
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue during a shutdown (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when there has been the connectivity change in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when there has been a link change in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close during a shutdown (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been the connectivity change in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been a link change in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close during a shutdown (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been the connectivity change in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been a link change in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when the offline state has changed in the last 60s (ms)."
+ },
+ "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Count the number of 0) successful connections to an ipv4 address, 1) failed connection an ipv4 address, 2) successful connection to an ipv6 address and 3) failed connections to an ipv6 address."
+ },
+ "NETWORK_SESSION_AT_900FD": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "session reached 900 fd limit sockets",
+ "bug_numbers": [1260218],
+ "alert_emails": ["necko@mozilla.com"]
+ },
+ "NETWORK_PROBE_MAXCOUNT": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 50,
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Result of nsSocketTransportService::ProbeMaxCount()",
+ "bug_numbers": [1260218],
+ "alert_emails": ["necko@mozilla.com"]
+ },
+ "FIND_PLUGINS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent scanning filesystem for plugins (ms)"
+ },
+ "CHECK_JAVA_ENABLED": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent checking if Java is enabled (ms)"
+ },
+ "PLUGIN_HANG_UI_USER_RESPONSE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "User response to Plugin Hang UI"
+ },
+ "PLUGIN_HANG_UI_DONT_ASK": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether the user has requested not to see the Plugin Hang UI again"
+ },
+ "PLUGIN_HANG_UI_RESPONSE_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 20,
+ "description": "Time spent in Plugin Hang UI (ms)"
+ },
+ "PLUGIN_HANG_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 20,
+ "description": "Value of dom.ipc.plugins.hangUITimeoutSecs plus time spent in Plugin Hang UI (ms)"
+ },
+ "PLUGIN_LOAD_METADATA": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Time spent loading plugin DLL and obtaining metadata (ms)"
+ },
+ "PLUGIN_SHUTDOWN_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Time spent shutting down plugins (ms)"
+ },
+ "PLUGIN_CALLED_DIRECTLY": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "A plugin object was successfully invoked as a function"
+ },
+ "FLASH_PLUGIN_STATES": {
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "A flash object's initialization state"
+ },
+ "FLASH_PLUGIN_AREA": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "low": 256,
+ "high": 16777216,
+ "n_buckets": 50,
+ "description": "Flash object area (width * height)"
+ },
+ "FLASH_PLUGIN_WIDTH": {
+ "expires_in_version": "50",
+ "kind": "linear",
+ "low": 1,
+ "high": 2000,
+ "n_buckets": 50,
+ "description": "Flash object width"
+ },
+ "FLASH_PLUGIN_HEIGHT": {
+ "expires_in_version": "50",
+ "kind": "linear",
+ "low": 1,
+ "high": 2000,
+ "n_buckets": 50,
+ "description": "Flash object height"
+ },
+ "FLASH_PLUGIN_INSTANCES_ON_PAGE": {
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 30,
+ "description": "Flash object instances count on page"
+ },
+ "MOZ_SQLITE_OPEN_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite open() (ms)"
+ },
+ "MOZ_SQLITE_OPEN_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite open() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_TRUNCATE_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite truncate() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_TRUNCATE_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite truncate() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_OTHER_READ_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_OTHER_READ_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_PLACES_READ_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_PLACES_READ_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on cookie DB open with readahead (ms)"
+ },
+ "MOZ_SQLITE_COOKIES_READ_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_COOKIES_READ_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_WEBAPPS_READ_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_WEBAPPS_READ_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite read() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_OTHER_WRITE_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_OTHER_WRITE_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_PLACES_WRITE_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms)"
+ },
+ "MOZ_SQLITE_PLACES_WRITE_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_COOKIES_WRITE_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_COOKIES_WRITE_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_WEBAPPS_WRITE_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_WEBAPPS_WRITE_MAIN_THREAD_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite write() (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_OTHER_SYNC_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_OTHER_SYNC_MAIN_THREAD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_PLACES_SYNC_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_PLACES_SYNC_MAIN_THREAD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_COOKIES_SYNC_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_COOKIES_SYNC_MAIN_THREAD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_WEBAPPS_SYNC_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_WEBAPPS_SYNC_MAIN_THREAD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on SQLite fsync() (ms)"
+ },
+ "MOZ_SQLITE_OTHER_READ_B": {
+ "expires_in_version": "default",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite read() (bytes)"
+ },
+ "MOZ_SQLITE_PLACES_READ_B": {
+ "expires_in_version": "40",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite read() (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_COOKIES_READ_B": {
+ "expires_in_version": "40",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite read() (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_WEBAPPS_READ_B": {
+ "expires_in_version": "40",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite read() (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_PLACES_WRITE_B": {
+ "expires_in_version": "40",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite write (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_COOKIES_WRITE_B": {
+ "expires_in_version": "40",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite write (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_WEBAPPS_WRITE_B": {
+ "expires_in_version": "40",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite write (bytes) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_SQLITE_OTHER_WRITE_B": {
+ "expires_in_version": "default",
+ "kind": "linear",
+ "high": 32768,
+ "n_buckets": 3,
+ "description": "SQLite write (bytes)"
+ },
+ "MOZ_STORAGE_ASYNC_REQUESTS_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 32768,
+ "n_buckets": 20,
+ "description": "mozStorage async requests completion (ms) *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "40",
+ "kind": "boolean",
+ "description": "mozStorage async requests success *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "STARTUP_MEASUREMENT_ERRORS": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "Flags errors in startup calculation()"
+ },
+ "NETWORK_DISK_CACHE_OPEN": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent opening disk cache (ms)"
+ },
+ "NETWORK_DISK_CACHE_TRASHRENAME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent renaming bad Cache to Cache.Trash (ms)"
+ },
+ "NETWORK_DISK_CACHE_DELETEDIR": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent deleting disk cache (ms)"
+ },
+ "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent during showdown stopping thread deleting old disk cache (ms)"
+ },
+ "NETWORK_DISK_CACHE_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Total Time spent (ms) during disk cache showdown"
+ },
+ "NETWORK_DISK_CACHE_SHUTDOWN_V2": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Total Time spent (ms) during disk cache showdown [cache2]"
+ },
+ "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent (ms) during showdown deleting disk cache for 'clear private data' option"
+ },
+ "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent (ms) during showdown deleting disk cache v2 for 'clear private data' option"
+ },
+ "NETWORK_ID": {
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1240932],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "Network identification (0=None, 1=New, 2=Same)"
+ },
+ "IDLE_NOTIFY_IDLE_MS": {
+ "alert_emails": ["froydnj@mozilla.com"],
+ "bug_numbers": [731004],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "Time spent checking for and notifying listeners that the user is idle (ms)"
+ },
+ "URLCLASSIFIER_LOOKUP_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 10,
+ "description": "Time spent per dbservice lookup (ms)"
+ },
+ "URLCLASSIFIER_SHUTDOWN_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "bug_numbers": [1315140],
+ "description": "Time spent per dbservice shutdown (ms)"
+ },
+ "URLCLASSIFIER_CL_CHECK_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 10,
+ "description": "Time spent per classifier lookup (ms)"
+ },
+ "URLCLASSIFIER_CL_UPDATE_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 20,
+ "high": 15000,
+ "n_buckets": 15,
+ "description": "Time spent per classifier update (ms)"
+ },
+ "URLCLASSIFIER_PS_FILELOAD_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Time spent loading PrefixSet from file (ms)"
+ },
+ "URLCLASSIFIER_PS_FALLOCATE_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Time spent fallocating PrefixSet (ms)"
+ },
+ "URLCLASSIFIER_PS_CONSTRUCT_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 15,
+ "description": "Time spent constructing PrefixSet from DB (ms)"
+ },
+ "URLCLASSIFIER_VLPS_FILELOAD_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "bug_numbers": [1283007],
+ "description": "Time spent loading Variable-Length PrefixSet from file (ms)"
+ },
+ "URLCLASSIFIER_VLPS_FALLOCATE_TIME": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "bug_numbers": [1283007],
+ "description": "Time spent fallocating Variable-Length PrefixSet (ms)"
+ },
+ "URLCLASSIFIER_VLPS_LOAD_CORRUPT": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "bug_numbers": [1305581],
+ "description": "Whether or not a variable-length prefix set loaded from disk is corrupted (true = file corrupted)."
+ },
+ "URLCLASSIFIER_LC_PREFIXES": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 1500000,
+ "n_buckets": 15,
+ "description": "Size of the prefix cache in entries"
+ },
+ "URLCLASSIFIER_LC_COMPLETIONS": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "Size of the completion cache in entries"
+ },
+ "URLCLASSIFIER_UPDATE_REMOTE_STATUS": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "bug_numbers": [1150921],
+ "description": "Server HTTP status code from SafeBrowsing database updates. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)"
+ },
+ "URLCLASSIFIER_COMPLETE_REMOTE_STATUS": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "bug_numbers": [1150921],
+ "description": "Server HTTP status code from remote SafeBrowsing gethash lookups. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)"
+ },
+ "URLCLASSIFIER_COMPLETE_TIMEOUT": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "56",
+ "kind": "boolean",
+ "bug_numbers": [1172688],
+ "description": "This metric is recorded every time a gethash lookup is performed, `true` is recorded if the lookup times out."
+ },
+ "URLCLASSIFIER_UPDATE_ERROR_TYPE": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1305801],
+ "description": "An error was encountered while parsing a partial update returned by a Safe Browsing V4 server (0 = addition of an already existing prefix, 1 = parser got into an infinite loop, 2 = removal index out of bounds, 3 = checksum mismatch, 4 = missing checksum)"
+ },
+ "URLCLASSIFIER_PREFIX_MATCH": {
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "enumerated",
+ "n_values": 4,
+ "bug_numbers": [1298257],
+ "description": "Classifier prefix matching result (0 = no match, 1 = match only V2, 2 = match only V4, 3 = match both V2 and V4)"
+ },
+ "CSP_DOCUMENTS_COUNT": {
+ "alert_emails": ["seceng@mozilla.com"],
+ "bug_numbers": [1252829],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Number of unique pages that contain a CSP"
+ },
+ "CSP_UNSAFE_INLINE_DOCUMENTS_COUNT": {
+ "alert_emails": ["seceng@mozilla.com"],
+ "bug_numbers": [1252829],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Number of unique pages that contain an unsafe-inline CSP directive"
+ },
+ "CSP_UNSAFE_EVAL_DOCUMENTS_COUNT": {
+ "alert_emails": ["seceng@mozilla.com"],
+ "bug_numbers": [1252829],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Number of unique pages that contain an unsafe-eval CSP directive"
+ },
+ "PLACES_DATABASE_CORRUPTION_HANDLING_STAGE": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1356812],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: stage reached when trying to fix a database corruption , see Places::Database::eCorruptDBReplaceStatus"
+ },
+ "PLACES_PAGES_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 150000,
+ "n_buckets": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Number of unique pages"
+ },
+ "PLACES_MOST_RECENT_EXPIRED_VISIT_DAYS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 30,
+ "high": 730,
+ "n_buckets": 12,
+ "description": "PLACES: the most recent expired visit in days"
+ },
+ "PLACES_BOOKMARKS_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 8000,
+ "n_buckets": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Number of bookmarks"
+ },
+ "PLACES_TAGS_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "PLACES: Number of tags"
+ },
+ "PLACES_KEYWORDS_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "PLACES: Number of keywords"
+ },
+ "PLACES_BACKUPS_DAYSFROMLAST": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "description": "PLACES: Days from last backup"
+ },
+ "PLACES_BACKUPS_BOOKMARKSTREE_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to build the bookmarks tree"
+ },
+ "PLACES_BACKUPS_TOJSON_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to convert and write the backup"
+ },
+ "PLACES_EXPORT_TOHTML_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to convert and write bookmarks.html"
+ },
+ "PLACES_FAVICON_ICO_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of the ICO favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_PNG_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of the PNG favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_GIF_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of the GIF favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_JPEG_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of the JPEG favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_BMP_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of the BMP favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_SVG_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of the SVG favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_OTHER_SIZES": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets" : 100,
+ "description": "PLACES: Size of favicon files without a specific file type probe, loaded from the web (Bytes)"
+ },
+ "LINK_ICON_SIZES_ATTR_USAGE": {
+ "expires_in_version" : "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "The possible types of the 'sizes' attribute for <link rel=icon>. 0: Attribute not specified, 1: 'any', 2: Integer dimensions, 3: Invalid value."
+ },
+ "LINK_ICON_SIZES_ATTR_DIMENSION": {
+ "expires_in_version" : "never",
+ "kind": "linear",
+ "high": 513,
+ "n_buckets" : 64,
+ "description": "The width dimension of the 'sizes' attribute for <link rel=icon>."
+ },
+ "FENNEC_DISTRIBUTION_REFERRER_INVALID": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the referrer intent specified an invalid distribution name",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_DISTRIBUTION_CODE_CATEGORY": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "First digit of HTTP result code, or error category, during distribution download",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 40000,
+ "n_buckets": 30,
+ "description": "Time taken to download a specified distribution file (msec)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_BOOKMARKS_COUNT": {
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 8000,
+ "n_buckets": 20,
+ "description": "Number of bookmarks stored in the browser DB",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "bug_numbers": [1244704]
+ },
+ "FENNEC_ORBOT_INSTALLED": {
+ "expires_in_version": "60",
+ "kind": "flag",
+ "cpp_guard": "ANDROID",
+ "description": "Whether or not users have Orbot installed",
+ "alert_emails": ["seceng@mozilla.org"],
+ "bug_numbers": [1314784]
+ },
+ "FENNEC_READING_LIST_COUNT": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "cpp_guard": "ANDROID",
+ "description": "Number of reading list items stored in the browser DB *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "FENNEC_READER_VIEW_CACHE_SIZE": {
+ "expires_in_version": "60",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "kind": "exponential",
+ "low": 32,
+ "high": 51200,
+ "n_buckets": 20,
+ "description": "Total disk space used by items in the reader view cache (KB)",
+ "bug_numbers": [1246159]
+ },
+ "PLACES_SORTED_BOOKMARKS_PERC": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "PLACES: Percentage of bookmarks organized in folders"
+ },
+ "PLACES_TAGGED_BOOKMARKS_PERC": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "PLACES: Percentage of tagged bookmarks"
+ },
+ "PLACES_DATABASE_FILESIZE_MB": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 5,
+ "high": 200,
+ "n_buckets": 10,
+ "description": "PLACES: Database filesize (MB)"
+ },
+ "PLACES_DATABASE_PAGESIZE_B": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 32768,
+ "n_buckets": 10,
+ "description": "PLACES: Database page size (bytes)"
+ },
+ "PLACES_DATABASE_SIZE_PER_PAGE_B": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 10240,
+ "n_buckets": 20,
+ "description": "PLACES: Average size of a place in the database (bytes)"
+ },
+ "PLACES_EXPIRATION_STEPS_TO_CLEAN2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "PLACES: Expiration steps to cleanup the database"
+ },
+ "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 500,
+ "n_buckets": 10,
+ "description": "PLACES: Time for first autocomplete result if > 50ms (ms)"
+ },
+ "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 1000,
+ "n_buckets": 30,
+ "description": "PLACES: Time for the 6 first autocomplete results (ms)"
+ },
+ "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 30,
+ "description": "PLACES: Time to load the sidebar history tree sorted by last visit (ms)"
+ },
+ "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 1000,
+ "n_buckets": 30,
+ "description": "PLACES: Time to search the history library (ms)"
+ },
+ "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Duration of the domain query for the url inline autocompletion (ms)"
+ },
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to decay all frecencies values on idle (ms)"
+ },
+ "PLACES_IDLE_MAINTENANCE_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to execute maintenance tasks on idle (ms)"
+ },
+ "PLACES_ANNOS_BOOKMARKS_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "PLACES: Number of bookmarks annotations"
+ },
+ "PLACES_ANNOS_PAGES_COUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "PLACES: Number of pages annotations"
+ },
+ "PLACES_MAINTENANCE_DAYSFROMLAST": {
+ "expires_in_version" : "never",
+ "kind": "exponential",
+ "low": 7,
+ "high": 60,
+ "n_buckets" : 10,
+ "description": "PLACES: Days from last maintenance"
+ },
+ "UPDATE_CHECK_NO_UPDATE_EXTERNAL" : {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of no updates were found for a background update check (externally initiated)"
+ },
+ "UPDATE_CHECK_NO_UPDATE_NOTIFY" : {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of no updates were found for a background update check (timer initiated)"
+ },
+ "UPDATE_CHECK_CODE_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: background update check result code except for no updates found (externally initiated)"
+ },
+ "UPDATE_CHECK_CODE_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: background update check result code except for no updates found (timer initiated)"
+ },
+ "UPDATE_CHECK_EXTENDED_ERROR_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (externally initiated)"
+ },
+ "UPDATE_CHECK_EXTENDED_ERROR_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (timer initiated)"
+ },
+ "UPDATE_INVALID_LASTUPDATETIME_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that have a last update time greater than the current time (externally initiated)"
+ },
+ "UPDATE_INVALID_LASTUPDATETIME_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that have a last update time greater than the current time (timer initiated)"
+ },
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 60,
+ "high": 365,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: interval in days since the last background update check (externally initiated)"
+ },
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 30,
+ "high": 180,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: interval in days since the last background update check (timer initiated)"
+ },
+ "UPDATE_PING_COUNT_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems for this ping for comparison with other pings (externally initiated)"
+ },
+ "UPDATE_PING_COUNT_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems for this ping for comparison with other pings (timer initiated)"
+ },
+ "UPDATE_SERVICE_INSTALLED_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: whether the service is installed (externally initiated)"
+ },
+ "UPDATE_SERVICE_INSTALLED_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: whether the service is installed (timer initiated)"
+ },
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that manually uninstalled the service (externally initiated)"
+ },
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that manually uninstalled the service (timer initiated)"
+ },
+ "UPDATE_UNABLE_TO_APPLY_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that cannot apply updates (externally initiated)"
+ },
+ "UPDATE_UNABLE_TO_APPLY_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that cannot apply updates (timer initiated)"
+ },
+ "UPDATE_CANNOT_STAGE_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that cannot stage updates (externally initiated)"
+ },
+ "UPDATE_CANNOT_STAGE_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of systems that cannot stage updates (timer initiated)"
+ },
+ "UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: number of sequential update elevation request cancelations greater than 0 (externally initiated)"
+ },
+ "UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: number of sequential update elevation request cancelations greater than 0 (timer initiated)"
+ },
+ "UPDATE_PREF_SERVICE_ERRORS_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: number of sequential update service errors greater than 0 (externally initiated)"
+ },
+ "UPDATE_PREF_SERVICE_ERRORS_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: number of sequential update service errors greater than 0 (timer initiated)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.auto boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.auto boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_ENABLED_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_ENABLED_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_DOWNLOAD_CODE_COMPLETE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: complete patch download result code"
+ },
+ "UPDATE_DOWNLOAD_CODE_PARTIAL": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: complete patch download result code"
+ },
+ "UPDATE_STATE_CODE_COMPLETE_STARTUP": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the state of a complete update from update.status on startup"
+ },
+ "UPDATE_STATE_CODE_PARTIAL_STARTUP": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the state of a partial patch update from update.status on startup"
+ },
+ "UPDATE_STATE_CODE_UNKNOWN_STARTUP": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the state of an unknown patch update from update.status on startup"
+ },
+ "UPDATE_STATE_CODE_COMPLETE_STAGE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the state of a complete patch update from update.status after staging"
+ },
+ "UPDATE_STATE_CODE_PARTIAL_STAGE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the state of a partial patch update from update.status after staging"
+ },
+ "UPDATE_STATE_CODE_UNKNOWN_STAGE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the state of an unknown patch update from update.status after staging"
+ },
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the status error code for a failed complete patch update from update.status on startup"
+ },
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the status error code for a failed partial patch update from update.status on startup"
+ },
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the status error code for a failed unknown patch update from update.status on startup"
+ },
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the status error code for a failed complete patch update from update.status after staging"
+ },
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the status error code for a failed partial patch update from update.status after staging"
+ },
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the status error code for a failed unknown patch update from update.status after staging"
+ },
+ "UPDATE_WIZ_LAST_PAGE_CODE": {
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the update wizard page displayed when the UI was closed (mapped in toolkit/mozapps/update/UpdateTelemetry.jsm)"
+ },
+ "THUNDERBIRD_GLODA_SIZE_MB": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 40,
+ "description": "Gloda: size of global-messages-db.sqlite (MB)"
+ },
+ "THUNDERBIRD_CONVERSATIONS_TIME_TO_2ND_GLODA_QUERY_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 30,
+ "description": "Conversations: time between the moment we click and the second gloda query returns (ms)"
+ },
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 20,
+ "description": "Gloda: indexing rate (message/s)"
+ },
+ "FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 30,
+ "description": "Firefox: Time taken to store the image capture of the page to a canvas, for reuse while swiping through history (ms)."
+ },
+ "FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 30,
+ "description": "Firefox: Time taken to kick off image compression of the canvas that will be used during swiping through history (ms)."
+ },
+ "FX_TAB_ANIM_OPEN_PREVIEW_FRAME_INTERVAL_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 7,
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Average frame interval during tab open animation of about:newtab (preview=on), when other tabs are unaffected"
+ },
+ "FX_TAB_ANIM_OPEN_FRAME_INTERVAL_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 7,
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Average frame interval during tab open animation of about:newtab (preview=off), when other tabs are unaffected"
+ },
+ "FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 7,
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Average frame interval during any tab open/close animation (excluding tabstrip scroll)"
+ },
+ "FX_REFRESH_DRIVER_CHROME_FRAME_DELAY_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1220699],
+ "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver in the chrome process."
+ },
+ "FX_REFRESH_DRIVER_CONTENT_FRAME_DELAY_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1221674],
+ "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver in the content process."
+ },
+ "FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1228147],
+ "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver while scrolling synchronously."
+ },
+ "FX_TAB_SWITCH_UPDATE_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "description": "Firefox: Time in ms spent updating UI in response to a tab switch"
+ },
+ "FX_TAB_SWITCH_TOTAL_MS": {
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: Time in ms till a tab switch is complete including the first paint"
+ },
+ "FX_TAB_SWITCH_TOTAL_E10S_MS": {
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: Time in ms between tab selection and tab content paint."
+ },
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_MS": {
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: If the spinner interstitial displays during tab switching, records the time in ms the graphic is visible"
+ },
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS": {
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 64000,
+ "n_buckets": 7,
+ "bug_numbers": [1301104],
+ "alert_emails": ["mconley@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: If the spinner interstitial displays during tab switching, records the time in ms the graphic is visible. This probe is similar to FX_TAB_SWITCH_SPINNER_VISIBLE_MS, but is for truly degenerate cases."
+ },
+ "FX_TAB_CLICK_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "description": "Firefox: Time in ms spent on switching tabs in response to a tab click"
+ },
+ "FX_BOOKMARKS_TOOLBAR_INIT_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "Firefox: Time to initialize the bookmarks toolbar view (ms)"
+ },
+ "FX_BROWSER_FULLSCREEN_USED": {
+ "expires_in_version": "46",
+ "kind": "count",
+ "description": "The number of times that a session enters browser fullscreen (f11-fullscreen)"
+ },
+ "FX_NEW_WINDOW_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "Firefox: Time taken to open a new browser window (ms)"
+ },
+ "FX_PAGE_LOAD_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "Firefox: Time taken to load a page (ms). This includes all static contents, no dynamic content. Loading of about: pages is not counted."
+ },
+ "FX_TOTAL_TOP_VISITS": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Count the number of times a new top page was starting to load"
+ },
+ "FX_THUMBNAILS_CAPTURE_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "description": "THUMBNAILS: Time (ms) it takes to capture a thumbnail"
+ },
+ "FX_THUMBNAILS_STORE_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "description": "THUMBNAILS: Time (ms) it takes to store a thumbnail in the cache"
+ },
+ "FX_THUMBNAILS_HIT_OR_MISS": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "THUMBNAILS: Thumbnail found"
+ },
+ "FX_MIGRATION_ENTRY_POINT": {
+ "bug_numbers": [731025],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Where the migration wizard was entered from. 0=Other/catch-all, 1=first-run, 2=refresh-firefox, 3=Places window, 4=Password manager"
+ },
+ "FX_MIGRATION_SOURCE_BROWSER": {
+ "bug_numbers": [731025],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "The browser that data is pulled from. The values correspond to the internal browser ID (see MigrationUtils.jsm)"
+ },
+ "FX_MIGRATION_ERRORS": {
+ "bug_numbers": [731025],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 12,
+ "releaseChannelCollection": "opt-out",
+ "description": "Errors encountered during migration in buckets defined by the datatype, keyed by the string description of the browser."
+ },
+ "FX_MIGRATION_USAGE": {
+ "bug_numbers": [731025],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 12,
+ "releaseChannelCollection": "opt-out",
+ "description": "Usage of migration for each datatype when migration is run through the post-firstrun flow which allows individual datatypes, keyed by the string description of the browser."
+ },
+ "FX_MIGRATION_IMPORTED_HOMEPAGE": {
+ "bug_numbers": [731025, 1298208],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether the homepage was imported during browser migration. Only available on release builds during firstrun."
+ },
+ "FX_MIGRATION_BOOKMARKS_IMPORT_MS": {
+ "bug_numbers": [1289436],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "54",
+ "kind": "exponential",
+ "n_buckets": 70,
+ "high": 100000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How long it took to import bookmarks from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_HISTORY_IMPORT_MS": {
+ "bug_numbers": [1289436],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "54",
+ "kind": "exponential",
+ "n_buckets": 70,
+ "high": 100000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How long it took to import history from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_LOGINS_IMPORT_MS": {
+ "bug_numbers": [1289436],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "54",
+ "kind": "exponential",
+ "n_buckets": 70,
+ "high": 100000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How long it took to import logins (passwords) from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_BOOKMARKS_JANK_MS": {
+ "bug_numbers": [1338522],
+ "alert_emails": ["dao@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing bookmarks from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably."
+ },
+ "FX_MIGRATION_HISTORY_JANK_MS": {
+ "bug_numbers": [1338522],
+ "alert_emails": ["dao@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing history from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably."
+ },
+ "FX_MIGRATION_LOGINS_JANK_MS": {
+ "bug_numbers": [1338522],
+ "alert_emails": ["dao@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing logins / passwords from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably."
+ },
+ "FX_MIGRATION_BOOKMARKS_QUANTITY": {
+ "bug_numbers": [1279501],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many bookmarks we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_HISTORY_QUANTITY": {
+ "bug_numbers": [1279501],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "n_buckets": 40,
+ "high": 10000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many history visits we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_LOGINS_QUANTITY": {
+ "bug_numbers": [1279501],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many logins (passwords) we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_STARTUP_MIGRATION_BROWSER_COUNT": {
+ "bug_numbers": [1275114],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of browsers from which the user could migrate on initial profile migration. Only available on release builds during firstrun."
+ },
+ "FX_STARTUP_MIGRATION_EXISTING_DEFAULT_BROWSER": {
+ "bug_numbers": [1275114],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "The browser that was the default on the initial profile migration. The values correspond to the internal browser ID (see MigrationUtils.jsm)"
+ },
+ "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_PROCESS_SUCCESS": {
+ "bug_numbers": [1271775],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 27,
+ "releaseChannelCollection": "opt-out",
+ "description": "Where automatic migration was attempted, indicates to what degree we succeeded. Values 0-25 indicate progress through the automatic migration sequence, with 25 indicating the migration finished. 26 is only used when the migration produced errors before it finished."
+ },
+ "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_UNDO": {
+ "bug_numbers": [1283565],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 31,
+ "releaseChannelCollection": "opt-out",
+ "description": "Where undo of the automatic migration was attempted, indicates to what degree we succeeded to undo. 0 means we started to undo, 5 means we bailed out from the undo because it was not possible to complete it (there was nothing to undo or the user was signed in to sync). All higher values indicate progression through the undo sequence, with 30 indicating we finished the undo without exceptions in the middle."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_REASON": {
+ "bug_numbers": [1289906],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "54",
+ "keyed": true,
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Why the undo functionality of an automatic migration was disabled: 0 means we used undo, 1 means the user signed in to sync, 2 means the user created/modified a password, 3 means the user created/modified a bookmark (item or folder), 4 means we showed an undo option repeatedly and the user did not use it, 5 means we showed an undo option and the user actively elected to keep the data. The whole thing is keyed to the identifiers of different browsers (so 'chrome', 'ie', 'edge', 'safari', etc.)."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_OFFERED": {
+ "bug_numbers": [1309617],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "56",
+ "kind": "enumerated",
+ "n_values": 5,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates we showed a 'would you like to undo this automatic migration?' notification bar. The bucket indicates which nth day we're on (1st/2nd/3rd, by default - 0 would be indicative the pref didn't get set which shouldn't happen). After 3 days on which the notification gets shown, it will get disabled and never shown again."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_ERRORCOUNT": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how many errors we find when trying to 'undo' bookmarks import. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_LOGINS_ERRORCOUNT": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how many errors we find when trying to 'undo' login (password) import. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_VISITS_ERRORCOUNT": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how many errors we find when trying to 'undo' history import. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_MS": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how long it took to undo the startup import of bookmarks, in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_LOGINS_MS": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how long it took to undo the startup import of logins, in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_VISITS_MS": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how long it took to undo the startup import of visits (history), in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_UNDO_TOTAL_MS": {
+ "bug_numbers": [1333233],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "58",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "description": "Indicates how long it took to undo the entirety of the startup undo, in ms. Keys are internal ids of browsers we import from, e.g. 'chrome' or 'ie', etc."
+ },
+ "FX_STARTUP_MIGRATION_DATA_RECENCY": {
+ "bug_numbers": [1276694],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "keyed": true,
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 8760,
+ "releaseChannelCollection": "opt-out",
+ "description": "The 'last modified' time of the data we imported on the initial profile migration (time delta with 'now' at the time of migration, in hours). Collected for all browsers for which migration data is available, and stored keyed by browser identifier (e.g. 'ie', 'edge', 'safari', etc.)."
+ },
+ "FX_STARTUP_MIGRATION_USED_RECENT_BROWSER": {
+ "bug_numbers": [1276694],
+ "alert_emails": ["gijs@mozilla.com"],
+ "expires_in_version": "53",
+ "keyed": true,
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether the browser we migrated from was the browser with the most recent data. Keyed by that browser's identifier (e.g. 'ie', 'edge', 'safari', etc.)."
+ },
+ "FX_STARTUP_EXTERNAL_CONTENT_HANDLER": {
+ "bug_numbers": [1276027],
+ "alert_emails": ["jaws@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Count how often the browser is opened as an external app handler. This is generally used when the browser is set as the default browser."
+ },
+ "FX_PREFERENCES_CATEGORY_OPENED": {
+ "bug_numbers": [1324167],
+ "alert_emails": ["jaws@mozilla.com"],
+ "expires_in_version": "56",
+ "kind": "categorical",
+ "labels": ["unknown", "general", "search", "content", "applications", "privacy", "security", "sync", "advancedGeneral", "advancedDataChoices", "advancedNetwork", "advancedUpdates", "advancedCerts"],
+ "releaseChannelCollection": "opt-out",
+ "description": "Count how often each preference category is opened."
+ },
+ "INPUT_EVENT_RESPONSE_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1235908],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time (ms) from the Input event being created to the end of it being handled"
+ },
+ "LOAD_INPUT_EVENT_RESPONSE_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1298101],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time (ms) from the Input event being created to the end of it being handled for events handling during page load only"
+ },
+ "EVENTLOOP_UI_ACTIVITY_EXP_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1198196],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Widget: Time it takes for the message before a UI message (ms)"
+ },
+ "FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Session restore: Time it takes to prepare the data structures for restoring a session (ms)"
+ },
+ "FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Session restore: Time it takes to finish restoration once we have first opened a window (ms)"
+ },
+ "FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to collect all window data (ms)"
+ },
+ "FX_SESSION_RESTORE_COLLECT_COOKIES_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to collect cookies (ms)"
+ },
+ "FX_SESSION_RESTORE_COLLECT_DATA_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to collect all window and tab data (ms)"
+ },
+ "FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Duration of the longest uninterruptible operation while collecting all window and tab data (ms)"
+ },
+ "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Duration of the longest uninterruptible operation while collecting data in the content process (ms)"
+ },
+ "FX_SESSION_RESTORE_SERIALIZE_DATA_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to JSON serialize session data (ms)"
+ },
+ "FX_SESSION_RESTORE_READ_FILE_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to read the session data from the file on disk (ms)"
+ },
+ "FX_SESSION_RESTORE_WRITE_FILE_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to write the session data to the file on disk (ms)"
+ },
+ "FX_SESSION_RESTORE_FILE_SIZE_BYTES": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 50000000,
+ "n_buckets": 30,
+ "description": "Session restore: The size of file sessionstore.js (bytes)"
+ },
+ "FX_SESSION_RESTORE_CORRUPT_FILE": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Session restore: Whether the file read on startup contained parse-able JSON"
+ },
+ "FX_SESSION_RESTORE_ALL_FILES_CORRUPT": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Session restore: Whether none of the backup files contained parse-able JSON"
+ },
+ "FX_SESSION_RESTORE_RESTORE_WINDOW_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)"
+ },
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "Count of messages sent by SessionRestore from child frames to the parent and that cannot be transmitted as they eat up too much memory."
+ },
+ "FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000000,
+ "n_buckets": 20,
+ "description": "Session restore: Number of characters in DOM Storage for a tab. Pages without DOM Storage or with an empty DOM Storage are ignored."
+ },
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 100,
+ "high": 100000,
+ "n_buckets": 20,
+ "description": "Session restore: If the browser is setup to auto-restore tabs, this probe measures the time elapsed between the instant we start Session Restore and the instant we have finished restoring tabs eagerly. At this stage, the tabs that are restored on demand are not restored yet."
+ },
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS": {
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 100,
+ "high": 100000,
+ "n_buckets": 20,
+ "description": "Session restore: If a session is restored by the user clicking on 'Restore Session', this probe measures the time elapsed between the instant the user has clicked and the instant we have finished restoring tabs eagerly. At this stage, the tabs that are restored on demand are not restored yet."
+ },
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 20,
+ "description": "Session restore: Number of tabs in the session that has just been restored."
+ },
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Session restore: Number of windows in the session that has just been restored."
+ },
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Session restore: Number of tabs restored eagerly in the session that has just been restored."
+ },
+ "FX_TABLETMODE_PAGE_LOAD": {
+ "expires_in_version": "47",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 30,
+ "keyed": true,
+ "description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)"
+ },
+ "FX_TOUCH_USED": {
+ "expires_in_version": "46",
+ "kind": "count",
+ "description": "Windows only. Counts occurrences of touch events"
+ },
+ "FX_URLBAR_SELECTED_RESULT_INDEX": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 17,
+ "bug_numbers": [775825],
+ "description": "Firefox: The index of the selected result in the URL bar popup"
+ },
+ "FX_URLBAR_SELECTED_RESULT_TYPE": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 14,
+ "bug_numbers": [775825],
+ "description": "Firefox: The type of the selected result in the URL bar popup. See nsBrowserGlue.js::_handleURLBarTelemetry for the result types."
+ },
+ "INNERWINDOWS_WITH_MUTATION_LISTENERS": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Deleted or to-be-reused innerwindow which has had mutation event listeners."
+ },
+ "CHARSET_OVERRIDE_SITUATION": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Labeling status of top-level page when overriding charset (0: unlabeled file URL without detection, 1: unlabeled non-TLD-guessed non-file URL without detection, 2: unlabeled file URL with detection, 3: unlabeled non-file URL with detection, 4: labeled, 5: already overridden, 6: bug, 7: unlabeled with TLD guessing)"
+ },
+ "CHARSET_OVERRIDE_USED": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the character encoding menu was used to override an encoding in this session."
+ },
+ "DECODER_INSTANTIATED_ISO2022JP": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for ISO-2022-JP has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_IBM866": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for IBM866 has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACGREEK": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACGREEK has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACICELANDIC": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACICELANDIC has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACCE": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACCE has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACHEBREW": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACHEBREW has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACARABIC": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACARABIC has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACFARSI": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACFARSI has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACCROATIAN": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACCROATIAN has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACCYRILLIC": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACCYRILLIC has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACROMANIAN": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACROMANIAN has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACTURKISH": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACTURKISH has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACDEVANAGARI": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACDEVANAGARI has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACGUJARATI": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACGUJARATI has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_MACGURMUKHI": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for MACGURMUKHI has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_KOI8R": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for KOI8R has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_KOI8U": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for KOI8U has been instantiated in this session."
+ },
+ "DECODER_INSTANTIATED_ISO_8859_5": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether the decoder for ISO-8859-5 has been instantiated in this session."
+ },
+ "LONG_REFLOW_INTERRUPTIBLE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Long running reflow, interruptible or not"
+ },
+ "XMLHTTPREQUEST_ASYNC_OR_SYNC": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Type of XMLHttpRequest, async or sync"
+ },
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to flush and close the localStorage database (ms)"
+ },
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "True when we had to wait for a pending preload on first access to localStorage data, false otherwise"
+ },
+ "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return a list of all keys in domain's LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return a key name in domain's LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return number of keys in domain's LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return a value for a key in LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we set a single key's value in LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we remove a single key from LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we clear LocalStorage for all domains (ms)"
+ },
+ "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to fetch LocalStorage data before we can clean the cache (ms)"
+ },
+ "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS": {
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to fetch LocalStorage data before we can expose them as session only data (ms)"
+ },
+ "RANGE_CHECKSUM_ERRORS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with range checksum errors"
+ },
+ "BUCKET_ORDER_ERRORS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with bucket order errors"
+ },
+ "TOTAL_COUNT_HIGH_ERRORS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with total count high errors"
+ },
+ "TOTAL_COUNT_LOW_ERRORS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with total count low errors"
+ },
+ "TELEMETRY_ARCHIVE_DIRECTORIES_COUNT": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 13,
+ "n_buckets": 12,
+ "bug_numbers": [1162538],
+ "description": "Number of directories in the archive at scan"
+ },
+ "TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 13,
+ "n_buckets": 12,
+ "bug_numbers": [1162538],
+ "description": "The age of the oldest Telemetry archive directory in months"
+ },
+ "TELEMETRY_ARCHIVE_SCAN_PING_COUNT": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry pings in the archive at scan"
+ },
+ "TELEMETRY_ARCHIVE_SESSION_PING_COUNT": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry pings added to the archive during the session"
+ },
+ "TELEMETRY_ARCHIVE_SIZE_MB": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 300,
+ "n_buckets": 60,
+ "bug_numbers": [1162538],
+ "description": "The size of the Telemetry archive (MB)"
+ },
+ "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry pings evicted from the archive during cleanup, because they were over the quota"
+ },
+ "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 13,
+ "n_buckets": 12,
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry directories evicted from the archive during cleanup, because they were too old"
+ },
+ "TELEMETRY_ARCHIVE_EVICTING_DIRS_MS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "bug_numbers": [1162538],
+ "description": "Time (ms) it takes for evicting old directories"
+ },
+ "TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "bug_numbers": [1162538],
+ "description": "Time (ms) it takes for checking if the archive is over-quota"
+ },
+ "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "bug_numbers": [1162538],
+ "description": "Time (ms) it takes for evicting over-quota pings"
+ },
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of pending Telemetry pings that failed to load from the disk"
+ },
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of pending Telemetry pings that failed to parse once loaded from the disk"
+ },
+ "TELEMETRY_PENDING_PINGS_SIZE_MB": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 17,
+ "n_buckets": 16,
+ "description": "The size of the Telemetry pending pings directory (MB). The special value 17 is used to indicate over quota pings."
+ },
+ "TELEMETRY_PENDING_PINGS_AGE": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 365,
+ "n_buckets": 30,
+ "description": "The age, in days, of the pending pings."
+ },
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "description": "Number of Telemetry pings evicted from the pending pings directory during cleanup, because they were over the quota"
+ },
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "description": "Time (ms) it takes for evicting over-quota pending pings"
+ },
+ "TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "description": "Time (ms) it takes for checking if the pending pings are over-quota"
+ },
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of Telemetry pings discarded before sending because they exceeded the maximum size"
+ },
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of Telemetry pending pings discarded because they exceeded the maximum size"
+ },
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of archived Telemetry pings discarded because they exceeded the maximum size"
+ },
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1233986],
+ "description": "The number of pings that were submitted and had to wait for a client id (i.e. before it was cached or loaded from disk)"
+ },
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The size (MB) of the Telemetry pending pings exceeding the maximum file size"
+ },
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The size (MB) of the Telemetry archived, compressed, pings exceeding the maximum file size"
+ },
+ "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The size (MB) of the ping data submitted to Telemetry exceeding the maximum size"
+ },
+ "TELEMETRY_DISCARDED_CONTENT_PINGS_COUNT": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count of discarded content payloads."
+ },
+ "TELEMETRY_COMPRESS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time taken to compress telemetry object (ms)"
+ },
+ "TELEMETRY_SEND_SUCCESS" : {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1318284],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "description": "Time needed (in ms) for a successful send of a Telemetry ping to the servers and getting a reply back."
+ },
+ "TELEMETRY_SEND_FAILURE" : {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1318284],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "description": "Time needed (in ms) for a failed send of a Telemetry ping to the servers and getting a reply back."
+ },
+ "TELEMETRY_STRINGIFY" : {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to stringify telemetry object (ms)"
+ },
+ "TELEMETRY_SUCCESS": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Successful telemetry submission"
+ },
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "Count of individual invalid ping types that were submitted to Telemetry."
+ },
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1292226],
+ "kind": "count",
+ "description": "Count of individual invalid payloads that were submitted to Telemetry."
+ },
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of Telemetry ping files evicted due to server errors (4XX HTTP code received)"
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_LOAD": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to load the session data from disk."
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_PARSE": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to parse the session data loaded from disk."
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_VALIDATION": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to validate the session data loaded from disk."
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_SAVE": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to save the session data to disk."
+ },
+ "TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1250640],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Count of exceptions in TelemetrySession.getSessionPayload()."
+ },
+ "TELEMETRY_SCHEDULER_TICK_EXCEPTION": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1250640],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Count of exceptions during executing the TelemetrySession scheduler tick logic."
+ },
+ "TELEMETRY_SCHEDULER_WAKEUP": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1250640],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Count of TelemetrySession scheduler ticks that were delayed long enough to suspect sleep."
+ },
+ "TELEMETRY_SCHEDULER_SEND_DAILY": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1250640],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Count of TelemetrySession triggering a daily ping."
+ },
+ "TELEMETRY_TEST_FLAG": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_COUNT": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_COUNT2": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1288745],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "a testing histogram; not meant to be touched - initially not recording"
+ },
+ "TELEMETRY_TEST_CATEGORICAL": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1188888],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "CommonLabel",
+ "Label2",
+ "Label3"
+ ],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_CATEGORICAL_OPTOUT": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1188888],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "CommonLabel",
+ "Label4",
+ "Label5",
+ "Label6"
+ ],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "a testing histogram; not meant to be touched - initially not recording"
+ },
+ "TELEMETRY_TEST_KEYED_FLAG": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "keyed": true,
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_COUNT": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_BOOLEAN": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "bug_numbers": [1299144],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_RELEASE_OPTOUT": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_RELEASE_OPTIN": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "releaseChannelCollection": "opt-in",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "keyed": true,
+ "releaseChannelCollection": "opt-in",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_EXPONENTIAL": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1288745],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_LINEAR": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1288745],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_BOOLEAN": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version" : "never",
+ "kind": "boolean",
+ "bug_numbers": [1288745],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "STARTUP_CRASH_DETECTED": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether there was a crash during the last startup"
+ },
+ "SAFE_MODE_USAGE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Whether the user is in safe mode (No, Yes, Forced)"
+ },
+ "SCRIPT_BLOCK_INCORRECT_MIME": {
+ "alert_emails": ["ckerschbaumer@mozilla.com"],
+ "bug_numbers": [1288361, 1299267],
+ "expires_in_version": "56",
+ "kind": "enumerated",
+ "n_values": 15,
+ "description": "Whether the script load has a MIME type of ...? (0=unknown, 1=js, 2=image, 3=audio, 4=video, 5=text/plain, 6=text/csv, 7=text/xml, 8=application/octet-stream, 9=application/xml, 10=text/html, 11=empty)"
+ },
+ "XCTO_NOSNIFF_BLOCK_IMAGE": {
+ "alert_emails": ["ckerschbaumer@mozilla.com"],
+ "bug_numbers": [1302539],
+ "expires_in_version": "56",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Whether XCTO: nosniff would allow/block an image load? (0=allow, 1=block)"
+ },
+ "NEWTAB_PAGE_ENABLED": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "New tab page is enabled."
+ },
+ "NEWTAB_PAGE_ENHANCED": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "New tab page is enhanced (showing suggestions)."
+ },
+ "NEWTAB_PAGE_LIFE_SPAN": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1200,
+ "n_buckets": 100,
+ "description": "Life-span of a new tab without suggested tile: time delta between first-visible and unload events (half-seconds)."
+ },
+ "NEWTAB_PAGE_LIFE_SPAN_SUGGESTED": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1200,
+ "n_buckets": 100,
+ "description": "Life-span of a new tab with suggested tile: time delta between first-visible and unload events (half-seconds)."
+ },
+ "NEWTAB_PAGE_PINNED_SITES_COUNT": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 9,
+ "description": "Number of pinned sites on the new tab page."
+ },
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "Number of sites blocked from the new tab page."
+ },
+ "NEWTAB_PAGE_SHOWN": {
+ "expires_in_version": "35",
+ "kind": "boolean",
+ "description": "Number of times about:newtab was shown from opening a new tab or window. *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "NEWTAB_PAGE_SITE_CLICKED": {
+ "expires_in_version": "35",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Track click count on about:newtab tiles per index (0-8). For non-default row or column configurations all clicks into the '9' bucket. *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
+ },
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 20,
+ "description": "Number of bookmarks in the original XUL places database",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_GLOBALHISTORY_ADD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 10,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time for a record to be added to history (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_GLOBALHISTORY_UPDATE_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 10,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time for a record to be updated in history (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 10,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time to update the visited link set (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_RESTORING_ACTIVITY": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Fennec is starting up but the Gecko thread was still running",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_SEARCH_LOADER_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 10,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time for a URL bar DB search to return (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_STARTUP_TIME_GECKOREADY": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time for the Gecko:Ready message to arrive (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_STARTUP_TIME_JAVAUI": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Time for the Java UI to load (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_TOPSITES_LOADER_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 10,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time for the home screen Top Sites query to return with no filter set (ms)",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_ACTIVITY_STREAM_TOPSITES_LOADER_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 10,
+ "high": 20000,
+ "n_buckets": 20,
+ "description": "Time for the Activity Stream home screen Top Sites query to return (ms)",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "bug_numbers": [1293790],
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_HOMEPANELS_CUSTOM": {
+ "expires_in_version": "54",
+ "kind": "boolean",
+ "bug_numbers": [1245368],
+ "description": "Whether the user has customized their homepanels",
+ "cpp_guard": "ANDROID"
+ },
+ "FENNEC_WAS_KILLED": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Killed, likely due to an OOM condition",
+ "cpp_guard": "ANDROID"
+ },
+ "FIPS_ENABLED": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "54",
+ "kind": "flag",
+ "bug_numbers": [1241317],
+ "releaseChannelCollection": "opt-out",
+ "description": "Has FIPS mode been enabled?"
+ },
+ "SECURITY_UI": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "Security UI Telemetry"
+ },
+ "JS_TELEMETRY_ADDON_EXCEPTIONS" : {
+ "expires_in_version" : "never",
+ "kind": "count",
+ "keyed" : true,
+ "description" : "Exceptions thrown by add-ons"
+ },
+ "IPC_TRANSACTION_CANCEL": {
+ "alert_emails": ["billm@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "True when an IPC transaction is canceled"
+ },
+ "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "low": 100,
+ "high": 10000000,
+ "n_buckets": 10,
+ "description": "Whenever the same-process MessageManager cannot be sent through sendAsyncMessage as it would cause an OOM, the size of the message content, in kb."
+ },
+ "SLOW_ADDON_WARNING_STATES": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "The states the Slow Add-on Warning goes through. 0: Displayed the warning. 1: User clicked on 'Disable add-on'. 2: User clicked 'Ignore add-on for now'. 3: User clicked 'Ignore add-on permanently'. 4: User closed notification. Other values are reserved for future uses."
+ },
+ "SLOW_ADDON_WARNING_RESPONSE_TIME": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 86400000,
+ "n_buckets": 30,
+ "description": "Time elapsed between before responding to Slow Add-on Warning UI (ms). Not updated if the user doesn't respond at all."
+ },
+ "SEARCH_COUNTS": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Record the search counts for search engines"
+ },
+ "SEARCH_RESET_RESULT": {
+ "alert_emails": ["fqueze@mozilla.com"],
+ "bug_numbers": [1203168],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 5,
+ "releaseChannelCollection": "opt-out",
+ "description": "Result of showing the search reset prompt to the user. 0=restored original default, 1=kept current engine, 2=changed engine, 3=closed the page, 4=opened search settings"
+ },
+ "SEARCH_SERVICE_INIT_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 15,
+ "description": "Time (ms) it takes to initialize the search service"
+ },
+ "SEARCH_SERVICE_INIT_SYNC": {
+ "alert_emails": ["rvitillo@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "search service has been initialized synchronously"
+ },
+ "SEARCH_SERVICE_ENGINE_COUNT": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["florian@mozilla.com"],
+ "expires_in_version": "55",
+ "bug_numbers": [1268424],
+ "kind": "linear",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Recorded once per session near startup: records the search plugin count, including both built-in plugins (including the ones the user has hidden) and user-installed plugins."
+ },
+ "SEARCH_SERVICE_HAS_UPDATES": {
+ "alert_emails": ["florian@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1259510],
+ "description": "Recorded once per session near startup: records true/false whether the search service has engines with update URLs.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "SEARCH_SERVICE_HAS_ICON_UPDATES": {
+ "alert_emails": ["florian@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1259510],
+ "description": "Recorded once per session near startup: records true/false whether the search service has engines with icon update URLs.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 30,
+ "high": 100000,
+ "description": "Time (ms) it takes to fetch the country code"
+ },
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Result of XHR request fetching the country-code. 0=SUCCESS, 1=SUCCESS_WITHOUT_DATA, 2=XHRTIMEOUT, 3=ERROR (rest reserved for finer-grained error codes later)"
+ },
+ "SEARCH_SERVICE_COUNTRY_TIMEOUT": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "True if we stopped waiting for the XHR response before it completed"
+ },
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "True if the search service was synchronously initialized while we were waiting for the XHR response"
+ },
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the fetched country-code indicates US but the time-zone heuristic doesn't"
+ },
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the time-zone heuristic indicates US but the fetched country code doesn't"
+ },
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on OSX and either the OSX countryCode or the geoip countryCode indicates we are in the US, set to false if they both do or true otherwise"
+ },
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on OSX and neither the OSX countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise"
+ },
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on Windows and either the Windows countryCode or the geoip countryCode indicates we are in the US, set to false if they both do or true otherwise"
+ },
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN": {
+ "alert_emails": ["mhammond@mozilla.com", "gavin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on Windows and neither the Windows countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise"
+ },
+ "SOCIAL_ENABLED_ON_SESSION": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Social has been enabled at least once on the current session"
+ },
+ "ENABLE_PRIVILEGE_EVER_CALLED": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether enablePrivilege has ever been called during the current session"
+ },
+ "SUBJECT_PRINCIPAL_ACCESSED_WITHOUT_SCRIPT_ON_STACK": {
+ "expires_in_version": "46",
+ "alert_emails": ["bholley@mozilla.com"],
+ "kind": "flag",
+ "description": "Whether the subject principal was accessed without script on the stack during the current session"
+ },
+ "TOUCH_ENABLED_DEVICE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "The device supports touch input",
+ "cpp_guard": "XP_WIN"
+ },
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether content ever accesed the Components shim in this session"
+ },
+ "CHECK_ADDONS_MODIFIED_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 15,
+ "description": "Time (ms) it takes to figure out extension last modified time"
+ },
+ "TELEMETRY_MEMORY_REPORTER_MS": {
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "Time (ms) it takes to run memory reporters when sending a telemetry ping"
+ },
+ "SSL_SUCCESFUL_CERT_VALIDATION_TIME_MOZILLAPKIX" : {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time spent on a successful cert verification in mozilla::pkix mode (ms)"
+ },
+ "SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_MOZILLAPKIX" : {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time spent on an initially failed cert verification in mozilla::pkix mode (ms)"
+ },
+ "CRASH_STORE_COMPRESSED_BYTES": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 202,
+ "description": "Size (in bytes) of the compressed crash store JSON file."
+ },
+ "PDF_VIEWER_USED": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "How many times PDF Viewer was used"
+ },
+ "PDF_VIEWER_FALLBACK_SHOWN": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "How many times PDF Viewer fallback bar was shown"
+ },
+ "PDF_VIEWER_PRINT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "How many times PDF Viewer print functionality was used"
+ },
+ "PDF_VIEWER_DOCUMENT_VERSION": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "The PDF document version (1.1, 1.2, etc.)"
+ },
+ "PDF_VIEWER_DOCUMENT_GENERATOR": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 30,
+ "description": "The PDF document generator"
+ },
+ "PDF_VIEWER_DOCUMENT_SIZE_KB": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 2,
+ "high": 65536,
+ "n_buckets": 20,
+ "description": "The PDF document size (KB)"
+ },
+ "PDF_VIEWER_FONT_TYPES": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 19,
+ "description": "The PDF document font types used"
+ },
+ "PDF_VIEWER_EMBED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "A PDF document was embedded: true using OBJECT/EMBED and false using IFRAME"
+ },
+ "PDF_VIEWER_FORM": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "A PDF form expected: true for AcroForm and false for XFA"
+ },
+ "PDF_VIEWER_STREAM_TYPES": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 19,
+ "description": "The PDF document compression stream types used"
+ },
+ "PDF_VIEWER_TIME_TO_VIEW_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to display first page in PDF Viewer (ms)"
+ },
+ "PLUGINS_NOTIFICATION_SHOWN": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "The number of times the click-to-activate notification was shown: false: shown by in-content activation true: shown by location bar activation"
+ },
+ "PLUGINS_NOTIFICATION_PLUGIN_COUNT": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The number of plugins present in the click-to-activate notification, minus one (1, 2, 3, 4, more than 4)"
+ },
+ "PLUGINS_NOTIFICATION_USER_ACTION": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "User actions taken in the plugin notification: 0: allownow 1: allowalways 2: block"
+ },
+ "PLUGINS_INFOBAR_SHOWN": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Count of when the hidden-plugin infobar was displayed."
+ },
+ "PLUGINS_INFOBAR_BLOCK": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Count the number of times the user clicked 'block' on the hidden-plugin infobar."
+ },
+ "PLUGINS_INFOBAR_ALLOW": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Count the number of times the user clicked 'allow' on the hidden-plugin infobar."
+ },
+ "POPUP_NOTIFICATION_STATS": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1207089],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 40,
+ "description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
+ },
+ "POPUP_NOTIFICATION_MAIN_ACTION_MS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1207089],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "keyed": true,
+ "low": 100,
+ "high": 600000,
+ "n_buckets": 40,
+ "description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID"
+ },
+ "POPUP_NOTIFICATION_DISMISSAL_MS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1207089],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "keyed": true,
+ "low": 200,
+ "high": 20000,
+ "n_buckets": 50,
+ "description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID"
+ },
+ "PRINT_PREVIEW_OPENED_COUNT": {
+ "alert_emails": ["carnold@mozilla.org"],
+ "bug_numbers": [1275570],
+ "expires_in_version": "56",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "A counter incremented every time the browser enters print preview."
+ },
+ "PRINT_PREVIEW_SIMPLIFY_PAGE_OPENED_COUNT": {
+ "alert_emails": ["carnold@mozilla.org"],
+ "bug_numbers": [1275570],
+ "expires_in_version": "56",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "A counter incremented every time the browser enters simplified mode on print preview."
+ },
+ "PRINT_PREVIEW_SIMPLIFY_PAGE_UNAVAILABLE_COUNT": {
+ "alert_emails": ["carnold@mozilla.org"],
+ "bug_numbers": [1287587],
+ "expires_in_version": "56",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "A counter incremented every time the simplified mode is unavailable on print preview."
+ },
+ "PRINT_DIALOG_OPENED_COUNT": {
+ "alert_emails": ["carnold@mozilla.org"],
+ "bug_numbers": [1306624],
+ "expires_in_version": "56",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "A counter incremented every time the user opens print dialog."
+ },
+ "PRINT_COUNT": {
+ "alert_emails": ["carnold@mozilla.org"],
+ "bug_numbers": [1287587],
+ "expires_in_version": "56",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "A counter incremented every time the user prints a document."
+ },
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to display a selected source to the user."
+ },
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to display a selected source to the user."
+ },
+ "MEDIA_RUST_MP4PARSE_SUCCESS": {
+ "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1220885],
+ "description": "(Bug 1220885) Whether the rust mp4 demuxer successfully parsed a stream segment.",
+ "cpp_guard": "MOZ_RUST_MP4PARSE"
+ },
+ "MEDIA_RUST_MP4PARSE_ERROR_CODE": {
+ "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 32,
+ "bug_numbers": [1238420],
+ "description": "The error code reported when an MP4 parse attempt has failed.0 = OK, 1 = bad argument, 2 = invalid data, 3 = unsupported, 4 = unexpected end of file, 5 = read error.",
+ "cpp_guard": "MOZ_RUST_MP4PARSE"
+ },
+ "MEDIA_RUST_MP4PARSE_TRACK_MATCH_AUDIO": {
+ "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1231169],
+ "description": "Whether rust and stagefight mp4 parser audio track results match.",
+ "cpp_guard": "MOZ_RUST_MP4PARSE"
+ },
+ "MEDIA_RUST_MP4PARSE_TRACK_MATCH_VIDEO": {
+ "alert_emails": ["giles@mozilla.com", "kinetik@flim.org"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "bug_numbers": [1231169],
+ "description": "Whether rust and stagefight mp4 parser video track results match.",
+ "cpp_guard": "MOZ_RUST_MP4PARSE"
+ },
+ "MEDIA_WMF_DECODE_ERROR": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 256,
+ "description": "WMF media decoder error or success (0) codes."
+ },
+ "MEDIA_OGG_LOADED_IS_CHAINED": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "description": "Whether Ogg audio/video encountered are chained or not.",
+ "bug_numbers": [1230295]
+ },
+ "MEDIA_HLS_CANPLAY_REQUESTED": {
+ "alert_emails": ["ajones@mozilla.com", "giles@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "description": "Reports a true value when a page requests canPlayType for an HTTP Live Streaming media type (or generic m3u playlist).",
+ "bug_numbers": [1262659]
+ },
+ "MEDIA_HLS_DECODER_SUCCESS": {
+ "alert_emails": ["ajones@mozilla.com", "giles@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "description": "Reports whether a decoder for an HTTP Live Streaming media type was created when requested.",
+ "bug_numbers": [1262659]
+ },
+ "MEDIA_DECODING_PROCESS_CRASH": {
+ "alert_emails": ["bwu@mozilla.com", "jolin@mozilla.com", "jacheng@mozilla.com"],
+ "expires_in_version": "57",
+ "kind": "count",
+ "bug_numbers": [1297556, 1257777],
+ "description": "Records a value each time Fennec remote decoding process crashes unexpected while decoding media content.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_MFT_OUTPUT_NULL_SAMPLES": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Does the WMF video decoder return success but null output? 0 = playback successful, 1 = excessive null output but able to decode some frames, 2 = excessive null output and gave up, 3 = null output but recovered, 4 = non-excessive null output without being able to decode frames.",
+ "bug_numbers": [1176071]
+ },
+ "AUDIO_MFT_OUTPUT_NULL_SAMPLES": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "How many times the audio MFT decoder returns success but output nothing.",
+ "bug_numbers": [1176071]
+ },
+ "VIDEO_CAN_CREATE_AAC_DECODER": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "description": "Whether at startup we report we can playback MP4 (AAC) audio. This is single value is recorded at every startup.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_CAN_CREATE_H264_DECODER": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "description": "Whether at startup we report we can playback MP4 (H.264) video. This is single value is recorded at every startup.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG": {
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 128,
+ "description": "The H.264 constraint set flag as extracted from the codecs parameter passed to HTMLMediaElement.canPlayType, with the addition of 0 for unknown values."
+ },
+ "VIDEO_CANPLAYTYPE_H264_LEVEL": {
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 51,
+ "description": "The H.264 level (level_idc) as extracted from the codecs parameter passed to HTMLMediaElement.canPlayType, from levels 1 (10) to 5.2 (51), with the addition of 0 for unknown values."
+ },
+ "VIDEO_CANPLAYTYPE_H264_PROFILE": {
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 244,
+ "description": "The H.264 profile number (profile_idc) as extracted from the codecs parameter passed to HTMLMediaElement.canPlayType."
+ },
+ "DECODER_DOCTOR_INFOBAR_STATS": {
+ "alert_emails": ["gsquelart@mozilla.com"],
+ "bug_numbers": [1271483],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 8,
+ "description": "Counts of various Decoder Doctor notification events. Used to track efficacy of Decoder Doctor at helping users fix problems with their audio/video codecs. Keys are localized string names that identify problem with audio/video codecs that Decoder Doctor attempts to solve; see string values in dom.properties for verbose description of problems being solved. 0=recorded every time the Decoder Doctor notification is shown, 1=recorded the first time in a profile when notification is shown, 2=recorded when 'Learn how' button clicked, 3=recorded when 'Learn how' button first clicked in a profile, 4=recorded when issue solved after infobar has been shown at least once in a profile."
+ },
+ "VIDEO_DECODED_H264_SPS_CONSTRAINT_SET_FLAG": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 128,
+ "description": "A bit pattern to collect H.264 constraint set flag from the decoded SPS. Bits 0 through 5 represent constraint_set0_flag through constraint_set5_flag, respectively."
+ },
+ "VIDEO_DECODED_H264_SPS_LEVEL": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 51,
+ "description": "The H.264 level (level_idc) as extracted from the decoded SPS, from levels 1 (10) to 5.2 (51), with the addition of 0 for unknown values."
+ },
+ "VIDEO_DECODED_H264_SPS_PROFILE": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 244,
+ "description": "The H.264 profile number (profile_idc) as extracted from the decoded SPS."
+ },
+ "VIDEO_H264_SPS_MAX_NUM_REF_FRAMES": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 17,
+ "description": "SPS.max_num_ref_frames indicates how deep the H.264 queue is going to be, and as such the minimum memory usage by the decoder, from 0 to 16. 17 indicates an invalid value."
+ },
+ "WEBRTC_ICE_FINAL_CONNECTION_STATE": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 7,
+ "description": "The ICE connection state when the PC was closed"
+ },
+ "WEBRTC_ICE_ON_TIME_TRICKLE_ARRIVAL_TIME": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "The length of time (in milliseconds) that a trickle candidate took to arrive after the start of ICE, given that it arrived when ICE was not in a failure state (ie; a candidate that we could do something with, hence 'on time')"
+ },
+ "WEBRTC_ICE_LATE_TRICKLE_ARRIVAL_TIME": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "The length of time (in milliseconds) that a trickle candidate took to arrive after the start of ICE, given that it arrived after ICE failed."
+ },
+ "WEBRTC_ICE_SUCCESS_TIME": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "The length of time (in milliseconds) it took for ICE to complete, given that ICE succeeded."
+ },
+ "WEBRTC_ICE_FAILURE_TIME": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "The length of time (in milliseconds) it took for ICE to complete, given that it failed."
+ },
+ "WEBRTC_ICE_SUCCESS_RATE": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "description": "The number of failed ICE Connections (0) vs. number of successful ICE connections (1)."
+ },
+ "WEBRTC_STUN_RATE_LIMIT_EXCEEDED_BY_TYPE_GIVEN_SUCCESS": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "For each successful PeerConnection, bit 0 indicates the short-duration rate limit was reached, bit 1 indicates the long-duration rate limit was reached"
+ },
+ "WEBRTC_STUN_RATE_LIMIT_EXCEEDED_BY_TYPE_GIVEN_FAILURE": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "For each failed PeerConnection, bit 0 indicates the short-duration rate limit was reached, bit 1 indicates the long-duration rate limit was reached"
+ },
+ "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The delay (in milliseconds) when audio is behind video. Zero delay is counted. Measured every second of a call."
+ },
+ "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The delay (in milliseconds) when video is behind audio. Zero delay is not counted. Measured every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Locally measured data rate of inbound video (kbit/s). Computed every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Locally measured data rate on inbound audio (kbit/s). Computed every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Data rate deduced from RTCP from remote recipient of outbound video (kbit/s). Computed every second of a call (for easy comparison)."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Data rate deduced from RTCP from remote recipient of outbound audio (kbit/s). Computed every second of a call (for easy comparison)."
+ },
+ "WEBRTC_VIDEO_QUALITY_INBOUND_PACKETLOSS_RATE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Locally measured packet loss on inbound video (permille). Sampled every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_INBOUND_PACKETLOSS_RATE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Locally measured packet loss on inbound audio (permille). Sampled every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_PACKETLOSS_RATE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "RTCP-reported packet loss by remote recipient of outbound video (permille). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_PACKETLOSS_RATE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "RTCP-reported packet loss by remote recipient of outbound audio (permille). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_VIDEO_QUALITY_INBOUND_JITTER": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "Locally measured jitter on inbound video (ms). Sampled every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "Locally measured jitter on inbound audio (ms). Sampled every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "RTCP-reported jitter by remote recipient of outbound video (ms). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "RTCP-reported jitter by remote recipient of outbound audio (ms). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_VIDEO_ERROR_RECOVERY_MS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 500,
+ "description": "Time to recover from a video error in ms"
+ },
+ "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 200,
+ "description": "Number of losses recovered before error per min"
+ },
+ "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 200,
+ "description": "Number of losses recovered after error per min"
+ },
+ "WEBRTC_VIDEO_DECODE_ERROR_TIME_PERMILLE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Percentage*10 (permille) of call decoding with errors or frozen due to errors"
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "Roundtrip time of outbound video (ms). Sampled every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "Roundtrip time of outbound audio (ms). Sampled every second of a call."
+ },
+ "WEBRTC_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "Video encoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Standard deviation from video encoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_AVG_PER_CALL": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Video encoder's average framerate (in fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_10X_STD_DEV_PER_CALL": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Standard deviation from video encoder's average framerate (in 1/10 fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_DROPPED_FRAMES_PER_CALL_FPM": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Video encoder's number of frames dropped (in frames/min) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "Video decoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Standard deviation from video decoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Video decoder's average framerate (in fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Standard deviation from video decoder's average framerate (in 1/10 fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Video decoder's number of discarded packets (in packets/min) over an entire call"
+ },
+ "WEBRTC_CALL_DURATION": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "The length of time (in seconds) that a call lasted."
+ },
+ "WEBRTC_CALL_COUNT": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "48",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "The number of calls made during a session."
+ },
+ "WEBRTC_CALL_COUNT_2": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1261063],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "The number of calls made during a session."
+ },
+ "WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_SUCCESS": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The number of times AddIceCandidate failed on a given PeerConnection, given that ICE succeeded."
+ },
+ "WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_FAILURE": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The number of times AddIceCandidate failed on a given PeerConnection, given that ICE failed."
+ },
+ "WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 15,
+ "description": "Origins for getUserMedia calls (0=other, 1=HTTPS, 2=file, 3=app, 4=localhost, 5=loop, 6=privileged)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEBRTC_GET_USER_MEDIA_TYPE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Type for media in getUserMedia calls (0=Camera, 1=Screen, 2=Application, 3=Window, 4=Browser, 5=Microphone, 6=AudioCapture, 7=Other)"
+ },
+ "WEBRTC_LOAD_STATE_RELAXED": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Relaxed load state in calls over 30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_RELAXED_SHORT": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Relaxed load state in calls 5-30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_NORMAL": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Normal load state in calls over 30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_NORMAL_SHORT": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Normal load state in calls over 5-30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_STRESSED": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Stressed load state in calls over 30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_STRESSED_SHORT": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Stressed load state in calls 5-30 seconds."
+ },
+ "WEBRTC_RENEGOTIATIONS": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 21,
+ "n_buckets": 20,
+ "description": "Number of Renegotiations during each call"
+ },
+ "WEBRTC_MAX_VIDEO_SEND_TRACK": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10,
+ "n_buckets": 9,
+ "description": "Number of Video tracks sent simultaneously"
+ },
+ "WEBRTC_MAX_VIDEO_RECEIVE_TRACK": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 20,
+ "n_buckets": 19,
+ "description": "Number of Video tracks received simultaneously"
+ },
+ "WEBRTC_MAX_AUDIO_SEND_TRACK": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 20,
+ "n_buckets": 19,
+ "description": "Number of Audio tracks sent simultaneously"
+ },
+ "WEBRTC_MAX_AUDIO_RECEIVE_TRACK": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "Number of Audio tracks received simultaneously"
+ },
+ "WEBRTC_DATACHANNEL_NEGOTIATED": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Was DataChannels negotiated"
+ },
+ "WEBRTC_CALL_TYPE": {
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Type of call: (Bitmask) Audio = 1, Video = 2, DataChannels = 4"
+ },
+ "DEVTOOLS_TOOLBOX_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools toolbox has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_OPTIONS_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools options panel has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBCONSOLE_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Web Console has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Browser Console has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_INSPECTOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_RULEVIEW_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Rule View has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Computed View has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Font Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Animation Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_JSDEBUGGER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Debugger has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Browser Debugger has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_STYLEEDITOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Style Editor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_SHADEREDITOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Shader Editor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBAUDIOEDITOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Web Audio Editor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_CANVASDEBUGGER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Canvas Debugger has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_JSPROFILER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools JS Profiler has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_MEMORY_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Memory Tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_NETMONITOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Network Monitor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_STORAGE_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Storage Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_PAINTFLASHING_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Paint Flashing has been opened via the toolbox button.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_TILT_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Tilt has been opened via the toolbox button.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_SCRATCHPAD_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Scratchpad toolbox panel has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_SCRATCHPAD_WINDOW_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1214352, 1247985],
+ "description": "Number of times the DevTools Scratchpad standalone window has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_RESPONSIVE_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Responsive Design Mode tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_EYEDROPPER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Eyedropper tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Eyedropper has been opened via the DevTools menu.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Eyedropper has been opened via the color picker.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Developer Toolbar / GCLI has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org", "jan@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985, 1204601],
+ "description": "Number of times about:debugging has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBIDE_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools WebIDE has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools WebIDE project editor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times a file has been saved in the DevTools WebIDE project editor.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBIDE_NEW_PROJECT_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times a new project has been created in the DevTools WebIDE.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times a project has been imported into the DevTools WebIDE.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_CUSTOM_OPENED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times a custom developer tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_RELOAD_ADDON_INSTALLED_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Number of times the reload addon has been installed.",
+ "bug_numbers": [1248435]
+ },
+ "DEVTOOLS_RELOAD_ADDON_RELOAD_COUNT": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Number of times the tools have been reloaded by the reload addon.",
+ "bug_numbers": [1248435]
+ },
+ "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the toolbox been active (seconds)"
+ },
+ "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the options panel been active (seconds)"
+ },
+ "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the web console been active (seconds)"
+ },
+ "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the browser console been active (seconds)"
+ },
+ "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the inspector been active (seconds)"
+ },
+ "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the rule view been active (seconds)"
+ },
+ "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the computed view been active (seconds)"
+ },
+ "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the font inspector been active (seconds)"
+ },
+ "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the animation inspector been active (seconds)"
+ },
+ "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the JS debugger been active (seconds)"
+ },
+ "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the JS browser debugger been active (seconds)"
+ },
+ "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the style editor been active (seconds)"
+ },
+ "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the Shader Editor been active (seconds)"
+ },
+ "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the Web Audio Editor been active (seconds)"
+ },
+ "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the Canvas Debugger been active (seconds)"
+ },
+ "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the JS profiler been active (seconds)"
+ },
+ "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the Memory Tool been active (seconds)"
+ },
+ "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the network monitor been active (seconds)"
+ },
+ "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the storage inspector been active (seconds)"
+ },
+ "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has paint flashing been active (seconds)"
+ },
+ "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has Tilt been active (seconds)"
+ },
+ "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has Scratchpad been active (seconds)"
+ },
+ "DEVTOOLS_SCRATCHPAD_WINDOW_TIME_ACTIVE_SECONDS": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has Scratchpad standalone window been active (seconds)",
+ "bug_numbers": [1214352]
+ },
+ "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1242057],
+ "description": "How long has the responsive view been active (seconds)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has the developer toolbar been active (seconds)"
+ },
+ "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org", "jan@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has about:debugging been active? (seconds) (bug 1204601)"
+ },
+ "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has WebIDE been active (seconds)"
+ },
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has WebIDE's project editor been active (seconds)"
+ },
+ "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long has a custom developer tool been active (seconds)"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_USB_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE USB runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_WIFI_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE WiFi runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_SIMULATOR_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE simulator runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_REMOTE_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE remote runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_LOCAL_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE local runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_OTHER_CONNECTION_RESULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Did WebIDE other runtime connection succeed?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "description": "How long was WebIDE connected to a runtime (seconds)?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTION_PLAY_USED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Was WebIDE's play button used during this runtime connection?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTION_DEBUG_USED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Was WebIDE's debug button used during this runtime connection?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime type did WebIDE connect to?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime ID did WebIDE connect to?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime processor did WebIDE connect to?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime OS did WebIDE connect to?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime platform version did WebIDE connect to?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime app type did WebIDE connect to?"
+ },
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What runtime version did WebIDE connect to?"
+ },
+ "DEVTOOLS_OS_ENUMERATED_PER_USER": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 13,
+ "description": "OS of DevTools user (0:Windows XP, 1:Windows Vista, 2:Windows 7, 3:Windows 8, 4:Windows 8.1, 5:OSX, 6:Linux 7:Windows 10, 8:reserved, 9:reserved, 10:reserved, 11:reserved, 12:other)"
+ },
+ "DEVTOOLS_OS_IS_64_BITS_PER_USER": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "OS bit size of DevTools user (0:32bit, 1:64bit, 2:128bit)"
+ },
+ "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 13,
+ "description": "Screen resolution of DevTools user (0:lower, 1:800x600, 2:1024x768, 3:1280x800, 4:1280x1024, 5:1366x768, 6:1440x900, 7:1920x1080, 8:2560×1440, 9:2560×1600, 10:2880x1800, 11:other, 12:higher)"
+ },
+ "DEVTOOLS_TABS_OPEN_PEAK_LINEAR": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 101,
+ "n_buckets": 100,
+ "description": "The peak number of open tabs in all windows for a session for devtools users."
+ },
+ "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 101,
+ "n_buckets": 100,
+ "description": "The mean number of open tabs in all windows for a session for devtools users."
+ },
+ "DEVTOOLS_TABS_PINNED_PEAK_LINEAR": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 101,
+ "n_buckets": 100,
+ "description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users."
+ },
+ "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 101,
+ "n_buckets": 100,
+ "description": "The mean number of pinned tabs (app tabs) in all windows for a session for devtools users."
+ },
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to save a heap snapshot in mozilla::devtools::ChromeUtils::SaveHeapSnapshot."
+ },
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to read a heap snapshot in mozilla::devtools::ChromeUtils::ReadHeapSnapshot."
+ },
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000000,
+ "n_buckets": 10000,
+ "description": "The number of nodes serialized into a heap snapshot."
+ },
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000000,
+ "n_buckets": 10000,
+ "description": "The number of edges serialized into a heap snapshot."
+ },
+ "DEVTOOLS_PERFTOOLS_RECORDING_COUNT": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Incremented whenever a performance tool recording is completed."
+ },
+ "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Incremented whenever a performance tool recording is completed that was initiated via console.profile."
+ },
+ "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "When a user imports a recording in the performance tool."
+ },
+ "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "When a user imports a recording in the performance tool."
+ },
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "When a user starts a recording with specific recording options, keyed by feature name (withMarkers, withAllocations, etc.)."
+ },
+ "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 600000,
+ "n_buckets": 20,
+ "description": "The length of a duration in MS of a performance tool recording."
+ },
+ "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "keyed": true,
+ "high": 600000,
+ "n_buckets": 20,
+ "description": "The amount of time spent in a specific performance tool view, keyed by view name (waterfall, js-calltree, js-flamegraph, etc)."
+ },
+ "DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED": {
+ "alert_emails": ["mphillips@mozilla.com"],
+ "bug_numbers": [1255133],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "Measures whether a particular JavaScript error has been displayed in the webconsole."
+ },
+ "DEVTOOLS_TOOLBOX_HOST": {
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "58",
+ "kind": "enumerated",
+ "bug_numbers": [1205845],
+ "n_values": 9,
+ "releaseChannelCollection": "opt-out",
+ "description": "Records DevTools toolbox host each time the toolbox is opened and when the host is changed (0:Bottom, 1:Side, 2:Window, 3:Custom, 9:Unknown)."
+ },
+ "VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN": {
+ "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "description": "How many times has view source in browser / tab been opened?"
+ },
+ "VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN": {
+ "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "description": "How many times has view source in a new window been opened?"
+ },
+ "VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN": {
+ "alert_emails": ["mozilla-dev-developer-tools@lists.mozilla.org", "jryans@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "description": "How many times has view source in an external editor been opened, and did it succeed?"
+ },
+ "BROWSER_IS_USER_DEFAULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "The result of the startup default desktop browser check."
+ },
+ "BROWSER_IS_USER_DEFAULT_ERROR": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "True if the browser was unable to determine if the browser was set as default."
+ },
+ "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 250,
+ "n_buckets": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times that a profile has seen the 'Set Default Browser' dialog."
+ },
+ "BROWSER_SET_DEFAULT_ALWAYS_CHECK": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "True if the profile has `browser.shell.checkDefaultBrowser` set to true."
+ },
+ "BROWSER_SET_DEFAULT_RESULT": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "releaseChannelCollection": "opt-out",
+ "description": "Result of the Set Default Browser dialog (0=Use Firefox + 'Always perform check' unchecked, 1=Use Firefox + 'Always perform check' checked, 2=Not Now + 'Always perform check' unchecked, 3=Not Now + 'Always perform check' checked)"
+ },
+ "BROWSER_SET_DEFAULT_ERROR": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "True if the browser was unable to set Firefox as the default browser"
+ },
+ "BROWSER_SET_DEFAULT_TIME_TO_COMPLETION_SECONDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time to successfully set Firefox as the default browser after clicking 'Set Firefox as Default'. Should be near-instant in some environments, others require user interaction. Measured in seconds."
+ },
+ "BROWSER_IS_ASSIST_DEFAULT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "The result of the default browser check for assist intent."
+ },
+ "MIXED_CONTENT_PAGE_LOAD": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Accumulates type of content per page load (0=no mixed or non-secure page, 1=mixed passive, 2=mixed active, 3=mixed passive and mixed active)"
+ },
+ "MIXED_CONTENT_UNBLOCK_COUNTER": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "A simple counter of daily mixed-content unblock operations and top documents loaded"
+ },
+ "MIXED_CONTENT_HSTS": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS"
+ },
+ "MIXED_CONTENT_HSTS_PRIMING": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "bug_numbers": [1246540],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed, including how often would we send an HSTS priming request? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS, 4=display/no-HSTS-priming, 5=display/do-HSTS-priming, 6=active/no-HSTS-priming, 7=active/do-HSTS-priming"
+ },
+ "MIXED_CONTENT_HSTS_PRIMING_RESULT": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "bug_numbers": [1246540],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "How often do we get back an HSTS priming result which upgrades the connection to HTTPS? 0=cached (no upgrade), 1=cached (do upgrade), 2=cached (blocked), 3=already upgraded, 4=priming succeeded, 5=priming succeeded (block due to pref), 6=priming succeeded (no upgrade due to pref), 7=priming failed (block), 8=priming failed (accept)"
+ },
+ "HSTS_PRIMING_REQUEST_DURATION": {
+ "alert_emails": ["seceng-telemetry@mozilla.org"],
+ "bug_numbers": [1311893],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "low": 100,
+ "high": 30000,
+ "n_buckets": 100,
+ "keyed": true,
+ "description": "The amount of time required for HSTS priming requests (ms), keyed by success or failure of the priming request. (success, failure)"
+ },
+ "MIXED_CONTENT_OBJECT_SUBREQUEST": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "bug_numbers": [1244116],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "How often objects load insecure content on secure pages (counting pages, not objects). 0=pages with no mixed object subrequests, 1=pages with mixed object subrequests"
+ },
+ "COOKIE_SCHEME_SECURITY": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "How often are secure cookies set from non-secure origins, and vice-versa? 0=nonsecure/http, 1=nonsecure/https, 2=secure/http, 3=secure/https"
+ },
+ "COOKIE_LEAVE_SECURE_ALONE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [976073],
+ "expires_in_version": "57",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Measuring the effects of draft-ietf-httpbis-cookie-alone blocking. 0=blocked http setting secure cookie; 1=blocked http downgrading secure cookie; 2=blocked evicting secure cookie; 3=evicting newer insecure cookie; 4=evicting the oldest insecure cookie; 5=evicting the preferred cookie; 6=evicting the secure blocked"
+ },
+ "NTLM_MODULE_USED_2": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "The module used for the NTLM protocol (Windows_API, Kerberos, Samba_auth or Generic) and whether or not the authentication was used to connect to a proxy server. This data is collected only once per session (at first NTLM authentification) ; fixed version."
+ },
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 15,
+ "description": "BACKGROUND THUMBNAILS: Size of capture queue when a capture request is received"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "description": "BACKGROUND THUMBNAILS: Time the capture request spent in the queue before being serviced (ms)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "BACKGROUND THUMBNAILS: Time the capture took once it started and successfully completed (ms)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "BACKGROUND THUMBNAILS: Reason the capture completed (see TEL_CAPTURE_DONE_* constants in BackgroundPageThumbs.jsm)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 20,
+ "description": "BACKGROUND THUMBNAILS: Time the capture's page load took (ms)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "description": "BACKGROUND THUMBNAILS: Time it took to draw the capture's window to canvas (ms)"
+ },
+ "NETWORK_CACHE_V2_MISS_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to find out a cache entry file is missing"
+ },
+ "NETWORK_CACHE_V2_HIT_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to open an existing file"
+ },
+ "NETWORK_CACHE_V1_TRUNCATE_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to reopen an entry with OPEN_TRUNCATE"
+ },
+ "NETWORK_CACHE_V1_MISS_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to find out a cache entry is missing"
+ },
+ "NETWORK_CACHE_V1_HIT_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to open an existing cache entry"
+ },
+ "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 7,
+ "description": "Final status of the CacheFileOutputStream (0=ok, 1=other error, 2=out of memory, 3=disk full, 4=file corrupted, 5=file not found, 6=binding aborted)"
+ },
+ "NETWORK_CACHE_V2_INPUT_STREAM_STATUS": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 7,
+ "description": "Final status of the CacheFileInputStream (0=ok, 1=other error, 2=out of memory, 3=disk full, 4=file corrupted, 5=file not found, 6=binding aborted)"
+ },
+ "NETWORK_CACHE_FS_TYPE": {
+ "expires_in_version": "42",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Type of FS that the cache is stored on (0=NTFS (Win), 1=FAT32 (Win), 2=FAT (Win), 3=other FS (Win), 4=other OS)"
+ },
+ "NETWORK_CACHE_SIZE_FULL_FAT": {
+ "expires_in_version": "42",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Size (in MB) of a cache that reached a file count limit"
+ },
+ "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 40,
+ "description": "Hit/Miss count split by cache size in file count (0=Hit 0-5000, 1=Miss 0-5000, 2=Hit 5001-10000, ...)"
+ },
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 400,
+ "description": "Hit rate for a specific cache size in file count. The hit rate is split into 20 buckets, the lower limit of the range in percents is 5*n/20. The cache size is divided into 20 ranges of length 5000, the lower limit of the range is 5000*(n%20)"
+ },
+ "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to read the first part of the metadata from the cache entry file."
+ },
+ "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to read the missing part of the metadata from the cache entry file."
+ },
+ "NETWORK_CACHE_METADATA_FIRST_READ_SIZE": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 5119,
+ "n_buckets": 256,
+ "description": "Guessed size of the metadata that we read from the cache file as the first part."
+ },
+ "NETWORK_CACHE_METADATA_SIZE": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 5119,
+ "n_buckets": 256,
+ "description": "Actual size of the metadata parsed from the disk."
+ },
+ "NETWORK_CACHE_HASH_STATS": {
+ "expires_in_version": "46",
+ "kind": "enumerated",
+ "n_values": 160,
+ "description": "The longest hash match between a newly added entry and all the existing entries."
+ },
+ "DATABASE_LOCKED_EXCEPTION": {
+ "expires_in_version": "42",
+ "kind": "enumerated",
+ "description": "Record database locks when opening one of Fennec's databases. The index corresponds to how many attempts, beginning with 0.",
+ "n_values": 5
+ },
+ "DATABASE_SUCCESSFUL_UNLOCK": {
+ "expires_in_version": "42",
+ "kind": "enumerated",
+ "description": "Record on which attempt we successfully unlocked a database. See DATABASE_LOCKED_EXCEPTION.",
+ "n_values": 5
+ },
+ "SSL_TLS13_INTOLERANCE_REASON_PRE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1250568],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.3 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS13_INTOLERANCE_REASON_POST": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1250568],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.3 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS12_INTOLERANCE_REASON_PRE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.2 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS12_INTOLERANCE_REASON_POST": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.2 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS11_INTOLERANCE_REASON_PRE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.1 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS11_INTOLERANCE_REASON_POST": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.1 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS10_INTOLERANCE_REASON_PRE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.0 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS10_INTOLERANCE_REASON_POST": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.0 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_VERSION_FALLBACK_INAPPROPRIATE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "TLS/SSL version intolerance was falsely detected, server rejected handshake (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_WEAK_CIPHERS_FALLBACK": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Fallback attempted when server did not support any strong cipher suites"
+ },
+ "SSL_CIPHER_SUITE_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 128,
+ "description": "Negotiated cipher suite in full handshake (see key in HandshakeCallback in nsNSSCallbacks.cpp)"
+ },
+ "SSL_CIPHER_SUITE_RESUMED": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 128,
+ "description": "Negotiated cipher suite in resumed handshake (see key in HandshakeCallback in nsNSSCallbacks.cpp)"
+ },
+ "SSL_KEA_RSA_KEY_SIZE_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "RSA KEA (TLS_RSA_*) key size in full handshake"
+ },
+ "SSL_KEA_DHE_KEY_SIZE_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "DHE KEA (TLS_DHE_*) key size in full handshake"
+ },
+ "SSL_KEA_ECDHE_CURVE_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 36,
+ "description": "ECDHE KEA (TLS_ECDHE_*) curve (23=P-256, 24=P-384, 25=P-521) in full handshake"
+ },
+ "SSL_AUTH_ALGORITHM_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Authentication Algorithm (null=0, rsa=1, dsa=2, ecdsa=4) in full handshake"
+ },
+ "SSL_AUTH_RSA_KEY_SIZE_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "RSA signature key size for TLS_*_RSA_* in full handshake"
+ },
+ "SSL_AUTH_ECDSA_CURVE_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 36,
+ "description": "ECDSA signature curve for TLS_*_ECDSA_* in full handshake (23=P-256, 24=P-384, 25=P-521)"
+ },
+ "SSL_SYMMETRIC_CIPHER_FULL": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 32,
+ "description": "Symmetric cipher used in full handshake (null=0, rc4=1, 3des=4, aes-cbc=7, camellia=8, seed=9, aes-gcm=10)"
+ },
+ "SSL_SYMMETRIC_CIPHER_RESUMED": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 32,
+ "description": "Symmetric cipher used in resumed handshake (null=0, rc4=1, 3des=4, aes-cbc=7, camellia=8, seed=9, aes-gcm=10)"
+ },
+ "SSL_REASONS_FOR_NOT_FALSE_STARTING": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 512,
+ "description": "Bitmask of reasons we did not false start when libssl would have let us (see key in nsNSSCallbacks.cpp)"
+ },
+ "SSL_HANDSHAKE_TYPE": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Type of handshake (1=resumption, 2=false started, 3=chose not to false start, 4=not allowed to false start)"
+ },
+ "SSL_OCSP_STAPLING": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Status of OCSP stapling on this handshake (1=present, good; 2=none; 3=present, expired; 4=present, other error)"
+ },
+ "SSL_OCSP_MAY_FETCH": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "For non-stapling cases, is OCSP fetching a possibility? (0=yes, 1=no because missing/invalid OCSP URI, 2=no because fetching disabled, 3=no because both)"
+ },
+ "SSL_CERT_ERROR_OVERRIDES": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "Was a certificate error overridden on this handshake? What was it? (0=unknown error (indicating bug), 1=no, >1=a specific error)"
+ },
+ "SSL_CERT_VERIFICATION_ERRORS": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "If certificate verification failed in a TLS handshake, what was the error? (see MapCertErrorToProbeValue in security/manager/ssl/SSLServerCertVerification.cpp and the values in security/pkix/include/pkix/Result.h)"
+ },
+ "SSL_PERMANENT_CERT_ERROR_OVERRIDES": {
+ "alert_emails": ["seceng@mozilla.org"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 10,
+ "description": "How many permanent certificate overrides a user has stored."
+ },
+ "SSL_SCTS_ORIGIN": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1293231],
+ "releaseChannelCollection": "opt-out",
+ "description": "Origin of Signed Certificate Timestamps received (1=Embedded, 2=TLS handshake extension, 3=Stapled OCSP response)"
+ },
+ "SSL_SCTS_PER_CONNECTION": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1293231],
+ "releaseChannelCollection": "opt-out",
+ "description": "Histogram of Signed Certificate Timestamps per SSL connection, from all sources (embedded / OCSP Stapling / TLS handshake). Bucket 0 counts the cases when no SCTs were received, or none were extracted due to parsing errors."
+ },
+ "SSL_SCTS_VERIFICATION_STATUS": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1293231],
+ "releaseChannelCollection": "opt-out",
+ "description": "Verification status of Signed Certificate Timestamps received (0=Decoding error, 1=SCT verified, 2=SCT from unknown log, 3=Invalid SCT signature, 4=SCT timestamp is in the future)"
+ },
+ "SSL_SERVER_AUTH_EKU": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Presence of of the Server Authenticaton EKU in accepted SSL server certificates (0=No EKU, 1=EKU present and has id_kp_serverAuth, 2=EKU present and has id_kp_serverAuth as well as some other EKU, 3=EKU present but does not contain id_kp_serverAuth)"
+ },
+ "TELEMETRY_TEST_EXPIRED": {
+ "expires_in_version": "4.0a1",
+ "kind": "flag",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TLS_ERROR_REPORT_UI": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "description": "User interaction with the TLS Error Reporter in about:neterror (0=Error seen, 1='auto' checked, 2='auto' unchecked, 3=Sent manually, 4=Sent automatically, 5=Send success, 6=Send failure, 7=Report section expanded)"
+ },
+ "CERT_OCSP_ENABLED": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Is OCSP fetching enabled? (pref security.OCSP.enabled)"
+ },
+ "CERT_OCSP_REQUIRED": {
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Is OCSP required when the cert has an OCSP URI? (pref security.OCSP.require)"
+ },
+ "OSFILE_WORKER_LAUNCH_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "description": "The duration between the instant the first message is sent to OS.File and the moment the OS.File worker starts executing JavaScript, in milliseconds",
+ "high": 5000,
+ "n_buckets": 10
+ },
+ "OSFILE_WORKER_READY_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "description": "The duration between the instant the first message is sent to OS.File and the moment the OS.File worker has finished executing its startup JavaScript and is ready to receive requests, in milliseconds",
+ "high": 5000,
+ "n_buckets": 10
+ },
+ "OSFILE_WRITEATOMIC_JANK_MS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "description": "The duration during which the main thread is blocked during a call to OS.File.writeAtomic, in milliseconds",
+ "high": 5000,
+ "n_buckets": 10
+ },
+ "CERT_EV_STATUS": {
+ "expires_in_version": "never",
+ "alert_emails": ["seceng@mozilla.org"],
+ "bug_numbers": [1254653],
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "EV status of a certificate, recorded on each TLS connection. 0=invalid, 1=DV, 2=EV"
+ },
+ "CERT_VALIDATION_SUCCESS_BY_CA": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 256,
+ "description": "Successful SSL server cert validations by CA (see RootHashes.inc for names of CAs)"
+ },
+ "CERT_PINNING_FAILURES_BY_CA": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 256,
+ "description": "Pinning failures by CA (see RootHashes.inc for names of CAs)"
+ },
+ "CERT_PINNING_RESULTS": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Certificate pinning results (0 = failure, 1 = success)"
+ },
+ "CERT_PINNING_TEST_RESULTS": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Certificate pinning test results (0 = failure, 1 = success)"
+ },
+ "CERT_PINNING_MOZ_RESULTS": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Certificate pinning results for Mozilla sites (0 = failure, 1 = success)"
+ },
+ "CERT_PINNING_MOZ_TEST_RESULTS": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Certificate pinning test results for Mozilla sites (0 = failure, 1 = success)"
+ },
+ "CERT_PINNING_MOZ_RESULTS_BY_HOST": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 512,
+ "description": "Certificate pinning results by host for Mozilla operational sites"
+ },
+ "CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST": {
+ "alert_emails": ["pinning@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 512,
+ "description": "Certificate pinning test results by host for Mozilla operational sites"
+ },
+ "CERT_CHAIN_KEY_SIZE_STATUS": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Does enforcing a larger minimum RSA key size cause verification failures? 1 = no, 2 = yes, 3 = another error prevented finding a verified chain"
+ },
+ "CERT_CHAIN_SHA1_POLICY_STATUS": {
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "1 = No SHA1 signatures, 2 = SHA1 certificates issued by an imported root, 3 = SHA1 certificates issued before 2016, 4 = SHA1 certificates issued after 2015, 5 = another error prevented successful verification"
+ },
+ "WEAVE_CONFIGURED": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "If any version of Firefox Sync is configured for this device",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEAVE_CONFIGURED_MASTER_PASSWORD": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If both Firefox Sync and Master Password are configured for this device"
+ },
+ "WEAVE_START_COUNT": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "The number of times a sync started in this session"
+ },
+ "WEAVE_COMPLETE_SUCCESS_COUNT": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "The number of times a sync successfully completed in this session"
+ },
+ "WEAVE_WIPE_SERVER_SUCCEEDED": {
+ "expires_in_version": "55",
+ "alert_emails": ["fx-team@mozilla.com"],
+ "kind": "boolean",
+ "bug_numbers": [1241699],
+ "description": "Stores 1 if a wipeServer call succeeded, and 0 if it failed."
+ },
+ "WEBCRYPTO_EXTRACTABLE_IMPORT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether an imported key was marked as extractable"
+ },
+ "WEBCRYPTO_EXTRACTABLE_GENERATE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a generated key was marked as extractable"
+ },
+ "WEBCRYPTO_EXTRACTABLE_ENC": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a key used in an encrypt/decrypt operation was marked as extractable"
+ },
+ "WEBCRYPTO_EXTRACTABLE_SIG": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a key used in a sign/verify operation was marked as extractable"
+ },
+ "WEBCRYPTO_RESOLVED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a promise created by WebCrypto was resolved (vs rejected)"
+ },
+ "WEBCRYPTO_METHOD": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Methods invoked under window.crypto.subtle (0=encrypt, 1=decrypt, 2=sign, 3=verify, 4=digest, 5=generateKey, 6=deriveKey, 7=deriveBits, 8=importKey, 9=exportKey, 10=wrapKey, 11=unwrapKey)"
+ },
+ "WEBCRYPTO_ALG": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "description": "Algorithms used with WebCrypto (see table in WebCryptoTask.cpp)"
+ },
+ "MASTER_PASSWORD_ENABLED": {
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "If a master-password is enabled for this profile"
+ },
+ "DISPLAY_SCALING_OSX" : {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 100,
+ "description": "Scaling percentage for the display where the first window is opened (OS X only)",
+ "cpp_guard": "XP_MACOSX"
+ },
+ "DISPLAY_SCALING_MSWIN" : {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 100,
+ "description": "Scaling percentage for the display where the first window is opened (MS Windows only)",
+ "cpp_guard": "XP_WIN"
+ },
+ "DISPLAY_SCALING_LINUX" : {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 100,
+ "description": "Scaling percentage for the display where the first window is opened (Linux only)",
+ "cpp_guard": "XP_LINUX"
+ },
+ "SOCIAL_SIDEBAR_STATE": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Social Sidebar state 0: closed, 1: opened. Toggling between providers will result in a higher opened rate."
+ },
+ "SOCIAL_TOOLBAR_BUTTONS": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Social toolbar button has been used (0:share, 1:status, 2:bookmark)"
+ },
+ "SOCIAL_PANEL_CLICKS": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Social content has been interacted with (0:share, 1:status, 2:bookmark, 3: sidebar)"
+ },
+ "SOCIAL_SIDEBAR_OPEN_DURATION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 10,
+ "description": "Sidebar showing: seconds that the sidebar has been opened"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "description": "Duration of shutdown phase quit-application, as measured by the shutdown terminator, in seconds of activity"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "description": "Duration of shutdown phase profile-change-teardown, as measured by the shutdown terminator, in seconds of activity"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "description": "Duration of shutdown phase xpcom-will-shutdown, as measured by the shutdown terminator, in seconds of activity"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "description": "Duration of shutdown phase profile-before-change, as measured by the shutdown terminator, in seconds of activity"
+ },
+ "BR_9_2_1_SUBJECT_ALT_NAMES": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Baseline Requirements section 9.2.1: subject alternative names extension (0: ok, 1 or more: error)"
+ },
+ "BR_9_2_2_SUBJECT_COMMON_NAME": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Baseline Requirements section 9.2.2: subject common name field (0: present, in subject alt. names; 1: not present; 2: not present in subject alt. names)"
+ },
+ "TAP_TO_LOAD_ENABLED": {
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Whether or not a user has tap-to-load enabled.",
+ "bug_numbers": [1208167]
+ },
+ "ZOOMED_VIEW_ENABLED": {
+ "expires_in_version": "60",
+ "kind": "boolean",
+ "description": "Whether or not a user has the zoomed view (a.k.a. \"Magnify small areas\") enabled.",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "bug_numbers": [1235061]
+ },
+ "TRACKING_PROTECTION_ENABLED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether or not a session has tracking protection enabled"
+ },
+ "TRACKING_PROTECTION_PBM_DISABLED": {
+ "expires_in_version": "60",
+ "kind": "boolean",
+ "description": "Is the tracking protection in private browsing mode disabled?"
+ },
+ "FENNEC_TRACKING_PROTECTION_STATE": {
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The state of the user-visible tracking protection setting (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)",
+ "alert_emails": ["mleibovic@mozilla.com"],
+ "bug_numbers": [1228090]
+ },
+ "TRACKING_PROTECTION_SHIELD": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Tracking protection shield (0 = not shown, 1 = loaded, 2 = blocked)"
+ },
+ "TRACKING_PROTECTION_EVENTS": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Doorhanger shown = 0, Disable = 1, Enable = 2"
+ },
+ "SERVICE_WORKER_REGISTRATION_LOADING": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Tracking how ServiceWorkerRegistrar loads data before the first content is shown. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_REQUEST_PASSTHROUGH": {
+ "expires_in_version": "50",
+ "kind": "boolean",
+ "description": "Intercepted fetch sending back same Request object. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "E10S_STATUS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 12,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1241294],
+ "description": "Why e10s is enabled or disabled (0=ENABLED_BY_USER, 1=ENABLED_BY_DEFAULT, 2=DISABLED_BY_USER, 3=DISABLED_IN_SAFE_MODE, 4=DISABLED_FOR_ACCESSIBILITY, 5=DISABLED_FOR_MAC_GFX, 6=DISABLED_FOR_BIDI, 7=DISABLED_FOR_ADDONS, 8=FORCE_DISABLED, 9=DISABLED_FOR_XPLAYERS, 10=DISABLED_FOR_OS_VERSION)"
+ },
+ "E10S_WINDOW": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a browser window is set as an e10s window"
+ },
+ "E10S_BLOCKED_FROM_RUNNING": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether the e10s pref was set but it was blocked from running due to blacklisted conditions"
+ },
+ "BLOCKED_ON_PLUGIN_MODULE_INIT_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Time (ms) that the main thread has been blocked on LoadModule and NP_Initialize in PluginModuleParent"
+ },
+ "BLOCKED_ON_PLUGIN_INSTANCE_INIT_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Time (ms) that the main thread has been blocked on NPP_New in an IPC plugin"
+ },
+ "BLOCKED_ON_PLUGIN_STREAM_INIT_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Time (ms) that the main thread has been blocked on NPP_NewStream in an IPC plugin"
+ },
+ "BLOCKED_ON_PLUGINASYNCSURROGATE_WAITFORINIT_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Time (ms) that the main thread has been blocked on PluginAsyncSurrogate::WaitForInit in an IPC plugin"
+ },
+ "BLOCKED_ON_PLUGIN_INSTANCE_DESTROY_MS": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Time (ms) that the main thread has been blocked on NPP_Destroy in an IPC plugin"
+ },
+ "ONBEFOREUNLOAD_PROMPT_ACTION" : {
+ "expires_in_version": "45",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "What button a user clicked in an onbeforeunload prompt. (Stay on Page = 0, Leave Page = 1, prompt aborted = 2)"
+ },
+ "ONBEFOREUNLOAD_PROMPT_COUNT" : {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "How many onbeforeunload prompts has the user encountered in their session?"
+ },
+ "SUBPROCESS_ABNORMAL_ABORT": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts of plugin/content process abnormal shutdown, whether or not a crash report was available."
+ },
+ "SUBPROCESS_CRASHES_WITH_DUMP": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts of plugin and content process crashes which are reported with a crash dump."
+ },
+ "SUBPROCESS_LAUNCH_FAILURE": {
+ "alert_emails": ["haftandilian@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1275430],
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times launching a subprocess fails. Counts are by subprocess-type using the GeckoProcessType enum."
+ },
+ "PROCESS_CRASH_SUBMIT_ATTEMPT": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "An attempt to submit a crash. Keyed on the CrashManager Crash.type."
+ },
+ "PROCESS_CRASH_SUBMIT_SUCCESS": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "The submission status when main/plugin/content crashes are submitted. 1 is success, 0 is failure. Keyed on the CrashManager Crash.type."
+ },
+ "STUMBLER_TIME_BETWEEN_UPLOADS_SEC": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 259200,
+ "description": "Stumbler: The time in seconds between uploads."
+ },
+ "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 1000000,
+ "description": "Stumbler: Volume measurement of bytes uploaded, normalized to per-second."
+ },
+ "STUMBLER_TIME_BETWEEN_START_SEC": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 259200,
+ "description": "Stumbler: The time between the service starts."
+ },
+ "STUMBLER_UPLOAD_BYTES": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 1000000,
+ "description": "Stumbler: The bytes per upload."
+ },
+ "STUMBLER_UPLOAD_OBSERVATION_COUNT": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 10000,
+ "description": "Stumbler: The observations per upload."
+ },
+ "STUMBLER_UPLOAD_CELL_COUNT": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 10000,
+ "description": "Stumbler: The cells per upload."
+ },
+ "STUMBLER_UPLOAD_WIFI_AP_COUNT": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 10000,
+ "description": "Stumbler: The Wi-Fi APs per upload."
+ },
+ "STUMBLER_OBSERVATIONS_PER_DAY": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 10000,
+ "description": "Stumbler: The number of observations between upload events, normalized to per day."
+ },
+ "STUMBLER_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "n_buckets": 50,
+ "high": 86400,
+ "description": "Stumbler: The time between receiving passive locations."
+ },
+ "DATA_STORAGE_ENTRIES": {
+ "expires_in_version": "default",
+ "kind": "linear",
+ "high": 1024,
+ "n_buckets": 16,
+ "description": "The number of entries in persistent DataStorage (HSTS and HPKP data, basically)"
+ },
+ "VIDEO_EME_PLAY_SUCCESS": {
+ "expires_in_version": "45",
+ "kind": "boolean",
+ "description": "EME video playback success or failure"
+ },
+ "VIDEO_PLAY_TIME_MS" : {
+ "alert_emails": ["ajones@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Total time spent playing video in milliseconds. This reports the total play time for an HTML Media Element whenever it is suspended or resumed, such as when the page is unloaded, or when the mute status changes when the AudioChannelAPI pref is set.",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1261955, 1127646]
+ },
+ "VIDEO_HIDDEN_PLAY_TIME_MS" : {
+ "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Total time spent playing video while element is hidden, in milliseconds. This reports the total hidden play time for an HTML Media Element whenever it is suspended or resumed, such as when the page is unloaded, or when the mute status changes when the AudioChannelAPI pref is set.",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1285419]
+ },
+ "VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE" : {
+ "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Percentage of total time spent playing video while element is hidden. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1287987]
+ },
+ "VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE" : {
+ "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Percentage of total time spent *not* fully decoding video while element is hidden (simulated, even when feature is not enabled). Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1293145]
+ },
+ "VIDEO_INTER_KEYFRAME_AVERAGE_MS" : {
+ "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Average interval between video keyframes in played videos, in milliseconds. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "bug_numbers": [1289668]
+ },
+ "VIDEO_INTER_KEYFRAME_MAX_MS" : {
+ "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Maximum interval between video keyframes in played videos, in milliseconds; '0' means only 1 keyframe found. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "bug_numbers": [1289668]
+ },
+ "VIDEO_SUSPEND_RECOVERY_TIME_MS" : {
+ "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+ "expires_in_version": "55",
+ "description": "Time taken for a video to resume after decoding was suspended, in milliseconds. Keyed by audio presence, hw acceleration, and by height ranges (boundaries: 240. 480, 720, 1080, 2160), e.g.: 'V,0-240', 'AV(hw),2160+'; and 'All' will accumulate all percentages.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1294349]
+ },
+ "VIDEO_AS_CONTENT_SOURCE" : {
+ "alert_emails": ["ajones@mozilla.com", "kaku@mozilla.com"],
+ "expires_in_version": "58",
+ "description": "Usage of a {visible / invisible} video element as the source of {drawImage(), createPattern(), createImageBitmap() and captureStream()} APIs. (0 = ALL_VISIBLE, 1 = ALL_INVISIBLE, 2 = drawImage_VISIBLE, 3 = drawImage_INVISIBLE, 4 = createPattern_VISIBLE, 5 = createPattern_INVISIBLE, 6 = createImageBitmap_VISIBLE, 7 = createImageBitmap_INVISIBLE, 8 = captureStream_VISIBLE, 9 = captureStream_INVISIBLE)",
+ "kind": "enumerated",
+ "n_values": 12,
+ "bug_numbers": [1299718]
+ },
+ "VIDEO_AS_CONTENT_SOURCE_IN_TREE_OR_NOT" : {
+ "alert_emails": ["ajones@mozilla.com", "kaku@mozilla.com"],
+ "expires_in_version": "58",
+ "description": "Usage of an invisible {in tree / not in tree} video element as the source of {drawImage(), createPattern(), createImageBitmap() and captureStream()} APIs. (0 = ALL_IN_TREE, 1 = ALL_NOT_IN_TREE, 2 = drawImage_IN_TREE, 3 = drawImage_NOT_IN_TREE, 4 = createPattern_IN_TREE, 5 = createPattern_NOT_IN_TREE, 6 = createImageBitmap_IN_TREE, 7 = createImageBitmap_NOT_IN_TREE, 8 = captureStream_IN_TREE, 9 = captureStream_NOT_IN_TREE)",
+ "kind": "enumerated",
+ "n_values": 12,
+ "bug_numbers": [1337301]
+ },
+ "VIDEO_UNLOAD_STATE": {
+ "alert_emails": ["ajones@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "HTML Media Element state when unloading. ended = 0, paused = 1, stalled = 2, seeking = 3, other = 4",
+ "bug_numbers": [1261955, 1261955]
+ },
+ "VIDEO_VP9_BENCHMARK_FPS": {
+ "alert_emails": ["ajones@mozilla.com"],
+ "expires_in_version": "55",
+ "bug_numbers": [1230265],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "720p VP9 decode benchmark measurement in frames per second",
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_CDM_CREATED": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "58",
+ "bug_numbers": [1304207],
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "Note the type of CDM (0=ClearKey, 1=Primetime, 2=Widevine, 3=unknown) every time we successfully instantiate an EME MediaKeys object.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_CDM_GENERATE_REQUEST_CALLED": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "58",
+ "bug_numbers": [1305552],
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "Note the type of CDM (0=ClearKey, 1=Primetime, 2=Widevine, 3=unknown) every time we call MediaKeySession.generateRequest().",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEDIA_CODEC_USED": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "never",
+ "keyed": true,
+ "kind": "count",
+ "description": "Count of use of audio/video codecs in HTMLMediaElements and WebAudio. Those with 'resource' prefix are approximate; report based on HTTP ContentType or sniffing. Those with 'webaudio' prefix are for WebAudio."
+ },
+ "FX_SANITIZE_TOTAL": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Total time it takes to sanitize (ms)"
+ },
+ "FX_SANITIZE_CACHE": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize the cache (ms)"
+ },
+ "FX_SANITIZE_COOKIES_2": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize firefox cookies (ms). A subset of FX_SANITIZE_COOKIES."
+ },
+ "FX_SANITIZE_LOADED_FLASH": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1251469],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize Flash when it's already loaded (ms). A subset of FX_SANITIZE_PLUGINS."
+ },
+ "FX_SANITIZE_UNLOADED_FLASH": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1251469],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize Flash when it's not yet loaded (ms). A subset of FX_SANITIZE_PLUGINS."
+ },
+ "FX_SANITIZE_HISTORY": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize history (ms)"
+ },
+ "FX_SANITIZE_FORMDATA": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize stored form data (ms)"
+ },
+ "FX_SANITIZE_DOWNLOADS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize recent downloads (ms)"
+ },
+ "FX_SANITIZE_SESSIONS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize saved sessions (ms)"
+ },
+ "FX_SANITIZE_SITESETTINGS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize site-specific settings (ms)"
+ },
+ "FX_SANITIZE_OPENWINDOWS": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize the open windows list (ms)"
+ },
+ "PWMGR_BLOCKLIST_NUM_SITES": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets" : 10,
+ "description": "The number of sites for which the user has explicitly rejected saving logins"
+ },
+ "PWMGR_FORM_AUTOFILL_RESULT": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values" : 20,
+ "description": "The result of auto-filling a login form. See http://mzl.la/1Mbs6jL for bucket descriptions."
+ },
+ "PWMGR_LOGIN_LAST_USED_DAYS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 750,
+ "n_buckets" : 40,
+ "description": "Time in days each saved login was last used"
+ },
+ "PWMGR_LOGIN_PAGE_SAFETY": {
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "The safety of a page where we see a password field. (0: safe page & safe submit; 1: safe page & unsafe submit; 2: safe page & unknown submit; 3: unsafe page & safe submit; 4: unsafe page & unsafe submit; 5: unsafe page & unknown submit)"
+ },
+ "PWMGR_MANAGE_COPIED_PASSWORD": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count of passwords copied from the password management interface"
+ },
+ "PWMGR_MANAGE_COPIED_USERNAME": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count of usernames copied from the password management interface"
+ },
+ "PWMGR_MANAGE_DELETED": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count of passwords deleted from the password management interface (including via Remove All)"
+ },
+ "PWMGR_MANAGE_DELETED_ALL": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count of times that Remove All was used from the password management interface"
+ },
+ "PWMGR_MANAGE_OPENED": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values" : 5,
+ "description": "Accumulates how the password management interface was opened. (0=Preferences, 1=Page Info)"
+ },
+ "PWMGR_MANAGE_SORTED": {
+ "expires_in_version": "never",
+ "keyed": true,
+ "kind": "count",
+ "description": "Reports the column that logins are sorted by"
+ },
+ "PWMGR_MANAGE_VISIBILITY_TOGGLED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether the visibility of passwords was toggled (0=Hide, 1=Show)"
+ },
+ "PWMGR_NUM_PASSWORDS_PER_HOSTNAME": {
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 21,
+ "n_buckets" : 20,
+ "description": "The number of passwords per hostname"
+ },
+ "PWMGR_NUM_SAVED_PASSWORDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 750,
+ "n_buckets" : 50,
+ "description": "Total number of saved logins, including those that cannot be decrypted"
+ },
+ "PWMGR_NUM_HTTPAUTH_PASSWORDS": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 750,
+ "n_buckets" : 50,
+ "description": "Number of HTTP Auth logins"
+ },
+ "PWMGR_PASSWORD_INPUT_IN_FORM": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether an <input type=password> is associated with a <form> when it is added to a document"
+ },
+ "PWMGR_PROMPT_REMEMBER_ACTION" : {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Action taken by user through prompt for creating a login. (0=Prompt displayed [always recorded], 1=Add login, 2=Don't save now, 3=Never save)"
+ },
+ "PWMGR_PROMPT_UPDATE_ACTION" : {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Action taken by user through prompt for modifying a login. (0=Prompt displayed [always recorded], 1=Update login)"
+ },
+ "PWMGR_SAVING_ENABLED": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Number of users who have password saving on globally"
+ },
+ "PWMGR_USERNAME_PRESENT": {
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a saved login has a username"
+ },
+ "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "The number of Sync 1.1 -> Sync 1.5 migration sentinels seen by Android Sync."
+ },
+ "FENNEC_SYNC11_MIGRATIONS_FAILED": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "The number of Sync 1.1 -> Sync 1.5 migrations that failed during Android Sync."
+ },
+ "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "The number of Sync 1.1 -> Sync 1.5 migrations that succeeded during Android Sync."
+ },
+ "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED": {
+ "expires_in_version": "45",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 5,
+ "description": "The number of Sync 1.5 'complete upgrade/migration' notifications offered by Android Sync."
+ },
+ "FENNEC_SYNC11_MIGRATIONS_COMPLETED": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "The number of Sync 1.5 migrations completed by Android Sync."
+ },
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "Counts the number of times that a sync has started."
+ },
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "Counts the number of times that a sync has completed with no errors."
+ },
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "Counts the number of times that a sync has failed with errors."
+ },
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF": {
+ "expires_in_version": "45",
+ "kind": "count",
+ "description": "Counts the number of times that a sync has failed because of trying to sync before server backoff interval has passed."
+ },
+ "SLOW_SCRIPT_NOTICE_COUNT": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count slow script notices"
+ },
+ "SLOW_SCRIPT_PAGE_COUNT": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1251667],
+ "description": "The number of pages that trigger slow script notices"
+ },
+ "SLOW_SCRIPT_NOTIFY_DELAY": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "bug_numbers": [1271978],
+ "description": "The difference between the js slow script timeout for content set in prefs and the actual time we waited before displaying the notification (msec)."
+ },
+ "PLUGIN_HANG_NOTICE_COUNT": {
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count plugin hang notices in e10s"
+ },
+ "SERVICE_WORKER_SPAWN_ATTEMPTS": {
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count attempts to spawn a ServiceWorker for a domain. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_WAS_SPAWNED": {
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count ServiceWorkers that really did get a thread created for them. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_SPAWN_GETS_QUEUED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "bug_numbers": [1286895],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Tracking whether a ServiceWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SHARED_WORKER_SPAWN_GETS_QUEUED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "bug_numbers": [1286895],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Tracking whether a SharedWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "DEDICATED_WORKER_SPAWN_GETS_QUEUED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "bug_numbers": [1286895],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Tracking whether a DedicatedWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_REGISTRATIONS": {
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count how many registrations occurs. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS": {
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count whenever a document is controlled. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_UPDATED": {
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count ServiceWorkers scripts that are updated. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_LIFE_TIME": {
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "description": "Tracking how long a ServiceWorker stays alive after it is spawned. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "GRAPHICS_SANITY_TEST": {
+ "expires_in_version": "never",
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","msreckovic@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports results from the graphics sanity test to track which drivers are having problems (0=TEST_PASSED, 1=TEST_FAILED_RENDER, 2=TEST_FAILED_VIDEO, 3=TEST_CRASHED)"
+ },
+ "READER_MODE_PARSE_RESULT" : {
+ "expires_in_version": "54",
+ "alert_emails": ["firefox-dev@mozilla.org", "gijs@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The result of trying to parse a document to show in reader view (0=Success, 1=Error too many elements, 2=Error in worker, 3=Error no article)"
+ },
+ "READER_MODE_DOWNLOAD_RESULT" : {
+ "expires_in_version": "54",
+ "alert_emails": ["firefox-dev@mozilla.org", "gijs@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The result of trying to download a document to show in reader view (0=Success, 1=Error XHR, 2=Error no document)"
+ },
+ "FENNEC_LOAD_SAVED_PAGE": {
+ "expires_in_version": "60",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "How often users load saved items when online/offline (0=RL online, 1=RL offline, 2=BM online, 3=BM offline)",
+ "bug_numbers": [1243387]
+ },
+ "PERMISSIONS_SQL_CORRUPTED": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Record the permissions.sqlite init failure"
+ },
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Record the removal of defective permissions.sqlite"
+ },
+ "FENNEC_TABQUEUE_QUEUESIZE" : {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 50,
+ "n_buckets": 10,
+ "description": "The number of tabs queued when opened."
+ },
+ "FENNEC_CUSTOM_HOMEPAGE": {
+ "expires_in_version": "60",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "bug_numbers": [1239102],
+ "kind": "boolean",
+ "description": "Whether the user has set a custom homepage."
+ },
+ "GRAPHICS_DRIVER_STARTUP_TEST": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports whether or not graphics drivers crashed during startup."
+ },
+ "GRAPHICS_SANITY_TEST_OS_SNAPSHOT": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports whether the graphics sanity test passed an OS snapshot test. 0=Pass, 1=Fail, 2=Error, 3=Timed out."
+ },
+ "DEVTOOLS_HUD_JANK": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "exponential",
+ "keyed": true,
+ "description": "The duration which a thread is blocked in ms, keyed by appName.",
+ "high": 5000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_REFLOW_DURATION": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "exponential",
+ "keyed": true,
+ "description": "The duration a reflow takes in ms, keyed by appName.",
+ "high": 1000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_REFLOWS": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "count",
+ "keyed": true,
+ "description": "A count of the number of reflows, keyed by appName."
+ },
+ "DEVTOOLS_HUD_SECURITY_CATEGORY": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "enumerated",
+ "keyed": true,
+ "description": "The security error enums, keyed by appName.",
+ "n_values": 8
+ },
+ "DEVTOOLS_HUD_ERRORS": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "count",
+ "keyed": true,
+ "description": "Number of errors, keyed by appName."
+ },
+ "DEVTOOLS_HUD_WARNINGS": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "count",
+ "keyed": true,
+ "description": "Number of warnings, keyed by appName."
+ },
+ "DEVTOOLS_HUD_USS": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "low": 20000000,
+ "high": 100000000,
+ "n_buckets": 52,
+ "description": "The USS memory consumed by an application, keyed by appName."
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_CONTENTINTERACTIVE": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'contentInteractive' performance mark, keyed by appName.",
+ "high": 2000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_NAVIGATIONINTERACTIVE": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'navigationInteractive' performance mark, keyed by appName.",
+ "high": 3000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_NAVIGATIONLOADED": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'navigationLoaded' performance mark, keyed by appName.",
+ "high": 4000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_VISUALLYLOADED": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'visuallyLoaded' performance mark, keyed by appName.",
+ "high": 5000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_MEDIAENUMERATED": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'mediaEnumerated' performance mark, keyed by appName.",
+ "high": 5000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_FULLYLOADED": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'fullyLoaded' performance mark, keyed by appName.",
+ "high": 30000,
+ "n_buckets": 30
+ },
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_SCANEND": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The duration in ms between application launch and the 'scanEnd' performance mark, keyed by appName.",
+ "high": 30000,
+ "n_buckets": 30
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_CONTENTINTERACTIVE_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'contentInteractive' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 30000000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_NAVIGATIONINTERACTIVE_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'navigationInteractive' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 30000000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_NAVIGATIONLOADED_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'navigationLoaded' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 30000000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_VISUALLYLOADED_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'visuallyLoaded' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 30000000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_MEDIAENUMERATED_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'mediaEnumerated' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 40000000,
+ "n_buckets": 10
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_FULLYLOADED_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'fullyLoaded' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 40000000,
+ "n_buckets": 20
+ },
+ "DEVTOOLS_HUD_APP_MEMORY_SCANEND_V2": {
+ "alert_emails": ["rnicoletti@mozilla.com","thills@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "linear",
+ "keyed": true,
+ "description": "The USS memory consumed by an application at the time of the 'scanEnd' performance mark, keyed by appName.",
+ "low": 20000000,
+ "high": 40000000,
+ "n_buckets": 20
+ },
+ "DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT": {
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1221619],
+ "description": "The number of heap snapshots taken by a user"
+ },
+ "DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT": {
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1221619],
+ "description": "The number of heap snapshots imported by a user"
+ },
+ "DEVTOOLS_MEMORY_EXPORT_SNAPSHOT_COUNT": {
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1221619],
+ "description": "The number of heap snapshots exported by a user"
+ },
+ "DEVTOOLS_MEMORY_FILTER_CENSUS": {
+ "expires_in_version": "56",
+ "kind": "boolean",
+ "bug_numbers": [1221619],
+ "description": "Whether a census tree was filtered or not"
+ },
+ "DEVTOOLS_MEMORY_DIFF_CENSUS": {
+ "expires_in_version": "56",
+ "kind": "boolean",
+ "bug_numbers": [1221619],
+ "description": "Whether a census was the result of diffing or not"
+ },
+ "DEVTOOLS_MEMORY_INVERTED_CENSUS": {
+ "expires_in_version": "56",
+ "kind": "boolean",
+ "bug_numbers": [1221619],
+ "description": "Whether a census tree was inverted or not"
+ },
+ "DEVTOOLS_MEMORY_BREAKDOWN_CENSUS_COUNT": {
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1221619],
+ "keyed": true,
+ "description": "The number of times a given type of breakdown was used for a census"
+ },
+ "DEVTOOLS_MEMORY_DOMINATOR_TREE_COUNT": {
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1221619],
+ "description": "The number of times a user requested a dominator tree be computed"
+ },
+ "DEVTOOLS_MEMORY_BREAKDOWN_DOMINATOR_TREE_COUNT": {
+ "expires_in_version": "56",
+ "kind": "count",
+ "bug_numbers": [1221619],
+ "keyed": true,
+ "description": "The number of times a given type of breakdown was used for a dominator tree"
+ },
+ "GRAPHICS_SANITY_TEST_REASON": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "43",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports why a graphics sanity test was run. 0=First Run, 1=App Updated, 2=Device Change, 3=Driver Change."
+ },
+ "TRANSLATION_OPPORTUNITIES": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "A number of successful and failed attempts to translate a document"
+ },
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "A number of successful and failed attempts to translate a document grouped by language"
+ },
+ "TRANSLATED_PAGES": {
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of sucessfully translated pages"
+ },
+ "TRANSLATED_PAGES_BY_LANGUAGE": {
+ "expires_in_version": "default",
+ "kind": "count",
+ "keyed": true,
+ "description": "A number of sucessfully translated pages by language"
+ },
+ "TRANSLATED_CHARACTERS": {
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10240,
+ "n_buckets": 50,
+ "description": "A number of sucessfully translated characters"
+ },
+ "DENIED_TRANSLATION_OFFERS": {
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of tranlation offers the user denied"
+ },
+ "AUTO_REJECTED_TRANSLATION_OFFERS": {
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of auto-rejected tranlation offers"
+ },
+ "REQUESTS_OF_ORIGINAL_CONTENT": {
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of times the user requested to see the original content of a translated page"
+ },
+ "CHANGES_OF_TARGET_LANGUAGE": {
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of times when the target language was changed by the user"
+ },
+ "CHANGES_OF_DETECTED_LANGUAGE": {
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "A number of changes of detected language before (true) or after (false) translating a page for the first time."
+ },
+ "SHOULD_TRANSLATION_UI_APPEAR": {
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "Tracks situations when the user opts for displaying translation UI"
+ },
+ "SHOULD_AUTO_DETECT_LANGUAGE": {
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "Tracks situations when the user opts for auto-detecting the language of a page"
+ },
+ "PERMISSIONS_REMIGRATION_COMPARISON": {
+ "alert_emails": ["michael@thelayzells.com"],
+ "expires_in_version": "44",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Reports a comparison between row count of original and re-migration of the v7 permissions DB. 0=New == 0, 1=New < Old, 2=New == Old, 3=New > Old"
+ },
+ "PERMISSIONS_MIGRATION_7_ERROR": {
+ "alert_emails": ["michael@thelayzells.com"],
+ "expires_in_version": "44",
+ "kind": "boolean",
+ "description": "Was there an error while performing the v7 permissions DB migration?"
+ },
+ "PERF_MONITORING_TEST_CPU_RESCHEDULING_PROPORTION_MOVED": {
+ "alert_emails": ["dteller@mozilla.com"],
+ "expires_in_version": "48",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 20,
+ "description": "Proportion (%) of reschedulings of the main process to another CPU during the execution of code inside a JS compartment. Updated while we are measuring jank."
+ },
+ "PERF_MONITORING_SLOW_ADDON_JANK_US": {
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 10000000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Contiguous time spent by an add-on blocking the main loop (microseconds, keyed by add-on ID)."
+ },
+ "PERF_MONITORING_SLOW_ADDON_CPOW_US": {
+ "expires_in_version": "70",
+ "kind": "exponential",
+ "low": 1,
+ "high": 10000000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Contiguous time spent by an add-on blocking the main loop by performing a blocking cross-process call (microseconds, keyed by add-on ID)."
+ },
+ "VIDEO_EME_REQUEST_SUCCESS_LATENCY_MS": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 60,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time spent waiting for a navigator.requestMediaKeySystemAccess call to succeed."
+ },
+ "VIDEO_EME_REQUEST_FAILURE_LATENCY_MS": {
+ "alert_emails": ["cpearce@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 60,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time spent waiting for a navigator.requestMediaKeySystemAccess call to fail."
+ },
+ "FXA_CONFIGURED": {
+ "alert_emails": ["fx-team@mozilla.com"],
+ "bug_numbers": [1236383],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "releaseChannelCollection": "opt-out",
+ "description": "If the user is signed in to a Firefox Account on this device. Recorded once per session just after startup as Sync is initialized."
+ },
+ "WEAVE_DEVICE_COUNT_DESKTOP": {
+ "alert_emails": ["fx-team@mozilla.com"],
+ "bug_numbers": [1232050],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of desktop devices (including this device) associated with this Sync account. Recorded each time Sync successfully completes the 'clients' engine."
+ },
+ "WEAVE_DEVICE_COUNT_MOBILE": {
+ "alert_emails": ["fx-team@mozilla.com"],
+ "bug_numbers": [1232050],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of mobile devices associated with this Sync account. Recorded each time Sync successfully completes the 'clients' engine."
+ },
+ "WEAVE_ENGINE_SYNC_ERRORS": {
+ "alert_emails": ["fx-team@mozilla.com"],
+ "bug_numbers": [1236383],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Exceptions thrown by a Sync engine. Keyed on the engine name."
+ },
+ "CONTENT_DOCUMENTS_DESTROYED": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of content documents destroyed; used in conjunction with use counter histograms"
+ },
+ "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED": {
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of top-level content documents destroyed; used in conjunction with use counter histograms"
+ },
+ "PUSH_API_USED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "flag",
+ "description": "A Push API subscribe() operation was performed at least once this session."
+ },
+ "PUSH_API_PERMISSION_REQUESTED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Count of number of times the PermissionManager explicitly prompted user for Push Notifications permission"
+ },
+ "PUSH_API_PERMISSION_DENIED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "User explicitly denied Push Notifications permission"
+ },
+ "PUSH_API_PERMISSION_GRANTED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "User explicitly granted Push Notifications permission"
+ },
+ "PUSH_API_SUBSCRIBE_ATTEMPT": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Push Service attempts to subscribe with Push Server."
+ },
+ "PUSH_API_SUBSCRIBE_FAILED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Attempt to subscribe with Push Server failed."
+ },
+ "PUSH_API_SUBSCRIBE_SUCCEEDED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Attempt to subscribe with Push Server succeeded."
+ },
+ "PUSH_API_UNSUBSCRIBE_ATTEMPT": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Push Service attempts to unsubscribe with Push Server."
+ },
+ "PUSH_API_UNSUBSCRIBE_FAILED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Attempt to unsubscribe with Push Server failed."
+ },
+ "PUSH_API_UNSUBSCRIBE_SUCCEEDED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Attempt to unsubscribe with Push Server succeeded."
+ },
+ "PUSH_API_SUBSCRIBE_WS_TIME": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 15000,
+ "n_buckets": 10,
+ "description": "Time taken to subscribe over WebSocket (ms)."
+ },
+ "PUSH_API_SUBSCRIBE_HTTP2_TIME": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 15000,
+ "n_buckets": 10,
+ "description": "Time taken to subscribe over HTTP2 (ms)."
+ },
+ "PUSH_API_QUOTA_EXPIRATION_TIME": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 31622400,
+ "n_buckets": 20,
+ "description": "Time taken for a push subscription to expire its quota (seconds). The maximum is just over an year."
+ },
+ "PUSH_API_QUOTA_RESET_TO": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "The value a push record quota (a count) is reset to based on the user's browsing history."
+ },
+ "PUSH_API_NOTIFICATION_RECEIVED": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Push notification was received from server."
+ },
+ "PUSH_API_NOTIFICATION_RECEIVED_BUT_DID_NOT_NOTIFY": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "Push notification was received from server, but not delivered to ServiceWorker. Enumeration values are defined in dom/push/PushService.jsm as kDROP_NOTIFICATION_REASON_*."
+ },
+ "PUSH_API_NOTIFY": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Number of push messages that were successfully decrypted and delivered to a ServiceWorker."
+ },
+ "PUSH_API_NOTIFY_REGISTRATION_LOST": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Attempt to notify ServiceWorker of push notification resubscription."
+ },
+ "D3D11_SYNC_HANDLE_FAILURE": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bschouten@mozilla.com","danderson@mozilla.com","msreckovic@mozilla.com","ashughes@mozilla.com"],
+ "expires_in_version": "60",
+ "releaseChannelCollection": "opt-out",
+ "kind": "count",
+ "description": "Number of times the D3D11 compositor failed to get a texture sync handle."
+ },
+ "GFX_CONTENT_FAILED_TO_ACQUIRE_DEVICE": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "Failed to create a gfx content device. 0=content d3d11, 1=image d3d11, 2=d2d1."
+ },
+ "GFX_CRASH": {
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Graphics Crash Reason (...)"
+ },
+ "PLUGIN_ACTIVATION_COUNT": {
+ "alert_emails": ["cpeterson@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [722110,1260065],
+ "description": "Counts number of times a certain plugin has been activated."
+ },
+ "SCROLL_INPUT_METHODS": {
+ "alert_emails": ["botond@mozilla.com"],
+ "bug_numbers": [1238137],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Count of scroll actions triggered by different input methods. See gfx/layers/apz/util/ScrollInputMethods.h for a list of possible values and their meanings."
+ },
+ "WEB_NOTIFICATION_CLICKED": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1225336],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Count of times a web notification was clicked"
+ },
+ "WEB_NOTIFICATION_MENU": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1225336],
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Count of times a contextual menu item was used from a Notification (0: DND, 1: Disable, 2: Settings)"
+ },
+ "WEB_NOTIFICATION_SHOWN": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1225336],
+ "expires_in_version": "55",
+ "kind": "count",
+ "description": "Count of times a Notification was rendered (accounting for XUL DND). A system backend may put the notification directly into the tray if its own DND is on."
+ },
+ "WEBFONT_DOWNLOAD_TIME": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time to download a webfont (ms)"
+ },
+ "WEBFONT_DOWNLOAD_TIME_AFTER_START": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time after navigationStart webfont download completed (ms)"
+ },
+ "WEBFONT_FONTTYPE": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Font format type (woff/woff2/ttf/...)"
+ },
+ "WEBFONT_SRCTYPE": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Font src type loaded (1 = local, 2 = url, 3 = data)"
+ },
+ "WEBFONT_PER_PAGE": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of fonts loaded at page load"
+ },
+ "WEBFONT_SIZE_PER_PAGE": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "Size of all fonts loaded at page load (kb)"
+ },
+ "WEBFONT_SIZE": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "Size of font loaded (kb)"
+ },
+ "WEBFONT_COMPRESSION_WOFF": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Compression ratio of WOFF data (%)"
+ },
+ "WEBFONT_COMPRESSION_WOFF2": {
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Compression ratio of WOFF2 data (%)"
+ },
+ "WEBRTC_ICE_CHECKING_RATE": {
+ "alert_emails": ["webrtc-ice-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "boolean",
+ "bug_numbers": [1188391],
+ "description": "The number of ICE connections which immediately failed (0) vs. reached at least checking state (1)."
+ },
+ "ALERTS_SERVICE_DND_ENABLED": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "50",
+ "kind": "boolean",
+ "description": "XUL-only: whether the user has toggled do not disturb."
+ },
+ "ALERTS_SERVICE_DND_SUPPORTED_FLAG": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "50",
+ "kind": "flag",
+ "description": "Whether the do not disturb option is supported. True if the browser uses XUL alerts."
+ },
+ "WEB_NOTIFICATION_EXCEPTIONS_OPENED": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Number of times the Notification Permissions dialog has been opened."
+ },
+ "WEB_NOTIFICATION_PERMISSIONS": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Number of origins with the web notifications permission (0 = denied, 1 = allowed)."
+ },
+ "WEB_NOTIFICATION_PERMISSION_REMOVED": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Number of removed web notifications permissions (0 = remove deny, 1 = remove allow)."
+ },
+ "WEB_NOTIFICATION_SENDERS": {
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Number of origins that have shown a web notification. Excludes system alerts like update reminders and add-ons."
+ },
+ "YOUTUBE_REWRITABLE_EMBED_SEEN": {
+ "alert_emails": ["cpeterson@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "flag",
+ "bug_numbers": [1229971],
+ "releaseChannelCollection": "opt-out",
+ "description": "Flag activated whenever a rewritable youtube flash embed is seen during a session."
+ },
+ "YOUTUBE_NONREWRITABLE_EMBED_SEEN": {
+ "alert_emails": ["cpeterson@mozilla.com"],
+ "expires_in_version": "53",
+ "kind": "flag",
+ "bug_numbers": [1237401],
+ "releaseChannelCollection": "opt-out",
+ "description": "Flag activated whenever a non-rewritable (enablejsapi=1) youtube flash embed is seen during a session."
+ },
+ "PLUGIN_DRAWING_MODEL": {
+ "alert_emails": ["danderson@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "bug_numbers": [1229961],
+ "n_values": 12,
+ "description": "Plugin drawing model. 0 when windowed, otherwise NPDrawingModel + 1."
+ },
+ "WEB_NOTIFICATION_REQUEST_PERMISSION_CALLBACK": {
+ "alert_emails": ["push@mozilla.com"],
+ "expires_in_version": "55",
+ "bug_numbers": [1241278],
+ "kind": "boolean",
+ "description": "Usage of the deprecated Notification.requestPermission() callback argument"
+ },
+ "VIDEO_FASTSEEK_USED": {
+ "alert_emails": ["lchristie@mozilla.com", "cpearce@mozilla.com"],
+ "expires_in_version": "55",
+ "bug_numbers": [1245982],
+ "kind": "count",
+ "description": "Uses of HTMLMediaElement.fastSeek"
+ },
+ "VIDEO_DROPPED_FRAMES_PROPORTION" : {
+ "alert_emails": ["lchristie@mozilla.com", "cpearce@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1238433],
+ "description": "Percentage of frames decoded frames dropped in an HTMLVideoElement"
+ },
+ "TAB_SWITCH_CACHE_POSITION": {
+ "expires_in_version": "55",
+ "bug_numbers": [1242013],
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "Position in (theoretical) tab cache of tab being switched to"
+ },
+ "REMOTE_JAR_PROTOCOL_USED": {
+ "alert_emails": ["dev-platform@lists.mozilla.org"],
+ "expires_in_version": "55",
+ "bug_numbers": [1255934],
+ "kind": "count",
+ "description": "Remote JAR protocol usage"
+ },
+ "MEDIA_DECODER_BACKEND_USED": {
+ "alert_emails": ["danderson@mozilla.com"],
+ "bug_numbers": [1259695],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Media decoder backend (0=WMF Software, 1=DXVA2D3D9, 2=DXVA2D3D11)"
+ },
+ "PLUGIN_BLOCKED_FOR_STABILITY": {
+ "alert_emails": ["cpeterson@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "count",
+ "bug_numbers": [1237198],
+ "description": "Count plugins blocked for stability"
+ },
+ "PLUGIN_TINY_CONTENT": {
+ "alert_emails": ["cpeterson@mozilla.com"],
+ "expires_in_version": "52",
+ "kind": "count",
+ "bug_numbers": [1237198],
+ "description": "Count tiny plugin content"
+ },
+ "IPC_MESSAGE_SIZE": {
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1260908],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 8000000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "Measures the size of IPC messages by message name"
+ },
+ "IPC_REPLY_SIZE": {
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1264820],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 8000000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "Measures the size of IPC messages by message name"
+ },
+ "MESSAGE_MANAGER_MESSAGE_SIZE2": {
+ "alert_emails": ["wmccloskey@mozilla.com","amccreight@mozilla.com"],
+ "bug_numbers": [1260908],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "low": 8192,
+ "high": 8000000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "Each key is the message name, with digits removed, from an async message manager message that was larger than 8192 bytes, recorded in the sending process at the time of sending."
+ },
+ "REJECTED_MESSAGE_MANAGER_MESSAGE": {
+ "alert_emails": ["amccreight@mozilla.com"],
+ "bug_numbers": [1272423],
+ "expires_in_version": "55",
+ "kind": "count",
+ "keyed": true,
+ "description": "Each key is the message name, with digits removed, from an async message manager message that was rejected for being over approximately 128MB, recorded in the sending process at the time of sending."
+ },
+ "SANDBOX_BROKER_INITIALIZED": {
+ "alert_emails": ["bowen@mozilla.com"],
+ "bug_numbers": [1256992],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "description": "Result of call to SandboxBroker::Initialize"
+ },
+ "SANDBOX_HAS_SECCOMP_BPF": {
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "cpp_guard": "XP_LINUX",
+ "description": "Whether the system has seccomp-bpf capability"
+ },
+ "SANDBOX_HAS_SECCOMP_TSYNC": {
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "cpp_guard": "XP_LINUX",
+ "description": "Whether the system has seccomp-bpf thread-sync capability"
+ },
+ "SANDBOX_HAS_USER_NAMESPACES": {
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "cpp_guard": "XP_LINUX",
+ "description": "Whether our process succedeed in creating a user namespace"
+ },
+ "SANDBOX_HAS_USER_NAMESPACES_PRIVILEGED": {
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "cpp_guard": "XP_LINUX",
+ "description": "Whether the system has the capability to create privileged user namespaces"
+ },
+ "SANDBOX_MEDIA_ENABLED": {
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "cpp_guard": "XP_LINUX",
+ "description": "Whether the sandbox is enabled for media/GMP plugins"
+ },
+ "SANDBOX_CONTENT_ENABLED": {
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "cpp_guard": "XP_LINUX",
+ "description": "Whether the sandbox is enabled for the content process"
+ },
+ "SYNC_WORKER_OPERATION": {
+ "alert_emails": ["amarchesini@mozilla.com", "khuey@mozilla.com" ],
+ "bug_numbers": [1267904],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Tracking how long a Worker thread is blocked when a sync operation is executed on the main-thread."
+ },
+ "SUBPROCESS_KILL_HARD": {
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times a subprocess was forcibly killed, and the reason."
+ },
+ "FX_CONTENT_CRASH_DUMP_UNAVAILABLE": {
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times that about:tabcrashed was unable to find a crash dump."
+ },
+ "FX_CONTENT_CRASH_PRESENTED": {
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times that about:tabcrashed appeared and found a crash dump."
+ },
+ "FX_CONTENT_CRASH_NOT_SUBMITTED": {
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times that about:tabcrashed was unloaded without submitting."
+ },
+ "ABOUTCRASHES_OPENED_COUNT": {
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com","bgirard@mozilla.com","msreckovic@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "bug_numbers": [1276714, 1276716],
+ "description": "Number of times about:crashes has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "D3D9_COMPOSITING_FAILURE_ID": {
+ "alert_emails": ["bgirard@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "D3D9 compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1002846]
+ },
+ "D3D11_COMPOSITING_FAILURE_ID": {
+ "alert_emails": ["bgirard@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "D3D11 compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1002846]
+ },
+ "OPENGL_COMPOSITING_FAILURE_ID": {
+ "alert_emails": ["bgirard@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "OpenGL compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1002846]
+ },
+ "XHR_IN_WORKER": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "bug_numbers": [1280229],
+ "description": "Number of the use of XHR in workers."
+ },
+ "WEBVTT_TRACK_KINDS": {
+ "alert_emails": ["alwu@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1280644],
+ "description": "Number of the use of the subtitles kind track. 0=Subtitles, 1=Captions, 2=Descriptions, 3=Chapters, 4=Metadata, 5=Undefined Error",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEBVTT_USED_VTT_CUES": {
+ "alert_emails": ["alwu@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "count",
+ "bug_numbers": [1280644],
+ "description": "Number of the use of the vtt cues.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "BLINK_FILESYSTEM_USED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1272501],
+ "releaseChannelCollection": "opt-out",
+ "description": "Webkit/Blink filesystem used"
+ },
+ "WEBKIT_DIRECTORY_USED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1272501],
+ "releaseChannelCollection": "opt-out",
+ "description": "HTMLInputElement.webkitdirectory attribute used"
+ },
+ "CONTAINER_USED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "bug_numbers": [1276006],
+ "n_values": 5,
+ "description": "Records a value each time a builtin container is opened. 1=personal 2=work 3=banking 4=shopping. Does not record usage of user-created containers."
+ },
+ "UNIQUE_CONTAINERS_OPENED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1276006],
+ "kind": "count",
+ "description": "Tracking the unique number of opened Containers."
+ },
+ "TOTAL_CONTAINERS_OPENED": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1276006],
+ "kind": "count",
+ "description": "Tracking the total number of opened Containers."
+ },
+ "FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE": {
+ "alert_emails": ["jh+bugzilla@buttercookie.de"],
+ "expires_in_version": "56",
+ "kind": "flag",
+ "bug_numbers": [1284017],
+ "description": "When restoring tabs on startup, reading from sessionstore.js failed, even though the file exists and is not containing an explicitly empty window.",
+ "cpp_guard": "ANDROID"
+ },
+ "SHARED_WORKER_COUNT": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "count",
+ "bug_numbers": [1295980],
+ "description": "Number of the use of SharedWorkers."
+ },
+ "FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP": {
+ "alert_emails": ["jh+bugzilla@buttercookie.de"],
+ "expires_in_version": "56",
+ "kind": "flag",
+ "bug_numbers": [1190627],
+ "description": "When restoring tabs on startup, reading from sessionstore.js failed, but sessionstore.bak was read successfully.",
+ "cpp_guard": "ANDROID"
+ },
+ "NUMBER_OF_PROFILES": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "58",
+ "bug_numbers": [1296606],
+ "kind": "count",
+ "description": "Number of named browser profiles for the current user, as reported by the profile service at startup."
+ },
+ "WEB_PERMISSION_CLEARED": {
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1286118],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 6,
+ "description": "Number of revoke actions on permissions in the control center, keyed by permission id. Values represent the permission type that was revoked. (0=unknown, 1=permanently allowed, 2=permanently blocked, 3=temporarily allowed, 4=temporarily blocked)"
+ },
+ "JS_AOT_USAGE": {
+ "alert_emails": ["luke@mozilla.com", "bbouvier@mozilla.com"],
+ "bug_numbers": [1288778],
+ "expires_in_version": "56",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Counts the number of asm.js vs WebAssembly modules instanciations, at the time modules are getting instanciated."
+ },
+ "DOCUMENT_WITH_EXPANDED_PRINCIPAL": {
+ "alert_emails": ["dev-platform@lists.mozilla.org"],
+ "bug_numbers": [1301123],
+ "expires_in_version": "58",
+ "kind": "count",
+ "description": "Number of documents encountered using an expanded principal."
+ },
+ "CONTENT_PAINT_TIME": {
+ "alert_emails": ["danderson@mozilla.com"],
+ "bug_numbers": [1309442],
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time spent in the paint pipeline for content."
+ },
+ "CONTENT_LARGE_PAINT_PHASE_WEIGHT": {
+ "alert_emails": ["danderson@mozilla.com"],
+ "bug_numbers": [1309442],
+ "expires_in_version": "56",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "Percentage of time taken by phases in expensive content paints."
+ },
+ "NARRATE_CONTENT_BY_LANGUAGE_2": {
+ "alert_emails": ["eisaacson@mozilla.com"],
+ "bug_numbers": [1308030, 1324868],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "56",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 4,
+ "description": "Number of Narrate initialization attempts and successes broken up by content's language (ISO 639-1 code) (0 = initialization attempt, 1 = successfully initialized)"
+ },
+ "NARRATE_CONTENT_SPEAKTIME_MS": {
+ "alert_emails": ["eisaacson@mozilla.com"],
+ "bug_numbers": [1308030],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "56",
+ "kind": "linear",
+ "high": 300000,
+ "n_buckets": 30,
+ "description": "Time in MS that content is narrated in 10 second increments up to 5 minutes"
+ },
+ "HANDLE_UNLOAD_MS": {
+ "alert_emails": ["kchen@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1301346],
+ "description": "The time spent handling unload event in milliseconds. It measures all documents and subframes separately. If there are multiple handlers for the unload event in a document, this will record a single value across all handlers in the document."
+ },
+ "HANDLE_BEFOREUNLOAD_MS": {
+ "alert_emails": ["kchen@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1301346],
+ "description": "The time spent handling beforeunload event in milliseconds. It measures all documents and subframes separately. If there are multiple handlers for the unload event in a document, this will record a single value across all handlers in the document."
+ },
+ "TABCHILD_PAINT_TIME": {
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1313686],
+ "expires_in_version": "56",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time spent painting the contents of a remote browser (ms).",
+ "releaseChannelCollection": "opt-out"
+ },
+ "TIME_TO_NON_BLANK_PAINT_MS": {
+ "alert_emails": ["hkirschner@mozilla.com"],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1307242],
+ "description": "The time between navigation start and the first non-blank paint of a foreground root content document, in milliseconds. This only records documents that were in an active docshell throughout the whole time between navigation start and non-blank paint. The non-blank paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "MOZ_BLOB_IN_XHR": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "bug_numbers": [1335365],
+ "releaseChannelCollection": "opt-out",
+ "description": "XMLHttpRequest.responseType set to moz-blob"
+ },
+ "MOZ_CHUNKED_TEXT_IN_XHR": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "bug_numbers": [1335365],
+ "releaseChannelCollection": "opt-out",
+ "description": "XMLHttpRequest.responseType set to moz-chunked-text"
+ },
+ "MOZ_CHUNKED_ARRAYBUFFER_IN_XHR": {
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "boolean",
+ "bug_numbers": [1335365],
+ "releaseChannelCollection": "opt-out",
+ "description": "XMLHttpRequest.responseType set to moz-chunked-arraybuffer"
+ }
+}
diff --git a/toolkit/components/telemetry/Makefile.in b/toolkit/components/telemetry/Makefile.in
new file mode 100644
index 0000000000..52016707cc
--- /dev/null
+++ b/toolkit/components/telemetry/Makefile.in
@@ -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/.
+
+include $(topsrcdir)/config/rules.mk
+
+# This is so hacky. Waiting on bug 988938.
+addondir = $(srcdir)/tests/addons
+testdir = $(topobjdir)/_tests/xpcshell/toolkit/components/telemetry/tests/unit
+
+misc:: $(call mkdir_deps,$(testdir))
+ $(EXIT_ON_ERROR) \
+ for dir in $(addondir)/*; do \
+ base=`basename $$dir`; \
+ (cd $$dir && zip -qr $(testdir)/$$base.xpi *); \
+ done
diff --git a/toolkit/components/telemetry/ProcessedStack.h b/toolkit/components/telemetry/ProcessedStack.h
new file mode 100644
index 0000000000..2bda550072
--- /dev/null
+++ b/toolkit/components/telemetry/ProcessedStack.h
@@ -0,0 +1,63 @@
+/* -*- 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 ProcessedStack_h__
+#define ProcessedStack_h__
+
+#include <string>
+#include <vector>
+
+namespace mozilla {
+namespace Telemetry {
+
+// This class represents a stack trace and the modules referenced in that trace.
+// It is designed to be easy to read and write to disk or network and doesn't
+// include any logic on how to collect or read the information it stores.
+class ProcessedStack
+{
+public:
+ ProcessedStack();
+ size_t GetStackSize() const;
+ size_t GetNumModules() const;
+
+ struct Frame
+ {
+ // The offset of this program counter in its module or an absolute pc.
+ uintptr_t mOffset;
+ // The index to pass to GetModule to get the module this program counter
+ // was in.
+ uint16_t mModIndex;
+ };
+ struct Module
+ {
+ // The file name, /foo/bar/libxul.so for example.
+ std::string mName;
+ std::string mBreakpadId;
+
+ bool operator==(const Module& other) const;
+ };
+
+ const Frame &GetFrame(unsigned aIndex) const;
+ void AddFrame(const Frame& aFrame);
+ const Module &GetModule(unsigned aIndex) const;
+ void AddModule(const Module& aFrame);
+
+ void Clear();
+
+private:
+ std::vector<Module> mModules;
+ std::vector<Frame> mStack;
+};
+
+// Get the current list of loaded modules, filter and pair it to the provided
+// stack. We let the caller collect the stack since different callers have
+// different needs (current thread X main thread, stopping the thread, etc).
+ProcessedStack
+GetStackAndModules(const std::vector<uintptr_t> &aPCs);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // ProcessedStack_h__
diff --git a/toolkit/components/telemetry/ScalarInfo.h b/toolkit/components/telemetry/ScalarInfo.h
new file mode 100644
index 0000000000..6c9d8aade7
--- /dev/null
+++ b/toolkit/components/telemetry/ScalarInfo.h
@@ -0,0 +1,27 @@
+/* -*- 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 TelemetryScalarInfo_h__
+#define TelemetryScalarInfo_h__
+
+// This module is internal to Telemetry. It defines a structure that holds the
+// scalar info. It should only be used by TelemetryScalarData.h automatically
+// generated file and TelemetryScalar.cpp. This should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+struct ScalarInfo {
+ uint32_t kind;
+ uint32_t name_offset;
+ uint32_t expiration_offset;
+ uint32_t dataset;
+ bool keyed;
+
+ const char *name() const;
+ const char *expiration() const;
+};
+} // namespace
+
+#endif // TelemetryScalarInfo_h__
diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml
new file mode 100644
index 0000000000..e958198794
--- /dev/null
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -0,0 +1,298 @@
+# This file contains a definition of the scalar probes that are recorded in Telemetry.
+# They are submitted with the "main" pings and can be inspected in about:telemetry.
+
+# The following section contains the aushelper system add-on scalars.
+aushelper:
+ websense_reg_version:
+ bug_numbers:
+ - 1305847
+ description: The Websense version from the Windows registry.
+ expires: "60"
+ kind: string
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+
+# The following section contains the browser engagement scalars.
+browser.engagement:
+ max_concurrent_tab_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of maximum number of tabs open during a subsession,
+ across all windows, including tabs in private windows and restored
+ at startup.
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+
+ tab_open_event_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of tab open events per subsession, across all windows, after the
+ session has been restored. This includes tab open events from private windows
+ and from manual session restorations (i.e. after crashes and from about:home).
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+
+ max_concurrent_window_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of maximum number of browser windows open during a subsession. This
+ includes private windows and the ones opened when starting the browser.
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+
+ window_open_event_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of browser window open events per subsession, after the session
+ has been restored. The count includes private windows and the ones from manual
+ session restorations (i.e. after crashes and from about:home).
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+
+ total_uri_count:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count of the total non-unique http(s) URIs visited in a subsession, including
+ page reloads, after the session has been restored. This does not include background
+ page requests and URIs from embedded pages or private browsing.
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+
+ unfiltered_uri_count:
+ bug_numbers:
+ - 1304647
+ description: >
+ The count of the total non-unique URIs visited in a subsession, not restricted to
+ a specific protocol, including page reloads and about:* pages (other than initial
+ pages such as about:blank, ...), after the session has been restored. This does
+ not include background page requests and URIs from embedded pages or private browsing.
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+
+ unique_domains_count:
+ bug_numbers:
+ - 1271310
+ description: >
+ The count of the unique domains visited in a subsession, after the session
+ has been restored. Subdomains under eTLD are aggregated after the first level
+ (i.e. test.example.com and other.example.com are only counted once).
+ This does not include background page requests and domains from embedded pages
+ or private browsing. The count is limited to 100 unique domains.
+ expires: "55"
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+
+# The following section contains the browser engagement scalars.
+browser.engagement.navigation:
+ urlbar:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from the urlbar (awesomebar),
+ broken down by the originating action.
+ expires: "55"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+
+ searchbar:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from the searchbar,
+ broken down by the originating action.
+ expires: "55"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+
+ about_home:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from about:home,
+ broken down by the originating action.
+ expires: "55"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+
+ about_newtab:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from about:newtab,
+ broken down by the originating action.
+ expires: "55"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+
+ contextmenu:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from the contextmenu,
+ broken down by the originating action.
+ expires: "55"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+
+# The following section is for probes testing the Telemetry system. They will not be
+# submitted in pings and are only used for testing.
+telemetry.test:
+ unsigned_int_kind:
+ bug_numbers:
+ - 1276190
+ description: >
+ This is a test uint type with a really long description, maybe spanning even multiple
+ lines, to just prove a point: everything works just fine.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ string_kind:
+ bug_numbers:
+ - 1276190
+ description: A string test type with a one line comment that works just fine!
+ expires: never
+ kind: string
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ boolean_kind:
+ bug_numbers:
+ - 1281214
+ description: A boolean test type with a one line comment that works just fine!
+ expires: never
+ kind: boolean
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ expired:
+ bug_numbers:
+ - 1276190
+ description: This is an expired testing scalar; not meant to be touched.
+ expires: 4.0a1
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ unexpired:
+ bug_numbers:
+ - 1276190
+ description: This is an unexpired testing scalar; not meant to be touched.
+ expires: "375.0"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ release_optin:
+ bug_numbers:
+ - 1276190
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-in
+
+ release_optout:
+ bug_numbers:
+ - 1276190
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+
+ keyed_release_optin:
+ bug_numbers:
+ - 1277806
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-in
+
+ keyed_release_optout:
+ bug_numbers:
+ - 1277806
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+
+ keyed_expired:
+ bug_numbers:
+ - 1277806
+ description: This is an expired testing scalar; not meant to be touched.
+ expires: 4.0a1
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ keyed_unsigned_int:
+ bug_numbers:
+ - 1277806
+ description: A testing keyed uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+ keyed_boolean_kind:
+ bug_numbers:
+ - 1277806
+ description: A testing keyed boolean scalar; not meant to be touched.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp
new file mode 100644
index 0000000000..ad2263c9b1
--- /dev/null
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -0,0 +1,3076 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <algorithm>
+
+#include <fstream>
+
+#include <prio.h>
+
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Unused.h"
+
+#include "base/pickle.h"
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsThreadManager.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsXPCOMPrivate.h"
+#include "nsIXULAppInfo.h"
+#include "nsVersionComparator.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/ModuleUtils.h"
+#include "nsIXPConnect.h"
+#include "mozilla/Services.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/GCAPI.h"
+#include "nsString.h"
+#include "nsITelemetry.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsIMemoryReporter.h"
+#include "nsISeekableStream.h"
+#include "Telemetry.h"
+#include "TelemetryCommon.h"
+#include "TelemetryHistogram.h"
+#include "TelemetryScalar.h"
+#include "TelemetryEvent.h"
+#include "WebrtcTelemetry.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsXULAppAPI.h"
+#include "nsReadableUtils.h"
+#include "nsThreadUtils.h"
+#if defined(XP_WIN)
+#include "nsUnicharUtils.h"
+#endif
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsJSUtils.h"
+#include "nsReadableUtils.h"
+#include "plstr.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/ThreadHangStats.h"
+#include "mozilla/ProcessedStack.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/IOInterposer.h"
+#include "mozilla/PoisonIOInterposer.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/HangMonitor.h"
+#if defined(MOZ_ENABLE_PROFILER_SPS)
+#include "shared-libraries.h"
+#endif
+
+namespace {
+
+using namespace mozilla;
+using namespace mozilla::HangMonitor;
+using Telemetry::Common::AutoHashtable;
+
+// The maximum number of chrome hangs stacks that we're keeping.
+const size_t kMaxChromeStacksKept = 50;
+// The maximum depth of a single chrome hang stack.
+const size_t kMaxChromeStackDepth = 50;
+
+// This class is conceptually a list of ProcessedStack objects, but it represents them
+// more efficiently by keeping a single global list of modules.
+class CombinedStacks {
+public:
+ CombinedStacks() : mNextIndex(0) {}
+ typedef std::vector<Telemetry::ProcessedStack::Frame> Stack;
+ const Telemetry::ProcessedStack::Module& GetModule(unsigned aIndex) const;
+ size_t GetModuleCount() const;
+ const Stack& GetStack(unsigned aIndex) const;
+ size_t AddStack(const Telemetry::ProcessedStack& aStack);
+ size_t GetStackCount() const;
+ size_t SizeOfExcludingThis() const;
+private:
+ std::vector<Telemetry::ProcessedStack::Module> mModules;
+ // A circular buffer to hold the stacks.
+ std::vector<Stack> mStacks;
+ // The index of the next buffer element to write to in mStacks.
+ size_t mNextIndex;
+};
+
+static JSObject *
+CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks);
+
+size_t
+CombinedStacks::GetModuleCount() const {
+ return mModules.size();
+}
+
+const Telemetry::ProcessedStack::Module&
+CombinedStacks::GetModule(unsigned aIndex) const {
+ return mModules[aIndex];
+}
+
+size_t
+CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) {
+ // Advance the indices of the circular queue holding the stacks.
+ size_t index = mNextIndex++ % kMaxChromeStacksKept;
+ // Grow the vector up to the maximum size, if needed.
+ if (mStacks.size() < kMaxChromeStacksKept) {
+ mStacks.resize(mStacks.size() + 1);
+ }
+ // Get a reference to the location holding the new stack.
+ CombinedStacks::Stack& adjustedStack = mStacks[index];
+ // If we're using an old stack to hold aStack, clear it.
+ adjustedStack.clear();
+
+ size_t stackSize = aStack.GetStackSize();
+ for (size_t i = 0; i < stackSize; ++i) {
+ const Telemetry::ProcessedStack::Frame& frame = aStack.GetFrame(i);
+ uint16_t modIndex;
+ if (frame.mModIndex == std::numeric_limits<uint16_t>::max()) {
+ modIndex = frame.mModIndex;
+ } else {
+ const Telemetry::ProcessedStack::Module& module =
+ aStack.GetModule(frame.mModIndex);
+ std::vector<Telemetry::ProcessedStack::Module>::iterator modIterator =
+ std::find(mModules.begin(), mModules.end(), module);
+ if (modIterator == mModules.end()) {
+ mModules.push_back(module);
+ modIndex = mModules.size() - 1;
+ } else {
+ modIndex = modIterator - mModules.begin();
+ }
+ }
+ Telemetry::ProcessedStack::Frame adjustedFrame = { frame.mOffset, modIndex };
+ adjustedStack.push_back(adjustedFrame);
+ }
+ return index;
+}
+
+const CombinedStacks::Stack&
+CombinedStacks::GetStack(unsigned aIndex) const {
+ return mStacks[aIndex];
+}
+
+size_t
+CombinedStacks::GetStackCount() const {
+ return mStacks.size();
+}
+
+size_t
+CombinedStacks::SizeOfExcludingThis() const {
+ // This is a crude approximation. We would like to do something like
+ // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call
+ // malloc_usable_size which is only safe on the pointers returned by malloc.
+ // While it works on current libstdc++, it is better to be safe and not assume
+ // that &vec[0] points to one. We could use a custom allocator, but
+ // it doesn't seem worth it.
+ size_t n = 0;
+ n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module);
+ n += mStacks.capacity() * sizeof(Stack);
+ for (std::vector<Stack>::const_iterator i = mStacks.begin(),
+ e = mStacks.end(); i != e; ++i) {
+ const Stack& s = *i;
+ n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame);
+ }
+ return n;
+}
+
+// This utility function generates a string key that is used to index the annotations
+// in a hash map from |HangReports::AddHang|.
+nsresult
+ComputeAnnotationsKey(const HangAnnotationsPtr& aAnnotations, nsAString& aKeyOut)
+{
+ UniquePtr<HangAnnotations::Enumerator> annotationsEnum = aAnnotations->GetEnumerator();
+ if (!annotationsEnum) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Append all the attributes to the key, to uniquely identify this annotation.
+ nsAutoString key;
+ nsAutoString value;
+ while (annotationsEnum->Next(key, value)) {
+ aKeyOut.Append(key);
+ aKeyOut.Append(value);
+ }
+
+ return NS_OK;
+}
+
+class HangReports {
+public:
+ /**
+ * This struct encapsulates information for an individual ChromeHang annotation.
+ * mHangIndex is the index of the corresponding ChromeHang.
+ */
+ struct AnnotationInfo {
+ AnnotationInfo(uint32_t aHangIndex,
+ HangAnnotationsPtr aAnnotations)
+ : mAnnotations(Move(aAnnotations))
+ {
+ mHangIndices.AppendElement(aHangIndex);
+ }
+ AnnotationInfo(AnnotationInfo&& aOther)
+ : mHangIndices(aOther.mHangIndices)
+ , mAnnotations(Move(aOther.mAnnotations))
+ {}
+ ~AnnotationInfo() {}
+ AnnotationInfo& operator=(AnnotationInfo&& aOther)
+ {
+ mHangIndices = aOther.mHangIndices;
+ mAnnotations = Move(aOther.mAnnotations);
+ return *this;
+ }
+ // To save memory, a single AnnotationInfo can be associated to multiple chrome
+ // hangs. The following array holds the index of each related chrome hang.
+ nsTArray<uint32_t> mHangIndices;
+ HangAnnotationsPtr mAnnotations;
+
+ private:
+ // Force move constructor
+ AnnotationInfo(const AnnotationInfo& aOther) = delete;
+ void operator=(const AnnotationInfo& aOther) = delete;
+ };
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ void AddHang(const Telemetry::ProcessedStack& aStack, uint32_t aDuration,
+ int32_t aSystemUptime, int32_t aFirefoxUptime,
+ HangAnnotationsPtr aAnnotations);
+ void PruneStackReferences(const size_t aRemovedStackIndex);
+ uint32_t GetDuration(unsigned aIndex) const;
+ int32_t GetSystemUptime(unsigned aIndex) const;
+ int32_t GetFirefoxUptime(unsigned aIndex) const;
+ const nsClassHashtable<nsStringHashKey, AnnotationInfo>& GetAnnotationInfo() const;
+ const CombinedStacks& GetStacks() const;
+private:
+ /**
+ * This struct encapsulates the data for an individual ChromeHang, excluding
+ * annotations.
+ */
+ struct HangInfo {
+ // Hang duration (in seconds)
+ uint32_t mDuration;
+ // System uptime (in minutes) at the time of the hang
+ int32_t mSystemUptime;
+ // Firefox uptime (in minutes) at the time of the hang
+ int32_t mFirefoxUptime;
+ };
+ std::vector<HangInfo> mHangInfo;
+ nsClassHashtable<nsStringHashKey, AnnotationInfo> mAnnotationInfo;
+ CombinedStacks mStacks;
+};
+
+void
+HangReports::AddHang(const Telemetry::ProcessedStack& aStack,
+ uint32_t aDuration,
+ int32_t aSystemUptime,
+ int32_t aFirefoxUptime,
+ HangAnnotationsPtr aAnnotations) {
+ // Append the new stack to the stack's circular queue.
+ size_t hangIndex = mStacks.AddStack(aStack);
+ // Append the hang info at the same index, in mHangInfo.
+ HangInfo info = { aDuration, aSystemUptime, aFirefoxUptime };
+ if (mHangInfo.size() < kMaxChromeStacksKept) {
+ mHangInfo.push_back(info);
+ } else {
+ mHangInfo[hangIndex] = info;
+ // Remove any reference to the stack overwritten in the circular queue
+ // from the annotations.
+ PruneStackReferences(hangIndex);
+ }
+
+ if (!aAnnotations) {
+ return;
+ }
+
+ nsAutoString annotationsKey;
+ // Generate a key to index aAnnotations in the hash map.
+ nsresult rv = ComputeAnnotationsKey(aAnnotations, annotationsKey);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ AnnotationInfo* annotationsEntry = mAnnotationInfo.Get(annotationsKey);
+ if (annotationsEntry) {
+ // If the key is already in the hash map, append the index of the chrome hang
+ // to its indices.
+ annotationsEntry->mHangIndices.AppendElement(hangIndex);
+ return;
+ }
+
+ // If the key was not found, add the annotations to the hash map.
+ mAnnotationInfo.Put(annotationsKey, new AnnotationInfo(hangIndex, Move(aAnnotations)));
+}
+
+/**
+ * This function removes links to discarded chrome hangs stacks and prunes unused
+ * annotations.
+ */
+void
+HangReports::PruneStackReferences(const size_t aRemovedStackIndex) {
+ // We need to adjust the indices that link annotations to chrome hangs. Since we
+ // removed a stack, we must remove all references to it and prune annotations
+ // linked to no stacks.
+ for (auto iter = mAnnotationInfo.Iter(); !iter.Done(); iter.Next()) {
+ nsTArray<uint32_t>& stackIndices = iter.Data()->mHangIndices;
+ size_t toRemove = stackIndices.NoIndex;
+ for (size_t k = 0; k < stackIndices.Length(); k++) {
+ // Is this index referencing the removed stack?
+ if (stackIndices[k] == aRemovedStackIndex) {
+ toRemove = k;
+ break;
+ }
+ }
+
+ // Remove the index referencing the old stack from the annotation.
+ if (toRemove != stackIndices.NoIndex) {
+ stackIndices.RemoveElementAt(toRemove);
+ }
+
+ // If this annotation no longer references any stack, drop it.
+ if (!stackIndices.Length()) {
+ iter.Remove();
+ }
+ }
+}
+
+size_t
+HangReports::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+ n += mStacks.SizeOfExcludingThis();
+ // This is a crude approximation. See comment on
+ // CombinedStacks::SizeOfExcludingThis.
+ n += mHangInfo.capacity() * sizeof(HangInfo);
+ n += mAnnotationInfo.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ n += mAnnotationInfo.Count() * sizeof(AnnotationInfo);
+ for (auto iter = mAnnotationInfo.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += iter.Data()->mAnnotations->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+const CombinedStacks&
+HangReports::GetStacks() const {
+ return mStacks;
+}
+
+uint32_t
+HangReports::GetDuration(unsigned aIndex) const {
+ return mHangInfo[aIndex].mDuration;
+}
+
+int32_t
+HangReports::GetSystemUptime(unsigned aIndex) const {
+ return mHangInfo[aIndex].mSystemUptime;
+}
+
+int32_t
+HangReports::GetFirefoxUptime(unsigned aIndex) const {
+ return mHangInfo[aIndex].mFirefoxUptime;
+}
+
+const nsClassHashtable<nsStringHashKey, HangReports::AnnotationInfo>&
+HangReports::GetAnnotationInfo() const {
+ return mAnnotationInfo;
+}
+
+/**
+ * IOInterposeObserver recording statistics of main-thread I/O during execution,
+ * aimed at consumption by TelemetryImpl
+ */
+class TelemetryIOInterposeObserver : public IOInterposeObserver
+{
+ /** File-level statistics structure */
+ struct FileStats {
+ FileStats()
+ : creates(0)
+ , reads(0)
+ , writes(0)
+ , fsyncs(0)
+ , stats(0)
+ , totalTime(0)
+ {}
+ uint32_t creates; /** Number of create/open operations */
+ uint32_t reads; /** Number of read operations */
+ uint32_t writes; /** Number of write operations */
+ uint32_t fsyncs; /** Number of fsync operations */
+ uint32_t stats; /** Number of stat operations */
+ double totalTime; /** Accumulated duration of all operations */
+ };
+
+ struct SafeDir {
+ SafeDir(const nsAString& aPath, const nsAString& aSubstName)
+ : mPath(aPath)
+ , mSubstName(aSubstName)
+ {}
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return mPath.SizeOfExcludingThisIfUnshared(aMallocSizeOf) +
+ mSubstName.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ nsString mPath; /** Path to the directory */
+ nsString mSubstName; /** Name to substitute with */
+ };
+
+public:
+ explicit TelemetryIOInterposeObserver(nsIFile* aXreDir);
+
+ /**
+ * An implementation of Observe that records statistics of all
+ * file IO operations.
+ */
+ void Observe(Observation& aOb);
+
+ /**
+ * Reflect recorded file IO statistics into Javascript
+ */
+ bool ReflectIntoJS(JSContext *cx, JS::Handle<JSObject*> rootObj);
+
+ /**
+ * Adds a path for inclusion in main thread I/O report.
+ * @param aPath Directory path
+ * @param aSubstName Name to substitute for aPath for privacy reasons
+ */
+ void AddPath(const nsAString& aPath, const nsAString& aSubstName);
+
+ /**
+ * Get size of hash table with file stats
+ */
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t size = 0;
+ size += mFileStats.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = mFileStats.ConstIter(); !iter.Done(); iter.Next()) {
+ size += iter.Get()->GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ size += mSafeDirs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ uint32_t safeDirsLen = mSafeDirs.Length();
+ for (uint32_t i = 0; i < safeDirsLen; ++i) {
+ size += mSafeDirs[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return size;
+ }
+
+private:
+ enum Stage
+ {
+ STAGE_STARTUP = 0,
+ STAGE_NORMAL,
+ STAGE_SHUTDOWN,
+ NUM_STAGES
+ };
+ static inline Stage NextStage(Stage aStage)
+ {
+ switch (aStage) {
+ case STAGE_STARTUP:
+ return STAGE_NORMAL;
+ case STAGE_NORMAL:
+ return STAGE_SHUTDOWN;
+ case STAGE_SHUTDOWN:
+ return STAGE_SHUTDOWN;
+ default:
+ return NUM_STAGES;
+ }
+ }
+
+ struct FileStatsByStage
+ {
+ FileStats mStats[NUM_STAGES];
+ };
+ typedef nsBaseHashtableET<nsStringHashKey, FileStatsByStage> FileIOEntryType;
+
+ // Statistics for each filename
+ AutoHashtable<FileIOEntryType> mFileStats;
+ // Container for whitelisted directories
+ nsTArray<SafeDir> mSafeDirs;
+ Stage mCurStage;
+
+ /**
+ * Reflect a FileIOEntryType object to a Javascript property on obj with
+ * filename as key containing array:
+ * [totalTime, creates, reads, writes, fsyncs, stats]
+ */
+ static bool ReflectFileStats(FileIOEntryType* entry, JSContext *cx,
+ JS::Handle<JSObject*> obj);
+};
+
+TelemetryIOInterposeObserver::TelemetryIOInterposeObserver(nsIFile* aXreDir)
+ : mCurStage(STAGE_STARTUP)
+{
+ nsAutoString xreDirPath;
+ nsresult rv = aXreDir->GetPath(xreDirPath);
+ if (NS_SUCCEEDED(rv)) {
+ AddPath(xreDirPath, NS_LITERAL_STRING("{xre}"));
+ }
+}
+
+void TelemetryIOInterposeObserver::AddPath(const nsAString& aPath,
+ const nsAString& aSubstName)
+{
+ mSafeDirs.AppendElement(SafeDir(aPath, aSubstName));
+}
+
+// Threshold for reporting slow main-thread I/O (50 milliseconds).
+const TimeDuration kTelemetryReportThreshold = TimeDuration::FromMilliseconds(50);
+
+void TelemetryIOInterposeObserver::Observe(Observation& aOb)
+{
+ // We only report main-thread I/O
+ if (!IsMainThread()) {
+ return;
+ }
+
+ if (aOb.ObservedOperation() == OpNextStage) {
+ mCurStage = NextStage(mCurStage);
+ MOZ_ASSERT(mCurStage < NUM_STAGES);
+ return;
+ }
+
+ if (aOb.Duration() < kTelemetryReportThreshold) {
+ return;
+ }
+
+ // Get the filename
+ const char16_t* filename = aOb.Filename();
+
+ // Discard observations without filename
+ if (!filename) {
+ return;
+ }
+
+#if defined(XP_WIN)
+ nsCaseInsensitiveStringComparator comparator;
+#else
+ nsDefaultStringComparator comparator;
+#endif
+ nsAutoString processedName;
+ nsDependentString filenameStr(filename);
+ uint32_t safeDirsLen = mSafeDirs.Length();
+ for (uint32_t i = 0; i < safeDirsLen; ++i) {
+ if (StringBeginsWith(filenameStr, mSafeDirs[i].mPath, comparator)) {
+ processedName = mSafeDirs[i].mSubstName;
+ processedName += Substring(filenameStr, mSafeDirs[i].mPath.Length());
+ break;
+ }
+ }
+
+ if (processedName.IsEmpty()) {
+ return;
+ }
+
+ // Create a new entry or retrieve the existing one
+ FileIOEntryType* entry = mFileStats.PutEntry(processedName);
+ if (entry) {
+ FileStats& stats = entry->mData.mStats[mCurStage];
+ // Update the statistics
+ stats.totalTime += (double) aOb.Duration().ToMilliseconds();
+ switch (aOb.ObservedOperation()) {
+ case OpCreateOrOpen:
+ stats.creates++;
+ break;
+ case OpRead:
+ stats.reads++;
+ break;
+ case OpWrite:
+ stats.writes++;
+ break;
+ case OpFSync:
+ stats.fsyncs++;
+ break;
+ case OpStat:
+ stats.stats++;
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+bool TelemetryIOInterposeObserver::ReflectFileStats(FileIOEntryType* entry,
+ JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ JS::AutoValueArray<NUM_STAGES> stages(cx);
+
+ FileStatsByStage& statsByStage = entry->mData;
+ for (int s = STAGE_STARTUP; s < NUM_STAGES; ++s) {
+ FileStats& fileStats = statsByStage.mStats[s];
+
+ if (fileStats.totalTime == 0 && fileStats.creates == 0 &&
+ fileStats.reads == 0 && fileStats.writes == 0 &&
+ fileStats.fsyncs == 0 && fileStats.stats == 0) {
+ // Don't add an array that contains no information
+ stages[s].setNull();
+ continue;
+ }
+
+ // Array we want to report
+ JS::AutoValueArray<6> stats(cx);
+ stats[0].setNumber(fileStats.totalTime);
+ stats[1].setNumber(fileStats.creates);
+ stats[2].setNumber(fileStats.reads);
+ stats[3].setNumber(fileStats.writes);
+ stats[4].setNumber(fileStats.fsyncs);
+ stats[5].setNumber(fileStats.stats);
+
+ // Create jsStats as array of elements above
+ JS::RootedObject jsStats(cx, JS_NewArrayObject(cx, stats));
+ if (!jsStats) {
+ continue;
+ }
+
+ stages[s].setObject(*jsStats);
+ }
+
+ JS::Rooted<JSObject*> jsEntry(cx, JS_NewArrayObject(cx, stages));
+ if (!jsEntry) {
+ return false;
+ }
+
+ // Add jsEntry to top-level dictionary
+ const nsAString& key = entry->GetKey();
+ return JS_DefineUCProperty(cx, obj, key.Data(), key.Length(),
+ jsEntry, JSPROP_ENUMERATE | JSPROP_READONLY);
+}
+
+bool TelemetryIOInterposeObserver::ReflectIntoJS(JSContext *cx,
+ JS::Handle<JSObject*> rootObj)
+{
+ return mFileStats.ReflectIntoJS(ReflectFileStats, cx, rootObj);
+}
+
+// This is not a member of TelemetryImpl because we want to record I/O during
+// startup.
+StaticAutoPtr<TelemetryIOInterposeObserver> sTelemetryIOObserver;
+
+void
+ClearIOReporting()
+{
+ if (!sTelemetryIOObserver) {
+ return;
+ }
+ IOInterposer::Unregister(IOInterposeObserver::OpAllWithStaging,
+ sTelemetryIOObserver);
+ sTelemetryIOObserver = nullptr;
+}
+
+class TelemetryImpl final
+ : public nsITelemetry
+ , public nsIMemoryReporter
+{
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITELEMETRY
+ NS_DECL_NSIMEMORYREPORTER
+
+public:
+ void InitMemoryReporter();
+
+ static already_AddRefed<nsITelemetry> CreateTelemetryInstance();
+ static void ShutdownTelemetry();
+ static void RecordSlowStatement(const nsACString &sql, const nsACString &dbName,
+ uint32_t delay);
+#if defined(MOZ_ENABLE_PROFILER_SPS)
+ static void RecordChromeHang(uint32_t aDuration,
+ Telemetry::ProcessedStack &aStack,
+ int32_t aSystemUptime,
+ int32_t aFirefoxUptime,
+ HangAnnotationsPtr aAnnotations);
+#endif
+ static void RecordThreadHangStats(Telemetry::ThreadHangStats& aStats);
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+ struct Stat {
+ uint32_t hitCount;
+ uint32_t totalTime;
+ };
+ struct StmtStats {
+ struct Stat mainThread;
+ struct Stat otherThreads;
+ };
+ typedef nsBaseHashtableET<nsCStringHashKey, StmtStats> SlowSQLEntryType;
+
+ static void RecordIceCandidates(const uint32_t iceCandidateBitmask,
+ const bool success);
+private:
+ TelemetryImpl();
+ ~TelemetryImpl();
+
+ static nsCString SanitizeSQL(const nsACString& sql);
+
+ enum SanitizedState { Sanitized, Unsanitized };
+
+ static void StoreSlowSQL(const nsACString &offender, uint32_t delay,
+ SanitizedState state);
+
+ static bool ReflectMainThreadSQL(SlowSQLEntryType *entry, JSContext *cx,
+ JS::Handle<JSObject*> obj);
+ static bool ReflectOtherThreadsSQL(SlowSQLEntryType *entry, JSContext *cx,
+ JS::Handle<JSObject*> obj);
+ static bool ReflectSQL(const SlowSQLEntryType *entry, const Stat *stat,
+ JSContext *cx, JS::Handle<JSObject*> obj);
+
+ bool AddSQLInfo(JSContext *cx, JS::Handle<JSObject*> rootObj, bool mainThread,
+ bool privateSQL);
+ bool GetSQLStats(JSContext *cx, JS::MutableHandle<JS::Value> ret,
+ bool includePrivateSql);
+
+ void ReadLateWritesStacks(nsIFile* aProfileDir);
+
+ static TelemetryImpl *sTelemetry;
+ AutoHashtable<SlowSQLEntryType> mPrivateSQL;
+ AutoHashtable<SlowSQLEntryType> mSanitizedSQL;
+ Mutex mHashMutex;
+ HangReports mHangReports;
+ Mutex mHangReportsMutex;
+ // mThreadHangStats stores recorded, inactive thread hang stats
+ Vector<Telemetry::ThreadHangStats> mThreadHangStats;
+ Mutex mThreadHangStatsMutex;
+
+ CombinedStacks mLateWritesStacks; // This is collected out of the main thread.
+ bool mCachedTelemetryData;
+ uint32_t mLastShutdownTime;
+ uint32_t mFailedLockCount;
+ nsCOMArray<nsIFetchTelemetryDataCallback> mCallbacks;
+ friend class nsFetchTelemetryData;
+
+ WebrtcTelemetry mWebrtcTelemetry;
+};
+
+TelemetryImpl* TelemetryImpl::sTelemetry = nullptr;
+
+MOZ_DEFINE_MALLOC_SIZE_OF(TelemetryMallocSizeOf)
+
+NS_IMETHODIMP
+TelemetryImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize)
+{
+ MOZ_COLLECT_REPORT(
+ "explicit/telemetry", KIND_HEAP, UNITS_BYTES,
+ SizeOfIncludingThis(TelemetryMallocSizeOf),
+ "Memory used by the telemetry system.");
+
+ return NS_OK;
+}
+
+void
+InitHistogramRecordingEnabled()
+{
+ TelemetryHistogram::InitHistogramRecordingEnabled();
+}
+
+static uint32_t
+ReadLastShutdownDuration(const char *filename) {
+ FILE *f = fopen(filename, "r");
+ if (!f) {
+ return 0;
+ }
+
+ int shutdownTime;
+ int r = fscanf(f, "%d\n", &shutdownTime);
+ fclose(f);
+ if (r != 1) {
+ return 0;
+ }
+
+ return shutdownTime;
+}
+
+const int32_t kMaxFailedProfileLockFileSize = 10;
+
+bool
+GetFailedLockCount(nsIInputStream* inStream, uint32_t aCount,
+ unsigned int& result)
+{
+ nsAutoCString bufStr;
+ nsresult rv;
+ rv = NS_ReadInputStreamToString(inStream, bufStr, aCount);
+ NS_ENSURE_SUCCESS(rv, false);
+ result = bufStr.ToInteger(&rv);
+ return NS_SUCCEEDED(rv) && result > 0;
+}
+
+nsresult
+GetFailedProfileLockFile(nsIFile* *aFile, nsIFile* aProfileDir)
+{
+ NS_ENSURE_ARG_POINTER(aProfileDir);
+
+ nsresult rv = aProfileDir->Clone(aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (*aFile)->AppendNative(NS_LITERAL_CSTRING("Telemetry.FailedProfileLocks.txt"));
+ return NS_OK;
+}
+
+class nsFetchTelemetryData : public Runnable
+{
+public:
+ nsFetchTelemetryData(const char* aShutdownTimeFilename,
+ nsIFile* aFailedProfileLockFile,
+ nsIFile* aProfileDir)
+ : mShutdownTimeFilename(aShutdownTimeFilename),
+ mFailedProfileLockFile(aFailedProfileLockFile),
+ mTelemetry(TelemetryImpl::sTelemetry),
+ mProfileDir(aProfileDir)
+ {
+ }
+
+private:
+ const char* mShutdownTimeFilename;
+ nsCOMPtr<nsIFile> mFailedProfileLockFile;
+ RefPtr<TelemetryImpl> mTelemetry;
+ nsCOMPtr<nsIFile> mProfileDir;
+
+public:
+ void MainThread() {
+ mTelemetry->mCachedTelemetryData = true;
+ for (unsigned int i = 0, n = mTelemetry->mCallbacks.Count(); i < n; ++i) {
+ mTelemetry->mCallbacks[i]->Complete();
+ }
+ mTelemetry->mCallbacks.Clear();
+ }
+
+ NS_IMETHOD Run() override {
+ LoadFailedLockCount(mTelemetry->mFailedLockCount);
+ mTelemetry->mLastShutdownTime =
+ ReadLastShutdownDuration(mShutdownTimeFilename);
+ mTelemetry->ReadLateWritesStacks(mProfileDir);
+ nsCOMPtr<nsIRunnable> e =
+ NewRunnableMethod(this, &nsFetchTelemetryData::MainThread);
+ NS_ENSURE_STATE(e);
+ NS_DispatchToMainThread(e);
+ return NS_OK;
+ }
+
+private:
+ nsresult
+ LoadFailedLockCount(uint32_t& failedLockCount)
+ {
+ failedLockCount = 0;
+ int64_t fileSize = 0;
+ nsresult rv = mFailedProfileLockFile->GetFileSize(&fileSize);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ NS_ENSURE_TRUE(fileSize <= kMaxFailedProfileLockFileSize,
+ NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIInputStream> inStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream),
+ mFailedProfileLockFile,
+ PR_RDONLY);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(GetFailedLockCount(inStream, fileSize, failedLockCount),
+ NS_ERROR_UNEXPECTED);
+ inStream->Close();
+
+ mFailedProfileLockFile->Remove(false);
+ return NS_OK;
+ }
+};
+
+static TimeStamp gRecordedShutdownStartTime;
+static bool gAlreadyFreedShutdownTimeFileName = false;
+static char *gRecordedShutdownTimeFileName = nullptr;
+
+static char *
+GetShutdownTimeFileName()
+{
+ if (gAlreadyFreedShutdownTimeFileName) {
+ return nullptr;
+ }
+
+ if (!gRecordedShutdownTimeFileName) {
+ nsCOMPtr<nsIFile> mozFile;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mozFile));
+ if (!mozFile)
+ return nullptr;
+
+ mozFile->AppendNative(NS_LITERAL_CSTRING("Telemetry.ShutdownTime.txt"));
+ nsAutoCString nativePath;
+ nsresult rv = mozFile->GetNativePath(nativePath);
+ if (!NS_SUCCEEDED(rv))
+ return nullptr;
+
+ gRecordedShutdownTimeFileName = PL_strdup(nativePath.get());
+ }
+
+ return gRecordedShutdownTimeFileName;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetLastShutdownDuration(uint32_t *aResult)
+{
+ // The user must call AsyncFetchTelemetryData first. We return zero instead of
+ // reporting a failure so that the rest of telemetry can uniformly handle
+ // the read not being available yet.
+ if (!mCachedTelemetryData) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ *aResult = mLastShutdownTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetFailedProfileLockCount(uint32_t* aResult)
+{
+ // The user must call AsyncFetchTelemetryData first. We return zero instead of
+ // reporting a failure so that the rest of telemetry can uniformly handle
+ // the read not being available yet.
+ if (!mCachedTelemetryData) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ *aResult = mFailedLockCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::AsyncFetchTelemetryData(nsIFetchTelemetryDataCallback *aCallback)
+{
+ // We have finished reading the data already, just call the callback.
+ if (mCachedTelemetryData) {
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // We already have a read request running, just remember the callback.
+ if (mCallbacks.Count() != 0) {
+ mCallbacks.AppendObject(aCallback);
+ return NS_OK;
+ }
+
+ // We make this check so that GetShutdownTimeFileName() doesn't get
+ // called; calling that function without telemetry enabled violates
+ // assumptions that the write-the-shutdown-timestamp machinery makes.
+ if (!Telemetry::CanRecordExtended()) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // Send the read to a background thread provided by the stream transport
+ // service to avoid a read in the main thread.
+ nsCOMPtr<nsIEventTarget> targetThread =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ if (!targetThread) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // We have to get the filename from the main thread.
+ const char *shutdownTimeFilename = GetShutdownTimeFileName();
+ if (!shutdownTimeFilename) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_FAILED(rv)) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> failedProfileLockFile;
+ rv = GetFailedProfileLockFile(getter_AddRefs(failedProfileLockFile),
+ profileDir);
+ if (NS_FAILED(rv)) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ mCallbacks.AppendObject(aCallback);
+
+ nsCOMPtr<nsIRunnable> event = new nsFetchTelemetryData(shutdownTimeFilename,
+ failedProfileLockFile,
+ profileDir);
+
+ targetThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ return NS_OK;
+}
+
+TelemetryImpl::TelemetryImpl()
+ : mHashMutex("Telemetry::mHashMutex")
+ , mHangReportsMutex("Telemetry::mHangReportsMutex")
+ , mThreadHangStatsMutex("Telemetry::mThreadHangStatsMutex")
+ , mCachedTelemetryData(false)
+ , mLastShutdownTime(0)
+ , mFailedLockCount(0)
+{
+ // We expect TelemetryHistogram::InitializeGlobalState() to have been
+ // called before we get to this point.
+ MOZ_ASSERT(TelemetryHistogram::GlobalStateHasBeenInitialized());
+}
+
+TelemetryImpl::~TelemetryImpl() {
+ UnregisterWeakMemoryReporter(this);
+}
+
+void
+TelemetryImpl::InitMemoryReporter() {
+ RegisterWeakMemoryReporter(this);
+}
+
+bool
+TelemetryImpl::ReflectSQL(const SlowSQLEntryType *entry,
+ const Stat *stat,
+ JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ if (stat->hitCount == 0)
+ return true;
+
+ const nsACString &sql = entry->GetKey();
+
+ JS::Rooted<JSObject*> arrayObj(cx, JS_NewArrayObject(cx, 0));
+ if (!arrayObj) {
+ return false;
+ }
+ return (JS_DefineElement(cx, arrayObj, 0, stat->hitCount, JSPROP_ENUMERATE)
+ && JS_DefineElement(cx, arrayObj, 1, stat->totalTime, JSPROP_ENUMERATE)
+ && JS_DefineProperty(cx, obj, sql.BeginReading(), arrayObj,
+ JSPROP_ENUMERATE));
+}
+
+bool
+TelemetryImpl::ReflectMainThreadSQL(SlowSQLEntryType *entry, JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ return ReflectSQL(entry, &entry->mData.mainThread, cx, obj);
+}
+
+bool
+TelemetryImpl::ReflectOtherThreadsSQL(SlowSQLEntryType *entry, JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ return ReflectSQL(entry, &entry->mData.otherThreads, cx, obj);
+}
+
+bool
+TelemetryImpl::AddSQLInfo(JSContext *cx, JS::Handle<JSObject*> rootObj, bool mainThread,
+ bool privateSQL)
+{
+ JS::Rooted<JSObject*> statsObj(cx, JS_NewPlainObject(cx));
+ if (!statsObj)
+ return false;
+
+ AutoHashtable<SlowSQLEntryType>& sqlMap = (privateSQL ? mPrivateSQL : mSanitizedSQL);
+ AutoHashtable<SlowSQLEntryType>::ReflectEntryFunc reflectFunction =
+ (mainThread ? ReflectMainThreadSQL : ReflectOtherThreadsSQL);
+ if (!sqlMap.ReflectIntoJS(reflectFunction, cx, statsObj)) {
+ return false;
+ }
+
+ return JS_DefineProperty(cx, rootObj,
+ mainThread ? "mainThread" : "otherThreads",
+ statsObj, JSPROP_ENUMERATE);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterAddonHistogram(const nsACString &id,
+ const nsACString &name,
+ uint32_t histogramType,
+ uint32_t min, uint32_t max,
+ uint32_t bucketCount,
+ uint8_t optArgCount)
+{
+ return TelemetryHistogram::RegisterAddonHistogram
+ (id, name, histogramType, min, max, bucketCount, optArgCount);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetAddonHistogram(const nsACString &id, const nsACString &name,
+ JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ return TelemetryHistogram::GetAddonHistogram(id, name, cx, ret);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::UnregisterAddonHistograms(const nsACString &id)
+{
+ return TelemetryHistogram::UnregisterAddonHistograms(id);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled)
+{
+ return TelemetryHistogram::SetHistogramRecordingEnabled(id, aEnabled);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, false, false);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotSubsessionHistograms(bool clearSubsession,
+ JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ return TelemetryHistogram::CreateHistogramSnapshots(cx, ret, true,
+ clearSubsession);
+#else
+ return NS_OK;
+#endif
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetAddonHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ return TelemetryHistogram::GetAddonHistogramSnapshots(cx, ret);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetKeyedHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ return TelemetryHistogram::GetKeyedHistogramSnapshots(cx, ret);
+}
+
+bool
+TelemetryImpl::GetSQLStats(JSContext *cx, JS::MutableHandle<JS::Value> ret, bool includePrivateSql)
+{
+ JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
+ if (!root_obj)
+ return false;
+ ret.setObject(*root_obj);
+
+ MutexAutoLock hashMutex(mHashMutex);
+ // Add info about slow SQL queries on the main thread
+ if (!AddSQLInfo(cx, root_obj, true, includePrivateSql))
+ return false;
+ // Add info about slow SQL queries on other threads
+ if (!AddSQLInfo(cx, root_obj, false, includePrivateSql))
+ return false;
+
+ return true;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSlowSQL(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ if (GetSQLStats(cx, ret, false))
+ return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetDebugSlowSQL(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ bool revealPrivateSql =
+ Preferences::GetBool("toolkit.telemetry.debugSlowSql", false);
+ if (GetSQLStats(cx, ret, revealPrivateSql))
+ return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetWebrtcStats(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ if (mWebrtcTelemetry.GetWebrtcStats(cx, ret))
+ return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetMaximalNumberOfConcurrentThreads(uint32_t *ret)
+{
+ *ret = nsThreadManager::get().GetHighestNumberOfThreads();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetChromeHangs(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ MutexAutoLock hangReportMutex(mHangReportsMutex);
+
+ const CombinedStacks& stacks = mHangReports.GetStacks();
+ JS::Rooted<JSObject*> fullReportObj(cx, CreateJSStackObject(cx, stacks));
+ if (!fullReportObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ret.setObject(*fullReportObj);
+
+ JS::Rooted<JSObject*> durationArray(cx, JS_NewArrayObject(cx, 0));
+ JS::Rooted<JSObject*> systemUptimeArray(cx, JS_NewArrayObject(cx, 0));
+ JS::Rooted<JSObject*> firefoxUptimeArray(cx, JS_NewArrayObject(cx, 0));
+ JS::Rooted<JSObject*> annotationsArray(cx, JS_NewArrayObject(cx, 0));
+ if (!durationArray || !systemUptimeArray || !firefoxUptimeArray ||
+ !annotationsArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool ok = JS_DefineProperty(cx, fullReportObj, "durations",
+ durationArray, JSPROP_ENUMERATE);
+ if (!ok) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ok = JS_DefineProperty(cx, fullReportObj, "systemUptime",
+ systemUptimeArray, JSPROP_ENUMERATE);
+ if (!ok) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ok = JS_DefineProperty(cx, fullReportObj, "firefoxUptime",
+ firefoxUptimeArray, JSPROP_ENUMERATE);
+ if (!ok) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ok = JS_DefineProperty(cx, fullReportObj, "annotations", annotationsArray,
+ JSPROP_ENUMERATE);
+ if (!ok) {
+ return NS_ERROR_FAILURE;
+ }
+
+
+ const size_t length = stacks.GetStackCount();
+ for (size_t i = 0; i < length; ++i) {
+ if (!JS_DefineElement(cx, durationArray, i, mHangReports.GetDuration(i),
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineElement(cx, systemUptimeArray, i, mHangReports.GetSystemUptime(i),
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineElement(cx, firefoxUptimeArray, i, mHangReports.GetFirefoxUptime(i),
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ size_t annotationIndex = 0;
+ const nsClassHashtable<nsStringHashKey, HangReports::AnnotationInfo>& annotationInfo =
+ mHangReports.GetAnnotationInfo();
+
+ for (auto iter = annotationInfo.ConstIter(); !iter.Done(); iter.Next()) {
+ const HangReports::AnnotationInfo* info = iter.Data();
+
+ JS::Rooted<JSObject*> keyValueArray(cx, JS_NewArrayObject(cx, 0));
+ if (!keyValueArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create an array containing all the indices of the chrome hangs relative to this
+ // annotation.
+ JS::Rooted<JS::Value> indicesArray(cx);
+ if (!mozilla::dom::ToJSValue(cx, info->mHangIndices, &indicesArray)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // We're saving the annotation as [[indices], {annotation-data}], so add the indices
+ // array as the first element of that structure.
+ if (!JS_DefineElement(cx, keyValueArray, 0, indicesArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Create the annotations object...
+ JS::Rooted<JSObject*> jsAnnotation(cx, JS_NewPlainObject(cx));
+ if (!jsAnnotation) {
+ return NS_ERROR_FAILURE;
+ }
+ UniquePtr<HangAnnotations::Enumerator> annotationsEnum =
+ info->mAnnotations->GetEnumerator();
+ if (!annotationsEnum) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // ... fill it with key:value pairs...
+ nsAutoString key;
+ nsAutoString value;
+ while (annotationsEnum->Next(key, value)) {
+ JS::RootedValue jsValue(cx);
+ jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length()));
+ if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(),
+ jsValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // ... and append it after the indices array.
+ if (!JS_DefineElement(cx, keyValueArray, 1, jsAnnotation, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineElement(cx, annotationsArray, annotationIndex++,
+ keyValueArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+static JSObject *
+CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks) {
+ JS::Rooted<JSObject*> ret(cx, JS_NewPlainObject(cx));
+ if (!ret) {
+ return nullptr;
+ }
+
+ JS::Rooted<JSObject*> moduleArray(cx, JS_NewArrayObject(cx, 0));
+ if (!moduleArray) {
+ return nullptr;
+ }
+ bool ok = JS_DefineProperty(cx, ret, "memoryMap", moduleArray,
+ JSPROP_ENUMERATE);
+ if (!ok) {
+ return nullptr;
+ }
+
+ const size_t moduleCount = stacks.GetModuleCount();
+ for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
+ // Current module
+ const Telemetry::ProcessedStack::Module& module =
+ stacks.GetModule(moduleIndex);
+
+ JS::Rooted<JSObject*> moduleInfoArray(cx, JS_NewArrayObject(cx, 0));
+ if (!moduleInfoArray) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray,
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ unsigned index = 0;
+
+ // Module name
+ JS::Rooted<JSString*> str(cx, JS_NewStringCopyZ(cx, module.mName.c_str()));
+ if (!str) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, moduleInfoArray, index++, str, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ // Module breakpad identifier
+ JS::Rooted<JSString*> id(cx, JS_NewStringCopyZ(cx, module.mBreakpadId.c_str()));
+ if (!id) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, moduleInfoArray, index++, id, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+
+ JS::Rooted<JSObject*> reportArray(cx, JS_NewArrayObject(cx, 0));
+ if (!reportArray) {
+ return nullptr;
+ }
+ ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE);
+ if (!ok) {
+ return nullptr;
+ }
+
+ const size_t length = stacks.GetStackCount();
+ for (size_t i = 0; i < length; ++i) {
+ // Represent call stack PCs as (module index, offset) pairs.
+ JS::Rooted<JSObject*> pcArray(cx, JS_NewArrayObject(cx, 0));
+ if (!pcArray) {
+ return nullptr;
+ }
+
+ if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ const CombinedStacks::Stack& stack = stacks.GetStack(i);
+ const uint32_t pcCount = stack.size();
+ for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
+ const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex];
+ JS::Rooted<JSObject*> framePair(cx, JS_NewArrayObject(cx, 0));
+ if (!framePair) {
+ return nullptr;
+ }
+ int modIndex = (std::numeric_limits<uint16_t>::max() == frame.mModIndex) ?
+ -1 : frame.mModIndex;
+ if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, framePair, 1, static_cast<double>(frame.mOffset),
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, pcArray, pcIndex, framePair, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+ }
+
+ return ret;
+}
+
+static bool
+IsValidBreakpadId(const std::string &breakpadId) {
+ if (breakpadId.size() < 33) {
+ return false;
+ }
+ for (unsigned i = 0, n = breakpadId.size(); i < n; ++i) {
+ char c = breakpadId[i];
+ if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Read a stack from the given file name. In case of any error, aStack is
+// unchanged.
+static void
+ReadStack(const char *aFileName, Telemetry::ProcessedStack &aStack)
+{
+ std::ifstream file(aFileName);
+
+ size_t numModules;
+ file >> numModules;
+ if (file.fail()) {
+ return;
+ }
+
+ char newline = file.get();
+ if (file.fail() || newline != '\n') {
+ return;
+ }
+
+ Telemetry::ProcessedStack stack;
+ for (size_t i = 0; i < numModules; ++i) {
+ std::string breakpadId;
+ file >> breakpadId;
+ if (file.fail() || !IsValidBreakpadId(breakpadId)) {
+ return;
+ }
+
+ char space = file.get();
+ if (file.fail() || space != ' ') {
+ return;
+ }
+
+ std::string moduleName;
+ getline(file, moduleName);
+ if (file.fail() || moduleName[0] == ' ') {
+ return;
+ }
+
+ Telemetry::ProcessedStack::Module module = {
+ moduleName,
+ breakpadId
+ };
+ stack.AddModule(module);
+ }
+
+ size_t numFrames;
+ file >> numFrames;
+ if (file.fail()) {
+ return;
+ }
+
+ newline = file.get();
+ if (file.fail() || newline != '\n') {
+ return;
+ }
+
+ for (size_t i = 0; i < numFrames; ++i) {
+ uint16_t index;
+ file >> index;
+ uintptr_t offset;
+ file >> std::hex >> offset >> std::dec;
+ if (file.fail()) {
+ return;
+ }
+
+ Telemetry::ProcessedStack::Frame frame = {
+ offset,
+ index
+ };
+ stack.AddFrame(frame);
+ }
+
+ aStack = stack;
+}
+
+static JSObject*
+CreateJSTimeHistogram(JSContext* cx, const Telemetry::TimeHistogram& time)
+{
+ /* Create JS representation of TimeHistogram,
+ in the format of Chromium-style histograms. */
+ JS::RootedObject ret(cx, JS_NewPlainObject(cx));
+ if (!ret) {
+ return nullptr;
+ }
+
+ if (!JS_DefineProperty(cx, ret, "min", time.GetBucketMin(0),
+ JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, ret, "max",
+ time.GetBucketMax(ArrayLength(time) - 1),
+ JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, ret, "histogram_type",
+ nsITelemetry::HISTOGRAM_EXPONENTIAL,
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ // TODO: calculate "sum"
+ if (!JS_DefineProperty(cx, ret, "sum", 0, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ JS::RootedObject ranges(
+ cx, JS_NewArrayObject(cx, ArrayLength(time) + 1));
+ JS::RootedObject counts(
+ cx, JS_NewArrayObject(cx, ArrayLength(time) + 1));
+ if (!ranges || !counts) {
+ return nullptr;
+ }
+ /* In a Chromium-style histogram, the first bucket is an "under" bucket
+ that represents all values below the histogram's range. */
+ if (!JS_DefineElement(cx, ranges, 0, time.GetBucketMin(0), JSPROP_ENUMERATE) ||
+ !JS_DefineElement(cx, counts, 0, 0, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < ArrayLength(time); i++) {
+ if (!JS_DefineElement(cx, ranges, i + 1, time.GetBucketMax(i),
+ JSPROP_ENUMERATE) ||
+ !JS_DefineElement(cx, counts, i + 1, time[i], JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+ if (!JS_DefineProperty(cx, ret, "ranges", ranges, JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, ret, "counts", counts, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ return ret;
+}
+
+static JSObject*
+CreateJSHangStack(JSContext* cx, const Telemetry::HangStack& stack)
+{
+ JS::RootedObject ret(cx, JS_NewArrayObject(cx, stack.length()));
+ if (!ret) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < stack.length(); i++) {
+ JS::RootedString string(cx, JS_NewStringCopyZ(cx, stack[i]));
+ if (!JS_DefineElement(cx, ret, i, string, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+ return ret;
+}
+
+static void
+CreateJSHangAnnotations(JSContext* cx, const HangAnnotationsVector& annotations,
+ JS::MutableHandleObject returnedObject)
+{
+ JS::RootedObject annotationsArray(cx, JS_NewArrayObject(cx, 0));
+ if (!annotationsArray) {
+ returnedObject.set(nullptr);
+ return;
+ }
+ // We keep track of the annotations we reported in this hash set, so we can
+ // discard duplicated ones.
+ nsTHashtable<nsStringHashKey> reportedAnnotations;
+ size_t annotationIndex = 0;
+ for (const HangAnnotationsPtr *i = annotations.begin(), *e = annotations.end();
+ i != e; ++i) {
+ JS::RootedObject jsAnnotation(cx, JS_NewPlainObject(cx));
+ if (!jsAnnotation) {
+ continue;
+ }
+ const HangAnnotationsPtr& curAnnotations = *i;
+ // Build a key to index the current annotations in our hash set.
+ nsAutoString annotationsKey;
+ nsresult rv = ComputeAnnotationsKey(curAnnotations, annotationsKey);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ // Check if the annotations are in the set. If that's the case, don't double report.
+ if (reportedAnnotations.GetEntry(annotationsKey)) {
+ continue;
+ }
+ // If not, report them.
+ reportedAnnotations.PutEntry(annotationsKey);
+ UniquePtr<HangAnnotations::Enumerator> annotationsEnum =
+ curAnnotations->GetEnumerator();
+ if (!annotationsEnum) {
+ continue;
+ }
+ nsAutoString key;
+ nsAutoString value;
+ while (annotationsEnum->Next(key, value)) {
+ JS::RootedValue jsValue(cx);
+ jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length()));
+ if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(),
+ jsValue, JSPROP_ENUMERATE)) {
+ returnedObject.set(nullptr);
+ return;
+ }
+ }
+ if (!JS_SetElement(cx, annotationsArray, annotationIndex, jsAnnotation)) {
+ continue;
+ }
+ ++annotationIndex;
+ }
+ // Return the array using a |MutableHandleObject| to avoid triggering a false
+ // positive rooting issue in the hazard analysis build.
+ returnedObject.set(annotationsArray);
+}
+
+static JSObject*
+CreateJSHangHistogram(JSContext* cx, const Telemetry::HangHistogram& hang)
+{
+ JS::RootedObject ret(cx, JS_NewPlainObject(cx));
+ if (!ret) {
+ return nullptr;
+ }
+
+ JS::RootedObject stack(cx, CreateJSHangStack(cx, hang.GetStack()));
+ JS::RootedObject time(cx, CreateJSTimeHistogram(cx, hang));
+ auto& hangAnnotations = hang.GetAnnotations();
+ JS::RootedObject annotations(cx);
+ CreateJSHangAnnotations(cx, hangAnnotations, &annotations);
+
+ if (!stack ||
+ !time ||
+ !annotations ||
+ !JS_DefineProperty(cx, ret, "stack", stack, JSPROP_ENUMERATE) ||
+ !JS_DefineProperty(cx, ret, "histogram", time, JSPROP_ENUMERATE) ||
+ (!hangAnnotations.empty() && // <-- Only define annotations when nonempty
+ !JS_DefineProperty(cx, ret, "annotations", annotations, JSPROP_ENUMERATE))) {
+ return nullptr;
+ }
+
+ if (!hang.GetNativeStack().empty()) {
+ JS::RootedObject native(cx, CreateJSHangStack(cx, hang.GetNativeStack()));
+ if (!native ||
+ !JS_DefineProperty(cx, ret, "nativeStack", native, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+ return ret;
+}
+
+static JSObject*
+CreateJSThreadHangStats(JSContext* cx, const Telemetry::ThreadHangStats& thread)
+{
+ JS::RootedObject ret(cx, JS_NewPlainObject(cx));
+ if (!ret) {
+ return nullptr;
+ }
+ JS::RootedString name(cx, JS_NewStringCopyZ(cx, thread.GetName()));
+ if (!name ||
+ !JS_DefineProperty(cx, ret, "name", name, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ JS::RootedObject activity(cx, CreateJSTimeHistogram(cx, thread.mActivity));
+ if (!activity ||
+ !JS_DefineProperty(cx, ret, "activity", activity, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ JS::RootedObject hangs(cx, JS_NewArrayObject(cx, 0));
+ if (!hangs) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < thread.mHangs.length(); i++) {
+ JS::RootedObject obj(cx, CreateJSHangHistogram(cx, thread.mHangs[i]));
+ if (!JS_DefineElement(cx, hangs, i, obj, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+ if (!JS_DefineProperty(cx, ret, "hangs", hangs, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ return ret;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetThreadHangStats(JSContext* cx, JS::MutableHandle<JS::Value> ret)
+{
+ JS::RootedObject retObj(cx, JS_NewArrayObject(cx, 0));
+ if (!retObj) {
+ return NS_ERROR_FAILURE;
+ }
+ size_t threadIndex = 0;
+
+ if (!BackgroundHangMonitor::IsDisabled()) {
+ /* First add active threads; we need to hold |iter| (and its lock)
+ throughout this method to avoid a race condition where a thread can
+ be recorded twice if the thread is destroyed while this method is
+ running */
+ BackgroundHangMonitor::ThreadHangStatsIterator iter;
+ for (Telemetry::ThreadHangStats* histogram = iter.GetNext();
+ histogram; histogram = iter.GetNext()) {
+ JS::RootedObject obj(cx, CreateJSThreadHangStats(cx, *histogram));
+ if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ // Add saved threads next
+ MutexAutoLock autoLock(mThreadHangStatsMutex);
+ for (size_t i = 0; i < mThreadHangStats.length(); i++) {
+ JS::RootedObject obj(cx,
+ CreateJSThreadHangStats(cx, mThreadHangStats[i]));
+ if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ ret.setObject(*retObj);
+ return NS_OK;
+}
+
+void
+TelemetryImpl::ReadLateWritesStacks(nsIFile* aProfileDir)
+{
+ nsAutoCString nativePath;
+ nsresult rv = aProfileDir->GetNativePath(nativePath);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ const char *name = nativePath.get();
+ PRDir *dir = PR_OpenDir(name);
+ if (!dir) {
+ return;
+ }
+
+ PRDirEntry *ent;
+ const char *prefix = "Telemetry.LateWriteFinal-";
+ unsigned int prefixLen = strlen(prefix);
+ while ((ent = PR_ReadDir(dir, PR_SKIP_NONE))) {
+ if (strncmp(prefix, ent->name, prefixLen) != 0) {
+ continue;
+ }
+
+ nsAutoCString stackNativePath = nativePath;
+ stackNativePath += XPCOM_FILE_PATH_SEPARATOR;
+ stackNativePath += nsDependentCString(ent->name);
+
+ Telemetry::ProcessedStack stack;
+ ReadStack(stackNativePath.get(), stack);
+ if (stack.GetStackSize() != 0) {
+ mLateWritesStacks.AddStack(stack);
+ }
+ // Delete the file so that we don't report it again on the next run.
+ PR_Delete(stackNativePath.get());
+ }
+ PR_CloseDir(dir);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetLateWrites(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ // The user must call AsyncReadTelemetryData first. We return an empty list
+ // instead of reporting a failure so that the rest of telemetry can uniformly
+ // handle the read not being available yet.
+
+ // FIXME: we allocate the js object again and again in the getter. We should
+ // figure out a way to cache it. In order to do that we have to call
+ // JS_AddNamedObjectRoot. A natural place to do so is in the TelemetryImpl
+ // constructor, but it is not clear how to get a JSContext in there.
+ // Another option would be to call it in here when we first call
+ // CreateJSStackObject, but we would still need to figure out where to call
+ // JS_RemoveObjectRoot. Would it be ok to never call JS_RemoveObjectRoot
+ // and just set the pointer to nullptr is the telemetry destructor?
+
+ JSObject *report;
+ if (!mCachedTelemetryData) {
+ CombinedStacks empty;
+ report = CreateJSStackObject(cx, empty);
+ } else {
+ report = CreateJSStackObject(cx, mLateWritesStacks);
+ }
+
+ if (report == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ret.setObject(*report);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisteredHistograms(uint32_t aDataset, uint32_t *aCount,
+ char*** aHistograms)
+{
+ return
+ TelemetryHistogram::RegisteredHistograms(aDataset, aCount, aHistograms);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisteredKeyedHistograms(uint32_t aDataset, uint32_t *aCount,
+ char*** aHistograms)
+{
+ return
+ TelemetryHistogram::RegisteredKeyedHistograms(aDataset, aCount,
+ aHistograms);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetHistogramById(const nsACString &name, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ return TelemetryHistogram::GetHistogramById(name, cx, ret);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetKeyedHistogramById(const nsACString &name, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ return TelemetryHistogram::GetKeyedHistogramById(name, cx, ret);
+}
+
+/**
+ * Indicates if Telemetry can record base data (FHR data). This is true if the
+ * FHR data reporting service or self-support are enabled.
+ *
+ * In the unlikely event that adding a new base probe is needed, please check the data
+ * collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection and talk to the
+ * Telemetry team.
+ */
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordBase(bool *ret) {
+ *ret = TelemetryHistogram::CanRecordBase();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetCanRecordBase(bool canRecord) {
+ TelemetryHistogram::SetCanRecordBase(canRecord);
+ TelemetryScalar::SetCanRecordBase(canRecord);
+ TelemetryEvent::SetCanRecordBase(canRecord);
+ return NS_OK;
+}
+
+/**
+ * Indicates if Telemetry is allowed to record extended data. Returns false if the user
+ * hasn't opted into "extended Telemetry" on the Release channel, when the user has
+ * explicitly opted out of Telemetry on Nightly/Aurora/Beta or if manually set to false
+ * during tests.
+ * If the returned value is false, gathering of extended telemetry statistics is disabled.
+ */
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordExtended(bool *ret) {
+ *ret = TelemetryHistogram::CanRecordExtended();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetCanRecordExtended(bool canRecord) {
+ TelemetryHistogram::SetCanRecordExtended(canRecord);
+ TelemetryScalar::SetCanRecordExtended(canRecord);
+ TelemetryEvent::SetCanRecordExtended(canRecord);
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+TelemetryImpl::GetIsOfficialTelemetry(bool *ret) {
+#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && !defined(DEBUG)
+ *ret = true;
+#else
+ *ret = false;
+#endif
+ return NS_OK;
+}
+
+already_AddRefed<nsITelemetry>
+TelemetryImpl::CreateTelemetryInstance()
+{
+ MOZ_ASSERT(sTelemetry == nullptr, "CreateTelemetryInstance may only be called once, via GetService()");
+
+ bool useTelemetry = false;
+ if (XRE_IsParentProcess() ||
+ XRE_IsContentProcess() ||
+ XRE_IsGPUProcess())
+ {
+ useTelemetry = true;
+ }
+
+ // First, initialize the TelemetryHistogram and TelemetryScalar global states.
+ TelemetryHistogram::InitializeGlobalState(useTelemetry, useTelemetry);
+
+ // Only record scalars from the parent process.
+ TelemetryScalar::InitializeGlobalState(XRE_IsParentProcess(), XRE_IsParentProcess());
+
+ // Only record events from the parent process.
+ TelemetryEvent::InitializeGlobalState(XRE_IsParentProcess(), XRE_IsParentProcess());
+
+ // Now, create and initialize the Telemetry global state.
+ sTelemetry = new TelemetryImpl();
+
+ // AddRef for the local reference
+ NS_ADDREF(sTelemetry);
+ // AddRef for the caller
+ nsCOMPtr<nsITelemetry> ret = sTelemetry;
+
+ sTelemetry->InitMemoryReporter();
+ InitHistogramRecordingEnabled(); // requires sTelemetry to exist
+
+ return ret.forget();
+}
+
+void
+TelemetryImpl::ShutdownTelemetry()
+{
+ // No point in collecting IO beyond this point
+ ClearIOReporting();
+ NS_IF_RELEASE(sTelemetry);
+
+ // Lastly, de-initialise the TelemetryHistogram and TelemetryScalar global states,
+ // so as to release any heap storage that would otherwise be kept alive by it.
+ TelemetryHistogram::DeInitializeGlobalState();
+ TelemetryScalar::DeInitializeGlobalState();
+ TelemetryEvent::DeInitializeGlobalState();
+}
+
+void
+TelemetryImpl::StoreSlowSQL(const nsACString &sql, uint32_t delay,
+ SanitizedState state)
+{
+ AutoHashtable<SlowSQLEntryType>* slowSQLMap = nullptr;
+ if (state == Sanitized)
+ slowSQLMap = &(sTelemetry->mSanitizedSQL);
+ else
+ slowSQLMap = &(sTelemetry->mPrivateSQL);
+
+ MutexAutoLock hashMutex(sTelemetry->mHashMutex);
+
+ SlowSQLEntryType *entry = slowSQLMap->GetEntry(sql);
+ if (!entry) {
+ entry = slowSQLMap->PutEntry(sql);
+ if (MOZ_UNLIKELY(!entry))
+ return;
+ entry->mData.mainThread.hitCount = 0;
+ entry->mData.mainThread.totalTime = 0;
+ entry->mData.otherThreads.hitCount = 0;
+ entry->mData.otherThreads.totalTime = 0;
+ }
+
+ if (NS_IsMainThread()) {
+ entry->mData.mainThread.hitCount++;
+ entry->mData.mainThread.totalTime += delay;
+ } else {
+ entry->mData.otherThreads.hitCount++;
+ entry->mData.otherThreads.totalTime += delay;
+ }
+}
+
+/**
+ * This method replaces string literals in SQL strings with the word :private
+ *
+ * States used in this state machine:
+ *
+ * NORMAL:
+ * - This is the active state when not iterating over a string literal or
+ * comment
+ *
+ * SINGLE_QUOTE:
+ * - Defined here: http://www.sqlite.org/lang_expr.html
+ * - This state represents iterating over a string literal opened with
+ * a single quote.
+ * - A single quote within the string can be encoded by putting 2 single quotes
+ * in a row, e.g. 'This literal contains an escaped quote '''
+ * - Any double quotes found within a single-quoted literal are ignored
+ * - This state covers BLOB literals, e.g. X'ABC123'
+ * - The string literal and the enclosing quotes will be replaced with
+ * the text :private
+ *
+ * DOUBLE_QUOTE:
+ * - Same rules as the SINGLE_QUOTE state.
+ * - According to http://www.sqlite.org/lang_keywords.html,
+ * SQLite interprets text in double quotes as an identifier unless it's used in
+ * a context where it cannot be resolved to an identifier and a string literal
+ * is allowed. This method removes text in double-quotes for safety.
+ *
+ * DASH_COMMENT:
+ * - http://www.sqlite.org/lang_comment.html
+ * - A dash comment starts with two dashes in a row,
+ * e.g. DROP TABLE foo -- a comment
+ * - Any text following two dashes in a row is interpreted as a comment until
+ * end of input or a newline character
+ * - Any quotes found within the comment are ignored and no replacements made
+ *
+ * C_STYLE_COMMENT:
+ * - http://www.sqlite.org/lang_comment.html
+ * - A C-style comment starts with a forward slash and an asterisk, and ends
+ * with an asterisk and a forward slash
+ * - Any text following comment start is interpreted as a comment up to end of
+ * input or comment end
+ * - Any quotes found within the comment are ignored and no replacements made
+ */
+nsCString
+TelemetryImpl::SanitizeSQL(const nsACString &sql) {
+ nsCString output;
+ int length = sql.Length();
+
+ typedef enum {
+ NORMAL,
+ SINGLE_QUOTE,
+ DOUBLE_QUOTE,
+ DASH_COMMENT,
+ C_STYLE_COMMENT,
+ } State;
+
+ State state = NORMAL;
+ int fragmentStart = 0;
+ for (int i = 0; i < length; i++) {
+ char character = sql[i];
+ char nextCharacter = (i + 1 < length) ? sql[i + 1] : '\0';
+
+ switch (character) {
+ case '\'':
+ case '"':
+ if (state == NORMAL) {
+ state = (character == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE;
+ output += nsDependentCSubstring(sql, fragmentStart, i - fragmentStart);
+ output += ":private";
+ fragmentStart = -1;
+ } else if ((state == SINGLE_QUOTE && character == '\'') ||
+ (state == DOUBLE_QUOTE && character == '"')) {
+ if (nextCharacter == character) {
+ // Two consecutive quotes within a string literal are a single escaped quote
+ i++;
+ } else {
+ state = NORMAL;
+ fragmentStart = i + 1;
+ }
+ }
+ break;
+ case '-':
+ if (state == NORMAL) {
+ if (nextCharacter == '-') {
+ state = DASH_COMMENT;
+ i++;
+ }
+ }
+ break;
+ case '\n':
+ if (state == DASH_COMMENT) {
+ state = NORMAL;
+ }
+ break;
+ case '/':
+ if (state == NORMAL) {
+ if (nextCharacter == '*') {
+ state = C_STYLE_COMMENT;
+ i++;
+ }
+ }
+ break;
+ case '*':
+ if (state == C_STYLE_COMMENT) {
+ if (nextCharacter == '/') {
+ state = NORMAL;
+ }
+ }
+ break;
+ default:
+ continue;
+ }
+ }
+
+ if ((fragmentStart >= 0) && fragmentStart < length)
+ output += nsDependentCSubstring(sql, fragmentStart, length - fragmentStart);
+
+ return output;
+}
+
+// A whitelist mechanism to prevent Telemetry reporting on Addon & Thunderbird
+// DBs.
+struct TrackedDBEntry
+{
+ const char* mName;
+ const uint32_t mNameLength;
+
+ // This struct isn't meant to be used beyond the static arrays below.
+ constexpr
+ TrackedDBEntry(const char* aName, uint32_t aNameLength)
+ : mName(aName)
+ , mNameLength(aNameLength)
+ { }
+
+ TrackedDBEntry() = delete;
+ TrackedDBEntry(TrackedDBEntry&) = delete;
+};
+
+#define TRACKEDDB_ENTRY(_name) { _name, (sizeof(_name) - 1) }
+
+// A whitelist of database names. If the database name exactly matches one of
+// these then its SQL statements will always be recorded.
+static constexpr TrackedDBEntry kTrackedDBs[] = {
+ // IndexedDB for about:home, see aboutHome.js
+ TRACKEDDB_ENTRY("818200132aebmoouht.sqlite"),
+ TRACKEDDB_ENTRY("addons.sqlite"),
+ TRACKEDDB_ENTRY("content-prefs.sqlite"),
+ TRACKEDDB_ENTRY("cookies.sqlite"),
+ TRACKEDDB_ENTRY("downloads.sqlite"),
+ TRACKEDDB_ENTRY("extensions.sqlite"),
+ TRACKEDDB_ENTRY("formhistory.sqlite"),
+ TRACKEDDB_ENTRY("index.sqlite"),
+ TRACKEDDB_ENTRY("netpredictions.sqlite"),
+ TRACKEDDB_ENTRY("permissions.sqlite"),
+ TRACKEDDB_ENTRY("places.sqlite"),
+ TRACKEDDB_ENTRY("reading-list.sqlite"),
+ TRACKEDDB_ENTRY("search.sqlite"),
+ TRACKEDDB_ENTRY("signons.sqlite"),
+ TRACKEDDB_ENTRY("urlclassifier3.sqlite"),
+ TRACKEDDB_ENTRY("webappsstore.sqlite")
+};
+
+// A whitelist of database name prefixes. If the database name begins with
+// one of these prefixes then its SQL statements will always be recorded.
+static const TrackedDBEntry kTrackedDBPrefixes[] = {
+ TRACKEDDB_ENTRY("indexedDB-")
+};
+
+#undef TRACKEDDB_ENTRY
+
+// Slow SQL statements will be automatically
+// trimmed to kMaxSlowStatementLength characters.
+// This limit doesn't include the ellipsis and DB name,
+// that are appended at the end of the stored statement.
+const uint32_t kMaxSlowStatementLength = 1000;
+
+void
+TelemetryImpl::RecordSlowStatement(const nsACString &sql,
+ const nsACString &dbName,
+ uint32_t delay)
+{
+ MOZ_ASSERT(!sql.IsEmpty());
+ MOZ_ASSERT(!dbName.IsEmpty());
+
+ if (!sTelemetry || !TelemetryHistogram::CanRecordExtended())
+ return;
+
+ bool recordStatement = false;
+
+ for (const TrackedDBEntry& nameEntry : kTrackedDBs) {
+ MOZ_ASSERT(nameEntry.mNameLength);
+ const nsDependentCString name(nameEntry.mName, nameEntry.mNameLength);
+ if (dbName == name) {
+ recordStatement = true;
+ break;
+ }
+ }
+
+ if (!recordStatement) {
+ for (const TrackedDBEntry& prefixEntry : kTrackedDBPrefixes) {
+ MOZ_ASSERT(prefixEntry.mNameLength);
+ const nsDependentCString prefix(prefixEntry.mName,
+ prefixEntry.mNameLength);
+ if (StringBeginsWith(dbName, prefix)) {
+ recordStatement = true;
+ break;
+ }
+ }
+ }
+
+ if (recordStatement) {
+ nsAutoCString sanitizedSQL(SanitizeSQL(sql));
+ if (sanitizedSQL.Length() > kMaxSlowStatementLength) {
+ sanitizedSQL.SetLength(kMaxSlowStatementLength);
+ sanitizedSQL += "...";
+ }
+ sanitizedSQL.AppendPrintf(" /* %s */", nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(sanitizedSQL, delay, Sanitized);
+ } else {
+ // Report aggregate DB-level statistics for addon DBs
+ nsAutoCString aggregate;
+ aggregate.AppendPrintf("Untracked SQL for %s",
+ nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(aggregate, delay, Sanitized);
+ }
+
+ nsAutoCString fullSQL;
+ fullSQL.AppendPrintf("%s /* %s */",
+ nsPromiseFlatCString(sql).get(),
+ nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(fullSQL, delay, Unsanitized);
+}
+
+void
+TelemetryImpl::RecordIceCandidates(const uint32_t iceCandidateBitmask,
+ const bool success)
+{
+ if (!sTelemetry || !TelemetryHistogram::CanRecordExtended())
+ return;
+
+ sTelemetry->mWebrtcTelemetry.RecordIceCandidateMask(iceCandidateBitmask, success);
+}
+
+#if defined(MOZ_ENABLE_PROFILER_SPS)
+void
+TelemetryImpl::RecordChromeHang(uint32_t aDuration,
+ Telemetry::ProcessedStack &aStack,
+ int32_t aSystemUptime,
+ int32_t aFirefoxUptime,
+ HangAnnotationsPtr aAnnotations)
+{
+ if (!sTelemetry || !TelemetryHistogram::CanRecordExtended())
+ return;
+
+ HangAnnotationsPtr annotations;
+ // We only pass aAnnotations if it is not empty.
+ if (aAnnotations && !aAnnotations->IsEmpty()) {
+ annotations = Move(aAnnotations);
+ }
+
+ MutexAutoLock hangReportMutex(sTelemetry->mHangReportsMutex);
+
+ sTelemetry->mHangReports.AddHang(aStack, aDuration,
+ aSystemUptime, aFirefoxUptime,
+ Move(annotations));
+}
+#endif
+
+void
+TelemetryImpl::RecordThreadHangStats(Telemetry::ThreadHangStats& aStats)
+{
+ if (!sTelemetry || !TelemetryHistogram::CanRecordExtended())
+ return;
+
+ MutexAutoLock autoLock(sTelemetry->mThreadHangStatsMutex);
+
+ // Ignore OOM.
+ mozilla::Unused << sTelemetry->mThreadHangStats.append(Move(aStats));
+}
+
+NS_IMPL_ISUPPORTS(TelemetryImpl, nsITelemetry, nsIMemoryReporter)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsITelemetry, TelemetryImpl::CreateTelemetryInstance)
+
+#define NS_TELEMETRY_CID \
+ {0xaea477f2, 0xb3a2, 0x469c, {0xaa, 0x29, 0x0a, 0x82, 0xd1, 0x32, 0xb8, 0x29}}
+NS_DEFINE_NAMED_CID(NS_TELEMETRY_CID);
+
+const Module::CIDEntry kTelemetryCIDs[] = {
+ { &kNS_TELEMETRY_CID, false, nullptr, nsITelemetryConstructor, Module::ALLOW_IN_GPU_PROCESS },
+ { nullptr }
+};
+
+const Module::ContractIDEntry kTelemetryContracts[] = {
+ { "@mozilla.org/base/telemetry;1", &kNS_TELEMETRY_CID, Module::ALLOW_IN_GPU_PROCESS },
+ { nullptr }
+};
+
+const Module kTelemetryModule = {
+ Module::kVersion,
+ kTelemetryCIDs,
+ kTelemetryContracts,
+ nullptr,
+ nullptr,
+ nullptr,
+ TelemetryImpl::ShutdownTelemetry,
+ Module::ALLOW_IN_GPU_PROCESS
+};
+
+NS_IMETHODIMP
+TelemetryImpl::GetFileIOReports(JSContext *cx, JS::MutableHandleValue ret)
+{
+ if (sTelemetryIOObserver) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!sTelemetryIOObserver->ReflectIntoJS(cx, obj)) {
+ return NS_ERROR_FAILURE;
+ }
+ ret.setObject(*obj);
+ return NS_OK;
+ }
+ ret.setNull();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStart(double* aResult)
+{
+ return Telemetry::Common::MsSinceProcessStart(aResult);
+}
+
+// Telemetry Scalars IDL Implementation
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarAdd(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
+{
+ return TelemetryScalar::Add(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarSet(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
+{
+ return TelemetryScalar::Set(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarSetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
+{
+ return TelemetryScalar::SetMaximum(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandleValue aResult)
+{
+ return TelemetryScalar::CreateSnapshots(aDataset, aClearScalars, aCx, optional_argc, aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey,
+ JS::HandleValue aVal, JSContext* aCx)
+{
+ return TelemetryScalar::Add(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey,
+ JS::HandleValue aVal, JSContext* aCx)
+{
+ return TelemetryScalar::Set(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName, const nsAString& aKey,
+ JS::HandleValue aVal, JSContext* aCx)
+{
+ return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotKeyedScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandleValue aResult)
+{
+ return TelemetryScalar::CreateKeyedSnapshots(aDataset, aClearScalars, aCx, optional_argc,
+ aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ClearScalars()
+{
+ TelemetryScalar::ClearScalars();
+ return NS_OK;
+}
+
+// Telemetry Event IDL implementation.
+
+NS_IMETHODIMP
+TelemetryImpl::RecordEvent(const nsACString & aCategory, const nsACString & aMethod,
+ const nsACString & aObject, JS::HandleValue aValue,
+ JS::HandleValue aExtra, JSContext* aCx, uint8_t optional_argc)
+{
+ return TelemetryEvent::RecordEvent(aCategory, aMethod, aObject, aValue, aExtra, aCx, optional_argc);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotBuiltinEvents(uint32_t aDataset, bool aClear, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandleValue aResult)
+{
+ return TelemetryEvent::CreateSnapshots(aDataset, aClear, aCx, optional_argc, aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ClearEvents()
+{
+ TelemetryEvent::ClearEvents();
+ return NS_OK;
+}
+
+
+NS_IMETHODIMP
+TelemetryImpl::FlushBatchedChildTelemetry()
+{
+ TelemetryHistogram::IPCTimerFired(nullptr, nullptr);
+ return NS_OK;
+}
+
+size_t
+TelemetryImpl::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ size_t n = aMallocSizeOf(this);
+
+ // Ignore the hashtables in mAddonMap; they are not significant.
+ n += TelemetryHistogram::GetMapShallowSizesOfExcludingThis(aMallocSizeOf);
+ n += TelemetryScalar::GetMapShallowSizesOfExcludingThis(aMallocSizeOf);
+ n += mWebrtcTelemetry.SizeOfExcludingThis(aMallocSizeOf);
+ { // Scope for mHashMutex lock
+ MutexAutoLock lock(mHashMutex);
+ n += mPrivateSQL.SizeOfExcludingThis(aMallocSizeOf);
+ n += mSanitizedSQL.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ { // Scope for mHangReportsMutex lock
+ MutexAutoLock lock(mHangReportsMutex);
+ n += mHangReports.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ { // Scope for mThreadHangStatsMutex lock
+ MutexAutoLock lock(mThreadHangStatsMutex);
+ n += mThreadHangStats.sizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ // It's a bit gross that we measure this other stuff that lives outside of
+ // TelemetryImpl... oh well.
+ if (sTelemetryIOObserver) {
+ n += sTelemetryIOObserver->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ n += TelemetryHistogram::GetHistogramSizesofIncludingThis(aMallocSizeOf);
+ n += TelemetryScalar::GetScalarSizesOfIncludingThis(aMallocSizeOf);
+ n += TelemetryEvent::SizeOfIncludingThis(aMallocSizeOf);
+
+ return n;
+}
+
+struct StackFrame
+{
+ uintptr_t mPC; // The program counter at this position in the call stack.
+ uint16_t mIndex; // The number of this frame in the call stack.
+ uint16_t mModIndex; // The index of module that has this program counter.
+};
+
+#ifdef MOZ_ENABLE_PROFILER_SPS
+static bool CompareByPC(const StackFrame &a, const StackFrame &b)
+{
+ return a.mPC < b.mPC;
+}
+
+static bool CompareByIndex(const StackFrame &a, const StackFrame &b)
+{
+ return a.mIndex < b.mIndex;
+}
+#endif
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in no name space
+// These are NOT listed in Telemetry.h
+
+NSMODULE_DEFN(nsTelemetryModule) = &kTelemetryModule;
+
+/**
+ * The XRE_TelemetryAdd function is to be used by embedding applications
+ * that can't use mozilla::Telemetry::Accumulate() directly.
+ */
+void
+XRE_TelemetryAccumulate(int aID, uint32_t aSample)
+{
+ mozilla::Telemetry::Accumulate((mozilla::Telemetry::ID) aID, aSample);
+}
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::
+// These are NOT listed in Telemetry.h
+
+namespace mozilla {
+
+void
+RecordShutdownStartTimeStamp() {
+#ifdef DEBUG
+ // FIXME: this function should only be called once, since it should be called
+ // at the earliest point we *know* we are shutting down. Unfortunately
+ // this assert has been firing. Given that if we are called multiple times
+ // we just keep the last timestamp, the assert is commented for now.
+ static bool recorded = false;
+ // MOZ_ASSERT(!recorded);
+ (void)recorded; // Silence unused-var warnings (remove when assert re-enabled)
+ recorded = true;
+#endif
+
+ if (!Telemetry::CanRecordExtended())
+ return;
+
+ gRecordedShutdownStartTime = TimeStamp::Now();
+
+ GetShutdownTimeFileName();
+}
+
+void
+RecordShutdownEndTimeStamp() {
+ if (!gRecordedShutdownTimeFileName || gAlreadyFreedShutdownTimeFileName)
+ return;
+
+ nsCString name(gRecordedShutdownTimeFileName);
+ PL_strfree(gRecordedShutdownTimeFileName);
+ gRecordedShutdownTimeFileName = nullptr;
+ gAlreadyFreedShutdownTimeFileName = true;
+
+ if (gRecordedShutdownStartTime.IsNull()) {
+ // If |CanRecordExtended()| is true before |AsyncFetchTelemetryData| is called and
+ // then disabled before shutdown, |RecordShutdownStartTimeStamp| will bail out and
+ // we will end up with a null |gRecordedShutdownStartTime| here. This can happen
+ // during tests.
+ return;
+ }
+
+ nsCString tmpName = name;
+ tmpName += ".tmp";
+ FILE *f = fopen(tmpName.get(), "w");
+ if (!f)
+ return;
+ // On a normal release build this should be called just before
+ // calling _exit, but on a debug build or when the user forces a full
+ // shutdown this is called as late as possible, so we have to
+ // white list this write as write poisoning will be enabled.
+ MozillaRegisterDebugFILE(f);
+
+ TimeStamp now = TimeStamp::Now();
+ MOZ_ASSERT(now >= gRecordedShutdownStartTime);
+ TimeDuration diff = now - gRecordedShutdownStartTime;
+ uint32_t diff2 = diff.ToMilliseconds();
+ int written = fprintf(f, "%d\n", diff2);
+ MozillaUnRegisterDebugFILE(f);
+ int rv = fclose(f);
+ if (written < 0 || rv != 0) {
+ PR_Delete(tmpName.get());
+ return;
+ }
+ PR_Delete(name.get());
+ PR_Rename(tmpName.get(), name.get());
+}
+
+} // namespace mozilla
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry::
+// These are NOT listed in Telemetry.h
+
+namespace mozilla {
+namespace Telemetry {
+
+ProcessedStack::ProcessedStack()
+{
+}
+
+size_t ProcessedStack::GetStackSize() const
+{
+ return mStack.size();
+}
+
+size_t ProcessedStack::GetNumModules() const
+{
+ return mModules.size();
+}
+
+bool ProcessedStack::Module::operator==(const Module& aOther) const {
+ return mName == aOther.mName &&
+ mBreakpadId == aOther.mBreakpadId;
+}
+
+const ProcessedStack::Frame &ProcessedStack::GetFrame(unsigned aIndex) const
+{
+ MOZ_ASSERT(aIndex < mStack.size());
+ return mStack[aIndex];
+}
+
+void ProcessedStack::AddFrame(const Frame &aFrame)
+{
+ mStack.push_back(aFrame);
+}
+
+const ProcessedStack::Module &ProcessedStack::GetModule(unsigned aIndex) const
+{
+ MOZ_ASSERT(aIndex < mModules.size());
+ return mModules[aIndex];
+}
+
+void ProcessedStack::AddModule(const Module &aModule)
+{
+ mModules.push_back(aModule);
+}
+
+void ProcessedStack::Clear() {
+ mModules.clear();
+ mStack.clear();
+}
+
+ProcessedStack
+GetStackAndModules(const std::vector<uintptr_t>& aPCs)
+{
+ std::vector<StackFrame> rawStack;
+ auto stackEnd = aPCs.begin() + std::min(aPCs.size(), kMaxChromeStackDepth);
+ for (auto i = aPCs.begin(); i != stackEnd; ++i) {
+ uintptr_t aPC = *i;
+ StackFrame Frame = {aPC, static_cast<uint16_t>(rawStack.size()),
+ std::numeric_limits<uint16_t>::max()};
+ rawStack.push_back(Frame);
+ }
+
+#ifdef MOZ_ENABLE_PROFILER_SPS
+ // Remove all modules not referenced by a PC on the stack
+ std::sort(rawStack.begin(), rawStack.end(), CompareByPC);
+
+ size_t moduleIndex = 0;
+ size_t stackIndex = 0;
+ size_t stackSize = rawStack.size();
+
+ SharedLibraryInfo rawModules = SharedLibraryInfo::GetInfoForSelf();
+ rawModules.SortByAddress();
+
+ while (moduleIndex < rawModules.GetSize()) {
+ const SharedLibrary& module = rawModules.GetEntry(moduleIndex);
+ uintptr_t moduleStart = module.GetStart();
+ uintptr_t moduleEnd = module.GetEnd() - 1;
+ // the interval is [moduleStart, moduleEnd)
+
+ bool moduleReferenced = false;
+ for (;stackIndex < stackSize; ++stackIndex) {
+ uintptr_t pc = rawStack[stackIndex].mPC;
+ if (pc >= moduleEnd)
+ break;
+
+ if (pc >= moduleStart) {
+ // If the current PC is within the current module, mark
+ // module as used
+ moduleReferenced = true;
+ rawStack[stackIndex].mPC -= moduleStart;
+ rawStack[stackIndex].mModIndex = moduleIndex;
+ } else {
+ // PC does not belong to any module. It is probably from
+ // the JIT. Use a fixed mPC so that we don't get different
+ // stacks on different runs.
+ rawStack[stackIndex].mPC =
+ std::numeric_limits<uintptr_t>::max();
+ }
+ }
+
+ if (moduleReferenced) {
+ ++moduleIndex;
+ } else {
+ // Remove module if no PCs within its address range
+ rawModules.RemoveEntries(moduleIndex, moduleIndex + 1);
+ }
+ }
+
+ for (;stackIndex < stackSize; ++stackIndex) {
+ // These PCs are past the last module.
+ rawStack[stackIndex].mPC = std::numeric_limits<uintptr_t>::max();
+ }
+
+ std::sort(rawStack.begin(), rawStack.end(), CompareByIndex);
+#endif
+
+ // Copy the information to the return value.
+ ProcessedStack Ret;
+ for (std::vector<StackFrame>::iterator i = rawStack.begin(),
+ e = rawStack.end(); i != e; ++i) {
+ const StackFrame &rawFrame = *i;
+ mozilla::Telemetry::ProcessedStack::Frame frame = { rawFrame.mPC, rawFrame.mModIndex };
+ Ret.AddFrame(frame);
+ }
+
+#ifdef MOZ_ENABLE_PROFILER_SPS
+ for (unsigned i = 0, n = rawModules.GetSize(); i != n; ++i) {
+ const SharedLibrary &info = rawModules.GetEntry(i);
+ const std::string &name = info.GetName();
+ std::string basename = name;
+#ifdef XP_MACOSX
+ // FIXME: We want to use just the basename as the libname, but the
+ // current profiler addon needs the full path name, so we compute the
+ // basename in here.
+ size_t pos = name.rfind('/');
+ if (pos != std::string::npos) {
+ basename = name.substr(pos + 1);
+ }
+#endif
+ mozilla::Telemetry::ProcessedStack::Module module = {
+ basename,
+ info.GetBreakpadId()
+ };
+ Ret.AddModule(module);
+ }
+#endif
+
+ return Ret;
+}
+
+void
+TimeHistogram::Add(PRIntervalTime aTime)
+{
+ uint32_t timeMs = PR_IntervalToMilliseconds(aTime);
+ size_t index = mozilla::FloorLog2(timeMs);
+ operator[](index)++;
+}
+
+const char*
+HangStack::InfallibleAppendViaBuffer(const char* aText, size_t aLength)
+{
+ MOZ_ASSERT(this->canAppendWithoutRealloc(1));
+ // Include null-terminator in length count.
+ MOZ_ASSERT(mBuffer.canAppendWithoutRealloc(aLength + 1));
+
+ const char* const entry = mBuffer.end();
+ mBuffer.infallibleAppend(aText, aLength);
+ mBuffer.infallibleAppend('\0'); // Explicitly append null-terminator
+ this->infallibleAppend(entry);
+ return entry;
+}
+
+const char*
+HangStack::AppendViaBuffer(const char* aText, size_t aLength)
+{
+ if (!this->reserve(this->length() + 1)) {
+ return nullptr;
+ }
+
+ // Keep track of the previous buffer in case we need to adjust pointers later.
+ const char* const prevStart = mBuffer.begin();
+ const char* const prevEnd = mBuffer.end();
+
+ // Include null-terminator in length count.
+ if (!mBuffer.reserve(mBuffer.length() + aLength + 1)) {
+ return nullptr;
+ }
+
+ if (prevStart != mBuffer.begin()) {
+ // The buffer has moved; we have to adjust pointers in the stack.
+ for (const char** entry = this->begin(); entry != this->end(); entry++) {
+ if (*entry >= prevStart && *entry < prevEnd) {
+ // Move from old buffer to new buffer.
+ *entry += mBuffer.begin() - prevStart;
+ }
+ }
+ }
+
+ return InfallibleAppendViaBuffer(aText, aLength);
+}
+
+uint32_t
+HangHistogram::GetHash(const HangStack& aStack)
+{
+ uint32_t hash = 0;
+ for (const char* const* label = aStack.begin();
+ label != aStack.end(); label++) {
+ /* If the string is within our buffer, we need to hash its content.
+ Otherwise, the string is statically allocated, and we only need
+ to hash the pointer instead of the content. */
+ if (aStack.IsInBuffer(*label)) {
+ hash = AddToHash(hash, HashString(*label));
+ } else {
+ hash = AddToHash(hash, *label);
+ }
+ }
+ return hash;
+}
+
+bool
+HangHistogram::operator==(const HangHistogram& aOther) const
+{
+ if (mHash != aOther.mHash) {
+ return false;
+ }
+ if (mStack.length() != aOther.mStack.length()) {
+ return false;
+ }
+ return mStack == aOther.mStack;
+}
+
+} // namespace Telemetry
+} // namespace mozilla
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry::
+// These are listed in Telemetry.h
+
+namespace mozilla {
+namespace Telemetry {
+
+// The external API for controlling recording state
+void
+SetHistogramRecordingEnabled(ID aID, bool aEnabled)
+{
+ TelemetryHistogram::SetHistogramRecordingEnabled(aID, aEnabled);
+}
+
+void
+Accumulate(ID aHistogram, uint32_t aSample)
+{
+ TelemetryHistogram::Accumulate(aHistogram, aSample);
+}
+
+void
+Accumulate(ID aID, const nsCString& aKey, uint32_t aSample)
+{
+ TelemetryHistogram::Accumulate(aID, aKey, aSample);
+}
+
+void
+Accumulate(const char* name, uint32_t sample)
+{
+ TelemetryHistogram::Accumulate(name, sample);
+}
+
+void
+Accumulate(const char *name, const nsCString& key, uint32_t sample)
+{
+ TelemetryHistogram::Accumulate(name, key, sample);
+}
+
+void
+AccumulateCategorical(ID id, const nsCString& label)
+{
+ TelemetryHistogram::AccumulateCategorical(id, label);
+}
+
+void
+AccumulateTimeDelta(ID aHistogram, TimeStamp start, TimeStamp end)
+{
+ Accumulate(aHistogram,
+ static_cast<uint32_t>((end - start).ToMilliseconds()));
+}
+
+void
+AccumulateChild(GeckoProcessType aProcessType,
+ const nsTArray<Accumulation>& aAccumulations)
+{
+ TelemetryHistogram::AccumulateChild(aProcessType, aAccumulations);
+}
+
+void
+AccumulateChildKeyed(GeckoProcessType aProcessType,
+ const nsTArray<KeyedAccumulation>& aAccumulations)
+{
+ TelemetryHistogram::AccumulateChildKeyed(aProcessType, aAccumulations);
+}
+
+const char*
+GetHistogramName(ID id)
+{
+ return TelemetryHistogram::GetHistogramName(id);
+}
+
+bool
+CanRecordBase()
+{
+ return TelemetryHistogram::CanRecordBase();
+}
+
+bool
+CanRecordExtended()
+{
+ return TelemetryHistogram::CanRecordExtended();
+}
+
+void
+RecordSlowSQLStatement(const nsACString &statement,
+ const nsACString &dbName,
+ uint32_t delay)
+{
+ TelemetryImpl::RecordSlowStatement(statement, dbName, delay);
+}
+
+void
+RecordWebrtcIceCandidates(const uint32_t iceCandidateBitmask,
+ const bool success)
+{
+ TelemetryImpl::RecordIceCandidates(iceCandidateBitmask, success);
+}
+
+void Init()
+{
+ // Make the service manager hold a long-lived reference to the service
+ nsCOMPtr<nsITelemetry> telemetryService =
+ do_GetService("@mozilla.org/base/telemetry;1");
+ MOZ_ASSERT(telemetryService);
+}
+
+#if defined(MOZ_ENABLE_PROFILER_SPS)
+void RecordChromeHang(uint32_t duration,
+ ProcessedStack &aStack,
+ int32_t aSystemUptime,
+ int32_t aFirefoxUptime,
+ HangAnnotationsPtr aAnnotations)
+{
+ TelemetryImpl::RecordChromeHang(duration, aStack,
+ aSystemUptime, aFirefoxUptime,
+ Move(aAnnotations));
+}
+#endif
+
+void RecordThreadHangStats(ThreadHangStats& aStats)
+{
+ TelemetryImpl::RecordThreadHangStats(aStats);
+}
+
+
+void
+WriteFailedProfileLock(nsIFile* aProfileDir)
+{
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = GetFailedProfileLockFile(getter_AddRefs(file), aProfileDir);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ int64_t fileSize = 0;
+ rv = file->GetFileSize(&fileSize);
+ // It's expected that the file might not exist yet
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ nsCOMPtr<nsIFileStream> fileStream;
+ rv = NS_NewLocalFileStream(getter_AddRefs(fileStream), file,
+ PR_RDWR | PR_CREATE_FILE, 0640);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ NS_ENSURE_TRUE_VOID(fileSize <= kMaxFailedProfileLockFileSize);
+ unsigned int failedLockCount = 0;
+ if (fileSize > 0) {
+ nsCOMPtr<nsIInputStream> inStream = do_QueryInterface(fileStream);
+ NS_ENSURE_TRUE_VOID(inStream);
+ if (!GetFailedLockCount(inStream, fileSize, failedLockCount)) {
+ failedLockCount = 0;
+ }
+ }
+ ++failedLockCount;
+ nsAutoCString bufStr;
+ bufStr.AppendInt(static_cast<int>(failedLockCount));
+ nsCOMPtr<nsISeekableStream> seekStream = do_QueryInterface(fileStream);
+ NS_ENSURE_TRUE_VOID(seekStream);
+ // If we read in an existing failed lock count, we need to reset the file ptr
+ if (fileSize > 0) {
+ rv = seekStream->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ nsCOMPtr<nsIOutputStream> outStream = do_QueryInterface(fileStream);
+ uint32_t bytesLeft = bufStr.Length();
+ const char* bytes = bufStr.get();
+ do {
+ uint32_t written = 0;
+ rv = outStream->Write(bytes, bytesLeft, &written);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ bytes += written;
+ bytesLeft -= written;
+ } while (bytesLeft > 0);
+ seekStream->SetEOF();
+}
+
+void
+InitIOReporting(nsIFile* aXreDir)
+{
+ // Never initialize twice
+ if (sTelemetryIOObserver) {
+ return;
+ }
+
+ sTelemetryIOObserver = new TelemetryIOInterposeObserver(aXreDir);
+ IOInterposer::Register(IOInterposeObserver::OpAllWithStaging,
+ sTelemetryIOObserver);
+}
+
+void
+SetProfileDir(nsIFile* aProfD)
+{
+ if (!sTelemetryIOObserver || !aProfD) {
+ return;
+ }
+ nsAutoString profDirPath;
+ nsresult rv = aProfD->GetPath(profDirPath);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ sTelemetryIOObserver->AddPath(profDirPath, NS_LITERAL_STRING("{profile}"));
+}
+
+void CreateStatisticsRecorder()
+{
+ TelemetryHistogram::CreateStatisticsRecorder();
+}
+
+void DestroyStatisticsRecorder()
+{
+ TelemetryHistogram::DestroyStatisticsRecorder();
+}
+
+// Scalar API C++ Endpoints
+
+void
+ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
+{
+ TelemetryScalar::Add(aId, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
+{
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, bool aVal)
+{
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal)
+{
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void
+ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
+{
+ TelemetryScalar::SetMaximum(aId, aVal);
+}
+
+void
+ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
+{
+ TelemetryScalar::Add(aId, aKey, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
+{
+ TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aVal)
+{
+ TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void
+ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
+{
+ TelemetryScalar::SetMaximum(aId, aKey, aVal);
+}
+
+} // namespace Telemetry
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/Telemetry.h b/toolkit/components/telemetry/Telemetry.h
new file mode 100644
index 0000000000..64f50013ab
--- /dev/null
+++ b/toolkit/components/telemetry/Telemetry.h
@@ -0,0 +1,436 @@
+/* -*- 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 Telemetry_h__
+#define Telemetry_h__
+
+#include "mozilla/GuardObjects.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/StartupTimeline.h"
+#include "nsTArray.h"
+#include "nsStringGlue.h"
+#include "nsXULAppAPI.h"
+
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "mozilla/TelemetryScalarEnums.h"
+
+/******************************************************************************
+ * This implements the Telemetry system.
+ * It allows recording into histograms as well some more specialized data
+ * points and gives access to the data.
+ *
+ * For documentation on how to add and use new Telemetry probes, see:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Adding_a_new_Telemetry_probe
+ *
+ * For more general information on Telemetry see:
+ * https://wiki.mozilla.org/Telemetry
+ *****************************************************************************/
+
+namespace mozilla {
+namespace HangMonitor {
+ class HangAnnotations;
+} // namespace HangMonitor
+namespace Telemetry {
+
+struct Accumulation;
+struct KeyedAccumulation;
+
+enum TimerResolution {
+ Millisecond,
+ Microsecond
+};
+
+/**
+ * Create and destroy the underlying base::StatisticsRecorder singleton.
+ * Creation has to be done very early in the startup sequence.
+ */
+void CreateStatisticsRecorder();
+void DestroyStatisticsRecorder();
+
+/**
+ * Initialize the Telemetry service on the main thread at startup.
+ */
+void Init();
+
+/**
+ * Adds sample to a histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param sample - value to record.
+ */
+void Accumulate(ID id, uint32_t sample);
+
+/**
+ * Adds sample to a keyed histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - keyed histogram id
+ * @param key - the string key
+ * @param sample - (optional) value to record, defaults to 1.
+ */
+void Accumulate(ID id, const nsCString& key, uint32_t sample = 1);
+
+/**
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
+ * This function is here to support telemetry measurements from Java,
+ * where we have only names and not numeric IDs. You should almost
+ * certainly be using the by-enum-id version instead of this one.
+ *
+ * @param name - histogram name
+ * @param sample - value to record
+ */
+void Accumulate(const char* name, uint32_t sample);
+
+/**
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
+ * This function is here to support telemetry measurements from Java,
+ * where we have only names and not numeric IDs. You should almost
+ * certainly be using the by-enum-id version instead of this one.
+ *
+ * @param name - histogram name
+ * @param key - the string key
+ * @param sample - sample - (optional) value to record, defaults to 1.
+ */
+void Accumulate(const char *name, const nsCString& key, uint32_t sample = 1);
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This is the typesafe - and preferred - way to use the categorical histograms
+ * by passing values from the corresponding Telemetry::LABELS_* enum.
+ *
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template<class E>
+void AccumulateCategorical(E enumValue) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ Accumulate(static_cast<ID>(CategoricalLabelId<E>::value),
+ static_cast<uint32_t>(enumValue));
+};
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This string will be matched against the labels defined in Histograms.json.
+ * If the string does not match a label defined for the histogram, nothing will
+ * be recorded.
+ *
+ * @param id - The histogram id.
+ * @param label - A string label value that is defined in Histograms.json for this histogram.
+ */
+void AccumulateCategorical(ID id, const nsCString& label);
+
+/**
+ * Adds time delta in milliseconds to a histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param start - start time
+ * @param end - end time
+ */
+void AccumulateTimeDelta(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now());
+
+/**
+ * Accumulate child process data into histograms for the given process type.
+ *
+ * @param aAccumulations - accumulation actions to perform
+ */
+void AccumulateChild(GeckoProcessType aProcessType, const nsTArray<Accumulation>& aAccumulations);
+
+/**
+ * Accumulate child process data into keyed histograms for the given process type.
+ *
+ * @param aAccumulations - accumulation actions to perform
+ */
+void AccumulateChildKeyed(GeckoProcessType aProcessType, const nsTArray<KeyedAccumulation>& aAccumulations);
+
+/**
+ * Enable/disable recording for this histogram at runtime.
+ * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[].
+ * id must be a valid telemetry enum, otherwise an assertion is triggered.
+ *
+ * @param id - histogram id
+ * @param enabled - whether or not to enable recording from now on.
+ */
+void SetHistogramRecordingEnabled(ID id, bool enabled);
+
+const char* GetHistogramName(ID id);
+
+/**
+ * Those wrappers are needed because the VS versions we use do not support free
+ * functions with default template arguments.
+ */
+template<TimerResolution res>
+struct AccumulateDelta_impl
+{
+ static void compute(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now());
+ static void compute(ID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now());
+};
+
+template<>
+struct AccumulateDelta_impl<Millisecond>
+{
+ static void compute(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()) {
+ Accumulate(id, static_cast<uint32_t>((end - start).ToMilliseconds()));
+ }
+ static void compute(ID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now()) {
+ Accumulate(id, key, static_cast<uint32_t>((end - start).ToMilliseconds()));
+ }
+};
+
+template<>
+struct AccumulateDelta_impl<Microsecond>
+{
+ static void compute(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now()) {
+ Accumulate(id, static_cast<uint32_t>((end - start).ToMicroseconds()));
+ }
+ static void compute(ID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now()) {
+ Accumulate(id, key, static_cast<uint32_t>((end - start).ToMicroseconds()));
+ }
+};
+
+
+template<ID id, TimerResolution res = Millisecond>
+class MOZ_RAII AutoTimer {
+public:
+ explicit AutoTimer(TimeStamp aStart = TimeStamp::Now() MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : start(aStart)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ }
+
+ explicit AutoTimer(const nsCString& aKey, TimeStamp aStart = TimeStamp::Now() MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : start(aStart)
+ , key(aKey)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ }
+
+ ~AutoTimer() {
+ if (key.IsEmpty()) {
+ AccumulateDelta_impl<res>::compute(id, start);
+ } else {
+ AccumulateDelta_impl<res>::compute(id, key, start);
+ }
+ }
+
+private:
+ const TimeStamp start;
+ const nsCString key;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+template<ID id>
+class MOZ_RAII AutoCounter {
+public:
+ explicit AutoCounter(uint32_t counterStart = 0 MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+ : counter(counterStart)
+ {
+ MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+ }
+
+ ~AutoCounter() {
+ Accumulate(id, counter);
+ }
+
+ // Prefix increment only, to encourage good habits.
+ void operator++() {
+ ++counter;
+ }
+
+ // Chaining doesn't make any sense, don't return anything.
+ void operator+=(int increment) {
+ counter += increment;
+ }
+
+private:
+ uint32_t counter;
+ MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
+/**
+ * Indicates whether Telemetry base data recording is turned on. Added for future uses.
+ */
+bool CanRecordBase();
+
+/**
+ * Indicates whether Telemetry extended data recording is turned on. This is intended
+ * to guard calls to Accumulate when the statistic being recorded is expensive to compute.
+ */
+bool CanRecordExtended();
+
+/**
+ * Records slow SQL statements for Telemetry reporting.
+ *
+ * @param statement - offending SQL statement to record
+ * @param dbName - DB filename
+ * @param delay - execution time in milliseconds
+ */
+void RecordSlowSQLStatement(const nsACString &statement,
+ const nsACString &dbName,
+ uint32_t delay);
+
+/**
+ * Record Webrtc ICE candidate type combinations in a 17bit bitmask
+ *
+ * @param iceCandidateBitmask - the bitmask representing local and remote ICE
+ * candidate types present for the connection
+ * @param success - did the peer connection connected
+ */
+void
+RecordWebrtcIceCandidates(const uint32_t iceCandidateBitmask,
+ const bool success);
+/**
+ * Initialize I/O Reporting
+ * Initially this only records I/O for files in the binary directory.
+ *
+ * @param aXreDir - XRE directory
+ */
+void InitIOReporting(nsIFile* aXreDir);
+
+/**
+ * Set the profile directory. Once called, files in the profile directory will
+ * be included in I/O reporting. We can't use the directory
+ * service to obtain this information because it isn't running yet.
+ */
+void SetProfileDir(nsIFile* aProfD);
+
+/**
+ * Called to inform Telemetry that startup has completed.
+ */
+void LeavingStartupStage();
+
+/**
+ * Called to inform Telemetry that shutdown is commencing.
+ */
+void EnteringShutdownStage();
+
+/**
+ * Thresholds for a statement to be considered slow, in milliseconds
+ */
+const uint32_t kSlowSQLThresholdForMainThread = 50;
+const uint32_t kSlowSQLThresholdForHelperThreads = 100;
+
+class ProcessedStack;
+
+/**
+ * Record the main thread's call stack after it hangs.
+ *
+ * @param aDuration - Approximate duration of main thread hang, in seconds
+ * @param aStack - Array of PCs from the hung call stack
+ * @param aSystemUptime - System uptime at the time of the hang, in minutes
+ * @param aFirefoxUptime - Firefox uptime at the time of the hang, in minutes
+ * @param aAnnotations - Any annotations to be added to the report
+ */
+#if defined(MOZ_ENABLE_PROFILER_SPS)
+void RecordChromeHang(uint32_t aDuration,
+ ProcessedStack &aStack,
+ int32_t aSystemUptime,
+ int32_t aFirefoxUptime,
+ mozilla::UniquePtr<mozilla::HangMonitor::HangAnnotations>
+ aAnnotations);
+#endif
+
+class ThreadHangStats;
+
+/**
+ * Move a ThreadHangStats to Telemetry storage. Normally Telemetry queries
+ * for active ThreadHangStats through BackgroundHangMonitor, but once a
+ * thread exits, the thread's copy of ThreadHangStats needs to be moved to
+ * inside Telemetry using this function.
+ *
+ * @param aStats ThreadHangStats to save; the data inside aStats
+ * will be moved and aStats should be treated as
+ * invalid after this function returns
+ */
+void RecordThreadHangStats(ThreadHangStats& aStats);
+
+/**
+ * Record a failed attempt at locking the user's profile.
+ *
+ * @param aProfileDir The profile directory whose lock attempt failed
+ */
+void WriteFailedProfileLock(nsIFile* aProfileDir);
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to, truncated to
+ * 50 characters if exceeding that length.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value the scalar is set to if its greater
+ * than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value the scalar is set to if its greater
+ * than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // Telemetry_h__
diff --git a/toolkit/components/telemetry/TelemetryArchive.jsm b/toolkit/components/telemetry/TelemetryArchive.jsm
new file mode 100644
index 0000000000..c5d251ab75
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryArchive.jsm
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetryArchive"
+];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryArchive::";
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
+
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage",
+ "resource://gre/modules/TelemetryStorage.jsm");
+
+this.TelemetryArchive = {
+ /**
+ * Get a list of the archived pings, sorted by the creation date.
+ * Note that scanning the archived pings on disk is delayed on startup,
+ * use promizeInitialized() to access this after scanning.
+ *
+ * @return {Promise<sequence<Object>>}
+ * A list of the archived ping info in the form:
+ * { id: <string>,
+ * timestampCreated: <number>,
+ * type: <string> }
+ */
+ promiseArchivedPingList: function() {
+ return TelemetryArchiveImpl.promiseArchivedPingList();
+ },
+
+ /**
+ * Load an archived ping from disk by id, asynchronously.
+ *
+ * @param id {String} The pings UUID.
+ * @return {Promise<PingData>} A promise resolved with the pings data on success.
+ */
+ promiseArchivedPingById: function(id) {
+ return TelemetryArchiveImpl.promiseArchivedPingById(id);
+ },
+
+ /**
+ * Archive a ping and persist it to disk.
+ *
+ * @param {object} ping The ping data to archive.
+ * @return {promise} Promise that is resolved when the ping is successfully archived.
+ */
+ promiseArchivePing: function(ping) {
+ return TelemetryArchiveImpl.promiseArchivePing(ping);
+ },
+};
+
+/**
+ * Checks if pings can be archived. Some products (e.g. Thunderbird) might not want
+ * to do that.
+ * @return {Boolean} True if pings should be archived, false otherwise.
+ */
+function shouldArchivePings() {
+ return Preferences.get(PREF_ARCHIVE_ENABLED, false);
+}
+
+var TelemetryArchiveImpl = {
+ _logger: null,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+
+ return this._logger;
+ },
+
+ promiseArchivePing: function(ping) {
+ if (!shouldArchivePings()) {
+ this._log.trace("promiseArchivePing - archiving is disabled");
+ return Promise.resolve();
+ }
+
+ for (let field of ["creationDate", "id", "type"]) {
+ if (!(field in ping)) {
+ this._log.warn("promiseArchivePing - missing field " + field)
+ return Promise.reject(new Error("missing field " + field));
+ }
+ }
+
+ return TelemetryStorage.saveArchivedPing(ping);
+ },
+
+ _buildArchivedPingList: function(archivedPingsMap) {
+ let list = Array.from(archivedPingsMap, p => ({
+ id: p[0],
+ timestampCreated: p[1].timestampCreated,
+ type: p[1].type,
+ }));
+
+ list.sort((a, b) => a.timestampCreated - b.timestampCreated);
+
+ return list;
+ },
+
+ promiseArchivedPingList: function() {
+ this._log.trace("promiseArchivedPingList");
+
+ return TelemetryStorage.loadArchivedPingList().then(loadedInfo => {
+ return this._buildArchivedPingList(loadedInfo);
+ });
+ },
+
+ promiseArchivedPingById: function(id) {
+ this._log.trace("promiseArchivedPingById - id: " + id);
+ return TelemetryStorage.loadArchivedPing(id);
+ },
+};
diff --git a/toolkit/components/telemetry/TelemetryCommon.cpp b/toolkit/components/telemetry/TelemetryCommon.cpp
new file mode 100644
index 0000000000..db9341ab57
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryCommon.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsITelemetry.h"
+#include "nsVersionComparator.h"
+#include "mozilla/TimeStamp.h"
+#include "nsIConsoleService.h"
+#include "nsThreadUtils.h"
+
+#include "TelemetryCommon.h"
+
+#include <cstring>
+
+namespace mozilla {
+namespace Telemetry {
+namespace Common {
+
+bool
+IsExpiredVersion(const char* aExpiration)
+{
+ MOZ_ASSERT(aExpiration);
+ // Note: We intentionally don't construct a static Version object here as we
+ // saw odd crashes around this (see bug 1334105).
+ return strcmp(aExpiration, "never") && strcmp(aExpiration, "default") &&
+ (mozilla::Version(aExpiration) <= MOZ_APP_VERSION);
+}
+
+bool
+IsInDataset(uint32_t aDataset, uint32_t aContainingDataset)
+{
+ if (aDataset == aContainingDataset) {
+ return true;
+ }
+
+ // The "optin on release channel" dataset is a superset of the
+ // "optout on release channel one".
+ if (aContainingDataset == nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN &&
+ aDataset == nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT) {
+ return true;
+ }
+
+ return false;
+}
+
+bool
+CanRecordDataset(uint32_t aDataset, bool aCanRecordBase, bool aCanRecordExtended)
+{
+ // If we are extended telemetry is enabled, we are allowed to record
+ // regardless of the dataset.
+ if (aCanRecordExtended) {
+ return true;
+ }
+
+ // If base telemetry data is enabled and we're trying to record base
+ // telemetry, allow it.
+ if (aCanRecordBase &&
+ IsInDataset(aDataset, nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT)) {
+ return true;
+ }
+
+ // We're not recording extended telemetry or this is not the base
+ // dataset. Bail out.
+ return false;
+}
+
+nsresult
+MsSinceProcessStart(double* aResult)
+{
+ bool error;
+ *aResult = (TimeStamp::NowLoRes() -
+ TimeStamp::ProcessCreation(error)).ToMilliseconds();
+ if (error) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+void
+LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg)
+{
+ if (!NS_IsMainThread()) {
+ nsString msg(aMsg);
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction([aLogLevel, msg]() { LogToBrowserConsole(aLogLevel, msg); });
+ NS_DispatchToMainThread(task.forget(), NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ nsCOMPtr<nsIConsoleService> console(do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
+ error->Init(aMsg, EmptyString(), EmptyString(), 0, 0, aLogLevel, "chrome javascript");
+ console->LogMessage(error);
+}
+
+} // namespace Common
+} // namespace Telemetry
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/TelemetryCommon.h b/toolkit/components/telemetry/TelemetryCommon.h
new file mode 100644
index 0000000000..3beefd673b
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryCommon.h
@@ -0,0 +1,75 @@
+/* -*- 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 TelemetryCommon_h__
+#define TelemetryCommon_h__
+
+#include "nsTHashtable.h"
+#include "jsapi.h"
+#include "nsIScriptError.h"
+
+namespace mozilla {
+namespace Telemetry {
+namespace Common {
+
+template<class EntryType>
+class AutoHashtable : public nsTHashtable<EntryType>
+{
+public:
+ explicit AutoHashtable(uint32_t initLength =
+ PLDHashTable::kDefaultInitialLength);
+ typedef bool (*ReflectEntryFunc)(EntryType *entry, JSContext *cx, JS::Handle<JSObject*> obj);
+ bool ReflectIntoJS(ReflectEntryFunc entryFunc, JSContext *cx, JS::Handle<JSObject*> obj);
+};
+
+template<class EntryType>
+AutoHashtable<EntryType>::AutoHashtable(uint32_t initLength)
+ : nsTHashtable<EntryType>(initLength)
+{
+}
+
+/**
+ * Reflect the individual entries of table into JS, usually by defining
+ * some property and value of obj. entryFunc is called for each entry.
+ */
+template<typename EntryType>
+bool
+AutoHashtable<EntryType>::ReflectIntoJS(ReflectEntryFunc entryFunc,
+ JSContext *cx, JS::Handle<JSObject*> obj)
+{
+ for (auto iter = this->Iter(); !iter.Done(); iter.Next()) {
+ if (!entryFunc(iter.Get(), cx, obj)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool IsExpiredVersion(const char* aExpiration);
+bool IsInDataset(uint32_t aDataset, uint32_t aContainingDataset);
+bool CanRecordDataset(uint32_t aDataset, bool aCanRecordBase, bool aCanRecordExtended);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes).
+ *
+ * @return NS_OK on success, NS_ERROR_NOT_AVAILABLE if TimeStamp doesn't have the data.
+ */
+nsresult MsSinceProcessStart(double* aResult);
+
+/**
+ * Dumps a log message to the Browser Console using the provided level.
+ *
+ * @param aLogLevel The level to use when displaying the message in the browser console
+ * (e.g. nsIScriptError::warningFlag, ...).
+ * @param aMsg The text message to print to the console.
+ */
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg);
+
+} // namespace Common
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // TelemetryCommon_h__
diff --git a/toolkit/components/telemetry/TelemetryComms.h b/toolkit/components/telemetry/TelemetryComms.h
new file mode 100644
index 0000000000..0f2d888e31
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryComms.h
@@ -0,0 +1,84 @@
+/* -*- 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
+ */
+
+#ifndef Telemetry_Comms_h__
+#define Telemetry_Comms_h__
+
+#include "ipc/IPCMessageUtils.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+enum ID : uint32_t;
+
+struct Accumulation
+{
+ mozilla::Telemetry::ID mId;
+ uint32_t mSample;
+};
+
+struct KeyedAccumulation
+{
+ mozilla::Telemetry::ID mId;
+ uint32_t mSample;
+ nsCString mKey;
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace IPC {
+
+template<>
+struct
+ParamTraits<mozilla::Telemetry::Accumulation>
+{
+ typedef mozilla::Telemetry::Accumulation paramType;
+
+ static void Write(Message* aMsg, const paramType& aParam)
+ {
+ aMsg->WriteUInt32(aParam.mId);
+ WriteParam(aMsg, aParam.mSample);
+ }
+
+ static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+ {
+ if (!aMsg->ReadUInt32(aIter, reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aMsg, aIter, &(aResult->mSample))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template<>
+struct
+ParamTraits<mozilla::Telemetry::KeyedAccumulation>
+{
+ typedef mozilla::Telemetry::KeyedAccumulation paramType;
+
+ static void Write(Message* aMsg, const paramType& aParam)
+ {
+ aMsg->WriteUInt32(aParam.mId);
+ WriteParam(aMsg, aParam.mSample);
+ WriteParam(aMsg, aParam.mKey);
+ }
+
+ static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+ {
+ if (!aMsg->ReadUInt32(aIter, reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aMsg, aIter, &(aResult->mSample)) ||
+ !ReadParam(aMsg, aIter, &(aResult->mKey))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif // Telemetry_Comms_h__
diff --git a/toolkit/components/telemetry/TelemetryController.jsm b/toolkit/components/telemetry/TelemetryController.jsm
new file mode 100644
index 0000000000..b8de776da3
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -0,0 +1,954 @@
+/* -*- js-indent-level: 2; 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const myScope = this;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/debug.js", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/DeferredTask.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+const Utils = TelemetryUtils;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryController::";
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_BRANCH_LOG = PREF_BRANCH + "log.";
+const PREF_SERVER = PREF_BRANCH + "server";
+const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
+const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
+const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
+const PREF_UNIFIED = PREF_BRANCH + "unified";
+
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
+
+const PING_FORMAT_VERSION = 4;
+
+// Delay before intializing telemetry (ms)
+const TELEMETRY_DELAY = Preferences.get("toolkit.telemetry.initDelay", 60) * 1000;
+// Delay before initializing telemetry if we're testing (ms)
+const TELEMETRY_TEST_DELAY = 1;
+
+// Ping types.
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_DELETION = "deletion";
+
+// Session ping reasons.
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ClientID",
+ "resource://gre/modules/ClientID.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
+ "@mozilla.org/base/telemetry;1",
+ "nsITelemetry");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage",
+ "resource://gre/modules/TelemetryStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ThirdPartyCookieProbe",
+ "resource://gre/modules/ThirdPartyCookieProbe.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
+ "resource://gre/modules/TelemetryEnvironment.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionRecorder",
+ "resource://gre/modules/SessionRecorder.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive",
+ "resource://gre/modules/TelemetryArchive.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySession",
+ "resource://gre/modules/TelemetrySession.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend",
+ "resource://gre/modules/TelemetrySend.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy",
+ "resource://gre/modules/TelemetryReportingPolicy.jsm");
+
+/**
+ * Setup Telemetry logging. This function also gets called when loggin related
+ * preferences change.
+ */
+var gLogger = null;
+var gLogAppenderDump = null;
+function configureLogging() {
+ if (!gLogger) {
+ gLogger = Log.repository.getLogger(LOGGER_NAME);
+
+ // Log messages need to go to the browser console.
+ let consoleAppender = new Log.ConsoleAppender(new Log.BasicFormatter());
+ gLogger.addAppender(consoleAppender);
+
+ Preferences.observe(PREF_BRANCH_LOG, configureLogging);
+ }
+
+ // Make sure the logger keeps up with the logging level preference.
+ gLogger.level = Log.Level[Preferences.get(PREF_LOG_LEVEL, "Warn")];
+
+ // If enabled in the preferences, add a dump appender.
+ let logDumping = Preferences.get(PREF_LOG_DUMP, false);
+ if (logDumping != !!gLogAppenderDump) {
+ if (logDumping) {
+ gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
+ gLogger.addAppender(gLogAppenderDump);
+ } else {
+ gLogger.removeAppender(gLogAppenderDump);
+ gLogAppenderDump = null;
+ }
+ }
+}
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+ now: () => new Date(),
+ generatePingId: () => Utils.generateUUID(),
+ getCachedClientID: () => ClientID.getCachedClientID(),
+}
+
+this.EXPORTED_SYMBOLS = ["TelemetryController"];
+
+this.TelemetryController = Object.freeze({
+ Constants: Object.freeze({
+ PREF_LOG_LEVEL: PREF_LOG_LEVEL,
+ PREF_LOG_DUMP: PREF_LOG_DUMP,
+ PREF_SERVER: PREF_SERVER,
+ }),
+
+ /**
+ * Used only for testing purposes.
+ */
+ testInitLogging: function() {
+ configureLogging();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testReset: function() {
+ return Impl.reset();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testSetup: function() {
+ return Impl.setupTelemetry(true);
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testShutdown: function() {
+ return Impl.shutdown();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testSetupContent: function() {
+ return Impl.setupContentTelemetry(true);
+ },
+
+ /**
+ * Send a notification.
+ */
+ observe: function (aSubject, aTopic, aData) {
+ return Impl.observe(aSubject, aTopic, aData);
+ },
+
+ /**
+ * Submit ping payloads to Telemetry. This will assemble a complete ping, adding
+ * environment data, client id and some general info.
+ * Depending on configuration, the ping will be sent to the server (immediately or later)
+ * and archived locally.
+ *
+ * To identify the different pings and to be able to query them pings have a type.
+ * A type is a string identifier that should be unique to the type ping that is being submitted,
+ * it should only contain alphanumeric characters and '-' for separation, i.e. satisfy:
+ * /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @returns {Promise} Test-only - a promise that resolves with the ping id once the ping is stored or sent.
+ */
+ submitExternalPing: function(aType, aPayload, aOptions = {}) {
+ aOptions.addClientId = aOptions.addClientId || false;
+ aOptions.addEnvironment = aOptions.addEnvironment || false;
+
+ return Impl.submitExternalPing(aType, aPayload, aOptions);
+ },
+
+ /**
+ * Get the current session ping data as it would be sent out or stored.
+ *
+ * @param {bool} aSubsession Whether to get subsession data. Optional, defaults to false.
+ * @return {object} The current ping data if Telemetry is enabled, null otherwise.
+ */
+ getCurrentPingData: function(aSubsession = false) {
+ return Impl.getCurrentPingData(aSubsession);
+ },
+
+ /**
+ * Save a ping to disk.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
+ * if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ addPendingPing: function(aType, aPayload, aOptions = {}) {
+ let options = aOptions;
+ options.addClientId = aOptions.addClientId || false;
+ options.addEnvironment = aOptions.addEnvironment || false;
+ options.overwrite = aOptions.overwrite || false;
+
+ return Impl.addPendingPing(aType, aPayload, options);
+ },
+
+ /**
+ * Check if we have an aborted-session ping from a previous session.
+ * If so, submit and then remove it.
+ *
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ checkAbortedSessionPing: function() {
+ return Impl.checkAbortedSessionPing();
+ },
+
+ /**
+ * Save an aborted-session ping to disk without adding it to the pending pings.
+ *
+ * @param {Object} aPayload The ping payload data.
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ saveAbortedSessionPing: function(aPayload) {
+ return Impl.saveAbortedSessionPing(aPayload);
+ },
+
+ /**
+ * Remove the aborted-session ping if any exists.
+ *
+ * @return {Promise} Promise that is resolved when the ping was removed.
+ */
+ removeAbortedSessionPing: function() {
+ return Impl.removeAbortedSessionPing();
+ },
+
+ /**
+ * Write a ping to a specified location on the disk. Does not add the ping to the
+ * pending pings.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {String} aFilePath The path to save the ping to.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
+ * if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ savePing: function(aType, aPayload, aFilePath, aOptions = {}) {
+ let options = aOptions;
+ options.addClientId = aOptions.addClientId || false;
+ options.addEnvironment = aOptions.addEnvironment || false;
+ options.overwrite = aOptions.overwrite || false;
+
+ return Impl.savePing(aType, aPayload, aFilePath, options);
+ },
+
+ /**
+ * The session recorder instance managed by Telemetry.
+ * @return {Object} The active SessionRecorder instance or null if not available.
+ */
+ getSessionRecorder: function() {
+ return Impl._sessionRecorder;
+ },
+
+ /**
+ * Allows waiting for TelemetryControllers delayed initialization to complete.
+ * The returned promise is guaranteed to resolve before TelemetryController is shutting down.
+ * @return {Promise} Resolved when delayed TelemetryController initialization completed.
+ */
+ promiseInitialized: function() {
+ return Impl.promiseInitialized();
+ },
+});
+
+var Impl = {
+ _initialized: false,
+ _initStarted: false, // Whether we started setting up TelemetryController.
+ _logger: null,
+ _prevValues: {},
+ // The previous build ID, if this is the first run with a new build.
+ // Undefined if this is not the first run, or the previous build ID is unknown.
+ _previousBuildID: undefined,
+ _clientID: null,
+ // A task performing delayed initialization
+ _delayedInitTask: null,
+ // The deferred promise resolved when the initialization task completes.
+ _delayedInitTaskDeferred: null,
+
+ // The session recorder, shared with FHR and the Data Reporting Service.
+ _sessionRecorder: null,
+ // This is a public barrier Telemetry clients can use to add blockers to the shutdown
+ // of TelemetryController.
+ // After this barrier, clients can not submit Telemetry pings anymore.
+ _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for clients."),
+ // This is a private barrier blocked by pending async ping activity (sending & saving).
+ _connectionsBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for pending ping activity"),
+ // This is true when running in the test infrastructure.
+ _testMode: false,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Get the data for the "application" section of the ping.
+ */
+ _getApplicationSection: function() {
+ // Querying architecture and update channel can throw. Make sure to recover and null
+ // those fields.
+ let arch = null;
+ try {
+ arch = Services.sysinfo.get("arch");
+ } catch (e) {
+ this._log.trace("_getApplicationSection - Unable to get system architecture.", e);
+ }
+
+ let updateChannel = null;
+ try {
+ updateChannel = UpdateUtils.getUpdateChannel(false);
+ } catch (e) {
+ this._log.trace("_getApplicationSection - Unable to get update channel.", e);
+ }
+
+ return {
+ architecture: arch,
+ buildId: Services.appinfo.appBuildID,
+ name: Services.appinfo.name,
+ version: Services.appinfo.version,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: Services.appinfo.vendor,
+ platformVersion: Services.appinfo.platformVersion,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ channel: updateChannel,
+ };
+ },
+
+ /**
+ * Assemble a complete ping following the common ping format specification.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ *
+ * @returns {Object} An object that contains the assembled ping data.
+ */
+ assemblePing: function assemblePing(aType, aPayload, aOptions = {}) {
+ this._log.trace("assemblePing - Type " + aType + ", aOptions " + JSON.stringify(aOptions));
+
+ // Clone the payload data so we don't race against unexpected changes in subobjects that are
+ // still referenced by other code.
+ // We can't trust all callers to do this properly on their own.
+ let payload = Cu.cloneInto(aPayload, myScope);
+
+ // Fill the common ping fields.
+ let pingData = {
+ type: aType,
+ id: Policy.generatePingId(),
+ creationDate: (Policy.now()).toISOString(),
+ version: PING_FORMAT_VERSION,
+ application: this._getApplicationSection(),
+ payload: payload,
+ };
+
+ if (aOptions.addClientId) {
+ pingData.clientId = this._clientID;
+ }
+
+ if (aOptions.addEnvironment) {
+ pingData.environment = aOptions.overrideEnvironment || TelemetryEnvironment.currentEnvironment;
+ }
+
+ return pingData;
+ },
+
+ /**
+ * Track any pending ping send and save tasks through the promise passed here.
+ * This is needed to block shutdown on any outstanding ping activity.
+ */
+ _trackPendingPingTask: function (aPromise) {
+ this._connectionsBarrier.client.addBlocker("Waiting for ping task", aPromise);
+ },
+
+ /**
+ * Internal function to assemble a complete ping, adding environment data, client id
+ * and some general info. This waits on the client id to be loaded/generated if it's
+ * not yet available. Note that this function is synchronous unless we need to load
+ * the client id.
+ * Depending on configuration, the ping will be sent to the server (immediately or later)
+ * and archived locally.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
+ */
+ _submitPingLogic: Task.async(function* (aType, aPayload, aOptions) {
+ // Make sure to have a clientId if we need one. This cover the case of submitting
+ // a ping early during startup, before Telemetry is initialized, if no client id was
+ // cached.
+ if (!this._clientID && aOptions.addClientId) {
+ Telemetry.getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID").add();
+ // We can safely call |getClientID| here and during initialization: we would still
+ // spawn and return one single loading task.
+ this._clientID = yield ClientID.getClientID();
+ }
+
+ const pingData = this.assemblePing(aType, aPayload, aOptions);
+ this._log.trace("submitExternalPing - ping assembled, id: " + pingData.id);
+
+ // Always persist the pings if we are allowed to. We should not yield on any of the
+ // following operations to keep this function synchronous for the majority of the calls.
+ let archivePromise = TelemetryArchive.promiseArchivePing(pingData)
+ .catch(e => this._log.error("submitExternalPing - Failed to archive ping " + pingData.id, e));
+ let p = [ archivePromise ];
+
+ p.push(TelemetrySend.submitPing(pingData));
+
+ return Promise.all(p).then(() => pingData.id);
+ }),
+
+ /**
+ * Submit ping payloads to Telemetry.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
+ */
+ submitExternalPing: function send(aType, aPayload, aOptions) {
+ this._log.trace("submitExternalPing - type: " + aType + ", aOptions: " + JSON.stringify(aOptions));
+
+ // Enforce the type string to only contain sane characters.
+ const typeUuid = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;
+ if (!typeUuid.test(aType)) {
+ this._log.error("submitExternalPing - invalid ping type: " + aType);
+ let histogram = Telemetry.getKeyedHistogramById("TELEMETRY_INVALID_PING_TYPE_SUBMITTED");
+ histogram.add(aType, 1);
+ return Promise.reject(new Error("Invalid type string submitted."));
+ }
+ // Enforce that the payload is an object.
+ if (aPayload === null || typeof aPayload !== 'object' || Array.isArray(aPayload)) {
+ this._log.error("submitExternalPing - invalid payload type: " + typeof aPayload);
+ let histogram = Telemetry.getHistogramById("TELEMETRY_INVALID_PAYLOAD_SUBMITTED");
+ histogram.add(1);
+ return Promise.reject(new Error("Invalid payload type submitted."));
+ }
+
+ let promise = this._submitPingLogic(aType, aPayload, aOptions);
+ this._trackPendingPingTask(promise);
+ return promise;
+ },
+
+ /**
+ * Save a ping to disk.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+ * false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ addPendingPing: function addPendingPing(aType, aPayload, aOptions) {
+ this._log.trace("addPendingPing - Type " + aType + ", aOptions " + JSON.stringify(aOptions));
+
+ let pingData = this.assemblePing(aType, aPayload, aOptions);
+
+ let savePromise = TelemetryStorage.savePendingPing(pingData);
+ let archivePromise = TelemetryArchive.promiseArchivePing(pingData).catch(e => {
+ this._log.error("addPendingPing - Failed to archive ping " + pingData.id, e);
+ });
+
+ // Wait for both the archiving and ping persistence to complete.
+ let promises = [
+ savePromise,
+ archivePromise,
+ ];
+ return Promise.all(promises).then(() => pingData.id);
+ },
+
+ /**
+ * Write a ping to a specified location on the disk. Does not add the ping to the
+ * pending pings.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {String} aFilePath The path to save the ping to.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+ * false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ savePing: function savePing(aType, aPayload, aFilePath, aOptions) {
+ this._log.trace("savePing - Type " + aType + ", File Path " + aFilePath +
+ ", aOptions " + JSON.stringify(aOptions));
+ let pingData = this.assemblePing(aType, aPayload, aOptions);
+ return TelemetryStorage.savePingToFile(pingData, aFilePath, aOptions.overwrite)
+ .then(() => pingData.id);
+ },
+
+ /**
+ * Check whether we have an aborted-session ping. If so add it to the pending pings and archive it.
+ *
+ * @return {Promise} Promise that is resolved when the ping is submitted and archived.
+ */
+ checkAbortedSessionPing: Task.async(function*() {
+ let ping = yield TelemetryStorage.loadAbortedSessionPing();
+ this._log.trace("checkAbortedSessionPing - found aborted-session ping: " + !!ping);
+ if (!ping) {
+ return;
+ }
+
+ try {
+ yield TelemetryStorage.addPendingPing(ping);
+ yield TelemetryArchive.promiseArchivePing(ping);
+ } catch (e) {
+ this._log.error("checkAbortedSessionPing - Unable to add the pending ping", e);
+ } finally {
+ yield TelemetryStorage.removeAbortedSessionPing();
+ }
+ }),
+
+ /**
+ * Save an aborted-session ping to disk without adding it to the pending pings.
+ *
+ * @param {Object} aPayload The ping payload data.
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ saveAbortedSessionPing: function(aPayload) {
+ this._log.trace("saveAbortedSessionPing");
+ const options = {addClientId: true, addEnvironment: true};
+ const pingData = this.assemblePing(PING_TYPE_MAIN, aPayload, options);
+ return TelemetryStorage.saveAbortedSessionPing(pingData);
+ },
+
+ removeAbortedSessionPing: function() {
+ return TelemetryStorage.removeAbortedSessionPing();
+ },
+
+ /**
+ * Perform telemetry initialization for either chrome or content process.
+ * @return {Boolean} True if Telemetry is allowed to record at least base (FHR) data,
+ * false otherwise.
+ */
+ enableTelemetryRecording: function enableTelemetryRecording() {
+ // The thumbnail service also runs in a content process, even with e10s off.
+ // We need to check if e10s is on so we don't submit child payloads for it.
+ // We still need xpcshell child tests to work, so we skip this if test mode is enabled.
+ if (Utils.isContentProcess && !this._testMode && !Services.appinfo.browserTabsRemoteAutostart) {
+ this._log.config("enableTelemetryRecording - not enabling Telemetry for non-e10s child process");
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ return false;
+ }
+
+ // Configure base Telemetry recording.
+ // Unified Telemetry makes it opt-out. If extended Telemetry is enabled, base recording
+ // is always on as well.
+ const enabled = Utils.isTelemetryEnabled;
+ Telemetry.canRecordBase = enabled || IS_UNIFIED_TELEMETRY;
+ Telemetry.canRecordExtended = enabled;
+
+ this._log.config("enableTelemetryRecording - canRecordBase:" + Telemetry.canRecordBase +
+ ", canRecordExtended: " + Telemetry.canRecordExtended);
+
+ return Telemetry.canRecordBase;
+ },
+
+ /**
+ * This triggers basic telemetry initialization and schedules a full initialized for later
+ * for performance reasons.
+ *
+ * This delayed initialization means TelemetryController init can be in the following states:
+ * 1) setupTelemetry was never called
+ * or it was called and
+ * 2) _delayedInitTask was scheduled, but didn't run yet.
+ * 3) _delayedInitTask is currently running.
+ * 4) _delayedInitTask finished running and is nulled out.
+ *
+ * @return {Promise} Resolved when TelemetryController and TelemetrySession are fully
+ * initialized. This is only used in tests.
+ */
+ setupTelemetry: function setupTelemetry(testing) {
+ this._initStarted = true;
+ this._testMode = testing;
+
+ this._log.trace("setupTelemetry");
+
+ if (this._delayedInitTask) {
+ this._log.error("setupTelemetry - init task already running");
+ return this._delayedInitTaskDeferred.promise;
+ }
+
+ if (this._initialized && !this._testMode) {
+ this._log.error("setupTelemetry - already initialized");
+ return Promise.resolve();
+ }
+
+ // This will trigger displaying the datachoices infobar.
+ TelemetryReportingPolicy.setup();
+
+ if (!this.enableTelemetryRecording()) {
+ this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
+ return Promise.resolve();
+ }
+
+ // Initialize the session recorder.
+ if (!this._sessionRecorder) {
+ this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH);
+ this._sessionRecorder.onStartup();
+ }
+
+ this._attachObservers();
+
+ // Perform a lightweight, early initialization for the component, just registering
+ // a few observers and initializing the session.
+ TelemetrySession.earlyInit(this._testMode);
+
+ // For very short session durations, we may never load the client
+ // id from disk.
+ // We try to cache it in prefs to avoid this, even though this may
+ // lead to some stale client ids.
+ this._clientID = ClientID.getCachedClientID();
+
+ // Delay full telemetry initialization to give the browser time to
+ // run various late initializers. Otherwise our gathered memory
+ // footprint and other numbers would be too optimistic.
+ this._delayedInitTaskDeferred = Promise.defer();
+ this._delayedInitTask = new DeferredTask(function* () {
+ try {
+ // TODO: This should probably happen after all the delayed init here.
+ this._initialized = true;
+ TelemetryEnvironment.delayedInit();
+
+ yield TelemetrySend.setup(this._testMode);
+
+ // Load the ClientID.
+ this._clientID = yield ClientID.getClientID();
+
+ // Perform TelemetrySession delayed init.
+ yield TelemetrySession.delayedInit();
+ // Purge the pings archive by removing outdated pings. We don't wait for
+ // this task to complete, but TelemetryStorage blocks on it during
+ // shutdown.
+ TelemetryStorage.runCleanPingArchiveTask();
+
+ // Now that FHR/healthreporter is gone, make sure to remove FHR's DB from
+ // the profile directory. This is a temporary measure that we should drop
+ // in the future.
+ TelemetryStorage.removeFHRDatabase();
+
+ this._delayedInitTaskDeferred.resolve();
+ } catch (e) {
+ this._delayedInitTaskDeferred.reject(e);
+ } finally {
+ this._delayedInitTask = null;
+ }
+ }.bind(this), this._testMode ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
+
+ AsyncShutdown.sendTelemetry.addBlocker("TelemetryController: shutting down",
+ () => this.shutdown(),
+ () => this._getState());
+
+ this._delayedInitTask.arm();
+ return this._delayedInitTaskDeferred.promise;
+ },
+
+ /**
+ * This triggers basic telemetry initialization for content processes.
+ * @param {Boolean} [testing=false] True if we are in test mode, false otherwise.
+ */
+ setupContentTelemetry: function (testing = false) {
+ this._testMode = testing;
+
+ // We call |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags
+ // are in sync between chrome and content processes.
+ if (!this.enableTelemetryRecording()) {
+ this._log.trace("setupContentTelemetry - Content process recording disabled.");
+ return;
+ }
+ TelemetrySession.setupContent(testing);
+ },
+
+ // Do proper shutdown waiting and cleanup.
+ _cleanupOnShutdown: Task.async(function*() {
+ if (!this._initialized) {
+ return;
+ }
+
+ Preferences.ignore(PREF_BRANCH_LOG, configureLogging);
+ this._detachObservers();
+
+ // Now do an orderly shutdown.
+ try {
+ // Stop the datachoices infobar display.
+ TelemetryReportingPolicy.shutdown();
+ TelemetryEnvironment.shutdown();
+
+ // Stop any ping sending.
+ yield TelemetrySend.shutdown();
+
+ yield TelemetrySession.shutdown();
+
+ // First wait for clients processing shutdown.
+ yield this._shutdownBarrier.wait();
+
+ // ... and wait for any outstanding async ping activity.
+ yield this._connectionsBarrier.wait();
+
+ // Perform final shutdown operations.
+ yield TelemetryStorage.shutdown();
+ } finally {
+ // Reset state.
+ this._initialized = false;
+ this._initStarted = false;
+ }
+ }),
+
+ shutdown: function() {
+ this._log.trace("shutdown");
+
+ // We can be in one the following states here:
+ // 1) setupTelemetry was never called
+ // or it was called and
+ // 2) _delayedInitTask was scheduled, but didn't run yet.
+ // 3) _delayedInitTask is running now.
+ // 4) _delayedInitTask finished running already.
+
+ // This handles 1).
+ if (!this._initStarted) {
+ return Promise.resolve();
+ }
+
+ // This handles 4).
+ if (!this._delayedInitTask) {
+ // We already ran the delayed initialization.
+ return this._cleanupOnShutdown();
+ }
+
+ // This handles 2) and 3).
+ return this._delayedInitTask.finalize().then(() => this._cleanupOnShutdown());
+ },
+
+ /**
+ * This observer drives telemetry.
+ */
+ observe: function (aSubject, aTopic, aData) {
+ // The logger might still be not available at this point.
+ if (aTopic == "profile-after-change" || aTopic == "app-startup") {
+ // If we don't have a logger, we need to make sure |Log.repository.getLogger()| is
+ // called before |getLoggerWithMessagePrefix|. Otherwise logging won't work.
+ configureLogging();
+ }
+
+ this._log.trace("observe - " + aTopic + " notified.");
+
+ switch (aTopic) {
+ case "profile-after-change":
+ // profile-after-change is only registered for chrome processes.
+ return this.setupTelemetry();
+ case "app-startup":
+ // app-startup is only registered for content processes.
+ return this.setupContentTelemetry();
+ }
+ return undefined;
+ },
+
+ /**
+ * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+ */
+ _getState: function() {
+ return {
+ initialized: this._initialized,
+ initStarted: this._initStarted,
+ haveDelayedInitTask: !!this._delayedInitTask,
+ shutdownBarrier: this._shutdownBarrier.state,
+ connectionsBarrier: this._connectionsBarrier.state,
+ sendModule: TelemetrySend.getShutdownState(),
+ };
+ },
+
+ /**
+ * Called whenever the FHR Upload preference changes (e.g. when user disables FHR from
+ * the preferences panel), this triggers sending the deletion ping.
+ */
+ _onUploadPrefChange: function() {
+ const uploadEnabled = Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
+ if (uploadEnabled) {
+ // There's nothing we should do if we are enabling upload.
+ return;
+ }
+
+ let p = Task.spawn(function*() {
+ try {
+ // Clear the current pings.
+ yield TelemetrySend.clearCurrentPings();
+
+ // Remove all the pending pings, but not the deletion ping.
+ yield TelemetryStorage.runRemovePendingPingsTask();
+ } catch (e) {
+ this._log.error("_onUploadPrefChange - error clearing pending pings", e);
+ } finally {
+ // Always send the deletion ping.
+ this._log.trace("_onUploadPrefChange - Sending deletion ping.");
+ this.submitExternalPing(PING_TYPE_DELETION, {}, { addClientId: true });
+ }
+ }.bind(this));
+
+ this._shutdownBarrier.client.addBlocker(
+ "TelemetryController: removing pending pings after data upload was disabled", p);
+ },
+
+ _attachObservers: function() {
+ if (IS_UNIFIED_TELEMETRY) {
+ // Watch the FHR upload setting to trigger deletion pings.
+ Preferences.observe(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this);
+ }
+ },
+
+ /**
+ * Remove the preference observer to avoid leaks.
+ */
+ _detachObservers: function() {
+ if (IS_UNIFIED_TELEMETRY) {
+ Preferences.ignore(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this);
+ }
+ },
+
+ /**
+ * Allows waiting for TelemetryControllers delayed initialization to complete.
+ * This will complete before TelemetryController is shutting down.
+ * @return {Promise} Resolved when delayed TelemetryController initialization completed.
+ */
+ promiseInitialized: function() {
+ return this._delayedInitTaskDeferred.promise;
+ },
+
+ getCurrentPingData: function(aSubsession) {
+ this._log.trace("getCurrentPingData - subsession: " + aSubsession)
+
+ // Telemetry is disabled, don't gather any data.
+ if (!Telemetry.canRecordBase) {
+ return null;
+ }
+
+ const reason = aSubsession ? REASON_GATHER_SUBSESSION_PAYLOAD : REASON_GATHER_PAYLOAD;
+ const type = PING_TYPE_MAIN;
+ const payload = TelemetrySession.getPayload(reason);
+ const options = { addClientId: true, addEnvironment: true };
+ const ping = this.assemblePing(type, payload, options);
+
+ return ping;
+ },
+
+ reset: Task.async(function*() {
+ this._clientID = null;
+ this._detachObservers();
+
+ yield TelemetrySession.testReset();
+
+ this._connectionsBarrier = new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for pending ping activity"
+ );
+ this._shutdownBarrier = new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for clients."
+ );
+
+ // We need to kick of the controller setup first for tests that check the
+ // cached client id.
+ let controllerSetup = this.setupTelemetry(true);
+
+ yield TelemetrySend.reset();
+ yield TelemetryStorage.reset();
+ yield TelemetryEnvironment.testReset();
+
+ yield controllerSetup;
+ }),
+};
diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm
new file mode 100644
index 0000000000..e2453649ce
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -0,0 +1,1459 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetryEnvironment",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const myScope = this;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/ObjectUtils.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+const Utils = TelemetryUtils;
+
+XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode",
+ "resource:///modules/AttributionCode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+ "resource://gre/modules/ctypes.jsm");
+if (AppConstants.platform !== "gonk") {
+ Cu.import("resource://gre/modules/AddonManager.jsm");
+ XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm");
+}
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
+ "resource://gre/modules/ProfileAge.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm");
+
+// The maximum length of a string (e.g. description) in the addons section.
+const MAX_ADDON_STRING_LENGTH = 100;
+// The maximum length of a string value in the settings.attribution object.
+const MAX_ATTRIBUTION_STRING_LENGTH = 100;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+ now: () => new Date(),
+};
+
+var gGlobalEnvironment;
+function getGlobal() {
+ if (!gGlobalEnvironment) {
+ gGlobalEnvironment = new EnvironmentCache();
+ }
+ return gGlobalEnvironment;
+}
+
+this.TelemetryEnvironment = {
+ get currentEnvironment() {
+ return getGlobal().currentEnvironment;
+ },
+
+ onInitialized: function() {
+ return getGlobal().onInitialized();
+ },
+
+ delayedInit: function() {
+ return getGlobal().delayedInit();
+ },
+
+ registerChangeListener: function(name, listener) {
+ return getGlobal().registerChangeListener(name, listener);
+ },
+
+ unregisterChangeListener: function(name) {
+ return getGlobal().unregisterChangeListener(name);
+ },
+
+ shutdown: function() {
+ return getGlobal().shutdown();
+ },
+
+ // Policy to use when saving preferences. Exported for using them in tests.
+ RECORD_PREF_STATE: 1, // Don't record the preference value
+ RECORD_PREF_VALUE: 2, // We only record user-set prefs.
+
+ // Testing method
+ testWatchPreferences: function(prefMap) {
+ return getGlobal()._watchPreferences(prefMap);
+ },
+
+ /**
+ * Intended for use in tests only.
+ *
+ * In multiple tests we need a way to shut and re-start telemetry together
+ * with TelemetryEnvironment. This is problematic due to the fact that
+ * TelemetryEnvironment is a singleton. We, therefore, need this helper
+ * method to be able to re-set TelemetryEnvironment.
+ */
+ testReset: function() {
+ return getGlobal().reset();
+ },
+
+ /**
+ * Intended for use in tests only.
+ */
+ testCleanRestart: function() {
+ getGlobal().shutdown();
+ gGlobalEnvironment = null;
+ return getGlobal();
+ },
+};
+
+const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
+const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
+const DEFAULT_ENVIRONMENT_PREFS = new Map([
+ ["app.feedback.baseURL", {what: RECORD_PREF_VALUE}],
+ ["app.support.baseURL", {what: RECORD_PREF_VALUE}],
+ ["accessibility.browsewithcaret", {what: RECORD_PREF_VALUE}],
+ ["accessibility.force_disabled", {what: RECORD_PREF_VALUE}],
+ ["app.update.auto", {what: RECORD_PREF_VALUE}],
+ ["app.update.enabled", {what: RECORD_PREF_VALUE}],
+ ["app.update.interval", {what: RECORD_PREF_VALUE}],
+ ["app.update.service.enabled", {what: RECORD_PREF_VALUE}],
+ ["app.update.silent", {what: RECORD_PREF_VALUE}],
+ ["app.update.url", {what: RECORD_PREF_VALUE}],
+ ["browser.cache.disk.enable", {what: RECORD_PREF_VALUE}],
+ ["browser.cache.disk.capacity", {what: RECORD_PREF_VALUE}],
+ ["browser.cache.memory.enable", {what: RECORD_PREF_VALUE}],
+ ["browser.cache.offline.enable", {what: RECORD_PREF_VALUE}],
+ ["browser.formfill.enable", {what: RECORD_PREF_VALUE}],
+ ["browser.newtab.url", {what: RECORD_PREF_STATE}],
+ ["browser.newtabpage.enabled", {what: RECORD_PREF_VALUE}],
+ ["browser.newtabpage.enhanced", {what: RECORD_PREF_VALUE}],
+ ["browser.shell.checkDefaultBrowser", {what: RECORD_PREF_VALUE}],
+ ["browser.search.suggest.enabled", {what: RECORD_PREF_VALUE}],
+ ["browser.startup.homepage", {what: RECORD_PREF_STATE}],
+ ["browser.startup.page", {what: RECORD_PREF_VALUE}],
+ ["browser.tabs.animate", {what: RECORD_PREF_VALUE}],
+ ["browser.urlbar.suggest.searches", {what: RECORD_PREF_VALUE}],
+ ["browser.urlbar.userMadeSearchSuggestionsChoice", {what: RECORD_PREF_VALUE}],
+ // Record "Zoom Text Only" pref in Firefox 50 to 52 (Bug 979323).
+ ["browser.zoom.full", {what: RECORD_PREF_VALUE}],
+ ["devtools.chrome.enabled", {what: RECORD_PREF_VALUE}],
+ ["devtools.debugger.enabled", {what: RECORD_PREF_VALUE}],
+ ["devtools.debugger.remote-enabled", {what: RECORD_PREF_VALUE}],
+ ["dom.ipc.plugins.asyncInit.enabled", {what: RECORD_PREF_VALUE}],
+ ["dom.ipc.plugins.enabled", {what: RECORD_PREF_VALUE}],
+ ["dom.ipc.processCount", {what: RECORD_PREF_VALUE, requiresRestart: true}],
+ ["dom.max_script_run_time", {what: RECORD_PREF_VALUE}],
+ ["experiments.manifest.uri", {what: RECORD_PREF_VALUE}],
+ ["extensions.autoDisableScopes", {what: RECORD_PREF_VALUE}],
+ ["extensions.enabledScopes", {what: RECORD_PREF_VALUE}],
+ ["extensions.blocklist.enabled", {what: RECORD_PREF_VALUE}],
+ ["extensions.blocklist.url", {what: RECORD_PREF_VALUE}],
+ ["extensions.strictCompatibility", {what: RECORD_PREF_VALUE}],
+ ["extensions.update.enabled", {what: RECORD_PREF_VALUE}],
+ ["extensions.update.url", {what: RECORD_PREF_VALUE}],
+ ["extensions.update.background.url", {what: RECORD_PREF_VALUE}],
+ ["general.smoothScroll", {what: RECORD_PREF_VALUE}],
+ ["gfx.direct2d.disabled", {what: RECORD_PREF_VALUE}],
+ ["gfx.direct2d.force-enabled", {what: RECORD_PREF_VALUE}],
+ ["gfx.direct2d.use1_1", {what: RECORD_PREF_VALUE}],
+ ["layers.acceleration.disabled", {what: RECORD_PREF_VALUE}],
+ ["layers.acceleration.force-enabled", {what: RECORD_PREF_VALUE}],
+ ["layers.async-pan-zoom.enabled", {what: RECORD_PREF_VALUE}],
+ ["layers.async-video-oop.enabled", {what: RECORD_PREF_VALUE}],
+ ["layers.async-video.enabled", {what: RECORD_PREF_VALUE}],
+ ["layers.componentalpha.enabled", {what: RECORD_PREF_VALUE}],
+ ["layers.d3d11.disable-warp", {what: RECORD_PREF_VALUE}],
+ ["layers.d3d11.force-warp", {what: RECORD_PREF_VALUE}],
+ ["layers.offmainthreadcomposition.force-disabled", {what: RECORD_PREF_VALUE}],
+ ["layers.prefer-d3d9", {what: RECORD_PREF_VALUE}],
+ ["layers.prefer-opengl", {what: RECORD_PREF_VALUE}],
+ ["layout.css.devPixelsPerPx", {what: RECORD_PREF_VALUE}],
+ ["network.proxy.autoconfig_url", {what: RECORD_PREF_STATE}],
+ ["network.proxy.http", {what: RECORD_PREF_STATE}],
+ ["network.proxy.ssl", {what: RECORD_PREF_STATE}],
+ ["pdfjs.disabled", {what: RECORD_PREF_VALUE}],
+ ["places.history.enabled", {what: RECORD_PREF_VALUE}],
+ ["privacy.trackingprotection.enabled", {what: RECORD_PREF_VALUE}],
+ ["privacy.donottrackheader.enabled", {what: RECORD_PREF_VALUE}],
+ ["services.sync.serverURL", {what: RECORD_PREF_STATE}],
+ ["security.mixed_content.block_active_content", {what: RECORD_PREF_VALUE}],
+ ["security.mixed_content.block_display_content", {what: RECORD_PREF_VALUE}],
+ ["security.sandbox.content.level", {what: RECORD_PREF_VALUE}],
+ ["xpinstall.signatures.required", {what: RECORD_PREF_VALUE}],
+]);
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_DISTRIBUTOR = "app.distributor";
+const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
+const PREF_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion";
+const PREF_APP_PARTNER_BRANCH = "app.partner.";
+const PREF_PARTNER_ID = "mozilla.partner.id";
+const PREF_UPDATE_ENABLED = "app.update.enabled";
+const PREF_UPDATE_AUTODOWNLOAD = "app.update.auto";
+const PREF_SEARCH_COHORT = "browser.search.cohort";
+const PREF_E10S_COHORT = "e10s.rollout.cohort";
+
+const COMPOSITOR_CREATED_TOPIC = "compositor:created";
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete";
+const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed";
+const GFX_FEATURES_READY_TOPIC = "gfx-features-ready";
+const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified";
+const SEARCH_SERVICE_TOPIC = "browser-search-service";
+
+/**
+ * Enforces the parameter to a boolean value.
+ * @param aValue The input value.
+ * @return {Boolean|Object} If aValue is a boolean or a number, returns its truthfulness
+ * value. Otherwise, return null.
+ */
+function enforceBoolean(aValue) {
+ if (typeof(aValue) !== "number" && typeof(aValue) !== "boolean") {
+ return null;
+ }
+ return (new Boolean(aValue)).valueOf();
+}
+
+/**
+ * Get the current browser.
+ * @return a string with the locale or null on failure.
+ */
+function getBrowserLocale() {
+ try {
+ return Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry).
+ getSelectedLocale('global');
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS locale.
+ * @return a string with the OS locale or null on failure.
+ */
+function getSystemLocale() {
+ try {
+ return Services.locale.getLocaleComponentForUserAgent();
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Asynchronously get a list of addons of the specified type from the AddonManager.
+ * @param aTypes An array containing the types of addons to request.
+ * @return Promise<Array> resolved when AddonManager has finished, returning an
+ * array of addons.
+ */
+function promiseGetAddonsByTypes(aTypes) {
+ return new Promise((resolve) =>
+ AddonManager.getAddonsByTypes(aTypes, (addons) => resolve(addons)));
+}
+
+/**
+ * Safely get a sysinfo property and return its value. If the property is not
+ * available, return aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getSysinfoProperty(aPropertyName, aDefault) {
+ try {
+ // |getProperty| may throw if |aPropertyName| does not exist.
+ return Services.sysinfo.getProperty(aPropertyName);
+ } catch (e) {}
+
+ return aDefault;
+}
+
+/**
+ * Safely get a gfxInfo field and return its value. If the field is not available, return
+ * aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getGfxField(aPropertyName, aDefault) {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ try {
+ // Accessing the field may throw if |aPropertyName| does not exist.
+ let gfxProp = gfxInfo[aPropertyName];
+ if (gfxProp !== undefined && gfxProp !== "") {
+ return gfxProp;
+ }
+ } catch (e) {}
+
+ return aDefault;
+}
+
+/**
+ * Returns a substring of the input string.
+ *
+ * @param {String} aString The input string.
+ * @param {Integer} aMaxLength The maximum length of the returned substring. If this is
+ * greater than the length of the input string, we return the whole input string.
+ * @return {String} The substring or null if the input string is null.
+ */
+function limitStringToLength(aString, aMaxLength) {
+ if (typeof(aString) !== "string") {
+ return null;
+ }
+ return aString.substring(0, aMaxLength);
+}
+
+/**
+ * Force a value to be a string.
+ * Only if the value is null, null is returned instead.
+ */
+function forceToStringOrNull(aValue) {
+ if (aValue === null) {
+ return null;
+ }
+
+ return String(aValue);
+}
+
+/**
+ * Get the information about a graphic adapter.
+ *
+ * @param aSuffix A suffix to add to the properties names.
+ * @return An object containing the adapter properties.
+ */
+function getGfxAdapter(aSuffix = "") {
+ // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures,
+ // not null.
+ let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10);
+ if (Number.isNaN(memoryMB)) {
+ memoryMB = null;
+ }
+
+ return {
+ description: getGfxField("adapterDescription" + aSuffix, null),
+ vendorID: getGfxField("adapterVendorID" + aSuffix, null),
+ deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
+ subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
+ RAM: memoryMB,
+ driver: getGfxField("adapterDriver" + aSuffix, null),
+ driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
+ driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
+ };
+}
+
+/**
+ * Gets the service pack and build information on Windows platforms. The initial version
+ * was copied from nsUpdateService.js.
+ *
+ * @return An object containing the service pack major and minor versions, along with the
+ * build number.
+ */
+function getWindowsVersionInfo() {
+ const UNKNOWN_VERSION_INFO = {servicePackMajor: null, servicePackMinor: null, buildNumber: null};
+
+ if (AppConstants.platform !== "win") {
+ return UNKNOWN_VERSION_INFO;
+ }
+
+ const BYTE = ctypes.uint8_t;
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+ const WCHAR = ctypes.char16_t;
+ const BOOL = ctypes.int;
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+ const SZCSDVERSIONLENGTH = 128;
+ const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+ [
+ {dwOSVersionInfoSize: DWORD},
+ {dwMajorVersion: DWORD},
+ {dwMinorVersion: DWORD},
+ {dwBuildNumber: DWORD},
+ {dwPlatformId: DWORD},
+ {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+ {wServicePackMajor: WORD},
+ {wServicePackMinor: WORD},
+ {wSuiteMask: WORD},
+ {wProductType: BYTE},
+ {wReserved: BYTE}
+ ]);
+
+ let kernel32 = ctypes.open("kernel32");
+ try {
+ let GetVersionEx = kernel32.declare("GetVersionExW",
+ ctypes.default_abi,
+ BOOL,
+ OSVERSIONINFOEXW.ptr);
+ let winVer = OSVERSIONINFOEXW();
+ winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+ if (0 === GetVersionEx(winVer.address())) {
+ throw ("Failure in GetVersionEx (returned 0)");
+ }
+
+ return {
+ servicePackMajor: winVer.wServicePackMajor,
+ servicePackMinor: winVer.wServicePackMinor,
+ buildNumber: winVer.dwBuildNumber,
+ };
+ } catch (e) {
+ return UNKNOWN_VERSION_INFO;
+ } finally {
+ kernel32.close();
+ }
+}
+
+/**
+ * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
+ * is owned by a parent environment object and is an addon listener.
+ */
+function EnvironmentAddonBuilder(environment) {
+ this._environment = environment;
+
+ // The pending task blocks addon manager shutdown. It can either be the initial load
+ // or a change load.
+ this._pendingTask = null;
+
+ // Set to true once initial load is complete and we're watching for changes.
+ this._loaded = false;
+}
+EnvironmentAddonBuilder.prototype = {
+ /**
+ * Get the initial set of addons.
+ * @returns Promise<void> when the initial load is complete.
+ */
+ init: function() {
+ // Some tests don't initialize the addon manager. This accounts for the
+ // unfortunate reality of life.
+ try {
+ AddonManager.shutdown.addBlocker("EnvironmentAddonBuilder",
+ () => this._shutdownBlocker());
+ } catch (err) {
+ return Promise.reject(err);
+ }
+
+ this._pendingTask = this._updateAddons().then(
+ () => { this._pendingTask = null; },
+ (err) => {
+ this._environment._log.error("init - Exception in _updateAddons", err);
+ this._pendingTask = null;
+ }
+ );
+
+ return this._pendingTask;
+ },
+
+ /**
+ * Register an addon listener and watch for changes.
+ */
+ watchForChanges: function() {
+ this._loaded = true;
+ AddonManager.addAddonListener(this);
+ Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
+ },
+
+ // AddonListener
+ onEnabled: function() {
+ this._onAddonChange();
+ },
+ onDisabled: function() {
+ this._onAddonChange();
+ },
+ onInstalled: function() {
+ this._onAddonChange();
+ },
+ onUninstalling: function() {
+ this._onAddonChange();
+ },
+
+ _onAddonChange: function() {
+ this._environment._log.trace("_onAddonChange");
+ this._checkForChanges("addons-changed");
+ },
+
+ // nsIObserver
+ observe: function (aSubject, aTopic, aData) {
+ this._environment._log.trace("observe - Topic " + aTopic);
+ this._checkForChanges("experiment-changed");
+ },
+
+ _checkForChanges: function(changeReason) {
+ if (this._pendingTask) {
+ this._environment._log.trace("_checkForChanges - task already pending, dropping change with reason " + changeReason);
+ return;
+ }
+
+ this._pendingTask = this._updateAddons().then(
+ (result) => {
+ this._pendingTask = null;
+ if (result.changed) {
+ this._environment._onEnvironmentChange(changeReason, result.oldEnvironment);
+ }
+ },
+ (err) => {
+ this._pendingTask = null;
+ this._environment._log.error("_checkForChanges: Error collecting addons", err);
+ });
+ },
+
+ _shutdownBlocker: function() {
+ if (this._loaded) {
+ AddonManager.removeAddonListener(this);
+ Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
+ }
+ return this._pendingTask;
+ },
+
+ /**
+ * Collect the addon data for the environment.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ *
+ * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
+ * changed - Whether the environment changed.
+ * oldEnvironment - Only set if a change occured, contains the environment data before the change.
+ */
+ _updateAddons: Task.async(function* () {
+ this._environment._log.trace("_updateAddons");
+ let personaId = null;
+ if (AppConstants.platform !== "gonk") {
+ let theme = LightweightThemeManager.currentTheme;
+ if (theme) {
+ personaId = theme.id;
+ }
+ }
+
+ let addons = {
+ activeAddons: yield this._getActiveAddons(),
+ theme: yield this._getActiveTheme(),
+ activePlugins: this._getActivePlugins(),
+ activeGMPlugins: yield this._getActiveGMPlugins(),
+ activeExperiment: this._getActiveExperiment(),
+ persona: personaId,
+ };
+
+ let result = {
+ changed: !this._environment._currentEnvironment.addons ||
+ !ObjectUtils.deepEqual(addons, this._environment._currentEnvironment.addons),
+ };
+
+ if (result.changed) {
+ this._environment._log.trace("_updateAddons: addons differ");
+ result.oldEnvironment = Cu.cloneInto(this._environment._currentEnvironment, myScope);
+ this._environment._currentEnvironment.addons = addons;
+ }
+
+ return result;
+ }),
+
+ /**
+ * Get the addon data in object form.
+ * @return Promise<object> containing the addon data.
+ */
+ _getActiveAddons: Task.async(function* () {
+ // Request addons, asynchronously.
+ let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
+
+ let activeAddons = {};
+ for (let addon of allAddons) {
+ // Skip addons which are not active.
+ if (!addon.isActive) {
+ continue;
+ }
+
+ // Weird addon data in the wild can lead to exceptions while collecting
+ // the data.
+ try {
+ // Make sure to have valid dates.
+ let installDate = new Date(Math.max(0, addon.installDate));
+ let updateDate = new Date(Math.max(0, addon.updateDate));
+
+ activeAddons[addon.id] = {
+ blocklisted: (addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+ description: limitStringToLength(addon.description, MAX_ADDON_STRING_LENGTH),
+ name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
+ userDisabled: enforceBoolean(addon.userDisabled),
+ appDisabled: addon.appDisabled,
+ version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
+ scope: addon.scope,
+ type: addon.type,
+ foreignInstall: enforceBoolean(addon.foreignInstall),
+ hasBinaryComponents: addon.hasBinaryComponents,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ signedState: addon.signedState,
+ isSystem: addon.isSystem,
+ };
+
+ if (addon.signedState !== undefined)
+ activeAddons[addon.id].signedState = addon.signedState;
+
+ } catch (ex) {
+ this._environment._log.error("_getActiveAddons - An addon was discarded due to an error", ex);
+ continue;
+ }
+ }
+
+ return activeAddons;
+ }),
+
+ /**
+ * Get the currently active theme data in object form.
+ * @return Promise<object> containing the active theme data.
+ */
+ _getActiveTheme: Task.async(function* () {
+ // Request themes, asynchronously.
+ let themes = yield promiseGetAddonsByTypes(["theme"]);
+
+ let activeTheme = {};
+ // We only store information about the active theme.
+ let theme = themes.find(theme => theme.isActive);
+ if (theme) {
+ // Make sure to have valid dates.
+ let installDate = new Date(Math.max(0, theme.installDate));
+ let updateDate = new Date(Math.max(0, theme.updateDate));
+
+ activeTheme = {
+ id: theme.id,
+ blocklisted: (theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED),
+ description: limitStringToLength(theme.description, MAX_ADDON_STRING_LENGTH),
+ name: limitStringToLength(theme.name, MAX_ADDON_STRING_LENGTH),
+ userDisabled: enforceBoolean(theme.userDisabled),
+ appDisabled: theme.appDisabled,
+ version: limitStringToLength(theme.version, MAX_ADDON_STRING_LENGTH),
+ scope: theme.scope,
+ foreignInstall: enforceBoolean(theme.foreignInstall),
+ hasBinaryComponents: theme.hasBinaryComponents,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ };
+ }
+
+ return activeTheme;
+ }),
+
+ /**
+ * Get the plugins data in object form.
+ * @return Object containing the plugins data.
+ */
+ _getActivePlugins: function () {
+ let pluginTags =
+ Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost).getPluginTags({});
+
+ let activePlugins = [];
+ for (let tag of pluginTags) {
+ // Skip plugins which are not active.
+ if (tag.disabled) {
+ continue;
+ }
+
+ try {
+ // Make sure to have a valid date.
+ let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
+
+ activePlugins.push({
+ name: limitStringToLength(tag.name, MAX_ADDON_STRING_LENGTH),
+ version: limitStringToLength(tag.version, MAX_ADDON_STRING_LENGTH),
+ description: limitStringToLength(tag.description, MAX_ADDON_STRING_LENGTH),
+ blocklisted: tag.blocklisted,
+ disabled: tag.disabled,
+ clicktoplay: tag.clicktoplay,
+ mimeTypes: tag.getMimeTypes({}),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ });
+ } catch (ex) {
+ this._environment._log.error("_getActivePlugins - A plugin was discarded due to an error", ex);
+ continue;
+ }
+ }
+
+ return activePlugins;
+ },
+
+ /**
+ * Get the GMPlugins data in object form.
+ * @return Object containing the GMPlugins data.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ */
+ _getActiveGMPlugins: Task.async(function* () {
+ // Request plugins, asynchronously.
+ let allPlugins = yield promiseGetAddonsByTypes(["plugin"]);
+
+ let activeGMPlugins = {};
+ for (let plugin of allPlugins) {
+ // Only get info for active GMplugins.
+ if (!plugin.isGMPlugin || !plugin.isActive) {
+ continue;
+ }
+
+ try {
+ activeGMPlugins[plugin.id] = {
+ version: plugin.version,
+ userDisabled: enforceBoolean(plugin.userDisabled),
+ applyBackgroundUpdates: plugin.applyBackgroundUpdates,
+ };
+ } catch (ex) {
+ this._environment._log.error("_getActiveGMPlugins - A GMPlugin was discarded due to an error", ex);
+ continue;
+ }
+ }
+
+ return activeGMPlugins;
+ }),
+
+ /**
+ * Get the active experiment data in object form.
+ * @return Object containing the active experiment data.
+ */
+ _getActiveExperiment: function () {
+ let experimentInfo = {};
+ try {
+ let scope = {};
+ Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
+ let experiments = scope.Experiments.instance();
+ let activeExperiment = experiments.getActiveExperimentID();
+ if (activeExperiment) {
+ experimentInfo.id = activeExperiment;
+ experimentInfo.branch = experiments.getActiveExperimentBranch();
+ }
+ } catch (e) {
+ // If this is not Firefox, the import will fail.
+ }
+
+ return experimentInfo;
+ },
+};
+
+function EnvironmentCache() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME, "TelemetryEnvironment::");
+ this._log.trace("constructor");
+
+ this._shutdown = false;
+ this._delayedInitFinished = false;
+
+ // A map of listeners that will be called on environment changes.
+ this._changeListeners = new Map();
+
+ // A map of watched preferences which trigger an Environment change when
+ // modified. Every entry contains a recording policy (RECORD_PREF_*).
+ this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
+
+ this._currentEnvironment = {
+ build: this._getBuild(),
+ partner: this._getPartner(),
+ system: this._getSystem(),
+ };
+
+ this._updateSettings();
+ // Fill in the default search engine, if the search provider is already initialized.
+ this._updateSearchEngine();
+ this._addObservers();
+
+ // Build the remaining asynchronous parts of the environment. Don't register change listeners
+ // until the initial environment has been built.
+
+ let p = [];
+ if (AppConstants.platform === "gonk") {
+ this._addonBuilder = {
+ watchForChanges: function() {}
+ };
+ } else {
+ this._addonBuilder = new EnvironmentAddonBuilder(this);
+ p = [ this._addonBuilder.init() ];
+ }
+
+ this._currentEnvironment.profile = {};
+ p.push(this._updateProfile());
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ p.push(this._updateAttribution());
+ }
+
+ let setup = () => {
+ this._initTask = null;
+ this._startWatchingPrefs();
+ this._addonBuilder.watchForChanges();
+ this._updateGraphicsFeatures();
+ return this.currentEnvironment;
+ };
+
+ this._initTask = Promise.all(p)
+ .then(
+ () => setup(),
+ (err) => {
+ // log errors but eat them for consumers
+ this._log.error("EnvironmentCache - error while initializing", err);
+ return setup();
+ });
+}
+EnvironmentCache.prototype = {
+ /**
+ * The current environment data. The returned data is cloned to avoid
+ * unexpected sharing or mutation.
+ * @returns object
+ */
+ get currentEnvironment() {
+ return Cu.cloneInto(this._currentEnvironment, myScope);
+ },
+
+ /**
+ * Wait for the current enviroment to be fully initialized.
+ * @returns Promise<object>
+ */
+ onInitialized: function() {
+ if (this._initTask) {
+ return this._initTask;
+ }
+ return Promise.resolve(this.currentEnvironment);
+ },
+
+ /**
+ * This gets called when the delayed init completes.
+ */
+ delayedInit: function() {
+ this._delayedInitFinished = true;
+ },
+
+ /**
+ * Register a listener for environment changes.
+ * @param name The name of the listener. If a new listener is registered
+ * with the same name, the old listener will be replaced.
+ * @param listener function(reason, oldEnvironment) - Will receive a reason for
+ the change and the environment data before the change.
+ */
+ registerChangeListener: function (name, listener) {
+ this._log.trace("registerChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.set(name, listener);
+ },
+
+ /**
+ * Unregister from listening to environment changes.
+ * It's fine to call this on an unitialized TelemetryEnvironment.
+ * @param name The name of the listener to remove.
+ */
+ unregisterChangeListener: function (name) {
+ this._log.trace("unregisterChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.delete(name);
+ },
+
+ shutdown: function() {
+ this._log.trace("shutdown");
+ this._shutdown = true;
+ },
+
+ /**
+ * Only used in tests, set the preferences to watch.
+ * @param aPreferences A map of preferences names and their recording policy.
+ */
+ _watchPreferences: function (aPreferences) {
+ this._stopWatchingPrefs();
+ this._watchedPrefs = aPreferences;
+ this._updateSettings();
+ this._startWatchingPrefs();
+ },
+
+ /**
+ * Get an object containing the values for the watched preferences. Depending on the
+ * policy, the value for a preference or whether it was changed by user is reported.
+ *
+ * @return An object containing the preferences values.
+ */
+ _getPrefData: function () {
+ let prefData = {};
+ for (let [pref, policy] of this._watchedPrefs.entries()) {
+ // Only record preferences if they are non-default
+ if (!Preferences.isSet(pref)) {
+ continue;
+ }
+
+ // Check the policy for the preference and decide if we need to store its value
+ // or whether it changed from the default value.
+ let prefValue = undefined;
+ if (policy.what == TelemetryEnvironment.RECORD_PREF_STATE) {
+ prefValue = "<user-set>";
+ } else {
+ prefValue = Preferences.get(pref, null);
+ }
+ prefData[pref] = prefValue;
+ }
+ return prefData;
+ },
+
+ /**
+ * Start watching the preferences.
+ */
+ _startWatchingPrefs: function () {
+ this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
+
+ for (let [pref, options] of this._watchedPrefs) {
+ if (!("requiresRestart" in options) || !options.requiresRestart) {
+ Preferences.observe(pref, this._onPrefChanged, this);
+ }
+ }
+ },
+
+ _onPrefChanged: function() {
+ this._log.trace("_onPrefChanged");
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ this._updateSettings();
+ this._onEnvironmentChange("pref-changed", oldEnvironment);
+ },
+
+ /**
+ * Do not receive any more change notifications for the preferences.
+ */
+ _stopWatchingPrefs: function () {
+ this._log.trace("_stopWatchingPrefs");
+
+ for (let [pref, options] of this._watchedPrefs) {
+ if (!("requiresRestart" in options) || !options.requiresRestart) {
+ Preferences.ignore(pref, this._onPrefChanged, this);
+ }
+ }
+ },
+
+ _addObservers: function () {
+ // Watch the search engine change and service topics.
+ Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC, false);
+ Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC, false);
+ Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC, false);
+ Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC, false);
+ Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC, false);
+ },
+
+ _removeObservers: function () {
+ Services.obs.removeObserver(this, COMPOSITOR_CREATED_TOPIC);
+ try {
+ Services.obs.removeObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+ } catch (ex) {}
+ Services.obs.removeObserver(this, GFX_FEATURES_READY_TOPIC);
+ Services.obs.removeObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
+ Services.obs.removeObserver(this, SEARCH_SERVICE_TOPIC);
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData);
+ switch (aTopic) {
+ case SEARCH_ENGINE_MODIFIED_TOPIC:
+ if (aData != "engine-current") {
+ return;
+ }
+ // Record the new default search choice and send the change notification.
+ this._onSearchEngineChange();
+ break;
+ case SEARCH_SERVICE_TOPIC:
+ if (aData != "init-complete") {
+ return;
+ }
+ // Now that the search engine init is complete, record the default search choice.
+ this._updateSearchEngine();
+ break;
+ case GFX_FEATURES_READY_TOPIC:
+ case COMPOSITOR_CREATED_TOPIC:
+ // Full graphics information is not available until we have created at
+ // least one off-main-thread-composited window. Thus we wait for the
+ // first compositor to be created and then query nsIGfxInfo again.
+ this._updateGraphicsFeatures();
+ break;
+ case DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC:
+ // Distribution customizations are applied after final-ui-startup. query
+ // partner prefs again when they are ready.
+ this._updatePartner();
+ Services.obs.removeObserver(this, aTopic);
+ break;
+ }
+ },
+
+ /**
+ * Get the default search engine.
+ * @return {String} Returns the search engine identifier, "NONE" if no default search
+ * engine is defined or "UNDEFINED" if no engine identifier or name can be found.
+ */
+ _getDefaultSearchEngine: function () {
+ let engine;
+ try {
+ engine = Services.search.defaultEngine;
+ } catch (e) {}
+
+ let name;
+ if (!engine) {
+ name = "NONE";
+ } else if (engine.identifier) {
+ name = engine.identifier;
+ } else if (engine.name) {
+ name = "other-" + engine.name;
+ } else {
+ name = "UNDEFINED";
+ }
+
+ return name;
+ },
+
+ /**
+ * Update the default search engine value.
+ */
+ _updateSearchEngine: function () {
+ if (!Services.search) {
+ // Just ignore cases where the search service is not implemented.
+ return;
+ }
+
+ this._log.trace("_updateSearchEngine - isInitialized: " + Services.search.isInitialized);
+ if (!Services.search.isInitialized) {
+ return;
+ }
+
+ // Make sure we have a settings section.
+ this._currentEnvironment.settings = this._currentEnvironment.settings || {};
+ // Update the search engine entry in the current environment.
+ this._currentEnvironment.settings.defaultSearchEngine = this._getDefaultSearchEngine();
+ this._currentEnvironment.settings.defaultSearchEngineData =
+ Services.search.getDefaultEngineInfo();
+
+ // Record the cohort identifier used for search defaults A/B testing.
+ if (Services.prefs.prefHasUserValue(PREF_SEARCH_COHORT))
+ this._currentEnvironment.settings.searchCohort = Services.prefs.getCharPref(PREF_SEARCH_COHORT);
+ },
+
+ /**
+ * Update the default search engine value and trigger the environment change.
+ */
+ _onSearchEngineChange: function () {
+ this._log.trace("_onSearchEngineChange");
+
+ // Finally trigger the environment change notification.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ this._updateSearchEngine();
+ this._onEnvironmentChange("search-engine-changed", oldEnvironment);
+ },
+
+ /**
+ * Update the graphics features object.
+ */
+ _updateGraphicsFeatures: function () {
+ let gfxData = this._currentEnvironment.system.gfx;
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ gfxData.features = gfxInfo.getFeatures();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getFeatures() caught error", e);
+ }
+ },
+
+ /**
+ * Update the partner prefs.
+ */
+ _updatePartner: function() {
+ this._currentEnvironment.partner = this._getPartner();
+ },
+
+ /**
+ * Get the build data in object form.
+ * @return Object containing the build data.
+ */
+ _getBuild: function () {
+ let buildData = {
+ applicationId: Services.appinfo.ID || null,
+ applicationName: Services.appinfo.name || null,
+ architecture: Services.sysinfo.get("arch"),
+ buildId: Services.appinfo.appBuildID || null,
+ version: Services.appinfo.version || null,
+ vendor: Services.appinfo.vendor || null,
+ platformVersion: Services.appinfo.platformVersion || null,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ hotfixVersion: Preferences.get(PREF_HOTFIX_LASTVERSION, null),
+ };
+
+ // Add |architecturesInBinary| only for Mac Universal builds.
+ if ("@mozilla.org/xpcom/mac-utils;1" in Cc) {
+ let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils);
+ if (macUtils && macUtils.isUniversalBinary) {
+ buildData.architecturesInBinary = macUtils.architecturesInBinary;
+ }
+ }
+
+ return buildData;
+ },
+
+ /**
+ * Determine if we're the default browser.
+ * @returns null on error, true if we are the default browser, or false otherwise.
+ */
+ _isDefaultBrowser: function () {
+ if (AppConstants.platform === "gonk") {
+ return true;
+ }
+
+ if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
+ this._log.info("_isDefaultBrowser - Could not obtain browser shell service");
+ return null;
+ }
+
+ let shellService;
+ try {
+ let scope = {};
+ Cu.import("resource:///modules/ShellService.jsm", scope);
+ shellService = scope.ShellService;
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service JSM");
+ }
+
+ if (!shellService) {
+ try {
+ shellService = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
+ return null;
+ }
+ }
+
+ try {
+ // This uses the same set of flags used by the pref pane.
+ return shellService.isDefaultBrowser(false, true) ? true : false;
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not determine if default browser", ex);
+ return null;
+ }
+ },
+
+ /**
+ * Update the cached settings data.
+ */
+ _updateSettings: function () {
+ let updateChannel = null;
+ try {
+ updateChannel = UpdateUtils.getUpdateChannel(false);
+ } catch (e) {}
+
+ this._currentEnvironment.settings = {
+ blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true),
+ e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
+ e10sCohort: Preferences.get(PREF_E10S_COHORT, "unknown"),
+ telemetryEnabled: Utils.isTelemetryEnabled,
+ locale: getBrowserLocale(),
+ update: {
+ channel: updateChannel,
+ enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
+ autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
+ },
+ userPrefs: this._getPrefData(),
+ };
+
+ if (AppConstants.platform !== "gonk") {
+ this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
+ AddonManager.checkCompatibility;
+ }
+
+ if (AppConstants.platform !== "android") {
+ this._currentEnvironment.settings.isDefaultBrowser =
+ this._isDefaultBrowser();
+ }
+
+ this._updateSearchEngine();
+ },
+
+ /**
+ * Update the cached profile data.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ _updateProfile: Task.async(function* () {
+ const logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "ProfileAge - ");
+ let profileAccessor = new ProfileAge(null, logger);
+
+ let creationDate = yield profileAccessor.created;
+ let resetDate = yield profileAccessor.reset;
+
+ this._currentEnvironment.profile.creationDate =
+ Utils.millisecondsToDays(creationDate);
+ if (resetDate) {
+ this._currentEnvironment.profile.resetDate =
+ Utils.millisecondsToDays(resetDate);
+ }
+ }),
+
+ /**
+ * Update the cached attribution data object.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ _updateAttribution: Task.async(function* () {
+ let data = yield AttributionCode.getAttrDataAsync();
+ if (Object.keys(data).length > 0) {
+ this._currentEnvironment.settings.attribution = {};
+ for (let key in data) {
+ this._currentEnvironment.settings.attribution[key] =
+ limitStringToLength(data[key], MAX_ATTRIBUTION_STRING_LENGTH);
+ }
+ }
+ }),
+
+ /**
+ * Get the partner data in object form.
+ * @return Object containing the partner data.
+ */
+ _getPartner: function () {
+ let partnerData = {
+ distributionId: Preferences.get(PREF_DISTRIBUTION_ID, null),
+ distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null),
+ partnerId: Preferences.get(PREF_PARTNER_ID, null),
+ distributor: Preferences.get(PREF_DISTRIBUTOR, null),
+ distributorChannel: Preferences.get(PREF_DISTRIBUTOR_CHANNEL, null),
+ };
+
+ // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
+ let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH);
+ partnerData.partnerNames = partnerBranch.getChildList("");
+
+ return partnerData;
+ },
+
+ /**
+ * Get the CPU information.
+ * @return Object containing the CPU information data.
+ */
+ _getCpuData: function () {
+ let cpuData = {
+ count: getSysinfoProperty("cpucount", null),
+ cores: getSysinfoProperty("cpucores", null),
+ vendor: getSysinfoProperty("cpuvendor", null),
+ family: getSysinfoProperty("cpufamily", null),
+ model: getSysinfoProperty("cpumodel", null),
+ stepping: getSysinfoProperty("cpustepping", null),
+ l2cacheKB: getSysinfoProperty("cpucachel2", null),
+ l3cacheKB: getSysinfoProperty("cpucachel3", null),
+ speedMHz: getSysinfoProperty("cpuspeed", null),
+ };
+
+ const CPU_EXTENSIONS = ["hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3",
+ "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasAVX", "hasAVX2",
+ "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON"];
+
+ // Enumerate the available CPU extensions.
+ let availableExts = [];
+ for (let ext of CPU_EXTENSIONS) {
+ if (getSysinfoProperty(ext, false)) {
+ availableExts.push(ext);
+ }
+ }
+
+ cpuData.extensions = availableExts;
+
+ return cpuData;
+ },
+
+ /**
+ * Get the device information, if we are on a portable device.
+ * @return Object containing the device information data, or null if
+ * not a portable device.
+ */
+ _getDeviceData: function () {
+ if (!["gonk", "android"].includes(AppConstants.platform)) {
+ return null;
+ }
+
+ return {
+ model: getSysinfoProperty("device", null),
+ manufacturer: getSysinfoProperty("manufacturer", null),
+ hardware: getSysinfoProperty("hardware", null),
+ isTablet: getSysinfoProperty("tablet", null),
+ };
+ },
+
+ /**
+ * Get the OS information.
+ * @return Object containing the OS data.
+ */
+ _getOSData: function () {
+ let data = {
+ name: forceToStringOrNull(getSysinfoProperty("name", null)),
+ version: forceToStringOrNull(getSysinfoProperty("version", null)),
+ locale: forceToStringOrNull(getSystemLocale()),
+ };
+
+ if (["gonk", "android"].includes(AppConstants.platform)) {
+ data.kernelVersion = forceToStringOrNull(getSysinfoProperty("kernel_version", null));
+ } else if (AppConstants.platform === "win") {
+ // The path to the "UBR" key, queried to get additional version details on Windows.
+ const WINDOWS_UBR_KEY_PATH = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
+
+ let versionInfo = getWindowsVersionInfo();
+ data.servicePackMajor = versionInfo.servicePackMajor;
+ data.servicePackMinor = versionInfo.servicePackMinor;
+ // We only need the build number and UBR if we're at or above Windows 10.
+ if (typeof(data.version) === 'string' &&
+ Services.vc.compare(data.version, "10") >= 0) {
+ data.windowsBuildNumber = versionInfo.buildNumber;
+ // Query the UBR key and only add it to the environment if it's available.
+ // |readRegKey| doesn't throw, but rather returns 'undefined' on error.
+ let ubr = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ WINDOWS_UBR_KEY_PATH, "UBR",
+ Ci.nsIWindowsRegKey.WOW64_64);
+ data.windowsUBR = (ubr !== undefined) ? ubr : null;
+ }
+ data.installYear = getSysinfoProperty("installYear", null);
+ }
+
+ return data;
+ },
+
+ /**
+ * Get the HDD information.
+ * @return Object containing the HDD data.
+ */
+ _getHDDData: function () {
+ return {
+ profile: { // hdd where the profile folder is located
+ model: getSysinfoProperty("profileHDDModel", null),
+ revision: getSysinfoProperty("profileHDDRevision", null),
+ },
+ binary: { // hdd where the application binary is located
+ model: getSysinfoProperty("binHDDModel", null),
+ revision: getSysinfoProperty("binHDDRevision", null),
+ },
+ system: { // hdd where the system files are located
+ model: getSysinfoProperty("winHDDModel", null),
+ revision: getSysinfoProperty("winHDDRevision", null),
+ },
+ };
+ },
+
+ /**
+ * Get the GFX information.
+ * @return Object containing the GFX data.
+ */
+ _getGFXData: function () {
+ let gfxData = {
+ D2DEnabled: getGfxField("D2DEnabled", null),
+ DWriteEnabled: getGfxField("DWriteEnabled", null),
+ ContentBackend: getGfxField("ContentBackend", null),
+ // The following line is disabled due to main thread jank and will be enabled
+ // again as part of bug 1154500.
+ // DWriteVersion: getGfxField("DWriteVersion", null),
+ adapters: [],
+ monitors: [],
+ features: {},
+ };
+
+ if (!["gonk", "android", "linux"].includes(AppConstants.platform)) {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ try {
+ gfxData.monitors = gfxInfo.getMonitors();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getMonitors() caught error", e);
+ }
+ }
+
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ gfxData.features = gfxInfo.getFeatures();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getFeatures() caught error", e);
+ }
+
+ // GfxInfo does not yet expose a way to iterate through all the adapters.
+ gfxData.adapters.push(getGfxAdapter(""));
+ gfxData.adapters[0].GPUActive = true;
+
+ // If we have a second adapter add it to the gfxData.adapters section.
+ let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null;
+ if (!hasGPU2) {
+ this._log.trace("_getGFXData - Only one display adapter detected.");
+ return gfxData;
+ }
+
+ this._log.trace("_getGFXData - Two display adapters detected.");
+
+ gfxData.adapters.push(getGfxAdapter("2"));
+ gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active", null);
+
+ return gfxData;
+ },
+
+ /**
+ * Get the system data in object form.
+ * @return Object containing the system data.
+ */
+ _getSystem: function () {
+ let memoryMB = getSysinfoProperty("memsize", null);
+ if (memoryMB) {
+ // Send RAM size in megabytes. Rounding because sysinfo doesn't
+ // always provide RAM in multiples of 1024.
+ memoryMB = Math.round(memoryMB / 1024 / 1024);
+ }
+
+ let virtualMB = getSysinfoProperty("virtualmemsize", null);
+ if (virtualMB) {
+ // Send the total virtual memory size in megabytes. Rounding because
+ // sysinfo doesn't always provide RAM in multiples of 1024.
+ virtualMB = Math.round(virtualMB / 1024 / 1024);
+ }
+
+ let data = {
+ memoryMB: memoryMB,
+ virtualMaxMB: virtualMB,
+ cpu: this._getCpuData(),
+ os: this._getOSData(),
+ hdd: this._getHDDData(),
+ gfx: this._getGFXData(),
+ };
+
+ if (AppConstants.platform === "win") {
+ data.isWow64 = getSysinfoProperty("isWow64", null);
+ } else if (["gonk", "android"].includes(AppConstants.platform)) {
+ data.device = this._getDeviceData();
+ }
+
+ return data;
+ },
+
+ _onEnvironmentChange: function (what, oldEnvironment) {
+ this._log.trace("_onEnvironmentChange for " + what);
+
+ // We are already skipping change events in _checkChanges if there is a pending change task running.
+ if (this._shutdown) {
+ this._log.trace("_onEnvironmentChange - Already shut down.");
+ return;
+ }
+
+ for (let [name, listener] of this._changeListeners) {
+ try {
+ this._log.debug("_onEnvironmentChange - calling " + name);
+ listener(what, oldEnvironment);
+ } catch (e) {
+ this._log.error("_onEnvironmentChange - listener " + name + " caught error", e);
+ }
+ }
+ },
+
+ reset: function () {
+ this._shutdown = false;
+ this._delayedInitFinished = false;
+ }
+};
diff --git a/toolkit/components/telemetry/TelemetryEvent.cpp b/toolkit/components/telemetry/TelemetryEvent.cpp
new file mode 100644
index 0000000000..1e8126f662
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryEvent.cpp
@@ -0,0 +1,687 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <prtime.h>
+#include "nsITelemetry.h"
+#include "nsHashKeys.h"
+#include "nsDataHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsTArray.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPtr.h"
+#include "jsapi.h"
+#include "nsJSUtils.h"
+#include "nsXULAppAPI.h"
+#include "nsUTF8Utils.h"
+
+#include "TelemetryCommon.h"
+#include "TelemetryEvent.h"
+#include "TelemetryEventData.h"
+
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::ArrayLength;
+using mozilla::Maybe;
+using mozilla::Nothing;
+using mozilla::Pair;
+using mozilla::StaticAutoPtr;
+using mozilla::Telemetry::Common::AutoHashtable;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::MsSinceProcessStart;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions taking a StaticMutexAutoLock: these can only be reached via
+// an interface function (TelemetryEvent::*). They expect the interface
+// function to have acquired |gTelemetryEventsMutex|, so they do not
+// have to be thread-safe.
+//
+// * Functions named TelemetryEvent::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryEventsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryEvent::*) are
+// serialised using the mutex |gTelemetryEventsMutex|. This means
+// that the external interface is thread-safe, and the internal
+// functions can ignore thread safety. But it also brings a danger
+// of deadlock if any function in the external interface can get back
+// to that interface. That is, we will deadlock on any call chain like
+// this:
+//
+// TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryEvent::* may directly call, nor take the
+// address of, any other function in TelemetryEvent::*.
+//
+// * No internal function may call, nor take the address
+// of, any function in TelemetryEvent::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kEventCount = mozilla::Telemetry::EventID::EventCount;
+// This is a special event id used to mark expired events, to make expiry checks
+// faster at runtime.
+const uint32_t kExpiredEventId = kEventCount + 1;
+static_assert(kEventCount < kExpiredEventId, "Should not overflow.");
+
+// This is the hard upper limit on the number of event records we keep in storage.
+// If we cross this limit, we will drop any further event recording until elements
+// are removed from storage.
+const uint32_t kMaxEventRecords = 1000;
+// Maximum length of any passed value string, in UTF8 byte sequence length.
+const uint32_t kMaxValueByteLength = 80;
+// Maximum length of any string value in the extra dictionary, in UTF8 byte sequence length.
+const uint32_t kMaxExtraValueByteLength = 80;
+
+typedef nsDataHashtable<nsCStringHashKey, uint32_t> EventMapType;
+typedef nsClassHashtable<nsCStringHashKey, nsCString> StringMap;
+
+enum class RecordEventResult {
+ Ok,
+ UnknownEvent,
+ InvalidExtraKey,
+ StorageLimitReached,
+};
+
+struct ExtraEntry {
+ const nsCString key;
+ const nsCString value;
+};
+
+typedef nsTArray<ExtraEntry> ExtraArray;
+
+class EventRecord {
+public:
+ EventRecord(double timestamp, uint32_t eventId, const Maybe<nsCString>& value,
+ const ExtraArray& extra)
+ : mTimestamp(timestamp)
+ , mEventId(eventId)
+ , mValue(value)
+ , mExtra(extra)
+ {}
+
+ EventRecord(const EventRecord& other)
+ : mTimestamp(other.mTimestamp)
+ , mEventId(other.mEventId)
+ , mValue(other.mValue)
+ , mExtra(other.mExtra)
+ {}
+
+ EventRecord& operator=(const EventRecord& other) = delete;
+
+ double Timestamp() const { return mTimestamp; }
+ uint32_t EventId() const { return mEventId; }
+ const Maybe<nsCString>& Value() const { return mValue; }
+ const ExtraArray& Extra() const { return mExtra; }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+private:
+ const double mTimestamp;
+ const uint32_t mEventId;
+ const Maybe<nsCString> mValue;
+ const ExtraArray mExtra;
+};
+
+// Implements the methods for EventInfo.
+const char*
+EventInfo::method() const
+{
+ return &gEventsStringTable[this->method_offset];
+}
+
+const char*
+EventInfo::object() const
+{
+ return &gEventsStringTable[this->object_offset];
+}
+
+// Implements the methods for CommonEventInfo.
+const char*
+CommonEventInfo::category() const
+{
+ return &gEventsStringTable[this->category_offset];
+}
+
+const char*
+CommonEventInfo::expiration_version() const
+{
+ return &gEventsStringTable[this->expiration_version_offset];
+}
+
+const char*
+CommonEventInfo::extra_key(uint32_t index) const
+{
+ MOZ_ASSERT(index < this->extra_count);
+ uint32_t key_index = gExtraKeysTable[this->extra_index + index];
+ return &gEventsStringTable[key_index];
+}
+
+// Implementation for the EventRecord class.
+size_t
+EventRecord::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ size_t n = 0;
+
+ if (mValue) {
+ n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mExtra.Length(); ++i) {
+ n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+nsCString
+UniqueEventName(const nsACString& category, const nsACString& method, const nsACString& object)
+{
+ nsCString name;
+ name.Append(category);
+ name.AppendLiteral("#");
+ name.Append(method);
+ name.AppendLiteral("#");
+ name.Append(object);
+ return name;
+}
+
+nsCString
+UniqueEventName(const EventInfo& info)
+{
+ return UniqueEventName(nsDependentCString(info.common_info.category()),
+ nsDependentCString(info.method()),
+ nsDependentCString(info.object()));
+}
+
+bool
+IsExpiredDate(uint32_t expires_days_since_epoch) {
+ if (expires_days_since_epoch == 0) {
+ return false;
+ }
+
+ const uint32_t days_since_epoch = PR_Now() / (PRTime(PR_USEC_PER_SEC) * 24 * 60 * 60);
+ return expires_days_since_epoch <= days_since_epoch;
+}
+
+void
+TruncateToByteLength(nsCString& str, uint32_t length)
+{
+ // last will be the index of the first byte of the current multi-byte sequence.
+ uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length);
+ str.Truncate(last);
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The Name -> ID cache map.
+EventMapType gEventNameIDMap(kEventCount);
+
+// The main event storage. Events are inserted here in recording order.
+StaticAutoPtr<nsTArray<EventRecord>> gEventRecords;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for event recording.
+
+namespace {
+
+bool
+CanRecordEvent(const StaticMutexAutoLock& lock, const CommonEventInfo& info)
+{
+ if (!gCanRecordBase) {
+ return false;
+ }
+
+ return CanRecordDataset(info.dataset, gCanRecordBase, gCanRecordExtended);
+}
+
+RecordEventResult
+RecordEvent(const StaticMutexAutoLock& lock, double timestamp,
+ const nsACString& category, const nsACString& method,
+ const nsACString& object, const Maybe<nsCString>& value,
+ const ExtraArray& extra)
+{
+ // Apply hard limit on event count in storage.
+ if (gEventRecords->Length() >= kMaxEventRecords) {
+ return RecordEventResult::StorageLimitReached;
+ }
+
+ // Look up the event id.
+ const nsCString& name = UniqueEventName(category, method, object);
+ uint32_t eventId;
+ if (!gEventNameIDMap.Get(name, &eventId)) {
+ return RecordEventResult::UnknownEvent;
+ }
+
+ // If the event is expired, silently drop this call.
+ // We don't want recording for expired probes to be an error so code doesn't
+ // have to be removed at a specific time or version.
+ // Even logging warnings would become very noisy.
+ if (eventId == kExpiredEventId) {
+ return RecordEventResult::Ok;
+ }
+
+ // Check whether we can record this event.
+ const CommonEventInfo& common = gEventInfo[eventId].common_info;
+ if (!CanRecordEvent(lock, common)) {
+ return RecordEventResult::Ok;
+ }
+
+ // Check whether the extra keys passed are valid.
+ nsTHashtable<nsCStringHashKey> validExtraKeys;
+ for (uint32_t i = 0; i < common.extra_count; ++i) {
+ validExtraKeys.PutEntry(nsDependentCString(common.extra_key(i)));
+ }
+
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ if (!validExtraKeys.GetEntry(extra[i].key)) {
+ return RecordEventResult::InvalidExtraKey;
+ }
+ }
+
+ // Add event record.
+ gEventRecords->AppendElement(EventRecord(timestamp, eventId, value, extra));
+ return RecordEventResult::Ok;
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryEventsMutex;
+
+void
+TelemetryEvent::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended)
+{
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(!gInitDone, "TelemetryEvent::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ gEventRecords = new nsTArray<EventRecord>();
+
+ // Populate the static event name->id cache. Note that the event names are
+ // statically allocated and come from the automatically generated TelemetryEventData.h.
+ const uint32_t eventCount = static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+ for (uint32_t i = 0; i < eventCount; ++i) {
+ const EventInfo& info = gEventInfo[i];
+ uint32_t eventId = i;
+
+ // If this event is expired, mark it with a special event id.
+ // This avoids doing expensive expiry checks at runtime.
+ if (IsExpiredVersion(info.common_info.expiration_version()) ||
+ IsExpiredDate(info.common_info.expiration_day)) {
+ eventId = kExpiredEventId;
+ }
+
+ gEventNameIDMap.Put(UniqueEventName(info), eventId);
+ }
+
+#ifdef DEBUG
+ gEventNameIDMap.MarkImmutable();
+#endif
+ gInitDone = true;
+}
+
+void
+TelemetryEvent::DeInitializeGlobalState()
+{
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(gInitDone);
+
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+
+ gEventNameIDMap.Clear();
+ gEventRecords->Clear();
+ gEventRecords = nullptr;
+
+ gInitDone = false;
+}
+
+void
+TelemetryEvent::SetCanRecordBase(bool b)
+{
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordBase = b;
+}
+
+void
+TelemetryEvent::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordExtended = b;
+}
+
+nsresult
+TelemetryEvent::RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
+ const nsACString& aObject, JS::HandleValue aValue,
+ JS::HandleValue aExtra, JSContext* cx,
+ uint8_t optional_argc)
+{
+ // Currently only recording in the parent process is supported.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ nsresult rv = MsSinceProcessStart(&timestamp);
+ if (NS_FAILED(rv)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Failed to get time since process start."));
+ return NS_OK;
+ }
+
+ // Check value argument.
+ if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Invalid type for value parameter."));
+ return NS_OK;
+ }
+
+ // Extract value parameter.
+ Maybe<nsCString> value;
+ if (aValue.isString()) {
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, aValue)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Invalid string value for value parameter."));
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxValueByteLength) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Value parameter exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxValueByteLength);
+ }
+ value = mozilla::Some(str);
+ }
+
+ // Check extra argument.
+ if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Invalid type for extra parameter."));
+ return NS_OK;
+ }
+
+ // Extract extra dictionary.
+ ExtraArray extra;
+ if (aExtra.isObject()) {
+ JS::RootedObject obj(cx, &aExtra.toObject());
+ JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &ids)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Failed to enumerate object."));
+ return NS_OK;
+ }
+
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSString key;
+ if (!key.init(cx, ids[i])) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Extra dictionary should only contain string keys."));
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, ids[i], &value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Failed to get extra property."));
+ return NS_OK;
+ }
+
+ nsAutoJSString jsStr;
+ if (!value.isString() || !jsStr.init(cx, value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Extra properties should have string values."));
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxExtraValueByteLength) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Extra value exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxExtraValueByteLength);
+ }
+
+ extra.AppendElement(ExtraEntry{NS_ConvertUTF16toUTF8(key), str});
+ }
+ }
+
+ // Lock for accessing internal data.
+ // While the lock is being held, no complex calls like JS calls can be made,
+ // as all of these could record Telemetry, which would result in deadlock.
+ RecordEventResult res;
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ res = ::RecordEvent(lock, timestamp, aCategory, aMethod, aObject, value, extra);
+ }
+
+ // Trigger warnings or errors where needed.
+ switch (res) {
+ case RecordEventResult::UnknownEvent: {
+ JS_ReportErrorASCII(cx, R"(Unknown event: ["%s", "%s", "%s"])",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+ case RecordEventResult::InvalidExtraKey:
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Invalid extra key for event."));
+ return NS_OK;
+ case RecordEventResult::StorageLimitReached:
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_STRING("Event storage limit reached."));
+ return NS_OK;
+ default:
+ return NS_OK;
+ }
+}
+
+nsresult
+TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* cx,
+ uint8_t optional_argc, JS::MutableHandleValue aResult)
+{
+ // Extract the events from storage.
+ nsTArray<EventRecord> events;
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t len = gEventRecords->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ const EventRecord& record = (*gEventRecords)[i];
+ const EventInfo& info = gEventInfo[record.EventId()];
+
+ if (IsInDataset(info.common_info.dataset, aDataset)) {
+ events.AppendElement(record);
+ }
+ }
+
+ if (aClear) {
+ gEventRecords->Clear();
+ }
+ }
+
+ // We serialize the events to a JS array.
+ JS::RootedObject eventsArray(cx, JS_NewArrayObject(cx, events.Length()));
+ if (!eventsArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < events.Length(); ++i) {
+ const EventRecord& record = events[i];
+ const EventInfo& info = gEventInfo[record.EventId()];
+
+ // Each entry is an array of one of the forms:
+ // [timestamp, category, method, object, value]
+ // [timestamp, category, method, object, null, extra]
+ // [timestamp, category, method, object, value, extra]
+ JS::AutoValueVector items(cx);
+
+ // Add timestamp.
+ JS::Rooted<JS::Value> val(cx);
+ if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add category, method, object.
+ const char* strings[] = {
+ info.common_info.category(),
+ info.method(),
+ info.object(),
+ };
+ for (const char* s : strings) {
+ const NS_ConvertUTF8toUTF16 wide(s);
+ if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional string value only when needed.
+ // When extra is empty and this has no value, we can save a little space.
+ if (record.Value()) {
+ const NS_ConvertUTF8toUTF16 wide(record.Value().value());
+ if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (!record.Extra().IsEmpty()) {
+ if (!items.append(JS::NullValue())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional extra dictionary.
+ // To save a little space, only add it when it is not empty.
+ if (!record.Extra().IsEmpty()) {
+ JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add extra key & value entries.
+ const ExtraArray& extra = record.Extra();
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ const NS_ConvertUTF8toUTF16 wide(extra[i].value);
+ JS::Rooted<JS::Value> value(cx);
+ value.setString(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length()));
+
+ if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ val.setObject(*obj);
+
+ if (!items.append(val)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the record to the events array.
+ JS::RootedObject itemsArray(cx, JS_NewArrayObject(cx, items));
+ if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ aResult.setObject(*eventsArray);
+ return NS_OK;
+}
+
+/**
+ * Resets all the stored events. This is intended to be only used in tests.
+ */
+void
+TelemetryEvent::ClearEvents()
+{
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ gEventRecords->Clear();
+}
+
+size_t
+TelemetryEvent::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ size_t n = 0;
+
+ n += gEventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < gEventRecords->Length(); ++i) {
+ n += (*gEventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
diff --git a/toolkit/components/telemetry/TelemetryEvent.h b/toolkit/components/telemetry/TelemetryEvent.h
new file mode 100644
index 0000000000..34a0720b72
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryEvent.h
@@ -0,0 +1,39 @@
+/* -*- 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 TelemetryEvent_h__
+#define TelemetryEvent_h__
+
+#include "mozilla/TelemetryEventEnums.h"
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// event recording and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryEvent {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+void SetCanRecordBase(bool b);
+void SetCanRecordExtended(bool b);
+
+// JS API Endpoints.
+nsresult RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
+ const nsACString& aObject, JS::HandleValue aValue,
+ JS::HandleValue aExtra, JSContext* aCx,
+ uint8_t optional_argc);
+nsresult CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandleValue aResult);
+
+// Only to be used for testing.
+void ClearEvents();
+
+size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+} // namespace TelemetryEvent
+
+#endif // TelemetryEvent_h__
diff --git a/toolkit/components/telemetry/TelemetryHistogram.cpp b/toolkit/components/telemetry/TelemetryHistogram.cpp
new file mode 100644
index 0000000000..abae9c6135
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryHistogram.cpp
@@ -0,0 +1,2725 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/GCAPI.h"
+#include "nsString.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsITelemetry.h"
+
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+
+#include "TelemetryCommon.h"
+#include "TelemetryHistogram.h"
+
+#include "base/histogram.h"
+
+using base::Histogram;
+using base::StatisticsRecorder;
+using base::BooleanHistogram;
+using base::CountHistogram;
+using base::FlagHistogram;
+using base::LinearHistogram;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::StaticAutoPtr;
+using mozilla::Telemetry::Accumulation;
+using mozilla::Telemetry::KeyedAccumulation;
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions named internal_*: these can only be reached via an
+// interface function (TelemetryHistogram::*). They mostly expect
+// the interface function to have acquired
+// |gTelemetryHistogramMutex|, so they do not have to be
+// thread-safe. However, those internal_* functions that are
+// reachable from internal_WrapAndReturnHistogram and
+// internal_WrapAndReturnKeyedHistogram can sometimes be called
+// without |gTelemetryHistogramMutex|, and so might be racey.
+//
+// * Functions named TelemetryHistogram::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryHistogramMutex|, except for GetAddonHistogramSnapshots,
+// GetKeyedHistogramSnapshots and CreateHistogramSnapshots.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryHistogram::*) are
+// serialised using the mutex |gTelemetryHistogramMutex|. This means
+// that the external interface is thread-safe, and many of the
+// internal_* functions can ignore thread safety. But it also brings
+// a danger of deadlock if any function in the external interface can
+// get back to that interface. That is, we will deadlock on any call
+// chain like this
+//
+// TelemetryHistogram::* -> .. any functions .. -> TelemetryHistogram::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryHistogram::* may directly call, nor take the
+// address of, any other function in TelemetryHistogram::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryHistogram::*.
+//
+// internal_WrapAndReturnHistogram and
+// internal_WrapAndReturnKeyedHistogram are not protected by
+// |gTelemetryHistogramMutex| because they make calls to the JS
+// engine, but that can in turn call back to Telemetry and hence back
+// to a TelemetryHistogram:: function, in order to report GC and other
+// statistics. This would lead to deadlock due to attempted double
+// acquisition of |gTelemetryHistogramMutex|, if the internal_* functions
+// were required to be protected by |gTelemetryHistogramMutex|. To
+// break that cycle, we relax that requirement. Unfortunately this
+// means that this file is not guaranteed race-free.
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+#define EXPIRED_ID "__expired__"
+#define SUBSESSION_HISTOGRAM_PREFIX "sub#"
+#define KEYED_HISTOGRAM_NAME_SEPARATOR "#"
+#define CONTENT_HISTOGRAM_SUFFIX "#content"
+#define GPU_HISTOGRAM_SUFFIX "#gpu"
+
+namespace {
+
+using mozilla::Telemetry::Common::AutoHashtable;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::IsInDataset;
+
+class KeyedHistogram;
+
+typedef nsBaseHashtableET<nsDepCharHashKey, mozilla::Telemetry::ID>
+ CharPtrEntryType;
+
+typedef AutoHashtable<CharPtrEntryType> HistogramMapType;
+
+typedef nsClassHashtable<nsCStringHashKey, KeyedHistogram>
+ KeyedHistogramMapType;
+
+// Hardcoded probes
+struct HistogramInfo {
+ uint32_t min;
+ uint32_t max;
+ uint32_t bucketCount;
+ uint32_t histogramType;
+ uint32_t id_offset;
+ uint32_t expiration_offset;
+ uint32_t dataset;
+ uint32_t label_index;
+ uint32_t label_count;
+ bool keyed;
+
+ const char *id() const;
+ const char *expiration() const;
+ nsresult label_id(const char* label, uint32_t* labelId) const;
+};
+
+struct AddonHistogramInfo {
+ uint32_t min;
+ uint32_t max;
+ uint32_t bucketCount;
+ uint32_t histogramType;
+ Histogram *h;
+};
+
+enum reflectStatus {
+ REFLECT_OK,
+ REFLECT_CORRUPT,
+ REFLECT_FAILURE
+};
+
+typedef StatisticsRecorder::Histograms::iterator HistogramIterator;
+
+typedef nsBaseHashtableET<nsCStringHashKey, AddonHistogramInfo>
+ AddonHistogramEntryType;
+
+typedef AutoHashtable<AddonHistogramEntryType>
+ AddonHistogramMapType;
+
+typedef nsBaseHashtableET<nsCStringHashKey, AddonHistogramMapType *>
+ AddonEntryType;
+
+typedef AutoHashtable<AddonEntryType> AddonMapType;
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized
+bool gInitDone = false;
+
+bool gCanRecordBase = false;
+bool gCanRecordExtended = false;
+
+HistogramMapType gHistogramMap(mozilla::Telemetry::HistogramCount);
+
+KeyedHistogramMapType gKeyedHistograms;
+
+bool gCorruptHistograms[mozilla::Telemetry::HistogramCount];
+
+// This is for gHistograms, gHistogramStringTable
+#include "TelemetryHistogramData.inc"
+
+AddonMapType gAddonMap;
+
+// The singleton StatisticsRecorder object for this process.
+base::StatisticsRecorder* gStatisticsRecorder = nullptr;
+
+// For batching and sending child process accumulations to the parent
+nsITimer* gIPCTimer = nullptr;
+mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArmed(false);
+mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArming(false);
+StaticAutoPtr<nsTArray<Accumulation>> gAccumulations;
+StaticAutoPtr<nsTArray<KeyedAccumulation>> gKeyedAccumulations;
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE CONSTANTS
+
+namespace {
+
+// List of histogram IDs which should have recording disabled initially.
+const mozilla::Telemetry::ID kRecordingInitiallyDisabledIDs[] = {
+ mozilla::Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS,
+
+ // The array must not be empty. Leave these item here.
+ mozilla::Telemetry::TELEMETRY_TEST_COUNT_INIT_NO_RECORD,
+ mozilla::Telemetry::TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD
+};
+
+// Sending each remote accumulation immediately places undue strain on the
+// IPC subsystem. Batch the remote accumulations for a period of time before
+// sending them all at once. This value was chosen as a balance between data
+// timeliness and performance (see bug 1218576)
+const uint32_t kBatchTimeoutMs = 2000;
+
+// To stop growing unbounded in memory while waiting for kBatchTimeoutMs to
+// drain the g*Accumulations arrays, request an immediate flush if the arrays
+// manage to reach this high water mark of elements.
+const size_t kAccumulationsArrayHighWaterMark = 5 * 1024;
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Misc small helpers
+
+namespace {
+
+bool
+internal_CanRecordBase() {
+ return gCanRecordBase;
+}
+
+bool
+internal_CanRecordExtended() {
+ return gCanRecordExtended;
+}
+
+bool
+internal_IsHistogramEnumId(mozilla::Telemetry::ID aID)
+{
+ static_assert(((mozilla::Telemetry::ID)-1 > 0), "ID should be unsigned.");
+ return aID < mozilla::Telemetry::HistogramCount;
+}
+
+// Note: this is completely unrelated to mozilla::IsEmpty.
+bool
+internal_IsEmpty(const Histogram *h)
+{
+ Histogram::SampleSet ss;
+ h->SnapshotSample(&ss);
+ return ss.counts(0) == 0 && ss.sum() == 0;
+}
+
+bool
+internal_IsExpired(const Histogram *histogram)
+{
+ return histogram->histogram_name() == EXPIRED_ID;
+}
+
+nsresult
+internal_GetRegisteredHistogramIds(bool keyed, uint32_t dataset,
+ uint32_t *aCount, char*** aHistograms)
+{
+ nsTArray<char*> collection;
+
+ for (size_t i = 0; i < mozilla::ArrayLength(gHistograms); ++i) {
+ const HistogramInfo& h = gHistograms[i];
+ if (IsExpiredVersion(h.expiration()) ||
+ h.keyed != keyed ||
+ !IsInDataset(h.dataset, dataset)) {
+ continue;
+ }
+
+ const char* id = h.id();
+ const size_t len = strlen(id);
+ collection.AppendElement(static_cast<char*>(nsMemory::Clone(id, len+1)));
+ }
+
+ const size_t bytes = collection.Length() * sizeof(char*);
+ char** histograms = static_cast<char**>(moz_xmalloc(bytes));
+ memcpy(histograms, collection.Elements(), bytes);
+ *aHistograms = histograms;
+ *aCount = collection.Length();
+
+ return NS_OK;
+}
+
+const char *
+HistogramInfo::id() const
+{
+ return &gHistogramStringTable[this->id_offset];
+}
+
+const char *
+HistogramInfo::expiration() const
+{
+ return &gHistogramStringTable[this->expiration_offset];
+}
+
+nsresult
+HistogramInfo::label_id(const char* label, uint32_t* labelId) const
+{
+ MOZ_ASSERT(label);
+ MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL);
+ if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < this->label_count; ++i) {
+ // gHistogramLabelTable contains the indices of the label strings in the
+ // gHistogramStringTable.
+ // They are stored in-order and consecutively, from the offset label_index
+ // to (label_index + label_count).
+ uint32_t string_offset = gHistogramLabelTable[this->label_index + i];
+ const char* const str = &gHistogramStringTable[string_offset];
+ if (::strcmp(label, str) == 0) {
+ *labelId = i;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+void internal_DispatchToMainThread(already_AddRefed<nsIRunnable>&& aEvent)
+{
+ nsCOMPtr<nsIRunnable> event(aEvent);
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_GetMainThread(getter_AddRefs(thread));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("NS_FAILED DispatchToMainThread. Maybe we're shutting down?");
+ return;
+ }
+ thread->Dispatch(event, 0);
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram Get, Add, Clone, Clear functions
+
+namespace {
+
+nsresult
+internal_CheckHistogramArguments(uint32_t histogramType,
+ uint32_t min, uint32_t max,
+ uint32_t bucketCount, bool haveOptArgs)
+{
+ if (histogramType != nsITelemetry::HISTOGRAM_BOOLEAN
+ && histogramType != nsITelemetry::HISTOGRAM_FLAG
+ && histogramType != nsITelemetry::HISTOGRAM_COUNT) {
+ // The min, max & bucketCount arguments are not optional for this type.
+ if (!haveOptArgs)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ // Sanity checks for histogram parameters.
+ if (min >= max)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (bucketCount <= 2)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (min < 1)
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ return NS_OK;
+}
+
+/*
+ * min, max & bucketCount are optional for boolean, flag & count histograms.
+ * haveOptArgs has to be set if the caller provides them.
+ */
+nsresult
+internal_HistogramGet(const char *name, const char *expiration,
+ uint32_t histogramType, uint32_t min, uint32_t max,
+ uint32_t bucketCount, bool haveOptArgs,
+ Histogram **result)
+{
+ nsresult rv = internal_CheckHistogramArguments(histogramType, min, max,
+ bucketCount, haveOptArgs);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (IsExpiredVersion(expiration)) {
+ name = EXPIRED_ID;
+ min = 1;
+ max = 2;
+ bucketCount = 3;
+ histogramType = nsITelemetry::HISTOGRAM_LINEAR;
+ }
+
+ switch (histogramType) {
+ case nsITelemetry::HISTOGRAM_EXPONENTIAL:
+ *result = Histogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag);
+ break;
+ case nsITelemetry::HISTOGRAM_LINEAR:
+ case nsITelemetry::HISTOGRAM_CATEGORICAL:
+ *result = LinearHistogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag);
+ break;
+ case nsITelemetry::HISTOGRAM_BOOLEAN:
+ *result = BooleanHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag);
+ break;
+ case nsITelemetry::HISTOGRAM_FLAG:
+ *result = FlagHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag);
+ break;
+ case nsITelemetry::HISTOGRAM_COUNT:
+ *result = CountHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag);
+ break;
+ default:
+ NS_ASSERTION(false, "Invalid histogram type");
+ return NS_ERROR_INVALID_ARG;
+ }
+ return NS_OK;
+}
+
+// Read the process type from the given histogram name. The process type, if
+// one exists, is embedded in a suffix.
+GeckoProcessType
+GetProcessFromName(const nsACString& aString)
+{
+ if (StringEndsWith(aString, NS_LITERAL_CSTRING(CONTENT_HISTOGRAM_SUFFIX))) {
+ return GeckoProcessType_Content;
+ }
+ if (StringEndsWith(aString, NS_LITERAL_CSTRING(GPU_HISTOGRAM_SUFFIX))) {
+ return GeckoProcessType_GPU;
+ }
+ return GeckoProcessType_Default;
+}
+
+const char*
+SuffixForProcessType(GeckoProcessType aProcessType)
+{
+ switch (aProcessType) {
+ case GeckoProcessType_Default:
+ return nullptr;
+ case GeckoProcessType_Content:
+ return CONTENT_HISTOGRAM_SUFFIX;
+ case GeckoProcessType_GPU:
+ return GPU_HISTOGRAM_SUFFIX;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unknown process type");
+ return nullptr;
+ }
+}
+
+CharPtrEntryType*
+internal_GetHistogramMapEntry(const char* aName)
+{
+ nsDependentCString name(aName);
+ GeckoProcessType process = GetProcessFromName(name);
+ const char* suffix = SuffixForProcessType(process);
+ if (!suffix) {
+ return gHistogramMap.GetEntry(aName);
+ }
+
+ auto root = Substring(name, 0, name.Length() - strlen(suffix));
+ return gHistogramMap.GetEntry(PromiseFlatCString(root).get());
+}
+
+nsresult
+internal_GetHistogramEnumId(const char *name, mozilla::Telemetry::ID *id)
+{
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CharPtrEntryType *entry = internal_GetHistogramMapEntry(name);
+ if (!entry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *id = entry->mData;
+ return NS_OK;
+}
+
+// O(1) histogram lookup by numeric id
+nsresult
+internal_GetHistogramByEnumId(mozilla::Telemetry::ID id, Histogram **ret, GeckoProcessType aProcessType)
+{
+ static Histogram* knownHistograms[mozilla::Telemetry::HistogramCount] = {0};
+ static Histogram* knownContentHistograms[mozilla::Telemetry::HistogramCount] = {0};
+ static Histogram* knownGPUHistograms[mozilla::Telemetry::HistogramCount] = {0};
+
+ Histogram** knownList = nullptr;
+
+ switch (aProcessType) {
+ case GeckoProcessType_Default:
+ knownList = knownHistograms;
+ break;
+ case GeckoProcessType_Content:
+ knownList = knownContentHistograms;
+ break;
+ case GeckoProcessType_GPU:
+ knownList = knownGPUHistograms;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unknown process type");
+ return NS_ERROR_FAILURE;
+ }
+
+ Histogram* h = knownList[id];
+ if (h) {
+ *ret = h;
+ return NS_OK;
+ }
+
+ const HistogramInfo &p = gHistograms[id];
+ if (p.keyed) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCString histogramName;
+ histogramName.Append(p.id());
+ if (const char* suffix = SuffixForProcessType(aProcessType)) {
+ histogramName.AppendASCII(suffix);
+ }
+
+ nsresult rv = internal_HistogramGet(histogramName.get(), p.expiration(),
+ p.histogramType, p.min, p.max,
+ p.bucketCount, true, &h);
+ if (NS_FAILED(rv))
+ return rv;
+
+#ifdef DEBUG
+ // Check that the C++ Histogram code computes the same ranges as the
+ // Python histogram code.
+ if (!IsExpiredVersion(p.expiration())) {
+ const struct bounds &b = gBucketLowerBoundIndex[id];
+ if (b.length != 0) {
+ MOZ_ASSERT(size_t(b.length) == h->bucket_count(),
+ "C++/Python bucket # mismatch");
+ for (int i = 0; i < b.length; ++i) {
+ MOZ_ASSERT(gBucketLowerBounds[b.offset + i] == h->ranges(i),
+ "C++/Python bucket mismatch");
+ }
+ }
+ }
+#endif
+
+ knownList[id] = h;
+ *ret = h;
+ return NS_OK;
+}
+
+nsresult
+internal_GetHistogramByName(const nsACString &name, Histogram **ret)
+{
+ mozilla::Telemetry::ID id;
+ nsresult rv
+ = internal_GetHistogramEnumId(PromiseFlatCString(name).get(), &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ GeckoProcessType process = GetProcessFromName(name);
+ rv = internal_GetHistogramByEnumId(id, ret, process);
+ if (NS_FAILED(rv))
+ return rv;
+
+ return NS_OK;
+}
+
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+
+/**
+ * This clones a histogram |existing| with the id |existingId| to a
+ * new histogram with the name |newName|.
+ * For simplicity this is limited to registered histograms.
+ */
+Histogram*
+internal_CloneHistogram(const nsACString& newName,
+ mozilla::Telemetry::ID existingId,
+ Histogram& existing)
+{
+ const HistogramInfo &info = gHistograms[existingId];
+ Histogram *clone = nullptr;
+ nsresult rv;
+
+ rv = internal_HistogramGet(PromiseFlatCString(newName).get(),
+ info.expiration(),
+ info.histogramType, existing.declared_min(),
+ existing.declared_max(), existing.bucket_count(),
+ true, &clone);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ Histogram::SampleSet ss;
+ existing.SnapshotSample(&ss);
+ clone->AddSampleSet(ss);
+
+ return clone;
+}
+
+GeckoProcessType
+GetProcessFromName(const std::string& aString)
+{
+ nsDependentCString string(aString.c_str(), aString.length());
+ return GetProcessFromName(string);
+}
+
+Histogram*
+internal_GetSubsessionHistogram(Histogram& existing)
+{
+ mozilla::Telemetry::ID id;
+ nsresult rv
+ = internal_GetHistogramEnumId(existing.histogram_name().c_str(), &id);
+ if (NS_FAILED(rv) || gHistograms[id].keyed) {
+ return nullptr;
+ }
+
+ static Histogram* subsession[mozilla::Telemetry::HistogramCount] = {};
+ static Histogram* subsessionContent[mozilla::Telemetry::HistogramCount] = {};
+ static Histogram* subsessionGPU[mozilla::Telemetry::HistogramCount] = {};
+
+ Histogram** cache = nullptr;
+
+ GeckoProcessType process = GetProcessFromName(existing.histogram_name());
+ switch (process) {
+ case GeckoProcessType_Default:
+ cache = subsession;
+ break;
+ case GeckoProcessType_Content:
+ cache = subsessionContent;
+ break;
+ case GeckoProcessType_GPU:
+ cache = subsessionGPU;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unknown process type");
+ return nullptr;
+ }
+
+ if (Histogram* cached = cache[id]) {
+ return cached;
+ }
+
+ NS_NAMED_LITERAL_CSTRING(prefix, SUBSESSION_HISTOGRAM_PREFIX);
+ nsDependentCString existingName(gHistograms[id].id());
+ if (StringBeginsWith(existingName, prefix)) {
+ return nullptr;
+ }
+
+ nsCString subsessionName(prefix);
+ subsessionName.Append(existing.histogram_name().c_str());
+
+ Histogram* clone = internal_CloneHistogram(subsessionName, id, existing);
+ cache[id] = clone;
+ return clone;
+}
+#endif
+
+nsresult
+internal_HistogramAdd(Histogram& histogram, int32_t value, uint32_t dataset)
+{
+ // Check if we are allowed to record the data.
+ bool canRecordDataset = CanRecordDataset(dataset,
+ internal_CanRecordBase(),
+ internal_CanRecordExtended());
+ if (!canRecordDataset || !histogram.IsRecordingEnabled()) {
+ return NS_OK;
+ }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ if (Histogram* subsession = internal_GetSubsessionHistogram(histogram)) {
+ subsession->Add(value);
+ }
+#endif
+
+ // It is safe to add to the histogram now: the subsession histogram was already
+ // cloned from this so we won't add the sample twice.
+ histogram.Add(value);
+
+ return NS_OK;
+}
+
+nsresult
+internal_HistogramAdd(Histogram& histogram, int32_t value)
+{
+ uint32_t dataset = nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN;
+ // We only really care about the dataset of the histogram if we are not recording
+ // extended telemetry. Otherwise, we always record histogram data.
+ if (!internal_CanRecordExtended()) {
+ mozilla::Telemetry::ID id;
+ nsresult rv
+ = internal_GetHistogramEnumId(histogram.histogram_name().c_str(), &id);
+ if (NS_FAILED(rv)) {
+ // If we can't look up the dataset, it might be because the histogram was added
+ // at runtime. Since we're not recording extended telemetry, bail out.
+ return NS_OK;
+ }
+ dataset = gHistograms[id].dataset;
+ }
+
+ return internal_HistogramAdd(histogram, value, dataset);
+}
+
+void
+internal_HistogramClear(Histogram& aHistogram, bool onlySubsession)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+ if (!onlySubsession) {
+ aHistogram.Clear();
+ }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ if (Histogram* subsession = internal_GetSubsessionHistogram(aHistogram)) {
+ subsession->Clear();
+ }
+#endif
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram corruption helpers
+
+namespace {
+
+void internal_Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample);
+
+void
+internal_IdentifyCorruptHistograms(StatisticsRecorder::Histograms &hs)
+{
+ for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) {
+ Histogram *h = *it;
+
+ mozilla::Telemetry::ID id;
+ nsresult rv = internal_GetHistogramEnumId(h->histogram_name().c_str(), &id);
+ // This histogram isn't a static histogram, just ignore it.
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ if (gCorruptHistograms[id]) {
+ continue;
+ }
+
+ Histogram::SampleSet ss;
+ h->SnapshotSample(&ss);
+
+ Histogram::Inconsistencies check = h->FindCorruption(ss);
+ bool corrupt = (check != Histogram::NO_INCONSISTENCIES);
+
+ if (corrupt) {
+ mozilla::Telemetry::ID corruptID = mozilla::Telemetry::HistogramCount;
+ if (check & Histogram::RANGE_CHECKSUM_ERROR) {
+ corruptID = mozilla::Telemetry::RANGE_CHECKSUM_ERRORS;
+ } else if (check & Histogram::BUCKET_ORDER_ERROR) {
+ corruptID = mozilla::Telemetry::BUCKET_ORDER_ERRORS;
+ } else if (check & Histogram::COUNT_HIGH_ERROR) {
+ corruptID = mozilla::Telemetry::TOTAL_COUNT_HIGH_ERRORS;
+ } else if (check & Histogram::COUNT_LOW_ERROR) {
+ corruptID = mozilla::Telemetry::TOTAL_COUNT_LOW_ERRORS;
+ }
+ internal_Accumulate(corruptID, 1);
+ }
+
+ gCorruptHistograms[id] = corrupt;
+ }
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram reflection helpers
+
+namespace {
+
+bool
+internal_FillRanges(JSContext *cx, JS::Handle<JSObject*> array, Histogram *h)
+{
+ JS::Rooted<JS::Value> range(cx);
+ for (size_t i = 0; i < h->bucket_count(); i++) {
+ range.setInt32(h->ranges(i));
+ if (!JS_DefineElement(cx, array, i, range, JSPROP_ENUMERATE))
+ return false;
+ }
+ return true;
+}
+
+enum reflectStatus
+internal_ReflectHistogramAndSamples(JSContext *cx,
+ JS::Handle<JSObject*> obj, Histogram *h,
+ const Histogram::SampleSet &ss)
+{
+ // We don't want to reflect corrupt histograms.
+ if (h->FindCorruption(ss) != Histogram::NO_INCONSISTENCIES) {
+ return REFLECT_CORRUPT;
+ }
+
+ if (!(JS_DefineProperty(cx, obj, "min",
+ h->declared_min(), JSPROP_ENUMERATE)
+ && JS_DefineProperty(cx, obj, "max",
+ h->declared_max(), JSPROP_ENUMERATE)
+ && JS_DefineProperty(cx, obj, "histogram_type",
+ h->histogram_type(), JSPROP_ENUMERATE)
+ && JS_DefineProperty(cx, obj, "sum",
+ double(ss.sum()), JSPROP_ENUMERATE))) {
+ return REFLECT_FAILURE;
+ }
+
+ const size_t count = h->bucket_count();
+ JS::Rooted<JSObject*> rarray(cx, JS_NewArrayObject(cx, count));
+ if (!rarray) {
+ return REFLECT_FAILURE;
+ }
+ if (!(internal_FillRanges(cx, rarray, h)
+ && JS_DefineProperty(cx, obj, "ranges", rarray, JSPROP_ENUMERATE))) {
+ return REFLECT_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> counts_array(cx, JS_NewArrayObject(cx, count));
+ if (!counts_array) {
+ return REFLECT_FAILURE;
+ }
+ if (!JS_DefineProperty(cx, obj, "counts", counts_array, JSPROP_ENUMERATE)) {
+ return REFLECT_FAILURE;
+ }
+ for (size_t i = 0; i < count; i++) {
+ if (!JS_DefineElement(cx, counts_array, i,
+ ss.counts(i), JSPROP_ENUMERATE)) {
+ return REFLECT_FAILURE;
+ }
+ }
+
+ return REFLECT_OK;
+}
+
+enum reflectStatus
+internal_ReflectHistogramSnapshot(JSContext *cx,
+ JS::Handle<JSObject*> obj, Histogram *h)
+{
+ Histogram::SampleSet ss;
+ h->SnapshotSample(&ss);
+ return internal_ReflectHistogramAndSamples(cx, obj, h, ss);
+}
+
+bool
+internal_ShouldReflectHistogram(Histogram *h)
+{
+ const char *name = h->histogram_name().c_str();
+ mozilla::Telemetry::ID id;
+ nsresult rv = internal_GetHistogramEnumId(name, &id);
+ if (NS_FAILED(rv)) {
+ // GetHistogramEnumId generally should not fail. But a lookup
+ // failure shouldn't prevent us from reflecting histograms into JS.
+ //
+ // However, these two histograms are created by Histogram itself for
+ // tracking corruption. We have our own histograms for that, so
+ // ignore these two.
+ if (strcmp(name, "Histogram.InconsistentCountHigh") == 0
+ || strcmp(name, "Histogram.InconsistentCountLow") == 0) {
+ return false;
+ }
+ return true;
+ } else {
+ return !gCorruptHistograms[id];
+ }
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: class KeyedHistogram
+
+namespace {
+
+class KeyedHistogram {
+public:
+ KeyedHistogram(const nsACString &name, const nsACString &expiration,
+ uint32_t histogramType, uint32_t min, uint32_t max,
+ uint32_t bucketCount, uint32_t dataset);
+ nsresult GetHistogram(const nsCString& name, Histogram** histogram, bool subsession);
+ Histogram* GetHistogram(const nsCString& name, bool subsession);
+ uint32_t GetHistogramType() const { return mHistogramType; }
+ nsresult GetDataset(uint32_t* dataset) const;
+ nsresult GetJSKeys(JSContext* cx, JS::CallArgs& args);
+ nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+ bool subsession, bool clearSubsession);
+
+ void SetRecordingEnabled(bool aEnabled) { mRecordingEnabled = aEnabled; };
+ bool IsRecordingEnabled() const { return mRecordingEnabled; };
+
+ nsresult Add(const nsCString& key, uint32_t aSample);
+ void Clear(bool subsession);
+
+ nsresult GetEnumId(mozilla::Telemetry::ID& id);
+
+private:
+ typedef nsBaseHashtableET<nsCStringHashKey, Histogram*> KeyedHistogramEntry;
+ typedef AutoHashtable<KeyedHistogramEntry> KeyedHistogramMapType;
+ KeyedHistogramMapType mHistogramMap;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ KeyedHistogramMapType mSubsessionMap;
+#endif
+
+ static bool ReflectKeyedHistogram(KeyedHistogramEntry* entry,
+ JSContext* cx,
+ JS::Handle<JSObject*> obj);
+
+ const nsCString mName;
+ const nsCString mExpiration;
+ const uint32_t mHistogramType;
+ const uint32_t mMin;
+ const uint32_t mMax;
+ const uint32_t mBucketCount;
+ const uint32_t mDataset;
+ mozilla::Atomic<bool, mozilla::Relaxed> mRecordingEnabled;
+};
+
+KeyedHistogram::KeyedHistogram(const nsACString &name,
+ const nsACString &expiration,
+ uint32_t histogramType,
+ uint32_t min, uint32_t max,
+ uint32_t bucketCount, uint32_t dataset)
+ : mHistogramMap()
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ , mSubsessionMap()
+#endif
+ , mName(name)
+ , mExpiration(expiration)
+ , mHistogramType(histogramType)
+ , mMin(min)
+ , mMax(max)
+ , mBucketCount(bucketCount)
+ , mDataset(dataset)
+ , mRecordingEnabled(true)
+{
+}
+
+nsresult
+KeyedHistogram::GetHistogram(const nsCString& key, Histogram** histogram,
+ bool subsession)
+{
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
+#else
+ KeyedHistogramMapType& map = mHistogramMap;
+#endif
+ KeyedHistogramEntry* entry = map.GetEntry(key);
+ if (entry) {
+ *histogram = entry->mData;
+ return NS_OK;
+ }
+
+ nsCString histogramName;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ if (subsession) {
+ histogramName.AppendLiteral(SUBSESSION_HISTOGRAM_PREFIX);
+ }
+#endif
+ histogramName.Append(mName);
+ histogramName.AppendLiteral(KEYED_HISTOGRAM_NAME_SEPARATOR);
+ histogramName.Append(key);
+
+ Histogram* h;
+ nsresult rv = internal_HistogramGet(histogramName.get(), mExpiration.get(),
+ mHistogramType, mMin, mMax, mBucketCount,
+ true, &h);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ h->ClearFlags(Histogram::kUmaTargetedHistogramFlag);
+ *histogram = h;
+
+ entry = map.PutEntry(key);
+ if (MOZ_UNLIKELY(!entry)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ entry->mData = h;
+ return NS_OK;
+}
+
+Histogram*
+KeyedHistogram::GetHistogram(const nsCString& key, bool subsession)
+{
+ Histogram* h = nullptr;
+ if (NS_FAILED(GetHistogram(key, &h, subsession))) {
+ return nullptr;
+ }
+ return h;
+}
+
+nsresult
+KeyedHistogram::GetDataset(uint32_t* dataset) const
+{
+ MOZ_ASSERT(dataset);
+ *dataset = mDataset;
+ return NS_OK;
+}
+
+nsresult
+KeyedHistogram::Add(const nsCString& key, uint32_t sample)
+{
+ bool canRecordDataset = CanRecordDataset(mDataset,
+ internal_CanRecordBase(),
+ internal_CanRecordExtended());
+ if (!canRecordDataset) {
+ return NS_OK;
+ }
+
+ Histogram* histogram = GetHistogram(key, false);
+ MOZ_ASSERT(histogram);
+ if (!histogram) {
+ return NS_ERROR_FAILURE;
+ }
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ Histogram* subsession = GetHistogram(key, true);
+ MOZ_ASSERT(subsession);
+ if (!subsession) {
+ return NS_ERROR_FAILURE;
+ }
+#endif
+
+ if (!IsRecordingEnabled()) {
+ return NS_OK;
+ }
+
+ histogram->Add(sample);
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ subsession->Add(sample);
+#endif
+ return NS_OK;
+}
+
+void
+KeyedHistogram::Clear(bool onlySubsession)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ for (auto iter = mSubsessionMap.Iter(); !iter.Done(); iter.Next()) {
+ iter.Get()->mData->Clear();
+ }
+ mSubsessionMap.Clear();
+ if (onlySubsession) {
+ return;
+ }
+#endif
+
+ for (auto iter = mHistogramMap.Iter(); !iter.Done(); iter.Next()) {
+ iter.Get()->mData->Clear();
+ }
+ mHistogramMap.Clear();
+}
+
+nsresult
+KeyedHistogram::GetJSKeys(JSContext* cx, JS::CallArgs& args)
+{
+ JS::AutoValueVector keys(cx);
+ if (!keys.reserve(mHistogramMap.Count())) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (auto iter = mHistogramMap.Iter(); !iter.Done(); iter.Next()) {
+ JS::RootedValue jsKey(cx);
+ const NS_ConvertUTF8toUTF16 key(iter.Get()->GetKey());
+ jsKey.setString(JS_NewUCStringCopyN(cx, key.Data(), key.Length()));
+ if (!keys.append(jsKey)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ JS::RootedObject jsKeys(cx, JS_NewArrayObject(cx, keys));
+ if (!jsKeys) {
+ return NS_ERROR_FAILURE;
+ }
+
+ args.rval().setObject(*jsKeys);
+ return NS_OK;
+}
+
+bool
+KeyedHistogram::ReflectKeyedHistogram(KeyedHistogramEntry* entry,
+ JSContext* cx, JS::Handle<JSObject*> obj)
+{
+ JS::RootedObject histogramSnapshot(cx, JS_NewPlainObject(cx));
+ if (!histogramSnapshot) {
+ return false;
+ }
+
+ if (internal_ReflectHistogramSnapshot(cx, histogramSnapshot,
+ entry->mData) != REFLECT_OK) {
+ return false;
+ }
+
+ const NS_ConvertUTF8toUTF16 key(entry->GetKey());
+ if (!JS_DefineUCProperty(cx, obj, key.Data(), key.Length(),
+ histogramSnapshot, JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult
+KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+ bool subsession, bool clearSubsession)
+{
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ KeyedHistogramMapType& map = subsession ? mSubsessionMap : mHistogramMap;
+#else
+ KeyedHistogramMapType& map = mHistogramMap;
+#endif
+ if (!map.ReflectIntoJS(&KeyedHistogram::ReflectKeyedHistogram, cx, obj)) {
+ return NS_ERROR_FAILURE;
+ }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ if (subsession && clearSubsession) {
+ Clear(true);
+ }
+#endif
+
+ return NS_OK;
+}
+
+nsresult
+KeyedHistogram::GetEnumId(mozilla::Telemetry::ID& id)
+{
+ return internal_GetHistogramEnumId(mName.get(), &id);
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: KeyedHistogram helpers
+
+namespace {
+
+KeyedHistogram*
+internal_GetKeyedHistogramById(const nsACString &name)
+{
+ if (!gInitDone) {
+ return nullptr;
+ }
+
+ KeyedHistogram* keyed = nullptr;
+ gKeyedHistograms.Get(name, &keyed);
+ return keyed;
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: functions related to addon histograms
+
+namespace {
+
+// Compute the name to pass into Histogram for the addon histogram
+// 'name' from the addon 'id'. We can't use 'name' directly because it
+// might conflict with other histograms in other addons or even with our
+// own.
+void
+internal_AddonHistogramName(const nsACString &id, const nsACString &name,
+ nsACString &ret)
+{
+ ret.Append(id);
+ ret.Append(':');
+ ret.Append(name);
+}
+
+bool
+internal_CreateHistogramForAddon(const nsACString &name,
+ AddonHistogramInfo &info)
+{
+ Histogram *h;
+ nsresult rv = internal_HistogramGet(PromiseFlatCString(name).get(), "never",
+ info.histogramType, info.min, info.max,
+ info.bucketCount, true, &h);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ // Don't let this histogram be reported via the normal means
+ // (e.g. Telemetry.registeredHistograms); we'll make it available in
+ // other ways.
+ h->ClearFlags(Histogram::kUmaTargetedHistogramFlag);
+ info.h = h;
+ return true;
+}
+
+bool
+internal_AddonHistogramReflector(AddonHistogramEntryType *entry,
+ JSContext *cx, JS::Handle<JSObject*> obj)
+{
+ AddonHistogramInfo &info = entry->mData;
+
+ // Never even accessed the histogram.
+ if (!info.h) {
+ // Have to force creation of HISTOGRAM_FLAG histograms.
+ if (info.histogramType != nsITelemetry::HISTOGRAM_FLAG)
+ return true;
+
+ if (!internal_CreateHistogramForAddon(entry->GetKey(), info)) {
+ return false;
+ }
+ }
+
+ if (internal_IsEmpty(info.h)) {
+ return true;
+ }
+
+ JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ // Just consider this to be skippable.
+ return true;
+ }
+ switch (internal_ReflectHistogramSnapshot(cx, snapshot, info.h)) {
+ case REFLECT_FAILURE:
+ case REFLECT_CORRUPT:
+ return false;
+ case REFLECT_OK:
+ const nsACString &histogramName = entry->GetKey();
+ if (!JS_DefineProperty(cx, obj, PromiseFlatCString(histogramName).get(),
+ snapshot, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ break;
+ }
+ return true;
+}
+
+bool
+internal_AddonReflector(AddonEntryType *entry, JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ const nsACString &addonId = entry->GetKey();
+ JS::Rooted<JSObject*> subobj(cx, JS_NewPlainObject(cx));
+ if (!subobj) {
+ return false;
+ }
+
+ AddonHistogramMapType *map = entry->mData;
+ if (!(map->ReflectIntoJS(internal_AddonHistogramReflector, cx, subobj)
+ && JS_DefineProperty(cx, obj, PromiseFlatCString(addonId).get(),
+ subobj, JSPROP_ENUMERATE))) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the external interface
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+static StaticMutex gTelemetryHistogramMutex;
+
+namespace {
+
+void
+internal_SetHistogramRecordingEnabled(mozilla::Telemetry::ID aID, bool aEnabled)
+{
+ if (gHistograms[aID].keyed) {
+ const nsDependentCString id(gHistograms[aID].id());
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(id);
+ if (keyed) {
+ keyed->SetRecordingEnabled(aEnabled);
+ return;
+ }
+ } else {
+ Histogram *h;
+ nsresult rv = internal_GetHistogramByEnumId(aID, &h, GeckoProcessType_Default);
+ if (NS_SUCCEEDED(rv)) {
+ h->SetRecordingEnabled(aEnabled);
+ return;
+ }
+ }
+
+ MOZ_ASSERT(false, "Telemetry::SetHistogramRecordingEnabled(...) id not found");
+}
+
+void internal_armIPCTimerMainThread()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ gIPCTimerArming = false;
+ if (gIPCTimerArmed) {
+ return;
+ }
+ if (!gIPCTimer) {
+ CallCreateInstance(NS_TIMER_CONTRACTID, &gIPCTimer);
+ }
+ if (gIPCTimer) {
+ gIPCTimer->InitWithFuncCallback(TelemetryHistogram::IPCTimerFired,
+ nullptr, kBatchTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT);
+ gIPCTimerArmed = true;
+ }
+}
+
+void internal_armIPCTimer()
+{
+ if (gIPCTimerArmed || gIPCTimerArming) {
+ return;
+ }
+ gIPCTimerArming = true;
+ if (NS_IsMainThread()) {
+ internal_armIPCTimerMainThread();
+ } else {
+ internal_DispatchToMainThread(NS_NewRunnableFunction([]() -> void {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_armIPCTimerMainThread();
+ }));
+ }
+}
+
+bool
+internal_RemoteAccumulate(mozilla::Telemetry::ID aId, uint32_t aSample)
+{
+ if (XRE_IsParentProcess()) {
+ return false;
+ }
+ Histogram *h;
+ nsresult rv = internal_GetHistogramByEnumId(aId, &h, GeckoProcessType_Default);
+ if (NS_SUCCEEDED(rv) && !h->IsRecordingEnabled()) {
+ return true;
+ }
+ if (!gAccumulations) {
+ gAccumulations = new nsTArray<Accumulation>();
+ }
+ if (gAccumulations->Length() == kAccumulationsArrayHighWaterMark) {
+ internal_DispatchToMainThread(NS_NewRunnableFunction([]() -> void {
+ TelemetryHistogram::IPCTimerFired(nullptr, nullptr);
+ }));
+ }
+ gAccumulations->AppendElement(Accumulation{aId, aSample});
+ internal_armIPCTimer();
+ return true;
+}
+
+bool
+internal_RemoteAccumulate(mozilla::Telemetry::ID aId,
+ const nsCString& aKey, uint32_t aSample)
+{
+ if (XRE_IsParentProcess()) {
+ return false;
+ }
+ const HistogramInfo& th = gHistograms[aId];
+ KeyedHistogram* keyed
+ = internal_GetKeyedHistogramById(nsDependentCString(th.id()));
+ MOZ_ASSERT(keyed);
+ if (!keyed->IsRecordingEnabled()) {
+ return false;
+ }
+ if (!gKeyedAccumulations) {
+ gKeyedAccumulations = new nsTArray<KeyedAccumulation>();
+ }
+ if (gKeyedAccumulations->Length() == kAccumulationsArrayHighWaterMark) {
+ internal_DispatchToMainThread(NS_NewRunnableFunction([]() -> void {
+ TelemetryHistogram::IPCTimerFired(nullptr, nullptr);
+ }));
+ }
+ gKeyedAccumulations->AppendElement(KeyedAccumulation{aId, aSample, aKey});
+ internal_armIPCTimer();
+ return true;
+}
+
+void internal_Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample)
+{
+ if (!internal_CanRecordBase() ||
+ internal_RemoteAccumulate(aHistogram, aSample)) {
+ return;
+ }
+ Histogram *h;
+ nsresult rv = internal_GetHistogramByEnumId(aHistogram, &h, GeckoProcessType_Default);
+ if (NS_SUCCEEDED(rv)) {
+ internal_HistogramAdd(*h, aSample, gHistograms[aHistogram].dataset);
+ }
+}
+
+void
+internal_Accumulate(mozilla::Telemetry::ID aID,
+ const nsCString& aKey, uint32_t aSample)
+{
+ if (!gInitDone || !internal_CanRecordBase() ||
+ internal_RemoteAccumulate(aID, aKey, aSample)) {
+ return;
+ }
+ const HistogramInfo& th = gHistograms[aID];
+ KeyedHistogram* keyed
+ = internal_GetKeyedHistogramById(nsDependentCString(th.id()));
+ MOZ_ASSERT(keyed);
+ keyed->Add(aKey, aSample);
+}
+
+void
+internal_Accumulate(Histogram& aHistogram, uint32_t aSample)
+{
+ if (XRE_IsParentProcess()) {
+ internal_HistogramAdd(aHistogram, aSample);
+ return;
+ }
+
+ mozilla::Telemetry::ID id;
+ nsresult rv = internal_GetHistogramEnumId(aHistogram.histogram_name().c_str(), &id);
+ if (NS_SUCCEEDED(rv)) {
+ internal_RemoteAccumulate(id, aSample);
+ }
+}
+
+void
+internal_Accumulate(KeyedHistogram& aKeyed,
+ const nsCString& aKey, uint32_t aSample)
+{
+ if (XRE_IsParentProcess()) {
+ aKeyed.Add(aKey, aSample);
+ return;
+ }
+
+ mozilla::Telemetry::ID id;
+ if (NS_SUCCEEDED(aKeyed.GetEnumId(id))) {
+ internal_RemoteAccumulate(id, aKey, aSample);
+ }
+}
+
+void
+internal_AccumulateChild(GeckoProcessType aProcessType, mozilla::Telemetry::ID aId, uint32_t aSample)
+{
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ Histogram* h;
+ nsresult rv = internal_GetHistogramByEnumId(aId, &h, aProcessType);
+ if (NS_SUCCEEDED(rv)) {
+ internal_HistogramAdd(*h, aSample, gHistograms[aId].dataset);
+ } else {
+ NS_WARNING("NS_FAILED GetHistogramByEnumId for CHILD");
+ }
+}
+
+void
+internal_AccumulateChildKeyed(GeckoProcessType aProcessType, mozilla::Telemetry::ID aId,
+ const nsCString& aKey, uint32_t aSample)
+{
+ if (!gInitDone || !internal_CanRecordBase()) {
+ return;
+ }
+
+ const char* suffix = SuffixForProcessType(aProcessType);
+ if (!suffix) {
+ MOZ_ASSERT_UNREACHABLE("suffix should not be null");
+ return;
+ }
+
+ const HistogramInfo& th = gHistograms[aId];
+
+ nsCString id;
+ id.Append(th.id());
+ id.AppendASCII(suffix);
+
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(id);
+ MOZ_ASSERT(keyed);
+ keyed->Add(aKey, aSample);
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: JSHistogram_* functions
+
+// NOTE: the functions in this section:
+//
+// internal_JSHistogram_Add
+// internal_JSHistogram_Snapshot
+// internal_JSHistogram_Clear
+// internal_JSHistogram_Dataset
+// internal_WrapAndReturnHistogram
+//
+// all run without protection from |gTelemetryHistogramMutex|. If they
+// held |gTelemetryHistogramMutex|, there would be the possibility of
+// deadlock because the JS_ calls that they make may call back into the
+// TelemetryHistogram interface, hence trying to re-acquire the mutex.
+//
+// This means that these functions potentially race against threads, but
+// that seems preferable to risking deadlock.
+
+namespace {
+
+bool
+internal_JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ MOZ_ASSERT(obj);
+ if (!obj) {
+ return false;
+ }
+
+ Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
+ MOZ_ASSERT(h);
+ Histogram::ClassType type = h->histogram_type();
+
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!internal_CanRecordBase()) {
+ return true;
+ }
+
+ uint32_t value = 0;
+ mozilla::Telemetry::ID id;
+ if ((type == base::CountHistogram::COUNT_HISTOGRAM) && (args.length() == 0)) {
+ // If we don't have an argument for the count histogram, assume an increment of 1.
+ // Otherwise, make sure to run some sanity checks on the argument.
+ value = 1;
+ } else if (type == base::LinearHistogram::LINEAR_HISTOGRAM &&
+ (args.length() > 0) && args[0].isString() &&
+ NS_SUCCEEDED(internal_GetHistogramEnumId(h->histogram_name().c_str(), &id)) &&
+ gHistograms[id].histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ // For categorical histograms we allow passing a string argument that specifies the label.
+ nsAutoJSString label;
+ if (!label.init(cx, args[0])) {
+ JS_ReportErrorASCII(cx, "Invalid string parameter");
+ return false;
+ }
+
+ nsresult rv = gHistograms[id].label_id(NS_ConvertUTF16toUTF8(label).get(), &value);
+ if (NS_FAILED(rv)) {
+ JS_ReportErrorASCII(cx, "Unknown label for categorical histogram");
+ return false;
+ }
+ } else {
+ // All other accumulations expect one numerical argument.
+ if (!args.length()) {
+ JS_ReportErrorASCII(cx, "Expected one argument");
+ return false;
+ }
+
+ if (!(args[0].isNumber() || args[0].isBoolean())) {
+ JS_ReportErrorASCII(cx, "Not a number");
+ return false;
+ }
+
+ if (!JS::ToUint32(cx, args[0], &value)) {
+ JS_ReportErrorASCII(cx, "Failed to convert argument");
+ return false;
+ }
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(*h, value);
+ }
+ return true;
+}
+
+bool
+internal_JSHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
+ JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot)
+ return false;
+
+ switch (internal_ReflectHistogramSnapshot(cx, snapshot, h)) {
+ case REFLECT_FAILURE:
+ return false;
+ case REFLECT_CORRUPT:
+ JS_ReportErrorASCII(cx, "Histogram is corrupt");
+ return false;
+ case REFLECT_OK:
+ args.rval().setObject(*snapshot);
+ return true;
+ default:
+ MOZ_CRASH("unhandled reflection status");
+ }
+}
+
+bool
+internal_JSHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ bool onlySubsession = false;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (args.length() >= 1) {
+ if (!args[0].isBoolean()) {
+ JS_ReportErrorASCII(cx, "Not a boolean");
+ return false;
+ }
+
+ onlySubsession = JS::ToBoolean(args[0]);
+ }
+#endif
+
+ Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
+ MOZ_ASSERT(h);
+ if (h) {
+ internal_HistogramClear(*h, onlySubsession);
+ }
+
+ return true;
+}
+
+bool
+internal_JSHistogram_Dataset(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
+ mozilla::Telemetry::ID id;
+ nsresult rv = internal_GetHistogramEnumId(h->histogram_name().c_str(), &id);
+ if (NS_SUCCEEDED(rv)) {
+ args.rval().setNumber(gHistograms[id].dataset);
+ return true;
+ }
+
+ return false;
+}
+
+// NOTE: Runs without protection from |gTelemetryHistogramMutex|.
+// See comment at the top of this section.
+nsresult
+internal_WrapAndReturnHistogram(Histogram *h, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ static const JSClass JSHistogram_class = {
+ "JSHistogram", /* name */
+ JSCLASS_HAS_PRIVATE /* flags */
+ };
+
+ JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &JSHistogram_class));
+ if (!obj)
+ return NS_ERROR_FAILURE;
+ // The 4 functions that are wrapped up here are eventually called
+ // by the same thread that runs this function.
+ if (!(JS_DefineFunction(cx, obj, "add", internal_JSHistogram_Add, 1, 0)
+ && JS_DefineFunction(cx, obj, "snapshot",
+ internal_JSHistogram_Snapshot, 0, 0)
+ && JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 0, 0)
+ && JS_DefineFunction(cx, obj, "dataset",
+ internal_JSHistogram_Dataset, 0, 0))) {
+ return NS_ERROR_FAILURE;
+ }
+ JS_SetPrivate(obj, h);
+ ret.setObject(*obj);
+ return NS_OK;
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: JSKeyedHistogram_* functions
+
+// NOTE: the functions in this section:
+//
+// internal_KeyedHistogram_SnapshotImpl
+// internal_JSKeyedHistogram_Add
+// internal_JSKeyedHistogram_Keys
+// internal_JSKeyedHistogram_Snapshot
+// internal_JSKeyedHistogram_SubsessionSnapshot
+// internal_JSKeyedHistogram_SnapshotSubsessionAndClear
+// internal_JSKeyedHistogram_Clear
+// internal_JSKeyedHistogram_Dataset
+// internal_WrapAndReturnKeyedHistogram
+//
+// Same comments as above, at the JSHistogram_* section, regarding
+// deadlock avoidance, apply.
+
+namespace {
+
+bool
+internal_KeyedHistogram_SnapshotImpl(JSContext *cx, unsigned argc,
+ JS::Value *vp,
+ bool subsession, bool clearSubsession)
+{
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
+ if (!keyed) {
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (args.length() == 0) {
+ JS::RootedObject snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ JS_ReportErrorASCII(cx, "Failed to create object");
+ return false;
+ }
+
+ if (!NS_SUCCEEDED(keyed->GetJSSnapshot(cx, snapshot, subsession, clearSubsession))) {
+ JS_ReportErrorASCII(cx, "Failed to reflect keyed histograms");
+ return false;
+ }
+
+ args.rval().setObject(*snapshot);
+ return true;
+ }
+
+ nsAutoJSString key;
+ if (!args[0].isString() || !key.init(cx, args[0])) {
+ JS_ReportErrorASCII(cx, "Not a string");
+ return false;
+ }
+
+ Histogram* h = nullptr;
+ nsresult rv = keyed->GetHistogram(NS_ConvertUTF16toUTF8(key), &h, subsession);
+ if (NS_FAILED(rv)) {
+ JS_ReportErrorASCII(cx, "Failed to get histogram");
+ return false;
+ }
+
+ JS::RootedObject snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ return false;
+ }
+
+ switch (internal_ReflectHistogramSnapshot(cx, snapshot, h)) {
+ case REFLECT_FAILURE:
+ return false;
+ case REFLECT_CORRUPT:
+ JS_ReportErrorASCII(cx, "Histogram is corrupt");
+ return false;
+ case REFLECT_OK:
+ args.rval().setObject(*snapshot);
+ return true;
+ default:
+ MOZ_CRASH("unhandled reflection status");
+ }
+}
+
+bool
+internal_JSKeyedHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
+ if (!keyed) {
+ return false;
+ }
+
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+ if (args.length() < 1) {
+ JS_ReportErrorASCII(cx, "Expected one argument");
+ return false;
+ }
+
+ nsAutoJSString key;
+ if (!args[0].isString() || !key.init(cx, args[0])) {
+ JS_ReportErrorASCII(cx, "Not a string");
+ return false;
+ }
+
+ const uint32_t type = keyed->GetHistogramType();
+
+ // If we don't have an argument for the count histogram, assume an increment of 1.
+ // Otherwise, make sure to run some sanity checks on the argument.
+ int32_t value = 1;
+ if ((type != base::CountHistogram::COUNT_HISTOGRAM) || (args.length() == 2)) {
+ if (args.length() < 2) {
+ JS_ReportErrorASCII(cx, "Expected two arguments for this histogram type");
+ return false;
+ }
+
+ if (!(args[1].isNumber() || args[1].isBoolean())) {
+ JS_ReportErrorASCII(cx, "Not a number");
+ return false;
+ }
+
+ if (!JS::ToInt32(cx, args[1], &value)) {
+ return false;
+ }
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(*keyed, NS_ConvertUTF16toUTF8(key), value);
+ }
+ return true;
+}
+
+bool
+internal_JSKeyedHistogram_Keys(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
+ if (!keyed) {
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ return NS_SUCCEEDED(keyed->GetJSKeys(cx, args));
+}
+
+bool
+internal_JSKeyedHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, false, false);
+}
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+bool
+internal_JSKeyedHistogram_SubsessionSnapshot(JSContext *cx,
+ unsigned argc, JS::Value *vp)
+{
+ return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, true, false);
+}
+#endif
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+bool
+internal_JSKeyedHistogram_SnapshotSubsessionAndClear(JSContext *cx,
+ unsigned argc,
+ JS::Value *vp)
+{
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ if (args.length() != 0) {
+ JS_ReportErrorASCII(cx, "No key arguments supported for snapshotSubsessionAndClear");
+ }
+
+ return internal_KeyedHistogram_SnapshotImpl(cx, argc, vp, true, true);
+}
+#endif
+
+bool
+internal_JSKeyedHistogram_Clear(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
+ if (!keyed) {
+ return false;
+ }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ bool onlySubsession = false;
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (args.length() >= 1) {
+ if (!(args[0].isNumber() || args[0].isBoolean())) {
+ JS_ReportErrorASCII(cx, "Not a boolean");
+ return false;
+ }
+
+ onlySubsession = JS::ToBoolean(args[0]);
+ }
+
+ keyed->Clear(onlySubsession);
+#else
+ keyed->Clear(false);
+#endif
+ return true;
+}
+
+bool
+internal_JSKeyedHistogram_Dataset(JSContext *cx, unsigned argc, JS::Value *vp)
+{
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+ JSObject *obj = JS_THIS_OBJECT(cx, vp);
+ if (!obj) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = static_cast<KeyedHistogram*>(JS_GetPrivate(obj));
+ if (!keyed) {
+ return false;
+ }
+
+ uint32_t dataset = nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN;
+ nsresult rv = keyed->GetDataset(&dataset);;
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ args.rval().setNumber(dataset);
+ return true;
+}
+
+// NOTE: Runs without protection from |gTelemetryHistogramMutex|.
+// See comment at the top of this section.
+nsresult
+internal_WrapAndReturnKeyedHistogram(KeyedHistogram *h, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ static const JSClass JSHistogram_class = {
+ "JSKeyedHistogram", /* name */
+ JSCLASS_HAS_PRIVATE /* flags */
+ };
+
+ JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &JSHistogram_class));
+ if (!obj)
+ return NS_ERROR_FAILURE;
+ // The 7 functions that are wrapped up here are eventually called
+ // by the same thread that runs this function.
+ if (!(JS_DefineFunction(cx, obj, "add", internal_JSKeyedHistogram_Add, 2, 0)
+ && JS_DefineFunction(cx, obj, "snapshot",
+ internal_JSKeyedHistogram_Snapshot, 1, 0)
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ && JS_DefineFunction(cx, obj, "subsessionSnapshot",
+ internal_JSKeyedHistogram_SubsessionSnapshot, 1, 0)
+ && JS_DefineFunction(cx, obj, "snapshotSubsessionAndClear",
+ internal_JSKeyedHistogram_SnapshotSubsessionAndClear, 0, 0)
+#endif
+ && JS_DefineFunction(cx, obj, "keys",
+ internal_JSKeyedHistogram_Keys, 0, 0)
+ && JS_DefineFunction(cx, obj, "clear",
+ internal_JSKeyedHistogram_Clear, 0, 0)
+ && JS_DefineFunction(cx, obj, "dataset",
+ internal_JSKeyedHistogram_Dataset, 0, 0))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS_SetPrivate(obj, h);
+ ret.setObject(*obj);
+ return NS_OK;
+}
+
+} // namespace
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryHistogram::
+
+// All of these functions are actually in namespace TelemetryHistogram::,
+// but the ::TelemetryHistogram prefix is given explicitly. This is
+// because it is critical to see which calls from these functions are
+// to another function in this interface. Mis-identifying "inwards
+// calls" from "calls to another function in this interface" will lead
+// to deadlocking and/or races. See comments at the top of the file
+// for further (important!) details.
+
+// Create and destroy the singleton StatisticsRecorder object.
+void TelemetryHistogram::CreateStatisticsRecorder()
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(!gStatisticsRecorder);
+ gStatisticsRecorder = new base::StatisticsRecorder();
+}
+
+void TelemetryHistogram::DestroyStatisticsRecorder()
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(gStatisticsRecorder);
+ if (gStatisticsRecorder) {
+ delete gStatisticsRecorder;
+ gStatisticsRecorder = nullptr;
+ }
+}
+
+void TelemetryHistogram::InitializeGlobalState(bool canRecordBase,
+ bool canRecordExtended)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(!gInitDone, "TelemetryHistogram::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = canRecordBase;
+ gCanRecordExtended = canRecordExtended;
+
+ // gHistogramMap should have been pre-sized correctly at the
+ // declaration point further up in this file.
+
+ // Populate the static histogram name->id cache.
+ // Note that the histogram names are statically allocated.
+ for (uint32_t i = 0; i < mozilla::Telemetry::HistogramCount; i++) {
+ CharPtrEntryType *entry = gHistogramMap.PutEntry(gHistograms[i].id());
+ entry->mData = (mozilla::Telemetry::ID) i;
+ }
+
+#ifdef DEBUG
+ gHistogramMap.MarkImmutable();
+#endif
+
+ mozilla::PodArrayZero(gCorruptHistograms);
+
+ // Create registered keyed histograms
+ for (size_t i = 0; i < mozilla::ArrayLength(gHistograms); ++i) {
+ const HistogramInfo& h = gHistograms[i];
+ if (!h.keyed) {
+ continue;
+ }
+
+ const nsDependentCString id(h.id());
+ const nsDependentCString expiration(h.expiration());
+ gKeyedHistograms.Put(id, new KeyedHistogram(id, expiration, h.histogramType,
+ h.min, h.max, h.bucketCount, h.dataset));
+ if (XRE_IsParentProcess()) {
+ // We must create registered child keyed histograms as well or else the
+ // same code in TelemetrySession.jsm that fails without parent keyed
+ // histograms will fail without child keyed histograms.
+ nsCString contentId(id);
+ contentId.AppendLiteral(CONTENT_HISTOGRAM_SUFFIX);
+ gKeyedHistograms.Put(contentId,
+ new KeyedHistogram(id, expiration, h.histogramType,
+ h.min, h.max, h.bucketCount, h.dataset));
+
+
+ nsCString gpuId(id);
+ gpuId.AppendLiteral(GPU_HISTOGRAM_SUFFIX);
+ gKeyedHistograms.Put(gpuId,
+ new KeyedHistogram(id, expiration, h.histogramType,
+ h.min, h.max, h.bucketCount, h.dataset));
+ }
+ }
+
+ // Some Telemetry histograms depend on the value of C++ constants and hardcode
+ // their values in Histograms.json.
+ // We add static asserts here for those values to match so that future changes
+ // don't go unnoticed.
+ // TODO: Compare explicitly with gHistograms[<histogram id>].bucketCount here
+ // once we can make gHistograms constexpr (requires VS2015).
+ static_assert((JS::gcreason::NUM_TELEMETRY_REASONS == 100),
+ "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json."
+ " If this was an intentional change, update this assert with its value "
+ "and update the n_values for the following in Histograms.json: "
+ "GC_MINOR_REASON, GC_MINOR_REASON_LONG, GC_REASON_2");
+ static_assert((mozilla::StartupTimeline::MAX_EVENT_ID == 16),
+ "MAX_EVENT_ID is assumed to be a fixed value in Histograms.json. If this"
+ " was an intentional change, update this assert with its value and update"
+ " the n_values for the following in Histograms.json:"
+ " STARTUP_MEASUREMENT_ERRORS");
+
+ gInitDone = true;
+}
+
+void TelemetryHistogram::DeInitializeGlobalState()
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gHistogramMap.Clear();
+ gKeyedHistograms.Clear();
+ gAddonMap.Clear();
+ gAccumulations = nullptr;
+ gKeyedAccumulations = nullptr;
+ if (gIPCTimer) {
+ NS_RELEASE(gIPCTimer);
+ }
+ gInitDone = false;
+}
+
+#ifdef DEBUG
+bool TelemetryHistogram::GlobalStateHasBeenInitialized() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return gInitDone;
+}
+#endif
+
+bool
+TelemetryHistogram::CanRecordBase() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_CanRecordBase();
+}
+
+void
+TelemetryHistogram::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordBase = b;
+}
+
+bool
+TelemetryHistogram::CanRecordExtended() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_CanRecordExtended();
+}
+
+void
+TelemetryHistogram::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordExtended = b;
+}
+
+
+void
+TelemetryHistogram::InitHistogramRecordingEnabled()
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ const size_t length = mozilla::ArrayLength(kRecordingInitiallyDisabledIDs);
+ for (size_t i = 0; i < length; i++) {
+ internal_SetHistogramRecordingEnabled(kRecordingInitiallyDisabledIDs[i],
+ false);
+ }
+}
+
+void
+TelemetryHistogram::SetHistogramRecordingEnabled(mozilla::Telemetry::ID aID,
+ bool aEnabled)
+{
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_SetHistogramRecordingEnabled(aID, aEnabled);
+}
+
+
+nsresult
+TelemetryHistogram::SetHistogramRecordingEnabled(const nsACString &id,
+ bool aEnabled)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ Histogram *h;
+ nsresult rv = internal_GetHistogramByName(id, &h);
+ if (NS_SUCCEEDED(rv)) {
+ h->SetRecordingEnabled(aEnabled);
+ return NS_OK;
+ }
+
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(id);
+ if (keyed) {
+ keyed->SetRecordingEnabled(aEnabled);
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+
+void
+TelemetryHistogram::Accumulate(mozilla::Telemetry::ID aID,
+ uint32_t aSample)
+{
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(aID, aSample);
+}
+
+void
+TelemetryHistogram::Accumulate(mozilla::Telemetry::ID aID,
+ const nsCString& aKey, uint32_t aSample)
+{
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(aID, aKey, aSample);
+}
+
+void
+TelemetryHistogram::Accumulate(const char* name, uint32_t sample)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ mozilla::Telemetry::ID id;
+ nsresult rv = internal_GetHistogramEnumId(name, &id);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ internal_Accumulate(id, sample);
+}
+
+void
+TelemetryHistogram::Accumulate(const char* name,
+ const nsCString& key, uint32_t sample)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ mozilla::Telemetry::ID id;
+ nsresult rv = internal_GetHistogramEnumId(name, &id);
+ if (NS_SUCCEEDED(rv)) {
+ internal_Accumulate(id, key, sample);
+ }
+}
+
+void
+TelemetryHistogram::AccumulateCategorical(mozilla::Telemetry::ID aId,
+ const nsCString& label)
+{
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ uint32_t labelId = 0;
+ if (NS_FAILED(gHistograms[aId].label_id(label.get(), &labelId))) {
+ return;
+ }
+ internal_Accumulate(aId, labelId);
+}
+
+void
+TelemetryHistogram::AccumulateChild(GeckoProcessType aProcessType,
+ const nsTArray<Accumulation>& aAccumulations)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aAccumulations.Length(); ++i) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ continue;
+ }
+ internal_AccumulateChild(aProcessType, aAccumulations[i].mId, aAccumulations[i].mSample);
+ }
+}
+
+void
+TelemetryHistogram::AccumulateChildKeyed(GeckoProcessType aProcessType,
+ const nsTArray<KeyedAccumulation>& aAccumulations)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aAccumulations.Length(); ++i) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ continue;
+ }
+ internal_AccumulateChildKeyed(aProcessType,
+ aAccumulations[i].mId,
+ aAccumulations[i].mKey,
+ aAccumulations[i].mSample);
+ }
+}
+
+nsresult
+TelemetryHistogram::GetHistogramById(const nsACString &name, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ Histogram *h = nullptr;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramByName(name, &h);
+ if (NS_FAILED(rv))
+ return rv;
+ }
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnHistogram(h, cx, ret);
+}
+
+nsresult
+TelemetryHistogram::GetKeyedHistogramById(const nsACString &name,
+ JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ KeyedHistogram* keyed = nullptr;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!gKeyedHistograms.Get(name, &keyed)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnKeyedHistogram(keyed, cx, ret);
+}
+
+const char*
+TelemetryHistogram::GetHistogramName(mozilla::Telemetry::ID id)
+{
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(id))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return nullptr;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ const HistogramInfo& h = gHistograms[id];
+ return h.id();
+}
+
+nsresult
+TelemetryHistogram::CreateHistogramSnapshots(JSContext *cx,
+ JS::MutableHandle<JS::Value> ret,
+ bool subsession,
+ bool clearSubsession)
+{
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
+ if (!root_obj)
+ return NS_ERROR_FAILURE;
+ ret.setObject(*root_obj);
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = false;
+ if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) {
+ includeGPUProcess = gpm->AttemptedGPUProcess();
+ }
+
+ // Ensure that all the HISTOGRAM_FLAG & HISTOGRAM_COUNT histograms have
+ // been created, so that their values are snapshotted.
+ for (size_t i = 0; i < mozilla::Telemetry::HistogramCount; ++i) {
+ if (gHistograms[i].keyed) {
+ continue;
+ }
+ const uint32_t type = gHistograms[i].histogramType;
+ if (type == nsITelemetry::HISTOGRAM_FLAG ||
+ type == nsITelemetry::HISTOGRAM_COUNT) {
+ Histogram *h;
+ mozilla::DebugOnly<nsresult> rv;
+ mozilla::Telemetry::ID id = mozilla::Telemetry::ID(i);
+
+ rv = internal_GetHistogramByEnumId(id, &h, GeckoProcessType_Default);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = internal_GetHistogramByEnumId(id, &h, GeckoProcessType_Content);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ if (includeGPUProcess) {
+ rv = internal_GetHistogramByEnumId(id, &h, GeckoProcessType_GPU);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+ }
+
+ StatisticsRecorder::Histograms hs;
+ StatisticsRecorder::GetHistograms(&hs);
+
+ // We identify corrupt histograms first, rather than interspersing it
+ // in the loop below, to ensure that our corruption statistics don't
+ // depend on histogram enumeration order.
+ //
+ // Of course, we hope that all of these corruption-statistics
+ // histograms are not themselves corrupt...
+ internal_IdentifyCorruptHistograms(hs);
+
+ // OK, now we can actually reflect things.
+ JS::Rooted<JSObject*> hobj(cx);
+ for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) {
+ Histogram *h = *it;
+ if (!internal_ShouldReflectHistogram(h) || internal_IsEmpty(h) ||
+ internal_IsExpired(h)) {
+ continue;
+ }
+
+ Histogram* original = h;
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ if (subsession) {
+ h = internal_GetSubsessionHistogram(*h);
+ if (!h) {
+ continue;
+ }
+ }
+#endif
+
+ hobj = JS_NewPlainObject(cx);
+ if (!hobj) {
+ return NS_ERROR_FAILURE;
+ }
+ switch (internal_ReflectHistogramSnapshot(cx, hobj, h)) {
+ case REFLECT_CORRUPT:
+ // We can still hit this case even if ShouldReflectHistograms
+ // returns true. The histogram lies outside of our control
+ // somehow; just skip it.
+ continue;
+ case REFLECT_FAILURE:
+ return NS_ERROR_FAILURE;
+ case REFLECT_OK:
+ if (!JS_DefineProperty(cx, root_obj, original->histogram_name().c_str(),
+ hobj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
+ if (subsession && clearSubsession) {
+ h->Clear();
+ }
+#endif
+ }
+ return NS_OK;
+}
+
+nsresult
+TelemetryHistogram::RegisteredHistograms(uint32_t aDataset, uint32_t *aCount,
+ char*** aHistograms)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_GetRegisteredHistogramIds(false,
+ aDataset, aCount, aHistograms);
+}
+
+nsresult
+TelemetryHistogram::RegisteredKeyedHistograms(uint32_t aDataset,
+ uint32_t *aCount,
+ char*** aHistograms)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_GetRegisteredHistogramIds(true,
+ aDataset, aCount, aHistograms);
+}
+
+nsresult
+TelemetryHistogram::GetKeyedHistogramSnapshots(JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (auto iter = gKeyedHistograms.Iter(); !iter.Done(); iter.Next()) {
+ JS::RootedObject snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!NS_SUCCEEDED(iter.Data()->GetJSSnapshot(cx, snapshot, false, false))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(cx, obj, PromiseFlatCString(iter.Key()).get(),
+ snapshot, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ ret.setObject(*obj);
+ return NS_OK;
+}
+
+nsresult
+TelemetryHistogram::RegisterAddonHistogram(const nsACString &id,
+ const nsACString &name,
+ uint32_t histogramType,
+ uint32_t min, uint32_t max,
+ uint32_t bucketCount,
+ uint8_t optArgCount)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (histogramType == nsITelemetry::HISTOGRAM_EXPONENTIAL ||
+ histogramType == nsITelemetry::HISTOGRAM_LINEAR) {
+ if (optArgCount != 3) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Sanity checks for histogram parameters.
+ if (min >= max)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (bucketCount <= 2)
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ if (min < 1)
+ return NS_ERROR_ILLEGAL_VALUE;
+ } else {
+ min = 1;
+ max = 2;
+ bucketCount = 3;
+ }
+
+ AddonEntryType *addonEntry = gAddonMap.GetEntry(id);
+ if (!addonEntry) {
+ addonEntry = gAddonMap.PutEntry(id);
+ if (MOZ_UNLIKELY(!addonEntry)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ addonEntry->mData = new AddonHistogramMapType();
+ }
+
+ AddonHistogramMapType *histogramMap = addonEntry->mData;
+ AddonHistogramEntryType *histogramEntry = histogramMap->GetEntry(name);
+ // Can't re-register the same histogram.
+ if (histogramEntry) {
+ return NS_ERROR_FAILURE;
+ }
+
+ histogramEntry = histogramMap->PutEntry(name);
+ if (MOZ_UNLIKELY(!histogramEntry)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ AddonHistogramInfo &info = histogramEntry->mData;
+ info.min = min;
+ info.max = max;
+ info.bucketCount = bucketCount;
+ info.histogramType = histogramType;
+
+ return NS_OK;
+}
+
+nsresult
+TelemetryHistogram::GetAddonHistogram(const nsACString &id,
+ const nsACString &name,
+ JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ AddonHistogramInfo* info = nullptr;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ AddonEntryType *addonEntry = gAddonMap.GetEntry(id);
+ // The given id has not been registered.
+ if (!addonEntry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AddonHistogramMapType *histogramMap = addonEntry->mData;
+ AddonHistogramEntryType *histogramEntry = histogramMap->GetEntry(name);
+ // The given histogram name has not been registered.
+ if (!histogramEntry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ info = &histogramEntry->mData;
+ if (!info->h) {
+ nsAutoCString actualName;
+ internal_AddonHistogramName(id, name, actualName);
+ if (!internal_CreateHistogramForAddon(actualName, *info)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnHistogram(info->h, cx, ret);
+}
+
+nsresult
+TelemetryHistogram::UnregisterAddonHistograms(const nsACString &id)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ AddonEntryType *addonEntry = gAddonMap.GetEntry(id);
+ if (addonEntry) {
+ // Histogram's destructor is private, so this is the best we can do.
+ // The histograms the addon created *will* stick around, but they
+ // will be deleted if and when the addon registers histograms with
+ // the same names.
+ delete addonEntry->mData;
+ gAddonMap.RemoveEntry(addonEntry);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+TelemetryHistogram::GetAddonHistogramSnapshots(JSContext *cx,
+ JS::MutableHandle<JS::Value> ret)
+{
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!gAddonMap.ReflectIntoJS(internal_AddonReflector, cx, obj)) {
+ return NS_ERROR_FAILURE;
+ }
+ ret.setObject(*obj);
+ return NS_OK;
+}
+
+size_t
+TelemetryHistogram::GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf
+ aMallocSizeOf)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return gAddonMap.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ gHistogramMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t
+TelemetryHistogram::GetHistogramSizesofIncludingThis(mozilla::MallocSizeOf
+ aMallocSizeOf)
+{
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ StatisticsRecorder::Histograms hs;
+ StatisticsRecorder::GetHistograms(&hs);
+ size_t n = 0;
+ for (HistogramIterator it = hs.begin(); it != hs.end(); ++it) {
+ Histogram *h = *it;
+ n += h->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+// This method takes the lock only to double-buffer the batched telemetry.
+// It releases the lock before calling out to IPC code which can (and does)
+// Accumulate (which would deadlock)
+//
+// To ensure we don't loop IPCTimerFired->AccumulateChild->arm timer, we don't
+// unset gIPCTimerArmed until the IPC completes
+//
+// This function must be called on the main thread, otherwise IPC will fail.
+void
+TelemetryHistogram::IPCTimerFired(nsITimer* aTimer, void* aClosure)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ nsTArray<Accumulation> accumulationsToSend;
+ nsTArray<KeyedAccumulation> keyedAccumulationsToSend;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (gAccumulations) {
+ accumulationsToSend.SwapElements(*gAccumulations);
+ }
+ if (gKeyedAccumulations) {
+ keyedAccumulationsToSend.SwapElements(*gKeyedAccumulations);
+ }
+ }
+
+ switch (XRE_GetProcessType()) {
+ case GeckoProcessType_Content: {
+ mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton();
+ mozilla::Unused << NS_WARN_IF(!contentChild);
+ if (contentChild) {
+ if (accumulationsToSend.Length()) {
+ mozilla::Unused <<
+ NS_WARN_IF(!contentChild->SendAccumulateChildHistogram(accumulationsToSend));
+ }
+ if (keyedAccumulationsToSend.Length()) {
+ mozilla::Unused <<
+ NS_WARN_IF(!contentChild->SendAccumulateChildKeyedHistogram(keyedAccumulationsToSend));
+ }
+ }
+ break;
+ }
+ case GeckoProcessType_GPU: {
+ if (mozilla::gfx::GPUParent* gpu = mozilla::gfx::GPUParent::GetSingleton()) {
+ if (accumulationsToSend.Length()) {
+ mozilla::Unused << gpu->SendAccumulateChildHistogram(accumulationsToSend);
+ }
+ if (keyedAccumulationsToSend.Length()) {
+ mozilla::Unused << gpu->SendAccumulateChildKeyedHistogram(keyedAccumulationsToSend);
+ }
+ }
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported process type");
+ break;
+ }
+
+ gIPCTimerArmed = false;
+}
diff --git a/toolkit/components/telemetry/TelemetryHistogram.h b/toolkit/components/telemetry/TelemetryHistogram.h
new file mode 100644
index 0000000000..4aa13e259d
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryHistogram.h
@@ -0,0 +1,104 @@
+/* -*- 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 TelemetryHistogram_h__
+#define TelemetryHistogram_h__
+
+#include "mozilla/TelemetryHistogramEnums.h"
+
+#include "mozilla/TelemetryComms.h"
+#include "nsXULAppAPI.h"
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// histogram accumulation and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryHistogram {
+
+void CreateStatisticsRecorder();
+void DestroyStatisticsRecorder();
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+#ifdef DEBUG
+bool GlobalStateHasBeenInitialized();
+#endif
+
+bool CanRecordBase();
+void SetCanRecordBase(bool b);
+bool CanRecordExtended();
+void SetCanRecordExtended(bool b);
+
+void InitHistogramRecordingEnabled();
+void SetHistogramRecordingEnabled(mozilla::Telemetry::ID aID, bool aEnabled);
+
+nsresult SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled);
+
+void Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample);
+void Accumulate(mozilla::Telemetry::ID aID, const nsCString& aKey,
+ uint32_t aSample);
+void Accumulate(const char* name, uint32_t sample);
+void Accumulate(const char* name, const nsCString& key, uint32_t sample);
+
+void AccumulateCategorical(mozilla::Telemetry::ID aId, const nsCString& aLabel);
+
+void AccumulateChild(GeckoProcessType aProcessType,
+ const nsTArray<mozilla::Telemetry::Accumulation>& aAccumulations);
+void AccumulateChildKeyed(GeckoProcessType aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedAccumulation>& aAccumulations);
+
+nsresult
+GetHistogramById(const nsACString &name, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret);
+
+nsresult
+GetKeyedHistogramById(const nsACString &name, JSContext *cx,
+ JS::MutableHandle<JS::Value> ret);
+
+const char*
+GetHistogramName(mozilla::Telemetry::ID id);
+
+nsresult
+CreateHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret,
+ bool subsession, bool clearSubsession);
+
+nsresult
+RegisteredHistograms(uint32_t aDataset, uint32_t *aCount,
+ char*** aHistograms);
+
+nsresult
+RegisteredKeyedHistograms(uint32_t aDataset, uint32_t *aCount,
+ char*** aHistograms);
+
+nsresult
+GetKeyedHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret);
+
+nsresult
+RegisterAddonHistogram(const nsACString &id, const nsACString &name,
+ uint32_t histogramType, uint32_t min, uint32_t max,
+ uint32_t bucketCount, uint8_t optArgCount);
+
+nsresult
+GetAddonHistogram(const nsACString &id, const nsACString &name,
+ JSContext *cx, JS::MutableHandle<JS::Value> ret);
+
+nsresult
+UnregisterAddonHistograms(const nsACString &id);
+
+nsresult
+GetAddonHistogramSnapshots(JSContext *cx, JS::MutableHandle<JS::Value> ret);
+
+size_t
+GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+size_t
+GetHistogramSizesofIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+void
+IPCTimerFired(nsITimer* aTimer, void* aClosure);
+} // namespace TelemetryHistogram
+
+#endif // TelemetryHistogram_h__
diff --git a/toolkit/components/telemetry/TelemetryLog.jsm b/toolkit/components/telemetry/TelemetryLog.jsm
new file mode 100644
index 0000000000..ab62f195bc
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryLog.jsm
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = ["TelemetryLog"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
+var gLogEntries = [];
+
+this.TelemetryLog = Object.freeze({
+ log: function(id, data) {
+ id = String(id);
+ var ts;
+ try {
+ ts = Math.floor(Telemetry.msSinceProcessStart());
+ } catch (e) {
+ // If timestamp is screwed up, we just give up instead of making up
+ // data.
+ return;
+ }
+
+ var entry = [id, ts];
+ if (data !== undefined) {
+ entry = entry.concat(Array.prototype.map.call(data, String));
+ }
+ gLogEntries.push(entry);
+ },
+
+ entries: function() {
+ return gLogEntries;
+ }
+});
diff --git a/toolkit/components/telemetry/TelemetryReportingPolicy.jsm b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm
new file mode 100644
index 0000000000..d9c99df492
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryReportingPolicy.jsm
@@ -0,0 +1,496 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetryReportingPolicy"
+];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://services-common/observers.js", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend",
+ "resource://gre/modules/TelemetrySend.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryReportingPolicy::";
+
+// Oldest year to allow in date preferences. The FHR infobar was implemented in
+// 2012 and no dates older than that should be encountered.
+const OLDEST_ALLOWED_ACCEPTANCE_YEAR = 2012;
+
+const PREF_BRANCH = "datareporting.policy.";
+// Indicates whether this is the first run or not. This is used to decide when to display
+// the policy.
+const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun";
+// Allows to skip the datachoices infobar. This should only be used in tests.
+const PREF_BYPASS_NOTIFICATION = PREF_BRANCH + "dataSubmissionPolicyBypassNotification";
+// The submission kill switch: if this preference is disable, no submission will ever take place.
+const PREF_DATA_SUBMISSION_ENABLED = PREF_BRANCH + "dataSubmissionEnabled";
+// This preference holds the current policy version, which overrides
+// DEFAULT_DATAREPORTING_POLICY_VERSION
+const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion";
+// This indicates the minimum required policy version. If the accepted policy version
+// is lower than this, the notification bar must be showed again.
+const PREF_MINIMUM_POLICY_VERSION = PREF_BRANCH + "minimumPolicyVersion";
+// The version of the accepted policy.
+const PREF_ACCEPTED_POLICY_VERSION = PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion";
+// The date user accepted the policy.
+const PREF_ACCEPTED_POLICY_DATE = PREF_BRANCH + "dataSubmissionPolicyNotifiedTime";
+// URL of privacy policy to be opened in a background tab on first run instead of showing the
+// data choices infobar.
+const PREF_FIRST_RUN_URL = PREF_BRANCH + "firstRunURL";
+// The following preferences are deprecated and will be purged during the preferences
+// migration process.
+const DEPRECATED_FHR_PREFS = [
+ PREF_BRANCH + "dataSubmissionPolicyAccepted",
+ PREF_BRANCH + "dataSubmissionPolicyBypassAcceptance",
+ PREF_BRANCH + "dataSubmissionPolicyResponseType",
+ PREF_BRANCH + "dataSubmissionPolicyResponseTime"
+];
+
+// How much time until we display the data choices notification bar, on the first run.
+const NOTIFICATION_DELAY_FIRST_RUN_MSEC = 60 * 1000; // 60s
+// Same as above, for the next runs.
+const NOTIFICATION_DELAY_NEXT_RUNS_MSEC = 10 * 1000; // 10s
+
+/**
+ * This is a policy object used to override behavior within this module.
+ * Tests override properties on this object to allow for control of behavior
+ * that would otherwise be very hard to cover.
+ */
+var Policy = {
+ now: () => new Date(),
+ setShowInfobarTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+ clearShowInfobarTimeout: (id) => clearTimeout(id),
+};
+
+/**
+ * Represents a request to display data policy.
+ *
+ * Receivers of these instances are expected to call one or more of the on*
+ * functions when events occur.
+ *
+ * When one of these requests is received, the first thing a callee should do
+ * is present notification to the user of the data policy. When the notice
+ * is displayed to the user, the callee should call `onUserNotifyComplete`.
+ *
+ * If for whatever reason the callee could not display a notice,
+ * it should call `onUserNotifyFailed`.
+ *
+ * @param {Object} aLog The log object used to log the error in case of failures.
+ */
+function NotifyPolicyRequest(aLog) {
+ this._log = aLog;
+}
+
+NotifyPolicyRequest.prototype = Object.freeze({
+ /**
+ * Called when the user is notified of the policy.
+ */
+ onUserNotifyComplete: function() {
+ return TelemetryReportingPolicyImpl._userNotified();
+ },
+
+ /**
+ * Called when there was an error notifying the user about the policy.
+ *
+ * @param error
+ * (Error) Explains what went wrong.
+ */
+ onUserNotifyFailed: function (error) {
+ this._log.error("onUserNotifyFailed - " + error);
+ },
+});
+
+this.TelemetryReportingPolicy = {
+ // The current policy version number. If the version number stored in the prefs
+ // is smaller than this, data upload will be disabled until the user is re-notified
+ // about the policy changes.
+ DEFAULT_DATAREPORTING_POLICY_VERSION: 1,
+
+ /**
+ * Setup the policy.
+ */
+ setup: function() {
+ return TelemetryReportingPolicyImpl.setup();
+ },
+
+ /**
+ * Shutdown and clear the policy.
+ */
+ shutdown: function() {
+ return TelemetryReportingPolicyImpl.shutdown();
+ },
+
+ /**
+ * Check if we are allowed to upload data. In order to submit data both these conditions
+ * should be true:
+ * - The data submission preference should be true.
+ * - The datachoices infobar should have been displayed.
+ *
+ * @return {Boolean} True if we are allowed to upload data, false otherwise.
+ */
+ canUpload: function() {
+ return TelemetryReportingPolicyImpl.canUpload();
+ },
+
+ /**
+ * Test only method, restarts the policy.
+ */
+ reset: function() {
+ return TelemetryReportingPolicyImpl.reset();
+ },
+
+ /**
+ * Test only method, used to check if user is notified of the policy in tests.
+ */
+ testIsUserNotified: function() {
+ return TelemetryReportingPolicyImpl.isUserNotifiedOfCurrentPolicy;
+ },
+
+ /**
+ * Test only method, used to simulate the infobar being shown in xpcshell tests.
+ */
+ testInfobarShown: function() {
+ return TelemetryReportingPolicyImpl._userNotified();
+ },
+};
+
+var TelemetryReportingPolicyImpl = {
+ _logger: null,
+ // Keep track of the notification status if user wasn't notified already.
+ _notificationInProgress: false,
+ // The timer used to show the datachoices notification at startup.
+ _startupNotificationTimerId: null,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Get the date the policy was notified.
+ * @return {Object} A date object or null on errors.
+ */
+ get dataSubmissionPolicyNotifiedDate() {
+ let prefString = Preferences.get(PREF_ACCEPTED_POLICY_DATE, "0");
+ let valueInteger = parseInt(prefString, 10);
+
+ // Bail out if we didn't store any value yet.
+ if (valueInteger == 0) {
+ this._log.info("get dataSubmissionPolicyNotifiedDate - No date stored yet.");
+ return null;
+ }
+
+ // If an invalid value is saved in the prefs, bail out too.
+ if (Number.isNaN(valueInteger)) {
+ this._log.error("get dataSubmissionPolicyNotifiedDate - Invalid date stored.");
+ return null;
+ }
+
+ // Make sure the notification date is newer then the oldest allowed date.
+ let date = new Date(valueInteger);
+ if (date.getFullYear() < OLDEST_ALLOWED_ACCEPTANCE_YEAR) {
+ this._log.error("get dataSubmissionPolicyNotifiedDate - The stored date is too old.");
+ return null;
+ }
+
+ return date;
+ },
+
+ /**
+ * Set the date the policy was notified.
+ * @param {Object} aDate A valid date object.
+ */
+ set dataSubmissionPolicyNotifiedDate(aDate) {
+ this._log.trace("set dataSubmissionPolicyNotifiedDate - aDate: " + aDate);
+
+ if (!aDate || aDate.getFullYear() < OLDEST_ALLOWED_ACCEPTANCE_YEAR) {
+ this._log.error("set dataSubmissionPolicyNotifiedDate - Invalid notification date.");
+ return;
+ }
+
+ Preferences.set(PREF_ACCEPTED_POLICY_DATE, aDate.getTime().toString());
+ },
+
+ /**
+ * Whether submission of data is allowed.
+ *
+ * This is the master switch for remote server communication. If it is
+ * false, we never request upload or deletion.
+ */
+ get dataSubmissionEnabled() {
+ // Default is true because we are opt-out.
+ return Preferences.get(PREF_DATA_SUBMISSION_ENABLED, true);
+ },
+
+ get currentPolicyVersion() {
+ return Preferences.get(PREF_CURRENT_POLICY_VERSION,
+ TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION);
+ },
+
+ /**
+ * The minimum policy version which for dataSubmissionPolicyAccepted to
+ * to be valid.
+ */
+ get minimumPolicyVersion() {
+ const minPolicyVersion = Preferences.get(PREF_MINIMUM_POLICY_VERSION, 1);
+
+ // First check if the current channel has a specific minimum policy version. If not,
+ // use the general minimum policy version.
+ let channel = "";
+ try {
+ channel = UpdateUtils.getUpdateChannel(false);
+ } catch (e) {
+ this._log.error("minimumPolicyVersion - Unable to retrieve the current channel.");
+ return minPolicyVersion;
+ }
+ const channelPref = PREF_MINIMUM_POLICY_VERSION + ".channel-" + channel;
+ return Preferences.get(channelPref, minPolicyVersion);
+ },
+
+ get dataSubmissionPolicyAcceptedVersion() {
+ return Preferences.get(PREF_ACCEPTED_POLICY_VERSION, 0);
+ },
+
+ set dataSubmissionPolicyAcceptedVersion(value) {
+ Preferences.set(PREF_ACCEPTED_POLICY_VERSION, value);
+ },
+
+ /**
+ * Checks to see if the user has been notified about data submission
+ * @return {Bool} True if user has been notified and the notification is still valid,
+ * false otherwise.
+ */
+ get isUserNotifiedOfCurrentPolicy() {
+ // If we don't have a sane notification date, the user was not notified yet.
+ if (!this.dataSubmissionPolicyNotifiedDate ||
+ this.dataSubmissionPolicyNotifiedDate.getTime() <= 0) {
+ return false;
+ }
+
+ // The accepted policy version should not be less than the minimum policy version.
+ if (this.dataSubmissionPolicyAcceptedVersion < this.minimumPolicyVersion) {
+ return false;
+ }
+
+ // Otherwise the user was already notified.
+ return true;
+ },
+
+ /**
+ * Test only method, restarts the policy.
+ */
+ reset: function() {
+ this.shutdown();
+ return this.setup();
+ },
+
+ /**
+ * Setup the policy.
+ */
+ setup: function() {
+ this._log.trace("setup");
+
+ // Migrate the data choices infobar, if needed.
+ this._migratePreferences();
+
+ // Add the event observers.
+ Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+ },
+
+ /**
+ * Clean up the reporting policy.
+ */
+ shutdown: function() {
+ this._log.trace("shutdown");
+
+ this._detachObservers();
+
+ Policy.clearShowInfobarTimeout(this._startupNotificationTimerId);
+ },
+
+ /**
+ * Detach the observers that were attached during setup.
+ */
+ _detachObservers: function() {
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ },
+
+ /**
+ * Check if we are allowed to upload data. In order to submit data both these conditions
+ * should be true:
+ * - The data submission preference should be true.
+ * - The datachoices infobar should have been displayed.
+ *
+ * @return {Boolean} True if we are allowed to upload data, false otherwise.
+ */
+ canUpload: function() {
+ // If data submission is disabled, there's no point in showing the infobar. Just
+ // forbid to upload.
+ if (!this.dataSubmissionEnabled) {
+ return false;
+ }
+
+ // Submission is enabled. We enable upload if user is notified or we need to bypass
+ // the policy.
+ const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
+ return this.isUserNotifiedOfCurrentPolicy || bypassNotification;
+ },
+
+ /**
+ * Migrate the data policy preferences, if needed.
+ */
+ _migratePreferences: function() {
+ // Current prefs are mostly the same than the old ones, except for some deprecated ones.
+ for (let pref of DEPRECATED_FHR_PREFS) {
+ Preferences.reset(pref);
+ }
+ },
+
+ /**
+ * Show the data choices infobar if the user wasn't already notified and data submission
+ * is enabled.
+ */
+ _showInfobar: function() {
+ if (!this.dataSubmissionEnabled) {
+ this._log.trace("_showInfobar - Data submission disabled by the policy.");
+ return;
+ }
+
+ const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, false);
+ if (this.isUserNotifiedOfCurrentPolicy || bypassNotification) {
+ this._log.trace("_showInfobar - User already notified or bypassing the policy.");
+ return;
+ }
+
+ if (this._notificationInProgress) {
+ this._log.trace("_showInfobar - User not notified, notification already in progress.");
+ return;
+ }
+
+ this._log.trace("_showInfobar - User not notified, notifying now.");
+ this._notificationInProgress = true;
+ let request = new NotifyPolicyRequest(this._log);
+ Observers.notify("datareporting:notify-data-policy:request", request);
+ },
+
+ /**
+ * Called when the user is notified with the infobar or otherwise.
+ */
+ _userNotified() {
+ this._log.trace("_userNotified");
+ this._recordNotificationData();
+ TelemetrySend.notifyCanUpload();
+ },
+
+ /**
+ * Record date and the version of the accepted policy.
+ */
+ _recordNotificationData: function() {
+ this._log.trace("_recordNotificationData");
+ this.dataSubmissionPolicyNotifiedDate = Policy.now();
+ this.dataSubmissionPolicyAcceptedVersion = this.currentPolicyVersion;
+ // The user was notified and the notification data saved: the notification
+ // is no longer in progress.
+ this._notificationInProgress = false;
+ },
+
+ /**
+ * Try to open the privacy policy in a background tab instead of showing the infobar.
+ */
+ _openFirstRunPage() {
+ let firstRunPolicyURL = Preferences.get(PREF_FIRST_RUN_URL, "");
+ if (!firstRunPolicyURL) {
+ return false;
+ }
+ firstRunPolicyURL = Services.urlFormatter.formatURL(firstRunPolicyURL);
+
+ let win;
+ try {
+ const { RecentWindow } = Cu.import("resource:///modules/RecentWindow.jsm", {});
+ win = RecentWindow.getMostRecentBrowserWindow();
+ } catch (e) {}
+
+ if (!win) {
+ this._log.info("Couldn't find browser window to open first-run page. Falling back to infobar.");
+ return false;
+ }
+
+ // We'll consider the user notified once the privacy policy has been loaded
+ // in a background tab even if that tab hasn't been selected.
+ let tab;
+ let progressListener = {};
+ progressListener.onStateChange =
+ (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) => {
+ if (aWebProgress.isTopLevel &&
+ tab &&
+ tab.linkedBrowser == aBrowser &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ let uri = aBrowser.documentURI;
+ if (uri && !/^about:(blank|neterror|certerror|blocked)/.test(uri.spec)) {
+ this._userNotified();
+ } else {
+ this._log.info("Failed to load first-run page. Falling back to infobar.");
+ this._showInfobar();
+ }
+ removeListeners();
+ }
+ };
+
+ let removeListeners = () => {
+ win.removeEventListener("unload", removeListeners);
+ win.gBrowser.removeTabsProgressListener(progressListener);
+ };
+
+ win.addEventListener("unload", removeListeners);
+ win.gBrowser.addTabsProgressListener(progressListener);
+
+ tab = win.gBrowser.loadOneTab(firstRunPolicyURL, { inBackground: true });
+
+ return true;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "sessionstore-windows-restored") {
+ return;
+ }
+
+ const isFirstRun = Preferences.get(PREF_FIRST_RUN, true);
+ if (isFirstRun) {
+ // We're performing the first run, flip firstRun preference for subsequent runs.
+ Preferences.set(PREF_FIRST_RUN, false);
+
+ try {
+ if (this._openFirstRunPage()) {
+ return;
+ }
+ } catch (e) {
+ this._log.error("Failed to open privacy policy tab: " + e);
+ }
+ }
+
+ // Show the info bar.
+ const delay =
+ isFirstRun ? NOTIFICATION_DELAY_FIRST_RUN_MSEC: NOTIFICATION_DELAY_NEXT_RUNS_MSEC;
+
+ this._startupNotificationTimerId = Policy.setShowInfobarTimeout(
+ // Calling |canUpload| eventually shows the infobar, if needed.
+ () => this._showInfobar(), delay);
+ },
+};
diff --git a/toolkit/components/telemetry/TelemetryScalar.cpp b/toolkit/components/telemetry/TelemetryScalar.cpp
new file mode 100644
index 0000000000..6e95580703
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryScalar.cpp
@@ -0,0 +1,1896 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsITelemetry.h"
+#include "nsIVariant.h"
+#include "nsVariant.h"
+#include "nsHashKeys.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsIXPConnect.h"
+#include "nsContentUtils.h"
+#include "nsThreadUtils.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/Unused.h"
+
+#include "TelemetryCommon.h"
+#include "TelemetryScalar.h"
+#include "TelemetryScalarData.h"
+
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::Telemetry::Common::AutoHashtable;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions named internal_*: these can only be reached via an
+// interface function (TelemetryScalar::*). They expect the interface
+// function to have acquired |gTelemetryScalarsMutex|, so they do not
+// have to be thread-safe.
+//
+// * Functions named TelemetryScalar::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryScalarsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryScalar::*) are
+// serialised using the mutex |gTelemetryScalarsMutex|. This means
+// that the external interface is thread-safe, and many of the
+// internal_* functions can ignore thread safety. But it also brings
+// a danger of deadlock if any function in the external interface can
+// get back to that interface. That is, we will deadlock on any call
+// chain like this
+//
+// TelemetryScalar::* -> .. any functions .. -> TelemetryScalar::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryScalar::* may directly call, nor take the
+// address of, any other function in TelemetryScalar::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryScalar::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kMaximumNumberOfKeys = 100;
+const uint32_t kMaximumKeyStringLength = 70;
+const uint32_t kMaximumStringValueLength = 50;
+const uint32_t kScalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+
+enum class ScalarResult : uint8_t {
+ // Nothing went wrong.
+ Ok,
+ // General Scalar Errors
+ OperationNotSupported,
+ InvalidType,
+ InvalidValue,
+ // Keyed Scalar Errors
+ KeyTooLong,
+ TooManyKeys,
+ // String Scalar Errors
+ StringTooLong,
+ // Unsigned Scalar Errors
+ UnsignedNegativeValue,
+ UnsignedTruncatedValue
+};
+
+typedef nsBaseHashtableET<nsDepCharHashKey, mozilla::Telemetry::ScalarID>
+ CharPtrEntryType;
+
+typedef AutoHashtable<CharPtrEntryType> ScalarMapType;
+
+/**
+ * Map the error codes used internally to NS_* error codes.
+ * @param aSr The error code used internally in this module.
+ * @return {nsresult} A NS_* error code.
+ */
+nsresult
+MapToNsResult(ScalarResult aSr)
+{
+ switch (aSr) {
+ case ScalarResult::Ok:
+ return NS_OK;
+ case ScalarResult::OperationNotSupported:
+ return NS_ERROR_NOT_AVAILABLE;
+ case ScalarResult::StringTooLong:
+ // We don't want to throw if we're setting a string that is too long.
+ return NS_OK;
+ case ScalarResult::InvalidType:
+ case ScalarResult::InvalidValue:
+ case ScalarResult::KeyTooLong:
+ return NS_ERROR_ILLEGAL_VALUE;
+ case ScalarResult::TooManyKeys:
+ return NS_ERROR_FAILURE;
+ case ScalarResult::UnsignedNegativeValue:
+ case ScalarResult::UnsignedTruncatedValue:
+ // We shouldn't throw if trying to set a negative number or are truncated,
+ // only warn the user.
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+bool
+IsValidEnumId(mozilla::Telemetry::ScalarID aID)
+{
+ return aID < mozilla::Telemetry::ScalarID::ScalarCount;
+}
+
+// Implements the methods for ScalarInfo.
+const char *
+ScalarInfo::name() const
+{
+ return &gScalarsStringTable[this->name_offset];
+}
+
+const char *
+ScalarInfo::expiration() const
+{
+ return &gScalarsStringTable[this->expiration_offset];
+}
+
+/**
+ * The base scalar object, that servers as a common ancestor for storage
+ * purposes.
+ */
+class ScalarBase
+{
+public:
+ virtual ~ScalarBase() {};
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ virtual ScalarResult SetValue(nsIVariant* aValue) = 0;
+ virtual ScalarResult AddValue(nsIVariant* aValue) { return ScalarResult::OperationNotSupported; }
+ virtual ScalarResult SetMaximum(nsIVariant* aValue) { return ScalarResult::OperationNotSupported; }
+
+ // Convenience methods used by the C++ API.
+ virtual void SetValue(uint32_t aValue) { mozilla::Unused << HandleUnsupported(); }
+ virtual ScalarResult SetValue(const nsAString& aValue) { return HandleUnsupported(); }
+ virtual void SetValue(bool aValue) { mozilla::Unused << HandleUnsupported(); }
+ virtual void AddValue(uint32_t aValue) { mozilla::Unused << HandleUnsupported(); }
+ virtual void SetMaximum(uint32_t aValue) { mozilla::Unused << HandleUnsupported(); }
+
+ // GetValue is used to get the value of the scalar when persisting it to JS.
+ virtual nsresult GetValue(nsCOMPtr<nsIVariant>& aResult) const = 0;
+
+ // To measure the memory stats.
+ virtual size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const = 0;
+
+private:
+ ScalarResult HandleUnsupported() const;
+};
+
+ScalarResult
+ScalarBase::HandleUnsupported() const
+{
+ MOZ_ASSERT(false, "This operation is not support for this scalar type.");
+ return ScalarResult::OperationNotSupported;
+}
+
+/**
+ * The implementation for the unsigned int scalar type.
+ */
+class ScalarUnsigned : public ScalarBase
+{
+public:
+ using ScalarBase::SetValue;
+
+ ScalarUnsigned() : mStorage(0) {};
+ ~ScalarUnsigned() {};
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(uint32_t aValue) final;
+ ScalarResult AddValue(nsIVariant* aValue) final;
+ void AddValue(uint32_t aValue) final;
+ ScalarResult SetMaximum(nsIVariant* aValue) final;
+ void SetMaximum(uint32_t aValue) final;
+ nsresult GetValue(nsCOMPtr<nsIVariant>& aResult) const final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+private:
+ uint32_t mStorage;
+
+ ScalarResult CheckInput(nsIVariant* aValue);
+
+ // Prevent copying.
+ ScalarUnsigned(const ScalarUnsigned& aOther) = delete;
+ void operator=(const ScalarUnsigned& aOther) = delete;
+};
+
+ScalarResult
+ScalarUnsigned::SetValue(nsIVariant* aValue)
+{
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ if (NS_FAILED(aValue->GetAsUint32(&mStorage))) {
+ return ScalarResult::InvalidValue;
+ }
+ return sr;
+}
+
+void
+ScalarUnsigned::SetValue(uint32_t aValue)
+{
+ mStorage = aValue;
+}
+
+ScalarResult
+ScalarUnsigned::AddValue(nsIVariant* aValue)
+{
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newAddend = 0;
+ nsresult rv = aValue->GetAsUint32(&newAddend);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+ mStorage += newAddend;
+ return sr;
+}
+
+void
+ScalarUnsigned::AddValue(uint32_t aValue)
+{
+ mStorage += aValue;
+}
+
+ScalarResult
+ScalarUnsigned::SetMaximum(nsIVariant* aValue)
+{
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newValue = 0;
+ nsresult rv = aValue->GetAsUint32(&newValue);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+ if (newValue > mStorage) {
+ mStorage = newValue;
+ }
+ return sr;
+}
+
+void
+ScalarUnsigned::SetMaximum(uint32_t aValue)
+{
+ if (aValue > mStorage) {
+ mStorage = aValue;
+ }
+}
+
+nsresult
+ScalarUnsigned::GetValue(nsCOMPtr<nsIVariant>& aResult) const
+{
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ nsresult rv = outVar->SetAsUint32(mStorage);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = outVar.forget();
+ return NS_OK;
+}
+
+size_t
+ScalarUnsigned::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ return aMallocSizeOf(this);
+}
+
+ScalarResult
+ScalarUnsigned::CheckInput(nsIVariant* aValue)
+{
+ // If this is a floating point value/double, we will probably get truncated.
+ uint16_t type;
+ aValue->GetDataType(&type);
+ if (type == nsIDataType::VTYPE_FLOAT ||
+ type == nsIDataType::VTYPE_DOUBLE) {
+ return ScalarResult::UnsignedTruncatedValue;
+ }
+
+ int32_t signedTest;
+ // If we're able to cast the number to an int, check its sign.
+ // Warn the user if he's trying to set the unsigned scalar to a negative
+ // number.
+ if (NS_SUCCEEDED(aValue->GetAsInt32(&signedTest)) &&
+ signedTest < 0) {
+ return ScalarResult::UnsignedNegativeValue;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * The implementation for the string scalar type.
+ */
+class ScalarString : public ScalarBase
+{
+public:
+ using ScalarBase::SetValue;
+
+ ScalarString() : mStorage(EmptyString()) {};
+ ~ScalarString() {};
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ ScalarResult SetValue(const nsAString& aValue) final;
+ nsresult GetValue(nsCOMPtr<nsIVariant>& aResult) const final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+private:
+ nsString mStorage;
+
+ // Prevent copying.
+ ScalarString(const ScalarString& aOther) = delete;
+ void operator=(const ScalarString& aOther) = delete;
+};
+
+ScalarResult
+ScalarString::SetValue(nsIVariant* aValue)
+{
+ // Check that we got the correct data type.
+ uint16_t type;
+ aValue->GetDataType(&type);
+ if (type != nsIDataType::VTYPE_CHAR &&
+ type != nsIDataType::VTYPE_WCHAR &&
+ type != nsIDataType::VTYPE_DOMSTRING &&
+ type != nsIDataType::VTYPE_CHAR_STR &&
+ type != nsIDataType::VTYPE_WCHAR_STR &&
+ type != nsIDataType::VTYPE_STRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_WSTRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_UTF8STRING &&
+ type != nsIDataType::VTYPE_CSTRING &&
+ type != nsIDataType::VTYPE_ASTRING) {
+ return ScalarResult::InvalidType;
+ }
+
+ nsAutoString convertedString;
+ nsresult rv = aValue->GetAsAString(convertedString);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+ return SetValue(convertedString);
+};
+
+ScalarResult
+ScalarString::SetValue(const nsAString& aValue)
+{
+ mStorage = Substring(aValue, 0, kMaximumStringValueLength);
+ if (aValue.Length() > kMaximumStringValueLength) {
+ return ScalarResult::StringTooLong;
+ }
+ return ScalarResult::Ok;
+}
+
+nsresult
+ScalarString::GetValue(nsCOMPtr<nsIVariant>& aResult) const
+{
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ nsresult rv = outVar->SetAsAString(mStorage);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = outVar.forget();
+ return NS_OK;
+}
+
+size_t
+ScalarString::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ size_t n = aMallocSizeOf(this);
+ n+= mStorage.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ return n;
+}
+
+/**
+ * The implementation for the boolean scalar type.
+ */
+class ScalarBoolean : public ScalarBase
+{
+public:
+ using ScalarBase::SetValue;
+
+ ScalarBoolean() : mStorage(false) {};
+ ~ScalarBoolean() {};
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(bool aValue) final;
+ nsresult GetValue(nsCOMPtr<nsIVariant>& aResult) const final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+private:
+ bool mStorage;
+
+ // Prevent copying.
+ ScalarBoolean(const ScalarBoolean& aOther) = delete;
+ void operator=(const ScalarBoolean& aOther) = delete;
+};
+
+ScalarResult
+ScalarBoolean::SetValue(nsIVariant* aValue)
+{
+ // Check that we got the correct data type.
+ uint16_t type;
+ aValue->GetDataType(&type);
+ if (type != nsIDataType::VTYPE_BOOL &&
+ type != nsIDataType::VTYPE_INT8 &&
+ type != nsIDataType::VTYPE_INT16 &&
+ type != nsIDataType::VTYPE_INT32 &&
+ type != nsIDataType::VTYPE_INT64 &&
+ type != nsIDataType::VTYPE_UINT8 &&
+ type != nsIDataType::VTYPE_UINT16 &&
+ type != nsIDataType::VTYPE_UINT32 &&
+ type != nsIDataType::VTYPE_UINT64) {
+ return ScalarResult::InvalidType;
+ }
+
+ if (NS_FAILED(aValue->GetAsBool(&mStorage))) {
+ return ScalarResult::InvalidValue;
+ }
+ return ScalarResult::Ok;
+};
+
+void
+ScalarBoolean::SetValue(bool aValue)
+{
+ mStorage = aValue;
+}
+
+nsresult
+ScalarBoolean::GetValue(nsCOMPtr<nsIVariant>& aResult) const
+{
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ nsresult rv = outVar->SetAsBool(mStorage);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = outVar.forget();
+ return NS_OK;
+}
+
+size_t
+ScalarBoolean::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ return aMallocSizeOf(this);
+}
+
+/**
+ * Allocate a scalar class given the scalar info.
+ *
+ * @param aInfo The informations for the scalar coming from the definition file.
+ * @return nullptr if the scalar type is unknown, otherwise a valid pointer to the
+ * scalar type.
+ */
+ScalarBase*
+internal_ScalarAllocate(uint32_t aScalarKind)
+{
+ ScalarBase* scalar = nullptr;
+ switch (aScalarKind) {
+ case nsITelemetry::SCALAR_COUNT:
+ scalar = new ScalarUnsigned();
+ break;
+ case nsITelemetry::SCALAR_STRING:
+ scalar = new ScalarString();
+ break;
+ case nsITelemetry::SCALAR_BOOLEAN:
+ scalar = new ScalarBoolean();
+ break;
+ default:
+ MOZ_ASSERT(false, "Invalid scalar type");
+ }
+ return scalar;
+}
+
+/**
+ * The implementation for the keyed scalar type.
+ */
+class KeyedScalar
+{
+public:
+ typedef mozilla::Pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
+
+ explicit KeyedScalar(uint32_t aScalarKind) : mScalarKind(aScalarKind) {};
+ ~KeyedScalar() {};
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ // These methods implicitly instantiate a Scalar[*] for each key.
+ ScalarResult SetValue(const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult AddValue(const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult SetMaximum(const nsAString& aKey, nsIVariant* aValue);
+
+ // Convenience methods used by the C++ API.
+ void SetValue(const nsAString& aKey, uint32_t aValue);
+ void SetValue(const nsAString& aKey, bool aValue);
+ void AddValue(const nsAString& aKey, uint32_t aValue);
+ void SetMaximum(const nsAString& aKey, uint32_t aValue);
+
+ // GetValue is used to get the key-value pairs stored in the keyed scalar
+ // when persisting it to JS.
+ nsresult GetValue(nsTArray<KeyValuePair>& aValues) const;
+
+ // To measure the memory stats.
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+private:
+ typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
+
+ ScalarKeysMapType mScalarKeys;
+ const uint32_t mScalarKind;
+
+ ScalarResult GetScalarForKey(const nsAString& aKey, ScalarBase** aRet);
+};
+
+ScalarResult
+KeyedScalar::SetValue(const nsAString& aKey, nsIVariant* aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+ScalarResult
+KeyedScalar::AddValue(const nsAString& aKey, nsIVariant* aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+ScalarResult
+KeyedScalar::SetMaximum(const nsAString& aKey, nsIVariant* aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+void
+KeyedScalar::SetValue(const nsAString& aKey, uint32_t aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void
+KeyedScalar::SetValue(const nsAString& aKey, bool aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void
+KeyedScalar::AddValue(const nsAString& aKey, uint32_t aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+ return;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+void
+KeyedScalar::SetMaximum(const nsAString& aKey, uint32_t aValue)
+{
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+ return;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+/**
+ * Get a key-value array with the values for the Keyed Scalar.
+ * @param aValue The array that will hold the key-value pairs.
+ * @return {nsresult} NS_OK or an error value as reported by the
+ * the specific scalar objects implementations (e.g.
+ * ScalarUnsigned).
+ */
+nsresult
+KeyedScalar::GetValue(nsTArray<KeyValuePair>& aValues) const
+{
+ for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) {
+ ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
+
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(scalarValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Append it to value list.
+ aValues.AppendElement(mozilla::MakePair(nsCString(iter.Key()), scalarValue));
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Get the scalar for the referenced key.
+ * If there's no such key, instantiate a new Scalar object with the
+ * same type of the Keyed scalar and create the key.
+ */
+ScalarResult
+KeyedScalar::GetScalarForKey(const nsAString& aKey, ScalarBase** aRet)
+{
+ if (aKey.Length() >= kMaximumKeyStringLength) {
+ return ScalarResult::KeyTooLong;
+ }
+
+ if (mScalarKeys.Count() >= kMaximumNumberOfKeys) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Key(aKey);
+
+ ScalarBase* scalar = nullptr;
+ if (mScalarKeys.Get(utf8Key, &scalar)) {
+ *aRet = scalar;
+ return ScalarResult::Ok;
+ }
+
+ scalar = internal_ScalarAllocate(mScalarKind);
+ if (!scalar) {
+ return ScalarResult::InvalidType;
+ }
+
+ mScalarKeys.Put(utf8Key, scalar);
+
+ *aRet = scalar;
+ return ScalarResult::Ok;
+}
+
+size_t
+KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ size_t n = aMallocSizeOf(this);
+ for (auto iter = mScalarKeys.Iter(); !iter.Done(); iter.Next()) {
+ ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
+ n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+typedef nsUint32HashKey ScalarIDHashKey;
+typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
+typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar> KeyedScalarStorageMapType;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The Name -> ID cache map.
+ScalarMapType gScalarNameIDMap(kScalarCount);
+// The ID -> Scalar Object map. This is a nsClassHashtable, it owns
+// the scalar instance and takes care of deallocating them when they
+// get removed from the map.
+ScalarStorageMapType gScalarStorageMap;
+// The ID -> Keyed Scalar Object map. As for plain scalars, this is
+// nsClassHashtable. See above.
+KeyedScalarStorageMapType gKeyedScalarStorageMap;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Function that may call JS code.
+
+// NOTE: the functions in this section all run without protection from
+// |gTelemetryScalarsMutex|. If they held the mutex, there would be the
+// possibility of deadlock because the JS_ calls that they make may call
+// back into the TelemetryScalar interface, hence trying to re-acquire the mutex.
+//
+// This means that these functions potentially race against threads, but
+// that seems preferable to risking deadlock.
+
+namespace {
+
+/**
+ * Checks if the error should be logged.
+ *
+ * @param aSr The error code.
+ * @return true if the error should be logged, false otherwise.
+ */
+bool
+internal_ShouldLogError(ScalarResult aSr)
+{
+ switch (aSr) {
+ case ScalarResult::StringTooLong: MOZ_FALLTHROUGH;
+ case ScalarResult::KeyTooLong: MOZ_FALLTHROUGH;
+ case ScalarResult::TooManyKeys: MOZ_FALLTHROUGH;
+ case ScalarResult::UnsignedNegativeValue: MOZ_FALLTHROUGH;
+ case ScalarResult::UnsignedTruncatedValue:
+ // Intentional fall-through.
+ return true;
+
+ default:
+ return false;
+ }
+
+ // It should never reach this point.
+ return false;
+}
+
+/**
+ * Converts the error code to a human readable error message and prints it to the
+ * browser console.
+ *
+ * @param aScalarName The name of the scalar that raised the error.
+ * @param aSr The error code.
+ */
+void
+internal_LogScalarError(const nsACString& aScalarName, ScalarResult aSr)
+{
+ nsAutoString errorMessage;
+ AppendUTF8toUTF16(aScalarName, errorMessage);
+
+ switch (aSr) {
+ case ScalarResult::StringTooLong:
+ errorMessage.Append(NS_LITERAL_STRING(" - Truncating scalar value to 50 characters."));
+ break;
+ case ScalarResult::KeyTooLong:
+ errorMessage.Append(NS_LITERAL_STRING(" - The key length must be limited to 70 characters."));
+ break;
+ case ScalarResult::TooManyKeys:
+ errorMessage.Append(NS_LITERAL_STRING(" - Keyed scalars cannot have more than 100 keys."));
+ break;
+ case ScalarResult::UnsignedNegativeValue:
+ errorMessage.Append(NS_LITERAL_STRING(" - Trying to set an unsigned scalar to a negative number."));
+ break;
+ case ScalarResult::UnsignedTruncatedValue:
+ errorMessage.Append(NS_LITERAL_STRING(" - Truncating float/double number."));
+ break;
+ default:
+ // Nothing.
+ return;
+ }
+
+ LogToBrowserConsole(nsIScriptError::warningFlag, errorMessage);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the external interface
+
+namespace {
+
+bool
+internal_CanRecordBase()
+{
+ return gCanRecordBase;
+}
+
+bool
+internal_CanRecordExtended()
+{
+ return gCanRecordExtended;
+}
+
+const ScalarInfo&
+internal_InfoForScalarID(mozilla::Telemetry::ScalarID aId)
+{
+ return gScalars[static_cast<uint32_t>(aId)];
+}
+
+/**
+ * Check if the given scalar is a keyed scalar.
+ *
+ * @param aId The scalar enum.
+ * @return true if aId refers to a keyed scalar, false otherwise.
+ */
+bool
+internal_IsKeyedScalar(mozilla::Telemetry::ScalarID aId)
+{
+ return internal_InfoForScalarID(aId).keyed;
+}
+
+bool
+internal_CanRecordForScalarID(mozilla::Telemetry::ScalarID aId)
+{
+ // Get the scalar info from the id.
+ const ScalarInfo &info = internal_InfoForScalarID(aId);
+
+ // Can we record at all?
+ bool canRecordBase = internal_CanRecordBase();
+ if (!canRecordBase) {
+ return false;
+ }
+
+ bool canRecordDataset = CanRecordDataset(info.dataset,
+ canRecordBase,
+ internal_CanRecordExtended());
+ if (!canRecordDataset) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Get the scalar enum id from the scalar name.
+ *
+ * @param aName The scalar name.
+ * @param aId The output variable to contain the enum.
+ * @return
+ * NS_ERROR_FAILURE if this was called before init is completed.
+ * NS_ERROR_INVALID_ARG if the name can't be found in the scalar definitions.
+ * NS_OK if the scalar was found and aId contains a valid enum id.
+ */
+nsresult
+internal_GetEnumByScalarName(const nsACString& aName, mozilla::Telemetry::ScalarID* aId)
+{
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CharPtrEntryType *entry = gScalarNameIDMap.GetEntry(PromiseFlatCString(aName).get());
+ if (!entry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aId = entry->mData;
+ return NS_OK;
+}
+
+/**
+ * Get a scalar object by its enum id. This implicitly allocates the scalar
+ * object in the storage if it wasn't previously allocated.
+ *
+ * @param aId The scalar id.
+ * @param aRes The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult
+internal_GetScalarByEnum(mozilla::Telemetry::ScalarID aId, ScalarBase** aRet)
+{
+ if (!IsValidEnumId(aId)) {
+ MOZ_ASSERT(false, "Requested a scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const uint32_t id = static_cast<uint32_t>(aId);
+
+ ScalarBase* scalar = nullptr;
+ if (gScalarStorageMap.Get(id, &scalar)) {
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ const ScalarInfo &info = gScalars[id];
+
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ scalar = internal_ScalarAllocate(info.kind);
+ if (!scalar) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ gScalarStorageMap.Put(id, scalar);
+
+ *aRet = scalar;
+ return NS_OK;
+}
+
+/**
+ * Get a scalar object by its enum id, if we're allowed to record it.
+ *
+ * @param aId The scalar id.
+ * @return The ScalarBase instance or nullptr if we're not allowed to record
+ * the scalar.
+ */
+ScalarBase*
+internal_GetRecordableScalar(mozilla::Telemetry::ScalarID aId)
+{
+ // Get the scalar by the enum (it also internally checks for aId validity).
+ ScalarBase* scalar = nullptr;
+ nsresult rv = internal_GetScalarByEnum(aId, &scalar);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ if (internal_IsKeyedScalar(aId)) {
+ return nullptr;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(aId)) {
+ return nullptr;
+ }
+
+ return scalar;
+}
+
+} // namespace
+
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the keyed scalars
+
+namespace {
+
+/**
+ * Get a keyed scalar object by its enum id. This implicitly allocates the keyed
+ * scalar object in the storage if it wasn't previously allocated.
+ *
+ * @param aId The scalar id.
+ * @param aRes The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed string
+ * scalar.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult
+internal_GetKeyedScalarByEnum(mozilla::Telemetry::ScalarID aId, KeyedScalar** aRet)
+{
+ if (!IsValidEnumId(aId)) {
+ MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const uint32_t id = static_cast<uint32_t>(aId);
+
+ KeyedScalar* scalar = nullptr;
+ if (gKeyedScalarStorageMap.Get(id, &scalar)) {
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ const ScalarInfo &info = gScalars[id];
+
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We don't currently support keyed string scalars. Disable them.
+ if (info.kind == nsITelemetry::SCALAR_STRING) {
+ MOZ_ASSERT(false, "Keyed string scalars are not currently supported.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalar = new KeyedScalar(info.kind);
+ if (!scalar) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ gKeyedScalarStorageMap.Put(id, scalar);
+
+ *aRet = scalar;
+ return NS_OK;
+}
+
+/**
+ * Get a keyed scalar object by its enum id, if we're allowed to record it.
+ *
+ * @param aId The scalar id.
+ * @return The KeyedScalar instance or nullptr if we're not allowed to record
+ * the scalar.
+ */
+KeyedScalar*
+internal_GetRecordableKeyedScalar(mozilla::Telemetry::ScalarID aId)
+{
+ // Get the scalar by the enum (it also internally checks for aId validity).
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(aId, &scalar);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ if (!internal_IsKeyedScalar(aId)) {
+ return nullptr;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(aId)) {
+ return nullptr;
+ }
+
+ return scalar;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryScalars::
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryScalarsMutex;
+
+void
+TelemetryScalar::InitializeGlobalState(bool aCanRecordBase, bool aCanRecordExtended)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ MOZ_ASSERT(!gInitDone, "TelemetryScalar::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static scalar name->id cache. Note that the scalar names are
+ // statically allocated and come from the automatically generated TelemetryScalarData.h.
+ uint32_t scalarCount = static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+ for (uint32_t i = 0; i < scalarCount; i++) {
+ CharPtrEntryType *entry = gScalarNameIDMap.PutEntry(gScalars[i].name());
+ entry->mData = static_cast<mozilla::Telemetry::ScalarID>(i);
+ }
+
+#ifdef DEBUG
+ gScalarNameIDMap.MarkImmutable();
+#endif
+ gInitDone = true;
+}
+
+void
+TelemetryScalar::DeInitializeGlobalState()
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gScalarNameIDMap.Clear();
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gInitDone = false;
+}
+
+void
+TelemetryScalar::SetCanRecordBase(bool b)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = b;
+}
+
+void
+TelemetryScalar::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordExtended = b;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allowed to record to this
+ * dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
+{
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv =
+ nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ mozilla::Telemetry::ScalarID id;
+ rv = internal_GetEnumByScalarName(aName, &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // We're trying to set a plain scalar, so make sure this is one.
+ if (internal_IsKeyedScalar(id)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(id)) {
+ return NS_OK;
+ }
+
+ // Finally get the scalar.
+ ScalarBase* scalar = nullptr;
+ rv = internal_GetScalarByEnum(id, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+ return rv;
+ }
+
+ sr = scalar->AddValue(unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (internal_ShouldLogError(sr)) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return MapToNsResult(sr);
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ * dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+ JSContext* aCx)
+{
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv =
+ nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ mozilla::Telemetry::ScalarID id;
+ rv = internal_GetEnumByScalarName(aName, &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Make sure this is a keyed scalar.
+ if (!internal_IsKeyedScalar(id)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(id)) {
+ return NS_OK;
+ }
+
+ // Finally get the scalar.
+ KeyedScalar* scalar = nullptr;
+ rv = internal_GetKeyedScalarByEnum(id, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+ return rv;
+ }
+
+ sr = scalar->AddValue(aKey, unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (internal_ShouldLogError(sr)) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return MapToNsResult(sr);
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void
+TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ ScalarBase* scalar = internal_GetRecordableScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->AddValue(aValue);
+}
+
+/**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void
+TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->AddValue(aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ * dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::Set(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
+{
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv =
+ nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ mozilla::Telemetry::ScalarID id;
+ rv = internal_GetEnumByScalarName(aName, &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // We're trying to set a plain scalar, so make sure this is one.
+ if (internal_IsKeyedScalar(id)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(id)) {
+ return NS_OK;
+ }
+
+ // Finally get the scalar.
+ ScalarBase* scalar = nullptr;
+ rv = internal_GetScalarByEnum(id, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+ return rv;
+ }
+
+ sr = scalar->SetValue(unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (internal_ShouldLogError(sr)) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return MapToNsResult(sr);
+}
+
+/**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ * dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+ JSContext* aCx)
+{
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv =
+ nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ mozilla::Telemetry::ScalarID id;
+ rv = internal_GetEnumByScalarName(aName, &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // We're trying to set a keyed scalar. Report an error if this isn't one.
+ if (!internal_IsKeyedScalar(id)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(id)) {
+ return NS_OK;
+ }
+
+ // Finally get the scalar.
+ KeyedScalar* scalar = nullptr;
+ rv = internal_GetKeyedScalarByEnum(id, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+ return rv;
+ }
+
+ sr = scalar->SetValue(aKey, unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (internal_ShouldLogError(sr)) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return MapToNsResult(sr);
+}
+
+/**
+ * Sets the scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ ScalarBase* scalar = internal_GetRecordableScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given string value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The string value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ ScalarBase* scalar = internal_GetRecordableScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ ScalarBase* scalar = internal_GetRecordableScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetValue(aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ bool aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetValue(aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ * dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
+{
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv =
+ nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ mozilla::Telemetry::ScalarID id;
+ rv = internal_GetEnumByScalarName(aName, &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Make sure this is not a keyed scalar.
+ if (internal_IsKeyedScalar(id)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(id)) {
+ return NS_OK;
+ }
+
+ // Finally get the scalar.
+ ScalarBase* scalar = nullptr;
+ rv = internal_GetScalarByEnum(id, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+ return rv;
+ }
+
+ sr = scalar->SetMaximum(unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (internal_ShouldLogError(sr)) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return MapToNsResult(sr);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ * dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+ JSContext* aCx)
+{
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv =
+ nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ mozilla::Telemetry::ScalarID id;
+ rv = internal_GetEnumByScalarName(aName, &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Make sure this is a keyed scalar.
+ if (!internal_IsKeyedScalar(id)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Are we allowed to record this scalar?
+ if (!internal_CanRecordForScalarID(id)) {
+ return NS_OK;
+ }
+
+ // Finally get the scalar.
+ KeyedScalar* scalar = nullptr;
+ rv = internal_GetKeyedScalarByEnum(id, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+ return rv;
+ }
+
+ sr = scalar->SetMaximum(aKey, unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (internal_ShouldLogError(sr)) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return MapToNsResult(sr);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void
+TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ ScalarBase* scalar = internal_GetRecordableScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetMaximum(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void
+TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+ if (!scalar) {
+ return;
+ }
+
+ scalar->SetMaximum(aKey, aValue);
+}
+
+/**
+ * Serializes the scalars from the given dataset to a json-style object and resets them.
+ * The returned structure looks like {"group1.probe":1,"group1.other_probe":false,...}.
+ *
+ * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+ * @param aClear Whether to clear out the scalars after snapshotting.
+ */
+nsresult
+TelemetryScalar::CreateSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult)
+{
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Only lock the mutex while accessing our data, without locking any JS related code.
+ typedef mozilla::Pair<const char*, nsCOMPtr<nsIVariant>> DataPair;
+ nsTArray<DataPair> scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // Iterate the scalars in gScalarStorageMap. The storage may contain empty or yet to be
+ // initialized scalars.
+ for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
+ ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
+
+ // Get the informations for this scalar.
+ const ScalarInfo& info = gScalars[iter.Key()];
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(scalarValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Append it to our list.
+ scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarValue));
+ }
+ }
+
+ if (aClearScalars) {
+ // The map already takes care of freeing the allocated memory.
+ gScalarStorageMap.Clear();
+ }
+ }
+
+ // Reflect it to JS.
+ for (nsTArray<DataPair>::size_type i = 0; i < scalarsToReflect.Length(); i++) {
+ const DataPair& scalar = scalarsToReflect[i];
+
+ // Convert it to a JS Val.
+ JS::Rooted<JS::Value> scalarJsValue(aCx);
+ nsresult rv =
+ nsContentUtils::XPConnect()->VariantToJS(aCx, root_obj, scalar.second(), &scalarJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add it to the scalar object.
+ if (!JS_DefineProperty(aCx, root_obj, scalar.first(), scalarJsValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Serializes the scalars from the given dataset to a json-style object and resets them.
+ * The returned structure looks like:
+ * { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... }
+ *
+ * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+ * @param aClear Whether to clear out the keyed scalars after snapshotting.
+ */
+nsresult
+TelemetryScalar::CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult)
+{
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Only lock the mutex while accessing our data, without locking any JS related code.
+ typedef mozilla::Pair<const char*, nsTArray<KeyedScalar::KeyValuePair>> DataPair;
+ nsTArray<DataPair> scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // Iterate the scalars in gKeyedScalarStorageMap. The storage may contain empty or yet
+ // to be initialized scalars.
+ for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
+ KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data());
+
+ // Get the informations for this scalar.
+ const ScalarInfo& info = gScalars[iter.Key()];
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the keys for this scalar.
+ nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
+ nsresult rv = scalar->GetValue(scalarKeyedData);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Append it to our list.
+ scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarKeyedData));
+ }
+ }
+
+ if (aClearScalars) {
+ // The map already takes care of freeing the allocated memory.
+ gKeyedScalarStorageMap.Clear();
+ }
+ }
+
+ // Reflect it to JS.
+ for (nsTArray<DataPair>::size_type i = 0; i < scalarsToReflect.Length(); i++) {
+ const DataPair& keyedScalarData = scalarsToReflect[i];
+
+ // Go through each keyed scalar and create a keyed scalar object.
+ // This object will hold the values for all the keyed scalar keys.
+ JS::RootedObject keyedScalarObj(aCx, JS_NewPlainObject(aCx));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps = keyedScalarData.second();
+ for (uint32_t i = 0; i < keyProps.Length(); i++) {
+ const KeyedScalar::KeyValuePair& keyData = keyProps[i];
+
+ // Convert the value for the key to a JSValue.
+ JS::Rooted<JS::Value> keyJsValue(aCx);
+ nsresult rv =
+ nsContentUtils::XPConnect()->VariantToJS(aCx, keyedScalarObj, keyData.second(), &keyJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add the key to the scalar representation.
+ const NS_ConvertUTF8toUTF16 key(keyData.first());
+ if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(), keyJsValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the scalar to the root object.
+ if (!JS_DefineProperty(aCx, root_obj, keyedScalarData.first(), keyedScalarObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+void
+TelemetryScalar::ClearScalars()
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+}
+
+size_t
+TelemetryScalar::GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ return gScalarNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t
+TelemetryScalar::GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ size_t n = 0;
+ // For the plain scalars...
+ for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
+ ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
+ n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ // ...and for the keyed scalars.
+ for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
+ KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data());
+ n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
diff --git a/toolkit/components/telemetry/TelemetryScalar.h b/toolkit/components/telemetry/TelemetryScalar.h
new file mode 100644
index 0000000000..b20a8dace2
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryScalar.h
@@ -0,0 +1,64 @@
+/* -*- 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 TelemetryScalar_h__
+#define TelemetryScalar_h__
+
+#include "mozilla/TelemetryScalarEnums.h"
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// scalar accumulation and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryScalar {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+void SetCanRecordBase(bool b);
+void SetCanRecordExtended(bool b);
+
+// JS API Endpoints.
+nsresult Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx);
+nsresult Set(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx);
+nsresult CreateSnapshots(unsigned int aDataset, bool aClearScalars,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult);
+
+// Keyed JS API Endpoints.
+nsresult Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+ JSContext* aCx);
+nsresult Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+ JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+ JSContext* aCx);
+nsresult CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult);
+
+// C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+void Set(mozilla::Telemetry::ScalarID aId, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+// Keyed C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+// Only to be used for testing.
+void ClearScalars();
+
+size_t GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+size_t GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+} // namespace TelemetryScalar
+
+#endif // TelemetryScalar_h__ \ No newline at end of file
diff --git a/toolkit/components/telemetry/TelemetrySend.jsm b/toolkit/components/telemetry/TelemetrySend.jsm
new file mode 100644
index 0000000000..4694ac6a96
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetrySend.jsm
@@ -0,0 +1,1114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 module is responsible for uploading pings to the server and persisting
+ * pings that can't be send now.
+ * Those pending pings are persisted on disk and sent at the next opportunity,
+ * newest first.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetrySend",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Log.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/ServiceRequest.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage",
+ "resource://gre/modules/TelemetryStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy",
+ "resource://gre/modules/TelemetryReportingPolicy.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
+ "@mozilla.org/base/telemetry;1",
+ "nsITelemetry");
+
+const Utils = TelemetryUtils;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetrySend::";
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_SERVER = PREF_BRANCH + "server";
+const PREF_UNIFIED = PREF_BRANCH + "unified";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+const TOPIC_IDLE_DAILY = "idle-daily";
+const TOPIC_QUIT_APPLICATION = "quit-application";
+
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
+
+const PING_FORMAT_VERSION = 4;
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+const PING_TYPE_DELETION = "deletion";
+
+// We try to spread "midnight" pings out over this interval.
+const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * MS_IN_A_MINUTE;
+// We delay sending "midnight" pings on this client by this interval.
+const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS;
+
+// Timeout after which we consider a ping submission failed.
+const PING_SUBMIT_TIMEOUT_MS = 1.5 * MS_IN_A_MINUTE;
+
+// To keep resource usage in check, we limit ping sending to a maximum number
+// of pings per minute.
+const MAX_PING_SENDS_PER_MINUTE = 10;
+
+// If we have more pending pings then we can send right now, we schedule the next
+// send for after SEND_TICK_DELAY.
+const SEND_TICK_DELAY = 1 * MS_IN_A_MINUTE;
+// If we had any ping send failures since the last ping, we use a backoff timeout
+// for the next ping sends. We increase the delay exponentially up to a limit of
+// SEND_MAXIMUM_BACKOFF_DELAY_MS.
+// This exponential backoff will be reset by external ping submissions & idle-daily.
+const SEND_MAXIMUM_BACKOFF_DELAY_MS = 120 * MS_IN_A_MINUTE;
+
+// The age of a pending ping to be considered overdue (in milliseconds).
+const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * MS_IN_A_MINUTE; // 1 week
+
+function monotonicNow() {
+ try {
+ return Telemetry.msSinceProcessStart();
+ } catch (ex) {
+ // If this fails fall back to the (non-monotonic) Date value.
+ return Date.now();
+ }
+}
+
+/**
+ * This is a policy object used to override behavior within this module.
+ * Tests override properties on this object to allow for control of behavior
+ * that would otherwise be very hard to cover.
+ */
+var Policy = {
+ now: () => new Date(),
+ midnightPingFuzzingDelay: () => MIDNIGHT_FUZZING_DELAY_MS,
+ setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+ clearSchedulerTickTimeout: (id) => clearTimeout(id),
+};
+
+/**
+ * Determine if the ping has the new v4 ping format or the legacy v2 one or earlier.
+ */
+function isV4PingFormat(aPing) {
+ return ("id" in aPing) && ("application" in aPing) &&
+ ("version" in aPing) && (aPing.version >= 2);
+}
+
+/**
+ * Check if the provided ping is a deletion ping.
+ * @param {Object} aPing The ping to check.
+ * @return {Boolean} True if the ping is a deletion ping, false otherwise.
+ */
+function isDeletionPing(aPing) {
+ return isV4PingFormat(aPing) && (aPing.type == PING_TYPE_DELETION);
+}
+
+/**
+ * Save the provided ping as a pending ping. If it's a deletion ping, save it
+ * to a special location.
+ * @param {Object} aPing The ping to save.
+ * @return {Promise} A promise resolved when the ping is saved.
+ */
+function savePing(aPing) {
+ if (isDeletionPing(aPing)) {
+ return TelemetryStorage.saveDeletionPing(aPing);
+ }
+ return TelemetryStorage.savePendingPing(aPing);
+}
+
+/**
+ * @return {String} This returns a string with the gzip compressed data.
+ */
+function gzipCompressString(string) {
+ let observer = {
+ buffer: "",
+ onStreamComplete: function(loader, context, status, length, result) {
+ this.buffer = String.fromCharCode.apply(this, result);
+ }
+ };
+
+ let scs = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+ let listener = Cc["@mozilla.org/network/stream-loader;1"]
+ .createInstance(Ci.nsIStreamLoader);
+ listener.init(observer);
+ let converter = scs.asyncConvertData("uncompressed", "gzip",
+ listener, null);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stringStream.data = string;
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, null, stringStream, 0, string.length);
+ converter.onStopRequest(null, null, null);
+ return observer.buffer;
+}
+
+this.TelemetrySend = {
+
+ /**
+ * Age in ms of a pending ping to be considered overdue.
+ */
+ get OVERDUE_PING_FILE_AGE() {
+ return OVERDUE_PING_FILE_AGE;
+ },
+
+ get pendingPingCount() {
+ return TelemetrySendImpl.pendingPingCount;
+ },
+
+ /**
+ * Initializes this module.
+ *
+ * @param {Boolean} testing Whether this is run in a test. This changes some behavior
+ * to enable proper testing.
+ * @return {Promise} Resolved when setup is finished.
+ */
+ setup: function(testing = false) {
+ return TelemetrySendImpl.setup(testing);
+ },
+
+ /**
+ * Shutdown this module - this will cancel any pending ping tasks and wait for
+ * outstanding async activity like network and disk I/O.
+ *
+ * @return {Promise} Promise that is resolved when shutdown is finished.
+ */
+ shutdown: function() {
+ return TelemetrySendImpl.shutdown();
+ },
+
+ /**
+ * Submit a ping for sending. This will:
+ * - send the ping right away if possible or
+ * - save the ping to disk and send it at the next opportunity
+ *
+ * @param {Object} ping The ping data to send, must be serializable to JSON.
+ * @return {Promise} Test-only - a promise that is resolved when the ping is sent or saved.
+ */
+ submitPing: function(ping) {
+ return TelemetrySendImpl.submitPing(ping);
+ },
+
+ /**
+ * Count of pending pings that were found to be overdue at startup.
+ */
+ get overduePingsCount() {
+ return TelemetrySendImpl.overduePingsCount;
+ },
+
+ /**
+ * Notify that we can start submitting data to the servers.
+ */
+ notifyCanUpload: function() {
+ return TelemetrySendImpl.notifyCanUpload();
+ },
+
+ /**
+ * Only used in tests. Used to reset the module data to emulate a restart.
+ */
+ reset: function() {
+ return TelemetrySendImpl.reset();
+ },
+
+ /**
+ * Only used in tests.
+ */
+ setServer: function(server) {
+ return TelemetrySendImpl.setServer(server);
+ },
+
+ /**
+ * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
+ */
+ clearCurrentPings: function() {
+ return TelemetrySendImpl.clearCurrentPings();
+ },
+
+ /**
+ * Only used in tests to wait on outgoing pending pings.
+ */
+ testWaitOnOutgoingPings: function() {
+ return TelemetrySendImpl.promisePendingPingActivity();
+ },
+
+ /**
+ * Test-only - this allows overriding behavior to enable ping sending in debug builds.
+ */
+ setTestModeEnabled: function(testing) {
+ TelemetrySendImpl.setTestModeEnabled(testing);
+ },
+
+ /**
+ * This returns state info for this module for AsyncShutdown timeout diagnostics.
+ */
+ getShutdownState: function() {
+ return TelemetrySendImpl.getShutdownState();
+ },
+};
+
+var CancellableTimeout = {
+ _deferred: null,
+ _timer: null,
+
+ /**
+ * This waits until either the given timeout passed or the timeout was cancelled.
+ *
+ * @param {Number} timeoutMs The timeout in ms.
+ * @return {Promise<bool>} Promise that is resolved with false if the timeout was cancelled,
+ * false otherwise.
+ */
+ promiseWaitOnTimeout: function(timeoutMs) {
+ if (!this._deferred) {
+ this._deferred = PromiseUtils.defer();
+ this._timer = Policy.setSchedulerTickTimeout(() => this._onTimeout(), timeoutMs);
+ }
+
+ return this._deferred.promise;
+ },
+
+ _onTimeout: function() {
+ if (this._deferred) {
+ this._deferred.resolve(false);
+ this._timer = null;
+ this._deferred = null;
+ }
+ },
+
+ cancelTimeout: function() {
+ if (this._deferred) {
+ Policy.clearSchedulerTickTimeout(this._timer);
+ this._deferred.resolve(true);
+ this._timer = null;
+ this._deferred = null;
+ }
+ },
+};
+
+/**
+ * SendScheduler implements the timer & scheduling behavior for ping sends.
+ */
+var SendScheduler = {
+ // Whether any ping sends failed since the last tick. If yes, we start with our exponential
+ // backoff timeout.
+ _sendsFailed: false,
+ // The current retry delay after ping send failures. We use this for the exponential backoff,
+ // increasing this value everytime we had send failures since the last tick.
+ _backoffDelay: SEND_TICK_DELAY,
+ _shutdown: false,
+ _sendTask: null,
+ // A string that tracks the last seen send task state, null if it never ran.
+ _sendTaskState: null,
+
+ _logger: null,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX + "Scheduler::");
+ }
+
+ return this._logger;
+ },
+
+ shutdown: function() {
+ this._log.trace("shutdown");
+ this._shutdown = true;
+ CancellableTimeout.cancelTimeout();
+ return Promise.resolve(this._sendTask);
+ },
+
+ start: function() {
+ this._log.trace("start");
+ this._sendsFailed = false;
+ this._backoffDelay = SEND_TICK_DELAY;
+ this._shutdown = false;
+ },
+
+ /**
+ * Only used for testing, resets the state to emulate a restart.
+ */
+ reset: function() {
+ this._log.trace("reset");
+ return this.shutdown().then(() => this.start());
+ },
+
+ /**
+ * Notify the scheduler of a failure in sending out pings that warrants retrying.
+ * This will trigger the exponential backoff timer behavior on the next tick.
+ */
+ notifySendsFailed: function() {
+ this._log.trace("notifySendsFailed");
+ if (this._sendsFailed) {
+ return;
+ }
+
+ this._sendsFailed = true;
+ this._log.trace("notifySendsFailed - had send failures");
+ },
+
+ /**
+ * Returns whether ping submissions are currently throttled.
+ */
+ isThrottled: function() {
+ const now = Policy.now();
+ const nextPingSendTime = this._getNextPingSendTime(now);
+ return (nextPingSendTime > now.getTime());
+ },
+
+ waitOnSendTask: function() {
+ return Promise.resolve(this._sendTask);
+ },
+
+ triggerSendingPings: function(immediately) {
+ this._log.trace("triggerSendingPings - active send task: " + !!this._sendTask + ", immediately: " + immediately);
+
+ if (!this._sendTask) {
+ this._sendTask = this._doSendTask();
+ let clear = () => this._sendTask = null;
+ this._sendTask.then(clear, clear);
+ } else if (immediately) {
+ CancellableTimeout.cancelTimeout();
+ }
+
+ return this._sendTask;
+ },
+
+ _doSendTask: Task.async(function*() {
+ this._sendTaskState = "send task started";
+ this._backoffDelay = SEND_TICK_DELAY;
+ this._sendsFailed = false;
+
+ const resetBackoffTimer = () => {
+ this._backoffDelay = SEND_TICK_DELAY;
+ };
+
+ for (;;) {
+ this._log.trace("_doSendTask iteration");
+ this._sendTaskState = "start iteration";
+
+ if (this._shutdown) {
+ this._log.trace("_doSendTask - shutting down, bailing out");
+ this._sendTaskState = "bail out - shutdown check";
+ return;
+ }
+
+ // Get a list of pending pings, sorted by last modified, descending.
+ // Filter out all the pings we can't send now. This addresses scenarios like "deletion" pings
+ // which can be send even when upload is disabled.
+ let pending = TelemetryStorage.getPendingPingList();
+ let current = TelemetrySendImpl.getUnpersistedPings();
+ this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length);
+ // Note that the two lists contain different kind of data. |pending| only holds ping
+ // info, while |current| holds actual ping data.
+ if (!TelemetrySendImpl.sendingEnabled()) {
+ pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id));
+ current = current.filter(p => isDeletionPing(p));
+ }
+ this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length);
+
+ // Bail out if there is nothing to send.
+ if ((pending.length == 0) && (current.length == 0)) {
+ this._log.trace("_doSendTask - no pending pings, bailing out");
+ this._sendTaskState = "bail out - no pings to send";
+ return;
+ }
+
+ // If we are currently throttled (e.g. fuzzing to avoid midnight spikes), wait for the next send window.
+ const now = Policy.now();
+ if (this.isThrottled()) {
+ const nextPingSendTime = this._getNextPingSendTime(now);
+ this._log.trace("_doSendTask - throttled, delaying ping send to " + new Date(nextPingSendTime));
+ this._sendTaskState = "wait for throttling to pass";
+
+ const delay = nextPingSendTime - now.getTime();
+ const cancelled = yield CancellableTimeout.promiseWaitOnTimeout(delay);
+ if (cancelled) {
+ this._log.trace("_doSendTask - throttling wait was cancelled, resetting backoff timer");
+ resetBackoffTimer();
+ }
+
+ continue;
+ }
+
+ let sending = pending.slice(0, MAX_PING_SENDS_PER_MINUTE);
+ pending = pending.slice(MAX_PING_SENDS_PER_MINUTE);
+ this._log.trace("_doSendTask - triggering sending of " + sending.length + " pings now" +
+ ", " + pending.length + " pings waiting");
+
+ this._sendsFailed = false;
+ const sendStartTime = Policy.now();
+ this._sendTaskState = "wait on ping sends";
+ yield TelemetrySendImpl.sendPings(current, sending.map(p => p.id));
+ if (this._shutdown || (TelemetrySend.pendingPingCount == 0)) {
+ this._log.trace("_doSendTask - bailing out after sending, shutdown: " + this._shutdown +
+ ", pendingPingCount: " + TelemetrySend.pendingPingCount);
+ this._sendTaskState = "bail out - shutdown & pending check after send";
+ return;
+ }
+
+ // Calculate the delay before sending the next batch of pings.
+ // We start with a delay that makes us send max. 1 batch per minute.
+ // If we had send failures in the last batch, we will override this with
+ // a backoff delay.
+ const timeSinceLastSend = Policy.now() - sendStartTime;
+ let nextSendDelay = Math.max(0, SEND_TICK_DELAY - timeSinceLastSend);
+
+ if (!this._sendsFailed) {
+ this._log.trace("_doSendTask - had no send failures, resetting backoff timer");
+ resetBackoffTimer();
+ } else {
+ const newDelay = Math.min(SEND_MAXIMUM_BACKOFF_DELAY_MS,
+ this._backoffDelay * 2);
+ this._log.trace("_doSendTask - had send failures, backing off -" +
+ " old timeout: " + this._backoffDelay +
+ ", new timeout: " + newDelay);
+ this._backoffDelay = newDelay;
+ nextSendDelay = this._backoffDelay;
+ }
+
+ this._log.trace("_doSendTask - waiting for next send opportunity, timeout is " + nextSendDelay)
+ this._sendTaskState = "wait on next send opportunity";
+ const cancelled = yield CancellableTimeout.promiseWaitOnTimeout(nextSendDelay);
+ if (cancelled) {
+ this._log.trace("_doSendTask - batch send wait was cancelled, resetting backoff timer");
+ resetBackoffTimer();
+ }
+ }
+ }),
+
+ /**
+ * This helper calculates the next time that we can send pings at.
+ * Currently this mostly redistributes ping sends from midnight until one hour after
+ * to avoid submission spikes around local midnight for daily pings.
+ *
+ * @param now Date The current time.
+ * @return Number The next time (ms from UNIX epoch) when we can send pings.
+ */
+ _getNextPingSendTime: function(now) {
+ // 1. First we check if the time is between 0am and 1am. If it's not, we send
+ // immediately.
+ // 2. If we confirmed the time is indeed between 0am and 1am in step 1, we disallow
+ // sending before (midnight + fuzzing delay), which is a random time between 0am-1am
+ // (decided at startup).
+
+ const midnight = Utils.truncateToDays(now);
+ // Don't delay pings if we are not within the fuzzing interval.
+ if ((now.getTime() - midnight.getTime()) > MIDNIGHT_FUZZING_INTERVAL_MS) {
+ return now.getTime();
+ }
+
+ // Delay ping send if we are within the midnight fuzzing range.
+ // We spread those ping sends out between |midnight| and |midnight + midnightPingFuzzingDelay|.
+ return midnight.getTime() + Policy.midnightPingFuzzingDelay();
+ },
+
+ getShutdownState: function() {
+ return {
+ shutdown: this._shutdown,
+ hasSendTask: !!this._sendTask,
+ sendsFailed: this._sendsFailed,
+ sendTaskState: this._sendTaskState,
+ backoffDelay: this._backoffDelay,
+ };
+ },
+ };
+
+var TelemetrySendImpl = {
+ _sendingEnabled: false,
+ // Tracks the shutdown state.
+ _shutdown: false,
+ _logger: null,
+ // This tracks all pending ping requests to the server.
+ _pendingPingRequests: new Map(),
+ // This tracks all the pending async ping activity.
+ _pendingPingActivity: new Set(),
+ // This is true when running in the test infrastructure.
+ _testMode: false,
+ // This holds pings that we currently try and haven't persisted yet.
+ _currentPings: new Map(),
+
+ // Count of pending pings that were overdue.
+ _overduePingCount: 0,
+
+ OBSERVER_TOPICS: [
+ TOPIC_IDLE_DAILY,
+ ],
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+
+ return this._logger;
+ },
+
+ get overduePingsCount() {
+ return this._overduePingCount;
+ },
+
+ get pendingPingRequests() {
+ return this._pendingPingRequests;
+ },
+
+ get pendingPingCount() {
+ return TelemetryStorage.getPendingPingList().length + this._currentPings.size;
+ },
+
+ setTestModeEnabled: function(testing) {
+ this._testMode = testing;
+ },
+
+ setup: Task.async(function*(testing) {
+ this._log.trace("setup");
+
+ this._testMode = testing;
+ this._sendingEnabled = true;
+
+ Services.obs.addObserver(this, TOPIC_IDLE_DAILY, false);
+
+ this._server = Preferences.get(PREF_SERVER, undefined);
+
+ // Check the pending pings on disk now.
+ try {
+ yield this._checkPendingPings();
+ } catch (ex) {
+ this._log.error("setup - _checkPendingPings rejected", ex);
+ }
+
+ // Enforce the pending pings storage quota. It could take a while so don't
+ // block on it.
+ TelemetryStorage.runEnforcePendingPingsQuotaTask();
+
+ // Start sending pings, but don't block on this.
+ SendScheduler.triggerSendingPings(true);
+ }),
+
+ /**
+ * Discard old pings from the pending pings and detect overdue ones.
+ * @return {Boolean} True if we have overdue pings, false otherwise.
+ */
+ _checkPendingPings: Task.async(function*() {
+ // Scan the pending pings - that gives us a list sorted by last modified, descending.
+ let infos = yield TelemetryStorage.loadPendingPingList();
+ this._log.info("_checkPendingPings - pending ping count: " + infos.length);
+ if (infos.length == 0) {
+ this._log.trace("_checkPendingPings - no pending pings");
+ return;
+ }
+
+ const now = Policy.now();
+
+ // Check for overdue pings.
+ const overduePings = infos.filter((info) =>
+ (now.getTime() - info.lastModificationDate) > OVERDUE_PING_FILE_AGE);
+ this._overduePingCount = overduePings.length;
+
+ // Submit the age of the pending pings.
+ for (let pingInfo of infos) {
+ const ageInDays =
+ Utils.millisecondsToDays(Math.abs(now.getTime() - pingInfo.lastModificationDate));
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_AGE").add(ageInDays);
+ }
+ }),
+
+ shutdown: Task.async(function*() {
+ this._shutdown = true;
+
+ for (let topic of this.OBSERVER_TOPICS) {
+ try {
+ Services.obs.removeObserver(this, topic);
+ } catch (ex) {
+ this._log.error("shutdown - failed to remove observer for " + topic, ex);
+ }
+ }
+
+ // We can't send anymore now.
+ this._sendingEnabled = false;
+
+ // Cancel any outgoing requests.
+ yield this._cancelOutgoingRequests();
+
+ // Stop any active send tasks.
+ yield SendScheduler.shutdown();
+
+ // Wait for any outstanding async ping activity.
+ yield this.promisePendingPingActivity();
+
+ // Save any outstanding pending pings to disk.
+ yield this._persistCurrentPings();
+ }),
+
+ reset: function() {
+ this._log.trace("reset");
+
+ this._shutdown = false;
+ this._currentPings = new Map();
+ this._overduePingCount = 0;
+
+ const histograms = [
+ "TELEMETRY_SUCCESS",
+ "TELEMETRY_SEND_SUCCESS",
+ "TELEMETRY_SEND_FAILURE",
+ ];
+
+ histograms.forEach(h => Telemetry.getHistogramById(h).clear());
+
+ return SendScheduler.reset();
+ },
+
+ /**
+ * Notify that we can start submitting data to the servers.
+ */
+ notifyCanUpload: function() {
+ // Let the scheduler trigger sending pings if possible.
+ SendScheduler.triggerSendingPings(true);
+ return this.promisePendingPingActivity();
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case TOPIC_IDLE_DAILY:
+ SendScheduler.triggerSendingPings(true);
+ break;
+ }
+ },
+
+ submitPing: function(ping) {
+ this._log.trace("submitPing - ping id: " + ping.id);
+
+ if (!this.sendingEnabled(ping)) {
+ this._log.trace("submitPing - Telemetry is not allowed to send pings.");
+ return Promise.resolve();
+ }
+
+ if (!this.canSendNow) {
+ // Sending is disabled or throttled, add this to the persisted pending pings.
+ this._log.trace("submitPing - can't send ping now, persisting to disk - " +
+ "canSendNow: " + this.canSendNow);
+ return savePing(ping);
+ }
+
+ // Let the scheduler trigger sending pings if possible.
+ // As a safety mechanism, this resets any currently active throttling.
+ this._log.trace("submitPing - can send pings, trying to send now");
+ this._currentPings.set(ping.id, ping);
+ SendScheduler.triggerSendingPings(true);
+ return Promise.resolve();
+ },
+
+ /**
+ * Only used in tests.
+ */
+ setServer: function (server) {
+ this._log.trace("setServer", server);
+ this._server = server;
+ },
+
+ /**
+ * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
+ */
+ clearCurrentPings: Task.async(function*() {
+ if (this._shutdown) {
+ this._log.trace("clearCurrentPings - in shutdown, bailing out");
+ return;
+ }
+
+ // Temporarily disable the scheduler. It must not try to reschedule ping sending
+ // while we're deleting them.
+ yield SendScheduler.shutdown();
+
+ // Now that the ping activity has settled, abort outstanding ping requests.
+ this._cancelOutgoingRequests();
+
+ // Also, purge current pings.
+ this._currentPings.clear();
+
+ // We might have been interrupted and shutdown could have been started.
+ // We need to bail out in that case to avoid triggering send activity etc.
+ // at unexpected times.
+ if (this._shutdown) {
+ this._log.trace("clearCurrentPings - in shutdown, not spinning SendScheduler up again");
+ return;
+ }
+
+ // Enable the scheduler again and spin the send task.
+ SendScheduler.start();
+ SendScheduler.triggerSendingPings(true);
+ }),
+
+ _cancelOutgoingRequests: function() {
+ // Abort any pending ping XHRs.
+ for (let [id, request] of this._pendingPingRequests) {
+ this._log.trace("_cancelOutgoingRequests - aborting ping request for id " + id);
+ try {
+ request.abort();
+ } catch (e) {
+ this._log.error("_cancelOutgoingRequests - failed to abort request for id " + id, e);
+ }
+ }
+ this._pendingPingRequests.clear();
+ },
+
+ sendPings: function(currentPings, persistedPingIds) {
+ let pingSends = [];
+
+ for (let current of currentPings) {
+ let ping = current;
+ let p = Task.spawn(function*() {
+ try {
+ yield this._doPing(ping, ping.id, false);
+ } catch (ex) {
+ this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex);
+ // Deletion pings must be saved to a special location.
+ yield savePing(ping);
+ } finally {
+ this._currentPings.delete(ping.id);
+ }
+ }.bind(this));
+
+ this._trackPendingPingTask(p);
+ pingSends.push(p);
+ }
+
+ if (persistedPingIds.length > 0) {
+ pingSends.push(this._sendPersistedPings(persistedPingIds).catch((ex) => {
+ this._log.info("sendPings - persisted pings not sent", ex);
+ }));
+ }
+
+ return Promise.all(pingSends);
+ },
+
+ /**
+ * Send the persisted pings to the server.
+ *
+ * @param {Array<string>} List of ping ids that should be sent.
+ *
+ * @return Promise A promise that is resolved when all pings finished sending or failed.
+ */
+ _sendPersistedPings: Task.async(function*(pingIds) {
+ this._log.trace("sendPersistedPings");
+
+ if (TelemetryStorage.pendingPingCount < 1) {
+ this._log.trace("_sendPersistedPings - no pings to send");
+ return;
+ }
+
+ if (pingIds.length < 1) {
+ this._log.trace("sendPersistedPings - no pings to send");
+ return;
+ }
+
+ // We can send now.
+ // If there are any send failures, _doPing() sets up handlers that e.g. trigger backoff timer behavior.
+ this._log.trace("sendPersistedPings - sending " + pingIds.length + " pings");
+ let pingSendPromises = [];
+ for (let pingId of pingIds) {
+ const id = pingId;
+ pingSendPromises.push(
+ TelemetryStorage.loadPendingPing(id)
+ .then((data) => this._doPing(data, id, true))
+ .catch(e => this._log.error("sendPersistedPings - failed to send ping " + id, e)));
+ }
+
+ let promise = Promise.all(pingSendPromises);
+ this._trackPendingPingTask(promise);
+ yield promise;
+ }),
+
+ _onPingRequestFinished: function(success, startTime, id, isPersisted) {
+ this._log.trace("_onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
+
+ let sendId = success ? "TELEMETRY_SEND_SUCCESS" : "TELEMETRY_SEND_FAILURE";
+ let hsend = Telemetry.getHistogramById(sendId);
+ let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+
+ hsend.add(monotonicNow() - startTime);
+ hsuccess.add(success);
+
+ if (!success) {
+ // Let the scheduler know about send failures for triggering backoff timeouts.
+ SendScheduler.notifySendsFailed();
+ }
+
+ if (success && isPersisted) {
+ if (TelemetryStorage.isDeletionPing(id)) {
+ return TelemetryStorage.removeDeletionPing();
+ }
+ return TelemetryStorage.removePendingPing(id);
+ }
+ return Promise.resolve();
+ },
+
+ _getSubmissionPath: function(ping) {
+ // The new ping format contains an "application" section, the old one doesn't.
+ let pathComponents;
+ if (isV4PingFormat(ping)) {
+ // We insert the Ping id in the URL to simplify server handling of duplicated
+ // pings.
+ let app = ping.application;
+ pathComponents = [
+ ping.id, ping.type, app.name, app.version, app.channel, app.buildId
+ ];
+ } else {
+ // This is a ping in the old format.
+ if (!("slug" in ping)) {
+ // That's odd, we don't have a slug. Generate one so that TelemetryStorage.jsm works.
+ ping.slug = Utils.generateUUID();
+ }
+
+ // Do we have enough info to build a submission URL?
+ let payload = ("payload" in ping) ? ping.payload : null;
+ if (payload && ("info" in payload)) {
+ let info = ping.payload.info;
+ pathComponents = [ ping.slug, info.reason, info.appName, info.appVersion,
+ info.appUpdateChannel, info.appBuildID ];
+ } else {
+ // Only use the UUID as the slug.
+ pathComponents = [ ping.slug ];
+ }
+ }
+
+ let slug = pathComponents.join("/");
+ return "/submit/telemetry/" + slug;
+ },
+
+ _doPing: function(ping, id, isPersisted) {
+ if (!this.sendingEnabled(ping)) {
+ // We can't send the pings to the server, so don't try to.
+ this._log.trace("_doPing - Can't send ping " + ping.id);
+ return Promise.resolve();
+ }
+
+ this._log.trace("_doPing - server: " + this._server + ", persisted: " + isPersisted +
+ ", id: " + id);
+
+ const isNewPing = isV4PingFormat(ping);
+ const version = isNewPing ? PING_FORMAT_VERSION : 1;
+ const url = this._server + this._getSubmissionPath(ping) + "?v=" + version;
+
+ let request = new ServiceRequest();
+ request.mozBackgroundRequest = true;
+ request.timeout = PING_SUBMIT_TIMEOUT_MS;
+
+ request.open("POST", url, true);
+ request.overrideMimeType("text/plain");
+ request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
+ request.setRequestHeader("Date", Policy.now().toUTCString());
+
+ this._pendingPingRequests.set(id, request);
+
+ // Prevent the request channel from running though URLClassifier (bug 1296802)
+ request.channel.loadFlags &= ~Ci.nsIChannel.LOAD_CLASSIFY_URI;
+
+ const monotonicStartTime = monotonicNow();
+ let deferred = PromiseUtils.defer();
+
+ let onRequestFinished = (success, event) => {
+ let onCompletion = () => {
+ if (success) {
+ deferred.resolve();
+ } else {
+ deferred.reject(event);
+ }
+ };
+
+ this._pendingPingRequests.delete(id);
+ this._onPingRequestFinished(success, monotonicStartTime, id, isPersisted)
+ .then(() => onCompletion(),
+ (error) => {
+ this._log.error("_doPing - request success: " + success + ", error: " + error);
+ onCompletion();
+ });
+ };
+
+ let errorhandler = (event) => {
+ this._log.error("_doPing - error making request to " + url + ": " + event.type);
+ onRequestFinished(false, event);
+ };
+ request.onerror = errorhandler;
+ request.ontimeout = errorhandler;
+ request.onabort = errorhandler;
+
+ request.onload = (event) => {
+ let status = request.status;
+ let statusClass = status - (status % 100);
+ let success = false;
+
+ if (statusClass === 200) {
+ // We can treat all 2XX as success.
+ this._log.info("_doPing - successfully loaded, status: " + status);
+ success = true;
+ } else if (statusClass === 400) {
+ // 4XX means that something with the request was broken.
+ this._log.error("_doPing - error submitting to " + url + ", status: " + status
+ + " - ping request broken?");
+ Telemetry.getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS").add();
+ // TODO: we should handle this better, but for now we should avoid resubmitting
+ // broken requests by pretending success.
+ success = true;
+ } else if (statusClass === 500) {
+ // 5XX means there was a server-side error and we should try again later.
+ this._log.error("_doPing - error submitting to " + url + ", status: " + status
+ + " - server error, should retry later");
+ } else {
+ // We received an unexpected status code.
+ this._log.error("_doPing - error submitting to " + url + ", status: " + status
+ + ", type: " + event.type);
+ }
+
+ onRequestFinished(success, event);
+ };
+
+ // If that's a legacy ping format, just send its payload.
+ let networkPayload = isNewPing ? ping : ping.payload;
+ request.setRequestHeader("Content-Encoding", "gzip");
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let startTime = new Date();
+ let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(networkPayload));
+ utf8Payload += converter.Finish();
+ Telemetry.getHistogramById("TELEMETRY_STRINGIFY").add(new Date() - startTime);
+
+ // Check the size and drop pings which are too big.
+ const pingSizeBytes = utf8Payload.length;
+ if (pingSizeBytes > TelemetryStorage.MAXIMUM_PING_SIZE) {
+ this._log.error("_doPing - submitted ping exceeds the size limit, size: " + pingSizeBytes);
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND").add();
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB")
+ .add(Math.floor(pingSizeBytes / 1024 / 1024));
+ // We don't need to call |request.abort()| as it was not sent yet.
+ this._pendingPingRequests.delete(id);
+ return TelemetryStorage.removePendingPing(id);
+ }
+
+ let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ startTime = new Date();
+ payloadStream.data = gzipCompressString(utf8Payload);
+ Telemetry.getHistogramById("TELEMETRY_COMPRESS").add(new Date() - startTime);
+ startTime = new Date();
+ request.send(payloadStream);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Check if sending is temporarily disabled.
+ * @return {Boolean} True if we can send pings to the server right now, false if
+ * sending is temporarily disabled.
+ */
+ get canSendNow() {
+ // If the reporting policy was not accepted yet, don't send pings.
+ if (!TelemetryReportingPolicy.canUpload()) {
+ return false;
+ }
+
+ return this._sendingEnabled;
+ },
+
+ /**
+ * Check if sending is disabled. If FHR is not allowed to upload,
+ * pings are not sent to the server (Telemetry is a sub-feature of FHR). If trying
+ * to send a deletion ping, don't block it.
+ * If unified telemetry is off, don't send pings if Telemetry is disabled.
+ *
+ * @param {Object} [ping=null] A ping to be checked.
+ * @return {Boolean} True if pings can be send to the servers, false otherwise.
+ */
+ sendingEnabled: function(ping = null) {
+ // We only send pings from official builds, but allow overriding this for tests.
+ if (!Telemetry.isOfficialTelemetry && !this._testMode) {
+ return false;
+ }
+
+ // With unified Telemetry, the FHR upload setting controls whether we can send pings.
+ // The Telemetry pref enables sending extended data sets instead.
+ if (IS_UNIFIED_TELEMETRY) {
+ // Deletion pings are sent even if the upload is disabled.
+ if (ping && isDeletionPing(ping)) {
+ return true;
+ }
+ return Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
+ }
+
+ // Without unified Telemetry, the Telemetry enabled pref controls ping sending.
+ return Utils.isTelemetryEnabled;
+ },
+
+ /**
+ * Track any pending ping send and save tasks through the promise passed here.
+ * This is needed to block shutdown on any outstanding ping activity.
+ */
+ _trackPendingPingTask: function (promise) {
+ let clear = () => this._pendingPingActivity.delete(promise);
+ promise.then(clear, clear);
+ this._pendingPingActivity.add(promise);
+ },
+
+ /**
+ * Return a promise that allows to wait on pending pings.
+ * @return {Object<Promise>} A promise resolved when all the pending pings promises
+ * are resolved.
+ */
+ promisePendingPingActivity: function () {
+ this._log.trace("promisePendingPingActivity - Waiting for ping task");
+ let p = Array.from(this._pendingPingActivity, p => p.catch(ex => {
+ this._log.error("promisePendingPingActivity - ping activity had an error", ex);
+ }));
+ p.push(SendScheduler.waitOnSendTask());
+ return Promise.all(p);
+ },
+
+ _persistCurrentPings: Task.async(function*() {
+ for (let [id, ping] of this._currentPings) {
+ try {
+ yield savePing(ping);
+ this._log.trace("_persistCurrentPings - saved ping " + id);
+ } catch (ex) {
+ this._log.error("_persistCurrentPings - failed to save ping " + id, ex);
+ } finally {
+ this._currentPings.delete(id);
+ }
+ }
+ }),
+
+ /**
+ * Returns the current pending, not yet persisted, pings, newest first.
+ */
+ getUnpersistedPings: function() {
+ let current = [...this._currentPings.values()];
+ current.reverse();
+ return current;
+ },
+
+ getShutdownState: function() {
+ return {
+ sendingEnabled: this._sendingEnabled,
+ pendingPingRequestCount: this._pendingPingRequests.size,
+ pendingPingActivityCount: this._pendingPingActivity.size,
+ unpersistedPingCount: this._currentPings.size,
+ persistedPingCount: TelemetryStorage.getPendingPingList().length,
+ schedulerState: SendScheduler.getShutdownState(),
+ };
+ },
+};
diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm
new file mode 100644
index 0000000000..3d97dc1554
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -0,0 +1,2124 @@
+/* -*- js-indent-level: 2; 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/debug.js", this);
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/DeferredTask.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+const Utils = TelemetryUtils;
+
+const myScope = this;
+
+// When modifying the payload in incompatible ways, please bump this version number
+const PAYLOAD_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
+const REASON_TEST_PING = "test-ping";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+const HISTOGRAM_SUFFIXES = {
+ PARENT: "",
+ CONTENT: "#content",
+ GPU: "#gpu",
+}
+
+const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange";
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MIN_SUBSESSION_LENGTH_MS = Preferences.get("toolkit.telemetry.minSubsessionLength", 10 * 60) * 1000;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::");
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit.enabled";
+const PREF_UNIFIED = PREF_BRANCH + "unified";
+
+
+const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
+const MESSAGE_TELEMETRY_THREAD_HANGS = "Telemetry:ChildThreadHangs";
+const MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS = "Telemetry:GetChildThreadHangs";
+const MESSAGE_TELEMETRY_USS = "Telemetry:USS";
+const MESSAGE_TELEMETRY_GET_CHILD_USS = "Telemetry:GetChildUSS";
+
+const DATAREPORTING_DIRECTORY = "datareporting";
+const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
+
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
+
+// Maximum number of content payloads that we are willing to store.
+const MAX_NUM_CONTENT_PAYLOADS = 10;
+
+// Do not gather data more than once a minute (ms)
+const TELEMETRY_INTERVAL = 60 * 1000;
+// Delay before intializing telemetry (ms)
+const TELEMETRY_DELAY = Preferences.get("toolkit.telemetry.initDelay", 60) * 1000;
+// Delay before initializing telemetry if we're testing (ms)
+const TELEMETRY_TEST_DELAY = 1;
+// Execute a scheduler tick every 5 minutes.
+const SCHEDULER_TICK_INTERVAL_MS = Preferences.get("toolkit.telemetry.scheduler.tickInterval", 5 * 60) * 1000;
+// When user is idle, execute a scheduler tick every 60 minutes.
+const SCHEDULER_TICK_IDLE_INTERVAL_MS = Preferences.get("toolkit.telemetry.scheduler.idleTickInterval", 60 * 60) * 1000;
+
+// The tolerance we have when checking if it's midnight (15 minutes).
+const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
+
+// Seconds of idle time before pinging.
+// On idle-daily a gather-telemetry notification is fired, during it probes can
+// start asynchronous tasks to gather data.
+const IDLE_TIMEOUT_SECONDS = Preferences.get("toolkit.telemetry.idleTimeout", 5 * 60);
+
+// To avoid generating too many main pings, we ignore environment changes that
+// happen within this interval since the last main ping.
+const CHANGE_THROTTLE_INTERVAL_MS = 5 * 60 * 1000;
+
+// The frequency at which we persist session data to the disk to prevent data loss
+// in case of aborted sessions (currently 5 minutes).
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+const TOPIC_CYCLE_COLLECTOR_BEGIN = "cycle-collector-begin";
+
+// How long to wait in millis for all the child memory reports to come in
+const TOTAL_MEMORY_COLLECTOR_TIMEOUT = 200;
+
+var gLastMemoryPoll = null;
+
+var gWasDebuggerAttached = false;
+
+XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
+ "@mozilla.org/base/telemetry;1",
+ "nsITelemetry");
+XPCOMUtils.defineLazyServiceGetter(this, "idleService",
+ "@mozilla.org/widget/idleservice;1",
+ "nsIIdleService");
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+XPCOMUtils.defineLazyServiceGetter(this, "cpml",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageListenerManager");
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageBroadcaster");
+XPCOMUtils.defineLazyServiceGetter(this, "ppml",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController",
+ "resource://gre/modules/TelemetryController.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage",
+ "resource://gre/modules/TelemetryStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
+ "resource://gre/modules/TelemetryLog.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ThirdPartyCookieProbe",
+ "resource://gre/modules/ThirdPartyCookieProbe.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "GCTelemetry",
+ "resource://gre/modules/GCTelemetry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
+ "resource://gre/modules/TelemetryEnvironment.jsm");
+
+function generateUUID() {
+ let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ // strip {}
+ return str.substring(1, str.length - 1);
+}
+
+function getMsSinceProcessStart() {
+ try {
+ return Telemetry.msSinceProcessStart();
+ } catch (ex) {
+ // If this fails return a special value.
+ return -1;
+ }
+}
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+ now: () => new Date(),
+ monotonicNow: getMsSinceProcessStart,
+ generateSessionUUID: () => generateUUID(),
+ generateSubsessionUUID: () => generateUUID(),
+ setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+ clearSchedulerTickTimeout: id => clearTimeout(id),
+};
+
+/**
+ * Get the ping type based on the payload.
+ * @param {Object} aPayload The ping payload.
+ * @return {String} A string representing the ping type.
+ */
+function getPingType(aPayload) {
+ // To remain consistent with server-side ping handling, set "saved-session" as the ping
+ // type for "saved-session" payload reasons.
+ if (aPayload.info.reason == REASON_SAVED_SESSION) {
+ return PING_TYPE_SAVED_SESSION;
+ }
+
+ return PING_TYPE_MAIN;
+}
+
+/**
+ * Annotate the current session ID with the crash reporter to map potential
+ * crash pings with the related main ping.
+ */
+function annotateCrashReport(sessionId) {
+ try {
+ const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"];
+ if (cr) {
+ cr.getService(Ci.nsICrashReporter).setTelemetrySessionId(sessionId);
+ }
+ } catch (e) {
+ // Ignore errors when crash reporting is disabled
+ }
+}
+
+/**
+ * Read current process I/O counters.
+ */
+var processInfo = {
+ _initialized: false,
+ _IO_COUNTERS: null,
+ _kernel32: null,
+ _GetProcessIoCounters: null,
+ _GetCurrentProcess: null,
+ getCounters: function() {
+ let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
+ if (isWindows)
+ return this.getCounters_Windows();
+ return null;
+ },
+ getCounters_Windows: function() {
+ if (!this._initialized) {
+ Cu.import("resource://gre/modules/ctypes.jsm");
+ this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [
+ {'readOps': ctypes.unsigned_long_long},
+ {'writeOps': ctypes.unsigned_long_long},
+ {'otherOps': ctypes.unsigned_long_long},
+ {'readBytes': ctypes.unsigned_long_long},
+ {'writeBytes': ctypes.unsigned_long_long},
+ {'otherBytes': ctypes.unsigned_long_long} ]);
+ try {
+ this._kernel32 = ctypes.open("Kernel32.dll");
+ this._GetProcessIoCounters = this._kernel32.declare("GetProcessIoCounters",
+ ctypes.winapi_abi,
+ ctypes.bool, // return
+ ctypes.voidptr_t, // hProcess
+ this._IO_COUNTERS.ptr); // lpIoCounters
+ this._GetCurrentProcess = this._kernel32.declare("GetCurrentProcess",
+ ctypes.winapi_abi,
+ ctypes.voidptr_t); // return
+ this._initialized = true;
+ } catch (err) {
+ return null;
+ }
+ }
+ let io = new this._IO_COUNTERS();
+ if (!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address()))
+ return null;
+ return [parseInt(io.readBytes), parseInt(io.writeBytes)];
+ }
+};
+
+/**
+ * TelemetryScheduler contains a single timer driving all regularly-scheduled
+ * Telemetry related jobs. Having a single place with this logic simplifies
+ * reasoning about scheduling actions in a single place, making it easier to
+ * coordinate jobs and coalesce them.
+ */
+var TelemetryScheduler = {
+ _lastDailyPingTime: 0,
+ _lastSessionCheckpointTime: 0,
+
+ // For sanity checking.
+ _lastAdhocPingTime: 0,
+ _lastTickTime: 0,
+
+ _log: null,
+
+ // The timer which drives the scheduler.
+ _schedulerTimer: null,
+ // The interval used by the scheduler timer.
+ _schedulerInterval: 0,
+ _shuttingDown: true,
+ _isUserIdle: false,
+
+ /**
+ * Initialises the scheduler and schedules the first daily/aborted session pings.
+ */
+ init: function() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "TelemetryScheduler::");
+ this._log.trace("init");
+ this._shuttingDown = false;
+ this._isUserIdle = false;
+
+ // Initialize the last daily ping and aborted session last due times to the current time.
+ // Otherwise, we might end up sending daily pings even if the subsession is not long enough.
+ let now = Policy.now();
+ this._lastDailyPingTime = now.getTime();
+ this._lastSessionCheckpointTime = now.getTime();
+ this._rescheduleTimeout();
+
+ idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+ Services.obs.addObserver(this, "wake_notification", false);
+ },
+
+ /**
+ * Stops the scheduler.
+ */
+ shutdown: function() {
+ if (this._shuttingDown) {
+ if (this._log) {
+ this._log.error("shutdown - Already shut down");
+ } else {
+ Cu.reportError("TelemetryScheduler.shutdown - Already shut down");
+ }
+ return;
+ }
+
+ this._log.trace("shutdown");
+ if (this._schedulerTimer) {
+ Policy.clearSchedulerTickTimeout(this._schedulerTimer);
+ this._schedulerTimer = null;
+ }
+
+ idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+ Services.obs.removeObserver(this, "wake_notification");
+
+ this._shuttingDown = true;
+ },
+
+ _clearTimeout: function() {
+ if (this._schedulerTimer) {
+ Policy.clearSchedulerTickTimeout(this._schedulerTimer);
+ }
+ },
+
+ /**
+ * Reschedules the tick timer.
+ */
+ _rescheduleTimeout: function() {
+ this._log.trace("_rescheduleTimeout - isUserIdle: " + this._isUserIdle);
+ if (this._shuttingDown) {
+ this._log.warn("_rescheduleTimeout - already shutdown");
+ return;
+ }
+
+ this._clearTimeout();
+
+ const now = Policy.now();
+ let timeout = SCHEDULER_TICK_INTERVAL_MS;
+
+ // When the user is idle we want to fire the timer less often.
+ if (this._isUserIdle) {
+ timeout = SCHEDULER_TICK_IDLE_INTERVAL_MS;
+ // We need to make sure though that we don't miss sending pings around
+ // midnight when we use the longer idle intervals.
+ const nextMidnight = Utils.getNextMidnight(now);
+ timeout = Math.min(timeout, nextMidnight.getTime() - now.getTime());
+ }
+
+ this._log.trace("_rescheduleTimeout - scheduling next tick for " + new Date(now.getTime() + timeout));
+ this._schedulerTimer =
+ Policy.setSchedulerTickTimeout(() => this._onSchedulerTick(), timeout);
+ },
+
+ _sentDailyPingToday: function(nowDate) {
+ // This is today's date and also the previous midnight (0:00).
+ const todayDate = Utils.truncateToDays(nowDate);
+ // We consider a ping sent for today if it occured after or at 00:00 today.
+ return (this._lastDailyPingTime >= todayDate.getTime());
+ },
+
+ /**
+ * Checks if we can send a daily ping or not.
+ * @param {Object} nowDate A date object.
+ * @return {Boolean} True if we can send the daily ping, false otherwise.
+ */
+ _isDailyPingDue: function(nowDate) {
+ // The daily ping is not due if we already sent one today.
+ if (this._sentDailyPingToday(nowDate)) {
+ this._log.trace("_isDailyPingDue - already sent one today");
+ return false;
+ }
+
+ // Avoid overly short sessions.
+ const timeSinceLastDaily = nowDate.getTime() - this._lastDailyPingTime;
+ if (timeSinceLastDaily < MIN_SUBSESSION_LENGTH_MS) {
+ this._log.trace("_isDailyPingDue - delaying daily to keep minimum session length");
+ return false;
+ }
+
+ this._log.trace("_isDailyPingDue - is due");
+ return true;
+ },
+
+ /**
+ * An helper function to save an aborted-session ping.
+ * @param {Number} now The current time, in milliseconds.
+ * @param {Object} [competingPayload=null] If we are coalescing the daily and the
+ * aborted-session pings, this is the payload for the former. Note
+ * that the reason field of this payload will be changed.
+ * @return {Promise} A promise resolved when the ping is saved.
+ */
+ _saveAbortedPing: function(now, competingPayload=null) {
+ this._lastSessionCheckpointTime = now;
+ return Impl._saveAbortedSessionPing(competingPayload)
+ .catch(e => this._log.error("_saveAbortedPing - Failed", e));
+ },
+
+ /**
+ * The notifications handler.
+ */
+ observe: function(aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic);
+ switch (aTopic) {
+ case "idle":
+ // If the user is idle, increase the tick interval.
+ this._isUserIdle = true;
+ return this._onSchedulerTick();
+ case "active":
+ // User is back to work, restore the original tick interval.
+ this._isUserIdle = false;
+ return this._onSchedulerTick();
+ case "wake_notification":
+ // The machine woke up from sleep, trigger a tick to avoid sessions
+ // spanning more than a day.
+ // This is needed because sleep time does not count towards timeouts
+ // on Mac & Linux - see bug 1262386, bug 1204823 et al.
+ return this._onSchedulerTick();
+ }
+ return undefined;
+ },
+
+ /**
+ * Performs a scheduler tick. This function manages Telemetry recurring operations.
+ * @return {Promise} A promise, only used when testing, resolved when the scheduled
+ * operation completes.
+ */
+ _onSchedulerTick: function() {
+ // This call might not be triggered from a timeout. In that case we don't want to
+ // leave any previously scheduled timeouts pending.
+ this._clearTimeout();
+
+ if (this._shuttingDown) {
+ this._log.warn("_onSchedulerTick - already shutdown.");
+ return Promise.reject(new Error("Already shutdown."));
+ }
+
+ let promise = Promise.resolve();
+ try {
+ promise = this._schedulerTickLogic();
+ } catch (e) {
+ Telemetry.getHistogramById("TELEMETRY_SCHEDULER_TICK_EXCEPTION").add(1);
+ this._log.error("_onSchedulerTick - There was an exception", e);
+ } finally {
+ this._rescheduleTimeout();
+ }
+
+ // This promise is returned to make testing easier.
+ return promise;
+ },
+
+ /**
+ * Implements the scheduler logic.
+ * @return {Promise} Resolved when the scheduled task completes. Only used in tests.
+ */
+ _schedulerTickLogic: function() {
+ this._log.trace("_schedulerTickLogic");
+
+ let nowDate = Policy.now();
+ let now = nowDate.getTime();
+
+ if ((now - this._lastTickTime) > (1.1 * SCHEDULER_TICK_INTERVAL_MS) &&
+ (this._lastTickTime != 0)) {
+ Telemetry.getHistogramById("TELEMETRY_SCHEDULER_WAKEUP").add(1);
+ this._log.trace("_schedulerTickLogic - First scheduler tick after sleep.");
+ }
+ this._lastTickTime = now;
+
+ // Check if the daily ping is due.
+ const shouldSendDaily = this._isDailyPingDue(nowDate);
+
+ if (shouldSendDaily) {
+ Telemetry.getHistogramById("TELEMETRY_SCHEDULER_SEND_DAILY").add(1);
+ this._log.trace("_schedulerTickLogic - Daily ping due.");
+ this._lastDailyPingTime = now;
+ return Impl._sendDailyPing();
+ }
+
+ // Check if the aborted-session ping is due. If a daily ping was saved above, it was
+ // already duplicated as an aborted-session ping.
+ const isAbortedPingDue =
+ (now - this._lastSessionCheckpointTime) >= ABORTED_SESSION_UPDATE_INTERVAL_MS;
+ if (isAbortedPingDue) {
+ this._log.trace("_schedulerTickLogic - Aborted session ping due.");
+ return this._saveAbortedPing(now);
+ }
+
+ // No ping is due.
+ this._log.trace("_schedulerTickLogic - No ping due.");
+ return Promise.resolve();
+ },
+
+ /**
+ * Update the scheduled pings if some other ping was sent.
+ * @param {String} reason The reason of the ping that was sent.
+ * @param {Object} [competingPayload=null] The payload of the ping that was sent. The
+ * reason of this payload will be changed.
+ */
+ reschedulePings: function(reason, competingPayload = null) {
+ if (this._shuttingDown) {
+ this._log.error("reschedulePings - already shutdown");
+ return;
+ }
+
+ this._log.trace("reschedulePings - reason: " + reason);
+ let now = Policy.now();
+ this._lastAdhocPingTime = now.getTime();
+ if (reason == REASON_ENVIRONMENT_CHANGE) {
+ // We just generated an environment-changed ping, save it as an aborted session and
+ // update the schedules.
+ this._saveAbortedPing(now.getTime(), competingPayload);
+ // If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
+ let nearestMidnight = Utils.getNearestMidnight(now, SCHEDULER_MIDNIGHT_TOLERANCE_MS);
+ if (nearestMidnight) {
+ this._lastDailyPingTime = now.getTime();
+ }
+ }
+
+ this._rescheduleTimeout();
+ },
+};
+
+this.EXPORTED_SYMBOLS = ["TelemetrySession"];
+
+this.TelemetrySession = Object.freeze({
+ Constants: Object.freeze({
+ PREF_PREVIOUS_BUILDID: PREF_PREVIOUS_BUILDID,
+ }),
+ /**
+ * Send a ping to a test server. Used only for testing.
+ */
+ testPing: function() {
+ return Impl.testPing();
+ },
+ /**
+ * Returns the current telemetry payload.
+ * @param reason Optional, the reason to trigger the payload.
+ * @param clearSubsession Optional, whether to clear subsession specific data.
+ * @returns Object
+ */
+ getPayload: function(reason, clearSubsession = false) {
+ return Impl.getPayload(reason, clearSubsession);
+ },
+ /**
+ * Returns a promise that resolves to an array of thread hang stats from content processes, one entry per process.
+ * The structure of each entry is identical to that of "threadHangStats" in nsITelemetry.
+ * While thread hang stats are also part of the child payloads, this function is useful for cheaply getting this information,
+ * which is useful for realtime hang monitoring.
+ * Child processes that do not respond, or spawn/die during execution of this function are excluded from the result.
+ * @returns Promise
+ */
+ getChildThreadHangs: function() {
+ return Impl.getChildThreadHangs();
+ },
+ /**
+ * Save the session state to a pending file.
+ * Used only for testing purposes.
+ */
+ testSavePendingPing: function() {
+ return Impl.testSavePendingPing();
+ },
+ /**
+ * Collect and store information about startup.
+ */
+ gatherStartup: function() {
+ return Impl.gatherStartup();
+ },
+ /**
+ * Inform the ping which AddOns are installed.
+ *
+ * @param aAddOns - The AddOns.
+ */
+ setAddOns: function(aAddOns) {
+ return Impl.setAddOns(aAddOns);
+ },
+ /**
+ * Descriptive metadata
+ *
+ * @param reason
+ * The reason for the telemetry ping, this will be included in the
+ * returned metadata,
+ * @return The metadata as a JS object
+ */
+ getMetadata: function(reason) {
+ return Impl.getMetadata(reason);
+ },
+ /**
+ * Used only for testing purposes.
+ */
+ testReset: function() {
+ Impl._sessionId = null;
+ Impl._subsessionId = null;
+ Impl._previousSessionId = null;
+ Impl._previousSubsessionId = null;
+ Impl._subsessionCounter = 0;
+ Impl._profileSubsessionCounter = 0;
+ Impl._subsessionStartActiveTicks = 0;
+ Impl._subsessionStartTimeMonotonic = 0;
+ Impl._lastEnvironmentChangeDate = Policy.monotonicNow();
+ this.testUninstall();
+ },
+ /**
+ * Triggers shutdown of the module.
+ */
+ shutdown: function() {
+ return Impl.shutdownChromeProcess();
+ },
+ /**
+ * Sets up components used in the content process.
+ */
+ setupContent: function(testing = false) {
+ return Impl.setupContentProcess(testing);
+ },
+ /**
+ * Used only for testing purposes.
+ */
+ testUninstall: function() {
+ try {
+ Impl.uninstall();
+ } catch (ex) {
+ // Ignore errors
+ }
+ },
+ /**
+ * Lightweight init function, called as soon as Firefox starts.
+ */
+ earlyInit: function(aTesting = false) {
+ return Impl.earlyInit(aTesting);
+ },
+ /**
+ * Does the "heavy" Telemetry initialization later on, so we
+ * don't impact startup performance.
+ * @return {Promise} Resolved when the initialization completes.
+ */
+ delayedInit: function() {
+ return Impl.delayedInit();
+ },
+ /**
+ * Send a notification.
+ */
+ observe: function (aSubject, aTopic, aData) {
+ return Impl.observe(aSubject, aTopic, aData);
+ },
+});
+
+var Impl = {
+ _histograms: {},
+ _initialized: false,
+ _logger: null,
+ _prevValues: {},
+ _slowSQLStartup: {},
+ _hasWindowRestoredObserver: false,
+ _hasXulWindowVisibleObserver: false,
+ _startupIO : {},
+ // The previous build ID, if this is the first run with a new build.
+ // Null if this is the first run, or the previous build ID is unknown.
+ _previousBuildId: null,
+ // Telemetry payloads sent by child processes.
+ // Each element is in the format {source: <weak-ref>, payload: <object>},
+ // where source is a weak reference to the child process,
+ // and payload is the telemetry payload from that child process.
+ _childTelemetry: [],
+ // Thread hangs from child processes.
+ // Used for TelemetrySession.getChildThreadHangs(); not sent with Telemetry pings.
+ // TelemetrySession.getChildThreadHangs() is used by extensions such as Statuser (https://github.com/chutten/statuser).
+ // Each element is in the format {source: <weak-ref>, payload: <object>},
+ // where source is a weak reference to the child process,
+ // and payload contains the thread hang stats from that child process.
+ _childThreadHangs: [],
+ // Array of the resolve functions of all the promises that are waiting for the child thread hang stats to arrive, used to resolve all those promises at once.
+ _childThreadHangsResolveFunctions: [],
+ // Timeout function for child thread hang stats retrieval.
+ _childThreadHangsTimeout: null,
+ // Unique id that identifies this session so the server can cope with duplicate
+ // submissions, orphaning and other oddities. The id is shared across subsessions.
+ _sessionId: null,
+ // Random subsession id.
+ _subsessionId: null,
+ // Session id of the previous session, null on first run.
+ _previousSessionId: null,
+ // Subsession id of the previous subsession (even if it was in a different session),
+ // null on first run.
+ _previousSubsessionId: null,
+ // The running no. of subsessions since the start of the browser session
+ _subsessionCounter: 0,
+ // The running no. of all subsessions for the whole profile life time
+ _profileSubsessionCounter: 0,
+ // Date of the last session split
+ _subsessionStartDate: null,
+ // Start time of the current subsession using a monotonic clock for the subsession
+ // length measurements.
+ _subsessionStartTimeMonotonic: 0,
+ // The active ticks counted when the subsession starts
+ _subsessionStartActiveTicks: 0,
+ // A task performing delayed initialization of the chrome process
+ _delayedInitTask: null,
+ // Need a timeout in case children are tardy in giving back their memory reports.
+ _totalMemoryTimeout: undefined,
+ _testing: false,
+ // An accumulator of total memory across all processes. Only valid once the final child reports.
+ _totalMemory: null,
+ // A Set of outstanding USS report ids
+ _childrenToHearFrom: null,
+ // monotonically-increasing id for USS reports
+ _nextTotalMemoryId: 1,
+ _lastEnvironmentChangeDate: 0,
+
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+ return this._logger;
+ },
+
+ /**
+ * Gets a series of simple measurements (counters). At the moment, this
+ * only returns startup data from nsIAppStartup.getStartupInfo().
+ * @param {Boolean} isSubsession True if this is a subsession, false otherwise.
+ * @param {Boolean} clearSubsession True if a new subsession is being started, false otherwise.
+ *
+ * @return simple measurements as a dictionary.
+ */
+ getSimpleMeasurements: function getSimpleMeasurements(forSavedSession, isSubsession, clearSubsession) {
+ this._log.trace("getSimpleMeasurements");
+
+ let si = Services.startup.getStartupInfo();
+
+ // Measurements common to chrome and content processes.
+ let elapsedTime = Date.now() - si.process;
+ var ret = {
+ totalTime: Math.round(elapsedTime / 1000), // totalTime, in seconds
+ uptime: Math.round(elapsedTime / 60000) // uptime in minutes
+ }
+
+ // Look for app-specific timestamps
+ var appTimestamps = {};
+ try {
+ let o = {};
+ Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", o);
+ appTimestamps = o.TelemetryTimestamps.get();
+ } catch (ex) {}
+
+ // Only submit this if the extended set is enabled.
+ if (!Utils.isContentProcess && Telemetry.canRecordExtended) {
+ try {
+ ret.addonManager = AddonManagerPrivate.getSimpleMeasures();
+ ret.UITelemetry = UITelemetry.getSimpleMeasures();
+ } catch (ex) {}
+ }
+
+ if (si.process) {
+ for (let field of Object.keys(si)) {
+ if (field == "process")
+ continue;
+ ret[field] = si[field] - si.process
+ }
+
+ for (let p in appTimestamps) {
+ if (!(p in ret) && appTimestamps[p])
+ ret[p] = appTimestamps[p] - si.process;
+ }
+ }
+
+ ret.startupInterrupted = Number(Services.startup.interrupted);
+
+ ret.js = Cu.getJSEngineTelemetryValue();
+
+ let maximalNumberOfConcurrentThreads = Telemetry.maximalNumberOfConcurrentThreads;
+ if (maximalNumberOfConcurrentThreads) {
+ ret.maximalNumberOfConcurrentThreads = maximalNumberOfConcurrentThreads;
+ }
+
+ if (Utils.isContentProcess) {
+ return ret;
+ }
+
+ // Measurements specific to chrome process
+
+ // Update debuggerAttached flag
+ let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+ let isDebuggerAttached = debugService.isDebuggerAttached;
+ gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached;
+ ret.debuggerAttached = Number(gWasDebuggerAttached);
+
+ let shutdownDuration = Telemetry.lastShutdownDuration;
+ if (shutdownDuration)
+ ret.shutdownDuration = shutdownDuration;
+
+ let failedProfileLockCount = Telemetry.failedProfileLockCount;
+ if (failedProfileLockCount)
+ ret.failedProfileLockCount = failedProfileLockCount;
+
+ for (let ioCounter in this._startupIO)
+ ret[ioCounter] = this._startupIO[ioCounter];
+
+ ret.savedPings = TelemetryStorage.pendingPingCount;
+
+ ret.activeTicks = -1;
+ let sr = TelemetryController.getSessionRecorder();
+ if (sr) {
+ let activeTicks = sr.activeTicks;
+ if (isSubsession) {
+ activeTicks = sr.activeTicks - this._subsessionStartActiveTicks;
+ }
+
+ if (clearSubsession) {
+ this._subsessionStartActiveTicks = activeTicks;
+ }
+
+ ret.activeTicks = activeTicks;
+ }
+
+ ret.pingsOverdue = TelemetrySend.overduePingsCount;
+
+ return ret;
+ },
+
+ /**
+ * When reflecting a histogram into JS, Telemetry hands us an object
+ * with the following properties:
+ *
+ * - min, max, histogram_type, sum, sum_squares_{lo,hi}: simple integers;
+ * - counts: array of counts for histogram buckets;
+ * - ranges: array of calculated bucket sizes.
+ *
+ * This format is not straightforward to read and potentially bulky
+ * with lots of zeros in the counts array. Packing histograms makes
+ * raw histograms easier to read and compresses the data a little bit.
+ *
+ * Returns an object:
+ * { range: [min, max], bucket_count: <number of buckets>,
+ * histogram_type: <histogram_type>, sum: <sum>,
+ * values: { bucket1: count1, bucket2: count2, ... } }
+ */
+ packHistogram: function packHistogram(hgram) {
+ let r = hgram.ranges;
+ let c = hgram.counts;
+ let retgram = {
+ range: [r[1], r[r.length - 1]],
+ bucket_count: r.length,
+ histogram_type: hgram.histogram_type,
+ values: {},
+ sum: hgram.sum
+ };
+
+ let first = true;
+ let last = 0;
+
+ for (let i = 0; i < c.length; i++) {
+ let value = c[i];
+ if (!value)
+ continue;
+
+ // add a lower bound
+ if (i && first) {
+ retgram.values[r[i - 1]] = 0;
+ }
+ first = false;
+ last = i + 1;
+ retgram.values[r[i]] = value;
+ }
+
+ // add an upper bound
+ if (last && last < c.length)
+ retgram.values[r[last]] = 0;
+ return retgram;
+ },
+
+ /**
+ * Get the type of the dataset that needs to be collected, based on the preferences.
+ * @return {Integer} A value from nsITelemetry.DATASET_*.
+ */
+ getDatasetType: function() {
+ return Telemetry.canRecordExtended ? Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN
+ : Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+ },
+
+ getHistograms: function getHistograms(subsession, clearSubsession) {
+ this._log.trace("getHistograms - subsession: " + subsession +
+ ", clearSubsession: " + clearSubsession);
+
+ let registered =
+ Telemetry.registeredHistograms(this.getDatasetType(), []);
+ if (this._testing == false) {
+ // Omit telemetry test histograms outside of tests.
+ registered = registered.filter(n => !n.startsWith("TELEMETRY_TEST_"));
+ }
+ registered = registered.concat(registered.map(n => "STARTUP_" + n));
+
+ let hls = subsession ? Telemetry.snapshotSubsessionHistograms(clearSubsession)
+ : Telemetry.histogramSnapshots;
+ let ret = {};
+
+ for (let name of registered) {
+ for (let suffix of Object.values(HISTOGRAM_SUFFIXES)) {
+ if (name + suffix in hls) {
+ if (!(suffix in ret)) {
+ ret[suffix] = {};
+ }
+ ret[suffix][name] = this.packHistogram(hls[name + suffix]);
+ }
+ }
+ }
+
+ return ret;
+ },
+
+ getAddonHistograms: function getAddonHistograms() {
+ this._log.trace("getAddonHistograms");
+
+ let ahs = Telemetry.addonHistogramSnapshots;
+ let ret = {};
+
+ for (let addonName in ahs) {
+ let addonHistograms = ahs[addonName];
+ let packedHistograms = {};
+ for (let name in addonHistograms) {
+ packedHistograms[name] = this.packHistogram(addonHistograms[name]);
+ }
+ if (Object.keys(packedHistograms).length != 0)
+ ret[addonName] = packedHistograms;
+ }
+
+ return ret;
+ },
+
+ getKeyedHistograms: function(subsession, clearSubsession) {
+ this._log.trace("getKeyedHistograms - subsession: " + subsession +
+ ", clearSubsession: " + clearSubsession);
+
+ let registered =
+ Telemetry.registeredKeyedHistograms(this.getDatasetType(), []);
+ if (this._testing == false) {
+ // Omit telemetry test histograms outside of tests.
+ registered = registered.filter(id => !id.startsWith("TELEMETRY_TEST_"));
+ }
+ let ret = {};
+
+ for (let id of registered) {
+ for (let suffix of Object.values(HISTOGRAM_SUFFIXES)) {
+ let keyed = Telemetry.getKeyedHistogramById(id + suffix);
+ let snapshot = null;
+ if (subsession) {
+ snapshot = clearSubsession ? keyed.snapshotSubsessionAndClear()
+ : keyed.subsessionSnapshot();
+ } else {
+ snapshot = keyed.snapshot();
+ }
+
+ let keys = Object.keys(snapshot);
+ if (keys.length == 0) {
+ // Skip empty keyed histogram.
+ continue;
+ }
+
+ if (!(suffix in ret)) {
+ ret[suffix] = {};
+ }
+ ret[suffix][id] = {};
+ for (let key of keys) {
+ ret[suffix][id][key] = this.packHistogram(snapshot[key]);
+ }
+ }
+ }
+
+ return ret;
+ },
+
+ /**
+ * Get a snapshot of the scalars and clear them.
+ * @param {subsession} If true, then we collect the data for a subsession.
+ * @param {clearSubsession} If true, we need to clear the subsession.
+ * @param {keyed} Take a snapshot of keyed or non keyed scalars.
+ * @return {Object} The scalar data as a Javascript object.
+ */
+ getScalars: function (subsession, clearSubsession, keyed) {
+ this._log.trace("getScalars - subsession: " + subsession + ", clearSubsession: " +
+ clearSubsession + ", keyed: " + keyed);
+
+ if (!subsession) {
+ // We only support scalars for subsessions.
+ this._log.trace("getScalars - We only support scalars in subsessions.");
+ return {};
+ }
+
+ let scalarsSnapshot = keyed ?
+ Telemetry.snapshotKeyedScalars(this.getDatasetType(), clearSubsession) :
+ Telemetry.snapshotScalars(this.getDatasetType(), clearSubsession);
+
+ // Don't return the test scalars.
+ let ret = {};
+ for (let name in scalarsSnapshot) {
+ if (name.startsWith('telemetry.test') && this._testing == false) {
+ this._log.trace("getScalars - Skipping test scalar: " + name);
+ } else {
+ ret[name] = scalarsSnapshot[name];
+ }
+ }
+
+ return ret;
+ },
+
+ getEvents: function(isSubsession, clearSubsession) {
+ if (!isSubsession) {
+ // We only support scalars for subsessions.
+ this._log.trace("getEvents - We only support events in subsessions.");
+ return [];
+ }
+
+ let events = Telemetry.snapshotBuiltinEvents(this.getDatasetType(),
+ clearSubsession);
+
+ // Don't return the test events outside of test environments.
+ if (!this._testing) {
+ events = events.filter(e => !e[1].startsWith("telemetry.test"));
+ }
+
+ return events;
+ },
+
+ getThreadHangStats: function getThreadHangStats(stats) {
+ this._log.trace("getThreadHangStats");
+
+ stats.forEach((thread) => {
+ thread.activity = this.packHistogram(thread.activity);
+ thread.hangs.forEach((hang) => {
+ hang.histogram = this.packHistogram(hang.histogram);
+ });
+ });
+ return stats;
+ },
+
+ /**
+ * Descriptive metadata
+ *
+ * @param reason
+ * The reason for the telemetry ping, this will be included in the
+ * returned metadata,
+ * @return The metadata as a JS object
+ */
+ getMetadata: function getMetadata(reason) {
+ this._log.trace("getMetadata - Reason " + reason);
+
+ const sessionStartDate = Utils.toLocalTimeISOString(Utils.truncateToDays(this._sessionStartDate));
+ const subsessionStartDate = Utils.toLocalTimeISOString(Utils.truncateToDays(this._subsessionStartDate));
+ const monotonicNow = Policy.monotonicNow();
+
+ let ret = {
+ reason: reason,
+ revision: AppConstants.SOURCE_REVISION_URL,
+ asyncPluginInit: Preferences.get(PREF_ASYNC_PLUGIN_INIT, false),
+
+ // Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of
+ // UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here.
+ timezoneOffset: -this._subsessionStartDate.getTimezoneOffset(),
+ previousBuildId: this._previousBuildId,
+
+ sessionId: this._sessionId,
+ subsessionId: this._subsessionId,
+ previousSessionId: this._previousSessionId,
+ previousSubsessionId: this._previousSubsessionId,
+
+ subsessionCounter: this._subsessionCounter,
+ profileSubsessionCounter: this._profileSubsessionCounter,
+
+ sessionStartDate: sessionStartDate,
+ subsessionStartDate: subsessionStartDate,
+
+ // Compute the session and subsession length in seconds.
+ // We use monotonic clocks as Date() is affected by jumping clocks (leading
+ // to negative lengths and other issues).
+ sessionLength: Math.floor(monotonicNow / 1000),
+ subsessionLength:
+ Math.floor((monotonicNow - this._subsessionStartTimeMonotonic) / 1000),
+ };
+
+ // TODO: Remove this when bug 1201837 lands.
+ if (this._addons)
+ ret.addons = this._addons;
+
+ // TODO: Remove this when bug 1201837 lands.
+ let flashVersion = this.getFlashVersion();
+ if (flashVersion)
+ ret.flashVersion = flashVersion;
+
+ return ret;
+ },
+
+ /**
+ * Pull values from about:memory into corresponding histograms
+ */
+ gatherMemory: function gatherMemory() {
+ if (!Telemetry.canRecordExtended) {
+ this._log.trace("gatherMemory - Extended data recording disabled, skipping.");
+ return;
+ }
+
+ this._log.trace("gatherMemory");
+
+ let mgr;
+ try {
+ mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+ } catch (e) {
+ // OK to skip memory reporters in xpcshell
+ return;
+ }
+
+ let histogram = Telemetry.getHistogramById("TELEMETRY_MEMORY_REPORTER_MS");
+ let startTime = new Date();
+
+ // Get memory measurements from distinguished amount attributes. We used
+ // to measure "explicit" too, but it could cause hangs, and the data was
+ // always really noisy anyway. See bug 859657.
+ //
+ // test_TelemetryController.js relies on some of these histograms being
+ // here. If you remove any of the following histograms from here, you'll
+ // have to modify test_TelemetryController.js:
+ //
+ // * MEMORY_JS_GC_HEAP, and
+ // * MEMORY_JS_COMPARTMENTS_SYSTEM.
+ //
+ // The distinguished amount attribute names don't match the telemetry id
+ // names in some cases due to a combination of (a) historical reasons, and
+ // (b) the fact that we can't change telemetry id names without breaking
+ // data continuity.
+ //
+ let boundHandleMemoryReport = this.handleMemoryReport.bind(this);
+ function h(id, units, amountName) {
+ try {
+ // If mgr[amountName] throws an exception, just move on -- some amounts
+ // aren't available on all platforms. But if the attribute simply
+ // isn't present, that indicates the distinguished amounts have changed
+ // and this file hasn't been updated appropriately.
+ let amount = mgr[amountName];
+ NS_ASSERT(amount !== undefined,
+ "telemetry accessed an unknown distinguished amount");
+ boundHandleMemoryReport(id, units, amount);
+ } catch (e) {
+ }
+ }
+ let b = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_BYTES, n);
+ let c = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT, n);
+ let cc= (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE, n);
+ let p = (id, n) => h(id, Ci.nsIMemoryReporter.UNITS_PERCENTAGE, n);
+
+ b("MEMORY_VSIZE", "vsize");
+ b("MEMORY_VSIZE_MAX_CONTIGUOUS", "vsizeMaxContiguous");
+ b("MEMORY_RESIDENT_FAST", "residentFast");
+ b("MEMORY_UNIQUE", "residentUnique");
+ b("MEMORY_HEAP_ALLOCATED", "heapAllocated");
+ p("MEMORY_HEAP_OVERHEAD_FRACTION", "heapOverheadFraction");
+ b("MEMORY_JS_GC_HEAP", "JSMainRuntimeGCHeap");
+ c("MEMORY_JS_COMPARTMENTS_SYSTEM", "JSMainRuntimeCompartmentsSystem");
+ c("MEMORY_JS_COMPARTMENTS_USER", "JSMainRuntimeCompartmentsUser");
+ b("MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED", "imagesContentUsedUncompressed");
+ b("MEMORY_STORAGE_SQLITE", "storageSQLite");
+ cc("LOW_MEMORY_EVENTS_VIRTUAL", "lowMemoryEventsVirtual");
+ cc("LOW_MEMORY_EVENTS_PHYSICAL", "lowMemoryEventsPhysical");
+ c("GHOST_WINDOWS", "ghostWindows");
+ cc("PAGE_FAULTS_HARD", "pageFaultsHard");
+
+ if (!Utils.isContentProcess && !this._totalMemoryTimeout) {
+ // Only the chrome process should gather total memory
+ // total = parent RSS + sum(child USS)
+ this._totalMemory = mgr.residentFast;
+ if (ppmm.childCount > 1) {
+ // Do not report If we time out waiting for the children to call
+ this._totalMemoryTimeout = setTimeout(
+ () => {
+ this._totalMemoryTimeout = undefined;
+ this._childrenToHearFrom.clear();
+ },
+ TOTAL_MEMORY_COLLECTOR_TIMEOUT);
+ this._childrenToHearFrom = new Set();
+ for (let i = 1; i < ppmm.childCount; i++) {
+ let child = ppmm.getChildAt(i);
+ try {
+ child.sendAsyncMessage(MESSAGE_TELEMETRY_GET_CHILD_USS, {id: this._nextTotalMemoryId});
+ this._childrenToHearFrom.add(this._nextTotalMemoryId);
+ this._nextTotalMemoryId++;
+ } catch (ex) {
+ // If a content process has just crashed, then attempting to send it
+ // an async message will throw an exception.
+ Cu.reportError(ex);
+ }
+ }
+ } else {
+ boundHandleMemoryReport(
+ "MEMORY_TOTAL",
+ Ci.nsIMemoryReporter.UNITS_BYTES,
+ this._totalMemory);
+ }
+ }
+
+ histogram.add(new Date() - startTime);
+ },
+
+ handleMemoryReport: function(id, units, amount) {
+ let val;
+ if (units == Ci.nsIMemoryReporter.UNITS_BYTES) {
+ val = Math.floor(amount / 1024);
+ }
+ else if (units == Ci.nsIMemoryReporter.UNITS_PERCENTAGE) {
+ // UNITS_PERCENTAGE amounts are 100x greater than their raw value.
+ val = Math.floor(amount / 100);
+ }
+ else if (units == Ci.nsIMemoryReporter.UNITS_COUNT) {
+ val = amount;
+ }
+ else if (units == Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE) {
+ // If the reporter gives us a cumulative count, we'll report the
+ // difference in its value between now and our previous ping.
+
+ if (!(id in this._prevValues)) {
+ // If this is the first time we're reading this reporter, store its
+ // current value but don't report it in the telemetry ping, so we
+ // ignore the effect startup had on the reporter.
+ this._prevValues[id] = amount;
+ return;
+ }
+
+ val = amount - this._prevValues[id];
+ this._prevValues[id] = amount;
+ }
+ else {
+ NS_ASSERT(false, "Can't handle memory reporter with units " + units);
+ return;
+ }
+
+ let h = this._histograms[id];
+ if (!h) {
+ h = Telemetry.getHistogramById(id);
+ this._histograms[id] = h;
+ }
+ h.add(val);
+ },
+
+ getChildPayloads: function getChildPayloads() {
+ return this._childTelemetry.map(child => child.payload);
+ },
+
+ /**
+ * Get the current session's payload using the provided
+ * simpleMeasurements and info, which are typically obtained by a call
+ * to |this.getSimpleMeasurements| and |this.getMetadata|,
+ * respectively.
+ */
+ assemblePayloadWithMeasurements: function(simpleMeasurements, info, reason, clearSubsession) {
+ const isSubsession = IS_UNIFIED_TELEMETRY && !this._isClassicReason(reason);
+ clearSubsession = IS_UNIFIED_TELEMETRY && clearSubsession;
+ this._log.trace("assemblePayloadWithMeasurements - reason: " + reason +
+ ", submitting subsession data: " + isSubsession);
+
+ // This allows wrapping data retrieval calls in a try-catch block so that
+ // failures don't break the rest of the ping assembly.
+ const protect = (fn, defaultReturn = null) => {
+ try {
+ return fn();
+ } catch (ex) {
+ this._log.error("assemblePayloadWithMeasurements - caught exception", ex);
+ return defaultReturn;
+ }
+ };
+
+ // Payload common to chrome and content processes.
+ let payloadObj = {
+ ver: PAYLOAD_VERSION,
+ simpleMeasurements: simpleMeasurements,
+ };
+
+ // Add extended set measurements common to chrome & content processes
+ if (Telemetry.canRecordExtended) {
+ payloadObj.chromeHangs = protect(() => Telemetry.chromeHangs);
+ payloadObj.threadHangStats = protect(() => this.getThreadHangStats(Telemetry.threadHangStats));
+ payloadObj.log = protect(() => TelemetryLog.entries());
+ payloadObj.webrtc = protect(() => Telemetry.webrtcStats);
+ }
+
+ if (Utils.isContentProcess) {
+ return payloadObj;
+ }
+
+ // Additional payload for chrome process.
+ let histograms = protect(() => this.getHistograms(isSubsession, clearSubsession), {});
+ let keyedHistograms = protect(() => this.getKeyedHistograms(isSubsession, clearSubsession), {});
+ payloadObj.histograms = histograms[HISTOGRAM_SUFFIXES.PARENT] || {};
+ payloadObj.keyedHistograms = keyedHistograms[HISTOGRAM_SUFFIXES.PARENT] || {};
+ payloadObj.processes = {
+ parent: {
+ scalars: protect(() => this.getScalars(isSubsession, clearSubsession)),
+ keyedScalars: protect(() => this.getScalars(isSubsession, clearSubsession, true)),
+ events: protect(() => this.getEvents(isSubsession, clearSubsession)),
+ },
+ content: {
+ histograms: histograms[HISTOGRAM_SUFFIXES.CONTENT],
+ keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.CONTENT],
+ },
+ };
+
+ // Only include the GPU process if we've accumulated data for it.
+ if (HISTOGRAM_SUFFIXES.GPU in histograms ||
+ HISTOGRAM_SUFFIXES.GPU in keyedHistograms)
+ {
+ payloadObj.processes.gpu = {
+ histograms: histograms[HISTOGRAM_SUFFIXES.GPU],
+ keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.GPU],
+ };
+ }
+
+ payloadObj.info = info;
+
+ // Add extended set measurements for chrome process.
+ if (Telemetry.canRecordExtended) {
+ payloadObj.slowSQL = protect(() => Telemetry.slowSQL);
+ payloadObj.fileIOReports = protect(() => Telemetry.fileIOReports);
+ payloadObj.lateWrites = protect(() => Telemetry.lateWrites);
+
+ // Add the addon histograms if they are present
+ let addonHistograms = protect(() => this.getAddonHistograms());
+ if (addonHistograms && Object.keys(addonHistograms).length > 0) {
+ payloadObj.addonHistograms = addonHistograms;
+ }
+
+ payloadObj.addonDetails = protect(() => AddonManagerPrivate.getTelemetryDetails());
+
+ let clearUIsession = !(reason == REASON_GATHER_PAYLOAD || reason == REASON_GATHER_SUBSESSION_PAYLOAD);
+ payloadObj.UIMeasurements = protect(() => UITelemetry.getUIMeasurements(clearUIsession));
+
+ if (this._slowSQLStartup &&
+ Object.keys(this._slowSQLStartup).length != 0 &&
+ (Object.keys(this._slowSQLStartup.mainThread).length ||
+ Object.keys(this._slowSQLStartup.otherThreads).length)) {
+ payloadObj.slowSQLStartup = this._slowSQLStartup;
+ }
+
+ if (!this._isClassicReason(reason)) {
+ payloadObj.processes.parent.gc = protect(() => GCTelemetry.entries("main", clearSubsession));
+ payloadObj.processes.content.gc = protect(() => GCTelemetry.entries("content", clearSubsession));
+ }
+ }
+
+ if (this._childTelemetry.length) {
+ payloadObj.childPayloads = protect(() => this.getChildPayloads());
+ }
+
+ return payloadObj;
+ },
+
+ /**
+ * Start a new subsession.
+ */
+ startNewSubsession: function () {
+ this._subsessionStartDate = Policy.now();
+ this._subsessionStartTimeMonotonic = Policy.monotonicNow();
+ this._previousSubsessionId = this._subsessionId;
+ this._subsessionId = Policy.generateSubsessionUUID();
+ this._subsessionCounter++;
+ this._profileSubsessionCounter++;
+ },
+
+ getSessionPayload: function getSessionPayload(reason, clearSubsession) {
+ this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession);
+
+ let payload;
+ try {
+ const isMobile = ["gonk", "android"].includes(AppConstants.platform);
+ const isSubsession = isMobile ? false : !this._isClassicReason(reason);
+
+ if (isMobile) {
+ clearSubsession = false;
+ }
+
+ let measurements =
+ this.getSimpleMeasurements(reason == REASON_SAVED_SESSION, isSubsession, clearSubsession);
+ let info = !Utils.isContentProcess ? this.getMetadata(reason) : null;
+ payload = this.assemblePayloadWithMeasurements(measurements, info, reason, clearSubsession);
+ } catch (ex) {
+ Telemetry.getHistogramById("TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION").add(1);
+ throw ex;
+ } finally {
+ if (!Utils.isContentProcess && clearSubsession) {
+ this.startNewSubsession();
+ // Persist session data to disk (don't wait until it completes).
+ let sessionData = this._getSessionDataObject();
+ TelemetryStorage.saveSessionData(sessionData);
+
+ // Notify that there was a subsession split in the parent process. This is an
+ // internal topic and is only meant for internal Telemetry usage.
+ Services.obs.notifyObservers(null, "internal-telemetry-after-subsession-split", null);
+ }
+ }
+
+ return payload;
+ },
+
+ /**
+ * Send data to the server. Record success/send-time in histograms
+ */
+ send: function send(reason) {
+ this._log.trace("send - Reason " + reason);
+ // populate histograms one last time
+ this.gatherMemory();
+
+ const isSubsession = !this._isClassicReason(reason);
+ let payload = this.getSessionPayload(reason, isSubsession);
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ return TelemetryController.submitExternalPing(getPingType(payload), payload, options);
+ },
+
+ attachObservers: function attachObservers() {
+ if (!this._initialized)
+ return;
+ Services.obs.addObserver(this, "idle-daily", false);
+ if (Telemetry.canRecordExtended) {
+ Services.obs.addObserver(this, TOPIC_CYCLE_COLLECTOR_BEGIN, false);
+ }
+ },
+
+ detachObservers: function detachObservers() {
+ if (!this._initialized)
+ return;
+ Services.obs.removeObserver(this, "idle-daily");
+ try {
+ // Tests may flip Telemetry.canRecordExtended on and off. Just try to remove this
+ // observer and catch if it fails because the observer was not added.
+ Services.obs.removeObserver(this, TOPIC_CYCLE_COLLECTOR_BEGIN);
+ } catch (e) {
+ this._log.warn("detachObservers - Failed to remove " + TOPIC_CYCLE_COLLECTOR_BEGIN, e);
+ }
+ },
+
+ /**
+ * Lightweight init function, called as soon as Firefox starts.
+ */
+ earlyInit: function(testing) {
+ this._log.trace("earlyInit");
+
+ this._initStarted = true;
+ this._testing = testing;
+
+ if (this._initialized && !testing) {
+ this._log.error("earlyInit - already initialized");
+ return;
+ }
+
+ if (!Telemetry.canRecordBase && !testing) {
+ this._log.config("earlyInit - Telemetry recording is disabled, skipping Chrome process setup.");
+ return;
+ }
+
+ // Generate a unique id once per session so the server can cope with duplicate
+ // submissions, orphaning and other oddities. The id is shared across subsessions.
+ this._sessionId = Policy.generateSessionUUID();
+ this.startNewSubsession();
+ // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
+ // the very same value for |_sessionStartDate|.
+ this._sessionStartDate = this._subsessionStartDate;
+
+ annotateCrashReport(this._sessionId);
+
+ // Initialize some probes that are kept in their own modules
+ this._thirdPartyCookies = new ThirdPartyCookieProbe();
+ this._thirdPartyCookies.init();
+
+ // Record old value and update build ID preference if this is the first
+ // run with a new build ID.
+ let previousBuildId = Preferences.get(PREF_PREVIOUS_BUILDID, null);
+ let thisBuildID = Services.appinfo.appBuildID;
+ // If there is no previousBuildId preference, we send null to the server.
+ if (previousBuildId != thisBuildID) {
+ this._previousBuildId = previousBuildId;
+ Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID);
+ }
+
+ Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+ if (AppConstants.platform === "android") {
+ Services.obs.addObserver(this, "application-background", false);
+ }
+ Services.obs.addObserver(this, "xul-window-visible", false);
+ this._hasWindowRestoredObserver = true;
+ this._hasXulWindowVisibleObserver = true;
+
+ ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this);
+ ppml.addMessageListener(MESSAGE_TELEMETRY_THREAD_HANGS, this);
+ ppml.addMessageListener(MESSAGE_TELEMETRY_USS, this);
+},
+
+/**
+ * Does the "heavy" Telemetry initialization later on, so we
+ * don't impact startup performance.
+ * @return {Promise} Resolved when the initialization completes.
+ */
+ delayedInit:function() {
+ this._log.trace("delayedInit");
+
+ this._delayedInitTask = Task.spawn(function* () {
+ try {
+ this._initialized = true;
+
+ yield this._loadSessionData();
+ // Update the session data to keep track of new subsessions created before
+ // the initialization.
+ yield TelemetryStorage.saveSessionData(this._getSessionDataObject());
+
+ this.attachObservers();
+ this.gatherMemory();
+
+ if (Telemetry.canRecordExtended) {
+ GCTelemetry.init();
+ }
+
+ Telemetry.asyncFetchTelemetryData(function () {});
+
+ if (IS_UNIFIED_TELEMETRY) {
+ // Check for a previously written aborted session ping.
+ yield TelemetryController.checkAbortedSessionPing();
+
+ // Write the first aborted-session ping as early as possible. Just do that
+ // if we are not testing, since calling Telemetry.reset() will make a previous
+ // aborted ping a pending ping.
+ if (!this._testing) {
+ yield this._saveAbortedSessionPing();
+ }
+
+ // The last change date for the environment, used to throttle environment changes.
+ this._lastEnvironmentChangeDate = Policy.monotonicNow();
+ TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
+ (reason, data) => this._onEnvironmentChange(reason, data));
+
+ // Start the scheduler.
+ // We skip this if unified telemetry is off, so we don't
+ // trigger the new unified ping types.
+ TelemetryScheduler.init();
+ }
+
+ this._delayedInitTask = null;
+ } catch (e) {
+ this._delayedInitTask = null;
+ throw e;
+ }
+ }.bind(this));
+
+ return this._delayedInitTask;
+ },
+
+ /**
+ * Initializes telemetry for a content process.
+ */
+ setupContentProcess: function setupContentProcess(testing) {
+ this._log.trace("setupContentProcess");
+ this._testing = testing;
+
+ if (!Telemetry.canRecordBase) {
+ this._log.trace("setupContentProcess - base recording is disabled, not initializing");
+ return;
+ }
+
+ Services.obs.addObserver(this, "content-child-shutdown", false);
+ cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS, this);
+ cpml.addMessageListener(MESSAGE_TELEMETRY_GET_CHILD_USS, this);
+
+ let delayedTask = new DeferredTask(function* () {
+ this._initialized = true;
+
+ this.attachObservers();
+ this.gatherMemory();
+
+ if (Telemetry.canRecordExtended) {
+ GCTelemetry.init();
+ }
+ }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
+
+ delayedTask.arm();
+ },
+
+ getFlashVersion: function getFlashVersion() {
+ this._log.trace("getFlashVersion");
+ let host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ let tags = host.getPluginTags();
+
+ for (let i = 0; i < tags.length; i++) {
+ if (tags[i].name == "Shockwave Flash")
+ return tags[i].version;
+ }
+
+ return null;
+ },
+
+ receiveMessage: function receiveMessage(message) {
+ this._log.trace("receiveMessage - Message name " + message.name);
+ switch (message.name) {
+ case MESSAGE_TELEMETRY_PAYLOAD:
+ {
+ // In parent process, receive Telemetry payload from child
+ let source = message.data.childUUID;
+ delete message.data.childUUID;
+
+ this._childTelemetry.push({
+ source: source,
+ payload: message.data,
+ });
+
+ if (this._childTelemetry.length == MAX_NUM_CONTENT_PAYLOADS + 1) {
+ this._childTelemetry.shift();
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_CONTENT_PINGS_COUNT").add();
+ }
+
+ break;
+ }
+ case MESSAGE_TELEMETRY_THREAD_HANGS:
+ {
+ // Accumulate child thread hang stats from this child
+ this._childThreadHangs.push(message.data);
+
+ // Check if we've got data from all the children, accounting for child processes dying
+ // if it happens before the last response is received and no new child processes are spawned at the exact same time
+ // If that happens, we can resolve the promise earlier rather than having to wait for the timeout to expire
+ // Basically, the number of replies is at most the number of messages sent out, this._childCount,
+ // and also at most the number of child processes that currently exist
+ if (this._childThreadHangs.length === Math.min(this._childCount, ppmm.childCount)) {
+ clearTimeout(this._childThreadHangsTimeout);
+
+ // Resolve all the promises that are waiting on these thread hang stats
+ // We resolve here instead of rejecting because
+ for (let resolve of this._childThreadHangsResolveFunctions) {
+ resolve(this._childThreadHangs);
+ }
+ this._childThreadHangsResolveFunctions = [];
+ }
+
+ break;
+ }
+ case MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS:
+ {
+ // In child process, send the requested child thread hangs
+ this.sendContentProcessThreadHangs();
+ break;
+ }
+ case MESSAGE_TELEMETRY_USS:
+ {
+ // In parent process, receive the USS report from the child
+ if (this._totalMemoryTimeout && this._childrenToHearFrom.delete(message.data.id)) {
+ this._totalMemory += message.data.bytes;
+ if (this._childrenToHearFrom.size == 0) {
+ clearTimeout(this._totalMemoryTimeout);
+ this._totalMemoryTimeout = undefined;
+ this.handleMemoryReport(
+ "MEMORY_TOTAL",
+ Ci.nsIMemoryReporter.UNITS_BYTES,
+ this._totalMemory);
+ }
+ } else {
+ this._log.trace("Child USS report was missed");
+ }
+ break;
+ }
+ case MESSAGE_TELEMETRY_GET_CHILD_USS:
+ {
+ // In child process, send the requested USS report
+ this.sendContentProcessUSS(message.data.id);
+ break
+ }
+ default:
+ throw new Error("Telemetry.receiveMessage: bad message name");
+ }
+ },
+
+ _processUUID: generateUUID(),
+
+ sendContentProcessUSS: function sendContentProcessUSS(aMessageId) {
+ this._log.trace("sendContentProcessUSS");
+
+ let mgr;
+ try {
+ mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(Ci.nsIMemoryReporterManager);
+ } catch (e) {
+ // OK to skip memory reporters in xpcshell
+ return;
+ }
+
+ cpmm.sendAsyncMessage(
+ MESSAGE_TELEMETRY_USS,
+ {bytes: mgr.residentUnique, id: aMessageId}
+ );
+ },
+
+ sendContentProcessPing: function sendContentProcessPing(reason) {
+ this._log.trace("sendContentProcessPing - Reason " + reason);
+ const isSubsession = !this._isClassicReason(reason);
+ let payload = this.getSessionPayload(reason, isSubsession);
+ payload.childUUID = this._processUUID;
+ cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_PAYLOAD, payload);
+ },
+
+ sendContentProcessThreadHangs: function sendContentProcessThreadHangs() {
+ this._log.trace("sendContentProcessThreadHangs");
+ let payload = {
+ childUUID: this._processUUID,
+ hangs: Telemetry.threadHangStats,
+ };
+ cpmm.sendAsyncMessage(MESSAGE_TELEMETRY_THREAD_HANGS, payload);
+ },
+
+ /**
+ * Save both the "saved-session" and the "shutdown" pings to disk.
+ * This needs to be called after TelemetrySend shuts down otherwise pings
+ * would be sent instead of getting persisted to disk.
+ */
+ saveShutdownPings: function() {
+ this._log.trace("saveShutdownPings");
+
+ // We don't wait for "shutdown" pings to be written to disk before gathering the
+ // "saved-session" payload. Instead we append the promises to this list and wait
+ // on both to be saved after kicking off their collection.
+ let p = [];
+
+ if (IS_UNIFIED_TELEMETRY) {
+ let shutdownPayload = this.getSessionPayload(REASON_SHUTDOWN, false);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ p.push(TelemetryController.submitExternalPing(getPingType(shutdownPayload), shutdownPayload, options)
+ .catch(e => this._log.error("saveShutdownPings - failed to submit shutdown ping", e)));
+ }
+
+ // As a temporary measure, we want to submit saved-session too if extended Telemetry is enabled
+ // to keep existing performance analysis working.
+ if (Telemetry.canRecordExtended) {
+ let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ p.push(TelemetryController.submitExternalPing(getPingType(payload), payload, options)
+ .catch (e => this._log.error("saveShutdownPings - failed to submit saved-session ping", e)));
+ }
+
+ // Wait on pings to be saved.
+ return Promise.all(p);
+ },
+
+
+ testSavePendingPing: function testSaveHistograms() {
+ this._log.trace("testSaveHistograms");
+ let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ overwrite: true,
+ };
+ return TelemetryController.addPendingPing(getPingType(payload), payload, options);
+ },
+
+ /**
+ * Do some shutdown work that is common to all process types.
+ */
+ uninstall: function uninstall() {
+ this.detachObservers();
+ if (this._hasWindowRestoredObserver) {
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this._hasWindowRestoredObserver = false;
+ }
+ if (this._hasXulWindowVisibleObserver) {
+ Services.obs.removeObserver(this, "xul-window-visible");
+ this._hasXulWindowVisibleObserver = false;
+ }
+ if (AppConstants.platform === "android") {
+ Services.obs.removeObserver(this, "application-background", false);
+ }
+ GCTelemetry.shutdown();
+ },
+
+ getPayload: function getPayload(reason, clearSubsession) {
+ this._log.trace("getPayload - clearSubsession: " + clearSubsession);
+ reason = reason || REASON_GATHER_PAYLOAD;
+ // This function returns the current Telemetry payload to the caller.
+ // We only gather startup info once.
+ if (Object.keys(this._slowSQLStartup).length == 0) {
+ this._slowSQLStartup = Telemetry.slowSQL;
+ }
+ this.gatherMemory();
+ return this.getSessionPayload(reason, clearSubsession);
+ },
+
+ getChildThreadHangs: function getChildThreadHangs() {
+ return new Promise((resolve) => {
+ // Return immediately if there are no child processes to get stats from
+ if (ppmm.childCount === 0) {
+ resolve([]);
+ return;
+ }
+
+ // Register our promise so it will be resolved when we receive the child thread hang stats on the parent process
+ // The resolve functions will all be called from "receiveMessage" when a MESSAGE_TELEMETRY_THREAD_HANGS message comes in
+ this._childThreadHangsResolveFunctions.push((threadHangStats) => {
+ let hangs = threadHangStats.map(child => child.hangs);
+ return resolve(hangs);
+ });
+
+ // If we (the parent) are not currently in the process of requesting child thread hangs, request them
+ // If we are, then the resolve function we registered above will receive the results without needing to request them again
+ if (this._childThreadHangsResolveFunctions.length === 1) {
+ // We have to cache the number of children we send messages to, in case the child count changes while waiting for messages to arrive
+ // This handles the case where the child count increases later on, in which case the new processes won't respond since we never sent messages to them
+ this._childCount = ppmm.childCount;
+
+ this._childThreadHangs = []; // Clear the child hangs
+ for (let i = 0; i < this._childCount; i++) {
+ // If a child dies at exactly while we're running this loop, the message sending will fail but we won't get an exception
+ // In this case, since we won't know this has happened, we will simply rely on the timeout to handle it
+ ppmm.getChildAt(i).sendAsyncMessage(MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS);
+ }
+
+ // Set up a timeout in case one or more of the content processes never responds
+ this._childThreadHangsTimeout = setTimeout(() => {
+ // Resolve all the promises that are waiting on these thread hang stats
+ // We resolve here instead of rejecting because the purpose of this function is
+ // to retrieve the BHR stats from all processes that will give us stats
+ // As a result, one process failing simply means it doesn't get included in the result.
+ for (let resolve of this._childThreadHangsResolveFunctions) {
+ resolve(this._childThreadHangs);
+ }
+ this._childThreadHangsResolveFunctions = [];
+ }, 200);
+ }
+ });
+ },
+
+ gatherStartup: function gatherStartup() {
+ this._log.trace("gatherStartup");
+ let counters = processInfo.getCounters();
+ if (counters) {
+ [this._startupIO.startupSessionRestoreReadBytes,
+ this._startupIO.startupSessionRestoreWriteBytes] = counters;
+ }
+ this._slowSQLStartup = Telemetry.slowSQL;
+ },
+
+ setAddOns: function setAddOns(aAddOns) {
+ this._addons = aAddOns;
+ },
+
+ testPing: function testPing() {
+ return this.send(REASON_TEST_PING);
+ },
+
+ /**
+ * This observer drives telemetry.
+ */
+ observe: function (aSubject, aTopic, aData) {
+ // Prevent the cycle collector begin topic from cluttering the log.
+ if (aTopic != TOPIC_CYCLE_COLLECTOR_BEGIN) {
+ this._log.trace("observe - " + aTopic + " notified.");
+ }
+
+ switch (aTopic) {
+ case "content-child-shutdown":
+ // content-child-shutdown is only registered for content processes.
+ Services.obs.removeObserver(this, "content-child-shutdown");
+ this.uninstall();
+ Telemetry.flushBatchedChildTelemetry();
+ this.sendContentProcessPing(REASON_SAVED_SESSION);
+ break;
+ case TOPIC_CYCLE_COLLECTOR_BEGIN:
+ let now = new Date();
+ if (!gLastMemoryPoll
+ || (TELEMETRY_INTERVAL <= now - gLastMemoryPoll)) {
+ gLastMemoryPoll = now;
+ this.gatherMemory();
+ }
+ break;
+ case "xul-window-visible":
+ Services.obs.removeObserver(this, "xul-window-visible");
+ this._hasXulWindowVisibleObserver = false;
+ var counters = processInfo.getCounters();
+ if (counters) {
+ [this._startupIO.startupWindowVisibleReadBytes,
+ this._startupIO.startupWindowVisibleWriteBytes] = counters;
+ }
+ break;
+ case "sessionstore-windows-restored":
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this._hasWindowRestoredObserver = false;
+ // Check whether debugger was attached during startup
+ let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+ gWasDebuggerAttached = debugService.isDebuggerAttached;
+ this.gatherStartup();
+ break;
+ case "idle-daily":
+ // Enqueue to main-thread, otherwise components may be inited by the
+ // idle-daily category and miss the gather-telemetry notification.
+ Services.tm.mainThread.dispatch((function() {
+ // Notify that data should be gathered now.
+ // TODO: We are keeping this behaviour for now but it will be removed as soon as
+ // bug 1127907 lands.
+ Services.obs.notifyObservers(null, "gather-telemetry", null);
+ }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ break;
+
+ case "application-background":
+ if (AppConstants.platform !== "android") {
+ break;
+ }
+ // On Android, we can get killed without warning once we are in the background,
+ // but we may also submit data and/or come back into the foreground without getting
+ // killed. To deal with this, we save the current session data to file when we are
+ // put into the background. This handles the following post-backgrounding scenarios:
+ // 1) We are killed immediately. In this case the current session data (which we
+ // save to a file) will be loaded and submitted on a future run.
+ // 2) We submit the data while in the background, and then are killed. In this case
+ // the file that we saved will be deleted by the usual process in
+ // finishPingRequest after it is submitted.
+ // 3) We submit the data, and then come back into the foreground. Same as case (2).
+ // 4) We do not submit the data, but come back into the foreground. In this case
+ // we have the option of either deleting the file that we saved (since we will either
+ // send the live data while in the foreground, or create the file again on the next
+ // backgrounding), or not (in which case we will delete it on submit, or overwrite
+ // it on the next backgrounding). Not deleting it is faster, so that's what we do.
+ let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ overwrite: true,
+ };
+ TelemetryController.addPendingPing(getPingType(payload), payload, options);
+ break;
+ }
+ return undefined;
+ },
+
+ /**
+ * This tells TelemetrySession to uninitialize and save any pending pings.
+ */
+ shutdownChromeProcess: function() {
+ this._log.trace("shutdownChromeProcess");
+
+ let cleanup = () => {
+ if (IS_UNIFIED_TELEMETRY) {
+ TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
+ TelemetryScheduler.shutdown();
+ }
+ this.uninstall();
+
+ let reset = () => {
+ this._initStarted = false;
+ this._initialized = false;
+ };
+
+ return Task.spawn(function*() {
+ yield this.saveShutdownPings();
+
+ if (IS_UNIFIED_TELEMETRY) {
+ yield TelemetryController.removeAbortedSessionPing();
+ }
+
+ reset();
+ }.bind(this));
+ };
+
+ // We can be in one the following states here:
+ // 1) delayedInit was never called
+ // or it was called and
+ // 2) _delayedInitTask is running now.
+ // 3) _delayedInitTask finished running already.
+
+ // This handles 1).
+ if (!this._initStarted) {
+ return Promise.resolve();
+ }
+
+ // This handles 3).
+ if (!this._delayedInitTask) {
+ // We already ran the delayed initialization.
+ return cleanup();
+ }
+
+ // This handles 2).
+ return this._delayedInitTask.then(cleanup);
+ },
+
+ /**
+ * Gather and send a daily ping.
+ * @return {Promise} Resolved when the ping is sent.
+ */
+ _sendDailyPing: function() {
+ this._log.trace("_sendDailyPing");
+ let payload = this.getSessionPayload(REASON_DAILY, true);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+
+ let promise = TelemetryController.submitExternalPing(getPingType(payload), payload, options);
+
+ // Also save the payload as an aborted session. If we delay this, aborted-session can
+ // lag behind for the profileSubsessionCounter and other state, complicating analysis.
+ if (IS_UNIFIED_TELEMETRY) {
+ this._saveAbortedSessionPing(payload)
+ .catch(e => this._log.error("_sendDailyPing - Failed to save the aborted session ping", e));
+ }
+
+ return promise;
+ },
+
+ /** Loads session data from the session data file.
+ * @return {Promise<object>} A promise which is resolved with an object when
+ * loading has completed, with null otherwise.
+ */
+ _loadSessionData: Task.async(function* () {
+ let data = yield TelemetryStorage.loadSessionData();
+
+ if (!data) {
+ return null;
+ }
+
+ if (!("profileSubsessionCounter" in data) ||
+ !(typeof(data.profileSubsessionCounter) == "number") ||
+ !("subsessionId" in data) || !("sessionId" in data)) {
+ this._log.error("_loadSessionData - session data is invalid");
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").add(1);
+ return null;
+ }
+
+ this._previousSessionId = data.sessionId;
+ this._previousSubsessionId = data.subsessionId;
+ // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for
+ // new subsession while loading still takes place. This will always be exactly
+ // 1 - the current subsessions.
+ this._profileSubsessionCounter = data.profileSubsessionCounter +
+ this._subsessionCounter;
+ return data;
+ }),
+
+ /**
+ * Get the session data object to serialise to disk.
+ */
+ _getSessionDataObject: function() {
+ return {
+ sessionId: this._sessionId,
+ subsessionId: this._subsessionId,
+ profileSubsessionCounter: this._profileSubsessionCounter,
+ };
+ },
+
+ _onEnvironmentChange: function(reason, oldEnvironment) {
+ this._log.trace("_onEnvironmentChange", reason);
+
+ let now = Policy.monotonicNow();
+ let timeDelta = now - this._lastEnvironmentChangeDate;
+ if (timeDelta <= CHANGE_THROTTLE_INTERVAL_MS) {
+ this._log.trace(`_onEnvironmentChange - throttling; last change was ${Math.round(timeDelta / 1000)}s ago.`);
+ return;
+ }
+
+ this._lastEnvironmentChangeDate = now;
+ let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
+ TelemetryScheduler.reschedulePings(REASON_ENVIRONMENT_CHANGE, payload);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ overrideEnvironment: oldEnvironment,
+ };
+ TelemetryController.submitExternalPing(getPingType(payload), payload, options);
+ },
+
+ _isClassicReason: function(reason) {
+ const classicReasons = [
+ REASON_SAVED_SESSION,
+ REASON_GATHER_PAYLOAD,
+ REASON_TEST_PING,
+ ];
+ return classicReasons.includes(reason);
+ },
+
+ /**
+ * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+ */
+ _getState: function() {
+ return {
+ initialized: this._initialized,
+ initStarted: this._initStarted,
+ haveDelayedInitTask: !!this._delayedInitTask,
+ };
+ },
+
+ /**
+ * Saves the aborted session ping to disk.
+ * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
+ * session ping. The reason of this payload is changed to aborted-session.
+ * If not provided, a new payload is gathered.
+ */
+ _saveAbortedSessionPing: function(aProvidedPayload = null) {
+ this._log.trace("_saveAbortedSessionPing");
+
+ let payload = null;
+ if (aProvidedPayload) {
+ payload = Cu.cloneInto(aProvidedPayload, myScope);
+ // Overwrite the original reason.
+ payload.info.reason = REASON_ABORTED_SESSION;
+ } else {
+ payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
+ }
+
+ return TelemetryController.saveAbortedSessionPing(payload);
+ },
+};
diff --git a/toolkit/components/telemetry/TelemetryStartup.js b/toolkit/components/telemetry/TelemetryStartup.js
new file mode 100644
index 0000000000..28041b36b5
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryStartup.js
@@ -0,0 +1,49 @@
+/* -*- js-indent-level: 2; 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/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController",
+ "resource://gre/modules/TelemetryController.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
+ "resource://gre/modules/TelemetryEnvironment.jsm");
+
+/**
+ * TelemetryStartup is needed to forward the "profile-after-change" notification
+ * to TelemetryController.jsm.
+ */
+function TelemetryStartup() {
+}
+
+TelemetryStartup.prototype.classID = Components.ID("{117b219f-92fe-4bd2-a21b-95a342a9d474}");
+TelemetryStartup.prototype.QueryInterface = XPCOMUtils.generateQI([Components.interfaces.nsIObserver]);
+TelemetryStartup.prototype.observe = function(aSubject, aTopic, aData) {
+ if (aTopic == "profile-after-change" || aTopic == "app-startup") {
+ TelemetryController.observe(null, aTopic, null);
+ }
+ if (aTopic == "profile-after-change") {
+ annotateEnvironment();
+ TelemetryEnvironment.registerChangeListener("CrashAnnotator", annotateEnvironment);
+ TelemetryEnvironment.onInitialized().then(() => annotateEnvironment());
+ }
+}
+
+function annotateEnvironment() {
+ try {
+ let cr = Cc["@mozilla.org/toolkit/crash-reporter;1"];
+ if (cr) {
+ let env = JSON.stringify(TelemetryEnvironment.currentEnvironment);
+ cr.getService(Ci.nsICrashReporter).annotateCrashReport("TelemetryEnvironment", env);
+ }
+ } catch (e) {
+ // crash reporting not built or disabled? Ignore errors
+ }
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TelemetryStartup]);
diff --git a/toolkit/components/telemetry/TelemetryStartup.manifest b/toolkit/components/telemetry/TelemetryStartup.manifest
new file mode 100644
index 0000000000..f1638530b5
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryStartup.manifest
@@ -0,0 +1,4 @@
+component {117b219f-92fe-4bd2-a21b-95a342a9d474} TelemetryStartup.js
+contract @mozilla.org/base/telemetry-startup;1 {117b219f-92fe-4bd2-a21b-95a342a9d474}
+category profile-after-change TelemetryStartup @mozilla.org/base/telemetry-startup;1 process=main
+category app-startup TelemetryStartup @mozilla.org/base/telemetry-startup;1 process=content
diff --git a/toolkit/components/telemetry/TelemetryStopwatch.jsm b/toolkit/components/telemetry/TelemetryStopwatch.jsm
new file mode 100644
index 0000000000..ab6c6eafbb
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryStopwatch.jsm
@@ -0,0 +1,335 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["TelemetryStopwatch"];
+
+Cu.import("resource://gre/modules/Log.jsm", this);
+var Telemetry = Cc["@mozilla.org/base/telemetry;1"]
+ .getService(Ci.nsITelemetry);
+
+// Weak map does not allow using null objects as keys. These objects are used
+// as 'null' placeholders.
+const NULL_OBJECT = {};
+const NULL_KEY = {};
+
+/**
+ * Timers is a variation of a Map used for storing information about running
+ * Stopwatches. Timers has the following data structure:
+ *
+ * {
+ * "HISTOGRAM_NAME": WeakMap {
+ * Object || NULL_OBJECT: Map {
+ * "KEY" || NULL_KEY: startTime
+ * ...
+ * }
+ * ...
+ * }
+ * ...
+ * }
+ *
+ *
+ * @example
+ * // Stores current time for a keyed histogram "PLAYING_WITH_CUTE_ANIMALS".
+ * Timers.put("PLAYING_WITH_CUTE_ANIMALS", null, "CATS", Date.now());
+ *
+ * @example
+ * // Returns information about a simple Stopwatch.
+ * let startTime = Timers.get("PLAYING_WITH_CUTE_ANIMALS", null, "CATS");
+ */
+let Timers = {
+ _timers: new Map(),
+
+ _validTypes: function(histogram, obj, key) {
+ let nonEmptyString = value => {
+ return typeof value === "string" && value !== "" && value.length > 0;
+ };
+ return nonEmptyString(histogram) &&
+ typeof obj == "object" &&
+ (key === NULL_KEY || nonEmptyString(key));
+ },
+
+ get: function(histogram, obj, key) {
+ key = key === null ? NULL_KEY : key;
+ obj = obj || NULL_OBJECT;
+
+ if (!this.has(histogram, obj, key)) {
+ return null;
+ }
+
+ return this._timers.get(histogram).get(obj).get(key);
+ },
+
+ put: function(histogram, obj, key, startTime) {
+ key = key === null ? NULL_KEY : key;
+ obj = obj || NULL_OBJECT;
+
+ if (!this._validTypes(histogram, obj, key)) {
+ return false;
+ }
+
+ let objectMap = this._timers.get(histogram) || new WeakMap();
+ let keyedInfo = objectMap.get(obj) || new Map();
+ keyedInfo.set(key, startTime);
+ objectMap.set(obj, keyedInfo);
+ this._timers.set(histogram, objectMap);
+ return true;
+ },
+
+ has: function(histogram, obj, key) {
+ key = key === null ? NULL_KEY : key;
+ obj = obj || NULL_OBJECT;
+
+ return this._timers.has(histogram) &&
+ this._timers.get(histogram).has(obj) &&
+ this._timers.get(histogram).get(obj).has(key);
+ },
+
+ delete: function(histogram, obj, key) {
+ key = key === null ? NULL_KEY : key;
+ obj = obj || NULL_OBJECT;
+
+ if (!this.has(histogram, obj, key)) {
+ return false;
+ }
+ let objectMap = this._timers.get(histogram);
+ let keyedInfo = objectMap.get(obj);
+ if (keyedInfo.size > 1) {
+ keyedInfo.delete(key);
+ return true;
+ }
+ objectMap.delete(obj);
+ // NOTE:
+ // We never delete empty objecMaps from this._timers because there is no
+ // nice solution for tracking the number of objects in a WeakMap.
+ // WeakMap is not enumerable, so we can't deterministically say when it's
+ // empty. We accept that trade-off here, given that entries for short-lived
+ // objects will go away when they are no longer referenced
+ return true;
+ }
+};
+
+this.TelemetryStopwatch = {
+ /**
+ * Starts a timer associated with a telemetry histogram. The timer can be
+ * directly associated with a histogram, or with a pair of a histogram and
+ * an object.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {Object} aObj - Optional parameter. If specified, the timer is
+ * associated with this object, meaning that multiple
+ * timers for the same histogram may be run
+ * concurrently, as long as they are associated with
+ * different objects.
+ *
+ * @returns {Boolean} True if the timer was successfully started, false
+ * otherwise. If a timer already exists, it can't be
+ * started again, and the existing one will be cleared in
+ * order to avoid measurements errors.
+ */
+ start: function(aHistogram, aObj) {
+ return TelemetryStopwatchImpl.start(aHistogram, aObj, null);
+ },
+
+ /**
+ * Deletes the timer associated with a telemetry histogram. The timer can be
+ * directly associated with a histogram, or with a pair of a histogram and
+ * an object. Important: Only use this method when a legitimate cancellation
+ * should be done.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {Object} aObj - Optional parameter. If specified, the timer is
+ * associated with this object, meaning that multiple
+ * timers or a same histogram may be run concurrently,
+ * as long as they are associated with different
+ * objects.
+ *
+ * @returns {Boolean} True if the timer exist and it was cleared, False
+ * otherwise.
+ */
+ cancel: function(aHistogram, aObj) {
+ return TelemetryStopwatchImpl.cancel(aHistogram, aObj, null);
+ },
+
+ /**
+ * Returns the elapsed time for a particular stopwatch. Primarily for
+ * debugging purposes. Must be called prior to finish.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ * If an invalid name is given, the function will
+ * throw.
+ *
+ * @param (Object) aObj - Optional parameter which associates the histogram
+ * timer with the given object.
+ *
+ * @returns {Integer} time in milliseconds or -1 if the stopwatch was not
+ * found.
+ */
+ timeElapsed: function(aHistogram, aObj) {
+ return TelemetryStopwatchImpl.timeElapsed(aHistogram, aObj, null);
+ },
+
+ /**
+ * Stops the timer associated with the given histogram (and object),
+ * calculates the time delta between start and finish, and adds the value
+ * to the histogram.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {Object} aObj - Optional parameter which associates the histogram
+ * timer with the given object.
+ *
+ * @returns {Boolean} True if the timer was succesfully stopped and the data
+ * was added to the histogram, False otherwise.
+ */
+ finish: function(aHistogram, aObj) {
+ return TelemetryStopwatchImpl.finish(aHistogram, aObj, null);
+ },
+
+ /**
+ * Starts a timer associated with a keyed telemetry histogram. The timer can
+ * be directly associated with a histogram and its key. Similarly to
+ * @see{TelemetryStopwatch.stat} the histogram and its key can be associated
+ * with an object. Each key may have multiple associated objects and each
+ * object can be associated with multiple keys.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {String} aKey - a string which must be a valid histgram key.
+ *
+ * @param {Object} aObj - Optional parameter. If specified, the timer is
+ * associated with this object, meaning that multiple
+ * timers for the same histogram may be run
+ * concurrently,as long as they are associated with
+ * different objects.
+ *
+ * @returns {Boolean} True if the timer was successfully started, false
+ * otherwise. If a timer already exists, it can't be
+ * started again, and the existing one will be cleared in
+ * order to avoid measurements errors.
+ */
+ startKeyed: function(aHistogram, aKey, aObj) {
+ return TelemetryStopwatchImpl.start(aHistogram, aObj, aKey);
+ },
+
+ /**
+ * Deletes the timer associated with a keyed histogram. Important: Only use
+ * this method when a legitimate cancellation should be done.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {String} aKey - a string which must be a valid histgram key.
+ *
+ * @param {Object} aObj - Optional parameter. If specified, the timer
+ * associated with this object is deleted.
+ *
+ * @return {Boolean} True if the timer exist and it was cleared, False
+ * otherwise.
+ */
+ cancelKeyed: function(aHistogram, aKey, aObj) {
+ return TelemetryStopwatchImpl.cancel(aHistogram, aObj, aKey);
+ },
+
+ /**
+ * Returns the elapsed time for a particular stopwatch. Primarily for
+ * debugging purposes. Must be called prior to finish.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {String} aKey - a string which must be a valid histgram key.
+ *
+ * @param {Object} aObj - Optional parameter. If specified, the timer
+ * associated with this object is used to calculate
+ * the elapsed time.
+ *
+ * @return {Integer} time in milliseconds or -1 if the stopwatch was not
+ * found.
+ */
+ timeElapsedKeyed: function(aHistogram, aKey, aObj) {
+ return TelemetryStopwatchImpl.timeElapsed(aHistogram, aObj, aKey);
+ },
+
+ /**
+ * Stops the timer associated with the given keyed histogram (and object),
+ * calculates the time delta between start and finish, and adds the value
+ * to the keyed histogram.
+ *
+ * @param {String} aHistogram - a string which must be a valid histogram name.
+ *
+ * @param {String} aKey - a string which must be a valid histgram key.
+ *
+ * @param {Object} aObj - optional parameter which associates the histogram
+ * timer with the given object.
+ *
+ * @returns {Boolean} True if the timer was succesfully stopped and the data
+ * was added to the histogram, False otherwise.
+ */
+ finishKeyed: function(aHistogram, aKey, aObj) {
+ return TelemetryStopwatchImpl.finish(aHistogram, aObj, aKey);
+ }
+};
+
+this.TelemetryStopwatchImpl = {
+ start: function(histogram, object, key) {
+ if (Timers.has(histogram, object, key)) {
+ Timers.delete(histogram, object, key);
+ Cu.reportError(`TelemetryStopwatch: key "${histogram}" was already ` +
+ "initialized");
+ return false;
+ }
+
+ return Timers.put(histogram, object, key, Components.utils.now());
+ },
+
+ cancel: function (histogram, object, key) {
+ return Timers.delete(histogram, object, key);
+ },
+
+ timeElapsed: function(histogram, object, key) {
+ let startTime = Timers.get(histogram, object, key);
+ if (startTime === null) {
+ Cu.reportError("TelemetryStopwatch: requesting elapsed time for " +
+ `nonexisting stopwatch. Histogram: "${histogram}", ` +
+ `key: "${key}"`);
+ return -1;
+ }
+
+ try {
+ let delta = Components.utils.now() - startTime
+ return Math.round(delta);
+ } catch (e) {
+ Cu.reportError("TelemetryStopwatch: failed to calculate elapsed time " +
+ `for Histogram: "${histogram}", key: "${key}", ` +
+ `exception: ${Log.exceptionStr(e)}`);
+ return -1;
+ }
+ },
+
+ finish: function(histogram, object, key) {
+ let delta = this.timeElapsed(histogram, object, key);
+ if (delta == -1) {
+ return false;
+ }
+
+ try {
+ if (key) {
+ Telemetry.getKeyedHistogramById(histogram).add(key, delta);
+ } else {
+ Telemetry.getHistogramById(histogram).add(delta);
+ }
+ } catch (e) {
+ Cu.reportError("TelemetryStopwatch: failed to update the Histogram " +
+ `"${histogram}", using key: "${key}", ` +
+ `exception: ${Log.exceptionStr(e)}`);
+ return false;
+ }
+
+ return Timers.delete(histogram, object, key);
+ }
+}
diff --git a/toolkit/components/telemetry/TelemetryStorage.jsm b/toolkit/components/telemetry/TelemetryStorage.jsm
new file mode 100644
index 0000000000..91cfc993dd
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -0,0 +1,1882 @@
+/* -*- js-indent-level: 2; 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["TelemetryStorage"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm", this);
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryStorage::";
+
+const Telemetry = Services.telemetry;
+const Utils = TelemetryUtils;
+
+// Compute the path of the pings archive on the first use.
+const DATAREPORTING_DIR = "datareporting";
+const PINGS_ARCHIVE_DIR = "archived";
+const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
+const DELETION_PING_FILE_NAME = "pending-deletion-ping";
+const SESSION_STATE_FILE_NAME = "session-state.json";
+
+XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
+});
+XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
+ return OS.Path.join(gDataReportingDir, PINGS_ARCHIVE_DIR);
+});
+XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() {
+ return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME);
+});
+XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() {
+ return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME);
+});
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
+ "resource://services-common/utils.js");
+// Maxmimum time, in milliseconds, archive pings should be retained.
+const MAX_ARCHIVED_PINGS_RETENTION_MS = 60 * 24 * 60 * 60 * 1000; // 60 days
+
+// Maximum space the archive can take on disk (in Bytes).
+const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB
+// Maximum space the outgoing pings can take on disk, for Desktop (in Bytes).
+const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB
+// Maximum space the outgoing pings can take on disk, for Mobile (in Bytes).
+const PENDING_PINGS_QUOTA_BYTES_MOBILE = 1024 * 1024; // 1 MB
+
+// The maximum size a pending/archived ping can take on disk.
+const PING_FILE_MAXIMUM_SIZE_BYTES = 1024 * 1024; // 1 MB
+
+// This special value is submitted when the archive is outside of the quota.
+const ARCHIVE_SIZE_PROBE_SPECIAL_VALUE = 300;
+
+// This special value is submitted when the pending pings is outside of the quota, as
+// we don't know the size of the pings above the quota.
+const PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE = 17;
+
+const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+/**
+ * This is thrown by |TelemetryStorage.loadPingFile| when reading the ping
+ * from the disk fails.
+ */
+function PingReadError(message="Error reading the ping file", becauseNoSuchFile = false) {
+ Error.call(this, message);
+ let error = new Error();
+ this.name = "PingReadError";
+ this.message = message;
+ this.stack = error.stack;
+ this.becauseNoSuchFile = becauseNoSuchFile;
+}
+PingReadError.prototype = Object.create(Error.prototype);
+PingReadError.prototype.constructor = PingReadError;
+
+/**
+ * This is thrown by |TelemetryStorage.loadPingFile| when parsing the ping JSON
+ * content fails.
+ */
+function PingParseError(message="Error parsing ping content") {
+ Error.call(this, message);
+ let error = new Error();
+ this.name = "PingParseError";
+ this.message = message;
+ this.stack = error.stack;
+}
+PingParseError.prototype = Object.create(Error.prototype);
+PingParseError.prototype.constructor = PingParseError;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+ now: () => new Date(),
+ getArchiveQuota: () => ARCHIVE_QUOTA_BYTES,
+ getPendingPingsQuota: () => (AppConstants.platform in ["android", "gonk"])
+ ? PENDING_PINGS_QUOTA_BYTES_MOBILE
+ : PENDING_PINGS_QUOTA_BYTES_DESKTOP,
+};
+
+/**
+ * Wait for all promises in iterable to resolve or reject. This function
+ * always resolves its promise with undefined, and never rejects.
+ */
+function waitForAll(it) {
+ let dummy = () => {};
+ let promises = Array.from(it, p => p.catch(dummy));
+ return Promise.all(promises);
+}
+
+/**
+ * Permanently intern the given string. This is mainly used for the ping.type
+ * strings that can be excessively duplicated in the _archivedPings map. Do not
+ * pass large or temporary strings to this function.
+ */
+function internString(str) {
+ return Symbol.keyFor(Symbol.for(str));
+}
+
+this.TelemetryStorage = {
+ get pingDirectoryPath() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");
+ },
+
+ /**
+ * The maximum size a ping can have, in bytes.
+ */
+ get MAXIMUM_PING_SIZE() {
+ return PING_FILE_MAXIMUM_SIZE_BYTES;
+ },
+ /**
+ * Shutdown & block on any outstanding async activity in this module.
+ *
+ * @return {Promise} Promise that is resolved when shutdown is complete.
+ */
+ shutdown: function() {
+ return TelemetryStorageImpl.shutdown();
+ },
+
+ /**
+ * Save an archived ping to disk.
+ *
+ * @param {object} ping The ping data to archive.
+ * @return {promise} Promise that is resolved when the ping is successfully archived.
+ */
+ saveArchivedPing: function(ping) {
+ return TelemetryStorageImpl.saveArchivedPing(ping);
+ },
+
+ /**
+ * Load an archived ping from disk.
+ *
+ * @param {string} id The pings id.
+ * @return {promise<object>} Promise that is resolved with the ping data.
+ */
+ loadArchivedPing: function(id) {
+ return TelemetryStorageImpl.loadArchivedPing(id);
+ },
+
+ /**
+ * Get a list of info on the archived pings.
+ * This will scan the archive directory and grab basic data about the existing
+ * pings out of their filename.
+ *
+ * @return {promise<sequence<object>>}
+ */
+ loadArchivedPingList: function() {
+ return TelemetryStorageImpl.loadArchivedPingList();
+ },
+
+ /**
+ * Clean the pings archive by removing old pings.
+ * This will scan the archive directory.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runCleanPingArchiveTask: function() {
+ return TelemetryStorageImpl.runCleanPingArchiveTask();
+ },
+
+ /**
+ * Run the task to enforce the pending pings quota.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runEnforcePendingPingsQuotaTask: function() {
+ return TelemetryStorageImpl.runEnforcePendingPingsQuotaTask();
+ },
+
+ /**
+ * Run the task to remove all the pending pings (except the deletion ping).
+ *
+ * @return {Promise} Resolved when the pings are removed.
+ */
+ runRemovePendingPingsTask: function() {
+ return TelemetryStorageImpl.runRemovePendingPingsTask();
+ },
+
+ /**
+ * Reset the storage state in tests.
+ */
+ reset: function() {
+ return TelemetryStorageImpl.reset();
+ },
+
+ /**
+ * Test method that allows waiting on the archive clean task to finish.
+ */
+ testCleanupTaskPromise: function() {
+ return (TelemetryStorageImpl._cleanArchiveTask || Promise.resolve());
+ },
+
+ /**
+ * Test method that allows waiting on the pending pings quota task to finish.
+ */
+ testPendingQuotaTaskPromise: function() {
+ return (TelemetryStorageImpl._enforcePendingPingsQuotaTask || Promise.resolve());
+ },
+
+ /**
+ * Save a pending - outgoing - ping to disk and track it.
+ *
+ * @param {Object} ping The ping data.
+ * @return {Promise} Resolved when the ping was saved.
+ */
+ savePendingPing: function(ping) {
+ return TelemetryStorageImpl.savePendingPing(ping);
+ },
+
+ /**
+ * Saves session data to disk.
+ * @param {Object} sessionData The session data.
+ * @return {Promise} Resolved when the data was saved.
+ */
+ saveSessionData: function(sessionData) {
+ return TelemetryStorageImpl.saveSessionData(sessionData);
+ },
+
+ /**
+ * Loads session data from a session data file.
+ * @return {Promise<object>} Resolved with the session data in object form.
+ */
+ loadSessionData: function() {
+ return TelemetryStorageImpl.loadSessionData();
+ },
+
+ /**
+ * Load a pending ping from disk by id.
+ *
+ * @param {String} id The pings id.
+ * @return {Promise} Resolved with the loaded ping data.
+ */
+ loadPendingPing: function(id) {
+ return TelemetryStorageImpl.loadPendingPing(id);
+ },
+
+ /**
+ * Remove a pending ping from disk by id.
+ *
+ * @param {String} id The pings id.
+ * @return {Promise} Resolved when the ping was removed.
+ */
+ removePendingPing: function(id) {
+ return TelemetryStorageImpl.removePendingPing(id);
+ },
+
+ /**
+ * Returns a list of the currently pending pings in the format:
+ * {
+ * id: <string>, // The pings UUID.
+ * lastModificationDate: <number>, // Timestamp of the pings last modification.
+ * }
+ * This populates the list by scanning the disk.
+ *
+ * @return {Promise<sequence>} Resolved with the ping list.
+ */
+ loadPendingPingList: function() {
+ return TelemetryStorageImpl.loadPendingPingList();
+ },
+
+ /**
+ * Returns a list of the currently pending pings in the format:
+ * {
+ * id: <string>, // The pings UUID.
+ * lastModificationDate: <number>, // Timestamp of the pings last modification.
+ * }
+ * This does not scan pending pings on disk.
+ *
+ * @return {sequence} The current pending ping list.
+ */
+ getPendingPingList: function() {
+ return TelemetryStorageImpl.getPendingPingList();
+ },
+
+ /**
+ * Save an aborted-session ping to disk. This goes to a special location so
+ * it is not picked up as a pending ping.
+ *
+ * @param {object} ping The ping data to save.
+ * @return {promise} Promise that is resolved when the ping is successfully saved.
+ */
+ saveAbortedSessionPing: function(ping) {
+ return TelemetryStorageImpl.saveAbortedSessionPing(ping);
+ },
+
+ /**
+ * Load the aborted-session ping from disk if present.
+ *
+ * @return {promise<object>} Promise that is resolved with the ping data if found.
+ * Otherwise returns null.
+ */
+ loadAbortedSessionPing: function() {
+ return TelemetryStorageImpl.loadAbortedSessionPing();
+ },
+
+ /**
+ * Save the deletion ping.
+ * @param ping The deletion ping.
+ * @return {Promise} A promise resolved when the ping is saved.
+ */
+ saveDeletionPing: function(ping) {
+ return TelemetryStorageImpl.saveDeletionPing(ping);
+ },
+
+ /**
+ * Remove the deletion ping.
+ * @return {Promise} Resolved when the ping is deleted from the disk.
+ */
+ removeDeletionPing: function() {
+ return TelemetryStorageImpl.removeDeletionPing();
+ },
+
+ /**
+ * Check if the ping id identifies a deletion ping.
+ */
+ isDeletionPing: function(aPingId) {
+ return TelemetryStorageImpl.isDeletionPing(aPingId);
+ },
+
+ /**
+ * Remove the aborted-session ping if present.
+ *
+ * @return {promise} Promise that is resolved once the ping is removed.
+ */
+ removeAbortedSessionPing: function() {
+ return TelemetryStorageImpl.removeAbortedSessionPing();
+ },
+
+ /**
+ * Save a single ping to a file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {string} file The destination file.
+ * @param {bool} overwrite If |true|, the file will be overwritten if it exists,
+ * if |false| the file will not be overwritten and no error will be reported if
+ * the file exists.
+ * @returns {promise}
+ */
+ savePingToFile: function(ping, file, overwrite) {
+ return TelemetryStorageImpl.savePingToFile(ping, file, overwrite);
+ },
+
+ /**
+ * Save a ping to its file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {bool} overwrite If |true|, the file will be overwritten
+ * if it exists.
+ * @returns {promise}
+ */
+ savePing: function(ping, overwrite) {
+ return TelemetryStorageImpl.savePing(ping, overwrite);
+ },
+
+ /**
+ * Add a ping to the saved pings directory so that it gets saved
+ * and sent along with other pings.
+ *
+ * @param {Object} pingData The ping object.
+ * @return {Promise} A promise resolved when the ping is saved to the pings directory.
+ */
+ addPendingPing: function(pingData) {
+ return TelemetryStorageImpl.addPendingPing(pingData);
+ },
+
+ /**
+ * Remove the file for a ping
+ *
+ * @param {object} ping The ping.
+ * @returns {promise}
+ */
+ cleanupPingFile: function(ping) {
+ return TelemetryStorageImpl.cleanupPingFile(ping);
+ },
+
+ /**
+ * The number of pending pings on disk.
+ */
+ get pendingPingCount() {
+ return TelemetryStorageImpl.pendingPingCount;
+ },
+
+ /**
+ * Loads a ping file.
+ * @param {String} aFilePath The path of the ping file.
+ * @return {Promise<Object>} A promise resolved with the ping content or rejected if the
+ * ping contains invalid data.
+ */
+ loadPingFile: Task.async(function* (aFilePath) {
+ return TelemetryStorageImpl.loadPingFile(aFilePath);
+ }),
+
+ /**
+ * Remove FHR database files. This is temporary and will be dropped in
+ * the future.
+ * @return {Promise} Resolved when the database files are deleted.
+ */
+ removeFHRDatabase: function() {
+ return TelemetryStorageImpl.removeFHRDatabase();
+ },
+
+ /**
+ * Only used in tests, builds an archived ping path from the ping metadata.
+ * @param {String} aPingId The ping id.
+ * @param {Object} aDate The ping creation date.
+ * @param {String} aType The ping type.
+ * @return {String} The full path to the archived ping.
+ */
+ _testGetArchivedPingPath: function(aPingId, aDate, aType) {
+ return getArchivedPingPath(aPingId, aDate, aType);
+ },
+
+ /**
+ * Only used in tests, this helper extracts ping metadata from a given filename.
+ *
+ * @param fileName {String} The filename.
+ * @return {Object} Null if the filename didn't match the expected form.
+ * Otherwise an object with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string> }
+ */
+ _testGetArchivedPingDataFromFileName: function(aFileName) {
+ return TelemetryStorageImpl._getArchivedPingDataFromFileName(aFileName);
+ },
+
+ /**
+ * Only used in tests, this helper allows cleaning up the pending ping storage.
+ */
+ testClearPendingPings: function() {
+ return TelemetryStorageImpl.runRemovePendingPingsTask();
+ }
+};
+
+/**
+ * This object allows the serialisation of asynchronous tasks. This is particularly
+ * useful to serialise write access to the disk in order to prevent race conditions
+ * to corrupt the data being written.
+ * We are using this to synchronize saving to the file that TelemetrySession persists
+ * its state in.
+ */
+function SaveSerializer() {
+ this._queuedOperations = [];
+ this._queuedInProgress = false;
+ this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+}
+
+SaveSerializer.prototype = {
+ /**
+ * Enqueues an operation to a list to serialise their execution in order to prevent race
+ * conditions. Useful to serialise access to disk.
+ *
+ * @param {Function} aFunction The task function to enqueue. It must return a promise.
+ * @return {Promise} A promise resolved when the enqueued task completes.
+ */
+ enqueueTask: function (aFunction) {
+ let promise = new Promise((resolve, reject) =>
+ this._queuedOperations.push([aFunction, resolve, reject]));
+
+ if (this._queuedOperations.length == 1) {
+ this._popAndPerformQueuedOperation();
+ }
+ return promise;
+ },
+
+ /**
+ * Make sure to flush all the pending operations.
+ * @return {Promise} A promise resolved when all the pending operations have completed.
+ */
+ flushTasks: function () {
+ let dummyTask = () => new Promise(resolve => resolve());
+ return this.enqueueTask(dummyTask);
+ },
+
+ /**
+ * Pop a task from the queue, executes it and continue to the next one.
+ * This function recursively pops all the tasks.
+ */
+ _popAndPerformQueuedOperation: function () {
+ if (!this._queuedOperations.length || this._queuedInProgress) {
+ return;
+ }
+
+ this._log.trace("_popAndPerformQueuedOperation - Performing queued operation.");
+ let [func, resolve, reject] = this._queuedOperations.shift();
+ let promise;
+
+ try {
+ this._queuedInProgress = true;
+ promise = func();
+ } catch (ex) {
+ this._log.warn("_popAndPerformQueuedOperation - Queued operation threw during execution. ",
+ ex);
+ this._queuedInProgress = false;
+ reject(ex);
+ this._popAndPerformQueuedOperation();
+ return;
+ }
+
+ if (!promise || typeof(promise.then) != "function") {
+ let msg = "Queued operation did not return a promise: " + func;
+ this._log.warn("_popAndPerformQueuedOperation - " + msg);
+
+ this._queuedInProgress = false;
+ reject(new Error(msg));
+ this._popAndPerformQueuedOperation();
+ return;
+ }
+
+ promise.then(result => {
+ this._queuedInProgress = false;
+ resolve(result);
+ this._popAndPerformQueuedOperation();
+ },
+ error => {
+ this._log.warn("_popAndPerformQueuedOperation - Failure when performing queued operation.",
+ error);
+ this._queuedInProgress = false;
+ reject(error);
+ this._popAndPerformQueuedOperation();
+ });
+ },
+};
+
+var TelemetryStorageImpl = {
+ _logger: null,
+ // Used to serialize aborted session ping writes to disk.
+ _abortedSessionSerializer: new SaveSerializer(),
+ // Used to serialize deletion ping writes to disk.
+ _deletionPingSerializer: new SaveSerializer(),
+ // Used to serialize session state writes to disk.
+ _stateSaveSerializer: new SaveSerializer(),
+
+ // Tracks the archived pings in a Map of (id -> {timestampCreated, type}).
+ // We use this to cache info on archived pings to avoid scanning the disk more than once.
+ _archivedPings: new Map(),
+ // A set of promises for pings currently being archived
+ _activelyArchiving: new Set(),
+ // Track the archive loading task to prevent multiple tasks from being executed.
+ _scanArchiveTask: null,
+ // Track the archive cleanup task.
+ _cleanArchiveTask: null,
+ // Whether we already scanned the archived pings on disk.
+ _scannedArchiveDirectory: false,
+
+ // Track the pending ping removal task.
+ _removePendingPingsTask: null,
+
+ // This tracks all the pending async ping save activity.
+ _activePendingPingSaves: new Set(),
+
+ // Tracks the pending pings in a Map of (id -> {timestampCreated, type}).
+ // We use this to cache info on pending pings to avoid scanning the disk more than once.
+ _pendingPings: new Map(),
+
+ // Track the pending pings enforce quota task.
+ _enforcePendingPingsQuotaTask: null,
+
+ // Track the shutdown process to bail out of the clean up task quickly.
+ _shutdown: false,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Shutdown & block on any outstanding async activity in this module.
+ *
+ * @return {Promise} Promise that is resolved when shutdown is complete.
+ */
+ shutdown: Task.async(function*() {
+ this._shutdown = true;
+
+ // If the following tasks are still running, block on them. They will bail out as soon
+ // as possible.
+ yield this._abortedSessionSerializer.flushTasks().catch(ex => {
+ this._log.error("shutdown - failed to flush aborted-session writes", ex);
+ });
+
+ yield this._deletionPingSerializer.flushTasks().catch(ex => {
+ this._log.error("shutdown - failed to flush deletion ping writes", ex);
+ });
+
+ if (this._cleanArchiveTask) {
+ yield this._cleanArchiveTask.catch(ex => {
+ this._log.error("shutdown - the archive cleaning task failed", ex);
+ });
+ }
+
+ if (this._enforcePendingPingsQuotaTask) {
+ yield this._enforcePendingPingsQuotaTask.catch(ex => {
+ this._log.error("shutdown - the pending pings quota task failed", ex);
+ });
+ }
+
+ if (this._removePendingPingsTask) {
+ yield this._removePendingPingsTask.catch(ex => {
+ this._log.error("shutdown - the pending pings removal task failed", ex);
+ });
+ }
+
+ // Wait on pending pings still being saved. While OS.File should have shutdown
+ // blockers in place, we a) have seen weird errors being reported that might
+ // indicate a bad shutdown path and b) might have completion handlers hanging
+ // off the save operations that don't expect to be late in shutdown.
+ yield this.promisePendingPingSaves();
+ }),
+
+ /**
+ * Save an archived ping to disk.
+ *
+ * @param {object} ping The ping data to archive.
+ * @return {promise} Promise that is resolved when the ping is successfully archived.
+ */
+ saveArchivedPing: function(ping) {
+ let promise = this._saveArchivedPingTask(ping);
+ this._activelyArchiving.add(promise);
+ promise.then((r) => { this._activelyArchiving.delete(promise); },
+ (e) => { this._activelyArchiving.delete(promise); });
+ return promise;
+ },
+
+ _saveArchivedPingTask: Task.async(function*(ping) {
+ const creationDate = new Date(ping.creationDate);
+ if (this._archivedPings.has(ping.id)) {
+ const data = this._archivedPings.get(ping.id);
+ if (data.timestampCreated > creationDate.getTime()) {
+ this._log.error("saveArchivedPing - trying to overwrite newer ping with the same id");
+ return Promise.reject(new Error("trying to overwrite newer ping with the same id"));
+ }
+ this._log.warn("saveArchivedPing - overwriting older ping with the same id");
+ }
+
+ // Get the archived ping path and append the lz4 suffix to it (so we have 'jsonlz4').
+ const filePath = getArchivedPingPath(ping.id, creationDate, ping.type) + "lz4";
+ yield OS.File.makeDir(OS.Path.dirname(filePath), { ignoreExisting: true,
+ from: OS.Constants.Path.profileDir });
+ yield this.savePingToFile(ping, filePath, /* overwrite*/ true, /* compressed*/ true);
+
+ this._archivedPings.set(ping.id, {
+ timestampCreated: creationDate.getTime(),
+ type: internString(ping.type),
+ });
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").add();
+ return undefined;
+ }),
+
+ /**
+ * Load an archived ping from disk.
+ *
+ * @param {string} id The pings id.
+ * @return {promise<object>} Promise that is resolved with the ping data.
+ */
+ loadArchivedPing: Task.async(function*(id) {
+ this._log.trace("loadArchivedPing - id: " + id);
+
+ const data = this._archivedPings.get(id);
+ if (!data) {
+ this._log.trace("loadArchivedPing - no ping with id: " + id);
+ return Promise.reject(new Error("TelemetryStorage.loadArchivedPing - no ping with id " + id));
+ }
+
+ const path = getArchivedPingPath(id, new Date(data.timestampCreated), data.type);
+ const pathCompressed = path + "lz4";
+
+ // Purge pings which are too big.
+ let checkSize = function*(path) {
+ const fileSize = (yield OS.File.stat(path)).size;
+ if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB")
+ .add(Math.floor(fileSize / 1024 / 1024));
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").add();
+ yield OS.File.remove(path, {ignoreAbsent: true});
+ throw new Error("loadArchivedPing - exceeded the maximum ping size: " + fileSize);
+ }
+ };
+
+ try {
+ // Try to load a compressed version of the archived ping first.
+ this._log.trace("loadArchivedPing - loading ping from: " + pathCompressed);
+ yield* checkSize(pathCompressed);
+ return yield this.loadPingFile(pathCompressed, /* compressed*/ true);
+ } catch (ex) {
+ if (!ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ // If that fails, look for the uncompressed version.
+ this._log.trace("loadArchivedPing - compressed ping not found, loading: " + path);
+ yield* checkSize(path);
+ return yield this.loadPingFile(path, /* compressed*/ false);
+ }
+ }),
+
+ /**
+ * Saves session data to disk.
+ */
+ saveSessionData: function(sessionData) {
+ return this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));
+ },
+
+ _saveSessionData: Task.async(function* (sessionData) {
+ let dataDir = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
+ yield OS.File.makeDir(dataDir);
+
+ let filePath = OS.Path.join(gDataReportingDir, SESSION_STATE_FILE_NAME);
+ try {
+ yield CommonUtils.writeJSON(sessionData, filePath);
+ } catch (e) {
+ this._log.error("_saveSessionData - Failed to write session data to " + filePath, e);
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_SAVE").add(1);
+ }
+ }),
+
+ /**
+ * Loads session data from the session data file.
+ * @return {Promise<Object>} A promise resolved with an object on success,
+ * with null otherwise.
+ */
+ loadSessionData: function() {
+ return this._stateSaveSerializer.enqueueTask(() => this._loadSessionData());
+ },
+
+ _loadSessionData: Task.async(function* () {
+ const dataFile = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR,
+ SESSION_STATE_FILE_NAME);
+ let content;
+ try {
+ content = yield OS.File.read(dataFile, { encoding: "utf-8" });
+ } catch (ex) {
+ this._log.info("_loadSessionData - can not load session data file", ex);
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_LOAD").add(1);
+ return null;
+ }
+
+ let data;
+ try {
+ data = JSON.parse(content);
+ } catch (ex) {
+ this._log.error("_loadSessionData - failed to parse session data", ex);
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_PARSE").add(1);
+ return null;
+ }
+
+ return data;
+ }),
+
+ /**
+ * Remove an archived ping from disk.
+ *
+ * @param {string} id The pings id.
+ * @param {number} timestampCreated The pings creation timestamp.
+ * @param {string} type The pings type.
+ * @return {promise<object>} Promise that is resolved when the pings is removed.
+ */
+ _removeArchivedPing: Task.async(function*(id, timestampCreated, type) {
+ this._log.trace("_removeArchivedPing - id: " + id + ", timestampCreated: " + timestampCreated + ", type: " + type);
+ const path = getArchivedPingPath(id, new Date(timestampCreated), type);
+ const pathCompressed = path + "lz4";
+
+ this._log.trace("_removeArchivedPing - removing ping from: " + path);
+ yield OS.File.remove(path, {ignoreAbsent: true});
+ yield OS.File.remove(pathCompressed, {ignoreAbsent: true});
+ // Remove the ping from the cache.
+ this._archivedPings.delete(id);
+ }),
+
+ /**
+ * Clean the pings archive by removing old pings.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runCleanPingArchiveTask: function() {
+ // If there's an archive cleaning task already running, return it.
+ if (this._cleanArchiveTask) {
+ return this._cleanArchiveTask;
+ }
+
+ // Make sure to clear |_cleanArchiveTask| once done.
+ let clear = () => this._cleanArchiveTask = null;
+ // Since there's no archive cleaning task running, start it.
+ this._cleanArchiveTask = this._cleanArchive().then(clear, clear);
+ return this._cleanArchiveTask;
+ },
+
+ /**
+ * Removes pings which are too old from the pings archive.
+ * @return {Promise} Resolved when the ping age check is complete.
+ */
+ _purgeOldPings: Task.async(function*() {
+ this._log.trace("_purgeOldPings");
+
+ const nowDate = Policy.now();
+ const startTimeStamp = nowDate.getTime();
+ let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath);
+ let subdirs = (yield dirIterator.nextBatch()).filter(e => e.isDir);
+ dirIterator.close();
+
+ // Keep track of the newest removed month to update the cache, if needed.
+ let newestRemovedMonthTimestamp = null;
+ let evictedDirsCount = 0;
+ let maxDirAgeInMonths = 0;
+
+ // Walk through the monthly subdirs of the form <YYYY-MM>/
+ for (let dir of subdirs) {
+ if (this._shutdown) {
+ this._log.trace("_purgeOldPings - Terminating the clean up task due to shutdown");
+ return;
+ }
+
+ if (!isValidArchiveDir(dir.name)) {
+ this._log.warn("_purgeOldPings - skipping invalidly named subdirectory " + dir.path);
+ continue;
+ }
+
+ const archiveDate = getDateFromArchiveDir(dir.name);
+ if (!archiveDate) {
+ this._log.warn("_purgeOldPings - skipping invalid subdirectory date " + dir.path);
+ continue;
+ }
+
+ // If this archive directory is older than 180 days, remove it.
+ if ((startTimeStamp - archiveDate.getTime()) > MAX_ARCHIVED_PINGS_RETENTION_MS) {
+ try {
+ yield OS.File.removeDir(dir.path);
+ evictedDirsCount++;
+
+ // Update the newest removed month.
+ newestRemovedMonthTimestamp = Math.max(archiveDate, newestRemovedMonthTimestamp);
+ } catch (ex) {
+ this._log.error("_purgeOldPings - Unable to remove " + dir.path, ex);
+ }
+ } else {
+ // We're not removing this directory, so record the age for the oldest directory.
+ const dirAgeInMonths = Utils.getElapsedTimeInMonths(archiveDate, nowDate);
+ maxDirAgeInMonths = Math.max(dirAgeInMonths, maxDirAgeInMonths);
+ }
+ }
+
+ // Trigger scanning of the archived pings.
+ yield this.loadArchivedPingList();
+
+ // Refresh the cache: we could still skip this, but it's cheap enough to keep it
+ // to avoid introducing task dependencies.
+ if (newestRemovedMonthTimestamp) {
+ // Scan the archive cache for pings older than the newest directory pruned above.
+ for (let [id, info] of this._archivedPings) {
+ const timestampCreated = new Date(info.timestampCreated);
+ if (timestampCreated.getTime() > newestRemovedMonthTimestamp) {
+ continue;
+ }
+ // Remove outdated pings from the cache.
+ this._archivedPings.delete(id);
+ }
+ }
+
+ const endTimeStamp = Policy.now().getTime();
+
+ // Save the time it takes to evict old directories and the eviction count.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS")
+ .add(evictedDirsCount);
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_DIRS_MS")
+ .add(Math.ceil(endTimeStamp - startTimeStamp));
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE")
+ .add(maxDirAgeInMonths);
+ }),
+
+ /**
+ * Enforce a disk quota for the pings archive.
+ * @return {Promise} Resolved when the quota check is complete.
+ */
+ _enforceArchiveQuota: Task.async(function*() {
+ this._log.trace("_enforceArchiveQuota");
+ let startTimeStamp = Policy.now().getTime();
+
+ // Build an ordered list, from newer to older, of archived pings.
+ let pingList = Array.from(this._archivedPings, p => ({
+ id: p[0],
+ timestampCreated: p[1].timestampCreated,
+ type: p[1].type,
+ }));
+
+ pingList.sort((a, b) => b.timestampCreated - a.timestampCreated);
+
+ // If our archive is too big, we should reduce it to reach 90% of the quota.
+ const SAFE_QUOTA = Policy.getArchiveQuota() * 0.9;
+ // The index of the last ping to keep. Pings older than this one will be deleted if
+ // the archive exceeds the quota.
+ let lastPingIndexToKeep = null;
+ let archiveSizeInBytes = 0;
+
+ // Find the disk size of the archive.
+ for (let i = 0; i < pingList.length; i++) {
+ if (this._shutdown) {
+ this._log.trace("_enforceArchiveQuota - Terminating the clean up task due to shutdown");
+ return;
+ }
+
+ let ping = pingList[i];
+
+ // Get the size for this ping.
+ const fileSize =
+ yield getArchivedPingSize(ping.id, new Date(ping.timestampCreated), ping.type);
+ if (!fileSize) {
+ this._log.warn("_enforceArchiveQuota - Unable to find the size of ping " + ping.id);
+ continue;
+ }
+
+ // Enforce a maximum file size limit on archived pings.
+ if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ this._log.error("_enforceArchiveQuota - removing file exceeding size limit, size: " + fileSize);
+ // We just remove the ping from the disk, we don't bother removing it from pingList
+ // since it won't contribute to the quota.
+ yield this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type)
+ .catch(e => this._log.error("_enforceArchiveQuota - failed to remove archived ping" + ping.id));
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB")
+ .add(Math.floor(fileSize / 1024 / 1024));
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").add();
+ continue;
+ }
+
+ archiveSizeInBytes += fileSize;
+
+ if (archiveSizeInBytes < SAFE_QUOTA) {
+ // We save the index of the last ping which is ok to keep in order to speed up ping
+ // pruning.
+ lastPingIndexToKeep = i;
+ } else if (archiveSizeInBytes > Policy.getArchiveQuota()) {
+ // Ouch, our ping archive is too big. Bail out and start pruning!
+ break;
+ }
+ }
+
+ // Save the time it takes to check if the archive is over-quota.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS")
+ .add(Math.round(Policy.now().getTime() - startTimeStamp));
+
+ let submitProbes = (sizeInMB, evictedPings, elapsedMs) => {
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").add(sizeInMB);
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").add(evictedPings);
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").add(elapsedMs);
+ };
+
+ // Check if we're using too much space. If not, submit the archive size and bail out.
+ if (archiveSizeInBytes < Policy.getArchiveQuota()) {
+ submitProbes(Math.round(archiveSizeInBytes / 1024 / 1024), 0, 0);
+ return;
+ }
+
+ this._log.info("_enforceArchiveQuota - archive size: " + archiveSizeInBytes + "bytes"
+ + ", safety quota: " + SAFE_QUOTA + "bytes");
+
+ startTimeStamp = Policy.now().getTime();
+ let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1);
+
+ // Remove all the pings older than the last one which we are safe to keep.
+ for (let ping of pingsToPurge) {
+ if (this._shutdown) {
+ this._log.trace("_enforceArchiveQuota - Terminating the clean up task due to shutdown");
+ return;
+ }
+
+ // This list is guaranteed to be in order, so remove the pings at its
+ // beginning (oldest).
+ yield this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type);
+ }
+
+ const endTimeStamp = Policy.now().getTime();
+ submitProbes(ARCHIVE_SIZE_PROBE_SPECIAL_VALUE, pingsToPurge.length,
+ Math.ceil(endTimeStamp - startTimeStamp));
+ }),
+
+ _cleanArchive: Task.async(function*() {
+ this._log.trace("cleanArchiveTask");
+
+ if (!(yield OS.File.exists(gPingsArchivePath))) {
+ return;
+ }
+
+ // Remove pings older than 180 days.
+ try {
+ yield this._purgeOldPings();
+ } catch (ex) {
+ this._log.error("_cleanArchive - There was an error removing old directories", ex);
+ }
+
+ // Make sure we respect the archive disk quota.
+ yield this._enforceArchiveQuota();
+ }),
+
+ /**
+ * Run the task to enforce the pending pings quota.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runEnforcePendingPingsQuotaTask: Task.async(function*() {
+ // If there's a cleaning task already running, return it.
+ if (this._enforcePendingPingsQuotaTask) {
+ return this._enforcePendingPingsQuotaTask;
+ }
+
+ // Since there's no quota enforcing task running, start it.
+ try {
+ this._enforcePendingPingsQuotaTask = this._enforcePendingPingsQuota();
+ yield this._enforcePendingPingsQuotaTask;
+ } finally {
+ this._enforcePendingPingsQuotaTask = null;
+ }
+ return undefined;
+ }),
+
+ /**
+ * Enforce a disk quota for the pending pings.
+ * @return {Promise} Resolved when the quota check is complete.
+ */
+ _enforcePendingPingsQuota: Task.async(function*() {
+ this._log.trace("_enforcePendingPingsQuota");
+ let startTimeStamp = Policy.now().getTime();
+
+ // Build an ordered list, from newer to older, of pending pings.
+ let pingList = Array.from(this._pendingPings, p => ({
+ id: p[0],
+ lastModificationDate: p[1].lastModificationDate,
+ }));
+
+ pingList.sort((a, b) => b.lastModificationDate - a.lastModificationDate);
+
+ // If our pending pings directory is too big, we should reduce it to reach 90% of the quota.
+ const SAFE_QUOTA = Policy.getPendingPingsQuota() * 0.9;
+ // The index of the last ping to keep. Pings older than this one will be deleted if
+ // the pending pings directory size exceeds the quota.
+ let lastPingIndexToKeep = null;
+ let pendingPingsSizeInBytes = 0;
+
+ // Find the disk size of the pending pings directory.
+ for (let i = 0; i < pingList.length; i++) {
+ if (this._shutdown) {
+ this._log.trace("_enforcePendingPingsQuota - Terminating the clean up task due to shutdown");
+ return;
+ }
+
+ let ping = pingList[i];
+
+ // Get the size for this ping.
+ const fileSize = yield getPendingPingSize(ping.id);
+ if (!fileSize) {
+ this._log.warn("_enforcePendingPingsQuota - Unable to find the size of ping " + ping.id);
+ continue;
+ }
+
+ pendingPingsSizeInBytes += fileSize;
+ if (pendingPingsSizeInBytes < SAFE_QUOTA) {
+ // We save the index of the last ping which is ok to keep in order to speed up ping
+ // pruning.
+ lastPingIndexToKeep = i;
+ } else if (pendingPingsSizeInBytes > Policy.getPendingPingsQuota()) {
+ // Ouch, our pending pings directory size is too big. Bail out and start pruning!
+ break;
+ }
+ }
+
+ // Save the time it takes to check if the pending pings are over-quota.
+ Telemetry.getHistogramById("TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS")
+ .add(Math.round(Policy.now().getTime() - startTimeStamp));
+
+ let recordHistograms = (sizeInMB, evictedPings, elapsedMs) => {
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").add(sizeInMB);
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").add(evictedPings);
+ Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").add(elapsedMs);
+ };
+
+ // Check if we're using too much space. If not, bail out.
+ if (pendingPingsSizeInBytes < Policy.getPendingPingsQuota()) {
+ recordHistograms(Math.round(pendingPingsSizeInBytes / 1024 / 1024), 0, 0);
+ return;
+ }
+
+ this._log.info("_enforcePendingPingsQuota - size: " + pendingPingsSizeInBytes + "bytes"
+ + ", safety quota: " + SAFE_QUOTA + "bytes");
+
+ startTimeStamp = Policy.now().getTime();
+ let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1);
+
+ // Remove all the pings older than the last one which we are safe to keep.
+ for (let ping of pingsToPurge) {
+ if (this._shutdown) {
+ this._log.trace("_enforcePendingPingsQuota - Terminating the clean up task due to shutdown");
+ return;
+ }
+
+ // This list is guaranteed to be in order, so remove the pings at its
+ // beginning (oldest).
+ yield this.removePendingPing(ping.id);
+ }
+
+ const endTimeStamp = Policy.now().getTime();
+ // We don't know the size of the pending pings directory if we are above the quota,
+ // since we stop scanning once we reach the quota. We use a special value to show
+ // this condition.
+ recordHistograms(PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE, pingsToPurge.length,
+ Math.ceil(endTimeStamp - startTimeStamp));
+ }),
+
+ /**
+ * Reset the storage state in tests.
+ */
+ reset: function() {
+ this._shutdown = false;
+ this._scannedArchiveDirectory = false;
+ this._archivedPings = new Map();
+ this._scannedPendingDirectory = false;
+ this._pendingPings = new Map();
+ },
+
+ /**
+ * Get a list of info on the archived pings.
+ * This will scan the archive directory and grab basic data about the existing
+ * pings out of their filename.
+ *
+ * @return {promise<sequence<object>>}
+ */
+ loadArchivedPingList: Task.async(function*() {
+ // If there's an archive loading task already running, return it.
+ if (this._scanArchiveTask) {
+ return this._scanArchiveTask;
+ }
+
+ yield waitForAll(this._activelyArchiving);
+
+ if (this._scannedArchiveDirectory) {
+ this._log.trace("loadArchivedPingList - Archive already scanned, hitting cache.");
+ return this._archivedPings;
+ }
+
+ // Since there's no archive loading task running, start it.
+ let result;
+ try {
+ this._scanArchiveTask = this._scanArchive();
+ result = yield this._scanArchiveTask;
+ } finally {
+ this._scanArchiveTask = null;
+ }
+ return result;
+ }),
+
+ _scanArchive: Task.async(function*() {
+ this._log.trace("_scanArchive");
+
+ let submitProbes = (pingCount, dirCount) => {
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT")
+ .add(pingCount);
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT")
+ .add(dirCount);
+ };
+
+ if (!(yield OS.File.exists(gPingsArchivePath))) {
+ submitProbes(0, 0);
+ return new Map();
+ }
+
+ let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath);
+ let subdirs =
+ (yield dirIterator.nextBatch()).filter(e => e.isDir).filter(e => isValidArchiveDir(e.name));
+ dirIterator.close();
+
+ // Walk through the monthly subdirs of the form <YYYY-MM>/
+ for (let dir of subdirs) {
+ this._log.trace("_scanArchive - checking in subdir: " + dir.path);
+ let pingIterator = new OS.File.DirectoryIterator(dir.path);
+ let pings = (yield pingIterator.nextBatch()).filter(e => !e.isDir);
+ pingIterator.close();
+
+ // Now process any ping files of the form "<timestamp>.<uuid>.<type>.[json|jsonlz4]".
+ for (let p of pings) {
+ // data may be null if the filename doesn't match the above format.
+ let data = this._getArchivedPingDataFromFileName(p.name);
+ if (!data) {
+ continue;
+ }
+
+ // In case of conflicts, overwrite only with newer pings.
+ if (this._archivedPings.has(data.id)) {
+ const overwrite = data.timestamp > this._archivedPings.get(data.id).timestampCreated;
+ this._log.warn("_scanArchive - have seen this id before: " + data.id +
+ ", overwrite: " + overwrite);
+ if (!overwrite) {
+ continue;
+ }
+
+ yield this._removeArchivedPing(data.id, data.timestampCreated, data.type)
+ .catch((e) => this._log.warn("_scanArchive - failed to remove ping", e));
+ }
+
+ this._archivedPings.set(data.id, {
+ timestampCreated: data.timestamp,
+ type: internString(data.type),
+ });
+ }
+ }
+
+ // Mark the archive as scanned, so we no longer hit the disk.
+ this._scannedArchiveDirectory = true;
+ // Update the ping and directories count histograms.
+ submitProbes(this._archivedPings.size, subdirs.length);
+ return this._archivedPings;
+ }),
+
+ /**
+ * Save a single ping to a file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {string} file The destination file.
+ * @param {bool} overwrite If |true|, the file will be overwritten if it exists,
+ * if |false| the file will not be overwritten and no error will be reported if
+ * the file exists.
+ * @param {bool} [compress=false] If |true|, the file will use lz4 compression. Otherwise no
+ * compression will be used.
+ * @returns {promise}
+ */
+ savePingToFile: Task.async(function*(ping, filePath, overwrite, compress = false) {
+ try {
+ this._log.trace("savePingToFile - path: " + filePath);
+ let pingString = JSON.stringify(ping);
+ let options = { tmpPath: filePath + ".tmp", noOverwrite: !overwrite };
+ if (compress) {
+ options.compression = "lz4";
+ }
+ yield OS.File.writeAtomic(filePath, pingString, options);
+ } catch (e) {
+ if (!e.becauseExists) {
+ throw e;
+ }
+ }
+ }),
+
+ /**
+ * Save a ping to its file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {bool} overwrite If |true|, the file will be overwritten
+ * if it exists.
+ * @returns {promise}
+ */
+ savePing: Task.async(function*(ping, overwrite) {
+ yield getPingDirectory();
+ let file = pingFilePath(ping);
+ yield this.savePingToFile(ping, file, overwrite);
+ return file;
+ }),
+
+ /**
+ * Add a ping to the saved pings directory so that it gets saved
+ * and sent along with other pings.
+ * Note: that the original ping file will not be modified.
+ *
+ * @param {Object} ping The ping object.
+ * @return {Promise} A promise resolved when the ping is saved to the pings directory.
+ */
+ addPendingPing: function(ping) {
+ return this.savePendingPing(ping);
+ },
+
+ /**
+ * Remove the file for a ping
+ *
+ * @param {object} ping The ping.
+ * @returns {promise}
+ */
+ cleanupPingFile: function(ping) {
+ return OS.File.remove(pingFilePath(ping));
+ },
+
+ savePendingPing: function(ping) {
+ let p = this.savePing(ping, true).then((path) => {
+ this._pendingPings.set(ping.id, {
+ path: path,
+ lastModificationDate: Policy.now().getTime(),
+ });
+ this._log.trace("savePendingPing - saved ping with id " + ping.id);
+ });
+ this._trackPendingPingSaveTask(p);
+ return p;
+ },
+
+ loadPendingPing: Task.async(function*(id) {
+ this._log.trace("loadPendingPing - id: " + id);
+ let info = this._pendingPings.get(id);
+ if (!info) {
+ this._log.trace("loadPendingPing - unknown id " + id);
+ throw new Error("TelemetryStorage.loadPendingPing - no ping with id " + id);
+ }
+
+ // Try to get the dimension of the ping. If that fails, update the histograms.
+ let fileSize = 0;
+ try {
+ fileSize = (yield OS.File.stat(info.path)).size;
+ } catch (e) {
+ if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile) {
+ throw e;
+ }
+ // Fall through and let |loadPingFile| report the error.
+ }
+
+ // Purge pings which are too big.
+ if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ yield this.removePendingPing(id);
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB")
+ .add(Math.floor(fileSize / 1024 / 1024));
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add();
+ throw new Error("loadPendingPing - exceeded the maximum ping size: " + fileSize);
+ }
+
+ // Try to load the ping file. Update the related histograms on failure.
+ let ping;
+ try {
+ ping = yield this.loadPingFile(info.path, false);
+ } catch (e) {
+ // If we failed to load the ping, check what happened and update the histogram.
+ if (e instanceof PingReadError) {
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").add();
+ } else if (e instanceof PingParseError) {
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").add();
+ }
+ // Remove the ping from the cache, so we don't try to load it again.
+ this._pendingPings.delete(id);
+ // Then propagate the rejection.
+ throw e;
+ }
+
+ return ping;
+ }),
+
+ removePendingPing: function(id) {
+ let info = this._pendingPings.get(id);
+ if (!info) {
+ this._log.trace("removePendingPing - unknown id " + id);
+ return Promise.resolve();
+ }
+
+ this._log.trace("removePendingPing - deleting ping with id: " + id +
+ ", path: " + info.path);
+ this._pendingPings.delete(id);
+ return OS.File.remove(info.path).catch((ex) =>
+ this._log.error("removePendingPing - failed to remove ping", ex));
+ },
+
+ /**
+ * Track any pending ping save tasks through the promise passed here.
+ * This is needed to block on any outstanding ping save activity.
+ *
+ * @param {Object<Promise>} The save promise to track.
+ */
+ _trackPendingPingSaveTask: function (promise) {
+ let clear = () => this._activePendingPingSaves.delete(promise);
+ promise.then(clear, clear);
+ this._activePendingPingSaves.add(promise);
+ },
+
+ /**
+ * Return a promise that allows to wait on pending pings being saved.
+ * @return {Object<Promise>} A promise resolved when all the pending pings save promises
+ * are resolved.
+ */
+ promisePendingPingSaves: function () {
+ // Make sure to wait for all the promises, even if they reject. We don't need to log
+ // the failures here, as they are already logged elsewhere.
+ return waitForAll(this._activePendingPingSaves);
+ },
+
+ /**
+ * Run the task to remove all the pending pings (except the deletion ping).
+ *
+ * @return {Promise} Resolved when the pings are removed.
+ */
+ runRemovePendingPingsTask: Task.async(function*() {
+ // If we already have a pending pings removal task active, return that.
+ if (this._removePendingPingsTask) {
+ return this._removePendingPingsTask;
+ }
+
+ // Start the task to remove all pending pings. Also make sure to clear the task once done.
+ try {
+ this._removePendingPingsTask = this.removePendingPings();
+ yield this._removePendingPingsTask;
+ } finally {
+ this._removePendingPingsTask = null;
+ }
+ return undefined;
+ }),
+
+ removePendingPings: Task.async(function*() {
+ this._log.trace("removePendingPings - removing all pending pings");
+
+ // Wait on pending pings still being saved, so so we don't miss removing them.
+ yield this.promisePendingPingSaves();
+
+ // Individually remove existing pings, so we don't interfere with operations expecting
+ // the pending pings directory to exist.
+ const directory = TelemetryStorage.pingDirectoryPath;
+ let iter = new OS.File.DirectoryIterator(directory);
+
+ try {
+ if (!(yield iter.exists())) {
+ this._log.trace("removePendingPings - the pending pings directory doesn't exist");
+ return;
+ }
+
+ let files = (yield iter.nextBatch()).filter(e => !e.isDir);
+ for (let file of files) {
+ try {
+ yield OS.File.remove(file.path);
+ } catch (ex) {
+ this._log.error("removePendingPings - failed to remove file " + file.path, ex);
+ continue;
+ }
+ }
+ } finally {
+ yield iter.close();
+ }
+ }),
+
+ loadPendingPingList: function() {
+ // If we already have a pending scanning task active, return that.
+ if (this._scanPendingPingsTask) {
+ return this._scanPendingPingsTask;
+ }
+
+ if (this._scannedPendingDirectory) {
+ this._log.trace("loadPendingPingList - Pending already scanned, hitting cache.");
+ return Promise.resolve(this._buildPingList());
+ }
+
+ // Since there's no pending pings scan task running, start it.
+ // Also make sure to clear the task once done.
+ this._scanPendingPingsTask = this._scanPendingPings().then(pings => {
+ this._scanPendingPingsTask = null;
+ return pings;
+ }, ex => {
+ this._scanPendingPingsTask = null;
+ throw ex;
+ });
+ return this._scanPendingPingsTask;
+ },
+
+ getPendingPingList: function() {
+ return this._buildPingList();
+ },
+
+ _scanPendingPings: Task.async(function*() {
+ this._log.trace("_scanPendingPings");
+
+ let directory = TelemetryStorage.pingDirectoryPath;
+ let iter = new OS.File.DirectoryIterator(directory);
+ let exists = yield iter.exists();
+
+ try {
+ if (!exists) {
+ return [];
+ }
+
+ let files = (yield iter.nextBatch()).filter(e => !e.isDir);
+
+ for (let file of files) {
+ if (this._shutdown) {
+ return [];
+ }
+
+ let info;
+ try {
+ info = yield OS.File.stat(file.path);
+ } catch (ex) {
+ this._log.error("_scanPendingPings - failed to stat file " + file.path, ex);
+ continue;
+ }
+
+ // Enforce a maximum file size limit on pending pings.
+ if (info.size > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ this._log.error("_scanPendingPings - removing file exceeding size limit " + file.path);
+ try {
+ yield OS.File.remove(file.path);
+ } catch (ex) {
+ this._log.error("_scanPendingPings - failed to remove file " + file.path, ex);
+ } finally {
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB")
+ .add(Math.floor(info.size / 1024 / 1024));
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add();
+ continue;
+ }
+ }
+
+ let id = OS.Path.basename(file.path);
+ if (!UUID_REGEX.test(id)) {
+ this._log.trace("_scanPendingPings - filename is not a UUID: " + id);
+ id = Utils.generateUUID();
+ }
+
+ this._pendingPings.set(id, {
+ path: file.path,
+ lastModificationDate: info.lastModificationDate.getTime(),
+ });
+ }
+ } finally {
+ yield iter.close();
+ }
+
+ // Explicitly load the deletion ping from its known path, if it's there.
+ if (yield OS.File.exists(gDeletionPingFilePath)) {
+ this._log.trace("_scanPendingPings - Adding pending deletion ping.");
+ // We can't get the ping id or the last modification date without hitting the disk.
+ // Since deletion has a special handling, we don't really need those.
+ this._pendingPings.set(Utils.generateUUID(), {
+ path: gDeletionPingFilePath,
+ lastModificationDate: Date.now(),
+ });
+ }
+
+ this._scannedPendingDirectory = true;
+ return this._buildPingList();
+ }),
+
+ _buildPingList: function() {
+ const list = Array.from(this._pendingPings, p => ({
+ id: p[0],
+ lastModificationDate: p[1].lastModificationDate,
+ }));
+
+ list.sort((a, b) => b.lastModificationDate - a.lastModificationDate);
+ return list;
+ },
+
+ get pendingPingCount() {
+ return this._pendingPings.size;
+ },
+
+ /**
+ * Loads a ping file.
+ * @param {String} aFilePath The path of the ping file.
+ * @param {Boolean} [aCompressed=false] If |true|, expects the file to be compressed using lz4.
+ * @return {Promise<Object>} A promise resolved with the ping content or rejected if the
+ * ping contains invalid data.
+ * @throws {PingReadError} There was an error while reading the ping file from the disk.
+ * @throws {PingParseError} There was an error while parsing the JSON content of the ping file.
+ */
+ loadPingFile: Task.async(function* (aFilePath, aCompressed = false) {
+ let options = {};
+ if (aCompressed) {
+ options.compression = "lz4";
+ }
+
+ let array;
+ try {
+ array = yield OS.File.read(aFilePath, options);
+ } catch (e) {
+ this._log.trace("loadPingfile - unreadable ping " + aFilePath, e);
+ throw new PingReadError(e.message, e.becauseNoSuchFile);
+ }
+
+ let decoder = new TextDecoder();
+ let string = decoder.decode(array);
+ let ping;
+ try {
+ ping = JSON.parse(string);
+ } catch (e) {
+ this._log.trace("loadPingfile - unparseable ping " + aFilePath, e);
+ yield OS.File.remove(aFilePath).catch((ex) => {
+ this._log.error("loadPingFile - failed removing unparseable ping file", ex);
+ });
+ throw new PingParseError(e.message);
+ }
+
+ return ping;
+ }),
+
+ /**
+ * Archived pings are saved with file names of the form:
+ * "<timestamp>.<uuid>.<type>.[json|jsonlz4]"
+ * This helper extracts that data from a given filename.
+ *
+ * @param fileName {String} The filename.
+ * @return {Object} Null if the filename didn't match the expected form.
+ * Otherwise an object with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string> }
+ */
+ _getArchivedPingDataFromFileName: function(fileName) {
+ // Extract the parts.
+ let parts = fileName.split(".");
+ if (parts.length != 4) {
+ this._log.trace("_getArchivedPingDataFromFileName - should have 4 parts");
+ return null;
+ }
+
+ let [timestamp, uuid, type, extension] = parts;
+ if (extension != "json" && extension != "jsonlz4") {
+ this._log.trace("_getArchivedPingDataFromFileName - should have 'json' or 'jsonlz4' extension");
+ return null;
+ }
+
+ // Check for a valid timestamp.
+ timestamp = parseInt(timestamp);
+ if (Number.isNaN(timestamp)) {
+ this._log.trace("_getArchivedPingDataFromFileName - should have a valid timestamp");
+ return null;
+ }
+
+ // Check for a valid UUID.
+ if (!UUID_REGEX.test(uuid)) {
+ this._log.trace("_getArchivedPingDataFromFileName - should have a valid id");
+ return null;
+ }
+
+ // Check for a valid type string.
+ const typeRegex = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;
+ if (!typeRegex.test(type)) {
+ this._log.trace("_getArchivedPingDataFromFileName - should have a valid type");
+ return null;
+ }
+
+ return {
+ timestamp: timestamp,
+ id: uuid,
+ type: type,
+ };
+ },
+
+ saveAbortedSessionPing: Task.async(function*(ping) {
+ this._log.trace("saveAbortedSessionPing - ping path: " + gAbortedSessionFilePath);
+ yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });
+
+ return this._abortedSessionSerializer.enqueueTask(() =>
+ this.savePingToFile(ping, gAbortedSessionFilePath, true));
+ }),
+
+ loadAbortedSessionPing: Task.async(function*() {
+ let ping = null;
+ try {
+ ping = yield this.loadPingFile(gAbortedSessionFilePath);
+ } catch (ex) {
+ if (ex.becauseNoSuchFile) {
+ this._log.trace("loadAbortedSessionPing - no such file");
+ } else {
+ this._log.error("loadAbortedSessionPing - error loading ping", ex)
+ }
+ }
+ return ping;
+ }),
+
+ removeAbortedSessionPing: function() {
+ return this._abortedSessionSerializer.enqueueTask(Task.async(function*() {
+ try {
+ yield OS.File.remove(gAbortedSessionFilePath, { ignoreAbsent: false });
+ this._log.trace("removeAbortedSessionPing - success");
+ } catch (ex) {
+ if (ex.becauseNoSuchFile) {
+ this._log.trace("removeAbortedSessionPing - no such file");
+ } else {
+ this._log.error("removeAbortedSessionPing - error removing ping", ex)
+ }
+ }
+ }.bind(this)));
+ },
+
+ /**
+ * Save the deletion ping.
+ * @param ping The deletion ping.
+ * @return {Promise} Resolved when the ping is saved.
+ */
+ saveDeletionPing: Task.async(function*(ping) {
+ this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath);
+ yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });
+
+ let p = this._deletionPingSerializer.enqueueTask(() =>
+ this.savePingToFile(ping, gDeletionPingFilePath, true));
+ this._trackPendingPingSaveTask(p);
+ return p;
+ }),
+
+ /**
+ * Remove the deletion ping.
+ * @return {Promise} Resolved when the ping is deleted from the disk.
+ */
+ removeDeletionPing: Task.async(function*() {
+ return this._deletionPingSerializer.enqueueTask(Task.async(function*() {
+ try {
+ yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false });
+ this._log.trace("removeDeletionPing - success");
+ } catch (ex) {
+ if (ex.becauseNoSuchFile) {
+ this._log.trace("removeDeletionPing - no such file");
+ } else {
+ this._log.error("removeDeletionPing - error removing ping", ex)
+ }
+ }
+ }.bind(this)));
+ }),
+
+ isDeletionPing: function(aPingId) {
+ this._log.trace("isDeletionPing - id: " + aPingId);
+ let pingInfo = this._pendingPings.get(aPingId);
+ if (!pingInfo) {
+ return false;
+ }
+
+ if (pingInfo.path != gDeletionPingFilePath) {
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Remove FHR database files. This is temporary and will be dropped in
+ * the future.
+ * @return {Promise} Resolved when the database files are deleted.
+ */
+ removeFHRDatabase: Task.async(function*() {
+ this._log.trace("removeFHRDatabase");
+
+ // Let's try to remove the FHR DB with the default filename first.
+ const FHR_DB_DEFAULT_FILENAME = "healthreport.sqlite";
+
+ // Even if it's uncommon, there may be 2 additional files: - a "write ahead log"
+ // (-wal) file and a "shared memory file" (-shm). We need to remove them as well.
+ let FILES_TO_REMOVE = [
+ OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME),
+ OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME + "-wal"),
+ OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME + "-shm"),
+ ];
+
+ // FHR could have used either the default DB file name or a custom one
+ // through this preference.
+ const FHR_DB_CUSTOM_FILENAME =
+ Preferences.get("datareporting.healthreport.dbName", undefined);
+ if (FHR_DB_CUSTOM_FILENAME) {
+ FILES_TO_REMOVE.push(
+ OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME),
+ OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME + "-wal"),
+ OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME + "-shm"));
+ }
+
+ for (let f of FILES_TO_REMOVE) {
+ yield OS.File.remove(f, {ignoreAbsent: true})
+ .catch(e => this._log.error("removeFHRDatabase - failed to remove " + f, e));
+ }
+ }),
+};
+
+// Utility functions
+
+function pingFilePath(ping) {
+ // Support legacy ping formats, who don't have an "id" field, but a "slug" field.
+ let pingIdentifier = (ping.slug) ? ping.slug : ping.id;
+ return OS.Path.join(TelemetryStorage.pingDirectoryPath, pingIdentifier);
+}
+
+function getPingDirectory() {
+ return Task.spawn(function*() {
+ let directory = TelemetryStorage.pingDirectoryPath;
+
+ if (!(yield OS.File.exists(directory))) {
+ yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });
+ }
+
+ return directory;
+ });
+}
+
+/**
+ * Build the path to the archived ping.
+ * @param {String} aPingId The ping id.
+ * @param {Object} aDate The ping creation date.
+ * @param {String} aType The ping type.
+ * @return {String} The full path to the archived ping.
+ */
+function getArchivedPingPath(aPingId, aDate, aType) {
+ // Helper to pad the month to 2 digits, if needed (e.g. "1" -> "01").
+ let addLeftPadding = value => (value < 10) ? ("0" + value) : value;
+ // Get the ping creation date and generate the archive directory to hold it. Note
+ // that getMonth returns a 0-based month, so we need to add an offset.
+ let archivedPingDir = OS.Path.join(gPingsArchivePath,
+ aDate.getFullYear() + '-' + addLeftPadding(aDate.getMonth() + 1));
+ // Generate the archived ping file path as YYYY-MM/<TIMESTAMP>.UUID.type.json
+ let fileName = [aDate.getTime(), aPingId, aType, "json"].join(".");
+ return OS.Path.join(archivedPingDir, fileName);
+}
+
+/**
+ * Get the size of the ping file on the disk.
+ * @return {Integer} The file size, in bytes, of the ping file or 0 on errors.
+ */
+var getArchivedPingSize = Task.async(function*(aPingId, aDate, aType) {
+ const path = getArchivedPingPath(aPingId, aDate, aType);
+ let filePaths = [ path + "lz4", path ];
+
+ for (let path of filePaths) {
+ try {
+ return (yield OS.File.stat(path)).size;
+ } catch (e) {}
+ }
+
+ // That's odd, this ping doesn't seem to exist.
+ return 0;
+});
+
+/**
+ * Get the size of the pending ping file on the disk.
+ * @return {Integer} The file size, in bytes, of the ping file or 0 on errors.
+ */
+var getPendingPingSize = Task.async(function*(aPingId) {
+ const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, aPingId)
+ try {
+ return (yield OS.File.stat(path)).size;
+ } catch (e) {}
+
+ // That's odd, this ping doesn't seem to exist.
+ return 0;
+});
+
+/**
+ * Check if a directory name is in the "YYYY-MM" format.
+ * @param {String} aDirName The name of the pings archive directory.
+ * @return {Boolean} True if the directory name is in the right format, false otherwise.
+ */
+function isValidArchiveDir(aDirName) {
+ const dirRegEx = /^[0-9]{4}-[0-9]{2}$/;
+ return dirRegEx.test(aDirName);
+}
+
+/**
+ * Gets a date object from an archive directory name.
+ * @param {String} aDirName The name of the pings archive directory. Must be in the YYYY-MM
+ * format.
+ * @return {Object} A Date object or null if the dir name is not valid.
+ */
+function getDateFromArchiveDir(aDirName) {
+ let [year, month] = aDirName.split("-");
+ year = parseInt(year);
+ month = parseInt(month);
+ // Make sure to have sane numbers.
+ if (!Number.isFinite(month) || !Number.isFinite(year) || month < 1 || month > 12) {
+ return null;
+ }
+ return new Date(year, month - 1, 1, 0, 0, 0);
+}
diff --git a/toolkit/components/telemetry/TelemetryTimestamps.jsm b/toolkit/components/telemetry/TelemetryTimestamps.jsm
new file mode 100644
index 0000000000..e49d7453c0
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryTimestamps.jsm
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = ["TelemetryTimestamps"];
+
+const Cu = Components.utils;
+
+/**
+ * This module's purpose is to collect timestamps for important
+ * application-specific events.
+ *
+ * The TelemetryController component attaches the timestamps stored by this module to
+ * the telemetry submission, substracting the process lifetime so that the times
+ * are relative to process startup. The overall goal is to produce a basic
+ * timeline of the startup process.
+ */
+var timeStamps = {};
+
+this.TelemetryTimestamps = {
+ /**
+ * Adds a timestamp to the list. The addition of TimeStamps that already have
+ * a value stored is ignored.
+ *
+ * @param name must be a unique, generally "camelCase" descriptor of what the
+ * timestamp represents. e.g.: "delayedStartupStarted"
+ * @param value is a timeStamp in milliseconds since the epoch. If omitted,
+ * defaults to Date.now().
+ */
+ add: function TT_add(name, value) {
+ // Default to "now" if not specified
+ if (value == null)
+ value = Date.now();
+
+ if (isNaN(value))
+ throw new Error("Value must be a timestamp");
+
+ // If there's an existing value, just ignore the new value.
+ if (timeStamps.hasOwnProperty(name))
+ return;
+
+ timeStamps[name] = value;
+ },
+
+ /**
+ * Returns a JS object containing all of the timeStamps as properties (can be
+ * easily serialized to JSON). Used by TelemetryController to retrieve the data
+ * to attach to the telemetry submission.
+ */
+ get: function TT_get() {
+ // Return a copy of the object.
+ return Cu.cloneInto(timeStamps, {});
+ }
+};
diff --git a/toolkit/components/telemetry/TelemetryUtils.jsm b/toolkit/components/telemetry/TelemetryUtils.jsm
new file mode 100644
index 0000000000..4d934c9c1d
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryUtils.jsm
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetryUtils"
+];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+
+const IS_CONTENT_PROCESS = (function() {
+ // We cannot use Services.appinfo here because in telemetry xpcshell tests,
+ // appinfo is initially unavailable, and becomes available only later on.
+ let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+ return runtime.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+})();
+
+this.TelemetryUtils = {
+ /**
+ * True if this is a content process.
+ */
+ get isContentProcess() {
+ return IS_CONTENT_PROCESS;
+ },
+
+ /**
+ * Returns the state of the Telemetry enabled preference, making sure
+ * it correctly evaluates to a boolean type.
+ */
+ get isTelemetryEnabled() {
+ return Preferences.get(PREF_TELEMETRY_ENABLED, false) === true;
+ },
+
+ /**
+ * Turn a millisecond timestamp into a day timestamp.
+ *
+ * @param aMsec A number of milliseconds since Unix epoch.
+ * @return The number of whole days since Unix epoch.
+ */
+ millisecondsToDays: function(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+ },
+
+ /**
+ * Takes a date and returns it trunctated to a date with daily precision.
+ */
+ truncateToDays: function(date) {
+ return new Date(date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ 0, 0, 0, 0);
+ },
+
+ /**
+ * Check if the difference between the times is within the provided tolerance.
+ * @param {Number} t1 A time in milliseconds.
+ * @param {Number} t2 A time in milliseconds.
+ * @param {Number} tolerance The tolerance, in milliseconds.
+ * @return {Boolean} True if the absolute time difference is within the tolerance, false
+ * otherwise.
+ */
+ areTimesClose: function(t1, t2, tolerance) {
+ return Math.abs(t1 - t2) <= tolerance;
+ },
+
+ /**
+ * Get the next midnight for a date.
+ * @param {Object} date The date object to check.
+ * @return {Object} The Date object representing the next midnight.
+ */
+ getNextMidnight: function(date) {
+ let nextMidnight = new Date(this.truncateToDays(date));
+ nextMidnight.setDate(nextMidnight.getDate() + 1);
+ return nextMidnight;
+ },
+
+ /**
+ * Get the midnight which is closer to the provided date.
+ * @param {Object} date The date object to check.
+ * @param {Number} tolerance The tolerance within we find the closest midnight.
+ * @return {Object} The Date object representing the closes midnight, or null if midnight
+ * is not within the midnight tolerance.
+ */
+ getNearestMidnight: function(date, tolerance) {
+ let lastMidnight = this.truncateToDays(date);
+ if (this.areTimesClose(date.getTime(), lastMidnight.getTime(), tolerance)) {
+ return lastMidnight;
+ }
+
+ const nextMidnightDate = this.getNextMidnight(date);
+ if (this.areTimesClose(date.getTime(), nextMidnightDate.getTime(), tolerance)) {
+ return nextMidnightDate;
+ }
+ return null;
+ },
+
+ generateUUID: function() {
+ let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ // strip {}
+ return str.substring(1, str.length - 1);
+ },
+
+ /**
+ * Find how many months passed between two dates.
+ * @param {Object} aStartDate The starting date.
+ * @param {Object} aEndDate The ending date.
+ * @return {Integer} The number of months between the two dates.
+ */
+ getElapsedTimeInMonths: function(aStartDate, aEndDate) {
+ return (aEndDate.getMonth() - aStartDate.getMonth())
+ + 12 * (aEndDate.getFullYear() - aStartDate.getFullYear());
+ },
+
+ /**
+ * Date.toISOString() gives us UTC times, this gives us local times in
+ * the ISO date format. See http://www.w3.org/TR/NOTE-datetime
+ * @param {Object} date The input date.
+ * @return {String} The local time ISO string.
+ */
+ toLocalTimeISOString: function(date) {
+ function padNumber(number, places) {
+ number = number.toString();
+ while (number.length < places) {
+ number = "0" + number;
+ }
+ return number;
+ }
+
+ let sign = (n) => n >= 0 ? "+" : "-";
+ // getTimezoneOffset counter-intuitively returns -60 for UTC+1.
+ let tzOffset = - date.getTimezoneOffset();
+
+ // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
+ return padNumber(date.getFullYear(), 4)
+ + "-" + padNumber(date.getMonth() + 1, 2)
+ + "-" + padNumber(date.getDate(), 2)
+ + "T" + padNumber(date.getHours(), 2)
+ + ":" + padNumber(date.getMinutes(), 2)
+ + ":" + padNumber(date.getSeconds(), 2)
+ + "." + date.getMilliseconds()
+ + sign(tzOffset) + padNumber(Math.floor(Math.abs(tzOffset / 60)), 2)
+ + ":" + padNumber(Math.abs(tzOffset % 60), 2);
+ },
+};
diff --git a/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm b/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm
new file mode 100644
index 0000000000..fedac17100
--- /dev/null
+++ b/toolkit/components/telemetry/ThirdPartyCookieProbe.jsm
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+this.EXPORTED_SYMBOLS = ["ThirdPartyCookieProbe"];
+
+const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
+
+/**
+ * A probe implementing the measurements detailed at
+ * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry
+ *
+ * This implementation uses only in-memory data.
+ */
+this.ThirdPartyCookieProbe = function() {
+ /**
+ * A set of third-party sites that have caused cookies to be
+ * rejected. These sites are trimmed down to ETLD + 1
+ * (i.e. "x.y.com" and "z.y.com" are both trimmed down to "y.com",
+ * "x.y.co.uk" is trimmed down to "y.co.uk").
+ *
+ * Used to answer the following question: "For each third-party
+ * site, how many other first parties embed them and result in
+ * cookie traffic?" (see
+ * https://wiki.mozilla.org/SecurityEngineering/ThirdPartyCookies/Telemetry#Breadth
+ * )
+ *
+ * @type Map<string, RejectStats> A mapping from third-party site
+ * to rejection statistics.
+ */
+ this._thirdPartyCookies = new Map();
+ /**
+ * Timestamp of the latest call to flush() in milliseconds since the Epoch.
+ */
+ this._latestFlush = Date.now();
+};
+
+this.ThirdPartyCookieProbe.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+ init: function() {
+ Services.obs.addObserver(this, "profile-before-change", false);
+ Services.obs.addObserver(this, "third-party-cookie-accepted", false);
+ Services.obs.addObserver(this, "third-party-cookie-rejected", false);
+ },
+ dispose: function() {
+ Services.obs.removeObserver(this, "profile-before-change");
+ Services.obs.removeObserver(this, "third-party-cookie-accepted");
+ Services.obs.removeObserver(this, "third-party-cookie-rejected");
+ },
+ /**
+ * Observe either
+ * - "profile-before-change" (no meaningful subject or data) - time to flush statistics and unregister; or
+ * - "third-party-cookie-accepted"/"third-party-cookie-rejected" with
+ * subject: the nsIURI of the third-party that attempted to set the cookie;
+ * data: a string holding the uri of the page seen by the user.
+ */
+ observe: function(docURI, topic, referrer) {
+ try {
+ if (topic == "profile-before-change") {
+ // A final flush, then unregister
+ this.flush();
+ this.dispose();
+ }
+ if (topic != "third-party-cookie-accepted"
+ && topic != "third-party-cookie-rejected") {
+ // Not a third-party cookie
+ return;
+ }
+ // Add host to this._thirdPartyCookies
+ // Note: nsCookieService passes "?" if the issuer is unknown. Avoid
+ // normalizing in this case since its not a valid URI.
+ let firstParty = (referrer === "?") ? referrer : normalizeHost(referrer);
+ let thirdParty = normalizeHost(docURI.QueryInterface(Ci.nsIURI).host);
+ let data = this._thirdPartyCookies.get(thirdParty);
+ if (!data) {
+ data = new RejectStats();
+ this._thirdPartyCookies.set(thirdParty, data);
+ }
+ if (topic == "third-party-cookie-accepted") {
+ data.addAccepted(firstParty);
+ } else {
+ data.addRejected(firstParty);
+ }
+ } catch (ex) {
+ if (ex instanceof Ci.nsIXPCException) {
+ if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+ ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ return;
+ }
+ }
+ // Other errors should not remain silent.
+ Services.console.logStringMessage("ThirdPartyCookieProbe: Uncaught error " + ex + "\n" + ex.stack);
+ }
+ },
+
+ /**
+ * Clear internal data, fill up corresponding histograms.
+ *
+ * @param {number} aNow (optional, used for testing purposes only)
+ * The current instant. Used to make tests time-independent.
+ */
+ flush: function(aNow = Date.now()) {
+ let updays = (aNow - this._latestFlush) / MILLISECONDS_PER_DAY;
+ if (updays <= 0) {
+ // Unlikely, but regardless, don't risk division by zero
+ // or weird stuff.
+ return;
+ }
+ this._latestFlush = aNow;
+ this._thirdPartyCookies.clear();
+ }
+};
+
+/**
+ * Data gathered on cookies that a third party site has attempted to set.
+ *
+ * Privacy note: the only data actually sent to the server is the size of
+ * the sets.
+ *
+ * @constructor
+ */
+var RejectStats = function() {
+ /**
+ * The set of all sites for which we have accepted third-party cookies.
+ */
+ this._acceptedSites = new Set();
+ /**
+ * The set of all sites for which we have rejected third-party cookies.
+ */
+ this._rejectedSites = new Set();
+ /**
+ * Total number of attempts to set a third-party cookie that have
+ * been accepted. Two accepted attempts on the same site will both
+ * augment this count.
+ */
+ this._acceptedRequests = 0;
+ /**
+ * Total number of attempts to set a third-party cookie that have
+ * been rejected. Two rejected attempts on the same site will both
+ * augment this count.
+ */
+ this._rejectedRequests = 0;
+};
+RejectStats.prototype = {
+ addAccepted: function(firstParty) {
+ this._acceptedSites.add(firstParty);
+ this._acceptedRequests++;
+ },
+ addRejected: function(firstParty) {
+ this._rejectedSites.add(firstParty);
+ this._rejectedRequests++;
+ },
+ get countAcceptedSites() {
+ return this._acceptedSites.size;
+ },
+ get countRejectedSites() {
+ return this._rejectedSites.size;
+ },
+ get countAcceptedRequests() {
+ return this._acceptedRequests;
+ },
+ get countRejectedRequests() {
+ return this._rejectedRequests;
+ }
+};
+
+/**
+ * Normalize a host to its eTLD + 1.
+ */
+function normalizeHost(host) {
+ return Services.eTLD.getBaseDomainFromHost(host);
+}
diff --git a/toolkit/components/telemetry/ThreadHangStats.h b/toolkit/components/telemetry/ThreadHangStats.h
new file mode 100644
index 0000000000..60aa680c87
--- /dev/null
+++ b/toolkit/components/telemetry/ThreadHangStats.h
@@ -0,0 +1,230 @@
+/* -*- 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 mozilla_BackgroundHangTelemetry_h
+#define mozilla_BackgroundHangTelemetry_h
+
+#include "mozilla/Array.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/HangAnnotations.h"
+#include "mozilla/Move.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/Vector.h"
+
+#include "nsString.h"
+#include "prinrval.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+static const size_t kTimeHistogramBuckets = 8 * sizeof(PRIntervalTime);
+
+/* TimeHistogram is an efficient histogram that puts time durations into
+ exponential (base 2) buckets; times are accepted in PRIntervalTime and
+ stored in milliseconds. */
+class TimeHistogram : public mozilla::Array<uint32_t, kTimeHistogramBuckets>
+{
+public:
+ TimeHistogram()
+ {
+ mozilla::PodArrayZero(*this);
+ }
+ // Get minimum (inclusive) range of bucket in milliseconds
+ uint32_t GetBucketMin(size_t aBucket) const {
+ MOZ_ASSERT(aBucket < ArrayLength(*this));
+ return (1u << aBucket) & ~1u; // Bucket 0 starts at 0, not 1
+ }
+ // Get maximum (inclusive) range of bucket in milliseconds
+ uint32_t GetBucketMax(size_t aBucket) const {
+ MOZ_ASSERT(aBucket < ArrayLength(*this));
+ return (1u << (aBucket + 1u)) - 1u;
+ }
+ void Add(PRIntervalTime aTime);
+};
+
+/* HangStack stores an array of const char pointers,
+ with optional internal storage for strings. */
+class HangStack
+{
+public:
+ static const size_t sMaxInlineStorage = 8;
+
+private:
+ typedef mozilla::Vector<const char*, sMaxInlineStorage> Impl;
+ Impl mImpl;
+
+ // Stack entries can either be a static const char*
+ // or a pointer to within this buffer.
+ mozilla::Vector<char, 0> mBuffer;
+
+public:
+ HangStack() { }
+
+ HangStack(HangStack&& aOther)
+ : mImpl(mozilla::Move(aOther.mImpl))
+ , mBuffer(mozilla::Move(aOther.mBuffer))
+ {
+ }
+
+ bool operator==(const HangStack& aOther) const {
+ for (size_t i = 0; i < length(); i++) {
+ if (!IsSameAsEntry(operator[](i), aOther[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool operator!=(const HangStack& aOther) const {
+ return !operator==(aOther);
+ }
+
+ const char*& operator[](size_t aIndex) {
+ return mImpl[aIndex];
+ }
+
+ const char* const& operator[](size_t aIndex) const {
+ return mImpl[aIndex];
+ }
+
+ size_t capacity() const { return mImpl.capacity(); }
+ size_t length() const { return mImpl.length(); }
+ bool empty() const { return mImpl.empty(); }
+ bool canAppendWithoutRealloc(size_t aNeeded) const {
+ return mImpl.canAppendWithoutRealloc(aNeeded);
+ }
+ void infallibleAppend(const char* aEntry) { mImpl.infallibleAppend(aEntry); }
+ bool reserve(size_t aRequest) { return mImpl.reserve(aRequest); }
+ const char** begin() { return mImpl.begin(); }
+ const char* const* begin() const { return mImpl.begin(); }
+ const char** end() { return mImpl.end(); }
+ const char* const* end() const { return mImpl.end(); }
+ const char*& back() { return mImpl.back(); }
+ void erase(const char** aEntry) { mImpl.erase(aEntry); }
+ void erase(const char** aBegin, const char** aEnd) {
+ mImpl.erase(aBegin, aEnd);
+ }
+
+ void clear() {
+ mImpl.clear();
+ mBuffer.clear();
+ }
+
+ bool IsInBuffer(const char* aEntry) const {
+ return aEntry >= mBuffer.begin() && aEntry < mBuffer.end();
+ }
+
+ bool IsSameAsEntry(const char* aEntry, const char* aOther) const {
+ // If the entry came from the buffer, we need to compare its content;
+ // otherwise we only need to compare its pointer.
+ return IsInBuffer(aEntry) ? !strcmp(aEntry, aOther) : (aEntry == aOther);
+ }
+
+ size_t AvailableBufferSize() const {
+ return mBuffer.capacity() - mBuffer.length();
+ }
+
+ bool EnsureBufferCapacity(size_t aCapacity) {
+ // aCapacity is the minimal capacity and Vector may make the actual
+ // capacity larger, in which case we want to use up all the space.
+ return mBuffer.reserve(aCapacity) &&
+ mBuffer.reserve(mBuffer.capacity());
+ }
+
+ const char* InfallibleAppendViaBuffer(const char* aText, size_t aLength);
+ const char* AppendViaBuffer(const char* aText, size_t aLength);
+};
+
+/* A hang histogram consists of a stack associated with the
+ hang, along with a time histogram of the hang times. */
+class HangHistogram : public TimeHistogram
+{
+private:
+ static uint32_t GetHash(const HangStack& aStack);
+
+ HangStack mStack;
+ // Native stack that corresponds to the pseudostack in mStack
+ HangStack mNativeStack;
+ // Use a hash to speed comparisons
+ const uint32_t mHash;
+ // Annotations attributed to this stack
+ HangMonitor::HangAnnotationsVector mAnnotations;
+
+public:
+ explicit HangHistogram(HangStack&& aStack)
+ : mStack(mozilla::Move(aStack))
+ , mHash(GetHash(mStack))
+ {
+ }
+ HangHistogram(HangHistogram&& aOther)
+ : TimeHistogram(mozilla::Move(aOther))
+ , mStack(mozilla::Move(aOther.mStack))
+ , mNativeStack(mozilla::Move(aOther.mNativeStack))
+ , mHash(mozilla::Move(aOther.mHash))
+ , mAnnotations(mozilla::Move(aOther.mAnnotations))
+ {
+ }
+ bool operator==(const HangHistogram& aOther) const;
+ bool operator!=(const HangHistogram& aOther) const
+ {
+ return !operator==(aOther);
+ }
+ const HangStack& GetStack() const {
+ return mStack;
+ }
+ HangStack& GetNativeStack() {
+ return mNativeStack;
+ }
+ const HangStack& GetNativeStack() const {
+ return mNativeStack;
+ }
+ const HangMonitor::HangAnnotationsVector& GetAnnotations() const {
+ return mAnnotations;
+ }
+ void Add(PRIntervalTime aTime, HangMonitor::HangAnnotationsPtr aAnnotations) {
+ TimeHistogram::Add(aTime);
+ if (aAnnotations) {
+ if (!mAnnotations.append(Move(aAnnotations))) {
+ MOZ_CRASH();
+ }
+ }
+ }
+};
+
+/* Thread hang stats consist of
+ - thread name
+ - time histogram of all task run times
+ - hang histograms of individual hangs
+ - annotations for each hang
+*/
+class ThreadHangStats
+{
+private:
+ nsCString mName;
+
+public:
+ TimeHistogram mActivity;
+ mozilla::Vector<HangHistogram, 4> mHangs;
+
+ explicit ThreadHangStats(const char* aName)
+ : mName(aName)
+ {
+ }
+ ThreadHangStats(ThreadHangStats&& aOther)
+ : mName(mozilla::Move(aOther.mName))
+ , mActivity(mozilla::Move(aOther.mActivity))
+ , mHangs(mozilla::Move(aOther.mHangs))
+ {
+ }
+ const char* GetName() const {
+ return mName.get();
+ }
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // mozilla_BackgroundHangTelemetry_h
diff --git a/toolkit/components/telemetry/UITelemetry.jsm b/toolkit/components/telemetry/UITelemetry.jsm
new file mode 100644
index 0000000000..bd7a34b725
--- /dev/null
+++ b/toolkit/components/telemetry/UITelemetry.jsm
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+const Cu = Components.utils;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_ENABLED = PREF_BRANCH + "enabled";
+
+this.EXPORTED_SYMBOLS = [
+ "UITelemetry",
+];
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+/**
+ * UITelemetry is a helper JSM used to record UI specific telemetry events.
+ *
+ * It implements nsIUITelemetryObserver, defined in nsIAndroidBridge.idl.
+ */
+this.UITelemetry = {
+ _enabled: undefined,
+ _activeSessions: {},
+ _measurements: [],
+
+ // Lazily decide whether telemetry is enabled.
+ get enabled() {
+ if (this._enabled !== undefined) {
+ return this._enabled;
+ }
+
+ // Set an observer to watch for changes at runtime.
+ Services.prefs.addObserver(PREF_ENABLED, this, false);
+ Services.obs.addObserver(this, "profile-before-change", false);
+
+ // Pick up the current value.
+ try {
+ this._enabled = Services.prefs.getBoolPref(PREF_ENABLED);
+ } catch (e) {
+ this._enabled = false;
+ }
+
+ return this._enabled;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "profile-before-change") {
+ Services.obs.removeObserver(this, "profile-before-change");
+ Services.prefs.removeObserver(PREF_ENABLED, this);
+ this._enabled = undefined;
+ return;
+ }
+
+ if (aTopic == "nsPref:changed") {
+ switch (aData) {
+ case PREF_ENABLED:
+ let on = Services.prefs.getBoolPref(PREF_ENABLED);
+ this._enabled = on;
+
+ // Wipe ourselves if we were just disabled.
+ if (!on) {
+ this._activeSessions = {};
+ this._measurements = [];
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * This exists exclusively for testing -- our events are not intended to
+ * be retrieved via an XPCOM interface.
+ */
+ get wrappedJSObject() {
+ return this;
+ },
+
+ /**
+ * Holds the functions that provide UITelemetry's simple
+ * measurements. Those functions are mapped to unique names,
+ * and should be registered with addSimpleMeasureFunction.
+ */
+ _simpleMeasureFunctions: {},
+
+ /**
+ * A hack to generate the relative timestamp from start when we don't have
+ * access to the Java timer.
+ * XXX: Bug 1007647 - Support realtime and/or uptime in JavaScript.
+ */
+ uptimeMillis: function() {
+ return Date.now() - Services.startup.getStartupInfo().process;
+ },
+
+ /**
+ * Adds a single event described by a timestamp, an action, and the calling
+ * method.
+ *
+ * Optionally provide a string 'extras', which will be recorded as part of
+ * the event.
+ *
+ * All extant sessions will be recorded by name for each event.
+ */
+ addEvent: function(aAction, aMethod, aTimestamp, aExtras) {
+ if (!this.enabled) {
+ return;
+ }
+
+ let sessions = Object.keys(this._activeSessions);
+ let aEvent = {
+ type: "event",
+ action: aAction,
+ method: aMethod,
+ sessions: sessions,
+ timestamp: (aTimestamp == undefined) ? this.uptimeMillis() : aTimestamp,
+ };
+
+ if (aExtras) {
+ aEvent.extras = aExtras;
+ }
+
+ this._recordEvent(aEvent);
+ },
+
+ /**
+ * Begins tracking a session by storing a timestamp for session start.
+ */
+ startSession: function(aName, aTimestamp) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (this._activeSessions[aName]) {
+ // Do not overwrite a previous event start if it already exists.
+ return;
+ }
+ this._activeSessions[aName] = (aTimestamp == undefined) ? this.uptimeMillis() : aTimestamp;
+ },
+
+ /**
+ * Tracks the end of a session with a timestamp.
+ */
+ stopSession: function(aName, aReason, aTimestamp) {
+ if (!this.enabled) {
+ return;
+ }
+
+ let sessionStart = this._activeSessions[aName];
+ delete this._activeSessions[aName];
+
+ if (!sessionStart) {
+ return;
+ }
+
+ let aEvent = {
+ type: "session",
+ name: aName,
+ reason: aReason,
+ start: sessionStart,
+ end: (aTimestamp == undefined) ? this.uptimeMillis() : aTimestamp,
+ };
+
+ this._recordEvent(aEvent);
+ },
+
+ _recordEvent: function(aEvent) {
+ this._measurements.push(aEvent);
+ },
+
+ /**
+ * Called by TelemetrySession to populate the simple measurement
+ * blob. This function will iterate over all functions added
+ * via addSimpleMeasureFunction and return an object with the
+ * results of those functions.
+ */
+ getSimpleMeasures: function() {
+ if (!this.enabled) {
+ return {};
+ }
+
+ let result = {};
+ for (let name in this._simpleMeasureFunctions) {
+ result[name] = this._simpleMeasureFunctions[name]();
+ }
+ return result;
+ },
+
+ /**
+ * Allows the caller to register functions that will get called
+ * for simple measures during a Telemetry ping. aName is a unique
+ * identifier used as they key for the simple measurement in the
+ * object that getSimpleMeasures returns.
+ *
+ * This function throws an exception if aName already has a function
+ * registered for it.
+ */
+ addSimpleMeasureFunction: function(aName, aFunction) {
+ if (!this.enabled) {
+ return;
+ }
+
+ if (aName in this._simpleMeasureFunctions) {
+ throw new Error("A simple measurement function is already registered for " + aName);
+ }
+
+ if (!aFunction || typeof aFunction !== 'function') {
+ throw new Error("addSimpleMeasureFunction called with non-function argument.");
+ }
+
+ this._simpleMeasureFunctions[aName] = aFunction;
+ },
+
+ removeSimpleMeasureFunction: function(aName) {
+ delete this._simpleMeasureFunctions[aName];
+ },
+
+ /**
+ * Called by TelemetrySession to populate the UI measurement
+ * blob.
+ *
+ * Optionally clears the set of measurements based on aClear.
+ */
+ getUIMeasurements: function(aClear) {
+ if (!this.enabled) {
+ return [];
+ }
+
+ let measurements = this._measurements.slice();
+ if (aClear) {
+ this._measurements = [];
+ }
+ return measurements;
+ }
+};
diff --git a/toolkit/components/telemetry/WebrtcTelemetry.cpp b/toolkit/components/telemetry/WebrtcTelemetry.cpp
new file mode 100644
index 0000000000..29c22be235
--- /dev/null
+++ b/toolkit/components/telemetry/WebrtcTelemetry.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "Telemetry.h"
+#include "TelemetryCommon.h"
+#include "WebrtcTelemetry.h"
+#include "jsapi.h"
+#include "nsPrintfCString.h"
+#include "nsTHashtable.h"
+
+using mozilla::Telemetry::Common::AutoHashtable;
+
+void
+WebrtcTelemetry::RecordIceCandidateMask(const uint32_t iceCandidateBitmask,
+ const bool success)
+{
+ WebrtcIceCandidateType *entry = mWebrtcIceCandidates.GetEntry(iceCandidateBitmask);
+ if (!entry) {
+ entry = mWebrtcIceCandidates.PutEntry(iceCandidateBitmask);
+ if (MOZ_UNLIKELY(!entry))
+ return;
+ }
+
+ if (success) {
+ entry->mData.webrtc.successCount++;
+ } else {
+ entry->mData.webrtc.failureCount++;
+ }
+}
+
+bool
+ReflectIceEntry(const WebrtcTelemetry::WebrtcIceCandidateType *entry,
+ const WebrtcTelemetry::WebrtcIceCandidateStats *stat, JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ if ((stat->successCount == 0) && (stat->failureCount == 0))
+ return true;
+
+ const uint32_t &bitmask = entry->GetKey();
+
+ JS::Rooted<JSObject*> statsObj(cx, JS_NewPlainObject(cx));
+ if (!statsObj)
+ return false;
+ if (!JS_DefineProperty(cx, obj,
+ nsPrintfCString("%lu", bitmask).BeginReading(),
+ statsObj, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ if (stat->successCount && !JS_DefineProperty(cx, statsObj, "successCount",
+ stat->successCount,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+ if (stat->failureCount && !JS_DefineProperty(cx, statsObj, "failureCount",
+ stat->failureCount,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+ return true;
+}
+
+bool
+ReflectIceWebrtc(WebrtcTelemetry::WebrtcIceCandidateType *entry, JSContext *cx,
+ JS::Handle<JSObject*> obj)
+{
+ return ReflectIceEntry(entry, &entry->mData.webrtc, cx, obj);
+}
+
+bool
+WebrtcTelemetry::AddIceInfo(JSContext *cx, JS::Handle<JSObject*> iceObj)
+{
+ JS::Rooted<JSObject*> statsObj(cx, JS_NewPlainObject(cx));
+ if (!statsObj)
+ return false;
+
+ if (!mWebrtcIceCandidates.ReflectIntoJS(ReflectIceWebrtc, cx, statsObj)) {
+ return false;
+ }
+
+ return JS_DefineProperty(cx, iceObj, "webrtc",
+ statsObj, JSPROP_ENUMERATE);
+}
+
+bool
+WebrtcTelemetry::GetWebrtcStats(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+ JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
+ if (!root_obj)
+ return false;
+ ret.setObject(*root_obj);
+
+ JS::Rooted<JSObject*> ice_obj(cx, JS_NewPlainObject(cx));
+ if (!ice_obj)
+ return false;
+ JS_DefineProperty(cx, root_obj, "IceCandidatesStats", ice_obj,
+ JSPROP_ENUMERATE);
+
+ if (!AddIceInfo(cx, ice_obj))
+ return false;
+
+ return true;
+}
+
+size_t
+WebrtcTelemetry::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ return mWebrtcIceCandidates.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
diff --git a/toolkit/components/telemetry/WebrtcTelemetry.h b/toolkit/components/telemetry/WebrtcTelemetry.h
new file mode 100644
index 0000000000..ed87c71073
--- /dev/null
+++ b/toolkit/components/telemetry/WebrtcTelemetry.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 WebrtcTelemetry_h__
+#define WebrtcTelemetry_h__
+
+#include "nsBaseHashtable.h"
+#include "nsHashKeys.h"
+#include "TelemetryCommon.h"
+
+class WebrtcTelemetry {
+public:
+ struct WebrtcIceCandidateStats {
+ uint32_t successCount;
+ uint32_t failureCount;
+ WebrtcIceCandidateStats() :
+ successCount(0),
+ failureCount(0)
+ {
+ }
+ };
+ struct WebrtcIceStatsCategory {
+ struct WebrtcIceCandidateStats webrtc;
+ };
+ typedef nsBaseHashtableET<nsUint32HashKey, WebrtcIceStatsCategory> WebrtcIceCandidateType;
+
+ void RecordIceCandidateMask(const uint32_t iceCandidateBitmask, bool success);
+
+ bool GetWebrtcStats(JSContext *cx, JS::MutableHandle<JS::Value> ret);
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+private:
+
+ bool AddIceInfo(JSContext *cx, JS::Handle<JSObject*> rootObj);
+
+ mozilla::Telemetry::Common::AutoHashtable<WebrtcIceCandidateType> mWebrtcIceCandidates;
+};
+
+#endif // WebrtcTelemetry_h__
diff --git a/toolkit/components/telemetry/datareporting-prefs.js b/toolkit/components/telemetry/datareporting-prefs.js
new file mode 100644
index 0000000000..6a61f1853d
--- /dev/null
+++ b/toolkit/components/telemetry/datareporting-prefs.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pref("datareporting.policy.dataSubmissionEnabled", true);
+pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
+pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
+pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);
+pref("datareporting.policy.currentPolicyVersion", 2);
+pref("datareporting.policy.minimumPolicyVersion", 1);
+pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2);
+pref("datareporting.policy.firstRunURL", "");
diff --git a/toolkit/components/telemetry/docs/collection/custom-pings.rst b/toolkit/components/telemetry/docs/collection/custom-pings.rst
new file mode 100644
index 0000000000..daad87bfe4
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/custom-pings.rst
@@ -0,0 +1,74 @@
+=======================
+Submitting custom pings
+=======================
+
+Custom pings can be submitted from JavaScript using:
+
+.. code-block:: js
+
+ TelemetryController.submitExternalPing(type, payload, options)
+
+- ``type`` - a ``string`` that is the type of the ping, limited to ``/^[a-z0-9][a-z0-9-]+[a-z0-9]$/i``.
+- ``payload`` - the actual payload data for the ping, has to be a JSON style object.
+- ``options`` - optional, an object containing additional options:
+ - ``addClientId``- whether to add the client id to the ping, defaults to ``false``
+ - ``addEnvironment`` - whether to add the environment data to the ping, defaults to ``false``
+ - ``overrideEnvironment`` - a JSON style object that overrides the environment data
+
+``TelemetryController`` will assemble a ping with the passed payload and the specified options.
+That ping will be archived locally for use with Shield and inspection in ``about:telemetry``.
+If the preferences allow upload of Telemetry pings, the ping will be uploaded at the next opportunity (this is subject to throttling, retry-on-failure, etc.).
+
+Submission constraints
+----------------------
+
+When submitting pings on shutdown, they should not be submitted after Telemetry shutdown.
+Pings should be submitted at the latest within:
+
+- the `observer notification <https://developer.mozilla.org/de/docs/Observer_Notifications#Application_shutdown>`_ ``"profile-before-change"``
+- the :ref:`AsyncShutdown phase <AsyncShutdown_phases>` ``sendTelemetry``
+
+There are other constraints that can lead to a ping submission getting dropped:
+
+- invalid ping type strings
+- invalid payload types: E.g. strings instead of objects.
+- oversized payloads: We currently only drop pings >1MB, but targetting sizes of <=10KB is recommended.
+
+Tools
+=====
+
+Helpful tools for designing new pings include:
+
+- `gzipServer <https://github.com/mozilla/gzipServer>`_ - a Python script that can run locally and receives and saves Telemetry pings. Making Firefox send to it allows inspecting outgoing pings easily.
+- ``about:telemetry`` - allows inspecting submitted pings from the local archive, including all custom ones.
+
+Designing custom pings
+======================
+
+In general, creating a new custom ping means you don't benefit automatically from the existing tooling. Further work is needed to make data show up in re:dash or other analysis tools.
+
+In addition to the `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`_, questions to guide a new pings design are:
+
+- Submission interval & triggers:
+ - What events trigger ping submission?
+ - What interval is the ping submitted in?
+ - Is there a throttling mechanism?
+ - What is the desired latency? (submitting "at least daily" still leads to certain latency tails)
+ - Are pings submitted on a clock schedule? Or based on "time since session start", "time since last ping" etc.? (I.e. will we get sharp spikes in submission volume?)
+- Size and volume:
+ - What’s the size of the submitted payload?
+ - What's the full ping size including metadata in the pipeline?
+ - What’s the target population?
+ - What's the overall estimated volume?
+- Dataset:
+ - Is it opt-out?
+ - Does it need to be opt-out?
+ - Does it need to be in a separate ping? (why can’t the data live in probes?)
+- Privacy:
+ - Is there risk to leak PII?
+ - How is that risk mitigated?
+- Data contents:
+ - Does the submitted data answer the posed product questions?
+ - Does the shape of the data allow to answer the questions efficiently?
+ - Is the data limited to whats needed to answer the questions?
+ - Does the data use common formats? (i.e. can we re-use tooling or analysis know-how)
diff --git a/toolkit/components/telemetry/docs/collection/histograms.rst b/toolkit/components/telemetry/docs/collection/histograms.rst
new file mode 100644
index 0000000000..8d0233dbfd
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/histograms.rst
@@ -0,0 +1,5 @@
+==========
+Histograms
+==========
+
+Recording into histograms is currently documented in `a MDN article <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Adding_a_new_Telemetry_probe>`_.
diff --git a/toolkit/components/telemetry/docs/collection/index.rst b/toolkit/components/telemetry/docs/collection/index.rst
new file mode 100644
index 0000000000..e4084e62ad
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/index.rst
@@ -0,0 +1,35 @@
+===============
+Data collection
+===============
+
+There are different APIs and formats to collect data in Firefox, all suiting different use cases.
+
+In general, we aim to submit data in a common format where possible. This has several advantages; from common code and tooling to sharing analysis know-how.
+
+In cases where this isn't possible and more flexibility is needed, we can submit custom pings or consider adding different data formats to existing pings.
+
+*Note:* Every new data collection must go through a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
+
+The current data collection possibilities include:
+
+* :doc:`scalars` allow recording of a single value (string, boolean, a number)
+* :doc:`histograms` can efficiently record multiple data points
+* ``environment`` data records information about the system and settings a session occurs in
+* ``TelemetryLog`` allows collecting ordered event entries (note: this does not have supporting analysis tools)
+* :doc:`measuring elapsed time <measuring-time>`
+* :doc:`custom pings <custom-pings>`
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :hidden:
+ :glob:
+
+ scalars
+ histograms
+ measuring-time
+ custom-pings
+
+Browser Usage Telemetry
+~~~~~~~~~~~~~~~~~~~~~~~
+For more information, see :ref:`browserusagetelemetry`.
diff --git a/toolkit/components/telemetry/docs/collection/measuring-time.rst b/toolkit/components/telemetry/docs/collection/measuring-time.rst
new file mode 100644
index 0000000000..918c8a85a4
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/measuring-time.rst
@@ -0,0 +1,74 @@
+======================
+Measuring elapsed time
+======================
+
+To make it easier to measure how long operations take, we have helpers for both JavaScript and C++.
+These helpers record the elapsed time into histograms, so you have to create suitable histograms for them first.
+
+From JavaScript
+===============
+JavaScript can measure elapsed time using `TelemetryStopwatch.jsm <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/TelemetryStopwatch.jsm>`_.
+
+``TelemetryStopwatch`` is a helper that simplifies recording elapsed time (in milliseconds) into histograms (plain or keyed).
+
+API:
+
+.. code-block:: js
+
+ TelemetryStopwatch = {
+ // Start, cancel & finish recording elapsed time into a histogram.
+ // |aObject| is optional. If specificied, the timer is associated with this
+ // object, so multiple time measurements can be done concurrently.
+ start(histogramId, aObject);
+ cancel(histogramId, aObject);
+ finish(histogramId, aObject);
+ // Start, cancel & finished recording elapsed time into a keyed histogram.
+ // |key| specificies the key to record into.
+ // |aObject| is optional and used as above.
+ startKeyed(histogramId, key, aObject);
+ cancelKeyed(histogramId, key, aObject);
+ finishKeyed(histogramId, key, aObject);
+ };
+
+Example:
+
+.. code-block:: js
+
+ TelemetryStopwatch.start("SAMPLE_FILE_LOAD_TIME_MS");
+ // ... start loading file.
+ if (failedToOpenFile) {
+ // Cancel this if the operation failed early etc.
+ TelemetryStopwatch.cancel("SAMPLE_FILE_LOAD_TIME_MS");
+ return;
+ }
+ // ... do more work.
+ TelemetryStopwatch.finish("SAMPLE_FILE_LOAD_TIME_MS");
+
+From C++
+========
+
+API:
+
+.. code-block:: cpp
+
+ // This helper class is the preferred way to record elapsed time.
+ template<ID id, TimerResolution res = MilliSecond>
+ class AutoTimer {
+ // Record into a plain histogram.
+ explicit AutoTimer(TimeStamp aStart = TimeStamp::Now());
+ // Record into a keyed histogram, with key |aKey|.
+ explicit AutoTimer(const nsCString& aKey,
+ TimeStamp aStart = TimeStamp::Now());
+ };
+
+ void AccumulateTimeDelta(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now());
+
+Example:
+
+.. code-block:: cpp
+
+ {
+ Telemetry::AutoTimer<Telemetry::FIND_PLUGINS> telemetry;
+ // ... scan disk for plugins.
+ }
+ // When leaving the scope, AutoTimers destructor will record the time that passed.
diff --git a/toolkit/components/telemetry/docs/collection/scalars.rst b/toolkit/components/telemetry/docs/collection/scalars.rst
new file mode 100644
index 0000000000..2c48601a4e
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/scalars.rst
@@ -0,0 +1,140 @@
+=======
+Scalars
+=======
+
+Historically we started to overload our histogram mechanism to also collect scalar data,
+such as flag values, counts, labels and others.
+The scalar measurement types are the suggested way to collect that kind of scalar data.
+We currently only support recording of scalars from the parent process.
+The serialized scalar data is submitted with the :doc:`main pings <../data/main-ping>`.
+
+The API
+=======
+Scalar probes can be managed either through the `nsITelemetry interface <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/nsITelemetry.idl>`_
+or the `C++ API <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.h>`_.
+
+JS API
+------
+Probes in privileged JavaScript code can use the following functions to manipulate scalars:
+
+.. code-block:: js
+
+ Services.telemetry.scalarAdd(aName, aValue);
+ Services.telemetry.scalarSet(aName, aValue);
+ Services.telemetry.scalarSetMaximum(aName, aValue);
+
+ Services.telemetry.keyedScalarAdd(aName, aKey, aValue);
+ Services.telemetry.keyedScalarSet(aName, aKey, aValue);
+ Services.telemetry.keyedScalarSetMaximum(aName, aKey, aValue);
+
+These functions can throw if, for example, an operation is performed on a scalar type that doesn't support it
+(e.g. calling scalarSetMaximum on a scalar of the string kind). Please look at the `code documentation <https://dxr.mozilla.org/mozilla-central/search?q=regexp%3ATelemetryScalar%3A%3A(Set%7CAdd)+file%3ATelemetryScalar.cpp&redirect=false>`_ for
+additional information.
+
+C++ API
+-------
+Probes in native code can use the more convenient helper functions declared in `Telemetry.h <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.h>`_:
+
+.. code-block:: cpp
+
+ void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
+ void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+ void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+ void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+The YAML definition file
+========================
+Scalar probes are required to be registered, both for validation and transparency reasons,
+in the `Scalars.yaml <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_
+definition file.
+
+The probes in the definition file are represented in a fixed-depth, two-level structure:
+
+.. code-block:: yaml
+
+ # The following is a group.
+ a.group.hierarchy:
+ a_probe_name:
+ kind: uint
+ ...
+ another_probe:
+ kind: string
+ ...
+ ...
+ group2:
+ probe:
+ kind: int
+ ...
+
+Group and probe names need to follow a few rules:
+
+- they cannot exceed 40 characters each;
+- group names must be alpha-numeric + ``.``, with no leading/trailing digit or ``.``;
+- probe names must be alpha-numeric + ``_``, with no leading/trailing digit or ``_``.
+
+A probe can be defined as follows:
+
+.. code-block:: yaml
+
+ a.group.hierarchy:
+ a_scalar:
+ bug_numbers:
+ - 1276190
+ description: A nice one-line description.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+Required Fields
+---------------
+
+- ``bug_numbers``: A list of unsigned integers representing the number of the bugs the probe was introduced in.
+- ``description``: A single or multi-line string describing what data the probe collects and when it gets collected.
+- ``expires``: The version number in which the scalar expires, e.g. "30"; a version number of type "N" and "N.0" is automatically converted to "N.0a1" in order to expire the scalar also in the development channels. A telemetry probe acting on an expired scalar will print a warning into the browser console. For scalars that never expire the value ``never`` can be used.
+- ``kind``: A string representing the scalar type. Allowed values are ``uint``, ``string`` and ``boolean``.
+- ``notification_emails``: A list of email addresses to notify with alerts of expiring probes. More importantly, these are used by the data steward to verify that the probe is still useful.
+
+Optional Fields
+---------------
+
+- ``cpp_guard``: A string that gets inserted as an ``#ifdef`` directive around the automatically generated C++ declaration. This is typically used for platform-specific scalars, e.g. ``ANDROID``.
+- ``release_channel_collection``: This can be either ``opt-in`` (default) or ``opt-out``. With the former the scalar is submitted by default on pre-release channels; on the release channel only if the user opted into additional data collection. With the latter the scalar is submitted by default on release and pre-release channels, unless the user opted out.
+- ``keyed``: A boolean that determines whether this is a keyed scalar. It defaults to ``False``.
+
+String type restrictions
+------------------------
+To prevent abuses, the content of a string scalar is limited to 50 characters in length. Trying
+to set a longer string will result in an error and no string being set.
+
+Keyed Scalars
+-------------
+Keyed scalars are collections of one of the available scalar types, indexed by a string key that can contain UTF8 characters and cannot be longer than 70 characters. Keyed scalars can contain up to 100 keys. This scalar type is for example useful when you want to break down certain counts by a name, like how often searches happen with which search engine.
+
+Keyed scalars should only be used if the set of keys are not known beforehand. If the keys are from a known set of strings, other options are preferred if suitable, like categorical histograms or splitting measurements up into separate scalars.
+
+The processor scripts
+=====================
+The scalar definition file is processed and checked for correctness at compile time. If it
+conforms to the specification, the processor scripts generate two C++ headers files, included
+by the Telemetry C++ core.
+
+gen-scalar-data.py
+------------------
+This script is called by the build system to generate the ``TelemetryScalarData.h`` C++ header
+file out of the scalar definitions.
+This header file contains an array holding the scalar names and version strings, in addition
+to an array of ``ScalarInfo`` structures representing all the scalars.
+
+gen-scalar-enum.py
+------------------
+This script is called by the build system to generate the ``TelemetryScalarEnums.h`` C++ header
+file out of the scalar definitions.
+This header file contains an enum class with all the scalar identifiers used to access them
+from code through the C++ API.
diff --git a/toolkit/components/telemetry/docs/concepts/archiving.rst b/toolkit/components/telemetry/docs/concepts/archiving.rst
new file mode 100644
index 0000000000..a2c57de43d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/archiving.rst
@@ -0,0 +1,12 @@
+=========
+Archiving
+=========
+
+When archiving is enabled through the relevant pref (``toolkit.telemetry.archive.enabled``), pings submitted to ``TelemetryController`` are also stored locally in the user profile directory, in ``<profile-dir>/datareporting/archived``.
+
+To allow for cheaper lookup of archived pings, storage follows a specific naming scheme for both the directory and the ping file name: `<YYYY-MM>/<timestamp>.<UUID>.<type>.jsonlz4`.
+
+* ``<YYYY-MM>`` - The subdirectory name, generated from the ping creation date.
+* ``<timestamp>`` - Timestamp of the ping creation date.
+* ``<UUID>`` - The ping identifier.
+* ``<type>`` - The ping type.
diff --git a/toolkit/components/telemetry/docs/concepts/crashes.rst b/toolkit/components/telemetry/docs/concepts/crashes.rst
new file mode 100644
index 0000000000..c9f69a23b0
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/crashes.rst
@@ -0,0 +1,23 @@
+=======
+Crashes
+=======
+
+There are many different kinds of crashes for Firefox, there is not a single system used to record all of them.
+
+Main process crashes
+====================
+
+If the Firefox main process dies, that should be recorded as an aborted session. We would submit a :doc:`main ping <../data/main-ping>` with the reason ``aborted-session``.
+If we have a crash dump for that crash, we should also submit a :doc:`crash ping <../data/crash-ping>`.
+
+The ``aborted-session`` information is first written to disk 60 seconds after startup, any earlier crashes will not trigger an ``aborted-session`` ping.
+Also, the ``aborted-session`` is updated at least every 5 minutes, so it may lag behind the last session state.
+
+Crashes during startup should be recorded in the next sessions main ping in the ``STARTUP_CRASH_DETECTED`` histogram.
+
+Child process crashes
+=====================
+
+If a Firefox plugin, content or gmplugin process dies unexpectedly, this is recorded in the main pings ``SUBPROCESS_ABNORMAL_ABORT`` keyed histogram.
+
+If we catch a crash report for this, then additionally the ``SUBPROCESS_CRASHES_WITH_DUMP`` keyed histogram is incremented.
diff --git a/toolkit/components/telemetry/docs/concepts/index.rst b/toolkit/components/telemetry/docs/concepts/index.rst
new file mode 100644
index 0000000000..a49466f8d0
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/index.rst
@@ -0,0 +1,23 @@
+========
+Concepts
+========
+
+There are common concepts used throughout Telemetry:
+
+* :doc:`pings <pings>` - the packets we use to submit data
+* :doc:`sessions & subsessions <sessions>` - how we slice a users' time in the browser
+* *measurements* - how we :doc:`collect data <../collection/index>`
+* *opt-in* & *opt-out* - the different sets of data we collect
+* :doc:`submission <submission>` - how we send data to the servers
+* :doc:`archiving <archiving>` - retaining ping data locally
+* :doc:`crashes <crashes>` - the different data crashes generate
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :glob:
+ :hidden:
+
+ pings
+ crashes
+ *
diff --git a/toolkit/components/telemetry/docs/concepts/pings.rst b/toolkit/components/telemetry/docs/concepts/pings.rst
new file mode 100644
index 0000000000..db7371b324
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/pings.rst
@@ -0,0 +1,32 @@
+.. _telemetry_pings:
+
+=====================
+Telemetry pings
+=====================
+
+A *Telemetry ping* is the data that we send to Mozillas Telemetry servers.
+
+That data is stored as a JSON object client-side and contains common information to all pings and a payload specific to a certain *ping types*.
+
+The top-level structure is defined by the :doc:`common ping format <../data/common-ping>` format.
+It contains:
+
+* some basic information shared between different ping types
+* the :doc:`environment data <../data/environment>` (optional)
+* the data specific to the *ping type*, the *payload*.
+
+Ping types
+==========
+
+We send Telemetry with different ping types. The :doc:`main <../data/main-ping>` ping is the ping that contains the bulk of the Telemetry measurements for Firefox. For more specific use-cases, we send other ping types.
+
+Pings sent from code that ships with Firefox are listed in the :doc:`data documentation <../data/index>`.
+
+Important examples are:
+
+* :doc:`main <../data/main-ping>` - contains the information collected by Telemetry (Histograms, hang stacks, ...)
+* :doc:`saved-session <../data/main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side. This is temporary and will be removed soon.
+* :doc:`crash <../data/crash-ping>` - a ping that is captured and sent after Firefox crashes.
+* ``activation`` - *planned* - sent right after installation or profile creation
+* ``upgrade`` - *planned* - sent right after an upgrade
+* :doc:`deletion <../data/deletion-ping>` - sent when FHR upload is disabled, requesting deletion of the data associated with this user
diff --git a/toolkit/components/telemetry/docs/concepts/sessions.rst b/toolkit/components/telemetry/docs/concepts/sessions.rst
new file mode 100644
index 0000000000..088556978d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/sessions.rst
@@ -0,0 +1,40 @@
+========
+Sessions
+========
+
+A *session* is the time from when Firefox starts until it shut down.
+A session can be very long-running. E.g. for Mac users that are used to always put their laptops into sleep-mode, Firefox may run for weeks.
+We slice the sessions into smaller logical units called *subsessions*.
+
+Subsessions
+===========
+
+The first subsession starts when the browser starts. After that, we split the subsession for different reasons:
+
+* ``daily``, when crossing local midnight. This keeps latency acceptable by triggering a ping at least daily for most active users.
+* ``environment-change``, when a change to the *environment* happens. This happens for important changes to the Firefox settings and when addons activate or deactivate.
+
+On a subsession split, a :doc:`main ping <../data/main-ping>` with that reason will be submitted. We store the reason in the pings payload, to see what triggered it.
+
+A session always ends with a subsession with one of two reason:
+
+* ``shutdown``, when the browser was cleanly shut down. To avoid delaying shutdown, we only save this ping to disk and send it at the next opportunity (typically the next browsing session).
+* ``aborted-session``, when the browser crashed. While Firefox is active, we write the current ``main`` ping data to disk every 5 minutes. If the browser crashes, we find this data on disk on the next start and send it with this reason.
+
+.. image:: subsession_triggers.png
+
+Subsession data
+===============
+
+A subsessions data consists of:
+
+* general information: the date the subsession started, how long it lasted, etc.
+* specific measurements: histogram & scalar data, etc.
+
+This has some advantages:
+
+* Latency - Sending a ping with all the data of a subsession immediately after it ends means we get the data from installs faster. For ``main`` pings, we aim to send a ping at least daily by starting a new subsession at local midnight.
+* Correlation - By starting new subsessions when fundamental settings change (i.e. changes to the *environment*), we can correlate a subsessions data better to those settings.
+
+
+
diff --git a/toolkit/components/telemetry/docs/concepts/submission.rst b/toolkit/components/telemetry/docs/concepts/submission.rst
new file mode 100644
index 0000000000..165917d402
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/submission.rst
@@ -0,0 +1,34 @@
+==========
+Submission
+==========
+
+*Note:* The server-side behaviour is documented in the `HTTP Edge Server specification <https://wiki.mozilla.org/CloudServices/DataPipeline/HTTPEdgeServerSpecification>`_.
+
+Pings are submitted via a common API on ``TelemetryController``.
+If a ping fails to successfully submit to the server immediately (e.g. because
+of missing internet connection), Telemetry will store it on disk and retry to
+send it until the maximum ping age is exceeded (14 days).
+
+*Note:* the :doc:`main pings <../data/main-ping>` are kept locally even after successful submission to enable the HealthReport and SelfSupport features. They will be deleted after their retention period of 180 days.
+
+Submission logic
+================
+
+Sending of pending pings starts as soon as the delayed startup is finished. They are sent in batches, newest-first, with up
+to 10 persisted pings per batch plus all unpersisted pings.
+The send logic then waits for each batch to complete.
+
+If it succeeds we trigger the next send of a ping batch. This is delayed as needed to only trigger one batch send per minute.
+
+If ping sending encounters an error that means retrying later, a backoff timeout behavior is
+triggered, exponentially increasing the timeout for the next try from 1 minute up to a limit of 120 minutes.
+Any new ping submissions and "idle-daily" events reset this behavior as a safety mechanism and trigger immediate ping sending.
+
+Status codes
+============
+
+The telemetry server team is working towards `the common services status codes <https://wiki.mozilla.org/CloudServices/DataPipeline/HTTPEdgeServerSpecification#Server_Responses>`_, but for now the following logic is sufficient for Telemetry:
+
+* `2XX` - success, don't resubmit
+* `4XX` - there was some problem with the request - the client should not try to resubmit as it would just receive the same response
+* `5XX` - there was a server-side error, the client should try to resubmit later
diff --git a/toolkit/components/telemetry/docs/concepts/subsession_triggers.png b/toolkit/components/telemetry/docs/concepts/subsession_triggers.png
new file mode 100644
index 0000000000..5717b00a93
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/subsession_triggers.png
Binary files differ
diff --git a/toolkit/components/telemetry/docs/data/addons-malware-ping.rst b/toolkit/components/telemetry/docs/data/addons-malware-ping.rst
new file mode 100644
index 0000000000..18502d7489
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/addons-malware-ping.rst
@@ -0,0 +1,42 @@
+
+Add-ons malware ping
+====================
+
+This ping is generated by an add-on created by Mozilla and shipped to users on older versions of Firefox (44-46). The ping contains information about the profile that might have been altered by a third party malicious add-on.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "malware-addon-states",
+ ...
+ clientId: <UUID>,
+ environment: { ... },
+ // Common ping data.
+ payload: {
+ // True if the blocklist was disabled at startup time.
+ blocklistDisabled: <bool>,
+ // True if the malicious add-on exists and is enabled. False if it
+ // exists and is disabled or null if the add-on was not found.
+ mainAddonActive: <bool | null>,
+ // A value of the malicious add-on block list state, or null if the
+ // add-on was not found.
+ mainAddonBlocked: <int | null>,
+ // True if a malicious user.js file was found in the profile.
+ foundUserJS: <bool>,
+ // If a malicious secmodd.db file was found the extension ID that the // file contained..
+ secmoddAddon: <string | null>, .
+ // A list of IDs for extensions which were hidden by malicious CSS.
+ hiddenAddons: [
+ <string>,
+ ...
+ ],
+ // A mapping of installed add-on IDs with known malicious
+ // update URL patterns to their exact update URLs.
+ updateURLs: {
+ <extensionID>: <updateURL>,
+ ...
+ }
+ }
+ }
diff --git a/toolkit/components/telemetry/docs/data/common-ping.rst b/toolkit/components/telemetry/docs/data/common-ping.rst
new file mode 100644
index 0000000000..445557efd5
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/common-ping.rst
@@ -0,0 +1,42 @@
+
+Common ping format
+==================
+
+This defines the top-level structure of a Telemetry ping.
+It contains basic information shared between different ping types, which enables proper storage and processing of the raw pings server-side.
+
+It also contains optional further information:
+
+* the :doc:`environment data <../data/environment>`, which contains important info to correlate the measurements against
+* the ``clientId``, a UUID identifying a profile and allowing user-oriented correlation of data
+
+*Note:* Both are not submitted with all ping types due to privacy concerns. This and the data it that can be correlated against is inspected under the `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
+
+Finally, the structure also contains the `payload`, which is the specific data submitted for the respective *ping type*.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: <string>, // "main", "activation", "deletion", "saved-session", ...
+ id: <UUID>, // a UUID that identifies this ping
+ creationDate: <ISO date>, // the date the ping was generated
+ version: <number>, // the version of the ping format, currently 4
+
+ application: {
+ architecture: <string>, // build architecture, e.g. x86
+ buildId: <string>, // "20141126041045"
+ name: <string>, // "Firefox"
+ version: <string>, // "35.0"
+ displayVersion: <string>, // "35.0b3"
+ vendor: <string>, // "Mozilla"
+ platformVersion: <string>, // "35.0"
+ xpcomAbi: <string>, // e.g. "x86-msvc"
+ channel: <string>, // "beta"
+ },
+
+ clientId: <UUID>, // optional
+ environment: { ... }, // optional, not all pings contain the environment
+ payload: { ... }, // the actual payload data for this ping type
+ }
diff --git a/toolkit/components/telemetry/docs/data/core-ping.rst b/toolkit/components/telemetry/docs/data/core-ping.rst
new file mode 100644
index 0000000000..7f38f2f7e9
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/core-ping.rst
@@ -0,0 +1,191 @@
+
+"core" ping
+============
+
+This mobile-specific ping is intended to provide the most critical
+data in a concise format, allowing for frequent uploads.
+
+Since this ping is used to measure retention, it should be sent
+each time the browser is opened.
+
+Submission will be per the Edge server specification::
+
+ /submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
+
+* ``docId`` is a UUID for deduping
+* ``docType`` is “core”
+* ``appName`` is “Fennec”
+* ``appVersion`` is the version of the application (e.g. "46.0a1")
+* ``appUpdateChannel`` is “release”, “beta”, etc.
+* ``appBuildID`` is the build number
+
+Note: Counts below (e.g. search & usage times) are “since the last
+ping”, not total for the whole application lifetime.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ "v": 7, // ping format version
+ "clientId": <string>, // client id, e.g.
+ // "c641eacf-c30c-4171-b403-f077724e848a"
+ "seq": <positive integer>, // running ping counter, e.g. 3
+ "locale": <string>, // application locale, e.g. "en-US"
+ "os": <string>, // OS name.
+ "osversion": <string>, // OS version.
+ "device": <string>, // Build.MANUFACTURER + " - " + Build.MODEL
+ // where manufacturer is truncated to 12 characters
+ // & model is truncated to 19 characters
+ "arch": <string>, // e.g. "arm", "x86"
+ "profileDate": <pos integer>, // Profile creation date in days since
+ // UNIX epoch.
+ "defaultSearch": <string>, // Identifier of the default search engine,
+ // e.g. "yahoo".
+ "distributionId": <string>, // Distribution identifier (optional)
+ "created": <string>, // date the ping was created
+ // in local time, "yyyy-mm-dd"
+ "tz": <integer>, // timezone offset (in minutes) of the
+ // device when the ping was created
+ "sessions": <integer>, // number of sessions since last upload
+ "durations": <integer>, // combined duration, in seconds, of all
+ // sessions since last upload
+ "searches": <object>, // Optional, object of search use counts in the
+ // format: { "engine.source": <pos integer> }
+ // e.g.: { "yahoo.suggestion": 3, "other.listitem": 1 }
+ "experiments": [<string>, /* … */], // Optional, array of identifiers
+ // for the active experiments
+ }
+
+Field details
+-------------
+
+device
+~~~~~~
+The ``device`` field is filled in with information specified by the hardware
+manufacturer. As such, it could be excessively long and use excessive amounts
+of limited user data. To avoid this, we limit the length of the field. We're
+more likely have collisions for models within a manufacturer (e.g. "Galaxy S5"
+vs. "Galaxy Note") than we are for shortened manufacturer names so we provide
+more characters for the model than the manufacturer.
+
+distributionId
+~~~~~~~~~~~~~~
+The ``distributionId`` contains the distribution ID as specified by
+preferences.json for a given distribution. More information on distributions
+can be found `here <https://wiki.mozilla.org/Mobile/Distribution_Files>`_.
+
+It is optional.
+
+defaultSearch
+~~~~~~~~~~~~~
+On Android, this field may be ``null``. To get the engine, we rely on
+``SearchEngineManager#getDefaultEngine``, which searches in several places in
+order to find the search engine identifier:
+
+* Shared Preferences
+* The distribution (if it exists)
+* The localized default engine
+
+If the identifier could not be retrieved, this field is ``null``. If the
+identifier is retrieved, we attempt to create an instance of the search
+engine from the search plugins (in order):
+
+* In the distribution
+* From the localized plugins shipped with the browser
+* The third-party plugins that are installed in the profile directory
+
+If the plugins fail to create a search engine instance, this field is also
+``null``.
+
+This field can also be ``null`` when a custom search engine is set as the
+default.
+
+sessions & durations
+~~~~~~~~~~~~~~~~~~~~
+On Android, a session is the time when Firefox is focused in the foreground.
+`sessions` tracks the number of sessions since the last upload and
+`durations` is the accumulated duration in seconds of all of these
+sessions. Note that showing a dialog (including a Firefox dialog) will
+take Firefox out of focus & end the current session.
+
+An implementation that records a session when Firefox is completely hidden is
+preferrable (e.g. to avoid the dialog issue above), however, it's more complex
+to implement and so we chose not to, at least for the initial implementation.
+
+profileDate
+~~~~~~~~~~~
+On Android, this value is created at profile creation time and retrieved or,
+for legacy profiles, taken from the package install time (note: this is not the
+same exact metric as profile creation time but we compromised in favor of ease
+of implementation).
+
+Additionally on Android, this field may be ``null`` in the unlikely event that
+all of the following events occur:
+
+#. The times.json file does not exist
+#. The package install date could not be persisted to disk
+
+The reason we don't just return the package install time even if the date could
+not be persisted to disk is to ensure the value doesn't change once we start
+sending it: we only want to send consistent values.
+
+searches
+~~~~~~~~
+In the case a search engine is added by a user, the engine identifier "other" is used, e.g. "other.<source>".
+
+Sources in Android are based on the existing UI telemetry values and are as
+follows:
+
+* actionbar: the user types in the url bar and hits enter to use the default
+ search engine
+* listitem: the user selects a search engine from the list of secondary search
+ engines at the bottom of the screen
+* suggestion: the user clicks on a search suggestion or, in the case that
+ suggestions are disabled, the row corresponding with the main engine
+
+Other parameters
+----------------
+
+HTTP "Date" header
+~~~~~~~~~~~~~~~~~~
+This header is used to track the submission date of the core ping in the format
+specified by
+`rfc 2616 sec 14.18 <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18>`_,
+et al (e.g. "Tue, 01 Feb 2011 14:00:00 GMT").
+
+
+Version history
+---------------
+* v7: added ``sessionCount`` & ``sessionDuration``
+* v6: added ``searches``
+* v5: added ``created`` & ``tz``
+* v4: ``profileDate`` will return package install time when times.json is not available
+* v3: added ``defaultSearch``
+* v2: added ``distributionId``
+* v1: initial version
+
+Notes
+~~~~~
+
+* ``distributionId`` (v2) actually landed after ``profileDate`` (v4) but was
+ uplifted to 46, whereas ``profileDate`` landed on 47. The version numbers in
+ code were updated to be increasing (bug 1264492) and the version history docs
+ rearranged accordingly.
+
+Android implementation notes
+----------------------------
+On Android, the uploader has a high probability of delivering the complete data
+for a given client but not a 100% probability. This was a conscious decision to
+keep the code simple. The cases where we can lose data:
+
+* Resetting the field measurements (including incrementing the sequence number)
+ and storing a ping for upload are not atomic. Android can kill our process
+ for memory pressure in between these distinct operations so we can just lose
+ a ping's worth of data. That sequence number will be missing on the server.
+* If we exceed some number of pings on disk that have not yet been uploaded,
+ we remove old pings to save storage space. For those pings, we will lose
+ their data and their sequence numbers will be missing on the server.
+
+Note: we never expect to drop data without also dropping a sequence number so
+we are able to determine when data loss occurs.
diff --git a/toolkit/components/telemetry/docs/data/crash-ping.rst b/toolkit/components/telemetry/docs/data/crash-ping.rst
new file mode 100644
index 0000000000..3cdbc6030b
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/crash-ping.rst
@@ -0,0 +1,144 @@
+
+"crash" ping
+============
+
+This ping is captured after the main Firefox process crashes, whether or not the crash report is submitted to crash-stats.mozilla.org. It includes non-identifying metadata about the crash.
+
+The environment block that is sent with this ping varies: if Firefox was running long enough to record the environment block before the crash, then the environment at the time of the crash will be recorded and ``hasCrashEnvironment`` will be true. If Firefox crashed before the environment was recorded, ``hasCrashEnvironment`` will be false and the recorded environment will be the environment at time of submission.
+
+The client ID is submitted with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 1,
+ type: "crash",
+ ... common ping data
+ clientId: <UUID>,
+ environment: { ... },
+ payload: {
+ crashDate: "YYYY-MM-DD",
+ sessionId: <UUID>, // may be missing for crashes that happen early
+ // in startup. Added in Firefox 48 with the
+ // intention of uplifting to Firefox 46
+ crashId: <UUID>, // Optional, ID of the associated crash
+ stackTraces: { ... }, // Optional, see below
+ metadata: { // Annotations saved while Firefox was running. See nsExceptionHandler.cpp for more information
+ ProductName: "Firefox",
+ ReleaseChannel: <channel>,
+ Version: <version number>,
+ BuildID: "YYYYMMDDHHMMSS",
+ AvailablePageFile: <size>, // Windows-only, available paging file
+ AvailablePhysicalMemory: <size>, // Windows-only, available physical memory
+ AvailableVirtualMemory: <size>, // Windows-only, available virtual memory
+ BlockedDllList: <list>, // Windows-only, see WindowsDllBlocklist.cpp for details
+ BlocklistInitFailed: 1, // Windows-only, present only if the DLL blocklist initialization failed
+ CrashTime: <time>, // Seconds since the Epoch
+ ContainsMemoryReport: 1, // Optional
+ EventLoopNestingLevel: <levels>, // Optional, present only if >0
+ IsGarbageCollecting: 1, // Optional, present only if set to 1
+ MozCrashReason: <reason>, // Optional, contains the string passed to MOZ_CRASH()
+ OOMAllocationSize: <size>, // Size of the allocation that caused an OOM
+ SecondsSinceLastCrash: <duration>, // Seconds elapsed since the last crash occurred
+ SystemMemoryUsePercentage: <percentage>, // Windows-only, percent of memory in use
+ TelemetrySessionId: <id>, // Active telemetry session ID when the crash was recorded
+ TextureUsage: <usage>, // Optional, usage of texture memory in bytes
+ TotalPageFile: <size>, // Windows-only, paging file in use
+ TotalPhysicalMemory: <size>, // Windows-only, physical memory in use
+ TotalVirtualMemory: <size>, // Windows-only, virtual memory in use
+ UptimeTS: <duration>, // Seconds since Firefox was started
+ User32BeforeBlocklist: 1, // Windows-only, present only if user32.dll was loaded before the DLL blocklist has been initialized
+ },
+ hasCrashEnvironment: bool
+ }
+ }
+
+Stack Traces
+------------
+
+The crash ping may contain a ``stackTraces`` field which has been populated
+with stack traces for all threads in the crashed process. The format of this
+field is similar to the one used by Socorro for representing a crash. The main
+differences are that redundant fields are not stored and that the module a
+frame belongs to is referenced by index in the module array rather than by its
+file name.
+
+Note that this field does not contain data from the application; only bare
+stack traces and module lists are stored.
+
+.. code-block:: js
+
+ {
+ status: <string>, // Status of the analysis, "OK" or an error message
+ crash_info: { // Basic crash information
+ type: <string>, // Type of crash, SIGSEGV, assertion, etc...
+ address: <addr>, // Crash address crash, hex format, see the notes below
+ crashing_thread: <index> // Index in the thread array below
+ },
+ main_module: <index>, // Index of Firefox' executable in the module list
+ modules: [{
+ base_addr: <addr>, // Base address of the module, hex format
+ end_addr: <addr>, // End address of the module, hex format
+ code_id: <string>, // Unique ID of this module, see the notes below
+ debug_file: <string>, // Name of the file holding the debug information
+ debug_id: <string>, // ID or hash of the debug information file
+ filename: <string>, // File name
+ version: <string>, // Library/executable version
+ },
+ ... // List of modules ordered by base memory address
+ ],
+ threads: [{ // Stack traces for every thread
+ frames: [{
+ module_index: <index>, // Index of the module this frame belongs to
+ ip: <ip>, // Program counter, hex format
+ trust: <string> // Trust of this frame, see the notes below
+ },
+ ... // List of frames, the first frame is the topmost
+ ]
+ }]
+ }
+
+Notes
+~~~~~
+
+Memory addresses and instruction pointers are always stored as strings in
+hexadecimal format (e.g. "0x4000"). They can be made of up to 16 characters for
+64-bit addresses.
+
+The crash type is both OS and CPU dependent and can be either a descriptive
+string (e.g. SIGSEGV, EXCEPTION_ACCESS_VIOLATION) or a raw numeric value. The
+crash address meaning depends on the type of crash. In a segmentation fault the
+crash address will be the memory address whose access caused the fault; in a
+crash triggered by an illegal instruction exception the address will be the
+instruction pointer where the invalid instruction resides.
+See `breakpad <https://chromium.googlesource.com/breakpad/breakpad/+/c99d374dde62654a024840accfb357b2851daea0/src/processor/minidump_processor.cc#675>`_'s
+relevant code for further information.
+
+Since it's not always possible to establish with certainty the address of the
+previous frame while walking the stack, every frame has a trust value that
+represents how it was found and thus how certain we are that it's a real frame.
+The trust levels are (from least trusted to most trusted):
+
++---------------+---------------------------------------------------+
+| Trust | Description |
++===============+===================================================+
+| context | Given as instruction pointer in a context |
++---------------+---------------------------------------------------+
+| prewalked | Explicitly provided by some external stack walker |
++---------------+---------------------------------------------------+
+| cfi | Derived from call frame info |
++---------------+---------------------------------------------------+
+| frame_pointer | Derived from frame pointer |
++---------------+---------------------------------------------------+
+| cfi_scan | Found while scanning stack using call frame info |
++---------------+---------------------------------------------------+
+| scan | Scanned the stack, found this |
++---------------+---------------------------------------------------+
+| none | Unknown, this is most likely not a valid frame |
++---------------+---------------------------------------------------+
+
+The ``code_id`` field holds a unique ID used to distinguish between different
+versions and builds of the same module. See `breakpad <https://chromium.googlesource.com/breakpad/breakpad/+/24f5931c5e0120982c0cbf1896641e3ef2bdd52f/src/google_breakpad/processor/code_module.h#60>`_'s
+description for further information. This field is populated only on Windows.
diff --git a/toolkit/components/telemetry/docs/data/deletion-ping.rst b/toolkit/components/telemetry/docs/data/deletion-ping.rst
new file mode 100644
index 0000000000..c4523ce540
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/deletion-ping.rst
@@ -0,0 +1,19 @@
+
+"deletion" ping
+===============
+
+This ping is generated when a user turns off FHR upload from the Preferences panel, changing the related ``datareporting.healthreport.uploadEnabled`` preference. This requests that all associated data from that user be deleted.
+
+This ping contains the client id and no environment data.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+ type: "deletion",
+ ... common ping data
+ clientId: <UUID>,
+ payload: { }
+ } \ No newline at end of file
diff --git a/toolkit/components/telemetry/docs/data/environment.rst b/toolkit/components/telemetry/docs/data/environment.rst
new file mode 100644
index 0000000000..ff0d204a48
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/environment.rst
@@ -0,0 +1,373 @@
+
+Environment
+===========
+
+The environment consists of data that is expected to be characteristic for performance and other behavior and not expected to change too often.
+
+Changes to most of these data points are detected (where possible and sensible) and will lead to a session split in the :doc:`main-ping`.
+The environment data may also be submitted by other ping types.
+
+*Note:* This is not submitted with all ping types due to privacy concerns. This and other data is inspected under the `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
+
+Some parts of the environment must be fetched asynchronously at startup. We don't want other Telemetry components to block on waiting for the environment, so some items may be missing from it until the async fetching finished.
+This currently affects the following sections:
+
+- profile
+- addons
+
+
+Structure:
+
+.. code-block:: js
+
+ {
+ build: {
+ applicationId: <string>, // nsIXULAppInfo.ID
+ applicationName: <string>, // "Firefox"
+ architecture: <string>, // e.g. "x86", build architecture for the active build
+ architecturesInBinary: <string>, // e.g. "i386-x86_64", from nsIMacUtils.architecturesInBinary, only present for mac universal builds
+ buildId: <string>, // e.g. "20141126041045"
+ version: <string>, // e.g. "35.0"
+ vendor: <string>, // e.g. "Mozilla"
+ platformVersion: <string>, // e.g. "35.0"
+ xpcomAbi: <string>, // e.g. "x86-msvc"
+ hotfixVersion: <string>, // e.g. "20141211.01"
+ },
+ settings: {
+ addonCompatibilityCheckEnabled: <bool>, // Whether application compatibility is respected for add-ons
+ blocklistEnabled: <bool>, // true on failure
+ isDefaultBrowser: <bool>, // null on failure, not available on Android
+ defaultSearchEngine: <string>, // e.g. "yahoo"
+ defaultSearchEngineData: {, // data about the current default engine
+ name: <string>, // engine name, e.g. "Yahoo"; or "NONE" if no default
+ loadPath: <string>, // where the engine line is located; missing if no default
+ origin: <string>, // 'default', 'verified', 'unverified', or 'invalid'; based on the presence and validity of the engine's loadPath verification hash.
+ submissionURL: <string> // missing if no default or for user-installed engines
+ },
+ searchCohort: <string>, // optional, contains an identifier for any active search A/B experiments
+ e10sEnabled: <bool>, // whether e10s is on, i.e. browser tabs open by default in a different process
+ e10sCohort: <string>, // which e10s cohort was assigned for this user
+ telemetryEnabled: <bool>, // false on failure
+ locale: <string>, // e.g. "it", null on failure
+ update: {
+ channel: <string>, // e.g. "release", null on failure
+ enabled: <bool>, // true on failure
+ autoDownload: <bool>, // true on failure
+ },
+ userPrefs: {
+ // Only prefs which are changed from the default value are listed
+ // in this block
+ "pref.name.value": value // some prefs send the value
+ "pref.name.url": "<user-set>" // For some privacy-sensitive prefs
+ // only the fact that the value has been changed is recorded
+ },
+ attribution: { // optional, only present if the installation has attribution data
+ // all of these values are optional.
+ source: <string>, // referring partner domain, when install happens via a known partner
+ medium: <string>, // category of the source, such as "organic" for a search engine
+ campaign: <string>, // identifier of the particular campaign that led to the download of the product
+ content: <string>, // identifier to indicate the particular link within a campaign
+ },
+ },
+ profile: {
+ creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
+ resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
+ },
+ partner: { // This section may not be immediately available on startup
+ distributionId: <string>, // pref "distribution.id", null on failure
+ distributionVersion: <string>, // pref "distribution.version", null on failure
+ partnerId: <string>, // pref mozilla.partner.id, null on failure
+ distributor: <string>, // pref app.distributor, null on failure
+ distributorChannel: <string>, // pref app.distributor.channel, null on failure
+ partnerNames: [
+ // list from prefs app.partner.<name>=<name>
+ ],
+ },
+ system: {
+ memoryMB: <number>,
+ virtualMaxMB: <number>, // windows-only
+ isWow64: <bool>, // windows-only
+ cpu: {
+ count: <number>, // desktop only, e.g. 8, or null on failure - logical cpus
+ cores: <number>, // desktop only, e.g., 4, or null on failure - physical cores
+ vendor: <string>, // desktop only, e.g. "GenuineIntel", or null on failure
+ family: <number>, // desktop only, null on failure
+ model: <number, // desktop only, null on failure
+ stepping: <number>, // desktop only, null on failure
+ l2cacheKB: <number>, // L2 cache size in KB, only on windows & mac
+ l3cacheKB: <number>, // desktop only, L3 cache size in KB
+ speedMHz: <number>, // desktop only, cpu clock speed in MHz
+ extensions: [
+ <string>,
+ ...
+ // as applicable:
+ // "MMX", "SSE", "SSE2", "SSE3", "SSSE3", "SSE4A", "SSE4_1",
+ // "SSE4_2", "AVX", "AVX2", "EDSP", "ARMv6", "ARMv7", "NEON"
+ ],
+ },
+ device: { // This section is only available on mobile devices.
+ model: <string>, // the "device" from FHR, null on failure
+ manufacturer: <string>, // null on failure
+ hardware: <string>, // null on failure
+ isTablet: <bool>, // null on failure
+ },
+ os: {
+ name: <string>, // "Windows_NT" or null on failure
+ version: <string>, // e.g. "6.1", null on failure
+ kernelVersion: <string>, // android/b2g only or null on failure
+ servicePackMajor: <number>, // windows only or null on failure
+ servicePackMinor: <number>, // windows only or null on failure
+ windowsBuildNumber: <number>, // windows 10 only or null on failure
+ windowsUBR: <number>, // windows 10 only or null on failure
+ installYear: <number>, // windows only or null on failure
+ locale: <string>, // "en" or null on failure
+ },
+ hdd: {
+ profile: { // hdd where the profile folder is located
+ model: <string>, // windows only or null on failure
+ revision: <string>, // windows only or null on failure
+ },
+ binary: { // hdd where the application binary is located
+ model: <string>, // windows only or null on failure
+ revision: <string>, // windows only or null on failure
+ },
+ system: { // hdd where the system files are located
+ model: <string>, // windows only or null on failure
+ revision: <string>, // windows only or null on failure
+ },
+ },
+ gfx: {
+ D2DEnabled: <bool>, // null on failure
+ DWriteEnabled: <bool>, // null on failure
+ //DWriteVersion: <string>, // temporarily removed, pending bug 1154500
+ adapters: [
+ {
+ description: <string>, // e.g. "Intel(R) HD Graphics 4600", null on failure
+ vendorID: <string>, // null on failure
+ deviceID: <string>, // null on failure
+ subsysID: <string>, // null on failure
+ RAM: <number>, // in MB, null on failure
+ driver: <string>, // null on failure
+ driverVersion: <string>, // null on failure
+ driverDate: <string>, // null on failure
+ GPUActive: <bool>, // currently always true for the first adapter
+ },
+ ...
+ ],
+ // Note: currently only added on Desktop. On Linux, only a single
+ // monitor is returned representing the entire virtual screen.
+ monitors: [
+ {
+ screenWidth: <number>, // screen width in pixels
+ screenHeight: <number>, // screen height in pixels
+ refreshRate: <number>, // refresh rate in hertz (present on Windows only).
+ // (values <= 1 indicate an unknown value)
+ pseudoDisplay: <bool>, // networked screen (present on Windows only)
+ scale: <number>, // backing scale factor (present on Mac only)
+ },
+ ...
+ ],
+ features: {
+ compositor: <string>, // Layers backend for compositing (eg "d3d11", "none", "opengl")
+
+ // Each the following features can have one of the following statuses:
+ // "unused" - This feature has not been requested.
+ // "unavailable" - Safe Mode or OS restriction prevents use.
+ // "blocked" - Blocked due to an internal condition such as safe mode.
+ // "blacklisted" - Blocked due to a blacklist restriction.
+ // "disabled" - User explicitly disabled this default feature.
+ // "failed" - This feature was attempted but failed to initialize.
+ // "available" - User has this feature available.
+ "d3d11" { // This feature is Windows-only.
+ status: <string>,
+ warp: <bool>, // Software rendering (WARP) mode was chosen.
+ textureSharing: <bool> // Whether or not texture sharing works.
+ version: <number>, // The D3D11 device feature level.
+ blacklisted: <bool>, // Whether D3D11 is blacklisted; use to see whether WARP
+ // was blacklist induced or driver-failure induced.
+ },
+ "d2d" { // This feature is Windows-only.
+ status: <string>,
+ version: <string>, // Either "1.0" or "1.1".
+ },
+ },
+ },
+ },
+ addons: {
+ activeAddons: { // the currently enabled addons
+ <addon id>: {
+ blocklisted: <bool>,
+ description: <string>, // null if not available
+ name: <string>,
+ userDisabled: <bool>,
+ appDisabled: <bool>,
+ version: <string>,
+ scope: <integer>,
+ type: <string>, // "extension", "service", ...
+ foreignInstall: <bool>,
+ hasBinaryComponents: <bool>
+ installDay: <number>, // days since UNIX epoch, 0 on failure
+ updateDay: <number>, // days since UNIX epoch, 0 on failure
+ signedState: <integer>, // whether the add-on is signed by AMO, only present for extensions
+ isSystem: <bool>, // true if this is a System Add-on
+ },
+ ...
+ },
+ theme: { // the active theme
+ id: <string>,
+ blocklisted: <bool>,
+ description: <string>,
+ name: <string>,
+ userDisabled: <bool>,
+ appDisabled: <bool>,
+ version: <string>,
+ scope: <integer>,
+ foreignInstall: <bool>,
+ hasBinaryComponents: <bool>
+ installDay: <number>, // days since UNIX epoch, 0 on failure
+ updateDay: <number>, // days since UNIX epoch, 0 on failure
+ },
+ activePlugins: [
+ {
+ name: <string>,
+ version: <string>,
+ description: <string>,
+ blocklisted: <bool>,
+ disabled: <bool>,
+ clicktoplay: <bool>,
+ mimeTypes: [<string>, ...],
+ updateDay: <number>, // days since UNIX epoch, 0 on failure
+ },
+ ...
+ ],
+ activeGMPlugins: {
+ <gmp id>: {
+ version: <string>,
+ userDisabled: <bool>,
+ applyBackgroundUpdates: <integer>,
+ },
+ ...
+ },
+ activeExperiment: { // section is empty if there's no active experiment
+ id: <string>, // id
+ branch: <string>, // branch name
+ },
+ persona: <string>, // id of the current persona, null on GONK
+ },
+ }
+
+build
+-----
+
+buildId
+~~~~~~~
+Firefox builds downloaded from mozilla.org use a 14-digit buildId. Builds included in other distributions may have a different format (e.g. only 10 digits).
+
+Settings
+--------
+
+defaultSearchEngine
+~~~~~~~~~~~~~~~~~~~
+Note: Deprecated, use defaultSearchEngineData instead.
+
+Contains the string identifier or name of the default search engine provider. This will not be present in environment data collected before the Search Service initialization.
+
+The special value ``NONE`` could occur if there is no default search engine.
+
+The special value ``UNDEFINED`` could occur if a default search engine exists but its identifier could not be determined.
+
+This field's contents are ``Services.search.defaultEngine.identifier`` (if defined) or ``"other-"`` + ``Services.search.defaultEngine.name`` if not. In other words, search engines without an ``.identifier`` are prefixed with ``other-``.
+
+defaultSearchEngineData
+~~~~~~~~~~~~~~~~~~~~~~~
+Contains data identifying the engine currently set as the default.
+
+The object contains:
+
+- a ``name`` property with the name of the engine, or ``NONE`` if no
+ engine is currently set as the default.
+
+- a ``loadPath`` property: an anonymized path of the engine xml file, e.g.
+ jar:[app]/omni.ja!browser/engine.xml
+ (where 'browser' is the name of the chrome package, not a folder)
+ [profile]/searchplugins/engine.xml
+ [distribution]/searchplugins/common/engine.xml
+ [other]/engine.xml
+
+- an ``origin`` property: the value will be ``default`` for engines that are built-in or from distribution partners, ``verified`` for user-installed engines with valid verification hashes, ``unverified`` for non-default engines without verification hash, and ``invalid`` for engines with broken verification hashes.
+
+- a ``submissionURL`` property with the HTTP url we would use to search.
+ For privacy, we don't record this for user-installed engines.
+
+``loadPath`` and ``submissionURL`` are not present if ``name`` is ``NONE``.
+
+searchCohort
+~~~~~~~~~~~~
+
+If the user has been enrolled into a search default change experiment, this contains the string identifying the experiment the user is taking part in. Most user profiles will never be part of any search default change experiment, and will not send this value.
+
+userPrefs
+~~~~~~~~~
+
+This object contains user preferences.
+
+Each key in the object is the name of a preference. A key's value depends on the policy with which the preference was collected. There are two such policies, "value" and "state". For preferences collected under the "value" policy, the value will be the preference's value. For preferences collected under the "state" policy, the value will be an opaque marker signifying only that the preference has a user value. The "state" policy is therefore used when user privacy is a concern.
+
+The following is a partial list of collected preferences.
+
+- ``browser.search.suggest.enabled``: The "master switch" for search suggestions everywhere in Firefox (search bar, urlbar, etc.). Defaults to true.
+
+- ``browser.urlbar.suggest.searches``: True if search suggestions are enabled in the urlbar. Defaults to false.
+
+- ``browser.urlbar.userMadeSearchSuggestionsChoice``: True if the user has clicked Yes or No in the urlbar's opt-in notification. Defaults to false.
+
+- ``browser.zoom.full``: True if zoom is enabled for both text and images, that is if "Zoom Text Only" is not enabled. Defaults to true. Collection of this preference has been enabled in Firefox 50 and will be disabled again in Firefox 53 (`Bug 979323 <https://bugzilla.mozilla.org/show_bug.cgi?id=979323>`_).
+
+- ``security.sandbox.content.level``: The meanings of the values are OS dependent, but 0 means not sandboxed for all OS. Details of the meanings can be found in the `Firefox prefs file <http://hg.mozilla.org/mozilla-central/file/tip/browser/app/profile/firefox.js>`_.
+
+attribution
+~~~~~~~~~~~
+
+This object contains the attribution data for the product installation.
+
+Attribution data is used to link installations of Firefox with the source that the user arrived at the Firefox download page from. It would indicate, for instance, when a user executed a web search for Firefox and arrived at the download page from there, directly navigated to the site, clicked on a link from a particular social media campaign, etc.
+
+The attribution data is included in some versions of the default Firefox installer for Windows (the "stub" installer) and stored as part of the installation. All platforms other than Windows and also Windows installations that did not use the stub installer do not have this data and will not include the ``attribution`` object.
+
+partner
+-------
+
+If the user is using a partner repack, this contains information identifying the repack being used, otherwise "partnerNames" will be an empty array and other entries will be null. The information may be missing when the profile just becomes available. In Firefox for desktop, the information along with other customizations defined in distribution.ini are processed later in the startup phase, and will be fully applied when "distribution-customization-complete" notification is sent.
+
+Distributions are most reliably identified by the ``distributionId`` field. Partner information can be found in the `partner repacks <https://github.com/mozilla-partners>`_ (`the old one <http://hg.mozilla.org/build/partner-repacks/>`_ is deprecated): it contains one private repository per partner.
+Important values for ``distributionId`` include:
+
+- "MozillaOnline" for the Mozilla China repack.
+- "canonical", for the `Ubuntu Firefox repack <http://bazaar.launchpad.net/~mozillateam/firefox/firefox.trusty/view/head:/debian/distribution.ini>`_.
+- "yandex", for the Firefox Build by Yandex.
+
+system
+------
+
+os
+~~
+
+This object contains operating system information.
+
+- ``name``: the name of the OS.
+- ``version``: a string representing the OS version.
+- ``kernelVersion``: an Android/B2G only string representing the kernel version.
+- ``servicePackMajor``: the Windows only major version number for the installed service pack.
+- ``servicePackMinor``: the Windows only minor version number for the installed service pack.
+- ``windowsBuildNumber``: the Windows build number, only available for Windows >= 10.
+- ``windowsUBR``: the Windows UBR number, only available for Windows >= 10. This value is incremented by Windows cumulative updates patches.
+- ``installYear``: the Windows only integer representing the year the OS was installed.
+- ``locale``: the string representing the OS locale.
+
+addons
+------
+
+activeAddons
+~~~~~~~~~~~~
+
+Starting from Firefox 44, the length of the following string fields: ``name``, ``description`` and ``version`` is limited to 100 characters. The same limitation applies to the same fields in ``theme`` and ``activePlugins``.
diff --git a/toolkit/components/telemetry/docs/data/heartbeat-ping.rst b/toolkit/components/telemetry/docs/data/heartbeat-ping.rst
new file mode 100644
index 0000000000..413da0376d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/heartbeat-ping.rst
@@ -0,0 +1,63 @@
+
+"heartbeat" ping
+=================
+
+This ping is submitted after a Firefox Heartbeat survey. Even if the user exits
+the browser, closes the survey window, or ignores the survey, Heartbeat will
+provide a ping to Telemetry for sending during the same session.
+
+The payload contains the user's survey response (if any) as well as timestamps
+of various Heartbeat events (survey shown, survey closed, link clicked, etc).
+
+The ping will also report the "surveyId", "surveyVersion" and "testing"
+Heartbeat survey parameters (if they are present in the survey config).
+These "meta fields" will be repeated verbatim in the payload section.
+
+The environment block and client ID are submitted with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "heartbeat",
+ version: 4,
+ clientId: <UUID>,
+ environment: { /* ... */ }
+ // ... common ping data
+ payload: {
+ version: 1,
+ flowId: <string>,
+ ... timestamps below ...
+ offeredTS: <integer epoch timestamp>,
+ learnMoreTS: <integer epoch timestamp>,
+ votedTS: <integer epoch timestamp>,
+ engagedTS: <integer epoch timestamp>,
+ closedTS: <integer epoch timestamp>,
+ expiredTS: <integer epoch timestamp>,
+ windowClosedTS: <integer epoch timestamp>,
+ // ... user's rating below
+ score: <integer>,
+ // ... survey meta fields below
+ surveyId: <string>,
+ surveyVersion: <integer>,
+ testing: <boolean>
+ }
+ }
+
+Notes:
+
+* Pings will **NOT** have all possible timestamps, timestamps are only reported for events that actually occurred.
+* Timestamp meanings:
+ * offeredTS: when the survey was shown to the user
+ * learnMoreTS: when the user clicked on the "Learn More" link
+ * votedTS: when the user voted
+ * engagedTS: when the user clicked on the survey-provided button (alternative to voting feature)
+ * closedTS: when the Heartbeat notification bar was closed
+ * expiredTS: indicates that the survey expired after 2 hours of no interaction (threshold regulated by "browser.uitour.surveyDuration" pref)
+ * windowClosedTS: the user closed the entire Firefox window containing the survey, thus ending the survey. This timestamp will also be reported when the survey is ended by the browser being shut down.
+* The surveyId/surveyVersion fields identify a specific survey (like a "1040EZ" tax paper form). The flowID is a UUID that uniquely identifies a single user's interaction with the survey. Think of it as a session token.
+* The self-support page cannot include additional data in this payload. Only the the 4 flowId/surveyId/surveyVersion/testing fields are under the self-support page's control.
+
+See also: :doc:`common ping fields <common-ping>`
+
diff --git a/toolkit/components/telemetry/docs/data/index.rst b/toolkit/components/telemetry/docs/data/index.rst
new file mode 100644
index 0000000000..a0467e9a12
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/index.rst
@@ -0,0 +1,18 @@
+==================
+Data documentation
+==================
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :glob:
+
+ common-ping
+ environment
+ main-ping
+ deletion-ping
+ crash-ping
+ *-ping
+ addons-malware-ping
+
+The `mozilla-pipeline-schemas repository <https://github.com/mozilla-services/mozilla-pipeline-schemas/>`_ contains schemas for some of the pings.
diff --git a/toolkit/components/telemetry/docs/data/main-ping.rst b/toolkit/components/telemetry/docs/data/main-ping.rst
new file mode 100644
index 0000000000..445090af92
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/main-ping.rst
@@ -0,0 +1,609 @@
+
+"main" ping
+===========
+
+.. toctree::
+ :maxdepth: 2
+
+This is the "main" Telemetry ping type, whose payload contains most of the measurements that are used to track the performance and health of Firefox in the wild.
+It includes the histograms and other performance and diagnostic data.
+
+This ping is triggered by different scenarios, which is documented by the ``reason`` field:
+
+* ``aborted-session`` - this ping is regularly saved to disk (every 5 minutes), overwriting itself, and deleted at shutdown. If a previous aborted session ping is found at startup, it gets sent to the server. The first aborted-session ping is generated as soon as Telemetry starts
+* ``environment-change`` - the :doc:`environment` changed, so the session measurements got reset and a new subsession starts
+* ``shutdown`` - triggered when the browser session ends
+* ``daily`` - a session split triggered in 24h hour intervals at local midnight. If an ``environment-change`` ping is generated by the time it should be sent, the daily ping is rescheduled for the next midnight
+* ``saved-session`` - the *"classic"* Telemetry payload with measurements covering the whole browser session (only submitted for a transition period)
+
+Most reasons lead to a session split, initiating a new *subsession*. We reset important measurements for those subsessions.
+
+After a new subsession split, the ``internal-telemetry-after-subsession-split`` topic is notified to all the observers. *This is an internal topic and is only meant for internal Telemetry usage.*
+
+*Note:* ``saved-session`` is sent with a different ping type (``saved-session``, not ``main``), but otherwise has the same format as discussed here.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+
+ info: {
+ reason: <string>, // what triggered this ping: "saved-session", "environment-change", "shutdown", ...
+ revision: <string>, // the Histograms.json revision
+ timezoneOffset: <integer>, // time-zone offset from UTC, in minutes, for the current locale
+ previousBuildId: <string>, // null if this is the first run, or the previous build ID is unknown
+
+ sessionId: <uuid>, // random session id, shared by subsessions
+ subsessionId: <uuid>, // random subsession id
+ previousSessionId: <uuid>, // session id of the previous session, null on first run.
+ previousSubsessionId: <uuid>, // subsession id of the previous subsession (even if it was in a different session),
+ // null on first run.
+
+ subsessionCounter: <unsigned integer>, // the running no. of this subsession since the start of the browser session
+ profileSubsessionCounter: <unsigned integer>, // the running no. of all subsessions for the whole profile life time
+
+ sessionStartDate: <ISO date>, // daily precision
+ subsessionStartDate: <ISO date>, // daily precision, ISO date in local time
+ sessionLength: <integer>, // the session length until now in seconds, monotonic
+ subsessionLength: <integer>, // the subsession length in seconds, monotonic
+
+ flashVersion: <string>, // obsolete, use ``environment.addons.activePlugins``
+ addons: <string>, // obsolete, use ``environment.addons``
+ },
+
+ processes: {...},
+ childPayloads: [...], // only present with e10s; reduced payloads from content processes, null on failure
+ simpleMeasurements: {...},
+
+ // The following properties may all be null if we fail to collect them.
+ histograms: {...},
+ keyedHistograms: {...},
+ chromeHangs: {...},
+ threadHangStats: [...],
+ log: [...],
+ webrtc: {...},
+ gc: {...},
+ fileIOReports: {...},
+ lateWrites: {...},
+ addonDetails: {...},
+ addonHistograms: {...},
+ UIMeasurements: [...],
+ slowSQL: {...},
+ slowSQLstartup: {...},
+ }
+
+info
+----
+
+sessionLength
+~~~~~~~~~~~~~
+The length of the current session so far in seconds.
+This uses a monotonic clock, so this may mismatch with other measurements that
+are not monotonic like calculations based on ``Date.now()``.
+
+If the monotonic clock failed, this will be ``-1``.
+
+subsessionLength
+~~~~~~~~~~~~~~~~
+The length of this subsession in seconds.
+This uses a monotonic clock, so this may mismatch with other measurements that are not monotonic (e.g. based on Date.now()).
+
+If ``sessionLength`` is ``-1``, the monotonic clock is not working.
+
+processes
+---------
+This section contains per-process data.
+
+Structure:
+
+.. code-block:: js
+
+ "processes" : {
+ ... other processes ...
+ "parent": {
+ scalars: {...},
+ },
+ "content": {
+ histograms: {...},
+ keyedHistograms: {...},
+ },
+ }
+
+histograms and keyedHistograms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This section contains histograms and keyed histograms accumulated on content processes. Histograms recorded on a content child process have different character than parent histograms. For instance, ``GC_MS`` will be much different in ``processes.content`` as it has to contend with web content, whereas the instance in ``payload.histograms`` has only to contend with browser JS. Also, some histograms may be absent if never recorded on a content child process (``EVENTLOOP_UI_ACTIVITY`` is parent-process-only).
+
+This format was adopted in Firefox 51 via bug 1218576.
+
+scalars
+~~~~~~~
+This section contains the :doc:`../collection/scalars` that are valid for the current platform. Scalars are not created nor submitted if no data was added to them, and are only reported with subsession pings. Scalar data is only currently reported for the main process. Their type and format is described by the ``Scalars.yaml`` file. Its most recent version is available `here <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_. The ``info.revision`` field indicates the revision of the file that describes the reported scalars.
+
+childPayloads
+-------------
+The Telemetry payloads sent by child processes, recorded on child process shutdown (event ``content-child-shutdown`` observed). They are reduced session payloads, only available with e10s. Among some other things, they don't contain histograms, keyed histograms, addon details, addon histograms, or UI Telemetry.
+
+Note: Child payloads are not collected and cleared with subsession splits, they are currently only meaningful when analysed from ``saved-session`` or ``main`` pings with ``reason`` set to ``shutdown``.
+
+Note: Before Firefox 51 and bug 1218576, content process histograms and keyedHistograms were in the individual child payloads instead of being aggregated into ``processes.content``.
+
+simpleMeasurements
+------------------
+This section contains a list of simple measurements, or counters. In addition to the ones highlighted below, Telemetry timestamps (see `here <https://dxr.mozilla.org/mozilla-central/search?q=%22TelemetryTimestamps.add%22&redirect=false&case=true>`_ and `here <https://dxr.mozilla.org/mozilla-central/search?q=%22recordTimestamp%22&redirect=false&case=true>`_) can be reported.
+
+totalTime
+~~~~~~~~~
+A non-monotonic integer representing the number of seconds the session has been alive.
+
+uptime
+~~~~~~
+A non-monotonic integer representing the number of minutes the session has been alive.
+
+addonManager
+~~~~~~~~~~~~
+Only available in the extended set of measures, it contains a set of counters related to Addons. See `here <https://dxr.mozilla.org/mozilla-central/search?q=%22AddonManagerPrivate.recordSimpleMeasure%22&redirect=false&case=true>`_ for a list of recorded measures.
+
+UITelemetry
+~~~~~~~~~~~
+Only available in the extended set of measures. For more see :ref:`uitelemetry`.
+
+startupInterrupted
+~~~~~~~~~~~~~~~~~~
+A boolean set to true if startup was interrupted by an interactive prompt.
+
+js
+~~
+This section contains a series of counters from the JavaScript engine.
+
+Structure:
+
+.. code-block:: js
+
+ "js" : {
+ "setProto": <unsigned integer>, // Number of times __proto__ is set
+ "customIter": <unsigned integer> // Number of times __iterator__ is used (i.e., is found for a for-in loop)
+ }
+
+maximalNumberOfConcurrentThreads
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+An integer representing the highest number of threads encountered so far during the session.
+
+startupSessionRestoreReadBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes read by the main process up until the session store has finished restoring the windows.
+
+startupSessionRestoreWriteBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes written by the main process up until the session store has finished restoring the windows.
+
+startupWindowVisibleReadBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes read by the main process up until after a XUL window is made visible.
+
+startupWindowVisibleWriteBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes written by the main process up until after a XUL window is made visible.
+
+debuggerAttached
+~~~~~~~~~~~~~~~~
+A boolean set to true if a debugger is attached to the main process.
+
+shutdownDuration
+~~~~~~~~~~~~~~~~
+The time, in milliseconds, it took to complete the last shutdown.
+
+failedProfileLockCount
+~~~~~~~~~~~~~~~~~~~~~~
+The number of times the system failed to lock the user profile.
+
+savedPings
+~~~~~~~~~~
+Integer count of the number of pings that need to be sent.
+
+activeTicks
+~~~~~~~~~~~
+Integer count of the number of five-second intervals ('ticks') the user was considered 'active' (sending UI events to the window). An extra event is fired immediately when the user becomes active after being inactive. This is for some mouse and gamepad events, and all touch, keyboard, wheel, and pointer events (see `EventStateManager.cpp <https://dxr.mozilla.org/mozilla-central/rev/e6463ae7eda2775bc84593bb4a0742940bb87379/dom/events/EventStateManager.cpp#549>`_).
+This measure might be useful to give a trend of how much a user actually interacts with the browser when compared to overall session duration. It does not take into account whether or not the window has focus or is in the foreground. Just if it is receiving these interaction events.
+Note that in ``main`` pings, this measure is reset on subsession splits, while in ``saved-session`` pings it covers the whole browser session.
+
+pingsOverdue
+~~~~~~~~~~~~
+Integer count of pending pings that are overdue.
+
+histograms
+----------
+This section contains the histograms that are valid for the current platform. ``Flag`` and ``count`` histograms are always created and submitted, with their default value being respectively ``false`` and ``0``. Other histogram types (`see here <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Adding_a_new_Telemetry_probe#Choosing_a_Histogram_Type>`_) are not created nor submitted if no data was added to them. The type and format of the reported histograms is described by the ``Histograms.json`` file. Its most recent version is available `here <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Histograms.json>`_. The ``info.revision`` field indicates the revision of the file that describes the reported histograms.
+
+keyedHistograms
+---------------
+This section contains the keyed histograms available for the current platform.
+
+As of Firefox 48, this section does not contain empty keyed histograms anymore.
+
+threadHangStats
+---------------
+Contains the statistics about the hangs in main and background threads. Note that hangs in this section capture the [C++ pseudostack](https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler#Native_stack_vs._Pseudo_stack) and an incomplete JS stack, which is not 100% precise.
+
+To avoid submitting overly large payloads, some limits are applied:
+
+* Identical, adjacent "(chrome script)" or "(content script)" stack entries are collapsed together. If a stack is reduced, the "(reduced stack)" frame marker is added as the oldest frame.
+* The depth of the reported stacks is limited to 11 entries. This value represents the 99.9th percentile of the thread hangs stack depths reported by Telemetry.
+
+Structure:
+
+.. code-block:: js
+
+ "threadHangStats" : [
+ {
+ "name" : "Gecko",
+ "activity" : {...}, // a time histogram of all task run times
+ "hangs" : [
+ {
+ "stack" : [
+ "Startup::XRE_Main",
+ "Timer::Fire",
+ "(content script)",
+ "IPDL::PPluginScriptableObject::SendGetChildProperty",
+ ... up to 11 frames ...
+ ],
+ "nativeStack": [...], // optionally available
+ "histogram" : {...}, // the time histogram of the hang times
+ "annotations" : [
+ {
+ "pluginName" : "Shockwave Flash",
+ "pluginVersion" : "18.0.0.209"
+ },
+ ... other annotations ...
+ ]
+ },
+ ],
+ },
+ ... other threads ...
+ ]
+
+chromeHangs
+-----------
+Contains the statistics about the hangs happening exclusively on the main thread of the parent process. Precise C++ stacks are reported. This is only available on Nightly Release on Windows, when building using "--enable-profiling" switch.
+
+Some limits are applied:
+
+* Reported chrome hang stacks are limited in depth to 50 entries.
+* The maximum number of reported stacks is 50.
+
+Structure:
+
+.. code-block:: js
+
+ "chromeHangs" : {
+ "memoryMap" : [
+ ["wgdi32.pdb", "08A541B5942242BDB4AEABD8C87E4CFF2"],
+ ["igd10iumd32.pdb", "D36DEBF2E78149B5BE1856B772F1C3991"],
+ ... other entries in the format ["module name", "breakpad identifier"] ...
+ ],
+ "stacks" : [
+ [
+ [
+ 0, // the module index or -1 for invalid module indices
+ 190649 // the offset of this program counter in its module or an absolute pc
+ ],
+ [1, 2540075],
+ ... other frames, up to 50 ...
+ ],
+ ... other stacks, up to 50 ...
+ ],
+ "durations" : [8, ...], // the hang durations (in seconds)
+ "systemUptime" : [692, ...], // the system uptime (in minutes) at the time of the hang
+ "firefoxUptime" : [672, ...], // the Firefox uptime (in minutes) at the time of the hang
+ "annotations" : [
+ [
+ [0, ...], // the indices of the related hangs
+ {
+ "pluginName" : "Shockwave Flash",
+ "pluginVersion" : "18.0.0.209",
+ ... other annotations as key:value pairs ...
+ }
+ ],
+ ...
+ ]
+ },
+
+log
+---
+This section contains a log of important or unusual events reported through Telemetry.
+
+Structure:
+
+.. code-block:: js
+
+ "log": [
+ [
+ "Event_ID",
+ 3785, // the timestamp (in milliseconds) for the log entry
+ ... other data ...
+ ],
+ ...
+ ]
+
+
+webrtc
+------
+Contains special statistics gathered by WebRTC related components.
+
+So far only a bitmask for the ICE candidate type present in a successful or
+failed WebRTC connection is getting reported through C++ code as
+IceCandidatesStats, because the required bitmask is too big to be represented
+in a regular enum histogram. Further this data differentiates between Loop
+(aka Firefox Hello) connections and everything else, which is categorized as
+WebRTC.
+
+Note: in most cases the webrtc and loop dictionaries inside of
+IceCandidatesStats will simply be empty as the user has not used any WebRTC
+PeerConnection at all during the ping report time.
+
+Structure:
+
+.. code-block:: js
+
+ "webrtc": {
+ "IceCandidatesStats": {
+ "webrtc": {
+ "34526345": {
+ "successCount": 5
+ },
+ "2354353": {
+ "failureCount": 1
+ }
+ },
+ "loop": {
+ "2349346359": {
+ "successCount": 3
+ },
+ "73424": {
+ "successCount": 1,
+ "failureCount": 5
+ }
+ }
+ }
+ },
+
+gc
+--
+Contains statistics about selected garbage collections. To avoid
+bloating the ping, only a few GCs are included. There are two
+selection strategies. We always save the two GCs with the worst
+max_pause time. Additionally, in content processes, two collections
+are selected at random. If a GC runs for C milliseconds and the total
+time for all GCs since the session began is T milliseconds, then the
+GC has a C/T probablility of being selected for one of these "slots".
+
+Structure:
+
+.. code-block:: js
+
+ "gc": {
+ "random": [
+ {
+ // Timestamps are in milliseconds since startup. All the times here
+ // are wall-clock times, which may not be monotonically increasing.
+ "timestamp": 294872.2,
+ // All durations are in milliseconds.
+ "max_pause": 73.629,
+ "total_time": 364.951, // Sum of all slice times.
+ "zones_collected": 9,
+ "total_zones": 9,
+ "total_compartments": 309,
+ "minor_gcs": 44,
+ "store_buffer_overflows": 19,
+ "mmu_20ms": 0,
+ "mmu_50ms": 0,
+ // Reasons include "None", "NonIncrementalRequested",
+ // "AbortRequested", "KeepAtomsSet", "IncrementalDisabled",
+ // "ModeChange", "MallocBytesTrigger", "GCBytesTrigger",
+ // "ZoneChange".
+ "nonincremental_reason": "None",
+ "allocated": 37, // In megabytes.
+ "added_chunks": 54,
+ "removed_chunks": 12,
+ // Total number of slices (some of which may not appear
+ // in the "slices" array).
+ "num_slices": 15,
+ // We record at most 4 slices.
+ "slices": [
+ {
+ "slice": 0, // The index of this slice.
+ "pause": 23.221, // How long the slice took.
+ "when": 0, // Milliseconds since the start of the GC.
+ "reason": "SET_NEW_DOCUMENT",
+ // GC state when the slice started
+ "initial_state": "NotActive",
+ // GC state when the slice ended
+ "final_state": "Mark",
+ // Budget is either "Xms", "work(Y)", or
+ // "unlimited".
+ "budget": "10ms",
+ // Number of page faults during the slice.
+ "page_faults": 0,
+ "start_timestamp": 294875,
+ "end_timestamp": 294879,
+ // Time taken by each phase. There are at most 65 possible
+ // phases, but usually only a few phases run in a given slice.
+ "times": {
+ "wait_background_thread": 0.012,
+ "mark_discard_code": 2.845,
+ "purge": 0.723,
+ "mark": 9.831,
+ "mark_roots": 0.102,
+ "buffer_gray_roots": 3.095,
+ "mark_cross_compartment_wrappers": 0.039,
+ "mark_c_and_js_stacks": 0.005,
+ "mark_runtime_wide_data": 2.313,
+ "mark_embedding": 0.117,
+ "mark_compartments": 2.27,
+ "unmark": 1.063,
+ "minor_gcs_to_evict_nursery": 8.701,
+ ...
+ }
+ },
+ { ... },
+ ],
+ // Sum of the phase times across all slices, including
+ // omitted slices. As before, there are <= 65 possible phases.
+ "totals": {
+ "wait_background_thread": 0.012,
+ "mark_discard_code": 2.845,
+ "purge": 0.723,
+ "mark": 9.831,
+ "mark_roots": 0.102,
+ "buffer_gray_roots": 3.095,
+ "mark_cross_compartment_wrappers": 0.039,
+ "mark_c_and_js_stacks": 0.005,
+ "mark_runtime_wide_data": 2.313,
+ "mark_embedding": 0.117,
+ "mark_compartments": 2.27,
+ "unmark": 1.063,
+ "minor_gcs_to_evict_nursery": 8.701,
+ ...
+ }
+ },
+ ... // Up to four more selected GCs follow.
+ ],
+ "worst": [
+ ... // Same as above, but the 2 worst GCs by max_pause.
+ ]
+ },
+
+fileIOReports
+-------------
+Contains the statistics of main-thread I/O recorded during the execution. Only the I/O stats for the XRE and the profile directories are currently reported, neither of them disclosing the full local path.
+
+Structure:
+
+.. code-block:: js
+
+ "fileIOReports": {
+ "{xre}": [
+ totalTime, // Accumulated duration of all operations
+ creates, // Number of create/open operations
+ reads, // Number of read operations
+ writes, // Number of write operations
+ fsyncs, // Number of fsync operations
+ stats, // Number of stat operations
+ ],
+ "{profile}": [ ... ],
+ ...
+ }
+
+lateWrites
+----------
+This sections reports writes to the file system that happen during shutdown. The reported data contains the stack and the loaded libraries at the time the writes happened.
+
+Structure:
+
+.. code-block:: js
+
+ "lateWrites" : {
+ "memoryMap" : [
+ ["wgdi32.pdb", "08A541B5942242BDB4AEABD8C87E4CFF2"],
+ ... other entries in the format ["module name", "breakpad identifier"] ...
+ ],
+ "stacks" : [
+ [
+ [
+ 0, // the module index or -1 for invalid module indices
+ 190649 // the offset of this program counter in its module or an absolute pc
+ ],
+ [1, 2540075],
+ ... other frames ...
+ ],
+ ... other stacks ...
+ ],
+ },
+
+addonDetails
+------------
+This section contains per-addon telemetry details, as reported by each addon provider. The XPI provider is the only one reporting at the time of writing (`see DXR <https://dxr.mozilla.org/mozilla-central/search?q=setTelemetryDetails&case=true>`_). Telemetry does not manipulate or enforce a specific format for the supplied provider's data.
+
+Structure:
+
+.. code-block:: js
+
+ "addonDetails": {
+ "XPI": {
+ "adbhelper@mozilla.org": {
+ "scan_items": 24,
+ "scan_MS": 3,
+ "location": "app-profile",
+ "name": "ADB Helper",
+ "creator": "Mozilla & Android Open Source Project",
+ "startup_MS": 30
+ },
+ ...
+ },
+ ...
+ }
+
+addonHistograms
+---------------
+This section contains the histogram registered by the addons (`see here <https://dxr.mozilla.org/mozilla-central/rev/584870f1cbc5d060a57e147ce249f736956e2b62/toolkit/components/telemetry/nsITelemetry.idl#303>`_). This section is not present if no addon histogram is available.
+
+UITelemetry
+-----------
+See the ``UITelemetry data format`` documentation.
+
+slowSQL
+-------
+This section contains the informations about the slow SQL queries for both the main and other threads. The execution of an SQL statement is considered slow if it takes 50ms or more on the main thread or 100ms or more on other threads. Slow SQL statements will be automatically trimmed to 1000 characters. This limit doesn't include the ellipsis and database name, that are appended at the end of the stored statement.
+
+Structure:
+
+.. code-block:: js
+
+ "slowSQL": {
+ "mainThread": {
+ "Sanitized SQL Statement": [
+ 1, // the number of times this statement was hit
+ 200 // the total time (in milliseconds) that was spent on this statement
+ ],
+ ...
+ },
+ "otherThreads": {
+ "VACUUM /* places.sqlite */": [
+ 1,
+ 330
+ ],
+ ...
+ }
+ },
+
+slowSQLStartup
+--------------
+This section contains the slow SQL statements gathered at startup (until the "sessionstore-windows-restored" event is fired). The structure of this section resembles the one for `slowSQL`_.
+
+UIMeasurements
+--------------
+This section contains UI specific telemetry measurements and events. This section is mainly populated with Android-specific data and events (`see here <https://dxr.mozilla.org/mozilla-central/search?q=regexp%3AUITelemetry.%28addEvent|startSession|stopSession%29&redirect=false&case=false>`_).
+
+Structure:
+
+.. code-block:: js
+
+ "UIMeasurements": [
+ {
+ "type": "event", // either "session" or "event"
+ "action": "action.1",
+ "method": "menu",
+ "sessions": [],
+ "timestamp": 12345,
+ "extras": "settings"
+ },
+ {
+ "type": "session",
+ "name": "awesomescreen.1",
+ "reason": "commit",
+ "start": 123,
+ "end": 456
+ }
+ ...
+ ],
diff --git a/toolkit/components/telemetry/docs/data/sync-ping.rst b/toolkit/components/telemetry/docs/data/sync-ping.rst
new file mode 100644
index 0000000000..775ab008a0
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/sync-ping.rst
@@ -0,0 +1,182 @@
+
+"sync" ping
+===========
+
+This is an aggregated format that contains information about each sync that occurred during a timeframe. It is submitted every 12 hours, and on browser shutdown, but only if the syncs property would not be empty. The ping does not contain the enviroment block, nor the clientId.
+
+Each item in the syncs property is generated after a sync is completed, for both successful and failed syncs, and contains measurements pertaining to sync performance and error information.
+
+A JSON-schema document describing the exact format of the ping's payload property can be found at `services/sync/tests/unit/sync\_ping\_schema.json <https://dxr.mozilla.org/mozilla-central/source/services/sync/tests/unit/sync_ping_schema.json>`_.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+ type: "sync",
+ ... common ping data
+ payload: {
+ version: 1,
+ discarded: <integer count> // Number of syncs discarded -- left out if zero.
+ why: <string>, // Why did we submit the ping? Either "shutdown" or "schedule".
+ // Array of recorded syncs. The ping is not submitted if this would be empty
+ syncs: [{
+ when: <integer milliseconds since epoch>,
+ took: <integer duration in milliseconds>,
+ uid: <string>, // Hashed FxA unique ID, or string of 32 zeros.
+ deviceID: <string>, // Hashed FxA Device ID, hex string of 64 characters, not included if the user is not logged in.
+ didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
+ why: <string>, // Optional, why the sync occured, excluded if we don't know.
+
+ // Optional, excluded if there was no error.
+ failureReason: {
+ name: <string>, // "httperror", "networkerror", "shutdownerror", etc.
+ code: <integer>, // Only present for "httperror" and "networkerror".
+ error: <string>, // Only present for "othererror" and "unexpectederror".
+ from: <string>, // Optional, and only present for "autherror".
+ },
+
+ // Optional, excluded if we couldn't get a valid uid or local device id
+ devices: [{
+ os: <string>, // OS string as reported by Services.appinfo.OS,
+ version: <string>, // Firefox version, as reported by Services.appinfo.version
+ id: <string>, // Hashed FxA device id for device
+ }],
+
+ // Internal sync status information. Omitted if it would be empty.
+ status: {
+ sync: <string>, // The value of the Status.sync property, unless it indicates success.
+ service: <string>, // The value of the Status.service property, unless it indicates success.
+ },
+ // Information about each engine's sync.
+ engines: [
+ {
+ name: <string>, // "bookmarks", "tabs", etc.
+ took: <integer duration in milliseconds>, // Optional, values of 0 are omitted.
+
+ status: <string>, // The value of Status.engines, if it holds a non-success value.
+
+ // Optional, excluded if all items would be 0. A missing item indicates a value of 0.
+ incoming: {
+ applied: <integer>, // Number of records applied
+ succeeded: <integer>, // Number of records that applied without error
+ failed: <integer>, // Number of records that failed to apply
+ newFailed: <integer>, // Number of records that failed for the first time this sync
+ reconciled: <integer>, // Number of records that were reconciled
+ },
+
+ // Optional, excluded if it would be empty. Records that would be
+ // empty (e.g. 0 sent and 0 failed) are omitted.
+ outgoing: [
+ {
+ sent: <integer>, // Number of outgoing records sent. Zero values are omitted.
+ failed: <integer>, // Number that failed to send. Zero values are omitted.
+ }
+ ],
+ // Optional, excluded if there were no errors
+ failureReason: { ... }, // Same as above.
+
+ // Optional, excluded if it would be empty or if the engine cannot
+ // or did not run validation on itself.
+ validation: {
+ // Optional validator version, default of 0.
+ version: <integer>,
+ checked: <integer>,
+ took: <non-monotonic integer duration in milliseconds>,
+ // Entries with a count of 0 are excluded, the array is excluded if no problems are found.
+ problems: [
+ {
+ name: <string>, // The problem identified.
+ count: <integer>, // Number of times it occurred.
+ }
+ ],
+ // Format is same as above, this is only included if we tried and failed
+ // to run validation, and if it's present, all other fields in this object are optional.
+ failureReason: { ... },
+ }
+ }
+ ]
+ }]
+ }
+ }
+
+info
+----
+
+discarded
+~~~~~~~~~
+
+The ping may only contain a certain number of entries in the ``"syncs"`` array, currently 500 (it is determined by the ``"services.sync.telemetry.maxPayloadCount"`` preference). Entries beyond this are discarded, and recorded in the discarded count.
+
+syncs.took
+~~~~~~~~~~
+
+These values should be monotonic. If we can't get a monotonic timestamp, -1 will be reported on the payload, and the values will be omitted from the engines. Additionally, the value will be omitted from an engine if it would be 0 (either due to timer inaccuracy or finishing instantaneously).
+
+syncs.uid
+~~~~~~~~~
+
+This property containing a hash of the FxA account identifier, which is a 32 character hexidecimal string. In the case that we are unable to authenticate with FxA and have never authenticated in the past, it will be a placeholder string consisting of 32 repeated ``0`` characters.
+
+syncs.why
+~~~~~~~~~
+
+One of the following values:
+
+- ``startup``: This is the first sync triggered after browser startup.
+- ``schedule``: This is a sync triggered because it has been too long since the last sync.
+- ``score``: This sync is triggered by a high score value one of sync's trackers, indicating that many changes have occurred since the last sync.
+- ``user``: The user manually triggered the sync.
+- ``tabs``: The user opened the synced tabs sidebar, which triggers a sync.
+
+syncs.status
+~~~~~~~~~~~~
+
+The ``engine.status``, ``payload.status.sync``, and ``payload.status.service`` properties are sync error codes, which are listed in `services/sync/modules/constants.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/constants.js>`_, and success values are not reported.
+
+syncs.failureReason
+~~~~~~~~~~~~~~~~~~~
+
+Stores error information, if any is present. Always contains the "name" property, which identifies the type of error it is. The types can be.
+
+- ``httperror``: Indicates that we recieved an HTTP error response code, but are unable to be more specific about the error. Contains the following properties:
+
+ - ``code``: Integer HTTP status code.
+
+- ``nserror``: Indicates that an exception with the provided error code caused sync to fail.
+
+ - ``code``: The nsresult error code (integer).
+
+- ``shutdownerror``: Indicates that the sync failed because we shut down before completion.
+
+- ``autherror``: Indicates an unrecoverable authentication error.
+
+ - ``from``: Where the authentication error occurred, one of the following values: ``tokenserver``, ``fxaccounts``, or ``hawkclient``.
+
+- ``othererror``: Indicates that it is a sync error code that we are unable to give more specific information on. As with the ``syncStatus`` property, it is a sync error code, which are listed in `services/sync/modules/constants.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/constants.js>`_.
+
+ - ``error``: String identifying which error was present.
+
+- ``unexpectederror``: Indicates that some other error caused sync to fail, typically an uncaught exception.
+
+ - ``error``: The message provided by the error.
+
+- ``sqlerror``: Indicates that we recieved a ``mozIStorageError`` from a database query.
+
+ - ``code``: Value of the ``error.result`` property, one of the constants listed `here <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/MozIStorageError#Constants>`_.
+
+syncs.engine.name
+~~~~~~~~~~~~~~~~~
+
+Third-party engines are not reported, so only the following values are allowed: ``addons``, ``bookmarks``, ``clients``, ``forms``, ``history``, ``passwords``, ``prefs``, and ``tabs``.
+
+syncs.engine.validation.problems
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For engines that can run validation on themselves, an array of objects describing validation errors that have occurred. Items that would have a count of 0 are excluded. Each engine will have its own set of items that it might put in the ``name`` field, but there are a finite number. See ``BookmarkProblemData.getSummary`` in `services/sync/modules/bookmark\_validator.js <https://dxr.mozilla.org/mozilla-central/source/services/sync/modules/bookmark_validator.js>`_ for an example.
+
+syncs.devices
+~~~~~~~~~~~~~
+
+The list of remote devices associated with this account, as reported by the clients collection. The ID of each device is hashed using the same algorithm as the local id.
diff --git a/toolkit/components/telemetry/docs/data/uitour-ping.rst b/toolkit/components/telemetry/docs/data/uitour-ping.rst
new file mode 100644
index 0000000000..8d17ec55ac
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/uitour-ping.rst
@@ -0,0 +1,26 @@
+
+"uitour-tag" ping
+=================
+
+This ping is submitted via the UITour setTreatmentTag API. It may be used by
+the tour to record what settings were made by a user or to track the result of
+A/B experiments.
+
+The client ID is submitted with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 1,
+ type: "uitour-tag",
+ clientId: <string>,
+ payload: {
+ tagName: <string>,
+ tagValue: <string>
+ }
+ }
+
+See also: :doc:`common ping fields <common-ping>`
+
diff --git a/toolkit/components/telemetry/docs/fhr/architecture.rst b/toolkit/components/telemetry/docs/fhr/architecture.rst
new file mode 100644
index 0000000000..6a857d3298
--- /dev/null
+++ b/toolkit/components/telemetry/docs/fhr/architecture.rst
@@ -0,0 +1,226 @@
+.. _healthreport_architecture:
+
+============
+Architecture
+============
+
+``healthreporter.jsm`` contains the main interface for FHR, the
+``HealthReporter`` type. An instance of this is created by the
+``data_reporting_service`.
+
+``providers.jsm`` contains numerous ``Metrics.Provider`` and
+``Metrics.Measurement`` used for collecting application metrics. If you
+are looking for the FHR probes, this is where they are.
+
+Storage
+=======
+
+Firefox Health Report stores data in 3 locations:
+
+* Metrics measurements and provider state is stored in a SQLite database
+ (via ``Metrics.Storage``).
+* Service state (such as the IDs of documents uploaded) is stored in a
+ JSON file on disk (via OS.File).
+* Lesser state and run-time options are stored in preferences.
+
+Preferences
+===========
+
+Preferences controlling behavior of Firefox Health Report live in the
+``datareporting.healthreport.*`` branch.
+
+Service and Data Control
+------------------------
+
+The follow preferences control behavior of the service and data upload.
+
+service.enabled
+ Controls whether the entire health report service runs. The overall
+ service performs data collection, storing, and submission.
+
+ This is the primary kill switch for Firefox Health Report
+ outside of the build system variable. i.e. if you are using an
+ official Firefox build and wish to disable FHR, this is what you
+ should set to false to prevent FHR from not only submitting but
+ also collecting data.
+
+uploadEnabled
+ Whether uploading of data is enabled. This is the preference the
+ checkbox in the preferences UI reflects. If this is
+ disabled, FHR still collects data - it just doesn't upload it.
+
+service.loadDelayMsec
+ How long (in milliseconds) after initial application start should FHR
+ wait before initializing.
+
+ FHR may initialize sooner than this if the FHR service is requested.
+ This will happen if e.g. the user goes to ``about:healthreport``.
+
+service.loadDelayFirstRunMsec
+ How long (in milliseconds) FHR should wait to initialize on first
+ application run.
+
+ FHR waits longer than normal to initialize on first application run
+ because first-time initialization can use a lot of I/O to initialize
+ the SQLite database and this I/O should not interfere with the
+ first-run user experience.
+
+documentServerURI
+ The URI of a Bagheera server that FHR should interface with for
+ submitting documents.
+
+ You typically do not need to change this.
+
+documentServerNamespace
+ The namespace on the document server FHR should upload documents to.
+
+ You typically do not need to change this.
+
+infoURL
+ The URL of a page containing more info about FHR, it's privacy
+ policy, etc.
+
+about.reportUrl
+ The URL to load in ``about:healthreport``.
+
+about.reportUrlUnified
+ The URL to load in ``about:healthreport``. This is used instead of ``reportUrl`` for UnifiedTelemetry when it is not opt-in.
+
+service.providerCategories
+ A comma-delimited list of category manager categories that contain
+ registered ``Metrics.Provider`` records. Read below for how provider
+ registration works.
+
+If the entire service is disabled, you lose data collection. This means
+that **local** data analysis won't be available because there is no data
+to analyze! Keep in mind that Firefox Health Report can be useful even
+if it's not submitting data to remote servers!
+
+Logging
+-------
+
+The following preferences allow you to control the logging behavior of
+Firefox Health Report.
+
+logging.consoleEnabled
+ Whether to write log messages to the web console. This is true by
+ default.
+
+logging.consoleLevel
+ The minimum log level FHR messages must have to be written to the
+ web console. By default, only FHR warnings or errors will be written
+ to the web console. During normal/expected operation, no messages of
+ this type should be produced.
+
+logging.dumpEnabled
+ Whether to write log messages via ``dump()``. If true, FHR will write
+ messages to stdout/stderr.
+
+ This is typically only enabled when developing FHR.
+
+logging.dumpLevel
+ The minimum log level messages must have to be written via
+ ``dump()``.
+
+State
+-----
+
+currentDaySubmissionFailureCount
+ How many submission failures the client has encountered while
+ attempting to upload the most recent document.
+
+lastDataSubmissionFailureTime
+ The time of the last failed document upload.
+
+lastDataSubmissionRequestedTime
+ The time of the last document upload attempt.
+
+lastDataSubmissionSuccessfulTime
+ The time of the last successful document upload.
+
+nextDataSubmissionTime
+ The time the next data submission is scheduled for. FHR will not
+ attempt to upload a new document before this time.
+
+pendingDeleteRemoteData
+ Whether the client currently has a pending request to delete remote
+ data. If true, the client will attempt to delete all remote data
+ before an upload is performed.
+
+FHR stores various state in preferences.
+
+Registering Providers
+=====================
+
+Firefox Health Report providers are registered via the category manager.
+See ``HealthReportComponents.manifest`` for providers defined in this
+directory.
+
+Essentially, the category manager receives the name of a JS type and the
+URI of a JSM to import that exports this symbol. At run-time, the
+providers registered in the category manager are instantiated.
+
+Providers are registered via the category manager to make registration
+simple and less prone to errors. Any XPCOM component can create a
+category manager entry. Therefore, new data providers can be added
+without having to touch core Firefox Health Report code. Additionally,
+category manager registration means providers are more likely to be
+registered on FHR's terms, when it wants. If providers were registered
+in code at application run-time, there would be the risk of other
+components prematurely instantiating FHR (causing a performance hit if
+performed at an inopportune time) or semi-complicated code around
+observers or listeners. Category manager entries are only 1 line per
+provider and leave FHR in control: they are simple and safe.
+
+Document Generation and Lifecycle
+=================================
+
+FHR will attempt to submit a JSON document containing data every 24 wall
+clock hours.
+
+At upload time, FHR will query the database for **all** information from
+the last 180 days and assemble this data into a JSON document. We
+attempt to upload this JSON document with a client-generated UUID to the
+configured server.
+
+Before we attempt upload, the generated UUID is stored in the JSON state
+file on local disk. At this point, the client assumes the document with
+that UUID has been successfully stored on the server.
+
+If the client is aware of other document UUIDs that presumably exist on
+the server, those UUIDs are sent with the upload request so the client
+can request those UUIDs be deleted. This helps ensure that each client
+only has 1 document/UUID on the server at any one time.
+
+Importance of Persisting UUIDs
+------------------------------
+
+The choices of how, where, and when document UUIDs are stored and updated
+are very important. One should not attempt to change things unless she
+has a very detailed understanding of why things are the way they are.
+
+The client is purposefully very conservative about forgetting about
+generated UUIDs. In other words, once a UUID is generated, the client
+deliberately holds on to that UUID until it's very confident that UUID
+is no longer stored on the server. The reason we do this is because
+*orphaned* documents/UUIDs on the server can lead to faulty analysis,
+such as over-reporting the number of Firefox installs that stop being
+used.
+
+When uploading a new UUID, we update the state and save the state file
+to disk *before* an upload attempt because if the upload succeeds but
+the response never makes it back to the client, we want the client to
+know about the uploaded UUID so it can delete it later to prevent an
+orphan.
+
+We maintain a list of UUIDs locally (not simply the last UUID) because
+multiple upload attempts could fail the same way as the previous
+paragraph describes and we have no way of knowing which (if any)
+actually succeeded. The safest approach is to assume every document
+produced managed to get uploaded some how.
+
+We store the UUIDs on a file on disk and not anywhere else because we
+want storage to be robust. We originally stored UUIDs in preferences,
+which only flush to disk periodically. Writes to preferences were
+apparently getting lost. We switched to writing directly to files to
+eliminate this window.
diff --git a/toolkit/components/telemetry/docs/fhr/dataformat.rst b/toolkit/components/telemetry/docs/fhr/dataformat.rst
new file mode 100644
index 0000000000..b067f9d0c7
--- /dev/null
+++ b/toolkit/components/telemetry/docs/fhr/dataformat.rst
@@ -0,0 +1,1997 @@
+.. _healthreport_dataformat:
+
+==============
+Payload Format
+==============
+
+Currently, the Firefox Health Report is submitted as a compressed JSON
+document. The root JSON element is an object. A *version* field defines
+the version of the payload which in turn defines the expected contents
+the object.
+
+As of 2013-07-03, desktop submits Version 2, and Firefox for Android submits
+Version 3 payloads.
+
+Version 3
+=========
+
+Version 3 is a complete rebuild of the document format. Events are tracked in
+an "environment". Environments are computed from a large swath of local data
+(e.g., add-ons, CPU count, versions), and a new environment comes into being
+when one of its attributes changes.
+
+Client documents, then, will include descriptions of many environments, and
+measurements will be attributed to one particular environment.
+
+A map of environments is present at the top level of the document, with the
+current named "current" in the map. Each environment has a hash identifier and
+a set of attributes. The current environment is completely described, and has
+its hash present in a "hash" attribute. All other environments are represented
+as a tree diff from the current environment, with their hash as the key in the
+"environments" object.
+
+A removed add-on has the value 'null'.
+
+There is no "last" data at present.
+
+Daily data is hierarchical: by day, then by environment, and then by
+measurement, and is present in "data", just as in v2.
+
+Leading by example::
+
+ {
+ "lastPingDate": "2013-06-29",
+ "thisPingDate": "2013-07-03",
+ "version": 3,
+ "environments": {
+ "current": {
+ "org.mozilla.sysinfo.sysinfo": {
+ "memoryMB": 1567,
+ "cpuCount": 4,
+ "architecture": "armeabi-v7a",
+ "_v": 1,
+ "version": "4.1.2",
+ "name": "Android"
+ },
+ "org.mozilla.profile.age": {
+ "_v": 1,
+ "profileCreation": 15827
+ },
+ "org.mozilla.addons.active": {
+ "QuitNow@TWiGSoftware.com": {
+ "appDisabled": false,
+ "userDisabled": false,
+ "scope": 1,
+ "updateDay": 15885,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15885,
+ "version": "1.18.02"
+ },
+ "{dbbf9331-b713-6eda-1006-205efead09dc}": {
+ "appDisabled": false,
+ "userDisabled": "askToActivate",
+ "scope": 8,
+ "updateDay": 15779,
+ "foreignInstall": true,
+ "blocklistState": 0,
+ "type": "plugin",
+ "installDay": 15779,
+ "version": "11.1 r115"
+ },
+ "desktopbydefault@bnicholson.mozilla.org": {
+ "appDisabled": false,
+ "userDisabled": true,
+ "scope": 1,
+ "updateDay": 15870,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15870,
+ "version": "1.1"
+ },
+ "{6e092a7f-ba58-4abb-88c1-1a4e50b217e4}": {
+ "appDisabled": false,
+ "userDisabled": false,
+ "scope": 1,
+ "updateDay": 15828,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15828,
+ "version": "1.1.0"
+ },
+ "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}": {
+ "appDisabled": false,
+ "userDisabled": true,
+ "scope": 1,
+ "updateDay": 15879,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15879,
+ "version": "1.3.2"
+ },
+ "_v": 1
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 3,
+ "appLocale": "en_us",
+ "osLocale": "en_us",
+ "distribution": "",
+ "acceptLangIsUserSet": 0,
+ "isTelemetryEnabled": 1,
+ "isBlocklistEnabled": 1
+ },
+ "geckoAppInfo": {
+ "updateChannel": "nightly",
+ "id": "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ "os": "Android",
+ "platformBuildID": "20130703031323",
+ "platformVersion": "25.0a1",
+ "vendor": "Mozilla",
+ "name": "fennec",
+ "xpcomabi": "arm-eabi-gcc3",
+ "appBuildID": "20130703031323",
+ "_v": 1,
+ "version": "25.0a1"
+ },
+ "hash": "tB4Pnnep9yTxnMDymc3dAB2RRB0=",
+ "org.mozilla.addons.counts": {
+ "extension": 4,
+ "plugin": 1,
+ "_v": 1,
+ "theme": 0
+ }
+ },
+ "k2O3hlreMeS7L1qtxeMsYWxgWWQ=": {
+ "geckoAppInfo": {
+ "platformBuildID": "20130630031138",
+ "appBuildID": "20130630031138",
+ "_v": 1
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 2,
+ }
+ },
+ "1+KN9TutMpzdl4TJEl+aCxK+xcw=": {
+ "geckoAppInfo": {
+ "platformBuildID": "20130626031100",
+ "appBuildID": "20130626031100",
+ "_v": 1
+ },
+ "org.mozilla.addons.active": {
+ "QuitNow@TWiGSoftware.com": null,
+ "{dbbf9331-b713-6eda-1006-205efead09dc}": null,
+ "desktopbydefault@bnicholson.mozilla.org": null,
+ "{6e092a7f-ba58-4abb-88c1-1a4e50b217e4}": null,
+ "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}": null,
+ "_v": 1
+ },
+ "org.mozilla.addons.counts": {
+ "extension": 0,
+ "plugin": 0,
+ "_v": 1
+ }
+ }
+ },
+ "data": {
+ "last": {},
+ "days": {
+ "2013-07-03": {
+ "tB4Pnnep9yTxnMDymc3dAB2RRB0=": {
+ "org.mozilla.appSessions": {
+ "normal": [
+ {
+ "r": "P",
+ "d": 2,
+ "sj": 653
+ },
+ {
+ "r": "P",
+ "d": 22
+ },
+ {
+ "r": "P",
+ "d": 5
+ },
+ {
+ "r": "P",
+ "d": 0
+ },
+ {
+ "r": "P",
+ "sg": 3560,
+ "d": 171,
+ "sj": 518
+ },
+ {
+ "r": "P",
+ "d": 16
+ },
+ {
+ "r": "P",
+ "d": 1079
+ }
+ ],
+ "_v": "4"
+ }
+ },
+ "k2O3hlreMeS7L1qtxeMsYWxgWWQ=": {
+ "org.mozilla.appSessions": {
+ "normal": [
+ {
+ "r": "P",
+ "d": 27
+ },
+ {
+ "r": "P",
+ "d": 19
+ },
+ {
+ "r": "P",
+ "d": 55
+ }
+ ],
+ "_v": "4"
+ },
+ "org.mozilla.searches.counts": {
+ "bartext": {
+ "google": 1
+ },
+ "_v": "4"
+ },
+ "org.mozilla.experiment": {
+ "lastActive": "some.experiment.id"
+ "_v": "1"
+ }
+ }
+ }
+ }
+ }
+ }
+
+App sessions in Version 3
+-------------------------
+
+Sessions are divided into "normal" and "abnormal". Session objects are stored as discrete JSON::
+
+ "org.mozilla.appSessions": {
+ _v: 4,
+ "normal": [
+ {"r":"P", "d": 123},
+ ],
+ "abnormal": [
+ {"r":"A", "oom": true, "stopped": false}
+ ]
+ }
+
+Keys are:
+
+"r"
+ reason. Values are "P" (activity paused), "A" (abnormal termination).
+"d"
+ duration. Value in seconds.
+"sg"
+ Gecko startup time (msec). Present if this is a clean launch. This
+ corresponds to the telemetry timer *FENNEC_STARTUP_TIME_GECKOREADY*.
+"sj"
+ Java activity init time (msec). Present if this is a clean launch. This
+ corresponds to the telemetry timer *FENNEC_STARTUP_TIME_JAVAUI*,
+ and includes initialization tasks beyond initial
+ *onWindowFocusChanged*.
+
+Abnormal terminations will be missing a duration and will feature these keys:
+
+"oom"
+ was the session killed by an OOM exception?
+"stopped"
+ was the session stopped gently?
+
+Version 3.2
+-----------
+
+As of Firefox 35, the search counts measurement is now bumped to v6, including the *activity* location for the search activity.
+
+Version 3.1
+-----------
+
+As of Firefox 27, *appinfo* is now bumped to v3, including *osLocale*,
+*appLocale* (currently always the same as *osLocale*), *distribution* (a string
+containing the distribution ID and version, separated by a colon), and
+*acceptLangIsUserSet*, an integer-boolean that describes whether the user set
+an *intl.accept_languages* preference.
+
+The search counts measurement is now at version 5, which indicates that
+non-partner searches are recorded. You'll see identifiers like "other-Foo Bar"
+rather than "other".
+
+
+Version 3.2
+-----------
+
+In Firefox 32, Firefox for Android includes a device configuration section
+in the environment description::
+
+ "org.mozilla.device.config": {
+ "hasHardwareKeyboard": false,
+ "screenXInMM": 58,
+ "screenLayout": 2,
+ "uiType": "default",
+ "screenYInMM": 103,
+ "_v": 1,
+ "uiMode": 1
+ }
+
+Of these, the only keys that need explanation are:
+
+uiType
+ One of "default", "smalltablet", "largetablet".
+uiMode
+ A mask of the Android *Configuration.uiMode* value, e.g.,
+ *UI_MODE_TYPE_CAR*.
+screenLayout
+ A mask of the Android *Configuration.screenLayout* value. One of the
+ *SCREENLAYOUT_SIZE_* constants.
+
+Note that screen dimensions can be incorrect due to device inaccuracies and platform limitations.
+
+Other notable differences from Version 2
+----------------------------------------
+
+* There is no default browser indicator on Android.
+* Add-ons include a *blocklistState* attribute, as returned by AddonManager.
+* Searches are now version 4, and are hierarchical: how the search was started
+ (bartext, barkeyword, barsuggest), and then counts per provider.
+
+Version 2
+=========
+
+Version 2 is the same as version 1 with the exception that it has an additional
+top-level field, *geckoAppInfo*, which contains basic application info.
+
+geckoAppInfo
+------------
+
+This field is an object that is a simple map of string keys and values
+describing basic application metadata. It is very similar to the *appinfo*
+measurement in the *last* section. The difference is this field is almost
+certainly guaranteed to exist whereas the one in the data part of the
+payload may be omitted in certain scenarios (such as catastrophic client
+error).
+
+Its keys are as follows:
+
+appBuildID
+ The build ID/date of the application. e.g. "20130314113542".
+
+version
+ The value of nsXREAppData.version. This is the application's version. e.g.
+ "21.0.0".
+
+vendor
+ The value of nsXREAppData.vendor. Can be empty an empty string. For
+ official Mozilla builds, this will be "Mozilla".
+
+name
+ The value of nsXREAppData.name. For official Firefox builds, this
+ will be "Firefox".
+
+id
+ The value of nsXREAppData.ID.
+
+platformVersion
+ The version of the Gecko platform (as opposed to the app version). For
+ Firefox, this is almost certainly equivalent to the *version* field.
+
+platformBuildID
+ The build ID/date of the Gecko platfor (as opposed to the app version).
+ This is commonly equivalent to *appBuildID*.
+
+os
+ The name of the operating system the application is running on.
+
+xpcomabi
+ The binary architecture of the build.
+
+updateChannel
+ The name of the channel used for application updates. Official Mozilla
+ builds have one of the values {release, beta, aurora, nightly}. Local and
+ test builds have *default* as the channel.
+
+Version 1
+=========
+
+Top-level Properties
+--------------------
+
+The main JSON object contains the following properties:
+
+lastPingDate
+ UTC date of the last upload. If this is the first upload from this client,
+ this will not be present.
+
+thisPingDate
+ UTC date when this payload was constructed.
+
+version
+ Integer version of this payload format. Currently only 1 is defined.
+
+clientID
+ An identifier that identifies the client that is submitting data.
+
+ This property may not be present in older clients.
+
+ See :ref:`healthreport_identifiers` for more info on identifiers.
+
+clientIDVersion
+ Integer version associated with the generation semantics for the
+ ``clientID``.
+
+ If the value is ``1``, ``clientID`` is a randomly-generated UUID.
+
+ This property may not be present in older clients.
+
+data
+ Object holding data constituting health report.
+
+Data Properties
+---------------
+
+The bulk of the health report is contained within the *data* object. This
+object has the following keys:
+
+days
+ Object mapping UTC days to measurements from that day. Keys are in the
+ *YYYY-MM-DD* format. e.g. "2013-03-14"
+
+last
+ Object mapping measurement names to their values.
+
+
+The value of *days* and *last* are objects mapping measurement names to that
+measurement's values. The values are always objects. Each object contains
+a *_v* property. This property defines the version of this measurement.
+Additional non-underscore-prefixed properties are defined by the measurement
+itself (see sections below).
+
+Example
+-------
+
+Here is an example JSON document for version 1::
+
+ {
+ "version": 1,
+ "thisPingDate": "2013-03-11",
+ "lastPingDate": "2013-03-10",
+ "data": {
+ "last": {
+ "org.mozilla.addons.active": {
+ "masspasswordreset@johnathan.nightingale": {
+ "userDisabled": false,
+ "appDisabled": false,
+ "version": "1.05",
+ "type": "extension",
+ "scope": 1,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 14973,
+ "updateDay": 15317
+ },
+ "places-maintenance@bonardo.net": {
+ "userDisabled": false,
+ "appDisabled": false,
+ "version": "1.3",
+ "type": "extension",
+ "scope": 1,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 15268,
+ "updateDay": 15379
+ },
+ "_v": 1
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "appBuildID": "20130309030841",
+ "distributionID": "",
+ "distributionVersion": "",
+ "hotfixVersion": "",
+ "id": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "locale": "en-US",
+ "name": "Firefox",
+ "os": "Darwin",
+ "platformBuildID": "20130309030841",
+ "platformVersion": "22.0a1",
+ "updateChannel": "nightly",
+ "vendor": "Mozilla",
+ "version": "22.0a1",
+ "xpcomabi": "x86_64-gcc3"
+ },
+ "org.mozilla.profile.age": {
+ "_v": 1,
+ "profileCreation": 12444
+ },
+ "org.mozilla.appSessions.current": {
+ "_v": 3,
+ "startDay": 15773,
+ "activeTicks": 522,
+ "totalTime": 70858,
+ "main": 1245,
+ "firstPaint": 2695,
+ "sessionRestored": 3436
+ },
+ "org.mozilla.sysinfo.sysinfo": {
+ "_v": 1,
+ "cpuCount": 8,
+ "memoryMB": 16384,
+ "architecture": "x86-64",
+ "name": "Darwin",
+ "version": "12.2.1"
+ }
+ },
+ "days": {
+ "2013-03-11": {
+ "org.mozilla.addons.counts": {
+ "_v": 1,
+ "extension": 15,
+ "plugin": 12,
+ "theme": 1
+ },
+ "org.mozilla.places.places": {
+ "_v": 1,
+ "bookmarks": 757,
+ "pages": 104858
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "isDefaultBrowser": 1
+ }
+ },
+ "2013-03-10": {
+ "org.mozilla.addons.counts": {
+ "_v": 1,
+ "extension": 15,
+ "plugin": 12,
+ "theme": 1
+ },
+ "org.mozilla.places.places": {
+ "_v": 1,
+ "bookmarks": 757,
+ "pages": 104857
+ },
+ "org.mozilla.searches.counts": {
+ "_v": 1,
+ "google.urlbar": 4
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "isDefaultBrowser": 1
+ }
+ }
+ }
+ }
+ }
+
+Measurements
+============
+
+The bulk of payloads consists of measurement data. An individual measurement
+is merely a collection of related values e.g. *statistics about the Places
+database* or *system information*.
+
+Each measurement has an integer version number attached. When the fields in
+a measurement or the semantics of data within that measurement change, the
+version number is incremented.
+
+All measurements are defined alphabetically in the sections below.
+
+org.mozilla.addons.addons
+-------------------------
+
+This measurement contains information about the currently-installed add-ons.
+
+Version 2
+^^^^^^^^^
+
+This version adds the human-readable fields *name* and *description*, both
+coming directly from the Addon instance as most properties in version 1.
+Also, all plugin details are now in org.mozilla.addons.plugins.
+
+Version 1
+^^^^^^^^^
+
+The measurement object is a mapping of add-on IDs to objects containing
+add-on metadata.
+
+Each add-on contains the following properties:
+
+* userDisabled
+* appDisabled
+* version
+* type
+* scope
+* foreignInstall
+* hasBinaryComponents
+* installDay
+* updateDay
+
+With the exception of *installDay* and *updateDay*, all these properties
+come direct from the Addon instance. See https://developer.mozilla.org/en-US/docs/Addons/Add-on_Manager/Addon.
+*installDay* and *updateDay* are the number of days since UNIX epoch of
+the add-ons *installDate* and *updateDate* properties, respectively.
+
+Notes
+^^^^^
+
+Add-ons that have opted out of AMO updates via the
+*extensions._id_.getAddons.cache.enabled* preference are, since Bug 868306
+(Firefox 24), included in the list of submitted add-ons.
+
+Example
+^^^^^^^
+::
+
+ "org.mozilla.addons.addons": {
+ "_v": 2,
+ "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}": {
+ "userDisabled": false,
+ "appDisabled": false,
+ "name": "Adblock Plus",
+ "version": "2.4.1",
+ "type": "extension",
+ "scope": 1,
+ "description": "Ads were yesterday!",
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 16093,
+ "updateDay": 16093
+ },
+ "{e4a8a97b-f2ed-450b-b12d-ee082ba24781}": {
+ "userDisabled": true,
+ "appDisabled": false,
+ "name": "Greasemonkey",
+ "version": "1.14",
+ "type": "extension",
+ "scope": 1,
+ "description": "A User Script Manager for Firefox",
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 16093,
+ "updateDay": 16093
+ }
+ }
+
+org.mozilla.addons.plugins
+--------------------------
+
+This measurement contains information about the currently-installed plugins.
+
+Version 1
+^^^^^^^^^
+
+The measurement object is a mapping of plugin IDs to objects containing
+plugin metadata.
+
+The plugin ID is constructed of the plugins filename, name, version and
+description. Every plugin has at least a filename and a name.
+
+Each plugin contains the following properties:
+
+* name
+* version
+* description
+* blocklisted
+* disabled
+* clicktoplay
+* mimeTypes
+* updateDay
+
+With the exception of *updateDay* and *mimeTypes*, all these properties come
+directly from ``nsIPluginTag`` via ``nsIPluginHost``.
+*updateDay* is the number of days since UNIX epoch of the plugins last modified
+time.
+*mimeTypes* is the list of mimetypes the plugin supports, see
+``nsIPluginTag.getMimeTypes()``.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.addons.plugins": {
+ "_v": 1,
+ "Flash Player.plugin:Shockwave Flash:12.0.0.38:Shockwave Flash 12.0 r0": {
+ "mimeTypes": [
+ "application/x-shockwave-flash",
+ "application/futuresplash"
+ ],
+ "name": "Shockwave Flash",
+ "version": "12.0.0.38",
+ "description": "Shockwave Flash 12.0 r0",
+ "blocklisted": false,
+ "disabled": false,
+ "clicktoplay": false
+ },
+ "Default Browser.plugin:Default Browser Helper:537:Provides information about the default web browser": {
+ "mimeTypes": [
+ "application/apple-default-browser"
+ ],
+ "name": "Default Browser Helper",
+ "version": "537",
+ "description": "Provides information about the default web browser",
+ "blocklisted": false,
+ "disabled": true,
+ "clicktoplay": false
+ }
+ }
+
+org.mozilla.addons.counts
+-------------------------
+
+This measurement contains information about historical add-on counts.
+
+Version 1
+^^^^^^^^^
+
+The measurement object consists of counts of different add-on types. The
+properties are:
+
+extension
+ Integer count of installed extensions.
+plugin
+ Integer count of installed plugins.
+theme
+ Integer count of installed themes.
+lwtheme
+ Integer count of installed lightweigh themes.
+
+Notes
+^^^^^
+
+Add-ons opted out of AMO updates are included in the counts. This differs from
+the behavior of the active add-ons measurement.
+
+If no add-ons of a particular type are installed, the property for that type
+will not be present (as opposed to an explicit property with value of 0).
+
+Example
+^^^^^^^
+
+::
+
+ "2013-03-14": {
+ "org.mozilla.addons.counts": {
+ "_v": 1,
+ "extension": 21,
+ "plugin": 4,
+ "theme": 1
+ }
+ }
+
+
+
+org.mozilla.appInfo.appinfo
+---------------------------
+
+This measurement contains basic XUL application and Gecko platform
+information. It is reported in the *last* section.
+
+Version 2
+^^^^^^^^^
+
+In addition to fields present in version 1, this version has the following
+fields appearing in the *days* section:
+
+isBlocklistEnabled
+ Whether the blocklist ping is enabled. This is an integer, 0 or 1.
+ This does not indicate whether the blocklist ping was sent but merely
+ whether the application will try to send the blocklist ping.
+
+isTelemetryEnabled
+ Whether Telemetry is enabled. This is an integer, 0 or 1.
+
+Version 1
+^^^^^^^^^
+
+The measurement object contains mostly string values describing the
+current application and build. The properties are:
+
+* vendor
+* name
+* id
+* version
+* appBuildID
+* platformVersion
+* platformBuildID
+* os
+* xpcomabi
+* updateChannel
+* distributionID
+* distributionVersion
+* hotfixVersion
+* locale
+* isDefaultBrowser
+
+Notes
+^^^^^
+
+All of the properties appear in the *last* section except for
+*isDefaultBrowser*, which appears under *days*.
+
+Example
+^^^^^^^
+
+This example comes from an official OS X Nightly build::
+
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "appBuildID": "20130311030946",
+ "distributionID": "",
+ "distributionVersion": "",
+ "hotfixVersion": "",
+ "id": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "locale": "en-US",
+ "name": "Firefox",
+ "os": "Darwin",
+ "platformBuildID": "20130311030946",
+ "platformVersion": "22.0a1",
+ "updateChannel": "nightly",
+ "vendor": "Mozilla",
+ "version": "22.0a1",
+ "xpcomabi": "x86_64-gcc3"
+ },
+
+org.mozilla.appInfo.update
+--------------------------
+
+This measurement contains information about the application update mechanism
+in the application.
+
+Version 1
+^^^^^^^^^
+
+The following daily values are reported:
+
+enabled
+ Whether automatic application update checking is enabled. 1 for yes,
+ 0 for no.
+autoDownload
+ Whether automatic download of available updates is enabled.
+
+Notes
+^^^^^
+
+This measurement was merged to mozilla-central for JS FHR on 2013-07-15.
+
+Example
+^^^^^^^
+
+::
+
+ "2013-07-15": {
+ "org.mozilla.appInfo.update": {
+ "_v": 1,
+ "enabled": 1,
+ "autoDownload": 1,
+ }
+ }
+
+org.mozilla.appInfo.versions
+----------------------------
+
+This measurement contains a history of application version numbers.
+
+Version 2
+^^^^^^^^^
+
+Version 2 reports more fields than version 1 and is not backwards compatible.
+The following fields are present in version 2:
+
+appVersion
+ An array of application version strings.
+appBuildID
+ An array of application build ID strings.
+platformVersion
+ An array of platform version strings.
+platformBuildID
+ An array of platform build ID strings.
+
+When the application is upgraded, the new version and/or build IDs are
+appended to their appropriate fields.
+
+Version 1
+^^^^^^^^^
+
+When the application version (*version* from *org.mozilla.appinfo.appinfo*)
+changes, we record the new version on the day the change was seen. The new
+versions for a day are recorded in an array under the *version* property.
+
+Notes
+^^^^^
+
+If the application isn't upgraded, this measurement will not be present.
+This means this measurement will not be present for most days if a user is
+on the release channel (since updates are typically released every 6 weeks).
+However, users on the Nightly and Aurora channels will likely have a lot
+of these entries since those builds are updated every day.
+
+Values for this measurement are collected when performing the daily
+collection (typically occurs at upload time). As a result, it's possible
+the actual upgrade day may not be attributed to the proper day - the
+reported day may lag behind.
+
+The app and platform versions and build IDs should be identical for most
+clients. If they are different, we are possibly looking at a *Frankenfox*.
+
+Example
+^^^^^^^
+
+::
+
+ "2013-03-27": {
+ "org.mozilla.appInfo.versions": {
+ "_v": 2,
+ "appVersion": [
+ "22.0.0"
+ ],
+ "appBuildID": [
+ "20130325031100"
+ ],
+ "platformVersion": [
+ "22.0.0"
+ ],
+ "platformBuildID": [
+ "20130325031100"
+ ]
+ }
+ }
+
+org.mozilla.appSessions.current
+-------------------------------
+
+This measurement contains information about the currently running XUL
+application's session.
+
+Version 3
+^^^^^^^^^
+
+This measurement has the following properties:
+
+startDay
+ Integer days since UNIX epoch when this session began.
+activeTicks
+ Integer count of *ticks* the session was active for. Gecko periodically
+ sends out a signal when the session is active. Session activity
+ involves keyboard or mouse interaction with the application. Each tick
+ represents a window of 5 seconds where there was interaction.
+totalTime
+ Integer seconds the session has been alive.
+main
+ Integer milliseconds it took for the Gecko process to start up.
+firstPaint
+ Integer milliseconds from process start to first paint.
+sessionRestored
+ Integer milliseconds from process start to session restore.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.appSessions.current": {
+ "_v": 3,
+ "startDay": 15775,
+ "activeTicks": 4282,
+ "totalTime": 249422,
+ "main": 851,
+ "firstPaint": 3271,
+ "sessionRestored": 5998
+ }
+
+org.mozilla.appSessions.previous
+--------------------------------
+
+This measurement contains information about previous XUL application sessions.
+
+Version 3
+^^^^^^^^^
+
+This measurement contains per-day lists of all the sessions started on that
+day. The following properties may be present:
+
+cleanActiveTicks
+ Active ticks of sessions that were properly shut down.
+cleanTotalTime
+ Total number of seconds for sessions that were properly shut down.
+abortedActiveTicks
+ Active ticks of sessions that were not properly shut down.
+abortedTotalTime
+ Total number of seconds for sessions that were not properly shut down.
+main
+ Time in milliseconds from process start to main process initialization.
+firstPaint
+ Time in milliseconds from process start to first paint.
+sessionRestored
+ Time in milliseconds from process start to session restore.
+
+Notes
+^^^^^
+
+Sessions are recorded on the date on which they began.
+
+If a session was aborted/crashed, the total time may be less than the actual
+total time. This is because we don't always update total time during periods
+of inactivity and the abort/crash could occur after a long period of idle,
+before we've updated the total time.
+
+The lengths of the arrays for {cleanActiveTicks, cleanTotalTime},
+{abortedActiveTicks, abortedTotalTime}, and {main, firstPaint, sessionRestored}
+should all be identical.
+
+The length of the clean sessions plus the length of the aborted sessions should
+be equal to the length of the {main, firstPaint, sessionRestored} properties.
+
+It is not possible to distinguish the main, firstPaint, and sessionRestored
+values from a clean vs aborted session: they are all lumped together.
+
+For sessions spanning multiple UTC days, it's not possible to know which
+days the session was active for. It's possible a week long session only
+had activity for 2 days and there's no way for us to tell which days.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.appSessions.previous": {
+ "_v": 3,
+ "cleanActiveTicks": [
+ 78,
+ 1785
+ ],
+ "cleanTotalTime": [
+ 4472,
+ 88908
+ ],
+ "main": [
+ 32,
+ 952
+ ],
+ "firstPaint": [
+ 2755,
+ 3497
+ ],
+ "sessionRestored": [
+ 5149,
+ 5520
+ ]
+ }
+
+org.mozilla.crashes.crashes
+---------------------------
+
+This measurement contains a historical record of application crashes.
+
+Version 6
+^^^^^^^^^
+
+This version adds tracking for out-of-memory (OOM) crashes in the main process.
+An OOM crash will be counted as both main-crash and main-crash-oom.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* gmplugin-crash
+* gmplugin-crash-submission-succeeded
+* gmplugin-crash-submission-failed
+* main-crash
+* main-crash-oom
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
+Version 5
+^^^^^^^^^
+
+This version adds support for Gecko media plugin (GMP) crashes.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* gmplugin-crash
+* gmplugin-crash-submission-succeeded
+* gmplugin-crash-submission-failed
+* main-crash
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
+Version 4
+^^^^^^^^^
+
+This version follows up from version 3, adding submissions which are now
+tracked by the :ref:`crashes_crashmanager`.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* main-crash
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
+Version 3
+^^^^^^^^^
+
+This version follows up from version 2, building on improvements to
+the :ref:`crashes_crashmanager`.
+
+This measurement will be reported on each day there was a
+crash. Records may contain the following fields, whose values indicate
+the number of crashes or hangs that occurred on the given day:
+
+* main-crash
+* main-hang
+* content-crash
+* content-hang
+* plugin-crash
+* plugin-hang
+
+Version 2
+^^^^^^^^^
+
+The switch to version 2 coincides with the introduction of the
+:ref:`crashes_crashmanager`, which provides a more robust source of
+crash data.
+
+This measurement will be reported on each day there was a crash. The
+following fields may be present in each record:
+
+mainCrash
+ The number of main process crashes that occurred on the given day.
+
+Yes, version 2 does not track submissions like version 1. It is very
+likely submissions will be re-added later.
+
+Also absent from version 2 are plugin crashes and hangs. These will be
+re-added, likely in version 3.
+
+Version 1
+^^^^^^^^^
+
+This measurement will be reported on each day there was a crash. The
+following properties are reported:
+
+pending
+ The number of crash reports that haven't been submitted.
+submitted
+ The number of crash reports that were submitted.
+
+Notes
+^^^^^
+
+Main process crashes are typically submitted immediately after they
+occur (by checking a box in the crash reporter, which should appear
+automatically after a crash). If the crash reporter submits the crash
+successfully, we get a submitted crash. Else, we leave it as pending.
+
+A pending crash does not mean it will eventually be submitted.
+
+Pending crash reports can be submitted post-crash by going to
+about:crashes.
+
+If a pending crash is submitted via about:crashes, the submitted count
+increments but the pending count does not decrement. This is because FHR
+does not know which pending crash was just submitted and therefore it does
+not know which day's pending crash to decrement.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.crashes.crashes": {
+ "_v": 1,
+ "pending": 1,
+ "submitted": 2
+ },
+ "org.mozilla.crashes.crashes": {
+ "_v": 2,
+ "mainCrash": 2
+ }
+ "org.mozilla.crashes.crashes": {
+ "_v": 4,
+ "main-crash": 2,
+ "main-crash-submission-succeeded": 1,
+ "main-crash-submission-failed": 1,
+ "main-hang": 1,
+ "plugin-crash": 2
+ }
+
+org.mozilla.healthreport.submissions
+------------------------------------
+
+This measurement contains a history of FHR's own data submission activity.
+It was added in Firefox 23 in early May 2013.
+
+Version 2
+^^^^^^^^^
+
+This is the same as version 1 except an additional field has been added.
+
+uploadAlreadyInProgress
+ A request for upload was initiated while another upload was in progress.
+ This should not occur in well-behaving clients. It (along with a lock
+ preventing simultaneous upload) was added to ensure this never occurs.
+
+Version 1
+^^^^^^^^^
+
+Daily counts of upload events are recorded.
+
+firstDocumentUploadAttempt
+ An attempt was made to upload the client's first document to the server.
+ These are uploads where the client is not aware of a previous document ID
+ on the server. Unless the client had disabled upload, there should be at
+ most one of these in the history of the client.
+
+continuationUploadAttempt
+ An attempt was made to upload a document that replaces an existing document
+ on the server. Most upload attempts should be attributed to this as opposed
+ to *firstDocumentUploadAttempt*.
+
+uploadSuccess
+ The upload attempt recorded by *firstDocumentUploadAttempt* or
+ *continuationUploadAttempt* was successful.
+
+uploadTransportFailure
+ An upload attempt failed due to transport failure (network unavailable,
+ etc).
+
+uploadServerFailure
+ An upload attempt failed due to a server-reported failure. Ideally these
+ are failures reported by the FHR server itself. However, intermediate
+ proxies, firewalls, etc may trigger this depending on how things are
+ configured.
+
+uploadClientFailure
+ An upload attempt failued due to an error/exception in the client.
+ This almost certainly points to a bug in the client.
+
+The result for an upload attempt is always attributed to the same day as
+the attempt, even if the result occurred on a different day from the attempt.
+Therefore, the sum of the result counts should equal the result of the attempt
+counts.
+
+org.mozilla.hotfix.update
+-------------------------
+
+This measurement contains results from the Firefox update hotfix.
+
+The Firefox update hotfix bypasses the built-in application update mechanism
+and installs a modern Firefox.
+
+Version 1
+^^^^^^^^^
+
+The fields in this measurement are dynamically created based on which
+versions of the update hotfix state file are found on disk.
+
+The general format of the fields is ``<version>.<thing>`` where ``version``
+is a hotfix version like ``v20140527`` and ``thing`` is a key from the
+hotfix state file, e.g. ``upgradedFrom``. Here are some of the ``things``
+that can be defined.
+
+upgradedFrom
+ String identifying the Firefox version that the hotfix upgraded from.
+ e.g. ``16.0`` or ``17.0.1``.
+
+uninstallReason
+ String with enumerated values identifying why the hotfix was uninstalled.
+ Value will be ``STILL_INSTALLED`` if the hotfix is still installed.
+
+downloadAttempts
+ Integer number of times the hotfix started downloading an installer.
+ Download resumes are part of this count.
+
+downloadFailures
+ Integer count of times a download supposedly completed but couldn't
+ be validated. This likely represents something wrong with the network
+ connection. The ratio of this to ``downloadAttempts`` should be low.
+
+installAttempts
+ Integer count of times the hotfix attempted to run the installer.
+ This should ideally be 1. It should only be greater than 1 if UAC
+ elevation was cancelled or not allowed.
+
+installFailures
+ Integer count of total installation failures this client experienced.
+ Can be 0. ``installAttempts - installFailures`` implies install successes.
+
+notificationsShown
+ Integer count of times a notification was displayed to the user that
+ they are running an older Firefox.
+
+org.mozilla.places.places
+-------------------------
+
+This measurement contains information about the Places database (where Firefox
+stores its history and bookmarks).
+
+Version 1
+^^^^^^^^^
+
+Daily counts of items in the database are reported in the following properties:
+
+bookmarks
+ Integer count of bookmarks present.
+pages
+ Integer count of pages in the history database.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.places.places": {
+ "_v": 1,
+ "bookmarks": 388,
+ "pages": 94870
+ }
+
+org.mozilla.profile.age
+-----------------------
+
+This measurement contains information about the current profile's age (and
+in version 2, the profile's most recent reset date)
+
+Version 2
+^^^^^^^^^
+
+*profileCreation* and *profileReset* properties are present. Both define
+the integer days since UNIX epoch that the current profile was created or
+reset accordingly.
+
+Version 1
+^^^^^^^^^
+
+A single *profileCreation* property is present. It defines the integer
+days since UNIX epoch that the current profile was created.
+
+Notes
+^^^^^
+
+It is somewhat difficult to obtain a reliable *profile born date* due to a
+number of factors, but since Version 2, improvements have been made - on a
+"profile reset" we copy the profileCreation date from the old profile and
+record the time of the reset in profileReset.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.profile.age": {
+ "_v": 2,
+ "profileCreation": 15176
+ "profileReset": 15576
+ }
+
+org.mozilla.searches.counts
+---------------------------
+
+This measurement contains information about searches performed in the
+application.
+
+Version 6 (mobile)
+^^^^^^^^^^^^^^^^^^
+
+This adds two new search locations: *widget* and *activity*, corresponding to the search widget and search activity respectively.
+
+Version 2
+^^^^^^^^^
+
+This behaves like version 1 except we added all search engines that
+Mozilla has a partner agreement with. Like version 1, we concatenate
+a search engine ID with a search origin.
+
+Another difference with version 2 is we should no longer misattribute
+a search to the *other* bucket if the search engine name is localized.
+
+The set of search engine providers is:
+
+* amazon-co-uk
+* amazon-de
+* amazon-en-GB
+* amazon-france
+* amazon-it
+* amazon-jp
+* amazondotcn
+* amazondotcom
+* amazondotcom-de
+* aol-en-GB
+* aol-web-search
+* bing
+* eBay
+* eBay-de
+* eBay-en-GB
+* eBay-es
+* eBay-fi
+* eBay-france
+* eBay-hu
+* eBay-in
+* eBay-it
+* google
+* google-jp
+* google-ku
+* google-maps-zh-TW
+* mailru
+* mercadolibre-ar
+* mercadolibre-cl
+* mercadolibre-mx
+* seznam-cz
+* twitter
+* twitter-de
+* twitter-ja
+* yahoo
+* yahoo-NO
+* yahoo-answer-zh-TW
+* yahoo-ar
+* yahoo-bid-zh-TW
+* yahoo-br
+* yahoo-ch
+* yahoo-cl
+* yahoo-de
+* yahoo-en-GB
+* yahoo-es
+* yahoo-fi
+* yahoo-france
+* yahoo-fy-NL
+* yahoo-id
+* yahoo-in
+* yahoo-it
+* yahoo-jp
+* yahoo-jp-auctions
+* yahoo-mx
+* yahoo-sv-SE
+* yahoo-zh-TW
+* yandex
+* yandex-ru
+* yandex-slovari
+* yandex-tr
+* yandex.by
+* yandex.ru-be
+
+And of course, *other*.
+
+The sources for searches remain:
+
+* abouthome
+* contextmenu
+* searchbar
+* urlbar
+
+The measurement will only be populated with providers and sources that
+occurred that day.
+
+If a user switches locales, searches from default providers on the older
+locale will still be supported. However, if that same search engine is
+added by the user to the new build and is *not* a default search engine
+provider, its searches will be attributed to the *other* bucket.
+
+Version 1
+^^^^^^^^^
+
+We record counts of performed searches grouped by search engine and search
+origin. Only search engines with which Mozilla has a business relationship
+are explicitly counted. All other search engines are grouped into an
+*other* bucket.
+
+The following search engines are explicitly counted:
+
+* Amazon.com
+* Bing
+* Google
+* Yahoo
+* Other
+
+The following search origins are distinguished:
+
+about:home
+ Searches initiated from the search text box on about:home.
+context menu
+ Searches initiated from the context menu (highlight text, right click,
+ and select "search for...")
+search bar
+ Searches initiated from the search bar (the text field next to the
+ Awesomebar)
+url bar
+ Searches initiated from the awesomebar/url bar.
+
+Due to the localization of search engine names, non en-US locales may wrongly
+attribute searches to the *other* bucket. This is fixed in version 2.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.searches.counts": {
+ "_v": 1,
+ "google.searchbar": 3,
+ "google.urlbar": 7
+ },
+
+org.mozilla.searches.engines
+----------------------------
+
+This measurement contains information about search engines.
+
+Version 1
+^^^^^^^^^
+
+This version debuted with Firefox 31 on desktop. It contains the
+following properties:
+
+default
+ Daily string identifier or name of the default search engine provider.
+
+ This field will only be collected if Telemetry is enabled. If
+ Telemetry is enabled and then later disabled, this field may
+ disappear from future days in the payload.
+
+ The special value ``NONE`` could occur if there is no default search
+ engine.
+
+ The special value ``UNDEFINED`` could occur if a default search
+ engine exists but its identifier could not be determined.
+
+ This field's contents are
+ ``Services.search.defaultEngine.identifier`` (if defined) or
+ ``"other-"`` + ``Services.search.defaultEngine.name`` if not.
+ In other words, search engines without an ``.identifier``
+ are prefixed with ``other-``.
+
+Version 2
+^^^^^^^^^
+
+Starting with Firefox 40, there is an additional optional value:
+
+cohort
+ Daily cohort string identifier, recorded if the user is part of
+ search defaults A/B testing.
+
+org.mozilla.sync.sync
+---------------------
+
+This daily measurement contains information about the Sync service.
+
+Values should be recorded for every day FHR measurements occurred.
+
+Version 1
+^^^^^^^^^
+
+This version debuted with Firefox 30 on desktop. It contains the following
+properties:
+
+enabled
+ Daily numeric indicating whether Sync is configured and enabled. 1 if so,
+ 0 otherwise.
+
+preferredProtocol
+ String version of the maximum Sync protocol version the client supports.
+ This will be ``1.1`` for for legacy Sync and ``1.5`` for clients that
+ speak the Firefox Accounts protocol.
+
+actualProtocol
+ The actual Sync protocol version the client is configured to use.
+
+ This will be ``1.1`` if the client is configured with the legacy Sync
+ service or if the client only supports ``1.1``.
+
+ It will be ``1.5`` if the client supports ``1.5`` and either a) the
+ client is not configured b) the client is using Firefox Accounts Sync.
+
+syncStart
+ Count of sync operations performed.
+
+syncSuccess
+ Count of sync operations that completed successfully.
+
+syncError
+ Count of sync operations that did not complete successfully.
+
+ This is a measure of overall sync success. This does *not* reflect
+ recoverable errors (such as record conflict) that can occur during
+ sync. This is thus a rough proxy of whether the sync service is
+ operating without error.
+
+org.mozilla.sync.devices
+------------------------
+
+This daily measurement contains information about the device type composition
+for the configured Sync account.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 30.
+
+Field names are dynamic according to the client-reported device types from
+Sync records. All fields are daily last seen integer values corresponding to
+the number of devices of that type.
+
+Common values include:
+
+desktop
+ Corresponds to a Firefox desktop client.
+
+mobile
+ Corresponds to a Fennec client.
+
+org.mozilla.sync.migration
+--------------------------
+
+This daily measurement contains information about sync migration (that is, the
+semi-automated process of migrating a legacy sync account to an FxA account.)
+
+Measurements will start being recorded after a migration is offered by the
+sync server and stop after migration is complete or the user elects to "unlink"
+their sync account. In other words, it is expected that users with Sync setup
+for FxA or with sync unconfigured will not collect data, and that for users
+where data is collected, the collection will only be for a relatively short
+period.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 37 and includes the following properties:
+
+state
+ Corresponds to either a STATE_USER_* string or a STATE_INTERNAL_* string in
+ FxaMigration.jsm. This reflects a state where we are waiting for the user,
+ or waiting for some internal process to complete on the way to completing
+ the migration.
+
+declined
+ Corresponds to the number of times the user closed the migration infobar.
+
+unlinked
+ Set if the user declined to migrate and instead "unlinked" Sync from the
+ browser.
+
+accepted
+ Corresponds to the number of times the user explicitly elected to start or
+ continue the migration - it counts how often the user clicked on any UI
+ created specifically for migration. The "ideal" UX for migration would see
+ this at exactly 1, some known edge-cases (eg, browser restart required to
+ finish) could expect this to be 2, and anything more means we are doing
+ something wrong.
+
+org.mozilla.sysinfo.sysinfo
+---------------------------
+
+This measurement contains basic information about the system the application
+is running on.
+
+Version 2
+^^^^^^^^^
+
+This version debuted with Firefox 29 on desktop.
+
+A single property was introduced.
+
+isWow64
+ If present, this property indicates whether the machine supports WoW64.
+ This property can be used to identify whether the host machine is 64-bit.
+
+ This property is only present on Windows machines. It is the preferred way
+ to identify 32- vs 64-bit support in that environment.
+
+Version 1
+^^^^^^^^^
+
+The following properties may be available:
+
+cpuCount
+ Integer number of CPUs/cores in the machine.
+memoryMB
+ Integer megabytes of memory in the machine.
+manufacturer
+ The manufacturer of the device.
+device
+ The name of the device (like model number).
+hardware
+ Unknown.
+name
+ OS name.
+version
+ OS version.
+architecture
+ OS architecture that the application is built for. This is not the
+ actual system architecture.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.sysinfo.sysinfo": {
+ "_v": 1,
+ "cpuCount": 8,
+ "memoryMB": 8192,
+ "architecture": "x86-64",
+ "name": "Darwin",
+ "version": "12.2.0"
+ }
+
+
+org.mozilla.translation.translation
+-----------------------------------
+
+This daily measurement contains information about the usage of the translation
+feature. It is a special telemetry measurement which will only be recorded in
+FHR if telemetry is enabled.
+
+Version 1
+^^^^^^^^^
+
+Daily counts are reported in the following properties:
+
+translationOpportunityCount
+ Integer count of the number of opportunities there were to translate a page.
+missedTranslationOpportunityCount
+ Integer count of the number of missed opportunities there were to translate a page.
+ A missed opportunity is when the page language is not supported by the translation
+ provider.
+pageTranslatedCount
+ Integer count of the number of pages translated.
+charactersTranslatedCount
+ Integer count of the number of characters translated.
+detectedLanguageChangedBefore
+ Integer count of the number of times the user manually adjusted the detected
+ language before translating.
+detectedLanguageChangedAfter
+ Integer count of the number of times the user manually adjusted the detected
+ language after having first translated the page.
+targetLanguageChanged
+ Integer count of the number of times the user manually adjusted the target
+ language.
+deniedTranslationOffer
+ Integer count of the number of times the user opted-out offered
+ page translation, either by the Not Now button or by the notification's
+ close button in the "offer" state.
+autoRejectedTranlationOffer
+ Integer count of the number of times the user is not offered page
+ translation because they had previously clicked "Never translate this
+ language" or "Never translate this site".
+showOriginalContent
+ Integer count of the number of times the user activated the Show Original
+ command.
+
+Additional daily counts broken down by language are reported in the following
+properties:
+
+translationOpportunityCountsByLanguage
+ A mapping from language to count of opportunities to translate that
+ language.
+missedTranslationOpportunityCountsByLanguage
+ A mapping from language to count of missed opportunities to translate that
+ language.
+pageTranslatedCountsByLanguage
+ A mapping from language to the counts of pages translated from that
+ language. Each language entry will be an object containing a "total" member
+ along with individual counts for each language translated to.
+
+Other properties:
+
+detectLanguageEnabled
+ Whether automatic language detection is enabled. This is an integer, 0 or 1.
+showTranslationUI
+ Whether the translation feature UI will be shown. This is an integer, 0 or 1.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.translation.translation": {
+ "_v": 1,
+ "detectLanguageEnabled": 1,
+ "showTranslationUI": 1,
+ "translationOpportunityCount": 134,
+ "missedTranslationOpportunityCount": 32,
+ "pageTranslatedCount": 6,
+ "charactersTranslatedCount": "1126",
+ "detectedLanguageChangedBefore": 1,
+ "detectedLanguageChangedAfter": 2,
+ "targetLanguageChanged": 0,
+ "deniedTranslationOffer": 3,
+ "autoRejectedTranlationOffer": 1,
+ "showOriginalContent": 2,
+ "translationOpportunityCountsByLanguage": {
+ "fr": 100,
+ "es": 34
+ },
+ "missedTranslationOpportunityCountsByLanguage": {
+ "it": 20,
+ "nl": 10,
+ "fi": 2
+ },
+ "pageTranslatedCountsByLanguage": {
+ "fr": {
+ "total": 6,
+ "es": 5,
+ "en": 1
+ }
+ }
+ }
+
+
+org.mozilla.experiments.info
+----------------------------------
+
+Daily measurement reporting information about the Telemetry Experiments service.
+
+Version 1
+^^^^^^^^^
+
+Property:
+
+lastActive
+ ID of the final Telemetry Experiment that is active on a given day, if any.
+
+
+Version 2
+^^^^^^^^^
+
+Adds an additional optional property:
+
+lastActiveBranch
+ If the experiment uses branches, the branch identifier string.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.experiments.info": {
+ "_v": 2,
+ "lastActive": "some.experiment.id",
+ "lastActiveBranch": "control"
+ }
+
+org.mozilla.uitour.treatment
+----------------------------
+
+Daily measurement reporting information about treatment tagging done
+by the UITour module.
+
+Version 1
+^^^^^^^^^
+
+Daily text values in the following properties:
+
+<tag>:
+ Array of discrete strings corresponding to calls for setTreatmentTag(tag, value).
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.uitour.treatment": {
+ "_v": 1,
+ "treatment": [
+ "optin",
+ "optin-DNT"
+ ],
+ "another-tag": [
+ "foobar-value"
+ ]
+ }
+
+org.mozilla.passwordmgr.passwordmgr
+-----------------------------------
+
+Daily measurement reporting information about the Password Manager
+
+Version 1
+^^^^^^^^^
+
+Property:
+
+numSavedPasswords
+ number of passwords saved in the Password Manager
+
+enabled
+ Whether or not the user has disabled the Password Manager in prefernces
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.passwordmgr.passwordmgr": {
+ "_v": 1,
+ "numSavedPasswords": 5,
+ "enabled": 0,
+ }
+
+Version 2
+^^^^^^^^^
+
+More detailed measurements of login forms & their behavior
+
+numNewSavedPasswordsInSession
+ Number of passwords saved to the password manager this session.
+
+numSuccessfulFills
+ Number of times the password manager filled in password fields for user this session.
+
+numTotalLoginsEncountered
+ Number of times a login form was encountered by the user in the session.
+
+Example
+^^^^^^^
+
+::
+ "org.mozilla.passwordmgr.passwordmgr": {
+ "_v": 2,
+ "numSavedPasswords": 32,
+ "enabled": 1,
+ "numNewSavedPasswords": 5,
+ "numSuccessfulFills": 11,
+ "numTotalLoginsEncountered": 23,
+ }
diff --git a/toolkit/components/telemetry/docs/fhr/identifiers.rst b/toolkit/components/telemetry/docs/fhr/identifiers.rst
new file mode 100644
index 0000000000..82ad0e49e6
--- /dev/null
+++ b/toolkit/components/telemetry/docs/fhr/identifiers.rst
@@ -0,0 +1,83 @@
+.. _healthreport_identifiers:
+
+===========
+Identifiers
+===========
+
+Firefox Health Report records some identifiers to keep track of clients
+and uploaded documents.
+
+Identifier Types
+================
+
+Document/Upload IDs
+-------------------
+
+A random UUID called the *Document ID* or *Upload ID* is generated when the FHR
+client creates or uploads a new document.
+
+When clients generate a new *Document ID*, they persist this ID to disk
+**before** the upload attempt.
+
+As part of the upload, the client sends all old *Document IDs* to the server
+and asks the server to delete them. In well-behaving clients, the server
+has a single record for each client with a randomly-changing *Document ID*.
+
+Client IDs
+----------
+
+A *Client ID* is an identifier that **attempts** to uniquely identify an
+individual FHR client. Please note the emphasis on *attempts* in that last
+sentence: *Client IDs* do not guarantee uniqueness.
+
+The *Client ID* is generated when the client first runs or as needed.
+
+The *Client ID* is transferred to the server as part of every upload. The
+server is thus able to affiliate multiple document uploads with a single
+*Client ID*.
+
+Client ID Versions
+^^^^^^^^^^^^^^^^^^
+
+The semantics for how a *Client ID* is generated are versioned.
+
+Version 1
+ The *Client ID* is a randomly-generated UUID.
+
+History of Identifiers
+======================
+
+In the beginning, there were just *Document IDs*. The thinking was clients
+would clean up after themselves and leave at most 1 active document on the
+server.
+
+Unfortunately, this did not work out. Using brute force analysis to
+deduplicate records on the server, a number of interesting patterns emerged.
+
+Orphaning
+ Clients would upload a new payload while not deleting the old payload.
+
+Divergent records
+ Records would share data up to a certain date and then the data would
+ almost completely diverge. This appears to be indicative of profile
+ copying.
+
+Rollback
+ Records would share data up to a certain date. Each record in this set
+ would contain data for a day or two but no extra data. This could be
+ explained by filesystem rollback on the client.
+
+A significant percentage of the records on the server belonged to
+misbehaving clients. Identifying these records was extremely resource
+intensive and error-prone. These records were undermining the ability
+to use Firefox Health Report data.
+
+Thus, the *Client ID* was born. The intent of the *Client ID* was to
+uniquely identify clients so the extreme effort required and the
+questionable reliability of deduplicating server data would become
+problems of the past.
+
+The *Client ID* was originally a randomly-generated UUID (version 1). This
+allowed detection of orphaning and rollback. However, these version 1
+*Client IDs* were still susceptible to use on multiple profiles and
+machines if the profile was copied.
diff --git a/toolkit/components/telemetry/docs/fhr/index.rst b/toolkit/components/telemetry/docs/fhr/index.rst
new file mode 100644
index 0000000000..497385dd8f
--- /dev/null
+++ b/toolkit/components/telemetry/docs/fhr/index.rst
@@ -0,0 +1,34 @@
+================================
+Firefox Health Report (Obsolete)
+================================
+
+**Firefox Health Report (FHR) is obsolete and no longer ships with Firefox.
+This documentation will live here for a few more cycles.**
+
+Firefox Health Report is a background service that collects application
+metrics and periodically submits them to a central server. The core
+parts of the service are implemented in this directory. However, the
+actual XPCOM service is implemented in the
+``data_reporting_service`.
+
+The core types can actually be instantiated multiple times and used to
+power multiple data submission services within a single Gecko
+application. In other words, everything in this directory is effectively
+a reusable library. However, the terminology and some of the features
+are very specific to what the Firefox Health Report feature requires.
+
+.. toctree::
+ :maxdepth: 1
+
+ architecture
+ dataformat
+ identifiers
+
+Legal and Privacy Concerns
+==========================
+
+Because Firefox Health Report collects and submits data to remote
+servers and is an opt-out feature, there are legal and privacy
+concerns over what data may be collected and submitted. **Additions or
+changes to submitted data should be signed off by responsible
+parties.**
diff --git a/toolkit/components/telemetry/docs/index.rst b/toolkit/components/telemetry/docs/index.rst
new file mode 100644
index 0000000000..5d30c5e923
--- /dev/null
+++ b/toolkit/components/telemetry/docs/index.rst
@@ -0,0 +1,25 @@
+.. _telemetry:
+
+=========
+Telemetry
+=========
+
+Telemetry is a feature that allows data collection. This is being used to collect performance metrics and other information about how Firefox performs in the wild.
+
+Client-side, this consists of:
+
+* data collection in `Histograms <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Adding_a_new_Telemetry_probe>`_, :doc:`collection/scalars` and other data structures
+* assembling :doc:`concepts/pings` with the general information and the data payload
+* sending them to the server and local ping retention
+
+*Note:* the `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_ documents the process and requirements that are applied here.
+
+.. toctree::
+ :maxdepth: 5
+ :titlesonly:
+
+ concepts/index
+ collection/index
+ data/index
+ internals/index
+ fhr/index
diff --git a/toolkit/components/telemetry/docs/internals/index.rst b/toolkit/components/telemetry/docs/internals/index.rst
new file mode 100644
index 0000000000..e912ea49a5
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/index.rst
@@ -0,0 +1,9 @@
+=========
+Internals
+=========
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+
+ preferences
diff --git a/toolkit/components/telemetry/docs/internals/preferences.rst b/toolkit/components/telemetry/docs/internals/preferences.rst
new file mode 100644
index 0000000000..c8af2f2d58
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/preferences.rst
@@ -0,0 +1,119 @@
+Preferences
+===========
+
+Telemetry behaviour is controlled through the preferences listed here.
+
+Default behaviors
+-----------------
+
+Sending only happens on official builds (i.e. with ``MOZILLA_OFFICIAL`` set) with ``MOZ_TELEMETRY_REPORTING`` defined.
+All other builds drop all outgoing pings, so they will also not retry sending them later.
+
+Preferences
+-----------
+
+``toolkit.telemetry.unified``
+
+ This controls whether unified behavior is enabled. If true:
+
+ * Telemetry is always enabled and recording *base* data.
+ * Telemetry will send additional ``main`` pings.
+
+``toolkit.telemetry.enabled``
+
+ If ``unified`` is off, this controls whether the Telemetry module is enabled.
+ If ``unified`` is on, this controls whether to record *extended* data.
+ This preference is controlled through the `Preferences` dialog.
+
+ Note that the default value here of this pref depends on the define ``RELEASE_OR_BETA`` and the channel.
+ If ``RELEASE_OR_BETA`` is set, ``MOZ_TELEMETRY_ON_BY_DEFAULT`` gets set, which means this pref will default to ``true``.
+ This is overridden by the preferences code on the "beta" channel, the pref also defaults to ``true`` there.
+
+``datareporting.healthreport.uploadEnabled``
+
+ Send the data we record if user has consented to FHR. This preference is controlled through the `Preferences` dialog.
+
+``toolkit.telemetry.archive.enabled``
+
+ Allow pings to be archived locally. This can only be enabled if ``unified`` is on.
+
+``toolkit.telemetry.server``
+
+ The server Telemetry pings are sent to.
+
+``toolkit.telemetry.log.level``
+
+ This sets the Telemetry logging verbosity per ``Log.jsm``, with ``Trace`` or ``0`` being the most verbose and the default being ``Warn``.
+ By default logging goes only the console service.
+
+``toolkit.telemetry.log.dump``
+
+ Sets whether to dump Telemetry log messages to ``stdout`` too.
+
+Data-choices notification
+-------------------------
+
+``toolkit.telemetry.reportingpolicy.firstRun``
+
+ This preference is not present until the first run. After, its value is set to false. This is used to show the infobar with a more aggressive timeout if it wasn't shown yet.
+
+``datareporting.policy.firstRunURL``
+
+ If set, a browser tab will be opened on first run instead of the infobar.
+
+``datareporting.policy.dataSubmissionEnabled``
+
+ This is the data submission master kill switch. If disabled, no policy is shown or upload takes place, ever.
+
+``datareporting.policy.dataSubmissionPolicyNotifiedTime``
+
+ Records the date user was shown the policy. This preference is also used on Android.
+
+``datareporting.policy.dataSubmissionPolicyAcceptedVersion``
+
+ Records the version of the policy notified to the user. This preference is also used on Android.
+
+``datareporting.policy.dataSubmissionPolicyBypassNotification``
+
+ Used in tests, it allows to skip the notification check.
+
+``datareporting.policy.currentPolicyVersion``
+
+ Stores the current policy version, overrides the default value defined in TelemetryReportingPolicy.jsm.
+
+``datareporting.policy.minimumPolicyVersion``
+
+ The minimum policy version that is accepted for the current policy. This can be set per channel.
+
+``datareporting.policy.minimumPolicyVersion.channel-NAME``
+
+ This is the only channel-specific version that we currently use for the minimum policy version.
+
+Testing
+-------
+
+The following prefs are for testing purpose only.
+
+``toolkit.telemetry.initDelay``
+
+ Delay before initializing telemetry (seconds).
+
+``toolkit.telemetry.minSubsessionLength``
+
+ Minimum length of a telemetry subsession (seconds).
+
+``toolkit.telemetry.collectInterval``
+
+ Minimum interval between data collection (seconds).
+
+``toolkit.telemetry.scheduler.tickInterval``
+
+ Interval between scheduler ticks (seconds).
+
+``toolkit.telemetry.scheduler.idleTickInterval``
+
+ Interval between scheduler ticks when the user is idle (seconds).
+
+``toolkit.telemetry.idleTimeout``
+
+ Timeout until we decide whether a user is idle or not (seconds).
diff --git a/toolkit/components/telemetry/gen-event-data.py b/toolkit/components/telemetry/gen-event-data.py
new file mode 100644
index 0000000000..0884dbba0a
--- /dev/null
+++ b/toolkit/components/telemetry/gen-event-data.py
@@ -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/.
+
+# Write out event information for C++. The events are defined
+# in a file provided as a command-line argument.
+
+from __future__ import print_function
+from shared_telemetry_utils import StringTable, static_assert
+
+import parse_events
+import sys
+import itertools
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated, only for internal use in TelemetryEvent.h,
+ see gen-event-data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryEventData_h
+#define mozilla_TelemetryEventData_h
+#include "EventInfo.h"
+namespace {
+"""
+
+file_footer = """\
+} // namespace
+#endif // mozilla_TelemetryEventData_h
+"""
+
+def write_extra_table(events, output, string_table):
+ table_name = "gExtraKeysTable"
+ extra_table = []
+ extra_count = 0
+
+ print("const uint32_t %s[] = {" % table_name, file=output)
+
+ for e in events:
+ extra_index = 0
+ extra_keys = e.extra_keys
+ if len(extra_keys) > 0:
+ extra_index = extra_count
+ extra_count += len(extra_keys)
+ indexes = string_table.stringIndexes(extra_keys)
+
+ print(" // %s, [%s], [%s]" % (
+ e.category,
+ ", ".join(e.methods),
+ ", ".join(e.objects)),
+ file=output)
+ print(" // extra_keys: %s" % ", ".join(extra_keys), file=output)
+ print(" %s," % ", ".join(map(str, indexes)),
+ file=output)
+
+ extra_table.append((extra_index, len(extra_keys)))
+
+ print("};", file=output)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name,
+ "index overflow")
+
+ return extra_table
+
+def write_common_event_table(events, output, string_table, extra_table):
+ table_name = "gCommonEventInfo"
+ extra_count = 0
+
+ print("const CommonEventInfo %s[] = {" % table_name, file=output)
+ for e,extras in zip(events, extra_table):
+ # Write a comment to make the file human-readable.
+ print(" // category: %s" % e.category, file=output)
+ print(" // methods: [%s]" % ", ".join(e.methods), file=output)
+ print(" // objects: [%s]" % ", ".join(e.objects), file=output)
+
+ # Write the common info structure
+ print(" {%d, %d, %d, %d, %d, %s}," %
+ (string_table.stringIndex(e.category),
+ string_table.stringIndex(e.expiry_version),
+ extras[0], # extra keys index
+ extras[1], # extra keys count
+ e.expiry_day,
+ e.dataset),
+ file=output)
+
+ print("};", file=output)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name,
+ "index overflow")
+
+def write_event_table(events, output, string_table):
+ table_name = "gEventInfo"
+ print("const EventInfo %s[] = {" % table_name, file=output)
+
+ for common_info_index,e in enumerate(events):
+ for method_name, object_name in itertools.product(e.methods, e.objects):
+ print(" // category: %s, method: %s, object: %s" %
+ (e.category, method_name, object_name),
+ file=output)
+
+ print(" {gCommonEventInfo[%d], %d, %d}," %
+ (common_info_index,
+ string_table.stringIndex(method_name),
+ string_table.stringIndex(object_name)),
+ file=output)
+
+ print("};", file=output)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name,
+ "index overflow")
+
+def main(output, *filenames):
+ # Load the event data.
+ if len(filenames) > 1:
+ raise Exception('We don\'t support loading from more than one file.')
+ events = parse_events.load_events(filenames[0])
+
+ # Write the scalar data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+
+ # Write the extra keys table.
+ string_table = StringTable()
+ extra_table = write_extra_table(events, output, string_table)
+ print("", file=output)
+
+ # Write a table with the common event data.
+ write_common_event_table(events, output, string_table, extra_table)
+ print("", file=output)
+
+ # Write the data for individual events.
+ write_event_table(events, output, string_table)
+ print("", file=output)
+
+ # Write the string table.
+ string_table_name = "gEventsStringTable"
+ string_table.writeDefinition(output, string_table_name)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % string_table_name,
+ "index overflow")
+ print("", file=output)
+
+ print(file_footer, file=output)
+
+if __name__ == '__main__':
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/gen-event-enum.py b/toolkit/components/telemetry/gen-event-enum.py
new file mode 100644
index 0000000000..775ff84756
--- /dev/null
+++ b/toolkit/components/telemetry/gen-event-enum.py
@@ -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/.
+
+# Write out C++ enum definitions that represent the different event types.
+#
+# The events are defined in files provided as command-line arguments.
+
+from __future__ import print_function
+
+import sys
+import parse_events
+
+banner = """/* This file is auto-generated, see gen-event-enum.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryEventEnums_h
+#define mozilla_TelemetryEventEnums_h
+namespace mozilla {
+namespace Telemetry {
+namespace EventID {
+"""
+
+file_footer = """\
+} // namespace EventID
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryEventEnums_h
+"""
+
+def main(output, *filenames):
+ # Load the events first.
+ if len(filenames) > 1:
+ raise Exception('We don\'t support loading from more than one file.')
+ events = parse_events.load_events(filenames[0])
+
+ grouped = dict()
+ index = 0
+ for e in events:
+ category = e.category
+ if not category in grouped:
+ grouped[category] = []
+ grouped[category].append((index, e))
+ index += len(e.enum_labels)
+
+ # Write the enum file.
+ print(banner, file=output)
+ print(file_header, file=output);
+
+ for category,indexed in grouped.iteritems():
+ category_cpp = indexed[0][1].category_cpp
+
+ print("// category: %s" % category, file=output)
+ print("enum class %s : uint32_t {" % category_cpp, file=output)
+
+ for event_index,e in indexed:
+ cpp_guard = e.cpp_guard
+ if cpp_guard:
+ print("#if defined(%s)" % cpp_guard, file=output)
+ for offset,label in enumerate(e.enum_labels):
+ print(" %s = %d," % (label, event_index + offset), file=output)
+ if cpp_guard:
+ print("#endif", file=output)
+
+ print("};\n", file=output)
+
+ print("const uint32_t EventCount = %d;\n" % index, file=output)
+
+ print(file_footer, file=output)
+
+if __name__ == '__main__':
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/gen-histogram-bucket-ranges.py b/toolkit/components/telemetry/gen-histogram-bucket-ranges.py
new file mode 100644
index 0000000000..286bc0e7b2
--- /dev/null
+++ b/toolkit/components/telemetry/gen-histogram-bucket-ranges.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env 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/.
+
+# Write out detailed histogram information, including the ranges of the
+# buckets specified by each histogram.
+
+import sys
+import re
+import histogram_tools
+import json
+
+from collections import OrderedDict
+
+def main(argv):
+ filenames = argv
+
+ all_histograms = OrderedDict()
+
+ for histogram in histogram_tools.from_files(filenames):
+ name = histogram.name()
+ parameters = OrderedDict()
+ table = {
+ 'boolean': '2',
+ 'flag': '3',
+ 'enumerated': '1',
+ 'linear': '1',
+ 'exponential': '0',
+ 'count': '4',
+ }
+ # Use __setitem__ because Python lambdas are so limited.
+ histogram_tools.table_dispatch(histogram.kind(), table,
+ lambda k: parameters.__setitem__('kind', k))
+ if histogram.low() == 0:
+ parameters['min'] = 1
+ else:
+ parameters['min'] = histogram.low()
+
+ try:
+ buckets = histogram.ranges()
+ parameters['buckets'] = buckets
+ parameters['max'] = buckets[-1]
+ parameters['bucket_count'] = len(buckets)
+ except histogram_tools.DefinitionException:
+ continue
+
+ all_histograms.update({ name: parameters });
+
+ print json.dumps({ 'histograms': all_histograms})
+
+main(sys.argv[1:])
diff --git a/toolkit/components/telemetry/gen-histogram-data.py b/toolkit/components/telemetry/gen-histogram-data.py
new file mode 100644
index 0000000000..8e227201d6
--- /dev/null
+++ b/toolkit/components/telemetry/gen-histogram-data.py
@@ -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/.
+
+# Write out histogram information for C++. The histograms are defined
+# in a file provided as a command-line argument.
+
+from __future__ import print_function
+from shared_telemetry_utils import StringTable, static_assert
+
+import sys
+import histogram_tools
+import itertools
+
+banner = """/* This file is auto-generated, see gen-histogram-data.py. */
+"""
+
+def print_array_entry(output, histogram, name_index, exp_index, label_index, label_count):
+ cpp_guard = histogram.cpp_guard()
+ if cpp_guard:
+ print("#if defined(%s)" % cpp_guard, file=output)
+ print(" { %s, %s, %s, %s, %d, %d, %s, %d, %d, %s }," \
+ % (histogram.low(),
+ histogram.high(),
+ histogram.n_buckets(),
+ histogram.nsITelemetry_kind(),
+ name_index,
+ exp_index,
+ histogram.dataset(),
+ label_index,
+ label_count,
+ "true" if histogram.keyed() else "false"), file=output)
+ if cpp_guard:
+ print("#endif", file=output)
+
+def write_histogram_table(output, histograms):
+ string_table = StringTable()
+ label_table = []
+ label_count = 0
+
+ print("const HistogramInfo gHistograms[] = {", file=output)
+ for histogram in histograms:
+ name_index = string_table.stringIndex(histogram.name())
+ exp_index = string_table.stringIndex(histogram.expiration())
+
+ labels = histogram.labels()
+ label_index = 0
+ if len(labels) > 0:
+ label_index = label_count
+ label_table.append((histogram.name(), string_table.stringIndexes(labels)))
+ label_count += len(labels)
+
+ print_array_entry(output, histogram,
+ name_index, exp_index,
+ label_index, len(labels))
+ print("};\n", file=output)
+
+ strtab_name = "gHistogramStringTable"
+ string_table.writeDefinition(output, strtab_name)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % strtab_name,
+ "index overflow")
+
+ print("\nconst uint32_t gHistogramLabelTable[] = {", file=output)
+ for name,indexes in label_table:
+ print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
+ print("};", file=output)
+
+
+# Write out static asserts for histogram data. We'd prefer to perform
+# these checks in this script itself, but since several histograms
+# (generally enumerated histograms) use compile-time constants for
+# their upper bounds, we have to let the compiler do the checking.
+
+def static_asserts_for_boolean(output, histogram):
+ pass
+
+def static_asserts_for_flag(output, histogram):
+ pass
+
+def static_asserts_for_count(output, histogram):
+ pass
+
+def static_asserts_for_enumerated(output, histogram):
+ n_values = histogram.high()
+ static_assert(output, "%s > 2" % n_values,
+ "Not enough values for %s" % histogram.name())
+
+def shared_static_asserts(output, histogram):
+ name = histogram.name()
+ low = histogram.low()
+ high = histogram.high()
+ n_buckets = histogram.n_buckets()
+ static_assert(output, "%s < %s" % (low, high), "low >= high for %s" % name)
+ static_assert(output, "%s > 2" % n_buckets, "Not enough values for %s" % name)
+ static_assert(output, "%s >= 1" % low, "Incorrect low value for %s" % name)
+ static_assert(output, "%s > %s" % (high, n_buckets),
+ "high must be > number of buckets for %s; you may want an enumerated histogram" % name)
+
+def static_asserts_for_linear(output, histogram):
+ shared_static_asserts(output, histogram)
+
+def static_asserts_for_exponential(output, histogram):
+ shared_static_asserts(output, histogram)
+
+def write_histogram_static_asserts(output, histograms):
+ print("""
+// Perform the checks at the beginning of HistogramGet at
+// compile time, so that incorrect histogram definitions
+// give compile-time errors, not runtime errors.""", file=output)
+
+ table = {
+ 'boolean' : static_asserts_for_boolean,
+ 'flag' : static_asserts_for_flag,
+ 'count': static_asserts_for_count,
+ 'enumerated' : static_asserts_for_enumerated,
+ 'categorical' : static_asserts_for_enumerated,
+ 'linear' : static_asserts_for_linear,
+ 'exponential' : static_asserts_for_exponential,
+ }
+
+ for histogram in histograms:
+ histogram_tools.table_dispatch(histogram.kind(), table,
+ lambda f: f(output, histogram))
+
+def write_debug_histogram_ranges(output, histograms):
+ ranges_lengths = []
+
+ # Collect all the range information from individual histograms.
+ # Write that information out as well.
+ print("#ifdef DEBUG", file=output)
+ print("const int gBucketLowerBounds[] = {", file=output)
+ for histogram in histograms:
+ ranges = []
+ try:
+ ranges = histogram.ranges()
+ except histogram_tools.DefinitionException:
+ pass
+ ranges_lengths.append(len(ranges))
+ # Note that we do not test cpp_guard here. We do this so we
+ # will have complete information about all the histograms in
+ # this array. Just having information about the ranges of
+ # histograms is not platform-specific; if there are histograms
+ # that have platform-specific constants in their definitions,
+ # those histograms will fail in the .ranges() call above and
+ # we'll have a zero-length array to deal with here.
+ if len(ranges) > 0:
+ print(','.join(map(str, ranges)), ',', file=output)
+ else:
+ print('/* Skipping %s */' % histogram.name(), file=output)
+ print("};", file=output)
+
+ # Write the offsets into gBucketLowerBounds.
+ print("struct bounds { int offset; int length; };", file=output)
+ print("const struct bounds gBucketLowerBoundIndex[] = {", file=output)
+ offset = 0
+ for (histogram, range_length) in itertools.izip(histograms, ranges_lengths):
+ cpp_guard = histogram.cpp_guard()
+ # We do test cpp_guard here, so that histogram IDs are valid
+ # indexes into this array.
+ if cpp_guard:
+ print("#if defined(%s)" % cpp_guard, file=output)
+ print("{ %d, %d }," % (offset, range_length), file=output)
+ if cpp_guard:
+ print("#endif", file=output)
+ offset += range_length
+ print("};", file=output)
+ print("#endif", file=output)
+
+def main(output, *filenames):
+ histograms = list(histogram_tools.from_files(filenames))
+
+ print(banner, file=output)
+ write_histogram_table(output, histograms)
+ write_histogram_static_asserts(output, histograms)
+ write_debug_histogram_ranges(output, histograms)
+
+if __name__ == '__main__':
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/gen-histogram-enum.py b/toolkit/components/telemetry/gen-histogram-enum.py
new file mode 100644
index 0000000000..8e08bc4846
--- /dev/null
+++ b/toolkit/components/telemetry/gen-histogram-enum.py
@@ -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/.
+
+# Write out a C++ enum definition whose members are the names of
+# histograms as well as the following other members:
+#
+# - HistogramCount
+# - HistogramFirstUseCounter
+# - HistogramLastUseCounter
+# - HistogramUseCounterCount
+#
+# The histograms are defined in files provided as command-line arguments.
+
+from __future__ import print_function
+
+import histogram_tools
+import itertools
+import sys
+
+banner = """/* This file is auto-generated, see gen-histogram-enum.py. */
+"""
+
+header = """
+#ifndef mozilla_TelemetryHistogramEnums_h
+#define mozilla_TelemetryHistogramEnums_h
+
+#include "mozilla/TemplateLib.h"
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+footer = """
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryHistogramEnums_h"""
+
+def main(output, *filenames):
+ # Print header.
+ print(banner, file=output)
+ print(header, file=output)
+
+ # Load the histograms.
+ all_histograms = list(histogram_tools.from_files(filenames))
+ groups = itertools.groupby(all_histograms,
+ lambda h: h.name().startswith("USE_COUNTER2_"))
+
+ # Print the histogram enums.
+ # Note that histogram_tools.py guarantees that all of the USE_COUNTER2_*
+ # histograms are defined in a contiguous block. We therefore assume
+ # that there's at most one group for which use_counter_group is true.
+ print("enum ID : uint32_t {", file=output)
+ seen_use_counters = False
+ for (use_counter_group, histograms) in groups:
+ if use_counter_group:
+ seen_use_counters = True
+
+ # The HistogramDUMMY* enum variables are used to make the computation
+ # of Histogram{First,Last}UseCounter easier. Otherwise, we'd have to
+ # special case the first and last histogram in the group.
+ if use_counter_group:
+ print(" HistogramFirstUseCounter,", file=output)
+ print(" HistogramDUMMY1 = HistogramFirstUseCounter - 1,", file=output)
+
+ for histogram in histograms:
+ cpp_guard = histogram.cpp_guard()
+ if cpp_guard:
+ print("#if defined(%s)" % cpp_guard, file=output)
+ print(" %s," % histogram.name(), file=output)
+ if cpp_guard:
+ print("#endif", file=output)
+
+ if use_counter_group:
+ print(" HistogramDUMMY2,", file=output)
+ print(" HistogramLastUseCounter = HistogramDUMMY2 - 1,", file=output)
+
+ print(" HistogramCount,", file=output)
+ if seen_use_counters:
+ print(" HistogramUseCounterCount = HistogramLastUseCounter - HistogramFirstUseCounter + 1", file=output)
+ else:
+ print(" HistogramFirstUseCounter = 0,", file=output)
+ print(" HistogramLastUseCounter = 0,", file=output)
+ print(" HistogramUseCounterCount = 0", file=output)
+ print("};", file=output)
+
+ # Write categorical label enums.
+ categorical = filter(lambda h: h.kind() == "categorical", all_histograms)
+ enums = [("LABELS_" + h.name(), h.labels(), h.name()) for h in categorical]
+ for name,labels,_ in enums:
+ print("\nenum class %s : uint32_t {" % name, file=output)
+ print(" %s" % ",\n ".join(labels), file=output)
+ print("};", file=output)
+
+ print("\ntemplate<class T> struct IsCategoricalLabelEnum : FalseType {};", file=output)
+ for name,_,_ in enums:
+ print("template<> struct IsCategoricalLabelEnum<%s> : TrueType {};" % name, file=output)
+
+ print("\ntemplate<class T> struct CategoricalLabelId {};", file=output)
+ for name,_,id in enums:
+ print("template<> struct CategoricalLabelId<%s> : IntegralConstant<uint32_t, %s> {};" % (name, id), file=output)
+
+ # Footer.
+ print(footer, file=output)
+
+if __name__ == '__main__':
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/gen-scalar-data.py b/toolkit/components/telemetry/gen-scalar-data.py
new file mode 100644
index 0000000000..6c17c602f8
--- /dev/null
+++ b/toolkit/components/telemetry/gen-scalar-data.py
@@ -0,0 +1,90 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out scalar information for C++. The scalars are defined
+# in a file provided as a command-line argument.
+
+from __future__ import print_function
+from shared_telemetry_utils import StringTable, static_assert
+
+import parse_scalars
+import sys
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated, only for internal use in TelemetryScalar.h,
+ see gen-scalar-data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryScalarData_h
+#define mozilla_TelemetryScalarData_h
+#include "ScalarInfo.h"
+namespace {
+"""
+
+file_footer = """\
+} // namespace
+#endif // mozilla_TelemetryScalarData_h
+"""
+
+def write_scalar_info(scalar, output, name_index, expiration_index):
+ """Writes a scalar entry to the output file.
+
+ :param scalar: a ScalarType instance describing the scalar.
+ :param output: the output stream.
+ :param name_index: the index of the scalar name in the strings table.
+ :param expiration_index: the index of the expiration version in the strings table.
+ """
+ cpp_guard = scalar.cpp_guard
+ if cpp_guard:
+ print("#if defined(%s)" % cpp_guard, file=output)
+
+ print(" {{ {}, {}, {}, {}, {} }},"\
+ .format(scalar.nsITelemetry_kind,
+ name_index,
+ expiration_index,
+ scalar.dataset,
+ "true" if scalar.keyed else "false"),
+ file=output)
+
+ if cpp_guard:
+ print("#endif", file=output)
+
+def write_scalar_tables(scalars, output):
+ """Writes the scalar and strings tables to an header file.
+
+ :param scalars: a list of ScalarType instances describing the scalars.
+ :param output: the output stream.
+ """
+ string_table = StringTable()
+
+ print("const ScalarInfo gScalars[] = {", file=output)
+ for s in scalars:
+ # We add both the scalar label and the expiration string to the strings
+ # table.
+ name_index = string_table.stringIndex(s.label)
+ exp_index = string_table.stringIndex(s.expires)
+ # Write the scalar info entry.
+ write_scalar_info(s, output, name_index, exp_index)
+ print("};", file=output)
+
+ string_table_name = "gScalarsStringTable"
+ string_table.writeDefinition(output, string_table_name)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % string_table_name,
+ "index overflow")
+
+def main(output, *filenames):
+ # Load the scalars first.
+ if len(filenames) > 1:
+ raise Exception('We don\'t support loading from more than one file.')
+ scalars = parse_scalars.load_scalars(filenames[0])
+
+ # Write the scalar data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+ write_scalar_tables(scalars, output)
+ print(file_footer, file=output)
+
+if __name__ == '__main__':
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/gen-scalar-enum.py b/toolkit/components/telemetry/gen-scalar-enum.py
new file mode 100644
index 0000000000..f0ca01d4b5
--- /dev/null
+++ b/toolkit/components/telemetry/gen-scalar-enum.py
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out a C++ enum definition whose members are the names of
+# scalar types.
+#
+# The scalars are defined in files provided as command-line arguments.
+
+from __future__ import print_function
+
+import sys
+import parse_scalars
+
+banner = """/* This file is auto-generated, see gen-scalar-enum.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryScalarEnums_h
+#define mozilla_TelemetryScalarEnums_h
+namespace mozilla {
+namespace Telemetry {
+enum class ScalarID : uint32_t {\
+"""
+
+file_footer = """\
+};
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryScalarEnums_h
+"""
+
+def main(output, *filenames):
+ # Load the scalars first.
+ if len(filenames) > 1:
+ raise Exception('We don\'t support loading from more than one file.')
+ scalars = parse_scalars.load_scalars(filenames[0])
+
+ # Write the enum file.
+ print(banner, file=output)
+ print(file_header, file=output);
+
+ for s in scalars:
+ cpp_guard = s.cpp_guard
+ if cpp_guard:
+ print("#if defined(%s)" % cpp_guard, file=output)
+ print(" %s," % s.enum_label, file=output)
+ if cpp_guard:
+ print("#endif", file=output)
+
+ print(" ScalarCount,", file=output)
+
+ print(file_footer, file=output)
+
+if __name__ == '__main__':
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/healthreport-prefs.js b/toolkit/components/telemetry/healthreport-prefs.js
new file mode 100644
index 0000000000..021028e1ca
--- /dev/null
+++ b/toolkit/components/telemetry/healthreport-prefs.js
@@ -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/. */
+
+pref("datareporting.healthreport.infoURL", "https://www.mozilla.org/legal/privacy/firefox.html#health-report");
+
+// Health Report is enabled by default on all channels.
+pref("datareporting.healthreport.uploadEnabled", true);
+
+pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");
diff --git a/toolkit/components/telemetry/histogram-whitelists.json b/toolkit/components/telemetry/histogram-whitelists.json
new file mode 100644
index 0000000000..52db331928
--- /dev/null
+++ b/toolkit/components/telemetry/histogram-whitelists.json
@@ -0,0 +1,1990 @@
+{
+ "alert_emails": [
+ "A11Y_CONSUMERS",
+ "A11Y_IATABLE_USAGE_FLAG",
+ "A11Y_INSTANTIATED_FLAG",
+ "A11Y_ISIMPLEDOM_USAGE_FLAG",
+ "A11Y_UPDATE_TIME",
+ "ADDON_SHIM_USAGE",
+ "AUDIOSTREAM_FIRST_OPEN_MS",
+ "AUDIOSTREAM_LATER_OPEN_MS",
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "BACKGROUNDFILESAVER_THREAD_COUNT",
+ "BAD_FALLBACK_FONT",
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS",
+ "BROWSER_IS_ASSIST_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_ALWAYS_CHECK",
+ "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT",
+ "BROWSER_SET_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_RESULT",
+ "BROWSER_SET_DEFAULT_TIME_TO_COMPLETION_SECONDS",
+ "BR_9_2_1_SUBJECT_ALT_NAMES",
+ "BR_9_2_2_SUBJECT_COMMON_NAME",
+ "CACHE_DEVICE_SEARCH_2",
+ "CACHE_DISK_SEARCH_2",
+ "CACHE_LM_INCONSISTENT",
+ "CACHE_MEMORY_SEARCH_2",
+ "CACHE_OFFLINE_SEARCH_2",
+ "CACHE_SERVICE_LOCK_WAIT_2",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_2",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSASYNCDOOMEVENT_RUN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSBLOCKONCACHETHREADEVENT_RUN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_CLOSE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOM",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOMANDFAILPENDINGREQUESTS",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCACHEELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCLIENTID",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDEVICEID",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETEXPIRATIONTIME",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFETCHCOUNT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFILE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETKEY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTFETCHED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTMODIFIED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETMETADATAELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETPREDICTEDDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSECURITYINFO",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEPOLICY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_ISSTREAMBASED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_MARKVALID",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENINPUTSTREAM",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENOUTPUTSTREAM",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_REQUESTDATASIZECHANGE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETCACHEELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETEXPIRATIONTIME",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETMETADATAELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETPREDICTEDDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSECURITYINFO",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSTORAGEPOLICY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_VISITMETADATA",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_CLOSEALLSTREAMS",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_DISKDEVICEHEAPSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_EVICTENTRIESFORCLIENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_GETCACHEIOTARGET",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILECHANGED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILESHUTDOWN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_OPENCACHEENTRY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_PROCESSREQUEST",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHECAPACITY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEENABLED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKSMARTSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHECAPACITY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHEENABLED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SHUTDOWN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_VISITENTRIES",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCOMPRESSOUTPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSDECOMPRESSINPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_CLOSEINTERNAL",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_LAZYINIT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_CLOSEINTERNAL",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_LAZYINIT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSPROCESSREQUESTEVENT_RUN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSSETDISKSMARTSIZECALLBACK_NOTIFY",
+ "CANVAS_2D_USED",
+ "CANVAS_WEBGL_USED",
+ "CERT_CHAIN_KEY_SIZE_STATUS",
+ "CERT_CHAIN_SHA1_POLICY_STATUS",
+ "CERT_VALIDATION_SUCCESS_BY_CA",
+ "CHANGES_OF_DETECTED_LANGUAGE",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "CHARSET_OVERRIDE_SITUATION",
+ "CHARSET_OVERRIDE_USED",
+ "CHECK_ADDONS_MODIFIED_MS",
+ "CHECK_JAVA_ENABLED",
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
+ "COMPOSITE_FRAME_ROUNDTRIP_TIME",
+ "COMPOSITE_TIME",
+ "CONTENT_DOCUMENTS_DESTROYED",
+ "CRASH_STORE_COMPRESSED_BYTES",
+ "DATABASE_LOCKED_EXCEPTION",
+ "DATABASE_SUCCESSFUL_UNLOCK",
+ "DATA_STORAGE_ENTRIES",
+ "DECODER_INSTANTIATED_IBM866",
+ "DECODER_INSTANTIATED_ISO2022JP",
+ "DECODER_INSTANTIATED_ISO_8859_5",
+ "DECODER_INSTANTIATED_KOI8R",
+ "DECODER_INSTANTIATED_KOI8U",
+ "DECODER_INSTANTIATED_MACARABIC",
+ "DECODER_INSTANTIATED_MACCE",
+ "DECODER_INSTANTIATED_MACCROATIAN",
+ "DECODER_INSTANTIATED_MACCYRILLIC",
+ "DECODER_INSTANTIATED_MACDEVANAGARI",
+ "DECODER_INSTANTIATED_MACFARSI",
+ "DECODER_INSTANTIATED_MACGREEK",
+ "DECODER_INSTANTIATED_MACGUJARATI",
+ "DECODER_INSTANTIATED_MACGURMUKHI",
+ "DECODER_INSTANTIATED_MACHEBREW",
+ "DECODER_INSTANTIATED_MACICELANDIC",
+ "DECODER_INSTANTIATED_MACROMANIAN",
+ "DECODER_INSTANTIATED_MACTURKISH",
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED",
+ "DEFERRED_FINALIZE_ASYNC",
+ "DENIED_TRANSLATION_OFFERS",
+ "DEVICE_RESET_REASON",
+ "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS",
+ "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT",
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT",
+ "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_MEMORY_BREAKDOWN_CENSUS_COUNT",
+ "DEVTOOLS_MEMORY_BREAKDOWN_DOMINATOR_TREE_COUNT",
+ "DEVTOOLS_MEMORY_DIFF_CENSUS",
+ "DEVTOOLS_MEMORY_DOMINATOR_TREE_COUNT",
+ "DEVTOOLS_MEMORY_EXPORT_SNAPSHOT_COUNT",
+ "DEVTOOLS_MEMORY_FILTER_CENSUS",
+ "DEVTOOLS_MEMORY_IMPORT_SNAPSHOT_COUNT",
+ "DEVTOOLS_MEMORY_INVERTED_CENSUS",
+ "DEVTOOLS_MEMORY_TAKE_SNAPSHOT_COUNT",
+ "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_OS_ENUMERATED_PER_USER",
+ "DEVTOOLS_OS_IS_64_BITS_PER_USER",
+ "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT",
+ "DEVTOOLS_PERFTOOLS_RECORDING_COUNT",
+ "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS",
+ "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG",
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG",
+ "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS",
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER",
+ "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR",
+ "DEVTOOLS_TABS_OPEN_PEAK_LINEAR",
+ "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR",
+ "DEVTOOLS_TABS_PINNED_PEAK_LINEAR",
+ "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
+ "DEVTOOLS_WEBIDE_CONNECTION_DEBUG_USED",
+ "DEVTOOLS_WEBIDE_CONNECTION_PLAY_USED",
+ "DEVTOOLS_WEBIDE_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS",
+ "DEVTOOLS_WEBIDE_LOCAL_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_OTHER_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBIDE_REMOTE_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_SIMULATOR_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBIDE_USB_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_WIFI_CONNECTION_RESULT",
+ "DISPLAY_SCALING_LINUX",
+ "DISPLAY_SCALING_MSWIN",
+ "DISPLAY_SCALING_OSX",
+ "DNS_BLACKLIST_COUNT",
+ "DNS_CLEANUP_AGE",
+ "DNS_FAILED_LOOKUP_TIME",
+ "DNS_LOOKUP_METHOD2",
+ "DNS_LOOKUP_TIME",
+ "DNS_RENEWAL_TIME",
+ "DNS_RENEWAL_TIME_FOR_TTL",
+ "DNT_USAGE",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COUNT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL",
+ "DWRITEFONT_INIT_PROBLEM",
+ "E10S_BLOCKED_FROM_RUNNING",
+ "E10S_WINDOW",
+ "ENABLE_PRIVILEGE_EVER_CALLED",
+ "FENNEC_DISTRIBUTION_CODE_CATEGORY",
+ "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS",
+ "FENNEC_DISTRIBUTION_REFERRER_INVALID",
+ "FENNEC_GLOBALHISTORY_ADD_MS",
+ "FENNEC_GLOBALHISTORY_UPDATE_MS",
+ "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS",
+ "FENNEC_HOMEPANELS_CUSTOM",
+ "FENNEC_READING_LIST_COUNT",
+ "FENNEC_RESTORING_ACTIVITY",
+ "FENNEC_SEARCH_LOADER_TIME_MS",
+ "FENNEC_STARTUP_TIME_GECKOREADY",
+ "FENNEC_STARTUP_TIME_JAVAUI",
+ "FENNEC_SYNC11_MIGRATIONS_COMPLETED",
+ "FENNEC_SYNC11_MIGRATIONS_FAILED",
+ "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED",
+ "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED",
+ "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED",
+ "FENNEC_TABQUEUE_QUEUESIZE",
+ "FENNEC_TOPSITES_LOADER_TIME_MS",
+ "FENNEC_WAS_KILLED",
+ "FETCH_IS_MAINTHREAD",
+ "FLASH_PLUGIN_AREA",
+ "FLASH_PLUGIN_HEIGHT",
+ "FLASH_PLUGIN_INSTANCES_ON_PAGE",
+ "FLASH_PLUGIN_STATES",
+ "FLASH_PLUGIN_WIDTH",
+ "FONTLIST_INITFACENAMELISTS",
+ "FONTLIST_INITOTHERFAMILYNAMES",
+ "FONT_CACHE_HIT",
+ "FORCED_DEVICE_RESET_REASON",
+ "FX_BOOKMARKS_TOOLBAR_INIT_MS",
+ "FX_BROWSER_FULLSCREEN_USED",
+ "FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE",
+ "FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE",
+ "FX_NEW_WINDOW_MS",
+ "FX_PAGE_LOAD_MS",
+ "FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS",
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
+ "FX_TABLETMODE_PAGE_LOAD",
+ "FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS",
+ "FX_TAB_ANIM_OPEN_FRAME_INTERVAL_MS",
+ "FX_TAB_ANIM_OPEN_PREVIEW_FRAME_INTERVAL_MS",
+ "FX_TAB_CLICK_MS",
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
+ "FX_TAB_SWITCH_TOTAL_E10S_MS",
+ "FX_TAB_SWITCH_TOTAL_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2",
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS",
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
+ "FX_THUMBNAILS_CAPTURE_TIME_MS",
+ "FX_THUMBNAILS_HIT_OR_MISS",
+ "FX_THUMBNAILS_STORE_TIME_MS",
+ "FX_TOTAL_TOP_VISITS",
+ "FX_TOUCH_USED",
+ "GDI_INITFONTLIST_TOTAL",
+ "GEOLOCATION_ACCURACY_EXPONENTIAL",
+ "GEOLOCATION_ERROR",
+ "GEOLOCATION_GETCURRENTPOSITION_SECURE_ORIGIN",
+ "GEOLOCATION_OSX_SOURCE_IS_MLS",
+ "GEOLOCATION_REQUEST_GRANTED",
+ "GEOLOCATION_WATCHPOSITION_SECURE_ORIGIN",
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS",
+ "GFX_CRASH",
+ "GRADIENT_DURATION",
+ "GRADIENT_RETENTION_TIME",
+ "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS",
+ "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN",
+ "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN",
+ "HTTPCONNMGR_USED_SPECULATIVE_CONN",
+ "HTTP_AUTH_DIALOG_STATS",
+ "HTTP_CACHE_DISPOSITION_2",
+ "HTTP_CACHE_DISPOSITION_2_V2",
+ "HTTP_CACHE_ENTRY_ALIVE_TIME",
+ "HTTP_CACHE_ENTRY_RELOAD_TIME",
+ "HTTP_CACHE_ENTRY_REUSE_COUNT",
+ "HTTP_CACHE_MISS_HALFLIFE_EXPERIMENT_2",
+ "HTTP_CONNECTION_ENTRY_CACHE_HIT_1",
+ "HTTP_CONTENT_ENCODING",
+ "HTTP_DISK_CACHE_DISPOSITION_2",
+ "HTTP_DISK_CACHE_OVERHEAD",
+ "HTTP_KBREAD_PER_CONN",
+ "HTTP_MEMORY_CACHE_DISPOSITION_2",
+ "HTTP_OFFLINE_CACHE_DISPOSITION_2",
+ "HTTP_OFFLINE_CACHE_DOCUMENT_LOAD",
+ "HTTP_PAGELOAD_IS_SSL",
+ "HTTP_PAGE_CACHE_READ_TIME",
+ "HTTP_PAGE_CACHE_READ_TIME_V2",
+ "HTTP_PAGE_COMPLETE_LOAD",
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED",
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_NET",
+ "HTTP_PAGE_COMPLETE_LOAD_NET_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_V2",
+ "HTTP_PAGE_DNS_ISSUE_TIME",
+ "HTTP_PAGE_DNS_LOOKUP_TIME",
+ "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE",
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_SENT",
+ "HTTP_PAGE_REVALIDATION",
+ "HTTP_PAGE_TCP_CONNECTION",
+ "HTTP_PROXY_TYPE",
+ "HTTP_REQUEST_PER_CONN",
+ "HTTP_REQUEST_PER_PAGE",
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE",
+ "HTTP_RESPONSE_VERSION",
+ "HTTP_SAW_QUIC_ALT_PROTOCOL",
+ "HTTP_SCHEME_UPGRADE",
+ "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME",
+ "HTTP_SUBITEM_OPEN_LATENCY_TIME",
+ "HTTP_SUB_CACHE_READ_TIME",
+ "HTTP_SUB_CACHE_READ_TIME_V2",
+ "HTTP_SUB_COMPLETE_LOAD",
+ "HTTP_SUB_COMPLETE_LOAD_CACHED",
+ "HTTP_SUB_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_SUB_COMPLETE_LOAD_NET",
+ "HTTP_SUB_COMPLETE_LOAD_NET_V2",
+ "HTTP_SUB_COMPLETE_LOAD_V2",
+ "HTTP_SUB_DNS_ISSUE_TIME",
+ "HTTP_SUB_DNS_LOOKUP_TIME",
+ "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE",
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_SUB_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_SENT",
+ "HTTP_SUB_REVALIDATION",
+ "HTTP_SUB_TCP_CONNECTION",
+ "HTTP_TRANSACTION_IS_SSL",
+ "HTTP_TRANSACTION_USE_ALTSVC",
+ "HTTP_TRANSACTION_USE_ALTSVC_OE",
+ "IMAGE_DECODE_CHUNKS",
+ "IMAGE_DECODE_COUNT",
+ "IMAGE_DECODE_LATENCY_US",
+ "IMAGE_DECODE_ON_DRAW_LATENCY",
+ "IMAGE_DECODE_SPEED_GIF",
+ "IMAGE_DECODE_SPEED_JPEG",
+ "IMAGE_DECODE_SPEED_PNG",
+ "IMAGE_DECODE_TIME",
+ "INNERWINDOWS_WITH_MUTATION_LISTENERS",
+ "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB",
+ "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY",
+ "JS_TELEMETRY_ADDON_EXCEPTIONS",
+ "LINK_ICON_SIZES_ATTR_DIMENSION",
+ "LINK_ICON_SIZES_ATTR_USAGE",
+ "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
+ "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
+ "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS",
+ "LONG_REFLOW_INTERRUPTIBLE",
+ "MAC_INITFONTLIST_TOTAL",
+ "MASTER_PASSWORD_ENABLED",
+ "MEDIA_WMF_DECODE_ERROR",
+ "MIXED_CONTENT_PAGE_LOAD",
+ "MIXED_CONTENT_UNBLOCK_COUNTER",
+ "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS",
+ "MOZ_SQLITE_COOKIES_READ_B",
+ "MOZ_SQLITE_COOKIES_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_COOKIES_READ_MS",
+ "MOZ_SQLITE_COOKIES_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_COOKIES_SYNC_MS",
+ "MOZ_SQLITE_COOKIES_WRITE_B",
+ "MOZ_SQLITE_COOKIES_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_COOKIES_WRITE_MS",
+ "MOZ_SQLITE_OPEN_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OPEN_MS",
+ "MOZ_SQLITE_OTHER_READ_B",
+ "MOZ_SQLITE_OTHER_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OTHER_READ_MS",
+ "MOZ_SQLITE_OTHER_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OTHER_SYNC_MS",
+ "MOZ_SQLITE_OTHER_WRITE_B",
+ "MOZ_SQLITE_OTHER_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OTHER_WRITE_MS",
+ "MOZ_SQLITE_PLACES_READ_B",
+ "MOZ_SQLITE_PLACES_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_PLACES_READ_MS",
+ "MOZ_SQLITE_PLACES_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_PLACES_SYNC_MS",
+ "MOZ_SQLITE_PLACES_WRITE_B",
+ "MOZ_SQLITE_PLACES_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_PLACES_WRITE_MS",
+ "MOZ_SQLITE_TRUNCATE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_TRUNCATE_MS",
+ "MOZ_SQLITE_WEBAPPS_READ_B",
+ "MOZ_SQLITE_WEBAPPS_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_WEBAPPS_READ_MS",
+ "MOZ_SQLITE_WEBAPPS_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_WEBAPPS_SYNC_MS",
+ "MOZ_SQLITE_WEBAPPS_WRITE_B",
+ "MOZ_SQLITE_WEBAPPS_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_WEBAPPS_WRITE_MS",
+ "NETWORK_CACHE_FS_TYPE",
+ "NETWORK_CACHE_HASH_STATS",
+ "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE",
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS",
+ "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS",
+ "NETWORK_CACHE_METADATA_SIZE",
+ "NETWORK_CACHE_SIZE_FULL_FAT",
+ "NETWORK_CACHE_V1_HIT_TIME_MS",
+ "NETWORK_CACHE_V1_MISS_TIME_MS",
+ "NETWORK_CACHE_V1_TRUNCATE_TIME_MS",
+ "NETWORK_CACHE_V2_HIT_TIME_MS",
+ "NETWORK_CACHE_V2_INPUT_STREAM_STATUS",
+ "NETWORK_CACHE_V2_MISS_TIME_MS",
+ "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS",
+ "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_DELETEDIR",
+ "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN",
+ "NETWORK_DISK_CACHE_OPEN",
+ "NETWORK_DISK_CACHE_SHUTDOWN",
+ "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_SHUTDOWN_V2",
+ "NETWORK_DISK_CACHE_TRASHRENAME",
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
+ "NEWTAB_PAGE_ENABLED",
+ "NEWTAB_PAGE_ENHANCED",
+ "NEWTAB_PAGE_LIFE_SPAN",
+ "NEWTAB_PAGE_LIFE_SPAN_SUGGESTED",
+ "NEWTAB_PAGE_PINNED_SITES_COUNT",
+ "NEWTAB_PAGE_SHOWN",
+ "NEWTAB_PAGE_SITE_CLICKED",
+ "NTLM_MODULE_USED_2",
+ "ONBEFOREUNLOAD_PROMPT_ACTION",
+ "ONBEFOREUNLOAD_PROMPT_COUNT",
+ "OSFILE_WORKER_LAUNCH_MS",
+ "OSFILE_WORKER_READY_MS",
+ "OSFILE_WRITEATOMIC_JANK_MS",
+ "PAGE_FAULTS_HARD",
+ "PAINT_BUILD_DISPLAYLIST_TIME",
+ "PAINT_RASTERIZE_TIME",
+ "PDF_VIEWER_DOCUMENT_GENERATOR",
+ "PDF_VIEWER_DOCUMENT_SIZE_KB",
+ "PDF_VIEWER_DOCUMENT_VERSION",
+ "PDF_VIEWER_EMBED",
+ "PDF_VIEWER_FALLBACK_SHOWN",
+ "PDF_VIEWER_FONT_TYPES",
+ "PDF_VIEWER_FORM",
+ "PDF_VIEWER_PRINT",
+ "PDF_VIEWER_STREAM_TYPES",
+ "PDF_VIEWER_TIME_TO_VIEW_MS",
+ "PDF_VIEWER_USED",
+ "PERF_MONITORING_SLOW_ADDON_CPOW_US",
+ "PERF_MONITORING_SLOW_ADDON_JANK_US",
+ "PERMISSIONS_SQL_CORRUPTED",
+ "PLACES_ANNOS_BOOKMARKS_COUNT",
+ "PLACES_ANNOS_PAGES_COUNT",
+ "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS",
+ "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS",
+ "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS",
+ "PLACES_BACKUPS_BOOKMARKSTREE_MS",
+ "PLACES_BACKUPS_DAYSFROMLAST",
+ "PLACES_BACKUPS_TOJSON_MS",
+ "PLACES_BOOKMARKS_COUNT",
+ "PLACES_DATABASE_FILESIZE_MB",
+ "PLACES_DATABASE_PAGESIZE_B",
+ "PLACES_DATABASE_SIZE_PER_PAGE_B",
+ "PLACES_EXPIRATION_STEPS_TO_CLEAN2",
+ "PLACES_EXPORT_TOHTML_MS",
+ "PLACES_FAVICON_BMP_SIZES",
+ "PLACES_FAVICON_GIF_SIZES",
+ "PLACES_FAVICON_ICO_SIZES",
+ "PLACES_FAVICON_JPEG_SIZES",
+ "PLACES_FAVICON_OTHER_SIZES",
+ "PLACES_FAVICON_PNG_SIZES",
+ "PLACES_FAVICON_SVG_SIZES",
+ "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS",
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS",
+ "PLACES_IDLE_MAINTENANCE_TIME_MS",
+ "PLACES_KEYWORDS_COUNT",
+ "PLACES_MAINTENANCE_DAYSFROMLAST",
+ "PLACES_PAGES_COUNT",
+ "PLACES_SORTED_BOOKMARKS_PERC",
+ "PLACES_TAGGED_BOOKMARKS_PERC",
+ "PLACES_TAGS_COUNT",
+ "PLUGINS_INFOBAR_ALLOW",
+ "PLUGINS_INFOBAR_BLOCK",
+ "PLUGINS_INFOBAR_SHOWN",
+ "PLUGINS_NOTIFICATION_PLUGIN_COUNT",
+ "PLUGINS_NOTIFICATION_SHOWN",
+ "PLUGINS_NOTIFICATION_USER_ACTION",
+ "PLUGIN_CALLED_DIRECTLY",
+ "PLUGIN_HANG_TIME",
+ "PLUGIN_HANG_UI_DONT_ASK",
+ "PLUGIN_HANG_UI_RESPONSE_TIME",
+ "PLUGIN_HANG_UI_USER_RESPONSE",
+ "PLUGIN_SHUTDOWN_MS",
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_NORMAL",
+ "PRCONNECT_BLOCKING_TIME_OFFLINE",
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
+ "PREDICTOR_BASE_CONFIDENCE",
+ "PREDICTOR_CONFIDENCE",
+ "PREDICTOR_GLOBAL_DEGRADATION",
+ "PREDICTOR_LEARN_ATTEMPTS",
+ "PREDICTOR_LEARN_FULL_QUEUE",
+ "PREDICTOR_LEARN_WORK_TIME",
+ "PREDICTOR_PREDICTIONS_CALCULATED",
+ "PREDICTOR_PREDICT_ATTEMPTS",
+ "PREDICTOR_PREDICT_FULL_QUEUE",
+ "PREDICTOR_PREDICT_TIME_TO_ACTION",
+ "PREDICTOR_PREDICT_TIME_TO_INACTION",
+ "PREDICTOR_PREDICT_WORK_TIME",
+ "PREDICTOR_SUBRESOURCE_DEGRADATION",
+ "PREDICTOR_TOTAL_PRECONNECTS",
+ "PREDICTOR_TOTAL_PRECONNECTS_CREATED",
+ "PREDICTOR_TOTAL_PRECONNECTS_UNUSED",
+ "PREDICTOR_TOTAL_PRECONNECTS_USED",
+ "PREDICTOR_TOTAL_PREDICTIONS",
+ "PREDICTOR_TOTAL_PRERESOLVES",
+ "PREDICTOR_WAIT_TIME",
+ "PROCESS_CRASH_SUBMIT_ATTEMPT",
+ "PROCESS_CRASH_SUBMIT_SUCCESS",
+ "PWMGR_BLOCKLIST_NUM_SITES",
+ "PWMGR_FORM_AUTOFILL_RESULT",
+ "PWMGR_LOGIN_LAST_USED_DAYS",
+ "PWMGR_LOGIN_PAGE_SAFETY",
+ "PWMGR_MANAGE_COPIED_PASSWORD",
+ "PWMGR_MANAGE_COPIED_USERNAME",
+ "PWMGR_MANAGE_DELETED",
+ "PWMGR_MANAGE_DELETED_ALL",
+ "PWMGR_MANAGE_OPENED",
+ "PWMGR_MANAGE_SORTED",
+ "PWMGR_MANAGE_VISIBILITY_TOGGLED",
+ "PWMGR_NUM_HTTPAUTH_PASSWORDS",
+ "PWMGR_NUM_PASSWORDS_PER_HOSTNAME",
+ "PWMGR_NUM_SAVED_PASSWORDS",
+ "PWMGR_PASSWORD_INPUT_IN_FORM",
+ "PWMGR_PROMPT_REMEMBER_ACTION",
+ "PWMGR_PROMPT_UPDATE_ACTION",
+ "PWMGR_SAVING_ENABLED",
+ "PWMGR_USERNAME_PRESENT",
+ "REFRESH_DRIVER_TICK",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "SAFE_MODE_USAGE",
+ "SEARCH_COUNTS",
+ "SEARCH_SERVICE_INIT_MS",
+ "SECURITY_UI",
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS",
+ "SERVICE_WORKER_LIFE_TIME",
+ "SERVICE_WORKER_REGISTRATIONS",
+ "SERVICE_WORKER_REGISTRATION_LOADING",
+ "SERVICE_WORKER_REQUEST_PASSTHROUGH",
+ "SERVICE_WORKER_SPAWN_ATTEMPTS",
+ "SERVICE_WORKER_UPDATED",
+ "SERVICE_WORKER_WAS_SPAWNED",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "SHUTDOWN_OK",
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE",
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN",
+ "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION",
+ "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN",
+ "SLOW_ADDON_WARNING_RESPONSE_TIME",
+ "SLOW_ADDON_WARNING_STATES",
+ "SOCIAL_ENABLED_ON_SESSION",
+ "SOCIAL_PANEL_CLICKS",
+ "SOCIAL_SIDEBAR_OPEN_DURATION",
+ "SOCIAL_SIDEBAR_STATE",
+ "SOCIAL_TOOLBAR_BUTTONS",
+ "SPDY_CHUNK_RECVD",
+ "SPDY_GOAWAY_LOCAL",
+ "SPDY_GOAWAY_PEER",
+ "SPDY_KBREAD_PER_CONN",
+ "SPDY_NPN_CONNECT",
+ "SPDY_NPN_JOIN",
+ "SPDY_PARALLEL_STREAMS",
+ "SPDY_REQUEST_PER_CONN",
+ "SPDY_SERVER_INITIATED_STREAMS",
+ "SPDY_SETTINGS_CWND",
+ "SPDY_SETTINGS_DL_BW",
+ "SPDY_SETTINGS_IW",
+ "SPDY_SETTINGS_MAX_STREAMS",
+ "SPDY_SETTINGS_RETRANS",
+ "SPDY_SETTINGS_RTT",
+ "SPDY_SETTINGS_UL_BW",
+ "SPDY_SYN_RATIO",
+ "SPDY_SYN_REPLY_RATIO",
+ "SPDY_SYN_REPLY_SIZE",
+ "SPDY_SYN_SIZE",
+ "SPDY_VERSION2",
+ "STARTUP_CACHE_AGE_HOURS",
+ "STARTUP_CRASH_DETECTED",
+ "STARTUP_MEASUREMENT_ERRORS",
+ "STS_NUMBER_OF_ONSOCKETREADY_CALLS",
+ "STS_NUMBER_OF_PENDING_EVENTS",
+ "STS_NUMBER_OF_PENDING_EVENTS_IN_THE_LAST_CYCLE",
+ "STS_POLL_AND_EVENTS_CYCLE",
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE",
+ "STS_POLL_BLOCK_TIME",
+ "STS_POLL_CYCLE",
+ "STUMBLER_OBSERVATIONS_PER_DAY",
+ "STUMBLER_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC",
+ "STUMBLER_TIME_BETWEEN_START_SEC",
+ "STUMBLER_TIME_BETWEEN_UPLOADS_SEC",
+ "STUMBLER_UPLOAD_BYTES",
+ "STUMBLER_UPLOAD_CELL_COUNT",
+ "STUMBLER_UPLOAD_OBSERVATION_COUNT",
+ "STUMBLER_UPLOAD_WIFI_AP_COUNT",
+ "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC",
+ "SUBPROCESS_ABNORMAL_ABORT",
+ "SUBPROCESS_CRASHES_WITH_DUMP",
+ "SYSTEM_FONT_FALLBACK",
+ "SYSTEM_FONT_FALLBACK_FIRST",
+ "SYSTEM_FONT_FALLBACK_SCRIPT",
+ "TAB_SWITCH_CACHE_POSITION",
+ "TAP_TO_LOAD_ENABLED",
+ "TAP_TO_LOAD_IMAGE_SIZE",
+ "TELEMETRY_COMPRESS",
+ "TELEMETRY_STRINGIFY",
+ "TELEMETRY_SUCCESS",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_EXPIRED",
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ "TELEMETRY_TEST_RELEASE_OPTIN",
+ "TELEMETRY_TEST_RELEASE_OPTOUT",
+ "THUNDERBIRD_CONVERSATIONS_TIME_TO_2ND_GLODA_QUERY_MS",
+ "THUNDERBIRD_GLODA_SIZE_MB",
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S",
+ "TLS_ERROR_REPORT_UI",
+ "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED",
+ "TOUCH_ENABLED_DEVICE",
+ "TRACKING_PROTECTION_ENABLED",
+ "TRACKING_PROTECTION_EVENTS",
+ "TRACKING_PROTECTION_PBM_DISABLED",
+ "TRACKING_PROTECTION_SHIELD",
+ "TRANSACTION_WAIT_TIME_HTTP",
+ "TRANSACTION_WAIT_TIME_HTTP_PIPELINES",
+ "TRANSACTION_WAIT_TIME_SPDY",
+ "TRANSLATED_CHARACTERS",
+ "TRANSLATED_PAGES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "TRANSLATION_OPPORTUNITIES",
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
+ "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG",
+ "VIDEO_CANPLAYTYPE_H264_LEVEL",
+ "VIDEO_CANPLAYTYPE_H264_PROFILE",
+ "VIDEO_DECODED_H264_SPS_CONSTRAINT_SET_FLAG",
+ "VIDEO_DECODED_H264_SPS_LEVEL",
+ "VIDEO_DECODED_H264_SPS_PROFILE",
+ "VIDEO_EME_PLAY_SUCCESS",
+ "VIDEO_H264_SPS_MAX_NUM_REF_FRAMES",
+ "WEAVE_COMPLETE_SUCCESS_COUNT",
+ "WEAVE_CONFIGURED",
+ "WEAVE_CONFIGURED_MASTER_PASSWORD",
+ "WEAVE_START_COUNT",
+ "WEBCRYPTO_ALG",
+ "WEBCRYPTO_EXTRACTABLE_ENC",
+ "WEBCRYPTO_EXTRACTABLE_GENERATE",
+ "WEBCRYPTO_EXTRACTABLE_IMPORT",
+ "WEBCRYPTO_EXTRACTABLE_SIG",
+ "WEBCRYPTO_METHOD",
+ "WEBCRYPTO_RESOLVED",
+ "WEBSOCKETS_HANDSHAKE_TYPE",
+ "WORD_CACHE_HITS_CHROME",
+ "WORD_CACHE_HITS_CONTENT",
+ "WORD_CACHE_MISSES_CHROME",
+ "WORD_CACHE_MISSES_CONTENT",
+ "XMLHTTPREQUEST_ASYNC_OR_SYNC",
+ "XUL_CACHE_DISABLED"
+ ],
+ "bug_numbers": [
+ "A11Y_CONSUMERS",
+ "A11Y_IATABLE_USAGE_FLAG",
+ "A11Y_INSTANTIATED_FLAG",
+ "A11Y_ISIMPLEDOM_USAGE_FLAG",
+ "A11Y_UPDATE_TIME",
+ "ADDON_SHIM_USAGE",
+ "APPLICATION_REPUTATION_COUNT",
+ "APPLICATION_REPUTATION_LOCAL",
+ "APPLICATION_REPUTATION_SERVER",
+ "APPLICATION_REPUTATION_SHOULD_BLOCK",
+ "AUDIOSTREAM_FIRST_OPEN_MS",
+ "AUDIOSTREAM_LATER_OPEN_MS",
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "BACKGROUNDFILESAVER_THREAD_COUNT",
+ "BAD_FALLBACK_FONT",
+ "BLOCKED_ON_PLUGINASYNCSURROGATE_WAITFORINIT_MS",
+ "BLOCKED_ON_PLUGIN_INSTANCE_DESTROY_MS",
+ "BLOCKED_ON_PLUGIN_INSTANCE_INIT_MS",
+ "BLOCKED_ON_PLUGIN_MODULE_INIT_MS",
+ "BLOCKED_ON_PLUGIN_STREAM_INIT_MS",
+ "BLOCKLIST_SYNC_FILE_LOAD",
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS",
+ "BROWSER_IS_ASSIST_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_ALWAYS_CHECK",
+ "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT",
+ "BROWSER_SET_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_RESULT",
+ "BROWSER_SET_DEFAULT_TIME_TO_COMPLETION_SECONDS",
+ "BR_9_2_1_SUBJECT_ALT_NAMES",
+ "BR_9_2_2_SUBJECT_COMMON_NAME",
+ "BUCKET_ORDER_ERRORS",
+ "CACHE_DEVICE_SEARCH_2",
+ "CACHE_DISK_SEARCH_2",
+ "CACHE_LM_INCONSISTENT",
+ "CACHE_MEMORY_SEARCH_2",
+ "CACHE_OFFLINE_SEARCH_2",
+ "CACHE_SERVICE_LOCK_WAIT_2",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_2",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSASYNCDOOMEVENT_RUN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSBLOCKONCACHETHREADEVENT_RUN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_CLOSE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOM",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_DOOMANDFAILPENDINGREQUESTS",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCACHEELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETCLIENTID",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETDEVICEID",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETEXPIRATIONTIME",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFETCHCOUNT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETFILE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETKEY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTFETCHED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETLASTMODIFIED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETMETADATAELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETPREDICTEDDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSECURITYINFO",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_GETSTORAGEPOLICY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_ISSTREAMBASED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_MARKVALID",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENINPUTSTREAM",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_OPENOUTPUTSTREAM",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_REQUESTDATASIZECHANGE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETCACHEELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETEXPIRATIONTIME",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETMETADATAELEMENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETPREDICTEDDATASIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSECURITYINFO",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_SETSTORAGEPOLICY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHEENTRYDESCRIPTOR_VISITMETADATA",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_CLOSEALLSTREAMS",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_DISKDEVICEHEAPSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_EVICTENTRIESFORCLIENT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_GETCACHEIOTARGET",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ISSTORAGEENABLEDFORPOLICY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILECHANGED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_ONPROFILESHUTDOWN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_OPENCACHEENTRY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_PROCESSREQUEST",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHECAPACITY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEENABLED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKCACHEMAXENTRYSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETDISKSMARTSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETMEMORYCACHEMAXENTRYSIZE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHECAPACITY",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SETOFFLINECACHEENABLED",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_SHUTDOWN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCACHESERVICE_VISITENTRIES",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSCOMPRESSOUTPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSDECOMPRESSINPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_CLOSEINTERNAL",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_LAZYINIT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSINPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_CLOSEINTERNAL",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_LAZYINIT",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSOUTPUTSTREAMWRAPPER_RELEASE",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSPROCESSREQUESTEVENT_RUN",
+ "CACHE_SERVICE_LOCK_WAIT_MAINTHREAD_NSSETDISKSMARTSIZECALLBACK_NOTIFY",
+ "CANVAS_2D_USED",
+ "CANVAS_WEBGL_USED",
+ "CERT_CHAIN_KEY_SIZE_STATUS",
+ "CERT_CHAIN_SHA1_POLICY_STATUS",
+ "CERT_OCSP_ENABLED",
+ "CERT_OCSP_REQUIRED",
+ "CERT_PINNING_FAILURES_BY_CA",
+ "CERT_PINNING_MOZ_RESULTS",
+ "CERT_PINNING_MOZ_RESULTS_BY_HOST",
+ "CERT_PINNING_MOZ_TEST_RESULTS",
+ "CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST",
+ "CERT_PINNING_RESULTS",
+ "CERT_PINNING_TEST_RESULTS",
+ "CERT_VALIDATION_HTTP_REQUEST_CANCELED_TIME",
+ "CERT_VALIDATION_HTTP_REQUEST_FAILED_TIME",
+ "CERT_VALIDATION_HTTP_REQUEST_RESULT",
+ "CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME",
+ "CERT_VALIDATION_SUCCESS_BY_CA",
+ "CHANGES_OF_DETECTED_LANGUAGE",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "CHARSET_OVERRIDE_SITUATION",
+ "CHARSET_OVERRIDE_USED",
+ "CHECK_ADDONS_MODIFIED_MS",
+ "CHECK_JAVA_ENABLED",
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
+ "COMPOSITE_FRAME_ROUNDTRIP_TIME",
+ "COMPOSITE_TIME",
+ "CONTENT_DOCUMENTS_DESTROYED",
+ "COOKIE_SCHEME_SECURITY",
+ "CRASH_STORE_COMPRESSED_BYTES",
+ "CYCLE_COLLECTOR",
+ "CYCLE_COLLECTOR_ASYNC_SNOW_WHITE_FREEING",
+ "CYCLE_COLLECTOR_COLLECTED",
+ "CYCLE_COLLECTOR_FINISH_IGC",
+ "CYCLE_COLLECTOR_FULL",
+ "CYCLE_COLLECTOR_MAX_PAUSE",
+ "CYCLE_COLLECTOR_NEED_GC",
+ "CYCLE_COLLECTOR_OOM",
+ "CYCLE_COLLECTOR_SYNC_SKIPPABLE",
+ "CYCLE_COLLECTOR_TIME_BETWEEN",
+ "CYCLE_COLLECTOR_VISITED_GCED",
+ "CYCLE_COLLECTOR_VISITED_REF_COUNTED",
+ "CYCLE_COLLECTOR_WORKER",
+ "CYCLE_COLLECTOR_WORKER_COLLECTED",
+ "CYCLE_COLLECTOR_WORKER_NEED_GC",
+ "CYCLE_COLLECTOR_WORKER_OOM",
+ "CYCLE_COLLECTOR_WORKER_VISITED_GCED",
+ "CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED",
+ "D3D11_SYNC_HANDLE_FAILURE",
+ "DATABASE_LOCKED_EXCEPTION",
+ "DATABASE_SUCCESSFUL_UNLOCK",
+ "DATA_STORAGE_ENTRIES",
+ "DECODER_INSTANTIATED_IBM866",
+ "DECODER_INSTANTIATED_ISO2022JP",
+ "DECODER_INSTANTIATED_ISO_8859_5",
+ "DECODER_INSTANTIATED_KOI8R",
+ "DECODER_INSTANTIATED_KOI8U",
+ "DECODER_INSTANTIATED_MACARABIC",
+ "DECODER_INSTANTIATED_MACCE",
+ "DECODER_INSTANTIATED_MACCROATIAN",
+ "DECODER_INSTANTIATED_MACCYRILLIC",
+ "DECODER_INSTANTIATED_MACDEVANAGARI",
+ "DECODER_INSTANTIATED_MACFARSI",
+ "DECODER_INSTANTIATED_MACGREEK",
+ "DECODER_INSTANTIATED_MACGUJARATI",
+ "DECODER_INSTANTIATED_MACGURMUKHI",
+ "DECODER_INSTANTIATED_MACHEBREW",
+ "DECODER_INSTANTIATED_MACICELANDIC",
+ "DECODER_INSTANTIATED_MACROMANIAN",
+ "DECODER_INSTANTIATED_MACTURKISH",
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED",
+ "DEFERRED_FINALIZE_ASYNC",
+ "DENIED_TRANSLATION_OFFERS",
+ "DEVICE_RESET_REASON",
+ "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_CANVASDEBUGGER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS",
+ "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT",
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT",
+ "DEVTOOLS_HUD_APP_MEMORY_CONTENTINTERACTIVE_V2",
+ "DEVTOOLS_HUD_APP_MEMORY_FULLYLOADED_V2",
+ "DEVTOOLS_HUD_APP_MEMORY_MEDIAENUMERATED_V2",
+ "DEVTOOLS_HUD_APP_MEMORY_NAVIGATIONINTERACTIVE_V2",
+ "DEVTOOLS_HUD_APP_MEMORY_NAVIGATIONLOADED_V2",
+ "DEVTOOLS_HUD_APP_MEMORY_SCANEND_V2",
+ "DEVTOOLS_HUD_APP_MEMORY_VISUALLYLOADED_V2",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_CONTENTINTERACTIVE",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_FULLYLOADED",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_MEDIAENUMERATED",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_NAVIGATIONINTERACTIVE",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_NAVIGATIONLOADED",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_SCANEND",
+ "DEVTOOLS_HUD_APP_STARTUP_TIME_VISUALLYLOADED",
+ "DEVTOOLS_HUD_ERRORS",
+ "DEVTOOLS_HUD_JANK",
+ "DEVTOOLS_HUD_REFLOWS",
+ "DEVTOOLS_HUD_REFLOW_DURATION",
+ "DEVTOOLS_HUD_SECURITY_CATEGORY",
+ "DEVTOOLS_HUD_USS",
+ "DEVTOOLS_HUD_WARNINGS",
+ "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_OS_ENUMERATED_PER_USER",
+ "DEVTOOLS_OS_IS_64_BITS_PER_USER",
+ "DEVTOOLS_PAINTFLASHING_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT",
+ "DEVTOOLS_PERFTOOLS_RECORDING_COUNT",
+ "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS",
+ "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG",
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG",
+ "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS",
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER",
+ "DEVTOOLS_SHADEREDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR",
+ "DEVTOOLS_TABS_OPEN_PEAK_LINEAR",
+ "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR",
+ "DEVTOOLS_TABS_PINNED_PEAK_LINEAR",
+ "DEVTOOLS_TILT_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBAUDIOEDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
+ "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
+ "DEVTOOLS_WEBIDE_CONNECTION_DEBUG_USED",
+ "DEVTOOLS_WEBIDE_CONNECTION_PLAY_USED",
+ "DEVTOOLS_WEBIDE_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS",
+ "DEVTOOLS_WEBIDE_LOCAL_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_OTHER_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBIDE_REMOTE_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_SIMULATOR_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS",
+ "DEVTOOLS_WEBIDE_USB_CONNECTION_RESULT",
+ "DEVTOOLS_WEBIDE_WIFI_CONNECTION_RESULT",
+ "DISPLAY_SCALING_LINUX",
+ "DISPLAY_SCALING_MSWIN",
+ "DISPLAY_SCALING_OSX",
+ "DNS_BLACKLIST_COUNT",
+ "DNS_CLEANUP_AGE",
+ "DNS_FAILED_LOOKUP_TIME",
+ "DNS_LOOKUP_METHOD2",
+ "DNS_LOOKUP_TIME",
+ "DNS_RENEWAL_TIME",
+ "DNS_RENEWAL_TIME_FOR_TTL",
+ "DNT_USAGE",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COUNT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL",
+ "DWRITEFONT_INIT_PROBLEM",
+ "E10S_BLOCKED_FROM_RUNNING",
+ "E10S_WINDOW",
+ "ENABLE_PRIVILEGE_EVER_CALLED",
+ "FENNEC_DISTRIBUTION_CODE_CATEGORY",
+ "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS",
+ "FENNEC_DISTRIBUTION_REFERRER_INVALID",
+ "FENNEC_GLOBALHISTORY_ADD_MS",
+ "FENNEC_GLOBALHISTORY_UPDATE_MS",
+ "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS",
+ "FENNEC_READING_LIST_COUNT",
+ "FENNEC_RESTORING_ACTIVITY",
+ "FENNEC_SEARCH_LOADER_TIME_MS",
+ "FENNEC_STARTUP_TIME_GECKOREADY",
+ "FENNEC_STARTUP_TIME_JAVAUI",
+ "FENNEC_SYNC11_MIGRATIONS_COMPLETED",
+ "FENNEC_SYNC11_MIGRATIONS_FAILED",
+ "FENNEC_SYNC11_MIGRATIONS_SUCCEEDED",
+ "FENNEC_SYNC11_MIGRATION_NOTIFICATIONS_OFFERED",
+ "FENNEC_SYNC11_MIGRATION_SENTINELS_SEEN",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_FAILED_BACKOFF",
+ "FENNEC_SYNC_NUMBER_OF_SYNCS_STARTED",
+ "FENNEC_TABQUEUE_QUEUESIZE",
+ "FENNEC_TOPSITES_LOADER_TIME_MS",
+ "FENNEC_WAS_KILLED",
+ "FETCH_IS_MAINTHREAD",
+ "FIND_PLUGINS",
+ "FLASH_PLUGIN_AREA",
+ "FLASH_PLUGIN_HEIGHT",
+ "FLASH_PLUGIN_INSTANCES_ON_PAGE",
+ "FLASH_PLUGIN_STATES",
+ "FLASH_PLUGIN_WIDTH",
+ "FONTLIST_INITFACENAMELISTS",
+ "FONTLIST_INITOTHERFAMILYNAMES",
+ "FONT_CACHE_HIT",
+ "FORCED_DEVICE_RESET_REASON",
+ "FORGET_SKIPPABLE_MAX",
+ "FX_BOOKMARKS_TOOLBAR_INIT_MS",
+ "FX_BROWSER_FULLSCREEN_USED",
+ "FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE",
+ "FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE",
+ "FX_NEW_WINDOW_MS",
+ "FX_PAGE_LOAD_MS",
+ "FX_SANITIZE_CACHE",
+ "FX_SANITIZE_COOKIES_2",
+ "FX_SANITIZE_DOWNLOADS",
+ "FX_SANITIZE_FORMDATA",
+ "FX_SANITIZE_HISTORY",
+ "FX_SANITIZE_OPENWINDOWS",
+ "FX_SANITIZE_SESSIONS",
+ "FX_SANITIZE_SITESETTINGS",
+ "FX_SANITIZE_TOTAL",
+ "FX_SESSION_RESTORE_ALL_FILES_CORRUPT",
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS",
+ "FX_SESSION_RESTORE_COLLECT_COOKIES_MS",
+ "FX_SESSION_RESTORE_COLLECT_DATA_LONGEST_OP_MS",
+ "FX_SESSION_RESTORE_COLLECT_DATA_MS",
+ "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS",
+ "FX_SESSION_RESTORE_CORRUPT_FILE",
+ "FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS",
+ "FX_SESSION_RESTORE_FILE_SIZE_BYTES",
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
+ "FX_SESSION_RESTORE_READ_FILE_MS",
+ "FX_SESSION_RESTORE_RESTORE_WINDOW_MS",
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM",
+ "FX_SESSION_RESTORE_SERIALIZE_DATA_MS",
+ "FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS",
+ "FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS",
+ "FX_SESSION_RESTORE_WRITE_FILE_MS",
+ "FX_TABLETMODE_PAGE_LOAD",
+ "FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS",
+ "FX_TAB_ANIM_OPEN_FRAME_INTERVAL_MS",
+ "FX_TAB_ANIM_OPEN_PREVIEW_FRAME_INTERVAL_MS",
+ "FX_TAB_CLICK_MS",
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
+ "FX_TAB_SWITCH_TOTAL_E10S_MS",
+ "FX_TAB_SWITCH_TOTAL_MS",
+ "FX_TAB_SWITCH_UPDATE_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2",
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS",
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
+ "FX_THUMBNAILS_CAPTURE_TIME_MS",
+ "FX_THUMBNAILS_HIT_OR_MISS",
+ "FX_THUMBNAILS_STORE_TIME_MS",
+ "FX_TOTAL_TOP_VISITS",
+ "FX_TOUCH_USED",
+ "GC_ANIMATION_MS",
+ "GC_BUDGET_MS",
+ "GC_COMPACT_MS",
+ "GC_INCREMENTAL_DISABLED",
+ "GC_IS_COMPARTMENTAL",
+ "GC_MARK_GRAY_MS",
+ "GC_MARK_MS",
+ "GC_MARK_ROOTS_MS",
+ "GC_MAX_PAUSE_MS",
+ "GC_MINOR_REASON",
+ "GC_MINOR_REASON_LONG",
+ "GC_MINOR_US",
+ "GC_MMU_50",
+ "GC_MS",
+ "GC_NON_INCREMENTAL",
+ "GC_REASON_2",
+ "GC_RESET",
+ "GC_SCC_SWEEP_MAX_PAUSE_MS",
+ "GC_SCC_SWEEP_TOTAL_MS",
+ "GC_SLICE_MS",
+ "GC_SLOW_PHASE",
+ "GC_SWEEP_MS",
+ "GDI_INITFONTLIST_TOTAL",
+ "GEOLOCATION_ACCURACY_EXPONENTIAL",
+ "GEOLOCATION_ERROR",
+ "GEOLOCATION_OSX_SOURCE_IS_MLS",
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS",
+ "GFX_CONTENT_FAILED_TO_ACQUIRE_DEVICE",
+ "GFX_CRASH",
+ "GHOST_WINDOWS",
+ "GRADIENT_DURATION",
+ "GRADIENT_RETENTION_TIME",
+ "GRAPHICS_DRIVER_STARTUP_TEST",
+ "GRAPHICS_SANITY_TEST",
+ "GRAPHICS_SANITY_TEST_OS_SNAPSHOT",
+ "GRAPHICS_SANITY_TEST_REASON",
+ "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS",
+ "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN",
+ "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN",
+ "HTTPCONNMGR_USED_SPECULATIVE_CONN",
+ "HTTP_AUTH_DIALOG_STATS",
+ "HTTP_CACHE_DISPOSITION_2",
+ "HTTP_CACHE_DISPOSITION_2_V2",
+ "HTTP_CACHE_ENTRY_ALIVE_TIME",
+ "HTTP_CACHE_ENTRY_RELOAD_TIME",
+ "HTTP_CACHE_ENTRY_REUSE_COUNT",
+ "HTTP_CACHE_MISS_HALFLIFE_EXPERIMENT_2",
+ "HTTP_CONNECTION_ENTRY_CACHE_HIT_1",
+ "HTTP_CONTENT_ENCODING",
+ "HTTP_DISK_CACHE_DISPOSITION_2",
+ "HTTP_DISK_CACHE_OVERHEAD",
+ "HTTP_KBREAD_PER_CONN",
+ "HTTP_MEMORY_CACHE_DISPOSITION_2",
+ "HTTP_OFFLINE_CACHE_DISPOSITION_2",
+ "HTTP_OFFLINE_CACHE_DOCUMENT_LOAD",
+ "HTTP_PAGELOAD_IS_SSL",
+ "HTTP_PAGE_CACHE_READ_TIME",
+ "HTTP_PAGE_CACHE_READ_TIME_V2",
+ "HTTP_PAGE_COMPLETE_LOAD",
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED",
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_NET",
+ "HTTP_PAGE_COMPLETE_LOAD_NET_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_V2",
+ "HTTP_PAGE_DNS_ISSUE_TIME",
+ "HTTP_PAGE_DNS_LOOKUP_TIME",
+ "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE",
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_SENT",
+ "HTTP_PAGE_REVALIDATION",
+ "HTTP_PAGE_TCP_CONNECTION",
+ "HTTP_PROXY_TYPE",
+ "HTTP_REQUEST_PER_CONN",
+ "HTTP_REQUEST_PER_PAGE",
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE",
+ "HTTP_RESPONSE_VERSION",
+ "HTTP_SAW_QUIC_ALT_PROTOCOL",
+ "HTTP_SCHEME_UPGRADE",
+ "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME",
+ "HTTP_SUBITEM_OPEN_LATENCY_TIME",
+ "HTTP_SUB_CACHE_READ_TIME",
+ "HTTP_SUB_CACHE_READ_TIME_V2",
+ "HTTP_SUB_COMPLETE_LOAD",
+ "HTTP_SUB_COMPLETE_LOAD_CACHED",
+ "HTTP_SUB_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_SUB_COMPLETE_LOAD_NET",
+ "HTTP_SUB_COMPLETE_LOAD_NET_V2",
+ "HTTP_SUB_COMPLETE_LOAD_V2",
+ "HTTP_SUB_DNS_ISSUE_TIME",
+ "HTTP_SUB_DNS_LOOKUP_TIME",
+ "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE",
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_SUB_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_SENT",
+ "HTTP_SUB_REVALIDATION",
+ "HTTP_SUB_TCP_CONNECTION",
+ "HTTP_TRANSACTION_IS_SSL",
+ "HTTP_TRANSACTION_USE_ALTSVC",
+ "HTTP_TRANSACTION_USE_ALTSVC_OE",
+ "IMAGE_DECODE_CHUNKS",
+ "IMAGE_DECODE_COUNT",
+ "IMAGE_DECODE_LATENCY_US",
+ "IMAGE_DECODE_ON_DRAW_LATENCY",
+ "IMAGE_DECODE_SPEED_GIF",
+ "IMAGE_DECODE_SPEED_JPEG",
+ "IMAGE_DECODE_SPEED_PNG",
+ "IMAGE_DECODE_TIME",
+ "INNERWINDOWS_WITH_MUTATION_LISTENERS",
+ "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB",
+ "IPC_TRANSACTION_CANCEL",
+ "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY",
+ "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_ADDONS",
+ "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT",
+ "JS_TELEMETRY_ADDON_EXCEPTIONS",
+ "LINK_ICON_SIZES_ATTR_DIMENSION",
+ "LINK_ICON_SIZES_ATTR_USAGE",
+ "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
+ "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
+ "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS",
+ "LONG_REFLOW_INTERRUPTIBLE",
+ "LOW_MEMORY_EVENTS_COMMIT_SPACE",
+ "LOW_MEMORY_EVENTS_PHYSICAL",
+ "LOW_MEMORY_EVENTS_VIRTUAL",
+ "MAC_INITFONTLIST_TOTAL",
+ "MASTER_PASSWORD_ENABLED",
+ "MEDIA_CODEC_USED",
+ "MEDIA_WMF_DECODE_ERROR",
+ "MEMORY_FREE_PURGED_PAGES_MS",
+ "MEMORY_HEAP_ALLOCATED",
+ "MEMORY_HEAP_COMMITTED_UNUSED",
+ "MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED",
+ "MEMORY_JS_COMPARTMENTS_SYSTEM",
+ "MEMORY_JS_COMPARTMENTS_USER",
+ "MEMORY_JS_GC_HEAP",
+ "MEMORY_STORAGE_SQLITE",
+ "MEMORY_VSIZE",
+ "MEMORY_VSIZE_MAX_CONTIGUOUS",
+ "MIXED_CONTENT_HSTS",
+ "MIXED_CONTENT_PAGE_LOAD",
+ "MIXED_CONTENT_UNBLOCK_COUNTER",
+ "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS",
+ "MOZ_SQLITE_COOKIES_READ_B",
+ "MOZ_SQLITE_COOKIES_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_COOKIES_READ_MS",
+ "MOZ_SQLITE_COOKIES_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_COOKIES_SYNC_MS",
+ "MOZ_SQLITE_COOKIES_WRITE_B",
+ "MOZ_SQLITE_COOKIES_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_COOKIES_WRITE_MS",
+ "MOZ_SQLITE_OPEN_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OPEN_MS",
+ "MOZ_SQLITE_OTHER_READ_B",
+ "MOZ_SQLITE_OTHER_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OTHER_READ_MS",
+ "MOZ_SQLITE_OTHER_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OTHER_SYNC_MS",
+ "MOZ_SQLITE_OTHER_WRITE_B",
+ "MOZ_SQLITE_OTHER_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_OTHER_WRITE_MS",
+ "MOZ_SQLITE_PLACES_READ_B",
+ "MOZ_SQLITE_PLACES_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_PLACES_READ_MS",
+ "MOZ_SQLITE_PLACES_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_PLACES_SYNC_MS",
+ "MOZ_SQLITE_PLACES_WRITE_B",
+ "MOZ_SQLITE_PLACES_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_PLACES_WRITE_MS",
+ "MOZ_SQLITE_TRUNCATE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_TRUNCATE_MS",
+ "MOZ_SQLITE_WEBAPPS_READ_B",
+ "MOZ_SQLITE_WEBAPPS_READ_MAIN_THREAD_MS",
+ "MOZ_SQLITE_WEBAPPS_READ_MS",
+ "MOZ_SQLITE_WEBAPPS_SYNC_MAIN_THREAD_MS",
+ "MOZ_SQLITE_WEBAPPS_SYNC_MS",
+ "MOZ_SQLITE_WEBAPPS_WRITE_B",
+ "MOZ_SQLITE_WEBAPPS_WRITE_MAIN_THREAD_MS",
+ "MOZ_SQLITE_WEBAPPS_WRITE_MS",
+ "MOZ_STORAGE_ASYNC_REQUESTS_MS",
+ "MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS",
+ "NETWORK_CACHE_FS_TYPE",
+ "NETWORK_CACHE_HASH_STATS",
+ "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE",
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS",
+ "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS",
+ "NETWORK_CACHE_METADATA_SIZE",
+ "NETWORK_CACHE_SIZE_FULL_FAT",
+ "NETWORK_CACHE_V1_HIT_TIME_MS",
+ "NETWORK_CACHE_V1_MISS_TIME_MS",
+ "NETWORK_CACHE_V1_TRUNCATE_TIME_MS",
+ "NETWORK_CACHE_V2_HIT_TIME_MS",
+ "NETWORK_CACHE_V2_INPUT_STREAM_STATUS",
+ "NETWORK_CACHE_V2_MISS_TIME_MS",
+ "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS",
+ "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_DELETEDIR",
+ "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN",
+ "NETWORK_DISK_CACHE_OPEN",
+ "NETWORK_DISK_CACHE_SHUTDOWN",
+ "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_SHUTDOWN_V2",
+ "NETWORK_DISK_CACHE_TRASHRENAME",
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
+ "NEWTAB_PAGE_ENABLED",
+ "NEWTAB_PAGE_ENHANCED",
+ "NEWTAB_PAGE_LIFE_SPAN",
+ "NEWTAB_PAGE_LIFE_SPAN_SUGGESTED",
+ "NEWTAB_PAGE_PINNED_SITES_COUNT",
+ "NEWTAB_PAGE_SHOWN",
+ "NEWTAB_PAGE_SITE_CLICKED",
+ "NTLM_MODULE_USED_2",
+ "ONBEFOREUNLOAD_PROMPT_ACTION",
+ "ONBEFOREUNLOAD_PROMPT_COUNT",
+ "OSFILE_WORKER_LAUNCH_MS",
+ "OSFILE_WORKER_READY_MS",
+ "OSFILE_WRITEATOMIC_JANK_MS",
+ "PAGE_FAULTS_HARD",
+ "PAINT_BUILD_DISPLAYLIST_TIME",
+ "PAINT_RASTERIZE_TIME",
+ "PDF_VIEWER_DOCUMENT_GENERATOR",
+ "PDF_VIEWER_DOCUMENT_SIZE_KB",
+ "PDF_VIEWER_DOCUMENT_VERSION",
+ "PDF_VIEWER_EMBED",
+ "PDF_VIEWER_FALLBACK_SHOWN",
+ "PDF_VIEWER_FONT_TYPES",
+ "PDF_VIEWER_FORM",
+ "PDF_VIEWER_PRINT",
+ "PDF_VIEWER_STREAM_TYPES",
+ "PDF_VIEWER_TIME_TO_VIEW_MS",
+ "PDF_VIEWER_USED",
+ "PERF_MONITORING_SLOW_ADDON_CPOW_US",
+ "PERF_MONITORING_SLOW_ADDON_JANK_US",
+ "PERF_MONITORING_TEST_CPU_RESCHEDULING_PROPORTION_MOVED",
+ "PERMISSIONS_MIGRATION_7_ERROR",
+ "PERMISSIONS_REMIGRATION_COMPARISON",
+ "PERMISSIONS_SQL_CORRUPTED",
+ "PLACES_ANNOS_BOOKMARKS_COUNT",
+ "PLACES_ANNOS_PAGES_COUNT",
+ "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS",
+ "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS",
+ "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS",
+ "PLACES_BACKUPS_BOOKMARKSTREE_MS",
+ "PLACES_BACKUPS_DAYSFROMLAST",
+ "PLACES_BACKUPS_TOJSON_MS",
+ "PLACES_BOOKMARKS_COUNT",
+ "PLACES_DATABASE_FILESIZE_MB",
+ "PLACES_DATABASE_PAGESIZE_B",
+ "PLACES_DATABASE_SIZE_PER_PAGE_B",
+ "PLACES_EXPIRATION_STEPS_TO_CLEAN2",
+ "PLACES_EXPORT_TOHTML_MS",
+ "PLACES_FAVICON_BMP_SIZES",
+ "PLACES_FAVICON_GIF_SIZES",
+ "PLACES_FAVICON_ICO_SIZES",
+ "PLACES_FAVICON_JPEG_SIZES",
+ "PLACES_FAVICON_OTHER_SIZES",
+ "PLACES_FAVICON_PNG_SIZES",
+ "PLACES_FAVICON_SVG_SIZES",
+ "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS",
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS",
+ "PLACES_IDLE_MAINTENANCE_TIME_MS",
+ "PLACES_KEYWORDS_COUNT",
+ "PLACES_MAINTENANCE_DAYSFROMLAST",
+ "PLACES_MOST_RECENT_EXPIRED_VISIT_DAYS",
+ "PLACES_PAGES_COUNT",
+ "PLACES_SORTED_BOOKMARKS_PERC",
+ "PLACES_TAGGED_BOOKMARKS_PERC",
+ "PLACES_TAGS_COUNT",
+ "PLUGINS_INFOBAR_ALLOW",
+ "PLUGINS_INFOBAR_BLOCK",
+ "PLUGINS_INFOBAR_SHOWN",
+ "PLUGINS_NOTIFICATION_PLUGIN_COUNT",
+ "PLUGINS_NOTIFICATION_SHOWN",
+ "PLUGINS_NOTIFICATION_USER_ACTION",
+ "PLUGIN_CALLED_DIRECTLY",
+ "PLUGIN_HANG_NOTICE_COUNT",
+ "PLUGIN_HANG_TIME",
+ "PLUGIN_HANG_UI_DONT_ASK",
+ "PLUGIN_HANG_UI_RESPONSE_TIME",
+ "PLUGIN_HANG_UI_USER_RESPONSE",
+ "PLUGIN_LOAD_METADATA",
+ "PLUGIN_SHUTDOWN_MS",
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_NORMAL",
+ "PRCONNECT_BLOCKING_TIME_OFFLINE",
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
+ "PREDICTOR_BASE_CONFIDENCE",
+ "PREDICTOR_CONFIDENCE",
+ "PREDICTOR_GLOBAL_DEGRADATION",
+ "PREDICTOR_LEARN_ATTEMPTS",
+ "PREDICTOR_LEARN_FULL_QUEUE",
+ "PREDICTOR_LEARN_WORK_TIME",
+ "PREDICTOR_PREDICTIONS_CALCULATED",
+ "PREDICTOR_PREDICT_ATTEMPTS",
+ "PREDICTOR_PREDICT_FULL_QUEUE",
+ "PREDICTOR_PREDICT_TIME_TO_ACTION",
+ "PREDICTOR_PREDICT_TIME_TO_INACTION",
+ "PREDICTOR_PREDICT_WORK_TIME",
+ "PREDICTOR_SUBRESOURCE_DEGRADATION",
+ "PREDICTOR_TOTAL_PRECONNECTS",
+ "PREDICTOR_TOTAL_PRECONNECTS_CREATED",
+ "PREDICTOR_TOTAL_PRECONNECTS_UNUSED",
+ "PREDICTOR_TOTAL_PRECONNECTS_USED",
+ "PREDICTOR_TOTAL_PREDICTIONS",
+ "PREDICTOR_TOTAL_PRERESOLVES",
+ "PREDICTOR_WAIT_TIME",
+ "PROCESS_CRASH_SUBMIT_ATTEMPT",
+ "PROCESS_CRASH_SUBMIT_SUCCESS",
+ "PUSH_API_NOTIFICATION_RECEIVED",
+ "PUSH_API_NOTIFICATION_RECEIVED_BUT_DID_NOT_NOTIFY",
+ "PUSH_API_NOTIFY",
+ "PUSH_API_NOTIFY_REGISTRATION_LOST",
+ "PUSH_API_PERMISSION_DENIED",
+ "PUSH_API_PERMISSION_GRANTED",
+ "PUSH_API_PERMISSION_REQUESTED",
+ "PUSH_API_QUOTA_EXPIRATION_TIME",
+ "PUSH_API_QUOTA_RESET_TO",
+ "PUSH_API_SUBSCRIBE_ATTEMPT",
+ "PUSH_API_SUBSCRIBE_FAILED",
+ "PUSH_API_SUBSCRIBE_HTTP2_TIME",
+ "PUSH_API_SUBSCRIBE_SUCCEEDED",
+ "PUSH_API_SUBSCRIBE_WS_TIME",
+ "PUSH_API_UNSUBSCRIBE_ATTEMPT",
+ "PUSH_API_UNSUBSCRIBE_FAILED",
+ "PUSH_API_UNSUBSCRIBE_SUCCEEDED",
+ "PUSH_API_USED",
+ "PWMGR_BLOCKLIST_NUM_SITES",
+ "PWMGR_FORM_AUTOFILL_RESULT",
+ "PWMGR_LOGIN_LAST_USED_DAYS",
+ "PWMGR_LOGIN_PAGE_SAFETY",
+ "PWMGR_MANAGE_COPIED_PASSWORD",
+ "PWMGR_MANAGE_COPIED_USERNAME",
+ "PWMGR_MANAGE_DELETED",
+ "PWMGR_MANAGE_DELETED_ALL",
+ "PWMGR_MANAGE_OPENED",
+ "PWMGR_MANAGE_SORTED",
+ "PWMGR_MANAGE_VISIBILITY_TOGGLED",
+ "PWMGR_NUM_HTTPAUTH_PASSWORDS",
+ "PWMGR_NUM_PASSWORDS_PER_HOSTNAME",
+ "PWMGR_NUM_SAVED_PASSWORDS",
+ "PWMGR_PASSWORD_INPUT_IN_FORM",
+ "PWMGR_PROMPT_REMEMBER_ACTION",
+ "PWMGR_PROMPT_UPDATE_ACTION",
+ "PWMGR_SAVING_ENABLED",
+ "PWMGR_USERNAME_PRESENT",
+ "RANGE_CHECKSUM_ERRORS",
+ "READER_MODE_DOWNLOAD_RESULT",
+ "READER_MODE_PARSE_RESULT",
+ "REFRESH_DRIVER_TICK",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "SAFE_MODE_USAGE",
+ "SEARCH_COUNTS",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS",
+ "SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_INIT_MS",
+ "SEARCH_SERVICE_INIT_SYNC",
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX",
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE",
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY",
+ "SECURITY_UI",
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS",
+ "SERVICE_WORKER_LIFE_TIME",
+ "SERVICE_WORKER_REGISTRATIONS",
+ "SERVICE_WORKER_REGISTRATION_LOADING",
+ "SERVICE_WORKER_REQUEST_PASSTHROUGH",
+ "SERVICE_WORKER_SPAWN_ATTEMPTS",
+ "SERVICE_WORKER_UPDATED",
+ "SERVICE_WORKER_WAS_SPAWNED",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "SHUTDOWN_OK",
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE",
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN",
+ "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION",
+ "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN",
+ "SLOW_ADDON_WARNING_RESPONSE_TIME",
+ "SLOW_ADDON_WARNING_STATES",
+ "SLOW_SCRIPT_NOTICE_COUNT",
+ "SOCIAL_ENABLED_ON_SESSION",
+ "SOCIAL_PANEL_CLICKS",
+ "SOCIAL_SIDEBAR_OPEN_DURATION",
+ "SOCIAL_SIDEBAR_STATE",
+ "SOCIAL_TOOLBAR_BUTTONS",
+ "SPDY_CHUNK_RECVD",
+ "SPDY_GOAWAY_LOCAL",
+ "SPDY_GOAWAY_PEER",
+ "SPDY_KBREAD_PER_CONN",
+ "SPDY_NPN_CONNECT",
+ "SPDY_NPN_JOIN",
+ "SPDY_PARALLEL_STREAMS",
+ "SPDY_REQUEST_PER_CONN",
+ "SPDY_SERVER_INITIATED_STREAMS",
+ "SPDY_SETTINGS_CWND",
+ "SPDY_SETTINGS_DL_BW",
+ "SPDY_SETTINGS_IW",
+ "SPDY_SETTINGS_MAX_STREAMS",
+ "SPDY_SETTINGS_RETRANS",
+ "SPDY_SETTINGS_RTT",
+ "SPDY_SETTINGS_UL_BW",
+ "SPDY_SYN_RATIO",
+ "SPDY_SYN_REPLY_RATIO",
+ "SPDY_SYN_REPLY_SIZE",
+ "SPDY_SYN_SIZE",
+ "SPDY_VERSION2",
+ "SSL_AUTH_ALGORITHM_FULL",
+ "SSL_AUTH_ECDSA_CURVE_FULL",
+ "SSL_AUTH_RSA_KEY_SIZE_FULL",
+ "SSL_BYTES_BEFORE_CERT_CALLBACK",
+ "SSL_CERT_ERROR_OVERRIDES",
+ "SSL_CERT_VERIFICATION_ERRORS",
+ "SSL_CIPHER_SUITE_FULL",
+ "SSL_CIPHER_SUITE_RESUMED",
+ "SSL_HANDSHAKE_TYPE",
+ "SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_MOZILLAPKIX",
+ "SSL_KEA_DHE_KEY_SIZE_FULL",
+ "SSL_KEA_ECDHE_CURVE_FULL",
+ "SSL_KEA_RSA_KEY_SIZE_FULL",
+ "SSL_KEY_EXCHANGE_ALGORITHM_FULL",
+ "SSL_KEY_EXCHANGE_ALGORITHM_RESUMED",
+ "SSL_NPN_TYPE",
+ "SSL_OBSERVED_END_ENTITY_CERTIFICATE_LIFETIME",
+ "SSL_OCSP_MAY_FETCH",
+ "SSL_OCSP_STAPLING",
+ "SSL_PERMANENT_CERT_ERROR_OVERRIDES",
+ "SSL_REASONS_FOR_NOT_FALSE_STARTING",
+ "SSL_RESUMED_SESSION",
+ "SSL_SERVER_AUTH_EKU",
+ "SSL_SUCCESFUL_CERT_VALIDATION_TIME_MOZILLAPKIX",
+ "SSL_SYMMETRIC_CIPHER_FULL",
+ "SSL_SYMMETRIC_CIPHER_RESUMED",
+ "SSL_TIME_UNTIL_HANDSHAKE_FINISHED",
+ "SSL_TIME_UNTIL_READY",
+ "SSL_TLS10_INTOLERANCE_REASON_POST",
+ "SSL_TLS10_INTOLERANCE_REASON_PRE",
+ "SSL_TLS11_INTOLERANCE_REASON_POST",
+ "SSL_TLS11_INTOLERANCE_REASON_PRE",
+ "SSL_TLS12_INTOLERANCE_REASON_POST",
+ "SSL_TLS12_INTOLERANCE_REASON_PRE",
+ "SSL_VERSION_FALLBACK_INAPPROPRIATE",
+ "SSL_WEAK_CIPHERS_FALLBACK",
+ "STARTUP_CACHE_AGE_HOURS",
+ "STARTUP_CACHE_INVALID",
+ "STARTUP_CRASH_DETECTED",
+ "STARTUP_MEASUREMENT_ERRORS",
+ "STS_NUMBER_OF_ONSOCKETREADY_CALLS",
+ "STS_NUMBER_OF_PENDING_EVENTS",
+ "STS_NUMBER_OF_PENDING_EVENTS_IN_THE_LAST_CYCLE",
+ "STS_POLL_AND_EVENTS_CYCLE",
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE",
+ "STS_POLL_BLOCK_TIME",
+ "STS_POLL_CYCLE",
+ "STUMBLER_OBSERVATIONS_PER_DAY",
+ "STUMBLER_TIME_BETWEEN_RECEIVED_LOCATIONS_SEC",
+ "STUMBLER_TIME_BETWEEN_START_SEC",
+ "STUMBLER_TIME_BETWEEN_UPLOADS_SEC",
+ "STUMBLER_UPLOAD_BYTES",
+ "STUMBLER_UPLOAD_CELL_COUNT",
+ "STUMBLER_UPLOAD_OBSERVATION_COUNT",
+ "STUMBLER_UPLOAD_WIFI_AP_COUNT",
+ "STUMBLER_VOLUME_BYTES_UPLOADED_PER_SEC",
+ "SUBJECT_PRINCIPAL_ACCESSED_WITHOUT_SCRIPT_ON_STACK",
+ "SUBPROCESS_ABNORMAL_ABORT",
+ "SUBPROCESS_CRASHES_WITH_DUMP",
+ "SYSTEM_FONT_FALLBACK",
+ "SYSTEM_FONT_FALLBACK_FIRST",
+ "SYSTEM_FONT_FALLBACK_SCRIPT",
+ "TELEMETRY_COMPRESS",
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB",
+ "TELEMETRY_DISCARDED_CONTENT_PINGS_COUNT",
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB",
+ "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB",
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED",
+ "TELEMETRY_MEMORY_REPORTER_MS",
+ "TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS",
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS",
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE",
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ",
+ "TELEMETRY_PENDING_PINGS_AGE",
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA",
+ "TELEMETRY_PENDING_PINGS_SIZE_MB",
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS",
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED",
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING",
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND",
+ "TELEMETRY_SESSIONDATA_FAILED_LOAD",
+ "TELEMETRY_SESSIONDATA_FAILED_PARSE",
+ "TELEMETRY_SESSIONDATA_FAILED_SAVE",
+ "TELEMETRY_SESSIONDATA_FAILED_VALIDATION",
+ "TELEMETRY_STRINGIFY",
+ "TELEMETRY_SUCCESS",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_EXPIRED",
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ "TELEMETRY_TEST_RELEASE_OPTIN",
+ "TELEMETRY_TEST_RELEASE_OPTOUT",
+ "THUNDERBIRD_CONVERSATIONS_TIME_TO_2ND_GLODA_QUERY_MS",
+ "THUNDERBIRD_GLODA_SIZE_MB",
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S",
+ "TLS_ERROR_REPORT_UI",
+ "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED",
+ "TOTAL_CONTENT_PAGE_LOAD_TIME",
+ "TOTAL_COUNT_HIGH_ERRORS",
+ "TOTAL_COUNT_LOW_ERRORS",
+ "TOUCH_ENABLED_DEVICE",
+ "TRACKING_PROTECTION_ENABLED",
+ "TRACKING_PROTECTION_EVENTS",
+ "TRACKING_PROTECTION_PBM_DISABLED",
+ "TRACKING_PROTECTION_SHIELD",
+ "TRANSACTION_WAIT_TIME_HTTP",
+ "TRANSACTION_WAIT_TIME_HTTP_PIPELINES",
+ "TRANSACTION_WAIT_TIME_SPDY",
+ "TRANSLATED_CHARACTERS",
+ "TRANSLATED_PAGES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "TRANSLATION_OPPORTUNITIES",
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
+ "UPDATE_CANNOT_STAGE_EXTERNAL",
+ "UPDATE_CANNOT_STAGE_NOTIFY",
+ "UPDATE_CHECK_CODE_EXTERNAL",
+ "UPDATE_CHECK_CODE_NOTIFY",
+ "UPDATE_CHECK_EXTENDED_ERROR_EXTERNAL",
+ "UPDATE_CHECK_EXTENDED_ERROR_NOTIFY",
+ "UPDATE_CHECK_NO_UPDATE_EXTERNAL",
+ "UPDATE_CHECK_NO_UPDATE_NOTIFY",
+ "UPDATE_DOWNLOAD_CODE_COMPLETE",
+ "UPDATE_DOWNLOAD_CODE_PARTIAL",
+ "UPDATE_INVALID_LASTUPDATETIME_EXTERNAL",
+ "UPDATE_INVALID_LASTUPDATETIME_NOTIFY",
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL",
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_ENABLED_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_ENABLED_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY",
+ "UPDATE_PING_COUNT_EXTERNAL",
+ "UPDATE_PING_COUNT_NOTIFY",
+ "UPDATE_PREF_SERVICE_ERRORS_EXTERNAL",
+ "UPDATE_PREF_SERVICE_ERRORS_NOTIFY",
+ "UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL",
+ "UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY",
+ "UPDATE_SERVICE_INSTALLED_EXTERNAL",
+ "UPDATE_SERVICE_INSTALLED_NOTIFY",
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL",
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY",
+ "UPDATE_STATE_CODE_COMPLETE_STAGE",
+ "UPDATE_STATE_CODE_COMPLETE_STARTUP",
+ "UPDATE_STATE_CODE_PARTIAL_STAGE",
+ "UPDATE_STATE_CODE_PARTIAL_STARTUP",
+ "UPDATE_STATE_CODE_UNKNOWN_STAGE",
+ "UPDATE_STATE_CODE_UNKNOWN_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP",
+ "UPDATE_UNABLE_TO_APPLY_EXTERNAL",
+ "UPDATE_UNABLE_TO_APPLY_NOTIFY",
+ "UPDATE_WIZ_LAST_PAGE_CODE",
+ "URLCLASSIFIER_CL_CHECK_TIME",
+ "URLCLASSIFIER_CL_UPDATE_TIME",
+ "URLCLASSIFIER_LC_COMPLETIONS",
+ "URLCLASSIFIER_LC_PREFIXES",
+ "URLCLASSIFIER_LOOKUP_TIME",
+ "URLCLASSIFIER_PS_CONSTRUCT_TIME",
+ "URLCLASSIFIER_PS_FALLOCATE_TIME",
+ "URLCLASSIFIER_PS_FILELOAD_TIME",
+ "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG",
+ "VIDEO_CANPLAYTYPE_H264_LEVEL",
+ "VIDEO_CANPLAYTYPE_H264_PROFILE",
+ "VIDEO_CAN_CREATE_AAC_DECODER",
+ "VIDEO_CAN_CREATE_H264_DECODER",
+ "VIDEO_DECODED_H264_SPS_CONSTRAINT_SET_FLAG",
+ "VIDEO_DECODED_H264_SPS_LEVEL",
+ "VIDEO_DECODED_H264_SPS_PROFILE",
+ "VIDEO_EME_PLAY_SUCCESS",
+ "VIDEO_EME_REQUEST_FAILURE_LATENCY_MS",
+ "VIDEO_EME_REQUEST_SUCCESS_LATENCY_MS",
+ "VIDEO_H264_SPS_MAX_NUM_REF_FRAMES",
+ "VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN",
+ "VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN",
+ "VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN",
+ "WEAVE_COMPLETE_SUCCESS_COUNT",
+ "WEAVE_CONFIGURED",
+ "WEAVE_CONFIGURED_MASTER_PASSWORD",
+ "WEAVE_START_COUNT",
+ "WEBCRYPTO_ALG",
+ "WEBCRYPTO_EXTRACTABLE_ENC",
+ "WEBCRYPTO_EXTRACTABLE_GENERATE",
+ "WEBCRYPTO_EXTRACTABLE_IMPORT",
+ "WEBCRYPTO_EXTRACTABLE_SIG",
+ "WEBCRYPTO_METHOD",
+ "WEBCRYPTO_RESOLVED",
+ "WEBFONT_COMPRESSION_WOFF",
+ "WEBFONT_COMPRESSION_WOFF2",
+ "WEBFONT_DOWNLOAD_TIME",
+ "WEBFONT_DOWNLOAD_TIME_AFTER_START",
+ "WEBFONT_FONTTYPE",
+ "WEBFONT_PER_PAGE",
+ "WEBFONT_SIZE",
+ "WEBFONT_SIZE_PER_PAGE",
+ "WEBFONT_SRCTYPE",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_PACKETLOSS_RATE",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_PACKETLOSS_RATE",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS",
+ "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS",
+ "WEBRTC_CALL_COUNT",
+ "WEBRTC_CALL_DURATION",
+ "WEBRTC_CALL_TYPE",
+ "WEBRTC_DATACHANNEL_NEGOTIATED",
+ "WEBRTC_GET_USER_MEDIA_SECURE_ORIGIN",
+ "WEBRTC_GET_USER_MEDIA_TYPE",
+ "WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_FAILURE",
+ "WEBRTC_ICE_ADD_CANDIDATE_ERRORS_GIVEN_SUCCESS",
+ "WEBRTC_ICE_FAILURE_TIME",
+ "WEBRTC_ICE_FINAL_CONNECTION_STATE",
+ "WEBRTC_ICE_LATE_TRICKLE_ARRIVAL_TIME",
+ "WEBRTC_ICE_ON_TIME_TRICKLE_ARRIVAL_TIME",
+ "WEBRTC_ICE_SUCCESS_RATE",
+ "WEBRTC_ICE_SUCCESS_TIME",
+ "WEBRTC_LOAD_STATE_NORMAL",
+ "WEBRTC_LOAD_STATE_NORMAL_SHORT",
+ "WEBRTC_LOAD_STATE_RELAXED",
+ "WEBRTC_LOAD_STATE_RELAXED_SHORT",
+ "WEBRTC_LOAD_STATE_STRESSED",
+ "WEBRTC_LOAD_STATE_STRESSED_SHORT",
+ "WEBRTC_MAX_AUDIO_RECEIVE_TRACK",
+ "WEBRTC_MAX_AUDIO_SEND_TRACK",
+ "WEBRTC_MAX_VIDEO_RECEIVE_TRACK",
+ "WEBRTC_MAX_VIDEO_SEND_TRACK",
+ "WEBRTC_RENEGOTIATIONS",
+ "WEBRTC_STUN_RATE_LIMIT_EXCEEDED_BY_TYPE_GIVEN_FAILURE",
+ "WEBRTC_STUN_RATE_LIMIT_EXCEEDED_BY_TYPE_GIVEN_SUCCESS",
+ "WEBRTC_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM",
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL",
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL",
+ "WEBRTC_VIDEO_DECODE_ERROR_TIME_PERMILLE",
+ "WEBRTC_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_ENCODER_DROPPED_FRAMES_PER_CALL_FPM",
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_10X_STD_DEV_PER_CALL",
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_AVG_PER_CALL",
+ "WEBRTC_VIDEO_ERROR_RECOVERY_MS",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_JITTER",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_PACKETLOSS_RATE",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_PACKETLOSS_RATE",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN",
+ "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN",
+ "WEBSOCKETS_HANDSHAKE_TYPE",
+ "WORD_CACHE_HITS_CHROME",
+ "WORD_CACHE_HITS_CONTENT",
+ "WORD_CACHE_MISSES_CHROME",
+ "WORD_CACHE_MISSES_CONTENT",
+ "XMLHTTPREQUEST_ASYNC_OR_SYNC",
+ "XUL_CACHE_DISABLED"
+ ],
+ "n_buckets": [
+ "MEMORY_JS_GC_HEAP",
+ "MEMORY_HEAP_ALLOCATED",
+ "SYSTEM_FONT_FALLBACK_SCRIPT",
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE",
+ "SSL_TIME_UNTIL_READY",
+ "SSL_TIME_UNTIL_HANDSHAKE_FINISHED",
+ "CERT_VALIDATION_HTTP_REQUEST_CANCELED_TIME",
+ "CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME",
+ "CERT_VALIDATION_HTTP_REQUEST_FAILED_TIME",
+ "SSL_OBSERVED_END_ENTITY_CERTIFICATE_LIFETIME",
+ "SPDY_SERVER_INITIATED_STREAMS",
+ "STS_POLL_AND_EVENTS_CYCLE",
+ "STS_POLL_CYCLE",
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE",
+ "STS_POLL_BLOCK_TIME",
+ "PRCONNECT_BLOCKING_TIME_NORMAL",
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_OFFLINE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE",
+ "UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL",
+ "UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY",
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE",
+ "SECURITY_UI",
+ "CRASH_STORE_COMPRESSED_BYTES",
+ "MEDIA_WMF_DECODE_ERROR",
+ "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG",
+ "VIDEO_CANPLAYTYPE_H264_PROFILE",
+ "VIDEO_DECODED_H264_SPS_CONSTRAINT_SET_FLAG",
+ "VIDEO_DECODED_H264_SPS_PROFILE",
+ "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS",
+ "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_VIDEO_ERROR_RECOVERY_MS",
+ "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN",
+ "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_CALL_DURATION",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS",
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT",
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT",
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_SIZE",
+ "NETWORK_CACHE_METADATA_SIZE",
+ "NETWORK_CACHE_HASH_STATS",
+ "SSL_CIPHER_SUITE_FULL",
+ "SSL_CIPHER_SUITE_RESUMED",
+ "SSL_HANDSHAKE_RESULT",
+ "SSL_REASONS_FOR_NOT_FALSE_STARTING",
+ "SSL_CERT_VERIFICATION_ERRORS",
+ "CERT_VALIDATION_SUCCESS_BY_CA",
+ "CERT_PINNING_FAILURES_BY_CA",
+ "CERT_PINNING_MOZ_RESULTS_BY_HOST",
+ "CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST",
+ "GFX_CRASH",
+ "GC_REASON_2",
+ "GC_MINOR_REASON",
+ "GC_MINOR_REASON_LONG"
+ ],
+ "expiry_default": [
+ "A11Y_CONSUMERS",
+ "IDLE_NOTIFY_IDLE_MS",
+ "CACHE_MEMORY_SEARCH_2",
+ "OSFILE_WORKER_LAUNCH_MS",
+ "GEOLOCATION_OSX_SOURCE_IS_MLS",
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2",
+ "TRANSLATED_PAGES",
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM",
+ "SSL_PERMANENT_CERT_ERROR_OVERRIDES",
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "TRANSLATED_CHARACTERS",
+ "WEAVE_CONFIGURED",
+ "NEWTAB_PAGE_ENABLED",
+ "GRADIENT_DURATION",
+ "MOZ_SQLITE_OPEN_MS",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "NEWTAB_PAGE_LIFE_SPAN",
+ "FX_TOTAL_TOP_VISITS",
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
+ "CACHE_DISK_SEARCH_2",
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
+ "NEWTAB_PAGE_PINNED_SITES_COUNT",
+ "WEAVE_COMPLETE_SUCCESS_COUNT",
+ "A11Y_UPDATE_TIME",
+ "OSFILE_WRITEATOMIC_JANK_MS",
+ "STARTUP_MEASUREMENT_ERRORS",
+ "CERT_CHAIN_KEY_SIZE_STATUS",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "FX_NEW_WINDOW_MS",
+ "PDF_VIEWER_TIME_TO_VIEW_MS",
+ "SSL_OCSP_MAY_FETCH",
+ "MOZ_SQLITE_OTHER_READ_B",
+ "CHECK_JAVA_ENABLED",
+ "TRANSLATION_OPPORTUNITIES",
+ "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS",
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED",
+ "WEAVE_START_COUNT",
+ "FX_SESSION_RESTORE_RESTORE_WINDOW_MS",
+ "NEWTAB_PAGE_LIFE_SPAN_SUGGESTED",
+ "HTTP_DISK_CACHE_OVERHEAD",
+ "FX_SESSION_RESTORE_CORRUPT_FILE",
+ "FX_TAB_CLICK_MS",
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
+ "PDF_VIEWER_DOCUMENT_VERSION",
+ "GEOLOCATION_ACCURACY_EXPONENTIAL",
+ "FX_SESSION_RESTORE_READ_FILE_MS",
+ "CHANGES_OF_DETECTED_LANGUAGE",
+ "OSFILE_WORKER_READY_MS",
+ "PDF_VIEWER_FORM",
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_COLLECT_DATA_MS",
+ "FX_SESSION_RESTORE_FILE_SIZE_BYTES",
+ "STARTUP_CACHE_AGE_HOURS",
+ "FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS",
+ "DATA_STORAGE_ENTRIES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "MOZ_SQLITE_OTHER_WRITE_B",
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
+ "SSL_CERT_VERIFICATION_ERRORS",
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
+ "MOZ_SQLITE_PLACES_WRITE_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS",
+ "FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS",
+ "FX_SESSION_RESTORE_WRITE_FILE_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "NEWTAB_PAGE_ENHANCED",
+ "CERT_CHAIN_SHA1_POLICY_STATUS",
+ "PDF_VIEWER_DOCUMENT_SIZE_KB",
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS",
+ "SHUTDOWN_OK",
+ "PLACES_BACKUPS_TOJSON_MS",
+ "A11Y_ISIMPLEDOM_USAGE_FLAG",
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "PDF_VIEWER_DOCUMENT_GENERATOR",
+ "PDF_VIEWER_FALLBACK_SHOWN",
+ "FX_SESSION_RESTORE_ALL_FILES_CORRUPT",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "A11Y_IATABLE_USAGE_FLAG",
+ "FX_PAGE_LOAD_MS",
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
+ "DENIED_TRANSLATION_OFFERS",
+ "XUL_CACHE_DISABLED",
+ "PAGE_FAULTS_HARD",
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS",
+ "PDF_VIEWER_USED",
+ "NETWORK_DISK_CACHE_OPEN",
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS"
+ ]
+}
diff --git a/toolkit/components/telemetry/histogram_tools.py b/toolkit/components/telemetry/histogram_tools.py
new file mode 100644
index 0000000000..db64be268d
--- /dev/null
+++ b/toolkit/components/telemetry/histogram_tools.py
@@ -0,0 +1,513 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 collections
+import itertools
+import json
+import math
+import os
+import re
+import sys
+
+# Constants.
+MAX_LABEL_LENGTH = 20
+MAX_LABEL_COUNT = 100
+
+# histogram_tools.py is used by scripts from a mozilla-central build tree
+# and also by outside consumers, such as the telemetry server. We need
+# to ensure that importing things works in both contexts. Therefore,
+# unconditionally importing things that are local to the build tree, such
+# as buildconfig, is a no-no.
+try:
+ import buildconfig
+
+ # Need to update sys.path to be able to find usecounters.
+ sys.path.append(os.path.join(buildconfig.topsrcdir, 'dom/base/'))
+except ImportError:
+ # Must be in an out-of-tree usage scenario. Trust that whoever is
+ # running this script knows we need the usecounters module and has
+ # ensured it's in our sys.path.
+ pass
+
+from collections import OrderedDict
+
+def table_dispatch(kind, table, body):
+ """Call body with table[kind] if it exists. Raise an error otherwise."""
+ if kind in table:
+ return body(table[kind])
+ else:
+ raise BaseException, "don't know how to handle a histogram of kind %s" % kind
+
+class DefinitionException(BaseException):
+ pass
+
+def linear_buckets(dmin, dmax, n_buckets):
+ ret_array = [0] * n_buckets
+ dmin = float(dmin)
+ dmax = float(dmax)
+ for i in range(1, n_buckets):
+ linear_range = (dmin * (n_buckets - 1 - i) + dmax * (i - 1)) / (n_buckets - 2)
+ ret_array[i] = int(linear_range + 0.5)
+ return ret_array
+
+def exponential_buckets(dmin, dmax, n_buckets):
+ log_max = math.log(dmax);
+ bucket_index = 2;
+ ret_array = [0] * n_buckets
+ current = dmin
+ ret_array[1] = current
+ for bucket_index in range(2, n_buckets):
+ log_current = math.log(current)
+ log_ratio = (log_max - log_current) / (n_buckets - bucket_index)
+ log_next = log_current + log_ratio
+ next_value = int(math.floor(math.exp(log_next) + 0.5))
+ if next_value > current:
+ current = next_value
+ else:
+ current = current + 1
+ ret_array[bucket_index] = current
+ return ret_array
+
+always_allowed_keys = ['kind', 'description', 'cpp_guard', 'expires_in_version',
+ 'alert_emails', 'keyed', 'releaseChannelCollection',
+ 'bug_numbers']
+
+whitelists = None;
+try:
+ whitelist_path = os.path.join(os.path.abspath(os.path.realpath(os.path.dirname(__file__))), 'histogram-whitelists.json')
+ with open(whitelist_path, 'r') as f:
+ try:
+ whitelists = json.load(f)
+ for name, whitelist in whitelists.iteritems():
+ whitelists[name] = set(whitelist)
+ except ValueError, e:
+ raise BaseException, 'error parsing whitelist (%s)' % whitelist_path
+except IOError:
+ whitelists = None
+ print 'Unable to parse whitelist (%s). Assuming all histograms are acceptable.' % whitelist_path
+
+class Histogram:
+ """A class for representing a histogram definition."""
+
+ def __init__(self, name, definition, strict_type_checks=False):
+ """Initialize a histogram named name with the given definition.
+definition is a dict-like object that must contain at least the keys:
+
+ - 'kind': The kind of histogram. Must be one of 'boolean', 'flag',
+ 'count', 'enumerated', 'linear', or 'exponential'.
+ - 'description': A textual description of the histogram.
+ - 'strict_type_checks': A boolean indicating whether to use the new, stricter type checks.
+ The server-side still has to deal with old, oddly typed submissions,
+ so we have to skip them there by default.
+
+The key 'cpp_guard' is optional; if present, it denotes a preprocessor
+symbol that should guard C/C++ definitions associated with the histogram."""
+ self._strict_type_checks = strict_type_checks
+ self._is_use_counter = name.startswith("USE_COUNTER2_")
+ self.verify_attributes(name, definition)
+ self._name = name
+ self._description = definition['description']
+ self._kind = definition['kind']
+ self._cpp_guard = definition.get('cpp_guard')
+ self._keyed = definition.get('keyed', False)
+ self._expiration = definition.get('expires_in_version')
+ self._labels = definition.get('labels', [])
+ self.compute_bucket_parameters(definition)
+ table = {
+ 'boolean': 'BOOLEAN',
+ 'flag': 'FLAG',
+ 'count': 'COUNT',
+ 'enumerated': 'LINEAR',
+ 'categorical': 'CATEGORICAL',
+ 'linear': 'LINEAR',
+ 'exponential': 'EXPONENTIAL',
+ }
+ table_dispatch(self.kind(), table,
+ lambda k: self._set_nsITelemetry_kind(k))
+ datasets = { 'opt-in': 'DATASET_RELEASE_CHANNEL_OPTIN',
+ 'opt-out': 'DATASET_RELEASE_CHANNEL_OPTOUT' }
+ value = definition.get('releaseChannelCollection', 'opt-in')
+ if not value in datasets:
+ raise DefinitionException, "unknown release channel collection policy for " + name
+ self._dataset = "nsITelemetry::" + datasets[value]
+
+ def name(self):
+ """Return the name of the histogram."""
+ return self._name
+
+ def description(self):
+ """Return the description of the histogram."""
+ return self._description
+
+ def kind(self):
+ """Return the kind of the histogram.
+Will be one of 'boolean', 'flag', 'count', 'enumerated', 'categorical', 'linear',
+or 'exponential'."""
+ return self._kind
+
+ def expiration(self):
+ """Return the expiration version of the histogram."""
+ return self._expiration
+
+ def nsITelemetry_kind(self):
+ """Return the nsITelemetry constant corresponding to the kind of
+the histogram."""
+ return self._nsITelemetry_kind
+
+ def _set_nsITelemetry_kind(self, kind):
+ self._nsITelemetry_kind = "nsITelemetry::HISTOGRAM_%s" % kind
+
+ def low(self):
+ """Return the lower bound of the histogram."""
+ return self._low
+
+ def high(self):
+ """Return the high bound of the histogram."""
+ return self._high
+
+ def n_buckets(self):
+ """Return the number of buckets in the histogram."""
+ return self._n_buckets
+
+ def cpp_guard(self):
+ """Return the preprocessor symbol that should guard C/C++ definitions
+associated with the histogram. Returns None if no guarding is necessary."""
+ return self._cpp_guard
+
+ def keyed(self):
+ """Returns True if this a keyed histogram, false otherwise."""
+ return self._keyed
+
+ def dataset(self):
+ """Returns the dataset this histogram belongs into."""
+ return self._dataset
+
+ def labels(self):
+ """Returns a list of labels for a categorical histogram, [] for others."""
+ return self._labels
+
+ def ranges(self):
+ """Return an array of lower bounds for each bucket in the histogram."""
+ table = {
+ 'boolean': linear_buckets,
+ 'flag': linear_buckets,
+ 'count': linear_buckets,
+ 'enumerated': linear_buckets,
+ 'categorical': linear_buckets,
+ 'linear': linear_buckets,
+ 'exponential': exponential_buckets,
+ }
+ return table_dispatch(self.kind(), table,
+ lambda p: p(self.low(), self.high(), self.n_buckets()))
+
+ def compute_bucket_parameters(self, definition):
+ table = {
+ 'boolean': Histogram.boolean_flag_bucket_parameters,
+ 'flag': Histogram.boolean_flag_bucket_parameters,
+ 'count': Histogram.boolean_flag_bucket_parameters,
+ 'enumerated': Histogram.enumerated_bucket_parameters,
+ 'categorical': Histogram.categorical_bucket_parameters,
+ 'linear': Histogram.linear_bucket_parameters,
+ 'exponential': Histogram.exponential_bucket_parameters,
+ }
+ table_dispatch(self.kind(), table,
+ lambda p: self.set_bucket_parameters(*p(definition)))
+
+ def verify_attributes(self, name, definition):
+ global always_allowed_keys
+ general_keys = always_allowed_keys + ['low', 'high', 'n_buckets']
+
+ table = {
+ 'boolean': always_allowed_keys,
+ 'flag': always_allowed_keys,
+ 'count': always_allowed_keys,
+ 'enumerated': always_allowed_keys + ['n_values'],
+ 'categorical': always_allowed_keys + ['labels'],
+ 'linear': general_keys,
+ 'exponential': general_keys,
+ }
+ # We removed extended_statistics_ok on the client, but the server-side,
+ # where _strict_type_checks==False, has to deal with historical data.
+ if not self._strict_type_checks:
+ table['exponential'].append('extended_statistics_ok')
+
+ table_dispatch(definition['kind'], table,
+ lambda allowed_keys: Histogram.check_keys(name, definition, allowed_keys))
+
+ self.check_name(name)
+ self.check_field_types(name, definition)
+ self.check_whitelistable_fields(name, definition)
+ self.check_expiration(name, definition)
+ self.check_label_values(name, definition)
+
+ def check_name(self, name):
+ if '#' in name:
+ raise ValueError, '"#" not permitted for %s' % (name)
+
+ # Avoid C++ identifier conflicts between histogram enums and label enum names.
+ if name.startswith("LABELS_"):
+ raise ValueError, "Histogram name '%s' can not start with LABELS_" % (name)
+
+ # To make it easier to generate C++ identifiers from this etc., we restrict
+ # the histogram names to a strict pattern.
+ # We skip this on the server to avoid failures with old Histogram.json revisions.
+ if self._strict_type_checks:
+ pattern = '^[a-z][a-z0-9_]+[a-z0-9]$'
+ if not re.match(pattern, name, re.IGNORECASE):
+ raise ValueError, "Histogram name '%s' doesn't confirm to '%s'" % (name, pattern)
+
+ def check_expiration(self, name, definition):
+ field = 'expires_in_version'
+ expiration = definition.get(field)
+
+ if not expiration:
+ return
+
+ # We forbid new probes from using "expires_in_version" : "default" field/value pair.
+ # Old ones that use this are added to the whitelist.
+ if expiration == "default" and name not in whitelists['expiry_default']:
+ raise ValueError, 'New histogram "%s" cannot have "default" %s value.' % (name, field)
+
+ if re.match(r'^[1-9][0-9]*$', expiration):
+ expiration = expiration + ".0a1"
+ elif re.match(r'^[1-9][0-9]*\.0$', expiration):
+ expiration = expiration + "a1"
+
+ definition[field] = expiration
+
+ def check_label_values(self, name, definition):
+ labels = definition.get('labels')
+ if not labels:
+ return
+
+ invalid = filter(lambda l: len(l) > MAX_LABEL_LENGTH, labels)
+ if len(invalid) > 0:
+ raise ValueError, 'Label values for %s exceed length limit of %d: %s' % \
+ (name, MAX_LABEL_LENGTH, ', '.join(invalid))
+
+ if len(labels) > MAX_LABEL_COUNT:
+ raise ValueError, 'Label count for %s exceeds limit of %d' % \
+ (name, MAX_LABEL_COUNT)
+
+ # To make it easier to generate C++ identifiers from this etc., we restrict
+ # the label values to a strict pattern.
+ pattern = '^[a-z][a-z0-9_]+[a-z0-9]$'
+ invalid = filter(lambda l: not re.match(pattern, l, re.IGNORECASE), labels)
+ if len(invalid) > 0:
+ raise ValueError, 'Label values for %s are not matching pattern "%s": %s' % \
+ (name, pattern, ', '.join(invalid))
+
+ # Check for the presence of fields that old histograms are whitelisted for.
+ def check_whitelistable_fields(self, name, definition):
+ # Use counters don't have any mechanism to add the fields checked here,
+ # so skip the check for them.
+ # We also don't need to run any of these checks on the server.
+ if self._is_use_counter or not self._strict_type_checks:
+ return
+
+ # In the pipeline we don't have whitelists available.
+ if whitelists is None:
+ return
+
+ for field in ['alert_emails', 'bug_numbers']:
+ if field not in definition and name not in whitelists[field]:
+ raise KeyError, 'New histogram "%s" must have a %s field.' % (name, field)
+ if field in definition and name in whitelists[field]:
+ msg = 'Should remove histogram "%s" from the whitelist for "%s" in histogram-whitelists.json'
+ raise KeyError, msg % (name, field)
+
+ def check_field_types(self, name, definition):
+ # Define expected types for the histogram properties.
+ type_checked_fields = {
+ "n_buckets": int,
+ "n_values": int,
+ "low": int,
+ "high": int,
+ "keyed": bool,
+ "expires_in_version": basestring,
+ "kind": basestring,
+ "description": basestring,
+ "cpp_guard": basestring,
+ "releaseChannelCollection": basestring,
+ }
+
+ # For list fields we check the items types.
+ type_checked_list_fields = {
+ "bug_numbers": int,
+ "alert_emails": basestring,
+ "labels": basestring,
+ }
+
+ # For the server-side, where _strict_type_checks==False, we want to
+ # skip the stricter type checks for these fields for dealing with
+ # historical data.
+ coerce_fields = ["low", "high", "n_values", "n_buckets"]
+ if not self._strict_type_checks:
+ def try_to_coerce_to_number(v):
+ try:
+ return eval(v, {})
+ except:
+ return v
+ for key in [k for k in coerce_fields if k in definition]:
+ definition[key] = try_to_coerce_to_number(definition[key])
+ # This handles old "keyed":"true" definitions (bug 1271986).
+ if definition.get("keyed", None) == "true":
+ definition["keyed"] = True
+
+ def nice_type_name(t):
+ if t is basestring:
+ return "string"
+ return t.__name__
+
+ for key, key_type in type_checked_fields.iteritems():
+ if not key in definition:
+ continue
+ if not isinstance(definition[key], key_type):
+ raise ValueError, ('value for key "{0}" in Histogram "{1}" '
+ 'should be {2}').format(key, name, nice_type_name(key_type))
+
+ for key, key_type in type_checked_list_fields.iteritems():
+ if not key in definition:
+ continue
+ if not all(isinstance(x, key_type) for x in definition[key]):
+ raise ValueError, ('all values for list "{0}" in Histogram "{1}" '
+ 'should be {2}').format(key, name, nice_type_name(key_type))
+
+ @staticmethod
+ def check_keys(name, definition, allowed_keys):
+ for key in definition.iterkeys():
+ if key not in allowed_keys:
+ raise KeyError, '%s not permitted for %s' % (key, name)
+
+ def set_bucket_parameters(self, low, high, n_buckets):
+ self._low = low
+ self._high = high
+ self._n_buckets = n_buckets
+ if whitelists is not None and self._n_buckets > 100 and type(self._n_buckets) is int:
+ if self._name not in whitelists['n_buckets']:
+ raise KeyError, ('New histogram "%s" is not permitted to have more than 100 buckets. '
+ 'Histograms with large numbers of buckets use disproportionately high amounts of resources. '
+ 'Contact the Telemetry team (e.g. in #telemetry) if you think an exception ought to be made.' % self._name)
+
+ @staticmethod
+ def boolean_flag_bucket_parameters(definition):
+ return (1, 2, 3)
+
+ @staticmethod
+ def linear_bucket_parameters(definition):
+ return (definition.get('low', 1),
+ definition['high'],
+ definition['n_buckets'])
+
+ @staticmethod
+ def enumerated_bucket_parameters(definition):
+ n_values = definition['n_values']
+ return (1, n_values, n_values + 1)
+
+ @staticmethod
+ def categorical_bucket_parameters(definition):
+ n_values = len(definition['labels'])
+ return (1, n_values, n_values + 1)
+
+ @staticmethod
+ def exponential_bucket_parameters(definition):
+ return (definition.get('low', 1),
+ definition['high'],
+ definition['n_buckets'])
+
+# We support generating histograms from multiple different input files, not
+# just Histograms.json. For each file's basename, we have a specific
+# routine to parse that file, and return a dictionary mapping histogram
+# names to histogram parameters.
+def from_Histograms_json(filename):
+ with open(filename, 'r') as f:
+ try:
+ histograms = json.load(f, object_pairs_hook=OrderedDict)
+ except ValueError, e:
+ raise BaseException, "error parsing histograms in %s: %s" % (filename, e.message)
+ return histograms
+
+def from_UseCounters_conf(filename):
+ return usecounters.generate_histograms(filename)
+
+def from_nsDeprecatedOperationList(filename):
+ operation_regex = re.compile('^DEPRECATED_OPERATION\\(([^)]+)\\)')
+ histograms = collections.OrderedDict()
+
+ with open(filename, 'r') as f:
+ for line in f:
+ match = operation_regex.search(line)
+ if not match:
+ continue
+
+ op = match.group(1)
+
+ def add_counter(context):
+ name = 'USE_COUNTER2_DEPRECATED_%s_%s' % (op, context.upper())
+ histograms[name] = {
+ 'expires_in_version': 'never',
+ 'kind': 'boolean',
+ 'description': 'Whether a %s used %s' % (context, op)
+ }
+ add_counter('document')
+ add_counter('page')
+
+ return histograms
+
+FILENAME_PARSERS = {
+ 'Histograms.json': from_Histograms_json,
+ 'nsDeprecatedOperationList.h': from_nsDeprecatedOperationList,
+}
+
+# Similarly to the dance above with buildconfig, usecounters may not be
+# available, so handle that gracefully.
+try:
+ import usecounters
+
+ FILENAME_PARSERS['UseCounters.conf'] = from_UseCounters_conf
+except ImportError:
+ pass
+
+def from_files(filenames):
+ """Return an iterator that provides a sequence of Histograms for
+the histograms defined in filenames.
+ """
+ all_histograms = OrderedDict()
+ for filename in filenames:
+ parser = FILENAME_PARSERS[os.path.basename(filename)]
+ histograms = parser(filename)
+
+ # OrderedDicts are important, because then the iteration order over
+ # the parsed histograms is stable, which makes the insertion into
+ # all_histograms stable, which makes ordering in generated files
+ # stable, which makes builds more deterministic.
+ if not isinstance(histograms, OrderedDict):
+ raise BaseException, "histogram parser didn't provide an OrderedDict"
+
+ for (name, definition) in histograms.iteritems():
+ if all_histograms.has_key(name):
+ raise DefinitionException, "duplicate histogram name %s" % name
+ all_histograms[name] = definition
+
+ # We require that all USE_COUNTER2_* histograms be defined in a contiguous
+ # block.
+ use_counter_indices = filter(lambda x: x[1].startswith("USE_COUNTER2_"),
+ enumerate(all_histograms.iterkeys()));
+ if use_counter_indices:
+ lower_bound = use_counter_indices[0][0]
+ upper_bound = use_counter_indices[-1][0]
+ n_counters = upper_bound - lower_bound + 1
+ if n_counters != len(use_counter_indices):
+ raise DefinitionException, "use counter histograms must be defined in a contiguous block"
+
+ # Check that histograms that were removed from Histograms.json etc. are also removed from the whitelists.
+ if whitelists is not None:
+ all_whitelist_entries = itertools.chain.from_iterable(whitelists.itervalues())
+ orphaned = set(all_whitelist_entries) - set(all_histograms.keys())
+ if len(orphaned) > 0:
+ msg = 'The following entries are orphaned and should be removed from histogram-whitelists.json: %s'
+ raise BaseException, msg % (', '.join(sorted(orphaned)))
+
+ for (name, definition) in all_histograms.iteritems():
+ yield Histogram(name, definition, strict_type_checks=True)
diff --git a/toolkit/components/telemetry/moz.build b/toolkit/components/telemetry/moz.build
new file mode 100644
index 0000000000..118d61b719
--- /dev/null
+++ b/toolkit/components/telemetry/moz.build
@@ -0,0 +1,130 @@
+# -*- 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/.
+
+HAS_MISC_RULE = True
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+DEFINES['MOZ_APP_VERSION'] = '"%s"' % CONFIG['MOZ_APP_VERSION']
+
+LOCAL_INCLUDES += [
+ '/xpcom/build',
+ '/xpcom/threads',
+]
+
+SPHINX_TREES['telemetry'] = 'docs'
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
+XPIDL_SOURCES += [
+ 'nsITelemetry.idl',
+]
+
+XPIDL_MODULE = 'telemetry'
+
+EXPORTS.mozilla += [
+ '!TelemetryEventEnums.h',
+ '!TelemetryHistogramEnums.h',
+ '!TelemetryScalarEnums.h',
+ 'ProcessedStack.h',
+ 'Telemetry.h',
+ 'TelemetryComms.h',
+ 'ThreadHangStats.h',
+]
+
+SOURCES += [
+ 'Telemetry.cpp',
+ 'TelemetryCommon.cpp',
+ 'TelemetryEvent.cpp',
+ 'TelemetryHistogram.cpp',
+ 'TelemetryScalar.cpp',
+ 'WebrtcTelemetry.cpp',
+]
+
+EXTRA_COMPONENTS += [
+ 'TelemetryStartup.js',
+ 'TelemetryStartup.manifest'
+]
+
+EXTRA_JS_MODULES += [
+ 'GCTelemetry.jsm',
+ 'TelemetryArchive.jsm',
+ 'TelemetryController.jsm',
+ 'TelemetryEnvironment.jsm',
+ 'TelemetryLog.jsm',
+ 'TelemetryReportingPolicy.jsm',
+ 'TelemetrySend.jsm',
+ 'TelemetrySession.jsm',
+ 'TelemetryStopwatch.jsm',
+ 'TelemetryStorage.jsm',
+ 'TelemetryTimestamps.jsm',
+ 'TelemetryUtils.jsm',
+ 'ThirdPartyCookieProbe.jsm',
+ 'UITelemetry.jsm',
+]
+
+TESTING_JS_MODULES += [
+ 'tests/unit/TelemetryArchiveTesting.jsm',
+]
+
+GENERATED_FILES = [
+ 'TelemetryEventData.h',
+ 'TelemetryEventEnums.h',
+ 'TelemetryHistogramData.inc',
+ 'TelemetryHistogramEnums.h',
+ 'TelemetryScalarData.h',
+ 'TelemetryScalarEnums.h',
+]
+
+# Generate histogram files.
+histogram_files = [
+ 'Histograms.json',
+ '/dom/base/UseCounters.conf',
+ '/dom/base/nsDeprecatedOperationList.h',
+]
+
+data = GENERATED_FILES['TelemetryHistogramData.inc']
+data.script = 'gen-histogram-data.py'
+data.inputs = histogram_files
+
+enums = GENERATED_FILES['TelemetryHistogramEnums.h']
+enums.script = 'gen-histogram-enum.py'
+enums.inputs = histogram_files
+
+# Generate scalar files.
+scalar_files = [
+ 'Scalars.yaml',
+]
+
+scalar_data = GENERATED_FILES['TelemetryScalarData.h']
+scalar_data.script = 'gen-scalar-data.py'
+scalar_data.inputs = scalar_files
+
+scalar_enums = GENERATED_FILES['TelemetryScalarEnums.h']
+scalar_enums.script = 'gen-scalar-enum.py'
+scalar_enums.inputs = scalar_files
+
+# Generate event files.
+event_files = [
+ 'Events.yaml',
+]
+
+event_data = GENERATED_FILES['TelemetryEventData.h']
+event_data.script = 'gen-event-data.py'
+event_data.inputs = event_files
+
+event_enums = GENERATED_FILES['TelemetryEventEnums.h']
+event_enums.script = 'gen-event-enum.py'
+event_enums.inputs = event_files
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Telemetry')
diff --git a/toolkit/components/telemetry/nsITelemetry.idl b/toolkit/components/telemetry/nsITelemetry.idl
new file mode 100644
index 0000000000..3b74b2d1ba
--- /dev/null
+++ b/toolkit/components/telemetry/nsITelemetry.idl
@@ -0,0 +1,469 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 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 "nsISupports.idl"
+#include "nsIFile.idl"
+
+[scriptable,function, uuid(3d3b9075-5549-4244-9c08-b64fefa1dd60)]
+interface nsIFetchTelemetryDataCallback : nsISupports
+{
+ void complete();
+};
+
+[scriptable, uuid(273d2dd0-6c63-475a-b864-cb65160a1909)]
+interface nsITelemetry : nsISupports
+{
+ /**
+ * Histogram types:
+ * HISTOGRAM_EXPONENTIAL - buckets increase exponentially
+ * HISTOGRAM_LINEAR - buckets increase linearly
+ * HISTOGRAM_BOOLEAN - For storing 0/1 values
+ * HISTOGRAM_FLAG - For storing a single value; its count is always == 1.
+ * HISTOGRAM_COUNT - For storing counter values without bucketing.
+ * HISTOGRAM_CATEGORICAL - For storing enumerated values by label.
+ */
+ const unsigned long HISTOGRAM_EXPONENTIAL = 0;
+ const unsigned long HISTOGRAM_LINEAR = 1;
+ const unsigned long HISTOGRAM_BOOLEAN = 2;
+ const unsigned long HISTOGRAM_FLAG = 3;
+ const unsigned long HISTOGRAM_COUNT = 4;
+ const unsigned long HISTOGRAM_CATEGORICAL = 5;
+
+ /**
+ * Scalar types:
+ * SCALAR_COUNT - for storing a numeric value
+ * SCALAR_STRING - for storing a string value
+ * SCALAR_BOOLEAN - for storing a boolean value
+ */
+ const unsigned long SCALAR_COUNT = 0;
+ const unsigned long SCALAR_STRING = 1;
+ const unsigned long SCALAR_BOOLEAN = 2;
+
+ /**
+ * Dataset types:
+ * DATASET_RELEASE_CHANNEL_OPTOUT - the basic dataset that is on-by-default on all channels
+ * DATASET_RELEASE_CHANNEL_OPTIN - the extended dataset that is opt-in on release,
+ * opt-out on pre-release channels.
+ */
+ const unsigned long DATASET_RELEASE_CHANNEL_OPTOUT = 0;
+ const unsigned long DATASET_RELEASE_CHANNEL_OPTIN = 1;
+
+
+ /**
+ * An object containing a snapshot from all of the currently registered histograms.
+ * { name1: {data1}, name2:{data2}...}
+ * where data is consists of the following properties:
+ * min - Minimal bucket size
+ * max - Maximum bucket size
+ * histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR, HISTOGRAM_BOOLEAN
+ * or HISTOGRAM_COUNT
+ * counts - array representing contents of the buckets in the histogram
+ * ranges - an array with calculated bucket sizes
+ * sum - sum of the bucket contents
+ */
+ [implicit_jscontext]
+ readonly attribute jsval histogramSnapshots;
+
+ /**
+ * Get a snapshot of the internally duplicated subsession histograms.
+ * @param clear Whether to clear out the subsession histograms after snapshotting.
+ * @return An object as histogramSnapshots, except this contains the internally duplicated histograms for subsession telemetry.
+ */
+ [implicit_jscontext]
+ jsval snapshotSubsessionHistograms([optional] in boolean clear);
+
+ /**
+ * The amount of time, in milliseconds, that the last session took
+ * to shutdown. Reads as 0 to indicate failure.
+ */
+ readonly attribute uint32_t lastShutdownDuration;
+
+ /**
+ * The number of failed profile lock attempts that have occurred prior to
+ * successfully locking the profile
+ */
+ readonly attribute uint32_t failedProfileLockCount;
+
+ /*
+ * An object containing information about slow SQL statements.
+ *
+ * {
+ * mainThread: { "sqlString1": [<hit count>, <total time>], "sqlString2": [...], ... },
+ * otherThreads: { "sqlString3": [<hit count>, <total time>], "sqlString4": [...], ... }
+ * }
+ *
+ * where:
+ * mainThread: Slow statements that executed on the main thread
+ * otherThreads: Slow statements that executed on a non-main thread
+ * sqlString - String of the offending statement (see note)
+ * hit count - The number of times this statement required longer than the threshold time to execute
+ * total time - The sum of all execution times above the threshold time for this statement
+ *
+ * Note that dynamic SQL strings and SQL strings executed against addon DBs could contain private information.
+ * This property represents such SQL as aggregate database-level stats and the sqlString contains the database
+ * filename instead.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval slowSQL;
+
+ /*
+ * See slowSQL above.
+ *
+ * An object containing full strings of every slow SQL statement if toolkit.telemetry.debugSlowSql = true
+ * The returned SQL strings may contain private information and should not be reported to Telemetry.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval debugSlowSQL;
+
+ /*
+ * An object containing information about Webrtc related stats. For now it
+ * only contains local and remote ICE candidates avaiable when a Webrtc
+ * PeerConnection gets terminated.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval webrtcStats;
+
+ /**
+ * A number representing the highest number of concurrent threads
+ * reached during this session.
+ */
+ readonly attribute uint32_t maximalNumberOfConcurrentThreads;
+
+ /*
+ * An array of chrome hang reports. Each element is a hang report represented
+ * as an object containing the hang duration, call stack PCs and information
+ * about modules in memory.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval chromeHangs;
+
+ /*
+ * An array of thread hang stats,
+ * [<thread>, <thread>, ...]
+ * <thread> represents a single thread,
+ * {"name": "<name>",
+ * "activity": <time>,
+ * "hangs": [<hang>, <hang>, ...]}
+ * <time> represents a histogram of time intervals in milliseconds,
+ * with the same format as histogramSnapshots
+ * <hang> represents a particular hang,
+ * {"stack": <stack>, "nativeStack": <stack>, "histogram": <time>}
+ * <stack> represents the hang's stack,
+ * ["<frame_0>", "<frame_1>", ...]
+ */
+ [implicit_jscontext]
+ readonly attribute jsval threadHangStats;
+
+ /*
+ * An object with two fields: memoryMap and stacks.
+ * * memoryMap is a list of loaded libraries.
+ * * stacks is a list of stacks. Each stack is a list of pairs of the form
+ * [moduleIndex, offset]. The moduleIndex is an index into the memoryMap and
+ * offset is an offset in the library at memoryMap[moduleIndex].
+ * This format is used to make it easier to send the stacks to the
+ * symbolication server.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval lateWrites;
+
+ /**
+ * Returns an array whose values are the names of histograms defined
+ * in Histograms.json.
+ *
+ * @param dataset - DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN
+ */
+ void registeredHistograms(in uint32_t dataset,
+ out uint32_t count,
+ [retval, array, size_is(count)] out string histograms);
+
+ /**
+ * Create and return a histogram registered in TelemetryHistograms.h.
+ *
+ * @param id - unique identifier from TelemetryHistograms.h
+ * The returned object has the following functions:
+ * add(int) - Adds an int value to the appropriate bucket
+ * snapshot() - Returns a snapshot of the histogram with the same data fields as in histogramSnapshots()
+ * clear() - Zeros out the histogram's buckets and sum
+ * dataset() - identifies what dataset this is in: DATASET_RELEASE_CHANNEL_OPTOUT or ...OPTIN
+ */
+ [implicit_jscontext]
+ jsval getHistogramById(in ACString id);
+
+ /*
+ * An object containing a snapshot from all of the currently registered keyed histograms.
+ * { name1: {histogramData1}, name2:{histogramData2}...}
+ * where the histogramData is as described in histogramSnapshots.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval keyedHistogramSnapshots;
+
+ /**
+ * Returns an array whose values are the names of histograms defined
+ * in Histograms.json.
+ *
+ * @param dataset - DATASET_RELEASE_CHANNEL_OPTOUT or ...OPTIN
+ */
+ void registeredKeyedHistograms(in uint32_t dataset, out uint32_t count,
+ [retval, array, size_is(count)] out string histograms);
+
+ /**
+ * Create and return a histogram registered in TelemetryHistograms.h.
+ *
+ * @param id - unique identifier from TelemetryHistograms.h
+ * The returned object has the following functions:
+ * add(string key, [optional] int) - Add an int value to the histogram for that key. If no histogram for that key exists yet, it is created.
+ * snapshot([optional] string key) - If key is provided, returns a snapshot for the histogram with that key or null. If key is not provided, returns the snapshots of all the registered keys in the form {key1: snapshot1, key2: snapshot2, ...}.
+ * keys() - Returns an array with the string keys of the currently registered histograms
+ * clear() - Clears the registered histograms from this.
+ * dataset() - identifies what dataset this is in: DATASET_RELEASE_CHANNEL_OPTOUT or ...OPTIN
+ */
+ [implicit_jscontext]
+ jsval getKeyedHistogramById(in ACString id);
+
+ /**
+ * A flag indicating if Telemetry can record base data (FHR data). This is true if the
+ * FHR data reporting service or self-support are enabled.
+ *
+ * In the unlikely event that adding a new base probe is needed, please check the data
+ * collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection and talk to the
+ * Telemetry team.
+ */
+ attribute boolean canRecordBase;
+
+ /**
+ * A flag indicating if Telemetry is allowed to record extended data. Returns false if
+ * the user hasn't opted into "extended Telemetry" on the Release channel, when the
+ * user has explicitly opted out of Telemetry on Nightly/Aurora/Beta or if manually
+ * set to false during tests.
+ *
+ * Set this to false in tests to disable gathering of extended telemetry statistics.
+ */
+ attribute boolean canRecordExtended;
+
+ /**
+ * A flag indicating whether Telemetry can submit official results (for base or extended
+ * data). This is true on official, non-debug builds with built in support for Mozilla
+ * Telemetry reporting.
+ */
+ readonly attribute boolean isOfficialTelemetry;
+
+ /** Addon telemetry hooks */
+
+ /**
+ * Register a histogram for an addon. Throws an error if the
+ * histogram name has been registered previously.
+ *
+ * @param addon_id - Unique ID of the addon
+ * @param name - Unique histogram name
+ * @param histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR,
+ * HISTOGRAM_BOOLEAN or HISTOGRAM_COUNT
+ * @param min - Minimal bucket size
+ * @param max - Maximum bucket size
+ * @param bucket_count - number of buckets in the histogram
+ */
+ [optional_argc]
+ void registerAddonHistogram(in ACString addon_id, in ACString name,
+ in unsigned long histogram_type,
+ [optional] in uint32_t min,
+ [optional] in uint32_t max,
+ [optional] in uint32_t bucket_count);
+
+ /**
+ * Return a histogram previously registered via
+ * registerAddonHistogram. Throws an error if the id/name combo has
+ * not been registered via registerAddonHistogram.
+ *
+ * @param addon_id - Unique ID of the addon
+ * @param name - Registered histogram name
+ *
+ */
+ [implicit_jscontext]
+ jsval getAddonHistogram(in ACString addon_id, in ACString name);
+
+ /**
+ * Delete all histograms associated with the given addon id.
+ *
+ * @param addon_id - Unique ID of the addon
+ */
+ void unregisterAddonHistograms(in ACString addon_id);
+
+ /**
+ * Enable/disable recording for this histogram at runtime.
+ * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[].
+ * Name must be a valid Histogram identifier, otherwise an assertion will be triggered.
+ *
+ * @param id - unique identifier from histograms.json
+ * @param enabled - whether or not to enable recording from now on.
+ */
+ void setHistogramRecordingEnabled(in ACString id, in boolean enabled);
+
+ /**
+ * An object containing a snapshot from all of the currently
+ * registered addon histograms.
+ * { addon-id1 : data1, ... }
+ *
+ * where data is an object whose properties are the names of the
+ * addon's histograms and whose corresponding values are as in
+ * histogramSnapshots.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval addonHistogramSnapshots;
+
+ /**
+ * Read data from the previous run. After the callback is called, the last
+ * shutdown time is available in lastShutdownDuration and any late
+ * writes in lateWrites.
+ */
+ void asyncFetchTelemetryData(in nsIFetchTelemetryDataCallback aCallback);
+
+ /**
+ * Get statistics of file IO reports, null, if not recorded.
+ *
+ * The statistics are returned as an object whose propoerties are the names
+ * of the files that have been accessed and whose corresponding values are
+ * arrays of size three, representing startup, normal, and shutdown stages.
+ * Each stage's entry is either null or an array with the layout
+ * [total_time, #creates, #reads, #writes, #fsyncs, #stats]
+ */
+ [implicit_jscontext]
+ readonly attribute jsval fileIOReports;
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes).
+ * @throws NS_ERROR_NOT_AVAILABLE if TimeStamp doesn't have the data.
+ */
+ double msSinceProcessStart();
+
+ /**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void scalarAdd(in ACString aName, in jsval aValue);
+
+ /**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+ * type of the scalar, the function will fail. For scalar string types, the this
+ * is truncated to 50 characters.
+ */
+ [implicit_jscontext]
+ void scalarSet(in ACString aName, in jsval aValue);
+
+ /**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void scalarSetMaximum(in ACString aName, in jsval aValue);
+
+ /**
+ * Serializes the scalars from the given dataset to a JSON-style object and resets them.
+ * The returned structure looks like:
+ * { "group1.probe": 1, "group1.other_probe": false, ... }
+ *
+ * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+ * @param [aClear=false] Whether to clear out the scalars after snapshotting.
+ */
+ [implicit_jscontext, optional_argc]
+ jsval snapshotScalars(in uint32_t aDataset, [optional] in boolean aClear);
+
+ /**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void keyedScalarAdd(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+ * type of the scalar, the function will fail.
+ */
+ [implicit_jscontext]
+ void keyedScalarSet(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void keyedScalarSetMaximum(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Serializes the keyed scalars from the given dataset to a JSON-style object and
+ * resets them.
+ * The returned structure looks like:
+ * { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... }
+ *
+ * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+ * @param [aClear=false] Whether to clear out the scalars after snapshotting.
+ */
+ [implicit_jscontext, optional_argc]
+ jsval snapshotKeyedScalars(in uint32_t aDataset, [optional] in boolean aClear);
+
+ /**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+ void clearScalars();
+
+ /**
+ * Immediately sends any Telemetry batched on this process to the parent
+ * process. This is intended only to be used on process shutdown.
+ */
+ void flushBatchedChildTelemetry();
+
+ /**
+ * Record an event in Telemetry.
+ *
+ * @param aCategory The category name.
+ * @param aMethod The method name.
+ * @param aMethod The object name.
+ * @param aValue An optional string value to record.
+ * @param aExtra An optional object of the form (string -> string).
+ * It should only contain registered extra keys.
+ *
+ * @throws NS_ERROR_INVALID_ARG When trying to record an unknown event.
+ */
+ [implicit_jscontext, optional_argc]
+ void recordEvent(in ACString aCategory, in ACString aMethod, in ACString aObject, [optional] in jsval aValue, [optional] in jsval extra);
+
+ /**
+ * Serializes the recorded events to a JSON-appropriate array and optionally resets them.
+ * The returned structure looks like this:
+ * [
+ * // [timestamp, category, method, object, stringValue, extraValues]
+ * [43245, "category1", "method1", "object1", "string value", null],
+ * [43258, "category1", "method2", "object1", null, {"key1": "string value"}],
+ * ...
+ * ]
+ *
+ * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+ * @param [aClear=false] Whether to clear out the scalars after snapshotting.
+ */
+ [implicit_jscontext, optional_argc]
+ jsval snapshotBuiltinEvents(in uint32_t aDataset, [optional] in boolean aClear);
+
+ /**
+ * Resets all the stored events. This is intended to be only used in tests.
+ */
+ void clearEvents();
+};
diff --git a/toolkit/components/telemetry/parse_events.py b/toolkit/components/telemetry/parse_events.py
new file mode 100644
index 0000000000..b31e9bc04b
--- /dev/null
+++ b/toolkit/components/telemetry/parse_events.py
@@ -0,0 +1,271 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 re
+import yaml
+import itertools
+import datetime
+import string
+from shared_telemetry_utils import add_expiration_postfix
+
+MAX_CATEGORY_NAME_LENGTH = 30
+MAX_METHOD_NAME_LENGTH = 20
+MAX_OBJECT_NAME_LENGTH = 20
+MAX_EXTRA_KEYS_COUNT = 10
+MAX_EXTRA_KEY_NAME_LENGTH = 15
+
+IDENTIFIER_PATTERN = r'^[a-zA-Z][a-zA-Z0-9_.]+[a-zA-Z0-9]$'
+DATE_PATTERN = r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
+
+def nice_type_name(t):
+ if isinstance(t, basestring):
+ return "string"
+ return t.__name__
+
+def convert_to_cpp_identifier(s, sep):
+ return string.capwords(s, sep).replace(sep, "")
+
+class OneOf:
+ """This is a placeholder type for the TypeChecker below.
+ It signals that the checked value should match one of the following arguments
+ passed to the TypeChecker constructor.
+ """
+ pass
+
+class TypeChecker:
+ """This implements a convenience type TypeChecker to make the validation code more readable."""
+ def __init__(self, kind, *args):
+ """This takes 1-3 arguments, specifying the value type to check for.
+ It supports:
+ - atomic values, e.g.: TypeChecker(int)
+ - list values, e.g.: TypeChecker(list, basestring)
+ - dict values, e.g.: TypeChecker(dict, basestring, int)
+ - atomic values that can have different types, e.g.: TypeChecker(OneOf, int, date)"""
+ self._kind = kind
+ self._args = args
+
+ def check(self, key, value):
+ # Check fields that can be one of two different types.
+ if self._kind is OneOf:
+ if not isinstance(value, self._args[0]) and not isinstance(value, self._args[1]):
+ raise ValueError, "failed type check for %s - expected %s or %s, got %s" %\
+ (key,
+ nice_type_name(self._args[0]),
+ nice_type_name(self._args[1]),
+ nice_type_name(type(value)))
+ return
+
+ # Check basic type of value.
+ if not isinstance(value, self._kind):
+ raise ValueError, "failed type check for %s - expected %s, got %s" %\
+ (key,
+ nice_type_name(self._kind),
+ nice_type_name(type(value)))
+
+ # Check types of values in lists.
+ if self._kind is list:
+ if len(value) < 1:
+ raise ValueError, "failed check for %s - list should not be empty" % key
+ for x in value:
+ if not isinstance(x, self._args[0]):
+ raise ValueError, "failed type check for %s - expected list value type %s, got %s" %\
+ (key,
+ nice_type_name(self._args[0]),
+ nice_type_name(type(x)))
+ # Check types of keys and values in dictionaries.
+ elif self._kind is dict:
+ if len(value.keys()) < 1:
+ raise ValueError, "failed check for %s - dict should not be empty" % key
+ for x in value.iterkeys():
+ if not isinstance(x, self._args[0]):
+ raise ValueError, "failed dict type check for %s - expected key type %s, got %s" %\
+ (key,
+ nice_type_name(self._args[0]),
+ nice_type_name(type(x)))
+ for k,v in value.iteritems():
+ if not isinstance(x, self._args[1]):
+ raise ValueError, "failed dict type check for %s - expected value type %s for key %s, got %s" %\
+ (key,
+ nice_type_name(self._args[1]),
+ k,
+ nice_type_name(type(x)))
+
+def type_check_event_fields(category, definition):
+ """Perform a type/schema check on the event definition."""
+ REQUIRED_FIELDS = {
+ 'methods': TypeChecker(list, basestring),
+ 'objects': TypeChecker(list, basestring),
+ 'bug_numbers': TypeChecker(list, int),
+ 'notification_emails': TypeChecker(list, basestring),
+ 'description': TypeChecker(basestring),
+ }
+ OPTIONAL_FIELDS = {
+ 'release_channel_collection': TypeChecker(basestring),
+ 'expiry_date': TypeChecker(OneOf, basestring, datetime.date),
+ 'expiry_version': TypeChecker(basestring),
+ 'extra_keys': TypeChecker(dict, basestring, basestring),
+ }
+ ALL_FIELDS = REQUIRED_FIELDS.copy()
+ ALL_FIELDS.update(OPTIONAL_FIELDS)
+
+ # Check that all the required fields are available.
+ missing_fields = [f for f in REQUIRED_FIELDS.keys() if f not in definition]
+ if len(missing_fields) > 0:
+ raise KeyError(category + ' - missing required fields: ' + ', '.join(missing_fields))
+
+ # Is there any unknown field?
+ unknown_fields = [f for f in definition.keys() if f not in ALL_FIELDS]
+ if len(unknown_fields) > 0:
+ raise KeyError(category + ' - unknown fields: ' + ', '.join(unknown_fields))
+
+ # Type-check fields.
+ for k,v in definition.iteritems():
+ ALL_FIELDS[k].check(k, v)
+
+def string_check(category, field_name, value, min_length, max_length, regex=None):
+ # Length check.
+ if len(value) > max_length:
+ raise ValueError("Value '%s' for %s in %s exceeds maximum length of %d" %\
+ (value, field_name, category, max_length))
+ # Regex check.
+ if regex and not re.match(regex, value):
+ raise ValueError, 'String value for %s in %s is not matching pattern "%s": %s' % \
+ (field_name, category, regex, value)
+
+class EventData:
+ """A class representing one event."""
+
+ def __init__(self, category, definition):
+ type_check_event_fields(category, definition)
+
+ string_check(category, 'methods', definition.get('methods')[0], 1, MAX_METHOD_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+ string_check(category, 'objects', definition.get('objects')[0], 1, MAX_OBJECT_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+
+ # Check release_channel_collection
+ rcc_key = 'release_channel_collection'
+ rcc = definition.get(rcc_key, 'opt-in')
+ allowed_rcc = ["opt-in", "opt-out"]
+ if not rcc in allowed_rcc:
+ raise ValueError, "Value for %s in %s should be one of: %s" %\
+ (rcc_key, category, ", ".join(allowed_rcc))
+
+ # Check extra_keys.
+ extra_keys = definition.get('extra_keys', {})
+ if len(extra_keys.keys()) > MAX_EXTRA_KEYS_COUNT:
+ raise ValueError, "Number of extra_keys in %s exceeds limit %d" %\
+ (category, MAX_EXTRA_KEYS_COUNT)
+ for key in extra_keys.iterkeys():
+ string_check(category, 'extra_keys', key, 1, MAX_EXTRA_KEY_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+
+ # Check expiry.
+ if not 'expiry_version' in definition and not 'expiry_date' in definition:
+ raise KeyError, "Event in %s is missing an expiration - either expiry_version or expiry_date is required" %\
+ (category)
+ expiry_date = definition.get('expiry_date')
+ if expiry_date and isinstance(expiry_date, basestring) and expiry_date != 'never':
+ if not re.match(DATE_PATTERN, expiry_date):
+ raise ValueError, "Event in %s has invalid expiry_date, it should be either 'never' or match this format: %s" %\
+ (category, DATE_PATTERN)
+ # Parse into date.
+ definition['expiry_date'] = datetime.datetime.strptime(expiry_date, '%Y-%m-%d')
+
+ # Finish setup.
+ self._category = category
+ self._definition = definition
+ definition['expiry_version'] = add_expiration_postfix(definition.get('expiry_version', 'never'))
+
+ @property
+ def category(self):
+ return self._category
+
+ @property
+ def category_cpp(self):
+ # Transform e.g. category.example into CategoryExample.
+ return convert_to_cpp_identifier(self._category, ".")
+
+ @property
+ def methods(self):
+ return self._definition.get('methods')
+
+ @property
+ def objects(self):
+ return self._definition.get('objects')
+
+ @property
+ def expiry_version(self):
+ return self._definition.get('expiry_version')
+
+ @property
+ def expiry_day(self):
+ date = self._definition.get('expiry_date')
+ if not date:
+ return 0
+ if isinstance(date, basestring) and date == 'never':
+ return 0
+
+ # Convert date to days since UNIX epoch.
+ epoch = datetime.date(1970, 1, 1)
+ days = (date - epoch).total_seconds() / (24 * 60 * 60)
+ return round(days)
+
+ @property
+ def cpp_guard(self):
+ return self._definition.get('cpp_guard')
+
+ @property
+ def enum_labels(self):
+ def enum(method_name, object_name):
+ m = convert_to_cpp_identifier(method_name, "_")
+ o = convert_to_cpp_identifier(object_name, "_")
+ return m + '_' + o
+ combinations = itertools.product(self.methods, self.objects)
+ return [enum(t[0], t[1]) for t in combinations]
+
+ @property
+ def dataset(self):
+ """Get the nsITelemetry constant equivalent for release_channel_collection.
+ """
+ rcc = self._definition.get('release_channel_collection', 'opt-in')
+ if rcc == 'opt-out':
+ return 'nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT'
+ else:
+ return 'nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN'
+
+ @property
+ def extra_keys(self):
+ return self._definition.get('extra_keys', {}).keys()
+
+def load_events(filename):
+ """Parses a YAML file containing the event definitions.
+
+ :param filename: the YAML file containing the event definitions.
+ :raises Exception: if the event file cannot be opened or parsed.
+ """
+
+ # Parse the event definitions from the YAML file.
+ events = None
+ try:
+ with open(filename, 'r') as f:
+ events = yaml.safe_load(f)
+ except IOError, e:
+ raise Exception('Error opening ' + filename + ': ' + e.message)
+ except ValueError, e:
+ raise Exception('Error parsing events in ' + filename + ': ' + e.message)
+
+ event_list = []
+
+ # Events are defined in a fixed two-level hierarchy within the definition file.
+ # The first level contains the category (group name), while the second level contains the
+ # event definitions (e.g. "category.name: [<event definition>, ...], ...").
+ for category_name,category in events.iteritems():
+ string_check('', 'category', category_name, 1, MAX_CATEGORY_NAME_LENGTH, regex=IDENTIFIER_PATTERN)
+
+ # Make sure that the category has at least one entry in it.
+ if not category or len(category) == 0:
+ raise ValueError(category_name + ' must contain at least one entry')
+
+ for entry in category:
+ event_list.append(EventData(category_name, entry))
+
+ return event_list
diff --git a/toolkit/components/telemetry/parse_scalars.py b/toolkit/components/telemetry/parse_scalars.py
new file mode 100644
index 0000000000..a560a30139
--- /dev/null
+++ b/toolkit/components/telemetry/parse_scalars.py
@@ -0,0 +1,262 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 re
+import yaml
+from shared_telemetry_utils import add_expiration_postfix
+
+# The map of containing the allowed scalar types and their mapping to
+# nsITelemetry::SCALAR_* type constants.
+SCALAR_TYPES_MAP = {
+ 'uint': 'nsITelemetry::SCALAR_COUNT',
+ 'string': 'nsITelemetry::SCALAR_STRING',
+ 'boolean': 'nsITelemetry::SCALAR_BOOLEAN'
+}
+
+class ScalarType:
+ """A class for representing a scalar definition."""
+
+ def __init__(self, group_name, probe_name, definition):
+ # Validate and set the name, so we don't need to pass it to the other
+ # validation functions.
+ self.validate_names(group_name, probe_name)
+ self._name = probe_name
+ self._group_name = group_name
+
+ # Validating the scalar definition.
+ self.validate_types(definition)
+ self.validate_values(definition)
+
+ # Everything is ok, set the rest of the data.
+ self._definition = definition
+ definition['expires'] = add_expiration_postfix(definition['expires'])
+
+ def validate_names(self, group_name, probe_name):
+ """Validate the group and probe name:
+ - Group name must be alpha-numeric + '.', no leading/trailing digit or '.'.
+ - Probe name must be alpha-numeric + '_', no leading/trailing digit or '_'.
+
+ :param group_name: the name of the group the probe is in.
+ :param probe_name: the name of the scalar probe.
+ :raises ValueError: if the length of the names exceeds the limit or they don't
+ conform our name specification.
+ """
+
+ # Enforce a maximum length on group and probe names.
+ MAX_NAME_LENGTH = 40
+ for n in [group_name, probe_name]:
+ if len(n) > MAX_NAME_LENGTH:
+ raise ValueError("Name '{}' exceeds maximum name length of {} characters."\
+ .format(n, MAX_NAME_LENGTH))
+
+ def check_name(name, error_msg_prefix, allowed_char_regexp):
+ # Check if we only have the allowed characters.
+ chars_regxp = r'^[a-zA-Z0-9' + allowed_char_regexp + r']+$'
+ if not re.search(chars_regxp, name):
+ raise ValueError(error_msg_prefix + " name must be alpha-numeric. Got: '{}'".format(name))
+
+ # Don't allow leading/trailing digits, '.' or '_'.
+ if re.search(r'(^[\d\._])|([\d\._])$', name):
+ raise ValueError(error_msg_prefix +
+ " name must not have a leading/trailing digit, a dot or underscore. Got: '{}'"\
+ .format(name))
+
+ check_name(group_name, 'Group', r'\.')
+ check_name(probe_name, 'Probe', r'_')
+
+ def validate_types(self, definition):
+ """This function performs some basic sanity checks on the scalar definition:
+ - Checks that all the required fields are available.
+ - Checks that all the fields have the expected types.
+
+ :param definition: the dictionary containing the scalar properties.
+ :raises TypeError: if a scalar definition field is of the wrong type.
+ :raise KeyError: if a required field is missing or unknown fields are present.
+ """
+
+ # The required and optional fields in a scalar type definition.
+ REQUIRED_FIELDS = {
+ 'bug_numbers': list, # This contains ints. See LIST_FIELDS_CONTENT.
+ 'description': basestring,
+ 'expires': basestring,
+ 'kind': basestring,
+ 'notification_emails': list # This contains strings. See LIST_FIELDS_CONTENT.
+ }
+
+ OPTIONAL_FIELDS = {
+ 'cpp_guard': basestring,
+ 'release_channel_collection': basestring,
+ 'keyed': bool
+ }
+
+ # The types for the data within the fields that hold lists.
+ LIST_FIELDS_CONTENT = {
+ 'bug_numbers': int,
+ 'notification_emails': basestring
+ }
+
+ # Concatenate the required and optional field definitions.
+ ALL_FIELDS = REQUIRED_FIELDS.copy()
+ ALL_FIELDS.update(OPTIONAL_FIELDS)
+
+ # Checks that all the required fields are available.
+ missing_fields = [f for f in REQUIRED_FIELDS.keys() if f not in definition]
+ if len(missing_fields) > 0:
+ raise KeyError(self._name + ' - missing required fields: ' + ', '.join(missing_fields))
+
+ # Do we have any unknown field?
+ unknown_fields = [f for f in definition.keys() if f not in ALL_FIELDS]
+ if len(unknown_fields) > 0:
+ raise KeyError(self._name + ' - unknown fields: ' + ', '.join(unknown_fields))
+
+ # Checks the type for all the fields.
+ wrong_type_names = ['{} must be {}'.format(f, ALL_FIELDS[f].__name__) \
+ for f in definition.keys() if not isinstance(definition[f], ALL_FIELDS[f])]
+ if len(wrong_type_names) > 0:
+ raise TypeError(self._name + ' - ' + ', '.join(wrong_type_names))
+
+ # Check that the lists are not empty and that data in the lists
+ # have the correct types.
+ list_fields = [f for f in definition if isinstance(definition[f], list)]
+ for field in list_fields:
+ # Check for empty lists.
+ if len(definition[field]) == 0:
+ raise TypeError("Field '{}' for probe '{}' must not be empty."
+ .format(field, self._name))
+ # Check the type of the list content.
+ broken_types =\
+ [not isinstance(v, LIST_FIELDS_CONTENT[field]) for v in definition[field]]
+ if any(broken_types):
+ raise TypeError("Field '{}' for probe '{}' must only contain values of type {}"
+ .format(field, self._name, LIST_FIELDS_CONTENT[field].__name__))
+
+ def validate_values(self, definition):
+ """This function checks that the fields have the correct values.
+
+ :param definition: the dictionary containing the scalar properties.
+ :raises ValueError: if a scalar definition field contains an unexpected value.
+ """
+
+ # Validate the scalar kind.
+ scalar_kind = definition.get('kind')
+ if scalar_kind not in SCALAR_TYPES_MAP.keys():
+ raise ValueError(self._name + ' - unknown scalar kind: ' + scalar_kind)
+
+ # Validate the collection policy.
+ collection_policy = definition.get('release_channel_collection', None)
+ if collection_policy and collection_policy not in ['opt-in', 'opt-out']:
+ raise ValueError(self._name + ' - unknown collection policy: ' + collection_policy)
+
+ # Validate the cpp_guard.
+ cpp_guard = definition.get('cpp_guard')
+ if cpp_guard and re.match(r'\W', cpp_guard):
+ raise ValueError(self._name + ' - invalid cpp_guard: ' + cpp_guard)
+
+ @property
+ def name(self):
+ """Get the scalar name"""
+ return self._name
+
+ @property
+ def label(self):
+ """Get the scalar label generated from the scalar and group names."""
+ return self._group_name + '.' + self._name
+
+ @property
+ def enum_label(self):
+ """Get the enum label generated from the scalar and group names. This is used to
+ generate the enum tables."""
+
+ # The scalar name can contain informations about its hierarchy (e.g. 'a.b.scalar').
+ # We can't have dots in C++ enums, replace them with an underscore. Also, make the
+ # label upper case for consistency with the histogram enums.
+ return self.label.replace('.', '_').upper()
+
+ @property
+ def bug_numbers(self):
+ """Get the list of related bug numbers"""
+ return self._definition['bug_numbers']
+
+ @property
+ def description(self):
+ """Get the scalar description"""
+ return self._definition['description']
+
+ @property
+ def expires(self):
+ """Get the scalar expiration"""
+ return self._definition['expires']
+
+ @property
+ def kind(self):
+ """Get the scalar kind"""
+ return self._definition['kind']
+
+ @property
+ def keyed(self):
+ """Boolean indicating whether this is a keyed scalar"""
+ return self._definition.get('keyed', False)
+
+ @property
+ def nsITelemetry_kind(self):
+ """Get the scalar kind constant defined in nsITelemetry"""
+ return SCALAR_TYPES_MAP.get(self.kind)
+
+ @property
+ def notification_emails(self):
+ """Get the list of notification emails"""
+ return self._definition['notification_emails']
+
+ @property
+ def dataset(self):
+ """Get the nsITelemetry constant equivalent to the chose release channel collection
+ policy for the scalar.
+ """
+ # The collection policy is optional, but we still define a default
+ # behaviour for it.
+ release_channel_collection = \
+ self._definition.get('release_channel_collection', 'opt-in')
+ return 'nsITelemetry::' + ('DATASET_RELEASE_CHANNEL_OPTOUT' \
+ if release_channel_collection == 'opt-out' else 'DATASET_RELEASE_CHANNEL_OPTIN')
+
+ @property
+ def cpp_guard(self):
+ """Get the cpp guard for this scalar"""
+ return self._definition.get('cpp_guard')
+
+def load_scalars(filename):
+ """Parses a YAML file containing the scalar definition.
+
+ :param filename: the YAML file containing the scalars definition.
+ :raises Exception: if the scalar file cannot be opened or parsed.
+ """
+
+ # Parse the scalar definitions from the YAML file.
+ scalars = None
+ try:
+ with open(filename, 'r') as f:
+ scalars = yaml.safe_load(f)
+ except IOError, e:
+ raise Exception('Error opening ' + filename + ': ' + e.message)
+ except ValueError, e:
+ raise Exception('Error parsing scalars in ' + filename + ': ' + e.message)
+
+ scalar_list = []
+
+ # Scalars are defined in a fixed two-level hierarchy within the definition file.
+ # The first level contains the group name, while the second level contains the
+ # probe name (e.g. "group.name: probe: ...").
+ for group_name in scalars:
+ group = scalars[group_name]
+
+ # Make sure that the group has at least one probe in it.
+ if not group or len(group) == 0:
+ raise ValueError(group_name + ' must have at least a probe in it')
+
+ for probe_name in group:
+ # We found a scalar type. Go ahead and parse it.
+ scalar_info = group[probe_name]
+ scalar_list.append(ScalarType(group_name, probe_name, scalar_info))
+
+ return scalar_list
diff --git a/toolkit/components/telemetry/schemas/core.schema.json b/toolkit/components/telemetry/schemas/core.schema.json
new file mode 100644
index 0000000000..327cdc2989
--- /dev/null
+++ b/toolkit/components/telemetry/schemas/core.schema.json
@@ -0,0 +1,41 @@
+{
+ "$schema" : "http://json-schema.org/draft-04/schema#",
+ "type" : "object",
+ "name" : "core",
+ "properties" : {
+ "arch" : {
+ "type" : "string"
+ },
+ "clientId" : {
+ "type" : "string",
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
+ },
+ "device" : {
+ "type" : "string"
+ },
+ "experiments" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "locale" : {
+ "type" : "string"
+ },
+ "os" : {
+ "type" : "string"
+ },
+ "osversion" : {
+ "type" : "string"
+ },
+ "seq" : {
+ "type" : "integer",
+ "minimum": 0
+ },
+ "v" : {
+ "type" : "integer",
+ "enum" : [ 1 ]
+ }
+ },
+ "required" : ["arch", "clientId", "device", "locale", "os", "osversion", "seq", "v"]
+}
diff --git a/toolkit/components/telemetry/shared_telemetry_utils.py b/toolkit/components/telemetry/shared_telemetry_utils.py
new file mode 100644
index 0000000000..740c27e340
--- /dev/null
+++ b/toolkit/components/telemetry/shared_telemetry_utils.py
@@ -0,0 +1,103 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 file contains utility functions shared by the scalars and the histogram generation
+# scripts.
+
+from __future__ import print_function
+
+import re
+
+class StringTable:
+ """Manages a string table and allows C style serialization to a file."""
+
+ def __init__(self):
+ self.current_index = 0;
+ self.table = {}
+
+ def c_strlen(self, string):
+ """The length of a string including the null terminating character.
+ :param string: the input string.
+ """
+ return len(string) + 1
+
+ def stringIndex(self, string):
+ """Returns the index in the table of the provided string. Adds the string to
+ the table if it's not there.
+ :param string: the input string.
+ """
+ if string in self.table:
+ return self.table[string]
+ else:
+ result = self.current_index
+ self.table[string] = result
+ self.current_index += self.c_strlen(string)
+ return result
+
+ def stringIndexes(self, strings):
+ """ Returns a list of indexes for the provided list of strings.
+ Adds the strings to the table if they are not in it yet.
+ :param strings: list of strings to put into the table.
+ """
+ return [self.stringIndex(s) for s in strings]
+
+ def writeDefinition(self, f, name):
+ """Writes the string table to a file as a C const char array.
+
+ This writes out the string table as one single C char array for memory
+ size reasons, separating the individual strings with '\0' characters.
+ This way we can index directly into the string array and avoid the additional
+ storage costs for the pointers to them (and potential extra relocations for those).
+
+ :param f: the output stream.
+ :param name: the name of the output array.
+ """
+ entries = self.table.items()
+ entries.sort(key=lambda x:x[1])
+
+ # Avoid null-in-string warnings with GCC and potentially
+ # overlong string constants; write everything out the long way.
+ def explodeToCharArray(string):
+ def toCChar(s):
+ if s == "'":
+ return "'\\''"
+ else:
+ return "'%s'" % s
+ return ", ".join(map(toCChar, string))
+
+ f.write("const char %s[] = {\n" % name)
+ for (string, offset) in entries:
+ if "*/" in string:
+ raise ValueError, "String in string table contains unexpected sequence '*/': %s" % string
+
+ e = explodeToCharArray(string)
+ if e:
+ f.write(" /* %5d - \"%s\" */ %s, '\\0',\n"
+ % (offset, string, explodeToCharArray(string)))
+ else:
+ f.write(" /* %5d - \"%s\" */ '\\0',\n" % (offset, string))
+ f.write("};\n\n")
+
+def static_assert(output, expression, message):
+ """Writes a C++ compile-time assertion expression to a file.
+ :param output: the output stream.
+ :param expression: the expression to check.
+ :param message: the string literal that will appear if the expression evaluates to
+ false.
+ """
+ print("static_assert(%s, \"%s\");" % (expression, message), file=output)
+
+def add_expiration_postfix(expiration):
+ """ Formats the expiration version and adds a version postfix if needed.
+
+ :param expiration: the expiration version string.
+ :return: the modified expiration string.
+ """
+ if re.match(r'^[1-9][0-9]*$', expiration):
+ return expiration + ".0a1"
+
+ if re.match(r'^[1-9][0-9]*\.0$', expiration):
+ return expiration + "a1"
+
+ return expiration
diff --git a/toolkit/components/telemetry/tests/addons/dictionary/install.rdf b/toolkit/components/telemetry/tests/addons/dictionary/install.rdf
new file mode 100644
index 0000000000..ff0039b39d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/dictionary/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>telemetry-dictionary@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>64</em:type>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Telemetry test dictionary</em:name>
+ <em:description>A nice dictionary to prevent all typos for Telemetry.</em:description>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/experiment/install.rdf b/toolkit/components/telemetry/tests/addons/experiment/install.rdf
new file mode 100644
index 0000000000..d12f06816d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/experiment/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>telemetry-experiment-1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>128</em:type>
+
+ <!-- Front End MetaData -->
+ <em:name>Telemetry test experiment</em:name>
+ <em:description>Yet another experiment that experiments experimentally.</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/extension-2/install.rdf b/toolkit/components/telemetry/tests/addons/extension-2/install.rdf
new file mode 100644
index 0000000000..ddb5904f86
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/extension-2/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>telemetry-ext-2@tests.mozilla.org</em:id>
+ <em:version>2</em:version>
+ <em:type>2</em:type>
+
+ <!-- Front End MetaData -->
+ <em:name>Telemetry test extension 2</em:name>
+ <em:description>Yet another extension that extends twice.</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/extension/install.rdf b/toolkit/components/telemetry/tests/addons/extension/install.rdf
new file mode 100644
index 0000000000..4b1bd2da7f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/extension/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>telemetry-ext-1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>2</em:type>
+
+ <!-- Front End MetaData -->
+ <em:name>Telemetry test extension</em:name>
+ <em:description>Yet another extension that extends.</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/long-fields/install.rdf b/toolkit/components/telemetry/tests/addons/long-fields/install.rdf
new file mode 100644
index 0000000000..23ca7523cf
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/long-fields/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>tel-longfields-xpi@tests.mozilla.org</em:id>
+ <em:version>This is a really long addon version, that will get limited to 100 characters. We're much longer, we're at about 116.</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>This is a really long addon name, that will get limited to 100 characters. We're much longer, we're at about 219. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus nullam sodales. Yeah, Latin placeholder.</em:name>
+ <em:description>This is a really long addon description, that will get limited to 100 characters. We're much longer, we're at about 200. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus nullam sodales.</em:description>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/restartless/install.rdf b/toolkit/components/telemetry/tests/addons/restartless/install.rdf
new file mode 100644
index 0000000000..f6cda9f252
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/restartless/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>tel-restartless-xpi@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Telemetry Restartless Test</em:name>
+ <em:description>A restartless addon which gets enabled without a reboot.</em:description>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/signed/META-INF/manifest.mf b/toolkit/components/telemetry/tests/addons/signed/META-INF/manifest.mf
new file mode 100644
index 0000000000..e6e279dbc2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed/META-INF/manifest.mf
@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+
+Name: install.rdf
+Digest-Algorithms: MD5 SHA1
+MD5-Digest: YEilRfaecTg2bMNPoYqexQ==
+SHA1-Digest: GEnQKM8Coyw83phx/z1oNh327+0=
+
diff --git a/toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.rsa b/toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..8e5a92650e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.sf b/toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.sf
new file mode 100644
index 0000000000..21ce46081d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed/META-INF/mozilla.sf
@@ -0,0 +1,4 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: Ko2bKTrwTXCdstWHWqCR4w==
+SHA1-Digest-Manifest: k6+jfNGFxXtDd1cSX0ZoIyQ1cww=
+
diff --git a/toolkit/components/telemetry/tests/addons/signed/install.rdf b/toolkit/components/telemetry/tests/addons/signed/install.rdf
new file mode 100644
index 0000000000..5fdca172c7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>tel-signed-xpi@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Telemetry Signed Test</em:name>
+ <em:description>A signed addon which gets enabled without a reboot.</em:description>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/system/install.rdf b/toolkit/components/telemetry/tests/addons/system/install.rdf
new file mode 100644
index 0000000000..12cb143a7d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/system/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>tel-system-xpi@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Telemetry System Add-on Test</em:name>
+ <em:description>A system addon which is shipped with Firefox.</em:description>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/addons/theme/install.rdf b/toolkit/components/telemetry/tests/addons/theme/install.rdf
new file mode 100644
index 0000000000..a35249dba7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/theme/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>telemetry-theme@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:type>4</em:type>
+
+ <!-- Front End MetaData -->
+ <em:name>Telemetry test theme</em:name>
+ <em:description>A good looking test theme for Telemetry.</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/telemetry/tests/browser/browser.ini b/toolkit/components/telemetry/tests/browser/browser.ini
new file mode 100644
index 0000000000..a1725d54d3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser.ini
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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_TelemetryGC.js]
diff --git a/toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js b/toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js
new file mode 100644
index 0000000000..262fd69ffa
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser_TelemetryGC.js
@@ -0,0 +1,193 @@
+"use strict";
+
+/*
+ *********************************************************************************
+ * *
+ * WARNING *
+ * *
+ * If you adjust any of the constants here (slice limit, number of keys, etc.) *
+ * make sure to update the JSON schema at: *
+ * https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/ *
+ * telemetry/main.schema.json *
+ * *
+ * Otherwise, pings may be dropped by the telemetry backend! *
+ * *
+ ********************************************************************************/
+
+const {GCTelemetry} = Cu.import("resource://gre/modules/GCTelemetry.jsm", {});
+
+function check(entries) {
+ const FIELDS = ["random", "worst"];
+
+ // Check that all FIELDS are in |entries|.
+ for (let f of FIELDS) {
+ ok(f in entries, `${f} found in entries`);
+ }
+
+ // Check that only FIELDS are in |entries|.
+ for (let k of Object.keys(entries)) {
+ ok(FIELDS.includes(k), `${k} found in FIELDS`);
+ }
+
+ let foundGCs = 0;
+
+ for (let f of FIELDS) {
+ ok(Array.isArray(entries[f]), "have an array of GCs");
+
+ ok(entries[f].length <= 2, "not too many GCs");
+
+ for (let gc of entries[f]) {
+ ok(gc !== null, "GC is non-null");
+
+ foundGCs++;
+
+ ok(Object.keys(gc).length <= 25, "number of keys in GC is not too large");
+
+ // Sanity check the GC data.
+ ok("total_time" in gc, "total_time field present");
+ ok("max_pause" in gc, "max_pause field present");
+
+ ok("slices" in gc, "slices field present");
+ ok(Array.isArray(gc.slices), "slices is an array");
+ ok(gc.slices.length > 0, "slices array non-empty");
+ ok(gc.slices.length <= 4, "slices array is not too long");
+
+ ok("totals" in gc, "totals field present");
+ ok(typeof(gc.totals) == "object", "totals is an object");
+ ok(Object.keys(gc.totals).length <= 65, "totals array is not too long");
+
+ // Make sure we don't skip any big objects.
+ for (let key in gc) {
+ if (key != "slices" && key != "totals") {
+ ok(typeof(gc[key]) != "object", `${key} property should be primitive`);
+ }
+ }
+
+ let phases = new Set();
+
+ for (let slice of gc.slices) {
+ ok(Object.keys(slice).length <= 15, "slice is not too large");
+
+ ok("pause" in slice, "pause field present in slice");
+ ok("reason" in slice, "reason field present in slice");
+ ok("times" in slice, "times field present in slice");
+
+ // Make sure we don't skip any big objects.
+ for (let key in slice) {
+ if (key != "times") {
+ ok(typeof(slice[key]) != "object", `${key} property should be primitive`);
+ }
+ }
+
+ ok(Object.keys(slice.times).length <= 65, "no more than 65 phases");
+
+ for (let phase in slice.times) {
+ phases.add(phase);
+ ok(typeof(slice.times[phase]) == "number", `${phase} property should be a number`);
+ }
+ }
+
+ let totals = gc.totals;
+ // Make sure we don't skip any big objects.
+ for (let phase in totals) {
+ ok(typeof(totals[phase]) == "number", `${phase} property should be a number`);
+ }
+
+ for (let phase of phases) {
+ ok(phase in totals, `${phase} is in totals`);
+ }
+ }
+ }
+
+ ok(foundGCs > 0, "saw at least one GC");
+}
+
+add_task(function* test() {
+ let multiprocess = Services.appinfo.browserTabsRemoteAutostart;
+
+ // Set these prefs to ensure that we get measurements.
+ const prefs = {"set": [["javascript.options.mem.notify", true]]};
+ yield new Promise(resolve => SpecialPowers.pushPrefEnv(prefs, resolve));
+
+ function runRemote(f) {
+ gBrowser.selectedBrowser.messageManager.loadFrameScript(`data:,(${f})()`, false);
+ }
+
+ function initScript() {
+ const {GCTelemetry} = Components.utils.import("resource://gre/modules/GCTelemetry.jsm", {});
+
+ /*
+ * Don't shut down GC telemetry if it was already running before the test!
+ * Note: We need to use a multiline comment here since this code is turned into a data: URI.
+ */
+ let shutdown = GCTelemetry.init();
+
+ function listener() {
+ removeMessageListener("GCTelemTest:Shutdown", listener);
+ if (shutdown) {
+ GCTelemetry.shutdown();
+ }
+ }
+ addMessageListener("GCTelemTest:Shutdown", listener);
+ }
+
+ if (multiprocess) {
+ runRemote(initScript);
+ }
+
+ // Don't shut down GC telemetry if it was already running before the test!
+ let shutdown = GCTelemetry.init();
+ registerCleanupFunction(() => {
+ if (shutdown) {
+ GCTelemetry.shutdown();
+ }
+
+ gBrowser.selectedBrowser.messageManager.sendAsyncMessage("GCTelemTest:Shutdown");
+ });
+
+ let localPromise = new Promise(resolve => {
+ function obs() {
+ Services.obs.removeObserver(obs, "garbage-collection-statistics");
+ resolve();
+ }
+ Services.obs.addObserver(obs, "garbage-collection-statistics", false);
+ });
+
+ let remotePromise;
+ if (multiprocess) {
+ remotePromise = new Promise(resolve => {
+ function obs() {
+ Services.ppmm.removeMessageListener("Telemetry:GCStatistics", obs);
+ resolve();
+ }
+ Services.ppmm.addMessageListener("Telemetry:GCStatistics", obs);
+ });
+ } else {
+ remotePromise = Promise.resolve();
+ }
+
+ // Make sure we have a GC to work with in both processes.
+ Cu.forceGC();
+ if (multiprocess) {
+ runRemote(() => Components.utils.forceGC());
+ }
+
+ info("Waiting for GCs");
+
+ yield Promise.all([localPromise, remotePromise]);
+
+ let localEntries = GCTelemetry.entries("main", true);
+ let remoteEntries = multiprocess ? GCTelemetry.entries("content", true) : localEntries;
+
+ check(localEntries);
+ check(remoteEntries);
+
+ localEntries = GCTelemetry.entries("main", false);
+ remoteEntries = multiprocess ? GCTelemetry.entries("content", false) : localEntries;
+
+ is(localEntries.random.length, 0, "no random GCs after reset");
+ is(localEntries.worst.length, 0, "no worst GCs after reset");
+
+ is(remoteEntries.random.length, 0, "no random GCs after reset");
+ is(remoteEntries.worst.length, 0, "no worst GCs after reset");
+});
diff --git a/toolkit/components/telemetry/tests/search/chrome.manifest b/toolkit/components/telemetry/tests/search/chrome.manifest
new file mode 100644
index 0000000000..ec412e0508
--- /dev/null
+++ b/toolkit/components/telemetry/tests/search/chrome.manifest
@@ -0,0 +1,3 @@
+locale testsearchplugin ar jar:jar:searchTest.jar!/chrome/searchTest.jar!/
+content testsearchplugin ./
+
diff --git a/toolkit/components/telemetry/tests/search/searchTest.jar b/toolkit/components/telemetry/tests/search/searchTest.jar
new file mode 100644
index 0000000000..b10fc0c3ec
--- /dev/null
+++ b/toolkit/components/telemetry/tests/search/searchTest.jar
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/.eslintrc.js b/toolkit/components/telemetry/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm
new file mode 100644
index 0000000000..9be82c8835
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm
@@ -0,0 +1,86 @@
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/TelemetryArchive.jsm");
+Cu.import("resource://testing-common/Assert.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm");
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetryArchiveTesting",
+];
+
+function checkForProperties(ping, expected) {
+ for (let [props, val] of expected) {
+ let test = ping;
+ for (let prop of props) {
+ test = test[prop];
+ if (test === undefined) {
+ return false;
+ }
+ }
+ if (test !== val) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * A helper object that allows test code to check whether a telemetry ping
+ * was properly saved. To use, first initialize to collect the starting pings
+ * and then check for new ping data.
+ */
+function Checker() {
+}
+Checker.prototype = {
+ promiseInit: function() {
+ this._pingMap = new Map();
+ return TelemetryArchive.promiseArchivedPingList().then((plist) => {
+ for (let ping of plist) {
+ this._pingMap.set(ping.id, ping);
+ }
+ });
+ },
+
+ /**
+ * Find and return a new ping with certain properties.
+ *
+ * @param expected: an array of [['prop'...], 'value'] to check
+ * For example:
+ * [
+ * [['environment', 'build', 'applicationId'], '20150101010101'],
+ * [['version'], 1],
+ * [['metadata', 'OOMAllocationSize'], 123456789],
+ * ]
+ * @returns a matching ping if found, or null
+ */
+ promiseFindPing: Task.async(function*(type, expected) {
+ let candidates = [];
+ let plist = yield TelemetryArchive.promiseArchivedPingList();
+ for (let ping of plist) {
+ if (this._pingMap.has(ping.id)) {
+ continue;
+ }
+ if (ping.type == type) {
+ candidates.push(ping);
+ }
+ }
+
+ for (let candidate of candidates) {
+ let ping = yield TelemetryArchive.promiseArchivedPingById(candidate.id);
+ if (checkForProperties(ping, expected)) {
+ return ping;
+ }
+ }
+ return null;
+ }),
+};
+
+const TelemetryArchiveTesting = {
+ setup: function() {
+ Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+ Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
+ },
+
+ Checker: Checker,
+};
diff --git a/toolkit/components/telemetry/tests/unit/engine.xml b/toolkit/components/telemetry/tests/unit/engine.xml
new file mode 100644
index 0000000000..2304fcdd7b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/engine.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-telemetry</ShortName>
+<Url type="text/html" method="GET" template="http://www.example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/telemetry/tests/unit/head.js b/toolkit/components/telemetry/tests/unit/head.js
new file mode 100644
index 0000000000..51be257666
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -0,0 +1,319 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { classes: Cc, utils: Cu, interfaces: Ci, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/FileUtils.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://testing-common/httpd.js", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonTestUtils",
+ "resource://testing-common/AddonTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const gIsWindows = AppConstants.platform == "win";
+const gIsMac = AppConstants.platform == "macosx";
+const gIsAndroid = AppConstants.platform == "android";
+const gIsGonk = AppConstants.platform == "gonk";
+const gIsLinux = AppConstants.platform == "linux";
+
+const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
+
+const MILLISECONDS_PER_MINUTE = 60 * 1000;
+const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
+const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
+
+const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+
+const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+var gGlobalScope = this;
+
+const PingServer = {
+ _httpServer: null,
+ _started: false,
+ _defers: [ PromiseUtils.defer() ],
+ _currentDeferred: 0,
+
+ get port() {
+ return this._httpServer.identity.primaryPort;
+ },
+
+ get started() {
+ return this._started;
+ },
+
+ registerPingHandler: function(handler) {
+ const wrapped = wrapWithExceptionHandler(handler);
+ this._httpServer.registerPrefixHandler("/submit/telemetry/", wrapped);
+ },
+
+ resetPingHandler: function() {
+ this.registerPingHandler((request, response) => {
+ let deferred = this._defers[this._defers.length - 1];
+ this._defers.push(PromiseUtils.defer());
+ deferred.resolve(request);
+ });
+ },
+
+ start: function() {
+ this._httpServer = new HttpServer();
+ this._httpServer.start(-1);
+ this._started = true;
+ this.clearRequests();
+ this.resetPingHandler();
+ },
+
+ stop: function() {
+ return new Promise(resolve => {
+ this._httpServer.stop(resolve);
+ this._started = false;
+ });
+ },
+
+ clearRequests: function() {
+ this._defers = [ PromiseUtils.defer() ];
+ this._currentDeferred = 0;
+ },
+
+ promiseNextRequest: function() {
+ const deferred = this._defers[this._currentDeferred++];
+ // Send the ping to the consumer on the next tick, so that the completion gets
+ // signaled to Telemetry.
+ return new Promise(r => Services.tm.currentThread.dispatch(() => r(deferred.promise),
+ Ci.nsIThread.DISPATCH_NORMAL));
+ },
+
+ promiseNextPing: function() {
+ return this.promiseNextRequest().then(request => decodeRequestPayload(request));
+ },
+
+ promiseNextRequests: Task.async(function*(count) {
+ let results = [];
+ for (let i=0; i<count; ++i) {
+ results.push(yield this.promiseNextRequest());
+ }
+
+ return results;
+ }),
+
+ promiseNextPings: function(count) {
+ return this.promiseNextRequests(count).then(requests => {
+ return Array.from(requests, decodeRequestPayload);
+ });
+ },
+};
+
+/**
+ * Decode the payload of an HTTP request into a ping.
+ * @param {Object} request The data representing an HTTP request (nsIHttpRequest).
+ * @return {Object} The decoded ping payload.
+ */
+function decodeRequestPayload(request) {
+ let s = request.bodyInputStream;
+ let payload = null;
+ let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON)
+
+ if (request.getHeader("content-encoding") == "gzip") {
+ let observer = {
+ buffer: "",
+ onStreamComplete: function(loader, context, status, length, result) {
+ this.buffer = String.fromCharCode.apply(this, result);
+ }
+ };
+
+ let scs = Cc["@mozilla.org/streamConverters;1"]
+ .getService(Ci.nsIStreamConverterService);
+ let listener = Cc["@mozilla.org/network/stream-loader;1"]
+ .createInstance(Ci.nsIStreamLoader);
+ listener.init(observer);
+ let converter = scs.asyncConvertData("gzip", "uncompressed",
+ listener, null);
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, null, s, 0, s.available());
+ converter.onStopRequest(null, null, null);
+ let unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = "UTF-8";
+ let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer);
+ utf8string += unicodeConverter.Finish();
+ payload = JSON.parse(utf8string);
+ } else {
+ payload = decoder.decodeFromStream(s, s.available());
+ }
+
+ return payload;
+}
+
+function wrapWithExceptionHandler(f) {
+ function wrapper(...args) {
+ try {
+ f(...args);
+ } catch (ex) {
+ if (typeof(ex) != 'object') {
+ throw ex;
+ }
+ dump("Caught exception: " + ex.message + "\n");
+ dump(ex.stack);
+ do_test_finished();
+ }
+ }
+ return wrapper;
+}
+
+function loadAddonManager(...args) {
+ AddonTestUtils.init(gGlobalScope);
+ AddonTestUtils.overrideCertDB();
+ createAppInfo(...args);
+
+ // As we're not running in application, we need to setup the features directory
+ // used by system add-ons.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+ AddonTestUtils.registerDirectory("XREAppFeat", distroDir);
+ return AddonTestUtils.promiseStartupManager();
+}
+
+var gAppInfo = null;
+
+function createAppInfo(ID="xpcshell@tests.mozilla.org", name="XPCShell",
+ version="1.0", platformVersion="1.0") {
+ AddonTestUtils.createAppInfo(ID, name, version, platformVersion);
+ gAppInfo = AddonTestUtils.appInfo;
+}
+
+// Fake the timeout functions for the TelemetryScheduler.
+function fakeSchedulerTimer(set, clear) {
+ let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
+ session.Policy.setSchedulerTickTimeout = set;
+ session.Policy.clearSchedulerTickTimeout = clear;
+}
+
+/**
+ * Fake the current date.
+ * This passes all received arguments to a new Date constructor and
+ * uses the resulting date to fake the time in Telemetry modules.
+ *
+ * @return Date The new faked date.
+ */
+function fakeNow(...args) {
+ const date = new Date(...args);
+ const modules = [
+ Cu.import("resource://gre/modules/TelemetrySession.jsm"),
+ Cu.import("resource://gre/modules/TelemetryEnvironment.jsm"),
+ Cu.import("resource://gre/modules/TelemetryController.jsm"),
+ Cu.import("resource://gre/modules/TelemetryStorage.jsm"),
+ Cu.import("resource://gre/modules/TelemetrySend.jsm"),
+ Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm"),
+ ];
+
+ for (let m of modules) {
+ m.Policy.now = () => date;
+ }
+
+ return new Date(date);
+}
+
+function fakeMonotonicNow(ms) {
+ const m = Cu.import("resource://gre/modules/TelemetrySession.jsm");
+ m.Policy.monotonicNow = () => ms;
+ return ms;
+}
+
+// Fake the timeout functions for TelemetryController sending.
+function fakePingSendTimer(set, clear) {
+ let module = Cu.import("resource://gre/modules/TelemetrySend.jsm");
+ let obj = Cu.cloneInto({set, clear}, module, {cloneFunctions:true});
+ module.Policy.setSchedulerTickTimeout = obj.set;
+ module.Policy.clearSchedulerTickTimeout = obj.clear;
+}
+
+function fakeMidnightPingFuzzingDelay(delayMs) {
+ let module = Cu.import("resource://gre/modules/TelemetrySend.jsm");
+ module.Policy.midnightPingFuzzingDelay = () => delayMs;
+}
+
+function fakeGeneratePingId(func) {
+ let module = Cu.import("resource://gre/modules/TelemetryController.jsm");
+ module.Policy.generatePingId = func;
+}
+
+function fakeCachedClientId(uuid) {
+ let module = Cu.import("resource://gre/modules/TelemetryController.jsm");
+ module.Policy.getCachedClientID = () => uuid;
+}
+
+// Return a date that is |offset| ms in the future from |date|.
+function futureDate(date, offset) {
+ return new Date(date.getTime() + offset);
+}
+
+function truncateToDays(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
+// Returns a promise that resolves to true when the passed promise rejects,
+// false otherwise.
+function promiseRejects(promise) {
+ return promise.then(() => false, () => true);
+}
+
+// Generates a random string of at least a specific length.
+function generateRandomString(length) {
+ let string = "";
+
+ while (string.length < length) {
+ string += Math.random().toString(36);
+ }
+
+ return string.substring(0, length);
+}
+
+// Short-hand for retrieving the histogram with that id.
+function getHistogram(histogramId) {
+ return Telemetry.getHistogramById(histogramId);
+}
+
+// Short-hand for retrieving the snapshot of the Histogram with that id.
+function getSnapshot(histogramId) {
+ return Telemetry.getHistogramById(histogramId).snapshot();
+}
+
+// Helper for setting an empty list of Environment preferences to watch.
+function setEmptyPrefWatchlist() {
+ let TelemetryEnvironment =
+ Cu.import("resource://gre/modules/TelemetryEnvironment.jsm").TelemetryEnvironment;
+ return TelemetryEnvironment.onInitialized().then(() => {
+ TelemetryEnvironment.testWatchPreferences(new Map());
+ });
+}
+
+if (runningInParent) {
+ // Set logging preferences for all the tests.
+ Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+ // Telemetry archiving should be on.
+ Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
+ // Telemetry xpcshell tests cannot show the infobar.
+ Services.prefs.setBoolPref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
+ // FHR uploads should be enabled.
+ Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true);
+
+ fakePingSendTimer((callback, timeout) => {
+ Services.tm.mainThread.dispatch(() => callback(), Ci.nsIThread.DISPATCH_NORMAL);
+ },
+ () => {});
+
+ do_register_cleanup(() => TelemetrySend.shutdown());
+}
+
+TelemetryController.testInitLogging();
+
+// Avoid timers interrupting test behavior.
+fakeSchedulerTimer(() => {}, () => {});
+// Make pind sending predictable.
+fakeMidnightPingFuzzingDelay(0);
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
new file mode 100644
index 0000000000..11d730499e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
@@ -0,0 +1,107 @@
+
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
+const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload";
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+function run_child_test() {
+ // Setup histograms with some fixed values.
+ let flagHist = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ flagHist.add(1);
+ let countHist = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false);
+ countHist.add();
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true);
+ countHist.add();
+ countHist.add();
+ let categHist = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ categHist.add("Label2");
+ categHist.add("Label3");
+
+ let flagKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
+ flagKeyed.add("a", 1);
+ flagKeyed.add("b", 1);
+ let countKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", false);
+ countKeyed.add("a");
+ countKeyed.add("b");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", true);
+ countKeyed.add("a");
+ countKeyed.add("b");
+ countKeyed.add("b");
+}
+
+function check_histogram_values(payload) {
+ const hs = payload.histograms;
+ Assert.ok("TELEMETRY_TEST_COUNT" in hs, "Should have count test histogram.");
+ Assert.ok("TELEMETRY_TEST_FLAG" in hs, "Should have flag test histogram.");
+ Assert.ok("TELEMETRY_TEST_CATEGORICAL" in hs, "Should have categorical test histogram.");
+ Assert.equal(hs["TELEMETRY_TEST_COUNT"].sum, 2,
+ "Count test histogram should have the right value.");
+ Assert.equal(hs["TELEMETRY_TEST_FLAG"].sum, 1,
+ "Flag test histogram should have the right value.");
+ Assert.equal(hs["TELEMETRY_TEST_CATEGORICAL"].sum, 3,
+ "Categorical test histogram should have the right sum.");
+
+ const kh = payload.keyedHistograms;
+ Assert.ok("TELEMETRY_TEST_KEYED_COUNT" in kh, "Should have keyed count test histogram.");
+ Assert.ok("TELEMETRY_TEST_KEYED_FLAG" in kh, "Should have keyed flag test histogram.");
+ Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["a"].sum, 1,
+ "Keyed count test histogram should have the right value.");
+ Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["b"].sum, 2,
+ "Keyed count test histogram should have the right value.");
+ Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["a"].sum, 1,
+ "Keyed flag test histogram should have the right value.");
+ Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["b"].sum, 1,
+ "Keyed flag test histogram should have the right value.");
+}
+
+add_task(function*() {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ dump("... done with child test\n");
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ yield TelemetryController.testSetup();
+ if (runningInParent) {
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+ }
+
+ // Run test in child, don't wait for it to finish.
+ run_test_in_child("test_ChildHistograms.js");
+ yield do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ let payload = TelemetrySession.getPayload("test-ping");
+ return payload &&
+ "processes" in payload &&
+ "content" in payload.processes &&
+ "histograms" in payload.processes.content &&
+ "TELEMETRY_TEST_COUNT" in payload.processes.content.histograms;
+ });
+ const payload = TelemetrySession.getPayload("test-ping");
+ Assert.ok("processes" in payload, "Should have processes section");
+ Assert.ok("content" in payload.processes, "Should have child process section");
+ Assert.ok("histograms" in payload.processes.content, "Child process section should have histograms.");
+ Assert.ok("keyedHistograms" in payload.processes.content, "Child process section should have keyed histograms.");
+ check_histogram_values(payload.processes.content);
+
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_PingAPI.js b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
new file mode 100644
index 0000000000..d4d79aad4e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -0,0 +1,502 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+Cu.import("resource://gre/modules/ClientID.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "datareporting", "archived");
+});
+
+/**
+ * Fakes the archive storage quota.
+ * @param {Integer} aArchiveQuota The new quota, in bytes.
+ */
+function fakeStorageQuota(aArchiveQuota) {
+ let storage = Cu.import("resource://gre/modules/TelemetryStorage.jsm");
+ storage.Policy.getArchiveQuota = () => aArchiveQuota;
+}
+
+/**
+ * Lists all the valid archived pings and their metadata, sorted by creation date.
+ *
+ * @param aFileName {String} The filename.
+ * @return {Object[]} A list of objects with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string>,
+ * size: <integer> }
+ */
+var getArchivedPingsInfo = Task.async(function*() {
+ let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath);
+ let subdirs = (yield dirIterator.nextBatch()).filter(e => e.isDir);
+ let archivedPings = [];
+
+ // Iterate through the subdirs of |gPingsArchivePath|.
+ for (let dir of subdirs) {
+ let fileIterator = new OS.File.DirectoryIterator(dir.path);
+ let files = (yield fileIterator.nextBatch()).filter(e => !e.isDir);
+
+ // Then get a list of the files for the current subdir.
+ for (let f of files) {
+ let pingInfo = TelemetryStorage._testGetArchivedPingDataFromFileName(f.name);
+ if (!pingInfo) {
+ // This is not a valid archived ping, skip it.
+ continue;
+ }
+ // Find the size of the ping and then add the info to the array.
+ pingInfo.size = (yield OS.File.stat(f.path)).size;
+ archivedPings.push(pingInfo);
+ }
+ }
+
+ // Sort the list by creation date and then return it.
+ archivedPings.sort((a, b) => b.timestamp - a.timestamp);
+ return archivedPings;
+});
+
+add_task(function* test_setup() {
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+});
+
+add_task(function* test_archivedPings() {
+ // TelemetryController should not be fully initialized at this point.
+ // Submitting pings should still work fine.
+
+ const PINGS = [
+ {
+ type: "test-ping-api-1",
+ payload: { foo: "bar"},
+ dateCreated: new Date(2010, 1, 1, 10, 0, 0),
+ },
+ {
+ type: "test-ping-api-2",
+ payload: { moo: "meh"},
+ dateCreated: new Date(2010, 2, 1, 10, 0, 0),
+ },
+ ];
+
+ // Submit pings and check the ping list.
+ let expectedPingList = [];
+
+ for (let data of PINGS) {
+ fakeNow(data.dateCreated);
+ data.id = yield TelemetryController.submitExternalPing(data.type, data.payload);
+ let list = yield TelemetryArchive.promiseArchivedPingList();
+
+ expectedPingList.push({
+ id: data.id,
+ type: data.type,
+ timestampCreated: data.dateCreated.getTime(),
+ });
+ Assert.deepEqual(list, expectedPingList, "Archived ping list should contain submitted pings");
+ }
+
+ // Check loading the archived pings.
+ let checkLoadingPings = Task.async(function*() {
+ for (let data of PINGS) {
+ let ping = yield TelemetryArchive.promiseArchivedPingById(data.id);
+ Assert.equal(ping.id, data.id, "Archived ping should have matching id");
+ Assert.equal(ping.type, data.type, "Archived ping should have matching type");
+ Assert.equal(ping.creationDate, data.dateCreated.toISOString(),
+ "Archived ping should have matching creation date");
+ }
+ });
+
+ yield checkLoadingPings();
+
+ // Check that we find the archived pings again by scanning after a restart.
+ yield TelemetryController.testReset();
+
+ let pingList = yield TelemetryArchive.promiseArchivedPingList();
+ Assert.deepEqual(expectedPingList, pingList,
+ "Should have submitted pings in archive list after restart");
+ yield checkLoadingPings();
+
+ // Write invalid pings into the archive with both valid and invalid names.
+ let writeToArchivedDir = Task.async(function*(dirname, filename, content, compressed) {
+ const dirPath = OS.Path.join(gPingsArchivePath, dirname);
+ yield OS.File.makeDir(dirPath, { ignoreExisting: true });
+ const filePath = OS.Path.join(dirPath, filename);
+ const options = { tmpPath: filePath + ".tmp", noOverwrite: false };
+ if (compressed) {
+ options.compression = "lz4";
+ }
+ yield OS.File.writeAtomic(filePath, content, options);
+ });
+
+ const FAKE_ID1 = "10000000-0123-0123-0123-0123456789a1";
+ const FAKE_ID2 = "20000000-0123-0123-0123-0123456789a2";
+ const FAKE_ID3 = "20000000-0123-0123-0123-0123456789a3";
+ const FAKE_TYPE = "foo";
+
+ // These should get rejected.
+ yield writeToArchivedDir("xx", "foo.json", "{}");
+ yield writeToArchivedDir("2010-02", "xx.xx.xx.json", "{}");
+ // This one should get picked up...
+ yield writeToArchivedDir("2010-02", "1." + FAKE_ID1 + "." + FAKE_TYPE + ".json", "{}");
+ // ... but get overwritten by this one.
+ yield writeToArchivedDir("2010-02", "2." + FAKE_ID1 + "." + FAKE_TYPE + ".json", "");
+ // This should get picked up fine.
+ yield writeToArchivedDir("2010-02", "3." + FAKE_ID2 + "." + FAKE_TYPE + ".json", "");
+ // This compressed ping should get picked up fine as well.
+ yield writeToArchivedDir("2010-02", "4." + FAKE_ID3 + "." + FAKE_TYPE + ".jsonlz4", "");
+
+ expectedPingList.push({
+ id: FAKE_ID1,
+ type: "foo",
+ timestampCreated: 2,
+ });
+ expectedPingList.push({
+ id: FAKE_ID2,
+ type: "foo",
+ timestampCreated: 3,
+ });
+ expectedPingList.push({
+ id: FAKE_ID3,
+ type: "foo",
+ timestampCreated: 4,
+ });
+ expectedPingList.sort((a, b) => a.timestampCreated - b.timestampCreated);
+
+ // Reset the TelemetryArchive so we scan the archived dir again.
+ yield TelemetryController.testReset();
+
+ // Check that we are still picking up the valid archived pings on disk,
+ // plus the valid ones above.
+ pingList = yield TelemetryArchive.promiseArchivedPingList();
+ Assert.deepEqual(expectedPingList, pingList, "Should have picked up valid archived pings");
+ yield checkLoadingPings();
+
+ // Now check that we fail to load the two invalid pings from above.
+ Assert.ok((yield promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID1))),
+ "Should have rejected invalid ping");
+ Assert.ok((yield promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID2))),
+ "Should have rejected invalid ping");
+});
+
+add_task(function* test_archiveCleanup() {
+ const PING_TYPE = "foo";
+
+ // Empty the archive.
+ yield OS.File.removeDir(gPingsArchivePath);
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").clear();
+ // Also reset these histograms to make sure normal sized pings don't get counted.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").clear();
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB").clear();
+
+ // Build the cache. Nothing should be evicted as there's no ping directory.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testCleanupTaskPromise();
+ yield TelemetryArchive.promiseArchivedPingList();
+
+ let h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must report 0 pings scanned if no archive dir exists.");
+ // One directory out of four was removed as well.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must report 0 evicted dirs if no archive dir exists.");
+
+ let expectedPrunedInfo = [];
+ let expectedNotPrunedInfo = [];
+
+ let checkArchive = Task.async(function*() {
+ // Check that the pruned pings are not on disk anymore.
+ for (let prunedInfo of expectedPrunedInfo) {
+ yield Assert.rejects(TelemetryArchive.promiseArchivedPingById(prunedInfo.id),
+ "Ping " + prunedInfo.id + " should have been pruned.");
+ const pingPath =
+ TelemetryStorage._testGetArchivedPingPath(prunedInfo.id, prunedInfo.creationDate, PING_TYPE);
+ Assert.ok(!(yield OS.File.exists(pingPath)), "The ping should not be on the disk anymore.");
+ }
+
+ // Check that the expected pings are there.
+ for (let expectedInfo of expectedNotPrunedInfo) {
+ Assert.ok((yield TelemetryArchive.promiseArchivedPingById(expectedInfo.id)),
+ "Ping" + expectedInfo.id + " should be in the archive.");
+ }
+ });
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").clear();
+
+ // Create a ping which should be pruned because it is past the retention period.
+ let date = fakeNow(2010, 1, 1, 1, 0, 0);
+ let firstDate = date;
+ let pingId = yield TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedPrunedInfo.push({ id: pingId, creationDate: date });
+
+ // Create a ping which should be kept because it is within the retention period.
+ const oldestDirectoryDate = fakeNow(2010, 2, 1, 1, 0, 0);
+ pingId = yield TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedNotPrunedInfo.push({ id: pingId, creationDate: oldestDirectoryDate });
+
+ // Create 20 other pings which are within the retention period, but would be affected
+ // by the disk quota.
+ for (let month of [3, 4]) {
+ for (let minute = 0; minute < 10; minute++) {
+ date = fakeNow(2010, month, 1, 1, minute, 0);
+ pingId = yield TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedNotPrunedInfo.push({ id: pingId, creationDate: date });
+ }
+ }
+
+ // We expect all the pings we archived to be in this histogram.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT");
+ Assert.equal(h.snapshot().sum, 22, "All the pings must be live-accumulated in the histogram.");
+ // Reset the histogram that will be populated by the archive scan.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").clear();
+
+ // Move the current date 60 days ahead of the first ping.
+ fakeNow(futureDate(firstDate, 60 * MILLISECONDS_PER_DAY));
+ // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
+ yield TelemetryController.testReset();
+ // Wait for the cleanup to finish.
+ yield TelemetryStorage.testCleanupTaskPromise();
+ // Then scan the archived dir.
+ yield TelemetryArchive.promiseArchivedPingList();
+
+ // Check that the archive is in the correct state.
+ yield checkArchive();
+
+ // Make sure the ping count is correct after the scan (one ping was removed).
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").snapshot();
+ Assert.equal(h.sum, 21, "The histogram must count all the pings in the archive.");
+ // One directory out of four was removed as well.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must correctly report removed archive directories.");
+ // Check that the remaining directories are correctly counted.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").snapshot();
+ Assert.equal(h.sum, 3, "Telemetry must correctly report the remaining archive directories.");
+ // Check that the remaining directories are correctly counted.
+ const oldestAgeInMonths = 1;
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").snapshot();
+ Assert.equal(h.sum, oldestAgeInMonths,
+ "Telemetry must correctly report age of the oldest directory in the archive.");
+
+ // We need to test the archive size before we hit the quota, otherwise a special
+ // value is recorded.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").clear();
+
+ // Move the current date 60 days ahead of the second ping.
+ fakeNow(futureDate(oldestDirectoryDate, 60 * MILLISECONDS_PER_DAY));
+ // Reset TelemetryController and TelemetryArchive.
+ yield TelemetryController.testReset();
+ // Wait for the cleanup to finish.
+ yield TelemetryStorage.testCleanupTaskPromise();
+ // Then scan the archived dir again.
+ yield TelemetryArchive.promiseArchivedPingList();
+
+ // Move the oldest ping to the unexpected pings list.
+ expectedPrunedInfo.push(expectedNotPrunedInfo.shift());
+ // Check that the archive is in the correct state.
+ yield checkArchive();
+
+ // Find how much disk space the archive takes.
+ const archivedPingsInfo = yield getArchivedPingsInfo();
+ let archiveSizeInBytes =
+ archivedPingsInfo.reduce((lastResult, element) => lastResult + element.size, 0);
+
+ // Check that the correct values for quota probes are reported when no quota is hit.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
+ Assert.equal(h.sum, Math.round(archiveSizeInBytes / 1024 / 1024),
+ "Telemetry must report the correct archive size.");
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must report 0 evictions if quota is not hit.");
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must report a null elapsed time if quota is not hit.");
+
+ // Set the quota to 80% of the space.
+ const testQuotaInBytes = archiveSizeInBytes * 0.8;
+ fakeStorageQuota(testQuotaInBytes);
+
+ // The storage prunes archived pings until we reach 90% of the requested storage quota.
+ // Based on that, find how many pings should be kept.
+ const safeQuotaSize = testQuotaInBytes * 0.9;
+ let sizeInBytes = 0;
+ let pingsWithinQuota = [];
+ let pingsOutsideQuota = [];
+
+ for (let pingInfo of archivedPingsInfo) {
+ sizeInBytes += pingInfo.size;
+ if (sizeInBytes >= safeQuotaSize) {
+ pingsOutsideQuota.push({ id: pingInfo.id, creationDate: new Date(pingInfo.timestamp) });
+ continue;
+ }
+ pingsWithinQuota.push({ id: pingInfo.id, creationDate: new Date(pingInfo.timestamp) });
+ }
+
+ expectedNotPrunedInfo = pingsWithinQuota;
+ expectedPrunedInfo = expectedPrunedInfo.concat(pingsOutsideQuota);
+
+ // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testCleanupTaskPromise();
+ yield TelemetryArchive.promiseArchivedPingList();
+ // Check that the archive is in the correct state.
+ yield checkArchive();
+
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").snapshot();
+ Assert.equal(h.sum, pingsOutsideQuota.length,
+ "Telemetry must correctly report the over quota pings evicted from the archive.");
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
+ Assert.equal(h.sum, 300, "Archive quota was hit, a special size must be reported.");
+
+ // Trigger a cleanup again and make sure we're not removing anything.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testCleanupTaskPromise();
+ yield TelemetryArchive.promiseArchivedPingList();
+ yield checkArchive();
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create and archive an oversized, uncompressed, ping.
+ const OVERSIZED_PING = {
+ id: OVERSIZED_PING_ID,
+ type: PING_TYPE,
+ creationDate: (new Date()).toISOString(),
+ // Generate a ~2MB string to use as the payload.
+ payload: generateRandomString(2 * 1024 * 1024)
+ };
+ yield TelemetryArchive.promiseArchivePing(OVERSIZED_PING);
+
+ // Get the size of the archived ping.
+ const oversizedPingPath =
+ TelemetryStorage._testGetArchivedPingPath(OVERSIZED_PING.id, new Date(OVERSIZED_PING.creationDate), PING_TYPE) + "lz4";
+ const archivedPingSizeMB = Math.floor((yield OS.File.stat(oversizedPingPath)).size / 1024 / 1024);
+
+ // We expect the oversized ping to be pruned when scanning the archive.
+ expectedPrunedInfo.push({ id: OVERSIZED_PING_ID, creationDate: new Date(OVERSIZED_PING.creationDate) });
+
+ // Scan the archive.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testCleanupTaskPromise();
+ yield TelemetryArchive.promiseArchivedPingList();
+ // The following also checks that non oversized pings are not removed.
+ yield checkArchive();
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report 1 oversized ping in the archive.");
+ h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB").snapshot();
+ Assert.equal(h.counts[archivedPingSizeMB], 1,
+ "Telemetry must report the correct size for the oversized ping.");
+});
+
+add_task(function* test_clientId() {
+ // Check that a ping submitted after the delayed telemetry initialization completed
+ // should get a valid client id.
+ yield TelemetryController.testReset();
+ const clientId = yield ClientID.getClientID();
+
+ let id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true});
+ let ping = yield TelemetryArchive.promiseArchivedPingById(id);
+
+ Assert.ok(!!ping, "Should have loaded the ping.");
+ Assert.ok("clientId" in ping, "Ping should have a client id.");
+ Assert.ok(UUID_REGEX.test(ping.clientId), "Client id is in UUID format.");
+ Assert.equal(ping.clientId, clientId, "Ping client id should match the global client id.");
+
+ // We should have cached the client id now. Lets confirm that by
+ // checking the client id on a ping submitted before the async
+ // controller setup is finished.
+ let promiseSetup = TelemetryController.testReset();
+ id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true});
+ ping = yield TelemetryArchive.promiseArchivedPingById(id);
+ Assert.equal(ping.clientId, clientId);
+
+ // Finish setup.
+ yield promiseSetup;
+});
+
+add_task(function* test_InvalidPingType() {
+ const TYPES = [
+ "a",
+ "-",
+ "¿€€€?",
+ "-foo-",
+ "-moo",
+ "zoo-",
+ ".bar",
+ "asfd.asdf",
+ ];
+
+ for (let type of TYPES) {
+ let histogram = Telemetry.getKeyedHistogramById("TELEMETRY_INVALID_PING_TYPE_SUBMITTED");
+ Assert.equal(histogram.snapshot(type).sum, 0,
+ "Should not have counted this invalid ping yet: " + type);
+ Assert.ok(promiseRejects(TelemetryController.submitExternalPing(type, {})),
+ "Ping type should have been rejected.");
+ Assert.equal(histogram.snapshot(type).sum, 1,
+ "Should have counted this as an invalid ping type.");
+ }
+});
+
+add_task(function* test_InvalidPayloadType() {
+ const PAYLOAD_TYPES = [
+ 19,
+ "string",
+ [1, 2, 3, 4],
+ null,
+ undefined,
+ ];
+
+ let histogram = Telemetry.getHistogramById("TELEMETRY_INVALID_PAYLOAD_SUBMITTED");
+ for (let i = 0; i < PAYLOAD_TYPES.length; i++) {
+ histogram.clear();
+ Assert.equal(histogram.snapshot().sum, 0,
+ "Should not have counted this invalid payload yet: " + JSON.stringify(PAYLOAD_TYPES[i]));
+ Assert.ok(yield promiseRejects(TelemetryController.submitExternalPing("payload-test", PAYLOAD_TYPES[i])),
+ "Payload type should have been rejected.");
+ Assert.equal(histogram.snapshot().sum, 1,
+ "Should have counted this as an invalid payload type.");
+ }
+});
+
+add_task(function* test_currentPingData() {
+ yield TelemetryController.testSetup();
+
+ // Setup test data.
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ h.add(1);
+ let k = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+ k.clear();
+ k.add("a", 1);
+
+ // Get current ping data objects and check that their data is sane.
+ for (let subsession of [true, false]) {
+ let ping = TelemetryController.getCurrentPingData(subsession);
+
+ Assert.ok(!!ping, "Should have gotten a ping.");
+ Assert.equal(ping.type, "main", "Ping should have correct type.");
+ const expectedReason = subsession ? "gather-subsession-payload" : "gather-payload";
+ Assert.equal(ping.payload.info.reason, expectedReason, "Ping should have the correct reason.");
+
+ let id = "TELEMETRY_TEST_RELEASE_OPTOUT";
+ Assert.ok(id in ping.payload.histograms, "Payload should have test count histogram.");
+ Assert.equal(ping.payload.histograms[id].sum, 1, "Test count value should match.");
+ id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT";
+ Assert.ok(id in ping.payload.keyedHistograms, "Payload should have keyed test histogram.");
+ Assert.equal(ping.payload.keyedHistograms[id]["a"].sum, 1, "Keyed test value should match.");
+ }
+});
+
+add_task(function* test_shutdown() {
+ yield TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
new file mode 100644
index 0000000000..c86fb04998
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "datareporting");
+});
+
+var promiseValidateArchivedPings = Task.async(function*(aExpectedReasons) {
+ // The list of ping reasons which mark the session end (and must reset the subsession
+ // count).
+ const SESSION_END_PING_REASONS = new Set([ REASON_ABORTED_SESSION, REASON_SHUTDOWN ]);
+
+ let list = yield TelemetryArchive.promiseArchivedPingList();
+
+ // We're just interested in the "main" pings.
+ list = list.filter(p => p.type == "main");
+
+ Assert.equal(aExpectedReasons.length, list.length, "All the expected pings must be received.");
+
+ let previousPing = yield TelemetryArchive.promiseArchivedPingById(list[0].id);
+ Assert.equal(aExpectedReasons.shift(), previousPing.payload.info.reason,
+ "Telemetry should only get pings with expected reasons.");
+ Assert.equal(previousPing.payload.info.previousSessionId, null,
+ "The first session must report a null previous session id.");
+ Assert.equal(previousPing.payload.info.previousSubsessionId, null,
+ "The first subsession must report a null previous subsession id.");
+ Assert.equal(previousPing.payload.info.profileSubsessionCounter, 1,
+ "profileSubsessionCounter must be 1 the first time.");
+ Assert.equal(previousPing.payload.info.subsessionCounter, 1,
+ "subsessionCounter must be 1 the first time.");
+
+ let expectedSubsessionCounter = 1;
+ let expectedPreviousSessionId = previousPing.payload.info.sessionId;
+
+ for (let i = 1; i < list.length; i++) {
+ let currentPing = yield TelemetryArchive.promiseArchivedPingById(list[i].id);
+ let currentInfo = currentPing.payload.info;
+ let previousInfo = previousPing.payload.info;
+ do_print("Archive entry " + i + " - id: " + currentPing.id + ", reason: " + currentInfo.reason);
+
+ Assert.equal(aExpectedReasons.shift(), currentInfo.reason,
+ "Telemetry should only get pings with expected reasons.");
+ Assert.equal(currentInfo.previousSessionId, expectedPreviousSessionId,
+ "Telemetry must correctly chain session identifiers.");
+ Assert.equal(currentInfo.previousSubsessionId, previousInfo.subsessionId,
+ "Telemetry must correctly chain subsession identifiers.");
+ Assert.equal(currentInfo.profileSubsessionCounter, previousInfo.profileSubsessionCounter + 1,
+ "Telemetry must correctly track the profile subsessions count.");
+ Assert.equal(currentInfo.subsessionCounter, expectedSubsessionCounter,
+ "The subsession counter should be monotonically increasing.");
+
+ // Store the current ping as previous.
+ previousPing = currentPing;
+ // Reset the expected subsession counter, if required. Otherwise increment the expected
+ // subsession counter.
+ // If this is the final subsession of a session we need to update expected values accordingly.
+ if (SESSION_END_PING_REASONS.has(currentInfo.reason)) {
+ expectedSubsessionCounter = 1;
+ expectedPreviousSessionId = currentInfo.sessionId;
+ } else {
+ expectedSubsessionCounter++;
+ }
+ }
+});
+
+add_task(function* test_setup() {
+ do_test_pending();
+
+ // Addon manager needs a profile directory
+ do_get_profile();
+ loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Preferences.set(PREF_TELEMETRY_ENABLED, true);
+});
+
+add_task(function* test_subsessionsChaining() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const PREF_TEST = PREF_BRANCH + "test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Fake the clock data to manually trigger an aborted-session ping and a daily ping.
+ // This is also helpful to make sure we get the archived pings in an expected order.
+ let now = fakeNow(2009, 9, 18, 0, 0, 0);
+ let monotonicNow = fakeMonotonicNow(1000);
+
+ let moveClockForward = (minutes) => {
+ let ms = minutes * MILLISECONDS_PER_MINUTE;
+ now = fakeNow(futureDate(now, ms));
+ monotonicNow = fakeMonotonicNow(monotonicNow + ms);
+ }
+
+ // Keep track of the ping reasons we're expecting in this test.
+ let expectedReasons = [];
+
+ // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1,
+ // subsessionCounter: 1, subsessionId: A, and previousSubsessionId: null to be archived.
+ yield TelemetryController.testSetup();
+ yield TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry but don't wait for it to initialise before shutting down. We expect a
+ // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B
+ // and previousSubsessionId: A to be archived.
+ moveClockForward(30);
+ TelemetryController.testReset();
+ yield TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping
+ // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and
+ // previousSubsessionId: B to be archived.
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+ moveClockForward(6);
+ // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session
+ // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being
+ // sent when calling |TelemetryController.testReset()|, thus breaking some tests.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Start Telemetry and trigger an environment change through a pref modification. We expect
+ // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1,
+ // subsessionId: D and previousSubsessionId: C to be archived.
+ moveClockForward(30);
+ yield TelemetryController.testReset();
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 1);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5,
+ // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived.
+ moveClockForward(30);
+ yield TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and trigger a daily ping. We expect a daily ping with
+ // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and
+ // previousSubsessionId: E to be archived.
+ moveClockForward(30);
+ yield TelemetryController.testReset();
+
+ // Delay the callback around midnight.
+ now = fakeNow(futureDate(now, MS_IN_ONE_DAY));
+ // Trigger the daily ping.
+ yield schedulerTickCallback();
+ expectedReasons.push(REASON_DAILY);
+
+ // Trigger an environment change ping. We expect an environment-changed ping with
+ // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and
+ // previousSubsessionId: F to be archived.
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 0);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Shut down Telemetry and trigger a shutdown ping.
+ moveClockForward(30);
+ yield TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and trigger an environment change.
+ yield TelemetryController.testReset();
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 1);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Don't shut down, instead trigger an aborted-session ping.
+ moveClockForward(6);
+ // Trigger the an aborted session ping save.
+ yield schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Start Telemetry and trigger a daily ping.
+ moveClockForward(30);
+ yield TelemetryController.testReset();
+ // Delay the callback around midnight.
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+ // Trigger the daily ping.
+ yield schedulerTickCallback();
+ expectedReasons.push(REASON_DAILY);
+
+ // Trigger an environment change.
+ moveClockForward(30);
+ Preferences.set(PREF_TEST, 0);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // And an aborted-session ping again.
+ moveClockForward(6);
+ // Trigger the an aborted session ping save.
+ yield schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Make sure the aborted-session ping gets archived.
+ yield TelemetryController.testReset();
+
+ yield promiseValidateArchivedPings(expectedReasons);
+});
+
+add_task(function* () {
+ yield TelemetryController.testShutdown();
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
new file mode 100644
index 0000000000..b383de6bf1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -0,0 +1,507 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+Cu.import("resource://gre/modules/ClientID.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const PING_FORMAT_VERSION = 4;
+const DELETION_PING_TYPE = "deletion";
+const TEST_PING_TYPE = "test-ping-type";
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_NAME = "XPCShell";
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_ENABLED = PREF_BRANCH + "enabled";
+const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+const PREF_UNIFIED = PREF_BRANCH + "unified";
+
+var gClientID = null;
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type", "id", "creationDate", "version", "application", "payload"
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version.");
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f],
+ f + " must have the correct value.");
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok("architecture" in aPing.application,
+ "The application section must have an architecture field.");
+ Assert.ok("channel" in aPing.application,
+ "The application section must have a channel field.");
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+add_task(function* test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(PREF_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
+
+ yield new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve)));
+});
+
+add_task(function* asyncSetup() {
+ yield TelemetryController.testSetup();
+});
+
+// Ensure that not overwriting an existing file fails silently
+add_task(function* test_overwritePing() {
+ let ping = {id: "foo"};
+ yield TelemetryStorage.savePing(ping, true);
+ yield TelemetryStorage.savePing(ping, false);
+ yield TelemetryStorage.cleanupPingFile(ping);
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(function* test_simplePing() {
+ PingServer.start();
+ // Update the Telemetry Server preference with the address of the local server.
+ // Otherwise we might end up sending stuff to a non-existing server after
+ // |TelemetryController.testReset| is called.
+ Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port);
+
+ yield sendPing(false, false);
+ let request = yield PingServer.promiseNextRequest();
+
+ // Check that we have a version query parameter in the URL.
+ Assert.notEqual(request.queryString, "");
+
+ // Make sure the version in the query string matches the new ping format version.
+ let params = request.queryString.split("&");
+ Assert.ok(params.find(p => p == ("v=" + PING_FORMAT_VERSION)));
+
+ let ping = decodeRequestPayload(request);
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+});
+
+add_task(function* test_disableDataUpload() {
+ const isUnified = Preferences.get(PREF_UNIFIED, false);
+ if (!isUnified) {
+ // Skipping the test if unified telemetry is off, as no deletion ping will
+ // be generated.
+ return;
+ }
+
+ // Disable FHR upload: this should trigger a deletion ping.
+ Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
+
+ let ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_PING_TYPE, true, false);
+ // Wait on ping activity to settle.
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ // Restore FHR Upload.
+ Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
+
+ // Simulate a failure in sending the deletion ping by disabling the HTTP server.
+ yield PingServer.stop();
+
+ // Try to send a ping. It will be saved as pending and get deleted when disabling upload.
+ TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Disable FHR upload to send a deletion ping again.
+ Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
+
+ // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that.
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't
+ // trigger the shutdown, so we need to call it ourselves.
+ yield TelemetryStorage.shutdown();
+ // Simulate a restart, and spin the send task.
+ yield TelemetryController.testReset();
+
+ // Disabling Telemetry upload must clear out all the pending pings.
+ let pendingPings = yield TelemetryStorage.loadPendingPingList();
+ Assert.equal(pendingPings.length, 1,
+ "All the pending pings but the deletion ping should have been deleted");
+
+ // Enable the ping server again.
+ PingServer.start();
+ // We set the new server using the pref, otherwise it would get reset with
+ // |TelemetryController.testReset|.
+ Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port);
+
+ // Stop the sending task and then start it again.
+ yield TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ yield TelemetryController.testReset();
+ ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_PING_TYPE, true, false);
+
+ // Wait on ping activity to settle before moving on to the next test. If we were
+ // to shut down telemetry, even though the PingServer caught the expected pings,
+ // TelemetrySend could still be processing them (clearing pings would happen in
+ // a couple of ticks). Shutting down would cancel the request and save them as
+ // pending pings.
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ // Restore FHR Upload.
+ Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
+});
+
+add_task(function* test_pingHasClientId() {
+ const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
+
+ // Make sure we have no cached client ID for this test: we'll try to send
+ // a ping with it while Telemetry is being initialized.
+ Preferences.reset(PREF_CACHED_CLIENTID);
+ yield TelemetryController.testShutdown();
+ yield ClientID._reset();
+ yield TelemetryStorage.testClearPendingPings();
+ // And also clear the counter histogram since we're here.
+ let h = Telemetry.getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID");
+ h.clear();
+
+ // Init telemetry and try to send a ping with a client ID.
+ let promisePingSetup = TelemetryController.testReset();
+ yield sendPing(true, false);
+ Assert.equal(h.snapshot().sum, 1,
+ "We must have a ping waiting for the clientId early during startup.");
+ // Wait until we are fully initialized. Pings will be assembled but won't get
+ // sent before then.
+ yield promisePingSetup;
+
+ let ping = yield PingServer.promiseNextPing();
+ // Fetch the client ID after initializing and fetching the the ping, so we
+ // don't unintentionally trigger its loading. We'll still need the client ID
+ // to see if the ping looks sane.
+ gClientID = yield ClientID.getClientID();
+
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");
+
+ // Shutdown Telemetry so we can safely restart it.
+ yield TelemetryController.testShutdown();
+ yield TelemetryStorage.testClearPendingPings();
+
+ // We should have cached the client ID now. Lets confirm that by checking it before
+ // the async ping setup is finished.
+ h.clear();
+ promisePingSetup = TelemetryController.testReset();
+ yield sendPing(true, false);
+ yield promisePingSetup;
+
+ // Check that we received the cached client id.
+ Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId.");
+ ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(ping.clientId, gClientID,
+ "Telemetry should report the correct cached clientId.");
+
+ // Check that sending a ping without relying on the cache, after the
+ // initialization, still works.
+ Preferences.reset(PREF_CACHED_CLIENTID);
+ yield TelemetryController.testShutdown();
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+ yield sendPing(true, false);
+ ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");
+ Assert.equal(h.snapshot().sum, 0, "No ping should have been waiting for a clientId.");
+});
+
+add_task(function* test_pingHasEnvironment() {
+ // Send a ping with the environment data.
+ yield sendPing(false, true);
+ let ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, false, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+});
+
+add_task(function* test_pingHasEnvironmentAndClientId() {
+ // Send a ping with the environment data and client id.
+ yield sendPing(true, true);
+ let ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+ // Test that we have the correct clientId.
+ Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");
+});
+
+add_task(function* test_archivePings() {
+ let now = new Date(2009, 10, 18, 12, 0, 0);
+ fakeNow(now);
+
+ // Disable ping upload so that pings don't get sent.
+ // With unified telemetry the FHR upload pref controls this,
+ // with non-unified telemetry the Telemetry enabled pref.
+ const isUnified = Preferences.get(PREF_UNIFIED, false);
+ const uploadPref = isUnified ? PREF_FHR_UPLOAD_ENABLED : PREF_ENABLED;
+ Preferences.set(uploadPref, false);
+
+ // If we're using unified telemetry, disabling ping upload will generate a "deletion"
+ // ping. Catch it.
+ if (isUnified) {
+ let ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_PING_TYPE, true, false);
+ }
+
+ // Register a new Ping Handler that asserts if a ping is received, then send a ping.
+ PingServer.registerPingHandler(() => Assert.ok(false, "Telemetry must not send pings if not allowed to."));
+ let pingId = yield sendPing(true, true);
+
+ // Check that the ping was archived, even with upload disabled.
+ let ping = yield TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(ping.id, pingId, "TelemetryController should still archive pings.");
+
+ // Check that pings don't get archived if not allowed to.
+ now = new Date(2010, 10, 18, 12, 0, 0);
+ fakeNow(now);
+ Preferences.set(PREF_ARCHIVE_ENABLED, false);
+ pingId = yield sendPing(true, true);
+ let promise = TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.ok((yield promiseRejects(promise)),
+ "TelemetryController should not archive pings if the archive pref is disabled.");
+
+ // Enable archiving and the upload so that pings get sent and archived again.
+ Preferences.set(uploadPref, true);
+ Preferences.set(PREF_ARCHIVE_ENABLED, true);
+
+ now = new Date(2014, 6, 18, 22, 0, 0);
+ fakeNow(now);
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ pingId = yield sendPing(true, true);
+
+ // Check that we archive pings when successfully sending them.
+ yield PingServer.promiseNextPing();
+ ping = yield TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(ping.id, pingId,
+ "TelemetryController should still archive pings if ping upload is enabled.");
+});
+
+// Test that we fuzz the submission time around midnight properly
+// to avoid overloading the telemetry servers.
+add_task(function* test_midnightPingSendFuzzing() {
+ const fuzzingDelay = 60 * 60 * 1000;
+ fakeMidnightPingFuzzingDelay(fuzzingDelay);
+ let now = new Date(2030, 5, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let waitForTimer = () => new Promise(resolve => {
+ fakePingSendTimer((callback, timeout) => {
+ resolve([callback, timeout]);
+ }, () => {});
+ });
+
+ PingServer.clearRequests();
+ yield TelemetryController.testReset();
+
+ // A ping after midnight within the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 40, 0);
+ fakeNow(now);
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No ping should be received yet.");
+ });
+ let timerPromise = waitForTimer();
+ yield sendPing(true, true);
+ let [timerCallback, timerTimeout] = yield timerPromise;
+ Assert.ok(!!timerCallback);
+ Assert.deepEqual(futureDate(now, timerTimeout), new Date(2030, 5, 2, 1, 0, 0));
+
+ // A ping just before the end of the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 59, 59);
+ fakeNow(now);
+ timerPromise = waitForTimer();
+ yield sendPing(true, true);
+ [timerCallback, timerTimeout] = yield timerPromise;
+ Assert.deepEqual(timerTimeout, 1 * 1000);
+
+ // Restore the previous ping handler.
+ PingServer.resetPingHandler();
+
+ // Setting the clock to after the fuzzing delay, we should trigger the two ping sends
+ // with the timer callback.
+ now = futureDate(now, timerTimeout);
+ fakeNow(now);
+ yield timerCallback();
+ const pings = yield PingServer.promiseNextPings(2);
+ for (let ping of pings) {
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ }
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ // Moving the clock further we should still send pings immediately.
+ now = futureDate(now, 5 * 60 * 1000);
+ yield sendPing(true, true);
+ let ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ // Check that pings shortly before midnight are immediately sent.
+ now = fakeNow(2030, 5, 3, 23, 59, 0);
+ yield sendPing(true, true);
+ ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ // Clean-up.
+ fakeMidnightPingFuzzingDelay(0);
+ fakePingSendTimer(() => {}, () => {});
+});
+
+add_task(function* test_changePingAfterSubmission() {
+ // Submit a ping with a custom payload.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(TEST_PING_TYPE, payload, options);
+
+ // Change the payload with a predefined value.
+ payload.canary = "changed";
+
+ // Wait for the ping to be archived.
+ const pingId = yield pingPromise;
+
+ // Make sure our changes didn't affect the submitted payload.
+ let archivedCopy = yield TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(archivedCopy.payload.canary, "test",
+ "The payload must not be changed after being submitted.");
+});
+
+add_task(function* test_telemetryEnabledUnexpectedValue() {
+ // Remove the default value for toolkit.telemetry.enabled from the default prefs.
+ // Otherwise, we wouldn't be able to set the pref to a string.
+ let defaultPrefBranch = Services.prefs.getDefaultBranch(null);
+ defaultPrefBranch.deleteBranch(PREF_ENABLED);
+
+ // Set the preferences controlling the Telemetry status to a string.
+ Preferences.set(PREF_ENABLED, "false");
+ // Check that Telemetry is not enabled.
+ yield TelemetryController.testReset();
+ Assert.equal(Telemetry.canRecordExtended, false,
+ "Invalid values must not enable Telemetry recording.");
+
+ // Delete the pref again.
+ defaultPrefBranch.deleteBranch(PREF_ENABLED);
+
+ // Make sure that flipping it to true works.
+ Preferences.set(PREF_ENABLED, true);
+ yield TelemetryController.testReset();
+ Assert.equal(Telemetry.canRecordExtended, true,
+ "True must enable Telemetry recording.");
+
+ // Also check that the false works as well.
+ Preferences.set(PREF_ENABLED, false);
+ yield TelemetryController.testReset();
+ Assert.equal(Telemetry.canRecordExtended, false,
+ "False must disable Telemetry recording.");
+});
+
+add_task(function* test_telemetryCleanFHRDatabase() {
+ const FHR_DBNAME_PREF = "datareporting.healthreport.dbName";
+ const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite";
+ const DEFAULT_DB_NAME = "healthreport.sqlite";
+
+ // Check that we're able to remove a FHR DB with a custom name.
+ const CUSTOM_DB_PATHS = [
+ OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME),
+ OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-wal"),
+ OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-shm"),
+ ];
+ Preferences.set(FHR_DBNAME_PREF, CUSTOM_DB_NAME);
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ yield OS.File.writeAtomic(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ yield TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath);
+ }
+
+ // We should not break anything if there's no DB file.
+ yield TelemetryStorage.removeFHRDatabase();
+
+ // Check that we're able to remove a FHR DB with the default name.
+ Preferences.reset(FHR_DBNAME_PREF);
+
+ const DEFAULT_DB_PATHS = [
+ OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME),
+ OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-wal"),
+ OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-shm"),
+ ];
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ yield OS.File.writeAtomic(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ yield TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath);
+ }
+});
+
+add_task(function* stopServer() {
+ yield PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
new file mode 100644
index 0000000000..b8a88afa29
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* Test inclusion of previous build ID in telemetry pings when build ID changes.
+ * bug 841028
+ *
+ * Cases to cover:
+ * 1) Run with no "previousBuildID" stored in prefs:
+ * -> no previousBuildID in telemetry system info, new value set in prefs.
+ * 2) previousBuildID in prefs, equal to current build ID:
+ * -> no previousBuildID in telemetry, prefs not updated.
+ * 3) previousBuildID in prefs, not equal to current build ID:
+ * -> previousBuildID in telemetry, new value set in prefs.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Force the Telemetry enabled preference so that TelemetrySession.testReset() doesn't exit early.
+Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+
+// Set up our dummy AppInfo object so we can control the appBuildID.
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo();
+
+// Check that when run with no previous build ID stored, we update the pref but do not
+// put anything into the metadata.
+add_task(function* test_firstRun() {
+ yield TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ do_check_false("previousBuildID" in metadata);
+ let appBuildID = getAppInfo().appBuildID;
+ let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID);
+ do_check_eq(appBuildID, buildIDPref);
+});
+
+// Check that a subsequent run with the same build ID does not put prev build ID in
+// metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
+add_task(function* test_secondRun() {
+ yield TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ do_check_false("previousBuildID" in metadata);
+});
+
+// Set up telemetry with a different app build ID and check that the old build ID
+// is returned in the metadata and the pref is updated to the new build ID.
+// Assumes testFirstRun() has been called to set the previousBuildID pref.
+const NEW_BUILD_ID = "20130314";
+add_task(function* test_newBuild() {
+ let info = getAppInfo();
+ let oldBuildID = info.appBuildID;
+ info.appBuildID = NEW_BUILD_ID;
+ yield TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ do_check_eq(metadata.previousBuildId, oldBuildID);
+ let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID);
+ do_check_eq(NEW_BUILD_ID, buildIDPref);
+});
+
+
+function run_test() {
+ // Make sure we have a profile directory.
+ do_get_profile();
+
+ run_next_test();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
new file mode 100644
index 0000000000..391db0d9de
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that TelemetryController sends close to shutdown don't lead
+// to AsyncShutdown timeouts.
+
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
+Cu.import("resource://testing-common/httpd.js", this);
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+function contentHandler(metadata, response)
+{
+ dump("contentHandler called for path: " + metadata._path + "\n");
+ // We intentionally don't finish writing the response here to let the
+ // client time out.
+ response.processAsync();
+ response.setHeader("Content-Type", "text/plain");
+}
+
+add_task(function* test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
+});
+
+/**
+ * Ensures that TelemetryController does not hang processing shutdown
+ * phases. Assumes that Telemetry shutdown routines do not take longer than
+ * CRASH_TIMEOUT_MS to complete.
+ */
+add_task(function* test_sendTelemetryShutsDownWithinReasonableTimeout() {
+ const CRASH_TIMEOUT_MS = 5 * 1000;
+ // Enable testing mode for AsyncShutdown, otherwise some testing-only functionality
+ // is not available.
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ // Reducing the max delay for waitiing on phases to complete from 1 minute
+ // (standard) to 10 seconds to avoid blocking the tests in case of misbehavior.
+ Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", CRASH_TIMEOUT_MS);
+
+ let httpServer = new HttpServer();
+ httpServer.registerPrefixHandler("/", contentHandler);
+ httpServer.start(-1);
+
+ yield TelemetryController.testSetup();
+ TelemetrySend.setServer("http://localhost:" + httpServer.identity.primaryPort);
+ let submissionPromise = TelemetryController.submitExternalPing("test-ping-type", {});
+
+ // Trigger the AsyncShutdown phase TelemetryController hangs off.
+ AsyncShutdown.profileBeforeChange._trigger();
+ AsyncShutdown.sendTelemetry._trigger();
+ // Now wait for the ping submission.
+ yield submissionPromise;
+
+ // If we get here, we didn't time out in the shutdown routines.
+ Assert.ok(true, "Didn't time out on shutdown.");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
new file mode 100644
index 0000000000..ca5d1820b3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that TelemetrySession notifies correctly on idle-daily.
+
+Cu.import("resource://testing-common/httpd.js", this);
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+var gHttpServer = null;
+
+add_task(function* test_setup() {
+ do_get_profile();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
+
+ // Start the webserver to check if the pending ping correctly arrives.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+});
+
+add_task(function* testSendPendingOnIdleDaily() {
+ // Create a valid pending ping.
+ const PENDING_PING = {
+ id: "2133234d-4ea1-44f4-909e-ce8c6c41e0fc",
+ type: "test-ping",
+ version: 4,
+ application: {},
+ payload: {},
+ };
+ yield TelemetryStorage.savePing(PENDING_PING, true);
+
+ // Telemetry will not send this ping at startup, because it's not overdue.
+ yield TelemetryController.testSetup();
+ TelemetrySend.setServer("http://localhost:" + gHttpServer.identity.primaryPort);
+
+ let pendingPromise = new Promise(resolve =>
+ gHttpServer.registerPrefixHandler("/submit/telemetry/", request => resolve(request)));
+
+ let gatherPromise = PromiseUtils.defer();
+ Services.obs.addObserver(gatherPromise.resolve, "gather-telemetry", false);
+
+ // Check that we are correctly receiving the gather-telemetry notification.
+ TelemetrySession.observe(null, "idle-daily", null);
+ yield gatherPromise;
+ Assert.ok(true, "Received gather-telemetry notification.");
+
+ Services.obs.removeObserver(gatherPromise.resolve, "gather-telemetry");
+
+ // Check that the pending ping is correctly received.
+ let ns = {};
+ let module = Cu.import("resource://gre/modules/TelemetrySend.jsm", ns);
+ module.TelemetrySendImpl.observe(null, "idle-daily", null);
+ let request = yield pendingPromise;
+ let ping = decodeRequestPayload(request);
+
+ // Validate the ping data.
+ Assert.equal(ping.id, PENDING_PING.id);
+ Assert.equal(ping.type, PENDING_PING.type);
+
+ yield new Promise(resolve => gHttpServer.stop(resolve));
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
new file mode 100644
index 0000000000..35181272ae
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -0,0 +1,1528 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://testing-common/AddonManagerTesting.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm", this);
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+// AttributionCode is only needed for Firefox
+XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode",
+ "resource:///modules/AttributionCode.jsm");
+
+// Lazy load |LightweightThemeManager|, we won't be using it on Gonk.
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
+ "resource://gre/modules/ProfileAge.jsm");
+
+// The webserver hosting the addons.
+var gHttpServer = null;
+// The URL of the webserver root.
+var gHttpRoot = null;
+// The URL of the data directory, on the webserver.
+var gDataRoot = null;
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+const APP_HOTFIX_VERSION = "2.3.4a";
+
+const DISTRIBUTION_ID = "distributor-id";
+const DISTRIBUTION_VERSION = "4.5.6b";
+const DISTRIBUTOR_NAME = "Some Distributor";
+const DISTRIBUTOR_CHANNEL = "A Channel";
+const PARTNER_NAME = "test";
+const PARTNER_ID = "NicePartner-ID-3785";
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete";
+
+const GFX_VENDOR_ID = "0xabcd";
+const GFX_DEVICE_ID = "0x1234";
+
+// The profile reset date, in milliseconds (Today)
+const PROFILE_RESET_DATE_MS = Date.now();
+// The profile creation date, in milliseconds (Yesterday).
+const PROFILE_CREATION_DATE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
+
+const FLASH_PLUGIN_NAME = "Shockwave Flash";
+const FLASH_PLUGIN_DESC = "A mock flash plugin";
+const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d";
+const PLUGIN_MIME_TYPE1 = "application/x-shockwave-flash";
+const PLUGIN_MIME_TYPE2 = "text/plain";
+
+const PLUGIN2_NAME = "Quicktime";
+const PLUGIN2_DESC = "A mock Quicktime plugin";
+const PLUGIN2_VERSION = "2.3";
+
+const PERSONA_ID = "3785";
+// Defined by LightweightThemeManager, it is appended to the PERSONA_ID.
+const PERSONA_ID_SUFFIX = "@personas.mozilla.org";
+const PERSONA_NAME = "Test Theme";
+const PERSONA_DESCRIPTION = "A nice theme/persona description.";
+
+const PLUGIN_UPDATED_TOPIC = "plugins-list-updated";
+
+// system add-ons are enabled at startup, so record date when the test starts
+const SYSTEM_ADDON_INSTALL_DATE = Date.now();
+
+// Valid attribution code to write so that settings.attribution can be tested.
+const ATTRIBUTION_CODE = "source%3Dgoogle.com";
+
+/**
+ * Used to mock plugin tags in our fake plugin host.
+ */
+function PluginTag(aName, aDescription, aVersion, aEnabled) {
+ this.name = aName;
+ this.description = aDescription;
+ this.version = aVersion;
+ this.disabled = !aEnabled;
+}
+
+PluginTag.prototype = {
+ name: null,
+ description: null,
+ version: null,
+ filename: null,
+ fullpath: null,
+ disabled: false,
+ blocklisted: false,
+ clicktoplay: true,
+
+ mimeTypes: [ PLUGIN_MIME_TYPE1, PLUGIN_MIME_TYPE2 ],
+
+ getMimeTypes: function(count) {
+ count.value = this.mimeTypes.length;
+ return this.mimeTypes;
+ }
+};
+
+// A container for the plugins handled by the fake plugin host.
+var gInstalledPlugins = [
+ new PluginTag("Java", "A mock Java plugin", "1.0", false /* Disabled */),
+ new PluginTag(FLASH_PLUGIN_NAME, FLASH_PLUGIN_DESC, FLASH_PLUGIN_VERSION, true),
+];
+
+// A fake plugin host for testing plugin telemetry environment.
+var PluginHost = {
+ getPluginTags: function(countRef) {
+ countRef.value = gInstalledPlugins.length;
+ return gInstalledPlugins;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIPluginHost)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+function registerFakePluginHost() {
+ MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost);
+}
+
+var SysInfo = {
+ overrides: {},
+
+ getProperty(name) {
+ // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides));
+ if (name in this.overrides) {
+ return this.overrides[name];
+ }
+ try {
+ return this._genuine.getProperty(name);
+ } catch (ex) {
+ throw ex;
+ }
+ },
+
+ get(name) {
+ return this._genuine.get(name);
+ },
+
+ QueryInterface(iid) {
+ if (iid.equals(Ci.nsIPropertyBag2)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+function registerFakeSysInfo() {
+ MockRegistrar.register("@mozilla.org/system-info;1", SysInfo);
+}
+
+function MockAddonWrapper(aAddon) {
+ this.addon = aAddon;
+}
+MockAddonWrapper.prototype = {
+ get id() {
+ return this.addon.id;
+ },
+
+ get type() {
+ return "service";
+ },
+
+ get appDisabled() {
+ return false;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get scope() {
+ return AddonManager.SCOPE_PROFILE;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get blocklistState() {
+ return 0; // Not blocked.
+ },
+
+ get pendingOperations() {
+ return AddonManager.PENDING_NONE;
+ },
+
+ get permissions() {
+ return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE;
+ },
+
+ get isActive() {
+ return true;
+ },
+
+ get name() {
+ return this.addon.name;
+ },
+
+ get version() {
+ return this.addon.version;
+ },
+
+ get creator() {
+ return new AddonManagerPrivate.AddonAuthor(this.addon.author);
+ },
+
+ get userDisabled() {
+ return this.appDisabled;
+ },
+};
+
+function createMockAddonProvider(aName) {
+ let mockProvider = {
+ _addons: [],
+
+ get name() {
+ return aName;
+ },
+
+ addAddon: function(aAddon) {
+ this._addons.push(aAddon);
+ AddonManagerPrivate.callAddonListeners("onInstalled", new MockAddonWrapper(aAddon));
+ },
+
+ getAddonsByTypes: function (aTypes, aCallback) {
+ aCallback(this._addons.map(a => new MockAddonWrapper(a)));
+ },
+
+ shutdown() {
+ return Promise.resolve();
+ },
+ };
+
+ return mockProvider;
+}
+
+/**
+ * Used to spoof the Persona Id.
+ */
+function spoofTheme(aId, aName, aDesc) {
+ return {
+ id: aId,
+ name: aName,
+ description: aDesc,
+ headerURL: "http://lwttest.invalid/a.png",
+ footerURL: "http://lwttest.invalid/b.png",
+ textcolor: Math.random().toString(),
+ accentcolor: Math.random().toString()
+ };
+}
+
+function spoofGfxAdapter() {
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
+ gfxInfo.spoofVendorID(GFX_VENDOR_ID);
+ gfxInfo.spoofDeviceID(GFX_DEVICE_ID);
+ } catch (x) {
+ // If we can't test gfxInfo, that's fine, we'll note it later.
+ }
+}
+
+function spoofProfileReset() {
+ let profileAccessor = new ProfileAge();
+
+ return profileAccessor.writeTimes({
+ created: PROFILE_CREATION_DATE_MS,
+ reset: PROFILE_RESET_DATE_MS
+ });
+}
+
+function spoofPartnerInfo() {
+ let prefsToSpoof = {};
+ prefsToSpoof["distribution.id"] = DISTRIBUTION_ID;
+ prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION;
+ prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME;
+ prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL;
+ prefsToSpoof["app.partner.test"] = PARTNER_NAME;
+ prefsToSpoof["mozilla.partner.id"] = PARTNER_ID;
+
+ // Spoof the preferences.
+ for (let pref in prefsToSpoof) {
+ Preferences.set(pref, prefsToSpoof[pref]);
+ }
+}
+
+function getAttributionFile() {
+ let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
+ file.append("mozilla");
+ file.append(AppConstants.MOZ_APP_NAME);
+ file.append("postSigningData");
+ return file;
+}
+
+function spoofAttributionData() {
+ if (gIsWindows) {
+ AttributionCode._clearCache();
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(getAttributionFile(), -1, -1, 0);
+ stream.write(ATTRIBUTION_CODE, ATTRIBUTION_CODE.length);
+ }
+}
+
+function cleanupAttributionData() {
+ if (gIsWindows) {
+ getAttributionFile().remove(false);
+ AttributionCode._clearCache();
+ }
+}
+
+/**
+ * Check that a value is a string and not empty.
+ *
+ * @param aValue The variable to check.
+ * @return True if |aValue| has type "string" and is not empty, False otherwise.
+ */
+function checkString(aValue) {
+ return (typeof aValue == "string") && (aValue != "");
+}
+
+/**
+ * If value is non-null, check if it's a valid string.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid string, false if it's non-null and an invalid
+ * string.
+ */
+function checkNullOrString(aValue) {
+ if (aValue) {
+ return checkString(aValue);
+ } else if (aValue === null) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * If value is non-null, check if it's a boolean.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid boolean, false if it's non-null and an invalid
+ * boolean.
+ */
+function checkNullOrBool(aValue) {
+ return aValue === null || (typeof aValue == "boolean");
+}
+
+function checkBuildSection(data) {
+ const expectedInfo = {
+ applicationId: APP_ID,
+ applicationName: APP_NAME,
+ buildId: gAppInfo.appBuildID,
+ version: APP_VERSION,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ Assert.ok("build" in data, "There must be a build section in Environment.");
+
+ for (let f in expectedInfo) {
+ Assert.ok(checkString(data.build[f]), f + " must be a valid string.");
+ Assert.equal(data.build[f], expectedInfo[f], f + " must have the correct value.");
+ }
+
+ // Make sure architecture and hotfixVersion are in the environment.
+ Assert.ok(checkString(data.build.architecture));
+ Assert.ok(checkString(data.build.hotfixVersion));
+ Assert.equal(data.build.hotfixVersion, APP_HOTFIX_VERSION);
+
+ if (gIsMac) {
+ let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils);
+ if (macUtils && macUtils.isUniversalBinary) {
+ Assert.ok(checkString(data.build.architecturesInBinary));
+ }
+ }
+}
+
+function checkSettingsSection(data) {
+ const EXPECTED_FIELDS_TYPES = {
+ blocklistEnabled: "boolean",
+ e10sEnabled: "boolean",
+ e10sCohort: "string",
+ telemetryEnabled: "boolean",
+ locale: "string",
+ update: "object",
+ userPrefs: "object",
+ };
+
+ Assert.ok("settings" in data, "There must be a settings section in Environment.");
+
+ for (let f in EXPECTED_FIELDS_TYPES) {
+ Assert.equal(typeof data.settings[f], EXPECTED_FIELDS_TYPES[f],
+ f + " must have the correct type.");
+ }
+
+ // Check "addonCompatibilityCheckEnabled" separately, as it is not available
+ // on Gonk.
+ if (gIsGonk) {
+ Assert.ok(!("addonCompatibilityCheckEnabled" in data.settings), "Must not be available on Gonk.");
+ } else {
+ Assert.equal(data.settings.addonCompatibilityCheckEnabled, AddonManager.checkCompatibility);
+ }
+
+ // Check "isDefaultBrowser" separately, as it is not available on Android an can either be
+ // null or boolean on other platforms.
+ if (gIsAndroid) {
+ Assert.ok(!("isDefaultBrowser" in data.settings), "Must not be available on Android.");
+ } else {
+ Assert.ok(checkNullOrBool(data.settings.isDefaultBrowser));
+ }
+
+ // Check "channel" separately, as it can either be null or string.
+ let update = data.settings.update;
+ Assert.ok(checkNullOrString(update.channel));
+ Assert.equal(typeof update.enabled, "boolean");
+ Assert.equal(typeof update.autoDownload, "boolean");
+
+ // Check "defaultSearchEngine" separately, as it can either be undefined or string.
+ if ("defaultSearchEngine" in data.settings) {
+ checkString(data.settings.defaultSearchEngine);
+ Assert.equal(typeof data.settings.defaultSearchEngineData, "object");
+ }
+
+ if ("attribution" in data.settings) {
+ Assert.equal(typeof data.settings.attribution, "object");
+ Assert.equal(data.settings.attribution.source, "google.com");
+ }
+}
+
+function checkProfileSection(data) {
+ Assert.ok("profile" in data, "There must be a profile section in Environment.");
+ Assert.equal(data.profile.creationDate, truncateToDays(PROFILE_CREATION_DATE_MS));
+ Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS));
+}
+
+function checkPartnerSection(data, isInitial) {
+ const EXPECTED_FIELDS = {
+ distributionId: DISTRIBUTION_ID,
+ distributionVersion: DISTRIBUTION_VERSION,
+ partnerId: PARTNER_ID,
+ distributor: DISTRIBUTOR_NAME,
+ distributorChannel: DISTRIBUTOR_CHANNEL,
+ };
+
+ Assert.ok("partner" in data, "There must be a partner section in Environment.");
+
+ for (let f in EXPECTED_FIELDS) {
+ let expected = isInitial ? null : EXPECTED_FIELDS[f];
+ Assert.strictEqual(data.partner[f], expected, f + " must have the correct value.");
+ }
+
+ // Check that "partnerNames" exists and contains the correct element.
+ Assert.ok(Array.isArray(data.partner.partnerNames));
+ if (isInitial) {
+ Assert.equal(data.partner.partnerNames.length, 0);
+ } else {
+ Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME));
+ }
+}
+
+function checkGfxAdapter(data) {
+ const EXPECTED_ADAPTER_FIELDS_TYPES = {
+ description: "string",
+ vendorID: "string",
+ deviceID: "string",
+ subsysID: "string",
+ RAM: "number",
+ driver: "string",
+ driverVersion: "string",
+ driverDate: "string",
+ GPUActive: "boolean",
+ };
+
+ for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+
+ if (data[f]) {
+ // Since we have a non-null value, check if it has the correct type.
+ Assert.equal(typeof data[f], EXPECTED_ADAPTER_FIELDS_TYPES[f],
+ f + " must have the correct type.");
+ }
+ }
+}
+
+function checkSystemSection(data) {
+ const EXPECTED_FIELDS = [ "memoryMB", "cpu", "os", "hdd", "gfx" ];
+ const EXPECTED_HDD_FIELDS = [ "profile", "binary", "system" ];
+
+ Assert.ok("system" in data, "There must be a system section in Environment.");
+
+ // Make sure we have all the top level sections and fields.
+ for (let f of EXPECTED_FIELDS) {
+ Assert.ok(f in data.system, f + " must be available.");
+ }
+
+ Assert.ok(Number.isFinite(data.system.memoryMB), "MemoryMB must be a number.");
+
+ if (gIsWindows || gIsMac || gIsLinux) {
+ let EXTRA_CPU_FIELDS = ["cores", "model", "family", "stepping",
+ "l2cacheKB", "l3cacheKB", "speedMHz", "vendor"];
+
+ for (let f of EXTRA_CPU_FIELDS) {
+ // Note this is testing TelemetryEnvironment.js only, not that the
+ // values are valid - null is the fallback.
+ Assert.ok(f in data.system.cpu, f + " must be available under cpu.");
+ }
+
+ if (gIsWindows) {
+ Assert.equal(typeof data.system.isWow64, "boolean",
+ "isWow64 must be available on Windows and have the correct type.");
+ Assert.ok("virtualMaxMB" in data.system, "virtualMaxMB must be available.");
+ Assert.ok(Number.isFinite(data.system.virtualMaxMB),
+ "virtualMaxMB must be a number.");
+ }
+
+ // We insist these are available
+ for (let f of ["cores"]) {
+ Assert.ok(!(f in data.system.cpu) ||
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null.");
+ }
+
+ // These should be numbers if they are not null
+ for (let f of ["model", "family", "stepping", "l2cacheKB",
+ "l3cacheKB", "speedMHz"]) {
+ Assert.ok(!(f in data.system.cpu) ||
+ data.system.cpu[f] === null ||
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null.");
+ }
+ }
+
+ let cpuData = data.system.cpu;
+ Assert.ok(Number.isFinite(cpuData.count), "CPU count must be a number.");
+ Assert.ok(Array.isArray(cpuData.extensions), "CPU extensions must be available.");
+
+ // Device data is only available on Android or Gonk.
+ if (gIsAndroid || gIsGonk) {
+ let deviceData = data.system.device;
+ Assert.ok(checkNullOrString(deviceData.model));
+ Assert.ok(checkNullOrString(deviceData.manufacturer));
+ Assert.ok(checkNullOrString(deviceData.hardware));
+ Assert.ok(checkNullOrBool(deviceData.isTablet));
+ }
+
+ let osData = data.system.os;
+ Assert.ok(checkNullOrString(osData.name));
+ Assert.ok(checkNullOrString(osData.version));
+ Assert.ok(checkNullOrString(osData.locale));
+
+ // Service pack is only available on Windows.
+ if (gIsWindows) {
+ Assert.ok(Number.isFinite(osData["servicePackMajor"]),
+ "ServicePackMajor must be a number.");
+ Assert.ok(Number.isFinite(osData["servicePackMinor"]),
+ "ServicePackMinor must be a number.");
+ if ("windowsBuildNumber" in osData) {
+ // This might not be available on all Windows platforms.
+ Assert.ok(Number.isFinite(osData["windowsBuildNumber"]),
+ "windowsBuildNumber must be a number.");
+ }
+ if ("windowsUBR" in osData) {
+ // This might not be available on all Windows platforms.
+ Assert.ok((osData["windowsUBR"] === null) || Number.isFinite(osData["windowsUBR"]),
+ "windowsUBR must be null or a number.");
+ }
+ } else if (gIsAndroid || gIsGonk) {
+ Assert.ok(checkNullOrString(osData.kernelVersion));
+ }
+
+ let check = gIsWindows ? checkString : checkNullOrString;
+ for (let disk of EXPECTED_HDD_FIELDS) {
+ Assert.ok(check(data.system.hdd[disk].model));
+ Assert.ok(check(data.system.hdd[disk].revision));
+ }
+
+ let gfxData = data.system.gfx;
+ Assert.ok("D2DEnabled" in gfxData);
+ Assert.ok("DWriteEnabled" in gfxData);
+ // DWriteVersion is disabled due to main thread jank and will be enabled
+ // again as part of bug 1154500.
+ // Assert.ok("DWriteVersion" in gfxData);
+ if (gIsWindows) {
+ Assert.equal(typeof gfxData.D2DEnabled, "boolean");
+ Assert.equal(typeof gfxData.DWriteEnabled, "boolean");
+ // As above, will be enabled again as part of bug 1154500.
+ // Assert.ok(checkString(gfxData.DWriteVersion));
+ }
+
+ Assert.ok("adapters" in gfxData);
+ Assert.ok(gfxData.adapters.length > 0, "There must be at least one GFX adapter.");
+ for (let adapter of gfxData.adapters) {
+ checkGfxAdapter(adapter);
+ }
+ Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean");
+ Assert.ok(gfxData.adapters[0].GPUActive, "The first GFX adapter must be active.");
+
+ Assert.ok(Array.isArray(gfxData.monitors));
+ if (gIsWindows || gIsMac) {
+ Assert.ok(gfxData.monitors.length >= 1, "There is at least one monitor.");
+ Assert.equal(typeof gfxData.monitors[0].screenWidth, "number");
+ Assert.equal(typeof gfxData.monitors[0].screenHeight, "number");
+ if (gIsWindows) {
+ Assert.equal(typeof gfxData.monitors[0].refreshRate, "number");
+ Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean");
+ }
+ if (gIsMac) {
+ Assert.equal(typeof gfxData.monitors[0].scale, "number");
+ }
+ }
+
+ Assert.equal(typeof gfxData.features, "object");
+ Assert.equal(typeof gfxData.features.compositor, "string");
+
+ try {
+ // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
+ // this test.
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
+
+ if (gIsWindows || gIsMac) {
+ Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID);
+ Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID);
+ }
+
+ let features = gfxInfo.getFeatures();
+ Assert.equal(features.compositor, gfxData.features.compositor);
+ Assert.equal(features.opengl, gfxData.features.opengl);
+ Assert.equal(features.webgl, gfxData.features.webgl);
+ }
+ catch (e) {}
+}
+
+function checkActiveAddon(data) {
+ let signedState = mozinfo.addon_signing ? "number" : "undefined";
+ // system add-ons have an undefined signState
+ if (data.isSystem)
+ signedState = "undefined";
+
+ const EXPECTED_ADDON_FIELDS_TYPES = {
+ blocklisted: "boolean",
+ name: "string",
+ userDisabled: "boolean",
+ appDisabled: "boolean",
+ version: "string",
+ scope: "number",
+ type: "string",
+ foreignInstall: "boolean",
+ hasBinaryComponents: "boolean",
+ installDay: "number",
+ updateDay: "number",
+ signedState: signedState,
+ isSystem: "boolean",
+ };
+
+ for (let f in EXPECTED_ADDON_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+ Assert.equal(typeof data[f], EXPECTED_ADDON_FIELDS_TYPES[f],
+ f + " must have the correct type.");
+ }
+
+ // We check "description" separately, as it can be null.
+ Assert.ok(checkNullOrString(data.description));
+}
+
+function checkPlugin(data) {
+ const EXPECTED_PLUGIN_FIELDS_TYPES = {
+ name: "string",
+ version: "string",
+ description: "string",
+ blocklisted: "boolean",
+ disabled: "boolean",
+ clicktoplay: "boolean",
+ updateDay: "number",
+ };
+
+ for (let f in EXPECTED_PLUGIN_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+ Assert.equal(typeof data[f], EXPECTED_PLUGIN_FIELDS_TYPES[f],
+ f + " must have the correct type.");
+ }
+
+ Assert.ok(Array.isArray(data.mimeTypes));
+ for (let type of data.mimeTypes) {
+ Assert.ok(checkString(type));
+ }
+}
+
+function checkTheme(data) {
+ // "hasBinaryComponents" is not available when testing.
+ const EXPECTED_THEME_FIELDS_TYPES = {
+ id: "string",
+ blocklisted: "boolean",
+ name: "string",
+ userDisabled: "boolean",
+ appDisabled: "boolean",
+ version: "string",
+ scope: "number",
+ foreignInstall: "boolean",
+ installDay: "number",
+ updateDay: "number",
+ };
+
+ for (let f in EXPECTED_THEME_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+ Assert.equal(typeof data[f], EXPECTED_THEME_FIELDS_TYPES[f],
+ f + " must have the correct type.");
+ }
+
+ // We check "description" separately, as it can be null.
+ Assert.ok(checkNullOrString(data.description));
+}
+
+function checkActiveGMPlugin(data) {
+ // GMP plugin version defaults to null until GMPDownloader runs to update it.
+ if (data.version) {
+ Assert.equal(typeof data.version, "string");
+ }
+ Assert.equal(typeof data.userDisabled, "boolean");
+ Assert.equal(typeof data.applyBackgroundUpdates, "number");
+}
+
+function checkAddonsSection(data, expectBrokenAddons) {
+ const EXPECTED_FIELDS = [
+ "activeAddons", "theme", "activePlugins", "activeGMPlugins", "activeExperiment",
+ "persona",
+ ];
+
+ Assert.ok("addons" in data, "There must be an addons section in Environment.");
+ for (let f of EXPECTED_FIELDS) {
+ Assert.ok(f in data.addons, f + " must be available.");
+ }
+
+ // Check the active addons, if available.
+ if (!expectBrokenAddons) {
+ let activeAddons = data.addons.activeAddons;
+ for (let addon in activeAddons) {
+ checkActiveAddon(activeAddons[addon]);
+ }
+ }
+
+ // Check "theme" structure.
+ if (Object.keys(data.addons.theme).length !== 0) {
+ checkTheme(data.addons.theme);
+ }
+
+ // Check the active plugins.
+ Assert.ok(Array.isArray(data.addons.activePlugins));
+ for (let plugin of data.addons.activePlugins) {
+ checkPlugin(plugin);
+ }
+
+ // Check active GMPlugins
+ let activeGMPlugins = data.addons.activeGMPlugins;
+ for (let gmPlugin in activeGMPlugins) {
+ checkActiveGMPlugin(activeGMPlugins[gmPlugin]);
+ }
+
+ // Check the active Experiment
+ let experiment = data.addons.activeExperiment;
+ if (Object.keys(experiment).length !== 0) {
+ Assert.ok(checkString(experiment.id));
+ Assert.ok(checkString(experiment.branch));
+ }
+
+ // Check persona
+ Assert.ok(checkNullOrString(data.addons.persona));
+}
+
+function checkEnvironmentData(data, isInitial = false, expectBrokenAddons = false) {
+ checkBuildSection(data);
+ checkSettingsSection(data);
+ checkProfileSection(data);
+ checkPartnerSection(data, isInitial);
+ checkSystemSection(data);
+ checkAddonsSection(data, expectBrokenAddons);
+}
+
+add_task(function* setup() {
+ // Load a custom manifest to provide search engine loading from JAR files.
+ do_load_manifest("chrome.manifest");
+ registerFakeSysInfo();
+ spoofGfxAdapter();
+ do_get_profile();
+
+ // The system add-on must be installed before AddonManager is started.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+ do_get_file("system.xpi").copyTo(distroDir, "tel-system-xpi@tests.mozilla.org.xpi");
+ let system_addon = FileUtils.File(distroDir.path);
+ system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
+ system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
+ loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+
+ // Spoof the persona ID, but not on Gonk.
+ if (!gIsGonk) {
+ LightweightThemeManager.currentTheme =
+ spoofTheme(PERSONA_ID, PERSONA_NAME, PERSONA_DESCRIPTION);
+ }
+ // Register a fake plugin host for consistent flash version data.
+ registerFakePluginHost();
+
+ // Setup a webserver to serve Addons, Plugins, etc.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ let port = gHttpServer.identity.primaryPort;
+ gHttpRoot = "http://localhost:" + port + "/";
+ gDataRoot = gHttpRoot + "data/";
+ gHttpServer.registerDirectory("/data/", do_get_cwd());
+ do_register_cleanup(() => gHttpServer.stop(() => {}));
+
+ // Spoof the the hotfixVersion
+ Preferences.set("extensions.hotfix.lastVersion", APP_HOTFIX_VERSION);
+
+ // Create the attribution data file, so that settings.attribution will exist.
+ // The attribution functionality only exists in Firefox.
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ spoofAttributionData();
+ do_register_cleanup(cleanupAttributionData);
+ }
+
+ yield spoofProfileReset();
+ TelemetryEnvironment.delayedInit();
+});
+
+add_task(function* test_checkEnvironment() {
+ let environmentData = yield TelemetryEnvironment.onInitialized();
+ checkEnvironmentData(environmentData, true);
+
+ spoofPartnerInfo();
+ Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC, null);
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(environmentData);
+});
+
+add_task(function* test_prefWatchPolicies() {
+ const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
+ const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
+ const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
+ const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
+ const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart";
+
+ const expectedValue = "some-test-value";
+ const unexpectedValue = "unexpected-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST_1, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
+ [PREF_TEST_2, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
+ [PREF_TEST_3, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
+ [PREF_TEST_4, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
+ [PREF_TEST_5, {what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true}],
+ ]);
+
+ Preferences.set(PREF_TEST_4, expectedValue);
+ Preferences.set(PREF_TEST_5, expectedValue);
+
+ // Set the Environment preferences to watch.
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = PromiseUtils.defer();
+
+ // Check that the pref values are missing or present as expected
+ Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined);
+ Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], expectedValue);
+ Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5], expectedValue);
+
+ TelemetryEnvironment.registerChangeListener("testWatchPrefs",
+ (reason, data) => deferred.resolve(data));
+ let oldEnvironmentData = TelemetryEnvironment.currentEnvironment;
+
+ // Trigger a change in the watched preferences.
+ Preferences.set(PREF_TEST_1, expectedValue);
+ Preferences.set(PREF_TEST_2, false);
+ Preferences.set(PREF_TEST_5, unexpectedValue);
+ let eventEnvironmentData = yield deferred.promise;
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");
+
+ // Check environment contains the correct data.
+ Assert.deepEqual(oldEnvironmentData, eventEnvironmentData);
+ let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;
+
+ Assert.equal(userPrefs[PREF_TEST_1], expectedValue,
+ "Environment contains the correct preference value.");
+ Assert.equal(userPrefs[PREF_TEST_2], "<user-set>",
+ "Report that the pref was user set but the value is not shown.");
+ Assert.ok(!(PREF_TEST_3 in userPrefs),
+ "Do not report if preference not user set.");
+ Assert.equal(userPrefs[PREF_TEST_5], expectedValue,
+ "The pref value in the environment data should still be the same");
+});
+
+add_task(function* test_prefWatch_prefReset() {
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
+ ]);
+
+ // Set the preference to a non-default value.
+ Preferences.set(PREF_TEST, false);
+
+ // Set the Environment preferences to watch.
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("testWatchPrefs_reset", deferred.resolve);
+
+ Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "<user-set>");
+
+ // Trigger a change in the watched preferences.
+ Preferences.reset(PREF_TEST);
+ yield deferred.promise;
+
+ Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], undefined);
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset");
+});
+
+add_task(function* test_addonsWatch_InterestingChange() {
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
+ // We only expect a single notification for each install, uninstall, enable, disable.
+ const EXPECTED_NOTIFICATIONS = 4;
+
+ let receivedNotifications = 0;
+
+ let registerCheckpointPromise = (aExpected) => {
+ return new Promise(resolve => TelemetryEnvironment.registerChangeListener(
+ "testWatchAddons_Changes" + aExpected, (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ receivedNotifications++;
+ resolve();
+ }));
+ };
+
+ let assertCheckpoint = (aExpected) => {
+ Assert.equal(receivedNotifications, aExpected);
+ TelemetryEnvironment.unregisterChangeListener("testWatchAddons_Changes" + aExpected);
+ };
+
+ // Test for receiving one notification after each change.
+ let checkpointPromise = registerCheckpointPromise(1);
+ yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL);
+ yield checkpointPromise;
+ assertCheckpoint(1);
+ Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons);
+
+ checkpointPromise = registerCheckpointPromise(2);
+ let addon = yield AddonManagerTesting.getAddonById(ADDON_ID);
+ addon.userDisabled = true;
+ yield checkpointPromise;
+ assertCheckpoint(2);
+ Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
+
+ checkpointPromise = registerCheckpointPromise(3);
+ addon.userDisabled = false;
+ yield checkpointPromise;
+ assertCheckpoint(3);
+ Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons);
+
+ checkpointPromise = registerCheckpointPromise(4);
+ yield AddonManagerTesting.uninstallAddonByID(ADDON_ID);
+ yield checkpointPromise;
+ assertCheckpoint(4);
+ Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons));
+
+ Assert.equal(receivedNotifications, EXPECTED_NOTIFICATIONS,
+ "We must only receive the notifications we expect.");
+});
+
+add_task(function* test_pluginsWatch_Add() {
+ if (gIsAndroid) {
+ Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
+ return;
+ }
+
+ Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 1);
+
+ let newPlugin = new PluginTag(PLUGIN2_NAME, PLUGIN2_DESC, PLUGIN2_VERSION, true);
+ gInstalledPlugins.push(newPlugin);
+
+ let deferred = PromiseUtils.defer();
+ let receivedNotifications = 0;
+ let callback = (reason, data) => {
+ receivedNotifications++;
+ Assert.equal(reason, "addons-changed");
+ deferred.resolve();
+ };
+ TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback);
+
+ Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null);
+ yield deferred.promise;
+
+ Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 2);
+
+ TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add");
+
+ Assert.equal(receivedNotifications, 1, "We must only receive one notification.");
+});
+
+add_task(function* test_pluginsWatch_Remove() {
+ if (gIsAndroid) {
+ Assert.ok(true, "Skipping: there is no Plugin Manager on Android.");
+ return;
+ }
+
+ // Find the test plugin.
+ let plugin = gInstalledPlugins.find(p => (p.name == PLUGIN2_NAME));
+ Assert.ok(plugin, "The test plugin must exist.");
+
+ // Remove it from the PluginHost.
+ gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin);
+
+ let deferred = PromiseUtils.defer();
+ let receivedNotifications = 0;
+ let callback = () => {
+ receivedNotifications++;
+ deferred.resolve();
+ };
+ TelemetryEnvironment.registerChangeListener("testWatchPlugins_Remove", callback);
+
+ Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null);
+ yield deferred.promise;
+
+ TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove");
+
+ Assert.equal(receivedNotifications, 1, "We must only receive one notification.");
+});
+
+add_task(function* test_addonsWatch_NotInterestingChange() {
+ // We are not interested to dictionary addons changes.
+ const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi";
+ const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+
+ let receivedNotification = false;
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("testNotInteresting",
+ () => {
+ Assert.ok(!receivedNotification, "Should not receive multiple notifications");
+ receivedNotification = true;
+ deferred.resolve();
+ });
+
+ yield AddonManagerTesting.installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL);
+ yield AddonManagerTesting.installXPIFromURL(INTERESTING_ADDON_INSTALL_URL);
+
+ yield deferred.promise;
+ Assert.ok(!("telemetry-dictionary@tests.mozilla.org" in
+ TelemetryEnvironment.currentEnvironment.addons.activeAddons),
+ "Dictionaries should not appear in active addons.");
+
+ TelemetryEnvironment.unregisterChangeListener("testNotInteresting");
+});
+
+add_task(function* test_addonsAndPlugins() {
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A restartless addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Restartless Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+ isSystem: false,
+ };
+ const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org";
+ const EXPECTED_SYSTEM_ADDON_DATA = {
+ blocklisted: false,
+ description: "A system addon which is shipped with Firefox.",
+ name: "XPI Telemetry System Add-on Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
+ updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
+ signedState: undefined,
+ isSystem: true,
+ };
+
+ const EXPECTED_PLUGIN_DATA = {
+ name: FLASH_PLUGIN_NAME,
+ version: FLASH_PLUGIN_VERSION,
+ description: FLASH_PLUGIN_DESC,
+ blocklisted: false,
+ disabled: false,
+ clicktoplay: true,
+ };
+
+ // Install an addon so we have some data.
+ yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL);
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+
+ // Check addon data.
+ Assert.ok(ADDON_ID in data.addons.activeAddons, "We must have one active addon.");
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+ for (let f in EXPECTED_ADDON_DATA) {
+ Assert.equal(targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value.");
+ }
+
+ // Check system add-on data.
+ Assert.ok(SYSTEM_ADDON_ID in data.addons.activeAddons, "We must have one active system addon.");
+ let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID];
+ for (let f in EXPECTED_SYSTEM_ADDON_DATA) {
+ Assert.equal(targetSystemAddon[f], EXPECTED_SYSTEM_ADDON_DATA[f], f + " must have the correct value.");
+ }
+
+ // Check theme data.
+ let theme = data.addons.theme;
+ Assert.equal(theme.id, (PERSONA_ID + PERSONA_ID_SUFFIX));
+ Assert.equal(theme.name, PERSONA_NAME);
+ Assert.equal(theme.description, PERSONA_DESCRIPTION);
+
+ // Check plugin data.
+ Assert.equal(data.addons.activePlugins.length, 1, "We must have only one active plugin.");
+ let targetPlugin = data.addons.activePlugins[0];
+ for (let f in EXPECTED_PLUGIN_DATA) {
+ Assert.equal(targetPlugin[f], EXPECTED_PLUGIN_DATA[f], f + " must have the correct value.");
+ }
+
+ // Check plugin mime types.
+ Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1));
+ Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2));
+ Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There."));
+
+ let personaId = (gIsGonk) ? null : PERSONA_ID;
+ Assert.equal(data.addons.persona, personaId, "The correct Persona Id must be reported.");
+
+ // Uninstall the addon.
+ yield AddonManagerTesting.uninstallAddonByID(ADDON_ID);
+});
+
+add_task(function* test_signedAddon() {
+ const ADDON_INSTALL_URL = gDataRoot + "signed.xpi";
+ const ADDON_ID = "tel-signed-xpi@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A signed addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Signed Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+ };
+
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("test_signedAddon", deferred.resolve);
+
+ // Install the addon.
+ yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL);
+
+ yield deferred.promise;
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("test_signedAddon");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+
+ // Check addon data.
+ Assert.ok(ADDON_ID in data.addons.activeAddons, "Add-on should be in the environment.");
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+ for (let f in EXPECTED_ADDON_DATA) {
+ Assert.equal(targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value.");
+ }
+});
+
+add_task(function* test_addonsFieldsLimit() {
+ const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi";
+ const ADDON_ID = "tel-longfields-xpi@tests.mozilla.org";
+
+ // Install the addon and wait for the TelemetryEnvironment to pick it up.
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("test_longFieldsAddon", deferred.resolve);
+ yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL);
+ yield deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+
+ // Check that the addon is available and that the string fields are limited.
+ Assert.ok(ADDON_ID in data.addons.activeAddons, "Add-on should be in the environment.");
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+
+ // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars,
+ // to mitigate misbehaving addons.
+ Assert.lessOrEqual(targetAddon.version.length, 100,
+ "The version string must have been limited");
+ Assert.lessOrEqual(targetAddon.name.length, 100,
+ "The name string must have been limited");
+ Assert.lessOrEqual(targetAddon.description.length, 100,
+ "The description string must have been limited");
+});
+
+add_task(function* test_collectionWithbrokenAddonData() {
+ const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org";
+ const BROKEN_MANIFEST = {
+ id: "telemetry-test2.example.com@services.mozilla.org",
+ name: "telemetry broken addon",
+ origin: "https://telemetry-test2.example.com",
+ version: 1, // This is intentionally not a string.
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ };
+
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A restartless addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Restartless Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_MISSING :
+ AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+ };
+
+ let receivedNotifications = 0;
+
+ let registerCheckpointPromise = (aExpected) => {
+ return new Promise(resolve => TelemetryEnvironment.registerChangeListener(
+ "testBrokenAddon_collection" + aExpected, (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ receivedNotifications++;
+ resolve();
+ }));
+ };
+
+ let assertCheckpoint = (aExpected) => {
+ Assert.equal(receivedNotifications, aExpected);
+ TelemetryEnvironment.unregisterChangeListener("testBrokenAddon_collection" + aExpected);
+ };
+
+ // Register the broken provider and install the broken addon.
+ let checkpointPromise = registerCheckpointPromise(1);
+ let brokenAddonProvider = createMockAddonProvider("Broken Extensions Provider");
+ AddonManagerPrivate.registerProvider(brokenAddonProvider);
+ brokenAddonProvider.addAddon(BROKEN_MANIFEST);
+ yield checkpointPromise;
+ assertCheckpoint(1);
+
+ // Now install an addon which returns the correct information.
+ checkpointPromise = registerCheckpointPromise(2);
+ yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL);
+ yield checkpointPromise;
+ assertCheckpoint(2);
+
+ // Check that the new environment contains the Social addon installed with the broken
+ // manifest and the rest of the data.
+ let data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data, false, true /* expect broken addons*/);
+
+ let activeAddons = data.addons.activeAddons;
+ Assert.ok(BROKEN_ADDON_ID in activeAddons,
+ "The addon with the broken manifest must be reported.");
+ Assert.equal(activeAddons[BROKEN_ADDON_ID].version, null,
+ "null should be reported for invalid data.");
+ Assert.ok(ADDON_ID in activeAddons,
+ "The valid addon must be reported.");
+ Assert.equal(activeAddons[ADDON_ID].description, EXPECTED_ADDON_DATA.description,
+ "The description for the valid addon should be correct.");
+
+ // Unregister the broken provider so we don't mess with other tests.
+ AddonManagerPrivate.unregisterProvider(brokenAddonProvider);
+
+ // Uninstall the valid addon.
+ yield AddonManagerTesting.uninstallAddonByID(ADDON_ID);
+});
+
+add_task(function* test_defaultSearchEngine() {
+ // Check that no default engine is in the environment before the search service is
+ // initialized.
+ let data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+ Assert.ok(!("defaultSearchEngine" in data.settings));
+ Assert.ok(!("defaultSearchEngineData" in data.settings));
+
+ // Load the engines definitions from a custom JAR file: that's needed so that
+ // the search provider reports an engine identifier.
+ let url = "chrome://testsearchplugin/locale/searchplugins/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ // Initialize the search service.
+ yield new Promise(resolve => Services.search.init(resolve));
+
+ // Our default engine from the JAR file has an identifier. Check if it is correctly
+ // reported.
+ data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier");
+ let expectedSearchEngineData = {
+ name: "telemetrySearchIdentifier",
+ loadPath: "jar:[other]/searchTest.jar!testsearchplugin/telemetrySearchIdentifier.xml",
+ origin: "default",
+ submissionURL: "http://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceid=Mozilla-search"
+ };
+ Assert.deepEqual(data.settings.defaultSearchEngineData, expectedSearchEngineData);
+
+ // Remove all the search engines.
+ for (let engine of Services.search.getEngines()) {
+ Services.search.removeEngine(engine);
+ }
+ // The search service does not notify "engine-current" when removing a default engine.
+ // Manually force the notification.
+ // TODO: remove this when bug 1165341 is resolved.
+ Services.obs.notifyObservers(null, "browser-search-engine-modified", "engine-current");
+
+ // Then check that no default engine is reported if none is available.
+ data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, "NONE");
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {name:"NONE"});
+
+ // Add a new search engine (this will have no engine identifier).
+ const SEARCH_ENGINE_ID = "telemetry_default";
+ const SEARCH_ENGINE_URL = "http://www.example.org/?search={searchTerms}";
+ Services.search.addEngineWithDetails(SEARCH_ENGINE_ID, "", null, "", "get", SEARCH_ENGINE_URL);
+
+ // Register a new change listener and then wait for the search engine change to be notified.
+ let deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("testWatch_SearchDefault", deferred.resolve);
+ Services.search.defaultEngine = Services.search.getEngineByName(SEARCH_ENGINE_ID);
+ yield deferred.promise;
+
+ data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+
+ const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+
+ const EXPECTED_SEARCH_ENGINE_DATA = {
+ name: "telemetry_default",
+ loadPath: "[other]addEngineWithDetails",
+ origin: "verified"
+ };
+ Assert.deepEqual(data.settings.defaultSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA);
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+
+ // Cleanly install an engine from an xml file, and check if origin is
+ // recorded as "verified".
+ let promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener("testWatch_SearchDefault", resolve);
+ });
+ let engine = yield new Promise((resolve, reject) => {
+ Services.obs.addObserver(function obs(obsSubject, obsTopic, obsData) {
+ try {
+ let searchEngine = obsSubject.QueryInterface(Ci.nsISearchEngine);
+ do_print("Observed " + obsData + " for " + searchEngine.name);
+ if (obsData != "engine-added" || searchEngine.name != "engine-telemetry") {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ resolve(searchEngine);
+ } catch (ex) {
+ reject(ex);
+ }
+ }, "browser-search-engine-modified", false);
+ Services.search.addEngine("file://" + do_get_cwd().path + "/engine.xml",
+ null, null, false);
+ });
+ Services.search.defaultEngine = engine;
+ yield promise;
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+ data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData,
+ {"name":"engine-telemetry", "loadPath":"[other]/engine.xml", "origin":"verified"});
+
+ // Now break this engine's load path hash.
+ promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener("testWatch_SearchDefault", resolve);
+ });
+ engine.wrappedJSObject.setAttr("loadPathHash", "broken");
+ Services.obs.notifyObservers(null, "browser-search-engine-modified", "engine-current");
+ yield promise;
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid");
+ Services.search.removeEngine(engine);
+
+ // Define and reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Watch the test preference.
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ deferred = PromiseUtils.defer();
+ TelemetryEnvironment.registerChangeListener("testSearchEngine_pref", deferred.resolve);
+ // Trigger an environment change.
+ Preferences.set(PREF_TEST, 1);
+ yield deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref");
+
+ // Check that the search engine information is correctly retained when prefs change.
+ data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+
+ // Check that by default we are not sending a cohort identifier...
+ Assert.equal(data.settings.searchCohort, undefined);
+
+ // ... but that if a cohort identifier is set, we send it.
+ Services.prefs.setCharPref("browser.search.cohort", "testcohort");
+ Services.obs.notifyObservers(null, "browser-search-service", "init-complete");
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(data.settings.searchCohort, "testcohort");
+});
+
+add_task(function* test_osstrings() {
+ // First test that numbers in sysinfo properties are converted to string fields
+ // in system.os.
+ SysInfo.overrides = {
+ version: 1,
+ name: 2,
+ kernel_version: 3,
+ };
+
+ yield TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+
+ Assert.equal(data.system.os.version, "1");
+ Assert.equal(data.system.os.name, "2");
+ if (AppConstants.platform == "android") {
+ Assert.equal(data.system.os.kernelVersion, "3");
+ }
+
+ // Check that null values are also handled.
+ SysInfo.overrides = {
+ version: null,
+ name: null,
+ kernel_version: null,
+ };
+
+ yield TelemetryEnvironment.testCleanRestart().onInitialized();
+ data = TelemetryEnvironment.currentEnvironment;
+ checkEnvironmentData(data);
+
+ Assert.equal(data.system.os.version, null);
+ Assert.equal(data.system.os.name, null);
+ if (AppConstants.platform == "android") {
+ Assert.equal(data.system.os.kernelVersion, null);
+ }
+
+ // Clean up.
+ SysInfo.overrides = {};
+ yield TelemetryEnvironment.testCleanRestart().onInitialized();
+});
+
+add_task(function* test_environmentShutdown() {
+ // Define and reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ // Set up the preferences and listener, then the trigger shutdown
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ TelemetryEnvironment.registerChangeListener("test_environmentShutdownChange", () => {
+ // Register a new change listener that asserts if change is propogated
+ Assert.ok(false, "No change should be propagated after shutdown.");
+ });
+ TelemetryEnvironment.shutdown();
+
+ // Flipping the test preference after shutdown should not trigger the listener
+ Preferences.set(PREF_TEST, 1);
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("test_environmentShutdownChange");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
new file mode 100644
index 0000000000..2bfb62c147
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
@@ -0,0 +1,249 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const OPTIN = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN;
+const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+
+function checkEventFormat(events) {
+ Assert.ok(Array.isArray(events), "Events should be serialized to an array.");
+ for (let e of events) {
+ Assert.ok(Array.isArray(e), "Event should be an array.");
+ Assert.greaterOrEqual(e.length, 4, "Event should have at least 4 elements.");
+ Assert.lessOrEqual(e.length, 6, "Event should have at most 6 elements.");
+
+ Assert.equal(typeof(e[0]), "number", "Element 0 should be a number.");
+ Assert.equal(typeof(e[1]), "string", "Element 1 should be a string.");
+ Assert.equal(typeof(e[2]), "string", "Element 2 should be a string.");
+ Assert.equal(typeof(e[3]), "string", "Element 3 should be a string.");
+
+ if (e.length > 4) {
+ Assert.ok(e[4] === null || typeof(e[4]) == "string",
+ "Event element 4 should be null or a string.");
+ }
+ if (e.length > 5) {
+ Assert.ok(e[5] === null || typeof(e[5]) == "object",
+ "Event element 5 should be null or an object.");
+ }
+
+ let extra = e[5];
+ if (extra) {
+ Assert.ok(Object.keys(extra).every(k => typeof(k) == "string"),
+ "All extra keys should be strings.");
+ Assert.ok(Object.values(extra).every(v => typeof(v) == "string"),
+ "All extra values should be strings.");
+ }
+ }
+}
+
+add_task(function* test_recording() {
+ Telemetry.clearEvents();
+
+ // Record some events.
+ let expected = [
+ {optout: false, event: ["telemetry.test", "test1", "object1"]},
+ {optout: false, event: ["telemetry.test", "test2", "object2"]},
+
+ {optout: false, event: ["telemetry.test", "test1", "object1", "value"]},
+ {optout: false, event: ["telemetry.test", "test1", "object1", "value", null]},
+ {optout: false, event: ["telemetry.test", "test1", "object1", null, {"key1": "value1"}]},
+ {optout: false, event: ["telemetry.test", "test1", "object1", "value", {"key1": "value1", "key2": "value2"}]},
+
+ {optout: true, event: ["telemetry.test", "optout", "object1"]},
+ {optout: false, event: ["telemetry.test.second", "test", "object1"]},
+ {optout: false, event: ["telemetry.test.second", "test", "object1", null, {"key1": "value1"}]},
+ ];
+
+ for (let entry of expected) {
+ entry.tsBefore = Math.floor(Telemetry.msSinceProcessStart());
+ try {
+ Telemetry.recordEvent(...entry.event);
+ } catch (ex) {
+ Assert.ok(false, `Failed to record event ${JSON.stringify(entry.event)}: ${ex}`);
+ }
+ entry.tsAfter = Math.floor(Telemetry.msSinceProcessStart());
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let entry of expected) {
+ let e = entry.event;
+ while ((e.length >= 3) && (e[e.length - 1] === null)) {
+ e.pop();
+ }
+ }
+
+ // The following should not result in any recorded events.
+ Assert.throws(() => Telemetry.recordEvent("unknown.category", "test1", "object1"),
+ /Error: Unknown event: \["unknown.category", "test1", "object1"\]/,
+ "Should throw on unknown category.");
+ Assert.throws(() => Telemetry.recordEvent("telemetry.test", "unknown", "object1"),
+ /Error: Unknown event: \["telemetry.test", "unknown", "object1"\]/,
+ "Should throw on unknown method.");
+ Assert.throws(() => Telemetry.recordEvent("telemetry.test", "test1", "unknown"),
+ /Error: Unknown event: \["telemetry.test", "test1", "unknown"\]/,
+ "Should throw on unknown object.");
+
+ let checkEvents = (events, expectedEvents) => {
+ checkEventFormat(events);
+ Assert.equal(events.length, expectedEvents.length,
+ "Snapshot should have the right number of events.");
+
+ for (let i = 0; i < events.length; ++i) {
+ let {tsBefore, tsAfter} = expectedEvents[i];
+ let ts = events[i][0];
+ Assert.greaterOrEqual(ts, tsBefore, "The recorded timestamp should be greater than the one before recording.");
+ Assert.lessOrEqual(ts, tsAfter, "The recorded timestamp should be less than the one after recording.");
+
+ let recordedData = events[i].slice(1);
+ let expectedData = expectedEvents[i].event.slice();
+ Assert.deepEqual(recordedData, expectedData, "The recorded event data should match.");
+ }
+ };
+
+ // Check that the expected events were recorded.
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, false);
+ checkEvents(events, expected);
+
+ // Check serializing only opt-out events.
+ events = Telemetry.snapshotBuiltinEvents(OPTOUT, false);
+ filtered = expected.filter(e => e.optout == true);
+ checkEvents(events, filtered);
+});
+
+add_task(function* test_clear() {
+ Telemetry.clearEvents();
+
+ const COUNT = 10;
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.second", "test", "object1");
+ }
+
+ // Check that events were recorded.
+ // The events are cleared by passing the respective flag.
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 2 * COUNT, `Should have recorded ${2 * COUNT} events.`);
+
+ // Now the events should be cleared.
+ events = Telemetry.snapshotBuiltinEvents(OPTIN, false);
+ Assert.equal(events.length, 0, `Should have cleared the events.`);
+});
+
+add_task(function* test_expiry() {
+ Telemetry.clearEvents();
+
+ // Recording call with event that is expired by version.
+ Telemetry.recordEvent("telemetry.test", "expired_version", "object1");
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 0, "Should not record event with expired version.");
+
+ // Recording call with event that is expired by date.
+ Telemetry.recordEvent("telemetry.test", "expired_date", "object1");
+ events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 0, "Should not record event with expired date.");
+
+ // Recording call with event that has expiry_version and expiry_date in the future.
+ Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1");
+ events = Telemetry.snapshotBuiltinEvents(OPTOUT, true);
+ Assert.equal(events.length, 1, "Should record event when date and version are not expired.");
+});
+
+add_task(function* test_invalidParams() {
+ Telemetry.clearEvents();
+
+ // Recording call with wrong type for value argument.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", 1);
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 0, "Should not record event when value argument with invalid type is passed.");
+
+ // Recording call with wrong type for extra argument.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid");
+ events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 0, "Should not record event when extra argument with invalid type is passed.");
+
+ // Recording call with unknown extra key.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key3": "x"});
+ events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 0, "Should not record event when extra argument with invalid key is passed.");
+
+ // Recording call with invalid value type.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key3": 1});
+ events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 0, "Should not record event when extra argument with invalid value type is passed.");
+});
+
+add_task(function* test_storageLimit() {
+ Telemetry.clearEvents();
+
+ // Record more events than the storage limit allows.
+ let LIMIT = 1000;
+ let COUNT = LIMIT + 10;
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i));
+ }
+
+ // Check that the right events were recorded.
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, LIMIT, `Should have only recorded ${LIMIT} events`);
+ Assert.ok(events.every((e, idx) => e[4] === String(idx)),
+ "Should have recorded all events from before hitting the limit.");
+});
+
+add_task(function* test_valueLimits() {
+ Telemetry.clearEvents();
+
+ // Record values that are at or over the limits for string lengths.
+ let LIMIT = 80;
+ let expected = [
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT - 10), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT ), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 1), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 10), null],
+
+ ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT - 10)}],
+ ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT )}],
+ ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT + 1)}],
+ ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT + 10)}],
+ ];
+
+ for (let event of expected) {
+ Telemetry.recordEvent(...event);
+ if (event[3]) {
+ event[3] = event[3].substr(0, LIMIT);
+ }
+ if (event[4]) {
+ event[4].key1 = event[4].key1.substr(0, LIMIT);
+ }
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let e of expected) {
+ while ((e.length >= 3) && (e[e.length - 1] === null)) {
+ e.pop();
+ }
+ }
+
+ // Check that the right events were recorded.
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, expected.length,
+ "Should have recorded the expected number of events");
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(events[i].slice(1), expected[i],
+ "Should have recorded the expected event data.");
+ }
+});
+
+add_task(function* test_unicodeValues() {
+ Telemetry.clearEvents();
+
+ // Record string values containing unicode characters.
+ let value = "漢語";
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", value);
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key1": value});
+
+ // Check that the values were correctly recorded.
+ let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+ Assert.equal(events.length, 2, "Should have recorded 2 events.");
+ Assert.equal(events[0][4], value, "Should have recorded the right value.");
+ Assert.equal(events[1][5]["key1"], value, "Should have recorded the right extra value.");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js
new file mode 100644
index 0000000000..712aceb3bd
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test()
+{
+ let testFlag = Services.telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ equal(JSON.stringify(testFlag.snapshot().counts), "[1,0,0]", "Original value is correct");
+ testFlag.add(1);
+ equal(JSON.stringify(testFlag.snapshot().counts), "[0,1,0]", "Value is correct after ping.");
+ testFlag.clear();
+ equal(JSON.stringify(testFlag.snapshot().counts), "[1,0,0]", "Value is correct after calling clear()");
+ testFlag.add(1);
+ equal(JSON.stringify(testFlag.snapshot().counts), "[0,1,0]", "Value is correct after ping.");
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
new file mode 100644
index 0000000000..f2b2b3bba4
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* A testcase to make sure reading late writes stacks works. */
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const STACK_SUFFIX1 = "stack1.txt";
+const STACK_SUFFIX2 = "stack2.txt";
+const STACK_BOGUS_SUFFIX = "bogus.txt";
+const LATE_WRITE_PREFIX = "Telemetry.LateWriteFinal-";
+
+// The names and IDs don't matter, but the format of the IDs does.
+const LOADED_MODULES = {
+ '4759A7E6993548C89CAF716A67EC242D00': 'libtest.so',
+ 'F77AF15BB8D6419FA875954B4A3506CA00': 'libxul.so',
+ '1E2F7FB590424E8F93D60BB88D66B8C500': 'libc.so'
+};
+const N_MODULES = Object.keys(LOADED_MODULES).length;
+
+// Format of individual items is [index, offset-in-library].
+const STACK1 = [
+ [ 0, 0 ],
+ [ 1, 1 ],
+ [ 2, 2 ]
+];
+const STACK2 = [
+ [ 0, 0 ],
+ [ 1, 5 ],
+ [ 2, 10 ],
+];
+// XXX The only error checking is for a zero-sized stack.
+const STACK_BOGUS = [];
+
+function write_string_to_file(file, contents) {
+ let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER, ostream.DEFER_OPEN);
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function construct_file(suffix) {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append(LATE_WRITE_PREFIX + suffix);
+ return file;
+}
+
+function write_late_writes_file(stack, suffix)
+{
+ let file = construct_file(suffix);
+ let contents = N_MODULES + "\n";
+ for (let id in LOADED_MODULES) {
+ contents += id + " " + LOADED_MODULES[id] + "\n";
+ }
+
+ contents += stack.length + "\n";
+ for (let element of stack) {
+ contents += element[0] + " " + element[1].toString(16) + "\n";
+ }
+
+ write_string_to_file(file, contents);
+}
+
+function run_test() {
+ do_get_profile();
+
+ write_late_writes_file(STACK1, STACK_SUFFIX1);
+ write_late_writes_file(STACK2, STACK_SUFFIX2);
+ write_late_writes_file(STACK_BOGUS, STACK_BOGUS_SUFFIX);
+
+ let lateWrites = Telemetry.lateWrites;
+ do_check_true("memoryMap" in lateWrites);
+ do_check_eq(lateWrites.memoryMap.length, 0);
+ do_check_true("stacks" in lateWrites);
+ do_check_eq(lateWrites.stacks.length, 0);
+
+ do_test_pending();
+ Telemetry.asyncFetchTelemetryData(function () {
+ actual_test();
+ });
+}
+
+function actual_test() {
+ do_check_false(construct_file(STACK_SUFFIX1).exists());
+ do_check_false(construct_file(STACK_SUFFIX2).exists());
+ do_check_false(construct_file(STACK_BOGUS_SUFFIX).exists());
+
+ let lateWrites = Telemetry.lateWrites;
+
+ do_check_true("memoryMap" in lateWrites);
+ do_check_eq(lateWrites.memoryMap.length, N_MODULES);
+ for (let id in LOADED_MODULES) {
+ let matchingLibrary = lateWrites.memoryMap.filter(function(library, idx, array) {
+ return library[1] == id;
+ });
+ do_check_eq(matchingLibrary.length, 1);
+ let library = matchingLibrary[0]
+ let name = library[0];
+ do_check_eq(LOADED_MODULES[id], name);
+ }
+
+ do_check_true("stacks" in lateWrites);
+ do_check_eq(lateWrites.stacks.length, 2);
+ let uneval_STACKS = [uneval(STACK1), uneval(STACK2)];
+ let first_stack = lateWrites.stacks[0];
+ let second_stack = lateWrites.stacks[1];
+ function stackChecker(canonicalStack) {
+ let unevalCanonicalStack = uneval(canonicalStack);
+ return function(obj, idx, array) {
+ return unevalCanonicalStack == obj;
+ }
+ }
+ do_check_eq(uneval_STACKS.filter(stackChecker(first_stack)).length, 1);
+ do_check_eq(uneval_STACKS.filter(stackChecker(second_stack)).length, 1);
+
+ do_test_finished();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
new file mode 100644
index 0000000000..808f2f3ec9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* A testcase to make sure reading the failed profile lock count works. */
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+const LOCK_FILE_NAME = "Telemetry.FailedProfileLocks.txt";
+const N_FAILED_LOCKS = 10;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+function write_string_to_file(file, contents) {
+ let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER, ostream.DEFER_OPEN);
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function construct_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append(LOCK_FILE_NAME);
+ return file;
+}
+
+function run_test() {
+ do_get_profile();
+
+ do_check_eq(Telemetry.failedProfileLockCount, 0);
+
+ write_string_to_file(construct_file(), N_FAILED_LOCKS.toString());
+
+ // Make sure that we're not eagerly reading the count now that the
+ // file exists.
+ do_check_eq(Telemetry.failedProfileLockCount, 0);
+
+ do_test_pending();
+ Telemetry.asyncFetchTelemetryData(actual_test);
+}
+
+function actual_test() {
+ do_check_eq(Telemetry.failedProfileLockCount, N_FAILED_LOCKS);
+ do_check_false(construct_file().exists());
+ do_test_finished();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js
new file mode 100644
index 0000000000..ea37a1bc51
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/TelemetryLog.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+
+const TEST_PREFIX = "TEST-";
+const TEST_REGEX = new RegExp("^" + TEST_PREFIX);
+
+function check_event(event, id, data)
+{
+ do_print("Checking message " + id);
+ do_check_eq(event[0], id);
+ do_check_true(event[1] > 0);
+
+ if (data === undefined) {
+ do_check_true(event.length == 2);
+ } else {
+ do_check_eq(event.length, data.length + 2);
+ for (var i = 0; i < data.length; ++i) {
+ do_check_eq(typeof(event[i + 2]), "string");
+ do_check_eq(event[i + 2], data[i]);
+ }
+ }
+}
+
+add_task(function* ()
+{
+ do_get_profile();
+ // TODO: After Bug 1254550 lands we should not need to set the pref here.
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ yield TelemetryController.testSetup();
+
+ TelemetryLog.log(TEST_PREFIX + "1", ["val", 123, undefined]);
+ TelemetryLog.log(TEST_PREFIX + "2", []);
+ TelemetryLog.log(TEST_PREFIX + "3");
+
+ var log = TelemetrySession.getPayload().log.filter(function(e) {
+ // Only want events that were generated by the test.
+ return TEST_REGEX.test(e[0]);
+ });
+
+ do_check_eq(log.length, 3);
+ check_event(log[0], TEST_PREFIX + "1", ["val", "123", "undefined"]);
+ check_event(log[1], TEST_PREFIX + "2", []);
+ check_event(log[2], TEST_PREFIX + "3", undefined);
+ do_check_true(log[0][1] <= log[1][1]);
+ do_check_true(log[1][1] <= log[2][1]);
+
+ yield TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
new file mode 100644
index 0000000000..68606a98e3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
@@ -0,0 +1,268 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that TelemetryController sends close to shutdown don't lead
+// to AsyncShutdown timeouts.
+
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/UpdateUtils.jsm", this);
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_SERVER = PREF_BRANCH + "server";
+
+const TEST_CHANNEL = "TestChannelABC";
+
+const PREF_POLICY_BRANCH = "datareporting.policy.";
+const PREF_BYPASS_NOTIFICATION = PREF_POLICY_BRANCH + "dataSubmissionPolicyBypassNotification";
+const PREF_DATA_SUBMISSION_ENABLED = PREF_POLICY_BRANCH + "dataSubmissionEnabled";
+const PREF_CURRENT_POLICY_VERSION = PREF_POLICY_BRANCH + "currentPolicyVersion";
+const PREF_MINIMUM_POLICY_VERSION = PREF_POLICY_BRANCH + "minimumPolicyVersion";
+const PREF_MINIMUM_CHANNEL_POLICY_VERSION = PREF_MINIMUM_POLICY_VERSION + ".channel-" + TEST_CHANNEL;
+const PREF_ACCEPTED_POLICY_VERSION = PREF_POLICY_BRANCH + "dataSubmissionPolicyAcceptedVersion";
+const PREF_ACCEPTED_POLICY_DATE = PREF_POLICY_BRANCH + "dataSubmissionPolicyNotifiedTime";
+
+function fakeShowPolicyTimeout(set, clear) {
+ let reportingPolicy = Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm");
+ reportingPolicy.Policy.setShowInfobarTimeout = set;
+ reportingPolicy.Policy.clearShowInfobarTimeout = clear;
+}
+
+function fakeResetAcceptedPolicy() {
+ Preferences.reset(PREF_ACCEPTED_POLICY_DATE);
+ Preferences.reset(PREF_ACCEPTED_POLICY_VERSION);
+}
+
+function setMinimumPolicyVersion(aNewPolicyVersion) {
+ const CHANNEL_NAME = UpdateUtils.getUpdateChannel(false);
+ // We might have channel-dependent minimum policy versions.
+ const CHANNEL_DEPENDENT_PREF = PREF_MINIMUM_POLICY_VERSION + ".channel-" + CHANNEL_NAME;
+
+ // Does the channel-dependent pref exist? If so, set its value.
+ if (Preferences.get(CHANNEL_DEPENDENT_PREF, undefined)) {
+ Preferences.set(CHANNEL_DEPENDENT_PREF, aNewPolicyVersion);
+ return;
+ }
+
+ // We don't have a channel specific minimu, so set the common one.
+ Preferences.set(PREF_MINIMUM_POLICY_VERSION, aNewPolicyVersion);
+}
+
+add_task(function* test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile(true);
+ loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ // Don't bypass the notifications in this test, we'll fake it.
+ Services.prefs.setBoolPref(PREF_BYPASS_NOTIFICATION, false);
+
+ TelemetryReportingPolicy.setup();
+});
+
+add_task(function* test_firstRun() {
+ const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun";
+ const FIRST_RUN_TIMEOUT_MSEC = 60 * 1000; // 60s
+ const OTHER_RUNS_TIMEOUT_MSEC = 10 * 1000; // 10s
+
+ Preferences.reset(PREF_FIRST_RUN);
+
+ let startupTimeout = 0;
+ fakeShowPolicyTimeout((callback, timeout) => startupTimeout = timeout, () => {});
+ TelemetryReportingPolicy.reset();
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", null);
+ Assert.equal(startupTimeout, FIRST_RUN_TIMEOUT_MSEC,
+ "The infobar display timeout should be 60s on the first run.");
+
+ // Run again, and check that we actually wait only 10 seconds.
+ TelemetryReportingPolicy.reset();
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", null);
+ Assert.equal(startupTimeout, OTHER_RUNS_TIMEOUT_MSEC,
+ "The infobar display timeout should be 10s on other runs.");
+});
+
+add_task(function* test_prefs() {
+ TelemetryReportingPolicy.reset();
+
+ let now = fakeNow(2009, 11, 18);
+
+ // If the date is not valid (earlier than 2012), we don't regard the policy as accepted.
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified());
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 0,
+ "Invalid dates should not make the policy accepted.");
+
+ // Check that the notification date and version are correctly saved to the prefs.
+ now = fakeNow(2012, 11, 18);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), now.getTime(),
+ "A valid date must correctly be saved.");
+
+ // Now that user is notified, check if we are allowed to upload.
+ Assert.ok(TelemetryReportingPolicy.canUpload(),
+ "We must be able to upload after the policy is accepted.");
+
+ // Disable submission and check that we're no longer allowed to upload.
+ Preferences.set(PREF_DATA_SUBMISSION_ENABLED, false);
+ Assert.ok(!TelemetryReportingPolicy.canUpload(),
+ "We must not be able to upload if data submission is disabled.");
+
+ // Turn the submission back on.
+ Preferences.set(PREF_DATA_SUBMISSION_ENABLED, true);
+ Assert.ok(TelemetryReportingPolicy.canUpload(),
+ "We must be able to upload if data submission is enabled and the policy was accepted.");
+
+ // Set a new minimum policy version and check that user is no longer notified.
+ let newMinimum = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1) + 1;
+ setMinimumPolicyVersion(newMinimum);
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "A greater minimum policy version must invalidate the policy and disable upload.");
+
+ // Eventually accept the policy and make sure user is notified.
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, newMinimum);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(TelemetryReportingPolicy.testIsUserNotified(),
+ "Accepting the policy again should show the user as notified.");
+ Assert.ok(TelemetryReportingPolicy.canUpload(),
+ "Accepting the policy again should let us upload data.");
+
+ // Set a new, per channel, minimum policy version. Start by setting a test current channel.
+ let defaultPrefs = new Preferences({ defaultBranch: true });
+ defaultPrefs.set("app.update.channel", TEST_CHANNEL);
+
+ // Increase and set the new minimum version, then check that we're not notified anymore.
+ newMinimum++;
+ Preferences.set(PREF_MINIMUM_CHANNEL_POLICY_VERSION, newMinimum);
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "Increasing the minimum policy version should invalidate the policy.");
+
+ // Eventually accept the policy and make sure user is notified.
+ Preferences.set(PREF_CURRENT_POLICY_VERSION, newMinimum);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(TelemetryReportingPolicy.testIsUserNotified(),
+ "Accepting the policy again should show the user as notified.");
+ Assert.ok(TelemetryReportingPolicy.canUpload(),
+ "Accepting the policy again should let us upload data.");
+});
+
+add_task(function* test_migratePrefs() {
+ const DEPRECATED_FHR_PREFS = {
+ "datareporting.policy.dataSubmissionPolicyAccepted": true,
+ "datareporting.policy.dataSubmissionPolicyBypassAcceptance": true,
+ "datareporting.policy.dataSubmissionPolicyResponseType": "foxyeah",
+ "datareporting.policy.dataSubmissionPolicyResponseTime": Date.now().toString(),
+ };
+
+ // Make sure the preferences are set before setting up the policy.
+ for (let name in DEPRECATED_FHR_PREFS) {
+ Preferences.set(name, DEPRECATED_FHR_PREFS[name]);
+ }
+ // Set up the policy.
+ TelemetryReportingPolicy.reset();
+ // They should have been removed by now.
+ for (let name in DEPRECATED_FHR_PREFS) {
+ Assert.ok(!Preferences.has(name), name + " should have been removed.");
+ }
+});
+
+add_task(function* test_userNotifiedOfCurrentPolicy() {
+ fakeResetAcceptedPolicy();
+ TelemetryReportingPolicy.reset();
+
+ // User should be reported as not notified by default.
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "The initial state should be unnotified.");
+
+ // Forcing a policy version should not automatically make the user notified.
+ Preferences.set(PREF_ACCEPTED_POLICY_VERSION,
+ TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION);
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "The default state of the date should have a time of 0 and it should therefore fail");
+
+ // Showing the notification bar should make the user notified.
+ fakeNow(2012, 11, 11);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(TelemetryReportingPolicy.testIsUserNotified(),
+ "Using the proper API causes user notification to report as true.");
+
+ // It is assumed that later versions of the policy will incorporate previous
+ // ones, therefore this should also return true.
+ let newVersion =
+ Preferences.get(PREF_CURRENT_POLICY_VERSION, 1) + 1;
+ Preferences.set(PREF_ACCEPTED_POLICY_VERSION, newVersion);
+ Assert.ok(TelemetryReportingPolicy.testIsUserNotified(),
+ "A future version of the policy should pass.");
+
+ newVersion =
+ Preferences.get(PREF_CURRENT_POLICY_VERSION, 1) - 1;
+ Preferences.set(PREF_ACCEPTED_POLICY_VERSION, newVersion);
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "A previous version of the policy should fail.");
+});
+
+add_task(function* test_canSend() {
+ const TEST_PING_TYPE = "test-ping";
+
+ PingServer.start();
+ Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port);
+
+ yield TelemetryController.testReset();
+ TelemetryReportingPolicy.reset();
+
+ // User should be reported as not notified by default.
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
+ "The initial state should be unnotified.");
+
+ // Assert if we receive any ping before the policy is accepted.
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Reset the ping handler.
+ PingServer.resetPingHandler();
+
+ // Fake the infobar: this should also trigger the ping send task.
+ TelemetryReportingPolicy.testInfobarShown();
+ let ping = yield PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(ping[0].type, TEST_PING_TYPE,
+ "We should have received the previous ping.");
+
+ // Submit another ping, to make sure it gets sent.
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Get the ping and check its type.
+ ping = yield PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the new ping.");
+
+ // Fake a restart with a pending ping.
+ yield TelemetryController.addPendingPing(TEST_PING_TYPE, {});
+ yield TelemetryController.testReset();
+
+ // We should be immediately sending the ping out.
+ ping = yield PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the pending ping.");
+
+ // Submit another ping, to make sure it gets sent.
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Get the ping and check its type.
+ ping = yield PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the new ping.");
+
+ yield PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
new file mode 100644
index 0000000000..5914a42356
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
@@ -0,0 +1,574 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+const STRING_SCALAR = "telemetry.test.string_kind";
+const BOOLEAN_SCALAR = "telemetry.test.boolean_kind";
+const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int";
+
+add_task(function* test_serializationFormat() {
+ Telemetry.clearScalars();
+
+ // Set the scalars to a known value.
+ const expectedUint = 3785;
+ const expectedString = "some value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, true);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "first_key", 1234);
+
+ // Get a snapshot of the scalars.
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(typeof(scalars[UINT_SCALAR]), "number",
+ UINT_SCALAR + " must be serialized to the correct format.");
+ Assert.ok(Number.isInteger(scalars[UINT_SCALAR]),
+ UINT_SCALAR + " must be a finite integer.");
+ Assert.equal(scalars[UINT_SCALAR], expectedUint,
+ UINT_SCALAR + " must have the correct value.");
+ Assert.equal(typeof(scalars[STRING_SCALAR]), "string",
+ STRING_SCALAR + " must be serialized to the correct format.");
+ Assert.equal(scalars[STRING_SCALAR], expectedString,
+ STRING_SCALAR + " must have the correct value.");
+ Assert.equal(typeof(scalars[BOOLEAN_SCALAR]), "boolean",
+ BOOLEAN_SCALAR + " must be serialized to the correct format.");
+ Assert.equal(scalars[BOOLEAN_SCALAR], true,
+ BOOLEAN_SCALAR + " must have the correct value.");
+ Assert.ok(!(KEYED_UINT_SCALAR in scalars),
+ "Keyed scalars must be reported in a separate section.");
+});
+
+add_task(function* test_keyedSerializationFormat() {
+ Telemetry.clearScalars();
+
+ const expectedKey = "first_key";
+ const expectedOtherKey = "漢語";
+ const expectedUint = 3785;
+ const expectedOtherValue = 1107;
+
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedKey, expectedUint);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedOtherKey, expectedOtherValue);
+
+ // Get a snapshot of the scalars.
+ const keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ Assert.ok(!(UINT_SCALAR in keyedScalars),
+ UINT_SCALAR + " must not be serialized with the keyed scalars.");
+ Assert.ok(KEYED_UINT_SCALAR in keyedScalars,
+ KEYED_UINT_SCALAR + " must be serialized with the keyed scalars.");
+ Assert.equal(Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length, 2,
+ "The keyed scalar must contain exactly 2 keys.");
+ Assert.ok(expectedKey in keyedScalars[KEYED_UINT_SCALAR],
+ KEYED_UINT_SCALAR + " must contain the expected keys.");
+ Assert.ok(expectedOtherKey in keyedScalars[KEYED_UINT_SCALAR],
+ KEYED_UINT_SCALAR + " must contain the expected keys.");
+ Assert.ok(Number.isInteger(keyedScalars[KEYED_UINT_SCALAR][expectedKey]),
+ KEYED_UINT_SCALAR + "." + expectedKey + " must be a finite integer.");
+ Assert.equal(keyedScalars[KEYED_UINT_SCALAR][expectedKey], expectedUint,
+ KEYED_UINT_SCALAR + "." + expectedKey + " must have the correct value.");
+ Assert.equal(keyedScalars[KEYED_UINT_SCALAR][expectedOtherKey], expectedOtherValue,
+ KEYED_UINT_SCALAR + "." + expectedOtherKey + " must have the correct value.");
+});
+
+add_task(function* test_nonexistingScalar() {
+ const NON_EXISTING_SCALAR = "telemetry.test.non_existing";
+
+ Telemetry.clearScalars();
+
+ // Make sure we throw on any operation for non-existing scalars.
+ Assert.throws(() => Telemetry.scalarAdd(NON_EXISTING_SCALAR, 11715),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Adding to a non existing scalar must throw.");
+ Assert.throws(() => Telemetry.scalarSet(NON_EXISTING_SCALAR, 11715),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting a non existing scalar must throw.");
+ Assert.throws(() => Telemetry.scalarSetMaximum(NON_EXISTING_SCALAR, 11715),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting the maximum of a non existing scalar must throw.");
+
+ // Make sure we throw on any operation for non-existing scalars.
+ Assert.throws(() => Telemetry.keyedScalarAdd(NON_EXISTING_SCALAR, "some_key", 11715),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Adding to a non existing keyed scalar must throw.");
+ Assert.throws(() => Telemetry.keyedScalarSet(NON_EXISTING_SCALAR, "some_key", 11715),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting a non existing keyed scalar must throw.");
+ Assert.throws(() => Telemetry.keyedScalarSetMaximum(NON_EXISTING_SCALAR, "some_key", 11715),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting the maximum of a non keyed existing scalar must throw.");
+
+ // Get a snapshot of the scalars.
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ Assert.ok(!(NON_EXISTING_SCALAR in scalars), "The non existing scalar must not be persisted.");
+
+ const keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ Assert.ok(!(NON_EXISTING_SCALAR in keyedScalars),
+ "The non existing keyed scalar must not be persisted.");
+});
+
+add_task(function* test_expiredScalar() {
+ const EXPIRED_SCALAR = "telemetry.test.expired";
+ const EXPIRED_KEYED_SCALAR = "telemetry.test.keyed_expired";
+ const UNEXPIRED_SCALAR = "telemetry.test.unexpired";
+
+ Telemetry.clearScalars();
+
+ // Try to set the expired scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ Telemetry.scalarAdd(EXPIRED_SCALAR, 11715);
+ Telemetry.scalarSet(EXPIRED_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(EXPIRED_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSet(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSetMaximum(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+
+ // The unexpired scalar has an expiration version, but far away in the future.
+ const expectedValue = 11716;
+ Telemetry.scalarSet(UNEXPIRED_SCALAR, expectedValue);
+
+ // Get a snapshot of the scalars.
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ const keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ Assert.ok(!(EXPIRED_SCALAR in scalars), "The expired scalar must not be persisted.");
+ Assert.equal(scalars[UNEXPIRED_SCALAR], expectedValue,
+ "The unexpired scalar must be persisted with the correct value.");
+ Assert.ok(!(EXPIRED_KEYED_SCALAR in keyedScalars),
+ "The expired keyed scalar must not be persisted.");
+});
+
+add_task(function* test_unsignedIntScalar() {
+ let checkScalar = (expectedValue) => {
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(scalars[UINT_SCALAR], expectedValue,
+ UINT_SCALAR + " must contain the expected value.");
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's start with an accumulation without a prior set.
+ Telemetry.scalarAdd(UINT_SCALAR, 1);
+ Telemetry.scalarAdd(UINT_SCALAR, 2);
+ // Do we get what we expect?
+ checkScalar(3);
+
+ // Let's test setting the scalar to a value.
+ Telemetry.scalarSet(UINT_SCALAR, 3785);
+ checkScalar(3785);
+ Telemetry.scalarAdd(UINT_SCALAR, 1);
+ checkScalar(3786);
+
+ // Does setMaximum work?
+ Telemetry.scalarSet(UINT_SCALAR, 2);
+ checkScalar(2);
+ Telemetry.scalarSetMaximum(UINT_SCALAR, 5);
+ checkScalar(5);
+ // The value of the probe should still be 5, as the previous value
+ // is greater than the one we want to set.
+ Telemetry.scalarSetMaximum(UINT_SCALAR, 3);
+ checkScalar(5);
+
+ // Check that non-integer numbers get truncated and set.
+ Telemetry.scalarSet(UINT_SCALAR, 3.785);
+ checkScalar(3);
+
+ // Setting or adding a negative number must report an error through
+ // the console and drop the change (shouldn't throw).
+ Telemetry.scalarAdd(UINT_SCALAR, -5);
+ Telemetry.scalarSet(UINT_SCALAR, -5);
+ Telemetry.scalarSetMaximum(UINT_SCALAR, -1);
+ checkScalar(3);
+
+ // What happens if we try to set a value of a different type?
+ Telemetry.scalarSet(UINT_SCALAR, 1);
+ Assert.throws(() => Telemetry.scalarSet(UINT_SCALAR, "unexpected value"),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting the scalar to an unexpected value type must throw.");
+ Assert.throws(() => Telemetry.scalarAdd(UINT_SCALAR, "unexpected value"),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Adding an unexpected value type must throw.");
+ Assert.throws(() => Telemetry.scalarSetMaximum(UINT_SCALAR, "unexpected value"),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting the scalar to an unexpected value type must throw.");
+ // The stored value must not be compromised.
+ checkScalar(1);
+});
+
+add_task(function* test_stringScalar() {
+ let checkExpectedString = (expectedString) => {
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(scalars[STRING_SCALAR], expectedString,
+ STRING_SCALAR + " must contain the expected string value.");
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's check simple strings...
+ let expected = "test string";
+ Telemetry.scalarSet(STRING_SCALAR, expected);
+ checkExpectedString(expected);
+ expected = "漢語";
+ Telemetry.scalarSet(STRING_SCALAR, expected);
+ checkExpectedString(expected);
+
+ // We have some unsupported operations for strings.
+ Assert.throws(() => Telemetry.scalarAdd(STRING_SCALAR, 1),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarAdd(STRING_SCALAR, "string value"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarSetMaximum(STRING_SCALAR, 1),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarSetMaximum(STRING_SCALAR, "string value"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarSet(STRING_SCALAR, 1),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "The string scalar must throw if we're not setting a string.");
+
+ // Try to set the scalar to a string longer than the maximum length limit.
+ const LONG_STRING = "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv";
+ Telemetry.scalarSet(STRING_SCALAR, LONG_STRING);
+ checkExpectedString(LONG_STRING.substr(0, 50));
+});
+
+add_task(function* test_booleanScalar() {
+ let checkExpectedBool = (expectedBoolean) => {
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(scalars[BOOLEAN_SCALAR], expectedBoolean,
+ BOOLEAN_SCALAR + " must contain the expected boolean value.");
+ };
+
+ Telemetry.clearScalars();
+
+ // Set a test boolean value.
+ let expected = false;
+ Telemetry.scalarSet(BOOLEAN_SCALAR, expected);
+ checkExpectedBool(expected);
+ expected = true;
+ Telemetry.scalarSet(BOOLEAN_SCALAR, expected);
+ checkExpectedBool(expected);
+
+ // Check that setting a numeric value implicitly converts to boolean.
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 1);
+ checkExpectedBool(true);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 0);
+ checkExpectedBool(false);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 1.0);
+ checkExpectedBool(true);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 0.0);
+ checkExpectedBool(false);
+
+ // Check that unsupported operations for booleans throw.
+ Assert.throws(() => Telemetry.scalarAdd(BOOLEAN_SCALAR, 1),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarAdd(BOOLEAN_SCALAR, "string value"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, 1),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, "string value"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+ Assert.throws(() => Telemetry.scalarSet(BOOLEAN_SCALAR, "true"),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "The boolean scalar must throw if we're not setting a boolean.");
+});
+
+add_task(function* test_scalarRecording() {
+ const OPTIN_SCALAR = "telemetry.test.release_optin";
+ const OPTOUT_SCALAR = "telemetry.test.release_optout";
+
+ let checkValue = (scalarName, expectedValue) => {
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(scalars[scalarName], expectedValue,
+ scalarName + " must contain the expected value.");
+ };
+
+ let checkNotSerialized = (scalarName) => {
+ const scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+ };
+
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ Telemetry.clearScalars();
+
+ // Check that no scalar is recorded if both base and extended recording are off.
+ Telemetry.scalarSet(OPTOUT_SCALAR, 3);
+ Telemetry.scalarSet(OPTIN_SCALAR, 3);
+ checkNotSerialized(OPTOUT_SCALAR);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that opt-out scalars are recorded, while opt-in are not.
+ Telemetry.canRecordBase = true;
+ Telemetry.scalarSet(OPTOUT_SCALAR, 3);
+ Telemetry.scalarSet(OPTIN_SCALAR, 3);
+ checkValue(OPTOUT_SCALAR, 3);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that both opt-out and opt-in scalars are recorded.
+ Telemetry.canRecordExtended = true;
+ Telemetry.scalarSet(OPTOUT_SCALAR, 5);
+ Telemetry.scalarSet(OPTIN_SCALAR, 6);
+ checkValue(OPTOUT_SCALAR, 5);
+ checkValue(OPTIN_SCALAR, 6);
+});
+
+add_task(function* test_keyedScalarRecording() {
+ const OPTIN_SCALAR = "telemetry.test.keyed_release_optin";
+ const OPTOUT_SCALAR = "telemetry.test.keyed_release_optout";
+ const testKey = "policy_key";
+
+ let checkValue = (scalarName, expectedValue) => {
+ const scalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(scalars[scalarName][testKey], expectedValue,
+ scalarName + " must contain the expected value.");
+ };
+
+ let checkNotSerialized = (scalarName) => {
+ const scalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+ };
+
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ Telemetry.clearScalars();
+
+ // Check that no scalar is recorded if both base and extended recording are off.
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+ checkNotSerialized(OPTOUT_SCALAR);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that opt-out scalars are recorded, while opt-in are not.
+ Telemetry.canRecordBase = true;
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+ checkValue(OPTOUT_SCALAR, 3);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that both opt-out and opt-in scalars are recorded.
+ Telemetry.canRecordExtended = true;
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 5);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 6);
+ checkValue(OPTOUT_SCALAR, 5);
+ checkValue(OPTIN_SCALAR, 6);
+});
+
+add_task(function* test_subsession() {
+ Telemetry.clearScalars();
+
+ // Set the scalars to a known value.
+ Telemetry.scalarSet(UINT_SCALAR, 3785);
+ Telemetry.scalarSet(STRING_SCALAR, "some value");
+ Telemetry.scalarSet(BOOLEAN_SCALAR, false);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "some_random_key", 12);
+
+ // Get a snapshot and reset the subsession. The value we set must be there.
+ let scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+ let keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
+ Assert.equal(scalars[UINT_SCALAR], 3785,
+ UINT_SCALAR + " must contain the expected value.");
+ Assert.equal(scalars[STRING_SCALAR], "some value",
+ STRING_SCALAR + " must contain the expected value.");
+ Assert.equal(scalars[BOOLEAN_SCALAR], false,
+ BOOLEAN_SCALAR + " must contain the expected value.");
+ Assert.equal(keyedScalars[KEYED_UINT_SCALAR]["some_random_key"], 12,
+ KEYED_UINT_SCALAR + " must contain the expected value.");
+
+ // Get a new snapshot and reset the subsession again. Since no new value
+ // was set, the scalars should not be reported.
+ scalars =
+ Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+ keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
+ Assert.ok(!(UINT_SCALAR in scalars), UINT_SCALAR + " must be empty and not reported.");
+ Assert.ok(!(STRING_SCALAR in scalars), STRING_SCALAR + " must be empty and not reported.");
+ Assert.ok(!(BOOLEAN_SCALAR in scalars), BOOLEAN_SCALAR + " must be empty and not reported.");
+ Assert.ok(!(KEYED_UINT_SCALAR in keyedScalars), KEYED_UINT_SCALAR + " must be empty and not reported.");
+});
+
+add_task(function* test_keyed_uint() {
+ Telemetry.clearScalars();
+
+ const KEYS = [ "a_key", "another_key", "third_key" ];
+ let expectedValues = [ 1, 1, 1 ];
+
+ // Set all the keys to a baseline value.
+ for (let key of KEYS) {
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, key, 1);
+ }
+
+ // Increment only one key.
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, KEYS[1], 1);
+ expectedValues[1]++;
+
+ // Use SetMaximum on the third key.
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, KEYS[2], 37);
+ expectedValues[2] = 37;
+
+ // Get a snapshot of the scalars and make sure the keys contain
+ // the correct values.
+ const keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ for (let k = 0; k < 3; k++) {
+ const keyName = KEYS[k];
+ Assert.equal(keyedScalars[KEYED_UINT_SCALAR][keyName], expectedValues[k],
+ KEYED_UINT_SCALAR + "." + keyName + " must contain the correct value.");
+ }
+
+ // Are we still throwing when doing unsupported things on uint keyed scalars?
+ // Just test one single unsupported operation, the other are covered in the plain
+ // unsigned scalar test.
+ Assert.throws(() => Telemetry.scalarSet(KEYED_UINT_SCALAR, "new_key", "unexpected value"),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Setting the scalar to an unexpected value type must throw.");
+});
+
+add_task(function* test_keyed_boolean() {
+ Telemetry.clearScalars();
+
+ const KEYED_BOOLEAN_TYPE = "telemetry.test.keyed_boolean_kind";
+ const first_key = "first_key";
+ const second_key = "second_key";
+
+ // Set the initial values.
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, true);
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, false);
+
+ // Get a snapshot of the scalars and make sure the keys contain
+ // the correct values.
+ let keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][first_key], true,
+ "The key must contain the expected value.");
+ Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][second_key], false,
+ "The key must contain the expected value.");
+
+ // Now flip the values and make sure we get the expected values back.
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, false);
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, true);
+
+ keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][first_key], false,
+ "The key must contain the expected value.");
+ Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][second_key], true,
+ "The key must contain the expected value.");
+
+ // Are we still throwing when doing unsupported things on a boolean keyed scalars?
+ // Just test one single unsupported operation, the other are covered in the plain
+ // boolean scalar test.
+ Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_BOOLEAN_TYPE, "somehey", 1),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Using an unsupported operation must throw.");
+});
+
+add_task(function* test_keyed_keys_length() {
+ Telemetry.clearScalars();
+
+ const LONG_KEY_STRING =
+ "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv.somemoresowereach70chars";
+ const NORMAL_KEY = "a_key";
+
+ // Set the value for a key within the length limits.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, NORMAL_KEY, 1);
+
+ // Now try to set and modify the value for a very long key.
+ Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Using keys longer than 70 characters must throw.");
+ Assert.throws(() => Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LONG_KEY_STRING, 1),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Using keys longer than 70 characters must throw.");
+ Assert.throws(() => Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Using keys longer than 70 characters must throw.");
+
+ // Make sure the key with the right length contains the expected value.
+ let keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+ Assert.equal(Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length, 1,
+ "The keyed scalar must contain exactly 1 key.");
+ Assert.ok(NORMAL_KEY in keyedScalars[KEYED_UINT_SCALAR],
+ "The keyed scalar must contain the expected key.");
+ Assert.equal(keyedScalars[KEYED_UINT_SCALAR][NORMAL_KEY], 1,
+ "The key must contain the expected value.");
+ Assert.ok(!(LONG_KEY_STRING in keyedScalars[KEYED_UINT_SCALAR]),
+ "The data for the long key should not have been recorded.");
+});
+
+add_task(function* test_keyed_max_keys() {
+ Telemetry.clearScalars();
+
+ // Generate the names for the first 100 keys.
+ let keyNamesSet = new Set();
+ for (let k = 0; k < 100; k++) {
+ keyNamesSet.add("key_" + k);
+ }
+
+ // Add 100 keys to an histogram and set their initial value.
+ let valueToSet = 0;
+ keyNamesSet.forEach(keyName => {
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, keyName, valueToSet++);
+ });
+
+ // Perform some operations on the 101th key. This should throw, as
+ // we're not allowed to have more than 100 keys.
+ const LAST_KEY_NAME = "overflowing_key";
+ Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10),
+ /NS_ERROR_FAILURE/,
+ "Using more than 100 keys must throw.");
+ Assert.throws(() => Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LAST_KEY_NAME, 1),
+ /NS_ERROR_FAILURE/,
+ "Using more than 100 keys must throw.");
+ Assert.throws(() => Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10),
+ /NS_ERROR_FAILURE/,
+ "Using more than 100 keys must throw.");
+
+ // Make sure all the keys except the last one are available and have the correct
+ // values.
+ let keyedScalars =
+ Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+ // Check that the keyed scalar only contain the first 100 keys.
+ const reportedKeysSet = new Set(Object.keys(keyedScalars[KEYED_UINT_SCALAR]));
+ Assert.ok([...keyNamesSet].filter(x => reportedKeysSet.has(x)) &&
+ [...reportedKeysSet].filter(x => keyNamesSet.has(x)),
+ "The keyed scalar must contain all the 100 keys, and drop the others.");
+
+ // Check that all the keys recorded the expected values.
+ let expectedValue = 0;
+ keyNamesSet.forEach(keyName => {
+ Assert.equal(keyedScalars[KEYED_UINT_SCALAR][keyName], expectedValue++,
+ "The key must contain the expected value.");
+ });
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
new file mode 100644
index 0000000000..88ff8cf44f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -0,0 +1,427 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server";
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+function countPingTypes(pings) {
+ let countByType = new Map();
+ for (let p of pings) {
+ countByType.set(p.type, 1 + (countByType.get(p.type) || 0));
+ }
+ return countByType;
+}
+
+function setPingLastModified(id, timestamp) {
+ const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, id);
+ return OS.File.setDates(path, null, timestamp);
+}
+
+// Mock out the send timer activity.
+function waitForTimer() {
+ return new Promise(resolve => {
+ fakePingSendTimer((callback, timeout) => {
+ resolve([callback, timeout]);
+ }, () => {});
+ });
+}
+
+// Allow easy faking of readable ping ids.
+// This helps with debugging issues with e.g. ordering in the send logic.
+function fakePingId(type, number) {
+ const HEAD = "93bd0011-2c8f-4e1c-bee0-";
+ const TAIL = "000000000000";
+ const N = String(number);
+ const id = HEAD + type + TAIL.slice(type.length, - N.length) + N;
+ fakeGeneratePingId(() => id);
+ return id;
+}
+
+var checkPingsSaved = Task.async(function* (pingIds) {
+ let allFound = true;
+ for (let id of pingIds) {
+ const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, id);
+ let exists = false;
+ try {
+ exists = yield OS.File.exists(path);
+ } catch (ex) {}
+
+ if (!exists) {
+ dump("checkPingsSaved - failed to find ping: " + path + "\n");
+ allFound = false;
+ }
+ }
+
+ return allFound;
+});
+
+function histogramValueCount(h) {
+ return h.counts.reduce((a, b) => a + b);
+}
+
+add_task(function* test_setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+});
+
+// Test the ping sending logic.
+add_task(function* test_sendPendingPings() {
+ const TYPE_PREFIX = "test-sendPendingPings-";
+ const TEST_TYPE_A = TYPE_PREFIX + "A";
+ const TEST_TYPE_B = TYPE_PREFIX + "B";
+
+ const TYPE_A_COUNT = 20;
+ const TYPE_B_COUNT = 5;
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ // Fake a current date.
+ let now = TelemetryUtils.truncateToDays(new Date());
+ now = fakeNow(futureDate(now, 10 * 60 * MS_IN_A_MINUTE));
+
+ // Enable test-mode for TelemetrySend, otherwise we won't store pending pings
+ // before the module is fully initialized later.
+ TelemetrySend.setTestModeEnabled(true);
+
+ // Submit some pings without the server and telemetry started yet.
+ for (let i = 0; i < TYPE_A_COUNT; ++i) {
+ fakePingId("a", i);
+ const id = yield TelemetryController.submitExternalPing(TEST_TYPE_A, {});
+ yield setPingLastModified(id, now.getTime() + (i * 1000));
+ }
+
+ Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT,
+ "Should have correct pending ping count");
+
+ // Submit some more pings of a different type.
+ now = fakeNow(futureDate(now, 5 * MS_IN_A_MINUTE));
+ for (let i = 0; i < TYPE_B_COUNT; ++i) {
+ fakePingId("b", i);
+ const id = yield TelemetryController.submitExternalPing(TEST_TYPE_B, {});
+ yield setPingLastModified(id, now.getTime() + (i * 1000));
+ }
+
+ Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT + TYPE_B_COUNT,
+ "Should have correct pending ping count");
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 0, 0],
+ "Should not have recorded any sending in histograms yet.");
+ Assert.equal(histSendTimeSuccess.snapshot().sum, 0,
+ "Should not have recorded any sending in histograms yet.");
+ Assert.equal(histSendTimeFail.snapshot().sum, 0,
+ "Should not have recorded any sending in histograms yet.");
+
+ // Now enable sending to the ping server.
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ PingServer.start();
+ Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
+
+ let timerPromise = waitForTimer();
+ yield TelemetryController.testReset();
+ let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
+
+ // We should have received 10 pings from the first send batch:
+ // 5 of type B and 5 of type A, as sending is newest-first.
+ // The other pings should be delayed by the 10-pings-per-minute limit.
+ let pings = yield PingServer.promiseNextPings(10);
+ Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT - 5,
+ "Should have correct pending ping count");
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_B), TYPE_B_COUNT,
+ "Should have received the correct amount of type B pings");
+ Assert.equal(countByType.get(TEST_TYPE_A), 10 - TYPE_B_COUNT,
+ "Should have received the correct amount of type A pings");
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 10, 0],
+ "Should have recorded sending success in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 10,
+ "Should have recorded successful send times in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0,
+ "Should not have recorded any failed sending in histograms yet.");
+
+ // As we hit the ping send limit and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.ok(!!pingSendTimerCallback, "Timer callback should be set");
+ Assert.equal(pingSendTimeout, MS_IN_A_MINUTE, "Send tick timeout should be correct");
+
+ // Trigger the next tick - we should receive the next 10 type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+
+ pings = yield PingServer.promiseNextPings(10);
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_A), 10, "Should have received the correct amount of type A pings");
+
+ // We hit the ping send limit again and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.equal(pingSendTimeout, MS_IN_A_MINUTE, "Send tick timeout should be correct");
+
+ // Trigger the next tick - we should receive the remaining type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ yield pingSendTimerCallback();
+
+ pings = yield PingServer.promiseNextPings(5);
+ PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
+ countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_A), 5, "Should have received the correct amount of type A pings");
+
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ PingServer.resetPingHandler();
+});
+
+add_task(function* test_sendDateHeader() {
+ fakeNow(new Date(Date.UTC(2011, 1, 1, 11, 0, 0)));
+ yield TelemetrySend.reset();
+
+ let pingId = yield TelemetryController.submitExternalPing("test-send-date-header", {});
+ let req = yield PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ Assert.equal(req.getHeader("Date"), "Tue, 01 Feb 2011 11:00:00 GMT",
+ "Telemetry should send the correct Date header with requests.");
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+});
+
+// Test the backoff timeout behavior after send failures.
+add_task(function* test_backoffTimeout() {
+ const TYPE_PREFIX = "test-backoffTimeout-";
+ const TEST_TYPE_C = TYPE_PREFIX + "C";
+ const TEST_TYPE_D = TYPE_PREFIX + "D";
+ const TEST_TYPE_E = TYPE_PREFIX + "E";
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+
+ // Failing a ping send now should trigger backoff behavior.
+ let now = fakeNow(2010, 1, 1, 11, 0, 0);
+ yield TelemetrySend.reset();
+ PingServer.stop();
+
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ fakePingId("c", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ let sendAttempts = 0;
+ let timerPromise = waitForTimer();
+ yield TelemetryController.submitExternalPing(TEST_TYPE_C, {});
+ let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.equal(TelemetrySend.pendingPingCount, 1, "Should have one pending ping.");
+ ++sendAttempts;
+
+ const MAX_BACKOFF_TIMEOUT = 120 * MS_IN_A_MINUTE;
+ for (let timeout = 2 * MS_IN_A_MINUTE; timeout <= MAX_BACKOFF_TIMEOUT; timeout *= 2) {
+ Assert.ok(!!pingSendTimerCallback, "Should have received a timer callback");
+ Assert.equal(pingSendTimeout, timeout, "Send tick timeout should be correct");
+
+ let callback = pingSendTimerCallback;
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ yield callback();
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ ++sendAttempts;
+ }
+
+ timerPromise = waitForTimer();
+ yield pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.equal(pingSendTimeout, MAX_BACKOFF_TIMEOUT, "Tick timeout should be capped");
+ ++sendAttempts;
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [sendAttempts, 0, 0],
+ "Should have recorded sending failure in histograms.");
+ Assert.equal(histSendTimeSuccess.snapshot().sum, 0,
+ "Should not have recorded any sending success in histograms yet.");
+ Assert.greater(histSendTimeFail.snapshot().sum, 0,
+ "Should have recorded send failure times in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), sendAttempts,
+ "Should have recorded send failure times in histograms.");
+
+ // Submitting a new ping should reset the backoff behavior.
+ fakePingId("d", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ yield TelemetryController.submitExternalPing(TEST_TYPE_D, {});
+ [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
+ Assert.equal(pingSendTimeout, 2 * MS_IN_A_MINUTE, "Send tick timeout should be correct");
+ sendAttempts += 2;
+
+ // With the server running again, we should send out the pending pings immediately
+ // when a new ping is submitted.
+ PingServer.start();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ fakePingId("e", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ yield TelemetryController.submitExternalPing(TEST_TYPE_E, {});
+
+ let pings = yield PingServer.promiseNextPings(3);
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(countByType.get(TEST_TYPE_C), 1, "Should have received the correct amount of type C pings");
+ Assert.equal(countByType.get(TEST_TYPE_D), 1, "Should have received the correct amount of type D pings");
+ Assert.equal(countByType.get(TEST_TYPE_E), 1, "Should have received the correct amount of type E pings");
+
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(TelemetrySend.pendingPingCount, 0, "Should have no pending pings left");
+
+ Assert.deepEqual(histSuccess.snapshot().counts, [sendAttempts, 3, 0],
+ "Should have recorded sending failure in histograms.");
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0,
+ "Should have recorded sending success in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 3,
+ "Should have recorded sending success in histograms.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), sendAttempts,
+ "Should have recorded send failure times in histograms.");
+});
+
+add_task(function* test_discardBigPings() {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ let histSizeExceeded = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND");
+ let histDiscardedSize = Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB");
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [histSizeExceeded, histDiscardedSize, histSuccess, histSendTimeSuccess, histSendTimeFail]) {
+ h.clear();
+ }
+
+ // Generate a 2MB string and create an oversized payload.
+ const OVERSIZED_PAYLOAD = {"data": generateRandomString(2 * 1024 * 1024)};
+
+ // Reset the histograms.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND").clear();
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB").clear();
+
+ // Submit a ping of a normal size and check that we don't count it in the histogram.
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, { test: "test" });
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(histSizeExceeded.snapshot().sum, 0, "Telemetry must report no oversized ping submitted.");
+ Assert.equal(histDiscardedSize.snapshot().sum, 0, "Telemetry must report no oversized pings.");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0], "Should have recorded sending success.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1, "Should have recorded send success time.");
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0, "Should have recorded send success time.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, "Should not have recorded send failure time.");
+
+ // Submit an oversized ping and check that it gets discarded.
+ yield TelemetryController.submitExternalPing(TEST_PING_TYPE, OVERSIZED_PAYLOAD);
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(histSizeExceeded.snapshot().sum, 1, "Telemetry must report 1 oversized ping submitted.");
+ Assert.equal(histDiscardedSize.snapshot().counts[2], 1, "Telemetry must report a 2MB, oversized, ping submitted.");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0], "Should have recorded sending success.");
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1, "Should have recorded send success time.");
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0, "Should have recorded send success time.");
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, "Should not have recorded send failure time.");
+});
+
+add_task(function* test_evictedOnServerErrors() {
+ const TEST_TYPE = "test-evicted";
+
+ yield TelemetrySend.reset();
+
+ let histEvicted = Telemetry.getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS");
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS");
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [histEvicted, histSuccess, histSendTimeSuccess, histSendTimeFail]) {
+ h.clear();
+ }
+
+ // Write a custom ping handler which will return 403. This will trigger ping eviction
+ // on client side.
+ PingServer.registerPingHandler((req, res) => {
+ res.setStatusLine(null, 403, "Forbidden");
+ res.processAsync();
+ res.finish();
+ });
+
+ // Clear the histogram and submit a ping.
+ let pingId = yield TelemetryController.submitExternalPing(TEST_TYPE, {});
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(histEvicted.snapshot().sum, 1,
+ "Telemetry must report a ping evicted due to server errors");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0]);
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1);
+ Assert.greater(histSendTimeSuccess.snapshot().sum, 0);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+
+ // The ping should not be persisted.
+ yield Assert.rejects(TelemetryStorage.loadPendingPing(pingId), "The ping must not be persisted.");
+
+ // Reset the ping handler and submit a new ping.
+ PingServer.resetPingHandler();
+ pingId = yield TelemetryController.submitExternalPing(TEST_TYPE, {});
+
+ let ping = yield PingServer.promiseNextPings(1);
+ Assert.equal(ping[0].id, pingId, "The correct ping must be received");
+
+ // We should not have updated the error histogram.
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(histEvicted.snapshot().sum, 1, "Telemetry must report only one ping evicted due to server errors");
+ Assert.deepEqual(histSuccess.snapshot().counts, [0, 2, 0]);
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 2);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+});
+
+// Test that the current, non-persisted pending pings are properly saved on shutdown.
+add_task(function* test_persistCurrentPingsOnShutdown() {
+ const TEST_TYPE = "test-persistCurrentPingsOnShutdown";
+ const PING_COUNT = 5;
+ yield TelemetrySend.reset();
+ PingServer.stop();
+ Assert.equal(TelemetrySend.pendingPingCount, 0, "Should have no pending pings yet");
+
+ // Submit new pings that shouldn't be persisted yet.
+ let ids = [];
+ for (let i=0; i<5; ++i) {
+ ids.push(fakePingId("f", i));
+ TelemetryController.submitExternalPing(TEST_TYPE, {});
+ }
+
+ Assert.equal(TelemetrySend.pendingPingCount, PING_COUNT, "Should have the correct pending ping count");
+
+ // Triggering a shutdown should persist the pings.
+ yield TelemetrySend.shutdown();
+ Assert.ok((yield checkPingsSaved(ids)), "All pending pings should have been persisted");
+
+ // After a restart the pings should have been found when scanning.
+ yield TelemetrySend.reset();
+ Assert.equal(TelemetrySend.pendingPingCount, PING_COUNT, "Should have the correct pending ping count");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
new file mode 100644
index 0000000000..221b6bcab7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -0,0 +1,547 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+
+/**
+ * This test case populates the profile with some fake stored
+ * pings, and checks that pending pings are immediatlely sent
+ * after delayed init.
+ */
+
+"use strict"
+
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+var {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+// We increment TelemetryStorage's MAX_PING_FILE_AGE and
+// OVERDUE_PING_FILE_AGE by 1 minute so that our test pings exceed
+// those points in time, even taking into account file system imprecision.
+const ONE_MINUTE_MS = 60 * 1000;
+const OVERDUE_PING_FILE_AGE = TelemetrySend.OVERDUE_PING_FILE_AGE + ONE_MINUTE_MS;
+
+const PING_SAVE_FOLDER = "saved-telemetry-pings";
+const PING_TIMEOUT_LENGTH = 5000;
+const OVERDUE_PINGS = 6;
+const OLD_FORMAT_PINGS = 4;
+const RECENT_PINGS = 4;
+
+const TOTAL_EXPECTED_PINGS = OVERDUE_PINGS + RECENT_PINGS + OLD_FORMAT_PINGS;
+
+const PREF_FHR_UPLOAD = "datareporting.healthreport.uploadEnabled";
+
+var gCreatedPings = 0;
+var gSeenPings = 0;
+
+/**
+ * Creates some Telemetry pings for the and saves them to disk. Each ping gets a
+ * unique ID based on an incrementor.
+ *
+ * @param {Array} aPingInfos An array of ping type objects. Each entry must be an
+ * object containing a "num" field for the number of pings to create and
+ * an "age" field. The latter representing the age in milliseconds to offset
+ * from now. A value of 10 would make the ping 10ms older than now, for
+ * example.
+ * @returns Promise
+ * @resolve an Array with the created pings ids.
+ */
+var createSavedPings = Task.async(function* (aPingInfos) {
+ let pingIds = [];
+ let now = Date.now();
+
+ for (let type in aPingInfos) {
+ let num = aPingInfos[type].num;
+ let age = now - (aPingInfos[type].age || 0);
+ for (let i = 0; i < num; ++i) {
+ let pingId = yield TelemetryController.addPendingPing("test-ping", {}, { overwrite: true });
+ if (aPingInfos[type].age) {
+ // savePing writes to the file synchronously, so we're good to
+ // modify the lastModifedTime now.
+ let filePath = getSavePathForPingId(pingId);
+ yield File.setDates(filePath, null, age);
+ }
+ gCreatedPings++;
+ pingIds.push(pingId);
+ }
+ }
+
+ return pingIds;
+});
+
+/**
+ * Deletes locally saved pings if they exist.
+ *
+ * @param aPingIds an Array of ping ids to delete.
+ * @returns Promise
+ */
+var clearPings = Task.async(function* (aPingIds) {
+ for (let pingId of aPingIds) {
+ yield TelemetryStorage.removePendingPing(pingId);
+ }
+});
+
+/**
+ * Fakes the pending pings storage quota.
+ * @param {Integer} aPendingQuota The new quota, in bytes.
+ */
+function fakePendingPingsQuota(aPendingQuota) {
+ let storage = Cu.import("resource://gre/modules/TelemetryStorage.jsm");
+ storage.Policy.getPendingPingsQuota = () => aPendingQuota;
+}
+
+/**
+ * Returns a handle for the file that a ping should be
+ * stored in locally.
+ *
+ * @returns path
+ */
+function getSavePathForPingId(aPingId) {
+ return Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, aPingId);
+}
+
+/**
+ * Check if the number of Telemetry pings received by the HttpServer is not equal
+ * to aExpectedNum.
+ *
+ * @param aExpectedNum the number of pings we expect to receive.
+ */
+function assertReceivedPings(aExpectedNum) {
+ do_check_eq(gSeenPings, aExpectedNum);
+}
+
+/**
+ * Throws if any pings with the id in aPingIds is saved locally.
+ *
+ * @param aPingIds an Array of pings ids to check.
+ * @returns Promise
+ */
+var assertNotSaved = Task.async(function* (aPingIds) {
+ let saved = 0;
+ for (let id of aPingIds) {
+ let filePath = getSavePathForPingId(id);
+ if (yield File.exists(filePath)) {
+ saved++;
+ }
+ }
+ if (saved > 0) {
+ do_throw("Found " + saved + " unexpected saved pings.");
+ }
+});
+
+/**
+ * Our handler function for the HttpServer that simply
+ * increments the gSeenPings global when it successfully
+ * receives and decodes a Telemetry payload.
+ *
+ * @param aRequest the HTTP request sent from HttpServer.
+ */
+function pingHandler(aRequest) {
+ gSeenPings++;
+}
+
+add_task(function* test_setup() {
+ PingServer.start();
+ PingServer.registerPingHandler(pingHandler);
+ do_get_profile();
+ loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER,
+ "http://localhost:" + PingServer.port);
+});
+
+/**
+ * Setup the tests by making sure the ping storage directory is available, otherwise
+ * |TelemetryController.testSaveDirectoryToFile| could fail.
+ */
+add_task(function* setupEnvironment() {
+ // The following tests assume this pref to be true by default.
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true);
+
+ yield TelemetryController.testSetup();
+
+ let directory = TelemetryStorage.pingDirectoryPath;
+ yield File.makeDir(directory, { ignoreExisting: true, unixMode: OS.Constants.S_IRWXU });
+
+ yield TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Test that really recent pings are sent on Telemetry initialization.
+ */
+add_task(function* test_recent_pings_sent() {
+ let pingTypes = [{ num: RECENT_PINGS }];
+ yield createSavedPings(pingTypes);
+
+ yield TelemetryController.testReset();
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ assertReceivedPings(RECENT_PINGS);
+
+ yield TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Create an overdue ping in the old format and try to send it.
+ */
+add_task(function* test_overdue_old_format() {
+ // A test ping in the old, standard format.
+ const PING_OLD_FORMAT = {
+ slug: "1234567abcd",
+ reason: "test-ping",
+ payload: {
+ info: {
+ reason: "test-ping",
+ OS: "XPCShell",
+ appID: "SomeId",
+ appVersion: "1.0",
+ appName: "XPCShell",
+ appBuildID: "123456789",
+ appUpdateChannel: "Test",
+ platformBuildID: "987654321",
+ },
+ },
+ };
+
+ // A ping with no info section, but with a slug.
+ const PING_NO_INFO = {
+ slug: "1234-no-info-ping",
+ reason: "test-ping",
+ payload: {}
+ };
+
+ // A ping with no payload.
+ const PING_NO_PAYLOAD = {
+ slug: "5678-no-payload",
+ reason: "test-ping",
+ };
+
+ // A ping with no info and no slug.
+ const PING_NO_SLUG = {
+ reason: "test-ping",
+ payload: {}
+ };
+
+ const PING_FILES_PATHS = [
+ getSavePathForPingId(PING_OLD_FORMAT.slug),
+ getSavePathForPingId(PING_NO_INFO.slug),
+ getSavePathForPingId(PING_NO_PAYLOAD.slug),
+ getSavePathForPingId("no-slug-file"),
+ ];
+
+ // Write the ping to file and make it overdue.
+ yield TelemetryStorage.savePing(PING_OLD_FORMAT, true);
+ yield TelemetryStorage.savePing(PING_NO_INFO, true);
+ yield TelemetryStorage.savePing(PING_NO_PAYLOAD, true);
+ yield TelemetryStorage.savePingToFile(PING_NO_SLUG, PING_FILES_PATHS[3], true);
+
+ for (let f in PING_FILES_PATHS) {
+ yield File.setDates(PING_FILES_PATHS[f], null, Date.now() - OVERDUE_PING_FILE_AGE);
+ }
+
+ gSeenPings = 0;
+ yield TelemetryController.testReset();
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ assertReceivedPings(OLD_FORMAT_PINGS);
+
+ // |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id,
+ // so remove it manually so that the next test doesn't fail.
+ yield OS.File.remove(PING_FILES_PATHS[3]);
+
+ yield TelemetryStorage.testClearPendingPings();
+});
+
+add_task(function* test_corrupted_pending_pings() {
+ const TEST_TYPE = "test_corrupted";
+
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").clear();
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").clear();
+
+ // Save a pending ping and get its id.
+ let pendingPingId = yield TelemetryController.addPendingPing(TEST_TYPE, {}, {});
+
+ // Try to load it: there should be no error.
+ yield TelemetryStorage.loadPendingPing(pendingPingId);
+
+ let h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must not report a pending ping load failure");
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must not report a pending ping parse failure");
+
+ // Delete it from the disk, so that its id will be kept in the cache but it will
+ // fail loading the file.
+ yield OS.File.remove(getSavePathForPingId(pendingPingId));
+
+ // Try to load a pending ping which isn't there anymore.
+ yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId),
+ "Telemetry must fail loading a ping which isn't there");
+
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must not report a pending ping parse failure");
+
+ // Save a new ping, so that it gets in the pending pings cache.
+ pendingPingId = yield TelemetryController.addPendingPing(TEST_TYPE, {}, {});
+ // Overwrite it with a corrupted JSON file and then try to load it.
+ const INVALID_JSON = "{ invalid,JSON { {1}";
+ yield OS.File.writeAtomic(getSavePathForPingId(pendingPingId), INVALID_JSON, { encoding: "utf-8" });
+
+ // Try to load the ping with the corrupted JSON content.
+ yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId),
+ "Telemetry must fail loading a corrupted ping");
+
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure");
+
+ let exists = yield OS.File.exists(getSavePathForPingId(pendingPingId));
+ Assert.ok(!exists, "The unparseable ping should have been removed");
+
+ yield TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Create some recent and overdue pings and verify that they get sent.
+ */
+add_task(function* test_overdue_pings_trigger_send() {
+ let pingTypes = [
+ { num: RECENT_PINGS },
+ { num: OVERDUE_PINGS, age: OVERDUE_PING_FILE_AGE },
+ ];
+ let pings = yield createSavedPings(pingTypes);
+ let recentPings = pings.slice(0, RECENT_PINGS);
+ let overduePings = pings.slice(-OVERDUE_PINGS);
+
+ yield TelemetryController.testReset();
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ assertReceivedPings(TOTAL_EXPECTED_PINGS);
+
+ yield assertNotSaved(recentPings);
+ yield assertNotSaved(overduePings);
+
+ Assert.equal(TelemetrySend.overduePingsCount, overduePings.length,
+ "Should have tracked the correct amount of overdue pings");
+
+ yield TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Create a ping in the old format, send it, and make sure the request URL contains
+ * the correct version query parameter.
+ */
+add_task(function* test_overdue_old_format() {
+ // A test ping in the old, standard format.
+ const PING_OLD_FORMAT = {
+ slug: "1234567abcd",
+ reason: "test-ping",
+ payload: {
+ info: {
+ reason: "test-ping",
+ OS: "XPCShell",
+ appID: "SomeId",
+ appVersion: "1.0",
+ appName: "XPCShell",
+ appBuildID: "123456789",
+ appUpdateChannel: "Test",
+ platformBuildID: "987654321",
+ },
+ },
+ };
+
+ const filePath =
+ Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, PING_OLD_FORMAT.slug);
+
+ // Write the ping to file and make it overdue.
+ yield TelemetryStorage.savePing(PING_OLD_FORMAT, true);
+ yield File.setDates(filePath, null, Date.now() - OVERDUE_PING_FILE_AGE);
+
+ let receivedPings = 0;
+ // Register a new prefix handler to validate the URL.
+ PingServer.registerPingHandler(request => {
+ // Check that we have a version query parameter in the URL.
+ Assert.notEqual(request.queryString, "");
+
+ // Make sure the version in the query string matches the old ping format version.
+ let params = request.queryString.split("&");
+ Assert.ok(params.find(p => p == "v=1"));
+
+ receivedPings++;
+ });
+
+ yield TelemetryController.testReset();
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(receivedPings, 1, "We must receive a ping in the old format.");
+
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.resetPingHandler();
+});
+
+add_task(function* test_pendingPingsQuota() {
+ const PING_TYPE = "foo";
+
+ // Disable upload so pings don't get sent and removed from the pending pings directory.
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD, false);
+
+ // Remove all the pending pings then startup and wait for the cleanup task to complete.
+ // There should be nothing to remove.
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+ yield TelemetrySend.testWaitOnOutgoingPings();
+ yield TelemetryStorage.testPendingQuotaTaskPromise();
+
+ // Remove the pending deletion ping generated when flipping FHR upload off.
+ yield TelemetryStorage.testClearPendingPings();
+
+ let expectedPrunedPings = [];
+ let expectedNotPrunedPings = [];
+
+ let checkPendingPings = Task.async(function*() {
+ // Check that the pruned pings are not on disk anymore.
+ for (let prunedPingId of expectedPrunedPings) {
+ yield Assert.rejects(TelemetryStorage.loadPendingPing(prunedPingId),
+ "Ping " + prunedPingId + " should have been pruned.");
+ const pingPath = getSavePathForPingId(prunedPingId);
+ Assert.ok(!(yield OS.File.exists(pingPath)), "The ping should not be on the disk anymore.");
+ }
+
+ // Check that the expected pings are there.
+ for (let expectedPingId of expectedNotPrunedPings) {
+ Assert.ok((yield TelemetryStorage.loadPendingPing(expectedPingId)),
+ "Ping" + expectedPingId + " should be among the pending pings.");
+ }
+ });
+
+ let pendingPingsInfo = [];
+ let pingsSizeInBytes = 0;
+
+ // Create 10 pings to test the pending pings quota.
+ for (let days = 1; days < 11; days++) {
+ const date = fakeNow(2010, 1, days, 1, 1, 0);
+ const pingId = yield TelemetryController.addPendingPing(PING_TYPE, {}, {});
+
+ // Find the size of the ping.
+ const pingFilePath = getSavePathForPingId(pingId);
+ const pingSize = (yield OS.File.stat(pingFilePath)).size;
+ // Add the info at the beginning of the array, so that most recent pings come first.
+ pendingPingsInfo.unshift({id: pingId, size: pingSize, timestamp: date.getTime() });
+
+ // Set the last modification date.
+ yield OS.File.setDates(pingFilePath, null, date.getTime());
+
+ // Add it to the pending ping directory size.
+ pingsSizeInBytes += pingSize;
+ }
+
+ // We need to test the pending pings size before we hit the quota, otherwise a special
+ // value is recorded.
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").clear();
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").clear();
+ Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").clear();
+
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testPendingQuotaTaskPromise();
+
+ // Check that the correct values for quota probes are reported when no quota is hit.
+ let h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();
+ Assert.equal(h.sum, Math.round(pingsSizeInBytes / 1024 / 1024),
+ "Telemetry must report the correct pending pings directory size.");
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must report 0 evictions if quota is not hit.");
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").snapshot();
+ Assert.equal(h.sum, 0, "Telemetry must report a null elapsed time if quota is not hit.");
+
+ // Set the quota to 80% of the space.
+ const testQuotaInBytes = pingsSizeInBytes * 0.8;
+ fakePendingPingsQuota(testQuotaInBytes);
+
+ // The storage prunes pending pings until we reach 90% of the requested storage quota.
+ // Based on that, find how many pings should be kept.
+ const safeQuotaSize = Math.round(testQuotaInBytes * 0.9);
+ let sizeInBytes = 0;
+ let pingsWithinQuota = [];
+ let pingsOutsideQuota = [];
+
+ for (let pingInfo of pendingPingsInfo) {
+ sizeInBytes += pingInfo.size;
+ if (sizeInBytes >= safeQuotaSize) {
+ pingsOutsideQuota.push(pingInfo.id);
+ continue;
+ }
+ pingsWithinQuota.push(pingInfo.id);
+ }
+
+ expectedNotPrunedPings = pingsWithinQuota;
+ expectedPrunedPings = pingsOutsideQuota;
+
+ // Reset TelemetryController to start the pending pings cleanup.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testPendingQuotaTaskPromise();
+ yield checkPendingPings();
+
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot();
+ Assert.equal(h.sum, pingsOutsideQuota.length,
+ "Telemetry must correctly report the over quota pings evicted from the pending pings directory.");
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();
+ Assert.equal(h.sum, 17, "Pending pings quota was hit, a special size must be reported.");
+
+ // Trigger a cleanup again and make sure we're not removing anything.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testPendingQuotaTaskPromise();
+ yield checkPendingPings();
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create a pending oversized ping.
+ const OVERSIZED_PING = {
+ id: OVERSIZED_PING_ID,
+ type: PING_TYPE,
+ creationDate: (new Date()).toISOString(),
+ // Generate a 2MB string to use as the ping payload.
+ payload: generateRandomString(2 * 1024 * 1024),
+ };
+ yield TelemetryStorage.savePendingPing(OVERSIZED_PING);
+
+ // Reset the histograms.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").clear();
+ Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").clear();
+
+ // Try to manually load the oversized ping.
+ yield Assert.rejects(TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID),
+ "The oversized ping should have been pruned.");
+ Assert.ok(!(yield OS.File.exists(getSavePathForPingId(OVERSIZED_PING_ID))),
+ "The ping should not be on the disk anymore.");
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report 1 oversized ping in the pending pings directory.");
+ h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot();
+ Assert.equal(h.counts[2], 1, "Telemetry must report a 2MB, oversized, ping.");
+
+ // Save the ping again to check if it gets pruned when scanning the pings directory.
+ yield TelemetryStorage.savePendingPing(OVERSIZED_PING);
+ expectedPrunedPings.push(OVERSIZED_PING_ID);
+
+ // Scan the pending pings directory.
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testPendingQuotaTaskPromise();
+ yield checkPendingPings();
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot();
+ Assert.equal(h.sum, 2, "Telemetry must report 1 oversized ping in the pending pings directory.");
+ h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot();
+ Assert.equal(h.counts[2], 2, "Telemetry must report two 2MB, oversized, pings.");
+
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true);
+});
+
+add_task(function* teardown() {
+ yield PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
new file mode 100644
index 0000000000..6981331629
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -0,0 +1,2029 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/ClientID.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
+Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+const PING_FORMAT_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_SHUTDOWN = "shutdown";
+const REASON_TEST_PING = "test-ping";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED";
+const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
+const ADDON_NAME = "Telemetry test addon";
+const ADDON_HISTOGRAM = "addon-histogram";
+// Add some unicode characters here to ensure that sending them works correctly.
+const SHUTDOWN_TIME = 10000;
+const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const NUMBER_OF_THREADS_TO_LAUNCH = 30;
+var gNumberOfThreadsLaunched = 0;
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+const PREF_SERVER = PREF_BRANCH + "server";
+const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+const DATAREPORTING_DIR = "datareporting";
+const ABORTED_PING_FILE_NAME = "aborted-session-ping";
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
+});
+
+var gClientID = null;
+var gMonotonicNow = 0;
+
+function generateUUID() {
+ let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ // strip {}
+ return str.substring(1, str.length - 1);
+}
+
+function truncateDateToDays(date) {
+ return new Date(date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ 0, 0, 0, 0);
+}
+
+function sendPing() {
+ TelemetrySession.gatherStartup();
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ return TelemetrySession.testPing();
+ }
+ TelemetrySend.setServer("http://doesnotexist");
+ return TelemetrySession.testPing();
+}
+
+function fakeGenerateUUID(sessionFunc, subsessionFunc) {
+ let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
+ session.Policy.generateSessionUUID = sessionFunc;
+ session.Policy.generateSubsessionUUID = subsessionFunc;
+}
+
+function fakeIdleNotification(topic) {
+ let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
+ return session.TelemetryScheduler.observe(null, topic, null);
+}
+
+function setupTestData() {
+
+ Services.startup.interrupted = true;
+ Telemetry.registerAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM,
+ Telemetry.HISTOGRAM_LINEAR,
+ 1, 5, 6);
+ let h1 = Telemetry.getAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM);
+ h1.add(1);
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ h2.add();
+
+ let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ k1.add("a");
+ k1.add("a");
+ k1.add("b");
+}
+
+function getSavedPingFile(basename) {
+ let tmpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let pingFile = tmpDir.clone();
+ pingFile.append(basename);
+ if (pingFile.exists()) {
+ pingFile.remove(true);
+ }
+ do_register_cleanup(function () {
+ try {
+ pingFile.remove(true);
+ } catch (e) {
+ }
+ });
+ return pingFile;
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type", "id", "creationDate", "version", "application", "payload"
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + "must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version.");
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f],
+ f + " must have the correct value.");
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok("architecture" in aPing.application,
+ "The application section must have an architecture field.");
+ Assert.ok("channel" in aPing.application,
+ "The application section must have a channel field.");
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+function checkPayloadInfo(data) {
+ const ALLOWED_REASONS = [
+ "environment-change", "shutdown", "daily", "saved-session", "test-ping"
+ ];
+ let numberCheck = arg => { return (typeof arg == "number"); };
+ let positiveNumberCheck = arg => { return numberCheck(arg) && (arg >= 0); };
+ let stringCheck = arg => { return (typeof arg == "string") && (arg != ""); };
+ let revisionCheck = arg => {
+ return (Services.appinfo.isOfficial) ? stringCheck(arg) : (typeof arg == "string");
+ };
+ let uuidCheck = arg => {
+ return UUID_REGEX.test(arg);
+ };
+ let isoDateCheck = arg => {
+ // We expect use of this version of the ISO format:
+ // 2015-04-12T18:51:19.1+00:00
+ const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/;
+ return stringCheck(arg) && !Number.isNaN(Date.parse(arg)) &&
+ isoDateRegEx.test(arg);
+ };
+
+ const EXPECTED_INFO_FIELDS_TYPES = {
+ reason: stringCheck,
+ revision: revisionCheck,
+ timezoneOffset: numberCheck,
+ sessionId: uuidCheck,
+ subsessionId: uuidCheck,
+ // Special cases: previousSessionId and previousSubsessionId are null on first run.
+ previousSessionId: (arg) => { return (arg) ? uuidCheck(arg) : true; },
+ previousSubsessionId: (arg) => { return (arg) ? uuidCheck(arg) : true; },
+ subsessionCounter: positiveNumberCheck,
+ profileSubsessionCounter: positiveNumberCheck,
+ sessionStartDate: isoDateCheck,
+ subsessionStartDate: isoDateCheck,
+ subsessionLength: positiveNumberCheck,
+ };
+
+ for (let f in EXPECTED_INFO_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+
+ let checkFunc = EXPECTED_INFO_FIELDS_TYPES[f];
+ Assert.ok(checkFunc(data[f]),
+ f + " must have the correct type and valid data " + data[f]);
+ }
+
+ // Previous buildId is not mandatory.
+ if (data.previousBuildId) {
+ Assert.ok(stringCheck(data.previousBuildId));
+ }
+
+ Assert.ok(ALLOWED_REASONS.find(r => r == data.reason),
+ "Payload must contain an allowed reason.");
+
+ Assert.ok(Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate));
+ Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter);
+ Assert.ok(data.timezoneOffset >= -12*60, "The timezone must be in a valid range.");
+ Assert.ok(data.timezoneOffset <= 12*60, "The timezone must be in a valid range.");
+}
+
+function checkScalars(processes) {
+ // Check that the scalars section is available in the ping payload.
+ const parentProcess = processes.parent;
+ Assert.ok("scalars" in parentProcess, "The scalars section must be available in the parent process.");
+ Assert.ok("keyedScalars" in parentProcess, "The keyedScalars section must be available in the parent process.");
+ Assert.equal(typeof parentProcess.scalars, "object", "The scalars entry must be an object.");
+ Assert.equal(typeof parentProcess.keyedScalars, "object", "The keyedScalars entry must be an object.");
+
+ let checkScalar = function(scalar) {
+ // Check if the value is of a supported type.
+ const valueType = typeof(scalar);
+ switch (valueType) {
+ case "string":
+ Assert.ok(scalar.length <= 50,
+ "String values can't have more than 50 characters");
+ break;
+ case "number":
+ Assert.ok(scalar >= 0,
+ "We only support unsigned integer values in scalars.");
+ break;
+ case "boolean":
+ Assert.ok(true,
+ "Boolean scalar found.");
+ break;
+ default:
+ Assert.ok(false,
+ name + " contains an unsupported value type (" + valueType + ")");
+ }
+ }
+
+ // Check that we have valid scalar entries.
+ const scalars = parentProcess.scalars;
+ for (let name in scalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ checkScalar(scalar[name]);
+ }
+
+ // Check that we have valid keyed scalar entries.
+ const keyedScalars = parentProcess.keyedScalars;
+ for (let name in keyedScalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ Assert.ok(Object.keys(keyedScalars[name]).length,
+ "The reported keyed scalars must contain at least 1 key.");
+ for (let key in keyedScalars[name]) {
+ Assert.equal(typeof key, "string", "Keyed scalar keys must be strings.");
+ Assert.ok(key.length <= 70, "Keyed scalar keys can't have more than 70 characters.");
+ checkScalar(scalar[name][key]);
+ }
+ }
+}
+
+function checkEvents(processes) {
+ // Check that the events section is available in the ping payload.
+ const parent = processes.parent;
+ Assert.ok("events" in parent, "The events section must be available in the parent process.");
+
+ // Check that the events section has the right format.
+ Assert.ok(Array.isArray(parent.events), "The events entry must be an array.");
+ for (let [ts, category, method, object, value, extra] of parent.events) {
+ Assert.equal(typeof(ts), "number", "Timestamp field should be a number.");
+ Assert.greaterOrEqual(ts, 0, "Timestamp should be >= 0.");
+
+ Assert.equal(typeof(category), "string", "Category should have the right type.");
+ Assert.lessOrEqual(category.length, 100, "Category should have the right string length.");
+
+ Assert.equal(typeof(method), "string", "Method should have the right type.");
+ Assert.lessOrEqual(method.length, 40, "Method should have the right string length.");
+
+ Assert.equal(typeof(object), "string", "Object should have the right type.");
+ Assert.lessOrEqual(object.length, 40, "Object should have the right string length.");
+
+ Assert.ok(value === null || typeof(value) === "string",
+ "Value should be null or a string.");
+ if (value) {
+ Assert.lessOrEqual(value.length, 100, "Value should have the right string length.");
+ }
+
+ Assert.ok(extra === null || typeof(extra) === "object",
+ "Extra should be null or an object.");
+ if (extra) {
+ let keys = Object.keys(extra);
+ let keyTypes = keys.map(k => typeof(k));
+ Assert.lessOrEqual(keys.length, 20, "Should not have too many extra keys.");
+ Assert.ok(keyTypes.every(t => t === "string"),
+ "All extra keys should be strings.");
+ Assert.ok(keys.every(k => k.length <= 20),
+ "All extra keys should have the right string length.");
+
+ let values = Object.values(extra);
+ let valueTypes = values.map(v => typeof(v));
+ Assert.ok(valueTypes.every(t => t === "string"),
+ "All extra values should be strings.");
+ Assert.ok(values.every(v => v.length <= 100),
+ "All extra values should have the right string length.");
+ }
+ }
+}
+
+function checkPayload(payload, reason, successfulPings, savedPings) {
+ Assert.ok("info" in payload, "Payload must contain an info section.");
+ checkPayloadInfo(payload.info);
+
+ Assert.ok(payload.simpleMeasurements.totalTime >= 0);
+ Assert.ok(payload.simpleMeasurements.uptime >= 0);
+ Assert.equal(payload.simpleMeasurements.startupInterrupted, 1);
+ Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);
+ Assert.equal(payload.simpleMeasurements.savedPings, savedPings);
+ Assert.ok("maximalNumberOfConcurrentThreads" in payload.simpleMeasurements);
+ Assert.ok(payload.simpleMeasurements.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched);
+
+ let activeTicks = payload.simpleMeasurements.activeTicks;
+ Assert.ok(activeTicks >= 0);
+
+ Assert.equal(payload.simpleMeasurements.failedProfileLockCount,
+ FAILED_PROFILE_LOCK_ATTEMPTS);
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let failedProfileLocksFile = profileDirectory.clone();
+ failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
+ Assert.ok(!failedProfileLocksFile.exists());
+
+
+ let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
+ if (isWindows) {
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
+ }
+
+ const TELEMETRY_SEND_SUCCESS = "TELEMETRY_SEND_SUCCESS";
+ const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
+ const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
+ const TELEMETRY_TEST_COUNT = "TELEMETRY_TEST_COUNT";
+ const TELEMETRY_TEST_KEYED_FLAG = "TELEMETRY_TEST_KEYED_FLAG";
+ const TELEMETRY_TEST_KEYED_COUNT = "TELEMETRY_TEST_KEYED_COUNT";
+
+ if (successfulPings > 0) {
+ Assert.ok(TELEMETRY_SEND_SUCCESS in payload.histograms);
+ }
+ Assert.ok(TELEMETRY_TEST_FLAG in payload.histograms);
+ Assert.ok(TELEMETRY_TEST_COUNT in payload.histograms);
+
+ Assert.ok(!(IGNORE_CLONED_HISTOGRAM in payload.histograms));
+
+ // Flag histograms should automagically spring to life.
+ const expected_flag = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 3,
+ values: {0:1, 1:0},
+ sum: 0
+ };
+ let flag = payload.histograms[TELEMETRY_TEST_FLAG];
+ Assert.equal(uneval(flag), uneval(expected_flag));
+
+ // We should have a test count.
+ const expected_count = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: {0:1, 1:0},
+ sum: 1,
+ };
+ let count = payload.histograms[TELEMETRY_TEST_COUNT];
+ Assert.equal(uneval(count), uneval(expected_count));
+
+ // There should be one successful report from the previous telemetry ping.
+ if (successfulPings > 0) {
+ const expected_tc = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 2,
+ values: {0:2, 1:successfulPings, 2:0},
+ sum: successfulPings
+ };
+ let tc = payload.histograms[TELEMETRY_SUCCESS];
+ Assert.equal(uneval(tc), uneval(expected_tc));
+ }
+
+ // The ping should include data from memory reporters. We can't check that
+ // this data is correct, because we can't control the values returned by the
+ // memory reporters. But we can at least check that the data is there.
+ //
+ // It's important to check for the presence of reporters with a mix of units,
+ // because TelemetryController has separate logic for each one. But we can't
+ // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because
+ // Telemetry doesn't touch a memory reporter with these units that's
+ // available on all platforms.
+
+ Assert.ok('MEMORY_JS_GC_HEAP' in payload.histograms); // UNITS_BYTES
+ Assert.ok('MEMORY_JS_COMPARTMENTS_SYSTEM' in payload.histograms); // UNITS_COUNT
+
+ // We should have included addon histograms.
+ Assert.ok("addonHistograms" in payload);
+ Assert.ok(ADDON_NAME in payload.addonHistograms);
+ Assert.ok(ADDON_HISTOGRAM in payload.addonHistograms[ADDON_NAME]);
+
+ Assert.ok(("mainThread" in payload.slowSQL) &&
+ ("otherThreads" in payload.slowSQL));
+
+ Assert.ok(("IceCandidatesStats" in payload.webrtc) &&
+ ("webrtc" in payload.webrtc.IceCandidatesStats));
+
+ // Check keyed histogram payload.
+
+ Assert.ok("keyedHistograms" in payload);
+ let keyedHistograms = payload.keyedHistograms;
+ Assert.ok(!(TELEMETRY_TEST_KEYED_FLAG in keyedHistograms));
+ Assert.ok(TELEMETRY_TEST_KEYED_COUNT in keyedHistograms);
+
+ const expected_keyed_count = {
+ "a": {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: {0:2, 1:0},
+ sum: 2,
+ },
+ "b": {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: {0:1, 1:0},
+ sum: 1,
+ },
+ };
+ Assert.deepEqual(expected_keyed_count, keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]);
+
+ Assert.ok("processes" in payload, "The payload must have a processes section.");
+ Assert.ok("parent" in payload.processes, "There must be at least a parent process.");
+ checkScalars(payload.processes);
+ checkEvents(payload.processes);
+}
+
+function writeStringToFile(file, contents) {
+ let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER, ostream.DEFER_OPEN);
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function write_fake_shutdown_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.ShutdownTime.txt");
+ let contents = "" + SHUTDOWN_TIME;
+ writeStringToFile(file, contents);
+}
+
+function write_fake_failedprofilelocks_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.FailedProfileLocks.txt");
+ let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
+ writeStringToFile(file, contents);
+}
+
+add_task(function* test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
+
+ // Make it look like we've previously failed to lock a profile a couple times.
+ write_fake_failedprofilelocks_file();
+
+ // Make it look like we've shutdown before.
+ write_fake_shutdown_file();
+
+ let currentMaxNumberOfThreads = Telemetry.maximalNumberOfConcurrentThreads;
+ do_check_true(currentMaxNumberOfThreads > 0);
+
+ // Try to augment the maximal number of threads currently launched
+ let threads = [];
+ try {
+ for (let i = 0; i < currentMaxNumberOfThreads + 10; ++i) {
+ threads.push(Services.tm.newThread(0));
+ }
+ } catch (ex) {
+ // If memory is too low, it is possible that not all threads will be launched.
+ }
+ gNumberOfThreadsLaunched = threads.length;
+
+ do_check_true(Telemetry.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched);
+
+ do_register_cleanup(function() {
+ threads.forEach(function(thread) {
+ thread.shutdown();
+ });
+ });
+
+ yield new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve)));
+});
+
+add_task(function* asyncSetup() {
+ yield TelemetryController.testSetup();
+ // Load the client ID from the client ID provider to check for pings sanity.
+ gClientID = yield ClientID.getClientID();
+});
+
+// Ensures that expired histograms are not part of the payload.
+add_task(function* test_expiredHistogram() {
+
+ let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED");
+
+ dummy.add(1);
+
+ do_check_eq(TelemetrySession.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
+});
+
+// Sends a ping to a non existing server. If we remove this test, we won't get
+// all the histograms we need in the main ping.
+add_task(function* test_noServerPing() {
+ yield sendPing();
+ // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms
+ // are initialised. See bug 1131585.
+ yield sendPing();
+ // Allowing Telemetry to persist unsent pings as pending. If omitted may cause
+ // problems to the consequent tests.
+ yield TelemetryController.testShutdown();
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(function* test_simplePing() {
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.start();
+ Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port);
+
+ let now = new Date(2020, 1, 1, 12, 0, 0);
+ let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5000);
+
+ const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
+ const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
+ fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
+ yield TelemetryController.testReset();
+
+ // Session and subsession start dates are faked during TelemetrySession setup. We can
+ // now fake the session duration.
+ const SESSION_DURATION_IN_MINUTES = 15;
+ fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + SESSION_DURATION_IN_MINUTES * 60 * 1000);
+
+ yield sendPing();
+ let ping = yield PingServer.promiseNextPing();
+
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that we get the data we expect.
+ let payload = ping.payload;
+ Assert.equal(payload.info.sessionId, expectedSessionUUID);
+ Assert.equal(payload.info.subsessionId, expectedSubsessionUUID);
+ let sessionStartDate = new Date(payload.info.sessionStartDate);
+ Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString());
+ let subsessionStartDate = new Date(payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+ Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60);
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(generateUUID, generateUUID);
+});
+
+// Saves the current session histograms, reloads them, performs a ping
+// and checks that the dummy http server received both the previously
+// saved ping and the new one.
+add_task(function* test_saveLoadPing() {
+ // Let's start out with a defined state.
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Setup test data and trigger pings.
+ setupTestData();
+ yield TelemetrySession.testSavePendingPing();
+ yield sendPing();
+
+ // Get requests received by dummy server.
+ const requests = yield PingServer.promiseNextRequests(2);
+
+ for (let req of requests) {
+ Assert.equal(req.getHeader("content-type"), "application/json; charset=UTF-8",
+ "The request must have the correct content-type.");
+ }
+
+ // We decode both requests to check for the |reason|.
+ let pings = Array.from(requests, decodeRequestPayload);
+
+ // Check we have the correct two requests. Ordering is not guaranteed. The ping type
+ // is encoded in the URL.
+ if (pings[0].type != PING_TYPE_MAIN) {
+ pings.reverse();
+ }
+
+ checkPingFormat(pings[0], PING_TYPE_MAIN, true, true);
+ checkPayload(pings[0].payload, REASON_TEST_PING, 0, 1);
+ checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true);
+ checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0, 0);
+});
+
+add_task(function* test_checkSubsessionScalars() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ // Clear the scalars.
+ Telemetry.clearScalars();
+ yield TelemetryController.testReset();
+
+ // Set some scalars.
+ const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+ const STRING_SCALAR = "telemetry.test.string_kind";
+ let expectedUint = 37;
+ let expectedString = "Test value. Yay.";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+
+ // Check that scalars are not available in classic pings but are in subsession
+ // pings. Also clear the subsession.
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+
+ const TEST_SCALARS = [ UINT_SCALAR, STRING_SCALAR ];
+ for (let name of TEST_SCALARS) {
+ // Scalar must be reported in subsession pings (e.g. main).
+ Assert.ok(name in subsession.processes.parent.scalars,
+ name + " must be reported in a subsession ping.");
+ }
+ // No scalar must be reported in classic pings (e.g. saved-session).
+ Assert.ok(Object.keys(classic.processes.parent.scalars).length == 0,
+ "Scalars must not be reported in a classic ping.");
+
+ // And make sure that we're getting the right values in the
+ // subsession ping.
+ Assert.equal(subsession.processes.parent.scalars[UINT_SCALAR], expectedUint,
+ UINT_SCALAR + " must contain the expected value.");
+ Assert.equal(subsession.processes.parent.scalars[STRING_SCALAR], expectedString,
+ STRING_SCALAR + " must contain the expected value.");
+
+ // Since we cleared the subsession in the last getPayload(), check that
+ // breaking subsessions clears the scalars.
+ subsession = TelemetrySession.getPayload("environment-change");
+ for (let name of TEST_SCALARS) {
+ Assert.ok(!(name in subsession.processes.parent.scalars),
+ name + " must be cleared with the new subsession.");
+ }
+
+ // Check if setting the scalars again works as expected.
+ expectedUint = 85;
+ expectedString = "A creative different value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ subsession = TelemetrySession.getPayload("environment-change");
+ Assert.equal(subsession.processes.parent.scalars[UINT_SCALAR], expectedUint,
+ UINT_SCALAR + " must contain the expected value.");
+ Assert.equal(subsession.processes.parent.scalars[STRING_SCALAR], expectedString,
+ STRING_SCALAR + " must contain the expected value.");
+});
+
+add_task(function* test_checkSubsessionEvents() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ // Clear the events.
+ Telemetry.clearEvents();
+ yield TelemetryController.testReset();
+
+ // Record some events.
+ let expected = [
+ ["telemetry.test", "test1", "object1", "a", null],
+ ["telemetry.test", "test1", "object1", null, {key1: "value"}],
+ ];
+ for (let event of expected) {
+ Telemetry.recordEvent(...event);
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let e of expected) {
+ while ((e.length >= 3) && (e[e.length - 1] === null)) {
+ e.pop();
+ }
+ }
+
+ // Check that events are not available in classic pings but are in subsession
+ // pings. Also clear the subsession.
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+
+ Assert.ok("events" in classic.processes.parent, "Should have an events field in classic payload.");
+ Assert.ok("events" in subsession.processes.parent, "Should have an events field in subsession payload.");
+
+ // They should be empty in the classic payload.
+ Assert.deepEqual(classic.processes.parent.events, [], "Events in classic payload should be empty.");
+
+ // In the subsession payload, they should contain the recorded test events.
+ let events = subsession.processes.parent.events.filter(e => e[1] === "telemetry.test");
+ Assert.equal(events.length, expected.length, "Should have the right amount of events in the payload.");
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(events[i].slice(1), expected[i],
+ "Should have the right event data in the ping.");
+ }
+
+ // As we cleared the subsession above, the events entry should now be empty.
+ subsession = TelemetrySession.getPayload("environment-change", false);
+ Assert.ok("events" in subsession.processes.parent, "Should have an events field in subsession payload.");
+ events = subsession.processes.parent.events.filter(e => e[1] === "telemetry.test");
+ Assert.equal(events.length, 0, "Should have no test events in the subsession payload now.");
+});
+
+add_task(function* test_checkSubsessionHistograms() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ let now = new Date(2020, 1, 1, 12, 0, 0);
+ let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ yield TelemetryController.testReset();
+
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+ const registeredIds =
+ new Set(Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []));
+
+ const stableHistograms = new Set([
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_RELEASE_OPTOUT",
+ "TELEMETRY_TEST_RELEASE_OPTIN",
+ "STARTUP_CRASH_DETECTED",
+ ]);
+
+ const stableKeyedHistograms = new Set([
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ ]);
+
+ // Compare the two sets of histograms.
+ // The "subsession" histograms should match the registered
+ // "classic" histograms. However, histograms can change
+ // between us collecting the different payloads, so we only
+ // check for deep equality on known stable histograms.
+ checkHistograms = (classic, subsession) => {
+ for (let id of Object.keys(classic)) {
+ if (!registeredIds.has(id)) {
+ continue;
+ }
+
+ Assert.ok(id in subsession);
+ if (stableHistograms.has(id)) {
+ Assert.deepEqual(classic[id],
+ subsession[id]);
+ } else {
+ Assert.equal(classic[id].histogram_type,
+ subsession[id].histogram_type);
+ }
+ }
+ };
+
+ // Same as above, except for keyed histograms.
+ checkKeyedHistograms = (classic, subsession) => {
+ for (let id of Object.keys(classic)) {
+ if (!registeredIds.has(id)) {
+ continue;
+ }
+
+ Assert.ok(id in subsession);
+ if (stableKeyedHistograms.has(id)) {
+ Assert.deepEqual(classic[id],
+ subsession[id]);
+ }
+ }
+ };
+
+ // Both classic and subsession payload histograms should start the same.
+ // The payloads should be identical for now except for the reason.
+ count.clear();
+ keyed.clear();
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change");
+
+ Assert.equal(classic.info.reason, "gather-payload");
+ Assert.equal(subsession.info.reason, "environment-change");
+ Assert.ok(!(COUNT_ID in classic.histograms));
+ Assert.ok(!(COUNT_ID in subsession.histograms));
+ Assert.ok(!(KEYED_ID in classic.keyedHistograms));
+ Assert.ok(!(KEYED_ID in subsession.keyedHistograms));
+
+ checkHistograms(classic.histograms, subsession.histograms);
+ checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms);
+
+ // Adding values should get picked up in both.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change");
+
+ Assert.ok(COUNT_ID in classic.histograms);
+ Assert.ok(COUNT_ID in subsession.histograms);
+ Assert.ok(KEYED_ID in classic.keyedHistograms);
+ Assert.ok(KEYED_ID in subsession.keyedHistograms);
+ Assert.equal(classic.histograms[COUNT_ID].sum, 1);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1);
+
+ checkHistograms(classic.histograms, subsession.histograms);
+ checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms);
+
+ // Values should still reset properly.
+ count.clear();
+ keyed.clear();
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change");
+
+ Assert.ok(!(COUNT_ID in classic.histograms));
+ Assert.ok(!(COUNT_ID in subsession.histograms));
+ Assert.ok(!(KEYED_ID in classic.keyedHistograms));
+ Assert.ok(!(KEYED_ID in subsession.keyedHistograms));
+
+ checkHistograms(classic.histograms, subsession.histograms);
+ checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms);
+
+ // Adding values should get picked up in both.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change");
+
+ Assert.ok(COUNT_ID in classic.histograms);
+ Assert.ok(COUNT_ID in subsession.histograms);
+ Assert.ok(KEYED_ID in classic.keyedHistograms);
+ Assert.ok(KEYED_ID in subsession.keyedHistograms);
+ Assert.equal(classic.histograms[COUNT_ID].sum, 1);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1);
+
+ checkHistograms(classic.histograms, subsession.histograms);
+ checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms);
+
+ // We should be able to reset only the subsession histograms.
+ // First check that "snapshot and clear" still returns the old state...
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change", true);
+
+ let subsessionStartDate = new Date(classic.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+ subsessionStartDate = new Date(subsession.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+ checkHistograms(classic.histograms, subsession.histograms);
+ checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms);
+
+ // ... then check that the next snapshot shows the subsession
+ // histograms got reset.
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change");
+
+ Assert.ok(COUNT_ID in classic.histograms);
+ Assert.ok(COUNT_ID in subsession.histograms);
+ Assert.equal(classic.histograms[COUNT_ID].sum, 1);
+ Assert.equal(subsession.histograms[COUNT_ID].sum, 0);
+
+ Assert.ok(KEYED_ID in classic.keyedHistograms);
+ Assert.ok(!(KEYED_ID in subsession.keyedHistograms));
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1);
+
+ // Adding values should get picked up in both again.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change");
+
+ Assert.ok(COUNT_ID in classic.histograms);
+ Assert.ok(COUNT_ID in subsession.histograms);
+ Assert.equal(classic.histograms[COUNT_ID].sum, 2);
+ Assert.equal(subsession.histograms[COUNT_ID].sum, 1);
+
+ Assert.ok(KEYED_ID in classic.keyedHistograms);
+ Assert.ok(KEYED_ID in subsession.keyedHistograms);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 2);
+ Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 2);
+ Assert.equal(subsession.keyedHistograms[KEYED_ID]["a"].sum, 1);
+ Assert.equal(subsession.keyedHistograms[KEYED_ID]["b"].sum, 1);
+});
+
+add_task(function* test_checkSubsessionData() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ // Keep track of the active ticks count if the session recorder is available.
+ let sessionRecorder = TelemetryController.getSessionRecorder();
+ let activeTicksAtSubsessionStart = sessionRecorder.activeTicks;
+ let expectedActiveTicks = activeTicksAtSubsessionStart;
+
+ incrementActiveTicks = () => {
+ sessionRecorder.incrementActiveTicks();
+ ++expectedActiveTicks;
+ }
+
+ yield TelemetryController.testReset();
+
+ // Both classic and subsession payload data should be the same on the first subsession.
+ incrementActiveTicks();
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change");
+ Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
+ "Classic pings must count active ticks since the beginning of the session.");
+ Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks,
+ "Subsessions must count active ticks as classic pings on the first subsession.");
+
+ // Start a new subsession and check that the active ticks are correctly reported.
+ incrementActiveTicks();
+ activeTicksAtSubsessionStart = sessionRecorder.activeTicks;
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change", true);
+ Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
+ "Classic pings must count active ticks since the beginning of the session.");
+ Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks,
+ "Pings must not loose the tick count when starting a new subsession.");
+
+ // Get a new subsession payload without clearing the subsession.
+ incrementActiveTicks();
+ classic = TelemetrySession.getPayload();
+ subsession = TelemetrySession.getPayload("environment-change");
+ Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
+ "Classic pings must count active ticks since the beginning of the session.");
+ Assert.equal(subsession.simpleMeasurements.activeTicks,
+ expectedActiveTicks - activeTicksAtSubsessionStart,
+ "Subsessions must count active ticks since the last new subsession.");
+});
+
+add_task(function* test_dailyCollection() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let now = new Date(2030, 1, 1, 12, 0, 0);
+ let nowDay = new Date(2030, 1, 1, 0, 0, 0);
+ let schedulerTickCallback = null;
+
+ PingServer.clearRequests();
+
+ fakeNow(now);
+
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+
+ // Init and check timer.
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testSetup();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+
+ // Set histograms to expected state.
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ keyed.add("b", 1);
+
+ // Make sure the daily ping gets triggered.
+ let expectedDate = nowDay;
+ now = futureDate(nowDay, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ Assert.ok(!!schedulerTickCallback);
+ // Run a scheduler tick: it should trigger the daily ping.
+ yield schedulerTickCallback();
+
+ // Collect the daily ping.
+ let ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 2);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ // Run a scheduler tick. Trigger and collect another ping. The histograms should be reset.
+ yield schedulerTickCallback();
+
+ ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0);
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another daily ping, with the histograms being set again.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ yield schedulerTickCallback();
+ ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 1);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_dailyDuplication() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ yield TelemetrySend.reset();
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+
+ // Make sure the daily ping gets triggered at midnight.
+ // We need to make sure that we trigger this after the period where we wait for
+ // the user to become idle.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // We don't expect to receive any other daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No more daily pings should be sent/received in this test.");
+ });
+
+ // Set the current time to a bit after midnight.
+ let secondDailyDue = new Date(firstDailyDue);
+ secondDailyDue.setHours(0);
+ secondDailyDue.setMinutes(15);
+ fakeNow(secondDailyDue);
+
+ // Run a scheduler tick: it should NOT trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ PingServer.resetPingHandler();
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_dailyOverdue() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 11, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+
+ // Skip one hour ahead: nothing should be due.
+ now.setHours(now.getHours() + 1);
+ fakeNow(now);
+
+ // Assert if we receive something!
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No daily ping should be received if not overdue!.");
+ });
+
+ // This tick should not trigger any daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ PingServer.clearRequests();
+
+ // Simulate an overdue ping: we're not close to midnight, but the last daily ping
+ // time is too long ago.
+ let dailyOverdue = new Date(2030, 1, 2, 13, 0, 0);
+ fakeNow(dailyOverdue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_environmentChange() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Preferences.reset(PREF_TEST);
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
+ ]);
+
+ // Setup.
+ yield TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set histograms to expected state.
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // Trigger and collect environment-change ping.
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+ let startDay = truncateDateToDays(now);
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Preferences.set(PREF_TEST, 1);
+ let ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startDay.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startDay = truncateDateToDays(now);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Preferences.set(PREF_TEST, 2);
+ ping = yield PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startDay.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0);
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+});
+
+add_task(function* test_savedPingsOnShutdown() {
+ // On desktop, we expect both "saved-session" and "shutdown" pings. We only expect
+ // the former on Android.
+ const expectedPingCount = (gIsAndroid) ? 1 : 2;
+ // Assure that we store the ping properly when saving sessions on shutdown.
+ // We make the TelemetryController shutdown to trigger a session save.
+ const dir = TelemetryStorage.pingDirectoryPath;
+ yield OS.File.removeDir(dir, {ignoreAbsent: true});
+ yield OS.File.makeDir(dir);
+ yield TelemetryController.testShutdown();
+
+ PingServer.clearRequests();
+ yield TelemetryController.testReset();
+
+ const pings = yield PingServer.promiseNextPings(expectedPingCount);
+
+ for (let ping of pings) {
+ Assert.ok("type" in ping);
+
+ let expectedReason =
+ (ping.type == PING_TYPE_SAVED_SESSION) ? REASON_SAVED_SESSION : REASON_SHUTDOWN;
+
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, expectedReason);
+ Assert.equal(ping.clientId, gClientID);
+ }
+});
+
+add_task(function* test_savedSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ yield OS.File.makeDir(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file.
+ const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ yield CommonUtils.writeJSON(sessionState, dataFilePath);
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Preferences.reset(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
+ ]);
+
+ // We expect one new subsession when starting TelemetrySession and one after triggering
+ // an environment change.
+ const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
+
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ // Start TelemetrySession so that it loads the session data file.
+ yield TelemetryController.testReset();
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Watch a test preference, trigger and environment change and wait for it to propagate.
+ // _watchPreferences triggers a subsession notification
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+ fakeNow(new Date(2050, 1, 1, 12, 0, 0));
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let changePromise = new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener("test_fake_change", resolve));
+ Preferences.set(PREF_TEST, 1);
+ yield changePromise;
+ TelemetryEnvironment.unregisterChangeListener("test_fake_change");
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ yield TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(generateUUID, generateUUID);
+
+ // Load back the serialised session data.
+ let data = yield CommonUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(function* test_sessionData_ShortSession() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
+
+ // Shut down Telemetry and remove the session state file.
+ yield TelemetryController.testReset();
+ yield OS.File.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
+
+ // We intentionally don't wait for the setup to complete and shut down to simulate
+ // short sessions. We expect the profile subsession counter to be 1.
+ TelemetryController.testReset();
+ yield TelemetryController.testShutdown();
+
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Restore the UUID generation functions.
+ fakeGenerateUUID(generateUUID, generateUUID);
+
+ // Start TelemetryController so that it loads the session data file. We expect the profile
+ // subsession counter to be incremented by 1 again.
+ yield TelemetryController.testReset();
+
+ // We expect 2 profile subsession counter updates.
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, 2);
+ Assert.equal(payload.info.previousSessionId, expectedSessionUUID);
+ Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+});
+
+add_task(function* test_invalidSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ yield OS.File.makeDir(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file. This should fail to parse.
+ const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
+ const unparseableData = "{asdf:@äü";
+ OS.File.writeAtomic(dataFilePath, unparseableData,
+ {encoding: "utf-8", tmpPath: dataFilePath + ".tmp"});
+
+ // Start TelemetryController so that it loads the session data file.
+ yield TelemetryController.testReset();
+
+ // The session data file should not load. Only expect the current subsession.
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Write test data to the session data file. This should fail validation.
+ const sessionState = {
+ profileSubsessionCounter: "not-a-number?",
+ someOtherField: 12,
+ };
+ yield CommonUtils.writeJSON(sessionState, dataFilePath);
+
+ // The session data file should not load. Only expect the current subsession.
+ const expectedSubsessions = 1;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
+
+ // Start TelemetryController so that it loads the session data file.
+ yield TelemetryController.testReset();
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ yield TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(generateUUID, generateUUID);
+
+ // Load back the serialised session data.
+ let data = yield CommonUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(function* test_abortedSession() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+
+ Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
+ "Telemetry must create the aborted session directory when starting.");
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ yield schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok((yield OS.File.exists(ABORTED_FILE)),
+ "There must be an aborted session ping.");
+
+ // This ping is not yet in the pending pings folder, so we can't access it using
+ // TelemetryStorage.popPendingPings().
+ let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
+ let abortedSessionPing = JSON.parse(pingContent);
+
+ // Validate the ping.
+ checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
+
+ // Trigger a another aborted-session ping and check that it overwrites the previous one.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ yield schedulerTickCallback();
+
+ pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
+ let updatedAbortedSessionPing = JSON.parse(pingContent);
+ checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(updatedAbortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
+ Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
+ Assert.notEqual(abortedSessionPing.creationDate, updatedAbortedSessionPing.creationDate);
+
+ yield TelemetryController.testShutdown();
+ Assert.ok(!(yield OS.File.exists(ABORTED_FILE)),
+ "No aborted session ping must be available after a shutdown.");
+
+ // Write the ping to the aborted-session file. TelemetrySession will add it to the
+ // saved pings directory when it starts.
+ yield TelemetryStorage.savePingToFile(abortedSessionPing, ABORTED_FILE, false);
+ Assert.ok((yield OS.File.exists(ABORTED_FILE)),
+ "The aborted session ping must exist in the aborted session ping directory.");
+
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ yield TelemetryController.testReset();
+
+ Assert.ok(!(yield OS.File.exists(ABORTED_FILE)),
+ "The aborted session ping must be removed from the aborted session ping directory.");
+
+ // Restarting Telemetry again to trigger sending pings in TelemetrySend.
+ yield TelemetryController.testReset();
+
+ // We should have received an aborted-session ping.
+ const receivedPing = yield PingServer.promiseNextPing();
+ Assert.equal(receivedPing.type, PING_TYPE_MAIN, "Should have the correct type");
+ Assert.equal(receivedPing.payload.info.reason, REASON_ABORTED_SESSION, "Ping should have the correct reason");
+
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_abortedSession_Shutdown() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
+
+ let schedulerTickCallback = null;
+ let now = fakeNow(2040, 1, 1, 0, 0, 0);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+
+ Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
+ "Telemetry must create the aborted session directory when starting.");
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS));
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ yield schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok((yield OS.File.exists(ABORTED_FILE)), "There must be an aborted session ping.");
+
+ // Remove the aborted session file and then shut down to make sure exceptions (e.g file
+ // not found) do not compromise the shutdown.
+ yield OS.File.remove(ABORTED_FILE);
+
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_abortedDailyCoalescing() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
+
+ let schedulerTickCallback = null;
+ PingServer.clearRequests();
+
+ let nowDate = new Date(2009, 10, 18, 0, 0, 0);
+ fakeNow(nowDate);
+
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ yield TelemetryController.testReset();
+
+ Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
+ "Telemetry must create the aborted session directory when starting.");
+
+ // Delay the callback around midnight so that the aborted-session ping gets merged with the
+ // daily ping.
+ let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
+ fakeNow(dailyDueDate);
+ // Trigger both the daily ping and the saved-session.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ yield schedulerTickCallback();
+
+ // Wait for the daily ping.
+ let dailyPing = yield PingServer.promiseNextPing();
+ Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);
+
+ // Check that an aborted session ping was also written to disk.
+ Assert.ok((yield OS.File.exists(ABORTED_FILE)),
+ "There must be an aborted session ping.");
+
+ // Read aborted session ping and check that the session/subsession ids equal the
+ // ones in the daily ping.
+ let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
+ let abortedSessionPing = JSON.parse(pingContent);
+ Assert.equal(abortedSessionPing.payload.info.sessionId, dailyPing.payload.info.sessionId);
+ Assert.equal(abortedSessionPing.payload.info.subsessionId, dailyPing.payload.info.subsessionId);
+
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_schedulerComputerSleep() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
+
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Remove any aborted-session ping from the previous tests.
+ yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2009, 10, 18, 0, 0, 0);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+
+ // Set the current time 3 days in the future at midnight, before running the callback.
+ nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY));
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ yield schedulerTickCallback();
+
+ let dailyPing = yield PingServer.promiseNextPing();
+ Assert.equal(dailyPing.payload.info.reason, REASON_DAILY,
+ "The wake notification should have triggered a daily ping.");
+ Assert.equal(dailyPing.creationDate, nowDate.toISOString(),
+ "The daily ping date should be correct.");
+
+ Assert.ok((yield OS.File.exists(ABORTED_FILE)),
+ "There must be an aborted session ping.");
+
+ // Now also test if we are sending a daily ping if we wake up on the next
+ // day even when the timer doesn't trigger.
+ // This can happen due to timeouts not running out during sleep times,
+ // see bug 1262386, bug 1204823 et al.
+ // Note that we don't get wake notifications on Linux due to bug 758848.
+ nowDate = fakeNow(futureDate(nowDate, 1 * MS_IN_ONE_DAY));
+
+ // We emulate the mentioned timeout behavior by sending the wake notification
+ // instead of triggering the timeout callback.
+ // This should trigger a daily ping, because we passed midnight.
+ Services.obs.notifyObservers(null, "wake_notification", null);
+
+ dailyPing = yield PingServer.promiseNextPing();
+ Assert.equal(dailyPing.payload.info.reason, REASON_DAILY,
+ "The wake notification should have triggered a daily ping.");
+ Assert.equal(dailyPing.creationDate, nowDate.toISOString(),
+ "The daily ping date should be correct.");
+
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_schedulerEnvironmentReschedules() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ // Reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Preferences.reset(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
+ ]);
+
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ yield TelemetryController.testReset();
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2060, 10, 18, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set the current time at midnight.
+ fakeNow(futureDate(nowDate, MS_IN_ONE_DAY));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+
+ // Trigger the environment change.
+ Preferences.set(PREF_TEST, 1);
+
+ // Wait for the environment-changed ping.
+ yield PingServer.promiseNextPing();
+
+ // We don't expect to receive any daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No ping should be sent/received in this test.");
+ });
+
+ // Execute one scheduler tick. It should not trigger a daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_schedulerNothingDue() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
+
+ // Remove any aborted-session ping from the previous tests.
+ yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+
+ // We don't expect to receive any ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No ping should be sent/received in this test.");
+ });
+
+ // Set a current date/time away from midnight, so that the daily ping doesn't get
+ // sent.
+ let nowDate = new Date(2009, 10, 18, 11, 0, 0);
+ fakeNow(nowDate);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+
+ // Delay the callback execution to a time when no ping should be due.
+ let nothingDueDate = futureDate(nowDate, ABORTED_SESSION_UPDATE_INTERVAL_MS / 2);
+ fakeNow(nothingDueDate);
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ yield schedulerTickCallback();
+
+ // Check that no aborted session ping was written to disk.
+ Assert.ok(!(yield OS.File.exists(ABORTED_FILE)));
+
+ yield TelemetryController.testShutdown();
+ PingServer.resetPingHandler();
+});
+
+add_task(function* test_pingExtendedStats() {
+ const EXTENDED_PAYLOAD_FIELDS = [
+ "chromeHangs", "threadHangStats", "log", "slowSQL", "fileIOReports", "lateWrites",
+ "addonHistograms", "addonDetails", "UIMeasurements", "webrtc"
+ ];
+
+ // Reset telemetry and disable sending extended statistics.
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ yield TelemetryController.testReset();
+ Telemetry.canRecordExtended = false;
+
+ yield sendPing();
+
+ let ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload does not contain extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(!(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload),
+ EXTENDED_PAYLOAD_FIELDS[f] + " must not be in the payload if the extended set is off.");
+ }
+
+ // We check this one separately so that we can reuse EXTENDED_PAYLOAD_FIELDS below, since
+ // slowSQLStartup might not be there.
+ Assert.ok(!("slowSQLStartup" in ping.payload),
+ "slowSQLStartup must not be sent if the extended set is off");
+
+ Assert.ok(!("addonManager" in ping.payload.simpleMeasurements),
+ "addonManager must not be sent if the extended set is off.");
+ Assert.ok(!("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent if the extended set is off.");
+
+ // Restore the preference.
+ Telemetry.canRecordExtended = true;
+
+ // Send a new ping that should contain the extended data.
+ yield sendPing();
+ ping = yield PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload now contains extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload,
+ EXTENDED_PAYLOAD_FIELDS[f] + " must be in the payload if the extended set is on.");
+ }
+
+ Assert.ok("addonManager" in ping.payload.simpleMeasurements,
+ "addonManager must be sent if the extended set is on.");
+ Assert.ok("UITelemetry" in ping.payload.simpleMeasurements,
+ "UITelemetry must be sent if the extended set is on.");
+});
+
+add_task(function* test_schedulerUserIdle() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
+ const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;
+
+ let now = new Date(2010, 1, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let schedulerTimeout = 0;
+ fakeSchedulerTimer((callback, timeout) => {
+ schedulerTimeout = timeout;
+ }, () => {});
+ yield TelemetryController.testReset();
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // When not idle, the scheduler should have a 5 minutes tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // Send an "idle" notification to the scheduler.
+ fakeIdleNotification("idle");
+
+ // When idle, the scheduler should have a 1hr tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS);
+
+ // Send an "active" notification to the scheduler.
+ fakeIdleNotification("active");
+
+ // When user is back active, the scheduler tick should be 5 minutes again.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // We should not miss midnight when going to idle.
+ now.setHours(23);
+ now.setMinutes(50);
+ fakeNow(now);
+ fakeIdleNotification("idle");
+ Assert.equal(schedulerTimeout, 10 * 60 * 1000);
+
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_DailyDueAndIdle() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ yield TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ // Faking scheduler timer has to happen before resetting TelemetryController
+ // to be effective.
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryController.testReset();
+
+ // Trigger the daily ping.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ let tickPromise = schedulerTickCallback();
+
+ // Send an idle and then an active user notification.
+ fakeIdleNotification("idle");
+ fakeIdleNotification("active");
+
+ // Wait on the tick promise.
+ yield tickPromise;
+
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_userIdleAndSchedlerTick() {
+ if (gIsAndroid || gIsGonk) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
+ yield TelemetryStorage.testClearPendingPings();
+ yield TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Move the current date/time to midnight.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // The active notification should trigger a scheduler tick. The latter will send the
+ // due daily ping.
+ fakeIdleNotification("active");
+
+ // Immediately running another tick should not send a daily ping again.
+ Assert.ok(!!schedulerTickCallback);
+ yield schedulerTickCallback();
+
+ // A new "idle" notification should not send a new daily ping.
+ fakeIdleNotification("idle");
+
+ yield TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ PingServer.resetPingHandler();
+ yield TelemetryController.testShutdown();
+});
+
+add_task(function* test_changeThrottling() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ let getSubsessionCount = () => {
+ return TelemetrySession.getPayload().info.subsessionCounter;
+ };
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
+ ]);
+ Preferences.reset(PREF_TEST);
+
+ let now = fakeNow(2050, 1, 2, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE);
+ yield TelemetryController.testReset();
+ Assert.equal(getSubsessionCount(), 1);
+
+ // Set the Environment preferences to watch.
+ TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // The first pref change should not trigger a notification.
+ Preferences.set(PREF_TEST, 1);
+ Assert.equal(getSubsessionCount(), 1);
+
+ // We should get a change notification after the 5min throttling interval.
+ fakeNow(futureDate(now, 5 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5 * MILLISECONDS_PER_MINUTE + 1);
+ Preferences.set(PREF_TEST, 2);
+ Assert.equal(getSubsessionCount(), 2);
+
+ // After that, changes should be throttled again.
+ now = fakeNow(futureDate(now, 1 * MILLISECONDS_PER_MINUTE));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 1 * MILLISECONDS_PER_MINUTE);
+ Preferences.set(PREF_TEST, 3);
+ Assert.equal(getSubsessionCount(), 2);
+
+ // ... for 5min.
+ now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1);
+ Preferences.set(PREF_TEST, 4);
+ Assert.equal(getSubsessionCount(), 3);
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_throttling");
+});
+
+add_task(function* stopServer() {
+ yield PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js
new file mode 100644
index 0000000000..d162d9b17b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var tmpScope = {};
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", tmpScope);
+var TelemetryStopwatch = tmpScope.TelemetryStopwatch;
+
+const HIST_NAME = "TELEMETRY_SEND_SUCCESS";
+const HIST_NAME2 = "RANGE_CHECKSUM_ERRORS";
+const KEYED_HIST = { id: "TELEMETRY_INVALID_PING_TYPE_SUBMITTED", key: "TEST" };
+
+var refObj = {}, refObj2 = {};
+
+var originalCount1, originalCount2;
+
+function run_test() {
+ let histogram = Telemetry.getHistogramById(HIST_NAME);
+ let snapshot = histogram.snapshot();
+ originalCount1 = snapshot.counts.reduce((a, b) => a += b);
+
+ histogram = Telemetry.getHistogramById(HIST_NAME2);
+ snapshot = histogram.snapshot();
+ originalCount2 = snapshot.counts.reduce((a, b) => a += b);
+
+ histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id);
+ snapshot = histogram.snapshot(KEYED_HIST.key);
+ originalCount3 = snapshot.counts.reduce((a, b) => a += b);
+
+ do_check_false(TelemetryStopwatch.start(3));
+ do_check_false(TelemetryStopwatch.start({}));
+ do_check_false(TelemetryStopwatch.start("", 3));
+ do_check_false(TelemetryStopwatch.start("", ""));
+ do_check_false(TelemetryStopwatch.start({}, {}));
+
+ do_check_true(TelemetryStopwatch.start("mark1"));
+ do_check_true(TelemetryStopwatch.start("mark2"));
+
+ do_check_true(TelemetryStopwatch.start("mark1", refObj));
+ do_check_true(TelemetryStopwatch.start("mark2", refObj));
+
+ // Same timer can't be re-started before being stopped
+ do_check_false(TelemetryStopwatch.start("mark1"));
+ do_check_false(TelemetryStopwatch.start("mark1", refObj));
+
+ // Can't stop a timer that was accidentaly started twice
+ do_check_false(TelemetryStopwatch.finish("mark1"));
+ do_check_false(TelemetryStopwatch.finish("mark1", refObj));
+
+ do_check_true(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM"));
+ do_check_false(TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM"));
+
+ do_check_true(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM", refObj));
+ do_check_false(TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM", refObj));
+
+ do_check_true(TelemetryStopwatch.start(HIST_NAME));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME2));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME2, refObj));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj2));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME2, refObj2));
+
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME2));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME, refObj));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME2, refObj));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME, refObj2));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME2, refObj2));
+
+ // Verify that TS.finish deleted the timers
+ do_check_false(TelemetryStopwatch.finish(HIST_NAME));
+ do_check_false(TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ // Verify that they can be used again
+ do_check_true(TelemetryStopwatch.start(HIST_NAME));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME));
+ do_check_true(TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ do_check_false(TelemetryStopwatch.finish("unknown-mark")); // Unknown marker
+ do_check_false(TelemetryStopwatch.finish("unknown-mark", {})); // Unknown object
+ do_check_false(TelemetryStopwatch.finish(HIST_NAME, {})); // Known mark on unknown object
+
+ // Test cancel
+ do_check_true(TelemetryStopwatch.start(HIST_NAME));
+ do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj));
+ do_check_true(TelemetryStopwatch.cancel(HIST_NAME));
+ do_check_true(TelemetryStopwatch.cancel(HIST_NAME, refObj));
+
+ // Verify that can not cancel twice
+ do_check_false(TelemetryStopwatch.cancel(HIST_NAME));
+ do_check_false(TelemetryStopwatch.cancel(HIST_NAME, refObj));
+
+ // Verify that cancel removes the timers
+ do_check_false(TelemetryStopwatch.finish(HIST_NAME));
+ do_check_false(TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ // Verify that keyed stopwatch reject invalid keys.
+ for (let key of [3, {}, ""]) {
+ do_check_false(TelemetryStopwatch.startKeyed(KEYED_HIST.id, key));
+ }
+
+ // Verify that keyed histograms can be started.
+ do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1"));
+ do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2"));
+ do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj));
+ do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2", refObj));
+
+ // Restarting keyed histograms should fail.
+ do_check_false(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1"));
+ do_check_false(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj));
+
+ // Finishing a stopwatch of a non existing histogram should return false.
+ do_check_false(TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2"));
+ do_check_false(TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2", refObj));
+
+ // Starting & finishing a keyed stopwatch for an existing histogram should work.
+ do_check_true(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ do_check_true(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ // Verify that TS.finish deleted the timers
+ do_check_false(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ // Verify that they can be used again
+ do_check_true(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ do_check_true(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ do_check_false(TelemetryStopwatch.finishKeyed("unknown-mark", "unknown-key"));
+ do_check_false(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, "unknown-key"));
+
+ // Verify that keyed histograms can only be canceled through "keyed" API.
+ do_check_true(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ do_check_false(TelemetryStopwatch.cancel(KEYED_HIST.id, KEYED_HIST.key));
+ do_check_true(TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ do_check_false(TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ finishTest();
+}
+
+function finishTest() {
+ let histogram = Telemetry.getHistogramById(HIST_NAME);
+ let snapshot = histogram.snapshot();
+ let newCount = snapshot.counts.reduce((a, b) => a += b);
+
+ do_check_eq(newCount - originalCount1, 5, "The correct number of histograms were added for histogram 1.");
+
+ histogram = Telemetry.getHistogramById(HIST_NAME2);
+ snapshot = histogram.snapshot();
+ newCount = snapshot.counts.reduce((a, b) => a += b);
+
+ do_check_eq(newCount - originalCount2, 3, "The correct number of histograms were added for histogram 2.");
+
+ histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id);
+ snapshot = histogram.snapshot(KEYED_HIST.key);
+ newCount = snapshot.counts.reduce((a, b) => a += b);
+
+ do_check_eq(newCount - originalCount3, 2, "The correct number of histograms were added for histogram 3.");
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
new file mode 100644
index 0000000000..75bf3157aa
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+// The @mozilla/xre/app-info;1 XPCOM object provided by the xpcshell test harness doesn't
+// implement the nsIXULAppInfo interface, which is needed by Services.jsm and
+// TelemetrySession.jsm. updateAppInfo() creates and registers a minimal mock app-info.
+Cu.import("resource://testing-common/AppInfo.jsm");
+updateAppInfo();
+
+var gGlobalScope = this;
+
+function getSimpleMeasurementsFromTelemetryController() {
+ return TelemetrySession.getPayload().simpleMeasurements;
+}
+
+add_task(function* test_setup() {
+ // Telemetry needs the AddonManager.
+ loadAddonManager();
+ // Make profile available for |TelemetryController.testShutdown()|.
+ do_get_profile();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ yield setEmptyPrefWatchlist();
+
+ yield new Promise(resolve =>
+ Services.telemetry.asyncFetchTelemetryData(resolve));
+});
+
+add_task(function* actualTest() {
+ yield TelemetryController.testSetup();
+
+ // Test the module logic
+ let tmp = {};
+ Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
+ let TelemetryTimestamps = tmp.TelemetryTimestamps;
+ let now = Date.now();
+ TelemetryTimestamps.add("foo");
+ do_check_true(TelemetryTimestamps.get().foo != null); // foo was added
+ do_check_true(TelemetryTimestamps.get().foo >= now); // foo has a reasonable value
+
+ // Add timestamp with value
+ // Use a value far in the future since TelemetryController substracts the time of
+ // process initialization.
+ const YEAR_4000_IN_MS = 64060588800000;
+ TelemetryTimestamps.add("bar", YEAR_4000_IN_MS);
+ do_check_eq(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar has the right value
+
+ // Can't add the same timestamp twice
+ TelemetryTimestamps.add("bar", 2);
+ do_check_eq(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar wasn't overwritten
+
+ let threw = false;
+ try {
+ TelemetryTimestamps.add("baz", "this isn't a number");
+ } catch (ex) {
+ threw = true;
+ }
+ do_check_true(threw); // adding non-number threw
+ do_check_null(TelemetryTimestamps.get().baz); // no baz was added
+
+ // Test that the data gets added to the telemetry ping properly
+ let simpleMeasurements = getSimpleMeasurementsFromTelemetryController();
+ do_check_true(simpleMeasurements != null); // got simple measurements from ping data
+ do_check_true(simpleMeasurements.foo > 1); // foo was included
+ do_check_true(simpleMeasurements.bar > 1); // bar was included
+ do_check_eq(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added
+
+ yield TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js b/toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js
new file mode 100644
index 0000000000..e8c9f868ab
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function getMainThreadHangStats() {
+ let threads = Services.telemetry.threadHangStats;
+ return threads.find((thread) => (thread.name === "Gecko"));
+}
+
+function run_test() {
+ let startHangs = getMainThreadHangStats();
+
+ // We disable hang reporting in several situations (e.g. debug builds,
+ // official releases). In those cases, we don't have hang stats available
+ // and should exit the test early.
+ if (!startHangs) {
+ ok("Hang reporting not enabled.");
+ return;
+ }
+
+ if (Services.appinfo.OS === 'Linux' || Services.appinfo.OS === 'Android') {
+ // We use the rt_tgsigqueueinfo syscall on Linux which requires a
+ // certain kernel version. It's not an error if the system running
+ // the test is older than that.
+ let kernel = Services.sysinfo.get('kernel_version') ||
+ Services.sysinfo.get('version');
+ if (Services.vc.compare(kernel, '2.6.31') < 0) {
+ ok("Hang reporting not supported for old kernel.");
+ return;
+ }
+ }
+
+ // Run three events in the event loop:
+ // the first event causes a transient hang;
+ // the second event causes a permanent hang;
+ // the third event checks results from previous events.
+
+ do_execute_soon(() => {
+ // Cause a hang lasting 1 second (transient hang).
+ let startTime = Date.now();
+ while ((Date.now() - startTime) < 1000);
+ });
+
+ do_execute_soon(() => {
+ // Cause a hang lasting 10 seconds (permanent hang).
+ let startTime = Date.now();
+ while ((Date.now() - startTime) < 10000);
+ });
+
+ do_execute_soon(() => {
+ do_test_pending();
+
+ let check_results = () => {
+ let endHangs = getMainThreadHangStats();
+
+ // Because hangs are recorded asynchronously, if we don't see new hangs,
+ // we should wait for pending hangs to be recorded. On the other hand,
+ // if hang monitoring is broken, this test will time out.
+ if (endHangs.hangs.length === startHangs.hangs.length) {
+ do_timeout(100, check_results);
+ return;
+ }
+
+ let check_histogram = (histogram) => {
+ equal(typeof histogram, "object");
+ equal(histogram.histogram_type, 0);
+ equal(typeof histogram.min, "number");
+ equal(typeof histogram.max, "number");
+ equal(typeof histogram.sum, "number");
+ ok(Array.isArray(histogram.ranges));
+ ok(Array.isArray(histogram.counts));
+ equal(histogram.counts.length, histogram.ranges.length);
+ };
+
+ // Make sure the hang stats structure is what we expect.
+ equal(typeof endHangs, "object");
+ check_histogram(endHangs.activity);
+
+ ok(Array.isArray(endHangs.hangs));
+ notEqual(endHangs.hangs.length, 0);
+
+ ok(Array.isArray(endHangs.hangs[0].stack));
+ notEqual(endHangs.hangs[0].stack.length, 0);
+ equal(typeof endHangs.hangs[0].stack[0], "string");
+
+ // Make sure one of the hangs is a permanent
+ // hang containing a native stack.
+ ok(endHangs.hangs.some((hang) => (
+ Array.isArray(hang.nativeStack) &&
+ hang.nativeStack.length !== 0 &&
+ typeof hang.nativeStack[0] === "string"
+ )));
+
+ check_histogram(endHangs.hangs[0].histogram);
+
+ do_test_finished();
+ };
+
+ check_results();
+ });
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
new file mode 100644
index 0000000000..8dc5526043
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
@@ -0,0 +1,883 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const INT_MAX = 0x7FFFFFFF;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+
+// Return an array of numbers from lower up to, excluding, upper
+function numberRange(lower, upper)
+{
+ let a = [];
+ for (let i=lower; i<upper; ++i) {
+ a.push(i);
+ }
+ return a;
+}
+
+function expect_fail(f) {
+ let failed = false;
+ try {
+ f();
+ failed = false;
+ } catch (e) {
+ failed = true;
+ }
+ do_check_true(failed);
+}
+
+function expect_success(f) {
+ let succeeded = false;
+ try {
+ f();
+ succeeded = true;
+ } catch (e) {
+ succeeded = false;
+ }
+ do_check_true(succeeded);
+}
+
+function compareHistograms(h1, h2) {
+ let s1 = h1.snapshot();
+ let s2 = h2.snapshot();
+
+ do_check_eq(s1.histogram_type, s2.histogram_type);
+ do_check_eq(s1.min, s2.min);
+ do_check_eq(s1.max, s2.max);
+ do_check_eq(s1.sum, s2.sum);
+
+ do_check_eq(s1.counts.length, s2.counts.length);
+ for (let i = 0; i < s1.counts.length; i++)
+ do_check_eq(s1.counts[i], s2.counts[i]);
+
+ do_check_eq(s1.ranges.length, s2.ranges.length);
+ for (let i = 0; i < s1.ranges.length; i++)
+ do_check_eq(s1.ranges[i], s2.ranges[i]);
+}
+
+function check_histogram(histogram_type, name, min, max, bucket_count) {
+ var h = Telemetry.getHistogramById(name);
+ var r = h.snapshot().ranges;
+ var sum = 0;
+ for (let i=0;i<r.length;i++) {
+ var v = r[i];
+ sum += v;
+ h.add(v);
+ }
+ var s = h.snapshot();
+ // verify properties
+ do_check_eq(sum, s.sum);
+
+ // there should be exactly one element per bucket
+ for (let i of s.counts) {
+ do_check_eq(i, 1);
+ }
+ var hgrams = Telemetry.histogramSnapshots
+ let gh = hgrams[name]
+ do_check_eq(gh.histogram_type, histogram_type);
+
+ do_check_eq(gh.min, min)
+ do_check_eq(gh.max, max)
+
+ // Check that booleans work with nonboolean histograms
+ h.add(false);
+ h.add(true);
+ s = h.snapshot().counts;
+ do_check_eq(s[0], 2)
+ do_check_eq(s[1], 2)
+
+ // Check that clearing works.
+ h.clear();
+ s = h.snapshot();
+ for (var i of s.counts) {
+ do_check_eq(i, 0);
+ }
+ do_check_eq(s.sum, 0);
+
+ h.add(0);
+ h.add(1);
+ var c = h.snapshot().counts;
+ do_check_eq(c[0], 1);
+ do_check_eq(c[1], 1);
+}
+
+// This MUST be the very first test of this file.
+add_task({
+ skip_if: () => gIsAndroid
+},
+function* test_instantiate() {
+ const ID = "TELEMETRY_TEST_COUNT";
+ let h = Telemetry.getHistogramById(ID);
+
+ // Instantiate the subsession histogram through |add| and make sure they match.
+ // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise
+ // |add| will not instantiate the histogram.
+ h.add(1);
+ let snapshot = h.snapshot();
+ let subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.equal(snapshot.sum, subsession[ID].sum,
+ "Histogram and subsession histogram sum must match.");
+ // Clear the histogram, so we don't void the assumptions from the other tests.
+ h.clear();
+});
+
+add_task(function* test_parameterChecks() {
+ let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR]
+ let testNames = ["TELEMETRY_TEST_EXPONENTIAL", "TELEMETRY_TEST_LINEAR"]
+ for (let i = 0; i < kinds.length; i++) {
+ let histogram_type = kinds[i];
+ let test_type = testNames[i];
+ let [min, max, bucket_count] = [1, INT_MAX - 1, 10]
+ check_histogram(histogram_type, test_type, min, max, bucket_count);
+ }
+});
+
+add_task(function* test_noSerialization() {
+ // Instantiate the storage for this histogram and make sure it doesn't
+ // get reflected into JS, as it has no interesting data in it.
+ Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT");
+ do_check_false("NEWTAB_PAGE_PINNED_SITES_COUNT" in Telemetry.histogramSnapshots);
+});
+
+add_task(function* test_boolean_histogram() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ var r = h.snapshot().ranges;
+ // boolean histograms ignore numeric parameters
+ do_check_eq(uneval(r), uneval([0, 1, 2]))
+ for (var i=0;i<r.length;i++) {
+ var v = r[i];
+ h.add(v);
+ }
+ h.add(true);
+ h.add(false);
+ var s = h.snapshot();
+ do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_BOOLEAN);
+ // last bucket should always be 0 since .add parameters are normalized to either 0 or 1
+ do_check_eq(s.counts[2], 0);
+ do_check_eq(s.sum, 3);
+ do_check_eq(s.counts[0], 2);
+});
+
+add_task(function* test_flag_histogram() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ var r = h.snapshot().ranges;
+ // Flag histograms ignore numeric parameters.
+ do_check_eq(uneval(r), uneval([0, 1, 2]));
+ // Should already have a 0 counted.
+ var c = h.snapshot().counts;
+ var s = h.snapshot().sum;
+ do_check_eq(uneval(c), uneval([1, 0, 0]));
+ do_check_eq(s, 0);
+ // Should switch counts.
+ h.add(1);
+ var c2 = h.snapshot().counts;
+ var s2 = h.snapshot().sum;
+ do_check_eq(uneval(c2), uneval([0, 1, 0]));
+ do_check_eq(s2, 1);
+ // Should only switch counts once.
+ h.add(1);
+ var c3 = h.snapshot().counts;
+ var s3 = h.snapshot().sum;
+ do_check_eq(uneval(c3), uneval([0, 1, 0]));
+ do_check_eq(s3, 1);
+ do_check_eq(h.snapshot().histogram_type, Telemetry.HISTOGRAM_FLAG);
+});
+
+add_task(function* test_count_histogram() {
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT2");
+ let s = h.snapshot();
+ do_check_eq(uneval(s.ranges), uneval([0, 1, 2]));
+ do_check_eq(uneval(s.counts), uneval([0, 0, 0]));
+ do_check_eq(s.sum, 0);
+ h.add();
+ s = h.snapshot();
+ do_check_eq(uneval(s.counts), uneval([1, 0, 0]));
+ do_check_eq(s.sum, 1);
+ h.add();
+ s = h.snapshot();
+ do_check_eq(uneval(s.counts), uneval([2, 0, 0]));
+ do_check_eq(s.sum, 2);
+});
+
+add_task(function* test_categorical_histogram()
+{
+ let h1 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) {
+ h1.add(v);
+ }
+ for (let s of ["", "Label4", "1234"]) {
+ Assert.throws(() => h1.add(s));
+ }
+
+ let snapshot = h1.snapshot();
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3]);
+ Assert.deepEqual(snapshot.counts, [3, 2, 2, 0]);
+
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_OPTOUT");
+ for (let v of ["CommonLabel", "CommonLabel", "Label4", "Label5", "Label6", 0, 1]) {
+ h2.add(v);
+ }
+ for (let s of ["", "Label3", "1234"]) {
+ Assert.throws(() => h2.add(s));
+ }
+
+ snapshot = h2.snapshot();
+ Assert.equal(snapshot.sum, 7);
+ Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3, 4]);
+ Assert.deepEqual(snapshot.counts, [3, 2, 1, 1, 0]);
+});
+
+add_task(function* test_getHistogramById() {
+ try {
+ Telemetry.getHistogramById("nonexistent");
+ do_throw("This can't happen");
+ } catch (e) {
+
+ }
+ var h = Telemetry.getHistogramById("CYCLE_COLLECTOR");
+ var s = h.snapshot();
+ do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_EXPONENTIAL);
+ do_check_eq(s.min, 1);
+ do_check_eq(s.max, 10000);
+});
+
+add_task(function* test_getSlowSQL() {
+ var slow = Telemetry.slowSQL;
+ do_check_true(("mainThread" in slow) && ("otherThreads" in slow));
+});
+
+add_task(function* test_getWebrtc() {
+ var webrtc = Telemetry.webrtcStats;
+ do_check_true("IceCandidatesStats" in webrtc);
+ var icestats = webrtc.IceCandidatesStats;
+ do_check_true("webrtc" in icestats);
+});
+
+// Check that telemetry doesn't record in private mode
+add_task(function* test_privateMode() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ var orig = h.snapshot();
+ Telemetry.canRecordExtended = false;
+ h.add(1);
+ do_check_eq(uneval(orig), uneval(h.snapshot()));
+ Telemetry.canRecordExtended = true;
+ h.add(1);
+ do_check_neq(uneval(orig), uneval(h.snapshot()));
+});
+
+// Check that telemetry records only when it is suppose to.
+add_task(function* test_histogramRecording() {
+ // Check that no histogram is recorded if both base and extended recording are off.
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ let orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum, h.snapshot().sum);
+
+ // Check that only base histograms are recorded.
+ Telemetry.canRecordBase = true;
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording.");
+
+ // Extended histograms should not be recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum, h.snapshot().sum,
+ "Histograms should be equal after recording.");
+
+ // Runtime created histograms should not be recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum, h.snapshot().sum,
+ "Histograms should be equal after recording.");
+
+ // Check that extended histograms are recorded when required.
+ Telemetry.canRecordExtended = true;
+
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "Runtime histogram value should have incremented by 1 due to recording.");
+
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording.");
+
+ // Check that base histograms are still being recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording.");
+});
+
+add_task(function* test_addons() {
+ var addon_id = "testing-addon";
+ var fake_addon_id = "fake-addon";
+ var name1 = "testing-histogram1";
+ var register = Telemetry.registerAddonHistogram;
+ expect_success(() =>
+ register(addon_id, name1, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6));
+ // Can't register the same histogram multiple times.
+ expect_fail(() =>
+ register(addon_id, name1, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6));
+ // Make sure we can't get at it with another name.
+ expect_fail(() => Telemetry.getAddonHistogram(fake_addon_id, name1));
+
+ // Check for reflection capabilities.
+ var h1 = Telemetry.getAddonHistogram(addon_id, name1);
+ // Verify that although we've created storage for it, we don't reflect it into JS.
+ var snapshots = Telemetry.addonHistogramSnapshots;
+ do_check_false(name1 in snapshots[addon_id]);
+ h1.add(1);
+ h1.add(3);
+ var s1 = h1.snapshot();
+ do_check_eq(s1.histogram_type, Telemetry.HISTOGRAM_LINEAR);
+ do_check_eq(s1.min, 1);
+ do_check_eq(s1.max, 5);
+ do_check_eq(s1.counts[1], 1);
+ do_check_eq(s1.counts[3], 1);
+
+ var name2 = "testing-histogram2";
+ expect_success(() =>
+ register(addon_id, name2, Telemetry.HISTOGRAM_LINEAR, 2, 4, 4));
+
+ var h2 = Telemetry.getAddonHistogram(addon_id, name2);
+ h2.add(2);
+ h2.add(3);
+ var s2 = h2.snapshot();
+ do_check_eq(s2.histogram_type, Telemetry.HISTOGRAM_LINEAR);
+ do_check_eq(s2.min, 2);
+ do_check_eq(s2.max, 4);
+ do_check_eq(s2.counts[1], 1);
+ do_check_eq(s2.counts[2], 1);
+
+ // Check that we can register histograms for a different addon with
+ // identical names.
+ var extra_addon = "testing-extra-addon";
+ expect_success(() =>
+ register(extra_addon, name1, Telemetry.HISTOGRAM_BOOLEAN));
+
+ // Check that we can register flag histograms.
+ var flag_addon = "testing-flag-addon";
+ var flag_histogram = "flag-histogram";
+ expect_success(() =>
+ register(flag_addon, flag_histogram, Telemetry.HISTOGRAM_FLAG));
+ expect_success(() =>
+ register(flag_addon, name2, Telemetry.HISTOGRAM_LINEAR, 2, 4, 4));
+
+ // Check that we reflect registered addons and histograms.
+ snapshots = Telemetry.addonHistogramSnapshots;
+ do_check_true(addon_id in snapshots)
+ do_check_true(extra_addon in snapshots);
+ do_check_true(flag_addon in snapshots);
+
+ // Check that we have data for our created histograms.
+ do_check_true(name1 in snapshots[addon_id]);
+ do_check_true(name2 in snapshots[addon_id]);
+ var s1_alt = snapshots[addon_id][name1];
+ var s2_alt = snapshots[addon_id][name2];
+ do_check_eq(s1_alt.min, s1.min);
+ do_check_eq(s1_alt.max, s1.max);
+ do_check_eq(s1_alt.histogram_type, s1.histogram_type);
+ do_check_eq(s2_alt.min, s2.min);
+ do_check_eq(s2_alt.max, s2.max);
+ do_check_eq(s2_alt.histogram_type, s2.histogram_type);
+
+ // Even though we've registered it, it shouldn't show up until data is added to it.
+ do_check_false(name1 in snapshots[extra_addon]);
+
+ // Flag histograms should show up automagically.
+ do_check_true(flag_histogram in snapshots[flag_addon]);
+ do_check_false(name2 in snapshots[flag_addon]);
+
+ // Check that we can remove addon histograms.
+ Telemetry.unregisterAddonHistograms(addon_id);
+ snapshots = Telemetry.addonHistogramSnapshots;
+ do_check_false(addon_id in snapshots);
+ // Make sure other addons are unaffected.
+ do_check_true(extra_addon in snapshots);
+});
+
+add_task(function* test_expired_histogram() {
+ var test_expired_id = "TELEMETRY_TEST_EXPIRED";
+ var dummy = Telemetry.getHistogramById(test_expired_id);
+ var rh = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []);
+ Assert.ok(!!rh);
+
+ dummy.add(1);
+
+ do_check_eq(Telemetry.histogramSnapshots["__expired__"], undefined);
+ do_check_eq(Telemetry.histogramSnapshots[test_expired_id], undefined);
+ do_check_eq(rh[test_expired_id], undefined);
+});
+
+add_task(function* test_keyed_histogram() {
+ // Check that invalid names get rejected.
+
+ let threw = false;
+ try {
+ Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
+ } catch (e) {
+ // This should throw as it is an unknown ID
+ threw = true;
+ }
+ Assert.ok(threw, "getKeyedHistogramById should have thrown");
+});
+
+add_task(function* test_keyed_boolean_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_BOOLEAN";
+ let KEYS = numberRange(0, 2).map(i => "key" + (i + 1));
+ KEYS.push("漢語");
+ let histogramBase = {
+ "min": 1,
+ "max": 2,
+ "histogram_type": 2,
+ "sum": 1,
+ "ranges": [0, 1, 2],
+ "counts": [0, 1, 0]
+ };
+ let testHistograms = numberRange(0, 3).map(i => JSON.parse(JSON.stringify(histogramBase)));
+ let testKeys = [];
+ let testSnapShot = {};
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ for (let i=0; i<2; ++i) {
+ let key = KEYS[i];
+ h.add(key, true);
+ testSnapShot[key] = testHistograms[i];
+ testKeys.push(key);
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+ }
+
+ h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let key = KEYS[2];
+ h.add(key, false);
+ testKeys.push(key);
+ testSnapShot[key] = testHistograms[2];
+ testSnapShot[key].sum = 0;
+ testSnapShot[key].counts = [1, 0, 0];
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let allSnapshots = Telemetry.keyedHistogramSnapshots;
+ Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(function* test_keyed_count_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
+ let histogramBase = {
+ "min": 1,
+ "max": 2,
+ "histogram_type": 4,
+ "sum": 0,
+ "ranges": [0, 1, 2],
+ "counts": [1, 0, 0]
+ };
+ let testHistograms = numberRange(0, 5).map(i => JSON.parse(JSON.stringify(histogramBase)));
+ let testKeys = [];
+ let testSnapShot = {};
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ for (let i=0; i<4; ++i) {
+ let key = KEYS[i];
+ let value = i*2 + 1;
+
+ for (let k=0; k<value; ++k) {
+ h.add(key);
+ }
+ testHistograms[i].counts[0] = value;
+ testHistograms[i].sum = value;
+ testSnapShot[key] = testHistograms[i];
+ testKeys.push(key);
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(key), testHistograms[i]);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+ }
+
+ h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let key = KEYS[4];
+ h.add(key);
+ testKeys.push(key);
+ testHistograms[4].counts[0] = 1;
+ testHistograms[4].sum = 1;
+ testSnapShot[key] = testHistograms[4];
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let allSnapshots = Telemetry.keyedHistogramSnapshots;
+ Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(function* test_keyed_flag_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_FLAG";
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ const KEY = "default";
+ h.add(KEY, true);
+
+ let testSnapshot = {};
+ testSnapshot[KEY] = {
+ "min": 1,
+ "max": 2,
+ "histogram_type": 3,
+ "sum": 1,
+ "ranges": [0, 1, 2],
+ "counts": [0, 1, 0]
+ };
+
+ Assert.deepEqual(h.keys().sort(), [KEY]);
+ Assert.deepEqual(h.snapshot(), testSnapshot);
+
+ let allSnapshots = Telemetry.keyedHistogramSnapshots;
+ Assert.deepEqual(allSnapshots[KEYED_ID], testSnapshot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(function* test_keyed_histogram_recording() {
+ // Check that no histogram is recorded if both base and extended recording are off.
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+
+ const TEST_KEY = "record_foo";
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 0);
+
+ // Check that only base histograms are recorded.
+ Telemetry.canRecordBase = true;
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "The keyed histogram should record the correct value.");
+
+ // Extended set keyed histograms should not be recorded.
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 0,
+ "The keyed histograms should not record any data.");
+
+ // Check that extended histograms are recorded when required.
+ Telemetry.canRecordExtended = true;
+
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "The runtime keyed histogram should record the correct value.");
+
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "The keyed histogram should record the correct value.");
+
+ // Check that base histograms are still being recorded.
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1);
+});
+
+add_task(function* test_histogram_recording_enabled() {
+ Telemetry.canRecordBase = true;
+ Telemetry.canRecordExtended = true;
+
+ // Check that a "normal" histogram respects recording-enabled on/off
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ var orig = h.snapshot();
+
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "add should record by default.");
+
+ // Check that when recording is disabled - add is ignored
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false);
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "When recording is disabled add should not record.");
+
+ // Check that we're back to normal after recording is enabled
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true);
+ h.add(1);
+ Assert.equal(orig.sum + 2, h.snapshot().sum,
+ "When recording is re-enabled add should record.");
+
+ // Check that we're correctly accumulating values other than 1.
+ h.clear();
+ h.add(3);
+ Assert.equal(3, h.snapshot().sum, "Recording counts greater than 1 should work.");
+
+ // Check that a histogram with recording disabled by default behaves correctly
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT_INIT_NO_RECORD");
+ orig = h.snapshot();
+
+ h.add(1);
+ Assert.equal(orig.sum, h.snapshot().sum,
+ "When recording is disabled by default, add should not record by default.");
+
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT_INIT_NO_RECORD", true);
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "When recording is enabled add should record.");
+
+ // Restore to disabled
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT_INIT_NO_RECORD", false);
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum,
+ "When recording is disabled add should not record.");
+});
+
+add_task(function* test_keyed_histogram_recording_enabled() {
+ Telemetry.canRecordBase = true;
+ Telemetry.canRecordExtended = true;
+
+ // Check RecordingEnabled for keyed histograms which are recording by default
+ const TEST_KEY = "record_foo";
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "Keyed histogram add should record by default");
+
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT", false);
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "Keyed histogram add should not record when recording is disabled");
+
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT", true);
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "Keyed histogram add should record when recording is re-enabled");
+
+ // Check that a histogram with recording disabled by default behaves correctly
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD");
+ h.clear();
+
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 0,
+ "Keyed histogram add should not record by default for histograms which don't record by default");
+
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD", true);
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "Keyed histogram add should record when recording is enabled");
+
+ // Restore to disabled
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD", false);
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot(TEST_KEY).sum, 1,
+ "Keyed histogram add should not record when recording is disabled");
+});
+
+add_task(function* test_datasets() {
+ // Check that datasets work as expected.
+
+ const RELEASE_CHANNEL_OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
+ const RELEASE_CHANNEL_OPTIN = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN;
+
+ // Histograms should default to the extended dataset
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN);
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
+ Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN);
+
+ // Check test histograms with explicit dataset definitions
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN);
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTOUT);
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN);
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+ Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTOUT);
+
+ // Check that registeredHistogram works properly
+ let registered = Telemetry.registeredHistograms(RELEASE_CHANNEL_OPTIN, []);
+ registered = new Set(registered);
+ Assert.ok(registered.has("TELEMETRY_TEST_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTIN"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT"));
+ registered = Telemetry.registeredHistograms(RELEASE_CHANNEL_OPTOUT, []);
+ registered = new Set(registered);
+ Assert.ok(!registered.has("TELEMETRY_TEST_FLAG"));
+ Assert.ok(!registered.has("TELEMETRY_TEST_RELEASE_OPTIN"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT"));
+
+ // Check that registeredKeyedHistograms works properly
+ registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTIN, []);
+ registered = new Set(registered);
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
+ registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTOUT, []);
+ registered = new Set(registered);
+ Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
+});
+
+add_task({
+ skip_if: () => gIsAndroid
+},
+function* test_subsession() {
+ const ID = "TELEMETRY_TEST_COUNT";
+ const FLAG = "TELEMETRY_TEST_FLAG";
+ let h = Telemetry.getHistogramById(ID);
+ let flag = Telemetry.getHistogramById(FLAG);
+
+ // Both original and duplicate should start out the same.
+ h.clear();
+ let snapshot = Telemetry.histogramSnapshots;
+ let subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.ok(!(ID in snapshot));
+ Assert.ok(!(ID in subsession));
+
+ // They should instantiate and pick-up the count.
+ h.add(1);
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.ok(ID in snapshot);
+ Assert.ok(ID in subsession);
+ Assert.equal(snapshot[ID].sum, 1);
+ Assert.equal(subsession[ID].sum, 1);
+
+ // They should still reset properly.
+ h.clear();
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.ok(!(ID in snapshot));
+ Assert.ok(!(ID in subsession));
+
+ // Both should instantiate and pick-up the count.
+ h.add(1);
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.equal(snapshot[ID].sum, 1);
+ Assert.equal(subsession[ID].sum, 1);
+
+ // Check that we are able to only reset the duplicate histogram.
+ h.clear(true);
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.ok(ID in snapshot);
+ Assert.ok(ID in subsession);
+ Assert.equal(snapshot[ID].sum, 1);
+ Assert.equal(subsession[ID].sum, 0);
+
+ // Both should register the next count.
+ h.add(1);
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.equal(snapshot[ID].sum, 2);
+ Assert.equal(subsession[ID].sum, 1);
+
+ // Retrieve a subsession snapshot and pass the flag to
+ // clear subsession histograms too.
+ h.clear();
+ flag.clear();
+ h.add(1);
+ flag.add(1);
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms(true);
+ Assert.ok(ID in snapshot);
+ Assert.ok(ID in subsession);
+ Assert.ok(FLAG in snapshot);
+ Assert.ok(FLAG in subsession);
+ Assert.equal(snapshot[ID].sum, 1);
+ Assert.equal(subsession[ID].sum, 1);
+ Assert.equal(snapshot[FLAG].sum, 1);
+ Assert.equal(subsession[FLAG].sum, 1);
+
+ // The next subsesssion snapshot should show the histograms
+ // got reset.
+ snapshot = Telemetry.histogramSnapshots;
+ subsession = Telemetry.snapshotSubsessionHistograms();
+ Assert.ok(ID in snapshot);
+ Assert.ok(ID in subsession);
+ Assert.ok(FLAG in snapshot);
+ Assert.ok(FLAG in subsession);
+ Assert.equal(snapshot[ID].sum, 1);
+ Assert.equal(subsession[ID].sum, 0);
+ Assert.equal(snapshot[FLAG].sum, 1);
+ Assert.equal(subsession[FLAG].sum, 0);
+});
+
+add_task({
+ skip_if: () => gIsAndroid
+},
+function* test_keyed_subsession() {
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
+ const KEY = "foo";
+
+ // Both original and subsession should start out the same.
+ h.clear();
+ Assert.ok(!(KEY in h.snapshot()));
+ Assert.ok(!(KEY in h.subsessionSnapshot()));
+ Assert.equal(h.snapshot(KEY).sum, 0);
+ Assert.equal(h.subsessionSnapshot(KEY).sum, 0);
+
+ // Both should register the flag.
+ h.add(KEY, 1);
+ Assert.ok(KEY in h.snapshot());
+ Assert.ok(KEY in h.subsessionSnapshot());
+ Assert.equal(h.snapshot(KEY).sum, 1);
+ Assert.equal(h.subsessionSnapshot(KEY).sum, 1);
+
+ // Check that we are able to only reset the subsession histogram.
+ h.clear(true);
+ Assert.ok(KEY in h.snapshot());
+ Assert.ok(!(KEY in h.subsessionSnapshot()));
+ Assert.equal(h.snapshot(KEY).sum, 1);
+ Assert.equal(h.subsessionSnapshot(KEY).sum, 0);
+
+ // Setting the flag again should make both match again.
+ h.add(KEY, 1);
+ Assert.ok(KEY in h.snapshot());
+ Assert.ok(KEY in h.subsessionSnapshot());
+ Assert.equal(h.snapshot(KEY).sum, 1);
+ Assert.equal(h.subsessionSnapshot(KEY).sum, 1);
+
+ // Check that "snapshot and clear" works properly.
+ let snapshot = h.snapshot();
+ let subsession = h.snapshotSubsessionAndClear();
+ Assert.ok(KEY in snapshot);
+ Assert.ok(KEY in subsession);
+ Assert.equal(snapshot[KEY].sum, 1);
+ Assert.equal(subsession[KEY].sum, 1);
+
+ subsession = h.subsessionSnapshot();
+ Assert.ok(!(KEY in subsession));
+ Assert.equal(h.subsessionSnapshot(KEY).sum, 0);
+});
diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.ini b/toolkit/components/telemetry/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..74067580a6
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -0,0 +1,63 @@
+[DEFAULT]
+head = head.js
+tail =
+firefox-appdir = browser
+# The *.xpi files are only needed for test_TelemetryEnvironment.js, but
+# xpcshell fails to install tests if we move them under the test entry.
+support-files =
+ ../search/chrome.manifest
+ ../search/searchTest.jar
+ dictionary.xpi
+ experiment.xpi
+ extension.xpi
+ extension-2.xpi
+ engine.xml
+ system.xpi
+ restartless.xpi
+ theme.xpi
+ !/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+generated-files =
+ dictionary.xpi
+ experiment.xpi
+ extension.xpi
+ extension-2.xpi
+ system.xpi
+ restartless.xpi
+ theme.xpi
+
+[test_nsITelemetry.js]
+[test_SubsessionChaining.js]
+tags = addons
+[test_TelemetryEnvironment.js]
+skip-if = os == "android"
+tags = addons
+[test_PingAPI.js]
+skip-if = os == "android"
+[test_TelemetryFlagClear.js]
+[test_TelemetryLateWrites.js]
+[test_TelemetryLockCount.js]
+[test_TelemetryLog.js]
+[test_TelemetryController.js]
+tags = addons
+[test_TelemetryController_idle.js]
+[test_TelemetryControllerShutdown.js]
+tags = addons
+[test_TelemetryStopwatch.js]
+[test_TelemetryControllerBuildID.js]
+[test_TelemetrySendOldPings.js]
+skip-if = os == "android" # Disabled due to intermittent orange on Android
+tags = addons
+[test_TelemetrySession.js]
+tags = addons
+[test_ThreadHangStats.js]
+run-sequentially = Bug 1046307, test can fail intermittently when CPU load is high
+[test_TelemetrySend.js]
+[test_ChildHistograms.js]
+skip-if = os == "android"
+tags = addons
+[test_TelemetryReportingPolicy.js]
+tags = addons
+[test_TelemetryScalars.js]
+[test_TelemetryTimestamps.js]
+skip-if = toolkit == 'android'
+[test_TelemetryEvents.js]
diff --git a/toolkit/components/terminator/moz.build b/toolkit/components/terminator/moz.build
new file mode 100644
index 0000000000..7e230ed4df
--- /dev/null
+++ b/toolkit/components/terminator/moz.build
@@ -0,0 +1,22 @@
+# -*- 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/xpcshell/xpcshell.ini']
+
+SOURCES += [
+ 'nsTerminator.cpp',
+]
+
+EXPORTS += [
+ 'nsTerminator.h',
+]
+
+EXTRA_COMPONENTS += [
+ 'nsTerminatorTelemetry.js',
+ 'terminator.manifest',
+]
+
+FINAL_LIBRARY = 'xul'
diff --git a/toolkit/components/terminator/nsTerminator.cpp b/toolkit/components/terminator/nsTerminator.cpp
new file mode 100644
index 0000000000..f9459cc5d6
--- /dev/null
+++ b/toolkit/components/terminator/nsTerminator.cpp
@@ -0,0 +1,554 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A watchdog designed to terminate shutdown if it lasts too long.
+ *
+ * This watchdog is designed as a worst-case problem container for the
+ * common case in which Firefox just won't shutdown.
+ *
+ * We spawn a thread during quit-application. If any of the shutdown
+ * steps takes more than n milliseconds (63000 by default), kill the
+ * process as fast as possible, without any cleanup.
+ */
+
+#include "nsTerminator.h"
+
+#include "prthread.h"
+#include "prmon.h"
+#include "plstr.h"
+#include "prio.h"
+
+#include "nsString.h"
+#include "nsServiceManagerUtils.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsAppDirectoryServiceDefs.h"
+
+#include "nsIObserverService.h"
+#include "nsIPrefService.h"
+#if defined(MOZ_CRASHREPORTER)
+#include "nsExceptionHandler.h"
+#endif
+
+#if defined(XP_WIN)
+#include <windows.h>
+#else
+#include <unistd.h>
+#endif
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/MemoryChecking.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Telemetry.h"
+
+// Normally, the number of milliseconds that AsyncShutdown waits until
+// it decides to crash is specified as a preference. We use the
+// following value as a fallback if for some reason the preference is
+// absent.
+#define FALLBACK_ASYNCSHUTDOWN_CRASH_AFTER_MS 60000
+
+// Additional number of milliseconds to wait until we decide to exit
+// forcefully.
+#define ADDITIONAL_WAIT_BEFORE_CRASH_MS 3000
+
+namespace mozilla {
+
+namespace {
+
+// Utility function: create a thread that is non-joinable,
+// does not prevent the process from terminating, is never
+// cooperatively scheduled, and uses a default stack size.
+PRThread* CreateSystemThread(void (*start)(void* arg),
+ void* arg)
+{
+ PRThread* thread = PR_CreateThread(
+ PR_SYSTEM_THREAD, /* This thread will not prevent the process from terminating */
+ start,
+ arg,
+ PR_PRIORITY_LOW,
+ PR_GLOBAL_THREAD /* Make sure that the thread is never cooperatively scheduled */,
+ PR_UNJOINABLE_THREAD,
+ 0 /* Use default stack size */
+ );
+ MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(thread); // This pointer will never be deallocated.
+ return thread;
+}
+
+
+////////////////////////////////////////////
+//
+// The watchdog
+//
+// This nspr thread is in charge of crashing the process if any stage of shutdown
+// lasts more than some predefined duration. As a side-effect, it measures the
+// duration of each stage of shutdown.
+//
+
+// The heartbeat of the operation.
+//
+// Main thread:
+//
+// * Whenever a shutdown step has been completed, the main thread
+// swaps gHeartbeat to 0 to mark that the shutdown process is still
+// progressing. The value swapped away indicates the number of ticks
+// it took for the shutdown step to advance.
+//
+// Watchdog thread:
+//
+// * Every tick, the watchdog thread increments gHearbeat atomically.
+//
+// A note about precision:
+// Since gHeartbeat is generally reset to 0 between two ticks, this means
+// that gHeartbeat stays at 0 less than one tick. Consequently, values
+// extracted from gHeartbeat must be considered rounded up.
+Atomic<uint32_t> gHeartbeat(0);
+
+struct Options {
+ /**
+ * How many ticks before we should crash the process.
+ */
+ uint32_t crashAfterTicks;
+};
+
+/**
+ * Entry point for the watchdog thread
+ */
+void
+RunWatchdog(void* arg)
+{
+ PR_SetCurrentThreadName("Shutdown Hang Terminator");
+
+ // Let's copy and deallocate options, that's one less leak to worry
+ // about.
+ UniquePtr<Options> options((Options*)arg);
+ uint32_t crashAfterTicks = options->crashAfterTicks;
+ options = nullptr;
+
+ const uint32_t timeToLive = crashAfterTicks;
+ while (true) {
+ //
+ // We do not want to sleep for the entire duration,
+ // as putting the computer to sleep would suddenly
+ // cause us to timeout on wakeup.
+ //
+ // Rather, we prefer sleeping for at most 1 second
+ // at a time. If the computer sleeps then wakes up,
+ // we have lost at most one second, which is much
+ // more reasonable.
+ //
+#if defined(XP_WIN)
+ Sleep(1000 /* ms */);
+#else
+ usleep(1000000 /* usec */);
+#endif
+
+ if (gHeartbeat++ < timeToLive) {
+ continue;
+ }
+
+ // Shutdown is apparently dead. Crash the process.
+ MOZ_CRASH("Shutdown too long, probably frozen, causing a crash.");
+ }
+}
+
+////////////////////////////////////////////
+//
+// Writer thread
+//
+// This nspr thread is in charge of writing to disk statistics produced by the
+// watchdog thread and collected by the main thread. Note that we use a nspr
+// thread rather than usual XPCOM I/O simply because we outlive XPCOM and its
+// threads.
+//
+
+// Utility class, used by UniquePtr<> to close nspr files.
+class PR_CloseDelete
+{
+public:
+ constexpr PR_CloseDelete() {}
+
+ PR_CloseDelete(const PR_CloseDelete& aOther)
+ {}
+
+ void operator()(PRFileDesc* aPtr) const
+ {
+ PR_Close(aPtr);
+ }
+};
+
+//
+// Communication between the main thread and the writer thread.
+//
+// Main thread:
+//
+// * Whenever a shutdown step has been completed, the main thread
+// obtains the number of ticks from the watchdog threads, builds
+// a string representing all the data gathered so far, places
+// this string in `gWriteData`, and wakes up the writer thread
+// using `gWriteReady`. If `gWriteData` already contained a non-null
+// pointer, this means that the writer thread is lagging behind the
+// main thread, and the main thread cleans up the memory.
+//
+// Writer thread:
+//
+// * When awake, the writer thread swaps `gWriteData` to nullptr. If
+// `gWriteData` contained data to write, the . If so, the writer
+// thread writes the data to a file named "ShutdownDuration.json.tmp",
+// then moves that file to "ShutdownDuration.json" and cleans up the
+// data. If `gWriteData` contains a nullptr, the writer goes to sleep
+// until it is awkened using `gWriteReady`.
+//
+//
+// The data written by the writer thread will be read by another
+// module upon the next restart and fed to Telemetry.
+//
+Atomic<nsCString*> gWriteData(nullptr);
+PRMonitor* gWriteReady = nullptr;
+
+void RunWriter(void* arg)
+{
+ PR_SetCurrentThreadName("Shutdown Statistics Writer");
+
+ MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(arg);
+ // Shutdown will generally complete before we have a chance to
+ // deallocate. This is not a leak.
+
+ // Setup destinationPath and tmpFilePath
+
+ nsCString destinationPath(static_cast<char*>(arg));
+ nsAutoCString tmpFilePath;
+ tmpFilePath.Append(destinationPath);
+ tmpFilePath.AppendLiteral(".tmp");
+
+ // Cleanup any file leftover from a previous run
+ Unused << PR_Delete(tmpFilePath.get());
+ Unused << PR_Delete(destinationPath.get());
+
+ while (true) {
+ //
+ // Check whether we have received data from the main thread.
+ //
+ // We perform the check before waiting on `gWriteReady` as we may
+ // have received data while we were busy writing.
+ //
+ // Also note that gWriteData may have been modified several times
+ // since we last checked. That's ok, we are not losing any important
+ // data (since we keep adding data), and we are not leaking memory
+ // (since the main thread deallocates any data that hasn't been
+ // consumed by the writer thread).
+ //
+ UniquePtr<nsCString> data(gWriteData.exchange(nullptr));
+ if (!data) {
+ // Data is not available yet.
+ // Wait until the main thread provides it.
+ PR_EnterMonitor(gWriteReady);
+ PR_Wait(gWriteReady, PR_INTERVAL_NO_TIMEOUT);
+ PR_ExitMonitor(gWriteReady);
+ continue;
+ }
+
+ MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(data.get());
+ // Shutdown may complete before we have a chance to deallocate.
+ // This is not a leak.
+
+ //
+ // Write to a temporary file
+ //
+ // In case of any error, we simply give up. Since the data is
+ // hardly critical, we don't want to spend too much effort
+ // salvaging it.
+ //
+ UniquePtr<PRFileDesc, PR_CloseDelete>
+ tmpFileDesc(PR_Open(tmpFilePath.get(),
+ PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE,
+ 00600));
+
+ // Shutdown may complete before we have a chance to close the file.
+ // This is not a leak.
+ MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(tmpFileDesc.get());
+
+ if (tmpFileDesc == nullptr) {
+ break;
+ }
+ if (PR_Write(tmpFileDesc.get(), data->get(), data->Length()) == -1) {
+ break;
+ }
+ tmpFileDesc.reset();
+
+ //
+ // Rename on top of destination file.
+ //
+ // This is not sufficient to guarantee that the destination file
+ // will be written correctly, but, again, we don't care enough
+ // about the data to make more efforts.
+ //
+ if (PR_Rename(tmpFilePath.get(), destinationPath.get()) != PR_SUCCESS) {
+ break;
+ }
+ }
+}
+
+/**
+ * A step during shutdown.
+ *
+ * Shutdown is divided in steps, which all map to an observer
+ * notification. The duration of a step is defined as the number of
+ * ticks between the time we receive a notification and the next one.
+ */
+struct ShutdownStep
+{
+ char const* const mTopic;
+ int mTicks;
+
+ constexpr explicit ShutdownStep(const char *const topic)
+ : mTopic(topic)
+ , mTicks(-1)
+ {}
+
+};
+
+static ShutdownStep sShutdownSteps[] = {
+ ShutdownStep("quit-application"),
+ ShutdownStep("profile-change-teardown"),
+ ShutdownStep("profile-before-change"),
+ ShutdownStep("xpcom-will-shutdown"),
+ ShutdownStep("xpcom-shutdown"),
+};
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(nsTerminator, nsIObserver)
+
+nsTerminator::nsTerminator()
+ : mInitialized(false)
+ , mCurrentStep(-1)
+{
+}
+
+// During startup, register as an observer for all interesting topics.
+nsresult
+nsTerminator::SelfInit()
+{
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (!os) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (size_t i = 0; i < ArrayLength(sShutdownSteps); ++i) {
+ DebugOnly<nsresult> rv = os->AddObserver(this, sShutdownSteps[i].mTopic, false);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AddObserver failed");
+ }
+
+ return NS_OK;
+}
+
+// Actually launch these threads. This takes place at the first sign of shutdown.
+void
+nsTerminator::Start()
+{
+ MOZ_ASSERT(!mInitialized);
+ StartWatchdog();
+#if !defined(DEBUG)
+ // Only allow nsTerminator to write on non-debug builds so we don't get leak warnings on
+ // shutdown for intentional leaks (see bug 1242084). This will be enabled again by bug
+ // 1255484 when 1255478 lands.
+ StartWriter();
+#endif // !defined(DEBUG)
+ mInitialized = true;
+}
+
+// Prepare, allocate and start the watchdog thread.
+// By design, it will never finish, nor be deallocated.
+void
+nsTerminator::StartWatchdog()
+{
+ int32_t crashAfterMS =
+ Preferences::GetInt("toolkit.asyncshutdown.crash_timeout",
+ FALLBACK_ASYNCSHUTDOWN_CRASH_AFTER_MS);
+ // Ignore negative values
+ if (crashAfterMS <= 0) {
+ crashAfterMS = FALLBACK_ASYNCSHUTDOWN_CRASH_AFTER_MS;
+ }
+
+ // Add a little padding, to ensure that we do not crash before
+ // AsyncShutdown.
+ if (crashAfterMS > INT32_MAX - ADDITIONAL_WAIT_BEFORE_CRASH_MS) {
+ // Defend against overflow
+ crashAfterMS = INT32_MAX;
+ } else {
+ crashAfterMS += ADDITIONAL_WAIT_BEFORE_CRASH_MS;
+ }
+
+ UniquePtr<Options> options(new Options());
+ const PRIntervalTime ticksDuration = PR_MillisecondsToInterval(1000);
+ options->crashAfterTicks = crashAfterMS / ticksDuration;
+
+ DebugOnly<PRThread*> watchdogThread = CreateSystemThread(RunWatchdog,
+ options.release());
+ MOZ_ASSERT(watchdogThread);
+}
+
+// Prepare, allocate and start the writer thread. By design, it will never
+// finish, nor be deallocated. In case of error, we degrade
+// gracefully to not writing Telemetry data.
+void
+nsTerminator::StartWriter()
+{
+ if (!Telemetry::CanRecordExtended()) {
+ return;
+ }
+ nsCOMPtr<nsIFile> profLD;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR,
+ getter_AddRefs(profLD));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ rv = profLD->Append(NS_LITERAL_STRING("ShutdownDuration.json"));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsAutoString path;
+ rv = profLD->GetPath(path);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ gWriteReady = PR_NewMonitor();
+ MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(gWriteReady); // We will never deallocate this object
+ PRThread* writerThread = CreateSystemThread(RunWriter,
+ ToNewUTF8String(path));
+
+ if (!writerThread) {
+ return;
+ }
+}
+
+NS_IMETHODIMP
+nsTerminator::Observe(nsISupports *, const char *aTopic, const char16_t *)
+{
+ if (strcmp(aTopic, "profile-after-change") == 0) {
+ return SelfInit();
+ }
+
+ // Other notifications are shutdown-related.
+
+ // As we have seen examples in the wild of shutdown notifications
+ // not being sent (or not being sent in the expected order), we do
+ // not assume a specific order.
+ if (!mInitialized) {
+ Start();
+ }
+
+ UpdateHeartbeat(aTopic);
+#if !defined(DEBUG)
+ // Only allow nsTerminator to write on non-debug builds so we don't get leak warnings on
+ // shutdown for intentional leaks (see bug 1242084). This will be enabled again by bug
+ // 1255484 when 1255478 lands.
+ UpdateTelemetry();
+#endif // !defined(DEBUG)
+ UpdateCrashReport(aTopic);
+
+ // Perform a little cleanup
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ MOZ_RELEASE_ASSERT(os);
+ (void)os->RemoveObserver(this, aTopic);
+
+ return NS_OK;
+}
+
+void
+nsTerminator::UpdateHeartbeat(const char* aTopic)
+{
+ // Reset the clock, find out how long the current phase has lasted.
+ uint32_t ticks = gHeartbeat.exchange(0);
+ if (mCurrentStep > 0) {
+ sShutdownSteps[mCurrentStep].mTicks = ticks;
+ }
+
+ // Find out where we now are in the current shutdown.
+ // Don't assume that shutdown takes place in the expected order.
+ int nextStep = -1;
+ for (size_t i = 0; i < ArrayLength(sShutdownSteps); ++i) {
+ if (strcmp(sShutdownSteps[i].mTopic, aTopic) == 0) {
+ nextStep = i;
+ break;
+ }
+ }
+ MOZ_ASSERT(nextStep != -1);
+ mCurrentStep = nextStep;
+}
+
+void
+nsTerminator::UpdateTelemetry()
+{
+ if (!Telemetry::CanRecordExtended() || !gWriteReady) {
+ return;
+ }
+
+ //
+ // We need Telemetry data on the effective duration of each step,
+ // to be able to tune the time-to-crash of each of both the
+ // Terminator and AsyncShutdown. However, at this stage, it is too
+ // late to record such data into Telemetry, so we write it to disk
+ // and read it upon the next startup.
+ //
+
+ // Build JSON.
+ UniquePtr<nsCString> telemetryData(new nsCString());
+ telemetryData->AppendLiteral("{");
+ size_t fields = 0;
+ for (size_t i = 0; i < ArrayLength(sShutdownSteps); ++i) {
+ if (sShutdownSteps[i].mTicks < 0) {
+ // Ignore this field.
+ continue;
+ }
+ if (fields++ > 0) {
+ telemetryData->Append(", ");
+ }
+ telemetryData->AppendLiteral("\"");
+ telemetryData->Append(sShutdownSteps[i].mTopic);
+ telemetryData->AppendLiteral("\": ");
+ telemetryData->AppendInt(sShutdownSteps[i].mTicks);
+ }
+ telemetryData->AppendLiteral("}");
+
+ if (fields == 0) {
+ // Nothing to write
+ return;
+ }
+
+ //
+ // Send data to the worker thread.
+ //
+ delete gWriteData.exchange(telemetryData.release()); // Clear any data that hasn't been written yet
+
+ // In case the worker thread was sleeping, wake it up.
+ PR_EnterMonitor(gWriteReady);
+ PR_Notify(gWriteReady);
+ PR_ExitMonitor(gWriteReady);
+}
+
+void
+nsTerminator::UpdateCrashReport(const char* aTopic)
+{
+#if defined(MOZ_CRASHREPORTER)
+ // In case of crash, we wish to know where in shutdown we are
+ nsAutoCString report(aTopic);
+
+ Unused << CrashReporter::AnnotateCrashReport(NS_LITERAL_CSTRING("ShutdownProgress"),
+ report);
+#endif // defined(MOZ_CRASH_REPORTER)
+}
+
+
+} // namespace mozilla
diff --git a/toolkit/components/terminator/nsTerminator.h b/toolkit/components/terminator/nsTerminator.h
new file mode 100644
index 0000000000..4c23740a56
--- /dev/null
+++ b/toolkit/components/terminator/nsTerminator.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsTerminator_h__
+#define nsTerminator_h__
+
+#include "nsISupports.h"
+#include "nsIObserver.h"
+
+namespace mozilla {
+
+class nsTerminator final: public nsIObserver {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ nsTerminator();
+
+private:
+ nsresult SelfInit();
+ void Start();
+ void StartWatchdog();
+ void StartWriter();
+
+ void UpdateHeartbeat(const char* aTopic);
+ void UpdateTelemetry();
+ void UpdateCrashReport(const char* aTopic);
+
+ ~nsTerminator() {}
+
+ bool mInitialized;
+ int32_t mCurrentStep;
+};
+
+} // namespace mozilla
+
+#define NS_TOOLKIT_TERMINATOR_CID { 0x2e59cc70, 0xf83a, 0x412f, \
+ { 0x89, 0xd4, 0x45, 0x38, 0x85, 0x83, 0x72, 0x17 } }
+#define NS_TOOLKIT_TERMINATOR_CONTRACTID "@mozilla.org/toolkit/shutdown-terminator;1"
+
+#endif // nsTerminator_h__
diff --git a/toolkit/components/terminator/nsTerminatorTelemetry.js b/toolkit/components/terminator/nsTerminatorTelemetry.js
new file mode 100644
index 0000000000..ed017da788
--- /dev/null
+++ b/toolkit/components/terminator/nsTerminatorTelemetry.js
@@ -0,0 +1,105 @@
+/* -*- 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";
+
+/**
+ * Read the data saved by nsTerminator during shutdown and feed it to the
+ * relevant telemetry histograms.
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+function nsTerminatorTelemetry() {}
+
+var HISTOGRAMS = {
+ "quit-application": "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION",
+ "profile-change-teardown": "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN",
+ "profile-before-change": "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE",
+ "xpcom-will-shutdown": "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN",
+};
+
+nsTerminatorTelemetry.prototype = {
+ classID: Components.ID("{3f78ada1-cba2-442a-82dd-d5fb300ddea7}"),
+
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsTerminatorTelemetry),
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ // nsIObserver
+
+ observe: function DS_observe(aSubject, aTopic, aData)
+ {
+ Task.spawn(function*() {
+ //
+ // This data is hardly critical, reading it can wait for a few seconds.
+ //
+ yield new Promise(resolve => setTimeout(resolve, 3000));
+
+ let PATH = OS.Path.join(OS.Constants.Path.localProfileDir,
+ "ShutdownDuration.json");
+ let raw;
+ try {
+ raw = yield OS.File.read(PATH, { encoding: "utf-8" });
+ } catch (ex) {
+ if (!ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ return;
+ }
+ // Let other errors be reported by Promise's error-reporting.
+
+ // Clean up
+ OS.File.remove(PATH);
+ OS.File.remove(PATH + ".tmp");
+
+ let data = JSON.parse(raw);
+ for (let k of Object.keys(data)) {
+ let id = HISTOGRAMS[k];
+ try {
+ let histogram = Services.telemetry.getHistogramById(id);
+ if (!histogram) {
+ throw new Error("Unknown histogram " + id);
+ }
+
+ histogram.add(Number.parseInt(data[k]));
+ } catch (ex) {
+ // Make sure that the error is reported and causes test failures,
+ // but otherwise, ignore it.
+ Promise.reject(ex);
+ continue;
+ }
+ }
+
+ // Inform observers that we are done.
+ Services.obs.notifyObservers(null,
+ "shutdown-terminator-telemetry-updated",
+ "");
+ });
+ },
+};
+
+// Module
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsTerminatorTelemetry]);
diff --git a/toolkit/components/terminator/terminator.manifest b/toolkit/components/terminator/terminator.manifest
new file mode 100644
index 0000000000..c1757ba86a
--- /dev/null
+++ b/toolkit/components/terminator/terminator.manifest
@@ -0,0 +1,5 @@
+category profile-after-change nsTerminator @mozilla.org/toolkit/shutdown-terminator;1
+
+component {3f78ada1-cba2-442a-82dd-d5fb300ddea7} nsTerminatorTelemetry.js
+contract @mozilla.org/toolkit/shutdown-terminator-telemetry;1 {3f78ada1-cba2-442a-82dd-d5fb300ddea7}
+category profile-after-change nsTerminatorTelemetry @mozilla.org/toolkit/shutdown-terminator-telemetry;1
diff --git a/toolkit/components/terminator/tests/xpcshell/.eslintrc.js b/toolkit/components/terminator/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/terminator/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/terminator/tests/xpcshell/test_terminator_record.js b/toolkit/components/terminator/tests/xpcshell/test_terminator_record.js
new file mode 100644
index 0000000000..248ead9ce3
--- /dev/null
+++ b/toolkit/components/terminator/tests/xpcshell/test_terminator_record.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+
+// Test that the Shutdown Terminator records durations correctly
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+var {Path, File, Constants} = OS;
+
+var PATH;
+var PATH_TMP;
+var terminator;
+
+add_task(function* init() {
+ do_get_profile();
+ PATH = Path.join(Constants.Path.localProfileDir, "ShutdownDuration.json");
+ PATH_TMP = PATH + ".tmp";
+
+ // Initialize the terminator
+ // (normally, this is done through the manifest file, but xpcshell
+ // doesn't take them into account).
+ do_print("Initializing the Terminator");
+ terminator = Cc["@mozilla.org/toolkit/shutdown-terminator;1"].
+ createInstance(Ci.nsIObserver);
+ terminator.observe(null, "profile-after-change", null);
+});
+
+var promiseShutdownDurationData = Task.async(function*() {
+ // Wait until PATH exists.
+ // Timeout if it is never created.
+ do_print("Waiting for file creation: " + PATH);
+ while (true) {
+ if ((yield OS.File.exists(PATH))) {
+ break;
+ }
+
+ do_print("The file does not exist yet. Waiting 1 second.");
+ yield new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ do_print("The file has been created");
+ let raw = yield OS.File.read(PATH, { encoding: "utf-8"} );
+ do_print(raw);
+ return JSON.parse(raw);
+});
+
+add_task(function* test_record() {
+ let PHASE0 = "profile-change-teardown";
+ let PHASE1 = "profile-before-change";
+ let PHASE2 = "xpcom-will-shutdown";
+ let t0 = Date.now();
+
+ do_print("Starting shutdown");
+ terminator.observe(null, "profile-change-teardown", null);
+
+ do_print("Moving to next phase");
+ terminator.observe(null, PHASE1, null);
+
+ let data = yield promiseShutdownDurationData();
+
+ let t1 = Date.now();
+
+ Assert.ok(PHASE0 in data, "The file contains the expected key");
+ let duration = data[PHASE0];
+ Assert.equal(typeof duration, "number");
+ Assert.ok(duration >= 0, "Duration is a non-negative number");
+ Assert.ok(duration <= Math.ceil((t1 - t0) / 1000) + 1,
+ "Duration is reasonable");
+
+ Assert.equal(Object.keys(data).length, 1, "Data does not contain other durations");
+
+ do_print("Cleaning up and moving to next phase");
+ yield File.remove(PATH);
+ yield File.remove(PATH_TMP);
+
+ do_print("Waiting at least one tick");
+ let WAIT_MS = 2000;
+ yield new Promise(resolve => setTimeout(resolve, WAIT_MS));
+
+ terminator.observe(null, PHASE2, null);
+ data = yield promiseShutdownDurationData();
+
+ let t2 = Date.now();
+
+ Assert.equal(Object.keys(data).sort().join(", "),
+ [PHASE0, PHASE1].sort().join(", "),
+ "The file contains the expected keys");
+ Assert.equal(data[PHASE0], duration, "Duration of phase 0 hasn't changed");
+ let duration2 = data[PHASE1];
+ Assert.equal(typeof duration2, "number");
+ Assert.ok(duration2 >= WAIT_MS / 2000, "We have waited at least " + (WAIT_MS / 2000) + " ticks");
+ Assert.ok(duration2 <= Math.ceil((t2 - t1) / 1000) + 1,
+ "Duration is reasonable");
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js b/toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js
new file mode 100644
index 0000000000..1c16395b35
--- /dev/null
+++ b/toolkit/components/terminator/tests/xpcshell/test_terminator_reload.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+
+// Test that the Shutdown Terminator reloads durations correctly
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+var {Path, File, Constants} = OS;
+
+var PATH;
+
+var HISTOGRAMS = {
+ "quit-application": "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION",
+ "profile-change-teardown": "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN",
+ "profile-before-change": "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE",
+ "xpcom-will-shutdown": "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN",
+};
+
+add_task(function* init() {
+ do_get_profile();
+ PATH = Path.join(Constants.Path.localProfileDir, "ShutdownDuration.json");
+});
+
+add_task(function* test_reload() {
+ do_print("Forging data");
+ let data = {};
+ let telemetrySnapshots = Services.telemetry.histogramSnapshots;
+ let i = 0;
+ for (let k of Object.keys(HISTOGRAMS)) {
+ let id = HISTOGRAMS[k];
+ data[k] = i++;
+ Assert.equal(telemetrySnapshots[id] || undefined, undefined, "Histogram " + id + " is empty");
+ }
+
+
+ yield OS.File.writeAtomic(PATH, JSON.stringify(data));
+
+ const TOPIC = "shutdown-terminator-telemetry-updated";
+
+ let wait = new Promise(resolve =>
+ Services.obs.addObserver(
+ function observer() {
+ do_print("Telemetry has been updated");
+ Services.obs.removeObserver(observer, TOPIC);
+ resolve();
+ },
+ TOPIC,
+ false));
+
+ do_print("Starting nsTerminatorTelemetry");
+ let tt = Cc["@mozilla.org/toolkit/shutdown-terminator-telemetry;1"].
+ createInstance(Ci.nsIObserver);
+ tt.observe(null, "profile-after-change", "");
+
+ do_print("Waiting until telemetry is updated");
+ // Now wait until Telemetry is updated
+ yield wait;
+
+ telemetrySnapshots = Services.telemetry.histogramSnapshots;
+ for (let k of Object.keys(HISTOGRAMS)) {
+ let id = HISTOGRAMS[k];
+ do_print("Testing histogram " + id);
+ let snapshot = telemetrySnapshots[id];
+ let count = 0;
+ for (let x of snapshot.counts) {
+ count += x;
+ }
+ Assert.equal(count, 1, "We have added one item");
+ }
+
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/terminator/tests/xpcshell/xpcshell.ini b/toolkit/components/terminator/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..7f77938aa1
--- /dev/null
+++ b/toolkit/components/terminator/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head=
+tail=
+
+[test_terminator_record.js]
+skip-if = debug # Disabled by bug 1242084, bug 1255484 will enable it again.
+[test_terminator_reload.js]
+skip-if = os == "android"
diff --git a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
new file mode 100644
index 0000000000..fded51cea1
--- /dev/null
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -0,0 +1,495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 EXPORTED_SYMBOLS = [
+ "BackgroundPageThumbs",
+];
+
+const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms
+const DESTROY_BROWSER_TIMEOUT = 60000; // ms
+const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js";
+
+const TELEMETRY_HISTOGRAM_ID_PREFIX = "FX_THUMBNAILS_BG_";
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const ABOUT_NEWTAB_SEGREGATION_PREF = "privacy.usercontext.about_newtab_segregation.enabled";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+// possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2 telemetry values
+const TEL_CAPTURE_DONE_OK = 0;
+const TEL_CAPTURE_DONE_TIMEOUT = 1;
+// 2 and 3 were used when we had special handling for private-browsing.
+const TEL_CAPTURE_DONE_CRASHED = 4;
+const TEL_CAPTURE_DONE_BAD_URI = 5;
+
+// These are looked up on the global as properties below.
+XPCOMUtils.defineConstant(this, "TEL_CAPTURE_DONE_OK", TEL_CAPTURE_DONE_OK);
+XPCOMUtils.defineConstant(this, "TEL_CAPTURE_DONE_TIMEOUT", TEL_CAPTURE_DONE_TIMEOUT);
+XPCOMUtils.defineConstant(this, "TEL_CAPTURE_DONE_CRASHED", TEL_CAPTURE_DONE_CRASHED);
+XPCOMUtils.defineConstant(this, "TEL_CAPTURE_DONE_BAD_URI", TEL_CAPTURE_DONE_BAD_URI);
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
+ "resource://gre/modules/ContextualIdentityService.jsm");
+const global = this;
+
+const BackgroundPageThumbs = {
+
+ /**
+ * Asynchronously captures a thumbnail of the given URL.
+ *
+ * The page is loaded anonymously, and plug-ins are disabled.
+ *
+ * @param url The URL to capture.
+ * @param options An optional object that configures the capture. Its
+ * properties are the following, and all are optional:
+ * @opt onDone A function that will be asynchronously called when the
+ * capture is complete or times out. It's called as
+ * onDone(url),
+ * where `url` is the captured URL.
+ * @opt timeout The capture will time out after this many milliseconds have
+ * elapsed after the capture has progressed to the head of
+ * the queue and started. Defaults to 30000 (30 seconds).
+ */
+ capture: function (url, options={}) {
+ if (!PageThumbs._prefEnabled()) {
+ if (options.onDone)
+ schedule(() => options.onDone(url));
+ return;
+ }
+ this._captureQueue = this._captureQueue || [];
+ this._capturesByURL = this._capturesByURL || new Map();
+
+ tel("QUEUE_SIZE_ON_CAPTURE", this._captureQueue.length);
+
+ // We want to avoid duplicate captures for the same URL. If there is an
+ // existing one, we just add the callback to that one and we are done.
+ let existing = this._capturesByURL.get(url);
+ if (existing) {
+ if (options.onDone)
+ existing.doneCallbacks.push(options.onDone);
+ // The queue is already being processed, so nothing else to do...
+ return;
+ }
+ let cap = new Capture(url, this._onCaptureOrTimeout.bind(this), options);
+ this._captureQueue.push(cap);
+ this._capturesByURL.set(url, cap);
+ this._processCaptureQueue();
+ },
+
+ /**
+ * Asynchronously captures a thumbnail of the given URL if one does not
+ * already exist. Otherwise does nothing.
+ *
+ * @param url The URL to capture.
+ * @param options An optional object that configures the capture. See
+ * capture() for description.
+ * @return {Promise} A Promise that resolves when this task completes
+ */
+ captureIfMissing: Task.async(function* (url, options={}) {
+ // The fileExistsForURL call is an optimization, potentially but unlikely
+ // incorrect, and no big deal when it is. After the capture is done, we
+ // atomically test whether the file exists before writing it.
+ let exists = yield PageThumbsStorage.fileExistsForURL(url);
+ if (exists) {
+ if (options.onDone) {
+ options.onDone(url);
+ }
+ return url;
+ }
+ let thumbPromise = new Promise((resolve, reject) => {
+ function observe(subject, topic, data) { // jshint ignore:line
+ if (data === url) {
+ switch (topic) {
+ case "page-thumbnail:create":
+ resolve();
+ break;
+ case "page-thumbnail:error":
+ reject(new Error("page-thumbnail:error"));
+ break;
+ }
+ Services.obs.removeObserver(observe, "page-thumbnail:create");
+ Services.obs.removeObserver(observe, "page-thumbnail:error");
+ }
+ }
+ Services.obs.addObserver(observe, "page-thumbnail:create", false);
+ Services.obs.addObserver(observe, "page-thumbnail:error", false);
+ });
+ try {
+ this.capture(url, options);
+ yield thumbPromise;
+ } catch (err) {
+ if (options.onDone) {
+ options.onDone(url);
+ }
+ throw err;
+ }
+ return url;
+ }),
+
+ /**
+ * Tell the service that the thumbnail browser should be recreated at next
+ * call of _ensureBrowser().
+ */
+ renewThumbnailBrowser: function() {
+ this._renewThumbBrowser = true;
+ },
+
+ /**
+ * Ensures that initialization of the thumbnail browser's parent window has
+ * begun.
+ *
+ * @return True if the parent window is completely initialized and can be
+ * used, and false if initialization has started but not completed.
+ */
+ _ensureParentWindowReady: function () {
+ if (this._parentWin)
+ // Already fully initialized.
+ return true;
+ if (this._startedParentWinInit)
+ // Already started initializing.
+ return false;
+
+ this._startedParentWinInit = true;
+
+ // Create an html:iframe, stick it in the parent document, and
+ // use it to host the browser. about:blank will not have the system
+ // principal, so it can't host, but a document with a chrome URI will.
+ let hostWindow = Services.appShell.hiddenDOMWindow;
+ let iframe = hostWindow.document.createElementNS(HTML_NS, "iframe");
+ iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
+ let onLoad = function onLoadFn() {
+ iframe.removeEventListener("load", onLoad, true);
+ this._parentWin = iframe.contentWindow;
+ this._processCaptureQueue();
+ }.bind(this);
+ iframe.addEventListener("load", onLoad, true);
+ hostWindow.document.documentElement.appendChild(iframe);
+ this._hostIframe = iframe;
+
+ return false;
+ },
+
+ /**
+ * Destroys the service. Queued and pending captures will never complete, and
+ * their consumer callbacks will never be called.
+ */
+ _destroy: function () {
+ if (this._captureQueue)
+ this._captureQueue.forEach(cap => cap.destroy());
+ this._destroyBrowser();
+ if (this._hostIframe)
+ this._hostIframe.remove();
+ delete this._captureQueue;
+ delete this._hostIframe;
+ delete this._startedParentWinInit;
+ delete this._parentWin;
+ },
+
+ /**
+ * Creates the thumbnail browser if it doesn't already exist.
+ */
+ _ensureBrowser: function () {
+ if (this._thumbBrowser && !this._renewThumbBrowser)
+ return;
+
+ this._destroyBrowser();
+ this._renewThumbBrowser = false;
+
+ let browser = this._parentWin.document.createElementNS(XUL_NS, "browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("remote", "true");
+ browser.setAttribute("disableglobalhistory", "true");
+
+ if (Services.prefs.getBoolPref(ABOUT_NEWTAB_SEGREGATION_PREF)) {
+ // Use the private container for thumbnails.
+ let privateIdentity =
+ ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail");
+ browser.setAttribute("usercontextid", privateIdentity.userContextId);
+ }
+
+ // Size the browser. Make its aspect ratio the same as the canvases' that
+ // the thumbnails are drawn into; the canvases' aspect ratio is the same as
+ // the screen's, so use that. Aim for a size in the ballpark of 1024x768.
+ let [swidth, sheight] = [{}, {}];
+ Cc["@mozilla.org/gfx/screenmanager;1"].
+ getService(Ci.nsIScreenManager).
+ primaryScreen.
+ GetRectDisplayPix({}, {}, swidth, sheight);
+ let bwidth = Math.min(1024, swidth.value);
+ // Setting the width and height attributes doesn't work -- the resulting
+ // thumbnails are blank and transparent -- but setting the style does.
+ browser.style.width = bwidth + "px";
+ browser.style.height = (bwidth * sheight.value / swidth.value) + "px";
+
+ this._parentWin.document.documentElement.appendChild(browser);
+
+ // an event that is sent if the remote process crashes - no need to remove
+ // it as we want it to be there as long as the browser itself lives.
+ browser.addEventListener("oop-browser-crashed", () => {
+ Cu.reportError("BackgroundThumbnails remote process crashed - recovering");
+ this._destroyBrowser();
+ let curCapture = this._captureQueue.length ? this._captureQueue[0] : null;
+ // we could retry the pending capture, but it's possible the crash
+ // was due directly to it, so trying again might just crash again.
+ // We could keep a flag to indicate if it previously crashed, but
+ // "resetting" the capture requires more work - so for now, we just
+ // discard it.
+ if (curCapture && curCapture.pending) {
+ // Continue queue processing by calling curCapture._done(). Do it after
+ // this crashed listener returns, though. A new browser will be created
+ // immediately (on the same stack as the _done call stack) if there are
+ // any more queued-up captures, and that seems to mess up the new
+ // browser's message manager if it happens on the same stack as the
+ // listener. Trying to send a message to the manager in that case
+ // throws NS_ERROR_NOT_INITIALIZED.
+ Services.tm.currentThread.dispatch(() => {
+ curCapture._done(null, TEL_CAPTURE_DONE_CRASHED);
+ }, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ }
+ // else: we must have been idle and not currently doing a capture (eg,
+ // maybe a GC or similar crashed) - so there's no need to attempt a
+ // queue restart - the next capture request will set everything up.
+ });
+
+ browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+ this._thumbBrowser = browser;
+ },
+
+ _destroyBrowser: function () {
+ if (!this._thumbBrowser)
+ return;
+ this._thumbBrowser.remove();
+ delete this._thumbBrowser;
+ },
+
+ /**
+ * Starts the next capture if the queue is not empty and the service is fully
+ * initialized.
+ */
+ _processCaptureQueue: function () {
+ if (!this._captureQueue.length ||
+ this._captureQueue[0].pending ||
+ !this._ensureParentWindowReady())
+ return;
+
+ // Ready to start the first capture in the queue.
+ this._ensureBrowser();
+ this._captureQueue[0].start(this._thumbBrowser.messageManager);
+ if (this._destroyBrowserTimer) {
+ this._destroyBrowserTimer.cancel();
+ delete this._destroyBrowserTimer;
+ }
+ },
+
+ /**
+ * Called when the current capture completes or fails (eg, times out, remote
+ * process crashes.)
+ */
+ _onCaptureOrTimeout: function (capture) {
+ // Since timeouts start as an item is being processed, only the first
+ // item in the queue can be passed to this method.
+ if (capture !== this._captureQueue[0])
+ throw new Error("The capture should be at the head of the queue.");
+ this._captureQueue.shift();
+ this._capturesByURL.delete(capture.url);
+ if (capture.doneReason != TEL_CAPTURE_DONE_OK) {
+ Services.obs.notifyObservers(null, "page-thumbnail:error", capture.url);
+ }
+
+ // Start the destroy-browser timer *before* processing the capture queue.
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this._destroyBrowser.bind(this),
+ this._destroyBrowserTimeout,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ this._destroyBrowserTimer = timer;
+
+ this._processCaptureQueue();
+ },
+
+ _destroyBrowserTimeout: DESTROY_BROWSER_TIMEOUT,
+};
+
+Services.prefs.addObserver(ABOUT_NEWTAB_SEGREGATION_PREF,
+ function(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed" && aData == ABOUT_NEWTAB_SEGREGATION_PREF) {
+ BackgroundPageThumbs.renewThumbnailBrowser();
+ }
+ },
+ false);
+
+Object.defineProperty(this, "BackgroundPageThumbs", {
+ value: BackgroundPageThumbs,
+ enumerable: true,
+ writable: false
+});
+
+/**
+ * Represents a single capture request in the capture queue.
+ *
+ * @param url The URL to capture.
+ * @param captureCallback A function you want called when the capture
+ * completes.
+ * @param options The capture options.
+ */
+function Capture(url, captureCallback, options) {
+ this.url = url;
+ this.captureCallback = captureCallback;
+ this.options = options;
+ this.id = Capture.nextID++;
+ this.creationDate = new Date();
+ this.doneCallbacks = [];
+ this.doneReason;
+ if (options.onDone)
+ this.doneCallbacks.push(options.onDone);
+}
+
+Capture.prototype = {
+
+ get pending() {
+ return !!this._msgMan;
+ },
+
+ /**
+ * Sends a message to the content script to start the capture.
+ *
+ * @param messageManager The nsIMessageSender of the thumbnail browser.
+ */
+ start: function (messageManager) {
+ this.startDate = new Date();
+ tel("CAPTURE_QUEUE_TIME_MS", this.startDate - this.creationDate);
+
+ // timeout timer
+ let timeout = typeof(this.options.timeout) == "number" ?
+ this.options.timeout :
+ DEFAULT_CAPTURE_TIMEOUT;
+ this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timeoutTimer.initWithCallback(this, timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+
+ // didCapture registration
+ this._msgMan = messageManager;
+ this._msgMan.sendAsyncMessage("BackgroundPageThumbs:capture",
+ { id: this.id, url: this.url });
+ this._msgMan.addMessageListener("BackgroundPageThumbs:didCapture", this);
+ },
+
+ /**
+ * The only intended external use of this method is by the service when it's
+ * uninitializing and doing things like destroying the thumbnail browser. In
+ * that case the consumer's completion callback will never be called.
+ */
+ destroy: function () {
+ // This method may be called for captures that haven't started yet, so
+ // guard against not yet having _timeoutTimer, _msgMan etc properties...
+ if (this._timeoutTimer) {
+ this._timeoutTimer.cancel();
+ delete this._timeoutTimer;
+ }
+ if (this._msgMan) {
+ this._msgMan.removeMessageListener("BackgroundPageThumbs:didCapture",
+ this);
+ delete this._msgMan;
+ }
+ delete this.captureCallback;
+ delete this.doneCallbacks;
+ delete this.options;
+ },
+
+ // Called when the didCapture message is received.
+ receiveMessage: function (msg) {
+ if (msg.data.imageData)
+ tel("CAPTURE_SERVICE_TIME_MS", new Date() - this.startDate);
+
+ // A different timed-out capture may have finally successfully completed, so
+ // discard messages that aren't meant for this capture.
+ if (msg.data.id != this.id)
+ return;
+
+ if (msg.data.failReason) {
+ let reason = global["TEL_CAPTURE_DONE_" + msg.data.failReason];
+ this._done(null, reason);
+ return;
+ }
+
+ this._done(msg.data, TEL_CAPTURE_DONE_OK);
+ },
+
+ // Called when the timeout timer fires.
+ notify: function () {
+ this._done(null, TEL_CAPTURE_DONE_TIMEOUT);
+ },
+
+ _done: function (data, reason) {
+ // Note that _done will be called only once, by either receiveMessage or
+ // notify, since it calls destroy here, which cancels the timeout timer and
+ // removes the didCapture message listener.
+ let { captureCallback, doneCallbacks, options } = this;
+ this.destroy();
+ this.doneReason = reason;
+
+ if (typeof(reason) != "number") {
+ throw new Error("A done reason must be given.");
+ }
+ tel("CAPTURE_DONE_REASON_2", reason);
+ if (data && data.telemetry) {
+ // Telemetry is currently disabled in the content process (bug 680508).
+ for (let id in data.telemetry) {
+ tel(id, data.telemetry[id]);
+ }
+ }
+
+ let done = () => {
+ captureCallback(this);
+ for (let callback of doneCallbacks) {
+ try {
+ callback.call(options, this.url);
+ }
+ catch (err) {
+ Cu.reportError(err);
+ }
+ }
+
+ if (Services.prefs.getBoolPref(ABOUT_NEWTAB_SEGREGATION_PREF)) {
+ // Clear the data in the private container for thumbnails.
+ let privateIdentity =
+ ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail");
+ Services.obs.notifyObservers(null, "clear-origin-attributes-data",
+ JSON.stringify({ userContextId: privateIdentity.userContextId }));
+ }
+ };
+
+ if (!data) {
+ done();
+ return;
+ }
+
+ PageThumbs._store(this.url, data.finalURL, data.imageData, true)
+ .then(done, done);
+ },
+};
+
+Capture.nextID = 0;
+
+/**
+ * Adds a value to one of this module's telemetry histograms.
+ *
+ * @param histogramID This is prefixed with this module's ID.
+ * @param value The value to add.
+ */
+function tel(histogramID, value) {
+ let id = TELEMETRY_HISTOGRAM_ID_PREFIX + histogramID;
+ Services.telemetry.getHistogramById(id).add(value);
+}
+
+function schedule(callback) {
+ Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+}
diff --git a/toolkit/components/thumbnails/BrowserPageThumbs.manifest b/toolkit/components/thumbnails/BrowserPageThumbs.manifest
new file mode 100644
index 0000000000..8dfc0597b2
--- /dev/null
+++ b/toolkit/components/thumbnails/BrowserPageThumbs.manifest
@@ -0,0 +1,2 @@
+component {5a4ae9b5-f475-48ae-9dce-0b4c1d347884} PageThumbsProtocol.js
+contract @mozilla.org/network/protocol;1?name=moz-page-thumb {5a4ae9b5-f475-48ae-9dce-0b4c1d347884}
diff --git a/toolkit/components/thumbnails/PageThumbUtils.jsm b/toolkit/components/thumbnails/PageThumbUtils.jsm
new file mode 100644
index 0000000000..dda3a81b3c
--- /dev/null
+++ b/toolkit/components/thumbnails/PageThumbUtils.jsm
@@ -0,0 +1,354 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Common thumbnailing routines used by various consumers, including
+ * PageThumbs and backgroundPageThumbsContent.
+ */
+
+this.EXPORTED_SYMBOLS = ["PageThumbUtils"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+
+this.PageThumbUtils = {
+ // The default background color for page thumbnails.
+ THUMBNAIL_BG_COLOR: "#fff",
+ // The namespace for thumbnail canvas elements.
+ HTML_NAMESPACE: "http://www.w3.org/1999/xhtml",
+
+ /**
+ * Creates a new canvas element in the context of aWindow, or if aWindow
+ * is undefined, in the context of hiddenDOMWindow.
+ *
+ * @param aWindow (optional) The document of this window will be used to
+ * create the canvas. If not given, the hidden window will be used.
+ * @param aWidth (optional) width of the canvas to create
+ * @param aHeight (optional) height of the canvas to create
+ * @return The newly created canvas.
+ */
+ createCanvas: function (aWindow, aWidth = 0, aHeight = 0) {
+ let doc = (aWindow || Services.appShell.hiddenDOMWindow).document;
+ let canvas = doc.createElementNS(this.HTML_NAMESPACE, "canvas");
+ canvas.mozOpaque = true;
+ canvas.imageSmoothingEnabled = true;
+ let [thumbnailWidth, thumbnailHeight] = this.getThumbnailSize(aWindow);
+ canvas.width = aWidth ? aWidth : thumbnailWidth;
+ canvas.height = aHeight ? aHeight : thumbnailHeight;
+ return canvas;
+ },
+
+ /**
+ * Calculates a preferred initial thumbnail size based based on newtab.css
+ * sizes or a preference for other applications. The sizes should be the same
+ * as set for the tile sizes in newtab.
+ *
+ * @param aWindow (optional) aWindow that is used to calculate the scaling size.
+ * @return The calculated thumbnail size or a default if unable to calculate.
+ */
+ getThumbnailSize: function (aWindow = null) {
+ if (!this._thumbnailWidth || !this._thumbnailHeight) {
+ let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager);
+ let left = {}, top = {}, screenWidth = {}, screenHeight = {};
+ screenManager.primaryScreen.GetRectDisplayPix(left, top, screenWidth, screenHeight);
+
+ /** *
+ * The system default scale might be different than
+ * what is reported by the window. For example,
+ * retina displays have 1:1 system scales, but 2:1 window
+ * scale as 1 pixel system wide == 2 device pixels.
+ * To get the best image quality, query both and take the highest one.
+ */
+ let systemScale = screenManager.systemDefaultScale;
+ let windowScale = aWindow ? aWindow.devicePixelRatio : systemScale;
+ let scale = Math.max(systemScale, windowScale);
+
+ /** *
+ * On retina displays, we can sometimes go down this path
+ * without a window object. In those cases, force 2x scaling
+ * as the system scale doesn't represent the 2x scaling
+ * on OS X.
+ */
+ if (AppConstants.platform == "macosx" && !aWindow) {
+ scale = 2;
+ }
+
+ /** *
+ * THESE VALUES ARE DEFINED IN newtab.css and hard coded.
+ * If you change these values from the prefs,
+ * ALSO CHANGE THEM IN newtab.css
+ */
+ let prefWidth = Services.prefs.getIntPref("toolkit.pageThumbs.minWidth");
+ let prefHeight = Services.prefs.getIntPref("toolkit.pageThumbs.minHeight");
+ let divisor = Services.prefs.getIntPref("toolkit.pageThumbs.screenSizeDivisor");
+
+ prefWidth *= scale;
+ prefHeight *= scale;
+
+ this._thumbnailWidth = Math.max(Math.round(screenWidth.value / divisor), prefWidth);
+ this._thumbnailHeight = Math.max(Math.round(screenHeight.value / divisor), prefHeight);
+ }
+
+ return [this._thumbnailWidth, this._thumbnailHeight];
+ },
+
+ /** *
+ * Given a browser window, return the size of the content
+ * minus the scroll bars.
+ */
+ getContentSize: function(aWindow) {
+ let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ // aWindow may be a cpow, add exposed props security values.
+ let sbWidth = {}, sbHeight = {};
+
+ try {
+ utils.getScrollbarSize(false, sbWidth, sbHeight);
+ } catch (e) {
+ // This might fail if the window does not have a presShell.
+ Cu.reportError("Unable to get scrollbar size in determineCropSize.");
+ sbWidth.value = sbHeight.value = 0;
+ }
+
+ // Even in RTL mode, scrollbars are always on the right.
+ // So there's no need to determine a left offset.
+ let width = aWindow.innerWidth - sbWidth.value;
+ let height = aWindow.innerHeight - sbHeight.value;
+
+ return [width, height];
+ },
+
+ /** *
+ * Given a browser window, this creates a snapshot of the content
+ * and returns a canvas with the resulting snapshot of the content
+ * at the thumbnail size. It has to do this through a two step process:
+ *
+ * 1) Render the content at the window size to a canvas that is 2x the thumbnail size
+ * 2) Downscale the canvas from (1) down to the thumbnail size
+ *
+ * This is because the thumbnail size is too small to render at directly,
+ * causing pages to believe the browser is a small resolution. Also,
+ * at that resolution, graphical artifacts / text become very jagged.
+ * It's actually better to the eye to have small blurry text than sharp
+ * jagged pixels to represent text.
+ *
+ * @params aWindow - the window to create a snapshot of.
+ * @params aDestCanvas destination canvas to draw the final
+ * snapshot to. Can be null.
+ * @param aArgs (optional) Additional named parameters:
+ * fullScale - request that a non-downscaled image be returned.
+ * @return Canvas with a scaled thumbnail of the window.
+ */
+ createSnapshotThumbnail: function(aWindow, aDestCanvas, aArgs) {
+ if (Cu.isCrossProcessWrapper(aWindow)) {
+ throw new Error('Do not pass cpows here.');
+ }
+ let fullScale = aArgs ? aArgs.fullScale : false;
+ let [contentWidth, contentHeight] = this.getContentSize(aWindow);
+ let [thumbnailWidth, thumbnailHeight] = aDestCanvas ?
+ [aDestCanvas.width, aDestCanvas.height] :
+ this.getThumbnailSize(aWindow);
+
+ // If the caller wants a fullscale image, set the desired thumbnail dims
+ // to the dims of content and (if provided) size the incoming canvas to
+ // support our results.
+ if (fullScale) {
+ thumbnailWidth = contentWidth;
+ thumbnailHeight = contentHeight;
+ if (aDestCanvas) {
+ aDestCanvas.width = contentWidth;
+ aDestCanvas.height = contentHeight;
+ }
+ }
+
+ let intermediateWidth = thumbnailWidth * 2;
+ let intermediateHeight = thumbnailHeight * 2;
+ let skipDownscale = false;
+
+ // If the intermediate thumbnail is larger than content dims (hiDPI
+ // devices can experience this) or a full preview is requested render
+ // at the final thumbnail size.
+ if ((intermediateWidth >= contentWidth ||
+ intermediateHeight >= contentHeight) || fullScale) {
+ intermediateWidth = thumbnailWidth;
+ intermediateHeight = thumbnailHeight;
+ skipDownscale = true;
+ }
+
+ // Create an intermediate surface
+ let snapshotCanvas = this.createCanvas(aWindow, intermediateWidth,
+ intermediateHeight);
+
+ // Step 1: capture the image at the intermediate dims. For thumbnails
+ // this is twice the thumbnail size, for fullScale images this is at
+ // content dims.
+ // Also by default, canvas does not draw the scrollbars, so no need to
+ // remove the scrollbar sizes.
+ let scale = Math.min(Math.max(intermediateWidth / contentWidth,
+ intermediateHeight / contentHeight), 1);
+
+ let snapshotCtx = snapshotCanvas.getContext("2d");
+ snapshotCtx.save();
+ snapshotCtx.scale(scale, scale);
+ snapshotCtx.drawWindow(aWindow, 0, 0, contentWidth, contentHeight,
+ PageThumbUtils.THUMBNAIL_BG_COLOR,
+ snapshotCtx.DRAWWINDOW_DO_NOT_FLUSH);
+ snapshotCtx.restore();
+
+ // Part 2: Downscale from our intermediate dims to the final thumbnail
+ // dims and copy the result to aDestCanvas. If the caller didn't
+ // provide a target canvas, create a new canvas and return it.
+ let finalCanvas = aDestCanvas ||
+ this.createCanvas(aWindow, thumbnailWidth, thumbnailHeight);
+
+ let finalCtx = finalCanvas.getContext("2d");
+ finalCtx.save();
+ if (!skipDownscale) {
+ finalCtx.scale(0.5, 0.5);
+ }
+ finalCtx.drawImage(snapshotCanvas, 0, 0);
+ finalCtx.restore();
+
+ return finalCanvas;
+ },
+
+ /**
+ * Determine a good thumbnail crop size and scale for a given content
+ * window.
+ *
+ * @param aWindow The content window.
+ * @param aCanvas The target canvas.
+ * @return An array containing width, height and scale.
+ */
+ determineCropSize: function (aWindow, aCanvas) {
+ if (Cu.isCrossProcessWrapper(aWindow)) {
+ throw new Error('Do not pass cpows here.');
+ }
+ let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ // aWindow may be a cpow, add exposed props security values.
+ let sbWidth = {}, sbHeight = {};
+
+ try {
+ utils.getScrollbarSize(false, sbWidth, sbHeight);
+ } catch (e) {
+ // This might fail if the window does not have a presShell.
+ Cu.reportError("Unable to get scrollbar size in determineCropSize.");
+ sbWidth.value = sbHeight.value = 0;
+ }
+
+ // Even in RTL mode, scrollbars are always on the right.
+ // So there's no need to determine a left offset.
+ let width = aWindow.innerWidth - sbWidth.value;
+ let height = aWindow.innerHeight - sbHeight.value;
+
+ let {width: thumbnailWidth, height: thumbnailHeight} = aCanvas;
+ let scale = Math.min(Math.max(thumbnailWidth / width, thumbnailHeight / height), 1);
+ let scaledWidth = width * scale;
+ let scaledHeight = height * scale;
+
+ if (scaledHeight > thumbnailHeight)
+ height -= Math.floor(Math.abs(scaledHeight - thumbnailHeight) * scale);
+
+ if (scaledWidth > thumbnailWidth)
+ width -= Math.floor(Math.abs(scaledWidth - thumbnailWidth) * scale);
+
+ return [width, height, scale];
+ },
+
+ shouldStoreContentThumbnail: function (aDocument, aDocShell) {
+ if (BrowserUtils.isToolbarVisible(aDocShell, "findbar")) {
+ return false;
+ }
+
+ // FIXME Bug 720575 - Don't capture thumbnails for SVG or XML documents as
+ // that currently regresses Talos SVG tests.
+ if (aDocument instanceof Ci.nsIDOMXMLDocument) {
+ return false;
+ }
+
+ let webNav = aDocShell.QueryInterface(Ci.nsIWebNavigation);
+
+ // Don't take screenshots of about: pages.
+ if (webNav.currentURI.schemeIs("about")) {
+ return false;
+ }
+
+ // There's no point in taking screenshot of loading pages.
+ if (aDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
+ return false;
+ }
+
+ let channel = aDocShell.currentDocumentChannel;
+
+ // No valid document channel. We shouldn't take a screenshot.
+ if (!channel) {
+ return false;
+ }
+
+ // Don't take screenshots of internally redirecting about: pages.
+ // This includes error pages.
+ let uri = channel.originalURI;
+ if (uri.schemeIs("about")) {
+ return false;
+ }
+
+ let httpChannel;
+ try {
+ httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ } catch (e) { /* Not an HTTP channel. */ }
+
+ if (httpChannel) {
+ // Continue only if we have a 2xx status code.
+ try {
+ if (Math.floor(httpChannel.responseStatus / 100) != 2) {
+ return false;
+ }
+ } catch (e) {
+ // Can't get response information from the httpChannel
+ // because mResponseHead is not available.
+ return false;
+ }
+
+ // Cache-Control: no-store.
+ if (httpChannel.isNoStoreResponse()) {
+ return false;
+ }
+
+ // Don't capture HTTPS pages unless the user explicitly enabled it.
+ if (uri.schemeIs("https") &&
+ !Services.prefs.getBoolPref("browser.cache.disk_cache_ssl")) {
+ return false;
+ }
+ } // httpChannel
+ return true;
+ },
+
+ /**
+ * Given a channel, returns true if it should be considered an "error
+ * response", false otherwise.
+ */
+ isChannelErrorResponse: function(channel) {
+ // No valid document channel sounds like an error to me!
+ if (!channel)
+ return true;
+ if (!(channel instanceof Ci.nsIHttpChannel))
+ // it might be FTP etc, so assume it's ok.
+ return false;
+ try {
+ return !channel.requestSucceeded;
+ } catch (_) {
+ // not being able to determine success is surely failure!
+ return true;
+ }
+ },
+};
diff --git a/toolkit/components/thumbnails/PageThumbs.jsm b/toolkit/components/thumbnails/PageThumbs.jsm
new file mode 100644
index 0000000000..9bd3ae4b3b
--- /dev/null
+++ b/toolkit/components/thumbnails/PageThumbs.jsm
@@ -0,0 +1,901 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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";
+
+this.EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsStorage"];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
+const LATEST_STORAGE_VERSION = 3;
+
+const EXPIRATION_MIN_CHUNK_SIZE = 50;
+const EXPIRATION_INTERVAL_SECS = 3600;
+
+var gRemoteThumbId = 0;
+
+// If a request for a thumbnail comes in and we find one that is "stale"
+// (or don't find one at all) we automatically queue a request to generate a
+// new one.
+const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
+
+/**
+ * Name of the directory in the profile that contains the thumbnails.
+ */
+const THUMBNAIL_DIRECTORY = "thumbnails";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+Cu.importGlobalProperties(['FileReader']);
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUpdateTimerManager",
+ "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
+
+XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
+ return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = 'utf8';
+ return converter;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageThumbUtils",
+ "resource://gre/modules/PageThumbUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+/**
+ * Utilities for dealing with promises and Task.jsm
+ */
+const TaskUtils = {
+ /**
+ * Read the bytes from a blob, asynchronously.
+ *
+ * @return {Promise}
+ * @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
+ * @reject {DOMError} In case of error, the underlying DOMError.
+ */
+ readBlob: function readBlob(blob) {
+ let deferred = Promise.defer();
+ let reader = new FileReader();
+ reader.onloadend = function onloadend() {
+ if (reader.readyState != FileReader.DONE) {
+ deferred.reject(reader.error);
+ } else {
+ deferred.resolve(reader.result);
+ }
+ };
+ reader.readAsArrayBuffer(blob);
+ return deferred.promise;
+ }
+};
+
+
+
+
+/**
+ * Singleton providing functionality for capturing web page thumbnails and for
+ * accessing them if already cached.
+ */
+this.PageThumbs = {
+ _initialized: false,
+
+ /**
+ * The calculated width and height of the thumbnails.
+ */
+ _thumbnailWidth : 0,
+ _thumbnailHeight : 0,
+
+ /**
+ * The scheme to use for thumbnail urls.
+ */
+ get scheme() {
+ return "moz-page-thumb";
+ },
+
+ /**
+ * The static host to use for thumbnail urls.
+ */
+ get staticHost() {
+ return "thumbnail";
+ },
+
+ /**
+ * The thumbnails' image type.
+ */
+ get contentType() {
+ return "image/png";
+ },
+
+ init: function PageThumbs_init() {
+ if (!this._initialized) {
+ this._initialized = true;
+ PlacesUtils.history.addObserver(PageThumbsHistoryObserver, true);
+
+ // Migrate the underlying storage, if needed.
+ PageThumbsStorageMigrator.migrate();
+ PageThumbsExpiration.init();
+ }
+ },
+
+ uninit: function PageThumbs_uninit() {
+ if (this._initialized) {
+ this._initialized = false;
+ }
+ },
+
+ /**
+ * Gets the thumbnail image's url for a given web page's url.
+ * @param aUrl The web page's url that is depicted in the thumbnail.
+ * @return The thumbnail image's url.
+ */
+ getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
+ return this.scheme + "://" + this.staticHost +
+ "/?url=" + encodeURIComponent(aUrl) +
+ "&revision=" + PageThumbsStorage.getRevision(aUrl);
+ },
+
+ /**
+ * Gets the path of the thumbnail file for a given web page's
+ * url. This file may or may not exist depending on whether the
+ * thumbnail has been captured or not.
+ *
+ * @param aUrl The web page's url.
+ * @return The path of the thumbnail file.
+ */
+ getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) {
+ return PageThumbsStorage.getFilePathForURL(aUrl);
+ },
+
+ /**
+ * Asynchronously returns a thumbnail as a blob for the given
+ * window.
+ *
+ * @param aBrowser The <browser> to capture a thumbnail from.
+ * @return {Promise}
+ * @resolve {Blob} The thumbnail, as a Blob.
+ */
+ captureToBlob: function PageThumbs_captureToBlob(aBrowser) {
+ if (!this._prefEnabled()) {
+ return null;
+ }
+
+ let deferred = Promise.defer();
+
+ let canvas = this.createCanvas(aBrowser.contentWindow);
+ this.captureToCanvas(aBrowser, canvas, () => {
+ canvas.toBlob(blob => {
+ deferred.resolve(blob, this.contentType);
+ });
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Captures a thumbnail from a given window and draws it to the given canvas.
+ * Note, when dealing with remote content, this api draws into the passed
+ * canvas asynchronously. Pass aCallback to receive an async callback after
+ * canvas painting has completed.
+ * @param aBrowser The browser to capture a thumbnail from.
+ * @param aCanvas The canvas to draw to. The thumbnail will be scaled to match
+ * the dimensions of this canvas. If callers pass a 0x0 canvas, the canvas
+ * will be resized to default thumbnail dimensions just prior to painting.
+ * @param aCallback (optional) A callback invoked once the thumbnail has been
+ * rendered to aCanvas.
+ * @param aArgs (optional) Additional named parameters:
+ * fullScale - request that a non-downscaled image be returned.
+ */
+ captureToCanvas: function (aBrowser, aCanvas, aCallback, aArgs) {
+ let telemetryCaptureTime = new Date();
+ let args = {
+ fullScale: aArgs ? aArgs.fullScale : false
+ };
+ this._captureToCanvas(aBrowser, aCanvas, args, (aCanvas) => {
+ Services.telemetry
+ .getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
+ .add(new Date() - telemetryCaptureTime);
+ if (aCallback) {
+ aCallback(aCanvas);
+ }
+ });
+ },
+
+ /**
+ * Asynchronously check the state of aBrowser to see if it passes a set of
+ * predefined security checks. Consumers should refrain from storing
+ * thumbnails if these checks fail. Note the final result of this call is
+ * transitory as it is based on current navigation state and the type of
+ * content being displayed.
+ *
+ * @param aBrowser The target browser
+ * @param aCallback(aResult) A callback invoked once security checks have
+ * completed. aResult is a boolean indicating the combined result of the
+ * security checks performed.
+ */
+ shouldStoreThumbnail: function (aBrowser, aCallback) {
+ // Don't capture in private browsing mode.
+ if (PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
+ aCallback(false);
+ return;
+ }
+ if (aBrowser.isRemoteBrowser) {
+ let mm = aBrowser.messageManager;
+ let resultFunc = function (aMsg) {
+ mm.removeMessageListener("Browser:Thumbnail:CheckState:Response", resultFunc);
+ aCallback(aMsg.data.result);
+ }
+ mm.addMessageListener("Browser:Thumbnail:CheckState:Response", resultFunc);
+ try {
+ mm.sendAsyncMessage("Browser:Thumbnail:CheckState");
+ } catch (ex) {
+ Cu.reportError(ex);
+ // If the message manager is not able send our message, taking a content
+ // screenshot is also not going to work: return false.
+ resultFunc({ data: { result: false } });
+ }
+ } else {
+ aCallback(PageThumbUtils.shouldStoreContentThumbnail(aBrowser.contentDocument,
+ aBrowser.docShell));
+ }
+ },
+
+ // The background thumbnail service captures to canvas but doesn't want to
+ // participate in this service's telemetry, which is why this method exists.
+ _captureToCanvas: function (aBrowser, aCanvas, aArgs, aCallback) {
+ if (aBrowser.isRemoteBrowser) {
+ Task.spawn(function* () {
+ let data =
+ yield this._captureRemoteThumbnail(aBrowser, aCanvas.width,
+ aCanvas.height, aArgs);
+ let canvas = data.thumbnail;
+ let ctx = canvas.getContext("2d");
+ let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ aCanvas.width = canvas.width;
+ aCanvas.height = canvas.height;
+ aCanvas.getContext("2d").putImageData(imgData, 0, 0);
+ if (aCallback) {
+ aCallback(aCanvas);
+ }
+ }.bind(this));
+ return;
+ }
+ // The content is a local page, grab a thumbnail sync.
+ PageThumbUtils.createSnapshotThumbnail(aBrowser.contentWindow,
+ aCanvas,
+ aArgs);
+
+ if (aCallback) {
+ aCallback(aCanvas);
+ }
+ },
+
+ /**
+ * Asynchrnously render an appropriately scaled thumbnail to canvas.
+ *
+ * @param aBrowser The browser to capture a thumbnail from.
+ * @param aWidth The desired canvas width.
+ * @param aHeight The desired canvas height.
+ * @param aArgs (optional) Additional named parameters:
+ * fullScale - request that a non-downscaled image be returned.
+ * @return a promise
+ */
+ _captureRemoteThumbnail: function (aBrowser, aWidth, aHeight, aArgs) {
+ let deferred = Promise.defer();
+
+ // The index we send with the request so we can identify the
+ // correct response.
+ let index = gRemoteThumbId++;
+
+ // Thumbnail request response handler
+ let mm = aBrowser.messageManager;
+
+ // Browser:Thumbnail:Response handler
+ let thumbFunc = function (aMsg) {
+ // Ignore events unrelated to our request
+ if (aMsg.data.id != index) {
+ return;
+ }
+
+ mm.removeMessageListener("Browser:Thumbnail:Response", thumbFunc);
+ let imageBlob = aMsg.data.thumbnail;
+ let doc = aBrowser.parentElement.ownerDocument;
+ let reader = new FileReader();
+ reader.addEventListener("loadend", function() {
+ let image = doc.createElementNS(PageThumbUtils.HTML_NAMESPACE, "img");
+ image.onload = function () {
+ let thumbnail = doc.createElementNS(PageThumbUtils.HTML_NAMESPACE, "canvas");
+ thumbnail.width = image.naturalWidth;
+ thumbnail.height = image.naturalHeight;
+ let ctx = thumbnail.getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ deferred.resolve({
+ thumbnail: thumbnail
+ });
+ }
+ image.src = reader.result;
+ });
+ // xxx wish there was a way to skip this encoding step
+ reader.readAsDataURL(imageBlob);
+ }
+
+ // Send a thumbnail request
+ mm.addMessageListener("Browser:Thumbnail:Response", thumbFunc);
+ mm.sendAsyncMessage("Browser:Thumbnail:Request", {
+ canvasWidth: aWidth,
+ canvasHeight: aHeight,
+ background: PageThumbUtils.THUMBNAIL_BG_COLOR,
+ id: index,
+ additionalArgs: aArgs
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Captures a thumbnail for the given browser and stores it to the cache.
+ * @param aBrowser The browser to capture a thumbnail for.
+ * @param aCallback The function to be called when finished (optional).
+ */
+ captureAndStore: function PageThumbs_captureAndStore(aBrowser, aCallback) {
+ if (!this._prefEnabled()) {
+ return;
+ }
+
+ let url = aBrowser.currentURI.spec;
+ let originalURL;
+ let channelError = false;
+
+ Task.spawn((function* task() {
+ if (!aBrowser.isRemoteBrowser) {
+ let channel = aBrowser.docShell.currentDocumentChannel;
+ originalURL = channel.originalURI.spec;
+ // see if this was an error response.
+ channelError = PageThumbUtils.isChannelErrorResponse(channel);
+ } else {
+ let resp = yield new Promise(resolve => {
+ let mm = aBrowser.messageManager;
+ let respName = "Browser:Thumbnail:GetOriginalURL:Response";
+ mm.addMessageListener(respName, function onResp(msg) {
+ mm.removeMessageListener(respName, onResp);
+ resolve(msg.data);
+ });
+ mm.sendAsyncMessage("Browser:Thumbnail:GetOriginalURL");
+ });
+ originalURL = resp.originalURL || url;
+ channelError = resp.channelError;
+ }
+
+ let isSuccess = true;
+ try {
+ let blob = yield this.captureToBlob(aBrowser);
+ let buffer = yield TaskUtils.readBlob(blob);
+ yield this._store(originalURL, url, buffer, channelError);
+ } catch (ex) {
+ Components.utils.reportError("Exception thrown during thumbnail capture: '" + ex + "'");
+ isSuccess = false;
+ }
+ if (aCallback) {
+ aCallback(isSuccess);
+ }
+ }).bind(this));
+ },
+
+ /**
+ * Checks if an existing thumbnail for the specified URL is either missing
+ * or stale, and if so, captures and stores it. Once the thumbnail is stored,
+ * an observer service notification will be sent, so consumers should observe
+ * such notifications if they want to be notified of an updated thumbnail.
+ *
+ * @param aBrowser The content window of this browser will be captured.
+ * @param aCallback The function to be called when finished (optional).
+ */
+ captureAndStoreIfStale: function PageThumbs_captureAndStoreIfStale(aBrowser, aCallback) {
+ let url = aBrowser.currentURI.spec;
+ PageThumbsStorage.isFileRecentForURL(url).then(recent => {
+ if (!recent &&
+ // Careful, the call to PageThumbsStorage is async, so the browser may
+ // have navigated away from the URL or even closed.
+ aBrowser.currentURI &&
+ aBrowser.currentURI.spec == url) {
+ this.captureAndStore(aBrowser, aCallback);
+ } else if (aCallback) {
+ aCallback(true);
+ }
+ }, err => {
+ if (aCallback)
+ aCallback(false);
+ });
+ },
+
+ /**
+ * Stores data to disk for the given URLs.
+ *
+ * NB: The background thumbnail service calls this, too.
+ *
+ * @param aOriginalURL The URL with which the capture was initiated.
+ * @param aFinalURL The URL to which aOriginalURL ultimately resolved.
+ * @param aData An ArrayBuffer containing the image data.
+ * @param aNoOverwrite If true and files for the URLs already exist, the files
+ * will not be overwritten.
+ * @return {Promise}
+ */
+ _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData, aNoOverwrite) {
+ return Task.spawn(function* () {
+ let telemetryStoreTime = new Date();
+ yield PageThumbsStorage.writeData(aFinalURL, aData, aNoOverwrite);
+ Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
+ .add(new Date() - telemetryStoreTime);
+
+ Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
+ // We've been redirected. Create a copy of the current thumbnail for
+ // the redirect source. We need to do this because:
+ //
+ // 1) Users can drag any kind of links onto the newtab page. If those
+ // links redirect to a different URL then we want to be able to
+ // provide thumbnails for both of them.
+ //
+ // 2) The newtab page should actually display redirect targets, only.
+ // Because of bug 559175 this information can get lost when using
+ // Sync and therefore also redirect sources appear on the newtab
+ // page. We also want thumbnails for those.
+ if (aFinalURL != aOriginalURL) {
+ yield PageThumbsStorage.copy(aFinalURL, aOriginalURL, aNoOverwrite);
+ Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
+ }
+ });
+ },
+
+ /**
+ * Register an expiration filter.
+ *
+ * When thumbnails are going to expire, each registered filter is asked for a
+ * list of thumbnails to keep.
+ *
+ * The filter (if it is a callable) or its filterForThumbnailExpiration method
+ * (if the filter is an object) is called with a single argument. The
+ * argument is a callback function. The filter must call the callback
+ * function and pass it an array of zero or more URLs. (It may do so
+ * asynchronously.) Thumbnails for those URLs will be except from expiration.
+ *
+ * @param aFilter callable, or object with filterForThumbnailExpiration method
+ */
+ addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) {
+ PageThumbsExpiration.addFilter(aFilter);
+ },
+
+ /**
+ * Unregister an expiration filter.
+ * @param aFilter A filter that was previously passed to addExpirationFilter.
+ */
+ removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) {
+ PageThumbsExpiration.removeFilter(aFilter);
+ },
+
+ /**
+ * Creates a new hidden canvas element.
+ * @param aWindow The document of this window will be used to create the
+ * canvas. If not given, the hidden window will be used.
+ * @return The newly created canvas.
+ */
+ createCanvas: function PageThumbs_createCanvas(aWindow) {
+ return PageThumbUtils.createCanvas(aWindow);
+ },
+
+ _prefEnabled: function PageThumbs_prefEnabled() {
+ try {
+ return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
+ }
+ catch (e) {
+ return true;
+ }
+ },
+};
+
+this.PageThumbsStorage = {
+ // The path for the storage
+ _path: null,
+ get path() {
+ if (!this._path) {
+ this._path = OS.Path.join(OS.Constants.Path.localProfileDir, THUMBNAIL_DIRECTORY);
+ }
+ return this._path;
+ },
+
+ ensurePath: function Storage_ensurePath() {
+ // Create the directory (ignore any error if the directory
+ // already exists). As all writes are done from the PageThumbsWorker
+ // thread, which serializes its operations, this ensures that
+ // future operations can proceed without having to check whether
+ // the directory exists.
+ return PageThumbsWorker.post("makeDir",
+ [this.path, {ignoreExisting: true}]).then(
+ null,
+ function onError(aReason) {
+ Components.utils.reportError("Could not create thumbnails directory" + aReason);
+ });
+ },
+
+ getLeafNameForURL: function Storage_getLeafNameForURL(aURL) {
+ if (typeof aURL != "string") {
+ throw new TypeError("Expecting a string");
+ }
+ let hash = this._calculateMD5Hash(aURL);
+ return hash + ".png";
+ },
+
+ getFilePathForURL: function Storage_getFilePathForURL(aURL) {
+ return OS.Path.join(this.path, this.getLeafNameForURL(aURL));
+ },
+
+ _revisionTable: {},
+
+ // Generate an arbitrary revision tag, i.e. one that can't be used to
+ // infer URL frecency.
+ _updateRevision(aURL) {
+ // Initialize with a random value and increment on each update. Wrap around
+ // modulo _revisionRange, so that even small values carry no meaning.
+ let rev = this._revisionTable[aURL];
+ if (rev == null)
+ rev = Math.floor(Math.random() * this._revisionRange);
+ this._revisionTable[aURL] = (rev + 1) % this._revisionRange;
+ },
+
+ // If two thumbnails with the same URL and revision are in cache at the
+ // same time, the image loader may pick the stale thumbnail in some cases.
+ // Therefore _revisionRange must be large enough to prevent this, e.g.
+ // in the pathological case image.cache.size (5MB by default) could fill
+ // with (abnormally small) 10KB thumbnail images if the browser session
+ // runs long enough (though this is unlikely as thumbnails are usually
+ // only updated every MAX_THUMBNAIL_AGE_SECS).
+ _revisionRange: 8192,
+
+ /**
+ * Return a revision tag for the thumbnail stored for a given URL.
+ *
+ * @param aURL The URL spec string
+ * @return A revision tag for the corresponding thumbnail. Returns a changed
+ * value whenever the stored thumbnail changes.
+ */
+ getRevision(aURL) {
+ let rev = this._revisionTable[aURL];
+ if (rev == null) {
+ this._updateRevision(aURL);
+ rev = this._revisionTable[aURL];
+ }
+ return rev;
+ },
+
+ /**
+ * Write the contents of a thumbnail, off the main thread.
+ *
+ * @param {string} aURL The url for which to store a thumbnail.
+ * @param {ArrayBuffer} aData The data to store in the thumbnail, as
+ * an ArrayBuffer. This array buffer will be detached and cannot be
+ * reused after the copy.
+ * @param {boolean} aNoOverwrite If true and the thumbnail's file already
+ * exists, the file will not be overwritten.
+ *
+ * @return {Promise}
+ */
+ writeData: function Storage_writeData(aURL, aData, aNoOverwrite) {
+ let path = this.getFilePathForURL(aURL);
+ this.ensurePath();
+ aData = new Uint8Array(aData);
+ let msg = [
+ path,
+ aData,
+ {
+ tmpPath: path + ".tmp",
+ bytes: aData.byteLength,
+ noOverwrite: aNoOverwrite,
+ flush: false /* thumbnails do not require the level of guarantee provided by flush*/
+ }];
+ return PageThumbsWorker.post("writeAtomic", msg,
+ msg /* we don't want that message garbage-collected,
+ as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
+ memory tricks to enforce zero-copy*/).
+ then(() => this._updateRevision(aURL), this._eatNoOverwriteError(aNoOverwrite));
+ },
+
+ /**
+ * Copy a thumbnail, off the main thread.
+ *
+ * @param {string} aSourceURL The url of the thumbnail to copy.
+ * @param {string} aTargetURL The url of the target thumbnail.
+ * @param {boolean} aNoOverwrite If true and the target file already exists,
+ * the file will not be overwritten.
+ *
+ * @return {Promise}
+ */
+ copy: function Storage_copy(aSourceURL, aTargetURL, aNoOverwrite) {
+ this.ensurePath();
+ let sourceFile = this.getFilePathForURL(aSourceURL);
+ let targetFile = this.getFilePathForURL(aTargetURL);
+ let options = { noOverwrite: aNoOverwrite };
+ return PageThumbsWorker.post("copy", [sourceFile, targetFile, options]).
+ then(() => this._updateRevision(aTargetURL), this._eatNoOverwriteError(aNoOverwrite));
+ },
+
+ /**
+ * Remove a single thumbnail, off the main thread.
+ *
+ * @return {Promise}
+ */
+ remove: function Storage_remove(aURL) {
+ return PageThumbsWorker.post("remove", [this.getFilePathForURL(aURL)]);
+ },
+
+ /**
+ * Remove all thumbnails, off the main thread.
+ *
+ * @return {Promise}
+ */
+ wipe: Task.async(function* Storage_wipe() {
+ //
+ // This operation may be launched during shutdown, so we need to
+ // take a few precautions to ensure that:
+ //
+ // 1. it is not interrupted by shutdown, in which case we
+ // could be leaving privacy-sensitive files on disk;
+ // 2. it is not launched too late during shutdown, in which
+ // case this could cause shutdown freezes (see bug 1005487,
+ // which will eventually be fixed by bug 965309)
+ //
+
+ let blocker = () => promise;
+
+ // The following operation will rise an error if we have already
+ // reached profileBeforeChange, in which case it is too late
+ // to clear the thumbnail wipe.
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "PageThumbs: removing all thumbnails",
+ blocker);
+
+ // Start the work only now that `profileBeforeChange` has had
+ // a chance to throw an error.
+
+ let promise = PageThumbsWorker.post("wipe", [this.path]);
+ try {
+ yield promise;
+ } finally {
+ // Generally, we will be done much before profileBeforeChange,
+ // so let's not hoard blockers.
+ if ("removeBlocker" in AsyncShutdown.profileBeforeChange) {
+ // `removeBlocker` was added with bug 985655. In the interest
+ // of backporting, let's degrade gracefully if `removeBlocker`
+ // doesn't exist.
+ AsyncShutdown.profileBeforeChange.removeBlocker(blocker);
+ }
+ }
+ }),
+
+ fileExistsForURL: function Storage_fileExistsForURL(aURL) {
+ return PageThumbsWorker.post("exists", [this.getFilePathForURL(aURL)]);
+ },
+
+ isFileRecentForURL: function Storage_isFileRecentForURL(aURL) {
+ return PageThumbsWorker.post("isFileRecent",
+ [this.getFilePathForURL(aURL),
+ MAX_THUMBNAIL_AGE_SECS]);
+ },
+
+ _calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
+ let hash = gCryptoHash;
+ let value = gUnicodeConverter.convertToByteArray(aValue);
+
+ hash.init(hash.MD5);
+ hash.update(value, value.length);
+ return this._convertToHexString(hash.finish(false));
+ },
+
+ _convertToHexString: function Storage_convertToHexString(aData) {
+ let hex = "";
+ for (let i = 0; i < aData.length; i++)
+ hex += ("0" + aData.charCodeAt(i).toString(16)).slice(-2);
+ return hex;
+ },
+
+ /**
+ * For functions that take a noOverwrite option, OS.File throws an error if
+ * the target file exists and noOverwrite is true. We don't consider that an
+ * error, and we don't want such errors propagated.
+ *
+ * @param {aNoOverwrite} The noOverwrite option used in the OS.File operation.
+ *
+ * @return {function} A function that should be passed as the second argument
+ * to then() (the `onError` argument).
+ */
+ _eatNoOverwriteError: function Storage__eatNoOverwriteError(aNoOverwrite) {
+ return function onError(err) {
+ if (!aNoOverwrite ||
+ !(err instanceof OS.File.Error) ||
+ !err.becauseExists) {
+ throw err;
+ }
+ };
+ },
+
+ // Deprecated, please do not use
+ getFileForURL: function Storage_getFileForURL_DEPRECATED(aURL) {
+ Deprecated.warning("PageThumbs.getFileForURL is deprecated. Please use PageThumbs.getFilePathForURL and OS.File",
+ "https://developer.mozilla.org/docs/JavaScript_OS.File");
+ // Note: Once this method has been removed, we can get rid of the dependency towards FileUtils
+ return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
+ }
+};
+
+var PageThumbsStorageMigrator = {
+ get currentVersion() {
+ try {
+ return Services.prefs.getIntPref(PREF_STORAGE_VERSION);
+ } catch (e) {
+ // The pref doesn't exist, yet. Return version 0.
+ return 0;
+ }
+ },
+
+ set currentVersion(aVersion) {
+ Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion);
+ },
+
+ migrate: function Migrator_migrate() {
+ let version = this.currentVersion;
+
+ // Storage version 1 never made it to beta.
+ // At the time of writing only Windows had (ProfD != ProfLD) and we
+ // needed to move thumbnails from the roaming profile to the locale
+ // one so that they're not needlessly included in backups and/or
+ // written via SMB.
+
+ // Storage version 2 also never made it to beta.
+ // The thumbnail folder structure has been changed and old thumbnails
+ // were not migrated. Instead, we just renamed the current folder to
+ // "<name>-old" and will remove it later.
+
+ if (version < 3) {
+ this.migrateToVersion3();
+ }
+
+ this.currentVersion = LATEST_STORAGE_VERSION;
+ },
+
+ /**
+ * Bug 239254 added support for having the disk cache and thumbnail
+ * directories on a local path (i.e. ~/.cache/) under Linux. We'll first
+ * try to move the old thumbnails to their new location. If that's not
+ * possible (because ProfD might be on a different file system than
+ * ProfLD) we'll just discard them.
+ *
+ * @param {string*} local The path to the local profile directory.
+ * Used for testing. Default argument is good for all non-testing uses.
+ * @param {string*} roaming The path to the roaming profile directory.
+ * Used for testing. Default argument is good for all non-testing uses.
+ */
+ migrateToVersion3: function Migrator_migrateToVersion3(
+ local = OS.Constants.Path.localProfileDir,
+ roaming = OS.Constants.Path.profileDir) {
+ PageThumbsWorker.post(
+ "moveOrDeleteAllThumbnails",
+ [OS.Path.join(roaming, THUMBNAIL_DIRECTORY),
+ OS.Path.join(local, THUMBNAIL_DIRECTORY)]
+ );
+ }
+};
+
+var PageThumbsExpiration = {
+ _filters: [],
+
+ init: function Expiration_init() {
+ gUpdateTimerManager.registerTimer("browser-cleanup-thumbnails", this,
+ EXPIRATION_INTERVAL_SECS);
+ },
+
+ addFilter: function Expiration_addFilter(aFilter) {
+ this._filters.push(aFilter);
+ },
+
+ removeFilter: function Expiration_removeFilter(aFilter) {
+ let index = this._filters.indexOf(aFilter);
+ if (index > -1)
+ this._filters.splice(index, 1);
+ },
+
+ notify: function Expiration_notify(aTimer) {
+ let urls = [];
+ let filtersToWaitFor = this._filters.length;
+
+ let expire = function expire() {
+ this.expireThumbnails(urls);
+ }.bind(this);
+
+ // No registered filters.
+ if (!filtersToWaitFor) {
+ expire();
+ return;
+ }
+
+ function filterCallback(aURLs) {
+ urls = urls.concat(aURLs);
+ if (--filtersToWaitFor == 0)
+ expire();
+ }
+
+ for (let filter of this._filters) {
+ if (typeof filter == "function")
+ filter(filterCallback)
+ else
+ filter.filterForThumbnailExpiration(filterCallback);
+ }
+ },
+
+ expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
+ let keep = aURLsToKeep.map(url => PageThumbsStorage.getLeafNameForURL(url));
+ let msg = [
+ PageThumbsStorage.path,
+ keep,
+ EXPIRATION_MIN_CHUNK_SIZE
+ ];
+
+ return PageThumbsWorker.post(
+ "expireFilesInDirectory",
+ msg
+ );
+ }
+};
+
+/**
+ * Interface to a dedicated thread handling I/O
+ */
+var PageThumbsWorker = new BasePromiseWorker("resource://gre/modules/PageThumbsWorker.js");
+// As the PageThumbsWorker performs I/O, we can receive instances of
+// OS.File.Error, so we need to install a decoder.
+PageThumbsWorker.ExceptionHandlers["OS.File.Error"] = OS.File.Error.fromMsg;
+
+var PageThumbsHistoryObserver = {
+ onDeleteURI(aURI, aGUID) {
+ PageThumbsStorage.remove(aURI.spec);
+ },
+
+ onClearHistory() {
+ PageThumbsStorage.wipe();
+ },
+
+ onTitleChanged: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
+ Ci.nsISupportsWeakReference])
+};
diff --git a/toolkit/components/thumbnails/PageThumbsProtocol.js b/toolkit/components/thumbnails/PageThumbsProtocol.js
new file mode 100644
index 0000000000..41dfe96be1
--- /dev/null
+++ b/toolkit/components/thumbnails/PageThumbsProtocol.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * PageThumbsProtocol.js
+ *
+ * This file implements the moz-page-thumb:// protocol and the corresponding
+ * channel delivering cached thumbnails.
+ *
+ * URL structure:
+ *
+ * moz-page-thumb://thumbnail/?url=http%3A%2F%2Fwww.mozilla.org%2F&revision=XX
+ *
+ * This URL requests an image for 'http://www.mozilla.org/'.
+ * The value of the revision key may change when the stored thumbnail changes.
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const SUBSTITUTING_URL_CID = "{dea9657c-18cf-4984-bde9-ccef5d8ab473}";
+
+/**
+ * Implements the thumbnail protocol handler responsible for moz-page-thumb: URLs.
+ */
+function Protocol() {
+}
+
+Protocol.prototype = {
+ /**
+ * The scheme used by this protocol.
+ */
+ get scheme() {
+ return PageThumbs.scheme;
+ },
+
+ /**
+ * The default port for this protocol (we don't support ports).
+ */
+ get defaultPort() {
+ return -1;
+ },
+
+ /**
+ * The flags specific to this protocol implementation.
+ */
+ get protocolFlags() {
+ return Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE |
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_NOAUTH;
+ },
+
+ /**
+ * Creates a new URI object that is suitable for loading by this protocol.
+ * @param aSpec The URI string in UTF8 encoding.
+ * @param aOriginCharset The charset of the document from which the URI originated.
+ * @return The newly created URI.
+ */
+ newURI: function Proto_newURI(aSpec, aOriginCharset) {
+ let uri = Components.classesByID[SUBSTITUTING_URL_CID].createInstance(Ci.nsIURL);
+ uri.spec = aSpec;
+ return uri;
+ },
+
+ /**
+ * Constructs a new channel from the given URI for this protocol handler.
+ * @param aURI The URI for which to construct a channel.
+ * @param aLoadInfo The Loadinfo which to use on the channel.
+ * @return The newly created channel.
+ */
+ newChannel2: function Proto_newChannel2(aURI, aLoadInfo) {
+ let {file} = aURI.QueryInterface(Ci.nsIFileURL);
+ let fileuri = Services.io.newFileURI(file);
+ let channel = Services.io.newChannelFromURIWithLoadInfo(fileuri, aLoadInfo);
+ channel.originalURI = aURI;
+ return channel;
+ },
+
+ newChannel: function Proto_newChannel(aURI) {
+ return this.newChannel2(aURI, null);
+ },
+
+ /**
+ * Decides whether to allow a blacklisted port.
+ * @return Always false, we'll never allow ports.
+ */
+ allowPort: () => false,
+
+ // nsISubstitutingProtocolHandler methods
+
+ /*
+ * Substituting the scheme and host isn't enough, we also transform the path.
+ * So declare no-op implementations for (get|set|has)Substitution methods and
+ * do all the work in resolveURI.
+ */
+
+ setSubstitution(root, baseURI) {},
+
+ getSubstitution(root) {
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+ },
+
+ hasSubstitution(root) {
+ return false;
+ },
+
+ resolveURI(resURI) {
+ let {url} = parseURI(resURI);
+ let path = PageThumbsStorage.getFilePathForURL(url);
+ return OS.Path.toFileURI(path);
+ },
+
+ // xpcom machinery
+ classID: Components.ID("{5a4ae9b5-f475-48ae-9dce-0b4c1d347884}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler,
+ Ci.nsISubstitutingProtocolHandler])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Protocol]);
+
+/**
+ * Parses a given URI and extracts all parameters relevant to this protocol.
+ * @param aURI The URI to parse.
+ * @return The parsed parameters.
+ */
+function parseURI(aURI) {
+ if (aURI.host != PageThumbs.staticHost)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let {query} = aURI.QueryInterface(Ci.nsIURL);
+ let params = {};
+
+ query.split("&").forEach(function (aParam) {
+ let [key, value] = aParam.split("=").map(decodeURIComponent);
+ params[key.toLowerCase()] = value;
+ });
+
+ return params;
+}
diff --git a/toolkit/components/thumbnails/PageThumbsWorker.js b/toolkit/components/thumbnails/PageThumbsWorker.js
new file mode 100644
index 0000000000..83171c91f4
--- /dev/null
+++ b/toolkit/components/thumbnails/PageThumbsWorker.js
@@ -0,0 +1,176 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * A worker dedicated for the I/O component of PageThumbs storage.
+ *
+ * Do not rely on the API of this worker. In a future version, it might be
+ * fully replaced by a OS.File global I/O worker.
+ */
+
+"use strict";
+
+importScripts("resource://gre/modules/osfile.jsm");
+
+var PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+var File = OS.File;
+var Type = OS.Shared.Type;
+
+var worker = new PromiseWorker.AbstractWorker();
+worker.dispatch = function(method, args = []) {
+ return Agent[method](...args);
+};
+worker.postMessage = function(message, ...transfers) {
+ self.postMessage(message, ...transfers);
+};
+worker.close = function() {
+ self.close();
+};
+
+self.addEventListener("message", msg => worker.handleMessage(msg));
+
+
+var Agent = {
+ // Checks if the specified file exists and has an age less than as
+ // specifed (in seconds).
+ isFileRecent: function Agent_isFileRecent(path, maxAge) {
+ try {
+ let stat = OS.File.stat(path);
+ let maxDate = new Date();
+ maxDate.setSeconds(maxDate.getSeconds() - maxAge);
+ return stat.lastModificationDate > maxDate;
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error)) {
+ throw ex;
+ }
+ // file doesn't exist (or can't be stat'd) - must be stale.
+ return false;
+ }
+ },
+
+ remove: function Agent_removeFile(path) {
+ try {
+ OS.File.remove(path);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ expireFilesInDirectory:
+ function Agent_expireFilesInDirectory(path, filesToKeep, minChunkSize) {
+ let entries = this.getFileEntriesInDirectory(path, filesToKeep);
+ let limit = Math.max(minChunkSize, Math.round(entries.length / 2));
+
+ for (let entry of entries) {
+ this.remove(entry.path);
+
+ // Check if we reached the limit of files to remove.
+ if (--limit <= 0) {
+ break;
+ }
+ }
+
+ return true;
+ },
+
+ getFileEntriesInDirectory:
+ function Agent_getFileEntriesInDirectory(path, skipFiles) {
+ let iter = new OS.File.DirectoryIterator(path);
+ try {
+ if (!iter.exists()) {
+ return [];
+ }
+
+ let skip = new Set(skipFiles);
+
+ let entries = [];
+ for (let entry in iter) {
+ if (!entry.isDir && !entry.isSymLink && !skip.has(entry.name)) {
+ entries.push(entry);
+ }
+ }
+ return entries;
+ } finally {
+ iter.close();
+ }
+ },
+
+ moveOrDeleteAllThumbnails:
+ function Agent_moveOrDeleteAllThumbnails(pathFrom, pathTo) {
+ OS.File.makeDir(pathTo, {ignoreExisting: true});
+ if (pathFrom == pathTo) {
+ return true;
+ }
+ let iter = new OS.File.DirectoryIterator(pathFrom);
+ if (iter.exists()) {
+ for (let entry in iter) {
+ if (entry.isDir || entry.isSymLink) {
+ continue;
+ }
+
+
+ let from = OS.Path.join(pathFrom, entry.name);
+ let to = OS.Path.join(pathTo, entry.name);
+
+ try {
+ OS.File.move(from, to, {noOverwrite: true, noCopy: true});
+ } catch (e) {
+ OS.File.remove(from);
+ }
+ }
+ }
+ iter.close();
+
+ try {
+ OS.File.removeEmptyDir(pathFrom);
+ } catch (e) {
+ // This could fail if there's something in
+ // the folder we're not permitted to remove.
+ }
+
+ return true;
+ },
+
+ writeAtomic: function Agent_writeAtomic(path, buffer, options) {
+ return File.writeAtomic(path,
+ buffer,
+ options);
+ },
+
+ makeDir: function Agent_makeDir(path, options) {
+ return File.makeDir(path, options);
+ },
+
+ copy: function Agent_copy(source, dest, options) {
+ return File.copy(source, dest, options);
+ },
+
+ wipe: function Agent_wipe(path) {
+ let iterator = new File.DirectoryIterator(path);
+ try {
+ for (let entry in iterator) {
+ try {
+ File.remove(entry.path);
+ } catch (ex) {
+ // If a file cannot be removed, we should still continue.
+ // This can happen at least for any of the following reasons:
+ // - access denied;
+ // - file has been removed recently during a previous wipe
+ // and the file system has not flushed that yet (yes, this
+ // can happen under Windows);
+ // - file has been removed by the user or another process.
+ }
+ }
+ } finally {
+ iterator.close();
+ }
+ },
+
+ exists: function Agent_exists(path) {
+ return File.exists(path);
+ },
+};
+
diff --git a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
new file mode 100644
index 0000000000..2103833b7b
--- /dev/null
+++ b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.importGlobalProperties(['Blob', 'FileReader']);
+
+Cu.import("resource://gre/modules/PageThumbUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const STATE_LOADING = 1;
+const STATE_CAPTURING = 2;
+const STATE_CANCELED = 3;
+
+// NOTE: Copied from nsSandboxFlags.h
+/**
+ * This flag prevents content from creating new auxiliary browsing contexts,
+ * e.g. using the target attribute, the window.open() method, or the
+ * showModalDialog() method.
+ */
+const SANDBOXED_AUXILIARY_NAVIGATION = 0x2;
+
+const backgroundPageThumbsContent = {
+
+ init: function () {
+ Services.obs.addObserver(this, "document-element-inserted", true);
+
+ // We want a low network priority for this service - lower than b/g tabs
+ // etc - so set it to the lowest priority available.
+ this._webNav.QueryInterface(Ci.nsIDocumentLoader).
+ loadGroup.QueryInterface(Ci.nsISupportsPriority).
+ priority = Ci.nsISupportsPriority.PRIORITY_LOWEST;
+
+ docShell.allowMedia = false;
+ docShell.allowPlugins = false;
+ docShell.allowContentRetargeting = false;
+ let defaultFlags = Ci.nsIRequest.LOAD_ANONYMOUS |
+ Ci.nsIRequest.LOAD_BYPASS_CACHE |
+ Ci.nsIRequest.INHIBIT_CACHING |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY;
+ docShell.defaultLoadFlags = defaultFlags;
+ docShell.sandboxFlags |= SANDBOXED_AUXILIARY_NAVIGATION;
+
+ addMessageListener("BackgroundPageThumbs:capture",
+ this._onCapture.bind(this));
+ docShell.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebProgress).
+ addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
+ },
+
+ observe: function (subj, topic, data) {
+ // Arrange to prevent (most) popup dialogs for this window - popups done
+ // in the parent (eg, auth) aren't prevented, but alert() etc are.
+ // disableDialogs only works on the current inner window, so it has
+ // to be called every page load, but before scripts run.
+ if (content && subj == content.document) {
+ content.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).
+ disableDialogs();
+ }
+ },
+
+ get _webNav() {
+ return docShell.QueryInterface(Ci.nsIWebNavigation);
+ },
+
+ _onCapture: function (msg) {
+ this._nextCapture = {
+ id: msg.data.id,
+ url: msg.data.url,
+ };
+ if (this._currentCapture) {
+ if (this._state == STATE_LOADING) {
+ // Cancel the current capture.
+ this._state = STATE_CANCELED;
+ this._loadAboutBlank();
+ }
+ // Let the current capture finish capturing, or if it was just canceled,
+ // wait for onStateChange due to the about:blank load.
+ return;
+ }
+ this._startNextCapture();
+ },
+
+ _startNextCapture: function () {
+ if (!this._nextCapture)
+ return;
+ this._currentCapture = this._nextCapture;
+ delete this._nextCapture;
+ this._state = STATE_LOADING;
+ this._currentCapture.pageLoadStartDate = new Date();
+
+ try {
+ this._webNav.loadURI(this._currentCapture.url,
+ Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT,
+ null, null, null);
+ } catch (e) {
+ this._failCurrentCapture("BAD_URI");
+ delete this._currentCapture;
+ this._startNextCapture();
+ }
+ },
+
+ onStateChange: function (webProgress, req, flags, status) {
+ if (webProgress.isTopLevel &&
+ (flags & Ci.nsIWebProgressListener.STATE_STOP) &&
+ this._currentCapture) {
+ if (req.name == "about:blank") {
+ if (this._state == STATE_CAPTURING) {
+ // about:blank has loaded, ending the current capture.
+ this._finishCurrentCapture();
+ delete this._currentCapture;
+ this._startNextCapture();
+ }
+ else if (this._state == STATE_CANCELED) {
+ delete this._currentCapture;
+ this._startNextCapture();
+ }
+ }
+ else if (this._state == STATE_LOADING &&
+ Components.isSuccessCode(status)) {
+ // The requested page has loaded. Capture it.
+ this._state = STATE_CAPTURING;
+ this._captureCurrentPage();
+ }
+ else if (this._state != STATE_CANCELED) {
+ // Something went wrong. Cancel the capture. Loading about:blank
+ // while onStateChange is still on the stack does not actually stop
+ // the request if it redirects, so do it asyncly.
+ this._state = STATE_CANCELED;
+ if (!this._cancelTimer) {
+ this._cancelTimer =
+ Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._cancelTimer.init(() => {
+ this._loadAboutBlank();
+ delete this._cancelTimer;
+ }, 0, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ }
+ }
+ },
+
+ _captureCurrentPage: function () {
+ let capture = this._currentCapture;
+ capture.finalURL = this._webNav.currentURI.spec;
+ capture.pageLoadTime = new Date() - capture.pageLoadStartDate;
+
+ let canvasDrawDate = new Date();
+
+ let finalCanvas = PageThumbUtils.createSnapshotThumbnail(content, null);
+ capture.canvasDrawTime = new Date() - canvasDrawDate;
+
+ finalCanvas.toBlob(blob => {
+ capture.imageBlob = new Blob([blob]);
+ // Load about:blank to finish the capture and wait for onStateChange.
+ this._loadAboutBlank();
+ });
+ },
+
+ _finishCurrentCapture: function () {
+ let capture = this._currentCapture;
+ let fileReader = new FileReader();
+ fileReader.onloadend = () => {
+ sendAsyncMessage("BackgroundPageThumbs:didCapture", {
+ id: capture.id,
+ imageData: fileReader.result,
+ finalURL: capture.finalURL,
+ telemetry: {
+ CAPTURE_PAGE_LOAD_TIME_MS: capture.pageLoadTime,
+ CAPTURE_CANVAS_DRAW_TIME_MS: capture.canvasDrawTime,
+ },
+ });
+ };
+ fileReader.readAsArrayBuffer(capture.imageBlob);
+ },
+
+ _failCurrentCapture: function (reason) {
+ let capture = this._currentCapture;
+ sendAsyncMessage("BackgroundPageThumbs:didCapture", {
+ id: capture.id,
+ failReason: reason,
+ });
+ },
+
+ // We load about:blank to finish all captures, even canceled captures. Two
+ // reasons: GC the captured page, and ensure it can't possibly load any more
+ // resources.
+ _loadAboutBlank: function _loadAboutBlank() {
+ this._webNav.loadURI("about:blank",
+ Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT,
+ null, null, null);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIObserver,
+ ]),
+};
+
+backgroundPageThumbsContent.init();
diff --git a/toolkit/components/thumbnails/jar.mn b/toolkit/components/thumbnails/jar.mn
new file mode 100644
index 0000000000..c83c64e48c
--- /dev/null
+++ b/toolkit/components/thumbnails/jar.mn
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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/backgroundPageThumbsContent.js (content/backgroundPageThumbsContent.js)
diff --git a/toolkit/components/thumbnails/moz.build b/toolkit/components/thumbnails/moz.build
new file mode 100644
index 0000000000..9bc218b6a0
--- /dev/null
+++ b/toolkit/components/thumbnails/moz.build
@@ -0,0 +1,22 @@
+# -*- 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 += ['test/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
+
+EXTRA_COMPONENTS += [
+ 'BrowserPageThumbs.manifest',
+ 'PageThumbsProtocol.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'BackgroundPageThumbs.jsm',
+ 'PageThumbs.jsm',
+ 'PageThumbsWorker.js',
+ 'PageThumbUtils.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/toolkit/components/thumbnails/test/.eslintrc.js b/toolkit/components/thumbnails/test/.eslintrc.js
new file mode 100644
index 0000000000..f6f8d62c24
--- /dev/null
+++ b/toolkit/components/thumbnails/test/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/browser.eslintrc.js",
+ "../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/thumbnails/test/authenticate.sjs b/toolkit/components/thumbnails/test/authenticate.sjs
new file mode 100644
index 0000000000..58da655cf9
--- /dev/null
+++ b/toolkit/components/thumbnails/test/authenticate.sjs
@@ -0,0 +1,220 @@
+function handleRequest(request, response)
+{
+ try {
+ reallyHandleRequest(request, response);
+ } catch (e) {
+ response.setStatusLine("1.0", 200, "AlmostOK");
+ response.write("Error handling request: " + e);
+ }
+}
+
+
+function reallyHandleRequest(request, response) {
+ var match;
+ var requestAuth = true, requestProxyAuth = true;
+
+ // Allow the caller to drive how authentication is processed via the query.
+ // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar
+ // The extra ? allows the user/pass/realm checks to succeed if the name is
+ // at the beginning of the query string.
+ var query = "?" + request.queryString;
+
+ var expected_user = "", expected_pass = "", realm = "mochitest";
+ var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy";
+ var huge = false, plugin = false, anonymous = false;
+ var authHeaderCount = 1;
+ // user=xxx
+ match = /[^_]user=([^&]*)/.exec(query);
+ if (match)
+ expected_user = match[1];
+
+ // pass=xxx
+ match = /[^_]pass=([^&]*)/.exec(query);
+ if (match)
+ expected_pass = match[1];
+
+ // realm=xxx
+ match = /[^_]realm=([^&]*)/.exec(query);
+ if (match)
+ realm = match[1];
+
+ // proxy_user=xxx
+ match = /proxy_user=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_user = match[1];
+
+ // proxy_pass=xxx
+ match = /proxy_pass=([^&]*)/.exec(query);
+ if (match)
+ proxy_expected_pass = match[1];
+
+ // proxy_realm=xxx
+ match = /proxy_realm=([^&]*)/.exec(query);
+ if (match)
+ proxy_realm = match[1];
+
+ // huge=1
+ match = /huge=1/.exec(query);
+ if (match)
+ huge = true;
+
+ // plugin=1
+ match = /plugin=1/.exec(query);
+ if (match)
+ plugin = true;
+
+ // multiple=1
+ match = /multiple=([^&]*)/.exec(query);
+ if (match)
+ authHeaderCount = match[1]+0;
+
+ // anonymous=1
+ match = /anonymous=1/.exec(query);
+ if (match)
+ anonymous = true;
+
+ // Look for an authentication header, if any, in the request.
+ //
+ // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+ //
+ // This test only supports Basic auth. The value sent by the client is
+ // "username:password", obscured with base64 encoding.
+
+ var actual_user = "", actual_pass = "", authHeader, authPresent = false;
+ if (request.hasHeader("Authorization")) {
+ authPresent = true;
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw "Couldn't parse auth header: " + authHeader;
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw "Couldn't decode auth header: " + userpass;
+ actual_user = match[1];
+ actual_pass = match[2];
+ }
+
+ var proxy_actual_user = "", proxy_actual_pass = "";
+ if (request.hasHeader("Proxy-Authorization")) {
+ authHeader = request.getHeader("Proxy-Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2)
+ throw "Couldn't parse auth header: " + authHeader;
+
+ var userpass = base64ToString(match[1]); // no atob() :-(
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3)
+ throw "Couldn't decode auth header: " + userpass;
+ proxy_actual_user = match[1];
+ proxy_actual_pass = match[2];
+ }
+
+ // Don't request authentication if the credentials we got were what we
+ // expected.
+ if (expected_user == actual_user &&
+ expected_pass == actual_pass) {
+ requestAuth = false;
+ }
+ if (proxy_expected_user == proxy_actual_user &&
+ proxy_expected_pass == proxy_actual_pass) {
+ requestProxyAuth = false;
+ }
+
+ if (anonymous) {
+ if (authPresent) {
+ response.setStatusLine("1.0", 400, "Unexpected authorization header found");
+ } else {
+ response.setStatusLine("1.0", 200, "Authorization header not found");
+ }
+ } else {
+ if (requestProxyAuth) {
+ response.setStatusLine("1.0", 407, "Proxy authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true);
+ } else if (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ for (i = 0; i < authHeaderCount; ++i)
+ response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true);
+ } else {
+ response.setStatusLine("1.0", 200, "OK");
+ }
+ }
+
+ response.setHeader("Content-Type", "application/xhtml+xml", false);
+ response.write("<html xmlns='http://www.w3.org/1999/xhtml'>");
+ response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n");
+ response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n");
+ response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n");
+ response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n");
+
+ if (huge) {
+ response.write("<div style='display: none'>");
+ for (i = 0; i < 100000; i++) {
+ response.write("123456789\n");
+ }
+ response.write("</div>");
+ response.write("<span id='footnote'>This is a footnote after the huge content fill</span>");
+ }
+
+ if (plugin) {
+ response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " +
+ "type='application/x-test'></embed>\n");
+ }
+
+ response.write("</html>");
+}
+
+
+// base64 decoder
+//
+// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa()
+// doesn't seem to exist. :-(
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/thumbnails/test/background_red.html b/toolkit/components/thumbnails/test/background_red.html
new file mode 100644
index 0000000000..95159dd297
--- /dev/null
+++ b/toolkit/components/thumbnails/test/background_red.html
@@ -0,0 +1,3 @@
+<html>
+ <body bgcolor=ff0000></body>
+</html>
diff --git a/toolkit/components/thumbnails/test/background_red_redirect.sjs b/toolkit/components/thumbnails/test/background_red_redirect.sjs
new file mode 100644
index 0000000000..5f0852e194
--- /dev/null
+++ b/toolkit/components/thumbnails/test/background_red_redirect.sjs
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(aRequest, aResponse) {
+ // Set HTTP Status.
+ aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently");
+
+ // Set redirect URI.
+ aResponse.setHeader("Location", "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/background_red.html");
+}
diff --git a/toolkit/components/thumbnails/test/background_red_scroll.html b/toolkit/components/thumbnails/test/background_red_scroll.html
new file mode 100644
index 0000000000..1e30bd3c67
--- /dev/null
+++ b/toolkit/components/thumbnails/test/background_red_scroll.html
@@ -0,0 +1,3 @@
+<html>
+ <body bgcolor=ff0000 style="overflow: scroll;"></body>
+</html>
diff --git a/toolkit/components/thumbnails/test/browser.ini b/toolkit/components/thumbnails/test/browser.ini
new file mode 100644
index 0000000000..3b87815ff0
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+support-files =
+ authenticate.sjs
+ background_red.html
+ background_red_redirect.sjs
+ background_red_scroll.html
+ head.js
+ privacy_cache_control.sjs
+ thumbnails_background.sjs
+ thumbnails_crash_content_helper.js
+ thumbnails_update.sjs
+
+[browser_thumbnails_bg_bad_url.js]
+[browser_thumbnails_bg_crash_during_capture.js]
+skip-if = !crashreporter
+[browser_thumbnails_bg_crash_while_idle.js]
+skip-if = !crashreporter
+[browser_thumbnails_bg_basic.js]
+[browser_thumbnails_bg_queueing.js]
+[browser_thumbnails_bg_timeout.js]
+[browser_thumbnails_bg_redirect.js]
+[browser_thumbnails_bg_destroy_browser.js]
+[browser_thumbnails_bg_no_cookies_sent.js]
+[browser_thumbnails_bg_no_cookies_stored.js]
+[browser_thumbnails_bg_no_auth_prompt.js]
+[browser_thumbnails_bg_no_alert.js]
+[browser_thumbnails_bg_no_duplicates.js]
+[browser_thumbnails_bg_captureIfMissing.js]
+[browser_thumbnails_bug726727.js]
+[browser_thumbnails_bug727765.js]
+[browser_thumbnails_bug818225.js]
+[browser_thumbnails_capture.js]
+[browser_thumbnails_expiration.js]
+[browser_thumbnails_privacy.js]
+[browser_thumbnails_redirect.js]
+[browser_thumbnails_storage.js]
+[browser_thumbnails_storage_migrate3.js]
+[browser_thumbnails_update.js]
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_bad_url.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_bad_url.js
new file mode 100644
index 0000000000..df8ef8d965
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_bad_url.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let url = "invalid-protocol://ffggfsdfsdf/";
+ ok(!thumbnailExists(url), "Thumbnail should not be cached already.");
+ let numCalls = 0;
+ BackgroundPageThumbs.capture(url, {
+ onDone: function onDone(capturedURL) {
+ is(capturedURL, url, "Captured URL should be URL passed to capture");
+ is(numCalls++, 0, "onDone should be called only once");
+ ok(!thumbnailExists(url),
+ "Capture failed so thumbnail should not be cached");
+ next();
+ },
+ });
+ yield new Promise(resolve => {
+ bgAddPageThumbObserver(url).catch(function(err) {
+ ok(true, "page-thumbnail error produced");
+ resolve();
+ });
+ });
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_basic.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_basic.js
new file mode 100644
index 0000000000..027e0bfb74
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_basic.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let url = "http://www.example.com/";
+ ok(!thumbnailExists(url), "Thumbnail should not be cached yet.");
+
+ let capturePromise = new Promise(resolve => {
+ bgAddPageThumbObserver(url).then(() => {
+ ok(true, `page-thumbnail created for ${url}`);
+ resolve();
+ });
+ });
+ let capturedURL = yield bgCapture(url);
+ is(capturedURL, url, "Captured URL should be URL passed to capture");
+ yield capturePromise;
+
+ ok(thumbnailExists(url), "Thumbnail should be cached after capture");
+ removeThumbnail(url);
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js
new file mode 100644
index 0000000000..cd1f1c5c22
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_captureIfMissing.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let numNotifications = 0;
+ function observe(subject, topic, data) {
+ is(topic, "page-thumbnail:create", "got expected topic");
+ numNotifications += 1;
+ }
+
+ Services.obs.addObserver(observe, "page-thumbnail:create", false);
+
+ let url = "http://example.com/";
+ let file = thumbnailFile(url);
+ ok(!file.exists(), "Thumbnail file should not already exist.");
+
+ let capturedURL = yield bgCaptureIfMissing(url);
+ is(numNotifications, 1, "got notification of item being created.");
+ is(capturedURL, url, "Captured URL should be URL passed to capture");
+ ok(file.exists(url), "Thumbnail should be cached after capture");
+
+ let past = Date.now() - 1000000000;
+ let pastFudge = past + 30000;
+ file.lastModifiedTime = past;
+ ok(file.lastModifiedTime < pastFudge, "Last modified time should stick!");
+ capturedURL = yield bgCaptureIfMissing(url);
+ is(numNotifications, 1, "still only 1 notification of item being created.");
+ is(capturedURL, url, "Captured URL should be URL passed to second capture");
+ ok(file.exists(), "Thumbnail should remain cached after second capture");
+ ok(file.lastModifiedTime < pastFudge,
+ "File should not have been overwritten");
+
+ file.remove(false);
+ Services.obs.removeObserver(observe, "page-thumbnail:create");
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_during_capture.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_during_capture.js
new file mode 100644
index 0000000000..db67a04a89
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_during_capture.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let crashObserver = bgAddCrashObserver();
+
+ // make a good capture first - this ensures we have the <browser>
+ let goodUrl = bgTestPageURL();
+ yield bgCapture(goodUrl);
+ ok(thumbnailExists(goodUrl), "Thumbnail should be cached after capture");
+ removeThumbnail(goodUrl);
+
+ // inject our content script.
+ let mm = bgInjectCrashContentScript();
+
+ // queue up 2 captures - the first has a wait, so this is the one that
+ // will die. The second one should immediately capture after the crash.
+ let waitUrl = bgTestPageURL({ wait: 30000 });
+ let sawWaitUrlCapture = false;
+ bgCapture(waitUrl, { onDone: () => {
+ sawWaitUrlCapture = true;
+ ok(!thumbnailExists(waitUrl), "Thumbnail should not have been saved due to the crash");
+ }});
+ bgCapture(goodUrl, { onDone: () => {
+ ok(sawWaitUrlCapture, "waitUrl capture should have finished first");
+ ok(thumbnailExists(goodUrl), "We should have recovered and completed the 2nd capture after the crash");
+ removeThumbnail(goodUrl);
+ // Test done.
+ ok(crashObserver.crashed, "Saw a crash from this test");
+ next();
+ }});
+ let crashPromise = new Promise(resolve => {
+ bgAddPageThumbObserver(waitUrl).catch(function(err) {
+ ok(true, `page-thumbnail error thrown for ${waitUrl}`);
+ resolve();
+ });
+ });
+ let capturePromise = new Promise(resolve => {
+ bgAddPageThumbObserver(goodUrl).then(() => {
+ ok(true, `page-thumbnail created for ${goodUrl}`);
+ resolve();
+ });
+ });
+
+ info("Crashing the thumbnail content process.");
+ mm.sendAsyncMessage("thumbnails-test:crash");
+ yield crashPromise;
+ yield capturePromise;
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_while_idle.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_while_idle.js
new file mode 100644
index 0000000000..8ff6a35093
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_crash_while_idle.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let crashObserver = bgAddCrashObserver();
+
+ // make a good capture first - this ensures we have the <browser>
+ let goodUrl = bgTestPageURL();
+ yield bgCapture(goodUrl);
+ ok(thumbnailExists(goodUrl), "Thumbnail should be cached after capture");
+ removeThumbnail(goodUrl);
+
+ // inject our content script.
+ let mm = bgInjectCrashContentScript();
+
+ // the observer for the crashing process is basically async, so it's hard
+ // to know when the <browser> has actually seen it. Easist is to just add
+ // our own observer.
+ Services.obs.addObserver(function onCrash() {
+ Services.obs.removeObserver(onCrash, "oop-frameloader-crashed");
+ // spin the event loop to ensure the BPT observer was called.
+ executeSoon(function() {
+ // Now queue another capture and ensure it recovers.
+ bgCapture(goodUrl, { onDone: () => {
+ ok(thumbnailExists(goodUrl), "We should have recovered and handled new capture requests");
+ removeThumbnail(goodUrl);
+ // Test done.
+ ok(crashObserver.crashed, "Saw a crash from this test");
+ next();
+ }});
+ });
+ }, "oop-frameloader-crashed", false);
+
+ // Nothing is pending - crash the process.
+ info("Crashing the thumbnail content process.");
+ mm.sendAsyncMessage("thumbnails-test:crash");
+ yield true;
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_destroy_browser.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_destroy_browser.js
new file mode 100644
index 0000000000..b83fdf5839
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_destroy_browser.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount", 1]]
+ });
+
+ let url1 = "http://example.com/1";
+ ok(!thumbnailExists(url1), "First file should not exist yet.");
+
+ let url2 = "http://example.com/2";
+ ok(!thumbnailExists(url2), "Second file should not exist yet.");
+
+ let defaultTimeout = BackgroundPageThumbs._destroyBrowserTimeout;
+ BackgroundPageThumbs._destroyBrowserTimeout = 1000;
+
+ yield bgCapture(url1);
+ ok(thumbnailExists(url1), "First file should exist after capture.");
+ removeThumbnail(url1);
+
+ yield wait(2000);
+ is(BackgroundPageThumbs._thumbBrowser, undefined,
+ "Thumb browser should be destroyed after timeout.");
+ BackgroundPageThumbs._destroyBrowserTimeout = defaultTimeout;
+
+ yield bgCapture(url2);
+ ok(thumbnailExists(url2), "Second file should exist after capture.");
+ removeThumbnail(url2);
+
+ isnot(BackgroundPageThumbs._thumbBrowser, undefined,
+ "Thumb browser should exist immediately after capture.");
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_alert.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_alert.js
new file mode 100644
index 0000000000..5d6bd81f86
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_alert.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let url = "data:text/html,<script>try { alert('yo!'); } catch (e) {}</script>";
+ ok(!thumbnailExists(url), "Thumbnail file should not already exist.");
+
+ let capturedURL = yield bgCapture(url);
+ is(capturedURL, url, "Captured URL should be URL passed to capture.");
+ ok(thumbnailExists(url),
+ "Thumbnail file should exist even though it alerted.");
+ removeThumbnail(url);
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_auth_prompt.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_auth_prompt.js
new file mode 100644
index 0000000000..0eb9df7a94
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_auth_prompt.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// the following tests attempt to display modal dialogs. The test just
+// relies on the fact that if the dialog was displayed the test will hang
+// and timeout. IOW - the tests would pass if the dialogs appear and are
+// manually closed by the user - so don't do that :) (obviously there is
+// noone available to do that when run via tbpl etc, so this should be safe,
+// and it's tricky to use the window-watcher to check a window *does not*
+// appear - how long should the watcher be active before assuming it's not
+// going to appear?)
+function* runTests() {
+ let url = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/authenticate.sjs?user=anyone";
+ ok(!thumbnailExists(url), "Thumbnail file should not already exist.");
+
+ let capturedURL = yield bgCapture(url);
+ is(capturedURL, url, "Captured URL should be URL passed to capture.");
+ ok(thumbnailExists(url),
+ "Thumbnail file should exist even though it requires auth.");
+ removeThumbnail(url);
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_sent.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_sent.js
new file mode 100644
index 0000000000..afbedb3829
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_sent.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ // Visit the test page in the browser and tell it to set a cookie.
+ let url = bgTestPageURL({ setGreenCookie: true });
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+
+ // The root element of the page shouldn't be green yet.
+ yield ContentTask.spawn(browser, null, function () {
+ Assert.notEqual(content.document.documentElement.style.backgroundColor,
+ "rgb(0, 255, 0)",
+ "The page shouldn't be green yet.");
+ });
+
+ // Cookie should be set now. Reload the page to verify. Its root element
+ // will be green if the cookie's set.
+ browser.reload();
+ yield BrowserTestUtils.browserLoaded(browser);
+ yield ContentTask.spawn(browser, null, function () {
+ Assert.equal(content.document.documentElement.style.backgroundColor,
+ "rgb(0, 255, 0)",
+ "The page should be green now.");
+ });
+
+ // Capture the page. Get the image data of the capture and verify it's not
+ // green. (Checking only the first pixel suffices.)
+ yield bgCapture(url);
+ ok(thumbnailExists(url), "Thumbnail file should exist after capture.");
+
+ retrieveImageDataForURL(url, function ([r, g, b]) {
+ isnot([r, g, b].toString(), [0, 255, 0].toString(),
+ "The captured page should not be green.");
+ gBrowser.removeTab(tab);
+ removeThumbnail(url);
+ next();
+ });
+ yield true;
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js
new file mode 100644
index 0000000000..90a1a890b0
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// check that if a page captured in the background attempts to set a cookie,
+// that cookie is not saved for subsequent requests.
+function* runTests() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["privacy.usercontext.about_newtab_segregation.enabled", true]]
+ });
+ let url = bgTestPageURL({
+ setRedCookie: true,
+ iframe: bgTestPageURL({ setRedCookie: true}),
+ xhr: bgTestPageURL({ setRedCookie: true})
+ });
+ ok(!thumbnailExists(url), "Thumbnail file should not exist before capture.");
+ yield bgCapture(url);
+ ok(thumbnailExists(url), "Thumbnail file should exist after capture.");
+ removeThumbnail(url);
+ // now load it up in a browser - it should *not* be red, otherwise the
+ // cookie above was saved.
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+
+ // The root element of the page shouldn't be red.
+ yield ContentTask.spawn(browser, null, function() {
+ Assert.notEqual(content.document.documentElement.style.backgroundColor,
+ "rgb(255, 0, 0)",
+ "The page shouldn't be red.");
+ });
+
+ gBrowser.removeTab(tab);
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_duplicates.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_duplicates.js
new file mode 100644
index 0000000000..31b5043356
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_duplicates.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let url = "http://example.com/1";
+ ok(!thumbnailExists(url), "Thumbnail file should not already exist.");
+ let numCallbacks = 0;
+ let doneCallback = function(doneUrl) {
+ is(doneUrl, url, "called back with correct url");
+ numCallbacks += 1;
+ // We will delete the file after the first callback, then check it
+ // still doesn't exist on the second callback, which should give us
+ // confidence that we didn't end up with 2 different captures happening
+ // for the same url...
+ if (numCallbacks == 1) {
+ ok(thumbnailExists(url), "Thumbnail file should now exist.");
+ removeThumbnail(url);
+ return;
+ }
+ if (numCallbacks == 2) {
+ ok(!thumbnailExists(url), "Thumbnail file should still be deleted.");
+ // and that's all we expect, so we are done...
+ next();
+ return;
+ }
+ ok(false, "only expecting 2 callbacks");
+ }
+ BackgroundPageThumbs.capture(url, {onDone: doneCallback});
+ BackgroundPageThumbs.capture(url, {onDone: doneCallback});
+ yield true;
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_queueing.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_queueing.js
new file mode 100644
index 0000000000..1426f6f4e9
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_queueing.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let urls = [
+ "http://www.example.com/0",
+ "http://www.example.com/1",
+ // an item that will timeout to ensure timeouts work and we resume.
+ bgTestPageURL({ wait: 2002 }),
+ "http://www.example.com/2",
+ ];
+ dontExpireThumbnailURLs(urls);
+ urls.forEach(url => {
+ ok(!thumbnailExists(url), "Thumbnail should not exist yet: " + url);
+ let isTimeoutTest = url.indexOf("wait") >= 0;
+ BackgroundPageThumbs.capture(url, {
+ timeout: isTimeoutTest ? 100 : 30000,
+ onDone: function onDone(capturedURL) {
+ ok(urls.length > 0, "onDone called, so URLs should still remain");
+ is(capturedURL, urls.shift(),
+ "Captured URL should be currently expected URL (i.e., " +
+ "capture() callbacks should be called in the correct order)");
+ if (isTimeoutTest) {
+ ok(!thumbnailExists(capturedURL),
+ "Thumbnail shouldn't exist for timed out capture");
+ } else {
+ ok(thumbnailExists(capturedURL),
+ "Thumbnail should be cached after capture");
+ removeThumbnail(url);
+ }
+ if (!urls.length)
+ // Test done.
+ next();
+ },
+ });
+ });
+ yield true;
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_redirect.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_redirect.js
new file mode 100644
index 0000000000..baa1b6d681
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_redirect.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let finalURL = "http://example.com/redirected";
+ let originalURL = bgTestPageURL({ redirect: finalURL });
+
+ ok(!thumbnailExists(originalURL),
+ "Thumbnail file for original URL should not exist yet.");
+ ok(!thumbnailExists(finalURL),
+ "Thumbnail file for final URL should not exist yet.");
+
+ let captureOriginalPromise = new Promise(resolve => {
+ bgAddPageThumbObserver(originalURL).then(() => {
+ ok(true, `page-thumbnail created for ${originalURL}`);
+ resolve();
+ });
+ });
+
+ let captureFinalPromise = new Promise(resolve => {
+ bgAddPageThumbObserver(finalURL).then(() => {
+ ok(true, `page-thumbnail created for ${finalURL}`);
+ resolve();
+ });
+ });
+
+ let capturedURL = yield bgCapture(originalURL);
+ is(capturedURL, originalURL,
+ "Captured URL should be URL passed to capture");
+ yield captureOriginalPromise;
+ yield captureFinalPromise;
+ ok(thumbnailExists(originalURL),
+ "Thumbnail for original URL should be cached");
+ ok(thumbnailExists(finalURL),
+ "Thumbnail for final URL should be cached");
+
+ removeThumbnail(originalURL);
+ removeThumbnail(finalURL);
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_timeout.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_timeout.js
new file mode 100644
index 0000000000..da05b43553
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_timeout.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function* runTests() {
+ let url = bgTestPageURL({ wait: 30000 });
+ ok(!thumbnailExists(url), "Thumbnail should not be cached already.");
+ let numCalls = 0;
+ BackgroundPageThumbs.capture(url, {
+ timeout: 0,
+ onDone: function onDone(capturedURL) {
+ is(capturedURL, url, "Captured URL should be URL passed to capture");
+ is(numCalls++, 0, "onDone should be called only once");
+ ok(!thumbnailExists(url),
+ "Capture timed out so thumbnail should not be cached");
+ next();
+ },
+ });
+ yield new Promise(resolve => {
+ bgAddPageThumbObserver(url).catch(function(err) {
+ ok(true, `page-thumbnail error thrown for ${url}`);
+ resolve();
+ });
+ });
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bug726727.js b/toolkit/components/thumbnails/test/browser_thumbnails_bug726727.js
new file mode 100644
index 0000000000..f7f1f3deb6
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bug726727.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests ensure that capturing a sites's thumbnail, saving it and
+ * retrieving it from the cache works.
+ */
+function* runTests() {
+ // Create a tab that shows an error page.
+ let tab = gBrowser.addTab("http://127.0.0.1:1/");
+ let browser = tab.linkedBrowser;
+ yield browser.addEventListener("DOMContentLoaded", function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false);
+ PageThumbs.shouldStoreThumbnail(browser, (aResult) => {
+ ok(!aResult, "we're not going to capture an error page");
+ executeSoon(next);
+ });
+ }, false);
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js b/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js
new file mode 100644
index 0000000000..c4faac685e
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bug727765.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/" +
+ "test/background_red_scroll.html";
+
+function isRedThumbnailFuzz(r, g, b, expectedR, expectedB, expectedG, aFuzz)
+{
+ return (Math.abs(r - expectedR) <= aFuzz) &&
+ (Math.abs(g - expectedG) <= aFuzz) &&
+ (Math.abs(b - expectedB) <= aFuzz);
+}
+
+// Test for black borders caused by scrollbars.
+function* runTests() {
+ // Create a tab with a page with a red background and scrollbars.
+ yield addTab(URL);
+ yield captureAndCheckColor(255, 0, 0, "we have a red thumbnail");
+
+ // Check the thumbnail color of the bottom right pixel.
+ yield whenFileExists(URL);
+ yield retrieveImageDataForURL(URL, function (aData) {
+ let [r, g, b] = [].slice.call(aData, -4);
+ let fuzz = 2; // Windows 8 x64 blends with the scrollbar a bit.
+ var message = "Expected red thumbnail rgb(255, 0, 0), got " + r + "," + g + "," + b;
+ ok(isRedThumbnailFuzz(r, g, b, 255, 0, 0, fuzz), message);
+ next();
+ });
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js b/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js
new file mode 100644
index 0000000000..a7e1caa04a
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bug818225.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/" +
+ "test/background_red.html?" + Date.now();
+
+// Test PageThumbs API function getThumbnailPath
+function* runTests() {
+
+ let path = PageThumbs.getThumbnailPath(URL);
+ yield testIfExists(path, false, "Thumbnail file does not exist");
+
+ yield addVisitsAndRepopulateNewTabLinks(URL, next);
+ yield createThumbnail(URL);
+
+ path = PageThumbs.getThumbnailPath(URL);
+ let expectedPath = PageThumbsStorage.getFilePathForURL(URL);
+ is(path, expectedPath, "Thumbnail file has correct path");
+
+ yield testIfExists(path, true, "Thumbnail file exists");
+
+}
+
+function createThumbnail(aURL) {
+ addTab(aURL, function () {
+ whenFileExists(aURL, function () {
+ gBrowser.removeTab(gBrowser.selectedTab);
+ next();
+ });
+ });
+}
+
+function testIfExists(aPath, aExpected, aMessage) {
+ return OS.File.exists(aPath).then(
+ function onSuccess(exists) {
+ is(exists, aExpected, aMessage);
+ },
+ function onFailure(error) {
+ ok(false, "OS.File.exists() failed " + error);
+ }
+ );
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_capture.js b/toolkit/components/thumbnails/test/browser_thumbnails_capture.js
new file mode 100644
index 0000000000..47d94d31ba
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_capture.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests ensure that capturing a sites's thumbnail, saving it and
+ * retrieving it from the cache works.
+ */
+function* runTests() {
+ // Create a tab with a red background.
+ yield addTab("data:text/html,<body bgcolor=ff0000></body>");
+ yield captureAndCheckColor(255, 0, 0, "we have a red thumbnail");
+
+ // Load a page with a green background.
+ yield navigateTo("data:text/html,<body bgcolor=00ff00></body>");
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+
+ // Load a page with a blue background.
+ yield navigateTo("data:text/html,<body bgcolor=0000ff></body>");
+ yield captureAndCheckColor(0, 0, 255, "we have a blue thumbnail");
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_expiration.js b/toolkit/components/thumbnails/test/browser_thumbnails_expiration.js
new file mode 100644
index 0000000000..4c73e17bef
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_expiration.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/?t=" + Date.now();
+const URL1 = URL + "#1";
+const URL2 = URL + "#2";
+const URL3 = URL + "#3";
+
+var tmp = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("resource://gre/modules/PageThumbs.jsm", tmp);
+
+const EXPIRATION_MIN_CHUNK_SIZE = 50;
+const {PageThumbsExpiration} = tmp;
+
+function* runTests() {
+ // Create dummy URLs.
+ let dummyURLs = [];
+ for (let i = 0; i < EXPIRATION_MIN_CHUNK_SIZE + 10; i++) {
+ dummyURLs.push(URL + "#dummy" + i);
+ }
+
+ // Make sure our thumbnails aren't expired too early.
+ dontExpireThumbnailURLs([URL1, URL2, URL3].concat(dummyURLs));
+
+ // Create three thumbnails.
+ yield createDummyThumbnail(URL1);
+ ok(thumbnailExists(URL1), "first thumbnail created");
+
+ yield createDummyThumbnail(URL2);
+ ok(thumbnailExists(URL2), "second thumbnail created");
+
+ yield createDummyThumbnail(URL3);
+ ok(thumbnailExists(URL3), "third thumbnail created");
+
+ // Remove the third thumbnail.
+ yield expireThumbnails([URL1, URL2]);
+ ok(thumbnailExists(URL1), "first thumbnail still exists");
+ ok(thumbnailExists(URL2), "second thumbnail still exists");
+ ok(!thumbnailExists(URL3), "third thumbnail has been removed");
+
+ // Remove the second thumbnail.
+ yield expireThumbnails([URL1]);
+ ok(thumbnailExists(URL1), "first thumbnail still exists");
+ ok(!thumbnailExists(URL2), "second thumbnail has been removed");
+
+ // Remove all thumbnails.
+ yield expireThumbnails([]);
+ ok(!thumbnailExists(URL1), "all thumbnails have been removed");
+
+ // Create some more files than the min chunk size.
+ for (let url of dummyURLs) {
+ yield createDummyThumbnail(url);
+ }
+
+ ok(dummyURLs.every(thumbnailExists), "all dummy thumbnails created");
+
+ // Expire thumbnails and expect 10 remaining.
+ yield expireThumbnails([]);
+ let remainingURLs = dummyURLs.filter(thumbnailExists);
+ is(remainingURLs.length, 10, "10 dummy thumbnails remaining");
+
+ // Expire thumbnails again. All should be gone by now.
+ yield expireThumbnails([]);
+ remainingURLs = remainingURLs.filter(thumbnailExists);
+ is(remainingURLs.length, 0, "no dummy thumbnails remaining");
+}
+
+function createDummyThumbnail(aURL) {
+ info("Creating dummy thumbnail for " + aURL);
+ let dummy = new Uint8Array(10);
+ for (let i = 0; i < 10; ++i) {
+ dummy[i] = i;
+ }
+ PageThumbsStorage.writeData(aURL, dummy).then(
+ function onSuccess() {
+ info("createDummyThumbnail succeeded");
+ executeSoon(next);
+ },
+ function onFailure(error) {
+ ok(false, "createDummyThumbnail failed " + error);
+ }
+ );
+}
+
+function expireThumbnails(aKeep) {
+ PageThumbsExpiration.expireThumbnails(aKeep).then(
+ function onSuccess() {
+ info("expireThumbnails succeeded");
+ executeSoon(next);
+ },
+ function onFailure(error) {
+ ok(false, "expireThumbnails failed " + error);
+ }
+ );
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_privacy.js b/toolkit/components/thumbnails/test/browser_thumbnails_privacy.js
new file mode 100644
index 0000000000..e7dc7b4d55
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_privacy.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_DISK_CACHE_SSL = "browser.cache.disk_cache_ssl";
+const URL = "://example.com/browser/toolkit/components/thumbnails/" +
+ "test/privacy_cache_control.sjs";
+
+function* runTests() {
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref(PREF_DISK_CACHE_SSL);
+ });
+
+ let positive = [
+ // A normal HTTP page without any Cache-Control header.
+ {scheme: "http", cacheControl: null, diskCacheSSL: false},
+
+ // A normal HTTP page with 'Cache-Control: private'.
+ {scheme: "http", cacheControl: "private", diskCacheSSL: false},
+
+ // Capture HTTPS pages if browser.cache.disk_cache_ssl == true.
+ {scheme: "https", cacheControl: null, diskCacheSSL: true},
+ {scheme: "https", cacheControl: "public", diskCacheSSL: true},
+ {scheme: "https", cacheControl: "private", diskCacheSSL: true}
+ ];
+
+ let negative = [
+ // Never capture pages with 'Cache-Control: no-store'.
+ {scheme: "http", cacheControl: "no-store", diskCacheSSL: false},
+ {scheme: "http", cacheControl: "no-store", diskCacheSSL: true},
+ {scheme: "https", cacheControl: "no-store", diskCacheSSL: false},
+ {scheme: "https", cacheControl: "no-store", diskCacheSSL: true},
+
+ // Don't capture HTTPS pages by default.
+ {scheme: "https", cacheControl: null, diskCacheSSL: false},
+ {scheme: "https", cacheControl: "public", diskCacheSSL: false},
+ {scheme: "https", cacheControl: "private", diskCacheSSL: false}
+ ];
+
+ yield checkCombinations(positive, true);
+ yield checkCombinations(negative, false);
+}
+
+function checkCombinations(aCombinations, aResult) {
+ let combi = aCombinations.shift();
+ if (!combi) {
+ next();
+ return;
+ }
+
+ let url = combi.scheme + URL;
+ if (combi.cacheControl)
+ url += "?" + combi.cacheControl;
+ Services.prefs.setBoolPref(PREF_DISK_CACHE_SSL, combi.diskCacheSSL);
+
+ // Add the test page as a top link so it has a chance to be thumbnailed
+ addVisitsAndRepopulateNewTabLinks(url, _ => {
+ testCombination(combi, url, aCombinations, aResult);
+ });
+}
+
+function testCombination(combi, url, aCombinations, aResult) {
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let browser = gBrowser.selectedBrowser;
+
+ whenLoaded(browser, () => {
+ let msg = JSON.stringify(combi) + " == " + aResult;
+ PageThumbs.shouldStoreThumbnail(browser, (aIsSafeSite) => {
+ is(aIsSafeSite, aResult, msg);
+ gBrowser.removeTab(tab);
+ // Continue with the next combination.
+ checkCombinations(aCombinations, aResult);
+ });
+ });
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js b/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js
new file mode 100644
index 0000000000..482dbc8032
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_redirect.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/" +
+ "test/background_red_redirect.sjs";
+// loading URL will redirect us to...
+const FINAL_URL = "http://mochi.test:8888/browser/toolkit/components/" +
+ "thumbnails/test/background_red.html";
+
+/**
+ * These tests ensure that we save and provide thumbnails for redirecting sites.
+ */
+function* runTests() {
+ dontExpireThumbnailURLs([URL, FINAL_URL]);
+
+ // Kick off history by loading a tab first or the test fails in single mode.
+ yield addTab(URL);
+ gBrowser.removeTab(gBrowser.selectedTab);
+
+ // Create a tab, redirecting to a page with a red background.
+ yield addTab(URL);
+ yield captureAndCheckColor(255, 0, 0, "we have a red thumbnail");
+
+ // Wait until the referrer's thumbnail's file has been written.
+ yield whenFileExists(URL);
+ yield retrieveImageDataForURL(URL, function ([r, g, b]) {
+ is("" + [r, g, b], "255,0,0", "referrer has a red thumbnail");
+ next();
+ });
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_storage.js b/toolkit/components/thumbnails/test/browser_thumbnails_storage.js
new file mode 100644
index 0000000000..972f956e5e
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_storage.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/";
+const URL_COPY = URL + "#copy";
+
+XPCOMUtils.defineLazyGetter(this, "Sanitizer", function () {
+ let tmp = {};
+ Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", tmp);
+ return tmp.Sanitizer;
+});
+
+/**
+ * These tests ensure that the thumbnail storage is working as intended.
+ * Newly captured thumbnails should be saved as files and they should as well
+ * be removed when the user sanitizes their history.
+ */
+function* runTests() {
+ yield Task.spawn(function*() {
+ dontExpireThumbnailURLs([URL, URL_COPY]);
+
+ yield promiseClearHistory();
+ yield promiseAddVisitsAndRepopulateNewTabLinks(URL);
+ yield promiseCreateThumbnail();
+
+ // Make sure Storage.copy() updates an existing file.
+ yield PageThumbsStorage.copy(URL, URL_COPY);
+ let copy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
+ let mtime = copy.lastModifiedTime -= 60;
+
+ yield PageThumbsStorage.copy(URL, URL_COPY);
+ isnot(new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY)).lastModifiedTime, mtime,
+ "thumbnail file was updated");
+
+ let file = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL));
+ let fileCopy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
+
+ // Clear the browser history. Retry until the files are gone because Windows
+ // locks them sometimes.
+ info("Clearing history");
+ while (file.exists() || fileCopy.exists()) {
+ yield promiseClearHistory();
+ }
+ info("History is clear");
+
+ info("Repopulating");
+ yield promiseAddVisitsAndRepopulateNewTabLinks(URL);
+ yield promiseCreateThumbnail();
+
+ info("Clearing the last 10 minutes of browsing history");
+ // Clear the last 10 minutes of browsing history.
+ yield promiseClearHistory(true);
+
+ info("Attempt to clear file");
+ // Retry until the file is gone because Windows locks it sometimes.
+ yield promiseClearFile(file, URL);
+
+ info("Done");
+ });
+}
+
+var promiseClearFile = Task.async(function*(aFile, aURL) {
+ if (!aFile.exists()) {
+ return undefined;
+ }
+ // Re-add our URL to the history so that history observer's onDeleteURI()
+ // is called again.
+ yield PlacesTestUtils.addVisits(makeURI(aURL));
+ yield promiseClearHistory(true);
+ // Then retry.
+ return promiseClearFile(aFile, aURL);
+});
+
+function promiseClearHistory(aUseRange) {
+ let s = new Sanitizer();
+ s.prefDomain = "privacy.cpd.";
+
+ let prefs = gPrefService.getBranch(s.prefDomain);
+ prefs.setBoolPref("history", true);
+ prefs.setBoolPref("downloads", false);
+ prefs.setBoolPref("cache", false);
+ prefs.setBoolPref("cookies", false);
+ prefs.setBoolPref("formdata", false);
+ prefs.setBoolPref("offlineApps", false);
+ prefs.setBoolPref("passwords", false);
+ prefs.setBoolPref("sessions", false);
+ prefs.setBoolPref("siteSettings", false);
+
+ if (aUseRange) {
+ let usec = Date.now() * 1000;
+ s.range = [usec - 10 * 60 * 1000 * 1000, usec];
+ s.ignoreTimespan = false;
+ }
+
+ return s.sanitize().then(() => {
+ s.range = null;
+ s.ignoreTimespan = true;
+ });
+}
+
+function promiseCreateThumbnail() {
+ return new Promise(resolve => {
+ addTab(URL, function () {
+ whenFileExists(URL, function () {
+ gBrowser.removeTab(gBrowser.selectedTab);
+ resolve();
+ });
+ });
+ });
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_storage_migrate3.js b/toolkit/components/thumbnails/test/browser_thumbnails_storage_migrate3.js
new file mode 100644
index 0000000000..e7f150f871
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_storage_migrate3.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/migration3";
+const URL2 = URL + "#2";
+const URL3 = URL + "#3";
+const THUMBNAIL_DIRECTORY = "thumbnails";
+const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
+
+var tmp = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("resource://gre/modules/PageThumbs.jsm", tmp);
+var {PageThumbsStorageMigrator} = tmp;
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDirSvc",
+ "@mozilla.org/file/directory_service;1", "nsIProperties");
+
+/**
+ * This test makes sure we correctly migrate to thumbnail storage version 3.
+ * This means copying existing thumbnails from the roaming to the local profile
+ * directory and should just apply to Linux.
+ */
+function* runTests() {
+ // Prepare a local profile directory.
+ let localProfile = FileUtils.getDir("ProfD", ["local-test"], true);
+ changeLocation("ProfLD", localProfile);
+
+ let roaming = FileUtils.getDir("ProfD", [THUMBNAIL_DIRECTORY], true);
+
+ // Set up some data in the roaming profile.
+ let name = PageThumbsStorage.getLeafNameForURL(URL);
+ let file = FileUtils.getFile("ProfD", [THUMBNAIL_DIRECTORY, name]);
+ writeDummyFile(file);
+
+ name = PageThumbsStorage.getLeafNameForURL(URL2);
+ file = FileUtils.getFile("ProfD", [THUMBNAIL_DIRECTORY, name]);
+ writeDummyFile(file);
+
+ name = PageThumbsStorage.getLeafNameForURL(URL3);
+ file = FileUtils.getFile("ProfD", [THUMBNAIL_DIRECTORY, name]);
+ writeDummyFile(file);
+
+ // Pretend to have one of the thumbnails
+ // already in place at the new storage site.
+ name = PageThumbsStorage.getLeafNameForURL(URL3);
+ file = FileUtils.getFile("ProfLD", [THUMBNAIL_DIRECTORY, name]);
+ writeDummyFile(file, "no-overwrite-plz");
+
+ // Kick off thumbnail storage migration.
+ PageThumbsStorageMigrator.migrateToVersion3(localProfile.path);
+ ok(true, "migration finished");
+
+ // Wait until the first thumbnail was moved to its new location.
+ yield whenFileExists(URL);
+ ok(true, "first thumbnail moved");
+
+ // Wait for the second thumbnail to be moved as well.
+ yield whenFileExists(URL2);
+ ok(true, "second thumbnail moved");
+
+ yield whenFileRemoved(roaming);
+ ok(true, "roaming thumbnail directory removed");
+
+ // Check that our existing thumbnail wasn't overwritten.
+ is(getFileContents(file), "no-overwrite-plz",
+ "existing thumbnail was not overwritten");
+
+ // Sanity check: ensure that, until it is removed, deprecated
+ // function |getFileForURL| points to the same path as
+ // |getFilePathForURL|.
+ if ("getFileForURL" in PageThumbsStorage) {
+ file = PageThumbsStorage.getFileForURL(URL);
+ is(file.path, PageThumbsStorage.getFilePathForURL(URL),
+ "Deprecated getFileForURL and getFilePathForURL return the same path");
+ }
+}
+
+function changeLocation(aLocation, aNewDir) {
+ let oldDir = gDirSvc.get(aLocation, Ci.nsILocalFile);
+ gDirSvc.undefine(aLocation);
+ gDirSvc.set(aLocation, aNewDir);
+
+ registerCleanupFunction(function () {
+ gDirSvc.undefine(aLocation);
+ gDirSvc.set(aLocation, oldDir);
+ });
+}
+
+function writeDummyFile(aFile, aContents) {
+ let fos = FileUtils.openSafeFileOutputStream(aFile);
+ let data = aContents || "dummy";
+ fos.write(data, data.length);
+ FileUtils.closeSafeFileOutputStream(fos);
+}
+
+function getFileContents(aFile) {
+ let istream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ istream.init(aFile, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ return NetUtil.readInputStreamToString(istream, istream.available());
+}
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_update.js b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
new file mode 100644
index 0000000000..971a2994e8
--- /dev/null
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_update.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests check the auto-update facility of the thumbnail service.
+ */
+
+function* runTests() {
+ // A "trampoline" - a generator that iterates over sub-iterators
+ let tests = [
+ simpleCaptureTest,
+ capIfStaleErrorResponseUpdateTest,
+ capIfStaleGoodResponseUpdateTest,
+ regularCapErrorResponseUpdateTest,
+ regularCapGoodResponseUpdateTest
+ ];
+ for (let test of tests) {
+ info("Running subtest " + test.name);
+ for (let iterator of test())
+ yield iterator;
+ }
+}
+
+function ensureThumbnailStale(url) {
+ // We go behind the back of the thumbnail service and change the
+ // mtime of the file to be in the past.
+ let fname = PageThumbsStorage.getFilePathForURL(url);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(fname);
+ ok(file.exists(), fname + " should exist");
+ // Set it as very stale...
+ file.lastModifiedTime = Date.now() - 1000000000;
+}
+
+function getThumbnailModifiedTime(url) {
+ let fname = PageThumbsStorage.getFilePathForURL(url);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(fname);
+ return file.lastModifiedTime;
+}
+
+// The tests!
+/* Check functionality of a normal captureAndStoreIfStale request */
+function* simpleCaptureTest() {
+ let numNotifications = 0;
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?simple";
+
+ function observe(subject, topic, data) {
+ is(topic, "page-thumbnail:create", "got expected topic");
+ is(data, URL, "data is our test URL");
+ if (++numNotifications == 2) {
+ // This is the final notification and signals test success...
+ Services.obs.removeObserver(observe, "page-thumbnail:create");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ next();
+ }
+ }
+
+ Services.obs.addObserver(observe, "page-thumbnail:create", false);
+ // Create a tab - we don't care what the content is.
+ yield addTab(URL);
+ let browser = gBrowser.selectedBrowser;
+
+ // Capture the screenshot.
+ PageThumbs.captureAndStore(browser, function () {
+ // We've got a capture so should have seen the observer.
+ is(numNotifications, 1, "got notification of item being created.");
+ // The capture is now "fresh" - so requesting the URL should not cause
+ // a new capture.
+ PageThumbs.captureAndStoreIfStale(browser, function() {
+ is(numNotifications, 1, "still only 1 notification of item being created.");
+
+ ensureThumbnailStale(URL);
+ // Ask for it to be updated.
+ PageThumbs.captureAndStoreIfStale(browser);
+ // But it's async, so wait - our observer above will call next() when
+ // the notification comes.
+ });
+ });
+ yield undefined // wait for callbacks to call 'next'...
+}
+
+/* Check functionality of captureAndStoreIfStale when there is an error response
+ from the server.
+ */
+function* capIfStaleErrorResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?fail";
+ yield addTab(URL);
+
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ // update the thumbnail to be stale, then re-request it. The server will
+ // return a 400 response and a red thumbnail.
+ // The service should not save the thumbnail - so we (a) check the thumbnail
+ // remains green and (b) check the mtime of the file is < now.
+ ensureThumbnailStale(URL);
+ yield navigateTo(URL);
+ // now() returns a higher-precision value than the modified time of a file.
+ // As we set the thumbnail very stale, allowing 1 second of "slop" here
+ // works around this while still keeping the test valid.
+ let now = Date.now() - 1000 ;
+ PageThumbs.captureAndStoreIfStale(gBrowser.selectedBrowser, () => {
+ ok(getThumbnailModifiedTime(URL) < now, "modified time should be < now");
+ retrieveImageDataForURL(URL, function ([r, g, b]) {
+ is("" + [r, g, b], "" + [0, 255, 0], "thumbnail is still green");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ next();
+ });
+ });
+ yield undefined; // wait for callback to call 'next'...
+}
+
+/* Check functionality of captureAndStoreIfStale when there is a non-error
+ response from the server. This test is somewhat redundant - although it is
+ using a http:// URL instead of a data: url like most others.
+ */
+function* capIfStaleGoodResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?ok";
+ yield addTab(URL);
+ let browser = gBrowser.selectedBrowser;
+
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ // update the thumbnail to be stale, then re-request it. The server will
+ // return a 200 response and a red thumbnail - so that new thumbnail should
+ // end up captured.
+ ensureThumbnailStale(URL);
+ yield navigateTo(URL);
+ // now() returns a higher-precision value than the modified time of a file.
+ // As we set the thumbnail very stale, allowing 1 second of "slop" here
+ // works around this while still keeping the test valid.
+ let now = Date.now() - 1000 ;
+ PageThumbs.captureAndStoreIfStale(browser, () => {
+ ok(getThumbnailModifiedTime(URL) >= now, "modified time should be >= now");
+ // the captureAndStoreIfStale request saw a 200 response with the red body,
+ // so we expect to see the red version here.
+ retrieveImageDataForURL(URL, function ([r, g, b]) {
+ is("" + [r, g, b], "" + [255, 0, 0], "thumbnail is now red");
+ next();
+ });
+ });
+ yield undefined; // wait for callback to call 'next'...
+}
+
+/* Check functionality of captureAndStore when there is an error response
+ from the server.
+ */
+function* regularCapErrorResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?fail";
+ yield addTab(URL);
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ // do it again - the server will return a 400, so the foreground service
+ // should not update it.
+ yield addTab(URL);
+ yield captureAndCheckColor(0, 255, 0, "we still have a green thumbnail");
+}
+
+/* Check functionality of captureAndStore when there is an OK response
+ from the server.
+ */
+function* regularCapGoodResponseUpdateTest() {
+ const URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_update.sjs?ok";
+ yield addTab(URL);
+ yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
+ gBrowser.removeTab(gBrowser.selectedTab);
+ // do it again - the server will return a 200, so the foreground service
+ // should update it.
+ yield addTab(URL);
+ yield captureAndCheckColor(255, 0, 0, "we now have a red thumbnail");
+}
diff --git a/toolkit/components/thumbnails/test/head.js b/toolkit/components/thumbnails/test/head.js
new file mode 100644
index 0000000000..e8229508a6
--- /dev/null
+++ b/toolkit/components/thumbnails/test/head.js
@@ -0,0 +1,356 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var tmp = {};
+Cu.import("resource://gre/modules/PageThumbs.jsm", tmp);
+Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", tmp);
+Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp);
+Cu.import("resource://gre/modules/FileUtils.jsm", tmp);
+Cu.import("resource://gre/modules/osfile.jsm", tmp);
+var {PageThumbs, BackgroundPageThumbs, NewTabUtils, PageThumbsStorage, SessionStore, FileUtils, OS} = tmp;
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+
+var oldEnabledPref = Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
+Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", false);
+
+registerCleanupFunction(function () {
+ while (gBrowser.tabs.length > 1)
+ gBrowser.removeTab(gBrowser.tabs[1]);
+ Services.prefs.setBoolPref("browser.pagethumbnails.capturing_disabled", oldEnabledPref)
+});
+
+/**
+ * Provide the default test function to start our test runner.
+ */
+function test() {
+ TestRunner.run();
+}
+
+/**
+ * The test runner that controls the execution flow of our tests.
+ */
+var TestRunner = {
+ /**
+ * Starts the test runner.
+ */
+ run: function () {
+ waitForExplicitFinish();
+
+ SessionStore.promiseInitialized.then(function () {
+ this._iter = runTests();
+ if (this._iter) {
+ this.next();
+ } else {
+ finish();
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Runs the next available test or finishes if there's no test left.
+ * @param aValue This value will be passed to the yielder via the runner's
+ * iterator.
+ */
+ next: function (aValue) {
+ let obj = TestRunner._iter.next(aValue);
+ if (obj.done) {
+ finish();
+ return;
+ }
+
+ let value = obj.value || obj;
+ if (value && typeof value.then == "function") {
+ value.then(result => {
+ next(result);
+ }, error => {
+ ok(false, error + "\n" + error.stack);
+ });
+ }
+ }
+};
+
+/**
+ * Continues the current test execution.
+ * @param aValue This value will be passed to the yielder via the runner's
+ * iterator.
+ */
+function next(aValue) {
+ TestRunner.next(aValue);
+}
+
+/**
+ * Creates a new tab with the given URI.
+ * @param aURI The URI that's loaded in the tab.
+ * @param aCallback The function to call when the tab has loaded.
+ */
+function addTab(aURI, aCallback) {
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aURI);
+ whenLoaded(tab.linkedBrowser, aCallback);
+}
+
+/**
+ * Loads a new URI into the currently selected tab.
+ * @param aURI The URI to load.
+ */
+function navigateTo(aURI) {
+ let browser = gBrowser.selectedBrowser;
+ whenLoaded(browser);
+ browser.loadURI(aURI);
+}
+
+/**
+ * Continues the current test execution when a load event for the given element
+ * has been received.
+ * @param aElement The DOM element to listen on.
+ * @param aCallback The function to call when the load event was dispatched.
+ */
+function whenLoaded(aElement, aCallback = next) {
+ aElement.addEventListener("load", function onLoad() {
+ aElement.removeEventListener("load", onLoad, true);
+ executeSoon(aCallback);
+ }, true);
+}
+
+/**
+ * Captures a screenshot for the currently selected tab, stores it in the cache,
+ * retrieves it from the cache and compares pixel color values.
+ * @param aRed The red component's intensity.
+ * @param aGreen The green component's intensity.
+ * @param aBlue The blue component's intensity.
+ * @param aMessage The info message to print when comparing the pixel color.
+ */
+function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) {
+ let browser = gBrowser.selectedBrowser;
+ // We'll get oranges if the expiration filter removes the file during the
+ // test.
+ dontExpireThumbnailURLs([browser.currentURI.spec]);
+
+ // Capture the screenshot.
+ PageThumbs.captureAndStore(browser, function () {
+ retrieveImageDataForURL(browser.currentURI.spec, function ([r, g, b]) {
+ is("" + [r, g, b], "" + [aRed, aGreen, aBlue], aMessage);
+ next();
+ });
+ });
+}
+
+/**
+ * For a given URL, loads the corresponding thumbnail
+ * to a canvas and passes its image data to the callback.
+ * Note, not compat with e10s!
+ * @param aURL The url associated with the thumbnail.
+ * @param aCallback The function to pass the image data to.
+ */
+function retrieveImageDataForURL(aURL, aCallback) {
+ let width = 100, height = 100;
+ let thumb = PageThumbs.getThumbnailURL(aURL, width, height);
+
+ let htmlns = "http://www.w3.org/1999/xhtml";
+ let img = document.createElementNS(htmlns, "img");
+ img.setAttribute("src", thumb);
+
+ whenLoaded(img, function () {
+ let canvas = document.createElementNS(htmlns, "canvas");
+ canvas.setAttribute("width", width);
+ canvas.setAttribute("height", height);
+
+ // Draw the image to a canvas and compare the pixel color values.
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0, width, height);
+ let result = ctx.getImageData(0, 0, 100, 100).data;
+ aCallback(result);
+ });
+}
+
+/**
+ * Returns the file of the thumbnail with the given URL.
+ * @param aURL The URL of the thumbnail.
+ */
+function thumbnailFile(aURL) {
+ return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
+}
+
+/**
+ * Checks if a thumbnail for the given URL exists.
+ * @param aURL The url associated to the thumbnail.
+ */
+function thumbnailExists(aURL) {
+ let file = thumbnailFile(aURL);
+ return file.exists() && file.fileSize;
+}
+
+/**
+ * Removes the thumbnail for the given URL.
+ * @param aURL The URL associated with the thumbnail.
+ */
+function removeThumbnail(aURL) {
+ let file = thumbnailFile(aURL);
+ file.remove(false);
+}
+
+/**
+ * Calls addVisits, and then forces the newtab module to repopulate its links.
+ * See addVisits for parameter descriptions.
+ */
+function addVisitsAndRepopulateNewTabLinks(aPlaceInfo, aCallback) {
+ PlacesTestUtils.addVisits(makeURI(aPlaceInfo)).then(() => {
+ NewTabUtils.links.populateCache(aCallback, true);
+ });
+}
+function promiseAddVisitsAndRepopulateNewTabLinks(aPlaceInfo) {
+ return new Promise(resolve => addVisitsAndRepopulateNewTabLinks(aPlaceInfo, resolve));
+}
+
+/**
+ * Calls a given callback when the thumbnail for a given URL has been found
+ * on disk. Keeps trying until the thumbnail has been created.
+ *
+ * @param aURL The URL of the thumbnail's page.
+ * @param [optional] aCallback
+ * Function to be invoked on completion.
+ */
+function whenFileExists(aURL, aCallback = next) {
+ let callback = aCallback;
+ if (!thumbnailExists(aURL)) {
+ callback = () => whenFileExists(aURL, aCallback);
+ }
+
+ executeSoon(callback);
+}
+
+/**
+ * Calls a given callback when the given file has been removed.
+ * Keeps trying until the file is removed.
+ *
+ * @param aFile The file that is being removed
+ * @param [optional] aCallback
+ * Function to be invoked on completion.
+ */
+function whenFileRemoved(aFile, aCallback) {
+ let callback = aCallback;
+ if (aFile.exists()) {
+ callback = () => whenFileRemoved(aFile, aCallback);
+ }
+
+ executeSoon(callback || next);
+}
+
+function wait(aMillis) {
+ setTimeout(next, aMillis);
+}
+
+/**
+ * Makes sure that a given list of URLs is not implicitly expired.
+ *
+ * @param aURLs The list of URLs that should not be expired.
+ */
+function dontExpireThumbnailURLs(aURLs) {
+ let dontExpireURLs = (cb) => cb(aURLs);
+ PageThumbs.addExpirationFilter(dontExpireURLs);
+
+ registerCleanupFunction(function () {
+ PageThumbs.removeExpirationFilter(dontExpireURLs);
+ });
+}
+
+function bgCapture(aURL, aOptions) {
+ bgCaptureWithMethod("capture", aURL, aOptions);
+}
+
+function bgCaptureIfMissing(aURL, aOptions) {
+ bgCaptureWithMethod("captureIfMissing", aURL, aOptions);
+}
+
+function bgCaptureWithMethod(aMethodName, aURL, aOptions = {}) {
+ // We'll get oranges if the expiration filter removes the file during the
+ // test.
+ dontExpireThumbnailURLs([aURL]);
+ if (!aOptions.onDone)
+ aOptions.onDone = next;
+ BackgroundPageThumbs[aMethodName](aURL, aOptions);
+}
+
+function bgTestPageURL(aOpts = {}) {
+ let TEST_PAGE_URL = "http://mochi.test:8888/browser/toolkit/components/thumbnails/test/thumbnails_background.sjs";
+ return TEST_PAGE_URL + "?" + encodeURIComponent(JSON.stringify(aOpts));
+}
+
+function bgAddPageThumbObserver(url) {
+ return new Promise((resolve, reject) => {
+ function observe(subject, topic, data) { // jshint ignore:line
+ if (data === url) {
+ switch (topic) {
+ case "page-thumbnail:create":
+ resolve();
+ break;
+ case "page-thumbnail:error":
+ reject(new Error("page-thumbnail:error"));
+ break;
+ }
+ Services.obs.removeObserver(observe, "page-thumbnail:create");
+ Services.obs.removeObserver(observe, "page-thumbnail:error");
+ }
+ }
+ Services.obs.addObserver(observe, "page-thumbnail:create", false);
+ Services.obs.addObserver(observe, "page-thumbnail:error", false);
+ });
+}
+
+function bgAddCrashObserver() {
+ let crashed = false;
+ Services.obs.addObserver(function crashObserver(subject, topic, data) {
+ is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
+ ok(subject instanceof Components.interfaces.nsIPropertyBag2,
+ 'Subject implements nsIPropertyBag2.');
+ // we might see this called as the process terminates due to previous tests.
+ // We are only looking for "abnormal" exits...
+ if (!subject.hasKey("abnormal")) {
+ info("This is a normal termination and isn't the one we are looking for...");
+ return;
+ }
+ Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown');
+ crashed = true;
+
+ var dumpID;
+ if ('nsICrashReporter' in Components.interfaces) {
+ dumpID = subject.getPropertyAsAString('dumpID');
+ ok(dumpID, "dumpID is present and not an empty string");
+ }
+
+ if (dumpID) {
+ var minidumpDirectory = getMinidumpDirectory();
+ removeFile(minidumpDirectory, dumpID + '.dmp');
+ removeFile(minidumpDirectory, dumpID + '.extra');
+ }
+ }, 'ipc:content-shutdown', false);
+ return {
+ get crashed() {
+ return crashed;
+ }
+ };
+}
+
+function bgInjectCrashContentScript() {
+ const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js";
+ let thumbnailBrowser = BackgroundPageThumbs._thumbBrowser;
+ let mm = thumbnailBrowser.messageManager;
+ mm.loadFrameScript(TEST_CONTENT_HELPER, false);
+ return mm;
+}
+
+function getMinidumpDirectory() {
+ var dir = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile);
+ dir.append("minidumps");
+ return dir;
+}
+
+function removeFile(directory, filename) {
+ var file = directory.clone();
+ file.append(filename);
+ if (file.exists()) {
+ file.remove(false);
+ }
+}
diff --git a/toolkit/components/thumbnails/test/privacy_cache_control.sjs b/toolkit/components/thumbnails/test/privacy_cache_control.sjs
new file mode 100644
index 0000000000..6c7c16edb4
--- /dev/null
+++ b/toolkit/components/thumbnails/test/privacy_cache_control.sjs
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(aRequest, aResponse) {
+ // Set HTTP Status
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+
+ // Set Cache-Control header.
+ let value = aRequest.queryString;
+ if (value)
+ aResponse.setHeader("Cache-Control", value);
+
+ // Set the response body.
+ aResponse.write("<!DOCTYPE html><html><body></body></html>");
+}
diff --git a/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js b/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js
new file mode 100644
index 0000000000..8272b2e069
--- /dev/null
+++ b/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js
@@ -0,0 +1,31 @@
+// tests to check that moz-page-thumb URLs correctly resolve as file:// URLS
+"use strict";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// need profile so that PageThumbsStorage can resolve the path to the underlying file
+do_get_profile();
+
+function run_test() {
+ // first check the protocol handler implements the correct interface
+ let handler = Services.io.getProtocolHandler("moz-page-thumb");
+ ok(handler instanceof Ci.nsISubstitutingProtocolHandler,
+ "moz-page-thumb handler provides substituting interface");
+
+ // then check that the file URL resolution works
+ let uri = Services.io.newURI("moz-page-thumb://thumbnail/?url=http%3A%2F%2Fwww.mozilla.org%2F",
+ null, null);
+ ok(uri instanceof Ci.nsIFileURL, "moz-page-thumb:// is a FileURL");
+ ok(uri.file, "This moz-page-thumb:// object is backed by a file");
+
+ // and check that the error case works as specified
+ let bad = Services.io.newURI("moz-page-thumb://wronghost/?url=http%3A%2F%2Fwww.mozilla.org%2F",
+ null, null);
+ Assert.throws(() => handler.resolveURI(bad), /NS_ERROR_NOT_AVAILABLE/i,
+ "moz-page-thumb object with wrong host must not resolve to a file path");
+}
diff --git a/toolkit/components/thumbnails/test/thumbnails_background.sjs b/toolkit/components/thumbnails/test/thumbnails_background.sjs
new file mode 100644
index 0000000000..f1cce96a0e
--- /dev/null
+++ b/toolkit/components/thumbnails/test/thumbnails_background.sjs
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The timer never fires if it's not declared and set to this variable outside
+// handleRequest, as if it's getting GC'ed when handleRequest's scope goes away.
+// Shouldn't the timer thread hold a strong reference to it?
+var timer;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache, no-store", false);
+ resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
+
+ let opts = {};
+ try {
+ opts = JSON.parse(decodeURIComponent(req.queryString));
+ }
+ catch (err) {}
+
+ let setCookieScript = "";
+ if (opts.setRedCookie) {
+ resp.setHeader("Set-Cookie", "red", false);
+ setCookieScript = '<script>document.cookie="red";</script>';
+ }
+
+ if (opts.setGreenCookie) {
+ resp.setHeader("Set-Cookie", "green", false);
+ setCookieScript = '<script>document.cookie="green";</script>';
+ }
+
+ if (opts.iframe) {
+ setCookieScript += '<iframe src="' + opts.iframe + '" />';
+ }
+
+ if (opts.xhr) {
+ setCookieScript += `
+ <script>
+ var req = new XMLHttpRequest();
+ req.open("GET", "${opts.xhr}", true);
+ req.send();
+ </script>
+ `;
+ }
+
+ if (req.hasHeader("Cookie") &&
+ req.getHeader("Cookie").split(";").indexOf("red") >= 0) {
+ resp.write('<html style="background: #f00;">' + setCookieScript + '</html>');
+ resp.finish();
+ return;
+ }
+
+ if (req.hasHeader("Cookie") &&
+ req.getHeader("Cookie").split(";").indexOf("green") >= 0) {
+ resp.write('<html style="background: #0f0;">' + setCookieScript + '</html>');
+ resp.finish();
+ return;
+ }
+
+ if (opts.redirect) {
+ resp.setHeader("Location", opts.redirect);
+ resp.setStatusLine(null, 303, null);
+ resp.finish();
+ return;
+ }
+
+ if (opts.wait) {
+ resp.write("Waiting " + opts.wait + " ms... ");
+ timer = Components.classes["@mozilla.org/timer;1"].
+ createInstance(Components.interfaces.nsITimer);
+ timer.init(function ding() {
+ resp.write("OK!");
+ resp.finish();
+ }, opts.wait, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+
+ resp.write("<pre>" + JSON.stringify(opts, undefined, 2) + "</pre>" + setCookieScript);
+ resp.finish();
+}
diff --git a/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js b/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js
new file mode 100644
index 0000000000..935175f864
--- /dev/null
+++ b/toolkit/components/thumbnails/test/thumbnails_crash_content_helper.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var Cu = Components.utils;
+
+// Ideally we would use CrashTestUtils.jsm, but that's only available for
+// xpcshell tests - so we just copy a ctypes crasher from it.
+Cu.import("resource://gre/modules/ctypes.jsm");
+var crash = function() { // this will crash when called.
+ let zero = new ctypes.intptr_t(8);
+ let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+ badptr.contents
+};
+
+
+var TestHelper = {
+ init: function() {
+ addMessageListener("thumbnails-test:crash", this);
+ },
+
+ receiveMessage: function(msg) {
+ switch (msg.name) {
+ case "thumbnails-test:crash":
+ privateNoteIntentionalCrash();
+ crash();
+ break;
+ }
+ },
+}
+
+TestHelper.init();
diff --git a/toolkit/components/thumbnails/test/thumbnails_update.sjs b/toolkit/components/thumbnails/test/thumbnails_update.sjs
new file mode 100644
index 0000000000..4d8ab406a2
--- /dev/null
+++ b/toolkit/components/thumbnails/test/thumbnails_update.sjs
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This server-side script is used for browser_thumbnails_update. One of the
+// main things it must do in all cases is ensure a Cache-Control: no-store
+// header, so the foreground capture doesn't interfere with the testing.
+
+// If the querystring is "simple", then all it does it return some content -
+// it doesn't really matter what that content is.
+
+// Otherwise, its main role is that it must return different *content* for the
+// second request than it did for the first.
+// Also, it should be able to return an error response when requested for the
+// second response.
+// So the basic tests will be to grab the thumbnail, then request it to be
+// grabbed again:
+// * If the second request succeeded, the new thumbnail should exist.
+// * If the second request is an error, the new thumbnail should be ignored.
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ // we want to disable gBrowserThumbnails on-load capture for these responses,
+ // so set as a "no-store" response.
+ aResponse.setHeader("Cache-Control", "no-store");
+
+ // for the simple test - just return some content.
+ if (aRequest.queryString == "simple") {
+ aResponse.write("<html><body></body></html>");
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "Its simply OK");
+ return;
+ }
+
+ // it's one of the more complex tests where the first request for the given
+ // URL must return different content than the second, and possibly an error
+ // response for the second
+ let doneError = getState(aRequest.queryString);
+ if (!doneError) {
+ // first request - return a response with a green body and 200 response.
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK - It's green");
+ aResponse.write("<html><body bgcolor=00ff00></body></html>");
+ // set the state so the next request does the "second request" thing below.
+ setState(aRequest.queryString, "yep");
+ } else {
+ // second request - this will be done by the b/g service.
+ // We always return a red background, but depending on the query string we
+ // return either a 200 or 401 response.
+ if (aRequest.queryString == "fail")
+ aResponse.setStatusLine(aRequest.httpVersion, 401, "Oh no you don't");
+ else
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK - It's red");
+ aResponse.write("<html><body bgcolor=ff0000></body></html>");
+ // reset the error state incase this ends up being reused for the
+ // same url and querystring.
+ setState(aRequest.queryString, "");
+ }
+}
diff --git a/toolkit/components/thumbnails/test/xpcshell.ini b/toolkit/components/thumbnails/test/xpcshell.ini
new file mode 100644
index 0000000000..4dae8ccede
--- /dev/null
+++ b/toolkit/components/thumbnails/test/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+tail =
+
+[test_thumbnails_interfaces.js]
+skip-if = os == 'android' # xpcom interface not packaged
diff --git a/toolkit/components/timermanager/moz.build b/toolkit/components/timermanager/moz.build
new file mode 100644
index 0000000000..9977df6b5c
--- /dev/null
+++ b/toolkit/components/timermanager/moz.build
@@ -0,0 +1,21 @@
+# -*- 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_MODULE = 'update'
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIUpdateTimerManager.idl',
+]
+
+EXTRA_COMPONENTS += [
+ 'nsUpdateTimerManager.js',
+ 'nsUpdateTimerManager.manifest',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Application Update')
diff --git a/toolkit/components/timermanager/nsIUpdateTimerManager.idl b/toolkit/components/timermanager/nsIUpdateTimerManager.idl
new file mode 100644
index 0000000000..6f9e2d169e
--- /dev/null
+++ b/toolkit/components/timermanager/nsIUpdateTimerManager.idl
@@ -0,0 +1,54 @@
+/* -*- 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 nsITimerCallback;
+
+/**
+ * An interface describing a global application service that allows long
+ * duration (e.g. 1-7 or more days, weeks or months) timers to be registered
+ * and then fired.
+ */
+[scriptable, uuid(0765c92c-6145-4253-9db4-594d8023087e)]
+interface nsIUpdateTimerManager : nsISupports
+{
+ /**
+ * Register an interval with the timer manager. The timer manager
+ * periodically checks to see if the interval has expired and if it has
+ * calls the specified callback. This is persistent across application
+ * restarts and can handle intervals of long durations.
+ * @param id
+ * An id that identifies the interval, used for persistence
+ * @param callback
+ * A nsITimerCallback object that is notified when the interval
+ * expires
+ * @param interval
+ * The length of time, in seconds, of the interval
+ *
+ * Note: to avoid having to instantiate a component to call registerTimer
+ * the component can intead register an update-timer category with comma
+ * separated values as a single string representing the timer as follows.
+ *
+ * _xpcom_categories: [{ category: "update-timer",
+ * value: "contractID," +
+ * "method," +
+ * "id," +
+ * "preference," +
+ * "interval" }],
+ * the values are as follows
+ * contractID : the contract ID for the component.
+ * method : the method used to instantiate the interface. This should be
+ * either getService or createInstance depending on your
+ * component.
+ * id : the id that identifies the interval, used for persistence.
+ * preference : the preference to for timer interval. This value can be
+ * optional by specifying an empty string for the value.
+ * interval : the default interval in seconds for the timer.
+ */
+ void registerTimer(in AString id,
+ in nsITimerCallback callback,
+ in unsigned long interval);
+};
diff --git a/toolkit/components/timermanager/nsUpdateTimerManager.js b/toolkit/components/timermanager/nsUpdateTimerManager.js
new file mode 100644
index 0000000000..d7fc159602
--- /dev/null
+++ b/toolkit/components/timermanager/nsUpdateTimerManager.js
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%";
+const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay";
+const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+
+const CATEGORY_UPDATE_TIMER = "update-timer";
+
+XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function tm_gLogEnabled() {
+ return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
+});
+
+/**
+ * Gets a preference value, handling the case where there is no default.
+ * @param func
+ * The name of the preference function to call, on nsIPrefBranch
+ * @param preference
+ * The name of the preference
+ * @param defaultValue
+ * The default value to return in the event the preference has
+ * no setting
+ * @returns The value of the preference, or undefined if there was no
+ * user or default value.
+ */
+function getPref(func, preference, defaultValue) {
+ try {
+ return Services.prefs[func](preference);
+ } catch (e) {
+ }
+ return defaultValue;
+}
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console.
+ */
+function LOG(string) {
+ if (gLogEnabled) {
+ dump("*** UTM:SVC " + string + "\n");
+ Services.console.logStringMessage("UTM:SVC " + string);
+ }
+}
+
+/**
+ * A manager for timers. Manages timers that fire over long periods of time
+ * (e.g. days, weeks, months).
+ * @constructor
+ */
+function TimerManager() {
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+}
+TimerManager.prototype = {
+ /**
+ * The Checker Timer
+ */
+ _timer: null,
+
+ /**
+ * The Checker Timer minimum delay interval as specified by the
+ * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay
+ * pref doesn't exist this will default to 120000.
+ */
+ _timerMinimumDelay: null,
+
+ /**
+ * The set of registered timers.
+ */
+ _timers: { },
+
+ /**
+ * See nsIObserver.idl
+ */
+ observe: function TM_observe(aSubject, aTopic, aData) {
+ // Prevent setting the timer interval to a value of less than 30 seconds.
+ var minInterval = 30000;
+ // Prevent setting the first timer interval to a value of less than 10
+ // seconds.
+ var minFirstInterval = 10000;
+ switch (aTopic) {
+ case "utm-test-init":
+ // Enforce a minimum timer interval of 500 ms for tests and fall through
+ // to profile-after-change to initialize the timer.
+ minInterval = 500;
+ minFirstInterval = 500;
+ case "profile-after-change":
+ this._timerMinimumDelay = Math.max(1000 * getPref("getIntPref", PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120),
+ minInterval);
+ // Prevent the timer delay between notifications to other consumers from
+ // being greater than 5 minutes which is 300000 milliseconds.
+ this._timerMinimumDelay = Math.min(this._timerMinimumDelay, 300000);
+ // Prevent the first interval from being less than the value of minFirstInterval
+ let firstInterval = Math.max(getPref("getIntPref", PREF_APP_UPDATE_TIMERFIRSTINTERVAL,
+ 30000), minFirstInterval);
+ // Prevent the first interval from being greater than 2 minutes which is
+ // 120000 milliseconds.
+ firstInterval = Math.min(firstInterval, 120000);
+ // Cancel the timer if it has already been initialized. This is primarily
+ // for tests.
+ this._canEnsureTimer = true;
+ this._ensureTimer(firstInterval);
+ break;
+ case "xpcom-shutdown":
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+
+ // Release everything we hold onto.
+ this._cancelTimer();
+ for (var timerID in this._timers) {
+ delete this._timers[timerID];
+ }
+ this._timers = null;
+ break;
+ }
+ },
+
+ /**
+ * Called when the checking timer fires.
+ *
+ * We only fire one notification each time, so that the operations are
+ * staggered. We don't want too many to happen at once, which could
+ * negatively impact responsiveness.
+ *
+ * @param timer
+ * The checking timer that fired.
+ */
+ notify: function TM_notify(timer) {
+ var nextDelay = null;
+ function updateNextDelay(delay) {
+ if (nextDelay === null || delay < nextDelay) {
+ nextDelay = delay;
+ }
+ }
+
+ // Each timer calls tryFire(), which figures out which is the one that
+ // wanted to be called earliest. That one will be fired; the others are
+ // skipped and will be done later.
+ var now = Math.round(Date.now() / 1000);
+
+ var callbackToFire = null;
+ var earliestIntendedTime = null;
+ var skippedFirings = false;
+ var lastUpdateTime = null;
+ function tryFire(callback, intendedTime) {
+ var selected = false;
+ if (intendedTime <= now) {
+ if (intendedTime < earliestIntendedTime ||
+ earliestIntendedTime === null) {
+ callbackToFire = callback;
+ earliestIntendedTime = intendedTime;
+ selected = true;
+ } else if (earliestIntendedTime !== null) {
+ skippedFirings = true;
+ }
+ }
+ // We do not need to updateNextDelay for the timer that actually fires;
+ // we'll update right after it fires, with the proper intended time.
+ // Note that we might select one, then select another later (with an
+ // earlier intended time); it is still ok that we did not update for
+ // the first one, since if we have skipped firings, the next delay
+ // will be the minimum delay anyhow.
+ if (!selected) {
+ updateNextDelay(intendedTime - now);
+ }
+ }
+
+ var catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ var entries = catMan.enumerateCategory(CATEGORY_UPDATE_TIMER);
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ let value = catMan.getCategoryEntry(CATEGORY_UPDATE_TIMER, entry);
+ let [cid, method, timerID, prefInterval, defaultInterval, maxInterval] = value.split(",");
+
+ defaultInterval = parseInt(defaultInterval);
+ // cid and method are validated below when calling notify.
+ if (!timerID || !defaultInterval || isNaN(defaultInterval)) {
+ LOG("TimerManager:notify - update-timer category registered" +
+ (cid ? " for " + cid : "") + " without required parameters - " +
+ "skipping");
+ continue;
+ }
+
+ let interval = getPref("getIntPref", prefInterval, defaultInterval);
+ // Allow the update-timer category to specify a maximum value to prevent
+ // values larger than desired.
+ maxInterval = parseInt(maxInterval);
+ if (maxInterval && !isNaN(maxInterval)) {
+ interval = Math.min(interval, maxInterval);
+ }
+ let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/,
+ timerID);
+ // Initialize the last update time to 0 when the preference isn't set so
+ // the timer will be notified soon after a new profile's first use.
+ lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0);
+
+ // If the last update time is greater than the current time then reset
+ // it to 0 and the timer manager will correct the value when it fires
+ // next for this consumer.
+ if (lastUpdateTime > now) {
+ lastUpdateTime = 0;
+ }
+
+ if (lastUpdateTime == 0) {
+ Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
+ }
+
+ tryFire(function () {
+ try {
+ Components.classes[cid][method](Ci.nsITimerCallback).notify(timer);
+ LOG("TimerManager:notify - notified " + cid);
+ } catch (e) {
+ LOG("TimerManager:notify - error notifying component id: " +
+ cid + " ,error: " + e);
+ }
+ lastUpdateTime = now;
+ Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
+ updateNextDelay(lastUpdateTime + interval - now);
+ }, lastUpdateTime + interval);
+ }
+
+ for (let _timerID in this._timers) {
+ let timerID = _timerID; // necessary for the closure to work properly
+ let timerData = this._timers[timerID];
+ // If the last update time is greater than the current time then reset
+ // it to 0 and the timer manager will correct the value when it fires
+ // next for this consumer.
+ if (timerData.lastUpdateTime > now) {
+ let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID);
+ timerData.lastUpdateTime = 0;
+ Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime);
+ }
+ tryFire(function () {
+ if (timerData.callback && timerData.callback.notify) {
+ try {
+ timerData.callback.notify(timer);
+ LOG("TimerManager:notify - notified timerID: " + timerID);
+ } catch (e) {
+ LOG("TimerManager:notify - error notifying timerID: " + timerID +
+ ", error: " + e);
+ }
+ } else {
+ LOG("TimerManager:notify - timerID: " + timerID + " doesn't " +
+ "implement nsITimerCallback - skipping");
+ }
+ lastUpdateTime = now;
+ timerData.lastUpdateTime = lastUpdateTime;
+ let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, timerID);
+ Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
+ updateNextDelay(timerData.lastUpdateTime + timerData.interval - now);
+ }, timerData.lastUpdateTime + timerData.interval);
+ }
+
+ if (callbackToFire) {
+ callbackToFire();
+ }
+
+ if (nextDelay !== null) {
+ if (skippedFirings) {
+ timer.delay = this._timerMinimumDelay;
+ } else {
+ timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay);
+ }
+ this.lastTimerReset = Date.now();
+ } else {
+ this._cancelTimer();
+ }
+ },
+
+ /**
+ * Starts the timer, if necessary, and ensures that it will fire soon enough
+ * to happen after time |interval| (in milliseconds).
+ */
+ _ensureTimer: function (interval) {
+ if (!this._canEnsureTimer) {
+ return;
+ }
+ if (!this._timer) {
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timer.initWithCallback(this, interval,
+ Ci.nsITimer.TYPE_REPEATING_SLACK);
+ this.lastTimerReset = Date.now();
+ } else if (Date.now() + interval < this.lastTimerReset + this._timer.delay) {
+ this._timer.delay = Math.max(this.lastTimerReset + interval - Date.now(), 0);
+ }
+ },
+
+ /**
+ * Stops the timer, if it is running.
+ */
+ _cancelTimer: function () {
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ },
+
+ /**
+ * See nsIUpdateTimerManager.idl
+ */
+ registerTimer: function TM_registerTimer(id, callback, interval) {
+ LOG("TimerManager:registerTimer - id: " + id);
+ if (id in this._timers && callback != this._timers[id].callback) {
+ LOG("TimerManager:registerTimer - Ignoring second registration for " + id);
+ return;
+ }
+ let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id);
+ // Initialize the last update time to 0 when the preference isn't set so
+ // the timer will be notified soon after a new profile's first use.
+ let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0);
+ let now = Math.round(Date.now() / 1000);
+ if (lastUpdateTime > now) {
+ lastUpdateTime = 0;
+ }
+ if (lastUpdateTime == 0) {
+ Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
+ }
+ this._timers[id] = {callback: callback,
+ interval: interval,
+ lastUpdateTime: lastUpdateTime};
+
+ this._ensureTimer(interval * 1000);
+ },
+
+ classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateTimerManager,
+ Ci.nsITimerCallback,
+ Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TimerManager]);
diff --git a/toolkit/components/timermanager/nsUpdateTimerManager.manifest b/toolkit/components/timermanager/nsUpdateTimerManager.manifest
new file mode 100644
index 0000000000..73cb11e352
--- /dev/null
+++ b/toolkit/components/timermanager/nsUpdateTimerManager.manifest
@@ -0,0 +1,3 @@
+component {B322A5C0-A419-484E-96BA-D7182163899F} nsUpdateTimerManager.js
+contract @mozilla.org/updates/timer-manager;1 {B322A5C0-A419-484E-96BA-D7182163899F}
+category profile-after-change nsUpdateTimerManager @mozilla.org/updates/timer-manager;1
diff --git a/toolkit/components/timermanager/tests/unit/.eslintrc.js b/toolkit/components/timermanager/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/timermanager/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/timermanager/tests/unit/consumerNotifications.js b/toolkit/components/timermanager/tests/unit/consumerNotifications.js
new file mode 100644
index 0000000000..b9926e11e9
--- /dev/null
+++ b/toolkit/components/timermanager/tests/unit/consumerNotifications.js
@@ -0,0 +1,519 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* General Update Timer Manager Tests */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
+ utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const CATEGORY_UPDATE_TIMER = "update-timer";
+
+const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay";
+const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval";
+const PREF_APP_UPDATE_LOG_ALL = "app.update.log.all";
+const PREF_BRANCH_LAST_UPDATE_TIME = "app.update.lastUpdateTime.";
+
+const MAIN_TIMER_INTERVAL = 1000; // milliseconds
+const CONSUMER_TIMER_INTERVAL = 1; // seconds
+
+const TESTS = [ {
+ desc: "Test Timer Callback 0",
+ timerID: "test0-update-timer",
+ defaultInterval: "bogus",
+ prefInterval: "test0.timer.interval",
+ contractID: "@mozilla.org/test0/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("9c7ce81f-98bb-4729-adb4-4d0deb0f59e5"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 1",
+ timerID: "test1-update-timer",
+ defaultInterval: 86400,
+ prefInterval: "test1.timer.interval",
+ contractID: "@mozilla.org/test2/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("512834f3-05bb-46be-84e0-81d881a140b7"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 2",
+ timerID: "test2-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test2.timer.interval",
+ contractID: "@mozilla.org/test2/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("c8ac5027-8d11-4471-9d7c-fd692501b437"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 3",
+ timerID: "test3-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test3.timer.interval",
+ contractID: "@mozilla.org/test3/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("6b0e79f3-4ab8-414c-8f14-dde10e185727"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 4",
+ timerID: "test4-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test4.timer.interval",
+ contractID: "@mozilla.org/test4/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("2f6b7b92-e40f-4874-bfbb-eeb2412c959d"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 5",
+ timerID: "test5-update-timer",
+ defaultInterval: 86400,
+ prefInterval: "test5.timer.interval",
+ contractID: "@mozilla.org/test5/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("8a95f611-b2ac-4c7e-8b73-9748c4839731"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 6",
+ timerID: "test6-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test6.timer.interval",
+ contractID: "@mozilla.org/test6/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("2d091020-e23c-11e2-a28f-0800200c9a66"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 7",
+ timerID: "test7-update-timer",
+ defaultInterval: 86400,
+ maxInterval: CONSUMER_TIMER_INTERVAL,
+ prefInterval: "test7.timer.interval",
+ contractID: "@mozilla.org/test7/timercallback;1",
+ method: "createInstance",
+ classID: Components.ID("8e8633ae-1d70-4a7a-8bea-6e1e6c5d7742"),
+ notified: false
+}, {
+ desc: "Test Timer Callback 8",
+ timerID: "test8-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ contractID: "@mozilla.org/test8/timercallback;1",
+ classID: Components.ID("af878d4b-1d12-41f6-9a90-4e687367ecc1"),
+ notified: false,
+ lastUpdateTime: 0
+}, {
+ desc: "Test Timer Callback 9",
+ timerID: "test9-update-timer",
+ defaultInterval: CONSUMER_TIMER_INTERVAL,
+ contractID: "@mozilla.org/test9/timercallback;1",
+ classID: Components.ID("5136b201-d64c-4328-8cf1-1a63491cc117"),
+ notified: false,
+ lastUpdateTime: 0
+} ];
+
+var gUTM;
+var gNextFunc;
+
+XPCOMUtils.defineLazyServiceGetter(this, "gPref",
+ "@mozilla.org/preferences-service;1",
+ "nsIPrefBranch");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gCatMan",
+ "@mozilla.org/categorymanager;1",
+ "nsICategoryManager");
+
+XPCOMUtils.defineLazyGetter(this, "gCompReg", function () {
+ return Cm.QueryInterface(Ci.nsIComponentRegistrar);
+});
+
+const gTest0TimerCallback = {
+ notify: function T0CB_notify(aTimer) {
+ // This can happen when another notification fails and this timer having
+ // time to fire so check other timers are successful.
+ do_throw("gTest0TimerCallback notify method should not have been called");
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest0Factory = {
+ createInstance: function T0F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest0TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest1TimerCallback = {
+ notify: function T1CB_notify(aTimer) {
+ // This can happen when another notification fails and this timer having
+ // time to fire so check other timers are successful.
+ do_throw("gTest1TimerCallback notify method should not have been called");
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimer])
+};
+
+const gTest1Factory = {
+ createInstance: function T1F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest1TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest2TimerCallback = {
+ notify: function T2CB_notify(aTimer) {
+ // This can happen when another notification fails and this timer having
+ // time to fire so check other timers are successful.
+ do_throw("gTest2TimerCallback notify method should not have been called");
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest2Factory = {
+ createInstance: function T2F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest2TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest3TimerCallback = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest3Factory = {
+ createInstance: function T3F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest3TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest4TimerCallback = {
+ notify: function T4CB_notify(aTimer) {
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[4].desc, true);
+ TESTS[4].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest4Factory = {
+ createInstance: function T4F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest4TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest5TimerCallback = {
+ notify: function T5CB_notify(aTimer) {
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[5].desc, true);
+ TESTS[5].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest5Factory = {
+ createInstance: function T5F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest5TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest6TimerCallback = {
+ notify: function T6CB_notify(aTimer) {
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[6].desc, true);
+ TESTS[6].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest6Factory = {
+ createInstance: function T6F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest6TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest7TimerCallback = {
+ notify: function T7CB_notify(aTimer) {
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[7].desc, true);
+ TESTS[7].notified = true;
+ finished_test0thru7();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest7Factory = {
+ createInstance: function T7F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest7TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest8TimerCallback = {
+ notify: function T8CB_notify(aTimer) {
+ TESTS[8].notified = true;
+ TESTS[8].notifyTime = Date.now();
+ do_execute_soon(function () {
+ check_test8thru9(gTest8TimerCallback);
+ });
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest8Factory = {
+ createInstance: function T8F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest8TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+const gTest9TimerCallback = {
+ notify: function T9CB_notify(aTimer) {
+ TESTS[9].notified = true;
+ TESTS[9].notifyTime = Date.now();
+ do_execute_soon(function () {
+ check_test8thru9(gTest9TimerCallback);
+ });
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+const gTest9Factory = {
+ createInstance: function T9F_createInstance(aOuter, aIID) {
+ if (aOuter == null) {
+ return gTest9TimerCallback.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+};
+
+function run_test() {
+ do_test_pending();
+
+ // Set the timer to fire every second
+ gPref.setIntPref(PREF_APP_UPDATE_TIMERMINIMUMDELAY, MAIN_TIMER_INTERVAL / 1000);
+ gPref.setIntPref(PREF_APP_UPDATE_TIMERFIRSTINTERVAL, MAIN_TIMER_INTERVAL);
+ gPref.setBoolPref(PREF_APP_UPDATE_LOG_ALL, true);
+
+ // Remove existing update timers to prevent them from being notified
+ let entries = gCatMan.enumerateCategory(CATEGORY_UPDATE_TIMER);
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false);
+ }
+
+ gUTM = Cc["@mozilla.org/updates/timer-manager;1"].
+ getService(Ci.nsIUpdateTimerManager).
+ QueryInterface(Ci.nsIObserver);
+ gUTM.observe(null, "utm-test-init", "");
+
+ do_execute_soon(run_test0thru7);
+}
+
+function end_test() {
+ gUTM.observe(null, "xpcom-shutdown", "");
+ do_test_finished();
+}
+
+function run_test0thru7() {
+ gNextFunc = check_test0thru7;
+ // bogus default interval
+ gCompReg.registerFactory(TESTS[0].classID, TESTS[0].desc,
+ TESTS[0].contractID, gTest0Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[0].desc,
+ [TESTS[0].contractID, TESTS[0].method,
+ TESTS[0].timerID, TESTS[0].prefInterval,
+ TESTS[0].defaultInterval].join(","), false, true);
+
+ // doesn't implement nsITimerCallback
+ gCompReg.registerFactory(TESTS[1].classID, TESTS[1].desc,
+ TESTS[1].contractID, gTest1Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[1].desc,
+ [TESTS[1].contractID, TESTS[1].method,
+ TESTS[1].timerID, TESTS[1].prefInterval,
+ TESTS[1].defaultInterval].join(","), false, true);
+
+ // has a last update time of now - 43200 which is half of its interval
+ let lastUpdateTime = Math.round(Date.now() / 1000) - 43200;
+ gPref.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[2].timerID, lastUpdateTime);
+ gCompReg.registerFactory(TESTS[2].classID, TESTS[2].desc,
+ TESTS[2].contractID, gTest2Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[2].desc,
+ [TESTS[2].contractID, TESTS[2].method,
+ TESTS[2].timerID, TESTS[2].prefInterval,
+ TESTS[2].defaultInterval].join(","), false, true);
+
+ // doesn't have a notify method
+ gCompReg.registerFactory(TESTS[3].classID, TESTS[3].desc,
+ TESTS[3].contractID, gTest3Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[3].desc,
+ [TESTS[3].contractID, TESTS[3].method,
+ TESTS[3].timerID, TESTS[3].prefInterval,
+ TESTS[3].defaultInterval].join(","), false, true);
+
+ // already has a last update time
+ gPref.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[4].timerID, 1);
+ gCompReg.registerFactory(TESTS[4].classID, TESTS[4].desc,
+ TESTS[4].contractID, gTest4Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[4].desc,
+ [TESTS[4].contractID, TESTS[4].method,
+ TESTS[4].timerID, TESTS[4].prefInterval,
+ TESTS[4].defaultInterval].join(","), false, true);
+
+ // has an interval preference that overrides the default
+ gPref.setIntPref(TESTS[5].prefInterval, CONSUMER_TIMER_INTERVAL);
+ gCompReg.registerFactory(TESTS[5].classID, TESTS[5].desc,
+ TESTS[5].contractID, gTest5Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[5].desc,
+ [TESTS[5].contractID, TESTS[5].method,
+ TESTS[5].timerID, TESTS[5].prefInterval,
+ TESTS[5].defaultInterval].join(","), false, true);
+
+ // has a next update time 24 hours from now
+ let nextUpdateTime = Math.round(Date.now() / 1000) + 86400;
+ gPref.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[6].timerID, nextUpdateTime);
+ gCompReg.registerFactory(TESTS[6].classID, TESTS[6].desc,
+ TESTS[6].contractID, gTest6Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[6].desc,
+ [TESTS[6].contractID, TESTS[6].method,
+ TESTS[6].timerID, TESTS[6].prefInterval,
+ TESTS[6].defaultInterval].join(","), false, true);
+
+ // has a maximum interval set by the value of MAIN_TIMER_INTERVAL
+ gPref.setIntPref(TESTS[7].prefInterval, 86400);
+ gCompReg.registerFactory(TESTS[7].classID, TESTS[7].desc,
+ TESTS[7].contractID, gTest7Factory);
+ gCatMan.addCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[7].desc,
+ [TESTS[7].contractID, TESTS[7].method,
+ TESTS[7].timerID, TESTS[7].prefInterval,
+ TESTS[7].defaultInterval,
+ TESTS[7].maxInterval].join(","), false, true);
+}
+
+function finished_test0thru7() {
+ if (TESTS[4].notified && TESTS[5].notified && TESTS[6].notified && TESTS[7].notified) {
+ do_execute_soon(gNextFunc);
+ }
+}
+
+function check_test0thru7() {
+ Assert.ok(!TESTS[0].notified,
+ "a category registered timer didn't fire due to an invalid " +
+ "default interval");
+
+ Assert.ok(!TESTS[1].notified,
+ "a category registered timer didn't fire due to not implementing " +
+ "nsITimerCallback");
+
+ Assert.ok(!TESTS[2].notified,
+ "a category registered timer didn't fire due to the next update " +
+ "time being in the future");
+
+ Assert.ok(!TESTS[3].notified,
+ "a category registered timer didn't fire due to not having a " +
+ "notify method");
+
+ Assert.ok(TESTS[4].notified,
+ "a category registered timer has fired");
+
+ Assert.ok(TESTS[5].notified,
+ "a category registered timer fired that has an interval " +
+ "preference that overrides a default that wouldn't have fired yet");
+
+ Assert.ok(TESTS[6].notified,
+ "a category registered timer has fired due to the next update " +
+ "time being reset due to a future last update time");
+
+ Assert.ok(gPref.prefHasUserValue(PREF_BRANCH_LAST_UPDATE_TIME +
+ TESTS[4].timerID),
+ "first of two category registered timers last update time has " +
+ "a user value");
+ Assert.ok(gPref.prefHasUserValue(PREF_BRANCH_LAST_UPDATE_TIME +
+ TESTS[5].timerID),
+ "second of two category registered timers last update time has " +
+ "a user value");
+
+ // Remove the category timers that should have failed
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[0].desc, true);
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[1].desc, true);
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[2].desc, true);
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, TESTS[3].desc, true);
+ let entries = gCatMan.enumerateCategory(CATEGORY_UPDATE_TIMER);
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ gCatMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false);
+ }
+
+ entries = gCatMan.enumerateCategory(CATEGORY_UPDATE_TIMER);
+ Assert.ok(!entries.hasMoreElements(),
+ "no " + CATEGORY_UPDATE_TIMER + " categories should still be " +
+ "registered");
+
+ do_execute_soon(run_test8thru9);
+}
+
+function run_test8thru9() {
+ gPref.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[8].timerID, 1);
+ gCompReg.registerFactory(TESTS[8].classID, TESTS[8].desc,
+ TESTS[8].contractID, gTest8Factory);
+ gUTM.registerTimer(TESTS[8].timerID, gTest8TimerCallback,
+ TESTS[8].defaultInterval);
+ gPref.setIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[9].timerID, 1);
+ gCompReg.registerFactory(TESTS[9].classID, TESTS[9].desc,
+ TESTS[9].contractID, gTest9Factory);
+ gUTM.registerTimer(TESTS[9].timerID, gTest9TimerCallback,
+ TESTS[9].defaultInterval);
+}
+
+function check_test8thru9(aTestTimerCallback) {
+ aTestTimerCallback.timesCalled = (aTestTimerCallback.timesCalled || 0) + 1;
+ if (aTestTimerCallback.timesCalled < 2) {
+ return;
+ }
+
+ Assert.ok(TESTS[8].notified,
+ "first registerTimer registered timer should have fired");
+
+ Assert.ok(TESTS[9].notified,
+ "second registerTimer registered timer should have fired");
+
+ // Check that 'staggering' has happened: even though the two events wanted to fire at
+ // the same time, we waited a full MAIN_TIMER_INTERVAL between them.
+ // (to avoid sensitivity to random timing issues, we fudge by a factor of 0.5 here)
+ Assert.ok(Math.abs(TESTS[8].notifyTime - TESTS[9].notifyTime) >=
+ MAIN_TIMER_INTERVAL * 0.5,
+ "staggering between two timers that want to fire at the same " +
+ "time should have occured");
+
+ let time = gPref.getIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[8].timerID);
+ Assert.notEqual(time, 1,
+ "first registerTimer registered timer last update time " +
+ "should have been updated");
+
+ time = gPref.getIntPref(PREF_BRANCH_LAST_UPDATE_TIME + TESTS[9].timerID);
+ Assert.notEqual(time, 1,
+ "second registerTimer registered timer last update time " +
+ "should have been updated");
+
+ end_test();
+}
diff --git a/toolkit/components/timermanager/tests/unit/xpcshell.ini b/toolkit/components/timermanager/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..89e192e941
--- /dev/null
+++ b/toolkit/components/timermanager/tests/unit/xpcshell.ini
@@ -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/.
+
+[DEFAULT]
+head =
+tail =
+
+[consumerNotifications.js]
diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.js b/toolkit/components/tooltiptext/TooltipTextProvider.js
new file mode 100644
index 0000000000..a63ab83ad6
--- /dev/null
+++ b/toolkit/components/tooltiptext/TooltipTextProvider.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function TooltipTextProvider() {}
+
+TooltipTextProvider.prototype = {
+ getNodeText(tipElement, textOut, directionOut) {
+ // Don't show the tooltip if the tooltip node is a document, browser, or disconnected.
+ if (!tipElement || !tipElement.ownerDocument ||
+ tipElement.localName == "browser" ||
+ (tipElement.ownerDocument.compareDocumentPosition(tipElement) &
+ tipElement.ownerDocument.DOCUMENT_POSITION_DISCONNECTED)) {
+ return false;
+ }
+
+ var defView = tipElement.ownerDocument.defaultView;
+ // XXX Work around bug 350679:
+ // "Tooltips can be fired in documents with no view".
+ if (!defView)
+ return false;
+
+ const XLinkNS = "http://www.w3.org/1999/xlink";
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var titleText = null;
+ var XLinkTitleText = null;
+ var SVGTitleText = null;
+ var XULtooltiptextText = null;
+ var lookingForSVGTitle = true;
+ var direction = tipElement.ownerDocument.dir;
+
+ // If the element is invalid per HTML5 Forms specifications and has no title,
+ // show the constraint validation error message.
+ if ((tipElement instanceof defView.HTMLInputElement ||
+ tipElement instanceof defView.HTMLTextAreaElement ||
+ tipElement instanceof defView.HTMLSelectElement ||
+ tipElement instanceof defView.HTMLButtonElement) &&
+ !tipElement.hasAttribute('title') &&
+ (!tipElement.form || !tipElement.form.noValidate)) {
+ // If the element is barred from constraint validation or valid,
+ // the validation message will be the empty string.
+ titleText = tipElement.validationMessage || null;
+ }
+
+ // If the element is an <input type='file'> without a title, we should show
+ // the current file selection.
+ if (!titleText &&
+ tipElement instanceof defView.HTMLInputElement &&
+ tipElement.type == 'file' &&
+ !tipElement.hasAttribute('title')) {
+ let files = tipElement.files;
+
+ try {
+ var bundle =
+ Services.strings.createBundle("chrome://global/locale/layout/HtmlForm.properties");
+ if (files.length == 0) {
+ if (tipElement.multiple) {
+ titleText = bundle.GetStringFromName("NoFilesSelected");
+ } else {
+ titleText = bundle.GetStringFromName("NoFileSelected");
+ }
+ } else {
+ titleText = files[0].name;
+ // For UX and performance (jank) reasons we cap the number of
+ // files that we list in the tooltip to 20 plus a "and xxx more"
+ // line, or to 21 if exactly 21 files were picked.
+ const TRUNCATED_FILE_COUNT = 20;
+ let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
+ for (let i = 1; i < count; ++i) {
+ titleText += "\n" + files[i].name;
+ }
+ if (files.length == TRUNCATED_FILE_COUNT + 1) {
+ titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
+ } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
+ let xmoreStr = bundle.GetStringFromName("AndNMoreFiles");
+ let xmoreNum = files.length - TRUNCATED_FILE_COUNT;
+ let tmp = {};
+ Cu.import("resource://gre/modules/PluralForm.jsm", tmp);
+ let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum);
+ titleText += "\n" + andXMoreStr;
+ }
+ }
+ } catch (e) {}
+ }
+
+ // Check texts against null so that title="" can be used to undefine a
+ // title on a child element.
+ while (tipElement &&
+ (titleText == null) && (XLinkTitleText == null) &&
+ (SVGTitleText == null) && (XULtooltiptextText == null)) {
+
+ if (tipElement.nodeType == defView.Node.ELEMENT_NODE) {
+ if (tipElement.namespaceURI == XULNS)
+ XULtooltiptextText = tipElement.getAttribute("tooltiptext");
+ else if (!(tipElement instanceof defView.SVGElement))
+ titleText = tipElement.getAttribute("title");
+
+ if ((tipElement instanceof defView.HTMLAnchorElement ||
+ tipElement instanceof defView.HTMLAreaElement ||
+ tipElement instanceof defView.HTMLLinkElement ||
+ tipElement instanceof defView.SVGAElement) && tipElement.href) {
+ XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
+ }
+ if (lookingForSVGTitle &&
+ (!(tipElement instanceof defView.SVGElement) ||
+ tipElement.parentNode.nodeType == defView.Node.DOCUMENT_NODE)) {
+ lookingForSVGTitle = false;
+ }
+ if (lookingForSVGTitle) {
+ for (let childNode of tipElement.childNodes) {
+ if (childNode instanceof defView.SVGTitleElement) {
+ SVGTitleText = childNode.textContent;
+ break;
+ }
+ }
+ }
+
+ direction = defView.getComputedStyle(tipElement, "")
+ .getPropertyValue("direction");
+ }
+
+ tipElement = tipElement.parentNode;
+ }
+
+ return [titleText, XLinkTitleText, SVGTitleText, XULtooltiptextText].some(function (t) {
+ if (t && /\S/.test(t)) {
+ // Make CRLF and CR render one line break each.
+ textOut.value = t.replace(/\r\n?/g, '\n');
+ directionOut.value = direction;
+ return true;
+ }
+
+ return false;
+ });
+ },
+
+ classID : Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITooltipTextProvider]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TooltipTextProvider]);
+
diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.manifest b/toolkit/components/tooltiptext/TooltipTextProvider.manifest
new file mode 100644
index 0000000000..a7dac6cd9c
--- /dev/null
+++ b/toolkit/components/tooltiptext/TooltipTextProvider.manifest
@@ -0,0 +1,2 @@
+component {f376627f-0bbc-47b8-887e-fc92574cc91f} TooltipTextProvider.js
+contract @mozilla.org/embedcomp/default-tooltiptextprovider;1 {f376627f-0bbc-47b8-887e-fc92574cc91f}
diff --git a/toolkit/components/tooltiptext/moz.build b/toolkit/components/tooltiptext/moz.build
new file mode 100644
index 0000000000..c75e6b7a46
--- /dev/null
+++ b/toolkit/components/tooltiptext/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/.
+
+BROWSER_CHROME_MANIFESTS += ['tests/browser.ini']
+
+EXTRA_COMPONENTS += [
+ 'TooltipTextProvider.js',
+ 'TooltipTextProvider.manifest',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'General')
diff --git a/toolkit/components/tooltiptext/tests/browser.ini b/toolkit/components/tooltiptext/tests/browser.ini
new file mode 100644
index 0000000000..9896fcd2c2
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser.ini
@@ -0,0 +1,7 @@
+[browser_bug329212.js]
+support-files = title_test.svg
+[browser_bug331772_xul_tooltiptext_in_html.js]
+support-files = xul_tooltiptext.xhtml
+[browser_bug561623.js]
+[browser_bug581947.js]
+[browser_input_file_tooltips.js]
diff --git a/toolkit/components/tooltiptext/tests/browser_bug329212.js b/toolkit/components/tooltiptext/tests/browser_bug329212.js
new file mode 100644
index 0000000000..b3434eff69
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_bug329212.js
@@ -0,0 +1,35 @@
+"use strict";
+
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/title_test.svg",
+ }, function*(browser) {
+ yield ContentTask.spawn(browser, "", function() {
+ let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
+ .getService(Ci.nsITooltipTextProvider);
+ function checkElement(id, expectedTooltipText) {
+ let el = content.document.getElementById(id);
+ let textObj = {};
+ let shouldHaveTooltip = expectedTooltipText !== null;
+ is(tttp.getNodeText(el, textObj, {}), shouldHaveTooltip,
+ "element " + id + " should " + (shouldHaveTooltip ? "" : "not ") + "have a tooltip");
+ if (shouldHaveTooltip) {
+ is(textObj.value, expectedTooltipText,
+ "element " + id + " should have the right tooltip text");
+ }
+ }
+ checkElement("svg1", "This is a non-root SVG element title");
+ checkElement("text1", "\n\n\n This is a title\n\n ");
+ checkElement("text2", null);
+ checkElement("text3", null);
+ checkElement("link1", "\n This is a title\n ");
+ checkElement("text4", "\n This is a title\n ");
+ checkElement("link2", null);
+ checkElement("link3", "This is an xlink:title attribute");
+ checkElement("link4", "This is an xlink:title attribute");
+ checkElement("text5", null);
+ });
+ });
+});
+
diff --git a/toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js b/toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js
new file mode 100644
index 0000000000..23d8c4a6ef
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js
@@ -0,0 +1,19 @@
+/**
+ * Tests that the tooltiptext attribute is used for XUL elements in an HTML doc.
+ */
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml",
+ }, function*(browser) {
+ yield ContentTask.spawn(browser, "", function() {
+ let textObj = {};
+ let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
+ .getService(Ci.nsITooltipTextProvider);
+ let xulToolbarButton = content.document.getElementById("xulToolbarButton");
+ ok(tttp.getNodeText(xulToolbarButton, textObj, {}), "should get tooltiptext");
+ is(textObj.value, "XUL tooltiptext");
+ });
+ });
+});
+
diff --git a/toolkit/components/tooltiptext/tests/browser_bug561623.js b/toolkit/components/tooltiptext/tests/browser_bug561623.js
new file mode 100644
index 0000000000..49c51c4ba2
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_bug561623.js
@@ -0,0 +1,24 @@
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "data:text/html,<!DOCTYPE html><html><body><input id='i'></body></html>",
+ }, function*(browser) {
+ yield ContentTask.spawn(browser, "", function() {
+ let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
+ .getService(Ci.nsITooltipTextProvider);
+ let i = content.document.getElementById("i");
+
+ ok(!tttp.getNodeText(i, {}, {}),
+ "No tooltip should be shown when @title is null");
+
+ i.title = "foo";
+ ok(tttp.getNodeText(i, {}, {}),
+ "A tooltip should be shown when @title is not the empty string");
+
+ i.pattern = "bar";
+ ok(tttp.getNodeText(i, {}, {}),
+ "A tooltip should be shown when @title is not the empty string");
+ });
+ });
+});
+
diff --git a/toolkit/components/tooltiptext/tests/browser_bug581947.js b/toolkit/components/tooltiptext/tests/browser_bug581947.js
new file mode 100644
index 0000000000..034e0a4d13
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_bug581947.js
@@ -0,0 +1,87 @@
+function check(aBrowser, aElementName, aBarred, aType) {
+ return ContentTask.spawn(aBrowser, [aElementName, aBarred, aType], function*([aElementName, aBarred, aType]) {
+ let e = content.document.createElement(aElementName);
+ let contentElement = content.document.getElementById('content');
+ contentElement.appendChild(e);
+
+ if (aType) {
+ e.type = aType;
+ }
+
+ let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
+ .getService(Ci.nsITooltipTextProvider);
+ ok(!tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown when the element is valid");
+
+ e.setCustomValidity('foo');
+ if (aBarred) {
+ ok(!tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown when the element is barred from constraint validation");
+ } else {
+ ok(tttp.getNodeText(e, {}, {}),
+ e.tagName + " " +"A tooltip should be shown when the element isn't valid");
+ }
+
+ e.setAttribute('title', '');
+ ok (!tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown if the title attribute is set");
+
+ e.removeAttribute('title');
+ contentElement.setAttribute('novalidate', '');
+ ok (!tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown if the novalidate attribute is set on the form owner");
+ contentElement.removeAttribute('novalidate');
+
+ e.remove();
+ });
+}
+
+function todo_check(aBrowser, aElementName, aBarred) {
+ return ContentTask.spawn(aBrowser, [aElementName, aBarred], function*([aElementName, aBarred]) {
+ let e = content.document.createElement(aElementName);
+ let contentElement = content.document.getElementById('content');
+ contentElement.appendChild(e);
+
+ let caught = false;
+ try {
+ e.setCustomValidity('foo');
+ } catch (e) {
+ caught = true;
+ }
+
+ todo(!caught, "setCustomValidity should exist for " + aElementName);
+
+ e.remove();
+ });
+}
+
+add_task(function*() {
+ yield BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: "data:text/html,<!DOCTYPE html><html><body><form id='content'></form></body></html>",
+ }, function*(browser) {
+ let testData = [
+ /* element name, barred */
+ [ 'input', false, null],
+ [ 'textarea', false, null],
+ [ 'button', true, 'button'],
+ [ 'button', false, 'submit'],
+ [ 'select', false, null],
+ [ 'output', true, null],
+ [ 'fieldset', true, null],
+ [ 'object', true, null],
+ ];
+
+ for (let data of testData) {
+ yield check(browser, data[0], data[1], data[2]);
+ }
+
+ let todo_testData = [
+ [ 'keygen', 'false' ],
+ ];
+
+ for (let data of todo_testData) {
+ yield todo_check(browser, data[0], data[1]);
+ }
+ });
+});
diff --git a/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js b/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js
new file mode 100644
index 0000000000..a1323095d4
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js
@@ -0,0 +1,122 @@
+
+let tempFile;
+add_task(function* setup() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [["ui.tooltipDelay", 0]]}, resolve);
+ });
+ tempFile = createTempFile();
+ registerCleanupFunction(function() {
+ tempFile.remove(true);
+ });
+});
+
+add_task(function* test_singlefile_selected() {
+ yield do_test({value: true, result: "testfile_bug1251809"});
+});
+
+add_task(function* test_title_set() {
+ yield do_test({title: "foo", result: "foo"});
+});
+
+add_task(function* test_nofile_selected() {
+ yield do_test({result: "No file selected."});
+});
+
+add_task(function* test_multipleset_nofile_selected() {
+ yield do_test({multiple: true, result: "No files selected."});
+});
+
+add_task(function* test_requiredset() {
+ yield do_test({required: true, result: "Please select a file."});
+});
+
+function* do_test(test) {
+ info(`starting test ${JSON.stringify(test)}`);
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Moving mouse out of the way.");
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 300, 300, resolve);
+ });
+
+ info("creating input field");
+ yield ContentTask.spawn(tab.linkedBrowser, test, function*(test) {
+ let doc = content.document;
+ let input = doc.createElement("input");
+ doc.body.appendChild(input);
+ input.id = "test_input";
+ input.setAttribute("style", "position: absolute; top: 0; left: 0;");
+ input.type = "file";
+ if (test.title) {
+ input.setAttribute("title", test.title);
+ }
+ if (test.multiple) {
+ input.multiple = true;
+ }
+ if (test.required) {
+ input.required = true;
+ }
+ });
+
+ if (test.value) {
+ info("Creating mock filepicker to select files");
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", [], false);
+ MockFilePicker.returnFiles = [tempFile];
+
+ try {
+ // Open the File Picker dialog (MockFilePicker) to select
+ // the files for the test.
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#test_input", {}, tab.linkedBrowser);
+ info("Waiting for the input to have the requisite files");
+ yield ContentTask.spawn(tab.linkedBrowser, {}, function*() {
+ let input = content.document.querySelector("#test_input");
+ yield ContentTaskUtils.waitForCondition(() => input.files.length,
+ "The input should have at least one file selected");
+ info(`The input has ${input.files.length} file(s) selected.`);
+ });
+ } finally {
+ MockFilePicker.cleanup();
+ }
+ } else {
+ info("No real file selection required.");
+ }
+
+ let awaitTooltipOpen = new Promise(resolve => {
+ let tooltipId = Services.appinfo.browserTabsRemoteAutostart ?
+ "remoteBrowserTooltip" :
+ "aHTMLTooltip";
+ let tooltip = document.getElementById(tooltipId);
+ tooltip.addEventListener("popupshown", function onpopupshown(event) {
+ tooltip.removeEventListener("popupshown", onpopupshown);
+ resolve(event.target);
+ });
+ });
+ info("Initial mouse move");
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 50, 5, resolve);
+ });
+ info("Waiting");
+ yield new Promise(resolve => setTimeout(resolve, 400));
+ info("Second mouse move");
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 70, 5, resolve);
+ });
+ info("Waiting for tooltip to open");
+ let tooltip = yield awaitTooltipOpen;
+
+ is(tooltip.getAttribute("label"), test.result, "tooltip label should match expectation");
+
+ info("Closing tab");
+ yield BrowserTestUtils.removeTab(tab);
+}
+
+function createTempFile() {
+ let file = FileUtils.getDir("TmpD", [], false);
+ file.append("testfile_bug1251809");
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ return file;
+}
diff --git a/toolkit/components/tooltiptext/tests/title_test.svg b/toolkit/components/tooltiptext/tests/title_test.svg
new file mode 100644
index 0000000000..7638fd5ccb
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/title_test.svg
@@ -0,0 +1,59 @@
+<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>This is a root SVG element's title</title>
+ <foreignObject>
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <svg xmlns="http://www.w3.org/2000/svg" id="svg1">
+ <title>This is a non-root SVG element title</title>
+ </svg>
+ </body>
+ </html>
+ </foreignObject>
+ <text id="text1" x="10px" y="32px" font-size="24px">
+ This contains only &lt;title&gt;
+ <title>
+
+
+ This is a title
+
+ </title>
+ </text>
+ <text id="text2" x="10px" y="96px" font-size="24px">
+ This contains only &lt;desc&gt;
+ <desc>This is a desc</desc>
+ </text>
+ <text id="text3" x="10px" y="128px" font-size="24px" title="ignored for SVG">
+ This contains nothing.
+ </text>
+ <a id="link1" xlink:href="#">
+ This link contains &lt;title&gt;
+ <title>
+ This is a title
+ </title>
+ <text id="text4" x="10px" y="192px" font-size="24px">
+ </text>
+ </a>
+ <a id="link2" xlink:href="#">
+ <text x="10px" y="192px" font-size="24px">
+ This text contains &lt;title&gt;
+ <title>
+ This is a title
+ </title>
+ </text>
+ </a>
+ <a id="link3" xlink:href="#" xlink:title="This is an xlink:title attribute">
+ <text x="10px" y="224px" font-size="24px">
+ This link contains &lt;title&gt; &amp; xlink:title attr.
+ <title>This is a title</title>
+ </text>
+ </a>
+ <a id="link4" xlink:href="#" xlink:title="This is an xlink:title attribute">
+ <text x="10px" y="256px" font-size="24px">
+ This link contains xlink:title attr.
+ </text>
+ </a>
+ <text id="text5" x="10px" y="160px" font-size="24px"
+ xlink:title="This is an xlink:title attribute but it isn't on a link" >
+ This contains nothing.
+ </text>
+</svg>
diff --git a/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml b/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml
new file mode 100644
index 0000000000..4a80864ddc
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <xul:toolbox xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <toolbar>
+ <toolbarbutton id="xulToolbarButton"
+ tooltiptext="XUL tooltiptext"
+ title="XUL title"/>
+ </toolbar>
+ </xul:toolbox>
+</html>
+
+
diff --git a/toolkit/components/typeaheadfind/content/notfound.wav b/toolkit/components/typeaheadfind/content/notfound.wav
new file mode 100644
index 0000000000..c6fd5cb869
--- /dev/null
+++ b/toolkit/components/typeaheadfind/content/notfound.wav
Binary files differ
diff --git a/toolkit/components/typeaheadfind/jar.mn b/toolkit/components/typeaheadfind/jar.mn
new file mode 100644
index 0000000000..173caf77ad
--- /dev/null
+++ b/toolkit/components/typeaheadfind/jar.mn
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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/notfound.wav (content/notfound.wav)
diff --git a/toolkit/components/typeaheadfind/moz.build b/toolkit/components/typeaheadfind/moz.build
new file mode 100644
index 0000000000..990c1d27cb
--- /dev/null
+++ b/toolkit/components/typeaheadfind/moz.build
@@ -0,0 +1,22 @@
+# -*- 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 += [
+ 'nsITypeAheadFind.idl',
+]
+
+XPIDL_MODULE = 'fastfind'
+
+SOURCES += [
+ 'nsTypeAheadFind.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Find Toolbar')
diff --git a/toolkit/components/typeaheadfind/nsITypeAheadFind.idl b/toolkit/components/typeaheadfind/nsITypeAheadFind.idl
new file mode 100644
index 0000000000..379d2c2a26
--- /dev/null
+++ b/toolkit/components/typeaheadfind/nsITypeAheadFind.idl
@@ -0,0 +1,95 @@
+/* -*- 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/. */
+
+
+/********************************* #includes *********************************/
+
+#include "domstubs.idl" // nsIDOMElement, nsIDOMWindow
+#include "nsISupports.idl" // nsISupports
+
+
+/******************************** Declarations *******************************/
+
+interface mozIDOMWindow;
+interface nsIDocShell;
+
+
+/****************************** nsTypeAheadFind ******************************/
+
+[scriptable, uuid(ae501e28-c57f-4692-ac74-410e1bed98b7)]
+interface nsITypeAheadFind : nsISupports
+{
+ /****************************** Initializer ******************************/
+
+ /* Necessary initialization that can't happen in the constructor, either
+ * because function calls here may fail, or because the docShell is
+ * required. */
+ void init(in nsIDocShell aDocShell);
+
+
+ /***************************** Core functions ****************************/
+
+ /* Find aSearchString in page. If aLinksOnly is true, only search the page's
+ * hyperlinks for the string. */
+ unsigned short find(in AString aSearchString, in boolean aLinksOnly);
+
+ /* Find another match in the page. */
+ unsigned short findAgain(in boolean findBackwards, in boolean aLinksOnly);
+
+ /* Return the range of the most recent match. */
+ nsIDOMRange getFoundRange();
+
+
+ /**************************** Helper functions ***************************/
+
+ /* Change searched docShell. This happens when e.g. we use the same
+ * nsITypeAheadFind object to search different tabs. */
+ void setDocShell(in nsIDocShell aDocShell);
+
+ /* Change the look of the the "found match" selection to aToggle, and repaint
+ * the selection. */
+ void setSelectionModeAndRepaint(in short toggle);
+
+ /* Collapse the "found match" selection to its start. Because not all
+ * matches are owned by the same selection controller, this doesn't
+ * necessarily happen automatically. */
+ void collapseSelection();
+
+ /* Check if a range is visible */
+ boolean isRangeVisible(in nsIDOMRange aRange, in boolean aMustBeInViewPort);
+
+ /******************************* Attributes ******************************/
+
+ readonly attribute AString searchString;
+ // Most recent search string
+ attribute boolean caseSensitive; // Searches are case sensitive
+ attribute boolean entireWord; // Search for whole words only
+ readonly attribute nsIDOMElement foundLink;
+ // Most recent elem found, if a link
+ readonly attribute nsIDOMElement foundEditable;
+ // Most recent elem found, if editable
+ readonly attribute mozIDOMWindow currentWindow;
+ // Window of most recent match
+
+
+ /******************************* Constants *******************************/
+
+ /* Find return codes */
+ const unsigned short FIND_FOUND = 0;
+ // Successful find
+ const unsigned short FIND_NOTFOUND = 1;
+ // Unsuccessful find
+ const unsigned short FIND_WRAPPED = 2;
+ // Successful find, but wrapped around
+ const unsigned short FIND_PENDING = 3;
+ // Unknown status, find has not finished
+
+
+ /*************************************************************************/
+
+};
+
+
+/*****************************************************************************/
diff --git a/toolkit/components/typeaheadfind/nsTypeAheadFind.cpp b/toolkit/components/typeaheadfind/nsTypeAheadFind.cpp
new file mode 100644
index 0000000000..6746815819
--- /dev/null
+++ b/toolkit/components/typeaheadfind/nsTypeAheadFind.cpp
@@ -0,0 +1,1325 @@
+/* -*- 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 "nsCOMPtr.h"
+#include "nsMemory.h"
+#include "nsIServiceManager.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/Services.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsCURILoader.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsNetUtil.h"
+#include "nsIURL.h"
+#include "nsIURI.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsISimpleEnumerator.h"
+#include "nsPIDOMWindow.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsString.h"
+#include "nsCRT.h"
+
+#include "nsIDOMNode.h"
+#include "mozilla/dom/Element.h"
+#include "nsIFrame.h"
+#include "nsFrameTraversal.h"
+#include "nsIImageDocument.h"
+#include "nsIDOMHTMLDocument.h"
+#include "nsIDOMHTMLElement.h"
+#include "nsIDocument.h"
+#include "nsISelection.h"
+#include "nsTextFragment.h"
+#include "nsIDOMNSEditableElement.h"
+#include "nsIEditor.h"
+
+#include "nsIDocShellTreeItem.h"
+#include "nsIWebNavigation.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsContentCID.h"
+#include "nsLayoutCID.h"
+#include "nsWidgetsCID.h"
+#include "nsIFormControl.h"
+#include "nsNameSpaceManager.h"
+#include "nsIWindowWatcher.h"
+#include "nsIObserverService.h"
+#include "nsFocusManager.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Link.h"
+#include "nsRange.h"
+#include "nsXBLBinding.h"
+
+#include "nsTypeAheadFind.h"
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTypeAheadFind)
+ NS_INTERFACE_MAP_ENTRY(nsITypeAheadFind)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITypeAheadFind)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTypeAheadFind)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTypeAheadFind)
+
+NS_IMPL_CYCLE_COLLECTION(nsTypeAheadFind, mFoundLink, mFoundEditable,
+ mCurrentWindow, mStartFindRange, mSearchRange,
+ mStartPointRange, mEndPointRange, mSoundInterface,
+ mFind, mFoundRange)
+
+static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID);
+
+#define NS_FIND_CONTRACTID "@mozilla.org/embedcomp/rangefind;1"
+
+nsTypeAheadFind::nsTypeAheadFind():
+ mStartLinksOnlyPref(false),
+ mCaretBrowsingOn(false),
+ mDidAddObservers(false),
+ mLastFindLength(0),
+ mIsSoundInitialized(false),
+ mCaseSensitive(false),
+ mEntireWord(false)
+{
+}
+
+nsTypeAheadFind::~nsTypeAheadFind()
+{
+ nsCOMPtr<nsIPrefBranch> prefInternal(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefInternal) {
+ prefInternal->RemoveObserver("accessibility.typeaheadfind", this);
+ prefInternal->RemoveObserver("accessibility.browsewithcaret", this);
+ }
+}
+
+nsresult
+nsTypeAheadFind::Init(nsIDocShell* aDocShell)
+{
+ nsCOMPtr<nsIPrefBranch> prefInternal(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ mSearchRange = nullptr;
+ mStartPointRange = nullptr;
+ mEndPointRange = nullptr;
+ if (!prefInternal || !EnsureFind())
+ return NS_ERROR_FAILURE;
+
+ SetDocShell(aDocShell);
+
+ if (!mDidAddObservers) {
+ mDidAddObservers = true;
+ // ----------- Listen to prefs ------------------
+ nsresult rv = prefInternal->AddObserver("accessibility.browsewithcaret", this, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // ----------- Get initial preferences ----------
+ PrefsReset();
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+nsTypeAheadFind::PrefsReset()
+{
+ nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(prefBranch, NS_ERROR_FAILURE);
+
+ prefBranch->GetBoolPref("accessibility.typeaheadfind.startlinksonly",
+ &mStartLinksOnlyPref);
+
+ bool isSoundEnabled = true;
+ prefBranch->GetBoolPref("accessibility.typeaheadfind.enablesound",
+ &isSoundEnabled);
+ nsXPIDLCString soundStr;
+ if (isSoundEnabled)
+ prefBranch->GetCharPref("accessibility.typeaheadfind.soundURL", getter_Copies(soundStr));
+
+ mNotFoundSoundURL = soundStr;
+
+ prefBranch->GetBoolPref("accessibility.browsewithcaret",
+ &mCaretBrowsingOn);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::SetCaseSensitive(bool isCaseSensitive)
+{
+ mCaseSensitive = isCaseSensitive;
+
+ if (mFind) {
+ mFind->SetCaseSensitive(mCaseSensitive);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetCaseSensitive(bool* isCaseSensitive)
+{
+ *isCaseSensitive = mCaseSensitive;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::SetEntireWord(bool isEntireWord)
+{
+ mEntireWord = isEntireWord;
+
+ if (mFind) {
+ mFind->SetEntireWord(mEntireWord);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetEntireWord(bool* isEntireWord)
+{
+ *isEntireWord = mEntireWord;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::SetDocShell(nsIDocShell* aDocShell)
+{
+ mDocShell = do_GetWeakReference(aDocShell);
+
+ mWebBrowserFind = do_GetInterface(aDocShell);
+ NS_ENSURE_TRUE(mWebBrowserFind, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIPresShell> presShell;
+ presShell = aDocShell->GetPresShell();
+ mPresShell = do_GetWeakReference(presShell);
+
+ ReleaseStrongMemberVariables();
+ return NS_OK;
+}
+
+void
+nsTypeAheadFind::ReleaseStrongMemberVariables()
+{
+ mStartFindRange = nullptr;
+ mStartPointRange = nullptr;
+ mSearchRange = nullptr;
+ mEndPointRange = nullptr;
+
+ mFoundLink = nullptr;
+ mFoundEditable = nullptr;
+ mFoundRange = nullptr;
+ mCurrentWindow = nullptr;
+
+ mSelectionController = nullptr;
+
+ mFind = nullptr;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::SetSelectionModeAndRepaint(int16_t aToggle)
+{
+ nsCOMPtr<nsISelectionController> selectionController =
+ do_QueryReferent(mSelectionController);
+ if (!selectionController) {
+ return NS_OK;
+ }
+
+ selectionController->SetDisplaySelection(aToggle);
+ selectionController->RepaintSelection(nsISelectionController::SELECTION_NORMAL);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::CollapseSelection()
+{
+ nsCOMPtr<nsISelectionController> selectionController =
+ do_QueryReferent(mSelectionController);
+ if (!selectionController) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISelection> selection;
+ selectionController->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(selection));
+ if (selection)
+ selection->CollapseToStart();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ if (!nsCRT::strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ return PrefsReset();
+ } else if (!nsCRT::strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) &&
+ SameCOMIdentity(aSubject, mCurrentWindow)) {
+ ReleaseStrongMemberVariables();
+ }
+
+ return NS_OK;
+}
+
+void
+nsTypeAheadFind::SaveFind()
+{
+ if (mWebBrowserFind)
+ mWebBrowserFind->SetSearchString(mTypeAheadBuffer.get());
+
+ // save the length of this find for "not found" sound
+ mLastFindLength = mTypeAheadBuffer.Length();
+}
+
+void
+nsTypeAheadFind::PlayNotFoundSound()
+{
+ if (mNotFoundSoundURL.IsEmpty()) // no sound
+ return;
+
+ if (!mSoundInterface)
+ mSoundInterface = do_CreateInstance("@mozilla.org/sound;1");
+
+ if (mSoundInterface) {
+ mIsSoundInitialized = true;
+
+ if (mNotFoundSoundURL.EqualsLiteral("beep")) {
+ mSoundInterface->Beep();
+ return;
+ }
+
+ nsCOMPtr<nsIURI> soundURI;
+ if (mNotFoundSoundURL.EqualsLiteral("default"))
+ NS_NewURI(getter_AddRefs(soundURI), NS_LITERAL_CSTRING(TYPEAHEADFIND_NOTFOUND_WAV_URL));
+ else
+ NS_NewURI(getter_AddRefs(soundURI), mNotFoundSoundURL);
+
+ nsCOMPtr<nsIURL> soundURL(do_QueryInterface(soundURI));
+ if (soundURL)
+ mSoundInterface->Play(soundURL);
+ }
+}
+
+nsresult
+nsTypeAheadFind::FindItNow(nsIPresShell *aPresShell, bool aIsLinksOnly,
+ bool aIsFirstVisiblePreferred, bool aFindPrev,
+ uint16_t* aResult)
+{
+ *aResult = FIND_NOTFOUND;
+ mFoundLink = nullptr;
+ mFoundEditable = nullptr;
+ mFoundRange = nullptr;
+ mCurrentWindow = nullptr;
+ nsCOMPtr<nsIPresShell> startingPresShell (GetPresShell());
+ if (!startingPresShell) {
+ nsCOMPtr<nsIDocShell> ds = do_QueryReferent(mDocShell);
+ NS_ENSURE_TRUE(ds, NS_ERROR_FAILURE);
+
+ startingPresShell = ds->GetPresShell();
+ mPresShell = do_GetWeakReference(startingPresShell);
+ }
+
+ nsCOMPtr<nsIPresShell> presShell(aPresShell);
+
+ if (!presShell) {
+ presShell = startingPresShell; // this is the current document
+
+ if (!presShell)
+ return NS_ERROR_FAILURE;
+ }
+
+ // There could be unflushed notifications which hide textareas or other
+ // elements that we don't want to find text in.
+ presShell->FlushPendingNotifications(Flush_Layout);
+
+ RefPtr<nsPresContext> presContext = presShell->GetPresContext();
+
+ if (!presContext)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsISelection> selection;
+ nsCOMPtr<nsISelectionController> selectionController =
+ do_QueryReferent(mSelectionController);
+ if (!selectionController) {
+ GetSelection(presShell, getter_AddRefs(selectionController),
+ getter_AddRefs(selection)); // cache for reuse
+ mSelectionController = do_GetWeakReference(selectionController);
+ } else {
+ selectionController->GetSelection(
+ nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection));
+ }
+
+ nsCOMPtr<nsIDocShell> startingDocShell(presContext->GetDocShell());
+ NS_ASSERTION(startingDocShell, "Bug 175321 Crashes with Type Ahead Find [@ nsTypeAheadFind::FindItNow]");
+ if (!startingDocShell)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIDocShellTreeItem> rootContentTreeItem;
+ nsCOMPtr<nsIDocShell> currentDocShell;
+
+ startingDocShell->GetSameTypeRootTreeItem(getter_AddRefs(rootContentTreeItem));
+ nsCOMPtr<nsIDocShell> rootContentDocShell =
+ do_QueryInterface(rootContentTreeItem);
+
+ if (!rootContentDocShell)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsISimpleEnumerator> docShellEnumerator;
+ rootContentDocShell->GetDocShellEnumerator(nsIDocShellTreeItem::typeContent,
+ nsIDocShell::ENUMERATE_FORWARDS,
+ getter_AddRefs(docShellEnumerator));
+
+ // Default: can start at the current document
+ nsCOMPtr<nsISupports> currentContainer =
+ do_QueryInterface(rootContentDocShell);
+
+ // Iterate up to current shell, if there's more than 1 that we're
+ // dealing with
+ bool hasMoreDocShells;
+
+ while (NS_SUCCEEDED(docShellEnumerator->HasMoreElements(&hasMoreDocShells)) && hasMoreDocShells) {
+ docShellEnumerator->GetNext(getter_AddRefs(currentContainer));
+ currentDocShell = do_QueryInterface(currentContainer);
+ if (!currentDocShell || currentDocShell == startingDocShell || aIsFirstVisiblePreferred)
+ break;
+ }
+
+ // ------------ Get ranges ready ----------------
+ nsCOMPtr<nsIDOMRange> returnRange;
+ if (NS_FAILED(GetSearchContainers(currentContainer,
+ (!aIsFirstVisiblePreferred ||
+ mStartFindRange) ?
+ selectionController.get() : nullptr,
+ aIsFirstVisiblePreferred, aFindPrev,
+ getter_AddRefs(presShell),
+ getter_AddRefs(presContext)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int16_t rangeCompareResult = 0;
+ if (!mStartPointRange) {
+ mStartPointRange = new nsRange(presShell->GetDocument());
+ }
+
+ mStartPointRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START, mSearchRange, &rangeCompareResult);
+ // No need to wrap find in doc if starting at beginning
+ bool hasWrapped = (rangeCompareResult < 0);
+
+ if (mTypeAheadBuffer.IsEmpty() || !EnsureFind())
+ return NS_ERROR_FAILURE;
+
+ mFind->SetFindBackwards(aFindPrev);
+
+ while (true) { // ----- Outer while loop: go through all docs -----
+ while (true) { // === Inner while loop: go through a single doc ===
+ mFind->Find(mTypeAheadBuffer.get(), mSearchRange, mStartPointRange,
+ mEndPointRange, getter_AddRefs(returnRange));
+
+ if (!returnRange)
+ break; // Nothing found in this doc, go to outer loop (try next doc)
+
+ // ------- Test resulting found range for success conditions ------
+ bool isInsideLink = false, isStartingLink = false;
+
+ if (aIsLinksOnly) {
+ // Don't check if inside link when searching all text
+ RangeStartsInsideLink(returnRange, presShell, &isInsideLink,
+ &isStartingLink);
+ }
+
+ bool usesIndependentSelection;
+ if (!IsRangeVisible(presShell, presContext, returnRange,
+ aIsFirstVisiblePreferred, false,
+ getter_AddRefs(mStartPointRange),
+ &usesIndependentSelection) ||
+ (aIsLinksOnly && !isInsideLink) ||
+ (mStartLinksOnlyPref && aIsLinksOnly && !isStartingLink)) {
+ // ------ Failure ------
+ // At this point mStartPointRange got updated to the first
+ // visible range in the viewport. We _may_ be able to just
+ // start there, if it's not taking us in the wrong direction.
+ if (aFindPrev) {
+ // We can continue at the end of mStartPointRange if its end is before
+ // the start of returnRange or coincides with it. Otherwise, we need
+ // to continue at the start of returnRange.
+ int16_t compareResult;
+ nsresult rv =
+ mStartPointRange->CompareBoundaryPoints(nsIDOMRange::START_TO_END,
+ returnRange, &compareResult);
+ if (NS_SUCCEEDED(rv) && compareResult <= 0) {
+ // OK to start at the end of mStartPointRange
+ mStartPointRange->Collapse(false);
+ } else {
+ // Start at the beginning of returnRange
+ returnRange->CloneRange(getter_AddRefs(mStartPointRange));
+ mStartPointRange->Collapse(true);
+ }
+ } else {
+ // We can continue at the start of mStartPointRange if its start is
+ // after the end of returnRange or coincides with it. Otherwise, we
+ // need to continue at the end of returnRange.
+ int16_t compareResult;
+ nsresult rv =
+ mStartPointRange->CompareBoundaryPoints(nsIDOMRange::END_TO_START,
+ returnRange, &compareResult);
+ if (NS_SUCCEEDED(rv) && compareResult >= 0) {
+ // OK to start at the start of mStartPointRange
+ mStartPointRange->Collapse(true);
+ } else {
+ // Start at the end of returnRange
+ returnRange->CloneRange(getter_AddRefs(mStartPointRange));
+ mStartPointRange->Collapse(false);
+ }
+ }
+ continue;
+ }
+
+ mFoundRange = returnRange;
+
+ // ------ Success! -------
+ // Hide old selection (new one may be on a different controller)
+ if (selection) {
+ selection->CollapseToStart();
+ SetSelectionModeAndRepaint(nsISelectionController::SELECTION_ON);
+ }
+
+ // Make sure new document is selected
+ if (presShell != startingPresShell) {
+ // We are in a new document (because of frames/iframes)
+ mPresShell = do_GetWeakReference(presShell);
+ }
+
+ nsCOMPtr<nsIDocument> document =
+ do_QueryInterface(presShell->GetDocument());
+ NS_ASSERTION(document, "Wow, presShell doesn't have document!");
+ if (!document)
+ return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsPIDOMWindowInner> window = document->GetInnerWindow();
+ NS_ASSERTION(window, "document has no window");
+ if (!window)
+ return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsIFocusManager> fm = do_GetService(FOCUSMANAGER_CONTRACTID);
+ if (usesIndependentSelection) {
+ /* If a search result is found inside an editable element, we'll focus
+ * the element only if focus is in our content window, i.e.
+ * |if (focusedWindow.top == ourWindow.top)| */
+ bool shouldFocusEditableElement = false;
+ if (fm) {
+ nsCOMPtr<mozIDOMWindowProxy> focusedWindow;
+ nsresult rv = fm->GetFocusedWindow(getter_AddRefs(focusedWindow));
+ if (NS_SUCCEEDED(rv) && focusedWindow) {
+ auto* fwPI = nsPIDOMWindowOuter::From(focusedWindow);
+ nsCOMPtr<nsIDocShellTreeItem> fwTreeItem
+ (do_QueryInterface(fwPI->GetDocShell(), &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIDocShellTreeItem> fwRootTreeItem;
+ rv = fwTreeItem->GetSameTypeRootTreeItem(getter_AddRefs(fwRootTreeItem));
+ if (NS_SUCCEEDED(rv) && fwRootTreeItem == rootContentTreeItem)
+ shouldFocusEditableElement = true;
+ }
+ }
+ }
+
+ // We may be inside an editable element, and therefore the selection
+ // may be controlled by a different selection controller. Walk up the
+ // chain of parent nodes to see if we find one.
+ nsCOMPtr<nsIDOMNode> node;
+ returnRange->GetStartContainer(getter_AddRefs(node));
+ while (node) {
+ nsCOMPtr<nsIDOMNSEditableElement> editable = do_QueryInterface(node);
+ if (editable) {
+ // Inside an editable element. Get the correct selection
+ // controller and selection.
+ nsCOMPtr<nsIEditor> editor;
+ editable->GetEditor(getter_AddRefs(editor));
+ NS_ASSERTION(editor, "Editable element has no editor!");
+ if (!editor) {
+ break;
+ }
+ editor->GetSelectionController(
+ getter_AddRefs(selectionController));
+ if (selectionController) {
+ selectionController->GetSelection(
+ nsISelectionController::SELECTION_NORMAL,
+ getter_AddRefs(selection));
+ }
+ mFoundEditable = do_QueryInterface(node);
+
+ if (!shouldFocusEditableElement)
+ break;
+
+ // Otherwise move focus/caret to editable element
+ if (fm)
+ fm->SetFocus(mFoundEditable, 0);
+ break;
+ }
+ nsIDOMNode* tmp = node;
+ tmp->GetParentNode(getter_AddRefs(node));
+ }
+
+ // If we reach here without setting mFoundEditable, then something
+ // besides editable elements can cause us to have an independent
+ // selection controller. I don't know whether this is possible.
+ // Currently, we simply fall back to grabbing the document's selection
+ // controller in this case. Perhaps we should reject this find match
+ // and search again.
+ NS_ASSERTION(mFoundEditable, "Independent selection controller on "
+ "non-editable element!");
+ }
+
+ if (!mFoundEditable) {
+ // Not using a separate selection controller, so just get the
+ // document's controller and selection.
+ GetSelection(presShell, getter_AddRefs(selectionController),
+ getter_AddRefs(selection));
+ }
+ mSelectionController = do_GetWeakReference(selectionController);
+
+ // Select the found text
+ if (selection) {
+ selection->RemoveAllRanges();
+ selection->AddRange(returnRange);
+ }
+
+ if (!mFoundEditable && fm) {
+ fm->MoveFocus(window->GetOuterWindow(),
+ nullptr, nsIFocusManager::MOVEFOCUS_CARET,
+ nsIFocusManager::FLAG_NOSCROLL | nsIFocusManager::FLAG_NOSWITCHFRAME,
+ getter_AddRefs(mFoundLink));
+ }
+
+ // Change selection color to ATTENTION and scroll to it. Careful: we
+ // must wait until after we goof with focus above before changing to
+ // ATTENTION, or when we MoveFocus() and the selection is not on a
+ // link, we'll blur, which will lose the ATTENTION.
+ if (selectionController) {
+ // Beware! This may flush notifications via synchronous
+ // ScrollSelectionIntoView.
+ SetSelectionModeAndRepaint(nsISelectionController::SELECTION_ATTENTION);
+ selectionController->ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_WHOLE_SELECTION,
+ nsISelectionController::SCROLL_CENTER_VERTICALLY |
+ nsISelectionController::SCROLL_SYNCHRONOUS);
+ }
+
+ mCurrentWindow = window;
+ *aResult = hasWrapped ? FIND_WRAPPED : FIND_FOUND;
+ return NS_OK;
+ }
+
+ // ======= end-inner-while (go through a single document) ==========
+
+ // ---------- Nothing found yet, try next document -------------
+ bool hasTriedFirstDoc = false;
+ do {
+ // ==== Second inner loop - get another while ====
+ if (NS_SUCCEEDED(docShellEnumerator->HasMoreElements(&hasMoreDocShells))
+ && hasMoreDocShells) {
+ docShellEnumerator->GetNext(getter_AddRefs(currentContainer));
+ NS_ASSERTION(currentContainer, "HasMoreElements lied to us!");
+ currentDocShell = do_QueryInterface(currentContainer);
+
+ if (currentDocShell)
+ break;
+ }
+ else if (hasTriedFirstDoc) // Avoid potential infinite loop
+ return NS_ERROR_FAILURE; // No content doc shells
+
+ // Reached last doc shell, loop around back to first doc shell
+ rootContentDocShell->GetDocShellEnumerator(nsIDocShellTreeItem::typeContent,
+ nsIDocShell::ENUMERATE_FORWARDS,
+ getter_AddRefs(docShellEnumerator));
+ hasTriedFirstDoc = true;
+ } while (docShellEnumerator); // ==== end second inner while ===
+
+ bool continueLoop = false;
+ if (currentDocShell != startingDocShell)
+ continueLoop = true; // Try next document
+ else if (!hasWrapped || aIsFirstVisiblePreferred) {
+ // Finished searching through docshells:
+ // If aFirstVisiblePreferred == true, we may need to go through all
+ // docshells twice -once to look for visible matches, the second time
+ // for any match
+ aIsFirstVisiblePreferred = false;
+ hasWrapped = true;
+ continueLoop = true; // Go through all docs again
+ }
+
+ if (continueLoop) {
+ if (NS_FAILED(GetSearchContainers(currentContainer, nullptr,
+ aIsFirstVisiblePreferred, aFindPrev,
+ getter_AddRefs(presShell),
+ getter_AddRefs(presContext)))) {
+ continue;
+ }
+
+ if (aFindPrev) {
+ // Reverse mode: swap start and end points, so that we start
+ // at end of document and go to beginning
+ nsCOMPtr<nsIDOMRange> tempRange;
+ mStartPointRange->CloneRange(getter_AddRefs(tempRange));
+ if (!mEndPointRange) {
+ mEndPointRange = new nsRange(presShell->GetDocument());
+ }
+
+ mStartPointRange = mEndPointRange;
+ mEndPointRange = tempRange;
+ }
+
+ continue;
+ }
+
+ // ------------- Failed --------------
+ break;
+ } // end-outer-while: go through all docs
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetSearchString(nsAString& aSearchString)
+{
+ aSearchString = mTypeAheadBuffer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetFoundLink(nsIDOMElement** aFoundLink)
+{
+ NS_ENSURE_ARG_POINTER(aFoundLink);
+ *aFoundLink = mFoundLink;
+ NS_IF_ADDREF(*aFoundLink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetFoundEditable(nsIDOMElement** aFoundEditable)
+{
+ NS_ENSURE_ARG_POINTER(aFoundEditable);
+ *aFoundEditable = mFoundEditable;
+ NS_IF_ADDREF(*aFoundEditable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetCurrentWindow(mozIDOMWindow** aCurrentWindow)
+{
+ NS_ENSURE_ARG_POINTER(aCurrentWindow);
+ *aCurrentWindow = mCurrentWindow;
+ NS_IF_ADDREF(*aCurrentWindow);
+ return NS_OK;
+}
+
+nsresult
+nsTypeAheadFind::GetSearchContainers(nsISupports *aContainer,
+ nsISelectionController *aSelectionController,
+ bool aIsFirstVisiblePreferred,
+ bool aFindPrev,
+ nsIPresShell **aPresShell,
+ nsPresContext **aPresContext)
+{
+ NS_ENSURE_ARG_POINTER(aContainer);
+ NS_ENSURE_ARG_POINTER(aPresShell);
+ NS_ENSURE_ARG_POINTER(aPresContext);
+
+ *aPresShell = nullptr;
+ *aPresContext = nullptr;
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer));
+ if (!docShell)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
+
+ RefPtr<nsPresContext> presContext;
+ docShell->GetPresContext(getter_AddRefs(presContext));
+
+ if (!presShell || !presContext)
+ return NS_ERROR_FAILURE;
+
+ nsIDocument* doc = presShell->GetDocument();
+
+ if (!doc)
+ return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIContent> rootContent;
+ nsCOMPtr<nsIDOMHTMLDocument> htmlDoc(do_QueryInterface(doc));
+ if (htmlDoc) {
+ nsCOMPtr<nsIDOMHTMLElement> bodyEl;
+ htmlDoc->GetBody(getter_AddRefs(bodyEl));
+ rootContent = do_QueryInterface(bodyEl);
+ }
+
+ if (!rootContent)
+ rootContent = doc->GetRootElement();
+
+ nsCOMPtr<nsIDOMNode> rootNode(do_QueryInterface(rootContent));
+
+ if (!rootNode)
+ return NS_ERROR_FAILURE;
+
+ if (!mSearchRange) {
+ mSearchRange = new nsRange(doc);
+ }
+ nsCOMPtr<nsIDOMNode> searchRootNode = rootNode;
+
+ // Hack for XMLPrettyPrinter. nsFind can't handle complex anonymous content.
+ // If the root node has an XBL binding then there's not much we can do in
+ // in general, but we can try searching the binding's first child, which
+ // in the case of XMLPrettyPrinter contains the visible pretty-printed
+ // content.
+ nsXBLBinding* binding = rootContent->GetXBLBinding();
+ if (binding) {
+ nsIContent* anonContent = binding->GetAnonymousContent();
+ if (anonContent) {
+ searchRootNode = do_QueryInterface(anonContent->GetFirstChild());
+ }
+ }
+ mSearchRange->SelectNodeContents(searchRootNode);
+
+ if (!mStartPointRange) {
+ mStartPointRange = new nsRange(doc);
+ }
+ mStartPointRange->SetStart(searchRootNode, 0);
+ mStartPointRange->Collapse(true); // collapse to start
+
+ if (!mEndPointRange) {
+ mEndPointRange = new nsRange(doc);
+ }
+ nsCOMPtr<nsINode> searchRootTmp = do_QueryInterface(searchRootNode);
+ mEndPointRange->SetEnd(searchRootNode, searchRootTmp->Length());
+ mEndPointRange->Collapse(false); // collapse to end
+
+ // Consider current selection as null if
+ // it's not in the currently focused document
+ nsCOMPtr<nsIDOMRange> currentSelectionRange;
+ nsCOMPtr<nsIPresShell> selectionPresShell = GetPresShell();
+ if (aSelectionController && selectionPresShell && selectionPresShell == presShell) {
+ nsCOMPtr<nsISelection> selection;
+ aSelectionController->GetSelection(
+ nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection));
+ if (selection)
+ selection->GetRangeAt(0, getter_AddRefs(currentSelectionRange));
+ }
+
+ if (!currentSelectionRange) {
+ // Ensure visible range, move forward if necessary
+ // This uses ignores the return value, but usese the side effect of
+ // IsRangeVisible. It returns the first visible range after searchRange
+ IsRangeVisible(presShell, presContext, mSearchRange,
+ aIsFirstVisiblePreferred, true,
+ getter_AddRefs(mStartPointRange), nullptr);
+ }
+ else {
+ int32_t startOffset;
+ nsCOMPtr<nsIDOMNode> startNode;
+ if (aFindPrev) {
+ currentSelectionRange->GetStartContainer(getter_AddRefs(startNode));
+ currentSelectionRange->GetStartOffset(&startOffset);
+ } else {
+ currentSelectionRange->GetEndContainer(getter_AddRefs(startNode));
+ currentSelectionRange->GetEndOffset(&startOffset);
+ }
+ if (!startNode)
+ startNode = rootNode;
+
+ // We need to set the start point this way, other methods haven't worked
+ mStartPointRange->SelectNode(startNode);
+ mStartPointRange->SetStart(startNode, startOffset);
+ }
+
+ mStartPointRange->Collapse(true); // collapse to start
+
+ presShell.forget(aPresShell);
+ presContext.forget(aPresContext);
+
+ return NS_OK;
+}
+
+void
+nsTypeAheadFind::RangeStartsInsideLink(nsIDOMRange *aRange,
+ nsIPresShell *aPresShell,
+ bool *aIsInsideLink,
+ bool *aIsStartingLink)
+{
+ *aIsInsideLink = false;
+ *aIsStartingLink = true;
+
+ // ------- Get nsIContent to test -------
+ nsCOMPtr<nsIDOMNode> startNode;
+ nsCOMPtr<nsIContent> startContent, origContent;
+ aRange->GetStartContainer(getter_AddRefs(startNode));
+ int32_t startOffset;
+ aRange->GetStartOffset(&startOffset);
+
+ startContent = do_QueryInterface(startNode);
+ if (!startContent) {
+ NS_NOTREACHED("startContent should never be null");
+ return;
+ }
+ origContent = startContent;
+
+ if (startContent->IsElement()) {
+ nsIContent *childContent = startContent->GetChildAt(startOffset);
+ if (childContent) {
+ startContent = childContent;
+ }
+ }
+ else if (startOffset > 0) {
+ const nsTextFragment *textFrag = startContent->GetText();
+ if (textFrag) {
+ // look for non whitespace character before start offset
+ for (int32_t index = 0; index < startOffset; index++) {
+ // FIXME: take content language into account when deciding whitespace.
+ if (!mozilla::dom::IsSpaceCharacter(textFrag->CharAt(index))) {
+ *aIsStartingLink = false; // not at start of a node
+
+ break;
+ }
+ }
+ }
+ }
+
+ // ------- Check to see if inside link ---------
+
+ // We now have the correct start node for the range
+ // Search for links, starting with startNode, and going up parent chain
+
+ nsCOMPtr<nsIAtom> hrefAtom(NS_Atomize("href"));
+ nsCOMPtr<nsIAtom> typeAtom(NS_Atomize("type"));
+
+ while (true) {
+ // Keep testing while startContent is equal to something,
+ // eventually we'll run out of ancestors
+
+ if (startContent->IsHTMLElement()) {
+ nsCOMPtr<mozilla::dom::Link> link(do_QueryInterface(startContent));
+ if (link) {
+ // Check to see if inside HTML link
+ *aIsInsideLink = startContent->HasAttr(kNameSpaceID_None, hrefAtom);
+ return;
+ }
+ }
+ else {
+ // Any xml element can be an xlink
+ *aIsInsideLink = startContent->HasAttr(kNameSpaceID_XLink, hrefAtom);
+ if (*aIsInsideLink) {
+ if (!startContent->AttrValueIs(kNameSpaceID_XLink, typeAtom,
+ NS_LITERAL_STRING("simple"),
+ eCaseMatters)) {
+ *aIsInsideLink = false; // Xlink must be type="simple"
+ }
+
+ return;
+ }
+ }
+
+ // Get the parent
+ nsCOMPtr<nsIContent> parent = startContent->GetParent();
+ if (!parent)
+ break;
+
+ nsIContent* parentsFirstChild = parent->GetFirstChild();
+
+ // We don't want to look at a whitespace-only first child
+ if (parentsFirstChild && parentsFirstChild->TextIsOnlyWhitespace()) {
+ parentsFirstChild = parentsFirstChild->GetNextSibling();
+ }
+
+ if (parentsFirstChild != startContent) {
+ // startContent wasn't a first child, so we conclude that
+ // if this is inside a link, it's not at the beginning of it
+ *aIsStartingLink = false;
+ }
+
+ startContent = parent;
+ }
+
+ *aIsStartingLink = false;
+}
+
+/* Find another match in the page. */
+NS_IMETHODIMP
+nsTypeAheadFind::FindAgain(bool aFindBackwards, bool aLinksOnly,
+ uint16_t* aResult)
+
+{
+ *aResult = FIND_NOTFOUND;
+
+ if (!mTypeAheadBuffer.IsEmpty())
+ // Beware! This may flush notifications via synchronous
+ // ScrollSelectionIntoView.
+ FindItNow(nullptr, aLinksOnly, false, aFindBackwards, aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::Find(const nsAString& aSearchString, bool aLinksOnly,
+ uint16_t* aResult)
+{
+ *aResult = FIND_NOTFOUND;
+
+ nsCOMPtr<nsIPresShell> presShell (GetPresShell());
+ if (!presShell) {
+ nsCOMPtr<nsIDocShell> ds (do_QueryReferent(mDocShell));
+ NS_ENSURE_TRUE(ds, NS_ERROR_FAILURE);
+
+ presShell = ds->GetPresShell();
+ NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+ mPresShell = do_GetWeakReference(presShell);
+ }
+
+ nsCOMPtr<nsISelection> selection;
+ nsCOMPtr<nsISelectionController> selectionController =
+ do_QueryReferent(mSelectionController);
+ if (!selectionController) {
+ GetSelection(presShell, getter_AddRefs(selectionController),
+ getter_AddRefs(selection)); // cache for reuse
+ mSelectionController = do_GetWeakReference(selectionController);
+ } else {
+ selectionController->GetSelection(
+ nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection));
+ }
+
+ if (selection)
+ selection->CollapseToStart();
+
+ if (aSearchString.IsEmpty()) {
+ mTypeAheadBuffer.Truncate();
+
+ // These will be initialized to their true values after the first character
+ // is typed
+ mStartFindRange = nullptr;
+ mSelectionController = nullptr;
+
+ *aResult = FIND_FOUND;
+ return NS_OK;
+ }
+
+ bool atEnd = false;
+ if (mTypeAheadBuffer.Length()) {
+ const nsAString& oldStr = Substring(mTypeAheadBuffer, 0, mTypeAheadBuffer.Length());
+ const nsAString& newStr = Substring(aSearchString, 0, mTypeAheadBuffer.Length());
+ if (oldStr.Equals(newStr))
+ atEnd = true;
+
+ const nsAString& newStr2 = Substring(aSearchString, 0, aSearchString.Length());
+ const nsAString& oldStr2 = Substring(mTypeAheadBuffer, 0, aSearchString.Length());
+ if (oldStr2.Equals(newStr2))
+ atEnd = true;
+
+ if (!atEnd)
+ mStartFindRange = nullptr;
+ }
+
+ if (!mIsSoundInitialized && !mNotFoundSoundURL.IsEmpty()) {
+ // This makes sure system sound library is loaded so that
+ // there's no lag before the first sound is played
+ // by waiting for the first keystroke, we still get the startup time benefits.
+ mIsSoundInitialized = true;
+ mSoundInterface = do_CreateInstance("@mozilla.org/sound;1");
+ if (mSoundInterface && !mNotFoundSoundURL.EqualsLiteral("beep")) {
+ mSoundInterface->Init();
+ }
+ }
+
+#ifdef XP_WIN
+ // After each keystroke, ensure sound object is destroyed, to free up memory
+ // allocated for error sound, otherwise Windows' nsISound impl
+ // holds onto the last played sound, using up memory.
+ mSoundInterface = nullptr;
+#endif
+
+ int32_t bufferLength = mTypeAheadBuffer.Length();
+
+ mTypeAheadBuffer = aSearchString;
+
+ bool isFirstVisiblePreferred = false;
+
+ // --------- Initialize find if 1st char ----------
+ if (bufferLength == 0) {
+ // If you can see the selection (not collapsed or thru caret browsing),
+ // or if already focused on a page element, start there.
+ // Otherwise we're going to start at the first visible element
+ bool isSelectionCollapsed = true;
+ if (selection)
+ selection->GetIsCollapsed(&isSelectionCollapsed);
+
+ // If true, we will scan from top left of visible area
+ // If false, we will scan from start of selection
+ isFirstVisiblePreferred = !atEnd && !mCaretBrowsingOn && isSelectionCollapsed;
+ if (isFirstVisiblePreferred) {
+ // Get the focused content. If there is a focused node, ensure the
+ // selection is at that point. Otherwise, we will just want to start
+ // from the caret position or the beginning of the document.
+ nsPresContext* presContext = presShell->GetPresContext();
+ NS_ENSURE_TRUE(presContext, NS_OK);
+
+ nsCOMPtr<nsIDocument> document =
+ do_QueryInterface(presShell->GetDocument());
+ if (!document)
+ return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsIFocusManager> fm = do_GetService(FOCUSMANAGER_CONTRACTID);
+ if (fm) {
+ nsPIDOMWindowOuter* window = document->GetWindow();
+ nsCOMPtr<nsIDOMElement> focusedElement;
+ nsCOMPtr<mozIDOMWindowProxy> focusedWindow;
+ fm->GetFocusedElementForWindow(window, false,
+ getter_AddRefs(focusedWindow),
+ getter_AddRefs(focusedElement));
+ // If the root element is focused, then it's actually the document
+ // that has the focus, so ignore this.
+ if (focusedElement &&
+ !SameCOMIdentity(focusedElement, document->GetRootElement())) {
+ fm->MoveCaretToFocus(window);
+ isFirstVisiblePreferred = false;
+ }
+ }
+ }
+ }
+
+ // ----------- Find the text! ---------------------
+ // Beware! This may flush notifications via synchronous
+ // ScrollSelectionIntoView.
+ nsresult rv = FindItNow(nullptr, aLinksOnly, isFirstVisiblePreferred,
+ false, aResult);
+
+ // ---------Handle success or failure ---------------
+ if (NS_SUCCEEDED(rv)) {
+ if (mTypeAheadBuffer.Length() == 1) {
+ // If first letter, store where the first find succeeded
+ // (mStartFindRange)
+
+ mStartFindRange = nullptr;
+ if (selection) {
+ nsCOMPtr<nsIDOMRange> startFindRange;
+ selection->GetRangeAt(0, getter_AddRefs(startFindRange));
+ if (startFindRange)
+ startFindRange->CloneRange(getter_AddRefs(mStartFindRange));
+ }
+ }
+ }
+ else {
+ // Error sound
+ if (mTypeAheadBuffer.Length() > mLastFindLength)
+ PlayNotFoundSound();
+ }
+
+ SaveFind();
+ return NS_OK;
+}
+
+void
+nsTypeAheadFind::GetSelection(nsIPresShell *aPresShell,
+ nsISelectionController **aSelCon,
+ nsISelection **aDOMSel)
+{
+ if (!aPresShell)
+ return;
+
+ // if aCurrentNode is nullptr, get selection for document
+ *aDOMSel = nullptr;
+
+ nsPresContext* presContext = aPresShell->GetPresContext();
+
+ nsIFrame *frame = aPresShell->GetRootFrame();
+
+ if (presContext && frame) {
+ frame->GetSelectionController(presContext, aSelCon);
+ if (*aSelCon) {
+ (*aSelCon)->GetSelection(nsISelectionController::SELECTION_NORMAL,
+ aDOMSel);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::GetFoundRange(nsIDOMRange** aFoundRange)
+{
+ NS_ENSURE_ARG_POINTER(aFoundRange);
+ if (mFoundRange == nullptr) {
+ *aFoundRange = nullptr;
+ return NS_OK;
+ }
+
+ mFoundRange->CloneRange(aFoundRange);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTypeAheadFind::IsRangeVisible(nsIDOMRange *aRange,
+ bool aMustBeInViewPort,
+ bool *aResult)
+{
+ // Jump through hoops to extract the docShell from the range.
+ nsCOMPtr<nsIDOMNode> node;
+ aRange->GetStartContainer(getter_AddRefs(node));
+ nsCOMPtr<nsIDOMDocument> document;
+ node->GetOwnerDocument(getter_AddRefs(document));
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ document->GetDefaultView(getter_AddRefs(window));
+ nsCOMPtr<nsIWebNavigation> navNav (do_GetInterface(window));
+ nsCOMPtr<nsIDocShell> docShell (do_GetInterface(navNav));
+
+ // Set up the arguments needed to check if a range is visible.
+ nsCOMPtr<nsIPresShell> presShell (docShell->GetPresShell());
+ RefPtr<nsPresContext> presContext = presShell->GetPresContext();
+ nsCOMPtr<nsIDOMRange> startPointRange = new nsRange(presShell->GetDocument());
+ *aResult = IsRangeVisible(presShell, presContext, aRange,
+ aMustBeInViewPort, false,
+ getter_AddRefs(startPointRange),
+ nullptr);
+ return NS_OK;
+}
+
+bool
+nsTypeAheadFind::IsRangeVisible(nsIPresShell *aPresShell,
+ nsPresContext *aPresContext,
+ nsIDOMRange *aRange, bool aMustBeInViewPort,
+ bool aGetTopVisibleLeaf,
+ nsIDOMRange **aFirstVisibleRange,
+ bool *aUsesIndependentSelection)
+{
+ NS_ASSERTION(aPresShell && aPresContext && aRange && aFirstVisibleRange,
+ "params are invalid");
+
+ // We need to know if the range start is visible.
+ // Otherwise, return the first visible range start
+ // in aFirstVisibleRange
+
+ aRange->CloneRange(aFirstVisibleRange);
+ nsCOMPtr<nsIDOMNode> node;
+ aRange->GetStartContainer(getter_AddRefs(node));
+
+ nsCOMPtr<nsIContent> content(do_QueryInterface(node));
+ if (!content)
+ return false;
+
+ nsIFrame *frame = content->GetPrimaryFrame();
+ if (!frame)
+ return false; // No frame! Not visible then.
+
+ if (!frame->StyleVisibility()->IsVisible())
+ return false;
+
+ // Detect if we are _inside_ a text control, or something else with its own
+ // selection controller.
+ if (aUsesIndependentSelection) {
+ *aUsesIndependentSelection =
+ (frame->GetStateBits() & NS_FRAME_INDEPENDENT_SELECTION);
+ }
+
+ // ---- We have a frame ----
+ if (!aMustBeInViewPort)
+ return true; // Don't need it to be on screen, just in rendering tree
+
+ // Get the next in flow frame that contains the range start
+ int32_t startRangeOffset, startFrameOffset, endFrameOffset;
+ aRange->GetStartOffset(&startRangeOffset);
+ while (true) {
+ frame->GetOffsets(startFrameOffset, endFrameOffset);
+ if (startRangeOffset < endFrameOffset)
+ break;
+
+ nsIFrame *nextContinuationFrame = frame->GetNextContinuation();
+ if (nextContinuationFrame)
+ frame = nextContinuationFrame;
+ else
+ break;
+ }
+
+ // Set up the variables we need, return true if we can't get at them all
+ const uint16_t kMinPixels = 12;
+ nscoord minDistance = nsPresContext::CSSPixelsToAppUnits(kMinPixels);
+
+ // Get the bounds of the current frame, relative to the current view.
+ // We don't use the more accurate AccGetBounds, because that is
+ // more expensive and the STATE_OFFSCREEN flag that this is used
+ // for only needs to be a rough indicator
+ nsRectVisibility rectVisibility = nsRectVisibility_kAboveViewport;
+
+ if (!aGetTopVisibleLeaf && !frame->GetRect().IsEmpty()) {
+ rectVisibility =
+ aPresShell->GetRectVisibility(frame,
+ nsRect(nsPoint(0,0), frame->GetSize()),
+ minDistance);
+
+ if (rectVisibility != nsRectVisibility_kAboveViewport) {
+ return true;
+ }
+ }
+
+ // We know that the target range isn't usable because it's not in the
+ // view port. Move range forward to first visible point,
+ // this speeds us up a lot in long documents
+ nsCOMPtr<nsIFrameEnumerator> frameTraversal;
+ nsCOMPtr<nsIFrameTraversal> trav(do_CreateInstance(kFrameTraversalCID));
+ if (trav)
+ trav->NewFrameTraversal(getter_AddRefs(frameTraversal),
+ aPresContext, frame,
+ eLeaf,
+ false, // aVisual
+ false, // aLockInScrollView
+ false, // aFollowOOFs
+ false // aSkipPopupChecks
+ );
+
+ if (!frameTraversal)
+ return false;
+
+ while (rectVisibility == nsRectVisibility_kAboveViewport) {
+ frameTraversal->Next();
+ frame = frameTraversal->CurrentItem();
+ if (!frame)
+ return false;
+
+ if (!frame->GetRect().IsEmpty()) {
+ rectVisibility =
+ aPresShell->GetRectVisibility(frame,
+ nsRect(nsPoint(0,0), frame->GetSize()),
+ minDistance);
+ }
+ }
+
+ if (frame) {
+ nsCOMPtr<nsIDOMNode> firstVisibleNode = do_QueryInterface(frame->GetContent());
+
+ if (firstVisibleNode) {
+ frame->GetOffsets(startFrameOffset, endFrameOffset);
+ (*aFirstVisibleRange)->SetStart(firstVisibleNode, startFrameOffset);
+ (*aFirstVisibleRange)->SetEnd(firstVisibleNode, endFrameOffset);
+ }
+ }
+
+ return false;
+}
+
+already_AddRefed<nsIPresShell>
+nsTypeAheadFind::GetPresShell()
+{
+ if (!mPresShell)
+ return nullptr;
+
+ nsCOMPtr<nsIPresShell> shell = do_QueryReferent(mPresShell);
+ if (shell) {
+ nsPresContext *pc = shell->GetPresContext();
+ if (!pc || !pc->GetContainerWeak()) {
+ return nullptr;
+ }
+ }
+
+ return shell.forget();
+}
diff --git a/toolkit/components/typeaheadfind/nsTypeAheadFind.h b/toolkit/components/typeaheadfind/nsTypeAheadFind.h
new file mode 100644
index 0000000000..8ff5ad1bfe
--- /dev/null
+++ b/toolkit/components/typeaheadfind/nsTypeAheadFind.h
@@ -0,0 +1,134 @@
+/* -*- 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 "nsCycleCollectionParticipant.h"
+#include "nsISelectionController.h"
+#include "nsIController.h"
+#include "nsIControllers.h"
+#include "nsIObserver.h"
+#include "nsUnicharUtils.h"
+#include "nsIFind.h"
+#include "nsIWebBrowserFind.h"
+#include "nsWeakReference.h"
+#include "nsISelection.h"
+#include "nsIDOMRange.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsITypeAheadFind.h"
+#include "nsISound.h"
+
+class nsPIDOMWindowInner;
+class nsIPresShell;
+class nsPresContext;
+
+#define TYPEAHEADFIND_NOTFOUND_WAV_URL \
+ "chrome://global/content/notfound.wav"
+
+class nsTypeAheadFind : public nsITypeAheadFind,
+ public nsIObserver,
+ public nsSupportsWeakReference
+{
+public:
+ nsTypeAheadFind();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSITYPEAHEADFIND
+ NS_DECL_NSIOBSERVER
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsTypeAheadFind, nsITypeAheadFind)
+
+protected:
+ virtual ~nsTypeAheadFind();
+
+ nsresult PrefsReset();
+
+ void SaveFind();
+ void PlayNotFoundSound();
+ nsresult GetWebBrowserFind(nsIDocShell *aDocShell,
+ nsIWebBrowserFind **aWebBrowserFind);
+
+ void RangeStartsInsideLink(nsIDOMRange *aRange, nsIPresShell *aPresShell,
+ bool *aIsInsideLink, bool *aIsStartingLink);
+
+ void GetSelection(nsIPresShell *aPresShell, nsISelectionController **aSelCon,
+ nsISelection **aDomSel);
+ // *aNewRange may not be collapsed. If you want to collapse it in a
+ // particular way, you need to do it yourself.
+ bool IsRangeVisible(nsIPresShell *aPresShell, nsPresContext *aPresContext,
+ nsIDOMRange *aRange, bool aMustBeVisible,
+ bool aGetTopVisibleLeaf, nsIDOMRange **aNewRange,
+ bool *aUsesIndependentSelection);
+ nsresult FindItNow(nsIPresShell *aPresShell, bool aIsLinksOnly,
+ bool aIsFirstVisiblePreferred, bool aFindPrev,
+ uint16_t* aResult);
+ nsresult GetSearchContainers(nsISupports *aContainer,
+ nsISelectionController *aSelectionController,
+ bool aIsFirstVisiblePreferred,
+ bool aFindPrev, nsIPresShell **aPresShell,
+ nsPresContext **aPresContext);
+
+ // Get the pres shell from mPresShell and return it only if it is still
+ // attached to the DOM window.
+ already_AddRefed<nsIPresShell> GetPresShell();
+
+ void ReleaseStrongMemberVariables();
+
+ // Current find state
+ nsString mTypeAheadBuffer;
+ nsCString mNotFoundSoundURL;
+
+ // PRBools are used instead of PRPackedBools because the address of the
+ // boolean variable is getting passed into a method.
+ bool mStartLinksOnlyPref;
+ bool mCaretBrowsingOn;
+ bool mDidAddObservers;
+ nsCOMPtr<nsIDOMElement> mFoundLink; // Most recent elem found, if a link
+ nsCOMPtr<nsIDOMElement> mFoundEditable; // Most recent elem found, if editable
+ nsCOMPtr<nsIDOMRange> mFoundRange; // Most recent range found
+ nsCOMPtr<nsPIDOMWindowInner> mCurrentWindow;
+ // mLastFindLength is the character length of the last find string. It is used for
+ // disabling the "not found" sound when using backspace or delete
+ uint32_t mLastFindLength;
+
+ // Sound is played asynchronously on some platforms.
+ // If we destroy mSoundInterface before sound has played, it won't play
+ nsCOMPtr<nsISound> mSoundInterface;
+ bool mIsSoundInitialized;
+
+ // where selection was when user started the find
+ nsCOMPtr<nsIDOMRange> mStartFindRange;
+ nsCOMPtr<nsIDOMRange> mSearchRange;
+ nsCOMPtr<nsIDOMRange> mStartPointRange;
+ nsCOMPtr<nsIDOMRange> mEndPointRange;
+
+ // Cached useful interfaces
+ nsCOMPtr<nsIFind> mFind;
+
+ bool mCaseSensitive;
+ bool mEntireWord;
+
+ bool EnsureFind() {
+ if (mFind) {
+ return true;
+ }
+
+ mFind = do_CreateInstance("@mozilla.org/embedcomp/rangefind;1");
+ if (!mFind) {
+ return false;
+ }
+
+ mFind->SetCaseSensitive(mCaseSensitive);
+ mFind->SetEntireWord(mEntireWord);
+
+ return true;
+ }
+
+ nsCOMPtr<nsIWebBrowserFind> mWebBrowserFind;
+
+ // The focused content window that we're listening to and its cached objects
+ nsWeakPtr mDocShell;
+ nsWeakPtr mPresShell;
+ nsWeakPtr mSelectionController;
+ // Most recent match's controller
+};
diff --git a/toolkit/components/url-classifier/ChunkSet.cpp b/toolkit/components/url-classifier/ChunkSet.cpp
new file mode 100644
index 0000000000..162a74260c
--- /dev/null
+++ b/toolkit/components/url-classifier/ChunkSet.cpp
@@ -0,0 +1,278 @@
+//* -*- 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 "ChunkSet.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+const size_t ChunkSet::IO_BUFFER_SIZE;
+
+nsresult
+ChunkSet::Serialize(nsACString& aChunkStr)
+{
+ aChunkStr.Truncate();
+ for (const Range* range = mRanges.begin(); range != mRanges.end(); range++) {
+ if (range != mRanges.begin()) {
+ aChunkStr.Append(',');
+ }
+
+ aChunkStr.AppendInt((int32_t)range->Begin());
+ if (range->Begin() != range->End()) {
+ aChunkStr.Append('-');
+ aChunkStr.AppendInt((int32_t)range->End());
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ChunkSet::Set(uint32_t aChunk)
+{
+ if (!Has(aChunk)) {
+ Range chunkRange(aChunk, aChunk);
+
+ if (mRanges.Length() == 0) {
+ if (!mRanges.AppendElement(chunkRange, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+ }
+
+ if (mRanges.LastElement().Precedes(chunkRange)) {
+ mRanges.LastElement().End(aChunk);
+ } else if (chunkRange.Precedes(mRanges[0])) {
+ mRanges[0].Begin(aChunk);
+ } else {
+ ChunkSet tmp;
+ if (!tmp.mRanges.AppendElement(chunkRange, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return Merge(tmp);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+ChunkSet::Has(uint32_t aChunk) const
+{
+ size_t idx;
+ return BinarySearchIf(mRanges, 0, mRanges.Length(),
+ Range::IntersectionComparator(Range(aChunk, aChunk)),
+ &idx);
+ // IntersectionComparator works because we create a
+ // single-chunk range.
+}
+
+nsresult
+ChunkSet::Merge(const ChunkSet& aOther)
+{
+ size_t oldLen = mRanges.Length();
+
+ for (const Range* mergeRange = aOther.mRanges.begin();
+ mergeRange != aOther.mRanges.end(); mergeRange++) {
+ if (!HasSubrange(*mergeRange)) {
+ if (!mRanges.InsertElementSorted(*mergeRange, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+
+ if (oldLen < mRanges.Length()) {
+ for (size_t i = 1; i < mRanges.Length(); i++) {
+ while (mRanges[i - 1].FoldLeft(mRanges[i])) {
+ mRanges.RemoveElementAt(i);
+
+ if (i == mRanges.Length()) {
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+uint32_t
+ChunkSet::Length() const
+{
+ uint32_t len = 0;
+ for (const Range* range = mRanges.begin(); range != mRanges.end(); range++) {
+ len += range->Length();
+ }
+
+ return len;
+}
+
+nsresult
+ChunkSet::Remove(const ChunkSet& aOther)
+{
+ for (const Range* removalRange = aOther.mRanges.begin();
+ removalRange != aOther.mRanges.end(); removalRange++) {
+
+ if (mRanges.Length() == 0) {
+ return NS_OK;
+ }
+
+ if (mRanges.LastElement().End() < removalRange->Begin() ||
+ aOther.mRanges.LastElement().End() < mRanges[0].Begin()) {
+ return NS_OK;
+ }
+
+ size_t intersectionIdx;
+ while (BinarySearchIf(mRanges, 0, mRanges.Length(),
+ Range::IntersectionComparator(*removalRange), &intersectionIdx)) {
+
+ ChunkSet remains;
+ nsresult rv = mRanges[intersectionIdx].Remove(*removalRange, remains);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mRanges.RemoveElementAt(intersectionIdx);
+ if (!mRanges.InsertElementsAt(intersectionIdx, remains.mRanges,
+ fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+void
+ChunkSet::Clear()
+{
+ mRanges.Clear();
+}
+
+nsresult
+ChunkSet::Write(nsIOutputStream* aOut)
+{
+ nsTArray<uint32_t> chunks(IO_BUFFER_SIZE);
+
+ for (const Range* range = mRanges.begin(); range != mRanges.end(); range++) {
+ for (uint32_t chunk = range->Begin(); chunk <= range->End(); chunk++) {
+ chunks.AppendElement(chunk);
+
+ if (chunks.Length() == chunks.Capacity()) {
+ nsresult rv = WriteTArray(aOut, chunks);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ chunks.Clear();
+ }
+ }
+ }
+
+ nsresult rv = WriteTArray(aOut, chunks);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ChunkSet::Read(nsIInputStream* aIn, uint32_t aNumElements)
+{
+ nsTArray<uint32_t> chunks(IO_BUFFER_SIZE);
+
+ while (aNumElements != 0) {
+ chunks.Clear();
+
+ uint32_t numToRead = aNumElements > IO_BUFFER_SIZE ? IO_BUFFER_SIZE : aNumElements;
+
+ nsresult rv = ReadTArray(aIn, &chunks, numToRead);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aNumElements -= numToRead;
+
+ for (const uint32_t* c = chunks.begin(); c != chunks.end(); c++) {
+ rv = Set(*c);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+ChunkSet::HasSubrange(const Range& aSubrange) const
+{
+ for (const Range* range = mRanges.begin(); range != mRanges.end(); range++) {
+ if (range->Contains(aSubrange)) {
+ return true;
+ } else if (!(aSubrange.Begin() > range->End() ||
+ range->Begin() > aSubrange.End())) {
+ // In this case, aSubrange overlaps this range but is not a subrange.
+ // because the ChunkSet implementation ensures that there are no
+ // overlapping ranges, this means that aSubrange cannot be a subrange of
+ // any of the following ranges
+ return false;
+ }
+ }
+
+ return false;
+}
+
+uint32_t
+ChunkSet::Range::Length() const
+{
+ return mEnd - mBegin + 1;
+}
+
+nsresult
+ChunkSet::Range::Remove(const Range& aRange, ChunkSet& aRemainderSet) const
+{
+ if (mBegin < aRange.mBegin && aRange.mBegin <= mEnd) {
+ // aRange overlaps & follows this range
+ Range range(mBegin, aRange.mBegin - 1);
+ if (!aRemainderSet.mRanges.AppendElement(range, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ if (mBegin <= aRange.mEnd && aRange.mEnd < mEnd) {
+ // aRange overlaps & precedes this range
+ Range range(aRange.mEnd + 1, mEnd);
+ if (!aRemainderSet.mRanges.AppendElement(range, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+ChunkSet::Range::FoldLeft(const Range& aRange)
+{
+ if (Contains(aRange)) {
+ return true;
+ } else if (Precedes(aRange) ||
+ (mBegin <= aRange.mBegin && aRange.mBegin <= mEnd)) {
+ mEnd = aRange.mEnd;
+ return true;
+ }
+
+ return false;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/ChunkSet.h b/toolkit/components/url-classifier/ChunkSet.h
new file mode 100644
index 0000000000..fc7c1eb15c
--- /dev/null
+++ b/toolkit/components/url-classifier/ChunkSet.h
@@ -0,0 +1,99 @@
+//* -*- 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/. */
+
+#ifndef ChunkSet_h__
+#define ChunkSet_h__
+
+#include "Entries.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+/**
+ * Store the chunk numbers as an array of ranges of uint32_t.
+ * We need chunk numbers in order to ask for incremental updates from the
+ * server.
+ */
+class ChunkSet {
+public:
+ nsresult Serialize(nsACString& aStr);
+ nsresult Set(uint32_t aChunk);
+ bool Has(uint32_t chunk) const;
+ nsresult Merge(const ChunkSet& aOther);
+ uint32_t Length() const;
+ nsresult Remove(const ChunkSet& aOther);
+ void Clear();
+
+ nsresult Write(nsIOutputStream* aOut);
+ nsresult Read(nsIInputStream* aIn, uint32_t aNumElements);
+
+private:
+ class Range {
+ public:
+ Range(uint32_t aBegin, uint32_t aEnd) : mBegin(aBegin), mEnd(aEnd) {}
+
+ uint32_t Length() const;
+ nsresult Remove(const Range& aRange, ChunkSet& aRemainderSet) const;
+ bool FoldLeft(const Range& aRange);
+
+ bool operator==(const Range& rhs) const {
+ return mBegin == rhs.mBegin;
+ }
+ bool operator<(const Range& rhs) const {
+ return mBegin < rhs.mBegin;
+ }
+
+ uint32_t Begin() const {
+ return mBegin;
+ }
+ void Begin(const uint32_t aBegin) {
+ mBegin = aBegin;
+ }
+ uint32_t End() const {
+ return mEnd;
+ }
+ void End(const uint32_t aEnd) {
+ mEnd = aEnd;
+ }
+
+ bool Contains(const Range& aRange) const {
+ return mBegin <= aRange.mBegin && aRange.mEnd <= mEnd;
+ }
+ bool Precedes(const Range& aRange) const {
+ return mEnd + 1 == aRange.mBegin;
+ }
+
+ struct IntersectionComparator {
+ int operator()(const Range& aRange) const {
+ if (aRange.mBegin > mTarget.mEnd) {
+ return -1;
+ }
+ if (mTarget.mBegin > aRange.mEnd) {
+ return 1;
+ }
+ return 0;
+ }
+
+ explicit IntersectionComparator(const Range& aTarget) : mTarget(aTarget){}
+ const Range& mTarget;
+ };
+
+ private:
+ uint32_t mBegin;
+ uint32_t mEnd;
+ };
+
+ static const size_t IO_BUFFER_SIZE = 1024;
+ FallibleTArray<Range> mRanges;
+
+ bool HasSubrange(const Range& aSubrange) const;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/Classifier.cpp b/toolkit/components/url-classifier/Classifier.cpp
new file mode 100644
index 0000000000..bbaeaf5352
--- /dev/null
+++ b/toolkit/components/url-classifier/Classifier.cpp
@@ -0,0 +1,1281 @@
+//* -*- 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 "Classifier.h"
+#include "LookupCacheV4.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIRandomGenerator.h"
+#include "nsIInputStream.h"
+#include "nsISeekableStream.h"
+#include "nsIFile.h"
+#include "nsNetCID.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/Base64.h"
+#include "mozilla/Unused.h"
+#include "mozilla/TypedEnumBits.h"
+#include "nsIUrlClassifierUtils.h"
+#include "nsUrlClassifierDBService.h"
+
+// MOZ_LOG=UrlClassifierDbService:5
+extern mozilla::LazyLogModule gUrlClassifierDbServiceLog;
+#define LOG(args) MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug)
+
+#define STORE_DIRECTORY NS_LITERAL_CSTRING("safebrowsing")
+#define TO_DELETE_DIR_SUFFIX NS_LITERAL_CSTRING("-to_delete")
+#define BACKUP_DIR_SUFFIX NS_LITERAL_CSTRING("-backup")
+
+#define METADATA_SUFFIX NS_LITERAL_CSTRING(".metadata")
+
+namespace mozilla {
+namespace safebrowsing {
+
+namespace {
+
+// A scoped-clearer for nsTArray<TableUpdate*>.
+// The owning elements will be deleted and the array itself
+// will be cleared on exiting the scope.
+class ScopedUpdatesClearer {
+public:
+ explicit ScopedUpdatesClearer(nsTArray<TableUpdate*> *aUpdates)
+ : mUpdatesArrayRef(aUpdates)
+ {
+ for (auto update : *aUpdates) {
+ mUpdatesPointerHolder.AppendElement(update);
+ }
+ }
+
+ ~ScopedUpdatesClearer()
+ {
+ mUpdatesArrayRef->Clear();
+ }
+
+private:
+ nsTArray<TableUpdate*>* mUpdatesArrayRef;
+ nsTArray<nsAutoPtr<TableUpdate>> mUpdatesPointerHolder;
+};
+
+} // End of unnamed namespace.
+
+void
+Classifier::SplitTables(const nsACString& str, nsTArray<nsCString>& tables)
+{
+ tables.Clear();
+
+ nsACString::const_iterator begin, iter, end;
+ str.BeginReading(begin);
+ str.EndReading(end);
+ while (begin != end) {
+ iter = begin;
+ FindCharInReadable(',', iter, end);
+ nsDependentCSubstring table = Substring(begin,iter);
+ if (!table.IsEmpty()) {
+ tables.AppendElement(Substring(begin, iter));
+ }
+ begin = iter;
+ if (begin != end) {
+ begin++;
+ }
+ }
+}
+
+nsresult
+Classifier::GetPrivateStoreDirectory(nsIFile* aRootStoreDirectory,
+ const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile** aPrivateStoreDirectory)
+{
+ NS_ENSURE_ARG_POINTER(aPrivateStoreDirectory);
+
+ if (!StringEndsWith(aTableName, NS_LITERAL_CSTRING("-proto"))) {
+ // Only V4 table names (ends with '-proto') would be stored
+ // to per-provider sub-directory.
+ nsCOMPtr<nsIFile>(aRootStoreDirectory).forget(aPrivateStoreDirectory);
+ return NS_OK;
+ }
+
+ if (aProvider.IsEmpty()) {
+ // When failing to get provider, just store in the root folder.
+ nsCOMPtr<nsIFile>(aRootStoreDirectory).forget(aPrivateStoreDirectory);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> providerDirectory;
+
+ // Clone first since we are gonna create a new directory.
+ nsresult rv = aRootStoreDirectory->Clone(getter_AddRefs(providerDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Append the provider name to the root store directory.
+ rv = providerDirectory->AppendNative(aProvider);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure existence of the provider directory.
+ bool dirExists;
+ rv = providerDirectory->Exists(&dirExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!dirExists) {
+ LOG(("Creating private directory for %s", nsCString(aTableName).get()));
+ rv = providerDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ providerDirectory.forget(aPrivateStoreDirectory);
+ return rv;
+ }
+
+ // Store directory exists. Check if it's a directory.
+ bool isDir;
+ rv = providerDirectory->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) {
+ return NS_ERROR_FILE_DESTINATION_NOT_DIR;
+ }
+
+ providerDirectory.forget(aPrivateStoreDirectory);
+
+ return NS_OK;
+}
+
+Classifier::Classifier()
+{
+}
+
+Classifier::~Classifier()
+{
+ Close();
+}
+
+nsresult
+Classifier::SetupPathNames()
+{
+ // Get the root directory where to store all the databases.
+ nsresult rv = mCacheDirectory->Clone(getter_AddRefs(mRootStoreDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mRootStoreDirectory->AppendNative(STORE_DIRECTORY);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make sure LookupCaches (which are persistent and survive updates)
+ // are reading/writing in the right place. We will be moving their
+ // files "underneath" them during backup/restore.
+ for (uint32_t i = 0; i < mLookupCaches.Length(); i++) {
+ mLookupCaches[i]->UpdateRootDirHandle(mRootStoreDirectory);
+ }
+
+ // Directory where to move a backup before an update.
+ rv = mCacheDirectory->Clone(getter_AddRefs(mBackupDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mBackupDirectory->AppendNative(STORE_DIRECTORY + BACKUP_DIR_SUFFIX);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Directory where to move the backup so we can atomically
+ // delete (really move) it.
+ rv = mCacheDirectory->Clone(getter_AddRefs(mToDeleteDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mToDeleteDirectory->AppendNative(STORE_DIRECTORY + TO_DELETE_DIR_SUFFIX);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::CreateStoreDirectory()
+{
+ // Ensure the safebrowsing directory exists.
+ bool storeExists;
+ nsresult rv = mRootStoreDirectory->Exists(&storeExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!storeExists) {
+ rv = mRootStoreDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ bool storeIsDir;
+ rv = mRootStoreDirectory->IsDirectory(&storeIsDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!storeIsDir)
+ return NS_ERROR_FILE_DESTINATION_NOT_DIR;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::Open(nsIFile& aCacheDirectory)
+{
+ // Remember the Local profile directory.
+ nsresult rv = aCacheDirectory.Clone(getter_AddRefs(mCacheDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the handles to the update and backup directories.
+ rv = SetupPathNames();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Clean up any to-delete directories that haven't been deleted yet.
+ rv = CleanToDelete();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check whether we have an incomplete update and recover from the
+ // backup if so.
+ rv = RecoverBackups();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make sure the main store directory exists.
+ rv = CreateStoreDirectory();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build the list of know urlclassifier lists
+ // XXX: Disk IO potentially on the main thread during startup
+ RegenActiveTables();
+
+ return NS_OK;
+}
+
+void
+Classifier::Close()
+{
+ DropStores();
+}
+
+void
+Classifier::Reset()
+{
+ DropStores();
+
+ mRootStoreDirectory->Remove(true);
+ mBackupDirectory->Remove(true);
+ mToDeleteDirectory->Remove(true);
+
+ CreateStoreDirectory();
+
+ mTableFreshness.Clear();
+ RegenActiveTables();
+}
+
+void
+Classifier::ResetTables(ClearType aType, const nsTArray<nsCString>& aTables)
+{
+ for (uint32_t i = 0; i < aTables.Length(); i++) {
+ LOG(("Resetting table: %s", aTables[i].get()));
+ // Spoil this table by marking it as no known freshness
+ mTableFreshness.Remove(aTables[i]);
+ LookupCache *cache = GetLookupCache(aTables[i]);
+ if (cache) {
+ // Remove any cached Completes for this table if clear type is Clear_Cache
+ if (aType == Clear_Cache) {
+ cache->ClearCache();
+ } else {
+ cache->ClearAll();
+ }
+ }
+ }
+
+ // Clear on-disk database if clear type is Clear_All
+ if (aType == Clear_All) {
+ DeleteTables(mRootStoreDirectory, aTables);
+
+ RegenActiveTables();
+ }
+}
+
+void
+Classifier::DeleteTables(nsIFile* aDirectory, const nsTArray<nsCString>& aTables)
+{
+ nsCOMPtr<nsISimpleEnumerator> entries;
+ nsresult rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = entries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> supports;
+ rv = entries->GetNext(getter_AddRefs(supports));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIFile> file = do_QueryInterface(supports);
+ NS_ENSURE_TRUE_VOID(file);
+
+ // If |file| is a directory, recurse to find its entries as well.
+ bool isDirectory;
+ if (NS_FAILED(file->IsDirectory(&isDirectory))) {
+ continue;
+ }
+ if (isDirectory) {
+ DeleteTables(file, aTables);
+ continue;
+ }
+
+ nsCString leafName;
+ rv = file->GetNativeLeafName(leafName);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ leafName.Truncate(leafName.RFind("."));
+ if (aTables.Contains(leafName)) {
+ if (NS_FAILED(file->Remove(false))) {
+ NS_WARNING(nsPrintfCString("Fail to remove file %s from the disk",
+ leafName.get()).get());
+ }
+ }
+ }
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+void
+Classifier::AbortUpdateAndReset(const nsCString& aTable)
+{
+ // We don't need to reset while shutting down. It will only slow us down.
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return;
+ }
+
+ LOG(("Abort updating table %s.", aTable.get()));
+
+ // ResetTables will clear both in-memory & on-disk data.
+ ResetTables(Clear_All, nsTArray<nsCString> { aTable });
+
+ // Remove the backup and delete directory since we are aborting
+ // from an update.
+ Unused << RemoveBackupTables();
+ Unused << CleanToDelete();
+}
+
+void
+Classifier::TableRequest(nsACString& aResult)
+{
+ // Generating v2 table info.
+ nsTArray<nsCString> tables;
+ ActiveTables(tables);
+ for (uint32_t i = 0; i < tables.Length(); i++) {
+ HashStore store(tables[i], GetProvider(tables[i]), mRootStoreDirectory);
+
+ nsresult rv = store.Open();
+ if (NS_FAILED(rv))
+ continue;
+
+ aResult.Append(store.TableName());
+ aResult.Append(';');
+
+ ChunkSet &adds = store.AddChunks();
+ ChunkSet &subs = store.SubChunks();
+
+ if (adds.Length() > 0) {
+ aResult.AppendLiteral("a:");
+ nsAutoCString addList;
+ adds.Serialize(addList);
+ aResult.Append(addList);
+ }
+
+ if (subs.Length() > 0) {
+ if (adds.Length() > 0)
+ aResult.Append(':');
+ aResult.AppendLiteral("s:");
+ nsAutoCString subList;
+ subs.Serialize(subList);
+ aResult.Append(subList);
+ }
+
+ aResult.Append('\n');
+ }
+
+ // Load meta data from *.metadata files in the root directory.
+ // Specifically for v4 tables.
+ nsCString metadata;
+ nsresult rv = LoadMetadata(mRootStoreDirectory, metadata);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ aResult.Append(metadata);
+}
+
+// This is used to record the matching statistics for v2 and v4.
+enum class PrefixMatch : uint8_t {
+ eNoMatch = 0x00,
+ eMatchV2Prefix = 0x01,
+ eMatchV4Prefix = 0x02,
+ eMatchBoth = eMatchV2Prefix | eMatchV4Prefix
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(PrefixMatch)
+
+nsresult
+Classifier::Check(const nsACString& aSpec,
+ const nsACString& aTables,
+ uint32_t aFreshnessGuarantee,
+ LookupResultArray& aResults)
+{
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_CL_CHECK_TIME> timer;
+
+ // Get the set of fragments based on the url. This is necessary because we
+ // only look up at most 5 URLs per aSpec, even if aSpec has more than 5
+ // components.
+ nsTArray<nsCString> fragments;
+ nsresult rv = LookupCache::GetLookupFragments(aSpec, &fragments);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsCString> activeTables;
+ SplitTables(aTables, activeTables);
+
+ nsTArray<LookupCache*> cacheArray;
+ for (uint32_t i = 0; i < activeTables.Length(); i++) {
+ LOG(("Checking table %s", activeTables[i].get()));
+ LookupCache *cache = GetLookupCache(activeTables[i]);
+ if (cache) {
+ cacheArray.AppendElement(cache);
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ PrefixMatch matchingStatistics = PrefixMatch::eNoMatch;
+
+ // Now check each lookup fragment against the entries in the DB.
+ for (uint32_t i = 0; i < fragments.Length(); i++) {
+ Completion lookupHash;
+ lookupHash.FromPlaintext(fragments[i], mCryptoHash);
+
+ if (LOG_ENABLED()) {
+ nsAutoCString checking;
+ lookupHash.ToHexString(checking);
+ LOG(("Checking fragment %s, hash %s (%X)", fragments[i].get(),
+ checking.get(), lookupHash.ToUint32()));
+ }
+
+ for (uint32_t i = 0; i < cacheArray.Length(); i++) {
+ LookupCache *cache = cacheArray[i];
+ bool has, complete;
+
+ if (LookupCache::Cast<LookupCacheV4>(cache)) {
+ // TODO Bug 1312339 Return length in LookupCache.Has and support
+ // VariableLengthPrefix in LookupResultArray
+ rv = cache->Has(lookupHash, &has, &complete);
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to lookup fragment %s V4", fragments[i].get()));
+ }
+ if (has) {
+ matchingStatistics |= PrefixMatch::eMatchV4Prefix;
+ // TODO: Bug 1311935 - Implement Safe Browsing v4 caching
+ // Should check cache expired
+ }
+ continue;
+ }
+
+ rv = cache->Has(lookupHash, &has, &complete);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (has) {
+ LookupResult *result = aResults.AppendElement();
+ if (!result)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ int64_t age;
+ bool found = mTableFreshness.Get(cache->TableName(), &age);
+ if (!found) {
+ age = 24 * 60 * 60; // just a large number
+ } else {
+ int64_t now = (PR_Now() / PR_USEC_PER_SEC);
+ age = now - age;
+ }
+
+ LOG(("Found a result in %s: %s (Age: %Lds)",
+ cache->TableName().get(),
+ complete ? "complete." : "Not complete.",
+ age));
+
+ result->hash.complete = lookupHash;
+ result->mComplete = complete;
+ result->mFresh = (age < aFreshnessGuarantee);
+ result->mTableName.Assign(cache->TableName());
+
+ matchingStatistics |= PrefixMatch::eMatchV2Prefix;
+ }
+ }
+
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_PREFIX_MATCH,
+ static_cast<uint8_t>(matchingStatistics));
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::ApplyUpdates(nsTArray<TableUpdate*>* aUpdates)
+{
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_CL_UPDATE_TIME> timer;
+
+ PRIntervalTime clockStart = 0;
+ if (LOG_ENABLED()) {
+ clockStart = PR_IntervalNow();
+ }
+
+ nsresult rv;
+
+ {
+ ScopedUpdatesClearer scopedUpdatesClearer(aUpdates);
+
+ LOG(("Backup before update."));
+
+ rv = BackupTables();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Applying %d table updates.", aUpdates->Length()));
+
+ for (uint32_t i = 0; i < aUpdates->Length(); i++) {
+ // Previous UpdateHashStore() may have consumed this update..
+ if ((*aUpdates)[i]) {
+ // Run all updates for one table
+ nsCString updateTable(aUpdates->ElementAt(i)->TableName());
+
+ if (TableUpdate::Cast<TableUpdateV2>((*aUpdates)[i])) {
+ rv = UpdateHashStore(aUpdates, updateTable);
+ } else {
+ rv = UpdateTableV4(aUpdates, updateTable);
+ }
+
+ if (NS_FAILED(rv)) {
+ if (rv != NS_ERROR_OUT_OF_MEMORY) {
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ DumpFailedUpdate();
+#endif
+ AbortUpdateAndReset(updateTable);
+ }
+ return rv;
+ }
+ }
+ }
+
+ } // End of scopedUpdatesClearer scope.
+
+ rv = RegenActiveTables();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Cleaning up backups."));
+
+ // Move the backup directory away (signaling the transaction finished
+ // successfully). This is atomic.
+ rv = RemoveBackupTables();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Do the actual deletion of the backup files.
+ rv = CleanToDelete();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Done applying updates."));
+
+ if (LOG_ENABLED()) {
+ PRIntervalTime clockEnd = PR_IntervalNow();
+ LOG(("update took %dms\n",
+ PR_IntervalToMilliseconds(clockEnd - clockStart)));
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::ApplyFullHashes(nsTArray<TableUpdate*>* aUpdates)
+{
+ LOG(("Applying %d table gethashes.", aUpdates->Length()));
+
+ ScopedUpdatesClearer scopedUpdatesClearer(aUpdates);
+ for (uint32_t i = 0; i < aUpdates->Length(); i++) {
+ TableUpdate *update = aUpdates->ElementAt(i);
+
+ nsresult rv = UpdateCache(update);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aUpdates->ElementAt(i) = nullptr;
+ }
+
+ return NS_OK;
+}
+
+int64_t
+Classifier::GetLastUpdateTime(const nsACString& aTableName)
+{
+ int64_t age;
+ bool found = mTableFreshness.Get(aTableName, &age);
+ return found ? (age * PR_MSEC_PER_SEC) : 0;
+}
+
+void
+Classifier::SetLastUpdateTime(const nsACString &aTable,
+ uint64_t updateTime)
+{
+ LOG(("Marking table %s as last updated on %u",
+ PromiseFlatCString(aTable).get(), updateTime));
+ mTableFreshness.Put(aTable, updateTime / PR_MSEC_PER_SEC);
+}
+
+void
+Classifier::DropStores()
+{
+ for (uint32_t i = 0; i < mLookupCaches.Length(); i++) {
+ delete mLookupCaches[i];
+ }
+ mLookupCaches.Clear();
+}
+
+nsresult
+Classifier::RegenActiveTables()
+{
+ mActiveTablesCache.Clear();
+
+ nsTArray<nsCString> foundTables;
+ ScanStoreDir(foundTables);
+
+ for (uint32_t i = 0; i < foundTables.Length(); i++) {
+ nsCString table(foundTables[i]);
+ HashStore store(table, GetProvider(table), mRootStoreDirectory);
+
+ nsresult rv = store.Open();
+ if (NS_FAILED(rv))
+ continue;
+
+ LookupCache *lookupCache = GetLookupCache(store.TableName());
+ if (!lookupCache) {
+ continue;
+ }
+
+ if (!lookupCache->IsPrimed())
+ continue;
+
+ const ChunkSet &adds = store.AddChunks();
+ const ChunkSet &subs = store.SubChunks();
+
+ if (adds.Length() == 0 && subs.Length() == 0)
+ continue;
+
+ LOG(("Active table: %s", store.TableName().get()));
+ mActiveTablesCache.AppendElement(store.TableName());
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::ScanStoreDir(nsTArray<nsCString>& aTables)
+{
+ nsCOMPtr<nsISimpleEnumerator> entries;
+ nsresult rv = mRootStoreDirectory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = entries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> supports;
+ rv = entries->GetNext(getter_AddRefs(supports));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> file = do_QueryInterface(supports);
+
+ nsCString leafName;
+ rv = file->GetNativeLeafName(leafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString suffix(NS_LITERAL_CSTRING(".sbstore"));
+
+ int32_t dot = leafName.RFind(suffix, 0);
+ if (dot != -1) {
+ leafName.Cut(dot, suffix.Length());
+ aTables.AppendElement(leafName);
+ }
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::ActiveTables(nsTArray<nsCString>& aTables)
+{
+ aTables = mActiveTablesCache;
+ return NS_OK;
+}
+
+nsresult
+Classifier::CleanToDelete()
+{
+ bool exists;
+ nsresult rv = mToDeleteDirectory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (exists) {
+ rv = mToDeleteDirectory->Remove(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+
+already_AddRefed<nsIFile>
+Classifier::GetFailedUpdateDirectroy()
+{
+ nsCString failedUpdatekDirName = STORE_DIRECTORY + nsCString("-failedupdate");
+
+ nsCOMPtr<nsIFile> failedUpdatekDirectory;
+ if (NS_FAILED(mCacheDirectory->Clone(getter_AddRefs(failedUpdatekDirectory))) ||
+ NS_FAILED(failedUpdatekDirectory->AppendNative(failedUpdatekDirName))) {
+ LOG(("Failed to init failedUpdatekDirectory."));
+ return nullptr;
+ }
+
+ return failedUpdatekDirectory.forget();
+}
+
+nsresult
+Classifier::DumpRawTableUpdates(const nsACString& aRawUpdates)
+{
+ // DumpFailedUpdate() MUST be called first to create the
+ // "failed update" directory
+
+ LOG(("Dumping raw table updates..."));
+
+ nsCOMPtr<nsIFile> failedUpdatekDirectory = GetFailedUpdateDirectroy();
+
+ // Create tableupdate.bin and dump raw table update data.
+ nsCOMPtr<nsIFile> rawTableUpdatesFile;
+ nsCOMPtr<nsIOutputStream> outputStream;
+ if (NS_FAILED(failedUpdatekDirectory->Clone(getter_AddRefs(rawTableUpdatesFile))) ||
+ NS_FAILED(rawTableUpdatesFile->AppendNative(nsCString("tableupdates.bin"))) ||
+ NS_FAILED(NS_NewLocalFileOutputStream(getter_AddRefs(outputStream),
+ rawTableUpdatesFile,
+ PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE))) {
+ LOG(("Failed to create file to dump raw table updates."));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Write out the data.
+ uint32_t written;
+ nsresult rv = outputStream->Write(aRawUpdates.BeginReading(),
+ aRawUpdates.Length(), &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == aRawUpdates.Length(), NS_ERROR_FAILURE);
+
+ return rv;
+}
+
+nsresult
+Classifier::DumpFailedUpdate()
+{
+ LOG(("Dumping failed update..."));
+
+ nsCOMPtr<nsIFile> failedUpdatekDirectory = GetFailedUpdateDirectroy();
+
+ // Remove the "failed update" directory no matter it exists or not.
+ // Failure is fine because the directory may not exist.
+ failedUpdatekDirectory->Remove(true);
+
+ nsCString failedUpdatekDirName;
+ nsresult rv = failedUpdatekDirectory->GetNativeLeafName(failedUpdatekDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Move backup to a clean "failed update" directory.
+ nsCOMPtr<nsIFile> backupDirectory;
+ if (NS_FAILED(mBackupDirectory->Clone(getter_AddRefs(backupDirectory))) ||
+ NS_FAILED(backupDirectory->MoveToNative(nullptr, failedUpdatekDirName))) {
+ LOG(("Failed to move backup to the \"failed update\" directory %s",
+ failedUpdatekDirName.get()));
+ return NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+#endif // MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+
+nsresult
+Classifier::BackupTables()
+{
+ // We have to work in reverse here: first move the normal directory
+ // away to be the backup directory, then copy the files over
+ // to the normal directory. This ensures that if we crash the backup
+ // dir always has a valid, complete copy, instead of a partial one,
+ // because that's the one we will copy over the normal store dir.
+
+ nsCString backupDirName;
+ nsresult rv = mBackupDirectory->GetNativeLeafName(backupDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString storeDirName;
+ rv = mRootStoreDirectory->GetNativeLeafName(storeDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mRootStoreDirectory->MoveToNative(nullptr, backupDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mRootStoreDirectory->CopyToNative(nullptr, storeDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We moved some things to new places, so move the handles around, too.
+ rv = SetupPathNames();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::RemoveBackupTables()
+{
+ nsCString toDeleteName;
+ nsresult rv = mToDeleteDirectory->GetNativeLeafName(toDeleteName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mBackupDirectory->MoveToNative(nullptr, toDeleteName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // mBackupDirectory now points to toDelete, fix that up.
+ rv = SetupPathNames();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::RecoverBackups()
+{
+ bool backupExists;
+ nsresult rv = mBackupDirectory->Exists(&backupExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (backupExists) {
+ // Remove the safebrowsing dir if it exists
+ nsCString storeDirName;
+ rv = mRootStoreDirectory->GetNativeLeafName(storeDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool storeExists;
+ rv = mRootStoreDirectory->Exists(&storeExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (storeExists) {
+ rv = mRootStoreDirectory->Remove(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Move the backup to the store location
+ rv = mBackupDirectory->MoveToNative(nullptr, storeDirName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // mBackupDirectory now points to storeDir, fix up.
+ rv = SetupPathNames();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+bool
+Classifier::CheckValidUpdate(nsTArray<TableUpdate*>* aUpdates,
+ const nsACString& aTable)
+{
+ // take the quick exit if there is no valid update for us
+ // (common case)
+ uint32_t validupdates = 0;
+
+ for (uint32_t i = 0; i < aUpdates->Length(); i++) {
+ TableUpdate *update = aUpdates->ElementAt(i);
+ if (!update || !update->TableName().Equals(aTable))
+ continue;
+ if (update->Empty()) {
+ aUpdates->ElementAt(i) = nullptr;
+ continue;
+ }
+ validupdates++;
+ }
+
+ if (!validupdates) {
+ // This can happen if the update was only valid for one table.
+ return false;
+ }
+
+ return true;
+}
+
+nsCString
+Classifier::GetProvider(const nsACString& aTableName)
+{
+ nsCOMPtr<nsIUrlClassifierUtils> urlUtil =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID);
+
+ nsCString provider;
+ nsresult rv = urlUtil->GetProvider(aTableName, provider);
+
+ return NS_SUCCEEDED(rv) ? provider : EmptyCString();
+}
+
+/*
+ * This will consume+delete updates from the passed nsTArray.
+*/
+nsresult
+Classifier::UpdateHashStore(nsTArray<TableUpdate*>* aUpdates,
+ const nsACString& aTable)
+{
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ LOG(("Classifier::UpdateHashStore(%s)", PromiseFlatCString(aTable).get()));
+
+ HashStore store(aTable, GetProvider(aTable), mRootStoreDirectory);
+
+ if (!CheckValidUpdate(aUpdates, store.TableName())) {
+ return NS_OK;
+ }
+
+ nsresult rv = store.Open();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = store.BeginUpdate();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Read the part of the store that is (only) in the cache
+ LookupCacheV2* lookupCache =
+ LookupCache::Cast<LookupCacheV2>(GetLookupCache(store.TableName()));
+ if (!lookupCache) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Clear cache when update
+ lookupCache->ClearCache();
+
+ FallibleTArray<uint32_t> AddPrefixHashes;
+ rv = lookupCache->GetPrefixes(AddPrefixHashes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = store.AugmentAdds(AddPrefixHashes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ AddPrefixHashes.Clear();
+
+ uint32_t applied = 0;
+
+ for (uint32_t i = 0; i < aUpdates->Length(); i++) {
+ TableUpdate *update = aUpdates->ElementAt(i);
+ if (!update || !update->TableName().Equals(store.TableName()))
+ continue;
+
+ rv = store.ApplyUpdate(*update);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ applied++;
+
+ auto updateV2 = TableUpdate::Cast<TableUpdateV2>(update);
+ if (updateV2) {
+ LOG(("Applied update to table %s:", store.TableName().get()));
+ LOG((" %d add chunks", updateV2->AddChunks().Length()));
+ LOG((" %d add prefixes", updateV2->AddPrefixes().Length()));
+ LOG((" %d add completions", updateV2->AddCompletes().Length()));
+ LOG((" %d sub chunks", updateV2->SubChunks().Length()));
+ LOG((" %d sub prefixes", updateV2->SubPrefixes().Length()));
+ LOG((" %d sub completions", updateV2->SubCompletes().Length()));
+ LOG((" %d add expirations", updateV2->AddExpirations().Length()));
+ LOG((" %d sub expirations", updateV2->SubExpirations().Length()));
+ }
+
+ aUpdates->ElementAt(i) = nullptr;
+ }
+
+ LOG(("Applied %d update(s) to %s.", applied, store.TableName().get()));
+
+ rv = store.Rebuild();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Table %s now has:", store.TableName().get()));
+ LOG((" %d add chunks", store.AddChunks().Length()));
+ LOG((" %d add prefixes", store.AddPrefixes().Length()));
+ LOG((" %d add completions", store.AddCompletes().Length()));
+ LOG((" %d sub chunks", store.SubChunks().Length()));
+ LOG((" %d sub prefixes", store.SubPrefixes().Length()));
+ LOG((" %d sub completions", store.SubCompletes().Length()));
+
+ rv = store.WriteFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // At this point the store is updated and written out to disk, but
+ // the data is still in memory. Build our quick-lookup table here.
+ rv = lookupCache->Build(store.AddPrefixes(), store.AddCompletes());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#if defined(DEBUG)
+ lookupCache->DumpCompletions();
+#endif
+ rv = lookupCache->WriteFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t now = (PR_Now() / PR_USEC_PER_SEC);
+ LOG(("Successfully updated %s", store.TableName().get()));
+ mTableFreshness.Put(store.TableName(), now);
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::UpdateTableV4(nsTArray<TableUpdate*>* aUpdates,
+ const nsACString& aTable)
+{
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ LOG(("Classifier::UpdateTableV4(%s)", PromiseFlatCString(aTable).get()));
+
+ if (!CheckValidUpdate(aUpdates, aTable)) {
+ return NS_OK;
+ }
+
+ LookupCacheV4* lookupCache =
+ LookupCache::Cast<LookupCacheV4>(GetLookupCache(aTable));
+ if (!lookupCache) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = NS_OK;
+
+ // If there are multiple updates for the same table, prefixes1 & prefixes2
+ // will act as input and output in turn to reduce memory copy overhead.
+ PrefixStringMap prefixes1, prefixes2;
+ PrefixStringMap* input = &prefixes1;
+ PrefixStringMap* output = &prefixes2;
+
+ TableUpdateV4* lastAppliedUpdate = nullptr;
+ for (uint32_t i = 0; i < aUpdates->Length(); i++) {
+ TableUpdate *update = aUpdates->ElementAt(i);
+ if (!update || !update->TableName().Equals(aTable)) {
+ continue;
+ }
+
+ auto updateV4 = TableUpdate::Cast<TableUpdateV4>(update);
+ NS_ENSURE_TRUE(updateV4, NS_ERROR_FAILURE);
+
+ if (updateV4->IsFullUpdate()) {
+ input->Clear();
+ output->Clear();
+ rv = lookupCache->ApplyUpdate(updateV4, *input, *output);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ // If both prefix sets are empty, this means we are doing a partial update
+ // without a prior full/partial update in the loop. In this case we should
+ // get prefixes from the lookup cache first.
+ if (prefixes1.IsEmpty() && prefixes2.IsEmpty()) {
+ lookupCache->GetPrefixes(prefixes1);
+ } else {
+ MOZ_ASSERT(prefixes1.IsEmpty() ^ prefixes2.IsEmpty());
+
+ // When there are multiple partial updates, input should always point
+ // to the non-empty prefix set(filled by previous full/partial update).
+ // output should always point to the empty prefix set.
+ input = prefixes1.IsEmpty() ? &prefixes2 : &prefixes1;
+ output = prefixes1.IsEmpty() ? &prefixes1 : &prefixes2;
+ }
+
+ rv = lookupCache->ApplyUpdate(updateV4, *input, *output);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ input->Clear();
+ }
+
+ // Keep track of the last applied update.
+ lastAppliedUpdate = updateV4;
+
+ aUpdates->ElementAt(i) = nullptr;
+ }
+
+ rv = lookupCache->Build(*output);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = lookupCache->WriteFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (lastAppliedUpdate) {
+ LOG(("Write meta data of the last applied update."));
+ rv = lookupCache->WriteMetadata(lastAppliedUpdate);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+
+ int64_t now = (PR_Now() / PR_USEC_PER_SEC);
+ LOG(("Successfully updated %s\n", PromiseFlatCString(aTable).get()));
+ mTableFreshness.Put(aTable, now);
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::UpdateCache(TableUpdate* aUpdate)
+{
+ if (!aUpdate) {
+ return NS_OK;
+ }
+
+ nsAutoCString table(aUpdate->TableName());
+ LOG(("Classifier::UpdateCache(%s)", table.get()));
+
+ LookupCache *lookupCache = GetLookupCache(table);
+ if (!lookupCache) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto updateV2 = TableUpdate::Cast<TableUpdateV2>(aUpdate);
+ lookupCache->AddCompletionsToCache(updateV2->AddCompletes());
+
+#if defined(DEBUG)
+ lookupCache->DumpCache();
+#endif
+
+ return NS_OK;
+}
+
+LookupCache *
+Classifier::GetLookupCache(const nsACString& aTable)
+{
+ for (uint32_t i = 0; i < mLookupCaches.Length(); i++) {
+ if (mLookupCaches[i]->TableName().Equals(aTable)) {
+ return mLookupCaches[i];
+ }
+ }
+
+ // TODO : Bug 1302600, It would be better if we have a more general non-main
+ // thread method to convert table name to protocol version. Currently
+ // we can only know this by checking if the table name ends with '-proto'.
+ UniquePtr<LookupCache> cache;
+ nsCString provider = GetProvider(aTable);
+ if (StringEndsWith(aTable, NS_LITERAL_CSTRING("-proto"))) {
+ cache = MakeUnique<LookupCacheV4>(aTable, provider, mRootStoreDirectory);
+ } else {
+ cache = MakeUnique<LookupCacheV2>(aTable, provider, mRootStoreDirectory);
+ }
+
+ nsresult rv = cache->Init();
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+ rv = cache->Open();
+ if (NS_FAILED(rv)) {
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ Reset();
+ }
+ return nullptr;
+ }
+ mLookupCaches.AppendElement(cache.get());
+ return cache.release();
+}
+
+nsresult
+Classifier::ReadNoiseEntries(const Prefix& aPrefix,
+ const nsACString& aTableName,
+ uint32_t aCount,
+ PrefixArray* aNoiseEntries)
+{
+ // TODO : Bug 1297962, support adding noise for v4
+ LookupCacheV2 *cache = static_cast<LookupCacheV2*>(GetLookupCache(aTableName));
+ if (!cache) {
+ return NS_ERROR_FAILURE;
+ }
+
+ FallibleTArray<uint32_t> prefixes;
+ nsresult rv = cache->GetPrefixes(prefixes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ size_t idx = prefixes.BinaryIndexOf(aPrefix.ToUint32());
+
+ if (idx == nsTArray<uint32_t>::NoIndex) {
+ NS_WARNING("Could not find prefix in PrefixSet during noise lookup");
+ return NS_ERROR_FAILURE;
+ }
+
+ idx -= idx % aCount;
+
+ for (size_t i = 0; (i < aCount) && ((idx+i) < prefixes.Length()); i++) {
+ Prefix newPref;
+ newPref.FromUint32(prefixes[idx+i]);
+ if (newPref != aPrefix) {
+ aNoiseEntries->AppendElement(newPref);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+Classifier::LoadMetadata(nsIFile* aDirectory, nsACString& aResult)
+{
+ nsCOMPtr<nsISimpleEnumerator> entries;
+ nsresult rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_ARG_POINTER(entries);
+
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = entries->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> supports;
+ rv = entries->GetNext(getter_AddRefs(supports));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> file = do_QueryInterface(supports);
+
+ // If |file| is a directory, recurse to find its entries as well.
+ bool isDirectory;
+ if (NS_FAILED(file->IsDirectory(&isDirectory))) {
+ continue;
+ }
+ if (isDirectory) {
+ LoadMetadata(file, aResult);
+ continue;
+ }
+
+ // Truncate file extension to get the table name.
+ nsCString tableName;
+ rv = file->GetNativeLeafName(tableName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t dot = tableName.RFind(METADATA_SUFFIX, 0);
+ if (dot == -1) {
+ continue;
+ }
+ tableName.Cut(dot, METADATA_SUFFIX.Length());
+
+ LookupCacheV4* lookupCache =
+ LookupCache::Cast<LookupCacheV4>(GetLookupCache(tableName));
+ if (!lookupCache) {
+ continue;
+ }
+
+ nsCString state;
+ nsCString checksum;
+ rv = lookupCache->LoadMetadata(state, checksum);
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to get metadata for table %s", tableName.get()));
+ continue;
+ }
+
+ // The state might include '\n' so that we have to encode.
+ nsAutoCString stateBase64;
+ rv = Base64Encode(state, stateBase64);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString checksumBase64;
+ rv = Base64Encode(checksum, checksumBase64);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Appending state '%s' and checksum '%s' for table %s",
+ stateBase64.get(), checksumBase64.get(), tableName.get()));
+
+ aResult.AppendPrintf("%s;%s:%s\n", tableName.get(),
+ stateBase64.get(),
+ checksumBase64.get());
+ }
+
+ return rv;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/Classifier.h b/toolkit/components/url-classifier/Classifier.h
new file mode 100644
index 0000000000..58c49fce51
--- /dev/null
+++ b/toolkit/components/url-classifier/Classifier.h
@@ -0,0 +1,167 @@
+//* -*- 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/. */
+
+#ifndef Classifier_h__
+#define Classifier_h__
+
+#include "Entries.h"
+#include "HashStore.h"
+#include "ProtocolParser.h"
+#include "LookupCache.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsICryptoHash.h"
+#include "nsDataHashtable.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+/**
+ * Maintains the stores and LookupCaches for the url classifier.
+ */
+class Classifier {
+public:
+ typedef nsClassHashtable<nsCStringHashKey, nsCString> ProviderDictType;
+
+public:
+ Classifier();
+ ~Classifier();
+
+ nsresult Open(nsIFile& aCacheDirectory);
+ void Close();
+ void Reset();
+
+ /**
+ * Clear data for specific tables.
+ * If ClearType is Clear_Cache, this function will only clear cache in lookup
+ * cache, otherwise, it will clear data in lookup cache and data stored on disk.
+ */
+ enum ClearType {
+ Clear_Cache,
+ Clear_All,
+ };
+ void ResetTables(ClearType aType, const nsTArray<nsCString>& aTables);
+
+ /**
+ * Get the list of active tables and their chunks in a format
+ * suitable for an update request.
+ */
+ void TableRequest(nsACString& aResult);
+
+ /*
+ * Get all tables that we know about.
+ */
+ nsresult ActiveTables(nsTArray<nsCString>& aTables);
+
+ /**
+ * Check a URL against the specified tables.
+ */
+ nsresult Check(const nsACString& aSpec,
+ const nsACString& tables,
+ uint32_t aFreshnessGuarantee,
+ LookupResultArray& aResults);
+
+ /**
+ * Apply the table updates in the array. Takes ownership of
+ * the updates in the array and clears it. Wacky!
+ */
+ nsresult ApplyUpdates(nsTArray<TableUpdate*>* aUpdates);
+
+ /**
+ * Apply full hashes retrived from gethash to cache.
+ */
+ nsresult ApplyFullHashes(nsTArray<TableUpdate*>* aUpdates);
+
+ void SetLastUpdateTime(const nsACString& aTableName, uint64_t updateTime);
+ int64_t GetLastUpdateTime(const nsACString& aTableName);
+ nsresult CacheCompletions(const CacheResultArray& aResults);
+ uint32_t GetHashKey(void) { return mHashKey; }
+ /*
+ * Get a bunch of extra prefixes to query for completion
+ * and mask the real entry being requested
+ */
+ nsresult ReadNoiseEntries(const Prefix& aPrefix,
+ const nsACString& aTableName,
+ uint32_t aCount,
+ PrefixArray* aNoiseEntries);
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ nsresult DumpRawTableUpdates(const nsACString& aRawUpdates);
+#endif
+
+ static void SplitTables(const nsACString& str, nsTArray<nsCString>& tables);
+
+ // Given a root store directory, return a private store directory
+ // based on the table name. To avoid migration issue, the private
+ // store directory is only different from root directory for V4 tables.
+ //
+ // For V4 tables (suffixed by '-proto'), the private directory would
+ // be [root directory path]/[provider]. The provider of V4 tables is
+ // 'google4'.
+ //
+ // Note that if the table name is not owned by any provider, just use
+ // the root directory.
+ static nsresult GetPrivateStoreDirectory(nsIFile* aRootStoreDirectory,
+ const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile** aPrivateStoreDirectory);
+
+private:
+ void DropStores();
+ void DeleteTables(nsIFile* aDirectory, const nsTArray<nsCString>& aTables);
+ void AbortUpdateAndReset(const nsCString& aTable);
+
+ nsresult CreateStoreDirectory();
+ nsresult SetupPathNames();
+ nsresult RecoverBackups();
+ nsresult CleanToDelete();
+ nsresult BackupTables();
+ nsresult RemoveBackupTables();
+ nsresult RegenActiveTables();
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ already_AddRefed<nsIFile> GetFailedUpdateDirectroy();
+ nsresult DumpFailedUpdate();
+#endif
+
+ nsresult ScanStoreDir(nsTArray<nsCString>& aTables);
+
+ nsresult UpdateHashStore(nsTArray<TableUpdate*>* aUpdates,
+ const nsACString& aTable);
+
+ nsresult UpdateTableV4(nsTArray<TableUpdate*>* aUpdates,
+ const nsACString& aTable);
+
+ nsresult UpdateCache(TableUpdate* aUpdates);
+
+ LookupCache *GetLookupCache(const nsACString& aTable);
+
+ bool CheckValidUpdate(nsTArray<TableUpdate*>* aUpdates,
+ const nsACString& aTable);
+
+ nsresult LoadMetadata(nsIFile* aDirectory, nsACString& aResult);
+
+ nsCString GetProvider(const nsACString& aTableName);
+
+ // Root dir of the Local profile.
+ nsCOMPtr<nsIFile> mCacheDirectory;
+ // Main directory where to store the databases.
+ nsCOMPtr<nsIFile> mRootStoreDirectory;
+ // Used for atomically updating the other dirs.
+ nsCOMPtr<nsIFile> mBackupDirectory;
+ nsCOMPtr<nsIFile> mToDeleteDirectory;
+ nsCOMPtr<nsICryptoHash> mCryptoHash;
+ nsTArray<LookupCache*> mLookupCaches;
+ nsTArray<nsCString> mActiveTablesCache;
+ uint32_t mHashKey;
+ // Stores the last time a given table was updated (seconds).
+ nsDataHashtable<nsCStringHashKey, int64_t> mTableFreshness;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/Entries.h b/toolkit/components/url-classifier/Entries.h
new file mode 100644
index 0000000000..969f4f739b
--- /dev/null
+++ b/toolkit/components/url-classifier/Entries.h
@@ -0,0 +1,322 @@
+//* -*- 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/. */
+
+// This header file defines the storage types of the actual safebrowsing
+// chunk data, which may be either 32-bit hashes or complete 256-bit hashes.
+// Chunk numbers are represented in ChunkSet.h.
+
+#ifndef SBEntries_h__
+#define SBEntries_h__
+
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsICryptoHash.h"
+#include "nsNetUtil.h"
+#include "nsIOutputStream.h"
+#include "nsClassHashtable.h"
+
+#if DEBUG
+#include "plbase64.h"
+#endif
+
+namespace mozilla {
+namespace safebrowsing {
+
+#define PREFIX_SIZE 4
+#define COMPLETE_SIZE 32
+
+// This is the struct that contains 4-byte hash prefixes.
+template <uint32_t S, class Comparator>
+struct SafebrowsingHash
+{
+ static_assert(S >= 4, "The SafebrowsingHash should be at least 4 bytes.");
+
+ static const uint32_t sHashSize = S;
+ typedef SafebrowsingHash<S, Comparator> self_type;
+ uint8_t buf[S];
+
+ nsresult FromPlaintext(const nsACString& aPlainText, nsICryptoHash* aHash) {
+ // From the protocol doc:
+ // Each entry in the chunk is composed
+ // of the SHA 256 hash of a suffix/prefix expression.
+
+ nsresult rv = aHash->Init(nsICryptoHash::SHA256);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aHash->Update
+ (reinterpret_cast<const uint8_t*>(aPlainText.BeginReading()),
+ aPlainText.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString hashed;
+ rv = aHash->Finish(false, hashed);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(hashed.Length() >= sHashSize,
+ "not enough characters in the hash");
+
+ memcpy(buf, hashed.BeginReading(), sHashSize);
+
+ return NS_OK;
+ }
+
+ void Assign(const nsACString& aStr) {
+ NS_ASSERTION(aStr.Length() >= sHashSize,
+ "string must be at least sHashSize characters long");
+ memcpy(buf, aStr.BeginReading(), sHashSize);
+ }
+
+ int Compare(const self_type& aOther) const {
+ return Comparator::Compare(buf, aOther.buf);
+ }
+
+ bool operator==(const self_type& aOther) const {
+ return Comparator::Compare(buf, aOther.buf) == 0;
+ }
+
+ bool operator!=(const self_type& aOther) const {
+ return Comparator::Compare(buf, aOther.buf) != 0;
+ }
+
+ bool operator<(const self_type& aOther) const {
+ return Comparator::Compare(buf, aOther.buf) < 0;
+ }
+
+#ifdef DEBUG
+ void ToString(nsACString& aStr) const {
+ uint32_t len = ((sHashSize + 2) / 3) * 4;
+ aStr.SetCapacity(len + 1);
+ PL_Base64Encode((char*)buf, sHashSize, aStr.BeginWriting());
+ aStr.BeginWriting()[len] = '\0';
+ }
+#endif
+
+ void ToHexString(nsACString& aStr) const {
+ static const char* const lut = "0123456789ABCDEF";
+ // 32 bytes is the longest hash
+ size_t len = 32;
+
+ aStr.SetCapacity(2 * len);
+ for (size_t i = 0; i < len; ++i) {
+ const char c = static_cast<const char>(buf[i]);
+ aStr.Append(lut[(c >> 4) & 0x0F]);
+ aStr.Append(lut[c & 15]);
+ }
+ }
+
+ uint32_t ToUint32() const {
+ uint32_t n;
+ memcpy(&n, buf, sizeof(n));
+ return n;
+ }
+ void FromUint32(uint32_t aHash) {
+ memcpy(buf, &aHash, sizeof(aHash));
+ }
+};
+
+class PrefixComparator {
+public:
+ static int Compare(const uint8_t* a, const uint8_t* b) {
+ uint32_t first;
+ memcpy(&first, a, sizeof(uint32_t));
+
+ uint32_t second;
+ memcpy(&second, b, sizeof(uint32_t));
+
+ if (first > second) {
+ return 1;
+ } else if (first == second) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+};
+// Use this for 4-byte hashes
+typedef SafebrowsingHash<PREFIX_SIZE, PrefixComparator> Prefix;
+typedef nsTArray<Prefix> PrefixArray;
+
+class CompletionComparator {
+public:
+ static int Compare(const uint8_t* a, const uint8_t* b) {
+ return memcmp(a, b, COMPLETE_SIZE);
+ }
+};
+// Use this for 32-byte hashes
+typedef SafebrowsingHash<COMPLETE_SIZE, CompletionComparator> Completion;
+typedef nsTArray<Completion> CompletionArray;
+
+struct AddPrefix {
+ // The truncated hash.
+ Prefix prefix;
+ // The chunk number to which it belongs.
+ uint32_t addChunk;
+
+ AddPrefix() : addChunk(0) {}
+
+ // Returns the chunk number.
+ uint32_t Chunk() const { return addChunk; }
+ const Prefix &PrefixHash() const { return prefix; }
+
+ template<class T>
+ int Compare(const T& other) const {
+ int cmp = prefix.Compare(other.PrefixHash());
+ if (cmp != 0) {
+ return cmp;
+ }
+ return addChunk - other.addChunk;
+ }
+};
+
+struct AddComplete {
+ Completion complete;
+ uint32_t addChunk;
+
+ AddComplete() : addChunk(0) {}
+
+ uint32_t Chunk() const { return addChunk; }
+ // The 4-byte prefix of the sha256 hash.
+ uint32_t ToUint32() const { return complete.ToUint32(); }
+ // The 32-byte sha256 hash.
+ const Completion &CompleteHash() const { return complete; }
+
+ template<class T>
+ int Compare(const T& other) const {
+ int cmp = complete.Compare(other.CompleteHash());
+ if (cmp != 0) {
+ return cmp;
+ }
+ return addChunk - other.addChunk;
+ }
+
+ bool operator!=(const AddComplete& aOther) const {
+ if (addChunk != aOther.addChunk) {
+ return true;
+ }
+ return complete != aOther.complete;
+ }
+};
+
+struct SubPrefix {
+ // The hash to subtract.
+ Prefix prefix;
+ // The chunk number of the add chunk to which the hash belonged.
+ uint32_t addChunk;
+ // The chunk number of this sub chunk.
+ uint32_t subChunk;
+
+ SubPrefix(): addChunk(0), subChunk(0) {}
+
+ uint32_t Chunk() const { return subChunk; }
+ uint32_t AddChunk() const { return addChunk; }
+ const Prefix &PrefixHash() const { return prefix; }
+
+ template<class T>
+ // Returns 0 if and only if the chunks are the same in every way.
+ int Compare(const T& aOther) const {
+ int cmp = prefix.Compare(aOther.PrefixHash());
+ if (cmp != 0)
+ return cmp;
+ if (addChunk != aOther.addChunk)
+ return addChunk - aOther.addChunk;
+ return subChunk - aOther.subChunk;
+ }
+
+ template<class T>
+ int CompareAlt(const T& aOther) const {
+ Prefix other;
+ other.FromUint32(aOther.ToUint32());
+ int cmp = prefix.Compare(other);
+ if (cmp != 0)
+ return cmp;
+ return addChunk - aOther.addChunk;
+ }
+};
+
+struct SubComplete {
+ Completion complete;
+ uint32_t addChunk;
+ uint32_t subChunk;
+
+ SubComplete() : addChunk(0), subChunk(0) {}
+
+ uint32_t Chunk() const { return subChunk; }
+ uint32_t AddChunk() const { return addChunk; }
+ const Completion &CompleteHash() const { return complete; }
+ // The 4-byte prefix of the sha256 hash.
+ uint32_t ToUint32() const { return complete.ToUint32(); }
+
+ int Compare(const SubComplete& aOther) const {
+ int cmp = complete.Compare(aOther.complete);
+ if (cmp != 0)
+ return cmp;
+ if (addChunk != aOther.addChunk)
+ return addChunk - aOther.addChunk;
+ return subChunk - aOther.subChunk;
+ }
+};
+
+typedef FallibleTArray<AddPrefix> AddPrefixArray;
+typedef FallibleTArray<AddComplete> AddCompleteArray;
+typedef FallibleTArray<SubPrefix> SubPrefixArray;
+typedef FallibleTArray<SubComplete> SubCompleteArray;
+
+/**
+ * Compares chunks by their add chunk, then their prefix.
+ */
+template<class T>
+class EntryCompare {
+public:
+ typedef T elem_type;
+ static int Compare(const void* e1, const void* e2) {
+ const elem_type* a = static_cast<const elem_type*>(e1);
+ const elem_type* b = static_cast<const elem_type*>(e2);
+ return a->Compare(*b);
+ }
+};
+
+/**
+ * Sort an array of store entries. nsTArray::Sort uses Equal/LessThan
+ * to sort, this does a single Compare so it's a bit quicker over the
+ * large sorts we do.
+ */
+template<class T, class Alloc>
+void
+EntrySort(nsTArray_Impl<T, Alloc>& aArray)
+{
+ qsort(aArray.Elements(), aArray.Length(), sizeof(T),
+ EntryCompare<T>::Compare);
+}
+
+template<class T, class Alloc>
+nsresult
+ReadTArray(nsIInputStream* aStream, nsTArray_Impl<T, Alloc>* aArray, uint32_t aNumElements)
+{
+ if (!aArray->SetLength(aNumElements, fallible))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ void *buffer = aArray->Elements();
+ nsresult rv = NS_ReadInputStreamToBuffer(aStream, &buffer,
+ (aNumElements * sizeof(T)));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+template<class T, class Alloc>
+nsresult
+WriteTArray(nsIOutputStream* aStream, nsTArray_Impl<T, Alloc>& aArray)
+{
+ uint32_t written;
+ return aStream->Write(reinterpret_cast<char*>(aArray.Elements()),
+ aArray.Length() * sizeof(T),
+ &written);
+}
+
+typedef nsClassHashtable<nsUint32HashKey, nsCString> PrefixStringMap;
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif // SBEntries_h__
diff --git a/toolkit/components/url-classifier/HashStore.cpp b/toolkit/components/url-classifier/HashStore.cpp
new file mode 100644
index 0000000000..c298612aa4
--- /dev/null
+++ b/toolkit/components/url-classifier/HashStore.cpp
@@ -0,0 +1,1248 @@
+//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+// Originally based on Chrome sources:
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+#include "HashStore.h"
+#include "nsICryptoHash.h"
+#include "nsISeekableStream.h"
+#include "nsIStreamConverterService.h"
+#include "nsNetUtil.h"
+#include "nsCheckSummedOutputStream.h"
+#include "prio.h"
+#include "mozilla/Logging.h"
+#include "zlib.h"
+#include "Classifier.h"
+#include "nsUrlClassifierDBService.h"
+
+// Main store for SafeBrowsing protocol data. We store
+// known add/sub chunks, prefixes and completions in memory
+// during an update, and serialize to disk.
+// We do not store the add prefixes, those are retrieved by
+// decompressing the PrefixSet cache whenever we need to apply
+// an update.
+//
+// byte slicing: Many of the 4-byte values stored here are strongly
+// correlated in the upper bytes, and uncorrelated in the lower
+// bytes. Because zlib/DEFLATE requires match lengths of at least
+// 3 to achieve good compression, and we don't get those if only
+// the upper 16-bits are correlated, it is worthwhile to slice 32-bit
+// values into 4 1-byte slices and compress the slices individually.
+// The slices corresponding to MSBs will compress very well, and the
+// slice corresponding to LSB almost nothing. Because of this, we
+// only apply DEFLATE to the 3 most significant bytes, and store the
+// LSB uncompressed.
+//
+// byte sliced (numValues) data format:
+// uint32_t compressed-size
+// compressed-size bytes zlib DEFLATE data
+// 0...numValues byte MSB of 4-byte numValues data
+// uint32_t compressed-size
+// compressed-size bytes zlib DEFLATE data
+// 0...numValues byte 2nd byte of 4-byte numValues data
+// uint32_t compressed-size
+// compressed-size bytes zlib DEFLATE data
+// 0...numValues byte 3rd byte of 4-byte numValues data
+// 0...numValues byte LSB of 4-byte numValues data
+//
+// Store data format:
+// uint32_t magic
+// uint32_t version
+// uint32_t numAddChunks
+// uint32_t numSubChunks
+// uint32_t numAddPrefixes
+// uint32_t numSubPrefixes
+// uint32_t numAddCompletes
+// uint32_t numSubCompletes
+// 0...numAddChunks uint32_t addChunk
+// 0...numSubChunks uint32_t subChunk
+// byte sliced (numAddPrefixes) uint32_t add chunk of AddPrefixes
+// byte sliced (numSubPrefixes) uint32_t add chunk of SubPrefixes
+// byte sliced (numSubPrefixes) uint32_t sub chunk of SubPrefixes
+// byte sliced (numSubPrefixes) uint32_t SubPrefixes
+// 0...numAddCompletes 32-byte Completions + uint32_t addChunk
+// 0...numSubCompletes 32-byte Completions + uint32_t addChunk
+// + uint32_t subChunk
+// 16-byte MD5 of all preceding data
+
+// Name of the SafeBrowsing store
+#define STORE_SUFFIX ".sbstore"
+
+// MOZ_LOG=UrlClassifierDbService:5
+extern mozilla::LazyLogModule gUrlClassifierDbServiceLog;
+#define LOG(args) MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug)
+
+// Either the return was successful or we call the Reset function (unless we
+// hit an OOM). Used while reading in the store.
+#define SUCCESS_OR_RESET(res) \
+ do { \
+ nsresult __rv = res; /* Don't evaluate |res| more than once */ \
+ if (__rv == NS_ERROR_OUT_OF_MEMORY) { \
+ NS_WARNING("SafeBrowsing OOM."); \
+ return __rv; \
+ } \
+ if (NS_FAILED(__rv)) { \
+ NS_WARNING("SafeBrowsing store corrupted or out of date."); \
+ Reset(); \
+ return __rv; \
+ } \
+ } while(0)
+
+namespace mozilla {
+namespace safebrowsing {
+
+const uint32_t STORE_MAGIC = 0x1231af3b;
+const uint32_t CURRENT_VERSION = 3;
+
+nsresult
+TableUpdateV2::NewAddPrefix(uint32_t aAddChunk, const Prefix& aHash)
+{
+ AddPrefix *add = mAddPrefixes.AppendElement(fallible);
+ if (!add) return NS_ERROR_OUT_OF_MEMORY;
+ add->addChunk = aAddChunk;
+ add->prefix = aHash;
+ return NS_OK;
+}
+
+nsresult
+TableUpdateV2::NewSubPrefix(uint32_t aAddChunk, const Prefix& aHash, uint32_t aSubChunk)
+{
+ SubPrefix *sub = mSubPrefixes.AppendElement(fallible);
+ if (!sub) return NS_ERROR_OUT_OF_MEMORY;
+ sub->addChunk = aAddChunk;
+ sub->prefix = aHash;
+ sub->subChunk = aSubChunk;
+ return NS_OK;
+}
+
+nsresult
+TableUpdateV2::NewAddComplete(uint32_t aAddChunk, const Completion& aHash)
+{
+ AddComplete *add = mAddCompletes.AppendElement(fallible);
+ if (!add) return NS_ERROR_OUT_OF_MEMORY;
+ add->addChunk = aAddChunk;
+ add->complete = aHash;
+ return NS_OK;
+}
+
+nsresult
+TableUpdateV2::NewSubComplete(uint32_t aAddChunk, const Completion& aHash, uint32_t aSubChunk)
+{
+ SubComplete *sub = mSubCompletes.AppendElement(fallible);
+ if (!sub) return NS_ERROR_OUT_OF_MEMORY;
+ sub->addChunk = aAddChunk;
+ sub->complete = aHash;
+ sub->subChunk = aSubChunk;
+ return NS_OK;
+}
+
+void
+TableUpdateV4::NewPrefixes(int32_t aSize, std::string& aPrefixes)
+{
+ NS_ENSURE_TRUE_VOID(aPrefixes.size() % aSize == 0);
+ NS_ENSURE_TRUE_VOID(!mPrefixesMap.Get(aSize));
+
+ if (LOG_ENABLED() && 4 == aSize) {
+ int numOfPrefixes = aPrefixes.size() / 4;
+ uint32_t* p = (uint32_t*)aPrefixes.c_str();
+
+ // Dump the first/last 10 fixed-length prefixes for debugging.
+ LOG(("* The first 10 (maximum) fixed-length prefixes: "));
+ for (int i = 0; i < std::min(10, numOfPrefixes); i++) {
+ uint8_t* c = (uint8_t*)&p[i];
+ LOG(("%.2X%.2X%.2X%.2X", c[0], c[1], c[2], c[3]));
+ }
+
+ LOG(("* The last 10 (maximum) fixed-length prefixes: "));
+ for (int i = std::max(0, numOfPrefixes - 10); i < numOfPrefixes; i++) {
+ uint8_t* c = (uint8_t*)&p[i];
+ LOG(("%.2X%.2X%.2X%.2X", c[0], c[1], c[2], c[3]));
+ }
+
+ LOG(("---- %d fixed-length prefixes in total.", aPrefixes.size() / aSize));
+ }
+
+ PrefixStdString* prefix = new PrefixStdString(aPrefixes);
+ mPrefixesMap.Put(aSize, prefix);
+}
+
+void
+TableUpdateV4::NewRemovalIndices(const uint32_t* aIndices, size_t aNumOfIndices)
+{
+ for (size_t i = 0; i < aNumOfIndices; i++) {
+ mRemovalIndiceArray.AppendElement(aIndices[i]);
+ }
+}
+
+void
+TableUpdateV4::NewChecksum(const std::string& aChecksum)
+{
+ mChecksum.Assign(aChecksum.data(), aChecksum.size());
+}
+
+HashStore::HashStore(const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile* aRootStoreDir)
+ : mTableName(aTableName)
+ , mInUpdate(false)
+ , mFileSize(0)
+{
+ nsresult rv = Classifier::GetPrivateStoreDirectory(aRootStoreDir,
+ aTableName,
+ aProvider,
+ getter_AddRefs(mStoreDirectory));
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to get private store directory for %s", mTableName.get()));
+ mStoreDirectory = aRootStoreDir;
+ }
+}
+
+HashStore::~HashStore()
+{
+}
+
+nsresult
+HashStore::Reset()
+{
+ LOG(("HashStore resetting"));
+
+ nsCOMPtr<nsIFile> storeFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(storeFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = storeFile->AppendNative(mTableName + NS_LITERAL_CSTRING(STORE_SUFFIX));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = storeFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFileSize = 0;
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::CheckChecksum(uint32_t aFileSize)
+{
+ if (!mInputStream) {
+ return NS_OK;
+ }
+
+ // Check for file corruption by
+ // comparing the stored checksum to actual checksum of data
+ nsAutoCString hash;
+ nsAutoCString compareHash;
+ char *data;
+ uint32_t read;
+
+ nsresult rv = CalculateChecksum(hash, aFileSize, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ compareHash.GetMutableData(&data, hash.Length());
+
+ if (hash.Length() > aFileSize) {
+ NS_WARNING("SafeBrowing file not long enough to store its hash");
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsISeekableStream> seekIn = do_QueryInterface(mInputStream);
+ rv = seekIn->Seek(nsISeekableStream::NS_SEEK_SET, aFileSize - hash.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mInputStream->Read(data, hash.Length(), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(read == hash.Length(), "Could not read hash bytes");
+
+ if (!hash.Equals(compareHash)) {
+ NS_WARNING("Safebrowing file failed checksum.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::Open()
+{
+ nsCOMPtr<nsIFile> storeFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(storeFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = storeFile->AppendNative(mTableName + NS_LITERAL_CSTRING(".sbstore"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> origStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(origStream), storeFile,
+ PR_RDONLY | nsIFile::OS_READAHEAD);
+
+ if (rv == NS_ERROR_FILE_NOT_FOUND) {
+ UpdateHeader();
+ return NS_OK;
+ } else {
+ SUCCESS_OR_RESET(rv);
+ }
+
+ int64_t fileSize;
+ rv = storeFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (fileSize < 0 || fileSize > UINT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mFileSize = static_cast<uint32_t>(fileSize);
+ mInputStream = NS_BufferInputStream(origStream, mFileSize);
+
+ rv = ReadHeader();
+ SUCCESS_OR_RESET(rv);
+
+ rv = SanityCheck();
+ SUCCESS_OR_RESET(rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::ReadHeader()
+{
+ if (!mInputStream) {
+ UpdateHeader();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mInputStream);
+ nsresult rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ void *buffer = &mHeader;
+ rv = NS_ReadInputStreamToBuffer(mInputStream,
+ &buffer,
+ sizeof(Header));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::SanityCheck()
+{
+ if (mHeader.magic != STORE_MAGIC || mHeader.version != CURRENT_VERSION) {
+ NS_WARNING("Unexpected header data in the store.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::CalculateChecksum(nsAutoCString& aChecksum,
+ uint32_t aFileSize,
+ bool aChecksumPresent)
+{
+ aChecksum.Truncate();
+
+ // Reset mInputStream to start
+ nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mInputStream);
+ nsresult rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+
+ nsCOMPtr<nsICryptoHash> hash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Size of MD5 hash in bytes
+ const uint32_t CHECKSUM_SIZE = 16;
+
+ // MD5 is not a secure hash function, but since this is a filesystem integrity
+ // check, this usage is ok.
+ rv = hash->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!aChecksumPresent) {
+ // Hash entire file
+ rv = hash->UpdateFromStream(mInputStream, UINT32_MAX);
+ } else {
+ // Hash everything but last checksum bytes
+ if (aFileSize < CHECKSUM_SIZE) {
+ NS_WARNING("SafeBrowsing file isn't long enough to store its checksum");
+ return NS_ERROR_FAILURE;
+ }
+ rv = hash->UpdateFromStream(mInputStream, aFileSize - CHECKSUM_SIZE);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hash->Finish(false, aChecksum);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+void
+HashStore::UpdateHeader()
+{
+ mHeader.magic = STORE_MAGIC;
+ mHeader.version = CURRENT_VERSION;
+
+ mHeader.numAddChunks = mAddChunks.Length();
+ mHeader.numSubChunks = mSubChunks.Length();
+ mHeader.numAddPrefixes = mAddPrefixes.Length();
+ mHeader.numSubPrefixes = mSubPrefixes.Length();
+ mHeader.numAddCompletes = mAddCompletes.Length();
+ mHeader.numSubCompletes = mSubCompletes.Length();
+}
+
+nsresult
+HashStore::ReadChunkNumbers()
+{
+ if (!mInputStream || AlreadyReadChunkNumbers()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mInputStream);
+ nsresult rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
+ sizeof(Header));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mAddChunks.Read(mInputStream, mHeader.numAddChunks);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(mAddChunks.Length() == mHeader.numAddChunks, "Read the right amount of add chunks.");
+
+ rv = mSubChunks.Read(mInputStream, mHeader.numSubChunks);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(mSubChunks.Length() == mHeader.numSubChunks, "Read the right amount of sub chunks.");
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::ReadHashes()
+{
+ if (!mInputStream) {
+ // BeginUpdate has been called but Open hasn't initialized mInputStream,
+ // because the existing HashStore is empty.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mInputStream);
+
+ uint32_t offset = sizeof(Header);
+ offset += (mHeader.numAddChunks + mHeader.numSubChunks) * sizeof(uint32_t);
+ nsresult rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ReadAddPrefixes();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ReadSubPrefixes();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If completions was read before, then we are done here.
+ if (AlreadyReadCompletions()) {
+ return NS_OK;
+ }
+
+ rv = ReadTArray(mInputStream, &mAddCompletes, mHeader.numAddCompletes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ReadTArray(mInputStream, &mSubCompletes, mHeader.numSubCompletes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+
+nsresult
+HashStore::ReadCompletions()
+{
+ if (!mInputStream || AlreadyReadCompletions()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> storeFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(storeFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = storeFile->AppendNative(mTableName + NS_LITERAL_CSTRING(STORE_SUFFIX));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t offset = mFileSize -
+ sizeof(struct AddComplete) * mHeader.numAddCompletes -
+ sizeof(struct SubComplete) * mHeader.numSubCompletes -
+ nsCheckSummedOutputStream::CHECKSUM_SIZE;
+
+ nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mInputStream);
+
+ rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ReadTArray(mInputStream, &mAddCompletes, mHeader.numAddCompletes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ReadTArray(mInputStream, &mSubCompletes, mHeader.numSubCompletes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::PrepareForUpdate()
+{
+ nsresult rv = CheckChecksum(mFileSize);
+ SUCCESS_OR_RESET(rv);
+
+ rv = ReadChunkNumbers();
+ SUCCESS_OR_RESET(rv);
+
+ rv = ReadHashes();
+ SUCCESS_OR_RESET(rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::BeginUpdate()
+{
+ // Check wether the file is corrupted and read the rest of the store
+ // in memory.
+ nsresult rv = PrepareForUpdate();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Close input stream, won't be needed any more and
+ // we will rewrite ourselves.
+ if (mInputStream) {
+ rv = mInputStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mInUpdate = true;
+
+ return NS_OK;
+}
+
+template<class T>
+static nsresult
+Merge(ChunkSet* aStoreChunks,
+ FallibleTArray<T>* aStorePrefixes,
+ ChunkSet& aUpdateChunks,
+ FallibleTArray<T>& aUpdatePrefixes,
+ bool aAllowMerging = false)
+{
+ EntrySort(aUpdatePrefixes);
+
+ T* updateIter = aUpdatePrefixes.Elements();
+ T* updateEnd = aUpdatePrefixes.Elements() + aUpdatePrefixes.Length();
+
+ T* storeIter = aStorePrefixes->Elements();
+ T* storeEnd = aStorePrefixes->Elements() + aStorePrefixes->Length();
+
+ // use a separate array so we can keep the iterators valid
+ // if the nsTArray grows
+ nsTArray<T> adds;
+
+ for (; updateIter != updateEnd; updateIter++) {
+ // skip this chunk if we already have it, unless we're
+ // merging completions, in which case we'll always already
+ // have the chunk from the original prefix
+ if (aStoreChunks->Has(updateIter->Chunk()))
+ if (!aAllowMerging)
+ continue;
+ // XXX: binary search for insertion point might be faster in common
+ // case?
+ while (storeIter < storeEnd && (storeIter->Compare(*updateIter) < 0)) {
+ // skip forward to matching element (or not...)
+ storeIter++;
+ }
+ // no match, add
+ if (storeIter == storeEnd
+ || storeIter->Compare(*updateIter) != 0) {
+ if (!adds.AppendElement(*updateIter))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ // Chunks can be empty, but we should still report we have them
+ // to make the chunkranges continuous.
+ aStoreChunks->Merge(aUpdateChunks);
+
+ if (!aStorePrefixes->AppendElements(adds, fallible))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ EntrySort(*aStorePrefixes);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::ApplyUpdate(TableUpdate &aUpdate)
+{
+ auto updateV2 = TableUpdate::Cast<TableUpdateV2>(&aUpdate);
+ NS_ENSURE_TRUE(updateV2, NS_ERROR_FAILURE);
+
+ TableUpdateV2& update = *updateV2;
+
+ nsresult rv = mAddExpirations.Merge(update.AddExpirations());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mSubExpirations.Merge(update.SubExpirations());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Expire();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Merge(&mAddChunks, &mAddPrefixes,
+ update.AddChunks(), update.AddPrefixes());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Merge(&mAddChunks, &mAddCompletes,
+ update.AddChunks(), update.AddCompletes(), true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Merge(&mSubChunks, &mSubPrefixes,
+ update.SubChunks(), update.SubPrefixes());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Merge(&mSubChunks, &mSubCompletes,
+ update.SubChunks(), update.SubCompletes(), true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::Rebuild()
+{
+ NS_ASSERTION(mInUpdate, "Must be in update to rebuild.");
+
+ nsresult rv = ProcessSubs();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ UpdateHeader();
+
+ return NS_OK;
+}
+
+void
+HashStore::ClearCompletes()
+{
+ NS_ASSERTION(mInUpdate, "Must be in update to clear completes.");
+
+ mAddCompletes.Clear();
+ mSubCompletes.Clear();
+
+ UpdateHeader();
+}
+
+template<class T>
+static void
+ExpireEntries(FallibleTArray<T>* aEntries, ChunkSet& aExpirations)
+{
+ T* addIter = aEntries->Elements();
+ T* end = aEntries->Elements() + aEntries->Length();
+
+ for (T *iter = addIter; iter != end; iter++) {
+ if (!aExpirations.Has(iter->Chunk())) {
+ *addIter = *iter;
+ addIter++;
+ }
+ }
+
+ aEntries->TruncateLength(addIter - aEntries->Elements());
+}
+
+nsresult
+HashStore::Expire()
+{
+ ExpireEntries(&mAddPrefixes, mAddExpirations);
+ ExpireEntries(&mAddCompletes, mAddExpirations);
+ ExpireEntries(&mSubPrefixes, mSubExpirations);
+ ExpireEntries(&mSubCompletes, mSubExpirations);
+
+ mAddChunks.Remove(mAddExpirations);
+ mSubChunks.Remove(mSubExpirations);
+
+ mAddExpirations.Clear();
+ mSubExpirations.Clear();
+
+ return NS_OK;
+}
+
+template<class T>
+nsresult DeflateWriteTArray(nsIOutputStream* aStream, nsTArray<T>& aIn)
+{
+ uLongf insize = aIn.Length() * sizeof(T);
+ uLongf outsize = compressBound(insize);
+ FallibleTArray<char> outBuff;
+ if (!outBuff.SetLength(outsize, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int zerr = compress(reinterpret_cast<Bytef*>(outBuff.Elements()),
+ &outsize,
+ reinterpret_cast<const Bytef*>(aIn.Elements()),
+ insize);
+ if (zerr != Z_OK) {
+ return NS_ERROR_FAILURE;
+ }
+ LOG(("DeflateWriteTArray: %d in %d out", insize, outsize));
+
+ outBuff.TruncateLength(outsize);
+
+ // Length of compressed data stream
+ uint32_t dataLen = outBuff.Length();
+ uint32_t written;
+ nsresult rv = aStream->Write(reinterpret_cast<char*>(&dataLen), sizeof(dataLen), &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(written == sizeof(dataLen), "Error writing deflate length");
+
+ // Store to stream
+ rv = WriteTArray(aStream, outBuff);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+template<class T>
+nsresult InflateReadTArray(nsIInputStream* aStream, FallibleTArray<T>* aOut,
+ uint32_t aExpectedSize)
+{
+
+ uint32_t inLen;
+ uint32_t read;
+ nsresult rv = aStream->Read(reinterpret_cast<char*>(&inLen), sizeof(inLen), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(read == sizeof(inLen), "Error reading inflate length");
+
+ FallibleTArray<char> inBuff;
+ if (!inBuff.SetLength(inLen, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ rv = ReadTArray(aStream, &inBuff, inLen);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uLongf insize = inLen;
+ uLongf outsize = aExpectedSize * sizeof(T);
+ if (!aOut->SetLength(aExpectedSize, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int zerr = uncompress(reinterpret_cast<Bytef*>(aOut->Elements()),
+ &outsize,
+ reinterpret_cast<const Bytef*>(inBuff.Elements()),
+ insize);
+ if (zerr != Z_OK) {
+ return NS_ERROR_FAILURE;
+ }
+ LOG(("InflateReadTArray: %d in %d out", insize, outsize));
+
+ NS_ASSERTION(outsize == aExpectedSize * sizeof(T), "Decompression size mismatch");
+
+ return NS_OK;
+}
+
+static nsresult
+ByteSliceWrite(nsIOutputStream* aOut, nsTArray<uint32_t>& aData)
+{
+ nsTArray<uint8_t> slice;
+ uint32_t count = aData.Length();
+
+ // Only process one slice at a time to avoid using too much memory.
+ if (!slice.SetLength(count, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Process slice 1.
+ for (uint32_t i = 0; i < count; i++) {
+ slice[i] = (aData[i] >> 24);
+ }
+
+ nsresult rv = DeflateWriteTArray(aOut, slice);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Process slice 2.
+ for (uint32_t i = 0; i < count; i++) {
+ slice[i] = ((aData[i] >> 16) & 0xFF);
+ }
+
+ rv = DeflateWriteTArray(aOut, slice);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Process slice 3.
+ for (uint32_t i = 0; i < count; i++) {
+ slice[i] = ((aData[i] >> 8) & 0xFF);
+ }
+
+ rv = DeflateWriteTArray(aOut, slice);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Process slice 4.
+ for (uint32_t i = 0; i < count; i++) {
+ slice[i] = (aData[i] & 0xFF);
+ }
+
+ // The LSB slice is generally uncompressible, don't bother
+ // compressing it.
+ rv = WriteTArray(aOut, slice);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+static nsresult
+ByteSliceRead(nsIInputStream* aInStream, FallibleTArray<uint32_t>* aData, uint32_t count)
+{
+ FallibleTArray<uint8_t> slice1;
+ FallibleTArray<uint8_t> slice2;
+ FallibleTArray<uint8_t> slice3;
+ FallibleTArray<uint8_t> slice4;
+
+ nsresult rv = InflateReadTArray(aInStream, &slice1, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InflateReadTArray(aInStream, &slice2, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = InflateReadTArray(aInStream, &slice3, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ReadTArray(aInStream, &slice4, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!aData->SetCapacity(count, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ aData->AppendElement((slice1[i] << 24) |
+ (slice2[i] << 16) |
+ (slice3[i] << 8) |
+ (slice4[i]),
+ fallible);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::ReadAddPrefixes()
+{
+ FallibleTArray<uint32_t> chunks;
+ uint32_t count = mHeader.numAddPrefixes;
+
+ nsresult rv = ByteSliceRead(mInputStream, &chunks, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mAddPrefixes.SetCapacity(count, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ for (uint32_t i = 0; i < count; i++) {
+ AddPrefix *add = mAddPrefixes.AppendElement(fallible);
+ add->prefix.FromUint32(0);
+ add->addChunk = chunks[i];
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::ReadSubPrefixes()
+{
+ FallibleTArray<uint32_t> addchunks;
+ FallibleTArray<uint32_t> subchunks;
+ FallibleTArray<uint32_t> prefixes;
+ uint32_t count = mHeader.numSubPrefixes;
+
+ nsresult rv = ByteSliceRead(mInputStream, &addchunks, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ByteSliceRead(mInputStream, &subchunks, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ByteSliceRead(mInputStream, &prefixes, count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mSubPrefixes.SetCapacity(count, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ for (uint32_t i = 0; i < count; i++) {
+ SubPrefix *sub = mSubPrefixes.AppendElement(fallible);
+ sub->addChunk = addchunks[i];
+ sub->prefix.FromUint32(prefixes[i]);
+ sub->subChunk = subchunks[i];
+ }
+
+ return NS_OK;
+}
+
+// Split up PrefixArray back into the constituents
+nsresult
+HashStore::WriteAddPrefixes(nsIOutputStream* aOut)
+{
+ nsTArray<uint32_t> chunks;
+ uint32_t count = mAddPrefixes.Length();
+ if (!chunks.SetCapacity(count, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ chunks.AppendElement(mAddPrefixes[i].Chunk());
+ }
+
+ nsresult rv = ByteSliceWrite(aOut, chunks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::WriteSubPrefixes(nsIOutputStream* aOut)
+{
+ nsTArray<uint32_t> addchunks;
+ nsTArray<uint32_t> subchunks;
+ nsTArray<uint32_t> prefixes;
+ uint32_t count = mSubPrefixes.Length();
+ addchunks.SetCapacity(count);
+ subchunks.SetCapacity(count);
+ prefixes.SetCapacity(count);
+
+ for (uint32_t i = 0; i < count; i++) {
+ addchunks.AppendElement(mSubPrefixes[i].AddChunk());
+ prefixes.AppendElement(mSubPrefixes[i].PrefixHash().ToUint32());
+ subchunks.AppendElement(mSubPrefixes[i].Chunk());
+ }
+
+ nsresult rv = ByteSliceWrite(aOut, addchunks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ByteSliceWrite(aOut, subchunks);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ByteSliceWrite(aOut, prefixes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::WriteFile()
+{
+ NS_ASSERTION(mInUpdate, "Must be in update to write database.");
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ nsCOMPtr<nsIFile> storeFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(storeFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = storeFile->AppendNative(mTableName + NS_LITERAL_CSTRING(".sbstore"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIOutputStream> out;
+ rv = NS_NewCheckSummedOutputStream(getter_AddRefs(out), storeFile,
+ PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t written;
+ rv = out->Write(reinterpret_cast<char*>(&mHeader), sizeof(mHeader), &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Write chunk numbers.
+ rv = mAddChunks.Write(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mSubChunks.Write(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Write hashes.
+ rv = WriteAddPrefixes(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = WriteSubPrefixes(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = WriteTArray(out, mAddCompletes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = WriteTArray(out, mSubCompletes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISafeOutputStream> safeOut = do_QueryInterface(out, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = safeOut->Finish();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+template <class T>
+static void
+Erase(FallibleTArray<T>* array, T* iterStart, T* iterEnd)
+{
+ uint32_t start = iterStart - array->Elements();
+ uint32_t count = iterEnd - iterStart;
+
+ if (count > 0) {
+ array->RemoveElementsAt(start, count);
+ }
+}
+
+// Find items matching between |subs| and |adds|, and remove them,
+// recording the item from |adds| in |adds_removed|. To minimize
+// copies, the inputs are processing in parallel, so |subs| and |adds|
+// should be compatibly ordered (either by SBAddPrefixLess or
+// SBAddPrefixHashLess).
+//
+// |predAS| provides add < sub, |predSA| provides sub < add, for the
+// tightest compare appropriate (see calls in SBProcessSubs).
+template<class TSub, class TAdd>
+static void
+KnockoutSubs(FallibleTArray<TSub>* aSubs, FallibleTArray<TAdd>* aAdds)
+{
+ // Keep a pair of output iterators for writing kept items. Due to
+ // deletions, these may lag the main iterators. Using erase() on
+ // individual items would result in O(N^2) copies. Using a list
+ // would work around that, at double or triple the memory cost.
+ TAdd* addOut = aAdds->Elements();
+ TAdd* addIter = aAdds->Elements();
+
+ TSub* subOut = aSubs->Elements();
+ TSub* subIter = aSubs->Elements();
+
+ TAdd* addEnd = addIter + aAdds->Length();
+ TSub* subEnd = subIter + aSubs->Length();
+
+ while (addIter != addEnd && subIter != subEnd) {
+ // additer compare, so it compares on add chunk
+ int32_t cmp = addIter->Compare(*subIter);
+ if (cmp > 0) {
+ // If |*sub_iter| < |*add_iter|, retain the sub.
+ *subOut = *subIter;
+ ++subOut;
+ ++subIter;
+ } else if (cmp < 0) {
+ // If |*add_iter| < |*sub_iter|, retain the add.
+ *addOut = *addIter;
+ ++addOut;
+ ++addIter;
+ } else {
+ // Drop equal items
+ ++addIter;
+ ++subIter;
+ }
+ }
+
+ Erase(aAdds, addOut, addIter);
+ Erase(aSubs, subOut, subIter);
+}
+
+// Remove items in |removes| from |fullHashes|. |fullHashes| and
+// |removes| should be ordered by SBAddPrefix component.
+template <class T>
+static void
+RemoveMatchingPrefixes(const SubPrefixArray& aSubs, FallibleTArray<T>* aFullHashes)
+{
+ // Where to store kept items.
+ T* out = aFullHashes->Elements();
+ T* hashIter = out;
+ T* hashEnd = aFullHashes->Elements() + aFullHashes->Length();
+
+ SubPrefix const * removeIter = aSubs.Elements();
+ SubPrefix const * removeEnd = aSubs.Elements() + aSubs.Length();
+
+ while (hashIter != hashEnd && removeIter != removeEnd) {
+ int32_t cmp = removeIter->CompareAlt(*hashIter);
+ if (cmp > 0) {
+ // Keep items less than |*removeIter|.
+ *out = *hashIter;
+ ++out;
+ ++hashIter;
+ } else if (cmp < 0) {
+ // No hit for |*removeIter|, bump it forward.
+ ++removeIter;
+ } else {
+ // Drop equal items, there may be multiple hits.
+ do {
+ ++hashIter;
+ } while (hashIter != hashEnd &&
+ !(removeIter->CompareAlt(*hashIter) < 0));
+ ++removeIter;
+ }
+ }
+ Erase(aFullHashes, out, hashIter);
+}
+
+static void
+RemoveDeadSubPrefixes(SubPrefixArray& aSubs, ChunkSet& aAddChunks)
+{
+ SubPrefix * subIter = aSubs.Elements();
+ SubPrefix * subEnd = aSubs.Elements() + aSubs.Length();
+
+ for (SubPrefix * iter = subIter; iter != subEnd; iter++) {
+ bool hasChunk = aAddChunks.Has(iter->AddChunk());
+ // Keep the subprefix if the chunk it refers to is one
+ // we haven't seen it yet.
+ if (!hasChunk) {
+ *subIter = *iter;
+ subIter++;
+ }
+ }
+
+ LOG(("Removed %u dead SubPrefix entries.", subEnd - subIter));
+ aSubs.TruncateLength(subIter - aSubs.Elements());
+}
+
+#ifdef DEBUG
+template <class T>
+static void EnsureSorted(FallibleTArray<T>* aArray)
+{
+ T* start = aArray->Elements();
+ T* end = aArray->Elements() + aArray->Length();
+ T* iter = start;
+ T* previous = start;
+
+ while (iter != end) {
+ previous = iter;
+ ++iter;
+ if (iter != end) {
+ MOZ_ASSERT(iter->Compare(*previous) >= 0);
+ }
+ }
+
+ return;
+}
+#endif
+
+nsresult
+HashStore::ProcessSubs()
+{
+#ifdef DEBUG
+ EnsureSorted(&mAddPrefixes);
+ EnsureSorted(&mSubPrefixes);
+ EnsureSorted(&mAddCompletes);
+ EnsureSorted(&mSubCompletes);
+ LOG(("All databases seem to have a consistent sort order."));
+#endif
+
+ RemoveMatchingPrefixes(mSubPrefixes, &mAddCompletes);
+ RemoveMatchingPrefixes(mSubPrefixes, &mSubCompletes);
+
+ // Remove any remaining subbed prefixes from both addprefixes
+ // and addcompletes.
+ KnockoutSubs(&mSubPrefixes, &mAddPrefixes);
+ KnockoutSubs(&mSubCompletes, &mAddCompletes);
+
+ // Remove any remaining subprefixes referring to addchunks that
+ // we have (and hence have been processed above).
+ RemoveDeadSubPrefixes(mSubPrefixes, mAddChunks);
+
+#ifdef DEBUG
+ EnsureSorted(&mAddPrefixes);
+ EnsureSorted(&mSubPrefixes);
+ EnsureSorted(&mAddCompletes);
+ EnsureSorted(&mSubCompletes);
+ LOG(("All databases seem to have a consistent sort order."));
+#endif
+
+ return NS_OK;
+}
+
+nsresult
+HashStore::AugmentAdds(const nsTArray<uint32_t>& aPrefixes)
+{
+ uint32_t cnt = aPrefixes.Length();
+ if (cnt != mAddPrefixes.Length()) {
+ LOG(("Amount of prefixes in cache not consistent with store (%d vs %d)",
+ aPrefixes.Length(), mAddPrefixes.Length()));
+ return NS_ERROR_FAILURE;
+ }
+ for (uint32_t i = 0; i < cnt; i++) {
+ mAddPrefixes[i].prefix.FromUint32(aPrefixes[i]);
+ }
+ return NS_OK;
+}
+
+ChunkSet&
+HashStore::AddChunks()
+{
+ ReadChunkNumbers();
+
+ return mAddChunks;
+}
+
+ChunkSet&
+HashStore::SubChunks()
+{
+ ReadChunkNumbers();
+
+ return mSubChunks;
+}
+
+AddCompleteArray&
+HashStore::AddCompletes()
+{
+ ReadCompletions();
+
+ return mAddCompletes;
+}
+
+SubCompleteArray&
+HashStore::SubCompletes()
+{
+ ReadCompletions();
+
+ return mSubCompletes;
+}
+
+bool
+HashStore::AlreadyReadChunkNumbers()
+{
+ // If there are chunks but chunk set not yet contains any data
+ // Then we haven't read chunk numbers.
+ if ((mHeader.numAddChunks != 0 && mAddChunks.Length() == 0) ||
+ (mHeader.numSubChunks != 0 && mSubChunks.Length() == 0)) {
+ return false;
+ }
+ return true;
+}
+
+bool
+HashStore::AlreadyReadCompletions()
+{
+ // If there are completions but completion set not yet contains any data
+ // Then we haven't read completions.
+ if ((mHeader.numAddCompletes != 0 && mAddCompletes.Length() == 0) ||
+ (mHeader.numSubCompletes != 0 && mSubCompletes.Length() == 0)) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/HashStore.h b/toolkit/components/url-classifier/HashStore.h
new file mode 100644
index 0000000000..3473b2f020
--- /dev/null
+++ b/toolkit/components/url-classifier/HashStore.h
@@ -0,0 +1,314 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 HashStore_h__
+#define HashStore_h__
+
+#include "Entries.h"
+#include "ChunkSet.h"
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsCOMPtr.h"
+#include "nsClassHashtable.h"
+#include "safebrowsing.pb.h"
+#include <string>
+
+namespace mozilla {
+namespace safebrowsing {
+
+// The abstract class of TableUpdateV2 and TableUpdateV4. This
+// is convenient for passing the TableUpdate* around associated
+// with v2 and v4 instance.
+class TableUpdate {
+public:
+ TableUpdate(const nsACString& aTable)
+ : mTable(aTable)
+ {
+ }
+
+ virtual ~TableUpdate() {}
+
+ // To be overriden.
+ virtual bool Empty() const = 0;
+
+ // Common interfaces.
+ const nsCString& TableName() const { return mTable; }
+
+ template<typename T>
+ static T* Cast(TableUpdate* aThat) {
+ return (T::TAG == aThat->Tag() ? reinterpret_cast<T*>(aThat) : nullptr);
+ }
+
+private:
+ virtual int Tag() const = 0;
+
+ nsCString mTable;
+};
+
+// A table update is built from a single update chunk from the server. As the
+// protocol parser processes each chunk, it constructs a table update with the
+// new hashes.
+class TableUpdateV2 : public TableUpdate {
+public:
+ explicit TableUpdateV2(const nsACString& aTable)
+ : TableUpdate(aTable) {}
+
+ bool Empty() const override {
+ return mAddChunks.Length() == 0 &&
+ mSubChunks.Length() == 0 &&
+ mAddExpirations.Length() == 0 &&
+ mSubExpirations.Length() == 0 &&
+ mAddPrefixes.Length() == 0 &&
+ mSubPrefixes.Length() == 0 &&
+ mAddCompletes.Length() == 0 &&
+ mSubCompletes.Length() == 0;
+ }
+
+ // Throughout, uint32_t aChunk refers only to the chunk number. Chunk data is
+ // stored in the Prefix structures.
+ MOZ_MUST_USE nsresult NewAddChunk(uint32_t aChunk) {
+ return mAddChunks.Set(aChunk);
+ };
+ MOZ_MUST_USE nsresult NewSubChunk(uint32_t aChunk) {
+ return mSubChunks.Set(aChunk);
+ };
+ MOZ_MUST_USE nsresult NewAddExpiration(uint32_t aChunk) {
+ return mAddExpirations.Set(aChunk);
+ };
+ MOZ_MUST_USE nsresult NewSubExpiration(uint32_t aChunk) {
+ return mSubExpirations.Set(aChunk);
+ };
+ MOZ_MUST_USE nsresult NewAddPrefix(uint32_t aAddChunk, const Prefix& aPrefix);
+ MOZ_MUST_USE nsresult NewSubPrefix(uint32_t aAddChunk,
+ const Prefix& aPrefix,
+ uint32_t aSubChunk);
+ MOZ_MUST_USE nsresult NewAddComplete(uint32_t aChunk,
+ const Completion& aCompletion);
+ MOZ_MUST_USE nsresult NewSubComplete(uint32_t aAddChunk,
+ const Completion& aCompletion,
+ uint32_t aSubChunk);
+
+ ChunkSet& AddChunks() { return mAddChunks; }
+ ChunkSet& SubChunks() { return mSubChunks; }
+
+ // Expirations for chunks.
+ ChunkSet& AddExpirations() { return mAddExpirations; }
+ ChunkSet& SubExpirations() { return mSubExpirations; }
+
+ // Hashes associated with this chunk.
+ AddPrefixArray& AddPrefixes() { return mAddPrefixes; }
+ SubPrefixArray& SubPrefixes() { return mSubPrefixes; }
+ AddCompleteArray& AddCompletes() { return mAddCompletes; }
+ SubCompleteArray& SubCompletes() { return mSubCompletes; }
+
+ // For downcasting.
+ static const int TAG = 2;
+
+private:
+
+ // The list of chunk numbers that we have for each of the type of chunks.
+ ChunkSet mAddChunks;
+ ChunkSet mSubChunks;
+ ChunkSet mAddExpirations;
+ ChunkSet mSubExpirations;
+
+ // 4-byte sha256 prefixes.
+ AddPrefixArray mAddPrefixes;
+ SubPrefixArray mSubPrefixes;
+
+ // 32-byte hashes.
+ AddCompleteArray mAddCompletes;
+ SubCompleteArray mSubCompletes;
+
+ virtual int Tag() const override { return TAG; }
+};
+
+// Structure for DBService/HashStore/Classifiers to update.
+// It would contain the prefixes (both fixed and variable length)
+// for addition and indices to removal. See Bug 1283009.
+class TableUpdateV4 : public TableUpdate {
+public:
+ struct PrefixStdString {
+ private:
+ std::string mStorage;
+ nsDependentCSubstring mString;
+
+ public:
+ explicit PrefixStdString(std::string& aString)
+ {
+ aString.swap(mStorage);
+ mString.Rebind(mStorage.data(), mStorage.size());
+ };
+
+ const nsACString& GetPrefixString() const { return mString; };
+ };
+
+ typedef nsClassHashtable<nsUint32HashKey, PrefixStdString> PrefixStdStringMap;
+ typedef nsTArray<int32_t> RemovalIndiceArray;
+
+public:
+ explicit TableUpdateV4(const nsACString& aTable)
+ : TableUpdate(aTable)
+ , mFullUpdate(false)
+ {
+ }
+
+ bool Empty() const override
+ {
+ return mPrefixesMap.IsEmpty() && mRemovalIndiceArray.IsEmpty();
+ }
+
+ bool IsFullUpdate() const { return mFullUpdate; }
+ PrefixStdStringMap& Prefixes() { return mPrefixesMap; }
+ RemovalIndiceArray& RemovalIndices() { return mRemovalIndiceArray; }
+ const nsACString& ClientState() const { return mClientState; }
+ const nsACString& Checksum() const { return mChecksum; }
+
+ // For downcasting.
+ static const int TAG = 4;
+
+ void SetFullUpdate(bool aIsFullUpdate) { mFullUpdate = aIsFullUpdate; }
+ void NewPrefixes(int32_t aSize, std::string& aPrefixes);
+ void NewRemovalIndices(const uint32_t* aIndices, size_t aNumOfIndices);
+ void SetNewClientState(const nsACString& aState) { mClientState = aState; }
+ void NewChecksum(const std::string& aChecksum);
+
+private:
+ virtual int Tag() const override { return TAG; }
+
+ bool mFullUpdate;
+ PrefixStdStringMap mPrefixesMap;
+ RemovalIndiceArray mRemovalIndiceArray;
+ nsCString mClientState;
+ nsCString mChecksum;
+};
+
+// There is one hash store per table.
+class HashStore {
+public:
+ HashStore(const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile* aRootStoreFile);
+ ~HashStore();
+
+ const nsCString& TableName() const { return mTableName; }
+
+ nsresult Open();
+ // Add Prefixes are stored partly in the PrefixSet (contains the
+ // Prefix data organized for fast lookup/low RAM usage) and partly in the
+ // HashStore (Add Chunk numbers - only used for updates, slow retrieval).
+ // AugmentAdds function joins the separate datasets into one complete
+ // prefixes+chunknumbers dataset.
+ nsresult AugmentAdds(const nsTArray<uint32_t>& aPrefixes);
+
+ ChunkSet& AddChunks();
+ ChunkSet& SubChunks();
+ AddPrefixArray& AddPrefixes() { return mAddPrefixes; }
+ SubPrefixArray& SubPrefixes() { return mSubPrefixes; }
+ AddCompleteArray& AddCompletes();
+ SubCompleteArray& SubCompletes();
+
+ // =======
+ // Updates
+ // =======
+ // Begin the update process. Reads the store into memory.
+ nsresult BeginUpdate();
+
+ // Imports the data from a TableUpdate.
+ nsresult ApplyUpdate(TableUpdate &aUpdate);
+
+ // Process expired chunks
+ nsresult Expire();
+
+ // Rebuild the store, Incorporating all the applied updates.
+ nsresult Rebuild();
+
+ // Write the current state of the store to disk.
+ // If you call between ApplyUpdate() and Rebuild(), you'll
+ // have a mess on your hands.
+ nsresult WriteFile();
+
+ // Wipe out all Completes.
+ void ClearCompletes();
+
+private:
+ nsresult Reset();
+
+ nsresult ReadHeader();
+ nsresult SanityCheck();
+ nsresult CalculateChecksum(nsAutoCString& aChecksum, uint32_t aFileSize,
+ bool aChecksumPresent);
+ nsresult CheckChecksum(uint32_t aFileSize);
+ void UpdateHeader();
+
+ nsresult ReadCompletions();
+ nsresult ReadChunkNumbers();
+ nsresult ReadHashes();
+
+ nsresult ReadAddPrefixes();
+ nsresult ReadSubPrefixes();
+
+ nsresult WriteAddPrefixes(nsIOutputStream* aOut);
+ nsresult WriteSubPrefixes(nsIOutputStream* aOut);
+
+ nsresult ProcessSubs();
+
+ nsresult PrepareForUpdate();
+
+ bool AlreadyReadChunkNumbers();
+ bool AlreadyReadCompletions();
+
+ // This is used for checking that the database is correct and for figuring out
+ // the number of chunks, etc. to read from disk on restart.
+ struct Header {
+ uint32_t magic;
+ uint32_t version;
+ uint32_t numAddChunks;
+ uint32_t numSubChunks;
+ uint32_t numAddPrefixes;
+ uint32_t numSubPrefixes;
+ uint32_t numAddCompletes;
+ uint32_t numSubCompletes;
+ };
+
+ Header mHeader;
+
+ // The name of the table (must end in -shavar or -digest256, or evidently
+ // -simple for unittesting.
+ nsCString mTableName;
+ nsCOMPtr<nsIFile> mStoreDirectory;
+
+ bool mInUpdate;
+
+ nsCOMPtr<nsIInputStream> mInputStream;
+
+ // Chunk numbers, stored as uint32_t arrays.
+ ChunkSet mAddChunks;
+ ChunkSet mSubChunks;
+
+ ChunkSet mAddExpirations;
+ ChunkSet mSubExpirations;
+
+ // Chunk data for shavar tables. See Entries.h for format.
+ AddPrefixArray mAddPrefixes;
+ SubPrefixArray mSubPrefixes;
+
+ // See bug 806422 for background. We must be able to distinguish between
+ // updates from the completion server and updates from the regular server.
+ AddCompleteArray mAddCompletes;
+ SubCompleteArray mSubCompletes;
+
+ uint32_t mFileSize;
+
+ // For gtest to inspect private members.
+ friend class PerProviderDirectoryTestUtils;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/LookupCache.cpp b/toolkit/components/url-classifier/LookupCache.cpp
new file mode 100644
index 0000000000..5a3b1e36d0
--- /dev/null
+++ b/toolkit/components/url-classifier/LookupCache.cpp
@@ -0,0 +1,599 @@
+//* -*- 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 "LookupCache.h"
+#include "HashStore.h"
+#include "nsISeekableStream.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Logging.h"
+#include "nsNetUtil.h"
+#include "prprf.h"
+#include "Classifier.h"
+
+// We act as the main entry point for all the real lookups,
+// so note that those are not done to the actual HashStore.
+// The latter solely exists to store the data needed to handle
+// the updates from the protocol.
+
+// This module provides a front for PrefixSet, mUpdateCompletions,
+// and mGetHashCache, which together contain everything needed to
+// provide a classification as long as the data is up to date.
+
+// PrefixSet stores and provides lookups for 4-byte prefixes.
+// mUpdateCompletions contains 32-byte completions which were
+// contained in updates. They are retrieved from HashStore/.sbtore
+// on startup.
+// mGetHashCache contains 32-byte completions which were
+// returned from the gethash server. They are not serialized,
+// only cached until the next update.
+
+// Name of the persistent PrefixSet storage
+#define PREFIXSET_SUFFIX ".pset"
+
+// MOZ_LOG=UrlClassifierDbService:5
+extern mozilla::LazyLogModule gUrlClassifierDbServiceLog;
+#define LOG(args) MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug)
+
+namespace mozilla {
+namespace safebrowsing {
+
+const int LookupCacheV2::VER = 2;
+
+LookupCache::LookupCache(const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile* aRootStoreDir)
+ : mPrimed(false)
+ , mTableName(aTableName)
+ , mProvider(aProvider)
+ , mRootStoreDirectory(aRootStoreDir)
+{
+ UpdateRootDirHandle(mRootStoreDirectory);
+}
+
+nsresult
+LookupCache::Open()
+{
+ LOG(("Loading PrefixSet"));
+ nsresult rv = LoadPrefixSet();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+LookupCache::UpdateRootDirHandle(nsIFile* aNewRootStoreDirectory)
+{
+ nsresult rv;
+
+ if (aNewRootStoreDirectory != mRootStoreDirectory) {
+ rv = aNewRootStoreDirectory->Clone(getter_AddRefs(mRootStoreDirectory));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = Classifier::GetPrivateStoreDirectory(mRootStoreDirectory,
+ mTableName,
+ mProvider,
+ getter_AddRefs(mStoreDirectory));
+
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to get private store directory for %s", mTableName.get()));
+ mStoreDirectory = mRootStoreDirectory;
+ }
+
+ if (LOG_ENABLED()) {
+ nsString path;
+ mStoreDirectory->GetPath(path);
+ LOG(("Private store directory for %s is %s", mTableName.get(),
+ NS_ConvertUTF16toUTF8(path).get()));
+ }
+
+ return rv;
+}
+
+nsresult
+LookupCache::Reset()
+{
+ LOG(("LookupCache resetting"));
+
+ nsCOMPtr<nsIFile> prefixsetFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(prefixsetFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefixsetFile->AppendNative(mTableName + NS_LITERAL_CSTRING(PREFIXSET_SUFFIX));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefixsetFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ClearAll();
+
+ return NS_OK;
+}
+
+nsresult
+LookupCache::AddCompletionsToCache(AddCompleteArray& aAddCompletes)
+{
+ for (uint32_t i = 0; i < aAddCompletes.Length(); i++) {
+ if (mGetHashCache.BinaryIndexOf(aAddCompletes[i].CompleteHash()) == mGetHashCache.NoIndex) {
+ mGetHashCache.AppendElement(aAddCompletes[i].CompleteHash());
+ }
+ }
+ mGetHashCache.Sort();
+
+ return NS_OK;
+}
+
+#if defined(DEBUG)
+void
+LookupCache::DumpCache()
+{
+ if (!LOG_ENABLED())
+ return;
+
+ for (uint32_t i = 0; i < mGetHashCache.Length(); i++) {
+ nsAutoCString str;
+ mGetHashCache[i].ToHexString(str);
+ LOG(("Caches: %s", str.get()));
+ }
+}
+#endif
+
+nsresult
+LookupCache::WriteFile()
+{
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ nsCOMPtr<nsIFile> psFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(psFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = psFile->AppendNative(mTableName + NS_LITERAL_CSTRING(PREFIXSET_SUFFIX));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = StoreToFile(psFile);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "failed to store the prefixset");
+
+ return NS_OK;
+}
+
+void
+LookupCache::ClearAll()
+{
+ ClearCache();
+ ClearPrefixes();
+ mPrimed = false;
+}
+
+void
+LookupCache::ClearCache()
+{
+ mGetHashCache.Clear();
+}
+
+/* static */ bool
+LookupCache::IsCanonicalizedIP(const nsACString& aHost)
+{
+ // The canonicalization process will have left IP addresses in dotted
+ // decimal with no surprises.
+ uint32_t i1, i2, i3, i4;
+ char c;
+ if (PR_sscanf(PromiseFlatCString(aHost).get(), "%u.%u.%u.%u%c",
+ &i1, &i2, &i3, &i4, &c) == 4) {
+ return (i1 <= 0xFF && i2 <= 0xFF && i3 <= 0xFF && i4 <= 0xFF);
+ }
+
+ return false;
+}
+
+/* static */ nsresult
+LookupCache::GetLookupFragments(const nsACString& aSpec,
+ nsTArray<nsCString>* aFragments)
+
+{
+ aFragments->Clear();
+
+ nsACString::const_iterator begin, end, iter;
+ aSpec.BeginReading(begin);
+ aSpec.EndReading(end);
+
+ iter = begin;
+ if (!FindCharInReadable('/', iter, end)) {
+ return NS_OK;
+ }
+
+ const nsCSubstring& host = Substring(begin, iter++);
+ nsAutoCString path;
+ path.Assign(Substring(iter, end));
+
+ /**
+ * From the protocol doc:
+ * For the hostname, the client will try at most 5 different strings. They
+ * are:
+ * a) The exact hostname of the url
+ * b) The 4 hostnames formed by starting with the last 5 components and
+ * successivly removing the leading component. The top-level component
+ * can be skipped. This is not done if the hostname is a numerical IP.
+ */
+ nsTArray<nsCString> hosts;
+ hosts.AppendElement(host);
+
+ if (!IsCanonicalizedIP(host)) {
+ host.BeginReading(begin);
+ host.EndReading(end);
+ int numHostComponents = 0;
+ while (RFindInReadable(NS_LITERAL_CSTRING("."), begin, end) &&
+ numHostComponents < MAX_HOST_COMPONENTS) {
+ // don't bother checking toplevel domains
+ if (++numHostComponents >= 2) {
+ host.EndReading(iter);
+ hosts.AppendElement(Substring(end, iter));
+ }
+ end = begin;
+ host.BeginReading(begin);
+ }
+ }
+
+ /**
+ * From the protocol doc:
+ * For the path, the client will also try at most 6 different strings.
+ * They are:
+ * a) the exact path of the url, including query parameters
+ * b) the exact path of the url, without query parameters
+ * c) the 4 paths formed by starting at the root (/) and
+ * successively appending path components, including a trailing
+ * slash. This behavior should only extend up to the next-to-last
+ * path component, that is, a trailing slash should never be
+ * appended that was not present in the original url.
+ */
+ nsTArray<nsCString> paths;
+ nsAutoCString pathToAdd;
+
+ path.BeginReading(begin);
+ path.EndReading(end);
+ iter = begin;
+ if (FindCharInReadable('?', iter, end)) {
+ pathToAdd = Substring(begin, iter);
+ paths.AppendElement(pathToAdd);
+ end = iter;
+ }
+
+ int numPathComponents = 1;
+ iter = begin;
+ while (FindCharInReadable('/', iter, end) &&
+ numPathComponents < MAX_PATH_COMPONENTS) {
+ iter++;
+ pathToAdd.Assign(Substring(begin, iter));
+ paths.AppendElement(pathToAdd);
+ numPathComponents++;
+ }
+
+ // If we haven't already done so, add the full path
+ if (!pathToAdd.Equals(path)) {
+ paths.AppendElement(path);
+ }
+ // Check an empty path (for whole-domain blacklist entries)
+ paths.AppendElement(EmptyCString());
+
+ for (uint32_t hostIndex = 0; hostIndex < hosts.Length(); hostIndex++) {
+ for (uint32_t pathIndex = 0; pathIndex < paths.Length(); pathIndex++) {
+ nsCString key;
+ key.Assign(hosts[hostIndex]);
+ key.Append('/');
+ key.Append(paths[pathIndex]);
+ LOG(("Checking fragment %s", key.get()));
+
+ aFragments->AppendElement(key);
+ }
+ }
+
+ return NS_OK;
+}
+
+/* static */ nsresult
+LookupCache::GetHostKeys(const nsACString& aSpec,
+ nsTArray<nsCString>* aHostKeys)
+{
+ nsACString::const_iterator begin, end, iter;
+ aSpec.BeginReading(begin);
+ aSpec.EndReading(end);
+
+ iter = begin;
+ if (!FindCharInReadable('/', iter, end)) {
+ return NS_OK;
+ }
+
+ const nsCSubstring& host = Substring(begin, iter);
+
+ if (IsCanonicalizedIP(host)) {
+ nsCString *key = aHostKeys->AppendElement();
+ if (!key)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ key->Assign(host);
+ key->Append("/");
+ return NS_OK;
+ }
+
+ nsTArray<nsCString> hostComponents;
+ ParseString(PromiseFlatCString(host), '.', hostComponents);
+
+ if (hostComponents.Length() < 2) {
+ // no host or toplevel host, this won't match anything in the db
+ return NS_OK;
+ }
+
+ // First check with two domain components
+ int32_t last = int32_t(hostComponents.Length()) - 1;
+ nsCString *lookupHost = aHostKeys->AppendElement();
+ if (!lookupHost)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ lookupHost->Assign(hostComponents[last - 1]);
+ lookupHost->Append(".");
+ lookupHost->Append(hostComponents[last]);
+ lookupHost->Append("/");
+
+ // Now check with three domain components
+ if (hostComponents.Length() > 2) {
+ nsCString *lookupHost2 = aHostKeys->AppendElement();
+ if (!lookupHost2)
+ return NS_ERROR_OUT_OF_MEMORY;
+ lookupHost2->Assign(hostComponents[last - 2]);
+ lookupHost2->Append(".");
+ lookupHost2->Append(*lookupHost);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LookupCache::LoadPrefixSet()
+{
+ nsCOMPtr<nsIFile> psFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(psFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = psFile->AppendNative(mTableName + NS_LITERAL_CSTRING(PREFIXSET_SUFFIX));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool exists;
+ rv = psFile->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (exists) {
+ LOG(("stored PrefixSet exists, loading from disk"));
+ rv = LoadFromFile(psFile);
+ if (NS_FAILED(rv)) {
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ Reset();
+ }
+ return rv;
+ }
+ mPrimed = true;
+ } else {
+ LOG(("no (usable) stored PrefixSet found"));
+ }
+
+#ifdef DEBUG
+ if (mPrimed) {
+ uint32_t size = SizeOfPrefixSet();
+ LOG(("SB tree done, size = %d bytes\n", size));
+ }
+#endif
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV2::Init()
+{
+ mPrefixSet = new nsUrlClassifierPrefixSet();
+ nsresult rv = mPrefixSet->Init(mTableName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV2::Open()
+{
+ nsresult rv = LookupCache::Open();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Reading Completions"));
+ rv = ReadCompletions();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+void
+LookupCacheV2::ClearAll()
+{
+ LookupCache::ClearAll();
+ mUpdateCompletions.Clear();
+}
+
+nsresult
+LookupCacheV2::Has(const Completion& aCompletion,
+ bool* aHas, bool* aComplete)
+{
+ *aHas = *aComplete = false;
+
+ uint32_t prefix = aCompletion.ToUint32();
+
+ bool found;
+ nsresult rv = mPrefixSet->Contains(prefix, &found);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Probe in %s: %X, found %d", mTableName.get(), prefix, found));
+
+ if (found) {
+ *aHas = true;
+ }
+
+ if ((mGetHashCache.BinaryIndexOf(aCompletion) != nsTArray<Completion>::NoIndex) ||
+ (mUpdateCompletions.BinaryIndexOf(aCompletion) != nsTArray<Completion>::NoIndex)) {
+ LOG(("Complete in %s", mTableName.get()));
+ *aComplete = true;
+ *aHas = true;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV2::Build(AddPrefixArray& aAddPrefixes,
+ AddCompleteArray& aAddCompletes)
+{
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_LC_COMPLETIONS,
+ static_cast<uint32_t>(aAddCompletes.Length()));
+
+ mUpdateCompletions.Clear();
+ mUpdateCompletions.SetCapacity(aAddCompletes.Length());
+ for (uint32_t i = 0; i < aAddCompletes.Length(); i++) {
+ mUpdateCompletions.AppendElement(aAddCompletes[i].CompleteHash());
+ }
+ aAddCompletes.Clear();
+ mUpdateCompletions.Sort();
+
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_LC_PREFIXES,
+ static_cast<uint32_t>(aAddPrefixes.Length()));
+
+ nsresult rv = ConstructPrefixSet(aAddPrefixes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mPrimed = true;
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV2::GetPrefixes(FallibleTArray<uint32_t>& aAddPrefixes)
+{
+ if (!mPrimed) {
+ // This can happen if its a new table, so no error.
+ LOG(("GetPrefixes from empty LookupCache"));
+ return NS_OK;
+ }
+ return mPrefixSet->GetPrefixesNative(aAddPrefixes);
+}
+
+nsresult
+LookupCacheV2::ReadCompletions()
+{
+ HashStore store(mTableName, mProvider, mRootStoreDirectory);
+
+ nsresult rv = store.Open();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mUpdateCompletions.Clear();
+
+ const AddCompleteArray& addComplete = store.AddCompletes();
+ for (uint32_t i = 0; i < addComplete.Length(); i++) {
+ mUpdateCompletions.AppendElement(addComplete[i].complete);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV2::ClearPrefixes()
+{
+ return mPrefixSet->SetPrefixes(nullptr, 0);
+}
+
+nsresult
+LookupCacheV2::StoreToFile(nsIFile* aFile)
+{
+ return mPrefixSet->StoreToFile(aFile);
+}
+
+nsresult
+LookupCacheV2::LoadFromFile(nsIFile* aFile)
+{
+ return mPrefixSet->LoadFromFile(aFile);
+}
+
+size_t
+LookupCacheV2::SizeOfPrefixSet()
+{
+ return mPrefixSet->SizeOfIncludingThis(moz_malloc_size_of);
+}
+
+#ifdef DEBUG
+template <class T>
+static void EnsureSorted(T* aArray)
+{
+ typename T::elem_type* start = aArray->Elements();
+ typename T::elem_type* end = aArray->Elements() + aArray->Length();
+ typename T::elem_type* iter = start;
+ typename T::elem_type* previous = start;
+
+ while (iter != end) {
+ previous = iter;
+ ++iter;
+ if (iter != end) {
+ MOZ_ASSERT(*previous <= *iter);
+ }
+ }
+ return;
+}
+#endif
+
+nsresult
+LookupCacheV2::ConstructPrefixSet(AddPrefixArray& aAddPrefixes)
+{
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_PS_CONSTRUCT_TIME> timer;
+
+ nsTArray<uint32_t> array;
+ if (!array.SetCapacity(aAddPrefixes.Length(), fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < aAddPrefixes.Length(); i++) {
+ array.AppendElement(aAddPrefixes[i].PrefixHash().ToUint32());
+ }
+ aAddPrefixes.Clear();
+
+#ifdef DEBUG
+ // PrefixSet requires sorted order
+ EnsureSorted(&array);
+#endif
+
+ // construct new one, replace old entries
+ nsresult rv = mPrefixSet->SetPrefixes(array.Elements(), array.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef DEBUG
+ uint32_t size;
+ size = mPrefixSet->SizeOfIncludingThis(moz_malloc_size_of);
+ LOG(("SB tree done, size = %d bytes\n", size));
+#endif
+
+ mPrimed = true;
+
+ return NS_OK;
+}
+
+#if defined(DEBUG)
+void
+LookupCacheV2::DumpCompletions()
+{
+ if (!LOG_ENABLED())
+ return;
+
+ for (uint32_t i = 0; i < mUpdateCompletions.Length(); i++) {
+ nsAutoCString str;
+ mUpdateCompletions[i].ToHexString(str);
+ LOG(("Update: %s", str.get()));
+ }
+}
+#endif
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/LookupCache.h b/toolkit/components/url-classifier/LookupCache.h
new file mode 100644
index 0000000000..d815ed4fc4
--- /dev/null
+++ b/toolkit/components/url-classifier/LookupCache.h
@@ -0,0 +1,213 @@
+//* -*- 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/. */
+
+#ifndef LookupCache_h__
+#define LookupCache_h__
+
+#include "Entries.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "mozilla/RefPtr.h"
+#include "nsUrlClassifierPrefixSet.h"
+#include "VariableLengthPrefixSet.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+#define MAX_HOST_COMPONENTS 5
+#define MAX_PATH_COMPONENTS 4
+
+class LookupResult {
+public:
+ LookupResult() : mComplete(false), mNoise(false),
+ mFresh(false), mProtocolConfirmed(false) {}
+
+ // The fragment that matched in the LookupCache
+ union {
+ Prefix prefix;
+ Completion complete;
+ } hash;
+
+ const Prefix &PrefixHash() {
+ return hash.prefix;
+ }
+ const Completion &CompleteHash() {
+ MOZ_ASSERT(!mNoise);
+ return hash.complete;
+ }
+
+ bool Confirmed() const { return (mComplete && mFresh) || mProtocolConfirmed; }
+ bool Complete() const { return mComplete; }
+
+ // True if we have a complete match for this hash in the table.
+ bool mComplete;
+
+ // True if this is a noise entry, i.e. an extra entry
+ // that is inserted to mask the true URL we are requesting.
+ // Noise entries will not have a complete 256-bit hash as
+ // they are fetched from the local 32-bit database and we
+ // don't know the corresponding full URL.
+ bool mNoise;
+
+ // True if we've updated this table recently-enough.
+ bool mFresh;
+
+ bool mProtocolConfirmed;
+
+ nsCString mTableName;
+};
+
+typedef nsTArray<LookupResult> LookupResultArray;
+
+struct CacheResult {
+ AddComplete entry;
+ nsCString table;
+
+ bool operator==(const CacheResult& aOther) const {
+ if (entry != aOther.entry) {
+ return false;
+ }
+ return table == aOther.table;
+ }
+};
+typedef nsTArray<CacheResult> CacheResultArray;
+
+class LookupCache {
+public:
+ // Check for a canonicalized IP address.
+ static bool IsCanonicalizedIP(const nsACString& aHost);
+
+ // take a lookup string (www.hostname.com/path/to/resource.html) and
+ // expand it into the set of fragments that should be searched for in an
+ // entry
+ static nsresult GetLookupFragments(const nsACString& aSpec,
+ nsTArray<nsCString>* aFragments);
+ // Similar to GetKey(), but if the domain contains three or more components,
+ // two keys will be returned:
+ // hostname.com/foo/bar -> [hostname.com]
+ // mail.hostname.com/foo/bar -> [hostname.com, mail.hostname.com]
+ // www.mail.hostname.com/foo/bar -> [hostname.com, mail.hostname.com]
+ static nsresult GetHostKeys(const nsACString& aSpec,
+ nsTArray<nsCString>* aHostKeys);
+
+ LookupCache(const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile* aStoreFile);
+ virtual ~LookupCache() {}
+
+ const nsCString &TableName() const { return mTableName; }
+
+ // The directory handle where we operate will
+ // be moved away when a backup is made.
+ nsresult UpdateRootDirHandle(nsIFile* aRootStoreDirectory);
+
+ // This will Clear() the passed arrays when done.
+ nsresult AddCompletionsToCache(AddCompleteArray& aAddCompletes);
+
+ // Write data stored in lookup cache to disk.
+ nsresult WriteFile();
+
+ // Clear completions retrieved from gethash request.
+ void ClearCache();
+
+ bool IsPrimed() const { return mPrimed; };
+
+#if DEBUG
+ void DumpCache();
+#endif
+
+ virtual nsresult Open();
+ virtual nsresult Init() = 0;
+ virtual nsresult ClearPrefixes() = 0;
+ virtual nsresult Has(const Completion& aCompletion,
+ bool* aHas, bool* aComplete) = 0;
+
+ virtual void ClearAll();
+
+ template<typename T>
+ static T* Cast(LookupCache* aThat) {
+ return ((aThat && T::VER == aThat->Ver()) ? reinterpret_cast<T*>(aThat) : nullptr);
+ }
+
+private:
+ nsresult Reset();
+ nsresult LoadPrefixSet();
+
+ virtual nsresult StoreToFile(nsIFile* aFile) = 0;
+ virtual nsresult LoadFromFile(nsIFile* aFile) = 0;
+ virtual size_t SizeOfPrefixSet() = 0;
+
+ virtual int Ver() const = 0;
+
+protected:
+ bool mPrimed;
+ nsCString mTableName;
+ nsCString mProvider;
+ nsCOMPtr<nsIFile> mRootStoreDirectory;
+ nsCOMPtr<nsIFile> mStoreDirectory;
+
+ // Full length hashes obtained in gethash request
+ CompletionArray mGetHashCache;
+
+ // For gtest to inspect private members.
+ friend class PerProviderDirectoryTestUtils;
+};
+
+class LookupCacheV2 final : public LookupCache
+{
+public:
+ explicit LookupCacheV2(const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile* aStoreFile)
+ : LookupCache(aTableName, aProvider, aStoreFile) {}
+ ~LookupCacheV2() {}
+
+ virtual nsresult Init() override;
+ virtual nsresult Open() override;
+ virtual void ClearAll() override;
+ virtual nsresult Has(const Completion& aCompletion,
+ bool* aHas, bool* aComplete) override;
+
+ nsresult Build(AddPrefixArray& aAddPrefixes,
+ AddCompleteArray& aAddCompletes);
+
+ nsresult GetPrefixes(FallibleTArray<uint32_t>& aAddPrefixes);
+
+#if DEBUG
+ void DumpCompletions();
+#endif
+
+ static const int VER;
+
+protected:
+ nsresult ReadCompletions();
+
+ virtual nsresult ClearPrefixes() override;
+ virtual nsresult StoreToFile(nsIFile* aFile) override;
+ virtual nsresult LoadFromFile(nsIFile* aFile) override;
+ virtual size_t SizeOfPrefixSet() override;
+
+private:
+ virtual int Ver() const override { return VER; }
+
+ // Construct a Prefix Set with known prefixes.
+ // This will Clear() aAddPrefixes when done.
+ nsresult ConstructPrefixSet(AddPrefixArray& aAddPrefixes);
+
+ // Full length hashes obtained in update request
+ CompletionArray mUpdateCompletions;
+
+ // Set of prefixes known to be in the database
+ RefPtr<nsUrlClassifierPrefixSet> mPrefixSet;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/LookupCacheV4.cpp b/toolkit/components/url-classifier/LookupCacheV4.cpp
new file mode 100644
index 0000000000..7258ae3583
--- /dev/null
+++ b/toolkit/components/url-classifier/LookupCacheV4.cpp
@@ -0,0 +1,584 @@
+//* -*- 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 "LookupCacheV4.h"
+#include "HashStore.h"
+#include "mozilla/Unused.h"
+#include <string>
+
+// MOZ_LOG=UrlClassifierDbService:5
+extern mozilla::LazyLogModule gUrlClassifierDbServiceLog;
+#define LOG(args) MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug)
+
+#define METADATA_SUFFIX NS_LITERAL_CSTRING(".metadata")
+
+namespace mozilla {
+namespace safebrowsing {
+
+const int LookupCacheV4::VER = 4;
+
+// Prefixes coming from updates and VLPrefixSet are both stored in the HashTable
+// where the (key, value) pair is a prefix size and a lexicographic-sorted string.
+// The difference is prefixes from updates use std:string(to avoid additional copies)
+// and prefixes from VLPrefixSet use nsCString.
+// This class provides a common interface for the partial update algorithm to make it
+// easier to operate on two different kind prefix string map..
+class VLPrefixSet
+{
+public:
+ explicit VLPrefixSet(const PrefixStringMap& aMap);
+ explicit VLPrefixSet(const TableUpdateV4::PrefixStdStringMap& aMap);
+
+ // This function will merge the prefix map in VLPrefixSet to aPrefixMap.
+ void Merge(PrefixStringMap& aPrefixMap);
+
+ // Find the smallest string from the map in VLPrefixSet.
+ bool GetSmallestPrefix(nsDependentCSubstring& aOutString);
+
+ // Return the number of prefixes in the map
+ uint32_t Count() const { return mCount; }
+
+private:
+ // PrefixString structure contains a lexicographic-sorted string with
+ // a |pos| variable to indicate which substring we are pointing to right now.
+ // |pos| increases each time GetSmallestPrefix finds the smallest string.
+ struct PrefixString {
+ PrefixString(const nsACString& aStr, uint32_t aSize)
+ : pos(0)
+ , size(aSize)
+ {
+ data.Rebind(aStr.BeginReading(), aStr.Length());
+ }
+
+ const char* get() {
+ return pos < data.Length() ? data.BeginReading() + pos : nullptr;
+ }
+ void next() { pos += size; }
+ uint32_t remaining() { return data.Length() - pos; }
+
+ nsDependentCSubstring data;
+ uint32_t pos;
+ uint32_t size;
+ };
+
+ nsClassHashtable<nsUint32HashKey, PrefixString> mMap;
+ uint32_t mCount;
+};
+
+nsresult
+LookupCacheV4::Init()
+{
+ mVLPrefixSet = new VariableLengthPrefixSet();
+ nsresult rv = mVLPrefixSet->Init(mTableName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV4::Has(const Completion& aCompletion,
+ bool* aHas, bool* aComplete)
+{
+ *aHas = false;
+
+ uint32_t length = 0;
+ nsDependentCSubstring fullhash;
+ fullhash.Rebind((const char *)aCompletion.buf, COMPLETE_SIZE);
+
+ nsresult rv = mVLPrefixSet->Matches(fullhash, &length);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aHas = length >= PREFIX_SIZE;
+ *aComplete = length == COMPLETE_SIZE;
+
+ if (LOG_ENABLED()) {
+ uint32_t prefix = aCompletion.ToUint32();
+ LOG(("Probe in V4 %s: %X, found %d, complete %d", mTableName.get(),
+ prefix, *aHas, *aComplete));
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV4::Build(PrefixStringMap& aPrefixMap)
+{
+ return mVLPrefixSet->SetPrefixes(aPrefixMap);
+}
+
+nsresult
+LookupCacheV4::GetPrefixes(PrefixStringMap& aPrefixMap)
+{
+ return mVLPrefixSet->GetPrefixes(aPrefixMap);
+}
+
+nsresult
+LookupCacheV4::ClearPrefixes()
+{
+ // Clear by seting a empty map
+ PrefixStringMap map;
+ return mVLPrefixSet->SetPrefixes(map);
+}
+
+nsresult
+LookupCacheV4::StoreToFile(nsIFile* aFile)
+{
+ return mVLPrefixSet->StoreToFile(aFile);
+}
+
+nsresult
+LookupCacheV4::LoadFromFile(nsIFile* aFile)
+{
+ nsresult rv = mVLPrefixSet->LoadFromFile(aFile);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCString state, checksum;
+ rv = LoadMetadata(state, checksum);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = VerifyChecksum(checksum);
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_VLPS_LOAD_CORRUPT,
+ rv == NS_ERROR_FILE_CORRUPTED);
+
+ return rv;
+}
+
+size_t
+LookupCacheV4::SizeOfPrefixSet()
+{
+ return mVLPrefixSet->SizeOfIncludingThis(moz_malloc_size_of);
+}
+
+static void
+AppendPrefixToMap(PrefixStringMap& prefixes, nsDependentCSubstring& prefix)
+{
+ if (!prefix.Length()) {
+ return;
+ }
+
+ nsCString* prefixString = prefixes.LookupOrAdd(prefix.Length());
+ prefixString->Append(prefix.BeginReading(), prefix.Length());
+}
+
+// Read prefix into a buffer and also update the hash which
+// keeps track of the checksum
+static void
+UpdateChecksum(nsICryptoHash* aCrypto, const nsACString& aPrefix)
+{
+ MOZ_ASSERT(aCrypto);
+ aCrypto->Update(reinterpret_cast<uint8_t*>(const_cast<char*>(
+ aPrefix.BeginReading())),
+ aPrefix.Length());
+}
+
+// Please see https://bug1287058.bmoattachments.org/attachment.cgi?id=8795366
+// for detail about partial update algorithm.
+nsresult
+LookupCacheV4::ApplyUpdate(TableUpdateV4* aTableUpdate,
+ PrefixStringMap& aInputMap,
+ PrefixStringMap& aOutputMap)
+{
+ MOZ_ASSERT(aOutputMap.IsEmpty());
+
+ nsCOMPtr<nsICryptoHash> crypto;
+ nsresult rv = InitCrypto(crypto);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // oldPSet contains prefixes we already have or we just merged last round.
+ // addPSet contains prefixes stored in tableUpdate which should be merged with oldPSet.
+ VLPrefixSet oldPSet(aInputMap);
+ VLPrefixSet addPSet(aTableUpdate->Prefixes());
+
+ // RemovalIndiceArray is a sorted integer array indicating the index of prefix we should
+ // remove from the old prefix set(according to lexigraphic order).
+ // |removalIndex| is the current index of RemovalIndiceArray.
+ // |numOldPrefixPicked| is used to record how many prefixes we picked from the old map.
+ TableUpdateV4::RemovalIndiceArray& removalArray = aTableUpdate->RemovalIndices();
+ uint32_t removalIndex = 0;
+ int32_t numOldPrefixPicked = -1;
+
+ nsDependentCSubstring smallestOldPrefix;
+ nsDependentCSubstring smallestAddPrefix;
+
+ bool isOldMapEmpty = false, isAddMapEmpty = false;
+
+ // This is used to avoid infinite loop for partial update algorithm.
+ // The maximum loops will be the number of old prefixes plus the number of add prefixes.
+ int32_t index = oldPSet.Count() + addPSet.Count() + 1;
+ for(;index > 0; index--) {
+ // Get smallest prefix from the old prefix set if we don't have one
+ if (smallestOldPrefix.IsEmpty() && !isOldMapEmpty) {
+ isOldMapEmpty = !oldPSet.GetSmallestPrefix(smallestOldPrefix);
+ }
+
+ // Get smallest prefix from add prefix set if we don't have one
+ if (smallestAddPrefix.IsEmpty() && !isAddMapEmpty) {
+ isAddMapEmpty = !addPSet.GetSmallestPrefix(smallestAddPrefix);
+ }
+
+ bool pickOld;
+
+ // If both prefix sets are not empty, then compare to find the smaller one.
+ if (!isOldMapEmpty && !isAddMapEmpty) {
+ if (smallestOldPrefix == smallestAddPrefix) {
+ LOG(("Add prefix should not exist in the original prefix set."));
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+ DUPLICATE_PREFIX);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Compare the smallest string in old prefix set and add prefix set,
+ // merge the smaller one into new map to ensure merged string still
+ // follows lexigraphic order.
+ pickOld = smallestOldPrefix < smallestAddPrefix;
+ } else if (!isOldMapEmpty && isAddMapEmpty) {
+ pickOld = true;
+ } else if (isOldMapEmpty && !isAddMapEmpty) {
+ pickOld = false;
+ // If both maps are empty, then partial update is complete.
+ } else {
+ break;
+ }
+
+ if (pickOld) {
+ numOldPrefixPicked++;
+
+ // If the number of picks from old map matches the removalIndex, then this prefix
+ // will be removed by not merging it to new map.
+ if (removalIndex < removalArray.Length() &&
+ numOldPrefixPicked == removalArray[removalIndex]) {
+ removalIndex++;
+ } else {
+ AppendPrefixToMap(aOutputMap, smallestOldPrefix);
+ UpdateChecksum(crypto, smallestOldPrefix);
+ }
+ smallestOldPrefix.SetLength(0);
+ } else {
+ AppendPrefixToMap(aOutputMap, smallestAddPrefix);
+ UpdateChecksum(crypto, smallestAddPrefix);
+
+ smallestAddPrefix.SetLength(0);
+ }
+ }
+
+ // We expect index will be greater to 0 because max number of runs will be
+ // the number of original prefix plus add prefix.
+ if (index <= 0) {
+ LOG(("There are still prefixes remaining after reaching maximum runs."));
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+ INFINITE_LOOP);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (removalIndex < removalArray.Length()) {
+ LOG(("There are still prefixes to remove after exhausting the old PrefixSet."));
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+ WRONG_REMOVAL_INDICES);
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString checksum;
+ crypto->Finish(false, checksum);
+ if (aTableUpdate->Checksum().IsEmpty()) {
+ LOG(("Update checksum missing."));
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+ MISSING_CHECKSUM);
+
+ // Generate our own checksum to tableUpdate to ensure there is always
+ // checksum in .metadata
+ std::string stdChecksum(checksum.BeginReading(), checksum.Length());
+ aTableUpdate->NewChecksum(stdChecksum);
+
+ } else if (aTableUpdate->Checksum() != checksum){
+ LOG(("Checksum mismatch after applying partial update"));
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+ CHECKSUM_MISMATCH);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+LookupCacheV4::InitCrypto(nsCOMPtr<nsICryptoHash>& aCrypto)
+{
+ nsresult rv;
+ aCrypto = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aCrypto->Init(nsICryptoHash::SHA256);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "InitCrypto failed");
+
+ return rv;
+}
+
+nsresult
+LookupCacheV4::VerifyChecksum(const nsACString& aChecksum)
+{
+ nsCOMPtr<nsICryptoHash> crypto;
+ nsresult rv = InitCrypto(crypto);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ PrefixStringMap map;
+ mVLPrefixSet->GetPrefixes(map);
+
+ VLPrefixSet loadPSet(map);
+ uint32_t index = loadPSet.Count() + 1;
+ for(;index > 0; index--) {
+ nsDependentCSubstring prefix;
+ if (!loadPSet.GetSmallestPrefix(prefix)) {
+ break;
+ }
+ UpdateChecksum(crypto, prefix);
+ }
+
+ nsAutoCString checksum;
+ crypto->Finish(false, checksum);
+
+ if (checksum != aChecksum) {
+ LOG(("Checksum mismatch when loading prefixes from file."));
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////
+// A set of lightweight functions for reading/writing value from/to file.
+
+namespace {
+
+template<typename T>
+struct ValueTraits
+{
+ static uint32_t Length(const T& aValue) { return sizeof(T); }
+ static char* WritePtr(T& aValue, uint32_t aLength) { return (char*)&aValue; }
+ static const char* ReadPtr(const T& aValue) { return (char*)&aValue; }
+ static bool IsFixedLength() { return true; }
+};
+
+template<>
+struct ValueTraits<nsACString>
+{
+ static bool IsFixedLength() { return false; }
+
+ static uint32_t Length(const nsACString& aValue)
+ {
+ return aValue.Length();
+ }
+
+ static char* WritePtr(nsACString& aValue, uint32_t aLength)
+ {
+ aValue.SetLength(aLength);
+ return aValue.BeginWriting();
+ }
+
+ static const char* ReadPtr(const nsACString& aValue)
+ {
+ return aValue.BeginReading();
+ }
+};
+
+template<typename T> static nsresult
+WriteValue(nsIOutputStream *aOutputStream, const T& aValue)
+{
+ uint32_t writeLength = ValueTraits<T>::Length(aValue);
+ if (!ValueTraits<T>::IsFixedLength()) {
+ // We need to write out the variable value length.
+ nsresult rv = WriteValue(aOutputStream, writeLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Write out the value.
+ auto valueReadPtr = ValueTraits<T>::ReadPtr(aValue);
+ uint32_t written;
+ nsresult rv = aOutputStream->Write(valueReadPtr, writeLength, &written);
+ if (NS_FAILED(rv) || written != writeLength) {
+ LOG(("Failed to write the value."));
+ return NS_FAILED(rv) ? rv : NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+template<typename T> static nsresult
+ReadValue(nsIInputStream* aInputStream, T& aValue)
+{
+ nsresult rv;
+
+ uint32_t readLength;
+ if (ValueTraits<T>::IsFixedLength()) {
+ readLength = ValueTraits<T>::Length(aValue);
+ } else {
+ // Read the variable value length from file.
+ nsresult rv = ReadValue(aInputStream, readLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Read the value.
+ uint32_t read;
+ auto valueWritePtr = ValueTraits<T>::WritePtr(aValue, readLength);
+ rv = aInputStream->Read(valueWritePtr, readLength, &read);
+ if (NS_FAILED(rv) || read != readLength) {
+ LOG(("Failed to read the value."));
+ return NS_FAILED(rv) ? rv : NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+} // end of unnamed namespace.
+////////////////////////////////////////////////////////////////////////
+
+nsresult
+LookupCacheV4::WriteMetadata(TableUpdateV4* aTableUpdate)
+{
+ NS_ENSURE_ARG_POINTER(aTableUpdate);
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ nsCOMPtr<nsIFile> metaFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(metaFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = metaFile->AppendNative(mTableName + METADATA_SUFFIX);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIOutputStream> outputStream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), metaFile,
+ PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE);
+ if (!NS_SUCCEEDED(rv)) {
+ LOG(("Unable to create file to store metadata."));
+ return rv;
+ }
+
+ // Write the state.
+ rv = WriteValue(outputStream, aTableUpdate->ClientState());
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to write the list state."));
+ return rv;
+ }
+
+ // Write the checksum.
+ rv = WriteValue(outputStream, aTableUpdate->Checksum());
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to write the list checksum."));
+ return rv;
+ }
+
+ return rv;
+}
+
+nsresult
+LookupCacheV4::LoadMetadata(nsACString& aState, nsACString& aChecksum)
+{
+ nsCOMPtr<nsIFile> metaFile;
+ nsresult rv = mStoreDirectory->Clone(getter_AddRefs(metaFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = metaFile->AppendNative(mTableName + METADATA_SUFFIX);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> localInFile;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(localInFile), metaFile,
+ PR_RDONLY | nsIFile::OS_READAHEAD);
+ if (NS_FAILED(rv)) {
+ LOG(("Unable to open metadata file."));
+ return rv;
+ }
+
+ // Read the list state.
+ rv = ReadValue(localInFile, aState);
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to read state."));
+ return rv;
+ }
+
+ // Read the checksum.
+ rv = ReadValue(localInFile, aChecksum);
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to read checksum."));
+ return rv;
+ }
+
+ return rv;
+}
+
+VLPrefixSet::VLPrefixSet(const PrefixStringMap& aMap)
+ : mCount(0)
+{
+ for (auto iter = aMap.ConstIter(); !iter.Done(); iter.Next()) {
+ uint32_t size = iter.Key();
+ mMap.Put(size, new PrefixString(*iter.Data(), size));
+ mCount += iter.Data()->Length() / size;
+ }
+}
+
+VLPrefixSet::VLPrefixSet(const TableUpdateV4::PrefixStdStringMap& aMap)
+ : mCount(0)
+{
+ for (auto iter = aMap.ConstIter(); !iter.Done(); iter.Next()) {
+ uint32_t size = iter.Key();
+ mMap.Put(size, new PrefixString(iter.Data()->GetPrefixString(), size));
+ mCount += iter.Data()->GetPrefixString().Length() / size;
+ }
+}
+
+void
+VLPrefixSet::Merge(PrefixStringMap& aPrefixMap) {
+ for (auto iter = mMap.ConstIter(); !iter.Done(); iter.Next()) {
+ nsCString* prefixString = aPrefixMap.LookupOrAdd(iter.Key());
+ PrefixString* str = iter.Data();
+
+ if (str->get()) {
+ prefixString->Append(str->get(), str->remaining());
+ }
+ }
+}
+
+bool
+VLPrefixSet::GetSmallestPrefix(nsDependentCSubstring& aOutString) {
+ PrefixString* pick = nullptr;
+ for (auto iter = mMap.ConstIter(); !iter.Done(); iter.Next()) {
+ PrefixString* str = iter.Data();
+
+ if (!str->get()) {
+ continue;
+ }
+
+ if (aOutString.IsEmpty()) {
+ aOutString.Rebind(str->get(), iter.Key());
+ pick = str;
+ continue;
+ }
+
+ nsDependentCSubstring cur(str->get(), iter.Key());
+ if (cur < aOutString) {
+ aOutString.Rebind(str->get(), iter.Key());
+ pick = str;
+ }
+ }
+
+ if (pick) {
+ pick->next();
+ }
+
+ return pick != nullptr;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/LookupCacheV4.h b/toolkit/components/url-classifier/LookupCacheV4.h
new file mode 100644
index 0000000000..c2f3cafd25
--- /dev/null
+++ b/toolkit/components/url-classifier/LookupCacheV4.h
@@ -0,0 +1,70 @@
+//* -*- 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/. */
+
+#ifndef LookupCacheV4_h__
+#define LookupCacheV4_h__
+
+#include "LookupCache.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+// Forward declaration.
+class TableUpdateV4;
+
+class LookupCacheV4 final : public LookupCache
+{
+public:
+ explicit LookupCacheV4(const nsACString& aTableName,
+ const nsACString& aProvider,
+ nsIFile* aStoreFile)
+ : LookupCache(aTableName, aProvider, aStoreFile) {}
+ ~LookupCacheV4() {}
+
+ virtual nsresult Init() override;
+ virtual nsresult Has(const Completion& aCompletion,
+ bool* aHas, bool* aComplete) override;
+
+ nsresult Build(PrefixStringMap& aPrefixMap);
+
+ nsresult GetPrefixes(PrefixStringMap& aPrefixMap);
+
+ // ApplyUpdate will merge data stored in aTableUpdate with prefixes in aInputMap.
+ nsresult ApplyUpdate(TableUpdateV4* aTableUpdate,
+ PrefixStringMap& aInputMap,
+ PrefixStringMap& aOutputMap);
+
+ nsresult WriteMetadata(TableUpdateV4* aTableUpdate);
+ nsresult LoadMetadata(nsACString& aState, nsACString& aChecksum);
+
+ static const int VER;
+
+protected:
+ virtual nsresult ClearPrefixes() override;
+ virtual nsresult StoreToFile(nsIFile* aFile) override;
+ virtual nsresult LoadFromFile(nsIFile* aFile) override;
+ virtual size_t SizeOfPrefixSet() override;
+
+private:
+ virtual int Ver() const override { return VER; }
+
+ nsresult InitCrypto(nsCOMPtr<nsICryptoHash>& aCrypto);
+ nsresult VerifyChecksum(const nsACString& aChecksum);
+
+ enum UPDATE_ERROR_TYPES {
+ DUPLICATE_PREFIX = 0,
+ INFINITE_LOOP = 1,
+ WRONG_REMOVAL_INDICES = 2,
+ CHECKSUM_MISMATCH = 3,
+ MISSING_CHECKSUM = 4,
+ };
+
+ RefPtr<VariableLengthPrefixSet> mVLPrefixSet;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/ProtocolParser.cpp b/toolkit/components/url-classifier/ProtocolParser.cpp
new file mode 100644
index 0000000000..5da7787be8
--- /dev/null
+++ b/toolkit/components/url-classifier/ProtocolParser.cpp
@@ -0,0 +1,1108 @@
+//* -*- 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 "ProtocolParser.h"
+#include "LookupCache.h"
+#include "nsNetCID.h"
+#include "mozilla/Logging.h"
+#include "prnetdb.h"
+#include "prprf.h"
+
+#include "nsUrlClassifierDBService.h"
+#include "nsUrlClassifierUtils.h"
+#include "nsPrintfCString.h"
+#include "mozilla/Base64.h"
+#include "RiceDeltaDecoder.h"
+#include "mozilla/EndianUtils.h"
+
+// MOZ_LOG=UrlClassifierProtocolParser:5
+mozilla::LazyLogModule gUrlClassifierProtocolParserLog("UrlClassifierProtocolParser");
+#define PARSER_LOG(args) MOZ_LOG(gUrlClassifierProtocolParserLog, mozilla::LogLevel::Debug, args)
+
+namespace mozilla {
+namespace safebrowsing {
+
+// Updates will fail if fed chunks larger than this
+const uint32_t MAX_CHUNK_SIZE = (1024 * 1024);
+// Updates will fail if the total number of tocuhed chunks is larger than this
+const uint32_t MAX_CHUNK_RANGE = 1000000;
+
+const uint32_t DOMAIN_SIZE = 4;
+
+// Parse one stringified range of chunks of the form "n" or "n-m" from a
+// comma-separated list of chunks. Upon return, 'begin' will point to the
+// next range of chunks in the list of chunks.
+static bool
+ParseChunkRange(nsACString::const_iterator& aBegin,
+ const nsACString::const_iterator& aEnd,
+ uint32_t* aFirst, uint32_t* aLast)
+{
+ nsACString::const_iterator iter = aBegin;
+ FindCharInReadable(',', iter, aEnd);
+
+ nsAutoCString element(Substring(aBegin, iter));
+ aBegin = iter;
+ if (aBegin != aEnd)
+ aBegin++;
+
+ uint32_t numRead = PR_sscanf(element.get(), "%u-%u", aFirst, aLast);
+ if (numRead == 2) {
+ if (*aFirst > *aLast) {
+ uint32_t tmp = *aFirst;
+ *aFirst = *aLast;
+ *aLast = tmp;
+ }
+ return true;
+ }
+
+ if (numRead == 1) {
+ *aLast = *aFirst;
+ return true;
+ }
+
+ return false;
+}
+
+///////////////////////////////////////////////////////////////
+// ProtocolParser implementation
+
+ProtocolParser::ProtocolParser()
+ : mUpdateStatus(NS_OK)
+ , mUpdateWaitSec(0)
+{
+}
+
+ProtocolParser::~ProtocolParser()
+{
+ CleanupUpdates();
+}
+
+nsresult
+ProtocolParser::Init(nsICryptoHash* aHasher)
+{
+ mCryptoHash = aHasher;
+ return NS_OK;
+}
+
+void
+ProtocolParser::CleanupUpdates()
+{
+ for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
+ delete mTableUpdates[i];
+ }
+ mTableUpdates.Clear();
+}
+
+TableUpdate *
+ProtocolParser::GetTableUpdate(const nsACString& aTable)
+{
+ for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
+ if (aTable.Equals(mTableUpdates[i]->TableName())) {
+ return mTableUpdates[i];
+ }
+ }
+
+ // We free automatically on destruction, ownership of these
+ // updates can be transferred to DBServiceWorker, which passes
+ // them back to Classifier when doing the updates, and that
+ // will free them.
+ TableUpdate *update = CreateTableUpdate(aTable);
+ mTableUpdates.AppendElement(update);
+ return update;
+}
+
+///////////////////////////////////////////////////////////////////////
+// ProtocolParserV2
+
+ProtocolParserV2::ProtocolParserV2()
+ : mState(PROTOCOL_STATE_CONTROL)
+ , mResetRequested(false)
+ , mTableUpdate(nullptr)
+{
+}
+
+ProtocolParserV2::~ProtocolParserV2()
+{
+}
+
+void
+ProtocolParserV2::SetCurrentTable(const nsACString& aTable)
+{
+ auto update = GetTableUpdate(aTable);
+ mTableUpdate = TableUpdate::Cast<TableUpdateV2>(update);
+}
+
+nsresult
+ProtocolParserV2::AppendStream(const nsACString& aData)
+{
+ if (NS_FAILED(mUpdateStatus))
+ return mUpdateStatus;
+
+ nsresult rv;
+ mPending.Append(aData);
+
+ bool done = false;
+ while (!done) {
+ if (nsUrlClassifierDBService::ShutdownHasStarted()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (mState == PROTOCOL_STATE_CONTROL) {
+ rv = ProcessControl(&done);
+ } else if (mState == PROTOCOL_STATE_CHUNK) {
+ rv = ProcessChunk(&done);
+ } else {
+ NS_ERROR("Unexpected protocol state");
+ rv = NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mUpdateStatus = rv;
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+void
+ProtocolParserV2::End()
+{
+ // Inbound data has already been processed in every AppendStream() call.
+}
+
+nsresult
+ProtocolParserV2::ProcessControl(bool* aDone)
+{
+ nsresult rv;
+
+ nsAutoCString line;
+ *aDone = true;
+ while (NextLine(line)) {
+ PARSER_LOG(("Processing %s\n", line.get()));
+
+ if (StringBeginsWith(line, NS_LITERAL_CSTRING("i:"))) {
+ // Set the table name from the table header line.
+ SetCurrentTable(Substring(line, 2));
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("n:"))) {
+ if (PR_sscanf(line.get(), "n:%d", &mUpdateWaitSec) != 1) {
+ PARSER_LOG(("Error parsing n: '%s' (%d)", line.get(), mUpdateWaitSec));
+ return NS_ERROR_FAILURE;
+ }
+ } else if (line.EqualsLiteral("r:pleasereset")) {
+ mResetRequested = true;
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("u:"))) {
+ rv = ProcessForward(line);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("a:")) ||
+ StringBeginsWith(line, NS_LITERAL_CSTRING("s:"))) {
+ rv = ProcessChunkControl(line);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aDone = false;
+ return NS_OK;
+ } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("ad:")) ||
+ StringBeginsWith(line, NS_LITERAL_CSTRING("sd:"))) {
+ rv = ProcessExpirations(line);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ *aDone = true;
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessExpirations(const nsCString& aLine)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Got an expiration without a table.");
+ return NS_ERROR_FAILURE;
+ }
+ const nsCSubstring &list = Substring(aLine, 3);
+ nsACString::const_iterator begin, end;
+ list.BeginReading(begin);
+ list.EndReading(end);
+ while (begin != end) {
+ uint32_t first, last;
+ if (ParseChunkRange(begin, end, &first, &last)) {
+ if (last < first) return NS_ERROR_FAILURE;
+ if (last - first > MAX_CHUNK_RANGE) return NS_ERROR_FAILURE;
+ for (uint32_t num = first; num <= last; num++) {
+ if (aLine[0] == 'a') {
+ nsresult rv = mTableUpdate->NewAddExpiration(num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ nsresult rv = mTableUpdate->NewSubExpiration(num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ }
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessChunkControl(const nsCString& aLine)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Got a chunk before getting a table.");
+ return NS_ERROR_FAILURE;
+ }
+
+ mState = PROTOCOL_STATE_CHUNK;
+ char command;
+
+ mChunkState.Clear();
+
+ if (PR_sscanf(aLine.get(),
+ "%c:%d:%d:%d",
+ &command,
+ &mChunkState.num, &mChunkState.hashSize, &mChunkState.length)
+ != 4)
+ {
+ NS_WARNING(("PR_sscanf failed"));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mChunkState.length > MAX_CHUNK_SIZE) {
+ NS_WARNING("Invalid length specified in update.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!(mChunkState.hashSize == PREFIX_SIZE || mChunkState.hashSize == COMPLETE_SIZE)) {
+ NS_WARNING("Invalid hash size specified in update.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-shavar")) ||
+ StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-simple"))) {
+ // Accommodate test tables ending in -simple for now.
+ mChunkState.type = (command == 'a') ? CHUNK_ADD : CHUNK_SUB;
+ } else if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-digest256"))) {
+ mChunkState.type = (command == 'a') ? CHUNK_ADD_DIGEST : CHUNK_SUB_DIGEST;
+ }
+ nsresult rv;
+ switch (mChunkState.type) {
+ case CHUNK_ADD:
+ rv = mTableUpdate->NewAddChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ case CHUNK_SUB:
+ rv = mTableUpdate->NewSubChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ case CHUNK_ADD_DIGEST:
+ rv = mTableUpdate->NewAddChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ case CHUNK_SUB_DIGEST:
+ rv = mTableUpdate->NewSubChunk(mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessForward(const nsCString& aLine)
+{
+ const nsCSubstring &forward = Substring(aLine, 2);
+ return AddForward(forward);
+}
+
+nsresult
+ProtocolParserV2::AddForward(const nsACString& aUrl)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Forward without a table name.");
+ return NS_ERROR_FAILURE;
+ }
+
+ ForwardedUpdate *forward = mForwards.AppendElement();
+ forward->table = mTableUpdate->TableName();
+ forward->url.Assign(aUrl);
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessChunk(bool* aDone)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Processing chunk without an active table.");
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ASSERTION(mChunkState.num != 0, "Must have a chunk number.");
+
+ if (mPending.Length() < mChunkState.length) {
+ *aDone = true;
+ return NS_OK;
+ }
+
+ // Pull the chunk out of the pending stream data.
+ nsAutoCString chunk;
+ chunk.Assign(Substring(mPending, 0, mChunkState.length));
+ mPending.Cut(0, mChunkState.length);
+
+ *aDone = false;
+ mState = PROTOCOL_STATE_CONTROL;
+
+ if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-shavar"))) {
+ return ProcessShaChunk(chunk);
+ }
+ if (StringEndsWith(mTableUpdate->TableName(),
+ NS_LITERAL_CSTRING("-digest256"))) {
+ return ProcessDigestChunk(chunk);
+ }
+ return ProcessPlaintextChunk(chunk);
+}
+
+/**
+ * Process a plaintext chunk (currently only used in unit tests).
+ */
+nsresult
+ProtocolParserV2::ProcessPlaintextChunk(const nsACString& aChunk)
+{
+ if (!mTableUpdate) {
+ NS_WARNING("Chunk received with no table.");
+ return NS_ERROR_FAILURE;
+ }
+
+ PARSER_LOG(("Handling a %d-byte simple chunk", aChunk.Length()));
+
+ nsTArray<nsCString> lines;
+ ParseString(PromiseFlatCString(aChunk), '\n', lines);
+
+ // non-hashed tables need to be hashed
+ for (uint32_t i = 0; i < lines.Length(); i++) {
+ nsCString& line = lines[i];
+
+ if (mChunkState.type == CHUNK_ADD) {
+ if (mChunkState.hashSize == COMPLETE_SIZE) {
+ Completion hash;
+ hash.FromPlaintext(line, mCryptoHash);
+ nsresult rv = mTableUpdate->NewAddComplete(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ NS_ASSERTION(mChunkState.hashSize == 4, "Only 32- or 4-byte hashes can be used for add chunks.");
+ Prefix hash;
+ hash.FromPlaintext(line, mCryptoHash);
+ nsresult rv = mTableUpdate->NewAddPrefix(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ } else {
+ nsCString::const_iterator begin, iter, end;
+ line.BeginReading(begin);
+ line.EndReading(end);
+ iter = begin;
+ uint32_t addChunk;
+ if (!FindCharInReadable(':', iter, end) ||
+ PR_sscanf(lines[i].get(), "%d:", &addChunk) != 1) {
+ NS_WARNING("Received sub chunk without associated add chunk.");
+ return NS_ERROR_FAILURE;
+ }
+ iter++;
+
+ if (mChunkState.hashSize == COMPLETE_SIZE) {
+ Completion hash;
+ hash.FromPlaintext(Substring(iter, end), mCryptoHash);
+ nsresult rv = mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ NS_ASSERTION(mChunkState.hashSize == 4, "Only 32- or 4-byte hashes can be used for add chunks.");
+ Prefix hash;
+ hash.FromPlaintext(Substring(iter, end), mCryptoHash);
+ nsresult rv = mTableUpdate->NewSubPrefix(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessShaChunk(const nsACString& aChunk)
+{
+ uint32_t start = 0;
+ while (start < aChunk.Length()) {
+ // First four bytes are the domain key.
+ Prefix domain;
+ domain.Assign(Substring(aChunk, start, DOMAIN_SIZE));
+ start += DOMAIN_SIZE;
+
+ // Then a count of entries.
+ uint8_t numEntries = static_cast<uint8_t>(aChunk[start]);
+ start++;
+
+ PARSER_LOG(("Handling a %d-byte shavar chunk containing %u entries"
+ " for domain %X", aChunk.Length(), numEntries,
+ domain.ToUint32()));
+
+ nsresult rv;
+ if (mChunkState.type == CHUNK_ADD && mChunkState.hashSize == PREFIX_SIZE) {
+ rv = ProcessHostAdd(domain, numEntries, aChunk, &start);
+ } else if (mChunkState.type == CHUNK_ADD && mChunkState.hashSize == COMPLETE_SIZE) {
+ rv = ProcessHostAddComplete(numEntries, aChunk, &start);
+ } else if (mChunkState.type == CHUNK_SUB && mChunkState.hashSize == PREFIX_SIZE) {
+ rv = ProcessHostSub(domain, numEntries, aChunk, &start);
+ } else if (mChunkState.type == CHUNK_SUB && mChunkState.hashSize == COMPLETE_SIZE) {
+ rv = ProcessHostSubComplete(numEntries, aChunk, &start);
+ } else {
+ NS_WARNING("Unexpected chunk type/hash size!");
+ PARSER_LOG(("Got an unexpected chunk type/hash size: %s:%d",
+ mChunkState.type == CHUNK_ADD ? "add" : "sub",
+ mChunkState.hashSize));
+ return NS_ERROR_FAILURE;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessDigestChunk(const nsACString& aChunk)
+{
+ PARSER_LOG(("Handling a %d-byte digest256 chunk", aChunk.Length()));
+
+ if (mChunkState.type == CHUNK_ADD_DIGEST) {
+ return ProcessDigestAdd(aChunk);
+ }
+ if (mChunkState.type == CHUNK_SUB_DIGEST) {
+ return ProcessDigestSub(aChunk);
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult
+ProtocolParserV2::ProcessDigestAdd(const nsACString& aChunk)
+{
+ // The ABNF format for add chunks is (HASH)+, where HASH is 32 bytes.
+ MOZ_ASSERT(aChunk.Length() % 32 == 0,
+ "Chunk length in bytes must be divisible by 4");
+ uint32_t start = 0;
+ while (start < aChunk.Length()) {
+ Completion hash;
+ hash.Assign(Substring(aChunk, start, COMPLETE_SIZE));
+ start += COMPLETE_SIZE;
+ nsresult rv = mTableUpdate->NewAddComplete(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessDigestSub(const nsACString& aChunk)
+{
+ // The ABNF format for sub chunks is (ADDCHUNKNUM HASH)+, where ADDCHUNKNUM
+ // is a 4 byte chunk number, and HASH is 32 bytes.
+ MOZ_ASSERT(aChunk.Length() % 36 == 0,
+ "Chunk length in bytes must be divisible by 36");
+ uint32_t start = 0;
+ while (start < aChunk.Length()) {
+ // Read ADDCHUNKNUM
+ const nsCSubstring& addChunkStr = Substring(aChunk, start, 4);
+ start += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ // Read the hash
+ Completion hash;
+ hash.Assign(Substring(aChunk, start, COMPLETE_SIZE));
+ start += COMPLETE_SIZE;
+
+ nsresult rv = mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostAdd(const Prefix& aDomain, uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == PREFIX_SIZE,
+ "ProcessHostAdd should only be called for prefix hashes.");
+
+ if (aNumEntries == 0) {
+ nsresult rv = mTableUpdate->NewAddPrefix(mChunkState.num, aDomain);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ if (*aStart + (PREFIX_SIZE * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ Prefix hash;
+ hash.Assign(Substring(aChunk, *aStart, PREFIX_SIZE));
+ PARSER_LOG(("Add prefix %X", hash.ToUint32()));
+ nsresult rv = mTableUpdate->NewAddPrefix(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *aStart += PREFIX_SIZE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostSub(const Prefix& aDomain, uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t *aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == PREFIX_SIZE,
+ "ProcessHostSub should only be called for prefix hashes.");
+
+ if (aNumEntries == 0) {
+ if ((*aStart) + 4 > aChunk.Length()) {
+ NS_WARNING("Received a zero-entry sub chunk without an associated add.");
+ return NS_ERROR_FAILURE;
+ }
+
+ const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
+ *aStart += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ PARSER_LOG(("Sub prefix (addchunk=%u)", addChunk));
+ nsresult rv = mTableUpdate->NewSubPrefix(addChunk, aDomain, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ if (*aStart + ((PREFIX_SIZE + 4) * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
+ *aStart += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ Prefix prefix;
+ prefix.Assign(Substring(aChunk, *aStart, PREFIX_SIZE));
+ *aStart += PREFIX_SIZE;
+
+ PARSER_LOG(("Sub prefix %X (addchunk=%u)", prefix.ToUint32(), addChunk));
+ nsresult rv = mTableUpdate->NewSubPrefix(addChunk, prefix, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostAddComplete(uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == COMPLETE_SIZE,
+ "ProcessHostAddComplete should only be called for complete hashes.");
+
+ if (aNumEntries == 0) {
+ // this is totally comprehensible.
+ // My sarcasm detector is going off!
+ NS_WARNING("Expected > 0 entries for a 32-byte hash add.");
+ return NS_OK;
+ }
+
+ if (*aStart + (COMPLETE_SIZE * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ Completion hash;
+ hash.Assign(Substring(aChunk, *aStart, COMPLETE_SIZE));
+ nsresult rv = mTableUpdate->NewAddComplete(mChunkState.num, hash);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *aStart += COMPLETE_SIZE;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserV2::ProcessHostSubComplete(uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart)
+{
+ NS_ASSERTION(mChunkState.hashSize == COMPLETE_SIZE,
+ "ProcessHostSubComplete should only be called for complete hashes.");
+
+ if (aNumEntries == 0) {
+ // this is totally comprehensible.
+ NS_WARNING("Expected > 0 entries for a 32-byte hash sub.");
+ return NS_OK;
+ }
+
+ if (*aStart + ((COMPLETE_SIZE + 4) * aNumEntries) > aChunk.Length()) {
+ NS_WARNING("Chunk is not long enough to contain the expected entries.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint8_t i = 0; i < aNumEntries; i++) {
+ Completion hash;
+ hash.Assign(Substring(aChunk, *aStart, COMPLETE_SIZE));
+ *aStart += COMPLETE_SIZE;
+
+ const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
+ *aStart += 4;
+
+ uint32_t addChunk;
+ memcpy(&addChunk, addChunkStr.BeginReading(), 4);
+ addChunk = PR_ntohl(addChunk);
+
+ nsresult rv = mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool
+ProtocolParserV2::NextLine(nsACString& aLine)
+{
+ int32_t newline = mPending.FindChar('\n');
+ if (newline == kNotFound) {
+ return false;
+ }
+ aLine.Assign(Substring(mPending, 0, newline));
+ mPending.Cut(0, newline + 1);
+ return true;
+}
+
+TableUpdate*
+ProtocolParserV2::CreateTableUpdate(const nsACString& aTableName) const
+{
+ return new TableUpdateV2(aTableName);
+}
+
+///////////////////////////////////////////////////////////////////////
+// ProtocolParserProtobuf
+
+ProtocolParserProtobuf::ProtocolParserProtobuf()
+{
+}
+
+ProtocolParserProtobuf::~ProtocolParserProtobuf()
+{
+}
+
+void
+ProtocolParserProtobuf::SetCurrentTable(const nsACString& aTable)
+{
+ // Should never occur.
+ MOZ_ASSERT_UNREACHABLE("SetCurrentTable shouldn't be called");
+}
+
+
+TableUpdate*
+ProtocolParserProtobuf::CreateTableUpdate(const nsACString& aTableName) const
+{
+ return new TableUpdateV4(aTableName);
+}
+
+nsresult
+ProtocolParserProtobuf::AppendStream(const nsACString& aData)
+{
+ // Protobuf data cannot be parsed progressively. Just save the incoming data.
+ mPending.Append(aData);
+ return NS_OK;
+}
+
+void
+ProtocolParserProtobuf::End()
+{
+ // mUpdateStatus will be updated to success as long as not all
+ // the responses are invalid.
+ mUpdateStatus = NS_ERROR_FAILURE;
+
+ FetchThreatListUpdatesResponse response;
+ if (!response.ParseFromArray(mPending.get(), mPending.Length())) {
+ NS_WARNING("ProtocolParserProtobuf failed parsing data.");
+ return;
+ }
+
+ auto minWaitDuration = response.minimum_wait_duration();
+ mUpdateWaitSec = minWaitDuration.seconds() +
+ minWaitDuration.nanos() / 1000000000;
+
+ for (int i = 0; i < response.list_update_responses_size(); i++) {
+ auto r = response.list_update_responses(i);
+ nsresult rv = ProcessOneResponse(r);
+ if (NS_SUCCEEDED(rv)) {
+ mUpdateStatus = rv;
+ } else {
+ NS_WARNING("Failed to process one response.");
+ }
+ }
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessOneResponse(const ListUpdateResponse& aResponse)
+{
+ // A response must have a threat type.
+ if (!aResponse.has_threat_type()) {
+ NS_WARNING("Threat type not initialized. This seems to be an invalid response.");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Convert threat type to list name.
+ nsCOMPtr<nsIUrlClassifierUtils> urlUtil =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID);
+ nsCString possibleListNames;
+ nsresult rv = urlUtil->ConvertThreatTypeToListNames(aResponse.threat_type(),
+ possibleListNames);
+ if (NS_FAILED(rv)) {
+ PARSER_LOG((nsPrintfCString("Threat type to list name conversion error: %d",
+ aResponse.threat_type())).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ // Match the table name we received with one of the ones we requested.
+ // We ignore the case where a threat type matches more than one list
+ // per provider and return the first one. See bug 1287059."
+ nsCString listName;
+ nsTArray<nsCString> possibleListNameArray;
+ Classifier::SplitTables(possibleListNames, possibleListNameArray);
+ for (auto possibleName : possibleListNameArray) {
+ if (mRequestedTables.Contains(possibleName)) {
+ listName = possibleName;
+ break;
+ }
+ }
+
+ if (listName.IsEmpty()) {
+ PARSER_LOG(("We received an update for a list we didn't ask for. Ignoring it."));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Test if this is a full update.
+ bool isFullUpdate = false;
+ if (aResponse.has_response_type()) {
+ isFullUpdate =
+ aResponse.response_type() == ListUpdateResponse::FULL_UPDATE;
+ } else {
+ NS_WARNING("Response type not initialized.");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Warn if there's no new state.
+ if (!aResponse.has_new_client_state()) {
+ NS_WARNING("New state not initialized.");
+ return NS_ERROR_FAILURE;
+ }
+
+ auto tu = GetTableUpdate(nsCString(listName.get()));
+ auto tuV4 = TableUpdate::Cast<TableUpdateV4>(tu);
+ NS_ENSURE_TRUE(tuV4, NS_ERROR_FAILURE);
+
+ nsCString state(aResponse.new_client_state().c_str(),
+ aResponse.new_client_state().size());
+ tuV4->SetNewClientState(state);
+
+ if (aResponse.has_checksum()) {
+ tuV4->NewChecksum(aResponse.checksum().sha256());
+ }
+
+ PARSER_LOG(("==== Update for threat type '%d' ====", aResponse.threat_type()));
+ PARSER_LOG(("* listName: %s\n", listName.get()));
+ PARSER_LOG(("* newState: %s\n", aResponse.new_client_state().c_str()));
+ PARSER_LOG(("* isFullUpdate: %s\n", (isFullUpdate ? "yes" : "no")));
+ PARSER_LOG(("* hasChecksum: %s\n", (aResponse.has_checksum() ? "yes" : "no")));
+
+ tuV4->SetFullUpdate(isFullUpdate);
+
+ rv = ProcessAdditionOrRemoval(*tuV4, aResponse.additions(), true /*aIsAddition*/);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = ProcessAdditionOrRemoval(*tuV4, aResponse.removals(), false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PARSER_LOG(("\n\n"));
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessAdditionOrRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySetList& aUpdate,
+ bool aIsAddition)
+{
+ nsresult ret = NS_OK;
+
+ for (int i = 0; i < aUpdate.size(); i++) {
+ auto update = aUpdate.Get(i);
+ if (!update.has_compression_type()) {
+ NS_WARNING(nsPrintfCString("%s with no compression type.",
+ aIsAddition ? "Addition" : "Removal").get());
+ continue;
+ }
+
+ switch (update.compression_type()) {
+ case COMPRESSION_TYPE_UNSPECIFIED:
+ NS_WARNING("Unspecified compression type.");
+ break;
+
+ case RAW:
+ ret = (aIsAddition ? ProcessRawAddition(aTableUpdate, update)
+ : ProcessRawRemoval(aTableUpdate, update));
+ break;
+
+ case RICE:
+ ret = (aIsAddition ? ProcessEncodedAddition(aTableUpdate, update)
+ : ProcessEncodedRemoval(aTableUpdate, update));
+ break;
+ }
+ }
+
+ return ret;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessRawAddition(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aAddition)
+{
+ if (!aAddition.has_raw_hashes()) {
+ PARSER_LOG(("* No raw addition."));
+ return NS_OK;
+ }
+
+ auto rawHashes = aAddition.raw_hashes();
+ if (!rawHashes.has_prefix_size()) {
+ NS_WARNING("Raw hash has no prefix size");
+ return NS_OK;
+ }
+
+ auto prefixes = rawHashes.raw_hashes();
+ if (4 == rawHashes.prefix_size()) {
+ // Process fixed length prefixes separately.
+ uint32_t* fixedLengthPrefixes = (uint32_t*)prefixes.c_str();
+ size_t numOfFixedLengthPrefixes = prefixes.size() / 4;
+ PARSER_LOG(("* Raw addition (4 bytes)"));
+ PARSER_LOG((" - # of prefixes: %d", numOfFixedLengthPrefixes));
+ PARSER_LOG((" - Memory address: 0x%p", fixedLengthPrefixes));
+ } else {
+ // TODO: Process variable length prefixes including full hashes.
+ // See Bug 1283009.
+ PARSER_LOG((" Raw addition (%d bytes)", rawHashes.prefix_size()));
+ }
+
+ if (!rawHashes.mutable_raw_hashes()) {
+ PARSER_LOG(("Unable to get mutable raw hashes. Can't perform a string move."));
+ return NS_ERROR_FAILURE;
+ }
+
+ aTableUpdate.NewPrefixes(rawHashes.prefix_size(),
+ *rawHashes.mutable_raw_hashes());
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessRawRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aRemoval)
+{
+ if (!aRemoval.has_raw_indices()) {
+ NS_WARNING("A removal has no indices.");
+ return NS_OK;
+ }
+
+ // indices is an array of int32.
+ auto indices = aRemoval.raw_indices().indices();
+ PARSER_LOG(("* Raw removal"));
+ PARSER_LOG((" - # of removal: %d", indices.size()));
+
+ aTableUpdate.NewRemovalIndices((const uint32_t*)indices.data(),
+ indices.size());
+
+ return NS_OK;
+}
+
+static nsresult
+DoRiceDeltaDecode(const RiceDeltaEncoding& aEncoding,
+ nsTArray<uint32_t>& aDecoded)
+{
+ if (!aEncoding.has_first_value()) {
+ PARSER_LOG(("The encoding info is incomplete."));
+ return NS_ERROR_FAILURE;
+ }
+ if (aEncoding.num_entries() > 0 &&
+ (!aEncoding.has_rice_parameter() || !aEncoding.has_encoded_data())) {
+ PARSER_LOG(("Rice parameter or encoded data is missing."));
+ return NS_ERROR_FAILURE;
+ }
+
+ PARSER_LOG(("* Encoding info:"));
+ PARSER_LOG((" - First value: %d", aEncoding.first_value()));
+ PARSER_LOG((" - Num of entries: %d", aEncoding.num_entries()));
+ PARSER_LOG((" - Rice parameter: %d", aEncoding.rice_parameter()));
+
+ // Set up the input buffer. Note that the bits should be read
+ // from LSB to MSB so that we in-place reverse the bits before
+ // feeding to the decoder.
+ auto encoded = const_cast<RiceDeltaEncoding&>(aEncoding).mutable_encoded_data();
+ RiceDeltaDecoder decoder((uint8_t*)encoded->c_str(), encoded->size());
+
+ // Setup the output buffer. The "first value" is included in
+ // the output buffer.
+ aDecoded.SetLength(aEncoding.num_entries() + 1);
+
+ // Decode!
+ bool rv = decoder.Decode(aEncoding.rice_parameter(),
+ aEncoding.first_value(), // first value.
+ aEncoding.num_entries(), // # of entries (first value not included).
+ &aDecoded[0]);
+
+ NS_ENSURE_TRUE(rv, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessEncodedAddition(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aAddition)
+{
+ if (!aAddition.has_rice_hashes()) {
+ PARSER_LOG(("* No rice encoded addition."));
+ return NS_OK;
+ }
+
+ nsTArray<uint32_t> decoded;
+ nsresult rv = DoRiceDeltaDecode(aAddition.rice_hashes(), decoded);
+ if (NS_FAILED(rv)) {
+ PARSER_LOG(("Failed to parse encoded prefixes."));
+ return rv;
+ }
+
+ // Say we have the following raw prefixes
+ // BE LE
+ // 00 00 00 01 1 16777216
+ // 00 00 02 00 512 131072
+ // 00 03 00 00 196608 768
+ // 04 00 00 00 67108864 4
+ //
+ // which can be treated as uint32 (big-endian) sorted in increasing order:
+ //
+ // [1, 512, 196608, 67108864]
+ //
+ // According to https://developers.google.com/safe-browsing/v4/compression,
+ // the following should be done prior to compression:
+ //
+ // 1) re-interpret in little-endian ==> [16777216, 131072, 768, 4]
+ // 2) sort in increasing order ==> [4, 768, 131072, 16777216]
+ //
+ // In order to get the original byte stream from |decoded|
+ // ([4, 768, 131072, 16777216] in this case), we have to:
+ //
+ // 1) sort in big-endian order ==> [16777216, 131072, 768, 4]
+ // 2) copy each uint32 in little-endian to the result string
+ //
+
+ // The 4-byte prefixes have to be re-sorted in Big-endian increasing order.
+ struct CompareBigEndian
+ {
+ bool Equals(const uint32_t& aA, const uint32_t& aB) const
+ {
+ return aA == aB;
+ }
+
+ bool LessThan(const uint32_t& aA, const uint32_t& aB) const
+ {
+ return NativeEndian::swapToBigEndian(aA) <
+ NativeEndian::swapToBigEndian(aB);
+ }
+ };
+ decoded.Sort(CompareBigEndian());
+
+ // The encoded prefixes are always 4 bytes.
+ std::string prefixes;
+ for (size_t i = 0; i < decoded.Length(); i++) {
+ // Note that the third argument is the number of elements we want
+ // to copy (and swap) but not the number of bytes we want to copy.
+ char p[4];
+ NativeEndian::copyAndSwapToLittleEndian(p, &decoded[i], 1);
+ prefixes.append(p, 4);
+ }
+
+ aTableUpdate.NewPrefixes(4, prefixes);
+
+ return NS_OK;
+}
+
+nsresult
+ProtocolParserProtobuf::ProcessEncodedRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aRemoval)
+{
+ if (!aRemoval.has_rice_indices()) {
+ PARSER_LOG(("* No rice encoded removal."));
+ return NS_OK;
+ }
+
+ nsTArray<uint32_t> decoded;
+ nsresult rv = DoRiceDeltaDecode(aRemoval.rice_indices(), decoded);
+ if (NS_FAILED(rv)) {
+ PARSER_LOG(("Failed to decode encoded removal indices."));
+ return rv;
+ }
+
+ // The encoded prefixes are always 4 bytes.
+ aTableUpdate.NewRemovalIndices(&decoded[0], decoded.Length());
+
+ return NS_OK;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/ProtocolParser.h b/toolkit/components/url-classifier/ProtocolParser.h
new file mode 100644
index 0000000000..ec1a695f44
--- /dev/null
+++ b/toolkit/components/url-classifier/ProtocolParser.h
@@ -0,0 +1,204 @@
+//* -*- 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/. */
+
+#ifndef ProtocolParser_h__
+#define ProtocolParser_h__
+
+#include "HashStore.h"
+#include "nsICryptoHMAC.h"
+#include "safebrowsing.pb.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+/**
+ * Abstract base class for parsing update data in multiple formats.
+ */
+class ProtocolParser {
+public:
+ struct ForwardedUpdate {
+ nsCString table;
+ nsCString url;
+ };
+
+ ProtocolParser();
+ virtual ~ProtocolParser();
+
+ nsresult Status() const { return mUpdateStatus; }
+
+ nsresult Init(nsICryptoHash* aHasher);
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ nsCString GetRawTableUpdates() const { return mPending; }
+#endif
+
+ virtual void SetCurrentTable(const nsACString& aTable) = 0;
+
+ void SetRequestedTables(const nsTArray<nsCString>& aRequestTables)
+ {
+ mRequestedTables = aRequestTables;
+ }
+
+ nsresult Begin();
+ virtual nsresult AppendStream(const nsACString& aData) = 0;
+
+ uint32_t UpdateWaitSec() { return mUpdateWaitSec; }
+
+ // Notify that the inbound data is ready for parsing if progressive
+ // parsing is not supported, for example in V4.
+ virtual void End() = 0;
+
+ // Forget the table updates that were created by this pass. It
+ // becomes the caller's responsibility to free them. This is shitty.
+ TableUpdate *GetTableUpdate(const nsACString& aTable);
+ void ForgetTableUpdates() { mTableUpdates.Clear(); }
+ nsTArray<TableUpdate*> &GetTableUpdates() { return mTableUpdates; }
+
+ // These are only meaningful to V2. Since they were originally public,
+ // moving them to ProtocolParserV2 requires a dymamic cast in the call
+ // sites. As a result, we will leave them until we remove support
+ // for V2 entirely..
+ virtual const nsTArray<ForwardedUpdate> &Forwards() const { return mForwards; }
+ virtual bool ResetRequested() { return false; }
+
+protected:
+ virtual TableUpdate* CreateTableUpdate(const nsACString& aTableName) const = 0;
+
+ nsCString mPending;
+ nsresult mUpdateStatus;
+
+ // Keep track of updates to apply before passing them to the DBServiceWorkers.
+ nsTArray<TableUpdate*> mTableUpdates;
+
+ nsTArray<ForwardedUpdate> mForwards;
+ nsCOMPtr<nsICryptoHash> mCryptoHash;
+
+ // The table names that were requested from the client.
+ nsTArray<nsCString> mRequestedTables;
+
+ // How long we should wait until the next update.
+ uint32_t mUpdateWaitSec;
+
+private:
+ void CleanupUpdates();
+};
+
+/**
+ * Helpers to parse the "shavar", "digest256" and "simple" list formats.
+ */
+class ProtocolParserV2 final : public ProtocolParser {
+public:
+ ProtocolParserV2();
+ virtual ~ProtocolParserV2();
+
+ virtual void SetCurrentTable(const nsACString& aTable) override;
+ virtual nsresult AppendStream(const nsACString& aData) override;
+ virtual void End() override;
+
+ // Update information.
+ virtual const nsTArray<ForwardedUpdate> &Forwards() const override { return mForwards; }
+ virtual bool ResetRequested() override { return mResetRequested; }
+
+private:
+ virtual TableUpdate* CreateTableUpdate(const nsACString& aTableName) const override;
+
+ nsresult ProcessControl(bool* aDone);
+ nsresult ProcessExpirations(const nsCString& aLine);
+ nsresult ProcessChunkControl(const nsCString& aLine);
+ nsresult ProcessForward(const nsCString& aLine);
+ nsresult AddForward(const nsACString& aUrl);
+ nsresult ProcessChunk(bool* done);
+ // Remove this, it's only used for testing
+ nsresult ProcessPlaintextChunk(const nsACString& aChunk);
+ nsresult ProcessShaChunk(const nsACString& aChunk);
+ nsresult ProcessHostAdd(const Prefix& aDomain, uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart);
+ nsresult ProcessHostSub(const Prefix& aDomain, uint8_t aNumEntries,
+ const nsACString& aChunk, uint32_t* aStart);
+ nsresult ProcessHostAddComplete(uint8_t aNumEntries, const nsACString& aChunk,
+ uint32_t *aStart);
+ nsresult ProcessHostSubComplete(uint8_t numEntries, const nsACString& aChunk,
+ uint32_t* start);
+ // Digest chunks are very similar to shavar chunks, except digest chunks
+ // always contain the full hash, so there is no need for chunk data to
+ // contain prefix sizes.
+ nsresult ProcessDigestChunk(const nsACString& aChunk);
+ nsresult ProcessDigestAdd(const nsACString& aChunk);
+ nsresult ProcessDigestSub(const nsACString& aChunk);
+ bool NextLine(nsACString& aLine);
+
+ enum ParserState {
+ PROTOCOL_STATE_CONTROL,
+ PROTOCOL_STATE_CHUNK
+ };
+ ParserState mState;
+
+ enum ChunkType {
+ // Types for shavar tables.
+ CHUNK_ADD,
+ CHUNK_SUB,
+ // Types for digest256 tables. digest256 tables differ in format from
+ // shavar tables since they only contain complete hashes.
+ CHUNK_ADD_DIGEST,
+ CHUNK_SUB_DIGEST
+ };
+
+ struct ChunkState {
+ ChunkType type;
+ uint32_t num;
+ uint32_t hashSize;
+ uint32_t length;
+ void Clear() { num = 0; hashSize = 0; length = 0; }
+ };
+ ChunkState mChunkState;
+
+ bool mResetRequested;
+
+ // Updates to apply to the current table being parsed.
+ TableUpdateV2 *mTableUpdate;
+};
+
+// Helpers to parse the "proto" list format.
+class ProtocolParserProtobuf final : public ProtocolParser {
+public:
+ typedef FetchThreatListUpdatesResponse_ListUpdateResponse ListUpdateResponse;
+ typedef google::protobuf::RepeatedPtrField<ThreatEntrySet> ThreatEntrySetList;
+
+public:
+ ProtocolParserProtobuf();
+
+ virtual void SetCurrentTable(const nsACString& aTable) override;
+ virtual nsresult AppendStream(const nsACString& aData) override;
+ virtual void End() override;
+
+private:
+ virtual ~ProtocolParserProtobuf();
+
+ virtual TableUpdate* CreateTableUpdate(const nsACString& aTableName) const override;
+
+ // For parsing update info.
+ nsresult ProcessOneResponse(const ListUpdateResponse& aResponse);
+
+ nsresult ProcessAdditionOrRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySetList& aUpdate,
+ bool aIsAddition);
+
+ nsresult ProcessRawAddition(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aAddition);
+
+ nsresult ProcessRawRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aRemoval);
+
+ nsresult ProcessEncodedAddition(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aAddition);
+
+ nsresult ProcessEncodedRemoval(TableUpdateV4& aTableUpdate,
+ const ThreatEntrySet& aRemoval);
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/RiceDeltaDecoder.cpp b/toolkit/components/url-classifier/RiceDeltaDecoder.cpp
new file mode 100644
index 0000000000..66b0b3d6d8
--- /dev/null
+++ b/toolkit/components/url-classifier/RiceDeltaDecoder.cpp
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "RiceDeltaDecoder.h"
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////
+// BitBuffer is copied and modified from webrtc/base/bitbuffer.h
+//
+
+/*
+ * Copyright 2015 The WebRTC Project Authors. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree (webrtc/base/bitbuffer.h/cc). An additional intellectual property
+ * rights grant can be found in the file PATENTS. All contributing
+ * project authors may be found in the AUTHORS file in the root of
+ * the source tree.
+ */
+
+class BitBuffer {
+ public:
+ BitBuffer(const uint8_t* bytes, size_t byte_count);
+
+ // The remaining bits in the byte buffer.
+ uint64_t RemainingBitCount() const;
+
+ // Reads bit-sized values from the buffer. Returns false if there isn't enough
+ // data left for the specified bit count..
+ bool ReadBits(uint32_t* val, size_t bit_count);
+
+ // Peeks bit-sized values from the buffer. Returns false if there isn't enough
+ // data left for the specified number of bits. Doesn't move the current
+ // offset.
+ bool PeekBits(uint32_t* val, size_t bit_count);
+
+ // Reads the exponential golomb encoded value at the current offset.
+ // Exponential golomb values are encoded as:
+ // 1) x = source val + 1
+ // 2) In binary, write [countbits(x) - 1] 1s, then x
+ // To decode, we count the number of leading 1 bits, read that many + 1 bits,
+ // and increment the result by 1.
+ // Returns false if there isn't enough data left for the specified type, or if
+ // the value wouldn't fit in a uint32_t.
+ bool ReadExponentialGolomb(uint32_t* val);
+
+ // Moves current position |bit_count| bits forward. Returns false if
+ // there aren't enough bits left in the buffer.
+ bool ConsumeBits(size_t bit_count);
+
+ protected:
+ const uint8_t* const bytes_;
+ // The total size of |bytes_|.
+ size_t byte_count_;
+ // The current offset, in bytes, from the start of |bytes_|.
+ size_t byte_offset_;
+ // The current offset, in bits, into the current byte.
+ size_t bit_offset_;
+};
+
+} // end of unnamed namespace
+
+static void
+ReverseByte(uint8_t& b)
+{
+ b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
+ b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
+ b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
+}
+
+namespace mozilla {
+namespace safebrowsing {
+
+RiceDeltaDecoder::RiceDeltaDecoder(uint8_t* aEncodedData,
+ size_t aEncodedDataSize)
+ : mEncodedData(aEncodedData)
+ , mEncodedDataSize(aEncodedDataSize)
+{
+}
+
+bool
+RiceDeltaDecoder::Decode(uint32_t aRiceParameter,
+ uint32_t aFirstValue,
+ uint32_t aNumEntries,
+ uint32_t* aDecodedData)
+{
+ // Reverse each byte before reading bits from the byte buffer.
+ for (size_t i = 0; i < mEncodedDataSize; i++) {
+ ReverseByte(mEncodedData[i]);
+ }
+
+ BitBuffer bitBuffer(mEncodedData, mEncodedDataSize);
+
+ // q = quotient
+ // r = remainder
+ // k = RICE parameter
+ const uint32_t k = aRiceParameter;
+ aDecodedData[0] = aFirstValue;
+ for (uint32_t i = 0; i < aNumEntries; i++) {
+ // Read the quotient of N.
+ uint32_t q;
+ if (!bitBuffer.ReadExponentialGolomb(&q)) {
+ LOG(("Encoded data underflow!"));
+ return false;
+ }
+
+ // Read the remainder of N, one bit at a time.
+ uint32_t r = 0;
+ for (uint32_t j = 0; j < k; j++) {
+ uint32_t b = 0;
+ if (!bitBuffer.ReadBits(&b, 1)) {
+ // Insufficient bits. Just leave them as zeros.
+ break;
+ }
+ // Add the bit to the right position so that it's in Little Endian order.
+ r |= b << j;
+ }
+
+ // Caculate N from q,r,k.
+ uint32_t N = (q << k) + r;
+
+ // We start filling aDecodedData from [1].
+ aDecodedData[i + 1] = N + aDecodedData[i];
+ }
+
+ return true;
+}
+
+} // end of namespace mozilla
+} // end of namespace safebrowsing
+
+namespace {
+//////////////////////////////////////////////////////////////////////////
+// The BitBuffer impl is copied and modified from webrtc/base/bitbuffer.cc
+//
+
+// Returns the lowest (right-most) |bit_count| bits in |byte|.
+uint8_t LowestBits(uint8_t byte, size_t bit_count) {
+ return byte & ((1 << bit_count) - 1);
+}
+
+// Returns the highest (left-most) |bit_count| bits in |byte|, shifted to the
+// lowest bits (to the right).
+uint8_t HighestBits(uint8_t byte, size_t bit_count) {
+ MOZ_ASSERT(bit_count < 8u);
+ uint8_t shift = 8 - static_cast<uint8_t>(bit_count);
+ uint8_t mask = 0xFF << shift;
+ return (byte & mask) >> shift;
+}
+
+BitBuffer::BitBuffer(const uint8_t* bytes, size_t byte_count)
+ : bytes_(bytes), byte_count_(byte_count), byte_offset_(), bit_offset_() {
+ MOZ_ASSERT(static_cast<uint64_t>(byte_count_) <=
+ std::numeric_limits<uint32_t>::max());
+}
+
+uint64_t BitBuffer::RemainingBitCount() const {
+ return (static_cast<uint64_t>(byte_count_) - byte_offset_) * 8 - bit_offset_;
+}
+
+bool BitBuffer::PeekBits(uint32_t* val, size_t bit_count) {
+ if (!val || bit_count > RemainingBitCount() || bit_count > 32) {
+ return false;
+ }
+ const uint8_t* bytes = bytes_ + byte_offset_;
+ size_t remaining_bits_in_current_byte = 8 - bit_offset_;
+ uint32_t bits = LowestBits(*bytes++, remaining_bits_in_current_byte);
+ // If we're reading fewer bits than what's left in the current byte, just
+ // return the portion of this byte that we need.
+ if (bit_count < remaining_bits_in_current_byte) {
+ *val = HighestBits(bits, bit_offset_ + bit_count);
+ return true;
+ }
+ // Otherwise, subtract what we've read from the bit count and read as many
+ // full bytes as we can into bits.
+ bit_count -= remaining_bits_in_current_byte;
+ while (bit_count >= 8) {
+ bits = (bits << 8) | *bytes++;
+ bit_count -= 8;
+ }
+ // Whatever we have left is smaller than a byte, so grab just the bits we need
+ // and shift them into the lowest bits.
+ if (bit_count > 0) {
+ bits <<= bit_count;
+ bits |= HighestBits(*bytes, bit_count);
+ }
+ *val = bits;
+ return true;
+}
+
+bool BitBuffer::ReadBits(uint32_t* val, size_t bit_count) {
+ return PeekBits(val, bit_count) && ConsumeBits(bit_count);
+}
+
+bool BitBuffer::ConsumeBits(size_t bit_count) {
+ if (bit_count > RemainingBitCount()) {
+ return false;
+ }
+
+ byte_offset_ += (bit_offset_ + bit_count) / 8;
+ bit_offset_ = (bit_offset_ + bit_count) % 8;
+ return true;
+}
+
+bool BitBuffer::ReadExponentialGolomb(uint32_t* val) {
+ if (!val) {
+ return false;
+ }
+
+ *val = 0;
+
+ // Count the number of leading 0 bits by peeking/consuming them one at a time.
+ size_t one_bit_count = 0;
+ uint32_t peeked_bit;
+ while (PeekBits(&peeked_bit, 1) && peeked_bit == 1) {
+ one_bit_count++;
+ ConsumeBits(1);
+ }
+ if (!ConsumeBits(1)) {
+ return false; // The stream is incorrectly terminated at '1'.
+ }
+
+ *val = one_bit_count;
+ return true;
+}
+}
diff --git a/toolkit/components/url-classifier/RiceDeltaDecoder.h b/toolkit/components/url-classifier/RiceDeltaDecoder.h
new file mode 100644
index 0000000000..cf87cea885
--- /dev/null
+++ b/toolkit/components/url-classifier/RiceDeltaDecoder.h
@@ -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/. */
+
+#ifndef RICE_DELTA_DECODER_H
+#define RICE_DELTA_DECODER_H
+
+namespace mozilla {
+namespace safebrowsing {
+
+class RiceDeltaDecoder {
+public:
+ // This decoder is tailored for safebrowsing v4, including the
+ // bit reading order and how the remainder part is interpreted.
+ // The caller just needs to feed the byte stream received from
+ // network directly. Note that the input buffer must be mutable
+ // since the decoder will do some pre-processing before decoding.
+ RiceDeltaDecoder(uint8_t* aEncodedData, size_t aEncodedDataSize);
+
+ // @param aNumEntries The number of values to be decoded, not including
+ // the first value.
+ // @param aDecodedData A pre-allocated output buffer. Note that
+ // aDecodedData[0] will be filled with |aFirstValue|
+ // and the buffer length (in byte) should be
+ // ((aNumEntries + 1) * sizeof(uint32_t)).
+ bool Decode(uint32_t aRiceParameter,
+ uint32_t aFirstValue,
+ uint32_t aNumEntries,
+ uint32_t* aDecodedData);
+
+private:
+ uint8_t* mEncodedData;
+ size_t mEncodedDataSize;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif // UPDATE_V4_DECODER_H
diff --git a/toolkit/components/url-classifier/SafeBrowsing.jsm b/toolkit/components/url-classifier/SafeBrowsing.jsm
new file mode 100644
index 0000000000..b49be71fe7
--- /dev/null
+++ b/toolkit/components/url-classifier/SafeBrowsing.jsm
@@ -0,0 +1,429 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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.EXPORTED_SYMBOLS = ["SafeBrowsing"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Log only if browser.safebrowsing.debug is true
+function log(...stuff) {
+ let logging = null;
+ try {
+ logging = Services.prefs.getBoolPref("browser.safebrowsing.debug");
+ } catch(e) {
+ return;
+ }
+ if (!logging) {
+ return;
+ }
+
+ var d = new Date();
+ let msg = "SafeBrowsing: " + d.toTimeString() + ": " + stuff.join(" ");
+ dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n");
+}
+
+function getLists(prefName) {
+ log("getLists: " + prefName);
+ let pref = null;
+ try {
+ pref = Services.prefs.getCharPref(prefName);
+ } catch(e) {
+ return null;
+ }
+ // Splitting an empty string returns [''], we really want an empty array.
+ if (!pref) {
+ return [];
+ }
+ return pref.split(",")
+ .map(function(value) { return value.trim(); });
+}
+
+const tablePreferences = [
+ "urlclassifier.phishTable",
+ "urlclassifier.malwareTable",
+ "urlclassifier.downloadBlockTable",
+ "urlclassifier.downloadAllowTable",
+ "urlclassifier.trackingTable",
+ "urlclassifier.trackingWhitelistTable",
+ "urlclassifier.blockedTable"
+];
+
+this.SafeBrowsing = {
+
+ init: function() {
+ if (this.initialized) {
+ log("Already initialized");
+ return;
+ }
+
+ Services.prefs.addObserver("browser.safebrowsing", this, false);
+ Services.prefs.addObserver("privacy.trackingprotection", this, false);
+ Services.prefs.addObserver("urlclassifier", this, false);
+
+ this.readPrefs();
+ this.addMozEntries();
+
+ this.controlUpdateChecking();
+ this.initialized = true;
+
+ log("init() finished");
+ },
+
+ registerTableWithURLs: function(listname) {
+ let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
+ getService(Ci.nsIUrlListManager);
+
+ let providerName = this.listToProvider[listname];
+ let provider = this.providers[providerName];
+
+ if (!providerName || !provider) {
+ log("No provider info found for " + listname);
+ log("Check browser.safebrowsing.provider.[google/mozilla].lists");
+ return;
+ }
+
+ listManager.registerTable(listname, providerName, provider.updateURL, provider.gethashURL);
+ },
+
+ registerTables: function() {
+ for (let i = 0; i < this.phishingLists.length; ++i) {
+ this.registerTableWithURLs(this.phishingLists[i]);
+ }
+ for (let i = 0; i < this.malwareLists.length; ++i) {
+ this.registerTableWithURLs(this.malwareLists[i]);
+ }
+ for (let i = 0; i < this.downloadBlockLists.length; ++i) {
+ this.registerTableWithURLs(this.downloadBlockLists[i]);
+ }
+ for (let i = 0; i < this.downloadAllowLists.length; ++i) {
+ this.registerTableWithURLs(this.downloadAllowLists[i]);
+ }
+ for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
+ this.registerTableWithURLs(this.trackingProtectionLists[i]);
+ }
+ for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
+ this.registerTableWithURLs(this.trackingProtectionWhitelists[i]);
+ }
+ for (let i = 0; i < this.blockedLists.length; ++i) {
+ this.registerTableWithURLs(this.blockedLists[i]);
+ }
+ },
+
+
+ initialized: false,
+ phishingEnabled: false,
+ malwareEnabled: false,
+ trackingEnabled: false,
+ blockedEnabled: false,
+
+ phishingLists: [],
+ malwareLists: [],
+ downloadBlockLists: [],
+ downloadAllowLists: [],
+ trackingProtectionLists: [],
+ trackingProtectionWhitelists: [],
+ blockedLists: [],
+
+ updateURL: null,
+ gethashURL: null,
+
+ reportURL: null,
+
+ getReportURL: function(kind, URI) {
+ let pref;
+ switch (kind) {
+ case "Phish":
+ pref = "browser.safebrowsing.reportPhishURL";
+ break;
+ case "PhishMistake":
+ pref = "browser.safebrowsing.reportPhishMistakeURL";
+ break;
+ case "MalwareMistake":
+ pref = "browser.safebrowsing.reportMalwareMistakeURL";
+ break;
+
+ default:
+ let err = "SafeBrowsing getReportURL() called with unknown kind: " + kind;
+ Components.utils.reportError(err);
+ throw err;
+ }
+ let reportUrl = Services.urlFormatter.formatURLPref(pref);
+
+ let pageUri = URI.clone();
+
+ // Remove the query to avoid including potentially sensitive data
+ if (pageUri instanceof Ci.nsIURL)
+ pageUri.query = '';
+
+ reportUrl += encodeURIComponent(pageUri.asciiSpec);
+
+ return reportUrl;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // skip nextupdatetime and lastupdatetime
+ if (aData.indexOf("lastupdatetime") >= 0 || aData.indexOf("nextupdatetime") >= 0) {
+ return;
+ }
+ this.readPrefs();
+ },
+
+ readPrefs: function() {
+ log("reading prefs");
+
+ this.debug = Services.prefs.getBoolPref("browser.safebrowsing.debug");
+ this.phishingEnabled = Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled");
+ this.malwareEnabled = Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled");
+ this.trackingEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") || Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled");
+ this.blockedEnabled = Services.prefs.getBoolPref("browser.safebrowsing.blockedURIs.enabled");
+
+ [this.phishingLists,
+ this.malwareLists,
+ this.downloadBlockLists,
+ this.downloadAllowLists,
+ this.trackingProtectionLists,
+ this.trackingProtectionWhitelists,
+ this.blockedLists] = tablePreferences.map(getLists);
+
+ this.updateProviderURLs();
+ this.registerTables();
+
+ // XXX The listManager backend gets confused if this is called before the
+ // lists are registered. So only call it here when a pref changes, and not
+ // when doing initialization. I expect to refactor this later, so pardon the hack.
+ if (this.initialized) {
+ this.controlUpdateChecking();
+ }
+ },
+
+
+ updateProviderURLs: function() {
+ try {
+ var clientID = Services.prefs.getCharPref("browser.safebrowsing.id");
+ } catch(e) {
+ clientID = Services.appinfo.name;
+ }
+
+ log("initializing safe browsing URLs, client id", clientID);
+
+ // Get the different providers
+ let branch = Services.prefs.getBranch("browser.safebrowsing.provider.");
+ let children = branch.getChildList("", {});
+ this.providers = {};
+ this.listToProvider = {};
+
+ for (let child of children) {
+ log("Child: " + child);
+ let prefComponents = child.split(".");
+ let providerName = prefComponents[0];
+ this.providers[providerName] = {};
+ }
+
+ if (this.debug) {
+ let providerStr = "";
+ Object.keys(this.providers).forEach(function(provider) {
+ if (providerStr === "") {
+ providerStr = provider;
+ } else {
+ providerStr += ", " + provider;
+ }
+ });
+ log("Providers: " + providerStr);
+ }
+
+ Object.keys(this.providers).forEach(function(provider) {
+ let updateURL = Services.urlFormatter.formatURLPref(
+ "browser.safebrowsing.provider." + provider + ".updateURL");
+ let gethashURL = Services.urlFormatter.formatURLPref(
+ "browser.safebrowsing.provider." + provider + ".gethashURL");
+ updateURL = updateURL.replace("SAFEBROWSING_ID", clientID);
+ gethashURL = gethashURL.replace("SAFEBROWSING_ID", clientID);
+
+ log("Provider: " + provider + " updateURL=" + updateURL);
+ log("Provider: " + provider + " gethashURL=" + gethashURL);
+
+ // Urls used to update DB
+ this.providers[provider].updateURL = updateURL;
+ this.providers[provider].gethashURL = gethashURL;
+
+ // Get lists this provider manages
+ let lists = getLists("browser.safebrowsing.provider." + provider + ".lists");
+ if (lists) {
+ lists.forEach(function(list) {
+ this.listToProvider[list] = provider;
+ }, this);
+ } else {
+ log("Update URL given but no lists managed for provider: " + provider);
+ }
+ }, this);
+ },
+
+ controlUpdateChecking: function() {
+ log("phishingEnabled:", this.phishingEnabled, "malwareEnabled:",
+ this.malwareEnabled, "trackingEnabled:", this.trackingEnabled,
+ "blockedEnabled:", this.blockedEnabled);
+
+ let listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].
+ getService(Ci.nsIUrlListManager);
+
+ for (let i = 0; i < this.phishingLists.length; ++i) {
+ if (this.phishingEnabled) {
+ listManager.enableUpdate(this.phishingLists[i]);
+ } else {
+ listManager.disableUpdate(this.phishingLists[i]);
+ }
+ }
+ for (let i = 0; i < this.malwareLists.length; ++i) {
+ if (this.malwareEnabled) {
+ listManager.enableUpdate(this.malwareLists[i]);
+ } else {
+ listManager.disableUpdate(this.malwareLists[i]);
+ }
+ }
+ for (let i = 0; i < this.downloadBlockLists.length; ++i) {
+ if (this.malwareEnabled) {
+ listManager.enableUpdate(this.downloadBlockLists[i]);
+ } else {
+ listManager.disableUpdate(this.downloadBlockLists[i]);
+ }
+ }
+ for (let i = 0; i < this.downloadAllowLists.length; ++i) {
+ if (this.malwareEnabled) {
+ listManager.enableUpdate(this.downloadAllowLists[i]);
+ } else {
+ listManager.disableUpdate(this.downloadAllowLists[i]);
+ }
+ }
+ for (let i = 0; i < this.trackingProtectionLists.length; ++i) {
+ if (this.trackingEnabled) {
+ listManager.enableUpdate(this.trackingProtectionLists[i]);
+ } else {
+ listManager.disableUpdate(this.trackingProtectionLists[i]);
+ }
+ }
+ for (let i = 0; i < this.trackingProtectionWhitelists.length; ++i) {
+ if (this.trackingEnabled) {
+ listManager.enableUpdate(this.trackingProtectionWhitelists[i]);
+ } else {
+ listManager.disableUpdate(this.trackingProtectionWhitelists[i]);
+ }
+ }
+ for (let i = 0; i < this.blockedLists.length; ++i) {
+ if (this.blockedEnabled) {
+ listManager.enableUpdate(this.blockedLists[i]);
+ } else {
+ listManager.disableUpdate(this.blockedLists[i]);
+ }
+ }
+ listManager.maybeToggleUpdateChecking();
+ },
+
+
+ addMozEntries: function() {
+ // Add test entries to the DB.
+ // XXX bug 779008 - this could be done by DB itself?
+ const phishURL = "itisatrap.org/firefox/its-a-trap.html";
+ const malwareURL = "itisatrap.org/firefox/its-an-attack.html";
+ const unwantedURL = "itisatrap.org/firefox/unwanted.html";
+ const trackerURLs = [
+ "trackertest.org/",
+ "itisatracker.org/",
+ ];
+ const whitelistURL = "itisatrap.org/?resource=itisatracker.org";
+ const blockedURL = "itisatrap.org/firefox/blocked.html";
+
+ const flashDenyURL = "flashblock.itisatrap.org/";
+ const flashDenyExceptURL = "except.flashblock.itisatrap.org/";
+ const flashAllowURL = "flashallow.itisatrap.org/";
+ const flashAllowExceptURL = "except.flashallow.itisatrap.org/";
+ const flashSubDocURL = "flashsubdoc.itisatrap.org/";
+ const flashSubDocExceptURL = "except.flashsubdoc.itisatrap.org/";
+
+ let update = "n:1000\ni:test-malware-simple\nad:1\n" +
+ "a:1:32:" + malwareURL.length + "\n" +
+ malwareURL + "\n";
+ update += "n:1000\ni:test-phish-simple\nad:1\n" +
+ "a:1:32:" + phishURL.length + "\n" +
+ phishURL + "\n";
+ update += "n:1000\ni:test-unwanted-simple\nad:1\n" +
+ "a:1:32:" + unwantedURL.length + "\n" +
+ unwantedURL + "\n";
+ update += "n:1000\ni:test-track-simple\n" +
+ "ad:" + trackerURLs.length + "\n";
+ trackerURLs.forEach((trackerURL, i) => {
+ update += "a:" + (i + 1) + ":32:" + trackerURL.length + "\n" +
+ trackerURL + "\n";
+ });
+ update += "n:1000\ni:test-trackwhite-simple\nad:1\n" +
+ "a:1:32:" + whitelistURL.length + "\n" +
+ whitelistURL;
+ update += "n:1000\ni:test-block-simple\nad:1\n" +
+ "a:1:32:" + blockedURL.length + "\n" +
+ blockedURL;
+ update += "n:1000\ni:test-flash-simple\nad:1\n" +
+ "a:1:32:" + flashDenyURL.length + "\n" +
+ flashDenyURL;
+ update += "n:1000\ni:testexcept-flash-simple\nad:1\n" +
+ "a:1:32:" + flashDenyExceptURL.length + "\n" +
+ flashDenyExceptURL;
+ update += "n:1000\ni:test-flashallow-simple\nad:1\n" +
+ "a:1:32:" + flashAllowURL.length + "\n" +
+ flashAllowURL;
+ update += "n:1000\ni:testexcept-flashallow-simple\nad:1\n" +
+ "a:1:32:" + flashAllowExceptURL.length + "\n" +
+ flashAllowExceptURL;
+ update += "n:1000\ni:test-flashsubdoc-simple\nad:1\n" +
+ "a:1:32:" + flashSubDocURL.length + "\n" +
+ flashSubDocURL;
+ update += "n:1000\ni:testexcept-flashsubdoc-simple\nad:1\n" +
+ "a:1:32:" + flashSubDocExceptURL.length + "\n" +
+ flashSubDocExceptURL;
+ log("addMozEntries:", update);
+
+ let db = Cc["@mozilla.org/url-classifier/dbservice;1"].
+ getService(Ci.nsIUrlClassifierDBService);
+
+ // nsIUrlClassifierUpdateObserver
+ let dummyListener = {
+ updateUrlRequested: function() { },
+ streamFinished: function() { },
+ // We notify observers when we're done in order to be able to make perf
+ // test results more consistent
+ updateError: function() {
+ Services.obs.notifyObservers(db, "mozentries-update-finished", "error");
+ },
+ updateSuccess: function() {
+ Services.obs.notifyObservers(db, "mozentries-update-finished", "success");
+ }
+ };
+
+ try {
+ let tables = "test-malware-simple,test-phish-simple,test-unwanted-simple,test-track-simple,test-trackwhite-simple,test-block-simple,test-flash-simple,testexcept-flash-simple,test-flashallow-simple,testexcept-flashallow-simple,test-flashsubdoc-simple,testexcept-flashsubdoc-simple";
+ db.beginUpdate(dummyListener, tables, "");
+ db.beginStream("", "");
+ db.updateStream(update);
+ db.finishStream();
+ db.finishUpdate();
+ } catch(ex) {
+ // beginUpdate will throw harmlessly if there's an existing update in progress, ignore failures.
+ log("addMozEntries failed!", ex);
+ Services.obs.notifyObservers(db, "mozentries-update-finished", "exception");
+ }
+ },
+
+ addMozEntriesFinishedPromise: new Promise(resolve => {
+ let finished = (subject, topic, data) => {
+ Services.obs.removeObserver(finished, "mozentries-update-finished");
+ if (data == "error") {
+ Cu.reportError("addMozEntries failed to update the db!");
+ }
+ resolve();
+ };
+ Services.obs.addObserver(finished, "mozentries-update-finished", false);
+ }),
+};
diff --git a/toolkit/components/url-classifier/VariableLengthPrefixSet.cpp b/toolkit/components/url-classifier/VariableLengthPrefixSet.cpp
new file mode 100644
index 0000000000..e9d6770d35
--- /dev/null
+++ b/toolkit/components/url-classifier/VariableLengthPrefixSet.cpp
@@ -0,0 +1,443 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "VariableLengthPrefixSet.h"
+#include "nsUrlClassifierPrefixSet.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include <algorithm>
+
+// MOZ_LOG=UrlClassifierPrefixSet:5
+static mozilla::LazyLogModule gUrlClassifierPrefixSetLog("UrlClassifierPrefixSet");
+#define LOG(args) MOZ_LOG(gUrlClassifierPrefixSetLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierPrefixSetLog, mozilla::LogLevel::Debug)
+
+namespace mozilla {
+namespace safebrowsing {
+
+#define PREFIX_SIZE_FIXED 4
+
+NS_IMPL_ISUPPORTS(VariableLengthPrefixSet, nsIMemoryReporter)
+
+// Definition required due to std::max<>()
+const uint32_t VariableLengthPrefixSet::MAX_BUFFER_SIZE;
+
+// This class will process prefix size between 4~32. But for 4 bytes prefixes,
+// they will be passed to nsUrlClassifierPrefixSet because of better optimization.
+VariableLengthPrefixSet::VariableLengthPrefixSet()
+ : mLock("VariableLengthPrefixSet.mLock")
+ , mMemoryReportPath()
+{
+ mFixedPrefixSet = new nsUrlClassifierPrefixSet();
+}
+
+NS_IMETHODIMP
+VariableLengthPrefixSet::Init(const nsACString& aName)
+{
+ mMemoryReportPath =
+ nsPrintfCString(
+ "explicit/storage/prefix-set/%s",
+ (!aName.IsEmpty() ? PromiseFlatCString(aName).get() : "?!")
+ );
+
+ RegisterWeakMemoryReporter(this);
+
+ return NS_OK;
+}
+
+VariableLengthPrefixSet::~VariableLengthPrefixSet()
+{
+ UnregisterWeakMemoryReporter(this);
+}
+
+NS_IMETHODIMP
+VariableLengthPrefixSet::SetPrefixes(const PrefixStringMap& aPrefixMap)
+{
+ MutexAutoLock lock(mLock);
+
+ // Prefix size should not less than 4-bytes or greater than 32-bytes
+ for (auto iter = aPrefixMap.ConstIter(); !iter.Done(); iter.Next()) {
+ if (iter.Key() < PREFIX_SIZE_FIXED ||
+ iter.Key() > COMPLETE_SIZE) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Clear old prefixSet before setting new one.
+ mFixedPrefixSet->SetPrefixes(nullptr, 0);
+ mVLPrefixSet.Clear();
+
+ // 4-bytes prefixes are handled by nsUrlClassifierPrefixSet.
+ nsCString* prefixes = aPrefixMap.Get(PREFIX_SIZE_FIXED);
+ if (prefixes) {
+ NS_ENSURE_TRUE(prefixes->Length() % PREFIX_SIZE_FIXED == 0, NS_ERROR_FAILURE);
+
+ uint32_t numPrefixes = prefixes->Length() / PREFIX_SIZE_FIXED;
+
+#if MOZ_BIG_ENDIAN
+ const uint32_t* arrayPtr = reinterpret_cast<const uint32_t*>(prefixes->BeginReading());
+#else
+ FallibleTArray<uint32_t> array;
+ // Prefixes are lexicographically-sorted, so the interger array
+ // passed to nsUrlClassifierPrefixSet should also follow the same order.
+ // To make sure of that, we convert char array to integer with Big-Endian
+ // instead of casting to integer directly.
+ if (!array.SetCapacity(numPrefixes, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ const char* begin = prefixes->BeginReading();
+ const char* end = prefixes->EndReading();
+
+ while (begin != end) {
+ array.AppendElement(BigEndian::readUint32(begin), fallible);
+ begin += sizeof(uint32_t);
+ }
+
+ const uint32_t* arrayPtr = array.Elements();
+#endif
+
+ nsresult rv = mFixedPrefixSet->SetPrefixes(arrayPtr, numPrefixes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // 5~32 bytes prefixes are stored in mVLPrefixSet.
+ for (auto iter = aPrefixMap.ConstIter(); !iter.Done(); iter.Next()) {
+ // Skip 4bytes prefixes because it is already stored in mFixedPrefixSet.
+ if (iter.Key() == PREFIX_SIZE_FIXED) {
+ continue;
+ }
+
+ mVLPrefixSet.Put(iter.Key(), new nsCString(*iter.Data()));
+ }
+
+ return NS_OK;
+}
+
+nsresult
+VariableLengthPrefixSet::GetPrefixes(PrefixStringMap& aPrefixMap)
+{
+ MutexAutoLock lock(mLock);
+
+ // 4-bytes prefixes are handled by nsUrlClassifierPrefixSet.
+ FallibleTArray<uint32_t> array;
+ nsresult rv = mFixedPrefixSet->GetPrefixesNative(array);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ size_t count = array.Length();
+ if (count) {
+ nsCString* prefixes = new nsCString();
+ prefixes->SetLength(PREFIX_SIZE_FIXED * count);
+
+ // Writing integer array to character array
+ uint32_t* begin = reinterpret_cast<uint32_t*>(prefixes->BeginWriting());
+ for (uint32_t i = 0; i < count; i++) {
+ begin[i] = NativeEndian::swapToBigEndian(array[i]);
+ }
+
+ aPrefixMap.Put(PREFIX_SIZE_FIXED, prefixes);
+ }
+
+ // Copy variable-length prefix set
+ for (auto iter = mVLPrefixSet.ConstIter(); !iter.Done(); iter.Next()) {
+ aPrefixMap.Put(iter.Key(), new nsCString(*iter.Data()));
+ }
+
+ return NS_OK;
+}
+
+// It should never be the case that more than one hash prefixes match a given
+// full hash. However, if that happens, this method returns any one of them.
+// It does not guarantee which one of those will be returned.
+NS_IMETHODIMP
+VariableLengthPrefixSet::Matches(const nsACString& aFullHash, uint32_t* aLength)
+{
+ MutexAutoLock lock(mLock);
+
+ // Only allow full-length hash to check if match any of the prefix
+ MOZ_ASSERT(aFullHash.Length() == COMPLETE_SIZE);
+ NS_ENSURE_ARG_POINTER(aLength);
+
+ *aLength = 0;
+
+ // Check if it matches 4-bytes prefixSet first
+ const uint32_t* hash = reinterpret_cast<const uint32_t*>(aFullHash.BeginReading());
+ uint32_t value = BigEndian::readUint32(hash);
+
+ bool found = false;
+ nsresult rv = mFixedPrefixSet->Contains(value, &found);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (found) {
+ *aLength = PREFIX_SIZE_FIXED;
+ return NS_OK;
+ }
+
+ for (auto iter = mVLPrefixSet.ConstIter(); !iter.Done(); iter.Next()) {
+ if (BinarySearch(aFullHash, *iter.Data(), iter.Key())) {
+ *aLength = iter.Key();
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VariableLengthPrefixSet::IsEmpty(bool* aEmpty)
+{
+ MutexAutoLock lock(mLock);
+
+ NS_ENSURE_ARG_POINTER(aEmpty);
+
+ mFixedPrefixSet->IsEmpty(aEmpty);
+ *aEmpty = *aEmpty && mVLPrefixSet.IsEmpty();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VariableLengthPrefixSet::LoadFromFile(nsIFile* aFile)
+{
+ MutexAutoLock lock(mLock);
+
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_VLPS_FILELOAD_TIME> timer;
+
+ nsCOMPtr<nsIInputStream> localInFile;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(localInFile), aFile,
+ PR_RDONLY | nsIFile::OS_READAHEAD);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Calculate how big the file is, make sure our read buffer isn't bigger
+ // than the file itself which is just wasting memory.
+ int64_t fileSize;
+ rv = aFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (fileSize < 0 || fileSize > UINT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t bufferSize = std::min<uint32_t>(static_cast<uint32_t>(fileSize),
+ MAX_BUFFER_SIZE);
+
+ // Convert to buffered stream
+ nsCOMPtr<nsIInputStream> in = NS_BufferInputStream(localInFile, bufferSize);
+
+ rv = mFixedPrefixSet->LoadPrefixes(in);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = LoadPrefixes(in);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;;
+}
+
+NS_IMETHODIMP
+VariableLengthPrefixSet::StoreToFile(nsIFile* aFile)
+{
+ NS_ENSURE_ARG_POINTER(aFile);
+
+ MutexAutoLock lock(mLock);
+
+ nsCOMPtr<nsIOutputStream> localOutFile;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(localOutFile), aFile,
+ PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t fileSize = 0;
+ // Preallocate the file storage
+ {
+ nsCOMPtr<nsIFileOutputStream> fos(do_QueryInterface(localOutFile));
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_VLPS_FALLOCATE_TIME> timer;
+
+ fileSize += mFixedPrefixSet->CalculatePreallocateSize();
+ fileSize += CalculatePreallocateSize();
+
+ Unused << fos->Preallocate(fileSize);
+ }
+
+ // Convert to buffered stream
+ nsCOMPtr<nsIOutputStream> out =
+ NS_BufferOutputStream(localOutFile, std::min(fileSize, MAX_BUFFER_SIZE));
+
+ rv = mFixedPrefixSet->WritePrefixes(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = WritePrefixes(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+VariableLengthPrefixSet::LoadPrefixes(nsIInputStream* in)
+{
+ uint32_t magic;
+ uint32_t read;
+
+ nsresult rv = in->Read(reinterpret_cast<char*>(&magic), sizeof(uint32_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint32_t), NS_ERROR_FAILURE);
+
+ if (magic != PREFIXSET_VERSION_MAGIC) {
+ LOG(("Version magic mismatch, not loading"));
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ mVLPrefixSet.Clear();
+
+ uint32_t count;
+ rv = in->Read(reinterpret_cast<char*>(&count), sizeof(uint32_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint32_t), NS_ERROR_FAILURE);
+
+ for(;count > 0; count--) {
+ uint8_t prefixSize;
+ rv = in->Read(reinterpret_cast<char*>(&prefixSize), sizeof(uint8_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint8_t), NS_ERROR_FAILURE);
+
+ uint32_t stringLength;
+ rv = in->Read(reinterpret_cast<char*>(&stringLength), sizeof(uint32_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint32_t), NS_ERROR_FAILURE);
+
+ nsCString* vlPrefixes = new nsCString();
+ if (!vlPrefixes->SetLength(stringLength, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ rv = in->Read(reinterpret_cast<char*>(vlPrefixes->BeginWriting()), stringLength, &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == stringLength, NS_ERROR_FAILURE);
+
+ mVLPrefixSet.Put(prefixSize, vlPrefixes);
+ }
+
+ return NS_OK;
+}
+
+uint32_t
+VariableLengthPrefixSet::CalculatePreallocateSize()
+{
+ uint32_t fileSize = 0;
+
+ // Store how many prefix string.
+ fileSize += sizeof(uint32_t);
+
+ for (auto iter = mVLPrefixSet.ConstIter(); !iter.Done(); iter.Next()) {
+ // Store prefix size, prefix string length, and prefix string.
+ fileSize += sizeof(uint8_t);
+ fileSize += sizeof(uint32_t);
+ fileSize += iter.Data()->Length();
+ }
+ return fileSize;
+}
+
+nsresult
+VariableLengthPrefixSet::WritePrefixes(nsIOutputStream* out)
+{
+ uint32_t written;
+ uint32_t writelen = sizeof(uint32_t);
+ uint32_t magic = PREFIXSET_VERSION_MAGIC;
+ nsresult rv = out->Write(reinterpret_cast<char*>(&magic), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ uint32_t count = mVLPrefixSet.Count();
+ rv = out->Write(reinterpret_cast<char*>(&count), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ // Store PrefixSize, Length of Prefix String and then Prefix String
+ for (auto iter = mVLPrefixSet.ConstIter(); !iter.Done(); iter.Next()) {
+ const nsCString& vlPrefixes = *iter.Data();
+
+ uint8_t prefixSize = iter.Key();
+ writelen = sizeof(uint8_t);
+ rv = out->Write(reinterpret_cast<char*>(&prefixSize), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ uint32_t stringLength = vlPrefixes.Length();
+ writelen = sizeof(uint32_t);
+ rv = out->Write(reinterpret_cast<char*>(&stringLength), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ rv = out->Write(const_cast<char*>(vlPrefixes.BeginReading()),
+ stringLength, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(stringLength == written, NS_ERROR_FAILURE);
+ }
+
+ return NS_OK;
+}
+
+bool
+VariableLengthPrefixSet::BinarySearch(const nsACString& aFullHash,
+ const nsACString& aPrefixes,
+ uint32_t aPrefixSize)
+{
+ const char* fullhash = aFullHash.BeginReading();
+ const char* prefixes = aPrefixes.BeginReading();
+ int32_t begin = 0, end = aPrefixes.Length() / aPrefixSize;
+
+ while (end > begin) {
+ int32_t mid = (begin + end) >> 1;
+ int cmp = memcmp(fullhash, prefixes + mid*aPrefixSize, aPrefixSize);
+ if (cmp < 0) {
+ end = mid;
+ } else if (cmp > 0) {
+ begin = mid + 1;
+ } else {
+ return true;
+ }
+ }
+ return false;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(UrlClassifierMallocSizeOf)
+
+NS_IMETHODIMP
+VariableLengthPrefixSet::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ size_t amount = SizeOfIncludingThis(UrlClassifierMallocSizeOf);
+
+ return aHandleReport->Callback(
+ EmptyCString(), mMemoryReportPath, KIND_HEAP, UNITS_BYTES, amount,
+ NS_LITERAL_CSTRING("Memory used by the variable-length prefix set for a URL classifier."),
+ aData);
+}
+
+size_t
+VariableLengthPrefixSet::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ MutexAutoLock lock(mLock);
+
+ size_t n = 0;
+ n += aMallocSizeOf(this);
+ n += mFixedPrefixSet->SizeOfIncludingThis(moz_malloc_size_of) - aMallocSizeOf(mFixedPrefixSet);
+
+ n += mVLPrefixSet.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = mVLPrefixSet.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Data()->SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
diff --git a/toolkit/components/url-classifier/VariableLengthPrefixSet.h b/toolkit/components/url-classifier/VariableLengthPrefixSet.h
new file mode 100644
index 0000000000..eca2148855
--- /dev/null
+++ b/toolkit/components/url-classifier/VariableLengthPrefixSet.h
@@ -0,0 +1,70 @@
+//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 VariableLengthPrefixSet_h
+#define VariableLengthPrefixSet_h
+
+#include "nsISupports.h"
+#include "nsIMemoryReporter.h"
+#include "Entries.h"
+#include "nsTArray.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+
+class nsUrlClassifierPrefixSet;
+
+namespace mozilla {
+namespace safebrowsing {
+
+class VariableLengthPrefixSet final
+ : public nsIMemoryReporter
+{
+public:
+ VariableLengthPrefixSet();
+
+ NS_IMETHOD Init(const nsACString& aName);
+ NS_IMETHOD SetPrefixes(const mozilla::safebrowsing::PrefixStringMap& aPrefixMap);
+ NS_IMETHOD GetPrefixes(mozilla::safebrowsing::PrefixStringMap& aPrefixMap);
+ NS_IMETHOD Matches(const nsACString& aFullHash, uint32_t* aLength);
+ NS_IMETHOD IsEmpty(bool* aEmpty);
+ NS_IMETHOD LoadFromFile(nsIFile* aFile);
+ NS_IMETHOD StoreToFile(nsIFile* aFile);
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+private:
+ virtual ~VariableLengthPrefixSet();
+
+ static const uint32_t MAX_BUFFER_SIZE = 64 * 1024;
+ static const uint32_t PREFIXSET_VERSION_MAGIC = 1;
+
+ bool BinarySearch(const nsACString& aFullHash,
+ const nsACString& aPrefixes,
+ uint32_t aPrefixSize);
+
+ uint32_t CalculatePreallocateSize();
+ nsresult WritePrefixes(nsIOutputStream* out);
+ nsresult LoadPrefixes(nsIInputStream* in);
+
+ // Lock to prevent races between the url-classifier thread (which does most
+ // of the operations) and the main thread (which does memory reporting).
+ // It should be held for all operations between Init() and destruction that
+ // touch this class's data members.
+ mozilla::Mutex mLock;
+
+ RefPtr<nsUrlClassifierPrefixSet> mFixedPrefixSet;
+ mozilla::safebrowsing::PrefixStringMap mVLPrefixSet;
+
+ nsCString mMemoryReportPath;
+};
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/url-classifier/chromium/README.txt b/toolkit/components/url-classifier/chromium/README.txt
new file mode 100644
index 0000000000..e5a0f41326
--- /dev/null
+++ b/toolkit/components/url-classifier/chromium/README.txt
@@ -0,0 +1,23 @@
+# Overview
+
+'safebrowsing.proto' is modified from [1] with the following line added:
+
+"package mozilla.safebrowsing;"
+
+to avoid naming pollution. We use this source file along with protobuf compiler (protoc) to generate safebrowsing.pb.h/cc for safebrowsing v4 update and hash completion. The current generated files are compiled by protoc 2.6.1 since the protobuf library in gecko is not upgraded to 3.0 yet.
+
+# Update
+
+If you want to update to the latest upstream version,
+
+1. Checkout the latest one in [2]
+2. Use protoc to generate safebrowsing.pb.h and safebrowsing.pb.cc. For example,
+
+$ protoc -I=. --cpp_out="../protobuf/" safebrowsing.proto
+
+(Note that we should use protoc v2.6.1 [3] to compile. You can find the compiled protoc in [4] if you don't have one.)
+
+[1] https://chromium.googlesource.com/chromium/src.git/+/9c4485f1ce7cac7ae82f7a4ae36ccc663afe806c/components/safe_browsing_db/safebrowsing.proto
+[2] https://chromium.googlesource.com/chromium/src.git/+/master/components/safe_browsing_db/safebrowsing.proto
+[3] https://github.com/google/protobuf/releases/tag/v2.6.1
+[4] https://repo1.maven.org/maven2/com/google/protobuf/protoc
diff --git a/toolkit/components/url-classifier/chromium/safebrowsing.proto b/toolkit/components/url-classifier/chromium/safebrowsing.proto
new file mode 100644
index 0000000000..d621552443
--- /dev/null
+++ b/toolkit/components/url-classifier/chromium/safebrowsing.proto
@@ -0,0 +1,473 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// This file includes Safe Browsing V4 API blacklist request and response
+// protocol buffers. They should be kept in sync with the server implementation.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package mozilla.safebrowsing;
+
+message ThreatInfo {
+ // The threat types to be checked.
+ repeated ThreatType threat_types = 1;
+
+ // The platform types to be checked.
+ repeated PlatformType platform_types = 2;
+
+ // The entry types to be checked.
+ repeated ThreatEntryType threat_entry_types = 4;
+
+ // The threat entries to be checked.
+ repeated ThreatEntry threat_entries = 3;
+}
+
+// A match when checking a threat entry in the Safe Browsing threat lists.
+message ThreatMatch {
+ // The threat type matching this threat.
+ optional ThreatType threat_type = 1;
+
+ // The platform type matching this threat.
+ optional PlatformType platform_type = 2;
+
+ // The threat entry type matching this threat.
+ optional ThreatEntryType threat_entry_type = 6;
+
+ // The threat matching this threat.
+ optional ThreatEntry threat = 3;
+
+ // Optional metadata associated with this threat.
+ optional ThreatEntryMetadata threat_entry_metadata = 4;
+
+ // The cache lifetime for the returned match. Clients must not cache this
+ // response for more than this duration to avoid false positives.
+ optional Duration cache_duration = 5;
+}
+
+// Request to check entries against lists.
+message FindThreatMatchesRequest {
+ // The client metadata.
+ optional ClientInfo client = 1;
+
+ // The lists and entries to be checked for matches.
+ optional ThreatInfo threat_info = 2;
+}
+
+// Response type for requests to find threat matches.
+message FindThreatMatchesResponse {
+ // The threat list matches.
+ repeated ThreatMatch matches = 1;
+}
+
+// Describes a Safe Browsing API update request. Clients can request updates for
+// multiple lists in a single request.
+message FetchThreatListUpdatesRequest {
+ // The client metadata.
+ optional ClientInfo client = 1;
+
+ // A single list update request.
+ message ListUpdateRequest {
+ // The type of threat posed by entries present in the list.
+ optional ThreatType threat_type = 1;
+
+ // The type of platform at risk by entries present in the list.
+ optional PlatformType platform_type = 2;
+
+ // The types of entries present in the list.
+ optional ThreatEntryType threat_entry_type = 5;
+
+ // The current state of the client for the requested list (the encrypted
+ // ClientState that was sent to the client from the previous update
+ // request).
+ optional bytes state = 3;
+
+ // The constraints for this update.
+ message Constraints {
+ // The maximum size in number of entries. The update will not contain more
+ // entries than this value. This should be a power of 2 between 2**10 and
+ // 2**20. If zero, no update size limit is set.
+ optional int32 max_update_entries = 1;
+
+ // Sets the maxmimum number of entries that the client is willing to have
+ // in the local database. This should be a power of 2 between 2**10 and
+ // 2**20. If zero, no database size limit is set.
+ optional int32 max_database_entries = 2;
+
+ // Requests the list for a specific geographic location. If not set the
+ // server may pick that value based on the user's IP address. Expects ISO
+ // 3166-1 alpha-2 format.
+ optional string region = 3;
+
+ // The compression types supported by the client.
+ repeated CompressionType supported_compressions = 4;
+ }
+
+ // The constraints associated with this request.
+ optional Constraints constraints = 4;
+ }
+
+ // The requested threat list updates.
+ repeated ListUpdateRequest list_update_requests = 3;
+}
+
+// Response type for threat list update requests.
+message FetchThreatListUpdatesResponse {
+ // An update to an individual list.
+ message ListUpdateResponse {
+ // The threat type for which data is returned.
+ optional ThreatType threat_type = 1;
+
+ // The format of the threats.
+ optional ThreatEntryType threat_entry_type = 2;
+
+ // The platform type for which data is returned.
+ optional PlatformType platform_type = 3;
+
+ // The type of response sent to the client.
+ enum ResponseType {
+ // Unknown.
+ RESPONSE_TYPE_UNSPECIFIED = 0;
+
+ // Partial updates are applied to the client's existing local database.
+ PARTIAL_UPDATE = 1;
+
+ // Full updates replace the client's entire local database. This means
+ // that either the client was seriously out-of-date or the client is
+ // believed to be corrupt.
+ FULL_UPDATE = 2;
+ }
+
+ // The type of response. This may indicate that an action is required by the
+ // client when the response is received.
+ optional ResponseType response_type = 4;
+
+ // A set of entries to add to a local threat type's list. Repeated to allow
+ // for a combination of compressed and raw data to be sent in a single
+ // response.
+ repeated ThreatEntrySet additions = 5;
+
+ // A set of entries to remove from a local threat type's list. Repeated for
+ // the same reason as above.
+ repeated ThreatEntrySet removals = 6;
+
+ // The new client state, in encrypted format. Opaque to clients.
+ optional bytes new_client_state = 7;
+
+ // The expected SHA256 hash of the client state; that is, of the sorted list
+ // of all hashes present in the database after applying the provided update.
+ // If the client state doesn't match the expected state, the client must
+ // disregard this update and retry later.
+ optional Checksum checksum = 8;
+ }
+
+ // The list updates requested by the clients.
+ repeated ListUpdateResponse list_update_responses = 1;
+
+ // The minimum duration the client must wait before issuing any update
+ // request. If this field is not set clients may update as soon as they want.
+ optional Duration minimum_wait_duration = 2;
+}
+
+// Request to return full hashes matched by the provided hash prefixes.
+message FindFullHashesRequest {
+ // The client metadata.
+ optional ClientInfo client = 1;
+
+ // The current client states for each of the client's local threat lists.
+ repeated bytes client_states = 2;
+
+ // The lists and hashes to be checked.
+ optional ThreatInfo threat_info = 3;
+}
+
+// Response type for requests to find full hashes.
+message FindFullHashesResponse {
+ // The full hashes that matched the requested prefixes.
+ repeated ThreatMatch matches = 1;
+
+ // The minimum duration the client must wait before issuing any find hashes
+ // request. If this field is not set, clients can issue a request as soon as
+ // they want.
+ optional Duration minimum_wait_duration = 2;
+
+ // For requested entities that did not match the threat list, how long to
+ // cache the response.
+ optional Duration negative_cache_duration = 3;
+}
+
+// A hit comprised of multiple resources; one is the threat list entry that was
+// encountered by the client, while others give context as to how the client
+// arrived at the unsafe entry.
+message ThreatHit {
+ // The threat type reported.
+ optional ThreatType threat_type = 1;
+
+ // The platform type reported.
+ optional PlatformType platform_type = 2;
+
+ // The threat entry responsible for the hit. Full hash should be reported for
+ // hash-based hits.
+ optional ThreatEntry entry = 3;
+
+ // Types of resources reported by the client as part of a single hit.
+ enum ThreatSourceType {
+ // Unknown.
+ THREAT_SOURCE_TYPE_UNSPECIFIED = 0;
+ // The URL that matched the threat list (for which GetFullHash returned a
+ // valid hash).
+ MATCHING_URL = 1;
+ // The final top-level URL of the tab that the client was browsing when the
+ // match occurred.
+ TAB_URL = 2;
+ // A redirect URL that was fetched before hitting the final TAB_URL.
+ TAB_REDIRECT = 3;
+ }
+
+ // A single resource related to a threat hit.
+ message ThreatSource {
+ // The URL of the resource.
+ optional string url = 1;
+
+ // The type of source reported.
+ optional ThreatSourceType type = 2;
+
+ // The remote IP of the resource in ASCII format. Either IPv4 or IPv6.
+ optional string remote_ip = 3;
+
+ // Referrer of the resource. Only set if the referrer is available.
+ optional string referrer = 4;
+ }
+
+ // The resources related to the threat hit.
+ repeated ThreatSource resources = 4;
+}
+
+// Types of threats.
+enum ThreatType {
+ // Unknown.
+ THREAT_TYPE_UNSPECIFIED = 0;
+
+ // Malware threat type.
+ MALWARE_THREAT = 1;
+
+ // Social engineering threat type.
+ SOCIAL_ENGINEERING_PUBLIC = 2;
+
+ // Unwanted software threat type.
+ UNWANTED_SOFTWARE = 3;
+
+ // Potentially harmful application threat type.
+ POTENTIALLY_HARMFUL_APPLICATION = 4;
+
+ // Social engineering threat type for internal use.
+ SOCIAL_ENGINEERING = 5;
+
+ // API abuse threat type.
+ API_ABUSE = 6;
+}
+
+// Types of platforms.
+enum PlatformType {
+ // Unknown platform.
+ PLATFORM_TYPE_UNSPECIFIED = 0;
+
+ // Threat posed to Windows.
+ WINDOWS_PLATFORM = 1;
+
+ // Threat posed to Linux.
+ LINUX_PLATFORM = 2;
+
+ // Threat posed to Android.
+ // This cannot be ANDROID because that symbol is defined for android builds
+ // here: build/config/android/BUILD.gn line21.
+ ANDROID_PLATFORM = 3;
+
+ // Threat posed to OSX.
+ OSX_PLATFORM = 4;
+
+ // Threat posed to iOS.
+ IOS_PLATFORM = 5;
+
+ // Threat posed to at least one of the defined platforms.
+ ANY_PLATFORM = 6;
+
+ // Threat posed to all defined platforms.
+ ALL_PLATFORMS = 7;
+
+ // Threat posed to Chrome.
+ CHROME_PLATFORM = 8;
+}
+
+// The client metadata associated with Safe Browsing API requests.
+message ClientInfo {
+ // A client ID that (hopefully) uniquely identifies the client implementation
+ // of the Safe Browsing API.
+ optional string client_id = 1;
+
+ // The version of the client implementation.
+ optional string client_version = 2;
+}
+
+// The expected state of a client's local database.
+message Checksum {
+ // The SHA256 hash of the client state; that is, of the sorted list of all
+ // hashes present in the database.
+ optional bytes sha256 = 1;
+}
+
+// The ways in which threat entry sets can be compressed.
+enum CompressionType {
+ // Unknown.
+ COMPRESSION_TYPE_UNSPECIFIED = 0;
+
+ // Raw, uncompressed data.
+ RAW = 1;
+
+ // Rice-Golomb encoded data.
+ RICE = 2;
+}
+
+// An individual threat; for example, a malicious URL or its hash
+// representation. Only one of these fields should be set.
+message ThreatEntry {
+ // A variable-length SHA256 hash with size between 4 and 32 bytes inclusive.
+ optional bytes hash = 1;
+
+ // A URL.
+ optional string url = 2;
+}
+
+// Types of entries that pose threats. Threat lists are collections of entries
+// of a single type.
+enum ThreatEntryType {
+ // Unspecified.
+ THREAT_ENTRY_TYPE_UNSPECIFIED = 0;
+
+ // A host-suffix/path-prefix URL expression; for example, "foo.bar.com/baz/".
+ URL = 1;
+
+ // An executable program.
+ EXECUTABLE = 2;
+
+ // An IP range.
+ IP_RANGE = 3;
+}
+
+// A set of threats that should be added or removed from a client's local
+// database.
+message ThreatEntrySet {
+ // The compression type for the entries in this set.
+ optional CompressionType compression_type = 1;
+
+ // At most one of the following fields should be set.
+
+ // The raw SHA256-formatted entries.
+ optional RawHashes raw_hashes = 2;
+
+ // The raw removal indices for a local list.
+ optional RawIndices raw_indices = 3;
+
+ // The encoded 4-byte prefixes of SHA256-formatted entries, using a
+ // Golomb-Rice encoding.
+ optional RiceDeltaEncoding rice_hashes = 4;
+
+ // The encoded local, lexicographically-sorted list indices, using a
+ // Golomb-Rice encoding. Used for sending compressed removal indicies.
+ optional RiceDeltaEncoding rice_indices = 5;
+}
+
+// A set of raw indicies to remove from a local list.
+message RawIndices {
+ // The indicies to remove from a lexicographically-sorted local list.
+ repeated int32 indices = 1;
+}
+
+// The uncompressed threat entries in hash format of a particular prefix length.
+// Hashes can be anywhere from 4 to 32 bytes in size. A large majority are 4
+// bytes, but some hashes are lengthened if they collide with the hash of a
+// popular URL.
+//
+// Used for sending ThreatEntrySet to clients that do not support compression,
+// or when sending non-4-byte hashes to clients that do support compression.
+message RawHashes {
+ // The number of bytes for each prefix encoded below. This field can be
+ // anywhere from 4 (shortest prefix) to 32 (full SHA256 hash).
+ optional int32 prefix_size = 1;
+
+ // The hashes, all concatenated into one long string. Each hash has a prefix
+ // size of |prefix_size| above. Hashes are sorted in lexicographic order.
+ optional bytes raw_hashes = 2;
+}
+
+// The Rice-Golomb encoded data. Used for sending compressed 4-byte hashes or
+// compressed removal indices.
+message RiceDeltaEncoding {
+ // The offset of the first entry in the encoded data, or, if only a single
+ // integer was encoded, that single integer's value.
+ optional int64 first_value = 1;
+
+ // The Golomb-Rice parameter which is a number between 2 and 28. This field
+ // is missing (that is, zero) if num_entries is zero.
+ optional int32 rice_parameter = 2;
+
+ // The number of entries that are delta encoded in the encoded data. If only a
+ // single integer was encoded, this will be zero and the single value will be
+ // stored in first_value.
+ optional int32 num_entries = 3;
+
+ // The encoded deltas that are encoded using the Golomb-Rice coder.
+ optional bytes encoded_data = 4;
+}
+
+// The metadata associated with a specific threat entry. The client is expected
+// to know the metadata key/value pairs associated with each threat type.
+message ThreatEntryMetadata {
+ // A single metadata entry.
+ message MetadataEntry {
+ // The metadata entry key.
+ optional bytes key = 1;
+
+ // The metadata entry value.
+ optional bytes value = 2;
+ }
+
+ // The metadata entries.
+ repeated MetadataEntry entries = 1;
+}
+
+// Describes an individual threat list. A list is defined by three parameters:
+// the type of threat posed, the type of platform targeted by the threat, and
+// the type of entries in the list.
+message ThreatListDescriptor {
+ // The threat type posed by the list's entries.
+ optional ThreatType threat_type = 1;
+
+ // The platform type targeted by the list's entries.
+ optional PlatformType platform_type = 2;
+
+ // The entry types contained in the list.
+ optional ThreatEntryType threat_entry_type = 3;
+}
+
+// A collection of lists available for download.
+message ListThreatListsResponse {
+ // The lists available for download.
+ repeated ThreatListDescriptor threat_lists = 1;
+}
+
+message Duration {
+ // Signed seconds of the span of time. Must be from -315,576,000,000
+ // to +315,576,000,000 inclusive.
+ optional int64 seconds = 1;
+
+ // Signed fractions of a second at nanosecond resolution of the span
+ // of time. Durations less than one second are represented with a 0
+ // `seconds` field and a positive or negative `nanos` field. For durations
+ // of one second or more, a non-zero value for the `nanos` field must be
+ // of the same sign as the `seconds` field. Must be from -999,999,999
+ // to +999,999,999 inclusive.
+ optional int32 nanos = 2;
+}
diff --git a/toolkit/components/url-classifier/content/listmanager.js b/toolkit/components/url-classifier/content/listmanager.js
new file mode 100644
index 0000000000..68325bec8f
--- /dev/null
+++ b/toolkit/components/url-classifier/content/listmanager.js
@@ -0,0 +1,601 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+// This is the only implementation of nsIUrlListManager.
+// A class that manages lists, namely white and black lists for
+// phishing or malware protection. The ListManager knows how to fetch,
+// update, and store lists.
+//
+// There is a single listmanager for the whole application.
+//
+// TODO more comprehensive update tests, for example add unittest check
+// that the listmanagers tables are properly written on updates
+
+// Lower and upper limits on the server-provided polling frequency
+const minDelayMs = 5 * 60 * 1000;
+const maxDelayMs = 24 * 60 * 60 * 1000;
+
+// Log only if browser.safebrowsing.debug is true
+this.log = function log(...stuff) {
+ var prefs_ = new G_Preferences();
+ var debug = prefs_.getPref("browser.safebrowsing.debug");
+ if (!debug) {
+ return;
+ }
+
+ var d = new Date();
+ let msg = "listmanager: " + d.toTimeString() + ": " + stuff.join(" ");
+ msg = Services.urlFormatter.trimSensitiveURLs(msg);
+ Services.console.logStringMessage(msg);
+ dump(msg + "\n");
+}
+
+this.QueryAdapter = function QueryAdapter(callback) {
+ this.callback_ = callback;
+};
+
+QueryAdapter.prototype.handleResponse = function(value) {
+ this.callback_.handleEvent(value);
+}
+
+/**
+ * A ListManager keeps track of black and white lists and knows
+ * how to update them.
+ *
+ * @constructor
+ */
+this.PROT_ListManager = function PROT_ListManager() {
+ log("Initializing list manager");
+ this.prefs_ = new G_Preferences();
+ this.updateInterval = this.prefs_.getPref("urlclassifier.updateinterval", 30 * 60) * 1000;
+
+ // A map of tableNames to objects of type
+ // { updateUrl: <updateUrl>, gethashUrl: <gethashUrl> }
+ this.tablesData = {};
+ // A map of updateUrls to maps of tables requiring updates, e.g.
+ // { safebrowsing-update-url: { goog-phish-shavar: true,
+ // goog-malware-shavar: true }
+ this.needsUpdate_ = {};
+
+ this.observerServiceObserver_ = new G_ObserverServiceObserver(
+ 'quit-application',
+ BindToObject(this.shutdown_, this),
+ true /*only once*/);
+
+ // A map of updateUrls to single-use G_Alarms. An entry exists if and only if
+ // there is at least one table with updates enabled for that url. G_Alarms
+ // are reset when enabling/disabling updates or on update callbacks (update
+ // success, update failure, download error).
+ this.updateCheckers_ = {};
+ this.requestBackoffs_ = {};
+ this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+
+
+ this.hashCompleter_ = Cc["@mozilla.org/url-classifier/hashcompleter;1"]
+ .getService(Ci.nsIUrlClassifierHashCompleter);
+}
+
+/**
+ * xpcom-shutdown callback
+ * Delete all of our data tables which seem to leak otherwise.
+ */
+PROT_ListManager.prototype.shutdown_ = function() {
+ for (var name in this.tablesData) {
+ delete this.tablesData[name];
+ }
+}
+
+/**
+ * Register a new table table
+ * @param tableName - the name of the table
+ * @param updateUrl - the url for updating the table
+ * @param gethashUrl - the url for fetching hash completions
+ * @returns true if the table could be created; false otherwise
+ */
+PROT_ListManager.prototype.registerTable = function(tableName,
+ providerName,
+ updateUrl,
+ gethashUrl) {
+ log("registering " + tableName + " with " + updateUrl);
+ if (!updateUrl) {
+ log("Can't register table " + tableName + " without updateUrl");
+ return false;
+ }
+ this.tablesData[tableName] = {};
+ this.tablesData[tableName].updateUrl = updateUrl;
+ this.tablesData[tableName].gethashUrl = gethashUrl;
+ this.tablesData[tableName].provider = providerName;
+
+ // Keep track of all of our update URLs.
+ if (!this.needsUpdate_[updateUrl]) {
+ this.needsUpdate_[updateUrl] = {};
+
+ // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
+ this.requestBackoffs_[updateUrl] = new RequestBackoffV4(
+ 4 /* num requests */,
+ 60*60*1000 /* request time, 60 min */);
+ }
+ this.needsUpdate_[updateUrl][tableName] = false;
+
+ return true;
+}
+
+PROT_ListManager.prototype.getGethashUrl = function(tableName) {
+ if (this.tablesData[tableName] && this.tablesData[tableName].gethashUrl) {
+ return this.tablesData[tableName].gethashUrl;
+ }
+ return "";
+}
+
+/**
+ * Enable updates for some tables
+ * @param tables - an array of table names that need updating
+ */
+PROT_ListManager.prototype.enableUpdate = function(tableName) {
+ var table = this.tablesData[tableName];
+ if (table) {
+ log("Enabling table updates for " + tableName);
+ this.needsUpdate_[table.updateUrl][tableName] = true;
+ }
+}
+
+/**
+ * Returns true if any table associated with the updateUrl requires updates.
+ * @param updateUrl - the updateUrl
+ */
+PROT_ListManager.prototype.updatesNeeded_ = function(updateUrl) {
+ let updatesNeeded = false;
+ for (var tableName in this.needsUpdate_[updateUrl]) {
+ if (this.needsUpdate_[updateUrl][tableName]) {
+ updatesNeeded = true;
+ }
+ }
+ return updatesNeeded;
+}
+
+/**
+ * Disables updates for some tables
+ * @param tables - an array of table names that no longer need updating
+ */
+PROT_ListManager.prototype.disableUpdate = function(tableName) {
+ var table = this.tablesData[tableName];
+ if (table) {
+ log("Disabling table updates for " + tableName);
+ this.needsUpdate_[table.updateUrl][tableName] = false;
+ if (!this.updatesNeeded_(table.updateUrl) &&
+ this.updateCheckers_[table.updateUrl]) {
+ this.updateCheckers_[table.updateUrl].cancel();
+ this.updateCheckers_[table.updateUrl] = null;
+ }
+ }
+}
+
+/**
+ * Determine if we have some tables that need updating.
+ */
+PROT_ListManager.prototype.requireTableUpdates = function() {
+ for (var name in this.tablesData) {
+ // Tables that need updating even if other tables don't require it
+ if (this.needsUpdate_[this.tablesData[name].updateUrl][name]) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Acts as a nsIUrlClassifierCallback for getTables.
+ */
+PROT_ListManager.prototype.kickoffUpdate_ = function (onDiskTableData)
+{
+ this.startingUpdate_ = false;
+ var initialUpdateDelay = 3000;
+ // Add a fuzz of 0-1 minutes for both v2 and v4 according to Bug 1305478.
+ initialUpdateDelay += Math.floor(Math.random() * (1 * 60 * 1000));
+
+ // If the user has never downloaded tables, do the check now.
+ log("needsUpdate: " + JSON.stringify(this.needsUpdate_, undefined, 2));
+ for (var updateUrl in this.needsUpdate_) {
+ // If we haven't already kicked off updates for this updateUrl, set a
+ // non-repeating timer for it. The timer delay will be reset either on
+ // updateSuccess to this.updateInterval, or backed off on downloadError.
+ // Don't set the updateChecker unless at least one table has updates
+ // enabled.
+ if (this.updatesNeeded_(updateUrl) && !this.updateCheckers_[updateUrl]) {
+ let provider = null;
+ Object.keys(this.tablesData).forEach(function(table) {
+ if (this.tablesData[table].updateUrl === updateUrl) {
+ let newProvider = this.tablesData[table].provider;
+ if (provider) {
+ if (newProvider !== provider) {
+ log("Multiple tables for the same updateURL have a different provider?!");
+ }
+ } else {
+ provider = newProvider;
+ }
+ }
+ }, this);
+ log("Initializing update checker for " + updateUrl
+ + " provided by " + provider);
+
+ // Use the initialUpdateDelay + fuzz unless we had previous updates
+ // and the server told us when to try again.
+ let updateDelay = initialUpdateDelay;
+ let targetPref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+ let nextUpdate = this.prefs_.getPref(targetPref);
+ if (nextUpdate) {
+ updateDelay = Math.min(maxDelayMs, Math.max(0, nextUpdate - Date.now()));
+ log("Next update at " + nextUpdate);
+ }
+ log("Next update " + updateDelay + "ms from now");
+
+ // Set the last update time to verify if data is still valid.
+ let freshnessPref = "browser.safebrowsing.provider." + provider + ".lastupdatetime";
+ let freshness = this.prefs_.getPref(freshnessPref);
+ if (freshness) {
+ Object.keys(this.tablesData).forEach(function(table) {
+ if (this.tablesData[table].provider === provider) {
+ this.dbService_.setLastUpdateTime(table, freshness);
+ }}, this);
+ }
+
+ this.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ updateDelay, false /* repeating */);
+ } else {
+ log("No updates needed or already initialized for " + updateUrl);
+ }
+ }
+}
+
+PROT_ListManager.prototype.stopUpdateCheckers = function() {
+ log("Stopping updates");
+ for (var updateUrl in this.updateCheckers_) {
+ if (this.updateCheckers_[updateUrl]) {
+ this.updateCheckers_[updateUrl].cancel();
+ this.updateCheckers_[updateUrl] = null;
+ }
+ }
+}
+
+/**
+ * Determine if we have any tables that require updating. Different
+ * Wardens may call us with new tables that need to be updated.
+ */
+PROT_ListManager.prototype.maybeToggleUpdateChecking = function() {
+ // We update tables if we have some tables that want updates. If there
+ // are no tables that want to be updated - we dont need to check anything.
+ if (this.requireTableUpdates()) {
+ log("Starting managing lists");
+
+ // Get the list of existing tables from the DBService before making any
+ // update requests.
+ if (!this.startingUpdate_) {
+ this.startingUpdate_ = true;
+ // check the current state of tables in the database
+ this.dbService_.getTables(BindToObject(this.kickoffUpdate_, this));
+ }
+ } else {
+ log("Stopping managing lists (if currently active)");
+ this.stopUpdateCheckers(); // Cancel pending updates
+ }
+}
+
+/**
+ * Provides an exception free way to look up the data in a table. We
+ * use this because at certain points our tables might not be loaded,
+ * and querying them could throw.
+ *
+ * @param table String Name of the table that we want to consult
+ * @param key Principal being used to lookup the database
+ * @param callback nsIUrlListManagerCallback (ie., Function) given false or the
+ * value in the table corresponding to key. If the table name does not
+ * exist, we return false, too.
+ */
+PROT_ListManager.prototype.safeLookup = function(key, callback) {
+ try {
+ log("safeLookup: " + key);
+ var cb = new QueryAdapter(callback);
+ this.dbService_.lookup(key,
+ BindToObject(cb.handleResponse, cb),
+ true);
+ } catch(e) {
+ log("safeLookup masked failure for key " + key + ": " + e);
+ callback.handleEvent("");
+ }
+}
+
+/**
+ * Updates our internal tables from the update server
+ *
+ * @param updateUrl: request updates for tables associated with that url, or
+ * for all tables if the url is empty.
+ */
+PROT_ListManager.prototype.checkForUpdates = function(updateUrl) {
+ log("checkForUpdates with " + updateUrl);
+ // See if we've triggered the request backoff logic.
+ if (!updateUrl) {
+ return false;
+ }
+ if (!this.requestBackoffs_[updateUrl] ||
+ !this.requestBackoffs_[updateUrl].canMakeRequest()) {
+ log("Can't make update request");
+ return false;
+ }
+ // Grab the current state of the tables from the database
+ this.dbService_.getTables(BindToObject(this.makeUpdateRequest_, this,
+ updateUrl));
+ return true;
+}
+
+/**
+ * Method that fires the actual HTTP update request.
+ * First we reset any tables that have disappeared.
+ * @param tableData List of table data already in the database, in the form
+ * tablename;<chunk ranges>\n
+ */
+PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
+ log("this.tablesData: " + JSON.stringify(this.tablesData, undefined, 2));
+ log("existing chunks: " + tableData + "\n");
+ // Disallow blank updateUrls
+ if (!updateUrl) {
+ return;
+ }
+ // An object of the form
+ // { tableList: comma-separated list of tables to request,
+ // tableNames: map of tables that need updating,
+ // request: list of tables and existing chunk ranges from tableData
+ // }
+ var streamerMap = { tableList: null,
+ tableNames: {},
+ requestPayload: "",
+ isPostRequest: true };
+
+ let useProtobuf = false;
+ let onceThru = false;
+ for (var tableName in this.tablesData) {
+ // Skip tables not matching this update url
+ if (this.tablesData[tableName].updateUrl != updateUrl) {
+ continue;
+ }
+
+ // Check if |updateURL| is for 'proto'. (only v4 uses protobuf for now.)
+ // We use the table name 'goog-*-proto' and an additional provider "google4"
+ // to describe the v4 settings.
+ let isCurTableProto = tableName.endsWith('-proto');
+ if (!onceThru) {
+ useProtobuf = isCurTableProto;
+ onceThru = true;
+ } else if (useProtobuf !== isCurTableProto) {
+ log('ERROR: Cannot mix "proto" tables with other types ' +
+ 'within the same provider.');
+ }
+
+ if (this.needsUpdate_[this.tablesData[tableName].updateUrl][tableName]) {
+ streamerMap.tableNames[tableName] = true;
+ }
+ if (!streamerMap.tableList) {
+ streamerMap.tableList = tableName;
+ } else {
+ streamerMap.tableList += "," + tableName;
+ }
+ }
+
+ if (useProtobuf) {
+ let tableArray = [];
+ Object.keys(streamerMap.tableNames).forEach(aTableName => {
+ if (streamerMap.tableNames[aTableName]) {
+ tableArray.push(aTableName);
+ }
+ });
+
+ // Build the <tablename, stateBase64> mapping.
+ let tableState = {};
+ tableData.split("\n").forEach(line => {
+ let p = line.indexOf(";");
+ if (-1 === p) {
+ return;
+ }
+ let tableName = line.substring(0, p);
+ let metadata = line.substring(p + 1).split(":");
+ let stateBase64 = metadata[0];
+ log(tableName + " ==> " + stateBase64);
+ tableState[tableName] = stateBase64;
+ });
+
+ // The state is a byte stream which server told us from the
+ // last table update. The state would be used to do the partial
+ // update and the empty string means the table has
+ // never been downloaded. See Bug 1287058 for supporting
+ // partial update.
+ let stateArray = [];
+ tableArray.forEach(listName => {
+ stateArray.push(tableState[listName] || "");
+ });
+
+ log("stateArray: " + stateArray);
+
+ let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+
+ streamerMap.requestPayload = urlUtils.makeUpdateRequestV4(tableArray,
+ stateArray,
+ tableArray.length);
+ streamerMap.isPostRequest = false;
+ } else {
+ // Build the request. For each table already in the database, include the
+ // chunk data from the database
+ var lines = tableData.split("\n");
+ for (var i = 0; i < lines.length; i++) {
+ var fields = lines[i].split(";");
+ var name = fields[0];
+ if (streamerMap.tableNames[name]) {
+ streamerMap.requestPayload += lines[i] + "\n";
+ delete streamerMap.tableNames[name];
+ }
+ }
+ // For each requested table that didn't have chunk data in the database,
+ // request it fresh
+ for (let tableName in streamerMap.tableNames) {
+ streamerMap.requestPayload += tableName + ";\n";
+ }
+
+ streamerMap.isPostRequest = true;
+ }
+
+ log("update request: " + JSON.stringify(streamerMap, undefined, 2) + "\n");
+
+ // Don't send an empty request.
+ if (streamerMap.requestPayload.length > 0) {
+ this.makeUpdateRequestForEntry_(updateUrl, streamerMap.tableList,
+ streamerMap.requestPayload,
+ streamerMap.isPostRequest);
+ } else {
+ // We were disabled between kicking off getTables and now.
+ log("Not sending empty request");
+ }
+}
+
+PROT_ListManager.prototype.makeUpdateRequestForEntry_ = function(updateUrl,
+ tableList,
+ requestPayload,
+ isPostRequest) {
+ log("makeUpdateRequestForEntry_: requestPayload " + requestPayload +
+ " update: " + updateUrl + " tablelist: " + tableList + "\n");
+ var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"]
+ .getService(Ci.nsIUrlClassifierStreamUpdater);
+
+ this.requestBackoffs_[updateUrl].noteRequest();
+
+ if (!streamer.downloadUpdates(
+ tableList,
+ requestPayload,
+ isPostRequest,
+ updateUrl,
+ BindToObject(this.updateSuccess_, this, tableList, updateUrl),
+ BindToObject(this.updateError_, this, tableList, updateUrl),
+ BindToObject(this.downloadError_, this, tableList, updateUrl))) {
+ // Our alarm gets reset in one of the 3 callbacks.
+ log("pending update, queued request until later");
+ }
+}
+
+/**
+ * Callback function if the update request succeeded.
+ * @param waitForUpdate String The number of seconds that the client should
+ * wait before requesting again.
+ */
+PROT_ListManager.prototype.updateSuccess_ = function(tableList, updateUrl,
+ waitForUpdateSec) {
+ log("update success for " + tableList + " from " + updateUrl + ": " +
+ waitForUpdateSec + "\n");
+
+ // The time unit below are all milliseconds if not specified.
+
+ var delay = 0;
+ if (waitForUpdateSec) {
+ delay = parseInt(waitForUpdateSec, 10) * 1000;
+ }
+ // As long as the delay is something sane (5 min to 1 day), update
+ // our delay time for requesting updates. We always use a non-repeating
+ // timer since the delay is set differently at every callback.
+ if (delay > maxDelayMs) {
+ log("Ignoring delay from server (too long), waiting " +
+ maxDelayMs + "ms");
+ delay = maxDelayMs;
+ } else if (delay < minDelayMs) {
+ log("Ignoring delay from server (too short), waiting " +
+ this.updateInterval + "ms");
+ delay = this.updateInterval;
+ } else {
+ log("Waiting " + delay + "ms");
+ }
+ this.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ delay, false);
+
+ // Let the backoff object know that we completed successfully.
+ this.requestBackoffs_[updateUrl].noteServerResponse(200);
+
+ // Set last update time for provider
+ // Get the provider for these tables, check for consistency
+ let tables = tableList.split(",");
+ let provider = null;
+ for (let table of tables) {
+ let newProvider = this.tablesData[table].provider;
+ if (provider) {
+ if (newProvider !== provider) {
+ log("Multiple tables for the same updateURL have a different provider?!");
+ }
+ } else {
+ provider = newProvider;
+ }
+ }
+
+ // Store the last update time (needed to know if the table is "fresh")
+ // and the next update time (to know when to update next).
+ let lastUpdatePref = "browser.safebrowsing.provider." + provider + ".lastupdatetime";
+ let now = Date.now();
+ log("Setting last update of " + provider + " to " + now);
+ this.prefs_.setPref(lastUpdatePref, now.toString());
+
+ let nextUpdatePref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+ let targetTime = now + delay;
+ log("Setting next update of " + provider + " to " + targetTime
+ + " (" + delay + "ms from now)");
+ this.prefs_.setPref(nextUpdatePref, targetTime.toString());
+}
+
+/**
+ * Callback function if the update request succeeded.
+ * @param result String The error code of the failure
+ */
+PROT_ListManager.prototype.updateError_ = function(table, updateUrl, result) {
+ log("update error for " + table + " from " + updateUrl + ": " + result + "\n");
+ // There was some trouble applying the updates. Don't try again for at least
+ // updateInterval milliseconds.
+ this.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ this.updateInterval, false);
+}
+
+/**
+ * Callback function when the download failed
+ * @param status String http status or an empty string if connection refused.
+ */
+PROT_ListManager.prototype.downloadError_ = function(table, updateUrl, status) {
+ log("download error for " + table + ": " + status + "\n");
+ // If status is empty, then we assume that we got an NS_CONNECTION_REFUSED
+ // error. In this case, we treat this is a http 500 error.
+ if (!status) {
+ status = 500;
+ }
+ status = parseInt(status, 10);
+ this.requestBackoffs_[updateUrl].noteServerResponse(status);
+ var delay = this.updateInterval;
+ if (this.requestBackoffs_[updateUrl].isErrorStatus(status)) {
+ // Schedule an update for when our backoff is complete
+ delay = this.requestBackoffs_[updateUrl].nextRequestDelay();
+ } else {
+ log("Got non error status for error callback?!");
+ }
+ this.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ delay, false);
+
+}
+
+PROT_ListManager.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlListManager) ||
+ iid.equals(Ci.nsITimerCallback))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
diff --git a/toolkit/components/url-classifier/content/moz/alarm.js b/toolkit/components/url-classifier/content/moz/alarm.js
new file mode 100644
index 0000000000..7de0675461
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/alarm.js
@@ -0,0 +1,157 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+// An Alarm fires a callback after a certain amount of time, or at
+// regular intervals. It's a convenient replacement for
+// setTimeout/Interval when you don't want to bind to a specific
+// window.
+//
+// The ConditionalAlarm is an Alarm that cancels itself if its callback
+// returns a value that type-converts to true.
+//
+// Example:
+//
+// function foo() { dump('hi'); };
+// new G_Alarm(foo, 10*1000); // Fire foo in 10 seconds
+// new G_Alarm(foo, 10*1000, true /*repeat*/); // Fire foo every 10 seconds
+// new G_Alarm(foo, 10*1000, true, 7); // Fire foo every 10 seconds
+// // seven times
+// new G_ConditionalAlarm(foo, 1000, true); // Fire every sec until foo()==true
+//
+// // Fire foo every 10 seconds until foo returns true or until it fires seven
+// // times, whichever happens first.
+// new G_ConditionalAlarm(foo, 10*1000, true /*repeating*/, 7);
+//
+// TODO: maybe pass an isFinal flag to the callback if they opted to
+// set maxTimes and this is the last iteration?
+
+
+/**
+ * Set an alarm to fire after a given amount of time, or at specific
+ * intervals.
+ *
+ * @param callback Function to call when the alarm fires
+ * @param delayMS Number indicating the length of the alarm period in ms
+ * @param opt_repeating Boolean indicating whether this should fire
+ * periodically
+ * @param opt_maxTimes Number indicating a maximum number of times to
+ * repeat (obviously only useful when opt_repeating==true)
+ */
+this.G_Alarm =
+function G_Alarm(callback, delayMS, opt_repeating, opt_maxTimes) {
+ this.debugZone = "alarm";
+ this.callback_ = callback;
+ this.repeating_ = !!opt_repeating;
+ this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ var type = opt_repeating ?
+ this.timer_.TYPE_REPEATING_SLACK :
+ this.timer_.TYPE_ONE_SHOT;
+ this.maxTimes_ = opt_maxTimes ? opt_maxTimes : null;
+ this.nTimes_ = 0;
+
+ this.observerServiceObserver_ = new G_ObserverServiceObserver(
+ 'xpcom-shutdown',
+ BindToObject(this.cancel, this));
+
+ // Ask the timer to use nsITimerCallback (.notify()) when ready
+ this.timer_.initWithCallback(this, delayMS, type);
+}
+
+/**
+ * Cancel this timer
+ */
+G_Alarm.prototype.cancel = function() {
+ if (!this.timer_) {
+ return;
+ }
+
+ this.timer_.cancel();
+ // Break circular reference created between this.timer_ and the G_Alarm
+ // instance (this)
+ this.timer_ = null;
+ this.callback_ = null;
+
+ // We don't need the shutdown observer anymore
+ this.observerServiceObserver_.unregister();
+}
+
+/**
+ * Invoked by the timer when it fires
+ *
+ * @param timer Reference to the nsITimer which fired (not currently
+ * passed along)
+ */
+G_Alarm.prototype.notify = function(timer) {
+ // fire callback and save results
+ var ret = this.callback_();
+
+ // If they've given us a max number of times to fire, enforce it
+ this.nTimes_++;
+ if (this.repeating_ &&
+ typeof this.maxTimes_ == "number"
+ && this.nTimes_ >= this.maxTimes_) {
+ this.cancel();
+ } else if (!this.repeating_) {
+ // Clear out the callback closure for TYPE_ONE_SHOT timers
+ this.cancel();
+ }
+ // We don't cancel/cleanup timers that repeat forever until either
+ // xpcom-shutdown occurs or cancel() is called explicitly.
+
+ return ret;
+}
+
+G_Alarm.prototype.setDelay = function(delay) {
+ this.timer_.delay = delay;
+}
+
+/**
+ * XPCOM cruft
+ */
+G_Alarm.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Components.interfaces.nsISupports) ||
+ iid.equals(Components.interfaces.nsITimerCallback))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+
+/**
+ * An alarm with the additional property that it cancels itself if its
+ * callback returns true.
+ *
+ * For parameter documentation, see G_Alarm
+ */
+this.G_ConditionalAlarm =
+function G_ConditionalAlarm(callback, delayMS, opt_repeating, opt_maxTimes) {
+ G_Alarm.call(this, callback, delayMS, opt_repeating, opt_maxTimes);
+ this.debugZone = "conditionalalarm";
+}
+
+G_ConditionalAlarm.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+
+G_ConditionalAlarm.inherits(G_Alarm);
+
+/**
+ * Invoked by the timer when it fires
+ *
+ * @param timer Reference to the nsITimer which fired (not currently
+ * passed along)
+ */
+G_ConditionalAlarm.prototype.notify = function(timer) {
+ // Call G_Alarm::notify
+ var rv = G_Alarm.prototype.notify.call(this, timer);
+
+ if (this.repeating_ && rv) {
+ G_Debug(this, "Callback of a repeating alarm returned true; cancelling.");
+ this.cancel();
+ }
+}
diff --git a/toolkit/components/url-classifier/content/moz/cryptohasher.js b/toolkit/components/url-classifier/content/moz/cryptohasher.js
new file mode 100644
index 0000000000..a1294aa938
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/cryptohasher.js
@@ -0,0 +1,176 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+// A very thin wrapper around nsICryptoHash. It's not strictly
+// necessary, but makes the code a bit cleaner and gives us the
+// opportunity to verify that our implementations give the results that
+// we expect, for example if we have to interoperate with a server.
+//
+// The digest* methods reset the state of the hasher, so it's
+// necessary to call init() explicitly after them.
+//
+// Works only in Firefox 1.5+.
+//
+// IMPORTANT NOTE: Due to https://bugzilla.mozilla.org/show_bug.cgi?id=321024
+// you cannot use the cryptohasher before app-startup. The symptom of doing
+// so is a segfault in NSS.
+
+/**
+ * Instantiate a new hasher. You must explicitly call init() before use!
+ */
+this.G_CryptoHasher =
+function G_CryptoHasher() {
+ this.debugZone = "cryptohasher";
+ this.hasher_ = null;
+}
+
+G_CryptoHasher.algorithms = {
+ MD2: Ci.nsICryptoHash.MD2,
+ MD5: Ci.nsICryptoHash.MD5,
+ SHA1: Ci.nsICryptoHash.SHA1,
+ SHA256: Ci.nsICryptoHash.SHA256,
+ SHA384: Ci.nsICryptoHash.SHA384,
+ SHA512: Ci.nsICryptoHash.SHA512,
+};
+
+/**
+ * Initialize the hasher. This function must be called after every call
+ * to one of the digest* methods.
+ *
+ * @param algorithm Constant from G_CryptoHasher.algorithms specifying the
+ * algorithm this hasher will use
+ */
+G_CryptoHasher.prototype.init = function(algorithm) {
+ var validAlgorithm = false;
+ for (var alg in G_CryptoHasher.algorithms)
+ if (algorithm == G_CryptoHasher.algorithms[alg])
+ validAlgorithm = true;
+
+ if (!validAlgorithm)
+ throw new Error("Invalid algorithm: " + algorithm);
+
+ this.hasher_ = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ this.hasher_.init(algorithm);
+}
+
+/**
+ * Update the hash's internal state with input given in a string. Can be
+ * called multiple times for incrementeal hash updates.
+ *
+ * @param input String containing data to hash.
+ */
+G_CryptoHasher.prototype.updateFromString = function(input) {
+ if (!this.hasher_)
+ throw new Error("You must initialize the hasher first!");
+
+ var stream = Cc['@mozilla.org/io/string-input-stream;1']
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(input, input.length);
+ this.updateFromStream(stream);
+}
+
+/**
+ * Update the hash's internal state with input given in an array. Can be
+ * called multiple times for incremental hash updates.
+ *
+ * @param input Array containing data to hash.
+ */
+G_CryptoHasher.prototype.updateFromArray = function(input) {
+ if (!this.hasher_)
+ throw new Error("You must initialize the hasher first!");
+
+ this.hasher_.update(input, input.length);
+}
+
+/**
+ * Update the hash's internal state with input given in a stream. Can be
+ * called multiple times from incremental hash updates.
+ */
+G_CryptoHasher.prototype.updateFromStream = function(stream) {
+ if (!this.hasher_)
+ throw new Error("You must initialize the hasher first!");
+
+ if (stream.available())
+ this.hasher_.updateFromStream(stream, stream.available());
+}
+
+/**
+ * @returns The hash value as a string (sequence of 8-bit values)
+ */
+G_CryptoHasher.prototype.digestRaw = function() {
+ var digest = this.hasher_.finish(false /* not b64 encoded */);
+ this.hasher_ = null;
+ return digest;
+}
+
+/**
+ * @returns The hash value as a base64-encoded string
+ */
+G_CryptoHasher.prototype.digestBase64 = function() {
+ var digest = this.hasher_.finish(true /* b64 encoded */);
+ this.hasher_ = null;
+ return digest;
+}
+
+/**
+ * @returns The hash value as a hex-encoded string
+ */
+G_CryptoHasher.prototype.digestHex = function() {
+ var raw = this.digestRaw();
+ return this.toHex_(raw);
+}
+
+/**
+ * Converts a sequence of values to a hex-encoded string. The input is a
+ * a string, so you can stick 16-bit values in each character.
+ *
+ * @param str String to conver to hex. (Often this is just a sequence of
+ * 16-bit values)
+ *
+ * @returns String containing the hex representation of the input
+ */
+G_CryptoHasher.prototype.toHex_ = function(str) {
+ var hexchars = '0123456789ABCDEF';
+ var hexrep = new Array(str.length * 2);
+
+ for (var i = 0; i < str.length; ++i) {
+ hexrep[i * 2] = hexchars.charAt((str.charCodeAt(i) >> 4) & 15);
+ hexrep[i * 2 + 1] = hexchars.charAt(str.charCodeAt(i) & 15);
+ }
+ return hexrep.join('');
+}
+
+#ifdef DEBUG
+/**
+ * Lame unittest function
+ */
+this.TEST_G_CryptoHasher = function TEST_G_CryptoHasher() {
+ if (G_GDEBUG) {
+ var z = "cryptohasher UNITTEST";
+ G_debugService.enableZone(z);
+
+ G_Debug(z, "Starting");
+
+ var md5 = function(str) {
+ var hasher = new G_CryptoHasher();
+ hasher.init(G_CryptoHasher.algorithms.MD5);
+ hasher.updateFromString(str);
+ return hasher.digestHex().toLowerCase();
+ };
+
+ // test vectors from: http://www.faqs.org/rfcs/rfc1321.html
+ var vectors = {"": "d41d8cd98f00b204e9800998ecf8427e",
+ "a": "0cc175b9c0f1b6a831c399e269772661",
+ "abc": "900150983cd24fb0d6963f7d28e17f72",
+ "message digest": "f96b697d7cb7938d525a2f31aaf161d0",
+ "abcdefghijklmnopqrstuvwxyz": "c3fcd3d76192e4007dfb496cca67e13b",
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789": "d174ab98d277d9f5a5611c2c9f419d9f",
+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890": "57edf4a22be3c955ac49da2e2107b67a"};
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/debug.js b/toolkit/components/url-classifier/content/moz/debug.js
new file mode 100644
index 0000000000..ed4c117932
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/debug.js
@@ -0,0 +1,867 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 DEBUG
+
+// Generic logging/debugging functionality that:
+//
+// (*) when disabled compiles to no-ops at worst (for calls to the service)
+// and to nothing at best (calls to G_Debug() and similar are compiled
+// away when you use a jscompiler that strips dead code)
+//
+// (*) has dynamically configurable/creatable debugging "zones" enabling
+// selective logging
+//
+// (*) hides its plumbing so that all calls in different zones are uniform,
+// so you can drop files using this library into other apps that use it
+// without any configuration
+//
+// (*) can be controlled programmatically or via preferences. The
+// preferences that control the service and its zones are under
+// the preference branch "safebrowsing-debug-service."
+//
+// (*) outputs function call traces when the "loggifier" zone is enabled
+//
+// (*) can write output to logfiles so that you can get a call trace
+// from someone who is having a problem
+//
+// Example:
+//
+// var G_GDEBUG = true // Enable this module
+// var G_debugService = new G_DebugService(); // in global context
+//
+// // You can use it with arbitrary primitive first arguement
+// G_Debug("myzone", "Yo yo yo"); // outputs: [myzone] Yo yo yo\n
+//
+// // But it's nice to use it with an object; it will probe for the zone name
+// function Obj() {
+// this.debugZone = "someobj";
+// }
+// Obj.prototype.foo = function() {
+// G_Debug(this, "foo called");
+// }
+// (new Obj).foo(); // outputs: [someobj] foo called\n
+//
+// G_debugService.loggifier.loggify(Obj.prototype); // enable call tracing
+//
+// // En/disable specific zones programmatically (you can also use preferences)
+// G_debugService.enableZone("somezone");
+// G_debugService.disableZone("someotherzone");
+// G_debugService.enableAllZones();
+//
+// // We also have asserts and errors:
+// G_Error(this, "Some error occurred"); // will throw
+// G_Assert(this, (x > 3), "x not greater than three!"); // will throw
+//
+// See classes below for more methods.
+//
+// TODO add code to set prefs when not found to the default value of a tristate
+// TODO add error level support
+// TODO add ability to turn off console output
+//
+// -------> TO START DEBUGGING: set G_GDEBUG to true
+
+// These are the functions code will typically call. Everything is
+// wrapped in if's so we can compile it away when G_GDEBUG is false.
+
+
+if (typeof G_GDEBUG == "undefined") {
+ throw new Error("G_GDEBUG constant must be set before loading debug.js");
+}
+
+
+/**
+ * Write out a debugging message.
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param msg Message to output
+ */
+this.G_Debug = function G_Debug(who, msg) {
+ if (G_GDEBUG) {
+ G_GetDebugZone(who).debug(msg);
+ }
+}
+
+/**
+ * Debugs loudly
+ */
+this.G_DebugL = function G_DebugL(who, msg) {
+ if (G_GDEBUG) {
+ var zone = G_GetDebugZone(who);
+
+ if (zone.zoneIsEnabled()) {
+ G_debugService.dump(
+ "\n************************************************************\n");
+
+ G_Debug(who, msg);
+
+ G_debugService.dump(
+ "************************************************************\n\n");
+ }
+ }
+}
+
+/**
+ * Write out a call tracing message
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param msg Message to output
+ */
+this.G_TraceCall = function G_TraceCall(who, msg) {
+ if (G_GDEBUG) {
+ if (G_debugService.callTracingEnabled()) {
+ G_debugService.dump(msg + "\n");
+ }
+ }
+}
+
+/**
+ * Write out an error (and throw)
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param msg Message to output
+ */
+this.G_Error = function G_Error(who, msg) {
+ if (G_GDEBUG) {
+ G_GetDebugZone(who).error(msg);
+ }
+}
+
+/**
+ * Assert something as true and signal an error if it's not
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param condition Boolean condition to test
+ * @param msg Message to output
+ */
+this.G_Assert = function G_Assert(who, condition, msg) {
+ if (G_GDEBUG) {
+ G_GetDebugZone(who).assert(condition, msg);
+ }
+}
+
+/**
+ * Helper function that takes input and returns the DebugZone
+ * corresponding to it.
+ *
+ * @param who Arbitrary input that will be converted into a zone name. Most
+ * likely an object that has .debugZone property, or a string.
+ * @returns The DebugZone object corresponding to the input
+ */
+this.G_GetDebugZone = function G_GetDebugZone(who) {
+ if (G_GDEBUG) {
+ var zone = "?";
+
+ if (who && who.debugZone) {
+ zone = who.debugZone;
+ } else if (typeof who == "string") {
+ zone = who;
+ }
+
+ return G_debugService.getZone(zone);
+ }
+}
+
+// Classes that implement the functionality.
+
+/**
+ * A debug "zone" is a string derived from arbitrary types (but
+ * typically derived from another string or an object). All debugging
+ * messages using a particular zone can be enabled or disabled
+ * independent of other zones. This enables you to turn on/off logging
+ * of particular objects or modules. This object implements a single
+ * zone and the methods required to use it.
+ *
+ * @constructor
+ * @param service Reference to the DebugService object we use for
+ * registration
+ * @param prefix String indicating the unique prefix we should use
+ * when creating preferences to control this zone
+ * @param zone String indicating the name of the zone
+ */
+this.G_DebugZone = function G_DebugZone(service, prefix, zone) {
+ if (G_GDEBUG) {
+ this.debugService_ = service;
+ this.prefix_ = prefix;
+ this.zone_ = zone;
+ this.zoneEnabledPrefName_ = prefix + ".zone." + this.zone_;
+ this.settings_ = new G_DebugSettings();
+ }
+}
+
+/**
+ * @returns Boolean indicating if this zone is enabled
+ */
+G_DebugZone.prototype.zoneIsEnabled = function() {
+ if (G_GDEBUG) {
+ var explicit = this.settings_.getSetting(this.zoneEnabledPrefName_, null);
+
+ if (explicit !== null) {
+ return explicit;
+ } else {
+ return this.debugService_.allZonesEnabled();
+ }
+ }
+}
+
+/**
+ * Enable this logging zone
+ */
+G_DebugZone.prototype.enableZone = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.zoneEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disable this logging zone
+ */
+G_DebugZone.prototype.disableZone = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.zoneEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Write a debugging message to this zone
+ *
+ * @param msg String of message to write
+ */
+G_DebugZone.prototype.debug = function(msg) {
+ if (G_GDEBUG) {
+ if (this.zoneIsEnabled()) {
+ this.debugService_.dump("[" + this.zone_ + "] " + msg + "\n");
+ }
+ }
+}
+
+/**
+ * Write an error to this zone and throw
+ *
+ * @param msg String of error to write
+ */
+G_DebugZone.prototype.error = function(msg) {
+ if (G_GDEBUG) {
+ this.debugService_.dump("[" + this.zone_ + "] " + msg + "\n");
+ throw new Error(msg);
+ debugger;
+ }
+}
+
+/**
+ * Assert something as true and error if it is not
+ *
+ * @param condition Boolean condition to test
+ * @param msg String of message to write if is false
+ */
+G_DebugZone.prototype.assert = function(condition, msg) {
+ if (G_GDEBUG) {
+ if (condition !== true) {
+ G_Error(this.zone_, "ASSERT FAILED: " + msg);
+ }
+ }
+}
+
+
+/**
+ * The debug service handles auto-registration of zones, namespacing
+ * the zones preferences, and various global settings such as whether
+ * all zones are enabled.
+ *
+ * @constructor
+ * @param opt_prefix Optional string indicating the unique prefix we should
+ * use when creating preferences
+ */
+this.G_DebugService = function G_DebugService(opt_prefix) {
+ if (G_GDEBUG) {
+ this.prefix_ = opt_prefix ? opt_prefix : "safebrowsing-debug-service";
+ this.consoleEnabledPrefName_ = this.prefix_ + ".alsologtoconsole";
+ this.allZonesEnabledPrefName_ = this.prefix_ + ".enableallzones";
+ this.callTracingEnabledPrefName_ = this.prefix_ + ".trace-function-calls";
+ this.logFileEnabledPrefName_ = this.prefix_ + ".logfileenabled";
+ this.logFileErrorLevelPrefName_ = this.prefix_ + ".logfile-errorlevel";
+ this.zones_ = {};
+
+ this.loggifier = new G_Loggifier();
+ this.settings_ = new G_DebugSettings();
+ }
+}
+
+// Error levels for reporting console messages to the log.
+G_DebugService.ERROR_LEVEL_INFO = "INFO";
+G_DebugService.ERROR_LEVEL_WARNING = "WARNING";
+G_DebugService.ERROR_LEVEL_EXCEPTION = "EXCEPTION";
+
+
+/**
+ * @returns Boolean indicating if we should send messages to the jsconsole
+ */
+G_DebugService.prototype.alsoDumpToConsole = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.consoleEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @returns whether to log output to a file as well as the console.
+ */
+G_DebugService.prototype.logFileIsEnabled = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.logFileEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Turns on file logging. dump() output will also go to the file specified by
+ * setLogFile()
+ */
+G_DebugService.prototype.enableLogFile = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.logFileEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Turns off file logging
+ */
+G_DebugService.prototype.disableLogFile = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.logFileEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @returns an nsIFile instance pointing to the current log file location
+ */
+G_DebugService.prototype.getLogFile = function() {
+ if (G_GDEBUG) {
+ return this.logFile_;
+ }
+}
+
+/**
+ * Sets a new log file location
+ */
+G_DebugService.prototype.setLogFile = function(file) {
+ if (G_GDEBUG) {
+ this.logFile_ = file;
+ }
+}
+
+/**
+ * Enables sending messages to the jsconsole
+ */
+G_DebugService.prototype.enableDumpToConsole = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.consoleEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disables sending messages to the jsconsole
+ */
+G_DebugService.prototype.disableDumpToConsole = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.consoleEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @param zone Name of the zone to get
+ * @returns The DebugZone object corresopnding to input. If not such
+ * zone exists, a new one is created and returned
+ */
+G_DebugService.prototype.getZone = function(zone) {
+ if (G_GDEBUG) {
+ if (!this.zones_[zone])
+ this.zones_[zone] = new G_DebugZone(this, this.prefix_, zone);
+
+ return this.zones_[zone];
+ }
+}
+
+/**
+ * @param zone Zone to enable debugging for
+ */
+G_DebugService.prototype.enableZone = function(zone) {
+ if (G_GDEBUG) {
+ var toEnable = this.getZone(zone);
+ toEnable.enableZone();
+ }
+}
+
+/**
+ * @param zone Zone to disable debugging for
+ */
+G_DebugService.prototype.disableZone = function(zone) {
+ if (G_GDEBUG) {
+ var toDisable = this.getZone(zone);
+ toDisable.disableZone();
+ }
+}
+
+/**
+ * @returns Boolean indicating whether debugging is enabled for all zones
+ */
+G_DebugService.prototype.allZonesEnabled = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.allZonesEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Enables all debugging zones
+ */
+G_DebugService.prototype.enableAllZones = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.allZonesEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disables all debugging zones
+ */
+G_DebugService.prototype.disableAllZones = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.allZonesEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @returns Boolean indicating whether call tracing is enabled
+ */
+G_DebugService.prototype.callTracingEnabled = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.callTracingEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Enables call tracing
+ */
+G_DebugService.prototype.enableCallTracing = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.callTracingEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disables call tracing
+ */
+G_DebugService.prototype.disableCallTracing = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.callTracingEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Gets the minimum error that will be reported to the log.
+ */
+G_DebugService.prototype.getLogFileErrorLevel = function() {
+ if (G_GDEBUG) {
+ var level = this.settings_.getSetting(this.logFileErrorLevelPrefName_,
+ G_DebugService.ERROR_LEVEL_EXCEPTION);
+
+ return level.toUpperCase();
+ }
+}
+
+/**
+ * Sets the minimum error level that will be reported to the log.
+ */
+G_DebugService.prototype.setLogFileErrorLevel = function(level) {
+ if (G_GDEBUG) {
+ // normalize case just to make it slightly easier to not screw up.
+ level = level.toUpperCase();
+
+ if (level != G_DebugService.ERROR_LEVEL_INFO &&
+ level != G_DebugService.ERROR_LEVEL_WARNING &&
+ level != G_DebugService.ERROR_LEVEL_EXCEPTION) {
+ throw new Error("Invalid error level specified: {" + level + "}");
+ }
+
+ this.settings_.setDefault(this.logFileErrorLevelPrefName_, level);
+ }
+}
+
+/**
+ * Internal dump() method
+ *
+ * @param msg String of message to dump
+ */
+G_DebugService.prototype.dump = function(msg) {
+ if (G_GDEBUG) {
+ dump(msg);
+
+ if (this.alsoDumpToConsole()) {
+ try {
+ var console = Components.classes['@mozilla.org/consoleservice;1']
+ .getService(Components.interfaces.nsIConsoleService);
+ console.logStringMessage(msg);
+ } catch(e) {
+ dump("G_DebugZone ERROR: COULD NOT DUMP TO CONSOLE\n");
+ }
+ }
+
+ this.maybeDumpToFile(msg);
+ }
+}
+
+/**
+ * Writes the specified message to the log file, if file logging is enabled.
+ */
+G_DebugService.prototype.maybeDumpToFile = function(msg) {
+ if (this.logFileIsEnabled() && this.logFile_) {
+
+ /* try to get the correct line end character for this platform */
+ if (!this._LINE_END_CHAR)
+ this._LINE_END_CHAR =
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .OS == "WINNT" ? "\r\n" : "\n";
+ if (this._LINE_END_CHAR != "\n")
+ msg = msg.replace(/\n/g, this._LINE_END_CHAR);
+
+ try {
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ stream.init(this.logFile_,
+ 0x02 | 0x08 | 0x10 /* PR_WRONLY | PR_CREATE_FILE | PR_APPEND */
+ -1 /* default perms */, 0 /* no special behavior */);
+ stream.write(msg, msg.length);
+ } finally {
+ stream.close();
+ }
+ }
+}
+
+/**
+ * Implements nsIConsoleListener.observe(). Gets called when an error message
+ * gets reported to the console and sends it to the log file as well.
+ */
+G_DebugService.prototype.observe = function(consoleMessage) {
+ if (G_GDEBUG) {
+ var errorLevel = this.getLogFileErrorLevel();
+
+ // consoleMessage can be either nsIScriptError or nsIConsoleMessage. The
+ // latter does not have things like line number, etc. So we special case
+ // it first.
+ if (!(consoleMessage instanceof Ci.nsIScriptError)) {
+ // Only report these messages if the error level is INFO.
+ if (errorLevel == G_DebugService.ERROR_LEVEL_INFO) {
+ this.maybeDumpToFile(G_DebugService.ERROR_LEVEL_INFO + ": " +
+ consoleMessage.message + "\n");
+ }
+
+ return;
+ }
+
+ // We make a local copy of these fields because writing to it doesn't seem
+ // to work.
+ var flags = consoleMessage.flags;
+ var sourceName = consoleMessage.sourceName;
+ var lineNumber = consoleMessage.lineNumber;
+
+ // Sometimes, a scripterror instance won't have any flags set. We
+ // default to exception.
+ if (!flags) {
+ flags = Ci.nsIScriptError.exceptionFlag;
+ }
+
+ // Default the filename and line number if they aren't set.
+ if (!sourceName) {
+ sourceName = "<unknown>";
+ }
+
+ if (!lineNumber) {
+ lineNumber = "<unknown>";
+ }
+
+ // Report the error in the log file.
+ if (flags & Ci.nsIScriptError.warningFlag) {
+ // Only report warnings if the error level is warning or better.
+ if (errorLevel == G_DebugService.ERROR_LEVEL_WARNING ||
+ errorLevel == G_DebugService.ERROR_LEVEL_INFO) {
+ this.reportScriptError_(consoleMessage.message,
+ sourceName,
+ lineNumber,
+ G_DebugService.ERROR_LEVEL_WARNING);
+ }
+ } else if (flags & Ci.nsIScriptError.exceptionFlag) {
+ // Always report exceptions.
+ this.reportScriptError_(consoleMessage.message,
+ sourceName,
+ lineNumber,
+ G_DebugService.ERROR_LEVEL_EXCEPTION);
+ }
+ }
+}
+
+/**
+ * Private helper to report an nsIScriptError instance to the log/console.
+ */
+G_DebugService.prototype.reportScriptError_ = function(message, sourceName,
+ lineNumber, label) {
+ message = "\n------------------------------------------------------------\n" +
+ label + ": " + message +
+ "\nlocation: " + sourceName + ", " + "line: " + lineNumber +
+ "\n------------------------------------------------------------\n\n";
+
+ dump(message);
+ this.maybeDumpToFile(message);
+}
+
+
+
+/**
+ * A class that instruments methods so they output a call trace,
+ * including the values of their actual parameters and return value.
+ * This code is mostly stolen from Aaron Boodman's original
+ * implementation in clobber utils.
+ *
+ * Note that this class uses the "loggifier" debug zone, so you'll see
+ * a complete call trace when that zone is enabled.
+ *
+ * @constructor
+ */
+this.G_Loggifier = function G_Loggifier() {
+ if (G_GDEBUG) {
+ // Careful not to loggify ourselves!
+ this.mark_(this);
+ }
+}
+
+/**
+ * Marks an object as having been loggified. Loggification is not
+ * idempotent :)
+ *
+ * @param obj Object to be marked
+ */
+G_Loggifier.prototype.mark_ = function(obj) {
+ if (G_GDEBUG) {
+ obj.__loggified_ = true;
+ }
+}
+
+/**
+ * @param obj Object to be examined
+ * @returns Boolean indicating if the object has been loggified
+ */
+G_Loggifier.prototype.isLoggified = function(obj) {
+ if (G_GDEBUG) {
+ return !!obj.__loggified_;
+ }
+}
+
+/**
+ * Attempt to extract the class name from the constructor definition.
+ * Assumes the object was created using new.
+ *
+ * @param constructor String containing the definition of a constructor,
+ * for example what you'd get by examining obj.constructor
+ * @returns Name of the constructor/object if it could be found, else "???"
+ */
+G_Loggifier.prototype.getFunctionName_ = function(constructor) {
+ if (G_GDEBUG) {
+ return constructor.name || "???";
+ }
+}
+
+/**
+ * Wraps all the methods in an object so that call traces are
+ * automatically outputted.
+ *
+ * @param obj Object to loggify. SHOULD BE THE PROTOTYPE OF A USER-DEFINED
+ * object. You can get into trouble if you attempt to
+ * loggify something that isn't, for example the Window.
+ *
+ * Any additional parameters are considered method names which should not be
+ * loggified.
+ *
+ * Usage:
+ * G_debugService.loggifier.loggify(MyClass.prototype,
+ * "firstMethodNotToLog",
+ * "secondMethodNotToLog",
+ * ... etc ...);
+ */
+G_Loggifier.prototype.loggify = function(obj) {
+ if (G_GDEBUG) {
+ if (!G_debugService.callTracingEnabled()) {
+ return;
+ }
+
+ if (typeof window != "undefined" && obj == window ||
+ this.isLoggified(obj)) // Don't go berserk!
+ return;
+
+ var zone = G_GetDebugZone(obj);
+ if (!zone || !zone.zoneIsEnabled()) {
+ return;
+ }
+
+ this.mark_(obj);
+
+ // Helper function returns an instrumented version of
+ // objName.meth, with "this" bound properly. (BTW, because we're
+ // in a conditional here, functions will only be defined as
+ // they're encountered during execution, so declare this helper
+ // before using it.)
+
+ let wrap = function (meth, objName, methName) {
+ return function() {
+
+ // First output the call along with actual parameters
+ var args = new Array(arguments.length);
+ var argsString = "";
+ for (var i = 0; i < args.length; i++) {
+ args[i] = arguments[i];
+ argsString += (i == 0 ? "" : ", ");
+
+ if (typeof args[i] == "function") {
+ argsString += "[function]";
+ } else {
+ argsString += args[i];
+ }
+ }
+
+ G_TraceCall(this, "> " + objName + "." + methName + "(" +
+ argsString + ")");
+
+ // Then run the function, capturing the return value and throws
+ try {
+ var retVal = meth.apply(this, arguments);
+ var reportedRetVal = retVal;
+
+ if (typeof reportedRetVal == "undefined")
+ reportedRetVal = "void";
+ else if (reportedRetVal === "")
+ reportedRetVal = "\"\" (empty string)";
+ } catch (e) {
+ if (e && !e.__logged) {
+ G_TraceCall(this, "Error: " + e.message + ". " +
+ e.fileName + ": " + e.lineNumber);
+ try {
+ e.__logged = true;
+ } catch (e2) {
+ // Sometimes we can't add the __logged flag because it's an
+ // XPC wrapper
+ throw e;
+ }
+ }
+
+ throw e; // Re-throw!
+ }
+
+ // And spit it out already
+ G_TraceCall(
+ this,
+ "< " + objName + "." + methName + ": " + reportedRetVal);
+
+ return retVal;
+ };
+ };
+
+ var ignoreLookup = {};
+
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ ignoreLookup[arguments[i]] = true;
+ }
+ }
+
+ // Wrap each method of obj
+ for (var p in obj) {
+ // Work around bug in Firefox. In ffox typeof RegExp is "function",
+ // so make sure this really is a function. Bug as of FFox 1.5b2.
+ if (typeof obj[p] == "function" && obj[p].call && !ignoreLookup[p]) {
+ var objName = this.getFunctionName_(obj.constructor);
+ obj[p] = wrap(obj[p], objName, p);
+ }
+ }
+ }
+}
+
+
+/**
+ * Simple abstraction around debug settings. The thing with debug settings is
+ * that we want to be able to specify a default in the application's startup,
+ * but have that default be overridable by the user via their prefs.
+ *
+ * To generalize this, we package up a dictionary of defaults with the
+ * preferences tree. If a setting isn't in the preferences tree, then we grab it
+ * from the defaults.
+ */
+this.G_DebugSettings = function G_DebugSettings() {
+ this.defaults_ = {};
+ this.prefs_ = new G_Preferences();
+}
+
+/**
+ * Returns the value of a settings, optionally defaulting to a given value if it
+ * doesn't exist. If no default is specified, the default is |undefined|.
+ */
+G_DebugSettings.prototype.getSetting = function(name, opt_default) {
+ var override = this.prefs_.getPref(name, null);
+
+ if (override !== null) {
+ return override;
+ } else if (typeof this.defaults_[name] != "undefined") {
+ return this.defaults_[name];
+ } else {
+ return opt_default;
+ }
+}
+
+/**
+ * Sets the default value for a setting. If the user doesn't override it with a
+ * preference, this is the value which will be returned by getSetting().
+ */
+G_DebugSettings.prototype.setDefault = function(name, val) {
+ this.defaults_[name] = val;
+}
+
+var G_debugService = new G_DebugService(); // Instantiate us!
+
+if (G_GDEBUG) {
+ G_debugService.enableAllZones();
+}
+
+#else
+
+// Stubs for the debugging aids scattered through this component.
+// They will be expanded if you compile yourself a debug build.
+
+this.G_Debug = function G_Debug(who, msg) { }
+this.G_Assert = function G_Assert(who, condition, msg) { }
+this.G_Error = function G_Error(who, msg) { }
+this.G_debugService = {
+ alsoDumpToConsole: () => {},
+ logFileIsEnabled: () => {},
+ enableLogFile: () => {},
+ disableLogFile: () => {},
+ getLogFile: () => {},
+ setLogFile: () => {},
+ enableDumpToConsole: () => {},
+ disableDumpToConsole: () => {},
+ getZone: () => {},
+ enableZone: () => {},
+ disableZone: () => {},
+ allZonesEnabled: () => {},
+ enableAllZones: () => {},
+ disableAllZones: () => {},
+ callTracingEnabled: () => {},
+ enableCallTracing: () => {},
+ disableCallTracing: () => {},
+ getLogFileErrorLevel: () => {},
+ setLogFileErrorLevel: () => {},
+ dump: () => {},
+ maybeDumpToFile: () => {},
+ observe: () => {},
+ reportScriptError_: () => {}
+};
+
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/lang.js b/toolkit/components/url-classifier/content/moz/lang.js
new file mode 100644
index 0000000000..804a6e973e
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/lang.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/.
+
+
+/**
+ * lang.js - Some missing JavaScript language features
+ */
+
+/**
+ * Partially applies a function to a particular "this object" and zero or
+ * more arguments. The result is a new function with some arguments of the first
+ * function pre-filled and the value of |this| "pre-specified".
+ *
+ * Remaining arguments specified at call-time are appended to the pre-
+ * specified ones.
+ *
+ * Usage:
+ * var barMethBound = BindToObject(myFunction, myObj, "arg1", "arg2");
+ * barMethBound("arg3", "arg4");
+ *
+ * @param fn {string} Reference to the function to be bound
+ *
+ * @param self {object} Specifies the object which |this| should point to
+ * when the function is run. If the value is null or undefined, it will default
+ * to the global object.
+ *
+ * @returns {function} A partially-applied form of the speficied function.
+ */
+this.BindToObject = function BindToObject(fn, self, opt_args) {
+ var boundargs = fn.boundArgs_ || [];
+ boundargs = boundargs.concat(Array.slice(arguments, 2, arguments.length));
+
+ if (fn.boundSelf_)
+ self = fn.boundSelf_;
+ if (fn.boundFn_)
+ fn = fn.boundFn_;
+
+ var newfn = function() {
+ // Combine the static args and the new args into one big array
+ var args = boundargs.concat(Array.slice(arguments));
+ return fn.apply(self, args);
+ }
+
+ newfn.boundArgs_ = boundargs;
+ newfn.boundSelf_ = self;
+ newfn.boundFn_ = fn;
+
+ return newfn;
+}
+
+/**
+ * Inherit the prototype methods from one constructor into another.
+ *
+ * Usage:
+ *
+ * function ParentClass(a, b) { }
+ * ParentClass.prototype.foo = function(a) { }
+ *
+ * function ChildClass(a, b, c) {
+ * ParentClass.call(this, a, b);
+ * }
+ *
+ * ChildClass.inherits(ParentClass);
+ *
+ * var child = new ChildClass("a", "b", "see");
+ * child.foo(); // works
+ *
+ * In addition, a superclass' implementation of a method can be invoked
+ * as follows:
+ *
+ * ChildClass.prototype.foo = function(a) {
+ * ChildClass.superClass_.foo.call(this, a);
+ * // other code
+ * };
+ */
+Function.prototype.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
diff --git a/toolkit/components/url-classifier/content/moz/observer.js b/toolkit/components/url-classifier/content/moz/observer.js
new file mode 100644
index 0000000000..a9d22ee217
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/observer.js
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+// A couple of classes to simplify creating observers.
+//
+// Example1:
+//
+// function doSomething() { ... }
+// var observer = new G_ObserverWrapper(topic, doSomething);
+// someObj.addObserver(topic, observer);
+//
+// Example2:
+//
+// function doSomething() { ... }
+// new G_ObserverServiceObserver("profile-after-change",
+// doSomething,
+// true /* run only once */);
+
+
+/**
+ * This class abstracts the admittedly simple boilerplate required of
+ * an nsIObserver. It saves you the trouble of implementing the
+ * indirection of your own observe() function.
+ *
+ * @param topic String containing the topic the observer will filter for
+ *
+ * @param observeFunction Reference to the function to call when the
+ * observer fires
+ *
+ * @constructor
+ */
+this.G_ObserverWrapper = function G_ObserverWrapper(topic, observeFunction) {
+ this.debugZone = "observer";
+ this.topic_ = topic;
+ this.observeFunction_ = observeFunction;
+}
+
+/**
+ * XPCOM
+ */
+G_ObserverWrapper.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIObserver))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+/**
+ * Invoked by the thingy being observed
+ */
+G_ObserverWrapper.prototype.observe = function(subject, topic, data) {
+ if (topic == this.topic_)
+ this.observeFunction_(subject, topic, data);
+}
+
+
+/**
+ * This class abstracts the admittedly simple boilerplate required of
+ * observing an observerservice topic. It implements the indirection
+ * required, and automatically registers to hear the topic.
+ *
+ * @param topic String containing the topic the observer will filter for
+ *
+ * @param observeFunction Reference to the function to call when the
+ * observer fires
+ *
+ * @param opt_onlyOnce Boolean indicating if the observer should unregister
+ * after it has fired
+ *
+ * @constructor
+ */
+this.G_ObserverServiceObserver =
+function G_ObserverServiceObserver(topic, observeFunction, opt_onlyOnce) {
+ this.debugZone = "observerserviceobserver";
+ this.topic_ = topic;
+ this.observeFunction_ = observeFunction;
+ this.onlyOnce_ = !!opt_onlyOnce;
+
+ this.observer_ = new G_ObserverWrapper(this.topic_,
+ BindToObject(this.observe_, this));
+ this.observerService_ = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ this.observerService_.addObserver(this.observer_, this.topic_, false);
+}
+
+/**
+ * Unregister the observer from the observerservice
+ */
+G_ObserverServiceObserver.prototype.unregister = function() {
+ this.observerService_.removeObserver(this.observer_, this.topic_);
+ this.observerService_ = null;
+}
+
+/**
+ * Invoked by the observerservice
+ */
+G_ObserverServiceObserver.prototype.observe_ = function(subject, topic, data) {
+ this.observeFunction_(subject, topic, data);
+ if (this.onlyOnce_)
+ this.unregister();
+}
+
+#ifdef DEBUG
+this.TEST_G_Observer = function TEST_G_Observer() {
+ if (G_GDEBUG) {
+
+ var z = "observer UNITTEST";
+ G_debugService.enableZone(z);
+
+ G_Debug(z, "Starting");
+
+ var regularObserverRan = 0;
+ var observerServiceObserverRan = 0;
+
+ let regularObserver = function () {
+ regularObserverRan++;
+ };
+
+ let observerServiceObserver = function () {
+ observerServiceObserverRan++;
+ };
+
+ var service = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ var topic = "google-observer-test";
+
+ var o1 = new G_ObserverWrapper(topic, regularObserver);
+ service.addObserver(o1, topic, false);
+
+ new G_ObserverServiceObserver(topic,
+ observerServiceObserver, true /* once */);
+
+ // Notifications happen synchronously, so this is easy
+ service.notifyObservers(null, topic, null);
+ service.notifyObservers(null, topic, null);
+
+ G_Assert(z, regularObserverRan == 2, "Regular observer broken");
+ G_Assert(z, observerServiceObserverRan == 1, "ObsServObs broken");
+
+ service.removeObserver(o1, topic);
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/preferences.js b/toolkit/components/url-classifier/content/moz/preferences.js
new file mode 100644
index 0000000000..30105ab344
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/preferences.js
@@ -0,0 +1,276 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+// Class for manipulating preferences. Aside from wrapping the pref
+// service, useful functionality includes:
+//
+// - abstracting prefobserving so that you can observe preferences
+// without implementing nsIObserver
+//
+// - getters that return a default value when the pref doesn't exist
+// (instead of throwing)
+//
+// - get-and-set getters
+//
+// Example:
+//
+// var p = new PROT_Preferences();
+// dump(p.getPref("some-true-pref")); // shows true
+// dump(p.getPref("no-such-pref", true)); // shows true
+// dump(p.getPref("no-such-pref", null)); // shows null
+//
+// function observe(prefThatChanged) {
+// dump("Pref changed: " + prefThatChanged);
+// };
+//
+// p.addObserver("somepref", observe);
+// p.setPref("somepref", true); // dumps
+// p.removeObserver("somepref", observe);
+//
+// TODO: should probably have the prefobserver pass in the new and old
+// values
+
+// TODO(tc): Maybe remove this class and just call natively since we're no
+// longer an extension.
+
+/**
+ * A class that wraps the preferences service.
+ *
+ * @param opt_startPoint A starting point on the prefs tree to resolve
+ * names passed to setPref and getPref.
+ *
+ * @param opt_useDefaultPranch Set to true to work against the default
+ * preferences tree instead of the profile one.
+ *
+ * @constructor
+ */
+this.G_Preferences =
+function G_Preferences(opt_startPoint, opt_getDefaultBranch) {
+ this.debugZone = "prefs";
+ this.observers_ = {};
+ this.getDefaultBranch_ = !!opt_getDefaultBranch;
+
+ this.startPoint_ = opt_startPoint || null;
+}
+
+G_Preferences.setterMap_ = { "string": "setCharPref",
+ "boolean": "setBoolPref",
+ "number": "setIntPref" };
+
+G_Preferences.getterMap_ = {};
+G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_STRING] = "getCharPref";
+G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_BOOL] = "getBoolPref";
+G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_INT] = "getIntPref";
+
+G_Preferences.prototype.__defineGetter__('prefs_', function() {
+ var prefs;
+ var prefSvc = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+
+ if (this.getDefaultBranch_) {
+ prefs = prefSvc.getDefaultBranch(this.startPoint_);
+ } else {
+ prefs = prefSvc.getBranch(this.startPoint_);
+ }
+
+ // QI to prefs in case we want to add observers
+ prefs.QueryInterface(Ci.nsIPrefBranchInternal);
+ return prefs;
+});
+
+/**
+ * Stores a key/value in a user preference. Valid types for val are string,
+ * boolean, and number. Complex values are not yet supported (but feel free to
+ * add them!).
+ */
+G_Preferences.prototype.setPref = function(key, val) {
+ var datatype = typeof(val);
+
+ if (datatype == "number" && (val % 1 != 0)) {
+ throw new Error("Cannot store non-integer numbers in preferences.");
+ }
+
+ var meth = G_Preferences.setterMap_[datatype];
+
+ if (!meth) {
+ throw new Error("Pref datatype {" + datatype + "} not supported.");
+ }
+
+ return this.prefs_[meth](key, val);
+}
+
+/**
+ * Retrieves a user preference. Valid types for the value are the same as for
+ * setPref. If the preference is not found, opt_default will be returned
+ * instead.
+ */
+G_Preferences.prototype.getPref = function(key, opt_default) {
+ var type = this.prefs_.getPrefType(key);
+
+ // zero means that the specified pref didn't exist
+ if (type == Ci.nsIPrefBranch.PREF_INVALID) {
+ return opt_default;
+ }
+
+ var meth = G_Preferences.getterMap_[type];
+
+ if (!meth) {
+ throw new Error("Pref datatype {" + type + "} not supported.");
+ }
+
+ // If a pref has been cleared, it will have a valid type but won't
+ // be gettable, so this will throw.
+ try {
+ return this.prefs_[meth](key);
+ } catch(e) {
+ return opt_default;
+ }
+}
+
+/**
+ * Delete a preference.
+ *
+ * @param which Name of preference to obliterate
+ */
+G_Preferences.prototype.clearPref = function(which) {
+ try {
+ // This throws if the pref doesn't exist, which is fine because a
+ // nonexistent pref is cleared
+ this.prefs_.clearUserPref(which);
+ } catch(e) {}
+}
+
+/**
+ * Add an observer for a given pref.
+ *
+ * @param which String containing the pref to listen to
+ * @param callback Function to be called when the pref changes. This
+ * function will receive a single argument, a string
+ * holding the preference name that changed
+ */
+G_Preferences.prototype.addObserver = function(which, callback) {
+ // Need to store the observer we create so we can eventually unregister it
+ if (!this.observers_[which])
+ this.observers_[which] = { callbacks: [], observers: [] };
+
+ /* only add an observer if the callback hasn't been registered yet */
+ if (this.observers_[which].callbacks.indexOf(callback) == -1) {
+ var observer = new G_PreferenceObserver(callback);
+ this.observers_[which].callbacks.push(callback);
+ this.observers_[which].observers.push(observer);
+ this.prefs_.addObserver(which, observer, false /* strong reference */);
+ }
+}
+
+/**
+ * Remove an observer for a given pref.
+ *
+ * @param which String containing the pref to stop listening to
+ * @param callback Function to remove as an observer
+ */
+G_Preferences.prototype.removeObserver = function(which, callback) {
+ var ix = this.observers_[which].callbacks.indexOf(callback);
+ G_Assert(this, ix != -1, "Tried to unregister a nonexistent observer");
+ this.observers_[which].callbacks.splice(ix, 1);
+ var observer = this.observers_[which].observers.splice(ix, 1)[0];
+ this.prefs_.removeObserver(which, observer);
+}
+
+/**
+ * Remove all preference observers registered through this object.
+ */
+G_Preferences.prototype.removeAllObservers = function() {
+ for (var which in this.observers_) {
+ for (var observer of this.observers_[which].observers) {
+ this.prefs_.removeObserver(which, observer);
+ }
+ }
+ this.observers_ = {};
+}
+
+/**
+ * Helper class that knows how to observe preference changes and
+ * invoke a callback when they do
+ *
+ * @constructor
+ * @param callback Function to call when the preference changes
+ */
+this.G_PreferenceObserver =
+function G_PreferenceObserver(callback) {
+ this.debugZone = "prefobserver";
+ this.callback_ = callback;
+}
+
+/**
+ * Invoked by the pref system when a preference changes. Passes the
+ * message along to the callback.
+ *
+ * @param subject The nsIPrefBranch that changed
+ * @param topic String "nsPref:changed" (aka
+ * NS_PREFBRANCH_PREFCHANGE_OBSERVER_ID -- but where does it
+ * live???)
+ * @param data Name of the pref that changed
+ */
+G_PreferenceObserver.prototype.observe = function(subject, topic, data) {
+ G_Debug(this, "Observed pref change: " + data);
+ this.callback_(data);
+}
+
+/**
+ * XPCOM cruft
+ *
+ * @param iid Interface id of the interface the caller wants
+ */
+G_PreferenceObserver.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIObserver) ||
+ iid.equals(Ci.nsISupportsWeakReference))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+#ifdef DEBUG
+// UNITTESTS
+this.TEST_G_Preferences = function TEST_G_Preferences() {
+ if (G_GDEBUG) {
+ var z = "preferences UNITTEST";
+ G_debugService.enableZone(z);
+ G_Debug(z, "Starting");
+
+ var p = new G_Preferences();
+
+ var testPref = "test-preferences-unittest";
+ var noSuchPref = "test-preferences-unittest-aypabtu";
+
+ // Used to test observing
+ var observeCount = 0;
+ let observe = function (prefChanged) {
+ G_Assert(z, prefChanged == testPref, "observer broken");
+ observeCount++;
+ };
+
+ // Test setting, getting, and observing
+ p.addObserver(testPref, observe);
+ p.setPref(testPref, true);
+ G_Assert(z, p.getPref(testPref), "get or set broken");
+ G_Assert(z, observeCount == 1, "observer adding not working");
+
+ p.removeObserver(testPref, observe);
+
+ p.setPref(testPref, false);
+ G_Assert(z, observeCount == 1, "observer removal not working");
+ G_Assert(z, !p.getPref(testPref), "get broken");
+
+ // Remember to clean up the prefs we've set, and test removing prefs
+ // while we're at it
+ p.clearPref(noSuchPref);
+ G_Assert(z, !p.getPref(noSuchPref, false), "clear broken");
+
+ p.clearPref(testPref);
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/protocol4.js b/toolkit/components/url-classifier/content/moz/protocol4.js
new file mode 100644
index 0000000000..a75f6b531e
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/protocol4.js
@@ -0,0 +1,133 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+// A helper class that knows how to parse from and serialize to
+// protocol4. This is a simple, historical format used by some Google
+// interfaces, for example the Toolbar (i.e., ancient services).
+//
+// Protocol4 consists of a newline-separated sequence of name/value
+// pairs (strings). Each line consists of the name, the value length,
+// and the value itself, all separated by colons. Example:
+//
+// foo:6:barbaz\n
+// fritz:33:issickofdynamicallytypedlanguages\n
+
+
+/**
+ * This class knows how to serialize/deserialize maps to/from their
+ * protocol4 representation.
+ *
+ * @constructor
+ */
+this.G_Protocol4Parser = function G_Protocol4Parser() {
+ this.debugZone = "protocol4";
+
+ this.protocol4RegExp_ = new RegExp("([^:]+):\\d+:(.*)$");
+ this.newlineRegExp_ = new RegExp("(\\r)?\\n");
+}
+
+/**
+ * Create a map from a protocol4 string. Silently skips invalid lines.
+ *
+ * @param text String holding the protocol4 representation
+ *
+ * @returns Object as an associative array with keys and values
+ * given in text. The empty object is returned if none
+ * are parsed.
+ */
+G_Protocol4Parser.prototype.parse = function(text) {
+
+ var response = {};
+ if (!text)
+ return response;
+
+ // Responses are protocol4: (repeated) name:numcontentbytes:content\n
+ var lines = text.split(this.newlineRegExp_);
+ for (var i = 0; i < lines.length; i++)
+ if (this.protocol4RegExp_.exec(lines[i]))
+ response[RegExp.$1] = RegExp.$2;
+
+ return response;
+}
+
+/**
+ * Create a protocol4 string from a map (object). Throws an error on
+ * an invalid input.
+ *
+ * @param map Object as an associative array with keys and values
+ * given as strings.
+ *
+ * @returns text String holding the protocol4 representation
+ */
+G_Protocol4Parser.prototype.serialize = function(map) {
+ if (typeof map != "object")
+ throw new Error("map must be an object");
+
+ var text = "";
+ for (var key in map) {
+ if (typeof map[key] != "string")
+ throw new Error("Keys and values must be strings");
+
+ text += key + ":" + map[key].length + ":" + map[key] + "\n";
+ }
+
+ return text;
+}
+
+#ifdef DEBUG
+/**
+ * Cheesey unittests
+ */
+this.TEST_G_Protocol4Parser = function TEST_G_Protocol4Parser() {
+ if (G_GDEBUG) {
+ var z = "protocol4 UNITTEST";
+ G_debugService.enableZone(z);
+
+ G_Debug(z, "Starting");
+
+ var p = new G_Protocol4Parser();
+
+ let isEmpty = function (map) {
+ for (var key in map)
+ return false;
+ return true;
+ };
+
+ G_Assert(z, isEmpty(p.parse(null)), "Parsing null broken");
+ G_Assert(z, isEmpty(p.parse("")), "Parsing nothing broken");
+
+ var t = "foo:3:bar";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing one line broken");
+
+ t = "foo:3:bar\n";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing line with lf broken");
+
+ t = "foo:3:bar\r\n";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing with crlf broken");
+
+
+ t = "foo:3:bar\nbar:3:baz\r\nbom:3:yaz\n";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "First in multiline");
+ G_Assert(z, p.parse(t)["bar"] === "baz", "Second in multiline");
+ G_Assert(z, p.parse(t)["bom"] === "yaz", "Third in multiline");
+ G_Assert(z, p.parse(t)[""] === undefined, "Nonexistent in multiline");
+
+ // Test serialization
+
+ var original = {
+ "1": "1",
+ "2": "2",
+ "foobar": "baz",
+ "hello there": "how are you?" ,
+ };
+ var deserialized = p.parse(p.serialize(original));
+ for (var key in original)
+ G_Assert(z, original[key] === deserialized[key],
+ "Trouble (de)serializing " + key);
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/multi-querier.js b/toolkit/components/url-classifier/content/multi-querier.js
new file mode 100644
index 0000000000..f79db8154d
--- /dev/null
+++ b/toolkit/components/url-classifier/content/multi-querier.js
@@ -0,0 +1,137 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 class helps us batch a series of async calls to the db.
+ * If any of the tokens is in the database, we fire callback with
+ * true as a param. If all the tokens are not in the database,
+ * we fire callback with false as a param.
+ * This is an "Abstract" base class. Subclasses need to supply
+ * the condition_ method.
+ *
+ * @param tokens Array of strings to lookup in the db
+ * @param tableName String name of the table
+ * @param callback Function callback function that takes true if the condition
+ * passes.
+ */
+this.MultiQuerier =
+function MultiQuerier(tokens, tableName, callback) {
+ this.tokens_ = tokens;
+ this.tableName_ = tableName;
+ this.callback_ = callback;
+ this.dbservice_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+ // We put the current token in this variable.
+ this.key_ = null;
+}
+
+/**
+ * Run the remaining tokens against the db.
+ */
+MultiQuerier.prototype.run = function() {
+ if (this.tokens_.length == 0) {
+ this.callback_.handleEvent(false);
+ this.dbservice_ = null;
+ this.callback_ = null;
+ return;
+ }
+
+ this.key_ = this.tokens_.pop();
+ G_Debug(this, "Looking up " + this.key_ + " in " + this.tableName_);
+ this.dbservice_.exists(this.tableName_, this.key_,
+ BindToObject(this.result_, this));
+}
+
+/**
+ * Callback from the db. If the returned value passes the this.condition_
+ * test, go ahead and call the main callback.
+ */
+MultiQuerier.prototype.result_ = function(value) {
+ if (this.condition_(value)) {
+ this.callback_.handleEvent(true)
+ this.dbservice_ = null;
+ this.callback_ = null;
+ } else {
+ this.run();
+ }
+}
+
+// Subclasses must override this.
+MultiQuerier.prototype.condition_ = function(value) {
+ throw "MultiQuerier is an abstract base class";
+}
+
+
+/**
+ * Concrete MultiQuerier that stops if the key exists in the db.
+ */
+this.ExistsMultiQuerier =
+function ExistsMultiQuerier(tokens, tableName, callback) {
+ MultiQuerier.call(this, tokens, tableName, callback);
+ this.debugZone = "existsMultiQuerier";
+}
+
+ExistsMultiQuerier.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+ExistsMultiQuerier.inherits(MultiQuerier);
+
+ExistsMultiQuerier.prototype.condition_ = function(value) {
+ return value.length > 0;
+}
+
+
+/**
+ * Concrete MultiQuerier that looks up a key, decrypts it, then
+ * checks the the resulting regular expressions for a match.
+ * @param tokens Array of hosts
+ */
+this.EnchashMultiQuerier =
+function EnchashMultiQuerier(tokens, tableName, callback, url) {
+ MultiQuerier.call(this, tokens, tableName, callback);
+ this.url_ = url;
+ this.enchashDecrypter_ = new PROT_EnchashDecrypter();
+ this.debugZone = "enchashMultiQuerier";
+}
+
+EnchashMultiQuerier.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+EnchashMultiQuerier.inherits(MultiQuerier);
+
+EnchashMultiQuerier.prototype.run = function() {
+ if (this.tokens_.length == 0) {
+ this.callback_.handleEvent(false);
+ this.dbservice_ = null;
+ this.callback_ = null;
+ return;
+ }
+ var host = this.tokens_.pop();
+ this.key_ = host;
+ var lookupKey = this.enchashDecrypter_.getLookupKey(host);
+ this.dbservice_.exists(this.tableName_, lookupKey,
+ BindToObject(this.result_, this));
+}
+
+EnchashMultiQuerier.prototype.condition_ = function(encryptedValue) {
+ if (encryptedValue.length > 0) {
+ // We have encrypted regular expressions for this host. Let's
+ // decrypt them and see if we have a match.
+ var decrypted = this.enchashDecrypter_.decryptData(encryptedValue,
+ this.key_);
+ var res = this.enchashDecrypter_.parseRegExps(decrypted);
+ for (var j = 0; j < res.length; j++) {
+ if (res[j].test(this.url_)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/toolkit/components/url-classifier/content/request-backoff.js b/toolkit/components/url-classifier/content/request-backoff.js
new file mode 100644
index 0000000000..17e815cf14
--- /dev/null
+++ b/toolkit/components/url-classifier/content/request-backoff.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 implements logic for stopping requests if the server starts to return
+// too many errors. If we get MAX_ERRORS errors in ERROR_PERIOD minutes, we
+// back off for TIMEOUT_INCREMENT minutes. If we get another error
+// immediately after we restart, we double the timeout and add
+// TIMEOUT_INCREMENT minutes, etc.
+//
+// This is similar to the logic used by the search suggestion service.
+
+// HTTP responses that count as an error. We also include any 5xx response
+// as an error.
+this.HTTP_FOUND = 302;
+this.HTTP_SEE_OTHER = 303;
+this.HTTP_TEMPORARY_REDIRECT = 307;
+
+/**
+ * @param maxErrors Number of times to request before backing off.
+ * @param retryIncrement Time (ms) for each retry before backing off.
+ * @param maxRequests Number the number of requests needed to trigger backoff
+ * @param requestPeriod Number time (ms) in which maxRequests have to occur to
+ * trigger the backoff behavior (0 to disable maxRequests)
+ * @param timeoutIncrement Number time (ms) the starting timeout period
+ * we double this time for consecutive errors
+ * @param maxTimeout Number time (ms) maximum timeout period
+ */
+this.RequestBackoff =
+function RequestBackoff(maxErrors, retryIncrement,
+ maxRequests, requestPeriod,
+ timeoutIncrement, maxTimeout) {
+ this.MAX_ERRORS_ = maxErrors;
+ this.RETRY_INCREMENT_ = retryIncrement;
+ this.MAX_REQUESTS_ = maxRequests;
+ this.REQUEST_PERIOD_ = requestPeriod;
+ this.TIMEOUT_INCREMENT_ = timeoutIncrement;
+ this.MAX_TIMEOUT_ = maxTimeout;
+
+ // Queue of ints keeping the time of all requests
+ this.requestTimes_ = [];
+
+ this.numErrors_ = 0;
+ this.errorTimeout_ = 0;
+ this.nextRequestTime_ = 0;
+}
+
+/**
+ * Reset the object for reuse. This deliberately doesn't clear requestTimes_.
+ */
+RequestBackoff.prototype.reset = function() {
+ this.numErrors_ = 0;
+ this.errorTimeout_ = 0;
+ this.nextRequestTime_ = 0;
+}
+
+/**
+ * Check to see if we can make a request.
+ */
+RequestBackoff.prototype.canMakeRequest = function() {
+ var now = Date.now();
+ if (now < this.nextRequestTime_) {
+ return false;
+ }
+
+ return (this.requestTimes_.length < this.MAX_REQUESTS_ ||
+ (now - this.requestTimes_[0]) > this.REQUEST_PERIOD_);
+}
+
+RequestBackoff.prototype.noteRequest = function() {
+ var now = Date.now();
+ this.requestTimes_.push(now);
+
+ // We only care about keeping track of MAX_REQUESTS
+ if (this.requestTimes_.length > this.MAX_REQUESTS_)
+ this.requestTimes_.shift();
+}
+
+RequestBackoff.prototype.nextRequestDelay = function() {
+ return Math.max(0, this.nextRequestTime_ - Date.now());
+}
+
+/**
+ * Notify this object of the last server response. If it's an error,
+ */
+RequestBackoff.prototype.noteServerResponse = function(status) {
+ if (this.isErrorStatus(status)) {
+ this.numErrors_++;
+
+ if (this.numErrors_ < this.MAX_ERRORS_)
+ this.errorTimeout_ = this.RETRY_INCREMENT_;
+ else if (this.numErrors_ == this.MAX_ERRORS_)
+ this.errorTimeout_ = this.TIMEOUT_INCREMENT_;
+ else
+ this.errorTimeout_ *= 2;
+
+ this.errorTimeout_ = Math.min(this.errorTimeout_, this.MAX_TIMEOUT_);
+ this.nextRequestTime_ = Date.now() + this.errorTimeout_;
+ } else {
+ // Reset error timeout, allow requests to go through.
+ this.reset();
+ }
+}
+
+/**
+ * We consider 302, 303, 307, 4xx, and 5xx http responses to be errors.
+ * @param status Number http status
+ * @return Boolean true if we consider this http status an error
+ */
+RequestBackoff.prototype.isErrorStatus = function(status) {
+ return ((400 <= status && status <= 599) ||
+ HTTP_FOUND == status ||
+ HTTP_SEE_OTHER == status ||
+ HTTP_TEMPORARY_REDIRECT == status);
+}
+
diff --git a/toolkit/components/url-classifier/content/trtable.js b/toolkit/components/url-classifier/content/trtable.js
new file mode 100644
index 0000000000..c58a80c9ad
--- /dev/null
+++ b/toolkit/components/url-classifier/content/trtable.js
@@ -0,0 +1,169 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// XXX: This should all be moved into the dbservice class so it happens
+// in the background thread.
+
+/**
+ * Abstract base class for a lookup table.
+ * @construction
+ */
+this.UrlClassifierTable = function UrlClassifierTable() {
+ this.debugZone = "urlclassifier-table";
+ this.name = '';
+ this.needsUpdate = false;
+ this.enchashDecrypter_ = new PROT_EnchashDecrypter();
+ this.wrappedJSObject = this;
+}
+
+UrlClassifierTable.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Components.interfaces.nsISupports) ||
+ iid.equals(Components.interfaces.nsIUrlClassifierTable))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+/**
+ * Subclasses need to implement this method.
+ */
+UrlClassifierTable.prototype.exists = function(url, callback) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/////////////////////////////////////////////////////////////////////
+// Url table implementation
+this.UrlClassifierTableUrl = function UrlClassifierTableUrl() {
+ UrlClassifierTable.call(this);
+}
+
+UrlClassifierTableUrl.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+UrlClassifierTableUrl.inherits(UrlClassifierTable);
+
+/**
+ * Look up a URL in a URL table
+ */
+UrlClassifierTableUrl.prototype.exists = function(url, callback) {
+ // nsIUrlClassifierUtils.canonicalizeURL is the old way of canonicalizing a
+ // URL. Unfortunately, it doesn't normalize numeric domains so alternate IP
+ // formats (hex, octal, etc) won't trigger a match.
+ // this.enchashDecrypter_.getCanonicalUrl does the right thing and
+ // normalizes a URL to 4 decimal numbers, but the update server may still be
+ // giving us encoded IP addresses. So to be safe, we check both cases.
+ var urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+ var oldCanonicalized = urlUtils.canonicalizeURL(url);
+ var canonicalized = this.enchashDecrypter_.getCanonicalUrl(url);
+ G_Debug(this, "Looking up: " + url + " (" + oldCanonicalized + " and " +
+ canonicalized + ")");
+ (new ExistsMultiQuerier([oldCanonicalized, canonicalized],
+ this.name,
+ callback)).run();
+}
+
+/////////////////////////////////////////////////////////////////////
+// Domain table implementation
+
+this.UrlClassifierTableDomain = function UrlClassifierTableDomain() {
+ UrlClassifierTable.call(this);
+ this.debugZone = "urlclassifier-table-domain";
+ this.ioService_ = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+}
+
+UrlClassifierTableDomain.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+UrlClassifierTableDomain.inherits(UrlClassifierTable);
+
+/**
+ * Look up a URL in a domain table
+ * We also try to lookup domain + first path component (e.g.,
+ * www.mozilla.org/products).
+ *
+ * @returns Boolean true if the url domain is in the table
+ */
+UrlClassifierTableDomain.prototype.exists = function(url, callback) {
+ var canonicalized = this.enchashDecrypter_.getCanonicalUrl(url);
+ var urlObj = this.ioService_.newURI(canonicalized, null, null);
+ var host = '';
+ try {
+ host = urlObj.host;
+ } catch (e) { }
+ var hostComponents = host.split(".");
+
+ // Try to get the path of the URL. Pseudo urls (like wyciwyg:) throw
+ // errors when trying to convert to an nsIURL so we wrap in a try/catch
+ // block.
+ var path = ""
+ try {
+ urlObj.QueryInterface(Ci.nsIURL);
+ path = urlObj.filePath;
+ } catch (e) { }
+
+ var pathComponents = path.split("/");
+
+ // We don't have a good way map from hosts to domains, so we instead try
+ // each possibility. Could probably optimize to start at the second dot?
+ var possible = [];
+ for (var i = 0; i < hostComponents.length - 1; i++) {
+ host = hostComponents.slice(i).join(".");
+ possible.push(host);
+
+ // The path starts with a "/", so we are interested in the second path
+ // component if it is available
+ if (pathComponents.length >= 2 && pathComponents[1].length > 0) {
+ host = host + "/" + pathComponents[1];
+ possible.push(host);
+ }
+ }
+
+ // Run the possible domains against the db.
+ (new ExistsMultiQuerier(possible, this.name, callback)).run();
+}
+
+/////////////////////////////////////////////////////////////////////
+// Enchash table implementation
+
+this.UrlClassifierTableEnchash = function UrlClassifierTableEnchash() {
+ UrlClassifierTable.call(this);
+ this.debugZone = "urlclassifier-table-enchash";
+}
+
+UrlClassifierTableEnchash.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+UrlClassifierTableEnchash.inherits(UrlClassifierTable);
+
+/**
+ * Look up a URL in an enchashDB. We try all sub domains (up to MAX_DOTS).
+ */
+UrlClassifierTableEnchash.prototype.exists = function(url, callback) {
+ url = this.enchashDecrypter_.getCanonicalUrl(url);
+ var host = this.enchashDecrypter_.getCanonicalHost(url,
+ PROT_EnchashDecrypter.MAX_DOTS);
+
+ var possible = [];
+ for (var i = 0; i < PROT_EnchashDecrypter.MAX_DOTS + 1; i++) {
+ possible.push(host);
+
+ var index = host.indexOf(".");
+ if (index == -1)
+ break;
+ host = host.substring(index + 1);
+ }
+ // Run the possible domains against the db.
+ (new EnchashMultiQuerier(possible, this.name, callback, url)).run();
+}
diff --git a/toolkit/components/url-classifier/content/wireformat.js b/toolkit/components/url-classifier/content/wireformat.js
new file mode 100644
index 0000000000..a24b120e6b
--- /dev/null
+++ b/toolkit/components/url-classifier/content/wireformat.js
@@ -0,0 +1,230 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+// A class that serializes and deserializes opaque key/value string to
+// string maps to/from maps (trtables). It knows how to create
+// trtables from the serialized format, so it also understands
+// meta-information like the name of the table and the table's
+// version. See docs for the protocol description.
+//
+// TODO: wireformatreader: if you have multiple updates for one table
+// in a call to deserialize, the later ones will be merged
+// (all but the last will be ignored). To fix, merge instead
+// of replace when you have an existing table, and only do so once.
+// TODO must have blank line between successive types -- problem?
+// TODO doesn't tolerate blank lines very well
+//
+// Maybe: These classes could use a LOT more cleanup, but it's not a
+// priority at the moment. For example, the tablesData/Known
+// maps should be combined into a single object, the parser
+// for a given type should be separate from the version info,
+// and there should be synchronous interfaces for testing.
+
+
+/**
+ * A class that knows how to serialize and deserialize meta-information.
+ * This meta information is the table name and version number, and
+ * in its serialized form looks like the first line below:
+ *
+ * [name-of-table X.Y update?]
+ * ...key/value pairs to add or delete follow...
+ * <blank line ends the table>
+ *
+ * The X.Y is the version number and the optional "update" token means
+ * that the table is a differential from the curent table the extension
+ * has. Its absence means that this is a full, new table.
+ */
+this.PROT_VersionParser =
+function PROT_VersionParser(type, opt_major, opt_minor, opt_requireMac) {
+ this.debugZone = "versionparser";
+ this.type = type;
+ this.major = 0;
+ this.minor = 0;
+
+ this.badHeader = false;
+
+ // Should the wireformatreader compute a mac?
+ this.mac = false;
+ this.macval = "";
+ this.macFailed = false;
+ this.requireMac = !!opt_requireMac;
+
+ this.update = false;
+ this.needsUpdate = false; // used by ListManager to determine update policy
+ // Used by ListerManager to see if we have read data for this table from
+ // disk. Once we read a table from disk, we are not going to do so again
+ // but instead update remotely if necessary.
+ this.didRead = false;
+ if (opt_major)
+ this.major = parseInt(opt_major);
+ if (opt_minor)
+ this.minor = parseInt(opt_minor);
+}
+
+/** Import the version information from another VersionParser
+ * @params version a version parser object
+ */
+PROT_VersionParser.prototype.ImportVersion = function(version) {
+ this.major = version.major;
+ this.minor = version.minor;
+
+ this.mac = version.mac;
+ this.macFailed = version.macFailed;
+ this.macval = version.macval;
+ // Don't set requireMac, since we create vparsers from scratch and doesn't
+ // know about it
+}
+
+/**
+ * Creates a string like [goog-white-black 1.1] from internal information
+ *
+ * @returns String
+ */
+PROT_VersionParser.prototype.toString = function() {
+ var s = "[" + this.type + " " + this.major + "." + this.minor + "]";
+ return s;
+}
+
+/**
+ * Creates a string like 1.123 with the version number. This is the
+ * format we store in prefs.
+ * @return String
+ */
+PROT_VersionParser.prototype.versionString = function() {
+ return this.major + "." + this.minor;
+}
+
+/**
+ * Creates a string like 1:1 from internal information used for
+ * fetching updates from the server. Called by the listmanager.
+ *
+ * @returns String
+ */
+PROT_VersionParser.prototype.toUrl = function() {
+ return this.major + ":" + this.minor;
+}
+
+/**
+ * Process the old format, [type major.minor [update]]
+ *
+ * @returns true if the string could be parsed, false otherwise
+ */
+PROT_VersionParser.prototype.processOldFormat_ = function(line) {
+ if (line[0] != '[' || line.slice(-1) != ']')
+ return false;
+
+ var description = line.slice(1, -1);
+
+ // Get the type name and version number of this table
+ var tokens = description.split(" ");
+ this.type = tokens[0];
+ var majorminor = tokens[1].split(".");
+ this.major = parseInt(majorminor[0]);
+ this.minor = parseInt(majorminor[1]);
+ if (isNaN(this.major) || isNaN(this.minor))
+ return false;
+
+ if (tokens.length >= 3) {
+ this.update = tokens[2] == "update";
+ }
+
+ return true;
+}
+
+/**
+ * Takes a string like [name-of-table 1.1 [update]][mac=MAC] and figures out the
+ * type and corresponding version numbers.
+ * @returns true if the string could be parsed, false otherwise
+ */
+PROT_VersionParser.prototype.fromString = function(line) {
+ G_Debug(this, "Calling fromString with line: " + line);
+ if (line[0] != '[' || line.slice(-1) != ']')
+ return false;
+
+ // There could be two [][], so take care of it
+ var secondBracket = line.indexOf('[', 1);
+ var firstPart = null;
+ var secondPart = null;
+
+ if (secondBracket != -1) {
+ firstPart = line.substring(0, secondBracket);
+ secondPart = line.substring(secondBracket);
+ G_Debug(this, "First part: " + firstPart + " Second part: " + secondPart);
+ } else {
+ firstPart = line;
+ G_Debug(this, "Old format: " + firstPart);
+ }
+
+ if (!this.processOldFormat_(firstPart))
+ return false;
+
+ if (secondPart && !this.processOptTokens_(secondPart))
+ return false;
+
+ return true;
+}
+
+/**
+ * Process optional tokens
+ *
+ * @param line A string [token1=val1 token2=val2...]
+ * @returns true if the string could be parsed, false otherwise
+ */
+PROT_VersionParser.prototype.processOptTokens_ = function(line) {
+ if (line[0] != '[' || line.slice(-1) != ']')
+ return false;
+ var description = line.slice(1, -1);
+ // Get the type name and version number of this table
+ var tokens = description.split(" ");
+
+ for (var i = 0; i < tokens.length; i++) {
+ G_Debug(this, "Processing optional token: " + tokens[i]);
+ var tokenparts = tokens[i].split("=");
+ switch(tokenparts[0]){
+ case "mac":
+ this.mac = true;
+ if (tokenparts.length < 2) {
+ G_Debug(this, "Found mac flag but not mac value!");
+ return false;
+ }
+ // The mac value may have "=" in it, so we can't just use tokenparts[1].
+ // Instead, just take the rest of tokens[i] after the first "="
+ this.macval = tokens[i].substr(tokens[i].indexOf("=")+1);
+ break;
+ default:
+ G_Debug(this, "Found unrecognized token: " + tokenparts[0]);
+ break;
+ }
+ }
+
+ return true;
+}
+
+#ifdef DEBUG
+this.TEST_PROT_WireFormat = function TEST_PROT_WireFormat() {
+ if (G_GDEBUG) {
+ var z = "versionparser UNITTEST";
+ G_Debug(z, "Starting");
+
+ var vp = new PROT_VersionParser("dummy");
+ G_Assert(z, vp.fromString("[foo-bar-url 1.234]"),
+ "failed to parse old format");
+ G_Assert(z, "foo-bar-url" == vp.type, "failed to parse type");
+ G_Assert(z, "1" == vp.major, "failed to parse major");
+ G_Assert(z, "234" == vp.minor, "failed to parse minor");
+
+ vp = new PROT_VersionParser("dummy");
+ G_Assert(z, vp.fromString("[foo-bar-url 1.234][mac=567]"),
+ "failed to parse new format");
+ G_Assert(z, "foo-bar-url" == vp.type, "failed to parse type");
+ G_Assert(z, "1" == vp.major, "failed to parse major");
+ G_Assert(z, "234" == vp.minor, "failed to parse minor");
+ G_Assert(z, true == vp.mac, "failed to parse mac");
+ G_Assert(z, "567" == vp.macval, "failed to parse macval");
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/xml-fetcher.js b/toolkit/components/url-classifier/content/xml-fetcher.js
new file mode 100644
index 0000000000..39b116e00a
--- /dev/null
+++ b/toolkit/components/url-classifier/content/xml-fetcher.js
@@ -0,0 +1,126 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// A simple class that encapsulates a request. You'll notice the
+// style here is different from the rest of the extension; that's
+// because this was re-used from really old code we had. At some
+// point it might be nice to replace this with something better
+// (e.g., something that has explicit onerror handler, ability
+// to set headers, and so on).
+
+/**
+ * Because we might be in a component, we can't just assume that
+ * XMLHttpRequest exists. So we use this tiny factory function to wrap the
+ * XPCOM version.
+ *
+ * @return XMLHttpRequest object
+ */
+this.PROT_NewXMLHttpRequest = function PROT_NewXMLHttpRequest() {
+ var Cc = Components.classes;
+ var Ci = Components.interfaces;
+ var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ // Need the following so we get onerror/load/progresschange
+ request.QueryInterface(Ci.nsIJSXMLHttpRequest);
+ return request;
+}
+
+/**
+ * A helper class that does HTTP GETs and calls back a function with
+ * the content it receives. Asynchronous, so uses a closure for the
+ * callback.
+ *
+ * Note, that XMLFetcher is only used for SafeBrowsing, therefore
+ * we inherit from nsILoadContext, so we can use the callbacks on the
+ * channel to separate the safebrowsing cookie based on a reserved
+ * appId.
+ * @constructor
+ */
+this.PROT_XMLFetcher = function PROT_XMLFetcher() {
+ this.debugZone = "xmlfetcher";
+ this._request = PROT_NewXMLHttpRequest();
+ // implements nsILoadContext
+ this.appId = Ci.nsIScriptSecurityManager.SAFEBROWSING_APP_ID;
+ this.isInIsolatedMozBrowserElement = false;
+ this.usePrivateBrowsing = false;
+ this.isContent = false;
+}
+
+PROT_XMLFetcher.prototype = {
+ /**
+ * Function that will be called back upon fetch completion.
+ */
+ _callback: null,
+
+
+ /**
+ * Fetches some content.
+ *
+ * @param page URL to fetch
+ * @param callback Function to call back when complete.
+ */
+ get: function(page, callback) {
+ this._request.abort(); // abort() is asynchronous, so
+ this._request = PROT_NewXMLHttpRequest();
+ this._callback = callback;
+ var asynchronous = true;
+ this._request.loadInfo.originAttributes = {
+ appId: this.appId,
+ inIsolatedMozBrowser: this.isInIsolatedMozBrowserElement
+ };
+ this._request.open("GET", page, asynchronous);
+ this._request.channel.notificationCallbacks = this;
+
+ // Create a closure
+ var self = this;
+ this._request.addEventListener("readystatechange", function() {
+ self.readyStateChange(self);
+ }, false);
+
+ this._request.send(null);
+ },
+
+ cancel: function() {
+ this._request.abort();
+ this._request = null;
+ },
+
+ /**
+ * Called periodically by the request to indicate some state change. 4
+ * means content has been received.
+ */
+ readyStateChange: function(fetcher) {
+ if (fetcher._request.readyState != 4)
+ return;
+
+ // If the request fails, on trunk we get status set to
+ // NS_ERROR_NOT_AVAILABLE. On 1.8.1 branch we get an exception
+ // forwarded from nsIHttpChannel::GetResponseStatus. To be consistent
+ // between branch and trunk, we send back NS_ERROR_NOT_AVAILABLE for
+ // http failures.
+ var responseText = null;
+ var status = Components.results.NS_ERROR_NOT_AVAILABLE;
+ try {
+ G_Debug(this, "xml fetch status code: \"" +
+ fetcher._request.status + "\"");
+ status = fetcher._request.status;
+ responseText = fetcher._request.responseText;
+ } catch(e) {
+ G_Debug(this, "Caught exception trying to read xmlhttprequest " +
+ "status/response.");
+ G_Debug(this, e);
+ }
+ if (fetcher._callback)
+ fetcher._callback(responseText, status);
+ },
+
+ // nsIInterfaceRequestor
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
+ Ci.nsISupports,
+ Ci.nsILoadContext])
+};
diff --git a/toolkit/components/url-classifier/moz.build b/toolkit/components/url-classifier/moz.build
new file mode 100644
index 0000000000..d8856ee4a9
--- /dev/null
+++ b/toolkit/components/url-classifier/moz.build
@@ -0,0 +1,86 @@
+# -*- 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/.
+
+TEST_DIRS += ['tests']
+
+XPIDL_SOURCES += [
+ 'nsIUrlClassifierDBService.idl',
+ 'nsIUrlClassifierHashCompleter.idl',
+ 'nsIUrlClassifierPrefixSet.idl',
+ 'nsIUrlClassifierStreamUpdater.idl',
+ 'nsIUrlClassifierUtils.idl',
+ 'nsIUrlListManager.idl',
+]
+
+XPIDL_MODULE = 'url-classifier'
+
+# Disable RTTI in google protocol buffer
+DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
+
+UNIFIED_SOURCES += [
+ 'ChunkSet.cpp',
+ 'Classifier.cpp',
+ 'LookupCache.cpp',
+ 'LookupCacheV4.cpp',
+ 'nsCheckSummedOutputStream.cpp',
+ 'nsUrlClassifierDBService.cpp',
+ 'nsUrlClassifierProxies.cpp',
+ 'nsUrlClassifierUtils.cpp',
+ 'protobuf/safebrowsing.pb.cc',
+ 'ProtocolParser.cpp',
+ 'RiceDeltaDecoder.cpp',
+]
+
+# define conflicting LOG() macros
+SOURCES += [
+ 'nsUrlClassifierPrefixSet.cpp',
+ 'nsUrlClassifierStreamUpdater.cpp',
+ 'VariableLengthPrefixSet.cpp',
+]
+
+# contains variables that conflict with LookupCache.cpp
+SOURCES += [
+ 'HashStore.cpp',
+]
+
+EXTRA_COMPONENTS += [
+ 'nsURLClassifier.manifest',
+ 'nsUrlClassifierHashCompleter.js',
+]
+
+# Same as JS components that are run through the pre-processor.
+EXTRA_PP_COMPONENTS += [
+ 'nsUrlClassifierLib.js',
+ 'nsUrlClassifierListManager.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'SafeBrowsing.jsm',
+]
+
+EXPORTS += [
+ 'Entries.h',
+ 'LookupCache.h',
+ 'LookupCacheV4.h',
+ 'nsUrlClassifierPrefixSet.h',
+ 'protobuf/safebrowsing.pb.h',
+ 'VariableLengthPrefixSet.h',
+]
+
+FINAL_LIBRARY = 'xul'
+
+LOCAL_INCLUDES += [
+ '../build',
+ '/ipc/chromium/src',
+]
+
+CXXFLAGS += CONFIG['SQLITE_CFLAGS']
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
+
+if CONFIG['NIGHTLY_BUILD'] or CONFIG['MOZ_DEBUG']:
+ DEFINES['MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES'] = True
diff --git a/toolkit/components/url-classifier/nsCheckSummedOutputStream.cpp b/toolkit/components/url-classifier/nsCheckSummedOutputStream.cpp
new file mode 100644
index 0000000000..68f9f1f6f4
--- /dev/null
+++ b/toolkit/components/url-classifier/nsCheckSummedOutputStream.cpp
@@ -0,0 +1,59 @@
+//* -*- 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 "nsILocalFile.h"
+#include "nsCRT.h"
+#include "nsIFile.h"
+#include "nsISupportsImpl.h"
+#include "nsCheckSummedOutputStream.h"
+
+////////////////////////////////////////////////////////////////////////////////
+// nsCheckSummedOutputStream
+
+NS_IMPL_ISUPPORTS_INHERITED(nsCheckSummedOutputStream,
+ nsSafeFileOutputStream,
+ nsISafeOutputStream,
+ nsIOutputStream,
+ nsIFileOutputStream)
+
+NS_IMETHODIMP
+nsCheckSummedOutputStream::Init(nsIFile* file, int32_t ioFlags, int32_t perm,
+ int32_t behaviorFlags)
+{
+ nsresult rv;
+ mHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mHash->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nsSafeFileOutputStream::Init(file, ioFlags, perm, behaviorFlags);
+}
+
+NS_IMETHODIMP
+nsCheckSummedOutputStream::Finish()
+{
+ nsresult rv = mHash->Finish(false, mCheckSum);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t written;
+ rv = nsSafeFileOutputStream::Write(reinterpret_cast<const char*>(mCheckSum.BeginReading()),
+ mCheckSum.Length(), &written);
+ NS_ASSERTION(written == mCheckSum.Length(), "Error writing stream checksum");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nsSafeFileOutputStream::Finish();
+}
+
+NS_IMETHODIMP
+nsCheckSummedOutputStream::Write(const char *buf, uint32_t count, uint32_t *result)
+{
+ nsresult rv = mHash->Update(reinterpret_cast<const uint8_t*>(buf), count);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nsSafeFileOutputStream::Write(buf, count, result);
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/toolkit/components/url-classifier/nsCheckSummedOutputStream.h b/toolkit/components/url-classifier/nsCheckSummedOutputStream.h
new file mode 100644
index 0000000000..c2fe26b5f5
--- /dev/null
+++ b/toolkit/components/url-classifier/nsCheckSummedOutputStream.h
@@ -0,0 +1,55 @@
+//* -*- 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/. */
+
+#ifndef nsCheckSummedOutputStream_h__
+#define nsCheckSummedOutputStream_h__
+
+#include "nsILocalFile.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+#include "nsICryptoHash.h"
+#include "nsNetCID.h"
+#include "nsString.h"
+#include "../../../netwerk/base/nsFileStreams.h"
+#include "nsToolkitCompsCID.h"
+
+class nsCheckSummedOutputStream : public nsSafeFileOutputStream
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // Size of MD5 hash in bytes
+ static const uint32_t CHECKSUM_SIZE = 16;
+
+ nsCheckSummedOutputStream() {}
+
+ NS_IMETHOD Finish() override;
+ NS_IMETHOD Write(const char *buf, uint32_t count, uint32_t *result) override;
+ NS_IMETHOD Init(nsIFile* file, int32_t ioFlags, int32_t perm, int32_t behaviorFlags) override;
+
+protected:
+ virtual ~nsCheckSummedOutputStream() { nsSafeFileOutputStream::Close(); }
+
+ nsCOMPtr<nsICryptoHash> mHash;
+ nsCString mCheckSum;
+};
+
+// returns a file output stream which can be QI'ed to nsIFileOutputStream.
+inline nsresult
+NS_NewCheckSummedOutputStream(nsIOutputStream **result,
+ nsIFile *file,
+ int32_t ioFlags = -1,
+ int32_t perm = -1,
+ int32_t behaviorFlags = 0)
+{
+ nsCOMPtr<nsIFileOutputStream> out = new nsCheckSummedOutputStream();
+ nsresult rv = out->Init(file, ioFlags, perm, behaviorFlags);
+ if (NS_SUCCEEDED(rv)) {
+ out.forget(result);
+ }
+ return rv;
+}
+
+#endif
diff --git a/toolkit/components/url-classifier/nsIUrlClassifierDBService.idl b/toolkit/components/url-classifier/nsIUrlClassifierDBService.idl
new file mode 100644
index 0000000000..498d9717e7
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlClassifierDBService.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#include "Entries.h"
+#include "LookupCache.h"
+class nsUrlClassifierLookupResult;
+%}
+[ptr] native ResultArray(nsTArray<mozilla::safebrowsing::LookupResult>);
+[ptr] native CacheCompletionArray(nsTArray<mozilla::safebrowsing::CacheResult>);
+[ptr] native PrefixArray(mozilla::safebrowsing::PrefixArray);
+
+interface nsIUrlClassifierHashCompleter;
+interface nsIPrincipal;
+
+// Interface for JS function callbacks
+[scriptable, function, uuid(4ca27b6b-a674-4b3d-ab30-d21e2da2dffb)]
+interface nsIUrlClassifierCallback : nsISupports {
+ void handleEvent(in ACString value);
+};
+
+/**
+ * The nsIUrlClassifierUpdateObserver interface is implemented by
+ * clients streaming updates to the url-classifier (usually
+ * nsUrlClassifierStreamUpdater.
+ */
+[scriptable, uuid(9fa11561-5816-4e1b-bcc9-b629ca05cce6)]
+interface nsIUrlClassifierUpdateObserver : nsISupports {
+ /**
+ * The update requested a new URL whose contents should be downloaded
+ * and sent to the classifier as a new stream.
+ *
+ * @param url The url that was requested.
+ * @param table The table name that this URL's contents will be associated
+ * with. This should be passed back to beginStream().
+ */
+ void updateUrlRequested(in ACString url,
+ in ACString table);
+
+ /**
+ * A stream update has completed.
+ *
+ * @param status The state of the update process.
+ * @param delay The amount of time the updater should wait to fetch the
+ * next URL in ms.
+ */
+ void streamFinished(in nsresult status, in unsigned long delay);
+
+ /* The update has encountered an error and should be cancelled */
+ void updateError(in nsresult error);
+
+ /**
+ * The update has completed successfully.
+ *
+ * @param requestedTimeout The number of seconds that the caller should
+ * wait before trying to update again.
+ **/
+ void updateSuccess(in unsigned long requestedTimeout);
+};
+
+/**
+ * This is a proxy class that is instantiated and called from the JS thread.
+ * It provides async methods for querying and updating the database. As the
+ * methods complete, they call the callback function.
+ */
+[scriptable, uuid(7a258022-6765-11e5-b379-b37b1f2354be)]
+interface nsIUrlClassifierDBService : nsISupports
+{
+ /**
+ * Looks up a URI in the specified tables.
+ *
+ * @param principal: The principal containing the URI to search.
+ * @param c: The callback will be called with a comma-separated list
+ * of tables to which the key belongs.
+ */
+ void lookup(in nsIPrincipal principal,
+ in ACString tables,
+ in nsIUrlClassifierCallback c);
+
+ /**
+ * Lists the tables along with their meta info in the following format:
+ *
+ * tablename;[metadata]\n
+ * tablename2;[metadata]\n
+ *
+ * For v2 tables, the metadata is the chunks info such as
+ *
+ * goog-phish-shavar;a:10,14,30-40s:56,67
+ * goog-unwanted-shavar;a:1-3,5
+ *
+ * For v4 tables, base64 encoded state is currently the only info in the
+ * metadata (can be extended whenever necessary). For exmaple,
+ *
+ * goog-phish-proto;Cg0IARAGGAEiAzAwMTABEKqTARoCGAjT1gDD:oCGAjT1gDD\n
+ * goog-malware-proto;Cg0IAhAGGAEiAzAwMTABENCQARoCGAjx5Yty:BENCQARoCGAj\n
+ *
+ * Note that the metadata is colon-separated.
+ *
+ */
+ void getTables(in nsIUrlClassifierCallback c);
+
+ /**
+ * Set the nsIUrlClassifierCompleter object for a given table. This
+ * object will be used to request complete versions of partial
+ * hashes.
+ */
+ void setHashCompleter(in ACString tableName,
+ in nsIUrlClassifierHashCompleter completer);
+
+ /**
+ * Set the last update time for the given table. We use this to
+ * remember freshness past restarts. Time is in milliseconds since epoch.
+ */
+ void setLastUpdateTime(in ACString tableName,
+ in unsigned long long lastUpdateTime);
+
+ /**
+ * Forget the results that were used in the last DB update.
+ */
+ void clearLastResults();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Incremental update methods.
+ //
+ // An update to the database has the following steps:
+ //
+ // 1) The update process is started with beginUpdate(). The client
+ // passes an nsIUrlClassifierUpdateObserver object which will be
+ // notified as the update is processed by the dbservice.
+ // 2) The client sends an initial update stream to the dbservice,
+ // using beginStream/updateStream/finishStream.
+ // 3) While reading this initial update stream, the dbservice may
+ // request additional streams from the client as requested by the
+ // update stream.
+ // 4) For each additional update stream, the client feeds the
+ // contents to the dbservice using beginStream/updateStream/endStream.
+ // 5) Once all streams have been processed, the client calls
+ // finishUpdate. When the dbservice has finished processing
+ // all streams, it will notify the observer that the update process
+ // is complete.
+
+ /**
+ * Begin an update process. Will throw NS_ERROR_NOT_AVAILABLE if there
+ * is already an update in progress.
+ *
+ * @param updater The update observer tied to this update.
+ * @param tables A comma-separated list of tables included in this update.
+ */
+ void beginUpdate(in nsIUrlClassifierUpdateObserver updater,
+ in ACString tables);
+
+ /**
+ * Begin a stream update. This should be called once per url being
+ * fetched.
+ *
+ * @param table The table the contents of this stream will be associated
+ * with, or empty for the initial stream.
+ */
+ void beginStream(in ACString table);
+
+ /**
+ * Update the table incrementally.
+ */
+ void updateStream(in ACString updateChunk);
+
+ // It would be nice to have an updateFromStream method to round out the
+ // interface, but it's tricky because of XPCOM proxies.
+
+ /**
+ * Finish an individual stream update. Must be called for every
+ * beginStream() call, before the next beginStream() or finishUpdate().
+ *
+ * The update observer's streamFinished will be called once the
+ * stream has been processed.
+ */
+ void finishStream();
+
+ /**
+ * Finish an incremental update. This will attempt to commit any
+ * pending changes and resets the update interface.
+ *
+ * The update observer's updateSucceeded or updateError methods
+ * will be called when the update has been processed.
+ */
+ void finishUpdate();
+
+ /**
+ * Cancel an incremental update. This rolls back any pending changes.
+ * and resets the update interface.
+ *
+ * The update observer's updateError method will be called when the
+ * update has been rolled back.
+ */
+ void cancelUpdate();
+
+ /**
+ * Reset the url-classifier database. This call will delete the existing
+ * database, emptying all tables. Mostly intended for use in unit tests.
+ */
+ void resetDatabase();
+
+ /**
+ * Reload he url-classifier database. This will empty all cache for
+ * completions from gethash, and reload it from database. Mostly intended
+ * for use in tests.
+ */
+ void reloadDatabase();
+};
+
+/**
+ * This is an internal helper interface for communication between the
+ * main thread and the dbservice worker thread. It is called for each
+ * lookup to provide a set of possible results, which the main thread
+ * may need to expand using an nsIUrlClassifierCompleter.
+ */
+[uuid(b903dc8f-dff1-42fe-894b-36e7a59bb801)]
+interface nsIUrlClassifierLookupCallback : nsISupports
+{
+ /**
+ * The lookup process is complete.
+ *
+ * @param results
+ * If this parameter is null, there were no results found.
+ * If not, it contains an array of nsUrlClassifierEntry objects
+ * with possible matches. The callee is responsible for freeing
+ * this array.
+ */
+ void lookupComplete(in ResultArray results);
+};
diff --git a/toolkit/components/url-classifier/nsIUrlClassifierHashCompleter.idl b/toolkit/components/url-classifier/nsIUrlClassifierHashCompleter.idl
new file mode 100644
index 0000000000..a3a8ab6171
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlClassifierHashCompleter.idl
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+/**
+ * This interface is implemented by nsIUrlClassifierHashCompleter clients.
+ */
+[scriptable, uuid(da16de40-df26-414d-bde7-c4faf4504868)]
+interface nsIUrlClassifierHashCompleterCallback : nsISupports
+{
+ /**
+ * A complete hash has been found that matches the partial hash.
+ * This method may be called 0-n times for a given
+ * nsIUrlClassifierCompleter::complete() call.
+ *
+ * @param hash
+ * The 128-bit hash that was discovered.
+ * @param table
+ * The name of the table that this hash belongs to.
+ * @param chunkId
+ * The database chunk that this hash belongs to.
+ */
+ void completion(in ACString hash,
+ in ACString table,
+ in uint32_t chunkId);
+
+ /**
+ * The completion is complete. This method is called once per
+ * nsIUrlClassifierCompleter::complete() call, after all completion()
+ * calls are finished.
+ *
+ * @param status
+ * NS_OK if the request completed successfully, or an error code.
+ */
+ void completionFinished(in nsresult status);
+};
+
+/**
+ * Clients updating the url-classifier database have the option of sending
+ * partial (32-bit) hashes of URL fragments to be blacklisted. If the
+ * url-classifier encounters one of these truncated hashes, it will ask an
+ * nsIUrlClassifierCompleter instance to asynchronously provide the complete
+ * hash, along with some associated metadata.
+ * This is only ever used for testing and should absolutely be deleted (I
+ * think).
+ */
+[scriptable, uuid(231fb2ad-ea8a-4e63-a331-eafc3b434811)]
+interface nsIUrlClassifierHashCompleter : nsISupports
+{
+ /**
+ * Request a completed hash from the given gethash url.
+ *
+ * @param partialHash
+ * The 32-bit hash encountered by the url-classifier.
+ * @param gethashUrl
+ * The gethash url to use.
+ * @param callback
+ * An nsIUrlClassifierCompleterCallback instance.
+ */
+ void complete(in ACString partialHash,
+ in ACString gethashUrl,
+ in nsIUrlClassifierHashCompleterCallback callback);
+};
diff --git a/toolkit/components/url-classifier/nsIUrlClassifierPrefixSet.idl b/toolkit/components/url-classifier/nsIUrlClassifierPrefixSet.idl
new file mode 100644
index 0000000000..7e1a527d76
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlClassifierPrefixSet.idl
@@ -0,0 +1,29 @@
+/* -*- 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"
+#include "nsIFile.idl"
+
+// Note that the PrefixSet name is historical and we do properly support
+// duplicated values, so it's really a Prefix Trie.
+// All methods are thread-safe.
+[scriptable, uuid(3d8579f0-75fa-4e00-ba41-38661d5b5d17)]
+interface nsIUrlClassifierPrefixSet : nsISupports
+{
+ // Initialize the PrefixSet. Give it a name for memory reporting.
+ void init(in ACString aName);
+ // Fills the PrefixSet with the given array of prefixes.
+ // Can send an empty Array to clear the tree.
+ // Requires array to be sorted.
+ void setPrefixes([const, array, size_is(aLength)] in unsigned long aPrefixes,
+ in unsigned long aLength);
+ void getPrefixes(out unsigned long aCount,
+ [array, size_is(aCount), retval] out unsigned long aPrefixes);
+ // Do a lookup in the PrefixSet, return whether the value is present.
+ boolean contains(in unsigned long aPrefix);
+ boolean isEmpty();
+ void loadFromFile(in nsIFile aFile);
+ void storeToFile(in nsIFile aFile);
+};
diff --git a/toolkit/components/url-classifier/nsIUrlClassifierStreamUpdater.idl b/toolkit/components/url-classifier/nsIUrlClassifierStreamUpdater.idl
new file mode 100644
index 0000000000..50844d0e03
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlClassifierStreamUpdater.idl
@@ -0,0 +1,39 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIUrlClassifierDBService.idl"
+
+/**
+ * This is a class to manage large table updates from the server. Rather than
+ * downloading the whole update and then updating the sqlite database, we
+ * update tables as the data is streaming in.
+ */
+[scriptable, uuid(e1797597-f4d6-4dd3-a1e1-745ad352cd80)]
+interface nsIUrlClassifierStreamUpdater : nsISupports
+{
+ /**
+ * Try to download updates from updateUrl. If an update is already in
+ * progress, queues the requested update. This is used in nsIUrlListManager
+ * as well as in testing.
+ * @param aRequestTables Comma-separated list of tables included in this
+ * update.
+ * @param aRequestPayload The payload for the request.
+ * @param aIsPostRequest Whether the request should be sent by POST method.
+ * Should be 'true' for v2 usage.
+ * @param aUpdateUrl The plaintext url from which to request updates.
+ * @param aSuccessCallback Called after a successful update.
+ * @param aUpdateErrorCallback Called for problems applying the update
+ * @param aDownloadErrorCallback Called if we get an http error or a
+ * connection refused error.
+ */
+ boolean downloadUpdates(in ACString aRequestTables,
+ in ACString aRequestPayload,
+ in boolean aIsPostRequest,
+ in ACString aUpdateUrl,
+ in nsIUrlClassifierCallback aSuccessCallback,
+ in nsIUrlClassifierCallback aUpdateErrorCallback,
+ in nsIUrlClassifierCallback aDownloadErrorCallback);
+};
diff --git a/toolkit/components/url-classifier/nsIUrlClassifierTable.idl b/toolkit/components/url-classifier/nsIUrlClassifierTable.idl
new file mode 100644
index 0000000000..123069556e
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlClassifierTable.idl
@@ -0,0 +1,31 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIUrlListManager.idl"
+
+// A map that contains a string keys mapped to string values.
+
+[scriptable, uuid(fd1f8334-1859-472d-b01f-4ac6b1121ce4)]
+interface nsIUrlClassifierTable : nsISupports
+{
+ /**
+ * The name used to identify this table
+ */
+ attribute ACString name;
+
+ /**
+ * Set to false if we don't want to update this table.
+ */
+ attribute boolean needsUpdate;
+
+ /**
+ * In the simple case, exists just looks up the string in the
+ * table and call the callback after the query returns with true or
+ * false. It's possible that something more complex happens
+ * (e.g., canonicalize the url).
+ */
+ void exists(in ACString key, in nsIUrlListManagerCallback cb);
+};
diff --git a/toolkit/components/url-classifier/nsIUrlClassifierUtils.idl b/toolkit/components/url-classifier/nsIUrlClassifierUtils.idl
new file mode 100644
index 0000000000..fa872ec27f
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlClassifierUtils.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+/**
+ * Some utility methods used by the url classifier.
+ */
+
+interface nsIURI;
+
+[scriptable, uuid(e4f0e59c-b922-48b0-a7b6-1735c1f96fed)]
+interface nsIUrlClassifierUtils : nsISupports
+{
+ /**
+ * Get the lookup string for a given URI. This normalizes the hostname,
+ * url-decodes the string, and strips off the protocol.
+ *
+ * @param uri URI to get the lookup key for.
+ *
+ * @returns String containing the canonicalized URI.
+ */
+ ACString getKeyForURI(in nsIURI uri);
+
+ /**
+ * Get the provider by table name.
+ *
+ * @param tableName The table name that we want to lookup
+ *
+ * @returns the provider name that the given table belongs.
+ */
+ ACString getProvider(in ACString tableName);
+
+ /**
+ * Get the protocol version for the given provider.
+ *
+ * @param provider String the provider name. e.g. "google"
+ *
+ * @returns String to indicate the protocol version. e.g. "2.2"
+ */
+ ACString getProtocolVersion(in ACString provider);
+
+ /**
+ * Convert threat type to list name.
+ *
+ * @param Integer to indicate threat type.
+ *
+ * @returns The list names separated by ','. For example,
+ * 'goog-phish-proto,test-phish-proto'.
+ */
+ ACString convertThreatTypeToListNames(in uint32_t threatType);
+
+ /**
+ * Convert list name to threat type.
+ *
+ * @param The list name.
+ *
+ * @returns The threat type in integer.
+ */
+ uint32_t convertListNameToThreatType(in ACString listName);
+
+ /**
+ * Make update request for given lists and their states.
+ *
+ * @param aListNames An array of list name represented in string.
+ * @param aState An array of states (encoded in base64 format) for each list.
+ * @param aCount The array length of aList and aState.
+ *
+ * @returns A base64url encoded string.
+ */
+ ACString makeUpdateRequestV4([array, size_is(aCount)] in string aListNames,
+ [array, size_is(aCount)] in string aStatesBase64,
+ in uint32_t aCount);
+};
diff --git a/toolkit/components/url-classifier/nsIUrlListManager.idl b/toolkit/components/url-classifier/nsIUrlListManager.idl
new file mode 100644
index 0000000000..112c567dcd
--- /dev/null
+++ b/toolkit/components/url-classifier/nsIUrlListManager.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIPrincipal;
+
+/**
+ * Interface for a class that manages updates of the url classifier database.
+ */
+
+// Interface for JS function callbacks
+[scriptable, function, uuid(fa4caf12-d057-4e7e-81e9-ce066ceee90b)]
+interface nsIUrlListManagerCallback : nsISupports {
+ void handleEvent(in ACString value);
+};
+
+
+[scriptable, uuid(d60a08ee-5c83-4eb6-bdfb-79fd0716501e)]
+interface nsIUrlListManager : nsISupports
+{
+ /**
+ * Get the gethash url for this table
+ */
+ ACString getGethashUrl(in ACString tableName);
+
+ /**
+ * Add a table to the list of tables we are managing. The name is a
+ * string of the format provider_name-semantic_type-table_type. For
+ * @param tableName A string of the format
+ * provider_name-semantic_type-table_type. For example,
+ * goog-white-enchash or goog-black-url.
+ * @param providerName The name of the entity providing the list.
+ * @param updateUrl The URL from which to fetch updates.
+ * @param gethashUrl The URL from which to fetch hash completions.
+ */
+ boolean registerTable(in ACString tableName,
+ in ACString providerName,
+ in ACString updateUrl,
+ in ACString gethashUrl);
+
+ /**
+ * Turn on update checking for a table. I.e., during the next server
+ * check, download updates for this table.
+ */
+ void enableUpdate(in ACString tableName);
+
+ /**
+ * Turn off update checking for a table.
+ */
+ void disableUpdate(in ACString tableName);
+
+ /**
+ * Toggle update checking, if necessary.
+ */
+ void maybeToggleUpdateChecking();
+
+ /**
+ * Lookup a key. Should not raise exceptions. Calls the callback
+ * function with a comma-separated list of tables to which the key
+ * belongs.
+ */
+ void safeLookup(in nsIPrincipal key,
+ in nsIUrlListManagerCallback cb);
+};
diff --git a/toolkit/components/url-classifier/nsURLClassifier.manifest b/toolkit/components/url-classifier/nsURLClassifier.manifest
new file mode 100644
index 0000000000..f035dea809
--- /dev/null
+++ b/toolkit/components/url-classifier/nsURLClassifier.manifest
@@ -0,0 +1,6 @@
+component {26a4a019-2827-4a89-a85c-5931a678823a} nsUrlClassifierLib.js
+contract @mozilla.org/url-classifier/jslib;1 {26a4a019-2827-4a89-a85c-5931a678823a}
+component {ca168834-cc00-48f9-b83c-fd018e58cae3} nsUrlClassifierListManager.js
+contract @mozilla.org/url-classifier/listmanager;1 {ca168834-cc00-48f9-b83c-fd018e58cae3}
+component {9111de73-9322-4bfc-8b65-2b727f3e6ec8} nsUrlClassifierHashCompleter.js
+contract @mozilla.org/url-classifier/hashcompleter;1 {9111de73-9322-4bfc-8b65-2b727f3e6ec8}
diff --git a/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp b/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp
new file mode 100644
index 0000000000..2ad8b6b515
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierDBService.cpp
@@ -0,0 +1,1866 @@
+//* -*- 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 "nsAutoPtr.h"
+#include "nsCOMPtr.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsCRT.h"
+#include "nsICryptoHash.h"
+#include "nsICryptoHMAC.h"
+#include "nsIDirectoryService.h"
+#include "nsIKeyModule.h"
+#include "nsIObserverService.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIProperties.h"
+#include "nsToolkitCompsCID.h"
+#include "nsIUrlClassifierUtils.h"
+#include "nsIXULRuntime.h"
+#include "nsUrlClassifierDBService.h"
+#include "nsUrlClassifierUtils.h"
+#include "nsUrlClassifierProxies.h"
+#include "nsURILoader.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsTArray.h"
+#include "nsNetCID.h"
+#include "nsThreadUtils.h"
+#include "nsXPCOMStrings.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Logging.h"
+#include "prprf.h"
+#include "prnetdb.h"
+#include "Entries.h"
+#include "HashStore.h"
+#include "Classifier.h"
+#include "ProtocolParser.h"
+#include "mozilla/Attributes.h"
+#include "nsIPrincipal.h"
+#include "Classifier.h"
+#include "ProtocolParser.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+nsresult
+TablesToResponse(const nsACString& tables)
+{
+ if (tables.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // We don't check mCheckMalware and friends because BuildTables never
+ // includes a table that is not enabled.
+ if (FindInReadable(NS_LITERAL_CSTRING("-malware-"), tables)) {
+ return NS_ERROR_MALWARE_URI;
+ }
+ if (FindInReadable(NS_LITERAL_CSTRING("-phish-"), tables)) {
+ return NS_ERROR_PHISHING_URI;
+ }
+ if (FindInReadable(NS_LITERAL_CSTRING("-unwanted-"), tables)) {
+ return NS_ERROR_UNWANTED_URI;
+ }
+ if (FindInReadable(NS_LITERAL_CSTRING("-track-"), tables)) {
+ return NS_ERROR_TRACKING_URI;
+ }
+ if (FindInReadable(NS_LITERAL_CSTRING("-block-"), tables)) {
+ return NS_ERROR_BLOCKED_URI;
+ }
+ return NS_OK;
+}
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+// MOZ_LOG=UrlClassifierDbService:5
+LazyLogModule gUrlClassifierDbServiceLog("UrlClassifierDbService");
+#define LOG(args) MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug)
+
+// Prefs for implementing nsIURIClassifier to block page loads
+#define CHECK_MALWARE_PREF "browser.safebrowsing.malware.enabled"
+#define CHECK_MALWARE_DEFAULT false
+
+#define CHECK_PHISHING_PREF "browser.safebrowsing.phishing.enabled"
+#define CHECK_PHISHING_DEFAULT false
+
+#define CHECK_TRACKING_PREF "privacy.trackingprotection.enabled"
+#define CHECK_TRACKING_DEFAULT false
+
+#define CHECK_TRACKING_PB_PREF "privacy.trackingprotection.pbmode.enabled"
+#define CHECK_TRACKING_PB_DEFAULT false
+
+#define CHECK_BLOCKED_PREF "browser.safebrowsing.blockedURIs.enabled"
+#define CHECK_BLOCKED_DEFAULT false
+
+#define GETHASH_NOISE_PREF "urlclassifier.gethashnoise"
+#define GETHASH_NOISE_DEFAULT 4
+
+// Comma-separated lists
+#define MALWARE_TABLE_PREF "urlclassifier.malwareTable"
+#define PHISH_TABLE_PREF "urlclassifier.phishTable"
+#define TRACKING_TABLE_PREF "urlclassifier.trackingTable"
+#define TRACKING_WHITELIST_TABLE_PREF "urlclassifier.trackingWhitelistTable"
+#define BLOCKED_TABLE_PREF "urlclassifier.blockedTable"
+#define DOWNLOAD_BLOCK_TABLE_PREF "urlclassifier.downloadBlockTable"
+#define DOWNLOAD_ALLOW_TABLE_PREF "urlclassifier.downloadAllowTable"
+#define DISALLOW_COMPLETION_TABLE_PREF "urlclassifier.disallow_completions"
+
+#define CONFIRM_AGE_PREF "urlclassifier.max-complete-age"
+#define CONFIRM_AGE_DEFAULT_SEC (45 * 60)
+
+class nsUrlClassifierDBServiceWorker;
+
+// Singleton instance.
+static nsUrlClassifierDBService* sUrlClassifierDBService;
+
+nsIThread* nsUrlClassifierDBService::gDbBackgroundThread = nullptr;
+
+// Once we've committed to shutting down, don't do work in the background
+// thread.
+static bool gShuttingDownThread = false;
+
+static mozilla::Atomic<int32_t> gFreshnessGuarantee(CONFIRM_AGE_DEFAULT_SEC);
+
+NS_IMPL_ISUPPORTS(nsUrlClassifierDBServiceWorker,
+ nsIUrlClassifierDBService)
+
+nsUrlClassifierDBServiceWorker::nsUrlClassifierDBServiceWorker()
+ : mInStream(false)
+ , mGethashNoise(0)
+ , mPendingLookupLock("nsUrlClassifierDBServerWorker.mPendingLookupLock")
+{
+}
+
+nsUrlClassifierDBServiceWorker::~nsUrlClassifierDBServiceWorker()
+{
+ NS_ASSERTION(!mClassifier,
+ "Db connection not closed, leaking memory! Call CloseDb "
+ "to close the connection.");
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::Init(uint32_t aGethashNoise,
+ nsCOMPtr<nsIFile> aCacheDir)
+{
+ mGethashNoise = aGethashNoise;
+ mCacheDir = aCacheDir;
+
+ ResetUpdate();
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::QueueLookup(const nsACString& spec,
+ const nsACString& tables,
+ nsIUrlClassifierLookupCallback* callback)
+{
+ MutexAutoLock lock(mPendingLookupLock);
+
+ PendingLookup* lookup = mPendingLookups.AppendElement();
+ if (!lookup) return NS_ERROR_OUT_OF_MEMORY;
+
+ lookup->mStartTime = TimeStamp::Now();
+ lookup->mKey = spec;
+ lookup->mCallback = callback;
+ lookup->mTables = tables;
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::DoLocalLookup(const nsACString& spec,
+ const nsACString& tables,
+ LookupResultArray* results)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ MOZ_ASSERT(!NS_IsMainThread(), "DoLocalLookup must be on background thread");
+ if (!results) {
+ return NS_ERROR_FAILURE;
+ }
+ // Bail if we haven't been initialized on the background thread.
+ if (!mClassifier) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We ignore failures from Check because we'd rather return the
+ // results that were found than fail.
+ mClassifier->Check(spec, tables, gFreshnessGuarantee, *results);
+
+ LOG(("Found %d results.", results->Length()));
+ return NS_OK;
+}
+
+static nsCString
+ProcessLookupResults(LookupResultArray* results)
+{
+ // Build a stringified list of result tables.
+ nsTArray<nsCString> tables;
+ for (uint32_t i = 0; i < results->Length(); i++) {
+ LookupResult& result = results->ElementAt(i);
+ MOZ_ASSERT(!result.mNoise, "Lookup results should not have noise added");
+ LOG(("Found result from table %s", result.mTableName.get()));
+ if (tables.IndexOf(result.mTableName) == nsTArray<nsCString>::NoIndex) {
+ tables.AppendElement(result.mTableName);
+ }
+ }
+ nsAutoCString tableStr;
+ for (uint32_t i = 0; i < tables.Length(); i++) {
+ if (i != 0)
+ tableStr.Append(',');
+ tableStr.Append(tables[i]);
+ }
+ return tableStr;
+}
+
+/**
+ * Lookup up a key in the database is a two step process:
+ *
+ * a) First we look for any Entries in the database that might apply to this
+ * url. For each URL there are one or two possible domain names to check:
+ * the two-part domain name (example.com) and the three-part name
+ * (www.example.com). We check the database for both of these.
+ * b) If we find any entries, we check the list of fragments for that entry
+ * against the possible subfragments of the URL as described in the
+ * "Simplified Regular Expression Lookup" section of the protocol doc.
+ */
+nsresult
+nsUrlClassifierDBServiceWorker::DoLookup(const nsACString& spec,
+ const nsACString& tables,
+ nsIUrlClassifierLookupCallback* c)
+{
+ if (gShuttingDownThread) {
+ c->LookupComplete(nullptr);
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ PRIntervalTime clockStart = 0;
+ if (LOG_ENABLED()) {
+ clockStart = PR_IntervalNow();
+ }
+
+ nsAutoPtr<LookupResultArray> results(new LookupResultArray());
+ if (!results) {
+ c->LookupComplete(nullptr);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsresult rv = DoLocalLookup(spec, tables, results);
+ if (NS_FAILED(rv)) {
+ c->LookupComplete(nullptr);
+ return rv;
+ }
+
+ LOG(("Found %d results.", results->Length()));
+
+
+ if (LOG_ENABLED()) {
+ PRIntervalTime clockEnd = PR_IntervalNow();
+ LOG(("query took %dms\n",
+ PR_IntervalToMilliseconds(clockEnd - clockStart)));
+ }
+
+ nsAutoPtr<LookupResultArray> completes(new LookupResultArray());
+
+ for (uint32_t i = 0; i < results->Length(); i++) {
+ if (!mMissCache.Contains(results->ElementAt(i).hash.prefix)) {
+ completes->AppendElement(results->ElementAt(i));
+ }
+ }
+
+ for (uint32_t i = 0; i < completes->Length(); i++) {
+ if (!completes->ElementAt(i).Confirmed()) {
+ // We're going to be doing a gethash request, add some extra entries.
+ // Note that we cannot pass the first two by reference, because we
+ // add to completes, whicah can cause completes to reallocate and move.
+ AddNoise(completes->ElementAt(i).hash.prefix,
+ completes->ElementAt(i).mTableName,
+ mGethashNoise, *completes);
+ break;
+ }
+ }
+
+ // At this point ownership of 'results' is handed to the callback.
+ c->LookupComplete(completes.forget());
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::HandlePendingLookups()
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ MutexAutoLock lock(mPendingLookupLock);
+ while (mPendingLookups.Length() > 0) {
+ PendingLookup lookup = mPendingLookups[0];
+ mPendingLookups.RemoveElementAt(0);
+ {
+ MutexAutoUnlock unlock(mPendingLookupLock);
+ DoLookup(lookup.mKey, lookup.mTables, lookup.mCallback);
+ }
+ double lookupTime = (TimeStamp::Now() - lookup.mStartTime).ToMilliseconds();
+ Telemetry::Accumulate(Telemetry::URLCLASSIFIER_LOOKUP_TIME,
+ static_cast<uint32_t>(lookupTime));
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::AddNoise(const Prefix aPrefix,
+ const nsCString tableName,
+ uint32_t aCount,
+ LookupResultArray& results)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (aCount < 1) {
+ return NS_OK;
+ }
+
+ PrefixArray noiseEntries;
+ nsresult rv = mClassifier->ReadNoiseEntries(aPrefix, tableName,
+ aCount, &noiseEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < noiseEntries.Length(); i++) {
+ LookupResult *result = results.AppendElement();
+ if (!result)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ result->hash.prefix = noiseEntries[i];
+ result->mNoise = true;
+
+ result->mTableName.Assign(tableName);
+ }
+
+ return NS_OK;
+}
+
+// Lookup a key in the db.
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::Lookup(nsIPrincipal* aPrincipal,
+ const nsACString& aTables,
+ nsIUrlClassifierCallback* c)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ return HandlePendingLookups();
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::GetTables(nsIUrlClassifierCallback* c)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = OpenDb();
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Unable to open SafeBrowsing database");
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString response;
+ mClassifier->TableRequest(response);
+ LOG(("GetTables: %s", response.get()));
+ c->HandleEvent(response);
+
+ return rv;
+}
+
+void
+nsUrlClassifierDBServiceWorker::ResetStream()
+{
+ LOG(("ResetStream"));
+ mInStream = false;
+ mProtocolParser = nullptr;
+}
+
+void
+nsUrlClassifierDBServiceWorker::ResetUpdate()
+{
+ LOG(("ResetUpdate"));
+ mUpdateWaitSec = 0;
+ mUpdateStatus = NS_OK;
+ mUpdateObserver = nullptr;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::SetHashCompleter(const nsACString &tableName,
+ nsIUrlClassifierHashCompleter *completer)
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::BeginUpdate(nsIUrlClassifierUpdateObserver *observer,
+ const nsACString &tables)
+{
+ LOG(("nsUrlClassifierDBServiceWorker::BeginUpdate [%s]", PromiseFlatCString(tables).get()));
+
+ if (gShuttingDownThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ENSURE_STATE(!mUpdateObserver);
+
+ nsresult rv = OpenDb();
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Unable to open SafeBrowsing database");
+ return NS_ERROR_FAILURE;
+ }
+
+ mUpdateStatus = NS_OK;
+ mUpdateObserver = observer;
+ Classifier::SplitTables(tables, mUpdateTables);
+
+ return NS_OK;
+}
+
+// Called from the stream updater.
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::BeginStream(const nsACString &table)
+{
+ LOG(("nsUrlClassifierDBServiceWorker::BeginStream"));
+ MOZ_ASSERT(!NS_IsMainThread(), "Streaming must be on the background thread");
+
+ if (gShuttingDownThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ENSURE_STATE(mUpdateObserver);
+ NS_ENSURE_STATE(!mInStream);
+
+ mInStream = true;
+
+ NS_ASSERTION(!mProtocolParser, "Should not have a protocol parser.");
+
+ // Check if we should use protobuf to parse the update.
+ bool useProtobuf = false;
+ for (size_t i = 0; i < mUpdateTables.Length(); i++) {
+ bool isCurProtobuf =
+ StringEndsWith(mUpdateTables[i], NS_LITERAL_CSTRING("-proto"));
+
+ if (0 == i) {
+ // Use the first table name to decice if all the subsequent tables
+ // should be '-proto'.
+ useProtobuf = isCurProtobuf;
+ continue;
+ }
+
+ if (useProtobuf != isCurProtobuf) {
+ NS_WARNING("Cannot mix 'proto' tables with other types "
+ "within the same provider.");
+ break;
+ }
+ }
+
+ mProtocolParser = (useProtobuf ? static_cast<ProtocolParser*>(new ProtocolParserProtobuf())
+ : static_cast<ProtocolParser*>(new ProtocolParserV2()));
+ if (!mProtocolParser)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ mProtocolParser->Init(mCryptoHash);
+
+ if (!table.IsEmpty()) {
+ mProtocolParser->SetCurrentTable(table);
+ }
+
+ mProtocolParser->SetRequestedTables(mUpdateTables);
+
+ return NS_OK;
+}
+
+/**
+ * Updating the database:
+ *
+ * The Update() method takes a series of chunks separated with control data,
+ * as described in
+ * http://code.google.com/p/google-safe-browsing/wiki/Protocolv2Spec
+ *
+ * It will iterate through the control data until it reaches a chunk. By
+ * the time it reaches a chunk, it should have received
+ * a) the table to which this chunk applies
+ * b) the type of chunk (add, delete, expire add, expire delete).
+ * c) the chunk ID
+ * d) the length of the chunk.
+ *
+ * For add and subtract chunks, it needs to read the chunk data (expires
+ * don't have any data). Chunk data is a list of URI fragments whose
+ * encoding depends on the type of table (which is indicated by the end
+ * of the table name):
+ * a) tables ending with -exp are a zlib-compressed list of URI fragments
+ * separated by newlines.
+ * b) tables ending with -sha128 have the form
+ * [domain][N][frag0]...[fragN]
+ * 16 1 16 16
+ * If N is 0, the domain is reused as a fragment.
+ * c) any other tables are assumed to be a plaintext list of URI fragments
+ * separated by newlines.
+ *
+ * Update() can be fed partial data; It will accumulate data until there is
+ * enough to act on. Finish() should be called when there will be no more
+ * data.
+ */
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::UpdateStream(const nsACString& chunk)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ENSURE_STATE(mInStream);
+
+ HandlePendingLookups();
+
+ // Feed the chunk to the parser.
+ return mProtocolParser->AppendStream(chunk);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::FinishStream()
+{
+ if (gShuttingDownThread) {
+ LOG(("shutting down"));
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ENSURE_STATE(mInStream);
+ NS_ENSURE_STATE(mUpdateObserver);
+
+ mInStream = false;
+
+ mProtocolParser->End();
+
+ if (NS_SUCCEEDED(mProtocolParser->Status())) {
+ if (mProtocolParser->UpdateWaitSec()) {
+ mUpdateWaitSec = mProtocolParser->UpdateWaitSec();
+ }
+ // XXX: Only allow forwards from the initial update?
+ const nsTArray<ProtocolParser::ForwardedUpdate> &forwards =
+ mProtocolParser->Forwards();
+ for (uint32_t i = 0; i < forwards.Length(); i++) {
+ const ProtocolParser::ForwardedUpdate &forward = forwards[i];
+ mUpdateObserver->UpdateUrlRequested(forward.url, forward.table);
+ }
+ // Hold on to any TableUpdate objects that were created by the
+ // parser.
+ mTableUpdates.AppendElements(mProtocolParser->GetTableUpdates());
+ mProtocolParser->ForgetTableUpdates();
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ // The assignment involves no string copy since the source string is sharable.
+ mRawTableUpdates = mProtocolParser->GetRawTableUpdates();
+#endif
+ } else {
+ LOG(("nsUrlClassifierDBService::FinishStream Failed to parse the stream "
+ "using mProtocolParser."));
+ mUpdateStatus = mProtocolParser->Status();
+ }
+ mUpdateObserver->StreamFinished(mProtocolParser->Status(), 0);
+
+ if (NS_SUCCEEDED(mUpdateStatus)) {
+ if (mProtocolParser->ResetRequested()) {
+ mClassifier->ResetTables(Classifier::Clear_All, mUpdateTables);
+ }
+ }
+
+ mProtocolParser = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::FinishUpdate()
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_ENSURE_STATE(mUpdateObserver);
+
+ if (NS_SUCCEEDED(mUpdateStatus)) {
+ mUpdateStatus = ApplyUpdate();
+ } else {
+ LOG(("nsUrlClassifierDBServiceWorker::FinishUpdate() Not running "
+ "ApplyUpdate() since the update has already failed."));
+ }
+
+ mMissCache.Clear();
+
+ if (NS_SUCCEEDED(mUpdateStatus)) {
+ LOG(("Notifying success: %d", mUpdateWaitSec));
+ mUpdateObserver->UpdateSuccess(mUpdateWaitSec);
+ } else if (NS_ERROR_NOT_IMPLEMENTED == mUpdateStatus) {
+ LOG(("Treating NS_ERROR_NOT_IMPLEMENTED a successful update "
+ "but still mark it spoiled."));
+ mUpdateObserver->UpdateSuccess(0);
+ mClassifier->ResetTables(Classifier::Clear_Cache, mUpdateTables);
+ } else {
+ if (LOG_ENABLED()) {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(mUpdateStatus, errorName);
+ LOG(("Notifying error: %s (%d)", errorName.get(), mUpdateStatus));
+ }
+
+ mUpdateObserver->UpdateError(mUpdateStatus);
+ /*
+ * mark the tables as spoiled(clear cache in LookupCache), we don't want to
+ * block hosts longer than normal because our update failed
+ */
+ mClassifier->ResetTables(Classifier::Clear_Cache, mUpdateTables);
+ }
+ mUpdateObserver = nullptr;
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::ApplyUpdate()
+{
+ LOG(("nsUrlClassifierDBServiceWorker::ApplyUpdate()"));
+ nsresult rv = mClassifier->ApplyUpdates(&mTableUpdates);
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ if (NS_FAILED(rv) && NS_ERROR_OUT_OF_MEMORY != rv) {
+ mClassifier->DumpRawTableUpdates(mRawTableUpdates);
+ }
+ // Invalidate the raw table updates.
+ mRawTableUpdates = EmptyCString();
+#endif
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::ResetDatabase()
+{
+ nsresult rv = OpenDb();
+
+ if (NS_SUCCEEDED(rv)) {
+ mClassifier->Reset();
+ }
+
+ rv = CloseDb();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::ReloadDatabase()
+{
+ nsTArray<nsCString> tables;
+ nsTArray<int64_t> lastUpdateTimes;
+ nsresult rv = mClassifier->ActiveTables(tables);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We need to make sure lastupdatetime is set after reload database
+ // Otherwise request will be skipped if it is not confirmed.
+ for (uint32_t table = 0; table < tables.Length(); table++) {
+ lastUpdateTimes.AppendElement(mClassifier->GetLastUpdateTime(tables[table]));
+ }
+
+ // This will null out mClassifier
+ rv = CloseDb();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create new mClassifier and load prefixset and completions from disk.
+ rv = OpenDb();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t table = 0; table < tables.Length(); table++) {
+ int64_t time = lastUpdateTimes[table];
+ if (time) {
+ mClassifier->SetLastUpdateTime(tables[table], lastUpdateTimes[table]);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::CancelUpdate()
+{
+ LOG(("nsUrlClassifierDBServiceWorker::CancelUpdate"));
+
+ if (mUpdateObserver) {
+ LOG(("UpdateObserver exists, cancelling"));
+
+ mUpdateStatus = NS_BINDING_ABORTED;
+
+ mUpdateObserver->UpdateError(mUpdateStatus);
+
+ /*
+ * mark the tables as spoiled(clear cache in LookupCache), we don't want to
+ * block hosts longer than normal because our update failed
+ */
+ mClassifier->ResetTables(Classifier::Clear_Cache, mUpdateTables);
+
+ ResetStream();
+ ResetUpdate();
+ } else {
+ LOG(("No UpdateObserver, nothing to cancel"));
+ }
+
+ return NS_OK;
+}
+
+// Allows the main thread to delete the connection which may be in
+// a background thread.
+// XXX This could be turned into a single shutdown event so the logic
+// is simpler in nsUrlClassifierDBService::Shutdown.
+nsresult
+nsUrlClassifierDBServiceWorker::CloseDb()
+{
+ if (mClassifier) {
+ mClassifier->Close();
+ mClassifier = nullptr;
+ }
+
+ mCryptoHash = nullptr;
+ LOG(("urlclassifier db closed\n"));
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::CacheCompletions(CacheResultArray *results)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ LOG(("nsUrlClassifierDBServiceWorker::CacheCompletions [%p]", this));
+ if (!mClassifier)
+ return NS_OK;
+
+ // Ownership is transferred in to us
+ nsAutoPtr<CacheResultArray> resultsPtr(results);
+
+ if (mLastResults == *resultsPtr) {
+ LOG(("Skipping completions that have just been cached already."));
+ return NS_OK;
+ }
+
+ nsAutoPtr<ProtocolParserV2> pParse(new ProtocolParserV2());
+ nsTArray<TableUpdate*> updates;
+
+ // Only cache results for tables that we have, don't take
+ // in tables we might accidentally have hit during a completion.
+ // This happens due to goog vs googpub lists existing.
+ nsTArray<nsCString> tables;
+ nsresult rv = mClassifier->ActiveTables(tables);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < resultsPtr->Length(); i++) {
+ bool activeTable = false;
+ for (uint32_t table = 0; table < tables.Length(); table++) {
+ if (tables[table].Equals(resultsPtr->ElementAt(i).table)) {
+ activeTable = true;
+ break;
+ }
+ }
+ if (activeTable) {
+ TableUpdateV2* tuV2 = TableUpdate::Cast<TableUpdateV2>(
+ pParse->GetTableUpdate(resultsPtr->ElementAt(i).table));
+
+ NS_ENSURE_TRUE(tuV2, NS_ERROR_FAILURE);
+
+ LOG(("CacheCompletion Addchunk %d hash %X", resultsPtr->ElementAt(i).entry.addChunk,
+ resultsPtr->ElementAt(i).entry.ToUint32()));
+ rv = tuV2->NewAddComplete(resultsPtr->ElementAt(i).entry.addChunk,
+ resultsPtr->ElementAt(i).entry.complete);
+ if (NS_FAILED(rv)) {
+ // We can bail without leaking here because ForgetTableUpdates
+ // hasn't been called yet.
+ return rv;
+ }
+ rv = tuV2->NewAddChunk(resultsPtr->ElementAt(i).entry.addChunk);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ updates.AppendElement(tuV2);
+ pParse->ForgetTableUpdates();
+ } else {
+ LOG(("Completion received, but table is not active, so not caching."));
+ }
+ }
+
+ mClassifier->ApplyFullHashes(&updates);
+ mLastResults = *resultsPtr;
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::CacheMisses(PrefixArray *results)
+{
+ LOG(("nsUrlClassifierDBServiceWorker::CacheMisses [%p] %d",
+ this, results->Length()));
+
+ // Ownership is transferred in to us
+ nsAutoPtr<PrefixArray> resultsPtr(results);
+
+ for (uint32_t i = 0; i < resultsPtr->Length(); i++) {
+ mMissCache.AppendElement(resultsPtr->ElementAt(i));
+ }
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::OpenDb()
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ MOZ_ASSERT(!NS_IsMainThread(), "Must initialize DB on background thread");
+ // Connection already open, don't do anything.
+ if (mClassifier) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ mCryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoPtr<Classifier> classifier(new Classifier());
+ if (!classifier) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ rv = classifier->Open(*mCacheDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mClassifier = classifier;
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBServiceWorker::SetLastUpdateTime(const nsACString &table,
+ uint64_t updateTime)
+{
+ MOZ_ASSERT(!NS_IsMainThread(), "Must be on the background thread");
+ MOZ_ASSERT(mClassifier, "Classifier connection must be opened");
+
+ mClassifier->SetLastUpdateTime(table, updateTime);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBServiceWorker::ClearLastResults()
+{
+ MOZ_ASSERT(!NS_IsMainThread(), "Must be on the background thread");
+ mLastResults.Clear();
+ return NS_OK;
+}
+
+
+// -------------------------------------------------------------------------
+// nsUrlClassifierLookupCallback
+//
+// This class takes the results of a lookup found on the worker thread
+// and handles any necessary partial hash expansions before calling
+// the client callback.
+
+class nsUrlClassifierLookupCallback final : public nsIUrlClassifierLookupCallback
+ , public nsIUrlClassifierHashCompleterCallback
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERLOOKUPCALLBACK
+ NS_DECL_NSIURLCLASSIFIERHASHCOMPLETERCALLBACK
+
+ nsUrlClassifierLookupCallback(nsUrlClassifierDBService *dbservice,
+ nsIUrlClassifierCallback *c)
+ : mDBService(dbservice)
+ , mResults(nullptr)
+ , mPendingCompletions(0)
+ , mCallback(c)
+ {}
+
+private:
+ ~nsUrlClassifierLookupCallback();
+
+ nsresult HandleResults();
+
+ RefPtr<nsUrlClassifierDBService> mDBService;
+ nsAutoPtr<LookupResultArray> mResults;
+
+ // Completed results to send back to the worker for caching.
+ nsAutoPtr<CacheResultArray> mCacheResults;
+
+ uint32_t mPendingCompletions;
+ nsCOMPtr<nsIUrlClassifierCallback> mCallback;
+};
+
+NS_IMPL_ISUPPORTS(nsUrlClassifierLookupCallback,
+ nsIUrlClassifierLookupCallback,
+ nsIUrlClassifierHashCompleterCallback)
+
+nsUrlClassifierLookupCallback::~nsUrlClassifierLookupCallback()
+{
+ if (mCallback) {
+ NS_ReleaseOnMainThread(mCallback.forget());
+ }
+}
+
+NS_IMETHODIMP
+nsUrlClassifierLookupCallback::LookupComplete(nsTArray<LookupResult>* results)
+{
+ NS_ASSERTION(mResults == nullptr,
+ "Should only get one set of results per nsUrlClassifierLookupCallback!");
+
+ if (!results) {
+ HandleResults();
+ return NS_OK;
+ }
+
+ mResults = results;
+
+ // Check the results entries that need to be completed.
+ for (uint32_t i = 0; i < results->Length(); i++) {
+ LookupResult& result = results->ElementAt(i);
+
+ // We will complete partial matches and matches that are stale.
+ if (!result.Confirmed()) {
+ nsCOMPtr<nsIUrlClassifierHashCompleter> completer;
+ nsCString gethashUrl;
+ nsresult rv;
+ nsCOMPtr<nsIUrlListManager> listManager = do_GetService(
+ "@mozilla.org/url-classifier/listmanager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = listManager->GetGethashUrl(result.mTableName, gethashUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LOG(("The match from %s needs to be completed at %s",
+ result.mTableName.get(), gethashUrl.get()));
+ // gethashUrls may be empty in 2 cases: test tables, and on startup where
+ // we may have found a prefix in an existing table before the listmanager
+ // has registered the table. In the second case we should not call
+ // complete.
+ if ((!gethashUrl.IsEmpty() ||
+ StringBeginsWith(result.mTableName, NS_LITERAL_CSTRING("test-"))) &&
+ mDBService->GetCompleter(result.mTableName,
+ getter_AddRefs(completer))) {
+ nsAutoCString partialHash;
+ partialHash.Assign(reinterpret_cast<char*>(&result.hash.prefix),
+ PREFIX_SIZE);
+
+ nsresult rv = completer->Complete(partialHash, gethashUrl, this);
+ if (NS_SUCCEEDED(rv)) {
+ mPendingCompletions++;
+ }
+ } else {
+ // For tables with no hash completer, a complete hash match is
+ // good enough, we'll consider it fresh, even if it hasn't been updated
+ // in 45 minutes.
+ if (result.Complete()) {
+ result.mFresh = true;
+ LOG(("Skipping completion in a table without a valid completer (%s).",
+ result.mTableName.get()));
+ } else {
+ NS_WARNING("Partial match in a table without a valid completer, ignoring partial match.");
+ }
+ }
+ }
+ }
+
+ LOG(("nsUrlClassifierLookupCallback::LookupComplete [%p] "
+ "%u pending completions", this, mPendingCompletions));
+ if (mPendingCompletions == 0) {
+ // All results were complete, we're ready!
+ HandleResults();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierLookupCallback::CompletionFinished(nsresult status)
+{
+ if (LOG_ENABLED()) {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(status, errorName);
+ LOG(("nsUrlClassifierLookupCallback::CompletionFinished [%p, %s]",
+ this, errorName.get()));
+ }
+
+ mPendingCompletions--;
+ if (mPendingCompletions == 0) {
+ HandleResults();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierLookupCallback::Completion(const nsACString& completeHash,
+ const nsACString& tableName,
+ uint32_t chunkId)
+{
+ LOG(("nsUrlClassifierLookupCallback::Completion [%p, %s, %d]",
+ this, PromiseFlatCString(tableName).get(), chunkId));
+ mozilla::safebrowsing::Completion hash;
+ hash.Assign(completeHash);
+
+ // Send this completion to the store for caching.
+ if (!mCacheResults) {
+ mCacheResults = new CacheResultArray();
+ if (!mCacheResults)
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ CacheResult result;
+ result.entry.addChunk = chunkId;
+ result.entry.complete = hash;
+ result.table = tableName;
+
+ // OK if this fails, we just won't cache the item.
+ mCacheResults->AppendElement(result);
+
+ // Check if this matched any of our results.
+ for (uint32_t i = 0; i < mResults->Length(); i++) {
+ LookupResult& result = mResults->ElementAt(i);
+
+ // Now, see if it verifies a lookup
+ if (!result.mNoise
+ && result.CompleteHash() == hash
+ && result.mTableName.Equals(tableName)) {
+ result.mProtocolConfirmed = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierLookupCallback::HandleResults()
+{
+ if (!mResults) {
+ // No results, this URI is clean.
+ LOG(("nsUrlClassifierLookupCallback::HandleResults [%p, no results]", this));
+ return mCallback->HandleEvent(NS_LITERAL_CSTRING(""));
+ }
+ MOZ_ASSERT(mPendingCompletions == 0, "HandleResults() should never be "
+ "called while there are pending completions");
+
+ LOG(("nsUrlClassifierLookupCallback::HandleResults [%p, %u results]",
+ this, mResults->Length()));
+
+ nsTArray<nsCString> tables;
+ // Build a stringified list of result tables.
+ for (uint32_t i = 0; i < mResults->Length(); i++) {
+ LookupResult& result = mResults->ElementAt(i);
+
+ // Leave out results that weren't confirmed, as their existence on
+ // the list can't be verified. Also leave out randomly-generated
+ // noise.
+ if (result.mNoise) {
+ LOG(("Skipping result %X from table %s (noise)",
+ result.hash.prefix.ToUint32(), result.mTableName.get()));
+ continue;
+ } else if (!result.Confirmed()) {
+ LOG(("Skipping result %X from table %s (not confirmed)",
+ result.hash.prefix.ToUint32(), result.mTableName.get()));
+ continue;
+ }
+
+ LOG(("Confirmed result %X from table %s",
+ result.hash.prefix.ToUint32(), result.mTableName.get()));
+
+ if (tables.IndexOf(result.mTableName) == nsTArray<nsCString>::NoIndex) {
+ tables.AppendElement(result.mTableName);
+ }
+ }
+
+ // Some parts of this gethash request generated no hits at all.
+ // Prefixes must have been removed from the database since our last update.
+ // Save the prefixes we checked to prevent repeated requests
+ // until the next update.
+ nsAutoPtr<PrefixArray> cacheMisses(new PrefixArray());
+ if (cacheMisses) {
+ for (uint32_t i = 0; i < mResults->Length(); i++) {
+ LookupResult &result = mResults->ElementAt(i);
+ if (!result.Confirmed() && !result.mNoise) {
+ cacheMisses->AppendElement(result.PrefixHash());
+ }
+ }
+ // Hands ownership of the miss array back to the worker thread.
+ mDBService->CacheMisses(cacheMisses.forget());
+ }
+
+ if (mCacheResults) {
+ // This hands ownership of the cache results array back to the worker
+ // thread.
+ mDBService->CacheCompletions(mCacheResults.forget());
+ }
+
+ nsAutoCString tableStr;
+ for (uint32_t i = 0; i < tables.Length(); i++) {
+ if (i != 0)
+ tableStr.Append(',');
+ tableStr.Append(tables[i]);
+ }
+
+ return mCallback->HandleEvent(tableStr);
+}
+
+
+// -------------------------------------------------------------------------
+// Helper class for nsIURIClassifier implementation, translates table names
+// to nsIURIClassifier enums.
+
+class nsUrlClassifierClassifyCallback final : public nsIUrlClassifierCallback
+{
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERCALLBACK
+
+ explicit nsUrlClassifierClassifyCallback(nsIURIClassifierCallback *c)
+ : mCallback(c)
+ {}
+
+private:
+ ~nsUrlClassifierClassifyCallback() {}
+
+ nsCOMPtr<nsIURIClassifierCallback> mCallback;
+};
+
+NS_IMPL_ISUPPORTS(nsUrlClassifierClassifyCallback,
+ nsIUrlClassifierCallback)
+
+NS_IMETHODIMP
+nsUrlClassifierClassifyCallback::HandleEvent(const nsACString& tables)
+{
+ nsresult response = TablesToResponse(tables);
+ mCallback->OnClassifyComplete(response);
+ return NS_OK;
+}
+
+
+// -------------------------------------------------------------------------
+// Proxy class implementation
+
+NS_IMPL_ISUPPORTS(nsUrlClassifierDBService,
+ nsIUrlClassifierDBService,
+ nsIURIClassifier,
+ nsIObserver)
+
+/* static */ nsUrlClassifierDBService*
+nsUrlClassifierDBService::GetInstance(nsresult *result)
+{
+ *result = NS_OK;
+ if (!sUrlClassifierDBService) {
+ sUrlClassifierDBService = new nsUrlClassifierDBService();
+ if (!sUrlClassifierDBService) {
+ *result = NS_ERROR_OUT_OF_MEMORY;
+ return nullptr;
+ }
+
+ NS_ADDREF(sUrlClassifierDBService); // addref the global
+
+ *result = sUrlClassifierDBService->Init();
+ if (NS_FAILED(*result)) {
+ NS_RELEASE(sUrlClassifierDBService);
+ return nullptr;
+ }
+ } else {
+ // Already exists, just add a ref
+ NS_ADDREF(sUrlClassifierDBService); // addref the return result
+ }
+ return sUrlClassifierDBService;
+}
+
+
+nsUrlClassifierDBService::nsUrlClassifierDBService()
+ : mCheckMalware(CHECK_MALWARE_DEFAULT)
+ , mCheckPhishing(CHECK_PHISHING_DEFAULT)
+ , mCheckTracking(CHECK_TRACKING_DEFAULT)
+ , mCheckBlockedURIs(CHECK_BLOCKED_DEFAULT)
+ , mInUpdate(false)
+{
+}
+
+nsUrlClassifierDBService::~nsUrlClassifierDBService()
+{
+ sUrlClassifierDBService = nullptr;
+}
+
+nsresult
+nsUrlClassifierDBService::ReadTablesFromPrefs()
+{
+ nsCString allTables;
+ nsCString tables;
+ Preferences::GetCString(PHISH_TABLE_PREF, &allTables);
+
+ Preferences::GetCString(MALWARE_TABLE_PREF, &tables);
+ if (!tables.IsEmpty()) {
+ allTables.Append(',');
+ allTables.Append(tables);
+ }
+
+ Preferences::GetCString(DOWNLOAD_BLOCK_TABLE_PREF, &tables);
+ if (!tables.IsEmpty()) {
+ allTables.Append(',');
+ allTables.Append(tables);
+ }
+
+ Preferences::GetCString(DOWNLOAD_ALLOW_TABLE_PREF, &tables);
+ if (!tables.IsEmpty()) {
+ allTables.Append(',');
+ allTables.Append(tables);
+ }
+
+ Preferences::GetCString(TRACKING_TABLE_PREF, &tables);
+ if (!tables.IsEmpty()) {
+ allTables.Append(',');
+ allTables.Append(tables);
+ }
+
+ Preferences::GetCString(TRACKING_WHITELIST_TABLE_PREF, &tables);
+ if (!tables.IsEmpty()) {
+ allTables.Append(',');
+ allTables.Append(tables);
+ }
+
+ Preferences::GetCString(BLOCKED_TABLE_PREF, &tables);
+ if (!tables.IsEmpty()) {
+ allTables.Append(',');
+ allTables.Append(tables);
+ }
+
+ Classifier::SplitTables(allTables, mGethashTables);
+
+ Preferences::GetCString(DISALLOW_COMPLETION_TABLE_PREF, &tables);
+ Classifier::SplitTables(tables, mDisallowCompletionsTables);
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierDBService::Init()
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Must initialize DB service on main thread");
+ nsCOMPtr<nsIXULRuntime> appInfo = do_GetService("@mozilla.org/xre/app-info;1");
+ if (appInfo) {
+ bool inSafeMode = false;
+ appInfo->GetInSafeMode(&inSafeMode);
+ if (inSafeMode) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+
+ // Retrieve all the preferences.
+ mCheckMalware = Preferences::GetBool(CHECK_MALWARE_PREF,
+ CHECK_MALWARE_DEFAULT);
+ mCheckPhishing = Preferences::GetBool(CHECK_PHISHING_PREF,
+ CHECK_PHISHING_DEFAULT);
+ mCheckTracking =
+ Preferences::GetBool(CHECK_TRACKING_PREF, CHECK_TRACKING_DEFAULT) ||
+ Preferences::GetBool(CHECK_TRACKING_PB_PREF, CHECK_TRACKING_PB_DEFAULT);
+ mCheckBlockedURIs = Preferences::GetBool(CHECK_BLOCKED_PREF,
+ CHECK_BLOCKED_DEFAULT);
+ uint32_t gethashNoise = Preferences::GetUint(GETHASH_NOISE_PREF,
+ GETHASH_NOISE_DEFAULT);
+ gFreshnessGuarantee = Preferences::GetInt(CONFIRM_AGE_PREF,
+ CONFIRM_AGE_DEFAULT_SEC);
+ ReadTablesFromPrefs();
+
+ nsresult rv;
+
+ {
+ // Force PSM loading on main thread
+ nsCOMPtr<nsICryptoHash> dummy = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ {
+ // Force nsIUrlClassifierUtils loading on main thread.
+ nsCOMPtr<nsIUrlClassifierUtils> dummy =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Directory providers must also be accessed on the main thread.
+ nsCOMPtr<nsIFile> cacheDir;
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR,
+ getter_AddRefs(cacheDir));
+ if (NS_FAILED(rv)) {
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(cacheDir));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Start the background thread.
+ rv = NS_NewNamedThread("URL Classifier", &gDbBackgroundThread);
+ if (NS_FAILED(rv))
+ return rv;
+
+ mWorker = new nsUrlClassifierDBServiceWorker();
+ if (!mWorker)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ rv = mWorker->Init(gethashNoise, cacheDir);
+ if (NS_FAILED(rv)) {
+ mWorker = nullptr;
+ return rv;
+ }
+
+ // Proxy for calling the worker on the background thread
+ mWorkerProxy = new UrlClassifierDBServiceWorkerProxy(mWorker);
+ rv = mWorkerProxy->OpenDb();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add an observer for shutdown
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService)
+ return NS_ERROR_FAILURE;
+
+ // The application is about to quit
+ observerService->AddObserver(this, "quit-application", false);
+ observerService->AddObserver(this, "profile-before-change", false);
+
+ // XXX: Do we *really* need to be able to change all of these at runtime?
+ // Note: These observers should only be added when everything else above has
+ // succeeded. Failing to do so can cause long shutdown times in certain
+ // situations. See Bug 1247798 and Bug 1244803.
+ Preferences::AddStrongObserver(this, CHECK_MALWARE_PREF);
+ Preferences::AddStrongObserver(this, CHECK_PHISHING_PREF);
+ Preferences::AddStrongObserver(this, CHECK_TRACKING_PREF);
+ Preferences::AddStrongObserver(this, CHECK_TRACKING_PB_PREF);
+ Preferences::AddStrongObserver(this, CHECK_BLOCKED_PREF);
+ Preferences::AddStrongObserver(this, GETHASH_NOISE_PREF);
+ Preferences::AddStrongObserver(this, CONFIRM_AGE_PREF);
+ Preferences::AddStrongObserver(this, PHISH_TABLE_PREF);
+ Preferences::AddStrongObserver(this, MALWARE_TABLE_PREF);
+ Preferences::AddStrongObserver(this, TRACKING_TABLE_PREF);
+ Preferences::AddStrongObserver(this, TRACKING_WHITELIST_TABLE_PREF);
+ Preferences::AddStrongObserver(this, BLOCKED_TABLE_PREF);
+ Preferences::AddStrongObserver(this, DOWNLOAD_BLOCK_TABLE_PREF);
+ Preferences::AddStrongObserver(this, DOWNLOAD_ALLOW_TABLE_PREF);
+ Preferences::AddStrongObserver(this, DISALLOW_COMPLETION_TABLE_PREF);
+
+ return NS_OK;
+}
+
+void
+nsUrlClassifierDBService::BuildTables(bool aTrackingProtectionEnabled,
+ nsCString &tables)
+{
+ nsAutoCString malware;
+ // LookupURI takes a comma-separated list already.
+ Preferences::GetCString(MALWARE_TABLE_PREF, &malware);
+ if (mCheckMalware && !malware.IsEmpty()) {
+ tables.Append(malware);
+ }
+ nsAutoCString phishing;
+ Preferences::GetCString(PHISH_TABLE_PREF, &phishing);
+ if (mCheckPhishing && !phishing.IsEmpty()) {
+ tables.Append(',');
+ tables.Append(phishing);
+ }
+ if (aTrackingProtectionEnabled) {
+ nsAutoCString tracking, trackingWhitelist;
+ Preferences::GetCString(TRACKING_TABLE_PREF, &tracking);
+ if (!tracking.IsEmpty()) {
+ tables.Append(',');
+ tables.Append(tracking);
+ }
+ Preferences::GetCString(TRACKING_WHITELIST_TABLE_PREF, &trackingWhitelist);
+ if (!trackingWhitelist.IsEmpty()) {
+ tables.Append(',');
+ tables.Append(trackingWhitelist);
+ }
+ }
+ nsAutoCString blocked;
+ Preferences::GetCString(BLOCKED_TABLE_PREF, &blocked);
+ if (mCheckBlockedURIs && !blocked.IsEmpty()) {
+ tables.Append(',');
+ tables.Append(blocked);
+ }
+
+ if (StringBeginsWith(tables, NS_LITERAL_CSTRING(","))) {
+ tables.Cut(0, 1);
+ }
+}
+
+// nsChannelClassifier is the only consumer of this interface.
+NS_IMETHODIMP
+nsUrlClassifierDBService::Classify(nsIPrincipal* aPrincipal,
+ bool aTrackingProtectionEnabled,
+ nsIURIClassifierCallback* c,
+ bool* result)
+{
+ NS_ENSURE_ARG(aPrincipal);
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ if (!(mCheckMalware || mCheckPhishing || aTrackingProtectionEnabled ||
+ mCheckBlockedURIs)) {
+ *result = false;
+ return NS_OK;
+ }
+
+ RefPtr<nsUrlClassifierClassifyCallback> callback =
+ new nsUrlClassifierClassifyCallback(c);
+ if (!callback) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsAutoCString tables;
+ BuildTables(aTrackingProtectionEnabled, tables);
+
+ nsresult rv = LookupURI(aPrincipal, tables, callback, false, result);
+ if (rv == NS_ERROR_MALFORMED_URI) {
+ *result = false;
+ // The URI had no hostname, don't try to classify it.
+ return NS_OK;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::ClassifyLocalWithTables(nsIURI *aURI,
+ const nsACString & aTables,
+ nsACString & aTableResults)
+{
+ if (gShuttingDownThread) {
+ return NS_ERROR_ABORT;
+ }
+
+ PROFILER_LABEL_FUNC(js::ProfileEntry::Category::OTHER);
+ MOZ_ASSERT(NS_IsMainThread(), "ClassifyLocalWithTables must be on main thread");
+
+ nsCOMPtr<nsIURI> uri = NS_GetInnermostURI(aURI);
+ NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
+
+ nsAutoCString key;
+ // Canonicalize the url
+ nsCOMPtr<nsIUrlClassifierUtils> utilsService =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID);
+ nsresult rv = utilsService->GetKeyForURI(uri, key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoPtr<LookupResultArray> results(new LookupResultArray());
+ if (!results) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // In unittests, we may not have been initalized, so don't crash.
+ rv = mWorkerProxy->DoLocalLookup(key, aTables, results);
+ if (NS_SUCCEEDED(rv)) {
+ aTableResults = ProcessLookupResults(results);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::Lookup(nsIPrincipal* aPrincipal,
+ const nsACString& tables,
+ nsIUrlClassifierCallback* c)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ bool dummy;
+ return LookupURI(aPrincipal, tables, c, true, &dummy);
+}
+
+nsresult
+nsUrlClassifierDBService::LookupURI(nsIPrincipal* aPrincipal,
+ const nsACString& tables,
+ nsIUrlClassifierCallback* c,
+ bool forceLookup,
+ bool *didLookup)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+ NS_ENSURE_ARG(aPrincipal);
+
+ if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
+ *didLookup = false;
+ return NS_OK;
+ }
+
+ if (gShuttingDownThread) {
+ *didLookup = false;
+ return NS_ERROR_ABORT;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
+
+ uri = NS_GetInnermostURI(uri);
+ NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE);
+
+ nsAutoCString key;
+ // Canonicalize the url
+ nsCOMPtr<nsIUrlClassifierUtils> utilsService =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID);
+ rv = utilsService->GetKeyForURI(uri, key);
+ if (NS_FAILED(rv))
+ return rv;
+
+ if (forceLookup) {
+ *didLookup = true;
+ } else {
+ bool clean = false;
+
+ if (!clean) {
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ services::GetPermissionManager();
+
+ if (permissionManager) {
+ uint32_t perm;
+ rv = permissionManager->TestPermissionFromPrincipal(aPrincipal,
+ "safe-browsing", &perm);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ clean |= (perm == nsIPermissionManager::ALLOW_ACTION);
+ }
+ }
+
+ *didLookup = !clean;
+ if (clean) {
+ return NS_OK;
+ }
+ }
+
+ // Create an nsUrlClassifierLookupCallback object. This object will
+ // take care of confirming partial hash matches if necessary before
+ // calling the client's callback.
+ nsCOMPtr<nsIUrlClassifierLookupCallback> callback =
+ new nsUrlClassifierLookupCallback(this, c);
+ if (!callback)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ nsCOMPtr<nsIUrlClassifierLookupCallback> proxyCallback =
+ new UrlClassifierLookupCallbackProxy(callback);
+
+ // Queue this lookup and call the lookup function to flush the queue if
+ // necessary.
+ rv = mWorker->QueueLookup(key, tables, proxyCallback);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This seems to just call HandlePendingLookups.
+ nsAutoCString dummy;
+ return mWorkerProxy->Lookup(nullptr, dummy, nullptr);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::GetTables(nsIUrlClassifierCallback* c)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ // The proxy callback uses the current thread.
+ nsCOMPtr<nsIUrlClassifierCallback> proxyCallback =
+ new UrlClassifierCallbackProxy(c);
+
+ return mWorkerProxy->GetTables(proxyCallback);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::SetHashCompleter(const nsACString &tableName,
+ nsIUrlClassifierHashCompleter *completer)
+{
+ if (completer) {
+ mCompleters.Put(tableName, completer);
+ } else {
+ mCompleters.Remove(tableName);
+ }
+ ClearLastResults();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::SetLastUpdateTime(const nsACString &tableName,
+ uint64_t lastUpdateTime)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->SetLastUpdateTime(tableName, lastUpdateTime);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::ClearLastResults()
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->ClearLastResults();
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::BeginUpdate(nsIUrlClassifierUpdateObserver *observer,
+ const nsACString &updateTables)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ if (mInUpdate) {
+ LOG(("Already updating, not available"));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mInUpdate = true;
+
+ // The proxy observer uses the current thread
+ nsCOMPtr<nsIUrlClassifierUpdateObserver> proxyObserver =
+ new UrlClassifierUpdateObserverProxy(observer);
+
+ return mWorkerProxy->BeginUpdate(proxyObserver, updateTables);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::BeginStream(const nsACString &table)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->BeginStream(table);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::UpdateStream(const nsACString& aUpdateChunk)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->UpdateStream(aUpdateChunk);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::FinishStream()
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->FinishStream();
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::FinishUpdate()
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ mInUpdate = false;
+
+ return mWorkerProxy->FinishUpdate();
+}
+
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::CancelUpdate()
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ mInUpdate = false;
+
+ return mWorkerProxy->CancelUpdate();
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::ResetDatabase()
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->ResetDatabase();
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::ReloadDatabase()
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->ReloadDatabase();
+}
+
+nsresult
+nsUrlClassifierDBService::CacheCompletions(CacheResultArray *results)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->CacheCompletions(results);
+}
+
+nsresult
+nsUrlClassifierDBService::CacheMisses(PrefixArray *results)
+{
+ NS_ENSURE_TRUE(gDbBackgroundThread, NS_ERROR_NOT_INITIALIZED);
+
+ return mWorkerProxy->CacheMisses(results);
+}
+
+bool
+nsUrlClassifierDBService::GetCompleter(const nsACString &tableName,
+ nsIUrlClassifierHashCompleter **completer)
+{
+ // If we have specified a completer, go ahead and query it. This is only
+ // used by tests.
+ if (mCompleters.Get(tableName, completer)) {
+ return true;
+ }
+
+ // If we don't know about this table at all, or are disallowing completions
+ // for it, skip completion checks.
+ if (!mGethashTables.Contains(tableName) ||
+ mDisallowCompletionsTables.Contains(tableName)) {
+ return false;
+ }
+
+ // Otherwise, call gethash to find the hash completions.
+ return NS_SUCCEEDED(CallGetService(NS_URLCLASSIFIERHASHCOMPLETER_CONTRACTID,
+ completer));
+}
+
+NS_IMETHODIMP
+nsUrlClassifierDBService::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefs(do_QueryInterface(aSubject, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ Unused << prefs;
+
+ if (NS_LITERAL_STRING(CHECK_MALWARE_PREF).Equals(aData)) {
+ mCheckMalware = Preferences::GetBool(CHECK_MALWARE_PREF,
+ CHECK_MALWARE_DEFAULT);
+ } else if (NS_LITERAL_STRING(CHECK_PHISHING_PREF).Equals(aData)) {
+ mCheckPhishing = Preferences::GetBool(CHECK_PHISHING_PREF,
+ CHECK_PHISHING_DEFAULT);
+ } else if (NS_LITERAL_STRING(CHECK_TRACKING_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(CHECK_TRACKING_PB_PREF).Equals(aData)) {
+ mCheckTracking =
+ Preferences::GetBool(CHECK_TRACKING_PREF, CHECK_TRACKING_DEFAULT) ||
+ Preferences::GetBool(CHECK_TRACKING_PB_PREF, CHECK_TRACKING_PB_DEFAULT);
+ } else if (NS_LITERAL_STRING(CHECK_BLOCKED_PREF).Equals(aData)) {
+ mCheckBlockedURIs = Preferences::GetBool(CHECK_BLOCKED_PREF,
+ CHECK_BLOCKED_DEFAULT);
+ } else if (
+ NS_LITERAL_STRING(PHISH_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(MALWARE_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(TRACKING_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(TRACKING_WHITELIST_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(BLOCKED_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(DOWNLOAD_BLOCK_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(DOWNLOAD_ALLOW_TABLE_PREF).Equals(aData) ||
+ NS_LITERAL_STRING(DISALLOW_COMPLETION_TABLE_PREF).Equals(aData)) {
+ // Just read everything again.
+ ReadTablesFromPrefs();
+ } else if (NS_LITERAL_STRING(CONFIRM_AGE_PREF).Equals(aData)) {
+ gFreshnessGuarantee = Preferences::GetInt(CONFIRM_AGE_PREF,
+ CONFIRM_AGE_DEFAULT_SEC);
+ }
+ } else if (!strcmp(aTopic, "quit-application")) {
+ Shutdown();
+ } else if (!strcmp(aTopic, "profile-before-change")) {
+ // Unit test does not receive "quit-application",
+ // need call shutdown in this case
+ Shutdown();
+ LOG(("joining background thread"));
+ mWorkerProxy = nullptr;
+
+ if (!gDbBackgroundThread) {
+ return NS_OK;
+ }
+
+ nsIThread *backgroundThread = gDbBackgroundThread;
+ gDbBackgroundThread = nullptr;
+ backgroundThread->Shutdown();
+ NS_RELEASE(backgroundThread);
+ } else {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+// Join the background thread if it exists.
+nsresult
+nsUrlClassifierDBService::Shutdown()
+{
+ LOG(("shutting down db service\n"));
+
+ if (!gDbBackgroundThread || gShuttingDownThread) {
+ return NS_OK;
+ }
+
+ gShuttingDownThread = true;
+
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_SHUTDOWN_TIME> timer;
+
+ mCompleters.Clear();
+
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ prefs->RemoveObserver(CHECK_MALWARE_PREF, this);
+ prefs->RemoveObserver(CHECK_PHISHING_PREF, this);
+ prefs->RemoveObserver(CHECK_TRACKING_PREF, this);
+ prefs->RemoveObserver(CHECK_TRACKING_PB_PREF, this);
+ prefs->RemoveObserver(CHECK_BLOCKED_PREF, this);
+ prefs->RemoveObserver(PHISH_TABLE_PREF, this);
+ prefs->RemoveObserver(MALWARE_TABLE_PREF, this);
+ prefs->RemoveObserver(TRACKING_TABLE_PREF, this);
+ prefs->RemoveObserver(TRACKING_WHITELIST_TABLE_PREF, this);
+ prefs->RemoveObserver(BLOCKED_TABLE_PREF, this);
+ prefs->RemoveObserver(DOWNLOAD_BLOCK_TABLE_PREF, this);
+ prefs->RemoveObserver(DOWNLOAD_ALLOW_TABLE_PREF, this);
+ prefs->RemoveObserver(DISALLOW_COMPLETION_TABLE_PREF, this);
+ prefs->RemoveObserver(CONFIRM_AGE_PREF, this);
+ }
+
+ DebugOnly<nsresult> rv;
+ // First close the db connection.
+ if (mWorker) {
+ rv = mWorkerProxy->CancelUpdate();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to post cancel update event");
+
+ rv = mWorkerProxy->CloseDb();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to post close db event");
+ }
+ return NS_OK;
+}
+
+nsIThread*
+nsUrlClassifierDBService::BackgroundThread()
+{
+ return gDbBackgroundThread;
+}
+
+// static
+bool
+nsUrlClassifierDBService::ShutdownHasStarted()
+{
+ return gShuttingDownThread;
+}
diff --git a/toolkit/components/url-classifier/nsUrlClassifierDBService.h b/toolkit/components/url-classifier/nsUrlClassifierDBService.h
new file mode 100644
index 0000000000..55c10c1bff
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierDBService.h
@@ -0,0 +1,270 @@
+//* -*- 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/. */
+
+#ifndef nsUrlClassifierDBService_h_
+#define nsUrlClassifierDBService_h_
+
+#include <nsISupportsUtils.h>
+
+#include "nsID.h"
+#include "nsInterfaceHashtable.h"
+#include "nsIObserver.h"
+#include "nsUrlClassifierPrefixSet.h"
+#include "nsIUrlClassifierHashCompleter.h"
+#include "nsIUrlListManager.h"
+#include "nsIUrlClassifierDBService.h"
+#include "nsIURIClassifier.h"
+#include "nsToolkitCompsCID.h"
+#include "nsICryptoHMAC.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/TimeStamp.h"
+
+#include "Entries.h"
+#include "LookupCache.h"
+
+// GCC < 6.1 workaround, see bug 1329593
+#if defined(XP_WIN) && defined(__MINGW32__)
+#define GCC_MANGLING_WORKAROUND __stdcall
+#else
+#define GCC_MANGLING_WORKAROUND
+#endif
+
+// The hash length for a domain key.
+#define DOMAIN_LENGTH 4
+
+// The hash length of a partial hash entry.
+#define PARTIAL_LENGTH 4
+
+// The hash length of a complete hash entry.
+#define COMPLETE_LENGTH 32
+
+using namespace mozilla::safebrowsing;
+
+class nsUrlClassifierDBServiceWorker;
+class nsIThread;
+class nsIURI;
+class UrlClassifierDBServiceWorkerProxy;
+namespace mozilla {
+namespace safebrowsing {
+class Classifier;
+class ProtocolParser;
+class TableUpdate;
+
+nsresult
+TablesToResponse(const nsACString& tables);
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+// This is a proxy class that just creates a background thread and delagates
+// calls to the background thread.
+class nsUrlClassifierDBService final : public nsIUrlClassifierDBService,
+ public nsIURIClassifier,
+ public nsIObserver
+{
+public:
+ // This is thread safe. It throws an exception if the thread is busy.
+ nsUrlClassifierDBService();
+
+ nsresult Init();
+
+ static nsUrlClassifierDBService* GetInstance(nsresult *result);
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_URLCLASSIFIERDBSERVICE_CID)
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERDBSERVICE
+ NS_DECL_NSIURICLASSIFIER
+ NS_DECL_NSIOBSERVER
+
+ bool GetCompleter(const nsACString& tableName,
+ nsIUrlClassifierHashCompleter** completer);
+ nsresult CacheCompletions(mozilla::safebrowsing::CacheResultArray *results);
+ nsresult CacheMisses(mozilla::safebrowsing::PrefixArray *results);
+
+ static nsIThread* BackgroundThread();
+
+ static bool ShutdownHasStarted();
+
+private:
+ // No subclassing
+ ~nsUrlClassifierDBService();
+
+ // Disallow copy constructor
+ nsUrlClassifierDBService(nsUrlClassifierDBService&);
+
+ nsresult LookupURI(nsIPrincipal* aPrincipal,
+ const nsACString& tables,
+ nsIUrlClassifierCallback* c,
+ bool forceCheck, bool *didCheck);
+
+ // Close db connection and join the background thread if it exists.
+ nsresult Shutdown();
+
+ // Check if the key is on a known-clean host.
+ nsresult CheckClean(const nsACString &lookupKey,
+ bool *clean);
+
+ // Read everything into mGethashTables and mDisallowCompletionTables
+ nsresult ReadTablesFromPrefs();
+
+ // Build a comma-separated list of tables to check
+ void BuildTables(bool trackingProtectionEnabled, nsCString& tables);
+
+ RefPtr<nsUrlClassifierDBServiceWorker> mWorker;
+ RefPtr<UrlClassifierDBServiceWorkerProxy> mWorkerProxy;
+
+ nsInterfaceHashtable<nsCStringHashKey, nsIUrlClassifierHashCompleter> mCompleters;
+
+ // TRUE if the nsURIClassifier implementation should check for malware
+ // uris on document loads.
+ bool mCheckMalware;
+
+ // TRUE if the nsURIClassifier implementation should check for phishing
+ // uris on document loads.
+ bool mCheckPhishing;
+
+ // TRUE if the nsURIClassifier implementation should check for tracking
+ // uris on document loads.
+ bool mCheckTracking;
+
+ // TRUE if the nsURIClassifier implementation should check for blocked
+ // uris on document loads.
+ bool mCheckBlockedURIs;
+
+ // TRUE if a BeginUpdate() has been called without an accompanying
+ // CancelUpdate()/FinishUpdate(). This is used to prevent competing
+ // updates, not to determine whether an update is still being
+ // processed.
+ bool mInUpdate;
+
+ // The list of tables that can use the default hash completer object.
+ nsTArray<nsCString> mGethashTables;
+
+ // The list of tables that should never be hash completed.
+ nsTArray<nsCString> mDisallowCompletionsTables;
+
+ // Thread that we do the updates on.
+ static nsIThread* gDbBackgroundThread;
+};
+
+class nsUrlClassifierDBServiceWorker final : public nsIUrlClassifierDBService
+{
+public:
+ nsUrlClassifierDBServiceWorker();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERDBSERVICE
+
+ nsresult Init(uint32_t aGethashNoise, nsCOMPtr<nsIFile> aCacheDir);
+
+ // Queue a lookup for the worker to perform, called in the main thread.
+ // tables is a comma-separated list of tables to query
+ nsresult QueueLookup(const nsACString& lookupKey,
+ const nsACString& tables,
+ nsIUrlClassifierLookupCallback* callback);
+
+ // Handle any queued-up lookups. We call this function during long-running
+ // update operations to prevent lookups from blocking for too long.
+ nsresult HandlePendingLookups();
+
+ // Perform a blocking classifier lookup for a given url. Can be called on
+ // either the main thread or the worker thread.
+ nsresult DoLocalLookup(const nsACString& spec,
+ const nsACString& tables,
+ LookupResultArray* results);
+
+ // Open the DB connection
+ nsresult GCC_MANGLING_WORKAROUND OpenDb();
+
+ // Provide a way to forcibly close the db connection.
+ nsresult GCC_MANGLING_WORKAROUND CloseDb();
+
+ nsresult CacheCompletions(CacheResultArray * aEntries);
+ nsresult CacheMisses(PrefixArray * aEntries);
+
+private:
+ // No subclassing
+ ~nsUrlClassifierDBServiceWorker();
+
+ // Disallow copy constructor
+ nsUrlClassifierDBServiceWorker(nsUrlClassifierDBServiceWorker&);
+
+ // Applies the current transaction and resets the update/working times.
+ nsresult ApplyUpdate();
+
+ // Reset the in-progress update stream
+ void ResetStream();
+
+ // Reset the in-progress update
+ void ResetUpdate();
+
+ // Perform a classifier lookup for a given url.
+ nsresult DoLookup(const nsACString& spec,
+ const nsACString& tables,
+ nsIUrlClassifierLookupCallback* c);
+
+ nsresult AddNoise(const Prefix aPrefix,
+ const nsCString tableName,
+ uint32_t aCount,
+ LookupResultArray& results);
+
+ // Can only be used on the background thread
+ nsCOMPtr<nsICryptoHash> mCryptoHash;
+
+ nsAutoPtr<mozilla::safebrowsing::Classifier> mClassifier;
+ // The class that actually parses the update chunks.
+ nsAutoPtr<ProtocolParser> mProtocolParser;
+
+ // Directory where to store the SB databases.
+ nsCOMPtr<nsIFile> mCacheDir;
+
+ // XXX: maybe an array of autoptrs. Or maybe a class specifically
+ // storing a series of updates.
+ nsTArray<mozilla::safebrowsing::TableUpdate*> mTableUpdates;
+
+ uint32_t mUpdateWaitSec;
+
+ // Entries that cannot be completed. We expect them to die at
+ // the next update
+ PrefixArray mMissCache;
+
+ // Stores the last results that triggered a table update.
+ CacheResultArray mLastResults;
+
+ nsresult mUpdateStatus;
+ nsTArray<nsCString> mUpdateTables;
+
+ nsCOMPtr<nsIUrlClassifierUpdateObserver> mUpdateObserver;
+ bool mInStream;
+
+ // The number of noise entries to add to the set of lookup results.
+ uint32_t mGethashNoise;
+
+ // Pending lookups are stored in a queue for processing. The queue
+ // is protected by mPendingLookupLock.
+ mozilla::Mutex mPendingLookupLock;
+
+ class PendingLookup {
+ public:
+ mozilla::TimeStamp mStartTime;
+ nsCString mKey;
+ nsCString mTables;
+ nsCOMPtr<nsIUrlClassifierLookupCallback> mCallback;
+ };
+
+ // list of pending lookups
+ nsTArray<PendingLookup> mPendingLookups;
+
+#ifdef MOZ_SAFEBROWSING_DUMP_FAILED_UPDATES
+ // The raw update response for debugging.
+ nsCString mRawTableUpdates;
+#endif
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsUrlClassifierDBService, NS_URLCLASSIFIERDBSERVICE_CID)
+
+#endif // nsUrlClassifierDBService_h_
diff --git a/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js b/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js
new file mode 100644
index 0000000000..ba4bf225a6
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierHashCompleter.js
@@ -0,0 +1,589 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+// COMPLETE_LENGTH and PARTIAL_LENGTH copied from nsUrlClassifierDBService.h,
+// they correspond to the length, in bytes, of a hash prefix and the total
+// hash.
+const COMPLETE_LENGTH = 32;
+const PARTIAL_LENGTH = 4;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+
+// Log only if browser.safebrowsing.debug is true
+function log(...stuff) {
+ let logging = null;
+ try {
+ logging = Services.prefs.getBoolPref("browser.safebrowsing.debug");
+ } catch(e) {
+ return;
+ }
+ if (!logging) {
+ return;
+ }
+
+ var d = new Date();
+ let msg = "hashcompleter: " + d.toTimeString() + ": " + stuff.join(" ");
+ dump(Services.urlFormatter.trimSensitiveURLs(msg) + "\n");
+}
+
+// Map the HTTP response code to a Telemetry bucket
+// https://developers.google.com/safe-browsing/developers_guide_v2?hl=en
+function httpStatusToBucket(httpStatus) {
+ var statusBucket;
+ switch (httpStatus) {
+ case 100:
+ case 101:
+ // Unexpected 1xx return code
+ statusBucket = 0;
+ break;
+ case 200:
+ // OK - Data is available in the HTTP response body.
+ statusBucket = 1;
+ break;
+ case 201:
+ case 202:
+ case 203:
+ case 205:
+ case 206:
+ // Unexpected 2xx return code
+ statusBucket = 2;
+ break;
+ case 204:
+ // No Content - There are no full-length hashes with the requested prefix.
+ statusBucket = 3;
+ break;
+ case 300:
+ case 301:
+ case 302:
+ case 303:
+ case 304:
+ case 305:
+ case 307:
+ case 308:
+ // Unexpected 3xx return code
+ statusBucket = 4;
+ break;
+ case 400:
+ // Bad Request - The HTTP request was not correctly formed.
+ // The client did not provide all required CGI parameters.
+ statusBucket = 5;
+ break;
+ case 401:
+ case 402:
+ case 405:
+ case 406:
+ case 407:
+ case 409:
+ case 410:
+ case 411:
+ case 412:
+ case 414:
+ case 415:
+ case 416:
+ case 417:
+ case 421:
+ case 426:
+ case 428:
+ case 429:
+ case 431:
+ case 451:
+ // Unexpected 4xx return code
+ statusBucket = 6;
+ break;
+ case 403:
+ // Forbidden - The client id is invalid.
+ statusBucket = 7;
+ break;
+ case 404:
+ // Not Found
+ statusBucket = 8;
+ break;
+ case 408:
+ // Request Timeout
+ statusBucket = 9;
+ break;
+ case 413:
+ // Request Entity Too Large - Bug 1150334
+ statusBucket = 10;
+ break;
+ case 500:
+ case 501:
+ case 510:
+ // Unexpected 5xx return code
+ statusBucket = 11;
+ break;
+ case 502:
+ case 504:
+ case 511:
+ // Local network errors, we'll ignore these.
+ statusBucket = 12;
+ break;
+ case 503:
+ // Service Unavailable - The server cannot handle the request.
+ // Clients MUST follow the backoff behavior specified in the
+ // Request Frequency section.
+ statusBucket = 13;
+ break;
+ case 505:
+ // HTTP Version Not Supported - The server CANNOT handle the requested
+ // protocol major version.
+ statusBucket = 14;
+ break;
+ default:
+ statusBucket = 15;
+ };
+ return statusBucket;
+}
+
+function HashCompleter() {
+ // The current HashCompleterRequest in flight. Once it is started, it is set
+ // to null. It may be used by multiple calls to |complete| in succession to
+ // avoid creating multiple requests to the same gethash URL.
+ this._currentRequest = null;
+ // A map of gethashUrls to HashCompleterRequests that haven't yet begun.
+ this._pendingRequests = {};
+
+ // A map of gethash URLs to RequestBackoff objects.
+ this._backoffs = {};
+
+ // Whether we have been informed of a shutdown by the shutdown event.
+ this._shuttingDown = false;
+
+ Services.obs.addObserver(this, "quit-application", false);
+
+}
+
+HashCompleter.prototype = {
+ classID: Components.ID("{9111de73-9322-4bfc-8b65-2b727f3e6ec8}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUrlClassifierHashCompleter,
+ Ci.nsIRunnable,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsITimerCallback,
+ Ci.nsISupports]),
+
+ // This is mainly how the HashCompleter interacts with other components.
+ // Even though it only takes one partial hash and callback, subsequent
+ // calls are made into the same HTTP request by using a thread dispatch.
+ complete: function HC_complete(aPartialHash, aGethashUrl, aCallback) {
+ if (!aGethashUrl) {
+ throw Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (!this._currentRequest) {
+ this._currentRequest = new HashCompleterRequest(this, aGethashUrl);
+ }
+ if (this._currentRequest.gethashUrl == aGethashUrl) {
+ this._currentRequest.add(aPartialHash, aCallback);
+ } else {
+ if (!this._pendingRequests[aGethashUrl]) {
+ this._pendingRequests[aGethashUrl] =
+ new HashCompleterRequest(this, aGethashUrl);
+ }
+ this._pendingRequests[aGethashUrl].add(aPartialHash, aCallback);
+ }
+
+ if (!this._backoffs[aGethashUrl]) {
+ // Initialize request backoffs separately, since requests are deleted
+ // after they are dispatched.
+ var jslib = Cc["@mozilla.org/url-classifier/jslib;1"]
+ .getService().wrappedJSObject;
+
+ // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
+ this._backoffs[aGethashUrl] = new jslib.RequestBackoffV4(
+ 10 /* keep track of max requests */,
+ 0 /* don't throttle on successful requests per time period */);
+ }
+ // Start off this request. Without dispatching to a thread, every call to
+ // complete makes an individual HTTP request.
+ Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ // This is called after several calls to |complete|, or after the
+ // currentRequest has finished. It starts off the HTTP request by making a
+ // |begin| call to the HashCompleterRequest.
+ run: function() {
+ // Clear everything on shutdown
+ if (this._shuttingDown) {
+ this._currentRequest = null;
+ this._pendingRequests = null;
+ for (var url in this._backoffs) {
+ this._backoffs[url] = null;
+ }
+ throw Cr.NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If we don't have an in-flight request, make one
+ let pendingUrls = Object.keys(this._pendingRequests);
+ if (!this._currentRequest && (pendingUrls.length > 0)) {
+ let nextUrl = pendingUrls[0];
+ this._currentRequest = this._pendingRequests[nextUrl];
+ delete this._pendingRequests[nextUrl];
+ }
+
+ if (this._currentRequest) {
+ try {
+ this._currentRequest.begin();
+ } finally {
+ // If |begin| fails, we should get rid of our request.
+ this._currentRequest = null;
+ }
+ }
+ },
+
+ // Pass the server response status to the RequestBackoff for the given
+ // gethashUrl and fetch the next pending request, if there is one.
+ finishRequest: function(url, aStatus) {
+ this._backoffs[url].noteServerResponse(aStatus);
+ Services.tm.currentThread.dispatch(this, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ // Returns true if we can make a request from the given url, false otherwise.
+ canMakeRequest: function(aGethashUrl) {
+ return this._backoffs[aGethashUrl].canMakeRequest();
+ },
+
+ // Notifies the RequestBackoff of a new request so we can throttle based on
+ // max requests/time period. This must be called before a channel is opened,
+ // and finishRequest must be called once the response is received.
+ noteRequest: function(aGethashUrl) {
+ return this._backoffs[aGethashUrl].noteRequest();
+ },
+
+ observe: function HC_observe(aSubject, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ this._shuttingDown = true;
+ Services.obs.removeObserver(this, "quit-application");
+ }
+ },
+};
+
+function HashCompleterRequest(aCompleter, aGethashUrl) {
+ // HashCompleter object that created this HashCompleterRequest.
+ this._completer = aCompleter;
+ // The internal set of hashes and callbacks that this request corresponds to.
+ this._requests = [];
+ // nsIChannel that the hash completion query is transmitted over.
+ this._channel = null;
+ // Response body of hash completion. Created in onDataAvailable.
+ this._response = "";
+ // Whether we have been informed of a shutdown by the quit-application event.
+ this._shuttingDown = false;
+ this.gethashUrl = aGethashUrl;
+}
+HashCompleterRequest.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
+ Ci.nsIStreamListener,
+ Ci.nsIObserver,
+ Ci.nsISupports]),
+
+ // This is called by the HashCompleter to add a hash and callback to the
+ // HashCompleterRequest. It must be called before calling |begin|.
+ add: function HCR_add(aPartialHash, aCallback) {
+ this._requests.push({
+ partialHash: aPartialHash,
+ callback: aCallback,
+ responses: []
+ });
+ },
+
+ // This initiates the HTTP request. It can fail due to backoff timings and
+ // will notify all callbacks as necessary. We notify the backoff object on
+ // begin.
+ begin: function HCR_begin() {
+ if (!this._completer.canMakeRequest(this.gethashUrl)) {
+ log("Can't make request to " + this.gethashUrl + "\n");
+ this.notifyFailure(Cr.NS_ERROR_ABORT);
+ return;
+ }
+
+ Services.obs.addObserver(this, "quit-application", false);
+
+ try {
+ this.openChannel();
+ // Notify the RequestBackoff if opening the channel succeeded. At this
+ // point, finishRequest must be called.
+ this._completer.noteRequest(this.gethashUrl);
+ }
+ catch (err) {
+ this.notifyFailure(err);
+ throw err;
+ }
+ },
+
+ notify: function HCR_notify() {
+ // If we haven't gotten onStopRequest, just cancel. This will call us
+ // with onStopRequest since we implement nsIStreamListener on the
+ // channel.
+ if (this._channel && this._channel.isPending()) {
+ log("cancelling request to " + this.gethashUrl + "\n");
+ Services.telemetry.getHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT").add(1);
+ this._channel.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ },
+
+ // Creates an nsIChannel for the request and fills the body.
+ openChannel: function HCR_openChannel() {
+ let loadFlags = Ci.nsIChannel.INHIBIT_CACHING |
+ Ci.nsIChannel.LOAD_BYPASS_CACHE;
+
+ let channel = NetUtil.newChannel({
+ uri: this.gethashUrl,
+ loadUsingSystemPrincipal: true
+ });
+ channel.loadFlags = loadFlags;
+
+ // Disable keepalive.
+ let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ httpChannel.setRequestHeader("Connection", "close", false);
+
+ this._channel = channel;
+
+ let body = this.buildRequest();
+ this.addRequestBody(body);
+
+ // Set a timer that cancels the channel after timeout_ms in case we
+ // don't get a gethash response.
+ this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ // Ask the timer to use nsITimerCallback (.notify()) when ready
+ let timeout = Services.prefs.getIntPref(
+ "urlclassifier.gethash.timeout_ms");
+ this.timer_.initWithCallback(this, timeout, this.timer_.TYPE_ONE_SHOT);
+ channel.asyncOpen2(this);
+ },
+
+ // Returns a string for the request body based on the contents of
+ // this._requests.
+ buildRequest: function HCR_buildRequest() {
+ // Sometimes duplicate entries are sent to HashCompleter but we do not need
+ // to propagate these to the server. (bug 633644)
+ let prefixes = [];
+
+ for (let i = 0; i < this._requests.length; i++) {
+ let request = this._requests[i];
+ if (prefixes.indexOf(request.partialHash) == -1) {
+ prefixes.push(request.partialHash);
+ }
+ }
+
+ // Randomize the order to obscure the original request from noise
+ // unbiased Fisher-Yates shuffle
+ let i = prefixes.length;
+ while (i--) {
+ let j = Math.floor(Math.random() * (i + 1));
+ let temp = prefixes[i];
+ prefixes[i] = prefixes[j];
+ prefixes[j] = temp;
+ }
+
+ let body;
+ body = PARTIAL_LENGTH + ":" + (PARTIAL_LENGTH * prefixes.length) +
+ "\n" + prefixes.join("");
+
+ log('Requesting completions for ' + prefixes.length + ' ' + PARTIAL_LENGTH + '-byte prefixes: ' + body);
+ return body;
+ },
+
+ // Sets the request body of this._channel.
+ addRequestBody: function HCR_addRequestBody(aBody) {
+ let inputStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+
+ inputStream.setData(aBody, aBody.length);
+
+ let uploadChannel = this._channel.QueryInterface(Ci.nsIUploadChannel);
+ uploadChannel.setUploadStream(inputStream, "text/plain", -1);
+
+ let httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel);
+ httpChannel.requestMethod = "POST";
+ },
+
+ // Parses the response body and eventually adds items to the |responses| array
+ // for elements of |this._requests|.
+ handleResponse: function HCR_handleResponse() {
+ if (this._response == "") {
+ return;
+ }
+
+ log('Response: ' + this._response);
+ let start = 0;
+
+ let length = this._response.length;
+ while (start != length) {
+ start = this.handleTable(start);
+ }
+ },
+
+ // This parses a table entry in the response body and calls |handleItem|
+ // for complete hash in the table entry.
+ handleTable: function HCR_handleTable(aStart) {
+ let body = this._response.substring(aStart);
+
+ // deal with new line indexes as there could be
+ // new line characters in the data parts.
+ let newlineIndex = body.indexOf("\n");
+ if (newlineIndex == -1) {
+ throw errorWithStack();
+ }
+ let header = body.substring(0, newlineIndex);
+ let entries = header.split(":");
+ if (entries.length != 3) {
+ throw errorWithStack();
+ }
+
+ let list = entries[0];
+ let addChunk = parseInt(entries[1]);
+ let dataLength = parseInt(entries[2]);
+
+ log('Response includes add chunks for ' + list + ': ' + addChunk);
+ if (dataLength % COMPLETE_LENGTH != 0 ||
+ dataLength == 0 ||
+ dataLength > body.length - (newlineIndex + 1)) {
+ throw errorWithStack();
+ }
+
+ let data = body.substr(newlineIndex + 1, dataLength);
+ for (let i = 0; i < (dataLength / COMPLETE_LENGTH); i++) {
+ this.handleItem(data.substr(i * COMPLETE_LENGTH, COMPLETE_LENGTH), list,
+ addChunk);
+ }
+
+ return aStart + newlineIndex + 1 + dataLength;
+ },
+
+ // This adds a complete hash to any entry in |this._requests| that matches
+ // the hash.
+ handleItem: function HCR_handleItem(aData, aTableName, aChunkId) {
+ for (let i = 0; i < this._requests.length; i++) {
+ let request = this._requests[i];
+ if (aData.substring(0,4) == request.partialHash) {
+ request.responses.push({
+ completeHash: aData,
+ tableName: aTableName,
+ chunkId: aChunkId,
+ });
+ }
+ }
+ },
+
+ // notifySuccess and notifyFailure are used to alert the callbacks with
+ // results. notifySuccess makes |completion| and |completionFinished| calls
+ // while notifyFailure only makes a |completionFinished| call with the error
+ // code.
+ notifySuccess: function HCR_notifySuccess() {
+ for (let i = 0; i < this._requests.length; i++) {
+ let request = this._requests[i];
+ for (let j = 0; j < request.responses.length; j++) {
+ let response = request.responses[j];
+ request.callback.completion(response.completeHash, response.tableName,
+ response.chunkId);
+ }
+
+ request.callback.completionFinished(Cr.NS_OK);
+ }
+ },
+
+ notifyFailure: function HCR_notifyFailure(aStatus) {
+ log("notifying failure\n");
+ for (let i = 0; i < this._requests.length; i++) {
+ let request = this._requests[i];
+ request.callback.completionFinished(aStatus);
+ }
+ },
+
+ onDataAvailable: function HCR_onDataAvailable(aRequest, aContext,
+ aInputStream, aOffset, aCount) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(aInputStream);
+ this._response += sis.readBytes(aCount);
+ },
+
+ onStartRequest: function HCR_onStartRequest(aRequest, aContext) {
+ // At this point no data is available for us and we have no reason to
+ // terminate the connection, so we do nothing until |onStopRequest|.
+ },
+
+ onStopRequest: function HCR_onStopRequest(aRequest, aContext, aStatusCode) {
+ Services.obs.removeObserver(this, "quit-application");
+
+ if (this._shuttingDown) {
+ throw Cr.NS_ERROR_ABORT;
+ }
+
+ // Default HTTP status to service unavailable, in case we can't retrieve
+ // the true status from the channel.
+ let httpStatus = 503;
+ if (Components.isSuccessCode(aStatusCode)) {
+ let channel = aRequest.QueryInterface(Ci.nsIHttpChannel);
+ let success = channel.requestSucceeded;
+ httpStatus = channel.responseStatus;
+ if (!success) {
+ aStatusCode = Cr.NS_ERROR_ABORT;
+ }
+ }
+ let success = Components.isSuccessCode(aStatusCode);
+ log('Received a ' + httpStatus + ' status code from the gethash server (success=' + success + ').');
+
+ let histogram =
+ Services.telemetry.getHistogramById("URLCLASSIFIER_COMPLETE_REMOTE_STATUS");
+ histogram.add(httpStatusToBucket(httpStatus));
+ Services.telemetry.getHistogramById("URLCLASSIFIER_COMPLETE_TIMEOUT").add(0);
+
+ // Notify the RequestBackoff once a response is received.
+ this._completer.finishRequest(this.gethashUrl, httpStatus);
+
+ if (success) {
+ try {
+ this.handleResponse();
+ }
+ catch (err) {
+ log(err.stack);
+ aStatusCode = err.value;
+ success = false;
+ }
+ }
+
+ if (success) {
+ this.notifySuccess();
+ } else {
+ this.notifyFailure(aStatusCode);
+ }
+ },
+
+ observe: function HCR_observe(aSubject, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ this._shuttingDown = true;
+ if (this._channel) {
+ this._channel.cancel(Cr.NS_ERROR_ABORT);
+ }
+
+ Services.obs.removeObserver(this, "quit-application");
+ }
+ },
+};
+
+// Converts a URL safe base64 string to a normal base64 string. Will not change
+// normal base64 strings. This is modelled after the same function in
+// nsUrlClassifierUtils.h.
+function unUrlsafeBase64(aStr) {
+ return !aStr ? "" : aStr.replace(/-/g, "+")
+ .replace(/_/g, "/");
+}
+
+function errorWithStack() {
+ let err = new Error();
+ err.value = Cr.NS_ERROR_FAILURE;
+ return err;
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HashCompleter]);
diff --git a/toolkit/components/url-classifier/nsUrlClassifierLib.js b/toolkit/components/url-classifier/nsUrlClassifierLib.js
new file mode 100644
index 0000000000..bb0d2b421c
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierLib.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/.
+
+// We wastefully reload the same JS files across components. This puts all
+// the common JS files used by safebrowsing and url-classifier into a
+// single component.
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const G_GDEBUG = false;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+#include ./content/moz/lang.js
+#include ./content/moz/preferences.js
+#include ./content/moz/debug.js
+#include ./content/moz/alarm.js
+#include ./content/moz/cryptohasher.js
+#include ./content/moz/observer.js
+#include ./content/moz/protocol4.js
+
+#include ./content/request-backoff.js
+#include ./content/xml-fetcher.js
+
+// Wrap a general-purpose |RequestBackoff| to a v4-specific one
+// since both listmanager and hashcompleter would use it.
+// Note that |maxRequests| and |requestPeriod| is still configurable
+// to throttle pending requests.
+function RequestBackoffV4(maxRequests, requestPeriod) {
+ let rand = Math.random();
+ let retryInterval = Math.floor(15 * 60 * 1000 * (rand + 1)); // 15 ~ 30 min.
+ let backoffInterval = Math.floor(30 * 60 * 1000 * (rand + 1)); // 30 ~ 60 min.
+
+ return new RequestBackoff(2 /* max errors */,
+ retryInterval /* retry interval, 15~30 min */,
+ maxRequests /* num requests */,
+ requestPeriod /* request time, 60 min */,
+ backoffInterval /* backoff interval, 60 min */,
+ 24 * 60 * 60 * 1000 /* max backoff, 24hr */);
+}
+
+// Expose this whole component.
+var lib = this;
+
+function UrlClassifierLib() {
+ this.wrappedJSObject = lib;
+}
+UrlClassifierLib.prototype.classID = Components.ID("{26a4a019-2827-4a89-a85c-5931a678823a}");
+UrlClassifierLib.prototype.QueryInterface = XPCOMUtils.generateQI([]);
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UrlClassifierLib]);
diff --git a/toolkit/components/url-classifier/nsUrlClassifierListManager.js b/toolkit/components/url-classifier/nsUrlClassifierListManager.js
new file mode 100644
index 0000000000..7b3c181af7
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierListManager.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/.
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+#include ./content/listmanager.js
+
+var modScope = this;
+function Init() {
+ // Pull the library in.
+ var jslib = Cc["@mozilla.org/url-classifier/jslib;1"]
+ .getService().wrappedJSObject;
+ Function.prototype.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+ },
+ modScope.G_Preferences = jslib.G_Preferences;
+ modScope.G_PreferenceObserver = jslib.G_PreferenceObserver;
+ modScope.G_ObserverServiceObserver = jslib.G_ObserverServiceObserver;
+ modScope.G_Debug = jslib.G_Debug;
+ modScope.G_Assert = jslib.G_Assert;
+ modScope.G_debugService = jslib.G_debugService;
+ modScope.G_Alarm = jslib.G_Alarm;
+ modScope.BindToObject = jslib.BindToObject;
+ modScope.PROT_XMLFetcher = jslib.PROT_XMLFetcher;
+ modScope.RequestBackoffV4 = jslib.RequestBackoffV4;
+
+ // We only need to call Init once.
+ modScope.Init = function() {};
+}
+
+function RegistrationData()
+{
+}
+RegistrationData.prototype = {
+ classID: Components.ID("{ca168834-cc00-48f9-b83c-fd018e58cae3}"),
+ _xpcom_factory: {
+ createInstance: function(outer, iid) {
+ if (outer != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ Init();
+ return (new PROT_ListManager()).QueryInterface(iid);
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RegistrationData]);
diff --git a/toolkit/components/url-classifier/nsUrlClassifierPrefixSet.cpp b/toolkit/components/url-classifier/nsUrlClassifierPrefixSet.cpp
new file mode 100644
index 0000000000..8745654703
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierPrefixSet.cpp
@@ -0,0 +1,526 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsUrlClassifierPrefixSet.h"
+#include "nsIUrlClassifierPrefixSet.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsPrintfCString.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsIFile.h"
+#include "nsToolkitCompsCID.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+#include "nsISeekableStream.h"
+#include "nsIBufferedStreams.h"
+#include "nsIFileStreams.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+// MOZ_LOG=UrlClassifierPrefixSet:5
+static LazyLogModule gUrlClassifierPrefixSetLog("UrlClassifierPrefixSet");
+#define LOG(args) MOZ_LOG(gUrlClassifierPrefixSetLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierPrefixSetLog, mozilla::LogLevel::Debug)
+
+NS_IMPL_ISUPPORTS(
+ nsUrlClassifierPrefixSet, nsIUrlClassifierPrefixSet, nsIMemoryReporter)
+
+// Definition required due to std::max<>()
+const uint32_t nsUrlClassifierPrefixSet::MAX_BUFFER_SIZE;
+
+nsUrlClassifierPrefixSet::nsUrlClassifierPrefixSet()
+ : mLock("nsUrlClassifierPrefixSet.mLock")
+ , mTotalPrefixes(0)
+ , mMemoryReportPath()
+{
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::Init(const nsACString& aName)
+{
+ mMemoryReportPath =
+ nsPrintfCString(
+ "explicit/storage/prefix-set/%s",
+ (!aName.IsEmpty() ? PromiseFlatCString(aName).get() : "?!")
+ );
+
+ RegisterWeakMemoryReporter(this);
+
+ return NS_OK;
+}
+
+nsUrlClassifierPrefixSet::~nsUrlClassifierPrefixSet()
+{
+ UnregisterWeakMemoryReporter(this);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::SetPrefixes(const uint32_t* aArray, uint32_t aLength)
+{
+ MutexAutoLock lock(mLock);
+
+ nsresult rv = NS_OK;
+
+ if (aLength <= 0) {
+ if (mIndexPrefixes.Length() > 0) {
+ LOG(("Clearing PrefixSet"));
+ mIndexDeltas.Clear();
+ mIndexPrefixes.Clear();
+ mTotalPrefixes = 0;
+ }
+ } else {
+ rv = MakePrefixSet(aArray, aLength);
+ }
+
+ return rv;
+}
+
+nsresult
+nsUrlClassifierPrefixSet::MakePrefixSet(const uint32_t* aPrefixes, uint32_t aLength)
+{
+ mLock.AssertCurrentThreadOwns();
+
+ if (aLength == 0) {
+ return NS_OK;
+ }
+
+#ifdef DEBUG
+ for (uint32_t i = 1; i < aLength; i++) {
+ MOZ_ASSERT(aPrefixes[i] >= aPrefixes[i-1]);
+ }
+#endif
+
+ mIndexPrefixes.Clear();
+ mIndexDeltas.Clear();
+ mTotalPrefixes = aLength;
+
+ mIndexPrefixes.AppendElement(aPrefixes[0]);
+ mIndexDeltas.AppendElement();
+
+ uint32_t numOfDeltas = 0;
+ uint32_t totalDeltas = 0;
+ uint32_t previousItem = aPrefixes[0];
+ for (uint32_t i = 1; i < aLength; i++) {
+ if ((numOfDeltas >= DELTAS_LIMIT) ||
+ (aPrefixes[i] - previousItem >= MAX_INDEX_DIFF)) {
+ // Compact the previous element.
+ // Note there is always at least one element when we get here,
+ // because we created the first element before the loop.
+ mIndexDeltas.LastElement().Compact();
+ mIndexDeltas.AppendElement();
+ mIndexPrefixes.AppendElement(aPrefixes[i]);
+ numOfDeltas = 0;
+ } else {
+ uint16_t delta = aPrefixes[i] - previousItem;
+ mIndexDeltas.LastElement().AppendElement(delta);
+ numOfDeltas++;
+ totalDeltas++;
+ }
+ previousItem = aPrefixes[i];
+ }
+
+ mIndexDeltas.LastElement().Compact();
+ mIndexDeltas.Compact();
+ mIndexPrefixes.Compact();
+
+ LOG(("Total number of indices: %d", aLength));
+ LOG(("Total number of deltas: %d", totalDeltas));
+ LOG(("Total number of delta chunks: %d", mIndexDeltas.Length()));
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierPrefixSet::GetPrefixesNative(FallibleTArray<uint32_t>& outArray)
+{
+ MutexAutoLock lock(mLock);
+
+ if (!outArray.SetLength(mTotalPrefixes, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ uint32_t prefixIdxLength = mIndexPrefixes.Length();
+ uint32_t prefixCnt = 0;
+
+ for (uint32_t i = 0; i < prefixIdxLength; i++) {
+ uint32_t prefix = mIndexPrefixes[i];
+
+ outArray[prefixCnt++] = prefix;
+ for (uint32_t j = 0; j < mIndexDeltas[i].Length(); j++) {
+ prefix += mIndexDeltas[i][j];
+ outArray[prefixCnt++] = prefix;
+ }
+ }
+
+ NS_ASSERTION(mTotalPrefixes == prefixCnt, "Lengths are inconsistent");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::GetPrefixes(uint32_t* aCount,
+ uint32_t** aPrefixes)
+{
+ // No need to get mLock here because this function does not directly touch
+ // the class's data members. (GetPrefixesNative() will get mLock, however.)
+
+ NS_ENSURE_ARG_POINTER(aCount);
+ *aCount = 0;
+ NS_ENSURE_ARG_POINTER(aPrefixes);
+ *aPrefixes = nullptr;
+
+ FallibleTArray<uint32_t> prefixes;
+ nsresult rv = GetPrefixesNative(prefixes);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ uint64_t itemCount = prefixes.Length();
+ uint32_t* prefixArray = static_cast<uint32_t*>(moz_xmalloc(itemCount * sizeof(uint32_t)));
+ NS_ENSURE_TRUE(prefixArray, NS_ERROR_OUT_OF_MEMORY);
+
+ memcpy(prefixArray, prefixes.Elements(), sizeof(uint32_t) * itemCount);
+
+ *aCount = itemCount;
+ *aPrefixes = prefixArray;
+
+ return NS_OK;
+}
+
+uint32_t nsUrlClassifierPrefixSet::BinSearch(uint32_t start,
+ uint32_t end,
+ uint32_t target)
+{
+ mLock.AssertCurrentThreadOwns();
+
+ while (start != end && end >= start) {
+ uint32_t i = start + ((end - start) >> 1);
+ uint32_t value = mIndexPrefixes[i];
+ if (value < target) {
+ start = i + 1;
+ } else if (value > target) {
+ end = i - 1;
+ } else {
+ return i;
+ }
+ }
+ return end;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::Contains(uint32_t aPrefix, bool* aFound)
+{
+ MutexAutoLock lock(mLock);
+
+ *aFound = false;
+
+ if (mIndexPrefixes.Length() == 0) {
+ return NS_OK;
+ }
+
+ uint32_t target = aPrefix;
+
+ // We want to do a "Price is Right" binary search, that is, we want to find
+ // the index of the value either equal to the target or the closest value
+ // that is less than the target.
+ //
+ if (target < mIndexPrefixes[0]) {
+ return NS_OK;
+ }
+
+ // |binsearch| does not necessarily return the correct index (when the
+ // target is not found) but rather it returns an index at least one away
+ // from the correct index.
+ // Because of this, we need to check if the target lies before the beginning
+ // of the indices.
+
+ uint32_t i = BinSearch(0, mIndexPrefixes.Length() - 1, target);
+ if (mIndexPrefixes[i] > target && i > 0) {
+ i--;
+ }
+
+ // Now search through the deltas for the target.
+ uint32_t diff = target - mIndexPrefixes[i];
+ uint32_t deltaSize = mIndexDeltas[i].Length();
+ uint32_t deltaIndex = 0;
+
+ while (diff > 0 && deltaIndex < deltaSize) {
+ diff -= mIndexDeltas[i][deltaIndex];
+ deltaIndex++;
+ }
+
+ if (diff == 0) {
+ *aFound = true;
+ }
+
+ return NS_OK;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(UrlClassifierMallocSizeOf)
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // No need to get mLock here because this function does not directly touch
+ // the class's data members. (SizeOfIncludingThis() will get mLock, however.)
+
+ aHandleReport->Callback(
+ EmptyCString(), mMemoryReportPath, KIND_HEAP, UNITS_BYTES,
+ SizeOfIncludingThis(UrlClassifierMallocSizeOf),
+ NS_LITERAL_CSTRING("Memory used by the prefix set for a URL classifier."),
+ aData);
+
+ return NS_OK;
+}
+
+size_t
+nsUrlClassifierPrefixSet::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+ MutexAutoLock lock(mLock);
+
+ size_t n = 0;
+ n += aMallocSizeOf(this);
+ n += mIndexDeltas.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mIndexDeltas.Length(); i++) {
+ n += mIndexDeltas[i].ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ n += mIndexPrefixes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::IsEmpty(bool * aEmpty)
+{
+ MutexAutoLock lock(mLock);
+
+ *aEmpty = (mIndexPrefixes.Length() == 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::LoadFromFile(nsIFile* aFile)
+{
+ MutexAutoLock lock(mLock);
+
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_PS_FILELOAD_TIME> timer;
+
+ nsCOMPtr<nsIInputStream> localInFile;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(localInFile), aFile,
+ PR_RDONLY | nsIFile::OS_READAHEAD);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Calculate how big the file is, make sure our read buffer isn't bigger
+ // than the file itself which is just wasting memory.
+ int64_t fileSize;
+ rv = aFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (fileSize < 0 || fileSize > UINT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t bufferSize = std::min<uint32_t>(static_cast<uint32_t>(fileSize),
+ MAX_BUFFER_SIZE);
+
+ // Convert to buffered stream
+ nsCOMPtr<nsIInputStream> in = NS_BufferInputStream(localInFile, bufferSize);
+
+ rv = LoadPrefixes(in);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierPrefixSet::StoreToFile(nsIFile* aFile)
+{
+ MutexAutoLock lock(mLock);
+
+ nsCOMPtr<nsIOutputStream> localOutFile;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(localOutFile), aFile,
+ PR_WRONLY | PR_TRUNCATE | PR_CREATE_FILE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t fileSize;
+
+ // Preallocate the file storage
+ {
+ nsCOMPtr<nsIFileOutputStream> fos(do_QueryInterface(localOutFile));
+ Telemetry::AutoTimer<Telemetry::URLCLASSIFIER_PS_FALLOCATE_TIME> timer;
+
+ fileSize = CalculatePreallocateSize();
+
+ // Ignore failure, the preallocation is a hint and we write out the entire
+ // file later on
+ Unused << fos->Preallocate(fileSize);
+ }
+
+ // Convert to buffered stream
+ nsCOMPtr<nsIOutputStream> out =
+ NS_BufferOutputStream(localOutFile, std::min(fileSize, MAX_BUFFER_SIZE));
+
+ rv = WritePrefixes(out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG(("Saving PrefixSet successful\n"));
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierPrefixSet::LoadPrefixes(nsIInputStream* in)
+{
+ uint32_t magic;
+ uint32_t read;
+
+ nsresult rv = in->Read(reinterpret_cast<char*>(&magic), sizeof(uint32_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint32_t), NS_ERROR_FAILURE);
+
+ if (magic == PREFIXSET_VERSION_MAGIC) {
+ uint32_t indexSize;
+ uint32_t deltaSize;
+
+ rv = in->Read(reinterpret_cast<char*>(&indexSize), sizeof(uint32_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint32_t), NS_ERROR_FAILURE);
+
+ rv = in->Read(reinterpret_cast<char*>(&deltaSize), sizeof(uint32_t), &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == sizeof(uint32_t), NS_ERROR_FAILURE);
+
+ if (indexSize == 0) {
+ LOG(("stored PrefixSet is empty!"));
+ return NS_OK;
+ }
+
+ if (deltaSize > (indexSize * DELTAS_LIMIT)) {
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ nsTArray<uint32_t> indexStarts;
+ indexStarts.SetLength(indexSize);
+ mIndexPrefixes.SetLength(indexSize);
+ mIndexDeltas.SetLength(indexSize);
+
+ mTotalPrefixes = indexSize;
+
+ uint32_t toRead = indexSize*sizeof(uint32_t);
+ rv = in->Read(reinterpret_cast<char*>(mIndexPrefixes.Elements()), toRead, &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == toRead, NS_ERROR_FAILURE);
+
+ rv = in->Read(reinterpret_cast<char*>(indexStarts.Elements()), toRead, &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == toRead, NS_ERROR_FAILURE);
+
+ if (indexSize != 0 && indexStarts[0] != 0) {
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+ for (uint32_t i = 0; i < indexSize; i++) {
+ uint32_t numInDelta = i == indexSize - 1 ? deltaSize - indexStarts[i]
+ : indexStarts[i + 1] - indexStarts[i];
+ if (numInDelta > DELTAS_LIMIT) {
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+ if (numInDelta > 0) {
+ mIndexDeltas[i].SetLength(numInDelta);
+ mTotalPrefixes += numInDelta;
+ toRead = numInDelta * sizeof(uint16_t);
+ rv = in->Read(reinterpret_cast<char*>(mIndexDeltas[i].Elements()), toRead, &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(read == toRead, NS_ERROR_FAILURE);
+ }
+ }
+ } else {
+ LOG(("Version magic mismatch, not loading"));
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ MOZ_ASSERT(mIndexPrefixes.Length() == mIndexDeltas.Length());
+ LOG(("Loading PrefixSet successful"));
+
+ return NS_OK;
+}
+
+uint32_t
+nsUrlClassifierPrefixSet::CalculatePreallocateSize()
+{
+ uint32_t fileSize = 4 * sizeof(uint32_t);
+ uint32_t deltas = mTotalPrefixes - mIndexPrefixes.Length();
+ fileSize += 2 * mIndexPrefixes.Length() * sizeof(uint32_t);
+ fileSize += deltas * sizeof(uint16_t);
+ return fileSize;
+}
+
+nsresult
+nsUrlClassifierPrefixSet::WritePrefixes(nsIOutputStream* out)
+{
+ uint32_t written;
+ uint32_t writelen = sizeof(uint32_t);
+ uint32_t magic = PREFIXSET_VERSION_MAGIC;
+ nsresult rv = out->Write(reinterpret_cast<char*>(&magic), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ uint32_t indexSize = mIndexPrefixes.Length();
+ uint32_t indexDeltaSize = mIndexDeltas.Length();
+ uint32_t totalDeltas = 0;
+
+ // Store the shape of mIndexDeltas by noting at which "count" of total
+ // indexes a new subarray starts. This is slightly cumbersome but keeps
+ // file format compatibility.
+ // If we ever update the format, we can gain space by storing the delta
+ // subarray sizes, which fit in bytes.
+ nsTArray<uint32_t> indexStarts;
+ indexStarts.AppendElement(0);
+
+ for (uint32_t i = 0; i < indexDeltaSize; i++) {
+ uint32_t deltaLength = mIndexDeltas[i].Length();
+ totalDeltas += deltaLength;
+ indexStarts.AppendElement(totalDeltas);
+ }
+
+ rv = out->Write(reinterpret_cast<char*>(&indexSize), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ rv = out->Write(reinterpret_cast<char*>(&totalDeltas), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ writelen = indexSize * sizeof(uint32_t);
+ rv = out->Write(reinterpret_cast<char*>(mIndexPrefixes.Elements()), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ rv = out->Write(reinterpret_cast<char*>(indexStarts.Elements()), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+
+ if (totalDeltas > 0) {
+ for (uint32_t i = 0; i < indexDeltaSize; i++) {
+ writelen = mIndexDeltas[i].Length() * sizeof(uint16_t);
+ rv = out->Write(reinterpret_cast<char*>(mIndexDeltas[i].Elements()), writelen, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(written == writelen, NS_ERROR_FAILURE);
+ }
+ }
+
+ LOG(("Saving PrefixSet successful\n"));
+
+ return NS_OK;
+}
diff --git a/toolkit/components/url-classifier/nsUrlClassifierPrefixSet.h b/toolkit/components/url-classifier/nsUrlClassifierPrefixSet.h
new file mode 100644
index 0000000000..8627b75d0c
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierPrefixSet.h
@@ -0,0 +1,89 @@
+//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsUrlClassifierPrefixSet_h_
+#define nsUrlClassifierPrefixSet_h_
+
+#include "nsISupportsUtils.h"
+#include "nsID.h"
+#include "nsIFile.h"
+#include "nsIMemoryReporter.h"
+#include "nsIMutableArray.h"
+#include "nsIFileStreams.h"
+#include "nsIUrlClassifierPrefixSet.h"
+#include "nsTArray.h"
+#include "nsToolkitCompsCID.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+class VariableLengthPrefixSet;
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+class nsUrlClassifierPrefixSet final
+ : public nsIUrlClassifierPrefixSet
+ , public nsIMemoryReporter
+{
+public:
+ nsUrlClassifierPrefixSet();
+
+ NS_IMETHOD Init(const nsACString& aName) override;
+ NS_IMETHOD SetPrefixes(const uint32_t* aArray, uint32_t aLength) override;
+ NS_IMETHOD GetPrefixes(uint32_t* aCount, uint32_t** aPrefixes) override;
+ NS_IMETHOD Contains(uint32_t aPrefix, bool* aFound) override;
+ NS_IMETHOD IsEmpty(bool* aEmpty) override;
+ NS_IMETHOD LoadFromFile(nsIFile* aFile) override;
+ NS_IMETHOD StoreToFile(nsIFile* aFile) override;
+
+ nsresult GetPrefixesNative(FallibleTArray<uint32_t>& outArray);
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ friend class mozilla::safebrowsing::VariableLengthPrefixSet;
+
+private:
+ virtual ~nsUrlClassifierPrefixSet();
+
+ static const uint32_t MAX_BUFFER_SIZE = 64 * 1024;
+ static const uint32_t DELTAS_LIMIT = 120;
+ static const uint32_t MAX_INDEX_DIFF = (1 << 16);
+ static const uint32_t PREFIXSET_VERSION_MAGIC = 1;
+
+ nsresult MakePrefixSet(const uint32_t* aArray, uint32_t aLength);
+ uint32_t BinSearch(uint32_t start, uint32_t end, uint32_t target);
+
+ uint32_t CalculatePreallocateSize();
+ nsresult WritePrefixes(nsIOutputStream* out);
+ nsresult LoadPrefixes(nsIInputStream* in);
+
+ // Lock to prevent races between the url-classifier thread (which does most
+ // of the operations) and the main thread (which does memory reporting).
+ // It should be held for all operations between Init() and destruction that
+ // touch this class's data members.
+ mozilla::Mutex mLock;
+ // list of fully stored prefixes, that also form the
+ // start of a run of deltas in mIndexDeltas.
+ nsTArray<uint32_t> mIndexPrefixes;
+ // array containing arrays of deltas from indices.
+ // Index to the place that matches the closest lower
+ // prefix from mIndexPrefix. Then every "delta" corresponds
+ // to a prefix in the PrefixSet.
+ nsTArray<nsTArray<uint16_t> > mIndexDeltas;
+ // how many prefixes we have.
+ uint32_t mTotalPrefixes;
+
+ nsCString mMemoryReportPath;
+};
+
+#endif
diff --git a/toolkit/components/url-classifier/nsUrlClassifierProxies.cpp b/toolkit/components/url-classifier/nsUrlClassifierProxies.cpp
new file mode 100644
index 0000000000..90cb967ea6
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierProxies.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 "nsUrlClassifierProxies.h"
+#include "nsUrlClassifierDBService.h"
+
+#include "mozilla/SyncRunnable.h"
+
+using namespace mozilla::safebrowsing;
+using mozilla::NewRunnableMethod;
+
+static nsresult
+DispatchToWorkerThread(nsIRunnable* r)
+{
+ nsIThread* t = nsUrlClassifierDBService::BackgroundThread();
+ if (!t)
+ return NS_ERROR_FAILURE;
+
+ return t->Dispatch(r, NS_DISPATCH_NORMAL);
+}
+
+NS_IMPL_ISUPPORTS(UrlClassifierDBServiceWorkerProxy, nsIUrlClassifierDBService)
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::Lookup(nsIPrincipal* aPrincipal,
+ const nsACString& aTables,
+ nsIUrlClassifierCallback* aCB)
+{
+ nsCOMPtr<nsIRunnable> r = new LookupRunnable(mTarget, aPrincipal, aTables,
+ aCB);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::LookupRunnable::Run()
+{
+ (void) mTarget->Lookup(mPrincipal, mLookupTables, mCB);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::GetTables(nsIUrlClassifierCallback* aCB)
+{
+ nsCOMPtr<nsIRunnable> r = new GetTablesRunnable(mTarget, aCB);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::GetTablesRunnable::Run()
+{
+ mTarget->GetTables(mCB);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::SetHashCompleter
+ (const nsACString&, nsIUrlClassifierHashCompleter*)
+{
+ NS_NOTREACHED("This method should not be called!");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::BeginUpdate
+ (nsIUrlClassifierUpdateObserver* aUpdater,
+ const nsACString& aTables)
+{
+ nsCOMPtr<nsIRunnable> r = new BeginUpdateRunnable(mTarget, aUpdater,
+ aTables);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::BeginUpdateRunnable::Run()
+{
+ mTarget->BeginUpdate(mUpdater, mTables);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::BeginStream(const nsACString& aTable)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new BeginStreamRunnable(mTarget, aTable);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::BeginStreamRunnable::Run()
+{
+ mTarget->BeginStream(mTable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::UpdateStream(const nsACString& aUpdateChunk)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new UpdateStreamRunnable(mTarget, aUpdateChunk);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::UpdateStreamRunnable::Run()
+{
+ mTarget->UpdateStream(mUpdateChunk);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::FinishStream()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::FinishStream);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::DoLocalLookupRunnable::Run()
+{
+ mTarget->DoLocalLookup(mSpec, mTables, mResults);
+ return NS_OK;
+}
+
+nsresult
+UrlClassifierDBServiceWorkerProxy::DoLocalLookup(const nsACString& spec,
+ const nsACString& tables,
+ LookupResultArray* results)
+
+{
+ // Run synchronously on background thread. NS_DISPATCH_SYNC does *not* do
+ // what we want -- it continues processing events on the main thread loop
+ // before the Dispatch returns.
+ nsCOMPtr<nsIRunnable> r = new DoLocalLookupRunnable(mTarget, spec, tables, results);
+ nsIThread* t = nsUrlClassifierDBService::BackgroundThread();
+ if (!t)
+ return NS_ERROR_FAILURE;
+
+ mozilla::SyncRunnable::DispatchToThread(t, r);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::FinishUpdate()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::FinishUpdate);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::CancelUpdate()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::CancelUpdate);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::ResetDatabase()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::ResetDatabase);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::ReloadDatabase()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::ReloadDatabase);
+ return DispatchToWorkerThread(r);
+}
+
+nsresult
+UrlClassifierDBServiceWorkerProxy::OpenDb()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::OpenDb);
+ return DispatchToWorkerThread(r);
+}
+
+nsresult
+UrlClassifierDBServiceWorkerProxy::CloseDb()
+{
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod(mTarget,
+ &nsUrlClassifierDBServiceWorker::CloseDb);
+ return DispatchToWorkerThread(r);
+}
+
+nsresult
+UrlClassifierDBServiceWorkerProxy::CacheCompletions(CacheResultArray * aEntries)
+{
+ nsCOMPtr<nsIRunnable> r = new CacheCompletionsRunnable(mTarget, aEntries);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::CacheCompletionsRunnable::Run()
+{
+ mTarget->CacheCompletions(mEntries);
+ return NS_OK;
+}
+
+nsresult
+UrlClassifierDBServiceWorkerProxy::CacheMisses(PrefixArray * aEntries)
+{
+ nsCOMPtr<nsIRunnable> r = new CacheMissesRunnable(mTarget, aEntries);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::CacheMissesRunnable::Run()
+{
+ mTarget->CacheMisses(mEntries);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::SetLastUpdateTime(const nsACString& table,
+ uint64_t lastUpdateTime)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new SetLastUpdateTimeRunnable(mTarget, table, lastUpdateTime);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::SetLastUpdateTimeRunnable::Run()
+{
+ mTarget->SetLastUpdateTime(mTable, mUpdateTime);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::ClearLastResults()
+{
+ nsCOMPtr<nsIRunnable> r = new ClearLastResultsRunnable(mTarget);
+ return DispatchToWorkerThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierDBServiceWorkerProxy::ClearLastResultsRunnable::Run()
+{
+ return mTarget->ClearLastResults();
+}
+
+NS_IMPL_ISUPPORTS(UrlClassifierLookupCallbackProxy,
+ nsIUrlClassifierLookupCallback)
+
+NS_IMETHODIMP
+UrlClassifierLookupCallbackProxy::LookupComplete
+ (LookupResultArray * aResults)
+{
+ nsCOMPtr<nsIRunnable> r = new LookupCompleteRunnable(mTarget, aResults);
+ return NS_DispatchToMainThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierLookupCallbackProxy::LookupCompleteRunnable::Run()
+{
+ mTarget->LookupComplete(mResults);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(UrlClassifierCallbackProxy,
+ nsIUrlClassifierCallback)
+
+NS_IMETHODIMP
+UrlClassifierCallbackProxy::HandleEvent(const nsACString& aValue)
+{
+ nsCOMPtr<nsIRunnable> r = new HandleEventRunnable(mTarget, aValue);
+ return NS_DispatchToMainThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierCallbackProxy::HandleEventRunnable::Run()
+{
+ mTarget->HandleEvent(mValue);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(UrlClassifierUpdateObserverProxy,
+ nsIUrlClassifierUpdateObserver)
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::UpdateUrlRequested
+ (const nsACString& aURL,
+ const nsACString& aTable)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new UpdateUrlRequestedRunnable(mTarget, aURL, aTable);
+ return NS_DispatchToMainThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::UpdateUrlRequestedRunnable::Run()
+{
+ mTarget->UpdateUrlRequested(mURL, mTable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::StreamFinished(nsresult aStatus,
+ uint32_t aDelay)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new StreamFinishedRunnable(mTarget, aStatus, aDelay);
+ return NS_DispatchToMainThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::StreamFinishedRunnable::Run()
+{
+ mTarget->StreamFinished(mStatus, mDelay);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::UpdateError(nsresult aError)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new UpdateErrorRunnable(mTarget, aError);
+ return NS_DispatchToMainThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::UpdateErrorRunnable::Run()
+{
+ mTarget->UpdateError(mError);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::UpdateSuccess(uint32_t aRequestedTimeout)
+{
+ nsCOMPtr<nsIRunnable> r =
+ new UpdateSuccessRunnable(mTarget, aRequestedTimeout);
+ return NS_DispatchToMainThread(r);
+}
+
+NS_IMETHODIMP
+UrlClassifierUpdateObserverProxy::UpdateSuccessRunnable::Run()
+{
+ mTarget->UpdateSuccess(mRequestedTimeout);
+ return NS_OK;
+}
diff --git a/toolkit/components/url-classifier/nsUrlClassifierProxies.h b/toolkit/components/url-classifier/nsUrlClassifierProxies.h
new file mode 100644
index 0000000000..3a6c39434d
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierProxies.h
@@ -0,0 +1,373 @@
+/* -*- 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/. */
+
+#ifndef nsUrlClassifierProxies_h
+#define nsUrlClassifierProxies_h
+
+#include "nsIUrlClassifierDBService.h"
+#include "nsUrlClassifierDBService.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Attributes.h"
+#include "nsIPrincipal.h"
+#include "LookupCache.h"
+
+
+/**
+ * Thread proxy from the main thread to the worker thread.
+ */
+class UrlClassifierDBServiceWorkerProxy final : public nsIUrlClassifierDBService
+{
+public:
+ explicit UrlClassifierDBServiceWorkerProxy(nsUrlClassifierDBServiceWorker* aTarget)
+ : mTarget(aTarget)
+ { }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERDBSERVICE
+
+ class LookupRunnable : public mozilla::Runnable
+ {
+ public:
+ LookupRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ nsIPrincipal* aPrincipal,
+ const nsACString& aTables,
+ nsIUrlClassifierCallback* aCB)
+ : mTarget(aTarget)
+ , mPrincipal(aPrincipal)
+ , mLookupTables(aTables)
+ , mCB(aCB)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ nsCString mLookupTables;
+ nsCOMPtr<nsIUrlClassifierCallback> mCB;
+ };
+
+ class GetTablesRunnable : public mozilla::Runnable
+ {
+ public:
+ GetTablesRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ nsIUrlClassifierCallback* aCB)
+ : mTarget(aTarget)
+ , mCB(aCB)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ nsCOMPtr<nsIUrlClassifierCallback> mCB;
+ };
+
+ class BeginUpdateRunnable : public mozilla::Runnable
+ {
+ public:
+ BeginUpdateRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ nsIUrlClassifierUpdateObserver* aUpdater,
+ const nsACString& aTables)
+ : mTarget(aTarget)
+ , mUpdater(aUpdater)
+ , mTables(aTables)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ nsCOMPtr<nsIUrlClassifierUpdateObserver> mUpdater;
+ nsCString mTables;
+ };
+
+ class BeginStreamRunnable : public mozilla::Runnable
+ {
+ public:
+ BeginStreamRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ const nsACString& aTable)
+ : mTarget(aTarget)
+ , mTable(aTable)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ nsCString mTable;
+ };
+
+ class UpdateStreamRunnable : public mozilla::Runnable
+ {
+ public:
+ UpdateStreamRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ const nsACString& aUpdateChunk)
+ : mTarget(aTarget)
+ , mUpdateChunk(aUpdateChunk)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ nsCString mUpdateChunk;
+ };
+
+ class CacheCompletionsRunnable : public mozilla::Runnable
+ {
+ public:
+ CacheCompletionsRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ mozilla::safebrowsing::CacheResultArray *aEntries)
+ : mTarget(aTarget)
+ , mEntries(aEntries)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ mozilla::safebrowsing::CacheResultArray *mEntries;
+ };
+
+ class CacheMissesRunnable : public mozilla::Runnable
+ {
+ public:
+ CacheMissesRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ mozilla::safebrowsing::PrefixArray *aEntries)
+ : mTarget(aTarget)
+ , mEntries(aEntries)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ mozilla::safebrowsing::PrefixArray *mEntries;
+ };
+
+ class DoLocalLookupRunnable : public mozilla::Runnable
+ {
+ public:
+ DoLocalLookupRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ const nsACString& spec,
+ const nsACString& tables,
+ mozilla::safebrowsing::LookupResultArray* results)
+ : mTarget(aTarget)
+ , mSpec(spec)
+ , mTables(tables)
+ , mResults(results)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+
+ nsCString mSpec;
+ nsCString mTables;
+ mozilla::safebrowsing::LookupResultArray* mResults;
+ };
+
+ class SetLastUpdateTimeRunnable : public mozilla::Runnable
+ {
+ public:
+ SetLastUpdateTimeRunnable(nsUrlClassifierDBServiceWorker* aTarget,
+ const nsACString& table,
+ uint64_t updateTime)
+ : mTarget(aTarget),
+ mTable(table),
+ mUpdateTime(updateTime)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ nsCString mTable;
+ uint64_t mUpdateTime;
+ };
+
+ class ClearLastResultsRunnable : public mozilla::Runnable
+ {
+ public:
+ explicit ClearLastResultsRunnable(nsUrlClassifierDBServiceWorker* aTarget)
+ : mTarget(aTarget)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+ private:
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+ };
+
+public:
+ nsresult DoLocalLookup(const nsACString& spec,
+ const nsACString& tables,
+ mozilla::safebrowsing::LookupResultArray* results);
+
+ nsresult OpenDb();
+ nsresult CloseDb();
+
+ nsresult CacheCompletions(mozilla::safebrowsing::CacheResultArray * aEntries);
+ nsresult CacheMisses(mozilla::safebrowsing::PrefixArray * aEntries);
+
+private:
+ ~UrlClassifierDBServiceWorkerProxy() {}
+
+ RefPtr<nsUrlClassifierDBServiceWorker> mTarget;
+};
+
+// The remaining classes here are all proxies to the main thread
+
+class UrlClassifierLookupCallbackProxy final :
+ public nsIUrlClassifierLookupCallback
+{
+public:
+ explicit UrlClassifierLookupCallbackProxy(nsIUrlClassifierLookupCallback* aTarget)
+ : mTarget(new nsMainThreadPtrHolder<nsIUrlClassifierLookupCallback>(aTarget))
+ { }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERLOOKUPCALLBACK
+
+ class LookupCompleteRunnable : public mozilla::Runnable
+ {
+ public:
+ LookupCompleteRunnable(const nsMainThreadPtrHandle<nsIUrlClassifierLookupCallback>& aTarget,
+ mozilla::safebrowsing::LookupResultArray *aResults)
+ : mTarget(aTarget)
+ , mResults(aResults)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ nsMainThreadPtrHandle<nsIUrlClassifierLookupCallback> mTarget;
+ mozilla::safebrowsing::LookupResultArray * mResults;
+ };
+
+private:
+ ~UrlClassifierLookupCallbackProxy() {}
+
+ nsMainThreadPtrHandle<nsIUrlClassifierLookupCallback> mTarget;
+};
+
+class UrlClassifierCallbackProxy final : public nsIUrlClassifierCallback
+{
+public:
+ explicit UrlClassifierCallbackProxy(nsIUrlClassifierCallback* aTarget)
+ : mTarget(new nsMainThreadPtrHolder<nsIUrlClassifierCallback>(aTarget))
+ { }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERCALLBACK
+
+ class HandleEventRunnable : public mozilla::Runnable
+ {
+ public:
+ HandleEventRunnable(const nsMainThreadPtrHandle<nsIUrlClassifierCallback>& aTarget,
+ const nsACString& aValue)
+ : mTarget(aTarget)
+ , mValue(aValue)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ nsMainThreadPtrHandle<nsIUrlClassifierCallback> mTarget;
+ nsCString mValue;
+ };
+
+private:
+ ~UrlClassifierCallbackProxy() {}
+
+ nsMainThreadPtrHandle<nsIUrlClassifierCallback> mTarget;
+};
+
+class UrlClassifierUpdateObserverProxy final :
+ public nsIUrlClassifierUpdateObserver
+{
+public:
+ explicit UrlClassifierUpdateObserverProxy(nsIUrlClassifierUpdateObserver* aTarget)
+ : mTarget(new nsMainThreadPtrHolder<nsIUrlClassifierUpdateObserver>(aTarget))
+ { }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERUPDATEOBSERVER
+
+ class UpdateUrlRequestedRunnable : public mozilla::Runnable
+ {
+ public:
+ UpdateUrlRequestedRunnable(const nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver>& aTarget,
+ const nsACString& aURL,
+ const nsACString& aTable)
+ : mTarget(aTarget)
+ , mURL(aURL)
+ , mTable(aTable)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver> mTarget;
+ nsCString mURL, mTable;
+ };
+
+ class StreamFinishedRunnable : public mozilla::Runnable
+ {
+ public:
+ StreamFinishedRunnable(const nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver>& aTarget,
+ nsresult aStatus, uint32_t aDelay)
+ : mTarget(aTarget)
+ , mStatus(aStatus)
+ , mDelay(aDelay)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver> mTarget;
+ nsresult mStatus;
+ uint32_t mDelay;
+ };
+
+ class UpdateErrorRunnable : public mozilla::Runnable
+ {
+ public:
+ UpdateErrorRunnable(const nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver>& aTarget,
+ nsresult aError)
+ : mTarget(aTarget)
+ , mError(aError)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver> mTarget;
+ nsresult mError;
+ };
+
+ class UpdateSuccessRunnable : public mozilla::Runnable
+ {
+ public:
+ UpdateSuccessRunnable(const nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver>& aTarget,
+ uint32_t aRequestedTimeout)
+ : mTarget(aTarget)
+ , mRequestedTimeout(aRequestedTimeout)
+ { }
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver> mTarget;
+ uint32_t mRequestedTimeout;
+ };
+
+private:
+ ~UrlClassifierUpdateObserverProxy() {}
+
+ nsMainThreadPtrHandle<nsIUrlClassifierUpdateObserver> mTarget;
+};
+
+#endif // nsUrlClassifierProxies_h
diff --git a/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp
new file mode 100644
index 0000000000..554bff3422
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.cpp
@@ -0,0 +1,812 @@
+//* -*- 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 "nsCRT.h"
+#include "nsIHttpChannel.h"
+#include "nsIObserverService.h"
+#include "nsIStringStream.h"
+#include "nsIUploadChannel.h"
+#include "nsIURI.h"
+#include "nsIUrlClassifierDBService.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsToolkitCompsCID.h"
+#include "nsUrlClassifierStreamUpdater.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Logging.h"
+#include "nsIInterfaceRequestor.h"
+#include "mozilla/LoadContext.h"
+#include "mozilla/Telemetry.h"
+#include "nsContentUtils.h"
+#include "nsIURLFormatter.h"
+
+using mozilla::DocShellOriginAttributes;
+
+static const char* gQuitApplicationMessage = "quit-application";
+
+// Limit the list file size to 32mb
+const uint32_t MAX_FILE_SIZE = (32 * 1024 * 1024);
+
+#undef LOG
+
+// MOZ_LOG=UrlClassifierStreamUpdater:5
+static mozilla::LazyLogModule gUrlClassifierStreamUpdaterLog("UrlClassifierStreamUpdater");
+#define LOG(args) TrimAndLog args
+
+// Calls nsIURLFormatter::TrimSensitiveURLs to remove sensitive
+// info from the logging message.
+static void TrimAndLog(const char* aFmt, ...)
+{
+ nsString raw;
+
+ va_list ap;
+ va_start(ap, aFmt);
+ raw.AppendPrintf(aFmt, ap);
+ va_end(ap);
+
+ nsCOMPtr<nsIURLFormatter> urlFormatter =
+ do_GetService("@mozilla.org/toolkit/URLFormatterService;1");
+
+ nsString trimmed;
+ nsresult rv = urlFormatter->TrimSensitiveURLs(raw, trimmed);
+ if (NS_FAILED(rv)) {
+ trimmed = EmptyString();
+ }
+
+ MOZ_LOG(gUrlClassifierStreamUpdaterLog,
+ mozilla::LogLevel::Debug,
+ (NS_ConvertUTF16toUTF8(trimmed).get()));
+}
+
+// This class does absolutely nothing, except pass requests onto the DBService.
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIUrlClassiferStreamUpdater implementation
+// Handles creating/running the stream listener
+
+nsUrlClassifierStreamUpdater::nsUrlClassifierStreamUpdater()
+ : mIsUpdating(false), mInitialized(false), mDownloadError(false),
+ mBeganStream(false), mChannel(nullptr)
+{
+ LOG(("nsUrlClassifierStreamUpdater init [this=%p]", this));
+}
+
+NS_IMPL_ISUPPORTS(nsUrlClassifierStreamUpdater,
+ nsIUrlClassifierStreamUpdater,
+ nsIUrlClassifierUpdateObserver,
+ nsIRequestObserver,
+ nsIStreamListener,
+ nsIObserver,
+ nsIInterfaceRequestor,
+ nsITimerCallback)
+
+/**
+ * Clear out the update.
+ */
+void
+nsUrlClassifierStreamUpdater::DownloadDone()
+{
+ LOG(("nsUrlClassifierStreamUpdater::DownloadDone [this=%p]", this));
+ mIsUpdating = false;
+
+ mPendingUpdates.Clear();
+ mDownloadError = false;
+ mSuccessCallback = nullptr;
+ mUpdateErrorCallback = nullptr;
+ mDownloadErrorCallback = nullptr;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIUrlClassifierStreamUpdater implementation
+
+nsresult
+nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl,
+ const nsACString & aRequestPayload,
+ bool aIsPostRequest,
+ const nsACString & aStreamTable)
+{
+
+#ifdef DEBUG
+ LOG(("Fetching update %s from %s",
+ aRequestPayload.Data(), aUpdateUrl->GetSpecOrDefault().get()));
+#endif
+
+ nsresult rv;
+ uint32_t loadFlags = nsIChannel::INHIBIT_CACHING |
+ nsIChannel::LOAD_BYPASS_CACHE;
+ rv = NS_NewChannel(getter_AddRefs(mChannel),
+ aUpdateUrl,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER,
+ nullptr, // aLoadGroup
+ this, // aInterfaceRequestor
+ loadFlags);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
+ loadInfo->SetOriginAttributes(mozilla::NeckoOriginAttributes(NECKO_SAFEBROWSING_APP_ID, false));
+
+ mBeganStream = false;
+
+ if (!aIsPostRequest) {
+ // We use POST method to send our request in v2. In v4, the request
+ // needs to be embedded to the URL and use GET method to send.
+ // However, from the Chromium source code, a extended HTTP header has
+ // to be sent along with the request to make the request succeed.
+ // The following description is from Chromium source code:
+ //
+ // "The following header informs the envelope server (which sits in
+ // front of Google's stubby server) that the received GET request should be
+ // interpreted as a POST."
+ //
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-HTTP-Method-Override"),
+ NS_LITERAL_CSTRING("POST"),
+ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (!aRequestPayload.IsEmpty()) {
+ rv = AddRequestBody(aRequestPayload);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set the appropriate content type for file/data URIs, for unit testing
+ // purposes.
+ // This is only used for testing and should be deleted.
+ bool match;
+ if ((NS_SUCCEEDED(aUpdateUrl->SchemeIs("file", &match)) && match) ||
+ (NS_SUCCEEDED(aUpdateUrl->SchemeIs("data", &match)) && match)) {
+ mChannel->SetContentType(NS_LITERAL_CSTRING("application/vnd.google.safebrowsing-update"));
+ } else {
+ // We assume everything else is an HTTP request.
+
+ // Disable keepalive.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Connection"), NS_LITERAL_CSTRING("close"), false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Create a custom LoadContext for SafeBrowsing, so we can use callbacks on
+ // the channel to query the appId which allows separation of safebrowsing
+ // cookies in a separate jar.
+ DocShellOriginAttributes attrs;
+ attrs.mAppId = NECKO_SAFEBROWSING_APP_ID;
+ nsCOMPtr<nsIInterfaceRequestor> sbContext = new mozilla::LoadContext(attrs);
+ rv = mChannel->SetNotificationCallbacks(sbContext);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make the request.
+ rv = mChannel->AsyncOpen2(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mStreamTable = aStreamTable;
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierStreamUpdater::FetchUpdate(const nsACString & aUpdateUrl,
+ const nsACString & aRequestPayload,
+ bool aIsPostRequest,
+ const nsACString & aStreamTable)
+{
+ LOG(("(pre) Fetching update from %s\n", PromiseFlatCString(aUpdateUrl).get()));
+
+ nsCString updateUrl(aUpdateUrl);
+ if (!aIsPostRequest) {
+ updateUrl.AppendPrintf("&$req=%s", nsCString(aRequestPayload).get());
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), updateUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString urlSpec;
+ uri->GetAsciiSpec(urlSpec);
+
+ LOG(("(post) Fetching update from %s\n", urlSpec.get()));
+
+ return FetchUpdate(uri, aRequestPayload, aIsPostRequest, aStreamTable);
+}
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::DownloadUpdates(
+ const nsACString &aRequestTables,
+ const nsACString &aRequestPayload,
+ bool aIsPostRequest,
+ const nsACString &aUpdateUrl,
+ nsIUrlClassifierCallback *aSuccessCallback,
+ nsIUrlClassifierCallback *aUpdateErrorCallback,
+ nsIUrlClassifierCallback *aDownloadErrorCallback,
+ bool *_retval)
+{
+ NS_ENSURE_ARG(aSuccessCallback);
+ NS_ENSURE_ARG(aUpdateErrorCallback);
+ NS_ENSURE_ARG(aDownloadErrorCallback);
+
+ if (mIsUpdating) {
+ LOG(("Already updating, queueing update %s from %s", aRequestPayload.Data(),
+ aUpdateUrl.Data()));
+ *_retval = false;
+ PendingRequest *request = mPendingRequests.AppendElement();
+ request->mTables = aRequestTables;
+ request->mRequestPayload = aRequestPayload;
+ request->mIsPostRequest = aIsPostRequest;
+ request->mUrl = aUpdateUrl;
+ request->mSuccessCallback = aSuccessCallback;
+ request->mUpdateErrorCallback = aUpdateErrorCallback;
+ request->mDownloadErrorCallback = aDownloadErrorCallback;
+ return NS_OK;
+ }
+
+ if (aUpdateUrl.IsEmpty()) {
+ NS_ERROR("updateUrl not set");
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv;
+
+ if (!mInitialized) {
+ // Add an observer for shutdown so we can cancel any pending list
+ // downloads. quit-application is the same event that the download
+ // manager listens for and uses to cancel pending downloads.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService)
+ return NS_ERROR_FAILURE;
+
+ observerService->AddObserver(this, gQuitApplicationMessage, false);
+
+ mDBService = do_GetService(NS_URLCLASSIFIERDBSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mInitialized = true;
+ }
+
+ rv = mDBService->BeginUpdate(this, aRequestTables);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ LOG(("Service busy, already updating, queuing update %s from %s",
+ aRequestPayload.Data(), aUpdateUrl.Data()));
+ *_retval = false;
+ PendingRequest *request = mPendingRequests.AppendElement();
+ request->mTables = aRequestTables;
+ request->mRequestPayload = aRequestPayload;
+ request->mIsPostRequest = aIsPostRequest;
+ request->mUrl = aUpdateUrl;
+ request->mSuccessCallback = aSuccessCallback;
+ request->mUpdateErrorCallback = aUpdateErrorCallback;
+ request->mDownloadErrorCallback = aDownloadErrorCallback;
+ return NS_OK;
+ }
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mSuccessCallback = aSuccessCallback;
+ mUpdateErrorCallback = aUpdateErrorCallback;
+ mDownloadErrorCallback = aDownloadErrorCallback;
+
+ mIsUpdating = true;
+ *_retval = true;
+
+ LOG(("FetchUpdate: %s", aUpdateUrl.Data()));
+
+ return FetchUpdate(aUpdateUrl, aRequestPayload, aIsPostRequest, EmptyCString());
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIUrlClassifierUpdateObserver implementation
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::UpdateUrlRequested(const nsACString &aUrl,
+ const nsACString &aTable)
+{
+ LOG(("Queuing requested update from %s\n", PromiseFlatCString(aUrl).get()));
+
+ PendingUpdate *update = mPendingUpdates.AppendElement();
+ if (!update)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // Allow data: and file: urls for unit testing purposes, otherwise assume http
+ if (StringBeginsWith(aUrl, NS_LITERAL_CSTRING("data:")) ||
+ StringBeginsWith(aUrl, NS_LITERAL_CSTRING("file:"))) {
+ update->mUrl = aUrl;
+ } else {
+ // For unittesting update urls to localhost should use http, not https
+ // (otherwise the connection will fail silently, since there will be no
+ // cert available).
+ if (!StringBeginsWith(aUrl, NS_LITERAL_CSTRING("localhost"))) {
+ update->mUrl = NS_LITERAL_CSTRING("https://") + aUrl;
+ } else {
+ update->mUrl = NS_LITERAL_CSTRING("http://") + aUrl;
+ }
+ }
+ update->mTable = aTable;
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierStreamUpdater::FetchNext()
+{
+ if (mPendingUpdates.Length() == 0) {
+ return NS_OK;
+ }
+
+ PendingUpdate &update = mPendingUpdates[0];
+ LOG(("Fetching update url: %s\n", update.mUrl.get()));
+ nsresult rv = FetchUpdate(update.mUrl,
+ EmptyCString(),
+ true, // This method is for v2 and v2 is always a POST.
+ update.mTable);
+ if (NS_FAILED(rv)) {
+ LOG(("Error fetching update url: %s\n", update.mUrl.get()));
+ // We can commit the urls that we've applied so far. This is
+ // probably a transient server problem, so trigger backoff.
+ mDownloadErrorCallback->HandleEvent(EmptyCString());
+ mDownloadError = true;
+ mDBService->FinishUpdate();
+ return rv;
+ }
+
+ mPendingUpdates.RemoveElementAt(0);
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierStreamUpdater::FetchNextRequest()
+{
+ if (mPendingRequests.Length() == 0) {
+ LOG(("No more requests, returning"));
+ return NS_OK;
+ }
+
+ PendingRequest &request = mPendingRequests[0];
+ LOG(("Stream updater: fetching next request: %s, %s",
+ request.mTables.get(), request.mUrl.get()));
+ bool dummy;
+ DownloadUpdates(
+ request.mTables,
+ request.mRequestPayload,
+ request.mIsPostRequest,
+ request.mUrl,
+ request.mSuccessCallback,
+ request.mUpdateErrorCallback,
+ request.mDownloadErrorCallback,
+ &dummy);
+ request.mSuccessCallback = nullptr;
+ request.mUpdateErrorCallback = nullptr;
+ request.mDownloadErrorCallback = nullptr;
+ mPendingRequests.RemoveElementAt(0);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::StreamFinished(nsresult status,
+ uint32_t requestedDelay)
+{
+ // We are a service and may not be reset with Init between calls, so reset
+ // mBeganStream manually.
+ mBeganStream = false;
+ LOG(("nsUrlClassifierStreamUpdater::StreamFinished [%x, %d]", status, requestedDelay));
+ if (NS_FAILED(status) || mPendingUpdates.Length() == 0) {
+ // We're done.
+ LOG(("nsUrlClassifierStreamUpdater::Done [this=%p]", this));
+ mDBService->FinishUpdate();
+ return NS_OK;
+ }
+
+ // This timer is for fetching indirect updates ("forwards") from any "u:" lines
+ // that we encountered while processing the server response. It is NOT for
+ // scheduling the next time we pull the list from the server. That's a different
+ // timer in listmanager.js (see bug 1110891).
+ nsresult rv;
+ mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = mTimer->InitWithCallback(this, requestedDelay,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Unable to initialize timer, fetching next safebrowsing item immediately");
+ return FetchNext();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::UpdateSuccess(uint32_t requestedTimeout)
+{
+ LOG(("nsUrlClassifierStreamUpdater::UpdateSuccess [this=%p]", this));
+ if (mPendingUpdates.Length() != 0) {
+ NS_WARNING("Didn't fetch all safebrowsing update redirects");
+ }
+
+ // DownloadDone() clears mSuccessCallback, so we save it off here.
+ nsCOMPtr<nsIUrlClassifierCallback> successCallback = mDownloadError ? nullptr : mSuccessCallback.get();
+ DownloadDone();
+
+ nsAutoCString strTimeout;
+ strTimeout.AppendInt(requestedTimeout);
+ if (successCallback) {
+ LOG(("nsUrlClassifierStreamUpdater::UpdateSuccess callback [this=%p]",
+ this));
+ successCallback->HandleEvent(strTimeout);
+ } else {
+ LOG(("nsUrlClassifierStreamUpdater::UpdateSuccess skipping callback [this=%p]",
+ this));
+ }
+ // Now fetch the next request
+ LOG(("stream updater: calling into fetch next request"));
+ FetchNextRequest();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::UpdateError(nsresult result)
+{
+ LOG(("nsUrlClassifierStreamUpdater::UpdateError [this=%p]", this));
+
+ // DownloadDone() clears mUpdateErrorCallback, so we save it off here.
+ nsCOMPtr<nsIUrlClassifierCallback> errorCallback = mDownloadError ? nullptr : mUpdateErrorCallback.get();
+
+ DownloadDone();
+
+ nsAutoCString strResult;
+ strResult.AppendInt(static_cast<uint32_t>(result));
+ if (errorCallback) {
+ errorCallback->HandleEvent(strResult);
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierStreamUpdater::AddRequestBody(const nsACString &aRequestBody)
+{
+ nsresult rv;
+ nsCOMPtr<nsIStringInputStream> strStream =
+ do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = strStream->SetData(aRequestBody.BeginReading(),
+ aRequestBody.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIUploadChannel> uploadChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = uploadChannel->SetUploadStream(strStream,
+ NS_LITERAL_CSTRING("text/plain"),
+ -1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("POST"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// Map the HTTP response code to a Telemetry bucket
+static uint32_t HTTPStatusToBucket(uint32_t status)
+{
+ uint32_t statusBucket;
+ switch (status) {
+ case 100:
+ case 101:
+ // Unexpected 1xx return code
+ statusBucket = 0;
+ break;
+ case 200:
+ // OK - Data is available in the HTTP response body.
+ statusBucket = 1;
+ break;
+ case 201:
+ case 202:
+ case 203:
+ case 205:
+ case 206:
+ // Unexpected 2xx return code
+ statusBucket = 2;
+ break;
+ case 204:
+ // No Content
+ statusBucket = 3;
+ break;
+ case 300:
+ case 301:
+ case 302:
+ case 303:
+ case 304:
+ case 305:
+ case 307:
+ case 308:
+ // Unexpected 3xx return code
+ statusBucket = 4;
+ break;
+ case 400:
+ // Bad Request - The HTTP request was not correctly formed.
+ // The client did not provide all required CGI parameters.
+ statusBucket = 5;
+ break;
+ case 401:
+ case 402:
+ case 405:
+ case 406:
+ case 407:
+ case 409:
+ case 410:
+ case 411:
+ case 412:
+ case 414:
+ case 415:
+ case 416:
+ case 417:
+ case 421:
+ case 426:
+ case 428:
+ case 429:
+ case 431:
+ case 451:
+ // Unexpected 4xx return code
+ statusBucket = 6;
+ break;
+ case 403:
+ // Forbidden - The client id is invalid.
+ statusBucket = 7;
+ break;
+ case 404:
+ // Not Found
+ statusBucket = 8;
+ break;
+ case 408:
+ // Request Timeout
+ statusBucket = 9;
+ break;
+ case 413:
+ // Request Entity Too Large - Bug 1150334
+ statusBucket = 10;
+ break;
+ case 500:
+ case 501:
+ case 510:
+ // Unexpected 5xx return code
+ statusBucket = 11;
+ break;
+ case 502:
+ case 504:
+ case 511:
+ // Local network errors, we'll ignore these.
+ statusBucket = 12;
+ break;
+ case 503:
+ // Service Unavailable - The server cannot handle the request.
+ // Clients MUST follow the backoff behavior specified in the
+ // Request Frequency section.
+ statusBucket = 13;
+ break;
+ case 505:
+ // HTTP Version Not Supported - The server CANNOT handle the requested
+ // protocol major version.
+ statusBucket = 14;
+ break;
+ default:
+ statusBucket = 15;
+ };
+ return statusBucket;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIStreamListenerObserver implementation
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::OnStartRequest(nsIRequest *request,
+ nsISupports* context)
+{
+ nsresult rv;
+ bool downloadError = false;
+ nsAutoCString strStatus;
+ nsresult status = NS_OK;
+
+ // Only update if we got http success header
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request);
+ if (httpChannel) {
+ rv = httpChannel->GetStatus(&status);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MOZ_LOG_TEST(gUrlClassifierStreamUpdaterLog, mozilla::LogLevel::Debug)) {
+ nsAutoCString errorName, spec;
+ mozilla::GetErrorName(status, errorName);
+ nsCOMPtr<nsIURI> uri;
+ rv = httpChannel->GetURI(getter_AddRefs(uri));
+ if (NS_SUCCEEDED(rv) && uri) {
+ uri->GetAsciiSpec(spec);
+ }
+ LOG(("nsUrlClassifierStreamUpdater::OnStartRequest "
+ "(status=%s, uri=%s, this=%p)", errorName.get(),
+ spec.get(), this));
+ }
+
+ if (NS_FAILED(status)) {
+ // Assume we're overloading the server and trigger backoff.
+ downloadError = true;
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::URLCLASSIFIER_UPDATE_REMOTE_STATUS,
+ 15 /* unknown response code */);
+
+ } else {
+ bool succeeded = false;
+ rv = httpChannel->GetRequestSucceeded(&succeeded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t requestStatus;
+ rv = httpChannel->GetResponseStatus(&requestStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::URLCLASSIFIER_UPDATE_REMOTE_STATUS,
+ HTTPStatusToBucket(requestStatus));
+ LOG(("nsUrlClassifierStreamUpdater::OnStartRequest %s (%d)", succeeded ?
+ "succeeded" : "failed", requestStatus));
+ if (!succeeded) {
+ // 404 or other error, pass error status back
+ strStatus.AppendInt(requestStatus);
+ downloadError = true;
+ }
+ }
+ }
+
+ if (downloadError) {
+ LOG(("nsUrlClassifierStreamUpdater::Download error [this=%p]", this));
+
+ // It's possible for mDownloadErrorCallback to be null on shutdown.
+ if (mDownloadErrorCallback) {
+ mDownloadErrorCallback->HandleEvent(strStatus);
+ }
+
+ mDownloadError = true;
+ status = NS_ERROR_ABORT;
+ } else if (NS_SUCCEEDED(status)) {
+ MOZ_ASSERT(mDownloadErrorCallback);
+ mBeganStream = true;
+ LOG(("nsUrlClassifierStreamUpdater::Beginning stream [this=%p]", this));
+ rv = mDBService->BeginStream(mStreamTable);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mStreamTable.Truncate();
+
+ return status;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::OnDataAvailable(nsIRequest *request,
+ nsISupports* context,
+ nsIInputStream *aIStream,
+ uint64_t aSourceOffset,
+ uint32_t aLength)
+{
+ if (!mDBService)
+ return NS_ERROR_NOT_INITIALIZED;
+
+ LOG(("OnDataAvailable (%d bytes)", aLength));
+
+ if (aSourceOffset > MAX_FILE_SIZE) {
+ LOG(("OnDataAvailable::Abort because exceeded the maximum file size(%lld)", aSourceOffset));
+ return NS_ERROR_FILE_TOO_BIG;
+ }
+
+ nsresult rv;
+
+ // Copy the data into a nsCString
+ nsCString chunk;
+ rv = NS_ConsumeStream(aIStream, aLength, chunk);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ //LOG(("Chunk (%d): %s\n\n", chunk.Length(), chunk.get()));
+ rv = mDBService->UpdateStream(chunk);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::OnStopRequest(nsIRequest *request, nsISupports* context,
+ nsresult aStatus)
+{
+ if (!mDBService)
+ return NS_ERROR_NOT_INITIALIZED;
+
+ LOG(("OnStopRequest (status %x, beganStream %s, this=%p)", aStatus,
+ mBeganStream ? "true" : "false", this));
+
+ nsresult rv;
+
+ if (NS_SUCCEEDED(aStatus)) {
+ // Success, finish this stream and move on to the next.
+ rv = mDBService->FinishStream();
+ } else if (mBeganStream) {
+ LOG(("OnStopRequest::Canceling update [this=%p]", this));
+ // We began this stream and couldn't finish it. We have to cancel the
+ // update, it's not in a consistent state.
+ rv = mDBService->CancelUpdate();
+ } else {
+ LOG(("OnStopRequest::Finishing update [this=%p]", this));
+ // The fetch failed, but we didn't start the stream (probably a
+ // server or connection error). We can commit what we've applied
+ // so far, and request again later.
+ rv = mDBService->FinishUpdate();
+ }
+
+ mChannel = nullptr;
+
+ // If the fetch failed, return the network status rather than NS_OK, the
+ // result of finishing a possibly-empty update
+ if (NS_SUCCEEDED(aStatus)) {
+ return rv;
+ }
+ return aStatus;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIObserver implementation
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ if (nsCRT::strcmp(aTopic, gQuitApplicationMessage) == 0) {
+ if (mIsUpdating && mChannel) {
+ LOG(("Cancel download"));
+ nsresult rv;
+ rv = mChannel->Cancel(NS_ERROR_ABORT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mIsUpdating = false;
+ mChannel = nullptr;
+ }
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ }
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIInterfaceRequestor implementation
+
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::GetInterface(const nsIID & eventSinkIID, void* *_retval)
+{
+ return QueryInterface(eventSinkIID, _retval);
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// nsITimerCallback implementation
+NS_IMETHODIMP
+nsUrlClassifierStreamUpdater::Notify(nsITimer *timer)
+{
+ LOG(("nsUrlClassifierStreamUpdater::Notify [%p]", this));
+
+ mTimer = nullptr;
+
+ // Start the update process up again.
+ FetchNext();
+
+ return NS_OK;
+}
+
diff --git a/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.h b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.h
new file mode 100644
index 0000000000..b24df61d2d
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierStreamUpdater.h
@@ -0,0 +1,103 @@
+//* -*- 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/. */
+
+#ifndef nsUrlClassifierStreamUpdater_h_
+#define nsUrlClassifierStreamUpdater_h_
+
+#include <nsISupportsUtils.h>
+
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsIUrlClassifierStreamUpdater.h"
+#include "nsIStreamListener.h"
+#include "nsIChannel.h"
+#include "nsTArray.h"
+#include "nsITimer.h"
+#include "mozilla/Attributes.h"
+
+// Forward declare pointers
+class nsIURI;
+
+class nsUrlClassifierStreamUpdater final : public nsIUrlClassifierStreamUpdater,
+ public nsIUrlClassifierUpdateObserver,
+ public nsIStreamListener,
+ public nsIObserver,
+ public nsIInterfaceRequestor,
+ public nsITimerCallback
+{
+public:
+ nsUrlClassifierStreamUpdater();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERSTREAMUPDATER
+ NS_DECL_NSIURLCLASSIFIERUPDATEOBSERVER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSITIMERCALLBACK
+
+private:
+ // No subclassing
+ ~nsUrlClassifierStreamUpdater() {}
+
+ // When the dbservice sends an UpdateComplete or UpdateFailure, we call this
+ // to reset the stream updater.
+ void DownloadDone();
+
+ // Disallow copy constructor
+ nsUrlClassifierStreamUpdater(nsUrlClassifierStreamUpdater&);
+
+ nsresult AddRequestBody(const nsACString &aRequestBody);
+
+ // Fetches an update for a single table.
+ nsresult FetchUpdate(nsIURI *aURI,
+ const nsACString &aRequest,
+ bool aIsPostRequest,
+ const nsACString &aTable);
+ // Dumb wrapper so we don't have to create URIs.
+ nsresult FetchUpdate(const nsACString &aURI,
+ const nsACString &aRequest,
+ bool aIsPostRequest,
+ const nsACString &aTable);
+
+ // Fetches the next table, from mPendingUpdates.
+ nsresult FetchNext();
+ // Fetches the next request, from mPendingRequests
+ nsresult FetchNextRequest();
+
+
+ bool mIsUpdating;
+ bool mInitialized;
+ bool mDownloadError;
+ bool mBeganStream;
+ nsCString mStreamTable;
+ nsCOMPtr<nsIChannel> mChannel;
+ nsCOMPtr<nsIUrlClassifierDBService> mDBService;
+ nsCOMPtr<nsITimer> mTimer;
+
+ struct PendingRequest {
+ nsCString mTables;
+ nsCString mRequestPayload;
+ bool mIsPostRequest;
+ nsCString mUrl;
+ nsCOMPtr<nsIUrlClassifierCallback> mSuccessCallback;
+ nsCOMPtr<nsIUrlClassifierCallback> mUpdateErrorCallback;
+ nsCOMPtr<nsIUrlClassifierCallback> mDownloadErrorCallback;
+ };
+ nsTArray<PendingRequest> mPendingRequests;
+
+ struct PendingUpdate {
+ nsCString mUrl;
+ nsCString mTable;
+ };
+ nsTArray<PendingUpdate> mPendingUpdates;
+
+ nsCOMPtr<nsIUrlClassifierCallback> mSuccessCallback;
+ nsCOMPtr<nsIUrlClassifierCallback> mUpdateErrorCallback;
+ nsCOMPtr<nsIUrlClassifierCallback> mDownloadErrorCallback;
+};
+
+#endif // nsUrlClassifierStreamUpdater_h_
diff --git a/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp b/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp
new file mode 100644
index 0000000000..e4cf68c982
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp
@@ -0,0 +1,665 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsEscape.h"
+#include "nsString.h"
+#include "nsIURI.h"
+#include "nsUrlClassifierUtils.h"
+#include "nsTArray.h"
+#include "nsReadableUtils.h"
+#include "plbase64.h"
+#include "nsPrintfCString.h"
+#include "safebrowsing.pb.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Mutex.h"
+
+#define DEFAULT_PROTOCOL_VERSION "2.2"
+
+static char int_to_hex_digit(int32_t i)
+{
+ NS_ASSERTION((i >= 0) && (i <= 15), "int too big in int_to_hex_digit");
+ return static_cast<char>(((i < 10) ? (i + '0') : ((i - 10) + 'A')));
+}
+
+static bool
+IsDecimal(const nsACString & num)
+{
+ for (uint32_t i = 0; i < num.Length(); i++) {
+ if (!isdigit(num[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+IsHex(const nsACString & num)
+{
+ if (num.Length() < 3) {
+ return false;
+ }
+
+ if (num[0] != '0' || !(num[1] == 'x' || num[1] == 'X')) {
+ return false;
+ }
+
+ for (uint32_t i = 2; i < num.Length(); i++) {
+ if (!isxdigit(num[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool
+IsOctal(const nsACString & num)
+{
+ if (num.Length() < 2) {
+ return false;
+ }
+
+ if (num[0] != '0') {
+ return false;
+ }
+
+ for (uint32_t i = 1; i < num.Length(); i++) {
+ if (!isdigit(num[i]) || num[i] == '8' || num[i] == '9') {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/////////////////////////////////////////////////////////////////
+// SafeBrowsing V4 related utits.
+
+namespace mozilla {
+namespace safebrowsing {
+
+static PlatformType
+GetPlatformType()
+{
+#if defined(ANDROID)
+ return ANDROID_PLATFORM;
+#elif defined(XP_MACOSX)
+ return OSX_PLATFORM;
+#elif defined(XP_LINUX)
+ return LINUX_PLATFORM;
+#elif defined(XP_WIN)
+ return WINDOWS_PLATFORM;
+#else
+ return PLATFORM_TYPE_UNSPECIFIED;
+#endif
+}
+
+typedef FetchThreatListUpdatesRequest_ListUpdateRequest ListUpdateRequest;
+typedef FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints Constraints;
+
+static void
+InitListUpdateRequest(ThreatType aThreatType,
+ const char* aStateBase64,
+ ListUpdateRequest* aListUpdateRequest)
+{
+ aListUpdateRequest->set_threat_type(aThreatType);
+ aListUpdateRequest->set_platform_type(GetPlatformType());
+ aListUpdateRequest->set_threat_entry_type(URL);
+
+ Constraints* contraints = new Constraints();
+ contraints->add_supported_compressions(RICE);
+ aListUpdateRequest->set_allocated_constraints(contraints);
+
+ // Only set non-empty state.
+ if (aStateBase64[0] != '\0') {
+ nsCString stateBinary;
+ nsresult rv = Base64Decode(nsCString(aStateBase64), stateBinary);
+ if (NS_SUCCEEDED(rv)) {
+ aListUpdateRequest->set_state(stateBinary.get(), stateBinary.Length());
+ }
+ }
+}
+
+static ClientInfo*
+CreateClientInfo()
+{
+ ClientInfo* c = new ClientInfo();
+
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ nsXPIDLCString clientId;
+ nsresult rv = prefBranch->GetCharPref("browser.safebrowsing.id",
+ getter_Copies(clientId));
+
+ if (NS_FAILED(rv)) {
+ clientId = "Firefox"; // Use "Firefox" as fallback.
+ }
+
+ c->set_client_id(clientId.get());
+
+ return c;
+}
+
+} // end of namespace safebrowsing.
+} // end of namespace mozilla.
+
+nsUrlClassifierUtils::nsUrlClassifierUtils()
+ : mEscapeCharmap(nullptr)
+ , mProviderDictLock("nsUrlClassifierUtils.mProviderDictLock")
+{
+}
+
+nsresult
+nsUrlClassifierUtils::Init()
+{
+ // Everything but alpha numerics, - and .
+ mEscapeCharmap = new Charmap(0xffffffff, 0xfc009fff, 0xf8000001, 0xf8000001,
+ 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff);
+ if (!mEscapeCharmap)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // nsIUrlClassifierUtils is a thread-safe service so it's
+ // allowed to use on non-main threads. However, building
+ // the provider dictionary must be on the main thread.
+ // We forcefully load nsUrlClassifierUtils in
+ // nsUrlClassifierDBService::Init() to ensure we must
+ // now be on the main thread.
+ nsresult rv = ReadProvidersFromPrefs(mProviderDict);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add an observer for shutdown
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService)
+ return NS_ERROR_FAILURE;
+
+ observerService->AddObserver(this, "xpcom-shutdown-threads", false);
+ Preferences::AddStrongObserver(this, "browser.safebrowsing");
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsUrlClassifierUtils,
+ nsIUrlClassifierUtils,
+ nsIObserver)
+
+/////////////////////////////////////////////////////////////////////////////
+// nsIUrlClassifierUtils
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::GetKeyForURI(nsIURI * uri, nsACString & _retval)
+{
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri);
+ if (!innerURI)
+ innerURI = uri;
+
+ nsAutoCString host;
+ innerURI->GetAsciiHost(host);
+
+ if (host.IsEmpty()) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ nsresult rv = CanonicalizeHostname(host, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString path;
+ rv = innerURI->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // strip out anchors
+ int32_t ref = path.FindChar('#');
+ if (ref != kNotFound)
+ path.SetLength(ref);
+
+ nsAutoCString temp;
+ rv = CanonicalizePath(path, temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ _retval.Append(temp);
+
+ return NS_OK;
+}
+
+// We use "goog-*-proto" as the list name for v4, where "proto" indicates
+// it's updated (as well as hash completion) via protobuf.
+//
+// In the mozilla official build, we are allowed to use the
+// private phishing list (goog-phish-proto). See Bug 1288840.
+static const struct {
+ const char* mListName;
+ uint32_t mThreatType;
+} THREAT_TYPE_CONV_TABLE[] = {
+ { "goog-malware-proto", MALWARE_THREAT}, // 1
+ { "googpub-phish-proto", SOCIAL_ENGINEERING_PUBLIC}, // 2
+ { "goog-unwanted-proto", UNWANTED_SOFTWARE}, // 3
+ { "goog-phish-proto", SOCIAL_ENGINEERING}, // 5
+
+ // For testing purpose.
+ { "test-phish-proto", SOCIAL_ENGINEERING_PUBLIC}, // 2
+ { "test-unwanted-proto", UNWANTED_SOFTWARE}, // 3
+};
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::ConvertThreatTypeToListNames(uint32_t aThreatType,
+ nsACString& aListNames)
+{
+ for (uint32_t i = 0; i < ArrayLength(THREAT_TYPE_CONV_TABLE); i++) {
+ if (aThreatType == THREAT_TYPE_CONV_TABLE[i].mThreatType) {
+ if (!aListNames.IsEmpty()) {
+ aListNames.AppendLiteral(",");
+ }
+ aListNames += THREAT_TYPE_CONV_TABLE[i].mListName;
+ }
+ }
+
+ return aListNames.IsEmpty() ? NS_ERROR_FAILURE : NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::ConvertListNameToThreatType(const nsACString& aListName,
+ uint32_t* aThreatType)
+{
+ for (uint32_t i = 0; i < ArrayLength(THREAT_TYPE_CONV_TABLE); i++) {
+ if (aListName.EqualsASCII(THREAT_TYPE_CONV_TABLE[i].mListName)) {
+ *aThreatType = THREAT_TYPE_CONV_TABLE[i].mThreatType;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::GetProvider(const nsACString& aTableName,
+ nsACString& aProvider)
+{
+ MutexAutoLock lock(mProviderDictLock);
+ nsCString* provider = nullptr;
+ if (mProviderDict.Get(aTableName, &provider)) {
+ aProvider = provider ? *provider : EmptyCString();
+ } else {
+ aProvider = EmptyCString();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::GetProtocolVersion(const nsACString& aProvider,
+ nsACString& aVersion)
+{
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefBranch) {
+ nsPrintfCString prefName("browser.safebrowsing.provider.%s.pver",
+ nsCString(aProvider).get());
+ nsXPIDLCString version;
+ nsresult rv = prefBranch->GetCharPref(prefName.get(), getter_Copies(version));
+
+ aVersion = NS_SUCCEEDED(rv) ? version : DEFAULT_PROTOCOL_VERSION;
+ } else {
+ aVersion = DEFAULT_PROTOCOL_VERSION;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::MakeUpdateRequestV4(const char** aListNames,
+ const char** aStatesBase64,
+ uint32_t aCount,
+ nsACString &aRequest)
+{
+ using namespace mozilla::safebrowsing;
+
+ FetchThreatListUpdatesRequest r;
+ r.set_allocated_client(CreateClientInfo());
+
+ for (uint32_t i = 0; i < aCount; i++) {
+ nsCString listName(aListNames[i]);
+ uint32_t threatType;
+ nsresult rv = ConvertListNameToThreatType(listName, &threatType);
+ if (NS_FAILED(rv)) {
+ continue; // Unknown list name.
+ }
+ auto lur = r.mutable_list_update_requests()->Add();
+ InitListUpdateRequest(static_cast<ThreatType>(threatType), aStatesBase64[i], lur);
+ }
+
+ // Then serialize.
+ std::string s;
+ r.SerializeToString(&s);
+
+ nsCString out;
+ nsresult rv = Base64URLEncode(s.size(),
+ (const uint8_t*)s.c_str(),
+ Base64URLEncodePaddingPolicy::Include,
+ out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aRequest = out;
+
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////
+// nsIObserver
+
+NS_IMETHODIMP
+nsUrlClassifierUtils::Observe(nsISupports *aSubject, const char *aTopic,
+ const char16_t *aData)
+{
+ if (0 == strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ MutexAutoLock lock(mProviderDictLock);
+ return ReadProvidersFromPrefs(mProviderDict);
+ }
+
+ if (0 == strcmp(aTopic, "xpcom-shutdown-threads")) {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(prefs, NS_ERROR_FAILURE);
+ return prefs->RemoveObserver("browser.safebrowsing", this);
+ }
+
+ return NS_ERROR_UNEXPECTED;
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// non-interface methods
+
+nsresult
+nsUrlClassifierUtils::ReadProvidersFromPrefs(ProviderDictType& aDict)
+{
+ MOZ_ASSERT(NS_IsMainThread(), "ReadProvidersFromPrefs must be on main thread");
+
+ nsCOMPtr<nsIPrefService> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(prefs, NS_ERROR_FAILURE);
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ nsresult rv = prefs->GetBranch("browser.safebrowsing.provider.",
+ getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We've got a pref branch for "browser.safebrowsing.provider.".
+ // Enumerate all children prefs and parse providers.
+ uint32_t childCount;
+ char** childArray;
+ rv = prefBranch->GetChildList("", &childCount, &childArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Collect providers from childArray.
+ nsTHashtable<nsCStringHashKey> providers;
+ for (uint32_t i = 0; i < childCount; i++) {
+ nsCString child(childArray[i]);
+ auto dotPos = child.FindChar('.');
+ if (dotPos < 0) {
+ continue;
+ }
+
+ nsDependentCSubstring provider = Substring(child, 0, dotPos);
+
+ providers.PutEntry(provider);
+ }
+ NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(childCount, childArray);
+
+ // Now we have all providers. Check which one owns |aTableName|.
+ // e.g. The owning lists of provider "google" is defined in
+ // "browser.safebrowsing.provider.google.lists".
+ for (auto itr = providers.Iter(); !itr.Done(); itr.Next()) {
+ auto entry = itr.Get();
+ nsCString provider(entry->GetKey());
+ nsPrintfCString owninListsPref("%s.lists", provider.get());
+
+ nsXPIDLCString owningLists;
+ nsresult rv = prefBranch->GetCharPref(owninListsPref.get(),
+ getter_Copies(owningLists));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ // We've got the owning lists (represented as string) of |provider|.
+ // Build the dictionary for the owning list and the current provider.
+ nsTArray<nsCString> tables;
+ Classifier::SplitTables(owningLists, tables);
+ for (auto tableName : tables) {
+ aDict.Put(tableName, new nsCString(provider));
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsUrlClassifierUtils::CanonicalizeHostname(const nsACString & hostname,
+ nsACString & _retval)
+{
+ nsAutoCString unescaped;
+ if (!NS_UnescapeURL(PromiseFlatCString(hostname).get(),
+ PromiseFlatCString(hostname).Length(),
+ 0, unescaped)) {
+ unescaped.Assign(hostname);
+ }
+
+ nsAutoCString cleaned;
+ CleanupHostname(unescaped, cleaned);
+
+ nsAutoCString temp;
+ ParseIPAddress(cleaned, temp);
+ if (!temp.IsEmpty()) {
+ cleaned.Assign(temp);
+ }
+
+ ToLowerCase(cleaned);
+ SpecialEncode(cleaned, false, _retval);
+
+ return NS_OK;
+}
+
+
+nsresult
+nsUrlClassifierUtils::CanonicalizePath(const nsACString & path,
+ nsACString & _retval)
+{
+ _retval.Truncate();
+
+ nsAutoCString decodedPath(path);
+ nsAutoCString temp;
+ while (NS_UnescapeURL(decodedPath.get(), decodedPath.Length(), 0, temp)) {
+ decodedPath.Assign(temp);
+ temp.Truncate();
+ }
+
+ SpecialEncode(decodedPath, true, _retval);
+ // XXX: lowercase the path?
+
+ return NS_OK;
+}
+
+void
+nsUrlClassifierUtils::CleanupHostname(const nsACString & hostname,
+ nsACString & _retval)
+{
+ _retval.Truncate();
+
+ const char* curChar = hostname.BeginReading();
+ const char* end = hostname.EndReading();
+ char lastChar = '\0';
+ while (curChar != end) {
+ unsigned char c = static_cast<unsigned char>(*curChar);
+ if (c == '.' && (lastChar == '\0' || lastChar == '.')) {
+ // skip
+ } else {
+ _retval.Append(*curChar);
+ }
+ lastChar = c;
+ ++curChar;
+ }
+
+ // cut off trailing dots
+ while (_retval.Length() > 0 && _retval[_retval.Length() - 1] == '.') {
+ _retval.SetLength(_retval.Length() - 1);
+ }
+}
+
+void
+nsUrlClassifierUtils::ParseIPAddress(const nsACString & host,
+ nsACString & _retval)
+{
+ _retval.Truncate();
+ nsACString::const_iterator iter, end;
+ host.BeginReading(iter);
+ host.EndReading(end);
+
+ if (host.Length() <= 15) {
+ // The Windows resolver allows a 4-part dotted decimal IP address to
+ // have a space followed by any old rubbish, so long as the total length
+ // of the string doesn't get above 15 characters. So, "10.192.95.89 xy"
+ // is resolved to 10.192.95.89.
+ // If the string length is greater than 15 characters, e.g.
+ // "10.192.95.89 xy.wildcard.example.com", it will be resolved through
+ // DNS.
+
+ if (FindCharInReadable(' ', iter, end)) {
+ end = iter;
+ }
+ }
+
+ for (host.BeginReading(iter); iter != end; iter++) {
+ if (!(isxdigit(*iter) || *iter == 'x' || *iter == 'X' || *iter == '.')) {
+ // not an IP
+ return;
+ }
+ }
+
+ host.BeginReading(iter);
+ nsTArray<nsCString> parts;
+ ParseString(PromiseFlatCString(Substring(iter, end)), '.', parts);
+ if (parts.Length() > 4) {
+ return;
+ }
+
+ // If any potentially-octal numbers (start with 0 but not hex) have
+ // non-octal digits, no part of the ip can be in octal
+ // XXX: this came from the old javascript implementation, is it really
+ // supposed to be like this?
+ bool allowOctal = true;
+ uint32_t i;
+
+ for (i = 0; i < parts.Length(); i++) {
+ const nsCString& part = parts[i];
+ if (part[0] == '0') {
+ for (uint32_t j = 1; j < part.Length(); j++) {
+ if (part[j] == 'x') {
+ break;
+ }
+ if (part[j] == '8' || part[j] == '9') {
+ allowOctal = false;
+ break;
+ }
+ }
+ }
+ }
+
+ for (i = 0; i < parts.Length(); i++) {
+ nsAutoCString canonical;
+
+ if (i == parts.Length() - 1) {
+ CanonicalNum(parts[i], 5 - parts.Length(), allowOctal, canonical);
+ } else {
+ CanonicalNum(parts[i], 1, allowOctal, canonical);
+ }
+
+ if (canonical.IsEmpty()) {
+ _retval.Truncate();
+ return;
+ }
+
+ if (_retval.IsEmpty()) {
+ _retval.Assign(canonical);
+ } else {
+ _retval.Append('.');
+ _retval.Append(canonical);
+ }
+ }
+ return;
+}
+
+void
+nsUrlClassifierUtils::CanonicalNum(const nsACString& num,
+ uint32_t bytes,
+ bool allowOctal,
+ nsACString& _retval)
+{
+ _retval.Truncate();
+
+ if (num.Length() < 1) {
+ return;
+ }
+
+ uint32_t val;
+ if (allowOctal && IsOctal(num)) {
+ if (PR_sscanf(PromiseFlatCString(num).get(), "%o", &val) != 1) {
+ return;
+ }
+ } else if (IsDecimal(num)) {
+ if (PR_sscanf(PromiseFlatCString(num).get(), "%u", &val) != 1) {
+ return;
+ }
+ } else if (IsHex(num)) {
+ if (PR_sscanf(PromiseFlatCString(num).get(), num[1] == 'X' ? "0X%x" : "0x%x",
+ &val) != 1) {
+ return;
+ }
+ } else {
+ return;
+ }
+
+ while (bytes--) {
+ char buf[20];
+ SprintfLiteral(buf, "%u", val & 0xff);
+ if (_retval.IsEmpty()) {
+ _retval.Assign(buf);
+ } else {
+ _retval = nsDependentCString(buf) + NS_LITERAL_CSTRING(".") + _retval;
+ }
+ val >>= 8;
+ }
+}
+
+// This function will encode all "special" characters in typical url
+// encoding, that is %hh where h is a valid hex digit. It will also fold
+// any duplicated slashes.
+bool
+nsUrlClassifierUtils::SpecialEncode(const nsACString & url,
+ bool foldSlashes,
+ nsACString & _retval)
+{
+ bool changed = false;
+ const char* curChar = url.BeginReading();
+ const char* end = url.EndReading();
+
+ unsigned char lastChar = '\0';
+ while (curChar != end) {
+ unsigned char c = static_cast<unsigned char>(*curChar);
+ if (ShouldURLEscape(c)) {
+ _retval.Append('%');
+ _retval.Append(int_to_hex_digit(c / 16));
+ _retval.Append(int_to_hex_digit(c % 16));
+
+ changed = true;
+ } else if (foldSlashes && (c == '/' && lastChar == '/')) {
+ // skip
+ } else {
+ _retval.Append(*curChar);
+ }
+ lastChar = c;
+ curChar++;
+ }
+ return changed;
+}
+
+bool
+nsUrlClassifierUtils::ShouldURLEscape(const unsigned char c) const
+{
+ return c <= 32 || c == '%' || c >=127;
+}
diff --git a/toolkit/components/url-classifier/nsUrlClassifierUtils.h b/toolkit/components/url-classifier/nsUrlClassifierUtils.h
new file mode 100644
index 0000000000..cd14cf2a72
--- /dev/null
+++ b/toolkit/components/url-classifier/nsUrlClassifierUtils.h
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsUrlClassifierUtils_h_
+#define nsUrlClassifierUtils_h_
+
+#include "nsAutoPtr.h"
+#include "nsIUrlClassifierUtils.h"
+#include "nsClassHashtable.h"
+#include "nsIObserver.h"
+
+class nsUrlClassifierUtils final : public nsIUrlClassifierUtils,
+ public nsIObserver
+{
+public:
+ typedef nsClassHashtable<nsCStringHashKey, nsCString> ProviderDictType;
+
+private:
+ /**
+ * A fast, bit-vector map for ascii characters.
+ *
+ * Internally stores 256 bits in an array of 8 ints.
+ * Does quick bit-flicking to lookup needed characters.
+ */
+ class Charmap
+ {
+ public:
+ Charmap(uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3,
+ uint32_t b4, uint32_t b5, uint32_t b6, uint32_t b7)
+ {
+ mMap[0] = b0; mMap[1] = b1; mMap[2] = b2; mMap[3] = b3;
+ mMap[4] = b4; mMap[5] = b5; mMap[6] = b6; mMap[7] = b7;
+ }
+
+ /**
+ * Do a quick lookup to see if the letter is in the map.
+ */
+ bool Contains(unsigned char c) const
+ {
+ return mMap[c >> 5] & (1 << (c & 31));
+ }
+
+ private:
+ // Store the 256 bits in an 8 byte array.
+ uint32_t mMap[8];
+ };
+
+
+public:
+ nsUrlClassifierUtils();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERUTILS
+ NS_DECL_NSIOBSERVER
+
+ nsresult Init();
+
+ nsresult CanonicalizeHostname(const nsACString & hostname,
+ nsACString & _retval);
+ nsresult CanonicalizePath(const nsACString & url, nsACString & _retval);
+
+ // This function will encode all "special" characters in typical url encoding,
+ // that is %hh where h is a valid hex digit. The characters which are encoded
+ // by this function are any ascii characters under 32(control characters and
+ // space), 37(%), and anything 127 or above (special characters). Url is the
+ // string to encode, ret is the encoded string. Function returns true if
+ // ret != url.
+ bool SpecialEncode(const nsACString & url,
+ bool foldSlashes,
+ nsACString & _retval);
+
+ void ParseIPAddress(const nsACString & host, nsACString & _retval);
+ void CanonicalNum(const nsACString & num,
+ uint32_t bytes,
+ bool allowOctal,
+ nsACString & _retval);
+
+private:
+ ~nsUrlClassifierUtils() {}
+
+ // Disallow copy constructor
+ nsUrlClassifierUtils(const nsUrlClassifierUtils&);
+
+ // Function to tell if we should encode a character.
+ bool ShouldURLEscape(const unsigned char c) const;
+
+ void CleanupHostname(const nsACString & host, nsACString & _retval);
+
+ nsresult ReadProvidersFromPrefs(ProviderDictType& aDict);
+
+ nsAutoPtr<Charmap> mEscapeCharmap;
+
+ // The provider lookup table and its mutex.
+ ProviderDictType mProviderDict;
+ mozilla::Mutex mProviderDictLock;
+};
+
+#endif // nsUrlClassifierUtils_h_
diff --git a/toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc b/toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc
new file mode 100644
index 0000000000..d3e49251bc
--- /dev/null
+++ b/toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc
@@ -0,0 +1,7166 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: safebrowsing.proto
+
+#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION
+#include "safebrowsing.pb.h"
+
+#include <algorithm>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+// @@protoc_insertion_point(includes)
+
+namespace mozilla {
+namespace safebrowsing {
+
+void protobuf_ShutdownFile_safebrowsing_2eproto() {
+ delete ThreatInfo::default_instance_;
+ delete ThreatMatch::default_instance_;
+ delete FindThreatMatchesRequest::default_instance_;
+ delete FindThreatMatchesResponse::default_instance_;
+ delete FetchThreatListUpdatesRequest::default_instance_;
+ delete FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_;
+ delete FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_;
+ delete FetchThreatListUpdatesResponse::default_instance_;
+ delete FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_;
+ delete FindFullHashesRequest::default_instance_;
+ delete FindFullHashesResponse::default_instance_;
+ delete ThreatHit::default_instance_;
+ delete ThreatHit_ThreatSource::default_instance_;
+ delete ClientInfo::default_instance_;
+ delete Checksum::default_instance_;
+ delete ThreatEntry::default_instance_;
+ delete ThreatEntrySet::default_instance_;
+ delete RawIndices::default_instance_;
+ delete RawHashes::default_instance_;
+ delete RiceDeltaEncoding::default_instance_;
+ delete ThreatEntryMetadata::default_instance_;
+ delete ThreatEntryMetadata_MetadataEntry::default_instance_;
+ delete ThreatListDescriptor::default_instance_;
+ delete ListThreatListsResponse::default_instance_;
+ delete Duration::default_instance_;
+}
+
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+void protobuf_AddDesc_safebrowsing_2eproto_impl() {
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+#else
+void protobuf_AddDesc_safebrowsing_2eproto() {
+ static bool already_here = false;
+ if (already_here) return;
+ already_here = true;
+ GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+#endif
+ ThreatInfo::default_instance_ = new ThreatInfo();
+ ThreatMatch::default_instance_ = new ThreatMatch();
+ FindThreatMatchesRequest::default_instance_ = new FindThreatMatchesRequest();
+ FindThreatMatchesResponse::default_instance_ = new FindThreatMatchesResponse();
+ FetchThreatListUpdatesRequest::default_instance_ = new FetchThreatListUpdatesRequest();
+ FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_ = new FetchThreatListUpdatesRequest_ListUpdateRequest();
+ FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_ = new FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints();
+ FetchThreatListUpdatesResponse::default_instance_ = new FetchThreatListUpdatesResponse();
+ FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_ = new FetchThreatListUpdatesResponse_ListUpdateResponse();
+ FindFullHashesRequest::default_instance_ = new FindFullHashesRequest();
+ FindFullHashesResponse::default_instance_ = new FindFullHashesResponse();
+ ThreatHit::default_instance_ = new ThreatHit();
+ ThreatHit_ThreatSource::default_instance_ = new ThreatHit_ThreatSource();
+ ClientInfo::default_instance_ = new ClientInfo();
+ Checksum::default_instance_ = new Checksum();
+ ThreatEntry::default_instance_ = new ThreatEntry();
+ ThreatEntrySet::default_instance_ = new ThreatEntrySet();
+ RawIndices::default_instance_ = new RawIndices();
+ RawHashes::default_instance_ = new RawHashes();
+ RiceDeltaEncoding::default_instance_ = new RiceDeltaEncoding();
+ ThreatEntryMetadata::default_instance_ = new ThreatEntryMetadata();
+ ThreatEntryMetadata_MetadataEntry::default_instance_ = new ThreatEntryMetadata_MetadataEntry();
+ ThreatListDescriptor::default_instance_ = new ThreatListDescriptor();
+ ListThreatListsResponse::default_instance_ = new ListThreatListsResponse();
+ Duration::default_instance_ = new Duration();
+ ThreatInfo::default_instance_->InitAsDefaultInstance();
+ ThreatMatch::default_instance_->InitAsDefaultInstance();
+ FindThreatMatchesRequest::default_instance_->InitAsDefaultInstance();
+ FindThreatMatchesResponse::default_instance_->InitAsDefaultInstance();
+ FetchThreatListUpdatesRequest::default_instance_->InitAsDefaultInstance();
+ FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_->InitAsDefaultInstance();
+ FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_->InitAsDefaultInstance();
+ FetchThreatListUpdatesResponse::default_instance_->InitAsDefaultInstance();
+ FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_->InitAsDefaultInstance();
+ FindFullHashesRequest::default_instance_->InitAsDefaultInstance();
+ FindFullHashesResponse::default_instance_->InitAsDefaultInstance();
+ ThreatHit::default_instance_->InitAsDefaultInstance();
+ ThreatHit_ThreatSource::default_instance_->InitAsDefaultInstance();
+ ClientInfo::default_instance_->InitAsDefaultInstance();
+ Checksum::default_instance_->InitAsDefaultInstance();
+ ThreatEntry::default_instance_->InitAsDefaultInstance();
+ ThreatEntrySet::default_instance_->InitAsDefaultInstance();
+ RawIndices::default_instance_->InitAsDefaultInstance();
+ RawHashes::default_instance_->InitAsDefaultInstance();
+ RiceDeltaEncoding::default_instance_->InitAsDefaultInstance();
+ ThreatEntryMetadata::default_instance_->InitAsDefaultInstance();
+ ThreatEntryMetadata_MetadataEntry::default_instance_->InitAsDefaultInstance();
+ ThreatListDescriptor::default_instance_->InitAsDefaultInstance();
+ ListThreatListsResponse::default_instance_->InitAsDefaultInstance();
+ Duration::default_instance_->InitAsDefaultInstance();
+ ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_safebrowsing_2eproto);
+}
+
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AddDesc_safebrowsing_2eproto_once_);
+void protobuf_AddDesc_safebrowsing_2eproto() {
+ ::google::protobuf::GoogleOnceInit(&protobuf_AddDesc_safebrowsing_2eproto_once_,
+ &protobuf_AddDesc_safebrowsing_2eproto_impl);
+}
+#else
+// Force AddDescriptors() to be called at static initialization time.
+struct StaticDescriptorInitializer_safebrowsing_2eproto {
+ StaticDescriptorInitializer_safebrowsing_2eproto() {
+ protobuf_AddDesc_safebrowsing_2eproto();
+ }
+} static_descriptor_initializer_safebrowsing_2eproto_;
+#endif
+bool ThreatType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool PlatformType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool CompressionType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool ThreatEntryType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatInfo::kThreatTypesFieldNumber;
+const int ThreatInfo::kPlatformTypesFieldNumber;
+const int ThreatInfo::kThreatEntryTypesFieldNumber;
+const int ThreatInfo::kThreatEntriesFieldNumber;
+#endif // !_MSC_VER
+
+ThreatInfo::ThreatInfo()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatInfo)
+}
+
+void ThreatInfo::InitAsDefaultInstance() {
+}
+
+ThreatInfo::ThreatInfo(const ThreatInfo& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatInfo)
+}
+
+void ThreatInfo::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatInfo::~ThreatInfo() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatInfo)
+ SharedDtor();
+}
+
+void ThreatInfo::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ThreatInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatInfo& ThreatInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatInfo* ThreatInfo::default_instance_ = NULL;
+
+ThreatInfo* ThreatInfo::New() const {
+ return new ThreatInfo;
+}
+
+void ThreatInfo::Clear() {
+ threat_types_.Clear();
+ platform_types_.Clear();
+ threat_entry_types_.Clear();
+ threat_entries_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+ case 1: {
+ if (tag == 8) {
+ parse_threat_types:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+ add_threat_types(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else if (tag == 10) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+ input,
+ &::mozilla::safebrowsing::ThreatType_IsValid,
+ this->mutable_threat_types())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(8)) goto parse_threat_types;
+ if (input->ExpectTag(16)) goto parse_platform_types;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_platform_types:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+ add_platform_types(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else if (tag == 18) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+ input,
+ &::mozilla::safebrowsing::PlatformType_IsValid,
+ this->mutable_platform_types())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_platform_types;
+ if (input->ExpectTag(26)) goto parse_threat_entries;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_threat_entries:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_threat_entries()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_threat_entries;
+ if (input->ExpectTag(32)) goto parse_threat_entry_types;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_threat_entry_types:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+ add_threat_entry_types(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else if (tag == 34) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+ input,
+ &::mozilla::safebrowsing::ThreatEntryType_IsValid,
+ this->mutable_threat_entry_types())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_threat_entry_types;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatInfo)
+ return false;
+#undef DO_
+}
+
+void ThreatInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatInfo)
+ // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+ for (int i = 0; i < this->threat_types_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->threat_types(i), output);
+ }
+
+ // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+ for (int i = 0; i < this->platform_types_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->platform_types(i), output);
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+ for (int i = 0; i < this->threat_entries_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->threat_entries(i), output);
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+ for (int i = 0; i < this->threat_entry_types_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 4, this->threat_entry_types(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatInfo)
+}
+
+int ThreatInfo::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->threat_types_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+ this->threat_types(i));
+ }
+ total_size += 1 * this->threat_types_size() + data_size;
+ }
+
+ // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->platform_types_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+ this->platform_types(i));
+ }
+ total_size += 1 * this->platform_types_size() + data_size;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->threat_entry_types_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+ this->threat_entry_types(i));
+ }
+ total_size += 1 * this->threat_entry_types_size() + data_size;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+ total_size += 1 * this->threat_entries_size();
+ for (int i = 0; i < this->threat_entries_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->threat_entries(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatInfo::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatInfo*>(&from));
+}
+
+void ThreatInfo::MergeFrom(const ThreatInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ threat_types_.MergeFrom(from.threat_types_);
+ platform_types_.MergeFrom(from.platform_types_);
+ threat_entry_types_.MergeFrom(from.threat_entry_types_);
+ threat_entries_.MergeFrom(from.threat_entries_);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatInfo::CopyFrom(const ThreatInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatInfo::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatInfo::Swap(ThreatInfo* other) {
+ if (other != this) {
+ threat_types_.Swap(&other->threat_types_);
+ platform_types_.Swap(&other->platform_types_);
+ threat_entry_types_.Swap(&other->threat_entry_types_);
+ threat_entries_.Swap(&other->threat_entries_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatInfo::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatInfo";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatMatch::kThreatTypeFieldNumber;
+const int ThreatMatch::kPlatformTypeFieldNumber;
+const int ThreatMatch::kThreatEntryTypeFieldNumber;
+const int ThreatMatch::kThreatFieldNumber;
+const int ThreatMatch::kThreatEntryMetadataFieldNumber;
+const int ThreatMatch::kCacheDurationFieldNumber;
+#endif // !_MSC_VER
+
+ThreatMatch::ThreatMatch()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatMatch)
+}
+
+void ThreatMatch::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ threat_ = const_cast< ::mozilla::safebrowsing::ThreatEntry*>(
+ ::mozilla::safebrowsing::ThreatEntry::internal_default_instance());
+#else
+ threat_ = const_cast< ::mozilla::safebrowsing::ThreatEntry*>(&::mozilla::safebrowsing::ThreatEntry::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ threat_entry_metadata_ = const_cast< ::mozilla::safebrowsing::ThreatEntryMetadata*>(
+ ::mozilla::safebrowsing::ThreatEntryMetadata::internal_default_instance());
+#else
+ threat_entry_metadata_ = const_cast< ::mozilla::safebrowsing::ThreatEntryMetadata*>(&::mozilla::safebrowsing::ThreatEntryMetadata::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ cache_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(
+ ::mozilla::safebrowsing::Duration::internal_default_instance());
+#else
+ cache_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(&::mozilla::safebrowsing::Duration::default_instance());
+#endif
+}
+
+ThreatMatch::ThreatMatch(const ThreatMatch& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatMatch)
+}
+
+void ThreatMatch::SharedCtor() {
+ _cached_size_ = 0;
+ threat_type_ = 0;
+ platform_type_ = 0;
+ threat_entry_type_ = 0;
+ threat_ = NULL;
+ threat_entry_metadata_ = NULL;
+ cache_duration_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatMatch::~ThreatMatch() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatMatch)
+ SharedDtor();
+}
+
+void ThreatMatch::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete threat_;
+ delete threat_entry_metadata_;
+ delete cache_duration_;
+ }
+}
+
+void ThreatMatch::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatMatch& ThreatMatch::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatMatch* ThreatMatch::default_instance_ = NULL;
+
+ThreatMatch* ThreatMatch::New() const {
+ return new ThreatMatch;
+}
+
+void ThreatMatch::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ThreatMatch*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 63) {
+ ZR_(threat_type_, platform_type_);
+ threat_entry_type_ = 0;
+ if (has_threat()) {
+ if (threat_ != NULL) threat_->::mozilla::safebrowsing::ThreatEntry::Clear();
+ }
+ if (has_threat_entry_metadata()) {
+ if (threat_entry_metadata_ != NULL) threat_entry_metadata_->::mozilla::safebrowsing::ThreatEntryMetadata::Clear();
+ }
+ if (has_cache_duration()) {
+ if (cache_duration_ != NULL) cache_duration_->::mozilla::safebrowsing::Duration::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatMatch::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatMatch)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+ set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_platform_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_platform_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+ set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_threat;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_threat:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_threat()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_threat_entry_metadata;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_threat_entry_metadata:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_threat_entry_metadata()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_cache_duration;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_cache_duration:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_cache_duration()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(48)) goto parse_threat_entry_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+ case 6: {
+ if (tag == 48) {
+ parse_threat_entry_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+ set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatMatch)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatMatch)
+ return false;
+#undef DO_
+}
+
+void ThreatMatch::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatMatch)
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->threat_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->platform_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+ if (has_threat()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->threat(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+ if (has_threat_entry_metadata()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->threat_entry_metadata(), output);
+ }
+
+ // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+ if (has_cache_duration()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->cache_duration(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+ if (has_threat_entry_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 6, this->threat_entry_type(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatMatch)
+}
+
+int ThreatMatch::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+ if (has_threat_entry_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+ if (has_threat()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->threat());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+ if (has_threat_entry_metadata()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->threat_entry_metadata());
+ }
+
+ // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+ if (has_cache_duration()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->cache_duration());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatMatch::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatMatch*>(&from));
+}
+
+void ThreatMatch::MergeFrom(const ThreatMatch& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_threat_type()) {
+ set_threat_type(from.threat_type());
+ }
+ if (from.has_platform_type()) {
+ set_platform_type(from.platform_type());
+ }
+ if (from.has_threat_entry_type()) {
+ set_threat_entry_type(from.threat_entry_type());
+ }
+ if (from.has_threat()) {
+ mutable_threat()->::mozilla::safebrowsing::ThreatEntry::MergeFrom(from.threat());
+ }
+ if (from.has_threat_entry_metadata()) {
+ mutable_threat_entry_metadata()->::mozilla::safebrowsing::ThreatEntryMetadata::MergeFrom(from.threat_entry_metadata());
+ }
+ if (from.has_cache_duration()) {
+ mutable_cache_duration()->::mozilla::safebrowsing::Duration::MergeFrom(from.cache_duration());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatMatch::CopyFrom(const ThreatMatch& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatMatch::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatMatch::Swap(ThreatMatch* other) {
+ if (other != this) {
+ std::swap(threat_type_, other->threat_type_);
+ std::swap(platform_type_, other->platform_type_);
+ std::swap(threat_entry_type_, other->threat_entry_type_);
+ std::swap(threat_, other->threat_);
+ std::swap(threat_entry_metadata_, other->threat_entry_metadata_);
+ std::swap(cache_duration_, other->cache_duration_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatMatch::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatMatch";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FindThreatMatchesRequest::kClientFieldNumber;
+const int FindThreatMatchesRequest::kThreatInfoFieldNumber;
+#endif // !_MSC_VER
+
+FindThreatMatchesRequest::FindThreatMatchesRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FindThreatMatchesRequest)
+}
+
+void FindThreatMatchesRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(
+ ::mozilla::safebrowsing::ClientInfo::internal_default_instance());
+#else
+ client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(&::mozilla::safebrowsing::ClientInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ threat_info_ = const_cast< ::mozilla::safebrowsing::ThreatInfo*>(
+ ::mozilla::safebrowsing::ThreatInfo::internal_default_instance());
+#else
+ threat_info_ = const_cast< ::mozilla::safebrowsing::ThreatInfo*>(&::mozilla::safebrowsing::ThreatInfo::default_instance());
+#endif
+}
+
+FindThreatMatchesRequest::FindThreatMatchesRequest(const FindThreatMatchesRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FindThreatMatchesRequest)
+}
+
+void FindThreatMatchesRequest::SharedCtor() {
+ _cached_size_ = 0;
+ client_ = NULL;
+ threat_info_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FindThreatMatchesRequest::~FindThreatMatchesRequest() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FindThreatMatchesRequest)
+ SharedDtor();
+}
+
+void FindThreatMatchesRequest::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete client_;
+ delete threat_info_;
+ }
+}
+
+void FindThreatMatchesRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FindThreatMatchesRequest& FindThreatMatchesRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FindThreatMatchesRequest* FindThreatMatchesRequest::default_instance_ = NULL;
+
+FindThreatMatchesRequest* FindThreatMatchesRequest::New() const {
+ return new FindThreatMatchesRequest;
+}
+
+void FindThreatMatchesRequest::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_client()) {
+ if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+ }
+ if (has_threat_info()) {
+ if (threat_info_ != NULL) threat_info_->::mozilla::safebrowsing::ThreatInfo::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FindThreatMatchesRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FindThreatMatchesRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_client()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_threat_info;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_threat_info:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_threat_info()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FindThreatMatchesRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FindThreatMatchesRequest)
+ return false;
+#undef DO_
+}
+
+void FindThreatMatchesRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FindThreatMatchesRequest)
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ if (has_client()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->client(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+ if (has_threat_info()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->threat_info(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FindThreatMatchesRequest)
+}
+
+int FindThreatMatchesRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ if (has_client()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->client());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+ if (has_threat_info()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->threat_info());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FindThreatMatchesRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FindThreatMatchesRequest*>(&from));
+}
+
+void FindThreatMatchesRequest::MergeFrom(const FindThreatMatchesRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_client()) {
+ mutable_client()->::mozilla::safebrowsing::ClientInfo::MergeFrom(from.client());
+ }
+ if (from.has_threat_info()) {
+ mutable_threat_info()->::mozilla::safebrowsing::ThreatInfo::MergeFrom(from.threat_info());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FindThreatMatchesRequest::CopyFrom(const FindThreatMatchesRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FindThreatMatchesRequest::IsInitialized() const {
+
+ return true;
+}
+
+void FindThreatMatchesRequest::Swap(FindThreatMatchesRequest* other) {
+ if (other != this) {
+ std::swap(client_, other->client_);
+ std::swap(threat_info_, other->threat_info_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FindThreatMatchesRequest::GetTypeName() const {
+ return "mozilla.safebrowsing.FindThreatMatchesRequest";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FindThreatMatchesResponse::kMatchesFieldNumber;
+#endif // !_MSC_VER
+
+FindThreatMatchesResponse::FindThreatMatchesResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FindThreatMatchesResponse)
+}
+
+void FindThreatMatchesResponse::InitAsDefaultInstance() {
+}
+
+FindThreatMatchesResponse::FindThreatMatchesResponse(const FindThreatMatchesResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FindThreatMatchesResponse)
+}
+
+void FindThreatMatchesResponse::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FindThreatMatchesResponse::~FindThreatMatchesResponse() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FindThreatMatchesResponse)
+ SharedDtor();
+}
+
+void FindThreatMatchesResponse::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void FindThreatMatchesResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FindThreatMatchesResponse& FindThreatMatchesResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FindThreatMatchesResponse* FindThreatMatchesResponse::default_instance_ = NULL;
+
+FindThreatMatchesResponse* FindThreatMatchesResponse::New() const {
+ return new FindThreatMatchesResponse;
+}
+
+void FindThreatMatchesResponse::Clear() {
+ matches_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FindThreatMatchesResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FindThreatMatchesResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_matches:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_matches()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_matches;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FindThreatMatchesResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FindThreatMatchesResponse)
+ return false;
+#undef DO_
+}
+
+void FindThreatMatchesResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FindThreatMatchesResponse)
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ for (int i = 0; i < this->matches_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->matches(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FindThreatMatchesResponse)
+}
+
+int FindThreatMatchesResponse::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ total_size += 1 * this->matches_size();
+ for (int i = 0; i < this->matches_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->matches(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FindThreatMatchesResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FindThreatMatchesResponse*>(&from));
+}
+
+void FindThreatMatchesResponse::MergeFrom(const FindThreatMatchesResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ matches_.MergeFrom(from.matches_);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FindThreatMatchesResponse::CopyFrom(const FindThreatMatchesResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FindThreatMatchesResponse::IsInitialized() const {
+
+ return true;
+}
+
+void FindThreatMatchesResponse::Swap(FindThreatMatchesResponse* other) {
+ if (other != this) {
+ matches_.Swap(&other->matches_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FindThreatMatchesResponse::GetTypeName() const {
+ return "mozilla.safebrowsing.FindThreatMatchesResponse";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kMaxUpdateEntriesFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kMaxDatabaseEntriesFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kRegionFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kSupportedCompressionsFieldNumber;
+#endif // !_MSC_VER
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::InitAsDefaultInstance() {
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ max_update_entries_ = 0;
+ max_database_entries_ = 0;
+ region_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::~FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+ SharedDtor();
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SharedDtor() {
+ if (region_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete region_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_ = NULL;
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::New() const {
+ return new FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 7) {
+ ZR_(max_update_entries_, max_database_entries_);
+ if (has_region()) {
+ if (region_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ region_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ supported_compressions_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int32 max_update_entries = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &max_update_entries_)));
+ set_has_max_update_entries();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_max_database_entries;
+ break;
+ }
+
+ // optional int32 max_database_entries = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_max_database_entries:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &max_database_entries_)));
+ set_has_max_database_entries();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_region;
+ break;
+ }
+
+ // optional string region = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_region:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_region()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_supported_compressions;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_supported_compressions:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::CompressionType_IsValid(value)) {
+ add_supported_compressions(static_cast< ::mozilla::safebrowsing::CompressionType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else if (tag == 34) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+ input,
+ &::mozilla::safebrowsing::CompressionType_IsValid,
+ this->mutable_supported_compressions())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_supported_compressions;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+ return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+ // optional int32 max_update_entries = 1;
+ if (has_max_update_entries()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->max_update_entries(), output);
+ }
+
+ // optional int32 max_database_entries = 2;
+ if (has_max_database_entries()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->max_database_entries(), output);
+ }
+
+ // optional string region = 3;
+ if (has_region()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->region(), output);
+ }
+
+ // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+ for (int i = 0; i < this->supported_compressions_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 4, this->supported_compressions(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+}
+
+int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int32 max_update_entries = 1;
+ if (has_max_update_entries()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->max_update_entries());
+ }
+
+ // optional int32 max_database_entries = 2;
+ if (has_max_database_entries()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->max_database_entries());
+ }
+
+ // optional string region = 3;
+ if (has_region()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->region());
+ }
+
+ }
+ // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->supported_compressions_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+ this->supported_compressions(i));
+ }
+ total_size += 1 * this->supported_compressions_size() + data_size;
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(&from));
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::MergeFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ supported_compressions_.MergeFrom(from.supported_compressions_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_max_update_entries()) {
+ set_max_update_entries(from.max_update_entries());
+ }
+ if (from.has_max_database_entries()) {
+ set_max_database_entries(from.max_database_entries());
+ }
+ if (from.has_region()) {
+ set_region(from.region());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::CopyFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::IsInitialized() const {
+
+ return true;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Swap(FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* other) {
+ if (other != this) {
+ std::swap(max_update_entries_, other->max_update_entries_);
+ std::swap(max_database_entries_, other->max_database_entries_);
+ std::swap(region_, other->region_);
+ supported_compressions_.Swap(&other->supported_compressions_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::GetTypeName() const {
+ return "mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kThreatTypeFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kPlatformTypeFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kThreatEntryTypeFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kStateFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kConstraintsFieldNumber;
+#endif // !_MSC_VER
+
+FetchThreatListUpdatesRequest_ListUpdateRequest::FetchThreatListUpdatesRequest_ListUpdateRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ constraints_ = const_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(
+ ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::internal_default_instance());
+#else
+ constraints_ = const_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(&::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest::FetchThreatListUpdatesRequest_ListUpdateRequest(const FetchThreatListUpdatesRequest_ListUpdateRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ threat_type_ = 0;
+ platform_type_ = 0;
+ threat_entry_type_ = 0;
+ state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ constraints_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest::~FetchThreatListUpdatesRequest_ListUpdateRequest() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+ SharedDtor();
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SharedDtor() {
+ if (state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete state_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete constraints_;
+ }
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesRequest_ListUpdateRequest& FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest* FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_ = NULL;
+
+FetchThreatListUpdatesRequest_ListUpdateRequest* FetchThreatListUpdatesRequest_ListUpdateRequest::New() const {
+ return new FetchThreatListUpdatesRequest_ListUpdateRequest;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<FetchThreatListUpdatesRequest_ListUpdateRequest*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 31) {
+ ZR_(threat_type_, platform_type_);
+ threat_entry_type_ = 0;
+ if (has_state()) {
+ if (state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ state_->clear();
+ }
+ }
+ if (has_constraints()) {
+ if (constraints_ != NULL) constraints_->::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+ set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_platform_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_platform_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+ set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_state;
+ break;
+ }
+
+ // optional bytes state = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_state:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_state()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_constraints;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_constraints:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_constraints()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(40)) goto parse_threat_entry_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+ case 5: {
+ if (tag == 40) {
+ parse_threat_entry_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+ set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+ return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->threat_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->platform_type(), output);
+ }
+
+ // optional bytes state = 3;
+ if (has_state()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 3, this->state(), output);
+ }
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+ if (has_constraints()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->constraints(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+ if (has_threat_entry_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 5, this->threat_entry_type(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+}
+
+int FetchThreatListUpdatesRequest_ListUpdateRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+ if (has_threat_entry_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+ }
+
+ // optional bytes state = 3;
+ if (has_state()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->state());
+ }
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+ if (has_constraints()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->constraints());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesRequest_ListUpdateRequest*>(&from));
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::MergeFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_threat_type()) {
+ set_threat_type(from.threat_type());
+ }
+ if (from.has_platform_type()) {
+ set_platform_type(from.platform_type());
+ }
+ if (from.has_threat_entry_type()) {
+ set_threat_entry_type(from.threat_entry_type());
+ }
+ if (from.has_state()) {
+ set_state(from.state());
+ }
+ if (from.has_constraints()) {
+ mutable_constraints()->::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::MergeFrom(from.constraints());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::CopyFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest::IsInitialized() const {
+
+ return true;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::Swap(FetchThreatListUpdatesRequest_ListUpdateRequest* other) {
+ if (other != this) {
+ std::swap(threat_type_, other->threat_type_);
+ std::swap(platform_type_, other->platform_type_);
+ std::swap(threat_entry_type_, other->threat_entry_type_);
+ std::swap(state_, other->state_);
+ std::swap(constraints_, other->constraints_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FetchThreatListUpdatesRequest_ListUpdateRequest::GetTypeName() const {
+ return "mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesRequest::kClientFieldNumber;
+const int FetchThreatListUpdatesRequest::kListUpdateRequestsFieldNumber;
+#endif // !_MSC_VER
+
+FetchThreatListUpdatesRequest::FetchThreatListUpdatesRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+}
+
+void FetchThreatListUpdatesRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(
+ ::mozilla::safebrowsing::ClientInfo::internal_default_instance());
+#else
+ client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(&::mozilla::safebrowsing::ClientInfo::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesRequest::FetchThreatListUpdatesRequest(const FetchThreatListUpdatesRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+}
+
+void FetchThreatListUpdatesRequest::SharedCtor() {
+ _cached_size_ = 0;
+ client_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesRequest::~FetchThreatListUpdatesRequest() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+ SharedDtor();
+}
+
+void FetchThreatListUpdatesRequest::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete client_;
+ }
+}
+
+void FetchThreatListUpdatesRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesRequest& FetchThreatListUpdatesRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FetchThreatListUpdatesRequest* FetchThreatListUpdatesRequest::default_instance_ = NULL;
+
+FetchThreatListUpdatesRequest* FetchThreatListUpdatesRequest::New() const {
+ return new FetchThreatListUpdatesRequest;
+}
+
+void FetchThreatListUpdatesRequest::Clear() {
+ if (has_client()) {
+ if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+ }
+ list_update_requests_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_client()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_list_update_requests;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_list_update_requests:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_list_update_requests()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_list_update_requests;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+ return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ if (has_client()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->client(), output);
+ }
+
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+ for (int i = 0; i < this->list_update_requests_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->list_update_requests(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+}
+
+int FetchThreatListUpdatesRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ if (has_client()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->client());
+ }
+
+ }
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+ total_size += 1 * this->list_update_requests_size();
+ for (int i = 0; i < this->list_update_requests_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->list_update_requests(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FetchThreatListUpdatesRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesRequest*>(&from));
+}
+
+void FetchThreatListUpdatesRequest::MergeFrom(const FetchThreatListUpdatesRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ list_update_requests_.MergeFrom(from.list_update_requests_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_client()) {
+ mutable_client()->::mozilla::safebrowsing::ClientInfo::MergeFrom(from.client());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesRequest::CopyFrom(const FetchThreatListUpdatesRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesRequest::IsInitialized() const {
+
+ return true;
+}
+
+void FetchThreatListUpdatesRequest::Swap(FetchThreatListUpdatesRequest* other) {
+ if (other != this) {
+ std::swap(client_, other->client_);
+ list_update_requests_.Swap(&other->list_update_requests_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FetchThreatListUpdatesRequest::GetTypeName() const {
+ return "mozilla.safebrowsing.FetchThreatListUpdatesRequest";
+}
+
+
+// ===================================================================
+
+bool FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::RESPONSE_TYPE_UNSPECIFIED;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::PARTIAL_UPDATE;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::FULL_UPDATE;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::ResponseType_MIN;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::ResponseType_MAX;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::ResponseType_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kThreatTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kThreatEntryTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kPlatformTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kResponseTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kAdditionsFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kRemovalsFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kNewClientStateFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kChecksumFieldNumber;
+#endif // !_MSC_VER
+
+FetchThreatListUpdatesResponse_ListUpdateResponse::FetchThreatListUpdatesResponse_ListUpdateResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ checksum_ = const_cast< ::mozilla::safebrowsing::Checksum*>(
+ ::mozilla::safebrowsing::Checksum::internal_default_instance());
+#else
+ checksum_ = const_cast< ::mozilla::safebrowsing::Checksum*>(&::mozilla::safebrowsing::Checksum::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesResponse_ListUpdateResponse::FetchThreatListUpdatesResponse_ListUpdateResponse(const FetchThreatListUpdatesResponse_ListUpdateResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ threat_type_ = 0;
+ threat_entry_type_ = 0;
+ platform_type_ = 0;
+ response_type_ = 0;
+ new_client_state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ checksum_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesResponse_ListUpdateResponse::~FetchThreatListUpdatesResponse_ListUpdateResponse() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+ SharedDtor();
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SharedDtor() {
+ if (new_client_state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete new_client_state_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete checksum_;
+ }
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesResponse_ListUpdateResponse& FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FetchThreatListUpdatesResponse_ListUpdateResponse* FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_ = NULL;
+
+FetchThreatListUpdatesResponse_ListUpdateResponse* FetchThreatListUpdatesResponse_ListUpdateResponse::New() const {
+ return new FetchThreatListUpdatesResponse_ListUpdateResponse;
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<FetchThreatListUpdatesResponse_ListUpdateResponse*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 207) {
+ ZR_(threat_type_, response_type_);
+ if (has_new_client_state()) {
+ if (new_client_state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ new_client_state_->clear();
+ }
+ }
+ if (has_checksum()) {
+ if (checksum_ != NULL) checksum_->::mozilla::safebrowsing::Checksum::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ additions_.Clear();
+ removals_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesResponse_ListUpdateResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+ set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_threat_entry_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_threat_entry_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+ set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_platform_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_platform_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+ set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(32)) goto parse_response_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+ case 4: {
+ if (tag == 32) {
+ parse_response_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(value)) {
+ set_response_type(static_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_additions;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_additions:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_additions()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_additions;
+ if (input->ExpectTag(50)) goto parse_removals;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+ case 6: {
+ if (tag == 50) {
+ parse_removals:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_removals()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(50)) goto parse_removals;
+ if (input->ExpectTag(58)) goto parse_new_client_state;
+ break;
+ }
+
+ // optional bytes new_client_state = 7;
+ case 7: {
+ if (tag == 58) {
+ parse_new_client_state:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_new_client_state()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(66)) goto parse_checksum;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.Checksum checksum = 8;
+ case 8: {
+ if (tag == 66) {
+ parse_checksum:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_checksum()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+ return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->threat_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+ if (has_threat_entry_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->threat_entry_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+ if (has_platform_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 3, this->platform_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+ if (has_response_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 4, this->response_type(), output);
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+ for (int i = 0; i < this->additions_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->additions(i), output);
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+ for (int i = 0; i < this->removals_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 6, this->removals(i), output);
+ }
+
+ // optional bytes new_client_state = 7;
+ if (has_new_client_state()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 7, this->new_client_state(), output);
+ }
+
+ // optional .mozilla.safebrowsing.Checksum checksum = 8;
+ if (has_checksum()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 8, this->checksum(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+}
+
+int FetchThreatListUpdatesResponse_ListUpdateResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+ if (has_threat_entry_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+ if (has_platform_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+ }
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+ if (has_response_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->response_type());
+ }
+
+ // optional bytes new_client_state = 7;
+ if (has_new_client_state()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->new_client_state());
+ }
+
+ // optional .mozilla.safebrowsing.Checksum checksum = 8;
+ if (has_checksum()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->checksum());
+ }
+
+ }
+ // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+ total_size += 1 * this->additions_size();
+ for (int i = 0; i < this->additions_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->additions(i));
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+ total_size += 1 * this->removals_size();
+ for (int i = 0; i < this->removals_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->removals(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesResponse_ListUpdateResponse*>(&from));
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::MergeFrom(const FetchThreatListUpdatesResponse_ListUpdateResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ additions_.MergeFrom(from.additions_);
+ removals_.MergeFrom(from.removals_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_threat_type()) {
+ set_threat_type(from.threat_type());
+ }
+ if (from.has_threat_entry_type()) {
+ set_threat_entry_type(from.threat_entry_type());
+ }
+ if (from.has_platform_type()) {
+ set_platform_type(from.platform_type());
+ }
+ if (from.has_response_type()) {
+ set_response_type(from.response_type());
+ }
+ if (from.has_new_client_state()) {
+ set_new_client_state(from.new_client_state());
+ }
+ if (from.has_checksum()) {
+ mutable_checksum()->::mozilla::safebrowsing::Checksum::MergeFrom(from.checksum());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::CopyFrom(const FetchThreatListUpdatesResponse_ListUpdateResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesResponse_ListUpdateResponse::IsInitialized() const {
+
+ return true;
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::Swap(FetchThreatListUpdatesResponse_ListUpdateResponse* other) {
+ if (other != this) {
+ std::swap(threat_type_, other->threat_type_);
+ std::swap(threat_entry_type_, other->threat_entry_type_);
+ std::swap(platform_type_, other->platform_type_);
+ std::swap(response_type_, other->response_type_);
+ additions_.Swap(&other->additions_);
+ removals_.Swap(&other->removals_);
+ std::swap(new_client_state_, other->new_client_state_);
+ std::swap(checksum_, other->checksum_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FetchThreatListUpdatesResponse_ListUpdateResponse::GetTypeName() const {
+ return "mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesResponse::kListUpdateResponsesFieldNumber;
+const int FetchThreatListUpdatesResponse::kMinimumWaitDurationFieldNumber;
+#endif // !_MSC_VER
+
+FetchThreatListUpdatesResponse::FetchThreatListUpdatesResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+}
+
+void FetchThreatListUpdatesResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ minimum_wait_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(
+ ::mozilla::safebrowsing::Duration::internal_default_instance());
+#else
+ minimum_wait_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(&::mozilla::safebrowsing::Duration::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesResponse::FetchThreatListUpdatesResponse(const FetchThreatListUpdatesResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+}
+
+void FetchThreatListUpdatesResponse::SharedCtor() {
+ _cached_size_ = 0;
+ minimum_wait_duration_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesResponse::~FetchThreatListUpdatesResponse() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+ SharedDtor();
+}
+
+void FetchThreatListUpdatesResponse::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete minimum_wait_duration_;
+ }
+}
+
+void FetchThreatListUpdatesResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesResponse& FetchThreatListUpdatesResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FetchThreatListUpdatesResponse* FetchThreatListUpdatesResponse::default_instance_ = NULL;
+
+FetchThreatListUpdatesResponse* FetchThreatListUpdatesResponse::New() const {
+ return new FetchThreatListUpdatesResponse;
+}
+
+void FetchThreatListUpdatesResponse::Clear() {
+ if (has_minimum_wait_duration()) {
+ if (minimum_wait_duration_ != NULL) minimum_wait_duration_->::mozilla::safebrowsing::Duration::Clear();
+ }
+ list_update_responses_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse list_update_responses = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_list_update_responses:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_list_update_responses()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_list_update_responses;
+ if (input->ExpectTag(18)) goto parse_minimum_wait_duration;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_minimum_wait_duration:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_minimum_wait_duration()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+ return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse list_update_responses = 1;
+ for (int i = 0; i < this->list_update_responses_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->list_update_responses(i), output);
+ }
+
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ if (has_minimum_wait_duration()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->minimum_wait_duration(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+}
+
+int FetchThreatListUpdatesResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ if (has_minimum_wait_duration()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->minimum_wait_duration());
+ }
+
+ }
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse list_update_responses = 1;
+ total_size += 1 * this->list_update_responses_size();
+ for (int i = 0; i < this->list_update_responses_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->list_update_responses(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FetchThreatListUpdatesResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesResponse*>(&from));
+}
+
+void FetchThreatListUpdatesResponse::MergeFrom(const FetchThreatListUpdatesResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ list_update_responses_.MergeFrom(from.list_update_responses_);
+ if (from._has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ if (from.has_minimum_wait_duration()) {
+ mutable_minimum_wait_duration()->::mozilla::safebrowsing::Duration::MergeFrom(from.minimum_wait_duration());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesResponse::CopyFrom(const FetchThreatListUpdatesResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesResponse::IsInitialized() const {
+
+ return true;
+}
+
+void FetchThreatListUpdatesResponse::Swap(FetchThreatListUpdatesResponse* other) {
+ if (other != this) {
+ list_update_responses_.Swap(&other->list_update_responses_);
+ std::swap(minimum_wait_duration_, other->minimum_wait_duration_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FetchThreatListUpdatesResponse::GetTypeName() const {
+ return "mozilla.safebrowsing.FetchThreatListUpdatesResponse";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FindFullHashesRequest::kClientFieldNumber;
+const int FindFullHashesRequest::kClientStatesFieldNumber;
+const int FindFullHashesRequest::kThreatInfoFieldNumber;
+#endif // !_MSC_VER
+
+FindFullHashesRequest::FindFullHashesRequest()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FindFullHashesRequest)
+}
+
+void FindFullHashesRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(
+ ::mozilla::safebrowsing::ClientInfo::internal_default_instance());
+#else
+ client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(&::mozilla::safebrowsing::ClientInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ threat_info_ = const_cast< ::mozilla::safebrowsing::ThreatInfo*>(
+ ::mozilla::safebrowsing::ThreatInfo::internal_default_instance());
+#else
+ threat_info_ = const_cast< ::mozilla::safebrowsing::ThreatInfo*>(&::mozilla::safebrowsing::ThreatInfo::default_instance());
+#endif
+}
+
+FindFullHashesRequest::FindFullHashesRequest(const FindFullHashesRequest& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FindFullHashesRequest)
+}
+
+void FindFullHashesRequest::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ client_ = NULL;
+ threat_info_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FindFullHashesRequest::~FindFullHashesRequest() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FindFullHashesRequest)
+ SharedDtor();
+}
+
+void FindFullHashesRequest::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete client_;
+ delete threat_info_;
+ }
+}
+
+void FindFullHashesRequest::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FindFullHashesRequest& FindFullHashesRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FindFullHashesRequest* FindFullHashesRequest::default_instance_ = NULL;
+
+FindFullHashesRequest* FindFullHashesRequest::New() const {
+ return new FindFullHashesRequest;
+}
+
+void FindFullHashesRequest::Clear() {
+ if (_has_bits_[0 / 32] & 5) {
+ if (has_client()) {
+ if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+ }
+ if (has_threat_info()) {
+ if (threat_info_ != NULL) threat_info_->::mozilla::safebrowsing::ThreatInfo::Clear();
+ }
+ }
+ client_states_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FindFullHashesRequest::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FindFullHashesRequest)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_client()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_client_states;
+ break;
+ }
+
+ // repeated bytes client_states = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_client_states:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->add_client_states()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_client_states;
+ if (input->ExpectTag(26)) goto parse_threat_info;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_threat_info:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_threat_info()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FindFullHashesRequest)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FindFullHashesRequest)
+ return false;
+#undef DO_
+}
+
+void FindFullHashesRequest::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FindFullHashesRequest)
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ if (has_client()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->client(), output);
+ }
+
+ // repeated bytes client_states = 2;
+ for (int i = 0; i < this->client_states_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytes(
+ 2, this->client_states(i), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 3;
+ if (has_threat_info()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->threat_info(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FindFullHashesRequest)
+}
+
+int FindFullHashesRequest::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ if (has_client()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->client());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 3;
+ if (has_threat_info()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->threat_info());
+ }
+
+ }
+ // repeated bytes client_states = 2;
+ total_size += 1 * this->client_states_size();
+ for (int i = 0; i < this->client_states_size(); i++) {
+ total_size += ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->client_states(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FindFullHashesRequest::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FindFullHashesRequest*>(&from));
+}
+
+void FindFullHashesRequest::MergeFrom(const FindFullHashesRequest& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ client_states_.MergeFrom(from.client_states_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_client()) {
+ mutable_client()->::mozilla::safebrowsing::ClientInfo::MergeFrom(from.client());
+ }
+ if (from.has_threat_info()) {
+ mutable_threat_info()->::mozilla::safebrowsing::ThreatInfo::MergeFrom(from.threat_info());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FindFullHashesRequest::CopyFrom(const FindFullHashesRequest& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FindFullHashesRequest::IsInitialized() const {
+
+ return true;
+}
+
+void FindFullHashesRequest::Swap(FindFullHashesRequest* other) {
+ if (other != this) {
+ std::swap(client_, other->client_);
+ client_states_.Swap(&other->client_states_);
+ std::swap(threat_info_, other->threat_info_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FindFullHashesRequest::GetTypeName() const {
+ return "mozilla.safebrowsing.FindFullHashesRequest";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FindFullHashesResponse::kMatchesFieldNumber;
+const int FindFullHashesResponse::kMinimumWaitDurationFieldNumber;
+const int FindFullHashesResponse::kNegativeCacheDurationFieldNumber;
+#endif // !_MSC_VER
+
+FindFullHashesResponse::FindFullHashesResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FindFullHashesResponse)
+}
+
+void FindFullHashesResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ minimum_wait_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(
+ ::mozilla::safebrowsing::Duration::internal_default_instance());
+#else
+ minimum_wait_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(&::mozilla::safebrowsing::Duration::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ negative_cache_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(
+ ::mozilla::safebrowsing::Duration::internal_default_instance());
+#else
+ negative_cache_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(&::mozilla::safebrowsing::Duration::default_instance());
+#endif
+}
+
+FindFullHashesResponse::FindFullHashesResponse(const FindFullHashesResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FindFullHashesResponse)
+}
+
+void FindFullHashesResponse::SharedCtor() {
+ _cached_size_ = 0;
+ minimum_wait_duration_ = NULL;
+ negative_cache_duration_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FindFullHashesResponse::~FindFullHashesResponse() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FindFullHashesResponse)
+ SharedDtor();
+}
+
+void FindFullHashesResponse::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete minimum_wait_duration_;
+ delete negative_cache_duration_;
+ }
+}
+
+void FindFullHashesResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FindFullHashesResponse& FindFullHashesResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+FindFullHashesResponse* FindFullHashesResponse::default_instance_ = NULL;
+
+FindFullHashesResponse* FindFullHashesResponse::New() const {
+ return new FindFullHashesResponse;
+}
+
+void FindFullHashesResponse::Clear() {
+ if (_has_bits_[0 / 32] & 6) {
+ if (has_minimum_wait_duration()) {
+ if (minimum_wait_duration_ != NULL) minimum_wait_duration_->::mozilla::safebrowsing::Duration::Clear();
+ }
+ if (has_negative_cache_duration()) {
+ if (negative_cache_duration_ != NULL) negative_cache_duration_->::mozilla::safebrowsing::Duration::Clear();
+ }
+ }
+ matches_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool FindFullHashesResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FindFullHashesResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_matches:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_matches()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_matches;
+ if (input->ExpectTag(18)) goto parse_minimum_wait_duration;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_minimum_wait_duration:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_minimum_wait_duration()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_negative_cache_duration;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.Duration negative_cache_duration = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_negative_cache_duration:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_negative_cache_duration()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FindFullHashesResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FindFullHashesResponse)
+ return false;
+#undef DO_
+}
+
+void FindFullHashesResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FindFullHashesResponse)
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ for (int i = 0; i < this->matches_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->matches(i), output);
+ }
+
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ if (has_minimum_wait_duration()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->minimum_wait_duration(), output);
+ }
+
+ // optional .mozilla.safebrowsing.Duration negative_cache_duration = 3;
+ if (has_negative_cache_duration()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->negative_cache_duration(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FindFullHashesResponse)
+}
+
+int FindFullHashesResponse::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ if (has_minimum_wait_duration()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->minimum_wait_duration());
+ }
+
+ // optional .mozilla.safebrowsing.Duration negative_cache_duration = 3;
+ if (has_negative_cache_duration()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->negative_cache_duration());
+ }
+
+ }
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ total_size += 1 * this->matches_size();
+ for (int i = 0; i < this->matches_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->matches(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void FindFullHashesResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const FindFullHashesResponse*>(&from));
+}
+
+void FindFullHashesResponse::MergeFrom(const FindFullHashesResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ matches_.MergeFrom(from.matches_);
+ if (from._has_bits_[1 / 32] & (0xffu << (1 % 32))) {
+ if (from.has_minimum_wait_duration()) {
+ mutable_minimum_wait_duration()->::mozilla::safebrowsing::Duration::MergeFrom(from.minimum_wait_duration());
+ }
+ if (from.has_negative_cache_duration()) {
+ mutable_negative_cache_duration()->::mozilla::safebrowsing::Duration::MergeFrom(from.negative_cache_duration());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FindFullHashesResponse::CopyFrom(const FindFullHashesResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool FindFullHashesResponse::IsInitialized() const {
+
+ return true;
+}
+
+void FindFullHashesResponse::Swap(FindFullHashesResponse* other) {
+ if (other != this) {
+ matches_.Swap(&other->matches_);
+ std::swap(minimum_wait_duration_, other->minimum_wait_duration_);
+ std::swap(negative_cache_duration_, other->negative_cache_duration_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string FindFullHashesResponse::GetTypeName() const {
+ return "mozilla.safebrowsing.FindFullHashesResponse";
+}
+
+
+// ===================================================================
+
+bool ThreatHit_ThreatSourceType_IsValid(int value) {
+ switch(value) {
+ case 0:
+ case 1:
+ case 2:
+ case 3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#ifndef _MSC_VER
+const ThreatHit_ThreatSourceType ThreatHit::THREAT_SOURCE_TYPE_UNSPECIFIED;
+const ThreatHit_ThreatSourceType ThreatHit::MATCHING_URL;
+const ThreatHit_ThreatSourceType ThreatHit::TAB_URL;
+const ThreatHit_ThreatSourceType ThreatHit::TAB_REDIRECT;
+const ThreatHit_ThreatSourceType ThreatHit::ThreatSourceType_MIN;
+const ThreatHit_ThreatSourceType ThreatHit::ThreatSourceType_MAX;
+const int ThreatHit::ThreatSourceType_ARRAYSIZE;
+#endif // _MSC_VER
+#ifndef _MSC_VER
+const int ThreatHit_ThreatSource::kUrlFieldNumber;
+const int ThreatHit_ThreatSource::kTypeFieldNumber;
+const int ThreatHit_ThreatSource::kRemoteIpFieldNumber;
+const int ThreatHit_ThreatSource::kReferrerFieldNumber;
+#endif // !_MSC_VER
+
+ThreatHit_ThreatSource::ThreatHit_ThreatSource()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatHit.ThreatSource)
+}
+
+void ThreatHit_ThreatSource::InitAsDefaultInstance() {
+}
+
+ThreatHit_ThreatSource::ThreatHit_ThreatSource(const ThreatHit_ThreatSource& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatHit.ThreatSource)
+}
+
+void ThreatHit_ThreatSource::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ type_ = 0;
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatHit_ThreatSource::~ThreatHit_ThreatSource() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatHit.ThreatSource)
+ SharedDtor();
+}
+
+void ThreatHit_ThreatSource::SharedDtor() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete remote_ip_;
+ }
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ThreatHit_ThreatSource::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatHit_ThreatSource& ThreatHit_ThreatSource::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatHit_ThreatSource* ThreatHit_ThreatSource::default_instance_ = NULL;
+
+ThreatHit_ThreatSource* ThreatHit_ThreatSource::New() const {
+ return new ThreatHit_ThreatSource;
+}
+
+void ThreatHit_ThreatSource::Clear() {
+ if (_has_bits_[0 / 32] & 15) {
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ type_ = 0;
+ if (has_remote_ip()) {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_->clear();
+ }
+ }
+ if (has_referrer()) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatHit_ThreatSource::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatHit.ThreatSource)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string url = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatHit.ThreatSourceType type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatHit_ThreatSourceType_IsValid(value)) {
+ set_type(static_cast< ::mozilla::safebrowsing::ThreatHit_ThreatSourceType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_remote_ip;
+ break;
+ }
+
+ // optional string remote_ip = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_remote_ip:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_remote_ip()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_referrer;
+ break;
+ }
+
+ // optional string referrer = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_referrer:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_referrer()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatHit.ThreatSource)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatHit.ThreatSource)
+ return false;
+#undef DO_
+}
+
+void ThreatHit_ThreatSource::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatHit.ThreatSource)
+ // optional string url = 1;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->url(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatHit.ThreatSourceType type = 2;
+ if (has_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->type(), output);
+ }
+
+ // optional string remote_ip = 3;
+ if (has_remote_ip()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 3, this->remote_ip(), output);
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 4, this->referrer(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatHit.ThreatSource)
+}
+
+int ThreatHit_ThreatSource::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string url = 1;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatHit.ThreatSourceType type = 2;
+ if (has_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+ }
+
+ // optional string remote_ip = 3;
+ if (has_remote_ip()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->remote_ip());
+ }
+
+ // optional string referrer = 4;
+ if (has_referrer()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->referrer());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatHit_ThreatSource::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatHit_ThreatSource*>(&from));
+}
+
+void ThreatHit_ThreatSource::MergeFrom(const ThreatHit_ThreatSource& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ if (from.has_type()) {
+ set_type(from.type());
+ }
+ if (from.has_remote_ip()) {
+ set_remote_ip(from.remote_ip());
+ }
+ if (from.has_referrer()) {
+ set_referrer(from.referrer());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatHit_ThreatSource::CopyFrom(const ThreatHit_ThreatSource& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatHit_ThreatSource::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatHit_ThreatSource::Swap(ThreatHit_ThreatSource* other) {
+ if (other != this) {
+ std::swap(url_, other->url_);
+ std::swap(type_, other->type_);
+ std::swap(remote_ip_, other->remote_ip_);
+ std::swap(referrer_, other->referrer_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatHit_ThreatSource::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatHit.ThreatSource";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ThreatHit::kThreatTypeFieldNumber;
+const int ThreatHit::kPlatformTypeFieldNumber;
+const int ThreatHit::kEntryFieldNumber;
+const int ThreatHit::kResourcesFieldNumber;
+#endif // !_MSC_VER
+
+ThreatHit::ThreatHit()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatHit)
+}
+
+void ThreatHit::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ entry_ = const_cast< ::mozilla::safebrowsing::ThreatEntry*>(
+ ::mozilla::safebrowsing::ThreatEntry::internal_default_instance());
+#else
+ entry_ = const_cast< ::mozilla::safebrowsing::ThreatEntry*>(&::mozilla::safebrowsing::ThreatEntry::default_instance());
+#endif
+}
+
+ThreatHit::ThreatHit(const ThreatHit& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatHit)
+}
+
+void ThreatHit::SharedCtor() {
+ _cached_size_ = 0;
+ threat_type_ = 0;
+ platform_type_ = 0;
+ entry_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatHit::~ThreatHit() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatHit)
+ SharedDtor();
+}
+
+void ThreatHit::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete entry_;
+ }
+}
+
+void ThreatHit::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatHit& ThreatHit::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatHit* ThreatHit::default_instance_ = NULL;
+
+ThreatHit* ThreatHit::New() const {
+ return new ThreatHit;
+}
+
+void ThreatHit::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ThreatHit*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 7) {
+ ZR_(threat_type_, platform_type_);
+ if (has_entry()) {
+ if (entry_ != NULL) entry_->::mozilla::safebrowsing::ThreatEntry::Clear();
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ resources_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatHit::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatHit)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+ set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_platform_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_platform_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+ set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_entry;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntry entry = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_entry:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_entry()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_resources;
+ break;
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatHit.ThreatSource resources = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_resources:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_resources()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_resources;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatHit)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatHit)
+ return false;
+#undef DO_
+}
+
+void ThreatHit::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatHit)
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->threat_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->platform_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntry entry = 3;
+ if (has_entry()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->entry(), output);
+ }
+
+ // repeated .mozilla.safebrowsing.ThreatHit.ThreatSource resources = 4;
+ for (int i = 0; i < this->resources_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->resources(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatHit)
+}
+
+int ThreatHit::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntry entry = 3;
+ if (has_entry()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->entry());
+ }
+
+ }
+ // repeated .mozilla.safebrowsing.ThreatHit.ThreatSource resources = 4;
+ total_size += 1 * this->resources_size();
+ for (int i = 0; i < this->resources_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->resources(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatHit::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatHit*>(&from));
+}
+
+void ThreatHit::MergeFrom(const ThreatHit& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ resources_.MergeFrom(from.resources_);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_threat_type()) {
+ set_threat_type(from.threat_type());
+ }
+ if (from.has_platform_type()) {
+ set_platform_type(from.platform_type());
+ }
+ if (from.has_entry()) {
+ mutable_entry()->::mozilla::safebrowsing::ThreatEntry::MergeFrom(from.entry());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatHit::CopyFrom(const ThreatHit& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatHit::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatHit::Swap(ThreatHit* other) {
+ if (other != this) {
+ std::swap(threat_type_, other->threat_type_);
+ std::swap(platform_type_, other->platform_type_);
+ std::swap(entry_, other->entry_);
+ resources_.Swap(&other->resources_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatHit::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatHit";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientInfo::kClientIdFieldNumber;
+const int ClientInfo::kClientVersionFieldNumber;
+#endif // !_MSC_VER
+
+ClientInfo::ClientInfo()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ClientInfo)
+}
+
+void ClientInfo::InitAsDefaultInstance() {
+}
+
+ClientInfo::ClientInfo(const ClientInfo& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ClientInfo)
+}
+
+void ClientInfo::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ client_id_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ client_version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientInfo::~ClientInfo() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ClientInfo)
+ SharedDtor();
+}
+
+void ClientInfo::SharedDtor() {
+ if (client_id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete client_id_;
+ }
+ if (client_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete client_version_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ClientInfo::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientInfo& ClientInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ClientInfo* ClientInfo::default_instance_ = NULL;
+
+ClientInfo* ClientInfo::New() const {
+ return new ClientInfo;
+}
+
+void ClientInfo::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_client_id()) {
+ if (client_id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_id_->clear();
+ }
+ }
+ if (has_client_version()) {
+ if (client_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_version_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ClientInfo::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ClientInfo)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional string client_id = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_client_id()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_client_version;
+ break;
+ }
+
+ // optional string client_version = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_client_version:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_client_version()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ClientInfo)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ClientInfo)
+ return false;
+#undef DO_
+}
+
+void ClientInfo::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ClientInfo)
+ // optional string client_id = 1;
+ if (has_client_id()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 1, this->client_id(), output);
+ }
+
+ // optional string client_version = 2;
+ if (has_client_version()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->client_version(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ClientInfo)
+}
+
+int ClientInfo::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional string client_id = 1;
+ if (has_client_id()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->client_id());
+ }
+
+ // optional string client_version = 2;
+ if (has_client_version()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->client_version());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ClientInfo::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ClientInfo*>(&from));
+}
+
+void ClientInfo::MergeFrom(const ClientInfo& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_client_id()) {
+ set_client_id(from.client_id());
+ }
+ if (from.has_client_version()) {
+ set_client_version(from.client_version());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientInfo::CopyFrom(const ClientInfo& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ClientInfo::IsInitialized() const {
+
+ return true;
+}
+
+void ClientInfo::Swap(ClientInfo* other) {
+ if (other != this) {
+ std::swap(client_id_, other->client_id_);
+ std::swap(client_version_, other->client_version_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ClientInfo::GetTypeName() const {
+ return "mozilla.safebrowsing.ClientInfo";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int Checksum::kSha256FieldNumber;
+#endif // !_MSC_VER
+
+Checksum::Checksum()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.Checksum)
+}
+
+void Checksum::InitAsDefaultInstance() {
+}
+
+Checksum::Checksum(const Checksum& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.Checksum)
+}
+
+void Checksum::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+Checksum::~Checksum() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.Checksum)
+ SharedDtor();
+}
+
+void Checksum::SharedDtor() {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete sha256_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void Checksum::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const Checksum& Checksum::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+Checksum* Checksum::default_instance_ = NULL;
+
+Checksum* Checksum::New() const {
+ return new Checksum;
+}
+
+void Checksum::Clear() {
+ if (has_sha256()) {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_->clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool Checksum::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.Checksum)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes sha256 = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_sha256()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.Checksum)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.Checksum)
+ return false;
+#undef DO_
+}
+
+void Checksum::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.Checksum)
+ // optional bytes sha256 = 1;
+ if (has_sha256()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->sha256(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.Checksum)
+}
+
+int Checksum::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes sha256 = 1;
+ if (has_sha256()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->sha256());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void Checksum::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const Checksum*>(&from));
+}
+
+void Checksum::MergeFrom(const Checksum& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_sha256()) {
+ set_sha256(from.sha256());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void Checksum::CopyFrom(const Checksum& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Checksum::IsInitialized() const {
+
+ return true;
+}
+
+void Checksum::Swap(Checksum* other) {
+ if (other != this) {
+ std::swap(sha256_, other->sha256_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string Checksum::GetTypeName() const {
+ return "mozilla.safebrowsing.Checksum";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatEntry::kHashFieldNumber;
+const int ThreatEntry::kUrlFieldNumber;
+#endif // !_MSC_VER
+
+ThreatEntry::ThreatEntry()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatEntry)
+}
+
+void ThreatEntry::InitAsDefaultInstance() {
+}
+
+ThreatEntry::ThreatEntry(const ThreatEntry& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatEntry)
+}
+
+void ThreatEntry::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ hash_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatEntry::~ThreatEntry() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatEntry)
+ SharedDtor();
+}
+
+void ThreatEntry::SharedDtor() {
+ if (hash_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete hash_;
+ }
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ThreatEntry::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatEntry& ThreatEntry::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatEntry* ThreatEntry::default_instance_ = NULL;
+
+ThreatEntry* ThreatEntry::New() const {
+ return new ThreatEntry;
+}
+
+void ThreatEntry::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_hash()) {
+ if (hash_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ hash_->clear();
+ }
+ }
+ if (has_url()) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatEntry::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatEntry)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes hash = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_hash()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_url;
+ break;
+ }
+
+ // optional string url = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_url:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+ input, this->mutable_url()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatEntry)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatEntry)
+ return false;
+#undef DO_
+}
+
+void ThreatEntry::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatEntry)
+ // optional bytes hash = 1;
+ if (has_hash()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->hash(), output);
+ }
+
+ // optional string url = 2;
+ if (has_url()) {
+ ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+ 2, this->url(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatEntry)
+}
+
+int ThreatEntry::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes hash = 1;
+ if (has_hash()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->hash());
+ }
+
+ // optional string url = 2;
+ if (has_url()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::StringSize(
+ this->url());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatEntry::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatEntry*>(&from));
+}
+
+void ThreatEntry::MergeFrom(const ThreatEntry& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_hash()) {
+ set_hash(from.hash());
+ }
+ if (from.has_url()) {
+ set_url(from.url());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatEntry::CopyFrom(const ThreatEntry& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatEntry::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatEntry::Swap(ThreatEntry* other) {
+ if (other != this) {
+ std::swap(hash_, other->hash_);
+ std::swap(url_, other->url_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatEntry::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatEntry";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatEntrySet::kCompressionTypeFieldNumber;
+const int ThreatEntrySet::kRawHashesFieldNumber;
+const int ThreatEntrySet::kRawIndicesFieldNumber;
+const int ThreatEntrySet::kRiceHashesFieldNumber;
+const int ThreatEntrySet::kRiceIndicesFieldNumber;
+#endif // !_MSC_VER
+
+ThreatEntrySet::ThreatEntrySet()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatEntrySet)
+}
+
+void ThreatEntrySet::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ raw_hashes_ = const_cast< ::mozilla::safebrowsing::RawHashes*>(
+ ::mozilla::safebrowsing::RawHashes::internal_default_instance());
+#else
+ raw_hashes_ = const_cast< ::mozilla::safebrowsing::RawHashes*>(&::mozilla::safebrowsing::RawHashes::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ raw_indices_ = const_cast< ::mozilla::safebrowsing::RawIndices*>(
+ ::mozilla::safebrowsing::RawIndices::internal_default_instance());
+#else
+ raw_indices_ = const_cast< ::mozilla::safebrowsing::RawIndices*>(&::mozilla::safebrowsing::RawIndices::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ rice_hashes_ = const_cast< ::mozilla::safebrowsing::RiceDeltaEncoding*>(
+ ::mozilla::safebrowsing::RiceDeltaEncoding::internal_default_instance());
+#else
+ rice_hashes_ = const_cast< ::mozilla::safebrowsing::RiceDeltaEncoding*>(&::mozilla::safebrowsing::RiceDeltaEncoding::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ rice_indices_ = const_cast< ::mozilla::safebrowsing::RiceDeltaEncoding*>(
+ ::mozilla::safebrowsing::RiceDeltaEncoding::internal_default_instance());
+#else
+ rice_indices_ = const_cast< ::mozilla::safebrowsing::RiceDeltaEncoding*>(&::mozilla::safebrowsing::RiceDeltaEncoding::default_instance());
+#endif
+}
+
+ThreatEntrySet::ThreatEntrySet(const ThreatEntrySet& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatEntrySet)
+}
+
+void ThreatEntrySet::SharedCtor() {
+ _cached_size_ = 0;
+ compression_type_ = 0;
+ raw_hashes_ = NULL;
+ raw_indices_ = NULL;
+ rice_hashes_ = NULL;
+ rice_indices_ = NULL;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatEntrySet::~ThreatEntrySet() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatEntrySet)
+ SharedDtor();
+}
+
+void ThreatEntrySet::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ delete raw_hashes_;
+ delete raw_indices_;
+ delete rice_hashes_;
+ delete rice_indices_;
+ }
+}
+
+void ThreatEntrySet::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatEntrySet& ThreatEntrySet::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatEntrySet* ThreatEntrySet::default_instance_ = NULL;
+
+ThreatEntrySet* ThreatEntrySet::New() const {
+ return new ThreatEntrySet;
+}
+
+void ThreatEntrySet::Clear() {
+ if (_has_bits_[0 / 32] & 31) {
+ compression_type_ = 0;
+ if (has_raw_hashes()) {
+ if (raw_hashes_ != NULL) raw_hashes_->::mozilla::safebrowsing::RawHashes::Clear();
+ }
+ if (has_raw_indices()) {
+ if (raw_indices_ != NULL) raw_indices_->::mozilla::safebrowsing::RawIndices::Clear();
+ }
+ if (has_rice_hashes()) {
+ if (rice_hashes_ != NULL) rice_hashes_->::mozilla::safebrowsing::RiceDeltaEncoding::Clear();
+ }
+ if (has_rice_indices()) {
+ if (rice_indices_ != NULL) rice_indices_->::mozilla::safebrowsing::RiceDeltaEncoding::Clear();
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatEntrySet::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatEntrySet)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.CompressionType compression_type = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::CompressionType_IsValid(value)) {
+ set_compression_type(static_cast< ::mozilla::safebrowsing::CompressionType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_raw_hashes;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.RawHashes raw_hashes = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_raw_hashes:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_raw_hashes()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(26)) goto parse_raw_indices;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.RawIndices raw_indices = 3;
+ case 3: {
+ if (tag == 26) {
+ parse_raw_indices:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_raw_indices()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_rice_hashes;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_hashes = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_rice_hashes:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_rice_hashes()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(42)) goto parse_rice_indices;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_indices = 5;
+ case 5: {
+ if (tag == 42) {
+ parse_rice_indices:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, mutable_rice_indices()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatEntrySet)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatEntrySet)
+ return false;
+#undef DO_
+}
+
+void ThreatEntrySet::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatEntrySet)
+ // optional .mozilla.safebrowsing.CompressionType compression_type = 1;
+ if (has_compression_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->compression_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.RawHashes raw_hashes = 2;
+ if (has_raw_hashes()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 2, this->raw_hashes(), output);
+ }
+
+ // optional .mozilla.safebrowsing.RawIndices raw_indices = 3;
+ if (has_raw_indices()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 3, this->raw_indices(), output);
+ }
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_hashes = 4;
+ if (has_rice_hashes()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 4, this->rice_hashes(), output);
+ }
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_indices = 5;
+ if (has_rice_indices()) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 5, this->rice_indices(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatEntrySet)
+}
+
+int ThreatEntrySet::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.CompressionType compression_type = 1;
+ if (has_compression_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->compression_type());
+ }
+
+ // optional .mozilla.safebrowsing.RawHashes raw_hashes = 2;
+ if (has_raw_hashes()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->raw_hashes());
+ }
+
+ // optional .mozilla.safebrowsing.RawIndices raw_indices = 3;
+ if (has_raw_indices()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->raw_indices());
+ }
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_hashes = 4;
+ if (has_rice_hashes()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->rice_hashes());
+ }
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_indices = 5;
+ if (has_rice_indices()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->rice_indices());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatEntrySet::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatEntrySet*>(&from));
+}
+
+void ThreatEntrySet::MergeFrom(const ThreatEntrySet& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_compression_type()) {
+ set_compression_type(from.compression_type());
+ }
+ if (from.has_raw_hashes()) {
+ mutable_raw_hashes()->::mozilla::safebrowsing::RawHashes::MergeFrom(from.raw_hashes());
+ }
+ if (from.has_raw_indices()) {
+ mutable_raw_indices()->::mozilla::safebrowsing::RawIndices::MergeFrom(from.raw_indices());
+ }
+ if (from.has_rice_hashes()) {
+ mutable_rice_hashes()->::mozilla::safebrowsing::RiceDeltaEncoding::MergeFrom(from.rice_hashes());
+ }
+ if (from.has_rice_indices()) {
+ mutable_rice_indices()->::mozilla::safebrowsing::RiceDeltaEncoding::MergeFrom(from.rice_indices());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatEntrySet::CopyFrom(const ThreatEntrySet& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatEntrySet::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatEntrySet::Swap(ThreatEntrySet* other) {
+ if (other != this) {
+ std::swap(compression_type_, other->compression_type_);
+ std::swap(raw_hashes_, other->raw_hashes_);
+ std::swap(raw_indices_, other->raw_indices_);
+ std::swap(rice_hashes_, other->rice_hashes_);
+ std::swap(rice_indices_, other->rice_indices_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatEntrySet::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatEntrySet";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int RawIndices::kIndicesFieldNumber;
+#endif // !_MSC_VER
+
+RawIndices::RawIndices()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.RawIndices)
+}
+
+void RawIndices::InitAsDefaultInstance() {
+}
+
+RawIndices::RawIndices(const RawIndices& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.RawIndices)
+}
+
+void RawIndices::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+RawIndices::~RawIndices() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.RawIndices)
+ SharedDtor();
+}
+
+void RawIndices::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void RawIndices::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const RawIndices& RawIndices::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+RawIndices* RawIndices::default_instance_ = NULL;
+
+RawIndices* RawIndices::New() const {
+ return new RawIndices;
+}
+
+void RawIndices::Clear() {
+ indices_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool RawIndices::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.RawIndices)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated int32 indices = 1;
+ case 1: {
+ if (tag == 8) {
+ parse_indices:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadRepeatedPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ 1, 8, input, this->mutable_indices())));
+ } else if (tag == 10) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPackedPrimitiveNoInline<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, this->mutable_indices())));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(8)) goto parse_indices;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.RawIndices)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.RawIndices)
+ return false;
+#undef DO_
+}
+
+void RawIndices::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.RawIndices)
+ // repeated int32 indices = 1;
+ for (int i = 0; i < this->indices_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(
+ 1, this->indices(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.RawIndices)
+}
+
+int RawIndices::ByteSize() const {
+ int total_size = 0;
+
+ // repeated int32 indices = 1;
+ {
+ int data_size = 0;
+ for (int i = 0; i < this->indices_size(); i++) {
+ data_size += ::google::protobuf::internal::WireFormatLite::
+ Int32Size(this->indices(i));
+ }
+ total_size += 1 * this->indices_size() + data_size;
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void RawIndices::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const RawIndices*>(&from));
+}
+
+void RawIndices::MergeFrom(const RawIndices& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ indices_.MergeFrom(from.indices_);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void RawIndices::CopyFrom(const RawIndices& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool RawIndices::IsInitialized() const {
+
+ return true;
+}
+
+void RawIndices::Swap(RawIndices* other) {
+ if (other != this) {
+ indices_.Swap(&other->indices_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string RawIndices::GetTypeName() const {
+ return "mozilla.safebrowsing.RawIndices";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int RawHashes::kPrefixSizeFieldNumber;
+const int RawHashes::kRawHashesFieldNumber;
+#endif // !_MSC_VER
+
+RawHashes::RawHashes()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.RawHashes)
+}
+
+void RawHashes::InitAsDefaultInstance() {
+}
+
+RawHashes::RawHashes(const RawHashes& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.RawHashes)
+}
+
+void RawHashes::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ prefix_size_ = 0;
+ raw_hashes_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+RawHashes::~RawHashes() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.RawHashes)
+ SharedDtor();
+}
+
+void RawHashes::SharedDtor() {
+ if (raw_hashes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete raw_hashes_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void RawHashes::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const RawHashes& RawHashes::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+RawHashes* RawHashes::default_instance_ = NULL;
+
+RawHashes* RawHashes::New() const {
+ return new RawHashes;
+}
+
+void RawHashes::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ prefix_size_ = 0;
+ if (has_raw_hashes()) {
+ if (raw_hashes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_hashes_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool RawHashes::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.RawHashes)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int32 prefix_size = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &prefix_size_)));
+ set_has_prefix_size();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_raw_hashes;
+ break;
+ }
+
+ // optional bytes raw_hashes = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_raw_hashes:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_raw_hashes()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.RawHashes)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.RawHashes)
+ return false;
+#undef DO_
+}
+
+void RawHashes::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.RawHashes)
+ // optional int32 prefix_size = 1;
+ if (has_prefix_size()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->prefix_size(), output);
+ }
+
+ // optional bytes raw_hashes = 2;
+ if (has_raw_hashes()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->raw_hashes(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.RawHashes)
+}
+
+int RawHashes::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int32 prefix_size = 1;
+ if (has_prefix_size()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->prefix_size());
+ }
+
+ // optional bytes raw_hashes = 2;
+ if (has_raw_hashes()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->raw_hashes());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void RawHashes::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const RawHashes*>(&from));
+}
+
+void RawHashes::MergeFrom(const RawHashes& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_prefix_size()) {
+ set_prefix_size(from.prefix_size());
+ }
+ if (from.has_raw_hashes()) {
+ set_raw_hashes(from.raw_hashes());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void RawHashes::CopyFrom(const RawHashes& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool RawHashes::IsInitialized() const {
+
+ return true;
+}
+
+void RawHashes::Swap(RawHashes* other) {
+ if (other != this) {
+ std::swap(prefix_size_, other->prefix_size_);
+ std::swap(raw_hashes_, other->raw_hashes_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string RawHashes::GetTypeName() const {
+ return "mozilla.safebrowsing.RawHashes";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int RiceDeltaEncoding::kFirstValueFieldNumber;
+const int RiceDeltaEncoding::kRiceParameterFieldNumber;
+const int RiceDeltaEncoding::kNumEntriesFieldNumber;
+const int RiceDeltaEncoding::kEncodedDataFieldNumber;
+#endif // !_MSC_VER
+
+RiceDeltaEncoding::RiceDeltaEncoding()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.RiceDeltaEncoding)
+}
+
+void RiceDeltaEncoding::InitAsDefaultInstance() {
+}
+
+RiceDeltaEncoding::RiceDeltaEncoding(const RiceDeltaEncoding& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.RiceDeltaEncoding)
+}
+
+void RiceDeltaEncoding::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ first_value_ = GOOGLE_LONGLONG(0);
+ rice_parameter_ = 0;
+ num_entries_ = 0;
+ encoded_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+RiceDeltaEncoding::~RiceDeltaEncoding() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.RiceDeltaEncoding)
+ SharedDtor();
+}
+
+void RiceDeltaEncoding::SharedDtor() {
+ if (encoded_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete encoded_data_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void RiceDeltaEncoding::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const RiceDeltaEncoding& RiceDeltaEncoding::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+RiceDeltaEncoding* RiceDeltaEncoding::default_instance_ = NULL;
+
+RiceDeltaEncoding* RiceDeltaEncoding::New() const {
+ return new RiceDeltaEncoding;
+}
+
+void RiceDeltaEncoding::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<RiceDeltaEncoding*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ if (_has_bits_[0 / 32] & 15) {
+ ZR_(first_value_, num_entries_);
+ if (has_encoded_data()) {
+ if (encoded_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ encoded_data_->clear();
+ }
+ }
+ }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool RiceDeltaEncoding::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.RiceDeltaEncoding)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int64 first_value = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &first_value_)));
+ set_has_first_value();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_rice_parameter;
+ break;
+ }
+
+ // optional int32 rice_parameter = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_rice_parameter:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &rice_parameter_)));
+ set_has_rice_parameter();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_num_entries;
+ break;
+ }
+
+ // optional int32 num_entries = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_num_entries:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &num_entries_)));
+ set_has_num_entries();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(34)) goto parse_encoded_data;
+ break;
+ }
+
+ // optional bytes encoded_data = 4;
+ case 4: {
+ if (tag == 34) {
+ parse_encoded_data:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_encoded_data()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.RiceDeltaEncoding)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.RiceDeltaEncoding)
+ return false;
+#undef DO_
+}
+
+void RiceDeltaEncoding::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.RiceDeltaEncoding)
+ // optional int64 first_value = 1;
+ if (has_first_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(1, this->first_value(), output);
+ }
+
+ // optional int32 rice_parameter = 2;
+ if (has_rice_parameter()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->rice_parameter(), output);
+ }
+
+ // optional int32 num_entries = 3;
+ if (has_num_entries()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(3, this->num_entries(), output);
+ }
+
+ // optional bytes encoded_data = 4;
+ if (has_encoded_data()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 4, this->encoded_data(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.RiceDeltaEncoding)
+}
+
+int RiceDeltaEncoding::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int64 first_value = 1;
+ if (has_first_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->first_value());
+ }
+
+ // optional int32 rice_parameter = 2;
+ if (has_rice_parameter()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->rice_parameter());
+ }
+
+ // optional int32 num_entries = 3;
+ if (has_num_entries()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->num_entries());
+ }
+
+ // optional bytes encoded_data = 4;
+ if (has_encoded_data()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->encoded_data());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void RiceDeltaEncoding::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const RiceDeltaEncoding*>(&from));
+}
+
+void RiceDeltaEncoding::MergeFrom(const RiceDeltaEncoding& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_first_value()) {
+ set_first_value(from.first_value());
+ }
+ if (from.has_rice_parameter()) {
+ set_rice_parameter(from.rice_parameter());
+ }
+ if (from.has_num_entries()) {
+ set_num_entries(from.num_entries());
+ }
+ if (from.has_encoded_data()) {
+ set_encoded_data(from.encoded_data());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void RiceDeltaEncoding::CopyFrom(const RiceDeltaEncoding& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool RiceDeltaEncoding::IsInitialized() const {
+
+ return true;
+}
+
+void RiceDeltaEncoding::Swap(RiceDeltaEncoding* other) {
+ if (other != this) {
+ std::swap(first_value_, other->first_value_);
+ std::swap(rice_parameter_, other->rice_parameter_);
+ std::swap(num_entries_, other->num_entries_);
+ std::swap(encoded_data_, other->encoded_data_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string RiceDeltaEncoding::GetTypeName() const {
+ return "mozilla.safebrowsing.RiceDeltaEncoding";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatEntryMetadata_MetadataEntry::kKeyFieldNumber;
+const int ThreatEntryMetadata_MetadataEntry::kValueFieldNumber;
+#endif // !_MSC_VER
+
+ThreatEntryMetadata_MetadataEntry::ThreatEntryMetadata_MetadataEntry()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+}
+
+void ThreatEntryMetadata_MetadataEntry::InitAsDefaultInstance() {
+}
+
+ThreatEntryMetadata_MetadataEntry::ThreatEntryMetadata_MetadataEntry(const ThreatEntryMetadata_MetadataEntry& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+}
+
+void ThreatEntryMetadata_MetadataEntry::SharedCtor() {
+ ::google::protobuf::internal::GetEmptyString();
+ _cached_size_ = 0;
+ key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatEntryMetadata_MetadataEntry::~ThreatEntryMetadata_MetadataEntry() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+ SharedDtor();
+}
+
+void ThreatEntryMetadata_MetadataEntry::SharedDtor() {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete key_;
+ }
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete value_;
+ }
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ThreatEntryMetadata_MetadataEntry::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatEntryMetadata_MetadataEntry& ThreatEntryMetadata_MetadataEntry::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatEntryMetadata_MetadataEntry* ThreatEntryMetadata_MetadataEntry::default_instance_ = NULL;
+
+ThreatEntryMetadata_MetadataEntry* ThreatEntryMetadata_MetadataEntry::New() const {
+ return new ThreatEntryMetadata_MetadataEntry;
+}
+
+void ThreatEntryMetadata_MetadataEntry::Clear() {
+ if (_has_bits_[0 / 32] & 3) {
+ if (has_key()) {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_->clear();
+ }
+ }
+ if (has_value()) {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_->clear();
+ }
+ }
+ }
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatEntryMetadata_MetadataEntry::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional bytes key = 1;
+ case 1: {
+ if (tag == 10) {
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_key()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(18)) goto parse_value;
+ break;
+ }
+
+ // optional bytes value = 2;
+ case 2: {
+ if (tag == 18) {
+ parse_value:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+ input, this->mutable_value()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+ return false;
+#undef DO_
+}
+
+void ThreatEntryMetadata_MetadataEntry::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+ // optional bytes key = 1;
+ if (has_key()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 1, this->key(), output);
+ }
+
+ // optional bytes value = 2;
+ if (has_value()) {
+ ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+ 2, this->value(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+}
+
+int ThreatEntryMetadata_MetadataEntry::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional bytes key = 1;
+ if (has_key()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->key());
+ }
+
+ // optional bytes value = 2;
+ if (has_value()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::BytesSize(
+ this->value());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatEntryMetadata_MetadataEntry::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatEntryMetadata_MetadataEntry*>(&from));
+}
+
+void ThreatEntryMetadata_MetadataEntry::MergeFrom(const ThreatEntryMetadata_MetadataEntry& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_key()) {
+ set_key(from.key());
+ }
+ if (from.has_value()) {
+ set_value(from.value());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatEntryMetadata_MetadataEntry::CopyFrom(const ThreatEntryMetadata_MetadataEntry& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatEntryMetadata_MetadataEntry::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatEntryMetadata_MetadataEntry::Swap(ThreatEntryMetadata_MetadataEntry* other) {
+ if (other != this) {
+ std::swap(key_, other->key_);
+ std::swap(value_, other->value_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatEntryMetadata_MetadataEntry::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int ThreatEntryMetadata::kEntriesFieldNumber;
+#endif // !_MSC_VER
+
+ThreatEntryMetadata::ThreatEntryMetadata()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatEntryMetadata)
+}
+
+void ThreatEntryMetadata::InitAsDefaultInstance() {
+}
+
+ThreatEntryMetadata::ThreatEntryMetadata(const ThreatEntryMetadata& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatEntryMetadata)
+}
+
+void ThreatEntryMetadata::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatEntryMetadata::~ThreatEntryMetadata() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatEntryMetadata)
+ SharedDtor();
+}
+
+void ThreatEntryMetadata::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ThreatEntryMetadata::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatEntryMetadata& ThreatEntryMetadata::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatEntryMetadata* ThreatEntryMetadata::default_instance_ = NULL;
+
+ThreatEntryMetadata* ThreatEntryMetadata::New() const {
+ return new ThreatEntryMetadata;
+}
+
+void ThreatEntryMetadata::Clear() {
+ entries_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatEntryMetadata::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatEntryMetadata)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry entries = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_entries:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_entries()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_entries;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatEntryMetadata)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatEntryMetadata)
+ return false;
+#undef DO_
+}
+
+void ThreatEntryMetadata::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatEntryMetadata)
+ // repeated .mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry entries = 1;
+ for (int i = 0; i < this->entries_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->entries(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatEntryMetadata)
+}
+
+int ThreatEntryMetadata::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry entries = 1;
+ total_size += 1 * this->entries_size();
+ for (int i = 0; i < this->entries_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->entries(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatEntryMetadata::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatEntryMetadata*>(&from));
+}
+
+void ThreatEntryMetadata::MergeFrom(const ThreatEntryMetadata& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ entries_.MergeFrom(from.entries_);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatEntryMetadata::CopyFrom(const ThreatEntryMetadata& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatEntryMetadata::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatEntryMetadata::Swap(ThreatEntryMetadata* other) {
+ if (other != this) {
+ entries_.Swap(&other->entries_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatEntryMetadata::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatEntryMetadata";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatListDescriptor::kThreatTypeFieldNumber;
+const int ThreatListDescriptor::kPlatformTypeFieldNumber;
+const int ThreatListDescriptor::kThreatEntryTypeFieldNumber;
+#endif // !_MSC_VER
+
+ThreatListDescriptor::ThreatListDescriptor()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatListDescriptor)
+}
+
+void ThreatListDescriptor::InitAsDefaultInstance() {
+}
+
+ThreatListDescriptor::ThreatListDescriptor(const ThreatListDescriptor& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatListDescriptor)
+}
+
+void ThreatListDescriptor::SharedCtor() {
+ _cached_size_ = 0;
+ threat_type_ = 0;
+ platform_type_ = 0;
+ threat_entry_type_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatListDescriptor::~ThreatListDescriptor() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatListDescriptor)
+ SharedDtor();
+}
+
+void ThreatListDescriptor::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ThreatListDescriptor::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatListDescriptor& ThreatListDescriptor::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ThreatListDescriptor* ThreatListDescriptor::default_instance_ = NULL;
+
+ThreatListDescriptor* ThreatListDescriptor::New() const {
+ return new ThreatListDescriptor;
+}
+
+void ThreatListDescriptor::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<ThreatListDescriptor*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ ZR_(threat_type_, threat_entry_type_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ThreatListDescriptor::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatListDescriptor)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ case 1: {
+ if (tag == 8) {
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+ set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_platform_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_platform_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+ set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(24)) goto parse_threat_entry_type;
+ break;
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 3;
+ case 3: {
+ if (tag == 24) {
+ parse_threat_entry_type:
+ int value;
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+ input, &value)));
+ if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+ set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+ } else {
+ unknown_fields_stream.WriteVarint32(tag);
+ unknown_fields_stream.WriteVarint32(value);
+ }
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatListDescriptor)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatListDescriptor)
+ return false;
+#undef DO_
+}
+
+void ThreatListDescriptor::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatListDescriptor)
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 1, this->threat_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 2, this->platform_type(), output);
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 3;
+ if (has_threat_entry_type()) {
+ ::google::protobuf::internal::WireFormatLite::WriteEnum(
+ 3, this->threat_entry_type(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatListDescriptor)
+}
+
+int ThreatListDescriptor::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ if (has_threat_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+ }
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ if (has_platform_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+ }
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 3;
+ if (has_threat_entry_type()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ThreatListDescriptor::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ThreatListDescriptor*>(&from));
+}
+
+void ThreatListDescriptor::MergeFrom(const ThreatListDescriptor& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_threat_type()) {
+ set_threat_type(from.threat_type());
+ }
+ if (from.has_platform_type()) {
+ set_platform_type(from.platform_type());
+ }
+ if (from.has_threat_entry_type()) {
+ set_threat_entry_type(from.threat_entry_type());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatListDescriptor::CopyFrom(const ThreatListDescriptor& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ThreatListDescriptor::IsInitialized() const {
+
+ return true;
+}
+
+void ThreatListDescriptor::Swap(ThreatListDescriptor* other) {
+ if (other != this) {
+ std::swap(threat_type_, other->threat_type_);
+ std::swap(platform_type_, other->platform_type_);
+ std::swap(threat_entry_type_, other->threat_entry_type_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ThreatListDescriptor::GetTypeName() const {
+ return "mozilla.safebrowsing.ThreatListDescriptor";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ListThreatListsResponse::kThreatListsFieldNumber;
+#endif // !_MSC_VER
+
+ListThreatListsResponse::ListThreatListsResponse()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ListThreatListsResponse)
+}
+
+void ListThreatListsResponse::InitAsDefaultInstance() {
+}
+
+ListThreatListsResponse::ListThreatListsResponse(const ListThreatListsResponse& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ListThreatListsResponse)
+}
+
+void ListThreatListsResponse::SharedCtor() {
+ _cached_size_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ListThreatListsResponse::~ListThreatListsResponse() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ListThreatListsResponse)
+ SharedDtor();
+}
+
+void ListThreatListsResponse::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void ListThreatListsResponse::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ListThreatListsResponse& ListThreatListsResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+ListThreatListsResponse* ListThreatListsResponse::default_instance_ = NULL;
+
+ListThreatListsResponse* ListThreatListsResponse::New() const {
+ return new ListThreatListsResponse;
+}
+
+void ListThreatListsResponse::Clear() {
+ threat_lists_.Clear();
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool ListThreatListsResponse::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ListThreatListsResponse)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // repeated .mozilla.safebrowsing.ThreatListDescriptor threat_lists = 1;
+ case 1: {
+ if (tag == 10) {
+ parse_threat_lists:
+ DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+ input, add_threat_lists()));
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(10)) goto parse_threat_lists;
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ListThreatListsResponse)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ListThreatListsResponse)
+ return false;
+#undef DO_
+}
+
+void ListThreatListsResponse::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ListThreatListsResponse)
+ // repeated .mozilla.safebrowsing.ThreatListDescriptor threat_lists = 1;
+ for (int i = 0; i < this->threat_lists_size(); i++) {
+ ::google::protobuf::internal::WireFormatLite::WriteMessage(
+ 1, this->threat_lists(i), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ListThreatListsResponse)
+}
+
+int ListThreatListsResponse::ByteSize() const {
+ int total_size = 0;
+
+ // repeated .mozilla.safebrowsing.ThreatListDescriptor threat_lists = 1;
+ total_size += 1 * this->threat_lists_size();
+ for (int i = 0; i < this->threat_lists_size(); i++) {
+ total_size +=
+ ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+ this->threat_lists(i));
+ }
+
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void ListThreatListsResponse::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const ListThreatListsResponse*>(&from));
+}
+
+void ListThreatListsResponse::MergeFrom(const ListThreatListsResponse& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ threat_lists_.MergeFrom(from.threat_lists_);
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ListThreatListsResponse::CopyFrom(const ListThreatListsResponse& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool ListThreatListsResponse::IsInitialized() const {
+
+ return true;
+}
+
+void ListThreatListsResponse::Swap(ListThreatListsResponse* other) {
+ if (other != this) {
+ threat_lists_.Swap(&other->threat_lists_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string ListThreatListsResponse::GetTypeName() const {
+ return "mozilla.safebrowsing.ListThreatListsResponse";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int Duration::kSecondsFieldNumber;
+const int Duration::kNanosFieldNumber;
+#endif // !_MSC_VER
+
+Duration::Duration()
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ // @@protoc_insertion_point(constructor:mozilla.safebrowsing.Duration)
+}
+
+void Duration::InitAsDefaultInstance() {
+}
+
+Duration::Duration(const Duration& from)
+ : ::google::protobuf::MessageLite() {
+ SharedCtor();
+ MergeFrom(from);
+ // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.Duration)
+}
+
+void Duration::SharedCtor() {
+ _cached_size_ = 0;
+ seconds_ = GOOGLE_LONGLONG(0);
+ nanos_ = 0;
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+Duration::~Duration() {
+ // @@protoc_insertion_point(destructor:mozilla.safebrowsing.Duration)
+ SharedDtor();
+}
+
+void Duration::SharedDtor() {
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ if (this != &default_instance()) {
+ #else
+ if (this != default_instance_) {
+ #endif
+ }
+}
+
+void Duration::SetCachedSize(int size) const {
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const Duration& Duration::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ protobuf_AddDesc_safebrowsing_2eproto();
+#else
+ if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+ return *default_instance_;
+}
+
+Duration* Duration::default_instance_ = NULL;
+
+Duration* Duration::New() const {
+ return new Duration;
+}
+
+void Duration::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>( \
+ &reinterpret_cast<Duration*>(16)->f) - \
+ reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do { \
+ size_t f = OFFSET_OF_FIELD_(first); \
+ size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last); \
+ ::memset(&first, 0, n); \
+ } while (0)
+
+ ZR_(seconds_, nanos_);
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+ ::memset(_has_bits_, 0, sizeof(_has_bits_));
+ mutable_unknown_fields()->clear();
+}
+
+bool Duration::MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+ ::google::protobuf::uint32 tag;
+ ::google::protobuf::io::StringOutputStream unknown_fields_string(
+ mutable_unknown_fields());
+ ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+ &unknown_fields_string);
+ // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.Duration)
+ for (;;) {
+ ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+ tag = p.first;
+ if (!p.second) goto handle_unusual;
+ switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+ // optional int64 seconds = 1;
+ case 1: {
+ if (tag == 8) {
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int64, ::google::protobuf::internal::WireFormatLite::TYPE_INT64>(
+ input, &seconds_)));
+ set_has_seconds();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectTag(16)) goto parse_nanos;
+ break;
+ }
+
+ // optional int32 nanos = 2;
+ case 2: {
+ if (tag == 16) {
+ parse_nanos:
+ DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+ ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+ input, &nanos_)));
+ set_has_nanos();
+ } else {
+ goto handle_unusual;
+ }
+ if (input->ExpectAtEnd()) goto success;
+ break;
+ }
+
+ default: {
+ handle_unusual:
+ if (tag == 0 ||
+ ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+ ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+ goto success;
+ }
+ DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+ input, tag, &unknown_fields_stream));
+ break;
+ }
+ }
+ }
+success:
+ // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.Duration)
+ return true;
+failure:
+ // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.Duration)
+ return false;
+#undef DO_
+}
+
+void Duration::SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const {
+ // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.Duration)
+ // optional int64 seconds = 1;
+ if (has_seconds()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt64(1, this->seconds(), output);
+ }
+
+ // optional int32 nanos = 2;
+ if (has_nanos()) {
+ ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->nanos(), output);
+ }
+
+ output->WriteRaw(unknown_fields().data(),
+ unknown_fields().size());
+ // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.Duration)
+}
+
+int Duration::ByteSize() const {
+ int total_size = 0;
+
+ if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ // optional int64 seconds = 1;
+ if (has_seconds()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int64Size(
+ this->seconds());
+ }
+
+ // optional int32 nanos = 2;
+ if (has_nanos()) {
+ total_size += 1 +
+ ::google::protobuf::internal::WireFormatLite::Int32Size(
+ this->nanos());
+ }
+
+ }
+ total_size += unknown_fields().size();
+
+ GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+ _cached_size_ = total_size;
+ GOOGLE_SAFE_CONCURRENT_WRITES_END();
+ return total_size;
+}
+
+void Duration::CheckTypeAndMergeFrom(
+ const ::google::protobuf::MessageLite& from) {
+ MergeFrom(*::google::protobuf::down_cast<const Duration*>(&from));
+}
+
+void Duration::MergeFrom(const Duration& from) {
+ GOOGLE_CHECK_NE(&from, this);
+ if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+ if (from.has_seconds()) {
+ set_seconds(from.seconds());
+ }
+ if (from.has_nanos()) {
+ set_nanos(from.nanos());
+ }
+ }
+ mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void Duration::CopyFrom(const Duration& from) {
+ if (&from == this) return;
+ Clear();
+ MergeFrom(from);
+}
+
+bool Duration::IsInitialized() const {
+
+ return true;
+}
+
+void Duration::Swap(Duration* other) {
+ if (other != this) {
+ std::swap(seconds_, other->seconds_);
+ std::swap(nanos_, other->nanos_);
+ std::swap(_has_bits_[0], other->_has_bits_[0]);
+ _unknown_fields_.swap(other->_unknown_fields_);
+ std::swap(_cached_size_, other->_cached_size_);
+ }
+}
+
+::std::string Duration::GetTypeName() const {
+ return "mozilla.safebrowsing.Duration";
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+// @@protoc_insertion_point(global_scope)
diff --git a/toolkit/components/url-classifier/protobuf/safebrowsing.pb.h b/toolkit/components/url-classifier/protobuf/safebrowsing.pb.h
new file mode 100644
index 0000000000..3c1b436dfe
--- /dev/null
+++ b/toolkit/components/url-classifier/protobuf/safebrowsing.pb.h
@@ -0,0 +1,6283 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: safebrowsing.proto
+
+#ifndef PROTOBUF_safebrowsing_2eproto__INCLUDED
+#define PROTOBUF_safebrowsing_2eproto__INCLUDED
+
+#include <string>
+
+#include <google/protobuf/stubs/common.h>
+
+#if GOOGLE_PROTOBUF_VERSION < 2006000
+#error This file was generated by a newer version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please update
+#error your headers.
+#endif
+#if 2006001 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION
+#error This file was generated by an older version of protoc which is
+#error incompatible with your Protocol Buffer headers. Please
+#error regenerate this file with a newer version of protoc.
+#endif
+
+#include <google/protobuf/generated_message_util.h>
+#include <google/protobuf/message_lite.h>
+#include <google/protobuf/repeated_field.h>
+#include <google/protobuf/extension_set.h>
+// @@protoc_insertion_point(includes)
+
+namespace mozilla {
+namespace safebrowsing {
+
+// Internal implementation detail -- do not call these.
+void protobuf_AddDesc_safebrowsing_2eproto();
+void protobuf_AssignDesc_safebrowsing_2eproto();
+void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+class ThreatInfo;
+class ThreatMatch;
+class FindThreatMatchesRequest;
+class FindThreatMatchesResponse;
+class FetchThreatListUpdatesRequest;
+class FetchThreatListUpdatesRequest_ListUpdateRequest;
+class FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints;
+class FetchThreatListUpdatesResponse;
+class FetchThreatListUpdatesResponse_ListUpdateResponse;
+class FindFullHashesRequest;
+class FindFullHashesResponse;
+class ThreatHit;
+class ThreatHit_ThreatSource;
+class ClientInfo;
+class Checksum;
+class ThreatEntry;
+class ThreatEntrySet;
+class RawIndices;
+class RawHashes;
+class RiceDeltaEncoding;
+class ThreatEntryMetadata;
+class ThreatEntryMetadata_MetadataEntry;
+class ThreatListDescriptor;
+class ListThreatListsResponse;
+class Duration;
+
+enum FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType {
+ FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_RESPONSE_TYPE_UNSPECIFIED = 0,
+ FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_PARTIAL_UPDATE = 1,
+ FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_FULL_UPDATE = 2
+};
+bool FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(int value);
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_MIN = FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_RESPONSE_TYPE_UNSPECIFIED;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_MAX = FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_FULL_UPDATE;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_ARRAYSIZE = FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_MAX + 1;
+
+enum ThreatHit_ThreatSourceType {
+ ThreatHit_ThreatSourceType_THREAT_SOURCE_TYPE_UNSPECIFIED = 0,
+ ThreatHit_ThreatSourceType_MATCHING_URL = 1,
+ ThreatHit_ThreatSourceType_TAB_URL = 2,
+ ThreatHit_ThreatSourceType_TAB_REDIRECT = 3
+};
+bool ThreatHit_ThreatSourceType_IsValid(int value);
+const ThreatHit_ThreatSourceType ThreatHit_ThreatSourceType_ThreatSourceType_MIN = ThreatHit_ThreatSourceType_THREAT_SOURCE_TYPE_UNSPECIFIED;
+const ThreatHit_ThreatSourceType ThreatHit_ThreatSourceType_ThreatSourceType_MAX = ThreatHit_ThreatSourceType_TAB_REDIRECT;
+const int ThreatHit_ThreatSourceType_ThreatSourceType_ARRAYSIZE = ThreatHit_ThreatSourceType_ThreatSourceType_MAX + 1;
+
+enum ThreatType {
+ THREAT_TYPE_UNSPECIFIED = 0,
+ MALWARE_THREAT = 1,
+ SOCIAL_ENGINEERING_PUBLIC = 2,
+ UNWANTED_SOFTWARE = 3,
+ POTENTIALLY_HARMFUL_APPLICATION = 4,
+ SOCIAL_ENGINEERING = 5,
+ API_ABUSE = 6
+};
+bool ThreatType_IsValid(int value);
+const ThreatType ThreatType_MIN = THREAT_TYPE_UNSPECIFIED;
+const ThreatType ThreatType_MAX = API_ABUSE;
+const int ThreatType_ARRAYSIZE = ThreatType_MAX + 1;
+
+enum PlatformType {
+ PLATFORM_TYPE_UNSPECIFIED = 0,
+ WINDOWS_PLATFORM = 1,
+ LINUX_PLATFORM = 2,
+ ANDROID_PLATFORM = 3,
+ OSX_PLATFORM = 4,
+ IOS_PLATFORM = 5,
+ ANY_PLATFORM = 6,
+ ALL_PLATFORMS = 7,
+ CHROME_PLATFORM = 8
+};
+bool PlatformType_IsValid(int value);
+const PlatformType PlatformType_MIN = PLATFORM_TYPE_UNSPECIFIED;
+const PlatformType PlatformType_MAX = CHROME_PLATFORM;
+const int PlatformType_ARRAYSIZE = PlatformType_MAX + 1;
+
+enum CompressionType {
+ COMPRESSION_TYPE_UNSPECIFIED = 0,
+ RAW = 1,
+ RICE = 2
+};
+bool CompressionType_IsValid(int value);
+const CompressionType CompressionType_MIN = COMPRESSION_TYPE_UNSPECIFIED;
+const CompressionType CompressionType_MAX = RICE;
+const int CompressionType_ARRAYSIZE = CompressionType_MAX + 1;
+
+enum ThreatEntryType {
+ THREAT_ENTRY_TYPE_UNSPECIFIED = 0,
+ URL = 1,
+ EXECUTABLE = 2,
+ IP_RANGE = 3
+};
+bool ThreatEntryType_IsValid(int value);
+const ThreatEntryType ThreatEntryType_MIN = THREAT_ENTRY_TYPE_UNSPECIFIED;
+const ThreatEntryType ThreatEntryType_MAX = IP_RANGE;
+const int ThreatEntryType_ARRAYSIZE = ThreatEntryType_MAX + 1;
+
+// ===================================================================
+
+class ThreatInfo : public ::google::protobuf::MessageLite {
+ public:
+ ThreatInfo();
+ virtual ~ThreatInfo();
+
+ ThreatInfo(const ThreatInfo& from);
+
+ inline ThreatInfo& operator=(const ThreatInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatInfo& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatInfo* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatInfo* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatInfo& from);
+ void MergeFrom(const ThreatInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+ inline int threat_types_size() const;
+ inline void clear_threat_types();
+ static const int kThreatTypesFieldNumber = 1;
+ inline ::mozilla::safebrowsing::ThreatType threat_types(int index) const;
+ inline void set_threat_types(int index, ::mozilla::safebrowsing::ThreatType value);
+ inline void add_threat_types(::mozilla::safebrowsing::ThreatType value);
+ inline const ::google::protobuf::RepeatedField<int>& threat_types() const;
+ inline ::google::protobuf::RepeatedField<int>* mutable_threat_types();
+
+ // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+ inline int platform_types_size() const;
+ inline void clear_platform_types();
+ static const int kPlatformTypesFieldNumber = 2;
+ inline ::mozilla::safebrowsing::PlatformType platform_types(int index) const;
+ inline void set_platform_types(int index, ::mozilla::safebrowsing::PlatformType value);
+ inline void add_platform_types(::mozilla::safebrowsing::PlatformType value);
+ inline const ::google::protobuf::RepeatedField<int>& platform_types() const;
+ inline ::google::protobuf::RepeatedField<int>* mutable_platform_types();
+
+ // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+ inline int threat_entry_types_size() const;
+ inline void clear_threat_entry_types();
+ static const int kThreatEntryTypesFieldNumber = 4;
+ inline ::mozilla::safebrowsing::ThreatEntryType threat_entry_types(int index) const;
+ inline void set_threat_entry_types(int index, ::mozilla::safebrowsing::ThreatEntryType value);
+ inline void add_threat_entry_types(::mozilla::safebrowsing::ThreatEntryType value);
+ inline const ::google::protobuf::RepeatedField<int>& threat_entry_types() const;
+ inline ::google::protobuf::RepeatedField<int>* mutable_threat_entry_types();
+
+ // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+ inline int threat_entries_size() const;
+ inline void clear_threat_entries();
+ static const int kThreatEntriesFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::ThreatEntry& threat_entries(int index) const;
+ inline ::mozilla::safebrowsing::ThreatEntry* mutable_threat_entries(int index);
+ inline ::mozilla::safebrowsing::ThreatEntry* add_threat_entries();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntry >&
+ threat_entries() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntry >*
+ mutable_threat_entries();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatInfo)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedField<int> threat_types_;
+ ::google::protobuf::RepeatedField<int> platform_types_;
+ ::google::protobuf::RepeatedField<int> threat_entry_types_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntry > threat_entries_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatInfo* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatMatch : public ::google::protobuf::MessageLite {
+ public:
+ ThreatMatch();
+ virtual ~ThreatMatch();
+
+ ThreatMatch(const ThreatMatch& from);
+
+ inline ThreatMatch& operator=(const ThreatMatch& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatMatch& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatMatch* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatMatch* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatMatch* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatMatch& from);
+ void MergeFrom(const ThreatMatch& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ inline bool has_threat_type() const;
+ inline void clear_threat_type();
+ static const int kThreatTypeFieldNumber = 1;
+ inline ::mozilla::safebrowsing::ThreatType threat_type() const;
+ inline void set_threat_type(::mozilla::safebrowsing::ThreatType value);
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ inline bool has_platform_type() const;
+ inline void clear_platform_type();
+ static const int kPlatformTypeFieldNumber = 2;
+ inline ::mozilla::safebrowsing::PlatformType platform_type() const;
+ inline void set_platform_type(::mozilla::safebrowsing::PlatformType value);
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+ inline bool has_threat_entry_type() const;
+ inline void clear_threat_entry_type();
+ static const int kThreatEntryTypeFieldNumber = 6;
+ inline ::mozilla::safebrowsing::ThreatEntryType threat_entry_type() const;
+ inline void set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value);
+
+ // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+ inline bool has_threat() const;
+ inline void clear_threat();
+ static const int kThreatFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::ThreatEntry& threat() const;
+ inline ::mozilla::safebrowsing::ThreatEntry* mutable_threat();
+ inline ::mozilla::safebrowsing::ThreatEntry* release_threat();
+ inline void set_allocated_threat(::mozilla::safebrowsing::ThreatEntry* threat);
+
+ // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+ inline bool has_threat_entry_metadata() const;
+ inline void clear_threat_entry_metadata();
+ static const int kThreatEntryMetadataFieldNumber = 4;
+ inline const ::mozilla::safebrowsing::ThreatEntryMetadata& threat_entry_metadata() const;
+ inline ::mozilla::safebrowsing::ThreatEntryMetadata* mutable_threat_entry_metadata();
+ inline ::mozilla::safebrowsing::ThreatEntryMetadata* release_threat_entry_metadata();
+ inline void set_allocated_threat_entry_metadata(::mozilla::safebrowsing::ThreatEntryMetadata* threat_entry_metadata);
+
+ // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+ inline bool has_cache_duration() const;
+ inline void clear_cache_duration();
+ static const int kCacheDurationFieldNumber = 5;
+ inline const ::mozilla::safebrowsing::Duration& cache_duration() const;
+ inline ::mozilla::safebrowsing::Duration* mutable_cache_duration();
+ inline ::mozilla::safebrowsing::Duration* release_cache_duration();
+ inline void set_allocated_cache_duration(::mozilla::safebrowsing::Duration* cache_duration);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatMatch)
+ private:
+ inline void set_has_threat_type();
+ inline void clear_has_threat_type();
+ inline void set_has_platform_type();
+ inline void clear_has_platform_type();
+ inline void set_has_threat_entry_type();
+ inline void clear_has_threat_entry_type();
+ inline void set_has_threat();
+ inline void clear_has_threat();
+ inline void set_has_threat_entry_metadata();
+ inline void clear_has_threat_entry_metadata();
+ inline void set_has_cache_duration();
+ inline void clear_has_cache_duration();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int threat_type_;
+ int platform_type_;
+ ::mozilla::safebrowsing::ThreatEntry* threat_;
+ ::mozilla::safebrowsing::ThreatEntryMetadata* threat_entry_metadata_;
+ ::mozilla::safebrowsing::Duration* cache_duration_;
+ int threat_entry_type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatMatch* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FindThreatMatchesRequest : public ::google::protobuf::MessageLite {
+ public:
+ FindThreatMatchesRequest();
+ virtual ~FindThreatMatchesRequest();
+
+ FindThreatMatchesRequest(const FindThreatMatchesRequest& from);
+
+ inline FindThreatMatchesRequest& operator=(const FindThreatMatchesRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FindThreatMatchesRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FindThreatMatchesRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FindThreatMatchesRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ FindThreatMatchesRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FindThreatMatchesRequest& from);
+ void MergeFrom(const FindThreatMatchesRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ inline bool has_client() const;
+ inline void clear_client();
+ static const int kClientFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ClientInfo& client() const;
+ inline ::mozilla::safebrowsing::ClientInfo* mutable_client();
+ inline ::mozilla::safebrowsing::ClientInfo* release_client();
+ inline void set_allocated_client(::mozilla::safebrowsing::ClientInfo* client);
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+ inline bool has_threat_info() const;
+ inline void clear_threat_info();
+ static const int kThreatInfoFieldNumber = 2;
+ inline const ::mozilla::safebrowsing::ThreatInfo& threat_info() const;
+ inline ::mozilla::safebrowsing::ThreatInfo* mutable_threat_info();
+ inline ::mozilla::safebrowsing::ThreatInfo* release_threat_info();
+ inline void set_allocated_threat_info(::mozilla::safebrowsing::ThreatInfo* threat_info);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FindThreatMatchesRequest)
+ private:
+ inline void set_has_client();
+ inline void clear_has_client();
+ inline void set_has_threat_info();
+ inline void clear_has_threat_info();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::mozilla::safebrowsing::ClientInfo* client_;
+ ::mozilla::safebrowsing::ThreatInfo* threat_info_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FindThreatMatchesRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FindThreatMatchesResponse : public ::google::protobuf::MessageLite {
+ public:
+ FindThreatMatchesResponse();
+ virtual ~FindThreatMatchesResponse();
+
+ FindThreatMatchesResponse(const FindThreatMatchesResponse& from);
+
+ inline FindThreatMatchesResponse& operator=(const FindThreatMatchesResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FindThreatMatchesResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FindThreatMatchesResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FindThreatMatchesResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ FindThreatMatchesResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FindThreatMatchesResponse& from);
+ void MergeFrom(const FindThreatMatchesResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ inline int matches_size() const;
+ inline void clear_matches();
+ static const int kMatchesFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ThreatMatch& matches(int index) const;
+ inline ::mozilla::safebrowsing::ThreatMatch* mutable_matches(int index);
+ inline ::mozilla::safebrowsing::ThreatMatch* add_matches();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >&
+ matches() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >*
+ mutable_matches();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FindThreatMatchesResponse)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch > matches_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FindThreatMatchesResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints : public ::google::protobuf::MessageLite {
+ public:
+ FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints();
+ virtual ~FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints();
+
+ FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from);
+
+ inline FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& operator=(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* other);
+
+ // implements Message ----------------------------------------------
+
+ FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from);
+ void MergeFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int32 max_update_entries = 1;
+ inline bool has_max_update_entries() const;
+ inline void clear_max_update_entries();
+ static const int kMaxUpdateEntriesFieldNumber = 1;
+ inline ::google::protobuf::int32 max_update_entries() const;
+ inline void set_max_update_entries(::google::protobuf::int32 value);
+
+ // optional int32 max_database_entries = 2;
+ inline bool has_max_database_entries() const;
+ inline void clear_max_database_entries();
+ static const int kMaxDatabaseEntriesFieldNumber = 2;
+ inline ::google::protobuf::int32 max_database_entries() const;
+ inline void set_max_database_entries(::google::protobuf::int32 value);
+
+ // optional string region = 3;
+ inline bool has_region() const;
+ inline void clear_region();
+ static const int kRegionFieldNumber = 3;
+ inline const ::std::string& region() const;
+ inline void set_region(const ::std::string& value);
+ inline void set_region(const char* value);
+ inline void set_region(const char* value, size_t size);
+ inline ::std::string* mutable_region();
+ inline ::std::string* release_region();
+ inline void set_allocated_region(::std::string* region);
+
+ // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+ inline int supported_compressions_size() const;
+ inline void clear_supported_compressions();
+ static const int kSupportedCompressionsFieldNumber = 4;
+ inline ::mozilla::safebrowsing::CompressionType supported_compressions(int index) const;
+ inline void set_supported_compressions(int index, ::mozilla::safebrowsing::CompressionType value);
+ inline void add_supported_compressions(::mozilla::safebrowsing::CompressionType value);
+ inline const ::google::protobuf::RepeatedField<int>& supported_compressions() const;
+ inline ::google::protobuf::RepeatedField<int>* mutable_supported_compressions();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+ private:
+ inline void set_has_max_update_entries();
+ inline void clear_has_max_update_entries();
+ inline void set_has_max_database_entries();
+ inline void clear_has_max_database_entries();
+ inline void set_has_region();
+ inline void clear_has_region();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::int32 max_update_entries_;
+ ::google::protobuf::int32 max_database_entries_;
+ ::std::string* region_;
+ ::google::protobuf::RepeatedField<int> supported_compressions_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FetchThreatListUpdatesRequest_ListUpdateRequest : public ::google::protobuf::MessageLite {
+ public:
+ FetchThreatListUpdatesRequest_ListUpdateRequest();
+ virtual ~FetchThreatListUpdatesRequest_ListUpdateRequest();
+
+ FetchThreatListUpdatesRequest_ListUpdateRequest(const FetchThreatListUpdatesRequest_ListUpdateRequest& from);
+
+ inline FetchThreatListUpdatesRequest_ListUpdateRequest& operator=(const FetchThreatListUpdatesRequest_ListUpdateRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FetchThreatListUpdatesRequest_ListUpdateRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FetchThreatListUpdatesRequest_ListUpdateRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FetchThreatListUpdatesRequest_ListUpdateRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ FetchThreatListUpdatesRequest_ListUpdateRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest& from);
+ void MergeFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints Constraints;
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ inline bool has_threat_type() const;
+ inline void clear_threat_type();
+ static const int kThreatTypeFieldNumber = 1;
+ inline ::mozilla::safebrowsing::ThreatType threat_type() const;
+ inline void set_threat_type(::mozilla::safebrowsing::ThreatType value);
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ inline bool has_platform_type() const;
+ inline void clear_platform_type();
+ static const int kPlatformTypeFieldNumber = 2;
+ inline ::mozilla::safebrowsing::PlatformType platform_type() const;
+ inline void set_platform_type(::mozilla::safebrowsing::PlatformType value);
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+ inline bool has_threat_entry_type() const;
+ inline void clear_threat_entry_type();
+ static const int kThreatEntryTypeFieldNumber = 5;
+ inline ::mozilla::safebrowsing::ThreatEntryType threat_entry_type() const;
+ inline void set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value);
+
+ // optional bytes state = 3;
+ inline bool has_state() const;
+ inline void clear_state();
+ static const int kStateFieldNumber = 3;
+ inline const ::std::string& state() const;
+ inline void set_state(const ::std::string& value);
+ inline void set_state(const char* value);
+ inline void set_state(const void* value, size_t size);
+ inline ::std::string* mutable_state();
+ inline ::std::string* release_state();
+ inline void set_allocated_state(::std::string* state);
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+ inline bool has_constraints() const;
+ inline void clear_constraints();
+ static const int kConstraintsFieldNumber = 4;
+ inline const ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& constraints() const;
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* mutable_constraints();
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* release_constraints();
+ inline void set_allocated_constraints(::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* constraints);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+ private:
+ inline void set_has_threat_type();
+ inline void clear_has_threat_type();
+ inline void set_has_platform_type();
+ inline void clear_has_platform_type();
+ inline void set_has_threat_entry_type();
+ inline void clear_has_threat_entry_type();
+ inline void set_has_state();
+ inline void clear_has_state();
+ inline void set_has_constraints();
+ inline void clear_has_constraints();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int threat_type_;
+ int platform_type_;
+ ::std::string* state_;
+ ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* constraints_;
+ int threat_entry_type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FetchThreatListUpdatesRequest_ListUpdateRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FetchThreatListUpdatesRequest : public ::google::protobuf::MessageLite {
+ public:
+ FetchThreatListUpdatesRequest();
+ virtual ~FetchThreatListUpdatesRequest();
+
+ FetchThreatListUpdatesRequest(const FetchThreatListUpdatesRequest& from);
+
+ inline FetchThreatListUpdatesRequest& operator=(const FetchThreatListUpdatesRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FetchThreatListUpdatesRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FetchThreatListUpdatesRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FetchThreatListUpdatesRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ FetchThreatListUpdatesRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FetchThreatListUpdatesRequest& from);
+ void MergeFrom(const FetchThreatListUpdatesRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FetchThreatListUpdatesRequest_ListUpdateRequest ListUpdateRequest;
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ inline bool has_client() const;
+ inline void clear_client();
+ static const int kClientFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ClientInfo& client() const;
+ inline ::mozilla::safebrowsing::ClientInfo* mutable_client();
+ inline ::mozilla::safebrowsing::ClientInfo* release_client();
+ inline void set_allocated_client(::mozilla::safebrowsing::ClientInfo* client);
+
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+ inline int list_update_requests_size() const;
+ inline void clear_list_update_requests();
+ static const int kListUpdateRequestsFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest& list_update_requests(int index) const;
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest* mutable_list_update_requests(int index);
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest* add_list_update_requests();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest >&
+ list_update_requests() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest >*
+ mutable_list_update_requests();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+ private:
+ inline void set_has_client();
+ inline void clear_has_client();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::mozilla::safebrowsing::ClientInfo* client_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest > list_update_requests_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FetchThreatListUpdatesRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FetchThreatListUpdatesResponse_ListUpdateResponse : public ::google::protobuf::MessageLite {
+ public:
+ FetchThreatListUpdatesResponse_ListUpdateResponse();
+ virtual ~FetchThreatListUpdatesResponse_ListUpdateResponse();
+
+ FetchThreatListUpdatesResponse_ListUpdateResponse(const FetchThreatListUpdatesResponse_ListUpdateResponse& from);
+
+ inline FetchThreatListUpdatesResponse_ListUpdateResponse& operator=(const FetchThreatListUpdatesResponse_ListUpdateResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FetchThreatListUpdatesResponse_ListUpdateResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FetchThreatListUpdatesResponse_ListUpdateResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FetchThreatListUpdatesResponse_ListUpdateResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ FetchThreatListUpdatesResponse_ListUpdateResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FetchThreatListUpdatesResponse_ListUpdateResponse& from);
+ void MergeFrom(const FetchThreatListUpdatesResponse_ListUpdateResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType ResponseType;
+ static const ResponseType RESPONSE_TYPE_UNSPECIFIED = FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_RESPONSE_TYPE_UNSPECIFIED;
+ static const ResponseType PARTIAL_UPDATE = FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_PARTIAL_UPDATE;
+ static const ResponseType FULL_UPDATE = FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_FULL_UPDATE;
+ static inline bool ResponseType_IsValid(int value) {
+ return FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(value);
+ }
+ static const ResponseType ResponseType_MIN =
+ FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_MIN;
+ static const ResponseType ResponseType_MAX =
+ FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_MAX;
+ static const int ResponseType_ARRAYSIZE =
+ FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_ResponseType_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ inline bool has_threat_type() const;
+ inline void clear_threat_type();
+ static const int kThreatTypeFieldNumber = 1;
+ inline ::mozilla::safebrowsing::ThreatType threat_type() const;
+ inline void set_threat_type(::mozilla::safebrowsing::ThreatType value);
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+ inline bool has_threat_entry_type() const;
+ inline void clear_threat_entry_type();
+ static const int kThreatEntryTypeFieldNumber = 2;
+ inline ::mozilla::safebrowsing::ThreatEntryType threat_entry_type() const;
+ inline void set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value);
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+ inline bool has_platform_type() const;
+ inline void clear_platform_type();
+ static const int kPlatformTypeFieldNumber = 3;
+ inline ::mozilla::safebrowsing::PlatformType platform_type() const;
+ inline void set_platform_type(::mozilla::safebrowsing::PlatformType value);
+
+ // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+ inline bool has_response_type() const;
+ inline void clear_response_type();
+ static const int kResponseTypeFieldNumber = 4;
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType response_type() const;
+ inline void set_response_type(::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType value);
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+ inline int additions_size() const;
+ inline void clear_additions();
+ static const int kAdditionsFieldNumber = 5;
+ inline const ::mozilla::safebrowsing::ThreatEntrySet& additions(int index) const;
+ inline ::mozilla::safebrowsing::ThreatEntrySet* mutable_additions(int index);
+ inline ::mozilla::safebrowsing::ThreatEntrySet* add_additions();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >&
+ additions() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >*
+ mutable_additions();
+
+ // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+ inline int removals_size() const;
+ inline void clear_removals();
+ static const int kRemovalsFieldNumber = 6;
+ inline const ::mozilla::safebrowsing::ThreatEntrySet& removals(int index) const;
+ inline ::mozilla::safebrowsing::ThreatEntrySet* mutable_removals(int index);
+ inline ::mozilla::safebrowsing::ThreatEntrySet* add_removals();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >&
+ removals() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >*
+ mutable_removals();
+
+ // optional bytes new_client_state = 7;
+ inline bool has_new_client_state() const;
+ inline void clear_new_client_state();
+ static const int kNewClientStateFieldNumber = 7;
+ inline const ::std::string& new_client_state() const;
+ inline void set_new_client_state(const ::std::string& value);
+ inline void set_new_client_state(const char* value);
+ inline void set_new_client_state(const void* value, size_t size);
+ inline ::std::string* mutable_new_client_state();
+ inline ::std::string* release_new_client_state();
+ inline void set_allocated_new_client_state(::std::string* new_client_state);
+
+ // optional .mozilla.safebrowsing.Checksum checksum = 8;
+ inline bool has_checksum() const;
+ inline void clear_checksum();
+ static const int kChecksumFieldNumber = 8;
+ inline const ::mozilla::safebrowsing::Checksum& checksum() const;
+ inline ::mozilla::safebrowsing::Checksum* mutable_checksum();
+ inline ::mozilla::safebrowsing::Checksum* release_checksum();
+ inline void set_allocated_checksum(::mozilla::safebrowsing::Checksum* checksum);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+ private:
+ inline void set_has_threat_type();
+ inline void clear_has_threat_type();
+ inline void set_has_threat_entry_type();
+ inline void clear_has_threat_entry_type();
+ inline void set_has_platform_type();
+ inline void clear_has_platform_type();
+ inline void set_has_response_type();
+ inline void clear_has_response_type();
+ inline void set_has_new_client_state();
+ inline void clear_has_new_client_state();
+ inline void set_has_checksum();
+ inline void clear_has_checksum();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int threat_type_;
+ int threat_entry_type_;
+ int platform_type_;
+ int response_type_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet > additions_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet > removals_;
+ ::std::string* new_client_state_;
+ ::mozilla::safebrowsing::Checksum* checksum_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FetchThreatListUpdatesResponse_ListUpdateResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FetchThreatListUpdatesResponse : public ::google::protobuf::MessageLite {
+ public:
+ FetchThreatListUpdatesResponse();
+ virtual ~FetchThreatListUpdatesResponse();
+
+ FetchThreatListUpdatesResponse(const FetchThreatListUpdatesResponse& from);
+
+ inline FetchThreatListUpdatesResponse& operator=(const FetchThreatListUpdatesResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FetchThreatListUpdatesResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FetchThreatListUpdatesResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FetchThreatListUpdatesResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ FetchThreatListUpdatesResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FetchThreatListUpdatesResponse& from);
+ void MergeFrom(const FetchThreatListUpdatesResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef FetchThreatListUpdatesResponse_ListUpdateResponse ListUpdateResponse;
+
+ // accessors -------------------------------------------------------
+
+ // repeated .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse list_update_responses = 1;
+ inline int list_update_responses_size() const;
+ inline void clear_list_update_responses();
+ static const int kListUpdateResponsesFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse& list_update_responses(int index) const;
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse* mutable_list_update_responses(int index);
+ inline ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse* add_list_update_responses();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse >&
+ list_update_responses() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse >*
+ mutable_list_update_responses();
+
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ inline bool has_minimum_wait_duration() const;
+ inline void clear_minimum_wait_duration();
+ static const int kMinimumWaitDurationFieldNumber = 2;
+ inline const ::mozilla::safebrowsing::Duration& minimum_wait_duration() const;
+ inline ::mozilla::safebrowsing::Duration* mutable_minimum_wait_duration();
+ inline ::mozilla::safebrowsing::Duration* release_minimum_wait_duration();
+ inline void set_allocated_minimum_wait_duration(::mozilla::safebrowsing::Duration* minimum_wait_duration);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+ private:
+ inline void set_has_minimum_wait_duration();
+ inline void clear_has_minimum_wait_duration();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse > list_update_responses_;
+ ::mozilla::safebrowsing::Duration* minimum_wait_duration_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FetchThreatListUpdatesResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FindFullHashesRequest : public ::google::protobuf::MessageLite {
+ public:
+ FindFullHashesRequest();
+ virtual ~FindFullHashesRequest();
+
+ FindFullHashesRequest(const FindFullHashesRequest& from);
+
+ inline FindFullHashesRequest& operator=(const FindFullHashesRequest& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FindFullHashesRequest& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FindFullHashesRequest* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FindFullHashesRequest* other);
+
+ // implements Message ----------------------------------------------
+
+ FindFullHashesRequest* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FindFullHashesRequest& from);
+ void MergeFrom(const FindFullHashesRequest& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ClientInfo client = 1;
+ inline bool has_client() const;
+ inline void clear_client();
+ static const int kClientFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ClientInfo& client() const;
+ inline ::mozilla::safebrowsing::ClientInfo* mutable_client();
+ inline ::mozilla::safebrowsing::ClientInfo* release_client();
+ inline void set_allocated_client(::mozilla::safebrowsing::ClientInfo* client);
+
+ // repeated bytes client_states = 2;
+ inline int client_states_size() const;
+ inline void clear_client_states();
+ static const int kClientStatesFieldNumber = 2;
+ inline const ::std::string& client_states(int index) const;
+ inline ::std::string* mutable_client_states(int index);
+ inline void set_client_states(int index, const ::std::string& value);
+ inline void set_client_states(int index, const char* value);
+ inline void set_client_states(int index, const void* value, size_t size);
+ inline ::std::string* add_client_states();
+ inline void add_client_states(const ::std::string& value);
+ inline void add_client_states(const char* value);
+ inline void add_client_states(const void* value, size_t size);
+ inline const ::google::protobuf::RepeatedPtrField< ::std::string>& client_states() const;
+ inline ::google::protobuf::RepeatedPtrField< ::std::string>* mutable_client_states();
+
+ // optional .mozilla.safebrowsing.ThreatInfo threat_info = 3;
+ inline bool has_threat_info() const;
+ inline void clear_threat_info();
+ static const int kThreatInfoFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::ThreatInfo& threat_info() const;
+ inline ::mozilla::safebrowsing::ThreatInfo* mutable_threat_info();
+ inline ::mozilla::safebrowsing::ThreatInfo* release_threat_info();
+ inline void set_allocated_threat_info(::mozilla::safebrowsing::ThreatInfo* threat_info);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FindFullHashesRequest)
+ private:
+ inline void set_has_client();
+ inline void clear_has_client();
+ inline void set_has_threat_info();
+ inline void clear_has_threat_info();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::mozilla::safebrowsing::ClientInfo* client_;
+ ::google::protobuf::RepeatedPtrField< ::std::string> client_states_;
+ ::mozilla::safebrowsing::ThreatInfo* threat_info_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FindFullHashesRequest* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class FindFullHashesResponse : public ::google::protobuf::MessageLite {
+ public:
+ FindFullHashesResponse();
+ virtual ~FindFullHashesResponse();
+
+ FindFullHashesResponse(const FindFullHashesResponse& from);
+
+ inline FindFullHashesResponse& operator=(const FindFullHashesResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const FindFullHashesResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const FindFullHashesResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(FindFullHashesResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ FindFullHashesResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const FindFullHashesResponse& from);
+ void MergeFrom(const FindFullHashesResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+ inline int matches_size() const;
+ inline void clear_matches();
+ static const int kMatchesFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ThreatMatch& matches(int index) const;
+ inline ::mozilla::safebrowsing::ThreatMatch* mutable_matches(int index);
+ inline ::mozilla::safebrowsing::ThreatMatch* add_matches();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >&
+ matches() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >*
+ mutable_matches();
+
+ // optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+ inline bool has_minimum_wait_duration() const;
+ inline void clear_minimum_wait_duration();
+ static const int kMinimumWaitDurationFieldNumber = 2;
+ inline const ::mozilla::safebrowsing::Duration& minimum_wait_duration() const;
+ inline ::mozilla::safebrowsing::Duration* mutable_minimum_wait_duration();
+ inline ::mozilla::safebrowsing::Duration* release_minimum_wait_duration();
+ inline void set_allocated_minimum_wait_duration(::mozilla::safebrowsing::Duration* minimum_wait_duration);
+
+ // optional .mozilla.safebrowsing.Duration negative_cache_duration = 3;
+ inline bool has_negative_cache_duration() const;
+ inline void clear_negative_cache_duration();
+ static const int kNegativeCacheDurationFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::Duration& negative_cache_duration() const;
+ inline ::mozilla::safebrowsing::Duration* mutable_negative_cache_duration();
+ inline ::mozilla::safebrowsing::Duration* release_negative_cache_duration();
+ inline void set_allocated_negative_cache_duration(::mozilla::safebrowsing::Duration* negative_cache_duration);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.FindFullHashesResponse)
+ private:
+ inline void set_has_minimum_wait_duration();
+ inline void clear_has_minimum_wait_duration();
+ inline void set_has_negative_cache_duration();
+ inline void clear_has_negative_cache_duration();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch > matches_;
+ ::mozilla::safebrowsing::Duration* minimum_wait_duration_;
+ ::mozilla::safebrowsing::Duration* negative_cache_duration_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static FindFullHashesResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatHit_ThreatSource : public ::google::protobuf::MessageLite {
+ public:
+ ThreatHit_ThreatSource();
+ virtual ~ThreatHit_ThreatSource();
+
+ ThreatHit_ThreatSource(const ThreatHit_ThreatSource& from);
+
+ inline ThreatHit_ThreatSource& operator=(const ThreatHit_ThreatSource& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatHit_ThreatSource& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatHit_ThreatSource* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatHit_ThreatSource* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatHit_ThreatSource* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatHit_ThreatSource& from);
+ void MergeFrom(const ThreatHit_ThreatSource& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string url = 1;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 1;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // optional .mozilla.safebrowsing.ThreatHit.ThreatSourceType type = 2;
+ inline bool has_type() const;
+ inline void clear_type();
+ static const int kTypeFieldNumber = 2;
+ inline ::mozilla::safebrowsing::ThreatHit_ThreatSourceType type() const;
+ inline void set_type(::mozilla::safebrowsing::ThreatHit_ThreatSourceType value);
+
+ // optional string remote_ip = 3;
+ inline bool has_remote_ip() const;
+ inline void clear_remote_ip();
+ static const int kRemoteIpFieldNumber = 3;
+ inline const ::std::string& remote_ip() const;
+ inline void set_remote_ip(const ::std::string& value);
+ inline void set_remote_ip(const char* value);
+ inline void set_remote_ip(const char* value, size_t size);
+ inline ::std::string* mutable_remote_ip();
+ inline ::std::string* release_remote_ip();
+ inline void set_allocated_remote_ip(::std::string* remote_ip);
+
+ // optional string referrer = 4;
+ inline bool has_referrer() const;
+ inline void clear_referrer();
+ static const int kReferrerFieldNumber = 4;
+ inline const ::std::string& referrer() const;
+ inline void set_referrer(const ::std::string& value);
+ inline void set_referrer(const char* value);
+ inline void set_referrer(const char* value, size_t size);
+ inline ::std::string* mutable_referrer();
+ inline ::std::string* release_referrer();
+ inline void set_allocated_referrer(::std::string* referrer);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatHit.ThreatSource)
+ private:
+ inline void set_has_url();
+ inline void clear_has_url();
+ inline void set_has_type();
+ inline void clear_has_type();
+ inline void set_has_remote_ip();
+ inline void clear_has_remote_ip();
+ inline void set_has_referrer();
+ inline void clear_has_referrer();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* url_;
+ ::std::string* remote_ip_;
+ ::std::string* referrer_;
+ int type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatHit_ThreatSource* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatHit : public ::google::protobuf::MessageLite {
+ public:
+ ThreatHit();
+ virtual ~ThreatHit();
+
+ ThreatHit(const ThreatHit& from);
+
+ inline ThreatHit& operator=(const ThreatHit& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatHit& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatHit* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatHit* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatHit* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatHit& from);
+ void MergeFrom(const ThreatHit& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ThreatHit_ThreatSource ThreatSource;
+
+ typedef ThreatHit_ThreatSourceType ThreatSourceType;
+ static const ThreatSourceType THREAT_SOURCE_TYPE_UNSPECIFIED = ThreatHit_ThreatSourceType_THREAT_SOURCE_TYPE_UNSPECIFIED;
+ static const ThreatSourceType MATCHING_URL = ThreatHit_ThreatSourceType_MATCHING_URL;
+ static const ThreatSourceType TAB_URL = ThreatHit_ThreatSourceType_TAB_URL;
+ static const ThreatSourceType TAB_REDIRECT = ThreatHit_ThreatSourceType_TAB_REDIRECT;
+ static inline bool ThreatSourceType_IsValid(int value) {
+ return ThreatHit_ThreatSourceType_IsValid(value);
+ }
+ static const ThreatSourceType ThreatSourceType_MIN =
+ ThreatHit_ThreatSourceType_ThreatSourceType_MIN;
+ static const ThreatSourceType ThreatSourceType_MAX =
+ ThreatHit_ThreatSourceType_ThreatSourceType_MAX;
+ static const int ThreatSourceType_ARRAYSIZE =
+ ThreatHit_ThreatSourceType_ThreatSourceType_ARRAYSIZE;
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ inline bool has_threat_type() const;
+ inline void clear_threat_type();
+ static const int kThreatTypeFieldNumber = 1;
+ inline ::mozilla::safebrowsing::ThreatType threat_type() const;
+ inline void set_threat_type(::mozilla::safebrowsing::ThreatType value);
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ inline bool has_platform_type() const;
+ inline void clear_platform_type();
+ static const int kPlatformTypeFieldNumber = 2;
+ inline ::mozilla::safebrowsing::PlatformType platform_type() const;
+ inline void set_platform_type(::mozilla::safebrowsing::PlatformType value);
+
+ // optional .mozilla.safebrowsing.ThreatEntry entry = 3;
+ inline bool has_entry() const;
+ inline void clear_entry();
+ static const int kEntryFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::ThreatEntry& entry() const;
+ inline ::mozilla::safebrowsing::ThreatEntry* mutable_entry();
+ inline ::mozilla::safebrowsing::ThreatEntry* release_entry();
+ inline void set_allocated_entry(::mozilla::safebrowsing::ThreatEntry* entry);
+
+ // repeated .mozilla.safebrowsing.ThreatHit.ThreatSource resources = 4;
+ inline int resources_size() const;
+ inline void clear_resources();
+ static const int kResourcesFieldNumber = 4;
+ inline const ::mozilla::safebrowsing::ThreatHit_ThreatSource& resources(int index) const;
+ inline ::mozilla::safebrowsing::ThreatHit_ThreatSource* mutable_resources(int index);
+ inline ::mozilla::safebrowsing::ThreatHit_ThreatSource* add_resources();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatHit_ThreatSource >&
+ resources() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatHit_ThreatSource >*
+ mutable_resources();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatHit)
+ private:
+ inline void set_has_threat_type();
+ inline void clear_has_threat_type();
+ inline void set_has_platform_type();
+ inline void clear_has_platform_type();
+ inline void set_has_entry();
+ inline void clear_has_entry();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int threat_type_;
+ int platform_type_;
+ ::mozilla::safebrowsing::ThreatEntry* entry_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatHit_ThreatSource > resources_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatHit* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ClientInfo : public ::google::protobuf::MessageLite {
+ public:
+ ClientInfo();
+ virtual ~ClientInfo();
+
+ ClientInfo(const ClientInfo& from);
+
+ inline ClientInfo& operator=(const ClientInfo& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ClientInfo& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ClientInfo* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ClientInfo* other);
+
+ // implements Message ----------------------------------------------
+
+ ClientInfo* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ClientInfo& from);
+ void MergeFrom(const ClientInfo& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional string client_id = 1;
+ inline bool has_client_id() const;
+ inline void clear_client_id();
+ static const int kClientIdFieldNumber = 1;
+ inline const ::std::string& client_id() const;
+ inline void set_client_id(const ::std::string& value);
+ inline void set_client_id(const char* value);
+ inline void set_client_id(const char* value, size_t size);
+ inline ::std::string* mutable_client_id();
+ inline ::std::string* release_client_id();
+ inline void set_allocated_client_id(::std::string* client_id);
+
+ // optional string client_version = 2;
+ inline bool has_client_version() const;
+ inline void clear_client_version();
+ static const int kClientVersionFieldNumber = 2;
+ inline const ::std::string& client_version() const;
+ inline void set_client_version(const ::std::string& value);
+ inline void set_client_version(const char* value);
+ inline void set_client_version(const char* value, size_t size);
+ inline ::std::string* mutable_client_version();
+ inline ::std::string* release_client_version();
+ inline void set_allocated_client_version(::std::string* client_version);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ClientInfo)
+ private:
+ inline void set_has_client_id();
+ inline void clear_has_client_id();
+ inline void set_has_client_version();
+ inline void clear_has_client_version();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* client_id_;
+ ::std::string* client_version_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ClientInfo* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class Checksum : public ::google::protobuf::MessageLite {
+ public:
+ Checksum();
+ virtual ~Checksum();
+
+ Checksum(const Checksum& from);
+
+ inline Checksum& operator=(const Checksum& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const Checksum& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const Checksum* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(Checksum* other);
+
+ // implements Message ----------------------------------------------
+
+ Checksum* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const Checksum& from);
+ void MergeFrom(const Checksum& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes sha256 = 1;
+ inline bool has_sha256() const;
+ inline void clear_sha256();
+ static const int kSha256FieldNumber = 1;
+ inline const ::std::string& sha256() const;
+ inline void set_sha256(const ::std::string& value);
+ inline void set_sha256(const char* value);
+ inline void set_sha256(const void* value, size_t size);
+ inline ::std::string* mutable_sha256();
+ inline ::std::string* release_sha256();
+ inline void set_allocated_sha256(::std::string* sha256);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.Checksum)
+ private:
+ inline void set_has_sha256();
+ inline void clear_has_sha256();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* sha256_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static Checksum* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatEntry : public ::google::protobuf::MessageLite {
+ public:
+ ThreatEntry();
+ virtual ~ThreatEntry();
+
+ ThreatEntry(const ThreatEntry& from);
+
+ inline ThreatEntry& operator=(const ThreatEntry& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatEntry& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatEntry* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatEntry* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatEntry* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatEntry& from);
+ void MergeFrom(const ThreatEntry& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes hash = 1;
+ inline bool has_hash() const;
+ inline void clear_hash();
+ static const int kHashFieldNumber = 1;
+ inline const ::std::string& hash() const;
+ inline void set_hash(const ::std::string& value);
+ inline void set_hash(const char* value);
+ inline void set_hash(const void* value, size_t size);
+ inline ::std::string* mutable_hash();
+ inline ::std::string* release_hash();
+ inline void set_allocated_hash(::std::string* hash);
+
+ // optional string url = 2;
+ inline bool has_url() const;
+ inline void clear_url();
+ static const int kUrlFieldNumber = 2;
+ inline const ::std::string& url() const;
+ inline void set_url(const ::std::string& value);
+ inline void set_url(const char* value);
+ inline void set_url(const char* value, size_t size);
+ inline ::std::string* mutable_url();
+ inline ::std::string* release_url();
+ inline void set_allocated_url(::std::string* url);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatEntry)
+ private:
+ inline void set_has_hash();
+ inline void clear_has_hash();
+ inline void set_has_url();
+ inline void clear_has_url();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* hash_;
+ ::std::string* url_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatEntry* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatEntrySet : public ::google::protobuf::MessageLite {
+ public:
+ ThreatEntrySet();
+ virtual ~ThreatEntrySet();
+
+ ThreatEntrySet(const ThreatEntrySet& from);
+
+ inline ThreatEntrySet& operator=(const ThreatEntrySet& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatEntrySet& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatEntrySet* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatEntrySet* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatEntrySet* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatEntrySet& from);
+ void MergeFrom(const ThreatEntrySet& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.CompressionType compression_type = 1;
+ inline bool has_compression_type() const;
+ inline void clear_compression_type();
+ static const int kCompressionTypeFieldNumber = 1;
+ inline ::mozilla::safebrowsing::CompressionType compression_type() const;
+ inline void set_compression_type(::mozilla::safebrowsing::CompressionType value);
+
+ // optional .mozilla.safebrowsing.RawHashes raw_hashes = 2;
+ inline bool has_raw_hashes() const;
+ inline void clear_raw_hashes();
+ static const int kRawHashesFieldNumber = 2;
+ inline const ::mozilla::safebrowsing::RawHashes& raw_hashes() const;
+ inline ::mozilla::safebrowsing::RawHashes* mutable_raw_hashes();
+ inline ::mozilla::safebrowsing::RawHashes* release_raw_hashes();
+ inline void set_allocated_raw_hashes(::mozilla::safebrowsing::RawHashes* raw_hashes);
+
+ // optional .mozilla.safebrowsing.RawIndices raw_indices = 3;
+ inline bool has_raw_indices() const;
+ inline void clear_raw_indices();
+ static const int kRawIndicesFieldNumber = 3;
+ inline const ::mozilla::safebrowsing::RawIndices& raw_indices() const;
+ inline ::mozilla::safebrowsing::RawIndices* mutable_raw_indices();
+ inline ::mozilla::safebrowsing::RawIndices* release_raw_indices();
+ inline void set_allocated_raw_indices(::mozilla::safebrowsing::RawIndices* raw_indices);
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_hashes = 4;
+ inline bool has_rice_hashes() const;
+ inline void clear_rice_hashes();
+ static const int kRiceHashesFieldNumber = 4;
+ inline const ::mozilla::safebrowsing::RiceDeltaEncoding& rice_hashes() const;
+ inline ::mozilla::safebrowsing::RiceDeltaEncoding* mutable_rice_hashes();
+ inline ::mozilla::safebrowsing::RiceDeltaEncoding* release_rice_hashes();
+ inline void set_allocated_rice_hashes(::mozilla::safebrowsing::RiceDeltaEncoding* rice_hashes);
+
+ // optional .mozilla.safebrowsing.RiceDeltaEncoding rice_indices = 5;
+ inline bool has_rice_indices() const;
+ inline void clear_rice_indices();
+ static const int kRiceIndicesFieldNumber = 5;
+ inline const ::mozilla::safebrowsing::RiceDeltaEncoding& rice_indices() const;
+ inline ::mozilla::safebrowsing::RiceDeltaEncoding* mutable_rice_indices();
+ inline ::mozilla::safebrowsing::RiceDeltaEncoding* release_rice_indices();
+ inline void set_allocated_rice_indices(::mozilla::safebrowsing::RiceDeltaEncoding* rice_indices);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatEntrySet)
+ private:
+ inline void set_has_compression_type();
+ inline void clear_has_compression_type();
+ inline void set_has_raw_hashes();
+ inline void clear_has_raw_hashes();
+ inline void set_has_raw_indices();
+ inline void clear_has_raw_indices();
+ inline void set_has_rice_hashes();
+ inline void clear_has_rice_hashes();
+ inline void set_has_rice_indices();
+ inline void clear_has_rice_indices();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::mozilla::safebrowsing::RawHashes* raw_hashes_;
+ ::mozilla::safebrowsing::RawIndices* raw_indices_;
+ ::mozilla::safebrowsing::RiceDeltaEncoding* rice_hashes_;
+ ::mozilla::safebrowsing::RiceDeltaEncoding* rice_indices_;
+ int compression_type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatEntrySet* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class RawIndices : public ::google::protobuf::MessageLite {
+ public:
+ RawIndices();
+ virtual ~RawIndices();
+
+ RawIndices(const RawIndices& from);
+
+ inline RawIndices& operator=(const RawIndices& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const RawIndices& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const RawIndices* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(RawIndices* other);
+
+ // implements Message ----------------------------------------------
+
+ RawIndices* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const RawIndices& from);
+ void MergeFrom(const RawIndices& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated int32 indices = 1;
+ inline int indices_size() const;
+ inline void clear_indices();
+ static const int kIndicesFieldNumber = 1;
+ inline ::google::protobuf::int32 indices(int index) const;
+ inline void set_indices(int index, ::google::protobuf::int32 value);
+ inline void add_indices(::google::protobuf::int32 value);
+ inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+ indices() const;
+ inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+ mutable_indices();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.RawIndices)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedField< ::google::protobuf::int32 > indices_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static RawIndices* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class RawHashes : public ::google::protobuf::MessageLite {
+ public:
+ RawHashes();
+ virtual ~RawHashes();
+
+ RawHashes(const RawHashes& from);
+
+ inline RawHashes& operator=(const RawHashes& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const RawHashes& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const RawHashes* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(RawHashes* other);
+
+ // implements Message ----------------------------------------------
+
+ RawHashes* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const RawHashes& from);
+ void MergeFrom(const RawHashes& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int32 prefix_size = 1;
+ inline bool has_prefix_size() const;
+ inline void clear_prefix_size();
+ static const int kPrefixSizeFieldNumber = 1;
+ inline ::google::protobuf::int32 prefix_size() const;
+ inline void set_prefix_size(::google::protobuf::int32 value);
+
+ // optional bytes raw_hashes = 2;
+ inline bool has_raw_hashes() const;
+ inline void clear_raw_hashes();
+ static const int kRawHashesFieldNumber = 2;
+ inline const ::std::string& raw_hashes() const;
+ inline void set_raw_hashes(const ::std::string& value);
+ inline void set_raw_hashes(const char* value);
+ inline void set_raw_hashes(const void* value, size_t size);
+ inline ::std::string* mutable_raw_hashes();
+ inline ::std::string* release_raw_hashes();
+ inline void set_allocated_raw_hashes(::std::string* raw_hashes);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.RawHashes)
+ private:
+ inline void set_has_prefix_size();
+ inline void clear_has_prefix_size();
+ inline void set_has_raw_hashes();
+ inline void clear_has_raw_hashes();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* raw_hashes_;
+ ::google::protobuf::int32 prefix_size_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static RawHashes* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class RiceDeltaEncoding : public ::google::protobuf::MessageLite {
+ public:
+ RiceDeltaEncoding();
+ virtual ~RiceDeltaEncoding();
+
+ RiceDeltaEncoding(const RiceDeltaEncoding& from);
+
+ inline RiceDeltaEncoding& operator=(const RiceDeltaEncoding& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const RiceDeltaEncoding& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const RiceDeltaEncoding* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(RiceDeltaEncoding* other);
+
+ // implements Message ----------------------------------------------
+
+ RiceDeltaEncoding* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const RiceDeltaEncoding& from);
+ void MergeFrom(const RiceDeltaEncoding& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int64 first_value = 1;
+ inline bool has_first_value() const;
+ inline void clear_first_value();
+ static const int kFirstValueFieldNumber = 1;
+ inline ::google::protobuf::int64 first_value() const;
+ inline void set_first_value(::google::protobuf::int64 value);
+
+ // optional int32 rice_parameter = 2;
+ inline bool has_rice_parameter() const;
+ inline void clear_rice_parameter();
+ static const int kRiceParameterFieldNumber = 2;
+ inline ::google::protobuf::int32 rice_parameter() const;
+ inline void set_rice_parameter(::google::protobuf::int32 value);
+
+ // optional int32 num_entries = 3;
+ inline bool has_num_entries() const;
+ inline void clear_num_entries();
+ static const int kNumEntriesFieldNumber = 3;
+ inline ::google::protobuf::int32 num_entries() const;
+ inline void set_num_entries(::google::protobuf::int32 value);
+
+ // optional bytes encoded_data = 4;
+ inline bool has_encoded_data() const;
+ inline void clear_encoded_data();
+ static const int kEncodedDataFieldNumber = 4;
+ inline const ::std::string& encoded_data() const;
+ inline void set_encoded_data(const ::std::string& value);
+ inline void set_encoded_data(const char* value);
+ inline void set_encoded_data(const void* value, size_t size);
+ inline ::std::string* mutable_encoded_data();
+ inline ::std::string* release_encoded_data();
+ inline void set_allocated_encoded_data(::std::string* encoded_data);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.RiceDeltaEncoding)
+ private:
+ inline void set_has_first_value();
+ inline void clear_has_first_value();
+ inline void set_has_rice_parameter();
+ inline void clear_has_rice_parameter();
+ inline void set_has_num_entries();
+ inline void clear_has_num_entries();
+ inline void set_has_encoded_data();
+ inline void clear_has_encoded_data();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::int64 first_value_;
+ ::google::protobuf::int32 rice_parameter_;
+ ::google::protobuf::int32 num_entries_;
+ ::std::string* encoded_data_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static RiceDeltaEncoding* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatEntryMetadata_MetadataEntry : public ::google::protobuf::MessageLite {
+ public:
+ ThreatEntryMetadata_MetadataEntry();
+ virtual ~ThreatEntryMetadata_MetadataEntry();
+
+ ThreatEntryMetadata_MetadataEntry(const ThreatEntryMetadata_MetadataEntry& from);
+
+ inline ThreatEntryMetadata_MetadataEntry& operator=(const ThreatEntryMetadata_MetadataEntry& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatEntryMetadata_MetadataEntry& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatEntryMetadata_MetadataEntry* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatEntryMetadata_MetadataEntry* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatEntryMetadata_MetadataEntry* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatEntryMetadata_MetadataEntry& from);
+ void MergeFrom(const ThreatEntryMetadata_MetadataEntry& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional bytes key = 1;
+ inline bool has_key() const;
+ inline void clear_key();
+ static const int kKeyFieldNumber = 1;
+ inline const ::std::string& key() const;
+ inline void set_key(const ::std::string& value);
+ inline void set_key(const char* value);
+ inline void set_key(const void* value, size_t size);
+ inline ::std::string* mutable_key();
+ inline ::std::string* release_key();
+ inline void set_allocated_key(::std::string* key);
+
+ // optional bytes value = 2;
+ inline bool has_value() const;
+ inline void clear_value();
+ static const int kValueFieldNumber = 2;
+ inline const ::std::string& value() const;
+ inline void set_value(const ::std::string& value);
+ inline void set_value(const char* value);
+ inline void set_value(const void* value, size_t size);
+ inline ::std::string* mutable_value();
+ inline ::std::string* release_value();
+ inline void set_allocated_value(::std::string* value);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry)
+ private:
+ inline void set_has_key();
+ inline void clear_has_key();
+ inline void set_has_value();
+ inline void clear_has_value();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::std::string* key_;
+ ::std::string* value_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatEntryMetadata_MetadataEntry* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatEntryMetadata : public ::google::protobuf::MessageLite {
+ public:
+ ThreatEntryMetadata();
+ virtual ~ThreatEntryMetadata();
+
+ ThreatEntryMetadata(const ThreatEntryMetadata& from);
+
+ inline ThreatEntryMetadata& operator=(const ThreatEntryMetadata& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatEntryMetadata& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatEntryMetadata* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatEntryMetadata* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatEntryMetadata* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatEntryMetadata& from);
+ void MergeFrom(const ThreatEntryMetadata& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ typedef ThreatEntryMetadata_MetadataEntry MetadataEntry;
+
+ // accessors -------------------------------------------------------
+
+ // repeated .mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry entries = 1;
+ inline int entries_size() const;
+ inline void clear_entries();
+ static const int kEntriesFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry& entries(int index) const;
+ inline ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry* mutable_entries(int index);
+ inline ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry* add_entries();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry >&
+ entries() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry >*
+ mutable_entries();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatEntryMetadata)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry > entries_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatEntryMetadata* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ThreatListDescriptor : public ::google::protobuf::MessageLite {
+ public:
+ ThreatListDescriptor();
+ virtual ~ThreatListDescriptor();
+
+ ThreatListDescriptor(const ThreatListDescriptor& from);
+
+ inline ThreatListDescriptor& operator=(const ThreatListDescriptor& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ThreatListDescriptor& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ThreatListDescriptor* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ThreatListDescriptor* other);
+
+ // implements Message ----------------------------------------------
+
+ ThreatListDescriptor* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ThreatListDescriptor& from);
+ void MergeFrom(const ThreatListDescriptor& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+ inline bool has_threat_type() const;
+ inline void clear_threat_type();
+ static const int kThreatTypeFieldNumber = 1;
+ inline ::mozilla::safebrowsing::ThreatType threat_type() const;
+ inline void set_threat_type(::mozilla::safebrowsing::ThreatType value);
+
+ // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+ inline bool has_platform_type() const;
+ inline void clear_platform_type();
+ static const int kPlatformTypeFieldNumber = 2;
+ inline ::mozilla::safebrowsing::PlatformType platform_type() const;
+ inline void set_platform_type(::mozilla::safebrowsing::PlatformType value);
+
+ // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 3;
+ inline bool has_threat_entry_type() const;
+ inline void clear_threat_entry_type();
+ static const int kThreatEntryTypeFieldNumber = 3;
+ inline ::mozilla::safebrowsing::ThreatEntryType threat_entry_type() const;
+ inline void set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ThreatListDescriptor)
+ private:
+ inline void set_has_threat_type();
+ inline void clear_has_threat_type();
+ inline void set_has_platform_type();
+ inline void clear_has_platform_type();
+ inline void set_has_threat_entry_type();
+ inline void clear_has_threat_entry_type();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ int threat_type_;
+ int platform_type_;
+ int threat_entry_type_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ThreatListDescriptor* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class ListThreatListsResponse : public ::google::protobuf::MessageLite {
+ public:
+ ListThreatListsResponse();
+ virtual ~ListThreatListsResponse();
+
+ ListThreatListsResponse(const ListThreatListsResponse& from);
+
+ inline ListThreatListsResponse& operator=(const ListThreatListsResponse& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const ListThreatListsResponse& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const ListThreatListsResponse* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(ListThreatListsResponse* other);
+
+ // implements Message ----------------------------------------------
+
+ ListThreatListsResponse* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const ListThreatListsResponse& from);
+ void MergeFrom(const ListThreatListsResponse& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // repeated .mozilla.safebrowsing.ThreatListDescriptor threat_lists = 1;
+ inline int threat_lists_size() const;
+ inline void clear_threat_lists();
+ static const int kThreatListsFieldNumber = 1;
+ inline const ::mozilla::safebrowsing::ThreatListDescriptor& threat_lists(int index) const;
+ inline ::mozilla::safebrowsing::ThreatListDescriptor* mutable_threat_lists(int index);
+ inline ::mozilla::safebrowsing::ThreatListDescriptor* add_threat_lists();
+ inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatListDescriptor >&
+ threat_lists() const;
+ inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatListDescriptor >*
+ mutable_threat_lists();
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.ListThreatListsResponse)
+ private:
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatListDescriptor > threat_lists_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static ListThreatListsResponse* default_instance_;
+};
+// -------------------------------------------------------------------
+
+class Duration : public ::google::protobuf::MessageLite {
+ public:
+ Duration();
+ virtual ~Duration();
+
+ Duration(const Duration& from);
+
+ inline Duration& operator=(const Duration& from) {
+ CopyFrom(from);
+ return *this;
+ }
+
+ inline const ::std::string& unknown_fields() const {
+ return _unknown_fields_;
+ }
+
+ inline ::std::string* mutable_unknown_fields() {
+ return &_unknown_fields_;
+ }
+
+ static const Duration& default_instance();
+
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ // Returns the internal default instance pointer. This function can
+ // return NULL thus should not be used by the user. This is intended
+ // for Protobuf internal code. Please use default_instance() declared
+ // above instead.
+ static inline const Duration* internal_default_instance() {
+ return default_instance_;
+ }
+ #endif
+
+ void Swap(Duration* other);
+
+ // implements Message ----------------------------------------------
+
+ Duration* New() const;
+ void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+ void CopyFrom(const Duration& from);
+ void MergeFrom(const Duration& from);
+ void Clear();
+ bool IsInitialized() const;
+
+ int ByteSize() const;
+ bool MergePartialFromCodedStream(
+ ::google::protobuf::io::CodedInputStream* input);
+ void SerializeWithCachedSizes(
+ ::google::protobuf::io::CodedOutputStream* output) const;
+ void DiscardUnknownFields();
+ int GetCachedSize() const { return _cached_size_; }
+ private:
+ void SharedCtor();
+ void SharedDtor();
+ void SetCachedSize(int size) const;
+ public:
+ ::std::string GetTypeName() const;
+
+ // nested types ----------------------------------------------------
+
+ // accessors -------------------------------------------------------
+
+ // optional int64 seconds = 1;
+ inline bool has_seconds() const;
+ inline void clear_seconds();
+ static const int kSecondsFieldNumber = 1;
+ inline ::google::protobuf::int64 seconds() const;
+ inline void set_seconds(::google::protobuf::int64 value);
+
+ // optional int32 nanos = 2;
+ inline bool has_nanos() const;
+ inline void clear_nanos();
+ static const int kNanosFieldNumber = 2;
+ inline ::google::protobuf::int32 nanos() const;
+ inline void set_nanos(::google::protobuf::int32 value);
+
+ // @@protoc_insertion_point(class_scope:mozilla.safebrowsing.Duration)
+ private:
+ inline void set_has_seconds();
+ inline void clear_has_seconds();
+ inline void set_has_nanos();
+ inline void clear_has_nanos();
+
+ ::std::string _unknown_fields_;
+
+ ::google::protobuf::uint32 _has_bits_[1];
+ mutable int _cached_size_;
+ ::google::protobuf::int64 seconds_;
+ ::google::protobuf::int32 nanos_;
+ #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ friend void protobuf_AddDesc_safebrowsing_2eproto_impl();
+ #else
+ friend void protobuf_AddDesc_safebrowsing_2eproto();
+ #endif
+ friend void protobuf_AssignDesc_safebrowsing_2eproto();
+ friend void protobuf_ShutdownFile_safebrowsing_2eproto();
+
+ void InitAsDefaultInstance();
+ static Duration* default_instance_;
+};
+// ===================================================================
+
+
+// ===================================================================
+
+// ThreatInfo
+
+// repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+inline int ThreatInfo::threat_types_size() const {
+ return threat_types_.size();
+}
+inline void ThreatInfo::clear_threat_types() {
+ threat_types_.Clear();
+}
+inline ::mozilla::safebrowsing::ThreatType ThreatInfo::threat_types(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatInfo.threat_types)
+ return static_cast< ::mozilla::safebrowsing::ThreatType >(threat_types_.Get(index));
+}
+inline void ThreatInfo::set_threat_types(int index, ::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ threat_types_.Set(index, value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatInfo.threat_types)
+}
+inline void ThreatInfo::add_threat_types(::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ threat_types_.Add(value);
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ThreatInfo.threat_types)
+}
+inline const ::google::protobuf::RepeatedField<int>&
+ThreatInfo::threat_types() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ThreatInfo.threat_types)
+ return threat_types_;
+}
+inline ::google::protobuf::RepeatedField<int>*
+ThreatInfo::mutable_threat_types() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ThreatInfo.threat_types)
+ return &threat_types_;
+}
+
+// repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+inline int ThreatInfo::platform_types_size() const {
+ return platform_types_.size();
+}
+inline void ThreatInfo::clear_platform_types() {
+ platform_types_.Clear();
+}
+inline ::mozilla::safebrowsing::PlatformType ThreatInfo::platform_types(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatInfo.platform_types)
+ return static_cast< ::mozilla::safebrowsing::PlatformType >(platform_types_.Get(index));
+}
+inline void ThreatInfo::set_platform_types(int index, ::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ platform_types_.Set(index, value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatInfo.platform_types)
+}
+inline void ThreatInfo::add_platform_types(::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ platform_types_.Add(value);
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ThreatInfo.platform_types)
+}
+inline const ::google::protobuf::RepeatedField<int>&
+ThreatInfo::platform_types() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ThreatInfo.platform_types)
+ return platform_types_;
+}
+inline ::google::protobuf::RepeatedField<int>*
+ThreatInfo::mutable_platform_types() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ThreatInfo.platform_types)
+ return &platform_types_;
+}
+
+// repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+inline int ThreatInfo::threat_entry_types_size() const {
+ return threat_entry_types_.size();
+}
+inline void ThreatInfo::clear_threat_entry_types() {
+ threat_entry_types_.Clear();
+}
+inline ::mozilla::safebrowsing::ThreatEntryType ThreatInfo::threat_entry_types(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatInfo.threat_entry_types)
+ return static_cast< ::mozilla::safebrowsing::ThreatEntryType >(threat_entry_types_.Get(index));
+}
+inline void ThreatInfo::set_threat_entry_types(int index, ::mozilla::safebrowsing::ThreatEntryType value) {
+ assert(::mozilla::safebrowsing::ThreatEntryType_IsValid(value));
+ threat_entry_types_.Set(index, value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatInfo.threat_entry_types)
+}
+inline void ThreatInfo::add_threat_entry_types(::mozilla::safebrowsing::ThreatEntryType value) {
+ assert(::mozilla::safebrowsing::ThreatEntryType_IsValid(value));
+ threat_entry_types_.Add(value);
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ThreatInfo.threat_entry_types)
+}
+inline const ::google::protobuf::RepeatedField<int>&
+ThreatInfo::threat_entry_types() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ThreatInfo.threat_entry_types)
+ return threat_entry_types_;
+}
+inline ::google::protobuf::RepeatedField<int>*
+ThreatInfo::mutable_threat_entry_types() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ThreatInfo.threat_entry_types)
+ return &threat_entry_types_;
+}
+
+// repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+inline int ThreatInfo::threat_entries_size() const {
+ return threat_entries_.size();
+}
+inline void ThreatInfo::clear_threat_entries() {
+ threat_entries_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatEntry& ThreatInfo::threat_entries(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatInfo.threat_entries)
+ return threat_entries_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntry* ThreatInfo::mutable_threat_entries(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatInfo.threat_entries)
+ return threat_entries_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntry* ThreatInfo::add_threat_entries() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ThreatInfo.threat_entries)
+ return threat_entries_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntry >&
+ThreatInfo::threat_entries() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ThreatInfo.threat_entries)
+ return threat_entries_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntry >*
+ThreatInfo::mutable_threat_entries() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ThreatInfo.threat_entries)
+ return &threat_entries_;
+}
+
+// -------------------------------------------------------------------
+
+// ThreatMatch
+
+// optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+inline bool ThreatMatch::has_threat_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatMatch::set_has_threat_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatMatch::clear_has_threat_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatMatch::clear_threat_type() {
+ threat_type_ = 0;
+ clear_has_threat_type();
+}
+inline ::mozilla::safebrowsing::ThreatType ThreatMatch::threat_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatMatch.threat_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatType >(threat_type_);
+}
+inline void ThreatMatch::set_threat_type(::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ set_has_threat_type();
+ threat_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatMatch.threat_type)
+}
+
+// optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+inline bool ThreatMatch::has_platform_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatMatch::set_has_platform_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatMatch::clear_has_platform_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatMatch::clear_platform_type() {
+ platform_type_ = 0;
+ clear_has_platform_type();
+}
+inline ::mozilla::safebrowsing::PlatformType ThreatMatch::platform_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatMatch.platform_type)
+ return static_cast< ::mozilla::safebrowsing::PlatformType >(platform_type_);
+}
+inline void ThreatMatch::set_platform_type(::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ set_has_platform_type();
+ platform_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatMatch.platform_type)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+inline bool ThreatMatch::has_threat_entry_type() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ThreatMatch::set_has_threat_entry_type() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ThreatMatch::clear_has_threat_entry_type() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ThreatMatch::clear_threat_entry_type() {
+ threat_entry_type_ = 0;
+ clear_has_threat_entry_type();
+}
+inline ::mozilla::safebrowsing::ThreatEntryType ThreatMatch::threat_entry_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatMatch.threat_entry_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatEntryType >(threat_entry_type_);
+}
+inline void ThreatMatch::set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value) {
+ assert(::mozilla::safebrowsing::ThreatEntryType_IsValid(value));
+ set_has_threat_entry_type();
+ threat_entry_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatMatch.threat_entry_type)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+inline bool ThreatMatch::has_threat() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ThreatMatch::set_has_threat() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ThreatMatch::clear_has_threat() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ThreatMatch::clear_threat() {
+ if (threat_ != NULL) threat_->::mozilla::safebrowsing::ThreatEntry::Clear();
+ clear_has_threat();
+}
+inline const ::mozilla::safebrowsing::ThreatEntry& ThreatMatch::threat() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatMatch.threat)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return threat_ != NULL ? *threat_ : *default_instance().threat_;
+#else
+ return threat_ != NULL ? *threat_ : *default_instance_->threat_;
+#endif
+}
+inline ::mozilla::safebrowsing::ThreatEntry* ThreatMatch::mutable_threat() {
+ set_has_threat();
+ if (threat_ == NULL) threat_ = new ::mozilla::safebrowsing::ThreatEntry;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatMatch.threat)
+ return threat_;
+}
+inline ::mozilla::safebrowsing::ThreatEntry* ThreatMatch::release_threat() {
+ clear_has_threat();
+ ::mozilla::safebrowsing::ThreatEntry* temp = threat_;
+ threat_ = NULL;
+ return temp;
+}
+inline void ThreatMatch::set_allocated_threat(::mozilla::safebrowsing::ThreatEntry* threat) {
+ delete threat_;
+ threat_ = threat;
+ if (threat) {
+ set_has_threat();
+ } else {
+ clear_has_threat();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatMatch.threat)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+inline bool ThreatMatch::has_threat_entry_metadata() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ThreatMatch::set_has_threat_entry_metadata() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ThreatMatch::clear_has_threat_entry_metadata() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ThreatMatch::clear_threat_entry_metadata() {
+ if (threat_entry_metadata_ != NULL) threat_entry_metadata_->::mozilla::safebrowsing::ThreatEntryMetadata::Clear();
+ clear_has_threat_entry_metadata();
+}
+inline const ::mozilla::safebrowsing::ThreatEntryMetadata& ThreatMatch::threat_entry_metadata() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatMatch.threat_entry_metadata)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return threat_entry_metadata_ != NULL ? *threat_entry_metadata_ : *default_instance().threat_entry_metadata_;
+#else
+ return threat_entry_metadata_ != NULL ? *threat_entry_metadata_ : *default_instance_->threat_entry_metadata_;
+#endif
+}
+inline ::mozilla::safebrowsing::ThreatEntryMetadata* ThreatMatch::mutable_threat_entry_metadata() {
+ set_has_threat_entry_metadata();
+ if (threat_entry_metadata_ == NULL) threat_entry_metadata_ = new ::mozilla::safebrowsing::ThreatEntryMetadata;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatMatch.threat_entry_metadata)
+ return threat_entry_metadata_;
+}
+inline ::mozilla::safebrowsing::ThreatEntryMetadata* ThreatMatch::release_threat_entry_metadata() {
+ clear_has_threat_entry_metadata();
+ ::mozilla::safebrowsing::ThreatEntryMetadata* temp = threat_entry_metadata_;
+ threat_entry_metadata_ = NULL;
+ return temp;
+}
+inline void ThreatMatch::set_allocated_threat_entry_metadata(::mozilla::safebrowsing::ThreatEntryMetadata* threat_entry_metadata) {
+ delete threat_entry_metadata_;
+ threat_entry_metadata_ = threat_entry_metadata;
+ if (threat_entry_metadata) {
+ set_has_threat_entry_metadata();
+ } else {
+ clear_has_threat_entry_metadata();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatMatch.threat_entry_metadata)
+}
+
+// optional .mozilla.safebrowsing.Duration cache_duration = 5;
+inline bool ThreatMatch::has_cache_duration() const {
+ return (_has_bits_[0] & 0x00000020u) != 0;
+}
+inline void ThreatMatch::set_has_cache_duration() {
+ _has_bits_[0] |= 0x00000020u;
+}
+inline void ThreatMatch::clear_has_cache_duration() {
+ _has_bits_[0] &= ~0x00000020u;
+}
+inline void ThreatMatch::clear_cache_duration() {
+ if (cache_duration_ != NULL) cache_duration_->::mozilla::safebrowsing::Duration::Clear();
+ clear_has_cache_duration();
+}
+inline const ::mozilla::safebrowsing::Duration& ThreatMatch::cache_duration() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatMatch.cache_duration)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return cache_duration_ != NULL ? *cache_duration_ : *default_instance().cache_duration_;
+#else
+ return cache_duration_ != NULL ? *cache_duration_ : *default_instance_->cache_duration_;
+#endif
+}
+inline ::mozilla::safebrowsing::Duration* ThreatMatch::mutable_cache_duration() {
+ set_has_cache_duration();
+ if (cache_duration_ == NULL) cache_duration_ = new ::mozilla::safebrowsing::Duration;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatMatch.cache_duration)
+ return cache_duration_;
+}
+inline ::mozilla::safebrowsing::Duration* ThreatMatch::release_cache_duration() {
+ clear_has_cache_duration();
+ ::mozilla::safebrowsing::Duration* temp = cache_duration_;
+ cache_duration_ = NULL;
+ return temp;
+}
+inline void ThreatMatch::set_allocated_cache_duration(::mozilla::safebrowsing::Duration* cache_duration) {
+ delete cache_duration_;
+ cache_duration_ = cache_duration;
+ if (cache_duration) {
+ set_has_cache_duration();
+ } else {
+ clear_has_cache_duration();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatMatch.cache_duration)
+}
+
+// -------------------------------------------------------------------
+
+// FindThreatMatchesRequest
+
+// optional .mozilla.safebrowsing.ClientInfo client = 1;
+inline bool FindThreatMatchesRequest::has_client() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FindThreatMatchesRequest::set_has_client() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FindThreatMatchesRequest::clear_has_client() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FindThreatMatchesRequest::clear_client() {
+ if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+ clear_has_client();
+}
+inline const ::mozilla::safebrowsing::ClientInfo& FindThreatMatchesRequest::client() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindThreatMatchesRequest.client)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return client_ != NULL ? *client_ : *default_instance().client_;
+#else
+ return client_ != NULL ? *client_ : *default_instance_->client_;
+#endif
+}
+inline ::mozilla::safebrowsing::ClientInfo* FindThreatMatchesRequest::mutable_client() {
+ set_has_client();
+ if (client_ == NULL) client_ = new ::mozilla::safebrowsing::ClientInfo;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindThreatMatchesRequest.client)
+ return client_;
+}
+inline ::mozilla::safebrowsing::ClientInfo* FindThreatMatchesRequest::release_client() {
+ clear_has_client();
+ ::mozilla::safebrowsing::ClientInfo* temp = client_;
+ client_ = NULL;
+ return temp;
+}
+inline void FindThreatMatchesRequest::set_allocated_client(::mozilla::safebrowsing::ClientInfo* client) {
+ delete client_;
+ client_ = client;
+ if (client) {
+ set_has_client();
+ } else {
+ clear_has_client();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FindThreatMatchesRequest.client)
+}
+
+// optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+inline bool FindThreatMatchesRequest::has_threat_info() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FindThreatMatchesRequest::set_has_threat_info() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FindThreatMatchesRequest::clear_has_threat_info() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FindThreatMatchesRequest::clear_threat_info() {
+ if (threat_info_ != NULL) threat_info_->::mozilla::safebrowsing::ThreatInfo::Clear();
+ clear_has_threat_info();
+}
+inline const ::mozilla::safebrowsing::ThreatInfo& FindThreatMatchesRequest::threat_info() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindThreatMatchesRequest.threat_info)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return threat_info_ != NULL ? *threat_info_ : *default_instance().threat_info_;
+#else
+ return threat_info_ != NULL ? *threat_info_ : *default_instance_->threat_info_;
+#endif
+}
+inline ::mozilla::safebrowsing::ThreatInfo* FindThreatMatchesRequest::mutable_threat_info() {
+ set_has_threat_info();
+ if (threat_info_ == NULL) threat_info_ = new ::mozilla::safebrowsing::ThreatInfo;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindThreatMatchesRequest.threat_info)
+ return threat_info_;
+}
+inline ::mozilla::safebrowsing::ThreatInfo* FindThreatMatchesRequest::release_threat_info() {
+ clear_has_threat_info();
+ ::mozilla::safebrowsing::ThreatInfo* temp = threat_info_;
+ threat_info_ = NULL;
+ return temp;
+}
+inline void FindThreatMatchesRequest::set_allocated_threat_info(::mozilla::safebrowsing::ThreatInfo* threat_info) {
+ delete threat_info_;
+ threat_info_ = threat_info;
+ if (threat_info) {
+ set_has_threat_info();
+ } else {
+ clear_has_threat_info();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FindThreatMatchesRequest.threat_info)
+}
+
+// -------------------------------------------------------------------
+
+// FindThreatMatchesResponse
+
+// repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+inline int FindThreatMatchesResponse::matches_size() const {
+ return matches_.size();
+}
+inline void FindThreatMatchesResponse::clear_matches() {
+ matches_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatMatch& FindThreatMatchesResponse::matches(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindThreatMatchesResponse.matches)
+ return matches_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatMatch* FindThreatMatchesResponse::mutable_matches(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindThreatMatchesResponse.matches)
+ return matches_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatMatch* FindThreatMatchesResponse::add_matches() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FindThreatMatchesResponse.matches)
+ return matches_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >&
+FindThreatMatchesResponse::matches() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FindThreatMatchesResponse.matches)
+ return matches_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >*
+FindThreatMatchesResponse::mutable_matches() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FindThreatMatchesResponse.matches)
+ return &matches_;
+}
+
+// -------------------------------------------------------------------
+
+// FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints
+
+// optional int32 max_update_entries = 1;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::has_max_update_entries() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_has_max_update_entries() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_has_max_update_entries() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_max_update_entries() {
+ max_update_entries_ = 0;
+ clear_has_max_update_entries();
+}
+inline ::google::protobuf::int32 FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::max_update_entries() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.max_update_entries)
+ return max_update_entries_;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_max_update_entries(::google::protobuf::int32 value) {
+ set_has_max_update_entries();
+ max_update_entries_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.max_update_entries)
+}
+
+// optional int32 max_database_entries = 2;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::has_max_database_entries() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_has_max_database_entries() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_has_max_database_entries() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_max_database_entries() {
+ max_database_entries_ = 0;
+ clear_has_max_database_entries();
+}
+inline ::google::protobuf::int32 FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::max_database_entries() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.max_database_entries)
+ return max_database_entries_;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_max_database_entries(::google::protobuf::int32 value) {
+ set_has_max_database_entries();
+ max_database_entries_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.max_database_entries)
+}
+
+// optional string region = 3;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::has_region() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_has_region() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_has_region() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_region() {
+ if (region_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ region_->clear();
+ }
+ clear_has_region();
+}
+inline const ::std::string& FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::region() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.region)
+ return *region_;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_region(const ::std::string& value) {
+ set_has_region();
+ if (region_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ region_ = new ::std::string;
+ }
+ region_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.region)
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_region(const char* value) {
+ set_has_region();
+ if (region_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ region_ = new ::std::string;
+ }
+ region_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.region)
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_region(const char* value, size_t size) {
+ set_has_region();
+ if (region_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ region_ = new ::std::string;
+ }
+ region_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.region)
+}
+inline ::std::string* FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::mutable_region() {
+ set_has_region();
+ if (region_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ region_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.region)
+ return region_;
+}
+inline ::std::string* FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::release_region() {
+ clear_has_region();
+ if (region_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = region_;
+ region_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_allocated_region(::std::string* region) {
+ if (region_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete region_;
+ }
+ if (region) {
+ set_has_region();
+ region_ = region;
+ } else {
+ clear_has_region();
+ region_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.region)
+}
+
+// repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+inline int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::supported_compressions_size() const {
+ return supported_compressions_.size();
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::clear_supported_compressions() {
+ supported_compressions_.Clear();
+}
+inline ::mozilla::safebrowsing::CompressionType FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::supported_compressions(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.supported_compressions)
+ return static_cast< ::mozilla::safebrowsing::CompressionType >(supported_compressions_.Get(index));
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::set_supported_compressions(int index, ::mozilla::safebrowsing::CompressionType value) {
+ assert(::mozilla::safebrowsing::CompressionType_IsValid(value));
+ supported_compressions_.Set(index, value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.supported_compressions)
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::add_supported_compressions(::mozilla::safebrowsing::CompressionType value) {
+ assert(::mozilla::safebrowsing::CompressionType_IsValid(value));
+ supported_compressions_.Add(value);
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.supported_compressions)
+}
+inline const ::google::protobuf::RepeatedField<int>&
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::supported_compressions() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.supported_compressions)
+ return supported_compressions_;
+}
+inline ::google::protobuf::RepeatedField<int>*
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::mutable_supported_compressions() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints.supported_compressions)
+ return &supported_compressions_;
+}
+
+// -------------------------------------------------------------------
+
+// FetchThreatListUpdatesRequest_ListUpdateRequest
+
+// optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest::has_threat_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_has_threat_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_has_threat_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_threat_type() {
+ threat_type_ = 0;
+ clear_has_threat_type();
+}
+inline ::mozilla::safebrowsing::ThreatType FetchThreatListUpdatesRequest_ListUpdateRequest::threat_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.threat_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatType >(threat_type_);
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_threat_type(::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ set_has_threat_type();
+ threat_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.threat_type)
+}
+
+// optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest::has_platform_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_has_platform_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_has_platform_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_platform_type() {
+ platform_type_ = 0;
+ clear_has_platform_type();
+}
+inline ::mozilla::safebrowsing::PlatformType FetchThreatListUpdatesRequest_ListUpdateRequest::platform_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.platform_type)
+ return static_cast< ::mozilla::safebrowsing::PlatformType >(platform_type_);
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_platform_type(::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ set_has_platform_type();
+ platform_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.platform_type)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest::has_threat_entry_type() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_has_threat_entry_type() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_has_threat_entry_type() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_threat_entry_type() {
+ threat_entry_type_ = 0;
+ clear_has_threat_entry_type();
+}
+inline ::mozilla::safebrowsing::ThreatEntryType FetchThreatListUpdatesRequest_ListUpdateRequest::threat_entry_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.threat_entry_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatEntryType >(threat_entry_type_);
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value) {
+ assert(::mozilla::safebrowsing::ThreatEntryType_IsValid(value));
+ set_has_threat_entry_type();
+ threat_entry_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.threat_entry_type)
+}
+
+// optional bytes state = 3;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest::has_state() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_has_state() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_has_state() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_state() {
+ if (state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ state_->clear();
+ }
+ clear_has_state();
+}
+inline const ::std::string& FetchThreatListUpdatesRequest_ListUpdateRequest::state() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.state)
+ return *state_;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_state(const ::std::string& value) {
+ set_has_state();
+ if (state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ state_ = new ::std::string;
+ }
+ state_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.state)
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_state(const char* value) {
+ set_has_state();
+ if (state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ state_ = new ::std::string;
+ }
+ state_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.state)
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_state(const void* value, size_t size) {
+ set_has_state();
+ if (state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ state_ = new ::std::string;
+ }
+ state_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.state)
+}
+inline ::std::string* FetchThreatListUpdatesRequest_ListUpdateRequest::mutable_state() {
+ set_has_state();
+ if (state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ state_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.state)
+ return state_;
+}
+inline ::std::string* FetchThreatListUpdatesRequest_ListUpdateRequest::release_state() {
+ clear_has_state();
+ if (state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = state_;
+ state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_allocated_state(::std::string* state) {
+ if (state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete state_;
+ }
+ if (state) {
+ set_has_state();
+ state_ = state;
+ } else {
+ clear_has_state();
+ state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.state)
+}
+
+// optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+inline bool FetchThreatListUpdatesRequest_ListUpdateRequest::has_constraints() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_has_constraints() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_has_constraints() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::clear_constraints() {
+ if (constraints_ != NULL) constraints_->::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Clear();
+ clear_has_constraints();
+}
+inline const ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& FetchThreatListUpdatesRequest_ListUpdateRequest::constraints() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.constraints)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return constraints_ != NULL ? *constraints_ : *default_instance().constraints_;
+#else
+ return constraints_ != NULL ? *constraints_ : *default_instance_->constraints_;
+#endif
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* FetchThreatListUpdatesRequest_ListUpdateRequest::mutable_constraints() {
+ set_has_constraints();
+ if (constraints_ == NULL) constraints_ = new ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.constraints)
+ return constraints_;
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* FetchThreatListUpdatesRequest_ListUpdateRequest::release_constraints() {
+ clear_has_constraints();
+ ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* temp = constraints_;
+ constraints_ = NULL;
+ return temp;
+}
+inline void FetchThreatListUpdatesRequest_ListUpdateRequest::set_allocated_constraints(::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* constraints) {
+ delete constraints_;
+ constraints_ = constraints;
+ if (constraints) {
+ set_has_constraints();
+ } else {
+ clear_has_constraints();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.constraints)
+}
+
+// -------------------------------------------------------------------
+
+// FetchThreatListUpdatesRequest
+
+// optional .mozilla.safebrowsing.ClientInfo client = 1;
+inline bool FetchThreatListUpdatesRequest::has_client() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FetchThreatListUpdatesRequest::set_has_client() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FetchThreatListUpdatesRequest::clear_has_client() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FetchThreatListUpdatesRequest::clear_client() {
+ if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+ clear_has_client();
+}
+inline const ::mozilla::safebrowsing::ClientInfo& FetchThreatListUpdatesRequest::client() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.client)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return client_ != NULL ? *client_ : *default_instance().client_;
+#else
+ return client_ != NULL ? *client_ : *default_instance_->client_;
+#endif
+}
+inline ::mozilla::safebrowsing::ClientInfo* FetchThreatListUpdatesRequest::mutable_client() {
+ set_has_client();
+ if (client_ == NULL) client_ = new ::mozilla::safebrowsing::ClientInfo;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesRequest.client)
+ return client_;
+}
+inline ::mozilla::safebrowsing::ClientInfo* FetchThreatListUpdatesRequest::release_client() {
+ clear_has_client();
+ ::mozilla::safebrowsing::ClientInfo* temp = client_;
+ client_ = NULL;
+ return temp;
+}
+inline void FetchThreatListUpdatesRequest::set_allocated_client(::mozilla::safebrowsing::ClientInfo* client) {
+ delete client_;
+ client_ = client;
+ if (client) {
+ set_has_client();
+ } else {
+ clear_has_client();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesRequest.client)
+}
+
+// repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+inline int FetchThreatListUpdatesRequest::list_update_requests_size() const {
+ return list_update_requests_.size();
+}
+inline void FetchThreatListUpdatesRequest::clear_list_update_requests() {
+ list_update_requests_.Clear();
+}
+inline const ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest& FetchThreatListUpdatesRequest::list_update_requests(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesRequest.list_update_requests)
+ return list_update_requests_.Get(index);
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest* FetchThreatListUpdatesRequest::mutable_list_update_requests(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesRequest.list_update_requests)
+ return list_update_requests_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest* FetchThreatListUpdatesRequest::add_list_update_requests() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FetchThreatListUpdatesRequest.list_update_requests)
+ return list_update_requests_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest >&
+FetchThreatListUpdatesRequest::list_update_requests() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FetchThreatListUpdatesRequest.list_update_requests)
+ return list_update_requests_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest >*
+FetchThreatListUpdatesRequest::mutable_list_update_requests() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FetchThreatListUpdatesRequest.list_update_requests)
+ return &list_update_requests_;
+}
+
+// -------------------------------------------------------------------
+
+// FetchThreatListUpdatesResponse_ListUpdateResponse
+
+// optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+inline bool FetchThreatListUpdatesResponse_ListUpdateResponse::has_threat_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_has_threat_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_has_threat_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_threat_type() {
+ threat_type_ = 0;
+ clear_has_threat_type();
+}
+inline ::mozilla::safebrowsing::ThreatType FetchThreatListUpdatesResponse_ListUpdateResponse::threat_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.threat_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatType >(threat_type_);
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_threat_type(::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ set_has_threat_type();
+ threat_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.threat_type)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+inline bool FetchThreatListUpdatesResponse_ListUpdateResponse::has_threat_entry_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_has_threat_entry_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_has_threat_entry_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_threat_entry_type() {
+ threat_entry_type_ = 0;
+ clear_has_threat_entry_type();
+}
+inline ::mozilla::safebrowsing::ThreatEntryType FetchThreatListUpdatesResponse_ListUpdateResponse::threat_entry_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.threat_entry_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatEntryType >(threat_entry_type_);
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value) {
+ assert(::mozilla::safebrowsing::ThreatEntryType_IsValid(value));
+ set_has_threat_entry_type();
+ threat_entry_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.threat_entry_type)
+}
+
+// optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+inline bool FetchThreatListUpdatesResponse_ListUpdateResponse::has_platform_type() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_has_platform_type() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_has_platform_type() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_platform_type() {
+ platform_type_ = 0;
+ clear_has_platform_type();
+}
+inline ::mozilla::safebrowsing::PlatformType FetchThreatListUpdatesResponse_ListUpdateResponse::platform_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.platform_type)
+ return static_cast< ::mozilla::safebrowsing::PlatformType >(platform_type_);
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_platform_type(::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ set_has_platform_type();
+ platform_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.platform_type)
+}
+
+// optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+inline bool FetchThreatListUpdatesResponse_ListUpdateResponse::has_response_type() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_has_response_type() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_has_response_type() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_response_type() {
+ response_type_ = 0;
+ clear_has_response_type();
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::response_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.response_type)
+ return static_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType >(response_type_);
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_response_type(::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType value) {
+ assert(::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(value));
+ set_has_response_type();
+ response_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.response_type)
+}
+
+// repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+inline int FetchThreatListUpdatesResponse_ListUpdateResponse::additions_size() const {
+ return additions_.size();
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_additions() {
+ additions_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatEntrySet& FetchThreatListUpdatesResponse_ListUpdateResponse::additions(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.additions)
+ return additions_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntrySet* FetchThreatListUpdatesResponse_ListUpdateResponse::mutable_additions(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.additions)
+ return additions_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntrySet* FetchThreatListUpdatesResponse_ListUpdateResponse::add_additions() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.additions)
+ return additions_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >&
+FetchThreatListUpdatesResponse_ListUpdateResponse::additions() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.additions)
+ return additions_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >*
+FetchThreatListUpdatesResponse_ListUpdateResponse::mutable_additions() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.additions)
+ return &additions_;
+}
+
+// repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+inline int FetchThreatListUpdatesResponse_ListUpdateResponse::removals_size() const {
+ return removals_.size();
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_removals() {
+ removals_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatEntrySet& FetchThreatListUpdatesResponse_ListUpdateResponse::removals(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.removals)
+ return removals_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntrySet* FetchThreatListUpdatesResponse_ListUpdateResponse::mutable_removals(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.removals)
+ return removals_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntrySet* FetchThreatListUpdatesResponse_ListUpdateResponse::add_removals() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.removals)
+ return removals_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >&
+FetchThreatListUpdatesResponse_ListUpdateResponse::removals() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.removals)
+ return removals_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntrySet >*
+FetchThreatListUpdatesResponse_ListUpdateResponse::mutable_removals() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.removals)
+ return &removals_;
+}
+
+// optional bytes new_client_state = 7;
+inline bool FetchThreatListUpdatesResponse_ListUpdateResponse::has_new_client_state() const {
+ return (_has_bits_[0] & 0x00000040u) != 0;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_has_new_client_state() {
+ _has_bits_[0] |= 0x00000040u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_has_new_client_state() {
+ _has_bits_[0] &= ~0x00000040u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_new_client_state() {
+ if (new_client_state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ new_client_state_->clear();
+ }
+ clear_has_new_client_state();
+}
+inline const ::std::string& FetchThreatListUpdatesResponse_ListUpdateResponse::new_client_state() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.new_client_state)
+ return *new_client_state_;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_new_client_state(const ::std::string& value) {
+ set_has_new_client_state();
+ if (new_client_state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ new_client_state_ = new ::std::string;
+ }
+ new_client_state_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.new_client_state)
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_new_client_state(const char* value) {
+ set_has_new_client_state();
+ if (new_client_state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ new_client_state_ = new ::std::string;
+ }
+ new_client_state_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.new_client_state)
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_new_client_state(const void* value, size_t size) {
+ set_has_new_client_state();
+ if (new_client_state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ new_client_state_ = new ::std::string;
+ }
+ new_client_state_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.new_client_state)
+}
+inline ::std::string* FetchThreatListUpdatesResponse_ListUpdateResponse::mutable_new_client_state() {
+ set_has_new_client_state();
+ if (new_client_state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ new_client_state_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.new_client_state)
+ return new_client_state_;
+}
+inline ::std::string* FetchThreatListUpdatesResponse_ListUpdateResponse::release_new_client_state() {
+ clear_has_new_client_state();
+ if (new_client_state_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = new_client_state_;
+ new_client_state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_allocated_new_client_state(::std::string* new_client_state) {
+ if (new_client_state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete new_client_state_;
+ }
+ if (new_client_state) {
+ set_has_new_client_state();
+ new_client_state_ = new_client_state;
+ } else {
+ clear_has_new_client_state();
+ new_client_state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.new_client_state)
+}
+
+// optional .mozilla.safebrowsing.Checksum checksum = 8;
+inline bool FetchThreatListUpdatesResponse_ListUpdateResponse::has_checksum() const {
+ return (_has_bits_[0] & 0x00000080u) != 0;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_has_checksum() {
+ _has_bits_[0] |= 0x00000080u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_has_checksum() {
+ _has_bits_[0] &= ~0x00000080u;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::clear_checksum() {
+ if (checksum_ != NULL) checksum_->::mozilla::safebrowsing::Checksum::Clear();
+ clear_has_checksum();
+}
+inline const ::mozilla::safebrowsing::Checksum& FetchThreatListUpdatesResponse_ListUpdateResponse::checksum() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.checksum)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return checksum_ != NULL ? *checksum_ : *default_instance().checksum_;
+#else
+ return checksum_ != NULL ? *checksum_ : *default_instance_->checksum_;
+#endif
+}
+inline ::mozilla::safebrowsing::Checksum* FetchThreatListUpdatesResponse_ListUpdateResponse::mutable_checksum() {
+ set_has_checksum();
+ if (checksum_ == NULL) checksum_ = new ::mozilla::safebrowsing::Checksum;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.checksum)
+ return checksum_;
+}
+inline ::mozilla::safebrowsing::Checksum* FetchThreatListUpdatesResponse_ListUpdateResponse::release_checksum() {
+ clear_has_checksum();
+ ::mozilla::safebrowsing::Checksum* temp = checksum_;
+ checksum_ = NULL;
+ return temp;
+}
+inline void FetchThreatListUpdatesResponse_ListUpdateResponse::set_allocated_checksum(::mozilla::safebrowsing::Checksum* checksum) {
+ delete checksum_;
+ checksum_ = checksum;
+ if (checksum) {
+ set_has_checksum();
+ } else {
+ clear_has_checksum();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.checksum)
+}
+
+// -------------------------------------------------------------------
+
+// FetchThreatListUpdatesResponse
+
+// repeated .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse list_update_responses = 1;
+inline int FetchThreatListUpdatesResponse::list_update_responses_size() const {
+ return list_update_responses_.size();
+}
+inline void FetchThreatListUpdatesResponse::clear_list_update_responses() {
+ list_update_responses_.Clear();
+}
+inline const ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse& FetchThreatListUpdatesResponse::list_update_responses(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.list_update_responses)
+ return list_update_responses_.Get(index);
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse* FetchThreatListUpdatesResponse::mutable_list_update_responses(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesResponse.list_update_responses)
+ return list_update_responses_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse* FetchThreatListUpdatesResponse::add_list_update_responses() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FetchThreatListUpdatesResponse.list_update_responses)
+ return list_update_responses_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse >&
+FetchThreatListUpdatesResponse::list_update_responses() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FetchThreatListUpdatesResponse.list_update_responses)
+ return list_update_responses_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse >*
+FetchThreatListUpdatesResponse::mutable_list_update_responses() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FetchThreatListUpdatesResponse.list_update_responses)
+ return &list_update_responses_;
+}
+
+// optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+inline bool FetchThreatListUpdatesResponse::has_minimum_wait_duration() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FetchThreatListUpdatesResponse::set_has_minimum_wait_duration() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FetchThreatListUpdatesResponse::clear_has_minimum_wait_duration() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FetchThreatListUpdatesResponse::clear_minimum_wait_duration() {
+ if (minimum_wait_duration_ != NULL) minimum_wait_duration_->::mozilla::safebrowsing::Duration::Clear();
+ clear_has_minimum_wait_duration();
+}
+inline const ::mozilla::safebrowsing::Duration& FetchThreatListUpdatesResponse::minimum_wait_duration() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FetchThreatListUpdatesResponse.minimum_wait_duration)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return minimum_wait_duration_ != NULL ? *minimum_wait_duration_ : *default_instance().minimum_wait_duration_;
+#else
+ return minimum_wait_duration_ != NULL ? *minimum_wait_duration_ : *default_instance_->minimum_wait_duration_;
+#endif
+}
+inline ::mozilla::safebrowsing::Duration* FetchThreatListUpdatesResponse::mutable_minimum_wait_duration() {
+ set_has_minimum_wait_duration();
+ if (minimum_wait_duration_ == NULL) minimum_wait_duration_ = new ::mozilla::safebrowsing::Duration;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FetchThreatListUpdatesResponse.minimum_wait_duration)
+ return minimum_wait_duration_;
+}
+inline ::mozilla::safebrowsing::Duration* FetchThreatListUpdatesResponse::release_minimum_wait_duration() {
+ clear_has_minimum_wait_duration();
+ ::mozilla::safebrowsing::Duration* temp = minimum_wait_duration_;
+ minimum_wait_duration_ = NULL;
+ return temp;
+}
+inline void FetchThreatListUpdatesResponse::set_allocated_minimum_wait_duration(::mozilla::safebrowsing::Duration* minimum_wait_duration) {
+ delete minimum_wait_duration_;
+ minimum_wait_duration_ = minimum_wait_duration;
+ if (minimum_wait_duration) {
+ set_has_minimum_wait_duration();
+ } else {
+ clear_has_minimum_wait_duration();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FetchThreatListUpdatesResponse.minimum_wait_duration)
+}
+
+// -------------------------------------------------------------------
+
+// FindFullHashesRequest
+
+// optional .mozilla.safebrowsing.ClientInfo client = 1;
+inline bool FindFullHashesRequest::has_client() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void FindFullHashesRequest::set_has_client() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void FindFullHashesRequest::clear_has_client() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void FindFullHashesRequest::clear_client() {
+ if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+ clear_has_client();
+}
+inline const ::mozilla::safebrowsing::ClientInfo& FindFullHashesRequest::client() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindFullHashesRequest.client)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return client_ != NULL ? *client_ : *default_instance().client_;
+#else
+ return client_ != NULL ? *client_ : *default_instance_->client_;
+#endif
+}
+inline ::mozilla::safebrowsing::ClientInfo* FindFullHashesRequest::mutable_client() {
+ set_has_client();
+ if (client_ == NULL) client_ = new ::mozilla::safebrowsing::ClientInfo;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindFullHashesRequest.client)
+ return client_;
+}
+inline ::mozilla::safebrowsing::ClientInfo* FindFullHashesRequest::release_client() {
+ clear_has_client();
+ ::mozilla::safebrowsing::ClientInfo* temp = client_;
+ client_ = NULL;
+ return temp;
+}
+inline void FindFullHashesRequest::set_allocated_client(::mozilla::safebrowsing::ClientInfo* client) {
+ delete client_;
+ client_ = client;
+ if (client) {
+ set_has_client();
+ } else {
+ clear_has_client();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FindFullHashesRequest.client)
+}
+
+// repeated bytes client_states = 2;
+inline int FindFullHashesRequest::client_states_size() const {
+ return client_states_.size();
+}
+inline void FindFullHashesRequest::clear_client_states() {
+ client_states_.Clear();
+}
+inline const ::std::string& FindFullHashesRequest::client_states(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+ return client_states_.Get(index);
+}
+inline ::std::string* FindFullHashesRequest::mutable_client_states(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+ return client_states_.Mutable(index);
+}
+inline void FindFullHashesRequest::set_client_states(int index, const ::std::string& value) {
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+ client_states_.Mutable(index)->assign(value);
+}
+inline void FindFullHashesRequest::set_client_states(int index, const char* value) {
+ client_states_.Mutable(index)->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+}
+inline void FindFullHashesRequest::set_client_states(int index, const void* value, size_t size) {
+ client_states_.Mutable(index)->assign(
+ reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+}
+inline ::std::string* FindFullHashesRequest::add_client_states() {
+ return client_states_.Add();
+}
+inline void FindFullHashesRequest::add_client_states(const ::std::string& value) {
+ client_states_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+}
+inline void FindFullHashesRequest::add_client_states(const char* value) {
+ client_states_.Add()->assign(value);
+ // @@protoc_insertion_point(field_add_char:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+}
+inline void FindFullHashesRequest::add_client_states(const void* value, size_t size) {
+ client_states_.Add()->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_add_pointer:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+}
+inline const ::google::protobuf::RepeatedPtrField< ::std::string>&
+FindFullHashesRequest::client_states() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+ return client_states_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::std::string>*
+FindFullHashesRequest::mutable_client_states() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FindFullHashesRequest.client_states)
+ return &client_states_;
+}
+
+// optional .mozilla.safebrowsing.ThreatInfo threat_info = 3;
+inline bool FindFullHashesRequest::has_threat_info() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FindFullHashesRequest::set_has_threat_info() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FindFullHashesRequest::clear_has_threat_info() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FindFullHashesRequest::clear_threat_info() {
+ if (threat_info_ != NULL) threat_info_->::mozilla::safebrowsing::ThreatInfo::Clear();
+ clear_has_threat_info();
+}
+inline const ::mozilla::safebrowsing::ThreatInfo& FindFullHashesRequest::threat_info() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindFullHashesRequest.threat_info)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return threat_info_ != NULL ? *threat_info_ : *default_instance().threat_info_;
+#else
+ return threat_info_ != NULL ? *threat_info_ : *default_instance_->threat_info_;
+#endif
+}
+inline ::mozilla::safebrowsing::ThreatInfo* FindFullHashesRequest::mutable_threat_info() {
+ set_has_threat_info();
+ if (threat_info_ == NULL) threat_info_ = new ::mozilla::safebrowsing::ThreatInfo;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindFullHashesRequest.threat_info)
+ return threat_info_;
+}
+inline ::mozilla::safebrowsing::ThreatInfo* FindFullHashesRequest::release_threat_info() {
+ clear_has_threat_info();
+ ::mozilla::safebrowsing::ThreatInfo* temp = threat_info_;
+ threat_info_ = NULL;
+ return temp;
+}
+inline void FindFullHashesRequest::set_allocated_threat_info(::mozilla::safebrowsing::ThreatInfo* threat_info) {
+ delete threat_info_;
+ threat_info_ = threat_info;
+ if (threat_info) {
+ set_has_threat_info();
+ } else {
+ clear_has_threat_info();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FindFullHashesRequest.threat_info)
+}
+
+// -------------------------------------------------------------------
+
+// FindFullHashesResponse
+
+// repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+inline int FindFullHashesResponse::matches_size() const {
+ return matches_.size();
+}
+inline void FindFullHashesResponse::clear_matches() {
+ matches_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatMatch& FindFullHashesResponse::matches(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindFullHashesResponse.matches)
+ return matches_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatMatch* FindFullHashesResponse::mutable_matches(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindFullHashesResponse.matches)
+ return matches_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatMatch* FindFullHashesResponse::add_matches() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.FindFullHashesResponse.matches)
+ return matches_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >&
+FindFullHashesResponse::matches() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.FindFullHashesResponse.matches)
+ return matches_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatMatch >*
+FindFullHashesResponse::mutable_matches() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.FindFullHashesResponse.matches)
+ return &matches_;
+}
+
+// optional .mozilla.safebrowsing.Duration minimum_wait_duration = 2;
+inline bool FindFullHashesResponse::has_minimum_wait_duration() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void FindFullHashesResponse::set_has_minimum_wait_duration() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void FindFullHashesResponse::clear_has_minimum_wait_duration() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void FindFullHashesResponse::clear_minimum_wait_duration() {
+ if (minimum_wait_duration_ != NULL) minimum_wait_duration_->::mozilla::safebrowsing::Duration::Clear();
+ clear_has_minimum_wait_duration();
+}
+inline const ::mozilla::safebrowsing::Duration& FindFullHashesResponse::minimum_wait_duration() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindFullHashesResponse.minimum_wait_duration)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return minimum_wait_duration_ != NULL ? *minimum_wait_duration_ : *default_instance().minimum_wait_duration_;
+#else
+ return minimum_wait_duration_ != NULL ? *minimum_wait_duration_ : *default_instance_->minimum_wait_duration_;
+#endif
+}
+inline ::mozilla::safebrowsing::Duration* FindFullHashesResponse::mutable_minimum_wait_duration() {
+ set_has_minimum_wait_duration();
+ if (minimum_wait_duration_ == NULL) minimum_wait_duration_ = new ::mozilla::safebrowsing::Duration;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindFullHashesResponse.minimum_wait_duration)
+ return minimum_wait_duration_;
+}
+inline ::mozilla::safebrowsing::Duration* FindFullHashesResponse::release_minimum_wait_duration() {
+ clear_has_minimum_wait_duration();
+ ::mozilla::safebrowsing::Duration* temp = minimum_wait_duration_;
+ minimum_wait_duration_ = NULL;
+ return temp;
+}
+inline void FindFullHashesResponse::set_allocated_minimum_wait_duration(::mozilla::safebrowsing::Duration* minimum_wait_duration) {
+ delete minimum_wait_duration_;
+ minimum_wait_duration_ = minimum_wait_duration;
+ if (minimum_wait_duration) {
+ set_has_minimum_wait_duration();
+ } else {
+ clear_has_minimum_wait_duration();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FindFullHashesResponse.minimum_wait_duration)
+}
+
+// optional .mozilla.safebrowsing.Duration negative_cache_duration = 3;
+inline bool FindFullHashesResponse::has_negative_cache_duration() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void FindFullHashesResponse::set_has_negative_cache_duration() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void FindFullHashesResponse::clear_has_negative_cache_duration() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void FindFullHashesResponse::clear_negative_cache_duration() {
+ if (negative_cache_duration_ != NULL) negative_cache_duration_->::mozilla::safebrowsing::Duration::Clear();
+ clear_has_negative_cache_duration();
+}
+inline const ::mozilla::safebrowsing::Duration& FindFullHashesResponse::negative_cache_duration() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.FindFullHashesResponse.negative_cache_duration)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return negative_cache_duration_ != NULL ? *negative_cache_duration_ : *default_instance().negative_cache_duration_;
+#else
+ return negative_cache_duration_ != NULL ? *negative_cache_duration_ : *default_instance_->negative_cache_duration_;
+#endif
+}
+inline ::mozilla::safebrowsing::Duration* FindFullHashesResponse::mutable_negative_cache_duration() {
+ set_has_negative_cache_duration();
+ if (negative_cache_duration_ == NULL) negative_cache_duration_ = new ::mozilla::safebrowsing::Duration;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.FindFullHashesResponse.negative_cache_duration)
+ return negative_cache_duration_;
+}
+inline ::mozilla::safebrowsing::Duration* FindFullHashesResponse::release_negative_cache_duration() {
+ clear_has_negative_cache_duration();
+ ::mozilla::safebrowsing::Duration* temp = negative_cache_duration_;
+ negative_cache_duration_ = NULL;
+ return temp;
+}
+inline void FindFullHashesResponse::set_allocated_negative_cache_duration(::mozilla::safebrowsing::Duration* negative_cache_duration) {
+ delete negative_cache_duration_;
+ negative_cache_duration_ = negative_cache_duration;
+ if (negative_cache_duration) {
+ set_has_negative_cache_duration();
+ } else {
+ clear_has_negative_cache_duration();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.FindFullHashesResponse.negative_cache_duration)
+}
+
+// -------------------------------------------------------------------
+
+// ThreatHit_ThreatSource
+
+// optional string url = 1;
+inline bool ThreatHit_ThreatSource::has_url() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatHit_ThreatSource::set_has_url() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatHit_ThreatSource::clear_has_url() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatHit_ThreatSource::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ThreatHit_ThreatSource::url() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.ThreatSource.url)
+ return *url_;
+}
+inline void ThreatHit_ThreatSource::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatHit.ThreatSource.url)
+}
+inline void ThreatHit_ThreatSource::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatHit.ThreatSource.url)
+}
+inline void ThreatHit_ThreatSource::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatHit.ThreatSource.url)
+}
+inline ::std::string* ThreatHit_ThreatSource::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatHit.ThreatSource.url)
+ return url_;
+}
+inline ::std::string* ThreatHit_ThreatSource::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatHit_ThreatSource::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatHit.ThreatSource.url)
+}
+
+// optional .mozilla.safebrowsing.ThreatHit.ThreatSourceType type = 2;
+inline bool ThreatHit_ThreatSource::has_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatHit_ThreatSource::set_has_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatHit_ThreatSource::clear_has_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatHit_ThreatSource::clear_type() {
+ type_ = 0;
+ clear_has_type();
+}
+inline ::mozilla::safebrowsing::ThreatHit_ThreatSourceType ThreatHit_ThreatSource::type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.ThreatSource.type)
+ return static_cast< ::mozilla::safebrowsing::ThreatHit_ThreatSourceType >(type_);
+}
+inline void ThreatHit_ThreatSource::set_type(::mozilla::safebrowsing::ThreatHit_ThreatSourceType value) {
+ assert(::mozilla::safebrowsing::ThreatHit_ThreatSourceType_IsValid(value));
+ set_has_type();
+ type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatHit.ThreatSource.type)
+}
+
+// optional string remote_ip = 3;
+inline bool ThreatHit_ThreatSource::has_remote_ip() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ThreatHit_ThreatSource::set_has_remote_ip() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ThreatHit_ThreatSource::clear_has_remote_ip() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ThreatHit_ThreatSource::clear_remote_ip() {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_->clear();
+ }
+ clear_has_remote_ip();
+}
+inline const ::std::string& ThreatHit_ThreatSource::remote_ip() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.ThreatSource.remote_ip)
+ return *remote_ip_;
+}
+inline void ThreatHit_ThreatSource::set_remote_ip(const ::std::string& value) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatHit.ThreatSource.remote_ip)
+}
+inline void ThreatHit_ThreatSource::set_remote_ip(const char* value) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatHit.ThreatSource.remote_ip)
+}
+inline void ThreatHit_ThreatSource::set_remote_ip(const char* value, size_t size) {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ remote_ip_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatHit.ThreatSource.remote_ip)
+}
+inline ::std::string* ThreatHit_ThreatSource::mutable_remote_ip() {
+ set_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ remote_ip_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatHit.ThreatSource.remote_ip)
+ return remote_ip_;
+}
+inline ::std::string* ThreatHit_ThreatSource::release_remote_ip() {
+ clear_has_remote_ip();
+ if (remote_ip_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = remote_ip_;
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatHit_ThreatSource::set_allocated_remote_ip(::std::string* remote_ip) {
+ if (remote_ip_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete remote_ip_;
+ }
+ if (remote_ip) {
+ set_has_remote_ip();
+ remote_ip_ = remote_ip;
+ } else {
+ clear_has_remote_ip();
+ remote_ip_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatHit.ThreatSource.remote_ip)
+}
+
+// optional string referrer = 4;
+inline bool ThreatHit_ThreatSource::has_referrer() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ThreatHit_ThreatSource::set_has_referrer() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ThreatHit_ThreatSource::clear_has_referrer() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ThreatHit_ThreatSource::clear_referrer() {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_->clear();
+ }
+ clear_has_referrer();
+}
+inline const ::std::string& ThreatHit_ThreatSource::referrer() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.ThreatSource.referrer)
+ return *referrer_;
+}
+inline void ThreatHit_ThreatSource::set_referrer(const ::std::string& value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatHit.ThreatSource.referrer)
+}
+inline void ThreatHit_ThreatSource::set_referrer(const char* value) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatHit.ThreatSource.referrer)
+}
+inline void ThreatHit_ThreatSource::set_referrer(const char* value, size_t size) {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ referrer_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatHit.ThreatSource.referrer)
+}
+inline ::std::string* ThreatHit_ThreatSource::mutable_referrer() {
+ set_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ referrer_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatHit.ThreatSource.referrer)
+ return referrer_;
+}
+inline ::std::string* ThreatHit_ThreatSource::release_referrer() {
+ clear_has_referrer();
+ if (referrer_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = referrer_;
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatHit_ThreatSource::set_allocated_referrer(::std::string* referrer) {
+ if (referrer_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete referrer_;
+ }
+ if (referrer) {
+ set_has_referrer();
+ referrer_ = referrer;
+ } else {
+ clear_has_referrer();
+ referrer_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatHit.ThreatSource.referrer)
+}
+
+// -------------------------------------------------------------------
+
+// ThreatHit
+
+// optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+inline bool ThreatHit::has_threat_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatHit::set_has_threat_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatHit::clear_has_threat_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatHit::clear_threat_type() {
+ threat_type_ = 0;
+ clear_has_threat_type();
+}
+inline ::mozilla::safebrowsing::ThreatType ThreatHit::threat_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.threat_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatType >(threat_type_);
+}
+inline void ThreatHit::set_threat_type(::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ set_has_threat_type();
+ threat_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatHit.threat_type)
+}
+
+// optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+inline bool ThreatHit::has_platform_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatHit::set_has_platform_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatHit::clear_has_platform_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatHit::clear_platform_type() {
+ platform_type_ = 0;
+ clear_has_platform_type();
+}
+inline ::mozilla::safebrowsing::PlatformType ThreatHit::platform_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.platform_type)
+ return static_cast< ::mozilla::safebrowsing::PlatformType >(platform_type_);
+}
+inline void ThreatHit::set_platform_type(::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ set_has_platform_type();
+ platform_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatHit.platform_type)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntry entry = 3;
+inline bool ThreatHit::has_entry() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ThreatHit::set_has_entry() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ThreatHit::clear_has_entry() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ThreatHit::clear_entry() {
+ if (entry_ != NULL) entry_->::mozilla::safebrowsing::ThreatEntry::Clear();
+ clear_has_entry();
+}
+inline const ::mozilla::safebrowsing::ThreatEntry& ThreatHit::entry() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.entry)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return entry_ != NULL ? *entry_ : *default_instance().entry_;
+#else
+ return entry_ != NULL ? *entry_ : *default_instance_->entry_;
+#endif
+}
+inline ::mozilla::safebrowsing::ThreatEntry* ThreatHit::mutable_entry() {
+ set_has_entry();
+ if (entry_ == NULL) entry_ = new ::mozilla::safebrowsing::ThreatEntry;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatHit.entry)
+ return entry_;
+}
+inline ::mozilla::safebrowsing::ThreatEntry* ThreatHit::release_entry() {
+ clear_has_entry();
+ ::mozilla::safebrowsing::ThreatEntry* temp = entry_;
+ entry_ = NULL;
+ return temp;
+}
+inline void ThreatHit::set_allocated_entry(::mozilla::safebrowsing::ThreatEntry* entry) {
+ delete entry_;
+ entry_ = entry;
+ if (entry) {
+ set_has_entry();
+ } else {
+ clear_has_entry();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatHit.entry)
+}
+
+// repeated .mozilla.safebrowsing.ThreatHit.ThreatSource resources = 4;
+inline int ThreatHit::resources_size() const {
+ return resources_.size();
+}
+inline void ThreatHit::clear_resources() {
+ resources_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatHit_ThreatSource& ThreatHit::resources(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatHit.resources)
+ return resources_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatHit_ThreatSource* ThreatHit::mutable_resources(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatHit.resources)
+ return resources_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatHit_ThreatSource* ThreatHit::add_resources() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ThreatHit.resources)
+ return resources_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatHit_ThreatSource >&
+ThreatHit::resources() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ThreatHit.resources)
+ return resources_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatHit_ThreatSource >*
+ThreatHit::mutable_resources() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ThreatHit.resources)
+ return &resources_;
+}
+
+// -------------------------------------------------------------------
+
+// ClientInfo
+
+// optional string client_id = 1;
+inline bool ClientInfo::has_client_id() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientInfo::set_has_client_id() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientInfo::clear_has_client_id() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientInfo::clear_client_id() {
+ if (client_id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_id_->clear();
+ }
+ clear_has_client_id();
+}
+inline const ::std::string& ClientInfo::client_id() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ClientInfo.client_id)
+ return *client_id_;
+}
+inline void ClientInfo::set_client_id(const ::std::string& value) {
+ set_has_client_id();
+ if (client_id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_id_ = new ::std::string;
+ }
+ client_id_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ClientInfo.client_id)
+}
+inline void ClientInfo::set_client_id(const char* value) {
+ set_has_client_id();
+ if (client_id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_id_ = new ::std::string;
+ }
+ client_id_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ClientInfo.client_id)
+}
+inline void ClientInfo::set_client_id(const char* value, size_t size) {
+ set_has_client_id();
+ if (client_id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_id_ = new ::std::string;
+ }
+ client_id_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ClientInfo.client_id)
+}
+inline ::std::string* ClientInfo::mutable_client_id() {
+ set_has_client_id();
+ if (client_id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_id_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ClientInfo.client_id)
+ return client_id_;
+}
+inline ::std::string* ClientInfo::release_client_id() {
+ clear_has_client_id();
+ if (client_id_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = client_id_;
+ client_id_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientInfo::set_allocated_client_id(::std::string* client_id) {
+ if (client_id_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete client_id_;
+ }
+ if (client_id) {
+ set_has_client_id();
+ client_id_ = client_id;
+ } else {
+ clear_has_client_id();
+ client_id_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ClientInfo.client_id)
+}
+
+// optional string client_version = 2;
+inline bool ClientInfo::has_client_version() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientInfo::set_has_client_version() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientInfo::clear_has_client_version() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientInfo::clear_client_version() {
+ if (client_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_version_->clear();
+ }
+ clear_has_client_version();
+}
+inline const ::std::string& ClientInfo::client_version() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ClientInfo.client_version)
+ return *client_version_;
+}
+inline void ClientInfo::set_client_version(const ::std::string& value) {
+ set_has_client_version();
+ if (client_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_version_ = new ::std::string;
+ }
+ client_version_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ClientInfo.client_version)
+}
+inline void ClientInfo::set_client_version(const char* value) {
+ set_has_client_version();
+ if (client_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_version_ = new ::std::string;
+ }
+ client_version_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ClientInfo.client_version)
+}
+inline void ClientInfo::set_client_version(const char* value, size_t size) {
+ set_has_client_version();
+ if (client_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_version_ = new ::std::string;
+ }
+ client_version_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ClientInfo.client_version)
+}
+inline ::std::string* ClientInfo::mutable_client_version() {
+ set_has_client_version();
+ if (client_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ client_version_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ClientInfo.client_version)
+ return client_version_;
+}
+inline ::std::string* ClientInfo::release_client_version() {
+ clear_has_client_version();
+ if (client_version_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = client_version_;
+ client_version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ClientInfo::set_allocated_client_version(::std::string* client_version) {
+ if (client_version_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete client_version_;
+ }
+ if (client_version) {
+ set_has_client_version();
+ client_version_ = client_version;
+ } else {
+ clear_has_client_version();
+ client_version_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ClientInfo.client_version)
+}
+
+// -------------------------------------------------------------------
+
+// Checksum
+
+// optional bytes sha256 = 1;
+inline bool Checksum::has_sha256() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void Checksum::set_has_sha256() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void Checksum::clear_has_sha256() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void Checksum::clear_sha256() {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_->clear();
+ }
+ clear_has_sha256();
+}
+inline const ::std::string& Checksum::sha256() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.Checksum.sha256)
+ return *sha256_;
+}
+inline void Checksum::set_sha256(const ::std::string& value) {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ sha256_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.Checksum.sha256)
+}
+inline void Checksum::set_sha256(const char* value) {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ sha256_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.Checksum.sha256)
+}
+inline void Checksum::set_sha256(const void* value, size_t size) {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ sha256_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.Checksum.sha256)
+}
+inline ::std::string* Checksum::mutable_sha256() {
+ set_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ sha256_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.Checksum.sha256)
+ return sha256_;
+}
+inline ::std::string* Checksum::release_sha256() {
+ clear_has_sha256();
+ if (sha256_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = sha256_;
+ sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void Checksum::set_allocated_sha256(::std::string* sha256) {
+ if (sha256_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete sha256_;
+ }
+ if (sha256) {
+ set_has_sha256();
+ sha256_ = sha256;
+ } else {
+ clear_has_sha256();
+ sha256_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.Checksum.sha256)
+}
+
+// -------------------------------------------------------------------
+
+// ThreatEntry
+
+// optional bytes hash = 1;
+inline bool ThreatEntry::has_hash() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatEntry::set_has_hash() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatEntry::clear_has_hash() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatEntry::clear_hash() {
+ if (hash_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ hash_->clear();
+ }
+ clear_has_hash();
+}
+inline const ::std::string& ThreatEntry::hash() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntry.hash)
+ return *hash_;
+}
+inline void ThreatEntry::set_hash(const ::std::string& value) {
+ set_has_hash();
+ if (hash_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ hash_ = new ::std::string;
+ }
+ hash_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatEntry.hash)
+}
+inline void ThreatEntry::set_hash(const char* value) {
+ set_has_hash();
+ if (hash_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ hash_ = new ::std::string;
+ }
+ hash_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatEntry.hash)
+}
+inline void ThreatEntry::set_hash(const void* value, size_t size) {
+ set_has_hash();
+ if (hash_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ hash_ = new ::std::string;
+ }
+ hash_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatEntry.hash)
+}
+inline ::std::string* ThreatEntry::mutable_hash() {
+ set_has_hash();
+ if (hash_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ hash_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntry.hash)
+ return hash_;
+}
+inline ::std::string* ThreatEntry::release_hash() {
+ clear_has_hash();
+ if (hash_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = hash_;
+ hash_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatEntry::set_allocated_hash(::std::string* hash) {
+ if (hash_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete hash_;
+ }
+ if (hash) {
+ set_has_hash();
+ hash_ = hash;
+ } else {
+ clear_has_hash();
+ hash_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntry.hash)
+}
+
+// optional string url = 2;
+inline bool ThreatEntry::has_url() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatEntry::set_has_url() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatEntry::clear_has_url() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatEntry::clear_url() {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_->clear();
+ }
+ clear_has_url();
+}
+inline const ::std::string& ThreatEntry::url() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntry.url)
+ return *url_;
+}
+inline void ThreatEntry::set_url(const ::std::string& value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatEntry.url)
+}
+inline void ThreatEntry::set_url(const char* value) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatEntry.url)
+}
+inline void ThreatEntry::set_url(const char* value, size_t size) {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ url_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatEntry.url)
+}
+inline ::std::string* ThreatEntry::mutable_url() {
+ set_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ url_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntry.url)
+ return url_;
+}
+inline ::std::string* ThreatEntry::release_url() {
+ clear_has_url();
+ if (url_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = url_;
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatEntry::set_allocated_url(::std::string* url) {
+ if (url_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete url_;
+ }
+ if (url) {
+ set_has_url();
+ url_ = url;
+ } else {
+ clear_has_url();
+ url_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntry.url)
+}
+
+// -------------------------------------------------------------------
+
+// ThreatEntrySet
+
+// optional .mozilla.safebrowsing.CompressionType compression_type = 1;
+inline bool ThreatEntrySet::has_compression_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatEntrySet::set_has_compression_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatEntrySet::clear_has_compression_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatEntrySet::clear_compression_type() {
+ compression_type_ = 0;
+ clear_has_compression_type();
+}
+inline ::mozilla::safebrowsing::CompressionType ThreatEntrySet::compression_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntrySet.compression_type)
+ return static_cast< ::mozilla::safebrowsing::CompressionType >(compression_type_);
+}
+inline void ThreatEntrySet::set_compression_type(::mozilla::safebrowsing::CompressionType value) {
+ assert(::mozilla::safebrowsing::CompressionType_IsValid(value));
+ set_has_compression_type();
+ compression_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatEntrySet.compression_type)
+}
+
+// optional .mozilla.safebrowsing.RawHashes raw_hashes = 2;
+inline bool ThreatEntrySet::has_raw_hashes() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatEntrySet::set_has_raw_hashes() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatEntrySet::clear_has_raw_hashes() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatEntrySet::clear_raw_hashes() {
+ if (raw_hashes_ != NULL) raw_hashes_->::mozilla::safebrowsing::RawHashes::Clear();
+ clear_has_raw_hashes();
+}
+inline const ::mozilla::safebrowsing::RawHashes& ThreatEntrySet::raw_hashes() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntrySet.raw_hashes)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return raw_hashes_ != NULL ? *raw_hashes_ : *default_instance().raw_hashes_;
+#else
+ return raw_hashes_ != NULL ? *raw_hashes_ : *default_instance_->raw_hashes_;
+#endif
+}
+inline ::mozilla::safebrowsing::RawHashes* ThreatEntrySet::mutable_raw_hashes() {
+ set_has_raw_hashes();
+ if (raw_hashes_ == NULL) raw_hashes_ = new ::mozilla::safebrowsing::RawHashes;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntrySet.raw_hashes)
+ return raw_hashes_;
+}
+inline ::mozilla::safebrowsing::RawHashes* ThreatEntrySet::release_raw_hashes() {
+ clear_has_raw_hashes();
+ ::mozilla::safebrowsing::RawHashes* temp = raw_hashes_;
+ raw_hashes_ = NULL;
+ return temp;
+}
+inline void ThreatEntrySet::set_allocated_raw_hashes(::mozilla::safebrowsing::RawHashes* raw_hashes) {
+ delete raw_hashes_;
+ raw_hashes_ = raw_hashes;
+ if (raw_hashes) {
+ set_has_raw_hashes();
+ } else {
+ clear_has_raw_hashes();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntrySet.raw_hashes)
+}
+
+// optional .mozilla.safebrowsing.RawIndices raw_indices = 3;
+inline bool ThreatEntrySet::has_raw_indices() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ThreatEntrySet::set_has_raw_indices() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ThreatEntrySet::clear_has_raw_indices() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ThreatEntrySet::clear_raw_indices() {
+ if (raw_indices_ != NULL) raw_indices_->::mozilla::safebrowsing::RawIndices::Clear();
+ clear_has_raw_indices();
+}
+inline const ::mozilla::safebrowsing::RawIndices& ThreatEntrySet::raw_indices() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntrySet.raw_indices)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return raw_indices_ != NULL ? *raw_indices_ : *default_instance().raw_indices_;
+#else
+ return raw_indices_ != NULL ? *raw_indices_ : *default_instance_->raw_indices_;
+#endif
+}
+inline ::mozilla::safebrowsing::RawIndices* ThreatEntrySet::mutable_raw_indices() {
+ set_has_raw_indices();
+ if (raw_indices_ == NULL) raw_indices_ = new ::mozilla::safebrowsing::RawIndices;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntrySet.raw_indices)
+ return raw_indices_;
+}
+inline ::mozilla::safebrowsing::RawIndices* ThreatEntrySet::release_raw_indices() {
+ clear_has_raw_indices();
+ ::mozilla::safebrowsing::RawIndices* temp = raw_indices_;
+ raw_indices_ = NULL;
+ return temp;
+}
+inline void ThreatEntrySet::set_allocated_raw_indices(::mozilla::safebrowsing::RawIndices* raw_indices) {
+ delete raw_indices_;
+ raw_indices_ = raw_indices;
+ if (raw_indices) {
+ set_has_raw_indices();
+ } else {
+ clear_has_raw_indices();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntrySet.raw_indices)
+}
+
+// optional .mozilla.safebrowsing.RiceDeltaEncoding rice_hashes = 4;
+inline bool ThreatEntrySet::has_rice_hashes() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void ThreatEntrySet::set_has_rice_hashes() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void ThreatEntrySet::clear_has_rice_hashes() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void ThreatEntrySet::clear_rice_hashes() {
+ if (rice_hashes_ != NULL) rice_hashes_->::mozilla::safebrowsing::RiceDeltaEncoding::Clear();
+ clear_has_rice_hashes();
+}
+inline const ::mozilla::safebrowsing::RiceDeltaEncoding& ThreatEntrySet::rice_hashes() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntrySet.rice_hashes)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return rice_hashes_ != NULL ? *rice_hashes_ : *default_instance().rice_hashes_;
+#else
+ return rice_hashes_ != NULL ? *rice_hashes_ : *default_instance_->rice_hashes_;
+#endif
+}
+inline ::mozilla::safebrowsing::RiceDeltaEncoding* ThreatEntrySet::mutable_rice_hashes() {
+ set_has_rice_hashes();
+ if (rice_hashes_ == NULL) rice_hashes_ = new ::mozilla::safebrowsing::RiceDeltaEncoding;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntrySet.rice_hashes)
+ return rice_hashes_;
+}
+inline ::mozilla::safebrowsing::RiceDeltaEncoding* ThreatEntrySet::release_rice_hashes() {
+ clear_has_rice_hashes();
+ ::mozilla::safebrowsing::RiceDeltaEncoding* temp = rice_hashes_;
+ rice_hashes_ = NULL;
+ return temp;
+}
+inline void ThreatEntrySet::set_allocated_rice_hashes(::mozilla::safebrowsing::RiceDeltaEncoding* rice_hashes) {
+ delete rice_hashes_;
+ rice_hashes_ = rice_hashes;
+ if (rice_hashes) {
+ set_has_rice_hashes();
+ } else {
+ clear_has_rice_hashes();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntrySet.rice_hashes)
+}
+
+// optional .mozilla.safebrowsing.RiceDeltaEncoding rice_indices = 5;
+inline bool ThreatEntrySet::has_rice_indices() const {
+ return (_has_bits_[0] & 0x00000010u) != 0;
+}
+inline void ThreatEntrySet::set_has_rice_indices() {
+ _has_bits_[0] |= 0x00000010u;
+}
+inline void ThreatEntrySet::clear_has_rice_indices() {
+ _has_bits_[0] &= ~0x00000010u;
+}
+inline void ThreatEntrySet::clear_rice_indices() {
+ if (rice_indices_ != NULL) rice_indices_->::mozilla::safebrowsing::RiceDeltaEncoding::Clear();
+ clear_has_rice_indices();
+}
+inline const ::mozilla::safebrowsing::RiceDeltaEncoding& ThreatEntrySet::rice_indices() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntrySet.rice_indices)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+ return rice_indices_ != NULL ? *rice_indices_ : *default_instance().rice_indices_;
+#else
+ return rice_indices_ != NULL ? *rice_indices_ : *default_instance_->rice_indices_;
+#endif
+}
+inline ::mozilla::safebrowsing::RiceDeltaEncoding* ThreatEntrySet::mutable_rice_indices() {
+ set_has_rice_indices();
+ if (rice_indices_ == NULL) rice_indices_ = new ::mozilla::safebrowsing::RiceDeltaEncoding;
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntrySet.rice_indices)
+ return rice_indices_;
+}
+inline ::mozilla::safebrowsing::RiceDeltaEncoding* ThreatEntrySet::release_rice_indices() {
+ clear_has_rice_indices();
+ ::mozilla::safebrowsing::RiceDeltaEncoding* temp = rice_indices_;
+ rice_indices_ = NULL;
+ return temp;
+}
+inline void ThreatEntrySet::set_allocated_rice_indices(::mozilla::safebrowsing::RiceDeltaEncoding* rice_indices) {
+ delete rice_indices_;
+ rice_indices_ = rice_indices;
+ if (rice_indices) {
+ set_has_rice_indices();
+ } else {
+ clear_has_rice_indices();
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntrySet.rice_indices)
+}
+
+// -------------------------------------------------------------------
+
+// RawIndices
+
+// repeated int32 indices = 1;
+inline int RawIndices::indices_size() const {
+ return indices_.size();
+}
+inline void RawIndices::clear_indices() {
+ indices_.Clear();
+}
+inline ::google::protobuf::int32 RawIndices::indices(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RawIndices.indices)
+ return indices_.Get(index);
+}
+inline void RawIndices::set_indices(int index, ::google::protobuf::int32 value) {
+ indices_.Set(index, value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RawIndices.indices)
+}
+inline void RawIndices::add_indices(::google::protobuf::int32 value) {
+ indices_.Add(value);
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.RawIndices.indices)
+}
+inline const ::google::protobuf::RepeatedField< ::google::protobuf::int32 >&
+RawIndices::indices() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.RawIndices.indices)
+ return indices_;
+}
+inline ::google::protobuf::RepeatedField< ::google::protobuf::int32 >*
+RawIndices::mutable_indices() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.RawIndices.indices)
+ return &indices_;
+}
+
+// -------------------------------------------------------------------
+
+// RawHashes
+
+// optional int32 prefix_size = 1;
+inline bool RawHashes::has_prefix_size() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void RawHashes::set_has_prefix_size() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void RawHashes::clear_has_prefix_size() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void RawHashes::clear_prefix_size() {
+ prefix_size_ = 0;
+ clear_has_prefix_size();
+}
+inline ::google::protobuf::int32 RawHashes::prefix_size() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RawHashes.prefix_size)
+ return prefix_size_;
+}
+inline void RawHashes::set_prefix_size(::google::protobuf::int32 value) {
+ set_has_prefix_size();
+ prefix_size_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RawHashes.prefix_size)
+}
+
+// optional bytes raw_hashes = 2;
+inline bool RawHashes::has_raw_hashes() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void RawHashes::set_has_raw_hashes() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void RawHashes::clear_has_raw_hashes() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void RawHashes::clear_raw_hashes() {
+ if (raw_hashes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_hashes_->clear();
+ }
+ clear_has_raw_hashes();
+}
+inline const ::std::string& RawHashes::raw_hashes() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RawHashes.raw_hashes)
+ return *raw_hashes_;
+}
+inline void RawHashes::set_raw_hashes(const ::std::string& value) {
+ set_has_raw_hashes();
+ if (raw_hashes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_hashes_ = new ::std::string;
+ }
+ raw_hashes_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RawHashes.raw_hashes)
+}
+inline void RawHashes::set_raw_hashes(const char* value) {
+ set_has_raw_hashes();
+ if (raw_hashes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_hashes_ = new ::std::string;
+ }
+ raw_hashes_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.RawHashes.raw_hashes)
+}
+inline void RawHashes::set_raw_hashes(const void* value, size_t size) {
+ set_has_raw_hashes();
+ if (raw_hashes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_hashes_ = new ::std::string;
+ }
+ raw_hashes_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.RawHashes.raw_hashes)
+}
+inline ::std::string* RawHashes::mutable_raw_hashes() {
+ set_has_raw_hashes();
+ if (raw_hashes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ raw_hashes_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.RawHashes.raw_hashes)
+ return raw_hashes_;
+}
+inline ::std::string* RawHashes::release_raw_hashes() {
+ clear_has_raw_hashes();
+ if (raw_hashes_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = raw_hashes_;
+ raw_hashes_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void RawHashes::set_allocated_raw_hashes(::std::string* raw_hashes) {
+ if (raw_hashes_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete raw_hashes_;
+ }
+ if (raw_hashes) {
+ set_has_raw_hashes();
+ raw_hashes_ = raw_hashes;
+ } else {
+ clear_has_raw_hashes();
+ raw_hashes_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.RawHashes.raw_hashes)
+}
+
+// -------------------------------------------------------------------
+
+// RiceDeltaEncoding
+
+// optional int64 first_value = 1;
+inline bool RiceDeltaEncoding::has_first_value() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void RiceDeltaEncoding::set_has_first_value() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void RiceDeltaEncoding::clear_has_first_value() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void RiceDeltaEncoding::clear_first_value() {
+ first_value_ = GOOGLE_LONGLONG(0);
+ clear_has_first_value();
+}
+inline ::google::protobuf::int64 RiceDeltaEncoding::first_value() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RiceDeltaEncoding.first_value)
+ return first_value_;
+}
+inline void RiceDeltaEncoding::set_first_value(::google::protobuf::int64 value) {
+ set_has_first_value();
+ first_value_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RiceDeltaEncoding.first_value)
+}
+
+// optional int32 rice_parameter = 2;
+inline bool RiceDeltaEncoding::has_rice_parameter() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void RiceDeltaEncoding::set_has_rice_parameter() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void RiceDeltaEncoding::clear_has_rice_parameter() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void RiceDeltaEncoding::clear_rice_parameter() {
+ rice_parameter_ = 0;
+ clear_has_rice_parameter();
+}
+inline ::google::protobuf::int32 RiceDeltaEncoding::rice_parameter() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RiceDeltaEncoding.rice_parameter)
+ return rice_parameter_;
+}
+inline void RiceDeltaEncoding::set_rice_parameter(::google::protobuf::int32 value) {
+ set_has_rice_parameter();
+ rice_parameter_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RiceDeltaEncoding.rice_parameter)
+}
+
+// optional int32 num_entries = 3;
+inline bool RiceDeltaEncoding::has_num_entries() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void RiceDeltaEncoding::set_has_num_entries() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void RiceDeltaEncoding::clear_has_num_entries() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void RiceDeltaEncoding::clear_num_entries() {
+ num_entries_ = 0;
+ clear_has_num_entries();
+}
+inline ::google::protobuf::int32 RiceDeltaEncoding::num_entries() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RiceDeltaEncoding.num_entries)
+ return num_entries_;
+}
+inline void RiceDeltaEncoding::set_num_entries(::google::protobuf::int32 value) {
+ set_has_num_entries();
+ num_entries_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RiceDeltaEncoding.num_entries)
+}
+
+// optional bytes encoded_data = 4;
+inline bool RiceDeltaEncoding::has_encoded_data() const {
+ return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void RiceDeltaEncoding::set_has_encoded_data() {
+ _has_bits_[0] |= 0x00000008u;
+}
+inline void RiceDeltaEncoding::clear_has_encoded_data() {
+ _has_bits_[0] &= ~0x00000008u;
+}
+inline void RiceDeltaEncoding::clear_encoded_data() {
+ if (encoded_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ encoded_data_->clear();
+ }
+ clear_has_encoded_data();
+}
+inline const ::std::string& RiceDeltaEncoding::encoded_data() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.RiceDeltaEncoding.encoded_data)
+ return *encoded_data_;
+}
+inline void RiceDeltaEncoding::set_encoded_data(const ::std::string& value) {
+ set_has_encoded_data();
+ if (encoded_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ encoded_data_ = new ::std::string;
+ }
+ encoded_data_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.RiceDeltaEncoding.encoded_data)
+}
+inline void RiceDeltaEncoding::set_encoded_data(const char* value) {
+ set_has_encoded_data();
+ if (encoded_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ encoded_data_ = new ::std::string;
+ }
+ encoded_data_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.RiceDeltaEncoding.encoded_data)
+}
+inline void RiceDeltaEncoding::set_encoded_data(const void* value, size_t size) {
+ set_has_encoded_data();
+ if (encoded_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ encoded_data_ = new ::std::string;
+ }
+ encoded_data_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.RiceDeltaEncoding.encoded_data)
+}
+inline ::std::string* RiceDeltaEncoding::mutable_encoded_data() {
+ set_has_encoded_data();
+ if (encoded_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ encoded_data_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.RiceDeltaEncoding.encoded_data)
+ return encoded_data_;
+}
+inline ::std::string* RiceDeltaEncoding::release_encoded_data() {
+ clear_has_encoded_data();
+ if (encoded_data_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = encoded_data_;
+ encoded_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void RiceDeltaEncoding::set_allocated_encoded_data(::std::string* encoded_data) {
+ if (encoded_data_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete encoded_data_;
+ }
+ if (encoded_data) {
+ set_has_encoded_data();
+ encoded_data_ = encoded_data;
+ } else {
+ clear_has_encoded_data();
+ encoded_data_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.RiceDeltaEncoding.encoded_data)
+}
+
+// -------------------------------------------------------------------
+
+// ThreatEntryMetadata_MetadataEntry
+
+// optional bytes key = 1;
+inline bool ThreatEntryMetadata_MetadataEntry::has_key() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_has_key() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatEntryMetadata_MetadataEntry::clear_has_key() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatEntryMetadata_MetadataEntry::clear_key() {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_->clear();
+ }
+ clear_has_key();
+}
+inline const ::std::string& ThreatEntryMetadata_MetadataEntry::key() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.key)
+ return *key_;
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_key(const ::std::string& value) {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ key_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.key)
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_key(const char* value) {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ key_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.key)
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_key(const void* value, size_t size) {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ key_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.key)
+}
+inline ::std::string* ThreatEntryMetadata_MetadataEntry::mutable_key() {
+ set_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ key_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.key)
+ return key_;
+}
+inline ::std::string* ThreatEntryMetadata_MetadataEntry::release_key() {
+ clear_has_key();
+ if (key_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = key_;
+ key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_allocated_key(::std::string* key) {
+ if (key_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete key_;
+ }
+ if (key) {
+ set_has_key();
+ key_ = key;
+ } else {
+ clear_has_key();
+ key_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.key)
+}
+
+// optional bytes value = 2;
+inline bool ThreatEntryMetadata_MetadataEntry::has_value() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_has_value() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatEntryMetadata_MetadataEntry::clear_has_value() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatEntryMetadata_MetadataEntry::clear_value() {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_->clear();
+ }
+ clear_has_value();
+}
+inline const ::std::string& ThreatEntryMetadata_MetadataEntry::value() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.value)
+ return *value_;
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_value(const ::std::string& value) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(value);
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.value)
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_value(const char* value) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(value);
+ // @@protoc_insertion_point(field_set_char:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.value)
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_value(const void* value, size_t size) {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ value_->assign(reinterpret_cast<const char*>(value), size);
+ // @@protoc_insertion_point(field_set_pointer:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.value)
+}
+inline ::std::string* ThreatEntryMetadata_MetadataEntry::mutable_value() {
+ set_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ value_ = new ::std::string;
+ }
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.value)
+ return value_;
+}
+inline ::std::string* ThreatEntryMetadata_MetadataEntry::release_value() {
+ clear_has_value();
+ if (value_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ return NULL;
+ } else {
+ ::std::string* temp = value_;
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ return temp;
+ }
+}
+inline void ThreatEntryMetadata_MetadataEntry::set_allocated_value(::std::string* value) {
+ if (value_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+ delete value_;
+ }
+ if (value) {
+ set_has_value();
+ value_ = value;
+ } else {
+ clear_has_value();
+ value_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+ }
+ // @@protoc_insertion_point(field_set_allocated:mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry.value)
+}
+
+// -------------------------------------------------------------------
+
+// ThreatEntryMetadata
+
+// repeated .mozilla.safebrowsing.ThreatEntryMetadata.MetadataEntry entries = 1;
+inline int ThreatEntryMetadata::entries_size() const {
+ return entries_.size();
+}
+inline void ThreatEntryMetadata::clear_entries() {
+ entries_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry& ThreatEntryMetadata::entries(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatEntryMetadata.entries)
+ return entries_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry* ThreatEntryMetadata::mutable_entries(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ThreatEntryMetadata.entries)
+ return entries_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry* ThreatEntryMetadata::add_entries() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ThreatEntryMetadata.entries)
+ return entries_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry >&
+ThreatEntryMetadata::entries() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ThreatEntryMetadata.entries)
+ return entries_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatEntryMetadata_MetadataEntry >*
+ThreatEntryMetadata::mutable_entries() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ThreatEntryMetadata.entries)
+ return &entries_;
+}
+
+// -------------------------------------------------------------------
+
+// ThreatListDescriptor
+
+// optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+inline bool ThreatListDescriptor::has_threat_type() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ThreatListDescriptor::set_has_threat_type() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void ThreatListDescriptor::clear_has_threat_type() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void ThreatListDescriptor::clear_threat_type() {
+ threat_type_ = 0;
+ clear_has_threat_type();
+}
+inline ::mozilla::safebrowsing::ThreatType ThreatListDescriptor::threat_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatListDescriptor.threat_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatType >(threat_type_);
+}
+inline void ThreatListDescriptor::set_threat_type(::mozilla::safebrowsing::ThreatType value) {
+ assert(::mozilla::safebrowsing::ThreatType_IsValid(value));
+ set_has_threat_type();
+ threat_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatListDescriptor.threat_type)
+}
+
+// optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+inline bool ThreatListDescriptor::has_platform_type() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ThreatListDescriptor::set_has_platform_type() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void ThreatListDescriptor::clear_has_platform_type() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void ThreatListDescriptor::clear_platform_type() {
+ platform_type_ = 0;
+ clear_has_platform_type();
+}
+inline ::mozilla::safebrowsing::PlatformType ThreatListDescriptor::platform_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatListDescriptor.platform_type)
+ return static_cast< ::mozilla::safebrowsing::PlatformType >(platform_type_);
+}
+inline void ThreatListDescriptor::set_platform_type(::mozilla::safebrowsing::PlatformType value) {
+ assert(::mozilla::safebrowsing::PlatformType_IsValid(value));
+ set_has_platform_type();
+ platform_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatListDescriptor.platform_type)
+}
+
+// optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 3;
+inline bool ThreatListDescriptor::has_threat_entry_type() const {
+ return (_has_bits_[0] & 0x00000004u) != 0;
+}
+inline void ThreatListDescriptor::set_has_threat_entry_type() {
+ _has_bits_[0] |= 0x00000004u;
+}
+inline void ThreatListDescriptor::clear_has_threat_entry_type() {
+ _has_bits_[0] &= ~0x00000004u;
+}
+inline void ThreatListDescriptor::clear_threat_entry_type() {
+ threat_entry_type_ = 0;
+ clear_has_threat_entry_type();
+}
+inline ::mozilla::safebrowsing::ThreatEntryType ThreatListDescriptor::threat_entry_type() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ThreatListDescriptor.threat_entry_type)
+ return static_cast< ::mozilla::safebrowsing::ThreatEntryType >(threat_entry_type_);
+}
+inline void ThreatListDescriptor::set_threat_entry_type(::mozilla::safebrowsing::ThreatEntryType value) {
+ assert(::mozilla::safebrowsing::ThreatEntryType_IsValid(value));
+ set_has_threat_entry_type();
+ threat_entry_type_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.ThreatListDescriptor.threat_entry_type)
+}
+
+// -------------------------------------------------------------------
+
+// ListThreatListsResponse
+
+// repeated .mozilla.safebrowsing.ThreatListDescriptor threat_lists = 1;
+inline int ListThreatListsResponse::threat_lists_size() const {
+ return threat_lists_.size();
+}
+inline void ListThreatListsResponse::clear_threat_lists() {
+ threat_lists_.Clear();
+}
+inline const ::mozilla::safebrowsing::ThreatListDescriptor& ListThreatListsResponse::threat_lists(int index) const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.ListThreatListsResponse.threat_lists)
+ return threat_lists_.Get(index);
+}
+inline ::mozilla::safebrowsing::ThreatListDescriptor* ListThreatListsResponse::mutable_threat_lists(int index) {
+ // @@protoc_insertion_point(field_mutable:mozilla.safebrowsing.ListThreatListsResponse.threat_lists)
+ return threat_lists_.Mutable(index);
+}
+inline ::mozilla::safebrowsing::ThreatListDescriptor* ListThreatListsResponse::add_threat_lists() {
+ // @@protoc_insertion_point(field_add:mozilla.safebrowsing.ListThreatListsResponse.threat_lists)
+ return threat_lists_.Add();
+}
+inline const ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatListDescriptor >&
+ListThreatListsResponse::threat_lists() const {
+ // @@protoc_insertion_point(field_list:mozilla.safebrowsing.ListThreatListsResponse.threat_lists)
+ return threat_lists_;
+}
+inline ::google::protobuf::RepeatedPtrField< ::mozilla::safebrowsing::ThreatListDescriptor >*
+ListThreatListsResponse::mutable_threat_lists() {
+ // @@protoc_insertion_point(field_mutable_list:mozilla.safebrowsing.ListThreatListsResponse.threat_lists)
+ return &threat_lists_;
+}
+
+// -------------------------------------------------------------------
+
+// Duration
+
+// optional int64 seconds = 1;
+inline bool Duration::has_seconds() const {
+ return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void Duration::set_has_seconds() {
+ _has_bits_[0] |= 0x00000001u;
+}
+inline void Duration::clear_has_seconds() {
+ _has_bits_[0] &= ~0x00000001u;
+}
+inline void Duration::clear_seconds() {
+ seconds_ = GOOGLE_LONGLONG(0);
+ clear_has_seconds();
+}
+inline ::google::protobuf::int64 Duration::seconds() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.Duration.seconds)
+ return seconds_;
+}
+inline void Duration::set_seconds(::google::protobuf::int64 value) {
+ set_has_seconds();
+ seconds_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.Duration.seconds)
+}
+
+// optional int32 nanos = 2;
+inline bool Duration::has_nanos() const {
+ return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void Duration::set_has_nanos() {
+ _has_bits_[0] |= 0x00000002u;
+}
+inline void Duration::clear_has_nanos() {
+ _has_bits_[0] &= ~0x00000002u;
+}
+inline void Duration::clear_nanos() {
+ nanos_ = 0;
+ clear_has_nanos();
+}
+inline ::google::protobuf::int32 Duration::nanos() const {
+ // @@protoc_insertion_point(field_get:mozilla.safebrowsing.Duration.nanos)
+ return nanos_;
+}
+inline void Duration::set_nanos(::google::protobuf::int32 value) {
+ set_has_nanos();
+ nanos_ = value;
+ // @@protoc_insertion_point(field_set:mozilla.safebrowsing.Duration.nanos)
+}
+
+
+// @@protoc_insertion_point(namespace_scope)
+
+} // namespace safebrowsing
+} // namespace mozilla
+
+// @@protoc_insertion_point(global_scope)
+
+#endif // PROTOBUF_safebrowsing_2eproto__INCLUDED
diff --git a/toolkit/components/url-classifier/tests/UrlClassifierTestUtils.jsm b/toolkit/components/url-classifier/tests/UrlClassifierTestUtils.jsm
new file mode 100644
index 0000000000..615769473c
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/UrlClassifierTestUtils.jsm
@@ -0,0 +1,98 @@
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["UrlClassifierTestUtils"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const TRACKING_TABLE_NAME = "mochitest-track-simple";
+const TRACKING_TABLE_PREF = "urlclassifier.trackingTable";
+const WHITELIST_TABLE_NAME = "mochitest-trackwhite-simple";
+const WHITELIST_TABLE_PREF = "urlclassifier.trackingWhitelistTable";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+this.UrlClassifierTestUtils = {
+
+ addTestTrackers() {
+ // Add some URLs to the tracking databases
+ let trackingURL1 = "tracking.example.com/";
+ let trackingURL2 = "itisatracker.org/";
+ let trackingURL3 = "trackertest.org/";
+ let whitelistedURL = "itisatrap.org/?resource=itisatracker.org";
+
+ let trackingUpdate =
+ "n:1000\ni:" + TRACKING_TABLE_NAME + "\nad:3\n" +
+ "a:1:32:" + trackingURL1.length + "\n" +
+ trackingURL1 + "\n" +
+ "a:2:32:" + trackingURL2.length + "\n" +
+ trackingURL2 + "\n" +
+ "a:3:32:" + trackingURL3.length + "\n" +
+ trackingURL3 + "\n";
+ let whitelistUpdate =
+ "n:1000\ni:" + WHITELIST_TABLE_NAME + "\nad:1\n" +
+ "a:1:32:" + whitelistedURL.length + "\n" +
+ whitelistedURL + "\n";
+
+ var tables = [
+ {
+ pref: TRACKING_TABLE_PREF,
+ name: TRACKING_TABLE_NAME,
+ update: trackingUpdate
+ },
+ {
+ pref: WHITELIST_TABLE_PREF,
+ name: WHITELIST_TABLE_NAME,
+ update: whitelistUpdate
+ }
+ ];
+
+ return this.useTestDatabase(tables);
+ },
+
+ cleanupTestTrackers() {
+ Services.prefs.clearUserPref(TRACKING_TABLE_PREF);
+ Services.prefs.clearUserPref(WHITELIST_TABLE_PREF);
+ },
+
+ /**
+ * Add some entries to a test tracking protection database, and resets
+ * back to the default database after the test ends.
+ *
+ * @return {Promise}
+ */
+ useTestDatabase(tables) {
+ for (var table of tables) {
+ Services.prefs.setCharPref(table.pref, table.name);
+ }
+
+ return new Promise((resolve, reject) => {
+ let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].
+ getService(Ci.nsIUrlClassifierDBService);
+ let listener = {
+ QueryInterface: iid => {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return listener;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+ updateUrlRequested: url => { },
+ streamFinished: status => { },
+ updateError: errorCode => {
+ reject("Couldn't update classifier.");
+ },
+ updateSuccess: requestedTimeout => {
+ resolve();
+ }
+ };
+
+ for (var table of tables) {
+ dbService.beginUpdate(listener, table.name, "");
+ dbService.beginStream("", "");
+ dbService.updateStream(table.update);
+ dbService.finishStream();
+ dbService.finishUpdate();
+ }
+ });
+ },
+};
diff --git a/toolkit/components/url-classifier/tests/gtest/Common.cpp b/toolkit/components/url-classifier/tests/gtest/Common.cpp
new file mode 100644
index 0000000000..b5f024b38e
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/Common.cpp
@@ -0,0 +1,78 @@
+#include "Common.h"
+#include "HashStore.h"
+#include "Classifier.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsTArray.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsUrlClassifierUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+template<typename Function>
+void RunTestInNewThread(Function&& aFunction) {
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(mozilla::Forward<Function>(aFunction));
+ nsCOMPtr<nsIThread> testingThread;
+ nsresult rv = NS_NewThread(getter_AddRefs(testingThread), r);
+ ASSERT_EQ(rv, NS_OK);
+ testingThread->Shutdown();
+}
+
+already_AddRefed<nsIFile>
+GetFile(const nsTArray<nsString>& path)
+{
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ for (uint32_t i = 0; i < path.Length(); i++) {
+ file->Append(path[i]);
+ }
+ return file.forget();
+}
+
+void ApplyUpdate(nsTArray<TableUpdate*>& updates)
+{
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+
+ UniquePtr<Classifier> classifier(new Classifier());
+ classifier->Open(*file);
+
+ {
+ // Force nsIUrlClassifierUtils loading on main thread
+ // because nsIUrlClassifierDBService will not run in advance
+ // in gtest.
+ nsresult rv;
+ nsCOMPtr<nsIUrlClassifierUtils> dummy =
+ do_GetService(NS_URLCLASSIFIERUTILS_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ }
+
+ RunTestInNewThread([&] () -> void {
+ classifier->ApplyUpdates(&updates);
+ });
+}
+
+void ApplyUpdate(TableUpdate* update)
+{
+ nsTArray<TableUpdate*> updates = { update };
+ ApplyUpdate(updates);
+}
+
+void
+PrefixArrayToPrefixStringMap(const nsTArray<nsCString>& prefixArray,
+ PrefixStringMap& out)
+{
+ out.Clear();
+
+ for (uint32_t i = 0; i < prefixArray.Length(); i++) {
+ const nsCString& prefix = prefixArray[i];
+ nsCString* prefixString = out.LookupOrAdd(prefix.Length());
+ prefixString->Append(prefix.BeginReading(), prefix.Length());
+ }
+}
+
diff --git a/toolkit/components/url-classifier/tests/gtest/Common.h b/toolkit/components/url-classifier/tests/gtest/Common.h
new file mode 100644
index 0000000000..c9a9cdf7ed
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/Common.h
@@ -0,0 +1,26 @@
+#include "HashStore.h"
+#include "nsIFile.h"
+#include "nsTArray.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+template<typename Function>
+void RunTestInNewThread(Function&& aFunction);
+
+// Return nsIFile with root directory - NS_APP_USER_PROFILE_50_DIR
+// Sub-directories are passed in path argument.
+already_AddRefed<nsIFile>
+GetFile(const nsTArray<nsString>& path);
+
+// ApplyUpdate will call |ApplyUpdates| of Classifier within a new thread
+void ApplyUpdate(nsTArray<TableUpdate*>& updates);
+
+void ApplyUpdate(TableUpdate* update);
+
+// This function converts lexigraphic-sorted prefixes to a hashtable
+// which key is prefix size and value is concatenated prefix string.
+void PrefixArrayToPrefixStringMap(const nsTArray<nsCString>& prefixArray,
+ PrefixStringMap& out);
+
diff --git a/toolkit/components/url-classifier/tests/gtest/TestChunkSet.cpp b/toolkit/components/url-classifier/tests/gtest/TestChunkSet.cpp
new file mode 100644
index 0000000000..dba2fc2c10
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestChunkSet.cpp
@@ -0,0 +1,279 @@
+/* -*- 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 <stdio.h>
+#include <stdlib.h>
+#include <set>
+
+#include "gtest/gtest.h"
+#include "ChunkSet.h"
+#include "mozilla/ArrayUtils.h"
+
+TEST(UrlClassifierChunkSet, Empty)
+{
+ mozilla::safebrowsing::ChunkSet chunkSet;
+ mozilla::safebrowsing::ChunkSet removeSet;
+
+ removeSet.Set(0);
+
+ ASSERT_FALSE(chunkSet.Has(0));
+ ASSERT_FALSE(chunkSet.Has(1));
+ ASSERT_TRUE(chunkSet.Remove(removeSet) == NS_OK);
+ ASSERT_TRUE(chunkSet.Length() == 0);
+
+ chunkSet.Set(0);
+
+ ASSERT_TRUE(chunkSet.Has(0));
+ ASSERT_TRUE(chunkSet.Length() == 1);
+ ASSERT_TRUE(chunkSet.Remove(removeSet) == NS_OK);
+ ASSERT_FALSE(chunkSet.Has(0));
+ ASSERT_TRUE(chunkSet.Length() == 0);
+}
+
+TEST(UrlClassifierChunkSet, Main)
+{
+ static int testVals[] = {2, 1, 5, 6, 8, 7, 14, 10, 12, 13};
+
+ mozilla::safebrowsing::ChunkSet chunkSet;
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ chunkSet.Set(testVals[i]);
+ }
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ ASSERT_TRUE(chunkSet.Has(testVals[i]));
+ }
+
+ ASSERT_FALSE(chunkSet.Has(3));
+ ASSERT_FALSE(chunkSet.Has(4));
+ ASSERT_FALSE(chunkSet.Has(9));
+ ASSERT_FALSE(chunkSet.Has(11));
+
+ ASSERT_TRUE(chunkSet.Length() == MOZ_ARRAY_LENGTH(testVals));
+}
+
+TEST(UrlClassifierChunkSet, Merge)
+{
+ static int testVals[] = {2, 1, 5, 6, 8, 7, 14, 10, 12, 13};
+ static int mergeVals[] = {9, 3, 4, 20, 14, 16};
+
+ mozilla::safebrowsing::ChunkSet chunkSet;
+ mozilla::safebrowsing::ChunkSet mergeSet;
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ chunkSet.Set(testVals[i]);
+ }
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ mergeSet.Set(mergeVals[i]);
+ }
+
+ chunkSet.Merge(mergeSet);
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ ASSERT_TRUE(chunkSet.Has(testVals[i]));
+ }
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ ASSERT_TRUE(chunkSet.Has(mergeVals[i]));
+ }
+
+ // -1 because 14 is duplicated in both sets
+ ASSERT_TRUE(chunkSet.Length() ==
+ MOZ_ARRAY_LENGTH(testVals) + MOZ_ARRAY_LENGTH(mergeVals) - 1);
+
+ ASSERT_FALSE(chunkSet.Has(11));
+ ASSERT_FALSE(chunkSet.Has(15));
+ ASSERT_FALSE(chunkSet.Has(17));
+ ASSERT_FALSE(chunkSet.Has(18));
+ ASSERT_FALSE(chunkSet.Has(19));
+}
+
+TEST(UrlClassifierChunkSet, Merge2)
+{
+ static int testVals[] = {2, 1, 5, 6, 8, 7, 14, 10, 12, 13};
+ static int mergeVals[] = {9, 3, 4, 20, 14, 16};
+ static int mergeVals2[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+ mozilla::safebrowsing::ChunkSet chunkSet;
+ mozilla::safebrowsing::ChunkSet mergeSet;
+ mozilla::safebrowsing::ChunkSet mergeSet2;
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ chunkSet.Set(testVals[i]);
+ }
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ mergeSet.Set(mergeVals[i]);
+ }
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals2); i++) {
+ mergeSet2.Set(mergeVals2[i]);
+ }
+
+ chunkSet.Merge(mergeSet);
+ chunkSet.Merge(mergeSet2);
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ ASSERT_TRUE(chunkSet.Has(testVals[i]));
+ }
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ ASSERT_TRUE(chunkSet.Has(mergeVals[i]));
+ }
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals2); i++) {
+ ASSERT_TRUE(chunkSet.Has(mergeVals2[i]));
+ }
+
+ ASSERT_FALSE(chunkSet.Has(15));
+ ASSERT_FALSE(chunkSet.Has(17));
+ ASSERT_FALSE(chunkSet.Has(18));
+ ASSERT_FALSE(chunkSet.Has(19));
+}
+
+TEST(UrlClassifierChunkSet, Stress)
+{
+ mozilla::safebrowsing::ChunkSet chunkSet;
+ mozilla::safebrowsing::ChunkSet mergeSet;
+ std::set<int> refSet;
+ std::set<int> refMergeSet;
+ static const int TEST_ITERS = 7000;
+ static const int REMOVE_ITERS = 3000;
+ static const int TEST_RANGE = 10000;
+
+ // Construction by Set
+ for (int i = 0; i < TEST_ITERS; i++) {
+ int chunk = rand() % TEST_RANGE;
+ chunkSet.Set(chunk);
+ refSet.insert(chunk);
+ }
+
+ // Same elements as reference set
+ for (auto it = refSet.begin(); it != refSet.end(); ++it) {
+ ASSERT_TRUE(chunkSet.Has(*it));
+ }
+
+ // Hole punching via Remove
+ for (int i = 0; i < REMOVE_ITERS; i++) {
+ int chunk = rand() % TEST_RANGE;
+ mozilla::safebrowsing::ChunkSet helpChunk;
+ helpChunk.Set(chunk);
+
+ chunkSet.Remove(helpChunk);
+ refSet.erase(chunk);
+
+ ASSERT_FALSE(chunkSet.Has(chunk));
+ }
+
+ // Should have chunks present in reference set
+ // Should not have chunks absent in reference set
+ for (int it = 0; it < TEST_RANGE; ++it) {
+ auto found = refSet.find(it);
+ if (chunkSet.Has(it)) {
+ ASSERT_FALSE(found == refSet.end());
+ } else {
+ ASSERT_TRUE(found == refSet.end());
+ }
+ }
+
+ // Construct set to merge with
+ for (int i = 0; i < TEST_ITERS; i++) {
+ int chunk = rand() % TEST_RANGE;
+ mergeSet.Set(chunk);
+ refMergeSet.insert(chunk);
+ }
+
+ // Merge set constructed correctly
+ for (auto it = refMergeSet.begin(); it != refMergeSet.end(); ++it) {
+ ASSERT_TRUE(mergeSet.Has(*it));
+ }
+
+ mozilla::safebrowsing::ChunkSet origSet;
+ origSet = chunkSet;
+
+ chunkSet.Merge(mergeSet);
+ refSet.insert(refMergeSet.begin(), refMergeSet.end());
+
+ // Check for presence of elements from both source
+ // Should not have chunks absent in reference set
+ for (int it = 0; it < TEST_RANGE; ++it) {
+ auto found = refSet.find(it);
+ if (chunkSet.Has(it)) {
+ ASSERT_FALSE(found == refSet.end());
+ } else {
+ ASSERT_TRUE(found == refSet.end());
+ }
+ }
+
+ // Unmerge
+ chunkSet.Remove(origSet);
+ for (int it = 0; it < TEST_RANGE; ++it) {
+ if (origSet.Has(it)) {
+ ASSERT_FALSE(chunkSet.Has(it));
+ } else if (mergeSet.Has(it)) {
+ ASSERT_TRUE(chunkSet.Has(it));
+ }
+ }
+}
+
+TEST(UrlClassifierChunkSet, RemoveClear)
+{
+ static int testVals[] = {2, 1, 5, 6, 8, 7, 14, 10, 12, 13};
+ static int mergeVals[] = {3, 4, 9, 16, 20};
+
+ mozilla::safebrowsing::ChunkSet chunkSet;
+ mozilla::safebrowsing::ChunkSet mergeSet;
+ mozilla::safebrowsing::ChunkSet removeSet;
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ chunkSet.Set(testVals[i]);
+ removeSet.Set(testVals[i]);
+ }
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ mergeSet.Set(mergeVals[i]);
+ }
+
+ ASSERT_TRUE(chunkSet.Merge(mergeSet) == NS_OK);
+ ASSERT_TRUE(chunkSet.Remove(removeSet) == NS_OK);
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ ASSERT_TRUE(chunkSet.Has(mergeVals[i]));
+ }
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ ASSERT_FALSE(chunkSet.Has(testVals[i]));
+ }
+
+ chunkSet.Clear();
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ ASSERT_FALSE(chunkSet.Has(mergeVals[i]));
+ }
+}
+
+TEST(UrlClassifierChunkSet, Serialize)
+{
+ static int testVals[] = {2, 1, 5, 6, 8, 7, 14, 10, 12, 13};
+ static int mergeVals[] = {3, 4, 9, 16, 20};
+
+ mozilla::safebrowsing::ChunkSet chunkSet;
+ mozilla::safebrowsing::ChunkSet mergeSet;
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(testVals); i++) {
+ chunkSet.Set(testVals[i]);
+ }
+
+ for (size_t i = 0; i < MOZ_ARRAY_LENGTH(mergeVals); i++) {
+ mergeSet.Set(mergeVals[i]);
+ }
+
+ chunkSet.Merge(mergeSet);
+
+ nsAutoCString mergeResult;
+ chunkSet.Serialize(mergeResult);
+
+ printf("mergeResult: %s\n", mergeResult.get());
+
+ nsAutoCString expected(NS_LITERAL_CSTRING("1-10,12-14,16,20"));
+
+ ASSERT_TRUE(mergeResult.Equals(expected));
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestFailUpdate.cpp b/toolkit/components/url-classifier/tests/gtest/TestFailUpdate.cpp
new file mode 100644
index 0000000000..bdb9eebb06
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestFailUpdate.cpp
@@ -0,0 +1,97 @@
+#include "HashStore.h"
+#include "nsPrintfCString.h"
+#include "string.h"
+#include "gtest/gtest.h"
+#include "mozilla/Unused.h"
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+static const char* kFilesInV2[] = {".pset", ".sbstore"};
+static const char* kFilesInV4[] = {".pset", ".metadata"};
+
+#define V2_TABLE "gtest-malware-simple"
+#define V4_TABLE1 "goog-malware-proto"
+#define V4_TABLE2 "goog-phish-proto"
+
+#define ROOT_DIR NS_LITERAL_STRING("safebrowsing")
+#define SB_FILE(x, y) NS_ConvertUTF8toUTF16(nsPrintfCString("%s%s",x, y))
+
+template<typename T, size_t N>
+void CheckFileExist(const char* table, const T (&files)[N], bool expectExists)
+{
+ for (uint32_t i = 0; i < N; i++) {
+ // This is just a quick way to know if this is v4 table
+ NS_ConvertUTF8toUTF16 SUB_DIR(strstr(table, "-proto") ? "google4" : "");
+ nsCOMPtr<nsIFile> file =
+ GetFile(nsTArray<nsString> { ROOT_DIR, SUB_DIR, SB_FILE(table, files[i]) });
+
+ bool exists;
+ file->Exists(&exists);
+
+ nsAutoCString path;
+ file->GetNativePath(path);
+ ASSERT_EQ(expectExists, exists) << path.get();
+ }
+}
+
+TEST(FailUpdate, CheckTableReset)
+{
+ const bool FULL_UPDATE = true;
+ const bool PARTIAL_UPDATE = false;
+
+ // Apply V2 update
+ {
+ auto update = new TableUpdateV2(NS_LITERAL_CSTRING(V2_TABLE));
+ Unused << update->NewAddChunk(1);
+
+ ApplyUpdate(update);
+
+ // A successful V2 update should create .pset & .sbstore files
+ CheckFileExist(V2_TABLE, kFilesInV2, true);
+ }
+
+ // Helper function to generate table update data
+ auto func = [](TableUpdateV4* update, bool full, const char* str) {
+ update->SetFullUpdate(full);
+ std::string prefix(str);
+ update->NewPrefixes(prefix.length(), prefix);
+ };
+
+ // Apply V4 update for table1
+ {
+ auto update = new TableUpdateV4(NS_LITERAL_CSTRING(V4_TABLE1));
+ func(update, FULL_UPDATE, "test_prefix");
+
+ ApplyUpdate(update);
+
+ // A successful V4 update should create .pset & .metadata files
+ CheckFileExist(V4_TABLE1, kFilesInV4, true);
+ }
+
+ // Apply V4 update for table2
+ {
+ auto update = new TableUpdateV4(NS_LITERAL_CSTRING(V4_TABLE2));
+ func(update, FULL_UPDATE, "test_prefix");
+
+ ApplyUpdate(update);
+
+ CheckFileExist(V4_TABLE2, kFilesInV4, true);
+ }
+
+ // Apply V4 update with the same prefix in previous full udpate
+ // This should cause an update error.
+ {
+ auto update = new TableUpdateV4(NS_LITERAL_CSTRING(V4_TABLE1));
+ func(update, PARTIAL_UPDATE, "test_prefix");
+
+ ApplyUpdate(update);
+
+ // A fail update should remove files for that table
+ CheckFileExist(V4_TABLE1, kFilesInV4, false);
+
+ // A fail update should NOT remove files for the other tables
+ CheckFileExist(V2_TABLE, kFilesInV2, true);
+ CheckFileExist(V4_TABLE2, kFilesInV4, true);
+ }
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestLookupCacheV4.cpp b/toolkit/components/url-classifier/tests/gtest/TestLookupCacheV4.cpp
new file mode 100644
index 0000000000..00525f7047
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestLookupCacheV4.cpp
@@ -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/. */
+
+#include "LookupCacheV4.h"
+#include "Common.h"
+
+#define GTEST_SAFEBROWSING_DIR NS_LITERAL_CSTRING("safebrowsing")
+#define GTEST_TABLE NS_LITERAL_CSTRING("gtest-malware-proto")
+
+typedef nsCString _Fragment;
+typedef nsTArray<nsCString> _PrefixArray;
+
+// Generate a hash prefix from string
+static const nsCString
+GeneratePrefix(const _Fragment& aFragment, uint8_t aLength)
+{
+ Completion complete;
+ nsCOMPtr<nsICryptoHash> cryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID);
+ complete.FromPlaintext(aFragment, cryptoHash);
+
+ nsCString hash;
+ hash.Assign((const char *)complete.buf, aLength);
+ return hash;
+}
+
+static UniquePtr<LookupCacheV4>
+SetupLookupCacheV4(const _PrefixArray& prefixArray)
+{
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+
+ file->AppendNative(GTEST_SAFEBROWSING_DIR);
+
+ UniquePtr<LookupCacheV4> cache = MakeUnique<LookupCacheV4>(GTEST_TABLE, EmptyCString(), file);
+ nsresult rv = cache->Init();
+ EXPECT_EQ(rv, NS_OK);
+
+ PrefixStringMap map;
+ PrefixArrayToPrefixStringMap(prefixArray, map);
+ rv = cache->Build(map);
+ EXPECT_EQ(rv, NS_OK);
+
+ return Move(cache);
+}
+
+void
+TestHasPrefix(const _Fragment& aFragment, bool aExpectedHas, bool aExpectedComplete)
+{
+ _PrefixArray array = { GeneratePrefix(_Fragment("bravo.com/"), 32),
+ GeneratePrefix(_Fragment("browsing.com/"), 8),
+ GeneratePrefix(_Fragment("gound.com/"), 5),
+ GeneratePrefix(_Fragment("small.com/"), 4)
+ };
+
+ RunTestInNewThread([&] () -> void {
+ UniquePtr<LookupCache> cache = SetupLookupCacheV4(array);
+
+ Completion lookupHash;
+ nsCOMPtr<nsICryptoHash> cryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID);
+ lookupHash.FromPlaintext(aFragment, cryptoHash);
+
+ bool has, complete;
+ nsresult rv = cache->Has(lookupHash, &has, &complete);
+
+ EXPECT_EQ(rv, NS_OK);
+ EXPECT_EQ(has, aExpectedHas);
+ EXPECT_EQ(complete, aExpectedComplete);
+
+ cache->ClearAll();
+ });
+
+}
+
+TEST(LookupCacheV4, HasComplete)
+{
+ TestHasPrefix(_Fragment("bravo.com/"), true, true);
+}
+
+TEST(LookupCacheV4, HasPrefix)
+{
+ TestHasPrefix(_Fragment("browsing.com/"), true, false);
+}
+
+TEST(LookupCacheV4, Nomatch)
+{
+ TestHasPrefix(_Fragment("nomatch.com/"), false, false);
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestPerProviderDirectory.cpp b/toolkit/components/url-classifier/tests/gtest/TestPerProviderDirectory.cpp
new file mode 100644
index 0000000000..72ff08a1e2
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestPerProviderDirectory.cpp
@@ -0,0 +1,98 @@
+#include "LookupCache.h"
+#include "LookupCacheV4.h"
+#include "HashStore.h"
+#include "gtest/gtest.h"
+#include "nsAppDirectoryServiceDefs.h"
+
+namespace mozilla {
+namespace safebrowsing {
+
+class PerProviderDirectoryTestUtils {
+public:
+ template<typename T>
+ static nsIFile* InspectStoreDirectory(const T& aT)
+ {
+ return aT.mStoreDirectory;
+ }
+};
+
+} // end of namespace safebrowsing
+} // end of namespace mozilla
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+template<typename T>
+void VerifyPrivateStorePath(const char* aTableName,
+ const char* aProvider,
+ nsIFile* aRootDir,
+ bool aUsePerProviderStore)
+{
+ nsString rootStorePath;
+ nsresult rv = aRootDir->GetPath(rootStorePath);
+ EXPECT_EQ(rv, NS_OK);
+
+ T target(nsCString(aTableName), nsCString(aProvider), aRootDir);
+
+ nsIFile* privateStoreDirectory =
+ PerProviderDirectoryTestUtils::InspectStoreDirectory(target);
+
+ nsString privateStorePath;
+ rv = privateStoreDirectory->GetPath(privateStorePath);
+ ASSERT_EQ(rv, NS_OK);
+
+ nsString expectedPrivateStorePath = rootStorePath;
+
+ if (aUsePerProviderStore) {
+ // Use API to append "provider" to the root directoy path
+ nsCOMPtr<nsIFile> expectedPrivateStoreDir;
+ rv = aRootDir->Clone(getter_AddRefs(expectedPrivateStoreDir));
+ ASSERT_EQ(rv, NS_OK);
+
+ expectedPrivateStoreDir->AppendNative(nsCString(aProvider));
+ rv = expectedPrivateStoreDir->GetPath(expectedPrivateStorePath);
+ ASSERT_EQ(rv, NS_OK);
+ }
+
+ printf("table: %s\nprovider: %s\nroot path: %s\nprivate path: %s\n\n",
+ aTableName,
+ aProvider,
+ NS_ConvertUTF16toUTF8(rootStorePath).get(),
+ NS_ConvertUTF16toUTF8(privateStorePath).get());
+
+ ASSERT_TRUE(privateStorePath == expectedPrivateStorePath);
+}
+
+TEST(PerProviderDirectory, LookupCache)
+{
+ RunTestInNewThread([] () -> void {
+ nsCOMPtr<nsIFile> rootDir;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(rootDir));
+
+ // For V2 tables (NOT ending with '-proto'), root directory should be
+ // used as the private store.
+ VerifyPrivateStorePath<LookupCacheV2>("goog-phish-shavar", "google", rootDir, false);
+
+ // For V4 tables, if provider is found, use per-provider subdirectory;
+ // If not found, use root directory.
+ VerifyPrivateStorePath<LookupCacheV4>("goog-noprovider-proto", "", rootDir, false);
+ VerifyPrivateStorePath<LookupCacheV4>("goog-phish-proto", "google4", rootDir, true);
+ });
+}
+
+TEST(PerProviderDirectory, HashStore)
+{
+ RunTestInNewThread([] () -> void {
+ nsCOMPtr<nsIFile> rootDir;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(rootDir));
+
+ // For V2 tables (NOT ending with '-proto'), root directory should be
+ // used as the private store.
+ VerifyPrivateStorePath<HashStore>("goog-phish-shavar", "google", rootDir, false);
+
+ // For V4 tables, if provider is found, use per-provider subdirectory;
+ // If not found, use root directory.
+ VerifyPrivateStorePath<HashStore>("goog-noprovider-proto", "", rootDir, false);
+ VerifyPrivateStorePath<HashStore>("goog-phish-proto", "google4", rootDir, true);
+ });
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestProtocolParser.cpp b/toolkit/components/url-classifier/tests/gtest/TestProtocolParser.cpp
new file mode 100644
index 0000000000..ea6ffb5e68
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestProtocolParser.cpp
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#include "gtest/gtest.h"
+#include "ProtocolParser.h"
+#include "mozilla/EndianUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+typedef FetchThreatListUpdatesResponse_ListUpdateResponse ListUpdateResponse;
+
+static bool
+InitUpdateResponse(ListUpdateResponse* aUpdateResponse,
+ ThreatType aThreatType,
+ const nsACString& aState,
+ const nsACString& aChecksum,
+ bool isFullUpdate,
+ const nsTArray<uint32_t>& aFixedLengthPrefixes,
+ bool aDoPrefixEncoding);
+
+static void
+DumpBinary(const nsACString& aBinary);
+
+TEST(ProtocolParser, UpdateWait)
+{
+ // Top level response which contains a list of update response
+ // for different lists.
+ FetchThreatListUpdatesResponse response;
+
+ auto r = response.mutable_list_update_responses()->Add();
+ InitUpdateResponse(r, SOCIAL_ENGINEERING_PUBLIC,
+ nsCString("sta\x00te", 6),
+ nsCString("check\x0sum", 9),
+ true,
+ {0, 1, 2, 3},
+ false /* aDoPrefixEncoding */ );
+
+ // Set min wait duration.
+ auto minWaitDuration = response.mutable_minimum_wait_duration();
+ minWaitDuration->set_seconds(8);
+ minWaitDuration->set_nanos(1 * 1000000000);
+
+ std::string s;
+ response.SerializeToString(&s);
+
+ DumpBinary(nsCString(s.c_str(), s.length()));
+
+ ProtocolParser* p = new ProtocolParserProtobuf();
+ p->AppendStream(nsCString(s.c_str(), s.length()));
+ p->End();
+ ASSERT_EQ(p->UpdateWaitSec(), 9u);
+ delete p;
+}
+
+TEST(ProtocolParser, SingleValueEncoding)
+{
+ // Top level response which contains a list of update response
+ // for different lists.
+ FetchThreatListUpdatesResponse response;
+
+ auto r = response.mutable_list_update_responses()->Add();
+
+ const char* expectedPrefix = "\x00\x01\x02\x00";
+ if (!InitUpdateResponse(r, SOCIAL_ENGINEERING_PUBLIC,
+ nsCString("sta\x00te", 6),
+ nsCString("check\x0sum", 9),
+ true,
+ // As per spec, we should interpret the prefix as uint32
+ // in little endian before encoding.
+ {LittleEndian::readUint32(expectedPrefix)},
+ true /* aDoPrefixEncoding */ )) {
+ printf("Failed to initialize update response.");
+ ASSERT_TRUE(false);
+ return;
+ }
+
+ // Set min wait duration.
+ auto minWaitDuration = response.mutable_minimum_wait_duration();
+ minWaitDuration->set_seconds(8);
+ minWaitDuration->set_nanos(1 * 1000000000);
+
+ std::string s;
+ response.SerializeToString(&s);
+
+ // Feed data to the protocol parser.
+ ProtocolParser* p = new ProtocolParserProtobuf();
+ p->SetRequestedTables({ nsCString("googpub-phish-proto") });
+ p->AppendStream(nsCString(s.c_str(), s.length()));
+ p->End();
+
+ auto& tus = p->GetTableUpdates();
+ auto tuv4 = TableUpdate::Cast<TableUpdateV4>(tus[0]);
+ auto& prefixMap = tuv4->Prefixes();
+ for (auto iter = prefixMap.Iter(); !iter.Done(); iter.Next()) {
+ // This prefix map should contain only a single 4-byte prefixe.
+ ASSERT_EQ(iter.Key(), 4u);
+
+ // The fixed-length prefix string from ProtcolParser should
+ // exactly match the expected prefix string.
+ auto& prefix = iter.Data()->GetPrefixString();
+ ASSERT_TRUE(prefix.Equals(nsCString(expectedPrefix, 4)));
+ }
+
+ delete p;
+}
+
+static bool
+InitUpdateResponse(ListUpdateResponse* aUpdateResponse,
+ ThreatType aThreatType,
+ const nsACString& aState,
+ const nsACString& aChecksum,
+ bool isFullUpdate,
+ const nsTArray<uint32_t>& aFixedLengthPrefixes,
+ bool aDoPrefixEncoding)
+{
+ aUpdateResponse->set_threat_type(aThreatType);
+ aUpdateResponse->set_new_client_state(aState.BeginReading(), aState.Length());
+ aUpdateResponse->mutable_checksum()->set_sha256(aChecksum.BeginReading(), aChecksum.Length());
+ aUpdateResponse->set_response_type(isFullUpdate ? ListUpdateResponse::FULL_UPDATE
+ : ListUpdateResponse::PARTIAL_UPDATE);
+
+ auto additions = aUpdateResponse->mutable_additions()->Add();
+
+ if (!aDoPrefixEncoding) {
+ additions->set_compression_type(RAW);
+ auto rawHashes = additions->mutable_raw_hashes();
+ rawHashes->set_prefix_size(4);
+ auto prefixes = rawHashes->mutable_raw_hashes();
+ for (auto p : aFixedLengthPrefixes) {
+ char buffer[4];
+ NativeEndian::copyAndSwapToBigEndian(buffer, &p, 1);
+ prefixes->append(buffer, 4);
+ }
+ return true;
+ }
+
+ if (1 != aFixedLengthPrefixes.Length()) {
+ printf("This function only supports single value encoding.\n");
+ return false;
+ }
+
+ uint32_t firstValue = aFixedLengthPrefixes[0];
+ additions->set_compression_type(RICE);
+ auto riceHashes = additions->mutable_rice_hashes();
+ riceHashes->set_first_value(firstValue);
+ riceHashes->set_num_entries(0);
+
+ return true;
+}
+
+static void DumpBinary(const nsACString& aBinary)
+{
+ nsCString s;
+ for (size_t i = 0; i < aBinary.Length(); i++) {
+ s.AppendPrintf("\\x%.2X", (uint8_t)aBinary[i]);
+ }
+ printf("%s\n", s.get());
+} \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/gtest/TestRiceDeltaDecoder.cpp b/toolkit/components/url-classifier/tests/gtest/TestRiceDeltaDecoder.cpp
new file mode 100644
index 0000000000..f03d27358a
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestRiceDeltaDecoder.cpp
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#include "gtest/gtest.h"
+#include "RiceDeltaDecoder.h"
+#include "mozilla/ArrayUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+struct TestingData {
+ std::vector<uint32_t> mExpectedDecoded;
+ std::vector<uint8_t> mEncoded;
+ uint32_t mRiceParameter;
+};
+
+static bool runOneTest(TestingData& aData);
+
+TEST(RiceDeltaDecoder, SingleEncodedValue) {
+ TestingData td = { { 99 }, { 99 }, 0 };
+
+ ASSERT_TRUE(runOneTest(td));
+}
+
+// In this batch of tests, the encoded data would be like
+// what we originally receive from the network. See comment
+// in |runOneTest| for more detail.
+TEST(RiceDeltaDecoder, Empty) {
+
+ // The following structure and testing data is copied from Chromium source code:
+ //
+ // https://chromium.googlesource.com/chromium/src.git/+/950f9975599768b6a08c7146cb4befa161be87aa/components/safe_browsing_db/v4_rice_unittest.cc#75
+ //
+ // and will be translated to our own testing format.
+
+ struct RiceDecodingTestInfo {
+ uint32_t mRiceParameter;
+ std::vector<uint32_t> mDeltas;
+ std::string mEncoded;
+
+ RiceDecodingTestInfo(uint32_t aRiceParameter,
+ const std::vector<uint32_t>& aDeltas,
+ const std::string& aEncoded)
+ : mRiceParameter(aRiceParameter)
+ , mDeltas(aDeltas)
+ , mEncoded(aEncoded)
+ {
+ }
+ };
+
+ // Copyright 2016 The Chromium Authors. All rights reserved.
+ // Use of this source code is governed by a BSD-style license that can be
+ // found in the media/webrtc/trunk/webrtc/LICENSE.
+
+ // ----- Start of Chromium test code ----
+ const std::vector<RiceDecodingTestInfo> TESTING_DATA_CHROMIUM = {
+ RiceDecodingTestInfo(2, {15, 9}, "\xf7\x2"),
+ RiceDecodingTestInfo(
+ 28, {1777762129, 2093280223, 924369848},
+ "\xbf\xa8\x3f\xfb\xfc\xfb\x5e\x27\xe6\xc3\x1d\xc6\x38"),
+ RiceDecodingTestInfo(
+ 28, {62763050, 1046523781, 192522171, 1800511020, 4442775, 582142548},
+ "\x54\x60\x7b\xe7\x0a\x5f\xc1\xdc\xee\x69\xde"
+ "\xfe\x58\x3c\xa3\xd6\xa5\xf2\x10\x8c\x4a\x59"
+ "\x56\x00"),
+ RiceDecodingTestInfo(
+ 28, {26067715, 344823336, 8420095, 399843890, 95029378, 731622412,
+ 35811335, 1047558127, 1117722715, 78698892},
+ "\x06\x86\x1b\x23\x14\xcb\x46\xf2\xaf\x07\x08\xc9\x88\x54\x1f\x41\x04"
+ "\xd5\x1a\x03\xeb\xe6\x3a\x80\x13\x91\x7b\xbf\x83\xf3\xb7\x85\xf1\x29"
+ "\x18\xb3\x61\x09"),
+ RiceDecodingTestInfo(
+ 27, {225846818, 328287420, 166748623, 29117720, 552397365, 350353215,
+ 558267528, 4738273, 567093445, 28563065, 55077698, 73091685,
+ 339246010, 98242620, 38060941, 63917830, 206319759, 137700744},
+ "\x89\x98\xd8\x75\xbc\x44\x91\xeb\x39\x0c\x3e\x30\x9a\x78\xf3\x6a\xd4"
+ "\xd9\xb1\x9f\xfb\x70\x3e\x44\x3e\xa3\x08\x67\x42\xc2\x2b\x46\x69\x8e"
+ "\x3c\xeb\xd9\x10\x5a\x43\x9a\x32\xa5\x2d\x4e\x77\x0f\x87\x78\x20\xb6"
+ "\xab\x71\x98\x48\x0c\x9e\x9e\xd7\x23\x0c\x13\x43\x2c\xa9\x01"),
+ RiceDecodingTestInfo(
+ 28, {339784008, 263128563, 63871877, 69723256, 826001074, 797300228,
+ 671166008, 207712688},
+ std::string("\x21\xc5\x02\x91\xf9\x82\xd7\x57\xb8\xe9\x3c\xf0\xc8\x4f"
+ "\xe8\x64\x8d\x77\x62\x04\xd6\x85\x3f\x1c\x97\x00\x04\x1b"
+ "\x17\xc6",
+ 30)),
+ RiceDecodingTestInfo(
+ 28, {471820069, 196333855, 855579133, 122737976, 203433838, 85354544,
+ 1307949392, 165938578, 195134475, 553930435, 49231136},
+ "\x95\x9c\x7d\xb0\x8f\xe8\xd9\xbd\xfe\x8c\x7f\x81\x53\x0d\x75\xdc\x4e"
+ "\x40\x18\x0c\x9a\x45\x3d\xa8\xdc\xfa\x26\x59\x40\x9e\x16\x08\x43\x77"
+ "\xc3\x4e\x04\x01\xa4\xe6\x5d\x00"),
+ RiceDecodingTestInfo(
+ 27, {87336845, 129291033, 30906211, 433549264, 30899891, 53207875,
+ 11959529, 354827862, 82919275, 489637251, 53561020, 336722992,
+ 408117728, 204506246, 188216092, 9047110, 479817359, 230317256},
+ "\x1a\x4f\x69\x2a\x63\x9a\xf6\xc6\x2e\xaf\x73\xd0\x6f\xd7\x31\xeb\x77"
+ "\x1d\x43\xe3\x2b\x93\xce\x67\x8b\x59\xf9\x98\xd4\xda\x4f\x3c\x6f\xb0"
+ "\xe8\xa5\x78\x8d\x62\x36\x18\xfe\x08\x1e\x78\xd8\x14\x32\x24\x84\x61"
+ "\x1c\xf3\x37\x63\xc4\xa0\x88\x7b\x74\xcb\x64\xc8\x5c\xba\x05"),
+ RiceDecodingTestInfo(
+ 28, {297968956, 19709657, 259702329, 76998112, 1023176123, 29296013,
+ 1602741145, 393745181, 177326295, 55225536, 75194472},
+ "\xf1\x94\x0a\x87\x6c\x5f\x96\x90\xe3\xab\xf7\xc0\xcb\x2d\xe9\x76\xdb"
+ "\xf8\x59\x63\xc1\x6f\x7c\x99\xe3\x87\x5f\xc7\x04\xde\xb9\x46\x8e\x54"
+ "\xc0\xac\x4a\x03\x0d\x6c\x8f\x00"),
+ RiceDecodingTestInfo(
+ 28, {532220688, 780594691, 436816483, 163436269, 573044456, 1069604,
+ 39629436, 211410997, 227714491, 381562898, 75610008, 196754597,
+ 40310339, 15204118, 99010842},
+ "\x41\x2c\xe4\xfe\x06\xdc\x0d\xbd\x31\xa5\x04\xd5\x6e\xdd\x9b\x43\xb7"
+ "\x3f\x11\x24\x52\x10\x80\x4f\x96\x4b\xd4\x80\x67\xb2\xdd\x52\xc9\x4e"
+ "\x02\xc6\xd7\x60\xde\x06\x92\x52\x1e\xdd\x35\x64\x71\x26\x2c\xfe\xcf"
+ "\x81\x46\xb2\x79\x01"),
+ RiceDecodingTestInfo(
+ 28, {219354713, 389598618, 750263679, 554684211, 87381124, 4523497,
+ 287633354, 801308671, 424169435, 372520475, 277287849},
+ "\xb2\x2c\x26\x3a\xcd\x66\x9c\xdb\x5f\x07\x2e\x6f\xe6\xf9\x21\x10\x52"
+ "\xd5\x94\xf4\x82\x22\x48\xf9\x9d\x24\xf6\xff\x2f\xfc\x6d\x3f\x21\x65"
+ "\x1b\x36\x34\x56\xea\xc4\x21\x00"),
+ };
+
+ // ----- End of Chromium test code ----
+
+ for (auto tdc : TESTING_DATA_CHROMIUM) {
+ // Populate chromium testing data to our native testing data struct.
+ TestingData d;
+
+ d.mRiceParameter = tdc.mRiceParameter; // Populate rice parameter.
+
+ // Populate encoded data from std::string to vector<uint8>.
+ d.mEncoded.resize(tdc.mEncoded.size());
+ memcpy(&d.mEncoded[0], tdc.mEncoded.c_str(), tdc.mEncoded.size());
+
+ // Populate deltas to expected decoded data. The first value would be just
+ // set to an arbitrary value, say 7, to avoid any assumption to the
+ // first value in the implementation.
+ d.mExpectedDecoded.resize(tdc.mDeltas.size() + 1);
+ for (size_t i = 0; i < d.mExpectedDecoded.size(); i++) {
+ if (0 == i) {
+ d.mExpectedDecoded[i] = 7; // "7" is an arbitrary starting value
+ } else {
+ d.mExpectedDecoded[i] = d.mExpectedDecoded[i - 1] + tdc.mDeltas[i - 1];
+ }
+ }
+
+ ASSERT_TRUE(runOneTest(d));
+ }
+}
+
+static bool
+runOneTest(TestingData& aData)
+{
+ RiceDeltaDecoder decoder(&aData.mEncoded[0], aData.mEncoded.size());
+
+ std::vector<uint32_t> decoded(aData.mExpectedDecoded.size());
+
+ uint32_t firstValue = aData.mExpectedDecoded[0];
+ bool rv = decoder.Decode(aData.mRiceParameter,
+ firstValue,
+ decoded.size() - 1, // # of entries (first value not included).
+ &decoded[0]);
+
+ return rv && decoded == aData.mExpectedDecoded;
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestSafeBrowsingProtobuf.cpp b/toolkit/components/url-classifier/tests/gtest/TestSafeBrowsingProtobuf.cpp
new file mode 100644
index 0000000000..fe6f28960a
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestSafeBrowsingProtobuf.cpp
@@ -0,0 +1,24 @@
+#include "safebrowsing.pb.h"
+#include "gtest/gtest.h"
+
+TEST(SafeBrowsingProtobuf, Empty)
+{
+ using namespace mozilla::safebrowsing;
+
+ const std::string CLIENT_ID = "firefox";
+
+ // Construct a simple update request.
+ FetchThreatListUpdatesRequest r;
+ r.set_allocated_client(new ClientInfo());
+ r.mutable_client()->set_client_id(CLIENT_ID);
+
+ // Then serialize.
+ std::string s;
+ r.SerializeToString(&s);
+
+ // De-serialize.
+ FetchThreatListUpdatesRequest r2;
+ r2.ParseFromString(s);
+
+ ASSERT_EQ(r2.client().client_id(), CLIENT_ID);
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestSafebrowsingHash.cpp b/toolkit/components/url-classifier/tests/gtest/TestSafebrowsingHash.cpp
new file mode 100644
index 0000000000..89ed74be6d
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestSafebrowsingHash.cpp
@@ -0,0 +1,52 @@
+#include "Entries.h"
+#include "mozilla/EndianUtils.h"
+
+TEST(SafebrowsingHash, ToFromUint32)
+{
+ using namespace mozilla::safebrowsing;
+
+ // typedef SafebrowsingHash<PREFIX_SIZE, PrefixComparator> Prefix;
+ // typedef nsTArray<Prefix> PrefixArray;
+
+ const char PREFIX_RAW[4] = { 0x1, 0x2, 0x3, 0x4 };
+ uint32_t PREFIX_UINT32;
+ memcpy(&PREFIX_UINT32, PREFIX_RAW, 4);
+
+ Prefix p;
+ p.Assign(nsCString(PREFIX_RAW, 4));
+ ASSERT_EQ(p.ToUint32(), PREFIX_UINT32);
+
+ p.FromUint32(PREFIX_UINT32);
+ ASSERT_EQ(memcmp(PREFIX_RAW, p.buf, 4), 0);
+}
+
+TEST(SafebrowsingHash, Compare)
+{
+ using namespace mozilla;
+ using namespace mozilla::safebrowsing;
+
+ Prefix p1, p2, p3;
+
+ // The order of p1,p2,p3 is "p1 == p3 < p2"
+#if MOZ_LITTLE_ENDIAN
+ p1.Assign(nsCString("\x01\x00\x00\x00", 4));
+ p2.Assign(nsCString("\x00\x00\x00\x01", 4));
+ p3.Assign(nsCString("\x01\x00\x00\x00", 4));
+#else
+ p1.Assign(nsCString("\x00\x00\x00\x01", 4));
+ p2.Assign(nsCString("\x01\x00\x00\x00", 4));
+ p3.Assign(nsCString("\x00\x00\x00\x01", 4));
+#endif
+
+ // Make sure "p1 == p3 < p2" is true
+ // on both little and big endian machine.
+
+ ASSERT_EQ(p1.Compare(p2), -1);
+ ASSERT_EQ(p1.Compare(p1), 0);
+ ASSERT_EQ(p2.Compare(p1), 1);
+ ASSERT_EQ(p1.Compare(p3), 0);
+
+ ASSERT_TRUE(p1 < p2);
+ ASSERT_TRUE(p1 == p1);
+ ASSERT_TRUE(p1 == p3);
+} \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/gtest/TestTable.cpp b/toolkit/components/url-classifier/tests/gtest/TestTable.cpp
new file mode 100644
index 0000000000..307587459b
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestTable.cpp
@@ -0,0 +1,47 @@
+#include "gtest/gtest.h"
+#include "nsUrlClassifierDBService.h"
+
+using namespace mozilla::safebrowsing;
+
+void
+TestResponseCode(const char* table, nsresult result)
+{
+ nsCString tableName(table);
+ ASSERT_EQ(TablesToResponse(tableName), result);
+}
+
+TEST(UrlClassifierTable, ResponseCode)
+{
+ // malware URIs.
+ TestResponseCode("goog-malware-shavar", NS_ERROR_MALWARE_URI);
+ TestResponseCode("test-malware-simple", NS_ERROR_MALWARE_URI);
+ TestResponseCode("goog-phish-shavar,test-malware-simple", NS_ERROR_MALWARE_URI);
+ TestResponseCode("test-malware-simple,mozstd-track-digest256,mozplugin-block-digest256", NS_ERROR_MALWARE_URI);
+
+ // phish URIs.
+ TestResponseCode("goog-phish-shavar", NS_ERROR_PHISHING_URI);
+ TestResponseCode("test-phish-simple", NS_ERROR_PHISHING_URI);
+ TestResponseCode("test-phish-simple,mozplugin-block-digest256", NS_ERROR_PHISHING_URI);
+ TestResponseCode("mozstd-track-digest256,test-phish-simple,goog-unwanted-shavar", NS_ERROR_PHISHING_URI);
+
+ // unwanted URIs.
+ TestResponseCode("goog-unwanted-shavar", NS_ERROR_UNWANTED_URI);
+ TestResponseCode("test-unwanted-simple", NS_ERROR_UNWANTED_URI);
+ TestResponseCode("mozplugin-unwanted-digest256,mozfull-track-digest256", NS_ERROR_UNWANTED_URI);
+ TestResponseCode("test-block-simple,mozfull-track-digest256,test-unwanted-simple", NS_ERROR_UNWANTED_URI);
+
+ // track URIs.
+ TestResponseCode("test-track-simple", NS_ERROR_TRACKING_URI);
+ TestResponseCode("mozstd-track-digest256", NS_ERROR_TRACKING_URI);
+ TestResponseCode("test-block-simple,mozstd-track-digest256", NS_ERROR_TRACKING_URI);
+
+ // block URIs
+ TestResponseCode("test-block-simple", NS_ERROR_BLOCKED_URI);
+ TestResponseCode("mozplugin-block-digest256", NS_ERROR_BLOCKED_URI);
+ TestResponseCode("mozplugin2-block-digest256", NS_ERROR_BLOCKED_URI);
+
+ TestResponseCode("test-trackwhite-simple", NS_OK);
+ TestResponseCode("mozstd-trackwhite-digest256", NS_OK);
+ TestResponseCode("goog-badbinurl-shavar", NS_OK);
+ TestResponseCode("goog-downloadwhite-digest256", NS_OK);
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestUrlClassifierTableUpdateV4.cpp b/toolkit/components/url-classifier/tests/gtest/TestUrlClassifierTableUpdateV4.cpp
new file mode 100644
index 0000000000..470a88ba25
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestUrlClassifierTableUpdateV4.cpp
@@ -0,0 +1,755 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#include "Common.h"
+#include "Classifier.h"
+#include "HashStore.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIFile.h"
+#include "nsIThread.h"
+#include "string.h"
+#include "gtest/gtest.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::safebrowsing;
+
+typedef nsCString _Prefix;
+typedef nsTArray<_Prefix> _PrefixArray;
+
+#define GTEST_SAFEBROWSING_DIR NS_LITERAL_CSTRING("safebrowsing")
+#define GTEST_TABLE NS_LITERAL_CSTRING("gtest-malware-proto")
+#define GTEST_PREFIXFILE NS_LITERAL_CSTRING("gtest-malware-proto.pset")
+
+// This function removes common elements of inArray and outArray from
+// outArray. This is used by partial update testcase to ensure partial update
+// data won't contain prefixes we already have.
+static void
+RemoveIntersection(const _PrefixArray& inArray, _PrefixArray& outArray)
+{
+ for (uint32_t i = 0; i < inArray.Length(); i++) {
+ int32_t idx = outArray.BinaryIndexOf(inArray[i]);
+ if (idx >= 0) {
+ outArray.RemoveElementAt(idx);
+ }
+ }
+}
+
+// This fucntion removes elements from outArray by index specified in
+// removal array.
+static void
+RemoveElements(const nsTArray<uint32_t>& removal, _PrefixArray& outArray)
+{
+ for (int32_t i = removal.Length() - 1; i >= 0; i--) {
+ outArray.RemoveElementAt(removal[i]);
+ }
+}
+
+static void
+MergeAndSortArray(const _PrefixArray& array1,
+ const _PrefixArray& array2,
+ _PrefixArray& output)
+{
+ output.Clear();
+ output.AppendElements(array1);
+ output.AppendElements(array2);
+ output.Sort();
+}
+
+static void
+CalculateCheckSum(_PrefixArray& prefixArray, nsCString& checksum)
+{
+ prefixArray.Sort();
+
+ nsresult rv;
+ nsCOMPtr<nsICryptoHash> cryptoHash =
+ do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+
+ cryptoHash->Init(nsICryptoHash::SHA256);
+ for (uint32_t i = 0; i < prefixArray.Length(); i++) {
+ const _Prefix& prefix = prefixArray[i];
+ cryptoHash->Update(reinterpret_cast<uint8_t*>(
+ const_cast<char*>(prefix.get())), prefix.Length());
+ }
+ cryptoHash->Finish(false, checksum);
+}
+
+// N: Number of prefixes, MIN/MAX: minimum/maximum prefix size
+// This function will append generated prefixes to outArray.
+static void
+CreateRandomSortedPrefixArray(uint32_t N,
+ uint32_t MIN,
+ uint32_t MAX,
+ _PrefixArray& outArray)
+{
+ outArray.SetCapacity(outArray.Length() + N);
+
+ const uint32_t range = (MAX - MIN + 1);
+
+ for (uint32_t i = 0; i < N; i++) {
+ uint32_t prefixSize = (rand() % range) + MIN;
+ _Prefix prefix;
+ prefix.SetLength(prefixSize);
+
+ while (true) {
+ char* dst = prefix.BeginWriting();
+ for (uint32_t j = 0; j < prefixSize; j++) {
+ dst[j] = rand() % 256;
+ }
+
+ if (!outArray.Contains(prefix)) {
+ outArray.AppendElement(prefix);
+ break;
+ }
+ }
+ }
+
+ outArray.Sort();
+}
+
+// N: Number of removal indices, MAX: maximum index
+static void
+CreateRandomRemovalIndices(uint32_t N,
+ uint32_t MAX,
+ nsTArray<uint32_t>& outArray)
+{
+ for (uint32_t i = 0; i < N; i++) {
+ uint32_t idx = rand() % MAX;
+ if (!outArray.Contains(idx)) {
+ outArray.InsertElementSorted(idx);
+ }
+ }
+}
+
+// Function to generate TableUpdateV4.
+static void
+GenerateUpdateData(bool fullUpdate,
+ PrefixStringMap& add,
+ nsTArray<uint32_t>* removal,
+ nsCString* checksum,
+ nsTArray<TableUpdate*>& tableUpdates)
+{
+ TableUpdateV4* tableUpdate = new TableUpdateV4(GTEST_TABLE);
+ tableUpdate->SetFullUpdate(fullUpdate);
+
+ for (auto iter = add.ConstIter(); !iter.Done(); iter.Next()) {
+ nsCString* pstring = iter.Data();
+ std::string str(pstring->BeginReading(), pstring->Length());
+
+ tableUpdate->NewPrefixes(iter.Key(), str);
+ }
+
+ if (removal) {
+ tableUpdate->NewRemovalIndices(removal->Elements(), removal->Length());
+ }
+
+ if (checksum) {
+ std::string stdChecksum;
+ stdChecksum.assign(const_cast<char*>(checksum->BeginReading()), checksum->Length());
+
+ tableUpdate->NewChecksum(stdChecksum);
+ }
+
+ tableUpdates.AppendElement(tableUpdate);
+}
+
+static void
+VerifyPrefixSet(PrefixStringMap& expected)
+{
+ // Verify the prefix set is written to disk.
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+
+ file->AppendNative(GTEST_SAFEBROWSING_DIR);
+ file->AppendNative(GTEST_PREFIXFILE);
+
+ RefPtr<VariableLengthPrefixSet> load = new VariableLengthPrefixSet;
+ load->Init(GTEST_TABLE);
+
+ PrefixStringMap prefixesInFile;
+ load->LoadFromFile(file);
+ load->GetPrefixes(prefixesInFile);
+
+ for (auto iter = expected.ConstIter(); !iter.Done(); iter.Next()) {
+ nsCString* expectedPrefix = iter.Data();
+ nsCString* resultPrefix = prefixesInFile.Get(iter.Key());
+
+ ASSERT_TRUE(*resultPrefix == *expectedPrefix);
+ }
+}
+
+static void
+Clear()
+{
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+
+ UniquePtr<Classifier> classifier(new Classifier());
+ classifier->Open(*file);
+ classifier->Reset();
+}
+
+static void
+testUpdateFail(nsTArray<TableUpdate*>& tableUpdates)
+{
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+
+ UniquePtr<Classifier> classifier(new Classifier());
+ classifier->Open(*file);
+
+ RunTestInNewThread([&] () -> void {
+ nsresult rv = classifier->ApplyUpdates(&tableUpdates);
+ ASSERT_TRUE(NS_FAILED(rv));
+ });
+}
+
+static void
+testUpdate(nsTArray<TableUpdate*>& tableUpdates,
+ PrefixStringMap& expected)
+{
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+
+ UniquePtr<Classifier> classifier(new Classifier());
+ classifier->Open(*file);
+
+ RunTestInNewThread([&] () -> void {
+ nsresult rv = classifier->ApplyUpdates(&tableUpdates);
+ ASSERT_TRUE(rv == NS_OK);
+
+ VerifyPrefixSet(expected);
+ });
+}
+
+static void
+testFullUpdate(PrefixStringMap& add, nsCString* checksum)
+{
+ nsTArray<TableUpdate*> tableUpdates;
+
+ GenerateUpdateData(true, add, nullptr, checksum, tableUpdates);
+
+ testUpdate(tableUpdates, add);
+}
+
+static void
+testPartialUpdate(PrefixStringMap& add,
+ nsTArray<uint32_t>* removal,
+ nsCString* checksum,
+ PrefixStringMap& expected)
+{
+ nsTArray<TableUpdate*> tableUpdates;
+ GenerateUpdateData(false, add, removal, checksum, tableUpdates);
+
+ testUpdate(tableUpdates, expected);
+}
+
+static void
+testOpenLookupCache()
+{
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+ file->AppendNative(GTEST_SAFEBROWSING_DIR);
+
+ RunTestInNewThread([&] () -> void {
+ LookupCacheV4 cache(nsCString(GTEST_TABLE), EmptyCString(), file);
+ nsresult rv = cache.Init();
+ ASSERT_EQ(rv, NS_OK);
+
+ rv = cache.Open();
+ ASSERT_EQ(rv, NS_OK);
+ });
+}
+
+// Tests start from here.
+TEST(UrlClassifierTableUpdateV4, FixLenghtPSetFullUpdate)
+{
+ srand(time(NULL));
+
+ _PrefixArray array;
+ PrefixStringMap map;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 4, 4, array);
+ PrefixArrayToPrefixStringMap(array, map);
+ CalculateCheckSum(array, checksum);
+
+ testFullUpdate(map, &checksum);
+
+ Clear();
+}
+
+TEST(UrlClassifierTableUpdateV4, VariableLenghtPSetFullUpdate)
+{
+ _PrefixArray array;
+ PrefixStringMap map;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 5, 32, array);
+ PrefixArrayToPrefixStringMap(array, map);
+ CalculateCheckSum(array, checksum);
+
+ testFullUpdate(map, &checksum);
+
+ Clear();
+}
+
+// This test contain both variable length prefix set and fixed-length prefix set
+TEST(UrlClassifierTableUpdateV4, MixedPSetFullUpdate)
+{
+ _PrefixArray array;
+ PrefixStringMap map;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 4, 4, array);
+ CreateRandomSortedPrefixArray(1000, 5, 32, array);
+ PrefixArrayToPrefixStringMap(array, map);
+ CalculateCheckSum(array, checksum);
+
+ testFullUpdate(map, &checksum);
+
+ Clear();
+}
+
+TEST(UrlClassifierTableUpdateV4, PartialUpdateWithRemoval)
+{
+ _PrefixArray fArray;
+
+ // Apply a full update first.
+ {
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(10000, 4, 4, fArray);
+ CreateRandomSortedPrefixArray(2000, 5, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply a partial update with removal.
+ {
+ _PrefixArray pArray, mergedArray;
+ PrefixStringMap pMap, mergedMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ // Remove 1/5 of elements of original prefix set.
+ nsTArray<uint32_t> removal;
+ CreateRandomRemovalIndices(fArray.Length() / 5, fArray.Length(), removal);
+ RemoveElements(removal, fArray);
+
+ // Calculate the expected prefix map.
+ MergeAndSortArray(fArray, pArray, mergedArray);
+ PrefixArrayToPrefixStringMap(mergedArray, mergedMap);
+ CalculateCheckSum(mergedArray, checksum);
+
+ testPartialUpdate(pMap, &removal, &checksum, mergedMap);
+ }
+
+ Clear();
+}
+
+TEST(UrlClassifierTableUpdateV4, PartialUpdateWithoutRemoval)
+{
+ _PrefixArray fArray;
+
+ // Apply a full update first.
+ {
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(10000, 4, 4, fArray);
+ CreateRandomSortedPrefixArray(2000, 5, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply a partial update without removal
+ {
+ _PrefixArray pArray, mergedArray;
+ PrefixStringMap pMap, mergedMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ // Calculate the expected prefix map.
+ MergeAndSortArray(fArray, pArray, mergedArray);
+ PrefixArrayToPrefixStringMap(mergedArray, mergedMap);
+ CalculateCheckSum(mergedArray, checksum);
+
+ testPartialUpdate(pMap, nullptr, &checksum, mergedMap);
+ }
+
+ Clear();
+}
+
+// Expect failure because partial update contains prefix already
+// in old prefix set.
+TEST(UrlClassifierTableUpdateV4, PartialUpdatePrefixAlreadyExist)
+{
+ _PrefixArray fArray;
+
+ // Apply a full update fist.
+ {
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(1000, 4, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply a partial update which contains a prefix in previous full update.
+ // This should cause an update error.
+ {
+ _PrefixArray pArray;
+ PrefixStringMap pMap;
+ nsTArray<TableUpdate*> tableUpdates;
+
+ // Pick one prefix from full update prefix and add it to partial update.
+ // This should result a failure when call ApplyUpdates.
+ pArray.AppendElement(fArray[rand() % fArray.Length()]);
+ CreateRandomSortedPrefixArray(200, 4, 32, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ GenerateUpdateData(false, pMap, nullptr, nullptr, tableUpdates);
+ testUpdateFail(tableUpdates);
+ }
+
+ Clear();
+}
+
+// Test apply partial update directly without applying an full update first.
+TEST(UrlClassifierTableUpdateV4, OnlyPartialUpdate)
+{
+ _PrefixArray pArray;
+ PrefixStringMap pMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+ CalculateCheckSum(pArray, checksum);
+
+ testPartialUpdate(pMap, nullptr, &checksum, pMap);
+
+ Clear();
+}
+
+// Test partial update without any ADD prefixes, only removalIndices.
+TEST(UrlClassifierTableUpdateV4, PartialUpdateOnlyRemoval)
+{
+ _PrefixArray fArray;
+
+ // Apply a full update first.
+ {
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(5000, 4, 4, fArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply a partial update without add prefix, only contain removal indices.
+ {
+ _PrefixArray pArray;
+ PrefixStringMap pMap, mergedMap;
+ nsCString checksum;
+
+ // Remove 1/5 of elements of original prefix set.
+ nsTArray<uint32_t> removal;
+ CreateRandomRemovalIndices(fArray.Length() / 5, fArray.Length(), removal);
+ RemoveElements(removal, fArray);
+
+ PrefixArrayToPrefixStringMap(fArray, mergedMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testPartialUpdate(pMap, &removal, &checksum, mergedMap);
+ }
+
+ Clear();
+}
+
+// Test one tableupdate array contains full update and multiple partial updates.
+TEST(UrlClassifierTableUpdateV4, MultipleTableUpdates)
+{
+ _PrefixArray fArray, pArray, mergedArray;
+ PrefixStringMap fMap, pMap, mergedMap;
+ nsCString checksum;
+
+ nsTArray<TableUpdate*> tableUpdates;
+
+ // Generate first full udpate
+ CreateRandomSortedPrefixArray(10000, 4, 4, fArray);
+ CreateRandomSortedPrefixArray(2000, 5, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ GenerateUpdateData(true, fMap, nullptr, &checksum, tableUpdates);
+
+ // Generate second partial update
+ CreateRandomSortedPrefixArray(3000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ MergeAndSortArray(fArray, pArray, mergedArray);
+ CalculateCheckSum(mergedArray, checksum);
+
+ GenerateUpdateData(false, pMap, nullptr, &checksum, tableUpdates);
+
+ // Generate thrid partial update
+ fArray.AppendElements(pArray);
+ fArray.Sort();
+ pArray.Clear();
+ CreateRandomSortedPrefixArray(3000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ // Remove 1/5 of elements of original prefix set.
+ nsTArray<uint32_t> removal;
+ CreateRandomRemovalIndices(fArray.Length() / 5, fArray.Length(), removal);
+ RemoveElements(removal, fArray);
+
+ MergeAndSortArray(fArray, pArray, mergedArray);
+ PrefixArrayToPrefixStringMap(mergedArray, mergedMap);
+ CalculateCheckSum(mergedArray, checksum);
+
+ GenerateUpdateData(false, pMap, &removal, &checksum, tableUpdates);
+
+ testUpdate(tableUpdates, mergedMap);
+
+ Clear();
+}
+
+// Test apply full update first, and then apply multiple partial updates
+// in one tableupdate array.
+TEST(UrlClassifierTableUpdateV4, MultiplePartialUpdateTableUpdates)
+{
+ _PrefixArray fArray;
+
+ // Apply a full update first
+ {
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ // Generate first full udpate
+ CreateRandomSortedPrefixArray(10000, 4, 4, fArray);
+ CreateRandomSortedPrefixArray(3000, 5, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply multiple partial updates in one table update
+ {
+ _PrefixArray pArray, mergedArray;
+ PrefixStringMap pMap, mergedMap;
+ nsCString checksum;
+ nsTArray<uint32_t> removal;
+ nsTArray<TableUpdate*> tableUpdates;
+
+ // Generate first partial update
+ CreateRandomSortedPrefixArray(3000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ // Remove 1/5 of elements of original prefix set.
+ CreateRandomRemovalIndices(fArray.Length() / 5, fArray.Length(), removal);
+ RemoveElements(removal, fArray);
+
+ MergeAndSortArray(fArray, pArray, mergedArray);
+ CalculateCheckSum(mergedArray, checksum);
+
+ GenerateUpdateData(false, pMap, &removal, &checksum, tableUpdates);
+
+ fArray.AppendElements(pArray);
+ fArray.Sort();
+ pArray.Clear();
+ removal.Clear();
+
+ // Generate second partial update.
+ CreateRandomSortedPrefixArray(2000, 4, 4, pArray);
+ CreateRandomSortedPrefixArray(1000, 5, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ // Remove 1/5 of elements of original prefix set.
+ CreateRandomRemovalIndices(fArray.Length() / 5, fArray.Length(), removal);
+ RemoveElements(removal, fArray);
+
+ MergeAndSortArray(fArray, pArray, mergedArray);
+ PrefixArrayToPrefixStringMap(mergedArray, mergedMap);
+ CalculateCheckSum(mergedArray, checksum);
+
+ GenerateUpdateData(false, pMap, &removal, &checksum, tableUpdates);
+
+ testUpdate(tableUpdates, mergedMap);
+ }
+
+ Clear();
+}
+
+// Test removal indices are larger than the original prefix set.
+TEST(UrlClassifierTableUpdateV4, RemovalIndexTooLarge)
+{
+ _PrefixArray fArray;
+
+ // Apply a full update first
+ {
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(1000, 4, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply a partial update with removal indice array larger than
+ // old prefix set(fArray). This should cause an error.
+ {
+ _PrefixArray pArray;
+ PrefixStringMap pMap;
+ nsTArray<uint32_t> removal;
+ nsTArray<TableUpdate*> tableUpdates;
+
+ CreateRandomSortedPrefixArray(200, 4, 32, pArray);
+ RemoveIntersection(fArray, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ for (uint32_t i = 0; i < fArray.Length() + 1 ;i++) {
+ removal.AppendElement(i);
+ }
+
+ GenerateUpdateData(false, pMap, &removal, nullptr, tableUpdates);
+ testUpdateFail(tableUpdates);
+ }
+
+ Clear();
+}
+
+TEST(UrlClassifierTableUpdateV4, ChecksumMismatch)
+{
+ // Apply a full update first
+ {
+ _PrefixArray fArray;
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(1000, 4, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+ }
+
+ // Apply a partial update with incorrect checksum
+ {
+ _PrefixArray pArray;
+ PrefixStringMap pMap;
+ nsCString checksum;
+ nsTArray<TableUpdate*> tableUpdates;
+
+ CreateRandomSortedPrefixArray(200, 4, 32, pArray);
+ PrefixArrayToPrefixStringMap(pArray, pMap);
+
+ // Checksum should be calculated with both old prefix set and add prefix set,
+ // here we only calculate checksum with add prefix set to check if applyUpdate
+ // will return failure.
+ CalculateCheckSum(pArray, checksum);
+
+ GenerateUpdateData(false, pMap, nullptr, &checksum, tableUpdates);
+ testUpdateFail(tableUpdates);
+ }
+
+ Clear();
+}
+
+TEST(UrlClassifierTableUpdateV4, ApplyUpdateThenLoad)
+{
+ // Apply update with checksum
+ {
+ _PrefixArray fArray;
+ PrefixStringMap fMap;
+ nsCString checksum;
+
+ CreateRandomSortedPrefixArray(1000, 4, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+ CalculateCheckSum(fArray, checksum);
+
+ testFullUpdate(fMap, &checksum);
+
+ // Open lookup cache will load prefix set and verify the checksum
+ testOpenLookupCache();
+ }
+
+ Clear();
+
+ // Apply update without checksum
+ {
+ _PrefixArray fArray;
+ PrefixStringMap fMap;
+
+ CreateRandomSortedPrefixArray(1000, 4, 32, fArray);
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+
+ testFullUpdate(fMap, nullptr);
+
+ testOpenLookupCache();
+ }
+
+ Clear();
+}
+
+// This test is used to avoid an eror from nsICryptoHash
+TEST(UrlClassifierTableUpdateV4, ApplyUpdateWithFixedChecksum)
+{
+ _PrefixArray fArray = { _Prefix("enus"), _Prefix("apollo"), _Prefix("mars"),
+ _Prefix("Hecatonchires cyclopes"),
+ _Prefix("vesta"), _Prefix("neptunus"), _Prefix("jupiter"),
+ _Prefix("diana"), _Prefix("minerva"), _Prefix("ceres"),
+ _Prefix("Aidos,Adephagia,Adikia,Aletheia"),
+ _Prefix("hecatonchires"), _Prefix("alcyoneus"), _Prefix("hades"),
+ _Prefix("vulcanus"), _Prefix("juno"), _Prefix("mercury"),
+ _Prefix("Stheno, Euryale and Medusa")
+ };
+ fArray.Sort();
+
+ PrefixStringMap fMap;
+ PrefixArrayToPrefixStringMap(fArray, fMap);
+
+ nsCString checksum("\xae\x18\x94\xd7\xd0\x83\x5f\xc1"
+ "\x58\x59\x5c\x2c\x72\xb9\x6e\x5e"
+ "\xf4\xe8\x0a\x6b\xff\x5e\x6b\x81"
+ "\x65\x34\x06\x16\x06\x59\xa0\x67");
+
+ testFullUpdate(fMap, &checksum);
+
+ // Open lookup cache will load prefix set and verify the checksum
+ testOpenLookupCache();
+
+ Clear();
+}
+
diff --git a/toolkit/components/url-classifier/tests/gtest/TestUrlClassifierUtils.cpp b/toolkit/components/url-classifier/tests/gtest/TestUrlClassifierUtils.cpp
new file mode 100644
index 0000000000..fa5ce4f56f
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestUrlClassifierUtils.cpp
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 <stdio.h>
+#include <ctype.h>
+
+#include <mozilla/RefPtr.h>
+#include "nsString.h"
+#include "nsEscape.h"
+#include "nsUrlClassifierUtils.h"
+#include "stdlib.h"
+#include "gtest/gtest.h"
+
+static char int_to_hex_digit(int32_t i) {
+ NS_ASSERTION((i >= 0) && (i <= 15), "int too big in int_to_hex_digit");
+ return static_cast<char>(((i < 10) ? (i + '0') : ((i - 10) + 'A')));
+}
+
+static void CheckEquals(nsCString& expected, nsCString& actual)
+{
+ ASSERT_TRUE((expected).Equals((actual)));
+}
+
+void TestUnescapeHelper(const char* in, const char* expected)
+{
+ nsCString out, strIn(in), strExp(expected);
+
+ NS_UnescapeURL(strIn.get(), strIn.Length(), esc_AlwaysCopy, out);
+ CheckEquals(strExp, out);
+}
+
+// Make sure Unescape from nsEncode.h's unescape does what the server does.
+TEST(UrlClassifierUtils, Unescape)
+{
+ // test empty string
+ TestUnescapeHelper("\0", "\0");
+
+ // Test docoding of all characters.
+ nsCString allCharsEncoded, allCharsEncodedLowercase, allCharsAsString;
+ for (int32_t i = 1; i < 256; ++i) {
+ allCharsEncoded.Append('%');
+ allCharsEncoded.Append(int_to_hex_digit(i / 16));
+ allCharsEncoded.Append((int_to_hex_digit(i % 16)));
+
+ allCharsEncodedLowercase.Append('%');
+ allCharsEncodedLowercase.Append(tolower(int_to_hex_digit(i / 16)));
+ allCharsEncodedLowercase.Append(tolower(int_to_hex_digit(i % 16)));
+
+ allCharsAsString.Append(static_cast<char>(i));
+ }
+
+ nsCString out;
+ NS_UnescapeURL(allCharsEncoded.get(),
+ allCharsEncoded.Length(),
+ esc_AlwaysCopy,
+ out);
+
+ CheckEquals(allCharsAsString, out);
+
+ out.Truncate();
+ NS_UnescapeURL(allCharsEncodedLowercase.get(),
+ allCharsEncodedLowercase.Length(),
+ esc_AlwaysCopy,
+ out);
+ CheckEquals(allCharsAsString, out);
+
+ // Test %-related edge cases
+ TestUnescapeHelper("%", "%");
+ TestUnescapeHelper("%xx", "%xx");
+ TestUnescapeHelper("%%", "%%");
+ TestUnescapeHelper("%%%", "%%%");
+ TestUnescapeHelper("%%%%", "%%%%");
+ TestUnescapeHelper("%1", "%1");
+ TestUnescapeHelper("%1z", "%1z");
+ TestUnescapeHelper("a%1z", "a%1z");
+ TestUnescapeHelper("abc%d%e%fg%hij%klmno%", "abc%d%e%fg%hij%klmno%");
+
+ // A few more tests
+ TestUnescapeHelper("%25", "%");
+ TestUnescapeHelper("%25%32%35", "%25");
+}
+
+void TestEncodeHelper(const char* in, const char* expected)
+{
+ nsCString out, strIn(in), strExp(expected);
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+
+ utils->SpecialEncode(strIn, true, out);
+ CheckEquals(strExp, out);
+}
+
+TEST(UrlClassifierUtils, Enc)
+{
+ // Test empty string
+ TestEncodeHelper("", "");
+
+ // Test that all characters we shouldn't encode ([33-36],[38,126]) are not.
+ nsCString noenc;
+ for (int32_t i = 33; i < 127; i++) {
+ if (i != 37) { // skip %
+ noenc.Append(static_cast<char>(i));
+ }
+ }
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+ nsCString out;
+ utils->SpecialEncode(noenc, false, out);
+ CheckEquals(noenc, out);
+
+ // Test that all the chars that we should encode [0,32],37,[127,255] are
+ nsCString yesAsString, yesExpectedString;
+ for (int32_t i = 1; i < 256; i++) {
+ if (i < 33 || i == 37 || i > 126) {
+ yesAsString.Append(static_cast<char>(i));
+ yesExpectedString.Append('%');
+ yesExpectedString.Append(int_to_hex_digit(i / 16));
+ yesExpectedString.Append(int_to_hex_digit(i % 16));
+ }
+ }
+
+ out.Truncate();
+ utils->SpecialEncode(yesAsString, false, out);
+ CheckEquals(yesExpectedString, out);
+
+ TestEncodeHelper("blah//blah", "blah/blah");
+}
+
+void TestCanonicalizeHelper(const char* in, const char* expected)
+{
+ nsCString out, strIn(in), strExp(expected);
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+
+ utils->CanonicalizePath(strIn, out);
+ CheckEquals(strExp, out);
+}
+
+TEST(UrlClassifierUtils, Canonicalize)
+{
+ // Test repeated %-decoding. Note: %25 --> %, %32 --> 2, %35 --> 5
+ TestCanonicalizeHelper("%25", "%25");
+ TestCanonicalizeHelper("%25%32%35", "%25");
+ TestCanonicalizeHelper("asdf%25%32%35asd", "asdf%25asd");
+ TestCanonicalizeHelper("%%%25%32%35asd%%", "%25%25%25asd%25%25");
+ TestCanonicalizeHelper("%25%32%35%25%32%35%25%32%35", "%25%25%25");
+ TestCanonicalizeHelper("%25", "%25");
+ TestCanonicalizeHelper("%257Ea%2521b%2540c%2523d%2524e%25f%255E00%252611%252A22%252833%252944_55%252B",
+ "~a!b@c#d$e%25f^00&11*22(33)44_55+");
+
+ TestCanonicalizeHelper("", "");
+ TestCanonicalizeHelper("%31%36%38%2e%31%38%38%2e%39%39%2e%32%36/%2E%73%65%63%75%72%65/%77%77%77%2E%65%62%61%79%2E%63%6F%6D/",
+ "168.188.99.26/.secure/www.ebay.com/");
+ TestCanonicalizeHelper("195.127.0.11/uploads/%20%20%20%20/.verify/.eBaysecure=updateuserdataxplimnbqmn-xplmvalidateinfoswqpcmlx=hgplmcx/",
+ "195.127.0.11/uploads/%20%20%20%20/.verify/.eBaysecure=updateuserdataxplimnbqmn-xplmvalidateinfoswqpcmlx=hgplmcx/");
+ // Added in bug 489455. %00 should no longer be changed to %01.
+ TestCanonicalizeHelper("%00", "%00");
+}
+
+void TestParseIPAddressHelper(const char *in, const char *expected)
+{
+ nsCString out, strIn(in), strExp(expected);
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+
+ utils->ParseIPAddress(strIn, out);
+ CheckEquals(strExp, out);
+}
+
+TEST(UrlClassifierUtils, ParseIPAddress)
+{
+ TestParseIPAddressHelper("123.123.0.0.1", "");
+ TestParseIPAddressHelper("255.0.0.1", "255.0.0.1");
+ TestParseIPAddressHelper("12.0x12.01234", "12.18.2.156");
+ TestParseIPAddressHelper("276.2.3", "20.2.0.3");
+ TestParseIPAddressHelper("012.034.01.055", "10.28.1.45");
+ TestParseIPAddressHelper("0x12.0x43.0x44.0x01", "18.67.68.1");
+ TestParseIPAddressHelper("167838211", "10.1.2.3");
+ TestParseIPAddressHelper("3279880203", "195.127.0.11");
+ TestParseIPAddressHelper("0x12434401", "18.67.68.1");
+ TestParseIPAddressHelper("413960661", "24.172.137.213");
+ TestParseIPAddressHelper("03053104725", "24.172.137.213");
+ TestParseIPAddressHelper("030.0254.0x89d5", "24.172.137.213");
+ TestParseIPAddressHelper("1.234.4.0377", "1.234.4.255");
+ TestParseIPAddressHelper("1.2.3.00x0", "");
+ TestParseIPAddressHelper("10.192.95.89 xy", "10.192.95.89");
+ TestParseIPAddressHelper("10.192.95.89 xyz", "");
+ TestParseIPAddressHelper("1.2.3.0x0", "1.2.3.0");
+ TestParseIPAddressHelper("1.2.3.4", "1.2.3.4");
+}
+
+void TestCanonicalNumHelper(const char *in, uint32_t bytes,
+ bool allowOctal, const char *expected)
+{
+ nsCString out, strIn(in), strExp(expected);
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+
+ utils->CanonicalNum(strIn, bytes, allowOctal, out);
+ CheckEquals(strExp, out);
+}
+
+TEST(UrlClassifierUtils, CanonicalNum)
+{
+ TestCanonicalNumHelper("", 1, true, "");
+ TestCanonicalNumHelper("10", 0, true, "");
+ TestCanonicalNumHelper("45", 1, true, "45");
+ TestCanonicalNumHelper("0x10", 1, true, "16");
+ TestCanonicalNumHelper("367", 2, true, "1.111");
+ TestCanonicalNumHelper("012345", 3, true, "0.20.229");
+ TestCanonicalNumHelper("0173", 1, true, "123");
+ TestCanonicalNumHelper("09", 1, false, "9");
+ TestCanonicalNumHelper("0x120x34", 2, true, "");
+ TestCanonicalNumHelper("0x12fc", 2, true, "18.252");
+ TestCanonicalNumHelper("3279880203", 4, true, "195.127.0.11");
+ TestCanonicalNumHelper("0x0000059", 1, true, "89");
+ TestCanonicalNumHelper("0x00000059", 1, true, "89");
+ TestCanonicalNumHelper("0x0000067", 1, true, "103");
+}
+
+void TestHostnameHelper(const char *in, const char *expected)
+{
+ nsCString out, strIn(in), strExp(expected);
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+
+ utils->CanonicalizeHostname(strIn, out);
+ CheckEquals(strExp, out);
+}
+
+TEST(UrlClassifierUtils, Hostname)
+{
+ TestHostnameHelper("abcd123;[]", "abcd123;[]");
+ TestHostnameHelper("abc.123", "abc.123");
+ TestHostnameHelper("abc..123", "abc.123");
+ TestHostnameHelper("trailing.", "trailing");
+ TestHostnameHelper("i love trailing dots....", "i%20love%20trailing%20dots");
+ TestHostnameHelper(".leading", "leading");
+ TestHostnameHelper("..leading", "leading");
+ TestHostnameHelper(".dots.", "dots");
+ TestHostnameHelper(".both.", "both");
+ TestHostnameHelper(".both..", "both");
+ TestHostnameHelper("..both.", "both");
+ TestHostnameHelper("..both..", "both");
+ TestHostnameHelper("..a.b.c.d..", "a.b.c.d");
+ TestHostnameHelper("..127.0.0.1..", "127.0.0.1");
+ TestHostnameHelper("asdf!@#$a", "asdf!@#$a");
+ TestHostnameHelper("AB CD 12354", "ab%20cd%2012354");
+ TestHostnameHelper("\1\2\3\4\112\177", "%01%02%03%04j%7F");
+ TestHostnameHelper("<>.AS/-+", "<>.as/-+");
+ // Added in bug 489455. %00 should no longer be changed to %01.
+ TestHostnameHelper("%00", "%00");
+}
+
+TEST(UrlClassifierUtils, LongHostname)
+{
+ static const int kTestSize = 1024 * 150;
+ char *str = static_cast<char*>(malloc(kTestSize + 1));
+ memset(str, 'x', kTestSize);
+ str[kTestSize] = '\0';
+
+ RefPtr<nsUrlClassifierUtils> utils = new nsUrlClassifierUtils;
+ utils->Init();
+
+ nsAutoCString out;
+ nsDependentCString in(str);
+ PRIntervalTime clockStart = PR_IntervalNow();
+ utils->CanonicalizeHostname(in, out);
+ PRIntervalTime clockEnd = PR_IntervalNow();
+
+ CheckEquals(in, out);
+
+ printf("CanonicalizeHostname on long string (%dms)\n",
+ PR_IntervalToMilliseconds(clockEnd - clockStart));
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/TestVariableLengthPrefixSet.cpp b/toolkit/components/url-classifier/tests/gtest/TestVariableLengthPrefixSet.cpp
new file mode 100644
index 0000000000..9e380a9d36
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/TestVariableLengthPrefixSet.cpp
@@ -0,0 +1,559 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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/RefPtr.h>
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsClassHashtable.h"
+#include "VariableLengthPrefixSet.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIFile.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla::safebrowsing;
+
+typedef nsCString _Prefix;
+typedef nsTArray<_Prefix> _PrefixArray;
+
+// Create fullhash by appending random characters.
+static nsCString* CreateFullHash(const nsACString& in)
+{
+ nsCString* out = new nsCString(in);
+ out->SetLength(32);
+ for (size_t i = in.Length(); i < 32; i++) {
+ out->SetCharAt(char(rand() % 256), i);
+ }
+
+ return out;
+}
+
+// This function generate N prefixes with size between MIN and MAX.
+// The output array will not be cleared, random result will append to it
+static void RandomPrefixes(uint32_t N, uint32_t MIN, uint32_t MAX, _PrefixArray& array)
+{
+ array.SetCapacity(array.Length() + N);
+
+ uint32_t range = (MAX - MIN + 1);
+
+ for (uint32_t i = 0; i < N; i++) {
+ uint32_t prefixSize = (rand() % range) + MIN;
+ _Prefix prefix;
+ prefix.SetLength(prefixSize);
+
+ bool added = false;
+ while(!added) {
+ char* dst = prefix.BeginWriting();
+ for (uint32_t j = 0; j < prefixSize; j++) {
+ dst[j] = rand() % 256;
+ }
+
+ if (!array.Contains(prefix)) {
+ array.AppendElement(prefix);
+ added = true;
+ }
+ }
+ }
+}
+
+static void CheckContent(VariableLengthPrefixSet* pset,
+ PrefixStringMap& expected)
+{
+ PrefixStringMap vlPSetMap;
+ pset->GetPrefixes(vlPSetMap);
+
+ for (auto iter = vlPSetMap.Iter(); !iter.Done(); iter.Next()) {
+ nsCString* expectedPrefix = expected.Get(iter.Key());
+ nsCString* resultPrefix = iter.Data();
+
+ ASSERT_TRUE(resultPrefix->Equals(*expectedPrefix));
+ }
+}
+
+// This test loops through all the prefixes and converts each prefix to
+// fullhash by appending random characters, each converted fullhash
+// should at least match its original length in the prefixSet.
+static void DoExpectedLookup(VariableLengthPrefixSet* pset,
+ _PrefixArray& array)
+{
+ uint32_t matchLength = 0;
+ for (uint32_t i = 0; i < array.Length(); i++) {
+ const nsCString& prefix = array[i];
+ UniquePtr<nsCString> fullhash(CreateFullHash(prefix));
+
+ // Find match for prefix-generated full hash
+ pset->Matches(*fullhash, &matchLength);
+ MOZ_ASSERT(matchLength != 0);
+
+ if (matchLength != prefix.Length()) {
+ // Return match size is not the same as prefix size.
+ // In this case it could be because the generated fullhash match other
+ // prefixes, check if this prefix exist.
+ bool found = false;
+
+ for (uint32_t j = 0; j < array.Length(); j++) {
+ if (array[j].Length() != matchLength) {
+ continue;
+ }
+
+ if (0 == memcmp(fullhash->BeginReading(),
+ array[j].BeginReading(),
+ matchLength)) {
+ found = true;
+ break;
+ }
+ }
+ ASSERT_TRUE(found);
+ }
+ }
+}
+
+static void DoRandomLookup(VariableLengthPrefixSet* pset,
+ uint32_t N,
+ _PrefixArray& array)
+{
+ for (uint32_t i = 0; i < N; i++) {
+ // Random 32-bytes test fullhash
+ char buf[32];
+ for (uint32_t j = 0; j < 32; j++) {
+ buf[j] = (char)(rand() % 256);
+ }
+
+ // Get the expected result.
+ nsTArray<uint32_t> expected;
+ for (uint32_t j = 0; j < array.Length(); j++) {
+ const nsACString& str = array[j];
+ if (0 == memcmp(buf, str.BeginReading(), str.Length())) {
+ expected.AppendElement(str.Length());
+ }
+ }
+
+ uint32_t matchLength = 0;
+ pset->Matches(nsDependentCSubstring(buf, 32), &matchLength);
+
+ ASSERT_TRUE(expected.IsEmpty() ? !matchLength : expected.Contains(matchLength));
+ }
+}
+
+static void SetupPrefixMap(const _PrefixArray& array,
+ PrefixStringMap& map)
+{
+ map.Clear();
+
+ // Buckets are keyed by prefix length and contain an array of
+ // all prefixes of that length.
+ nsClassHashtable<nsUint32HashKey, _PrefixArray> table;
+
+ for (uint32_t i = 0; i < array.Length(); i++) {
+ _PrefixArray* prefixes = table.Get(array[i].Length());
+ if (!prefixes) {
+ prefixes = new _PrefixArray();
+ table.Put(array[i].Length(), prefixes);
+ }
+
+ prefixes->AppendElement(array[i]);
+ }
+
+ // The resulting map entries will be a concatenation of all
+ // prefix data for the prefixes of a given size.
+ for (auto iter = table.Iter(); !iter.Done(); iter.Next()) {
+ uint32_t size = iter.Key();
+ uint32_t count = iter.Data()->Length();
+
+ _Prefix* str = new _Prefix();
+ str->SetLength(size * count);
+
+ char* dst = str->BeginWriting();
+
+ iter.Data()->Sort();
+ for (uint32_t i = 0; i < count; i++) {
+ memcpy(dst, iter.Data()->ElementAt(i).get(), size);
+ dst += size;
+ }
+
+ map.Put(size, str);
+ }
+}
+
+
+// Test setting prefix set with only 4-bytes prefixes
+TEST(VariableLengthPrefixSet, FixedLengthSet)
+{
+ srand(time(nullptr));
+
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ PrefixStringMap map;
+ _PrefixArray array = { _Prefix("alph"), _Prefix("brav"), _Prefix("char"),
+ _Prefix("delt"), _Prefix("echo"), _Prefix("foxt"),
+ };
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+
+ // Run random test
+ array.Clear();
+ map.Clear();
+
+ RandomPrefixes(1500, 4, 4, array);
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+}
+
+// Test setting prefix set with only 5~32 bytes prefixes
+TEST(VariableLengthPrefixSet, VariableLengthSet)
+{
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ PrefixStringMap map;
+ _PrefixArray array = { _Prefix("bravo"), _Prefix("charlie"), _Prefix("delta"),
+ _Prefix("EchoEchoEchoEchoEcho"), _Prefix("foxtrot"),
+ _Prefix("GolfGolfGolfGolfGolfGolfGolfGolf"),
+ _Prefix("hotel"), _Prefix("november"),
+ _Prefix("oscar"), _Prefix("quebec"), _Prefix("romeo"),
+ _Prefix("sierrasierrasierrasierrasierra"),
+ _Prefix("Tango"), _Prefix("whiskey"), _Prefix("yankee"),
+ _Prefix("ZuluZuluZuluZulu")
+ };
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+
+ // Run random test
+ array.Clear();
+ map.Clear();
+
+ RandomPrefixes(1500, 5, 32, array);
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+
+}
+
+// Test setting prefix set with both 4-bytes prefixes and 5~32 bytes prefixes
+TEST(VariableLengthPrefixSet, MixedPrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ PrefixStringMap map;
+ _PrefixArray array = { _Prefix("enus"), _Prefix("apollo"), _Prefix("mars"),
+ _Prefix("Hecatonchires cyclopes"),
+ _Prefix("vesta"), _Prefix("neptunus"), _Prefix("jupiter"),
+ _Prefix("diana"), _Prefix("minerva"), _Prefix("ceres"),
+ _Prefix("Aidos,Adephagia,Adikia,Aletheia"),
+ _Prefix("hecatonchires"), _Prefix("alcyoneus"), _Prefix("hades"),
+ _Prefix("vulcanus"), _Prefix("juno"), _Prefix("mercury"),
+ _Prefix("Stheno, Euryale and Medusa")
+ };
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+
+ // Run random test
+ array.Clear();
+ map.Clear();
+
+ RandomPrefixes(1500, 4, 32, array);
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+}
+
+// Test resetting prefix set
+TEST(VariableLengthPrefixSet, ResetPrefix)
+{
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ // First prefix set
+ _PrefixArray array1 = { _Prefix("Iceland"), _Prefix("Peru"), _Prefix("Mexico"),
+ _Prefix("Australia"), _Prefix("Japan"), _Prefix("Egypt"),
+ _Prefix("America"), _Prefix("Finland"), _Prefix("Germany"),
+ _Prefix("Italy"), _Prefix("France"), _Prefix("Taiwan"),
+ };
+ {
+ PrefixStringMap map;
+
+ SetupPrefixMap(array1, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array1);
+ }
+
+ // Second
+ _PrefixArray array2 = { _Prefix("Pikachu"), _Prefix("Bulbasaur"), _Prefix("Charmander"),
+ _Prefix("Blastoise"), _Prefix("Pidgey"), _Prefix("Mewtwo"),
+ _Prefix("Jigglypuff"), _Prefix("Persian"), _Prefix("Tentacool"),
+ _Prefix("Onix"), _Prefix("Eevee"), _Prefix("Jynx"),
+ };
+ {
+ PrefixStringMap map;
+
+ SetupPrefixMap(array2, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array2);
+ }
+
+ // Should not match any of the first prefix set
+ uint32_t matchLength = 0;
+ for (uint32_t i = 0; i < array1.Length(); i++) {
+ UniquePtr<nsACString> fullhash(CreateFullHash(array1[i]));
+
+ pset->Matches(*fullhash, &matchLength);
+ ASSERT_TRUE(matchLength == 0);
+ }
+}
+
+// Test only set one 4-bytes prefix and one full-length prefix
+TEST(VariableLengthPrefixSet, TinyPrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ PrefixStringMap map;
+ _PrefixArray array = { _Prefix("AAAA"),
+ _Prefix("11112222333344445555666677778888"),
+ };
+
+ SetupPrefixMap(array, map);
+ pset->SetPrefixes(map);
+
+ DoExpectedLookup(pset, array);
+
+ DoRandomLookup(pset, 1000, array);
+
+ CheckContent(pset, map);
+}
+
+// Test empty prefix set and IsEmpty function
+TEST(VariableLengthPrefixSet, EmptyPrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ bool empty;
+ pset->IsEmpty(&empty);
+ ASSERT_TRUE(empty);
+
+ PrefixStringMap map;
+ _PrefixArray array1;
+
+ // Lookup an empty array should never match
+ DoRandomLookup(pset, 100, array1);
+
+ // Insert an 4-bytes prefix, then IsEmpty should return false
+ _PrefixArray array2 = { _Prefix("test") };
+ SetupPrefixMap(array2, map);
+ pset->SetPrefixes(map);
+
+ pset->IsEmpty(&empty);
+ ASSERT_TRUE(!empty);
+
+ _PrefixArray array3 = { _Prefix("test variable length") };
+
+ // Insert an 5~32 bytes prefix, then IsEmpty should return false
+ SetupPrefixMap(array3, map);
+ pset->SetPrefixes(map);
+
+ pset->IsEmpty(&empty);
+ ASSERT_TRUE(!empty);
+}
+
+// Test prefix size should only between 4~32 bytes
+TEST(VariableLengthPrefixSet, MinMaxPrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> pset = new VariableLengthPrefixSet;
+ pset->Init(NS_LITERAL_CSTRING("test"));
+
+ PrefixStringMap map;
+ {
+ _PrefixArray array = { _Prefix("1234"),
+ _Prefix("ABCDEFGHIJKKMNOP"),
+ _Prefix("1aaa2bbb3ccc4ddd5eee6fff7ggg8hhh") };
+
+ SetupPrefixMap(array, map);
+ nsresult rv = pset->SetPrefixes(map);
+ ASSERT_TRUE(rv == NS_OK);
+ }
+
+ // Prefix size less than 4-bytes should fail
+ {
+ _PrefixArray array = { _Prefix("123") };
+
+ SetupPrefixMap(array, map);
+ nsresult rv = pset->SetPrefixes(map);
+ ASSERT_TRUE(NS_FAILED(rv));
+ }
+
+ // Prefix size greater than 32-bytes should fail
+ {
+ _PrefixArray array = { _Prefix("1aaa2bbb3ccc4ddd5eee6fff7ggg8hhh9") };
+
+ SetupPrefixMap(array, map);
+ nsresult rv = pset->SetPrefixes(map);
+ ASSERT_TRUE(NS_FAILED(rv));
+ }
+}
+
+// Test save then load prefix set with only 4-bytes prefixes
+TEST(VariableLengthPrefixSet, LoadSaveFixedLengthPrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> save = new VariableLengthPrefixSet;
+ save->Init(NS_LITERAL_CSTRING("test-save"));
+
+ _PrefixArray array;
+ RandomPrefixes(10000, 4, 4, array);
+
+ PrefixStringMap map;
+ SetupPrefixMap(array, map);
+ save->SetPrefixes(map);
+
+ DoExpectedLookup(save, array);
+
+ DoRandomLookup(save, 1000, array);
+
+ CheckContent(save, map);
+
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(file));
+ file->Append(NS_LITERAL_STRING("test.vlpset"));
+
+ save->StoreToFile(file);
+
+ RefPtr<VariableLengthPrefixSet> load = new VariableLengthPrefixSet;
+ load->Init(NS_LITERAL_CSTRING("test-load"));
+
+ load->LoadFromFile(file);
+
+ DoExpectedLookup(load, array);
+
+ DoRandomLookup(load, 1000, array);
+
+ CheckContent(load, map);
+
+ file->Remove(false);
+}
+
+// Test save then load prefix set with only 5~32 bytes prefixes
+TEST(VariableLengthPrefixSet, LoadSaveVariableLengthPrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> save = new VariableLengthPrefixSet;
+ save->Init(NS_LITERAL_CSTRING("test-save"));
+
+ _PrefixArray array;
+ RandomPrefixes(10000, 5, 32, array);
+
+ PrefixStringMap map;
+ SetupPrefixMap(array, map);
+ save->SetPrefixes(map);
+
+ DoExpectedLookup(save, array);
+
+ DoRandomLookup(save, 1000, array);
+
+ CheckContent(save, map);
+
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(file));
+ file->Append(NS_LITERAL_STRING("test.vlpset"));
+
+ save->StoreToFile(file);
+
+ RefPtr<VariableLengthPrefixSet> load = new VariableLengthPrefixSet;
+ load->Init(NS_LITERAL_CSTRING("test-load"));
+
+ load->LoadFromFile(file);
+
+ DoExpectedLookup(load, array);
+
+ DoRandomLookup(load, 1000, array);
+
+ CheckContent(load, map);
+
+ file->Remove(false);
+}
+
+// Test save then load prefix with both 4 bytes prefixes and 5~32 bytes prefixes
+TEST(VariableLengthPrefixSet, LoadSavePrefixSet)
+{
+ RefPtr<VariableLengthPrefixSet> save = new VariableLengthPrefixSet;
+ save->Init(NS_LITERAL_CSTRING("test-save"));
+
+ // Try to simulate the real case that most prefixes are 4bytes
+ _PrefixArray array;
+ RandomPrefixes(20000, 4, 4, array);
+ RandomPrefixes(1000, 5, 32, array);
+
+ PrefixStringMap map;
+ SetupPrefixMap(array, map);
+ save->SetPrefixes(map);
+
+ DoExpectedLookup(save, array);
+
+ DoRandomLookup(save, 1000, array);
+
+ CheckContent(save, map);
+
+ nsCOMPtr<nsIFile> file;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(file));
+ file->Append(NS_LITERAL_STRING("test.vlpset"));
+
+ save->StoreToFile(file);
+
+ RefPtr<VariableLengthPrefixSet> load = new VariableLengthPrefixSet;
+ load->Init(NS_LITERAL_CSTRING("test-load"));
+
+ load->LoadFromFile(file);
+
+ DoExpectedLookup(load, array);
+
+ DoRandomLookup(load, 1000, array);
+
+ CheckContent(load, map);
+
+ file->Remove(false);
+}
diff --git a/toolkit/components/url-classifier/tests/gtest/moz.build b/toolkit/components/url-classifier/tests/gtest/moz.build
new file mode 100644
index 0000000000..e66af90245
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/gtest/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/.
+
+LOCAL_INCLUDES += [
+ '../..',
+]
+
+UNIFIED_SOURCES += [
+ 'Common.cpp',
+ 'TestChunkSet.cpp',
+ 'TestFailUpdate.cpp',
+ 'TestLookupCacheV4.cpp',
+ 'TestPerProviderDirectory.cpp',
+ 'TestProtocolParser.cpp',
+ 'TestRiceDeltaDecoder.cpp',
+ 'TestSafebrowsingHash.cpp',
+ 'TestSafeBrowsingProtobuf.cpp',
+ 'TestTable.cpp',
+ 'TestUrlClassifierTableUpdateV4.cpp',
+ 'TestUrlClassifierUtils.cpp',
+ 'TestVariableLengthPrefixSet.cpp',
+]
+
+FINAL_LIBRARY = 'xul-gtest'
diff --git a/toolkit/components/url-classifier/tests/jar.mn b/toolkit/components/url-classifier/tests/jar.mn
new file mode 100644
index 0000000000..2264c28965
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/jar.mn
@@ -0,0 +1,2 @@
+toolkit.jar:
+ content/global/url-classifier/unittests.xul (unittests.xul)
diff --git a/toolkit/components/url-classifier/tests/mochitest/.eslintrc.js b/toolkit/components/url-classifier/tests/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..58b3df4a74
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/mochitest.eslintrc.js",
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/url-classifier/tests/mochitest/allowlistAnnotatedFrame.html b/toolkit/components/url-classifier/tests/mochitest/allowlistAnnotatedFrame.html
new file mode 100644
index 0000000000..9aae1b8415
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/allowlistAnnotatedFrame.html
@@ -0,0 +1,144 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+// Modified by evil.js
+var scriptItem;
+
+var scriptItem1 = "untouched";
+var imageItem1 = "untouched";
+var frameItem1 = "untouched";
+var scriptItem2 = "untouched";
+var imageItem2 = "untouched";
+var frameItem2 = "untouched";
+var xhrItem = "untouched";
+var fetchItem = "untouched";
+var mediaItem1 = "untouched";
+
+function checkLoads() {
+ window.parent.is(scriptItem1, "spoiled", "Should not block tracking js 1");
+ window.parent.is(scriptItem2, "spoiled", "Should not block tracking js 2");
+ window.parent.is(imageItem1, "spoiled", "Should not block tracking img 1");
+ window.parent.is(imageItem2, "spoiled", "Should not block tracking img 2");
+ window.parent.is(frameItem1, "spoiled", "Should not block tracking iframe 1");
+ window.parent.is(frameItem2, "spoiled", "Should not block tracking iframe 2");
+ window.parent.is(mediaItem1, "loaded", "Should not block tracking video");
+ window.parent.is(xhrItem, "loaded", "Should not block tracking XHR");
+ window.parent.is(fetchItem, "loaded", "Should not block fetches from tracking domains");
+ window.parent.is(window.document.blockedTrackingNodeCount, 0,
+ "No elements should be blocked");
+
+ // End (parent) test.
+ window.parent.clearPermissions();
+ window.parent.SimpleTest.finish();
+}
+
+var onloadCalled = false;
+var xhrFinished = false;
+var fetchFinished = false;
+var videoLoaded = false;
+function loaded(type) {
+ if (type === "onload") {
+ onloadCalled = true;
+ } else if (type === "xhr") {
+ xhrFinished = true;
+ } else if (type === "fetch") {
+ fetchFinished = true;
+ } else if (type === "video") {
+ videoLoaded = true;
+ }
+
+ if (onloadCalled && xhrFinished && fetchFinished && videoLoaded) {
+ checkLoads();
+ }
+}
+</script>
+
+</head>
+
+<body onload="loaded('onload')">
+
+<!-- Try loading from a tracking script URI (1) -->
+<script id="badscript1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js" onload="scriptItem1 = 'spoiled';"></script>
+
+<!-- Try loading from a tracking image URI (1) -->
+<img id="badimage1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg" onload="imageItem1 = 'spoiled';"/>
+
+<!-- Try loading from a tracking frame URI (1) -->
+<iframe id="badframe1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/track.html" onload="frameItem1 = 'spoiled';"></iframe>
+
+<!-- Try loading from a tracking video URI -->
+<video id="badmedia1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/vp9.webm"></video>
+
+<script>
+var v = document.getElementById("badmedia1");
+v.addEventListener("loadedmetadata", function() {
+ mediaItem1 = "loaded";
+ loaded("video");
+}, true);
+v.addEventListener("error", function() {
+ mediaItem1 = "error";
+ loaded("video");
+}, true);
+
+// Try loading from a tracking script URI (2) - The loader may follow a
+// different path depending on whether the resource is loaded from JS or HTML.
+var newScript = document.createElement("script");
+newScript.id = "badscript2";
+newScript.src = "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js";
+newScript.addEventListener("load", function onload() {scriptItem2 = 'spoiled';});
+document.body.appendChild(newScript);
+
+/// Try loading from a tracking image URI (2)
+var newImage = document.createElement("img");
+newImage.id = "badimage2";
+newImage.src = "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg";
+newImage.addEventListener("load", function onload() {imageItem2 = 'spoiled'});
+document.body.appendChild(newImage);
+
+// Try loading from a tracking iframe URI (2)
+var newFrame = document.createElement("iframe");
+newFrame.id = "badframe2";
+newFrame.src = "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/track.html"
+newFrame.addEventListener("load", function onload() {frameItem2 = 'spoiled'});
+document.body.appendChild(newFrame);
+
+// Try doing an XHR against a tracking domain (bug 1216793)
+function reqListener() {
+ xhrItem = "loaded";
+ loaded("xhr");
+}
+function transferFailed() {
+ xhrItem = "failed";
+ loaded("xhr");
+}
+function transferCanceled() {
+ xhrItem = "canceled";
+ loaded("xhr");
+}
+var oReq = new XMLHttpRequest();
+oReq.addEventListener("load", reqListener);
+oReq.addEventListener("error", transferFailed);
+oReq.addEventListener("abort", transferCanceled);
+oReq.open("GET", "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js");
+oReq.send();
+
+// Fetch from a tracking domain
+fetch("http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js").then(function(response) {
+ if(response.ok) {
+ fetchItem = "loaded";
+ loaded("fetch");
+ } else {
+ fetchItem = "badresponse";
+ loaded("fetch");
+ }
+ }).catch(function(error) {
+ fetchItem = "error";
+ loaded("fetch");
+});
+</script>
+</body>
+</html>
+
diff --git a/toolkit/components/url-classifier/tests/mochitest/bad.css b/toolkit/components/url-classifier/tests/mochitest/bad.css
new file mode 100644
index 0000000000..f57b36a778
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/bad.css
@@ -0,0 +1 @@
+#styleBad { visibility: hidden; }
diff --git a/toolkit/components/url-classifier/tests/mochitest/bad.css^headers^ b/toolkit/components/url-classifier/tests/mochitest/bad.css^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/bad.css^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/toolkit/components/url-classifier/tests/mochitest/basic.vtt b/toolkit/components/url-classifier/tests/mochitest/basic.vtt
new file mode 100644
index 0000000000..7781790d04
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/basic.vtt
@@ -0,0 +1,27 @@
+WEBVTT
+Region: id=testOne lines=2 width=30%
+Region: id=testTwo lines=4 width=20%
+
+1
+00:00.500 --> 00:00.700 region:testOne
+This
+
+2
+00:01.200 --> 00:02.400 region:testTwo
+Is
+
+2.5
+00:02.000 --> 00:03.500 region:testOne
+(Over here?!)
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+5
+00:03.217 --> 00:03.989
+And more!
diff --git a/toolkit/components/url-classifier/tests/mochitest/basic.vtt^headers^ b/toolkit/components/url-classifier/tests/mochitest/basic.vtt^headers^
new file mode 100644
index 0000000000..23de552c1a
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/basic.vtt^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: * \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/mochitest/bug_1281083.html b/toolkit/components/url-classifier/tests/mochitest/bug_1281083.html
new file mode 100644
index 0000000000..cd57701772
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/bug_1281083.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+var scriptItem = "untouched";
+
+function checkLoads() {
+ // Make sure the javascript did not load.
+ window.parent.is(scriptItem, "untouched", "Should not load bad javascript");
+
+ // Call parent.loadTestFrame again to test classification metadata in HTTP
+ // cache entries.
+ if (window.parent.firstLoad) {
+ window.parent.info("Reloading from cache...");
+ window.parent.firstLoad = false;
+ window.parent.loadTestFrame();
+ return;
+ }
+
+ // End (parent) test.
+ window.parent.SimpleTest.finish();
+}
+
+</script>
+
+<!-- Try loading from a malware javascript URI -->
+<script type="text/javascript" src="http://bug1281083.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js"></script>
+
+</head>
+
+<body onload="checkLoads()">
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/chrome.ini b/toolkit/components/url-classifier/tests/mochitest/chrome.ini
new file mode 100644
index 0000000000..1652e7421b
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/chrome.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ allowlistAnnotatedFrame.html
+ classifiedAnnotatedFrame.html
+ classifiedAnnotatedPBFrame.html
+ bug_1281083.html
+
+[test_lookup_system_principal.html]
+[test_classified_annotations.html]
+tags = trackingprotection
+skip-if = os == 'linux' && asan # Bug 1202548
+[test_allowlisted_annotations.html]
+tags = trackingprotection
+[test_privatebrowsing_trackingprotection.html]
+tags = trackingprotection
+[test_trackingprotection_bug1157081.html]
+tags = trackingprotection
+[test_trackingprotection_whitelist.html]
+tags = trackingprotection
+[test_safebrowsing_bug1272239.html]
+[test_donottrack.html]
+[test_classifier_changetablepref.html]
diff --git a/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedFrame.html b/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedFrame.html
new file mode 100644
index 0000000000..8aab13dd3b
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedFrame.html
@@ -0,0 +1,213 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+"use strict";
+
+var scriptItem = "untouched";
+var scriptItem1 = "untouched";
+var scriptItem2 = "untouched";
+var imageItem1 = "untouched";
+var imageItem2 = "untouched";
+var frameItem1 = "untouched";
+var frameItem2 = "untouched";
+var xhrItem = "untouched";
+var fetchItem = "untouched";
+var mediaItem1 = "untouched";
+
+var badids = [
+ "badscript1",
+ "badscript2",
+ "badimage1",
+ "badimage2",
+ "badframe1",
+ "badframe2",
+ "badmedia1",
+ "badcss"
+];
+
+function checkLoads() {
+ window.parent.is(
+ scriptItem1, "untouched", "Should not load tracking javascript");
+ window.parent.is(
+ scriptItem2, "untouched", "Should not load tracking javascript (2)");
+
+ window.parent.is(
+ imageItem1, "untouched", "Should not load tracking images");
+ window.parent.is(
+ imageItem2, "untouched", "Should not load tracking images (2)");
+
+ window.parent.is(
+ frameItem1, "untouched", "Should not load tracking iframes");
+ window.parent.is(
+ frameItem2, "untouched", "Should not load tracking iframes (2)");
+ window.parent.is(
+ mediaItem1, "error", "Should not load tracking videos");
+ window.parent.is(
+ xhrItem, "failed", "Should not load tracking XHRs");
+ window.parent.is(
+ fetchItem, "error", "Should not fetch from tracking URLs");
+
+ var elt = document.getElementById("styleCheck");
+ var style = document.defaultView.getComputedStyle(elt, "");
+ window.parent.isnot(
+ style.visibility, "hidden", "Should not load tracking css");
+
+ window.parent.is(window.document.blockedTrackingNodeCount, badids.length,
+ "Should identify all tracking elements");
+
+ var blockedTrackingNodes = window.document.blockedTrackingNodes;
+
+ // Make sure that every node in blockedTrackingNodes exists in the tree
+ // (that may not always be the case but do not expect any nodes to disappear
+ // from the tree here)
+ var allNodeMatch = true;
+ for (var i = 0; i < blockedTrackingNodes.length; i++) {
+ var nodeMatch = false;
+ for (var j = 0; j < badids.length && !nodeMatch; j++) {
+ nodeMatch = nodeMatch ||
+ (blockedTrackingNodes[i] == document.getElementById(badids[j]));
+ }
+
+ allNodeMatch = allNodeMatch && nodeMatch;
+ }
+ window.parent.ok(allNodeMatch,
+ "All annotated nodes are expected in the tree");
+
+ // Make sure that every node with a badid (see badids) is found in the
+ // blockedTrackingNodes. This tells us if we are neglecting to annotate
+ // some nodes
+ allNodeMatch = true;
+ for (var j = 0; j < badids.length; j++) {
+ var nodeMatch = false;
+ for (var i = 0; i < blockedTrackingNodes.length && !nodeMatch; i++) {
+ nodeMatch = nodeMatch ||
+ (blockedTrackingNodes[i] == document.getElementById(badids[j]));
+ }
+
+ if (!nodeMatch) {
+ console.log(badids[j] + " was not found in blockedTrackingNodes");
+ }
+ allNodeMatch = allNodeMatch && nodeMatch;
+ }
+ window.parent.ok(allNodeMatch,
+ "All tracking nodes are expected to be annotated as such");
+
+ // Unset prefs, etc.
+ window.parent.cleanup();
+ // End (parent) test.
+ window.parent.SimpleTest.finish();
+}
+
+var onloadCalled = false;
+var xhrFinished = false;
+var fetchFinished = false;
+var videoLoaded = false;
+function loaded(type) {
+ if (type === "onload") {
+ onloadCalled = true;
+ } else if (type === "xhr") {
+ xhrFinished = true;
+ } else if (type === "fetch") {
+ fetchFinished = true;
+ } else if (type === "video") {
+ videoLoaded = true;
+ }
+ if (onloadCalled && xhrFinished && fetchFinished && videoLoaded) {
+ checkLoads();
+ }
+}
+</script>
+
+<!-- Try loading from a tracking CSS URI -->
+<link id="badcss" rel="stylesheet" type="text/css" href="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.css"></link>
+
+</head>
+
+<body onload="loaded('onload')">
+
+<!-- Try loading from a tracking script URI (1): evil.js onload will have updated the scriptItem variable -->
+<script id="badscript1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js" onload="scriptItem1 = scriptItem;"></script>
+
+<!-- Try loading from a tracking image URI (1) -->
+<img id="badimage1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?reload=true" onload="imageItem1 = 'spoiled';"/>
+
+<!-- Try loading from a tracking frame URI (1) -->
+<iframe id="badframe1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/track.html" onload="frameItem1 = 'spoiled';"></iframe>
+
+<!-- Try loading from a tracking video URI -->
+<video id="badmedia1" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/vp9.webm?reload=true"></video>
+
+<script>
+var v = document.getElementById("badmedia1");
+v.addEventListener("loadedmetadata", function() {
+ mediaItem1 = "loaded";
+ loaded("video");
+}, true);
+v.addEventListener("error", function() {
+ mediaItem1 = "error";
+ loaded("video");
+}, true);
+
+// Try loading from a tracking script URI (2) - The loader may follow a different path depending on whether the resource is loaded from JS or HTML.
+var newScript = document.createElement("script");
+newScript.id = "badscript2";
+newScript.src = "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js";
+newScript.addEventListener("load", function() {scriptItem2 = scriptItem;});
+document.body.appendChild(newScript);
+
+/// Try loading from a tracking image URI (2)
+var newImage = document.createElement("img");
+newImage.id = "badimage2";
+newImage.src = "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?reload=true";
+newImage.addEventListener("load", function() {imageItem2 = 'spoiled'});
+document.body.appendChild(newImage);
+
+// Try loading from a tracking iframe URI (2)
+var newFrame = document.createElement("iframe");
+newFrame.id = "badframe2";
+newFrame.src = "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/track.html"
+newFrame.addEventListener("load", function() {frameItem2 = 'spoiled'});
+document.body.appendChild(newFrame);
+
+// Try doing an XHR against a tracking domain (bug 1216793)
+function reqListener() {
+ xhrItem = "loaded";
+ loaded("xhr");
+}
+function transferFailed() {
+ xhrItem = "failed";
+ loaded("xhr");
+}
+function transferCanceled() {
+ xhrItem = "canceled";
+ loaded("xhr");
+}
+var oReq = new XMLHttpRequest();
+oReq.addEventListener("load", reqListener);
+oReq.addEventListener("error", transferFailed);
+oReq.addEventListener("abort", transferCanceled);
+oReq.open("GET", "http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js");
+oReq.send();
+
+// Fetch from a tracking domain
+fetch("http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js").then(function(response) {
+ if(response.ok) {
+ fetchItem = "loaded";
+ loaded("fetch");
+ } else {
+ fetchItem = "badresponse";
+ loaded("fetch");
+ }
+ }).catch(function(error) {
+ fetchItem = "error";
+ loaded("fetch");
+});
+</script>
+
+The following should not be hidden:
+<div id="styleCheck">STYLE TEST</div>
+
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html b/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html
new file mode 100644
index 0000000000..f11ec1de3e
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+<title></title>
+
+<link id="badcss" rel="stylesheet" type="text/css" href="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.css"></link>
+
+</head>
+<body>
+
+<script id="badscript" data-touched="not sure" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js" onload="this.dataset.touched = 'yes';" onerror="this.dataset.touched = 'no';"></script>
+
+<script id="goodscript" data-touched="not sure" src="http://itisatracker.org/tests/toolkit/components/url-classifier/tests/mochitest/good.js" onload="this.dataset.touched = 'yes';" onerror="this.dataset.touched = 'no';"></script>
+
+<!-- The image cache can cache JS handlers, so make sure we use a different URL for raptor.jpg each time -->
+<img id="badimage" data-touched="not sure" src="http://tracking.example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?pbmode=test" onload="this.dataset.touched = 'yes';" onerror="this.dataset.touched = 'no';"/>
+
+The following should not be hidden:
+<div id="styleCheck">STYLE TEST</div>
+
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js b/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js
new file mode 100644
index 0000000000..49bda38db1
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/classifierCommon.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, results: Cr } = Components;
+
+var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+
+var timer;
+function setTimeout(callback, delay) {
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback({ notify: callback },
+ delay,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+function doUpdate(update) {
+ let listener = {
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+ updateUrlRequested: function(url) { },
+ streamFinished: function(status) { },
+ updateError: function(errorCode) {
+ sendAsyncMessage("updateError", errorCode);
+ },
+ updateSuccess: function(requestedTimeout) {
+ sendAsyncMessage("updateSuccess");
+ }
+ };
+
+ let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+
+ try {
+ dbService.beginUpdate(listener, "test-malware-simple,test-unwanted-simple", "");
+ dbService.beginStream("", "");
+ dbService.updateStream(update);
+ dbService.finishStream();
+ dbService.finishUpdate();
+ } catch(e) {
+ // beginUpdate may fail if there's an existing update in progress
+ // retry until success or testcase timeout.
+ setTimeout(() => { doUpdate(update); }, 1000);
+ }
+}
+
+function doReload() {
+ dbService.reloadDatabase();
+
+ sendAsyncMessage("reloadSuccess");
+}
+
+// SafeBrowsing.jsm is initialized after mozEntries are added. Add observer
+// to receive "finished" event. For the case when this function is called
+// after the event had already been notified, we lookup entries to see if
+// they are already added to database.
+function waitForInit() {
+ let observerService = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+
+ observerService.addObserver(function() {
+ sendAsyncMessage("safeBrowsingInited");
+ }, "mozentries-update-finished", false);
+
+ // This url must sync with the table, url in SafeBrowsing.jsm addMozEntries
+ const table = "test-phish-simple";
+ const url = "http://itisatrap.org/firefox/its-a-trap.html";
+
+ let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+ let iosvc = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+
+ let principal = secMan.createCodebasePrincipal(
+ iosvc.newURI(url, null, null), {});
+
+ let listener = {
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ handleEvent: function(value)
+ {
+ if (value === table) {
+ sendAsyncMessage("safeBrowsingInited");
+ }
+ },
+ };
+ dbService.lookup(principal, table, listener);
+}
+
+addMessageListener("doUpdate", ({ testUpdate }) => {
+ doUpdate(testUpdate);
+});
+
+addMessageListener("doReload", () => {
+ doReload();
+});
+
+addMessageListener("waitForInit", () => {
+ waitForInit();
+});
diff --git a/toolkit/components/url-classifier/tests/mochitest/classifierFrame.html b/toolkit/components/url-classifier/tests/mochitest/classifierFrame.html
new file mode 100644
index 0000000000..c7923f4484
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/classifierFrame.html
@@ -0,0 +1,57 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+var scriptItem = "untouched";
+
+function checkLoads() {
+ // Make sure the javascript did not load.
+ window.parent.is(scriptItem, "untouched", "Should not load bad javascript");
+
+ // Make sure the css did not load.
+ var elt = document.getElementById("styleCheck");
+ var style = document.defaultView.getComputedStyle(elt, "");
+ window.parent.isnot(style.visibility, "hidden", "Should not load bad css");
+
+ elt = document.getElementById("styleBad");
+ style = document.defaultView.getComputedStyle(elt, "");
+ window.parent.isnot(style.visibility, "hidden", "Should not load bad css");
+
+ elt = document.getElementById("styleImport");
+ style = document.defaultView.getComputedStyle(elt, "");
+ window.parent.isnot(style.visibility, "visible", "Should import clean css");
+
+ // Call parent.loadTestFrame again to test classification metadata in HTTP
+ // cache entries.
+ if (window.parent.firstLoad) {
+ window.parent.info("Reloading from cache...");
+ window.parent.firstLoad = false;
+ window.parent.loadTestFrame();
+ return;
+ }
+
+ // End (parent) test.
+ window.parent.SimpleTest.finish();
+}
+
+</script>
+
+<!-- Try loading from a malware javascript URI -->
+<script type="text/javascript" src="http://malware.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js"></script>
+
+<!-- Try loading from an uwanted software css URI -->
+<link rel="stylesheet" type="text/css" href="http://unwanted.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.css"></link>
+
+<!-- Try loading a marked-as-malware css through an @import from a clean URI -->
+<link rel="stylesheet" type="text/css" href="import.css"></link>
+</head>
+
+<body onload="checkLoads()">
+The following should not be hidden:
+<div id="styleCheck">STYLE TEST</div>
+<div id="styleBad">STYLE BAD</div>
+<div id="styleImport">STYLE IMPORT</div>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js b/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js
new file mode 100644
index 0000000000..973f0c2c4d
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/classifierHelper.js
@@ -0,0 +1,201 @@
+if (typeof(classifierHelper) == "undefined") {
+ var classifierHelper = {};
+}
+
+const CLASSIFIER_COMMON_URL = SimpleTest.getTestFileURL("classifierCommon.js");
+var gScript = SpecialPowers.loadChromeScript(CLASSIFIER_COMMON_URL);
+
+const ADD_CHUNKNUM = 524;
+const SUB_CHUNKNUM = 523;
+const HASHLEN = 32;
+
+const PREFS = {
+ PROVIDER_LISTS : "browser.safebrowsing.provider.mozilla.lists",
+ DISALLOW_COMPLETIONS : "urlclassifier.disallow_completions",
+ PROVIDER_GETHASHURL : "browser.safebrowsing.provider.mozilla.gethashURL"
+};
+
+// addUrlToDB & removeUrlFromDB are asynchronous, queue the task to ensure
+// the callback follow correct order.
+classifierHelper._updates = [];
+
+// Keep urls added to database, those urls should be automatically
+// removed after test complete.
+classifierHelper._updatesToCleanup = [];
+
+classifierHelper._initsCB = [];
+
+// This function return a Promise, promise is resolved when SafeBrowsing.jsm
+// is initialized.
+classifierHelper.waitForInit = function() {
+ return new Promise(function(resolve, reject) {
+ classifierHelper._initsCB.push(resolve);
+ gScript.sendAsyncMessage("waitForInit");
+ });
+}
+
+// This function is used to allow completion for specific "list",
+// some lists like "test-malware-simple" is default disabled to ask for complete.
+// "list" is the db we would like to allow it
+// "url" is the completion server
+classifierHelper.allowCompletion = function(lists, url) {
+ for (var list of lists) {
+ // Add test db to provider
+ var pref = SpecialPowers.getCharPref(PREFS.PROVIDER_LISTS);
+ pref += "," + list;
+ SpecialPowers.setCharPref(PREFS.PROVIDER_LISTS, pref);
+
+ // Rename test db so we will not disallow it from completions
+ pref = SpecialPowers.getCharPref(PREFS.DISALLOW_COMPLETIONS);
+ pref = pref.replace(list, list + "-backup");
+ SpecialPowers.setCharPref(PREFS.DISALLOW_COMPLETIONS, pref);
+ }
+
+ // Set get hash url
+ SpecialPowers.setCharPref(PREFS.PROVIDER_GETHASHURL, url);
+}
+
+// Pass { url: ..., db: ... } to add url to database,
+// onsuccess/onerror will be called when update complete.
+classifierHelper.addUrlToDB = function(updateData) {
+ return new Promise(function(resolve, reject) {
+ var testUpdate = "";
+ for (var update of updateData) {
+ var LISTNAME = update.db;
+ var CHUNKDATA = update.url;
+ var CHUNKLEN = CHUNKDATA.length;
+ var HASHLEN = update.len ? update.len : 32;
+
+ classifierHelper._updatesToCleanup.push(update);
+ testUpdate +=
+ "n:1000\n" +
+ "i:" + LISTNAME + "\n" +
+ "ad:1\n" +
+ "a:" + ADD_CHUNKNUM + ":" + HASHLEN + ":" + CHUNKLEN + "\n" +
+ CHUNKDATA;
+ }
+
+ classifierHelper._update(testUpdate, resolve, reject);
+ });
+}
+
+// Pass { url: ..., db: ... } to remove url from database,
+// onsuccess/onerror will be called when update complete.
+classifierHelper.removeUrlFromDB = function(updateData) {
+ return new Promise(function(resolve, reject) {
+ var testUpdate = "";
+ for (var update of updateData) {
+ var LISTNAME = update.db;
+ var CHUNKDATA = ADD_CHUNKNUM + ":" + update.url;
+ var CHUNKLEN = CHUNKDATA.length;
+ var HASHLEN = update.len ? update.len : 32;
+
+ testUpdate +=
+ "n:1000\n" +
+ "i:" + LISTNAME + "\n" +
+ "s:" + SUB_CHUNKNUM + ":" + HASHLEN + ":" + CHUNKLEN + "\n" +
+ CHUNKDATA;
+ }
+
+ classifierHelper._updatesToCleanup =
+ classifierHelper._updatesToCleanup.filter((v) => {
+ return updateData.indexOf(v) == -1;
+ });
+
+ classifierHelper._update(testUpdate, resolve, reject);
+ });
+};
+
+// This API is used to expire all add/sub chunks we have updated
+// by using addUrlToDB and removeUrlFromDB.
+classifierHelper.resetDB = function() {
+ return new Promise(function(resolve, reject) {
+ var testUpdate = "";
+ for (var update of classifierHelper._updatesToCleanup) {
+ if (testUpdate.includes(update.db))
+ continue;
+
+ testUpdate +=
+ "n:1000\n" +
+ "i:" + update.db + "\n" +
+ "ad:" + ADD_CHUNKNUM + "\n" +
+ "sd:" + SUB_CHUNKNUM + "\n"
+ }
+
+ classifierHelper._update(testUpdate, resolve, reject);
+ });
+};
+
+classifierHelper.reloadDatabase = function() {
+ return new Promise(function(resolve, reject) {
+ gScript.addMessageListener("reloadSuccess", function handler() {
+ gScript.removeMessageListener('reloadSuccess', handler);
+ resolve();
+ });
+
+ gScript.sendAsyncMessage("doReload");
+ });
+}
+
+classifierHelper._update = function(testUpdate, onsuccess, onerror) {
+ // Queue the task if there is still an on-going update
+ classifierHelper._updates.push({"data": testUpdate,
+ "onsuccess": onsuccess,
+ "onerror": onerror});
+ if (classifierHelper._updates.length != 1) {
+ return;
+ }
+
+ gScript.sendAsyncMessage("doUpdate", { testUpdate });
+};
+
+classifierHelper._updateSuccess = function() {
+ var update = classifierHelper._updates.shift();
+ update.onsuccess();
+
+ if (classifierHelper._updates.length) {
+ var testUpdate = classifierHelper._updates[0].data;
+ gScript.sendAsyncMessage("doUpdate", { testUpdate });
+ }
+};
+
+classifierHelper._updateError = function(errorCode) {
+ var update = classifierHelper._updates.shift();
+ update.onerror(errorCode);
+
+ if (classifierHelper._updates.length) {
+ var testUpdate = classifierHelper._updates[0].data;
+ gScript.sendAsyncMessage("doUpdate", { testUpdate });
+ }
+};
+
+classifierHelper._inited = function() {
+ classifierHelper._initsCB.forEach(function (cb) {
+ cb();
+ });
+ classifierHelper._initsCB = [];
+};
+
+classifierHelper._setup = function() {
+ gScript.addMessageListener("updateSuccess", classifierHelper._updateSuccess);
+ gScript.addMessageListener("updateError", classifierHelper._updateError);
+ gScript.addMessageListener("safeBrowsingInited", classifierHelper._inited);
+
+ // cleanup will be called at end of each testcase to remove all the urls added to database.
+ SimpleTest.registerCleanupFunction(classifierHelper._cleanup);
+};
+
+classifierHelper._cleanup = function() {
+ // clean all the preferences may touch by helper
+ for (var pref in PREFS) {
+ SpecialPowers.clearUserPref(pref);
+ }
+
+ if (!classifierHelper._updatesToCleanup) {
+ return Promise.resolve();
+ }
+
+ return classifierHelper.resetDB();
+};
+
+classifierHelper._setup();
diff --git a/toolkit/components/url-classifier/tests/mochitest/cleanWorker.js b/toolkit/components/url-classifier/tests/mochitest/cleanWorker.js
new file mode 100644
index 0000000000..6856483730
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/cleanWorker.js
@@ -0,0 +1,10 @@
+onmessage = function() {
+ try {
+ importScripts("evilWorker.js");
+ } catch(ex) {
+ postMessage("success");
+ return;
+ }
+
+ postMessage("failure");
+};
diff --git a/toolkit/components/url-classifier/tests/mochitest/dnt.html b/toolkit/components/url-classifier/tests/mochitest/dnt.html
new file mode 100644
index 0000000000..effc3a4f8c
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/dnt.html
@@ -0,0 +1,31 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+function makeXHR(url, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.onload = function() {
+ callback(xhr.response);
+ };
+ xhr.send();
+}
+
+function loaded(type) {
+ window.parent.postMessage("navigator.doNotTrack=" + navigator.doNotTrack, "*");
+
+ makeXHR("dnt.sjs", (res) => {
+ window.parent.postMessage("DNT=" + res, "*");
+ window.parent.postMessage("finish", "*");
+ });
+}
+
+</script>
+</head>
+
+<body onload="loaded('onload')">
+</body>
+
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/dnt.sjs b/toolkit/components/url-classifier/tests/mochitest/dnt.sjs
new file mode 100644
index 0000000000..bbb836482a
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/dnt.sjs
@@ -0,0 +1,9 @@
+function handleRequest(request, response) {
+ var dnt = "unspecified";
+ if (request.hasHeader("DNT")) {
+ dnt = "1";
+ }
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(dnt);
+}
diff --git a/toolkit/components/url-classifier/tests/mochitest/evil.css b/toolkit/components/url-classifier/tests/mochitest/evil.css
new file mode 100644
index 0000000000..f6f08d7c5c
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/evil.css
@@ -0,0 +1 @@
+#styleCheck { visibility: hidden; } \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/mochitest/evil.css^headers^ b/toolkit/components/url-classifier/tests/mochitest/evil.css^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/evil.css^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/toolkit/components/url-classifier/tests/mochitest/evil.js b/toolkit/components/url-classifier/tests/mochitest/evil.js
new file mode 100644
index 0000000000..27f2e8c43d
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/evil.js
@@ -0,0 +1 @@
+scriptItem = "loaded malware javascript!";
diff --git a/toolkit/components/url-classifier/tests/mochitest/evil.js^headers^ b/toolkit/components/url-classifier/tests/mochitest/evil.js^headers^
new file mode 100644
index 0000000000..3eced96143
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/evil.js^headers^
@@ -0,0 +1,2 @@
+Access-Control-Allow-Origin: *
+Cache-Control: no-store
diff --git a/toolkit/components/url-classifier/tests/mochitest/evilWorker.js b/toolkit/components/url-classifier/tests/mochitest/evilWorker.js
new file mode 100644
index 0000000000..ac34977d71
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/evilWorker.js
@@ -0,0 +1,3 @@
+onmessage = function() {
+ postMessage("loaded bad file");
+}
diff --git a/toolkit/components/url-classifier/tests/mochitest/gethash.sjs b/toolkit/components/url-classifier/tests/mochitest/gethash.sjs
new file mode 100644
index 0000000000..9dcc6e0d57
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/gethash.sjs
@@ -0,0 +1,130 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(request, response)
+{
+ var query = {};
+ request.queryString.split('&').forEach(function (val) {
+ var idx = val.indexOf('=');
+ query[val.slice(0, idx)] = unescape(val.slice(idx + 1));
+ });
+
+ var responseBody;
+
+ // Store fullhash in the server side.
+ if ("list" in query && "fullhash" in query) {
+ // In the server side we will store:
+ // 1. All the full hashes for a given list
+ // 2. All the lists we have right now
+ // data is separate by '\n'
+ let list = query["list"];
+ let hashes = getState(list);
+
+ let hash = base64ToString(query["fullhash"]);
+ hashes += hash + "\n";
+ setState(list, hashes);
+
+ let lists = getState("lists");
+ if (lists.indexOf(list) == -1) {
+ lists += list + "\n";
+ setState("lists", lists);
+ }
+
+ return;
+ // gethash count return how many gethash request received.
+ // This is used by client to know if a gethash request is triggered by gecko
+ } else if ("gethashcount" == request.queryString) {
+ var counter = getState("counter");
+ responseBody = counter == "" ? "0" : counter;
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var counter = getState("counter");
+ counter = counter == "" ? "1" : (parseInt(counter) + 1).toString();
+ setState("counter", counter);
+
+ responseBody = parseV2Request(bytes);
+ }
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(responseBody);
+
+}
+
+function parseV2Request(bytes) {
+ var request = String.fromCharCode.apply(this, bytes);
+ var [HEADER, PREFIXES] = request.split("\n");
+ var [PREFIXSIZE, LENGTH] = HEADER.split(":").map(val => {
+ return parseInt(val);
+ });
+
+ var ret = "";
+ for(var start = 0; start < LENGTH; start += PREFIXSIZE) {
+ getState("lists").split("\n").forEach(function(list) {
+ var completions = getState(list).split("\n");
+
+ for (var completion of completions) {
+ if (completion.indexOf(PREFIXES.substr(start, PREFIXSIZE)) == 0) {
+ ret += list + ":" + "1" + ":" + "32" + "\n";
+ ret += completion;
+ }
+ }
+ });
+ }
+
+ return ret;
+}
+
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/url-classifier/tests/mochitest/gethashFrame.html b/toolkit/components/url-classifier/tests/mochitest/gethashFrame.html
new file mode 100644
index 0000000000..560ddcde6e
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/gethashFrame.html
@@ -0,0 +1,62 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+var scriptItem = "untouched";
+
+function checkLoads() {
+
+ var title = document.getElementById("title");
+ title.innerHTML = window.parent.shouldLoad ?
+ "The following should be hidden:" :
+ "The following should not be hidden:"
+
+ if (window.parent.shouldLoad) {
+ window.parent.is(scriptItem, "loaded malware javascript!", "Should load bad javascript");
+ } else {
+ window.parent.is(scriptItem, "untouched", "Should not load bad javascript");
+ }
+
+ var elt = document.getElementById("styleImport");
+ var style = document.defaultView.getComputedStyle(elt, "");
+ window.parent.isnot(style.visibility, "visible", "Should load clean css");
+
+ // Make sure the css did not load.
+ elt = document.getElementById("styleCheck");
+ style = document.defaultView.getComputedStyle(elt, "");
+ if (window.parent.shouldLoad) {
+ window.parent.isnot(style.visibility, "visible", "Should load bad css");
+ } else {
+ window.parent.isnot(style.visibility, "hidden", "Should not load bad css");
+ }
+
+ elt = document.getElementById("styleBad");
+ style = document.defaultView.getComputedStyle(elt, "");
+ if (window.parent.shouldLoad) {
+ window.parent.isnot(style.visibility, "visible", "Should import bad css");
+ } else {
+ window.parent.isnot(style.visibility, "hidden", "Should not import bad css");
+ }
+}
+
+</script>
+
+<!-- Try loading from a malware javascript URI -->
+<script type="text/javascript" src="http://malware.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.js"></script>
+
+<!-- Try loading from an uwanted software css URI -->
+<link rel="stylesheet" type="text/css" href="http://unwanted.example.com/tests/toolkit/components/url-classifier/tests/mochitest/evil.css"></link>
+
+<!-- Try loading a marked-as-malware css through an @import from a clean URI -->
+<link rel="stylesheet" type="text/css" href="import.css"></link>
+</head>
+
+<body onload="checkLoads()">
+<div id="title"></div>
+<div id="styleCheck">STYLE EVIL</div>
+<div id="styleBad">STYLE BAD</div>
+<div id="styleImport">STYLE IMPORT</div>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/good.js b/toolkit/components/url-classifier/tests/mochitest/good.js
new file mode 100644
index 0000000000..015b9fe520
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/good.js
@@ -0,0 +1 @@
+scriptItem = "loaded whitelisted javascript!";
diff --git a/toolkit/components/url-classifier/tests/mochitest/import.css b/toolkit/components/url-classifier/tests/mochitest/import.css
new file mode 100644
index 0000000000..9b86c82169
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/import.css
@@ -0,0 +1,3 @@
+/* malware.example.com is in the malware database. */
+@import url("http://malware.example.com/tests/toolkit/components/url-classifier/tests/mochitest/bad.css");
+#styleImport { visibility: hidden; }
diff --git a/toolkit/components/url-classifier/tests/mochitest/mochitest.ini b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..c5679e86be
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
@@ -0,0 +1,39 @@
+[DEFAULT]
+support-files =
+ classifiedAnnotatedPBFrame.html
+ classifierCommon.js
+ classifierFrame.html
+ classifierHelper.js
+ cleanWorker.js
+ good.js
+ evil.css
+ evil.css^headers^
+ evil.js
+ evil.js^headers^
+ evilWorker.js
+ import.css
+ raptor.jpg
+ track.html
+ unwantedWorker.js
+ vp9.webm
+ whitelistFrame.html
+ workerFrame.html
+ ping.sjs
+ basic.vtt
+ basic.vtt^headers^
+ dnt.html
+ dnt.sjs
+ update.sjs
+ bad.css
+ bad.css^headers^
+ gethash.sjs
+ gethashFrame.html
+ seek.webm
+
+[test_classifier.html]
+skip-if = (os == 'linux' && debug) #Bug 1199778
+[test_classifier_worker.html]
+[test_classify_ping.html]
+[test_classify_track.html]
+[test_gethash.html]
+[test_bug1254766.html]
diff --git a/toolkit/components/url-classifier/tests/mochitest/ping.sjs b/toolkit/components/url-classifier/tests/mochitest/ping.sjs
new file mode 100644
index 0000000000..37a78956e0
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/ping.sjs
@@ -0,0 +1,16 @@
+function handleRequest(request, response)
+{
+ var query = {};
+ request.queryString.split('&').forEach(function (val) {
+ var [name, value] = val.split('=');
+ query[name] = unescape(value);
+ });
+
+ if (request.method == "POST") {
+ setState(query["id"], "ping");
+ } else {
+ var value = getState(query["id"]);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(value);
+ }
+}
diff --git a/toolkit/components/url-classifier/tests/mochitest/raptor.jpg b/toolkit/components/url-classifier/tests/mochitest/raptor.jpg
new file mode 100644
index 0000000000..243ba9e2d4
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/raptor.jpg
Binary files differ
diff --git a/toolkit/components/url-classifier/tests/mochitest/seek.webm b/toolkit/components/url-classifier/tests/mochitest/seek.webm
new file mode 100644
index 0000000000..72b0297233
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/seek.webm
Binary files differ
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_allowlisted_annotations.html b/toolkit/components/url-classifier/tests/mochitest/test_allowlisted_annotations.html
new file mode 100644
index 0000000000..ba9c86f95e
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_allowlisted_annotations.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the URI Classifier</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+// Add https://allowlisted.example.com to the permissions manager
+SpecialPowers.addPermission("trackingprotection",
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ { url: "https://allowlisted.example.com" });
+
+function clearPermissions() {
+ SpecialPowers.removePermission("trackingprotection",
+ { url: "https://allowlisted.example.com" });
+ ok(!SpecialPowers.testPermission("trackingprotection",
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ { url: "https://allowlisted.example.com" }));
+}
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.trackingTable", "test-track-simple"],
+ ["privacy.trackingprotection.enabled", true],
+ ["channelclassifier.allowlist_example", true]]},
+ test);
+
+function test() {
+ SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
+ UrlClassifierTestUtils.addTestTrackers().then(() => {
+ document.getElementById("testFrame").src = "allowlistAnnotatedFrame.html";
+ });
+}
+
+// Expected finish() call is in "allowlistedAnnotatedFrame.html".
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_bug1254766.html b/toolkit/components/url-classifier/tests/mochitest/test_bug1254766.html
new file mode 100644
index 0000000000..1c149406ac
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_bug1254766.html
@@ -0,0 +1,305 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1272239 - Test gethash.</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="classifierHelper.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+const MALWARE_LIST = "test-malware-simple";
+const MALWARE_HOST1 = "malware.example.com/";
+const MALWARE_HOST2 = "test1.example.com/";
+
+const UNWANTED_LIST = "test-unwanted-simple";
+const UNWANTED_HOST1 = "unwanted.example.com/";
+const UNWANTED_HOST2 = "test2.example.com/";
+
+
+const UNUSED_MALWARE_HOST = "unused.malware.com/";
+const UNUSED_UNWANTED_HOST = "unused.unwanted.com/";
+
+const GETHASH_URL =
+ "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/gethash.sjs";
+
+var gPreGethashCounter = 0;
+var gCurGethashCounter = 0;
+
+var expectLoad = false;
+
+function loadTestFrame() {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "gethashFrame.html");
+ document.body.appendChild(iframe);
+
+ iframe.onload = function() {
+ document.body.removeChild(iframe);
+ resolve();
+ };
+ }).then(getGethashCounter);
+}
+
+function getGethashCounter() {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open("PUT", GETHASH_URL + "?gethashcount");
+ xhr.setRequestHeader("Content-Type", "text/plain");
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ gPreGethashCounter = gCurGethashCounter;
+ gCurGethashCounter = parseInt(xhr.response);
+ resolve();
+ }
+ };
+ xhr.send();
+ });
+}
+
+// calculate the fullhash and send it to gethash server
+function addCompletionToServer(list, url) {
+ return new Promise(function(resolve, reject) {
+ var listParam = "list=" + list;
+ var fullhashParam = "fullhash=" + hash(url);
+
+ var xhr = new XMLHttpRequest;
+ xhr.open("PUT", GETHASH_URL + "?" + listParam + "&" + fullhashParam, true);
+ xhr.setRequestHeader("Content-Type", "text/plain");
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ resolve();
+ }
+ };
+ xhr.send();
+ });
+}
+
+function hash(str) {
+ function bytesFromString(str) {
+ var converter =
+ SpecialPowers.Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(SpecialPowers.Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter.convertToByteArray(str);
+ }
+
+ var hasher = SpecialPowers.Cc["@mozilla.org/security/hash;1"]
+ .createInstance(SpecialPowers.Ci.nsICryptoHash);
+
+ var data = bytesFromString(str);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+}
+
+// setup function allows classifier send gethash request for test database
+// also it calculate to fullhash for url and store those hashes in gethash sjs.
+function setup() {
+ classifierHelper.allowCompletion([MALWARE_LIST, UNWANTED_LIST], GETHASH_URL);
+
+ return Promise.all([
+ addCompletionToServer(MALWARE_LIST, MALWARE_HOST1),
+ addCompletionToServer(MALWARE_LIST, MALWARE_HOST2),
+ addCompletionToServer(UNWANTED_LIST, UNWANTED_HOST1),
+ addCompletionToServer(UNWANTED_LIST, UNWANTED_HOST2),
+ ]);
+}
+
+// Reset function in helper try to simulate the behavior we restart firefox
+function reset() {
+ return classifierHelper.resetDB()
+ .catch(err => {
+ ok(false, "Couldn't update classifier. Error code: " + errorCode);
+ // Abort test.
+ SimpleTest.finish();
+ });
+}
+
+function updateUnusedUrl() {
+ var testData = [
+ { url: UNUSED_MALWARE_HOST, db: MALWARE_LIST },
+ { url: UNUSED_UNWANTED_HOST, db: UNWANTED_LIST }
+ ];
+
+ return classifierHelper.addUrlToDB(testData)
+ .catch(err => {
+ ok(false, "Couldn't update classifier. Error code: " + err);
+ // Abort test.
+ SimpleTest.finish();
+ });
+}
+
+function addPrefixToDB() {
+ return update(true);
+}
+
+function addCompletionToDB() {
+ return update(false);
+}
+
+function update(prefix = false) {
+ var length = prefix ? 4 : 32;
+ var testData = [
+ { url: MALWARE_HOST1, db: MALWARE_LIST, len: length },
+ { url: MALWARE_HOST2, db: MALWARE_LIST, len: length },
+ { url: UNWANTED_HOST1, db: UNWANTED_LIST, len: length },
+ { url: UNWANTED_HOST2, db: UNWANTED_LIST, len: length }
+ ];
+
+ return classifierHelper.addUrlToDB(testData)
+ .catch(err => {
+ ok(false, "Couldn't update classifier. Error code: " + errorCode);
+ // Abort test.
+ SimpleTest.finish();
+ });
+}
+
+// This testcase is to make sure gethash works:
+// 1. Add prefixes to DB.
+// 2. Load test frame contains malware & unwanted url, those urls should be blocked.
+// 3. The second step should also trigger a gethash request since completions is not in
+// either cache or DB.
+// 4. Load test frame again, since completions is stored in cache now, no gethash
+// request should be triggered.
+function testGethash() {
+ return Promise.resolve()
+ .then(addPrefixToDB)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
+ .then(reset);
+}
+
+// This testcase is to make sure an update request will clear completion cache:
+// 1. Add prefixes to DB.
+// 2. Load test frame, this should trigger a gethash request
+// 3. Trigger an update, completion cache should be cleared now.
+// 4. Load test frame again, since cache is cleared now, gethash request should be triggered.
+function testUpdateClearCache() {
+ return Promise.resolve()
+ .then(addPrefixToDB)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
+ .then(updateUnusedUrl)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
+ .then(reset);
+}
+
+// This testcae is to make sure completions in update works:
+// 1. Add completions to DB.
+// 2. Load test frame, since completions is stored in DB, gethash request should
+// not be triggered.
+function testUpdate() {
+ return Promise.resolve()
+ .then(addCompletionToDB)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
+ .then(reset);
+}
+
+// This testcase is to make sure an update request will not clear completions in DB:
+// 1. Add completions to DB.
+// 2. Load test frame to make sure completions is stored in database, in this case, gethash
+// should not be triggered.
+// 3. Trigger an update, cache is cleared, but completions in DB should still remain.
+// 4. Load test frame again, since completions is in DB, gethash request should not be triggered.
+function testUpdateNotClearCompletions() {
+ return Promise.resolve()
+ .then(addCompletionToDB)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
+ .then(updateUnusedUrl)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
+ .then(reset);
+}
+
+// This testcase is to make sure completion store in DB will properly load after restarting.
+// 1. Add completions to DB.
+// 2. Simulate firefox restart by calling reloadDatabase.
+// 3. Load test frame, since completions should be loaded from DB, no gethash request should
+// be triggered.
+function testUpdateCompletionsAfterReload() {
+ return Promise.resolve()
+ .then(addCompletionToDB)
+ .then(classifierHelper.reloadDatabase)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
+ .then(reset);
+}
+
+// This testcase is to make sure cache will be cleared after restarting
+// 1. Add prefixes to DB.
+// 2. Load test frame, this should trigger a gethash request and completions will be stored in
+// cache.
+// 3. Load test frame again, no gethash should be triggered because of cache.
+// 4. Simulate firefox restart by calling reloadDatabase.
+// 5. Load test frame again, since cache is cleared, gethash request should be triggered.
+function testGethashCompletionsAfterReload() {
+ return Promise.resolve()
+ .then(addPrefixToDB)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter == gPreGethashCounter, "Gethash request is not triggered."); })
+ .then(classifierHelper.reloadDatabase)
+ .then(loadTestFrame)
+ .then(() => {
+ ok(gCurGethashCounter > gPreGethashCounter, "Gethash request is triggered."); })
+ .then(reset);
+}
+
+function runTest() {
+ Promise.resolve()
+ .then(classifierHelper.waitForInit)
+ .then(setup)
+ .then(testGethash)
+ .then(testUpdateClearCache)
+ .then(testUpdate)
+ .then(testUpdateNotClearCompletions)
+ .then(testUpdateCompletionsAfterReload)
+ .then(testGethashCompletionsAfterReload)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// 'network.predictor.enabled' is disabled because if other testcase load
+// evil.js, evil.css ...etc resources, it may cause we load them from cache
+// directly and bypass classifier check
+SpecialPowers.pushPrefEnv({"set": [
+ ["browser.safebrowsing.malware.enabled", true],
+ ["network.predictor.enabled", false],
+ ["urlclassifier.gethash.timeout_ms", 30000],
+]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_classified_annotations.html b/toolkit/components/url-classifier/tests/mochitest/test_classified_annotations.html
new file mode 100644
index 0000000000..5814fff000
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classified_annotations.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the URI Classifier</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+function cleanup() {
+ SpecialPowers.clearUserPref("privacy.trackingprotection.enabled");
+ SpecialPowers.clearUserPref("channelclassifier.allowlist_example");
+}
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.trackingTable", "test-track-simple"]]},
+ test);
+
+function test() {
+ UrlClassifierTestUtils.addTestTrackers().then(() => {
+ SpecialPowers.setBoolPref("privacy.trackingprotection.enabled", true);
+ // Make sure chrome:// URIs are processed. This does not white-list
+ // any URIs unless 'https://allowlisted.example.com' is added in the
+ // permission manager (see test_allowlisted_annotations.html)
+ SpecialPowers.setBoolPref("channelclassifier.allowlist_example", true);
+ document.getElementById("testFrame").src = "classifiedAnnotatedFrame.html";
+ });
+}
+
+// Expected finish() call is in "classifiedAnnotatedFrame.html".
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_classifier.html b/toolkit/components/url-classifier/tests/mochitest/test_classifier.html
new file mode 100644
index 0000000000..9533db426e
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classifier.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the URI Classifier</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="classifierHelper.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var firstLoad = true;
+
+// Add some URLs to the malware database.
+var testData = [
+ { url: "malware.example.com/",
+ db: "test-malware-simple"
+ },
+ { url: "unwanted.example.com/",
+ db: "test-unwanted-simple"
+ }
+];
+
+function loadTestFrame() {
+ document.getElementById("testFrame").src = "classifierFrame.html";
+}
+
+// Expected finish() call is in "classifierFrame.html".
+SimpleTest.waitForExplicitFinish();
+
+function updateSuccess() {
+ SpecialPowers.pushPrefEnv(
+ {"set" : [["browser.safebrowsing.malware.enabled", true]]},
+ loadTestFrame);
+}
+
+function updateError(errorCode) {
+ ok(false, "Couldn't update classifier. Error code: " + errorCode);
+ // Abort test.
+ SimpleTest.finish();
+}
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.malwareTable", "test-malware-simple,test-unwanted-simple"],
+ ["urlclassifier.phishTable", "test-phish-simple"]]},
+ function() {
+ classifierHelper.waitForInit()
+ .then(() => classifierHelper.addUrlToDB(testData))
+ .then(updateSuccess)
+ .catch(err => {
+ updateError(err);
+ });
+ });
+
+</script>
+
+</pre>
+<iframe id="testFrame" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_classifier_changetablepref.html b/toolkit/components/url-classifier/tests/mochitest/test_classifier_changetablepref.html
new file mode 100644
index 0000000000..7423d3e8ef
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classifier_changetablepref.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1281083 - Changing the urlclassifier.*Table prefs doesn't take effect before the next browser restart.</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="classifierHelper.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+const testTable = "moz-track-digest256";
+const UPDATE_URL = "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/update.sjs";
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+var prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+
+var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+
+// If default preference contain the table we want to test,
+// We should change test table to a different one.
+var trackingTables = SpecialPowers.getCharPref("urlclassifier.trackingTable").split(",");
+ok(!trackingTables.includes(testTable), "test table should not be in the preference");
+
+var listmanager = Cc["@mozilla.org/url-classifier/listmanager;1"].
+ getService(Ci.nsIUrlListManager);
+
+is(listmanager.getGethashUrl(testTable), "",
+ "gethash url for test table should be empty before setting to preference");
+
+function loadTestFrame() {
+ // gethash url of test table "moz-track-digest256" should be updated
+ // after setting preference.
+ var url = listmanager.getGethashUrl(testTable);
+ var expected = SpecialPowers.getCharPref("browser.safebrowsing.provider.mozilla.gethashURL");
+
+ is(url, expected, testTable + " matches its gethash url");
+
+ // Trigger update
+ listmanager.disableUpdate(testTable);
+ listmanager.enableUpdate(testTable);
+ listmanager.maybeToggleUpdateChecking();
+
+ // We wait until "nextupdattime" was set as a signal that update is complete.
+ waitForUpdateSuccess(function() {
+ document.getElementById("testFrame").src = "bug_1281083.html";
+ });
+}
+
+function waitForUpdateSuccess(callback) {
+ let nextupdatetime =
+ SpecialPowers.getCharPref("browser.safebrowsing.provider.mozilla.nextupdatetime");
+
+ if (nextupdatetime !== "1") {
+ callback();
+ return;
+ }
+
+ timer.initWithCallback(function() {
+ waitForUpdateSuccess(callback);
+ }, 10, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+}
+
+function addCompletionToServer(list, url) {
+ return new Promise(function(resolve, reject) {
+ var listParam = "list=" + list;
+ var fullhashParam = "fullhash=" + hash(url);
+
+ var xhr = new XMLHttpRequest;
+ xhr.open("PUT", UPDATE_URL + "?" +
+ listParam + "&" +
+ fullhashParam, true);
+ xhr.setRequestHeader("Content-Type", "text/plain");
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ resolve();
+ }
+ };
+ xhr.send();
+ });
+}
+
+function hash(str) {
+ function bytesFromString(str) {
+ var converter =
+ SpecialPowers.Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(SpecialPowers.Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter.convertToByteArray(str);
+ }
+
+ var hasher = SpecialPowers.Cc["@mozilla.org/security/hash;1"]
+ .createInstance(SpecialPowers.Ci.nsICryptoHash);
+
+ var data = bytesFromString(str);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+}
+
+function runTest() {
+ /**
+ * In this test we try to modify only urlclassifier.*Table preference to see if
+ * url specified in the table will be blocked after update.
+ */
+ var pushPrefPromise = SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.trackingTable", testTable]]});
+
+ // To make sure url is not blocked by an already blocked url.
+ // Here we use non-tracking.example.com as a tracked url.
+ // Since this table is only used in this bug, so it won't affect other testcases.
+ var addCompletePromise =
+ addCompletionToServer(testTable, "bug1281083.example.com/");
+
+ Promise.all([pushPrefPromise, addCompletePromise])
+ .then(() => {
+ loadTestFrame();
+ });
+}
+
+// Set nextupdatetime to 1 to trigger an update
+SpecialPowers.pushPrefEnv(
+ {"set" : [["privacy.trackingprotection.enabled", true],
+ ["channelclassifier.allowlist_example", true],
+ ["browser.safebrowsing.provider.mozilla.nextupdatetime", "1"],
+ ["browser.safebrowsing.provider.mozilla.lists", testTable],
+ ["browser.safebrowsing.provider.mozilla.updateURL", UPDATE_URL]]},
+ runTest);
+
+// Expected finish() call is in "bug_1281083.html".
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
+
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_classifier_worker.html b/toolkit/components/url-classifier/tests/mochitest/test_classifier_worker.html
new file mode 100644
index 0000000000..1f54d45b05
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classifier_worker.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the URI Classifier</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="classifierHelper.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+// Add some URLs to the malware database.
+var testData = [
+ { url: "example.com/tests/toolkit/components/url-classifier/tests/mochitest/evilWorker.js",
+ db: "test-malware-simple"
+ },
+ { url: "example.com/tests/toolkit/components/url-classifier/tests/mochitest/unwantedWorker.js",
+ db: "test-unwanted-simple"
+ }
+];
+
+function loadTestFrame() {
+ document.getElementById("testFrame").src =
+ "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/workerFrame.html";
+}
+
+function onmessage(event)
+{
+ var pieces = event.data.split(':');
+ if (pieces[0] == "finish") {
+ SimpleTest.finish();
+ return;
+ }
+
+ is(pieces[0], "success", pieces[1]);
+}
+
+function updateSuccess() {
+ SpecialPowers.pushPrefEnv(
+ {"set" : [["browser.safebrowsing.malware.enabled", true]]},
+ loadTestFrame);
+}
+
+function updateError(errorCode) {
+ ok(false, "Couldn't update classifier. Error code: " + errorCode);
+ // Abort test.
+ SimpleTest.finish();
+};
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.malwareTable", "test-malware-simple,test-unwanted-simple"],
+ ["urlclassifier.phishTable", "test-phish-simple"]]},
+ function() {
+ classifierHelper.waitForInit()
+ .then(() => classifierHelper.addUrlToDB(testData))
+ .then(updateSuccess)
+ .catch(err => {
+ updateError(err);
+ });
+ });
+
+window.addEventListener("message", onmessage, false);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</pre>
+<iframe id="testFrame" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_classify_ping.html b/toolkit/components/url-classifier/tests/mochitest/test_classify_ping.html
new file mode 100644
index 0000000000..96fa2891a7
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classify_ping.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1233914 - ping doesn't honor the TP list here.</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+ SimpleTest.requestFlakyTimeout("Delay to make sure ping is made prior than XHR");
+
+ const timeout = 200;
+ const host_nottrack = "http://not-tracking.example.com/";
+ const host_track = "http://trackertest.org/";
+ const path_ping = "tests/toolkit/components/url-classifier/tests/mochitest/ping.sjs";
+ const TP_ENABLE_PREF = "privacy.trackingprotection.enabled";
+
+ function testPingNonBlacklist() {
+ SpecialPowers.setBoolPref(TP_ENABLE_PREF, true);
+
+ var msg = "ping should reach page not in blacklist";
+ var expectPing = true;
+ var id = "1111";
+ ping(id, host_nottrack);
+
+ return new Promise(function(resolve, reject) {
+ setTimeout(function() {
+ isPinged(id, expectPing, msg, resolve);
+ }, timeout);
+ });
+ }
+
+ function testPingBlacklistSafebrowsingOff() {
+ SpecialPowers.setBoolPref(TP_ENABLE_PREF, false);
+
+ var msg = "ping should reach page in blacklist when tracking protection is off";
+ var expectPing = true;
+ var id = "2222";
+ ping(id, host_track);
+
+ return new Promise(function(resolve, reject) {
+ setTimeout(function() {
+ isPinged(id, expectPing, msg, resolve);
+ }, timeout);
+ });
+ }
+
+ function testPingBlacklistSafebrowsingOn() {
+ SpecialPowers.setBoolPref(TP_ENABLE_PREF, true);
+
+ var msg = "ping should not reach page in blacklist when tracking protection is on";
+ var expectPing = false;
+ var id = "3333";
+ ping(id, host_track);
+
+ return new Promise(function(resolve, reject) {
+ setTimeout(function() {
+ isPinged(id, expectPing, msg, resolve);
+ }, timeout);
+ });
+ }
+
+ function ping(id, host) {
+ var elm = document.createElement("a");
+ elm.setAttribute('ping', host + path_ping + "?id=" + id);
+ elm.setAttribute('href', "#");
+ document.body.appendChild(elm);
+
+ // Trigger ping.
+ elm.click();
+
+ document.body.removeChild(elm);
+ }
+
+ function isPinged(id, expected, msg, callback) {
+ var url = "http://mochi.test:8888/" + path_ping;
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url + "?id=" + id);
+ xhr.onload = function() {
+ var isPinged = xhr.response === "ping";
+ is(expected, isPinged, msg);
+
+ callback();
+ };
+ xhr.send();
+ }
+
+ function cleanup() {
+ SpecialPowers.clearUserPref(TP_ENABLE_PREF);
+ }
+
+ function runTest() {
+ Promise.resolve()
+ .then(testPingNonBlacklist)
+ .then(testPingBlacklistSafebrowsingOff)
+ .then(testPingBlacklistSafebrowsingOn)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.registerCleanupFunction(cleanup);
+ SpecialPowers.pushPrefEnv({"set": [
+ ["browser.send_pings", true],
+ ["urlclassifier.trackingTable", "test-track-simple"],
+ ]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_classify_track.html b/toolkit/components/url-classifier/tests/mochitest/test_classify_track.html
new file mode 100644
index 0000000000..a868d79602
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_classify_track.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1262406 - Track element doesn't use the URL classifier.</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="classifierHelper.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+ const PREF = "browser.safebrowsing.malware.enabled";
+ const track_path = "tests/toolkit/components/url-classifier/tests/mochitest/basic.vtt";
+ const malware_url = "http://malware.example.com/" + track_path;
+ const validtrack_url = "http://mochi.test:8888/" + track_path;
+
+ var video = document.createElement("video");
+ video.src = "seek.webm";
+ video.crossOrigin = "anonymous";
+
+ document.body.appendChild(video);
+
+ function testValidTrack() {
+ SpecialPowers.setBoolPref(PREF, true);
+
+ return new Promise(function(resolve, reject) {
+ var track = document.createElement("track");
+ track.src = validtrack_url;
+ video.appendChild(track);
+
+ function onload() {
+ ok(true, "Track should be loaded when url is not in blacklist");
+ finish();
+ }
+
+ function onerror() {
+ ok(false, "Error while loading track");
+ finish();
+ }
+
+ function finish() {
+ track.removeEventListener("load", onload);
+ track.removeEventListener("error", onerror)
+ resolve();
+ }
+
+ track.addEventListener("load", onload);
+ track.addEventListener("error", onerror)
+ });
+ }
+
+ function testBlacklistTrackSafebrowsingOff() {
+ SpecialPowers.setBoolPref(PREF, false);
+
+ return new Promise(function(resolve, reject) {
+ var track = document.createElement("track");
+ track.src = malware_url;
+ video.appendChild(track);
+
+ function onload() {
+ ok(true, "Track should be loaded when url is in blacklist and safebrowsing is off");
+ finish();
+ }
+
+ function onerror() {
+ ok(false, "Error while loading track");
+ finish();
+ }
+
+ function finish() {
+ track.removeEventListener("load", onload);
+ track.removeEventListener("error", onerror)
+ resolve();
+ }
+
+ track.addEventListener("load", onload);
+ track.addEventListener("error", onerror)
+ });
+ }
+
+ function testBlacklistTrackSafebrowsingOn() {
+ SpecialPowers.setBoolPref(PREF, true);
+
+ return new Promise(function(resolve, reject) {
+ var track = document.createElement("track");
+
+ // Add a query string parameter here to avoid url classifier bypass classify
+ // because of cache.
+ track.src = malware_url + "?testsbon";
+ video.appendChild(track);
+
+ function onload() {
+ ok(false, "Unexpected result while loading track in blacklist");
+ finish();
+ }
+
+ function onerror() {
+ ok(true, "Track should not be loaded when url is in blacklist and safebrowsing is on");
+ finish();
+ }
+
+ function finish() {
+ track.removeEventListener("load", onload);
+ track.removeEventListener("error", onerror)
+ resolve();
+ }
+
+ track.addEventListener("load", onload);
+ track.addEventListener("error", onerror)
+ });
+ }
+
+ function cleanup() {
+ SpecialPowers.clearUserPref(PREF);
+ }
+
+ function setup() {
+ var testData = [
+ { url: "malware.example.com/",
+ db: "test-malware-simple"
+ }
+ ];
+
+ return classifierHelper.addUrlToDB(testData)
+ .catch(function(err) {
+ ok(false, "Couldn't update classifier. Error code: " + err);
+ // Abort test.
+ SimpleTest.finish();
+ });
+ }
+
+ function runTest() {
+ Promise.resolve()
+ .then(classifierHelper.waitForInit)
+ .then(setup)
+ .then(testValidTrack)
+ .then(testBlacklistTrackSafebrowsingOff)
+ .then(testBlacklistTrackSafebrowsingOn)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.registerCleanupFunction(cleanup);
+ SpecialPowers.pushPrefEnv({"set": [
+ ["media.webvtt.regions.enabled", true],
+ ["urlclassifier.malwareTable", "test-malware-simple"],
+ ]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_donottrack.html b/toolkit/components/url-classifier/tests/mochitest/test_donottrack.html
new file mode 100644
index 0000000000..56003e7eb6
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_donottrack.html
@@ -0,0 +1,150 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1258033 - Fix the DNT loophole for tracking protection</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+const tests = [
+ // DNT turned on and TP turned off, DNT signal sent in both private browsing
+ // and normal mode.
+ {
+ setting: {dntPref:true, tpPref:false, tppbPref:false, pbMode:true},
+ expected: {dnt: "1"},
+ },
+ {
+ setting: {dntPref:true, tpPref:false, tppbPref:false, pbMode:false},
+ expected: {dnt: "1"}
+ },
+ // DNT turned off and TP turned on globally, DNT signal sent in both private
+ // browsing and normal mode.
+ {
+ setting: {dntPref:false, tpPref:true, tppbPref:false, pbMode:true},
+ expected: {dnt: "1"}
+ },
+ {
+ setting: {dntPref:false, tpPref:true, tppbPref:false, pbMode:false},
+ expected: {dnt: "1"}
+ },
+ // DNT turned off and TP in Private Browsing only, DNT signal sent in private
+ // browsing mode only.
+ {
+ setting: {dntPref:false, tpPref:false, tppbPref:true, pbMode:true},
+ expected: {dnt: "1"}
+ },
+ {
+ setting: {dntPref:false, tpPref:false, tppbPref:true, pbMode:false},
+ expected: {dnt: "unspecified"}
+ },
+ // DNT turned off and TP turned off, DNT signal is never sent.
+ {
+ setting: {dntPref:false, tpPref:false, tppbPref:false, pbMode:true},
+ expected: {dnt: "unspecified"}
+ },
+ {
+ setting: {dntPref:false, tpPref:false, tppbPref:false, pbMode:false},
+ expected: {dnt: "unspecified"}
+ },
+]
+
+const DNT_PREF = 'privacy.donottrackheader.enabled';
+const TP_PREF = 'privacy.trackingprotection.enabled';
+const TP_PB_PREF = 'privacy.trackingprotection.pbmode.enabled';
+
+const contentPage =
+ "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/dnt.html";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function executeTest(test) {
+ SpecialPowers.pushPrefEnv({"set" : [
+ [DNT_PREF, test.setting.dntPref],
+ [TP_PREF, test.setting.tpPref],
+ [TP_PB_PREF, test.setting.tppbPref]
+ ]});
+
+ var win = mainWindow.OpenBrowserWindow({private: test.setting.pbMode});
+
+ return new Promise(function(resolve, reject) {
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(contentPage);
+ return;
+ }
+
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+
+ win.content.addEventListener('message', function (event) {
+ let [key, value] = event.data.split("=");
+ if (key == "finish") {
+ win.close();
+ resolve();
+ } else if (key == "navigator.doNotTrack") {
+ is(value, test.expected.dnt, "navigator.doNotTrack should be " + test.expected.dnt);
+ } else if (key == "DNT") {
+ let msg = test.expected.dnt == "1" ? "" : "not ";
+ is(value, test.expected.dnt, "DNT header should " + msg + "be sent");
+ } else {
+ ok(false, "unexpected message");
+ }
+ });
+ }, true);
+ SimpleTest.executeSoon(function() { win.gBrowser.loadURI(contentPage); });
+ });
+ }, true);
+ });
+}
+
+let loop = function loop(index) {
+ if (index >= tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ let test = tests[index];
+ let next = function next() {
+ loop(index + 1);
+ };
+ let result = executeTest(test);
+ result.then(next, next);
+};
+
+SimpleTest.waitForExplicitFinish();
+loop(0);
+
+</script>
+
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_gethash.html b/toolkit/components/url-classifier/tests/mochitest/test_gethash.html
new file mode 100644
index 0000000000..af995e2a54
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_gethash.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1272239 - Test gethash.</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="classifierHelper.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<iframe id="testFrame1" onload=""></iframe>
+<iframe id="testFrame2" onload=""></iframe>
+
+<script class="testbody" type="text/javascript">
+
+const MALWARE_LIST = "test-malware-simple";
+const MALWARE_HOST = "malware.example.com/";
+
+const UNWANTED_LIST = "test-unwanted-simple";
+const UNWANTED_HOST = "unwanted.example.com/";
+
+const GETHASH_URL = "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/gethash.sjs";
+const NOTEXIST_URL = "http://mochi.test:8888/tests/toolkit/components/url-classifier/tests/mochitest/nonexistserver.sjs";
+
+var shouldLoad = false;
+
+// In this testcase we store prefixes to localdb and send the fullhash to gethash server.
+// When access the test page gecko should trigger gethash request to server and
+// get the completion response.
+function loadTestFrame(id) {
+ return new Promise(function(resolve, reject) {
+
+ var iframe = document.getElementById(id);
+ iframe.setAttribute("src", "gethashFrame.html");
+
+ iframe.onload = function() {
+ resolve();
+ };
+ });
+}
+
+// add 4-bytes prefixes to local database, so when we access the url,
+// it will trigger gethash request.
+function addPrefixToDB(list, url) {
+ var testData = [{ db: list, url: url, len: 4 }];
+
+ return classifierHelper.addUrlToDB(testData)
+ .catch(function(err) {
+ ok(false, "Couldn't update classifier. Error code: " + err);
+ // Abort test.
+ SimpleTest.finish();
+ });
+}
+
+// calculate the fullhash and send it to gethash server
+function addCompletionToServer(list, url) {
+ return new Promise(function(resolve, reject) {
+ var listParam = "list=" + list;
+ var fullhashParam = "fullhash=" + hash(url);
+
+ var xhr = new XMLHttpRequest;
+ xhr.open("PUT", GETHASH_URL + "?" +
+ listParam + "&" +
+ fullhashParam, true);
+ xhr.setRequestHeader("Content-Type", "text/plain");
+ xhr.onreadystatechange = function() {
+ if (this.readyState == this.DONE) {
+ resolve();
+ }
+ };
+ xhr.send();
+ });
+}
+
+function hash(str) {
+ function bytesFromString(str) {
+ var converter =
+ SpecialPowers.Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(SpecialPowers.Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter.convertToByteArray(str);
+ }
+
+ var hasher = SpecialPowers.Cc["@mozilla.org/security/hash;1"]
+ .createInstance(SpecialPowers.Ci.nsICryptoHash);
+
+ var data = bytesFromString(str);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+}
+
+function setup404() {
+ shouldLoad = true;
+
+ classifierHelper.allowCompletion([MALWARE_LIST, UNWANTED_LIST], NOTEXIST_URL);
+
+ return Promise.all([
+ addPrefixToDB(MALWARE_LIST, MALWARE_HOST),
+ addPrefixToDB(UNWANTED_LIST, UNWANTED_HOST)
+ ]);
+}
+
+function setup() {
+ classifierHelper.allowCompletion([MALWARE_LIST, UNWANTED_LIST], GETHASH_URL);
+
+ return Promise.all([
+ addPrefixToDB(MALWARE_LIST, MALWARE_HOST),
+ addPrefixToDB(UNWANTED_LIST, UNWANTED_HOST),
+ addCompletionToServer(MALWARE_LIST, MALWARE_HOST),
+ addCompletionToServer(UNWANTED_LIST, UNWANTED_HOST),
+ ]);
+}
+
+// manually reset DB to make sure next test won't be affected by cache.
+function reset() {
+ return classifierHelper.resetDB;
+}
+
+function runTest() {
+ Promise.resolve()
+ // This test resources get blocked when gethash returns successfully
+ .then(classifierHelper.waitForInit)
+ .then(setup)
+ .then(() => loadTestFrame("testFrame1"))
+ .then(reset)
+ // This test resources are not blocked when gethash returns an error
+ .then(setup404)
+ .then(() => loadTestFrame("testFrame2"))
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// 'network.predictor.enabled' is disabled because if other testcase load
+// evil.js, evil.css ...etc resources, it may cause we load them from cache
+// directly and bypass classifier check
+SpecialPowers.pushPrefEnv({"set": [
+ ["browser.safebrowsing.malware.enabled", true],
+ ["network.predictor.enabled", false],
+ ["urlclassifier.gethash.timeout_ms", 30000],
+]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_lookup_system_principal.html b/toolkit/components/url-classifier/tests/mochitest/test_lookup_system_principal.html
new file mode 100644
index 0000000000..fa61e6a00b
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_lookup_system_principal.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that lookup() on a system principal doesn't crash</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script type="text/javascript">
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+
+dbService.lookup(document.nodePrincipal, "", function(arg) {});
+
+ok(true, "lookup() didn't crash");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_privatebrowsing_trackingprotection.html b/toolkit/components/url-classifier/tests/mochitest/test_privatebrowsing_trackingprotection.html
new file mode 100644
index 0000000000..02ef57b467
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_privatebrowsing_trackingprotection.html
@@ -0,0 +1,154 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test Tracking Protection in Private Browsing mode</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+var contentPage = "http://www.itisatrap.org/tests/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function testOnWindow(aPrivate, aCallback) {
+ var win = mainWindow.OpenBrowserWindow({private: aPrivate});
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(contentPage);
+ return;
+ }
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+
+ win.content.addEventListener('load', function innerLoad2() {
+ win.content.removeEventListener('load', innerLoad2, false);
+ SimpleTest.executeSoon(function() { aCallback(win); });
+ }, false, true);
+ }, true);
+ SimpleTest.executeSoon(function() { win.gBrowser.loadURI(contentPage); });
+ });
+ }, true);
+}
+
+var badids = [
+ "badscript",
+ "badimage",
+ "badcss"
+];
+
+function checkLoads(aWindow, aBlocked) {
+ var win = aWindow.content;
+ is(win.document.getElementById("badscript").dataset.touched, aBlocked ? "no" : "yes", "Should not load tracking javascript");
+ is(win.document.getElementById("badimage").dataset.touched, aBlocked ? "no" : "yes", "Should not load tracking images");
+ is(win.document.getElementById("goodscript").dataset.touched, "yes", "Should load whitelisted tracking javascript");
+
+ var elt = win.document.getElementById("styleCheck");
+ var style = win.document.defaultView.getComputedStyle(elt, "");
+ isnot(style.visibility, aBlocked ? "hidden" : "", "Should not load tracking css");
+
+ is(win.document.blockedTrackingNodeCount, aBlocked ? badids.length : 0, "Should identify all tracking elements");
+
+ var blockedTrackingNodes = win.document.blockedTrackingNodes;
+
+ // Make sure that every node in blockedTrackingNodes exists in the tree
+ // (that may not always be the case but do not expect any nodes to disappear
+ // from the tree here)
+ var allNodeMatch = true;
+ for (var i = 0; i < blockedTrackingNodes.length; i++) {
+ var nodeMatch = false;
+ for (var j = 0; j < badids.length && !nodeMatch; j++) {
+ nodeMatch = nodeMatch ||
+ (blockedTrackingNodes[i] == win.document.getElementById(badids[j]));
+ }
+
+ allNodeMatch = allNodeMatch && nodeMatch;
+ }
+ is(allNodeMatch, true, "All annotated nodes are expected in the tree");
+
+ // Make sure that every node with a badid (see badids) is found in the
+ // blockedTrackingNodes. This tells us if we are neglecting to annotate
+ // some nodes
+ allNodeMatch = true;
+ for (var j = 0; j < badids.length; j++) {
+ var nodeMatch = false;
+ for (var i = 0; i < blockedTrackingNodes.length && !nodeMatch; i++) {
+ nodeMatch = nodeMatch ||
+ (blockedTrackingNodes[i] == win.document.getElementById(badids[j]));
+ }
+
+ allNodeMatch = allNodeMatch && nodeMatch;
+ }
+ is(allNodeMatch, aBlocked, "All tracking nodes are expected to be annotated as such");
+}
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.trackingTable", "test-track-simple"],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", true],
+ ["channelclassifier.allowlist_example", true]]},
+ test);
+
+function test() {
+ SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
+ UrlClassifierTestUtils.addTestTrackers().then(() => {
+ // Normal mode, with the pref (trackers should be loaded)
+ testOnWindow(false, function(aWindow) {
+ checkLoads(aWindow, false);
+ aWindow.close();
+
+ // Private Browsing, with the pref (trackers should be blocked)
+ testOnWindow(true, function(aWindow) {
+ checkLoads(aWindow, true);
+ aWindow.close();
+
+ // Private Browsing, without the pref (trackers should be loaded)
+ SpecialPowers.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
+ testOnWindow(true, function(aWindow) {
+ checkLoads(aWindow, false);
+ aWindow.close();
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_safebrowsing_bug1272239.html b/toolkit/components/url-classifier/tests/mochitest/test_safebrowsing_bug1272239.html
new file mode 100644
index 0000000000..8066c2a372
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_safebrowsing_bug1272239.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1272239 - Only tables with provider could register gethash url in listmanager.</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+// List all the tables
+const prefs = [
+ "urlclassifier.phishTable",
+ "urlclassifier.malwareTable",
+ "urlclassifier.downloadBlockTable",
+ "urlclassifier.downloadAllowTable",
+ "urlclassifier.trackingTable",
+ "urlclassifier.trackingWhitelistTable",
+ "urlclassifier.blockedTable"
+];
+
+var prefService = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+
+// Get providers
+var providers = {};
+
+var branch = prefService.getBranch("browser.safebrowsing.provider.");
+var children = branch.getChildList("", {});
+
+for (var child of children) {
+ var prefComponents = child.split(".");
+ var providerName = prefComponents[0];
+ providers[providerName] = {};
+}
+
+// Get lists from |browser.safebrowsing.provider.PROVIDER_NAME.lists| preference.
+var listsWithProvider = [];
+var listsToProvider = [];
+for (var provider in providers) {
+ var pref = "browser.safebrowsing.provider." + provider + ".lists";
+ var list = SpecialPowers.getCharPref(pref).split(",");
+
+ listsToProvider = listsToProvider.concat(list.map( () => { return provider; }));
+ listsWithProvider = listsWithProvider.concat(list);
+}
+
+// Get all the lists
+var lists = [];
+for (var pref of prefs) {
+ lists = lists.concat(SpecialPowers.getCharPref(pref).split(","));
+}
+
+var listmanager = Cc["@mozilla.org/url-classifier/listmanager;1"].
+ getService(Ci.nsIUrlListManager);
+
+for (var list of lists) {
+ if (!list)
+ continue;
+
+ // For lists having a provider, it should have a correct gethash url
+ // For lists without a provider, for example, test-malware-simple, it should not
+ // have a gethash url.
+ var url = listmanager.getGethashUrl(list);
+ var index = listsWithProvider.indexOf(list);
+ if (index >= 0) {
+ var provider = listsToProvider[index];
+ var pref = "browser.safebrowsing.provider." + provider + ".gethashURL";
+ is(url, SpecialPowers.getCharPref(pref), list + " matches its gethash url");
+ } else {
+ is(url, "", list + " should not have a gethash url");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_bug1157081.html b/toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_bug1157081.html
new file mode 100644
index 0000000000..7611dd245b
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_bug1157081.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test Tracking Protection with and without Safe Browsing (Bug #1157081)</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+var contentPage = "chrome://mochitests/content/chrome/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html"
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function testOnWindow(aCallback) {
+ var win = mainWindow.OpenBrowserWindow();
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(contentPage);
+ return;
+ }
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+
+ win.content.addEventListener('load', function innerLoad2() {
+ win.content.removeEventListener('load', innerLoad2, false);
+ SimpleTest.executeSoon(function() { aCallback(win); });
+ }, false, true);
+ }, true);
+ SimpleTest.executeSoon(function() { win.gBrowser.loadURI(contentPage); });
+ });
+ }, true);
+}
+
+var badids = [
+ "badscript"
+];
+
+function checkLoads(aWindow, aBlocked) {
+ var win = aWindow.content;
+ is(win.document.getElementById("badscript").dataset.touched, aBlocked ? "no" : "yes", "Should not load tracking javascript");
+}
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["urlclassifier.trackingTable", "test-track-simple"],
+ ["privacy.trackingprotection.enabled", true],
+ ["browser.safebrowsing.malware.enabled", false],
+ ["browser.safebrowsing.phishing.enabled", false],
+ ["channelclassifier.allowlist_example", true]]},
+ test);
+
+function test() {
+ SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
+ UrlClassifierTestUtils.addTestTrackers().then(() => {
+ // Safe Browsing turned OFF, tracking protection should work nevertheless
+ testOnWindow(function(aWindow) {
+ checkLoads(aWindow, true);
+ aWindow.close();
+
+ // Safe Browsing turned ON, tracking protection should still work
+ SpecialPowers.setBoolPref("browser.safebrowsing.phishing.enabled", true);
+ testOnWindow(function(aWindow) {
+ checkLoads(aWindow, true);
+ aWindow.close();
+ SimpleTest.finish();
+ });
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_whitelist.html b/toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_whitelist.html
new file mode 100644
index 0000000000..29de0dfed8
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_trackingprotection_whitelist.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test Tracking Protection in Private Browsing mode</title>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+var contentPage1 = "http://www.itisatrap.org/tests/toolkit/components/url-classifier/tests/mochitest/whitelistFrame.html";
+var contentPage2 = "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/whitelistFrame.html";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function testOnWindow(contentPage, aCallback) {
+ var win = mainWindow.OpenBrowserWindow();
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ whenDelayedStartupFinished(win, function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ win.gBrowser.loadURI(contentPage);
+ return;
+ }
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+
+ win.content.addEventListener('load', function innerLoad2() {
+ win.content.removeEventListener('load', innerLoad2, false);
+ SimpleTest.executeSoon(function() { aCallback(win); });
+ }, false, true);
+ }, true);
+ SimpleTest.executeSoon(function() { win.gBrowser.loadURI(contentPage); });
+ });
+ }, true);
+}
+
+var alwaysbadids = [
+ "badscript",
+];
+
+function checkLoads(aWindow, aWhitelisted) {
+ var win = aWindow.content;
+ is(win.document.getElementById("badscript").dataset.touched, "no", "Should not load tracking javascript");
+ is(win.document.getElementById("goodscript").dataset.touched, aWhitelisted ? "yes" : "no", "Should load whitelisted tracking javascript");
+
+ var badids = alwaysbadids.slice();
+ if (!aWhitelisted) {
+ badids.push("goodscript");
+ }
+ is(win.document.blockedTrackingNodeCount, badids.length, "Should identify all tracking elements");
+
+ var blockedTrackingNodes = win.document.blockedTrackingNodes;
+
+ // Make sure that every node in blockedTrackingNodes exists in the tree
+ // (that may not always be the case but do not expect any nodes to disappear
+ // from the tree here)
+ var allNodeMatch = true;
+ for (var i = 0; i < blockedTrackingNodes.length; i++) {
+ var nodeMatch = false;
+ for (var j = 0; j < badids.length && !nodeMatch; j++) {
+ nodeMatch = nodeMatch ||
+ (blockedTrackingNodes[i] == win.document.getElementById(badids[j]));
+ }
+
+ allNodeMatch = allNodeMatch && nodeMatch;
+ }
+ is(allNodeMatch, true, "All annotated nodes are expected in the tree");
+
+ // Make sure that every node with a badid (see badids) is found in the
+ // blockedTrackingNodes. This tells us if we are neglecting to annotate
+ // some nodes
+ allNodeMatch = true;
+ for (var j = 0; j < badids.length; j++) {
+ var nodeMatch = false;
+ for (var i = 0; i < blockedTrackingNodes.length && !nodeMatch; i++) {
+ nodeMatch = nodeMatch ||
+ (blockedTrackingNodes[i] == win.document.getElementById(badids[j]));
+ }
+
+ allNodeMatch = allNodeMatch && nodeMatch;
+ }
+ is(allNodeMatch, true, "All tracking nodes are expected to be annotated as such");
+}
+
+SpecialPowers.pushPrefEnv(
+ {"set" : [["privacy.trackingprotection.enabled", true],
+ ["channelclassifier.allowlist_example", true]]},
+ test);
+
+function test() {
+ SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
+ UrlClassifierTestUtils.addTestTrackers().then(() => {
+ // Load the test from a URL on the whitelist
+ testOnWindow(contentPage1, function(aWindow) {
+ checkLoads(aWindow, true);
+ aWindow.close();
+
+ // Load the test from a URL that's NOT on the whitelist
+ testOnWindow(contentPage2, function(aWindow) {
+ checkLoads(aWindow, false);
+ aWindow.close();
+
+ // Load the test from a URL on the whitelist but without the whitelist
+ SpecialPowers.pushPrefEnv({"set" : [["urlclassifier.trackingWhitelistTable", ""]]},
+ function() {
+ testOnWindow(contentPage1, function(aWindow) {
+ checkLoads(aWindow, false);
+ aWindow.close();
+ SimpleTest.finish();
+ });
+ });
+
+ });
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+
+</pre>
+<iframe id="testFrame" width="100%" height="100%" onload=""></iframe>
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/track.html b/toolkit/components/url-classifier/tests/mochitest/track.html
new file mode 100644
index 0000000000..8785e7c5b1
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/track.html
@@ -0,0 +1,7 @@
+<html>
+ <head>
+ </head>
+ <body>
+ <h1>Tracking Works!</h1>
+ </body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/unwantedWorker.js b/toolkit/components/url-classifier/tests/mochitest/unwantedWorker.js
new file mode 100644
index 0000000000..ac34977d71
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/unwantedWorker.js
@@ -0,0 +1,3 @@
+onmessage = function() {
+ postMessage("loaded bad file");
+}
diff --git a/toolkit/components/url-classifier/tests/mochitest/update.sjs b/toolkit/components/url-classifier/tests/mochitest/update.sjs
new file mode 100644
index 0000000000..53efaafdfc
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/update.sjs
@@ -0,0 +1,114 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(request, response)
+{
+ var query = {};
+ request.queryString.split('&').forEach(function (val) {
+ var idx = val.indexOf('=');
+ query[val.slice(0, idx)] = unescape(val.slice(idx + 1));
+ });
+
+ // Store fullhash in the server side.
+ if ("list" in query && "fullhash" in query) {
+ // In the server side we will store:
+ // 1. All the full hashes for a given list
+ // 2. All the lists we have right now
+ // data is separate by '\n'
+ let list = query["list"];
+ let hashes = getState(list);
+
+ let hash = base64ToString(query["fullhash"]);
+ hashes += hash + "\n";
+ setState(list, hashes);
+
+ let lists = getState("lists");
+ if (lists.indexOf(list) == -1) {
+ lists += list + "\n";
+ setState("lists", lists);
+ }
+
+ return;
+ }
+
+ var body = new BinaryInputStream(request.bodyInputStream);
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var responseBody = parseV2Request(bytes);
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write(responseBody);
+}
+
+function parseV2Request(bytes) {
+ var table = String.fromCharCode.apply(this, bytes).slice(0,-2);
+
+ var ret = "";
+ getState("lists").split("\n").forEach(function(list) {
+ if (list == table) {
+ var completions = getState(list).split("\n");
+ ret += "n:1000\n"
+ ret += "i:" + list + "\n";
+ ret += "a:1:32:" + 32*(completions.length - 1) + "\n";
+
+ for (var completion of completions) {
+ ret += completion;
+ }
+ }
+ });
+
+ return ret;
+}
+
+/* Convert Base64 data to a string */
+const toBinaryTable = [
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+ 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
+ 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+ -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+ 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+];
+const base64Pad = '=';
+
+function base64ToString(data) {
+ var result = '';
+ var leftbits = 0; // number of bits decoded, but yet to be appended
+ var leftdata = 0; // bits decoded, but yet to be appended
+
+ // Convert one by one.
+ for (var i = 0; i < data.length; i++) {
+ var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
+ var padding = (data[i] == base64Pad);
+ // Skip illegal characters and whitespace
+ if (c == -1) continue;
+
+ // Collect data into leftdata, update bitcount
+ leftdata = (leftdata << 6) | c;
+ leftbits += 6;
+
+ // If we have 8 or more bits, append 8 bits to the result
+ if (leftbits >= 8) {
+ leftbits -= 8;
+ // Append if not padding.
+ if (!padding)
+ result += String.fromCharCode((leftdata >> leftbits) & 0xff);
+ leftdata &= (1 << leftbits) - 1;
+ }
+ }
+
+ // If there are any bits left, the base64 string was corrupted
+ if (leftbits)
+ throw Components.Exception('Corrupted base64 string');
+
+ return result;
+}
diff --git a/toolkit/components/url-classifier/tests/mochitest/vp9.webm b/toolkit/components/url-classifier/tests/mochitest/vp9.webm
new file mode 100644
index 0000000000..221877e303
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/vp9.webm
Binary files differ
diff --git a/toolkit/components/url-classifier/tests/mochitest/whitelistFrame.html b/toolkit/components/url-classifier/tests/mochitest/whitelistFrame.html
new file mode 100644
index 0000000000..620416fc74
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/whitelistFrame.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+<title></title>
+</head>
+<body>
+
+<script id="badscript" data-touched="not sure" src="http://trackertest.org/tests/toolkit/components/url-classifier/tests/mochitest/evil.js" onload="this.dataset.touched = 'yes';" onerror="this.dataset.touched = 'no';"></script>
+
+<script id="goodscript" data-touched="not sure" src="http://itisatracker.org/tests/toolkit/components/url-classifier/tests/mochitest/good.js" onload="this.dataset.touched = 'yes';" onerror="this.dataset.touched = 'no';"></script>
+
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/mochitest/workerFrame.html b/toolkit/components/url-classifier/tests/mochitest/workerFrame.html
new file mode 100644
index 0000000000..69e8dd0074
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/workerFrame.html
@@ -0,0 +1,65 @@
+<html>
+<head>
+<title></title>
+
+<script type="text/javascript">
+
+function startCleanWorker() {
+ var worker = new Worker("cleanWorker.js");
+
+ worker.onmessage = function(event) {
+ if (event.data == "success") {
+ window.parent.postMessage("success:blocked importScripts('evilWorker.js')", "*");
+ } else {
+ window.parent.postMessage("failure:failed to block importScripts('evilWorker.js')", "*");
+ }
+ window.parent.postMessage("finish", "*");
+ };
+
+ worker.onerror = function(event) {
+ window.parent.postmessage("failure:failed to load cleanWorker.js", "*");
+ window.parent.postMessage("finish", "*");
+ };
+
+ worker.postMessage("");
+}
+
+function startEvilWorker() {
+ var worker = new Worker("evilWorker.js");
+
+ worker.onmessage = function(event) {
+ window.parent.postMessage("failure:failed to block evilWorker.js", "*");
+ startUnwantedWorker();
+ };
+
+ worker.onerror = function(event) {
+ window.parent.postMessage("success:blocked evilWorker.js", "*");
+ startUnwantedWorker();
+ };
+
+ worker.postMessage("");
+}
+
+function startUnwantedWorker() {
+ var worker = new Worker("unwantedWorker.js");
+
+ worker.onmessage = function(event) {
+ window.parent.postMessage("failure:failed to block unwantedWorker.js", "*");
+ startCleanWorker();
+ };
+
+ worker.onerror = function(event) {
+ window.parent.postMessage("success:blocked unwantedWorker.js", "*");
+ startCleanWorker();
+ };
+
+ worker.postMessage("");
+}
+
+</script>
+
+</head>
+
+<body onload="startEvilWorker()">
+</body>
+</html>
diff --git a/toolkit/components/url-classifier/tests/moz.build b/toolkit/components/url-classifier/tests/moz.build
new file mode 100644
index 0000000000..599727ab98
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/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/.
+
+MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['mochitest/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+TESTING_JS_MODULES += [
+ 'UrlClassifierTestUtils.jsm',
+]
+
+if CONFIG['ENABLE_TESTS']:
+ DIRS += ['gtest']
diff --git a/toolkit/components/url-classifier/tests/unit/.eslintrc.js b/toolkit/components/url-classifier/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/url-classifier/tests/unit/data/digest1.chunk b/toolkit/components/url-classifier/tests/unit/data/digest1.chunk
new file mode 100644
index 0000000000..3850373c19
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/data/digest1.chunk
Binary files differ
diff --git a/toolkit/components/url-classifier/tests/unit/data/digest2.chunk b/toolkit/components/url-classifier/tests/unit/data/digest2.chunk
new file mode 100644
index 0000000000..738c96f6ba
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/data/digest2.chunk
@@ -0,0 +1,2 @@
+a:5:32:32
+_H^a7]=#nmnoQ \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js b/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js
new file mode 100644
index 0000000000..21849ced7c
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/head_urlclassifier.js
@@ -0,0 +1,429 @@
+//* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- *
+function dumpn(s) {
+ dump(s + "\n");
+}
+
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+const NS_APP_USER_PROFILE_LOCAL_50_DIR = "ProfLD";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://testing-common/httpd.js");
+
+do_get_profile();
+
+var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+
+var iosvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+
+var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+
+// Disable hashcompleter noise for tests
+var prefBranch = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+prefBranch.setIntPref("urlclassifier.gethashnoise", 0);
+
+// Enable malware/phishing checking for tests
+prefBranch.setBoolPref("browser.safebrowsing.malware.enabled", true);
+prefBranch.setBoolPref("browser.safebrowsing.blockedURIs.enabled", true);
+prefBranch.setBoolPref("browser.safebrowsing.phishing.enabled", true);
+
+// Enable all completions for tests
+prefBranch.setCharPref("urlclassifier.disallow_completions", "");
+
+// Hash completion timeout
+prefBranch.setIntPref("urlclassifier.gethash.timeout_ms", 5000);
+
+function delFile(name) {
+ try {
+ // Delete a previously created sqlite file
+ var file = dirSvc.get('ProfLD', Ci.nsIFile);
+ file.append(name);
+ if (file.exists())
+ file.remove(false);
+ } catch(e) {
+ }
+}
+
+function cleanUp() {
+ delFile("urlclassifier3.sqlite");
+ delFile("safebrowsing/classifier.hashkey");
+ delFile("safebrowsing/test-phish-simple.sbstore");
+ delFile("safebrowsing/test-malware-simple.sbstore");
+ delFile("safebrowsing/test-unwanted-simple.sbstore");
+ delFile("safebrowsing/test-block-simple.sbstore");
+ delFile("safebrowsing/test-track-simple.sbstore");
+ delFile("safebrowsing/test-trackwhite-simple.sbstore");
+ delFile("safebrowsing/test-phish-simple.pset");
+ delFile("safebrowsing/test-malware-simple.pset");
+ delFile("safebrowsing/test-unwanted-simple.pset");
+ delFile("safebrowsing/test-block-simple.pset");
+ delFile("safebrowsing/test-track-simple.pset");
+ delFile("safebrowsing/test-trackwhite-simple.pset");
+ delFile("safebrowsing/moz-phish-simple.sbstore");
+ delFile("safebrowsing/moz-phish-simple.pset");
+ delFile("testLarge.pset");
+ delFile("testNoDelta.pset");
+}
+
+// Update uses allTables by default
+var allTables = "test-phish-simple,test-malware-simple,test-unwanted-simple,test-track-simple,test-trackwhite-simple,test-block-simple";
+var mozTables = "moz-phish-simple";
+
+var dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIUrlClassifierDBService);
+var streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"]
+ .getService(Ci.nsIUrlClassifierStreamUpdater);
+
+
+/*
+ * Builds an update from an object that looks like:
+ *{ "test-phish-simple" : [{
+ * "chunkType" : "a", // 'a' is assumed if not specified
+ * "chunkNum" : 1, // numerically-increasing chunk numbers are assumed
+ * // if not specified
+ * "urls" : [ "foo.com/a", "foo.com/b", "bar.com/" ]
+ * }
+ */
+
+function buildUpdate(update, hashSize) {
+ if (!hashSize) {
+ hashSize = 32;
+ }
+ var updateStr = "n:1000\n";
+
+ for (var tableName in update) {
+ if (tableName != "")
+ updateStr += "i:" + tableName + "\n";
+ var chunks = update[tableName];
+ for (var j = 0; j < chunks.length; j++) {
+ var chunk = chunks[j];
+ var chunkType = chunk.chunkType ? chunk.chunkType : 'a';
+ var chunkNum = chunk.chunkNum ? chunk.chunkNum : j;
+ updateStr += chunkType + ':' + chunkNum + ':' + hashSize;
+
+ if (chunk.urls) {
+ var chunkData = chunk.urls.join("\n");
+ updateStr += ":" + chunkData.length + "\n" + chunkData;
+ }
+
+ updateStr += "\n";
+ }
+ }
+
+ return updateStr;
+}
+
+function buildPhishingUpdate(chunks, hashSize) {
+ return buildUpdate({"test-phish-simple" : chunks}, hashSize);
+}
+
+function buildMalwareUpdate(chunks, hashSize) {
+ return buildUpdate({"test-malware-simple" : chunks}, hashSize);
+}
+
+function buildUnwantedUpdate(chunks, hashSize) {
+ return buildUpdate({"test-unwanted-simple" : chunks}, hashSize);
+}
+
+function buildBlockedUpdate(chunks, hashSize) {
+ return buildUpdate({"test-block-simple" : chunks}, hashSize);
+}
+
+function buildMozPhishingUpdate(chunks, hashSize) {
+ return buildUpdate({"moz-phish-simple" : chunks}, hashSize);
+}
+
+function buildBareUpdate(chunks, hashSize) {
+ return buildUpdate({"" : chunks}, hashSize);
+}
+
+/**
+ * Performs an update of the dbservice manually, bypassing the stream updater
+ */
+function doSimpleUpdate(updateText, success, failure) {
+ var listener = {
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ updateUrlRequested: function(url) { },
+ streamFinished: function(status) { },
+ updateError: function(errorCode) { failure(errorCode); },
+ updateSuccess: function(requestedTimeout) { success(requestedTimeout); }
+ };
+
+ dbservice.beginUpdate(listener, allTables);
+ dbservice.beginStream("", "");
+ dbservice.updateStream(updateText);
+ dbservice.finishStream();
+ dbservice.finishUpdate();
+}
+
+/**
+ * Simulates a failed database update.
+ */
+function doErrorUpdate(tables, success, failure) {
+ var listener = {
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ updateUrlRequested: function(url) { },
+ streamFinished: function(status) { },
+ updateError: function(errorCode) { success(errorCode); },
+ updateSuccess: function(requestedTimeout) { failure(requestedTimeout); }
+ };
+
+ dbservice.beginUpdate(listener, tables, null);
+ dbservice.beginStream("", "");
+ dbservice.cancelUpdate();
+}
+
+/**
+ * Performs an update of the dbservice using the stream updater and a
+ * data: uri
+ */
+function doStreamUpdate(updateText, success, failure, downloadFailure) {
+ var dataUpdate = "data:," + encodeURIComponent(updateText);
+
+ if (!downloadFailure) {
+ downloadFailure = failure;
+ }
+
+ streamUpdater.downloadUpdates(allTables, "", true,
+ dataUpdate, success, failure, downloadFailure);
+}
+
+var gAssertions = {
+
+tableData : function(expectedTables, cb)
+{
+ dbservice.getTables(function(tables) {
+ // rebuild the tables in a predictable order.
+ var parts = tables.split("\n");
+ while (parts[parts.length - 1] == '') {
+ parts.pop();
+ }
+ parts.sort();
+ tables = parts.join("\n");
+
+ do_check_eq(tables, expectedTables);
+ cb();
+ });
+},
+
+checkUrls: function(urls, expected, cb, useMoz = false)
+{
+ // work with a copy of the list.
+ urls = urls.slice(0);
+ var doLookup = function() {
+ if (urls.length > 0) {
+ var tables = useMoz ? mozTables : allTables;
+ var fragment = urls.shift();
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + fragment, null, null), {});
+ dbservice.lookup(principal, tables,
+ function(arg) {
+ do_check_eq(expected, arg);
+ doLookup();
+ }, true);
+ } else {
+ cb();
+ }
+ };
+ doLookup();
+},
+
+checkTables: function(url, expected, cb)
+{
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + url, null, null), {});
+ dbservice.lookup(principal, allTables, function(tables) {
+ // Rebuild tables in a predictable order.
+ var parts = tables.split(",");
+ while (parts[parts.length - 1] == '') {
+ parts.pop();
+ }
+ parts.sort();
+ tables = parts.join(",");
+ do_check_eq(tables, expected);
+ cb();
+ }, true);
+},
+
+urlsDontExist: function(urls, cb)
+{
+ this.checkUrls(urls, '', cb);
+},
+
+urlsExist: function(urls, cb)
+{
+ this.checkUrls(urls, 'test-phish-simple', cb);
+},
+
+malwareUrlsExist: function(urls, cb)
+{
+ this.checkUrls(urls, 'test-malware-simple', cb);
+},
+
+unwantedUrlsExist: function(urls, cb)
+{
+ this.checkUrls(urls, 'test-unwanted-simple', cb);
+},
+
+blockedUrlsExist: function(urls, cb)
+{
+ this.checkUrls(urls, 'test-block-simple', cb);
+},
+
+mozPhishingUrlsExist: function(urls, cb)
+{
+ this.checkUrls(urls, 'moz-phish-simple', cb, true);
+},
+
+subsDontExist: function(urls, cb)
+{
+ // XXX: there's no interface for checking items in the subs table
+ cb();
+},
+
+subsExist: function(urls, cb)
+{
+ // XXX: there's no interface for checking items in the subs table
+ cb();
+},
+
+urlExistInMultipleTables: function(data, cb)
+{
+ this.checkTables(data["url"], data["tables"], cb);
+}
+
+};
+
+/**
+ * Check a set of assertions against the gAssertions table.
+ */
+function checkAssertions(assertions, doneCallback)
+{
+ var checkAssertion = function() {
+ for (var i in assertions) {
+ var data = assertions[i];
+ delete assertions[i];
+ gAssertions[i](data, checkAssertion);
+ return;
+ }
+
+ doneCallback();
+ }
+
+ checkAssertion();
+}
+
+function updateError(arg)
+{
+ do_throw(arg);
+}
+
+// Runs a set of updates, and then checks a set of assertions.
+function doUpdateTest(updates, assertions, successCallback, errorCallback) {
+ var errorUpdate = function() {
+ checkAssertions(assertions, errorCallback);
+ }
+
+ var runUpdate = function() {
+ if (updates.length > 0) {
+ var update = updates.shift();
+ doStreamUpdate(update, runUpdate, errorUpdate, null);
+ } else {
+ checkAssertions(assertions, successCallback);
+ }
+ }
+
+ runUpdate();
+}
+
+var gTests;
+var gNextTest = 0;
+
+function runNextTest()
+{
+ if (gNextTest >= gTests.length) {
+ do_test_finished();
+ return;
+ }
+
+ dbservice.resetDatabase();
+ dbservice.setHashCompleter('test-phish-simple', null);
+
+ let test = gTests[gNextTest++];
+ dump("running " + test.name + "\n");
+ test();
+}
+
+function runTests(tests)
+{
+ gTests = tests;
+ runNextTest();
+}
+
+var timerArray = [];
+
+function Timer(delay, cb) {
+ this.cb = cb;
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, delay, timer.TYPE_ONE_SHOT);
+ timerArray.push(timer);
+}
+
+Timer.prototype = {
+QueryInterface: function(iid) {
+ if (!iid.equals(Ci.nsISupports) && !iid.equals(Ci.nsITimerCallback)) {
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+notify: function(timer) {
+ this.cb();
+ }
+}
+
+// LFSRgenerator is a 32-bit linear feedback shift register random number
+// generator. It is highly predictable and is not intended to be used for
+// cryptography but rather to allow easier debugging than a test that uses
+// Math.random().
+function LFSRgenerator(seed) {
+ // Force |seed| to be a number.
+ seed = +seed;
+ // LFSR generators do not work with a value of 0.
+ if (seed == 0)
+ seed = 1;
+
+ this._value = seed;
+}
+LFSRgenerator.prototype = {
+ // nextNum returns a random unsigned integer of in the range [0,2^|bits|].
+ nextNum: function(bits) {
+ if (!bits)
+ bits = 32;
+
+ let val = this._value;
+ // Taps are 32, 22, 2 and 1.
+ let bit = ((val >>> 0) ^ (val >>> 10) ^ (val >>> 30) ^ (val >>> 31)) & 1;
+ val = (val >>> 1) | (bit << 31);
+ this._value = val;
+
+ return (val >>> (32 - bits));
+ },
+};
+
+cleanUp();
diff --git a/toolkit/components/url-classifier/tests/unit/tail_urlclassifier.js b/toolkit/components/url-classifier/tests/unit/tail_urlclassifier.js
new file mode 100644
index 0000000000..37f39d1a8d
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/tail_urlclassifier.js
@@ -0,0 +1 @@
+cleanUp();
diff --git a/toolkit/components/url-classifier/tests/unit/test_addsub.js b/toolkit/components/url-classifier/tests/unit/test_addsub.js
new file mode 100644
index 0000000000..1ed65c7baa
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_addsub.js
@@ -0,0 +1,488 @@
+
+function doTest(updates, assertions)
+{
+ doUpdateTest(updates, assertions, runNextTest, updateError);
+}
+
+// Test an add of two urls to a fresh database
+function testSimpleAdds() {
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls
+ };
+
+ doTest([update], assertions);
+}
+
+// Same as testSimpleAdds, but make the same-domain URLs come from different
+// chunks.
+function testMultipleAdds() {
+ var add1Urls = [ "foo.com/a", "bar.com/c" ];
+ var add2Urls = [ "foo.com/b" ];
+
+ var update = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls },
+ { "chunkNum" : 2,
+ "urls" : add2Urls }]);
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2",
+ "urlsExist" : add1Urls.concat(add2Urls)
+ };
+
+ doTest([update], assertions);
+}
+
+// Test that a sub will remove an existing add
+function testSimpleSub()
+{
+ var addUrls = ["foo.com/a", "bar.com/b"];
+ var subUrls = ["1:foo.com/a"];
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1, // adds and subtracts don't share a chunk numbering space
+ "urls": addUrls }]);
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 50,
+ "chunkType" : "s",
+ "urls": subUrls }]);
+
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1:s:50",
+ "urlsExist" : [ "bar.com/b" ],
+ "urlsDontExist": ["foo.com/a" ],
+ "subsDontExist" : [ "foo.com/a" ]
+ }
+
+ doTest([addUpdate, subUpdate], assertions);
+
+}
+
+// Same as testSimpleSub(), but the sub comes in before the add.
+function testSubEmptiesAdd()
+{
+ var subUrls = ["1:foo.com/a"];
+ var addUrls = ["foo.com/a", "bar.com/b"];
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 50,
+ "chunkType" : "s",
+ "urls": subUrls }]);
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls": addUrls }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1:s:50",
+ "urlsExist" : [ "bar.com/b" ],
+ "urlsDontExist": ["foo.com/a" ],
+ "subsDontExist" : [ "foo.com/a" ] // this sub was found, it shouldn't exist anymore
+ }
+
+ doTest([subUpdate, addUpdate], assertions);
+}
+
+// Very similar to testSubEmptiesAdd, except that the domain entry will
+// still have an item left over that needs to be synced.
+function testSubPartiallyEmptiesAdd()
+{
+ var subUrls = ["1:foo.com/a"];
+ var addUrls = ["foo.com/a", "foo.com/b", "bar.com/b"];
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls": subUrls }]);
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1, // adds and subtracts don't share a chunk numbering space
+ "urls": addUrls }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1:s:1",
+ "urlsExist" : [ "foo.com/b", "bar.com/b" ],
+ "urlsDontExist" : ["foo.com/a" ],
+ "subsDontExist" : [ "foo.com/a" ] // this sub was found, it shouldn't exist anymore
+ }
+
+ doTest([subUpdate, addUpdate], assertions);
+}
+
+// We SHOULD be testing that pending subs are removed using
+// subsDontExist assertions. Since we don't have a good interface for getting
+// at sub entries, we'll verify it by side-effect. Subbing a url once
+// then adding it twice should leave the url intact.
+function testPendingSubRemoved()
+{
+ var subUrls = ["1:foo.com/a", "2:foo.com/b"];
+ var addUrls = ["foo.com/a", "foo.com/b"];
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls": subUrls }]);
+
+ var addUpdate1 = buildPhishingUpdate(
+ [{ "chunkNum" : 1, // adds and subtracts don't share a chunk numbering space
+ "urls": addUrls }]);
+
+ var addUpdate2 = buildPhishingUpdate(
+ [{ "chunkNum" : 2,
+ "urls": addUrls }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2:s:1",
+ "urlsExist" : [ "foo.com/a", "foo.com/b" ],
+ "subsDontExist" : [ "foo.com/a", "foo.com/b" ] // this sub was found, it shouldn't exist anymore
+ }
+
+ doTest([subUpdate, addUpdate1, addUpdate2], assertions);
+}
+
+// Make sure that a saved sub is removed when the sub chunk is expired.
+function testPendingSubExpire()
+{
+ var subUrls = ["1:foo.com/a", "1:foo.com/b"];
+ var addUrls = ["foo.com/a", "foo.com/b"];
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls": subUrls }]);
+
+ var expireUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "sd" }]);
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1, // adds and subtracts don't share a chunk numbering space
+ "urls": addUrls }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : [ "foo.com/a", "foo.com/b" ],
+ "subsDontExist" : [ "foo.com/a", "foo.com/b" ] // this sub was expired
+ }
+
+ doTest([subUpdate, expireUpdate, addUpdate], assertions);
+}
+
+// Make sure that the sub url removes from only the chunk that it specifies
+function testDuplicateAdds()
+{
+ var urls = ["foo.com/a"];
+
+ var addUpdate1 = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls": urls }]);
+ var addUpdate2 = buildPhishingUpdate(
+ [{ "chunkNum" : 2,
+ "urls": urls }]);
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 3,
+ "chunkType" : "s",
+ "urls": ["2:foo.com/a"]}]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2:s:3",
+ "urlsExist" : [ "foo.com/a"],
+ "subsDontExist" : [ "foo.com/a"]
+ }
+
+ doTest([addUpdate1, addUpdate2, subUpdate], assertions);
+}
+
+// Tests a sub which matches some existing adds but leaves others.
+function testSubPartiallyMatches()
+{
+ var subUrls = ["foo.com/a"];
+ var addUrls = ["1:foo.com/a", "2:foo.com/b"];
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : addUrls }]);
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls" : addUrls }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1:s:1",
+ "urlsDontExist" : ["foo.com/a"],
+ "subsDontExist" : ["foo.com/a"],
+ "subsExist" : ["foo.com/b"]
+ };
+
+ doTest([addUpdate, subUpdate], assertions);
+}
+
+// XXX: because subsExist isn't actually implemented, this is the same
+// test as above but with a second add chunk that should fail to be added
+// because of a pending sub chunk.
+function testSubPartiallyMatches2()
+{
+ var addUrls = ["foo.com/a"];
+ var subUrls = ["1:foo.com/a", "2:foo.com/b"];
+ var addUrls2 = ["foo.com/b"];
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : addUrls }]);
+
+ var subUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls" : subUrls }]);
+
+ var addUpdate2 = buildPhishingUpdate(
+ [{ "chunkNum" : 2,
+ "urls" : addUrls2 }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2:s:1",
+ "urlsDontExist" : ["foo.com/a", "foo.com/b"],
+ "subsDontExist" : ["foo.com/a", "foo.com/b"]
+ };
+
+ doTest([addUpdate, subUpdate, addUpdate2], assertions);
+}
+
+// Verify that two subs for the same domain but from different chunks
+// match (tests that existing sub entries are properly updated)
+function testSubsDifferentChunks() {
+ var subUrls1 = [ "3:foo.com/a" ];
+ var subUrls2 = [ "3:foo.com/b" ];
+
+ var addUrls = [ "foo.com/a", "foo.com/b", "foo.com/c" ];
+
+ var subUpdate1 = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls": subUrls1 }]);
+ var subUpdate2 = buildPhishingUpdate(
+ [{ "chunkNum" : 2,
+ "chunkType" : "s",
+ "urls" : subUrls2 }]);
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 3,
+ "urls" : addUrls }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:3:s:1-2",
+ "urlsExist" : [ "foo.com/c" ],
+ "urlsDontExist" : [ "foo.com/a", "foo.com/b" ],
+ "subsDontExist" : [ "foo.com/a", "foo.com/b" ]
+ };
+
+ doTest([subUpdate1, subUpdate2, addUpdate], assertions);
+}
+
+// for bug 534079
+function testSubsDifferentChunksSameHostId() {
+ var subUrls1 = [ "1:foo.com/a" ];
+ var subUrls2 = [ "1:foo.com/b", "2:foo.com/c" ];
+
+ var addUrls = [ "foo.com/a", "foo.com/b" ];
+ var addUrls2 = [ "foo.com/c" ];
+
+ var subUpdate1 = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls": subUrls1 }]);
+ var subUpdate2 = buildPhishingUpdate(
+ [{ "chunkNum" : 2,
+ "chunkType" : "s",
+ "urls" : subUrls2 }]);
+
+ var addUpdate = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : addUrls }]);
+ var addUpdate2 = buildPhishingUpdate(
+ [{ "chunkNum" : 2,
+ "urls" : addUrls2 }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2:s:1-2",
+ "urlsDontExist" : [ "foo.com/c", "foo.com/b", "foo.com/a", ],
+ };
+
+ doTest([addUpdate, addUpdate2, subUpdate1, subUpdate2], assertions);
+}
+
+// Test lists of expired chunks
+function testExpireLists() {
+ var addUpdate = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : [ "foo.com/a" ]
+ },
+ { "chunkNum" : 3,
+ "urls" : [ "bar.com/a" ]
+ },
+ { "chunkNum" : 4,
+ "urls" : [ "baz.com/a" ]
+ },
+ { "chunkNum" : 5,
+ "urls" : [ "blah.com/a" ]
+ },
+ ]);
+ var subUpdate = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "chunkType" : "s",
+ "urls" : [ "50:foo.com/1" ]
+ },
+ { "chunkNum" : 2,
+ "chunkType" : "s",
+ "urls" : [ "50:bar.com/1" ]
+ },
+ { "chunkNum" : 3,
+ "chunkType" : "s",
+ "urls" : [ "50:baz.com/1" ]
+ },
+ { "chunkNum" : 5,
+ "chunkType" : "s",
+ "urls" : [ "50:blah.com/1" ]
+ },
+ ]);
+
+ var expireUpdate = buildPhishingUpdate(
+ [ { "chunkType" : "ad:1,3-5" },
+ { "chunkType" : "sd:1-3,5" }]);
+
+ var assertions = {
+ // "tableData" : "test-phish-simple;"
+ "tableData": ""
+ };
+
+ doTest([addUpdate, subUpdate, expireUpdate], assertions);
+}
+
+// Test a duplicate add chunk.
+function testDuplicateAddChunks() {
+ var addUrls1 = [ "foo.com/a" ];
+ var addUrls2 = [ "bar.com/b" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls1
+ },
+ { "chunkNum" : 1,
+ "urls" : addUrls2
+ }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls1,
+ "urlsDontExist" : addUrls2
+ };
+
+ doTest([update], assertions);
+}
+
+// This test is a bit tricky. We want to test that an add removes all
+// subs with the same add chunk id, even if there is no match. To do
+// that we need to add the same add chunk twice, with an expiration
+// in the middle. This would be easier if subsDontExist actually
+// worked...
+function testExpireWholeSub()
+{
+ var subUrls = ["1:foo.com/a"];
+
+ var update = buildPhishingUpdate(
+ [{ "chunkNum" : 5,
+ "chunkType" : "s",
+ "urls" : subUrls
+ },
+ // empty add chunk should still cause foo.com/a to go away.
+ { "chunkNum" : 1,
+ "urls" : []
+ },
+ // and now adding chunk 1 again with foo.com/a should succeed,
+ // because the sub should have been expired with the empty
+ // add chunk.
+
+ // we need to expire this chunk to let us add chunk 1 again.
+ {
+ "chunkType" : "ad:1"
+ },
+ { "chunkNum" : 1,
+ "urls" : [ "foo.com/a" ]
+ }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1:s:5",
+ "urlsExist" : ["foo.com/a"]
+ };
+
+ doTest([update], assertions);
+}
+
+
+// This test is roughly the opposite of testExpireWholeSub(). We add
+// the empty add first, and make sure that it prevents a sub for that
+// add from being applied.
+function testPreventWholeSub()
+{
+ var subUrls = ["1:foo.com/a"];
+
+ var update = buildPhishingUpdate(
+ [ // empty add chunk should cause foo.com/a to not be saved
+ { "chunkNum" : 1,
+ "urls" : []
+ },
+ { "chunkNum" : 5,
+ "chunkType" : "s",
+ "urls" : subUrls
+ },
+ // and now adding chunk 1 again with foo.com/a should succeed,
+ // because the sub should have been expired with the empty
+ // add chunk.
+
+ // we need to expire this chunk to let us add chunk 1 again.
+ {
+ "chunkType" : "ad:1"
+ },
+ { "chunkNum" : 1,
+ "urls" : [ "foo.com/a" ]
+ }]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1:s:5",
+ "urlsExist" : ["foo.com/a"]
+ };
+
+ doTest([update], assertions);
+}
+
+function run_test()
+{
+ runTests([
+ testSimpleAdds,
+ testMultipleAdds,
+ testSimpleSub,
+ testSubEmptiesAdd,
+ testSubPartiallyEmptiesAdd,
+ testPendingSubRemoved,
+ testPendingSubExpire,
+ testDuplicateAdds,
+ testSubPartiallyMatches,
+ testSubPartiallyMatches2,
+ testSubsDifferentChunks,
+ testSubsDifferentChunksSameHostId,
+ testExpireLists
+ ]);
+}
+
+do_test_pending();
diff --git a/toolkit/components/url-classifier/tests/unit/test_backoff.js b/toolkit/components/url-classifier/tests/unit/test_backoff.js
new file mode 100644
index 0000000000..365568c479
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_backoff.js
@@ -0,0 +1,89 @@
+// Some unittests (e.g., paste into JS shell)
+var jslib = Cc["@mozilla.org/url-classifier/jslib;1"].
+ getService().wrappedJSObject;
+var _Datenow = jslib.Date.now;
+function setNow(time) {
+ jslib.Date.now = function() {
+ return time;
+ }
+}
+
+function run_test() {
+ // 3 errors, 1ms retry period, max 3 requests per ten milliseconds,
+ // 5ms backoff interval, 19ms max delay
+ var rb = new jslib.RequestBackoff(3, 1, 3, 10, 5, 19);
+ setNow(1);
+ rb.noteServerResponse(200);
+ do_check_true(rb.canMakeRequest());
+ setNow(2);
+ do_check_true(rb.canMakeRequest());
+
+ // First error should trigger a 1ms delay
+ rb.noteServerResponse(500);
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 3);
+ setNow(3);
+ do_check_true(rb.canMakeRequest());
+
+ // Second error should also trigger a 1ms delay
+ rb.noteServerResponse(500);
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 4);
+ setNow(4);
+ do_check_true(rb.canMakeRequest());
+
+ // Third error should trigger a 5ms backoff
+ rb.noteServerResponse(500);
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 9);
+ setNow(9);
+ do_check_true(rb.canMakeRequest());
+
+ // Trigger backoff again
+ rb.noteServerResponse(503);
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 19);
+ setNow(19);
+ do_check_true(rb.canMakeRequest());
+
+ // Trigger backoff a third time and hit max timeout
+ rb.noteServerResponse(302);
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 38);
+ setNow(38);
+ do_check_true(rb.canMakeRequest());
+
+ // One more backoff, should still be at the max timeout
+ rb.noteServerResponse(400);
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 57);
+ setNow(57);
+ do_check_true(rb.canMakeRequest());
+
+ // Request goes through
+ rb.noteServerResponse(200);
+ do_check_true(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 0);
+ setNow(58);
+ rb.noteServerResponse(500);
+
+ // Another error, should trigger a 1ms backoff
+ do_check_false(rb.canMakeRequest());
+ do_check_eq(rb.nextRequestTime_, 59);
+
+ setNow(59);
+ do_check_true(rb.canMakeRequest());
+
+ setNow(200);
+ rb.noteRequest();
+ setNow(201);
+ rb.noteRequest();
+ setNow(202);
+ do_check_true(rb.canMakeRequest());
+ rb.noteRequest();
+ do_check_false(rb.canMakeRequest());
+ setNow(211);
+ do_check_true(rb.canMakeRequest());
+
+ jslib.Date.now = _Datenow;
+}
diff --git a/toolkit/components/url-classifier/tests/unit/test_bug1274685_unowned_list.js b/toolkit/components/url-classifier/tests/unit/test_bug1274685_unowned_list.js
new file mode 100644
index 0000000000..037bc7b884
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_bug1274685_unowned_list.js
@@ -0,0 +1,32 @@
+Cu.import("resource://gre/modules/SafeBrowsing.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/AppInfo.jsm");
+
+// 'Cc["@mozilla.org/xre/app-info;1"]' for xpcshell has no nsIXULAppInfo
+// so that we have to update it to make nsURLFormatter.js happy.
+// (SafeBrowsing.init() will indirectly use nsURLFormatter.js)
+updateAppInfo();
+
+function run_test() {
+ SafeBrowsing.init();
+
+ let origList = Services.prefs.getCharPref("browser.safebrowsing.provider.google.lists");
+
+ // Remove 'goog-malware-shavar' from the original.
+ let trimmedList = origList.replace('goog-malware-shavar,', '');
+ Services.prefs.setCharPref("browser.safebrowsing.provider.google.lists", trimmedList);
+
+ try {
+ // Bug 1274685 - Unowned Safe Browsing tables break list updates
+ //
+ // If SafeBrowsing.registerTableWithURLs() doesn't check if
+ // a provider is found before registering table, an exception
+ // will be thrown while accessing a null object.
+ //
+ SafeBrowsing.registerTables();
+ } catch (e) {
+ ok(false, 'Exception thrown due to ' + e.toString());
+ }
+
+ Services.prefs.setCharPref("browser.safebrowsing.provider.google.lists", origList);
+}
diff --git a/toolkit/components/url-classifier/tests/unit/test_dbservice.js b/toolkit/components/url-classifier/tests/unit/test_dbservice.js
new file mode 100644
index 0000000000..4b01e7016c
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_dbservice.js
@@ -0,0 +1,314 @@
+var checkUrls = [];
+var checkExpect;
+
+var chunk1Urls = [
+ "test.com/aba",
+ "test.com/foo/bar",
+ "foo.bar.com/a/b/c"
+];
+var chunk1 = chunk1Urls.join("\n");
+
+var chunk2Urls = [
+ "blah.com/a",
+ "baz.com/",
+ "255.255.0.1/",
+ "www.foo.com/test2?param=1"
+];
+var chunk2 = chunk2Urls.join("\n");
+
+var chunk3Urls = [
+ "test.com/a",
+ "foo.bar.com/a",
+ "blah.com/a",
+ ];
+var chunk3 = chunk3Urls.join("\n");
+
+var chunk3SubUrls = [
+ "1:test.com/a",
+ "1:foo.bar.com/a",
+ "2:blah.com/a" ];
+var chunk3Sub = chunk3SubUrls.join("\n");
+
+var chunk4Urls = [
+ "a.com/b",
+ "b.com/c",
+ ];
+var chunk4 = chunk4Urls.join("\n");
+
+var chunk5Urls = [
+ "d.com/e",
+ "f.com/g",
+ ];
+var chunk5 = chunk5Urls.join("\n");
+
+var chunk6Urls = [
+ "h.com/i",
+ "j.com/k",
+ ];
+var chunk6 = chunk6Urls.join("\n");
+
+var chunk7Urls = [
+ "l.com/m",
+ "n.com/o",
+ ];
+var chunk7 = chunk7Urls.join("\n");
+
+// we are going to add chunks 1, 2, 4, 5, and 6 to phish-simple,
+// chunk 2 to malware-simple, and chunk 3 to unwanted-simple,
+// and chunk 7 to block-simple.
+// Then we'll remove the urls in chunk3 from phish-simple, then
+// expire chunk 1 and chunks 4-7 from phish-simple.
+var phishExpected = {};
+var phishUnexpected = {};
+var malwareExpected = {};
+var unwantedExpected = {};
+var blockedExpected = {};
+for (var i = 0; i < chunk2Urls.length; i++) {
+ phishExpected[chunk2Urls[i]] = true;
+ malwareExpected[chunk2Urls[i]] = true;
+}
+for (var i = 0; i < chunk3Urls.length; i++) {
+ unwantedExpected[chunk3Urls[i]] = true;
+ delete phishExpected[chunk3Urls[i]];
+ phishUnexpected[chunk3Urls[i]] = true;
+}
+for (var i = 0; i < chunk1Urls.length; i++) {
+ // chunk1 urls are expired
+ phishUnexpected[chunk1Urls[i]] = true;
+}
+for (var i = 0; i < chunk4Urls.length; i++) {
+ // chunk4 urls are expired
+ phishUnexpected[chunk4Urls[i]] = true;
+}
+for (var i = 0; i < chunk5Urls.length; i++) {
+ // chunk5 urls are expired
+ phishUnexpected[chunk5Urls[i]] = true;
+}
+for (var i = 0; i < chunk6Urls.length; i++) {
+ // chunk6 urls are expired
+ phishUnexpected[chunk6Urls[i]] = true;
+}
+for (var i = 0; i < chunk7Urls.length; i++) {
+ blockedExpected[chunk7Urls[i]] = true;
+ // chunk7 urls are expired
+ phishUnexpected[chunk7Urls[i]] = true;
+}
+
+// Check that the entries hit based on sub-parts
+phishExpected["baz.com/foo/bar"] = true;
+phishExpected["foo.bar.baz.com/foo"] = true;
+phishExpected["bar.baz.com/"] = true;
+
+var numExpecting;
+
+function testFailure(arg) {
+ do_throw(arg);
+}
+
+function checkNoHost()
+{
+ // Looking up a no-host uri such as a data: uri should throw an exception.
+ var exception;
+ try {
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("data:text/html,<b>test</b>", null, null), {});
+ dbservice.lookup(principal, allTables);
+
+ exception = false;
+ } catch(e) {
+ exception = true;
+ }
+ do_check_true(exception);
+
+ do_test_finished();
+}
+
+function tablesCallbackWithoutSub(tables)
+{
+ var parts = tables.split("\n");
+ parts.sort();
+
+ // there's a leading \n here because splitting left an empty string
+ // after the trailing newline, which will sort first
+ do_check_eq(parts.join("\n"),
+ "\ntest-block-simple;a:1\ntest-malware-simple;a:1\ntest-phish-simple;a:2\ntest-unwanted-simple;a:1");
+
+ checkNoHost();
+}
+
+
+function expireSubSuccess(result) {
+ dbservice.getTables(tablesCallbackWithoutSub);
+}
+
+function tablesCallbackWithSub(tables)
+{
+ var parts = tables.split("\n");
+ parts.sort();
+
+ // there's a leading \n here because splitting left an empty string
+ // after the trailing newline, which will sort first
+ do_check_eq(parts.join("\n"),
+ "\ntest-block-simple;a:1\ntest-malware-simple;a:1\ntest-phish-simple;a:2:s:3\ntest-unwanted-simple;a:1");
+
+ // verify that expiring a sub chunk removes its name from the list
+ var data =
+ "n:1000\n" +
+ "i:test-phish-simple\n" +
+ "sd:3\n";
+
+ doSimpleUpdate(data, expireSubSuccess, testFailure);
+}
+
+function checkChunksWithSub()
+{
+ dbservice.getTables(tablesCallbackWithSub);
+}
+
+function checkDone() {
+ if (--numExpecting == 0)
+ checkChunksWithSub();
+}
+
+function phishExists(result) {
+ dumpn("phishExists: " + result);
+ try {
+ do_check_true(result.indexOf("test-phish-simple") != -1);
+ } finally {
+ checkDone();
+ }
+}
+
+function phishDoesntExist(result) {
+ dumpn("phishDoesntExist: " + result);
+ try {
+ do_check_true(result.indexOf("test-phish-simple") == -1);
+ } finally {
+ checkDone();
+ }
+}
+
+function malwareExists(result) {
+ dumpn("malwareExists: " + result);
+
+ try {
+ do_check_true(result.indexOf("test-malware-simple") != -1);
+ } finally {
+ checkDone();
+ }
+}
+
+function unwantedExists(result) {
+ dumpn("unwantedExists: " + result);
+
+ try {
+ do_check_true(result.indexOf("test-unwanted-simple") != -1);
+ } finally {
+ checkDone();
+ }
+}
+
+function blockedExists(result) {
+ dumpn("blockedExists: " + result);
+
+ try {
+ do_check_true(result.indexOf("test-block-simple") != -1);
+ } finally {
+ checkDone();
+ }
+}
+
+function checkState()
+{
+ numExpecting = 0;
+
+
+ for (var key in phishExpected) {
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + key, null, null), {});
+ dbservice.lookup(principal, allTables, phishExists, true);
+ numExpecting++;
+ }
+
+ for (var key in phishUnexpected) {
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + key, null, null), {});
+ dbservice.lookup(principal, allTables, phishDoesntExist, true);
+ numExpecting++;
+ }
+
+ for (var key in malwareExpected) {
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + key, null, null), {});
+ dbservice.lookup(principal, allTables, malwareExists, true);
+ numExpecting++;
+ }
+
+ for (var key in unwantedExpected) {
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + key, null, null), {});
+ dbservice.lookup(principal, allTables, unwantedExists, true);
+ numExpecting++;
+ }
+
+ for (var key in blockedExpected) {
+ var principal = secMan.createCodebasePrincipal(iosvc.newURI("http://" + key, null, null), {});
+ dbservice.lookup(principal, allTables, blockedExists, true);
+ numExpecting++;
+ }
+}
+
+function testSubSuccess(result)
+{
+ do_check_eq(result, "1000");
+ checkState();
+}
+
+function do_subs() {
+ var data =
+ "n:1000\n" +
+ "i:test-phish-simple\n" +
+ "s:3:32:" + chunk3Sub.length + "\n" +
+ chunk3Sub + "\n" +
+ "ad:1\n" +
+ "ad:4-6\n";
+
+ doSimpleUpdate(data, testSubSuccess, testFailure);
+}
+
+function testAddSuccess(arg) {
+ do_check_eq(arg, "1000");
+
+ do_subs();
+}
+
+function do_adds() {
+ // This test relies on the fact that only -regexp tables are ungzipped,
+ // and only -hash tables are assumed to be pre-md5'd. So we use
+ // a 'simple' table type to get simple hostname-per-line semantics.
+
+ var data =
+ "n:1000\n" +
+ "i:test-phish-simple\n" +
+ "a:1:32:" + chunk1.length + "\n" +
+ chunk1 + "\n" +
+ "a:2:32:" + chunk2.length + "\n" +
+ chunk2 + "\n" +
+ "a:4:32:" + chunk4.length + "\n" +
+ chunk4 + "\n" +
+ "a:5:32:" + chunk5.length + "\n" +
+ chunk5 + "\n" +
+ "a:6:32:" + chunk6.length + "\n" +
+ chunk6 + "\n" +
+ "i:test-malware-simple\n" +
+ "a:1:32:" + chunk2.length + "\n" +
+ chunk2 + "\n" +
+ "i:test-unwanted-simple\n" +
+ "a:1:32:" + chunk3.length + "\n" +
+ chunk3 + "\n" +
+ "i:test-block-simple\n" +
+ "a:1:32:" + chunk7.length + "\n" +
+ chunk7 + "\n";
+
+ doSimpleUpdate(data, testAddSuccess, testFailure);
+}
+
+function run_test() {
+ do_adds();
+ do_test_pending();
+}
diff --git a/toolkit/components/url-classifier/tests/unit/test_digest256.js b/toolkit/components/url-classifier/tests/unit/test_digest256.js
new file mode 100644
index 0000000000..6ae6529159
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_digest256.js
@@ -0,0 +1,147 @@
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+// Global test server for serving safebrowsing updates.
+var gHttpServ = null;
+// Global nsIUrlClassifierDBService
+var gDbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+// Security manager for creating nsIPrincipals from URIs
+var gSecMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+
+// A map of tables to arrays of update redirect urls.
+var gTables = {};
+
+// Construct an update from a file.
+function readFileToString(aFilename) {
+ let f = do_get_file(aFilename);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(f, -1, 0, 0);
+ let buf = NetUtil.readInputStreamToString(stream, stream.available());
+ return buf;
+}
+
+// Registers a table for which to serve update chunks. Returns a promise that
+// resolves when that chunk has been downloaded.
+function registerTableUpdate(aTable, aFilename) {
+ let deferred = Promise.defer();
+ // If we haven't been given an update for this table yet, add it to the map
+ if (!(aTable in gTables)) {
+ gTables[aTable] = [];
+ }
+
+ // The number of chunks associated with this table.
+ let numChunks = gTables[aTable].length + 1;
+ let redirectPath = "/" + aTable + "-" + numChunks;
+ let redirectUrl = "localhost:4444" + redirectPath;
+
+ // Store redirect url for that table so we can return it later when we
+ // process an update request.
+ gTables[aTable].push(redirectUrl);
+
+ gHttpServ.registerPathHandler(redirectPath, function(request, response) {
+ do_print("Mock safebrowsing server handling request for " + redirectPath);
+ let contents = readFileToString(aFilename);
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(contents, contents.length);
+ deferred.resolve(contents);
+ });
+ return deferred.promise;
+}
+
+// Construct a response with redirect urls.
+function processUpdateRequest() {
+ let response = "n:1000\n";
+ for (let table in gTables) {
+ response += "i:" + table + "\n";
+ for (let i = 0; i < gTables[table].length; ++i) {
+ response += "u:" + gTables[table][i] + "\n";
+ }
+ }
+ do_print("Returning update response: " + response);
+ return response;
+}
+
+// Set up our test server to handle update requests.
+function run_test() {
+ gHttpServ = new HttpServer();
+ gHttpServ.registerDirectory("/", do_get_cwd());
+
+ gHttpServ.registerPathHandler("/downloads", function(request, response) {
+ let buf = NetUtil.readInputStreamToString(request.bodyInputStream,
+ request.bodyInputStream.available());
+ let blob = processUpdateRequest();
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(blob, blob.length);
+ });
+
+ gHttpServ.start(4444);
+ run_next_test();
+}
+
+function createURI(s) {
+ let service = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+ return service.newURI(s, null, null);
+}
+
+// Just throw if we ever get an update or download error.
+function handleError(aEvent) {
+ do_throw("We didn't download or update correctly: " + aEvent);
+}
+
+add_test(function test_update() {
+ let streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"]
+ .getService(Ci.nsIUrlClassifierStreamUpdater);
+
+ // Load up some update chunks for the safebrowsing server to serve.
+ registerTableUpdate("goog-downloadwhite-digest256", "data/digest1.chunk");
+ registerTableUpdate("goog-downloadwhite-digest256", "data/digest2.chunk");
+
+ // Download some updates, and don't continue until the downloads are done.
+ function updateSuccess(aEvent) {
+ // Timeout of n:1000 is constructed in processUpdateRequest above and
+ // passed back in the callback in nsIUrlClassifierStreamUpdater on success.
+ do_check_eq("1000", aEvent);
+ do_print("All data processed");
+ run_next_test();
+ }
+ streamUpdater.downloadUpdates(
+ "goog-downloadwhite-digest256",
+ "goog-downloadwhite-digest256;\n",
+ true,
+ "http://localhost:4444/downloads",
+ updateSuccess, handleError, handleError);
+});
+
+add_test(function test_url_not_whitelisted() {
+ let uri = createURI("http://example.com");
+ let principal = gSecMan.createCodebasePrincipal(uri, {});
+ gDbService.lookup(principal, "goog-downloadwhite-digest256",
+ function handleEvent(aEvent) {
+ // This URI is not on any lists.
+ do_check_eq("", aEvent);
+ run_next_test();
+ });
+});
+
+add_test(function test_url_whitelisted() {
+ // Hash of "whitelisted.com/" (canonicalized URL) is:
+ // 93CA5F48E15E9861CD37C2D95DB43D23CC6E6DE5C3F8FA6E8BE66F97CC518907
+ let uri = createURI("http://whitelisted.com");
+ let principal = gSecMan.createCodebasePrincipal(uri, {});
+ gDbService.lookup(principal, "goog-downloadwhite-digest256",
+ function handleEvent(aEvent) {
+ do_check_eq("goog-downloadwhite-digest256", aEvent);
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/url-classifier/tests/unit/test_hashcompleter.js b/toolkit/components/url-classifier/tests/unit/test_hashcompleter.js
new file mode 100644
index 0000000000..40fafd9230
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_hashcompleter.js
@@ -0,0 +1,403 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test ensures that the nsIUrlClassifierHashCompleter works as expected
+// and simulates an HTTP server to provide completions.
+//
+// In order to test completions, each group of completions sent as one request
+// to the HTTP server is called a completion set. There is currently not
+// support for multiple requests being sent to the server at once, in this test.
+// This tests makes a request for each element of |completionSets|, waits for
+// a response and then moves to the next element.
+// Each element of |completionSets| is an array of completions, and each
+// completion is an object with the properties:
+// hash: complete hash for the completion. Automatically right-padded
+// to be COMPLETE_LENGTH.
+// expectCompletion: boolean indicating whether the server should respond
+// with a full hash.
+// forceServerError: boolean indicating whether the server should respond
+// with a 503.
+// table: name of the table that the hash corresponds to. Only needs to be set
+// if a completion is expected.
+// chunkId: positive integer corresponding to the chunk that the hash belongs
+// to. Only needs to be set if a completion is expected.
+// multipleCompletions: boolean indicating whether the server should respond
+// with more than one full hash. If this is set to true
+// then |expectCompletion| must also be set to true and
+// |hash| must have the same prefix as all |completions|.
+// completions: an array of completions (objects with a hash, table and
+// chunkId property as described above). This property is only
+// used when |multipleCompletions| is set to true.
+
+// Basic prefixes with 2/3 completions.
+var basicCompletionSet = [
+ {
+ hash: "abcdefgh",
+ expectCompletion: true,
+ table: "test",
+ chunkId: 1234,
+ },
+ {
+ hash: "1234",
+ expectCompletion: false,
+ },
+ {
+ hash: "\u0000\u0000\u000012312",
+ expectCompletion: true,
+ table: "test",
+ chunkId: 1234,
+ }
+];
+
+// 3 prefixes with 0 completions to test HashCompleter handling a 204 status.
+var falseCompletionSet = [
+ {
+ hash: "1234",
+ expectCompletion: false,
+ },
+ {
+ hash: "",
+ expectCompletion: false,
+ },
+ {
+ hash: "abc",
+ expectCompletion: false,
+ }
+];
+
+// The current implementation (as of Mar 2011) sometimes sends duplicate
+// entries to HashCompleter and even expects responses for duplicated entries.
+var dupedCompletionSet = [
+ {
+ hash: "1234",
+ expectCompletion: true,
+ table: "test",
+ chunkId: 1,
+ },
+ {
+ hash: "5678",
+ expectCompletion: false,
+ table: "test2",
+ chunkId: 2,
+ },
+ {
+ hash: "1234",
+ expectCompletion: true,
+ table: "test",
+ chunkId: 1,
+ },
+ {
+ hash: "5678",
+ expectCompletion: false,
+ table: "test2",
+ chunkId: 2
+ }
+];
+
+// It is possible for a hash completion request to return with multiple
+// completions, the HashCompleter should return all of these.
+var multipleResponsesCompletionSet = [
+ {
+ hash: "1234",
+ expectCompletion: true,
+ multipleCompletions: true,
+ completions: [
+ {
+ hash: "123456",
+ table: "test1",
+ chunkId: 3,
+ },
+ {
+ hash: "123478",
+ table: "test2",
+ chunkId: 4,
+ }
+ ],
+ }
+];
+
+function buildCompletionRequest(aCompletionSet) {
+ let prefixes = [];
+ let prefixSet = new Set();
+ aCompletionSet.forEach(s => {
+ let prefix = s.hash.substring(0, 4);
+ if (prefixSet.has(prefix)) {
+ return;
+ }
+ prefixSet.add(prefix);
+ prefixes.push(prefix);
+ });
+ return 4 + ":" + (4 * prefixes.length) + "\n" + prefixes.join("");
+}
+
+function parseCompletionRequest(aRequest) {
+ // Format: [partial_length]:[num_of_prefix * partial_length]\n[prefixes_data]
+
+ let tokens = /(\d):(\d+)/.exec(aRequest);
+ if (tokens.length < 3) {
+ dump("Request format error.");
+ return null;
+ }
+
+ let partialLength = parseInt(tokens[1]);
+ let payloadLength = parseInt(tokens[2]);
+
+ let payloadStart = tokens[1].length + // partial length
+ 1 + // ':'
+ tokens[2].length + // payload length
+ 1; // '\n'
+
+ let prefixSet = [];
+ for (let i = payloadStart; i < aRequest.length; i += partialLength) {
+ let prefix = aRequest.substr(i, partialLength);
+ if (prefix.length !== partialLength) {
+ dump("Header info not correct: " + aRequest.substr(0, payloadStart));
+ return null;
+ }
+ prefixSet.push(prefix);
+ }
+ prefixSet.sort();
+
+ return prefixSet;
+}
+
+// Compare the requests in string format.
+function compareCompletionRequest(aRequest1, aRequest2) {
+ let prefixSet1 = parseCompletionRequest(aRequest1);
+ let prefixSet2 = parseCompletionRequest(aRequest2);
+
+ return equal(JSON.stringify(prefixSet1), JSON.stringify(prefixSet2));
+}
+
+// The fifth completion set is added at runtime by getRandomCompletionSet.
+// Each completion in the set only has one response and its purpose is to
+// provide an easy way to test the HashCompleter handling an arbitrarily large
+// completion set (determined by SIZE_OF_RANDOM_SET).
+const SIZE_OF_RANDOM_SET = 16;
+function getRandomCompletionSet(forceServerError) {
+ let completionSet = [];
+ let hashPrefixes = [];
+
+ let seed = Math.floor(Math.random() * Math.pow(2, 32));
+ dump("Using seed of " + seed + " for random completion set.\n");
+ let rand = new LFSRgenerator(seed);
+
+ for (let i = 0; i < SIZE_OF_RANDOM_SET; i++) {
+ let completion = { expectCompletion: false, forceServerError: false, _finished: false };
+
+ // Generate a random 256 bit hash. First we get a random number and then
+ // convert it to a string.
+ let hash;
+ let prefix;
+ do {
+ hash = "";
+ let length = 1 + rand.nextNum(5);
+ for (let i = 0; i < length; i++)
+ hash += String.fromCharCode(rand.nextNum(8));
+ prefix = hash.substring(0,4);
+ } while (hashPrefixes.indexOf(prefix) != -1);
+
+ hashPrefixes.push(prefix);
+ completion.hash = hash;
+
+ if (!forceServerError) {
+ completion.expectCompletion = rand.nextNum(1) == 1;
+ } else {
+ completion.forceServerError = true;
+ }
+ if (completion.expectCompletion) {
+ // Generate a random alpha-numeric string of length at most 6 for the
+ // table name.
+ completion.table = (rand.nextNum(31)).toString(36);
+
+ completion.chunkId = rand.nextNum(16);
+ }
+ completionSet.push(completion);
+ }
+
+ return completionSet;
+}
+
+var completionSets = [basicCompletionSet, falseCompletionSet,
+ dupedCompletionSet, multipleResponsesCompletionSet];
+var currentCompletionSet = -1;
+var finishedCompletions = 0;
+
+const SERVER_PORT = 8080;
+const SERVER_PATH = "/hash-completer";
+var server;
+
+// Completion hashes are automatically right-padded with null chars to have a
+// length of COMPLETE_LENGTH.
+// Taken from nsUrlClassifierDBService.h
+const COMPLETE_LENGTH = 32;
+
+var completer = Cc["@mozilla.org/url-classifier/hashcompleter;1"].
+ getService(Ci.nsIUrlClassifierHashCompleter);
+
+var gethashUrl;
+
+// Expected highest completion set for which the server sends a response.
+var expectedMaxServerCompletionSet = 0;
+var maxServerCompletionSet = 0;
+
+function run_test() {
+ // Generate a random completion set that return successful responses.
+ completionSets.push(getRandomCompletionSet(false));
+ // We backoff after receiving an error, so requests shouldn't reach the
+ // server after that.
+ expectedMaxServerCompletionSet = completionSets.length;
+ // Generate some completion sets that return 503s.
+ for (let j = 0; j < 10; ++j) {
+ completionSets.push(getRandomCompletionSet(true));
+ }
+
+ // Fix up the completions before running the test.
+ for (let completionSet of completionSets) {
+ for (let completion of completionSet) {
+ // Pad the right of each |hash| so that the length is COMPLETE_LENGTH.
+ if (completion.multipleCompletions) {
+ for (let responseCompletion of completion.completions) {
+ let numChars = COMPLETE_LENGTH - responseCompletion.hash.length;
+ responseCompletion.hash += (new Array(numChars + 1)).join("\u0000");
+ }
+ }
+ else {
+ let numChars = COMPLETE_LENGTH - completion.hash.length;
+ completion.hash += (new Array(numChars + 1)).join("\u0000");
+ }
+ }
+ }
+ do_test_pending();
+
+ server = new HttpServer();
+ server.registerPathHandler(SERVER_PATH, hashCompleterServer);
+
+ server.start(-1);
+ const SERVER_PORT = server.identity.primaryPort;
+
+ gethashUrl = "http://localhost:" + SERVER_PORT + SERVER_PATH;
+
+ runNextCompletion();
+}
+
+function runNextCompletion() {
+ // The server relies on currentCompletionSet to send the correct response, so
+ // don't increment it until we start the new set of callbacks.
+ currentCompletionSet++;
+ if (currentCompletionSet >= completionSets.length) {
+ finish();
+ return;
+ }
+
+ dump("Now on completion set index " + currentCompletionSet + ", length " +
+ completionSets[currentCompletionSet].length + "\n");
+ // Number of finished completions for this set.
+ finishedCompletions = 0;
+ for (let completion of completionSets[currentCompletionSet]) {
+ completer.complete(completion.hash.substring(0,4), gethashUrl,
+ (new callback(completion)));
+ }
+}
+
+function hashCompleterServer(aRequest, aResponse) {
+ let stream = aRequest.bodyInputStream;
+ let wrapperStream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ wrapperStream.setInputStream(stream);
+
+ let len = stream.available();
+ let data = wrapperStream.readBytes(len);
+
+ // Check if we got the expected completion request.
+ let expectedRequest = buildCompletionRequest(completionSets[currentCompletionSet]);
+ compareCompletionRequest(data, expectedRequest);
+
+ // To avoid a response with duplicate hash completions, we keep track of all
+ // completed hash prefixes so far.
+ let completedHashes = [];
+ let responseText = "";
+
+ function responseForCompletion(x) {
+ return x.table + ":" + x.chunkId + ":" + x.hash.length + "\n" + x.hash;
+ }
+ // As per the spec, a server should response with a 204 if there are no
+ // full-length hashes that match the prefixes.
+ let httpStatus = 204;
+ for (let completion of completionSets[currentCompletionSet]) {
+ if (completion.expectCompletion &&
+ (completedHashes.indexOf(completion.hash) == -1)) {
+ completedHashes.push(completion.hash);
+
+ if (completion.multipleCompletions)
+ responseText += completion.completions.map(responseForCompletion).join("");
+ else
+ responseText += responseForCompletion(completion);
+ }
+ if (completion.forceServerError) {
+ httpStatus = 503;
+ }
+ }
+
+ dump("Server sending response for " + currentCompletionSet + "\n");
+ maxServerCompletionSet = currentCompletionSet;
+ if (responseText && httpStatus != 503) {
+ aResponse.write(responseText);
+ } else {
+ aResponse.setStatusLine(null, httpStatus, null);
+ }
+}
+
+
+function callback(completion) {
+ this._completion = completion;
+}
+
+callback.prototype = {
+ completion: function completion(hash, table, chunkId, trusted) {
+ do_check_true(this._completion.expectCompletion);
+ if (this._completion.multipleCompletions) {
+ for (let completion of this._completion.completions) {
+ if (completion.hash == hash) {
+ do_check_eq(JSON.stringify(hash), JSON.stringify(completion.hash));
+ do_check_eq(table, completion.table);
+ do_check_eq(chunkId, completion.chunkId);
+
+ completion._completed = true;
+
+ if (this._completion.completions.every(x => x._completed))
+ this._completed = true;
+
+ break;
+ }
+ }
+ }
+ else {
+ // Hashes are not actually strings and can contain arbitrary data.
+ do_check_eq(JSON.stringify(hash), JSON.stringify(this._completion.hash));
+ do_check_eq(table, this._completion.table);
+ do_check_eq(chunkId, this._completion.chunkId);
+
+ this._completed = true;
+ }
+ },
+
+ completionFinished: function completionFinished(status) {
+ finishedCompletions++;
+ do_check_eq(!!this._completion.expectCompletion, !!this._completed);
+ this._completion._finished = true;
+
+ // currentCompletionSet can mutate before all of the callbacks are complete.
+ if (currentCompletionSet < completionSets.length &&
+ finishedCompletions == completionSets[currentCompletionSet].length) {
+ runNextCompletion();
+ }
+ },
+};
+
+function finish() {
+ do_check_eq(expectedMaxServerCompletionSet, maxServerCompletionSet);
+ server.stop(function() {
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/url-classifier/tests/unit/test_listmanager.js b/toolkit/components/url-classifier/tests/unit/test_listmanager.js
new file mode 100644
index 0000000000..ba11d930ee
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_listmanager.js
@@ -0,0 +1,376 @@
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+// These tables share the same updateURL.
+const TEST_TABLE_DATA_LIST = [
+ // 0:
+ {
+ tableName: "test-listmanager0-digest256",
+ providerName: "google",
+ updateUrl: "http://localhost:4444/safebrowsing/update",
+ gethashUrl: "http://localhost:4444/safebrowsing/gethash0",
+ },
+
+ // 1:
+ {
+ tableName: "test-listmanager1-digest256",
+ providerName: "google",
+ updateUrl: "http://localhost:4444/safebrowsing/update",
+ gethashUrl: "http://localhost:4444/safebrowsing/gethash1",
+ },
+
+ // 2.
+ {
+ tableName: "test-listmanager2-digest256",
+ providerName: "google",
+ updateUrl: "http://localhost:4444/safebrowsing/update",
+ gethashUrl: "http://localhost:4444/safebrowsing/gethash2",
+ }
+];
+
+// These tables have a different update URL (for v4).
+const TEST_TABLE_DATA_V4 = {
+ tableName: "test-phish-proto",
+ providerName: "google4",
+ updateUrl: "http://localhost:5555/safebrowsing/update?",
+ gethashUrl: "http://localhost:5555/safebrowsing/gethash-v4",
+};
+const TEST_TABLE_DATA_V4_DISABLED = {
+ tableName: "test-unwanted-proto",
+ providerName: "google4",
+ updateUrl: "http://localhost:5555/safebrowsing/update?",
+ gethashUrl: "http://localhost:5555/safebrowsing/gethash-v4",
+};
+
+const PREF_NEXTUPDATETIME = "browser.safebrowsing.provider.google.nextupdatetime";
+const PREF_NEXTUPDATETIME_V4 = "browser.safebrowsing.provider.google4.nextupdatetime";
+
+let gListManager = Cc["@mozilla.org/url-classifier/listmanager;1"]
+ .getService(Ci.nsIUrlListManager);
+
+let gUrlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+
+// Global test server for serving safebrowsing updates.
+let gHttpServ = null;
+let gUpdateResponse = "";
+let gExpectedUpdateRequest = "";
+let gExpectedQueryV4 = "";
+
+// Handles request for TEST_TABLE_DATA_V4.
+let gHttpServV4 = null;
+
+// These two variables are used to synchronize the last two racing updates
+// (in terms of "update URL") in test_update_all_tables().
+let gUpdatedCntForTableData = 0; // For TEST_TABLE_DATA_LIST.
+let gIsV4Updated = false; // For TEST_TABLE_DATA_V4.
+
+const NEW_CLIENT_STATE = 'sta\0te';
+const CHECKSUM = '\x30\x67\xc7\x2c\x5e\x50\x1c\x31\xe3\xfe\xca\x73\xf0\x47\xdc\x34\x1a\x95\x63\x99\xec\x70\x5e\x0a\xee\x9e\xfb\x17\xa1\x55\x35\x78';
+
+prefBranch.setBoolPref("browser.safebrowsing.debug", true);
+
+// The "\xFF\xFF" is to generate a base64 string with "/".
+prefBranch.setCharPref("browser.safebrowsing.id", "Firefox\xFF\xFF");
+
+// Register tables.
+TEST_TABLE_DATA_LIST.forEach(function(t) {
+ gListManager.registerTable(t.tableName,
+ t.providerName,
+ t.updateUrl,
+ t.gethashUrl);
+});
+
+gListManager.registerTable(TEST_TABLE_DATA_V4.tableName,
+ TEST_TABLE_DATA_V4.providerName,
+ TEST_TABLE_DATA_V4.updateUrl,
+ TEST_TABLE_DATA_V4.gethashUrl);
+
+// To test Bug 1302044.
+gListManager.registerTable(TEST_TABLE_DATA_V4_DISABLED.tableName,
+ TEST_TABLE_DATA_V4_DISABLED.providerName,
+ TEST_TABLE_DATA_V4_DISABLED.updateUrl,
+ TEST_TABLE_DATA_V4_DISABLED.gethashUrl);
+
+const SERVER_INVOLVED_TEST_CASE_LIST = [
+ // - Do table0 update.
+ // - Server would respond "a:5:32:32\n[DATA]".
+ function test_update_table0() {
+ disableAllUpdates();
+
+ gListManager.enableUpdate(TEST_TABLE_DATA_LIST[0].tableName);
+ gExpectedUpdateRequest = TEST_TABLE_DATA_LIST[0].tableName + ";\n";
+
+ gUpdateResponse = "n:1000\ni:" + TEST_TABLE_DATA_LIST[0].tableName + "\n";
+ gUpdateResponse += readFileToString("data/digest2.chunk");
+
+ forceTableUpdate();
+ },
+
+ // - Do table0 update again. Since chunk 5 was added to table0 in the last
+ // update, the expected request contains "a:5".
+ // - Server would respond "s;2-12\n[DATA]".
+ function test_update_table0_with_existing_chunks() {
+ disableAllUpdates();
+
+ gListManager.enableUpdate(TEST_TABLE_DATA_LIST[0].tableName);
+ gExpectedUpdateRequest = TEST_TABLE_DATA_LIST[0].tableName + ";a:5\n";
+
+ gUpdateResponse = "n:1000\ni:" + TEST_TABLE_DATA_LIST[0].tableName + "\n";
+ gUpdateResponse += readFileToString("data/digest1.chunk");
+
+ forceTableUpdate();
+ },
+
+ // - Do all-table update.
+ // - Server would respond no chunk control.
+ //
+ // Note that this test MUST be the last one in the array since we rely on
+ // the number of sever-involved test case to synchronize the racing last
+ // two udpates for different URL.
+ function test_update_all_tables() {
+ disableAllUpdates();
+
+ // Enable all tables including TEST_TABLE_DATA_V4!
+ TEST_TABLE_DATA_LIST.forEach(function(t) {
+ gListManager.enableUpdate(t.tableName);
+ });
+
+ // We register two v4 tables but only enable one of them
+ // to verify that the disabled tables are not updated.
+ // See Bug 1302044.
+ gListManager.enableUpdate(TEST_TABLE_DATA_V4.tableName);
+ gListManager.disableUpdate(TEST_TABLE_DATA_V4_DISABLED.tableName);
+
+ // Expected results for v2.
+ gExpectedUpdateRequest = TEST_TABLE_DATA_LIST[0].tableName + ";a:5:s:2-12\n" +
+ TEST_TABLE_DATA_LIST[1].tableName + ";\n" +
+ TEST_TABLE_DATA_LIST[2].tableName + ";\n";
+ gUpdateResponse = "n:1000\n";
+
+ // We test the request against the query string since v4 request
+ // would be appened to the query string. The request is generated
+ // by protobuf API (binary) then encoded to base64 format.
+ let requestV4 = gUrlUtils.makeUpdateRequestV4([TEST_TABLE_DATA_V4.tableName],
+ [""],
+ 1);
+ gExpectedQueryV4 = "&$req=" + requestV4;
+
+ forceTableUpdate();
+ },
+
+];
+
+SERVER_INVOLVED_TEST_CASE_LIST.forEach(t => add_test(t));
+
+add_test(function test_partialUpdateV4() {
+ disableAllUpdates();
+
+ gListManager.enableUpdate(TEST_TABLE_DATA_V4.tableName);
+
+ // Since the new client state has been responded and saved in
+ // test_update_all_tables, this update request should send
+ // a partial update to the server.
+ let requestV4 = gUrlUtils.makeUpdateRequestV4([TEST_TABLE_DATA_V4.tableName],
+ [btoa(NEW_CLIENT_STATE)],
+ 1);
+ gExpectedQueryV4 = "&$req=" + requestV4;
+
+ forceTableUpdate();
+});
+
+// Tests nsIUrlListManager.getGethashUrl.
+add_test(function test_getGethashUrl() {
+ TEST_TABLE_DATA_LIST.forEach(function (t) {
+ equal(gListManager.getGethashUrl(t.tableName), t.gethashUrl);
+ });
+ equal(gListManager.getGethashUrl(TEST_TABLE_DATA_V4.tableName),
+ TEST_TABLE_DATA_V4.gethashUrl);
+ run_next_test();
+});
+
+function run_test() {
+ // Setup primary testing server.
+ gHttpServ = new HttpServer();
+ gHttpServ.registerDirectory("/", do_get_cwd());
+
+ gHttpServ.registerPathHandler("/safebrowsing/update", function(request, response) {
+ let body = NetUtil.readInputStreamToString(request.bodyInputStream,
+ request.bodyInputStream.available());
+
+ // Verify if the request is as expected.
+ equal(body, gExpectedUpdateRequest);
+
+ // Respond the update which is controlled by the test case.
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(gUpdateResponse, gUpdateResponse.length);
+
+ gUpdatedCntForTableData++;
+
+ if (gUpdatedCntForTableData !== SERVER_INVOLVED_TEST_CASE_LIST.length) {
+ // This is not the last test case so run the next once upon the
+ // the update success.
+ waitForUpdateSuccess(run_next_test);
+ return;
+ }
+
+ if (gIsV4Updated) {
+ run_next_test(); // All tests are done. Just finish.
+ return;
+ }
+
+ do_print("Waiting for TEST_TABLE_DATA_V4 to be tested ...");
+ });
+
+ gHttpServ.start(4444);
+
+ // Setup v4 testing server for the different update URL.
+ gHttpServV4 = new HttpServer();
+ gHttpServV4.registerDirectory("/", do_get_cwd());
+
+ gHttpServV4.registerPathHandler("/safebrowsing/update", function(request, response) {
+ // V4 update request body should be empty.
+ equal(request.bodyInputStream.available(), 0);
+
+ // Not on the spec. Found in Chromium source code...
+ equal(request.getHeader("X-HTTP-Method-Override"), "POST");
+
+ // V4 update request uses GET.
+ equal(request.method, "GET");
+
+ // V4 append the base64 encoded request to the query string.
+ equal(request.queryString, gExpectedQueryV4);
+ equal(request.queryString.indexOf('+'), -1);
+ equal(request.queryString.indexOf('/'), -1);
+
+ // Respond a V2 compatible content for now. In the future we can
+ // send a meaningful response to test Bug 1284178 to see if the
+ // update is successfully stored to database.
+ response.setHeader("Content-Type",
+ "application/vnd.google.safebrowsing-update", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ // The protobuf binary represention of response:
+ //
+ // [
+ // {
+ // 'threat_type': 2, // SOCIAL_ENGINEERING_PUBLIC
+ // 'response_type': 2, // FULL_UPDATE
+ // 'new_client_state': 'sta\x00te', // NEW_CLIENT_STATE
+ // 'checksum': { "sha256": CHECKSUM }, // CHECKSUM
+ // 'additions': { 'compression_type': RAW,
+ // 'prefix_size': 4,
+ // 'raw_hashes': "00000001000000020000000300000004"}
+ // }
+ // ]
+ //
+ let content = "\x0A\x4A\x08\x02\x20\x02\x2A\x18\x08\x01\x12\x14\x08\x04\x12\x10\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x3A\x06\x73\x74\x61\x00\x74\x65\x42\x22\x0A\x20\x30\x67\xC7\x2C\x5E\x50\x1C\x31\xE3\xFE\xCA\x73\xF0\x47\xDC\x34\x1A\x95\x63\x99\xEC\x70\x5E\x0A\xEE\x9E\xFB\x17\xA1\x55\x35\x78\x12\x08\x08\x08\x10\x80\x94\xEB\xDC\x03";
+
+ response.bodyOutputStream.write(content, content.length);
+
+ if (gIsV4Updated) {
+ // This falls to the case where test_partialUpdateV4 is running.
+ // We are supposed to have verified the update request contains
+ // the state we set in the previous request.
+ run_next_test();
+ return;
+ }
+
+ waitUntilMetaDataSaved(NEW_CLIENT_STATE, CHECKSUM, () => {
+ gIsV4Updated = true;
+
+ if (gUpdatedCntForTableData === SERVER_INVOLVED_TEST_CASE_LIST.length) {
+ // All tests are done!
+ run_next_test();
+ return;
+ }
+
+ do_print("Wait for all sever-involved tests to be done ...");
+ });
+
+ });
+
+ gHttpServV4.start(5555);
+
+ run_next_test();
+}
+
+// A trick to force updating tables. However, before calling this, we have to
+// call disableAllUpdates() first to clean up the updateCheckers in listmanager.
+function forceTableUpdate() {
+ prefBranch.setCharPref(PREF_NEXTUPDATETIME, "1");
+ prefBranch.setCharPref(PREF_NEXTUPDATETIME_V4, "1");
+ gListManager.maybeToggleUpdateChecking();
+}
+
+function disableAllUpdates() {
+ TEST_TABLE_DATA_LIST.forEach(t => gListManager.disableUpdate(t.tableName));
+ gListManager.disableUpdate(TEST_TABLE_DATA_V4.tableName);
+}
+
+// Since there's no public interface on listmanager to know the update success,
+// we could only rely on the refresh of "nextupdatetime".
+function waitForUpdateSuccess(callback) {
+ let nextupdatetime = parseInt(prefBranch.getCharPref(PREF_NEXTUPDATETIME));
+ do_print("nextupdatetime: " + nextupdatetime);
+ if (nextupdatetime !== 1) {
+ callback();
+ return;
+ }
+ do_timeout(1000, waitForUpdateSuccess.bind(null, callback));
+}
+
+// Construct an update from a file.
+function readFileToString(aFilename) {
+ let f = do_get_file(aFilename);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(f, -1, 0, 0);
+ let buf = NetUtil.readInputStreamToString(stream, stream.available());
+ return buf;
+}
+
+function waitUntilMetaDataSaved(expectedState, expectedChecksum, callback) {
+ let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+
+ dbService.getTables(metaData => {
+ do_print("metadata: " + metaData);
+ let didCallback = false;
+ metaData.split("\n").some(line => {
+ // Parse [tableName];[stateBase64]
+ let p = line.indexOf(";");
+ if (-1 === p) {
+ return false; // continue.
+ }
+ let tableName = line.substring(0, p);
+ let metadata = line.substring(p + 1).split(":");
+ let stateBase64 = metadata[0];
+ let checksumBase64 = metadata[1];
+
+ if (tableName !== 'test-phish-proto') {
+ return false; // continue.
+ }
+
+ if (stateBase64 === btoa(expectedState) &&
+ checksumBase64 === btoa(expectedChecksum)) {
+ do_print('State has been saved to disk!');
+ callback();
+ didCallback = true;
+ }
+
+ return true; // break no matter whether the state is matching.
+ });
+
+ if (!didCallback) {
+ do_timeout(1000, waitUntilMetaDataSaved.bind(null, expectedState,
+ expectedChecksum,
+ callback));
+ }
+ });
+}
diff --git a/toolkit/components/url-classifier/tests/unit/test_partial.js b/toolkit/components/url-classifier/tests/unit/test_partial.js
new file mode 100644
index 0000000000..83243fb4e7
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_partial.js
@@ -0,0 +1,825 @@
+
+/**
+ * DummyCompleter() lets tests easily specify the results of a partial
+ * hash completion request.
+ */
+function DummyCompleter() {
+ this.fragments = {};
+ this.queries = [];
+ this.tableName = "test-phish-simple";
+}
+
+DummyCompleter.prototype =
+{
+QueryInterface: function(iid)
+{
+ if (!iid.equals(Ci.nsISupports) &&
+ !iid.equals(Ci.nsIUrlClassifierHashCompleter)) {
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+},
+
+complete: function(partialHash, gethashUrl, cb)
+{
+ this.queries.push(partialHash);
+ var fragments = this.fragments;
+ var self = this;
+ var doCallback = function() {
+ if (self.alwaysFail) {
+ cb.completionFinished(1);
+ return;
+ }
+ var results;
+ if (fragments[partialHash]) {
+ for (var i = 0; i < fragments[partialHash].length; i++) {
+ var chunkId = fragments[partialHash][i][0];
+ var hash = fragments[partialHash][i][1];
+ cb.completion(hash, self.tableName, chunkId);
+ }
+ }
+ cb.completionFinished(0);
+ }
+ var timer = new Timer(0, doCallback);
+},
+
+getHash: function(fragment)
+{
+ var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var data = converter.convertToByteArray(fragment);
+ var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ ch.init(ch.SHA256);
+ ch.update(data, data.length);
+ var hash = ch.finish(false);
+ return hash.slice(0, 32);
+},
+
+addFragment: function(chunkId, fragment)
+{
+ this.addHash(chunkId, this.getHash(fragment));
+},
+
+// This method allows the caller to generate complete hashes that match the
+// prefix of a real fragment, but have different complete hashes.
+addConflict: function(chunkId, fragment)
+{
+ var realHash = this.getHash(fragment);
+ var invalidHash = this.getHash("blah blah blah blah blah");
+ this.addHash(chunkId, realHash.slice(0, 4) + invalidHash.slice(4, 32));
+},
+
+addHash: function(chunkId, hash)
+{
+ var partial = hash.slice(0, 4);
+ if (this.fragments[partial]) {
+ this.fragments[partial].push([chunkId, hash]);
+ } else {
+ this.fragments[partial] = [[chunkId, hash]];
+ }
+},
+
+compareQueries: function(fragments)
+{
+ var expectedQueries = [];
+ for (var i = 0; i < fragments.length; i++) {
+ expectedQueries.push(this.getHash(fragments[i]).slice(0, 4));
+ }
+ do_check_eq(this.queries.length, expectedQueries.length);
+ expectedQueries.sort();
+ this.queries.sort();
+ for (var i = 0; i < this.queries.length; i++) {
+ do_check_eq(this.queries[i], expectedQueries[i]);
+ }
+}
+};
+
+function setupCompleter(table, hits, conflicts)
+{
+ var completer = new DummyCompleter();
+ completer.tableName = table;
+ for (var i = 0; i < hits.length; i++) {
+ var chunkId = hits[i][0];
+ var fragments = hits[i][1];
+ for (var j = 0; j < fragments.length; j++) {
+ completer.addFragment(chunkId, fragments[j]);
+ }
+ }
+ for (var i = 0; i < conflicts.length; i++) {
+ var chunkId = conflicts[i][0];
+ var fragments = conflicts[i][1];
+ for (var j = 0; j < fragments.length; j++) {
+ completer.addConflict(chunkId, fragments[j]);
+ }
+ }
+
+ dbservice.setHashCompleter(table, completer);
+
+ return completer;
+}
+
+function installCompleter(table, fragments, conflictFragments)
+{
+ return setupCompleter(table, fragments, conflictFragments);
+}
+
+function installFailingCompleter(table) {
+ var completer = setupCompleter(table, [], []);
+ completer.alwaysFail = true;
+ return completer;
+}
+
+// Helper assertion for checking dummy completer queries
+gAssertions.completerQueried = function(data, cb)
+{
+ var completer = data[0];
+ completer.compareQueries(data[1]);
+ cb();
+}
+
+function doTest(updates, assertions)
+{
+ doUpdateTest(updates, assertions, runNextTest, updateError);
+}
+
+// Test an add of two partial urls to a fresh database
+function testPartialAdds() {
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+
+ doTest([update], assertions);
+}
+
+function testPartialAddsWithConflicts() {
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+ // Each result will have both a real match and a conflict
+ var completer = installCompleter('test-phish-simple',
+ [[1, addUrls]],
+ [[1, addUrls]]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doTest([update], assertions);
+}
+
+// Test whether the fragmenting code does not cause duplicated completions
+function testFragments() {
+ var addUrls = [ "foo.com/a/b/c", "foo.net/", "foo.com/c/" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+
+ doTest([update], assertions);
+}
+
+// Test http://code.google.com/p/google-safe-browsing/wiki/Protocolv2Spec
+// section 6.2 example 1
+function testSpecFragments() {
+ var probeUrls = [ "a.b.c/1/2.html?param=1" ];
+
+ var addUrls = [ "a.b.c/1/2.html",
+ "a.b.c/",
+ "a.b.c/1/",
+ "b.c/1/2.html?param=1",
+ "b.c/1/2.html",
+ "b.c/",
+ "b.c/1/",
+ "a.b.c/1/2.html?param=1" ];
+
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : probeUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doTest([update], assertions);
+
+}
+
+// Test http://code.google.com/p/google-safe-browsing/wiki/Protocolv2Spec
+// section 6.2 example 2
+function testMoreSpecFragments() {
+ var probeUrls = [ "a.b.c.d.e.f.g/1.html" ];
+
+ var addUrls = [ "a.b.c.d.e.f.g/1.html",
+ "a.b.c.d.e.f.g/",
+ "c.d.e.f.g/1.html",
+ "c.d.e.f.g/",
+ "d.e.f.g/1.html",
+ "d.e.f.g/",
+ "e.f.g/1.html",
+ "e.f.g/",
+ "f.g/1.html",
+ "f.g/" ];
+
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : probeUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doTest([update], assertions);
+
+}
+
+function testFalsePositives() {
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+ // Each result will have no matching complete hashes and a non-matching
+ // conflict
+ var completer = installCompleter('test-phish-simple', [], [[1, addUrls]]);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsDontExist" : addUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doTest([update], assertions);
+}
+
+function testEmptyCompleter() {
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+ // Completer will never return full hashes
+ var completer = installCompleter('test-phish-simple', [], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsDontExist" : addUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doTest([update], assertions);
+}
+
+function testCompleterFailure() {
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+ // Completer will never return full hashes
+ var completer = installFailingCompleter('test-phish-simple');
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsDontExist" : addUrls,
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doTest([update], assertions);
+}
+
+function testMixedSizesSameDomain() {
+ var add1Urls = [ "foo.com/a" ];
+ var add2Urls = [ "foo.com/b" ];
+
+ var update1 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : add1Urls }],
+ 4);
+ var update2 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 2,
+ "urls" : add2Urls }],
+ 32);
+
+ // We should only need to complete the partial hashes
+ var completer = installCompleter('test-phish-simple', [[1, add1Urls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2",
+ // both urls should match...
+ "urlsExist" : add1Urls.concat(add2Urls),
+ // ... but the completer should only be queried for the partial entry
+ "completerQueried" : [completer, add1Urls]
+ };
+
+ doTest([update1, update2], assertions);
+}
+
+function testMixedSizesDifferentDomains() {
+ var add1Urls = [ "foo.com/a" ];
+ var add2Urls = [ "bar.com/b" ];
+
+ var update1 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : add1Urls }],
+ 4);
+ var update2 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 2,
+ "urls" : add2Urls }],
+ 32);
+
+ // We should only need to complete the partial hashes
+ var completer = installCompleter('test-phish-simple', [[1, add1Urls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-2",
+ // both urls should match...
+ "urlsExist" : add1Urls.concat(add2Urls),
+ // ... but the completer should only be queried for the partial entry
+ "completerQueried" : [completer, add1Urls]
+ };
+
+ doTest([update1, update2], assertions);
+}
+
+function testInvalidHashSize()
+{
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 12); // only 4 and 32 are legal hash sizes
+
+ var addUrls2 = [ "zaz.com/a", "xyz.com/b" ];
+ var update2 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 2,
+ "urls" : addUrls2
+ }],
+ 4);
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:2",
+ "urlsDontExist" : addUrls
+ };
+
+ // A successful update will trigger an error
+ doUpdateTest([update2, update], assertions, updateError, runNextTest);
+}
+
+function testWrongTable()
+{
+ var addUrls = [ "foo.com/a" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+ var completer = installCompleter('test-malware-simple', // wrong table
+ [[1, addUrls]], []);
+
+ // The above installCompleter installs the completer for test-malware-simple,
+ // we want it to be used for test-phish-simple too.
+ dbservice.setHashCompleter("test-phish-simple", completer);
+
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ // The urls were added as phishing urls, but the completer is claiming
+ // that they are malware urls, and we trust the completer in this case.
+ // The result will be discarded, so we can only check for non-existence.
+ "urlsDontExist" : addUrls,
+ // Make sure the completer was actually queried.
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doUpdateTest([update], assertions,
+ function() {
+ // Give the dbservice a chance to (not) cache the result.
+ var timer = new Timer(3000, function() {
+ // The miss earlier will have caused a miss to be cached.
+ // Resetting the completer does not count as an update,
+ // so we will not be probed again.
+ var newCompleter = installCompleter('test-malware-simple', [[1, addUrls]], []); dbservice.setHashCompleter("test-phish-simple",
+ newCompleter);
+
+ var assertions = {
+ "urlsDontExist" : addUrls
+ };
+ checkAssertions(assertions, runNextTest);
+ });
+ }, updateError);
+}
+
+function setupCachedResults(addUrls, part2)
+{
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ // Request the add url. This should cause the completion to be cached.
+ "urlsExist" : addUrls,
+ // Make sure the completer was actually queried.
+ "completerQueried" : [completer, addUrls]
+ };
+
+ doUpdateTest([update], assertions,
+ function() {
+ // Give the dbservice a chance to cache the result.
+ var timer = new Timer(3000, part2);
+ }, updateError);
+}
+
+function testCachedResults()
+{
+ setupCachedResults(["foo.com/a"], function(add) {
+ // This is called after setupCachedResults(). Verify that
+ // checking the url again does not cause a completer request.
+
+ // install a new completer, this one should never be queried.
+ var newCompleter = installCompleter('test-phish-simple', [[1, []]], []);
+
+ var assertions = {
+ "urlsExist" : ["foo.com/a"],
+ "completerQueried" : [newCompleter, []]
+ };
+ checkAssertions(assertions, runNextTest);
+ });
+}
+
+function testCachedResultsWithSub() {
+ setupCachedResults(["foo.com/a"], function() {
+ // install a new completer, this one should never be queried.
+ var newCompleter = installCompleter('test-phish-simple', [[1, []]], []);
+
+ var removeUpdate = buildPhishingUpdate(
+ [ { "chunkNum" : 2,
+ "chunkType" : "s",
+ "urls": ["1:foo.com/a"] }],
+ 4);
+
+ var assertions = {
+ "urlsDontExist" : ["foo.com/a"],
+ "completerQueried" : [newCompleter, []]
+ }
+
+ doTest([removeUpdate], assertions);
+ });
+}
+
+function testCachedResultsWithExpire() {
+ setupCachedResults(["foo.com/a"], function() {
+ // install a new completer, this one should never be queried.
+ var newCompleter = installCompleter('test-phish-simple', [[1, []]], []);
+
+ var expireUpdate =
+ "n:1000\n" +
+ "i:test-phish-simple\n" +
+ "ad:1\n";
+
+ var assertions = {
+ "urlsDontExist" : ["foo.com/a"],
+ "completerQueried" : [newCompleter, []]
+ }
+ doTest([expireUpdate], assertions);
+ });
+}
+
+function testCachedResultsUpdate()
+{
+ var existUrls = ["foo.com/a"];
+ setupCachedResults(existUrls, function() {
+ // This is called after setupCachedResults(). Verify that
+ // checking the url again does not cause a completer request.
+
+ // install a new completer, this one should never be queried.
+ var newCompleter = installCompleter('test-phish-simple', [[1, []]], []);
+
+ var assertions = {
+ "urlsExist" : existUrls,
+ "completerQueried" : [newCompleter, []]
+ };
+
+ var addUrls = ["foobar.org/a"];
+
+ var update2 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 2,
+ "urls" : addUrls
+ }],
+ 4);
+
+ checkAssertions(assertions, function () {
+ // Apply the update. The cached completes should be gone.
+ doStreamUpdate(update2, function() {
+ // Now the completer gets queried again.
+ var newCompleter2 = installCompleter('test-phish-simple', [[1, existUrls]], []);
+ var assertions2 = {
+ "tableData" : "test-phish-simple;a:1-2",
+ "urlsExist" : existUrls,
+ "completerQueried" : [newCompleter2, existUrls]
+ };
+ checkAssertions(assertions2, runNextTest);
+ }, updateError);
+ });
+ });
+}
+
+function testCachedResultsFailure()
+{
+ var existUrls = ["foo.com/a"];
+ setupCachedResults(existUrls, function() {
+ // This is called after setupCachedResults(). Verify that
+ // checking the url again does not cause a completer request.
+
+ // install a new completer, this one should never be queried.
+ var newCompleter = installCompleter('test-phish-simple', [[1, []]], []);
+
+ var assertions = {
+ "urlsExist" : existUrls,
+ "completerQueried" : [newCompleter, []]
+ };
+
+ var addUrls = ["foobar.org/a"];
+
+ var update2 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 2,
+ "urls" : addUrls
+ }],
+ 4);
+
+ checkAssertions(assertions, function() {
+ // Apply the update. The cached completes should be gone.
+ doErrorUpdate("test-phish-simple,test-malware-simple", function() {
+ // Now the completer gets queried again.
+ var newCompleter2 = installCompleter('test-phish-simple', [[1, existUrls]], []);
+ var assertions2 = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : existUrls,
+ "completerQueried" : [newCompleter2, existUrls]
+ };
+ checkAssertions(assertions2, runNextTest);
+ }, updateError);
+ });
+ });
+}
+
+function testErrorList()
+{
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 4);
+ // The update failure should will kill the completes, so the above
+ // must be a prefix to get any hit at all past the update failure.
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls,
+ // These are complete urls, and will only be completed if the
+ // list is stale.
+ "completerQueried" : [completer, addUrls]
+ };
+
+ // Apply the update.
+ doStreamUpdate(update, function() {
+ // Now the test-phish-simple and test-malware-simple tables are marked
+ // as fresh. Fake an update failure to mark them stale.
+ doErrorUpdate("test-phish-simple,test-malware-simple", function() {
+ // Now the lists should be marked stale. Check assertions.
+ checkAssertions(assertions, runNextTest);
+ }, updateError);
+ }, updateError);
+}
+
+
+function testStaleList()
+{
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 32);
+
+ var completer = installCompleter('test-phish-simple', [[1, addUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : addUrls,
+ // These are complete urls, and will only be completed if the
+ // list is stale.
+ "completerQueried" : [completer, addUrls]
+ };
+
+ // Consider a match stale after one second.
+ prefBranch.setIntPref("urlclassifier.max-complete-age", 1);
+
+ // Apply the update.
+ doStreamUpdate(update, function() {
+ // Now the test-phish-simple and test-malware-simple tables are marked
+ // as fresh. Wait three seconds to make sure the list is marked stale.
+ new Timer(3000, function() {
+ // Now the lists should be marked stale. Check assertions.
+ checkAssertions(assertions, function() {
+ prefBranch.setIntPref("urlclassifier.max-complete-age", 2700);
+ runNextTest();
+ });
+ }, updateError);
+ }, updateError);
+}
+
+// Same as testStaleList, but verifies that an empty response still
+// unconfirms the entry.
+function testStaleListEmpty()
+{
+ var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls
+ }],
+ 32);
+
+ var completer = installCompleter('test-phish-simple', [], []);
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ // None of these should match, because they won't be completed
+ "urlsDontExist" : addUrls,
+ // These are complete urls, and will only be completed if the
+ // list is stale.
+ "completerQueried" : [completer, addUrls]
+ };
+
+ // Consider a match stale after one second.
+ prefBranch.setIntPref("urlclassifier.max-complete-age", 1);
+
+ // Apply the update.
+ doStreamUpdate(update, function() {
+ // Now the test-phish-simple and test-malware-simple tables are marked
+ // as fresh. Wait three seconds to make sure the list is marked stale.
+ new Timer(3000, function() {
+ // Now the lists should be marked stale. Check assertions.
+ checkAssertions(assertions, function() {
+ prefBranch.setIntPref("urlclassifier.max-complete-age", 2700);
+ runNextTest();
+ });
+ }, updateError);
+ }, updateError);
+}
+
+
+// Verify that different lists (test-phish-simple,
+// test-malware-simple) maintain their freshness separately.
+function testErrorListIndependent()
+{
+ var phishUrls = [ "phish.com/a" ];
+ var malwareUrls = [ "attack.com/a" ];
+ var update = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : phishUrls
+ }],
+ 4);
+ // These have to persist past the update failure, so they must be prefixes,
+ // not completes.
+
+ update += buildMalwareUpdate(
+ [
+ { "chunkNum" : 2,
+ "urls" : malwareUrls
+ }],
+ 32);
+
+ var completer = installCompleter('test-phish-simple', [[1, phishUrls]], []);
+
+ var assertions = {
+ "tableData" : "test-malware-simple;a:2\ntest-phish-simple;a:1",
+ "urlsExist" : phishUrls,
+ "malwareUrlsExist" : malwareUrls,
+ // Only this phishing urls should be completed, because only the phishing
+ // urls will be stale.
+ "completerQueried" : [completer, phishUrls]
+ };
+
+ // Apply the update.
+ doStreamUpdate(update, function() {
+ // Now the test-phish-simple and test-malware-simple tables are
+ // marked as fresh. Fake an update failure to mark *just*
+ // phishing data as stale.
+ doErrorUpdate("test-phish-simple", function() {
+ // Now the lists should be marked stale. Check assertions.
+ checkAssertions(assertions, runNextTest);
+ }, updateError);
+ }, updateError);
+}
+
+function run_test()
+{
+ runTests([
+ testPartialAdds,
+ testPartialAddsWithConflicts,
+ testFragments,
+ testSpecFragments,
+ testMoreSpecFragments,
+ testFalsePositives,
+ testEmptyCompleter,
+ testCompleterFailure,
+ testMixedSizesSameDomain,
+ testMixedSizesDifferentDomains,
+ testInvalidHashSize,
+ testWrongTable,
+ testCachedResults,
+ testCachedResultsWithSub,
+ testCachedResultsWithExpire,
+ testCachedResultsUpdate,
+ testCachedResultsFailure,
+ testStaleList,
+ testStaleListEmpty,
+ testErrorList,
+ testErrorListIndependent
+ ]);
+}
+
+do_test_pending();
diff --git a/toolkit/components/url-classifier/tests/unit/test_pref.js b/toolkit/components/url-classifier/tests/unit/test_pref.js
new file mode 100644
index 0000000000..68030a2466
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_pref.js
@@ -0,0 +1,14 @@
+function run_test() {
+ let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+
+ // The google protocol version should be "2.2" until we enable SB v4
+ // by default.
+ equal(urlUtils.getProtocolVersion("google"), "2.2");
+
+ // Mozilla protocol version will stick to "2.2".
+ equal(urlUtils.getProtocolVersion("mozilla"), "2.2");
+
+ // Unknown provider version will be "2.2".
+ equal(urlUtils.getProtocolVersion("unknown-provider"), "2.2");
+} \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/unit/test_prefixset.js b/toolkit/components/url-classifier/tests/unit/test_prefixset.js
new file mode 100644
index 0000000000..f2ecc9c2b3
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_prefixset.js
@@ -0,0 +1,232 @@
+// newPset: returns an empty nsIUrlClassifierPrefixSet.
+function newPset() {
+ let pset = Cc["@mozilla.org/url-classifier/prefixset;1"]
+ .createInstance(Ci.nsIUrlClassifierPrefixSet);
+ pset.init("all");
+ return pset;
+}
+
+// arrContains: returns true if |arr| contains the element |target|. Uses binary
+// search and requires |arr| to be sorted.
+function arrContains(arr, target) {
+ let start = 0;
+ let end = arr.length - 1;
+ let i = 0;
+
+ while (end > start) {
+ i = start + (end - start >> 1);
+ let value = arr[i];
+
+ if (value < target)
+ start = i+1;
+ else if (value > target)
+ end = i-1;
+ else
+ break;
+ }
+ if (start == end)
+ i = start;
+
+ return (!(i < 0 || i >= arr.length) && arr[i] == target);
+}
+
+// checkContents: Check whether the PrefixSet pset contains
+// the prefixes in the passed array.
+function checkContents(pset, prefixes) {
+ var outcount = {}, outset = {};
+ outset = pset.getPrefixes(outcount);
+ let inset = prefixes;
+ do_check_eq(inset.length, outset.length);
+ inset.sort((x,y) => x - y);
+ for (let i = 0; i < inset.length; i++) {
+ do_check_eq(inset[i], outset[i]);
+ }
+}
+
+function wrappedProbe(pset, prefix) {
+ return pset.contains(prefix);
+};
+
+// doRandomLookups: we use this to test for false membership with random input
+// over the range of prefixes (unsigned 32-bits integers).
+// pset: a nsIUrlClassifierPrefixSet to test.
+// prefixes: an array of prefixes supposed to make up the prefix set.
+// N: number of random lookups to make.
+function doRandomLookups(pset, prefixes, N) {
+ for (let i = 0; i < N; i++) {
+ let randInt = prefixes[0];
+ while (arrContains(prefixes, randInt))
+ randInt = Math.floor(Math.random() * Math.pow(2, 32));
+
+ do_check_false(wrappedProbe(pset, randInt));
+ }
+}
+
+// doExpectedLookups: we use this to test expected membership.
+// pset: a nsIUrlClassifierPrefixSet to test.
+// prefixes:
+function doExpectedLookups(pset, prefixes, N) {
+ for (let i = 0; i < N; i++) {
+ prefixes.forEach(function (x) {
+ dump("Checking " + x + "\n");
+ do_check_true(wrappedProbe(pset, x));
+ });
+ }
+}
+
+// testBasicPset: A very basic test of the prefix set to make sure that it
+// exists and to give a basic example of its use.
+function testBasicPset() {
+ let pset = Cc["@mozilla.org/url-classifier/prefixset;1"]
+ .createInstance(Ci.nsIUrlClassifierPrefixSet);
+ let prefixes = [2,50,100,2000,78000,1593203];
+ pset.setPrefixes(prefixes, prefixes.length);
+
+ do_check_true(wrappedProbe(pset, 100));
+ do_check_false(wrappedProbe(pset, 100000));
+ do_check_true(wrappedProbe(pset, 1593203));
+ do_check_false(wrappedProbe(pset, 999));
+ do_check_false(wrappedProbe(pset, 0));
+
+
+ checkContents(pset, prefixes);
+}
+
+function testDuplicates() {
+ let pset = Cc["@mozilla.org/url-classifier/prefixset;1"]
+ .createInstance(Ci.nsIUrlClassifierPrefixSet);
+ let prefixes = [1,1,2,2,2,3,3,3,3,3,3,5,6,6,7,7,9,9,9];
+ pset.setPrefixes(prefixes, prefixes.length);
+
+ do_check_true(wrappedProbe(pset, 1));
+ do_check_true(wrappedProbe(pset, 2));
+ do_check_true(wrappedProbe(pset, 5));
+ do_check_true(wrappedProbe(pset, 9));
+ do_check_false(wrappedProbe(pset, 4));
+ do_check_false(wrappedProbe(pset, 8));
+
+
+ checkContents(pset, prefixes);
+}
+
+function testSimplePset() {
+ let pset = newPset();
+ let prefixes = [1,2,100,400,123456789];
+ pset.setPrefixes(prefixes, prefixes.length);
+
+ doRandomLookups(pset, prefixes, 100);
+ doExpectedLookups(pset, prefixes, 1);
+
+
+ checkContents(pset, prefixes);
+}
+
+function testReSetPrefixes() {
+ let pset = newPset();
+ let prefixes = [1, 5, 100, 1000, 150000];
+ pset.setPrefixes(prefixes, prefixes.length);
+
+ doExpectedLookups(pset, prefixes, 1);
+
+ let secondPrefixes = [12, 50, 300, 2000, 5000, 200000];
+ pset.setPrefixes(secondPrefixes, secondPrefixes.length);
+
+ doExpectedLookups(pset, secondPrefixes, 1);
+ for (let i = 0; i < prefixes.length; i++) {
+ do_check_false(wrappedProbe(pset, prefixes[i]));
+ }
+
+
+ checkContents(pset, secondPrefixes);
+}
+
+function testLoadSaveLargeSet() {
+ let N = 1000;
+ let arr = [];
+
+ for (let i = 0; i < N; i++) {
+ let randInt = Math.floor(Math.random() * Math.pow(2, 32));
+ arr.push(randInt);
+ }
+
+ arr.sort((x,y) => x - y);
+
+ let pset = newPset();
+ pset.setPrefixes(arr, arr.length);
+
+ doExpectedLookups(pset, arr, 1);
+ doRandomLookups(pset, arr, 1000);
+
+ checkContents(pset, arr);
+
+ // Now try to save, restore, and redo the lookups
+ var file = dirSvc.get('ProfLD', Ci.nsIFile);
+ file.append("testLarge.pset");
+
+ pset.storeToFile(file);
+
+ let psetLoaded = newPset();
+ psetLoaded.loadFromFile(file);
+
+ doExpectedLookups(psetLoaded, arr, 1);
+ doRandomLookups(psetLoaded, arr, 1000);
+
+ checkContents(psetLoaded, arr);
+}
+
+function testTinySet() {
+ let pset = Cc["@mozilla.org/url-classifier/prefixset;1"]
+ .createInstance(Ci.nsIUrlClassifierPrefixSet);
+ let prefixes = [1];
+ pset.setPrefixes(prefixes, prefixes.length);
+
+ do_check_true(wrappedProbe(pset, 1));
+ do_check_false(wrappedProbe(pset, 100000));
+ checkContents(pset, prefixes);
+
+ prefixes = [];
+ pset.setPrefixes(prefixes, prefixes.length);
+ do_check_false(wrappedProbe(pset, 1));
+ checkContents(pset, prefixes);
+}
+
+function testLoadSaveNoDelta() {
+ let N = 100;
+ let arr = [];
+
+ for (let i = 0; i < N; i++) {
+ // construct a tree without deltas by making the distance
+ // between entries larger than 16 bits
+ arr.push(((1 << 16) + 1) * i);
+ }
+
+ let pset = newPset();
+ pset.setPrefixes(arr, arr.length);
+
+ doExpectedLookups(pset, arr, 1);
+
+ var file = dirSvc.get('ProfLD', Ci.nsIFile);
+ file.append("testNoDelta.pset");
+
+ pset.storeToFile(file);
+ pset.loadFromFile(file);
+
+ doExpectedLookups(pset, arr, 1);
+}
+
+var tests = [testBasicPset,
+ testSimplePset,
+ testReSetPrefixes,
+ testLoadSaveLargeSet,
+ testDuplicates,
+ testTinySet,
+ testLoadSaveNoDelta];
+
+function run_test() {
+ // None of the tests use |executeSoon| or any sort of callbacks, so we can
+ // just run them in succession.
+ for (let i = 0; i < tests.length; i++) {
+ dump("Running " + tests[i].name + "\n");
+ tests[i]();
+ }
+}
diff --git a/toolkit/components/url-classifier/tests/unit/test_provider_url.js b/toolkit/components/url-classifier/tests/unit/test_provider_url.js
new file mode 100644
index 0000000000..9a946dc3fe
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_provider_url.js
@@ -0,0 +1,34 @@
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm");
+
+function updateVersion(version) {
+ updateAppInfo({ version });
+}
+
+add_test(function test_provider_url() {
+ let urls = [
+ "browser.safebrowsing.provider.google.updateURL",
+ "browser.safebrowsing.provider.google.gethashURL",
+ "browser.safebrowsing.provider.mozilla.updateURL",
+ "browser.safebrowsing.provider.mozilla.gethashURL"
+ ];
+
+ let versions = [
+ "49.0",
+ "49.0.1",
+ "49.0a1",
+ "49.0b1",
+ "49.0esr",
+ "49.0.1esr"
+ ];
+
+ for (let version of versions) {
+ for (let url of urls) {
+ updateVersion(version);
+ let value = Services.urlFormatter.formatURLPref(url);
+ Assert.notEqual(value.indexOf("&appver=49.0&"), -1);
+ }
+ }
+
+ run_next_test();
+});
diff --git a/toolkit/components/url-classifier/tests/unit/test_safebrowsing_protobuf.js b/toolkit/components/url-classifier/tests/unit/test_safebrowsing_protobuf.js
new file mode 100644
index 0000000000..45309ba54e
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_safebrowsing_protobuf.js
@@ -0,0 +1,23 @@
+function run_test() {
+ let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+
+ // No list at all.
+ let requestNoList = urlUtils.makeUpdateRequestV4([], [], 0);
+
+ // Only one valid list name.
+ let requestOneValid =
+ urlUtils.makeUpdateRequestV4(["goog-phish-proto"], ["AAAAAA"], 1);
+
+ // Only one invalid list name.
+ let requestOneInvalid =
+ urlUtils.makeUpdateRequestV4(["bad-list-name"], ["AAAAAA"], 1);
+
+ // One valid and one invalid list name.
+ let requestOneInvalidOneValid =
+ urlUtils.makeUpdateRequestV4(["goog-phish-proto", "bad-list-name"],
+ ["AAAAAA", "AAAAAA"], 2);
+
+ equal(requestNoList, requestOneInvalid);
+ equal(requestOneValid, requestOneInvalidOneValid);
+} \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/unit/test_streamupdater.js b/toolkit/components/url-classifier/tests/unit/test_streamupdater.js
new file mode 100644
index 0000000000..e5abc4e91a
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_streamupdater.js
@@ -0,0 +1,288 @@
+function doTest(updates, assertions, expectError)
+{
+ if (expectError) {
+ doUpdateTest(updates, assertions, updateError, runNextTest);
+ } else {
+ doUpdateTest(updates, assertions, runNextTest, updateError);
+ }
+}
+
+// Never use the same URLs for multiple tests, because we aren't guaranteed
+// to reset the database between tests.
+function testFillDb() {
+ var add1Urls = [ "zaz.com/a", "yxz.com/c" ];
+
+ var update = "n:1000\n";
+ update += "i:test-phish-simple\n";
+
+ var update1 = buildBareUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update1) + "\n";
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1",
+ "urlsExist" : add1Urls
+ };
+
+ doTest([update], assertions, false);
+}
+
+function testSimpleForward() {
+ var add1Urls = [ "foo-simple.com/a", "bar-simple.com/c" ];
+ var add2Urls = [ "foo-simple.com/b" ];
+ var add3Urls = [ "bar-simple.com/d" ];
+
+ var update = "n:1000\n";
+ update += "i:test-phish-simple\n";
+
+ var update1 = buildBareUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update1) + "\n";
+
+ var update2 = buildBareUpdate(
+ [{ "chunkNum" : 2,
+ "urls" : add2Urls }]);
+ update += "u:data:," + encodeURIComponent(update2) + "\n";
+
+ var update3 = buildBareUpdate(
+ [{ "chunkNum" : 3,
+ "urls" : add3Urls }]);
+ update += "u:data:," + encodeURIComponent(update3) + "\n";
+
+ var assertions = {
+ "tableData" : "test-phish-simple;a:1-3",
+ "urlsExist" : add1Urls.concat(add2Urls).concat(add3Urls)
+ };
+
+ doTest([update], assertions, false);
+}
+
+// Make sure that a nested forward (a forward within a forward) causes
+// the update to fail.
+function testNestedForward() {
+ var add1Urls = [ "foo-nested.com/a", "bar-nested.com/c" ];
+ var add2Urls = [ "foo-nested.com/b" ];
+
+ var update = "n:1000\n";
+ update += "i:test-phish-simple\n";
+
+ var update1 = buildBareUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update1) + "\n";
+
+ var update2 = buildBareUpdate(
+ [{ "chunkNum" : 2 }]);
+ var update3 = buildBareUpdate(
+ [{ "chunkNum" : 3,
+ "urls" : add1Urls }]);
+
+ update2 += "u:data:," + encodeURIComponent(update3) + "\n";
+
+ update += "u:data:," + encodeURIComponent(update2) + "\n";
+
+ var assertions = {
+ "tableData" : "",
+ "urlsDontExist" : add1Urls.concat(add2Urls)
+ };
+
+ doTest([update], assertions, true);
+}
+
+// An invalid URL forward causes the update to fail.
+function testInvalidUrlForward() {
+ var add1Urls = [ "foo-invalid.com/a", "bar-invalid.com/c" ];
+
+ var update = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:asdf://blah/blah\n"; // invalid URL scheme
+
+ // add1Urls is present, but that is an artifact of the way we do the test.
+ var assertions = {
+ "tableData" : "",
+ "urlsExist" : add1Urls
+ };
+
+ doTest([update], assertions, true);
+}
+
+// A failed network request causes the update to fail.
+function testErrorUrlForward() {
+ var add1Urls = [ "foo-forward.com/a", "bar-forward.com/c" ];
+
+ var update = buildPhishingUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:http://test.invalid/asdf/asdf\n"; // invalid URL scheme
+
+ // add1Urls is present, but that is an artifact of the way we do the test.
+ var assertions = {
+ "tableData" : "",
+ "urlsExist" : add1Urls
+ };
+
+ doTest([update], assertions, true);
+}
+
+function testMultipleTables() {
+ var add1Urls = [ "foo-multiple.com/a", "bar-multiple.com/c" ];
+ var add2Urls = [ "foo-multiple.com/b" ];
+ var add3Urls = [ "bar-multiple.com/d" ];
+ var add4Urls = [ "bar-multiple.com/e" ];
+ var add6Urls = [ "bar-multiple.com/g" ];
+
+ var update = "n:1000\n";
+ update += "i:test-phish-simple\n";
+
+ var update1 = buildBareUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update1) + "\n";
+
+ var update2 = buildBareUpdate(
+ [{ "chunkNum" : 2,
+ "urls" : add2Urls }]);
+ update += "u:data:," + encodeURIComponent(update2) + "\n";
+
+ update += "i:test-malware-simple\n";
+
+ var update3 = buildBareUpdate(
+ [{ "chunkNum" : 3,
+ "urls" : add3Urls }]);
+ update += "u:data:," + encodeURIComponent(update3) + "\n";
+
+ update += "i:test-unwanted-simple\n";
+ var update4 = buildBareUpdate(
+ [{ "chunkNum" : 4,
+ "urls" : add4Urls }]);
+ update += "u:data:," + encodeURIComponent(update4) + "\n";
+
+ update += "i:test-block-simple\n";
+ var update6 = buildBareUpdate(
+ [{ "chunkNum" : 6,
+ "urls" : add6Urls }]);
+ update += "u:data:," + encodeURIComponent(update6) + "\n";
+
+ var assertions = {
+ "tableData" : "test-block-simple;a:6\ntest-malware-simple;a:3\ntest-phish-simple;a:1-2\ntest-unwanted-simple;a:4",
+ "urlsExist" : add1Urls.concat(add2Urls),
+ "malwareUrlsExist" : add3Urls,
+ "unwantedUrlsExist" : add4Urls,
+ "blockedUrlsExist" : add6Urls
+ };
+
+ doTest([update], assertions, false);
+}
+
+function testUrlInMultipleTables() {
+ var add1Urls = [ "foo-forward.com/a" ];
+
+ var update = "n:1000\n";
+ update += "i:test-phish-simple\n";
+
+ var update1 = buildBareUpdate(
+ [{ "chunkNum" : 1,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update1) + "\n";
+
+ update += "i:test-malware-simple\n";
+ var update2 = buildBareUpdate(
+ [{ "chunkNum" : 2,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update2) + "\n";
+
+ update += "i:test-unwanted-simple\n";
+ var update3 = buildBareUpdate(
+ [{ "chunkNum" : 3,
+ "urls" : add1Urls }]);
+ update += "u:data:," + encodeURIComponent(update3) + "\n";
+
+ var assertions = {
+ "tableData" : "test-malware-simple;a:2\ntest-phish-simple;a:1\ntest-unwanted-simple;a:3",
+ "urlExistInMultipleTables" : { url: add1Urls,
+ tables: "test-malware-simple,test-phish-simple,test-unwanted-simple" }
+ };
+
+ doTest([update], assertions, false);
+}
+
+function Observer(callback) {
+ this.observe = callback;
+}
+
+Observer.prototype =
+{
+QueryInterface: function(iid)
+{
+ if (!iid.equals(Ci.nsISupports) &&
+ !iid.equals(Ci.nsIObserver)) {
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+}
+};
+
+// Tests a database reset request.
+function testReset() {
+ // The moz-phish-simple table is populated separately from the other update in
+ // a separate update request. Therefore it should not be reset when we run the
+ // updates later in this function.
+ var mozAddUrls = [ "moz-reset.com/a" ];
+ var mozUpdate = buildMozPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : mozAddUrls
+ }]);
+
+ var dataUpdate = "data:," + encodeURIComponent(mozUpdate);
+
+ streamUpdater.downloadUpdates(mozTables, "", true,
+ dataUpdate, () => {}, updateError, updateError);
+
+ var addUrls1 = [ "foo-reset.com/a", "foo-reset.com/b" ];
+ var update1 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 1,
+ "urls" : addUrls1
+ }]);
+
+ var update2 = "n:1000\nr:pleasereset\n";
+
+ var addUrls3 = [ "bar-reset.com/a", "bar-reset.com/b" ];
+ var update3 = buildPhishingUpdate(
+ [
+ { "chunkNum" : 3,
+ "urls" : addUrls3
+ }]);
+
+ var assertions = {
+ "tableData" : "moz-phish-simple;a:1\ntest-phish-simple;a:3", // tables that should still be there.
+ "mozPhishingUrlsExist" : mozAddUrls, // mozAddUrls added prior to the reset
+ // but it should still exist after reset.
+ "urlsExist" : addUrls3, // addUrls3 added after the reset.
+ "urlsDontExist" : addUrls1 // addUrls1 added prior to the reset
+ };
+
+ // Use these update responses in order. The update request only
+ // contains test-*-simple tables so the reset will only apply to these.
+ doTest([update1, update2, update3], assertions, false);
+}
+
+
+function run_test()
+{
+ runTests([
+ testSimpleForward,
+ testNestedForward,
+ testInvalidUrlForward,
+ testErrorUrlForward,
+ testMultipleTables,
+ testUrlInMultipleTables,
+ testReset
+ ]);
+}
+
+do_test_pending();
diff --git a/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js b/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js
new file mode 100644
index 0000000000..f7c51b9562
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js
@@ -0,0 +1,37 @@
+function run_test() {
+ let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+
+ // Test list name to threat type conversion.
+
+ equal(urlUtils.convertListNameToThreatType("goog-malware-proto"), 1);
+ equal(urlUtils.convertListNameToThreatType("googpub-phish-proto"), 2);
+ equal(urlUtils.convertListNameToThreatType("goog-unwanted-proto"), 3);
+ equal(urlUtils.convertListNameToThreatType("goog-phish-proto"), 5);
+
+ try {
+ urlUtils.convertListNameToThreatType("bad-list-name");
+ ok(false, "Bad list name should lead to exception.");
+ } catch (e) {}
+
+ try {
+ urlUtils.convertListNameToThreatType("bad-list-name");
+ ok(false, "Bad list name should lead to exception.");
+ } catch (e) {}
+
+ // Test threat type to list name conversion.
+ equal(urlUtils.convertThreatTypeToListNames(1), "goog-malware-proto");
+ equal(urlUtils.convertThreatTypeToListNames(2), "googpub-phish-proto,test-phish-proto");
+ equal(urlUtils.convertThreatTypeToListNames(3), "goog-unwanted-proto,test-unwanted-proto");
+ equal(urlUtils.convertThreatTypeToListNames(5), "goog-phish-proto");
+
+ try {
+ urlUtils.convertThreatTypeToListNames(0);
+ ok(false, "Bad threat type should lead to exception.");
+ } catch (e) {}
+
+ try {
+ urlUtils.convertThreatTypeToListNames(100);
+ ok(false, "Bad threat type should lead to exception.");
+ } catch (e) {}
+} \ No newline at end of file
diff --git a/toolkit/components/url-classifier/tests/unit/xpcshell.ini b/toolkit/components/url-classifier/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..c34d575c62
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unit/xpcshell.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+head = head_urlclassifier.js
+tail = tail_urlclassifier.js
+skip-if = toolkit == 'android'
+support-files =
+ data/digest1.chunk
+ data/digest2.chunk
+
+[test_addsub.js]
+[test_bug1274685_unowned_list.js]
+[test_backoff.js]
+[test_dbservice.js]
+[test_hashcompleter.js]
+# Bug 752243: Profile cleanup frequently fails
+#skip-if = os == "mac" || os == "linux"
+[test_partial.js]
+[test_prefixset.js]
+[test_threat_type_conversion.js]
+[test_provider_url.js]
+[test_streamupdater.js]
+[test_digest256.js]
+[test_listmanager.js]
+[test_pref.js]
+[test_safebrowsing_protobuf.js]
diff --git a/toolkit/components/url-classifier/tests/unittests.xul b/toolkit/components/url-classifier/tests/unittests.xul
new file mode 100644
index 0000000000..0c9ce898bc
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/unittests.xul
@@ -0,0 +1,188 @@
+<?xml version="1.0"?>
+<window id="PROT_unittest"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onProtUnittestLoad();"
+ title="prot unittests">
+
+<script><![CDATA[
+ const Cc = Components.classes;
+ const Ci = Components.interfaces;
+
+ function G_Debug(zone, s) {
+ var label = document.createElement('label');
+ var txt = "[" + zone + "] " + s;
+ label.appendChild(document.createTextNode(txt));
+
+ document.documentElement.appendChild(label);
+ }
+
+ function G_Assert(zone, cond, msg) {
+ if (!cond) {
+ G_Debug(zone, msg);
+ throw msg;
+ }
+ }
+
+ function ProtectionTableTests() {
+ var z = "trtable UNITTEST";
+
+ G_Debug(z, "Starting");
+
+ var url = "http://www.yahoo.com?foo=bar";
+ var url2 = "http://168.188.99.26/.secure/www.ebay.com/";
+ var urlTable = Cc['@mozilla.org/url-classifier/table;1?type=url']
+ .createInstance(Ci.nsIUrlClassifierTable);
+ urlTable.insert(url, "1");
+ urlTable.insert(url2, "1");
+ G_Assert(z, urlTable.exists(url), "URL lookups broken");
+ G_Assert(z, !urlTable.exists("about:config"), "about:config breaks domlook");
+ G_Assert(z, urlTable.exists(url2), "URL lookups broken");
+ G_Assert(z, urlTable.exists("http://%31%36%38%2e%31%38%38%2e%39%39%2e%32%36/%2E%73%65%63%75%72%65/%77%77%77%2E%65%62%61%79%2E%63%6F%6D/") == true,
+ "URL Canonicalization broken");
+ G_Assert(z, urlTable.count == 2, 'urlTable: wrong size');
+
+ var dom1 = "bar.com";
+ var dom2 = "amazon.co.uk";
+ var dom3 = "127.0.0.1";
+ var domainTable = Cc['@mozilla.org/url-classifier/table;1?type=domain']
+ .createInstance(Ci.nsIUrlClassifierTable);
+ domainTable.insert(dom1, "1");
+ domainTable.insert(dom2, "1");
+ domainTable.insert(dom3, "1");
+ G_Assert(z, domainTable.exists("http://www.bar.com/?zaz=asdf#url"),
+ "Domain lookups broken (single dot)");
+ G_Assert(z, domainTable.exists("http://www.amazon.co.uk/?z=af#url"),
+ "Domain lookups broken (two dots)");
+ G_Assert(z, domainTable.exists("http://127.0.0.1/?z=af#url"),
+ "Domain lookups broken (IP)");
+ G_Assert(z, domainTable.count == 3, 'domainTable: wrong size');
+
+ var site1 = "google.com/safebrowsing/";
+ var site2 = "www.foo.bar/";
+ var site3 = "127.0.0.1/";
+ var siteTable = Cc['@mozilla.org/url-classifier/table;1?type=site']
+ .createInstance(Ci.nsIUrlClassifierTable);
+ siteTable.insert(site1, "1");
+ siteTable.insert(site2, "1");
+ siteTable.insert(site3, "1");
+ G_Assert(z, siteTable.exists("http://www.google.com/safebrowsing/1.php"),
+ "Site lookups broken - reducing");
+ G_Assert(z, siteTable.exists("http://www.foo.bar/some/random/path"),
+ "Site lookups broken - fqdn");
+ G_Assert(z, siteTable.exists("http://127.0.0.1/something?hello=1"),
+ "Site lookups broken - IP");
+ G_Assert(z, !siteTable.exists("http://www.google.com/search/"),
+ "Site lookups broken - overreaching");
+ G_Assert(z, siteTable.count == 3, 'siteTable: wrong size');
+
+ var url1 = "http://poseidon.marinet.gr/~eleni/eBay/index.php";
+ var domainHash = "01844755C8143C4579BB28DD59C23747";
+ var enchashTable = Cc['@mozilla.org/url-classifier/table;1?type=enchash']
+ .createInstance(Ci.nsIUrlClassifierTable);
+ enchashTable.insert(domainHash, "bGtEQWJuMl9FA3Kl5RiXMpgFU8nDJl9J0hXjUck9+"
+ + "mMUQwAN6llf0gJeY5DIPPc2f+a8MSBFJN17ANGJ"
+ + "Zl5oZVsQfSW4i12rlScsx4tweZAE");
+ G_Assert(z, enchashTable.exists(url1), 'enchash lookup failed');
+ G_Assert(z, !enchashTable.exists(url1 + '/foo'),
+ "enchash lookup broken - overreaching");
+ G_Assert(z, enchashTable.count == 1, 'enchashTable: wrong size');
+
+ // TODO: test replace
+ G_Debug(z, "PASSED");
+ }
+
+ function ProtectionListManagerTests() {
+ var z = "listmanager UNITTEST";
+ G_Debug(z, "Starting");
+
+ // test lookup and register
+ var listManagerInst = Cc["@mozilla.org/url-classifier/listmanager;1"]
+ .createInstance(Ci.nsIUrlListManager);
+ var listName = 'foo-bar-url';
+ listManagerInst.registerTable(listName, false);
+ listManagerInst.safeInsert(listName, 'test', '1');
+ G_Assert(z, listManagerInst.safeExists(listName, 'test'),
+ 'insert/exist failed');
+
+ // test serialization
+ var baseName = (new Date().getTime()) + ".tmp";
+ var tempDir = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsILocalFile);
+ tempDir.append(baseName);
+ tempDir.createUnique(tempDir.DIRECTORY_TYPE, 0744);
+
+ var listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]
+ .getService(Ci.nsIUrlListManager);
+ listManager.setAppDir(tempDir);
+
+ var data = "";
+
+ var set1Name = "test1-foo-domain";
+ data += "[" + set1Name + " 1.2]\n";
+ var set1 = {};
+ for (var i = 0; i < 10; i++) {
+ set1["http://" + i + ".com"] = 1;
+ data += "+" + i + ".com\t1\n";
+ }
+
+ data += "\n";
+ var set2Name = "test2-foo-domain";
+ // TODO must have blank line
+ data += "\n[" + set2Name + " 1.7]\n";
+ var set2 = {};
+ for (var i = 0; i < 5; i++) {
+ set2["http://" + i + ".com"] = 1;
+ data += "+" + i + ".com\t1\n";
+ }
+
+ function deserialized(tablesKnown, tablesData) {
+ listManager.wrappedJSObject.dataReady(tablesKnown, tablesData);
+
+ var file = tempDir.clone();
+ file.append(set1Name + ".sst");
+ G_Assert(z, file.exists() && file.isFile() && file.isReadable(),
+ "Failed to write out: " + file.path);
+
+ file = tempDir.clone();
+ file.append(set2Name + ".sst");
+ G_Assert(z, file.exists() && file.isFile() && file.isReadable(),
+ "Failed to write out: " + file.path);
+
+ // now try to read them back from disk
+ listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]
+ .createInstance(Ci.nsIUrlListManager);
+ listManager.setAppDir(tempDir);
+ var tables = [ set1Name, set2Name ];
+ listManager.enableUpdate(set1Name);
+ listManager.enableUpdate(set2Name);
+ listManager.wrappedJSObject.readDataFiles();
+
+ // assert that the values match
+ for (var prop in set1) {
+ G_Assert(z,
+ listManager.wrappedJSObject.tablesData[set1Name].exists(prop),
+ "Couldn't find member " + prop + "of set1 from disk.");
+ }
+
+ for (var prop in set2) {
+ G_Assert(z,
+ listManager.wrappedJSObject.tablesData[set2Name].exists(prop),
+ "Couldn't find member " + prop + "of set2 from disk.");
+ }
+
+ tempDir.remove(true);
+
+ G_Debug(z, "PASSED");
+ };
+
+ // Use the unwrapped object for the unittest
+ listManager.wrappedJSObject.deserialize_(data, deserialized);
+ }
+
+ function onProtUnittestLoad() {
+ ProtectionTableTests();
+ ProtectionListManagerTests();
+ }
+]]></script>
+</window>
diff --git a/toolkit/components/urlformatter/api_keys.in b/toolkit/components/urlformatter/api_keys.in
new file mode 100644
index 0000000000..3fa48dd9a7
--- /dev/null
+++ b/toolkit/components/urlformatter/api_keys.in
@@ -0,0 +1,4 @@
+#define MOZ_MOZILLA_API_KEY @MOZ_MOZILLA_API_KEY@
+#define MOZ_GOOGLE_API_KEY @MOZ_GOOGLE_API_KEY@
+#define MOZ_BING_API_KEY @MOZ_BING_API_KEY@
+#define MOZ_BING_API_CLIENTID @MOZ_BING_API_CLIENTID@
diff --git a/toolkit/components/urlformatter/moz.build b/toolkit/components/urlformatter/moz.build
new file mode 100644
index 0000000000..1543fddda8
--- /dev/null
+++ b/toolkit/components/urlformatter/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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIURLFormatter.idl',
+]
+
+XPIDL_MODULE = 'urlformatter'
+
+EXTRA_COMPONENTS += [
+ 'nsURLFormatter.manifest',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'nsURLFormatter.js',
+]
+
+CONFIGURE_SUBST_FILES += [
+ 'api_keys',
+]
+
+DEFINES['OBJDIR'] = OBJDIR
diff --git a/toolkit/components/urlformatter/nsIURLFormatter.idl b/toolkit/components/urlformatter/nsIURLFormatter.idl
new file mode 100644
index 0000000000..a4a549d571
--- /dev/null
+++ b/toolkit/components/urlformatter/nsIURLFormatter.idl
@@ -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/. */
+
+/**
+ * nsIURLFormatter
+ *
+ * nsIURLFormatter exposes methods to substitute variables in URL formats.
+ * Variable names can contain 'A-Z' letters and '_' characters.
+ *
+ * Mozilla Applications linking to Mozilla websites are strongly encouraged to use
+ * URLs of the following format:
+ *
+ * http[s]://%SERVICE%.mozilla.[com|org]/%LOCALE%/
+ */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(4ab31d30-372d-11db-a98b-0800200c9a66)]
+interface nsIURLFormatter: nsISupports
+{
+ /**
+ * formatURL - Formats a string URL
+ *
+ * The set of known variables is predefined.
+ * If a variable is unknown, it is left unchanged and a non-fatal error is reported.
+ *
+ * @param aFormat string Unformatted URL.
+ *
+ * @return The formatted URL.
+ */
+ AString formatURL(in AString aFormat);
+
+ /**
+ * formatURLPref - Formats a string URL stored in a preference
+ *
+ * If the preference value cannot be retrieved, a fatal error is reported
+ * and the "about:blank" URL is returned.
+ *
+ * @param aPref string Preference name.
+ *
+ * @return The formatted URL returned by formatURL(), or "about:blank".
+ */
+ AString formatURLPref(in AString aPref);
+
+ /**
+ * Remove all of the sensitive query parameter strings from URLs in |aMsg|.
+ */
+ AString trimSensitiveURLs(in AString aMsg);
+};
diff --git a/toolkit/components/urlformatter/nsURLFormatter.js b/toolkit/components/urlformatter/nsURLFormatter.js
new file mode 100644
index 0000000000..970de05374
--- /dev/null
+++ b/toolkit/components/urlformatter/nsURLFormatter.js
@@ -0,0 +1,168 @@
+#filter substitution
+#include @OBJDIR@/api_keys
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /**
+ * @class nsURLFormatterService
+ *
+ * nsURLFormatterService exposes methods to substitute variables in URL formats.
+ *
+ * Mozilla Applications linking to Mozilla websites are strongly encouraged to use
+ * URLs of the following format:
+ *
+ * http[s]://%SERVICE%.mozilla.[com|org]/%LOCALE%/
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_APP_DISTRIBUTION = "distribution.id";
+const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+function nsURLFormatterService() {
+ XPCOMUtils.defineLazyGetter(this, "appInfo", function UFS_appInfo() {
+ return Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).
+ QueryInterface(Ci.nsIXULRuntime);
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "ABI", function UFS_ABI() {
+ let ABI = "default";
+ try {
+ ABI = this.appInfo.XPCOMABI;
+
+ if ("@mozilla.org/xpcom/mac-utils;1" in Cc) {
+ // Mac universal build should report a different ABI than either macppc
+ // or mactel.
+ let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"]
+ .getService(Ci.nsIMacUtils);
+ if (macutils && macutils.isUniversalBinary) {
+ ABI = "Universal-gcc3";
+ }
+ }
+ } catch (e) {}
+
+ return ABI;
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "OSVersion", function UFS_OSVersion() {
+ let OSVersion = "default";
+ let sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ try {
+ OSVersion = sysInfo.getProperty("name") + " " +
+ sysInfo.getProperty("version");
+ OSVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ } catch (e) {}
+
+ return encodeURIComponent(OSVersion);
+ });
+
+ XPCOMUtils.defineLazyGetter(this, "distribution", function UFS_distribution() {
+ let distribution = { id: "default", version: "default" };
+
+ let defaults = Services.prefs.getDefaultBranch(null);
+ try {
+ distribution.id = defaults.getCharPref(PREF_APP_DISTRIBUTION);
+ } catch (e) {}
+ try {
+ distribution.version = defaults.getCharPref(PREF_APP_DISTRIBUTION_VERSION);
+ } catch (e) {}
+
+ return distribution;
+ });
+}
+
+nsURLFormatterService.prototype = {
+ classID: Components.ID("{e6156350-2be8-11db-a98b-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIURLFormatter]),
+
+ _defaults: {
+ LOCALE: () => Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry).
+ getSelectedLocale('global'),
+ REGION: function() {
+ try {
+ // When the geoip lookup failed to identify the region, we fallback to
+ // the 'ZZ' region code to mean 'unknown'.
+ return Services.prefs.getCharPref("browser.search.region") || "ZZ";
+ } catch(e) {
+ return "ZZ";
+ }
+ },
+ VENDOR: function() { return this.appInfo.vendor; },
+ NAME: function() { return this.appInfo.name; },
+ ID: function() { return this.appInfo.ID; },
+ VERSION: function() { return this.appInfo.version; },
+ MAJOR_VERSION: function() { return this.appInfo.version.replace(/^([^\.]+\.[0-9]+[a-z]*).*/gi, "$1"); },
+ APPBUILDID: function() { return this.appInfo.appBuildID; },
+ PLATFORMVERSION: function() { return this.appInfo.platformVersion; },
+ PLATFORMBUILDID: function() { return this.appInfo.platformBuildID; },
+ APP: function() { return this.appInfo.name.toLowerCase().replace(/ /, ""); },
+ OS: function() { return this.appInfo.OS; },
+ XPCOMABI: function() { return this.ABI; },
+ BUILD_TARGET: function() { return this.appInfo.OS + "_" + this.ABI; },
+ OS_VERSION: function() { return this.OSVersion; },
+ CHANNEL: () => UpdateUtils.UpdateChannel,
+ MOZILLA_API_KEY: () => "@MOZ_MOZILLA_API_KEY@",
+ GOOGLE_API_KEY: () => "@MOZ_GOOGLE_API_KEY@",
+ BING_API_CLIENTID:() => "@MOZ_BING_API_CLIENTID@",
+ BING_API_KEY: () => "@MOZ_BING_API_KEY@",
+ DISTRIBUTION: function() { return this.distribution.id; },
+ DISTRIBUTION_VERSION: function() { return this.distribution.version; }
+ },
+
+ formatURL: function uf_formatURL(aFormat) {
+ var _this = this;
+ var replacementCallback = function(aMatch, aKey) {
+ if (aKey in _this._defaults) {
+ return _this._defaults[aKey].call(_this);
+ }
+ Cu.reportError("formatURL: Couldn't find value for key: " + aKey);
+ return aMatch;
+ }
+ return aFormat.replace(/%([A-Z_]+)%/g, replacementCallback);
+ },
+
+ formatURLPref: function uf_formatURLPref(aPref) {
+ var format = null;
+ var PS = Cc['@mozilla.org/preferences-service;1'].
+ getService(Ci.nsIPrefBranch);
+
+ try {
+ format = PS.getComplexValue(aPref, Ci.nsISupportsString).data;
+ } catch(ex) {
+ Cu.reportError("formatURLPref: Couldn't get pref: " + aPref);
+ return "about:blank";
+ }
+
+ if (!PS.prefHasUserValue(aPref) &&
+ /^(data:text\/plain,.+=.+|chrome:\/\/.+\/locale\/.+\.properties)$/.test(format)) {
+ // This looks as if it might be a localised preference
+ try {
+ format = PS.getComplexValue(aPref, Ci.nsIPrefLocalizedString).data;
+ } catch(ex) {}
+ }
+
+ return this.formatURL(format);
+ },
+
+ trimSensitiveURLs: function uf_trimSensitiveURLs(aMsg) {
+ // Only the google API key is sensitive for now.
+ return "@MOZ_GOOGLE_API_KEY@" ? aMsg.replace(/@MOZ_GOOGLE_API_KEY@/g,
+ "[trimmed-google-api-key]")
+ : aMsg;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsURLFormatterService]);
diff --git a/toolkit/components/urlformatter/nsURLFormatter.manifest b/toolkit/components/urlformatter/nsURLFormatter.manifest
new file mode 100644
index 0000000000..3f9123d735
--- /dev/null
+++ b/toolkit/components/urlformatter/nsURLFormatter.manifest
@@ -0,0 +1,2 @@
+component {e6156350-2be8-11db-a98b-0800200c9a66} nsURLFormatter.js
+contract @mozilla.org/toolkit/URLFormatterService;1 {e6156350-2be8-11db-a98b-0800200c9a66}
diff --git a/toolkit/components/urlformatter/tests/unit/.eslintrc.js b/toolkit/components/urlformatter/tests/unit/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/urlformatter/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/urlformatter/tests/unit/head_urlformatter.js b/toolkit/components/urlformatter/tests/unit/head_urlformatter.js
new file mode 100644
index 0000000000..8af2aaac4a
--- /dev/null
+++ b/toolkit/components/urlformatter/tests/unit/head_urlformatter.js
@@ -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/. */
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://testing-common/AppInfo.jsm", this);
+updateAppInfo({
+ name: "Url Formatter Test",
+ ID: "urlformattertest@test.mozilla.org",
+ version: "1",
+ platformVersion: "2.0",
+});
+var gAppInfo = getAppInfo();
diff --git a/toolkit/components/urlformatter/tests/unit/test_urlformatter.js b/toolkit/components/urlformatter/tests/unit/test_urlformatter.js
new file mode 100644
index 0000000000..393b0dc33b
--- /dev/null
+++ b/toolkit/components/urlformatter/tests/unit/test_urlformatter.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 run_test() {
+ var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].
+ getService(Ci.nsIURLFormatter);
+ var locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry).
+ getSelectedLocale('global');
+ var prefs = Cc['@mozilla.org/preferences-service;1'].
+ getService(Ci.nsIPrefBranch);
+ var sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ var OSVersion = sysInfo.getProperty("name") + " " +
+ sysInfo.getProperty("version");
+ try {
+ OSVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ } catch (e) {}
+ OSVersion = encodeURIComponent(OSVersion);
+ var macutils = null;
+ try {
+ macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
+ getService(Ci.nsIMacUtils);
+ } catch (e) {}
+ var appInfo = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).
+ QueryInterface(Ci.nsIXULRuntime);
+ var abi = macutils && macutils.isUniversalBinary ? "Universal-gcc3" : appInfo.XPCOMABI;
+
+ let channel = "default";
+ let defaults = prefs.QueryInterface(Ci.nsIPrefService).getDefaultBranch(null);
+ try {
+ channel = defaults.getCharPref("app.update.channel");
+ } catch (e) {}
+ // Set distribution values.
+ defaults.setCharPref("distribution.id", "bacon");
+ defaults.setCharPref("distribution.version", "1.0");
+
+ var upperUrlRaw = "http://%LOCALE%.%VENDOR%.foo/?name=%NAME%&id=%ID%&version=%VERSION%&platversion=%PLATFORMVERSION%&abid=%APPBUILDID%&pbid=%PLATFORMBUILDID%&app=%APP%&os=%OS%&abi=%XPCOMABI%";
+ var lowerUrlRaw = "http://%locale%.%vendor%.foo/?name=%name%&id=%id%&version=%version%&platversion=%platformversion%&abid=%appbuildid%&pbid=%platformbuildid%&app=%app%&os=%os%&abi=%xpcomabi%";
+ // XXX %APP%'s RegExp is not global, so it only replaces the first space
+ var ulUrlRef = "http://" + locale + ".Mozilla.foo/?name=Url Formatter Test&id=urlformattertest@test.mozilla.org&version=1&platversion=2.0&abid=" + gAppInfo.appBuildID + "&pbid=" + gAppInfo.platformBuildID + "&app=urlformatter test&os=XPCShell&abi=" + abi;
+ var multiUrl = "http://%VENDOR%.%VENDOR%.%NAME%.%VENDOR%.%NAME%";
+ var multiUrlRef = "http://Mozilla.Mozilla.Url Formatter Test.Mozilla.Url Formatter Test";
+ var encodedUrl = "https://%LOCALE%.%VENDOR%.foo/?q=%E3%82%BF%E3%83%96&app=%NAME%&ver=%PLATFORMVERSION%";
+ var encodedUrlRef = "https://" + locale + ".Mozilla.foo/?q=%E3%82%BF%E3%83%96&app=Url Formatter Test&ver=2.0";
+ var advancedUrl = "http://test.mozilla.com/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/";
+ var advancedUrlRef = "http://test.mozilla.com/Url Formatter Test/1/" + gAppInfo.appBuildID + "/XPCShell_" + abi + "/" + locale + "/" + channel + "/" + OSVersion + "/bacon/1.0/";
+
+ var pref = "xpcshell.urlformatter.test";
+ var str = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ str.data = upperUrlRaw;
+ prefs.setComplexValue(pref, Ci.nsISupportsString, str);
+
+ do_check_eq(formatter.formatURL(upperUrlRaw), ulUrlRef);
+ do_check_eq(formatter.formatURLPref(pref), ulUrlRef);
+ // Keys must be uppercase
+ do_check_neq(formatter.formatURL(lowerUrlRaw), ulUrlRef);
+ do_check_eq(formatter.formatURL(multiUrl), multiUrlRef);
+ // Encoded strings must be kept as is (Bug 427304)
+ do_check_eq(formatter.formatURL(encodedUrl), encodedUrlRef);
+
+ do_check_eq(formatter.formatURL(advancedUrl), advancedUrlRef);
+}
diff --git a/toolkit/components/urlformatter/tests/unit/xpcshell.ini b/toolkit/components/urlformatter/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..2f82beaa32
--- /dev/null
+++ b/toolkit/components/urlformatter/tests/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head_urlformatter.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_urlformatter.js]
diff --git a/toolkit/components/utils/moz.build b/toolkit/components/utils/moz.build
new file mode 100644
index 0000000000..3b8c1c5755
--- /dev/null
+++ b/toolkit/components/utils/moz.build
@@ -0,0 +1,10 @@
+# -*- 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 += [
+ 'simpleServices.js',
+ 'utils.manifest',
+]
diff --git a/toolkit/components/utils/simpleServices.js b/toolkit/components/utils/simpleServices.js
new file mode 100644
index 0000000000..0b8dfe877c
--- /dev/null
+++ b/toolkit/components/utils/simpleServices.js
@@ -0,0 +1,313 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Dumping ground for simple services for which the isolation of a full global
+ * is overkill. Be careful about namespace pollution, and be mindful about
+ * importing lots of JSMs in global scope, since this file will almost certainly
+ * be loaded from enough callsites that any such imports will always end up getting
+ * eagerly loaded at startup.
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+function AddonPolicyService()
+{
+ this.wrappedJSObject = this;
+ this.cspStrings = new Map();
+ this.backgroundPageUrlCallbacks = new Map();
+ this.checkHasPermissionCallbacks = new Map();
+ this.mayLoadURICallbacks = new Map();
+ this.localizeCallbacks = new Map();
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this, "baseCSP", "extensions.webextensions.base-content-security-policy",
+ "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " +
+ "object-src 'self' https://* moz-extension: blob: filesystem:;");
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this, "defaultCSP", "extensions.webextensions.default-content-security-policy",
+ "script-src 'self'; object-src 'self';");
+}
+
+AddonPolicyService.prototype = {
+ classID: Components.ID("{89560ed3-72e3-498d-a0e8-ffe50334d7c5}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonPolicyService]),
+
+ /**
+ * Returns the content security policy which applies to documents belonging
+ * to the extension with the given ID. This may be either a custom policy,
+ * if one was supplied, or the default policy if one was not.
+ */
+ getAddonCSP(aAddonId) {
+ let csp = this.cspStrings.get(aAddonId);
+ return csp || this.defaultCSP;
+ },
+
+ /**
+ * Returns the generated background page as a data-URI, if any. If the addon
+ * does not have an auto-generated background page, an empty string is
+ * returned.
+ */
+ getGeneratedBackgroundPageUrl(aAddonId) {
+ let cb = this.backgroundPageUrlCallbacks.get(aAddonId);
+ return cb && cb(aAddonId) || '';
+ },
+
+ /*
+ * Invokes a callback (if any) associated with the addon to determine whether
+ * the addon is granted the |aPerm| API permission.
+ *
+ * @see nsIAddonPolicyService.addonHasPermission
+ */
+ addonHasPermission(aAddonId, aPerm) {
+ let cb = this.checkHasPermissionCallbacks.get(aAddonId);
+ return cb ? cb(aPerm) : false;
+ },
+
+ /*
+ * Invokes a callback (if any) associated with the addon to determine whether
+ * unprivileged code running within the addon is allowed to perform loads from
+ * the given URI.
+ *
+ * @see nsIAddonPolicyService.addonMayLoadURI
+ */
+ addonMayLoadURI(aAddonId, aURI) {
+ let cb = this.mayLoadURICallbacks.get(aAddonId);
+ return cb ? cb(aURI) : false;
+ },
+
+ /*
+ * Invokes a callback (if any) associated with the addon to loclaize a
+ * resource belonging to that add-on.
+ */
+ localizeAddonString(aAddonId, aString) {
+ let cb = this.localizeCallbacks.get(aAddonId);
+ return cb ? cb(aString) : aString;
+ },
+
+ /*
+ * Invokes a callback (if any) to determine if an extension URI should be
+ * web-accessible.
+ *
+ * @see nsIAddonPolicyService.extensionURILoadableByAnyone
+ */
+ extensionURILoadableByAnyone(aURI) {
+ if (aURI.scheme != "moz-extension") {
+ throw new TypeError("non-extension URI passed");
+ }
+
+ let cb = this.extensionURILoadCallback;
+ return cb ? cb(aURI) : false;
+ },
+
+ /*
+ * Maps an extension URI to an addon ID.
+ *
+ * @see nsIAddonPolicyService.extensionURIToAddonId
+ */
+ extensionURIToAddonId(aURI) {
+ if (aURI.scheme != "moz-extension") {
+ throw new TypeError("non-extension URI passed");
+ }
+
+ let cb = this.extensionURIToAddonIdCallback;
+ if (!cb) {
+ throw new Error("no callback set to map extension URIs to addon Ids");
+ }
+ return cb(aURI);
+ },
+
+ /*
+ * Sets the callbacks used in addonHasPermission above. Not accessible over
+ * XPCOM - callers should use .wrappedJSObject on the service to call it
+ * directly.
+ */
+ setAddonHasPermissionCallback(aAddonId, aCallback) {
+ if (aCallback) {
+ this.checkHasPermissionCallbacks.set(aAddonId, aCallback);
+ } else {
+ this.checkHasPermissionCallbacks.delete(aAddonId);
+ }
+ },
+
+ /*
+ * Sets the callbacks used in addonMayLoadURI above. Not accessible over
+ * XPCOM - callers should use .wrappedJSObject on the service to call it
+ * directly.
+ */
+ setAddonLoadURICallback(aAddonId, aCallback) {
+ if (aCallback) {
+ this.mayLoadURICallbacks.set(aAddonId, aCallback);
+ } else {
+ this.mayLoadURICallbacks.delete(aAddonId);
+ }
+ },
+
+ /*
+ * Sets the custom CSP string to be used for the add-on. Not accessible over
+ * XPCOM - callers should use .wrappedJSObject on the service to call it
+ * directly.
+ */
+ setAddonCSP(aAddonId, aCSPString) {
+ if (aCSPString) {
+ this.cspStrings.set(aAddonId, aCSPString);
+ } else {
+ this.cspStrings.delete(aAddonId);
+ }
+ },
+
+ /**
+ * Set the callback that generates a data-URL for the background page.
+ */
+ setBackgroundPageUrlCallback(aAddonId, aCallback) {
+ if (aCallback) {
+ this.backgroundPageUrlCallbacks.set(aAddonId, aCallback);
+ } else {
+ this.backgroundPageUrlCallbacks.delete(aAddonId);
+ }
+ },
+
+ /*
+ * Sets the callbacks used by the stream converter service to localize
+ * add-on resources.
+ */
+ setAddonLocalizeCallback(aAddonId, aCallback) {
+ if (aCallback) {
+ this.localizeCallbacks.set(aAddonId, aCallback);
+ } else {
+ this.localizeCallbacks.delete(aAddonId);
+ }
+ },
+
+ /*
+ * Sets the callback used in extensionURILoadableByAnyone above. Not
+ * accessible over XPCOM - callers should use .wrappedJSObject on the
+ * service to call it directly.
+ */
+ setExtensionURILoadCallback(aCallback) {
+ var old = this.extensionURILoadCallback;
+ this.extensionURILoadCallback = aCallback;
+ return old;
+ },
+
+ /*
+ * Sets the callback used in extensionURIToAddonId above. Not accessible over
+ * XPCOM - callers should use .wrappedJSObject on the service to call it
+ * directly.
+ */
+ setExtensionURIToAddonIdCallback(aCallback) {
+ var old = this.extensionURIToAddonIdCallback;
+ this.extensionURIToAddonIdCallback = aCallback;
+ return old;
+ }
+};
+
+/*
+ * This class provides a stream filter for locale messages in CSS files served
+ * by the moz-extension: protocol handler.
+ *
+ * See SubstituteChannel in netwerk/protocol/res/ExtensionProtocolHandler.cpp
+ * for usage.
+ */
+function AddonLocalizationConverter()
+{
+ this.aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService)
+ .wrappedJSObject;
+}
+
+AddonLocalizationConverter.prototype = {
+ classID: Components.ID("{ded150e3-c92e-4077-a396-0dba9953e39f}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamConverter]),
+
+ FROM_TYPE: "application/vnd.mozilla.webext.unlocalized",
+ TO_TYPE: "text/css",
+
+ checkTypes(aFromType, aToType) {
+ if (aFromType != this.FROM_TYPE) {
+ throw Components.Exception("Invalid aFromType value", Cr.NS_ERROR_INVALID_ARG,
+ Components.stack.caller.caller);
+ }
+ if (aToType != this.TO_TYPE) {
+ throw Components.Exception("Invalid aToType value", Cr.NS_ERROR_INVALID_ARG,
+ Components.stack.caller.caller);
+ }
+ },
+
+ // aContext must be a nsIURI object for a valid moz-extension: URL.
+ getAddonId(aContext) {
+ // In this case, we want the add-on ID even if the URL is web accessible,
+ // so check the root rather than the exact path.
+ let uri = Services.io.newURI("/", null, aContext);
+
+ let id = this.aps.extensionURIToAddonId(uri);
+ if (id == undefined) {
+ throw new Components.Exception("Invalid context", Cr.NS_ERROR_INVALID_ARG);
+ }
+ return id;
+ },
+
+ convertToStream(aAddonId, aString) {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+
+ stream.data = this.aps.localizeAddonString(aAddonId, aString);
+ return stream;
+ },
+
+ convert(aStream, aFromType, aToType, aContext) {
+ this.checkTypes(aFromType, aToType);
+ let addonId = this.getAddonId(aContext);
+
+ let string = (
+ aStream.available() ?
+ NetUtil.readInputStreamToString(aStream, aStream.available()): ""
+ );
+ return this.convertToStream(addonId, string);
+ },
+
+ asyncConvertData(aFromType, aToType, aListener, aContext) {
+ this.checkTypes(aFromType, aToType);
+ this.addonId = this.getAddonId(aContext);
+ this.listener = aListener;
+ },
+
+ onStartRequest(aRequest, aContext) {
+ this.parts = [];
+ },
+
+ onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
+ this.parts.push(NetUtil.readInputStreamToString(aInputStream, aCount));
+ },
+
+ onStopRequest(aRequest, aContext, aStatusCode) {
+ try {
+ this.listener.onStartRequest(aRequest, null);
+ if (Components.isSuccessCode(aStatusCode)) {
+ let string = this.parts.join("");
+ let stream = this.convertToStream(this.addonId, string);
+
+ this.listener.onDataAvailable(aRequest, null, stream, 0, stream.data.length);
+ }
+ } catch (e) {
+ aStatusCode = e.result || Cr.NS_ERROR_FAILURE;
+ }
+ this.listener.onStopRequest(aRequest, null, aStatusCode);
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonPolicyService,
+ AddonLocalizationConverter]);
diff --git a/toolkit/components/utils/utils.manifest b/toolkit/components/utils/utils.manifest
new file mode 100644
index 0000000000..b2d30bd98d
--- /dev/null
+++ b/toolkit/components/utils/utils.manifest
@@ -0,0 +1,6 @@
+component {dfd07380-6083-11e4-9803-0800200c9a66} simpleServices.js
+contract @mozilla.org/addons/remote-tag-service;1 {dfd07380-6083-11e4-9803-0800200c9a66}
+component {89560ed3-72e3-498d-a0e8-ffe50334d7c5} simpleServices.js
+contract @mozilla.org/addons/policy-service;1 {89560ed3-72e3-498d-a0e8-ffe50334d7c5}
+component {ded150e3-c92e-4077-a396-0dba9953e39f} simpleServices.js
+contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.webext.unlocalized&to=text/css {ded150e3-c92e-4077-a396-0dba9953e39f}
diff --git a/toolkit/components/viewconfig/content/config.js b/toolkit/components/viewconfig/content/config.js
new file mode 100644
index 0000000000..562962894e
--- /dev/null
+++ b/toolkit/components/viewconfig/content/config.js
@@ -0,0 +1,635 @@
+// -*- 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/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const nsIPrefLocalizedString = Components.interfaces.nsIPrefLocalizedString;
+const nsISupportsString = Components.interfaces.nsISupportsString;
+const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+const nsIClipboardHelper = Components.interfaces.nsIClipboardHelper;
+
+const nsSupportsString_CONTRACTID = "@mozilla.org/supports-string;1";
+const nsPrompt_CONTRACTID = "@mozilla.org/embedcomp/prompt-service;1";
+const nsPrefService_CONTRACTID = "@mozilla.org/preferences-service;1";
+const nsClipboardHelper_CONTRACTID = "@mozilla.org/widget/clipboardhelper;1";
+const nsAtomService_CONTRACTID = "@mozilla.org/atom-service;1";
+
+const gPrefBranch = Services.prefs;
+const gClipboardHelper = Components.classes[nsClipboardHelper_CONTRACTID].getService(nsIClipboardHelper);
+
+var gLockProps = ["default", "user", "locked"];
+// we get these from a string bundle
+var gLockStrs = [];
+var gTypeStrs = [];
+
+const PREF_IS_DEFAULT_VALUE = 0;
+const PREF_IS_USER_SET = 1;
+const PREF_IS_LOCKED = 2;
+
+var gPrefHash = {};
+var gPrefArray = [];
+var gPrefView = gPrefArray; // share the JS array
+var gSortedColumn = "prefCol";
+var gSortFunction = null;
+var gSortDirection = 1; // 1 is ascending; -1 is descending
+var gConfigBundle = null;
+var gFilter = null;
+
+var view = {
+ get rowCount() { return gPrefView.length; },
+ getCellText : function(index, col) {
+ if (!(index in gPrefView))
+ return "";
+
+ var value = gPrefView[index][col.id];
+
+ switch (col.id) {
+ case "lockCol":
+ return gLockStrs[value];
+ case "typeCol":
+ return gTypeStrs[value];
+ default:
+ return value;
+ }
+ },
+ getRowProperties : function(index) { return ""; },
+ getCellProperties : function(index, col) {
+ if (index in gPrefView)
+ return gLockProps[gPrefView[index].lockCol];
+
+ return "";
+ },
+ getColumnProperties : function(col) { return ""; },
+ treebox : null,
+ selection : null,
+ isContainer : function(index) { return false; },
+ isContainerOpen : function(index) { return false; },
+ isContainerEmpty : function(index) { return false; },
+ isSorted : function() { return true; },
+ canDrop : function(index, orientation) { return false; },
+ drop : function(row, orientation) {},
+ setTree : function(out) { this.treebox = out; },
+ getParentIndex: function(rowIndex) { return -1; },
+ hasNextSibling: function(rowIndex, afterIndex) { return false; },
+ getLevel: function(index) { return 1; },
+ getImageSrc: function(row, col) { return ""; },
+ toggleOpenState : function(index) {},
+ cycleHeader: function(col) {
+ var index = this.selection.currentIndex;
+ if (col.id == gSortedColumn) {
+ gSortDirection = -gSortDirection;
+ gPrefArray.reverse();
+ if (gPrefView != gPrefArray)
+ gPrefView.reverse();
+ if (index >= 0)
+ index = gPrefView.length - index - 1;
+ }
+ else {
+ var pref = null;
+ if (index >= 0)
+ pref = gPrefView[index];
+
+ var old = document.getElementById(gSortedColumn);
+ old.removeAttribute("sortDirection");
+ gPrefArray.sort(gSortFunction = gSortFunctions[col.id]);
+ if (gPrefView != gPrefArray)
+ gPrefView.sort(gSortFunction);
+ gSortedColumn = col.id;
+ if (pref)
+ index = getViewIndexOfPref(pref);
+ }
+ col.element.setAttribute("sortDirection", gSortDirection > 0 ? "ascending" : "descending");
+ this.treebox.invalidate();
+ if (index >= 0) {
+ this.selection.select(index);
+ this.treebox.ensureRowIsVisible(index);
+ }
+ },
+ 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) {},
+ performAction: function(action) {},
+ performActionOnRow: function(action, row) {},
+ performActionOnCell: function(action, row, col) {},
+ isSeparator: function(index) { return false; }
+};
+
+// find the index in gPrefView of a pref object
+// or -1 if it does not exist in the filtered view
+function getViewIndexOfPref(pref)
+{
+ var low = -1, high = gPrefView.length;
+ var index = (low + high) >> 1;
+ while (index > low) {
+ var mid = gPrefView[index];
+ if (mid == pref)
+ return index;
+ if (gSortFunction(mid, pref) < 0)
+ low = index;
+ else
+ high = index;
+ index = (low + high) >> 1;
+ }
+ return -1;
+}
+
+// find the index in gPrefView where a pref object belongs
+function getNearestViewIndexOfPref(pref)
+{
+ var low = -1, high = gPrefView.length;
+ var index = (low + high) >> 1;
+ while (index > low) {
+ if (gSortFunction(gPrefView[index], pref) < 0)
+ low = index;
+ else
+ high = index;
+ index = (low + high) >> 1;
+ }
+ return high;
+}
+
+// find the index in gPrefArray of a pref object
+function getIndexOfPref(pref)
+{
+ var low = -1, high = gPrefArray.length;
+ var index = (low + high) >> 1;
+ while (index > low) {
+ var mid = gPrefArray[index];
+ if (mid == pref)
+ return index;
+ if (gSortFunction(mid, pref) < 0)
+ low = index;
+ else
+ high = index;
+ index = (low + high) >> 1;
+ }
+ return index;
+}
+
+function getNearestIndexOfPref(pref)
+{
+ var low = -1, high = gPrefArray.length;
+ var index = (low + high) >> 1;
+ while (index > low) {
+ if (gSortFunction(gPrefArray[index], pref) < 0)
+ low = index;
+ else
+ high = index;
+ index = (low + high) >> 1;
+ }
+ return high;
+}
+
+var gPrefListener =
+{
+ observe: function(subject, topic, prefName)
+ {
+ if (topic != "nsPref:changed")
+ return;
+
+ var arrayIndex = gPrefArray.length;
+ var viewIndex = arrayIndex;
+ var selectedIndex = view.selection.currentIndex;
+ var pref;
+ var updateView = false;
+ var updateArray = false;
+ var addedRow = false;
+ if (prefName in gPrefHash) {
+ pref = gPrefHash[prefName];
+ viewIndex = getViewIndexOfPref(pref);
+ arrayIndex = getIndexOfPref(pref);
+ fetchPref(prefName, arrayIndex);
+ // fetchPref replaces the existing pref object
+ pref = gPrefHash[prefName];
+ if (viewIndex >= 0) {
+ // Might need to update the filtered view
+ gPrefView[viewIndex] = gPrefHash[prefName];
+ view.treebox.invalidateRow(viewIndex);
+ }
+ if (gSortedColumn == "lockCol" || gSortedColumn == "valueCol") {
+ updateArray = true;
+ gPrefArray.splice(arrayIndex, 1);
+ if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) {
+ updateView = true;
+ gPrefView.splice(viewIndex, 1);
+ }
+ }
+ }
+ else {
+ fetchPref(prefName, arrayIndex);
+ pref = gPrefArray.pop();
+ updateArray = true;
+ addedRow = true;
+ if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) {
+ updateView = true;
+ }
+ }
+ if (updateArray) {
+ // Reinsert in the data array
+ var newIndex = getNearestIndexOfPref(pref);
+ gPrefArray.splice(newIndex, 0, pref);
+
+ if (updateView) {
+ // View is filtered, reinsert in the view separately
+ newIndex = getNearestViewIndexOfPref(pref);
+ gPrefView.splice(newIndex, 0, pref);
+ }
+ else if (gFilter) {
+ // View is filtered, but nothing to update
+ return;
+ }
+
+ if (addedRow)
+ view.treebox.rowCountChanged(newIndex, 1);
+
+ // Invalidate the changed range in the view
+ var low = Math.min(viewIndex, newIndex);
+ var high = Math.max(viewIndex, newIndex);
+ view.treebox.invalidateRange(low, high);
+
+ if (selectedIndex == viewIndex) {
+ selectedIndex = newIndex;
+ }
+ else if (selectedIndex >= low && selectedIndex <= high) {
+ selectedIndex += (newIndex > viewIndex) ? -1 : 1;
+ }
+ if (selectedIndex >= 0) {
+ view.selection.select(selectedIndex);
+ if (selectedIndex == newIndex)
+ view.treebox.ensureRowIsVisible(selectedIndex);
+ }
+ }
+ }
+};
+
+function prefObject(prefName, prefIndex)
+{
+ this.prefCol = prefName;
+}
+
+prefObject.prototype =
+{
+ lockCol: PREF_IS_DEFAULT_VALUE,
+ typeCol: nsIPrefBranch.PREF_STRING,
+ valueCol: ""
+};
+
+function fetchPref(prefName, prefIndex)
+{
+ var pref = new prefObject(prefName);
+
+ gPrefHash[prefName] = pref;
+ gPrefArray[prefIndex] = pref;
+
+ if (gPrefBranch.prefIsLocked(prefName))
+ pref.lockCol = PREF_IS_LOCKED;
+ else if (gPrefBranch.prefHasUserValue(prefName))
+ pref.lockCol = PREF_IS_USER_SET;
+
+ try {
+ switch (gPrefBranch.getPrefType(prefName)) {
+ case gPrefBranch.PREF_BOOL:
+ pref.typeCol = gPrefBranch.PREF_BOOL;
+ // convert to a string
+ pref.valueCol = gPrefBranch.getBoolPref(prefName).toString();
+ break;
+ case gPrefBranch.PREF_INT:
+ pref.typeCol = gPrefBranch.PREF_INT;
+ // convert to a string
+ pref.valueCol = gPrefBranch.getIntPref(prefName).toString();
+ break;
+ default:
+ case gPrefBranch.PREF_STRING:
+ pref.valueCol = gPrefBranch.getComplexValue(prefName, nsISupportsString).data;
+ // Try in case it's a localized string (will throw an exception if not)
+ if (pref.lockCol == PREF_IS_DEFAULT_VALUE &&
+ /^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.valueCol))
+ pref.valueCol = gPrefBranch.getComplexValue(prefName, nsIPrefLocalizedString).data;
+ break;
+ }
+ } catch (e) {
+ // Also catch obscure cases in which you can't tell in advance
+ // that the pref exists but has no user or default value...
+ }
+}
+
+function onConfigLoad()
+{
+ // Load strings
+ gConfigBundle = document.getElementById("configBundle");
+
+ gLockStrs[PREF_IS_DEFAULT_VALUE] = gConfigBundle.getString("default");
+ gLockStrs[PREF_IS_USER_SET] = gConfigBundle.getString("user");
+ gLockStrs[PREF_IS_LOCKED] = gConfigBundle.getString("locked");
+
+ gTypeStrs[nsIPrefBranch.PREF_STRING] = gConfigBundle.getString("string");
+ gTypeStrs[nsIPrefBranch.PREF_INT] = gConfigBundle.getString("int");
+ gTypeStrs[nsIPrefBranch.PREF_BOOL] = gConfigBundle.getString("bool");
+
+ var showWarning = gPrefBranch.getBoolPref("general.warnOnAboutConfig");
+
+ if (showWarning)
+ document.getElementById("warningButton").focus();
+ else
+ ShowPrefs();
+}
+
+// Unhide the warning message
+function ShowPrefs()
+{
+ gPrefBranch.getChildList("").forEach(fetchPref);
+
+ var descending = document.getElementsByAttribute("sortDirection", "descending");
+ if (descending.item(0)) {
+ gSortedColumn = descending[0].id;
+ gSortDirection = -1;
+ }
+ else {
+ var ascending = document.getElementsByAttribute("sortDirection", "ascending");
+ if (ascending.item(0))
+ gSortedColumn = ascending[0].id;
+ else
+ document.getElementById(gSortedColumn).setAttribute("sortDirection", "ascending");
+ }
+ gSortFunction = gSortFunctions[gSortedColumn];
+ gPrefArray.sort(gSortFunction);
+
+ gPrefBranch.addObserver("", gPrefListener, false);
+
+ var configTree = document.getElementById("configTree");
+ configTree.view = view;
+ configTree.controllers.insertControllerAt(0, configController);
+
+ document.getElementById("configDeck").setAttribute("selectedIndex", 1);
+ document.getElementById("configTreeKeyset").removeAttribute("disabled");
+ if (!document.getElementById("showWarningNextTime").checked)
+ gPrefBranch.setBoolPref("general.warnOnAboutConfig", false);
+
+ // Process about:config?filter=<string>
+ var textbox = document.getElementById("textbox");
+ // About URIs don't support query params, so do this manually
+ var loc = document.location.href;
+ var matches = /[?&]filter\=([^&]+)/i.exec(loc);
+ if (matches)
+ textbox.value = decodeURIComponent(matches[1]);
+
+ // Even if we did not set the filter string via the URL query,
+ // textbox might have been set via some other mechanism
+ if (textbox.value)
+ FilterPrefs();
+ textbox.focus();
+}
+
+function onConfigUnload()
+{
+ if (document.getElementById("configDeck").getAttribute("selectedIndex") == 1) {
+ gPrefBranch.removeObserver("", gPrefListener);
+ var configTree = document.getElementById("configTree");
+ configTree.view = null;
+ configTree.controllers.removeController(configController);
+ }
+}
+
+function FilterPrefs()
+{
+ if (document.getElementById("configDeck").getAttribute("selectedIndex") != 1) {
+ return;
+ }
+
+ var substring = document.getElementById("textbox").value;
+ // Check for "/regex/[i]"
+ if (substring.charAt(0) == '/') {
+ var r = substring.match(/^\/(.*)\/(i?)$/);
+ try {
+ gFilter = RegExp(r[1], r[2]);
+ }
+ catch (e) {
+ return; // Do nothing on incomplete or bad RegExp
+ }
+ }
+ else if (substring) {
+ gFilter = RegExp(substring.replace(/([^* \w])/g, "\\$1")
+ .replace(/^\*+/, "").replace(/\*+/g, ".*"), "i");
+ } else {
+ gFilter = null;
+ }
+
+ var prefCol = (view.selection && view.selection.currentIndex < 0) ?
+ null : gPrefView[view.selection.currentIndex].prefCol;
+ var oldlen = gPrefView.length;
+ gPrefView = gPrefArray;
+ if (gFilter) {
+ gPrefView = [];
+ for (var i = 0; i < gPrefArray.length; ++i)
+ if (gFilter.test(gPrefArray[i].prefCol + ";" + gPrefArray[i].valueCol))
+ gPrefView.push(gPrefArray[i]);
+ }
+ view.treebox.invalidate();
+ view.treebox.rowCountChanged(oldlen, gPrefView.length - oldlen);
+ gotoPref(prefCol);
+}
+
+function prefColSortFunction(x, y)
+{
+ if (x.prefCol > y.prefCol)
+ return gSortDirection;
+ if (x.prefCol < y.prefCol)
+ return -gSortDirection;
+ return 0;
+}
+
+function lockColSortFunction(x, y)
+{
+ if (x.lockCol != y.lockCol)
+ return gSortDirection * (y.lockCol - x.lockCol);
+ return prefColSortFunction(x, y);
+}
+
+function typeColSortFunction(x, y)
+{
+ if (x.typeCol != y.typeCol)
+ return gSortDirection * (y.typeCol - x.typeCol);
+ return prefColSortFunction(x, y);
+}
+
+function valueColSortFunction(x, y)
+{
+ if (x.valueCol > y.valueCol)
+ return gSortDirection;
+ if (x.valueCol < y.valueCol)
+ return -gSortDirection;
+ return prefColSortFunction(x, y);
+}
+
+const gSortFunctions =
+{
+ prefCol: prefColSortFunction,
+ lockCol: lockColSortFunction,
+ typeCol: typeColSortFunction,
+ valueCol: valueColSortFunction
+};
+
+const configController = {
+ supportsCommand: function supportsCommand(command) {
+ return command == "cmd_copy";
+ },
+ isCommandEnabled: function isCommandEnabled(command) {
+ return view.selection && view.selection.currentIndex >= 0;
+ },
+ doCommand: function doCommand(command) {
+ copyPref();
+ },
+ onEvent: function onEvent(event) {
+ }
+}
+
+function updateContextMenu()
+{
+ var lockCol = PREF_IS_LOCKED;
+ var typeCol = nsIPrefBranch.PREF_STRING;
+ var valueCol = "";
+ var copyDisabled = true;
+ var prefSelected = view.selection.currentIndex >= 0;
+
+ if (prefSelected) {
+ var prefRow = gPrefView[view.selection.currentIndex];
+ lockCol = prefRow.lockCol;
+ typeCol = prefRow.typeCol;
+ valueCol = prefRow.valueCol;
+ copyDisabled = false;
+ }
+
+ var copyPref = document.getElementById("copyPref");
+ copyPref.setAttribute("disabled", copyDisabled);
+
+ var copyName = document.getElementById("copyName");
+ copyName.setAttribute("disabled", copyDisabled);
+
+ var copyValue = document.getElementById("copyValue");
+ copyValue.setAttribute("disabled", copyDisabled);
+
+ var resetSelected = document.getElementById("resetSelected");
+ resetSelected.setAttribute("disabled", lockCol != PREF_IS_USER_SET);
+
+ var canToggle = typeCol == nsIPrefBranch.PREF_BOOL && valueCol != "";
+ // indicates that a pref is locked or no pref is selected at all
+ var isLocked = lockCol == PREF_IS_LOCKED;
+
+ var modifySelected = document.getElementById("modifySelected");
+ modifySelected.setAttribute("disabled", isLocked);
+ modifySelected.hidden = canToggle;
+
+ var toggleSelected = document.getElementById("toggleSelected");
+ toggleSelected.setAttribute("disabled", isLocked);
+ toggleSelected.hidden = !canToggle;
+}
+
+function copyPref()
+{
+ var pref = gPrefView[view.selection.currentIndex];
+ gClipboardHelper.copyString(pref.prefCol + ';' + pref.valueCol);
+}
+
+function copyName()
+{
+ gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].prefCol);
+}
+
+function copyValue()
+{
+ gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].valueCol);
+}
+
+function ModifySelected()
+{
+ if (view.selection.currentIndex >= 0)
+ ModifyPref(gPrefView[view.selection.currentIndex]);
+}
+
+function ResetSelected()
+{
+ var entry = gPrefView[view.selection.currentIndex];
+ gPrefBranch.clearUserPref(entry.prefCol);
+}
+
+function NewPref(type)
+{
+ var result = { value: "" };
+ var dummy = { value: 0 };
+ if (Services.prompt.prompt(window,
+ gConfigBundle.getFormattedString("new_title",
+ [gTypeStrs[type]]),
+ gConfigBundle.getString("new_prompt"),
+ result,
+ null,
+ dummy)) {
+ result.value = result.value.trim();
+ if (!result.value) {
+ return;
+ }
+
+ var pref;
+ if (result.value in gPrefHash)
+ pref = gPrefHash[result.value];
+ else
+ pref = { prefCol: result.value, lockCol: PREF_IS_DEFAULT_VALUE, typeCol: type, valueCol: "" };
+ if (ModifyPref(pref))
+ setTimeout(gotoPref, 0, result.value);
+ }
+}
+
+function gotoPref(pref)
+{
+ // make sure the pref exists and is displayed in the current view
+ var index = pref in gPrefHash ? getViewIndexOfPref(gPrefHash[pref]) : -1;
+ if (index >= 0) {
+ view.selection.select(index);
+ view.treebox.ensureRowIsVisible(index);
+ } else {
+ view.selection.clearSelection();
+ view.selection.currentIndex = -1;
+ }
+}
+
+function ModifyPref(entry)
+{
+ if (entry.lockCol == PREF_IS_LOCKED)
+ return false;
+ var title = gConfigBundle.getFormattedString("modify_title", [gTypeStrs[entry.typeCol]]);
+ if (entry.typeCol == nsIPrefBranch.PREF_BOOL) {
+ var check = { value: entry.valueCol == "false" };
+ if (!entry.valueCol && !Services.prompt.select(window, title, entry.prefCol, 2, [false, true], check))
+ return false;
+ gPrefBranch.setBoolPref(entry.prefCol, check.value);
+ } else {
+ var result = { value: entry.valueCol };
+ var dummy = { value: 0 };
+ if (!Services.prompt.prompt(window, title, entry.prefCol, result, null, dummy))
+ return false;
+ if (entry.typeCol == nsIPrefBranch.PREF_INT) {
+ // | 0 converts to integer or 0; - 0 to float or NaN.
+ // Thus, this check should catch all cases.
+ var val = result.value | 0;
+ if (val != result.value - 0) {
+ var err_title = gConfigBundle.getString("nan_title");
+ var err_text = gConfigBundle.getString("nan_text");
+ Services.prompt.alert(window, err_title, err_text);
+ return false;
+ }
+ gPrefBranch.setIntPref(entry.prefCol, val);
+ } else {
+ var supportsString = Components.classes[nsSupportsString_CONTRACTID].createInstance(nsISupportsString);
+ supportsString.data = result.value;
+ gPrefBranch.setComplexValue(entry.prefCol, nsISupportsString, supportsString);
+ }
+ }
+
+ Services.prefs.savePrefFile(null);
+ return true;
+}
diff --git a/toolkit/components/viewconfig/content/config.xul b/toolkit/components/viewconfig/content/config.xul
new file mode 100644
index 0000000000..25ce39da5f
--- /dev/null
+++ b/toolkit/components/viewconfig/content/config.xul
@@ -0,0 +1,101 @@
+<?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/in-content/info-pages.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/config.css" type="text/css"?>
+
+<!DOCTYPE window SYSTEM "chrome://global/locale/config.dtd">
+
+<window id="config"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&window.title;"
+ windowtype="Preferences:ConfigManager"
+ role="application"
+ aria-describedby="warningTitle warningText"
+ width="750"
+ height="500"
+ disablefastfind="true"
+ onunload="onConfigUnload();"
+ onload="onConfigLoad();">
+
+<script src="chrome://global/content/config.js"/>
+
+<stringbundle id="configBundle" src="chrome://global/locale/config.properties"/>
+
+<menupopup id="configContext" onpopupshowing="if (event.target == this) updateContextMenu();">
+ <menuitem id="toggleSelected" default="true"
+ label="&toggle.label;" accesskey="&toggle.accesskey;"
+ oncommand="ModifySelected();"/>
+ <menuitem id="modifySelected" default="true"
+ label="&modify.label;" accesskey="&modify.accesskey;"
+ oncommand="ModifySelected();"/>
+ <menuseparator/>
+ <menuitem id="copyPref" label="&copyPref.label;" accesskey="&copyPref.accesskey;" oncommand="copyPref();"/>
+ <menuitem id="copyName" label="&copyName.label;" accesskey="&copyName.accesskey;" oncommand="copyName();"/>
+ <menuitem id="copyValue" label="&copyValue.label;" accesskey="&copyValue.accesskey;" oncommand="copyValue();"/>
+ <menu label="&new.label;" accesskey="&new.accesskey;">
+ <menupopup>
+ <menuitem label="&string.label;" accesskey="&string.accesskey;" oncommand="NewPref(nsIPrefBranch.PREF_STRING);"/>
+ <menuitem label="&integer.label;" accesskey="&integer.accesskey;" oncommand="NewPref(nsIPrefBranch.PREF_INT);"/>
+ <menuitem label="&boolean.label;" accesskey="&boolean.accesskey;" oncommand="NewPref(nsIPrefBranch.PREF_BOOL);"/>
+ </menupopup>
+ </menu>
+ <menuitem id="resetSelected" label="&reset.label;" accesskey="&reset.accesskey;" oncommand="ResetSelected();"/>
+</menupopup>
+
+<keyset id="configTreeKeyset" disabled="true">
+ <key keycode="VK_RETURN" oncommand="ModifySelected();"/>
+ <key key="&focusSearch.key;" modifiers="accel" oncommand="document.getElementById('textbox').focus();"/>
+ <key key="&focusSearch2.key;" modifiers="accel" oncommand="document.getElementById('textbox').focus();"/>
+</keyset>
+<deck id="configDeck" flex="1">
+ <vbox id="warningScreen" flex="1" align="center" style="overflow: auto;">
+ <spacer flex="1"/>
+ <vbox id="warningBox" class="container">
+ <hbox class="title" flex="1">
+ <label id="warningTitle" class="title-text" flex="1">&aboutWarningTitle.label;</label>
+ </hbox>
+ <vbox class="description" flex="1">
+ <label id="warningText">&aboutWarningText.label;</label>
+ <checkbox id="showWarningNextTime" label="&aboutWarningCheckbox.label;" checked="true"/>
+ <hbox class="button-container">
+ <button id="warningButton" class="primary" oncommand="ShowPrefs();" label="&aboutWarningButton2.label;"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ <spacer flex="2"/>
+ </vbox>
+ <vbox flex="1">
+ <hbox id="filterRow" align="center">
+ <label value="&searchPrefs.label;" accesskey="&searchPrefs.accesskey;" control="textbox"/>
+ <textbox id="textbox" flex="1" type="search" class="compact"
+ aria-controls="configTree"
+ oncommand="FilterPrefs();"/>
+ </hbox>
+
+ <tree id="configTree" flex="1" seltype="single"
+ onselect="updateCommands('select');"
+ enableColumnDrag="true" context="configContext">
+ <treecols>
+ <treecol id="prefCol" label="&prefColumn.label;" flex="7"
+ ignoreincolumnpicker="true"
+ persist="hidden width ordinal sortDirection"/>
+ <splitter class="tree-splitter" />
+ <treecol id="lockCol" label="&lockColumn.label;" flex="1"
+ persist="hidden width ordinal sortDirection"/>
+ <splitter class="tree-splitter" />
+ <treecol id="typeCol" label="&typeColumn.label;" flex="1"
+ persist="hidden width ordinal sortDirection"/>
+ <splitter class="tree-splitter" />
+ <treecol id="valueCol" label="&valueColumn.label;" flex="10"
+ persist="hidden width ordinal sortDirection"/>
+ </treecols>
+
+ <treechildren id="configTreeBody" ondblclick="if (event.button == 0) ModifySelected();"/>
+ </tree>
+ </vbox>
+</deck>
+</window>
diff --git a/toolkit/components/viewconfig/jar.mn b/toolkit/components/viewconfig/jar.mn
new file mode 100644
index 0000000000..372902d57c
--- /dev/null
+++ b/toolkit/components/viewconfig/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/.
+
+toolkit.jar:
+ content/global/config.xul (content/config.xul)
+ content/global/config.js (content/config.js)
diff --git a/toolkit/components/viewconfig/moz.build b/toolkit/components/viewconfig/moz.build
new file mode 100644
index 0000000000..eb4454d28f
--- /dev/null
+++ b/toolkit/components/viewconfig/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'] \ No newline at end of file
diff --git a/toolkit/components/viewsource/ViewSourceBrowser.jsm b/toolkit/components/viewsource/ViewSourceBrowser.jsm
new file mode 100644
index 0000000000..0623e244a9
--- /dev/null
+++ b/toolkit/components/viewsource/ViewSourceBrowser.jsm
@@ -0,0 +1,331 @@
+// -*- 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 { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
+
+const FRAME_SCRIPT = "chrome://global/content/viewSource-content.js";
+
+this.EXPORTED_SYMBOLS = ["ViewSourceBrowser"];
+
+// Keep a set of browsers we've seen before, so we can load our frame script as
+// needed into any new ones.
+var gKnownBrowsers = new WeakSet();
+
+/**
+ * ViewSourceBrowser manages the view source <browser> from the chrome side.
+ * It's companion frame script, viewSource-content.js, needs to be loaded as a
+ * frame script into the browser being managed.
+ *
+ * For a view source window using viewSource.xul, the script viewSource.js in
+ * the window extends an instance of this with more window specific functions.
+ * The page script takes care of loading the companion frame script.
+ *
+ * For a view source tab (or some other non-window case), an instance of this is
+ * created by viewSourceUtils.js to wrap the <browser>. The frame script will
+ * be loaded by this module at construction time.
+ */
+this.ViewSourceBrowser = function ViewSourceBrowser(aBrowser) {
+ this._browser = aBrowser;
+ this.init();
+}
+
+ViewSourceBrowser.prototype = {
+ /**
+ * The <browser> that will be displaying the view source content.
+ */
+ get browser() {
+ return this._browser;
+ },
+
+ /**
+ * Holds the value of the last line found via the "Go to line"
+ * command, to pre-populate the prompt the next time it is
+ * opened.
+ */
+ lastLineFound: null,
+
+ /**
+ * These are the messages that ViewSourceBrowser will listen for
+ * from the frame script it injects. Any message names added here
+ * will automatically have ViewSourceBrowser listen for those messages,
+ * and remove the listeners on teardown.
+ */
+ messages: [
+ "ViewSource:PromptAndGoToLine",
+ "ViewSource:GoToLine:Success",
+ "ViewSource:GoToLine:Failed",
+ "ViewSource:StoreWrapping",
+ "ViewSource:StoreSyntaxHighlighting",
+ ],
+
+ /**
+ * This should be called as soon as the script loads. When this function
+ * executes, we can assume the DOM content has not yet loaded.
+ */
+ init() {
+ this.messages.forEach((msgName) => {
+ this.mm.addMessageListener(msgName, this);
+ });
+
+ // If we have a known <browser> already, load the frame script here. This
+ // is not true for the window case, as the element does not exist until the
+ // XUL document loads. For that case, the frame script is loaded by
+ // viewSource.js.
+ if (this._browser) {
+ this.loadFrameScript();
+ }
+ },
+
+ /**
+ * This should be called when the window is closing. This function should
+ * clean up event and message listeners.
+ */
+ uninit() {
+ this.messages.forEach((msgName) => {
+ this.mm.removeMessageListener(msgName, this);
+ });
+ },
+
+ /**
+ * For a new browser we've not seen before, load the frame script.
+ */
+ loadFrameScript() {
+ if (!gKnownBrowsers.has(this.browser)) {
+ gKnownBrowsers.add(this.browser);
+ this.mm.loadFrameScript(FRAME_SCRIPT, false);
+ }
+ },
+
+ /**
+ * Anything added to the messages array will get handled here, and should
+ * get dispatched to a specific function for the message name.
+ */
+ receiveMessage(message) {
+ let data = message.data;
+
+ switch (message.name) {
+ case "ViewSource:PromptAndGoToLine":
+ this.promptAndGoToLine();
+ break;
+ case "ViewSource:GoToLine:Success":
+ this.onGoToLineSuccess(data.lineNumber);
+ break;
+ case "ViewSource:GoToLine:Failed":
+ this.onGoToLineFailed();
+ break;
+ case "ViewSource:StoreWrapping":
+ this.storeWrapping(data.state);
+ break;
+ case "ViewSource:StoreSyntaxHighlighting":
+ this.storeSyntaxHighlighting(data.state);
+ break;
+ }
+ },
+
+ /**
+ * Getter for the message manager of the view source browser.
+ */
+ get mm() {
+ return this.browser.messageManager;
+ },
+
+ /**
+ * Send a message to the view source browser.
+ */
+ sendAsyncMessage(...args) {
+ this.browser.messageManager.sendAsyncMessage(...args);
+ },
+
+ /**
+ * A getter for the view source string bundle.
+ */
+ get bundle() {
+ if (this._bundle) {
+ return this._bundle;
+ }
+ return this._bundle = Services.strings.createBundle(BUNDLE_URL);
+ },
+
+ /**
+ * Loads the source for a URL while applying some optional features if
+ * enabled.
+ *
+ * For the viewSource.xul window, this is called by onXULLoaded above.
+ * For view source in a specific browser, this is manually called after
+ * this object is constructed.
+ *
+ * This takes a single object argument containing:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser:
+ * The browser containing the document that we would like to view the
+ * source of. This argument is optional if outerWindowID is not passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. This is the only way of attempting to
+ * load the source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+ loadViewSource({ URL, browser, outerWindowID, lineNumber }) {
+ if (!URL) {
+ throw new Error("Must supply a URL when opening view source.");
+ }
+
+ if (browser) {
+ this.browser.relatedBrowser = browser;
+
+ // If we're dealing with a remote browser, then the browser
+ // for view source needs to be remote as well.
+ this.updateBrowserRemoteness(browser.isRemoteBrowser);
+ } else if (outerWindowID) {
+ throw new Error("Must supply the browser if passing the outerWindowID");
+ }
+
+ this.sendAsyncMessage("ViewSource:LoadSource",
+ { URL, outerWindowID, lineNumber });
+ },
+
+ /**
+ * Loads a view source selection showing the given view-source url and
+ * highlight the selection.
+ *
+ * @param uri view-source uri to show
+ * @param drawSelection true to highlight the selection
+ * @param baseURI base URI of the original document
+ */
+ loadViewSourceFromSelection(URL, drawSelection, baseURI) {
+ this.sendAsyncMessage("ViewSource:LoadSourceWithSelection",
+ { URL, drawSelection, baseURI });
+ },
+
+ /**
+ * Updates the "remote" attribute of the view source browser. This
+ * will remove the browser from the DOM, and then re-add it in the
+ * same place it was taken from.
+ *
+ * @param shouldBeRemote
+ * True if the browser should be made remote. If the browsers
+ * remoteness already matches this value, this function does
+ * nothing.
+ */
+ updateBrowserRemoteness(shouldBeRemote) {
+ if (this.browser.isRemoteBrowser != shouldBeRemote) {
+ // In this base case, where we are handed a <browser> someone else is
+ // managing, we don't know for sure that it's safe to toggle remoteness.
+ // For view source in a window, this is overridden to actually do the
+ // flip if needed.
+ throw new Error("View source browser's remoteness mismatch");
+ }
+ },
+
+ /**
+ * Opens the "Go to line" prompt for a user to hop to a particular line
+ * of the source code they're viewing. This will keep prompting until the
+ * user either cancels out of the prompt, or enters a valid line number.
+ */
+ promptAndGoToLine() {
+ let input = { value: this.lastLineFound };
+ let window = Services.wm.getMostRecentWindow(null);
+
+ let ok = Services.prompt.prompt(
+ window,
+ this.bundle.GetStringFromName("goToLineTitle"),
+ this.bundle.GetStringFromName("goToLineText"),
+ input,
+ null,
+ {value:0});
+
+ if (!ok)
+ return;
+
+ let line = parseInt(input.value, 10);
+
+ if (!(line > 0)) {
+ Services.prompt.alert(window,
+ this.bundle.GetStringFromName("invalidInputTitle"),
+ this.bundle.GetStringFromName("invalidInputText"));
+ this.promptAndGoToLine();
+ } else {
+ this.goToLine(line);
+ }
+ },
+
+ /**
+ * Go to a particular line of the source code. This act is asynchronous.
+ *
+ * @param lineNumber
+ * The line number to try to go to to.
+ */
+ goToLine(lineNumber) {
+ this.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
+ },
+
+ /**
+ * Called when the frame script reports that a line was successfully gotten
+ * to.
+ *
+ * @param lineNumber
+ * The line number that we successfully got to.
+ */
+ onGoToLineSuccess(lineNumber) {
+ // We'll pre-populate the "Go to line" prompt with this value the next
+ // time it comes up.
+ this.lastLineFound = lineNumber;
+ },
+
+ /**
+ * Called when the frame script reports that we failed to go to a particular
+ * line. This informs the user that their selection was likely out of range,
+ * and then reprompts the user to try again.
+ */
+ onGoToLineFailed() {
+ let window = Services.wm.getMostRecentWindow(null);
+ Services.prompt.alert(window,
+ this.bundle.GetStringFromName("outOfRangeTitle"),
+ this.bundle.GetStringFromName("outOfRangeText"));
+ this.promptAndGoToLine();
+ },
+
+ /**
+ * Update the wrapping pref based on the child's current state.
+ * @param state
+ * Whether wrapping is currently enabled in the child.
+ */
+ storeWrapping(state) {
+ Services.prefs.setBoolPref("view_source.wrap_long_lines", state);
+ },
+
+ /**
+ * Update the syntax highlighting pref based on the child's current state.
+ * @param state
+ * Whether syntax highlighting is currently enabled in the child.
+ */
+ storeSyntaxHighlighting(state) {
+ Services.prefs.setBoolPref("view_source.syntax_highlight", state);
+ },
+
+};
+
+/**
+ * Helper to decide if a URI maps to view source content.
+ * @param uri
+ * String containing the URI
+ */
+ViewSourceBrowser.isViewSource = function(uri) {
+ return uri.startsWith("view-source:") ||
+ (uri.startsWith("data:") && uri.includes("MathML"));
+};
diff --git a/toolkit/components/viewsource/content/viewPartialSource.js b/toolkit/components/viewsource/content/viewPartialSource.js
new file mode 100644
index 0000000000..0b069344a1
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewPartialSource.js
@@ -0,0 +1,22 @@
+// -*- 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/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function onLoadViewPartialSource() {
+ // check the view_source.wrap_long_lines pref
+ // and set the menuitem's checked attribute accordingly
+ let wrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
+ document.getElementById("menu_wrapLongLines")
+ .setAttribute("checked", wrapLongLines);
+ document.getElementById("menu_highlightSyntax")
+ .setAttribute("checked",
+ Services.prefs.getBoolPref("view_source.syntax_highlight"));
+
+ let args = window.arguments[0];
+ viewSourceChrome.loadViewSourceFromSelection(args.URI, args.drawSelection, args.baseURI);
+ window.content.focus();
+}
diff --git a/toolkit/components/viewsource/content/viewPartialSource.xul b/toolkit/components/viewsource/content/viewPartialSource.xul
new file mode 100644
index 0000000000..fdec367b1c
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewPartialSource.xul
@@ -0,0 +1,163 @@
+<?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/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/viewsource/viewsource.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % sourceDTD SYSTEM "chrome://global/locale/viewSource.dtd" >
+%sourceDTD;
+]>
+
+<window id="viewSource"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoadViewPartialSource();"
+ contenttitlesetting="true"
+ title="&mainWindow.title;"
+ titlemodifier="&mainWindow.titlemodifier;"
+ titlepreface=""
+ titlemenuseparator ="&mainWindow.titlemodifierseparator;"
+ windowtype="navigator:view-source"
+ width="500" height="300"
+ screenX="10" screenY="10"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
+ <script type="application/javascript" src="chrome://global/content/viewSource.js"/>
+ <script type="application/javascript" src="chrome://global/content/viewPartialSource.js"/>
+ <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+
+ <stringbundle id="viewSourceBundle" src="chrome://global/locale/viewSource.properties"/>
+
+ <command id="cmd_savePage" oncommand="ViewSourceSavePage();"/>
+ <command id="cmd_print" oncommand="PrintUtils.printWindow(gBrowser.outerWindowID, gBrowser);"/>
+ <command id="cmd_printpreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/>
+ <command id="cmd_pagesetup" oncommand="PrintUtils.showPageSetup();"/>
+ <command id="cmd_close" oncommand="window.close();"/>
+ <commandset id="editMenuCommands"/>
+ <command id="cmd_find"
+ oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
+ <command id="cmd_findAgain"
+ oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
+ <command id="cmd_findPrevious"
+ oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
+ <command id="cmd_goToLine" oncommand="viewSourceChrome.promptAndGoToLine();" disabled="true"/>
+ <command id="cmd_highlightSyntax" oncommand="viewSourceChrome.toggleSyntaxHighlighting();"/>
+ <command id="cmd_wrapLongLines" oncommand="viewSourceChrome.toggleWrapping();"/>
+ <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
+ <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
+ <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
+
+ <keyset id="editMenuKeys"/>
+ <keyset id="viewSourceKeys">
+ <key id="key_savePage" key="&savePageCmd.commandkey;" modifiers="accel" command="cmd_savePage"/>
+ <key id="key_print" key="&printCmd.commandkey;" modifiers="accel" command="cmd_print"/>
+ <key id="key_close" key="&closeCmd.commandkey;" modifiers="accel" command="cmd_close"/>
+ <key keycode="VK_ESCAPE" command="cmd_close"/>
+
+ <key id="key_textZoomEnlarge" key="&textEnlarge.commandkey;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <key id="key_textZoomEnlarge2" key="&textEnlarge.commandkey2;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <key id="key_textZoomEnlarge3" key="&textEnlarge.commandkey3;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <key id="key_textZoomReduce" key="&textReduce.commandkey;" command="cmd_textZoomReduce" modifiers="accel"/>
+ <key id="key_textZoomReduce2" key="&textReduce.commandkey2;" command="cmd_textZoomReduce" modifiers="accel"/>
+ <key id="key_textZoomReset" key="&textReset.commandkey;" command="cmd_textZoomReset" modifiers="accel"/>
+ <key id="key_textZoomReset2" key="&textReset.commandkey2;" command="cmd_textZoomReset" modifiers="accel"/>
+ </keyset>
+
+ <menupopup id="viewSourceContextMenu">
+ <menuitem id="cMenu_findAgain"/>
+ <menuseparator/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="context-copyLink"
+ label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuitem id="context-copyEmail"
+ label="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+
+ <!-- Menu -->
+ <toolbox id="viewSource-toolbox">
+ <menubar id="viewSource-main-menubar">
+
+ <menu id="menu_file" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="menu_FilePopup">
+ <menuitem key="key_savePage" command="cmd_savePage" id="menu_savePage"
+ label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;"/>
+ <menuitem command="cmd_pagesetup" id="menu_pageSetup"
+ label="&pageSetupCmd.label;" accesskey="&pageSetupCmd.accesskey;"/>
+#ifndef XP_MACOSX
+ <menuitem command="cmd_printpreview" id="menu_printPreview"
+ label="&printPreviewCmd.label;" accesskey="&printPreviewCmd.accesskey;"/>
+#endif
+ <menuitem key="key_print" command="cmd_print" id="menu_print"
+ label="&printCmd.label;" accesskey="&printCmd.accesskey;"/>
+ <menuseparator/>
+ <menuitem key="key_close" command="cmd_close" id="menu_close"
+ label="&closeCmd.label;" accesskey="&closeCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_edit">
+ <menupopup id="editmenu-popup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuitem id="menu_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_find"/>
+ <menuitem id="menu_findAgain"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_view" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
+ <menupopup id="viewmenu-popup">
+ <menu id="viewTextZoomMenu" label="&menu_textSize.label;" accesskey="&menu_textSize.accesskey;">
+ <menupopup>
+ <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge"
+ label="&menu_textEnlarge.label;" accesskey="&menu_textEnlarge.accesskey;"
+ key="key_textZoomEnlarge"/>
+ <menuitem id="menu_textReduce" command="cmd_textZoomReduce"
+ label="&menu_textReduce.label;" accesskey="&menu_textReduce.accesskey;"
+ key="key_textZoomReduce"/>
+ <menuseparator/>
+ <menuitem id="menu_textReset" command="cmd_textZoomReset"
+ label="&menu_textReset.label;" accesskey="&menu_textReset.accesskey;"
+ key="key_textZoomReset"/>
+ </menupopup>
+ </menu>
+ <menuseparator/>
+ <menuitem id="menu_wrapLongLines" type="checkbox" command="cmd_wrapLongLines"
+ label="&menu_wrapLongLines.title;" accesskey="&menu_wrapLongLines.accesskey;"/>
+ <menuitem type="checkbox" id="menu_highlightSyntax" command="cmd_highlightSyntax"
+ label="&menu_highlightSyntax.label;" accesskey="&menu_highlightSyntax.accesskey;"/>
+ </menupopup>
+ </menu>
+ </menubar>
+ </toolbox>
+
+ <vbox id="appcontent" flex="1">
+ <browser id="content" type="content-primary" name="content" src="about:blank" flex="1"
+ disablehistory="true" context="viewSourceContextMenu" />
+ <findbar id="FindToolbar" browserid="content"/>
+ </vbox>
+
+</window>
diff --git a/toolkit/components/viewsource/content/viewSource-content.js b/toolkit/components/viewsource/content/viewSource-content.js
new file mode 100644
index 0000000000..4efa1e9525
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource-content.js
@@ -0,0 +1,978 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
+
+// These are markers used to delimit the selection during processing. They
+// are removed from the final rendering.
+// We use noncharacter Unicode codepoints to minimize the risk of clashing
+// with anything that might legitimately be present in the document.
+// U+FDD0..FDEF <noncharacters>
+const MARK_SELECTION_START = "\uFDD0";
+const MARK_SELECTION_END = "\uFDEF";
+
+var global = this;
+
+/**
+ * ViewSourceContent should be loaded in the <xul:browser> of the
+ * view source window, and initialized as soon as it has loaded.
+ */
+var ViewSourceContent = {
+ /**
+ * We'll act as an nsISelectionListener as well so that we can send
+ * updates to the view source window's status bar.
+ */
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISelectionListener]),
+
+ /**
+ * These are the messages that ViewSourceContent is prepared to listen
+ * for. If you need ViewSourceContent to handle more messages, add them
+ * here.
+ */
+ messages: [
+ "ViewSource:LoadSource",
+ "ViewSource:LoadSourceDeprecated",
+ "ViewSource:LoadSourceWithSelection",
+ "ViewSource:GoToLine",
+ "ViewSource:ToggleWrapping",
+ "ViewSource:ToggleSyntaxHighlighting",
+ "ViewSource:SetCharacterSet",
+ ],
+
+ /**
+ * When showing selection source, chrome will construct a page fragment to
+ * show, and then instruct content to draw a selection after load. This is
+ * set true when there is a pending request to draw selection.
+ */
+ needsDrawSelection: false,
+
+ /**
+ * ViewSourceContent is attached as an nsISelectionListener on pageshow,
+ * and removed on pagehide. When the initial about:blank is transitioned
+ * away from, a pagehide is fired without us having attached ourselves
+ * first. We use this boolean to keep track of whether or not we're
+ * attached, so we don't attempt to remove our listener when it's not
+ * yet there (which throws).
+ */
+ selectionListenerAttached: false,
+
+ get isViewSource() {
+ let uri = content.document.documentURI;
+ return uri.startsWith("view-source:") ||
+ (uri.startsWith("data:") && uri.includes("MathML"));
+ },
+
+ get isAboutBlank() {
+ let uri = content.document.documentURI;
+ return uri == "about:blank";
+ },
+
+ /**
+ * This should be called as soon as this frame script has loaded.
+ */
+ init() {
+ this.messages.forEach((msgName) => {
+ addMessageListener(msgName, this);
+ });
+
+ addEventListener("pagehide", this, true);
+ addEventListener("pageshow", this, true);
+ addEventListener("click", this);
+ addEventListener("unload", this);
+ Services.els.addSystemEventListener(global, "contextmenu", this, false);
+ },
+
+ /**
+ * This should be called when the frame script is being unloaded,
+ * and the browser is tearing down.
+ */
+ uninit() {
+ this.messages.forEach((msgName) => {
+ removeMessageListener(msgName, this);
+ });
+
+ removeEventListener("pagehide", this, true);
+ removeEventListener("pageshow", this, true);
+ removeEventListener("click", this);
+ removeEventListener("unload", this);
+
+ Services.els.removeSystemEventListener(global, "contextmenu", this, false);
+
+ // Cancel any pending toolbar updates.
+ if (this.updateStatusTask) {
+ this.updateStatusTask.disarm();
+ }
+ },
+
+ /**
+ * Anything added to the messages array will get handled here, and should
+ * get dispatched to a specific function for the message name.
+ */
+ receiveMessage(msg) {
+ if (!this.isViewSource && !this.isAboutBlank) {
+ return;
+ }
+ let data = msg.data;
+ let objects = msg.objects;
+ switch (msg.name) {
+ case "ViewSource:LoadSource":
+ this.viewSource(data.URL, data.outerWindowID, data.lineNumber,
+ data.shouldWrap);
+ break;
+ case "ViewSource:LoadSourceDeprecated":
+ this.viewSourceDeprecated(data.URL, objects.pageDescriptor, data.lineNumber,
+ data.forcedCharSet);
+ break;
+ case "ViewSource:LoadSourceWithSelection":
+ this.viewSourceWithSelection(data.URL, data.drawSelection, data.baseURI);
+ break;
+ case "ViewSource:GoToLine":
+ this.goToLine(data.lineNumber);
+ break;
+ case "ViewSource:ToggleWrapping":
+ this.toggleWrapping();
+ break;
+ case "ViewSource:ToggleSyntaxHighlighting":
+ this.toggleSyntaxHighlighting();
+ break;
+ case "ViewSource:SetCharacterSet":
+ this.setCharacterSet(data.charset, data.doPageLoad);
+ break;
+ }
+ },
+
+ /**
+ * Any events should get handled here, and should get dispatched to
+ * a specific function for the event type.
+ */
+ handleEvent(event) {
+ if (!this.isViewSource) {
+ return;
+ }
+ switch (event.type) {
+ case "pagehide":
+ this.onPageHide(event);
+ break;
+ case "pageshow":
+ this.onPageShow(event);
+ break;
+ case "click":
+ this.onClick(event);
+ break;
+ case "unload":
+ this.uninit();
+ break;
+ case "contextmenu":
+ this.onContextMenu(event);
+ break;
+ }
+ },
+
+ /**
+ * A getter for the view source string bundle.
+ */
+ get bundle() {
+ delete this.bundle;
+ this.bundle = Services.strings.createBundle(BUNDLE_URL);
+ return this.bundle;
+ },
+
+ /**
+ * A shortcut to the nsISelectionController for the content.
+ */
+ get selectionController() {
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+ },
+
+ /**
+ * A shortcut to the nsIWebBrowserFind for the content.
+ */
+ get webBrowserFind() {
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserFind);
+ },
+
+ /**
+ * Called when the parent sends a message to view some source code.
+ *
+ * @param URL (required)
+ * The URL string of the source to be shown.
+ * @param outerWindowID (optional)
+ * The outerWindowID of the content window that has hosted
+ * the document, in case we want to retrieve it from the network
+ * cache.
+ * @param lineNumber (optional)
+ * The line number to focus as soon as the source has finished
+ * loading.
+ */
+ viewSource(URL, outerWindowID, lineNumber) {
+ let pageDescriptor, forcedCharSet;
+
+ if (outerWindowID) {
+ let contentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
+ let requestor = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ try {
+ let otherWebNav = requestor.getInterface(Ci.nsIWebNavigation);
+ pageDescriptor = otherWebNav.QueryInterface(Ci.nsIWebPageDescriptor)
+ .currentDescriptor;
+ } catch (e) {
+ // We couldn't get the page descriptor, so we'll probably end up re-retrieving
+ // this document off of the network.
+ }
+
+ let utils = requestor.getInterface(Ci.nsIDOMWindowUtils);
+ let doc = contentWindow.document;
+ forcedCharSet = utils.docCharsetIsForced ? doc.characterSet
+ : null;
+ }
+
+ this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
+ },
+
+ /**
+ * Called when the parent is using the deprecated API for viewSource.xul.
+ * This function will throw if it's called on a remote browser.
+ *
+ * @param URL (required)
+ * The URL string of the source to be shown.
+ * @param pageDescriptor (optional)
+ * The currentDescriptor off of an nsIWebPageDescriptor, in the
+ * event that the caller wants to try to load the source out of
+ * the network cache.
+ * @param lineNumber (optional)
+ * The line number to focus as soon as the source has finished
+ * loading.
+ * @param forcedCharSet (optional)
+ * The document character set to use instead of the default one.
+ */
+ viewSourceDeprecated(URL, pageDescriptor, lineNumber, forcedCharSet) {
+ // This should not be called if this frame script is running
+ // in a content process!
+ if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ throw new Error("ViewSource deprecated API should not be used with " +
+ "remote browsers.");
+ }
+
+ this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
+ },
+
+ /**
+ * Common utility function used by both the current and deprecated APIs
+ * for loading source.
+ *
+ * @param URL (required)
+ * The URL string of the source to be shown.
+ * @param pageDescriptor (optional)
+ * The currentDescriptor off of an nsIWebPageDescriptor, in the
+ * event that the caller wants to try to load the source out of
+ * the network cache.
+ * @param lineNumber (optional)
+ * The line number to focus as soon as the source has finished
+ * loading.
+ * @param forcedCharSet (optional)
+ * The document character set to use instead of the default one.
+ */
+ loadSource(URL, pageDescriptor, lineNumber, forcedCharSet) {
+ const viewSrcURL = "view-source:" + URL;
+
+ if (forcedCharSet) {
+ try {
+ docShell.charset = forcedCharSet;
+ } catch (e) { /* invalid charset */ }
+ }
+
+ if (lineNumber && lineNumber > 0) {
+ let doneLoading = (event) => {
+ // Ignore possible initial load of about:blank
+ if (this.isAboutBlank ||
+ !content.document.body) {
+ return;
+ }
+ this.goToLine(lineNumber);
+ removeEventListener("pageshow", doneLoading);
+ };
+
+ addEventListener("pageshow", doneLoading);
+ }
+
+ if (!pageDescriptor) {
+ this.loadSourceFromURL(viewSrcURL);
+ return;
+ }
+
+ try {
+ let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
+ pageLoader.loadPage(pageDescriptor,
+ Ci.nsIWebPageDescriptor.DISPLAY_AS_SOURCE);
+ } catch (e) {
+ // We were not able to load the source from the network cache.
+ this.loadSourceFromURL(viewSrcURL);
+ return;
+ }
+
+ let shEntrySource = pageDescriptor.QueryInterface(Ci.nsISHEntry);
+ let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]
+ .createInstance(Ci.nsISHEntry);
+ shEntry.setURI(BrowserUtils.makeURI(viewSrcURL, null, null));
+ shEntry.setTitle(viewSrcURL);
+ shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+ shEntry.cacheKey = shEntrySource.cacheKey;
+ docShell.QueryInterface(Ci.nsIWebNavigation)
+ .sessionHistory
+ .QueryInterface(Ci.nsISHistoryInternal)
+ .addEntry(shEntry, true);
+ },
+
+ /**
+ * Load some URL in the browser.
+ *
+ * @param URL
+ * The URL string to load.
+ */
+ loadSourceFromURL(URL) {
+ let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(URL, loadFlags, null, null, null);
+ },
+
+ /**
+ * This handler is for click events from:
+ * * error page content, which can show up if the user attempts to view the
+ * source of an attack page.
+ * * in-page context menu actions
+ */
+ onClick(event) {
+ let target = event.originalTarget;
+ // Check for content menu actions
+ if (target.id) {
+ this.contextMenuItems.forEach(itemSpec => {
+ if (itemSpec.id !== target.id) {
+ return;
+ }
+ itemSpec.handler.call(this, event);
+ event.stopPropagation();
+ });
+ }
+
+ // Don't trust synthetic events
+ if (!event.isTrusted || event.target.localName != "button")
+ return;
+
+ let errorDoc = target.ownerDocument;
+
+ if (/^about:blocked/.test(errorDoc.documentURI)) {
+ // The event came from a button on a malware/phishing block page
+
+ if (target == errorDoc.getElementById("getMeOutButton")) {
+ // Instead of loading some safe page, just close the window
+ sendAsyncMessage("ViewSource:Close");
+ } else if (target == errorDoc.getElementById("reportButton")) {
+ // This is the "Why is this site blocked" button. We redirect
+ // to the generic page describing phishing/malware protection.
+ let URL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ sendAsyncMessage("ViewSource:OpenURL", { URL })
+ } else if (target == errorDoc.getElementById("ignoreWarningButton")) {
+ // Allow users to override and continue through to the site
+ docShell.QueryInterface(Ci.nsIWebNavigation)
+ .loadURIWithOptions(content.location.href,
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
+ null, Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+ null, null, null);
+ }
+ }
+ },
+
+ /**
+ * Handler for the pageshow event.
+ *
+ * @param event
+ * The pageshow event being handled.
+ */
+ onPageShow(event) {
+ let selection = content.getSelection();
+ if (selection) {
+ selection.QueryInterface(Ci.nsISelectionPrivate)
+ .addSelectionListener(this);
+ this.selectionListenerAttached = true;
+ }
+ content.focus();
+
+ // If we need to draw the selection, wait until an actual view source page
+ // has loaded, instead of about:blank.
+ if (this.needsDrawSelection &&
+ content.document.documentURI.startsWith("view-source:")) {
+ this.needsDrawSelection = false;
+ this.drawSelection();
+ }
+
+ if (content.document.body) {
+ this.injectContextMenu();
+ }
+
+ sendAsyncMessage("ViewSource:SourceLoaded");
+ },
+
+ /**
+ * Handler for the pagehide event.
+ *
+ * @param event
+ * The pagehide event being handled.
+ */
+ onPageHide(event) {
+ // The initial about:blank will fire pagehide before we
+ // ever set a selectionListener, so we have a boolean around
+ // to keep track of when the listener is attached.
+ if (this.selectionListenerAttached) {
+ content.getSelection()
+ .QueryInterface(Ci.nsISelectionPrivate)
+ .removeSelectionListener(this);
+ this.selectionListenerAttached = false;
+ }
+ sendAsyncMessage("ViewSource:SourceUnloaded");
+ },
+
+ onContextMenu(event) {
+ let addonInfo = {};
+ let subject = {
+ event: event,
+ addonInfo: addonInfo,
+ };
+
+ subject.wrappedJSObject = subject;
+ Services.obs.notifyObservers(subject, "content-contextmenu", null);
+
+ let node = event.target;
+
+ let result = {
+ isEmail: false,
+ isLink: false,
+ href: "",
+ // We have to pass these in the event that we're running in
+ // a remote browser, so that ViewSourceChrome knows where to
+ // open the context menu.
+ screenX: event.screenX,
+ screenY: event.screenY,
+ };
+
+ if (node && node.localName == "a") {
+ result.isLink = node.href.startsWith("view-source:");
+ result.isEmail = node.href.startsWith("mailto:");
+ result.href = node.href.substring(node.href.indexOf(":") + 1);
+ }
+
+ sendSyncMessage("ViewSource:ContextMenuOpening", result);
+ },
+
+ /**
+ * Attempts to go to a particular line in the source code being
+ * shown. If it succeeds in finding the line, it will fire a
+ * "ViewSource:GoToLine:Success" message, passing up an object
+ * with the lineNumber we just went to. If it cannot find the line,
+ * it will fire a "ViewSource:GoToLine:Failed" message.
+ *
+ * @param lineNumber
+ * The line number to attempt to go to.
+ */
+ goToLine(lineNumber) {
+ let body = content.document.body;
+
+ // The source document is made up of a number of pre elements with
+ // id attributes in the format <pre id="line123">, meaning that
+ // the first line in the pre element is number 123.
+ // Do binary search to find the pre element containing the line.
+ // However, in the plain text case, we have only one pre without an
+ // attribute, so assume it begins on line 1.
+ let pre;
+ for (let lbound = 0, ubound = body.childNodes.length; ; ) {
+ let middle = (lbound + ubound) >> 1;
+ pre = body.childNodes[middle];
+
+ let firstLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
+
+ if (lbound == ubound - 1) {
+ break;
+ }
+
+ if (lineNumber >= firstLine) {
+ lbound = middle;
+ } else {
+ ubound = middle;
+ }
+ }
+
+ let result = {};
+ let found = this.findLocation(pre, lineNumber, null, -1, false, result);
+
+ if (!found) {
+ sendAsyncMessage("ViewSource:GoToLine:Failed");
+ return;
+ }
+
+ let selection = content.getSelection();
+ selection.removeAllRanges();
+
+ // In our case, the range's startOffset is after "\n" on the previous line.
+ // Tune the selection at the beginning of the next line and do some tweaking
+ // to position the focusNode and the caret at the beginning of the line.
+ selection.QueryInterface(Ci.nsISelectionPrivate)
+ .interlinePosition = true;
+
+ selection.addRange(result.range);
+
+ if (!selection.isCollapsed) {
+ selection.collapseToEnd();
+
+ let offset = result.range.startOffset;
+ let node = result.range.startContainer;
+ if (offset < node.data.length) {
+ // The same text node spans across the "\n", just focus where we were.
+ selection.extend(node, offset);
+ }
+ else {
+ // There is another tag just after the "\n", hook there. We need
+ // to focus a safe point because there are edgy cases such as
+ // <span>...\n</span><span>...</span> vs.
+ // <span>...\n<span>...</span></span><span>...</span>
+ node = node.nextSibling ? node.nextSibling : node.parentNode.nextSibling;
+ selection.extend(node, 0);
+ }
+ }
+
+ let selCon = this.selectionController;
+ selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
+ selCon.setCaretVisibilityDuringSelection(true);
+
+ // Scroll the beginning of the line into view.
+ selCon.scrollSelectionIntoView(
+ Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_FOCUS_REGION,
+ true);
+
+ sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber });
+ },
+
+
+ /**
+ * Some old code from the original view source implementation. Original
+ * documentation follows:
+ *
+ * "Loops through the text lines in the pre element. The arguments are either
+ * (pre, line) or (node, offset, interlinePosition). result is an out
+ * argument. If (pre, line) are specified (and node == null), result.range is
+ * a range spanning the specified line. If the (node, offset,
+ * interlinePosition) are specified, result.line and result.col are the line
+ * and column number of the specified offset in the specified node relative to
+ * the whole file."
+ */
+ findLocation(pre, lineNumber, node, offset, interlinePosition, result) {
+ if (node && !pre) {
+ // Look upwards to find the current pre element.
+ for (pre = node;
+ pre.nodeName != "PRE";
+ pre = pre.parentNode);
+ }
+
+ // The source document is made up of a number of pre elements with
+ // id attributes in the format <pre id="line123">, meaning that
+ // the first line in the pre element is number 123.
+ // However, in the plain text case, there is only one <pre> without an id,
+ // so assume line 1.
+ let curLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
+
+ // Walk through each of the text nodes and count newlines.
+ let treewalker = content.document
+ .createTreeWalker(pre, Ci.nsIDOMNodeFilter.SHOW_TEXT, null);
+
+ // The column number of the first character in the current text node.
+ let firstCol = 1;
+
+ let found = false;
+ for (let textNode = treewalker.firstChild();
+ textNode && !found;
+ textNode = treewalker.nextNode()) {
+
+ // \r is not a valid character in the DOM, so we only check for \n.
+ let lineArray = textNode.data.split(/\n/);
+ let lastLineInNode = curLine + lineArray.length - 1;
+
+ // Check if we can skip the text node without further inspection.
+ if (node ? (textNode != node) : (lastLineInNode < lineNumber)) {
+ if (lineArray.length > 1) {
+ firstCol = 1;
+ }
+ firstCol += lineArray[lineArray.length - 1].length;
+ curLine = lastLineInNode;
+ continue;
+ }
+
+ // curPos is the offset within the current text node of the first
+ // character in the current line.
+ for (var i = 0, curPos = 0;
+ i < lineArray.length;
+ curPos += lineArray[i++].length + 1) {
+
+ if (i > 0) {
+ curLine++;
+ }
+
+ if (node) {
+ if (offset >= curPos && offset <= curPos + lineArray[i].length) {
+ // If we are right after the \n of a line and interlinePosition is
+ // false, the caret looks as if it were at the end of the previous
+ // line, so we display that line and column instead.
+
+ if (i > 0 && offset == curPos && !interlinePosition) {
+ result.line = curLine - 1;
+ var prevPos = curPos - lineArray[i - 1].length;
+ result.col = (i == 1 ? firstCol : 1) + offset - prevPos;
+ } else {
+ result.line = curLine;
+ result.col = (i == 0 ? firstCol : 1) + offset - curPos;
+ }
+ found = true;
+
+ break;
+ }
+
+ } else if (curLine == lineNumber && !("range" in result)) {
+ result.range = content.document.createRange();
+ result.range.setStart(textNode, curPos);
+
+ // This will always be overridden later, except when we look for
+ // the very last line in the file (this is the only line that does
+ // not end with \n).
+ result.range.setEndAfter(pre.lastChild);
+
+ } else if (curLine == lineNumber + 1) {
+ result.range.setEnd(textNode, curPos - 1);
+ found = true;
+ break;
+ }
+ }
+ }
+
+ return found || ("range" in result);
+ },
+
+ /**
+ * Toggles the "wrap" class on the document body, which sets whether
+ * or not long lines are wrapped. Notifies parent to update the pref.
+ */
+ toggleWrapping() {
+ let body = content.document.body;
+ let state = body.classList.toggle("wrap");
+ sendAsyncMessage("ViewSource:StoreWrapping", { state });
+ },
+
+ /**
+ * Toggles the "highlight" class on the document body, which sets whether
+ * or not syntax highlighting is displayed. Notifies parent to update the
+ * pref.
+ */
+ toggleSyntaxHighlighting() {
+ let body = content.document.body;
+ let state = body.classList.toggle("highlight");
+ sendAsyncMessage("ViewSource:StoreSyntaxHighlighting", { state });
+ },
+
+ /**
+ * Called when the parent has changed the character set to view the
+ * source with.
+ *
+ * @param charset
+ * The character set to use.
+ * @param doPageLoad
+ * Whether or not we should reload the page ourselves with the
+ * nsIWebPageDescriptor. Part of a workaround for bug 136322.
+ */
+ setCharacterSet(charset, doPageLoad) {
+ docShell.charset = charset;
+ if (doPageLoad) {
+ this.reload();
+ }
+ },
+
+ /**
+ * Reloads the content.
+ */
+ reload() {
+ let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
+ try {
+ pageLoader.loadPage(pageLoader.currentDescriptor,
+ Ci.nsIWebPageDescriptor.DISPLAY_NORMAL);
+ } catch (e) {
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
+ }
+ },
+
+ /**
+ * A reference to a DeferredTask that is armed every time the
+ * selection changes.
+ */
+ updateStatusTask: null,
+
+ /**
+ * Called once the DeferredTask fires. Sends a message up to the
+ * parent to update the status bar text.
+ */
+ updateStatus() {
+ let selection = content.getSelection();
+
+ if (!selection.focusNode) {
+ sendAsyncMessage("ViewSource:UpdateStatus", { label: "" });
+ return;
+ }
+ if (selection.focusNode.nodeType != Ci.nsIDOMNode.TEXT_NODE) {
+ return;
+ }
+
+ let selCon = this.selectionController;
+ selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
+ selCon.setCaretVisibilityDuringSelection(true);
+
+ let interlinePosition = selection.QueryInterface(Ci.nsISelectionPrivate)
+ .interlinePosition;
+
+ let result = {};
+ this.findLocation(null, -1,
+ selection.focusNode, selection.focusOffset, interlinePosition, result);
+
+ let label = this.bundle.formatStringFromName("statusBarLineCol",
+ [result.line, result.col], 2);
+ sendAsyncMessage("ViewSource:UpdateStatus", { label });
+ },
+
+ /**
+ * Loads a view source selection showing the given view-source url and
+ * highlight the selection.
+ *
+ * @param uri view-source uri to show
+ * @param drawSelection true to highlight the selection
+ * @param baseURI base URI of the original document
+ */
+ viewSourceWithSelection(uri, drawSelection, baseURI)
+ {
+ this.needsDrawSelection = drawSelection;
+
+ // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
+ let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURIWithOptions(uri, loadFlags,
+ null, referrerPolicy, // referrer
+ null, null, // postData, headers
+ Services.io.newURI(baseURI, null, null));
+ },
+
+ /**
+ * nsISelectionListener
+ */
+
+ /**
+ * Gets called every time the selection is changed. Coalesces frequent
+ * changes, and calls updateStatus after 100ms of no selection change
+ * activity.
+ */
+ notifySelectionChanged(doc, sel, reason) {
+ if (!this.updateStatusTask) {
+ this.updateStatusTask = new DeferredTask(() => {
+ this.updateStatus();
+ }, 100);
+ }
+
+ this.updateStatusTask.arm();
+ },
+
+ /**
+ * Using special markers left in the serialized source, this helper makes the
+ * underlying markup of the selected fragment to automatically appear as
+ * selected on the inflated view-source DOM.
+ */
+ drawSelection() {
+ content.document.title =
+ this.bundle.GetStringFromName("viewSelectionSourceTitle");
+
+ // find the special selection markers that we added earlier, and
+ // draw the selection between the two...
+ var findService = null;
+ try {
+ // get the find service which stores the global find state
+ findService = Cc["@mozilla.org/find/find_service;1"]
+ .getService(Ci.nsIFindService);
+ } catch (e) { }
+ if (!findService)
+ return;
+
+ // cache the current global find state
+ var matchCase = findService.matchCase;
+ var entireWord = findService.entireWord;
+ var wrapFind = findService.wrapFind;
+ var findBackwards = findService.findBackwards;
+ var searchString = findService.searchString;
+ var replaceString = findService.replaceString;
+
+ // setup our find instance
+ var findInst = this.webBrowserFind;
+ findInst.matchCase = true;
+ findInst.entireWord = false;
+ findInst.wrapFind = true;
+ findInst.findBackwards = false;
+
+ // ...lookup the start mark
+ findInst.searchString = MARK_SELECTION_START;
+ var startLength = MARK_SELECTION_START.length;
+ findInst.findNext();
+
+ var selection = content.getSelection();
+ if (!selection.rangeCount)
+ return;
+
+ var range = selection.getRangeAt(0);
+
+ var startContainer = range.startContainer;
+ var startOffset = range.startOffset;
+
+ // ...lookup the end mark
+ findInst.searchString = MARK_SELECTION_END;
+ var endLength = MARK_SELECTION_END.length;
+ findInst.findNext();
+
+ var endContainer = selection.anchorNode;
+ var endOffset = selection.anchorOffset;
+
+ // reset the selection that find has left
+ selection.removeAllRanges();
+
+ // delete the special markers now...
+ endContainer.deleteData(endOffset, endLength);
+ startContainer.deleteData(startOffset, startLength);
+ if (startContainer == endContainer)
+ endOffset -= startLength; // has shrunk if on same text node...
+ range.setEnd(endContainer, endOffset);
+
+ // show the selection and scroll it into view
+ selection.addRange(range);
+ // the default behavior of the selection is to scroll at the end of
+ // the selection, whereas in this situation, it is more user-friendly
+ // to scroll at the beginning. So we override the default behavior here
+ try {
+ this.selectionController.scrollSelectionIntoView(
+ Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
+ true);
+ }
+ catch (e) { }
+
+ // restore the current find state
+ findService.matchCase = matchCase;
+ findService.entireWord = entireWord;
+ findService.wrapFind = wrapFind;
+ findService.findBackwards = findBackwards;
+ findService.searchString = searchString;
+ findService.replaceString = replaceString;
+
+ findInst.matchCase = matchCase;
+ findInst.entireWord = entireWord;
+ findInst.wrapFind = wrapFind;
+ findInst.findBackwards = findBackwards;
+ findInst.searchString = searchString;
+ },
+
+ /**
+ * In-page context menu items that are injected after page load.
+ */
+ contextMenuItems: [
+ {
+ id: "goToLine",
+ accesskey: true,
+ handler() {
+ sendAsyncMessage("ViewSource:PromptAndGoToLine");
+ }
+ },
+ {
+ id: "wrapLongLines",
+ get checked() {
+ return Services.prefs.getBoolPref("view_source.wrap_long_lines");
+ },
+ handler() {
+ this.toggleWrapping();
+ }
+ },
+ {
+ id: "highlightSyntax",
+ get checked() {
+ return Services.prefs.getBoolPref("view_source.syntax_highlight");
+ },
+ handler() {
+ this.toggleSyntaxHighlighting();
+ }
+ },
+ ],
+
+ /**
+ * Add context menu items for view source specific actions.
+ */
+ injectContextMenu() {
+ let doc = content.document;
+
+ let menu = doc.createElementNS(NS_XHTML, "menu");
+ menu.setAttribute("type", "context");
+ menu.setAttribute("id", "actions");
+ doc.body.appendChild(menu);
+ doc.body.setAttribute("contextmenu", "actions");
+
+ this.contextMenuItems.forEach(itemSpec => {
+ let item = doc.createElementNS(NS_XHTML, "menuitem");
+ item.setAttribute("id", itemSpec.id);
+ let labelName = `context_${itemSpec.id}_label`;
+ let label = this.bundle.GetStringFromName(labelName);
+ item.setAttribute("label", label);
+ if ("checked" in itemSpec) {
+ item.setAttribute("type", "checkbox");
+ }
+ if (itemSpec.accesskey) {
+ let accesskeyName = `context_${itemSpec.id}_accesskey`;
+ item.setAttribute("accesskey",
+ this.bundle.GetStringFromName(accesskeyName))
+ }
+ menu.appendChild(item);
+ });
+
+ this.updateContextMenu();
+ },
+
+ /**
+ * Update state of checkbox-style context menu items.
+ */
+ updateContextMenu() {
+ let doc = content.document;
+ this.contextMenuItems.forEach(itemSpec => {
+ if (!("checked" in itemSpec)) {
+ return;
+ }
+ let item = doc.getElementById(itemSpec.id);
+ if (itemSpec.checked) {
+ item.setAttribute("checked", true);
+ } else {
+ item.removeAttribute("checked");
+ }
+ });
+ },
+};
+ViewSourceContent.init();
diff --git a/toolkit/components/viewsource/content/viewSource.css b/toolkit/components/viewsource/content/viewSource.css
new file mode 100644
index 0000000000..d03efcc8c5
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource.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/. */
+
+toolbar[printpreview="true"] {
+ -moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar");
+}
+
+browser[remote="true"] {
+ -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
+} \ No newline at end of file
diff --git a/toolkit/components/viewsource/content/viewSource.js b/toolkit/components/viewsource/content/viewSource.js
new file mode 100644
index 0000000000..873e1bcdb3
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource.js
@@ -0,0 +1,884 @@
+// -*- 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 { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ViewSourceBrowser.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
+ "resource://gre/modules/CharsetMenu.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+[
+ ["gBrowser", "content"],
+ ["gViewSourceBundle", "viewSourceBundle"],
+ ["gContextMenu", "viewSourceContextMenu"]
+].forEach(function ([name, id]) {
+ window.__defineGetter__(name, function () {
+ var element = document.getElementById(id);
+ if (!element)
+ return null;
+ delete window[name];
+ return window[name] = element;
+ });
+});
+
+/**
+ * ViewSourceChrome is the primary interface for interacting with
+ * the view source browser from a self-contained window. It extends
+ * ViewSourceBrowser with additional things needed inside the special window.
+ *
+ * It initializes itself on script load.
+ */
+function ViewSourceChrome() {
+ ViewSourceBrowser.call(this);
+}
+
+ViewSourceChrome.prototype = {
+ __proto__: ViewSourceBrowser.prototype,
+
+ /**
+ * The <browser> that will be displaying the view source content.
+ */
+ get browser() {
+ return gBrowser;
+ },
+
+ /**
+ * The context menu, when opened from the content process, sends
+ * up a chunk of serialized data describing the items that the
+ * context menu is being opened on. This allows us to avoid using
+ * CPOWs.
+ */
+ contextMenuData: {},
+
+ /**
+ * These are the messages that ViewSourceChrome will listen for
+ * from the frame script it injects. Any message names added here
+ * will automatically have ViewSourceChrome listen for those messages,
+ * and remove the listeners on teardown.
+ */
+ messages: ViewSourceBrowser.prototype.messages.concat([
+ "ViewSource:SourceLoaded",
+ "ViewSource:SourceUnloaded",
+ "ViewSource:Close",
+ "ViewSource:OpenURL",
+ "ViewSource:UpdateStatus",
+ "ViewSource:ContextMenuOpening",
+ ]),
+
+ /**
+ * This called via ViewSourceBrowser's constructor. This should be called as
+ * soon as the script loads. When this function executes, we can assume the
+ * DOM content has not yet loaded.
+ */
+ init() {
+ this.mm.loadFrameScript("chrome://global/content/viewSource-content.js", true);
+
+ this.shouldWrap = Services.prefs.getBoolPref("view_source.wrap_long_lines");
+ this.shouldHighlight =
+ Services.prefs.getBoolPref("view_source.syntax_highlight");
+
+ addEventListener("load", this);
+ addEventListener("unload", this);
+ addEventListener("AppCommand", this, true);
+ addEventListener("MozSwipeGesture", this, true);
+
+ ViewSourceBrowser.prototype.init.call(this);
+ },
+
+ /**
+ * This should be called when the window is closing. This function should
+ * clean up event and message listeners.
+ */
+ uninit() {
+ ViewSourceBrowser.prototype.uninit.call(this);
+
+ // "load" event listener is removed in its handler, to
+ // ensure we only fire it once.
+ removeEventListener("unload", this);
+ removeEventListener("AppCommand", this, true);
+ removeEventListener("MozSwipeGesture", this, true);
+ gContextMenu.removeEventListener("popupshowing", this);
+ gContextMenu.removeEventListener("popuphidden", this);
+ Services.els.removeSystemEventListener(this.browser, "dragover", this,
+ true);
+ Services.els.removeSystemEventListener(this.browser, "drop", this, true);
+ },
+
+ /**
+ * Anything added to the messages array will get handled here, and should
+ * get dispatched to a specific function for the message name.
+ */
+ receiveMessage(message) {
+ let data = message.data;
+
+ switch (message.name) {
+ // Begin messages from super class
+ case "ViewSource:PromptAndGoToLine":
+ this.promptAndGoToLine();
+ break;
+ case "ViewSource:GoToLine:Success":
+ this.onGoToLineSuccess(data.lineNumber);
+ break;
+ case "ViewSource:GoToLine:Failed":
+ this.onGoToLineFailed();
+ break;
+ case "ViewSource:StoreWrapping":
+ this.storeWrapping(data.state);
+ break;
+ case "ViewSource:StoreSyntaxHighlighting":
+ this.storeSyntaxHighlighting(data.state);
+ break;
+ // End messages from super class
+ case "ViewSource:SourceLoaded":
+ this.onSourceLoaded();
+ break;
+ case "ViewSource:SourceUnloaded":
+ this.onSourceUnloaded();
+ break;
+ case "ViewSource:Close":
+ this.close();
+ break;
+ case "ViewSource:OpenURL":
+ this.openURL(data.URL);
+ break;
+ case "ViewSource:UpdateStatus":
+ this.updateStatus(data.label);
+ break;
+ case "ViewSource:ContextMenuOpening":
+ this.onContextMenuOpening(data.isLink, data.isEmail, data.href);
+ if (this.browser.isRemoteBrowser) {
+ this.openContextMenu(data.screenX, data.screenY);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Any events should get handled here, and should get dispatched to
+ * a specific function for the event type.
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "load":
+ this.onXULLoaded();
+ break;
+ case "AppCommand":
+ this.onAppCommand(event);
+ break;
+ case "MozSwipeGesture":
+ this.onSwipeGesture(event);
+ break;
+ case "popupshowing":
+ this.onContextMenuShowing(event);
+ break;
+ case "popuphidden":
+ this.onContextMenuHidden(event);
+ break;
+ case "dragover":
+ this.onDragOver(event);
+ break;
+ case "drop":
+ this.onDrop(event);
+ break;
+ }
+ },
+
+ /**
+ * Getter that returns whether or not the view source browser
+ * has history enabled on it.
+ */
+ get historyEnabled() {
+ return !this.browser.hasAttribute("disablehistory");
+ },
+
+ /**
+ * Getter for the message manager used to communicate with the view source
+ * browser.
+ *
+ * In this window version of view source, we use the window message manager
+ * for loading scripts and listening for messages so that if we switch
+ * remoteness of the browser (which we might do if we're attempting to load
+ * the document source out of the network cache), we automatically re-load
+ * the frame script.
+ */
+ get mm() {
+ return window.messageManager;
+ },
+
+ /**
+ * Getter for the nsIWebNavigation of the view source browser.
+ */
+ get webNav() {
+ return this.browser.webNavigation;
+ },
+
+ /**
+ * Send the browser forward in its history.
+ */
+ goForward() {
+ this.browser.goForward();
+ },
+
+ /**
+ * Send the browser backward in its history.
+ */
+ goBack() {
+ this.browser.goBack();
+ },
+
+ /**
+ * This should be called once when the DOM has finished loading. Here we
+ * set the state of various menu items, and add event listeners to
+ * DOM nodes.
+ *
+ * This is also the place where we handle any arguments that have been
+ * passed to viewSource.xul.
+ *
+ * Modern consumers should pass a single object argument to viewSource.xul:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser:
+ * The browser containing the document that we would like to view the
+ * source of. This argument is optional if outerWindowID is not passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. This is the only way of attempting to
+ * load the source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ *
+ * The deprecated API has the opener pass in a number of arguments:
+ *
+ * arg[0] - URL string.
+ * arg[1] - Charset value string in the form 'charset=xxx'.
+ * arg[2] - Page descriptor from nsIWebPageDescriptor used to load content
+ * from the cache.
+ * arg[3] - Line number to go to.
+ * arg[4] - Boolean for whether charset was forced by the user
+ */
+ onXULLoaded() {
+ // This handler should only ever run the first time the XUL is loaded.
+ removeEventListener("load", this);
+
+ let wrapMenuItem = document.getElementById("menu_wrapLongLines");
+ if (this.shouldWrap) {
+ wrapMenuItem.setAttribute("checked", "true");
+ }
+
+ let highlightMenuItem = document.getElementById("menu_highlightSyntax");
+ if (this.shouldHighlight) {
+ highlightMenuItem.setAttribute("checked", "true");
+ }
+
+ gContextMenu.addEventListener("popupshowing", this);
+ gContextMenu.addEventListener("popuphidden", this);
+
+ Services.els.addSystemEventListener(this.browser, "dragover", this, true);
+ Services.els.addSystemEventListener(this.browser, "drop", this, true);
+
+ if (!this.historyEnabled) {
+ // Disable the BACK and FORWARD commands and hide the related menu items.
+ let viewSourceNavigation = document.getElementById("viewSourceNavigation");
+ if (viewSourceNavigation) {
+ viewSourceNavigation.setAttribute("disabled", "true");
+ viewSourceNavigation.setAttribute("hidden", "true");
+ }
+ }
+
+ // We require the first argument to do any loading of source.
+ // otherwise, we're done.
+ if (!window.arguments[0]) {
+ return undefined;
+ }
+
+ if (typeof window.arguments[0] == "string") {
+ // We're using the deprecated API
+ return this._loadViewSourceDeprecated(window.arguments);
+ }
+
+ // We're using the modern API, which allows us to view the
+ // source of documents from out of process browsers.
+ let args = window.arguments[0];
+
+ // viewPartialSource.js will take care of loading the content in partial mode.
+ if (!args.partial) {
+ this.loadViewSource(args);
+ }
+
+ return undefined;
+ },
+
+ /**
+ * This is the deprecated API for viewSource.xul, for old-timer consumers.
+ * This API might eventually go away.
+ */
+ _loadViewSourceDeprecated(aArguments) {
+ Deprecated.warning("The arguments you're passing to viewSource.xul " +
+ "are using an out-of-date API.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ // Parse the 'arguments' supplied with the dialog.
+ // arg[0] - URL string.
+ // arg[1] - Charset value in the form 'charset=xxx'.
+ // arg[2] - Page descriptor used to load content from the cache.
+ // arg[3] - Line number to go to.
+ // arg[4] - Whether charset was forced by the user
+
+ if (aArguments[2]) {
+ let pageDescriptor = aArguments[2];
+ if (Cu.isCrossProcessWrapper(pageDescriptor)) {
+ throw new Error("Cannot pass a CPOW as the page descriptor to viewSource.xul.");
+ }
+ }
+
+ if (this.browser.isRemoteBrowser) {
+ throw new Error("Deprecated view source API should not use a remote browser.");
+ }
+
+ let forcedCharSet;
+ if (aArguments[4] && aArguments[1].startsWith("charset=")) {
+ forcedCharSet = aArguments[1].split("=")[1];
+ }
+
+ this.sendAsyncMessage("ViewSource:LoadSourceDeprecated", {
+ URL: aArguments[0],
+ lineNumber: aArguments[3],
+ forcedCharSet,
+ }, {
+ pageDescriptor: aArguments[2],
+ });
+ },
+
+ /**
+ * Handler for the AppCommand event.
+ *
+ * @param event
+ * The AppCommand event being handled.
+ */
+ onAppCommand(event) {
+ event.stopPropagation();
+ switch (event.command) {
+ case "Back":
+ this.goBack();
+ break;
+ case "Forward":
+ this.goForward();
+ break;
+ }
+ },
+
+ /**
+ * Handler for the MozSwipeGesture event.
+ *
+ * @param event
+ * The MozSwipeGesture event being handled.
+ */
+ onSwipeGesture(event) {
+ event.stopPropagation();
+ switch (event.direction) {
+ case SimpleGestureEvent.DIRECTION_LEFT:
+ this.goBack();
+ break;
+ case SimpleGestureEvent.DIRECTION_RIGHT:
+ this.goForward();
+ break;
+ case SimpleGestureEvent.DIRECTION_UP:
+ goDoCommand("cmd_scrollTop");
+ break;
+ case SimpleGestureEvent.DIRECTION_DOWN:
+ goDoCommand("cmd_scrollBottom");
+ break;
+ }
+ },
+
+ /**
+ * Called as soon as the frame script reports that some source
+ * code has been loaded in the browser.
+ */
+ onSourceLoaded() {
+ document.getElementById("cmd_goToLine").removeAttribute("disabled");
+
+ if (this.historyEnabled) {
+ this.updateCommands();
+ }
+
+ this.browser.focus();
+ },
+
+ /**
+ * Called as soon as the frame script reports that some source
+ * code has been unloaded from the browser.
+ */
+ onSourceUnloaded() {
+ // Disable "go to line" while reloading due to e.g. change of charset
+ // or toggling of syntax highlighting.
+ document.getElementById("cmd_goToLine").setAttribute("disabled", "true");
+ },
+
+ /**
+ * Called by clicks on a menu populated by CharsetMenu.jsm to
+ * change the selected character set.
+ *
+ * @param event
+ * The click event on a character set menuitem.
+ */
+ onSetCharacterSet(event) {
+ if (event.target.hasAttribute("charset")) {
+ let charset = event.target.getAttribute("charset");
+
+ // If we don't have history enabled, we have to do a reload in order to
+ // show the character set change. See bug 136322.
+ this.sendAsyncMessage("ViewSource:SetCharacterSet", {
+ charset: charset,
+ doPageLoad: this.historyEnabled,
+ });
+
+ if (!this.historyEnabled) {
+ this.browser
+ .reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ }
+ }
+ },
+
+ /**
+ * Called from the frame script when the context menu is about to
+ * open. This tells ViewSourceChrome things about the item that
+ * the context menu is being opened on. This should be called before
+ * the popupshowing event handler fires.
+ */
+ onContextMenuOpening(isLink, isEmail, href) {
+ this.contextMenuData = { isLink, isEmail, href, isOpen: true };
+ },
+
+ /**
+ * Event handler for the popupshowing event on the context menu.
+ * This handler is responsible for setting the state on various
+ * menu items in the context menu, and reads values that were sent
+ * up from the frame script and stashed into this.contextMenuData.
+ *
+ * @param event
+ * The popupshowing event for the context menu.
+ */
+ onContextMenuShowing(event) {
+ let copyLinkMenuItem = document.getElementById("context-copyLink");
+ copyLinkMenuItem.hidden = !this.contextMenuData.isLink;
+
+ let copyEmailMenuItem = document.getElementById("context-copyEmail");
+ copyEmailMenuItem.hidden = !this.contextMenuData.isEmail;
+ },
+
+ /**
+ * Called when the user chooses the "Copy Link" or "Copy Email"
+ * menu items in the context menu. Copies the relevant selection
+ * into the system clipboard.
+ */
+ onContextMenuCopyLinkOrEmail() {
+ // It doesn't make any sense to call this if the context menu
+ // isn't open...
+ if (!this.contextMenuData.isOpen) {
+ return;
+ }
+
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this.contextMenuData.href);
+ },
+
+ /**
+ * Called when the context menu closes, and invalidates any data
+ * that the frame script might have sent up about what the context
+ * menu was opened on.
+ */
+ onContextMenuHidden(event) {
+ this.contextMenuData = {
+ isOpen: false,
+ };
+ },
+
+ /**
+ * Called when the user drags something over the content browser.
+ */
+ onDragOver(event) {
+ // For drags that appear to be internal text (for example, tab drags),
+ // set the dropEffect to 'none'. This prevents the drop even if some
+ // other listener cancelled the event.
+ let types = event.dataTransfer.types;
+ if (types.includes("text/x-moz-text-internal") && !types.includes("text/plain")) {
+ event.dataTransfer.dropEffect = "none";
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ let linkHandler = Cc["@mozilla.org/content/dropped-link-handler;1"]
+ .getService(Ci.nsIDroppedLinkHandler);
+
+ if (linkHandler.canDropLink(event, false)) {
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Called twhen the user drops something onto the content browser.
+ */
+ onDrop(event) {
+ if (event.defaultPrevented)
+ return;
+
+ let name = { };
+ let linkHandler = Cc["@mozilla.org/content/dropped-link-handler;1"]
+ .getService(Ci.nsIDroppedLinkHandler);
+ let uri;
+ try {
+ // Pass true to prevent the dropping of javascript:/data: URIs
+ uri = linkHandler.dropLink(event, name, true);
+ } catch (e) {
+ return;
+ }
+
+ if (uri) {
+ this.loadURL(uri);
+ }
+ },
+
+ /**
+ * For remote browsers, the contextmenu event is received in the
+ * content process, and a message is sent up from the frame script
+ * to ViewSourceChrome, but then it stops. The event does not bubble
+ * up to the point that the popup is opened in the parent process.
+ * ViewSourceChrome is responsible for opening up the context menu in
+ * that case. This is called when we receive the contextmenu message
+ * from the child, and we know that the browser is currently remote.
+ *
+ * @param screenX
+ * The screenX coordinate to open the popup at.
+ * @param screenY
+ * The screenY coordinate to open the popup at.
+ */
+ openContextMenu(screenX, screenY) {
+ gContextMenu.openPopupAtScreen(screenX, screenY, true);
+ },
+
+ /**
+ * Loads the source of a URL. This will probably end up hitting the
+ * network.
+ *
+ * @param URL
+ * A URL string to be opened in the view source browser.
+ */
+ loadURL(URL) {
+ this.sendAsyncMessage("ViewSource:LoadSource", { URL });
+ },
+
+ /**
+ * Updates any commands that are dependant on command broadcasters.
+ */
+ updateCommands() {
+ let backBroadcaster = document.getElementById("Browser:Back");
+ let forwardBroadcaster = document.getElementById("Browser:Forward");
+
+ if (this.webNav.canGoBack) {
+ backBroadcaster.removeAttribute("disabled");
+ } else {
+ backBroadcaster.setAttribute("disabled", "true");
+ }
+ if (this.webNav.canGoForward) {
+ forwardBroadcaster.removeAttribute("disabled");
+ } else {
+ forwardBroadcaster.setAttribute("disabled", "true");
+ }
+ },
+
+ /**
+ * Updates the status displayed in the status bar of the view source window.
+ *
+ * @param label
+ * The string to be displayed in the statusbar-lin-col element.
+ */
+ updateStatus(label) {
+ let statusBarField = document.getElementById("statusbar-line-col");
+ if (statusBarField) {
+ statusBarField.label = label;
+ }
+ },
+
+ /**
+ * Called when the frame script reports that a line was successfully gotten
+ * to.
+ *
+ * @param lineNumber
+ * The line number that we successfully got to.
+ */
+ onGoToLineSuccess(lineNumber) {
+ ViewSourceBrowser.prototype.onGoToLineSuccess.call(this, lineNumber);
+ document.getElementById("statusbar-line-col").label =
+ gViewSourceBundle.getFormattedString("statusBarLineCol", [lineNumber, 1]);
+ },
+
+ /**
+ * Reloads the browser, bypassing the network cache.
+ */
+ reload() {
+ this.browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ },
+
+ /**
+ * Closes the view source window.
+ */
+ close() {
+ window.close();
+ },
+
+ /**
+ * Called when the user clicks on the "Wrap Long Lines" menu item.
+ */
+ toggleWrapping() {
+ this.shouldWrap = !this.shouldWrap;
+ this.sendAsyncMessage("ViewSource:ToggleWrapping");
+ },
+
+ /**
+ * Called when the user clicks on the "Syntax Highlighting" menu item.
+ */
+ toggleSyntaxHighlighting() {
+ this.shouldHighlight = !this.shouldHighlight;
+ this.sendAsyncMessage("ViewSource:ToggleSyntaxHighlighting");
+ },
+
+ /**
+ * Updates the "remote" attribute of the view source browser. This
+ * will remove the browser from the DOM, and then re-add it in the
+ * same place it was taken from.
+ *
+ * @param shouldBeRemote
+ * True if the browser should be made remote. If the browsers
+ * remoteness already matches this value, this function does
+ * nothing.
+ */
+ updateBrowserRemoteness(shouldBeRemote) {
+ if (this.browser.isRemoteBrowser == shouldBeRemote) {
+ return;
+ }
+
+ let parentNode = this.browser.parentNode;
+ let nextSibling = this.browser.nextSibling;
+
+ // XX Removing and re-adding the browser from and to the DOM strips its
+ // XBL properties. Save and restore relatedBrowser. Note that when we
+ // restore relatedBrowser, there won't yet be a binding or setter. This
+ // works in conjunction with the hack in <xul:browser>'s constructor to
+ // re-get the weak reference to it.
+ let relatedBrowser = this.browser.relatedBrowser;
+
+ this.browser.remove();
+ if (shouldBeRemote) {
+ this.browser.setAttribute("remote", "true");
+ } else {
+ this.browser.removeAttribute("remote");
+ }
+
+ this.browser.relatedBrowser = relatedBrowser;
+
+ // If nextSibling was null, this will put the browser at
+ // the end of the list.
+ parentNode.insertBefore(this.browser, nextSibling);
+
+ if (shouldBeRemote) {
+ // We're going to send a message down to the remote browser
+ // to load the source content - however, in order for the
+ // contentWindowAsCPOW and contentDocumentAsCPOW values on
+ // the remote browser to be set, we must set up the
+ // RemoteWebProgress, which is lazily loaded. We only need
+ // contentWindowAsCPOW for the printing support, and this
+ // should go away once bug 1146454 is fixed, since we can
+ // then just pass the outerWindowID of the this.browser to
+ // PrintUtils.
+ this.browser.webProgress;
+ }
+ },
+};
+
+var viewSourceChrome = new ViewSourceChrome();
+
+/**
+ * PrintUtils uses this to make Print Preview work.
+ */
+var PrintPreviewListener = {
+ _ppBrowser: null,
+
+ getPrintPreviewBrowser() {
+ if (!this._ppBrowser) {
+ this._ppBrowser = document.createElement("browser");
+ this._ppBrowser.setAttribute("flex", "1");
+ this._ppBrowser.setAttribute("type", "content");
+ }
+
+ if (gBrowser.isRemoteBrowser) {
+ this._ppBrowser.setAttribute("remote", "true");
+ } else {
+ this._ppBrowser.removeAttribute("remote");
+ }
+
+ let findBar = document.getElementById("FindToolbar");
+ document.getElementById("appcontent")
+ .insertBefore(this._ppBrowser, findBar);
+
+ return this._ppBrowser;
+ },
+
+ getSourceBrowser() {
+ return gBrowser;
+ },
+
+ getNavToolbox() {
+ return document.getElementById("appcontent");
+ },
+
+ onEnter() {
+ let toolbox = document.getElementById("viewSource-toolbox");
+ toolbox.hidden = true;
+ gBrowser.collapsed = true;
+ },
+
+ onExit() {
+ this._ppBrowser.remove();
+ gBrowser.collapsed = false;
+ document.getElementById("viewSource-toolbox").hidden = false;
+ },
+
+ activateBrowser(browser) {
+ browser.docShellIsActive = true;
+ },
+};
+
+// viewZoomOverlay.js uses this
+function getBrowser() {
+ return gBrowser;
+}
+
+this.__defineGetter__("gPageLoader", function () {
+ var webnav = viewSourceChrome.webNav;
+ if (!webnav)
+ return null;
+ delete this.gPageLoader;
+ this.gPageLoader = (webnav instanceof Ci.nsIWebPageDescriptor) ? webnav
+ : null;
+ return this.gPageLoader;
+});
+
+// Strips the |view-source:| for internalSave()
+function ViewSourceSavePage()
+{
+ internalSave(gBrowser.currentURI.spec.replace(/^view-source:/i, ""),
+ null, null, null, null, null, "SaveLinkTitle",
+ null, null, gBrowser.contentDocumentAsCPOW, null,
+ gPageLoader);
+}
+
+// Below are old deprecated functions and variables left behind for
+// compatibility reasons. These will be removed soon via bug 1159293.
+
+this.__defineGetter__("gLastLineFound", function () {
+ Deprecated.warning("gLastLineFound is deprecated - please use " +
+ "viewSourceChrome.lastLineFound instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ return viewSourceChrome.lastLineFound;
+});
+
+function onLoadViewSource() {
+ Deprecated.warning("onLoadViewSource() is deprecated - please use " +
+ "viewSourceChrome.onXULLoaded() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.onXULLoaded();
+}
+
+function isHistoryEnabled() {
+ Deprecated.warning("isHistoryEnabled() is deprecated - please use " +
+ "viewSourceChrome.historyEnabled instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ return viewSourceChrome.historyEnabled;
+}
+
+function ViewSourceClose() {
+ Deprecated.warning("ViewSourceClose() is deprecated - please use " +
+ "viewSourceChrome.close() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.close();
+}
+
+function ViewSourceReload() {
+ Deprecated.warning("ViewSourceReload() is deprecated - please use " +
+ "viewSourceChrome.reload() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.reload();
+}
+
+function getWebNavigation()
+{
+ Deprecated.warning("getWebNavigation() is deprecated - please use " +
+ "viewSourceChrome.webNav instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ // The original implementation returned null if anything threw during
+ // the getting of the webNavigation.
+ try {
+ return viewSourceChrome.webNav;
+ } catch (e) {
+ return null;
+ }
+}
+
+function viewSource(url) {
+ Deprecated.warning("viewSource() is deprecated - please use " +
+ "viewSourceChrome.loadURL() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.loadURL(url);
+}
+
+function ViewSourceGoToLine()
+{
+ Deprecated.warning("ViewSourceGoToLine() is deprecated - please use " +
+ "viewSourceChrome.promptAndGoToLine() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.promptAndGoToLine();
+}
+
+function goToLine(line)
+{
+ Deprecated.warning("goToLine() is deprecated - please use " +
+ "viewSourceChrome.goToLine() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.goToLine(line);
+}
+
+function BrowserForward(aEvent) {
+ Deprecated.warning("BrowserForward() is deprecated - please use " +
+ "viewSourceChrome.goForward() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.goForward();
+}
+
+function BrowserBack(aEvent) {
+ Deprecated.warning("BrowserBack() is deprecated - please use " +
+ "viewSourceChrome.goBack() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.goBack();
+}
+
+function UpdateBackForwardCommands() {
+ Deprecated.warning("UpdateBackForwardCommands() is deprecated - please use " +
+ "viewSourceChrome.updateCommands() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.updateCommands();
+}
diff --git a/toolkit/components/viewsource/content/viewSource.xul b/toolkit/components/viewsource/content/viewSource.xul
new file mode 100644
index 0000000000..c6ca58234e
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource.xul
@@ -0,0 +1,235 @@
+<?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/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/viewsource/viewsource.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % sourceDTD SYSTEM "chrome://global/locale/viewSource.dtd" >
+%sourceDTD;
+<!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >
+%charsetDTD;
+]>
+
+<window id="viewSource"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ contenttitlesetting="true"
+ title="&mainWindow.title;"
+ titlemodifier="&mainWindow.titlemodifier;"
+ titlepreface="&mainWindow.preface;"
+ titlemenuseparator ="&mainWindow.titlemodifierseparator;"
+ windowtype="navigator:view-source"
+ width="640" height="480"
+ screenX="10" screenY="10"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
+ <script type="application/javascript" src="chrome://global/content/viewSource.js"/>
+ <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+
+ <stringbundle id="viewSourceBundle" src="chrome://global/locale/viewSource.properties"/>
+
+ <command id="cmd_savePage" oncommand="ViewSourceSavePage();"/>
+ <command id="cmd_print" oncommand="PrintUtils.printWindow(gBrowser.outerWindowID, gBrowser);"/>
+ <command id="cmd_printpreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/>
+ <command id="cmd_pagesetup" oncommand="PrintUtils.showPageSetup();"/>
+ <command id="cmd_close" oncommand="window.close();"/>
+ <commandset id="editMenuCommands"/>
+ <command id="cmd_find"
+ oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
+ <command id="cmd_findAgain"
+ oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
+ <command id="cmd_findPrevious"
+ oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
+#ifdef XP_MACOSX
+ <command id="cmd_findSelection"
+ oncommand="document.getElementById('FindToolbar').onFindSelectionCommand();"/>
+#endif
+ <command id="cmd_reload" oncommand="viewSourceChrome.reload();"/>
+ <command id="cmd_goToLine" oncommand="viewSourceChrome.promptAndGoToLine();" disabled="true"/>
+ <command id="cmd_highlightSyntax" oncommand="viewSourceChrome.toggleSyntaxHighlighting();"/>
+ <command id="cmd_wrapLongLines" oncommand="viewSourceChrome.toggleWrapping();"/>
+ <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
+ <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
+ <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
+
+ <command id="Browser:Back" oncommand="viewSourceChrome.goBack()" observes="viewSourceNavigation"/>
+ <command id="Browser:Forward" oncommand="viewSourceChrome.goForward()" observes="viewSourceNavigation"/>
+
+ <broadcaster id="viewSourceNavigation"/>
+
+ <keyset id="editMenuKeys"/>
+ <keyset id="viewSourceKeys">
+ <key id="key_savePage" key="&savePageCmd.commandkey;" modifiers="accel" command="cmd_savePage"/>
+ <key id="key_print" key="&printCmd.commandkey;" modifiers="accel" command="cmd_print"/>
+ <key id="key_close" key="&closeCmd.commandkey;" modifiers="accel" command="cmd_close"/>
+ <key id="key_goToLine" key="&goToLineCmd.commandkey;" command="cmd_goToLine" modifiers="accel"/>
+
+ <key id="key_textZoomEnlarge" key="&textEnlarge.commandkey;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <key id="key_textZoomEnlarge2" key="&textEnlarge.commandkey2;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <key id="key_textZoomEnlarge3" key="&textEnlarge.commandkey3;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <key id="key_textZoomReduce" key="&textReduce.commandkey;" command="cmd_textZoomReduce" modifiers="accel"/>
+ <key id="key_textZoomReduce2" key="&textReduce.commandkey2;" command="cmd_textZoomReduce" modifiers="accel"/>
+ <key id="key_textZoomReset" key="&textReset.commandkey;" command="cmd_textZoomReset" modifiers="accel"/>
+ <key id="key_textZoomReset2" key="&textReset.commandkey2;" command="cmd_textZoomReset" modifiers="accel"/>
+
+ <key id="key_reload" key="&reloadCmd.commandkey;" command="cmd_reload" modifiers="accel"/>
+ <key key="&reloadCmd.commandkey;" command="cmd_reload" modifiers="accel,shift"/>
+ <key keycode="VK_F5" command="cmd_reload"/>
+ <key keycode="VK_F5" command="cmd_reload" 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"/>
+#ifdef XP_MACOSX
+ <key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
+#endif
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/>
+
+ <key keycode="VK_BACK" command="Browser:Back"/>
+ <key keycode="VK_BACK" command="Browser:Forward" modifiers="shift"/>
+#ifndef XP_MACOSX
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/>
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/>
+#else
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="accel" />
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="accel" />
+#endif
+#ifdef XP_UNIX
+ <key id="goBackKb2" key="&goBackCmd.commandKey;" command="Browser:Back" modifiers="accel"/>
+ <key id="goForwardKb2" key="&goForwardCmd.commandKey;" command="Browser:Forward" modifiers="accel"/>
+#endif
+
+ </keyset>
+
+ <tooltip id="aHTMLTooltip" page="true"/>
+
+ <menupopup id="viewSourceContextMenu">
+ <menuitem id="context-back"
+ label="&backCmd.label;"
+ accesskey="&backCmd.accesskey;"
+ command="Browser:Back"
+ observes="viewSourceNavigation"/>
+ <menuitem id="context-forward"
+ label="&forwardCmd.label;"
+ accesskey="&forwardCmd.accesskey;"
+ command="Browser:Forward"
+ observes="viewSourceNavigation"/>
+ <menuseparator observes="viewSourceNavigation"/>
+ <menuitem id="cMenu_findAgain"/>
+ <menuseparator/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="context-copyLink"
+ label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuitem id="context-copyEmail"
+ label="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+
+ <!-- Menu -->
+ <toolbox id="viewSource-toolbox">
+ <menubar id="viewSource-main-menubar">
+
+ <menu id="menu_file" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="menu_FilePopup">
+ <menuitem key="key_savePage" command="cmd_savePage" id="menu_savePage"
+ label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;"/>
+ <menuitem command="cmd_pagesetup" id="menu_pageSetup"
+ label="&pageSetupCmd.label;" accesskey="&pageSetupCmd.accesskey;"/>
+#ifndef XP_MACOSX
+ <menuitem command="cmd_printpreview" id="menu_printPreview"
+ label="&printPreviewCmd.label;" accesskey="&printPreviewCmd.accesskey;"/>
+#endif
+ <menuitem key="key_print" command="cmd_print" id="menu_print"
+ label="&printCmd.label;" accesskey="&printCmd.accesskey;"/>
+ <menuseparator/>
+ <menuitem key="key_close" command="cmd_close" id="menu_close"
+ label="&closeCmd.label;" accesskey="&closeCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_edit">
+ <menupopup id="editmenu-popup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuitem id="menu_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_find"/>
+ <menuitem id="menu_findAgain"/>
+ <menuseparator/>
+ <menuitem id="menu_goToLine" key="key_goToLine" command="cmd_goToLine"
+ label="&goToLineCmd.label;" accesskey="&goToLineCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_view" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
+ <menupopup id="viewmenu-popup">
+ <menuitem id="menu_reload" command="cmd_reload" accesskey="&reloadCmd.accesskey;"
+ label="&reloadCmd.label;" key="key_reload"/>
+ <menuseparator />
+ <menu id="viewTextZoomMenu" label="&menu_textSize.label;" accesskey="&menu_textSize.accesskey;">
+ <menupopup>
+ <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge"
+ label="&menu_textEnlarge.label;" accesskey="&menu_textEnlarge.accesskey;"
+ key="key_textZoomEnlarge"/>
+ <menuitem id="menu_textReduce" command="cmd_textZoomReduce"
+ label="&menu_textReduce.label;" accesskey="&menu_textReduce.accesskey;"
+ key="key_textZoomReduce"/>
+ <menuseparator/>
+ <menuitem id="menu_textReset" command="cmd_textZoomReset"
+ label="&menu_textReset.label;" accesskey="&menu_textReset.accesskey;"
+ key="key_textZoomReset"/>
+ </menupopup>
+ </menu>
+
+ <!-- Charset Menu -->
+ <menu id="charsetMenu"
+ label="&charsetMenu2.label;"
+ accesskey="&charsetMenu2.accesskey;"
+ oncommand="viewSourceChrome.onSetCharacterSet(event);"
+ onpopupshowing="CharsetMenu.build(event.target);
+ CharsetMenu.update(event.target, content.document.characterSet);">
+ <menupopup/>
+ </menu>
+ <menuseparator/>
+ <menuitem id="menu_wrapLongLines" type="checkbox" command="cmd_wrapLongLines"
+ label="&menu_wrapLongLines.title;" accesskey="&menu_wrapLongLines.accesskey;"/>
+ <menuitem type="checkbox" id="menu_highlightSyntax" command="cmd_highlightSyntax"
+ label="&menu_highlightSyntax.label;" accesskey="&menu_highlightSyntax.accesskey;"/>
+ </menupopup>
+ </menu>
+ </menubar>
+ </toolbox>
+
+ <vbox id="appcontent" flex="1">
+
+ <browser id="content" type="content-primary" name="content" src="about:blank" flex="1"
+ context="viewSourceContextMenu" showcaret="true" tooltip="aHTMLTooltip" />
+ <findbar id="FindToolbar" browserid="content"/>
+ </vbox>
+
+ <statusbar id="status-bar" class="chromeclass-status">
+ <statusbarpanel id="statusbar-line-col" label="" flex="1"/>
+ </statusbar>
+
+</window>
diff --git a/toolkit/components/viewsource/content/viewSourceUtils.js b/toolkit/components/viewsource/content/viewSourceUtils.js
new file mode 100644
index 0000000000..5752683e99
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSourceUtils.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/. */
+
+/*
+ * To keep the global namespace safe, don't define global variables and
+ * functions in this file.
+ *
+ * This file silently depends on contentAreaUtils.js for
+ * getDefaultFileName, getNormalizedLeafName and getDefaultExtension
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser",
+ "resource://gre/modules/ViewSourceBrowser.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+var gViewSourceUtils = {
+
+ mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist,
+ mnsIWebProgress: Components.interfaces.nsIWebProgress,
+ mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor,
+
+ /**
+ * Opens the view source window.
+ *
+ * @param aArgsOrURL (required)
+ * This is either an Object containing parameters, or a string
+ * URL for the page we want to view the source of. In the latter
+ * case we will be paying attention to the other parameters, as
+ * we will be supporting the old API for this method.
+ * If aArgsOrURL is an Object, the other parameters will be ignored.
+ * aArgsOrURL as an Object can include the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ *
+ * @param aPageDescriptor (deprecated, optional)
+ * Accepted for compatibility reasons, but is otherwise ignored.
+ * @param aDocument (deprecated, optional)
+ * The content document we would like to view the source of. This
+ * function will throw if aDocument is a CPOW.
+ * @param aLineNumber (deprecated, optional)
+ * The line number to focus on once the source is loaded.
+ */
+ viewSource: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber)
+ {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ if (prefs.getBoolPref("view_source.editor.external")) {
+ this.openInExternalEditor(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber);
+ } else {
+ this._openInInternalViewer(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber);
+ }
+ },
+
+ /**
+ * Displays view source in the provided <browser>. This allows for non-window
+ * display methods, such as a tab from Firefox.
+ *
+ * @param aArgs
+ * An object with the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * viewSourceBrowser (required):
+ * The browser to display the view source in.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+ viewSourceInBrowser: function(aArgs) {
+ Services.telemetry
+ .getHistogramById("VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN")
+ .add(true);
+ let viewSourceBrowser = new ViewSourceBrowser(aArgs.viewSourceBrowser);
+ viewSourceBrowser.loadViewSource(aArgs);
+ },
+
+ /**
+ * Displays view source for a selection from some document in the provided
+ * <browser>. This allows for non-window display methods, such as a tab from
+ * Firefox.
+ *
+ * @param aViewSourceInBrowser
+ * The browser containing the page to view the source of.
+ * @param aTarget
+ * Set to the target node for MathML. Null for other types of elements.
+ * @param aGetBrowserFn
+ * If set, a function that will return a browser to open the source in.
+ * If null, or this function returns null, opens the source in a new window.
+ */
+ viewPartialSourceInBrowser: function(aViewSourceInBrowser, aTarget, aGetBrowserFn) {
+ let mm = aViewSourceInBrowser.messageManager;
+ mm.addMessageListener("ViewSource:GetSelectionDone", function gotSelection(message) {
+ mm.removeMessageListener("ViewSource:GetSelectionDone", gotSelection);
+
+ if (!message.data)
+ return;
+
+ let browserToOpenIn = aGetBrowserFn ? aGetBrowserFn() : null;
+ if (browserToOpenIn) {
+ let viewSourceBrowser = new ViewSourceBrowser(browserToOpenIn);
+ viewSourceBrowser.loadViewSourceFromSelection(message.data.uri, message.data.drawSelection,
+ message.data.baseURI);
+ }
+ else {
+ window.openDialog("chrome://global/content/viewPartialSource.xul",
+ "_blank", "all,dialog=no",
+ {
+ URI: message.data.uri,
+ drawSelection: message.data.drawSelection,
+ baseURI: message.data.baseURI,
+ partial: true,
+ });
+ }
+ });
+
+ mm.sendAsyncMessage("ViewSource:GetSelection", { }, { target: aTarget });
+ },
+
+ // Opens the interval view source viewer
+ _openInInternalViewer: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber)
+ {
+ // try to open a view-source window while inheriting the charset (if any)
+ var charset = null;
+ var isForcedCharset = false;
+ if (aDocument) {
+ if (Components.utils.isCrossProcessWrapper(aDocument)) {
+ throw new Error("View Source cannot accept a CPOW as a document.");
+ }
+
+ charset = "charset=" + aDocument.characterSet;
+ try {
+ isForcedCharset =
+ aDocument.defaultView
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .docCharsetIsForced;
+ } catch (ex) {
+ }
+ }
+ Services.telemetry
+ .getHistogramById("VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN")
+ .add(true);
+ openDialog("chrome://global/content/viewSource.xul",
+ "_blank",
+ "all,dialog=no",
+ aArgsOrURL, charset, aPageDescriptor, aLineNumber, isForcedCharset);
+ },
+
+ buildEditorArgs: function(aPath, aLineNumber) {
+ // Determine the command line arguments to pass to the editor.
+ // We currently support a %LINE% placeholder which is set to the passed
+ // line number (or to 0 if there's none)
+ var editorArgs = [];
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var args = prefs.getCharPref("view_source.editor.args");
+ if (args) {
+ args = args.replace("%LINE%", aLineNumber || "0");
+ // add the arguments to the array (keeping quoted strings intact)
+ const argumentRE = /"([^"]+)"|(\S+)/g;
+ while (argumentRE.test(args))
+ editorArgs.push(RegExp.$1 || RegExp.$2);
+ }
+ editorArgs.push(aPath);
+ return editorArgs;
+ },
+
+ /**
+ * Opens an external editor with the view source content.
+ *
+ * @param aArgsOrURL (required)
+ * This is either an Object containing parameters, or a string
+ * URL for the page we want to view the source of. In the latter
+ * case we will be paying attention to the other parameters, as
+ * we will be supporting the old API for this method.
+ * If aArgsOrURL is an Object, the other parameters will be ignored.
+ * aArgsOrURL as an Object can include the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ *
+ * @param aPageDescriptor (deprecated, optional)
+ * Accepted for compatibility reasons, but is otherwise ignored.
+ * @param aDocument (deprecated, optional)
+ * The content document we would like to view the source of. This
+ * function will throw if aDocument is a CPOW.
+ * @param aLineNumber (deprecated, optional)
+ * The line number to focus on once the source is loaded.
+ * @param aCallBack
+ * A function accepting two arguments:
+ * * result (true = success)
+ * * data object
+ * The function defaults to opening an internal viewer if external
+ * viewing fails.
+ */
+ openInExternalEditor: function(aArgsOrURL, aPageDescriptor, aDocument,
+ aLineNumber, aCallBack) {
+ let data;
+ if (typeof aArgsOrURL == "string") {
+ Deprecated.warning("The arguments you're passing to " +
+ "openInExternalEditor are using an out-of-date API.",
+ "https://developer.mozilla.org/en-US/Add-ons/" +
+ "Code_snippets/View_Source_for_XUL_Applications");
+ if (Components.utils.isCrossProcessWrapper(aDocument)) {
+ throw new Error("View Source cannot accept a CPOW as a document.");
+ }
+ data = {
+ url: aArgsOrURL,
+ pageDescriptor: aPageDescriptor,
+ doc: aDocument,
+ lineNumber: aLineNumber,
+ isPrivate: false,
+ };
+ if (aDocument) {
+ data.isPrivate =
+ PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
+ }
+ } else {
+ let { URL, browser, lineNumber } = aArgsOrURL;
+ data = {
+ url: URL,
+ lineNumber,
+ isPrivate: false,
+ };
+ if (browser) {
+ data.doc = {
+ characterSet: browser.characterSet,
+ contentType: browser.documentContentType,
+ title: browser.contentTitle,
+ };
+ data.isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
+ }
+ }
+
+ try {
+ var editor = this.getExternalViewSourceEditor();
+ if (!editor) {
+ this.handleCallBack(aCallBack, false, data);
+ return;
+ }
+
+ // make a uri
+ var ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var charset = data.doc ? data.doc.characterSet : null;
+ var uri = ios.newURI(data.url, charset, null);
+ data.uri = uri;
+
+ var path;
+ var contentType = data.doc ? data.doc.contentType : null;
+ if (uri.scheme == "file") {
+ // it's a local file; we can open it directly
+ path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path;
+
+ var editorArgs = this.buildEditorArgs(path, data.lineNumber);
+ editor.runw(false, editorArgs, editorArgs.length);
+ this.handleCallBack(aCallBack, true, data);
+ } else {
+ // set up the progress listener with what we know so far
+ this.viewSourceProgressListener.contentLoaded = false;
+ this.viewSourceProgressListener.editor = editor;
+ this.viewSourceProgressListener.callBack = aCallBack;
+ this.viewSourceProgressListener.data = data;
+ if (!data.pageDescriptor) {
+ // without a page descriptor, loadPage has no chance of working. download the file.
+ var file = this.getTemporaryFile(uri, data.doc, contentType);
+ this.viewSourceProgressListener.file = file;
+
+ var webBrowserPersist = Components
+ .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(this.mnsIWebBrowserPersist);
+ // the default setting is to not decode. we need to decode.
+ webBrowserPersist.persistFlags = this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
+ webBrowserPersist.progressListener = this.viewSourceProgressListener;
+ let referrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER;
+ webBrowserPersist.savePrivacyAwareURI(uri, null, null, referrerPolicy, null, null, file, data.isPrivate);
+
+ let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Components.interfaces.nsPIExternalAppLauncher);
+ if (data.isPrivate) {
+ // register the file to be deleted when possible
+ helperService.deleteTemporaryPrivateFileWhenPossible(file);
+ } else {
+ // register the file to be deleted on app exit
+ helperService.deleteTemporaryFileOnExit(file);
+ }
+ } else {
+ // we'll use nsIWebPageDescriptor to get the source because it may
+ // not have to refetch the file from the server
+ // XXXbz this is so broken... This code doesn't set up this docshell
+ // at all correctly; if somehow the view-source stuff managed to
+ // execute script we'd be in big trouble here, I suspect.
+ var webShell = Components.classes["@mozilla.org/docshell;1"].createInstance();
+ webShell.QueryInterface(Components.interfaces.nsIBaseWindow).create();
+ this.viewSourceProgressListener.webShell = webShell;
+ var progress = webShell.QueryInterface(this.mnsIWebProgress);
+ progress.addProgressListener(this.viewSourceProgressListener,
+ this.mnsIWebProgress.NOTIFY_STATE_DOCUMENT);
+ var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor);
+ pageLoader.loadPage(data.pageDescriptor, this.mnsIWebPageDescriptor.DISPLAY_AS_SOURCE);
+ }
+ }
+ } catch (ex) {
+ // we failed loading it with the external editor.
+ Components.utils.reportError(ex);
+ this.handleCallBack(aCallBack, false, data);
+ return;
+ }
+ },
+
+ // Default callback - opens the internal viewer if the external editor failed
+ internalViewerFallback: function(result, data)
+ {
+ if (!result) {
+ this._openInInternalViewer(data.url, data.pageDescriptor, data.doc, data.lineNumber);
+ }
+ },
+
+ // Calls the callback, keeping in mind undefined or null values.
+ handleCallBack: function(aCallBack, result, data)
+ {
+ Services.telemetry
+ .getHistogramById("VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN")
+ .add(result);
+ // if callback is undefined, default to the internal viewer
+ if (aCallBack === undefined) {
+ this.internalViewerFallback(result, data);
+ } else if (aCallBack) {
+ aCallBack(result, data);
+ }
+ },
+
+ // Returns nsIProcess of the external view source editor or null
+ getExternalViewSourceEditor: function()
+ {
+ try {
+ let viewSourceAppPath =
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch)
+ .getComplexValue("view_source.editor.path",
+ Components.interfaces.nsIFile);
+ let editor = Components.classes['@mozilla.org/process/util;1']
+ .createInstance(Components.interfaces.nsIProcess);
+ editor.init(viewSourceAppPath);
+
+ return editor;
+ }
+ catch (ex) {
+ Components.utils.reportError(ex);
+ }
+
+ return null;
+ },
+
+ viewSourceProgressListener: {
+
+ mnsIWebProgressListener: Components.interfaces.nsIWebProgressListener,
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(this.mnsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ destroy: function() {
+ if (this.webShell) {
+ this.webShell.QueryInterface(Components.interfaces.nsIBaseWindow).destroy();
+ }
+ this.webShell = null;
+ this.editor = null;
+ this.callBack = null;
+ this.data = null;
+ this.file = null;
+ },
+
+ // This listener is used both for tracking the progress of an HTML parse
+ // in one case and for tracking the progress of nsIWebBrowserPersist in
+ // another case.
+ onStateChange: function(aProgress, aRequest, aFlag, aStatus) {
+ // once it's done loading...
+ if ((aFlag & this.mnsIWebProgressListener.STATE_STOP) && aStatus == 0) {
+ if (!this.webShell) {
+ // We aren't waiting for the parser. Instead, we are waiting for
+ // an nsIWebBrowserPersist.
+ this.onContentLoaded();
+ return 0;
+ }
+ var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ if (webNavigation.document.readyState == "complete") {
+ // This branch is probably never taken. Including it for completeness.
+ this.onContentLoaded();
+ } else {
+ webNavigation.document.addEventListener("DOMContentLoaded",
+ this.onContentLoaded.bind(this));
+ }
+ }
+ return 0;
+ },
+
+ onContentLoaded: function() {
+ // The progress listener may call this multiple times, so be sure we only
+ // run once.
+ if (this.contentLoaded) {
+ return;
+ }
+ try {
+ if (!this.file) {
+ // it's not saved to file yet, it's in the webshell
+
+ // get a temporary filename using the attributes from the data object that
+ // openInExternalEditor gave us
+ this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc,
+ this.data.doc.contentType);
+
+ // we have to convert from the source charset.
+ var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Components.interfaces.nsIFileOutputStream);
+ foStream.init(this.file, 0x02 | 0x08 | 0x20, -1, 0); // write | create | truncate
+ var coStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+ coStream.init(foStream, this.data.doc.characterSet, 0, null);
+
+ // write the source to the file
+ coStream.writeString(webNavigation.document.body.textContent);
+
+ // clean up
+ coStream.close();
+ foStream.close();
+
+ let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Components.interfaces.nsPIExternalAppLauncher);
+ if (this.data.isPrivate) {
+ // register the file to be deleted when possible
+ helperService.deleteTemporaryPrivateFileWhenPossible(this.file);
+ } else {
+ // register the file to be deleted on app exit
+ helperService.deleteTemporaryFileOnExit(this.file);
+ }
+ }
+
+ var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path,
+ this.data.lineNumber);
+ this.editor.runw(false, editorArgs, editorArgs.length);
+
+ this.contentLoaded = true;
+ gViewSourceUtils.handleCallBack(this.callBack, true, this.data);
+ } catch (ex) {
+ // we failed loading it with the external editor.
+ Components.utils.reportError(ex);
+ gViewSourceUtils.handleCallBack(this.callBack, false, this.data);
+ } finally {
+ this.destroy();
+ }
+ },
+
+ onLocationChange: function() { return 0; },
+ onProgressChange: function() { return 0; },
+ onStatusChange: function() { return 0; },
+ onSecurityChange: function() { return 0; },
+
+ webShell: null,
+ editor: null,
+ callBack: null,
+ data: null,
+ file: null
+ },
+
+ // returns an nsIFile for the passed document in the system temp directory
+ getTemporaryFile: function(aURI, aDocument, aContentType) {
+ // include contentAreaUtils.js in our own context when we first need it
+ if (!this._caUtils) {
+ var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader);
+ this._caUtils = {};
+ scriptLoader.loadSubScript("chrome://global/content/contentAreaUtils.js", this._caUtils);
+ }
+
+ var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties);
+ var tempFile = fileLocator.get("TmpD", Components.interfaces.nsIFile);
+ var fileName = this._caUtils.getDefaultFileName(null, aURI, aDocument, aContentType);
+ var extension = this._caUtils.getDefaultExtension(fileName, aURI, aContentType);
+ var leafName = this._caUtils.getNormalizedLeafName(fileName, extension);
+ tempFile.append(leafName);
+ return tempFile;
+ }
+}
diff --git a/toolkit/components/viewsource/jar.mn b/toolkit/components/viewsource/jar.mn
new file mode 100644
index 0000000000..00a1f19a46
--- /dev/null
+++ b/toolkit/components/viewsource/jar.mn
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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/viewSource.css (content/viewSource.css)
+ content/global/viewSource.js (content/viewSource.js)
+* content/global/viewSource.xul (content/viewSource.xul)
+ content/global/viewPartialSource.js (content/viewPartialSource.js)
+* content/global/viewPartialSource.xul (content/viewPartialSource.xul)
+ content/global/viewSourceUtils.js (content/viewSourceUtils.js)
+ content/global/viewSource-content.js (content/viewSource-content.js)
diff --git a/toolkit/components/viewsource/moz.build b/toolkit/components/viewsource/moz.build
new file mode 100644
index 0000000000..aecd256829
--- /dev/null
+++ b/toolkit/components/viewsource/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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+ 'ViewSourceBrowser.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'View Source')
diff --git a/toolkit/components/viewsource/test/.eslintrc.js b/toolkit/components/viewsource/test/.eslintrc.js
new file mode 100644
index 0000000000..2c669d844e
--- /dev/null
+++ b/toolkit/components/viewsource/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/viewsource/test/browser/.eslintrc.js b/toolkit/components/viewsource/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..7c80211924
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/viewsource/test/browser/browser.ini b/toolkit/components/viewsource/test/browser/browser.ini
new file mode 100644
index 0000000000..d9ebbd25f9
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files = head.js
+ file_bug464222.html
+
+[browser_bug464222.js]
+[browser_bug699356.js]
+[browser_bug713810.js]
+[browser_contextmenu.js]
+subsuite = clipboard
+[browser_gotoline.js]
+[browser_srcdoc.js]
+[browser_viewsourceprefs.js]
diff --git a/toolkit/components/viewsource/test/browser/browser_bug464222.js b/toolkit/components/viewsource/test/browser/browser_bug464222.js
new file mode 100644
index 0000000000..30c4fb67a2
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug464222.js
@@ -0,0 +1,12 @@
+const source = "http://example.com/browser/toolkit/components/viewsource/test/browser/file_bug464222.html";
+
+add_task(function *() {
+ let viewSourceTab = yield* openDocumentSelect(source, "a");
+
+ let href = yield ContentTask.spawn(viewSourceTab.linkedBrowser, { }, function* () {
+ return content.document.querySelectorAll("a[href]")[0].href;
+ });
+
+ is(href, "view-source:" + source, "Relative links broken?");
+ gBrowser.removeTab(viewSourceTab);
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_bug699356.js b/toolkit/components/viewsource/test/browser/browser_bug699356.js
new file mode 100644
index 0000000000..e55c4cf205
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug699356.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ let source = "about:blank";
+
+ waitForExplicitFinish();
+ openViewSourceWindow(source, function(aWindow) {
+ let gBrowser = aWindow.gBrowser;
+ let docEl = aWindow.document.documentElement;
+
+ is(gBrowser.contentDocument.title, source, "Correct document title");
+ is(docEl.getAttribute("title"),
+ "Source of: " + source + ("nsILocalFileMac" in Components.interfaces ? "" : " - " + docEl.getAttribute("titlemodifier")),
+ "Correct window title");
+ closeViewSourceWindow(aWindow, finish);
+ });
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_bug713810.js b/toolkit/components/viewsource/test/browser/browser_bug713810.js
new file mode 100644
index 0000000000..d5b2f3424f
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug713810.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const source = '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>This is a paragraph.</p></body></html>';
+
+add_task(function *() {
+ let viewSourceTab = yield* openDocumentSelect("data:text/html," + source, "p");
+ yield ContentTask.spawn(viewSourceTab.linkedBrowser, null, function* () {
+ Assert.equal(content.document.body.textContent, "<p>This is a paragraph.</p>",
+ "Correct source for text/html");
+ });
+ gBrowser.removeTab(viewSourceTab);
+
+ viewSourceTab = yield* openDocumentSelect("data:application/xhtml+xml," + source, "p");
+ yield ContentTask.spawn(viewSourceTab.linkedBrowser, null, function* () {
+ Assert.equal(content.document.body.textContent,
+ '<p xmlns="http://www.w3.org/1999/xhtml">This is a paragraph.</p>',
+ "Correct source for application/xhtml+xml");
+ });
+ gBrowser.removeTab(viewSourceTab);
+});
+
diff --git a/toolkit/components/viewsource/test/browser/browser_contextmenu.js b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
new file mode 100644
index 0000000000..72b8a40be0
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var source = "data:text/html,text<link%20href='http://example.com/'%20/>more%20text<a%20href='mailto:abc@def.ghi'>email</a>";
+var gViewSourceWindow, gContextMenu, gCopyLinkMenuItem, gCopyEmailMenuItem;
+
+var expectedData = [];
+
+add_task(function *() {
+ // Full source in view source window
+ let newWindow = yield loadViewSourceWindow(source);
+ yield SimpleTest.promiseFocus(newWindow);
+
+ yield* onViewSourceWindowOpen(newWindow, false);
+
+ let contextMenu = gViewSourceWindow.document.getElementById("viewSourceContextMenu");
+
+ for (let test of expectedData) {
+ yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
+ }
+
+ yield new Promise(resolve => {
+ closeViewSourceWindow(newWindow, resolve);
+ });
+
+ // Selection source in view source tab
+ expectedData = [];
+ let newTab = yield openDocumentSelect(source, "body");
+ yield* onViewSourceWindowOpen(window, true);
+
+ contextMenu = document.getElementById("contentAreaContextMenu");
+
+ for (let test of expectedData) {
+ yield* checkMenuItems(contextMenu, true, test[0], test[1], test[2], test[3]);
+ }
+
+ gBrowser.removeTab(newTab);
+
+ // Selection source in view source window
+ yield pushPrefs(["view_source.tab", false]);
+
+ expectedData = [];
+ newWindow = yield openDocumentSelect(source, "body");
+ yield SimpleTest.promiseFocus(newWindow);
+
+ yield* onViewSourceWindowOpen(newWindow, false);
+
+ contextMenu = newWindow.document.getElementById("viewSourceContextMenu");
+
+ for (let test of expectedData) {
+ yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
+ }
+
+ yield new Promise(resolve => {
+ closeViewSourceWindow(newWindow, resolve);
+ });
+});
+
+function* onViewSourceWindowOpen(aWindow, aIsTab) {
+ gViewSourceWindow = aWindow;
+
+ gCopyLinkMenuItem = aWindow.document.getElementById(aIsTab ? "context-copylink" : "context-copyLink");
+ gCopyEmailMenuItem = aWindow.document.getElementById(aIsTab ? "context-copyemail" : "context-copyEmail");
+
+ let browser = aIsTab ? gBrowser.selectedBrowser : gViewSourceWindow.gBrowser;
+ yield ContentTask.spawn(browser, null, function* (arg) {
+ let tags = content.document.querySelectorAll("a[href]");
+ Assert.equal(tags[0].href, "view-source:http://example.com/", "Link has correct href");
+ Assert.equal(tags[1].href, "mailto:abc@def.ghi", "Link has correct href");
+ });
+
+ expectedData.push(["a[href]", true, false, "http://example.com/"]);
+ expectedData.push(["a[href^=mailto]", false, true, "abc@def.ghi"]);
+ expectedData.push(["span", false, false, null]);
+}
+
+function* checkMenuItems(contextMenu, isTab, selector, copyLinkExpected, copyEmailExpected, expectedClipboardContent) {
+
+ let browser = isTab ? gBrowser.selectedBrowser : gViewSourceWindow.gBrowser;
+ yield ContentTask.spawn(browser, { selector: selector }, function* (arg) {
+ content.document.querySelector(arg.selector).scrollIntoView();
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter(selector,
+ { type: "contextmenu", button: 2}, browser);
+ yield popupShownPromise;
+
+ is(gCopyLinkMenuItem.hidden, !copyLinkExpected, "Copy link menuitem is " + (copyLinkExpected ? "not hidden" : "hidden"));
+ is(gCopyEmailMenuItem.hidden, !copyEmailExpected, "Copy email menuitem is " + (copyEmailExpected ? "not hidden" : "hidden"));
+
+ if (copyLinkExpected || copyEmailExpected) {
+ yield new Promise((resolve, reject) => {
+ waitForClipboard(expectedClipboardContent, function() {
+ if (copyLinkExpected)
+ gCopyLinkMenuItem.click();
+ else
+ gCopyEmailMenuItem.click();
+ }, resolve, reject);
+ });
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ yield popupHiddenPromise;
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_gotoline.js b/toolkit/components/viewsource/test/browser/browser_gotoline.js
new file mode 100644
index 0000000000..5bb45f9caf
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_gotoline.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+var content = "line 1\nline 2\nline 3";
+
+add_task(function*() {
+ // First test with text with the text/html mimetype.
+ let win = yield loadViewSourceWindow("data:text/html," + encodeURIComponent(content));
+ yield checkViewSource(win);
+ yield BrowserTestUtils.closeWindow(win);
+
+ win = yield loadViewSourceWindow("data:text/plain," + encodeURIComponent(content));
+ yield checkViewSource(win);
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+var checkViewSource = Task.async(function* (aWindow) {
+ is(aWindow.gBrowser.contentDocument.body.textContent, content, "Correct content loaded");
+ let statusPanel = aWindow.document.getElementById("statusbar-line-col");
+ is(statusPanel.getAttribute("label"), "", "Correct status bar text");
+
+ for (let i = 1; i <= 3; i++) {
+ aWindow.viewSourceChrome.goToLine(i);
+ yield ContentTask.spawn(aWindow.gBrowser, i, function*(i) {
+ let selection = content.getSelection();
+ Assert.equal(selection.toString(), "line " + i, "Correct text selected");
+ });
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return (statusPanel.getAttribute("label") == "Line " + i + ", Col 1");
+ }, "Correct status bar text");
+ }
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_srcdoc.js b/toolkit/components/viewsource/test/browser/browser_srcdoc.js
new file mode 100644
index 0000000000..542741ffc3
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_srcdoc.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const frameSource = `<a href="about:mozilla">good</a>`;
+const source = `<html><iframe srcdoc='${frameSource}' id="f"></iframe></html>`;
+
+add_task(function*() {
+ let url = `data:text/html,${source}`;
+ yield BrowserTestUtils.withNewTab({ gBrowser, url }, checkFrameSource);
+});
+
+function* checkFrameSource() {
+ let sourceTab = yield openViewFrameSourceTab("#f");
+ registerCleanupFunction(function() {
+ gBrowser.removeTab(sourceTab);
+ });
+
+ yield waitForSourceLoaded(sourceTab);
+
+ let browser = gBrowser.selectedBrowser;
+ let textContent = yield ContentTask.spawn(browser, {}, function*() {
+ return content.document.body.textContent;
+ });
+ is(textContent, frameSource, "Correct content loaded");
+ let id = yield ContentTask.spawn(browser, {}, function*() {
+ return content.document.body.id;
+ });
+ is(id, "viewsource", "View source mode enabled")
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
new file mode 100644
index 0000000000..7361a70a5a
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var plaintextURL = "data:text/plain,hello+world";
+var htmlURL = "about:mozilla";
+
+add_task(function* setup() {
+ registerCleanupFunction(function() {
+ SpecialPowers.clearUserPref("view_source.tab_size");
+ SpecialPowers.clearUserPref("view_source.wrap_long_lines");
+ SpecialPowers.clearUserPref("view_source.syntax_highlight");
+ });
+});
+
+add_task(function*() {
+ yield exercisePrefs(plaintextURL, false);
+ yield exercisePrefs(htmlURL, true);
+});
+
+var exercisePrefs = Task.async(function* (source, highlightable) {
+ let win = yield loadViewSourceWindow(source);
+ let wrapMenuItem = win.document.getElementById("menu_wrapLongLines");
+ let syntaxMenuItem = win.document.getElementById("menu_highlightSyntax");
+
+ // Strip checked="false" attributes, since we're not interested in them.
+ if (wrapMenuItem.getAttribute("checked") == "false") {
+ wrapMenuItem.removeAttribute("checked");
+ }
+ if (syntaxMenuItem.getAttribute("checked") == "false") {
+ syntaxMenuItem.removeAttribute("checked");
+ }
+
+ // Test the default states of these menu items.
+ is(wrapMenuItem.hasAttribute("checked"), false,
+ "Wrap menu item not checked by default");
+ is(syntaxMenuItem.hasAttribute("checked"), true,
+ "Syntax menu item checked by default");
+
+ yield checkStyle(win, "-moz-tab-size", 4);
+ yield checkStyle(win, "white-space", "pre");
+
+ // Next, test that the Wrap Long Lines menu item works.
+ let prefReady = waitForPrefChange("view_source.wrap_long_lines");
+ simulateClick(wrapMenuItem);
+ is(wrapMenuItem.hasAttribute("checked"), true, "Wrap menu item checked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.wrap_long_lines"), true, "Wrap pref set");
+
+ yield checkStyle(win, "white-space", "pre-wrap");
+
+ prefReady = waitForPrefChange("view_source.wrap_long_lines");
+ simulateClick(wrapMenuItem);
+ is(wrapMenuItem.hasAttribute("checked"), false, "Wrap menu item unchecked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.wrap_long_lines"), false, "Wrap pref set");
+ yield checkStyle(win, "white-space", "pre");
+
+ // Check that the Syntax Highlighting menu item works.
+ prefReady = waitForPrefChange("view_source.syntax_highlight");
+ simulateClick(syntaxMenuItem);
+ is(syntaxMenuItem.hasAttribute("checked"), false, "Syntax menu item unchecked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), false, "Syntax highlighting pref set");
+ yield checkHighlight(win, false);
+
+ prefReady = waitForPrefChange("view_source.syntax_highlight");
+ simulateClick(syntaxMenuItem);
+ is(syntaxMenuItem.hasAttribute("checked"), true, "Syntax menu item checked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), true, "Syntax highlighting pref set");
+ yield checkHighlight(win, highlightable);
+ yield BrowserTestUtils.closeWindow(win);
+
+ // Open a new view-source window to check that the prefs are obeyed.
+ SpecialPowers.setIntPref("view_source.tab_size", 2);
+ SpecialPowers.setBoolPref("view_source.wrap_long_lines", true);
+ SpecialPowers.setBoolPref("view_source.syntax_highlight", false);
+
+ win = yield loadViewSourceWindow(source);
+ wrapMenuItem = win.document.getElementById("menu_wrapLongLines");
+ syntaxMenuItem = win.document.getElementById("menu_highlightSyntax");
+
+ // Strip checked="false" attributes, since we're not interested in them.
+ if (wrapMenuItem.getAttribute("checked") == "false") {
+ wrapMenuItem.removeAttribute("checked");
+ }
+ if (syntaxMenuItem.getAttribute("checked") == "false") {
+ syntaxMenuItem.removeAttribute("checked");
+ }
+
+ is(wrapMenuItem.hasAttribute("checked"), true, "Wrap menu item checked");
+ is(syntaxMenuItem.hasAttribute("checked"), false, "Syntax menu item unchecked");
+ yield checkStyle(win, "-moz-tab-size", 2);
+ yield checkStyle(win, "white-space", "pre-wrap");
+ yield checkHighlight(win, false);
+
+ SpecialPowers.clearUserPref("view_source.tab_size");
+ SpecialPowers.clearUserPref("view_source.wrap_long_lines");
+ SpecialPowers.clearUserPref("view_source.syntax_highlight");
+
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+// Simulate a menu item click, including toggling the checked state.
+// This saves us from opening the menu and trying to click on the item,
+// which doesn't work on Mac OS X.
+function simulateClick(aMenuItem) {
+ if (aMenuItem.hasAttribute("checked"))
+ aMenuItem.removeAttribute("checked");
+ else
+ aMenuItem.setAttribute("checked", "true");
+
+ aMenuItem.click();
+}
+
+var checkStyle = Task.async(function* (win, styleProperty, expected) {
+ let browser = win.gBrowser;
+ let value = yield ContentTask.spawn(browser, styleProperty, function* (styleProperty) {
+ let style = content.getComputedStyle(content.document.body, null);
+ return style.getPropertyValue(styleProperty);
+ });
+ is(value, expected, "Correct value of " + styleProperty);
+});
+
+var checkHighlight = Task.async(function* (win, expected) {
+ let browser = win.gBrowser;
+ let highlighted = yield ContentTask.spawn(browser, {}, function* () {
+ let spans = content.document.getElementsByTagName("span");
+ return Array.some(spans, (span) => {
+ let style = content.getComputedStyle(span, null);
+ return style.getPropertyValue("color") !== "rgb(0, 0, 0)";
+ });
+ });
+ is(highlighted, expected, "Syntax highlighting " + (expected ? "on" : "off"));
+});
diff --git a/toolkit/components/viewsource/test/browser/file_bug464222.html b/toolkit/components/viewsource/test/browser/file_bug464222.html
new file mode 100644
index 0000000000..4f2a43f0d4
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/file_bug464222.html
@@ -0,0 +1 @@
+<a href="file_bug464222.html">I'm a link</a> \ No newline at end of file
diff --git a/toolkit/components/viewsource/test/browser/head.js b/toolkit/components/viewsource/test/browser/head.js
new file mode 100644
index 0000000000..bb46369b04
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/head.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const WINDOW_TYPE = "navigator:view-source";
+
+function openViewSourceWindow(aURI, aCallback) {
+ let viewSourceWindow = openDialog("chrome://global/content/viewSource.xul", null, null, aURI);
+ viewSourceWindow.addEventListener("pageshow", function pageShowHandler(event) {
+ // Wait for the inner window to load, not viewSourceWindow.
+ if (event.target.location == "view-source:" + aURI) {
+ info("View source window opened: " + event.target.location);
+ viewSourceWindow.removeEventListener("pageshow", pageShowHandler, false);
+ aCallback(viewSourceWindow);
+ }
+ }, false);
+}
+
+function loadViewSourceWindow(URL) {
+ return new Promise((resolve) => {
+ openViewSourceWindow(URL, resolve);
+ })
+}
+
+function closeViewSourceWindow(aWindow, aCallback) {
+ Services.wm.addListener({
+ onCloseWindow: function() {
+ Services.wm.removeListener(this);
+ executeSoon(aCallback);
+ }
+ });
+ aWindow.close();
+}
+
+function testViewSourceWindow(aURI, aTestCallback, aCloseCallback) {
+ openViewSourceWindow(aURI, function(aWindow) {
+ aTestCallback(aWindow);
+ closeViewSourceWindow(aWindow, aCloseCallback);
+ });
+}
+
+function waitForViewSourceWindow() {
+ return new Promise(resolve => {
+ let windowListener = {
+ onOpenWindow(xulWindow) {
+ let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, false);
+ if (win.document.documentElement.getAttribute("windowtype") !=
+ WINDOW_TYPE) {
+ return;
+ }
+ // Found the window
+ resolve(win);
+ Services.wm.removeListener(windowListener);
+ }, false);
+ },
+ onCloseWindow() {},
+ onWindowTitleChange() {}
+ };
+ Services.wm.addListener(windowListener);
+ });
+}
+
+/**
+ * Opens a view source tab / window for a selection (View Selection Source)
+ * within the currently selected browser in gBrowser.
+ *
+ * @param aCSSSelector - used to specify a node within the selection to
+ * view the source of. It is expected that this node is
+ * within an existing selection.
+ * @returns the new tab / window which shows the source.
+ */
+function* openViewPartialSource(aCSSSelector) {
+ let contentAreaContextMenuPopup =
+ document.getElementById("contentAreaContextMenu");
+ let popupShownPromise =
+ BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter(aCSSSelector,
+ { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ let openPromise;
+ if (Services.prefs.getBoolPref("view_source.tab")) {
+ openPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ } else {
+ openPromise = waitForViewSourceWindow();
+ }
+
+ let popupHiddenPromise =
+ BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popuphidden");
+ let item = document.getElementById("context-viewpartialsource-selection");
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ yield popupHiddenPromise;
+
+ return (yield openPromise);
+}
+
+/**
+ * Opens a view source tab for a frame (View Frame Source) within the
+ * currently selected browser in gBrowser.
+ *
+ * @param aCSSSelector - used to specify the frame to view the source of.
+ * @returns the new tab which shows the source.
+ */
+function* openViewFrameSourceTab(aCSSSelector) {
+ let contentAreaContextMenuPopup =
+ document.getElementById("contentAreaContextMenu");
+ let popupShownPromise =
+ BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter(aCSSSelector,
+ { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ let frameContextMenu = document.getElementById("frame");
+ popupShownPromise =
+ BrowserTestUtils.waitForEvent(frameContextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(frameContextMenu, {});
+ yield popupShownPromise;
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
+
+ let popupHiddenPromise =
+ BrowserTestUtils.waitForEvent(frameContextMenu, "popuphidden");
+ let item = document.getElementById("context-viewframesource");
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ yield popupHiddenPromise;
+
+ return (yield newTabPromise);
+}
+
+registerCleanupFunction(function() {
+ var windows = Services.wm.getEnumerator(WINDOW_TYPE);
+ ok(!windows.hasMoreElements(), "No remaining view source windows still open");
+ while (windows.hasMoreElements())
+ windows.getNext().close();
+});
+
+/**
+ * For a given view source tab / window, wait for the source loading step to
+ * complete.
+ */
+function waitForSourceLoaded(tabOrWindow) {
+ return new Promise(resolve => {
+ let mm = tabOrWindow.messageManager ||
+ tabOrWindow.linkedBrowser.messageManager;
+ mm.addMessageListener("ViewSource:SourceLoaded", function sourceLoaded() {
+ mm.removeMessageListener("ViewSource:SourceLoaded", sourceLoaded);
+ setTimeout(resolve, 0);
+ });
+ });
+}
+
+/**
+ * Open a new document in a new tab, select part of it, and view the source of
+ * that selection. The document is not closed afterwards.
+ *
+ * @param aURI - url to load
+ * @param aCSSSelector - used to specify a node to select. All of this node's
+ * children will be selected.
+ * @returns the new tab / window which shows the source.
+ */
+function* openDocumentSelect(aURI, aCSSSelector) {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, aURI);
+ registerCleanupFunction(function() {
+ gBrowser.removeTab(tab);
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { selector: aCSSSelector }, function* (arg) {
+ let element = content.document.querySelector(arg.selector);
+ content.getSelection().selectAllChildren(element);
+ });
+
+ let tabOrWindow = yield openViewPartialSource(aCSSSelector);
+
+ // Wait until the source has been loaded.
+ yield waitForSourceLoaded(tabOrWindow);
+
+ return tabOrWindow;
+}
+
+function pushPrefs(...aPrefs) {
+ return new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve);
+ });
+}
+
+function waitForPrefChange(pref) {
+ let deferred = PromiseUtils.defer();
+ let observer = () => {
+ Preferences.ignore(pref, observer);
+ deferred.resolve();
+ };
+ Preferences.observe(pref, observer);
+ return deferred.promise;
+}
diff --git a/toolkit/components/viewsource/test/chrome.ini b/toolkit/components/viewsource/test/chrome.ini
new file mode 100644
index 0000000000..bd013ab6c9
--- /dev/null
+++ b/toolkit/components/viewsource/test/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_bug428653.html]
+support-files = file_empty.html
diff --git a/toolkit/components/viewsource/test/file_empty.html b/toolkit/components/viewsource/test/file_empty.html
new file mode 100644
index 0000000000..495c23ec8a
--- /dev/null
+++ b/toolkit/components/viewsource/test/file_empty.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><body></body></html>
diff --git a/toolkit/components/viewsource/test/test_bug428653.html b/toolkit/components/viewsource/test/test_bug428653.html
new file mode 100644
index 0000000000..b1d48bfb39
--- /dev/null
+++ b/toolkit/components/viewsource/test/test_bug428653.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=428653
+-->
+<head>
+ <title>View Source Test (bug 428653)</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+ <iframe id="content" src="http://example.org/tests/toolkit/components/viewsource/test/file_empty.html"></iframe>
+
+ <script type="application/javascript">
+ /*
+ Test that we can't call the content browser's document.open() over Xrays.
+ See the security checks in nsHTMLDocument::Open, which make sure that the
+ entry global's principal matches that of the document.
+ */
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function testDocumentOpen() {
+ var browser = document.getElementById("content");
+ ok(browser, "got browser");
+ var doc = browser.contentDocument;
+ ok(doc, "got content document");
+
+ var opened = false;
+ try {
+ doc.open("text/html", "replace");
+ opened = true;
+ } catch (e) {
+ is(e.name, "SecurityError", "Unexpected exception")
+ }
+ is(opened, false, "Shouldn't have opened document");
+
+ doc.wrappedJSObject.open("text/html", "replace");
+ ok(true, "Should be able to open document via Xray Waiver");
+
+ SimpleTest.finish();
+ });
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/workerloader/moz.build b/toolkit/components/workerloader/moz.build
new file mode 100644
index 0000000000..145f7adc05
--- /dev/null
+++ b/toolkit/components/workerloader/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/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
+EXTRA_JS_MODULES.workers += [
+ 'require.js'
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Async Tooling')
diff --git a/toolkit/components/workerloader/require.js b/toolkit/components/workerloader/require.js
new file mode 100644
index 0000000000..223132f7e9
--- /dev/null
+++ b/toolkit/components/workerloader/require.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+/**
+ * Implementation of a CommonJS module loader for workers.
+ *
+ * Use:
+ * // in the .js file loaded by the constructor of the worker
+ * importScripts("resource://gre/modules/workers/require.js");
+ * let module = require("resource://gre/modules/worker/myModule.js");
+ *
+ * // in myModule.js
+ * // Load dependencies
+ * let SimpleTest = require("resource://gre/modules/workers/SimpleTest.js");
+ * let Logger = require("resource://gre/modules/workers/Logger.js");
+ *
+ * // Define things that will not be exported
+ * let someValue = // ...
+ *
+ * // Export symbols
+ * exports.foo = // ...
+ * exports.bar = // ...
+ *
+ *
+ * Note #1:
+ * Properties |fileName| and |stack| of errors triggered from a module
+ * contain file names that do not correspond to human-readable module paths.
+ * Human readers should rather use properties |moduleName| and |moduleStack|.
+ *
+ * Note #2:
+ * The current version of |require()| only accepts absolute URIs.
+ *
+ * Note #3:
+ * By opposition to some other module loader implementations, this module
+ * loader does not enforce separation of global objects. Consequently, if
+ * a module modifies a global object (e.g. |String.prototype|), all other
+ * modules in the same worker may be affected.
+ */
+
+
+(function(exports) {
+ "use strict";
+
+ if (exports.require) {
+ // Avoid double-imports
+ return;
+ }
+
+ // Simple implementation of |require|
+ let require = (function() {
+
+ /**
+ * Mapping from module paths to module exports.
+ *
+ * @keys {string} The absolute path to a module.
+ * @values {object} The |exports| objects for that module.
+ */
+ let modules = new Map();
+
+ /**
+ * A human-readable version of |stack|.
+ *
+ * @type {string}
+ */
+ Object.defineProperty(Error.prototype, "moduleStack",
+ {
+ get: function() {
+ return this.stack;
+ }
+ });
+ /**
+ * A human-readable version of |fileName|.
+ *
+ * @type {string}
+ */
+ Object.defineProperty(Error.prototype, "moduleName",
+ {
+ get: function() {
+ let match = this.stack.match(/\@(.*):.*:/);
+ if (match) {
+ return match[1];
+ }
+ return "(unknown module)";
+ }
+ });
+
+ /**
+ * Import a module
+ *
+ * @param {string} path The path to the module.
+ * @return {*} An object containing the properties exported by the module.
+ */
+ return function require(path) {
+ if (typeof path != "string" || path.indexOf("://") == -1) {
+ throw new TypeError("The argument to require() must be a string uri, got " + path);
+ }
+ // Automatically add ".js" if there is no extension
+ let uri;
+ if (path.lastIndexOf(".") <= path.lastIndexOf("/")) {
+ uri = path + ".js";
+ } else {
+ uri = path;
+ }
+
+ // Exports provided by the module
+ let exports = Object.create(null);
+
+ // Identification of the module
+ let module = {
+ id: path,
+ uri: uri,
+ exports: exports
+ };
+
+ // Make module available immediately
+ // (necessary in case of circular dependencies)
+ if (modules.has(path)) {
+ return modules.get(path).exports;
+ }
+ modules.set(path, module);
+
+ try {
+ // Load source of module, synchronously
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, false);
+ xhr.responseType = "text";
+ xhr.send();
+
+
+ let source = xhr.responseText;
+ if (source == "") {
+ // There doesn't seem to be a better way to detect that the file couldn't be found
+ throw new Error("Could not find module " + path);
+ }
+ let code = new Function("exports", "require", "module",
+ source + "\n//# sourceURL=" + uri + "\n"
+ );
+ code(exports, require, module);
+ } catch (ex) {
+ // Module loading has failed, exports should not be made available
+ // after all.
+ modules.delete(path);
+ throw ex;
+ }
+
+ Object.freeze(module.exports);
+ Object.freeze(module);
+ return module.exports;
+ };
+ })();
+
+ Object.freeze(require);
+
+ Object.defineProperty(exports, "require", {
+ value: require,
+ enumerable: true,
+ configurable: false
+ });
+})(this);
diff --git a/toolkit/components/workerloader/tests/.eslintrc.js b/toolkit/components/workerloader/tests/.eslintrc.js
new file mode 100644
index 0000000000..2c669d844e
--- /dev/null
+++ b/toolkit/components/workerloader/tests/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/workerloader/tests/chrome.ini b/toolkit/components/workerloader/tests/chrome.ini
new file mode 100644
index 0000000000..c83a0b0813
--- /dev/null
+++ b/toolkit/components/workerloader/tests/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+support-files =
+ moduleA-depends.js
+ moduleB-dependency.js
+ moduleC-circular.js
+ moduleD-circular.js
+ moduleE-throws-during-require.js
+ moduleF-syntax-error.js
+ moduleG-throws-later.js
+ moduleH-module-dot-exports.js
+ utils_mainthread.js
+ utils_worker.js
+ worker_test_loading.js
+
+[test_loading.xul]
diff --git a/toolkit/components/workerloader/tests/moduleA-depends.js b/toolkit/components/workerloader/tests/moduleA-depends.js
new file mode 100644
index 0000000000..0e1cc7c8b3
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleA-depends.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// A trivial module that depends on an equally trivial module
+var B = require("chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/moduleB-dependency.js");
+
+// Ensure that the initial set of exports is empty
+if (Object.keys(exports).length) {
+ throw new Error("exports should be empty, initially");
+}
+
+// Export some values
+exports.A = true;
+exports.importedFoo = B.foo;
diff --git a/toolkit/components/workerloader/tests/moduleB-dependency.js b/toolkit/components/workerloader/tests/moduleB-dependency.js
new file mode 100644
index 0000000000..5c9831fc3a
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleB-dependency.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+exports.B = true;
+exports.foo = "foo";
+
+// Side-effect to detect if we attempt to re-execute this module.
+if ("loadedB" in self) {
+ throw new Error("B has been evaluted twice");
+}
+self.loadedB = true;
diff --git a/toolkit/components/workerloader/tests/moduleC-circular.js b/toolkit/components/workerloader/tests/moduleC-circular.js
new file mode 100644
index 0000000000..5dc14259a4
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleC-circular.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Module C and module D have circular dependencies.
+// This should not prevent from loading them.
+
+// This value is set before any circular dependency, it should be visible
+// in D.
+exports.enteredC = true;
+
+var D = require("chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/moduleD-circular.js");
+
+// The following values are set after importing D.
+// copiedFromD.copiedFromC should have only one field |enteredC|
+exports.copiedFromD = JSON.parse(JSON.stringify(D));
+// exportedFromD.copiedFromC should have all the fields defined in |exports|
+exports.exportedFromD = D;
+exports.finishedC = true;
diff --git a/toolkit/components/workerloader/tests/moduleD-circular.js b/toolkit/components/workerloader/tests/moduleD-circular.js
new file mode 100644
index 0000000000..d77bdc74db
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleD-circular.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Module C and module D have circular dependencies.
+// This should not prevent from loading them.
+
+exports.enteredD = true;
+var C = require("chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/moduleC-circular.js");
+exports.copiedFromC = JSON.parse(JSON.stringify(C));
+exports.exportedFromC = C;
+exports.finishedD = true;
diff --git a/toolkit/components/workerloader/tests/moduleE-throws-during-require.js b/toolkit/components/workerloader/tests/moduleE-throws-during-require.js
new file mode 100644
index 0000000000..b0be0449f9
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleE-throws-during-require.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Skip a few lines
+// 5
+// 6
+// 7
+// 8
+// 9
+throw new Error("Let's see if this error is obtained with the right origin");
diff --git a/toolkit/components/workerloader/tests/moduleF-syntax-error.js b/toolkit/components/workerloader/tests/moduleF-syntax-error.js
new file mode 100644
index 0000000000..c03fa32f8a
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleF-syntax-error.js
@@ -0,0 +1,6 @@
+<!--
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<?xml version="1.0" encoding="UTF-8" ?>
+<foo>Anything that doesn't parse as JavaScript</foo>
diff --git a/toolkit/components/workerloader/tests/moduleG-throws-later.js b/toolkit/components/workerloader/tests/moduleG-throws-later.js
new file mode 100644
index 0000000000..8a24bc7e46
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleG-throws-later.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Skip a few lines
+// 5
+// 6
+// 7
+// 8
+// 9
+exports.doThrow = function doThrow() {
+ Array.prototype.sort.apply("foo"); // This will raise a native TypeError
+};
diff --git a/toolkit/components/workerloader/tests/moduleH-module-dot-exports.js b/toolkit/components/workerloader/tests/moduleH-module-dot-exports.js
new file mode 100644
index 0000000000..a6b93bbccb
--- /dev/null
+++ b/toolkit/components/workerloader/tests/moduleH-module-dot-exports.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This should be overwritten by module.exports
+exports.key = "wrong value";
+
+module.exports = {
+ key: "value"
+};
+
+// This should also be overwritten by module.exports
+exports.key = "another wrong value";
diff --git a/toolkit/components/workerloader/tests/test_loading.xul b/toolkit/components/workerloader/tests/test_loading.xul
new file mode 100644
index 0000000000..2744270e14
--- /dev/null
+++ b/toolkit/components/workerloader/tests/test_loading.xul
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing the worker loader"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="utils_mainthread.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+let main = this;
+
+function test() {
+ info("Starting test " + document.uri);
+
+ worker = new ChromeWorker("worker_test_loading.js");
+ SimpleTest.waitForExplicitFinish();
+ info("Chrome worker created");
+ worker_handler(worker);
+ worker.postMessage(document.uri);
+ ok(true, "Test in progress");
+};
+]]>
+ </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>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/workerloader/tests/utils_mainthread.js b/toolkit/components/workerloader/tests/utils_mainthread.js
new file mode 100644
index 0000000000..148591c3df
--- /dev/null
+++ b/toolkit/components/workerloader/tests/utils_mainthread.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function worker_handler(worker) {
+ worker.onerror = function(error) {
+ error.preventDefault();
+ ok(false, "error "+ error.message);
+ };
+ worker.onmessage = function(msg) {
+// ok(true, "MAIN: onmessage " + JSON.stringify(msg.data));
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile.xul: wrong message " + JSON.stringify(msg.data));
+ return;
+ }
+ };
+}
diff --git a/toolkit/components/workerloader/tests/utils_worker.js b/toolkit/components/workerloader/tests/utils_worker.js
new file mode 100644
index 0000000000..da82d4b0ab
--- /dev/null
+++ b/toolkit/components/workerloader/tests/utils_worker.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function log(text) {
+ dump("WORKER " + text + "\n");
+}
+
+function send(message) {
+ self.postMessage(message);
+}
+
+function finish() {
+ send({kind: "finish"});
+}
+
+function ok(condition, description) {
+ send({kind: "ok", condition: !!condition, description: "" + description});
+}
+
+function is(a, b, description) {
+ let outcome = a == b; // Need to decide outcome here, as not everything can be serialized
+ send({kind: "is", outcome: outcome, description: "" + description, a: "" + a, b: "" + b});
+}
+
+function isnot(a, b, description) {
+ let outcome = a != b; // Need to decide outcome here, as not everything can be serialized
+ send({kind: "isnot", outcome: outcome, description: "" + description, a: "" + a, b: "" + b});
+}
+
+function info(description) {
+ send({kind: "info", description: "" + description});
+}
diff --git a/toolkit/components/workerloader/tests/worker_handler.js b/toolkit/components/workerloader/tests/worker_handler.js
new file mode 100644
index 0000000000..b09b8c34ca
--- /dev/null
+++ b/toolkit/components/workerloader/tests/worker_handler.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function worker_handler(worker) {
+ worker.onerror = function(error) {
+ error.preventDefault();
+ ok(false, "error "+error);
+ }
+ worker.onmessage = function(msg) {
+ ok(true, "MAIN: onmessage " + JSON.stringify(msg.data));
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile.xul: wrong message " + JSON.stringify(msg.data));
+ return;
+ }
+ };
+}
diff --git a/toolkit/components/workerloader/tests/worker_test_loading.js b/toolkit/components/workerloader/tests/worker_test_loading.js
new file mode 100644
index 0000000000..40702e4e1f
--- /dev/null
+++ b/toolkit/components/workerloader/tests/worker_test_loading.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+importScripts("utils_worker.js"); // Test suite code
+info("Test suite configured");
+
+importScripts("resource://gre/modules/workers/require.js");
+info("Loader imported");
+
+var PATH = "chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/";
+var tests = [];
+var add_test = function(test) {
+ tests.push(test);
+};
+
+add_test(function test_setup() {
+ ok(typeof require != "undefined", "Function |require| is defined");
+});
+
+// Test simple loading (moduleA-depends.js requires moduleB-dependency.js)
+add_test(function test_load() {
+ let A = require(PATH + "moduleA-depends.js");
+ ok(true, "Opened module A");
+
+ is(A.A, true, "Module A exported value A");
+ ok(!("B" in A), "Module A did not export value B");
+ is(A.importedFoo, "foo", "Module A re-exported B.foo");
+
+ // re-evaluating moduleB-dependency.js would cause an error, but re-requiring it shouldn't
+ let B = require(PATH + "moduleB-dependency.js");
+ ok(true, "Managed to re-require module B");
+ is(B.B, true, "Module B exported value B");
+ is(B.foo, "foo", "Module B exported value foo");
+});
+
+// Test simple circular loading (moduleC-circular.js and moduleD-circular.js require each other)
+add_test(function test_circular() {
+ let C = require(PATH + "moduleC-circular.js");
+ ok(true, "Loaded circular modules C and D");
+ is(C.copiedFromD.copiedFromC.enteredC, true, "Properties exported by C before requiring D can be seen by D immediately");
+
+ let D = require(PATH + "moduleD-circular.js");
+ is(D.exportedFromC.finishedC, true, "Properties exported by C after requiring D can be seen by D eventually");
+});
+
+// Testing error cases
+add_test(function test_exceptions() {
+ let should_throw = function(f) {
+ try {
+ f();
+ return null;
+ } catch (ex) {
+ return ex;
+ }
+ };
+
+ let exn = should_throw(() => require(PATH + "this module doesn't exist"));
+ ok(!!exn, "Attempting to load a module that doesn't exist raises an error");
+
+ exn = should_throw(() => require(PATH + "moduleE-throws-during-require.js"));
+ ok(!!exn, "Attempting to load a module that throws at toplevel raises an error");
+ is(exn.moduleName, PATH + "moduleE-throws-during-require.js",
+ "moduleName is correct");
+ isnot(exn.moduleStack.indexOf("moduleE-throws-during-require.js"), -1,
+ "moduleStack contains the name of the module");
+ is(exn.lineNumber, 10, "The error comes with the right line number");
+
+ exn = should_throw(() => require(PATH + "moduleF-syntaxerror.xml"));
+ ok(!!exn, "Attempting to load a non-well formatted module raises an error");
+
+ exn = should_throw(() => require(PATH + "moduleG-throws-later.js").doThrow());
+ ok(!!exn, "G.doThrow() has raised an error");
+ info(exn);
+ ok(exn.toString().startsWith("TypeError"), "The exception is a TypeError.");
+ is(exn.moduleName, PATH + "moduleG-throws-later.js", "The name of the module is correct");
+ isnot(exn.moduleStack.indexOf("moduleG-throws-later.js"), -1,
+ "The name of the right file appears somewhere in the stack");
+ is(exn.lineNumber, 11, "The error comes with the right line number");
+});
+
+function get_exn(f) {
+ try {
+ f();
+ return undefined;
+ } catch (ex) {
+ return ex;
+ }
+}
+
+// Test module.exports
+add_test(function test_module_dot_exports() {
+ let H = require(PATH + "moduleH-module-dot-exports.js");
+ is(H.key, "value", "module.exports worked");
+ let H2 = require(PATH + "moduleH-module-dot-exports.js");
+ is(H2.key, "value", "module.exports returned the same key");
+ ok(H2 === H, "module.exports returned the same module the second time");
+ let exn = get_exn(() => H.key = "this should not be accepted");
+ ok(exn instanceof TypeError, "Cannot alter value in module.exports after export");
+ exn = get_exn(() => H.key2 = "this should not be accepted, either");
+ ok(exn instanceof TypeError, "Cannot add value to module.exports after export");
+});
+
+self.onmessage = function(message) {
+ for (let test of tests) {
+ info("Entering " + test.name);
+ try {
+ test();
+ } catch (ex) {
+ ok(false, "Test " + test.name + " failed");
+ info(ex);
+ info(ex.stack);
+ }
+ info("Leaving " + test.name);
+ }
+ finish();
+};
+
+
+
diff --git a/toolkit/components/xulstore/XULStore.js b/toolkit/components/xulstore/XULStore.js
new file mode 100644
index 0000000000..c2721327c2
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.js
@@ -0,0 +1,336 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+// Enables logging and shorter save intervals.
+const debugMode = false;
+
+// Delay when a change is made to when the file is saved.
+// 30 seconds normally, or 3 seconds for testing
+const WRITE_DELAY_MS = (debugMode ? 3 : 30) * 1000;
+
+const XULSTORE_CONTRACTID = "@mozilla.org/xul/xulstore;1";
+const XULSTORE_CID = Components.ID("{6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}");
+const STOREDB_FILENAME = "xulstore.json";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+function XULStore() {
+ if (!Services.appinfo.inSafeMode)
+ this.load();
+}
+
+XULStore.prototype = {
+ classID: XULSTORE_CID,
+ classInfo: XPCOMUtils.generateCI({classID: XULSTORE_CID,
+ contractID: XULSTORE_CONTRACTID,
+ classDescription: "XULStore",
+ interfaces: [Ci.nsIXULStore]}),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIXULStore,
+ Ci.nsISupportsWeakReference]),
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(XULStore),
+
+ /* ---------- private members ---------- */
+
+ /*
+ * The format of _data is _data[docuri][elementid][attribute]. For example:
+ * {
+ * "chrome://blah/foo.xul" : {
+ * "main-window" : { aaa : 1, bbb : "c" },
+ * "barColumn" : { ddd : 9, eee : "f" },
+ * },
+ *
+ * "chrome://foopy/b.xul" : { ... },
+ * ...
+ * }
+ */
+ _data: {},
+ _storeFile: null,
+ _needsSaving: false,
+ _saveAllowed: true,
+ _writeTimer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
+
+ load: function () {
+ Services.obs.addObserver(this, "profile-before-change", true);
+
+ this._storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ this._storeFile.append(STOREDB_FILENAME);
+
+ if (!this._storeFile.exists()) {
+ this.import();
+ } else {
+ this.readFile();
+ }
+ },
+
+ observe: function(subject, topic, data) {
+ this.writeFile();
+ if (topic == "profile-before-change") {
+ this._saveAllowed = false;
+ }
+ },
+
+ /*
+ * Internal function for logging debug messages to the Error Console window
+ */
+ log: function (message) {
+ if (!debugMode)
+ return;
+ dump("XULStore: " + message + "\n");
+ Services.console.logStringMessage("XULStore: " + message);
+ },
+
+ import: function() {
+ let localStoreFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+
+ localStoreFile.append("localstore.rdf");
+ if (!localStoreFile.exists()) {
+ return;
+ }
+
+ const RDF = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
+ const persistKey = RDF.GetResource("http://home.netscape.com/NC-rdf#persist");
+
+ this.log("Import localstore from " + localStoreFile.path);
+
+ let localStoreURI = Services.io.newFileURI(localStoreFile).spec;
+ let localStore = RDF.GetDataSourceBlocking(localStoreURI);
+ let resources = localStore.GetAllResources();
+
+ while (resources.hasMoreElements()) {
+ let resource = resources.getNext().QueryInterface(Ci.nsIRDFResource);
+ let uri;
+
+ try {
+ uri = NetUtil.newURI(resource.ValueUTF8);
+ } catch (ex) {
+ continue; // skip invalid uris
+ }
+
+ // If this has a ref, then this is an attribute reference. Otherwise,
+ // this is a document reference.
+ if (!uri.hasRef)
+ continue;
+
+ // Verify that there the persist key is connected up.
+ let docURI = uri.specIgnoringRef;
+
+ if (!localStore.HasAssertion(RDF.GetResource(docURI), persistKey, resource, true))
+ continue;
+
+ let id = uri.ref;
+ let attrs = localStore.ArcLabelsOut(resource);
+
+ while (attrs.hasMoreElements()) {
+ let attr = attrs.getNext().QueryInterface(Ci.nsIRDFResource);
+ let value = localStore.GetTarget(resource, attr, true);
+
+ if (value instanceof Ci.nsIRDFLiteral) {
+ this.setValue(docURI, id, attr.ValueUTF8, value.Value);
+ }
+ }
+ }
+ },
+
+ readFile: function() {
+ const MODE_RDONLY = 0x01;
+ const FILE_PERMS = 0o600;
+
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+ try {
+ stream.init(this._storeFile, MODE_RDONLY, FILE_PERMS, 0);
+ this._data = json.decodeFromStream(stream, stream.available());
+ } catch (e) {
+ this.log("Error reading JSON: " + e);
+ // Ignore problem, we'll just continue on with an empty dataset.
+ } finally {
+ stream.close();
+ }
+ },
+
+ writeFile: Task.async(function* () {
+ if (!this._needsSaving)
+ return;
+
+ this._needsSaving = false;
+
+ this.log("Writing to xulstore.json");
+
+ try {
+ let data = JSON.stringify(this._data);
+ let encoder = new TextEncoder();
+
+ data = encoder.encode(data);
+ yield OS.File.writeAtomic(this._storeFile.path, data,
+ { tmpPath: this._storeFile.path + ".tmp" });
+ } catch (e) {
+ this.log("Failed to write xulstore.json: " + e);
+ throw e;
+ }
+ }),
+
+ markAsChanged: function() {
+ if (this._needsSaving || !this._storeFile)
+ return;
+
+ // Don't write the file more than once every 30 seconds.
+ this._needsSaving = true;
+ this._writeTimer.init(this, WRITE_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /* ---------- interface implementation ---------- */
+
+ setValue: function (docURI, id, attr, value) {
+ this.log("Saving " + attr + "=" + value + " for id=" + id + ", doc=" + docURI);
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
+ return;
+ }
+
+ // bug 319846 -- don't save really long attributes or values.
+ if (id.length > 512 || attr.length > 512) {
+ throw Components.Exception("id or attribute name too long", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (value.length > 4096) {
+ Services.console.logStringMessage("XULStore: Warning, truncating long attribute value")
+ value = value.substr(0, 4096);
+ }
+
+ let obj = this._data;
+ if (!(docURI in obj)) {
+ obj[docURI] = {};
+ }
+ obj = obj[docURI];
+ if (!(id in obj)) {
+ obj[id] = {};
+ }
+ obj = obj[id];
+
+ // Don't set the value if it is already set to avoid saving the file.
+ if (attr in obj && obj[attr] == value)
+ return;
+
+ obj[attr] = value; // IE, this._data[docURI][id][attr] = value;
+
+ this.markAsChanged();
+ },
+
+ hasValue: function (docURI, id, attr) {
+ this.log("has store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs) {
+ return attr in attrs;
+ }
+ }
+
+ return false;
+ },
+
+ getValue: function (docURI, id, attr) {
+ this.log("get store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs) {
+ return attrs[attr] || "";
+ }
+ }
+
+ return "";
+ },
+
+ removeValue: function (docURI, id, attr) {
+ this.log("remove store value for id=" + id + ", attr=" + attr + ", doc=" + docURI);
+
+ if (!this._saveAllowed) {
+ Services.console.logStringMessage("XULStore: Changes after profile-before-change are ignored!");
+ return;
+ }
+
+ let ids = this._data[docURI];
+ if (ids) {
+ let attrs = ids[id];
+ if (attrs && attr in attrs) {
+ delete attrs[attr];
+
+ if (Object.getOwnPropertyNames(attrs).length == 0) {
+ delete ids[id];
+
+ if (Object.getOwnPropertyNames(ids).length == 0) {
+ delete this._data[docURI];
+ }
+ }
+
+ this.markAsChanged();
+ }
+ }
+ },
+
+ getIDsEnumerator: function (docURI) {
+ this.log("Getting ID enumerator for doc=" + docURI);
+
+ if (!(docURI in this._data))
+ return new nsStringEnumerator([]);
+
+ let result = [];
+ let ids = this._data[docURI];
+ if (ids) {
+ for (let id in this._data[docURI]) {
+ result.push(id);
+ }
+ }
+
+ return new nsStringEnumerator(result);
+ },
+
+ getAttributeEnumerator: function (docURI, id) {
+ this.log("Getting attribute enumerator for id=" + id + ", doc=" + docURI);
+
+ if (!(docURI in this._data) || !(id in this._data[docURI]))
+ return new nsStringEnumerator([]);
+
+ let attrs = [];
+ for (let attr in this._data[docURI][id]) {
+ attrs.push(attr);
+ }
+
+ return new nsStringEnumerator(attrs);
+ }
+};
+
+function nsStringEnumerator(items) {
+ this._items = items;
+}
+
+nsStringEnumerator.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIStringEnumerator]),
+ _nextIndex : 0,
+ hasMore: function() {
+ return this._nextIndex < this._items.length;
+ },
+ getNext : function() {
+ if (!this.hasMore())
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+ return this._items[this._nextIndex++];
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([XULStore]);
diff --git a/toolkit/components/xulstore/XULStore.manifest b/toolkit/components/xulstore/XULStore.manifest
new file mode 100644
index 0000000000..249f0c447d
--- /dev/null
+++ b/toolkit/components/xulstore/XULStore.manifest
@@ -0,0 +1,2 @@
+component {6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea} XULStore.js
+contract @mozilla.org/xul/xulstore;1 {6f46b6f4-c8b1-4bd4-a4fa-9ebbed0753ea}
diff --git a/toolkit/components/xulstore/moz.build b/toolkit/components/xulstore/moz.build
new file mode 100644
index 0000000000..30559fccb6
--- /dev/null
+++ b/toolkit/components/xulstore/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/.
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+XPIDL_SOURCES += [
+ 'nsIXULStore.idl',
+]
+
+XPIDL_MODULE = 'toolkit_xulstore'
+
+EXTRA_COMPONENTS += [
+ 'XULStore.js',
+ 'XULStore.manifest',
+]
diff --git a/toolkit/components/xulstore/nsIXULStore.idl b/toolkit/components/xulstore/nsIXULStore.idl
new file mode 100644
index 0000000000..57050b31b2
--- /dev/null
+++ b/toolkit/components/xulstore/nsIXULStore.idl
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 nsIStringEnumerator;
+
+/**
+ * The XUL store is used to store information related to a XUL document/application.
+ * Typically it is used to store the persisted state for the document, such as
+ * window location, toolbars that are open and nodes that are open and closed in a tree.
+ *
+ * The data is serialized to [profile directory]/xulstore.json
+ */
+[scriptable, uuid(987c4b35-c426-4dd7-ad49-3c9fa4c65d20)]
+
+interface nsIXULStore: nsISupports
+{
+ /**
+ * Sets a value in the store.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to store
+ * @param value - value of the attribute
+ */
+ void setValue(in AString doc, in AString id, in AString attr, in AString value);
+
+ /**
+ * Returns true if the store contains a value for attr.
+ *
+ * @param doc - URI of the document
+ * @param id - identifier of the node
+ * @param attr - attribute
+ */
+ bool hasValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Retrieves a value in the store, or an empty string if it does not exist.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to retrieve
+ *
+ * @returns the value of the attribute
+ */
+ AString getValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Removes a value in the store.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ * @param attr - attribute to remove
+ */
+ void removeValue(in AString doc, in AString id, in AString attr);
+
+ /**
+ * Iterates over all of the ids associated with a given document uri that
+ * have stored data.
+ *
+ * @param doc - document URI
+ */
+ nsIStringEnumerator getIDsEnumerator(in AString doc);
+
+ /**
+ * Iterates over all of the attributes associated with a given document uri
+ * and id that have stored data.
+ *
+ * @param doc - document URI
+ * @param id - identifier of the node
+ */
+ nsIStringEnumerator getAttributeEnumerator(in AString doc, in AString id);
+};
diff --git a/toolkit/components/xulstore/tests/chrome/.eslintrc.js b/toolkit/components/xulstore/tests/chrome/.eslintrc.js
new file mode 100644
index 0000000000..8c0f4f574c
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/xulstore/tests/chrome/animals.rdf b/toolkit/components/xulstore/tests/chrome/animals.rdf
new file mode 100644
index 0000000000..c7319e641c
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/animals.rdf
@@ -0,0 +1,142 @@
+<?xml version="1.0"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:ANIMALS="http://www.some-fictitious-zoo.com/rdf#">
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/arachnids">
+ <ANIMALS:name>Arachnids</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/arachnids/tarantula">
+ <ANIMALS:name>Tarantula</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/birds">
+ <ANIMALS:name>Birds</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/birds/emu">
+ <ANIMALS:name>Emu</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/birds/barnowl">
+ <ANIMALS:name>Barn Owl</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/birds/raven">
+ <ANIMALS:name>Raven</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/crustaceans">
+ <ANIMALS:name>Crustaceans</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/fish">
+ <ANIMALS:name>Fish</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/fish/cod">
+ <ANIMALS:name>Cod</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/fish/swordfish">
+ <ANIMALS:name>Swordfish</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/mammals">
+ <ANIMALS:name>Mammals</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/lion">
+ <ANIMALS:name>Lion</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/hippopotamus">
+ <ANIMALS:name>HIPPOPOTAMUS</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/africanelephant">
+ <ANIMALS:name>African Elephant</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/llama">
+ <ANIMALS:name>LLAMA</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/polarbear">
+ <ANIMALS:name>Polar Bear</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/aardvark">
+ <ANIMALS:name>aardvark</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/ninebandedarmadillo">
+ <ANIMALS:name>Nine-banded Armadillo</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/mammals/gorilla">
+ <ANIMALS:name>Gorilla</ANIMALS:name>
+ </RDF:Description>
+
+ <ANIMALS:Class RDF:about="http://www.some-fictitious-zoo.com/reptiles">
+ <ANIMALS:name>Reptiles</ANIMALS:name>
+ </ANIMALS:Class>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/reptiles/anaconda">
+ <ANIMALS:name>Anaconda</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Description RDF:about="http://www.some-fictitious-zoo.com/reptiles/chameleon">
+ <ANIMALS:name>Chameleon</ANIMALS:name>
+ </RDF:Description>
+
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/some-animals" ANIMALS:name="Zoo Animals">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/arachnids"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds"/>
+ </RDF:Seq>
+
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/all-animals" ANIMALS:name="Zoo Animals">
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/arachnids">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/arachnids/tarantula"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/birds">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds/emu"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds/barnowl"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/birds/raven"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/crustaceans"/>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/fish">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/fish/cod"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/fish/swordfish"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/mammals">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/lion"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/hippopotamus"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/africanelephant"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/llama"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/polarbear"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/aardvark"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/ninebandedarmadillo"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/mammals/gorilla"/>
+ </RDF:Seq>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Seq RDF:about="http://www.some-fictitious-zoo.com/reptiles">
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/reptiles/anaconda"/>
+ <RDF:li RDF:resource="http://www.some-fictitious-zoo.com/reptiles/chameleon"/>
+ </RDF:Seq>
+ </RDF:li>
+ </RDF:Seq>
+
+</RDF:RDF>
diff --git a/toolkit/components/xulstore/tests/chrome/chrome.ini b/toolkit/components/xulstore/tests/chrome/chrome.ini
new file mode 100644
index 0000000000..91efd5455a
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/chrome.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+ window_persistence.xul
+ animals.rdf
+
+[test_persistence.xul]
diff --git a/toolkit/components/xulstore/tests/chrome/test_persistence.xul b/toolkit/components/xulstore/tests/chrome/test_persistence.xul
new file mode 100644
index 0000000000..736a067edd
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/test_persistence.xul
@@ -0,0 +1,31 @@
+<?xml version="1.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="Persistence Tests"
+ onload="runTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function runTest() {
+ window.openDialog("window_persistence.xul", "_blank", "chrome", true);
+ }
+
+ function windowOpened() {
+ window.openDialog("window_persistence.xul", "_blank", "chrome", false);
+ }
+
+ function testDone() {
+ SimpleTest.finish();
+ }
+ </script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"/>
+</body>
+
+</window>
diff --git a/toolkit/components/xulstore/tests/chrome/window_persistence.xul b/toolkit/components/xulstore/tests/chrome/window_persistence.xul
new file mode 100644
index 0000000000..4d76fe11d1
--- /dev/null
+++ b/toolkit/components/xulstore/tests/chrome/window_persistence.xul
@@ -0,0 +1,98 @@
+<?xml version="1.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="Persistence Tests"
+ onload="opened()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ persist="screenX screenY width height">
+
+<button id="button1" label="Button1" persist="value"/>
+<button id="button2" label="Button2" value="Normal" persist="value"/>
+
+<tree id="tree" datasources="animals.rdf" ref="http://www.some-fictitious-zoo.com/all-animals"
+ flags="dont-build-content" width="200" height="200">
+ <treecols orient="horizontal" id="treecols">
+ <treecol id="treecol" primary="true" label="Name" flex="1"/>
+ </treecols>
+ <template id="template">
+ <treechildren>
+ <treeitem uri="rdf:*">
+ <treerow>
+ <treecell label="rdf:http://www.some-fictitious-zoo.com/rdf#name"/>
+ <treecell/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </template>
+</tree>
+
+<script>
+<![CDATA[
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+let XULStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+let URI = "chrome://mochitests/content/chrome/toolkit/components/xulstore/tests/chrome/window_persistence.xul";
+
+function opened()
+{
+ // If the data in the tree has not been loaded yet, wait a bit and try again.
+ var treeView = document.getElementById("tree").view;
+ if (treeView.rowCount != 6 && treeView.rowCount != 17) {
+ setTimeout(opened, 50);
+ return;
+ }
+
+ runTest(treeView);
+}
+
+function runTest(treeView)
+{
+ var firstRun = window.arguments[0];
+ if (firstRun) {
+ document.getElementById("button1").setAttribute("value", "Pressed");
+ document.getElementById("button2").removeAttribute("value");
+
+ document.getElementById("button2").setAttribute("foo", "bar");
+ document.persist("button2", "foo");
+ is(XULStore.getValue(URI, "button2", "foo"), "bar", "attribute persisted")
+ document.getElementById("button2").removeAttribute("foo");
+ document.persist("button2", "foo");
+ is(XULStore.hasValue(URI, "button2", "foo"), false, "attribute removed")
+
+ is(treeView.rowCount, 6, "tree rows are closed");
+ treeView.toggleOpenState(1);
+ treeView.toggleOpenState(7);
+
+ window.close();
+ window.opener.windowOpened();
+ }
+ else {
+ is(document.getElementById("button1").getAttribute("value"), "Pressed",
+ "Attribute set");
+ is(document.getElementById("button2").hasAttribute("value"), true,
+ "Attribute cleared");
+ is(document.getElementById("button2").getAttribute("value"), "",
+ "Attribute cleared");
+ is(document.getElementById("button2").hasAttribute("foo"), false,
+ "Attribute cleared");
+ is(document.getElementById("button2").getAttribute("foo"), "",
+ "Attribute cleared");
+
+ is(treeView.rowCount, 17, "tree rows are open");
+ is(treeView.isContainerOpen(0), false, "first closed row");
+ is(treeView.isContainerOpen(1), true, "first open row");
+ is(treeView.isContainerOpen(7), true, "second open row");
+
+ window.close();
+ window.opener.testDone();
+ }
+}
+
+function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); }
+
+]]></script>
+
+</window>
diff --git a/toolkit/components/xulstore/tests/xpcshell/.eslintrc.js b/toolkit/components/xulstore/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..d35787cd2c
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/xulstore/tests/xpcshell/localstore.rdf b/toolkit/components/xulstore/tests/xpcshell/localstore.rdf
new file mode 100644
index 0000000000..458eb50eac
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/localstore.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<RDF:RDF xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul#sidebar-title"
+ value="" />
+ <RDF:Description RDF:about="about:config#prefCol"
+ ordinal="1"
+ sortDirection="ascending" />
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul#addon-bar"
+ collapsed="true" />
+ <RDF:Description RDF:about="about:config">
+ <NC:persist RDF:resource="about:config#prefCol"/>
+ <NC:persist RDF:resource="about:config#lockCol"/>
+ <NC:persist RDF:resource="about:config#typeCol"/>
+ <NC:persist RDF:resource="about:config#valueCol"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="about:config#lockCol"
+ ordinal="3" />
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul">
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#main-window"/>
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#addon-bar"/>
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#sidebar-box"/>
+ <NC:persist RDF:resource="chrome://browser/content/browser.xul#sidebar-title"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="chrome://browser/content/browser.xul#main-window"
+ width="994"
+ height="768"
+ screenX="4"
+ screenY="22"
+ sizemode="normal" />
+</RDF:RDF>
diff --git a/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
new file mode 100644
index 0000000000..c3c96654b0
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/test_XULStore.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/◦
+*/
+
+"use strict"
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/osfile.jsm")
+
+var XULStore = null;
+var browserURI = "chrome://browser/content/browser.xul";
+var aboutURI = "about:config";
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+function checkValue(uri, id, attr, reference) {
+ let value = XULStore.getValue(uri, id, attr);
+ do_check_true(value === reference);
+}
+
+function checkValueExists(uri, id, attr, exists) {
+ do_check_eq(XULStore.hasValue(uri, id, attr), exists);
+}
+
+function getIDs(uri) {
+ let it = XULStore.getIDsEnumerator(uri);
+ let result = [];
+
+ while (it.hasMore()) {
+ let value = it.getNext();
+ result.push(value);
+ }
+
+ result.sort();
+ return result;
+}
+
+function getAttributes(uri, id) {
+ let it = XULStore.getAttributeEnumerator(uri, id);
+
+ let result = [];
+
+ while (it.hasMore()) {
+ let value = it.getNext();
+ result.push(value);
+ }
+
+ result.sort();
+ return result;
+}
+
+function checkArrays(a, b) {
+ a.sort();
+ b.sort();
+ do_check_true(a.toString() == b.toString());
+}
+
+function checkOldStore() {
+ checkArrays(['addon-bar', 'main-window', 'sidebar-title'], getIDs(browserURI));
+ checkArrays(['collapsed'], getAttributes(browserURI, 'addon-bar'));
+ checkArrays(['height', 'screenX', 'screenY', 'sizemode', 'width'],
+ getAttributes(browserURI, 'main-window'));
+ checkArrays(['value'], getAttributes(browserURI, 'sidebar-title'));
+
+ checkValue(browserURI, "addon-bar", "collapsed", "true");
+ checkValue(browserURI, "main-window", "width", "994");
+ checkValue(browserURI, "main-window", "height", "768");
+ checkValue(browserURI, "main-window", "screenX", "4");
+ checkValue(browserURI, "main-window", "screenY", "22");
+ checkValue(browserURI, "main-window", "sizemode", "normal");
+ checkValue(browserURI, "sidebar-title", "value", "");
+
+ checkArrays(['lockCol', 'prefCol'], getIDs(aboutURI));
+ checkArrays(['ordinal'], getAttributes(aboutURI, 'lockCol'));
+ checkArrays(['ordinal', 'sortDirection'], getAttributes(aboutURI, 'prefCol'));
+
+ checkValue(aboutURI, "prefCol", "ordinal", "1");
+ checkValue(aboutURI, "prefCol", "sortDirection", "ascending");
+ checkValue(aboutURI, "lockCol", "ordinal", "3");
+}
+
+add_task(function* testImport() {
+ let src = "localstore.rdf";
+ let dst = OS.Path.join(OS.Constants.Path.profileDir, src);
+
+ yield OS.File.copy(src, dst);
+
+ // Importing relies on XULStore not yet being loaded before this point.
+ XULStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+ checkOldStore();
+});
+
+add_task(function* testTruncation() {
+ let dos = Array(8192).join("~");
+ // Long id names should trigger an exception
+ Assert.throws(() => XULStore.setValue(browserURI, dos, "foo", "foo"), /NS_ERROR_ILLEGAL_VALUE/);
+
+ // Long attr names should trigger an exception
+ Assert.throws(() => XULStore.setValue(browserURI, "foo", dos, "foo"), /NS_ERROR_ILLEGAL_VALUE/);
+
+ // Long values should be truncated
+ XULStore.setValue(browserURI, "dos", "dos", dos);
+ dos =XULStore.getValue(browserURI, "dos", "dos");
+ do_check_true(dos.length == 4096)
+ XULStore.removeValue(browserURI, "dos", "dos")
+});
+
+add_task(function* testGetValue() {
+ // Get non-existing property
+ checkValue(browserURI, "side-window", "height", "");
+
+ // Get existing property
+ checkValue(browserURI, "main-window", "width", "994");
+});
+
+add_task(function* testHasValue() {
+ // Check non-existing property
+ checkValueExists(browserURI, "side-window", "height", false);
+
+ // Check existing property
+ checkValueExists(browserURI, "main-window", "width", true);
+});
+
+add_task(function* testSetValue() {
+ // Set new attribute
+ checkValue(browserURI, "side-bar", "width", "");
+ XULStore.setValue(browserURI, "side-bar", "width", "1000");
+ checkValue(browserURI, "side-bar", "width", "1000");
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["width"], getAttributes(browserURI, 'side-bar'));
+
+ // Modify existing property
+ checkValue(browserURI, "side-bar", "width", "1000");
+ XULStore.setValue(browserURI, "side-bar", "width", "1024");
+ checkValue(browserURI, "side-bar", "width", "1024");
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["width"], getAttributes(browserURI, 'side-bar'));
+
+ // Add another attribute
+ checkValue(browserURI, "side-bar", "height", "");
+ XULStore.setValue(browserURI, "side-bar", "height", "1000");
+ checkValue(browserURI, "side-bar", "height", "1000");
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["width", "height"], getAttributes(browserURI, 'side-bar'));
+});
+
+add_task(function* testRemoveValue() {
+ // Remove first attribute
+ checkValue(browserURI, "side-bar", "width", "1024");
+ XULStore.removeValue(browserURI, "side-bar", "width");
+ checkValue(browserURI, "side-bar", "width", "");
+ checkValueExists(browserURI, "side-bar", "width", false);
+ checkArrays(["addon-bar", "main-window", "side-bar", "sidebar-title"], getIDs(browserURI));
+ checkArrays(["height"], getAttributes(browserURI, 'side-bar'));
+
+ // Remove second attribute
+ checkValue(browserURI, "side-bar", "height", "1000");
+ XULStore.removeValue(browserURI, "side-bar", "height");
+ checkValue(browserURI, "side-bar", "height", "");
+ checkArrays(["addon-bar", "main-window", "sidebar-title"], getIDs(browserURI));
+
+ // Removing an attribute that doesn't exists shouldn't fail
+ XULStore.removeValue(browserURI, "main-window", "bar");
+
+ // Removing from an id that doesn't exists shouldn't fail
+ XULStore.removeValue(browserURI, "foo", "bar");
+
+ // Removing from a document that doesn't exists shouldn't fail
+ let nonDocURI = "chrome://example/content/other.xul";
+ XULStore.removeValue(nonDocURI, "foo", "bar");
+
+ // Remove all attributes in browserURI
+ XULStore.removeValue(browserURI, "addon-bar", "collapsed");
+ checkArrays([], getAttributes(browserURI, "addon-bar"));
+ XULStore.removeValue(browserURI, "main-window", "width");
+ XULStore.removeValue(browserURI, "main-window", "height");
+ XULStore.removeValue(browserURI, "main-window", "screenX");
+ XULStore.removeValue(browserURI, "main-window", "screenY");
+ XULStore.removeValue(browserURI, "main-window", "sizemode");
+ checkArrays([], getAttributes(browserURI, "main-window"));
+ XULStore.removeValue(browserURI, "sidebar-title", "value");
+ checkArrays([], getAttributes(browserURI, "sidebar-title"));
+ checkArrays([], getIDs(browserURI));
+
+ // Remove all attributes in aboutURI
+ XULStore.removeValue(aboutURI, "prefCol", "ordinal");
+ XULStore.removeValue(aboutURI, "prefCol", "sortDirection");
+ checkArrays([], getAttributes(aboutURI, "prefCol"));
+ XULStore.removeValue(aboutURI, "lockCol", "ordinal");
+ checkArrays([], getAttributes(aboutURI, "lockCol"));
+ checkArrays([], getIDs(aboutURI));
+});
diff --git a/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..d483dae027
--- /dev/null
+++ b/toolkit/components/xulstore/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = toolkit == 'android'
+support-files =
+ localstore.rdf
+
+[test_XULStore.js]